omnilane 0.42.4 → 0.42.5

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.4"
9
+ "version": "0.42.5"
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.4",
16
+ "version": "0.42.5",
17
17
  "category": "development",
18
18
  "keywords": [
19
19
  "routing",
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "omnilane",
3
- "version": "0.42.4",
3
+ "version": "0.42.5",
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,41 @@ semantic version tags.
6
6
 
7
7
  ## [Unreleased]
8
8
 
9
+ ## [0.42.5] - 2026-09-09
10
+
11
+ ### Fixed
12
+
13
+ - One vendor's CLI upgrade refused every vendor's dispatch. Overlay evidence
14
+ compared in a single loop raised on the first mismatch, so an `agy`
15
+ 1.1.27 -> 1.1.28 upgrade failed `load_registry` and returned
16
+ `invalid-policy-input` for codex, claude, and grok as well, although only the
17
+ seven gemini mappings depended on that binary. Evidence entries now take a
18
+ `vendor` tag; a tagged entry that drifts or disappears degrades only its own
19
+ vendor to `unknown-target-runtime`. Untagged evidence stays globally
20
+ fail-closed, and structural overlay checks stay hard failures.
21
+ - `omnilane doctor` reported 19 passed, 0 failed while every dispatch was
22
+ refused. A new `transport-overlay` check loads the configured overlay, names
23
+ the offending file and vendor on failure, reports per-vendor verified counts,
24
+ and warns when a vendor has degraded.
25
+ - `build_overlay.py` signed probes it never read. Six `claude-fable-5-1`
26
+ configurations had been unusable since 2026-09-07 because their probes hit a
27
+ quota refusal and were silently omitted. Probes now carry a per-vendor
28
+ `verdict`, non-passing probes are refused and recorded in the overlay's
29
+ `unproven[]`, and evidence predating the field is signed with a warning. The
30
+ six Fable configurations are listed in the probe table so their failures are
31
+ reported; a 2026-09-09 re-probe returned the same quota refusal, so they stay
32
+ unusable and `omnilane doctor` now says so.
33
+ - A Claude CLI probe that requested an unknown `--effort` returned exit 0, no
34
+ error, the right `modelUsage`, and the expected token while silently using the
35
+ default effort. The verdict now fails that case, so a mapping cannot be
36
+ certified at the wrong effort tier.
37
+
38
+ ### Changed
39
+
40
+ - `build_overlay.py` and `probe.py` moved from an untracked `.rollback` sweep
41
+ directory into `scripts/lib/` and take `--root`, so overlay rebuilds survive a
42
+ cleanup of that directory.
43
+
9
44
  ## [0.42.4] - 2026-09-07
10
45
 
11
46
  ### Fixed
@@ -886,7 +921,8 @@ work to the wrong model, and records the evidence behind the shipped defaults.
886
921
  - Initial shared routing table, cross-vendor dispatcher, runners, installer,
887
922
  and baseline lint fixes.
888
923
 
889
- [Unreleased]: https://github.com/Seraphim0916/omnilane/compare/v0.42.4...HEAD
924
+ [Unreleased]: https://github.com/Seraphim0916/omnilane/compare/v0.42.5...HEAD
925
+ [0.42.5]: https://github.com/Seraphim0916/omnilane/compare/v0.42.4...v0.42.5
890
926
  [0.42.4]: https://github.com/Seraphim0916/omnilane/compare/v0.42.3...v0.42.4
891
927
  [0.42.3]: https://github.com/Seraphim0916/omnilane/compare/v0.42.2...v0.42.3
892
928
  [0.42.2]: https://github.com/Seraphim0916/omnilane/compare/v0.42.1...v0.42.2
package/README.ja.md CHANGED
@@ -521,6 +521,36 @@ work の別名ではありません。サービス管理など、work の境界
521
521
 
522
522
  </details>
523
523
 
524
+ <details>
525
+ <summary><b>ディスパッチが拒否されました。どの拒否ですか?</b></summary>
526
+
527
+ <br/>
528
+
529
+ 3 つのコードには 3 つの異なる対処があります。まず `omnilane doctor` を実行して
530
+ ください。その `transport-overlay` チェックが、問題はこのマシンの設定なのか
531
+ リクエストなのかをすぐに示します。
532
+
533
+ `missing-caller-context` — 呼び出し元の識別情報がありません。人間は
534
+ `OMNILANE_AA_OPERATOR_ASSERTED_HUMAN=1` または `--operator-asserted-human` を
535
+ 使います。omnilane を動かすモデルは、正確なベンダー・モデル・effort を含む
536
+ `--caller-context FILE` を渡し、人間向けの免除を自称してはいけません。
537
+
538
+ `runtime-mapping-unverified` — 識別情報は正しく、**ターゲット**にホストローカルの
539
+ リクエストセレクタの証明がありません。未プローブか、プローブが失敗しています。
540
+ `omnilane doctor` がその件数を報告し、overlay の `unproven[]` に各失敗の理由が
541
+ 残ります。プロバイダーの利用上限による拒否は、その上限が解消するまで続きます。
542
+
543
+ `invalid-policy-input` と "transport contract evidence changed" — 上記のいずれ
544
+ でもありません。overlay 自体が読み込めないため、**すべてのベンダー**が拒否され
545
+ ます。よくある原因はベンダー CLI の更新です。overlay は各ベンダーの実行ファイルと
546
+ ランナースクリプトのハッシュを固定しており、Codex と Claude の証拠パスは
547
+ バージョンディレクトリを含むため、更新ではダイジェストが変わるのではなく
548
+ ファイルが消えます。タグ付きの証拠は自分のベンダーだけを降格させ、プローブ
549
+ マニフェストのようなタグなしの証拠はゲート全体を閉じます。doctor がファイルと
550
+ ベンダーを示し、再署名の手順はディスパッチスキルにあります。
551
+
552
+ </details>
553
+
524
554
  ## 📊 デフォルト値と出典
525
555
 
526
556
  デフォルトのレーン割当は Artificial Analysis の 2026-07 スナップショット
@@ -543,6 +573,14 @@ work の別名ではありません。サービス管理など、work の境界
543
573
 
544
574
  ## 📜 リリース履歴
545
575
 
576
+ ## v0.42.5 の新機能
577
+
578
+ - **1 つの CLI 更新が全ベンダーを止めることはなくなりました。** overlay の evidence に `vendor` タグを付与でき、タグ付きエントリのハッシュ差異やファイル消失は当該ベンダーのみを `unknown-target-runtime` に降格させます。タグなしの evidence は従来どおり全体を fail-closed にします。
579
+ - **`omnilane doctor` が overlay を読み込みます。** 新しい `transport-overlay` チェックは失敗時に該当ファイルとベンダーを示し、成功時はベンダーごとの検証済みマッピング数を報告します。
580
+ - **プローブが判定を記録します。** `probe.py` は課金された `modelUsage` で Claude の応答を判定し、CLI が不明な `--effort` を既定値へ黙って置き換えた場合を失敗とします。`build_overlay.py` は不合格のプローブに署名せず、overlay の `unproven[]` に記録します。
581
+ - **再構築ツールをバージョン管理下へ。** `build_overlay.py` と `probe.py` は `scripts/lib/` に移り、`--root` を受け取ります。
582
+ - **アップグレード。** npm 公開後は `npm i -g omnilane@0.42.5` を実行してください。
583
+
546
584
  ## v0.42.4 の新機能
547
585
 
548
586
  - **クイックスタートが実際に動くようになりました。** `omnilane route` は「誰が依頼しているか」を必要としますが、60 秒クイックスタートにその記載が無く、新規インストールでは案内無しに `missing-caller-context` で拒否されていました。今は `OMNILANE_AA_OPERATOR_ASSERTED_HUMAN=1` で人間の操作者を一度宣言し、モデル呼び出し元が代わりに渡すものも説明します。
package/README.ko.md CHANGED
@@ -507,6 +507,36 @@ work 는 지정한 디렉터리 안의 변경만 허용하며 모델 연결은
507
507
 
508
508
  </details>
509
509
 
510
+ <details>
511
+ <summary><b>디스패치가 거부되었습니다. 어떤 거부인가요?</b></summary>
512
+
513
+ <br/>
514
+
515
+ 세 가지 코드에는 각각 다른 해결책이 있습니다. 먼저 `omnilane doctor`를 실행하세요.
516
+ `transport-overlay` 검사가 문제의 원인이 이 머신의 설정인지 요청인지 바로
517
+ 알려줍니다.
518
+
519
+ `missing-caller-context` — 호출자 신원을 전달하지 않았습니다. 사람은
520
+ `OMNILANE_AA_OPERATOR_ASSERTED_HUMAN=1` 또는 `--operator-asserted-human`을
521
+ 사용합니다. omnilane을 구동하는 모델은 정확한 벤더·모델·effort가 담긴
522
+ `--caller-context FILE`을 전달해야 하며, 사람용 면제를 스스로 주장해서는 안 됩니다.
523
+
524
+ `runtime-mapping-unverified` — 신원은 정상이지만 **대상**에 검증된 호스트 로컬
525
+ 요청 셀렉터가 없습니다. 프로브를 한 적이 없거나 프로브가 실패한 경우입니다.
526
+ `omnilane doctor`가 해당 구성의 개수를 보고하고, overlay의 `unproven[]`에 각
527
+ 실패 사유가 기록됩니다. 공급자 할당량 제한으로 인한 거부는 그 할당량이 회복될
528
+ 때까지 해소되지 않습니다.
529
+
530
+ `invalid-policy-input`과 "transport contract evidence changed" — 위 둘 다
531
+ 아닙니다. overlay 자체가 로드되지 않아 **모든 벤더**가 거부됩니다. 흔한 원인은
532
+ 벤더 CLI 업그레이드입니다. overlay는 각 벤더의 실행 파일과 러너 스크립트 해시를
533
+ 고정하며, Codex와 Claude의 증거 경로에는 버전 디렉터리가 포함되어 업그레이드 시
534
+ 다이제스트가 바뀌는 대신 파일이 사라집니다. 태그가 있는 증거는 해당 벤더만
535
+ 강등시키고, 프로브 매니페스트처럼 태그가 없는 증거는 게이트 전체를 닫습니다.
536
+ doctor가 파일과 벤더를 지목하며, 재서명 절차는 디스패치 스킬에 있습니다.
537
+
538
+ </details>
539
+
510
540
  ## 📊 기본값과 출처
511
541
 
512
542
  기본 레인 배치는 Artificial Analysis 2026-07 스냅샷(AA 사이트 원본 레코드와
@@ -528,6 +558,14 @@ work 는 지정한 디렉터리 안의 변경만 허용하며 모델 연결은
528
558
 
529
559
  ## 📜 릴리스 기록
530
560
 
561
+ ## v0.42.5 새 기능
562
+
563
+ - **CLI 하나를 업그레이드해도 모든 벤더가 막히지 않습니다.** overlay evidence 항목에 `vendor` 태그를 붙일 수 있으며, 태그가 있는 항목의 해시가 바뀌거나 파일이 사라지면 해당 벤더만 `unknown-target-runtime`으로 강등됩니다. 태그가 없는 evidence는 기존처럼 전체 fail-closed입니다.
564
+ - **`omnilane doctor`가 overlay를 로드합니다.** 새 `transport-overlay` 검사는 실패 시 문제가 된 파일과 벤더를 지목하고, 성공 시 벤더별 검증된 매핑 수를 보고합니다.
565
+ - **프로브가 판정을 기록합니다.** `probe.py`는 청구된 `modelUsage`로 Claude 응답을 판정하며, CLI가 알 수 없는 `--effort`를 기본값으로 조용히 대체한 경우를 실패로 처리합니다. `build_overlay.py`는 통과하지 못한 프로브에 서명하지 않고 overlay의 `unproven[]`에 기록합니다.
566
+ - **재빌드 도구를 버전 관리로.** `build_overlay.py`와 `probe.py`는 `scripts/lib/`로 옮겼고 `--root`를 받습니다.
567
+ - **업그레이드.** npm 게시 후 `npm i -g omnilane@0.42.5`를 실행하세요.
568
+
531
569
  ## v0.42.4 새 기능
532
570
 
533
571
  - **퀵스타트가 실제로 동작합니다.** `omnilane route` 는 «누가 요청하는지»를 알아야 하지만 60초 시작에 그 내용이 없어, 새 설치에서는 안내 없이 `missing-caller-context` 로 거부되었습니다. 이제 `OMNILANE_AA_OPERATOR_ASSERTED_HUMAN=1` 로 사람 운영자를 한 번 선언하며, 모델 호출자가 대신 전달할 것도 설명합니다.
package/README.md CHANGED
@@ -379,6 +379,7 @@ omnilane ui status # report whether the Live UI is r
379
379
  omnilane ui url # print the current authenticated local URL
380
380
  omnilane ui stop # stop the Live UI
381
381
  omnilane doctor [--json] [--strict] [--probe V] [--probe-timeout SEC] # live probe is opt-in
382
+ # transport-overlay check names a stale vendor
382
383
  omnilane benchmark [--json] [--run] [--vendor V] [--cost-per-call V=USD] # dry-run by default
383
384
  dispatch.sh [--background] [--dry-run] [--thread NAME] [--mode advise|work|sysops] [--workdir DIR]
384
385
  [--vendor V] [--model M] [--effort E] [--timeout SEC] [--job-timeout SEC]
@@ -615,6 +616,37 @@ so one command can never spiral into a chain of agents spending your quota.
615
616
 
616
617
  </details>
617
618
 
619
+ <details>
620
+ <summary><b>My dispatch was refused. Which refusal is it?</b></summary>
621
+
622
+ <br/>
623
+
624
+ Three codes, three different fixes. Run `omnilane doctor` first — its
625
+ `transport-overlay` check tells you immediately whether the problem is your
626
+ machine's configuration or your request.
627
+
628
+ `missing-caller-context` — you passed no identity. A human adds
629
+ `OMNILANE_AA_OPERATOR_ASSERTED_HUMAN=1` or `--operator-asserted-human`; a model
630
+ driving omnilane writes a `--caller-context FILE` with its exact vendor, model,
631
+ and effort, and must not assert the human exemption for itself.
632
+
633
+ `runtime-mapping-unverified` — your identity is fine, but the *target* has no
634
+ proven host-local request selector. Either it was never probed, or its probe
635
+ failed; `omnilane doctor` reports the count of such configurations and the
636
+ overlay's `unproven[]` records why each one failed. A refusal caused by a
637
+ provider quota limit will not clear until that quota does.
638
+
639
+ `invalid-policy-input` with "transport contract evidence changed" — neither of
640
+ the above. The overlay itself will not load, so *every* vendor is refused. The
641
+ usual cause is a vendor CLI upgrade: the overlay pins each vendor's executable
642
+ and runner-script hash, and Codex and Claude evidence paths embed version
643
+ directories, so an upgrade removes the file rather than changing its digest.
644
+ Tagged evidence degrades only its own vendor; untagged evidence, such as the
645
+ probe manifest, still closes the whole gate. Doctor names the file and the
646
+ vendor; the dispatch skill carries the re-signing runbook.
647
+
648
+ </details>
649
+
618
650
  ## 📊 Defaults and provenance
619
651
 
620
652
  Default lane assignments follow Artificial Analysis coding/intelligence data
@@ -638,6 +670,14 @@ working notes, including per-benchmark caveats, live in
638
670
 
639
671
  ## 📜 Release history
640
672
 
673
+ ## What's new in v0.42.5
674
+
675
+ - **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.
676
+ - **`omnilane doctor` loads the overlay.** A new `transport-overlay` check names the offending file and vendor on failure and reports per-vendor verified counts on success — an all-green doctor could previously coexist with a gate refusing every dispatch.
677
+ - **Probes record a verdict.** `probe.py` judges Claude responses on the billed `modelUsage` and fails a run where the CLI silently substituted the default effort for an unknown `--effort`. `build_overlay.py` refuses to sign a non-passing probe and records it in the overlay's `unproven[]` instead of dropping it silently.
678
+ - **Rebuild tooling is versioned.** `build_overlay.py` and `probe.py` live in `scripts/lib/` and take `--root`.
679
+ - **Upgrade.** After npm publication, run `npm i -g omnilane@0.42.5`. Existing repo-symlink installations can update their checkout and verify `omnilane --version` without rerunning installation.
680
+
641
681
  ## What's new in v0.42.4
642
682
 
643
683
  - **The quickstart actually runs now.** `omnilane route` needs to know who is asking; the 60-second start omitted that, so a fresh install hit `missing-caller-context` with no guidance. It now asserts the human operator once with `OMNILANE_AA_OPERATOR_ASSERTED_HUMAN=1`, and explains what a model caller passes instead.
package/README.zh-CN.md CHANGED
@@ -481,6 +481,33 @@ scripts/dispatch.sh --dry-run hardest-coding "…" # 完整解析后的计划,
481
481
 
482
482
  </details>
483
483
 
484
+ <details>
485
+ <summary><b>派工被拒了,是哪一种拒绝?</b></summary>
486
+
487
+ <br/>
488
+
489
+ 三个代码,三种不同的修法。先运行 `omnilane doctor`——它的 `transport-overlay`
490
+ 检查会直接告诉你问题出在本机配置还是你的请求。
491
+
492
+ `missing-caller-context`——你没带身份。真人加上
493
+ `OMNILANE_AA_OPERATOR_ASSERTED_HUMAN=1` 或 `--operator-asserted-human`;
494
+ 模型驱动 omnilane 时要写一份 `--caller-context FILE`,包含它精确的厂商、模型与
495
+ 强度,且不得替自己主张真人豁免。
496
+
497
+ `runtime-mapping-unverified`——你的身份没问题,但**目标**没有已验证的本机请求
498
+ 选择器。可能从未探测过,也可能探测失败;`omnilane doctor` 会报告这类配置的数量,
499
+ overlay 的 `unproven[]` 记录每一条失败的原因。因供应商额度上限造成的拒绝,
500
+ 在额度恢复前不会自行解除。
501
+
502
+ `invalid-policy-input` 搭配 "transport contract evidence changed"——以上皆非。
503
+ 是 overlay 本身加载失败,因此**所有厂商**都会被拒。常见成因是厂商 CLI 升级:
504
+ overlay 钉住每家的可执行文件与 runner 脚本哈希,而 Codex 与 Claude 的证据路径
505
+ 内嵌版本目录,升级后是文件消失而非哈希改变。带标签的证据只降级自己那一家;
506
+ 未标签的证据(例如探测清单)仍会关闭整个闸门。doctor 会指出文件与厂商,
507
+ 重签流程写在派工技能里。
508
+
509
+ </details>
510
+
484
511
  ## 📊 默认值与数据来源
485
512
 
486
513
  默认通道配置依据 Artificial Analysis 2026-07 快照(已对 AA 站上原始记录与
@@ -503,6 +530,14 @@ scripts/dispatch.sh --dry-run hardest-coding "…" # 完整解析后的计划,
503
530
 
504
531
  ## 📜 版本历程
505
532
 
533
+ ## v0.42.5 新功能
534
+
535
+ - **升级一支 CLI 不再阻断所有厂商。** overlay 的证据项目可带 `vendor` 标签;带标签的项目哈希漂移或文件消失时,只让该厂商降级为 `unknown-target-runtime`。未标签的证据维持全局 fail-closed。
536
+ - **`omnilane doctor` 会加载 overlay。** 新增 `transport-overlay` 检查,失败时指出是哪个文件、哪一家厂商,成功时报告各厂商的已验证映射数。
537
+ - **探测会记录判定。** `probe.py` 以计费的 `modelUsage` 判断 Claude 响应,并在 CLI 静默改用默认强度时判为失败。`build_overlay.py` 拒签未通过的探测,改记入 overlay 的 `unproven[]`。
538
+ - **重建工具纳入版本控制。** `build_overlay.py` 与 `probe.py` 移入 `scripts/lib/`,并接受 `--root`。
539
+ - **升级。** npm 发布后运行 `npm i -g omnilane@0.42.5`。既有的 repo symlink 安装更新检出后确认 `omnilane --version` 即可。
540
+
506
541
  ## v0.42.4 新功能
507
542
 
508
543
  - **快速上手现在真的跑得起来。** `omnilane route` 必须知道「是谁在问」,但 60 秒上手漏了这件事,新安装照抄会直接吃到 `missing-caller-context` 且没有任何指引。现在会先用 `OMNILANE_AA_OPERATOR_ASSERTED_HUMAN=1` 表明操作者身分,并说明模型主控该改用什么。
package/README.zh-TW.md CHANGED
@@ -525,6 +525,33 @@ scripts/dispatch.sh --dry-run hardest-coding "…" # 完整解析後的計畫,
525
525
 
526
526
  </details>
527
527
 
528
+ <details>
529
+ <summary><b>派工被拒了,是哪一種拒絕?</b></summary>
530
+
531
+ <br/>
532
+
533
+ 三個代碼,三種不同的修法。先跑 `omnilane doctor`——它的 `transport-overlay`
534
+ 檢查會直接告訴你問題出在本機設定還是你的請求。
535
+
536
+ `missing-caller-context`——你沒帶身分。真人加上
537
+ `OMNILANE_AA_OPERATOR_ASSERTED_HUMAN=1` 或 `--operator-asserted-human`;
538
+ 模型驅動 omnilane 時要寫一份 `--caller-context FILE`,內含它精確的廠商、模型與
539
+ 強度,且不得替自己主張真人豁免。
540
+
541
+ `runtime-mapping-unverified`——你的身分沒問題,但**目標**沒有已驗證的本機請求
542
+ 選擇器。可能從未探測過,也可能探測失敗;`omnilane doctor` 會回報這類設定的數量,
543
+ overlay 的 `unproven[]` 記錄每一條失敗的原因。因供應商額度上限造成的拒絕,
544
+ 在額度恢復前不會自行解除。
545
+
546
+ `invalid-policy-input` 搭配 "transport contract evidence changed"——以上皆非。
547
+ 是 overlay 本身載不起來,因此**所有廠商**都會被拒。常見成因是廠商 CLI 升級:
548
+ overlay 釘住每家的執行檔與 runner 腳本雜湊,而 Codex 與 Claude 的證據路徑內嵌
549
+ 版本目錄,升級後是檔案消失而非雜湊改變。帶標籤的證據只降級自己那一家;
550
+ 未標籤的證據(例如探測清單)仍會關閉整個閘門。doctor 會指名檔案與廠商,
551
+ 重簽流程寫在派工技能裡。
552
+
553
+ </details>
554
+
528
555
  ## 📊 預設值與資料來源
529
556
 
530
557
  預設通道配置依據 Artificial Analysis 2026-07 快照(已對 AA 站上原始紀錄與
@@ -544,6 +571,14 @@ scripts/dispatch.sh --dry-run hardest-coding "…" # 完整解析後的計畫,
544
571
 
545
572
  ## 📜 版本歷程
546
573
 
574
+ ## v0.42.5 新功能
575
+
576
+ - **升級一支 CLI 不再擋掉所有廠商。** overlay 的證據項目可帶 `vendor` 標籤;帶標籤的項目雜湊漂移或檔案消失時,只讓該廠商降級為 `unknown-target-runtime`。未標籤的證據維持全域 fail-closed。
577
+ - **`omnilane doctor` 會載入 overlay。** 新增 `transport-overlay` 檢查,失敗時指名是哪個檔案、哪一家廠商,成功時回報各廠商的已驗證映射數——先前 doctor 全綠可以和「閘門拒絕所有派工」同時成立。
578
+ - **探測會記錄判定。** `probe.py` 以計費的 `modelUsage` 判斷 Claude 回應,並在 CLI 靜默改用預設強度時判為失敗。`build_overlay.py` 拒簽未通過的探測,改記進 overlay 的 `unproven[]`,不再無聲丟棄。
579
+ - **重建工具納入版控。** `build_overlay.py` 與 `probe.py` 移入 `scripts/lib/`,並接受 `--root`。
580
+ - **升級。** npm 發布後執行 `npm i -g omnilane@0.42.5`。既有的 repo symlink 安裝更新檢出後確認 `omnilane --version` 即可,不需重跑安裝。
581
+
547
582
  ## v0.42.4 新功能
548
583
 
549
584
  - **快速上手現在真的跑得起來。** `omnilane route` 必須知道「是誰在問」,但 60 秒上手漏了這件事,新安裝照抄會直接吃到 `missing-caller-context` 且沒有任何指引。現在會先用 `OMNILANE_AA_OPERATOR_ASSERTED_HUMAN=1` 表明操作者身分,並說明模型主控該改用什麼。
package/VERSION CHANGED
@@ -1 +1 @@
1
- 0.42.4
1
+ 0.42.5
@@ -0,0 +1,99 @@
1
+ # Omnilane 0.42.5
2
+
3
+ This release stops one vendor's CLI upgrade from refusing every vendor's dispatch,
4
+ makes that failure visible in `omnilane doctor`, and stops the probe harness from
5
+ signing evidence it never read. Routing, scores, and the frozen AA registry are
6
+ unchanged.
7
+
8
+ ## Why
9
+
10
+ On 2026-09-09 `agy` was upgraded from 1.1.27 to 1.1.28. That changed its sha256,
11
+ and `apply_transport_overlay` compared every evidence hash in one loop and raised
12
+ on the first mismatch, so `load_registry` failed and **every** dispatch was
13
+ refused:
14
+
15
+ ```json
16
+ {"allowed":false,"code":"invalid-policy-input","message":"transport contract evidence changed"}
17
+ ```
18
+
19
+ Only the seven gemini mappings depended on that binary; the other forty-two were
20
+ collateral. `omnilane doctor` reported 19 passed, 0 failed throughout, because no
21
+ check ever loaded the overlay.
22
+
23
+ Re-probing showed the upgrade had changed nothing observable: all six agy
24
+ selectors still answered correctly. The outage was entirely the blast radius of a
25
+ config linter that had been given the authority of a security gate.
26
+
27
+ The same investigation found six `claude-fable-5-1` configurations that had been
28
+ unusable since 2026-09-07. Their probes had failed with a quota refusal, and
29
+ `build_overlay.py` simply omitted them — no record, no warning, nothing for an
30
+ operator to notice.
31
+
32
+ ## Changes
33
+
34
+ ### Evidence staleness is per vendor
35
+
36
+ - Overlay `evidence[]` entries take an optional `vendor` tag. A tagged entry whose
37
+ hash drifts, or whose file no longer exists, marks that vendor stale and skips
38
+ the `verified` upgrade for its mappings; `decide()` then returns
39
+ `unknown-target-runtime` for that vendor alone.
40
+ - Untagged entries keep the previous global fail-closed behaviour, so overlays
41
+ built before the tags exist are unaffected.
42
+ - Missing files are treated as staleness, not corruption: the codex and claude
43
+ evidence paths embed version directories, so their upgrades delete the file
44
+ rather than change its digest.
45
+ - Structural overlay checks — schema, snapshot, host, exact identity, selector
46
+ type, effort alignment — remain hard failures.
47
+
48
+ ### Doctor loads the overlay
49
+
50
+ - A new `transport-overlay` check loads the configured overlay through
51
+ `aa_policy.load_registry` and names the offending file and vendor on failure.
52
+ It reports per-vendor verified counts on success and warns when a vendor has
53
+ degraded. `live-capable` is unchanged: it answers whether a CLI supports a live
54
+ session, which stays true while the gate refuses the vendor.
55
+
56
+ ### Probes record a verdict
57
+
58
+ - `probe.py` now derives a `verdict`, `verdict_reason`, `observed_model`, and
59
+ `probed_at` from the raw evidence through a pure function, per vendor:
60
+ - Claude responses are judged on `modelUsage` — the billed model the CLI
61
+ reports — plus `is_error`, and are failed when stderr shows the CLI silently
62
+ substituted the default effort for an unknown `--effort`. Effort is half of a
63
+ scored identity, so that path would otherwise certify a mapping at the wrong
64
+ tier while exit status, `is_error`, `modelUsage`, and the expected token all
65
+ look correct.
66
+ - grok and agy reject invalid input outright, so exit status and a clean stderr
67
+ are sufficient.
68
+ - codex prints a banner to stderr on every run, so stderr is recorded for review
69
+ rather than treated as failure.
70
+ - `build_overlay.py` refuses to sign a non-passing probe and records it in a new
71
+ `unproven[]` block with its reason, so a quota refusal is visible instead of
72
+ silently dropping configurations. Evidence predating the verdict field is
73
+ still signed, with a warning naming each legacy entry.
74
+ - The six `claude-fable-5-1` and `claude-fable-5` configurations are listed in
75
+ the probe table so their failures reach `unproven[]`. Re-probing them on
76
+ 2026-09-09 returned the same quota refusal as on 09-07, so they remain
77
+ unusable — but `omnilane doctor` now says so rather than leaving an operator
78
+ to discover it from a refused dispatch.
79
+ - `build_overlay.py` and `probe.py` moved from an untracked `.rollback` sweep
80
+ directory into `scripts/lib/`, and take `--root`; overlay rebuilds no longer
81
+ depend on a directory that a cleanup can delete.
82
+
83
+ ## Verification boundary
84
+
85
+ A passing probe proves the CLI accepted the selector and, for Claude, that the
86
+ billed model matches the request. It still does not certify upstream provider
87
+ identity, and `upstream_identity_verified` remains `false`. grok, agy, and codex
88
+ expose no equivalent of `modelUsage` in the evidence captured so far.
89
+
90
+ The frozen AA registry and its approved SHA are unchanged. Coverage remains 78
91
+ scored targets, and the host overlay still verifies 49 mappings — codex 26,
92
+ claude 11, gemini 7, grok 5.
93
+
94
+ ## Upgrade
95
+
96
+ Existing overlays keep working untouched. To gain per-vendor degradation, rebuild
97
+ the overlay with the new `build_overlay.py` so its evidence carries vendor tags.
98
+ Run `omnilane doctor` afterwards and confirm the `transport-overlay` check reports
99
+ the vendor counts you expect.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "omnilane",
3
- "version": "0.42.4",
3
+ "version": "0.42.5",
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"
@@ -29,7 +29,7 @@
29
29
  "docs/model-capabilities-2026-09.md",
30
30
  "docs/native-executor.md",
31
31
  "docs/completion-wakeup.md",
32
- "docs/release-notes-0.42.4.md",
32
+ "docs/release-notes-0.42.5.md",
33
33
  "hooks/",
34
34
  "skills/",
35
35
  ".claude-plugin/",
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",
4
+ "version": "0.42.5",
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/doctor.sh CHANGED
@@ -33,6 +33,7 @@ REPO="${OMNILANE_DOCTOR_REPO:-$SCRIPT_ROOT}"
33
33
  OMNILANE_HOME="${OMNILANE_HOME:-$HOME/.omnilane}"
34
34
  PROBE_SCRIPT="${OMNILANE_PROVIDER_PROBE_SCRIPT:-$REPO/scripts/provider-probe.sh}"
35
35
  GOAL_LOOP="${OMNILANE_DOCTOR_GOAL_LOOP:-$REPO/scripts/lib/goal-loop.sh}"
36
+ OVERLAY_HEALTH="${OMNILANE_DOCTOR_OVERLAY_HEALTH:-$REPO/scripts/lib/overlay_health.py}"
36
37
  # shellcheck disable=SC1091
37
38
  source "$SCRIPT_ROOT/scripts/lib/live-protocol.sh"
38
39
  PASS_COUNT=0
@@ -409,6 +410,31 @@ else
409
410
  report PASS live-unavailable "none"
410
411
  fi
411
412
 
413
+ # live-capable above answers "does this CLI support a live session", which stays
414
+ # true while the AA gate refuses every dispatch. Nothing else loads the overlay,
415
+ # so one drifted evidence hash used to go unreported by an all-green doctor.
416
+ overlay_path="$(
417
+ set +u
418
+ [[ -f "$OMNILANE_HOME/local.sh" ]] && . "$OMNILANE_HOME/local.sh" 2>/dev/null
419
+ printf '%s' "${OMNILANE_AA_TRANSPORT_OVERLAY:-}"
420
+ )"
421
+ if [[ -z "$overlay_path" ]]; then
422
+ report PASS transport-overlay "no overlay configured; every runtime mapping stays unverified"
423
+ elif ! command -v python3 >/dev/null 2>&1; then
424
+ report WARN transport-overlay "python3 is absent; cannot load the AA transport overlay"
425
+ elif [[ ! -r "$OVERLAY_HEALTH" ]]; then
426
+ report WARN transport-overlay "$OVERLAY_HEALTH is missing"
427
+ else
428
+ overlay_line="$(OMNILANE_AA_TRANSPORT_OVERLAY="$overlay_path" \
429
+ python3 "$OVERLAY_HEALTH" "$REPO" 2>&1)"
430
+ overlay_level="${overlay_line%% *}"
431
+ overlay_message="${overlay_line#* }"
432
+ case "$overlay_level" in
433
+ PASS|WARN|FAIL) report "$overlay_level" transport-overlay "$overlay_message" ;;
434
+ *) report WARN transport-overlay "unreadable overlay health output: $overlay_line" ;;
435
+ esac
436
+ fi
437
+
412
438
  if [[ -n "$PROBE_VENDOR" ]]; then
