@r1z4x/tezgah 1.5.1 → 1.7.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +126 -1
- package/MANIFEST +11 -0
- package/README.md +6 -3
- package/bin/consult +13 -10
- package/bin/tezgah-docs +18 -16
- package/bin/tezgah-doctor +152 -0
- package/bin/tezgah-research +286 -12
- package/bin/tezgah-route +48 -1
- package/bin/tezgah-setup +27 -14
- package/bin/tezgah-task +771 -46
- package/bin/tezgah-taste +48 -10
- package/docs/architecture.md +7 -4
- package/docs/citations-baseline.json +1 -1
- package/docs/clarity-baseline.json +5 -5
- package/docs/contract.md +112 -25
- package/docs/evidence.md +201 -28
- package/docs/feature-audit.md +25 -4
- package/docs/gate.md +70 -33
- package/docs/glossary.md +4 -4
- package/docs/hosts.md +8 -8
- package/docs/index.json +30 -10
- package/docs/judge.md +9 -3
- package/docs/models.md +35 -2
- package/docs/operations.md +103 -3
- package/docs/research.md +392 -13
- package/docs/skills.md +24 -6
- package/docs/status-line.md +5 -5
- package/docs/testing.md +2 -2
- package/docs/vision.md +12 -4
- package/hooks/hooks.json +2 -2
- package/hooks/lang/en.json +323 -0
- package/hooks/lang/tr.json +344 -0
- package/hooks/projects-posttooluse.py +24 -5
- package/hooks/projects-pretooluse.py +9 -4
- package/hooks/projects-stop.py +28 -16
- package/hooks/tezgah_agents.py +29 -9
- package/hooks/tezgah_clarity.py +7 -3
- package/hooks/tezgah_components.py +2 -1
- package/hooks/tezgah_context.py +460 -390
- package/hooks/tezgah_gate.py +640 -104
- package/hooks/tezgah_integrity.py +1480 -197
- package/hooks/tezgah_models.py +337 -15
- package/hooks/tezgah_paths.py +103 -10
- package/hooks/tezgah_policy.py +286 -130
- package/hooks/tezgah_product.py +107 -0
- package/hooks/tezgah_rank.py +29 -27
- package/hooks/tezgah_replay.py +200 -30
- package/hooks/tezgah_research.py +1297 -12
- package/hooks/tezgah_skill_pick.py +58 -17
- package/hooks/tezgah_stopspec.py +7 -0
- package/hooks/tezgah_store.py +74 -27
- package/hooks/tezgah_task.py +917 -8
- package/hosts/codex/hook.py +9 -10
- package/hosts/cursor/hook.py +21 -14
- package/hosts/dsh/hooks.json +2 -2
- package/hosts/omp/hook.py +44 -23
- package/hosts/omp/tezgah-hook.ts.in +93 -7
- package/hosts/opencode/plugins/tezgah.js +119 -39
- package/output-styles/tezgah.md +76 -42
- package/package.json +1 -1
- package/skills/analyze-app/SKILL.md +57 -0
- package/skills/design-contract/EVALS.md +24 -1
- package/skills/design-contract/SKILL.md +17 -1
- package/skills/design-contract/data-views.md +86 -0
- package/skills/feature-audit/SKILL.md +32 -3
- package/skills/i-have-adhd/EVALS.md +22 -2
- package/skills/i-have-adhd/SKILL.md +12 -5
- package/skills/plan-add/EVALS.md +8 -8
- package/skills/plan-add/SKILL.md +22 -21
- package/skills/plan-sync/EVALS.md +2 -2
- package/skills/plan-sync/SKILL.md +14 -8
- package/skills/pm-frameworks/SKILL.md +6 -0
- package/skills/ponytail/EVALS.md +21 -2
- package/skills/ponytail/SKILL.md +4 -4
- package/skills/product-analysis/EVALS.md +23 -1
- package/skills/product-analysis/SKILL.md +50 -11
- package/skills/product-flow/EVALS.md +81 -0
- package/skills/product-flow/SKILL.md +91 -0
- package/skills/research/SKILL.md +99 -2
- package/skills/tezgah-contract/EVALS.md +52 -7
- package/skills/tezgah-contract/SKILL.md +204 -85
package/hooks/tezgah_models.py
CHANGED
|
@@ -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
|
|
241
|
-
|
|
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
|
|
245
|
-
|
|
246
|
-
|
|
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
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
(
|
|
315
|
-
|
|
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
|
-
|
|
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.
|
|
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)
|
package/hooks/tezgah_paths.py
CHANGED
|
@@ -70,7 +70,8 @@ OFF_DIRS = (CONFIG_DIR, os.path.join(HOME, ".claude"))
|
|
|
70
70
|
# the opencode plugin's own `off()` and the gate's control rule all answer from
|
|
71
71
|
# it, and tests/test_paths.py fails when any of them names a switch it lacks.
|
|
72
72
|
# CORE_SWITCHES are the sixteen the always-on contract names. ADR 007 leaves
|
|
73
|
-
# UNCLASSIFIED_SWITCHES open -
|
|
73
|
+
# UNCLASSIFIED_SWITCHES open - three `off()` names CORE does not list (the
|
|
74
|
+
# third, `slices-off`, stops the slice journal: hooks/tezgah_task.py) and two
|
|
74
75
|
# opt-in markers read with `armed()` - and until the owner classifies them the
|
|
75
76
|
# control rule protects them like the rest (bin/tezgah-docs carries the same
|
|
76
77
|
# set for its inventory check; the test pins the two equal).
|
|
@@ -78,7 +79,7 @@ CORE_SWITCHES = ("adhd-off", "consult-off", "docs-judge-off", "exec-mode.off",
|
|
|
78
79
|
"judge-off", "lang-off", "orchestrate-off", "ponytail-auto.off",
|
|
79
80
|
"pretooluse-off", "reminder-off", "research-off", "spec-off",
|
|
80
81
|
"task-off", "triage-off", "verify-off", "workspace-off")
|
|
81
|
-
UNCLASSIFIED_SWITCHES = ("agents-off", "skill-suggest-on", "taste-on",
|
|
82
|
+
UNCLASSIFIED_SWITCHES = ("agents-off", "skill-suggest-on", "slices-off", "taste-on",
|
|
82
83
|
"update-check-off")
|
|
83
84
|
SWITCHES = CORE_SWITCHES + UNCLASSIFIED_SWITCHES
|
|
84
85
|
REPO_MARKS = (".no-adhd", ".no-graph", ".no-lessons", ".no-plan-gate",
|
|
@@ -737,17 +738,99 @@ def codex_bin():
|
|
|
737
738
|
return which_user(os.environ.get("TEZGAH_CODEX_BIN") or "codex")
|
|
738
739
|
|
|
739
740
|
|
|
740
|
-
# The
|
|
741
|
-
#
|
|
742
|
-
#
|
|
743
|
-
|
|
741
|
+
# The reply language (config.json `language`; an older config's `reply_lang`
|
|
742
|
+
# still reads and is never rewritten). The value is the switch: a code with a
|
|
743
|
+
# pack under hooks/lang/ asks for replies in that language, `any` for the
|
|
744
|
+
# user's own, and only a pack that ships reply `markers` (tr) is judged by the
|
|
745
|
+
# Stop language check. A config naming neither key reads as `tr`, the default
|
|
746
|
+
# every install made before the packs keeps; a first install writes the
|
|
747
|
+
# environment's language instead (`env_language`, bin/tezgah-setup).
|
|
748
|
+
LANG_DIR = os.path.join(os.path.dirname(os.path.abspath(__file__)), "lang")
|
|
749
|
+
_PACKS = []
|
|
744
750
|
|
|
745
751
|
|
|
746
|
-
def
|
|
747
|
-
"""The
|
|
752
|
+
def languages():
|
|
753
|
+
"""The values `language` takes: every shipped pack's code, then `any`."""
|
|
754
|
+
return tuple(packs()) + ("any",)
|
|
755
|
+
|
|
756
|
+
|
|
757
|
+
def language():
|
|
758
|
+
"""The configured reply language, `tr` when unset or not one of `languages()`."""
|
|
748
759
|
cfg = config()
|
|
749
|
-
|
|
750
|
-
|
|
760
|
+
for key in ("language", "reply_lang"):
|
|
761
|
+
value = cfg.get(key) if isinstance(cfg, dict) else None
|
|
762
|
+
if value in languages():
|
|
763
|
+
return value
|
|
764
|
+
return "tr"
|
|
765
|
+
|
|
766
|
+
|
|
767
|
+
def spend_cap():
|
|
768
|
+
"""The standing spend cap in USD, `spend_cap` in config.json: a paid run at
|
|
769
|
+
or under it is a process parameter the agent picks, one over it is asked
|
|
770
|
+
and waits (the ask policy's never-default class). 0 when unset, negative
|
|
771
|
+
or not a number, so every spend is asked."""
|
|
772
|
+
cfg = config()
|
|
773
|
+
value = cfg.get("spend_cap") if isinstance(cfg, dict) else None
|
|
774
|
+
ok = isinstance(value, (int, float)) and not isinstance(value, bool)
|
|
775
|
+
return float(value) if ok and value > 0 else 0.0
|
|
776
|
+
|
|
777
|
+
|
|
778
|
+
def env_language(environ=None):
|
|
779
|
+
"""The language a first install asks for: the first of LANGUAGE (its own
|
|
780
|
+
first `:` entry), LC_ALL, LC_MESSAGES and LANG that is set and not empty -
|
|
781
|
+
GNU gettext's order - cut to its language code (`tr_TR.UTF-8` -> `tr`), or
|
|
782
|
+
`en` when that code has no pack (`C`, `POSIX`, a language not shipped)."""
|
|
783
|
+
environ = os.environ if environ is None else environ
|
|
784
|
+
for key in ("LANGUAGE", "LC_ALL", "LC_MESSAGES", "LANG"):
|
|
785
|
+
value = (environ.get(key) or "").split(":")[0]
|
|
786
|
+
if value:
|
|
787
|
+
code = value.split(".")[0].split("@")[0].split("_")[0].lower()
|
|
788
|
+
return code if code in packs() else "en"
|
|
789
|
+
return "en"
|
|
790
|
+
|
|
791
|
+
|
|
792
|
+
def packs():
|
|
793
|
+
"""{code: pack} for every hooks/lang/<code>.json, read once per process.
|
|
794
|
+
|
|
795
|
+
A pack is data: one language's half of every detection class (S5's
|
|
796
|
+
inventory) and the contract's own words (`say`), keyed alike in every pack
|
|
797
|
+
(tests/test_lang_packs.py). The detectors compile the union of all packs,
|
|
798
|
+
so a reply or prompt in any shipped language arms the same classes whatever
|
|
799
|
+
`language` says; a language is added by one file, never a regex edit."""
|
|
800
|
+
if not _PACKS:
|
|
801
|
+
found = {}
|
|
802
|
+
for name in sorted(os.listdir(LANG_DIR)):
|
|
803
|
+
if name.endswith(".json"):
|
|
804
|
+
with open(os.path.join(LANG_DIR, name), encoding="utf-8") as fh:
|
|
805
|
+
found[name[:-5]] = json.load(fh)
|
|
806
|
+
_PACKS.append(found)
|
|
807
|
+
return _PACKS[0]
|
|
808
|
+
|
|
809
|
+
|
|
810
|
+
def pack_values(*path):
|
|
811
|
+
"""Each pack's value at `path` (nested keys), in pack order, skipping a
|
|
812
|
+
pack that lacks it."""
|
|
813
|
+
for pack in packs().values():
|
|
814
|
+
node = pack
|
|
815
|
+
for key in path:
|
|
816
|
+
node = node.get(key) if isinstance(node, dict) else None
|
|
817
|
+
if node is not None:
|
|
818
|
+
yield node
|
|
819
|
+
|
|
820
|
+
|
|
821
|
+
def alts(*path):
|
|
822
|
+
"""The fragments every pack lists at `path`, joined into one alternation;
|
|
823
|
+
`(?!)`, which never matches, when no pack lists one."""
|
|
824
|
+
return "|".join(f for frags in pack_values(*path) for f in frags) or "(?!)"
|
|
825
|
+
|
|
826
|
+
|
|
827
|
+
def say(code=None):
|
|
828
|
+
"""The contract's words for `code` (the configured language by default):
|
|
829
|
+
the English pack's `say` map with the code's own pack laid over it, so a
|
|
830
|
+
language with no pack, or `any`, speaks the canonical English tokens."""
|
|
831
|
+
words = dict(packs().get("en", {}).get("say", {}))
|
|
832
|
+
words.update(packs().get(code or language(), {}).get("say", {}))
|
|
833
|
+
return words
|
|
751
834
|
|
|
752
835
|
|
|
753
836
|
# Switch latching (plan 051, ADR 018). A hook process that knows its session
|
|
@@ -861,6 +944,16 @@ def pony_level():
|
|
|
861
944
|
return value if value in PONY_LEVELS else "full"
|
|
862
945
|
|
|
863
946
|
|
|
947
|
+
# The router's input-side tokens (input plus cache reads and writes) on its
|
|
948
|
+
# newest step, above which the per-turn reminder asks it to push heavy reads
|
|
949
|
+
# through a slice (tezgah_context.context_nudge). Measured on 12,576 omp
|
|
950
|
+
# main-thread steps (orchestrator-flow-audit S2 F1): median 396 k, p90 724 k. The
|
|
951
|
+
# median is the normal working state and would fire on every other turn, so the
|
|
952
|
+
# line sits just under p90: it marks the heaviest tenth of steps and stays rare
|
|
953
|
+
# enough to be read.
|
|
954
|
+
CONTEXT_NUDGE_TOKENS = 700000
|
|
955
|
+
|
|
956
|
+
|
|
864
957
|
def tool(name):
|
|
865
958
|
"""Stable installed path for a tezgah CLI, else the plugin's own copy, so
|
|
866
959
|
the text injected into a session names something that actually exists."""
|