@r1z4x/tezgah 1.7.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.
package/CHANGELOG.md CHANGED
@@ -6,6 +6,63 @@ adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
6
6
 
7
7
  ## [Unreleased]
8
8
 
9
+ ## [1.8.2] - 2026-10-13
10
+
11
+ ### Fixed
12
+
13
+ - **The v1.8.1 release builds.** A packaging test still pinned the serial suite step that 1.8.1 removed from CI. So the release workflow stopped before it built the artifact. `tests/impacted.py` no longer crashes on a deleted test module. Each neuter fall-through module runs in its own HOME, as the control does.
14
+
15
+ ## [1.8.1] - 2026-10-13
16
+
17
+ ### Changed
18
+
19
+ - **The neuter matrix runs in CI, and in minutes.** Each mutant first runs the one test that killed it last time, kept in `tests/neuter_killers.json`. A full search runs only when that test passes. A cached run took 240 s, against about an hour before. CI runs it on a push that changes a guarded file, and weekly. A session checks a new rule's mutant with `--only <name>`.
20
+ - **The suite is faster.** It starts the largest modules first. Slow tests share their expensive installs per class, and one test now covers each duplicated path. A change under `benchmarks/arm-bench/` runs that lab's module, not all 100.
21
+ - **CI runs the suite once per Python.** The serial `unittest discover` step is gone. The sharded job runs the same modules, and both caught the same failures. A push to `main` now takes about 11 minutes, not 30.
22
+
23
+ ### Fixed
24
+
25
+ - **A checkout under the temp dir is not scratch.** The gate treated any path under the temp dir as scratch. So the task scope, the race rule and the main-checkout rule skipped it, and a plan worktree there let a write outside its scope pass. A file inside a git checkout now meets those rules, and a plain temp file stays scratch.
26
+ - **The test suite runs on CI runners.** A fixture had no git identity for the record commit that `close` now makes. The opencode capture test read only the SQLite ledger, so it missed the rows node 20 writes to the legacy file.
27
+
28
+ ## [1.8.0] - 2026-10-13
29
+
30
+ ### Changed
31
+
32
+ - **Typed worker output and a recall hop.** Tier workers and `tezgah-tester` now end their report with an `Output to orchestrator` block. It comes after any other shape the session asks for. A worker lists changed files, the commit sha and the `ESCALATE:` reason. For each check it gives the command, the exit code and the last lines of its output file. The tester gives pass, fail and skip counts and the first failure verbatim. A report that misses a field, or an `ESCALATE:`, gets one narrower re-brief of the same role. Only a second miss goes to `tezgah-frontier`, with the original brief and a fresh start. This ships as text only. An h05 spike on omp+tezgah spawned no subagent, so the arm-bench pilot could not measure it. No default changed.
33
+ - **A route joins the workers it actually started.** `tezgah-route --report` used to drop a route when the session spawned unrouted workers of its type. It also dropped a route whose one task call started several workers. A route now owns the first task call of its type after it. One failed first check in that call fails the route. A task call more than 30 minutes after its route stays unjoined. Two routes before one shared task call stay unjoined, since the report cannot tell whose worker is whose. On this machine 6 of the 10 real omp routes now join, up from 1.
34
+ - **On Claude Code, `tezgah-task` now reads the session id.** Claude's shells now carry `TEZGAH_SESSION`, as omp's do. So the review guard counts only this session's slice records. A bare name in `slice board`, `slice done` or `slice resume` now matches only this session's or unowned records.
35
+ - **A key passed to a script as an environment prefix is no longer refused.** `API_KEY="$(cat key)" python3 tool.py > run.log` now passes, since the key reaches only the program's environment. Only a script runner given a script file qualifies. A trace or verbose option, a startup or trace variable, `/proc/*/environ` or a function defined on the line holds it. A later printer that reads a key assigned earlier on the line is now refused too. The opencode plugin answers the same.
36
+ - **One `.tezgah` per repository, in its main checkout.** A session or CLI in a linked worktree now reads and writes the main checkout's `.tezgah`. That covers the session context, research lines, decisions, lessons, taste and the design contract. Before, `git worktree remove` deleted research lines and lessons written there. A worktree's session also never saw the lessons or open plans. `tezgah-research import` moves a line left in an old worktree workspace.
37
+ - **Any `plan/NNN-*` branch answers to plan NNN.** A plan's slice worktrees on their own `plan/NNN-*` branches now meet that plan's phase and allowlist. When every phased plan is held by a worktree, the main checkout has no active task. It used to take the first held plan's scope.
38
+ - **`tezgah-task close NNN done` refuses an open Acceptance item.** It names each unticked item that carries no `UNMET: <reason>`. `--resolved` does not lift it, and `discarded` still closes freely. Close also records the `landed:` commit once main holds the work, and commits the moved record in the private `.tezgah` repository.
39
+
40
+ ### Added
41
+
42
+ - **Claude Code, Codex and Cursor routes can join their workers.** On Claude and Codex, SubagentStart writes a `subagent_start` row. It names the child's id and type, and it goes into the parent's ledger. Claude's SessionStart exports `TEZGAH_SESSION` through `CLAUDE_ENV_FILE`. On Codex, `tezgah-route` writes under the shell's `CODEX_THREAD_ID`. On Cursor it writes under `CURSOR_CONVERSATION_ID`. Cursor's subagentStart opens the child's ledger with a row that names the parent conversation. The report counts that link only after the child writes a row of its own. `cursor-agent` fires no subagentStart, so its routes stay unjoined, and so do dsh routes.
43
+ - **Two plans at once, one worktree each.** A linked worktree on a plan's own `branch:` now answers to that plan's phase, allowlist and checkpoint. This holds for a session inside the worktree and for a write into it from anywhere else. `tezgah-task start` keeps another plan's phase only when both plans run in their own worktrees on disjoint scopes. Otherwise it clears the others as before. The main checkout reads the first phased plan no worktree holds. A worktree on any other branch reads the main checkout's task. `allow` and `phase --any-path` refuse a scope that a held plan could share. The CLI takes a pending checkpoint from the plan's own worktree, and its refusal names `git -C <worktree>`. `tick`, `note`, `spike`, `review` and `close` run from any worktree. `review` records the plan worktree's HEAD, and the verification steps print `git -C <worktree> diff`.
44
+ - **`tezgah-task spike NNN "<answer>"`.** It writes `spike_recorded:` on an open plan, active or not. `phase implementation` then passes, with no stop, hand edit and restart.
45
+ - **A forced-delegation arm and 20 harder tasks for arm-bench.** `omp+tezgah-delegate` hands every edit to the worker `tezgah-route` names, so a run exercises the role text. One paid arming run verified it. `block.py` takes task globs and `--dry-run`. Tasks x01-x20 cover hidden invariants, sites a grep misses, spec edge clauses and long sweeps. On deepseek-v4-flash they passed 99 of 100 runs, so they stay as a regression corpus.
46
+ - **The Stop rule holds a done claim over an in-progress todo item.** The new `open todo` class names the open items. Finish them, mark them done, or type each one with its blocker. A step a running slice works on passes. A Cursor `merge: true` list is partial, so the class skips it.
47
+
48
+ ### Fixed
49
+
50
+ - **A route row never lands in another host's ledger.** A host started from another host's agent shell inherits that host's session id. `tezgah-route` now writes no row when its three session variables name different sessions. Such a route counts as unjoined.
51
+ - **A Codex subagent's repeat limits count only its own attempts**, as on Claude. The root's next try no longer fails for the subagent's attempts.
52
+ - **A shell line is read target by target.** Each redirect and `tee` target meets the phase, allowlist and checkpoint of the plan it lands under. Some lines also meet the task where the session runs. That holds for a line with more write shapes than read targets. It also holds for a positional writer such as `cp` or `sed -i`. So `tee <plan worktree>/a README.md` no longer escapes a reading phase. Two redirects into one plan worktree meet only that plan.
53
+ - **Two edits cannot give a plan a phase.** The gate judged an edit to an open plan by its new string alone. So `xphase: implementation` and then `xphase` -> `phase` both passed. It now judges the plan's text as it will read after the write.
54
+ - **The plan record is the record in any case and from any checkout.** The gate refuses a write to `.TEZGAH/PLANS/open/<plan>.md` as a record write. It refuses a write to any phased record from a session in a linked worktree too.
55
+ - **A slice keeps its own plan.** Its tag comes from its `plan/NNN` branch first. Next comes `plan NNN` in its brief or shared context, then its earlier tag. A re-open with no brief keeps the tag.
56
+ - **`tezgah-task close` run inside a landed worktree keeps that worktree.** The session's own directory is no longer removed from under it.
57
+ - **arm-bench's arming proof reads the evidence store.** `session_rows` and `stop_classes` read the old JSONL ledger, which the hooks no longer write. So every arm recorded 0 and `{}`, armed or not. Both now read the SQLite store, as `counters` does. A fixture copy also leaves out stale `__pycache__` and bytecode.
58
+ - **The task rule holds repository paths only.** Some writes land under no git checkout and no configured root, such as a user's `~/.config/runpod/key`. Those no longer meet the active plan's phase, allowlist or checkpoint. The secret rule and every other rule still read them. A target in another repository is still refused. Any doubt holds the write. That covers a line that moves the directory or reads `$PWD`, and a line that makes a link. It also covers a target whose parent does not exist yet and a file with a second hard link. Host agent and instruction files and shell rc files stay held too, in any letter case.
59
+ - **`tezgah-task` exits cleanly when its reader leaves early.** `tezgah-task stop | head -1` prints no traceback. It finishes its work and keeps its own exit code, buffered or under `PYTHONUNBUFFERED=1`.
60
+ - **Nothing inside `.tezgah` treats it as a project.** The workspace keeps its own `.git`, so git named `.tezgah` itself as the top level there. Session start then set up `.tezgah/.tezgah`, agents and a graph index inside it. The plans table renderer wrote `.tezgah/.tezgah/plans/README.md`. Every resolver now maps a path inside `.tezgah` to the repository around it.
61
+ - **Plan records survive concurrent writers.** `tick`, `note`, field writes and `close` hold a lock on the plans directory and replace the file atomically. A reader saw a torn record in 29 of 40 rounds before. `tick` now finds its item under the lock.
62
+ - **The slice journal closes a landed slice whose worktree `close` removed.** `reconcile` no longer names the main checkout's HEAD as a removed worktree's last commit.
63
+ - **`close` deletes a squash-merged plan branch.** It used `git branch -d`, which refuses a squash merge, and dropped the error. It now deletes with `-D` only after proving the branch landed, and names a branch it keeps.
64
+ - **Two worktrees can commit to the private `.tezgah` repository at once.** In a fixture, a busy `index.lock` failed 11 of 19 close commits. Close still exited 0. Commits there now wait out a short lock, and close exits 1 when its record commit fails.
65
+
9
66
  ## [1.7.0] - 2026-10-12
