@r1z4x/tezgah 1.6.0 → 1.8.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (71) hide show
  1. package/CHANGELOG.md +140 -1
  2. package/MANIFEST +7 -0
  3. package/README.md +6 -3
  4. package/bin/tezgah-decisions +6 -14
  5. package/bin/tezgah-design +21 -5
  6. package/bin/tezgah-docs +18 -16
  7. package/bin/tezgah-doctor +152 -0
  8. package/bin/tezgah-lessons +4 -1
  9. package/bin/tezgah-research +249 -17
  10. package/bin/tezgah-route +70 -8
  11. package/bin/tezgah-setup +26 -13
  12. package/bin/tezgah-task +877 -60
  13. package/bin/tezgah-taste +56 -15
  14. package/docs/architecture.md +28 -11
  15. package/docs/clarity-baseline.json +2 -2
  16. package/docs/contract.md +108 -24
  17. package/docs/environment.md +3 -1
  18. package/docs/evidence.md +209 -28
  19. package/docs/feature-audit.md +1 -1
  20. package/docs/gate.md +78 -23
  21. package/docs/glossary.md +4 -4
  22. package/docs/hosts.md +8 -8
  23. package/docs/index.json +11 -5
  24. package/docs/judge.md +9 -3
  25. package/docs/models.md +75 -14
  26. package/docs/operations.md +133 -3
  27. package/docs/research.md +349 -15
  28. package/docs/skills.md +1 -1
  29. package/docs/status-line.md +5 -5
  30. package/docs/testing.md +20 -13
  31. package/docs/vision.md +12 -4
  32. package/hooks/hooks.json +2 -2
  33. package/hooks/lang/en.json +323 -0
  34. package/hooks/lang/tr.json +344 -0
  35. package/hooks/projects-auto-init.py +29 -1
  36. package/hooks/projects-posttooluse.py +24 -5
  37. package/hooks/projects-pretooluse.py +9 -4
  38. package/hooks/projects-stop.py +28 -16
  39. package/hooks/tezgah_agents.py +53 -17
  40. package/hooks/tezgah_clarity.py +7 -3
  41. package/hooks/tezgah_context.py +489 -438
  42. package/hooks/tezgah_gate.py +996 -80
  43. package/hooks/tezgah_integrity.py +1547 -197
  44. package/hooks/tezgah_models.py +427 -45
  45. package/hooks/tezgah_paths.py +147 -16
  46. package/hooks/tezgah_policy.py +261 -129
  47. package/hooks/tezgah_rank.py +29 -27
  48. package/hooks/tezgah_replay.py +200 -30
  49. package/hooks/tezgah_research.py +1303 -14
  50. package/hooks/tezgah_stopspec.py +7 -0
  51. package/hooks/tezgah_store.py +74 -27
  52. package/hooks/tezgah_task.py +1201 -40
  53. package/hooks/tezgah_taste.py +8 -4
  54. package/hosts/codex/hook.py +32 -13
  55. package/hosts/cursor/hook.py +32 -14
  56. package/hosts/dsh/hooks.json +2 -2
  57. package/hosts/omp/hook.py +44 -23
  58. package/hosts/omp/tezgah-hook.ts.in +93 -7
  59. package/hosts/opencode/plugins/tezgah.js +273 -6
  60. package/output-styles/tezgah.md +76 -42
  61. package/package.json +1 -1
  62. package/skills/i-have-adhd/EVALS.md +22 -2
  63. package/skills/i-have-adhd/SKILL.md +12 -5
  64. package/skills/plan-add/SKILL.md +27 -11
  65. package/skills/plan-add/render_table.py +8 -3
  66. package/skills/plan-sync/SKILL.md +5 -3
  67. package/skills/ponytail/EVALS.md +21 -2
  68. package/skills/ponytail/SKILL.md +4 -4
  69. package/skills/research/SKILL.md +89 -1
  70. package/skills/tezgah-contract/EVALS.md +52 -7
  71. package/skills/tezgah-contract/SKILL.md +154 -59
@@ -37,6 +37,7 @@ tier from the static table, and with no phase the middle tier is used.
37
37
 
38
38
  Stdlib only; every network or subprocess failure is a fallback, never a raise.
