@r1z4x/tezgah 1.8.2 → 1.9.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.
@@ -3483,8 +3483,9 @@ def note_tool(session_id, tool, inp, failed=None, *, interrupted=False,
3483
3483
  # so the call's outcome is the slice's: Claude/dsh/Cursor `Agent`/`Task`. omp's
3484
3484
  # `task` and Codex `spawn_agent` return while the child still runs (a Codex
3485
3485
  # rollout shows the call answered with its `task_name` alone), so their records
3486
- # stay open: omp's for `tezgah_task.pending` to close from the child
3487
- # transcripts, Codex's until `pending` closes them `stale`.
3486
+ # stay open: omp's for `tezgah_task.pending` to close from the child transcripts,
3487
+ # and Codex's until `pending` closes them `stopped` with no failure class - an
3488
+ # end this machine cannot see is owed work, not a `stale` record (plan 108).
3488
3489
  SYNC_SPAWN_HOSTS = frozenset(("claude", "dsh", "cursor"))
3489
3490
 
3490
3491
 
@@ -4493,18 +4494,6 @@ def stop_reason(text, session_id, cwd=None, after_block=0,
4493
4494
  detail, kind = "ok", "claim"
4494
4495
  else:
4495
4496
  return None
4496
- # The temporal spec's verdict, in shadow (plan 063): recorded beside this
4497
- # one and never read back, and only for a reply that leaves a claim or a
4498
- # refusal row - the population its agreement bar is read on - so a quiet
4499
- # allowed reply pays nothing. Its own guard, because the host wraps this
4500
- # whole function in `safe()` and an escaping exception would lose the refusal.
4501
- if not record_only:
4502
- try:
4503
- import tezgah_stopspec
4504
- tezgah_stopspec.shadow(cls, text, session_id, rows, cwd, True, key)
4505
- except Exception:
4506
- if os.environ.get("TEZGAH_STOPSPEC_STRICT"):
4507
- raise
4508
4497
  # A refusal is skipped only as the same verdict on the same reply: the same
4509
4498
  # reply text refused for a new class is the turn's next refusal, and
4510
4499
  # `_record_only` counts refusals by these rows. A row bound for a parent's
@@ -4879,15 +4868,14 @@ def _stop_block(text, session_id, rows=None, cwd=None, shape=True, fold=None):
4879
4868
  scoped the way `_partial_state`'s already was, and the Stop path still reads
4880
4869
  the file once per turn.
4881
4870
 
4882
- `fold` is the evidence fold the selector below calls, `_evidence_block` by
4883
- default; `tezgah_stopspec.judge` passes the temporal spec's. Under
4884
- `TEZGAH_STOPSPEC_STRICT` (tests) the default call also asks the spec and
4885
- raises on a disagreement."""
4871
+ `fold` is the evidence fold the selector below calls. None, the default,
4872
+ is the temporal spec's (`tezgah_stopspec.fold`: the class from its formulas,
4873
+ the text from `_evidence_text`), and that outer call fills the text's
4874
+ placeholders once (`_said`)."""
4886
4875
  if fold is None:
4887
- verdict = _stop_block(text, session_id, rows, cwd, shape, _evidence_block)
4888
- if os.environ.get("TEZGAH_STOPSPEC_STRICT"):
4889
- import tezgah_stopspec
4890
- tezgah_stopspec.check(verdict[0], text, session_id, rows, cwd, shape)
4876
+ # imported here: tezgah_stopspec imports this module at its top
4877
+ import tezgah_stopspec
4878
+ verdict = _stop_block(text, session_id, rows, cwd, shape, tezgah_stopspec.fold)
4891
4879
  return (verdict[0], verdict[1] and _said(verdict[1]))
4892
4880
  t = str(text or "")
4893
4881
  shaped = _shape_block(t, cwd) if shape else (None, None)
@@ -4957,11 +4945,19 @@ def _stop_block(text, session_id, rows=None, cwd=None, shape=True, fold=None):
4957
4945
  return _residual_block(t, rows, shape,
4958
4946
  fold(rows, worked | {BEGAN_KIND}, external),
4959
4947
  session_id, cwd)
4948
+ # A check that answered with an outcome nobody saw (exit 0 and nothing
4949
+ # returned, a status a pipe owns: not `passing_check`) is no pass, and as a
4950
+ # check it is no work row either. A done or tested claim resting on it owes
4951
+ # the `no verify_ok` floor a lost check owes (plan 101: a work-free turn of
4952
+ # one such row let "Tamamlandı, tüm testler geçti" end), so it is read
4953
+ # before the settled exit below, which an earlier pass would otherwise open.
4954
+ unseen = (done or verified) and any(
4955
+ r.get("kind") == "verify_ok" and not passing_check(r) for r in rows)
4960
4956
  # Two shapes the turn's own rows cannot judge, so they are judged against
4961
4957
  # the session's (the contract says "no successful check recorded in the
4962
4958
  # session"). Read only for these two, so every other turn still pays for
4963
4959
  # its own rows alone. Both ask the same fold
4964
- # this turn is judged by (`_evidence_block`), over the session's rows BEFORE
4960
+ # this turn is judged by (`fold`, the spec's), over the session's rows BEFORE
4965
4961
  # this turn - every class it has, the UI/design and partial-failure ones
4966
4962
  # included, so a state the earlier turns left refused stays refused (review
4967
4963
  # F5). The current turn is left out because it holds no work of its own
@@ -4978,7 +4974,7 @@ def _stop_block(text, session_id, rows=None, cwd=None, shape=True, fold=None):
4978
4974
  # failed check, a command this cannot read as bookkeeping - keeps
4979
4975
  # the turn fold below, which asks for a fresh check.
4980
4976
  srows = events(session_id)
4981
- if not external and _settled(srows):
4977
+ if not external and not unseen and _settled(srows):
4982
4978
  return _residual_block(t, rows, shape, fold(
4983
4979
  _before_turn(srows), True, None, "this session"),
4984
4980
  session_id, cwd)
@@ -4999,6 +4995,8 @@ def _stop_block(text, session_id, rows=None, cwd=None, shape=True, fold=None):
4999
4995
  # turn can prove
5000
4996
  judged, worked = [r for r in rows if not _work_row(r) or (
5001
4997
  external and _external_read_row([r]) == 0)], set()
4998
+ if unseen:
4999
+ worked = worked | {"verify_ok"}
5002
5000
  return _residual_block(t, rows, shape, fold(judged, worked, external),
5003
5001
  session_id, cwd)
5004
5002
 
@@ -5028,21 +5026,18 @@ def _before_turn(rows):
5028
5026
  return out
5029
5027
 
5030
5028
 
5031
- def _evidence_block(rows, worked, external, where="this turn"):
5032
- """The evidence half of `_stop_block` over `rows`: (class, text) or (None,
5033
- None). `where` names the span the rows cover in the refusal text."""
5034
- # the newest check decides: "the tests pass" is false when a later run
5035
- # failed, even though an earlier one succeeded
5036
- if _last_verify(rows) == "fail":
5029
+ def _evidence_text(cls, rows, external, where="this turn"):
5030
+ """(class, block text) for the evidence class the temporal spec picked over
5031
+ `rows` (`tezgah_stopspec.fold`), or (None, None) for none. The spec decides
5032
+ the class; this words it, reading the rows only for what the text names.
5033
+ `where` names the span the rows cover."""
5034
+ if cls == "check failed":
5037
5035
  return ("check failed",
5038
5036
  "A check failed in %s and the reply claims success. "
5039
5037
  "Report the failure with its exact error line, or fix it and "
5040
5038
  "re-run; do not describe a failed check as passing. %s."
5041
5039
  % (where, ADMISSION))
5042
- # a failure the turn never resolved: the newest check is not a pass, so an
5043
- # earlier green run over a different command does not license the claim
5044
- state = _partial_state(rows)
5045
- if state["failed"] and not state["verified"]:
5040
+ if cls == "partial failure":
5046
5041
  return ("partial failure",
5047
5042
  "Partial failure: %s failed %s and no check has passed since, so "
5048
5043
  "an earlier green run does not cover it. Report that failure "
@@ -5050,89 +5045,44 @@ def _evidence_block(rows, worked, external, where="this turn"):
5050
5045
  "not describe a partial result as done. If you are deliberately "
5051
5046
  "stopping on the failure, say what is still broken; %s."
5052
5047
  % (_failed_check(rows),
5053
- "after %s's edits" % where if state["edited"] else "in " + where,
5054
- ADMISSION))
5055
- # A passing check licenses the claim only when it is newer than the newest
5056
- # write the gate saw change the tree: a green run over the previous revision
5057
- # is not evidence about this one, and the ledger already carries both sides
5058
- # (the check's position, and `changed` on the write). The admission is the
5059
- # reply's own "doğrulanmadı" after a check that failed since that write
5060
- # (`_admitted`). The pass has to be one
5061
- # run in the newest change's own repository (`_last_pass`'s `repo`).
5062
- last_change = _last_change(rows)
5063
- last_pass = _last_pass(rows, _change_repo(rows, last_change))
5064
- # The UI half, read before the generic freshness pair because it is a stricter
5065
- # question about the same rows: a proof of the screen - a check that renders,
5066
- # or a read of the rendered screen - stands where a unit pass stands, and only
5067
- # when it is newer than the write it is a proof of. A green unit run is not
5068
- # that proof, which is the whole point: it never sees what a person sees.
5069
- # The write it is a proof of is the newest UI write, not the newest write of
5070
- # anything: a later edit to a `.py` file does not unmake a screen read, and
5071
- # reading the fold that way refused honest turns and named a screen check the
5072
- # turn had in fact run.
5073
- ui_write, ui_proof = _ui_evidence(rows)
5074
- if ui_write >= 0 and ui_proof <= ui_write:
5048
+ "after %s's edits" % where if _partial_state(rows)["edited"]
5049
+ else "in " + where, ADMISSION))
5050
+ if cls in ("no ui_ok", "stale evidence"):
5075
5051
  names = _stale_paths(rows)