10
67
 
11
68
  ### Added
@@ -1104,7 +1161,10 @@ adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
1104
1161
  the copy could not find `hooks/` and died on import. It now links into the
1105
1162
  installed tree, and also installs the command as `tezgah`.
1106
1163
 
1107
- [Unreleased]: https://github.com/r1z4x/tezgah/compare/v1.7.0...HEAD
1164
+ [Unreleased]: https://github.com/r1z4x/tezgah/compare/v1.8.2...HEAD
1165
+ [1.8.2]: https://github.com/r1z4x/tezgah/releases/tag/v1.8.2
1166
+ [1.8.1]: https://github.com/r1z4x/tezgah/releases/tag/v1.8.1
1167
+ [1.8.0]: https://github.com/r1z4x/tezgah/releases/tag/v1.8.0
1108
1168
  [1.7.0]: https://github.com/r1z4x/tezgah/releases/tag/v1.7.0
1109
1169
  [1.6.0]: https://github.com/r1z4x/tezgah/releases/tag/v1.6.0
1110
1170
  [1.5.1]: https://github.com/r1z4x/tezgah/releases/tag/v1.5.1
package/MANIFEST CHANGED
@@ -667,6 +667,8 @@ tests/e2e_tezgah_mcp.py
667
667
  tests/fuzz_shell.py