39
39
  """
40
+ import contextlib
40
41
  import datetime
41
42
  import json
42
43
  import os
@@ -232,18 +233,331 @@ def _omp_stored_providers():
232
233
  return {row[0] for row in rows}
233
234
 
234
235
 
235
- def funded_families():
236
+ def funded_families(live=True):
236
237
  """The `OMP_FAMILIES` with a credential this machine shows: an omp auth-store
237
- entry, an env key omp reads, or (for `any`) `openrouter_ready`.
238
+ entry, an env key omp reads, or (for `any`) `openrouter_ready` - minus, with
239
+ `live`, the families the health record holds as down (`down_families`).
238
240
 
239
241
  A credential is not a balance - a funded-looking key can still answer 402 -
240
- which is why each agent gets a chain rather than one more pin. A family whose
241
- credential cannot be detected is left out, never guessed in."""
242
+ which is why each agent gets a chain rather than one more pin, and why a
243
+ family whose provider answered a limit leaves the chain until its reset. A
244
+ family whose credential cannot be detected is left out, never guessed in."""
242
245
  stored = _omp_stored_providers()
246
+ down = down_families() if live else set()
243
247
  return [family for family in OMP_FAMILIES
244
- if OMP_AUTH[family][0] in stored
245
- or any(os.environ.get(env, "").strip() for env in OMP_AUTH[family][1])
246
- or (family == "any" and openrouter_ready())]
248
+ if family not in down
249
+ and (OMP_AUTH[family][0] in stored
250
+ or any(os.environ.get(env, "").strip() for env in OMP_AUTH[family][1])
251
+ or (family == "any" and openrouter_ready()))]
252
+
253
+
254
+ # ------------------------------------------------------------ provider health
255
+ #
256
+ # A family with a credential can still be spent: on 2026-10-11 every subagent
257
+ # chain read `anthropic, openrouter`, one Anthropic 429 moved the subagents onto
258
+ # an OpenRouter key past its limit (403), and they died; zai, funded in omp's
259
+ # store, answered `429 Weekly/Monthly Limit Exhausted. Your limit will reset at
260
+ # 2026-10-12 02:02:35`. The record below holds which family is down and until
261
+ # when; `funded_families` leaves those out, and `tezgah-route --health` - started
262
+ # in the background at session start when due - probes the non-mode families
263
+ # and re-applies omp's chain, so a family rejoins it once its reset has passed.
264
+ HEALTH_FILE = "model-health.json"
265
+ HEALTH_EVERY = 6 * 3600 # re-probe at most this often, unless a reset passed
266
+ HEALTH_DOWN = 6 * 3600 # how long a limit with no reset time keeps a family out
267
+ HEALTH_RATE = 600 # the same for a rate limit (429): it lifts in minutes
268
+ HEALTH_DOWN_MAX = 7 * 86400 # the longest a named reset is believed
269
+ OPENROUTER_KEY = "https://openrouter.ai/api/v1/key"
270
+ _RESET_AT = re.compile(r"reset[s]? (?:at|on) (\d{4}-\d{2}-\d{2})[ T](\d{2}:\d{2}:\d{2})")
271
+ _RETRY_MS = re.compile(r"retry-after-ms=(\d+)")
272
+ _LIMITED = re.compile(r"\b(?:429|402|403)\b|rate[ _-]?limit|quota|limit exhausted|"
273
+ r"key limit exceeded|usage limit|insufficient (?:credit|balance)",
274
+ re.I)
275
+
276
+
277
+ def _health_path():
278
+ return os.path.join(tp.cache_dir(), HEALTH_FILE)
279
+
280
+
281
+ def read_health():
282
+ """{"checked": epoch or None, "down": {family: {"until": epoch, "reason": str}}};
283
+ an unreadable record reads as empty - nothing known down."""
284
+ try:
285
+ with open(_health_path(), encoding="utf-8") as fh:
286
+ data = json.load(fh)
287
+ except (OSError, ValueError):
288
+ data = {}
289
+ if not isinstance(data, dict):
290
+ data = {}
291
+ down = data.get("down") if isinstance(data.get("down"), dict) else {}
292
+ return {"checked": data.get("checked"), "down": down}
293
+
294
+
295
+ def write_health(data):
296
+ """Write the record atomically; False when the cache dir cannot be written."""
297
+ path = _health_path()
298
+ try:
299
+ os.makedirs(os.path.dirname(path), exist_ok=True)
300
+ tmp = "%s.%d.tmp" % (path, os.getpid())
301
+ with open(tmp, "w", encoding="utf-8") as fh:
302
+ json.dump(data, fh, indent=1, sort_keys=True)
303
+ os.replace(tmp, path)
304
+ return True
305
+ except OSError:
306
+ return False
307
+
308
+
309
+ def down_families(now=None, data=None):
310
+ """The families the record holds as down whose reset has not passed yet."""
311
+ import time
312
+ now = time.time() if now is None else now
313
+ data = read_health() if data is None else data
314
+ return {family for family, row in data["down"].items()
315
+ if isinstance(row, dict) and isinstance(row.get("until"), (int, float))
316
+ and row["until"] > now}
317
+
318
+
319
+ def limit_until(text, now=None, default=HEALTH_DOWN):
320
+ """When a provider's limit answer says it lifts, as an epoch, or None when the
321
+ text is no limit answer at all. A named reset (`reset at 2026-10-12 02:02:35`,
322
+ read as local time) or a `retry-after-ms` wins; else `default` from now. A
323
+ reset further than HEALTH_DOWN_MAX away is capped: a wrong year must not lock a
324
+ family out for good."""
325
+ import time
326
+ now = time.time() if now is None else now
327
+ text = str(text or "")
328
+ if not _LIMITED.search(text):
329
+ return None
330
+ until = None
331
+ hit = _RESET_AT.search(text)
332
+ if hit:
333
+ try:
334
+ until = time.mktime(time.strptime("%s %s" % hit.groups(), "%Y-%m-%d %H:%M:%S"))
335
+ except (ValueError, OverflowError):
336
+ until = None
337
+ if until is not None and until <= now:
338
+ # a named reset already past is a clock we misread (the provider's
339
+ # zone is not stated): a minute's hold would re-probe every prompt
340
+ until = now + default
341
+ if until is None:
342
+ ms = _RETRY_MS.search(text)
343
+ until = now + int(ms.group(1)) / 1000.0 if ms else now + default
344
+ return min(max(until, now + 60), now + HEALTH_DOWN_MAX)
345
+
346
+
347
+ @contextlib.contextmanager
348
+ def _health_locked():
349
+ """An exclusive flock on the record's lock file around one re-read, change
350
+ and write, so two writers (a slice's stop, the probe) do not drop each
351
+ other's hold. Unlocked where fcntl is missing or the file cannot be opened."""
352
+ fh = None
353
+ try:
354
+ import fcntl
355
+ os.makedirs(os.path.dirname(_health_path()), exist_ok=True)
356
+ fh = open(_health_path() + ".lock", "a", encoding="utf-8")
357
+ fcntl.flock(fh, fcntl.LOCK_EX)
358
+ except (ImportError, OSError):
359
+ pass
360
+ try:
361
+ yield
362
+ finally:
363
+ if fh is not None:
364
+ fh.close()
365
+
366
+
367
+ def _family(provider="", model="", default=""):
368
+ """The OMP_FAMILIES entry a provider id (omp's auth-store id), else a model id
369
+ (`_vendor`), else `default` runs under; None when none is one."""
370
+ by_provider = {auth[0]: family for family, auth in OMP_AUTH.items()}
371
+ for family in (by_provider.get(str(provider or "").lower()),
372
+ _vendor(str(model)) if model else None, default):
373
+ if family in OMP_FAMILIES:
374
+ return family
375
+ return None
376
+
377
+
378
+ def note_limit(text, provider="", model="", transient=False, default="", now=None):
379
+ """Hold down the family a slice stopped on with a limit answer (`text`), and
380
+ re-apply omp's chain, so the next spawn of this session leaves that family
381
+ instead of the next session start's probe moving it (research
382
+ orchestrator-flow-audit S2, F3a). A rate limit (`transient`) with no reset
383
+ named holds HEALTH_RATE, a quota HEALTH_DOWN. Returns the family newly held,
384
+ else None: no family resolves (`_family`), `text` is no limit answer
385
+ (`limit_until`), the family is already held, or TEZGAH_HEALTH_CHECK=0 (the
386
+ suite's switch)."""
387
+ import time
388
+ family = _family(provider, model, default)
389
+ if not family or os.environ.get("TEZGAH_HEALTH_CHECK") == "0":
390
+ return None
391
+ now = time.time() if now is None else now
392
+ until = limit_until(text, now, HEALTH_RATE if transient else HEALTH_DOWN)
393
+ if until is None:
394
+ return None
395
+ with _health_locked():
396
+ data = read_health()
397
+ if family in down_families(now, data):
398
+ return None
399
+ data["down"][family] = {"until": until,
400
+ "reason": " ".join(str(text).split())[:200]}
401
+ if not write_health(data):
402
+ return None
403
+ apply_omp()
404
+ return family
405
+
406
+
407
+ def other_family_up(text):
408
+ """Where work a limit stopped on can move: the first family the routing lets
409
+ a chain fall back to (`_chain` under `tp.fallback_policy()`, every slot) that
410
+ is funded and not down (`funded_families`) and that `text` does not name
411
+ (`named_families`), else None. None with routing off or under
412
+ TEZGAH_HEALTH_CHECK=0, so the suite never reads this machine's credentials."""
413
+ if os.environ.get("TEZGAH_HEALTH_CHECK") == "0":
414
+ return None
415
+ mode = omp_mode()
416
+ if mode not in OMP_FAMILIES:
417
+ return None
418
+ funded = funded_families(live=True)
419
+ chained = {sel for slot in SLOTS for sel in _chain(mode, slot, funded)}
420
+ named = named_families(text)
421
+ return next((family for family in funded if family not in named and any(
422
+ sel.startswith(OMP_AUTH[family][0] + "/") for sel in chained)), None)
423
+
424
+
425
+ def named_families(text):
426
+ """The OMP_FAMILIES `text` names by an auth-store provider id (a whole word)
427
+ or a model stem (`VENDOR_STEMS`, a word start): `openrouter 403` names `any`,
428
+ `glm-5.3` names `zai`."""
429
+ low = str(text or "").lower()
430
+ return {family for family in OMP_FAMILIES
431
+ if re.search(r"\b%s\b" % re.escape(OMP_AUTH[family][0]), low)
432
+ or any(re.search(r"\b%s" % re.escape(s), low)
433
+ for s, v in VENDOR_STEMS if v == family)}
434
+
435
+
436
+ def _openrouter_key():
437
+ env, home = tp.PROVIDER_KEYS["openrouter"]
438
+ key = os.environ.get(env, "").strip()
439
+ if key:
440
+ return key
441
+ try:
442
+ with open(os.path.join(tp.HOME, home), encoding="utf-8") as fh:
443
+ return fh.read().strip()
444
+ except OSError:
445
+ return ""
446
+
447
+
448
+ def probe_family(family, timeout=60):
449
+ """(up, reason) for one family: None for up when the probe could not tell.
450
+
451
+ `any` asks OpenRouter's key endpoint, which costs no tokens: a key whose
452
+ `limit_remaining` is 0 or less is down. A family omp runs is asked one short
453
+ prompt on that family's cheap row through `omp -p --no-session`; exit 0 is
454
+ up, a limit answer is down (its reset in `reason`), anything else is unknown."""
455
+ if family == "any":
456
+ key = _openrouter_key()
457
+ if not key:
458
+ return None, "no OpenRouter key"
459
+ req = urllib.request.Request(OPENROUTER_KEY,
460
+ headers={"Authorization": "Bearer " + key})
461
+ try:
462
+ with urllib.request.urlopen(req, timeout=15) as resp:
463
+ info = (json.loads(resp.read().decode("utf-8")) or {}).get("data") or {}
464
+ except Exception as exc: # noqa: BLE001 - any network failure is "cannot tell"
465
+ text = str(exc)
466
+ return (False, text) if _LIMITED.search(text) else (None, text)
467
+ left = info.get("limit_remaining")
468
+ if isinstance(left, (int, float)) and left <= 0:
469
+ return False, "OpenRouter key limit exhausted (limit %s, usage %s)" % (
470
+ info.get("limit"), info.get("usage"))
471
+ return True, "OpenRouter key has room"
472
+ exe = tp.omp_bin()
473
+ if not exe:
474
+ return None, "omp not found"
475
+ # TEZGAH_NESTED, as consult and the judge set it: a hook that does load in
476
+ # the child records nothing and refuses nothing
477
+ env = dict(os.environ, PI_CODING_AGENT_DIR=OMP_AGENT, TEZGAH_NESTED="1")
478
+ try:
479
+ # the consult argv: no tools, rules, skills, extensions or title call, so
480
+ # the probe is one short request and fires no nested tezgah hooks
481
+ argv = [exe] + tp.CONSULT_CLIS["omp"][0][1:]
482
+ proc = subprocess.run(argv + ["--model", _selector(family, "cheap"),
483
+ "--max-time=%d" % timeout, "Reply with exactly: OK"],
484
+ capture_output=True, text=True, timeout=timeout + 15,
485
+ env=env, cwd=tp.HOME)
486
+ except (OSError, subprocess.SubprocessError) as exc:
487
+ return None, str(exc)
488
+ text = (proc.stdout or "") + (proc.stderr or "")
489
+ if proc.returncode == 0:
490
+ return True, "answered"
491
+ tail = " ".join(text.split())[-200:]
492
+ return (False, tail) if _LIMITED.search(text) else (None, tail)
493
+
494
+
495
+ def refresh_health(mode=None, probe=probe_family, now=None):
496
+ """Probe every family with a credential except the mode's own (it leads the
497
+ chain whatever its state, and the session is running on it), record the
498
+ downs with their resets, and return the record. A family the probe cannot
499
+ judge keeps the state it had. The probes run unlocked; their results land on
500
+ the record re-read under the lock (`_health_locked`), so a hold a slice's
501
+ stop wrote meanwhile stays."""
502
+ import time
503
+ now = time.time() if now is None else now
504
+ mode = mode or omp_mode()
505
+ seen = {family: probe(family) for family in funded_families(live=False)
506
+ if family != mode}
507
+ with _health_locked():
508
+ down = dict(read_health()["down"])
509
+ for family, (up, reason) in seen.items():
510
+ if up is True:
511
+ down.pop(family, None)
512
+ elif up is False:
513
+ down[family] = {"until": limit_until(reason, now) or now + HEALTH_DOWN,
514
+ "reason": reason[:200]}
515
+ data = {"checked": now, "down": {f: r for f, r in down.items()
516
+ if isinstance(r, dict) and r.get("until", 0) > now}}
517
+ write_health(data)
518
+ return data
519
+
520
+
521
+ def health_due(data=None, now=None):
522
+ """True when the record was never checked, is older than HEALTH_EVERY, or
523
+ holds a down family whose reset has passed since it was checked. Never with
524
+ TEZGAH_HEALTH_CHECK=0 (the test suite's switch, like TEZGAH_UPDATE_CHECK)."""
525
+ import time
526
+ if os.environ.get("TEZGAH_HEALTH_CHECK") == "0":
527
+ return False
528
+ now = time.time() if now is None else now
529
+ data = read_health() if data is None else data
530
+ checked = data.get("checked")
531
+ if not isinstance(checked, (int, float)) or now - checked >= HEALTH_EVERY:
532
+ return True
533
+ return any(isinstance(row, dict) and checked < row.get("until", 0) <= now
534
+ for row in data["down"].values())
535
+
536
+
537
+ def start_health():
538
+ """Stamp the record, then start one detached `tezgah-route --health`, so the
539
+ session starts that follow read a fresh stamp and start nothing. False when
540
+ nothing started."""
541
+ import sys
542
+ import time
543
+ with _health_locked():
544
+ data = read_health()
545
+ data["checked"] = time.time()
546
+ if not write_health(data):
547
+ return False
548
+ route = os.path.join(os.path.dirname(os.path.dirname(os.path.realpath(__file__))),
549
+ "bin", "tezgah-route")
550
+ kwargs = {"stdin": subprocess.DEVNULL, "stdout": subprocess.DEVNULL,
551
+ "stderr": subprocess.DEVNULL, "close_fds": True}
552
+ if os.name == "posix":
553
+ kwargs["start_new_session"] = True
554
+ else: # pragma: no cover - Windows: no console, outlives its parent
555
+ kwargs["creationflags"] = 0x00000008 | 0x00000200
556
+ try:
557
+ subprocess.Popen([sys.executable, route, "--health"], **kwargs)
558
+ return True
559
+ except OSError:
560
+ return False
247
561
 