413
439
  if [[ ! -x "$PROBE_SCRIPT" ]]; then
414
440
  report FAIL provider-probe "probe runner is unavailable"
@@ -28,6 +28,7 @@ MAX_BYTES = 1_048_576
28
28
  APPROVED_REGISTRY_SHA256 = "0782c87de123c02738c3ff60e4bc3c1cc10d110113e872b8f8627212861cdaab"
29
29
 
30
30
  IDENTITY_FIELDS = ("vendor", "model", "effort", "reasoning", "fallback")
31
+ TRANSPORT_EVIDENCE_VENDORS = frozenset(("codex", "claude", "grok", "gemini"))
31
32
  IDENTIFIER = re.compile(r"[A-Za-z0-9][A-Za-z0-9._:/-]{0,255}\Z")
32
33
 
33
34
 
@@ -147,14 +148,35 @@ def apply_transport_overlay(registry: dict[str, Any]) -> None:
147
148
  _check(overlay.get("schema_version") == 1, "unsupported transport overlay")
148
149
  _check(overlay.get("snapshot_id") == registry["snapshot"]["id"], "transport overlay snapshot mismatch")
149
150
  _check(overlay.get("host") == socket.gethostname(), "transport overlay host mismatch")
151
+ stale_vendors: set[str] = set()
150
152
  for evidence in overlay.get("evidence", []):