5076
5052
  shown = ", ".join(names[:3]) + (" (+%d more)" % (len(names) - 3)
5077
5053
  if len(names) > 3 else "")
5078
- return ("no ui_ok",
5079
- "UI evidence: %s changed a UI source (%s) and the check "
5080
- "that passed was not one that sees the screen - a unit run "
5081
- "never does. Run the browser/e2e or visual check and report "
5082
- "its output, or read the rendered screen (`analyze-app`: the "
5083
- "accessibility/DOM tree, a screenshot at the widths in scope) "
5084
- "and say what it showed. A green unit suite does not cover "
5085
- "what a person sees, and \"{UNVERIFIED}\" does not stand in for "
5086
- "the screen; if nothing can render it here, write "
5087
- "\"{UNVERIFIED}\" and name it under \"{BLOCKERS_HEADING}\" as {ENV} "
5088
- "with the command that failed."
5089
- % (where, shown or "a UI file"))
5090
- if ui_write >= 0:
5091
- # The floor, asked of a component turn on top of the screen proof: a
5092
- # screenshot or a rendered check says what the thing looks like and
5093
- # nothing about whether it is on the repository's own contract. A screen
5094
- # a person looks at is unchanged - it is the component that owes this,
5095
- # and the check has to be newer than the component write it judges.
5096
- component, design = _design_evidence(rows)
5097
- if component >= 0 and design <= component:
5098
- names = _stale_paths(rows)
5099
- shown = ", ".join(names[:3]) + (" (+%d more)" % (len(names) - 3)
5100
- if len(names) > 3 else "")
5054
+ if cls == "no ui_ok":
5055
+ # the screen half is asked first, so a turn that owes both reads the
5056
+ # screen's text; the design floor is the component's (`_design_evidence`)
5057
+ ui_write, ui_proof = _ui_evidence(rows)
5058
+ if ui_write >= 0 and ui_proof <= ui_write:
5101
5059
  return ("no ui_ok",
5102
- "Design contract: %s changed a component (%s) and no "
5103
- "`tezgah-design check` ran over a measurement, so nothing "
5104
- "says the component is on the repository's floor - a read of "
5105
- "the screen says what it looks like, not whether it is on "
5106
- "that floor. Run `tezgah-design check --contract "
5107
- ".tezgah/design-contract.md --measured <measurement.json>` "
5108
- "(`tezgah-design derive` writes the contract when the repo "
5109
- "has none; the measurement is the per-component styles and "
5110
- "states the analyze-app loop produces) and report every "
5111
- "violation it prints; if the check cannot run here, write "
5060
+ "UI evidence: %s changed a UI source (%s) and the check "
5061
+ "that passed was not one that sees the screen - a unit run "
5062
+ "never does. Run the browser/e2e or visual check and report "
5063
+ "its output, or read the rendered screen (`analyze-app`: the "
5064
+ "accessibility/DOM tree, a screenshot at the widths in scope) "
5065
+ "and say what it showed. A green unit suite does not cover "
5066
+ "what a person sees, and \"{UNVERIFIED}\" does not stand in for "
5067
+ "the screen; if nothing can render it here, write "
5112
5068
  "\"{UNVERIFIED}\" and name it under \"{BLOCKERS_HEADING}\" as {ENV} "
5113
5069
  "with the command that failed."
5114
- % (where, shown or "a component file"))
5115
- # A screen proof stands where a unit pass stands for the rest of this fold,
5116
- # so a UI turn whose UI work is proven is not asked for a check it already
5117
- # has - and one whose UI work is not proven was refused above, so an
5118
- # unrelated write it left unverified is still the generic question below.
5119
- # The read of the authoritative source stands where a passing check stands,
5120
- # for the one claim it is evidence about (`external`): a reply that states
5121
- # the state of a registry, a release or a CI run and carried the read that
5122
- # answers it has settled that half of itself. It is folded into the proof
5123
- # only for that claim - an `npm view` run is not evidence about the tree, and
5124
- # letting it stand for a turn's own work would excuse every unverified edit
5125
- # beside it.
5126
- ext_read = _external_read_row(rows) if external else -1
5127
- unread = external is not None and ext_read < 0
5128
- proof = max(last_pass, ui_proof, ext_read)
5129
- if proof > last_change:
5130
- if not unread:
5131
- return (None, None)
5132
- elif last_pass >= 0:
5133
- names = _stale_paths(rows)
5134
- shown = ", ".join(names[:3]) + (" (+%d more)" % (len(names) - 3)
5135
- if len(names) > 3 else "")
5070
+ % (where, shown or "a UI file"))
5071
+ return ("no ui_ok",
5072
+ "Design contract: %s changed a component (%s) and no "
5073
+ "`tezgah-design check` ran over a measurement, so nothing "
5074
+ "says the component is on the repository's floor - a read of "
5075
+ "the screen says what it looks like, not whether it is on "
5076
+ "that floor. Run `tezgah-design check --contract "
5077
+ ".tezgah/design-contract.md --measured <measurement.json>` "
5078
+ "(`tezgah-design derive` writes the contract when the repo "
5079
+ "has none; the measurement is the per-component styles and "
5080
+ "states the analyze-app loop produces) and report every "
5081
+ "violation it prints; if the check cannot run here, write "
5082
+ "\"{UNVERIFIED}\" and name it under \"{BLOCKERS_HEADING}\" as {ENV} "
5083
+ "with the command that failed."
5084
+ % (where, shown or "a component file"))
5085
+ if cls == "stale evidence":
5136
5086
  return ("stale evidence",
5137
5087
  "Stale evidence: the newest check that passed ran before %s %s "
5138
5088
  "written, so it verified an earlier revision of the tree than the "
@@ -5142,18 +5092,19 @@ def _evidence_block(rows, worked, external, where="this turn"):
5142
5092
  "A green run over the previous revision does not cover this one."
5143
5093
  % (shown or "a file this session wrote",
5144
5094
  "was" if len(names) <= 1 else "were", ADMISSION))