248
562
 
249
563
  def omp_mode(default_selector=None):
@@ -308,11 +622,14 @@ def _chain(mode, slot, funded, agent=""):
308
622
  because a session on Opus that ran its subagents on GPT and GLM after one
309
623
  Anthropic 429 is the failure the owner reported (2026-10-07). `none`: the
310
624
  mode's selector alone. `any`: every other funded family, each on that
311
- slot's own row, rotated by the agent's own name (`agent`): identical chains
312
- for every agent meant one family's 429 moved all of them onto the same next
313
- family in the same moment, which is the cascade the chain exists to break
314
- (independent review, 2026-10-01). The rotation is deterministic - the same
315
- agent resolves the same order on every machine - so nothing here is random."""
625
+ slot's own row. The families omp runs on an account (zai) come before
626
+ OpenRouter, which bills per token - the owner's ask (2026-10-11) was to use
627
+ zai when it is up - and among those the order is rotated by the agent's own
628
+ name (`agent`): identical chains for every agent meant one family's 429
629
+ moved all of them onto the same next family in the same moment, which is the
630
+ cascade the chain exists to break (independent review, 2026-10-01). The
631
+ rotation is deterministic - the same agent resolves the same order on every
632
+ machine - so nothing here is random."""
316
633
  policy = tp.fallback_policy()