151
- fd = os.open(evidence["path"], os.O_RDONLY | os.O_NOFOLLOW | os.O_NONBLOCK)
153
+ _check(isinstance(evidence, dict), "invalid transport evidence")
154
+ vendor = evidence.get("vendor")
155
+ _check(
156
+ "vendor" not in evidence
157
+ or type(vendor) is str and vendor in TRANSPORT_EVIDENCE_VENDORS,
158
+ "invalid transport evidence vendor",
159
+ )
160
+ evidence_path = evidence["path"]
161
+ evidence_sha256 = evidence["sha256"]
162
+ _check(type(evidence_path) is str, "invalid transport evidence path")
163
+ _check(type(evidence_sha256) is str, "invalid transport evidence digest")
164
+ try:
165
+ fd = os.open(evidence_path, os.O_RDONLY | os.O_NOFOLLOW | os.O_NONBLOCK)
166
+ except FileNotFoundError:
167
+ if vendor is None:
168
+ raise
169
+ stale_vendors.add(vendor)
170
+ continue
152
171
  with os.fdopen(fd, "rb") as stream:
153
172
  _check(stat.S_ISREG(os.fstat(stream.fileno()).st_mode), "invalid transport evidence file")
154
173
  digest_file = hashlib.sha256()
155
174
  for block in iter(lambda: stream.read(1024 * 1024), b""):