5145
- elif worked and _last_pass(rows) > last_change:
5146
- # a pass newer than the change exists, only in another repository: the
5147
- # class stays (ledger continuity), the text names the real reason
5148
- return ("no verify_ok",
5149
- "%s changed %s, and the check that passed after it ran in "
5150
- "another repository (%s), so it says nothing about this one. "
5151
- "Run the check in the repository you changed and report its "
5152
- "output; %s."
5153
- % (where.capitalize(), _change_repo(rows, last_change),
5154
- rows[max(i for i, r in enumerate(rows)
5155
- if passing_check(r))].get("repo"), ADMISSION))
5156
- elif worked:
5095
+ if cls == "no verify_ok":
5096
+ last_change = _last_change(rows)
5097
+ if _last_pass(rows) > last_change:
5098
+ # a pass newer than the change exists, only in another repository:
5099
+ # the class stays (ledger continuity), the text names the real reason
5100
+ return ("no verify_ok",
5101
+ "%s changed %s, and the check that passed after it ran in "
5102
+ "another repository (%s), so it says nothing about this one. "
5103
+ "Run the check in the repository you changed and report its "
5104
+ "output; %s."
5105
+ % (where.capitalize(), _change_repo(rows, last_change),
5106
+ rows[max(i for i, r in enumerate(rows)
5107
+ if passing_check(r))].get("repo"), ADMISSION))
5157
5108
  return ("no verify_ok",
5158
5109
  "%s did work (edits or commands) and no check ran "
5159
5110
  "successfully in it (nothing recorded as verify_ok with a "
@@ -5164,11 +5115,7 @@ def _evidence_block(rows, worked, external, where="this turn"):
5164
5115
  "%s. Do not "
5165
5116
  "describe a check you did not run as if it ran."
5166
5117
  % (where.capitalize(), ADMISSION))
