@r1z4x/tezgah 1.4.0 → 1.5.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/bin/tezgah-taste CHANGED
@@ -39,9 +39,11 @@ design T1P). Each prompt after a writing turn - from the capture rows in the
39
39
  from this repository's omp and Claude transcripts - becomes one structured signal
40
40
  built by code: the prompt, the files written, the agent's edits and the user's
41
41
  own edit between two agent writes. One typed decision per signal, required from
42
- TypeSafe (`only=("typesafe",)`): preference, defect or none; the category; the
43
- scope; and how it relates to each learning held. A decision any other provider
44
- gave is recorded `unverified` and never applied. A defect goes to the `defects`
42
+ the Jev model (`only=("jev",)`: TypeSafe, OpenRouter System One, Cloudflare
43
+ Workers AI or a compatible endpoint - whichever carrier the judge seam resolves):
44
+ preference, defect or none; the category; the scope; and how it relates to each
45
+ learning held. A decision any other provider gave is recorded `unverified` and
46
+ never applied. A defect goes to the `defects`
45
47
  table, never into the ledger. The ledger itself is `hooks/tezgah_taste_ledger.py`. A
46
48
  learning that becomes active gets one readable line from a generative provider
47
49
  (a `text` question; the user's own words when none answers).
@@ -320,7 +322,7 @@ def classify(texts, model, only=None):
320
322
 
321
323
  The rule rides the state, so each request pays for it once and the
322
324
  questions carry only their own prompt."""
323
- labels, tokens_in, tokens_out, used = [], 0, 0, model
325
+ labels, tokens_in, tokens_out, used, spent = [], 0, 0, model, 0.0
324
326
  for start in range(0, len(texts), BATCH):
325
327
  chunk = texts[start:start + BATCH]
326
328
  questions = {"p%d" % i: {"type": "choice", "criteria": CRITERIA,
@@ -332,24 +334,36 @@ def classify(texts, model, only=None):
332
334
  tokens_in += result["usage"]["input_tokens"]
333
335
  tokens_out += result["usage"]["output_tokens"]
334
336
  used = result.get("model", used)
337
+ one = result_cost(result)
338
+ spent = None if spent is None or one is None else spent + one
335
339
  for i in range(len(chunk)):
336
340
  label = tezgah_judge.choice(result, "p%d" % i)
337
341
  labels.append(label if label in CLASSES else None)
338
342
  return labels, {"model": used, "input_tokens": tokens_in,
339
- "output_tokens": tokens_out, "cost_usd": cost(used, tokens_in,
340
- tokens_out)}
341
-
342
-
343
- def cost(model, tokens_in, tokens_out):
344
- """Dollars at the model's listed price per 1M tokens, None when unlisted."""
345
- price = JEV_PRICE if model == tezgah_judge.MODEL else (
346
- tezgah_models.overlay().get("prices", {}).get(model)
347
- or tezgah_models.SNAPSHOT.get(model))
343
+ "output_tokens": tokens_out,
344
+ "cost_usd": None if spent is None else round(spent, 6)}
345
+
346
+
347
+ def cost(model, tokens_in, tokens_out, provider=None):
348
+ """Dollars at the model's listed price per 1M tokens, None when unlisted. A
349
+ typed carrier (`tezgah_judge.input_price`) is priced by the carrier that
350
+ answered: Clef's price is not Jev's."""
351
+ typed = tezgah_judge.input_price(provider)
352
+ price = (typed, 0.0) if typed is not None else JEV_PRICE \
353
+ if model == tezgah_judge.MODEL else (
354
+ tezgah_models.overlay().get("prices", {}).get(model)
355
+ or tezgah_models.SNAPSHOT.get(model))
348
356
  if not (isinstance(price, (list, tuple)) and len(price) == 2):
349
357
  return None
350
358
  return round((tokens_in * price[0] + tokens_out * price[1]) / 1e6, 6)
351
359
 
352
360
 
361
+ def result_cost(result):
362
+ """One judge result's cost, priced by the provider that answered it."""
363
+ return cost(result.get("model"), result["usage"]["input_tokens"],
364
+ result["usage"]["output_tokens"], result.get("provider"))
365
+
366
+
353
367
  def usage_line(usage):
354
368
  money = ("$%.6f" % usage["cost_usd"] if usage["cost_usd"] is not None
355
369
  else "cost unknown (no listed price)")
@@ -370,7 +384,7 @@ def cmd_measure(args):
370
384
  if not tezgah_judge.available():
371
385
  return misuse("no judge: no credential resolves or judge-off is set; nothing sent")
372
386
  got, usage = classify([text for text, _gold in pairs], args.model,
373
- only=("typesafe",) if args.typed else None)
387
+ only=("jev",) if args.typed else None)
374
388
  confusion = {g: {p: 0 for p in CLASSES} for g in CLASSES}
375
389
  unjudged = 0
376
390
  for (_text, gold), pred in zip(pairs, got):
@@ -628,12 +642,12 @@ def picked(result, qid, default):
628
642
 
629
643
 
630
644
  def decide(sig, learnings, fallback=True):
631
- """(decision, result): the typed decision, TypeSafe required; when it cannot
645
+ """(decision, result): the typed decision, the Jev model required; when it cannot
632
646
  answer, the seam's own providers, recorded unverified - only on a signal's
633
647
  first try, since an unverified answer is never applied and a rerun would pay