156
175
  digest_file.update(block)
157
- _check(digest_file.hexdigest() == evidence["sha256"], "transport contract evidence changed")
176
+ if digest_file.hexdigest() != evidence_sha256:
177
+ if vendor is None:
178
+ _check(False, "transport contract evidence changed")
179
+ stale_vendors.add(vendor)
158
180
  _check(bool(overlay.get("evidence")), "transport overlay requires local evidence")
159
181
  for mapping in overlay.get("mappings", []):
160
182
  rows = [row for row in registry["scored_configs"] if row["id"] == mapping.get("config_id")]
@@ -175,6 +197,8 @@ def apply_transport_overlay(registry: dict[str, Any]) -> None:
175
197
  _check(mapping["runtime_model"].endswith("-" + row["effort"]), "encoded effort does not match exact tuple")
176
198
  else:
177
199
  _check(mapping.get("runtime_model") == row["model"], "overlay model mismatch")
200
+ if row["vendor"] in stale_vendors:
201
+ continue
178
202
  row["transport_mapping"].update(
179
203
  status="verified", runtime_verified=True,
180
204
  runtime_model=mapping["runtime_model"], runtime_effort=mapping["runtime_effort"],
@@ -183,6 +207,7 @@ def apply_transport_overlay(registry: dict[str, Any]) -> None:
183
207
  verification="request-selector-contract", upstream_identity_verified=False,
184
208
  overlay_sha256=digest, overlay_host=overlay["host"],
185
209
  )
