loki-mode 11.3.1 → 11.3.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (107) hide show
  1. package/SKILL.md +4 -4
  2. package/VERSION +1 -1
  3. package/autonomy/lib/efficiency_cost.py +8 -0
  4. package/autonomy/lib/engine10-push.sh +22 -8
  5. package/autonomy/loki +62 -30
  6. package/bin/loki +30 -1
  7. package/completions/_loki +6 -0
  8. package/completions/loki.bash +2 -2
  9. package/dashboard/__init__.py +1 -1
  10. package/dashboard/server.py +22 -14
  11. package/docs/AGENT-CHANGE-RECEIPT.md +2 -2
  12. package/docs/CLI-REFERENCE.md +5 -1
  13. package/docs/INSTALLATION.md +1 -1
  14. package/docs/OTEL-COLLECTOR.md +24 -0
  15. package/docs/ROUTING-EVIDENCE.md +32 -0
  16. package/docs/environment-variables.md +1 -0
  17. package/docs/spec-first-intent.md +28 -0
  18. package/docs/v10/CLI-MODERN.md +6 -3
  19. package/docs/v10/DECISIONS.md +15 -0
  20. package/docs/v10/FAILURE-CLASSES.md +95 -1
  21. package/docs/v10/GUARDS.md +18 -0
  22. package/docs/v10/GUIDE.md +7 -5
  23. package/docs/v10/METRICS.md +12 -0
  24. package/docs/v10/OPERATING-MODEL.md +3 -3
  25. package/docs/v10/PROGRESS.md +6 -0
  26. package/docs/v11/INTOTO-AGENT-CHANGE-PREDICATE.md +77 -0
  27. package/docs/v11/MCP-2026-07-28.md +55 -0
  28. package/docs/v11/VERIFY-PR-SANDBOX.md +41 -0
  29. package/loki-ts/dist/loki.js +1130 -969
  30. package/mcp/__init__.py +1 -1
  31. package/mcp/v10_tools.py +29 -0
  32. package/package.json +9 -11
  33. package/packages/control-plane/dist/ask-tools-server.js +8 -2
  34. package/packages/control-plane/dist/server.js +15728 -13988
  35. package/packages/control-plane/drizzle/0006_token_sessions.sql +1 -0
  36. package/packages/control-plane/drizzle/meta/0006_snapshot.json +807 -0
  37. package/packages/control-plane/drizzle/meta/_journal.json +7 -0
  38. package/packages/control-plane/src/db/schema.ts +1 -0
  39. package/packages/control-plane/src/server/routes/cost.ts +4 -3
  40. package/packages/control-plane/src/server/runs.ts +3 -3
  41. package/packages/control-plane/ui/dist/assets/index-DbLzr87u.js +190 -0
  42. package/packages/control-plane/ui/dist/index.html +1 -1
  43. package/plugins/loki-mode/.claude-plugin/plugin.json +1 -1
  44. package/providers/opencode.sh +2 -3
  45. package/src/observability/genai-spans.js +104 -0
  46. package/src/observability/otel-bridge.js +49 -1
  47. package/src/observability/otel.js +2 -2
  48. package/src/protocols/mcp-server.js +180 -5
  49. package/src/protocols/transport/sse.js +23 -2
  50. package/web-app/dist/assets/{AdminPage-CMkHNJbF.js → AdminPage-DuRQZsF9.js} +1 -1
  51. package/web-app/dist/assets/{Avatar-BJyjV7Zi.js → Avatar-7GtOQvsO.js} +1 -1
  52. package/web-app/dist/assets/{Badge-D572l0uq.js → Badge-CYgTnIxj.js} +1 -1
  53. package/web-app/dist/assets/{Button-T4BnOZgl.js → Button-rYSyIu7X.js} +1 -1
  54. package/web-app/dist/assets/{CockpitPage-Cj_GFsNn.js → CockpitPage-9adANY4o.js} +1 -1
  55. package/web-app/dist/assets/{ComparePage-A8m4ZXak.js → ComparePage-DEG2oA-R.js} +1 -1
  56. package/web-app/dist/assets/{ErrorBoundary-hugCg5yg.js → ErrorBoundary-CwtvY4xs.js} +1 -1
  57. package/web-app/dist/assets/{EvidenceReceiptPanel-CuJKDC2G.js → EvidenceReceiptPanel-DTT73pPB.js} +1 -1
  58. package/web-app/dist/assets/{GitHubIssuesPanel-BYGKjarC.js → GitHubIssuesPanel-CpWZLz2x.js} +1 -1
  59. package/web-app/dist/assets/{GitHubPRsPanel-c7sFIJlf.js → GitHubPRsPanel-H293R7E2.js} +1 -1
  60. package/web-app/dist/assets/{HomePage-A1MIkG2P.js → HomePage-B11Imp9k.js} +2 -2
  61. package/web-app/dist/assets/{LoginPage-8KQMRhtR.js → LoginPage-BZBgx5Jr.js} +1 -1
  62. package/web-app/dist/assets/{MagicPage-C05QW2Lx.js → MagicPage-B27S4_X8.js} +1 -1
  63. package/web-app/dist/assets/{MetricsPage-Oukv5hme.js → MetricsPage-CfbJWNDG.js} +1 -1
  64. package/web-app/dist/assets/{NotFoundPage-DKIwsawz.js → NotFoundPage-Tp_4zMSm.js} +1 -1
  65. package/web-app/dist/assets/{ProjectPage-DwFRPLCG.js → ProjectPage-CM4fgMsV.js} +3 -3
  66. package/web-app/dist/assets/{ProjectsPage-CgY4YYDX.js → ProjectsPage-DP2Fvsrd.js} +1 -1
  67. package/web-app/dist/assets/{SettingsPage-BpfcSjUA.js → SettingsPage-C4G9-EAy.js} +1 -1
  68. package/web-app/dist/assets/{ShowcasePage-BqBakC4K.js → ShowcasePage-Dj-n4dwu.js} +1 -1
  69. package/web-app/dist/assets/{SystemSettingsPage-CZLu5VB0.js → SystemSettingsPage-BqMLpuGt.js} +1 -1
  70. package/web-app/dist/assets/{TeamsPage-D55S1jKz.js → TeamsPage-mIH8H96m.js} +1 -1
  71. package/web-app/dist/assets/{TemplatesPage-D3SydvxN.js → TemplatesPage-BagW-v4n.js} +1 -1
  72. package/web-app/dist/assets/{TerminalOutput-hNg8G1-e.js → TerminalOutput-CTQRHhnK.js} +1 -1
  73. package/web-app/dist/assets/{activity-D2C0VagO.js → activity-DNQiBDDL.js} +1 -1
  74. package/web-app/dist/assets/{bell-CMvYi36w.js → bell-Coiwdad1.js} +1 -1
  75. package/web-app/dist/assets/{bot-BvtQ6lmo.js → bot-Cou-rTgR.js} +1 -1
  76. package/web-app/dist/assets/{check-B13xkXed.js → check-CsF-XtQp.js} +1 -1
  77. package/web-app/dist/assets/{chevron-left-emu6r49E.js → chevron-left-BJiy55Ht.js} +1 -1
  78. package/web-app/dist/assets/{circle-alert-CExlwjuy.js → circle-alert-D6-pQQWq.js} +1 -1
  79. package/web-app/dist/assets/{clock-D7Uas3oL.js → clock-BOGqwgt9.js} +1 -1
  80. package/web-app/dist/assets/{cloud-CkB4S86C.js → cloud-DsxhTtf5.js} +1 -1
  81. package/web-app/dist/assets/{code-xml-SL9IIWIn.js → code-xml-CxJDHUlE.js} +1 -1
  82. package/web-app/dist/assets/{database-BQFkd7t1.js → database-BJePscl_.js} +1 -1
  83. package/web-app/dist/assets/{dollar-sign-qHmnNYsU.js → dollar-sign-6qWGAkZo.js} +1 -1
  84. package/web-app/dist/assets/{file-code-corner-C2YUDP0K.js → file-code-corner-DcOtYeDb.js} +1 -1
  85. package/web-app/dist/assets/{file-plus-C_ooIkDe.js → file-plus-WuRyLFo0.js} +1 -1
  86. package/web-app/dist/assets/{globe-CQ-GL485.js → globe-r-xCP4DE.js} +1 -1
  87. package/web-app/dist/assets/{hammer-U05L_D7V.js → hammer-DTt0SsCB.js} +1 -1
  88. package/web-app/dist/assets/{index-CooHd7I-.js → index-BkbXyH8y.js} +3 -3
  89. package/web-app/dist/assets/{layers-Buo17rjW.js → layers-BvZhvSMY.js} +1 -1
  90. package/web-app/dist/assets/{loader-circle-15vK3oV1.js → loader-circle-3gosTei9.js} +1 -1
  91. package/web-app/dist/assets/{lock-BeXLebuJ.js → lock-CZZMxP0U.js} +1 -1
  92. package/web-app/dist/assets/{package-MVh_6-6R.js → package-C5A8CkJr.js} +1 -1
  93. package/web-app/dist/assets/{plus-Dh_nd9vS.js → plus-Btv7tTIF.js} +1 -1
  94. package/web-app/dist/assets/{refresh-cw-C9cBJEAT.js → refresh-cw-DsWLXgs_.js} +1 -1
  95. package/web-app/dist/assets/{rotate-ccw-D3PAtzST.js → rotate-ccw-DOVDjway.js} +1 -1
  96. package/web-app/dist/assets/{scroll-text-DZCoXQjo.js → scroll-text-smySzlrX.js} +1 -1
  97. package/web-app/dist/assets/{server-OCOLI5J_.js → server-b5PHsqAU.js} +1 -1
  98. package/web-app/dist/assets/{shield-alert-oVhmf1_h.js → shield-alert-BD7FDlh-.js} +1 -1
  99. package/web-app/dist/assets/{thumbs-up-B-kbX4vN.js → thumbs-up-CAsFzbyE.js} +1 -1
  100. package/web-app/dist/assets/{trash-2-EL6moveW.js → trash-2-BJ35qHgl.js} +1 -1
  101. package/web-app/dist/assets/{trending-down-Cl3kjpsx.js → trending-down-Ozrd26cZ.js} +1 -1
  102. package/web-app/dist/assets/{trending-up-DST7I6M2.js → trending-up-BhnMcM32.js} +1 -1
  103. package/web-app/dist/assets/{upload-CUOT780G.js → upload-JVHfdwoA.js} +1 -1
  104. package/web-app/dist/assets/{usePolling-Djmt90eb.js → usePolling-CTzbDV-e.js} +1 -1
  105. package/web-app/dist/assets/{user-BRBohA6J.js → user-Bks3-GYv.js} +1 -1
  106. package/web-app/dist/index.html +1 -1
  107. package/packages/control-plane/ui/dist/assets/index-DcBJXjCJ.js +0 -190