634
648
  for it again."""
635
649
  state, questions = decision_request(sig, learnings)
636
- result = tezgah_judge.ask(state, questions, only=("typesafe",), timeout=60)
650
+ result = tezgah_judge.ask(state, questions, only=("jev",), timeout=60)
637
651
  if result is None and fallback:
638
652
  result = tezgah_judge.ask(state, questions, timeout=120)
639
653
  if result is None:
@@ -643,7 +657,7 @@ def decide(sig, learnings, fallback=True):
643
657
  "scope": picked(result, "scope", "repository"),
644
658
  "relations": {lr["id"]: picked(result, "rel_" + lr["id"], "unrelated")
645
659
  for lr in learnings},
646
- "verified": result.get("provider") == "typesafe"}
660
+ "verified": tezgah_judge.is_jev(result.get("provider"))}
647
661
  return dec, result
648
662
 
649
663
 
@@ -678,7 +692,7 @@ def learn_refusal(repo):
678
692
  """Why `learn` sends and writes nothing here, or None - read before the
679
693
  store lock, so a refused run leaves no lock file behind."""
680
694
  if tezgah_judge.tp.off("judge-off") or not (
681
- tezgah_judge.available() or tezgah_judge.named(("typesafe",))):
695
+ tezgah_judge.available() or tezgah_judge.named(("jev",))):
682
696
  return "no judge: no credential resolves or judge-off is set; nothing sent"
683
697
  import tezgah_context
684
698
  if ".no-taste" in tezgah_context.repo_marks(repo)[1]:
@@ -692,16 +706,16 @@ def cmd_learn(args):
692
706
  else capture_signals(repo))
693
707
  decided = {}
694
708
  for r in tl.rows(repo, "decisions"):
695
- decided[r.get("id")] = decided.get(r.get("id")) or r.get("provider") == "typesafe"
696
- # a typed decision is final; an unverified one is retried, TypeSafe only
709
+ decided[r.get("id")] = decided.get(r.get("id")) or tezgah_judge.is_jev(r.get("provider"))
710
+ # a typed decision is final; an unverified one is retried, the Jev model only
697
711
  todo = [s for s in signals if not decided.get(s["id"])]
698
712
  if args.limit is not None:
699
713
  todo = todo[:args.limit]
700
714
  ledgers = tl.load(repo)
701
715
  before = {k for k, v in tl.every(ledgers).items() if v["state"] == "active"}
702
- counts = {"signals": len(todo), "typesafe": 0, "unverified": 0, "unanswered": 0,
716
+ counts = {"signals": len(todo), "jev": 0, "unverified": 0, "unanswered": 0,
703
717
  "preference": 0, "defect": 0, "none": 0}
704
- tokens = [0, 0]
718
+ tokens, spent = [0, 0], 0.0
705
719
  for sig in todo:
706
720
  dec, result = decide(sig, held(ledgers),
707
721
  fallback=not args.no_fallback and sig["id"] not in decided)
@@ -710,6 +724,8 @@ def cmd_learn(args):
710
724
  continue
711
725
  tokens[0] += result["usage"]["input_tokens"]
712
726
  tokens[1] += result["usage"]["output_tokens"]
727
+ one = result_cost(result)
728
+ spent = None if spent is None or one is None else spent + one
713
729
  day = tl.now_day()
714
730
  tl.append(repo, "decisions", {
715
731
  "id": sig["id"], "day": day, "at": sig.get("at"), "session": sig["session"],
@@ -721,7 +737,7 @@ def cmd_learn(args):
721
737
  if not dec["verified"]:
722
738
  counts["unverified"] += 1
723
739
  continue
724
- counts["typesafe"] += 1
740
+ counts["jev"] += 1
725
741
  counts[dec["kind"][0] if dec["kind"][0] in CLASSES else "none"] += 1
726
742
  if dec["kind"][0] == "defect":
727
743
  tl.append(repo, "defects",
@@ -743,16 +759,18 @@ def cmd_learn(args):
743
759
  for learning in tl.every(ledgers).values():
744
760
  states[learning["state"]] = states.get(learning["state"], 0) + 1
745
761
  report = dict(counts, states=states, input_tokens=tokens[0], output_tokens=tokens[1],
746
- cost_usd=cost(tezgah_judge.MODEL, tokens[0], tokens[1]))
762
+ cost_usd=None if spent is None else round(spent, 6))
747
763
  if args.json:
748
764
  print(json.dumps(report, indent=2))
749
765
  return 0
750
- print("signals %d: typesafe %d, unverified %d, unanswered %d" % (
751
- counts["signals"], counts["typesafe"], counts["unverified"], counts["unanswered"]))
766
+ print("signals %d: jev %d, unverified %d, unanswered %d" % (
767
+ counts["signals"], counts["jev"], counts["unverified"], counts["unanswered"]))
752
768
  print("decided: preference %d, defect %d, none %d" % (
753
769
  counts["preference"], counts["defect"], counts["none"]))
754
770
  print("ledger: " + (", ".join("%s %d" % kv for kv in sorted(states.items())) or "empty"))
755
- print("typed decisions: %d input tokens, $%s" % (tokens[0], report["cost_usd"]))
771
+ money = report["cost_usd"]
772
+ print("typed decisions: %d input tokens, %s" % (
773
+ tokens[0], "$%s" % money if money is not None else "cost unknown (no listed price)"))
756
774
  return 0
757
775
 
758
776
 
@@ -808,8 +826,8 @@ def cmd_label(args):
808
826
  sample of unlabelled typed decisions - drawn without regard to what was
809
827
  injected, so the calibration bound rests on an unselected sample."""
810
828
  repo = store_or_exit(args)
811
- decided = {r["id"]: r for r in tl.rows(repo, "decisions", "WHERE provider = ?",
812
- ("typesafe",)) if r.get("id")}
829
+ decided = {r["id"]: r for r in tl.rows(repo, "decisions", *tl.jev_rows())
830
+ if r.get("id")}
813
831
  labelled = {r.get("id") for r in tl.rows(repo, "labels")}
814
832
  if args.id:
815
833
  if args.label not in CLASSES:
@@ -873,7 +891,7 @@ def main(argv):
873
891
  measure.add_argument("--model", default=tezgah_judge.MODEL)