5167
- # Last, so none of the classes above loses its turn to it: every shape this
5168
- # one refuses is a shape the fold was about to allow. That is the whole
5169
- # ordering rule of this function - the branch is asked only where the answer
5170
- # would otherwise be "this turn may end".
5171
- if unread:
5118
+ if cls == "no external read":
5172
5119
  return ("no external read", _external_text(external, where))
5173
5120
  return (None, None)
5174
5121
 
@@ -5392,8 +5339,7 @@ def _admitted(fold, text):
5392
5339
  is the check that ran and failed (`_proof_attempt_failed`), and an external
5393
5340
  claim with no read of its system still owes the read (`no external read`) -
5394
5341
  otherwise a failed unit check would end a turn its passing twin cannot.
5395
- Wrapped around whichever fold the selector was given, so the live rule and
5396
- the temporal spec read the admission the same way."""
5342
+ Wrapped around whichever fold the selector was given."""
5397
5343
  if not NEGATED.search(_prose(text)):
5398
5344
  return fold
5399
5345
 
@@ -5882,6 +5828,9 @@ def _now_closed(item, label):
5882
5828
  QUESTION_END = re.compile(r"(?:\?|\b(?:%s)[.!]?)$" % alts("question_end"))
5883
5829
  PROCESS_ASK = re.compile(alts("ask", "process"), re.I)
