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.
- package/.claude-plugin/marketplace.json +2 -2
- package/.claude-plugin/plugin.json +1 -1
- package/CHANGELOG.md +60 -0
- package/README.ja.md +10 -0
- package/README.ko.md +10 -0
- package/README.md +39 -0
- package/README.zh-CN.md +34 -0
- package/README.zh-TW.md +34 -0
- package/package.json +1 -1
- package/plugin.json +1 -1
- package/scripts/lib/aa_policy.py +9 -0
- package/scripts/lib/build_overlay.py +42 -17
- package/scripts/lib/overlay_health.py +8 -2
- package/scripts/lib/probe.py +261 -37
- package/skills/omnilane/SKILL.md +42 -3
|
@@ -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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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
|
}
|
package/scripts/lib/aa_policy.py
CHANGED
|
@@ -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", "
|
|
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", "
|
|
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
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
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
|
|
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"{
|
|
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:
|
package/scripts/lib/probe.py
CHANGED
|
@@ -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,
|
|
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
|
-
|
|
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
|
-
|
|
30
|
-
|
|
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
|
-
|
|
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
|
|
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 "
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
152
|
-
|
|
153
|
-
|
|
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))
|
package/skills/omnilane/SKILL.md
CHANGED
|
@@ -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
|
|
392
|
-
(`releases/0.153.4-…`, `versions/2.1.
|
|
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
|