omnilane 0.42.4 → 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.
@@ -0,0 +1,408 @@
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, the observed model, and the tier of evidence that model
7
+ rests on.
8
+ """
9
+ import argparse
10
+ import glob
11
+ import json
12
+ import os
13
+ import re
14
+ import subprocess
15
+ import time
16
+ from datetime import datetime, timezone
17
+ from pathlib import Path
18
+
19
+ SWEEP_ID = os.environ.get("OMNILANE_TRANSPORT_SWEEP_ID", "overlay-reprobe-20260909")
20
+ DEFAULT_ROOT = Path.home() / ".omnilane" / "transport-evidence" / SWEEP_ID
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
+
104
+
105
+ def verdict(
106
+ evidence_json: dict,
107
+ stdout_text: str,
108
+ stderr_text: str,
109
+ vendor: str,
110
+ expected_token: str | None,
111
+ extra: dict | None = None,
112
+ ) -> tuple[str, str, str | None, str]:
113
+ """Judge raw evidence without reading files, running commands or mutating it.
114
+
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.
117
+ """
118
+ extra = extra or {}
119
+ if evidence_json.get("timed_out"):
120
+ return "fail", "timeout", None, TIER_SELECTOR
121
+ if not expected_token:
122
+ return "fail", "missing-expected-token", None, TIER_SELECTOR
123
+
124
+ exit_code = evidence_json.get("exit_code")
125
+ if vendor == "claude":
126
+ try:
127
+ response = json.loads(stdout_text)
128
+ except (json.JSONDecodeError, TypeError):
129
+ return "fail", "invalid-json", None, TIER_SELECTOR
130
+ if not isinstance(response, dict):
131
+ return "fail", "invalid-json-result", None, TIER_SELECTOR
132
+ usage = response.get("modelUsage")
133
+ models = sorted(usage) if isinstance(usage, dict) else []
134
+ observed_model = ", ".join(models) or None
135
+ result = response.get("result", "")
136
+ if not isinstance(result, str):
137
+ result = str(result)
138
+ if response.get("is_error"):
139
+ lower_result = result.lower()
140
+ if "limit" in lower_result or "quota" in lower_result:
141
+ reason = "quota-exhausted"
142
+ elif "api error" in lower_result:
143
+ reason = "api-error"
144
+ else:
145
+ reason = "result-error"
146
+ return "fail", f"{reason}: {result[:120]}", observed_model, TIER_SELECTOR
147
+ if not models:
148
+ return "fail", "missing-model-usage", None, TIER_SELECTOR
149
+ requested_model = _requested_model(evidence_json.get("command", []))
150
+ if not requested_model:
151
+ return "fail", "missing-requested-model", observed_model, TIER_SELECTOR
152
+ if models != [requested_model]:
153
+ return "fail", "model-mismatch", observed_model, TIER_SELECTOR
154
+ if "unknown --effort" in stderr_text.lower():
155
+ return "fail", "effort-silently-defaulted", observed_model, TIER_SELECTOR
156
+ if exit_code != 0:
157
+ return "fail", f"exit-code: {exit_code}", observed_model, TIER_SELECTOR
158
+ if expected_token not in result:
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
186
+
187
+ if vendor == "agy":
188
+ if exit_code != 0:
189
+ return "fail", f"exit-code: {exit_code}: {stderr_text[:120]}", None, TIER_SELECTOR
190
+ if stderr_text:
191
+ return "fail", f"unexpected-stderr: {stderr_text[:120]}", None, TIER_SELECTOR
192
+ if expected_token not in stdout_text:
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")
196
+
197
+ if vendor == "codex":
198
+ diagnostics = [line[:120] for line in stderr_text.splitlines()
199
+ if "error" in line.lower() or "warning" in line.lower()]
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)
205
+ if exit_code != 0:
206
+ return "fail", f"exit-code: {exit_code}{why}{review}", None, TIER_SELECTOR
207
+ if expected_token not in stdout_text:
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
+
281
+
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"
308
+
309
+
310
+ def probe(
311
+ name: str,
312
+ argv: list[str],
313
+ timeout: int = 180,
314
+ cwd: Path | None = None,
315
+ root: Path | None = None,
316
+ *,
317
+ vendor: str | None = None,
318
+ expected_token: str | None = None,
319
+ app_root: Path | None = None,
320
+ ) -> dict:
321
+ if not expected_token:
322
+ raise ValueError("expected_token is required before running a probe")
323
+ if not argv:
324
+ raise ValueError("command is required")
325
+ vendor = vendor or Path(argv[0]).name
326
+ if vendor not in ("claude", "grok", "agy", "codex"):
327
+ raise ValueError(f"unsupported vendor: {vendor}; pass vendor explicitly")
328
+ root = (root or DEFAULT_ROOT).expanduser()
329
+ evidence = root / "evidence"
330
+ work = root / "work"
331
+ evidence.mkdir(parents=True, exist_ok=True)
332
+ work.mkdir(parents=True, exist_ok=True)
333
+ out_path = evidence / f"{name}.stdout"
334
+ err_path = evidence / f"{name}.stderr"
335
+ started = time.time()
336
+ timed_out = False
337
+ env = dict(os.environ)
338
+ # The runners drop the API key so the subscription OAuth path is used.
339
+ env.pop("XAI_API_KEY", None)
340
+ with open(out_path, "wb") as out, open(err_path, "wb") as err:
341
+ proc = subprocess.Popen(argv, stdout=out, stderr=err, stdin=subprocess.DEVNULL,
342
+ cwd=str(cwd or work), env=env)
343
+ try:
344
+ rc = proc.wait(timeout=timeout)
345
+ except subprocess.TimeoutExpired:
346
+ timed_out = True
347
+ proc.kill()
348
+ rc = proc.wait()
349
+ record = {
350
+ "command": argv,
351
+ "cwd": str(cwd or work),
352
+ "exit_code": rc,
353
+ "timed_out": timed_out,
354
+ "elapsed_seconds": round(time.time() - started, 3),
355
+ "probed_at": datetime.fromtimestamp(started, timezone.utc).isoformat(),
356
+ "stdout": str(out_path),
357
+ "stderr": str(err_path),
358
+ }
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
+ )
377
+ (evidence / f"{name}.json").write_text(json.dumps(record, indent=2) + "\n")
378
+ return record
379
+
380
+
381
+ def read(
382
+ name: str,
383
+ stream: str = "stdout",
384
+ limit: int = 4000,
385
+ root: Path | None = None,
386
+ ) -> str:
387
+ path = (root or DEFAULT_ROOT).expanduser() / "evidence" / f"{name}.{stream}"
388
+ if not path.exists():
389
+ return ""
390
+ return path.read_text(errors="replace")[:limit]
391
+
392
+
393
+ if __name__ == "__main__":
394
+ parser = argparse.ArgumentParser(description=__doc__)
395
+ parser.add_argument("--root", type=Path, default=DEFAULT_ROOT)
396
+ parser.add_argument("--expect", required=True, help="expected response token")
397
+ parser.add_argument("--vendor", choices=("claude", "grok", "agy", "codex"),
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")
401
+ parser.add_argument("name")
402
+ parser.add_argument("command", nargs=argparse.REMAINDER)
403
+ args = parser.parse_args()
404
+ if not args.command:
405
+ parser.error("command is required")
406
+ rec = probe(args.name, args.command, root=args.root, app_root=args.app_root,
407
+ vendor=args.vendor, expected_token=args.expect)
408
+ print(json.dumps(rec, ensure_ascii=False))
@@ -371,12 +371,94 @@ 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 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.
409
+
410
+ Re-signing is a probe, a rebuild, and an install, in that order. Back up
411
+ `~/.omnilane/transport-contracts.local.json` first; restoring it is the rollback.
412
+ `scripts/lib/probe.py --expect TOKEN [--vendor V] NAME COMMAND…` invokes the CLI
413
+ directly through `subprocess`, so it works while the gate is refusing everything —
414
+ this is what breaks the deadlock. `scripts/provider-probe.sh` goes through
415
+ `dispatch.sh` and therefore through the gate, so it is useless in this state.
416
+ Then `scripts/lib/build_overlay.py` rebuilds, and you copy the result over the
417
+ live overlay. Verify with a real dispatch on a lane belonging to the vendor you
418
+ re-probed; loading the registry in Python is not the runtime surface.
419
+
420
+ Keep the sweep where its default `--root` puts it,
421
+ `~/.omnilane/transport-evidence/<sweep-id>/`. The rebuilt overlay anchors
422
+ `probe-manifest.json` by absolute path as untagged evidence, so a sweep parked
423
+ inside a repository is one `git clean -fdx` away from taking every vendor down
424
+ at once — the same global refusal a re-signing session is usually trying to end.
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
+
450
+ Never sign a probe you did not read. `probe.py` records a `verdict` because exit
451
+ status alone is not evidence: the Claude CLI answers a quota refusal with a JSON
452
+ body carrying `is_error`, and it accepts an unknown `--effort` by silently using
453
+ the default, returning exit 0, the right `modelUsage`, and the expected token
454
+ with only a stderr warning to show for it. Effort is half of a scored identity,
455
+ so that path would certify a mapping at the wrong tier. Configurations whose
456
+ probes failed are recorded in the overlay's `unproven[]` and surfaced by doctor
457
+ instead of vanishing — six Fable rows sat unusable for two days in September
458
+ 2026 because a 429 quota refusal left no trace anywhere. A refused probe is not
459
+ always transient: re-probing those six two days later returned the same 429, so
460
+ an `unproven[]` entry can mean the account, not the moment. Read the reason
461
+ before assuming a retry will clear it.
380
462
 
381
463
  A `--transport-overlay /absolute/overlay.json` may prove a small set of host-local
382
464
  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.