5884
5830
  NEVER_DEFAULT = re.compile(alts("ask", "never_default"), re.I)
5831
+ # A reply that names a plan it wrote, or asks to build one: building a plan the
5832
+ # turn wrote is class F (plan 105), so asking it is the policy, not a needless ask
5833
+ PLAN_BUILD = re.compile(alts("ask", "plan_build"), re.I)
5885
5834
  SPEND = re.compile(alts("ask", "spend"), re.I)
5886
5835
  AMOUNT = re.compile(r"\$\s?(\d+(?:\.\d+)?)|(\d+(?:\.\d+)?)\s?(?:\$|usd\b|dolar)", re.I)
5887
5836
  DEFAULT_MARK = re.compile(alts("ask", "default_mark"), re.I)
@@ -5902,7 +5851,8 @@ NEEDLESS_ASK = (
5902
5851
  "merge, a run parameter within the standing spend cap - are taken and "
5903
5852
  "stated in one line, and an outward step the user granted this session is "
5904
5853
  "taken, not asked again. An outward step the user has not granted waits "
5905
- "for the answer and never proceeds on a default. Do it now, or, when only "
5854
+ "for the answer and never proceeds on a default, and so does building a "
5855
+ "plan this turn wrote. Do it now, or, when only "
5906
5856
  "the user can decide it, put it under a \"{APPROVAL_HEADING}\" heading with "
5907
5857
  "a \"{RECOMMENDED}:\" default and proceed on that default.")
5908
5858
 
@@ -5963,7 +5913,8 @@ def needless_ask(text, granted=frozenset):
5963
5913
  if not lines or not QUESTION_END.search(lines[-1]):
5964
5914
  return None
5965
5915
  last = lines[-1]
5966
- if never_default(last) or DEFAULT_MARK.search(_tr_lower(re.split(r"\n\s*\n", t)[-1])):
5916
+ if (never_default(last) or PLAN_BUILD.search(_tr_lower(t))
5917
+ or DEFAULT_MARK.search(_tr_lower(re.split(r"\n\s*\n", t)[-1]))):
5967
5918
  return None
5968
5919
  low = _tr_lower(last)
5969
5920
  outward = outward_classes(low)
@@ -6180,8 +6131,13 @@ def ready_left(text, rows, session_id, cwd=None):
6180
6131
  def open_left(text, row, session_id=None, cwd=None):