668
668
  tests/gate_vectors.json
669
669
  tests/impacted.py
670
+ tests/install_cache.py
671
+ tests/neuter_killers.json
670
672
  tests/neuter_matrix.py
671
673
  tests/replay_context.py
672
674
  tests/routing-fixtures.md
@@ -46,7 +46,6 @@ Exit codes: 0 every record is whole (or there are none), 1 a record was refused
46
46
  import json
47
47
  import os
48
48
  import re
49
- import subprocess
50
49
  import sys
51
50
 
52
51
  HERE = os.path.dirname(os.path.dirname(os.path.realpath(__file__)))
@@ -67,19 +66,12 @@ RECORD = re.compile(r"^(\d{3})-[^/]+\.md$")
67
66
 
68
67
 
69
68
  def repo_root(path):
70
- """The repository this run is about: git's own answer, else the directory the
71
- CLI was called in - a decision recorded under a tree git does not know is
72
- still a decision. Resolved here the way bin/tezgah-task resolves its root,
73
- because the two CLIs have to agree on which repository they are talking
74
- about."""
75
- try:
76
- out = subprocess.run(["git", "-C", path, "rev-parse", "--show-toplevel"],
77
- capture_output=True, text=True)
78
- if out.returncode == 0 and out.stdout.strip():
79
- return out.stdout.strip()
80
- except OSError:
81
- pass
82
- return os.path.realpath(path)
69
+ """The repository this run is about: its main checkout, the one whose
70
+ `.tezgah` holds the records from a linked worktree or from inside `.tezgah`
71
+ too (`tezgah_task.main_repo`, the root bin/tezgah-task keeps plans under),
72
+ else the directory the CLI was called in - a decision recorded under a tree
73
+ git does not know is still a decision."""
74
+ return tt.main_repo(path) or os.path.realpath(path)
83
75
 