874
892
  measure.add_argument("--json", action="store_true")
875
893
  measure.add_argument("--typed", action="store_true",
876
- help="require TypeSafe (the decision T1P's ledger trusts)")
894
+ help="require the Jev model (the decision T1P's ledger trusts)")
877
895
  rate = sub.add_parser("rate")
878
896
  rate.add_argument("--in", dest="inp", required=True)
879
897
  rate.add_argument("--limit", type=int)
@@ -883,7 +901,7 @@ def main(argv):
883
901
  learn.add_argument("--host", choices=("omp", "claude", "all"), default="all")
884
902
  learn.add_argument("--limit", type=int)
885
903
  learn.add_argument("--no-fallback", action="store_true",
886
- help="TypeSafe only: never pay another provider for a decision")
904
+ help="the Jev model only: never pay another provider for a decision")
887
905
  listing = sub.add_parser("list")
888
906
  listing.add_argument("--all", action="store_true")
889
907
  for name in ("show", "accept", "reject", "edit"):
package/bin/tezgah-triage CHANGED
@@ -4,7 +4,8 @@
4
4
  A `browser_snapshot` / `mobile_list_elements_on_screen` result is the whole screen
5
5
  as text - one measured 40-row work-order table came back as 13,045 characters in
6
6
  356 lines - and the analyze-app loop pays for all of it to find the dozen lines
7
- that carry the controls. This flattens the snapshot, asks TypeSafe about the
7
+ that carry the controls. This flattens the snapshot, asks the judge seam (the
8
+ session's own CLI or the Jev model through any carrier) about the
8
9
  units of the tree (a row, a table cell, a control on its own) in ONE batched
9
10
  request, and prints the line ids under the selected units with their original
10
11
  element refs.
@@ -90,10 +91,13 @@ MEASURED = ("100% of the control lines at a 94% read on a reproduced 356-line "
90
91
  "screen")
91
92
  CUT = 96
92
93
  USAGE = "tezgah-triage --select FILE --task TEXT | --states FILE [--component TEXT]"
93
- NO_CREDENTIAL = ("no judge: no session CLI (omp or claude, from OMPCODE/CLAUDECODE), "
94
- "and no third party the `fallback` setting allows (TYPESAFE_API_KEY "
95
- "or ~/.config/typesafe/key, or the OpenRouter fallback's "
96
- "OPENROUTER_API_KEY or ~/.config/openrouter/key), or the judge-off "
94
+ NO_CREDENTIAL = ("no judge: no session CLI (omp, claude, opencode, cursor or codex, "
95
+ "the one this session runs under), and no third party the `fallback` "
96
+ "setting allows - the Jev model through TypeSafe (TYPESAFE_API_KEY or "
97
+ "~/.config/typesafe/key), OpenRouter System One or the OpenRouter chat "
98
+ "fallback (OPENROUTER_API_KEY or ~/.config/openrouter/key), Cloudflare "
99
+ "Workers AI, or a compatible endpoint (JEV_API_BASE_URL), the carrier "
100
+ "picked by TEZGAH_JEV_PROVIDER or config `jev` - or the judge-off "
97
101
  "switch is on")
98
102
  TRIAGE_OFF = "the triage-off switch is on"
99
103
  FAILED = ("the call failed - a timeout, a refusal, or a reply that is not the "
@@ -197,10 +201,11 @@ def trim(line):
197
201
 
198
202
  def judge_line(result):
199
203
  usage = result["usage"]
204
+ # the carrier's own price (Clef's is not Jev's); Jev's for a chat stand-in
205
+ price = tezgah_judge.input_price(result.get("provider")) or PRICE_PER_MILLION
200
206
  print("judge: %d input tokens, %d output, %d ms; $%.6f at $%.3f/1M input"
201
207
  % (usage["input_tokens"], usage["output_tokens"], result["latency_ms"],
202
- usage["input_tokens"] * PRICE_PER_MILLION / 1000000,
203
- PRICE_PER_MILLION))
208
+ usage["input_tokens"] * price / 1000000, price))
204
209
 
205
210
 
206
211
  def units(numbered):
@@ -18,10 +18,10 @@
18
18
  "semicolons": 9
19
19
  },
20
20
  "docs/contract.md": {
21
- "long_sentences": 43,
22
- "passive": 26,
21
+ "long_sentences": 42,
22
+ "passive": 25,
23
23
  "replace_words": 0,
24
- "semicolons": 31
24
+ "semicolons": 30
25
25
  },
26
26
  "docs/environment.md": {
27
27
  "long_sentences": 3,
@@ -61,9 +61,9 @@
61
61
  },
62
62
  "docs/judge.md": {
63
63
  "long_sentences": 22,
64
- "passive": 15,
64
+ "passive": 14,
65
65
  "replace_words": 0,
66
- "semicolons": 14
66
+ "semicolons": 13
67
67
  },
68
68
  "docs/layers.md": {
69
69
  "long_sentences": 20,
@@ -78,7 +78,7 @@
78
78
  "semicolons": 9
79
79
  },