6181
6132
  """The heads of the in-progress items a done claim leaves (the `open todo`
6182
6133
  class): the row's `open - blocked - ready` count, named by its `taken`
6183
- heads, when the reply claims done (`asserted_claims`). A blocked item is
6184
- not counted, an in-progress one a running slice of this session works on
6134
+ heads, when the reply claims done (`asserted_claims`); a turn that did work
6135
+ and leaves an in-progress item without a claim is not judged here, because a
6136
+ session waiting on a running suite and a session that stopped are the same
6137
+ row - the `ready steps` class covers the ready ones (tests/test_integrity.py
6138
+ StopHook.test_a_done_claim_over_an_in_progress_todo_is_refused_once, plan
6139
+ 097). A blocked item is not counted, an in-progress one a running slice of
6140
+ this session works on
6185
6141
  (`_not_running`: Claude's TodoWrite has no blocked status, so a dispatched
6186
6142
  step reads in_progress) is not either, and one the reply types with its
6187
6143
  blocker (`_untyped`) passes. A `partial` row (Cursor `merge: true`) holds
@@ -55,7 +55,7 @@ READ_ON = "2026-09-30"
55
55
  STALE_DAYS = 60
56
56
  OVERLAY = os.path.join(tp.CONFIG_DIR, "models.json")
57
57
  OPENROUTER_MODELS = "https://openrouter.ai/api/v1/models"
58
- MODES = ("auto", "anthropic", "zai", "any", "off")
58
+ MODES = ("auto", "anthropic", "zai", "any", "openai", "off")
59
59
  # The two status markers `bin/tezgah-setup` classifies an `apply_omp()` line by:
60
60
  # a skip is not a write and not a failure - the user's omp is left untouched, and
61
61
  # nothing is left to repair, which is why the status report excuses it rather
@@ -105,6 +105,13 @@ SLOTS = {
105
105
  "zai": ("zai/glm-5.3", None),
106
106
  "any": ("anthropic/claude-opus-5.5", "high")},
107
107
  }
108
+ # omp's own OpenAI column (plan 107): omp reaches OpenAI through its
109
+ # `openai-codex` provider (a ChatGPT login), which on 2026-10-13 listed
110
+ # gpt-5.6-luna, gpt-5.6-terra and gpt-6-luna and none of SLOTS' `openai` ids,
111
+ # which stay the Codex host's. One model, effort as the lever, as the
112
+ # Anthropic column does it.
113
+ OMP_OPENAI = {"cheap": ("gpt-6-luna", "low"), "explore": ("gpt-6-luna", "medium"),
114
+ "standard": ("gpt-6-luna", "medium"), "frontier": ("gpt-6-luna", "high")}
108
115
  AGENT_SLOT = {"tezgah-cheap": "cheap", "tezgah-standard": "standard",
109
116
  "tezgah-frontier": "frontier", "tezgah-reviewer": "frontier",
110
117
  "tezgah-tester": "cheap", "tezgah-docs": "standard",
@@ -209,7 +216,12 @@ OMP_FAMILIES = ("anthropic", "zai", "any")
209
216
  # (omp://providers.md:39, omp://environment-variables.md:39-40,71,73).
210
217
  OMP_AUTH = {"anthropic": ("anthropic", ("ANTHROPIC_OAUTH_TOKEN", "ANTHROPIC_API_KEY")),
211
218
  "zai": ("zai", ("ZAI_API_KEY",)),
212
- "any": ("openrouter", ("OPENROUTER_API_KEY",))}
219
+ "any": ("openrouter", ("OPENROUTER_API_KEY",)),
220
+ "openai": ("openai-codex", ("OPENAI_CODEX_OAUTH_TOKEN",))}
221
+ # Every family `--mode` can pin. `openai` is no fallback for the others: it is
222
+ # its own subscription with its own limit, and the owner's fallback ask
223
+ # (2026-10-11) named zai, so it is reached only when named.
224
+ MODE_FAMILIES = OMP_FAMILIES + ("openai",)
213
225
 
214
226
 
215
227
  def _omp_stored_providers():
@@ -413,7 +425,7 @@ def other_family_up(text):
413
425
  if os.environ.get("TEZGAH_HEALTH_CHECK") == "0":
414
426
  return None
415
427
  mode = omp_mode()
416
- if mode not in OMP_FAMILIES:
428
+ if mode not in MODE_FAMILIES:
417
429
  return None
418
430
  funded = funded_families(live=True)
419
431
  chained = {sel for slot in SLOTS for sel in _chain(mode, slot, funded)}
@@ -561,17 +573,17 @@ def start_health():
561
573
 
562
574
 
563
575
  def omp_mode(default_selector=None):
564
- """`anthropic`, `zai` or `any`: the saved mode, else the default's own family.
576
+ """`anthropic`, `zai`, `openai` or `any`: the saved mode, else the
577
+ default's own family.
565
578
 
566
- A non-Anthropic default reads as `any`, never as a silent fall back to the
567
- Anthropic column - the caller decides what to do when the OpenRouter
568
- credential the `any` selectors need is missing (`apply_omp` writes nothing
569
- and says so)."""
579
+ A default of no listed family reads as `any`, never as a silent fall back
580
+ to the Anthropic column - the caller decides what to do when the credential
581
+ the selectors need is missing (`apply_omp` writes nothing and says so)."""
570
582
  mode = overlay().get("mode")
571
- if mode in ("anthropic", "zai", "any", "off"):
583
+ if mode in ("anthropic", "zai", "any", "openai", "off"):
572
584
  return mode
573
- for family in ("anthropic", "zai"):
574
- if str(default_selector or "").startswith(family + "/"):
585
+ for family in ("anthropic", "zai", "openai"):
586
+ if str(default_selector or "").startswith(FAMILY_PREFIX[family] or family + "/"):
575
587
  return family
576
588
  return "any"
577
589
 
@@ -585,15 +597,22 @@ OMP_ROLES = ("plan", "slow")
585
597
 
586
598
  # The prefix each family's stored id needs to become an omp selector. `zai` needs
587
599
  # none: its id already is `<provider>/<model>`, the shape omp's own config uses.
588
- FAMILY_PREFIX = {"anthropic": "anthropic/", "zai": "", "any": "openrouter/"}
600
+ FAMILY_PREFIX = {"anthropic": "anthropic/", "zai": "", "any": "openrouter/",
601
+ "openai": "openai-codex/"}
602
+
603
+
604
+ def _row(family, slot):
605
+ """(model, effort) of one slot on one family: omp's own OpenAI column for
606
+ `openai`, else the table's."""
607
+ return OMP_OPENAI[slot] if family == "openai" else SLOTS[slot][family]
589
608
 
