@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.
- package/CHANGELOG.md +48 -1
- package/MANIFEST +6 -0
- package/bin/tezgah-docs +5 -4
- package/bin/tezgah-optimize +338 -0
- package/bin/tezgah-research +2 -2
- package/bin/tezgah-route +6 -2
- package/bin/tezgah-setup +15 -1
- package/bin/tezgah-task +239 -27
- package/bin/tezgah-taste +3 -1
- package/docs/architecture.md +11 -0
- package/docs/clarity-baseline.json +1 -1
- package/docs/environment.md +1 -1
- package/docs/evidence.md +23 -20
- package/docs/gate.md +19 -14
- package/docs/models.md +9 -2
- package/docs/operations.md +9 -5
- package/docs/testing.md +18 -14
- package/hooks/lang/en.json +3 -0
- package/hooks/lang/tr.json +3 -0
- package/hooks/tezgah_context.py +36 -20
- package/hooks/tezgah_gate.py +75 -36
- package/hooks/tezgah_integrity.py +90 -134
- package/hooks/tezgah_models.py +62 -26
- package/hooks/tezgah_paths.py +107 -23
- package/hooks/tezgah_policy.py +10 -4
- package/hooks/tezgah_research.py +14 -8
- package/hooks/tezgah_stopspec.py +36 -83
- package/hooks/tezgah_store.py +7 -0
- package/hooks/tezgah_task.py +104 -35
- package/hosts/opencode/plugins/tezgah.js +136 -0
- package/output-styles/tezgah.md +5 -2
- package/package.json +1 -1
- package/skills/plan-add/SKILL.md +8 -4
- package/skills/tezgah-contract/SKILL.md +14 -0
|
@@ -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
|
-
#
|
|
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,
|
|
4883
|
-
|
|
4884
|
-
`
|
|
4885
|
-
|
|
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
|
-
|
|
4888
|
-
|
|
4889
|
-
|
|
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 (`
|
|
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
|
|
5032
|
-
"""
|
|
5033
|
-
|
|
5034
|
-
|
|
5035
|
-
|
|
5036
|
-
if
|
|
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
|
-
|
|
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
|
|
5054
|
-
ADMISSION))
|
|
5055
|
-
|
|
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
|
-
|
|
5079
|
-
|
|
5080
|
-
|
|
5081
|
-
|
|
5082
|
-
|
|
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
|
-
"
|
|
5103
|
-
"
|
|
5104
|
-
"
|
|
5105
|
-
"
|
|
5106
|
-
"
|
|
5107
|
-
"
|
|
5108
|
-
"
|
|
5109
|
-
"
|
|
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
|
|
5115
|
-
|
|
5116
|
-
|
|
5117
|
-
|
|
5118
|
-
|
|
5119
|
-
|
|
5120
|
-
|
|
5121
|
-
|
|
5122
|
-
|
|
5123
|
-
|
|
5124
|
-
|
|
5125
|
-
|
|
5126
|
-
|
|
5127
|
-
|
|
5128
|
-
|
|
5129
|
-
if
|
|
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
|
-
|
|
5146
|
-
|
|
5147
|
-
|
|
5148
|
-
|
|
5149
|
-
|
|
5150
|
-
|
|
5151
|
-
|
|
5152
|
-
|
|
5153
|
-
|
|
5154
|
-
|
|
5155
|
-
|
|
5156
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
|
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`)
|
|
6184
|
-
|
|
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
|
package/hooks/tezgah_models.py
CHANGED
|
@@ -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
|
|
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
|
|
576
|
+
"""`anthropic`, `zai`, `openai` or `any`: the saved mode, else the
|
|
577
|
+
default's own family.
|
|
565
578
|
|
|
566
|
-
A
|
|
567
|
-
Anthropic column - the caller decides what to do when the
|
|
568
|
-
|
|
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 =
|
|
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 =
|
|
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
|
|
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
|
|
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
|
|
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
|
-
|
|
854
|
-
|
|
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)" %
|
|
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 ----
|