210
+ registry["_stale_transport_vendors"] = sorted(stale_vendors)
186
211
 
187
212
 
188
213
  def load_registry(path: str | Path, expected_sha256: str | None = None) -> tuple[dict[str, Any], str]:
@@ -233,6 +258,8 @@ def _runtime_target(registry: dict[str, Any], vendor: str, model: str,
233
258
  exact_id = [row for row in registry["scored_configs"] if row["id"] == target_config] if target_config else registry["scored_configs"]
234
259
  if target_config and not exact_id:
235
260
  return None, "unknown-target-config", {"target_config": target_config}
261
+ if vendor in registry.get("_stale_transport_vendors", []):
262
+ return None, "unknown-target-runtime", {"vendor": vendor, "model": model, "effort": effort}
236
263
  vendor_rows = [row for row in exact_id if row["vendor"] == vendor]
237
264
  candidates: list[dict[str, Any]] = []
238
265
  unresolved: list[str] = []
@@ -0,0 +1,189 @@
1
+ #!/usr/bin/env python3
2
+ """Build the merged omnilane AA transport overlay from verified probe evidence.
3
+
4
+ Only (config_id, selector) pairs listed in PROVEN are written. Every entry here
5
+ corresponds to a probe run under the selected evidence root whose raw
6
+ stdout/stderr is hashed into the manifest, so the overlay's evidence[] anchors
7
+ the whole set.
8
+ """
9
+ import argparse
10
+ import hashlib
11
+ import json
12
+ import os
13
+ import socket
14
+ from datetime import datetime, timezone
15
+ from pathlib import Path
16
+
17
+ REPO = Path(os.environ.get("OMNILANE_REPO", "/Users/vincentw/dev/omnilane"))
18
+ HOME = Path.home()
19
+ SWEEP_ID = os.environ.get("OMNILANE_TRANSPORT_SWEEP_ID", "overlay-reprobe-20260909")
20
+ DEFAULT_ROOT = HOME / ".omnilane" / "transport-evidence" / SWEEP_ID
21
+ REGISTRY = json.loads((REPO / "config/aa-model-policy.json").read_text())
22
+ ROWS = {r["id"]: r for r in REGISTRY["scored_configs"]}
23
+ IDENTITY_FIELDS = ("vendor", "model", "effort", "reasoning", "fallback")
24
+
25
+ # config_id -> (selector_type, runtime_model, probe evidence basename)
26
+ PROVEN: dict[str, tuple[str, str, str]] = {}
27
+
28
+ for model, slug in [("gpt-6-astra", "gpt-6-astra"), ("gpt-5.6-sol", "gpt-5_6-sol"),
29
+ ("gpt-5.6-luna", "gpt-5_6-luna"), ("gpt-5.6-terra", "gpt-5_6-terra")]:
30
+ base = model.replace(".", "-").replace("gpt-", "gpt-")
31
+ for effort in ["max", "xhigh", "high", "medium", "low"]:
32
+ cid = f"codex/{model.replace('.', '-')}" + ("" if effort == "max" else f"-{effort}")
33
+ ev = f"cx-avail-{model.replace('.', '_')}" if effort == "high" else f"cx-{model.replace('.', '_')}-{effort}"
34
+ PROVEN[cid] = ("model_and_effort", model, ev)
35
+
36
+ for effort in ["xhigh", "medium"]:
37
+ PROVEN[f"codex/gpt-5-4-mini" + ("" if effort == "xhigh" else f"-{effort}")] = (
38
+ "model_and_effort", "gpt-5.4-mini", f"cx-gpt-5_4-mini-{effort}")
39
+
40
+ PROVEN["grok/grok-4-6"] = ("cli_reasoning_effort", "grok-4.6", "PRIOR:grok-effort-2026-09-07")
41
+ for effort in ["xhigh", "medium", "low"]:
42
+ PROVEN[f"grok/grok-4-6-{effort}"] = ("cli_reasoning_effort", "grok-4.6", f"gk-grok-4_6-{effort}")
43
+ PROVEN["grok/grok-4-5"] = ("cli_reasoning_effort", "grok-4.5", "gk-grok-4_5-high")
44
+
45
+ for cid, rid, ev in [
46
+ ("gemini/gemini-3-8-flash", "gemini-3.8-flash-high", "PRIOR:gemini-flash-high"),
47
+ ("gemini/gemini-3-8-flash-medium", "gemini-3.8-flash-medium", "agy-gemini-3_8-flash-medium"),
48
+ ("gemini/gemini-3-8-flash-low", "gemini-3.8-flash-low", "agy-gemini-3_8-flash-low"),
49
+ ("gemini/gemini-3-7-flash", "gemini-3.7-flash-high", "agy-gemini-3_7-flash-high"),
50
+ ("gemini/gemini-3-7-flash-medium", "gemini-3.7-flash-medium", "agy-gemini-3_7-flash-medium"),
51
+ ("gemini/gemini-3-7-flash-low", "gemini-3.7-flash-low", "agy-gemini-3_7-flash-low"),
52
+ ("gemini/gemini-3-6-flash", "gemini-3.6-flash-high", "agy-gemini-3_6-flash-high"),
53
+ ]:
54
+ PROVEN[cid] = ("model_id_encoded_effort", rid, ev)
55
+
56
+ for effort in ["max", "xhigh", "high", "medium", "low"]:
57
+ cid = "claude/claude-opus-5" + ("" if effort == "max" else f"-{effort}")
58
+ PROVEN[cid] = ("model_and_effort", "claude-opus-5", f"cl-claude-opus-5-{effort}")
59
+ # gpt-6-astra rejects effort "none" upstream ("Unsupported value: 'none' is not
60
+ # supported with the 'gpt-6-astra' model"), so it has no non-reasoning selector.
61
+ for model in ["gpt-5.6-sol", "gpt-5.6-terra", "gpt-5.6-luna", "gpt-5.4-mini"]:
62
+ PROVEN[f"codex/{model.replace('.', '-')}-non-reasoning"] = (
63
+ "model_and_effort", model, f"cx-{model.replace('.', '_')}-none")
64
+
65
+ # Default (no --effort) Haiku 4.5 spent 124 thinking tokens, so the selector
66
+ # lands on the reasoning row rather than its non-reasoning sibling.
67
+ PROVEN["claude/claude-4-5-haiku-reasoning"] = (
68
+ "model_and_effort", "claude-haiku-4-5", "cl-rmode-claude-haiku-4-5-noeffort")
69
+
70
+ for cid, model in [("claude/claude-sonnet-5", "claude-sonnet-5"),
71
+ ("claude/claude-opus-4-8", "claude-opus-4-8"),
72
+ ("claude/claude-opus-4-7", "claude-opus-4-7"),
73
+ ("claude/claude-opus-4-6-adaptive", "claude-opus-4-6"),
74
+ ("claude/claude-sonnet-4-6-adaptive", "claude-sonnet-4-6")]:
75
+ PROVEN[cid] = ("model_and_effort", model, f"cl-{model}-max")
76
+
77
+ # Fable is listed so its failures reach unproven[] rather than vanishing. Its
78
+ # probes were refused for quota on 2026-09-07 and again on 2026-09-09; the
79
+ # verdict decides whether these rows become mappings or stay visible failures.
80
+ for effort in ["max", "xhigh", "high", "medium", "low"]:
81
+ cid = "claude/claude-fable-5-1" + ("" if effort == "max" else f"-{effort}")
82
+ PROVEN[cid] = ("model_and_effort", "claude-fable-5-1", f"cl-claude-fable-5-1-{effort}")
83
+ PROVEN["claude/claude-fable-5"] = (
84
+ "model_and_effort", "claude-fable-5", "cl-claude-fable-5-max")
85
+
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
+ ]
96
+
97
+
98
+ def sha256(path: Path) -> str:
99
+ digest = hashlib.sha256()
100
+ with open(path, "rb") as stream:
101
+ for block in iter(lambda: stream.read(1 << 20), b""):
102
+ digest.update(block)
103
+ return digest.hexdigest()
104
+
105
+
106
+ def main(argv: list[str] | None = None) -> None:
107
+ parser = argparse.ArgumentParser(description=__doc__)
108
+ parser.add_argument(
109
+ "--root",
110
+ type=Path,
111
+ default=DEFAULT_ROOT,
112
+ help=f"probe sweep root (default: {DEFAULT_ROOT})",
113
+ )
114
+ args = parser.parse_args(argv)
115
+ root = args.root.expanduser()
116
+
117
+ manifest = {"probe_runs": {}}
118
+ unproven = []
119
+ 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
+ entry = {}
124
+ for suffix in ("json", "stdout", "stderr"):
125
+ path = root / "evidence" / f"{ev}.{suffix}"
126
+ if path.exists():
127
+ entry[suffix] = {"path": str(path), "sha256": sha256(path)}
128
+ if "json" not in entry:
129
+ raise SystemExit(f"missing probe evidence for {cid}: {ev}")
130
+ descriptor_path = Path(entry["json"]["path"])
131
+ descriptor = json.loads(descriptor_path.read_text())
132
+ if not isinstance(descriptor, dict):
133
+ raise SystemExit(f"invalid probe descriptor for {cid}: {ev}")
134
+ if "verdict" not in descriptor:
135
+ print(f"warning: legacy evidence (verdict=unknown): {cid}: {ev}")
136
+ elif descriptor["verdict"] != "pass":
137
+ unproven.append({
138
+ "config_id": cid,
139
+ "verdict_reason": descriptor.get("verdict_reason") or f"verdict: {descriptor['verdict']}",
140
+ "observed_model": descriptor.get("observed_model"),
141
+ "probed_at": descriptor.get("probed_at") or datetime.fromtimestamp(
142
+ descriptor_path.stat().st_mtime, timezone.utc).isoformat(),
143
+ })
144
+ # Visibility only: failed evidence must not enter the signed manifest.
145
+ continue
146
+ manifest["probe_runs"][cid] = entry
147
+ manifest_path = root / "probe-manifest.json"
148
+ manifest_path.write_text(json.dumps(manifest, indent=2, sort_keys=True) + "\n")
149
+
150
+ mappings = []
151
+ for cid, (selector, runtime_model, _) in sorted(PROVEN.items()):
152
+ if cid not in manifest["probe_runs"]:
153
+ continue
154
+ row = ROWS[cid]
155
+ mapping = {
156
+ "config_id": cid,
157
+ "identity": {key: row[key] for key in IDENTITY_FIELDS},
158
+ "runtime_model": runtime_model,
159
+ "runtime_effort": row["effort"],
160
+ "selector_type": selector,
161
+ "verification": "request-selector-contract",
162
+ }
163
+ if selector == "cli_reasoning_effort":
164
+ mapping["cli_flag"] = "--reasoning-effort"
165
+ mappings.append(mapping)
166
+
167
+ evidence = [
168
+ {"path": str(path), "sha256": sha256(path), "vendor": vendor}
169
+ for path, vendor in CORE_EVIDENCE
170
+ ]
171
+ evidence.append({"path": str(manifest_path), "sha256": sha256(manifest_path)})
172
+
173
+ overlay = {
174
+ "schema_version": 1,
175
+ "snapshot_id": REGISTRY["snapshot"]["id"],
176
+ "host": socket.gethostname(),
177
+ "source": ("claude-code / MacStudio / operator-directed full sweep 2026-09-07; "
178
+ "gemini selectors re-probed 2026-09-09 after agy 1.1.27 -> 1.1.28"),
179
+ "evidence": evidence,
180
+ "mappings": mappings,
181
+ "unproven": unproven,
182
+ }
183
+ out = root / "transport-contracts.local.json"
184
+ out.write_text(json.dumps(overlay, indent=2, ensure_ascii=False) + "\n")
185
+ print(f"wrote {out} with {len(mappings)} mappings and {len(evidence)} evidence anchors")
186
+
187
+
188
+ if __name__ == "__main__":
189
+ main()
@@ -0,0 +1,91 @@
1
+ #!/usr/bin/env python3
2
+ """Report whether the configured AA transport overlay still loads.
3
+
4
+ Prints one `LEVEL<TAB>message` line for `omnilane doctor`. Always exits 0; the
5
+ caller decides how to grade the level. Nothing here contacts a provider.
6
+
7
+ Exists because no other doctor check observes the AA gate: every vendor CLI can
8
+ be reachable and every lane resolvable while `load_registry` refuses the whole
9
+ registry over one drifted evidence hash.
10
+ """
11
+ import hashlib
12
+ import os
13
+ import sys
14
+ from collections import Counter
15
+ from pathlib import Path
16
+
17
+ REPO = Path(sys.argv[1]) if len(sys.argv) > 1 else Path(__file__).resolve().parents[1]
18
+ sys.path.insert(0, str(REPO / "scripts" / "lib"))
19
+
20
+
21
+ def emit(level: str, message: str) -> None:
22
+ print(f"{level}\t{message}")
23
+ raise SystemExit(0)
24
+
25
+
26
+ def digest(path: Path) -> str:
27
+ value = hashlib.sha256()
28
+ with open(path, "rb") as stream:
29
+ for block in iter(lambda: stream.read(1024 * 1024), b""):
30
+ value.update(block)
31
+ return value.hexdigest()
32
+
33
+
34
+ def offenders(overlay_path: Path) -> list[str]:
35
+ """Name the evidence entries that no longer match, for an actionable report."""
36
+ import json
37
+
38
+ try:
39
+ overlay = json.loads(overlay_path.read_text())
40
+ except (OSError, ValueError):
41
+ return []
42
+ found = []
43
+ for entry in overlay.get("evidence", []):
44
+ path = Path(entry.get("path", ""))
45
+ tag = entry.get("vendor") or "untagged"
46
+ if not path.exists():
47
+ found.append(f"{tag}:missing {path}")
48
+ elif digest(path) != entry.get("sha256"):
49
+ found.append(f"{tag}:hash drift {path}")
50
+ return found
51
+
52
+
53
+ def main() -> None:
54
+ overlay_path = os.environ.get("OMNILANE_AA_TRANSPORT_OVERLAY", "")
55
+ if not overlay_path:
56
+ emit("PASS", "no overlay configured; every runtime mapping stays unverified")
57
+ if not Path(overlay_path).exists():
58
+ emit("FAIL", f"configured overlay is missing: {overlay_path}")
59
+
60
+ try:
61
+ import aa_policy
62
+ except ImportError as error:
63
+ emit("WARN", f"cannot import aa_policy: {error}")
64
+
65
+ try:
66
+ registry, _ = aa_policy.load_registry(str(REPO / "config" / "aa-model-policy.json"))
67
+ except Exception as error: # PolicyError, OSError, and anything else fails the gate
68
+ detail = "; ".join(offenders(Path(overlay_path))) or str(error)
69
+ emit("FAIL", f"overlay rejected, every dispatch is refused: {error} ({detail})")
70
+
71
+ verified = Counter()
72
+ for row in registry["scored_configs"]:
73
+ 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"
76
+
77
+ import json
78
+
79
+ overlay = json.loads(Path(overlay_path).read_text())
80
+ unproven = overlay.get("unproven", [])
81
+ extra = f"; {len(unproven)} config(s) recorded unproven" if unproven else ""
82
+
83
+ stale = registry.get("_stale_transport_vendors", [])
84
+ if stale:
85
+ detail = "; ".join(o for o in offenders(Path(overlay_path))) or "unknown cause"
86
+ emit("WARN", f"stale vendor(s) {', '.join(stale)} degraded to unverified "
87
+ f"({detail}); still verified: {summary}{extra}")
88
+ emit("PASS", f"verified mappings: {summary}{extra}")
89
+
90
+
91
+ main()
@@ -0,0 +1,184 @@
1
+ #!/usr/bin/env python3
2
+ """Request-selector probe harness for the omnilane AA transport overlay.
3
+
4
+ Runs one CLI invocation, captures raw stdout/stderr to files, and writes a
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.
7
+ """
8
+ import argparse
9
+ import json
10
+ import os
11
+ import subprocess
12
+ import time
13
+ from datetime import datetime, timezone
14
+ from pathlib import Path
15
+
16
+ SWEEP_ID = os.environ.get("OMNILANE_TRANSPORT_SWEEP_ID", "overlay-reprobe-20260909")
17
+ DEFAULT_ROOT = Path.home() / ".omnilane" / "transport-evidence" / SWEEP_ID
18
+
19
+
20
+ def verdict(
21
+ evidence_json: dict,
22
+ stdout_text: str,
23
+ stderr_text: str,
24
+ vendor: str,
25
+ expected_token: str | None,
26
+ ) -> tuple[str, str, str | None]:
27
+ """Judge raw evidence without reading files, running commands or mutating it.
28
+
29
+ Only Claude's billed modelUsage keys currently prove the responding model.
30
+ A requested selector (including the Codex banner) is not observed identity.
31
+ """
32
+ if evidence_json.get("timed_out"):
33
+ return "fail", "timeout", None
34
+ if not expected_token:
35
+ return "fail", "missing-expected-token", None
36
+
37
+ exit_code = evidence_json.get("exit_code")
38
+ if vendor == "claude":
39
+ try:
40
+ response = json.loads(stdout_text)
41
+ except (json.JSONDecodeError, TypeError):
42
+ return "fail", "invalid-json", None
43
+ if not isinstance(response, dict):
44
+ return "fail", "invalid-json-result", None
45
+ usage = response.get("modelUsage")
46
+ models = sorted(usage) if isinstance(usage, dict) else []
47
+ observed_model = ", ".join(models) or None
48
+ result = response.get("result", "")
49
+ if not isinstance(result, str):
50
+ result = str(result)
51
+ if response.get("is_error"):
52
+ lower_result = result.lower()
53
+ if "limit" in lower_result or "quota" in lower_result:
54
+ reason = "quota-exhausted"
55
+ elif "api error" in lower_result:
56
+ reason = "api-error"
57
+ else:
58
+ reason = "result-error"
59
+ return "fail", f"{reason}: {result[:120]}", observed_model
60
+ 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]
69
+ if not requested_model:
70
+ return "fail", "missing-requested-model", observed_model
71
+ if models != [requested_model]:
72
+ return "fail", "model-mismatch", observed_model
73
+ if "unknown --effort" in stderr_text.lower():
74
+ return "fail", "effort-silently-defaulted", observed_model
75
+ if exit_code != 0:
76
+ return "fail", f"exit-code: {exit_code}", observed_model
77
+ if expected_token not in result:
78
+ return "fail", "missing-expected-token", observed_model
79
+ return "pass", "expected-token-and-model-matched", observed_model
80
+
81
+ if vendor in ("grok", "agy"):
82
+ if exit_code != 0:
83
+ return "fail", f"exit-code: {exit_code}: {stderr_text[:120]}", None
84
+ if stderr_text:
85
+ return "fail", f"unexpected-stderr: {stderr_text[:120]}", None
86
+ if expected_token not in stdout_text:
87
+ return "fail", "missing-expected-token", None
88
+ return "pass", "expected-token-and-clean-stderr", None
89
+
90
+ if vendor == "codex":
91
+ diagnostics = [line[:120] for line in stderr_text.splitlines()
92
+ if "error" in line.lower() or "warning" in line.lower()]
93
+ review = "; stderr-review: " + " | ".join(diagnostics) if diagnostics else ""
94
+ if exit_code != 0:
95
+ return "fail", f"exit-code: {exit_code}{review}", None
96
+ 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
99
+
100
+ return "fail", f"unsupported-vendor: {vendor}", None
101
+
102
+
103
+ def probe(
104
+ name: str,
105
+ argv: list[str],
106
+ timeout: int = 180,
107
+ cwd: Path | None = None,
108
+ root: Path | None = None,
109
+ *,
110
+ vendor: str | None = None,
111
+ expected_token: str | None = None,
112
+ ) -> dict:
113
+ if not expected_token:
114
+ raise ValueError("expected_token is required before running a probe")
115
+ if not argv:
116
+ raise ValueError("command is required")
117
+ vendor = vendor or Path(argv[0]).name
118
+ if vendor not in ("claude", "grok", "agy", "codex"):
119
+ raise ValueError(f"unsupported vendor: {vendor}; pass vendor explicitly")
120
+ root = (root or DEFAULT_ROOT).expanduser()
121
+ evidence = root / "evidence"
122
+ work = root / "work"
123
+ evidence.mkdir(parents=True, exist_ok=True)
124
+ work.mkdir(parents=True, exist_ok=True)
125
+ out_path = evidence / f"{name}.stdout"
126
+ err_path = evidence / f"{name}.stderr"
127
+ started = time.time()
128
+ timed_out = False
129
+ env = dict(os.environ)
130
+ # The runners drop the API key so the subscription OAuth path is used.
131
+ env.pop("XAI_API_KEY", None)
132
+ with open(out_path, "wb") as out, open(err_path, "wb") as err:
133
+ proc = subprocess.Popen(argv, stdout=out, stderr=err, stdin=subprocess.DEVNULL,
134
+ cwd=str(cwd or work), env=env)
135
+ try:
136
+ rc = proc.wait(timeout=timeout)
137
+ except subprocess.TimeoutExpired:
138
+ timed_out = True
139
+ proc.kill()
140
+ rc = proc.wait()
141
+ record = {
142
+ "command": argv,
143
+ "cwd": str(cwd or work),
144
+ "exit_code": rc,
145
+ "timed_out": timed_out,
146
+ "elapsed_seconds": round(time.time() - started, 3),
147
+ "probed_at": datetime.fromtimestamp(started, timezone.utc).isoformat(),
148
+ "stdout": str(out_path),
149
+ "stderr": str(err_path),
150
+ }
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
+ )
155
+ (evidence / f"{name}.json").write_text(json.dumps(record, indent=2) + "\n")
156
+ return record
157
+
158
+
159
+ def read(
160
+ name: str,
161
+ stream: str = "stdout",
162
+ limit: int = 4000,
163
+ root: Path | None = None,
164
+ ) -> str:
165
+ path = (root or DEFAULT_ROOT).expanduser() / "evidence" / f"{name}.{stream}"
166
+ if not path.exists():
167
+ return ""
168
+ return path.read_text(errors="replace")[:limit]
169
+
170
+
171
+ if __name__ == "__main__":
172
+ parser = argparse.ArgumentParser(description=__doc__)
173
+ parser.add_argument("--root", type=Path, default=DEFAULT_ROOT)
174
+ parser.add_argument("--expect", required=True, help="expected response token")
175
+ parser.add_argument("--vendor", choices=("claude", "grok", "agy", "codex"),
176
+ help="defaults to the command executable's basename")
177
+ parser.add_argument("name")
178
+ parser.add_argument("command", nargs=argparse.REMAINDER)
179
+ args = parser.parse_args()
180
+ if not args.command:
181
+ parser.error("command is required")
182
+ rec = probe(args.name, args.command, root=args.root,
183
+ vendor=args.vendor, expected_token=args.expect)
184
+ print(json.dumps(rec, ensure_ascii=False))
@@ -371,12 +371,55 @@ closes lanes and pushes the question back onto the operator. Never raise the
371
371
  declared effort to unblock a refused target, and never assert