590
609
 
591
610
  def _selector(family, slot):
592
611
  """`<provider>/<model>[:<effort>]` for one slot on one family, the shape omp
593
612
  takes for a model override and for a role alike (omp://models.md:492-505):
594
613
  the Anthropic column names the host's own id, `zai` a z.ai id, `any` an
595
- OpenRouter one."""
596
- model, effort = SLOTS[slot][family]
614
+ OpenRouter one, `openai` an `openai-codex` one."""
615
+ model, effort = _row(family, slot)
597
616
  return FAMILY_PREFIX[family] + model + (":" + effort if effort else "")
598
617
 
599
618
 
@@ -635,7 +654,7 @@ def _chain(mode, slot, funded, agent=""):
635
654
  if policy == "none":
636
655
  return [_selector(mode, slot)]
637
656
  if policy == "vendor":
638
- model, effort = SLOTS[slot][mode]
657
+ model, effort = _row(mode, slot)
639
658
  out = [_selector(mode, slot)]
640
659
  for family in families[1:]:
641
660
  if _vendor(SLOTS[slot][family][0]) == _vendor(model):
@@ -652,8 +671,15 @@ def _chain(mode, slot, funded, agent=""):
652
671
  return [_selector(family, slot) for family in families]
653
672
 
654
673
 
674
+ def codex_ready():
675
+ """True when omp holds an `openai-codex` login or its env token."""
676
+ return ("openai-codex" in _omp_stored_providers()
677
+ or bool(os.environ.get("OPENAI_CODEX_OAUTH_TOKEN", "").strip()))
678
+
679
+
655
680
  def _runnable(mode):
656
- return mode in OMP_FAMILIES and not (mode == "any" and not openrouter_ready())
681
+ return mode in MODE_FAMILIES and not (mode == "any" and not openrouter_ready()) \
682
+ and not (mode == "openai" and not codex_ready())
657
683
 
658
684
 
659
685
  def omp_overrides(mode):
@@ -766,17 +792,17 @@ def _ours_by_shape(key, value):
766
792
  # writes chains: pre-chain installs hold single-family selectors, and the
767
793
  # bytes are ours whichever shape we wrote.
768
794
  slot = AGENT_SLOT.get(key) or BUNDLED.get(key)
769
- if slot and any(_selector(family, slot) == value for family in OMP_FAMILIES):
795
+ if slot and any(_selector(family, slot) == value for family in MODE_FAMILIES):
770
796
  return True
771
797
  if key.split(".", 1)[-1] in OMP_ROLES and any(
772
- _selector(family, "frontier") == value for family in OMP_FAMILIES):
798
+ _selector(family, "frontier") == value for family in MODE_FAMILIES):
773
799
  return True
774
800
  return any((omp_overrides(mode) or {}).get(key) == value or
775
801
  (omp_role_overrides(mode) or {}).get(key) == value
776
802
  for mode in MODES)
777
803
 
778
804
 
779
- def _reconcile(current, written, want):
805
+ def _reconcile(current, written, want, taken=None):
780
806
  """(new, owns) for one flat {key: value} record, keys tezgah's own and the