80
80
  "docs/operations.md": {
81
- "long_sentences": 67,
81
+ "long_sentences": 66,
82
82
  "passive": 21,
83
83
  "replace_words": 3,
84
84
  "semicolons": 42
package/docs/contract.md CHANGED
@@ -38,7 +38,9 @@ rules and all.
38
38
  The skill suggestion is the one surface a judgement writes rather than a constant.
39
39
  The roster reaches a session as an index of host-truncated one-liners, so which
40
40
  entry to look at first is the turn's own weak spot; one batched judgement request
41
- answers it - TypeSafe, or the OpenRouter fallback when no TypeSafe key resolves
41
+ answers it - the session's own CLI, else the Jev model through whichever carrier
42
+ resolves (TypeSafe, OpenRouter System One, Cloudflare Workers AI, a compatible
43
+ endpoint), else the OpenRouter chat fallback
42
44
  (`credential()`, `hooks/tezgah_judge.py::credential`) - a Choice across the installed
43
45
  skill names plus `none`, and one Noul
44
46
  asking whether the turn wants a skill at all - and the winner is appended after
@@ -158,11 +160,14 @@ never told this; and a ledger with no such row costs no bytes. `verify-off` drop
158
160
  it with the integrity rule it restates.
159
161
 
160
162
  The on-demand contract also carries detail the core has no budget for. One is
161
- the review rule: a change with an executable oracle - a checker, a gate rule, a
162
- parser - is reviewed by four direction-led reviewers whose every claim is built
163
- as a fixture and run before it counts, a tentative default measured on two
164
- research lines with a written flip; everything else keeps one fresh reviewer
165
- (`skills/tezgah-contract/SKILL.md:503-527`).
163
+ the review rule. A plan gets one review over its whole diff, in at most two
164
+ rounds. Round one fans out one reviewer per dimension. Only a confirmed critical
165
+ or major finding opens round two, and `tezgah-task review` refuses a third. A
166
+ change with an executable oracle - a checker, a gate rule, a parser - spends the
167
+ same budget. Its round one is four direction-led reviewers, and each claim
168
+ becomes a fixture that runs before it counts. That population is a tentative
169
+ default, measured on two research lines with a written flip
170
+ (`skills/tezgah-contract/SKILL.md:519-550`).
166
171
 
167
172
  ## Composition order of the always-on core
168
173
 
@@ -272,12 +277,17 @@ sentence of the rule.
272
277
  `PROMPT_HINTS` (`hooks/tezgah_context.py::PROMPT_HINTS`) is one compiled pattern per key
273
278
  — `spec`, `consult`, `research`, `product`, `graph`, `fanout` — and `classify_prompt()` returns the keys
274
279
  a prompt matches (`hooks/tezgah_context.py::classify_prompt`). `fanout` arms on the
275
- shape of the request, not a word. Two or more list entries on their own lines
276
- arm it, and so does an explicit subagent, parallel or worktree ask. The owner
277
- no longer has to say "do it with subagents, in parallel". On that turn only, the
278
- matching paragraphs are appended after the reminder
280
+ shape of the request, not a word. Two list entries on their own lines arm it.
281
+ An entry starts with `1.`, `1)`, `1 -`, `1:`, `a)` or a bullet. An inline
282
+ `1. ... 2. ...` arms it too. So do three files named as one list, "First, ...
283
+ Second, ..." and three imperative clauses in one sentence. An explicit
284
+ subagent, parallel or worktree ask arms it as well. The owner no longer has to
285
+ say "do it with subagents, in parallel". On that turn only, the matching
286
+ paragraphs are appended after the reminder
279
287
  (`hooks/tezgah_context.py::context_for`); a session that never asks such a question
280
- pays the one-line pointer instead. The patterns carry Turkish stems because the
288
+ pays the one-line pointer instead. A later match in the same session pays one
289
+ line. The fan-out line still names the action
290
+ (`hooks/tezgah_context.py::armed_again`). The patterns carry Turkish stems because the
281
291
  user writes Turkish, and a plain prompt arms nothing. The same prompt arms the
282
292
  same rules on every host (`tests/test_context.py::ArmingConformance.test_all_hosts_arm_the_same_rules`) and each advisory
283
293
  rule keeps a pointer line in the always-on text (`tests/test_context.py::ArmingConformance.test_every_advisory_rule_has_an_always_on_pointer`).
@@ -56,6 +56,7 @@ the kill switches are files, not variables, and live in
56
56
  | `TEZGAH_ORCA_BIN` | `hooks/tezgah_orca.py::cli` | `ORCA_CLI_COMMAND`, then `$ORCA_CLI_BIN_DIR/orca`, then `orca` on PATH (`orca-ide` on Linux outside Orca), then the macOS app's CLI | The Orca CLI `tezgah-status --orca` and `tezgah-setup --status` ask. A missing path is no CLI, never a fallback: the test suite points it at one. |
57
57
  | `TEZGAH_OMP_BIN` | `hooks/tezgah_paths.py::omp_bin` | `omp` on PATH | The omp binary the installer, the model table, consult's session-model lookup and the judgement seam's session call run; tests point it at a stand-in. |
58
58
  | `TEZGAH_CLAUDE_BIN` | `claude_bin`, `hooks/tezgah_paths.py::claude_bin` | `claude` on PATH | The Claude Code CLI `--install` registers the plugin with (`claude plugin`) and the judgement seam asks in a Claude Code session; the test suite points it at a path that does not exist, or at a stand-in. |
59
+ | `TEZGAH_OPENCODE_BIN`, `TEZGAH_CURSOR_BIN`, `TEZGAH_CODEX_BIN` | `hooks/tezgah_paths.py::opencode_bin`, `hooks/tezgah_paths.py::cursor_bin`, `hooks/tezgah_paths.py::codex_bin` | `opencode`, `cursor-agent`, `codex` on PATH or a per-user bin dir | The CLI the judgement seam asks in that host's session; tests point them at stand-ins. |
59
60
  | `TEZGAH_DSH_BIN` | `bin/tezgah-setup::reconcile_json_servers` | the profile-local entry, then `bin/tezgah-dsh`, then npx | How the installer invokes the dsh CLI. |
60
61
  | `TEZGAH_INDEX_BIN` | `bin/tezgah-dsh:19`, `bin/tezgah-dsh.cmd` | `~/.config/tezgah/bin/tezgah-index` | The index worker `tezgah-dsh` warms before it starts dsh. |
61
62
  | `TEZGAH_STATUS_BIN` | `hosts/dsh/statusline/lib/index.js:24`, `hosts/opencode/tui/tezgah-tui.tsx` | `~/.config/tezgah/bin/tezgah-status` | The status renderer the dsh status line and the opencode TUI call. |
@@ -73,7 +74,7 @@ the kill switches are files, not variables, and live in
73
74
 
74
75
  | Variable | Read by | Default | Effect |
75
76
  |---|---|---|---|
76
- | `OMPCODE`, `CLAUDECODE` | `hooks/tezgah_paths.py::session_cli`, `bin/consult`, `hooks/tezgah_models.py::worker_model` | set by the host | Name the host session: the judgement seam asks that host's CLI first (`OMPCODE` wins, omp sets both). The test suite unsets both. |
77
+ | `OMPCODE`, `CLAUDECODE`, `OPENCODE`, `CURSOR_AGENT`, `CURSOR_VERSION`, `CODEX_THREAD_ID` | `hooks/tezgah_paths.py::session_cli` (all), `bin/consult`, `hooks/tezgah_models.py::worker_model` (the first two) | set by the host | Name the host session: the judgement seam asks that host's CLI first, in the order listed (omp sets both of the first two, so `OMPCODE` wins; Cursor sets `CURSOR_AGENT` in its shell tool and `CURSOR_VERSION` in its hooks; Codex sets `CODEX_THREAD_ID` in its shell tool only). The test suite unsets all of them. |
77
78
  | `TYPESAFE_API_KEY` | `hooks/tezgah_judge.py::key`, `hooks/tezgah_paths.py` | `~/.config/typesafe/key` | The judgement seam's first third-party credential, used as `fallback` in `config.json` allows ([judge](judge.md)); omp also reads it for its own judge. |
78
79
  | `OPENROUTER_API_KEY` | `hooks/tezgah_judge.py::openrouter_key`, `bin/consult`, `bin/codegen`, `hooks/tezgah_models.py` | `~/.config/openrouter/key` | The seam's chat fallback, consult's and codegen's `openrouter` provider, and omp's `any` family. |
79
80
  | `DEEPSEEK_API_KEY`, `INCEPTION_API_KEY` | `bin/consult`, `bin/codegen`, `hooks/tezgah_replay.py::label_model` | `~/.config/deepseek/key`, `~/.config/inception/key` | The `deepseek` and `inception` providers of consult and codegen; `DEEPSEEK_API_KEY` alone is also the replay sheet's `--provider deepseek` rater. |
@@ -82,6 +83,7 @@ the kill switches are files, not variables, and live in
82
83
  | `TEZGAH_OPENROUTER_URL` | `hooks/tezgah_judge.py::openrouter_url` | OpenRouter's chat-completions URL | Repoints the seam's chat fallback (tests). Plain `http` is refused unless the host is this machine. |
83
84
  | `TEZGAH_DEEPSEEK_URL` | `hooks/tezgah_replay.py::_deepseek` | `https://api.deepseek.com/chat/completions` | Repoints the replay sheet's `--provider deepseek` rater (tests). Plain `http` is refused unless the host is this machine. |
84
85
  | `TEZGAH_JUDGE_MODEL` | `hooks/tezgah_judge.py::fallback_model` | the cheap `any` row of the models table | The model the seam's chat fallback asks. |
86
+ | `TEZGAH_JUDGE_CLI` | `hooks/tezgah_paths.py::session_cli` | `judge_cli` in `config.json`, else `auto` | The session CLI the judgement seam asks: `omp`, `claude`, `opencode`, `cursor` or `codex`, or `auto` to read the host's marker. A named CLI that is not installed means no session CLI, never another one. |
85
87
 
86
88
  ## consult and codegen
87
89
 
package/docs/evidence.md CHANGED
@@ -785,11 +785,12 @@ one rater's totals would show the other rater those labels.
785
785
 
786
786
  `replay --label-model --prompt <file>` is rater 2 when that rater is a model. It sends each sheet
787
787
  row to the judge seam (`hooks/tezgah_judge.py`) as one `choice` question and writes
788
- `labels-model.jsonl` beside the sheet. It redacts the row's text first (`ti.redact`), and it runs
789
- only when TypeSafe answers. The prompt file holds each set's instructions and criteria. A reply
788
+ `labels-model.jsonl` beside the sheet. It redacts the row's text first (`ti.redact`). It runs
789
+ only when a Jev carrier answers: TypeSafe, OpenRouter System One, Cloudflare Workers AI or a
790
+ compatible endpoint. `--provider jev` names it, and the old name `typesafe` still works. The prompt file holds each set's instructions and criteria. A reply
790
791
  with no label from those criteria leaves its row unlabelled. The command writes each label as it
791
792
  arrives, so a run that stops early keeps what it paid for. A second run asks only the unlabelled rows. Every run prints the input tokens and their cost. The sheet rows leave the machine for
792
- `api.typesafe.ai`. The key and the corpus stay on it. Kappa against a model rater
793
+ the Jev carrier's endpoint. The key and the corpus stay on it. Kappa against a model rater
793
794
  measures human-model agreement, not inter-rater reliability.
794
795
 
795
796
  `--provider deepseek --model deepseek-v4-pro` asks the same questions from the same prompt file
package/docs/glossary.md CHANGED
@@ -98,7 +98,7 @@ A mark consumed by its own first use, so the thing it protects happens once: the
98
98
  An opt-out file in the repo tree - `.no-ponytail`, `.no-adhd`, `.no-graph`, `.no-lessons`, `.no-taste` - collected by `repo_marks()` walking up to the enclosing root (`hooks/tezgah_context.py::repo_marks`). Not a [kill switch](#kill-switch): a mark travels with the repo, a switch with the machine.
99
99
 
100
100
  ### plan
101
- A piece of work spanning sessions, one markdown file with frontmatter under `<repo root>/.tezgah/plans/open/`, moved to `.tezgah/plans/done/` when closed, up to three of them injected by `open_plans()` (`hooks/tezgah_context.py::open_plans`) and its work happening on a `plan/NNN-slug` branch. Not a TODO in code.
101
+ A piece of work spanning sessions, one markdown file with frontmatter under `<repo root>/.tezgah/plans/open/`, moved to `.tezgah/plans/done/` when closed, up to three of them injected by `open_plans()` (`hooks/tezgah_context.py::open_plans`) and its work happening on a `plan/NNN-slug` branch. An optional `after:` names the plans it waits on, and `tezgah-task status` reads it with the ticked Acceptance boxes (`hooks/tezgah_task.py::queue`). Not a TODO in code, and not the host's session todo list, which no reader syncs with it.
102
102
 
103
103
  ### plan table
104
104
  The status table in `.tezgah/plans/README.md` (`.tezgah/plans/README.md:9`), owned by the `plan-status` skill (`skills/plan-status/SKILL.md:1`) and among the first blocks the [budget](#budget) gives up because it lives on disk (`hooks/tezgah_context.py::DROP_ORDER`). Not the injected list of open plans.
package/docs/judge.md CHANGED
@@ -14,9 +14,10 @@ the kill-switch paragraph and nothing else (`CORE`,
14
14
  One seam, five callers, one provider order, one egress boundary, one price, one
15
15
  redirect guard. The module is stdlib only and total - a failure is a `None`, never
16
16
  an exception, because a hook imports it and a hook that raises takes a session
17
- down (`_request`, `hooks/tezgah_judge.py::_request`). One endpoint and one key path
18
- are module constants (`URL`, `hooks/tezgah_judge.py::URL`; `KEY_FILE`,
19
- `hooks/tezgah_judge.py::KEY_FILE`), one opener is shared by every call (`OPENER`,
17
+ down (`_request`, `hooks/tezgah_judge.py::_request`). The carriers' endpoints are
18
+ module constants: `URL` (`hooks/tezgah_judge.py::URL`), `JEV_OPENROUTER_URL`
19
+ (`hooks/tezgah_judge.py::JEV_OPENROUTER_URL`) and `CLOUDFLARE_URL`
20
+ (`hooks/tezgah_judge.py::CLOUDFLARE_URL`). One opener serves every call (`OPENER`,
20
21
  `hooks/tezgah_judge.py::OPENER`), and it refuses a redirect that leaves the
21
22
  endpoint's host, so a `Location` cannot carry the bearer (`guarded_opener`,
22
23
  `hooks/tezgah_paths.py`, the one copy `bin/consult` and `bin/codegen` use too). A
@@ -25,15 +26,18 @@ attempt. A 4xx, a session CLI's non-zero exit and a malformed reply never do
25
26
  (`_transient`, `hooks/tezgah_judge.py::_transient`). A hook caller asks for one
26
27
  attempt and a wall-clock `deadline` instead (`ask`, `hooks/tezgah_judge.py::ask`).
27
28
  A session CLI answers in 4-10 s (measured 2026-10-07), so the skill hint's
28
- 4 s deadline usually ends first. `TEZGAH_TYPESAFE_URL`, `TEZGAH_OPENROUTER_URL`
29
- and `TEZGAH_OMP_BIN`/`TEZGAH_CLAUDE_BIN` are the test seams.
29
+ 4 s deadline usually ends first. `TEZGAH_TYPESAFE_URL`, `TEZGAH_JEV_OPENROUTER_URL`,
30
+ `TEZGAH_CLOUDFLARE_URL`, `TEZGAH_OPENROUTER_URL` and
31
+ `TEZGAH_OMP_BIN`/`TEZGAH_CLAUDE_BIN` are the test seams. Each URL override goes
32
+ through `override` (`hooks/tezgah_judge.py::override`), which refuses plain http
33
+ off this machine.
30
34
 
31
35
  Every caller reads an answer through the same accessors rather than reaching
32
36
  into the raw reply, so a Choice and a Noul are read one way for all five
33
37
  (`choice`, `hooks/tezgah_judge.py::choice`; `noul`,
34
38
  `hooks/tezgah_judge.py::noul`). A `text` question is the one shape that asks
35
39
  for prose. `bin/tezgah-taste` asks it for a learning's readable line, and only
36
- a generative provider answers one. TypeSafe is never sent one (`text`,
40
+ a generative provider answers one. No Jev carrier is sent one (`text`,
37
41
  `hooks/tezgah_judge.py::text`). All three are total: a missing or
38
42
  wrongly-typed answer is a `None`, never an exception.
39
43
 
@@ -44,9 +48,11 @@ are untouched, so a model answer can never license a "done" (`STEP_KINDS`,
44
48
 
45
49
  The state leaves the machine. By default it goes to the session's own vendor,
46
50
  through the session's own CLI and credential ([below](#the-providers-and-their-order)).
47
- A third party - `api.typesafe.ai` or `openrouter.ai` (`OPENROUTER_URL`,
48
- `hooks/tezgah_judge.py::OPENROUTER_URL`) - reads it only as the `fallback`
49
- setting allows. For the triage the state is the screen's own text, so whoever
51
+ A third party reads it only as the `fallback` setting allows. The third parties
52
+ are the Jev carriers' hosts - `api.typesafe.ai`, `openrouter.ai`,
53
+ `api.cloudflare.com` or a user's `JEV_API_BASE_URL` - and the chat endpoint
54
+ (`OPENROUTER_URL`, `hooks/tezgah_judge.py::OPENROUTER_URL`).
55
+ For the triage the state is the screen's own text, so whoever
50
56
  answers reads any personal data on it. For the docs fallback it is the query.
51
57
  Nothing else goes: no session id, no
52
58
  workspace path, no environment. The seam does not rewrite the state, because
@@ -89,18 +95,74 @@ background `learn` that armed taste starts.
89
95
 
90
96
  `providers()` (`hooks/tezgah_judge.py::providers`) is the order `ask()` tries.
91
97
  The next one is asked only when the previous failed. First comes the session's own
92
- CLI (`tp.session_cli`, `hooks/tezgah_paths.py::session_cli`): `omp` when
93
- `OMPCODE` is set, else `claude` when `CLAUDECODE` is. It runs headless with no
94
- tools, rules, skills, extensions, MCP servers or settings (`SESSION_ARGV`,
95
- `hooks/tezgah_judge.py::SESSION_ARGV`). Measured 2026-10-07, a call cost $0.007
96
- on claude and $0.015 on omp. The session's own credential pays, OAuth subscription
97
- included. The model and usage are read from the CLI's JSON output
98
- (`_session_request`, `hooks/tezgah_judge.py::_session_request`). Then come the
99
- third parties. TypeSafe reads `TYPESAFE_API_KEY`, else `~/.config/typesafe/key`
100
- (`key`, `hooks/tezgah_judge.py::key`). OpenRouter reads `OPENROUTER_API_KEY`, else
101
- `~/.config/openrouter/key` (`openrouter_key`, `hooks/tezgah_judge.py::openrouter_key`).
98
+ CLI (`tp.session_cli`, `hooks/tezgah_paths.py::session_cli`). The host's marker
99
+ names it, in this order:
100
+
101
+ | CLI | Marker | Set by |
102
+ |---|---|---|
103
+ | `omp` | `OMPCODE` | omp, which sets `CLAUDECODE` too |
104
+ | `claude` | `CLAUDECODE` | Claude Code |
105
+ | `opencode` | `OPENCODE` | opencode, in its own process |
106
+ | `cursor` (runs `cursor-agent`) | `CURSOR_AGENT`, `CURSOR_VERSION` | Cursor: the first in its shell tool, the second in its hooks |
107
+ | `codex` | `CODEX_THREAD_ID` | Codex in its shell tool; tezgah's Codex hook sets it from the session id |
108
+
109
+ A marked host whose binary is missing has no session CLI. No other CLI stands
110
+ in. `TEZGAH_JUDGE_CLI`, else `judge_cli` in `~/.config/tezgah/config.json`,
111
+ names one of the five instead. `auto`, the default, reads the marker. A named CLI
112
+ that is not installed is no session CLI either.
113
+
114
+ The CLI runs headless in an empty temp dir. Its flags are `SESSION_ARGV`
115
+ (`hooks/tezgah_judge.py::SESSION_ARGV`). The omp and claude runs drop tools,
116
+ rules, skills, extensions, MCP servers and settings. The other three have no flag
117
+ that drops their tools or takes a system prompt. They run with the least their
118
+ `--help` offers: opencode `--pure`, cursor-agent `--mode ask`, codex
119
+ `-s read-only --ephemeral`. Their prompt starts with the system prompt.
120
+ `opencode run` ran a shell command its prompt asked for in a probe on
121
+ 2026-10-09. So it also gets an inline config that denies every tool
122
+ (`SESSION_ENV`, `hooks/tezgah_judge.py::SESSION_ENV`). With it, the same probe
123
+ wrote nothing. cursor-agent in ask mode refused the same shell call.
124
+ Measured 2026-10-07, a call cost $0.007 on claude and $0.015 on omp. Measured
125
+ 2026-10-09 on one choice question, opencode read 28,646 input tokens ($0.004 by
126
+ its own report), cursor-agent 17,095 and codex 20,919. The session's own
127
+ credential pays, OAuth subscription included. The answer and usage come from the
128
+ CLI's JSON output (`_session_request`, `hooks/tezgah_judge.py::_session_request`).
129
+ `_cli_answer` (`hooks/tezgah_judge.py::_cli_answer`) reads the last three. Only
130
+ omp and claude name the model. Then come the
131
+ third parties: first the chosen Jev carriers, each a real System One transport
132
+ (`_jev_call`, `hooks/tezgah_judge.py::_jev_call`), then the OpenRouter chat
133
+ fallback. `JEV_CARRIERS` (`hooks/tezgah_judge.py::JEV_CARRIERS`) names the six.
134
+ Two of them are not Jev. Clef and Clef-flash are Cloudflare's own decision
135
+ models. They take the same System One request. They answer in the same typed
136
+ shape, so a caller that needs a typed answer takes them like Jev.
137
+
138
+ | Carrier | Endpoint | Credential (env, else file, stripped) | Body |
139
+ |---|---|---|---|
140
+ | `typesafe` | `https://api.typesafe.ai/v1/systemone` | `TYPESAFE_API_KEY`, else `~/.config/typesafe/key`, else the active omp login (`~/.omp/agent/agent.db`, read-only; `typesafe_key`, `hooks/tezgah_paths.py::typesafe_key`) | `{state, model, questions}` |
141
+ | `jev-openrouter` | `https://openrouter.ai/api/v1/systemone` | `OPENROUTER_API_KEY`, else `~/.config/openrouter/key` | the same, a bare `jev-*` id sent as `typesafe/<id>` |
142
+ | `jev-cloudflare` | `https://api.cloudflare.com/client/v4/accounts/<id>/ai/run` | `JEV_CLOUDFLARE_API_TOKEN`, else `CLOUDFLARE_API_TOKEN`, else `~/.config/cloudflare/token`; the account id from `CLOUDFLARE_ACCOUNT_ID`, else `~/.config/cloudflare/account_id` | `{model: "typesafe/jev", input: {state, questions}}`; the reply is read bare or inside `result` |
143
+ | `jev-compatible` | `JEV_API_BASE_URL`, used verbatim | `JEV_API_KEY` | TypeSafe's, the model from `TEZGAH_JEV_MODEL` if set |
144
+ | `clef`, `clef-flash` | `https://api.cloudflare.com/client/v4/accounts/<id>/ai/run/@cf/cloudflare/<name>` | the Cloudflare token and account id above | `{model: "<name>", state, questions}`; the reply read like Cloudflare's above |
145
+
146
+ `JEV_API_BASE_URL` and `JEV_API_KEY` are the names the public jev-mcp server
147
+ reads, so one export serves both. `TEZGAH_JEV_PROVIDER` chooses the carrier,
148
+ else `JEV_PROVIDER`, else `jev` in `~/.config/tezgah/config.json`, else `auto`
149
+ (`jev_choice`, `hooks/tezgah_paths.py::jev_choice`). The values are `auto`,
150
+ `typesafe`, `openrouter`, `cloudflare`, `compatible`, `clef` and `clef-flash`.
151
+ `tezgah-setup --jev <value>` writes the config key
152
+ (`set_jev`, `bin/tezgah-setup::set_jev`). `auto`
153
+ asks every carrier that resolves, in the table's order (`jev_carriers`,
154
+ `hooks/tezgah_paths.py::jev_carriers`). The seam asks a named carrier without
155
+ its credential nothing, and nothing stands in for it: the call records
156
+ `no credential (jev=<value>)` as a failure. `tezgah-status --judge` prints the
157
+ choice and the carriers it resolves to (`jev_summary`,
158
+ `hooks/tezgah_judge.py::jev_summary`). A cost row prices the carrier that
159
+ answered (`input_price`, `hooks/tezgah_judge.py::input_price`). Per 1M input
160
+ tokens: $0.042 for Jev, $0.24 for Clef, $0.09 for Clef-flash. Output is free.
161
+
162
+ OpenRouter's chat fallback reads the OpenRouter key (`openrouter_key`,
163
+ `hooks/tezgah_judge.py::openrouter_key`).
102
164
  It asks the table's cheap row unless `TEZGAH_JUDGE_MODEL` names another
103
- (`fallback_model`, `hooks/tezgah_judge.py::fallback_model`). The key file is
165
+ (`fallback_model`, `hooks/tezgah_judge.py::fallback_model`). The key files are
104
166
  read because a hook runs where `~/.zshenv` never did. A chat answer is asked for in prose
105
167
  (`CHAT_SYSTEM`, `hooks/tezgah_judge.py::CHAT_SYSTEM`) and filtered to the ids
106
168
  asked, a wrongly typed answer dropped (`_chat_answers`,
@@ -111,8 +173,8 @@ asked, a wrongly typed answer dropped (`_chat_answers`,
111
173
 
112
174
  | `fallback` | Session CLI present | No session CLI |
113
175
  |---|---|---|
114
- | `vendor` (default) | the session CLI only; when it fails, no judgement | TypeSafe, then OpenRouter |
115
- | `any` | the session CLI, then TypeSafe, then OpenRouter | TypeSafe, then OpenRouter |
176
+ | `vendor` (default) | the session CLI only; when it fails, no judgement | the Jev carriers, then the chat fallback |
177
+ | `any` | the session CLI, then the Jev carriers, then the chat fallback | the Jev carriers, then the chat fallback |
116
178
  | `none` | the session CLI only | nobody: `available()` is false |
117
179
 
118
180
  Nothing that stands in is silent. A result carries `provider`, the `model`
@@ -124,7 +186,7 @@ hint's cost row and the router's `route` row also carry `judge=<provider>/<model
124
186
 
125
187
  A caller that needs one provider in particular passes `only` (`named`,
126
188
  `hooks/tezgah_judge.py::named`). `learn`'s decision has to come from a typed
127
- model, so it passes `only=("typesafe",)`. It gets `None` rather than another
189
+ model, so it passes `only=("jev",)`, every Jev carrier that resolves. It gets `None` rather than another
128
190
  provider's answer. The caller named the provider, so the `vendor` order does not
129
191
  apply. `none` still keeps every third party out.
130
192
 
@@ -173,8 +235,9 @@ known (`note`, `hooks/tezgah_integrity.py::note`), counted by the row's kind
173
235
  costs a fraction of a cent, so a cache is not worth its state file, and a naive
174
236
  one would make two runs of the same command disagree. Beside the last-use
175
237
  record, the seam keeps a failure marker (`DOWN_FOR`, 300 s, in `hooks/tezgah_judge.py`). A
176
- call that ends on a 401, 402, 5xx or a session CLI's non-zero exit marks that
177
- provider, endpoint and credential down for five minutes. Until the marker
238
+ 401, 402, 403, 5xx or a session CLI's non-zero exit marks that
239
+ provider, endpoint and credential down for five minutes. A 403 is OpenRouter's
240
+ answer to a key past its limit. Until the marker
178
241
  expires, that provider is skipped and sent no request. A dead key or an empty account then costs
179
242
  one refusal per five minutes, not one per prompt. A rotated key gets a new
180
243
  marker, so the seam asks it at once.
@@ -193,8 +256,11 @@ known (`note`, `hooks/tezgah_integrity.py::note`), counted by the row's kind
193
256
 
194
257
  ## Source of truth
195
258
 
196
- - `hooks/tezgah_judge.py` — the seam: endpoint, key path, `available()`, `ask()`,
197
- the answer accessors, the retry class and the provider-down marker.
259
+ - `hooks/tezgah_judge.py` — the seam: the carriers' endpoints and bodies,
260
+ `available()`, `ask()`, the answer accessors, the retry class and the
261
+ provider-down marker.
262
+ - `hooks/tezgah_paths.py` — the key channels (`typesafe_key`, `jev_carriers`)
263
+ and the carrier choice (`jev_choice`).
198
264
  - `bin/tezgah-triage` — the snapshot triage and the per-state matrix; prints the
199
265
  unit it selected, the characters it saved and the cost it paid.
200
266
  - `bin/tezgah-docs` — the page router; its index match, then its one fallback