@@ -0,0 +1,28 @@
1
+ # Spec-first intent
2
+
3
+ The intent card (`LOKI_INTENT_CARD`, `LOKI_CONFIRM`) shows what the plan model thinks you want and the acceptance checks it will hold itself to. Spec-first intent turns those checks into a file you can edit before any code is written.
4
+
5
+ ## Flow
6
+
7
+ ```bash
8
+ loki plan "add search ranking" --spec # or: loki start --spec-first "add search ranking"
9
+ # edit .loki/specs/add-search-ranking.md
10
+ loki start --spec .loki/specs/add-search-ranking.md
11
+ ```
12
+
13
+ 1. `loki plan <task> --spec` runs one model session. The model writes the intent sentence and 2 to 6 criteria as JSON; the engine checks that JSON against a schema and renders `.loki/specs/<slug>.md`. The model never writes the markdown itself. An existing spec is never overwritten unless you pass `--force`.
14
+ 2. You edit the file. Keep the marker comment on line 1 and the three headings (`## Task`, `## Intent`, `## Acceptance criteria`). Task lines start with `> `; criteria are `- ` bullets.
15
+ 3. `loki start --spec FILE` parses the file and treats it as the authoritative intent: the implement brief carries the intent and every criterion, and wins over the task text where they differ. A positional task, if given, replaces the task text but not the spec.
16
+
17
+ ## Refusals
18
+
19
+ A spec that does not parse exits 2 before any run state is created, naming the file and line, for example `engine10: .loki/specs/a.md: line 14: criteria must be bullet lines starting with "- "`. `--spec` and `--spec-first` cannot be combined.
20
+
21
+ ## Evidence
22
+
23
+ - The receipt gains an additive `spec` block: `{ "path": ".loki/specs/<slug>.md", "sha256": "<hex>" }`. The hash covers the exact bytes the run started from (the engine snapshots them into the run directory), so later edits to the file do not change what the receipt vouches for. Runs without `--spec` have no `spec` key and their receipt hashes are unchanged.
24
+ - The PR body gains a `Spec:` line linking the file with the first 12 hex characters of its hash, next to the `## Intent` section.
25
+
26
+ ## Unchanged without the flags
27
+
28
+ Without `--spec` or `--spec-first`, `loki plan` stays on its existing route, `loki start` behaves as before, and the plan and implement briefs are byte-identical (pinned by golden fixtures in `loki-ts/tests/engine10/fixtures`).
@@ -6,12 +6,12 @@ Classes: KEEP-MODERN (v10-native or engine-neutral), UPDATE (live but needs mode
6
6
 
7
7
  ## Counts
8
8
 
9
- - KEEP-MODERN: 61
9
+ - KEEP-MODERN: 64
10
10
  - UPDATE: 22
11
11
  - DROP-LEGACY: 31
12
12
  - DELETE: 14
13
- - Total rows: 128
14
- - Registry paths (commands and nested subcommands): 177
13
+ - Total rows: 131
14
+ - Registry paths (commands and nested subcommands): 181
15
15
 
16
16
  ## Commands
17
17
 
@@ -36,6 +36,9 @@ Classes: KEEP-MODERN (v10-native or engine-neutral), UPDATE (live but needs mode
36
36
  | start | bun route, bash fallthrough | UPDATE | Forks between v10 and the SDK loop; flags still legacy-shaped |
37
37
  | slack | loki-ts/src/cli.ts | KEEP-MODERN | Bun-native integration |
38
38
  | queue | loki-ts/src/cli.ts | KEEP-MODERN | Governor-aware batch of issue-mode runs |
39
+ | issues | loki-ts/src/cli.ts | KEEP-MODERN | Mass issue pickup on the T7 queue |
40
+ | undo | loki-ts/src/cli.ts | KEEP-MODERN | UNDO-1/2: --plan is read-only, apply behind LOKI_UNDO=1 |
41
+ | verify-pr | loki-ts/src/cli.ts | KEEP-MODERN | VPR-2: fail-to-pass check, LOKI_VERIFY_PR=1 |
39
42
  | answer | loki-ts/src/cli.ts | KEEP-MODERN | v10 blocked-run flow |
40
43
  | engine10 | loki-ts/src/cli.ts | KEEP-MODERN | The v10 engine |
41
44
  | internal (hidden) | loki-ts/src/cli.ts | KEEP-MODERN | Hidden plumbing |
@@ -777,3 +777,18 @@ Amends FC-35 (the router-on plan no longer skips small tasks) and D89 Amendment
777
777
  4. FC-35 R-B holds unchanged: always emit a route or a recorded reason. A failed, timed-out or malformed routing call writes routed:false with the reason to route.json and runs the full plan as before (fail-safe toward more planning, never less). Wall, verify, commit, seal and pr never skip.
778
778
  5. Measurement gate: the B9 row on trivial-sum shows the routing call below 10% of total run cost (n>=3), and router-on cost at most 1.05x router-off (D89 Amendment 2). Router-off output stays byte-identical with the flags off.
779
779
  6. Slices: COST-HALF LD-01 (gate plus routing call) and LD-02 (marker as a late input), HIGH tier.
780
+
781
+ ## D94 (2026-10-08, CTO ruling relayed by the CoS): main holds the last pushed release until npm has it; RG-06 contrib cap
782
+ 1. Main stays at the last pushed release commit until that version is on npm. Later approved slices merge onto a train branch (train-<version>), which fast-forwards or merges into main only after the cut is published. Origin: 11.3.2 train merges sat on main while 11.3.1 was blocked on a red nightly (NIGHTLY-BLOCK), which forced a cherry-pick-only push.
783
+ 2. Line budgets are never raised to admit a feature. Overflow is extracted along docs/v11/REGISTRIES.md.
784
+ 3. RG-06 approved: loki-ts/src/contrib/ with a 1200-line cap and the D66 import rules. Binding guards: core (engine10) never imports contrib; each contrib module is optional (deleting one leaves core compiling and flag-off goldens unchanged); no verdict logic in contrib (D42); the cap failure lists per-file line counts.
785
+
786
+ ## D96 (2026-10-08, founder directive relayed by steering at 17:45Z, CoS records): every approved slice is a release; next by merge, latest by promotion
787
+ Founder: "you are taking hours long for releases, I wanted 3 releases to 10 releases per hour ... release asap to accommodate that many". Supersedes D94 point 1 (trains) and the D91 clock cut; D90 moves from publish to promotion. D94 points 2 and 3 stand.
788
+ 1. A slice that passes merge-gate merges straight to main, and main publishes a PATCH to npm `next` within 15 minutes. There are no trains and no batching. Target: 3 or more releases per hour, 10 when the queue allows.
789
+ 2. `next` gate (budget 10 min from push to npm): tsc, impacted tests plus always-run guards, moat P9 (never waived), dist guard, pack smoke.
790
+ 3. `latest` is promotion only, never a separate build. promote.yml moves `latest` to the newest `next` version whose SHA, or a descendant of it, has a green full-suite backstop and a green Post-Release Smoke. The backstop runs on main HEAD every 60 minutes. A red backstop blocks promotion, not publishing, and opens a P0 fix slice within the hour.
791
+ 4. Pipeline: the workflow bumps the version on push to main (no human bump), CHANGELOG lines come from slice cards and verdicts, the backstop runs hourly, and promotion is driven by a green backstop. Every existing release guard stays: never-backwards, the gitHead stamp, the release-actor tag check. First slice: D96-GATE (move the red-nightly block from release.yml to promote.yml).
792
+ 5. Slices are 20-40 minutes of build, one concern, with a user-visible line on the card. Large features ship as a sequence of small flag-gated slices.
793
+ 6. HIGH reviews run in pair mode. A TL reviews LOW and MEDIUM with the machine gate, and merge-gate is the hard check. There is no serial review queue.
794
+ 7. Every tick reports merge-to-`next` minutes for each release and releases per hour.
@@ -457,7 +457,7 @@ L0 review rule (ENGINE-LAWS.md L0): a fix that adds an `if` or a regex about the
457
457
  - Law: moat P9 (the token reaches only the harness's own push, to a destination fixed before any agent ran), L2 (one credential path).
458
458
  - Siblings swept: loki-ts/src/commands/start.ts attempt child (fixed), runner/attempts.ts git helper and gh pr create (fixed: safeGit, explicit env), engine10/supervisor.ts worker env (already withholdGithubTokens on a copy), engine10/stages/pr.ts push script (pinned origin, already correct), util/pr_lessons.ts and commands/proof.ts gh spawns (credentialed by design, explicit env), engine10/preflight.ts (env passed), runner/checkpoint.ts, council.ts, quality_gates.ts and engine10/session.ts and verify.ts git spawns (read-only local git; session.ts and verify.ts still pass process.env and are NOT converted here). OPEN gap: under allowToken the repo-local credential.helper, core.sshCommand, url.*.insteadOf and pushInsteadOf are still honored (CREDENTIAL_CONFIG keeps the user's helper and ssh command), so a hostile attempt can write them into the shared .git/config; a helper receives the token on store and sshCommand runs code with the token in its env. 11.3.1 therefore refuses `start --attempts N` (N>1) without --no-pr (exit 2, before any pin, worktree or spawn). The 11.3.2 fix (T5-PUSHPATH) is to push from a fresh temp bare repo with clean config, or to pass explicit -c overrides plus GIT_CONFIG_GLOBAL only.
459
459
  - Mechanism: `tokenFreeEnv` / `safeGit` (loki-ts/src/util/safe_git.ts); attempts pin `remote.origin.url` in memory in productionDeps before the first attempt and push to that literal URL with allowToken, deriving `gh --repo` from it (no GitHub parse, no PR).
460
- - Fixture: loki-ts/tests/runner/attempts-env-token.test.ts (attempt child env token-free), attempts-git-token.test.ts (only the push keeps the token), attempts-origin-pin.test.ts (rewritten origin cannot redirect the push), attempts-pr-refused.test.ts (11.3.1 refusal).
460
+ - Fixture: loki-ts/tests/runner/attempts-env-token.test.ts (attempt child env token-free), attempts-origin-pin.test.ts (rewritten origin cannot redirect the push), attempts-push-pr.test.ts (winner pushed only via engine10-push.sh push-pr; hostile worktree config sees no credential), util/credentialed_push_guard.test.ts (no credentialed push in loki-ts/src outside push-pr).
461
461
 
462
462
  ## FC-41 safeGit blanks diff.external, so every patch-producing call dies
463
463
  - User saw: nothing yet. Raw: `diff.external=` (blank, set to stop a repo-planted external diff) made git try to run an empty program, so any `diff`, `show -p` or `log -p` through safeGit failed with "external diff died" unless the caller remembered `--no-ext-diff`.
@@ -498,3 +498,97 @@ L0 review rule (ENGINE-LAWS.md L0): a fix that adds an `if` or a regex about the
498
498
  - Siblings swept: every one of the six suites was run individually red then green; the fast tier covers the same files only through select-tests.sh diff selection.
499
499
  - Mechanism: a change that touches a file a deferred suite reads (workflows, README, docs/v11, bin/loki, tests/real-run) must name that suite in its slice card Wall checks and run it before merge; the nightly is the backstop, not the first detector.
500
500
  - Fixture: the six suites themselves (tests/test-dep-inventory.sh, tests/test-docs-cli-drift.sh, tests/test-agent-types-loaded.sh, tests/test-telemetry-analytics-toggle.sh, tests/test-train-verdict-reuse.sh, shellcheck -S error tests/real-run/scenarios/*.sh).
501
+
502
+ ## FC-44 The receipt understates facts the console measured
503
+ - User saw: a billed trivial-sum run printed 40s and 107k tokens on the console while its receipt said time.wall_s 8.3 and cost.input_tokens 24.
504
+ - Raw: seal summed `duration_s` from stage outputs, but machine.ts put it only on the emitted event, so only implement carried one; the receipt cost block omitted cache tokens (the bulk of input) and supervisor.ts's summary tokens omitted them too, disagreeing with the E-59 total. Cache keys no result file carried were also written and summed as 0, which reads as a measured zero, and a cache total from only some sessions was published as the run total.
505
+ - Law: L2 (one fact has one author: the receipt and the console must read the same measurement), evidence-or-it-did-not-happen (a receipt figure that contradicts the console is a false claim).
506
+ - Author of total_s: the first line of events.jsonl, the earliest timestamp the supervisor writes, so it precedes worker boot and preflight; the end is the seal clock. The result-line duration_ms is carried separately as cost.sdk_duration_ms (model time only) and is never used as total_s.
507
+ - Reconciliation (1%): `time.stages` is a disjoint partition of total_s: setup (first event to machine start), each stage, a parallel group such as plan||wall as ONE bucket ("plan+wall", union of its intervals), orchestration (gaps) and seal. Per-member seconds are in `time.stage_s`. Any consumer may add the buckets. `reconciledTotalS` (loki-ts/src/engine10/receipt_time.ts) returns total_s only when the buckets are non-negative numbers summing to it within 1%; a gap or a clock fault reads NOT RECORDED downstream (receipt.md Total line, recordRun history, receiptBlock actual), never a number. total_s ends at seal, so the PR stage is excluded and the receipt.md line says "to seal".
508
+ - Siblings swept: engine10/pr_body.ts stageTimes (same reduction, now fed), runner/router/cost_preview.ts receiptBlock/recordRun/estimateFromHistory (reconciled total only; null wall reads NOT RECORDED), history_store.ts (version marker wallKind "total-v1"; unmarked old rows hold the stage sum and are ignored for time estimates, still used for dollars), autonomy/lib/proof-template.html (reads the cache keys, now populated), stages/seal.ts renderReceiptMd, supervisor.ts summary.cost.tokens (summaryTokens), control-plane packages/control-plane/src/server/runs.ts:37 (inputTokens now includes cache read and creation), routes/cost.ts:24 (sums that column), ui/src/pages/cost/index.tsx (tile and column relabelled "Tokens in (incl. cache)"). Not changed: control-plane ingest and mcp/cp_tools.py read the run.completed event wall_s (already true elapsed), not receipt.time.
509
+ - Mechanism: machine.ts records duration_s on every completed stage output and a per-run timeline; receipt_time.ts builds and checks the time block; sdk_stream_parser writes a cache key only when usage carries it, and cost.ts sumResultCosts marks a cache total seen only when EVERY session carried the key and costTotalsOf maps them, so an unseen key is omitted from the receipt. All additive: absent means NOT RECORDED, old receipts still verify (receipt hash covers the whole body).
510
+ - COST-RECORDS: the SDK result `usage` is main-loop only while `total_cost_usd` covers the whole pipeline. writeResultCost now also records `modelUsage` (per-model tokens and costUSD), `num_turns`, the main-loop 5m/1h cache split, `session_id` and `resumed_from`. When the per-model costs sum to total_cost_usd within 1%, cost.tokens_scope is "all-models", tokens come from modelUsage and cost.per_model is published; otherwise tokens_scope is "main-loop" and no per_model. cost.cache_creation_main_loop is always labelled main-loop scope (modelUsage has no split).
511
+ - FIX-RESUME: the SDK says a resumed session's total is cumulative only when its transcript saved one. A resumed result whose total is below its predecessor's is provably separate and is summed (cost.resume "separate"); an equal-or-higher total fits both cases, so the iteration joins `missing`, usd reads NOT RECORDED and cost.resume is "ambiguous". Never subtracted, never guessed. An ambiguous file is excluded from every summed figure (measuredCount, partial dollars, tokens, cache, per_model, turns, sdk_duration_ms), and input_tokens/output_tokens (required numbers) then sum only the sessions that count, with cost.tokens_measured {k, n} written only when k < n (receipt.md prints "Tokens: partial: X input / Y output for k of n sessions"; real-run.sh prints NA for them; the per-session cost event omits its token keys). The control-plane fold counts the cost events that carried usage against all cost events (below). And per_model/turns/cache split/all-models tokens are published only when every session in the run produced a non-ambiguous file carrying them. Reachability: ambiguous is reachable in DEFAULT runs, no flag: resumeAfterConflict (FC-19, conflict_resume.ts) and resumeAfterEmptyDone (FC-43, the "-e" iteration) pass resumeSessionId on claude, and LOKI_E10_FIX_RESUME=1 adds the CH-03 plan->implement->fix chain on top; each is covered by a test. The real resumed-session capture (LOKI_E10_FIX_RESUME=1) was attempted once on the trivial-sum fixture (claude-sonnet-5-5, CH-03 chain on): the task was solved on the first implement attempt, so no fix round ran and no session was resumed (4 sessions, none with resumed_from). Cumulative vs separate is therefore UNMEASURED and "ambiguous" stays the shipped rule. That run did validate the rest of the receipt: total_s 39.687 equals the sum of time.stages, and per_model costs reconcile to total_cost_usd. Fixtures for the resume branch are shaped from sdk.d.ts.
512
+ - Readers of unrecorded usage (sibling sweep, R4): the ambiguous session's efficiency record (cost.ts writeEfficiencyRecord) now omits cost_usd and every token key and carries tokens_measured:false (a resume provably below its predecessor, found among the sibling result-cost files, keeps its numbers). Every reader treats the flag as NOT RECORDED, never a partial sum: loki-ts/src/metrics/kpis.ts (recordsMeasured gate, unmeasured_iterations only when >0), loki-ts/src/commands/stats.ts (JSON null, text "NOT RECORDED", per-iteration rows), autonomy/lib/efficiency_cost.py collect_efficiency (token fields None, tokens_partial and tokens_unmeasured_records only when >0), dashboard/server.py /api/cost (totals and by_phase/by_model None, unmeasured_sessions only when >0; the context-tracker fallback is skipped), while _compute_budget_snapshot and _compute_cost_timeline already count a record with no measured field as unmeasured. Control plane: runs.ts stores token_sessions (nullable, migration 0006, null on rows ingested earlier) = cost events that carried usage; cost routes add token_sessions and token_sessions_total only when some session lacked usage; the cost tile, the cost table and the run page print "partial: k of n sessions". loki-ts/src/runner/budget.ts calculateCostFromRecords reads cost_usd or prices tokens, so an unrecorded session adds $0 to the budget breaker exactly as any unpriced session did before; not changed. Tests: tests/engine10/cost.test.ts R4-1, kpis.test.ts, stats.test.ts, tests/test_efficiency_cost_availability.py, tests/dashboard/test_cost_endpoint_honesty.py, packages/control-plane/test/server/runs-cache-tokens.test.ts. The cost event (loki-ts/src/engine10/session.ts recordCost) shares the tokensUnmeasured predicate (cost.ts) with writeEfficiencyRecord (CostResult.tokens_measured set, or an ambiguous resume): such a session emits no token keys, so the control plane never reads a missing session as 0 (R5-1; test in tests/engine10/cost.test.ts, plus the FC-19 "-r" case). budget.ts recordPartialStreamCost (a killed session with no partial-usage file) sets tokens_measured {k:0,n:1}, so its efficiency record and cost event carry no zero token keys (R5-2; tests in tests/engine10/cost.test.ts). Migration 0006 leaves pre-existing runs at NULL, pinned by ask_schema.test.ts (R5-4).
513
+ - Fixture: loki-ts/tests/engine10/seal.test.ts (real-shaped boot/stages/seal stream reconciles at total_s 35; gap and 1% boundary read null; parallel overlap), tests/engine10/cost.test.ts (cache keys through the real reader and worker mapping), tests/runner/cost_preview.test.ts (old unmarked entry ignored for time).
514
+
515
+ ## FC-49 Weekly integrity audit ran the gitleaks-dependent suites without installing gitleaks (issue 223)
516
+ - User saw: issue 223 "Weekly Integrity Audit Failed - 2026-10-05". Raw: run 37311758053 (head dee1553234d4), pytest 6 failed / 3637 passed, and the shell suite "security-audit.yml gitleaks config isolation" FAILED (21 passed, 1 failed).
517
+ - Law: evidence or it did not happen; a guard that silently does not run is an absent measurement (_require_gitleaks fails under CI by design).
518
+ - Cause (measured): integrity-audit.yml had no install-gitleaks.sh step, unlike test.yml, full-suite.yml and release.yml. Failing tests: tests/test_release_required_ci.py::PollLoopPriorityIsExercisedForReal::{test_e157_clean_range_still_reuses, test_e157_eligible_bump_reuses_parent_security_audit_success, test_e157_hex_debugid_is_still_eligible, test_e157_merge_release_hiding_a_leak_in_second_parent_is_not_reused, test_e157_secret_in_allowlisted_changelog_block_is_not_reused, test_e160_parent_main_stubs_plus_one_train_success_still_reuses), all "gitleaks is not installed in CI"; shell: "no gitleaks binary under CI".
519
+ - Siblings swept: every workflow with a bare `python3 -m pytest` step (only integrity-audit.yml lacked the install).
520
+ - Mechanism: install step added before the test steps; a pytest guard requires the install in every workflow that runs bare pytest.
521
+ - Fixture: tests/test_release_required_ci.py::EveryWorkflowThatRunsThePythonSuiteInstallsGitleaks (red before the step existed).
522
+
523
+ - Sibling instance (ALIAS-FWD): tests/cli/test-alias-forwarding.sh carried 3 stale reds on main: help entry bound 23 vs 27 shipped commands (test stale, no decision caps the page), a local-ci Dashboard-normalizer assertion for the bun-parity matrix removed in 5adff01a9 (test stale, assertion retired), and the main-parity export case whose extracted main script could not locate the skill dir (harness stale, fixed with SKILL_DIR). Product had not drifted in any of the three; the suite is not in the fast tier.
524
+
525
+ ## FC-47 select-tests --run returned success having run nothing (SEL-RUN-FC47)
526
+ - User saw: nothing yet. Raw: `scripts/select-tests.sh --run` on an R0 diff (package.json, tests/run-all-tests.sh, unparseable, unknown shape) exited 0 with no suites executed, because every early `emit ...; exit 0` returned before the DO_RUN check.
527
+ - Law: Engine Laws (a gate that can pass by doing nothing is theatre); FC-16 class (green with zero work).
528
+ - Siblings swept: R0 broad-blast, R0 unparseable, R0 unknown shape, docs-only, empty diff and --guards-only all exited before the run step (all fixed).
529
+ - Mechanism: one finish() function that every exit path calls; with --run it executes the selection, prints "suites executed: N", and exits nonzero with "NOTHING RUN" when N is 0, except the documented empty diff.
530
+ - Fixture: tests/test-select-tests-run.sh (fake repo with a stub full suite; mutation: restore the broad-blast `exit 0` and the test goes red).
531
+
532
+ ## FC-48 The diff-selected gate skipped the repo-wide guards
533
+ - User saw: nothing yet. Raw: a slice touching only loki-ts/src/runner/attempts.ts was gated by the impacted-test planner, which selects by diff, so fc25_raw_spawn_guard and the other repo-wide guards never ran; a raw git spawn passed 3 reviews.
534
+ - Law: Engine Laws (a guard that only runs when its own file changes is a checklist, not a guard); D26 guard 5.
535
+ - Siblings swept: scripts/select-tests.sh (fixed, rule R8), scripts/ci/fast-gate.sh plan (fixed, appends --guards-only even when every path is R0-class), scripts/impacted-gate.sh (fixed, runs R8 bun guards), scripts/local-ci.sh --impacted (goes through impacted-gate.sh).
536
+ - Mechanism: scripts/global-guards.tsv is the one declared list; select-tests.sh emits it as R8 on every non-empty diff (docs-only included) and via --guards-only.
537
+ - Fixture: tests/test-select-tests.sh (attempts.ts-only diff and docs-only diff both select every declared guard; --guards-only lists them).
538
+
539
+ ## FC-50 Cold wasm render inside bun's default 5s test budget reddens the nightly (NIGHTLY-COCKPIT)
540
+ - User saw: the 11.3.1 cut blocked by D90 because Nightly was red since 10-04. Raw: run 37686475225, job "Bun tests on macos-latest bun=1.3.13 (nightly)", `(fail) cockpit render orchestration > renders a real inline image on a graphics terminal via the bundled wasm [5479.10ms]` followed by `this test timed out after 5000ms`; the next wasm test in the same file passed in 2142ms once warm.
541
+ - Law: evidence or it did not happen (a red gate is read from the log, then reproduced); never weaken the assertion or skip on macOS to turn a timeout green.
542
+ - Cause (measured): the first test in the file to rasterize pays the one-time cost of importing @resvg/resvg-wasm, reading the vendored .wasm, initWasm and loading the font (raster.ts caches all three per process), and a cold macOS runner spends over 5000ms on it, which is bun's default per-test timeout. No logic failed.
543
+ - Siblings swept: grep for rasterize, resvg and render( across loki-ts/tests finds only tests/cockpit.test.ts; its two real-render tests (inline image, font-bearing PNG) both got the explicit timeout. All other cockpit tests are pure string work.
544
+ - Mechanism: the cold one-time init is warmed in a beforeAll with its own 60s budget, and every test that performs a real rasterize passes an explicit RENDER_TIMEOUT_MS (30s); a test that does real wasm work must not rely on the 5s default.
545
+ - Fixture: loki-ts/tests/cockpit.test.ts. Red evidence: with RENDER_TIMEOUT_MS=1 and the warm disabled both render tests fail "timed out after 1ms"; with the fix 26 pass, 0 fail.
546
+
547
+ ## FC-51 A publish-path change silently dropped a field a later gate depends on (PROMOTE-GITHEAD)
548
+ - User saw: 11.3.1 stuck on the npm `next` tag; Promote run 37784566223 failed "FATAL: loki-mode@11.3.1 has no gitHead on npm", so the release never reached latest, Docker latest or Homebrew. Every later release would stick the same way.
549
+ - Law: evidence or it did not happen; a change to one stage must keep every field a downstream stage reads.
550
+ - Cause (measured): D90 (6247695f5) changed release.yml to `npm pack` then `npm publish <tgz>`. A tarball publish records no gitHead (only a directory publish reads .git); 11.3.0 and 11.2.0 have it, 11.3.1 does not. promote.yml requires gitHead to prove the build is an ancestor of origin/main.
551
+ - Siblings swept: grep of .github/workflows and scripts for gitHead finds only promote.yml (reader). The other publish in release.yml (loki-mode-sdk, `npm publish` from sdk/typescript, a directory publish) still records gitHead and promote does not read it, so it is unaffected. sbom.yml packs but does not publish.
552
+ - Mechanism: (1) release.yml stamps `git rev-parse HEAD` into the manifest before `npm pack`, restores package.json, and asserts the packed manifest gitHead equals HEAD. (2) promote.yml, when npm has no gitHead, falls back to the commit of tag v${VERSION} only if the tag is annotated (release.yml creates it with `git tag -a`), that commit has a successful Tests run on main, and the published tarball's loki-ts/dist/loki.js embeds the version; the source used is logged as GITHEAD_SOURCE. The 40-hex, ancestor-of-origin/main and never-move-latest-backwards checks apply to both paths unchanged.
553
+ - Fixture: tests/test-promote-head-stamp.sh (static check of both workflow definitions plus 8 negative mutations, one per required piece). Red on a0d599d0c (11 requirement failures), green after.
554
+
555
+ ## FC-53 A host-dependent or pipe-dependent assertion reddens the nightly on one OS (NIGHTLY-FLAKE)
556
+ - User saw: Nightly 37790534786 (a0d599d0c) red, blocking releases under D90. Raw: Bun macOS (both bun versions) `(fail) gate-failures cap head/tail parity (W4 L1) > doctor per-provider install hint goes to STDERR` "Expected: <= 1, Received: 2" at gate-failures-cap-parity.test.ts:81; and Full suite Shell shard 1/8 `TEST 1: basic onboard on loki-mode repo ... FAIL` after `echo: write error: Broken pipe`.
557
+ - Law: evidence or it did not happen; an assertion must test the property it names, not a host side effect; never retry, lengthen a sleep or skip to turn a gate green.
558
+ - Cause (measured): (1) The doctor test counted any stdout line containing "Install:". On macOS the system python3 is 3.9, so doctor prints a Python 3.12 warning ending "Install: brew install python@3.12", a second match. It failed on the same SHA in the dispatched run 37780519439 too (job 113322165616, same line), so it was never flaky: deterministic on macOS, hidden because that run's overall verdict was success. (2) `loki onboard` ran `find ... | wc -l` under `set -euo pipefail` (autonomy/loki cmd_onboard); when find exits non-zero on an unreadable or concurrently vanishing entry (other suites create and delete temp files in the repo root during shard runs) the whole command died silently, rc 1 and 0 bytes. Reproduced locally under 14-way parallel load, trace ends at the find|wc line; the same suite passed in run 37780519439 (job 113322165426). In the log the test also saw `echo: write error: Broken pipe` because test-onboard-command.sh used `echo "$output" | grep -q` under pipefail on a 37KB report.
559
+ - Siblings swept: all 11 `echo "$output" | grep -q` sites in test-onboard-command.sh converted. Other suites use the same idiom (grep over tests/*.sh finds it in ~100 files) but only misfire when the producer output exceeds the pipe buffer; none of those showed a nightly failure, so they are tracked by the flake tracker rather than bulk-edited. Bun tests: the only bare "Install:" counter in loki-ts/tests was the doctor test.
560
+ - Mechanism: (1) the doctor test counts only lines carrying a provider install command and requires the sole allowed one to be the claude fallback. (2) Product fix: the two find/grep count pipelines in cmd_onboard tolerate a non-zero producer (`|| true`, the count still comes from wc). Test fix: grep reads the variable through a here-string, which has no upstream writer in the pipeline status. Shared guard: scripts/nightly-flake-tracker.sh compares saved job logs per SHA and emits FLIP (pass in one run, fail in another on one SHA, with a BOARD-ready line) or STABLE-FAIL (red in every run, a real defect).
561
+ - Fixture: test-onboard-command.sh TEST 11 (unreadable directory makes find exit 1; red on the unguarded line with rc 1, green after) and tests/test-nightly-flake-tracker.sh (fixtures in tests/fixtures/nightly-flake). Replay of the real logs: `nightly-flake-tracker.sh SHA:a:<job 113356420172 log> SHA:b:<job 113322165616 log>` reports the doctor test as STABLE-FAIL.
562
+ ## FC-54 A path guard selected every file under tests/ and ran helpers as suites (MAIN-RED 6ecedaefe)
563
+ - Seen: Tests run 37792537759 on main went red on 4 fast-gate shards for a diff that only touched release.yml, promote.yml and tests/run-all-tests.sh. Raw: rows such as `py_test tests/lib/scan-workflow-rc.py` (rc=5, pytest collected nothing), `shell_test tests/shard-durations.tsv`, and `shell_test tests/run-all-tests.sh` (rc=124, the whole suite run bare).
564
+ - Law broken: a gate runs only what it can run; a selection row is a runnable test or it is not a row.
565
+ - Cause (measured): scripts/ci/fast-gate.sh guards_for wrote `grep -rlF -- PATTERN tests --include=...`. After `--` the --include flags are file operands, so the filter was silently dropped and any file under tests/ mentioning the workflow name was planned (py_test or shell_test by extension). Separately scripts/select-tests.sh match_kind accepted run-*.sh, which admitted the aggregate tests/run-all-tests.sh. Latent since D90; exposed when workflow files first changed together with run-all-tests.sh.
566
+ - Siblings swept: no other `grep ... -- ... --include` in scripts/, autonomy/ or tests/*.sh. tests/cli/test-alias-forwarding.sh is a legitimate selection with its own red, fixed by ALIAS-FWD 164eb7da2 on the train.
567
+ - Mechanism: options before `--` in the three guards_for greps; match_kind returns no kind for run-all-tests.sh.
568
+ - Fixture: tests/test-fast-gate.sh FC-54 case (release.yml + run-all-tests.sh + a registering test plans only test-*.sh, test_*.py, run[-_]*.sh). Red before, green after.
569
+
570
+
571
+ ## FC-60 Parallel slices each pass a repo-wide cap or allowlist alone, and the merged train is over it (TRAIN-GREEN)
572
+ - User saw: the 11.3.2 train (832fa4b57) was red before push on four repo-wide guards that each merged slice passed on its own base: loki-ts/tests/engine10/l0_guard.test.ts ("src/commands/verify_pr.ts npm test" and "package.json" unlisted), budget.test.ts (core engine10 5462 lines against the 5,000 cap; features 3158 against 3,000), and tests/cli/test-alias-forwarding.sh ("help: front-page entry count in [12,27] -- got 28").
573
+ - Law: evidence or it did not happen; D33 (core cap never raised), D66 (features cap), L0 (guard allowlist may only shrink without a stated reason).
574
+ - Cause (measured): at 6ecedaefe core is 4734 and features 2977 lines. Each slice (COST, RECEIPT-TRUTH, SARIF, AI-BOM, UNDO-2, VPR-2 and others) was reviewed against a base with room, and no merge step re-ran the repo-wide guards on the union. UNDO-2 added the 28th front-page command after ALIAS-FWD pinned the bound at 27. The VPR-2 package-manifest read in verify_pr.ts predates the guard being selected for it (FC-48).
575
+ - Siblings swept: budget.test.ts, l0_guard.test.ts, the alias-forwarding bound and tests/test-structural-checks.sh are the only cumulative caps in the fast tier. Core and features caps are NOT fixed here: D33 forbids raising the core cap, so the overage is handled by moving code out (EXT-CORE-2-r2 for core, EXT-FEAT-r2 for features), never by raising a cap.
576
+ - Mechanism: (1) l0_guard.allowlist gains two exact file-and-token rows for verify_pr.ts with a stated sunset (the PR own manifest test script is read as data); any other file or token is still red. (2) The alias bound moves 27 to 28 with the added command named in the comment. (3) Train rule: the merge of the last slice must run the guards named in this row on the union before push.
577
+ - Fixture: l0_guard.test.ts red then green; test-alias-forwarding.sh help bound red (got 28) then green.
578
+
579
+ ## FC-61 A test asserted on the first FAIL line, so one earlier real failure masked every later planted-defect check (TRAIN-GREEN)
580
+ - User saw: tests/test-structural-checks.sh "6 passed, 5 failed" with T6, T7 and T9 reporting "committed en dash not caught", "added line starting with + not scanned", "task NOTES.md dash not caught", although the emoji/dash check was working.
581
+ - Law: an assertion must test the property it names, not whichever line prints first.
582
+ - Cause (measured): line_of "$out" "FAIL" | head -1 returns the first failing check. The real line-budget failure (core 5421 >= 5000) prints before "emoji/dash on changes", so the grep for emoji/dash never saw its own FAIL line. T9 asserted rc 0 for the exempt case, which also failed for the unrelated budget reason. T2, T3 and T5 passed only because their checks print before line budgets.
583
+ - Siblings swept: all seven line_of call sites in the file now name their check label through failed_check; T1 stays the one assertion that the whole tree is clean.
584
+ - Mechanism: failed_check matches `^FAIL ... <label>` anywhere in the output.
585
+ - Fixture: with the budget red (as on the train), T6, T7, T9 are red before and green after; T1 remains red until the budget ruling.
586
+
587
+ ## FC-62 A test built an expected path from a raw $TMPDIR and macOS leaves a trailing slash (NIGHTLY-REDS)
588
+ - User saw: tests/test-caveman-flags.sh `bootstrap cwd custody` FAIL on macOS (exit=0, cwd=/var/folders/.../T/loki-caveman-XXXX/bootstrap-tmp/loki-caveman-bootstrap.YYYY).
589
+ - Law: an assertion must test the property it names, not a host path spelling.
590
+ - Cause (measured): macOS $TMPDIR ends in a slash, so `${TMPDIR:-/tmp}/loki-caveman-XXXXXX` became `T//loki-caveman-...`; the product records the canonical single-slash cwd, so the `case "$bootstrap_cwd" in "$BOOT_TMP"/...` pattern never matched although the bootstrap behaved correctly. Reproduced locally: 44 passed, 1 failed before; 45 passed, 0 failed after.
591
+ - Siblings swept: other suites use the same mktemp spelling but only this one compares the result against a recorded cwd string.
592
+ - Mechanism: the test strips one trailing slash from TMPDIR before mktemp.
593
+ - Fixture: tests/test-caveman-flags.sh section 5c is the regression (red on macOS before the strip).
594
+ - Note: the other two Nightly 37790534786 reds (doctor hint, onboard) were already fixed by FC-53 (0c0069df3, in this train); re-verified green here (bun test gate-failures-cap-parity 3 pass; test-onboard-command.sh 11/11).
@@ -850,3 +850,21 @@ previously mismarked two merged guards (S-16, S-74) as PENDING.
850
850
  6. `model_output_regex_guard.test.ts` (L0, FC-36 family): regex exec/match on model-output variables under engine10. Baselined: xreview VERDICT and session sentinel parsing (follow-up to schema-checked JSON).
851
851
  - **The test that proves it fires:** each file plants a violation in the working tree during review (mutation run recorded in the slice report); `full_env_spawn_guard` and `model_output_regex_guard` also carry in-file detector self-tests.
852
852
  Run: `cd loki-ts && bun test tests/util`.
853
+
854
+ ## 22. Repo-wide guards skipped by the diff-selected gate (GATE-GUARDS, FC-48)
855
+
856
+ - **Incident:** the CI-FAST planner selects tests by diff, so a slice touching
857
+ only `loki-ts/src/runner/attempts.ts` never ran `fc25_raw_spawn_guard`, and a
858
+ raw git spawn passed 3 reviews.
859
+ - **Root cause:** every guard was selected only when something it names
860
+ changed; repo-wide guards name nothing in a given diff.
861
+ - **Guard:** `scripts/global-guards.tsv` is the single declared list of
862
+ repo-wide guards (loki-ts/tests/util, spawn_env_guard, never_below_raw,
863
+ l0_guard, structural checks, no-hardcoded-paths, moat P9).
864
+ `scripts/select-tests.sh` emits it as rule R8 on every non-empty diff,
865
+ docs-only included, and via `--guards-only`; `scripts/ci/fast-gate.sh plan`
866
+ and `scripts/impacted-gate.sh` consume it. Add a guard by editing that file
867
+ only.
868
+ - **The test that proves it fires:** `tests/test-select-tests.sh` (R8 cases):
869
+ an attempts.ts-only diff and a docs-only diff each select every declared
870
+ guard. Run: `bash tests/test-select-tests.sh`.
package/docs/v10/GUIDE.md CHANGED
@@ -17,9 +17,10 @@ longer read; any value is ignored.
17
17
 
18
18
  `loki start owner/repo#N`, `loki start <issue URL>` and `loki start "<multi-word
19
19
  task>"` run Loki 10, the same as `loki owner/repo#N` and `loki "<task>"`. A PRD
20
- file, a flag-first call (`loki start --simple prd.md`) and a one-word start stay
21
- on the legacy engine. The legacy engine is being removed (planned work
22
- resumes 2026-10-07, see docs/v10/LEGACY-REMOVAL.md).
20
+ file, a flag-first call (`loki start --simple prd.md`) and a one-word start still
21
+ run on the previous bash loop (autonomy/run.sh), and so does the opencode
22
+ provider, until FC38-SWEEP ports them. `loki legacy` and LOKI_ENGINE remain
23
+ removed. The previous loop is being removed (see docs/v10/LEGACY-REMOVAL.md).
23
24
 
24
25
  Some pieces named in this guide are still being built. Each one below says
25
26
  so plainly instead of describing a finished feature.
@@ -322,7 +323,8 @@ runs on the B9 corpus, and it can emit shape defaults from those runs
322
323
 
323
324
  D48 made v10 the default for `loki "<task>"`, `loki owner/repo#N` and
324
325
  `loki quick "<task>"`. 11.0.0 removed the engine switch: `loki legacy` exits
325
- 2 with a removal message and LOKI_ENGINE is ignored. Remaining legacy paths
326
- (`loki start ./prd.md` and the no-bun fallback) are tracked in
326
+ 2 with a removal message and LOKI_ENGINE is ignored. PRD-file starts
327
+ (`loki start ./prd.md`), the opencode provider, and the no-bun
328
+ fallback still run on the previous bash loop until FC38-SWEEP ports them; they are tracked in
327
329
  docs/v10/LEGACY-REMOVAL.md. The marked line near the top of this file and of
328
330
  README.md's Loki 10 section records the current state.
@@ -595,3 +595,15 @@ Cost per completed task for sonnet arms measured before this date used the old 3
595
595
  | 10.9.1 | BLOCKED | $0.75 | brief handicap, FC-19 class |
596
596
  | 10.10.3 | PARTIAL | $2.27 | 12m; full migration (83 files); backend 5855/5858 green; implement stopped at a 900s run cap and verify was skipped (FC-21); run e10-20261003T220847Z-78fa, main be0799f, reported by steering 22:08Z |
597
597
  | 10.10.5 | PARTIAL (CLI and receipt match) | $1.73 | run e10-20261003T233414Z-30ca, defaults, cap_s 2700, 12m05s, 2.42M tokens; implement 6m36s finished with no limit; verify, fix, verify ran; 77 files +4325/-2006; independent check: backend 5914 pass 0 fail 29 skip, tsc 0 errors; PARTIAL only from FC-22 (4 frontend tests over-selected, deps absent, honestly NOT PROVEN) and FC-23 (a Wall file failed the package tsc, TS1470); integration request-approval-race NOT PROVEN (needs DB). Zero false claims. |
598
+
599
+ ## B9 raw vs loki (D91 COST-HALF and 10x metric owner)
600
+
601
+ `bash scripts/b9-scoreboard.sh --ab [--n 3] [--model M] --version V --json-out F --metrics-out docs/v10/METRICS.md`
602
+ runs `claude -p` (raw) and `loki start` (loki) on the same task text. cost_ratio = (loki usd per VERIFIED and hidden-check passing task) /
603
+ (raw usd per task solved), failed-run cost included; correctness_ratio = loki solve rate / raw solve rate, both judged by
604
+ the same hidden checks; wall_ratio = mean loki wall / mean raw wall; 95% percentile bootstrap over runs within each
605
+ (arm, fixture) cell. Raw cost and time come only from the claude SDK result line (`total_cost_usd`, `duration_ms`, cache read/creation tokens); loki cost and time only from the receipt (`cost.usd`, `time.total_s`, `cost.cache_read_tokens`, `cost.cache_creation_tokens`). The old `time.wall_s` and `cost.input_tokens` are never read as totals. A missing field, or `total_s` differing from the sum of `time.stages` by more than 1%, reads NOT RECORDED.
606
+
607
+ | Date | Run | Fixtures | Result |
608
+ |---|---|---|---|
609
+ | 2026-10-08T04:58Z | b9-ab n1-plumbing (SUPERSEDED) | trivial-sum | SUPERSEDED, do not cite. Used our own clock for wall and the old receipt fields: its loki wall (43s) was not the receipt `time.wall_s`/`total_s` and `time.wall_s` is invalid as a total (it is a stage sum that excludes boot and seal). Rerun on SDK `duration_ms` and receipt `time.total_s` once RECEIPT-TRUTH ships. Former text: cost_ratio=0.92 wall_ratio=3.07 n=1/1, not significant. |
@@ -31,9 +31,9 @@ The CEO is Lokesh Mure (Loki), the founder. Everyone else is an agent. The compa
31
31
  ## Release pipeline
32
32
  - Tier A fast gate on every change (target 60-120s): syntax checks, changed-file shellcheck, diff-selected tests, moat suite split per property. It is feedback, not release authority.
33
33
  - Tier B: every main commit has a full Tier B verdict on its exact tree, run on main or on the train that produced it (D55); never cancelled on main.
34
- - Release = a lookup: publish a commit whose tree already has a Tier B pass. A VERSION-only bump reuses its parent's verdict. Target: 2 min from verified to npm.
35
- - Clock cuts (D91): one release every 45 min from whatever is approved and green on main; no cut waits for a slice. Target per cut: >=3 features and >=5 enhancements or fixes, user-facing CHANGELOG. Approved slices merge to main continuously (tsc, impacted tests, global guards, P9); risky work merges flag-gated. A moat regression still blocks a cut.
36
- - If Tier B fails on main: stop releases, open a P0 fix-forward slice. Never unpublish; npm deprecate is allowed.
34
+ - Every approved slice is a release (D96): merge-gate merges to main, and main publishes a PATCH to npm `next` within 15 min. No trains, no batching. Target 3-10 releases per hour.
35
+ - `latest` is promotion only: the newest `next` whose SHA has a green hourly full-suite backstop and green Post-Release Smoke. A red backstop blocks promotion, not publishing.
36
+ - If the backstop fails on main: promotion stops, and a P0 fix-forward slice opens within the hour. Never unpublish; npm deprecate is allowed.
37
37
 
38
38
  ## Large tasks (for example "rebuild the dashboard")
39
39
  - The Architect splits the task into 10-40 slices behind a feature flag.
@@ -2297,3 +2297,9 @@ The swarm is stopped: .loki/V10-STOP exists, the loop has no wake-ups, and the c
2297
2297
  - PLAN-TIMEOUT engineer had no edits from 07:40Z to 08:14Z; re-pinged with a commit-now instruction.
2298
2298
 
2299
2299
  - 2026-10-08T10:27Z CoS: PLAN-TIMEOUT-1 merged by SHA b43536756 (TL APPROVE: fast-gate 10, train-verdict-reuse 40, select-tests 42, rc 0; git diff approved..HEAD^2 empty). Full local tier on 82eb1ad31 hit the 45-min cap (rc 124); reds classified env-only (receipt_attest needs cryptography; indexer + typecheck worktree-path only, green in main). bun 2004/0, util 175/0, moat rc 0 5/9. Lost ~1h: TL verdict sat in an idle teammate; next time read the subagent transcript after 10 min instead of waiting. Pushing the 11.3.1 train now.
2300
+
2301
+ ## 2026-10-08 12:21Z CoS
2302
+ - 11.3.1 (604145bbc, pushed) release run 37774482965 failed at required-ci NIGHTLY-BLOCK (D90): nightly 37686475225 red since 10-04 on one test, macOS bun cockpit "renders a real inline image ... via the bundled wasm" timed out after 5000ms (cold wasm+font load). npm 11.3.1 NOT published.
2303
+ - Fix-forward NIGHTLY-COCKPIT (row FC-50, beforeAll warm-up + 30s render timeout, assertions unchanged): built f7f48a120, cockpit 26/0; TL round 1 BLOCK only on FC-47 id collision, r2 is the rename, in review. Then: merge, push, dispatch nightly, FULL rerun of 37774482965.
2304
+ - 11.3.2 train on local main (unpushed), now e18e4ca11: 12 earlier slices plus 133-B4..ER-01-r2, 133-E3-r2, SIGS-DOC (run-all-tests.sh registration union by hand), VPR-1-r2. UNDO-2-r2 merge reverted locally (tripped raw-gh and full-env spawn guards), r3 in HIGH review.
2305
+ - Product-code conflicts sent to engineers: XV-1 (seal.ts), MARK-1 (pr.ts), MASS-2 (queue.ts/pr.ts). BLOCKs back to engineers: MCP-D (unauthenticated HTTP task creation), VPR-2-r2 (gh authorAssociation field does not exist), T5-ATTEMPTS-PR-r2 (stale help line, r3 in review).
@@ -0,0 +1,77 @@
1
+ # Draft: an in-toto predicate for AI-agent code changes
2
+
3
+ Status: DRAFT proposal by the Loki Mode project. It is not accepted, adopted or standardized by in-toto or any other body, and nothing here claims otherwise. No upstream pull request exists, and none is referenced. Loki is the only implementation of this draft's source data (its receipt), so the draft is a reference for discussion, not an ecosystem convention.
4
+
5
+ This follows the shape the in-toto attestation project asks of new predicates (a name, a type identifier, a stated purpose, a field list, a worked example). Check the current in-toto "New Predicate Guidelines" before any submission; this document does not restate them.
6
+
7
+ ## Name and type identifier
8
+
9
+ - Name: Agent Change.
10
+ - Proposed predicateType: `https://autonomi.dev/loki/agent-change/v0`. This is an identifier only. It is a placeholder in the namespace Loki already uses for its DSSE export (`https://autonomi.dev/loki/receipt/v10`, see `docs/AGENT-CHANGE-RECEIPT.md`). It is not registered anywhere and is not claimed to resolve.
11
+ - Statement: in-toto Statement v1. Subject: the resulting git commit, `gitCommit` digest (and `gitTree`), as Loki's DSSE export already does.
12
+
13
+ ## Purpose
14
+
15
+ State, in a signable and machine-checkable form, what an automated agent changed in a repository, which task and models were involved, what was checked, what the outcome was, and, as a first-class list, what was not proven. A consumer (a reviewer, a policy engine, an admission check) can then decide whether to trust the change without reading the agent's transcript.
16
+
17
+ ## Fields
18
+
19
+ "Receipt source" names the field in the Loki receipt (documented in `docs/AGENT-CHANGE-RECEIPT.md`). "new" means the current receipt does not record it, so the draft would need the producer to add it.
20
+
21
+ | Predicate field | Meaning | Receipt source |
22
+ |---|---|---|
23
+ | `task.source` | `text` or `issue` | `task` |
24
+ | `task.sha256` | digest of the task text; the text is not included | `task` |
25
+ | `baseCommit` | commit the change starts from | `base_sha` |
26
+ | `headCommit` | resulting commit | `head_sha` |
27
+ | `diffSha256` | digest of the diff between them | `diff_sha256` |
28
+ | `agent.name` | agent product identity | new |
29
+ | `agent.provider` | model provider or CLI | `provider` |
30
+ | `models` | model identifiers used | `model` |
31
+ | `tools` | tools the agent used | new |
32
+ | `mcpServers` | MCP servers the agent could call | new |
33
+ | `checks` | list of `{name, cmd, result}` | `checks` |
34
+ | `outcome` | `VERIFIED`, `PARTIAL`, `ALREADY_SATISFIED`, `SPEC_CONFLICT` or `FAILED` | `verdict` |
35
+ | `notProven` | what the run did not prove | `not_proven` |
36
+ | `receiptSha256` | digest of the full producer receipt, when one exists | `receipt_sha256` |
37
+
38
+ Rules: `notProven` MUST be present, and an empty list is a claim that nothing was left unproven. A producer that cannot know a field omits it; it never writes a placeholder. `tools` and `mcpServers` are `new` because no existing receipt field carries them; until a producer records them they are omitted.
39
+
40
+ ## Worked example
41
+
42
+ Derived from the Loki test fixture `packages/control-plane/test/fixtures/runs/verified-pr/receipt.json` (a stubbed CLI-invoker run, so cost is unmetered). Hashes are copied from that fixture. The signature is omitted here; a real attestation is wrapped in a DSSE envelope.
43
+
44
+ ```json
45
+ {
46
+ "_type": "https://in-toto.io/Statement/v1",
47
+ "subject": [
48
+ { "name": "git+commit:7421fae2f14df16949a06dbcec18628bb45e50f0", "digest": { "gitCommit": "7421fae2f14df16949a06dbcec18628bb45e50f0" } }
49
+ ],
50
+ "predicateType": "https://autonomi.dev/loki/agent-change/v0",
51
+ "predicate": {
52
+ "task": { "source": "text", "sha256": "982a130e3cb8ef5642b3f3fa83f9e564126fba16a73537bb1251b4328a2fa3b0" },
53
+ "baseCommit": "ebc3d8091bfe6e75393a26c719aa7535247044f9",
54
+ "headCommit": "7421fae2f14df16949a06dbcec18628bb45e50f0",
55
+ "diffSha256": "eea5ea2cb26cec5473c1b2cec4195de8b17a1733b70ebbdcc4ede90eb58a7e30",
56
+ "agent": { "name": "loki", "provider": "claude" },
57
+ "models": ["sonnet"],
58
+ "checks": [
59
+ { "name": "bun:calc.test.ts", "cmd": "bun test calc.test.ts", "result": "pass" }
60
+ ],
61
+ "outcome": "VERIFIED",
62
+ "notProven": ["full suite", "app boot", "council", "security scan", "cost unmetered (CLI invoker; recorded as 0)"],
63
+ "receiptSha256": "04e777f6b9b3c854b5d1329ec1f8d96ee8ff78ac7aa432de63fbcf0d323b729e"
64
+ }
65
+ }
66
+ ```
67
+
68
+ ## Open questions
69
+
70
+ - Whether `models` should carry provider-qualified identifiers; the fixture records the short name `sonnet`.
71
+ - Whether `outcome` should be a smaller vocabulary than Loki's five verdicts.
72
+ - Whether `tools` and `mcpServers` belong in this predicate or in a separate inventory attestation.
73
+ - Relationship to SLSA provenance (Loki's SIGS-1 slice maps a receipt onto SLSA v1 instead); this draft complements it and does not replace it.
74
+
75
+ ## Reference implementation
76
+
77
+ Loki Mode produces the source receipt and can export a Statement v1 with its own predicate today (`loki verify --export-dsse`). Emitting this draft predicate is not implemented.
@@ -0,0 +1,55 @@
1
+ # MCP 2026-07-28: what applies to Loki, and who implements it
2
+
3
+ Slice MCP-0. Documentation only; no code changed. Written 2026-10-08.
4
+ Every claim is marked VERIFIED (read from the cited source on the fetch date) or UNVERIFIED (inferred, not run).
5
+
6
+ ## Sources
7
+
8
+ | Source | URL | Fetched |
9
+ |---|---|---|
10
+ | Spec "Key Changes" (changelog vs 2025-11-25) | https://modelcontextprotocol.io/specification/2026-07-28/changelog | 2026-10-08 |
11
+ | PyPI `mcp` JSON | https://pypi.org/pypi/mcp/json | 2026-10-08 |
12
+ | PyPI `mcp` 2.3.0 wheel and `mcp-types` 2.3.0 wheel (pip download, files read) | https://pypi.org/project/mcp/2.3.0/ | 2026-10-08 |
13
+ | npm `@modelcontextprotocol/sdk` (registry via `npm view`, tarball 1.32.1 read) | https://www.npmjs.com/package/@modelcontextprotocol/sdk | 2026-10-08 |
14
+ | npm `@modelcontextprotocol/server` and `@modelcontextprotocol/client` (`npm view`) | https://www.npmjs.com/package/@modelcontextprotocol/server | 2026-10-08 |
15
+
16
+ ## SDK versions found
17
+
18
+ - Python SDK latest version found: mcp 2.3.0, uploaded 2026-10-02 (VERIFIED, PyPI JSON). Also on PyPI: 2.2.0 (2026-09-07), 1.30.0 (2026-09-07). The installed copy on the build machine is mcp 1.27.0 (VERIFIED, `importlib.metadata`).
19
+ - Does Python mcp 2.3.0 implement 2026-07-28: its `mcp-types` 2.3.0 wheel lists "2026-07-28" in `MODERN_PROTOCOL_VERSIONS` (`mcp_types/version.py`), and the `mcp` wheel contains `server/_streamable_http_modern.py` and references to `server/discover` (VERIFIED, files read). Runtime behavior was not exercised: UNVERIFIED.
20
+ - Loki pins `mcp>=1.0.0,<2.0.0` in `mcp/requirements.txt`, so a fresh install can never pick up 2.x (VERIFIED, repo file). 2.3.0 requires Python >=3.10 and new dependencies (`mcp-types`, `httpx2`, `pyjwt`); compatibility with Loki's `mcp/_sdk_loader.py` is UNVERIFIED.
21
+ - npm `@modelcontextprotocol/sdk` latest: 1.32.1, modified 2026-10-05 (VERIFIED, `npm view`). Its README states it implements the spec up to 2025-11-25 and that 2026-07-28 support "is not planned for v1.x"; it lives in v2 as separate packages (VERIFIED, README in tarball).
22
+ - npm v2 packages `@modelcontextprotocol/server` and `@modelcontextprotocol/client`: latest 2.3.1, modified 2026-10-05 (VERIFIED, `npm view`). Whether they implement every item below: UNVERIFIED.
23
+
24
+ ## Conformance table
25
+
26
+ "Applies" means a requirement on a stdio tool server plus a client such as Loki's (`mcp/server.py`, `mcp/lsp_proxy.py`, `src/protocols/mcp-server.js`, `src/protocols/mcp-client.js`).
27
+
28
+ | Spec change (2026-07-28) | Applies to Loki | Status of claim |
29
+ |---|---|---|
30
+ | Stateless protocol: the `initialize` / `notifications/initialized` handshake is removed (initialize removal); each request carries protocol version and client capabilities in `_meta` (`io.modelcontextprotocol/protocolVersion`, `io.modelcontextprotocol/clientCapabilities`) | Yes, server and client | VERIFIED (changelog) Slice MCP-C: JS server done (no initialize needed, `_meta` version checked, no session stored); behind LOKI_MCP_2026_07=1. |
31
+ | `server/discover` RPC: servers MUST implement it; clients MAY call it first or use it as a stdio compatibility probe | Yes, server MUST | VERIFIED (changelog) Slice MCP-C: JS server done (`supportedVersions`, `serverInfo`, `capabilities`); result field names are our reading, UNVERIFIED against the spec schema. |
32
+ | Multi Round-Trip Requests (MRTR): `InputRequiredResult` (`resultType: "input_required"`) with `inputRequests`, client retries with `inputResponses`; replaces server-initiated `roots/list`, `sampling/createMessage`, `elicitation/create`. All results carry `resultType` | Yes if Loki ever asks the client for input; today it does not (UNVERIFIED, inferred from tool list) | Spec VERIFIED (changelog) |
33
+ | Tasks moved out of core to the extension `io.modelcontextprotocol/tasks` (polling via `tasks/get`, new `tasks/update`, no `tasks/list`) | Only if Loki exposes long-running tools as tasks; optional | VERIFIED (changelog) |
34
+ | OpenTelemetry trace context in `_meta`: `traceparent`, `tracestate`, `baggage` (traceparent in _meta) | Optional; useful with the OTEL-GENAI program | VERIFIED (changelog) |
35
+ | Deprecated: Roots, Sampling, Logging (`logging/setLevel` removed; level per request via `io.modelcontextprotocol/logLevel`), HTTP+SSE transport, OAuth Dynamic Client Registration (DCR) in favor of Client ID Metadata Documents | Loki implements none of Roots/Sampling/Logging as a server; DCR applies only to remote HTTP auth (`tests/test-mcp-http-auth.sh`) | VERIFIED (changelog); Loki usage UNVERIFIED beyond grep |
36
+ | Streamable HTTP: sessions and `Mcp-Session-Id` removed; `subscriptions/listen` replaces GET and `resources/subscribe`; `Mcp-Method` and `Mcp-Name` headers required on POST; SSE resumability removed | Only for the HTTP transport; stdio unaffected | VERIFIED (changelog) |
37
+ | `ping` and `notifications/roots/list_changed` removed; `ttlMs` and `cacheScope` required on list results; deterministic `tools/list` order; resource-not-found code now -32602 | Yes for the server's list endpoints | VERIFIED (changelog) |
38
+ | Error code renumbering: HeaderMismatch -32020, MissingRequiredClientCapability -32021, UnsupportedProtocolVersion -32022 | Yes once the stateless profile is implemented | VERIFIED (changelog) Slice MCP-C: -32022 UnsupportedProtocolVersion done; the other two codes are not used yet. |
39
+
40
+ ## Consequences for the plan
41
+
42
+ - Python side (MCP-B, MCP-C): do not hand-roll the 2026-07-28 profile. A pip release that carries it exists (2.3.0), so the path is an SDK bump behind a test, not new protocol code. Raising the `<2.0.0` ceiling needs a compatibility run first (UNVERIFIED today).
43
+ - JS side (MCP-A, MCP-C): the hand-rolled server cannot get 2026-07-28 from `@modelcontextprotocol/sdk` 1.x; the v2 packages are the candidate. Adding a dependency needs its own decision.
44
+ - MCP-A (legacy version negotiation fixes) is independent of this document.
45
+
46
+ ## MCP-C status (JS server)
47
+
48
+ - `server/discover` is answered always; the version list includes 2026-07-28 only when `LOKI_MCP_2026_07=1` (default off until every row above passes).
49
+ - `tools/list`, `tools/call` and the other methods work with no prior `initialize`. A `_meta["io.modelcontextprotocol/protocolVersion"]` outside the advertised list gets -32022 with `data.supported`.
50
+ - `initialize` for legacy clients is unchanged; its fallback stays 2025-11-25, never 2026-07-28. No session id is stored. `tools/call` keeps the existing auth check, which runs before the version check.
51
+ - Not done: `resultType`, `ttlMs`/`cacheScope`, MRTR, Tasks, `Mcp-Method` headers, `subscriptions/listen`. Python side: SDK-only, no hand-rolled protocol (see Consequences).
52
+
53
+ ## Method
54
+
55
+ Changelog text read from the fetched page on 2026-10-08. PyPI facts from the JSON API. Wheel and tarball contents read after `pip download --no-deps` and `npm pack` into scratch directories (not installed, not executed).
@@ -0,0 +1,41 @@
1
+ # VERIFY-PR sandbox: spec, threat model, runner contract (VPR-1)
2
+
3
+ Runner: `scripts/verify-pr-sandbox.sh`. Test: `tests/test-verify-pr-sandbox.sh`. Consumer: VPR-2 (`loki verify-pr`).
4
+
5
+ ## Rule of Two
6
+ The verify job holds untrusted input (the PR code). It holds NO token and has NO network. A separate job that never executes PR code holds `checks: write`, reads only the result file, and posts. This runner is the first job's execution boundary.
7
+
8
+ ## Contract
9
+ ```
10
+ verify-pr-sandbox.sh --repo DIR --out DIR --cmd 'SHELL CMD' --image IMG [--timeout S] [--max-output B] [--print-argv]
11
+ ```
12
+ - Exit 0: the check ran; read `result.json` (`status` COMPLETED, TIMEOUT or ERROR, plus `exit_code`).
13
+ - Exit 2: input refused. Exit 3: BLOCKED (docker absent, daemon down, image not local, resource exhaustion). BLOCKED says nothing about the PR and VPR-2 must never map it to a verdict.
14
+ - `OUT/result.json` and `OUT/output.txt` are the only files written, both by the host. `output.txt` is the check's captured output, capped at `--max-output` bytes, with the sentinel line removed; treat it as untrusted data. result.json fields: status, exit_code, output_truncated, detail, image, timeout_s.
15
+ - The image is never pulled (`--pull never`); dependencies must already be in the image or the caller reports NOT PROVEN.
16
+
17
+ ## Controls
18
+ | Control | Mechanism |
19
+ |---|---|
20
+ | No network | `--network none` |
21
+ | Read-only root | `--read-only`; `/work` (exec), `/cap`, `/tmp` are size-capped tmpfs |
22
+ | Non-root | `--user 65534:65534`, `--cap-drop ALL`, `no-new-privileges` |
23
+ | Caps | `--pids-limit 256`, `--memory 512m` (swap equal), `--cpus 1`, `--init` |
24
+ | Wall timeout | in-container `timeout -s KILL`, plus a host watchdog that runs `docker kill <exact name>` (never by pattern) |
25
+ | Env scrub | `docker run` inherits no host env; only PATH, HOME=/work, CI=1 are passed. GH_TOKEN, GITHUB_TOKEN, SSH_AUTH_SOCK never reach argv |
26
+ | Mounts | exactly one: the repo, read-only. No HOME, no docker socket, no output mount |
27
+ | Output | stdout is captured by the host, capped in-container (`--max-output`); the host writes result.json and output.txt |
28
+
29
+ Deviation from the card: there is no mounted output dir. A writable host mount would let the checked code fill the host disk, so the result travels on stdout and only the host writes `OUT`. Numeric flags are forced to base 10 (a leading zero is not octal) and every value flag requires a value. `--image` is restricted to `[A-Za-z0-9._/:@-]`.
30
+
31
+ ## Threat model
32
+ - Malicious package.json scripts / test code: run only inside the container, as nobody, no network.
33
+ - Git hooks: the repo is mounted read-only and the runner never runs git; checkout of base/head is VPR-2's job and must use `util/safe_git.ts`.
34
+ - Symlinks out of the repo: refused on the host before launch (absolute target, or relative target resolving outside the repo). The copy to `/work` happens inside the container, so a link resolves against the container filesystem only.
35
+ - Oversized output: capped by `--max-output`; `output_truncated` is set.
36
+ - Fork bomb: `--pids-limit 256`. Memory bomb: `--memory`. Disk fill: tmpfs size caps.
37
+ - Forged result: the checked code can print a fake `LOKI_VPR_END` line. A per-run 128-bit nonce is sent to the wrapper on stdin and the checked command runs with stdin from /dev/null; the host accepts only the last line carrying that nonce. Docker's default seccomp profile does NOT block ptrace or process_vm_readv on kernels >= 4.8, and the checked command runs as the same uid as the wrapper inside the container, so a hostile process can read the wrapper's memory and recover the nonce. The nonce therefore only stops accidental or naive forgery; it is not a security boundary. One cross-check is added: the wrapper exits with the check's code and the host rejects (status ERROR) a sentinel that disagrees with docker's own exit code. The PR code can still make its own check report success, which is why `result.json` is a signal about the check and not the final verdict (VPR-2 owns that, and it must not treat a bare COMPLETED exit 0 as proof). A custom seccomp profile denying ptrace is a possible hardening and is not shipped here.
38
+ - Docker pool exhaustion: reported as BLOCKED, never as a failing check.
39
+
40
+ ## Not covered (residual, tracked for VPR-2 review)
41
+ Kernel or runtime escapes (use gVisor or a microVM runtime for a hosted deployment); Docker Desktop shares the VM across containers; the hostile-container part of the test needs a daemon and a local image and SKIPs loudly otherwise.