781
807
  user's alike: `current` what omp holds, `written` the last recorded write,
782
808
  `want` what this mode asks for.
@@ -785,7 +811,9 @@ def _reconcile(current, written, want):
785
811
  `omp_written` recorded (or, on a machine written before that record existed,
786
812
  one of the table's own selectors - `_ours_by_shape`). A value the user
787
813
  changed by hand is left alone, an entry tezgah never wrote is never dropped,
788
- and ours that is no longer wanted goes."""
814
+ and ours that is no longer wanted goes. With `taken` (a list, an explicit
815
+ `tezgah-route --mode`, plan 106) a wanted key the user set by hand is
816
+ written over too, and its key is appended to `taken`."""
789
817
  new, owns = {}, {}
790
818
  for key in sorted(set(current) | set(written) | set(want)):
791
819
  cur = current.get(key)
@@ -795,7 +823,9 @@ def _reconcile(current, written, want):
795
823
  # byte-identical to what we want counts as ours: claiming it writes
796
824
  # the same value and only records ownership, which is what lets the
797
825
  # next mode switch move it instead of leaving it behind
798
- if cur is None or mine or cur == want[key]:
826
+ if cur is None or mine or cur == want[key] or taken is not None:
827
+ if taken is not None and not (cur is None or mine or cur == want[key]):
828
+ taken.append(key)
799
829
  new[key], owns[key] = want[key], want[key]
800
830
  else:
801
831
  new[key] = cur # the user's own choice for this key wins
@@ -811,7 +841,7 @@ def _reconcile(current, written, want):
811
841
  return new, owns
812
842
 
813
843
 
814
- def apply_omp(remove=False):
844
+ def apply_omp(remove=False, take=False):
815
845
  """Write (or, with `remove`, drop) this table's entries of omp's
816
846
  `task.agentModelOverrides` and its `modelRoles.plan` and `.slow`, leaving
817
847
  everything else as it is.
@@ -846,12 +876,16 @@ def apply_omp(remove=False):
846
876
  elif mode == "any" and not openrouter_ready():
847
877
  return ("omp model overrides %s the any mode needs an OpenRouter key "
848
878
  "(or `tezgah-route --mode anthropic`)" % SKIP_MARK)
879
+ elif mode == "openai" and not codex_ready():
880
+ return ("omp model overrides %s the openai mode needs omp's "
881
+ "`openai-codex` login (`/login openai-codex` in omp)" % SKIP_MARK)
849
882
  else:
850
883
  want = omp_overrides(mode)
851
884
  want_roles = omp_role_overrides(mode)
852
885
  flat_roles = {"modelRoles." + role: roles.get(role) for role in OMP_ROLES}
853
- new, owns = _reconcile(current, written, want)
854
- new_roles, owns_roles = _reconcile(flat_roles, written, want_roles)
886
+ taken = [] if take else None
887
+ new, owns = _reconcile(current, written, want, taken)
888
+ new_roles, owns_roles = _reconcile(flat_roles, written, want_roles, taken)
855
889
  owns.update(owns_roles)
856
890
  merged = dict(roles) # every other role the user set stays as it is
857
891
  for key in flat_roles: # a role this table drops leaves no key behind
@@ -866,7 +900,7 @@ def apply_omp(remove=False):
866
900
  flat_chains = {"retry.fallbackChains." + role: chains.get(role)
867
901
  for role in OMP_ROLES}
868
902
  want_chains = {} if remove else omp_role_chains(mode)
869
- new_chains, owns_chains = _reconcile(flat_chains, written, want_chains)
903
+ new_chains, owns_chains = _reconcile(flat_chains, written, want_chains, taken)
870
904
  owns.update(owns_chains)
871
905
  merged_chains = dict(chains)
872
906
  for key in flat_chains:
@@ -898,7 +932,9 @@ def apply_omp(remove=False):
898
932
  return ("omp model overrides written, ownership NOT recorded "
899
933
  "(overlay unreadable)")
900
934
  return ("omp model overrides removed (%s)" % (mode or "uninstall") if remove
901
- else "omp model overrides written (%s)" % mode)
935
+ else "omp model overrides written (%s)%s" % (
936
+ mode, "; replaced %d hand-set: %s" % (len(taken), ", ".join(taken))
937
+ if taken else ""))
902
938
 
903
939
 
904
940
  # ---------------------------------------------------------------- refresh ----