372
372
  `--operator-asserted-human` on your own behalf.
373
373
 
374
- Two refusal codes mean different things and need different fixes.
374
+ Three refusal codes mean different things and need different fixes.
375
375
  `missing-caller-context` means you passed no file — write one.
376
376
  `runtime-mapping-unverified` means the file is fine but the *target* has no proven
377
377
  host-local request selector; that is fixed by a `--transport-overlay` entry backed
378
378
  by real evidence, never by editing the frozen registry (its sha256 is pinned in
379
379
  `scripts/lib/aa_policy.py`, so any edit fails the whole gate closed).
380
+ `invalid-policy-input` with "transport contract evidence changed" is neither: the
381
+ overlay itself will not load, so nothing about your caller or your target is wrong.
382
+ Run `omnilane doctor` first — its `transport-overlay` check names the offending
383
+ file and the vendor it belongs to. Do not go hunting by hand.
384
+
385
+ Upgrading a vendor CLI is the usual cause. The overlay pins the sha256 of each
386
+ vendor's executable and runner script, so a new release invalidates that vendor's
387
+ selector evidence. Evidence entries carry a `vendor` tag: a tagged entry that
388
+ drifts marks only its own vendor stale, and the other three keep dispatching.
389
+ Untagged evidence — `probe-manifest.json`, and any overlay built before the tags
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.
394
+
395
+ Re-signing is a probe, a rebuild, and an install, in that order. Back up
396
+ `~/.omnilane/transport-contracts.local.json` first; restoring it is the rollback.
397
+ `scripts/lib/probe.py --expect TOKEN [--vendor V] NAME COMMAND…` invokes the CLI
398
+ directly through `subprocess`, so it works while the gate is refusing everything —
399
+ this is what breaks the deadlock. `scripts/provider-probe.sh` goes through
400
+ `dispatch.sh` and therefore through the gate, so it is useless in this state.
401
+ Then `scripts/lib/build_overlay.py` rebuilds, and you copy the result over the
402
+ live overlay. Verify with a real dispatch on a lane belonging to the vendor you
403
+ re-probed; loading the registry in Python is not the runtime surface.
404
+
405
+ Keep the sweep where its default `--root` puts it,
406
+ `~/.omnilane/transport-evidence/<sweep-id>/`. The rebuilt overlay anchors
407
+ `probe-manifest.json` by absolute path as untagged evidence, so a sweep parked
408
+ inside a repository is one `git clean -fdx` away from taking every vendor down
409
+ at once — the same global refusal a re-signing session is usually trying to end.
410
+
411
+ Never sign a probe you did not read. `probe.py` records a `verdict` because exit
412
+ status alone is not evidence: the Claude CLI answers a quota refusal with a JSON
413
+ body carrying `is_error`, and it accepts an unknown `--effort` by silently using
414
+ the default, returning exit 0, the right `modelUsage`, and the expected token
415
+ with only a stderr warning to show for it. Effort is half of a scored identity,
416
+ so that path would certify a mapping at the wrong tier. Configurations whose
417
+ probes failed are recorded in the overlay's `unproven[]` and surfaced by doctor
418
+ instead of vanishing — six Fable rows sat unusable for two days in September
419
+ 2026 because a 429 quota refusal left no trace anywhere. A refused probe is not
420
+ always transient: re-probing those six two days later returned the same 429, so
421
+ an `unproven[]` entry can mean the account, not the moment. Read the reason
422
+ before assuming a retry will clear it.
380
423
 
