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.
mapPool and verifyJobs in util/check_result.ts, and the test command resolver, failure-line normalizer, sanitizeReason and the clock, duration and token formatters move to existing util modules with re-exports, behavior unchanged (STAB-1).loki start imports safe_git statically so the moat P9 allowToken caller scan passes again; the L2 destructive git registry scan no longer counts the queue rm subcommand name as a git call (STAB-1).npx --no-install argv, a new preflight test uses a here-string under the sigpipe ceiling, and router_history.test.ts carries its select marker (STAB-1).--password=x, --db-password=x, --api-key=x), separator-prefixed names (_token=x, __SECRET=x), quoted, JSON and YAML values; names that only contain a keyword mid-word (mytoken=1, --max-tokens=5) stay readable (TEN-SECURITY-25).sk_test_ and rk_ keys, and whsec_ webhook secrets (TEN-SECURITY-24).updateCommandFor returns pnpm and yarn global upgrade hints (TEN-DEPLOY-27).loki doctor --fix seeds the default provider from the first installed provider CLI, and with consent backs up an invalid .loki/config/settings.json and rewrites defaults; doctor.ts uses one shared version comparator (TEN-ADOPTION-35, TEN-UX-58, TEN-QUALITY-40).timeout_abandon, distinct from a user cancel (TEN-STABILITY-60, TEN-STABILITY-61).loki run status picks the newest run by events.jsonl mtime instead of reading every run; a failed registry version fetch is cached briefly so offline commands do not wait on the network (TEN-SPEED-24, TEN-SPEED-25).loki --version and loki version reject extra positional arguments with exit 2 (TEN-UX-53).parseCapUsd accepts .5 and 5.; OPENROUTER_API_KEY and LOKI_MODEL_API_KEY count as API billing so those runs get a cap; resolveRunCapS reads the first loki.yaml/loki.yml like the other readers; yamlKey reads flow-style maps such as budgets: { per_run: 5 } (TEN-COST-59, TEN-COST-60, TEN-COST-61, TEN-STABILITY-58).LOKI_POLICY_REQUIRE_NODE=0, and reports a check.js timeout as a timeout (TEN-GATES-31, TEN-GATES-32).loki start rejects a repeated single-value flag; loki issues run accepts only decimal digits for its limit and parallel flags; loki queue add rejects issue 0; the engine10 supervisor exits 2 when --spec or --max-cost has no value; loki undo validates the run id before the gate; loki provider show <unknown> exits 2 with the valid list; loki status --json rejects an unknown option; loki stats writes its unknown-option message to stderr; internal handoff requires an explicit consecutive-failure count (TEN-UX-56, TEN-ADOPTION-36, TEN-STABILITY-62, TEN-STABILITY-63, TEN-STABILITY-59, TEN-UX-52, TEN-UX-57, TEN-UX-54, TEN-STABILITY-65).--password=x.loki start --dry-run validates every flag, runs the preflight and prints the provider, model and cost cap it would use, then exits 0 without spending a token (TEN-UX-45).LOKI_E10_VERIFY_JOBS) and the Wall base run (LOKI_E10_BASE_JOBS) run test files with bounded concurrency, default 1; a group suite-fix unit reuses the failing-suite output integrate already captured (TEN-SPEED-17 to TEN-SPEED-20).LOKI_PRICE_TABLE=<json file> overrides or extends the built-in price table; an invalid file falls back to the built-in table (TEN-COST-54).loki cost cover all 5 providers (TEN-COST-55).__init__.py and Rust mod.rs to the package name (TEN-QUALITY-32).loki start unknown-flag errors suggest the closest valid flag, and an unknown --provider lists the valid providers (TEN-UX-46, TEN-UX-47).noImplicitReturns is enabled; handleSlackEvent narrows an unknown payload; SessionResult carries an optional stderrTail; new characterization tests for fix_units, compound_match and buildOpencodeArgv (TEN-QUALITY-33 to TEN-QUALITY-38).index.ts/index.js (TEN-QUALITY-39).usdFor clamps non-finite or negative token counts and caps cached tokens at input; mini and nano OpenAI models get their own prices instead of the full-model rate (TEN-COST-56, TEN-COST-58).loki estimate --model requires a non-empty value that is not another flag (TEN-COST-57).loki start fails with “needs a value” for an empty --provider, --session-model, --cost, --spec, --health-url, --domain, --destination or --env-file, and treats a missing PRD path with spaces as a path, not a paid brief (TEN-UX-48, TEN-UX-50, TEN-DEPLOY-26)..DS_Store or other OS metadata counts as empty (TEN-UX-51).appendRunOutcome no longer loses an outcome when two runs finish together (TEN-STABILITY-57).secure-scan.py rule 9 flags unsafe deserialization (pickle.loads, yaml.load without a safe loader, marshal.loads) and rule 10 flags SQL built by f-string, % formatting or concatenation passed to execute() in added lines (TEN-SECURITY-19, TEN-SECURITY-20).redactSecrets strips AWS ASIA keys and Azure AccountKey= strings; export_sarif flags PyPI, Docker, DigitalOcean, Google OAuth, Shopify and Postman tokens; secure-scan.py matches AWS ASIA, Azure AccountKey and Twilio SK keys (TEN-SECURITY-16, TEN-SECURITY-17, TEN-SECURITY-18).cost_claim.py refuses a proven verdict when rows lack cost_source or an arm mixes cost sources, and reports NOT PROVEN when the arms cover different task sets (TEN-COST-51, TEN-COST-52).loki rollback deploy --json prints the matched rollback command as JSON (TEN-DEPLOY-25).--json, --list and --dry-run flags of control status, trust, undo, memory list and memory lessons (TEN-ADOPTION-28 to TEN-ADOPTION-33).loki control, loki queue, loki queue run, loki undo and loki memory suggest the closest valid subcommand, flag or argument on a typo (TEN-UX-40 to TEN-UX-44).cost_claim.py no longer counts a missing, None, NaN or negative cost_usd as zero (TEN-COST-53).issues run gh calls and the verify pr gh helper (TEN-SECURITY-21, TEN-STABILITY-50, TEN-STABILITY-51, TEN-STABILITY-53, TEN-STABILITY-54, TEN-STABILITY-55).LOKI_MUTATION_PROOF_TIMEOUT_S or LOKI_E10_NOTIFY_TIMEOUT_MS falls back to the default instead of failing the verifier gate or dropping Slack events (TEN-GATES-30, TEN-STABILITY-49).recordFailures appends atomically, so concurrent runs no longer lose failure-memory lines (TEN-STABILITY-52).cost_claim.py verdicts name the loki arm’s cost_profile (lean, default, off) and refuse a loki arm that mixes profiles, so a lean-profile cost claim versus Claude Code is explicit evidence (TEN-COST-49).cost_claim.py --exclude-task warns on stderr for any excluded name that matches no row and lists it as unmatched_exclusions in the verdict, so a mistyped exclusion no longer looks applied (TEN-COST-50).loki doctor no longer passes the full parent environment to its helper and provider-offer spawns; both use the token-free environment, and the stale spawn-guard allowlist row is removed.engine10/pr_body.ts to util/reviewer_brief.ts and are re-exported unchanged.deep_wiring.test.ts creates an initial commit, matching the preflight that now refuses repos with no commits.loki-ts/todo-app/package.json) are green again after wave AO.catch_rate, read only from the packaged benchmarks/seeded-defects manifest, never from the target repo (TEN-GATES-15).redactSecrets iterates to a fixpoint so hashing once-redacted bytes stays stable, and the private shipper copy is removed (Q17-05b).duration_ms (TEN-GATES-22, TEN-GATES-24).loki start prints the effective run cap and its source before the first iteration (TEN-COST-41).loki provider list prints an install hint for each missing provider CLI; loki queue run prints an [n/m] progress line per item (TEN-ADOPTION-22, TEN-UX-29).loki doctor --json runs the sentrux, memory and requirements probes concurrently; the repo profile is cached on HEAD sha plus dirty flag; loki report kpis --since skips efficiency files older than the cutoff without reading them (TEN-SPEED-10, TEN-SPEED-12, TEN-SPEED-11).loki start exits 2 on a missing PRD path instead of running it as a brief; --deploy validates --health-url, --domain and --env-file up front with exit 2 (TEN-STABILITY-30, TEN-DEPLOY-19, TEN-DEPLOY-20).budget_used is rebuilt from efficiency records when budget.json is corrupt; an unreadable model-pricing.json warns once before falling back to a 100 USD cap (TEN-COST-39, TEN-COST-40).loki verify-pr exits 3 with “no proof:” when no proof exists and 4 when proof ran and failed (TEN-GATES-23).loki undo rejects unknown flags, loki control backfill --repo needs a value, loki report kpis --since needs a value, loki wiki show lists valid sections on a bad one, and bash loki provider, config and api exit 2 on an unknown subcommand (TEN-STABILITY-28, TEN-STABILITY-26, TEN-UX-28, TEN-QUALITY-26, TEN-UX-25, TEN-UX-26).loki doctor --quiet keeps Install hints under their WARN or FAIL row; bash loki control --help lists prune, stop and status –json (TEN-UX-27, TEN-UX-24).loki deploy clears every leftover provenance doc before a deploy decides, so a stale doc from an earlier build can never vouch for the current SHA (TEN-DEPLOY-03). tests/test-deploy-receipt-gate.sh 83/0.loki provider list --json and loki control status --json emit machine-readable output; unknown runs report null, never 0 (TEN-ADOPTION-17, TEN-ADOPTION-16).loki queue remove and loki queue clear, plus a validated --max-total-cost cap for loki queue run (TEN-UX-16, TEN-COST-33).loki doctor --quiet prints only WARN and FAIL rows with their fixes; doctor flags orphaned runs whose owner is dead and prints resume and undo commands (TEN-UX-18, TEN-STABILITY-17).loki export --sarif now catches Anthropic API keys, JWTs and Stripe live keys (TEN-SECURITY-09); redaction covers more vendor secret shapes (TEN-SECURITY-10).subprocess calls with shell=True outside tests and comments (TEN-SECURITY-07).loki run status hardening and loki cancel refuses with “no live run” (exit 1) when the supervisor is dead instead of printing “requested” (TEN-SECURITY-08, TEN-STABILITY-22).loki provider <unknown> and loki control serve --port without a value now exit 2 with a usage line (TEN-UX-20, TEN-STABILITY-21).loki report kpis --since and loki undo --list, closing the help-changelog-drift red introduced by v11.17.40 (TEN-STABILITY-20, FC-TEN-STABILITY-20 HELP-FLAG-DRIFT).P0 fix-forward for the nightly 39016259458 failures.
loki verify --require-exogenous routes through bin/loki and autonomy/loki and appears in help.Wave AJ: 9 reviewed slices.
loki proof list --json prints the proof index as JSON.loki status --probe --json prints one JSON object.loki start --help ends with brief, PRD file and issue reference examples.loki doctor runs its integration probes concurrently.loki queue add quarantines a corrupt queue file and warns, instead of overwriting every queued item.Wave AG: 14 reviewed slices.
loki report kpis --since <Nd|YYYY-MM-DD> windows the iteration totals.tsc --noEmit typecheck.loki undo --list prints recent runs with verdict, files changed and age.loki proof share --dry-run runs the secret scan and lists what would be shared.gh auth status probe for 10 minutes.loki status --probe fails when the health URL redirects to a different origin.loki start --deploy passes –destination, –canary, –env-file, –domain and –github through to loki deploy; chat help and docs describe the brief-to-verified-URL path. With –deploy, –env-file is no longer loaded as start config.loki deploy --domain attaches a custom domain and proves TLS.loki queue list shows id, age and status columns with a count footer.loki trust prints a copyable README badge snippet for the last sealed run.loki verify --require-exogenous.start --cost is registered in shell completions; registry flags are guarded in every shell.loki status --probe retries once after 5s before reporting a regression.loki verify --explain lists each gate as exogenous or agent-influenced, with the evidence it read.loki undo --dry-run prints the diffstat it would revert.loki rollback deploy prints the provider rollback command for the last deploy record.loki-ts/src/util/run_pid.ts verifyRunPid already compares process start time to catch PID reuse.uv run as the project interpreter.LOKI_E10_REPOMAP_MAX_FILES knob; truncation drops vendored and generated dirs first.engine10-push-gitlab.sh push-mr.mcp install --print and the MCP stdio handshake.claude auth status probe once, so a slow probe adds no warning line.Wave Y: 4 HIGH slices.
app_probe and applies the secure-scan rules to changed files in the security check; a HIGH finding fails, a missing scanner is not_run (TEN-GATES-06).loki deploy --env-file FILE with provider verbs for Fly, Netlify and Cloudflare; env values are never written to the deploy record and the deploy argv is never evaluated (TEN-DEPLOY-05).loki github publish; fabricated fallback URLs (vercel.app, netlify.app, github.io) are gone (TEN-DEPLOY-08).n, so loki verify --require-exogenous counts real sealed receipts. Since TEN-GATES-03-F1 every real receipt counted 0 agent-independent checks and the flag always exited 4. The key is additive and hashed; receipts without it verify unchanged. P0 fix-forward from the v11.17.33 combined-tree red (HIGH APPROVE c958f563c).--fix real-entry-path red was a stale dist only; the v11.17.33 release build already carries TEN-UX-04, no code change.Wave W and wave AB: 16 reviewed slices.
loki tour verifies a bundled sealed receipt offline, then tampers a copy and shows TAMPERED (TEN-ADOPTION-01).LOKI_E10_REPRO=1 adds a reproduce-first sentence to the lean implement brief when no plan exists (TEN-QUALITY-08).loki doctor --fix offers a consent-gated claude install and claude login (TEN-UX-04).provenance key and receipt.md splits agent-independent from advisory checks. Opt out with LOKI_EXOGENOUS_PROOF=0. Recorded as D104 (TEN-GATES-02).loki start reads cost.profile from loki.yaml as the team default (TEN-COST-27).loki verify --fast and records p95_ms (TEN-SPEED-02).uses: line pinned to the current version (TEN-ADOPTION-10).loki status --probe re-probes the last deploy health URL (TEN-DEPLOY-10).withholdGithubTokens, closing the P9 rule-of-two gap on the login probe (TEN-P9-FIX-F1).scripts/release.sh --ship commits only HEAD plus its own edits (TEN-RELEASE-FAST).--cost lean no longer turns on the router, which forced a plan session on every small task. It now sets haiku-first implement, prompt trim and the small-task sizing knobs, each only when not already set.loki prints how to give work (loki start "<brief>") and how to stop the UI (loki control stop).claude auth status, so the login probe never stalls or warns in stub runs. This targets the red main Tests runs. The v11.17.30 notes listed this fix, but it was not in that release because its merge conflicted; it ships here.claude auth status, so the login probe never stalls or warns in stub runs. This targets the red main Tests runs.loki control stop stops the detached control plane using the PID in instance.json, only after its /health endpoint confirms it is the Loki control service. A mismatched identity or an invalid PID is refused and nothing is signalled.--loki-env LOKI_<NAME>=<value> and pools runs by arm plus lever settings, so a lean-profile arm is never averaged with a default arm. A malformed lever is refused with exit 2.loki verify --max-age refuses a provenance document whose built_at is in the future, and --seal-composite and --export-dsse refuse --max-age with a clear message instead of ignoring it.loki start --cost lean or LOKI_COST_PROFILE=lean opts a run into the cheaper model and effort settings, printed on the start line.scripts/first-run-gate.sh --entry start-empty measures loki start in an empty folder end to end and compares the wall time against its budget numerically.--require-exogenous also applies to unsigned receipts accepted with --allow-unsigned, and only checks that executed (n > 0) count as agent-independent proof.loki proof share refuses any bundle file holding a GitHub, AWS, Anthropic, OpenAI or Slack token, even with --yes; proof badge, proof md and proof bundle are registered in help and completions.claude auth status) now runs with the GitHub token, gh config store, credential helper and SSH agent withheld, the same as a provider session. Since v11.17.22 the probe ran with the full environment, so the P9 injection moat failed.loki status --badge is routed by the dispatcher and listed in help, the command registry and shell completions.loki status --badge prints a green badge only when the last deploy record verifies and its signed live binding is VERIFIED, a red badge for TAMPERED or FAILED_HEALTH, and otherwise no badge with the reason.loki start in an empty folder with no repo initialises one (branch main, empty commit) with a one-line notice; a non-empty folder still stops with the exact git commands, and LOKI_AUTO_GIT_INIT=0 refuses. For the Claude provider, a logged-out CLI stops before any spend.Location: auth/ for GET /api/auth no longer reads as a login redirect while /api/auth/ serves anonymously.github publish, the never-below-raw guard regex, test selection, shellcheck, the stale future-version guard and loki proof help drift).loki stats; their cost and tokens still count. One shared predicate serves both readers, and a test ties the writer’s record kind to the reader.loki verify prints how many passing checks are agent independent (k of n) from the receipt’s check provenance, and says “provenance not recorded” when a receipt has none. --require-exogenous (or LOKI_VERIFY_REQUIRE_EXOGENOUS=1) exits 4 for a VERIFIED receipt with no agent-independent pass. Tamper and signature verdicts are unchanged.LOKI_GATE_SECURITY_SCAN=0 turns it off.loki proof bundle and npx loki-mode verify commands so anyone can check the run on another machine. The Loki footer says verified only for a non-draft VERIFIED body, and LOKI_PR_FOOTER=0 removes it. No network call, no telemetry.loki proof badge, bundle and share work for engine10 runs, not only legacy runs.@anthropic-ai/sdk and the 11.17.15 images were not published. The denylist now lives in the Claude flags module, and a control-plane test fails if the server bundle ever reaches the SDK invoker again.latest, the release still publishes under next but refuses to move latest, so a registry hiccup can never move latest backwards.cost_claim skips non-object rows with a counted warning instead of crashing, and never counts them toward the ratio.Could not resolve: "@anthropic-ai/sdk", so v11.17.14 never reached npm. This release ships everything listed under v11.17.14.loki mcp install merges the Loki server into Cursor, VS Code and Claude configs without touching other entries, keeps a .bak copy, refuses invalid JSON, and --print writes nothing.loki publish creates the GitHub repo (private by default, --public opt-in), pushes it and posts the receipt verdict as the loki/receipt commit status. A dirty tree is refused.loki deploy --execute streams provider output live and keeps the provider exit code.latest and next as soon as the package is built, with a never-backwards version guard. The nightly promotion workflow is removed.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.
loki start heartbeat line shows an estimated time remaining, and actual durations are recorded at seal so later estimates improve.--max-cost or LOKI_E10_MAX_COST_USD is set. Without a cap the line is unchanged.loki deploy --execute captures the public URL the provider prints, shows “Live at: loki start now names the exact fix for its failure class (login, billing or provider, attach, answer, doctor) instead of one generic hint. For providers other than Claude (TEN-UX-02b) the login hint names that provider’s own re-authentication step instead of claude login.benchmarks/bench/cost_claim.py proves a cost ratio only when a seeded paired bootstrap confidence interval clears the target with no acceptance loss, no partial cost, one engine version and at least 3 runs per task.loki restore, and bash completion offers loki deploy --destination.loki share, and test-file text helpers moved out of the engine core (4931 of 5000 lines) with no behavior change.loki trust, the Evidence Receipt hero and cost card, the proof fallback page, the social hook and loki cost summary now show a partial cost as a lower bound (“at least”) or as n/a for cost per accepted, instead of presenting it as a complete total. The TypeScript and Python trust calculations now agree.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.
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.
loki modernize now emits unit.planned and codemod.skipped, and reslice runs in production, so every declared modernize event has a live producer.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.
loki start prints a warning on stderr when a backlog cannot be split into independent parallel units (Q18-08); the run still launches as before.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.
loki modernize runs, because unit discovery lists files with git ls-files; a run that discovers zero units now exits 2 instead of reporting success.attribution.py dry-run prints a cost-per-attempt table offline, with no paid spend.name: prefix, and the real-run scenarios pin the exact non-Wall test checks so the Wall test alone cannot satisfy them.C-F1 cost benchmark (COST-HARN): loki rows store the real exit code and stdout/stderr tails, and reported success follows the engine verdict; seed repos get an origin remote so multi-unit specs no longer exit in 0.2s at the group runner; the loki adapter passes –no-pr and disables git and gh credential prompts, so a benchmark run never pushes or opens a PR; the adapter reads v10 run evidence (receipt verdict, stage.failed and cap.hit reasons, session count) before legacy paths; per-run .loki evidence and stderr are kept before the workdir is removed, skipping secret-looking file names.
loki modernize run now writes a sealed .loki/modernize/benchmarks/bench/attribution.py --execute) now runs the repo bin/loki (or LOKI_BENCH_LOKI_BIN) instead of whatever loki is first on PATH, and refuses to run with exit 2 when the binary’s version does not match the repo VERSION. Set LOKI_BENCH_ALLOW_VERSION_MISMATCH=1 to override. The run metadata records the binary used. A stale global v11.3.9 binary had silently measured an old engine.scripts/release.sh under LOKI_RELEASE_ALLOW_RED=1 now runs the global guard rows (scripts/impacted-gate.sh --guards-only) before any write and exits 3 when one is red. LOKI_RELEASE_SKIP_GUARDS=1 skips them; LOKI_RELEASE_GUARD_TIMEOUT sets the per-row limit (default 170s). New guard rows: help-changelog-drift, no-tmp-sweep, hard-deadline-confinement and cli-commands. Six ALLOW_RED cuts had shipped trees that failed these CI-only guards.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.
loki grill and PRD enrich keep plain-text provider output when the CLI does not return JSON, so the lean JSON unwrap from v11.17.4 no longer empties the reply.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.
run.sh reads the lean flags from a here-string instead of process substitution, so it stays inside the hard deadline confinement rule.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.
claude_permissions section exists, loki start runs Claude with the compiled policy (--permission-mode acceptEdits plus the compiled allow and deny lists) instead of skipping permissions. With no operator policy, runs keep the previous behaviour. LOKI_PERMISSION_POLICY=legacy forces the old mode and LOKI_PERMISSION_POLICY=compiled forces the compiled mode. A one-line banner at start names the active policy and why (C-C2).${var:?} and quoted variable paths under a named subdirectory are allowed, while defaults that expand to ~, .. or a temp root, backtick substitutions, and heredoc tricks that hide a later delete are denied (P12-26g-d).python3 -m compileall) instead of “build not run”, so a working Python project is no longer reported as VERIFIED WITH GAPS. Pytest runs as python3 -m pytest, and when pytest collects nothing the gate falls back to unittest discover; zero tests run stays inconclusive and never counts as a pass (cost S3)..loki/metrics/child-sessions.jsonl, not only the main session. A missing ledger is reported as absent, never as zero, and unpriced lines mark the total as partial (cost S1).raw-configured (Claude Code with the operator’s settings) and raw-bare (claude --bare, needs ANTHROPIC_API_KEY), and stops at --max-spend-usd using the true ledger spend (cost S10).--strict-mcp-config --tools=) and append one line each to the child-session cost ledger (cost S2).--tools= as one argument; the earlier --tools "" form made the Claude CLI swallow the prompt. A missing batched section now prints a warning (cost S6).tests/integration/ or integration/ directories that hold Python files, reads tox.ini whenever present, and picks Maven verify only when Failsafe is declared (M-27D-b)..loki/runs/<id>/ and never reach agent-visible events (C-C4b-c-b).pytest or python3: it falls back to unittest discover, and a missing interpreter or zero tests run is inconclusive, never a failure or a pass (cost S4).--bare (hooks and the command guard stay on), full mandatory and security reviewers, and an empty reviewer list now fails closed instead of passing as 0/0. The supervised reviewer passes --tools= as one argument (cost S5)./bin/bash 3.2 again. A literal backtick in an embedded Python block made bash 3.2 reject the whole hook, which then denied every command.loki modernize --help lists the loki modernize <repo> --to <lang> form.scripts/v10-guard.sh (the session safety guard) parses under macOS /bin/bash 3.2 again, for the same reason.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.
loki modernize <repo> --to python3 runs end to end: it captures an oracle for each unit, schedules waves, merges each wave, verifies it and ships. Capture needs python2.7 or LOKI_MOD_OLD_RUNTIME; without it every unit is NOT PROVEN and the command exits 1. Multi-file units are NOT PROVEN for now, and a wave with no integration tests is verified with a warning (M-26b, M-27A to M-27E).LOKI_WALL_HOLDOUT=0 to opt out (C-C4).loki backup and loki restore, and audit retention with a chain checkpoint. LOKI_AUDIT_RETENTION_DAYS sets the window (default 90) (P12-48).loki doctor labels an unqualified gateway lane and prints the features that lane loses (P12-27). loki doctor --repo appends a readiness record, and --round-trip adds an opt-in live provider check (P12-41).LOKI_VISUAL_GATE=1 makes a visual evidence failure a verify failure (P12-47).loki status shows a freshness label and a Control Plane deep link (P12-19). A large sizing tier (P12-46).loki start --resume reruns verification before it trusts a resumed run, refuses to resume over a dirty working tree or a run that already hit its cost cap, seeds the spend already recorded, passes only the remaining cap to the resumed run, and emits a run.resumed event (P12-07i).LOKI_E10_CAP_S, and a non-finite cap value is ignored instead of disabling the cap (P12-07l, P12-07m).LOKI_LEGACY_BASH=1, or no bun available), loki start --resume and --workspace combined with a Bun-only flag now exit 64 with a message instead of silently ignoring the flag. One allowlist governs which start flags each route accepts; --model is kept (P12-07k).loki verify reports a receipt with an unknown schema as TAMPERED instead of passing it, and a test that could pass without running is fixed (P12-01e).rm, joins backslash-newline continuations before matching, treats GCONV_PATH, LOCPATH and EXECIGNORE as risky, and accepts only uppercase names as safe environment assignments (P12-26e, P12-26f).loki modernize with --help or -h anywhere on the command line prints usage and exits 0 instead of failing with exit 2 (M-MOD).kill -9 and resume, steers are covered end to end, and a skipped Wall no longer freezes steers (P12-13).rm of temp roots (P12-26g, P12-26g-b).loki doctor spawns the provider with a token-free environment, workspace scan accepts forwarded flags, and the harness clone pins main.LOKI_E10_STABLE_PREFIX=0 opts out (LOST-2).loki pause, loki resume and loki cancel dispatch to a live run with a liveness-checked poll; cancel completes as cancelled (P12-10b).loki modernize oracle capture runs token-free in a temp directory with wave-scoped compile checks (M-27F).gh pr edit and gh pr comment arguments (P12-13b).loki backup and loki restore gain shell completions.| CLI help guard: the dispatch-label check now accepts labels that follow “ | ” in an alias row, so pause | resume | cancel counts for all three commands. |
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.
loki start --resume is listed in the start help and in bash completion.loki verify reports a group/1 receipt with no group section as TAMPERED. A receipt with an unrecognized schema also verifies as TAMPERED, and the MCP verify test now fails on a real verdict (P12-01d).git status -z without trimming, records rename sources, and matches exact .. segments (P12-12, P12-25).rm -rf safe-path override is checked per segment and refuses .. and dot-glob path segments by default. It also refuses newline, CR and ; bypasses (P12-26d)./run waits for the start against a deadline, uses one monotonic clock per launch, and caps its iterations. A failed clock read ends the wait at the deadline. If the clock read fails at launch, chat reports a clock failure instead of a false timeout (P12-16c, P12-16d, P12-16e).Eval task fixtures are relative to eval/loki10 and resolve against the harness directory, with no founder-path heuristic (P12-30b).
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.
loki verify checks Seal drift: a receipt whose sealed commit no longer matches the repository is reported, not trusted (P12-01).LOKI_PERMISSION_POLICY. It is applied at every Claude launch site in the shell, Python and TypeScript paths, and a repository policy can never widen the user’s policy. The SDK loop path honours it too: under LOKI_PERMISSION_POLICY=compiled it runs in acceptEdits and never in bypass mode. The default is unchanged when the variable is unset (P12-26, P12-26b, P12-26c).loki status now reports only states the engine actually writes: observing follows the deep-verify start after a PR opens, and blockers show the implement stage’s spec conflict reason. The paused and waiting_for_policy states and pr_ref had no writer and are removed (P12-08b).loki start --resume routes each run to the right resume path, the start and resume commands share one exit-code mapping, and the handoff names both commands (P12-07b, P12-07c).A structural guard refuses uncited “Nx faster” or “N% cheaper” claims added to docs unless the line cites a measurement (P12-40).
loki verify reports a receipt/1 missing a valid time or events_sha256 as TAMPERED, and the test fixtures now match real Seal output (P12-01b, P12-01c).intent.revised and Wall start events, and fails closed when the Wall start is missing or in the future (P12-07d)./run reports “Run started” only once a live run exists, and prints the real error and log path when the start fails (P12-16b).Slack echoes redact secrets and escape user text before posting (P12-17b).
loki start pre-scans skip the value of every value-taking flag, so a flag value is never read as a command, and the help no longer advertises a nonexistent --model; it points to LOKI_MODEL_ROUTE instead (P12-07g).grep -q use here-strings, keeping the SIGPIPE ceiling.LOKI_DIR itself, so a value leaked by another test module under parallel runs no longer points it at the wrong directory.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.
loki steer help no longer claims the modern path needs LOKI_PROMPT_INJECTION; the legacy loop still does (P12-20).loki deploy now offers --health-match, so the completion drift guard passes again.gh pr merge is pinned to the proven head commit; proofs for one head must agree, dirty proofs are ignored, and a moved head is recorded as blocked (P12-03).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.
loki modernize now drives waves end to end through a coordinator: each wave is scheduled (or resumed from the unit log), verified, gated, and shipped as one pull request. A real run still exits 2 before doing any work, and names each missing piece: oracle capture, unit merge operations, the ablation runner and the wave verify commands. It never reports a partial run as done.loki modernize unit now runs a unit instead of exiting 2. The implement step runs as an engine session child whose environment never holds a GitHub token. New-tree cases replay through the Python capture runner for python3 targets; other targets fail closed.--budget, a non-finite unit cost (NaN or Infinity) now stops the run instead of slipping past the cap.loki verify-pr respond re-checks the branch after each fix and requires every recorded commit to be an ancestor of the PR branch before it pushes. Repository names are compared case-insensitively.loki steer and is posted only after the steer is confirmed. If the revision cannot be parsed, the reply says “Steer received” without guessing a number. Live-run lookup honors LOKI_DIR.gh and uses portable stat ordering, and the modernize CLI test carries the Tier A walk-all-src marker.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.
mergeOnce. In-flight units reattach their worktree and hand the recorded run id to the runner, or restart cleanly with a logged reason.loki modernize unit --repo --ref --unit [--mid] [--target] runs one modernize unit (M-23). It fetches repo@ref into a run-owned temp directory, runs the unit there and exits 0 only on PROVEN. The trigger server accepts POST /jobs with kind unit using the same validation as the CLI, and the Helm job worker runs units with worker.engine: v10 in job mode. Bash completion now covers loki modernize (heal, migrate, unit and the unit flags).loki merge pr and loki merge watch now record the merge gate’s outcome (merged, waiting, or blocked at a named gate) and the last checks state in .loki/state/pr.json for the matching PR (by branch, PR number, #N or URL), so the review handoff reports the real result and a blocked merge never reads as merged.loki steer prints. Comments Loki posts itself (the status comment and the end-of-run backstop comment) carry a hidden marker and are never treated as steers. Unauthorized, bot and failed steers, and runs with prompt injection off, get no reply.loki verify-pr respond now pushes only through the trusted push script (pinned origin, credentials scoped to the push), never with its own credentialed git push. It refuses before making any change when the PR is closed or the checkout’s origin is a different repository than the PR (for example a fork), the origin is read once before the fix and the push refuses if it changed during the fix, and if a push would ever open a new PR the error names that PR’s URL.loki verify-pr respond no longer treats Loki’s own .loki run files as uncommitted changes or stages them into fix commits. Only the repository-root .loki is excluded; a nested .loki directory belonging to the project is handled normally.LOKI_WRITEBACK_ISSUE and LOKI_WRITEBACK_KIND.loki start is now tracked per intake call, so one run’s folded readiness line can no longer leak into another intake in the same process.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.
loki start --ship auto-merge now goes through the same gate as loki merge: it refuses to merge a draft PR, a PR whose checks are not green after polling, or a run whose verdict is not VERIFIED, prints the exact blocking gate, and leaves the PR open. A new loki merge pr <pr|branch> merges one PR through that gate.loki merge watch <pr> repairs red CI: it hands the failed-run log to a bounded repair session on the PR branch (LOKI_PR_REPAIR_MAX, default 2), pushes, re-checks, merges only when green, and returns the PR to draft with the failure evidence if attempts run out. --ship uses it after opening the PR, and /merge works inside loki chat.loki verify-pr respond <owner/repo#N> fixes open review comments on the PR branch with one commit per comment (up to LOKI_PR_RESPOND_MAX, default 5), pushes, and replies to each thread with the commit sha. Resolved or already-answered threads are skipped and threads are never resolved. It refuses to run unless the PR’s head branch is checked out and the working tree is clean, pushes only to that branch, and runs its fix sessions in the token-free environment.[[bench]] harness = false and keywords such as “jest-like”.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.
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.
loki memory learn --recent was missing from loki help, and --recent, --repo and --explain-match were missing from the bash completions. All three are now listed, and loki quickstart has its own flag completions.Four new operator commands and options. loki start with Claude remains the primary path.
loki intent show <run> prints a past run’s intent card (the header and Acceptance lines recorded by the plan stage), followed by “answered: y” or “answered: n” when the run has an intent answer. A run whose plan produced no card prints the “Intent: NOT STATED by plan” line, and an unknown run id exits 1 with “no such run”. It is read-only and listed in help and completions.loki quickstart --explain-match (with --dry-run) prints the top three template scores for the brief and a match confidence label (high, medium or low). When the best score is below 2 it adds “no strong match, using a plain spec”. Without the flag the output is unchanged; the flag is refused with exit 2 without --dry-run and cannot be combined with --json.loki memory learn --recent N (1 to 20, optional --repo owner/repo) learns from the newest N merged pull requests in one command and prints one summary line per PR plus a total. An already-learned PR is reported as duplicates without adding anything; N outside 1 to 20 exits 2, and a missing gh exits 1 with the install hint. The single-PR path is unchanged.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.
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.
loki start --brief with --ship no longer tries to close a nonexistent GitHub issue, auto-merge now works for brief runs in the foreground path, and loki run --help marks --brief-text as internal.loki backlog now resolves a ref-name base in a DAG unit to its commit SHA and exits with an error when the base cannot be resolved, instead of silently starting from origin/HEAD.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.
loki estimate resolves its model inside the cost preview module, so the CLI’s estimate arm no longer reaches into engine10 and the single-arm dispatch shape holds again. The dispatch test now asserts that loki estimate routes to the Bun runtime.loki job command; it describes the “Queued” reply the trigger server actually sends.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.
loki start --brief "<text>" --pr --detach honors the isolation flags in brief mode and uses the same worktree and PR machinery as loki start <issue>. Delegation never passes a deploy argument.loki deploy --execute writes a loki.deploy/1 record bound to the receipt that authorized it, signed with the existing receipt signer. loki verify <record> re-checks it offline, also with --pubkey. Tampering gives TAMPERED; an unsigned record, a missing or non-Ed25519 key, or a cited receipt that is not VERIFIED is never VERIFIED. --max-age on a deploy record is refused with UNCHECKED and exit 2, because the record carries no signed issue time. The receipt itself is unchanged.loki deploy --execute --canary uses only documented provider verbs (Vercel preview deploy, health check, then vercel promote; Fly --strategy canary). A failed health check never promotes, and providers with no staged path refuse --canary with the reason.loki workspace infer also suggests after: edges from source imports of a sibling repo’s package or module name (TS/JS, Python, Go; bounded to 400 files per repo) and from OpenAPI client generator configs whose input spec lives in a sibling repo. Each edge prints its evidence as file:line. Output stays print-only and cycle-checked.loki estimate prints the same estimate loki start shows (run history for this repo’s shape, a per-model prior, or NOT AVAILABLE), replacing the old iteration-based projection. One estimator now serves both. --iterations is removed and rejected with an explanation; --json, --model and --provider are supported.loki control serve. The New run picker gained a Browse repos link and a model choice that shows a muted note for haiku without blocking, and failed diff-stat lookups are cached briefly.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.
features/ (D66 fence). Seal dependencies are injected through GroupCtx.seal, supplied by the supervisor; a group run with no Seal dependencies exits 4 before any unit starts.loki-ts/src/util/ts_signatures.ts, and the --max-age parsing and age check moved to loki-ts/src/util/receipt_signer.ts. features is 2692 lines (cap 3000) and core engine10 is 4985 (cap 5000).git checkout calls in group resume are classified in the L2 destructive registry. Neither uses --force.tests/test-backlog-units.sh reads file modes GNU-first, matching the stat portability rule.loki help verify matches loki verify --help again.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.
loki start now composes one signed group receipt over its unit receipts after integration, using the existing receipt sealer. The run exits 0 only when every unit is VERIFIED and the group receipt verifies offline with loki verify; any other outcome keeps the honest non-zero exit.LOKI_GROUP_RESUME=0 forces a fresh run. The backlog runner harvests each unit’s receipt and events (copy then rename, symlinks refused) before its worktree is removed, and forwards the frozen acceptance contract to each unit through LOKI_UNIT_SPEC.## Milestone N sections runs milestone by milestone, each as its own unit group with its own acceptance contract. The run stops at the first milestone that is not VERIFIED, returns that exit code, and names the milestone. Resume applies per milestone. Format and exit semantics are documented in docs/v10/D61-SPEED.md.POST /slack verifies the Slack signing secret (v0 HMAC-SHA256, 300 second replay window, constant-time compare) and POST /linear verifies the Linear-Signature HMAC and webhook timestamp. Both enqueue through the same job path as /jobs, reply with the job id, and post a completion summary back (Slack response_url on hooks.slack.com only; a Linear comment when LINEAR_API_KEY is set). A missing secret returns 503 naming the variable. The new secrets are denied to child runs. See docs/sentry-intake.md.loki verify --max-age DUR (for example 30m, 24h, 7d) refuses a receipt whose signed issue time is older, has no signed issue time, or is more than 300 seconds in the future, with UNCHECKED (exit 2) and a named reason. Invalid values exit 2. Group receipts under .loki/groups/ are verified by engine10, and chat /verify passes its arguments through.loki deploy now offers --dir, --no-clip and --allow-unanchored, and loki verify offers --max-age.LOKI_GROUP_TEST_CMD over the discovered test command for group runs.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.
loki verify --help now documents --seal-composite <run-dir>. Bash completion now offers the loki deploy flags (--execute, --health-url, --auto-rollback), the loki verify flags including --seal-composite, and loki memory brief --task.grep -q, which kept the repo under its FC-64 ceiling for that form.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.
loki start --workspace <name> and loki start --deploy, plus /workspace and /deploy in loki chat. The workspace path now forwards every value-taking loki start flag (isolation, model, max cost, provider, budget and the rest) and --brief TEXT. The bash divert passes an allowlisted environment, so unrelated secrets such as DATABASE_PASSWORD are not forwarded.loki verify --seal-composite <run dir> seals a workspace run, and loki verify checks it offline. Verification requires every repo in the integration order to be present once, with outcome ok, a receipt path inside its own worktree, and a recorded integrated head; a missing head gives UNCHECKED. A workspace run seals the composite once after integration, and refuses with a named reason when integration failed or a repo has no receipt.plan: true (or LOKI_WORKSPACE_PLAN=1) one planner call writes a per-repo task plan. A plan that is malformed or names unknown repos is not used; the run says why and falls back to the generic task text.loki workspace infer <name> reads package.json, go.mod and pyproject.toml of local checkouts and prints a proposed after: graph. It never writes loki.yaml, never clones, and rejects a cyclic graph.integration.per_stage: true runs the integration command after each dependency layer and holds later layers until the earlier gate passes. A failed stage marks later repos SKIPPED and the run exits non-zero.loki deploy --execute, still gated on a VERIFIED receipt. Deploy also requires a policy that explicitly grants deploy (autonomy level or actions.deploy); an empty policy or a missing node refuses.loki trust graduation advice: only VERIFIED runs count toward the 10 needed before execute is suggested. Other recorded verdicts are listed as missing evidence.loki memory brief --task TEXT prints the brief, and the engine10 implement stage and chat inject it.acceptance criteria, which seed that unit’s Wall; the group contract is frozen before workers start, and an unowned criterion fails the group closed.loki start. The integrator merges unit branches in dependency order, refuses a dirty tree or an existing group branch, and never discards user edits. The selected provider now reaches every unit, and the group test command comes from the repo’s own detected commands (npm, pytest, make, go, cargo and others) unless LOKI_GROUP_TEST_CMD is set. Honest limit: the group Seal is not wired yet, so a parallel group run exits 4 (NOT PROVEN, Group Seal receipt UNCHECKED) rather than 0.coord-1-api-web, longspec-1-three-units). The Factory arm is reported as UNMEASURED: no droid run has been measured headless, and unmeasured runs are excluded from every rate rather than shown as zero.LOKI_COST_CONFIRM=1 on a terminal, a run shows its cost estimate and asks once before spending.workspaces: (for example afer) exits 2 and names the key. The schema gains concurrency, plan and integration.per_stage.loki verify matches engine10 (0 VERIFIED, 1 TAMPERED, 2 UNCHECKED, 3 UNSIGNED, 4 run outcome not verified, 66 no runs). Workspaces docs and CLI reference cover the new flags and verbs.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.
loki status renderer. contrib/status.ts and its duplicate run-dir helpers and latestRunId copies are gone; commands/run_status.ts owns them. The group grid, blocked answer and dashboard now honour LOKI_DIR.loki.yaml reader. lokiYamlTexts now serves the run cap, budget cap, PR and install settings, replacing four separate readers.util/atomic.ts atomicWriteText. It gained a durable option so the non-fsync callers keep their cost. The bytes written to disk are unchanged.renderReviewerBody is canonical. The never-used renderPrBody, pr_criteria.ts and unitTableLines are deleted.loki issues now validates the repo, refuses dash-leading labels and milestones, enforces the issue row shape, and accepts --milestone. The uncalled e10ext/issues.ts is deleted.prune-worktrees.sh (open cwd, 30-minute commit age, eval archive run ids, git cherry merge detection) are now an opt-in v10-worktree-reap --strict mode. That mode is a dry run unless --apply is given. The default behaviour is unchanged, and prune-worktrees.sh is deleted.dashboard/server.py is the only dashboard server. The standalone FastAPI control app and dashboard/run.py are deleted, and their auth assertions now run against the server.usage_window parser (the usage governor owns usage parsing) and the unused MCP tools.py and resources.py (mcp/server.py owns tools and resources)..claude/commands files are now symlinks to the plugin’s commands. Copied skill doc blocks now point to their canonical references.*_v2, *_mc1, *_haiku55 and similar) are merged into their base test files as describe blocks. The test case count is unchanged.docs/exit-codes.md. Key rotation and OIDC claim mapping now live only in docs/authentication.md. That page no longer claims that OIDC users get full access by default.docs/v10/ENGINE.md is restored to the engine specification. A cut log had overwritten it; that log now lives in ARCHITECT-CUTS.md. The OpenClaw integration page is folded into integrations/openclaw/README.md.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.
runner/repo_profile.ts.loki quick with an unknown option now exits 64 even on a host with no provider installed. Usage is validated before the provider check.util/receipt_signer.ts, and run-output notes moved into util/run_outputs.ts. Both keep their old import paths. scripts/structural-checks.sh now counts lines the same way as the budget test.export const USAGE;cost_usd and records cost_unmeasured_reason.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.
loki start now discovers each repo’s test, lint, typecheck and build commands and records a baseline test run in .loki/state/repo-profile.json, reusing it until the manifests or HEAD change. This lives in the existing runner/repo_profile.ts. A check that already failed at baseline, with the same failing ids and outside the files the diff touched, is reported as pre-existing and does not block; every other failure blocks as before. loki doctor --repo prints the profile; combined with --requirements it prints both. Set LOKI_REPO_BASELINE=0 to skip the baseline run..loki/state/risk-tier.json. High covers sensitive paths (auth, crypto, payments, migrations, CI config, lockfiles, infra, .env), deleted tests, public API surface, or large diffs. A lower tier runs targeted checks, and every check it skips is recorded as not run, never as passed. The run summary shows the tier. LOKI_RISK_TIER can raise the tier; lowering it requires LOKI_RISK_OVERRIDE_REASON, which is recorded. The logic lives in util/check_result.ts; see docs/environment-variables.md.loki start run writes .loki/review/HANDOFF.md (about 30 lines) and .loki/review/handoff.json. Both list the outcome, spend, the risk tier, the change, what was demonstrated, failures, what was not checked, and the exact accept and reject commands. The PR body includes the same handoff, loki chat /run prints it, and the final summary line points to it..loki/state/requirements-map.json. Uncovered requirements are counted in the run summary and listed in the handoff and PR body. A run with no requirements removes any map left by an earlier run, so it never reports another run’s requirements. This is built into the existing features/contract.ts.util/run_outputs.ts, util/run_pid.ts and engine10/events.ts, and documented in docs/v10/ENGINE.md.loki-stop-verify.sh) runs Loki’s fast verification when a Claude Code session ends, and the /loki-mode:start and /loki-mode:handoff commands are new. /loki-mode:verify accepts --fast. Install with loki setup-skill --claude-code.loki doctor --requirements lists what each execution path needs, whether each item is required or optional, whether it is present, and a one-line fix. loki doctor --json includes the same data. The table is shared by bash and TypeScript through autonomy/lib/setup-requirements.json and documented in docs/alternative-installations.md.loki route --json reports the adapter and its capability differences from Claude Code. The capability table lives in the existing providers/provider_guard.ts and is documented in docs/cost-controls.md.benchmarks/bench/attribution.py and six tasks compare Loki against a well-configured Claude Code on accepted tasks, human interventions and cost. It reuses the existing bench fixture hash and Wilson interval code.loki-ts/data/model-pricing.json. Precedence is --max-cost, then LOKI_BUDGET_LIMIT, then loki.yaml, then the default. Unmeasured cost is reported as unknown with a reason, never as $0.00. See docs/cost-controls.md.autonomy/lib/proof-verify.py and shares its git and JSON helpers. No second verifier was added.loki quick with an unknown option now exits 64. The routing contract and exit-code cases are documented in docs/exit-codes.md and checked by the existing tests/test-engine10-dispatch.sh and tests/test-exit-codes-documented.sh.docs/contracts/*.md files were folded into the docs that own each topic, and the parallel modules were merged into their owners.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.
loki start output on the v10 engine stays within the 8-line cap (D48). The unified run summary added a six-line block after the engine output. A default run now gets a single Next: line, and when the output already has seven lines that line is folded into the last one. --verbose and --quiet still print the full summary.loki doctor --fix now passes a scoped environment (PATH only) to its command -v probe. It no longer lets the child process inherit the full parent environment, which the spawn environment guards forbid.chat, edit, diff, models and setup, plus the config list subcommand. docs/v10/CLI-MODERN.md was regenerated from the registry.providers/provider_guard.ts) is now classified in the destructive git call registry. The reviewer stage’s user-configured --model is vetted in the model-pin guard, so a cascade or cost downgrade still cannot pin a weaker model.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.
loki steer "<guidance>" and /steer in loki chat now take effect on Loki 10 runs. Before each plan and implement session the engine reads .loki/HUMAN_INPUT.md and adds it to the next brief as a delimited operator-guidance block, outside the cache-stable prefix. It then archives the file to .loki/logs/human-input-<ts>.md, so the guidance applies once. The engine rejects symlinks, and archives files over 1 MB without injecting them. Set LOKI_PROMPT_INJECTION=0 to turn this off.LOKI_REVIEW_PROVIDER. An agent named reviewer.md or council.md, or any agent with role: reviewer or role: council in .loki/agents/ or ~/.config/loki/agents/, sets the review stage’s prompt, provider and model. The agents loader accepts the new optional role key.loki edit "<change>" ends with the unified run summary. --quiet prints only the summary, --json prints one JSON object, and NO_COLOR is honored.loki diff is a real command. It shows uncommitted changes, or the last commit if the tree is clean. Run summaries already suggest it as a next step.start flags (--model, --isolation, --max-cost, --no-pr, --explain-route).Plan mode in loki chat is read-only for every provider. Each provider runs with its own read-only flags:
| Provider | Plan-mode flags |
|---|---|
| codex | exec --sandbox read-only |
| opencode | --agent plan |
| aider | --dry-run --no-auto-commits |
| cline | --plan |
A post-turn guard also reverts any tracked or untracked file a plan turn changed anyway.
loki chat --attach finds running sessions the same way loki status does. It reads the session and run pid files, PAUSE, STOP and session.json, and the Loki 10 run pid. It no longer guesses from the event log.loki help keeps the short core-verbs page with start first. It now also lists:
--isolation dialcost and estimateloki help <command> once again shows that command’s own --help. loki help all lists agents, chat, edit and models.
README’s loki verify exit codes now match docs/exit-codes.md:
| Exit code | Meaning |
|---|---|
| 0 | VERIFIED |
| 1 | CONCERNS |
| 2 | BLOCKED |
| 3 | verifier error |
| 64 | usage error |
Treat only exit 0 as verified.
loki verify --json once again prints only the evidence JSON on stdout. A generic JSON wrapper had captured it.loki start routes to the Bun engine, which has an opencode invoker.autonomy/route.sh is shellcheck clean.loki route and loki diff now pass the docs-drift check, which reads route.sh’s dispatch.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.
loki start (and loki quick): loki start ./prd.md --model ollama/qwen3-coder runs OpenRouter (openrouter/<id>, key from OPENROUTER_API_KEY), Ollama, LM Studio, llama.cpp, vLLM or any Anthropic-compatible endpoint (custom/<id> with LOKI_MODEL_BASE_URL) through the Claude Code harness. Cost tracking, the spend cap, kill-blocking and receipts keep working. The route env is set only on the provider child process, and the token is never logged. --model also accepts a tier (small, medium, high) or a model id on both engines, and LOKI_MODEL_ROUTE is the env form.loki models --local finds running local runtimes (Ollama :11434, LM Studio :1234, llama.cpp :8080, vLLM :8000) and prints ready-to-paste --model values. --json is supported.loki start ./prd.md --provider opencode runs opencode run, passing the model through with LOKI_OPENCODE_MODEL=provider/model. These runs no longer fall back to the previous loop..loki/agents/<name>.md or ~/.config/loki/agents/. Manage them with loki agents list|new|show and export them as Claude Code subagents with loki agents export --claude. Council reviewers and the Loki 10 cross-review use a matching agent’s prompt and model; nothing changes when no agents are configured. Templates for reviewer, planner and tester ship in templates/agents/.loki edit "<change>" is the lightest path for small changes. It makes one Claude pass on the current repo, with no PRD, plan, council, PR, receipt or branch switch. It then prints the diff stat, the diff, and the cost and elapsed time. Opt-ins: --verify, --receipt, --commit and --dry-run. Scope it with a path: loki edit src/foo.ts "rename X to Y".loki chat, an interactive alternative to loki start, with Plan mode (read-only) and Build mode:
/run <prd|brief> starts a normal loki start run with the chat’s provider and model, and the chat stays open afterwards.loki chat --attach (or /attach inside chat) joins the current or most recent loki start run, with /status, /steer, /diff, /verify, /receipt and /stop, and answers questions about the run.loki chat --tui opens Claude Code’s own interface with Loki context added.loki is unchanged and does not open chat.loki route start ./prd.md and --explain-route show which engine, provider, model and input type a command would use, and why, without running it. --json is supported.loki setup is one wizard that covers required tools, Claude Code install and login, an optional provider, a model route and a spend cap. loki setup --yes runs it non-interactively. loki doctor --fix applies the safe fixes and prints the exact command for each remaining one. loki config list [--json] completes loki config get|set|list|path.next: step that suggests loki chat --attach. NO_COLOR is honored, --quiet prints only the summary and --json adds a summary object.--engine legacy or LOKI_ENGINE=legacy.--engine engine10 forces Loki 10 and refuses unsupported flags.guard.violation event). Each CLI’s native sandbox flags are used where it has them.loki help shows the core verbs, with loki start first as the primary command and loki chat as the interactive alternative. loki help all lists every command. loki help <topic> covers start, models, providers, agents, verify, exit-codes and config. Unknown commands get a “did you mean” hint.loki verify gaps in docs/KNOWN-ISSUES.md:
loki verify exits 3 on an empty diff (was 1).loki quick with no task, and loki start with an unknown option, exit 64.
See docs/exit-codes.md. Scripts that matched the old codes need updating./steer from chat takes effect on the previous loop only (with LOKI_PROMPT_INJECTION=1). On a Loki 10 run it records the note in .loki/HUMAN_INPUT.md.npm install -g loki-mode@latest, then loki setup and loki start "<brief>". Check the version with loki version.Security follow-up to v11.3.6 plus a release-pipeline fix that lets verified releases reach the latest tag sooner.
latest accepts that run. Previously only the hourly schedule ran the backstop, so a release could wait up to an hour, or longer under steady releases, before it was eligible for latest.loki version.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.
pack-npm job (read-only permissions, no secrets) and the release job publishes those exact bytes. The sha256 is verified after download and again immediately before npm publish, and publish still refuses unless the release tag exists on the remote. The separate publish-npm job is absorbed into release, which removes one job start from the path between dispatch and npm publish.npm-visible (registry propagation poll) is now non-blocking: Docker, the SDKs and Slack no longer wait on it. Registry lag is reported separately in docs/v10/METRICS.md.npm publish now fails the release job, so the Docker, SDK and Slack jobs are skipped for that run instead of running independently. A rerun after the tag and GitHub release exist skips publish; use workflow_dispatch with force_republish to recover.Maintenance patch release for contributors running many parallel worktrees. Nothing changes in what Loki builds or verifies for users.
scripts/v10-worktree-reap.sh removes .claude/worktrees entries that are merged or idle, after saving any uncommitted work to a local wt-save/<name> branch. It runs from the train cycle and from a post-merge hook (under a 120s timeout), and scripts/v10-worktree-add.sh refuses to create a worktree below 40G of free disk until a reap frees space. The pulse reports a WORKTREE_SPRAWL violation above 20 worktrees..claude/worktrees only; the main checkout and the calling worktree are never touched.echo | grep -q, which could fail intermittently with a broken pipe under load (FC-64). The legacy pipe-to-grep ceiling drops from 1789 to 1742.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.
echo | grep -q, which could fail intermittently with a broken pipe under load (FC-64).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.
loki-ts/src (raw spawn, full-env spawn, credentialed push, hardcoded model IDs, regex and registration guards) could be skipped on a diff that added a new violation in a file it did not name. Every guard that walks the source tree now carries a content marker, and the fast gate selects all marked guards on any diff (FC-89, FASTGATE-GLOBAL-GUARDS).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.
LOKI_ALLOW_AGENT_GITHUB_TOKEN=1. Untrusted repo test code never receives your credentials. A project whose tests fetch private dependencies over the SSH agent or a token will now fail those fetches. An explicit opt-in that passes named variables to project tests only is coming in the next release (FC-87).Full suite (backstop) job as a measurement.loki plan --spec, loki start --spec and loki memory forget (HELP-DRIFT).tests/test-verify-runner-selection.sh could report a false failure from SIGPIPE under pipefail: its pipe-to-grep checks are now here-strings, enforced by the SIGPIPE guard (FC-74).claude was installed: it now runs against a stub (FC-76).loki start cases entered the real runner: the stub provider now refuses login and the suite runs in about 20 seconds with every privacy assertion intact (FC-83).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.
latest moves only when the newest covering nightly is green and a Post-Release Smoke run for the same release is green. Smoke runs are matched by their run title, which carries the Release run’s head SHA (workflow_run) or v<VERSION> (dispatch), so a smoke for another release can never satisfy the gate.tests/test-workflow-no-masked-failures.sh fails when a workflow step runs a test and then hides its failure (|| true, a capture that is reset before exit, a grouped test followed by || true, continue-on-error). Common error idioms such as echo "::error::..." >&2; exit 1 stay allowed. Contrived bypasses are logged as a LOW follow-up (FC-65).tests/test-sigpipe-guard.sh blocks new <large output> | grep -q shapes under pipefail, which die of SIGPIPE on large input and report a false failure..gitleaksignore, whose comment text matched a rule; fixtures now drop comment lines and a guard scans the real file with the pinned gitleaks (FC-66, PP-GITLEAKS-FIXTURE).loki doctor nightly red: a subprocess test pinned PATH so tightly that the product under test never ran, and its host-dependent counter had no host-independent fixture (FC-63, NR-DOCTOR).v from a dispatched version, so v11.3.3 and 11.3.3 produce the same title.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.
engine10-push.sh push-pr, so the 11.3.1 --no-pr requirement is dropped. Preflight uses push-pr’s own origin validation and is pinned before attempt 1, the winner branch name is trusted-derived, and identity falls back per field.loki undo <run-id>: UNDO-1 adds a read-only plan (--plan) that validates receipt refs as hex object ids and ends option parsing. UNDO-2 applies the undo behind LOKI_UNDO=1, refuses hostile receipt refs before touching git, and closes PRs with an allowlisted env. The command is registered in the CLI registry and generated docs.loki verify-pr (VPR-1, VPR-2): a fail-closed sandbox runner and contract, and a core that runs the fail-to-pass check through that sandbox. Issue checks are trust-gated, base and head suites are compared, and issue author association is read from the REST API with a failed trust lookup reported.loki issues run (MASS-1, MASS-2): triages open issues and runs them through the T7 queue. MASS-2 splits a too-large issue into stacked slices (--no-split opts out) with a stacked PR base.loki export --sarif (SARIF-1, SARIF-2): converts findings to SARIF 2.1.0 and writes findings.sarif per run, with repo-relative encoded artifact URIs and paths outside the repo omitted.loki plan --spec writes an editable .loki/specs/loki start and loki queue run, shown only on an interactive stderr.loki memory forget <prl-id> and lessons --json (133-F3); lessons are demoted after 3 or more decided uses with no VERIFIED run (132-F3); a lesson cites the receipt of its PR’s run (MC-1); lessons can be learned from closed-unmerged Loki PRs behind LOKI_LESSONS_REJECTED (MC-2).loki status shows the last run’s sealed verdict (133-F4).--human-floor-min), a raw-codex arm with an --arms selector, claimed-done extraction for each arm, and an FCR task-set validator.next, latest by backstop-green promotion).select-tests --run exits 1 with NOTHING RUN when zero suites executed (FC-47), and batches candidate greps with xargs -0.loki help start no longer advertises refused flags (133-E3), and start help no longer claims attempts N>1 requires --no-pr.--no-ext-diff and --no-textconv are deduplicated in central injection, with acceptance and canary tests (132-B1).duration_s must be a number (WC-01b).loki onboard survives a failing find under pipefail, and the doctor install-hint test counts only provider hints (FC-53).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.
loki start --attempts N runs N engine10 worktree attempts and scores each one by the executed passing checks in its verified receipt. The winner is applied, every sealed receipt is kept after its worktree is removed, and the run is BLOCKED when no attempt sealed. In 11.3.1 it requires –no-pr (PR support ships in 11.3.2), and no start path reaches the legacy loop (FC-37, FC-38).loki queue add|list|run runs queued issues overnight and writes a morning digest. It reads engine10 runs/loki doctor: the failure count is derived from a single result list, and an inconclusive login probe reports UNKNOWN (FC-DOCTOR-COUNT).--attempts without --no-pr exits 2, so the attempts path never uses a credential; PR support ships in 11.3.2.loki start, on both the bash and Bun routes, behaves exactly as in 11.3.0.loki start --attempts N --no-pr.loki-ts/dist for this release.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.
readScopeText reader, and any rejected scope file (symlink, oversized, missing, unreadable) records NOT PROVEN and defaults to sonnet, never haiku.loki-ts/src/runner/router/ (session_route.ts, implement_route.ts, unit_model.stallClimb); the non-modernize engine10 core goes from 5010 to 4948 lines, under its 5000-line cap.npm root -g is bounded to 3s and fails closed.bun test --coverage died after loading them; the mutations now run in a child bun process, a crashed child fails the test loudly, and a guard test asserts no temp src copy is ever imported in-process.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.
scripts/b9-scoreboard.sh): per-run cache-read, cache-creation, fresh input and output tokens, dollars, wall seconds and advisor calls, read from result-cost files (arms 2-4) or the claude JSON usage (arm 1). Per-arm mean/min/max summary, router_cost_ratio and gate_1_05 (requires n>=3 recorded runs per arm, else FAIL). New --repeat and --summarize FILE flags. Missing data prints NOT RECORDED, never 0.claude -p /usage (40s timeout, cached 15 min, failures cached 3 min under ~/.claude/usage-governor). Up to 8 seats, hold above 70% session, zero at the 85/90 ceilings. The projection is only the fallback, and the pulse labels the basis as measured or projected with the failure reason.loki doctor has one implementation, loki-ts/src/commands/doctor.ts (FC-33). The bash cmd_doctor and cmd_doctor_json are thin delegators through _loki_bun_delegate, with a minimal “bun route unavailable” fallback whose last line is the single reason (--json reports route “unavailable”). --airgap stays on bash. The Cockpit block, install-integrity wording, PATH shadowing blocker, first_run_blocked telemetry and the SDK-only Next recommendation were ported into doctor.ts. Under LOKI_LEGACY_BASH=1, doctor prints “Active runtime: Bash (autonomy/loki)” plus the existing WARN, matching 11.2.0..loki/state/usage-readings.tsv; docs/v10/usage-readings.tsv stays founder-only. One /usage read per refresh (no second claude call for --read-usage).loki-ts/dist for this release.gate_1_05.plan-scope.json reader (readScopeText) shared by run_cap and plan. It rejects symlinks and non-regular files and caps reads at 256 KB; a rejected file behaves as no scope file.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.
LOKI_ROUTER on, Opus assigns an executor to each plan unit. resolveExecutor takes an explicit env. The advisor probe is provider-aware, units are capped, duplicate ids are rejected, and an invalid unit id gets a placeholder that cannot collide with a real id. plan-scope.json must be a regular file.1, true and on, matching the single flag reader (R1-08c). This resolves the 11.2.0 known issue where LOKI_ROUTER=yes added telemetry fields while the router stayed off.run.sh no longer trims the completion promise globally and detects comparator symbols the Bun route already detected. A behavioral bash/Bun parity guard covers 187 goals (RC-DOCTOR-ONE S5).loki-ts/src now select the shell suites that guard them. A new scripts/impacted-gate.sh keeps its own run directory, fails closed and handles the bash 3.2 wait status (FC-31).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.
flag.ts; 1, true and on opt in, 0, false and off opt out). No routing by task type or path pattern.units: [{id, kind, executor, reason}]. A missing or invalid unit runs on Sonnet and is marked NOT PROVEN. A Haiku unit whose failure is code-owned is redone on Sonnet. A Wall unit assigned to Haiku is flagged for Opus review.advisorModel and autoCompactWindow pass into both SDK and CLI sessions. Any Haiku id without a reachable advisor is raised to Sonnet. The bundled Claude Code version probe is cached.scripts/b9-scoreboard.sh (R1-17): a B9 comparison harness with --emit-shape-defaults and --emit-seed, which writes a provisional loss-shape seed. It runs an auth preflight, reports BLOCKED instead of solved=0 when it cannot run, requires a confirm rerun, guards the arm env and keeps HOME in real mode.loki doctor now prints the advisor status and the bundled Claude Code version when LOKI_ROUTER is on (R1-20). With the flag unset the doctor output is unchanged from 11.1.0 (R1-20b).cp_* tools that issue GET requests against the Control Plane API. That brings the server to 47 tools. Id, artifact and filter checks use full matches.LOKI_ROUTER_EXECUTOR is now treated as a route input that must pass every floor, not as a bypass. LOKI_MODEL_OVERRIDE and LOKI_CLAUDE_MODEL_DEVELOPMENT remain the user bypass and are recorded with source override (R1-05 B1). LOKI_ROUTER_EXECUTOR is scheduled for removal in a following patch release.LOKI_ROUTER=yes leaves the router off but still adds the router telemetry fields to the result-cost file, because the telemetry gate reads the flag with its own word list. Only 1, true and on are documented values. A following patch release moves the telemetry gate onto the single flag reader.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.
claude-haiku-5-5 and claude-sonnet-5-5 (ids verified by real calls on Claude Code 2.1.293). The haiku and sonnet CLI aliases and the fast and development tier defaults now point at the 5-5 ids; the previous ids stay catalogued as legacy (R1-03).haiku family alias row stays at the Haiku 4.5 price of $1 / $5 (R1-02).ANTHROPIC_BASE_URL, below Claude Code 2.1.293, or after an advisor tool error, which is remembered for the rest of the run. Unavailable never fails a run (R1-06).TARGET_DIR, and scripts/local-ci.sh installs dependencies in a fresh worktree (FC-29).Patch release. A one-line product brief no longer skips the Wall because a common word sits inside a longer name.
Patch release. A one-line product brief no longer skips the Wall just because a token appears on most paths in the repo.
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.
Dockerfile.control-plane now copies engine10/stages/xreview.ts, which the CP routes import since 11.0.0. Without it the image build missed a value import (CP-04).Verified, Needs your answer, Tampered) and the dash shown for stages that ran no model session, matching the 11.0.0 display contract (bun 515 pass, 0 fail).loki legacy removal: shard duration rows for the removed legacy suites are dropped (S-134), the historical loki legacy mentions in GUIDE, LEGACY-REMOVAL and RELEASE-11 are allowlisted (DOC-02), and the gitleaks fingerprint file is allowlisted for the legacy dashboard guard (CPE24-L7).receipt-check composite action (docs/v10/DEPS.md).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.
loki legacy and the LOKI_ENGINE variable. loki legacy now exits with a removal message, and the issue-to-PR Action no longer pins LOKI_ENGINE (A6b).stop: "cap" in --json (A2b, FC-21b L7). Scripts keyed on exit code 3 should read the verdict instead.implement_limit (A2b).LOKI_CP_ASK=1): a read-only, asynchronous chat over runs, receipts and repos on the user’s provider, with threads stored in control.db, SSE streaming and run-id citations. It never starts a build; it can only offer a confirm dialog (A4-ASK-1, A4-ASK-2).loki doctor check (A5).loki verify --pubkey and posts success or failure (B2).gh attestation of the receipt, plus the Agent Change Receipt spec in docs/AGENT-CHANGE-RECEIPT.md (B3).review: codex or claude, or LOKI_REVIEW_PROVIDER). The second provider can only downgrade a verdict, never upgrade it (B4)..loki/project.json is loaded as the shared Project Model and is part of the cache key; Loki never overwrites the tracked file (B5).pr.author: me or bot (LOKI_PR_AUTHOR); bot mode uses LOKI_PR_BOT_TOKEN only in the push child (B7).GET /v1/cost/repos aggregates real run rows by origin repo (B8)./v1/start refuses HOME, / and non-git or unregistered repos; a dead-PID reconciler marks orphaned runs STOPPED (A3d-f).safeGit blanks filter, textconv and gpg configuration (A1b, FC-25b).loki status prints the URL the running Control Plane actually listens on (A6a).--attempts N best-of-N (B1) is deferred: the selector has no caller yet, and wiring it untested would risk Seal accuracy.claude -p on the same bases.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.
packages/control-plane/src/ask/tools_server.ts): runs_search, run_get, run_events, run_artifact, runs_compare, stats, cost and repos_list. There is no write tool, and artifact reads are allowlisted. Nothing calls it until LOKI_CP_ASK ships.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.
project_model/scope.ts). It is opt-in with LOKI_E10_SCOPE=1 (default off). A test owned by the root package, or one that imports a changed file, is always kept, and the tests scoped out are emitted as a test.scoped_out verify event.project_model/install.ts, not yet wired into verify). It runs the Project Model’s own install command once per package per run, records the result as a check with its cwd, restores tracked files the install changed that were clean before it ran, and lists everything else it could not restore. Every destructive git path in loki-ts is now listed, with a trust or work class, in a static L2 registry test.-json first-error line now also reads build-output events, so a compile failure reports the compiler line instead of “FAIL pkg [build failed]”.safeGit too.test check that exits 0 is never a pass: its output, including the go test -json event stream, is produced by the code under test, which can print the test2json framing marker (or exit early with os.Exit(0)) and forge run and pass events for any name, so no static check can confirm that a test executed. The reason recorded is “go test output is produced by the code under test and cannot confirm execution”. go test -json is now used only for fail evidence (a fail event, failed test names in firstError) on a red run.goRunner in util/check_result.ts) used by verify, deep and the per-package suites; a wrapped command (env X=1 go test, GOFLAGS=x go test, a path to go) no longer falls to the generic path.OK after a real FAILED or after an all-skipped OK (skipped=N)) or a verdict contradicting the exit code makes the count unknown; OK (skipped=N) subtracts the skipped tests.core.fsmonitor or hook with the real GitHub token in the environment. A new safeGit() helper (loki-ts/src/util/safe_git.ts) disables fsmonitor, hooks, core.sshCommand, ext:: and the credential helper, sets GIT_CONFIG_NOSYSTEM=1, and strips the token family and SSH_AUTH_SOCK unless a call opts in (authenticated fetch). resolveRunCapS (the regression since v10.10.4), repomap, stop-restore, preflight, base resolution, the backstop commit, PR head lookup, load-owner and warm/contract lookups use it. A static guard fails on any new raw git spawn outside an allowlist with a reason.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.
mcp/server.py --read-only (or LOKI_MCP_READ_ONLY=1) registers only the 16-tool read allowlist (READ_ONLY_TOOL_ALLOWLIST); every other tool is absent, so a tool added later fails closed until classified. In this mode the event and learning-signal emitters, code-search auto-reindex, the StateManager and the memory store (new MemoryStorage.READ_ONLY: no directories, no init, no lock files, writes raise) do no filesystem work, and code search refuses a non-loopback LOKI_CHROMA_HOST. LOKI_MCP_READ_ONLY accepts 1, true, yes. Default mode is unchanged.dependsOn (roots of the packages it builds on) and install (its cited dependency-install command), both decided by the discovery model from manifests (no import parsing). An unknown dependsOn root is rejected; an absent field means edges are unknown. New project_model/graph.ts (dependentsOf, transitive and cycle-safe) and util/yaml_key.ts (a two-level yaml key reader that ignores deeper same-named keys and reads CRLF). The cache key is salted with model-rev:2, so a cached older model is rediscovered once.ask_threads, ask_messages and ask_events tables (migration 0005_ask) for the coming async Ask Loki box. Existing runs rows migrate byte-equal. Nothing reads or writes them yet; the feature stays behind LOKI_CP_ASK (default off).LOKI_CP_ASK.MemoryStorage mutator now calls one shared _check_writable() and raises PermissionError under MemoryStorage.READ_ONLY (previously save_pattern, update_pattern, update_timeline, set_active_context, delete_episode, delete_file, ensure_directory and the namespace copy/merge wrote unguarded); the code-search auto-reindex gate is now covered by a test that arms LOKI_CODE_INDEX_AUTOREINDEX=1 against a stale manifest and asserts no spawn.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.
loki-ts/src/util/run_cap.ts, git ls-files) and the load-owner process-tree walk (loki-ts/src/runner/load_owner.ts, pgrep -P) now pass an explicit env, so they see the current environment rather than the one captured at process start.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).
loki-ts/src/runner/load_owner.ts): when the same command also fails to load on the base commit and no changed file is named in the error, the check is NOT PROVEN (harness-owned, no fix rounds) instead of a code failure. Applies to the per-file checks, the deep full suite and per-package suites; lint, typecheck and selector checks (kind: static) never go through it.--- PASS|FAIL across every package, with [no test files] zero only when no package ran tests; a non-verbose go summary is reported as unmeasured, never as “no tests executed”. Runner summaries are read only from the last 12 output lines (a testless file printing “Tests: 5 passed” no longer counts), ANSI colour codes are stripped, and python -m unittest and Playwright summaries parse. User-visible: intake’s closed-issue / already-done short-circuit now always ends PARTIAL (NOT PROVEN) because it has no Loki-executed check.sig_checked true), “Receipt unchecked” when it was not, and “receipt unmeasured” when the receipt carries no check result.loki-ts/src/engine10 into loki-ts/src/util. Behavior is unchanged; the engine core is back under its 5,000-line cap (4,719 lines).test.yml prints the coverage table (bun test --coverage). It replaces the removed coverage.yml and enforces no floor.docs/v10/cp-redesign images: only files listed in ALLOWED-IMAGES.txt may be committed there, and the pre-commit hook rejects an unlisted staged image (tests/test-cp-redesign-images.sh).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.
loki status shows the current or latest Loki 10 run through one renderer shared with loki engine10 status; schemas/status-result.schema.json rewritten to match.loki report serves kpis only and exits 2 on anything else (LEGACY-ZERO W1-02).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.
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.
Could not resolve: "../../../../../schemas/loki-yaml.schema.json".claude -p (D86, FC-04, L1): with no model configured the claude session is invoked with no --model flag (the SDK path sets no model), so the provider default runs instead of a pinned sonnet; SessionRunOptions gains an optional effort passed to the provider only when set; an explicit sonnet or haiku pin (LOKI_MODEL_OVERRIDE, LOKI_CLAUDE_MODEL_DEVELOPMENT) is printed on the start line and recorded as downgrades on run.started (key only when non-empty). receipt.model reads “claude (provider default)” when unpinned. The label is never passed as a model: fix escalation goes up from the provider default (opus), and the cheap auxiliary stages that stay pinned (plan fast tier, Wall, already-done check) are listed in downgrades and printed on the start line. Known FC-04 sibling: loki start prd.md still goes to the legacy run.sh route, whose default is sonnet (not changed here).LOKI_E10_FIX_RESUME=1 (default off): in the Loki 10 engine on the Agent SDK, a fix round resumes the previous session (implement, then each fix round) with the failure output and prior diagnosis as the new turn, keeping the agent’s context and prompt cache. It resumes only on the same model; escalation (Engine Law L1), a missing session id, a non-SDK provider or the CLI invoker, or a resume error starts a fresh session as before and records fix_resume: fresh|fallback (<reason>). The stage data and the receipt cost block record fix_rounds with per-round fix_resume and cache-read tokens for the Parity Gate A/B. Verdicts are unchanged: seal reads only execution evidence and the engine-recorded mode, never the resumed transcript..loki/project.json, keyed by a hash of the model-named manifest and lockfiles plus the shallow directory set, and the key is recorded in the intake stage data (project_model). New typed API in loki-ts/src/project_model/api.ts (packageRootOf, commandFor, hasUI, uiBoot); consumers are wired in EL-W1-03. Default OFF until a consumer exists: LOKI_E10_PROJECT_MODEL=1 enables it. A cached file is re-validated on every load, fingerprint files must be regular files inside the repo, a provider failure is reported as owner=provider, and the session limit is the remaining intake budget so discovery fails open.tests/test-legacy-dashboard-removed.sh fails if the old UI directories exist, are listed in package.json “files”, would ship in npm pack, or are still referenced outside CHANGELOG.md, docs/ and a shrink-only allowlist (with a --self-test mode), and tests/test-cp-fresh-install-smoke.sh installs a fresh npm pack tarball, starts the Control Plane on a loopback port and checks /health and its UI./v1/audit/summary, /v1/checkpoints, /v1/checkpoints/{id} and, on the loopback-only action path, POST /v1/checkpoints and POST /v1/checkpoints/{id}/rollback (loopback peer, loopback Host and JSON required, a pre-rollback snapshot taken first, every call audited); audit data stays read-only, unmeasured summary fields are null, and the legacy /api/enterprise/audit* and /api/checkpoints* paths now map onto them (the legacy POSTs now also require a loopback JSON request, a CP token alone does not reach a restore)./v1/tasks, /v1/memory/{episodes,patterns,skills,economics}, /v1/context and /v1/focus read the same .loki/ files, return null instead of a zero when unmeasured, and the legacy /api/tasks, /api/memory/*, /api/context and POST /api/focus paths map onto them (focus only accepts this checkout or a registered project, and only from a loopback JSON same-origin caller). Task create/update/delete, memory consolidate/retrieve and the SQLite memory backend stay 501.POST /v1/control/{pause,resume,stop,council-review} (a fixed action list that only writes the .loki/PAUSE, STOP and council-review signal files run.sh already reads, never a command, loopback JSON plus same-origin, audited) and read-only /v1/council/{state,verdicts,convergence,report,transcripts}; the legacy /api/control/* and /api/council/* paths map onto them (stop now writes the STOP signal only and no longer signals the process)./ now returns a 410 page naming the Control Plane, and loki dashboard open prints the Control Plane hint instead of opening a browser. The browser UI is the Control Plane (loki ui).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.
/v1/metrics, /v1/cost/snapshot, /v1/cost/timeline, /v1/fleet/runs and /v1/fleet/summary read the same .loki/ and registry files, return null with a “not measured” marker instead of a zero, and the legacy /metrics, /api/cost, /api/cost/timeline and /api/fleet/* paths now map onto them (the legacy /metrics now requires the same auth as other legacy routes).secrets.controlTokenKey from your existingSecret), compose requires LOKI_CONTROL_TOKEN and publishes on 127.0.0.1, and the ECS module requires control_token_secret_arn unless allow_insecure_bind is set, so no deploy path runs the Control Plane open; the image now builds the Control Plane in its own stage and ships only the bundle, UI and migrations, workers ship to it with the same token, and persistence.controlDb.enabled keeps its database across restarts.loki control serve, probed on /health and /ready) instead of the legacy dashboard; the Service and compose host port stay 57374, config.dashboardPort and dashboard_port remain as deprecated aliases of the new config.controlPort and control_port.<verdict> (unattested); the Overview VERIFIED tile counts plain VERIFIED only, with a separate signature-not-checked tile; the boot recompute is chunked and re-runs when the key set changes. A parity test, including signed cases, keeps the CP checks aligned with loki verify.outside stated scope: <file>. The unit-mode write set flags the same way. Secrets, lockfile, .loki/ and Wall-file drops are unchanged.loki control prune --repo OWNER/NAME | --before ISO_DATE [--dry-run] removes fixture or unwanted runs (with their events and orphaned sources) from the local Control Plane DB in one transaction, with or without the server running; DELETE /v1/runs/:source/:run and a confirmed Remove button on the run page do the same for one run, each recorded in a new audit table. Clean up leaked fixture runs with loki control prune --repo acme/widget.LOKI_E10_CASCADE=1 opts in to the sonnet downgrade, prints model downgraded by cascade: X -> Y (opt-in) and records it in the implement and fix stage data. A repeated fix failure escalates up to the strongest model (never below the run model) with the full failure output and the prior diagnosis before STALLED. A static guard test fails if any stage pins a weaker model without the opt-in.util/run_outputs.ts), prefers the issue’s own acceptance criteria, and a genuinely absent source still reads “not recorded”. A golden test built from the FireLater#17 recording and a guard test pin both directions.effectiveVerdict and VerdictBadge in the design primitives derive it (also in /v1/stats), with a guard test against hard-coded verdict strings.loki merge list|add|run [--dry-run] and loki review --risk --json. Server routes GET/POST /v1/merge/queue, POST /v1/merge/run ({dryRun} required boolean) and GET /v1/review/risk (pr, staged=1 or since) run the CLI with execFile and a fixed argv, allowlisted arguments, a timeout and an output cap; every mutation and refusal is audited. A real merge needs an explicit confirm; an unparseable or failed risk run reads “not measured”, never 0.--cp-accent-solid, --cp-accent-ink, --cp-error-ink, dark --cp-info-ink) used by Button, Badge and NavItem, plus the legacy Settings button on sky-700; KNOWN_CONTRAST is now empty (33/33 e2e). Receipts tests pin the Badge tone per verdict, and the copied-envelope test now uses a real engine --export-dsse envelope, so removing runIdGuard turns it VERIFIED rather than UNCHECKED.ui/src/pages/run-controls exports RunControls ({source, run, status}) for the run header slot. Stop asks for confirmation and is shown only for a running run; Resume only for a BLOCKED run; Retry for a finished one. Actions update optimistically, roll back on failure and show the server refusal text as returned (409 busy, 501 retry unavailable, pid refused).ui/src/pages/plans renders a matrix per run (requirement from issue.json, plan step, files changed from diff.patch and the receipt, linked checks, verdict) at #/plans/<source>/<run> with a run picker at #/plans. A criterion with no passing linked check, or any non-VERIFIED receipt, reads NOT PROVEN, never green.Control Plane UI end-to-end tests (CPE-23): packages/control-plane/test/e2e/cp-ui.spec.ts runs headless Playwright against a loopback stub API built from the CP-00 fixtures (axe WCAG A/AA scan of 8 pages in light and dark, Cmd+K, nav, Cmd+Enter keyboard path, 375 px drawer and no horizontal scroll, element captures, computed-token parity against a committed legacy dashboard baseline in test/e2e/legacy-baseline/). The scan found real dark-theme contrast failures (primary Button, error and info Badge, active NavItem) and a legacy Settings button; they are annotated known-a11y, not suppressed. bunfig.toml keeps bun test off the e2e specs.
ui/src/pages/board is a kanban derived from GET /v1/runs with Issue, Running, PR open and Verified / Not proven columns (a verdict decides first, then a PR url, then a live or started run); each card links to #/runs/<source>/<run>. It replaces the old Work page at #/work once registered.GET /v1/providers reports each provider CLI (claude, codex, cline, aider, opencode, plus deprecated gemini) as installed or not with its version from a timeout-bounded --version probe, which auth env var NAMES are set (never values), and the model tiers from providers/model_catalog.json; behind the token guard. New Models and providers page (settings area).POST /v1/runs accepts; Cmd+Enter starts the run, an optimistic row shows while it starts, and 400, 403 and 409 answers are worded plainly.GET /v1/runs and actions (New run, Toggle theme), with arrow-key navigation and ARIA combobox/listbox semantics. Shortcuts: Cmd+N opens a new run, Cmd+Shift+D toggles the theme, Esc closes, and Cmd+Enter dispatches the loki:composer-submit window event for the composer to handle. Mounted in App.tsx through the CPE-02 setCommandPaletteHandler hook.GET and PUT /v1/config read and edit the repo loki.yaml on the loopback-only router (real-peer check, token guard, Origin check, audited). Input is validated against schemas/loki-yaml.schema.json, values that look like secrets are refused (env var names only), writes keep comments (yaml Document API), are atomic (temp file plus rename) with a loki.yaml.bak backup, use a sha256 ETag with If-Match (stale gets 409), and a symlink resolving outside the repo is refused. The Configuration settings page has one form card per schema section.GET /v1/runs and refreshed from the runs-list stream, one Settings entry) plus a hash-routed outlet, built only from the CPE-01 tokens and primitives with a mobile drawer. Pages register through ui/src/pages/registry.ts (inSettings pages appear behind Settings); the import-repo empty state and Live run view stay reachable, and Cmd+K is left as a hook for the command palette.GET /v1/stats/cost?group=day,model,repo,provider aggregates the runs table with measured, partial and unmeasured runs kept separate (unmeasured never counts as $0) plus token totals, and a Cost and usage page with bars and a budget banner (“no cap” for subscription runs, the $100 default per-run cap for API-key runs, per D82).ui/src/pages/run shows a run as a chat-like thread with stage messages, a live log (events page plus the /v1/runs/:s/:r/stream SSE), diff and receipt from the artifacts API, NOT PROVEN reasons, cost (a missing cost reads “not measured”), PR link, a BLOCKED reply prompt, a Details drawer and a header slot for the run controls.GET /v1/stats?since= returns run counts, VERIFIED rate, cost with measured and partial kept apart (an unpriced run is never summed as zero) and the receipts count, behind the token guard; the Home page shows KPI tiles (runs today, verified rate and cost over 7 days, BLOCKED waiting), recent runs and a BLOCKED inbox, with unmeasured values reading “not measured”.POST /v1/runs/:source/:run/verify runs the engine’s own receipt verifier in process, read-only, and returns VERIFIED, UNSIGNED, TAMPERED or UNCHECKED with the reasons; GET /v1/keys returns the public signing key (JWK) only. Both are loopback-only (real peer check, JSON, token guard) and verify writes an audit row. The Receipts page lists finished runs with an in-place Verify, the public key and the verified-rate trend.#/runs/<source>/<run>, and live refresh from the /v1/stream SSE feed.GET /v1/runs/:source/:run/stream and GET /v1/stream are Server-Sent Events fed from the store (so events shipped by the watcher appear within a second), with a 15s heartbeat, Last-Event-ID resume, a cap of 32 concurrent streams (429 beyond it), slots freed on disconnect, and the existing bearer token guard.GET /v1/notifications derives BLOCKED runs awaiting an answer, finished runs with their verdict, tampered logs, ingest conflicts and cap hits straight from stored runs and events (each links to its run, nothing is invented), and GET /v1/audit lists the actions table newest first, paged. Both sit behind the existing token guard.GET /v1/runs/:source/:run/events?after=&limit= pages a run’s events, and GET /v1/runs/:source/:run/artifact/<name> serves a fixed allowlist (issue.json, plan.json, receipt.json, receipt.md, report.md, task.md, diff.patch, evidence/*.png) from .loki/runs/<run>/. It is loopback-only and behind the token guard; the run and source must be a known ingested run with a discovered local repo, the realpath must stay inside the run directory (traversal, absolute names and symlink escapes are refused), and files over 5 MB are refused.POST /v1/runs (with /v1/start kept as an alias) moves into routes/start.ts and spawn.ts, keeps every loopback, Host, JSON, 409 and env-scrub guard, adds an Origin check, and accepts optional provider and budget (mapped to loki start --provider/--budget), model (sent as LOKI_SESSION_MODEL) and workspace (runs loki workspace run), each validated against an allowlist; every start is recorded in the audit table.<run dir>/run.pid (pid, process start time, argv) and removes it on exit; the loopback-only, audited POST /v1/runs/:source/:run/stop|retry|resume endpoints SIGTERM only a pid whose file belongs to that run, is alive and has the recorded start time (PID-reuse guard, never by name), retry re-starts the recorded issue ref, resume spawns loki answer <run>, and an unsupported retry returns 501 with a reason.local_repos and actions tables with a migration, an audit helper, and local discovery that fills local_repos (never /v1/ingest). GET /v1/repos now returns project display names only (no home paths) behind the same loopback guard, and /v1/start accepts a unique display name as well as a known path.Control Plane design system (CPE-01, D83): packages/control-plane/ui/src/design/ ships the light and dark --cp-* tokens extracted from the legacy dashboard (light-grey ground, Fraunces/Inter/JetBrains Mono, Tailwind preset) and 19 primitives (Card, KpiTile, Badge, Pill, StatusDot, Button, Input, Chip, Table, NavItem, GroupHead, Timeline, Message, EmptyState, Spinner, Toast, Dialog, Drawer, Kbd). Text uses AA-safe -ink and text-subtle companions where the legacy light status and muted hex fall below 4.5:1.
tests/run-all-tests.sh, scripts/local-ci.sh and the bun preload (loki-ts/tests/preload.ts, also used by packages/control-plane) export HOME as a temp dir via tests/lib/hermetic-home.sh (real HOME kept as LOKI_REAL_HOME, toolchain homes pinned, LOKI_RUN_TMP never exported), so no suite can write the real ~/.loki (control, dashboard registry, answers, keys) or ~/.gitconfig. _loki_control_ui now honors LOKI_CONTROL=0 and never starts a detached control serve under it. New guard tests/test-hermetic-home.sh.loki start owner/repo#N line (no env-var instructions), and a Start form (owner/repo#N or a plain task, repo picker from the project registry) posts to a loopback-only, strictly validated endpoint that spawns loki start with an argv array. BLOCKED runs were already answerable inline..loki/runs/*/events.jsonl, so live and already-running runs appear without LOKI_CONTROL_URL or loki control backfill (set LOKI_CONTROL_AUTOINGEST=0 to opt out).loki dashboard start|open (with or without flags), loki web, the host side of loki docker start and the run.sh auto-open now open the Control Plane instead of the legacy Python dashboard. The legacy server is reachable only with LOKI_LEGACY_DASHBOARD=1 (or LOKI_CONTROL_DEFAULT=0, or --host/--tls-* flags); LOKI_NO_BROWSER=1 is respected everywhere.Control Plane UI: a live run view (stage timeline with elapsed time, heartbeat file count, cost, model, and outcome with PR and receipt links) is now the landing page while a run is active, and an Overview page (runs today and this week, VERIFIED/PARTIAL/FAILED counts, total cost, PRs opened, last 10 runs) otherwise. Values the events do not carry (stage limits, diff +/-, merged PRs, unpriced cost) read “unmeasured”.
--test-isolation=none; the fixture now uses that flag only where Node supports it (test-only, product fail-closed behaviour unchanged).
--max-cost and budgets.per_run in loki.yaml override both, and a cap hit still ends the run BUDGET_STOP (exit 3).
@playwright/test) is installed in the repo, visual evidence now also records a video and a trace of the changed pages; both are hashed into the receipt and listed as “video:” and “trace:” lines in the PR body Evidence section. When it is absent the run records “playwright e2e video and trace skipped: …” under NOT PROVEN and continues (docs/visual-evidence.md).
loki merge add <pr>..., loki merge run [--dry-run], list, clear: a serial PR merge queue that merges a PR only when gh pr checks is green, rebases the next PR onto the new base (gh pr update-branch --rebase) and re-checks it before merging; red or unrebasable PRs stay queued and the exit code is nonzero. --dry-run lists what would happen and changes nothing (docs/merge-queue.md).
loki review --risk (or loki review <pr-number> --risk) prints a deterministic 0-100 diff risk score with a per-factor breakdown (files touched, sensitive paths, size, test delta, deleted tests); --json for machine output. Same diff always gives the same score.
autonomy/mobile-verify.sh [dir]: when the product is a React Native, Expo or Flutter app, runs its e2e tests on a booted Android emulator or iOS simulator. With no device it prints a “mobile tests: NOT VERIFIED” line and exits 3; it never prints PASS without a run (see docs/mobile-verify.md).
loki acp runs Loki as an Agent Client Protocol agent over stdio, so ACP-capable editors (VS Code, JetBrains, Zed) can drive loki quick and stream its output; a non-zero exit is reported as NOT VERIFIED. Setup in docs/acp-editors.md.
loki start shows one live status line (for example [wall] writing acceptance checks 1m02s (plan done 14s)) that rewrites in place and is cleared before the summary; piped output gets one plain line per stage that runs longer than 3 seconds.grep ROW >> SAME_FILE, which GNU grep refuses (“input file is also the output”, rc 2, nothing appended), so the writer saw an unduplicated row on Linux and the R2-3 duplicate-refusal check failed; the hook now copies through a side file. The writer in scripts/cloud-dispatch.sh was already fail-closed (T101-G04-DUPROW).printf | grep -q under pipefail, so grep exiting on its first match SIGPIPEd printf and read as “region lacks _loki_with_github_tokens” under CPU load (32 of 12000 checks failed at –cpus=1 with busy loops); it now matches with a bash pattern and no pipe. The P9 [default] and [planted] failure messages also carry the start rc and a tail of start.out and start.err.\s is now [[:space:]] and the problem, acceptance-criteria and technical section extractors use sed -E alternation instead of BSD-unsupported \|, so macOS no longer silently falls back to first-paragraph and grep heuristics; GNU output is unchanged and pinned by a new case in tests/cli/test-issue-to-pr.sh.loki docker start owner/repo#N no longer exits immediately on macOS Docker Desktop: the container now trusts /workspace despite the bind-mount ownership, and a non-zero container exit prints the last error line plus a fix hint and propagates the exit code.loki start accepts a Sentry issue: an exported Sentry JSON file (works offline) or a Sentry issue URL becomes a task spec with the error, stack trace and breadcrumbs. The API is contacted only when SENTRY_AUTH_TOKEN is set, and the token is never printed (docs/sentry-intake.md).TRUST_CORE_PROBE_MODE=anchors for tests/test-trust-core-tests-detect.sh checks in about 0.1s that every probe_case find-string still matches its file, naming each stale anchor, and local-ci fast tier now runs it (issue #214 option 3).scripts/cloud-dispatch.sh (G-04): dry-run-by-default dispatcher that prints the exact claude --cloud command and BOARD row for a ready slice, and refuses on file-set overlap with in-flight rows, a reached or unknown usage governor max, or a not-ready or dependency-blocked row; --live is explicit and unexercised.loki help now lists keys and workspace in its “All commands:” block, and the discoverability test checks membership in that block instead of anywhere in the help text..loki/PAUSED.md no longer says “Press Enter in terminal” when stdin is not a TTY; it now uses the bash no-TTY wording “(no TTY: keypress resume unavailable)” (issue #212).loki memory index rebuild now calls MemoryEngine.rebuild_index, prints the real count of indexed memories, and exits nonzero with the error on failure instead of emptying the index and printing “Index rebuilt” (#204).loki --help on the Bun route now lists every routed command (kpis, report, trust, crash, contract, start, slack, engine10) and the stale “8 highest-traffic” header is gone, guarded by cli_help_routes.test.ts.loki quickstart no longer reports simple-todo-app as the “top match” for briefs that match no template (for example “a URL shortener with click stats”): it prints the match score, adds url, shortener and stats keywords, and falls back to a plain-spec build from the brief when nothing matches (#217).node --test with process isolation, so coverage comes only from a node --test run that loki-seal launches itself (D80 amendment 2): the test script must be node --test plus allowlisted flags (built-in reporters spec, tap, dot, junit, lcov only) and plain relative file paths, and loki-seal spawns the node binary directly with NODE_OPTIONS, NODE_PATH, NODE_TEST_CONTEXT and npm_* removed and no npm config read, so a PATH shim, a package or node_modules reporter, npm_config_node_options, a user ~/.npmrc or a quoted project .npmrc key cannot forge a pass; any other script, a pretest or posttest script, jest and vitest are NOT VERIFIED with a stated reason (red and green still count), and a test file calling process.exit, reallyExit, abort or kill is NOT VERIFIED (D80) (an unterminated write cannot hide the real summary behind a forged one, and a flood cannot drop it); not caught: a child that writes forged runner-protocol messages, a forged complete summary printed after the runner exits, go test, and runners with no summary line.loki workspace status lists an unreadable run as “unreadable” instead of hiding it (WS-INTERRUPT).
loki stats and loki status print “unmeasured” instead of $0.00 when no iteration records cost_usd or budget.json has no budget_used, on both the bash and bun routes (S-226r).
loki workspace run, status, metrics and PR comments through both autonomy/loki and bin/loki (D51-B16r).-I -S), so a json.py committed in the repo being built can no longer make a NOT VERIFIED proof render as “Headline: VERIFIED”./api/proofs returns an error row for a bad proof.json and a 503 for an unreadable proofs directory, and the memory patterns, episodes, skills and index endpoints return 503 instead of [] (HONEST-READ-1).loki verify --fast no longer exits 0 when it cannot give a verdict: nothing scanned (INCONCLUSIVE) exits 3, an unknown flag exits 64, and a missing root directory exits 2 (P2-FV-EXIT).loki verify no longer lets a planted json.py in the reviewed tree forge its recorded gates (a dependency audit with high CVEs could read as pass); every inline Python call site in verify.sh runs isolated (-I -S, or -E with the cwd stripped from sys.path), and the syntax gate’s py_compile runs isolated too (S-216r).git status call now ignores a repo-local core.fsmonitor, the untracked cache, any configured filter.* clean/smudge/process driver (including names containing “=” or non-UTF-8 bytes), and partial-clone lazy fetch, and returns non-zero without running status when git is older than 2.44 or does not honor its config overrides, so an agent-written .git/config cannot run a command inside the snapshot step (S-218r).loki start refuses to resume the recorded agent branch because checking it out would overwrite one of your gitignored files, the warning now names that file and the branch where the earlier commits stay, instead of only saying the branch “could not be checked out” (S-233)./api/proofs reports cost_partial: true) instead of presenting a lower bound as a complete total (PO-STANDALONE-COST-1).loki help answer documents loki answer [<run-id>] [--text "..."], and loki help verify now describes --export-dsse with its exit codes (0, 1, 2, 4, 66) (PO-HELP-1).npm view --prefer-online) until npm serves the published version before the Release run can succeed, so Post-Release Smoke and Promote never run against a version npm does not serve yet; a never-listed version fails with NPM-LAG-TIMEOUT. The scheduled-audit gate also no longer reads a response without total_count as zero runs (NPM-LAG-b, D75b).scripts/train-cycle.sh refuses to cut a release while the previous release: vX is not visible on npm (logs PREV_NOT_ON_NPM, then PREV_NOT_ON_NPM_STALE after LOKI_TC_NPM_WAIT_MIN, default 45; an unreachable registry fails closed) and fast-forwards local main to origin/main after each release (NPM-LAG-a).renderPrBody (library renderer, not yet wired into the live PR stage, which still uses renderReviewerBody) can render a reviewer-first body: contract, criteria with files and passing check only, NOT PROVEN last, capped at 60 lines with a “+N more” line when cut; criteria files and checks are shared run-wide, not mapped per criterion (INTEL-3).loki verify <run> --export-dsse exports a verified v10 receipt as an in-toto Statement v1 in a DSSE envelope signed with the receipt Ed25519 key, and loki verify accepts that envelope (INTEL-1).loki workspace run records per-repo started_at and finished_at in integration.json, and autonomy/lib/workspace_metrics.py <run-dir> [--attention-min N] reports PRs per wall-clock hour and per attention minute; runs without timestamps (and a missing attention figure) print unmeasured, never zero (D51-B15, PO-WS-METRICS-1).moat-suite) is renamed “Moat rules (no regression)” so a green check reads as “no moat rule regressed”, not “moat proven” (S-231r).loki workspace run posts one PR comment per repo PR with the integration status and head SHAs when a PR exists on the run branch (no LLM, best effort, opt out with LOKI_WORKSPACE_COMMENT=0) (D51-B11).budgets.per_run in loki.yaml, or --max-cost); a run that reaches the cap ends BUDGET_STOP with exit code 3 (INTEL-2).loki verify now exits 2 with attestation: UNCHECKED and a VERDICT that is not VERIFIED when a well-formed attestation carries a kid no local key matches (it used to exit 0), matching engine10; a missing or unusable .loki/state/last-proof-id.txt with proofs present reports receipt: NOT VERIFIED instead of skipping the check. Cross-machine check: loki verify --pubkey FILE <run-id> for a Loki 10 run receipt (it does not read a legacy proof.json; for that, put the signer’s public key in LOKI_RECEIPT_RETIRED_PUBKEYS). A tree with no proofs prints receipt: NONE and is not failed for it (D76, A-121c). Standalone bash autonomy/verify.sh (no _deploy_receipt_verdict) now fails the same way (rc non-zero, never VERIFIED) for a forged or unsigned attestation instead of printing UNCHECKED and passing.loki serve --help and the docs now point to loki or loki control serve for the UI; loki serve stays a deprecated alias of loki api start (the dashboard API, not the Control Plane) (D65-UI-NAMING).loki control help and the bash fallback now match the bun route: the Control Plane is on by default, LOKI_CONTROL=0 turns it off, and the fallback names bun as required instead of demanding LOKI_CONTROL=1 (D65-UI-NAMING-2).loki with no arguments now opens the Control Plane (set LOKI_CONTROL_DEFAULT=0 for the previous dashboard).loki verify catches an altered image (needs Playwright and changed pages; set LOKI_VISUAL_EVIDENCE=0 to turn off; the API HTTP transcript for openapi repos needs an explicit LOKI_VISUAL_EVIDENCE=1).python3 dashboard/audit.py verify <dir> now exits 2 (status nothing_checked) when it checked no files, instead of exiting 0 like a verified chain; a valid chain still exits 0, a tampered one 1, and tip is unchanged (PO-AUDIT-CLI-1).running_agents: null (unmeasured) instead of a claimed 0, and the notification triggers panel shows “Could not load triggers” instead of “No triggers configured” when the triggers file is unreadable (GET /api/notifications/triggers returns triggers: null plus error; a missing file is still an empty list) (PO-DASH-HONEST-1).run.sh session commit no longer swallows a failed git add -A (it now warns “Left uncommitted” and commits nothing, instead of the silent clean no-op; git’s benign exit 1 for an ignored .loki is still tolerated), and _loki_untrack_agent_committed_user_files now keeps files the agent force-staged instead of dropping them with a global git reset -q (S-219, S-220, PO-SESSION-COMMIT-1).loki control status shows runs as unknown when the control plane cannot be read.loki workspace run per-repo runs start (D65-BUG7)..loki/contract.json) is frozen at intake: seal traces only that copy, records its sha256 in receipt.contract.sha256, and an edit, deletion or late creation after intake shows as a NOT PROVEN line; the verdict never changes (D65-SPEC-F2).docker build no longer fails with Could not resolve).loki legacy (the pre-v10 engine) is deprecated as of v10.0.0 and remains fully supported; no removal date is set. Set LOKI_ENGINE=legacy to pin it. See docs/v10/GUIDE.md (E-35).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”.
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.
train/100 green after full Tests rerun
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.
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).
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.
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.
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.
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.
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.
| fix(cli): D58 loki start <issue ref | issue URL | “task”> takes the v10 path, minimal routing only |
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.
unrelated edit reverted: <file>. Test files, new files and files you had already modified are kept. When the planner produced no scope (small tasks), nothing is reverted and the receipt says so.LOKI_CONTROL_URL is set, plus loki control. Off unless configured.loki UI test (v10.6.3); planned for next week.loki start <issue ref> routed to v10: withdrawn after it regressed moat property P9 (v10.6.5); planned for next week.Not published. Its loki start routing change regressed moat property P9 and was withdrawn; everything else shipped in v10.6.6.
Not published. Its CI hit a transient npm registry 404 on a dependency tarball; the same content shipped in v10.6.5.
Not published. The dashboard port fix failed the bare loki UI test and was withdrawn; everything else shipped in v10.6.4.
Not published. Its loki start routing change failed the test suite, including three moat properties, and was withdrawn; it returns in a later release.
Not published; shipped in v10.6.2.
unrelated edit reverted: <file>. Test files, new files and files you had already modified are kept. When the planner produced no scope (small tasks), nothing is reverted and the receipt says so.LOKI_CONTROL_URL is set, plus loki control. Off unless configured..loki/dashboard/url), and the banner and browser opener use it instead of a guessed port. loki ui and dashboard start reuse a running dashboard only when it belongs to this project. Test runs no longer leave dashboards on ports 57374-57399.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.
npm i -g loki-mode now depends on the per-platform @oven/bun-* packages instead of the bun meta-package, so no postinstall script has to run. Windows resolves bun.exe from the @oven package, and the old baseline pins are gone.scripts/real-repo-gate.sh): a clean npm install of the candidate, then loki start against three real repositories with a 15 minute cap, before latest may move. Its first run against 10.5.35 failed all three repos on routing, which is the next fix.scripts/train-cycle.sh) no longer cancels a train whose required checks are still running by cutting a newer one.loki start runs v10, the dashboard prints the URL it actually bound, scope control, clean install, and the real-repo gate.loki start routed to v10 (with --no-pr and loki <file.md>), scope control, the Control Plane live view, and the dashboard port-leak fix. A first dashboard port fix was reverted from this release after it broke three test suites.npm view can lag a publish by up to about 30 minutes; the release pipeline waits rather than failing.A next release.
A next release. loki start now always opens a working dashboard instead of a “dashboard_frontend_not_found” error page.
loki start could open the browser on a page showing {"error":"dashboard_frontend_not_found", ...}. A dashboard left running from another install (or from a test run) was reused because it answered its health check, even though it could not serve the UI. Now a running dashboard is reused only when its /health reports the same version and the same install path as this loki, and its home page actually returns HTML; otherwise a fresh dashboard starts on a free port (P0-DASH-STATIC).index.html): the installed package, then LOKI_SKILL_DIR, then ~/.claude/skills/loki-mode, then a local dashboard-ui/dist build. Before this, an empty or partial directory earlier in that list was chosen and the UI could not load (P0-DASH-STATIC).npm install -g loki-mode@latest), instead of developer JSON (P0-DASH-STATIC).loki dashboard start no longer reports “port already in use” just because a browser still holds a closed connection to that port; only a listening process counts (P0-DASH-STATIC)./health now reports the dashboard’s version and install path.search-command module with a test that imports it) now finds that evidence and can stop as already satisfied, instead of re-implementing the feature (D50-F5).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.
bun.lock), and .loki/. If nothing of the run’s own work is left, no commit is made and no pull request opens. Before this, the backstop ran git add -A, so leftover setup changes (for example npm install output in tracked files) could be committed and pushed in a pull request for a run that changed nothing. A worker that is stopped mid-run still gets its own edits committed, as before (D50-F4).git hash-object --no-filters), so a clean filter configured in the repository can never see the supervisor’s GitHub token (D50-F4).A next release. Stopping a Loki 10 run with Ctrl-C or SIGTERM now returns your checkout to the branch you started on.
loki/e10-... branch and a session process could keep running after the supervisor exited. The run branch and its commits are kept. If the run left uncommitted edits to tracked files, the checkout stays on the run branch, so those edits are never carried onto your branch, and one line names the run branch and the git checkout command to return. Nothing is reset or discarded (D50-F1c).eval/loki10/archive/ output directory and its test checks that the directory is created when absent; a test harness for the planned workspaces feature (D51 Phase B, behind LOKI_WORKSPACES) is registered with the test runner. No user-facing behaviour changes.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.
git add -A and a “loki: backstop commit”) in the repository. No run branch had been created yet, so the user’s modified and untracked files were committed onto the branch they were on, usually main. The backstop now runs only after intake has completed and the run’s own branch exists. A worker killed after intake still gets its work backstop-committed, as before (P0-backstop-refused).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.
loki why --json (schemas/why-result.schema.json) and loki status --json (schemas/status-result.schema.json), validated in tests against real command output, each with a sample that must be rejected (D48 row 5).npm install -g loki-mode --omit=optional, without the bundled bun): every Loki 10 command then prints the “cannot run on this machine: no working bun” line and runs the legacy engine. Homebrew installs need bun installed separately (brew install oven-sh/bun/bun) (E-170).A next release. A Loki 10 run whose commit stage fails no longer lands a backstop commit or opens a pull request.
git add -A with hooks off), so files that were already modified before the run, Wall files and stray lockfiles are not committed, and no pull request is opened for that run. A worker that is killed before reaching the commit stage still gets its work backstop-committed and a draft PR, as before (A-104c).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.
:(top)x/file is not read as a pathspec and committed (A-104b).packages/b lockfile no longer rides along with a packages/a manifest change (A-104b).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.
claude -p calls, the merge-conflict resolver and the review calls) read stdin from /dev/null. A verbose run (LOKI_VERBOSE=1) started with a stdin pipe that never closes no longer blocks the provider. Calls that feed the prompt through stdin are unchanged (A-134c).LOKI_RUN_TMP when set (A-04b)..githooks refuses a commit in the main checkout whose author email differs from the repo-local user.email, so a fixture commit made from the wrong directory fails loudly; LOKI_ALLOW_FOREIGN_AUTHOR=1 overrides it (E-153).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.
loki actually prints (“the Loki 10 engine cannot run on this machine: no working bun”), and the bun leg’s fallback check also catches “Running the legacy engine instead”. v10.5.25’s promote failed only on this text check (every other check on both legs passed), so latest was not moved. A test now fails if the gate and bin/loki stop sharing that text.Dockerfile, Dockerfile.sandbox) install bun 1.4.2, matching the npm optional dependency; a structural test keeps the two pins equal (E-169).cd DIR (with &&, || exit or || return) or git -C DIR before judging a detached checkout, so a checkout inside a linked worktree is allowed; an unresolvable target, cd X || true, or a cd inside a subshell that has closed is judged by the session’s directory and stays blocked (E-161b).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.
@oven/bun-<os>-<arch>). Measured cost: about 62MB on macOS arm64; published sizes are 79.5MB for linux-x64, 69.3MB for macOS x64 and 86.1MB for Windows x64. Platforms bun does not support skip it without failing the install (P0-nobun).loki looks for a working bun on PATH, then the bundled copy (node_modules/bun/bin/bun.exe, or the platform package’s bin/bun when install scripts were skipped), and accepts one only if bun --version succeeds within 2 seconds. A bun-less or broken machine runs the legacy engine and prints this as the first line on stderr: loki: the Loki 10 engine cannot run on this machine: no working bun (...). Running the legacy engine instead. To fix: install bun from https://bun.sh, or reinstall loki-mode without --omit=optional. LOKI_ENGINE=v10 with no working bun still exits 1. The probe adds about 10ms to commands that route to the engine, and none to loki --version or loki status (P0-nobun).loki verify, at most 15 output lines, a skipped target test must not read VERIFIED, and the fallback line must name the reason). latest moves only when both pass (E-167).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.
loki start in the repository root. The MiroFish and funnel-privacy tests run loki start in a throwaway git fixture with a stub provider that exits at once, under timeout -k. A new structural guard (tests/test-no-start-against-repo-root.sh) fails when any test starts loki start or run.sh from the repo root, including "$LOKI" start, bin/loki start, bare loki start and a cd back to the root; an allow comment covers only the line directly below it (E-165).run.sh posting project_dir to 127.0.0.1) is not egress; a telemetry call with telemetry off still fails both zero-egress checks (E-165).test-*.sh, run-*.sh, run_*.sh, test_*.py, *_test.py); helper scripts under tests/lib get only the syntax check (E-168).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.
LOKI_JIRA_EMAIL, new) and API token (LOKI_JIRA_TOKEN) to the Jira client. When only some of LOKI_JIRA_URL, LOKI_JIRA_EMAIL and LOKI_JIRA_TOKEN are set it logs one line naming the missing variables (never their values); with none set it stays silent. The setup docs list LOKI_JIRA_EMAIL and the known-issue note is removed (E-166).git checkout --detach, a tag or SHA, git switch --detach) in the primary checkout, and agent sessions are also blocked from other checkouts, hard resets, stash, clean and removing that checkout (E-161).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.
loki keys export prints the public receipt-signing key as an Ed25519 JWK (kty, crv, x, kid, alg, use). It never prints the private key (D48).loki verify --pubkey FILE <receipt.json|run-id> (v10) checks a receipt’s signature against the supplied public key (a JWK from loki keys export, or PEM) and never the local key set, so someone without your machine can check your receipt. Exits 0 VERIFIED, 1 TAMPERED, 2 UNCHECKED (the key’s kid does not match, or the key file is unusable), 3 UNSIGNED, 4 run outcome not verified. A receipt with no signature is refused with 3 even with --allow-unsigned, because the caller asked for a signature check; the sealed event log is checked against the supplied key too (D48).keys export is listed in loki help and in shell completions.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.
loki "<task>", loki owner/repo#N (issue mode) and loki quick "<task>" (D48 rows 7-9). Each prints one start line, Loki 10 engine (set LOKI_ENGINE=legacy or run 'loki legacy' for the previous engine), then Outcome, PR, Receipt, NOT PROVEN, Cost and Time (at most 8 lines). loki quick runs the engine’s lean small-task path (E-64) and opens no PR. With no bun, or with a provider the engine has no invoker for, these entry points fall back to the legacy engine. loki start, loki status and loki dashboard are unchanged.loki legacy <args> and LOKI_ENGINE=legacy run the previous engine exactly as before; both are now covered by routing tests.LOKI_E10_INVOKER=cli, the stub provider) records cost 0 with the source marker cli-invoker-unmetered in the cost event, receipt and efficiency record instead of null, and lists cost unmetered (CLI invoker; recorded as 0) under NOT PROVEN, so every run has a non-null cost without presenting 0 as measured.scripts/first-run-gate.sh) now runs the v10 engine for loki quick: output budget 8 lines (was 15), a start-line check, a non-null cost check, and the legacy skipped-target check runs under LOKI_ENGINE=legacy.loki verify with no arguments or with a Loki 10 run id (e10-...) checks the sealed receipt with the v10 verifier. Any other argument or flag (--fast, --pr, --json, a path) keeps the legacy verify. A sealed run whose outcome is not VERIFIED exits 4, checked before the unsigned-receipt rule, so --allow-unsigned cannot turn a failed run into exit 0; a receipt whose outcome cannot be read also exits 4 (D48).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.
loki quick run fails, the tail it prints is the last three [ERROR] or [WARN] lines of the run log (falling back to the last three lines), each capped at 200 characters, instead of an unanchored log tail (A-134b).loki quick hands stdin back to the inner run only when stdin is a terminal, so a run started from a pipe no longer blocks a provider that reads inherited stdin (A-134b).autonomy/verify.sh on its own prints attestation: UNCHECKED (run via loki verify) instead of omitting the attestation line (A-134b).slice-* branches run the fast Tier A gate plus the suites selected by the diff instead of the full 8-shard Tests matrix. A newer push to the same train/* branch cancels the superseded Tests run. main and train runs still run the full matrix, and main runs are never cancelled (E-162)./usage reading. The read times out, kills its whole process group and falls back to the recorded readings when it cannot parse the result. This affects only the development swarm and is not part of the CLI (E-163).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.
/start dashboard API resolves provider binaries and credential file names only from fixed allowlists, so request input can never choose the program that is run or the file that is written. This closes CodeQL alerts 604-607 (command-line and path injection) (P0).loki quick exits 3 when the tests_integrity check fails: a skip was added, the test runner config was changed to deselect tests, or a test file was weakened. Before, it printed Evidence Receipt: NOT VERIFIED and still exited 0. Module-level pytestmark skips are caught. On Python 3.10, where tomllib is missing, only the [tool.pytest.ini_options] section of pyproject.toml is compared, so a dependency edit is not read as a test-config change (A-118, D47).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.
loki with no arguments starts the local dashboard and opens /start: choose a provider (a logged-in claude or codex CLI is detected), connect GitHub with a personal access token (stored at ~/.loki/credentials/github, file mode 0600, never logged or returned by the API), pick a repo, then select issues and press “Complete selected” or “Complete all”. Each issue runs as a v10 issue-mode run in its own worktree and branch, with live status (queued, running, PR open, BLOCKED with its question, failed). --no-open, LOKI_HEADLESS=1 or a non-interactive shell print the URL instead of opening a browser; LOKI_LANDING=1 keeps the previous landing text (D51-A12).loki command or flag the CLI does not accept, or shows an old version as current (DOC-02).127.0.0.1, localhost or [::1], which blocks DNS-rebinding pages from driving it. /health and /metrics stay open for probes. A dashboard bound to a non-loopback address accepts other hosts only with enterprise auth enabled or hosts listed in LOKI_DASHBOARD_ALLOWED_HOSTS (wired into the Helm chart and Terraform module) (D51-A12).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.
loki backlog owner/repo --all | --label X | --issues 1,2,3 runs a v10 issue-mode run per issue, each in its own worktree and branch, with a concurrency cap and a daily budget stop, and prints one line per issue plus a summary. Exit 0 only when every issue ended with a PR or VERIFIED. --dry-run lists what would run (D51-A3).loki.yaml (repo root, then ~/.loki/loki.yaml) configures provider and models, the NAME of the env var holding a GitHub token, repos, concurrency, budgets, knowledge sources and notifications; loki config validate checks it against schemas/loki-yaml.schema.json. An annotated example is in docs/loki.yaml.example (D51-A3).LOKI_SLACK_WEBHOOK_ENV (default LOKI_SLACK_WEBHOOK_URL). The URL is never logged, and a slow or failing webhook never fails a run (D51-A4)./plugin marketplace add asklokesh/loki-mode, then /plugin install loki-seal@loki-mode (D48).next release that passes Post-Release Smoke and the first-run gate on the exact npm-installed version is promoted to latest automatically; a lower version is never promoted (D49).loki verify refuses a receipt with no signature: exit 3 on the v10 engine and BLOCKED on the legacy path. --allow-unsigned or LOKI_VERIFY_ALLOW_UNSIGNED=1 accepts it with an explicit “integrity not attested” line. A malformed signature reads TAMPERED on both engines, and the check cannot be disabled by a base64.py or json.py committed in the verified repo (A-121b).log.sealed line, so deleting the end of the event log after a tamper was detected now reads TAMPERED in loki verify instead of VERIFIED. Receipts sealed by earlier versions still verify. A run interrupted before it completed reads UNCHECKED (exit 2) (A-117).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.
loki quick no longer prints stray stderr lines (the caveman output-compressor bootstrap notices, a .loki/config “File exists” error, and an echo: write error: Broken pipe), so the default output stays within its line budget. The bootstrap notices go to the quiet run log instead (P0-t15)..loki/config is a leftover file holding only the disclosure marker (DISCLOSURE_SHOWN=true), it is folded into the .loki/config/ directory instead of failing. A config file with any other setting is left untouched, and ~/.loki/config (for example one holding TELEMETRY_DISABLED=true) is never moved, even when loki quick runs from your home directory (P0-t15).os and signal; a missing or broken python3 falls back to plain bash (P0-t15).Reason: line redacts GitHub tokens, Anthropic, OpenAI and AWS keys, Slack tokens, JWTs, Google AI keys and Bearer tokens before it is cut to 200 characters, so a credential echoed in a failing check’s output never reaches the terminal. The patterns match the receipt redactor in autonomy/lib/proof_redact.py (A-130b).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.
sum.test.js to sum.spec.test.js and weakening an expectation) now seals PARTIAL with weakened test: <file> under NOT PROVEN; previously the entry was listed but the verdict could still be VERIFIED (A-119).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.
--verbose restores the stage lines; --json is unchanged. Every outcome other than VERIFIED or ALREADY_SATISFIED prints a Reason: line (for example the failing check, the spec-conflict question, “provider credit exhausted”, or “engine ended before sealing a receipt”), and NOT PROVEN is never printed empty (A-130).loki doctor computes the model catalog age in UTC on both the bash and Bun routes, so the two routes agree at any local time of day (E-158).Receipt: TAMPERED; previously it could report VERIFIED and exit 0 (A-130).CLAUDE_PROJECT_DIR, so they run from any working directory (founder queue 13).slice-* branches with per-branch cancellation; main and train runs are never cancelled (D46).next releases shipped in the trailing hour (D46).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.
--json prints one object with ok, outcome, stop, run_id and receipt_sha256. A BLOCKED run posts its one question, and names an opened draft PR when there is one (A-110).loki quick prints its Evidence Receipt after the final commit, so the printed head and digest match what loki verify checks; it is quiet by default, with --verbose (or LOKI_VERBOSE=1) for the setup chatter, and its headline names the unsigned state and the not-proven count. Legacy loki verify reports the receipt’s signature as VERIFIED, UNCHECKED, TAMPERED or UNSIGNED, and a tampered receipt is BLOCKED everywhere, including evidence.json (A-134).loki doctor blocks only on the selected provider and ends with one line: Ready: <provider> (<model>), <receipt signing state>, or the blocker (A-123).pre red in NOT PROVEN, but only when the run proves progress on the task’s own target tests. The task’s relevant tests are always run, whatever files the diff touched (A-112, A-114).not_run, never a pass (A-111).--resume is removed from v10 (exit 2) (A-113).pytest.exit, or deleted; a test-configuration change (conftest.py, pytest/jest/vitest config) is disclosed in NOT PROVEN (A-112, A-114, A-115).loki doctor: the bash and Bun routes name a missing Node.js identically, and a not-yet-created signing key reads “receipts will be signed on first run”.loki quick path.~/.loki/keys, and the test runner fails loudly if a test switches the parent checkout’s branch (E-154, E-155, E-154b).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.
packages/loki-seal: a Claude Code plugin whose Stop hook blocks the agent from finishing while tests are red or were weakened (fewer tests than the session baseline, a crashed or empty run). It subtracts tests that were already red at session start, releases after repeated hook errors so a broken hook never traps a session, and ships an advisory skill and a demo. It is installed from a clone of this repository (A-04).~/.loki/keys/receipt-ed25519.pem, file mode 0600) so receipts are signed by default. A failed permission change never silently downgrades a receipt to unsigned; the server path never auto-generates a key (A-120).docs/README-FULL.md, with retracted claims removed (A-09, D45). The package description is the same category line.loki quick on an existing repository: HANDOFF.md is written under .loki/, the USAGE.md instruction and regeneration are skipped, and a lockfile that did not exist at base is not committed when no manifest changed. New-project builds are unchanged (A-132).node --test is detected as node:test, so the Wall no longer writes Jest-style tests into it (A-102).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.
next dist-tag. latest, the Docker :latest image and the Homebrew formula move only through a separate promote step, and only after the first-run gate passes on that exact version and its published gitHead is on main (A-01)..loki/ are left out of the commit (A-104).loki verify uses the same receipt canonicalizer as the sealer, so a receipt containing non-ASCII text no longer verifies as TAMPERED; loki verify --help prints usage; the full 64-character receipt digest is printed (A-101).-q addopts is still seen, a pytest.exit banner reads as not run, and a missing pytest reads as not run instead of red; the classifier lives in the audited core (E-125, E-132).scripts/first-run-gate.sh: a first-run gate on a throwaway HOME that checks the default entry point on a one-line bug repo (tests fully green, only the fix changed, printed digest equals the verified one, signed receipt, at most 15 lines of output, wall time). It runs as a non-required CI job and gates promotion (A-02).scripts/release.sh --check-clean refuses a release commit that leaves a stamped file modified, and the stage list prints git add -f for ignored paths (E-151, E-152).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.
~ are dropped like absolute ones (S41-10b).lg-*) tasks must declare tier large and carry per-file provenance, sha256-frozen hidden tests and a requirements map naming each requirement’s tests; the harness applies every saved shortcut patch and requires it to still fail, and requires RED at the reference commit to be a real assertion failure (EV-12E). Medium tier adds pub-dotenv-661.~/.loki; the probe-isolation test sizes its inner timeout from a CI measurement and names the case that was running when it fires (E-147, E-148)./tmp paths into a run-owned directory (E-143).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.
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.
scripts/cleanup-test-processes.sh no longer removes loki-*, test-*, package or *.tgz entries under /tmp or $TMPDIR by glob; it reports them and removes only its own run-owned directory, and only with --aggressive. A glob sweep had been deleting other agents’ live run directories (E-140).tests/test-no-tmp-sweep.sh bans glob removal of temp-root entries across scripts and tests (E-140).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).
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.
LOKI_E10_TRIM=1 (off by default): v10 engine sessions shorten oversized tool results as they arrive. Bash output over 200 lines keeps its first 40 and last 120 lines, Read keeps 400 lines and Grep keeps 100 matches, and the head and thresholds halve from tool call 25 on. A Bash result always keeps its last 120 lines so a failing command’s tail is never cut, because the SDK exposes no exit code to the hook (S41-11).node_modules keyed on the lockfile, package.json, Node version and platform, stored under .loki/cache/install and capped at the 3 newest entries. The install cache is no longer copied into each worktree’s .loki, which removed a 2.3 second copy per worktree with a 71 MB cache (E-130).scripts/release.sh --bump-only refuses to continue when a dist source map records an absolute path or one that climbs out of the repository, which a symlinked node_modules produced at v10.5.4 (E-133).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.
loki modernize: verifying a pre-sealed Wall now fails when the normalizer hash is missing on either side, and sealing refuses an oracle with no normalizer hash, so two missing values never count as a match (E-124).loki modernize re-slicing refuses a re-slice that is not strictly smaller than its parent, so a flaky check cannot turn the depth budget into retries (M-18).loki modernize pre-sealed Wall mode: the oracle and target conformance tests are sealed before any conversion exists and re-verified before use (M-14, not yet wired into the full modernize run).loki modernize re-slices a non-equivalent unit to at most depth 2; if any child is still not equivalent the unit is NOT PROVEN, never forced to pass (M-18, not yet wired into the full modernize run).LOKI_E10_PREFIX=lean (off by default): v10 engine sessions use a short fixed system prompt in place of the Claude Code preset, for a smaller cached prefix; tools, CLAUDE.md loading and permission hooks are unchanged (S41-09).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.
loki modernize: verifying a pre-sealed Wall now fails when the normalizer hash is missing on either side, and sealing refuses an oracle with no normalizer hash, so two missing values never count as a match (E-124).loki modernize re-slicing refuses a re-slice that is not strictly smaller than its parent, so a flaky check cannot turn the depth budget into retries (M-18).loki modernize pre-sealed Wall mode: the oracle and target conformance tests are sealed before any conversion exists and re-verified before use (M-14, not yet wired into the full modernize run).loki modernize re-slices a non-equivalent unit to at most depth 2; if any child is still not equivalent the unit is NOT PROVEN, never forced to pass (M-18, not yet wired into the full modernize run).LOKI_E10_PREFIX=lean (off by default): v10 engine sessions use a short fixed system prompt in place of the Claude Code preset, for a smaller cached prefix; tools, CLAUDE.md loading and permission hooks are unchanged (S41-09).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.
.gitleaks.toml; with no release tag it uses the scanner’s default rules and refuses any .gitleaks.toml (E-114). Before, a pushed zero-rule configuration could disable the scan for that same push..gitleaks.toml change check cannot run, instead of treating the error as “unchanged” (E-119).loki modernize equivalence checker (M-13, not yet wired into the full modernize run): compares captured behaviour of the original and the converted code under normalizers sealed with the oracle before any conversion exists. A missing capture, malformed value, runner error, too few cases, or any mismatch with the sealed normalizer hash is reported as NOT PROVEN, never as equal.eval/loki10/scorecard): per model tier completion, cost per completed and p50 time with raw alongside (S41-02); per-stage token baseline in docs/v10/METRICS.md (S41-04); the swarm pulse recognises slice ids such as S41-01 (E-121).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.
LOKI_ENGINE=v10): when implement stops with a specification conflict, the receipt carries the reason in receipt.json and shows it on one line in receipt.md. The text is flattened to a single line, length-capped and rendered as inline code, so a model-written reason cannot add headings or a fake verdict to the receipt; receipts without a conflict are unchanged (E-120).npm audit (DEP-05).scripts/board-mark-released.sh marks BOARD rows released once their merge is in a published tag (E-90); the pulse expands dependency ranges such as E-98a..e (capped at 50 ids) (E-117); real Seal tests for the SPEC_CONFLICT verdict (E-116).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.
.gitleaks.toml in the pushed range is refused before any skip option applies unless LOKI_ALLOW_GITLEAKS_CONFIG_CHANGE=1 is set, and even then the scan uses the previous configuration, so a pushed config cannot switch the scan off. A missing scanner refuses the push unless LOKI_ALLOW_UNSCANNED_PUSH=1 is set, which never overrides a finding or a scanner error; a scan that times out refuses the push.scripts/metrics-usage-append.py (internal swarm tooling): appends an hourly usage-governor snapshot (5-hour and weekly output tokens, per-engineer burn, output by model and role, max engineers) to docs/v10/METRICS.md.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.
.venv, then venv, then an in-repo $VIRTUAL_ENV, then python3, then python) and ruff the same way (E-98a). Before, it called python, so on hosts with only python3 every test check was skipped and no fix round ever ran.def, async def and class names, so planning finds the relevant files in Python repositories (E-98d).partial-stream), instead of leaving the run’s cost unmeasured (E-98e).@anthropic-ai/claude-agent-sdk 0.3.267 (and the Docker image 0.3.208) while the engine was built and evaluated on 0.3.283; all pins now match, with a test that fails on any drift (E-106).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).
loki modernize <repo> --to <target> reaches the v10 modernize command under every engine setting, including LOKI_ENGINE=legacy (M-08). The existing loki modernize heal and loki modernize migrate commands, and loki modernize --help, keep their current behaviour.scripts/usage-governor.py reads Claude Code usage from the local transcripts, counts each streamed message once, and projects the 5-hour and weekly windows to their real reset times (daylight saving aware). At 85% of the window or 90% of the week it recommends zero additional engineers.~/loki-ci-logs/eval/<run>/ and eval/loki10/archive/. scripts/prune-worktrees.sh never removes a worktree that has a live process, a commit in the last 30 minutes, or unarchived eval results.scripts/release.sh --bump-only stops before building when loki-ts/node_modules is missing, and restores loki-ts/dist and exits non-zero when the build fails or does not embed the new version (E-102, E-103). Before, a failed build could leave the tracked dist files deleted..gitleaksignore, and nothing else is suppressed.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.
loki modernize <repo> --to <target> reaches the v10 modernize command under every engine setting, including LOKI_ENGINE=legacy (M-08). The existing loki modernize heal and loki modernize migrate commands, and loki modernize --help, keep their current behaviour.scripts/usage-governor.py reads Claude Code usage from the local transcripts, counts each streamed message once, and projects the 5-hour and weekly windows to their real reset times (daylight saving aware). At 85% of the window or 90% of the week it recommends zero additional engineers.~/loki-ci-logs/eval/<run>/ and eval/loki10/archive/. scripts/prune-worktrees.sh never removes a worktree that has a live process, a commit in the last 30 minutes, or unarchived eval results.scripts/release.sh --bump-only stops before building when loki-ts/node_modules is missing, and restores loki-ts/dist and exits non-zero when the build fails or does not embed the new version (E-102, E-103). Before, a failed build could leave the tracked dist files deleted.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.
loki modernize at docs/v10/GUIDE-MODERNIZE.md (M-30): what it does, every flag, a worked example and its current limits. A test keeps the guide in step with the CLI: it fails when the guide names a flag the CLI does not parse, omits one it does, or shows a modernization id the CLI would reject..gitleaksignore, the same way CI scans, so a push is no longer refused for an old finding that the tip already allowlists, and an earlier commit cannot carry its own allowlist past the scan. GITLEAKS_CONFIG from the environment is ignored, any change to .gitleaks.toml is refused unless LOKI_ALLOW_GITLEAKS_CONFIG_CHANGE=1 is set, and the pinned gitleaks binary is checked against a per-platform sha256.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.
loki modernize <repo> --to <target> command and flag parsing on the v10 engine (M-07). It rejects a repo path that does not exist instead of reporting success. The conversion pipeline behind it is still being built; this release adds the entry point and its help text.eval/loki10/tasks/ or eval/loki10/refdiff/ run a checksum-pinned gitleaks v8.30.0 over every pushed commit and refuse on a finding, naming the file, line and rule but never the secret. Install it with scripts/install-gitleaks.sh.Test guard and planning docs. No change to how loki runs your builds.
tests/test-dep-inventory.sh now also runs the dependency inventory’s self-test with no GitHub credentials, no gh on the path and an empty home directory, so a self-test that quietly reaches the network fails on a developer machine the same way it would on a CI runner (E-93; guard for the main-branch failure fixed in v10.2.2).docs/v10/SCALE.md: what the Claude Max plan sustains for the engineering swarm and what larger headcounts would cost on API billing, with every figure labelled as measured or estimated (G-03).docs/v10/METRICS.md: the medium-tier gate result for the Loki 10 engine against raw claude -p (EV-14): raw completed 12 of 14 runs, the v10 engine 10 of 14, so the default engine stays legacy.Eval tooling release: one size rule for the medium and large eval tiers. No change to how loki runs your builds.
eval/loki10/measure-size.py (D34): measures every tiered eval task from its committed reference diff, counting only non-test Python product files and added non-blank lines (deletions never count), and exits 1 when a task misses its tier’s bar (medium: at least 2 files; large: at least 4 files, at least 150 added lines and above the largest medium task), when a reference diff is missing, or, with --online, when the committed diff no longer matches upstream.eval/loki10/refdiff/, and a new eval/loki10/test-harness.sh section that runs the size gate with negative controls.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.
.claude/worktrees is flagged (E-81).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.
scripts/dep-inventory.py and docs/v10/DEPS.md: an inventory of every dependency Loki Mode uses (7 npm/bun manifests, Python requirements and pyproject files, 24 GitHub Actions workflows and 2 composite actions, the Node, Python and Bun CI matrices, Docker base images, both Helm charts, Terraform providers and the Homebrew formula), each with its current and latest stable version, the bump class (patch, minor, MAJOR, or 0.x breaking), runtime end-of-life dates, and floating action tags resolved to the release they actually point at. It drives the dependency upgrade slices (DEP-01, D35).actions/checkout@v4 and actions/setup-node@v4 run on the deprecated Node 20 runtime; both Helm charts carry an appVersion that no longer matches the product version.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.
bin/loki, records its path and the agent SDK version, and refuses to run when loki-ts/node_modules does not match bun.lock (E-62).origin/main reflog, so a plain git push counts (E-84); the repo guard also refuses moving main without a checkout (E-78).Loki 10 engine (still opt-in: LOKI_ENGINE=v10).
loki modernize groundwork, not yet wired to the CLI: deterministic codemods first (futurize, OpenRewrite) or a recorded skip (M-16), strangler targets and routes.json (M-19), dashboard view (M-24), Java graph that records every unresolved import (M-04).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.
autonomy/lib/modernize/py_imports.py lexes source with tokenize, so it reads Python 2 files (print statements, backticks, old except X, e syntax) that ast rejects, and loki-ts/src/engine10/modernize/lang/python.ts turns the result into the dependency graph the planner clusters. It resolves absolute, relative and bare relative imports (from . import X, from .. import X), and records imports it cannot resolve instead of dropping them.autonomy/lib/modernize/py_capture.py runs a module’s functions under Python 2 or 3 and records inputs, return values and raised exceptions as type-tagged JSON with branch coverage, so a migrated module can later be checked for identical behaviour. Supported values are tagged faithfully, including sets and frozensets (sorted), Decimal, the datetime family and bytearray.namedtuple, IntEnum and OrderedDict, is recorded as not proven with its type and call site. It never compares equal, so the equivalence oracle cannot certify behaviour it did not actually capture.Loki 10 engine improvements (still opt-in: LOKI_ENGINE=v10; the default engine is unchanged).
LOKI_E10_CASCADE=0 restores the previous behaviour. Not yet re-measured on the full 29-task arm, so the release gate numbers in v10.0.0 still describe the default configuration.no_change_needed expected outcome, for tasks where the feature already exists (EV-13).loki modernize groundwork, not yet wired to the CLI: event types and state paths, inventory, dependency clustering with an iterative SCC that handles 100,000-node chains, up-front estimate (M-01, M-02, M-05, M-06).scripts/release.sh --bump-only no longer rewrites dist bundles whose only change is the debugId (E-72).loki modernize its own 4,000-line cap; core code may not import it (D33, E-76).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.
tests/test-start-update-hint.sh uses 999.0.0 as the far-future latest, so the stale-install warning test (and the trust-core baseline that runs it) stays meaningful across major versions.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.
| 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 |
loki modernize (Part 2).LOKI_ENGINE=v10): cost path for small tasks (plan call skipped, Wall on sonnet with a short brief, sized “normal” when the repo map is truncated) (E-45); first-run preflight with actionable refusals (E-36); runs from the npm package via a static module registry (E-32); --resume through the supervisor (E-46); detached deep verify after the PR and Slack notify (E-48); issue-ref runs create their run dir before the fetch (E-58).LOKI_TS_ENTRY and LOKI_E10_* knobs to the v10 arm (E-52) and reaps arm processes that escape the timeout group (EV-10).scripts/local-ci.sh fast tier runs gitleaks over the push range when installed (E-60).claude -p 27/29 (93.1%), $0.2363 per completed task, p50 39s; legacy 15/29 completed (26/29 hidden tests pass, no PR opened), cost not measured. v10 on 5 tasks: full design 4/5 at $0.595 per completed, Wall off 5/5 at $0.398. The v10 full arm has not run; the default stays legacy..gitleaksignore; none is a real credential. The gitleaks step failed on the v9.79.0 and v9.80.0 release commits, so those two tags were never published to npm. This release carries their content (see the v9.80.0 and v9.79.0 entries below).LOKI_ENGINE=v10): the real entry runs end to end (supervisor and worker, PR opened only by the supervisor with the pinned origin); the temporary run.ts glue is deleted (E-42).LOKI_ENGINE=legacy with a --tasks subset (E-38); eval PR path regression test (E-51).LOKI_E10_LIVE_REPO (E-40).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).
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).
LOKI_ENGINE=v10 entry point (E-12). With LOKI_ENGINE unset, every command routes exactly as before; with v10, multi-word tasks, issue refs, status, verify and dashboard reach the new engine, which is not complete yet in this release.timeout -k 10, so a script that ignores SIGTERM is killed (E-00). Two such orphans had been running for over 24 hours.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).
/api/focus (S-195).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).
-I -S; voter agents, proof-check, PRD checklist verification and Bun loki proof verify run the resolved isolated interpreter, so a user-site .pth file cannot change a verdict (S-196, S-202, S-203, S-204, S-205).LOKI_AUTO_PR refuses to push a branch whose history still holds user files an agent committed, and the advisory path prints the cleanup step first; the record survives until the history no longer holds those files (S-194).python3 -E, so cryptography supplied through PYTHONPATH is not seen (S-197).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).
-I -S), so a user-site .pth file can no longer change them; an unmeasured sycophancy score on a unanimous approve now runs the devil’s advocate (S-200, S-201)._loki_snapshot_py_tool copy under autonomy/ against run.sh’s (S-199).loki verify codes that stay pending until v10.0.0 (S-182); the R3 cost design doc describes project totals as partial-aware (S-190).v10-ops worktree-budget prints how many worktrees a batch may open (S-193); the guard blocks a glob rm in a shared temp or scratchpad root, relative paths included (S-181).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).
/api/cost reports tokens without a USD figure as unmeasured cost, not $0, and an unknown model as unknown (S-178); loki cost shows unknown spend as not recorded instead of 0.0 (S-179).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).
pass:true as green when failed_count is above zero (S-157).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).
/api/cost/timeline and /api/proofs carry per-run cost_partial, so a partly priced run renders as “at least”, never as a total (S-158).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).
loki metrics --json reports null for tokens, iterations and time saved that were never recorded, instead of invented numbers; an empty tracking file no longer marks tokens as known (S-147).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).
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).
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).
scripts/release.sh --bump-only also updates the web-app Footer version, which had been stuck at v9.55.0 (S-144).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.
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).
--add-dir directories reach the provider again, and two more CI-only test bugs are fixed, so the quarantine list is empty (S-110).pytest -n auto again, now that the launch race behind the earlier failures is fixed (S-111).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).
.loki/quality/ablation.json says the checks are not load-bearing; the ablation facts are copied verbatim onto the receipt (S-113, moat P8).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).
scripts/v10-pulse.sh serves npm and gh results from a 90s on-disk cache refreshed in the background, so the per-prompt hook runs in about 0.2-0.5s instead of 2s or more (and never blocks on the network). Stale values are labeled with their age; missing values stay UNKNOWN (S-104).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:
spawn_tracked read the child’s /proc/<pid>/environ before exec had finished and failed closed with “provider attempt lineage marker is unavailable” (exit 127) under load. The child now signals readiness after exec, and the launcher waits for it (30s cap, fail closed). Linux, 2x load: 20 of 20 test runs failed before, 0 of 30 after (S-102).XDG_CONFIG_HOME and GH_CONFIG_DIR, so the planted hosts.yml is where gh actually looks. On GitHub runners two P9 checks had been passing without measuring anything (S-107).LOKI_NO_BROWSER=1, CI, a test runner or a non-TTY. loki migrate had opened its dashboard with an unguarded open (S-103).scripts/release.sh refuses any version bump unless Tests and Bun Parity passed on that exact commit, and gains --bump-only (S-108).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).
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:
tests/quarantine.txt lets a listed suite run without blocking, with an owner, an issue and an expiry no more than 7 days out. Expired entries, moat suites and review suites are rejected (S-92, Part C item 14).learning/suggest.sh emitted invalid Python for its verbose flag; a dashboard hook test had aged out of its own 7-day window; a Cline e2e test hard-failed without the CLI; loki welcome never confirmed that the telemetry opt-out took effect. 8 suites were silently exiting under set -e at their first counter increment (S-93, item 15).tests/test-review-assurance-tail.sh dispatches only the requirements verifier for its contract-only cases, about 1.9x faster per case (S-95).Moat P7 (no fabricated data):
Release train 6: no shipped GitHub Action step runs the agent while holding a token.
Security (S-54, BACKLOG 139):
GH_TOKEN, GITHUB_TOKEN, GH_ENTERPRISE_TOKEN and GITHUB_ENTERPRISE_TOKEN: “Install loki-mode” and “Cleanup” in action.yml, and “Run Quality Review” in .github/actions/review/action.yml. None of them used a token (verified: loki version, loki stop and loki review without --pr make no GitHub call). The root action’s “Post review as PR comment” step keeps its token.tests/test-action-agent-step-no-token.sh discovers every composite action by glob and treats any step that invokes loki (bare, via npx with a version, by path, through a variable, after if/while/!/timeout/env wrappers, in a pipe or subshell, or through a repo script) as an agent step that must blank all four token variables.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):
gh config, the git credential helper, SSH agent and GIT_SSH_COMMAND.core.fsmonitor no longer sees the start-of-process credentials.core.sshCommand, a repo-local credential helper and insteadOf never run with credentials. Trusted gh calls run from / with an explicit repo.LOKI_AUTO_PR and the delegated PR push only to a github.com origin, and only to the origin the repo had when the run started. GitHub Enterprise and GitLab are planned for M9. The local branch no longer gets an upstream set.Release (S-84, Part C items 3 and 5):
required-ci reuses the parent’s Tests and Bun Parity success when the parent-to-release diff only bumps versions. Every allowlisted file is compared byte for byte apart from the version literal. The source map is compared as JSON without debugId. CHANGELOG.md may only gain one inserted version entry.needs: gate and gate’s duplicate pytest are removed.Tooling (D27):
.githooks/pre-push is now an identity check, bash -n and the red-main warning: 0.8s instead of a full serial pytest (S-97).scripts/v10-ops.sh push-main pushes without a pipe and succeeds only when git ls-remote matches HEAD (S-98).Release train 4: council readers hardened, a Tier A fast test gate, and a release script that bumps every file.
Security:
-I -S, so a planted user-site .pth can no longer forge a verdict. An unreadable count fails closed, and devil’s advocate vetoes when no isolated interpreter exists. A byte-identity test keeps the council’s copy of the interpreter resolver equal to run.sh’s (S-49, BACKLOG 134).CI:
scripts/select-tests.sh picks the tests for a diff (syntax always, the changed tests, tests that reference or import the changed source, bun/typecheck for loki-ts, dashboard and dashboard-ui suites, moat properties in both directions) and falls back to the full suite on anything it cannot classify (S-91, Part C item 13).Release:
scripts/release.sh bumps every version-bearing file in the checklist and preserves file modes, with tests/test-release-sh.sh registered in the runner (S-16).Docs: a 60-line slice-card template and the dispatch-from-card rule in docs/v10/SWARM.md (S-77, D26 guard 6).
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:
loki ci no longer forks echo | cut once per finding. The full ARG_MAX-scale argmax fixture now finishes in 4 to 9 seconds (it took 326s on the pre-fix code, which also still crashes with “Argument list too long”), so the 2400s per-suite override is gone (S-79, Part C item 1).tests/shard-durations.tsv) instead of index % 4; a coverage test proves every suite runs exactly once (S-81, item 4).timeout-minutes: 10 (S-82, item 6).update-homebrew depends only on release, not on the Docker publish (S-83, item 7).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):
scripts/v10-guard.sh, a PreToolUse Bash hook, blocks kill-by-name or kill-by-pattern, force-push, reset --hard on main, git add -A/git add ., a commit that drops a BOARD.md row, rm -rf outside the worktree and temp roots, and a VERSION write outside the release path. 114 fixtures; two HIGH reviewers over four rounds (S-73).scripts/v10-ops.sh runs the orchestrator’s trivial operations (clean check, status, commit-message template, BOARD row status flip) as a script instead of an agent. Atomic write, mode and symlink preserved, and it refuses rather than edit a cell it cannot place by position (S-74).scripts/v10-pulse.sh gains AGENT_OVER_BUDGET, CI_CANCELLED_STREAK and the merged-unreleased age check, and an UNKNOWN CI state no longer hides any other violation (S-71, S-75).CI:
bun=latest, the macOS bun job and hyperfine move from the push gate to nightly (S-90).Fixes:
.loki/state/agent-committed-user-files.z is cleared by a later session with no hits (S-68, BACKLOG 102).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).
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:
claude --help capability probe receives the live (token-withheld) environment.tcp) that silently never matched a bind; it now uses tcp4, so confined builds really cannot bind Docker or dashboard ports.Honest verdicts and receipts:
loki proof verify distinguishes TAMPERED from FAILED using the attestation result, not only the hash.first_result_verified_patch no longer claims a verification that never ran.TARGET_DIR is a subdirectory.tests_ok=inconclusive instead of True for an inconclusive test axis.error outcomes as failures.Dashboard and web app:
unknown instead of guessing completed for stale or unrecorded runs; a historical session no longer shows the live project’s checklist.Moat and tests:
Array.from generator rows and module-level fallback tables.claude-opus-5-5.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).
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:
claude CLI (used for a few narrow provider-agnostic checks) are now off by default. Set LOKI_ALLOW_CLAUDE_SIDECALLS=1 to restore the previous behavior if you rely on it. This closes an air-gap/sovereignty gap: a non-Claude setup could still quietly reach Anthropic’s API for these side-calls.loki-mode session from terminating unrelated Claude Code sessions) is now applied to more surfaces that had the same unscoped-kill pattern: loki web stop/start, the verification runtime’s teardown, and a GitHub Actions marketplace-action cleanup step.PROJECT_DIR environment variable was unset: council_augment_from_managed_memory (read-only import) and council_should_stop’s shadow-write step (real code execution, the more severe of the two) both fell back to the agent’s own working directory instead of failing closed. Both now fail closed (no-op) when PROJECT_DIR is unset; production is unaffected since the CLI always sets it..pth file in user-writable Python site-packages, a fake awk binary placed earlier on PATH, and finally a fake git/interpreter shadowing the snapshot’s own writer and reader steps (not just the seal’s digest computation). All three are now closed; the digest, writer and reader all resolve their tools by fixed, verified absolute paths first, using pure shell string handling with no unguarded interpreter or PATH-resolved helper program in the loop.Correctness fixes:
doctor --airgap judged provider locality (whether inference stays on this machine) with a bare substring match on the model id; a remote OLLAMA_HOST pointed at Ollama’s own cloud service still read as air-gapped. It now parses the actual host.proof-verify.py reported “tampered” (exit 1) for a proof it genuinely could not check (e.g. verified outside a git repository) instead of “could not check” (exit 2) – collapsing two different meanings into one signal.FAILED/TAMPERED instead of NOT CHECKED, and rendered attestation: false inconsistently between the local and remote verification routes.evidence-block.json could report a test axis as passing when it was genuinely inconclusive (no results recorded), if a different axis was the one actually blocking completion.hashlib.py/json.py in the agent’s own repo shadowing Python’s standard library and flipping a verdict, but only 2 of 5 readers using that hardening had test coverage exercising the attack. The other 3 (council_heuristic_review, council_evaluate_member, council_devils_advocate_review) are now covered by the same case.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.
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).
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.
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:
.gitignore cannot expose them to the session commit. A directory is
listed only when a pattern matches it, so a new agent file under a
directory that merely holds ignored files is still committed.--no-overwrite-ignore: git’s default silently
replaced a gitignored user file at a path the session branch tracks. On a
conflict a fresh session branch is minted instead.preexisting_modified (path only, never content) and still
never committed.P6.resume-does-not-sweep, P6.ignored-files-not-swept,
P6.preexisting-edit-disclosed, P6.resume-keeps-ignored-user-file,
P6.resume-after-interrupt-commits-agent-files. P6 stays proven.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.
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.
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.
loki proof verify --jwks was silently ignored on the default (Bun) route,
so no signature was ever checked there. Flagged calls now reach the verifier
that checks it.--jwks '', --jwk), an unknown option, a second proof id, or
-h/--help exits 64 with “Nothing was checked”; loki proof verify never
exits 0 without a verdict. -- ends options. Full help is loki proof help.hashlib.py or json.py, or a sitecustomize.py
combined with an empty PYTHONPATH component, printed VERIFIED for an
unsigned receipt and passed tampered receipts and over-budget chains. Every
verify-path interpreter (proof verify on both routes, the remote and
deploy verifiers, proof passport, proof chain and each chain stage) now
runs python3 -E and never imports from the checkout.loki proof verify follows the verifier exit contract: 64 usage, 66 unknown
id (they were 2 and 1, and 1 meant “tampered”); no python3 or a killed
verifier is 2 on both routes. loki proof chain -h (and argparse
abbreviations) exits 64 instead of 0. docs/exit-codes.md and
docs/SIGNED-RECEIPTS.md list every outcome.cryptography was labelled
UNSIGNED with advice to set a gpg key; it now reads NOT CHECKED.pass key as a
pass in four places. Only a boolean true counts now.loki doctor --airgap reported “air-gap ready” for opencode, cline and
aider with no local model configured, and read another provider’s model
variable to decide. It now judges only the active provider’s own variable.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/.
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.
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.
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.
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).
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:
### Added - SWE-bench Lite Benchmark Results (50 Problems) and its
“100% Patch Generation” claim. That 50-instance run was never re-measured, so
its true rate is UNKNOWN.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.
A quality gate was calling four real tests fake, and a guard nobody could clear was failing every push.
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.
./foo.js commonly names a
real ./foo.ts on disk; the extension in the specifier is the post-build one.
The check tried only the literal path. It now tries the literal first, then
the TS sibling, for .js/.jsx/.mjs/.cjs.await import('./x.ts') is bun:test’s documented pattern
for loading a module after mock.module() has stubbed an import. Only static
and require forms matched.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.
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.
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.
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.
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.
A toggle reported a quality gate as enabled while the gate skipped itself.
readToggles registered flag("LOKI_GATE_MAGIC_DEBATE", true), so with the
variable unset the orchestrator recorded magic_debate as ENABLED, while the
gate body short-circuits to pass unless the value is exactly "true". Unset,
"" and "1" all disagreed; "1" is the sharp case, because flag()
accepts it as true and the gate does not. The toggle now uses the gate body’s
own predicate. No gate outcome changes: every value that skipped still skips.
Checked across every other gate in readToggles and this was a singleton,
not a family.
The receipt verifier and generator disagreed about what a receipt meant.
proof-verify.py defined _is_exogenous and never called it, while the
generator filters by it. A failed ADVISORY gate therefore read VERIFIED to the
generator and NOT VERIFIED to the verifier, so loki proof verify told users
an honest receipt “was edited to misrepresent the facts”. Demonstrated on a
receipt fixture through both real code paths before the fix, and both now
agree on all five cases.
A test bound could not scale with the call it measured. The speculative
devils-advocate assertion used a bare elapsed_ms < 6000 while the call it
measures is dispatched with a budget this suite scales 4x on a sharded runner.
A first attempt scaled the bound by that budget; an adversarial reviewer
showed that permitted a 10s regression, and measuring settled it: the call is
concurrent and completes in 1498ms regardless of budget. The bound now scales
by the shard factor only, and the comment states the ceiling that remains.
Documentation claimed a gate blocks when it is advisory on both routes.
Five places, including a competitor-comparison document, listed the Magic
Modules Debate gate as “Yes (BLOCK severity)”. bash enforces it only with
LOKI_GATE_MAGIC_DEBATE_BLOCKING=true; the Bun route self-skips unless the
env var is exactly "true". The in-code comment records why it was made
advisory: 3 of 4 personas returned block on a deliberately thorough spec.
The README promised an install that doctor rejects. It said an API key was
enough while doctor marks jq and Node.js required and fails without them.
The dashboard local-caller check failed open on a forwarded value.
_real_client_host substitutes the left-most X-Forwarded-For entry when the
peer is a trusted proxy, and _is_local_caller then treated any host it could
not parse as an IP as local. A caller behind a trusted proxy could therefore
pass the local check by sending a non-IP token, and unknown is a literal
some proxies emit when the client address is unavailable, so this was
reachable without an attacker choosing the value. The direct-peer leniency is
deliberate and unchanged: ASGI test transports report testclient and UDS
transports report socket names. Only the forwarded path now fails closed, at
both the HTTP and websocket boundaries.
Scope, measured rather than asserted: LOKI_TRUSTED_PROXIES appears in no
deploy artifact, so the substitution only happens for an operator who set it.
But dashboard/Dockerfile does ship LOKI_DASHBOARD_HOST=0.0.0.0, so the
container is not loopback-only and the boundary carries real weight.
web-app/src unreachable from main.tsx, including
pages/ConnectionsPage.tsx, which rendered a complete deploy-connections
panel that the router imported zero times, so the route fell through the SPA
catch-all to NotFoundPage. The set was confirmed by extracting the previous
commit into a clean directory and checking the guard names exactly the deleted
modules, with neither over- nor under-reach.tests/test-web-app-no-orphan-components.sh: every module under
web-app/src must be reachable from the entry point. Deleted and unreachable
code is invisible to every other gate, so nothing else in CI could see this
class.tests/test-readme-lists-required-tools.sh: every tool doctor marks
required must appear in the README prerequisites. Demonstrated by adding a
sixth required tool to doctor and watching the guard go red.loki-ts/tests/magic-debate-toggle-agreement.test.ts: the toggle and the gate
body must agree for unset, "", "true", "1" and "false". It asserts the
two real predicates against each other rather than a hardcoded table.[ "$_passed" -lt 20 ] threshold became eight individually named assertions,
one per audited manifest plus the SDK. A count cannot say which manifest went
unaudited, and it picks up slack it was never meant to have: drop one
manifest, add two assertions elsewhere, and the threshold still passes while a
shipped manifest goes unscanned.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.
Budget spend readers read a key nothing writes. loki_remaining_budget
(autonomy/lib/claude-flags.sh) and its byte-mirror remainingBudget
(loki-ts/src/providers/claude_flags.ts) read current_spend. All six
production writers of .loki/metrics/budget.json write budget_used, so
spend resolved to 0 forever and --max-budget-usd received the FULL cap on
every call instead of the remainder – the per-call backstop never tightened
as spend accumulated. Reachable only for users who set LOKI_BUDGET_LIMIT
explicitly, i.e. exactly the cost-conscious users who asked for a cap. Both
readers are fixed together (they are byte-mirrors; fixing one alone would
make the two routes disagree), with the legacy key kept as a fallback.
The 80% budget notification never fired. check_budget_threshold read
used from a sub-dict whose key is budget_used. Verified: 0 notifications
at 85% of cap against the exact state run.sh writes. The in-loop 80%
log_warn was unaffected, so terminal users did still get a warning; the
notification half had simply never worked.
The quality-gate notification read a file nothing writes.
check_quality_gate read .loki/state/quality-gates.json; all seven repo
references to it are readers, and proof-generator.py already records this
as issue #125 and works around it. It now reads
.loki/quality/gate-failures.txt, the artifact run.sh actually writes.
An expired deploy token rendered as “never connected”. The server has
always returned {"connected": false, "error": "Token expired or revoked"};
the client had zero readers of that field, so a revoked token looked
identical to a platform never set up, with no way to fix it in place. The
blindness was structural: ConnectionStatus is declared twice and
ConnectionCard is typed against the component-local copy, so adding the
field to only one declaration compiles and changes nothing on screen. Both
declarations now carry it, and the card offers Reconnect.
A comment quoting a call site broke two test shards. A comment in
run.sh reproduced the literal call-site expression while explaining it. One
suite COUNTS that string and asserts exactly one occurrence; another MUTATES
it to prove a valve’s guard goes red. Both broke. The comment now describes
the call site instead of reproducing it.
Live cost visibility. check_budget_limit has always computed cumulative
spend every iteration and printed nothing below 80% of cap. It now reports
the running total each iteration, reusing the already-computed value rather
than adding a second reader. Unmeasured runs print nothing – the zero test
is numeric, so a run with no recorded cost never claims $0.0.
tests/test-verify-budget-keys.sh (12 assertions) and
tests/test-verify-deploy-error-surfaced.sh (7 assertions), both in the
fast tier via _FAST_KEEP and run_check, both mutation-verified in both
directions. The budget suite also asserts that no production file writes the
orphan keys, so a future writer cannot silently resurrect either
contradiction.
The release gate now runs its cheapest failing check first. release.yml
ran bun run typecheck as the LAST step of the job that gates every publish.
v9.49.0 died on a one-line TS2322 after the job had already spent ~3m46s on
pip installs and pytest. Bash syntax validation and the Bun typecheck need no
Python, so they now run first. Nothing about what is verified changes.
Corrected four places claiming LOKI_BUDGET_LIMIT defaults to unset/no cap.
It has defaulted to 100.00 since v9.42.0, and breaching it PAUSES the run
rather than reporting a terminal failure.
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.
v9.49.0 died on a TypeScript error. SkillStatus requires
dangling: boolean and none of the three checkSkills return arms set it.
Fixed with the value the type’s own comment calls for: pass false,
broken-symlink true, not-found false.
v9.49.1 died on two tests that asserted host state. The doctor suite
asserted doctor --json exits 0, which is true on a machine with a provider
CLI installed and false on a CI runner, where the ai_provider check
correctly fails. And tests/test-e2e-features.sh piped stderr into a JSON
parser, so it failed on any warning; every other doctor --json assertion in
the repo already discards stderr. Neither was a product defect.
The gate blind spot behind all of it. bun run typecheck, bun test and
two suites added this session ran in CI but not in the fast tier, so the local
gate reported green on trees that CI rejected. All are now registered and in
_FAST_KEEP – allowlist membership alone does nothing, since it only
controls deferral of checks that are already registered.
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.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.
The type error that blocked it. SkillStatus requires dangling: boolean
and none of the three checkSkills return arms set it. Fixed with the value
the type’s own comment calls for, since the text renderer uses it to print the
Fix line for a BROKEN link but not an ABSENT one: pass false, broken-symlink
true, not-found false. Defaulting all three to false would have satisfied the
compiler while silently changing doctor output.
The gate blind spot that let it through. bun run typecheck and bun test
were deferred in the fast tier, while the release workflow runs that exact
typecheck first, ahead of every publish job. The local gate reported green on
a tree whose typecheck was broken. All three checks, including their
dependency-install prerequisite, now run in the fast tier. The promotion
earned itself immediately: the next gate run caught a stale bundle before a
push, a class that previously reached CI.
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.
Every integration reaches real data, and the client can no longer drift away from the server.
Ten client paths pointed at working backends by the wrong URL.
client.ts called /sessions/{id}/github/runs while the server served
/sessions/{id}/github/actions/runs. The same drift hit run detail, logs,
workflows, dispatch, rerun and cancel, plus two deploy calls asking for
/connect where the server serves /token. The entire CI/CD panel and the
entire deploy-connections panel were dead, with complete backends behind
them. All nine drifted paths corrected; the missing disconnect route added.
A route that returned None. get_all_deploy_status defined its two
platform checkers and then the function body simply ended: no asyncio.gather,
no return. Because res.ok was true and the content-type was JSON, no error
ever fired. The panel showed every platform as permanently disconnected and
re-polled every 30 seconds forever. It now gathers all three checkers.
The catch-all told users to restart the server for a client typo. The SPA
route did not exclude /api/, so a dead API GET returned 200 with
text/html, which the client reported as “API endpoint not available. Please
restart the server with the latest version.” Restarting could never fix it.
/api/* now returns a JSON 404 while the SPA still serves HTML.
A worktree-detection regression. run.sh had replaced
git rev-parse --is-inside-work-tree with [ -d .git ], which is false for
every git worktree (measured: a worktree’s .git is a 63-byte file). That
silently disabled scoped-change detection for the parallel worktree runs this
project uses by default.
A UI that minted fake credentials. APIKeyManager generated
pk_live_${Math.random()...} client-side with zero backend calls. Deleted; a
real audit-logged implementation already ships at /api/v2/api-keys.
321 lines of invented metrics. MetricsPage rendered a cost trend, token
split, builds-per-day, a Math.random() activity heatmap, a provider radar, a
pipeline timeline, a quality gauge pinned to 87, invented sparklines and a
code timeline of three fabricated iterations. None of it was measured. It is
now backed by /api/cost and /api/cost/timeline, and surfaces with no
backing field were deleted rather than re-fed from a new source of invented
numbers. Unrecorded values render “Not recorded”, never $0.00.
A third, dead frontend. dashboard/frontend/ had no dist/, had not been
touched since 2026-02-13, and had zero live references against a positive
control of 23.
tests/test-verify-client-routes.sh proves every web-app client path
resolves to a real FastAPI route. It reads the actual route table via
server.app.routes and walks client.ts with the TypeScript AST – no regex
on either side, because a regex differ produced both a false positive
(reporting /sessions missing while 58 such routes exist) and a false
negative (missing the /github/runs drift entirely). Four rules exit 2 rather
than under-report: zero captures, an unresolvable path, a partially
interpolated segment, and a non-literal HTTP method.
/api/cost and /api/cost/timeline in web-app, delegating to the dashboard
readers using the pattern already established for /api/proofs, so the two
surfaces cannot drift into disagreeing about what a build cost.
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.
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.
A flag that announced work it never did.
loki migrate --multi-repo stops claiming to migrate repositories it never
touches. Reproduced: loki migrate <alpha> --multi-repo './services/*'
printed “Multi-repo migration (3 repositories)”, listed all three, then
created a single migration scoped to alpha. beta and gamma were never touched.
The discovery block populates repos[] and never references it again.
Measured with a positive control: repos[] / repo_count appear four times
inside that block and zero times after it; every downstream step operates
on the single $codebase_path.
A user reading “Multi-repo migration (3 repositories)” reasonably believes three repositories were migrated. One was.
The output now names the repository actually migrating, says plainly that
--multi-repo discovers rather than orchestrates, and hands over runnable
commands covering the rest.
Guarded by tests/test-multi-repo-claims-honest.sh (9 assertions,
source-level plus end-to-end against the real CLI).
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.
Hand Loki a GitHub issue and get a pull request, with nothing installed. And two commands that used to contradict each other now agree.
An issue-to-PR GitHub Action. The headline use case had no entry point:
the only Action in the repo was Loki Mode Code Review, and NO workflow or
action was listed in package.json files[], so npm users got no GitHub
trigger at all.
Ships .github/actions/issue-to-pr/ (a composite action) and
.github/workflows/loki-issue-to-pr.yml (a gated workflow). Label an issue
loki, or comment /loki, and the run opens a pull request. It fails fast
with a clear message when ANTHROPIC_API_KEY is absent rather than burning a
runner, holds a concurrency group per issue so double-labelling cannot race
itself, and checks out with fetch-depth: 0 so the agent can read history.
Both are in files[]. The packaged-artifact blind spot has cost four
releases; a shipped entry point that is not actually packaged is the same
bug.
Guarded by tests/test-issue-to-pr-action.sh (8 assertions).
loki next and loki resume no longer contradict each other. Reproduced:
cmd_next maps max_iterations_reached and budget_exceeded to
“Will run: loki resume”, but the resume hint accepted only status
interrupted. So loki next told you to run loki resume, and loki resume
answered “No session to resume. Start a session with: loki start” – and
exited 0, so nothing went red. The two commands whose entire job is “do the
right next thing” disagreed on every run that stopped at a limit.
The hint now covers the three statuses that stop WITHOUT a verdict, and a
capped run is told to raise LOKI_MAX_ITERATIONS or LOKI_BUDGET_LIMIT
first, because resuming it unchanged hits the same wall on the next
iteration.
Deliberately unchanged: council_approved, council_force_approved and
completion_promise_fulfilled still decline to resume. Those carry a verdict
and route to loki ship; inviting an approved build back into the iteration
loop would be worse than the silence this replaces.
Guarded by tests/test-next-resume-agree.sh (16 assertions). It reads the
command loki next actually announces and then RUNS it, so the agreement is
the property under test rather than either command alone.
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).
council_approved run still has no delta path and still cannot be resumed;
that work is open. Resuming a capped run and revising a finished one are
different problems, and conflating them is how the first would get called
done.A second repository the agent can actually read, plans that arrive whole, and the flake that blocked two releases.
LOKI_ADD_DIRS grants the agent read access to sibling repositories.
Nothing in autonomy/, providers/, loki-ts/src/ or bin/ ever passed
--add-dir or additionalDirectories (0 files; positive control
run_autonomous = 7 files). An agent asked to change a shared type in
../service-b could not read it, did not error, and guessed, so the user
got a change that does not compile with no signal why.
Colon-separated, matching PATH convention, wired into the auto-flags builder so every call site inherits it. Gated on CLI flag support, so an older CLI degrades instead of erroring.
A nonexistent entry is skipped WITH A WARNING, never passed through. A typo reaching the CLI aborts it and takes the whole run down, turning a convenience into an outage; dropping it silently would reproduce the exact silent-wrong-output defect being fixed.
A decomposed plan was capped at 3 tasks. load_queue_tasks used
tasks[:3], applied SEPARATELY to in-progress.json and pending.json, so a
release doc decomposed into 5 tasks silently lost 2 from each file. The agent
received a partial plan and was never told it was partial.
A count was the wrong bound anyway: one rich PRD task (300-char description
plus acceptance criteria plus a user story) can outweigh ten legacy one-liners.
The bound is now characters (LOKI_QUEUE_TASK_CHARS, default 6000), and
truncation is disclosed in the prompt instead of hidden. Measured: 5 tasks
in, 5 tasks out; at a tight budget, at least one task always survives and the
remainder is announced.
The CI flake that blocked two releases.
tests/test-review-assurance-tail.sh used
quality_review="$(find ... -type d | head -1)" under its own
set -o pipefail. head -1 closes the pipe, find dies of SIGPIPE, and the
pipeline reports 141 even though the value was captured correctly.
It only fires once find emits enough output to fill the pipe buffer, so it is
load-dependent: the suite passed 44/0 locally every single time and failed
twice on a loaded CI shard. Reproduced directly: 4,000 entries through
find | head -1 under pipefail returns rc=141. Replaced with a pipe-free
glob loop.
Verified that while read ... done < <(cmd | head -N) does NOT have this
defect (process substitution does not propagate the inner status: reads 40
lines, rc=0), so the same-shaped lines elsewhere are safe and were left alone
rather than changed cosmetically.
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.
The agent was handed a manual with half its pages missing.
copy_skill_files copied skills and never copied their references.
autonomy/run.sh copied skills/*.md into .loki/skills/ and nothing, in
any file, copied references/. The copied skills cite references/*.md
21 times across 8 files (skills/00-index.md alone has 11), and every one
of those files exists in the repo. So the agent followed 21 dead paths every
run and silently lost guidance this function believed it had shipped.
Measured after the fix: 26 reference files copied, 0 dead cited paths.
The SKILL.md path rewrite named 8 filenames by hand. It rewrote
skills/00-index.md, skills/model-selection.md and six siblings explicitly,
plus a Read skills/ catchall. Three consequences, all silent: any NEW skill
kept an unrewritten path, a path written as “See skills/…” survived because
it is not “Read”, and references/ paths were never rewritten at all.
Replaced with one portable, idempotent transform: protect already-correct
.loki/ prefixes with a sentinel, rewrite bare paths unconditionally, restore.
Measured: 17 rewritten paths, 0 bare paths left, no .loki/.loki/ doubling.
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”.
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.
The first artifact a user sees no longer misstates the product or ignores the user.
The generated PRD reported Loki Mode CLI v6.0.0. A hardcoded literal at
autonomy/issue-providers.sh, printing on a v9.44.0 install: three majors
stale. This is the first file the loki start owner/repo#123 path produces
and the document the agent then works from, so it is what a user reads in
their first five minutes. A product whose entire pitch is a verifiable receipt
cannot misreport its own version in its own first artifact.
The real version is now plumbed through from get_version() at the call site
(autonomy/loki:10457). Absent that, it reports unknown rather than a number
it cannot substantiate.
Acceptance Criteria discarded the user’s own requirements. The section was a fixed four-line list beginning “Address all requirements specified in the issue body above”. An issue whose body enumerated three specific checkboxes had them rendered in the PRD and then overridden by that boilerplate in the same document.
Checkboxes and numbered lists are now extracted from the issue body and become
the acceptance criteria. Measured on a real issue shape: three - [ ] items
in, three numbered criteria out.
It never fabricates. A body with no checklist still gets the default list, and that output says so explicitly (“The issue body lists no explicit checklist, so these are defaults”) rather than presenting defaults as if the user had written them.
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.
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.
An MCP tool that was broken on every call that did real work.
loki_graph_query raised NameError: name 'subprocess' is not defined.
mcp/server.py imports subprocess inside _maybe_autoreindex_code (:1724)
and nowhere else. loki_graph_query called subprocess.run at :2578 with the
name in NO enclosing scope, so every invocation that reached a real graph
failed. Not a cross-repo edge case: single-repo too. The early-return path
(no graph present) returned before the call, which is why the tool looked
alive.
One line: a function-local import subprocess.
Guarded by tests/mcp/test_graph_query_executes.py, which drives the function
against a real graph rather than asserting on source text. Mutation-verified:
removing the import turns it red.
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.
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.
LOKI_DELEGATE_PR now defaults ON. On completion the product used to
print Pull request: not opened (set LOKI_DELEGATE_PR=1 to open one). It knew
exactly what the user wanted and asked them to go read documentation instead
of doing it. For the core use case – hand it a GitHub issue and get a
resolution – the PR IS the outcome, so shipping it off shipped the product
off.
Nothing about the safety model changed. Every guard was already built and
is unchanged: it requires a GitHub repo AND a successful gh auth status AND
a non-default branch, it opens a PR and NEVER merges, and every call is
best-effort so a failure cannot block completion. LOKI_DELEGATE_PR=0 opts
out, and an explicit 0 is honored.
The completion line no longer advertises a flag that is now the default; it names the actual reason a PR was not opened (no GitHub remote, gh not authenticated, or on a default branch).
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.
A benchmark number that counted strings, an unbounded spend default, and a supply-chain gate that did not exist.
“99.67% SWE-bench” was a count of non-empty strings. Re-measuring the
stored 300-instance run: 179 of 300 model_patch values were prose, not
diffs (a model preamble followed by a fenced diff). clean_patch stripped a
fence only at position 0, so any preamble defeated it. qa_agent then
validated with substring tests over the whole blob ("---" in patch, "@@" in
patch), which prose quoting a diff satisfies: 178 of 179 prose entries
passed every format check, with attempts == 1, so the retry loop never
fired. generated_count incremented on any non-empty string, and that counter
is what was published.
Three fixes: the extractor finds a fenced diff anywhere (falling back to the first real diff line, and never fabricating), the validator is anchored to LINE STARTS which prose cannot satisfy by accident, and the counter requires a real diff header.
Measured on the same data after the fix: 174 of 179 prose entries now yield a genuine diff, 5 are honestly rejected, 0 remain falsely certified. A genuine clean diff still validates.
All four published instances of 99.67% in this file are annotated in place rather than deleted. No corrected figure is published: producing one requires re-running the harness, and an estimate would repeat the original error of publishing a number nobody measured.
LOKI_BUDGET_LIMIT shipped as unlimited. This file’s own F4 analysis
already named it as one of three runaway valves that all ship disabled,
leaving a measured 8.3-day iteration ceiling as the only backstop. Now
defaults to 100.00 USD, well above the measured envelope (median 0.48 per
trial, max 3.06 across 79 trials). Breaching PAUSES and saves state; it never
kills work. Uses ${VAR-default}, so an explicit empty value still means
unlimited and the operator’s choice is honored.
pinned-subset.json declared dataset: "SWE-bench Verified" while its own
VERIFICATION_STATUS said UNVERIFIED_AS_VERIFIED_SPLIT and its note said the
ids came from a LITE run. The note was honest; the field a consumer reads was
not.
tests/detect-hallucinated-deps.sh – a slopsquatting guard. A model can
emit a dependency name that does not exist, and an attacker who registers it
owns code execution in every install that follows
(arxiv.org/pdf/2606.13918, 2026-06-15). The repo had no dependency
verification at all.
Three states, not two: resolved / MISSING / unchecked. No network or a 5xx means NOT CHECKED, never a pass. Verified against a fabricated npm name and a fabricated PyPI name, against 88 of this repo’s real dependencies (zero false positives), and with an unreachable registry.
One bug was found while verifying it: curl -fsS exits non-zero on 404, so the
fallback discarded the status code and every MISSING package was reported as
unchecked. Without catching that, the detector would have been theatre.
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.
The upgrade that could not work, and the diagnosis that never fired.
$ 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.
The shadow check was gated behind a registry lookup. v9.36.0 added
findShadowedNewerInstall, and it works. But the call sat INSIDE the “a newer
release exists” branch, so it only ran after a successful registry check said
the running version was outdated. Two consequences, both hit in the field:
with the registry unreachable the user got silence, and with a stale
<=24h cache they got the generic “install 9.35.0” nudge quoting a version
that was neither installed nor latest.
Shadowing is a local, on-disk fact. It does not depend on the registry, the cache, or the network, so nothing about a registry result should gate reporting it. The check now runs FIRST. The ordinary “newer release” nudge is unchanged and still does the registry lookup, just after.
loki doctor did not detect it at all. Doctor is where a user goes when
something is wrong, and on the bash route (which a default loki invocation
takes) there was no shadow handling anywhere. It now walks PATH, resolves each
entry through realpath, and reports every OTHER install with its version and
location as a BLOCKER, stating plainly that reinstalling will not fix it.
Verified against the real shadow on a developer machine (running 9.40.0 from a
Homebrew node prefix while 9.39.0 sat in ~/.bun/bin), and against two
negative controls: a single install reports OK, and the SAME install reachable
through two PATH entries is deduped by realpath rather than reported as
shadowing itself.
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.
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.
Authored reviewer personas now reach the prompt, and a shipped test that could silently invert itself.
All 41 agent types carry hand-written persona prose that was thrown away.
agents/types.json gives every type a real authored persona (“You are a
senior backend engineer specializing in server-side development, API design,
and distributed systems…”). The selector SET that persona on the specialist
dict and the reviewers payload then copied only name, focus and checks,
so it was discarded before any prompt was built.
Nothing looked broken, which is why it survived: the role still reached the reviewer through a SYNTHESIZED line (“Review from X perspective: …”). What was lost was the specific authored expertise that makes a specialist reviewer worth more than a generic one.
Wired through all six sites: payload (built-in and installed specialists), read-out, export, prompt interpolation, and unset. The persona leads the prompt; the “Your SOLE focus is” constraint that keeps a blind reviewer in its lane is untouched.
Strictly additive. A specialist with no persona, or a whitespace-only one, produces a prompt byte-identical to the previous behavior. Verified by driving the prompt logic directly in both directions.
tests/test-provider-arm-coverage.sh (shipped in v9.39.0) contained a
latent self-inverting bug. It used printf '%s' "$blk" | grep -q ... under
set -o pipefail. grep -q exits at the first match and closes the pipe, so
printf dies of SIGPIPE and pipefail reports the PIPELINE as failed even
though grep MATCHED.
It passed only because the provider case block is currently small enough that printf finishes before grep exits. Demonstrated with input past the pipe buffer: grep matches and the pipeline still returns 141. As the file grew, a correct provider arm would have started reading as missing. Now greps a file instead of a pipe.
agent-skills/README.md claimed a runtime loader that does not exist.
It stated “Agents automatically discover skills in this directory at runtime”
and showed discover_agent_skills(). That function exists nowhere in the
repository, and agent-skills/ is in no distribution artifact, so nothing
reads those files on any route. Marked as a proposed pattern and design
sketch, pointing at skills/ as the live system. Documentation fix; building
an unrequested loader would have been the wrong answer.
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.
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.
Three capabilities the product advertised and could not actually perform.
loki memory enable-hook wrote a dead path into the user’s real Claude
settings and reported success. claude/hooks/loki-session-end.sh exists in
the git checkout but claude/ was absent from package.json files[], so it
shipped in NO npm tarball. Verified against the published 9.38.0 package:
package/claude/ does not exist (positive control: package/autonomy/run.sh
is present). The command built the path with no existence check
(autonomy/loki:24584), mutated ~/.claude/settings.json, and printed a
green “SessionEnd hook ready”.
Every npm user who ran it believed session capture was on while it captured
nothing: the hook fired on each /clear, failed with “No such file or
directory”, and that failure surfaced nowhere a user reads.
Two changes, because either alone leaves a hole: claude/hooks/ now ships,
AND the command fails closed when the script is missing rather than writing a
settings entry it cannot honor. Shipping the file fixes today’s users; the
guard means a future packaging slip reports itself instead of lying.
loki agent run was dead for every opencode user. opencode is listed as
one of five active providers, loki provider set opencode accepts it with no
warning and writes .loki/state/provider, and then the provider case in
cmd_agent (autonomy/loki:29430-29447) had arms for claude, codex, cline
and aider only. The *) arm printed “Unknown provider: opencode” and returned
1 – after already printing the persona banner, so it looked like it started
working and then died.
This was an omission, not an unsupported provider: the phase dispatcher in the
SAME file already had a working opencode) arm calling provider_invoke
(providers/opencode.sh:121). The error message even listed opencode as
supported.
Reproduced through the real entrypoint before the fix and after: the banner now reaches the actual opencode CLI instead of dying in a case statement.
loki agent review was missing BOTH opencode and aider. Same shape, wider
blast radius, found while fixing the above.
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.
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”.
Four receipt fields that degraded to values indistinguishable from success.
A phantom reviewer inflated the council roster. A real proof artifact
carried FOUR council.reviewers[] entries where three agents voted; the
fourth was role:"" vote:"" summary:"", materialized from a
devil’s-advocate file that is not a reviewer. A buyer counting reviewers on
the receipt counted one that never voted. The flat reviewer path now skips a
record carrying neither a role nor a vote. Measured on the real artifact:
4 rows -> 3, roles convergence-voter, requirements-verifier,
test-auditor.
disabled_phases always reported []. The audit finding as written said
loki_apply_build_profile “never exports”; that was wrong, it has 24 export
lines. The real defect is sharper: it exported exactly the five phases it
ENABLES and none of the six it DISABLES, so the receipt structurally could
not report what a build profile had switched off. An absent export reads as
“nothing was disabled”, which is the same shape of lie as a zero that means
“unmeasured”. Six phases now export: api_tests, integration,
performance, regression, uat, web_research.
cost.usd summed a 0.0 default for unpriced records. available was a
single flag covering cost AND tokens, so a run with real token counts and no
priced record reported usd: 0.0, available: true – the receipt asserting
the run cost nothing while its own token counts proved work happened. Cost
now reports separately:
| records | usd | cost_available | cost_partial |
|---|---|---|---|
| priced | 0.5 | true | false |
explicit cost_usd: 0 |
0.0 | true | false |
| priced + key absent on some | 0.5 | true | true (lower bound) |
| tokens, no cost key anywhere | null | false | – |
An explicit measured {"cost_usd": 0} is a GENUINE zero (a free cache-hit
iteration) and stays 0.0. The first version of this fix keyed on
cost_usd > 0 and nulled that case, which is its own dishonesty; it keys on
PRESENCE of the field instead. test_genuine_zero_cost_stays_zero_not_null
passes unmodified, which is the check that no contract was redefined to fit
the change.
base_sha was a fabricated empty-tree constant on a non-git run.
_empty_tree_sha returned the well-known 4b825dc6... hash without first
establishing that a repository exists, so a receipt from a non-git workspace
carried a real-looking base SHA for a tree it never read. It now runs
git rev-parse --is-inside-work-tree first and returns "" outside a repo,
matching the head_sha -> "" discipline already applied next to it. In a
real repo the constant is preserved unchanged.
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.
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.
A gate that scanned nothing no longer reports a pass, and the receipt says NOT VERIFIED when nothing was verified.
static_analysis reported passed after examining ZERO files. When there
were no changed files to check, enforce_static_analysis touched
static-analysis.pass and returned. The receipt reader promotes a bare .pass
marker straight to status: "passed" (proof-generator.py:352-353), so a gate
that examined nothing rendered identically to one that examined everything and
found it clean.
This was not cosmetic. static_analysis is typically the only EXOGENOUS
(agent-independent) gate in a receipt, and any_verified
(proof-generator.py:1694-1707) is satisfied by a single passed exogenous
gate. That no-op pass was the one term standing between the honest headline and
a green-ish one. Measured through the real reader and the real headline
function:
| static_analysis | exogenous passed | headline |
|---|---|---|
passed (before) |
1/1 | VERIFIED WITH GAPS |
inconclusive (after) |
0/1 | NOT VERIFIED |
Observed on a real run in a NON-GIT directory: file discovery is git-based, so
changed_files was empty, the gate examined none of the three files the run
had just created, and the receipt still reported a passing exogenous gate. That
run left static-analysis.pass at 0 bytes with static-analysis.json absent –
a combination only the no-changed-files early return produces.
The path now writes {"files_checked":0,"status":"inconclusive",
"reason":"no_changed_files"} and does not touch the marker.
INCONCLUSIVE, not failed, deliberately: having nothing to scan is not a defect in the delivered code, and reporting a failure would trade one dishonesty for another while blocking runs that legitimately changed nothing. The gate still returns 0 – only the CLAIM changed, not the control flow.
Guarded by tests/test-static-analysis-noop-not-pass.sh (5 assertions). Two
mutations verified: restoring the touch goes red, and over-correcting to
failed also goes red. The guard catches dishonesty in both directions.
unit_tests reported passed for a project with no test runner. The same
defect, one gate over. The no-test-runner branch wrote an honest record
({"runner":"none","status":"not_run","pass":"inconclusive"}) and ALSO touched
unit-tests.pass. The reader checks the marker FIRST, so the honest record was
never read, and the receipt claimed a passing gate while honesty.degraded in
the same document said tests did not run.
What kept the bug alive was a comment asserting “unit-tests.pass is only read
for the status-line display”. That premise was false –
proof-generator.py:346 reads the same marker into the receipt. Measured by
driving the real collector:
marker present + {"status":"not_run"} json -> "passed"
json only, no marker -> "not_run"
The touch is removed. All three outcomes now stay distinct and were each
verified: real tests pass -> passed; no runner -> not_run; real tests fail
-> failed. Non-blocking behaviour is preserved by the existing return 0,
not by claiming a pass. The status line falls back to PENDING, which is the
truthful rendering for a project whose tests never ran.
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.
unit_tests passing via the
no-test-runner branch, base_sha as a fabricated empty-tree constant, a
phantom reviewer row, disabled_phases reporting [], cost.usd summing a
0.0 default, and wall_clock_sec collapsing.The most expensive step in a build now reports how long it took.
completion_council stage timing. Sixteen build stages emit a
stage_complete record carrying duration_s. The completion council did not
– and on a measured one-function build the council window was 142s against
71s of actual agent work, making it the largest single cost in the run and
the only major step whose cost could not be read from an emitted event.
It now emits one record like every other stage. status reports what the
council DECIDED: pass when it approved a stop, not_run when it ran and
declined to stop. Both are normal outcomes, so neither is reported as a
failure.
Purely additive. The dispatched decision is unchanged; the emit only reads
_loki_completion_ready, never assigns to it, and a failing emitter cannot
fail the caller.
Guarded by tests/test-council-stage-timing.sh (6 assertions).
“Installed 9.35.0” then loki --version says 9.22.3, and the nudge tells you
to install again. Reported by a user, reproduced exactly:
$ bun install -g loki-mode
installed loki-mode@9.35.0
$ 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
The install worked. A different, older loki sat earlier on PATH and kept
winning – here a leftover ~/.local/bin/loki symlink into an old npm-global
tree under Homebrew’s node. Reinstalling updates a copy PATH never reaches, so
the advice sent the user round a loop with no exit.
The nudge now detects a newer copy that is already installed but shadowed, and says so:
Loki Mode 9.35.0 is installed but not the one running (you are running 9.22.3).
Another loki earlier on PATH is winning. Newer copy: /Users/you/.bun/bin/loki
Run `which -a loki` to see the order, then remove or re-point the earlier entry.
Best-effort and fail-silent: an unreadable or non-package PATH entry is
skipped, so the worst case is the ordinary update hint, never a crash or a
false report. Guarded by 5 tests in
loki-ts/tests/util/update_check.test.ts; verified against the unfixed code,
where the reported behaviour returns and the test fails.
Found while fixing this: the existing update-check tests read the host’s real PATH, so a developer machine that genuinely had a shadowed install broke an unrelated assertion. The shared setup now pins PATH and restores it, and the pre-existing test passes unmodified.
A timed-out benchmark run leaked its whole process tree.
subprocess.run(timeout=...) kills only the process it spawned.
loki start re-execs as /tmp/loki-run-*.sh and spawns its own descendants,
so a timed-out cell left that tree ALIVE and still holding provider capacity.
Measured, not theorised: stopping a benchmark matrix leaked seven live
loki-bench-loki-* runs that survived more than an hour. They starved later
trials into their own 600s timeouts and made clean verification runs look
like they had “died immediately”. A timeout that does not reap manufactures
the very failure it reports – the next trial times out because the last one
is still running.
run_cli now spawns with start_new_session and, on timeout, signals the
whole process group: TERM first so the engine can flush state, then KILL for
whatever remains. The injected-runner seam that tests mock through is
untouched and is asserted to stay that way.
Guarded by benchmarks/bench/tests/test_timeout_reaps_tree.py (3 tests). The
grandchild is asserted BY PID after the timeout returns, because a test that
only checked the direct child would have passed against the buggy code –
subprocess.run always killed that one. Verified against the original code:
the grandchild survives, and the test fails.
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.
emit_stage_complete directly and are mutation-verified, but
every end-to-end run attempted for this release completed through a path that
did not reach the council dispatch, so no live completion_council record has
been captured yet. Stated plainly rather than implied: the code is guarded, the
field measurement is still outstanding.agent (71s), code_review (128s) and now
completion_council are attributable from emitted events. The remainder is
still unattributed, and it will stay that way until it is instrumented too –
it is deliberately NOT estimated here.claude processes observed at once). Each record is a real bracket
around one stage; the set of them is not a partition of the run.Pass-through
(no_test_runner, no_app_runner, no_ui_files, not_serveable), so the
voters ran where there was structurally nothing to verify. That is a real
efficiency signal, but acting on it needs the measurement this release adds,
not an inference.When the model you pinned is not the model that ran, the receipt now says so.
model_substituted records. An operator can pin a model and get a
different one. The substitution is often legitimate – a fable pin collapses
to opus because Claude Fable 5 was reported unavailable at the Claude API –
but until now it was silent: the receipt showed an opus run and nothing
recorded that a pin existed, what it was, or why it was not honored.
A silent correct substitution is indistinguishable from a silent wrong one. Neither can be audited, and neither can be refuted by someone holding evidence against it. Our wedge is a receipt you can check yourself; a receipt that omits which model actually ran does not deliver that.
emit_model_substituted now records pinned, dispatched, reason and
site at the dispatch chokepoint (autonomy/run.sh), and
src/audit/subscriber.js maps the event into the tamper-evident chain so it
reaches the receipt rather than sitting unread in events.jsonl.
Behaviour is unchanged. The same model is dispatched as before. This makes the existing collapse visible and attributable, nothing more.
Guarded by tests/test-model-substitution-visible.sh (7 assertions). Five
mutations verified, including the three that matter: removing the call at the
dispatch site, dropping reason from the record, and deleting the audit
mapping so the event reaches no reader.
It does not change the fable -> opus mapping. I set out to, on the
grounds that the comment (“Fable 5 is not available at the Claude API”) looked
stale: the CLI accepts both --model fable and --model claude-fable-5-1,
exit 0.
That test does not prove what I wanted it to prove. ANTHROPIC_API_KEY was
unset – the CLI authenticates by subscription, while the comment cites an
error from the raw API. Different transports, different model
availability, and loki-ts/src/runner/sdk_invoker.ts uses the SDK rather than
the CLI. The premise can be false on one route and true on the other at the
same time.
The collapse is also not a forgotten line: it is a deliberately maintained
four-site invariant (providers/claude.sh, the static fallback and dispatch
backstop in run.sh, and the cost estimator in autonomy/loki, which quotes
opus pricing on purpose so the quote matches dispatch), defended by ~8
assertions in tests/test-model-override.sh.
Flipping it would have meant rewriting those assertions to match a belief I
could not verify, and would have made the estimator quote $10/$50 for runs
that may still dispatch opus on the SDK route – a new cost-honesty defect
introduced while claiming to fix one. test-model-override.sh passes
unmodified in this release, which is the check that the contract was not
quietly redefined.
Settling it needs a direct /v1/messages call with a real API key. Until
then the comment’s claim is the better-evidenced one: it cites an observed
error, and my test probed a different transport. The new record makes the
question self-answering – a user with a key now sees
reason=fable_unavailable_at_api and has the receipt to refute it.
It does not de-vendor capability_router.ts’s tier defaults. Planned, then
dropped after reading the code: routeTaskClass is default-OFF, has exactly
one caller (autonomous.ts:753), and an operator pin already wins at :171
through provider-neutral env vars. A non-Anthropic user has a working override
path today, so changing the defaults would be a refactor with no user-visible
defect behind it.
docs/AUDIT-CHAIN-THREAT-MODEL.md.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.
My tests/test-workflow-rc-capture.sh failed CI on good files. Its YAML
assertion ran import yaml and treated ANY exception as “invalid YAML”.
pyyaml is installed on a developer laptop and not on the GitHub runner,
so an ImportError was reported as a broken workflow file. Every file was fine.
That is the unmeasured-versus-failed confusion this release series exists to
correct, committed by the guard written to prevent it. Now delegated to
tests/lib/check-workflow-yaml.py, which reports three states that never
collapse: OK, NO_PARSER (skipped, explicitly not counted as a pass), and
INVALID (named, with the reason).
Verified under a simulated runner with pyyaml hidden: the suite reports SKIP
and exits 0. Both arms mutation-tested – genuinely invalid YAML still FAILS
rather than skipping, and reintroducing an unreachable RC=$? handler still
fails.
The workflow RC scan moved to tests/lib/scan-workflow-rc.py for the same
reason the YAML check did: an inline heredoc inside a shell heredoc broke the
file twice while editing it.
Post-release smoke diagnostics now print the FAILING checks, not the first 40 lines of a long JSON document. The v9.34.0 fix made CI print doctor output for the first time, and the output was truncated before reaching the entry that failed – better than silence, still not diagnostic.
The buyer’s verification command gets a front door, and post-release smoke stops failing with no diagnostic.
loki proof chain. tools/verify-chain.py runs the whole verification
chain and reports one verdict. It shipped in package.json files[] and no
loki command surfaced it, so the command a buyer runs to check our output
without trusting us was undiscoverable. Our wedge is “a receipt you verify
yourself”; a verifier nobody can invoke does not deliver it.
The tool’s exit contract is preserved exactly: 0 PASSED, 1 FAILED,
2 UNAVAILABLE, 3 NOTHING. Collapsing 2 or 3 into 1 would destroy the
distinction that makes the verdict worth anything: a chain that could not be
checked is not a chain that failed, and zero receipts is not a passing audit.
Both are asserted individually, and the human output says so in words rather
than only in an exit code.
Guarded by tests/test-proof-chain-command.sh (7 assertions). Three mutations
verified, including the two dangerous ones: turning UNAVAILABLE into a pass,
and swallowing the verdict so an empty workspace reports success.
Post-release smoke failed on npm and docker with no diagnostic, and the handler that was supposed to explain why could never run. The steps looked correct:
set -uo pipefail
loki doctor --json > /tmp/doctor.json
RC=$?
if [ "$RC" -ne 0 ]; then echo "FATAL: ..."; exit 1; fi
GitHub runs steps with bash -e {0}, so -e is on regardless of that set
line. A non-zero exit aborts the step before RC=$? is read, so the
handler never runs and the job dies silently. Demonstrated rather than
asserted:
bash probe.sh -> "REACHED:1" exit 0
bash -e probe.sh -> (no output) exit 1
Fixed with || RC=$? at five sites across post-release-smoke.yml and
test.yml – the same bug in every one – and the handlers now print the
captured output, so the next failure says what broke instead of nothing.
Worth stating plainly: this was CI reporting a real problem badly, not a false alarm. The npm and docker artifacts were fine.
A misleading claim of my own, from v9.33.0. That entry repeated an audit
finding that “~46 of 54 tools/*.py have no production callers”. Measured:
42 of them are operator-run CLI utilities with __main__ entrypoints,
which is a legitimate design, not dead code. Zero tools are dead. A CLI tool’s
caller is the operator. The v9.33.0 entry is annotated in place with the
correction.
tests/test-workflow-rc-capture.sh (4 assertions). The bash -e failure
mode is silent – the workflow does not report “your handler is unreachable”,
it just dies – so it needs a guard rather than vigilance. The suite first
DEMONSTRATES the mechanism with a probe rather than asserting it from memory,
then scans all 21 workflow files, and guards against vacuity: a scan that
found no workflows would otherwise report nothing wrong.loki proof chain fronts the existing verifier; it does not make the chain
stronger. What it verifies is still bounded by
docs/AUDIT-CHAIN-THREAT-MODEL.md.loki doctor exits 1 for a
real local reason. That is correct behaviour and is not changed here.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.
“Jira bidirectional sync” and “Linear bidirectional sync” were never
reachable. Both were built, tested, and listed in package.json files[],
so they ship to every npm user and CI stays green because their own tests
import them. Nothing runs them.
Verified, not inferred: the only dispatcher is
src/integrations/sync-subscriber.js, and nothing spawns it (the only
matches for its name in the repo are its own log strings). Even if something
did, its Jira branch would throw on construction: it passes {baseUrl, token}
while jira/api-client.js:32 requires {baseUrl, email, apiToken}.
Reproduced by execution:
THROWS with the caller shape: JiraApiClient requires baseUrl, email, and apiToken
constructed OK with the documented shape
The 2026 CHANGELOG entry advertising both is annotated in place with the correction rather than rewritten, because it shipped and the record should show what was claimed and what was true.
What IS real: the Jira READ path. loki start PROJ-456
(autonomy/issue-providers.sh:321) works and README.md:634 documents the
right env vars. Write-back does not exist. Linear has no read path either, so
Linear support is not user-reachable at all – recorded here rather than
advertised.
tests/lib/scan-unreachable-shipped.py and
tests/test-no-unreachable-shipped.sh (4 assertions). The durable
deliverable: the repo can no longer acquire shipped-but-unreachable modules
without someone recording a decision.
The distinction it enforces is the one that took this defect years to surface.
Not “does anything reference this file” – tests and CI smoke-imports
reference everything – but “does any RUNTIME path reach it”. Four of the
seven flagged modules are required only by
.github/workflows/integrity-audit.yml running node -e "require(...)",
which proves they LOAD, never that they RUN.
Allowlisting requires a REASON, and an entry whose reason is blank fails the suite. That is deliberate: a mute button would recreate the defect one level up, and this guard exists precisely because plausible-looking evidence (passing tests, green CI, a files[] entry) hid unreachable code.
Two corrections found while building it, both by the guard failing on itself:
graphify-out/cache/stat-index.json indexes every path in the repo and
matched everything. A generated cache is not a caller. Excluding generated
trees took the count from 0 to 17.api-client.js against a dashboard-ui component and package-lock.json.
Proximity is not a caller. Matching require/import/spawn SYNTAX took 17 down
to the 7 real ones.The suite also asserts the scan is non-vacuous (22 modules against 810 files). A scanner that examines nothing reports nothing missing, and an empty result is an absent measurement rather than a pass.
All mutations verified: a new unreachable module, an allowlist entry with an empty reason, and removing the CHANGELOG correction each turn the suite red.
src/integrations/. An earlier framing of “~46 of 54 tools/*.py
have no production callers” is CORRECTED in v9.34.0: measured, 42 of them are
operator-run CLI utilities with __main__ entrypoints, which is a legitimate
design rather than dead code. Zero tools are dead. dashboard/api_evidence.py
is not covered by this scanner and is not claimed to be.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.
Every audit entry claimed an actor nobody established. _audit() in
web-app/server.py hardcoded user="system", and neither of its two call
sites passed anything else. “system” is worse than blank: it asserts an actor
that was never verified.
This is the DEFAULT posture, not an edge case. web-app/auth.py:1-4 states
that with no DATABASE_URL authentication is completely disabled and every
endpoint is open, and /api/audit-log carries no auth dependency. So in the
default configuration the actor is genuinely unknowable.
Entries now carry an actor_state of identified, unauthenticated, or
unreadable, and the three never collapse. An authenticated principal is
attributed by id; an unconfigured deployment says so rather than inventing a
name; and a lookup that throws is recorded as its own state so it cannot be
mistaken for “nobody was logged in”. Attribution can never break the action it
describes.
The team role field looked like a permission and enforced nothing.
Measured: it is written (:7887) and echoed back and never read for any
authorization decision anywhere in the file. /api/teams,
/api/teams/{id}/members and /api/audit-log have no auth dependency at all.
Kept, because the UI displays it and removing it would break that surface, but
now labelled NOT AN AUTHORIZATION CONTROL at the point a reader would
otherwise trust it. A decorative control is worse than none: a buyer may rely
on it. Wiring real enforcement means putting an auth dependency on those
routes first, which is a larger change than a label and is not pretended here.
wiki/Enterprise.md claimed “authorization (role-based scopes)” without its
precondition. require_scope IS real enforcement on the dashboard API, but
dashboard/auth.py:787-788 returns allow when neither LOKI_ENTERPRISE_AUTH
nor OIDC is configured, and that is the default. The claim now names the
mechanism and the condition under which it applies.
The web-app audit log is not the tamper-evident one. It rewrites a
500-entry JSON array in place on every append, so it is a recent-activity
view. Said so at the function, pointing at
docs/AUDIT-CHAIN-THREAT-MODEL.md, so it is not cited as audit evidence.
tests/test-audit-actor-attribution.sh (8 assertions). It extracts and
drives the REAL _audit functions out of server.py rather than
reimplementing them, and asserts each of the three actor states individually
rather than checking that “an actor was recorded” – a count or a presence
check would pass on the fabricated “system” it exists to prevent.
All mutations verified: restoring the hardcoded actor, collapsing unreadable
into unauthenticated, and ignoring an authenticated principal each turn the
suite red. The second matters most: folding the error state into the honest
one is the subtle version of this defect, and a test that only checked for
“not system” would have missed it.
unauthenticated for every entry, which is accurate and
not useful. Turning that into identified requires enabling auth, which is a
deployment decision.role field still enforces nothing. It is now labelled, not wired.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.
The only policy writer fed a file the reader never opened. The dashboard
writes policies to ~/.loki/policies.json (dashboard/api_v2.py:75,105, via
LOKI_DATA_DIR), while src/policies/engine.js:_init read only
<projectDir>/.loki/policies.json. An operator who created a policy through
the UI got no enforcement and no error explaining why.
The engine now falls back to the global file when the project defines none. Project-local still wins: a repo shipping its own policy must not be silently overridden by a machine-global one.
autonomy/run.sh:check_policy had to change with it. It short-circuits on
“no policy file” before invoking the engine, so a project-local-only test
there would have skipped before the new fallback was ever consulted. The two
gates now agree, which is the point: when they disagree, enforcement
disappears between them with nothing logged.
A missing Node runtime silently disabled enforcement. check_policy
returned 0 (allow) when node was unavailable, even though reaching that line
means a policy file EXISTS and the operator has expressed intent to enforce.
The run proceeded as if every action were permitted, and nothing was logged.
That is the worst shape a security control can take: the operator believes it
is on.
It now fails CLOSED, matching what check.js already does for a corrupt
policy file (tests/test-policy-failclosed.sh). LOKI_POLICY_REQUIRE_NODE=0
restores the old behaviour for a host that genuinely cannot install node and
accepts running unenforced – and it says so, rather than staying silent.
The refusal is recorded, as a new policy_unevaluable event distinct from
policy_denied. A block that leaves no trace is indistinguishable from a run
that was never gated, and the receipt must be able to tell “denied by a rule”
from “could not be checked”. The audit subscriber maps the new type, so it
reaches the chain rather than stopping at the log.
Absence of a policy still ALLOWS. That is not a bypass, it is the absence of anything to apply, and turning it into a refusal would break every user who has never written one.
tests/test-policy-node-failclosed.sh (10 assertions). It extracts and
drives the REAL check_policy out of run.sh rather than reimplementing it,
because a second copy is how the two drift apart silently.
Two assertions exist to avoid vacuity. The engine-side check asserts on the engine’s REASON (“All policies passed” when the global file loaded, versus “No policies configured” when nothing was found) rather than its exit code: an empty global policy and no policy at all both exit 0, so an exit-code assertion there would prove nothing. And the nodeless cases prove the run.sh gate consults the global path, which the engine-side cases cannot, so both halves are needed.
Every mutation verified: restoring the fail-open, refusing without recording, unmapping the subscriber event, reverting the run.sh gate to project-local only, and removing the engine’s fallback branches each turn the suite red.
pre_execution). Writes, network, and
spend are not gated by this engine.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.
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.
README.md overstated the mechanism. It said Loki “assembles an agent
team from 41 specialized agent roles across 8 domains”. Measured: the review
selector keyword-scores 10 of the 41 (run.sh FOCUS_KEYWORDS), and the
other 31 exist as role descriptions in references/agents.md that the
orchestrator adopts per phase – nothing injects them into the prompt.
references/agent-types.md:9 already described this correctly (“prompt-defined
specifications the orchestrator adopts per phase, not separate processes”); the
README summary had drifted from it. Now says what actually happens, and cites
where each half lives.
tests/test-agent-types-loaded.sh (10 assertions). Pins a three-link chain
that can break silently at any point: agents/types.json exists, agents/ is
in package.json files[] so npm users receive it, and run.sh exports the
path to an existing file before the selector runs. Break any link and the
specialized roles quietly stop reaching reviews with no error anywhere.
Two assertions exist because a weaker version let a mutation through:
matching the export as a literal string passed when the path was repointed at
a nonexistent file, so the suite now resolves what run.sh actually names and
checks that file exists. It also checks the export precedes the selector,
since the quoted heredoc reads only what was exported before it ran – an
export that drifts below it is dead code that still greps fine.
All mutations verified: deleting the export, repointing it at a missing file,
and dropping agents/ from files[] each turn the suite red.
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.
A witness nobody reconciles is not a control. writeWitness
(src/audit/crosslink.js:234) had zero production callers, which was the
known half. The unknown half was worse: verifyUnified already called
verifyWitnessFile, and that function only checks the witness file’s own
monotonicity. Nothing ever compared a witnessed tip against the live chain.
Measured before the fix, with a valid witness file sitting right beside the forged chain:
witnessed tip : 337d47ce70... live tip now : d376cdd97d...
verifyUnified : {"valid":true, "witness":{"present":true,"valid":true}}
Simply calling writeWitness from the engine would have shipped a control
that controls nothing, while making the receipt look stronger. That is
security theatre, and it is worse than the honest gap v9.29.0 published.
reconcileWitnessedPrefix now compares each witnessed tip against the chain
entry at that position, and verifyUnified folds the result into its verdict.
The same forgery now returns valid:false and names the entry: “entry 2
hashes 0d2f5b17… but a witness recorded 91852f2b…; witnessed history was
rewritten”.
The comparison is prefix-based, not tip-equality. A chain legitimately grows after a witness is taken, so requiring the tips to match would fire on normal operation, and a guard that fires on normal operation gets turned off. Both failure directions are mutation-tested: removing the reconciliation misses three forgeries, and switching to tip-equality breaks honest growth.
The audit subscriber now writes witnesses. At session end, after the
flush (witnessing before it would pin a tip missing every buffered entry and
then read as truncation), and every LOKI_AUDIT_WITNESS_INTERVAL_SEC
(default 300). Periodic witnessing matters because a witness taken only at
shutdown is lost to SIGKILL, which is exactly when the trail matters most.
LOKI_AUDIT_WITNESS=0 opts out. LOKI_AUDIT_WITNESS_COMMAND ships each
witness line to an external party (a WORM mount, a timestamping authority).
A witness that cannot be written is reported on stderr and never takes down
the run; a missing witness reads as no_records, never as a pass.
tests/test-witness-reconciliation.js (11 assertions) and
tests/test-audit-js-suites.sh. The wrapper exists because
tests/test-manifest-truncation.js and tests/audit/crosslink.test.js both
passed and neither was registered in run-all-tests.sh, local-ci.sh, or any
workflow: CI had never run either. The audit chain is the trust core of
the receipt wedge and its tests were the least-run code in the repo. All three
now run, asserted individually rather than summed, and a missing node is a
failure to measure rather than a skip.
One assertion is deliberately inverted: the suite asserts that a re-forged
chain still self-reports valid. Asserting on agent.valid would miss the
whole defect, because the chain cannot detect this and is not expected to.
The trail verdict is what must go false.
LOKI_AUDIT_WITNESS_COMMAND is
what actually closes it, and it is off by default because it needs
infrastructure we cannot assume.LOKI_AUDIT_ENABLED (default false), so a
default install writes no agent chain and no witness. That default flip is a
separate change with its own migration note.loki-ts/src/runner/).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.
“Tamper-evident” overstated what the hash chains prove. Both
implementations compute an unkeyed SHA-256 over public fields from a constant
genesis: src/audit/log.js:16,127-134 (genesis is the literal GENESIS) and
dashboard/audit.py:58,194-200 (genesis is "0" * 64). Every input to the
hash is present in the file being edited, so anyone who can write the log can
recompute a complete, internally consistent chain over invented history.
Reproduced on v9.28.1 against both, not argued:
honest verify : {"valid":true,"entries":2,"brokenAt":null,"error":null}
forged verify : {"valid":true,"entries":2,"brokenAt":null,"error":null}
forged contents : NEVER HAPPENED | ALSO FORGED
What the chain does prove is real and worth keeping: it detects corruption and truncation. What it does not survive is the threat an audit log exists for – a writer who rewrites history. Note the direction of the failure: a broken chain is strong evidence of a problem, but an intact chain is not evidence of integrity, because it is what both an honest run and a competent forgery produce.
Corrected at every buyer-facing surface: wiki/Enterprise.md (which paired
the claim with SOC 2 / ISO 27001 / GDPR report generation),
wiki/Security.md, wiki/Home.md, the demo narration in
demo/run-demo-auto.sh, and the _compute_chain_hash docstring itself, which
asserted tamper-evidence at the point a future reader would trust it most.
wiki/Enterprise.md additionally now states that compliance reports are
generated in SOC 2 / ISO 27001 / GDPR shapes but that Loki Mode holds no
certification against those standards: the reports are inputs to your audit,
not a substitute for one.
docs/AUDIT-CHAIN-THREAT-MODEL.md. States what the chain proves, carries
both reproductions, and names what would actually close the gap: a keyed MAC
(which moves the problem to key custody, and on a developer laptop the agent
usually runs as the user), an external witness, or a signature over the tip
with an off-machine key. Only the last two survive an adversary who controls
the machine, which is the case that matters for third-party evidence.
writeWitness already exists at src/audit/crosslink.js:234 with zero
production callers. Wiring it is the natural next step and is deliberately not
bundled here: this release is about not overclaiming, and shipping the fix in
the same breath would blur whether the claim or the code changed.
tests/test-audit-chain-honesty.sh (6 assertions) and
tests/lib/scan-tamper-claims.py. The suite pins the measured property
itself: if someone later adds a keyed MAC or a witness, assertion 1 turns red
and the suite must be rewritten to match the new truth. The test tracks
reality rather than a wish.
Three details that are load-bearing, each found by the guard failing on itself:
entries_checked, not just the verdict. A
first reproduction guessed the entry schema and got valid=False,
entries_checked=0 – the verifier had rejected the probe, not detected
tampering. Asserting the verdict alone would have recorded a false all-clear
and concluded the chain was sound.tamper-proof into a line that already read
“not tamper-proof against …” exempted the whole line, and the mutation
stayed green. An honest caveat can no longer launder a false claim beside
it..loki/. It is runtime state, and
.loki/logs/bash-audit.jsonl recorded the very mutation commands used to
test this guard and reported them as claims. A guard over user runtime state
fires on whatever the user typed.A missing scanner reports UNMEASURED and fails, never clean. tamper-proof
against X stays permitted: src/audit/crosslink.js:462-465 uses it to draw
the correct distinction, and banning an accurate statement would push authors
toward vaguer language, which is the opposite of the point.
Every assertion is mutation-verified, including the property guard (simulating a keyed MAC turns it red) and the unmeasured path.
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.
tests/test-stop-latency.sh. docs/stop-latency.md published “about 1
second to SIGKILL” and labelled it MEASURED, while the file it named as
the enforcing test did not exist. The number was derived from reading a
sleep 1 in the source, never from timing a stop. A MEASURED label with no
measurement behind it is exactly what the previous release was about.
The suite now makes the label true. The victim installs a SIGTERM trap and would otherwise sleep 7200 seconds, so it tests timeout-independence rather than cooperative shutdown – a victim that dies on SIGTERM would pass whether or not the SIGKILL escalation existed.
Writing it surfaced a measurement error worth recording: the first version
drove loki stop end to end, and deleting the group SIGKILL entirely left
it green. loki stop reaps by two independent routes (_kill_pid on the
recorded pid, which carries its own kill -9, and the process-group path), so
an end-to-end test cannot attribute the bound to either. The suite now drives
_stop_group_by_pgid_files directly for the attributing assertion and times
the whole command separately. Both assertions are mutation-verified; the
end-to-end one alone was not sufficient.
tests/test-dashboard-multiproject.sh failed 4 of 29 assertions on macOS
and passed on Linux CI. _cleanup_registry_entry_state refuses a PID
registry entry whose parent directory fails realpath == abspath – a
deliberate symlink guard. On macOS $TMPDIR is /var/folders/... and /var
is a symlink to /private/var, so every entry the fixture built was
refused and the suite could never exercise the code it targeted. The product
check is correct; the fixture was the thing lying about its path. Resolved the
fixture root with pwd -P: 29/29.
tests/test-cluster-workflow.sh had never printed a single result. Under
set -euo pipefail, ((PASS++)) evaluates to PASS’s old value, so with
PASS at 0 the first passing assertion returned exit status 1 and killed the
script – no output, no counts, exit 1. Switched to ((++PASS)), which
evaluates to the new value: 12/12. The suite was also in no runner, so nothing
had ever noticed.
tests/test-failover.sh asserted host state it had assumed. It treated an
absent ANTHROPIC_API_KEY as “no authentication” and demanded the provider
report UNHEALTHY. But check_provider_health deliberately accepts an API key
or an OAuth session, because Claude Code supports both – so the suite
failed on every machine with a real Claude Code login while passing on a bare
CI runner. It now enumerates the same auth sources the function does and
asserts agreement with the documented contract: 14/14.
tests/test-cross-project-learning.sh asserted a UI that no longer
exists. Two assertions grepped autonomy/.loki/dashboard/index.html for
markers (learnings-patterns, fetchLearnings, and an API_URL literal).
That file was deleted in a451fb02 and is untracked; those markers now appear
nowhere in the repo except in the test itself. grep on a missing file
reports failure without ever saying the file is gone.
The feature moved to the dashboard API rather than disappearing, so the
assertions were retargeted to the surface that exists: the
/api/registry/learnings endpoint and its require_scope("read") guard. The
scope guard is the property worth pinning – a cross-project learnings store
read without authentication leaks one project’s history to another. Both are
mutation-verified. 9/9.
Three suites registered in tests/run-all-tests.sh for the first time
(test-cluster-workflow, test-cross-project-learning, test-failover).
All three existed on disk and were in no runner, so CI had never executed
them; one of them had never produced output at all. They pass now, and a
future regression in any of them will be seen.
docs/stop-latency.md states precisely what MEASURED covers: which victim,
which call, and the two-reaping-routes caveat that keeps the end-to-end number
from attributing the bound. The STOP-file worst case stays DERIVED, and a
guard now fails if either label drifts from what the suite actually checks.
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.
Buyer-facing docs claimed security enforcement the source disclaims.
autonomy/run.sh:515 states LOKI_ALLOWED_PATHS “Does NOT restrict
provider-driven agent writes (run.sh never sees them)”, and
check_command_allowed carries “intentionally NOT called by run.sh” with zero
callers. That honesty never reached the documentation: wiki/Enterprise-Features.md
listed both variables in a production security checklist, and
docs/certification/answer-key.md marked “LOKI_ALLOWED_PATHS restricts which
directories agents can modify” as the correct exam answer. Measured: zero
caveat mentions across nine buyer-facing files.
All nine now carry a SANDBOX-SCOPED note stating what is enforced (two
mount-time call sites in autonomy/sandbox.sh, plus one for operator-typed
loki sandbox run argv), what is not, and that neither enforces anything
unless LOKI_SANDBOX_MODE=true. Guarded by
tests/test-enforcement-doc-honesty.sh, which asserts marker PRESENCE rather
than phrase absence: a grep for “restrict” near the variable name would fire
on the caveat text itself.
Fixed at the same time: a pre-existing certification answer-key mismatch (the key said A, the quiz option was B).
loki config validate printed OK for a file it never checked. With no
usable parser the unknown-key walk returned nothing, and the caller turned
that silence into an affirmative config validate: OK with exit 0 for a YAML
file full of bogus keys. Reproduced before the fix. This shipped in v9.27.2
under my own mistaken description of it as “correct degradation” – silence in
the helper was fine, the caller asserting validity from it was not.
The helper now reports “could not check” as a distinct status, and the verdict
reads INCOMPLETE with a stderr line naming the skipped check. A host with no
python3 at all was affected for every format, not just YAML.
loki logs was dead. It read logs/session.log, a path nothing in the
tree writes, and reported “No log file found” while the runner’s real logs sat
in that same directory (autonomy/run.sh:22409). It now resolves the newest
autonomy-YYYYMMDD.log and falls back to agent.log.
The first version of this fix was worse than the bug: under set -euo pipefail,
ls exits 2 on a non-matching glob and pipefail propagates it, aborting before
the fallback – exit 1 with zero output. Caught by review before release. The
test asserts log CONTENT via sentinels, because asserting “the error string is
absent” passes on a silent abort.
report cost contradicted its own state file. It read only the cap from
.loki/metrics/budget.json and then substituted the current run’s cost, which
is unmeasured when no iteration has a recorded figure, falling back to 0.0.
Result: Used: $0.00 (0.0%) ... Status: OK over a file reading
"budget_used": 0.7992, "exceeded": true, while loki status showed 160%.
The --json surface asserted "exceeded": false, which is what automation
gates on.
It now reads the recorded spend, falling back to the current run only when the file carries no figure. Three other readers already did this; it was one divergent reader. The “Cost not recorded for this run” contract for unmeasured costs is preserved, and the exit code is unchanged.
docs/stop-latency.md. Enterprises ask how fast a run can be stopped, and
the answer was undocumented. Investigating it inverted the premise: loki stop
is already bounded at about 1 second (process-group SIGTERM, 1s grace, then
SIGKILL), while autonomy/run.sh:161 told users touch .loki/STOP - stops
immediately, which was false. The STOP file is read only at the top of an
iteration, so a STOP written mid-dispatch waits for the provider call to
return, bounded by LOKI_PROVIDER_CALL_TIMEOUT (default 7200s). The product
was recommending the two-hour path and calling it immediate.
Both numbers are published with their derivations, and the false claim is corrected in place.
The Bun runner now bounds its provider call. It passed no timeout at all,
so on that route a provider call had no upper bound and a STOP file could
never be observed. It now honors LOKI_PROVIDER_CALL_TIMEOUT like the bash
route, using the SIGTERM-then-SIGKILL escalation shellRun already
implemented (loki-ts/src/runner/providers.ts).
docs/COMPETITIVE-NEXT-10.md: item 4’s claim that quiet degradation was
“correct” is retracted with the reproduction that refuted it, and the hard
command blocklist is moved out of “architecturally unavailable”. A design
review found it IS enforceable via read-only bind-mounts over resolved binary
inodes (seccomp cannot do it: execve’s pathname is a userspace pointer a BPF
filter cannot dereference; a PATH shim cannot either). It stays OPEN and
unbuilt, with the scoping constraint recorded: only Docker container mode has
a mount surface, so it must fail closed in the other two modes.guard-changed.sh cried wolf on every autonomy/loki edit. Its
ShellCheck arm ran bare shellcheck -S warning, while the repo gate
(tests/run-shellcheck.sh) scans find . -name "*.sh" with exclusions
(SC1090,SC1091 globally, plus SC2034 under tests/, providers/ and
benchmarks/). Two consequences:
autonomy/loki has no .sh extension, so the repo gate never lints it
– but this tool did, hard-failing on 90+ long-standing SC2155/SC2034
warnings for any edit to the most-edited file in the repo.A pre-push check that always fails gets ignored, which is the same defect as
one that is too slow. The arm now mirrors the repo gate exactly. Verified it
still catches the real thing: the SC2034 in autonomy/verify.sh that blocked
v9.26.0 is caught under the corrected configuration.
scripts/guard-changed.sh was slower than the gate it replaces. A change
to autonomy/loki selects 144 suites, and six of them never finish
without a live model (test-magic-injection, test-magic-rarv,
test-mirofish-integration, test-model-override,
test-trust-core-tests-detect, test-watch-command). Measured end to end:
1320s – worse than the 26m50s FULL tier this exists to avoid. A gate
nobody will run protects nothing.
Each suite now runs under a 60s budget (GUARD_SUITE_TIMEOUT), and a suite
that hits it is reported as SLOW ... NOT measured with its name in the
summary – never counted as a pass. A timeout is an absent measurement, not
evidence of health.
Selection is also capped at 60 suites (GUARD_MAX_SUITES), and the count
that was not run is printed. Silent truncation would read as “everything
was covered” when it was not.
Same case measured after the change: 4m52s, 3 suites named as unmeasured.
Corrected the “~135s worst case” claim in the v9.26.3 entry. It was extrapolated from an unrepresentative 8-suite sample; the real figure was 1320s. The entry now states both the wrong number and the measured one rather than quietly editing history.
loki start --help now states the exit-code contract. docs/exit-codes.md
documents a strong two-tier contract: with LOKI_DURABLE_STATE=1, code 20
means “deterministic terminal failure, retrying cannot help”, which Helm wires
into a Kubernetes podFailurePolicy so a Job fails immediately instead of
burning backoffLimit. The help mentioned it zero times across 110 lines,
so a CI author read the help, saw only “0 on success, nonzero on failure”, and
built the coarse gate. A contract nobody can discover might as well not have
shipped.
The help now names LOKI_DURABLE_STATE, code 20, what a platform should do
with each code, and points at the full table rather than duplicating it.
A drift assertion fails if the code stated in the help stops matching the code in the doc: two documents disagreeing about a value a Job is configured on is worse than one document.
loki verify --help needed no change – it already carried an
EXIT CODES section. A test now asserts there is exactly one, because adding
a second is an easy mistake and duplicated help that drifts apart is worse
than a single statement.
The evidence receipt now reports model provenance.
autonomy/lib/decision_record.py has written an append-only trail to
.loki/decisions/decisions.jsonl once per dispatch for some time, and
already computed the audit fact worth showing: model_changed, meaning more
than one model id served a single project. The proof generator read that file
zero times, so the receipt could not answer the question a regulated
buyer actually asks – did the deciding component change mid-run without
anyone saying so? The run-level “Model” row cannot answer it.
Both renderers now carry it, with three states never collapsed:
measured – per-model dispatch counts and whether the model changedno_records – no trail for this run, stated explicitlyunreadable – the trail exists but could not be read, with the reasonA section that renders nothing when the trail is absent reads as “no swap
happened”, which is the false green this receipt exists to prevent. Absence
of evidence is reported as absence of evidence. Corrupt trail lines are
counted in unparseable_lines rather than dropped, because an audit trail
that quietly discards what it cannot parse is worse than one admitting a gap.
model_changed is a disclosed fact, not a fault. A tier clamp, an
operator override and a mid-flight failover all cause it legitimately.
affects_verdict is false: this is provenance and never moves the verdict.
A receipt generated before this release has no decisions key and stays
silent rather than being described – a run that never recorded the trail
cannot honestly report on it.
scripts/guard-changed.sh now sees uncommitted work. It compared only
committed history against the base, so running it before a commit – the one
moment a pre-push guard is for – reported “nothing to guard” and guarded
nothing. It now covers committed, staged, unstaged and untracked paths.
Measured on this release’s own diff: 12 receipt-guarding suites in 37s.yq – the documented fallback – is preinstalled on the GitHub
ubuntu-24.04 runner. The fallback worked exactly as designed and returned
rc=1; the test called that a failure and blocked v9.26.2 from publishing.
The assertion now names its condition (command -v yq) and checks the
correct outcome for each case: detection via yq when present, quiet
degradation only when no parser exists at all. No production code changed.scripts/guard-changed.sh – runs the test suites that reference the
files in your diff, plus ShellCheck on the changed shell files, before a push.
The FAST tier is the release gate and it defers the 282-suite shell run, so three releases in one cycle (v9.25.0, v9.25.1, v9.26.0) failed to publish on checks that guard files we had just edited – each discovered one 25-minute CI cycle at a time. CLAUDE.md already stated the rule; nothing enforced it.
Measured: 8s for a one-file change. The “~135s worst case” figure in this
entry was extrapolated from an unrepresentative 8-suite sample and is wrong:
the real worst case (a change to autonomy/loki, 144 suites) is 1320s,
because six provider-backed or long-polling suites never finish without a
live model. v9.27.2 bounds each suite and reports the unmeasured ones by
name, bringing that case to ~5 minutes. Selection matches on repo-relative
paths only – basename matching
pulled 486 suites for autonomy/loki (more than the FULL tier) while
path-only pulls 143 and still selects the suites that actually broke v9.25.0
and v9.25.1. Release-churn files (VERSION, package.json, Dockerfile, dist)
are excluded, since they are named in hundreds of suites and are already
checked by the FAST tier.
It is necessary, not sufficient: it cannot catch a failure that depends on the CI environment differing from yours, which is exactly how v9.26.2 failed. That limitation is documented in the script header.
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.
yq. The check added in
9.26.1 depended on pyyaml alone, so on a host with yq but no pyyaml – and
CI installs neither – YAML configs silently got no detection at all. It now
reaches for pyyaml first and yq second, the same order the rest of
config-map.sh uses. With neither parser present the walk still degrades
quietly rather than inventing a verdict, and the JSON path is unaffected
either way.loki config validate now detects unknown keys in JSON and YAML configs.
Extraction walks LOKI_CONFIG_MAP and pulls each KNOWN path out of the file,
so a key the map does not contain was never emitted and could not reach the
validate loop. A misspelled key validated clean (rc=0) while the identical
typo in .env format was correctly rejected (rc=1) – the check existed but
was blind to two of its three input formats.
Detection now walks the file’s own key set and diffs it against the map.
Scoped to validate only: the load and emit paths are unchanged, so a config
that runs today still runs. Container parents of a mapped key are not reported
(in {"dashboard":{"port":1}}, dashboard is a container, not a typo), and
inert metadata written by loki init (version, template, created) is
allowlisted rather than rejected.
YAML detection requires a YAML parser (pyyaml, or yq as a fallback, the
same order the rest of the config code uses). With neither installed, YAML
configs get no unknown-key detection and validate exactly as before – a
missing parser must degrade quietly, never invent a verdict. JSON and .env
detection need no extra dependency.
Removed a dead local declaration in _verify_llm_review. Five variables
were assigned and never read; every return path uses explicit printf
literals. ShellCheck SC2034 flagged two of them, which failed the repo-wide
lint gate and blocked the v9.26.0 release from publishing. No behavior change.
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.
loki verify now runs an LLM review stage, on by default. Until now the
receipt reported llm_review.status = "skipped" with the reason
“deterministic-only MVP (30-day cut)” for every user. Half the quality story
was unshipped, and anyone evaluating the receipt could see it. The stage runs
through the existing pure-HTTPS loki internal sdk-judge bridge, so
verify.sh stays free of the iteration-loop globals that
completion-council.sh depends on (a constraint documented at
autonomy/verify.sh:6-13).
Three states are recorded and never collapsed into each other:
reviewed - the judge ran and returned findingsskipped - the operator passed --no-llmunavailable - no API key, transport failure, or malformed responseFail-closed throughout: an unavailable judge records the reason rather than
reporting a silent pass. The raw payload is written to llm-review.json
alongside the receipt.
--no-llm is now a real opt-out. It was previously parsed and discarded
(shift only). Scripts already passing it keep working and now get the
behavior the flag name has always implied.
llm_review is recorded in the
evidence document but does not influence the verdict this release
(affects_verdict is false in the schema). Verdict influence lands behind
an explicit flag once it has been measured on real diffs; flipping it here
would silently break anyone gating CI on exit 0.LOKI_VERIFY_LLM_DIFF_BYTES (default 200000).
When a diff is truncated, the truncation is disclosed in the recorded reason
rather than being applied silently.loki web --prd <file> no longer discards the spec silently. The dashboard
cannot accept a PRD, so dropping the flag is correct – doing it without a word
left the user in front of an empty dashboard believing their spec had loaded.
It now says so and names loki start.loki web stop and loki web status now act on what loki web start
launched. Start redirects to the dashboard (57374) while stop and status still
targeted the deprecated Purple Lab (57375), so a user could not stop what they
had just started.loki dashboard, which prints help and exits; it now says
loki dashboard start. This is the last line every first-run user reads.claude -p "$prompt" to be adjacent
failed on it, blocking v9.25.0 from publishing while the behavior it guards was
intact. The assertion now tests the invariant rather than argv layout.LOKI_DOCS_TIMEOUT still forces it.--model, so all eight calls inherited the account default – Opus for many
users, at 5x Sonnet’s input and output price, for a summarization task.
Pinned, with LOKI_DOCS_MODEL to override.sdk_invoker.ts
“applies cache_control on that split” as a statement of fact. That path is
opt-in and default OFF (LOKI_SDK_PROMPT_CACHE=1), it covers the raw-SDK judge
rather than the main agent path, and on the bash route [CACHE_BREAKPOINT] is
a documentation anchor that sets no header. The prefix-ordering rule it teaches
is still correct on every route; only the claim about what enforces it was wrong.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.
sleep 2 is off the critical path of every build. The dashboard is
polled on the same /api/status endpoint the reuse path already trusts and
proceeds the moment it serves. The liveness property is preserved rather than
traded for speed: the process must still be alive when the poll ends, which is
strictly stronger than a single check at t=2s, and the flat sleep remains as a
fallback where curl is unavailable.fast-uri advisories (GHSA-5jgf-p345-68v8,
GHSA-f65p-4m7j-42xc, GHSA-fph4-wmhf-6fwf, GHSA-jqff-g426-hqxp): SSRF via
malformed IPv6 normalization and via repeated hostname percent-decoding, plus
two host-confusion paths. Fixed at the source with an override to the patched
release rather than waived, because the accepted-advisory list exists for
advisories that are genuinely unreachable in the shipped CLI, and a URL parser
reached through ajv is not one to argue about when a fix is one patch release
away. This predates v9.23.0: the same gate already failed on 2026-09-07.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.
plugin.json declared
hooks: "./hooks/hooks.json", but Claude Code loads that standard path
automatically, so the manifest registered it twice and the loader rejected the
whole plugin: all three skills, the commands and the MCP server died with it
while claude plugin install still reported success. manifest.hooks is only
for hook files beyond the standard one, so the key is dropped. Guarded for both
the string and array forms while still allowing a genuine extra hook file.
Reported with the correct diagnosis and fix in #195/#196 by robert-clayton.LOKI_SUBAGENT_CONCURRENCY opts in. An operator’s own
pre-set value always wins, and LOKI_SUBAGENT_CAPACITY=0 opts out entirely.plugin.json had drifted three releases behind VERSION (pinned at
9.22.10 while 9.22.11-13 shipped), so installed plugins were never offered the
update. The release checklist already required this file to track VERSION; a
checklist did not prevent three consecutive misses, so a test now enforces it.^0.111.0 could never
float to 0.124.0 and was a hard pin wearing a range’s clothes. Gated on a real
test pass rather than a version check, because three intervening releases are
breaking (0.3.233 removed the todo/task tools from the default surface, and
Loki reads TodoWrite): typecheck clean, 1589 pass / 0 fail across 113 files.claude-fable-5-1 (released 2026-09-01;
1M context, 128K output, $10/$50 per MTok, cache reads 0.025x base rather than
the usual 0.1x). claude-fable-5 is retained as a legacy catalog entry rather
than deleted.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.
loki doctor --json still emits its
complete parseable report, but now exits nonzero whenever required checks
fail on both the Bash and Bun routes. Optional warnings remain non-blocking.Headers, URLSearchParams, and FormData paths without local overrides.loki ship --publish is the deliberate
GitHub mutation boundary. It publishes the exact prepared title and evidence
body, reuses an existing open PR idempotently, preserves artifacts on
failure, and provides exact branch rollback guidance.Live capability/task-class routing: the Bun runner now routes each
iteration’s RARV phase to a task class (REASON -> planning, ACT ->
implementation, REFLECT -> review, VERIFY -> verification, and any retry
-> recovery) and hands that class to the capability router, which previously
had no production callers. The router moves the model TIER only, leaving
providers.ts the sole owner of model identity so the LOKI_MAX_TIER
ceiling still applies. Only a decision the router actually made
(task_class, session_ceiling, explicit_override) may move the tier, so
an opus- or haiku-pinned session is never clobbered down to development;
fable-pinned sessions are skipped because the tier has no capability
mapping. Default off, gated on LOKI_CAPABILITY_ROUTER=1.
Execution-manifest enforcement before parallel merge: the parallel
worktree orchestrator can now pin a manifest of the expected base SHA and
per-stream path scope, then validate each stream’s actual changed paths
against it before the branch is merged. A rejected stream is logged and
blocked instead of merged. A new hidden bridge, loki internal exec-manifest
plan|validate, connects the bash orchestrator to the Bun implementation.
Default off, gated on LOKI_EXEC_MANIFEST=1; with the flag unset the
parallel workflow performs no additional I/O and stays on its previous path.
loki quickstart --from-preview - --yes. Stdin is
bounded to 1 MiB and the preview is revalidated and re-estimated before the
existing build boundary.loki start owner/repo#123 --pr
imports a bounded GitHub issue, runs the existing autonomous engine, and
publishes a pull request only after the generated proof reaches a verified
verdict.loki quickstart --list-templates enumerates all 21 shipped starter
templates and their purposes in stable catalog order before provider
discovery, estimation, consent, project writes, or build execution.
--json emits the same catalog as a schema-v1 object for local automation.loki quickstart "<idea-or-prd>" --yes now
supports non-interactive shells while preserving the estimator-backed plan,
explicit argv consent, collision-safe PRD writes, and the existing build
path. Operators can select an exact shipped starter with --template.--dry-run shows the same deterministic
template and estimator plan before any provider, file, or build boundary;
--json emits a stable schema-v1 object for local automation.proof.json.--require-verdict PROMOTE|HOLD|ROLLBACK.
Complete results retain their actual verdict while returning a nonzero exit
when the caller-required verdict does not match.timeout is unavailable, so stuck child
runners are terminated and the macOS Bun matrix completes reliably.loki outcomes canary {plan,record,evaluate,receipt,verify} from any
directory. Consented observations, decisions, and portable receipts remain
bound to exact local evidence and preserve fail-closed, no-write refusals.Source-bound measurement closes the outcome-routing canary loop without invoking or switching a provider.
tools/outcome-canary-evaluate.py): validates a canary plan against its
exact router report, consumes bounded consented control and challenger
observations, and returns PROMOTE, HOLD, or ROLLBACK with stable reason
codes. The evaluator rejects tampered, mismatched, stale, sparse, and
threshold-invalid evidence and remains offline and read-only.Four features from the parallel lanes plus one post-release fix to v9.20.0’s lineage guard.
loki start <dir> accepts an existing repository directory, not only a
spec file, with an opt-in fast repository pass for the common case.tools/outcome-router.py): picks a route from
recorded outcome data rather than a fixed default.tools/outcome-canary.py): rolls a
route change forward on evidence and back out without one.A reaped child’s lineage marker is UNKNOWN, not absent. v9.20.0’s Tests
run failed on Python 3.10 only – 3.11, 3.12 and 3.13 passed in the same
matrix – with 127 != 0 and “provider attempt lineage marker is
unavailable”. 127 is reported for a LAUNCH failure; the runner had in fact
launched and completed, then been reaped before the check could read
/proc/<pid>/environ.
That read has three “process is gone” outcomes and only one was handled.
ENOENT raised _LineageUnknown; ESRCH was swallowed by
except OSError: return False; and a zero-length read raised nothing at all,
splitting to [b""] and answering “no marker” for a process that carried
one. Both now classify as unknown and reach the caller’s existing
poll()-based decision.
Fail-closed is preserved and asserted twice: a populated environ without the
marker still returns False, and EACCES/EIO stay False – a read failure
is not evidence of a completed child. Only “the process is gone” is
reclassified. Linux-only: macOS has the pipe-handle fallback, which is why
the local repro passed 5/5 while CI stayed red.
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.
GET /.well-known/jwks.json – unauthenticated, deliberately, because a
receipt only a token-holder can check is not independently verifiable.
kid and multi-key rotation from day one: without them the first key
rotation would make every historical receipt fail verification, and a
receipt that stops verifying is indistinguishable from a tampered one.loki proof verify <id> --jwks <url|file>. The third-party path. A file
is accepted as well as a URL so an air-gapped reviewer can verify offline.
Four outcomes kept distinct – VERIFIED, FAILED, ABSENT, NOT CHECKED –
because “we could not check” must never render as “unattested”.LOKI_RECEIPT_SIGNING_KEY_FILE is set.unknown is its own series and is
never folded into verified./metrics: loki_receipts_total,
loki_receipts_by_verdict, loki_receipts_with_provenance. Twelve metric
lines became nineteen; verification was previously invisible to monitoring.autonomy/lib/
outcome_contract.py, tools/proof-passport.py, schema, tests).loki dashboard start refused an exposed bind without auth.
dashboard/auth.py returns allow when neither LOKI_ENTERPRISE_AUTH nor
OIDC is configured, leaving all 166 scoped endpoints open. Fine on loopback;
not on 0.0.0.0, which docker run -p 57374:57374 <img> dashboard start
reaches. Now refused unless auth is configured, checked AFTER argument
parsing so an explicit --host is covered. Refuses rather than auto-minting
a token: a credential the operator never sees is worse than a refusal..resolve() while the attester embeds the
unresolved path; /var -> /private/var meant the redactor never matched.
Green on Linux CI, leaking on every developer Mac.schema_version. The generator has
stamped it on every receipt since it shipped and nothing read it – a future
incompatible receipt would have been judged by the wrong rules and reported
as a clean pass. Fails closed on absent or unknown major; every minor of
major 1 still verifies, so no existing receipt breaks.loki web user.
/api/proofs* existed only on the dashboard server, not the one loki web
runs, and the panel rendered only while a build was in flight.droid measured in the competitor surface audit; the head-to-head
corpus now refuses a zero-result row without a positive control.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.
The DA review cases now carry their own budget. Shard 2/4 failed three consecutive CI runs while the suite passed 43/43 locally, and the failing assertion MOVED between adjacent devils-advocate cases. A failure that relocates between neighbours is a shared environmental cause, not a defect in any one assertion. All four DA cases took the shared default budget, and that path dispatches a reviewer AND a speculative devils-advocate – two sequential model calls under one budget. Now scaled explicitly: tight locally, generous on a contended runner. Verified 43/43 in both configurations.
A failure message that named a cause it had not measured. “council dissent discarded a speculative DA blocker” asserted a fail-open across six shared conditions. Each clause now reports separately with the assertion text captured. The first attempt at this fix re-ran the assertions in the else branch, which passes on its own correct copy and reported an empty cause – the gating run is now captured once and branched on.
Closes the spec-to-production loop. Two gaps, both of which broke the promise of an end-to-end autonomous lifecycle.
A deploy is now authorized by a verified receipt, or refused. loki deploy
was print-only: Loki built, verified, produced an Evidence Receipt, then handed
the user a string to paste. --execute now runs the detected command, but only
when the receipt’s verdict is VERIFIED, its facts still hash to their digest, it
anchors to the tree being deployed, and the tree is clean. UNSIGNED, UNCHECKED
and TAMPERED all refuse, because integrity is not provenance. Fail-closed
throughout: a check that cannot be evaluated is a refusal, and the refusal names
which check could not run. Destructive operations are refused even with a valid
receipt; git push and PR creation stay out of scope; Loki never reads cloud
credentials. Every executed deploy writes its own receipt naming the command,
exit code, anchor and authorizing run_id.
Known limitation, stated rather than hidden: an unanchored receipt cannot
authorize a deploy, and 8 of 9 receipts measured in this repo carry no
baseline. --execute will refuse those by name. A per-invocation override
covering “unknown” but never “known wrong” is the fix and is not in this
release.
The build pipeline is visible while it runs. dashboard/api_phases.py was
262 lines of working phase-timeline parsing that nothing imported and nothing
consumed. Now wired to an endpoint and a panel. An unmeasured duration renders
as unmeasured rather than 0; not-started, running and failed are three distinct
renderings, because collapsing not-started into running shows a stalled build as
healthy; sampled or stale data is labelled as such.
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.
A failed remote build exited 0. loki start --remote was shipped in
9.18.2 with the job status taken from the LAUNCH result, so a build that
started and then failed reported success. Anyone gating CI on it got a green
pipeline on a red build – the exact defect class this product exists to
prevent. Terminal statuses are now explicit: only passed means the build
succeeded, failed means it ran and failed, and unknown (detached past the
wait window, exit code never observed) fails CLOSED because an unobserved
outcome is not a pass. queued and running are deliberately not terminal so
a client can tell “not done yet” from “done and failed”.
A webhook credential could overwrite a job’s terminal status. The same
defect had a second door: job_id was honoured from any payload, and a GitHub
webhook payload carries none of its own, so a holder of the webhook HMAC could
supply one and flip passed back to fired. That is a webhook credential
reaching a /jobs capability, which the separate-credential design exists to
prevent. job_id is now honoured only for a remotely-submitted job.
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.
docs/VERIFICATION-COST.md corrected: loki proof verify does not simply
“exit 1 on tamper” – an untampered receipt whose tree has drifted also exits
hash_ok for integrity. A three-command reproduction is included.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.
GET /jobs/<id>/proof serves the receipt and its detached
signature; loki start --remote re-hashes and verifies before reporting
success, with four distinct verdicts: VERIFIED, UNSIGNED (integrity only, not
provenance), UNCHECKED (signed but the key is absent, so it cannot be
evaluated), and TAMPERED. UNCHECKED is deliberately not folded into either
neighbour: calling it TAMPERED accuses a user of a forgery that did not
happen, and calling it UNSIGNED is false about a receipt that is signed.loki start could kill the user’s other terminal sessions. This is the
headline fix and it affects anyone running builds from a terminal that also
holds an editor or agent session. reap_own_process_group() TERM/KILLs every
PID sharing a recorded process group; .loki/loki.pgid was removed only on
the normal exit path, so a Ctrl+C’d or crashed run left it behind. Measured on
a real machine: five orphan files, the oldest 1435 hours (60 days). PIDs
recycle (macOS wraps near 99999; max observed there was 99762), so a stale
pgid eventually matches a LIVE unrelated shell group, the “is this my own
group?” check passes, and every sibling in that terminal is killed.
The file is now stamped pgid/boot/started; the reader requires the boot id
to match this boot AND the stamp not to predate the reading process, which a
recycled pgid cannot forge. Removal moved onto the EXIT trap, guarded on
BASHPID rather than $$ (which does not change in a subshell). Fails closed:
an unprovable stamp reaps nothing, because an orphaned agent is strictly less
harmful than killing someone’s editor.
A green doctor recommended a command that exits 2. With the bundled SDK
and no provider CLI, doctor PASSed and its final line said
Next: loki quickstart – which exits 2. A warning existed 520 lines above
the recommendation and never reached it.
POST /jobs on the trigger server: an authenticated
non-GitHub submit with a credential separate from the webhook HMAC,
constant-time comparison, fail-closed 503 when unconfigured, and validation of
untrusted spec input. Plus helm/loki-mode (receiver + worker + Redis) with
one tenant per worker, a worker grace period that exceeds the drain budget,
and a value-gated NetworkPolicy that carves the cloud metadata endpoint out of
any operator-supplied CIDR.loki telemetry analytics on|off|status. The first-run funnel was gated on
ANALYTICS_ENABLED, a key with no writer anywhere in the repo, so drop-off was
structurally unmeasurable. Still default-off and opt-in.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.
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.
loki verify returned BLOCKED on a clean tree, permanently. The test
runner was chosen by grepping package.json for "jest" / "vitest" /
"mocha", which matches a devDependency. This repository is the case
that exposed it: jest is a devDependency with no jest config while
scripts.test runs bash -n plus node --test, so verify ran jest, jest
globbed 895 files that are not jest tests, and every run blocked for a defect
that did not exist. Now reads scripts.test with a JSON parser and runs what
the project declares. Fixed on both routes (autonomy/verify.sh and
autonomy/run.sh). A false BLOCK is the more damaging direction of that
error: a gate that cries wolf every run trains users to ignore the verdict.npm audit --omit=dev reports 0, so nothing a
user installs is affected; the old wording read as “the shipped product is
vulnerable”. Adds one audit call, measured at 418ms (0.5% of a 77s verify)./api/proofs
served receipts with a verdict, file count and cost, and nothing in the UI
read it. Adds receipts, learnings and budget panels, each rendering an absent
value as - or “not measured” rather than a fabricated zero.tests/e2e/dashboard-evidence-panels.mjs),
in the fast tier at 7s. Three unit tests over stubbed fetches all passed
while the real page rendered three empty panels, so a real DOM against a real
server is the only non-vacuous check here.run_test now reports a missing script as a bookkeeping fault, not as a
product failure. Removes 13 stale registrations found this way.Completes the 10-item competitive roadmap.
Agent readiness – a measurement where the competitor ships an opinion.
Factory AI’s Agent Readiness Model records modelUsed and reasoningEffort
per report, so its score is a model’s opinion and two runs can disagree about
the same commit. Ours is filesystem facts: three runs of the same repo produce
byte-identical output, with no key, no spend and no network. It asks the
narrower question our product depends on, can an agent verify itself here,
and does not duplicate the deterministic maturity rubric
loki modernize heal --assess already ships.
SCIM 2.0 provisioning. Measured before building: OIDC is real here (76 references in dashboard/auth.py) and SAML and SCIM were both zero. The gap was never authentication, it was PROVISIONING – with OIDC alone a terminated employee stops being able to log in while an already-issued token keeps working. IdP groups map onto the scope hierarchy auth.py already enforces, so there is one authorization model rather than two that drift. An unknown group maps to the lowest scope, and a corrupt store denies rather than reading as “nobody provisioned”; both are mutation-checked. Deprovisioning is soft by default because an audit log that cannot say who acted is not an audit log.
loki verdict – the five measured signals, where a reviewer sees them.
A moat nobody sees is not a moat. One block, five lines, each a measured fact
or an explicit UNKNOWN, rendered into the Evidence Receipt already attached to
PRs. No composite score: averaging a revert count, a hash comparison and a
model id yields a number nobody can explain, which is what competitors already
ship. UNKNOWN lines are printed, never hidden.
docs/VERIFICATION-COST.md – what our verification costs, and what it does
not prove. The FULL gate takes 23 to 26 minutes and we do not offer a mode
that makes it free. The page states that the unsigned receipt path is
forgeable, that only four of eight gates are agent-independent, that
verification cannot prove the spec was right, and that 0 of 9 of our own
receipts are anchored so loki outcomes reports UNKNOWN for all of them. A
14-assertion test keeps it true rather than merely present.
Claim grounding – the completion claim is the one artifact nobody checks. Our evidence gate has six axes (diff non-empty, tests green, runtime boot, no-mock, authorization, secret leak) and every one is a REPO-LEVEL fact. None reads what the agent actually claimed. So an agent could finish by asserting “added retry logic to payments.py and covered it with tests” while the diff showed a README edit, and all six axes pass. Path-shaped tokens in a claim are now resolved against the real changed-file set. Deliberately fail-open: only a path demonstrably absent from the diff is a finding, because a check that fires on ordinary prose gets disabled inside a week and takes the real detections with it.
LLM Decision Record – which model, at what temperature, made each call. Factory AI’s audit log has eight event types and not one records an agent action; agent forensics exists only as customer-built OTEL. Records are append-only and make a mid-project model or temperature change detectable, which is what makes “we ran an approved configuration” falsifiable. The field set is a pinned allowlist: a prompt body, an API key and file contents are all refused, with the refusal surfaced rather than silent. Nothing is transmitted.
Failure memory – learn from being wrong without poisoning the next run. Factory has no memory system at all; Devin discards session state at teardown and ships a “Misleading Knowledge” tab enumerating memory items that led it astray. That names the mechanism: memory written from an agent’s narration records what the agent believed, which is exactly what was wrong when it failed. So a lesson requires a named gate, a verdict, and EVIDENCE, and is refused without it. Recall returns checkable counts rather than generalizations, and states what happened rather than prescribing a fix.
loki intent – detect the drift a verification gate structurally cannot see.
Our gates prove code matches spec. They cannot prove the spec was RIGHT. 8090 AI
documents a real build where “the software converged with the interpretation.
The interpretation had diverged from the intent” – a perfect verification gate
passes that build and the build is still wrong. No competitor ships an answer:
8090 published the argument and their own “Tests” module has zero documentation
pages and zero changelog entries.
An intent statement is linked to a spec requirement at that requirement’s
current content_hash, so divergence is a pure hash comparison against
.loki/spec/spec.lock. Verified end to end: a requirement rewritten from “users
can export their data” to “admins can export via internal API” reads
LINKED-STALE naming both hashes, while the code would still match that spec byte
for byte.
There is deliberately no semantic fidelity score. Asking a model “does this spec express this intent?” and printing a percentage is an LLM judgment wearing the costume of a measurement, and our receipts exist to separate deterministic FACTS from AI ASSESSMENTS. Two structural tests enforce the absence against executable code, so it cannot be added later as an improvement without the suite going red.
Pre-edit snapshot – measure the agent, not the agent-plus-whoever-fixed-it. Every quality number we publish is measured after a human may have touched the diff, so a strong reviewer flatters the agent and a weak one maligns it. 8090’s framing: a post-edit score “describes the writer-plus-AI workflow, not the AI alone.” An immutable snapshot of the agent’s output is captured at the moment it stops, with a sha256 anyone can recompute.
Write-once is load-bearing: re-capturing after a human edit would record the human’s work as the agent’s, reintroducing the exact contamination via the tool meant to prevent it. It scores nothing on purpose – a low pre-edit result is a diagnostic, not a failure.
loki outcomes – measure whether the work was RIGHT, not that it happened.
Every competing agent reports volume. Measured against their own published
docs: Factory AI’s analytics expose files edited, lines modified, commits, PRs
created, tokens and DAU, and no defect rate, no revert rate, no change-failure
rate; their telemetry doc leaves outcome correlation to the customer’s own
stack. Devin’s security page concedes the agent “can still experience
hallucinations, introduce bugs into code” and points at your existing review.
Both can prove the agent was busy. Neither shows it was right.
loki outcomes [--json] [--run-id X] follows each Evidence Receipt past the
moment it was written and reports, from local git alone: was it reverted (read
from git’s own “This reverts commit” trailer, so a fact rather than an
inference), did its lines survive (git blame --porcelain attribution), how
much was reworked, and the change-failure rate across receipts. Read-only; it
never writes to the repo it analyses.
The anchor gate is the feature. facts.git.head_sha is git rev-parse
HEAD at receipt-generation time, which for uncommitted work is the run’s
starting commit, not what the change became. On this repo’s own nine receipts,
eight carry an empty base_sha and share a head_sha whose commit touches two
files while those receipts attest to eight. Following it regardless would have
attributed one commit’s fate to eight unrelated runs and printed it as a
change-failure rate. File-overlap was tested as a fallback and rejected (those
receipts overlap that commit by two files, so any overlap rule accepts all
eight). A receipt is measured only when sha algebra proves base..head is that
change, including a full file-set match.
So on this repo today it reports ANCHORED 0 of 9 with a reason distribution and
a change-failure rate of UNKNOWN. That is the correct output: an unmeasurable
denominator yields 0.0 if nobody guards it, and 0.0 would look better and be a
lie. Verified in a synthetic repo that an anchored change reports SURVIVED, and
after git revert the same receipt reports REVERTED with three independent
signals agreeing.
loki verify works on stock macOS Bash 3.2. A quoted-heredoc comment no
longer breaks parsing, and the Bash 3.2 gate now checks shipped entrypoints
rather than test scripts alone.A running build displayed the badge “PENDING”. This was a direct
consequence of 9.12.4: making the runs API read the orchestrator’s
currentPhase widened the status vocabulary it emits to include building,
bootstrap and complete. The dashboard’s RUN_STATUS_CONFIG did not know
those, and the lookup falls back to pending:
const cfg = RUN_STATUS_CONFIG[status] || RUN_STATUS_CONFIG.pending;
So an actively building run told an operator it was queued – worse than showing nothing, because it states something false with confidence to someone deciding whether their build is stuck.
unknown had the same defect and is arguably worse: the API returns it when
NO signal could be read at all, and rendering “Pending” claims the run is
queued when the truth is that nothing is known. It now renders “Unknown”.
The regression test DERIVES the required vocabulary from autonomy/run.sh’s
own currentPhase writes rather than hand-copying a list, so it fails if the
runtime gains a phase the UI cannot render, and includes a vacuity guard so
an empty match set fails rather than passing silently.
The runs API reported unknown for every live run. Found by running a
real build, not by reading code: during an actual loki start, the CLI and
the API disagreed about the same workspace at the same instant –
loki status --json reported phase=BUILDING while api_runs.list_runs
reported status=unknown.
_current_status read only .loki/session.json, which the current runtime
does not write. A live run writes .loki/state/orchestrator.json, and the
CLI reads its currentPhase (autonomy/loki:4782). The API was keyed on a
file that never appears, so every live run read unknown while the truth sat
one directory away with 24-second freshness.
Precedence is preserved: PAUSE wins, then STOP, then session.json if a
deployment writes it, and orchestrator.json is consulted last. A missing
signal still yields unknown and never completed, because absence of a
signal is not evidence of success.
Verified against the live run that exposed it: after the fix the CLI reported BUILDING and the API reported building on the same workspace. That run went on to build working code from its spec, pass its own test, complete a four-reviewer blind council, and write a receipt recording $0.7381 of measured cost.
WebSockets were outside the boundary added in 9.12.2. That guard is an
@app.middleware("http"), which only wraps HTTP scopes, so every WebSocket
route stayed reachable. Measured with auth off from a routable remote
address, both accepted the upgrade:
/ws CONNECTED
/ws/collab CONNECTED
/ws/collab is writable, so a network caller could push collaboration
state. The routes are not careless – /ws validates a query-parameter
token when enterprise auth is ON, and FastAPI’s Depends() does not work on
websocket routes – the hole is the auth-OFF default.
A plain ASGI middleware now applies the same decision to websocket scopes: same trusted-proxy resolution, same loopback rule, same deferral when auth is enabled. Registering it centrally also covers sockets declared by other modules, which a per-route decorator would miss. A refused upgrade closes with policy code 1008 rather than being dropped, so a client can tell refusal from a network fault.
Verified: direct remote refused on both sockets, loopback still connects
(the SPA depends on it), an untrusted X-Forwarded-For is ignored, a
trusted proxy forwarding a remote client is refused, and a scope with no
peer address fails closed.
A remote anonymous caller could stop a running build. Every mutating
dashboard route carried Depends(auth.require_scope(...)), and all 46 of
them were bypassable: require_scope returns True when enterprise auth is
disabled, which is the default. Measured with LOKI_ENTERPRISE_AUTH unset,
POST /api/control/stop and POST /api/control/app-stop both returned 200
with no credentials.
Bound to 127.0.0.1 that is harmless. But running with
LOKI_DASHBOARD_HOST=0.0.0.0 is a documented container configuration
(docs/architecture/DASHBOARD_V2_ARCHITECTURE.md), and there anyone on the
network could stop a build.
Mutations now pass through ONE central boundary rather than per-route checks, so a route added later is covered the moment it exists:
auth enabled require_scope decides, unchanged
loopback caller allowed, exactly as before
non-IP peer (test/UDS) allowed; a name is not evidence of remote
routable remote, no auth 403
A reverse proxy on the same host defeated that check. A proxy presents
127.0.0.1 as the peer, so “the peer is loopback” did not mean “the caller is
local”: a remote request carrying X-Forwarded-For through a same-host proxy
reached POST /api/control/stop and got 200. Proxies are now named
explicitly in LOKI_TRUSTED_PROXIES; the forwarded client is believed only
when the peer is a declared proxy, and an undeclared proxy’s headers are
ignored rather than trusted, because any direct caller can set them.
Sensitive reads were open too. Gating mutations alone left /api/logs,
/api/secrets/status, /api/github/status, /api/tasks,
/api/council/transcripts and /api/proofs returning 200 to an anonymous
remote caller. Reads that expose workspace or credential-adjacent state now
pass through the same boundary.
/health and /metrics are deliberately excluded, so container health
probes and Prometheus scrapes keep working with no configuration – neither
carries workspace content. Zero-config local use is unchanged, proven on a
real socket bound to 0.0.0.0 with a trusted proxy declared:
loopback mutation 200
loopback + X-Forwarded-For remote 403
loopback sensitive read 200
loopback health probe 200
remote LAN address sensitive read 403
loki proof phases and loki proof releases were unreachable through
bin/loki. Both shipped in 9.12.0 and worked when invoked through
autonomy/loki, but the entry point npm users actually get returned
Unknown subcommand. The Bun runtime kept its own loki proof subcommand
list and rejected anything not in it, so every bash-side subcommand added
after that file was written was invisible to the shipped CLI.
The TypeScript dispatch now delegates unknown subcommands to the bash CLI instead of rejecting them, so bash stays the single source of truth for what the surface is. A genuinely unknown subcommand still exits non-zero.
Found by verifying the PUBLISHED package rather than the repository. The
parity suite had 14 passing assertions at the time, all of which invoked
autonomy/loki directly – they tested the implementation, not the surface
a user touches. Three assertions now run through bin/loki.
The dashboard reported things it had not measured. Four fixes, each verified by mutation testing:
$0.00. null + null is 0 in JavaScript, so a run
nobody measured summed to a confident zero while individual phases beside it
correctly read “unknown”. Now unknown; a genuinely measured zero still reads
$0.00.freshness_s was consumed by 0 of 43 components,
so a 40-minute-old poll looked identical to a fresh one. Age is now shown,
preferring the server’s measurement of the FILE over client receive time.Math.random(), rendered as history. It now reads
real phase_change events; boundaries that were never emitted read UNKNOWN
rather than being back-computed from process uptime./api/operator/*, all carrying the read scope:
run detail, gate results, receipts, releases, and phase history. Four readers
existed before this and were reachable by no user.loki proof phases [--json] and loki proof releases [--json] read the SAME
modules the API serves, so the two surfaces cannot drift about the same run.
An empty result exits 3 (nothing to check) and states why.--help output, the other operator surfaces were
already covered: receipts by proof list/show/verify, gate results by
loki verify, run state by loki status --json.proof phases worked but was
absent from --help (a command nobody can discover), and proof releases
failed with “No module named dashboard” from any other directory WHILE
EXITING 0 – a failure reported as success. Both are now asserted by the
parity suite, which checks discoverability and cwd-independence rather than
only behaviour.GET /api/operator/receipts?workspace= forwarded a caller-supplied path into
an unbounded filesystem walk. Measured: walking / visits over 200,000
entries in 6 seconds. Now confined to configured roots, compared on realpath
so .. and symlink escapes cannot pass, and bounded with honest PARTIAL
semantics when a limit stops the walk.app.routes, an implementation detail
FastAPI changed in 0.141 (an included router is now stored as one lazy
wrapper). They assert reachability by routing a request instead, which is
version-independent.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.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 happenedA 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.
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.
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.
npm i -g @anthropic-ai/cline returns 404. The
real package is cline.opencode missing from the provider list in 9 places, including
INSTALLATION.md:846, which was directly contradicted by
completions/loki.bash:24.LOKI_RBAC_*, LOKI_TLS_ENABLED,
LOKI_AUDIT_*, loki enterprise rbac check, loki audit verify. RBAC is
real, but the variable is LOKI_OIDC_DEFAULT_ROLE; HTTPS turns on when BOTH
LOKI_TLS_CERT and LOKI_TLS_KEY are set.docs/DEMOS.md listed 5 files that do not exist. 11 claimed, 6 real.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.
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.
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.
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.
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.
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.
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.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.
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.
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.
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.
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.
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.
_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 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.
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.
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.
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.
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.
$ 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
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).
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.
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.
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.
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.
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.
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.
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.
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.
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.
A JSON true in output_tokens DOES fabricate a 1-token measurement and exits
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.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”.
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.
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”.
--json produced non-JSON on the path a consumer most needs to readTwelve 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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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”.
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.
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.
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 statesproof-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.
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.
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.
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.
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.
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.
$ 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.
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.
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.
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.
_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.
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”.
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.
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.
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.
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.
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
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.
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.
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.
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.
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.
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.
timeout(1) is homebrew-only on macOS; hardcoding it exited 127 on stock
bash and made the scrubbed-PATH test unrunnable. Now degrades to uncapped.loki resume could not be used to detect resumable state: cmd_resume does
rm -f "$LOKI_DIR/PAUSE" and emits telemetry. loki next --dry-run was
verified to write nothing.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.
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.
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.
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 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.
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.
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
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.
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.
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.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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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 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.
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.
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.
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.
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.
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.
$ 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
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.
--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.
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.
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.
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.
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.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.
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.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.
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.
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.
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.
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.
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.
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.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).
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”.
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.
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.
loki why gave the same advice for a 401 and a timeoutcmd_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 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.
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.
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:
files[], so mutation-integrity
failed closed on EVERY iteration for EVERY npm usertests/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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
--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.
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.
cmd_start. The --provider parse sites in cmd_quick keep their
own narrower checks; this is not a global fix.validate_provider, so
--provider <any-binary-on-PATH> still passes both. Pre-existing, unchanged.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.
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.
loki doctor on the Bun route now reports missing detectors as a BLOCKING
failure with a reinstall instruction, rather than staying silent.
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)
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.
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.
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/.
.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.
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.
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).
“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.
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 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.
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.
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.
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.
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
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.
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.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.
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.
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.
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.
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.
|| true): this is hot-path guidance; a failure here must
never abort an iteration that was otherwise fineTrust-core detector: 86 invariants.
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.
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.
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.
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.
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:
status: "stopped" with
active_sessions: 1. The live session PID was found later in the same
handler; running had been frozen ~120 lines earlier and never reconsidered.running while REST returned stopped.Now one shared _registry_run_alive() called by both, so they cannot diverge
again.
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.
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.
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.
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.
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.
Internal coherence – the receipt must not contradict itself:
available=true with every token count and usd zero -> claims a measurement
it does not haveavailable=false carrying non-zero values -> claims unmeasured, shows numbersusd non-zero with no tokens at all -> a cost from nowhere (the inflation
direction; under-reporting is not the only lie)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.
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.
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.
$0.02, with cache ratio)$0.00 and that
is fine because it was measured. This case exists so a future “just hide
$0.00 everywhere” fix cannot silently discard real sub-cent measurements.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.
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.
A guard so strict it suppressed genuine data would blank the cost block permanently, which is its own dishonesty. Three cases pinned:
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.
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.
--jsoncodex 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.
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.
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.
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.
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.
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.
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.
LOKI_MAX_ITERATIONS always wins – anyone who tuned this
sees no change, in either directionHitting 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 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
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.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.
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.
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 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.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.
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.
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.
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.
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.
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.
“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.
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.
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.
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.
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.
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.
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.
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.
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.
[ ! -t 1 ]: the hint deliberately stays silent when stdout is not a
terminal, so piping to grep suppressed it. Every early “it does not work”
reading was the harness, not the feature. Fixed with script(1).| grep -qi: -q exits on first match and closes the pipe, which races
with output delivery under a pty – the assertion failed intermittently while
the identical command passed by hand. Now captures first, matches second.Trust-core detector: 62 invariants.
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.
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.
CLAUDE.md names dist freshness the sharpest reason the fast tier exists:
CI never validates that the committed
loki-ts/dist/loki.jsmatches 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.
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.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 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.
npm pack listing has fewer than 50 entries, the
test fails loudly rather than passing on assertions that prove nothinglearning/ from
files[] – unrelated to any detector – now turns the test red, where the
original assertions would not have noticedTrust-core detector: 60 invariants.
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.
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[].
-- BLOCK
(fail-closed). A mutation that changed both the message and return 1 ->
return 0 slipped through, because the assertion read the part of the line
carrying no authority. Now asserted on the return value._LOKI_MOCK_INTEGRITY_REASON="detector_missing". Both directions are correct
for their own gate, so the invariant is now per-gate: a missing detector
either blocks, or records why it did not run – never silence.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.
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.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.
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.
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.
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.
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.
0sThe 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.
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.
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:
pass (this would approve every failing build)elif so the skip is a no-opgate_failures condition, which would skip review on GREEN
builds – losing it entirely rather than deferring itThe 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.
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.
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.
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.
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.
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.
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.
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.
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.
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:
models[]cli_aliases entry and its tier resolve to different ids – the tier a user
picks and the model actually dispatched would silently differupdated is not a valid ISO date (which degrades doctor’s age check into a
vague “unreadable” warning)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.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.
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.
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:
high would get the literal
string "high" passed through instead of a resolved modelBoth 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.
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.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:
lookup_license (scripts/license-audit.sh) – the sharpest of the set.
Hardcoding the caller to "MIT" left every assertion green. The result feeds
is_permissive, which builds the offenders list, so a GPL or AGPL dependency
would pass the license audit silently. A compliance hole behind a green
test._loki_state_file – emptying the baseline reader left all assertions
green; state would silently stop being read or written._loki_iteration_spec_summary – emptying the caller left all assertions
green; the card a user reads each iteration would lose its spec summary while
the tests still described one.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.
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.
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:
enforce_build_check – replacing its call site with if true; left all
28 of its own assertions green. A failed production build would silently stop
blocking completion on the hosted simple-web route._loki_test_provenance – emptying the council’s call left all 8
assertions green. The council would stop checking test provenance entirely._loki_claude_login_state – hardcoding the caller to loggedin left all
8 assertions green. A never-logged-in user would stop getting the fail-fast
with a one-step fix, and would instead enter the build, make a failing call,
and 401 – the worst possible first impression.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.
It was broken in three independent ways, any one of which alone was fatal:
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.run_debate RETURNS a dict and prints nothing. The CLI discarded the
return value, so even a successful debate produced no output."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.
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.
“Could not run” splits in two, and the halves need opposite handling:
TypeError, ImportError, bad arguments): blocks loudly. Nobody’s
build is being judged and nobody is being told – the exact class that hid
here for so long.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.
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:
.loki is unwritable.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.
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.
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.
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.
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 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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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 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.
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.
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.
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.
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.
/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.
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.
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.
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.
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.
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.
“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=
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.
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 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.
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.
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.
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.
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 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.
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.
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.
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.
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 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 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.
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.
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.
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.
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.
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.
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.
The lineage guard proves a spawned runner is the child we launched. On Linux it
reads /proc/
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.
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.
--model flag.
providers/claude.sh called loki_tier_route_model "$tier" with one argument
to a two-argument function; it returned empty at rc=0, so the || fallback
never fired. Verified live in v8.6.0/v8.6.1. The benchmark data is
unaffected – autonomy/run.sh never uses the argv seam, so trials went
through provider_invoke with the model correctly pinned.LOKI_MODEL_DEVELOPMENT sat ahead of the catalog in aider, cline and
opencode, so setting a development model for one provider changed the others.
Provider-scoped variables now win. claude.sh deliberately keeps its global
chain (it genuinely reads LOKI_MODEL_PLANNING for max-tier pinning) and a
test locks that asymmetry so the fix cannot be over-applied.claude-opus-4-8, while
model_catalog.json declares aider, cline and opencode with open-weights
defaults. It asserted that our provider-agnostic CLIs default to Claude –
encoding the bug rather than catching it. Now derives from the catalog.PROVIDER_CONTEXT_WINDOW. That value
was declared by all four providers and read by nothing but a display line,
while the caps were hardcoded at 400000/425000 bytes. A 12b-14b model with a
32k window would have received prompts sized for 200k and failed in a way
that looks like the model being bad. Measured: unset window preserves
400000/425000 byte-for-byte; a 32k window derives 67764/72000. The clamp only
ever lowers the cap.provider_invoke_argv for aider and cline (previously in three of five
providers).run.sh and autonomy/loki.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.
loki proof md was unreachable on the default route. It shipped in
v8.6.0 on the bash route only; the Bun route – what bin/loki runs by
default – answered “Unknown subcommand”. The feature was invisible to every
user who had not set LOKI_LEGACY_BASH=1. The test passed because it drove
autonomy/loki directly, while a real user types loki proof, which is the
shim. Both routes now emit byte-identical output.run_code_review assigns its failure discriminator after the size guard
returns, so a file-size condition was reported as “Critical/High findings”.
Now classified as infrastructure, still fail-closed.hard-2-ledger, authored so a one-shot draft fails it: haiku without the
harness passed 1 of 4; with the harness on, it passed – at lower cost per
correct result. Same model, same prompt.hard-2-ledger fixes that, verified in
both directions before adoption.docs/STRATEGY-2026-2028.md
sets the floor – not the peak – as the two-year metric.docs/STRATEGY-2026-2028.md, docs/adoption-baseline-2026-07-31.md,
benchmarks/EQUIVALENCE-STATE-2026-07-31.md.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.
provenance.yml claimed it
signed image digests. It triggers on release: published, our releases are
created with the default GITHUB_TOKEN, and GitHub suppresses trigger events
for that token – so it fired twice ever, both manual, in April. The registry
confirmed zero sha256-*.sig tags. Signing now runs inside the job that
pushes the image, where no trigger can skip it. A workflow that never fires
leaves an EMPTY run list, never a red one.budget_exceeded
exited 0 on the reasoning that a human would resume – true at a terminal,
false inside a k8s Job, where the pipeline went green over incomplete work.
Now exit 20 (deterministic: the same cap exhausts the same way).doctor --json reported ok: true on a host that cannot run a build,
contradicting the same command’s exit code. Each provider CLI is individually
optional, so none can be marked required; the aggregate “none at all is a
blocker” check existed only on the text path.loki tour (no provider, no key, no spend) while the
other said “some required prerequisites are missing” and stopped.helm uninstall deleted the audit-log PVC; worker.mode=Deployment
installed clean and rendered no worker; NetworkPolicy declared
policyTypes: [Ingress, Egress] with egress: - {}, restricting nothing;
k8s SIGKILLed builds 30s into a rolling upgrade.LOKI_MAX_DURATION / --max-duration (accepts 90m, 2h): spend and
iterations were capped, but a STALLED run was bounded by neither.loki verify --json on stdout, and --json as an alias on loki ci.--quiet / LOKI_LOG_LEVEL, with errors as a hard floor – a verbosity
control that can hide why a build failed is a footgun.first_run_blocked telemetry: names the CLASS of dependency that stops a
first run. One enum-clamped field, once per install, strict opt-in, every
existing opt-out still wins, documented in docs/PRIVACY.md.values.schema.json, a helm test hook, and scripts/enterprise-smoke.sh.docs/exit-codes.md and docs/environment-variables.md, both linked from
loki --help and both guarded by tests asserting doc-matches-source.loki help – including
loki proof, the Evidence Receipt, the command the trust argument rests on.
A complete index now backs the curated sections, gated so a new command
cannot ship invisible.loki start --help points at loki plan for a no-spend preview.loki verify 1-vs-2 exit-code contradiction in favour of the
implementation; severity now rises with the code everywhere.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.
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.
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 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.
Minor release. Know what you paid for, and what the wait was.
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.
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.
Minor release. Time to first preview is no longer invisible.
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.
.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.
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.
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.
Patch. loki quickstart was returning nothing on every Mac.
_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.
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.
Patch. The gate stops lying about why it failed.
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.
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.
Minor release. The first minute, the shop window, and the enterprise promise.
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.
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 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.
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.
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.
Minor release. Run any model, and find the feature that was already here.
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 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.
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.
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.
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.
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.
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.
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.
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:
flock -x 9 with no timeout (events/emit.sh:48). A holder killed
mid-write – a reaped CI run, a Ctrl-C’d build – blocked every later emit
forever. Because emit.sh runs per invocation, those hangs accumulated.flock(1), checked staleness only after exhausting all
500 attempts, forking a perl sleep helper on each one. Measured cost of a
single append against an already-stale lock: 10 seconds and ~500 forked
processes, returning failure.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.
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.
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.
Three defects that together made the “run any model” story unusable:
model_catalog.json pointed the non-Claude providers at Claude. Both
aider and cline defaulted all three tiers to claude-opus-4-8 /
claude-sonnet-5, and the catalog contained zero open-weight models. A
user who switched provider specifically to stop paying frontier prices kept
paying them, silently, unless they knew to set LOKI_MODEL_OVERRIDE. Both
now default to openrouter/deepseek/deepseek-v3.2, and the catalog carries
DeepSeek, GLM, MiniMax, and local Ollama entries. The claude provider is
unchanged, and the env-override chain still wins.ANTHROPIC_BASE_URL at OpenRouter. OpenRouter
serves only the OpenAI-shaped /v1/chat/completions – it has no
Anthropic /v1/messages endpoint – so that routes an Anthropic-shaped
client at an API it does not speak. The CLI example even named
anthropic/claude-sonnet-4.5, using OpenRouter to reach Claude again.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.txt reported Failed: 0 while COMPLETION.txt reported failed=1
from the same queue/failed.json. The cause was age, not source: the
status monitor refreshes every 2s but is not running once the engine blocks
in a pause. All three blocking pause sites now refresh before writing the
durable record.loki why --json emitted a stale completion record unlabeled, so a
script reading completion.branch or completion.pr_url after a crashed run
would attribute a previous run’s branch and PR to the current one. It now
emits completion_is_stale and head_sha explicitly.tests/test-emit-lock-no-hang.sh (8 assertions, registered). Non-vacuity
proven by reverting the fix: 3 go red, including the exact 10s regression.tests/test-status-surface-agrees.sh (10 assertions, registered). The
pause-site check is structural, so a future site added without the refresh is
caught. Non-vacuity proven three ways.Patch release. The two DURABLE status surfaces disagreed, and --json hid the
one field that would have let a script notice.
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.
STATUS.txt
every 2 seconds, but it is no longer running by the time the engine blocks in
a pause. STATUS.txt was frozen at its last tick while COMPLETION.txt was
written fresh from the same queue.STATUS.txt immediately
before writing the durable record, so the two snapshot the same instant. All
three sites (PAUSE file, budget-exceeded, checkpoint pause) are covered.(from previous completed run); the JSON consumer received the identical
record with no such marker, so a script reading completion.branch or
completion.pr_url after a crashed run would attribute a PREVIOUS run’s
branch and PR to the current one – silently, with no field to check.--json now emits completion_is_stale and head_sha as explicit
fields, using the same determination as the human path.tests/test-status-surface-agrees.sh (7 assertions, registered). The pause
site check is structural, so a fourth pause site added later without the
refresh is caught rather than silently skewing again. Non-vacuity proven by
reverting both fixes: 5 of 7 go red, and green again on restore.Patch release. Two live status surfaces contradicted each other about whether a build was running.
Measured on a preserved real run that was genuinely paused on a gate:
The WebSocket stream reported “running” while /api/status reported
“paused” – for the same run. The stream derived status from
dashboard-state.json’s mode field, which the engine writes and which goes
stale the moment a run pauses; on that run it still read autonomous.
/api/status reads .loki/PAUSE and was correct.
Two live surfaces disagreeing about whether a build is running is worse than
either being wrong alone, because the user cannot tell which to believe. The
WebSocket now consults the control files (.loki/STOP, .loki/PAUSE) FIRST
and falls back to the engine-written mode only when neither exists.
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.
facts.execution (reason / outcome / run_status), but the
template rendered it zero times – so a receipt for a run that stopped on a
gate displayed a verdict with no cause. The honesty banner now shows
Stopped: <reason> beneath the headline when a run did not complete, and
stays silent for a clean run. Display-only: the value is whatever the
generator recorded, never recomputed in the template.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 whyMeasured against a preserved real run that produced 9 files and 1300 insertions and paused on a gate:
loki why printed that as fact.
A recorded zero is now re-derived from git and only reported as zero when git
agrees; a trustworthy non-zero record is printed unchanged.code_review after
3 failures at threshold 3, and the advice was “Resume with: loki resume” –
the exact thing the run’s own pause notice warns will stop at the same gate
again. It now names the gate, its count and threshold, and says to read the
findings first.inconclusive_spec_contradiction – a real terminal state – produced
“No diagnosis mapping for this status”. It now explains the stop and points
at .loki/assumptions/ledger.md.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.
Patch release. Every change is a bug fix; no behavior a working v8.0.0 install depends on was altered.
_git_diffstat
computed the receipt’s stat against _LOKI_ITER_START_SHA – the
per-ITERATION baseline, which is re-captured to current HEAD at the top of
every iteration – while presenting the result as run-level. A multi-iteration
run therefore attested only to changes since its last iteration, and that
count feeds diff_sha256, the integrity hash written on every run. Measured
on a real 2-commit run: 850 insertions + 76 deletions reported where the run
produced 1479 across 10 files. Now resolves the run-level baseline, falls back
to the empty tree for greenfield runs, and records base_sha from the same
baseline the diff used so a verifier recomputing it cannot disagree._rev_resolvable probed only <ref>^{commit}; the empty tree is a TREE, so
genuine proofs failed with “base ref unresolvable”. Fixed without loosening
the forgery defense – fabricated, all-zero, and garbage SHAs are all still
rejected.Every gate decision examined during this work was a TRUE positive. The defects were in how those correct decisions were reported.
files_changed: 0 for work it had done. A build
that produced 4 spec-required files and passed 28/28 of its own tests told the
user nothing was changed. Now diffs against the empty tree when no baseline
commit exists: the same artifact reports 9 files, 1300 insertions.PAUSED.md never said why it paused. It was static boilerplate. It now
names the blocking gate, its failure count and threshold, the findings path,
and what was built before the pause.complete|max_iterations AND files_changed == 0, so intervention,
failed, stopped, force_stopped and inconclusive_spec_contradiction
got nothing – exactly the outcomes where a user is stuck. Each now names a
concrete next action, and the guidance is independent of the file count.loki doctor hid the inline-image protocol when bun was absent. The
Cockpit section printed “Render path” but not “Inline-image protocol”, so a
user could not tell whether their terminal was unsupported or simply never
probed.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:
tar tzf | grep -q under pipefail reported a missing tarball artifact that
was present – grep -q closes the pipe and GNU tar dies of SIGPIPE.stat -f means “filesystem status” and exits 0, so a macOS-first
stat -f ... || stat -c ... never fell through on Linux (5 suites).$( ) makes macOS bash 3.2
reject the whole file, breaking both macOS Bun jobs./Users/<name>/git/loki-mode checkout, and
two pinned /opt/homebrew/bin/python3.12.requirements-test.txt.import mcp resolved to this repo’s own mcp/ directory rather than the SDK,
so a preflight guard passed with no SDK installed at all.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.
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.
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.
src/auth/token.ts – a routine file in any auth-enabled
app, exactly what this engine is asked to build – failed the gate with
nothing secret in it. .env.example shipping a placeholder key failed too.
The council now judges CONTENT; template and fixture paths are skipped. Real
keys in real paths still block (verified against live-shaped keys, an AWS id,
and a PEM block). The filename heuristic is retained on the auto-commit path,
where refusing to STAGE a .env is correct.inconclusive, so the loop kept working on a finished job and the user paid
for iterations they did not need. Failing tests still block, unchanged.loki init --json emitted invalid JSON on reinit. The banner went to
stdout, so the second init in a directory produced prose followed by JSON.
Tools parsing it worked once, then broke. Diagnostics now go to stderr.loki doctor
demanded a separate CLI install that Loki already ships internally.claude-binary
probe, so it silently vanished on exactly the route v8 promotes.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.
Four measured-harness disciplines, adopted onto the existing trust core rather than bolted beside it. None of them can weaken a gate.
<loki_system> prefix and a volatile
<dynamic_context> tail at an explicit [CACHE_BREAKPOINT], which
sdk_invoker.ts uses to apply cache_control. Every new instruction added in
this release was placed in the prefix to preserve that property.COMPLETION_PROMISE with no measurable
target (no number, comparator, named metric, or verifiable artifact) gets an
advisory asking for a checkable success condition. Advisory only – it never
blocks a build or rewrites the goal. Suppressed for an absent goal and in
perpetual mode, where open-endedness is the chosen configuration.
Byte-mirrored across the bash and TypeScript routes.capability_degraded record onto the existing
.loki/events.jsonl stream instead of existing only as prose in the captured
output, so an unattended operator can distinguish “the SDK could not load”
from “the model did poor work”. No new env var.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):
off (default) writes nothing – every site keeps its own :-0 default.judges turns on the seven one-shot JUDGE/TEXT sites; the RARV loop stays off.
The recommended scale tier.full turns on all eight, including the loop (LOKI_SDK_LOOP). Pre-flight
tier; the loop default-flip remains a later release.LOKI_SDK=1 is an alias for judges; LOKI_SDK=full for full.
An explicit per-site flag ALWAYS wins over the mode (even an explicit =0 under
full) – the resolver only writes a var that is unset. The resolver runs in all
entrypoints (bin/loki, autonomy/loki, run.sh, and the Bun cli.ts) so
LOKI_SDK_MODE=full loki start correctly routes the loop to the Bun runner. bash
and TS bind to one shared parity fixture (loki-ts/test/fixtures/
sdk-mode-table.json) so the two resolvers cannot drift. Opt-in
LOKI_SDK_MODE_DEBUG=1 prints what one switch turned on.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.
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):
LOKI_SDK_DONE_RECOG=1), council-v2 (LOKI_SDK_COUNCIL_V2=1),
code-review reviewer (LOKI_SDK_CODE_REVIEW=1), completion-council member +
contrarian VOTE (LOKI_SDK_COUNCIL_VOTE=1), voter-agents council
(LOKI_SDK_VOTER_AGENTS=1) - all schema-constrained via the shared
internal sdk-judge bridge.LOKI_SDK_GRILL=1, LOKI_SDK_PRD_ENRICH=1) - free-form
text via the internal sdk-text bridge.claude-binary guard (binary-free deploy), keeps
the downstream parser/verdict shape byte-compatible, and is mirrored on both
the bash and Bun/TypeScript routes.RARV main loop on the Agent SDK (LOKI_SDK_LOOP=1):
loki start on the Bun route drives the agentic loop with query() instead of
spawning claude ... --output-format stream-json. A new stream parser
(sdk_stream_parser) consumes the typed SDK message stream and writes the SAME
.loki state the bash Python parser wrote (agents.json, queue/in-progress.json,
events.jsonl hook records, per-iteration result-cost) so the dashboard,
efficiency writer, event bus, and council are unchanged. bin/loki routes
start to Bun only when the flag is truthy; the bash claude main loop stays
the default.Packaging / distribution:
@anthropic-ai/sdk is statically bundled into loki-ts/dist/loki.js,
so the npm tarball and Docker image ship the judge SDK route self-contained.optionalDependency so npm install resolves it, and the Docker image
installs it into loki-ts/, making the LOKI_SDK_LOOP path work in both
shipped channels.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.
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.
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.
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):
.gitignore would ignore
(git ls-files | git check-ignore --no-index --stdin), reduces them to
top-level prefixes, and excludes each from the review diff – even though they
are tracked. Byte-identical diff when nothing is tracked-but-ignored. Capped by
LOKI_REVIEW_GITIGNORE_MAX_EXCLUDES (default 200); opt out
LOKI_REVIEW_GITIGNORE_FILTER=0.LOKI_REVIEW_MAX_DIFF_BYTES (default 400000), the gate emits an explicit warning
naming the size, limit, biggest contributing dirs, and remedy, plus a
code_review_diff_oversized event. The all-NO_OUTPUT INCONCLUSIVE block now
references the oversize cause + fix, so a block is self-explanatory.sizes.tsv, so a post-mortem is a one-line
read instead of repo archaeology.Immediate unblock on older versions: git rm -r --cached <stale-tracked-dir>.
Follow-up fixes found while validating v7.129.1 end to end.
--detach: the detached inner script inherits only a curated
LOKI_* env, so CLAUDECODE was invisible to it. It is now forwarded as
LOKI_NESTED_AGENT, so the sub-streams stay off when nested even after the fork.loki docs generate
runs after completion and before the detached --pr push/PR step and spawned a
provider call with no timeout; a hang there silently blocked the PR from ever
being created (verified work committed locally but never pushed). It is now
wrapped in a timeout (LOKI_DOCS_TIMEOUT, default 300s); docs are non-gating,
so on timeout the run warns and proceeds to push the PR.Addresses a client report of running several issues concurrently from one checkout.
check_merge_queue re-logged unconsumed
MERGE_REQUESTED signals on every orchestrator pass when auto-merge was off
(--pr without --ship), producing a long “Merge requested: testing/docs”
loop that looked stuck while real work completed. It now logs once per feature
and preserves the signal (the PR is left for a human to merge).testing/docs worktree sub-streams each
spawn a further nested claude session; when Loki runs inside another agent
session (Claude Code sets CLAUDECODE, or the Loki Mode skill) those collide
on the shared checkout. They now default OFF when nested (opt back in with
LOKI_PARALLEL_TESTING/LOKI_PARALLEL_DOCS=true). Non-nested behavior unchanged.loki start in one
checkout with a shared .loki/ collided on non-namespaced worktrees/branches
(issue-mode namespaces pids per session, so the old lock never tripped for
different issues). loki start now fails fast with the safe per-issue command
when a live sibling shares the default .loki/; runs that isolate their own
LOKI_DIR are exempt. Escape hatch: LOKI_ALLOW_SHARED_CHECKOUT=1.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.
--json-schema output across the review + completion trust pathsAdopted 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.
autonomy/run.sh _dispatch_reviewer, TS
loki-ts/src/runner/quality_gates.ts claudeReviewer): both routes emit
--json-schema and re-materialize VERDICT: X\nFINDINGS: .... New
loki-ts/data/code-review-schema.json, autonomy/lib/cr-rematerialize.py.autonomy/council-v2.sh): structured verdict with the
sed-carve retained as the fail-closed fallback. New
loki-ts/data/council-v2-schema.json.autonomy/lib/done-recognition.sh): structured verdict so
the requirements array is always valid JSON. New
loki-ts/data/done-recognition-schema.json.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).
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.
node --test detection on Node 20+/26Node 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.
timeout is absent, and unittest zero-discoveryThe 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.
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.
Three fixes that make loki cockpit genuinely useful instead of a tiny,
noisy-looking frame:
width=<cols> / Kitty c=<cols> from the terminal
width so the cockpit fills the pane.loki start left its registry
entry frozen at status “running”/”building” with a dead pid, so the fleet
accumulated 100+ zombie “BUILDING” entries. get_fleet_runs now reports a
dead-pid in-flight entry as “stale”, and the new registry.reconcile_stale_runs()
(run opportunistically by the cockpit) marks them “stopped”. The fleet header
now reads “FLEET (N active / M total)” (or “M total, none active”) so a long
tail of finished runs is not alarming, and the list is ordered live-first.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.
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.
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.
Five enhancements to the v7.126.0 cockpit, all dependency-free and honest:
loki cockpit --follow: enters the alternate
screen buffer with cursor save/restore and INT/TERM/EXIT cleanup traps, and
redraws only when the focused run’s state files change (mtime-driven) instead
of blind interval polling.--follow: Tab / arrows cycle repo focus across
the multi-repo fleet, e prints verify –explain for the focused run, s shows a
steering hint, q / Ctrl-C quits and restores the terminal. Skipped on non-TTY.loki cockpit.loki doctor cockpit report: graceful degradation for
no-bun / no-resvg / tiny terminal / huge fleet (fleet capped with a “+N more”
line), and a new doctor section reporting the detected inline-image protocol,
truecolor, bun, @resvg, and exactly which render path loki cockpit will use.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.
loki cockpit, dashboard identityA complete observability layer, built with zero new user-facing tooling (no chafa, no headless Chrome, nothing to install):
loki start now DETACHES to the background by default on an interactive TTY,
after a stare-worthy handoff: a crafted truecolor Autonomi banner (the real
purple squircle + white “A” + teal dot logo, “LOKI” wordmark) and a rich card
(spec, path, tier, provider, budget + breaker, log, dashboard URL) with a
choice - Both (default) / Dashboard / Watch (cockpit) / Detach / Stop - and an
8s auto-Both. The choice is remembered per project. CI / –bg / –yes /
non-interactive shells bypass it entirely and keep the exact prior foreground
behavior. Both opens the dashboard AND points at the cockpit.loki cockpit (new command; loki watch is unchanged - it remains the PRD
auto-rerun watcher) renders live multi-repo status as an inline terminal image
on graphics-capable terminals (Kitty / iTerm2 / WezTerm / Ghostty), via a
dependency-free pipeline: state -> self-contained SVG -> PNG (optional
@resvg/resvg-js) -> pure-base64 Kitty/iTerm2 protocol, with capability
detection. On any other terminal it falls back HONESTLY to a text summary plus
the browser dashboard - it never claims an image rendered when it did not. The
multi-repo fleet is read from the existing registry (~/.loki/dashboard/
projects.json); per-run state from .loki/autonomy-state.json, .loki/verify/
evidence.json, and .loki/council/*.json.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.
--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):
--isolation none (default) – in-place, exactly today’s behavior.--isolation worktree – git-worktree isolation (same mechanism as --parallel).--isolation docker – Docker sandbox (same mechanism as --sandbox).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.
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.
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).
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).
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.
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.
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.
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
claude, codex, cline, aider everywhere, matching
providers/*.sh and the existing tests. No trust-core / build_prompt change.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.
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).
enforce_static_analysis
(autonomy/run.sh) routed .jsx files into a per-file node --check, which
cannot parse JSX (ERR_UNKNOWN_FILE_EXTENSION) and flagged EVERY React
component as a “Syntax error” (observed: 11 false findings on one landing
build). .jsx now routes to the JSX-capable tsc/bun path like .tsx, and is
included in tsconfig project-mode detection.package.json
declares a lint script (oxlint / biome / eslint – increasingly the default
scaffold, which the eslint-config probe did not recognize), the gate runs
npm run lint. This is additive: the type/syntax check still runs, so a TS
app’s type errors are never silenced by a passing lint. A missing linter
(rc 127) is an honest skip, never a false finding.proof-generator.py:_collect_quality_gates
read "passed"/"status", but the marker writes "pass" (bool), so a real
failing result collapsed to not_run (understating a failure as “did not
run”). The collector now reads "pass" too; an absent outcome key still
stays not_run (never fabricated passed).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.
tests_pass requires POSITIVE “N passed” proof to read green;
rc==0 alone is no longer a pass. The v7.121.0 runner-agnostic change passed a
check on rc==0, but a no-op scripts.test (echo done, exit 0, true, :)
exits 0 having run ZERO tests -> a required verification went green with nothing
tested (a FAKE-GREEN, and the v7.121.0 change had introduced it: the old hardcoded
npx jest printed “No tests found” -> blocked). Fix: True requires a runner’s
“N passed” signal (jest/vitest N passed, pytest N passed, mocha N passing,
node:test # pass N, tap pass N) with the count [1-9]\d* (>=1, so a zero-count
line like mocha’s “0 passing” over an empty suite does NOT count); rc==0 without
that proof -> INCONCLUSIVE (None -> pending), never green and never a fake-RED
False; rc!=0 real failure -> honest False; no-test signals -> False. Found by the
release council (two rounds: no-op scripts, then the zero-count regex hole), closed
empirically against real jest/vitest/pytest/mocha runs; council APPROVE, 1648 tests
passed. npm 7.121.0 was immutable once published, so this ships as 7.121.1.grep_codebase now speaks ERE (grep -E), so
LLM-emitted patterns stop erroring and blocking correct builds. The checklist
verifier ran grep in its BRE default. LLM-emitted patterns are ERE/PCRE-flavored
(app\.get\('/api/tasks', router.get\('/tasks'|router.get\("/tasks",
base62|BASE62), where an escaped \( is a BRE group-open and | is a literal:
grep ERRORED (rc>=2, e.g. macOS ugrep/BSD grep “parentheses not balanced”) and the
code collapsed BOTH not-found (rc=1) AND error (rc>=2) to passed=False. Result:
genuinely-present, tested, curl-verified endpoints read checklist failing -> the
completion-council hard gate critical_checklist_failures blocked a CORRECT build
for iterations until it TIMED OUT. Observed live on the web-app-e2e build
(cdbbb5af): 3 endpoints present at src/app.js:38/42/59, gate blocked at iteration 9.
A FAKE-RED (validated work reads failing) – the founder’s “no progress” class.-E (ERE) so \( is a literal paren and . * +
| keep regex meaning; rc==0 -> True, rc==1 -> honest False (a genuinely-absent
endpoint still blocks – moat intact). If a pattern still errors even under -E, a
fixed-string (grep -F) retry recovers a real match to True; anything else
(literal-absent or double-error) is INCONCLUSIVE (None -> checklist pending,
never a hard False), because once -E fails to parse, an escape-stripped literal
cannot distinguish “present via a quantifier” from “absent”. No fake-green: None
is pending, never counted verified.Found in 0 file(s)/failing ->
Found in 1 file(s); replaying the exact council gate logic flips BLOCK (3 critical
failing) -> PASS (0 failing). The build that timed out clears the gate. Full suite
1642 passed; council APPROVE (chain-to-green verified, no fake-green/fake-RED, ERE
dialect audit clean).tests_pass is runner-agnostic – runs the project’s OWN declared
test command instead of hardcoding npx jest. The verifier ran npx jest for any
package.json project. cdbbb5af declares "test":"vitest run" (vitest installed, jest
NOT): npx jest either errored on jest’s drifted --testPathPattern flag or tried to
FETCH jest and timed out -> a genuinely-passing 26/26 vitest suite read False/None
(“tests not run”) -> the item never reached verified. Fix: read package.json
scripts.test and run it via npm test (the runner the build actually chose –
vitest/jest/mocha/node:test/ava), same list-form + shell=False + cwd posture (the
engine already ran this exact command to build the workspace). Model-/complexity-
agnostic by construction. Anti-fake-green gate preserved and widened: a required
tests_pass still FAILS on a no-test run (jest “No tests found”, vitest “No test files
found”, pytest exit 5) so a suite that runs zero tests never reads green; couldn’t-run
(timeout/not-found/no-scripts.test) -> inconclusive None, ran-and-failed -> False,
ran-and-passed-with-tests -> True (same None-vs-False moat as grep).file_exists/file_contains accept the LLM-emitted path under path,
target, OR file. The checklist is LLM-authored (generated_from a PRD), so the
field name is not pinnable. A valid file_exists carrying target:"package.json" (no
path) raised “Invalid path characters: ‘’” -> None -> the item stuck pending. Alias
the variants so a real check on a present file verifies. (This is the REAL instance of
the target-vs-path class; the earlier #141 self-report of it was a hallucination against
a checklist that used path – verified here against the actual on-disk target schema.)_compute_headline) requires the Evidence Receipt axes (tests+build+security captured,
no degraded), which remain the separate Evidence Receipt phases 2/3/4 work. The 7
still-pending items are prose-only tests_pass (no runner signal) and a manual_curl
check the verifier does not implement – both correctly stay inconclusive (None), never
faked green; implementing behavioral manual_curl (run the app, hit the endpoint) is
the next rock.test_*.py so validated work reads
verified. A Python CLI with a ROOT-LEVEL test_invoice_cli.py (no tests/ dir, no
config) had its genuinely-passing 16-test suite read as “tests not run” ->
“VERIFIED WITH GAPS” – the founder’s exact false-negative class (validated work reads
unvalidated), hitting every Python build of that shape. Fixed in BOTH mirrors (run.sh
enforce_test_coverage + verify.sh verify_gate_tests):
test_*.py/*_test.py (was config-or-tests/
-dir only).python3 -m unittest discover (stdlib, zero
deps) so a real unittest suite is VERIFIED, mirroring pytest’s exact recording path.unittest case in both zero-test guards). A failing
suite -> failed.test-python-test-detection.sh (7/7, non-vacuous), 44/44 proof-generator
unchanged, receipt-flip proven end-to-end. Known bounded edge (documented): the
fallback’s -p test_*.py does not match a *_test.py naming with pytest absent.build-results.json
had NO writer, so facts.build was permanently not_run and counted as a gap by
_compute_degraded.
run.sh enforce_build_check() – the missing writer, a sibling to
enforce_static_analysis/run_secure_scan. Detects the stack’s build command (npm/go/cargo);
if one exists it RUNS the build (verified/failed – a real gap stays a gap); if the stack
genuinely has no build step it records applicable:false (status not_applicable), an
honest N/A that is NOT a gap.not_run (honest gap) is the SAFE CATCH-ALL; not_applicable (N/A)
fires ONLY on a POSITIVE proof-of-no-build – a package.json present with NO build-ish
script and no build-tool devDep (the founder’s CLI). ANY project without that positive
signal – a foreign build manifest (Make/Maven/Gradle/.NET/autotools/meson/bazel), a
build-ish npm script, OR no package.json at all (Zig, bare C, anything unenumerated) –
stays not_run. This fixes the whole CLASS: a forgotten stack fails in the HONEST
direction (under-claim), never fake-green. Pinned by test-build-check-applicability.sh
(23/23, incl. the exact stacks 3 council rounds found). A build that FAILED stays
NOT VERIFIED (the mirror-image guard). Known bounded edge (documented, non-blocking): a
polyglot repo with a no-build package.json co-present with an unlisted foreign manifest.LOKI_BUILD_CHECK=0 opts out.not_applicable as “N/A - no build step” (neutral
tier), not the alarming “not run”._council_effective_min_iter()
resolves the floor from DETECTED_COMPLEXITY (explicit env override wins; simple->1; else 3).
Accuracy-preserving: the floor only stops FORCING more iterations after done-is-claimed – every
gate (evidence, council, provenance, boot smoke, devil’s advocate) still runs. MEASURED on a real
build through the deployed stack: 28m52s+timed_out -> 8m25s+passed (3.4x), gates+council still ran,
app 15/15 tests pass. test-council-convergence-floor.sh 24/24..loki/quality/*.pass + test-results.json (gates) and expands the nested
council/votes/round-N.json {verdict, votes:[…]} shape into per-reviewer rows. NOT a verification
change; fixes the PROOF’s reporting only. Regression tests fail-on-old/pass-on-new; proof-generator
38/38, redaction 28/28 intact.loki docs
generate burn). auto_generate_docs_if_needed and the doc-coverage gate now honor
DETECTED_COMPLEXITY: simple -> README + USAGE only; standard/complex keep the full suite.
MEASURED on a real build: docs 9 -> 3, ~450s -> 250s (~45% faster), verdict UNCHANGED
(VERIFIED WITH GAPS both, zero accuracy cost). test-doc-scope-generator.sh 6/6; parity
test still 12/12 (DOC_SCOPE prompt strings unchanged).autonomy/completion-council.sh lines 1768-1774: a python3 -c "..." invocation
had Python comment lines containing literal double-quotes (pass:"inconclusive",
status:"no_tests_run", runner=="none"). Those unescaped " prematurely closed
the shell double-quoted string, causing SC2140 + SC1078 warnings that failed the
CI ShellCheck gate. Fixed by converting the invocation to a quoted heredoc
(python3 <<'PYEOF' ... PYEOF) which makes ALL inner quotes safe. Behavior is
identical: zero-test INCONCLUSIVE routing (anti-fake-green, #82) is preserved.
tests/test-heal-assess-readiness.sh lines 100 and 240: cd "$FIX" inside
subshells lacked || exit 1, triggering SC2164. Fixed inline.
Non-regression verified: 5 test cases (pass:true/PASS, pass:false/FAIL,
pass:”inconclusive”+status:”no_tests_run”/INCONCLUSIVE, runner==”none”/PASS,
malformed-JSON/INCONCLUSIVE) all produce outputs identical to pre-fix baseline.*.test.js with no test()
calls (node –test exits 0, “# tests 0”) and jest --passWithNoTests with no
suites previously recorded pass:true / passed_count:0 – a mini fake-green.
Fixed across ALL runners in BOTH the recording paths (autonomy/run.sh in-loop
test gate + autonomy/verify.sh) AND the READING path (autonomy/completion-council.sh
evidence gate): a real runner that ran zero tests records
{pass:”inconclusive”, status:”no_tests_run”}, and the completion gate routes
that to INCONCLUSIVE (pass-through, NOT affirmative green, NOT a block) rather
than counting the string as pass. A real test file with >=1 passing test still
records pass (unchanged); a failing suite still blocks (unchanged). This is a
shipping-contract change (a repo with jest –passWithNoTests and no tests goes
from VERIFIED to inconclusive – intended, honest, source_without_runnable_tests).
Tests: tests/test-zero-test-inconclusive.sh (13/13) + Case 8 in
test-evidence-gate-no-tests.sh (29/29; a targeted revert of only the gate proves
the gate fix is load-bearing).loki heal --assess read-only modernization readiness triage. A
new read-only subcommand that scans a codebase and emits a readiness report:
LOC / language mix, technical-debt signals, maintenance-risk indicators, a
modernization maturity-level placement, and ranked candidate targets with
rationale – all from real deterministic scans (never fabricated numbers).
Proven READ-ONLY: git status stays clean and no .loki dir is created in the
target after –assess. Test: tests/test-heal-assess-readiness.sh (9/9, incl. the
read-only assertion).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.
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.
tests: not_run, so
genuinely-correct, fully-tested work got a NOT VERIFIED headline (false
negative). The test-runner detection did not recognize Node’s built-in
node --test runner (stable since Node 18, runs *.test.{js,mjs,cjs} with
zero config and NO package.json). A deliverable of slug.js + slug.test.js
(9/9 passing) fell through to runner:none / status:not_run /
verification_gap:source_without_tests, so the proof honestly derived NOT
VERIFIED – a false negative that is as corrosive to a trust-as-product engine
as fake-green. The detection lives in BOTH paths (the in-loop test gate in
autonomy/run.sh AND the loki verify gate in autonomy/verify.sh); both are
now fixed. A node --test branch was added as a FALLBACK below every explicit
path (a package.json declaring vitest/jest/mocha still selects that runner):
it fires only when node is on PATH and matching test files exist, records
the REAL result (runner=”node-test”, pass from exit code, parsed counts), and
a FAILING suite records pass:false (never swallowed). Source without test
files still records source_without_tests (no over-fire); no node still falls
through to the honest inconclusive path (never fabricates a pass). Test:
tests/test-node-test-detection.sh (9/9 incl. both-paths + failing-honesty +
jest-no-regression cases)..loki path when writing the COMPLETED / orchestrator state
markers. autonomy/loki sets LOKI_DIR="${LOKI_DIR:-.loki}", and two sites
in autonomy/run.sh defined loki_dir with the /.loki OUTSIDE the :-
default ("${LOKI_DIR:-${TARGET_DIR:-.}}/.loki"), so a set LOKI_DIR of .loki
became .loki/.loki – the build failed with
.loki/.loki/COMPLETED: No such file or directory. Both sites
(_advance_current_phase + main) are corrected to the sibling form
"${LOKI_DIR:-${TARGET_DIR:-.}/.loki}" (the /.loki inside the default), so a
set LOKI_DIR is used as-is. Behavior when LOKI_DIR is unset is unchanged. An
exhaustive repo-wide audit confirms these were the only two buggy sites. Test:
tests/test-loki-dir-double-path.sh (11/11).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.
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.
loki verify now emits a structured scope record in evidence.json:
{files_changed, net_lines, verdict, advisory:true, thresholds}. Verdict is
“ok” under soft thresholds, “warn” over them, and “block” ONLY when an operator
explicitly sets a hard cap (LOKI_SCOPE_MAX_FILES / LOKI_SCOPE_MAX_NET_LINES)
and it is exceeded. Critically, the record NEVER feeds verify_compute_verdict and
never emits a gate, so by default it CANNOT change the exit code or block a build
(zero friction regression, verified). Greenfield builds are byte-identical by
construction (the helper is reachable only via loki verify, never loki start).
Test: tests/test-verify-scope-record.sh (4/4).loki verify now writes .loki/setup-recipe.json
{install, seed, env_keys, start, port, health_path} so later verify/e2e runs
replay the exact steps instead of re-detecting heuristically (Devin’s saved-setup
pattern; the CONSUMER already existed). SECURITY: env_keys are variable NAMES
ONLY – the writer never reads the value side of any env line, proven by a test
that plants a secret value and asserts it is absent from the recipe. A failed
boot writes no recipe. Test: tests/test-verify-setup-recipe.sh (3/3, incl. the
secret-leak assertion).effort_estimate
{hours, low, high, method, model, inputs_hash, calibrated, label,
estimator_version}. It is a work-based estimate (diff insertions/deletions +
files + tests + brief size), so two builds of different scope get materially
different hours (a 1-line tweak vs a full-stack feature differ by >3x – the old
flat “iterations x 15min” gave both the same 30min). HONESTY: it FAILS OPEN to
the deterministic heuristic when no model is available (method=”heuristic”,
model=””), and method=”llm” ONLY when an opt-in LOKI_EFFORT_LLM=1 model call
actually returned a parseable number – it never fabricates an LLM figure. Every
estimate is labeled calibrated:false (“uncalibrated”) until a real
ground-truth calibration dataset exists (tracked follow-up). effort_estimate is
a TOP-LEVEL key, kept out of the facts block, so the LLM opinion can never leak
into the deterministic proof headline. inputs_hash is a sha256 over the canonical
inputs (recomputable from the written receipt) for auditability. The existing
loki metrics iterations-based report is unchanged. Test:
tests/test_effort_estimate.py (7/7); no proof-verify change needed (re-hashes the
whole dict; 50 proof pytests still pass).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.
LOKI_COUNCIL_MIN_ITERATIONS when affirmative evidence is already green
(tests present + passing; checklist has items and none failing), instead of waiting
for the next LOKI_COUNCIL_CHECK_INTERVAL boundary. MEASURABLE: on the fixture a
genuinely-complete no-promise run stops at iteration 2 instead of grinding to
iteration 5. This changes only WHEN the council evaluates, never WHETHER it approves:
an early evaluation flows into the SAME council_evaluate (hard checklist gate +
evidence gate + aggregate vote + devil’s-advocate-on-unanimous); there is no
green-evidence shortcut that returns DONE bypassing the vote. Stagnation and evidence
gates still block premature/unverified stops; the explicit-claim and circuit-breaker
paths are byte-unchanged. New knob LOKI_COUNCIL_CONVERGENCE_EARLY (default ON –
this IS a live default behavior change to the no-claim stop path); set 0 to restore
interval-only checking. Bash-only (the loki-ts council shouldStop is a stub). Test:
tests/test-council-convergence-floor.sh (14/14; the early branch reverted makes 3
positive cases fail = non-vacuous).quality_score in aggregate.json:
0 if any blocker, else 100 - 5medium - 2low (floored at 0). MEASURABLE: a tight fix
with no non-blockers scores 100; one Medium non-blocker scores 95; any blocker scores
LOKI_REVIEW_MERGEABILITY_MIN floor
is set (default OFF, so no new blocking behavior). Parity-locked in loki-ts
quality_gates.ts. Tests: tests/test-mergeability-review.sh (10/10) + loki-ts
quality_gates (105/105).oracle-route-missing,
mirroring the existing oracle-datastore-conflict shape); (2) spec’d public API symbols
are verified to EXIST via the LSP proxy (new additive mcp/lsp_proxy.py --check-symbols
one-shot probe that mirrors the existing –write-diagnostics contract), NOT via LLM
grep – and fail-open to inconclusive (never a fabricated exists) when no language
server is reachable; (3) a plaintext-password domain-invariant check (positive-evidence
only; suppressed when a hash primitive is present). MEASURABLE: on 3 clean fixtures the
false-positive count is 0; a spec naming an absent /orders endpoint yields a High
finding; a plaintext password store yields an invariant finding. Bash-only (no loki-ts
oracle mirror exists). Test: tests/test-oracle-source-grounded.sh (16/16, FP rate 0).
HONEST LIMITATION: the LSP symbol-miss High path needs a live fast language server; in
environments without one (or a cold server past the timeout) the LSP leg fail-opens to
inconclusive, which the test covers explicitly.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.
hook_pre_healing_modify (autonomy/hooks/migration-hooks.sh)
decides whether an autonomous healing edit may remove catalogued friction.
Two independent fail-open holes let a removal through that should have been
blocked: (A) type-coercion – safe_to_remove was read with Python truthiness,
so the JSON string "false" (truthy) wrongly cleared the block; (B)
blacklist-only – the gate blocked only the two labels business_rule/unknown,
so any LLM-invented classification with safe_to_remove:false slipped past.
Fix: strict-boolean whitelist – safe = (friction.get('safe_to_remove') is True)
then block unless explicitly cleared, regardless of classification label. Only
a real JSON boolean true clears; strings, ints, null, and missing keys all
block. Not over-blocking: the healing protocol’s prescribed clearance path
(safe_to_remove:true) is preserved, and heal-mode-off / non-matching frictions
stay byte-identical no-ops. RED/GREEN test: tests/test-healing-friction-gate.sh
(8/8; old gate allowed cases A and B, proven by reconstruction).loki-task-board.js _renderTaskDetailModal, acceptance_criteria and
context_files used || [] with no type guard, so a string value (truthy)
reached .map() and threw “not a function”, blanking the modal. Fix:
Array.isArray(...) guards, matching the existing notes/logs pattern.
Test: dashboard-ui/tests/loki-task-board-modal-guard.node.test.mjs (6/6;
3/6 fail against pre-fix).memory/retrieval.py _progressive_retrieve emitted the same memory id in both
the Layer-2 summary and Layer-3 detail passes. Fix: dedup by id, preferring the
fuller record; records without an id are preserved. Test:
tests/test-memory-retrieval-wave6-bugs.py (8/8).loki-overview.js / loki-api-client.js: adopting
a new client on project switch left the old socket open; disconnect() fired
onclose asynchronously which then re-opened via _scheduleReconnect. Fix:
close the old client before adopting the new one; add an _intentionalClose
flag so a deliberate disconnect is not reconnected. Test:
dashboard-ui/tests/loki-ws-leak-reconnect.node.test.mjs (3/3).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.
loki proof share <id> now actually prints the badge it was documented
to print in v7.100.0 (the CHANGELOG described it, but only the gist URL ever
printed – the badge was never implemented). After publishing, the share path
now also prints a copy-paste markdown badge linked to the shared gist/hosted
URL. New loki proof badge <id> subcommand prints the same badge without
publishing (for pasting into a README/PR/post).honesty.headline, read from the SAME redacted proof.json the
share path uses: VERIFIED -> green “Verified by Loki”, VERIFIED WITH GAPS
-> amber “Verified with gaps”, NOT VERIFIED -> red “Not verified”. An absent,
empty, or unrecognized headline prints an honest “no badge” note – never a
fabricated green. Green is reachable ONLY from the exact string VERIFIED.
This is additive and does not touch the trust core or any evidence gate.loki-ts/src/commands/proof.ts), the live route
when bun is installed. The LOKI_LEGACY_BASH=1 bash route ALSO prints a
“Verified by Loki” share badge (autonomy/loki), so both routes carry the badge
on loki proof share. Both render a VERIFIED headline GREEN; the routes differ
only on the VERIFIED-WITH-GAPS color (bash orange, Bun yellow) and on message
style (bash: lowercase message + shields static-v1 URL; Bun: “Verified by Loki”
loki proof badge <id> subcommand and the image-only rendering. The badge uses
a shields.io static-badge image URL (external image host; no infra, no new
dependency). RED/GREEN test: loki-ts/tests/commands/proof_badge.test.ts.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.
[ "$da_result" = "OVERRIDE_CONTINUE" ]
NEVER matched. A DA that found a real blocking issue was ignored and the council approved
anyway. Now compares the LAST line (tail -n1), so the veto forces CONTINUE. Proven: OLD
stops despite a real veto, NEW continues (tests/test-da-veto.sh 9/0).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).
--transport http called
FastMCP.run(transport=’http’, port=…) which raises TypeError/ValueError (no port kwarg, no
‘http’ literal) - the same latent crash fixed for the main MCP server in v7.107.0, present in
the sibling file. Now runs via the supported streamable-http path bound to 127.0.0.1.--search flag was placed as an exec-subcommand
flag, but codex 0.141.0 exposes it as a top-level flag, so LOKI_CODEX_WEB_SEARCH did not
actually enable web search. Corrected the placement (verified against codex –help).claude-opus-4-7 fallback that does not exist
in the model catalog; uses the current opus id.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.
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):
.listen(/createServer( in a test/example/dist file, so a plain
CLI or library that ships a start script plus an integration test that spins an ephemeral
server was wrongly detected as a bootable HTTP app, booted, timed out, and got a false
boot-failure that dropped the verdict from VERIFIED. Now excludes test/tests/tests/
examples/dist/build/spec and requires a stronger signal. Impact: any project with an
integration test no longer false-blocks.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):
The HTTP transport path for loki mcp --transport http was fixed and hardened.
mcp.run(transport='http', port=...) call could never
work: FastMCP’s run() has no ‘http’ transport literal (only stdio/sse/streamable-http) and
takes no port keyword, so the http branch raised TypeError/ValueError. The command now runs
via a supported path (streamable_http_app served by uvicorn), so loki mcp --transport http
actually starts.Authorization: Bearer <token> or it is rejected 401 (constant-time compare
via hmac.compare_digest, with WWW-Authenticate: Bearer). When the env var is unset or empty,
NO auth middleware is installed, so the request path is the plain unauthenticated behavior.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.
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.
An explicit completion claim now triggers an immediate council evaluation instead of
deferring to the 5-iteration interval. When the agent invokes loki_complete_task (or the
COMPLETION_REQUESTED fallback signal), council_should_stop evaluates on that iteration.
This is a SPEED win that INCREASES trust alignment: the loop stops as soon as work is
verified-done, rather than grinding past it.
No verification was weakened. The fix changes only WHEN the council evaluates, never
WHETHER it must approve. COUNCIL_MIN_ITERATIONS (a claim before the minimum still does not
stop), the hard checklist gate, the verified-completion evidence gate, the aggregate vote,
and the devil’s-advocate re-review all run unchanged. A premature or unearned claim is still
rejected and the loop continues – never a forced green. The normal N-iteration cadence is
unchanged when no claim is present.
The claim signal is the STRUCTURED one (loki_complete_task / .loki/signals/
COMPLETION_REQUESTED), not the fuzzy log-grep the circuit breaker uses, so it fires only on
a real, intentional completion claim.
Regression coverage: tests/test-council-convergence-on-claim.sh extracts the real
council_should_stop and drives the full truth table (claim off-interval evaluates now;
no-claim cadence preserved; claim before MIN_ITERATIONS does NOT stop; file-signalled claim
path). Proven RED on the pre-fix code, GREEN on the fix.
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.
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.
/api/memory/summary no longer reports 0 on a JSON-backed project. The
SQLite fast path returned all-zero stats whenever the module imports even with
no .db file, short-circuiting the JSON fallback. It now only trusts SQLite
when it actually reports data, else falls through to the JSON counts (the core
PR #178 fix). Episodes and skills are counted by recursing the dated
subdirectories (episodic/YYYY-MM-DD/*.json, skills/*/*.json) instead of a
flat glob that found nothing.
The summary count and the drill-in lists can no longer disagree. PR #178
fixed only /api/memory/summary; /api/memory/episodes, /api/memory/skills,
the single-item lookups, and the JSON stats fallback still used a flat glob, so
the panel showed a count of N but the list returned 0. All of them now go
through two single-source-of-truth helpers, _memory_episode_files() and
_memory_skill_files(), so the count and the list are always computed the same
way. .lock files are ignored everywhere.
latestDate is the newest episode by recorded timestamp, not by filename.
The intra-day filename tiebreak is a hash, not a time, so a filename sort could
report the wrong episode (observed hours off). The summary and the episodes
list now select the newest by parsed timestamp, and an honest null/empty is
preferred over a confidently-wrong value when no timestamp is readable.
Verified against a real JSON-backed project (dated episodic subdirs, no .db):
the summary count equals the episodes-list length and the reported latestDate
matches the true maximum timestamp. Regression coverage:
tests/dashboard/test_memory_json_parity.py (summary/episodes parity,
summary/skills parity, timestamp-not-filename latestDate, .lock exclusion).
The task board never blanks out on a bad state file. dashboard-state.json
is user/agent-written and can be malformed or partially written. If its
tasks value was not a dict, or a task group was not a list, or a group item
was not a dict, GET /api/tasks raised an AttributeError (from
task_groups.get / task.get) that the surrounding (JSONDecodeError,
KeyError) handler did not catch, 500ing the request and blanking the whole
board. The state-group reader now coerces defensively: a non-dict tasks is
treated as empty, a non-list group is skipped, and a non-dict item is skipped,
so a bad shape degrades to “skip that part” instead of a crash. The valid
tasks still render. (The queue-file reader already had these guards; this
closes the one remaining gap, found during the v7.104.3 task-list review.)
Regression coverage: tests/dashboard/test_tasks_malformed_state.py (dict-not-
list group, non-dict tasks, non-dict item), each proven to 500 on the old
code and pass on the fix.
The dashboard “done” column now shows honest, populated iteration cards.
Previously, when an iteration finished, track_iteration_complete() (autonomy/run.sh)
wrote a THIN completed marker ({id, type, title: "Iteration N", status, exitCode,
provider}) with no description and no logs, and then deleted the RICH in-progress
record - so completed iterations rendered as empty cards with only “Iteration N”
and nothing inside. The completer now LIFTS the honest body (logs, startedAt) from
the in-progress record before removing it, and writes an honest, iteration-scoped
title (“Iteration N complete - )") plus a
one-line description with the phase and duration. It deliberately does NOT reuse the
borrowed PRD-story title from the in-progress record (that title is always the first
pending story, so carrying it onto N done cards would have falsely implied that story
was built N times - a fake-green the trust model forbids).
No more duplicate iteration cards. The completer replaced a blind
data.append(...) (which accumulated the same id across sub-runs - real data showed
iteration-1 five times) with an upsert-by-id, and now makes the completed/failed
terminal state mutually exclusive per id (an id is removed from the other terminal
file and from in-progress on completion).
GET /api/tasks deduplicates by id with a terminal-wins rule (dashboard/server.py).
The dashboard reads dashboard-state.json task groups first; against pre-fix or
partially-written state the same id could appear in more than one column (observed:
iteration-13 in in-progress AND completed AND failed at once). The endpoint now
keeps exactly one entry per id, preferring the terminal (done) record so a completed
iteration no longer lingers in the in-progress/todo columns. Pending PRD-story ids and
iteration ids never collide, so no task that is legitimately pending is dropped.
Regression coverage: tests/test-iteration-complete-accuracy.sh (extracts and drives
the real run.sh completer block - honest title, lifted logs, upsert, mutual exclusion)
and tests/dashboard/test_tasks_dedup.py (terminal-wins dedup across columns).
loki start no longer falsely blocks a genuinely logged-in user. The auth
preflight assumed the Claude Code OAuth login always lives in
~/.claude/.credentials.json. That is false for the native Claude Code install
(2.x on macOS), which stores the login in the macOS Keychain (service
“Claude Code-credentials”) and writes NO .credentials.json. A subscription
user who was fully logged in was told “Claude Code is installed but not logged
in – the build would stall” and blocked from every build: the exact opposite
of the zero-friction intent.
The preflight now asks the CLI’s own local auth-status first. A new
_loki_claude_login_state() helper (autonomy/run.sh) prefers claude auth
status (zero-network, ~0.2s, JSON "loggedIn"), which is authoritative for
BOTH the file-based and the native/Keychain login, then falls back to the
credentials file, then to a macOS Keychain existence check (existence only,
never reading the secret, so it can never trigger a GUI keychain prompt that
would hang a headless build). Critically it applies “inconclusive never
false-fails” to auth: it hard-blocks ONLY on positive evidence of no login
(“loggedout”) or an expired token (“expired”); when the state cannot be proven
(“unknown”, e.g. an older CLI on a non-macOS box with no file) it fails OPEN
and proceeds, so a real login is never blocked on uncertainty. claude login
guidance and the LOKI_SKIP_AUTH_PREFLIGHT=1 / ANTHROPIC_API_KEY opt-outs
are unchanged.
A stale expired credentials file never overrides a live Keychain login.
The native install can leave a now-expired ~/.claude/.credentials.json next
to a valid Keychain login (it rotates the Keychain, not the file). The helper
therefore checks the Keychain (a positive signal) BEFORE concluding “expired”
from the file: a valid file -> logged in; else a Keychain entry -> logged in;
only with neither is an expired file treated as genuinely expired. Genuine
expiry (expired file, no Keychain) still returns “expired”, so the 401-avoiding
fast-fail is preserved.
loki doctor now affirmatively confirms the login, on BOTH the bash and Bun
routes. Both read claude auth status, so each shows “Claude CLI is logged in
(subscription/OAuth login)” for a Keychain-backed login (previously the bash
route fell through to a neutral “ANTHROPIC_API_KEY not set” line and the Bun
route wrongly reported “login EXPIRED” for a Keychain login), warns on a genuine
logged-out or expired state, and keeps the file-based expiry fallback for older
CLIs. The bash<->Bun doctor output stays byte-identical (enforced by the
bun-parity matrix).
Regression coverage: tests/test-claude-login-state.sh extracts the real
_loki_claude_login_state() helper from run.sh and drives every login state
with fake claude AND security binaries on PATH (fully deterministic,
independent of the machine’s real login). The cases: auth-status logged in
(no file); auth-status logged out; valid JSON with no explicit loggedIn
boolean plus a valid file (must fall through to logged in, never a false hard
block); valid file; expired file with no keychain (-> expired); expired file
WITH a live keychain (-> logged in: the regression guard that a stale file
never overrides a real Keychain login); keychain-only with no file; and the
confident-logout path. Proves the exact user-reported scenario (logged in, no
.credentials.json) no longer blocks.
loki doctor (Bun route) now performs the Claude OAuth login check that the
bash route already had. Previously the Bun doctor only checked ANTHROPIC_API_KEY
and never inspected the Claude login state, so on a machine with an expired Claude
access token the two routes diverged (bash warned “login EXPIRED”, Bun said nothing).
The check is a zero-network read of ${CLAUDE_CONFIG_DIR:-~/.claude}/.credentials.json
comparing claudeAiOauth.expiresAt against now; it is byte-faithful with the bash
output in every login state (expired -> WARN, absent/fresh -> the neutral line), so
the two routes agree. Conservative by design (no refresh attempt, which needs a
network call), matching run.sh’s fail-fast build preflight. Proven parity across
expired / absent / fresh credential fixtures.sonnet (providers/claude.sh), which the
Claude CLI resolves to the latest Sonnet (Claude Sonnet 5). Builds are cheaper
(~40% less per token than Opus) and faster, at near-frontier intelligence. This
affects every user’s default loki start/loki verify run. Completion is still
council-verified. To restore Opus execution: set
LOKI_CLAUDE_MODEL_DEVELOPMENT=opus (and _PLANNING=opus), or pick Opus from
the dashboard. We do NOT claim identical accuracy (no controlled study); the
quality gates are unchanged, so if it passes it passes and if it does not Loki
tells you why.LOKI_ADVISOR_MODEL). The trust-gate code
reviewers still run on the account default by default (unchanged behavior). Set
LOKI_ADVISOR_MODEL=opus (or the dashboard “Advisor” toggle) to pin the
reviewers to a stronger, consistent Opus judge. haiku|sonnet|opus are honored;
fable is refused (its safety classifiers can end a review turn with a refusal
and break the council gate). Mirrored on both the bash and Bun routes..loki/state/model-override, read each iteration); this exposes it
in the UI. An explicit Opus pick dispatches Opus on every iteration.LOKI_SESSION_MODEL=opus
pin previously routed through the (now-Sonnet) planning tier and silently
dispatched Sonnet. It now dispatches Opus on every iteration (still clamped by
LOKI_MAX_TIER, so an operator cost ceiling still binds). Fixed consistently in
the runner, the loki plan estimator, and the dashboard, and locked by the
session-pin parity matrix.providers/model_catalog.json refreshed
to current IDs (claude-opus-4-8, claude-sonnet-5). Corrected the
gpt-5.3-codex price to the real standard-tier $1.75/$14.00 per MTok (was
$1.50/$12.00) in the cost tables. Added a display-only note for the Sonnet 5
introductory price ($2/$10 per MTok through 2026-08-31); the estimator quotes the
standing $3/$15 list price (over-estimating the display is the safe direction).loki-ts/src/runner/autonomous.ts):
it already refused completion on a code_review BLOCK; it now also refuses on a
semantic_tests BLOCK and an invariants BLOCK, mirroring the live bash route
exactly (same LOKI_GATE_SEMANTIC_TESTS_BLOCK / LOKI_GATE_INVARIANTS_BLOCK
toggles, same conditions). This closes a trust-parity gap ahead of the runner
being wired on. It refuses ONLY when the gate genuinely blocked (the gate is in
the failed set and its toggle is on), so a clean run still completes - no new
user-visible knob, the existing toggles are reused.autonomy/verify.sh): the base ref now
resolves an actually-existing ref (origin/main, then main) instead of
hardcoding it in both branches; VERIFY_VERDICT/VERIFY_EXIT are initialized
before the error path so an early error can never emit an empty or VERIFIED
verdict or exit 0 (fail-closed).autonomy/lib/proof-generator.py):
_norm_gate_status returned an unrecognized gate status verbatim (e.g. a gate
emitting “blocked” or a custom token). The Evidence Receipt headline only
checks gate status against “passed” and “failed”, so such a value matched
neither and silently vanished - a gate that did not pass would not count
against a VERIFIED headline (a fake-green vector). An unrecognized status now
maps to “inconclusive”, which lands the gate in honesty.degraded and forces an
honest non-green headline. Known statuses are unchanged. Regression test added.autonomy/lib/proof-generator.py): a build that produced a non-empty diff but
ran ZERO tests, ZERO build, and passed ZERO quality gates could emit a
“VERIFIED WITH GAPS” headline. A non-empty diff is a PREREQUISITE for VERIFIED,
not a positive fact of passage - code was written but nothing was shown to pass.
The diff is no longer counted as a verified fact; such a build is now honestly
NOT VERIFIED. A genuine pass is unchanged, and a verified-but-degraded build is
still VERIFIED WITH GAPS. One headline codepath, so this covers both the bash
and Bun routes. Regression test added.dashboard-ui): a brand-new user with no
builds now sees a “Start your first build” hero (with loki quickstart and the
zero-key loki tour) instead of a blank, gated to a genuinely never-built
project and fail-safe hidden on any error (never falsely claims you have not
built).autonomy/run.sh): a non-writable workspace
(disk full / permissions / read-only) now fails fast with a copy-paste remedy;
a build that produced no file changes gets an honest next-step instead of a
silent no-op. loki tour added to help.bin/postinstall.js (no postinstall hook since v7.4.12).README.md): npx loki-mode tour works
today with no global install, no provider CLI, no API key, no spend - the
lowest-friction taste of the verified-completion receipt. It is now the leading
“Get Started” line (it was previously undocumented).loki tour (autonomy/loki): the no-args
landing and the first-run welcome both previously gated all value behind a
provider CLI + API key (doctor/quickstart/start); they now offer loki tour
first - a real sample result with no key and no spend - so a brand-new user
sees value before any setup.loki demo --offline (and loki tour): a zero-key, zero-spend, no-network
taste path. It replays a bundled real Evidence Receipt and prints its honest
headline + facts in under a second, so a newcomer can see what Loki produces
WITHOUT installing a provider CLI, logging in, or spending anything. It is
clearly framed as a SAMPLE/replay (never presented as a verdict on the user’s
own code) and never renders a green/verified signal unless the bundled
receipt’s real headline is VERIFIED (it is VERIFIED WITH GAPS, shown as such).loki proof share receipt badge: after sharing, prints a copy-paste
“Verified by Loki” markdown badge for READMEs/PRs/posts. The badge color and
text derive ONLY from the receipt’s honest headline: green only for VERIFIED,
a distinct badge for VERIFIED WITH GAPS, red for NOT VERIFIED, and no badge at
all when there is no verifiable headline (no fake-green, no new network egress).loki quickstart: the guided one-command first build is
now the headline path; loki init + loki start remain as the explicit path.memory/retrieval.py): when the embedding engine has fallen back to a
different model/dimension since a vector index was built, the query vector no
longer matches the index and VectorSearchIndex.search raises ValueError.
retrieve_by_similarity now catches that and degrades to keyword search (the
same honest fallback the staleness path uses) instead of crashing or returning
wrong-dimension neighbors. Regression test added. (Supersedes the reverted
embeddings-only signal from the wave-7 batch, which was inert.)memory/retrieval.py): anti-patterns were
double-counted when present in both patterns.json and the legacy
anti-patterns.json (now deduped); search_all_namespaces omitted the
anti_patterns collection entirely, so “do not repeat this mistake” memories
were silently missed in cross-namespace search (now included); vector-index
staleness was checked only for the semantic collection after consolidation
(now also the anti_patterns collection invalidates over its source files).autonomy/run.sh): on
loki start, start_dashboard acquired the dashboard port by killing any
python/uvicorn process already listening there - which dropped a user’s own
live dashboard mid-use (connection refused, websocket failure). It now probes
/api/status first and REUSES a healthy dashboard already serving on the port
(skips launching a second server) instead of killing it; only a non-serving
stuck dashboard is reclaimed (opt out: LOKI_DASHBOARD_FORCE_RECLAIM=1). The
shutdown/teardown port-reclaim path got the same probe-before-kill guard.autonomy/checklist-verify.py): the tests_pass
check ran with --passWithNoTests, so a zero-match test pattern reported
SUCCESS - a checklist item that REQUIRES test verification passing with no
tests run. It now requires at least one test to be discovered.Cost aggregation glob (dashboard/server.py): cost totals globbed
*.json (including non-iteration files); now uses iteration-*.json,
consistent with the budget-snapshot and cost-timeline readers.
Found by the wave-7 adversarial hunt.
autonomy/completion-council.sh):
the operator’s LOKI_COUNCIL_THRESHOLD was silently ignored by the vote (a
hardcoded 2/3 formula was used), so setting a stricter threshold was a no-op
while the logs claimed it was active. The effective threshold is now
max(2/3-majority floor, operator threshold), clamped to council size: an
operator can TIGHTEN the completion gate but never weaken it below the safety
floor. Shared _council_effective_threshold helper used at all three vote
sites (council_vote, council_aggregate_votes, council_evaluate). This covers
the live bash route; the dormant Bun runner council (not yet wired into the
completion-decision path) carries the same latent gap and is a tracked
follow-up.autonomy/completion-council.sh): a non-positive or
non-numeric LOKI_COUNCIL_SIZE made the 2/3 floor 0 and could let an empty
council “approve” by default; it now falls back to a sane size (3).autonomy/run.sh): the
COMPLETION_REQUESTED fallback synthesized its payload with a bare redirect that
truncated the file before python ran; a python crash mid-write could leave an
empty/partial file that downstream json.loads silently dropped. It now writes
to a temp file and atomically renames, with a hand-built valid-JSON fallback
when python is unavailable.autonomy/run.sh): when claude is the provider, no ANTHROPIC_API_KEY, and
no credentials file exists (never logged in), the build used to enter, make a
failing call, and 401 - forcing the user to run loki why. It now fails fast
with the same one-step claude login fix (opt out: LOKI_SKIP_AUTH_PREFLIGHT=1).Docs: floating Docker tag (README.md): the raw-docker quickstart pinned a
stale :7.58.1; now uses :latest so the copy-paste pulls the current image.
Found by wave-6 adversarial hunt + a zero-friction adoption analysis.
memory/storage.py, memory/engine.py): importance calc clamps a
non-numeric/negative access_count (was a TypeError on stored strings); shared
namespace charset validation so init and with_namespace cannot drift, and
with_namespace rejects empty/whitespace/non-string (was silently resolving to
root); a corrupt ISO timestamp on one record no longer crashes the whole
retrieval batch (tolerant parse keeps the record retrievable).memory/retrieval.py, memory/rag_injector.py):
unstamped legacy entries are EXCLUDED from a namespaced query by default
(closes a silent cross-namespace read leak; opt-in escape hatch retained);
memory fields are sanitized before entering the prompt (prompt-injection
hardening: control-char strip, newline collapse, markdown-header/code-fence
defang, length cap).events/bus.py, events/emit.sh): widened the deterministic
dedup id from 32-bit to 64-bit (collisions could silently drop distinct
events); event-file write is now temp-file + atomic rename (was non-atomic,
could expose truncated JSON to readers); removed dead/malformed sed escape
entries (the awk pass already emits valid JSON for control chars).autonomy/hooks/migration-hooks.sh): snapshot pairing contract
enforced (a revert with no matching snapshot fails loudly instead of silently).autonomy/run.sh): Claude parallel-worktree invocations
now apply the same auto-flags (effort, max-budget, fallback model, mcp-config)
as the main invocation.FIX A rework (loki-ts/src/runner/autonomous.ts): the Bun runner honors
the quality-gate verdict with CORRECT bash parity - it refuses completion only
on a real code_review BLOCK (gated on hardGates), not on the broad
blocked||escalated that over-blocked clean runs in the reverted first attempt.
Clean runs still complete (autonomous.test.ts 17/17, not edited to pass).
The mcp code-index staleness finding was investigated and REFUTED (the path already uses an atomic manifest rename); no change made.
autonomy/run.sh): the
interval force-review gate chain had the same fail-open hole fixed on the
default route in v7.95.0 (a missing gate fn silently skipped), letting a
partial council-library load force-approve completion ungated. It now probes
the core gate functions first and refuses the force approval if any is missing.
Found by the wave-4 adversarial bug-hunt.
autonomy/run.sh): each completion gate was
armed as type <gate_fn> && ! <gate_fn>, so if the council library failed to
source, the type probe was false and the gate was silently skipped - a
completion claim could then reach the accept branch UNVERIFIED (a fake-green).
The gate chain now verifies the core gate functions are loadable before running;
if any is missing, the completion claim is refused and the loop keeps iterating
rather than passing ungated. Never fires in a healthy install (functions are
sourced); only triggers on a genuinely broken/partial load, exactly when
failing open is unsafe. Found by the cycle-3 adversarial bug-hunt.loki start with NO PRD over a project Loki already built and
completed reverse-engineered (reused) the prior generated PRD and then rebuilt
a full task queue and re-ran the RARV loop, re-doing finished work. The reuse
path never asked “is this reused PRD already satisfied by the current code?”
Observed live: a completed project (completion signal present, all tests green,
advisor said stop) still got an 11-task prd-* queue and fresh iterations.reuse_done_recognition_gate, in the new
autonomy/lib/done-recognition.sh) runs in run_autonomous() between
load_state and the queue/loop, only on a no-PRD reuse of an existing
generated PRD. It model-verifies whether the codebase already satisfies the
spec and routes to one of three outcomes:
loki start --fresh-prd; to extend it, edit the
spec or pass a new/changed PRD.”). No wasted iterations, no stray delegate
branch (the gate runs before the start-sha/delegate block).prd_sha-guarded satisfied-requirements manifest so
populate_prd_queue builds ONLY the unsatisfied requirements (no full
replan).done verdict is always
the model’s, grounded in re-run reality, never asserted from a stale artifact.
A model done is DOWNGRADED to build if the fresh test-results.json is red or
any requirement is unmet/uncertain. The update action (PRD stale by
definition) may never fast-stop as done; it only feeds the incremental queue.
The only deterministic shortcut is NEGATIVE (cheap signals that route to
BUILD, e.g. no completion footprint at all, or a provider that cannot verify).LOKI_DONE_RECOGNITION=0 disables the gate (legacy
reuse-then-build behavior). Trust-safe because any uncertainty falls through to
build, so the worst case is today’s behavior.tests/test-reuse-done-recognition.sh (registered in
tests/run-all-tests.sh and scripts/local-ci.sh) exercises the gate with a
stubbed model seam across done+green, done+red-downgrade, inconclusive,
incremental-only-unsatisfied, opt-out, update-never-fast-stops, the negative
fast-paths, and a static no-emoji/no-em-dash scan.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):
validate_api_keys now does a fast preflight before a build: git presence (a
genuine hard requirement - the build inits a repo - so a missing git blocks with
a copy-paste fix), plus ADVISORY node-version and provider-reachability checks
that WARN and continue (never block a working user; the real call is the test).
Opt out of the network probe with LOKI_SKIP_NET_PREFLIGHT=1.Added (failure diagnosis):
.loki/state/LAST_ERROR.json ({iteration, error_class, brief, timestamp}),
cleared at the start of every fresh run. loki why now surfaces it (text +
--json) only when the current run actually failed - never beside a success..loki/NEXT_STEPS.txt..loki/signals/RATE_LIMITED record (forward-laid).Added / changed (CLI + doctor):
loki and loki help; deprecated
run demoted out of main help into loki run --help.loki doctor; friendlier unknown-command pointer + grouped
“more commands” footer.loki doctor Python check reworded honestly: Python 3.12 is recommended for
memory vector search; core memory and the rest of Loki work without it (WARN,
not a hard fail).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.
workspace param on /api/control/start and workspace-scoped /api/status,
/api/cost, /api/proofs, /api/proofs/:id (validated against the workspace-root
allowlist, traversal-safe, deny-by-default), so a host can run and correlate a
per-build proof by exact run_id. Proven: proof.run_id == trust-run-id.validate_api_keys now does a zero-network, zero-token local
expiry check and stops immediately with a claude login fix. Fail-open on any
unknown schema; opt out with LOKI_SKIP_AUTH_PREFLIGHT=1.loki doctor expired-login warning: mirrors the preflight so a user catches an
expired login BEFORE starting a build.currentPhase now advances BOOTSTRAP -> BUILDING ->
COMPLETED (with a COMPLETED marker file), so the dashboard and reconciliation gate
see real progress instead of a stuck “Planning”.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.
Fixes from watching the live dashboard during a real build, all aimed at a cleaner experience for whoever is watching:
loki dashboard stays
up and shows a clear “no active runs” note. It never tears down a dashboard that
other running projects still need.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.
loki: verified-completion) maps the headline
to a check-run conclusion. It is OPT-IN (LOKI_PROVEN_PR_CHECK) and can never
block a merge – only the repo owner can require it as a branch-protection
check. To make verified-completion blocking, add it as a required status check
on your repo.The dashboard proofs panel now links each proof to its PR and headline.
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.
A pass over what a user actually sees while Loki works, found by watching real builds:
loki wiki generate), so the overview / architecture / modules / data-flow and
the diagram stay current. It is incremental (only when the codebase changed),
default-on, opt out with LOKI_WIKI_AUTO=0.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.
A real loki start build (haiku, not fixtures) surfaced a batch of issues across
the trust + first-run surfaces. All are fixed here:
loki proof verify <id> now works on the default route. The finish-and-own
handoff (loki own) tells you to run exactly this command to re-check that a
receipt still matches your code, but on the default (Bun) route it returned
“Unknown subcommand” – only the legacy bash route had it. It now shells to the
same verifier on both routes, byte-for-byte identical (clean = exit 0,
tamper/drift = 1). loki receipt is a working alias for loki proof.loki own and the generated HANDOFF.md no longer render broken nested code
fences. The run/verify commands quoted from USAGE.md are wrapped exactly once.loki proof list shows the honest verdict (VERIFIED / VERIFIED WITH GAPS /
NOT VERIFIED) in the VERDICT column instead of a blank, and proof.json records
the real Loki version instead of “unknown”.loki own, never silently dropped).loki web (deprecated) now launches the real dashboard instead of the old
Purple Lab dead end. loki version shows a one-line hint when a newer release
is available (cached, opt-out, silent in CI).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.
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).
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.
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.
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.
loki proof verify <id> re-hashes the receipt (tamper
check) and re-derives the diff from the recorded base commit against the
current branch (drift check); exit 0 means clean, exit 1 means tampered or
drifted. The shareable receipt page prints the exact command and the base
commit so a skeptic can re-run it. loki receipt is a friendly alias.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.
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/.
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.
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.
loki next reads the build’s own state and runs the
right next step (resume / ship / why), confirm-then-run on a TTY, print-only
with –dry-run or off a TTY. loki ship finishes a build in one command:
optional preview, the same diff quality gates as review, and the exact
branch-aware PR command on a clean result (print-only; never pushes).loki start: when run with no spec it auto-discovers an on-disk
spec (prd.md, a single top-level .md/.yaml), shows a resume hint if a previous
build here was interrupted (naming the last checkpoint), and prints a one-line
pre-flight summary. All opt-out; nothing changes when a spec is given or none
is found.loki doctor ends with a copy-paste next step,
a one-command completions install, quickstart surfaced on the landing/welcome
screen, and a drift-proof completion-coverage check so the command list cannot
silently fall out of sync again.Full suite green (1422 pytest + bun + shell suites); completion-coverage drift check passing.
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.
run_test "..." "python3 <file>", but that runner invokes the entry via
bash "$file" (it expects a single bash-executable file, not a command string),
so it ran bash "python3 <file>" -> “No such file or directory” and the CI
“Shell tests” job failed. local-ci passed because its run_check runs the entry
as a shell command string (different contract), masking the bug.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)
loki start as a flag and could change build mode. The
consumer now rejects a leading-dash spec as malformed, and loki start gained a
-- end-of-options handler so an untrusted spec can never be read as a flag.Honesty
local-ci green; full test suite (1408 pytest + bash suites) green; helm lint clean across all worker modes.
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
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
loki start --config <file> (aliases --vars,
--env-file) loads one .env / YAML / JSON file, so Docker / compose / k8s /
Vault operators inject a single mounted file instead of a wall of LOKI_* env
vars. Locked precedence: CLI flags > --config > ambient env > auto
.loki/config.yaml > settings.json > defaults. Secrets are referenced via
${VAR} (resolved by bash indirect expansion, never eval), never inlined; a
literal secret value warns on load and fails loki config validate. New
loki config example|schema|validate, all generated from one canonical key map
(the three config-mapping tables that used to drift are now one).lokistore/): one local-first interface every
durability feature binds to. Default is the same .loki/ files you already
use (zero new deps; importing it loads no cloud SDK). Opt in to s3 / gcs /
azure-blob (lazy-imported only when selected) via storage.backend.storage.backend, a
build’s checkpoints sync off-cluster and a pod rescheduled onto a fresh node
hydrates and resumes. The Helm worker sets a stable LOKI_RUN_ID so keys match
across pod restarts.LOKI_SESSION_ID,
so two builds in one pod no longer clobber each other (legacy single-build path
unchanged when unset).Security and hardening
loki start (not the deprecated loki run), reaps children
(no zombies), dedups redelivered webhooks, and rejects a malformed
owner/repo so a webhook payload can never inject a CLI flag into the build./lab sub-app and the native
/ws/collab endpoint were reachable without auth when enterprise auth was on
(the collab path could mutate shared state); both now require a valid scoped
token (close 1008 otherwise) and are pass-through when auth is off.validate_yaml_value used a regex bracket class
that matched nothing and silently accepted shell metacharacters; it now
rejects $(...), backticks, pipes, etc./api/memory/* reads and the /api/collab/*
routes that were missing scope checks now match their gated siblings.Honest scope (NOT overclaimed)
LOKI_ALLOWED_PATHS is PARTIAL enforcement: it restricts the sandbox custom
--mount host path and an operator-supplied sandbox command. It does NOT
restrict provider-driven agent file writes (those go through the provider CLI,
not run.sh; the container is their containment). Docs say exactly this..loki checkpoint state; the git refs/loki/cp/*
worktree snapshots are not synced. SSO/SAML, SCIM, app-level/tenant RBAC, and
SOC2 remain roadmap, not shipped.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.
Cleanup release. No behavior change for any live path.
type fn && fn from autonomy/loki / traps / export -f / hook patterns)
and the bash<->Bun parity mirrors. Removed: the unused notify_task_* /
notify_phase_complete / notify_all_complete wrappers (the live helpers are
notify_intervention_needed / notify_rate_limit), spawn_feature_stream,
load_learnings_context, set_phase, save_learning, get_gate_failure_count,
generate_dashboard (the largest dead block, ~431 lines; the CLI uses
cmd_dashboard / server.py), list_checkpoints + rollback_to_checkpoint (the live
path is the cmd_rollback CLI), get_phase_names, get_complexity_phases,
enrich_from_knowledge_graph, store_to_knowledge_graph, and run_adversarial_testing.
Functions that LOOK orphan but are reachable (export_tasks_to_github via the
loki dispatch, check_command_allowed for external sandbox/hook callers) were
kept.local-ci 85/0. Full bash + memory + dashboard + loki-ts test sweep green.
Final wave-13 bug-fix batch. Council 3/3.
claude --agents with no
timeout, so a hung dispatch stalled the entire run indefinitely. It is now
wrapped in timeout ${LOKI_COUNCIL_REVIEW_TIMEOUT:-600} (parity with the
heuristic path); a timeout (exit 124) or any non-zero exit falls back to the
heuristic council and can never become a false COMPLETE.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).
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:
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).
.loki/ checkpoints, state, queue,
signals, logs, the agent feature branch, and the refs/loki/cp/* checkpoint refs
in the checkout’s .git) lives under the working directory, so mounting ONE
durable volume at the container workdir makes a build survive pod loss with no
path refactor. A startup assertion (LOKI_DURABLE_STATE=1 only) fails fast with
exit 20 if the mount is missing or read-only, instead of silently losing state on
the first crash. The machine-global registry stays off the volume; /tmp scratch
stays ephemeral.running) with a valid run-start-SHA baseline on the volume RESUMES from its last
iteration on Job restart, instead of restarting from scratch. The agent branch,
checkpoint refs, queue, and checklist all survive on the volume and the next
iteration continues from them. Resume re-enters the RARV verification loop: a crash
never inherits a gate PASS, and a completed-then-rerun is still a new run. Outside
durable mode, the original safe reset behavior is unchanged.0 for a
completed / human-stopped run, 20 for a deterministic terminal failure (failed
gate, max iterations) so the Job’s podFailurePolicy fails it immediately without
burning retries, and a retryable nonzero for a crash so the Job retries and
resumes. (Local/CI exit codes are unchanged.)LOKI_AUTO_PR=1 / LOKI_DELEGATE_PR=1
paths; the default advisory-only path is unchanged.)deploy/helm/autonomi): the RARV worker is now a batch/v1 Job
(restartPolicy Never + backoffLimit + activeDeadlineSeconds + podFailurePolicy:
exit 20 -> FailJob, pod eviction -> Ignore) with one durable volume at the
workdir and LOKI_DURABLE_STATE=1 set. The broken liveness probe (pgrep -f loki
matched a hung process) and the phantom readiness probe (a file nothing created)
are removed. Decorative, never-read LOKI_CHECKPOINT_DIR / LOKI_AUDIT_LOG_PATH
env are removed. worker.spec selects what the Job builds (repo at the mounted
workdir, a PRD path, or a GitHub issue ref). The control plane stays a Deployment;
its audit mount now points at the path dashboard/audit.py actually writes. HPA /
worker Service / worker PDB render only under an opt-in worker.mode: deployment
(a reserved queue-consumer stub, off by default). Chart appVersion and image tag
bumped to current. Requires Kubernetes 1.31+ for the podFailurePolicy exit-code
contract.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.
A deep-hunt wave found and fixed trust-surface defects on the live loki start path.
loki start) previously lifted a real Critical/High
code_review BLOCK when the gated agent supplied a trusted proofType plus any
artifact string. Because the gated agent authors the counter-evidence file
itself, this was self-certification: a trust gate the gated party can bypass is
not a gate. The stub route now lifts NOTHING. A code_review BLOCK is cleared only
by the operator (review and fix, or the human-escape path), or by the real
multi-judge override council (an adjudicator the agent does not control), which
is unchanged. Escalation and docs re-framed to stop promising that self-supplied
counter-evidence lifts a block.loki projects add|remove|sync now go through
the locked, atomic registry API (matching the v7.45.1 hardening) instead of an
unlocked truncating write that could lose updates or expose a partial file to a
concurrent reader.loki docker --image, loki watch --interval
/ --debounce), and loki context add no longer crashes on filenames with an
apostrophe.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.
main). Detached HEAD and non-git directories no-op honestly.loki/session-* branches only; a
branch you named yourself is never auto-committed..loki/ runtime state is self-ignored, and every staged
file is scanned (filename heuristic + content patterns, including URI-embedded
credentials like postgres://user:pass@host and redis://:pass@host). If a
possible secret is detected the commit ABORTS, leaving your work uncommitted with
an honest message naming the file – it never commits a possible secret.LOKI_AUTO_PR=1 restores
auto-push + auto-PR for anyone who wants it (now with the correct --base).LOKI_BRANCH_PROTECTION=false remains a full opt-out (prior behavior preserved).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.
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)
[y/N] (default no). Non-interactive use
requires --yes; without it on a non-TTY, it refuses rather than silently
exposing anything.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
--bg. Off-TTY output is byte-identical to before
(verified). Set LOKI_HUD=0 to disable.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:
loki internal phase1-hooks override path
(internal_phase1.ts) now require at least one non-empty artifact before a
trusted proofType yields APPROVE_OVERRIDE; empty -> the BLOCK stays.|| echo "OK"). It now
fails closed (BLOCK) on a parse error, matching every sibling gate in the file.Provider cost ceiling:
PRD reuse:
toString("binary") then utf8 write), which also made the next
same-spec run misclassify the PRD as hand-edited. Now a byte-exact rename.Memory:
Honesty:
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.
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:
loki internal phase1-hooks H2 batch-abort tests stubbed
learnings_writer via Bun’s mock.module, a GLOBAL registry with no reliable
per-test revert (mock.restore() does not touch it). Under cross-file test
discovery ordering on Linux the throwing stub leaked into the learnings_writer
suite, producing a nondeterministic CI red on the Tests and Coverage workflows
(green on macOS where readdir order differs). Replaced with a module-local
test seam reset deterministically in afterEach. Production behavior unchanged.Memory subsystem:
Dashboard / migration:
CLI / runtime (bash):
loki status --json no longer crashes on a malformed status file: the
JSON-error fallback was dead under set -e (a bare python3 call aborted the
function before the fallback ran); it is now guarded so a parse error degrades
to an honest error object.loki explain --json, loki heal, and loki onboard pass values to their
python helpers via the environment instead of interpolating raw bash values
into python3 -c heredocs, fixing a quote/apostrophe-injection class that
silently degraded output (e.g. a project path containing an apostrophe).invoke_with_timeout helper and its unused spawn-config vars
from run.sh (never wired to the provider invocation; kept route parity with
the Bun side, which has no such wrapper).refs/loki/cp/<id> git refs (and
their anchored stash commits) for pruned checkpoints, which previously leaked
unbounded.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).
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):
VERDICT: FAIL with ANY suffix (“FAIL - [Critical]
SQLi”, “FAIL.”, “FAIL (3 criticals)”) was exact-matched against the literal
string “FAIL” and, failing that, counted as PASS. A real Critical/High review
was silently waved through and completion proceeded. The verdict is now
classified on its leading token with a safe default: any FAIL/REJECT/BLOCK
token blocks, and any unparseable/ambiguous verdict is treated as non-passing
(never silently passed), mirroring the completion council’s parse-miss-to-REJECT
contract. The token classifier also avoids the inverse error (a valid
“PASS, no failures found” is no longer misread as FAIL).[Critical]/[High] brackets;
it now also matches **Critical**, Severity: High, and - Critical: forms
(without over-matching prose like “highly readable”), at both the council and
Devil’s-Advocate sites.grep -c ... || echo 0 idiom emitted a two-line “0\n0” that broke the integer
comparison; feature_count and section_count are now digit-sanitized.App runner (live):
bash -lc "exec docker build ... && docker run ...", and exec
replaced the shell with docker build so && docker run never executed (image
rebuilt forever, dead preview URL). The exec optimization is now applied only
to single commands; compound methods run in full. The Dockerfile image tag is
now project-hashed (was a shared loki-app that clobbered across projects), and
the verify/health/watchdog paths gained a Dockerfile-container branch.CLI:
loki stop <session-id> now performs the same process-group kill as the no-arg
loki stop, scoped to that session’s process group. Previously the by-id path
returned early and left the autonomous agent (which shares the orchestrator’s
group and reparents to init) running and still editing files (the v7.7.34
orphaned-agent bug, which had survived on the by-id route).Memory data-integrity:
loki internal phase1-hooks) now holds
a cross-process file lock across the full read-modify-write of
relevant-learnings.json (was an in-process mutex only, dropping entries under
concurrent phase1-hook processes); the reflect loop isolates per-finding
failures so one bad finding no longer drops the rest; and the learning dedup key
now includes the file so distinct same-worded findings in different files are
not collapsed.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).
_file_lock: removed the os.remove(lock_path) on lock
release. Unlinking the lock file on every release is a flock+unlink
inode-replacement race: with 3+ contenders, a holder can unlink the inode a
waiter just acquired, after which a third contender opens the path, finds it
gone, creates a NEW inode, and flocks that – entering the critical section
while the waiter is still inside. This dropped index.json topics under
concurrent store_pattern / store_episode. Reproduced deterministically on
Linux CPython 3.13 (16 threads: 3/25 runs dropped a topic pre-fix; 25/25 pass
after). Persistent lock files are the standard flock pattern; stale ones are
GC’d by _cleanup_stale_locks, which is itself flock-safe (probe-before-unlink,
v7.65.0). This completes the v7.65.0 lost-update hardening (the lock body was
correct; the release path was not).execFileSync("head")
subprocess spawns. They were non-hermetic (returned 0 bytes on some CI runners,
e.g. macos-latest + bun 1.3.13) and unnecessary – the head-vs-tail semantics
the test pins are fully captured by comparing Bun’s cap against the
deterministic expected slice. No loss of coverage.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):
council_should_stop returns 0 both from a genuine approval
AND from two safety valves (stagnation flood, repeated done-signals), and the
runner treated all three identically as “PROJECT COMPLETE” / council_approved,
even opening a “done” PR (when LOKI_DELEGATE_PR=1). A force-stop is not a verified
completion. A sentinel (COUNCIL_FORCE_STOPPED, reset at function entry, set only
at the two valves) now disambiguates: the runner reports force_stopped (honest
“stopped without approval” summary, no PR) for a force-stop and keeps the genuine
council-approval path byte-for-byte unchanged. The run still stops in both cases.
This upholds the core “does not call work done until verified” contract.Reliability fixes (live bash route):
timeout (LOKI_COUNCIL_REVIEW_TIMEOUT, default 600s), so a hung provider CLI can
no longer stall the whole council. A timed-out call yields an empty verdict that
flows to the existing conservative fallback (member to heuristic, devil’s-advocate
to REJECT). Mirrors the Bun route’s reviewer timeout.tail -n1, neutralizing all current and future log-line pollution.Forward-looking fixes (Bun route, currently dormant / tree-shaken until loki start
routes through runAutonomous):
/^\s*VERDICT:/i), matching the override-judge parser and the bash route,
shrinking the unparseable surface that fed the fail-open path.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):
storage._decay_semantic /
_decay_episodic / _decay_skills and engine._update_index_with_episode /
_update_index_with_pattern previously read a JSON state file under one lock
scope, mutated in memory, then wrote under a separate lock scope. A concurrent
save_pattern / store_episode landing between the read and the write was
silently clobbered (dropped patterns, under-counted index totals). All sites
now hold ONE exclusive file lock across the full read-modify-write, mirroring
the already-correct save_pattern template. This completes the v7.58.0
lost-update fix, which had been applied unevenly. Cross-process safe (fcntl
flock on the sidecar lock file).Defensive robustness (corrupt / hand-edited records; no live writer emits these values, schema validation enforces ranges):
.lower() on a raw record field: boost_on_retrieval, apply_decay,
calculate_importance (importance, phase, category), _score_result,
_keyword_search_semantic / _anti_patterns / _skills / _episodic,
optimize_context (confidence, usage_count), and the engine search /
update-pattern confidence + usage_count paths. An explicit null field no
longer crashes a query with TypeError / AttributeError. The guard preserves a
legitimate stored 0.0 (is-None check, not truthiness) where 0.0 is
meaningful (importance, confidence).engine.store_skill and storage.save_episode
now sanitize the filename / validate the date component before building the
on-disk path, matching save_skill. A poisoned skill name or episode
timestamp (../../../tmp/evil) can no longer escape the memory root._cleanup_stale_locks now probes a stale lock with a non-blocking flock
before unlinking, so a long-running (>5min) live writer’s lock is not removed
out from under it.Correctness:
id / summary), so the previously-inert layer-1 ranking works.optimize_context is now strict-by-default (never exceeds
the stated budget); a previously-hardcoded 10% overage is now an explicit,
documented, opt-in slack_ratio parameter (default 0.0).Test infrastructure:
python_files = test_*.py test-*.py). The repo had long carried hyphen-named
tests (e.g. test-memory-retrieval-bugs.py, test-memory-consolidation-merge.py)
that the default test_*.py-only pattern silently skipped, so their
regression guards never ran under pytest tests/. They now do.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.
bun test --coverage --coverage-reporter=lcov, which writes lcov.info but pipes
NO text summary, so the ^All files line-% parse found nothing and the gate
failed every run (“could not parse line coverage”). Now emits BOTH text (for
the parse) and lcov (for the artifact), and strips ANSI before parsing.Gate green: pytest 1196 passed, bun typecheck clean, bun –coverage 1067 pass/0 fail.
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).
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.
loki start continues from the last PRD on rerun. First run with a
PRD/spec file persists it to .loki/generated-prd.md (source=user); first run with no
file generates one from codebase analysis (source=generated, unchanged). A later run
with NO file continues from the persisted PRD instead of re-analyzing. A later run WITH
a file makes that the new PRD (brownfield: the app exists, a new spec applies). This is
an intelligent default auto-decided from the file argument plus persisted state, not a
new flag. Identical semantics on both the bash route (autonomy/run.sh) and the Bun
route (loki-ts/src/runner/prd_reuse.ts). A user-provided PRD is always reused as-is
and never triggers the generated-PRD update path; --fresh-prd / LOKI_PRD_REGEN
re-runs analysis.loki docker start opens the dashboard on a host port. It now starts (or reuses)
the host dashboard and auto-opens the browser, gated like local loki start
(TTY, not background, LOKI_NO_AUTO_OPEN honored). The dashboard shows both local and
docker runs (the host dashboard already aggregates both; the container stays
dashboard-off to avoid a port collision).loki docker start prunes old images after pulling latest. After pulling
asklokesh/loki-mode:latest, it removes only dangling/old asklokesh/loki-mode images
that are not in use by a running container (never the just-pulled latest, never an
in-use image, never a non-loki-mode image, never a blanket docker image prune -a).
Default-on; opt out with LOKI_DOCKER_PRUNE=0. Honest output of what was reclaimed.loki stop reaps the docker container. Fixes the case where loki docker start
ran a container but loki stop reported “No active session running” while the
container kept running. The docker run is tracked in .loki/docker/run.json;
loki stop (folder-scoped) stops and removes this project’s loki-mode container,
with a deterministic-name fallback if the state file is missing. loki stop --all
reaps every loki-mode container on the machine.((x++)) post-increments aborted loki sandbox stop and
loki sandbox cleanup under set -e on the first iteration (leaving containers and
worktrees behind); replaced with x=$((x+1)). The desktop env-file and readonly state
copy that wrote secrets/state to predictable, never-deleted temp paths now use
mktemp + restricted perms + lifecycle cleanup.pg driver token substring-matched words like
“upgrade” in package.json, falsely blocking a clean spec until max iterations; driver
detection now matches dependency names with boundaries. Reworded “no issues” negatives
are skipped and non-N./- finding formats are parsed (with an unparsed-line count).LOKI_CHECKLIST_INTERVAL crashed the
sourced RARV loop (set -u unbound-variable in arithmetic); the guard now normalizes
any invalid value to the default.council.ts / voter_agents.ts): the reviewer subcall had no timeout,
so a hung reviewer wedged the loop forever despite a “we never hang” comment; added a
killable timeout (LOKI_COUNCIL_TIMEOUT_MS, default 10 min) with documented
fall-through, and stderr is no longer left undrained. A corrupt failed.json is now
treated as CANNOT_VALIDATE, not APPROVE.build_prompt.ts): a non-string queue task id threw and
discarded the entire prompt for an iteration (now coerced); the runner adapter passed
PRD content where a path was contracted (now passes the path); the gate-failures read
is capped at the first 8000 bytes on both routes (parity with bash head -c 8000).fcntl file lock per migration
serializes the read-modify-write. Advancing an already-advanced phase returns 409 not
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:
Lead theme: gates that silently pass when they should fail (the on-brand-critical class).
has_changes = "+" in patch or "-" in patch was always true (every diff has
+++/— headers), so the empty-patch QA gate could never fire. Now counts real
change lines only.Each fix ships with a non-vacuous regression test. Full pytest gate: 1176 passed.
Authorization: Basic ...,
lowercase auth headers, and Cookie:/Set-Cookie: leaked their credentials
in proofs and crash reports. A header-line rule now redacts the value (scheme
word preserved) without over-redacting normal lines.claude --version in-container.Each fix ships with a non-vacuous regression test. Full pytest gate: 1172 passed.
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.
[ -t 2 ], which returns 1 when stderr is not a TTY. Under
set -euo pipefail that non-zero return aborted the entire bash loki doctor
before the Summary line, on CI / headless / non-TTY runs only (it was masked
on an interactive Mac TTY). The GitHub Bun Parity workflow caught it. Fixed by
returning 0 from both helpers. TTY progress feedback is unchanged.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/consolidation.py):
when two newly-generated patterns merged into the same existing pattern in one
consolidation run, the second merge read a stale in-memory copy and the full
overwrite silently dropped the first merge’s conditions, source episodes, and
confidence bump. The in-memory list is now refreshed after each merge, so no
entry is lost. Regression-tested (proven non-vacuous: the test fails against
the old code with the exact data-loss symptom).memory/storage.py,
memory/retrieval.py): non-dict entries in a pattern list are now skipped
instead of raising AttributeError; a store missing the patterns key
degrades to empty instead of KeyError; re-saving a pattern stays idempotent
(no index double-count). Each failure mode has a regression fixture.benchmarks/swebench-pro-pilot/run_batch.py): the pilot harness wrote
per-instance results only under /tmp and rewrote a single monolithic ledger
every iteration, so a reboot or a crash mid-write could lose every completed
instance. Results now write incrementally, one atomic file per instance
(temp + fsync + rename), to a durable repo-relative ledger under
benchmarks/results/swebench-pro-pilot/ (git-ignored data, tracked
.gitignore, so it survives reboots but stays internal). Re-runs skip
already-completed instances; --force/--rerun re-runs from scratch. Infra
failures and budget placeholders are not frozen as “done”, so they stay
re-runnable. Cleanup globs (/tmp, test-branch pruning) structurally cannot
reach the durable results.dashboard/server.py):
several FastAPI endpoints did blocking filesystem work (event-log reads,
learning-signal scans, memory blobs, cost/timeline aggregation, council
transcripts, checkpoint listing, log tails) directly on the asyncio event
loop. A single slow read could freeze every other in-flight request, so the
status poller and live panels could hang while one endpoint did heavy I/O.
Those blocking call sites now run via asyncio.to_thread, keeping the event
loop responsive. Verified: /api/status stayed sub-millisecond while a
deliberately slow endpoint blocked for seconds; all endpoints still return
threading.Lock-guarded (read-modify-write bracketed); the wrapped
helpers are pure reads, and no shared-state writer was newly threaded.$<digit> footgun
(tests/check-heredoc-dollar-digit.sh, wired into scripts/local-ci.sh):
scans autonomy/loki and autonomy/run.sh for unescaped $1..$9 inside
python3 -c "..." bodies, which set -u bash silently expands as positional
params (the class of bug that once crashed loki plan --json for every user).
Non-vacuity proven (the check fires on a planted violation). Runs on every
local-ci pre-push.loki doctor now prints the exact install command for a missing provider
(autonomy/loki): a missing optional provider CLI (claude/codex/cline/aider)
previously printed only “not found” with no remedy; it now prints the precise
install command under the warning. Bounded network probes (ChromaDB, MiroFish)
gained --connect-timeout/--max-time so a doctor run can no longer hang on a
dead endpoint, with transient stderr-only progress feedback (stdout stays
byte-identical for bash/Bun parity).tests/test-healing-hooks-safety.sh): locks in that a healing revert
preserves unrelated uncommitted edits (snapshot-scoped restore, no blanket
git checkout) and that friction path matching is exact (no substring false
hits). The source already had these guards; this test prevents regression and
proves non-vacuity against the old destructive behavior.tests/test-e2e-features.sh, tests/test-stop-process-group.sh): added
unconditional trap ... EXIT INT TERM cleanup so an interrupted test can no
longer leave a loki-test-wt-* worktree/branch in the real repo or a
loki-pgtest-* temp dir behind. New scripts/clean-test-branches.sh safely
prunes only test-artifact branches from a sandbox repo (refuses the real repo,
protected branches, or while a live run is active).dashboard/server.py, dashboard/static/index.html via
dashboard-ui/): previously the panel showed “Not Started / No app yet” even
when a full docker-compose stack was running (e.g. web on :3000 + api on :8000),
because the app-runner only recorded state for apps IT launched. The dashboard
now discovers a running compose stack for the active project (bounded, fail-open
docker probes; ~2.5s cache; offloaded off the event loop), identifies the
primary web service, and surfaces a clickable URL + a live-preview iframe with
honest status (running/starting). Never fabricates a URL for a non-running
container. autonomy/app-runner.sh also persists url/primary_service/runtime
port for its own compose launches so both paths look consistent.autonomy/run.sh, autonomy/lib/prd-enrich.sh):
PRD-derived tasks no longer show placeholder junk (description == title,
“…so that the product delivers its core value” boilerplate). A deterministic
fix routes the real spec section body into the description, and an LLM
post-pass synthesizes a real description, acceptance criteria, and user story
grounded in the spec. Degrades gracefully to the deterministic output when the
provider is unavailable; never blocks queue population, never fabricates.*_BLOCK flags. A clean run
never newly blocks (deny-filtered); coverage measurement stays opt-in.--sandbox workspace-write, the deprecated --full-auto replacement).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).
autonomy/loki): the manifest tamper-evidence
helpers (v7.55 linkManifest/verifyManifestLink) and the compliance snapshot
scheduler (v7.54 maybeGenerateSnapshot) shipped as tested-but-not-invoked.
They are now reachable on demand: loki audit link-manifest [dir] hashes the
run manifest into the audit chain, loki audit verify-manifest [dir] checks it
(detects tamper), and loki compliance snapshot [dir] [--force] generates a
compliance snapshot (respecting the default-disabled interval unless –force).
All are explicit on-demand commands (never gate or slow a run); honest no-op
when the manifest is absent.loki-ts/src/runner/build_prompt.ts):
the invariant gate wrote .loki/quality/invariant-findings.txt but nothing read
it into the next-iteration prompt (the actionable per-finding text was not
surfaced). build_prompt.ts now reads and injects it, mirroring the semantic-
findings consumer. Closes the writer-with-no-reader on the prompt side.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).
tests/detect-invariant-violations.sh
(property/metamorphic checks: idempotency, round-trip, no-secrets-in-output)
existed but was never invoked (inert). It is now an opt-in gate via
LOKI_GATE_INVARIANTS (default off), wired on the bash route
(autonomy/run.sh enforce_invariant_integrity) and the Bun route
(loki-ts/src/runner/quality_gates.ts runInvariants) with byte-identical
semantics: CRITICAL/HIGH (detector exit 1 under –strict) blocks; clean,
timeout, absent, or any other result never fires (deny-filtered); default-off
means the detector is not invoked.src/audit): linkManifest /
verifyManifestLink hash the run manifest (loki-run.json) and record it into
the unified audit chain so the build bill-of-materials becomes tamper-evident
and verifiable against the evidence chain. Honest no-op when the manifest is
absent; verification fails if the manifest is tampered after recording. Ships
as a tested API; not yet auto-invoked (documented).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.
loki-ts/src/runner/autonomous.ts): the Bun
runner carried logStub/noopCouncil/stubProvider fallbacks for modules that all
exist and conform. These were unreachable dead code that silently degraded to
wrong results if ever hit. Replaced with a fail-fast requireModule() (a
missing load-bearing module now errors loudly at resolution time instead of
producing a wrong build), while genuine runtime-error fallbacks are preserved.loki-ts/src/runner/build_prompt.ts):
the Bun semantic-test gate persisted .loki/quality/semantic-findings.txt but
nothing on the Bun route read it (a writer-with-no-reader). build_prompt.ts now
reads and injects those severity-tagged findings into the next-iteration prompt,
independent of gate-failures.txt, byte-parity with the bash route. Absent/empty
injects nothing.autonomy/run.sh check_policy): ${2:-{}}
brace-eating expansion turned a non-empty JSON context into invalid JSON
({"a":1}}), so the policy engine received garbage and returned DENY every
iteration when a .loki/policies.json/.yaml existed (and made the P3-3
approval-wait unreachable on the live path). Fixed to a split default yielding
valid {}; non-empty contexts now pass through unchanged. Users with no policy
file are unaffected.src/audit/compliance-scheduler.js):
a default-disabled helper that periodically persists a compliance snapshot from
the real audit chain (honest empty-state, never a fabricated verdict). Ships as
a tested helper; not yet auto-invoked (documented).tests/test-no-deprecated-codex-flag.sh (fails if any live codex exec
--full-auto is reintroduced), plus coverage-artifact, evidence-gate-consumer,
approval-phase-gate, and semantic-gate-bash-route regression tests.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).
autonomy/prd-checklist.sh):
small checklists (2 <= N < 4) previously reserved ZERO held-out items, leaving
the entire checklist gameable. They now reserve exactly 1 held-out item via the
same deterministic sha256 ranking used for larger sets. N=1 stays a no-op
(cannot hold out from a single-item checklist); N>=4 is unchanged. Idempotent.tests/detect-semantic-test-problems.sh, which catches fake tests
that regex detectors miss) is now an opt-in quality gate via
LOKI_GATE_SEMANTIC_TESTS (default off). Wired on the bash route
(autonomy/run.sh) and mirrored on the Bun route
(loki-ts/src/runner/quality_gates.ts) with byte-identical blocking semantics:
blocks only on CRITICAL/HIGH findings; clean/absent/timeout/malformed never
fires (deny-filtered); default-off means the detector is not invoked (zero
cost, cannot deadlock the loop). Documented as the opt-in 9th gate in
skills/quality-gates.md.LOKI_SANDBOX_MODE=true); the
loki start CLI help now accurately states “default: off; requires Docker”
instead of an ambiguous description.GET /api/compliance dashboard
endpoint returns the live SOC2/ISO27001/GDPR compliance report generated from
the real audit chain (via src/audit/index.js getReport + a CLI shim),
including the real verifyChain() tamper-evidence verdict. Auth/tenant-scoped
(require_scope("audit")); returns an honest empty report (never a fabricated
“compliant” verdict) when no audit data exists, and degrades to
available:false if the audit engine is unavailable.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.
--full-auto -> --sandbox workspace-write (repo-wide): Codex CLI
v0.125+ deprecated --full-auto (removed from codex exec --help, emits a
deprecation warning; will hard-error in a future release). Every live
codex exec --full-auto invocation across the codebase is now
codex exec --sandbox workspace-write (the documented replacement;
codex exec is non-interactive by default so the loop stays autonomous).
--skip-git-repo-check is preserved where it already applied. Swept across
both routes and all helpers: autonomy/run.sh, autonomy/loki,
providers/codex.sh, loki-ts/src/runner/providers.ts,
autonomy/completion-council.sh, autonomy/grill.sh, magic/core/debate.py,
magic/core/generator.py, autonomy/lib/wiki_llm.py, plus all user-facing
docs. The tier path and Bun route (which previously used the broader
danger-full-access) now converge on workspace-write, matching what
--full-auto always expanded to. Verified empirically against codex 0.132.0
(new flag: no deprecation warning, runs at approval: never; old flag: warns).autonomy/lib/claude-flags.sh): the
remaining-budget computation interpolated a file path into a python3 -c
program, so a path containing a quote raised a SyntaxError. The path and
numeric values now pass via os.environ (single-quoted program), matching the
established run.sh convention. Behavior-preserving; closes the injection
footgun.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).
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.
autonomy/run.sh): at the default (measurement
off) the coverage block was skipped entirely, so no coverage.json was ever
written – a missing-artifact gap in the reproducibility manifest. The gate now
writes coverage.json with measured:false even when measurement is off, with
zero added runtime (no instrumented re-run) and no blocking. Measurement stays
opt-in (LOKI_COVERAGE_GATE=1); default-on would double every test run..loki/quality/lsp-diagnostics.json but nothing wrote it (inert). A
route-neutral writer (mcp/lsp_proxy.py --write-diagnostics) is now invoked
identically from the Bun gate (loki-ts/src/runner/quality_gates.ts) and the
bash gate (autonomy/run.sh), with byte-identical blocking semantics: single
knob LOKI_GATE_LSP_DIAGNOSTICS (default off); count_errors > 0 blocks only
when enabled; absent/malformed/clean never fires; no language server present ->
writes nothing (never fabricates a clean verdict). LOKI_GATE_LSP_WRITER=0
lets a caller supply a pre-built artifact. The bash/Bun parity test now
validates these as genuinely shared toggles (prior bun-only carve-out removed).autonomy/run.sh, closes P1-1): the
completion council wrote .loki/council/evidence-gate-details.json but run.sh
had no consumer. run.sh now surfaces the details advisory (WARN on block, INFO
otherwise) without introducing a new block; the wrapper preserves the gate’s
exact return code, so completion detection is unchanged. Absent/malformed file
degrades silently.autonomy/run.sh): a policy
“approval required” result (exit 2) previously logged and proceeded. It now
honors an approval wait on .loki/signals/POLICY_APPROVED|POLICY_REJECTED
(same mechanism as staged autonomy) when LOKI_STAGED_AUTONOMY=true or
LOKI_POLICY_APPROVAL_ENFORCE=1. With neither knob set, behavior is unchanged
(advisory log + proceed).Gates: local-ci 78/78, full pytest + bun test + bash/Bun parity matrix green, 3-reviewer council unanimous APPROVE (2 Opus adversarial + 1 Sonnet).
All default-on or zero-config-intelligent; opt-out knobs only. Each stream built in an isolated worktree, council-reviewed.
LOKI_CHECKLIST_ORACLE=0..loki/prd-observations.md.loki spec lock),
real drift emits High and blocks loki verify instead of a soft concern.loki verify secret scan hardened with a
documented high-confidence pattern set (AWS, private keys, GitHub/Slack/cloud
tokens) that blocks, with low false positives (placeholders and env refs
ignored). Also fixed a latent bug where PEM private-key blocks were never caught.loki status now renders the budget/context
gauges identically on the Bun route; parity test extended..loki/council/evidence-gate-details.json for audit. Opt out:
LOKI_EVIDENCE_NO_TESTS_AFFIRMATIVE=1.LOKI_GATE_LSP_DIAGNOSTICS gate on the Bun route blocks on language-server
errors in changed files (pass-through when no LSP server is available; a
diagnostics writer is a tracked follow-up).Each stream built in an isolated worktree, council-reviewed, local-ci green.
# shellcheck disable=SC2120 with rationale (the [path]
argument is optional by design). Tests workflow is green again.LOKI_TELEMETRY=on or loki telemetry on).
A default install, including air-gapped / GDPR / FedRAMP environments, sends us
nothing. All prior opt-out flags still work and always win. Documented the exact
(PII-free) data inventory in docs/PRIVACY.md.tests/detect-semantic-test-problems.sh catches fake tests that the
existing mock/mutation detectors miss: assertions that echo a literal through a
variable, assertions on a mock’s own return value, and net-deleted assertions
across commits. Conservative by design (never flags a genuine computed
assertion like expect(add(2,2)).toBe(4)). Available now; wiring it as a
blocking gate is a tracked follow-up..loki/quality/coverage.json. Measurement is opt-in (it re-runs the suite
instrumented, so it is off by default to keep autonomous iterations fast): set
LOKI_COVERAGE_GATE=1 to measure-and-record (warn if below LOKI_MIN_COVERAGE,
default 80), or LOKI_ENFORCE_COVERAGE=1 to also block a below-threshold-and-
measurable result. When no coverage tool is present the gate passes through and
records “not measured” (never fabricates a number)..loki/loki-run.json, a bill-of-materials (spec path+hash,
model tier used, provider, loki + tool versions, evidence hashes, git SHAs,
outcome) so a run is auditable and reproducible. Best-effort, never aborts a run.Each stream built in an isolated worktree, council-reviewed, local-ci green.
validate_oidc_token hardcoded
"scopes": ["*"] for all OIDC-authenticated users, defeating the entire RBAC
system. Now OIDC role/group claims are mapped to the existing roles
(admin/operator/viewer/auditor) with the scope hierarchy. When no recognized
role claim is present, the user defaults to least-privilege viewer, never
admin. Recognized claim sources: a configurable LOKI_OIDC_ROLES_CLAIM
(supports dotted paths), roles, groups, Keycloak realm_access.roles, and
Cognito cognito:groups. Default role configurable via LOKI_OIDC_DEFAULT_ROLE
(defaults to viewer). API-token auth is unchanged.loki grill) now runs by
default in the DISCOVERY phase before the first iteration, classifying findings
as ambiguous / contradictory / underspecified / missing. Opt out with
LOKI_SPEC_GRILL=0..loki/assumptions/ (the gap, the assumption, why,
severity, what it affects). A new council_assumption_ledger_gate surfaces them
in the proof-of-done so “done” means “done, plus here are the places your spec
was ambiguous and what I assumed.” Set LOKI_ASSUMPTIONS_REQUIRE_CONFIRM=1 to
make high-severity unconfirmed assumptions block completion (human-in-the-loop);
default mode records and surfaces without blocking autonomous runs. Opt out with
LOKI_ASSUMPTION_GATE=0. Degrades cleanly when no provider CLI is present (no
fabricated questions).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).
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.
tests/detect-mock-problems.sh and tests/detect-test-mutations.sh existed but
were never invoked by the build loop. They now run every iteration: HIGH-severity
findings block; Medium/Low are advisory and injected into the next iteration.
Opt out with LOKI_GATE_MOCK=0 / LOKI_GATE_MUTATION=0. Both detectors honor
LOKI_SCAN_DIR so they scan the target project, not the Loki install.LOKI_GATE_DEVILS_ADVOCATE=0.min_coverage JSON field is
retained as a target. Real coverage measurement is a planned follow-up.loki-ts/src/runner/quality_gates.ts.tests/test-p0-verification-sweep.sh,
tests/test-p0-gate-behavior.sh, wired into local-ci) so gate-count and
severity-blocking claims cannot silently drift again.Reviewed by a 3-of-3 unanimous completion council. No runtime behavior changes beyond the gates now actually blocking on the conditions documented.
dashboard/registry.py _save_registry
now writes atomically (temp file in the registry dir + os.replace) so a reader
never sees a torn file, and the leaf mutators (register_project,
unregister_project, update_last_accessed, mark_project_stopped) take a
best-effort advisory file lock (graceful fallback when fcntl is unavailable).
Matters most now that multi-repo loki docker makes concurrent registration the
common path.loki docker project is
honored at the next iteration boundary (writes .loki/STOP, the runner polls
it), so Stop is reliable but not instant for containerized builds;
docker stop loki-<hash> stops immediately.loki docker start
against the published asklokesh/loki-mode:7.45.0 built a working app and
committed as the correct GitHub identity (asklokesh), confirming the v7.45.0
Docker fixes work in the real artifact.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.
asklokesh/loki-mode image
shipped the Loki runtime (bun/node/python) but NO AI provider CLI, so
docker run ... start died at the first provider call with “No AI provider CLI
found; cannot prompt to install in a non-interactive shell.” The Dockerfile now
installs @anthropic-ai/claude-code (the Tier-1 default provider). Verified:
claude --version (2.1.177) runs in the image and a claude -p call succeeds
with valid auth..env defeated gitignore. An empty .env had been
committed since v6.36.2, so .gitignore could not protect a user’s real .env.
Untracked it (git rm --cached .env) and added .env, .env.*, and
.loki-oauth-credentials.json to .gitignore (with !.env.example kept
tracked).--rm
container the orchestrator is always PID 1, so a prior run recorded
loki.pid=1. On the next loki docker start in that folder the stale-PID
lock check ran kill -0 1, which always succeeds (PID 1 is the live init
process), so the lock falsely read “another loki instance is running” and
refused to resume. Fixed: a recorded pid of 1 from a prior container is now
treated as stale unless it is the live container’s own pid ($$ == 1), so
re-running loki docker start in the same folder resumes correctly with full
memory and session continuity.loki docker <any-loki-command> zero-friction host wrapper. Runs loki inside
the published image with the same experience as the local CLI. It bind-mounts
the current folder to /workspace so .loki/ state (memory, session, queue,
checkpoints) persists on the host and resume + continuity work exactly like
local. Auth is auto-detected: ANTHROPIC_API_KEY if set, else the host’s
existing Claude Code login (macOS keychain or ~/.claude/.credentials.json) is
extracted to a private 0600 temp file and mounted read-write (so the
in-container claude can refresh the short-lived token and it persists), else an
honest error with setup guidance. Forwards ~/.gitconfig, ~/.config/gh, and
GITHUB_TOKEN/GH_TOKEN. Flags: --image, --dry-run. New module
autonomy/docker-run.sh with 24 hermetic unit tests (tests/test-docker-run.sh).loki docker. You can now run
loki docker start in several different repos at once, exactly like the host
CLI supports many repos. Each repo gets its own container (deterministic name
loki-<sha256-prefix-of-path>), its own bind-mounted /workspace, and its own
.loki/ state on the host, so two repos run as two concurrent containers
without colliding. Each loki docker project also registers on the HOST with
its real folder path and no pid into the same machine-global registry
(~/.loki/dashboard/projects.json) the host dashboard reads, so a single host
loki dashboard lists ALL your projects whether they run via local
loki start or via loki docker start (the dashboard derives “running” from
each project’s bind-mounted .loki/session.json). Docker is now a first-class
multi-repo surface with one unified view, matching the local CLI experience.
Builds run with the dashboard off by default (so concurrent runs never fight
over port 57374); use loki start --api or the host loki dashboard for the
UI.docker-compose.yml now has env_file: .env, an
ANTHROPIC_API_KEY passthrough, and a commented host-OAuth-mount volume. New
.env.example template. Users copy .env.example to .env, set their key, run
docker compose run loki start prd.md, and edit/re-run without retyping flags.loki docker
wrapper across DOCKER_README.md, README.md, docs/INSTALLATION.md, and SKILL.md.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.
loki web, web-app/, port 57375) is DEPRECATED as of this release.
Rationale: it was a second web surface (its own FastAPI server, auth, crypto,
DB migrations) whose monitoring half duplicated the dashboard, whose advertised
multi-user/account layer was unbacked UI, and whose hosted-platform role now
lives in the separate Autonomi Cloud product. Consolidating to one local
surface simplifies the UX and sharpens the open-core line (free CLI + local
dashboard; paid hosted = Autonomi Cloud).loki web still works (value-preserving): it prints a deprecation banner
pointing to the dashboard (auto-launches at loki start, http://localhost:57374,
or loki dashboard) and to Autonomi Cloud for the hosted platform, emits a
cli_command_deprecated telemetry event, then still launches Purple Lab so no
existing user is stranded (mirrors the loki run -> loki start deprecation).POST /api/control/start (scope: control) accepts a PRD as
text or path (path-traversal guarded, post-resolution containment check),
single-flight (409 if a run is already active), and spawns the run via run.sh.
The SPA gains a spec textarea + a wired Start button (startSession in the API
client). The dashboard is now the single local surface: monitor AND launch.--serial / LOCAL_CI_SERIAL=1 is the bisect lever.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).
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.
Defensive hardening from the bug-hunt fleet’s lower-severity findings. Each fix has a regression test proven non-vacuous.
loki memory index. The aggregates now increment
only when the episode id is genuinely new to the topic.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.
applyMaxTierCeiling normalizes LOKI_MAX_TIER (lowercase + trim, empty
means no ceiling) before comparing, matching claude.sh:352. A user-typed
LOKI_MAX_TIER="Sonnet" / " haiku " is now honored instead of silently
blown past. The sonnet ceiling now also caps a fable session tier down to
development (claude.sh:358-362), not only planning.claudeTierToModel reads LOKI_ALLOW_HAIKU with exact === "true",
matching bash (claude.sh:294, claude-flags.sh:104) and Bun’s own
fallbackForPrimary. LOKI_ALLOW_HAIKU=1 no longer dispatches haiku on one
path while bash and the rest of the Bun route resolve sonnet.cavemanActivateEnv branches on whether LOKI_CAVEMAN_LEVEL is
SET (not merely truthy), so an exported-empty LOKI_CAVEMAN_LEVEL="" resolves
to the full override like bash (claude-flags.sh:543/709) instead of inferring
lite. The wenyan-* compression ranks now mirror bash exactly so a user’s
global CAVEMAN_DEFAULT_MODE=wenyan-lite is honored (no-raise), not raised to
full.applyCodexMaxTier is intentionally left raw (no normalization): codex.sh
also compares LOKI_MAX_TIER raw, so normalizing only the Bun codex side
would create drift. Locked with a guard test.getProviderTierParam (rarv.ts) is unchanged: it mirrors bash’s legacy
fallback table, which applies no max-tier clamp; the clamp lives in
applyMaxTierCeiling on the dispatch path.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).
code_review (BLOCKED) and code_review_ESCALATED but not
code_review_PAUSED, so an iteration that hit the PAUSE limit AND claimed
completion could return “fulfilled”, bypassing the human-intervention PAUSE.tail-truncates, so a reviewer that lists many issues can no
longer push its own VOTE: line out of the window and be silently counted as
REJECT; (b) the matcher is word-bounded and markdown-tolerant, so VOTE:
APPROVED / VOTE: APPROVE_WITH_CONCERNS no longer count as a clean APPROVE and
**VOTE:** APPROVE now parses; ambiguous verdicts resolve conservatively.port(s): N parser./api/wiki/ask (up to 180s) and
/api/checkpoints (git + copytree) now run via asyncio.to_thread, so one
request can no longer stall liveness/status/WebSocket for every other client.
The checkpoint git-sha is now read with the correct project cwd.loki monitor --help (and loki assets --help) now print help and exit 0
even when Docker / python3 is unavailable, instead of erroring on a
precondition before reaching the help arm.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.
tests/test-completion-summary.sh now accepts the v7.41.1
pathspec-filtered review_cmd (git diff <sha>..HEAD -- . ':(exclude).loki/');
the glob previously required the command to end at ..HEAD._loki_caveman_infer_level in autonomy/lib/claude-flags.sh is
annotated # shellcheck disable=SC2120 (its $1 is intentionally optional with
a global fallback). Older shellcheck on CI flagged SC2120 as a warning and
failed the lint gate; newer shellcheck does not.tests/test-plan-json-smoke.sh: a fast direct regression guard asserting
loki plan --json exits 0, emits valid JSON with a populated
cost.iterations_by_model, and never prints “unbound variable” (the v7.41.1
heredoc $5-unbound regression). Wired into tests/run-all-tests.sh.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.
loki
stop path, so the orchestrator and its agent terminate cleanly instead of
lingering. Foreign loki runs (different pgid / .loki) are structurally
unreachable and untouched.export CAVEMAN_DEFAULT_MODE=off in
autonomy/lib/claude-flags.sh (the one module every Loki process tree sources)
makes EVERY claude subcall inherit suppression, so any parsed trust-gate call –
existing or future, in any tree (run.sh, completion-council, council-v2,
voter-agents, grill) – is suppressed unless a site explicitly opts into
compression. Activation (the compression level) is then set per-invocation ONLY
at the known free-form generation sites (main RARV loop, parallel dev stream).
This fails safe: a missed activation site only loses token savings, never
corrupts a verdict. A tree-wide audit test asserts no parsed claude subcall is
left unsuppressed.
LOKI_CAVEMAN=0. Level via LOKI_CAVEMAN_LEVEL
(default full; also lite, ultra, wenyan*). Version pin via
LOKI_CAVEMAN_VERSION (default 1.9.0); vendor-less – Loki ships no copy of
caveman and bootstraps the pinned version on demand (idempotent, cached
marker under .loki/, opt out with LOKI_CAVEMAN_AUTO_BOOTSTRAP=0).LOKI_LEGACY_COMPLETION_MATCH=true), main-loop activation is disabled so
compression can never mangle the prose completion-promise grep. The default
completion path (the loki_complete_task MCP tool / completion signal file)
is immune to compression and keeps caveman on.ultracode trigger) for more thorough first-run analysis, and
otherwise keeps the cheaper three-pass analysis. The decision is made once per
run (not always-on) and is parity-locked across both routes
(autonomy/run.sh and loki-ts/src/runner/build_prompt.ts), evaluated in the same
order: non-Claude provider or degraded provider -> three-pass;
LOKI_USE_CLAUDE_WORKFLOWS=0 -> three-pass (escape hatch);
LOKI_USE_CLAUDE_WORKFLOWS=1 -> workflow (force on); variable unset ->
workflow only when complexity == complex. Simple and standard repos keep the
byte-identical default behavior. When the workflow path is taken autonomously,
a one-time stderr disclosure names the higher cost class (no fabricated dollar
figure; there is no price API) and the LOKI_USE_CLAUDE_WORKFLOWS=0 opt-out.detect_complexity() was defined but never called on the bash route, so
DETECTED_COMPLEXITY stayed empty and every complexity consumer
(effort-for-tier, telemetry, phase selection, and the new analysis gate)
silently fell back to “standard”. It now runs once per run before the first
prompt build, so the complexity signal is live on bash, matching the Bun route.bun install -g loki-mode) with npm as a fully supported alternative,
matching the README and clarifying that the core autonomous engine runs the
same on both routes (you lose no capability by choosing Bun).loki plan
cost quote, and the dashboard effective-model. A fable pin now both runs AND
quotes opus ($5/$25), not fable ($10/$50), so the quote matches the dispatch
(cost honesty). The fable tier label, session-pin parsing (fable stays a valid
input alias), and pricing-table rows are preserved; only the resolved model
changes. The 224-cell session-pin parity matrix stays green (dispatch == quote
== dashboard for every fable cell)./plugin marketplace add asklokesh/loki-mode then
/plugin install loki-mode@loki-mode. The plugin bundles the loki slash
commands, wires the MCP server via loki mcp --transport stdio (PATH-resolved,
install-location-independent, requires the loki CLI on PATH), and includes an
opt-in Bash safety guard hook gated on LOKI_GUARD=1 (default OFF). License is
BUSL-1.1 (source-available), declared as “SEE LICENSE IN LICENSE”.loki --help trimmed: grill, spec, and cleanup moved to the
collapsed “More commands” footer to keep the front page lean. All three remain
fully dispatchable and documented via loki <command> --help.loki ultracode "<task>": a passthrough that prepends the ultracode
keyword so a native Claude Code Dynamic Workflow (background multi-agent
fan-out) runs for the task. Loki adds no orchestration of its own. Opt-in
(explicit command), capability-gated (Claude provider AND claude CLI
= 2.1.154 AND workflows not disabled), with a cost-class disclosure printed every time (workflows spawn many agents and cost meaningfully more; no dollar figure is shown because there is no price API). Non-interactive without
--yesrefuses with exit 2 and zero invocation. On Codex/Cline/Aider or an older/disabled CLI it prints an honest message and exits cleanly.
LOKI_USE_CLAUDE_WORKFLOWS=1 (default OFF): when set on the Claude provider,
the first-run READ-ONLY codebase-analysis pass (the no-PRD reverse-engineer
step) is dispatched as a workflow fan-out. Default behavior is byte-identical
when the flag is off or the provider is not Claude (deterministic fallback to
the existing three-pass analysis). Read-only; never gates “done”.tests/test-cli-session-v734.sh tripped the shellcheck gate (SC2164: a
cd without an || exit guard at line 516), failing the Tests workflow on
v7.37.0. Added the guard. Test-only change; no runtime behavior change. Root
cause: shellcheck was not installed locally when v7.37.0 was cut, so the
warning was not caught pre-push.LOKI_RESUME_SESSION=1, default OFF. When a previously interrupted run
(paused, rate-limited, or budget-cutoff) is restarted, the FIRST main-loop
claude call emits --resume <stored-session-uuid> to reattach the prior
Claude session context, then the run reverts to normal stateless
per-iteration behavior (no resume chain, so transcript context cannot
accumulate). LOKI_SESSION_FORK=1 optionally adds --fork-session on the
resumed call. Gated on CLI support; degrades to a fresh call on an older
claude or a missing/malformed stored session file. Default argv is
byte-identical to v7.36; the resumed call never co-emits --session-id and
--resume together.
LOKI_RESUME_SESSION governs the underlying CLAUDE session-resume
layer. It is UNRELATED to the existing loki heal/loki migrate --resume
CHECKPOINT flag, which resumes a Loki run from its own saved checkpoint.
This is a narrow recovery feature, not whole-loop session continuity (a
--resume chain over every iteration was deliberately rejected because it
would compete with Loki’s curated injected memory and grow context
unboundedly).resumeFirstCall invocation field for parity, but
the production autonomous loop runs the bash route (autonomy/run.sh), which
is the fully-wired path; the Bun path is staged for when its main loop
becomes the live route.loki review --ultra (issue #168): an explicit, opt-in, on-demand cloud
multi-agent code review that wraps the upstream claude ultrareview
subcommand. Paid cloud operation, billed by Anthropic, separate from local
model spend. Strictly opt-in: the disclosure prints every time, an interactive
TTY prompts with default NO, and a non-interactive shell without --yes (or
LOKI_ULTRAREVIEW=1) refuses with exit 2 and makes zero cloud calls. Findings
are advisory and do not block the completion gate. Capability-gated: if the
installed claude lacks ultrareview it prints an honest upgrade note and
exits. No dollar amount is shown (no price API exists); the disclosure states
the cost class only.loki plan now states the trust moat (issue #166): a short, honest block
(“Why this estimate is trustworthy”) naming verified completion (the
completion council blocks “done” without a real diff and green tests) and cost
honesty (the quoted model is the model the run dispatches and the dashboard
reports). Mirrored as a structured moat field in loki plan --json.--allowedTools positive allowlist for reviewer/adversarial/council subcalls
(issue #167), opt-in via LOKI_REVIEW_ALLOWLIST=1, default OFF and
byte-identical default argv on both routes. Complements the v7.33.0
--disallowedTools denylist with a least-privilege read/inspect allowlist
(Read, Grep, Glob, read-only git and shell). Deny-precedence verified against
the live CLI and the Claude Code permissions docs, so allowlist and denylist
ship together. Gated on CLI support.LOKI_PRD_SIG_CONTENT_MAXFILES, default 20000) now
use a sampled head+tail content hash (files-sampled:) instead of the old
content-blind files-shallow: listing, so most same-size edits on large
non-git trees are detected and a stale generated PRD is no longer silently
reused. Batched (xargs -0 -n 64) to avoid per-file fork cost. A stored
pre-upgrade files-shallow: signature with a matching listing + count still
decides reuse on the first post-upgrade run. Git projects unaffected. Residual
honest gap (a same-size edit confined to the middle of a file larger than 8KB)
documented in the signature docblock.doctor output,
matching the normalization already in scripts/local-ci.sh. This stops
env-only false positives while still surfacing real route-logic divergence.#section=<id> and
restored on load (hash -> localStorage -> overview), so a project switch
(which reloads the page) and a browser refresh both land on the SAME tab.
Added a hashchange listener for back/forward + shared links; stale or
invalid hashes fall back safely and can never blank the page.loki api start --help (and -h) now short-circuit to the help text
instead of being swallowed by the --host/--port parse loop and starting
the dashboard server. loki api --help already worked; this covers the
flag-after-subcommand case.cacheSeconds=86400 so shields.io serves a
cached count instead of leaking its rate-limit error (“UNABLE TO SELECT NEXT
GITHUB TOKEN FROM POOL”) into the badge when its shared GitHub token pool is
exhausted..loki/state/claude-session.json and surfaces it as claude_session_id on
the dashboard /api/status, so a run can be correlated with its Claude
session. Opt-in LOKI_SESSION_STAMP=1 additionally passes a distinct
per-iteration --session-id to the main-loop Claude call (never to subcalls;
never a pinned id across the run, so context is not accumulated). Default OFF
keeps the Claude argv byte-identical to v7.33.0. Bash and Bun routes derive
the same UUID byte-for-byte. Gated on CLI support.LOKI_NO_SESSION_PERSIST=1 (opt-in, default OFF): passes
--no-session-persistence so a run leaves no Claude session state on disk
(useful for ephemeral/CI runs). Gated on CLI support.claude_session_id documented in wiki/API-Reference.md.loki config help no longer advertises a nonexistent
config provider subcommand; cold loki status tips now point at the
canonical loki analyze context show / loki analyze code overview
(instead of deprecated aliases that emitted a deprecation note); README.md
and docs/INSTALLATION.md use the canonical command names
(loki modernize heal, loki analyze onboard, loki preview,
loki api start) with “was:” annotations.claude degrades to no-op):
--strict-mcp-config (LOKI_STRICT_MCP=0 to disable): added wherever
Loki already passes an explicit --mcp-config bundle, so a run loads MCP
servers only from that curated bundle (which includes your
~/.claude/mcp.json overlay) and ignores all other MCP sources, keeping
runs reproducible.--bare (LOKI_BARE_SUBCALLS=0 to disable): added to cheap,
self-contained internal subcalls (USAGE generation, conflict resolution,
bash-route reviewer/adversarial/council votes, the grill) for a faster,
cheaper, cache-friendlier call. Never applied to the main RARV loop.
Auth-gated: --bare reads Anthropic auth strictly from
ANTHROPIC_API_KEY/apiKeyHelper, so Loki only enables it when one is
present; subscription/OAuth logins run full-auth unchanged.--disallowedTools (LOKI_REVIEW_TOOL_GUARD=0 to disable): added to
reviewer/adversarial/council subcalls denying Edit/Write/NotebookEdit and
the git mutation forms (commit/reset/push/checkout/clean/rm/stash,
including the git -C/--git-dir/-c flag-prefixed forms), so a review
agent does not casually mutate the working tree. Read-only git stays
allowed. A guardrail, not a sandbox.wiki/Environment-Variables.md.LOKI_PRD_SIG_CONTENT_BUDGET bytes (default 50MB, documented in
wiki/Environment-Variables.md) fall back to the previous fast listing
(files-shallow:), where the limitation is documented honestly.--fresh-prd lever; a hand-edited generated PRD is detected
(user_owned) and used as-is with its own disclosure, never overwritten or
re-baselined; the incremental-update path also discloses --fresh-prd;
reuse runs make zero re-analysis provider calls (proven end-to-end by a
stub suite: byte-identical PRD on run 2).--fresh-prd (the flag named in the runtime disclosure) is now documented
in loki start --help, wiki/CLI-Reference.md, and
wiki/Environment-Variables.md alongside its aliases (--regen-prd,
--regenerate-prd, --regen, LOKI_PRD_REGEN=1).loki report kpis is the canonical KPI command
(old loki kpis kept as a working alias); loki memory compound,
loki analyze (explain/onboard/code/context), and loki modernize
(heal/migrate) nouns group their legacy commands under the alias contract:
byte-identical stdout, exit parity, exactly one stderr deprecation pointer,
suppressed under --json/-q/--quiet and positional machine formats
(json/csv/timeline), zero side effects in clean directories.git archive tarball, uploads it to
the GitHub Release, and points the formula at it (sha256 of the same
bytes), so brew installs appear in release download counts.tests/test_dashboard_port_arg.py: coverage for the v7.31 --port/--host
argparse on direct python -m dashboard.server launches.sonnet
session pin routes through the development tier to Opus; the quote said
Sonnet and underquoted ~1.7x). loki plan output now names the model AND
the lever that selected it. Demo cost quote corrected to the Opus truth.planning/development/fast), documented in
skills/model-selection.md, now resolve byte-faithfully across all three
readers (run.sh dispatch, loki plan estimator, dashboard). run.sh folds
miscased/padded pins (trim + lowercase). Session-pin parity matrix grown
to 224 cells; dashboard endpoint tests 37/37.loki report export json kpis was hijacked by a flag-anywhere kpis scan;
kpis routing now keys on the first non-flag token in all three layers
(bash dispatch, bin/loki shim, Bun CLI) with byte-parity against main.loki mcp probe now resolves the server exactly like the launch exec
(file-exec of mcp/server.py with PYTHONPATH from the user cwd), so probe
and launch can no longer disagree under a decoy SDK; the runpy
RuntimeWarning on launch is gone and MCP serverInfo reports the real
loki-mode version.loki plan estimator comment
block executed a stray claude --model on every plan invocation,
polluting stderr (plan suite 19/25). Fixed; loki plan --json now emits
zero stderr bytes (25/25).loki compound help title reads the canonical loki memory compound
(the alias keeps working with a pointer).-m mcp.server from the repo root.loki mcp: launch the Loki Mode MCP server reliably from any project
directory. Fixes three independent launch blockers a fresh npm consumer
hit: the repo’s local mcp/ package shadowed the pip MCP SDK (namespace
collision; the loader now imports the real SDK subtree and restores the
local package), SDK detection only recognized the legacy file layout, and
FastMCP 1.x rejected legacy constructor kwargs. The launcher checks
python3 and SDK importability, offers a consent-gated dependency
bootstrap into the project-local .loki/mcp-venv (non-TTY/CI never
install: honest manual command and exit 2; LOKI_NO_INSTALL_OFFER=1
opt-out), and execs the server over stdio with the install root on
PYTHONPATH while preserving the user’s cwd for .loki resolution.
Verified by a real MCP initialize/tools-list handshake from a non-repo
directory: 34 tools. Known narrow residual (documented in code): a user
cwd containing its own Python package literally named mcp still shadows
under python -m.loki landing now also points at loki web (the visual builder)
alongside the run monitor.loki mcp implementation launched the pip SDK’s stub server with
ZERO Loki tools whenever run outside the install root, because the SDK
probe cd-ed into the install root while the launch did not. The
PYTHONPATH-based fix above closes it, with a decoy-SDK regression test
that fails on the old behavior.loki web stop no longer kills unrelated live loki runs: the machine-wide
orchestrator pkill was removed (Purple Lab’s own children are still
reaped via its scoped child-pids ledger), with a foreign-survival
regression test.loki demo no longer
contains ANSI color codes (TTY-gated in addition to NO_COLOR).loki quickstart: a guided 4-step first build (setup check, one-line idea,
template pick, plan review). Every step defaults so Enter-Enter-Enter-Enter
builds the sample Todo app. Template matching is a deterministic offline
keyword scorer over the bundled templates (top 3 shown, no LLM, no
network). The plan step quotes the real estimator’s figures before any
spend; declining costs nothing. Lands the PRD at ./prd.md with no-clobber
guards (existing files are never silently overwritten; the fallback walks
numbered suffixes). Non-TTY/CI invocations exit 2 with an automation hint,
never hang.loki demo cost confirm: the estimate (cost, time, iterations, labeled as
an estimate) always prints before spending; interactive runs confirm
[Y/n]; –yes skips the prompt but never the estimate; non-TTY/CI without
–yes refuse with exit 2. Declining prints “Cancelled. Nothing was spent.”loki plan (the same env var the runner
honors), with an honest “forced via LOKI_COMPLEXITY” note in formatted
output; invalid values are ignored. This makes the demo quote the figures
for the tier the demo actually runs.loki stop --all exercised by the test suite no longer kills unrelated
live loki runs on the same machine: the stop-scoping suite now scopes the
machine-wide kill to its own uniquely marked test runners via a test-only
pattern knob (user-facing –all semantics are unchanged), and local-ci
permanently proves harmlessness by spawning a foreign-mimic sentinel
before the stop suites and asserting it survives by PID.loki now prints an 11-line newcomer landing (what Loki Mode is, the
three commands that matter, a provider-prerequisite pointer to loki doctor
placed before any paid action, and a cost-preview tip) instead of the
168-line full command table. loki help and loki --help are unchanged.loki doctor timing label corrected from “(instant)” to “(a few seconds)”
(measured 3.2-4.4s across routes).loki dashboard start and loki web each print one line distinguishing the
two browser UIs (run monitor on 57374 vs project web UI on 57375).loki demo actually runs.loki grill now works on stock macOS (no GNU coreutils). The
provider invocation hard-depended on the timeout command, which stock
macOS does not ship; the command-not-found was swallowed by stderr
suppression and surfaced as a misleading “provider returned no output”.
grill now uses the same fallback chain as the runner (timeout, then
gtimeout, then bare execution). Caught by the new grill success-path CI
test on macos-latest runners: the v7.28.0 Tests workflow ran red on macOS
and the Release workflow was cancelled mid-publish (npm/tag/GitHub Release
for 7.28.0 had already gone out; 7.28.1 supersedes it on all channels).
Verified by repro under a stock-macOS PATH (no timeout/gtimeout): exit 0,
full report header.checklist_select_heldout
(autonomy/prd-checklist.sh) deterministically reserves a slice of checklist
items as held-out: count = clamp(round(0.25 * N), 1, 5) for checklists with
N >= 4 items (smaller checklists reserve nothing). Selection is reproducible,
not random: items are ranked by sha256(id) and the first count are taken,
then written once to .loki/checklist/held-out.json (idempotent). Held-out
item IDs are excluded from everything the build loop sees (checklist summary,
visible counts, per-iteration checklist gate), so the build agent cannot tune
to those specific acceptance checks. The completion council evaluates them only
at the ship gate via council_heldout_gate (autonomy/completion-council.sh),
wired into council_evaluate and into both non-council completion routes in
autonomy/run.sh (the completion-promise route and the force-review route). A
held-out item whose status is failing (and not waived) blocks completion like
any other critical failure. Each evaluation records a heldout_eval trust
event. Opt out with LOKI_HELDOUT_GATE=0. Honest limit: this guards the PROMPT
FEED, not the filesystem. The reservation lives on disk at
.loki/checklist/held-out.json; an agent with read access to the working tree
can open that file and learn which items were held out. The guarantee is that
held-out items are kept out of the build loop’s own prompt context, not that
they are sandboxed.no_git_repo or
no_run_start_sha) it still passes through (it never blocks a non-git project),
but completion is no longer independently verified. Instead of passing silently,
the gate writes .loki/state/evidence-inconclusive.json (recording the reason,
iteration, and timestamp), emits an evidence_inconclusive trust event, and
.loki/COMPLETION.txt carries one honest line:
Evidence gate: inconclusive (<reason>) - completion not independently
verified. The record is removed automatically on a later run that resolves a
conclusive baseline. This is a diff-baseline-only disclosure: red tests still
block completion independently, regardless of the inconclusive state.loki spec (living-spec family: lock / status / sync). Binds spec
requirements to content hashes in .loki/spec/spec.lock at lock time, then
detects drift deterministically with no LLM cost. loki spec lock builds or
refreshes the lock; loki spec status recomputes hashes and reports drift,
emitting .loki/spec/drift-report.json; loki spec sync refreshes the lock
after human review. Exit codes are CI-gate usable: 0 in-sync (status) or lock
written (lock/sync), 1 drift detected (status), 2 usage or spec-not-found. When
a spec lock exists, loki verify folds a single Medium SPEC_DRIFT finding
(CONCERNS) into its evidence; graceful no-op when no lock is present.loki grill (Devil’s-Advocate spec interrogation, pre-build). Invokes the
provider once with a Devil’s-Advocate prompt to surface the hardest questions
exposing spec ambiguities, missing acceptance criteria, unstated assumptions,
and security/scale blind spots; writes .loki/grill/report.md. Honest about
its provider dependency: requires the provider CLI and fails cleanly with no
fabricated questions when it is absent. Exit codes: 0 success, 2 usage or
spec-not-found, 3 provider unavailable or interrogation failed (never silent)..claude/commands/ (repo-local Claude Code config, not part
of the npm tarball): loki-verify.md, loki-spec-status.md, and
loki-grill.md, exposing loki verify, loki spec status, and loki grill
as in-editor slash commands. The underlying CLI commands themselves ship in
the package.mcpName in package.json (io.github.asklokesh/loki-mode) for the official
MCP registry.commit.gpgsign or identity setting can no
longer break the suite), and a route-level wiring regression test asserts that
council_heldout_gate is invoked from both completion routes in run.sh and
from the council evaluate path, failing loudly if any wiring is silently
removed.loki spec lock on a repo with no commits records an honest no-commits
sentinel instead of the literal string “HEAD”; loki grill validates the
provider before logging that the interrogation is starting; a stale
held-out block file is removed on the NONE and STALE verdicts.loki verify [base-ref]: standalone deterministic verification of any branch or PR diff.
Runs build, tests, and static analysis scoped to changed files; diff-scoped secret scan;
dependency audit. Exit codes: 0 VERIFIED / 1 CONCERNS / 2 BLOCKED for CI gating.
Machine-readable evidence written to .loki/verify/evidence.json and report.md.
Inconclusive evidence is never reported VERIFIED. Empty diffs yield CONCERNS. Bare
root-level test files are detected so discoverable tests are never silently skipped.
No LLM review in this MVP (stated in help text). Tests: 11/11.loki trust-metrics: aggregates evidence-gate block rate, per-gate failure distribution
(median and p90), council rejection/split rate, and cost-per-verified-task from a new
durable append-only .loki/metrics/trust-events.jsonl. Honesty rule: un-instrumented
projects report available: false, never fabricated zeros. Tests: 12/12;
evidence-gate regression suite: 33/33.--skip-git-repo-check
added at all invocation sites (previously hard-failed on fresh non-git directories).
Documented CLI flags corrected (cline -y).docker-compose.yml with a clearly-named primary web
service, healthchecks on every service, depends_on wiring, env-var config,
and a .env.example. Single-service apps continue to use a plain run command
with no forced Docker involvement.loki.primary=true label,
web/app service name, or common web ports) and surfaces that service’s URL
in the Live App iframe. Previously the runner could accidentally surface a
database port as the app URL.healthy/unhealthy) rather than whether any
container in the stack is running. A dead or non-serving web service shows as
crashed even when the database remains up. The watchdog monitors compose stacks
by web-service health instead of a PID check (which was incorrect for
detached compose stacks).loki start. For interactive foreground sessions Loki
now opens the dashboard in the default browser automatically after the run
starts. The behavior is cross-platform (macOS open, Linux
xdg-open/wslview, Windows start) and is automatically skipped when
running in CI (CI=true), with --detach / --background, over SSH without
a TTY, or with piped stdin. Set LOKI_NO_AUTO_OPEN=1 to opt out entirely..loki/COMPLETION.txt now
includes a “Your app is live at --effort (adaptive reasoning matched to the current RARV
phase: low for Haiku-tier utility steps, medium for standard iterations,
high for planning and critical-path phases), --max-budget-usd (a per-call
cost backstop to prevent runaway spending on a single iteration), and
--fallback-model (automatic model failover when the primary model is
overloaded or unavailable) on each provider invocation. Each flag is gated on
CLI support detection and an individual opt-out env var
(LOKI_AUTO_EFFORT=off, LOKI_AUTO_BUDGET=off, LOKI_AUTO_FALLBACK=off).
Loki’s deterministic trust and verification gates (RARV-C closure, 11 quality
gates, completion council, verified-completion evidence gate) are unchanged.open/xdg-open/start invocation only.--effort mapping follows the existing RARV tier definitions: tier 1
(Opus/planning) maps to high, tier 2 (Sonnet/development) maps to medium,
tier 3 (Haiku/parallelization) maps to low. Tiers are unchanged.loki start run is in
progress the dashboard now embeds the locally-running app (served by the
existing app-runner) in an iframe so users can interact with it immediately,
without leaving the dashboard. The panel includes a status badge (running /
stopped / error), a toolbar with Refresh, Open-in-browser, and Restart
buttons, and a crash error banner with a redacted technical-details disclosure
widget.loki preview command (alias loki open). Prints the running app URL to
stdout and opens it in the default browser. Works whether or not the dashboard
is open.GET /api/app-runner/errors endpoint. Returns a redacted error summary for
the current session. Raw stack traces and absolute paths are stripped before
the response leaves the server, so only developer-relevant context is
surfaced.GET /api/app-runner/logs. The logs endpoint now applies
the same redaction filter as the new errors endpoint, closing a pre-existing
raw-log information-leak.localhost, not any hosted service. This closes the “spin up UI to try it”
gap that cloud builders such as Replit/Lovable/Bolt have, while preserving
Loki Mode’s local-first and no-vendor-lock architecture.git init uses the
host default branch (main on newer git, master on older runners), so the
assertion failed on the runner even though the product correctly reported the
real branch. The test now forces a deterministic branch (git branch -m main)
after the first commit. Product behavior is unchanged; this only de-flakes the
test. This is what kept the v7.23.0 Tests workflow red on the Shell tests job.loki code search "<query>". Combines ripgrep keyword
matching with ChromaDB semantic search via reciprocal-rank fusion (deduped by
file:line), with token-budget-aware output. Flags: –grep-only, –semantic-only,
–budget N, –top N, –json. When ChromaDB is unavailable it degrades to a
grep-only result instead of failing, so search always returns something.tools/index-codebase.py --changed
re-indexes only files that changed (mtime or content hash) and deletes orphan
chunks left by renamed or removed functions, fixing stale hits after refactors.
A manifest at .loki/state/code-index-manifest.json tracks per-file state. The
MCP code-search tools now report stale / stale_files; opt-in
LOKI_CODE_INDEX_AUTOREINDEX=1 reindexes on demand (default is warn-if-stale).LOKI_DYNAMIC_CONCURRENCY=1
makes the parallel-session cap adapt to machine load: it reads
.loki/state/resources.json and scales the cap down under high CPU or memory,
always clamped to [1, ceiling], never above the configured maximum. Knobs:
LOKI_MAX_PARALLEL_SESSIONS_CEILING, LOKI_CONCURRENCY_CPU_THRESHOLD (85),
LOKI_CONCURRENCY_MEM_THRESHOLD (85), LOKI_CONCURRENCY_CRITICAL_THRESHOLD
(95). Default off: behavior is identical to before unless explicitly enabled. A
supervisor/judge pattern (CONTINUE / COMPLETE / ESCALATE / PIVOT) is documented
in skills/parallel-workflows.md with an honest scaling ceiling (dozens of
adaptive concurrent sessions on a local machine, not thousands).grep -q,
which exits on first match and kills the upstream echo with SIGPIPE (“write
error: Broken pipe”); on a slow CI cell that broken-pipe exit was misread as a
no-match. It now uses a race-free in-shell case-insensitive match. This is what
turned the v7.22.0 Tests workflow red on one matrix cell (the product artifacts
were correct; only the test harness raced).loki start --bg)
finishes, Loki now writes a durable completion summary and fires a local
desktop notification, so you can delegate a run, walk away, and be told when
it is ready. This closes the “async agent” gap for Loki’s local-only model
without any hosted backend or CI.
.loki/COMPLETION.txt (human-readable) and
.loki/state/completion.json (machine-readable): outcome, branch, files
changed, the exact git diff review command, and task counts. The files are
always written even when notifications are disabled (they are state, not a
ping), which is the reliable signal for a detached run with no terminal.LOKI_NOTIFICATIONS (default on). Zero network egress.LOKI_DELEGATE_BRANCH=1 isolates a run’s work on a dedicated
loki/delegate-<timestamp> branch. Opt-in LOKI_DELEGATE_PR=1 opens a local
pull request on completion (a gh call from your own machine, never CI, no
auto-merge), only when you are in a GitHub repo with gh authenticated.
Both default off; the network calls are bounded with a 30s timeout.AGENTS.md in the repository
root for build, test, and style conventions, falling back to CLAUDE.md when
AGENTS.md is absent (nearest-file-wins, never merged). AGENTS.md is the
emerging cross-tool agent-config standard (read by Claude Code, Codex, Cursor,
Aider, and others), so repos that adopt it now drive Loki without a
Loki-specific config. The instruction line is byte-identical across the bash
and Bun routes; the layered project-graph walker (autonomy/lib/project-graph.sh)
prefers AGENTS.md over CLAUDE.md at each directory layer. Three new tests cover
the walker precedence, prompt emission, and cross-route byte-parity.CODEBASE_ANALYSIS_MODE instruction
was still the pre-v7.8.1 short prompt while the bash route had the v7.8.1
three-pass version. The byte-exact fixture corpus masked this because both the
Bun constant and the gold fixtures held the same stale string. The Bun
ANALYSIS_INSTRUCTION is now byte-identical to the bash route, and the no-PRD
fixtures were regenerated from the canonical bash output. No fixtures are
suppressed (KNOWN_FAILING_FIXTURES stays empty); parity is genuinely green.mcp/server.py, 7 magic
tools, 1 managed-memory tool), plus 3 resources and 2 prompts. Numbers
verified by counting registrations, not estimated.benchmarks/results/humaneval-loki-results.json) for provenance.loki crash list now lists crash reports, matching the bare loki crash
command. Previously only the bare form listed and loki crash list errored
with “Unknown crash subcommand”, which a user mirroring loki proof list or
loki memory list would hit. Fixed on both routes (bash and Bun) with the
help text updated to show (none) | list. Found during real-user CLI QA of
the v7.19.x arc.withFileLock concurrency unit test no longer times out
on slow CI runners. The advisory file-lock backoff cap was tightened from
200ms to 50ms (a real throughput improvement under contention for the
parallel gate-failure call sites), the test polls faster, and it carries an
explicit generous per-test timeout. The static-analysis parallelism test now
gates its strict speedup assertion on CPU count, so it does not false-fail on
a single-core CI cell while still verifying parallelism on multi-core
machines. No product behavior change beyond the faster lock backoff.loki proof) now renders a branded
in-page card (what was built, verified, cost, duration, with Loki/Autonomi
branding) and opt-in share buttons (X/Twitter, LinkedIn, Copy link). The
local proof file stays ZERO-EGRESS by default (the v7.18.2 posture): no
og:image, no external assets, no literal social URLs in the static HTML, and
no network call on load. Share intent URLs are assembled in JavaScript at
click time, so a button is inert until the user clicks it. The card and the
social hook render only from the already-redacted proof data, and an
operator-provided publish URL (LOKI_PROOF_PUBLIC_URL) is threaded through
the same redaction before it can appear anywhere. Opt out of the buttons with
LOKI_PROOF_SHARE_BUTTONS=0.LOKI_HOSTED_ENDPOINT
seam). loki proof share to a GitHub Gist does NOT yield a rich preview: the
gist page serves GitHub’s own preview tags and the raw file is served as
text/plain, which social scrapers do not parse. There is no official Loki
hosted backend yet; the seam is honest about this.LOKI_UNCERTAINTY_ESCALATION=0). When Loki looks stuck, it now proactively
escalates to the human (writes a structured handoff, fires an intervention
notification, drops a .loki/signals/UNCERTAINTY_ESCALATION marker, and
touches .loki/PAUSE) instead of silently burning iterations until
max-iterations. The trigger reuses three signals already in the loop, with no
new metacognition: P1 the circuit-breaker no-change counter, P2 diff-hash
oscillation (a recurrence A -> B -> A, NOT a trivial A -> A repeat, which is
P1’s territory), and P3 a persistent council split (trailing rounds rejected
with at least one approver). Escalation fires only when at least two of the
three co-occur for N consecutive rounds (LOKI_UNCERTAINTY_ROUNDS, default 2),
so a single noisy proxy cannot escalate alone, and it debounces to once per
stuck-episode (re-arms when the co-occurrence clears) so it never spams. When
it fires it prints a loud terminal line naming the opt-out..loki/PAUSE auto-clears, so by
DEFAULT this feature is NOTIFY-ONLY: it surfaces the stuck state and saves a
handoff but does not halt the run. It halts only in checkpoint/supervised
modes. This is disclosed at the escalation site and in the docs.LOKI_EVIDENCE_GATE=0). Loki no longer accepts a “done” claim without real
evidence: the completion council blocks completion unless there is a nonzero
git diff vs the run-start SHA (something was actually shipped) AND tests are
green. This kills fabricated completion. The gate runs on BOTH completion
paths: the interval-gated council path AND the default completion-promise
route (an agent invoking loki_complete_task or emitting the promise text),
so a self-asserted “done” with an empty diff or red tests cannot slip through.
Diff evidence is the union of committed, staged, unstaged, and untracked work
(--exclude-standard, so gitignored build artifacts do not count), with
Loki’s own .loki/ runtime state excluded so it is never mistaken for shipped
work. Inconclusive cases (no git repo, no baseline, no test-results file,
runner=none) pass through and never false-block a legitimate first run. When
the gate blocks, it prints the reason and the LOKI_EVIDENCE_GATE=0 opt-out
to the terminal AND surfaces it in the dashboard (/api/council/gate now
reports an evidence block; the Quality Gates panel shows a banner).MAX_ITERATIONS, then stops cleanly. It can never hang.loki start after a terminal run
(success, failure, or crash) now resets the iteration baseline so the
evidence gate diffs against the new run’s HEAD, not the prior run’s start SHA
(which would have made the gate toothless). Genuine resume states (paused,
interrupted, budget_exceeded, stopped) are deliberately preserved.tests_red fires whenever a test runner ran and reported failure, including a
project that was ALREADY red before this run. That is the expected
false-block vector; the one-step LOKI_EVIDENCE_GATE=0 opt-out at the block
site is the escape hatch.LOKI_FAILURE_MEMORY=0). Loki
now learns from its own failures within a run so it stops repeating the same
mistake. When an iteration fails, the scrubbed crash record (or, when
telemetry is off, a synthesized record from non-sensitive fields only) is
mapped to an ErrorEntry on that iteration’s episode. The next iteration’s
prompt then carries a “PAST FAILURES TO AVOID:” block built from the most
recent failure lessons (sorted by true wall-clock timestamp, capped at 3), so
the agent sees what it just got wrong. Local, zero new setup, zero network.
Builds on the v7.18.2 crash capture; reuses the existing memory schemas,
storage, and retrieval. No new scrubbing: every injected value comes from the
already-whitelisted crash record or non-sensitive fallback fields, so
docs/PRIVACY.md is preserved.ErrorEntry.resolution is not auto-derived,
so a lesson states what failed, not a proven fix.retrieve_anti_patterns secondary rarely matches on local
setups (documented); the within-run loop is closed by the recency read, not
that secondary.loki with no arguments now leads with a “New here?”
getting-started block (loki doctor, loki quick, loki demo, loki start) so a
new user has an obvious next step instead of a wall of 30+ commands. Reduces
time-to-first-value. Help text only, no behavior change.loki demo description. The command help and the no-arg command list
previously called demo a “~60s simulated session,” but loki demo runs a
real end-to-end build of a sample todo app. All three descriptions now say
“Build a sample todo app end to end (real run)” so the copy matches behavior.loki crash submit prefilled GitHub issue URL, and
the references in docs/PRIVACY.md) now resolves: GitHub issues were
re-enabled on the repository, so the v7.18.2 crash-report promise works..loki/crash/<fingerprint>-<ts>.json. Nothing is sent anywhere in this phase.autonomy/lib/crash_redact.py: the single scrubbing chokepoint. Whitelist-only
emit (deny-by-default): only os, arch, loki_version, node_version, bun_version,
error_class, stack_signature (symbol names only, no paths or line numbers),
rarv_phase, exit_code, friction_kind, project_id_hash, fingerprint,
rules_version, redactions_count, captured_at. Free-text fields (prompts,
diffs, file paths, source) are dropped, not redacted. Layers the existing
proof_redact rules plus crash-specific deny rules (emails, IPs, repo names),
and sanitizes error_class / rarv_phase / exit_code so no secret can ride in a
whitelisted field. Fail-closed: if python3 is unavailable, no file is written.loki crash: list, loki crash show <id>, loki crash submit <id> to
inspect and manually file local reports. submit prints a prefilled GitHub
issue URL and the exact scrubbed payload; it sends nothing automatically.
Ported to both routes (bash cmd_crash, Bun loki-ts/src/commands/crash.ts)
with byte-identical output. Path-traversal guarded (ids with /, \, ..,
or leading separators are rejected).loki start describing what
is collected and how to opt out. Stored as a sentinel in ~/.loki/config,
never re-shown.docs/PRIVACY.md: honest disclosure of what is and is not collected.LOKI_TELEMETRY=off, loki telemetry off,
DO_NOT_TRACK=1, and LOKI_TELEMETRY_DISABLED=true now ALL disable both the
new crash capture AND the existing PostHog usage telemetry
(session_start / session_end / install events). Previously the PostHog
client honored only LOKI_TELEMETRY_DISABLED / DO_NOT_TRACK and had no
first-run disclosure. One switch now gates every collection path on both
routes and in bin/postinstall.js and dashboard/telemetry.py.loki start: the one-time disclosure still prints once in that case.loki doctor now emits byte-identical output on the bash route and the Bun
route when ANTHROPIC_BASE_URL is set. Phase I (v7.5.25) added the
alt-provider routing detection block (OpenRouter, Ollama, LiteLLM,
self-hosted) to the Bun doctor only; the bash route lagged and printed two
fewer lines (PASS ANTHROPIC_BASE_URL: <endpoint> and the
WARN LOKI_MODEL_OVERRIDE not set advisory). The bash doctor
(autonomy/loki cmd_doctor) now carries the same detection block, so the
bun-parity matrix in scripts/local-ci.sh stays green when
ANTHROPIC_BASE_URL is present in the environment. The divergence only
surfaced locally (CI has no ANTHROPIC_BASE_URL), but it was a real
cross-route parity bug. Added doctor-output regression tests to
tests/test-anthropic-base-url.sh..loki
data, opening the Wiki panel flooded the browser console with 404s and
AbortErrors. Wiki section views now show a clean “no wiki generated yet”
message instead of fetching missing data, the server returns a soft empty
state instead of a 404, and a notification action no longer leaks an
unhandled rejection. A section tab clicked while the wiki list is still
loading now resolves correctly once the list arrives (no stuck spinner).tests/e2e/dashboard-fresh-repo.mjs +
scripts/run-dashboard-fresh-repo-harness.sh, wired into local-ci). It
boots the dashboard against a fresh repo (no .loki) and drives a real
browser to assert no cold-load console 404s/AbortErrors and that the trust
iframe matches the SPA theme in both light and after the Dark toggle. This
closes the verification gap that let the above bugs ship: prior checks ran
against seeded data in isolation and never exercised the real first-run path..loki-test-tmp/) and added it to .gitignore.loki start "<brief>". Previously the inline brief was dropped and the proof
page showed “No brief recorded” with source “codebase-analysis”, even though
the user gave a clear one-liner. The brief is written to .loki/state/brief.txt
at brief-mode dispatch and surfaced verbatim in the proof (source “brief”),
which is a stronger, more honest shareable artifact than the synthesized PRD.
The marker is cleared at the start of every run so a later PRD or
codebase-analysis run can never inherit a stale brief label. The brief still
flows through the R1 redaction chokepoint before any published artifact, so a
secret typed into a brief is redacted in the shared proof and gist.loki proof share gist URL no longer includes gh progress chatter: the
uploader captured gh gist create stdout and stderr together, folding the
multi-line progress text into the gist URL, the “Shared:” line, the
ready-to-post hook, and LOKI_LAST_GIST_URL. stdout (the permalink) and
stderr (progress) are now captured separately and the clean URL is extracted.loki wiki generate [path] accepts the documented positional path argument.
The help text and design doc advertised it but argparse only defined --root,
so a positional path errored with “unrecognized arguments”. The positional now
takes precedence over --root; --root and bare-cwd invocations are
unchanged.claude-sonnet-4 alias (HTTP 404 on current CLI) to the rolling sonnet
alias. The adapter already parametrizes the model; this only fixes the default.<script> block in dashboard/static/index.html (compile-only, never
executed; ES-module blocks are skipped to avoid false positives) and fails the
build on any syntax error. tests/run-shellcheck.sh now prunes
.claude/worktrees/ so transient agent worktrees do not pollute the lint set.loki assets export <bundle>
/ import <bundle> / inspect - bundle a team’s reusable assets (learnings,
project memory, custom agents, PRD templates, council config, optionally the
wiki) into a portable .tgz and restore them in another project or clone, so
individual setup compounds into shared org value. Every bundled asset is run
through the R1 redaction chokepoint before writing, so secrets, keys, tokens,
and absolute home/repo paths are stripped (it is safe to share; note free-text
PII like author names is not stripped). --into-install opts agents/templates
into the global install location.loki agent install <source> /
loki template install <source> install community agents and PRD templates
from a git repo, local path, or URL, validated and usable via loki agent
list/run and loki init --template. Source-based today (a hosted central hub
is future work, stated honestly)... sub-path, or a symlink) outside the restore root is SKIPPED,
validated by realpath BEFORE any filesystem write (council round 1 found an
arbitrary-file-write/RCE escape here; fixed + regression-tested).\n became a real newline (breaking string literals
across lines) and regex escapes (\*, \[, \s, \d, \/) were stripped
(corrupting the markdown renderer’s patterns, e.g. /\*\*..\*\*/ emitted as
/**..**/). Escaped these to \\n / \\* etc. so they survive into the
output. This was a PRE-EXISTING bug (present before v7.8.3), found by a
real-user Playwright sweep of the dashboard. All inline scripts now parse with
zero errors; the dashboard root and the cost / trust / proofs panels render
clean (0 console/page errors), verified in a headless browser.loki trust (--json): the across-runs trajectory with a per-axis direction
(improving / regressing / stable, polarity-aware so “fewer iterations” reads
as improving) and a one-line verdict. Distinct from loki kpis (single-run
snapshot), which gains a see-also pointer./api/trust/trajectory endpoint + a /trust dashboard panel with sparklines,
plus a trust_update WebSocket push (mirrors the R3 cost pattern) so the panel
updates live..loki/proofs/<run_id>/proof.json; with fewer
than two runs it says “not enough history yet” rather than inventing a trend,
it shows a DECLINING trajectory when the data declines (not a feel-good
always-up chart), and the human-interventions axis is explicitly marked
unavailable until a per-run intervention counter exists (no fabricated number).loki proof share --hosted <id>: publishes the already-redacted proof artifact
to an operator-supplied LOKI_HOSTED_ENDPOINT (both bash and Bun routes). With
no endpoint set, it prints an honest “no official hosted backend yet; set
LOKI_HOSTED_ENDPOINT or use a gist” message and exits non-zero. It never
fabricates a URL and only reports the URL the operator endpoint returns.LOKI_TIER, default oss; LOKI_LICENSE_KEY optional): a no-op
ALLOW for OSS, wired ONLY into the opt-in --hosted seam, never into any free
command. License verification is a documented stub (no backend yet).--hosted publish guard fails CLOSED: it refuses unless the proof’s
redaction was confirmed applied (redaction.applied == true), on both routes.
This prevents publishing an old/degraded proof whose HTML might carry secrets
to the operator endpoint (council round 1 found the Bun route fail-OPENED on
absent redaction metadata; fixed + regression-tested on both routes).loki rollback latest and loki rollback to <id> (plus loki rollback list):
one-command restore of .loki/ state, with a per-iteration “you can undo this”
signal and an automatic pre-rollback snapshot so a rollback is itself
undoable (“undo your undo”); the snapshot id is printed for the user.--force to roll back without a
safety snapshot. Proven by tests that assert state is preserved on snapshot
failure..loki/ state restore is atomic (tmp + rename). Working-tree code restore
is opt-in (--code / the surfaced git stash apply refs/loki/cp/<id>): a
divergent apply aborts or shows conflict markers (exit 1), never a silent
half-apply, so uncommitted work is protected.loki wiki generate | show | ask builds a
persistent per-project wiki (architecture overview, key modules, data flow)
from the codebase under .loki/wiki/, and loki wiki ask "<question>"
returns an answer grounded in the actual code with file:line citations..loki/wiki/ directory (wiki.json + per-section markdown)
is portable and readable without the dashboard.loki wiki generate skips when the codebase is
unchanged, with --force to override.loki start "<one
line brief>" now works and produces a fast, visible artifact, lowering
time-to-first-value so a new user gets a good first experience instead of a
blank or heavy one. Built additively, reusing the existing start/quick/proof
flow (no parallel second start path).loki start "build a todo app" previously errored “PRD file not found”: a
whitespace one-line brief was misrouted as a PRD file path. detect_arg_type
now returns a brief type for whitespace args that match no file/issue/path
pattern; single-token args, .md/.yaml PRDs, issue refs, and empty input
all behave exactly as before (back-compat preserved).loki start or loki start ./prd.md). The end-of-run
message is branched so the council-off brief path does not claim council
verdicts. The brief PRD is kept distinct from .loki/generated-prd.md so it
does not pollute the v7.8.1 generated-PRD-reuse signature.loki cost CLI (--json, --last N): current-run spend, per-model routing,
per-run history, and budget status, reading the shared
autonomy/lib/efficiency_cost.py (single source of truth; “not recorded”
rather than a fabricated $0.00 when no cost data exists)./api/cost/timeline endpoint: intra-run per-iteration cumulative spend +
per-run history with project totals + read-time budget status, additive to the
existing /api/cost./cost dashboard panel: budget gauge with an 80% warn line,
model-routing-by-spend, per-run table, inline cumulative chart.check_budget_limit (bash) and checkBudgetLimit (Bun) without
changing the hard-cap behavior, so an autonomous run flags a budget approach
before it stops.# shellcheck disable=SC2086 -- comment on one line), which shellcheck
itself cannot parse (SC1072/SC1073 error). The CI Shell-tests job runs
shellcheck via tests/run-all-tests.sh and went red on v7.10.0 because of it
(local-ci ran bash -n on the file but not the shellcheck linter, so it slipped
through). Moved the explanation to its own comment line above a bare
# shellcheck disable=SC2086. The file is now clean at both -S error and
-S warning. No behavior change.loki bench
(run | vs | list | verify | report). A reproducible harness that runs a task
on Loki and competitors and produces a results table, built so a third party
can reproduce or refute the numbers. Core design principle (from research on
why vendor benchmarks get dismissed): the harness is the product, not the
number, and Loki NEVER grades itself.benchmarks/bench/report.py): winner is whoever has the
highest grader success-rate (a Loki-loses input renders the competitor as
winner, regression-tested); null cost renders “not recorded” not “$0.00”; a
mandatory methodology + disclaimers section (reproduce-it-yourself, provenance,
read-only grader, variance, two-column cost, contamination disclosure,
published failures). Sample outputs carry a prominent mock-data banner.autonomy/lib/efficiency_cost.py extracted from the R1
proof generator so benchmark cost and proof cost are computed identically.loki bench runs through the bash route on both runtimes
(the Bun shim falls through); no separate Bun command.verdict/decision and expected flat
council/votes/*.json files, but completion-council.sh actually writes the
outcome as result (APPROVED/REJECTED) in verdicts[] plus aggregate
approve_votes/reject_votes in council/state.json, with per-iteration
detail under council/votes/iteration-N/. The proof’s council verdict and
reviewer rows therefore came out empty, blanking the central trust signal on
every real proof page. The generator now reads result, derives the
approve/reject ratio and threshold from the aggregate, and synthesizes a
council-tally row when no flat per-reviewer files exist. Caught by R1
real-user UAT. Added regression tests for the real APPROVED and REJECTED
council shapes (tests/test_proof_generator.py)..loki/proofs/<run_id>/
(proof.json + index.html) summarizing what the run built and how cheaply +
reliably: itemized cost USD + token breakdown, files-changed diffstat,
per-reviewer council verdicts (with evidence, not a bare badge), quality gates,
wall-clock time, provider/model, and an integrity hash. The page leads with the
itemized bill and ranks every claim by stranger-verifiability (clickable
deployed URL, linked diff), states limitations honestly, and carries a
“Run this yourself” CTA. Default-on at run completion, fire-and-forget (never
blocks or alters the run), opt-out via LOKI_PROOF=0.loki proof command (list | show | open | share) on both the bash and Bun
routes, plus shell completions. loki proof share <id> publishes the proof as
a GitHub gist, but ONLY after showing a redaction preview and an explicit
confirm (publishing is opt-in, never automatic); --yes skips the prompt for
automation, --hosted is reserved for a future hosted endpoint.autonomy/lib/proof_redact.py): a single chokepoint that runs
once before serialization and refuses to emit if it did not run. Scrubs API
keys (Anthropic/OpenAI/Google/GitHub/AWS/Slack), Bearer tokens, JWTs, PEM
private-key blocks, quoted-and-bare secret assignments (password, passphrase,
token, api_key, client_secret, etc.), DB/URI connection-string credentials, and
absolute user paths, in both proof.json and the rendered HTML./api/proofs, /api/proofs/{id},
/api/proofs/{id}/html) with a path-traversal guard, plus a proofs.html
index page.deployment.deployed_url is local-only today; a public hosted URL is reserved
for a later release.gh); the proof page
rendered in every browser engine (verified self-contained + rendered in a
headless browser).cli_command
emit and the first-run installed emit) now fully detach their file
descriptors (</dev/null >/dev/null 2>&1 &). Previously the backgrounded
child inherited and held the shim’s stdout open, which changed pipe-teardown
timing and produced a macOS-only broken-pipe in callers that capture the
shim’s output via a pipe (surfaced as a loki completions zsh failure in the
CI shim-route test harness on macOS; Linux teardown timing masked it). No
user-facing behavior change; telemetry still fires identically and the
existing opt-out still suppresses it. Verified with 20 consecutive
shim-route test runs on macOS (0 failures).bin/loki shim now emits the cli_command
product-analytics event for Bun-routed commands (version, status, stats,
doctor, provider, memory, rollback, internal, kpis). These commands bypass
the bash main() that fires cli_command, so Bun-routed invocations were
previously invisible to usage analytics. The emit reuses the existing
PostHog client (autonomy/telemetry.sh), is fire-and-forget and backgrounded
(no added latency), honors the existing opt-out (LOKI_TELEMETRY_DISABLED=true
/ DO_NOT_TRACK=1), and sends only the subcommand name (never args, flags,
or paths). No double-counting: the bash route still emits its own copy from
main(), and the two routes are mutually exclusive per invocation.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.
.loki/ and .git/ churn excluded) stored at
.loki/state/prd-signature.json, and decides:
.loki/generated-prd.md as-is
(no regeneration);loki start --regen-prd (alias --regenerate-prd / --regen, or
LOKI_PRD_REGEN=1) forces a fresh PRD, overriding reuse.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.
--setting-sources user,project,local on the Claude provider invocation
(when supported). Pins which settings sources Claude Code loads so Loki’s
invocation does not drift if the implicit default changes upstream.
Behavior-neutral (these are the standard sources). Opt out with
LOKI_SETTING_SOURCES=off.--include-partial-messages so the agent’s output streams to the
dashboard and terminal in real time (incremental deltas) instead of appearing
only at message boundaries. The stream-json parser gained an additive
stream_event branch that renders content_block_delta text live and
de-dupes against the final assistant message (no double-print). When partial
messages are off or unsupported, no stream_event lines arrive and the parser
behaves exactly as before. Opt out with LOKI_PARTIAL_MESSAGES=off.--session-id / --resume) was investigated and
deliberately NOT adopted: the token savings it would offer are already
captured by Loki’s existing prompt-cache strategy (a resumed call gets a
near-full cache hit on the static prefix anyway), while session resume would
add unbounded transcript growth and a cache-miss trap. Verified by repro; may
revisit with real-run A/B data.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.
loki start runs an
orchestrator (/tmp/loki-run-*.sh) that spawns the provider agent
(claude/codex/aider) as a child. Stop signaled only the orchestrator
pid; when the orchestrator died (especially on SIGKILL, which skips its
cleanup trap) the agent CHILD reparented to init (PPID 1) and kept iterating.
v7.7.33’s cwd reaper only matched loki-run-*.sh, never the agent, so the
agent survived. Root-caused on a live session (agent PID alive, PPID=1,
cwd=project, still editing).loki start) is now launched as a session / process-group leader
(via setsid, or perl/python setsid on macOS which has no setsid binary),
so the orchestrator, the agent, and all monitors share ONE dedicated process
group whose pgid is recorded at .loki/loki.pgid. All three stop paths
(loki stop, POST /api/control/stop, POST /api/running-projects/stop)
signal the whole group (kill -- -PGID / os.killpg): SIGTERM, wait, then
SIGKILL, so the agent dies atomically with the orchestrator, no orphan window,
even if it ignores SIGTERM. Verified end-to-end: a SIGTERM-ignoring agent
child sharing the group is reaped by Stop.loki start keeps Ctrl+C. Creating a new session detaches the
controlling terminal, which would break the terminal Ctrl+C pause/exit UX. So
an INTERACTIVE foreground loki start is NOT group-launched (it keeps its
tty); for that case stop relies on the agent sweep below rather than a group
kill (a group kill on a shared shell group could kill the user’s shell). Use
LOKI_FORCE_NEW_SESSION=1 to force group mode, LOKI_NO_NEW_SESSION=1 to
disable it.[LOKI-AUTONOMY-AGENT] sentinel (the first
line of the agent’s appended system prompt) combined with the cwd scope, so
agents orphaned by a pre-v7.7.34 session are cleaned up too. An interactive
provider session never carries the sentinel, so it is never touched..loki/pids) shares the target group, it falls back to per-pid kills that
spare the protected pids rather than blasting the whole group.PATCH release. Makes the dashboard Stop button actually stop the session.
/api/control/stop (and the per-project /api/running-projects/stop) only
sent SIGTERM to the pid in loki.pid. When that pid was stale (a crashed or
restarted session can leave an orphaned loki-run-*.sh reparented to init
under a NEW pid), the kill was a no-op, yet the endpoint hit its
“process already gone” path and reported success, so the dashboard showed
STOPPED while the terminal kept iterating. Both stop endpoints now also reap
the actual orchestrator process(es) whose working directory IS the target
project’s directory (the orchestrator temp-script name carries no project
identity, but its CWD reliably does), and report stopped only after verifying
no orchestrator for that project survives. A zombie/defunct process counts as
gone. Reproduced and verified against a live session with a stale loki.pid.loki stop). Linux reads /proc/<pid>/cwd;
macOS/BSD falls back to lsof.PATCH release. Fixes the dashboard task-detail modal showing only the title (no description, acceptance criteria, or logs).
dashboard-state.json (the source for a live
loki start session), /api/tasks read the task description from
payload.description and dropped every enrichment field. But run.sh writes
description, acceptance_criteria, notes, logs, provider, and
startedAt at the TOP LEVEL of the task object, so the description came back
empty and the modal had nothing to show. The richer queue-file path was also
skipped because the stripped entry claimed the task id first. /api/tasks
now reads enrichment from the top level (with a payload.* fallback for
legacy entries) and passes through acceptance_criteria, notes, logs,
provider, startedAt, and friends. The modal already renders these
sections; it was being starved of data. Verified against a live session: the
iteration task’s modal now shows its description, 4 acceptance criteria, and
logs.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.
loki stop) did not stop execution
promptly. The inter-iteration countdown slept in 10s chunks (60s for long
waits) and never checked the STOP file, and bash deferred the Stop endpoint’s
SIGTERM until the current sleep chunk finished. So a Stop issued during the
wait did nothing for up to 60s, and the app the runner had started kept
logging the whole time. The countdown now ticks every 1s and checks the
STOP/PAUSE signal on every tick, so Stop and SIGTERM take effect within ~1s.
Reproduced and verified./api/running-projects fell back to session.json status when a recorded
pid was dead, so a hard-killed or crashed session whose session.json still
said “running” stayed green and the Stop button targeted a dead pid. A
recorded-but-dead pid is now authoritative; the session.json fallback only
applies to legacy sessions that never recorded a pid.loki
start spawned the Claude provider, the agent read the user’s global
~/.claude/CLAUDE.md (rules like “always ask for clarification”, “never
commit without permission”), judged it to conflict with Loki’s “never ask,
never stop” prompt (which was only a user-message instruction, lower
precedence), called AskUserQuestion, and exited having done nothing. Loki now
passes --append-system-prompt with an authorization + precedence override
so the loki_system instructions win for the authorized autonomous session.
An appended system prompt outranks CLAUDE.md memory (verified empirically:
with a conflicting CLAUDE.md, the agent refused without the flag and proceeded
with it). Default-on; opt out with LOKI_AUTONOMY_OVERRIDE=off. Loki never
edits the user’s CLAUDE.md. Applied on both the bash and Bun routes (override
text kept byte-identical between providers/claude.sh and
loki-ts/src/providers/claude_flags.ts). The override is deliberately
narrow: it does not relax any safety rule, it keeps git checkpoints local
only (never push/force-push) and staged by explicit path (never git add
-A), and it leaves destructive or irreversible actions (deleting data,
dropping databases, publishing, rotating secrets, touching production) out of
scope. Note: with the override on, loki start will make local atomic git
checkpoints in the target repo as it works (this is Loki’s existing RALPH
checkpoint behavior, now unblocked); it never pushes. Council voter agents do
NOT receive the override, so reviewers keep their ability to raise CONCERN or
REJECT.main (not a feature branch) and the current routed-command
set, and fixed stale Docker image tags in the install table.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.
loki stop stopped EVERY folder’s session, not just the current one.
With no session-id, cmd_stop ran an unconditional machine-global
pkill -f "loki-run-" that matched every folder’s runner temp script
(the script name carries no folder identity). Running loki stop in one
repo silently killed unrelated Loki sessions in other repos. loki stop
is now FOLDER-SCOPED: it stops only the current folder’s runner, monitors,
and app-runner (via the folder-scoped pid files that already exist), and
marks that project stopped in the dashboard registry. Reproduced and
verified: stopping folder A leaves folder B’s session alive.loki stop --all preserves the legacy machine-wide behavior for users
who want to tear down every Loki runner at once. It works even from a
folder with no live session (the “clean everything” case).POST /api/running-projects/stop endpoint. The endpoint resolves the
target project through the registry, writes its STOP file, runs the same
graceful SIGTERM -> 5s -> SIGKILL dance as /api/control/stop against that
project’s recorded pid, and marks it stopped. Stopping one project never
affects another. Rows are built with textContent only (no innerHTML).~/.loki/dashboard) up while any
other registered project is still running, and only stops it when no
project remains. loki stop --all always stops it.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).
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).
loki dashboard stop|status|open failed from a different directory
than start. The standalone dashboard PID/control dir was relative
(${LOKI_DIR}/dashboard), so stopping from any other cwd reported “not
running” and orphaned the server. It now lives at a fixed
~/.loki/dashboard (the in-build dashboard run.sh starts stays
project-local). Verified: start in dir A, stop from dir B, port freed.127.0.0.1 inside the container, so -p 57374:57374 forwarded
to nothing. The default bind host is now 0.0.0.0 in a container
(detected via /.dockerenv or LOKI_SANDBOX_MODE), 127.0.0.1 on the host.loki serve / loki api ignored --host/--port and lacked a
port-in-use guard. cmd_api now parses those flags, refuses a busy port,
shares the standalone PID dir, computes the TLS scheme, persists
host/port/scheme side-files, and prints correct URLs.http://.../api/status, which fails under TLS
and 401s under LOKI_ENTERPRISE_AUTH. It now probes the unauthenticated
/health over the actual scheme with -k for self-signed certs.loki start --api swallowed dashboard startup failures. It is now a
contained subshell that surfaces the outcome and cannot abort the build.loki status (human + –json) and loki cleanup now check BOTH the
project-local and the ~/.loki/dashboard PID locations and honor the
saved scheme/host/port, so they never miss or misreport a running
dashboard. The Bun route (loki-ts/src/commands/status.ts, both the JSON
and text renderers) mirrors the same dual-path + side-file logic, keeping
the bash and Bun runtimes at parity (verified: both report the identical
dashboard_url for a standalone dashboard).loki start auto-registers the running project (path, pid, port,
status) in the machine-global registry (~/.loki/dashboard/projects.json)
so the dashboard can see projects running in different folders. Fully
non-blocking and failure-swallowed; opt out with
LOKI_SKIP_PROJECT_REGISTRY=1.GET /api/running-projects: lists registered projects with a live
running flag derived from pid liveness (robust on hard kills) and an
is_active flag (realpath-compared so macOS /tmp -> /private/tmp
symlinks match). Deliberately not under /api/projects/* to avoid the
{project_id} int route shadowing it./api/focus and reloads so every panel re-resolves
against that project’s .loki. Each project keeps running independently
(per-process per-directory); switching only changes what the dashboard
shows.tests/test-dashboard-multiproject.sh (NEW, 16/16 PASS): static checks
for every fix, bash/python syntax, bash/Bun status-parity for the dual
pid location, and a functional test of /api/running-projects live
status + /api/focus switching (with a genuinely-alive pid and the
realpath edge case).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.)
autonomy/run.sh store_episode_trace called
MemoryEngine(f'{dir}/.loki/memory'), but __init__(self, storage=None,
base_path=...) takes storage first, so the path became self.storage
and engine.initialize() crashed on str.ensure_directory, dropping
every episode into the except handler. Now passes base_path=.auto_capture_episode reconstructed the episode path as
episodic/<id>.json, but storage writes
episodic/<YYYY-MM-DD>/task-<id>.json. The non-existent path failed the
[ -f ] guard every time. Now reconstructs the real date/task path.consolidation saves anti-patterns as SemanticPattern objects with
category="anti-pattern" into semantic/patterns.json, but anti-pattern
retrieval only read the separate semantic/anti-patterns.json (different
schema). Retrieval now also bridges anti-patterns out of
patterns.json, mapping incorrect_approach/description/correct_approach
onto the what_fails/why/prevention scoring shape. The optional
embedding/vector anti-pattern index is bridged the same way for parity.grep -ciE "(FAIL|ERROR|failed|error:)",
which counted benign lines (“0 errors”, a test_error_handling case,
“no errors found”) as failures, forcing CONTINUE forever. Replaced with
a regex that matches real failure signals (N failed/errors, FAILED,
AssertionError, Traceback) and ignores the zero-count forms.migration-hooks.sh appended via data.get('modes', []), which mutates
a throwaway list when the key is missing, so the record was lost. Now
uses data.setdefault('modes', []).tests/test-memory-audit-fixes.sh (NEW, 4/4): B#1/B#2/B#3.tests/test-council-healing-audit-fixes.sh (NEW, 4/4): the council
grep refinement and the healing setdefault, each with a functional repro.PATCH release. A welcome opener (the “magic opener”) shown on first run and
via loki welcome, styled in the dashboard design language.
assets/welcome/welcome.html (NEW): a self-contained welcome page
using the loki dashboard design tokens (accent #553DE9, DM Serif Display
/ Inter / JetBrains Mono, glass cards, light + dark). It introduces the
product, subtly highlights the RARV-C closure loop, the cross-project
memory compounding moat, and the research backing (Anthropic, DeepMind,
OpenAI, NVIDIA), and links to autonomi.dev/docs.welcome_profile event to the EXISTING
PostHog endpoint (us.i.posthog.com, the same public ingest key the
install telemetry already uses). The page makes ZERO network calls on
load; the submit is the only call. The payload carries only
{role, company_size, tools, source, loki_version, distinct_id} and
never any prompt, PRD, code, or path content.loki welcome command: opens the page in a browser, or prints a
clean terminal welcome when headless / Docker / CI / no browser.loki start, the welcome
opens a single time, gated by a ~/.loki/.welcomed marker; never
repeats, and never auto-opens a browser in CI or non-interactive shells.loki welcome.LOKI_TELEMETRY_DISABLED=true and DO_NOT_TRACK=1 everywhere:
the page is loaded with ?telemetry=off (form disabled, no capture) and
the terminal welcome shows an “analytics off” notice. Disclosed, never
covert. We never collect prompts, PRDs, or code.assets/ now ships to npm (added to package.json files) and to both
Docker images (COPY assets/ in Dockerfile + Dockerfile.sandbox).tests/test-welcome-opener.sh (NEW, 14/14 PASS): single-network-call
guarantee (the only fetch is inside the submit handler, never on load),
no-PII payload (asserts the properties object, not the safety comment),
opt-out paths, terminal fallback, first-run wiring that verifies the hook
is actually CALLED from cmd_start (not just defined), and packaging.
Light + dark screenshots verified against the dashboard design tokens
(bg #FAFAF7, accent #553DE9).PATCH release. UX: “more running in the background, less input required.” Plus the docs reframe completion.
/api/council/transcripts?type_prefix=claude_hook_, returned under
hook_events) and renders them in a “Live Tool Activity” section, so
users can watch background tool calls (PreToolUse / PostToolUse / Stop)
as a run proceeds without running any extra command. The server already
supported the type_prefix filter since v7.5.22; this wires the UI to
it (the component previously never passed the parameter, so the
capability was unreachable from the dashboard). The hook fetch is
independent of the transcript fetch, so a hook failure never blanks the
transcripts. tests/test-dashboard-hook-events.sh (NEW, 3/3 PASS).description label, the
bash runner ASCII banner and dashboard HTML subtitle
(autonomy/run.sh), docs/INSTALLATION.md skill description, the demo
README / run script / voice-over, and the auto-claude comparison doc.
All now lead with “autonomous spec-to-product system (RARV-C closure
loop)” and frame providers as provider-agnostic. The multi-reviewer
council remains a listed feature, not the headline.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).
tools/ was not shipping to npm or Docker. The two benchmark
tools added in v7.7.23-v7.7.24 (bench_memory_retrieval.py,
bench_cross_project_lift.py) lived in a top-level tools/ directory
that was absent from package.json “files” and from both Dockerfiles’
COPY lists, so npm and Docker users got the code fixes but could not
run the benches. Added tools/ to package.json “files”,
Dockerfile, and Dockerfile.sandbox. (The repo and the source-route
CLI were unaffected; only the packaged distributions.)POST /api/memory/consolidate was a stub that always returned
zeros. It now runs the real ConsolidationPipeline.consolidate
(episodic-to-semantic) and returns true counts
(patternsCreated/Merged, episodesProcessed, durationSeconds).POST /api/memory/retrieve was a stub that always returned
{"results": []}. It now runs the real
MemoryRetrieval.retrieve_task_aware against the project’s memory
store, keyed on a goal (with optional phase, task_type,
top_k, token_budget). Empty goal returns a clean empty result.package.json description now leads with autonomous
spec-to-product + RARV-C and frames providers as provider-agnostic,
matching the v7.7.24 docs positioning.loki-mode@7.7.24 installed from the registry: version, doctor
(11/11 checks), status, memory, provider list, and the v7.7.20-24
memory subcommands all return real data.asklokesh/loki-mode:7.7.24 (both Bun and LOKI_LEGACY_BASH=1
routes): version/doctor/status correct; in-container dashboard serves
the real UI and live /api/* endpoints.loki start
./PRD.md: produced a FastAPI backend + vanilla-JS frontend + tests;
the 11 generated tests pass and all 5 PRD acceptance criteria
(create/list/patch/422-validation/served-frontend) pass on the live
API. Completion council confirmed “all PRD requirements implemented
and tests passing”./api/v2/runs/{id}/timeline (real, in
dashboard/api_v2.py).tests/test-dashboard-memory-endpoints.sh (NEW, 4/4 PASS): the two
endpoints run the real engine, empty-goal returns clean, and tools/
is present in both the npm tarball and both Dockerfiles.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.
memory/knowledge_graph.py query_patterns now scores by TOKEN
OVERLAP, not whole-string substring. The prior code only matched when
the entire query string was a literal substring of a pattern field, so
a natural-language goal like “make the charge endpoint safe to retry”
retrieved nothing from a pattern named “idempotency-key-on-charge”.
Token overlap (with per-field weights preserved and an exact-substring
bonus kept for backward compatibility) lets real goals retrieve real
patterns. This silently fixes the two live callers that pass
multi-word context: memory/rag_injector.py and the in-loop pattern
injection at autonomy/run.sh – both previously read an effectively
inert cross-project graph on any multi-word query.tools/bench_cross_project_lift.py (NEW): the memory moat proof.
Seeds two sibling source projects (payments-api, auth-service) and one
target project, builds the org knowledge graph in two conditions
(target-alone vs target+siblings), and reports retrieval-coverage
LIFT over the target’s task goals. Measured on the bundled fixture:
0/6 goals covered by the target alone -> 3/6 covered once sibling
patterns are in the graph (+50 pts, all 3 net-new from siblings).
Exits non-zero if lift <= 0 (CI-gateable regression guard).tests/test-cross-project-lift.sh (NEW): 4/4 PASS. Covers the
query token-overlap fix (NL goal retrieves + exact match preserved),
positive lift exit code, lift JSON invariants, and the honesty
disclaimer presence.PATCH release. Speed bench (excellence bar 7) + privacy opt-out (bar 6)
tools/bench_memory_retrieval.py (NEW, bar 7): seeds N synthetic
episodes, runs M COLD retrievals (fresh MemoryRetrieval+MemoryStorage
per iteration, no warm cache), reports p50/p95/p99, exits 1 if
p95 > threshold (default 500ms per bar 7). --episodes/--runs/
--threshold-ms/--json. Self-cleans its temp dir.
HONEST measured status (council Opus 1 fix – no fabrication):
p95 ~26ms at 200 episodes, ~72ms at 1k (bar 7 MET), but ~1,648ms at
10k episodes (bar 7 NOT MET – 3.3x over). The bar-7 GOAL names 10k;
the file-per-episode cold read does not yet scale there. The tool’s
docstring + –help state this plainly and the tool reports the real
verdict at whatever –episodes you run (default 1000, an honest PASS).
Closing the 10k gap needs an index/cache layer – tracked as a future
optimization, NOT claimed as done..loki/config.json
{"memory": {"disabled": true}} now disables BOTH capture (ingest)
AND retrieval (load_memory_context). memory/ingest.py
::_capture_disabled(memory_base) reads the sibling config; both
ingest entry points + the MCP capture tool pass memory_base.
autonomy/loki::load_memory_context gained a config.json gate after
the existing LOKI_SKIP_MEMORY gate. Covers the v7.7.20 cross-project
augmentation path (gate is before retrieval).tests/test-memory-speed-privacy.sh (5/5 PASS): bench percentile
report + JSON; bench exit-code gate (0 under / 1 over threshold);
config.json opt-out blocks ingest; load_memory_context opt-out wiring;
captured episode scrubs keyword secrets + sk-/ghp_ tokens + sensitive
paths.python3 tools/bench_memory_retrieval.py --episodes 10000 manually).loki start session (logic +
the bash gate are tested; full session not exercised)._capture_disabled + the bash load_memory_context gate FAIL CLOSED
on a malformed config.json (suppress capture/retrieval) rather than
fail open. A JSON typo on a sensitive project can no longer silently
re-enable capture. No-config case still fails open (default behavior).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.
memory/replay.py (NEW ~210 LOC): replay_episode(episode_id,
memory_base) loads a past episode and renders a READ-ONLY report:
the recorded action_log as a timeline + the CURRENT state of every
touched file (missing / unchanged-since / changed-since /
exists-not-in-git, via git log --since=<episode_ts>). Plus
render_markdown() for human output.loki memory replay <episode-id> [--json] CLI: renders the
replay as Markdown (default) or JSON (--json). Exits 1 when the
episode is not found (script-friendly).loki memory help lists the new subcommand.tests/test-memory-replay.sh (4/4 PASS): timeline + file-state +
markdown render; missing-episode graceful found=false; CLI
capture-then-replay round-trip; read-only invariant (hashes a
touched file before/after replay, asserts unchanged).--apply re-execution mode is deferred to a future release with
proper sandboxing + confirmation. Documented in the report’s mode
field and the CLI.-- pathspec separator), 10s git timeout, graceful missing-episode +
malformed-episode handling (top-level try/except).git log --since behavior with a clock-skewed episode timestamp
(best-effort; returns “exists-no-timestamp” on parse failure).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.
/api/memory/economics endpoint shape defect: the pre-v7.7.21
fallback returned camelCase keys (discoveryTokens) that did NOT
match the snake_case file (metrics.discovery_tokens). Now reads the
file correctly, computes cache hit_rate = cache_hits/(cache_hits+
cache_misses), surfaces top-10 most-accessed episodes/semantic
patterns (ranked by access_count then importance), returns normalized
snake_case + backward-compat camelCase aliases + raw passthrough.tests/test-memory-economics-endpoint.sh (9/9 PASS)./api/memory/economics now carries
Depends(auth.require_scope("read")) matching sibling memory
endpoints (was unauthenticated – a regression-adjacent gap).os.walk(followlinks=False) + realpath containment check
(every candidate must resolve under .loki/memory/). Prevents a
symlink under episodic/ from exfiltrating outside JSON or
amplifying a DoS via a symlink to a huge tree. Mirrors the sibling
get_skill endpoint’s containment. Test 9 plants a symlink to an
outside high-access “LEAKED” episode and asserts it does NOT surface.MAX_SCAN=300 files-scanned cap per
subdir applied DURING the walk (not just on surfaced results), so a
large store + the 30s auto-refresh cannot make the request
unboundedly slow.textContent (createElement + removeChild) instead of innerHTML
single-char escape. Agent/PRD-derived summaries can no longer inject
markup.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.
memory/knowledge_graph.py + cross_project.py as ZERO-call-site
dead code):
autonomy/loki::load_memory_context now augments local episodic
retrieval with OrganizationKnowledgeGraph.query_patterns(goal)
results under output['cross_project']. Best-effort, isolated in
its own try/except so a failure never breaks the main retrieval.
Opt out with LOKI_SKIP_CROSS_PROJECT=true.loki memory crossproject [--for <goal>]: lists cross-project
patterns matching a goal.loki memory graph [--export <path>]: dumps/exports the org
knowledge graph (read-only).loki memory graph rebuild: the WRITE-side population path
(council fix Opus 1). Mines .loki/memory/semantic/*.json across
all discovered projects, dedups the union with existing patterns,
truncate-rewrites the org graph. Idempotent (repeated runs do not
accumulate duplicates – council fix Opus 1). Without this the read
side was an inert wake reading an always-empty graph.loki memory enable-hook / disable-hook (deferred from
v7.7.18): installs a Claude Code SessionEnd hook using the VERIFIED
schema {matcher: "clear", hooks: [{type: "command", command}]}
(WebSearch-confirmed). Points at the shipped
claude/hooks/loki-session-end.sh. Idempotent (detects existing
entry by command containing loki-session-end.sh); atomic tmp+rename;
preserves all other settings.json keys. No-op under
LOKI_MEMORY_HOOK_DISABLED=true. disable-hook removes by the same
detection, idempotent on absent.loki memory help updated with the new subcommands.tests/test-memory-wake-dead-code.sh (9/9 PASS): crossproject CLI,
graph dump, graph –export, graph rebuild write-path, rebuild
idempotency, enable-hook verified-schema install + idempotent +
settings preservation, disable-hook reverse, LOKI_MEMORY_HOOK_DISABLED
honored, load_memory_context augmentation wiring.graph rebuild population
path added + idempotency dedup fix -> re-review APPROVE.graph rebuild is manual / not auto-triggered post-consolidation.
The full automatic chain (episode -> consolidate -> semantic pattern
-> auto-rebuild -> graph) needs a hook or scheduler; deferred.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.
loki-ts/src/commands/memory.ts::runMemory default-case fall-through:
was using run() from loki-ts/src/util/shell.ts which spawns the
bash subprocess without inheriting stdin. The shipped npm package
always takes the Bun route by default, so piped JSON to
loki memory ingest --from-stdin was lost. Now uses Bun.spawn
directly with stdin: "inherit", stdout: "inherit", stderr:
"inherit" so the bash subprocess sees the full pipe.setTimeout + proc.kill("SIGKILL").tests/test-memory-capture-wedge.sh 9/9 PASS including new Test 9
(Bun route stdin propagation regression). The test pipes JSON via
BUN_FROM_SOURCE=1 so it exercises the actual default-route code
path that was broken in v7.7.18.bun install -g loki-mode@7.7.18 confirmed the
bug pre-fix (json.loads on empty input); fix verified locally; will
re-smoke post-v7.7.19 publish.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
` 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/ /