317
634
  families = [mode] + [f for f in OMP_FAMILIES if f != mode and f in funded]
318
635
  if policy == "none":
@@ -327,10 +644,11 @@ def _chain(mode, slot, funded, agent=""):
327
644
  out.append("openrouter/" + OPENROUTER_ID[model]
328
645
  + (":" + effort if effort else ""))
329
646
  return list(dict.fromkeys(out))
330
- tail = families[1:]
647
+ tail = [f for f in families[1:] if f != "any"]
331
648
  if agent and len(tail) > 1:
332
649
  shift = sum(ord(c) for c in agent) % len(tail)
333
- families = families[:1] + tail[shift:] + tail[:shift]
650
+ tail = tail[shift:] + tail[:shift]
651
+ families = families[:1] + tail + [f for f in families[1:] if f == "any"]
334
652
  return [_selector(family, slot) for family in families]
335
653
 
336
654
 
@@ -552,7 +870,11 @@ def apply_omp(remove=False):
552
870
  owns.update(owns_chains)
553
871
  merged_chains = dict(chains)
554
872
  for key in flat_chains:
555
- role = key.split(".", 1)[1]
873
+ role = key.rsplit(".", 1)[1]
874
+ # an earlier release wrote the chain under `fallbackChains.<role>`, a key
875
+ # omp never reads (the `split(".", 1)` of the flat key); it is tezgah's
876
+ # own and goes
877
+ merged_chains.pop("fallbackChains." + role, None)
556
878
  value = new_chains.get(key)