381
424
  A `--transport-overlay /absolute/overlay.json` may prove a small set of host-local
382
425
  request selectors using exact identities and hashed local contract evidence. It does
@@ -1,47 +0,0 @@
1
- # Omnilane 0.42.4
2
-
3
- This patch fixes the user-facing quickstart. It changes no routing, scoring, gate,
4
- runner, or CLI behaviour.
5
-
6
- ## Why
7
-
8
- 0.42.3 documented `--caller-context` inside the dispatch skill, which is what a
9
- model driving omnilane reads. It left the READMEs' 60-second start untouched —
10
- and that is the path a new install actually takes. A user who ran
11
-
12
- ```bash
13
- npm i -g omnilane
14
- omnilane route hardest-coding "fix the flaky auth token refresh"
15
- ```
16
-
17
- was refused with `missing-caller-context`, and no README section explained the
18
- flag that resolves it. The gate was working as designed; the documentation simply
19
- never told a first-time user how to satisfy it.
20
-
21
- ## Changes
22
-
23
- - The 60-second start in all five READMEs asserts the human operator once with
24
- `OMNILANE_AA_OPERATOR_ASSERTED_HUMAN=1` before the first `omnilane route`.
25
- - A note after the quickstart explains why a dispatch must say who is asking:
26
- a human at a terminal asserts it with that variable or `--operator-asserted-human`
27
- per call; a model driving omnilane cannot assert it for itself and passes
28
- `--caller-context FILE` with its exact vendor, model, and effort instead; with
29
- neither, the dispatch is refused before any job is created.
30
- - The `dispatch.sh` synopsis in the command reference now shows
31
- `[--caller-context FILE | --operator-asserted-human]`.
32
-
33
- ## Verification boundary
34
-
35
- `--operator-asserted-human` is cooperative operator metadata. It is not automatic
36
- model detection and not OS authentication, and this release does not change that.
37
- A model caller still must not assert it on its own behalf.
38
-
39
- The frozen AA registry and its approved SHA are unchanged. Coverage remains 78
40
- scored targets, one scored reference-only entry, and 10 unknown configurations.
41
-
42
- ## Upgrade
43
-
44
- After npm publication, run `npm i -g omnilane@0.42.4`. An existing repo-symlink
45
- installation can update its checkout and verify `omnilane --version` without
46
- rerunning installation. GitHub release and npm publication remain separate
47
- verification surfaces from Linux CI.