omnilane 0.42.5 → 0.42.6

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.
@@ -6,14 +6,14 @@
6
6
  },
7
7
  "metadata": {
8
8
  "description": "One routing table, native-first or CLI delegation, supervised completion workflows.",
9
- "version": "0.42.5"
9
+ "version": "0.42.6"
10
10
  },
11
11
  "plugins": [
12
12
  {
13
13
  "name": "omnilane",
14
14
  "source": "./",
15
15
  "description": "Route each subtask to the model that fits, then delegate through a compatible caller-owned native agent or vendor CLI.",
16
- "version": "0.42.5",
16
+ "version": "0.42.6",
17
17
  "category": "development",
18
18
  "keywords": [
19
19
  "routing",
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "omnilane",
3
- "version": "0.42.5",
3
+ "version": "0.42.6",
4
4
  "description": "One routing table, every harness: classify subtasks into lanes and delegate through compatible caller-owned native agents or vendor CLIs with exact-AA downward policy and supervised jobs.",
5
5
  "author": {
6
6
  "name": "Seraphim0916"
package/CHANGELOG.md CHANGED
@@ -6,6 +6,66 @@ semantic version tags.
6
6
 
7
7
  ## [Unreleased]
8
8
 
9
+ ## [0.42.6] - 2026-09-10
10
+
11
+ ### Added
12
+
13
+ - Every transport overlay mapping now records an `evidence_tier` saying how
14
+ strongly its probe identified the responder. `billed-model` is the provider
15
+ naming the model it charged for (Claude's `modelUsage`, grok's under
16
+ `--output-format json`); `client-echo` is the CLI recording the model it asked
17
+ for (codex's session rollout, agy's `cli.log` resolver line); `selector-only`
18
+ is a CLI accepting the selector and reporting nothing further. A verified
19
+ mapping previously flattened these into one word, which overstated two of the
20
+ four vendors in the same way the gate itself did before 0.42.5.
21
+ - `omnilane doctor` reports the tier alongside each vendor's verified count and
22
+ names the vendors worth re-probing.
23
+
24
+ ### Changed
25
+
26
+ - The tier is derived from what a run produced rather than assigned per vendor,
27
+ so evidence written before this release re-judges as `selector-only` and a CLI
28
+ that begins reporting a billed model is promoted with no code change.
29
+ - `probe.py` gathers each CLI's own on-disk record of the request — codex through
30
+ the `thread.started` id that names its rollout, agy through the app data
31
+ directory prepared the way `run-gemini.sh` prepares it — and stores a digest of
32
+ the model-bearing lines beside the streams, so the judgement stays a pure
33
+ function that can re-judge an old sweep offline.
34
+ - Grok probes accept the billed model only as an exact match or with the single
35
+ `-build` suffix the provider adds; a prefix test would let `grok-4.6-anything`
36
+ pass as `grok-4.6`.
37
+
38
+ ### Fixed
39
+
40
+ - The overlay anchored vendor executables by paths written into
41
+ `build_overlay.py`, which drift out of use without failing: the live overlay
42
+ hashed claude `2.1.263` while every dispatch ran `2.1.266`, so eleven mappings
43
+ were verified against a binary that had not run in a day. Core evidence now
44
+ anchors the executable the runners resolve.
45
+ - The last two mappings carrying `PRIOR:` references instead of probe evidence
46
+ were re-probed, so every signed mapping now has a descriptor a reader can open.
47
+ - A failing codex probe recorded `exit-code: 1` and nothing else. Under
48
+ `exec --json` the refusal that ends a run is an stdout event, so the reason
49
+ never reached `unproven[]`.
50
+
51
+ ### Removed
52
+
53
+ - The three `gpt-5.4-mini` mappings. The model passed its probe on 2026-09-07
54
+ and now returns HTTP 400 — "not supported when using Codex with a ChatGPT
55
+ account" — so those configurations move to `unproven[]` carrying that reason.
56
+ A signed overlay does not notice a lane dying upstream; only a re-probe does.
57
+ The overlay now holds 46 mappings and 9 unproven configurations.
58
+
59
+ ### Notes
60
+
61
+ - The tier is reported and never enforced. Dispatch continues to turn on
62
+ `runtime_verified` alone, covered by a test asserting every decision is
63
+ byte-identical under all three tiers.
64
+ - agy and grok update themselves in the background when invoked; agy's `cli.log`
65
+ records `auto_updater.go: Spawned background update process`. Overlay evidence
66
+ drift is a routine consequence of using a vendor rather than an operator
67
+ action, and per-vendor degradation is what keeps that from stopping work.
68
+
9
69
  ## [0.42.5] - 2026-09-09
10
70
 
11
71
  ### Fixed
package/README.ja.md CHANGED
@@ -573,6 +573,16 @@ work の別名ではありません。サービス管理など、work の境界
573
573
 
574
574
  ## 📜 リリース履歴
575
575
 
576
+ ## v0.42.6 の新機能
577
+
578
+ - **「検証済み」が、どう検証されたかを示すようになりました。** 各 overlay マッピングは `evidence_tier` を持ちます。`billed-model` はプロバイダー自身が課金対象のモデル名を返した場合(claude、grok)、`client-echo` は CLI が自ら送信したモデルを記録した場合(codex、agy)、`selector-only` は CLI がセレクターを受け付けただけの場合です。`client-echo` は CLI が控えた注文書、`billed-model` はプロバイダーが発行した領収書です。
579
+ - **報告するだけで、拒否はしません。** ディスパッチは従来どおり `runtime_verified` のみで判断するため、ティアが低くても動いていたレーンが拒否されることはありません。3 つのティアすべてで判定が変わらないことをテストで保証しています。
580
+ - **ティアはベンダーではなく証拠に従います。** 本リリース以前のプローブは `selector-only` として再判定され、課金モデルを返すようになった CLI はコード変更なしに昇格します。
581
+ - **`omnilane doctor` が内訳を表示**し、再プローブすべきベンダーを名指しします。
582
+ - **overlay は実際に実行されるバイナリを固定します。** 従来はパスが `build_overlay.py` に直書きされ、使われていないバージョンを黙って指していました。実際には claude `2.1.266` が実行されているのに、overlay は `2.1.263` をハッシュしていました。
583
+ - **失われた 3 レーンを検出。** `gpt-5.4-mini` は 2026-09-07 のプローブでは通っていましたが、現在は HTTP 400(ChatGPT アカウントの Codex では非対応)を返します。署名済みの overlay はレーンが上流で失われても気付きません。該当の 3 構成は理由付きで `unproven[]` に移り、マッピングは 46 件になりました。
584
+ - **アップグレード。** npm 公開後は `npm i -g omnilane@0.42.6` を実行してください。
585
+
576
586
  ## v0.42.5 の新機能
577
587
 
578
588
  - **1 つの CLI 更新が全ベンダーを止めることはなくなりました。** overlay の evidence に `vendor` タグを付与でき、タグ付きエントリのハッシュ差異やファイル消失は当該ベンダーのみを `unknown-target-runtime` に降格させます。タグなしの evidence は従来どおり全体を fail-closed にします。
package/README.ko.md CHANGED
@@ -558,6 +558,16 @@ doctor가 파일과 벤더를 지목하며, 재서명 절차는 디스패치 스
558
558
 
559
559
  ## 📜 릴리스 기록
560
560
 
561
+ ## v0.42.6 새 기능
562
+
563
+ - **"검증됨"이 어떻게 검증했는지도 알려줍니다.** 각 overlay 매핑은 `evidence_tier`를 가집니다. `billed-model`은 공급자가 과금한 모델을 직접 밝힌 경우(claude, grok), `client-echo`는 CLI가 자신이 보낸 모델을 기록한 경우(codex, agy), `selector-only`는 CLI가 셀렉터를 받아들이기만 한 경우입니다. `client-echo`는 CLI가 적어 둔 주문서이고, `billed-model`은 공급자가 발행한 영수증입니다.
564
+ - **보고만 하고 막지 않습니다.** 디스패치는 여전히 `runtime_verified`만으로 결정되므로, 낮은 등급이 기존에 동작하던 레인을 거부하는 일은 없습니다. 세 등급 모두에서 판정이 동일함을 테스트로 보장합니다.
565
+ - **등급은 공급업체가 아니라 증거를 따릅니다.** 이번 릴리스 이전의 프로브는 `selector-only`로 재판정되며, 과금 모델을 보고하기 시작한 CLI는 코드 변경 없이 승격됩니다.
566
+ - **`omnilane doctor`가 분포를 표시**하고 다시 프로브할 공급업체를 지목합니다.
567
+ - **overlay는 실제로 실행되는 바이너리를 고정합니다.** 기존에는 경로가 `build_overlay.py`에 하드코딩되어 사용되지 않는 버전을 조용히 가리켰습니다. 모든 디스패치가 claude `2.1.266`을 실행하는 동안 overlay는 `2.1.263`을 해시하고 있었습니다.
568
+ - **죽은 레인 3개를 찾았습니다.** `gpt-5.4-mini`는 2026-09-07 프로브에서는 통과했지만 지금은 HTTP 400(ChatGPT 계정의 Codex에서는 미지원)을 반환합니다. 서명된 overlay는 레인이 상류에서 사라져도 알아차리지 못합니다. 해당 3개 구성은 이유와 함께 `unproven[]`으로 이동했고, 매핑은 46개가 되었습니다.
569
+ - **업그레이드.** npm 게시 후 `npm i -g omnilane@0.42.6`를 실행하세요.
570
+
561
571
  ## v0.42.5 새 기능
562
572
 
563
573
  - **CLI 하나를 업그레이드해도 모든 벤더가 막히지 않습니다.** overlay evidence 항목에 `vendor` 태그를 붙일 수 있으며, 태그가 있는 항목의 해시가 바뀌거나 파일이 사라지면 해당 벤더만 `unknown-target-runtime`으로 강등됩니다. 태그가 없는 evidence는 기존처럼 전체 fail-closed입니다.
package/README.md CHANGED
@@ -647,6 +647,35 @@ vendor; the dispatch skill carries the re-signing runbook.
647
647
 
648
648
  </details>
649
649
 
650
+ <details>
651
+ <summary><b>What does my mapping's evidence tier mean?</b></summary>
652
+
653
+ <br/>
654
+
655
+ It says how strongly the probe pinned down who answered. It does not affect
656
+ whether you can dispatch.
657
+
658
+ `billed-model` — the provider named the model it charged for. Claude reports it
659
+ in `modelUsage`; grok reports it the same way under `--output-format json`. This
660
+ is the provider's receipt.
661
+
662
+ `client-echo` — the CLI wrote down the model it asked for, and that record
663
+ matches your request. Codex keeps it in the session rollout; agy writes it to
664
+ `cli.log`. This is the CLI's copy of your order, not the provider's receipt: it
665
+ proves the request left as intended, not who served it.
666
+
667
+ `selector-only` — the CLI accepted the selector and reported nothing further.
668
+ Every mapping probed before v0.42.6 reads this way. It still dispatches; it is
669
+ simply the weakest of the three, and `omnilane doctor` names the vendors worth
670
+ re-probing.
671
+
672
+ None of the three certifies upstream provider identity, and none of them can
673
+ refuse a lane. The tier is derived from what a probe produced rather than from
674
+ the vendor, so a CLI that starts reporting a billed model is promoted on its
675
+ next sweep with no change to omnilane.
676
+
677
+ </details>
678
+
650
679
  ## 📊 Defaults and provenance
651
680
 
652
681
  Default lane assignments follow Artificial Analysis coding/intelligence data
@@ -670,6 +699,16 @@ working notes, including per-benchmark caveats, live in
670
699
 
671
700
  ## 📜 Release history
672
701
 
702
+ ## What's new in v0.42.6
703
+
704
+ - **A verified mapping now says how it was verified.** Each overlay mapping carries an `evidence_tier`: `billed-model` when the provider named the model it charged for (Claude, grok), `client-echo` when the CLI recorded the model it asked for (codex, agy), `selector-only` when the CLI accepted the selector and said nothing more. `client-echo` is the CLI's copy of your order; `billed-model` is the provider's receipt.
705
+ - **Reported, never enforced.** Dispatch still turns on `runtime_verified` alone, so a weaker tier never refuses a lane that used to run. A test asserts every decision is unchanged under all three tiers.
706
+ - **The tier follows the evidence, not the vendor.** A sweep predating this release re-judges as `selector-only`, and a CLI that begins reporting a billed model is promoted with no code change.
707
+ - **`omnilane doctor` shows the spread** and names the vendors worth re-probing.
708
+ - **Overlay evidence anchors the binary that runs.** Paths were written into `build_overlay.py` and drifted out of use silently — the live overlay hashed claude `2.1.263` while every dispatch ran `2.1.266`. Core evidence now resolves the executable the runners resolve.
709
+ - **Three dead lanes found.** `gpt-5.4-mini` passed its probe on 2026-09-07 and now returns HTTP 400 — "not supported when using Codex with a ChatGPT account". A signed overlay never notices a lane dying upstream; the re-probe did. Those three configurations move to `unproven[]` with that reason attached, leaving 46 mappings.
710
+ - **Upgrade.** After npm publication, run `npm i -g omnilane@0.42.6`. Existing repo-symlink installations can update their checkout and verify `omnilane --version` without rerunning installation.
711
+
673
712
  ## What's new in v0.42.5
674
713
 
675
714
  - **One CLI upgrade no longer refuses every vendor.** Overlay evidence entries take a `vendor` tag; a tagged entry whose hash drifts or whose file has disappeared degrades only that vendor to `unknown-target-runtime`. Untagged evidence stays globally fail-closed.
package/README.zh-CN.md CHANGED
@@ -508,6 +508,30 @@ overlay 钉住每家的可执行文件与 runner 脚本哈希,而 Codex 与 Cl
508
508
 
509
509
  </details>
510
510
 
511
+ <details>
512
+ <summary><b>映射上的证据等级是什么意思?</b></summary>
513
+
514
+ <br/>
515
+
516
+ 它说明这次探测把「谁回答的」钉到多紧。它**不影响**你能不能派工。
517
+
518
+ `billed-model`——供应商自己说出它计费的模型。claude 放在 `modelUsage`;
519
+ grok 在 `--output-format json` 下也一样。这是供应商开的收据。
520
+
521
+ `client-echo`——CLI 记下自己送出的模型,而且那笔记录与你的请求相符。
522
+ codex 记在 session rollout,agy 写进 `cli.log`。这是 CLI 自己抄的订单,
523
+ 不是收据:它证明请求照原样送出去了,不能证明是谁接的。
524
+
525
+ `selector-only`——CLI 收下选择器,其余不表态。v0.42.6 之前探测的每一条映射
526
+ 都是这一级。它照样能派工,只是三级里最弱的一级;`omnilane doctor` 会点名
527
+ 哪几家值得重探。
528
+
529
+ 三者都不能证明上游供应商身分,也都不会让任何车道被拒。等级是从探测产出的
530
+ 东西推导出来的,不是按厂商指定,所以哪支 CLI 开始汇报计费模型,下一次重探
531
+ 就会自动升级,omnilane 不用改。
532
+
533
+ </details>
534
+
511
535
  ## 📊 默认值与数据来源
512
536
 
513
537
  默认通道配置依据 Artificial Analysis 2026-07 快照(已对 AA 站上原始记录与
@@ -530,6 +554,16 @@ overlay 钉住每家的可执行文件与 runner 脚本哈希,而 Codex 与 Cl
530
554
 
531
555
  ## 📜 版本历程
532
556
 
557
+ ## v0.42.6 新功能
558
+
559
+ - **「已验证」现在会说明是怎么验的。** 每条 overlay 映射带一个 `evidence_tier`:`billed-model` 是供应商自己说出计费的模型(claude、grok),`client-echo` 是 CLI 记下自己送出的模型(codex、agy),`selector-only` 是 CLI 收下选择器、其余不表态。`client-echo` 是 CLI 自己抄的订单,`billed-model` 是供应商开的收据。
560
+ - **只汇报,不拦截。** 派工照旧只看 `runtime_verified`,等级低不会让原本跑得动的车道被拒。已有测试确认三种等级下每个判定都不变。
561
+ - **等级跟着证据走,不跟着厂商走。** 本次发布之前的探测会重判为 `selector-only`;哪天某支 CLI 开始汇报计费模型,不改代码就自动升级。
562
+ - **`omnilane doctor` 显示分布**,并点名哪几家值得重探。
563
+ - **overlay 锚定的是真正在跑的可执行文件。** 过去路径写死在 `build_overlay.py` 里,会无声地锚到没在用的版本——线上 overlay 哈希的是 claude `2.1.263`,但每次派工跑的都是 `2.1.266`。
564
+ - **抓到三条已死的车道。** `gpt-5.4-mini` 在 2026-09-07 探测还会过,现在回 HTTP 400——「ChatGPT 账号使用 Codex 时不支持此模型」。签好的 overlay 永远不会发现某条车道在上游死掉,重探才会。那三条移进 `unproven[]` 并附上原因,映射剩 46 条。
565
+ - **升级。** npm 发布后运行 `npm i -g omnilane@0.42.6`。既有的 repo symlink 安装更新检出后确认 `omnilane --version` 即可。
566
+
533
567
  ## v0.42.5 新功能
534
568
 
535
569
  - **升级一支 CLI 不再阻断所有厂商。** overlay 的证据项目可带 `vendor` 标签;带标签的项目哈希漂移或文件消失时,只让该厂商降级为 `unknown-target-runtime`。未标签的证据维持全局 fail-closed。
package/README.zh-TW.md CHANGED
@@ -552,6 +552,30 @@ overlay 釘住每家的執行檔與 runner 腳本雜湊,而 Codex 與 Claude
552
552
 
553
553
  </details>
554
554
 
555
+ <details>
556
+ <summary><b>映射上的證據等級是什麼意思?</b></summary>
557
+
558
+ <br/>
559
+
560
+ 它說明這次探測把「誰回答的」釘到多緊。它**不影響**你能不能派工。
561
+
562
+ `billed-model`——供應商自己說出它計費的模型。claude 放在 `modelUsage`;
563
+ grok 在 `--output-format json` 下也一樣。這是供應商開的收據。
564
+
565
+ `client-echo`——CLI 記下自己送出的模型,而且那筆紀錄與你的請求相符。
566
+ codex 記在 session rollout,agy 寫進 `cli.log`。這是 CLI 自己抄的訂單,
567
+ 不是收據:它證明請求照原樣送出去了,不能證明是誰接的。
568
+
569
+ `selector-only`——CLI 收下選擇器,其餘不表態。v0.42.6 之前探測的每一條映射
570
+ 都是這一級。它照樣能派工,只是三級裡最弱的一級;`omnilane doctor` 會點名
571
+ 哪幾家值得重探。
572
+
573
+ 三者都不能證明上游供應商身分,也都不會讓任何車道被拒。等級是從探測產出的
574
+ 東西推導出來的,不是按廠商指定,所以哪支 CLI 開始回報計費模型,下一次重探
575
+ 就會自動升級,omnilane 不用改。
576
+
577
+ </details>
578
+
555
579
  ## 📊 預設值與資料來源
556
580
 
557
581
  預設通道配置依據 Artificial Analysis 2026-07 快照(已對 AA 站上原始紀錄與
@@ -571,6 +595,16 @@ overlay 釘住每家的執行檔與 runner 腳本雜湊,而 Codex 與 Claude
571
595
 
572
596
  ## 📜 版本歷程
573
597
 
598
+ ## v0.42.6 新功能
599
+
600
+ - **「已驗證」現在會說明是怎麼驗的。** 每條 overlay 映射帶一個 `evidence_tier`:`billed-model` 是供應商自己說出計費的模型(claude、grok),`client-echo` 是 CLI 記下自己送出的模型(codex、agy),`selector-only` 是 CLI 收下選擇器、其餘不表態。白話說:`client-echo` 是 CLI 自己抄的訂單,`billed-model` 是供應商開的收據。
601
+ - **只回報,不擋人。** 派工照舊只看 `runtime_verified`,等級低不會讓原本跑得動的車道被拒。已有測試確認三種等級下每個判定都不變。
602
+ - **等級跟著證據走,不跟著廠商走。** 本次發布之前的探測會重判為 `selector-only`;哪天某支 CLI 開始回報計費模型,不改程式就自動升級。
603
+ - **`omnilane doctor` 顯示分佈**,並點名哪幾家值得重探。
604
+ - **overlay 錨定的是真正在跑的執行檔。** 過去路徑寫死在 `build_overlay.py` 裡,會無聲地錨到沒在用的版本——線上 overlay 雜湊的是 claude `2.1.263`,但每次派工跑的都是 `2.1.266`。現在改用 runner 實際解析到的執行檔。
605
+ - **抓到三條已死的車道。** `gpt-5.4-mini` 在 2026-09-07 探測還會過,現在回 HTTP 400——「ChatGPT 帳號使用 Codex 時不支援此模型」。簽好的 overlay 永遠不會發現某條車道在上游死掉,重探才會。那三條移進 `unproven[]` 並附上原因,映射剩 46 條。
606
+ - **升級。** npm 發布後執行 `npm i -g omnilane@0.42.6`。既有的 repo symlink 安裝更新檢出後確認 `omnilane --version` 即可,不需重跑安裝。
607
+
574
608
  ## v0.42.5 新功能
575
609
 
576
610
  - **升級一支 CLI 不再擋掉所有廠商。** overlay 的證據項目可帶 `vendor` 標籤;帶標籤的項目雜湊漂移或檔案消失時,只讓該廠商降級為 `unknown-target-runtime`。未標籤的證據維持全域 fail-closed。
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "omnilane",
3
- "version": "0.42.5",
3
+ "version": "0.42.6",
4
4
  "description": "One routing table, every harness — classify subtasks into lanes and delegate each lane through a compatible caller-owned native agent or vendor CLI.",
5
5
  "bin": {
6
6
  "omnilane": "bin/omnilane"
package/plugin.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "$schema": "https://antigravity.google/schemas/v1/plugin.json",
3
3
  "name": "omnilane",
4
- "version": "0.42.5",
4
+ "version": "0.42.6",
5
5
  "description": "One routing table, every harness: classify subtasks into lanes and delegate through compatible caller-owned native agents or vendor CLIs with exact-AA downward policy and supervised jobs."
6
6
  }
@@ -29,6 +29,10 @@ APPROVED_REGISTRY_SHA256 = "0782c87de123c02738c3ff60e4bc3c1cc10d110113e872b8f862
29
29
 
30
30
  IDENTITY_FIELDS = ("vendor", "model", "effort", "reasoning", "fallback")
31
31
  TRANSPORT_EVIDENCE_VENDORS = frozenset(("codex", "claude", "grok", "gemini"))
32
+ # How strongly a mapping's probe identified the responder. Reported, never
33
+ # enforced: dispatch turns on runtime_verified alone, as it did before the field
34
+ # existed, so a weaker tier can never refuse a lane that used to run.
35
+ TRANSPORT_EVIDENCE_TIERS = frozenset(("billed-model", "client-echo", "selector-only"))
32
36
  IDENTIFIER = re.compile(r"[A-Za-z0-9][A-Za-z0-9._:/-]{0,255}\Z")
33
37
 
34
38
 
@@ -178,6 +182,7 @@ def apply_transport_overlay(registry: dict[str, Any]) -> None:
178
182
  _check(False, "transport contract evidence changed")
179
183
  stale_vendors.add(vendor)
180
184
  _check(bool(overlay.get("evidence")), "transport overlay requires local evidence")
185
+ tiers: dict[str, str] = {}
181
186
  for mapping in overlay.get("mappings", []):
182
187
  rows = [row for row in registry["scored_configs"] if row["id"] == mapping.get("config_id")]
183
188
  _check(len(rows) == 1, "unknown overlay config")
@@ -197,8 +202,11 @@ def apply_transport_overlay(registry: dict[str, Any]) -> None:
197
202
  _check(mapping["runtime_model"].endswith("-" + row["effort"]), "encoded effort does not match exact tuple")
198
203
  else:
199
204
  _check(mapping.get("runtime_model") == row["model"], "overlay model mismatch")
205
+ tier = mapping.get("evidence_tier", "selector-only")
206
+ _check(tier in TRANSPORT_EVIDENCE_TIERS, "unknown transport evidence tier")
200
207
  if row["vendor"] in stale_vendors:
201
208
  continue
209
+ tiers[row["id"]] = tier
202
210
  row["transport_mapping"].update(
203
211
  status="verified", runtime_verified=True,
204
212
  runtime_model=mapping["runtime_model"], runtime_effort=mapping["runtime_effort"],
@@ -208,6 +216,7 @@ def apply_transport_overlay(registry: dict[str, Any]) -> None:
208
216
  overlay_sha256=digest, overlay_host=overlay["host"],
209
217
  )
210
218
  registry["_stale_transport_vendors"] = sorted(stale_vendors)
219
+ registry["_transport_evidence_tiers"] = tiers
211
220
 
212
221
 
213
222
  def load_registry(path: str | Path, expected_sha256: str | None = None) -> tuple[dict[str, Any], str]:
@@ -10,7 +10,9 @@ import argparse
10
10
  import hashlib
11
11
  import json
12
12
  import os
13
+ import shutil
13
14
  import socket
15
+ from collections import Counter
14
16
  from datetime import datetime, timezone
15
17
  from pathlib import Path
16
18
 
@@ -37,13 +39,13 @@ for effort in ["xhigh", "medium"]:
37
39
  PROVEN[f"codex/gpt-5-4-mini" + ("" if effort == "xhigh" else f"-{effort}")] = (
38
40
  "model_and_effort", "gpt-5.4-mini", f"cx-gpt-5_4-mini-{effort}")
39
41
 
40
- PROVEN["grok/grok-4-6"] = ("cli_reasoning_effort", "grok-4.6", "PRIOR:grok-effort-2026-09-07")
42
+ PROVEN["grok/grok-4-6"] = ("cli_reasoning_effort", "grok-4.6", "gk-grok-4_6-high")
41
43
  for effort in ["xhigh", "medium", "low"]:
42
44
  PROVEN[f"grok/grok-4-6-{effort}"] = ("cli_reasoning_effort", "grok-4.6", f"gk-grok-4_6-{effort}")
43
45
  PROVEN["grok/grok-4-5"] = ("cli_reasoning_effort", "grok-4.5", "gk-grok-4_5-high")
44
46
 
45
47
  for cid, rid, ev in [
46
- ("gemini/gemini-3-8-flash", "gemini-3.8-flash-high", "PRIOR:gemini-flash-high"),
48
+ ("gemini/gemini-3-8-flash", "gemini-3.8-flash-high", "agy-gemini-3_8-flash-high"),
47
49
  ("gemini/gemini-3-8-flash-medium", "gemini-3.8-flash-medium", "agy-gemini-3_8-flash-medium"),
48
50
  ("gemini/gemini-3-8-flash-low", "gemini-3.8-flash-low", "agy-gemini-3_8-flash-low"),
49
51
  ("gemini/gemini-3-7-flash", "gemini-3.7-flash-high", "agy-gemini-3_7-flash-high"),
@@ -83,16 +85,32 @@ for effort in ["max", "xhigh", "high", "medium", "low"]:
83
85
  PROVEN["claude/claude-fable-5"] = (
84
86
  "model_and_effort", "claude-fable-5", "cl-claude-fable-5-max")
85
87
 
86
- CORE_EVIDENCE = [
87
- (HOME / ".grok/downloads/grok-1.0.13-macos-aarch64", "grok"),
88
- (REPO / "scripts/runners/run-grok.sh", "grok"),
89
- (HOME / ".codex/packages/standalone/releases/0.153.4-aarch64-apple-darwin/bin/codex", "codex"),
90
- (REPO / "scripts/runners/run-codex.sh", "codex"),
91
- (HOME / ".local/share/claude/versions/2.1.263", "claude"),
92
- (REPO / "scripts/runners/run-claude.sh", "claude"),
93
- (HOME / ".local/bin/agy", "gemini"),
94
- (REPO / "scripts/runners/run-gemini.sh", "gemini"),
95
- ]
88
+ def cli_path(name: str) -> Path:
89
+ """Anchor the executable the runners resolve, not a version pinned here.
90
+
91
+ The runners invoke bare names, so a pinned path can name a binary that has
92
+ not run since the last self-update; the overlay must hash what answers.
93
+ aa_policy opens evidence with O_NOFOLLOW, so this resolves past the symlink.
94
+ """
95
+ found = shutil.which(name)
96
+ if not found:
97
+ raise SystemExit(f"cannot resolve the {name} CLI to anchor its evidence")
98
+ return Path(found).resolve()
99
+
100
+
101
+ def core_evidence() -> list[tuple[Path, str]]:
102
+ """Resolved when a build runs, not at import: a host missing one CLI can
103
+ still load this module to read PROVEN."""
104
+ return [
105
+ (cli_path("grok"), "grok"),
106
+ (REPO / "scripts/runners/run-grok.sh", "grok"),
107
+ (cli_path("codex"), "codex"),
108
+ (REPO / "scripts/runners/run-codex.sh", "codex"),
109
+ (cli_path("claude"), "claude"),
110
+ (REPO / "scripts/runners/run-claude.sh", "claude"),
111
+ (cli_path("agy"), "gemini"),
112
+ (REPO / "scripts/runners/run-gemini.sh", "gemini"),
113
+ ]
96
114
 
97
115
 
98
116
  def sha256(path: Path) -> str:
@@ -116,12 +134,12 @@ def main(argv: list[str] | None = None) -> None:
116
134
 
117
135
  manifest = {"probe_runs": {}}
118
136
  unproven = []
137
+ # Evidence written before 0.42.6 carries no tier; it proved the selector and
138
+ # nothing about who answered, which is exactly what selector-only records.
139
+ tiers: dict[str, str] = {}
119
140
  for cid, (_, _, ev) in sorted(PROVEN.items()):
120
- if ev.startswith("PRIOR:"):
121
- manifest["probe_runs"][cid] = {"source": ev, "note": "verified in the 2026-09-07 Codex run"}
122
- continue
123
141
  entry = {}
124
- for suffix in ("json", "stdout", "stderr"):
142
+ for suffix in ("json", "stdout", "stderr", "rollout", "cli_log"):
125
143
  path = root / "evidence" / f"{ev}.{suffix}"
126
144
  if path.exists():
127
145
  entry[suffix] = {"path": str(path), "sha256": sha256(path)}
@@ -143,6 +161,10 @@ def main(argv: list[str] | None = None) -> None:
143
161
  })
144
162
  # Visibility only: failed evidence must not enter the signed manifest.
145
163
  continue
164
+ tier = descriptor.get("evidence_tier", "selector-only")
165
+ if tier not in ("billed-model", "client-echo", "selector-only"):
166
+ raise SystemExit(f"unknown evidence tier for {cid}: {tier}")
167
+ tiers[cid] = tier
146
168
  manifest["probe_runs"][cid] = entry
147
169
  manifest_path = root / "probe-manifest.json"
148
170
  manifest_path.write_text(json.dumps(manifest, indent=2, sort_keys=True) + "\n")
@@ -159,6 +181,7 @@ def main(argv: list[str] | None = None) -> None:
159
181
  "runtime_effort": row["effort"],
160
182
  "selector_type": selector,
161
183
  "verification": "request-selector-contract",
184
+ "evidence_tier": tiers.get(cid, "selector-only"),
162
185
  }
163
186
  if selector == "cli_reasoning_effort":
164
187
  mapping["cli_flag"] = "--reasoning-effort"
@@ -166,7 +189,7 @@ def main(argv: list[str] | None = None) -> None:
166
189
 
167
190
  evidence = [
168
191
  {"path": str(path), "sha256": sha256(path), "vendor": vendor}
169
- for path, vendor in CORE_EVIDENCE
192
+ for path, vendor in core_evidence()
170
193
  ]
171
194
  evidence.append({"path": str(manifest_path), "sha256": sha256(manifest_path)})
172
195
 
@@ -182,7 +205,9 @@ def main(argv: list[str] | None = None) -> None:
182
205
  }
183
206
  out = root / "transport-contracts.local.json"
184
207
  out.write_text(json.dumps(overlay, indent=2, ensure_ascii=False) + "\n")
208
+ spread = Counter(m["evidence_tier"] for m in mappings)
185
209
  print(f"wrote {out} with {len(mappings)} mappings and {len(evidence)} evidence anchors")
210
+ print(" evidence tiers: " + ", ".join(f"{tier} {count}" for tier, count in sorted(spread.items())))
186
211
 
187
212
 
188
213
  if __name__ == "__main__":
@@ -68,17 +68,23 @@ def main() -> None:
68
68
  detail = "; ".join(offenders(Path(overlay_path))) or str(error)
69
69
  emit("FAIL", f"overlay rejected, every dispatch is refused: {error} ({detail})")
70
70
 
71
+ tiers = registry.get("_transport_evidence_tiers", {})
71
72
  verified = Counter()
72
73
  for row in registry["scored_configs"]:
73
74
  if row["transport_mapping"].get("runtime_verified") is True:
74
- verified[row["vendor"]] += 1
75
- summary = ", ".join(f"{v} {n}" for v, n in sorted(verified.items())) or "none"
75
+ verified[row["vendor"], tiers.get(row["id"], "selector-only")] += 1
76
+ summary = ", ".join(f"{vendor} {count} {tier}"
77
+ for (vendor, tier), count in sorted(verified.items())) or "none"
76
78
 
77
79
  import json
78
80
 
79
81
  overlay = json.loads(Path(overlay_path).read_text())
80
82
  unproven = overlay.get("unproven", [])
81
83
  extra = f"; {len(unproven)} config(s) recorded unproven" if unproven else ""
84
+ weak = sorted({vendor for (vendor, tier) in verified if tier == "selector-only"})
85
+ if weak:
86
+ extra += (f"; {', '.join(weak)} prove only the request selector, re-probe to "
87
+ "record who answered")
82
88
 
83
89
  stale = registry.get("_stale_transport_vendors", [])
84
90
  if stale:
@@ -3,11 +3,14 @@
3
3
 
4
4
  Runs one CLI invocation, captures raw stdout/stderr to files, and writes a
5
5
  descriptor with the original command/stream fields plus a vendor-specific
6
- verdict, its reason, and the observed model when the response proves it.
6
+ verdict, its reason, the observed model, and the tier of evidence that model
7
+ rests on.
7
8
  """
8
9
  import argparse
10
+ import glob
9
11
  import json
10
12
  import os
13
+ import re
11
14
  import subprocess
12
15
  import time
13
16
  from datetime import datetime, timezone
@@ -16,6 +19,88 @@ from pathlib import Path
16
19
  SWEEP_ID = os.environ.get("OMNILANE_TRANSPORT_SWEEP_ID", "overlay-reprobe-20260909")
17
20
  DEFAULT_ROOT = Path.home() / ".omnilane" / "transport-evidence" / SWEEP_ID
18
21
 
22
+ # Ordered strongest first. The tier is read off the evidence a run produced, not
23
+ # off the vendor: a CLI that starts reporting a billed model earns the higher
24
+ # tier with no change here.
25
+ TIER_BILLED = "billed-model" # the provider named the model it charged for
26
+ TIER_ECHO = "client-echo" # the CLI recorded the model it asked for
27
+ TIER_SELECTOR = "selector-only" # the CLI accepted the selector and said no more
28
+ EVIDENCE_TIERS = (TIER_BILLED, TIER_ECHO, TIER_SELECTOR)
29
+
30
+
31
+ def codex_failure(stdout_text: str) -> str:
32
+ """The last thing codex's event stream said went wrong, if anything.
33
+
34
+ Later events supersede earlier ones: a retry notice is progress, the message
35
+ on `turn.failed` is the outcome.
36
+ """
37
+ latest = ""
38
+ for line in stdout_text.splitlines():
39
+ line = line.strip()
40
+ if not line.startswith("{"):
41
+ continue
42
+ try:
43
+ event = json.loads(line)
44
+ except json.JSONDecodeError:
45
+ continue
46
+ if not isinstance(event, dict):
47
+ continue
48
+ if event.get("type") == "turn.failed":
49
+ message = (event.get("error") or {}).get("message")
50
+ elif event.get("type") == "error":
51
+ message = event.get("message")
52
+ else:
53
+ continue
54
+ if isinstance(message, str) and message:
55
+ latest = message
56
+ return f"; codex-event: {latest[:200]}" if latest else ""
57
+
58
+
59
+ def _requested_model(command: list) -> str | None:
60
+ requested = None
61
+ for index, argument in enumerate(command):
62
+ if not isinstance(argument, str):
63
+ continue
64
+ if argument in ("--model", "-m") and index + 1 < len(command):
65
+ requested = command[index + 1]
66
+ elif argument.startswith("--model="):
67
+ requested = argument.split("=", 1)[1]
68
+ return requested
69
+
70
+
71
+ def record_values(record_text: str) -> list[str]:
72
+ """Comparable values from a client record digest: `key<TAB>value` lines.
73
+
74
+ `#` lines carry provenance and display labels that must never be compared —
75
+ agy's backend label is "Gemini 3.8 Flash (Low)", not a model identifier.
76
+ """
77
+ values = []
78
+ for line in (record_text or "").splitlines():
79
+ if line.startswith("#") or "\t" not in line:
80
+ continue
81
+ value = line.split("\t", 1)[1].strip()
82
+ if value:
83
+ values.append(value)
84
+ return values
85
+
86
+
87
+ def _client_record(
88
+ evidence_json: dict,
89
+ record_text: str,
90
+ passed_reason: str,
91
+ ) -> tuple[str, str, str | None, str]:
92
+ """Judge a CLI's own on-disk record of the request it sent."""
93
+ values = record_values(record_text)
94
+ if not values:
95
+ return "pass", f"{passed_reason}; no-client-record", None, TIER_SELECTOR
96
+ observed_model = ", ".join(sorted(set(values)))
97
+ requested_model = _requested_model(evidence_json.get("command", []))
98
+ if not requested_model:
99
+ return "fail", "missing-requested-model", observed_model, TIER_SELECTOR
100
+ if requested_model not in values:
101
+ return "fail", "client-record-mismatch", observed_model, TIER_SELECTOR
102
+ return "pass", f"{passed_reason}-and-client-record-matched", observed_model, TIER_ECHO
103
+
19
104
 
20
105
  def verdict(
21
106
  evidence_json: dict,
@@ -23,25 +108,27 @@ def verdict(
23
108
  stderr_text: str,
24
109
  vendor: str,
25
110
  expected_token: str | None,
26
- ) -> tuple[str, str, str | None]:
111
+ extra: dict | None = None,
112
+ ) -> tuple[str, str, str | None, str]:
27
113
  """Judge raw evidence without reading files, running commands or mutating it.
28
114
 
29
- Only Claude's billed modelUsage keys currently prove the responding model.
30
- A requested selector (including the Codex banner) is not observed identity.
115
+ `extra` carries text the caller already gathered from disk, so this stays a
116
+ pure function that can re-judge an old sweep offline.
31
117
  """
118
+ extra = extra or {}
32
119
  if evidence_json.get("timed_out"):
33
- return "fail", "timeout", None
120
+ return "fail", "timeout", None, TIER_SELECTOR
34
121
  if not expected_token:
35
- return "fail", "missing-expected-token", None
122
+ return "fail", "missing-expected-token", None, TIER_SELECTOR
36
123
 
37
124
  exit_code = evidence_json.get("exit_code")
38
125
  if vendor == "claude":
39
126
  try:
40
127
  response = json.loads(stdout_text)
41
128
  except (json.JSONDecodeError, TypeError):
42
- return "fail", "invalid-json", None
129
+ return "fail", "invalid-json", None, TIER_SELECTOR
43
130
  if not isinstance(response, dict):
44
- return "fail", "invalid-json-result", None
131
+ return "fail", "invalid-json-result", None, TIER_SELECTOR
45
132
  usage = response.get("modelUsage")
46
133
  models = sorted(usage) if isinstance(usage, dict) else []
47
134
  observed_model = ", ".join(models) or None
@@ -56,48 +143,168 @@ def verdict(
56
143
  reason = "api-error"
57
144
  else:
58
145
  reason = "result-error"
59
- return "fail", f"{reason}: {result[:120]}", observed_model
146
+ return "fail", f"{reason}: {result[:120]}", observed_model, TIER_SELECTOR
60
147
  if not models:
61
- return "fail", "missing-model-usage", None
62
- command = evidence_json.get("command", [])
63
- requested_model = None
64
- for index, argument in enumerate(command):
65
- if argument == "--model" and index + 1 < len(command):
66
- requested_model = command[index + 1]
67
- elif isinstance(argument, str) and argument.startswith("--model="):
68
- requested_model = argument.split("=", 1)[1]
148
+ return "fail", "missing-model-usage", None, TIER_SELECTOR
149
+ requested_model = _requested_model(evidence_json.get("command", []))
69
150
  if not requested_model:
70
- return "fail", "missing-requested-model", observed_model
151
+ return "fail", "missing-requested-model", observed_model, TIER_SELECTOR
71
152
  if models != [requested_model]:
72
- return "fail", "model-mismatch", observed_model
153
+ return "fail", "model-mismatch", observed_model, TIER_SELECTOR
73
154
  if "unknown --effort" in stderr_text.lower():
74
- return "fail", "effort-silently-defaulted", observed_model
155
+ return "fail", "effort-silently-defaulted", observed_model, TIER_SELECTOR
75
156
  if exit_code != 0:
76
- return "fail", f"exit-code: {exit_code}", observed_model
157
+ return "fail", f"exit-code: {exit_code}", observed_model, TIER_SELECTOR
77
158
  if expected_token not in result:
78
- return "fail", "missing-expected-token", observed_model
79
- return "pass", "expected-token-and-model-matched", observed_model
159
+ return "fail", "missing-expected-token", observed_model, TIER_SELECTOR
160
+ return "pass", "expected-token-and-model-matched", observed_model, TIER_BILLED
161
+
162
+ if vendor == "grok":
163
+ if exit_code != 0:
164
+ return "fail", f"exit-code: {exit_code}: {stderr_text[:120]}", None, TIER_SELECTOR
165
+ if stderr_text:
166
+ return "fail", f"unexpected-stderr: {stderr_text[:120]}", None, TIER_SELECTOR
167
+ if expected_token not in stdout_text:
168
+ return "fail", "missing-expected-token", None, TIER_SELECTOR
169
+ try:
170
+ response = json.loads(stdout_text)
171
+ except (json.JSONDecodeError, TypeError):
172
+ response = None
173
+ usage = response.get("modelUsage") if isinstance(response, dict) else None
174
+ models = sorted(usage) if isinstance(usage, dict) else []
175
+ if not models:
176
+ return "pass", "expected-token-and-clean-stderr; no-billed-model", None, TIER_SELECTOR
177
+ observed_model = ", ".join(models)
178
+ requested_model = _requested_model(evidence_json.get("command", []))
179
+ if not requested_model:
180
+ return "fail", "missing-requested-model", observed_model, TIER_SELECTOR
181
+ # Grok bills `grok-4.6` as `grok-4.6-build`. Accept that one suffix and
182
+ # nothing else: a prefix test would let `grok-4.6-anything` pass.
183
+ if models not in ([requested_model], [f"{requested_model}-build"]):
184
+ return "fail", "model-mismatch", observed_model, TIER_SELECTOR
185
+ return "pass", "expected-token-and-billed-model-matched", observed_model, TIER_BILLED
80
186
 
81
- if vendor in ("grok", "agy"):
187
+ if vendor == "agy":
82
188
  if exit_code != 0:
83
- return "fail", f"exit-code: {exit_code}: {stderr_text[:120]}", None
189
+ return "fail", f"exit-code: {exit_code}: {stderr_text[:120]}", None, TIER_SELECTOR
84
190
  if stderr_text:
85
- return "fail", f"unexpected-stderr: {stderr_text[:120]}", None
191
+ return "fail", f"unexpected-stderr: {stderr_text[:120]}", None, TIER_SELECTOR
86
192
  if expected_token not in stdout_text:
87
- return "fail", "missing-expected-token", None
88
- return "pass", "expected-token-and-clean-stderr", None
193
+ return "fail", "missing-expected-token", None, TIER_SELECTOR
194
+ return _client_record(evidence_json, extra.get("cli_log", ""),
195
+ "expected-token-and-clean-stderr")
89
196
 
90
197
  if vendor == "codex":
91
198
  diagnostics = [line[:120] for line in stderr_text.splitlines()
92
199
  if "error" in line.lower() or "warning" in line.lower()]
93
200
  review = "; stderr-review: " + " | ".join(diagnostics) if diagnostics else ""
201
+ # Under `--json` the refusal that ended the run is an stdout event, not
202
+ # a stderr line, so a failure would otherwise be recorded as a bare exit
203
+ # code and leave unproven[] saying nothing a reader can act on.
204
+ why = codex_failure(stdout_text)
94
205
  if exit_code != 0:
95
- return "fail", f"exit-code: {exit_code}{review}", None
206
+ return "fail", f"exit-code: {exit_code}{why}{review}", None, TIER_SELECTOR
96
207
  if expected_token not in stdout_text:
97
- return "fail", f"missing-expected-token{review}", None
98
- return "pass", f"expected-token-matched{review}", None
208
+ return "fail", f"missing-expected-token{why}{review}", None, TIER_SELECTOR
209
+ result, reason, observed, tier = _client_record(
210
+ evidence_json, extra.get("rollout", ""), "expected-token-matched")
211
+ return result, f"{reason}{review}", observed, tier
212
+
213
+ return "fail", f"unsupported-vendor: {vendor}", None, TIER_SELECTOR
214
+
215
+
216
+ def _sha256(path: Path) -> str:
217
+ import hashlib
218
+
219
+ digest = hashlib.sha256()
220
+ with open(path, "rb") as stream:
221
+ for block in iter(lambda: stream.read(1 << 20), b""):
222
+ digest.update(block)
223
+ return digest.hexdigest()
224
+
225
+
226
+ def _walk_models(node, prefix: str, found: dict) -> None:
227
+ if isinstance(node, dict):
228
+ for key, value in node.items():
229
+ here = f"{prefix}.{key}" if prefix else key
230
+ if key == "model" and isinstance(value, str):
231
+ found.setdefault(here, value)
232
+ _walk_models(value, here, found)
233
+ elif isinstance(node, list):
234
+ for item in node:
235
+ _walk_models(item, f"{prefix}[]", found)
236
+
237
+
238
+ def codex_record(stdout_text: str, sessions_dir: Path | None = None) -> str:
239
+ """Digest the rollout codex persisted for the thread this run started.
240
+
241
+ Needs `codex exec --json` for the thread id and no `--ephemeral`, which
242
+ would suppress the rollout this reads.
243
+ """
244
+ thread_id = None
245
+ for line in stdout_text.splitlines():
246
+ line = line.strip()
247
+ if not line.startswith("{"):
248
+ continue
249
+ try:
250
+ event = json.loads(line)
251
+ except json.JSONDecodeError:
252
+ continue
253
+ if isinstance(event, dict) and isinstance(event.get("thread_id"), str):
254
+ thread_id = event["thread_id"]
255
+ break
256
+ if not thread_id:
257
+ return ""
258
+ root = sessions_dir or Path.home() / ".codex" / "sessions"
259
+ matches = sorted(glob.glob(str(root / "**" / f"rollout-*{thread_id}.jsonl"), recursive=True))
260
+ if not matches:
261
+ return ""
262
+ path = Path(matches[0])
263
+ found: dict[str, str] = {}
264
+ for line in path.read_text(errors="replace").splitlines():
265
+ line = line.strip()
266
+ if not line.startswith("{"):
267
+ continue
268
+ try:
269
+ event = json.loads(line)
270
+ except json.JSONDecodeError:
271
+ continue
272
+ _walk_models(event, "", found)
273
+ lines = [f"# thread_id\t{thread_id}", f"# source\t{path}", f"# sha256\t{_sha256(path)}"]
274
+ lines += [f"{key}\t{value}" for key, value in sorted(found.items())]
275
+ return "\n".join(lines) + "\n"
276
+
277
+
278
+ AGY_MODEL = re.compile(r"Resolving model (\S+)")
279
+ AGY_LABEL = re.compile(r'selected model override to backend: label="([^"]+)"')
280
+
99
281
 
100
- return "fail", f"unsupported-vendor: {vendor}", None
282
+ def agy_record(app_root: Path, since: float) -> str:
283
+ """Digest the model agy resolved in the logs this run wrote under app_root.
284
+
285
+ `since` rejects logs from an earlier run, so a reused app root cannot lend
286
+ its model string to a later probe.
287
+ """
288
+ app_root = Path(app_root).expanduser()
289
+ if not app_root.is_dir():
290
+ return ""
291
+ lines: list[str] = []
292
+ values: dict[str, str] = {}
293
+ for path in sorted(app_root.rglob("cli*.log")):
294
+ if not path.is_file() or path.stat().st_mtime < since - 2:
295
+ continue
296
+ text = path.read_text(errors="replace")
297
+ models = AGY_MODEL.findall(text)
298
+ if not models:
299
+ continue
300
+ lines += [f"# source\t{path}", f"# sha256\t{_sha256(path)}"]
301
+ lines += [f"# label\t{label}" for label in dict.fromkeys(AGY_LABEL.findall(text))]
302
+ for model in models:
303
+ values.setdefault(f"{path.name}:resolved-model:{model}", model)
304
+ if not values:
305
+ return ""
306
+ lines += [f"{key}\t{value}" for key, value in sorted(values.items())]
307
+ return "\n".join(lines) + "\n"
101
308
 
102
309
 
103
310
  def probe(
@@ -109,6 +316,7 @@ def probe(
109
316
  *,
110
317
  vendor: str | None = None,
111
318
  expected_token: str | None = None,
319
+ app_root: Path | None = None,
112
320
  ) -> dict:
113
321
  if not expected_token:
114
322
  raise ValueError("expected_token is required before running a probe")
@@ -148,10 +356,24 @@ def probe(
148
356
  "stdout": str(out_path),
149
357
  "stderr": str(err_path),
150
358
  }
151
- record["verdict"], record["verdict_reason"], record["observed_model"] = verdict(
152
- record, out_path.read_text(errors="replace"), err_path.read_text(errors="replace"),
153
- vendor, expected_token,
154
- )
359
+ stdout_text = out_path.read_text(errors="replace")
360
+ extra = {}
361
+ if vendor == "codex":
362
+ extra["rollout"] = codex_record(stdout_text)
363
+ elif vendor == "agy" and app_root is not None:
364
+ extra["cli_log"] = agy_record(app_root, started)
365
+ for key, text in extra.items():
366
+ if not text:
367
+ continue
368
+ # Stored so the sweep hashes it and verdict() can re-judge it offline.
369
+ record_path = evidence / f"{name}.{key}"
370
+ record_path.write_text(text)
371
+ record[key] = str(record_path)
372
+ record["verdict"], record["verdict_reason"], record["observed_model"], \
373
+ record["evidence_tier"] = verdict(
374
+ record, stdout_text, err_path.read_text(errors="replace"),
375
+ vendor, expected_token, extra,
376
+ )
155
377
  (evidence / f"{name}.json").write_text(json.dumps(record, indent=2) + "\n")
156
378
  return record
157
379
 
@@ -174,11 +396,13 @@ if __name__ == "__main__":
174
396
  parser.add_argument("--expect", required=True, help="expected response token")
175
397
  parser.add_argument("--vendor", choices=("claude", "grok", "agy", "codex"),
176
398
  help="defaults to the command executable's basename")
399
+ parser.add_argument("--app-root", type=Path,
400
+ help="agy app data root for this run, to read back its cli log")
177
401
  parser.add_argument("name")
178
402
  parser.add_argument("command", nargs=argparse.REMAINDER)
179
403
  args = parser.parse_args()
180
404
  if not args.command:
181
405
  parser.error("command is required")
182
- rec = probe(args.name, args.command, root=args.root,
406
+ rec = probe(args.name, args.command, root=args.root, app_root=args.app_root,
183
407
  vendor=args.vendor, expected_token=args.expect)
184
408
  print(json.dumps(rec, ensure_ascii=False))
@@ -388,9 +388,24 @@ selector evidence. Evidence entries carry a `vendor` tag: a tagged entry that
388
388
  drifts marks only its own vendor stale, and the other three keep dispatching.
389
389
  Untagged evidence — `probe-manifest.json`, and any overlay built before the tags
390
390
  existed — still fails the whole gate closed, which is what an unpatched host
391
- looks like. Codex and Claude evidence paths embed version directories
392
- (`releases/0.153.4-…`, `versions/2.1.263`), so their upgrades remove the file
393
- rather than change its digest; both are treated as staleness, not corruption.
391
+ looks like. Codex and Claude resolve through version directories
392
+ (`releases/0.153.4-…`, `versions/2.1.266`), so their upgrades remove the anchored
393
+ file rather than change its digest; both are treated as staleness, not corruption.
394
+
395
+ Do not expect these upgrades to be operator actions. agy and grok update
396
+ themselves in the background when invoked — agy's own `cli.log` records
397
+ `auto_updater.go: Spawned background update process`, and both binaries changed
398
+ under a probing session on 2026-09-10, minutes after their first call. Overlay
399
+ drift is therefore a routine consequence of using a vendor, not an occasional
400
+ maintenance event, which is why per-vendor degradation matters more than it
401
+ looks. It also means any test asserting a fixed number of verified live
402
+ mappings will go red on its own schedule.
403
+
404
+ Because of that, `build_overlay.py` anchors the executable `shutil.which` resolves
405
+ rather than a version written into the script. A pinned path drifts out of use
406
+ silently: before 0.42.6 the overlay hashed claude `2.1.263` while every dispatch
407
+ ran `2.1.266`, so eleven mappings were "verified" against a binary that had not
408
+ run for a day.
394
409
 
395
410
  Re-signing is a probe, a rebuild, and an install, in that order. Back up
396
411
  `~/.omnilane/transport-contracts.local.json` first; restoring it is the rollback.
@@ -408,6 +423,30 @@ Keep the sweep where its default `--root` puts it,
408
423
  inside a repository is one `git clean -fdx` away from taking every vendor down
409
424
  at once — the same global refusal a re-signing session is usually trying to end.
410
425
 
426
+ Every mapping carries an `evidence_tier` saying how strongly its probe pinned the
427
+ responder. `billed-model` means the provider named the model it charged for —
428
+ Claude's `modelUsage`, grok's under `--output-format json`. `client-echo` means
429
+ the CLI wrote down the model it asked for — codex's session rollout, agy's
430
+ `cli.log` resolver line. `selector-only` means the CLI accepted the selector and
431
+ said nothing more. Put plainly: `client-echo` is the CLI's copy of your order,
432
+ `billed-model` is the provider's receipt. Neither certifies upstream identity,
433
+ but only one of them was written by the party that answered.
434
+
435
+ The tier is reported, never enforced. Dispatch still turns on `runtime_verified`
436
+ alone, so a mapping that drops to `selector-only` keeps working and simply shows
437
+ up in doctor as worth re-probing. Do not add a tier check to the gate: that would
438
+ rebuild the failure 0.42.5 removed, where evidence quality could refuse a lane
439
+ that runs. The tier is read off the evidence a run produced rather than assigned
440
+ per vendor, so a sweep predating 0.42.6 re-judges as `selector-only` and a CLI
441
+ that starts reporting a billed model is promoted with no code change.
442
+
443
+ Two probe details follow from this. Codex needs `exec --json` (the thread id that
444
+ locates the rollout) and must *not* use `--ephemeral`, which suppresses the very
445
+ rollout the tier reads. agy needs its own app data directory, prepared exactly
446
+ the way `run-gemini.sh` does it — `prepare-agy-mode.py --mode advise` returns a
447
+ path relative to `~/.gemini` that is passed as `--app_data_dir=`; the environment
448
+ variables that look like they would do this are ignored.
449
+
411
450
  Never sign a probe you did not read. `probe.py` records a `verdict` because exit
412
451
  status alone is not evidence: the Claude CLI answers a quota refusal with a JSON
413
452
  body carrying `is_error`, and it accepts an unknown `--effort` by silently using