557
879
  if value is None:
558
880
  merged_chains.pop(role, None)
@@ -863,40 +1185,84 @@ def _in_order(routes, kids):
863
1185
  return [stem for _ts, stem in kids]
864
1186
 
865
1187
 
866
- def _join(routes, kids, spawned_as):
867
- """[child stem or None] per route. A child's type is what its parent's task
868
- call recorded for its name (`spawn` rows, omp), else its own `agent` (the
869
- type itself on opencode). With every child typed, each route agent's routes
870
- join that agent's children by `_in_order`, and a child of an unrouted type
871
- is left out; with any child untyped, order over all of them is the only
872
- evidence. Whatever is not proven stays None (`unjoined`)."""
873
- typed = [(ts, stem, spawned_as.get(agent, agent)) for ts, stem, agent in kids]
874
- if any(kind is None for _ts, _stem, kind in typed):
875
- stems = _in_order(routes, [(ts, stem) for ts, stem, _k in typed])
876
- return stems or [None] * len(routes)
877
- out = [None] * len(routes)
1188
+ # How far apart, in seconds, two children of one task call start: an omp call
1189
+ # of 11 workers opened their ledgers over 2 seconds (ledger 01a12277). A second
1190
+ # task call within that gap would read as part of the first - ponytail: a model
1191
+ # turn between two calls takes longer than this.
1192
+ FAN_OUT_GAP = 2
1193
+
1194
+ # How long after its route a route's first worker may start: tezgah-route runs
1195
+ # right before the task call. The joined omp routes (ledgers 01a12277, 01a12bf1)
1196
+ # started theirs 39 to 1296 s later; 1296 s is a route made beside another and
1197
+ # run after it finished. Past this a same-type child proves nothing.
1198
+ ROUTE_GAP = 1800
1199
+
1200
+
1201
+ def _fan_out(kids):
1202
+ """The children of the first task call in sorted [(ts, key)]: each started
1203
+ within FAN_OUT_GAP of the one before."""
1204
+ out = kids[:1]
1205
+ for kid in kids[1:]:
1206
+ if kid[0] - out[-1][0] > FAN_OUT_GAP:
1207
+ break
1208
+ out.append(kid)
1209
+ return out
1210
+
1211
+
1212
+ def _join(routes, kids):
1213
+ """[[child key]] per route, [] where unjoined; `kids` is [(ts, key, type)],
1214
+ type None when no row typed the child.
1215
+
1216
+ With no child typed, order over all of them is the only evidence
1217
+ (`_in_order`). Otherwise each route agent's routes join that agent's
1218
+ children: a route owns the first fan-out of its type after it and before
1219
+ the next route of its type - later same-type spawns are ones no route sent.
1220
+ It owns nothing when the route before it of its type had no worker yet
1221
+ (both routes' workers may be in this one fan-out), when the fan-out started
1222
+ more than ROUTE_GAP after it, or when an untyped child started inside its
1223
+ span (that child could be its worker). A child of an
1224
+ unrouted type is left out. Whatever is not proven stays unjoined."""
1225
+ if kids and all(kind is None for _ts, _key, kind in kids):
1226
+ keys = _in_order(routes, [(ts, key) for ts, key, _k in kids])
1227
+ return [[key] for key in keys] if keys else [[] for _r in routes]
1228
+ out = [[] for _r in routes]
1229
+ untyped = [ts for ts, _key, kind in kids if kind is None]
878
1230
  for agent in {fields.get("agent") for _ts, fields in routes}:
879
1231
  mine = [i for i, (_ts, fields) in enumerate(routes) if fields.get("agent") == agent]
880
- stems = _in_order([routes[i] for i in mine],
881
- [(ts, stem) for ts, stem, kind in typed if kind == agent])
882
- for i, stem in zip(mine, stems or []):
883
- out[i] = stem
1232
+ theirs = sorted((ts, key) for ts, key, kind in kids if kind == agent)
1233
+ for n, i in enumerate(mine):
1234
+ start = routes[i][0]
1235
+ end = routes[mine[n + 1]][0] if n + 1 < len(mine) else float("inf")
1236
+ if n and not any(routes[mine[n - 1]][0] <= ts < start for ts, _key in theirs):
1237
+ continue
1238
+ batch = _fan_out([kid for kid in theirs if start <= kid[0] < end])
1239
+ if batch and batch[0][0] - start <= ROUTE_GAP \
1240
+ and not any(start <= ts <= batch[-1][0] for ts in untyped):
1241
+ out[i] = [key for _ts, key in batch]
884
1242
  return out
885
1243
 
886
1244
 
1245
+ # A fan-out's outcome is its most telling first check: one failed worker fails
1246
+ # the route, so a fan-out never hides a failure behind a sibling's pass.
1247
+ OUTCOME_ORDER = ("fail", "pass", "ran", "none")
1248
+
1249
+
887
1250
  def route_report(ledgers):