84
76
 
85
77
  def decision_record(text):
package/bin/tezgah-design CHANGED
@@ -30,6 +30,10 @@ import os
30
30
  import re
31
31
  import sys
32
32
 
33
+ HERE = os.path.dirname(os.path.dirname(os.path.realpath(__file__)))
34
+ sys.path.insert(0, os.path.join(HERE, "hooks"))
35
+ from tezgah_paths import linked_main, project_top # noqa: E402
36
+
33
37
  # The state set every interactive control and every data view owes: seven per
34
38
  # control, eight per data view, thirteen distinct states - `loading` and `error`
35
39
  # are the two the halves share, and the shipped lists name them twice rather than
@@ -818,10 +822,14 @@ def _print_json(payload):
818
822
 
819
823
 
820
824
  def derive_cli(opts):
821
- root = os.path.abspath(opts.get("repo") or "")
825
+ root = project_top(os.path.abspath(opts.get("repo") or ""))
822
826
  if not os.path.isdir(root):
823
827
  raise Usage("--repo must name a directory")
824
- out = opts.get("out") or os.path.join(root, ".tezgah", "design-contract.md")
828
+ # the tokens are this checkout's; the contract goes to the repository's one
829
+ # workspace, the main checkout's `.tezgah` (plan 097), which a worktree's
830
+ # removal does not take with it
831
+ ws = linked_main(root) or root
832
+ out = opts.get("out") or os.path.join(ws, ".tezgah", "design-contract.md")
825
833
  if os.path.exists(out):
826
834
  # A contract that exists is the repository's own decision: it carries
827
835
  # the component inventory someone filled in, so overwriting it would
@@ -829,7 +837,7 @@ def derive_cli(opts):
829
837
  # deliberate `rm` first.
830
838
  raise Usage("the contract already exists at %s: a contract is edited, "
831
839
  "not overwritten - remove it first or pass --out elsewhere"
832
- % os.path.relpath(out, root))
840
+ % os.path.abspath(out))
833
841
  tokens, source, sources = derive(root)
834
842
  if tokens is None:
835
843
  if opts.get("json"):
@@ -850,7 +858,7 @@ def derive_cli(opts):
850
858
  fh.write(text)
851
859
  except OSError as exc:
852
860
  raise Usage("cannot write %s: %s" % (out, exc))
853
- shown = os.path.relpath(out, root)
861
+ shown = os.path.relpath(out, ws)
854
862
  if opts.get("json"):
855
863
  _print_json({"ok": True, "out": shown, "source": source,
856
864
  "colors": len(data["tokens"]["colors"]),
@@ -858,9 +866,11 @@ def derive_cli(opts):
858
866
  "type_scale": data["tokens"]["type_scale"],
859
867
  "components": len(data["components"])})
860
868
  else:
869
+ # absolute: from a worktree the contract is the main checkout's, which a
870
+ # `.tezgah/...` read from the worktree's cwd does not name (review N1)
861
871
  sys.stdout.write(
862
872
  "wrote %s (source: %s, %d colour roles, %s, %d type steps)\n"
863
- % (shown, source, len(data["tokens"]["colors"]),
873
+ % (os.path.abspath(out), source, len(data["tokens"]["colors"]),
864
874
  "%spx unit" % data["tokens"]["space_unit"]
865
875
  if data["tokens"]["space_unit"] else "no spacing unit found",
866
876
  len(data["tokens"]["type_scale"])))
@@ -876,6 +886,12 @@ def check_cli(opts):
876
886
  measured = opts.get("measured")
877
887
  if not contract or not measured:
878
888
  raise Usage("check wants --contract FILE and --measured FILE|-")
889
+ if (not os.path.isabs(contract) and not os.path.exists(contract)
890
+ and contract.replace(os.sep, "/").startswith(".tezgah/")):
891
+ # a worktree's `.tezgah/...` is the main checkout's workspace (plan 097,
892
+ # review N1): the path the Stop refusal and the reviewer prompt name
893
+ cwd = os.getcwd()
894
+ contract = os.path.join(linked_main(cwd) or cwd, contract)
879
895
  data, reason = read_contract(contract)
880
896
  if data is None:
881
897
  raise Usage(reason)
@@ -112,7 +112,10 @@ def merged(lines, groups):
112
112
 
113
113
  def main(argv):
114
114
  ap = argparse.ArgumentParser(prog="tezgah-lessons", description=__doc__.split("\n")[0])
115
- ap.add_argument("--root", default=os.getcwd())
115
+ # a linked worktree's lessons are the main checkout's (plan 097)
116
+ from tezgah_paths import linked_main
117
+ here = tc.repo_root(os.getcwd())
118
+ ap.add_argument("--root", default=linked_main(here) or here)
116
119
  ap.add_argument("--fixture")
117
120
  args = ap.parse_args(argv)
118
121
  lines = tezgah_lessons.lines(args.root)
@@ -131,20 +131,27 @@ import tezgah_product as tp_product # noqa: E402
131
131
  import tezgah_research as tr # noqa: E402
132
132
 
133
133
 
134
- def repo_root(path):
134
+ def checkout_root(path):
135
+ """The checkout `path` is in: git's top level, the project around it inside
136
+ `.tezgah`, else the directory itself."""
135
137
  try:
136
138
  out = subprocess.run(["git", "-C", path, "rev-parse", "--show-toplevel"],
137
139
  capture_output=True, text=True)
138
140
  if out.returncode == 0 and out.stdout.strip():
139
- top = out.stdout.strip()
140
- # inside `.tezgah` git answers with the private repository; the line
141
- # belongs to the project around it
142
- return os.path.dirname(top) if os.path.basename(top) == tp.WORKSPACE else top
141
+ return tp.project_top(out.stdout.strip())
143
142
  except OSError:
144
143
  pass
145
144
  return os.path.realpath(path)
146
145
 
147
146
 
147
+ def repo_root(path):
148
+ """The checkout whose `.tezgah` holds the lines: the main checkout, from a
149
+ linked worktree too, as for bin/tezgah-task - a line written into a
150
+ worktree's own `.tezgah` went with `git worktree remove` (plan 097)."""
151
+ top = checkout_root(path)
152
+ return tp.linked_main(top) or top
153
+
154
+
148
155
  def misuse(message):
149
156
  """Misuse exits 2, so a caller can tell it from a line that fails the checks."""
150
157
  print("tezgah-research: %s" % message, file=sys.stderr)
@@ -355,7 +362,7 @@ def cmd_check(args):
355
362
  report = {}
356
363
  if known:
357
364
  report = tr.check(repo, rest[0] if rest else None, strict=strict,
358
- all_lines=all_lines)
365
+ all_lines=all_lines, code=checkout_root(os.getcwd()))
359
366
  for slug, row in report.items():
360
367
  for err in row["errors"]:
361
368
  problems += 1
@@ -1004,7 +1011,8 @@ def cmd_import(args):
1004
1011
  def cmd_all(args):
1005
1012
  if args:
1006
1013
  return misuse("--all takes no arguments")
1007
- for row in tr.across(repo_root(os.getcwd())):
1014
+ # every checkout's own `.tezgah`, marking the one this runs in
1015
+ for row in tr.across(checkout_root(os.getcwd())):
1008
1016
  print(row)
1009
1017
  return 0
1010
1018
 
package/bin/tezgah-route CHANGED
@@ -26,7 +26,10 @@ model through TypeSafe, OpenRouter System One, Cloudflare Workers AI or a
26
26
  compatible endpoint (TEZGAH_JEV_PROVIDER picks one), else the OpenRouter chat
27
27
  fallback - so write it in English, the language the judge reads best.
28
28
 
29
- Every routed call writes one `route` row to the session's ledger (TEZGAH_SESSION):
29
+ Every routed call writes one `route` row to the session's ledger, named by
30
+ TEZGAH_SESSION (omp, opencode, Claude), CODEX_THREAD_ID (Codex) or
31
+ CURSOR_CONVERSATION_ID (Cursor); when two of them name different sessions (a host
32
+ started from another host's shell) it writes none. The row holds the
30
33
  tier, agent, what set the tier (`via`: rule|jev|phase|default), the static
31
34
  table's tier when --phase was given, the worker's model, and `override` (user: an
32
35
  omp entry the user owns; mode: no entry, so the session model). `--report` joins
@@ -36,8 +39,9 @@ order only where no spawn could be swapped - and counts a route it cannot prove
36
39
  as `unjoined`. That outcome is what the V2-as-default
37
40
  decision waits for (docs/models.md).
38
41
 
39
- A worker that answers `ESCALATE:` is restarted on tezgah-frontier with the same
40
- brief; its trajectory is not continued. Exit codes: 0 routed / current, 1 the
42
+ A report missing an output field, or an `ESCALATE:`, gets one narrower re-brief
43
+ of the same role naming that field or judgement; only a second miss is restarted
44
+ on tezgah-frontier with the original brief, never continued. Exit codes: 0 routed / current, 1 the
41
45
  table is stale or flagged (`--check`) or the refresh failed, 2 misuse.
42
46
  """
43
47
  import argparse
@@ -58,8 +62,10 @@ def main(argv):
58
62
  parser = argparse.ArgumentParser(
59
63
  prog="tezgah-route",
60
64
  description="Name the tier worker a delegated task goes to, and keep the model table.",
61
- epilog="A worker that answers `ESCALATE:` is restarted on tezgah-frontier with "
62
- "the same brief; its trajectory is not continued.")
65
+ epilog="A report missing an output field, or an `ESCALATE:`, gets one "
66
+ "narrower re-brief of the same role naming that field or judgement; "
67
+ "only a second miss is restarted on tezgah-frontier with the original "
68
+ "brief, never continued.")
63
69
  parser.add_argument("brief", nargs="*", help="the delegation brief, in English")
64
70
  parser.add_argument("--phase", choices=sorted(tm.PHASE_TIER),
65
71
  help="the static table's row when no judgement is available")
@@ -154,6 +160,16 @@ def main(argv):
154
160
  ask = tezgah_judge.ask if tezgah_judge.available() else None
155
161
  out = tm.route(brief, args.phase, ask)
156
162
  judged = out.pop("judged")
163
+ # The session the row goes under: omp, opencode and Claude (CLAUDE_ENV_FILE)
164
+ # export TEZGAH_SESSION, a Codex shell carries CODEX_THREAD_ID (the root
165
+ # thread its hooks' session_id names), a Cursor agent shell
166
+ # CURSOR_CONVERSATION_ID (its hooks' conversation_id). A host started from
167
+ # another host's agent shell inherits that host's id and markers alike, so
168
+ # two different ids name no host: no row, and the route is never written
169
+ # into another host's ledger.
170
+ ids = {os.environ.get(v) for v in ("TEZGAH_SESSION", "CODEX_THREAD_ID",
171
+ "CURSOR_CONVERSATION_ID")} - {None, ""}
172
+ session = ids.pop() if len(ids) == 1 else None
157
173
  if judged:
158
174
  usage = judged.get("usage")
159
175
  usage = usage if isinstance(usage, dict) else {}
@@ -161,8 +177,7 @@ def main(argv):
161
177
  judged.get("model", tezgah_judge.MODEL), out["tier"],
162
178
  usage.get("input_tokens"), usage.get("output_tokens"),
163
179
  judged.get("latency_ms"))
164
- ti.note(os.environ.get("TEZGAH_SESSION"), "judge", detail)
165
- session = os.environ.get("TEZGAH_SESSION")
180
+ ti.note(session, "judge", detail)
166
181
  if session: # the omp read in worker_model costs a process; skip it with no ledger
167
182
  ti.note(session, "route", tm.route_detail(out, args.phase,
168
183
  *tm.worker_model(out["agent"])))