888
1251
  """({(via, tier): counts}, counts for Jev routes below the static tier) over
889
1252
  {ledger stem: rows}.
890
1253
 
891
- A routed worker runs in its own session, so its checks are in its own ledger,
892
- whose `spawned` row names the parent session the route row was written under.
893
- A route joins the child `_join` can prove is its worker, and the outcome is
894
- that child's first check (`pass`, `fail`, `ran`, `none`). A route it cannot
895
- prove - an unrouted spawn, a tie, a host that writes no `spawned` row - is
896
- `unjoined`: the parent's own next check is never taken for the worker's, and
897
- neither is a guess. The second count is the V2-as-default question: Jev
898
- picking under the static table's tier, and how often that first attempt
899
- failed (docs/models.md)."""
1254
+ A routed worker on omp and opencode runs in its own session, so its checks
1255
+ are in its own ledger, whose `spawned` row names the parent session the
1256
+ route row was written under. On Claude and Codex it runs under its parent's
1257
+ session id: its rows are in the parent's ledger with `agent` = its id, and
1258
+ the `subagent_start` row (SubagentStart's agent_id, agent_type in `detail`)
1259
+ types it. A route joins the children `_join` can prove are its workers, and
1260
+ the outcome is their first check (`pass`, `fail`, `ran`, `none`; over a
1261
+ fan-out, OUTCOME_ORDER's first). A route it cannot prove - an unrouted
1262
+ spawn, a tie, a host that writes no child row - is `unjoined`: the parent's
1263
+ own next check is never taken for the worker's, and neither is a guess. The
1264
+ second count is the V2-as-default question: Jev picking under the static
1265
+ table's tier, and how often that first attempt failed (docs/models.md)."""
900
1266
  def counts():
901
1267
  return {"routes": 0, "pass": 0, "fail": 0, "ran": 0, "none": 0, "unjoined": 0}
902
1268
  def spawn_rows(rows):
@@ -906,7 +1272,11 @@ def route_report(ledgers):
906
1272
  for stem, rows in ledgers.items():
907
1273
  link = next((r for r in rows if r.get("kind") == "spawned" and r.get("parent")),
908
1274
  None)
909
- if link:
1275
+ # Cursor's row is written from the parent's subagentStart under the
1276
+ # child's `subagent_id`; that the child's own calls carry this id is not
1277
+ # documented, so the link counts only once a row of the child's landed
1278
+ if link and not (link.get("host") == "cursor"
1279
+ and all(r.get("kind") == "spawned" for r in rows)):
910
1280
  children.setdefault(ti._slug(link["parent"]), []).append(
911
1281
  (link.get("ts") or 0, stem, link.get("agent")))
912
1282
  groups, below = {}, counts()
@@ -918,15 +1288,27 @@ def route_report(ledgers):
918
1288
  # orchestrator shares the parent's TEZGAH_SESSION, so its worker names
919
1289
  # that parent while the row typing the worker is in the orchestrator's
920
1290
  # ledger. Another session's task names never type this one's children.
921
- # A name two calls typed differently is untyped (None): order decides.
1291
+ # A name two calls typed differently is untyped (None).
922
1292
  spawned_as = {}
923
1293
  for r in spawn_rows(rows) + [r for _ts, kid, _a in children.get(stem, [])
924
1294
  for r in spawn_rows(ledgers[kid])]:
925
1295
  if spawned_as.setdefault(r["child"], r["agent"]) != r["agent"]:
926
1296
  spawned_as[r["child"]] = None
927
- for (_ts, fields), kid in zip(routes, _join(routes, children.get(stem, []),
928
- spawned_as)):
929
- outcome = _outcome(ledgers[kid]) if kid else "unjoined"
1297
+ # a child key is (ledger stem, agent id): "" for a whole ledger; an
1298
+ # empty type (a Cursor subagentStart with no subagent_type) is no type
1299
+ kids = [(ts, (kid, ""), spawned_as.get(name, name) or None)
1300
+ for ts, kid, name in children.get(stem, [])]
1301
+ # a Claude plugin agent's type is plugin-scoped (`tezgah:tezgah-cheap`)
1302
+ kids += [(r.get("ts") or 0, (stem, r["agent"]),
1303
+ str(r.get("detail") or "").rsplit(":", 1)[-1] or None)
1304
+ for r in rows if r.get("kind") == "subagent_start"
1305
+ and isinstance(r.get("agent"), str) and r["agent"]]
1306
+ for (_ts, fields), keys in zip(routes, _join(routes, kids)):
1307
+ got = {_outcome(ledgers[kid] if not agent else
1308
+ [r for r in ledgers[kid] if r.get("agent") == agent
1309
+ and r.get("kind") != "subagent_start"])
1310
+ for kid, agent in keys}
1311
+ outcome = next((o for o in OUTCOME_ORDER if o in got), "unjoined")
930
1312
  bins = [groups.setdefault((fields["via"], fields["tier"]), counts())]
931
1313
  static = fields.get("static")
932
1314
  if fields["via"] == "jev" and static in TIERS and \