@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/CHANGELOG.md +71 -1
- package/agents/tezgah-reviewer.md +19 -10
- package/bin/tezgah-docs +3 -3
- package/bin/tezgah-route +5 -2
- package/bin/tezgah-setup +24 -1
- package/bin/tezgah-status +3 -1
- package/bin/tezgah-task +130 -37
- package/bin/tezgah-taste +49 -31
- package/bin/tezgah-triage +12 -7
- package/docs/clarity-baseline.json +6 -6
- package/docs/contract.md +21 -11
- package/docs/environment.md +3 -1
- package/docs/evidence.md +4 -3
- package/docs/glossary.md +1 -1
- package/docs/judge.md +93 -27
- package/docs/operations.md +23 -12
- package/docs/skills.md +1 -1
- package/docs/vision.md +10 -5
- package/hooks/tezgah_agents.py +22 -11
- package/hooks/tezgah_context.py +40 -6
- package/hooks/tezgah_judge.py +245 -79
- package/hooks/tezgah_paths.py +165 -27
- package/hooks/tezgah_policy.py +35 -8
- package/hooks/tezgah_replay.py +26 -15
- package/hooks/tezgah_task.py +146 -40
- package/hooks/tezgah_taste.py +2 -2
- package/hooks/tezgah_taste_ledger.py +13 -4
- package/hosts/codex/hook.py +5 -0
- package/package.json +1 -1
- package/skills/harness/SKILL.md +4 -0
- package/skills/plan-add/SKILL.md +23 -7
- package/skills/plan-add/render_table.py +3 -2
- package/skills/plan-status/SKILL.md +12 -4
- package/skills/plan-sync/SKILL.md +19 -9
- package/skills/tezgah-contract/SKILL.md +25 -12
package/CHANGELOG.md
CHANGED
|
@@ -6,6 +6,75 @@ adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
|
6
6
|
|
|
7
7
|
## [Unreleased]
|
|
8
8
|
|
|
9
|
+
## [1.5.0] - 2026-10-09
|
|
10
|
+
|
|
11
|
+
### Changed
|
|
12
|
+
|
|
13
|
+
- **A plan gets one review, at most two rounds.** The review reads the plan's
|
|
14
|
+
whole diff once, in its verification phase. Round one spawns its reviewers in
|
|
15
|
+
one message, one per dimension, or one reviewer for a diff under 300 lines.
|
|
16
|
+
Only a confirmed critical or major finding opens round two. Round two reads
|
|
17
|
+
the fix's delta. A minor finding goes on the fix-later list. `tezgah-task review`
|
|
18
|
+
refuses a third round. What is left after round two goes to the user. The
|
|
19
|
+
reviewer agent, the contract and plan-sync now state this one rule. Before,
|
|
20
|
+
the reviewer looped until a round confirmed nothing.
|
|
21
|
+
- **One Acceptance rule.** A plan enters a writing phase only when each
|
|
22
|
+
Acceptance item names its command or says `unverifiable` and why. Before, one
|
|
23
|
+
command-bearing item let the move through. `render_table --acceptance
|
|
24
|
+
--strict` then refused the same plan. Both now read one predicate,
|
|
25
|
+
`tezgah_task.unproven`, and the refusal names each item's line.
|
|
26
|
+
- **Typed decisions take any Jev carrier.** Taste's `learn`, `label`,
|
|
27
|
+
`calibrate`, `gate` and `measure --typed` require the Jev model, not
|
|
28
|
+
TypeSafe's address. So do taste's background start and replay's
|
|
29
|
+
`--label-model`. OpenRouter System One, Cloudflare Workers AI or a
|
|
30
|
+
compatible endpoint counts the same. Old `typesafe` rows keep counting.
|
|
31
|
+
`learn`'s report key `typesafe` is now `jev`. Replay's `--provider typesafe`
|
|
32
|
+
is now `--provider jev`, and the old name still works. The triage's no-judge
|
|
33
|
+
message names every carrier, the selection knob and the session CLIs.
|
|
34
|
+
- **The fan-out rule arms on the lists the owner writes.** Items written
|
|
35
|
+
`1 - ...`, `1- `, `1:`, `a)` or `+` now arm it. An inline `1. ... 2. ...`
|
|
36
|
+
arms it, and so does "subagentlerle". Three files named as one list arm it
|
|
37
|
+
too, and so do "First, ... Second, ..." and three imperative clauses. A later
|
|
38
|
+
list in the same session gets a line that names the action: count the items,
|
|
39
|
+
one subagent per item in one message. The rule covers every phase: a plan's
|
|
40
|
+
tasks, its acceptance commands and a survey's sources fan out too. The clause
|
|
41
|
+
that let a list of small items run one by one is gone.
|
|
42
|
+
|
|
43
|
+
### Added
|
|
44
|
+
|
|
45
|
+
- **Plan order and progress in `tezgah-task status`.** A plan can name the
|
|
46
|
+
plans it waits on with `after:`. `status` prints each open plan's ticked
|
|
47
|
+
Acceptance boxes and what it still waits on. A `parallel:` line names the
|
|
48
|
+
ready plans whose `allowed_paths:` share no prefix. Those can run at once.
|
|
49
|
+
An open plan whose `status:` is not open or blocked is a `FAIL` line, and so
|
|
50
|
+
is an `after:` id no plan carries. `--json` adds `open` and `parallel`.
|
|
51
|
+
- **The judge asks opencode, Cursor and Codex sessions too.** The judgement
|
|
52
|
+
seam's session CLI was omp or claude only. It now also runs `opencode run`,
|
|
53
|
+
`cursor-agent -p` and `codex exec` headless when their host's marker is set
|
|
54
|
+
(`OPENCODE`, `CURSOR_AGENT`/`CURSOR_VERSION`, `CODEX_THREAD_ID`).
|
|
55
|
+
`TEZGAH_JUDGE_CLI`, else `judge_cli` in `config.json`, picks one of the five
|
|
56
|
+
by name. A picked CLI that is not installed means no session CLI.
|
|
57
|
+
- **Jev through any carrier.** The judge reaches Jev through TypeSafe,
|
|
58
|
+
OpenRouter, Cloudflare Workers AI or a System One-compatible URL
|
|
59
|
+
(`JEV_API_BASE_URL`, `JEV_API_KEY`).
|
|
60
|
+
`TEZGAH_JEV_PROVIDER`, `JEV_PROVIDER` or config.json `jev` picks the carrier,
|
|
61
|
+
and `tezgah-setup --jev <value>` writes the config key. `auto` asks every
|
|
62
|
+
carrier that resolves. A named carrier without its credential gets no
|
|
63
|
+
request, and no other carrier stands in. `tezgah-status --judge` prints the
|
|
64
|
+
choice and its carriers.
|
|
65
|
+
- **The omp login is a TypeSafe key.** With no `TYPESAFE_API_KEY` and no key
|
|
66
|
+
file, the seam reads the active omp login, read-only.
|
|
67
|
+
- **Clef and Clef-flash as typed judges.** Cloudflare's own decision models
|
|
68
|
+
answer the System One API on Workers AI with the Cloudflare token and account
|
|
69
|
+
id. `--jev clef` or `--jev clef-flash` picks one, and `auto` asks them after
|
|
70
|
+
the Jev carriers. A typed decision counts them like Jev. Cost rows now price
|
|
71
|
+
the carrier that answered: $0.24 per 1M input for Clef, $0.09 for Clef-flash.
|
|
72
|
+
|
|
73
|
+
### Fixed
|
|
74
|
+
|
|
75
|
+
- **A spent key stops costing a request.** OpenRouter answers 403 to a key
|
|
76
|
+
past its limit. A 403 now marks a provider down for five minutes, like a 401.
|
|
77
|
+
|
|
9
78
|
## [1.4.0] - 2026-10-08
|
|
10
79
|
|
|
11
80
|
### Changed
|
|
@@ -864,7 +933,8 @@ adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
|
864
933
|
the copy could not find `hooks/` and died on import. It now links into the
|
|
865
934
|
installed tree, and also installs the command as `tezgah`.
|
|
866
935
|
|
|
867
|
-
[Unreleased]: https://github.com/r1z4x/tezgah/compare/v1.
|
|
936
|
+
[Unreleased]: https://github.com/r1z4x/tezgah/compare/v1.5.0...HEAD
|
|
937
|
+
[1.5.0]: https://github.com/r1z4x/tezgah/releases/tag/v1.5.0
|
|
868
938
|
[1.4.0]: https://github.com/r1z4x/tezgah/releases/tag/v1.4.0
|
|
869
939
|
[1.3.0]: https://github.com/r1z4x/tezgah/releases/tag/v1.3.0
|
|
870
940
|
[1.2.0]: https://github.com/r1z4x/tezgah/releases/tag/v1.2.0
|
|
@@ -3,7 +3,9 @@ name: tezgah-reviewer
|
|
|
3
3
|
description: >
|
|
4
4
|
Adversarial, read-only review of a diff, branch or PR: derives the blast
|
|
5
5
|
radius, checks correctness, contract, security, tests and performance, and
|
|
6
|
-
classifies every finding confirmed/refuted/unverified.
|
|
6
|
+
classifies every finding confirmed/refuted/unverified. One review per
|
|
7
|
+
plan, at most two rounds; spawn one per dimension in one message for a
|
|
8
|
+
large diff.
|
|
7
9
|
model: opus
|
|
8
10
|
effort: high
|
|
9
11
|
readonly: true
|
|
@@ -56,15 +58,22 @@ is lowered only by evidence: a check you could not complete never
|
|
|
56
58
|
downgrades a confirmed finding - keep its severity and mark it
|
|
57
59
|
provisional. Disclose the order you read the files in.
|
|
58
60
|
|
|
59
|
-
Scope and budget.
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
61
|
+
Scope and budget. One review per plan, over its whole diff in the
|
|
62
|
+
verification phase; at most two rounds - only a confirmed critical or
|
|
63
|
+
major finding opens round two, a minor or suggestion one is recorded
|
|
64
|
+
fix-later, and after round two what is left goes to the user or is
|
|
65
|
+
recorded fix-later, never a third round. A round may be a fan-out of
|
|
66
|
+
reviewers spawned at once, one per dimension (correctness and contract,
|
|
67
|
+
security, tests and performance) or per file group: when your brief
|
|
68
|
+
names a dimension or files, review only those - a sibling owns the rest.
|
|
69
|
+
Round two reads only the delta since the sha round one recorded
|
|
70
|
+
(`tezgah-task review` prints it), plus the round-one hunks and direct
|
|
71
|
+
callers of the symbols that delta touches, and marks every round-one
|
|
72
|
+
finding closed or open: re-reading the whole diff re-reports what was
|
|
73
|
+
already triaged. Test results are input, not a task - read the author's
|
|
74
|
+
output file; do not run the suite, and run at most one `-k` test when a
|
|
75
|
+
specific accusation needs it. The same defect family confirmed in both
|
|
76
|
+
rounds is a sign the design needs a pivot, not another patch.
|
|
68
77
|
|
|
69
78
|
You may use: the codegraph MCP tools, read, grep and glob. Read-only:
|
|
70
79
|
no writes, no edits, no shell.
|
package/bin/tezgah-docs
CHANGED
|
@@ -24,7 +24,7 @@ every word is part of the query.
|
|
|
24
24
|
|
|
25
25
|
Matching is word-based and case-insensitive against an entry's title, its
|
|
26
26
|
questions, its sources and its path; every word must match, so a second word
|
|
27
|
-
narrows. A query that matches nothing is the last resort of one
|
|
27
|
+
narrows. A query that matches nothing is the last resort of one judge-seam Choice
|
|
28
28
|
question over the pages themselves; it answers `none` for a query no page
|
|
29
29
|
covers. When that question gives no judgement - unavailable (`docs-judge-off`,
|
|
30
30
|
the seam's `judge-off`, no credential) or a call that failed or came back
|
|
@@ -106,7 +106,7 @@ RUN = re.compile(r"((?:`[A-Za-z_][A-Za-z0-9_]*(?:\(\))?`\s*[/,]?\s*){1,5})$")
|
|
|
106
106
|
CITE = re.compile(r"`([\w.-]+(?:/[\w.-]+)+|[\w-]+\.\w+):(\d+)(?:-(\d+))?`"
|
|
107
107
|
r"|`(:(\d+)(?:-(\d+))?)`")
|
|
108
108
|
# The symbol written after the citation instead of before it, the way a table
|
|
109
|
-
# cell reads: `hooks/tezgah_policy.py:
|
|
109
|
+
# cell reads: `hooks/tezgah_policy.py:678-946` (`CORE`).
|
|
110
110
|
AFTER = re.compile(r"\s*\(`([A-Za-z_][A-Za-z0-9_]*)(?:\(\))?`\)")
|
|
111
111
|
# Any `path:line`, backticked or not: what CITE does not match of these (a
|
|
112
112
|
# bare token, a comma list `x.py:12,40`) is counted as unjudged, so a citation
|
|
@@ -1036,7 +1036,7 @@ def record(result):
|
|
|
1036
1036
|
|
|
1037
1037
|
|
|
1038
1038
|
def judge_pick(query):
|
|
1039
|
-
"""(judged, page): whether a
|
|
1039
|
+
"""(judged, page): whether a judge-seam Choice question came back with one of
|
|
1040
1040
|
its options, and the page it named (None for `none`).
|
|
1041
1041
|
|
|
1042
1042
|
The last resort for a query the index could not place: a word the index does
|
package/bin/tezgah-route
CHANGED
|
@@ -12,8 +12,11 @@ A brief naming stored data, a migration, credentials, the gate or security goes
|
|
|
12
12
|
tezgah-frontier by rule; otherwise Jev picks the tier (one Choice, ~650 input
|
|
13
13
|
tokens, ~0.4 s); with no key or `judge-off`, `--phase`
|
|
14
14
|
(explore|code|mechanical|plan|review|research) picks it from the static table, and
|
|
15
|
-
with neither the middle tier is used. The brief is sent to
|
|
16
|
-
|
|
15
|
+
with neither the middle tier is used. The brief is sent to the judge seam's
|
|
16
|
+
provider - the session's own CLI (omp, claude, opencode, cursor, codex) or the Jev
|
|
17
|
+
model through TypeSafe, OpenRouter System One, Cloudflare Workers AI or a
|
|
18
|
+
compatible endpoint (TEZGAH_JEV_PROVIDER picks one), else the OpenRouter chat
|
|
19
|
+
fallback - so write it in English, the language the judge reads best.
|
|
17
20
|
|
|
18
21
|
Every routed call writes one `route` row to the session's ledger (TEZGAH_SESSION):
|
|
19
22
|
tier, agent, what set the tier (`via`: rule|jev|phase|default), the static
|
package/bin/tezgah-setup
CHANGED
|
@@ -884,6 +884,22 @@ def save_features(ids):
|
|
|
884
884
|
return True
|
|
885
885
|
|
|
886
886
|
|
|
887
|
+
def set_jev(value):
|
|
888
|
+
"""Persist `value` as config.json["jev"] - this key's only writer - and print
|
|
889
|
+
the carriers it resolves to now. An unchanged value writes nothing, like
|
|
890
|
+
`save_features`; a config that does not parse is refused, not replaced."""
|
|
891
|
+
import tezgah_judge
|
|
892
|
+
cfg = read_config(tp.CONFIG, {})
|
|
893
|
+
if cfg is None:
|
|
894
|
+
sys.stderr.write("tezgah-setup: %s is not valid JSON; jev not set\n" % tp.CONFIG)
|
|
895
|
+
return 1
|
|
896
|
+
if cfg.get("jev") != value:
|
|
897
|
+
cfg["jev"] = value
|
|
898
|
+
write_json(tp.CONFIG, cfg)
|
|
899
|
+
print("%s %s %s" % (OK, tp.CONFIG, tezgah_judge.jev_summary()))
|
|
900
|
+
return 0
|
|
901
|
+
|
|
902
|
+
|
|
887
903
|
def app_servers(selected=None):
|
|
888
904
|
"""The app-analysis MCP servers to wire this run (shared spec)."""
|
|
889
905
|
return tezgah_apps.servers(selection() if selected is None else selected)
|
|
@@ -4767,7 +4783,7 @@ def host_checks_omp():
|
|
|
4767
4783
|
tp.have_typesafe_key()),
|
|
4768
4784
|
# One row per channel: the two disagree in both directions - a key file
|
|
4769
4785
|
# alone serves the seam, an omp login-store record alone serves omp.
|
|
4770
|
-
("judgement seam key resolvable (
|
|
4786
|
+
("judgement seam key resolvable (session CLI, Jev carrier or OpenRouter)",
|
|
4771
4787
|
tp.have_judge_key()),
|
|
4772
4788
|
("status line answers", omp_hook_answers()), ("tier models in task.agentModelOverrides and modelRoles.plan/slow", tier_models_current()),
|
|
4773
4789
|
]
|
|
@@ -5679,6 +5695,11 @@ def main(argv):
|
|
|
5679
5695
|
help="with --install: the language replies are asked for and "
|
|
5680
5696
|
"the Stop rule checks (config.json `reply_lang`; `tr` "
|
|
5681
5697
|
"when unset, `any` checks none)")
|
|
5698
|
+
ap.add_argument("--jev", choices=tp.JEV_CHOICES,
|
|
5699
|
+
help="the carrier the judgement seam reaches the Jev model "
|
|
5700
|
+
"through (config.json `jev`; `auto` tries every one "
|
|
5701
|
+
"that resolves); TEZGAH_JEV_PROVIDER and JEV_PROVIDER "
|
|
5702
|
+
"still win over it")
|
|
5682
5703
|
# `--version` is argparse's print-and-exit action, so `--upgrade --version V`
|
|
5683
5704
|
# would print the current version, exit 0 and move nothing at all - the
|
|
5684
5705
|
# "reads as done, did nothing" shape this repo hunts. Refused by name here,
|
|
@@ -5731,6 +5752,8 @@ def main(argv):
|
|
|
5731
5752
|
if args.features:
|
|
5732
5753
|
features_report()
|
|
5733
5754
|
return 0
|
|
5755
|
+
if args.jev:
|
|
5756
|
+
return set_jev(args.jev)
|
|
5734
5757
|
if args.refresh:
|
|
5735
5758
|
paths = refresh_contract()
|
|
5736
5759
|
for path in paths:
|
package/bin/tezgah-status
CHANGED
|
@@ -298,7 +298,9 @@ def main():
|
|
|
298
298
|
last = tj.last_use()
|
|
299
299
|
if "--json" in flags:
|
|
300
300
|
print(json.dumps(last, sort_keys=True))
|
|
301
|
-
|
|
301
|
+
return 0
|
|
302
|
+
print(tj.jev_summary())
|
|
303
|
+
if not last:
|
|
302
304
|
print("judge: no judgement recorded")
|
|
303
305
|
elif last.get("provider"):
|
|
304
306
|
print("judge: %s answered with %s/%s (fallback=%s)%s" % (
|
package/bin/tezgah-task
CHANGED
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
tezgah-task phase P [--any-path] change the active task's phase
|
|
6
6
|
tezgah-task allow GLOB ... | --any-path replace the allowlist
|
|
7
7
|
tezgah-task stop deactivate (clear phase)
|
|
8
|
-
tezgah-task status [--json] the active task,
|
|
8
|
+
tezgah-task status [--json] the active task, then every open plan: progress, what it waits on, what can run at once
|
|
9
9
|
tezgah-task review <NNN|path> <reviewer> approve|changes record a fresh review's verdict
|
|
10
10
|
tezgah-task close <NNN|path> done|discarded move a plan to .tezgah/plans/done
|
|
11
11
|
|
|
@@ -22,21 +22,27 @@ creates the plan, and `start` without `--allow` keeps it. A writing phase with
|
|
|
22
22
|
no allowlist is refused unless `--any-path` says so explicitly, and the record
|
|
23
23
|
keeps where its scope came from (`allowed_from:`), which `status` prints.
|
|
24
24
|
|
|
25
|
-
`review` and `close` are the session's: the verification phase asks for
|
|
26
|
-
|
|
25
|
+
`review` and `close` are the session's: the verification phase asks for one
|
|
26
|
+
review of the plan's whole diff, `review` records its verdict as
|
|
27
27
|
`review: <reviewer> <verdict> <date>` together with `reviewed_sha:` (the
|
|
28
28
|
project's HEAD it read) and `review_round:`, so the next round reads only
|
|
29
|
-
`git diff <reviewed_sha>..HEAD
|
|
30
|
-
NNN done` refuses a plan whose review is missing or not
|
|
31
|
-
refuses the active task - that one is the user's to stop first.
|
|
29
|
+
`git diff <reviewed_sha>..HEAD`, and refuses a third round: the budget is two
|
|
30
|
+
rounds per plan; `close NNN done` refuses a plan whose review is missing or not
|
|
31
|
+
`approve`. `close` refuses the active task - that one is the user's to stop first.
|
|
32
32
|
|
|
33
33
|
Two things the plan owes are asked here, at the only moment they can still be
|
|
34
34
|
fixed - the phase move - because the record is the user's and the refusal is
|
|
35
|
-
this CLI's: a writing phase needs
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
`implementation` needs the plan's spike
|
|
39
|
-
evidence rather than on the guess the spike
|
|
35
|
+
this CLI's: a writing phase needs every Acceptance item to name the command
|
|
36
|
+
proving it (or say `unverifiable` and why), since the report CI runs afterwards
|
|
37
|
+
refuses an open plan on any one item that does neither, through the same
|
|
38
|
+
predicate (`tezgah_task.unproven`); and `implementation` needs the plan's spike
|
|
39
|
+
answered, so the build starts on evidence rather than on the guess the spike
|
|
40
|
+
exists to retire.
|
|
41
|
+
|
|
42
|
+
`status` lists every open plan after the active task: its Acceptance boxes
|
|
43
|
+
ticked, the plans its `after:` still waits on, and - on a `parallel:` line - the
|
|
44
|
+
ready plans whose `allowed_paths:` share no prefix, which can run at once. A
|
|
45
|
+
status outside open|blocked, or an `after:` id no plan carries, is a `FAIL` line.
|
|
40
46
|
|
|
41
47
|
Entering `implementation` also records where the risky work returns to: a
|
|
42
48
|
`checkpoint:` field holding the pre-work commit's sha, or `pending <sha>` while
|
|
@@ -139,6 +145,14 @@ def write_fields(path, **fields):
|
|
|
139
145
|
|
|
140
146
|
ANY_PATH = "--any-path"
|
|
141
147
|
VERDICTS = ("approve", "changes")
|
|
148
|
+
# The review budget, enforced here and nowhere else: `review` refuses a third
|
|
149
|
+
# round, and every text that states the rule (the reviewer agent, the contract,
|
|
150
|
+
# plan-sync) says the same two rounds.
|
|
151
|
+
REVIEW_ROUNDS = 2
|
|
152
|
+
# Under this many changed lines one reviewer reads every dimension; at or above
|
|
153
|
+
# it the round fans out. ponytail: a line count, not a risk model - a small diff
|
|
154
|
+
# that touches a trust boundary is the brief's to route to security.
|
|
155
|
+
SMALL_DIFF = 300
|
|
142
156
|
|
|
143
157
|
|
|
144
158
|
def allow_source(text):
|
|
@@ -279,13 +293,14 @@ def phase_problems(phase, text, path):
|
|
|
279
293
|
items = tt.acceptance_gap(text)
|
|
280
294
|
if items:
|
|
281
295
|
out.append(
|
|
282
|
-
"%s cannot enter %s:
|
|
283
|
-
"command and
|
|
284
|
-
"hooks/tezgah_task.py:acceptance_items), so
|
|
285
|
-
"to be held to. Give
|
|
296
|
+
"%s cannot enter %s: %d of its %d Acceptance items name no "
|
|
297
|
+
"command and declare no `unverifiable` (read by "
|
|
298
|
+
"hooks/tezgah_task.py:acceptance_items; at %s), so that work has "
|
|
299
|
+
"no check to be held to. Give each one the command that proves "
|
|
286
300
|
"it, or `unverifiable` and why; "
|
|
287
|
-
"`tezgah-render-table --acceptance`
|
|
288
|
-
% (path, phase, len(items)))
|
|
301
|
+
"`tezgah-render-table --acceptance --strict` refuses the same items"
|
|
302
|
+
% (path, phase, len(items), len(tt.acceptance_items(text)),
|
|
303
|
+
", ".join("%s:%d" % (path, i["line"]) for i in items)))
|
|
289
304
|
if phase == "implementation" and tt.spike_unanswered(text):
|
|
290
305
|
out.append(
|
|
291
306
|
"%s cannot enter implementation: its spike is unanswered - `spike:` "
|
|
@@ -295,12 +310,23 @@ def phase_problems(phase, text, path):
|
|
|
295
310
|
return out
|
|
296
311
|
|
|
297
312
|
|
|
313
|
+
def changed_lines(root, base):
|
|
314
|
+
"""Lines `git diff <base>..HEAD` adds and removes, or None when git cannot
|
|
315
|
+
say (no base, not a repository)."""
|
|
316
|
+
stat = git_out(root, "diff", "--shortstat", base + "..HEAD") \
|
|
317
|
+
if base != "<base>" else None
|
|
318
|
+
if stat is None:
|
|
319
|
+
return None
|
|
320
|
+
return sum(int(n) for n in re.findall(r"(\d+) (?:insertion|deletion)", stat))
|
|
321
|
+
|
|
322
|
+
|
|
298
323
|
def verification_steps(text, path):
|
|
299
324
|
"""What the verification phase requires before the plan is done: the plan's
|
|
300
|
-
own acceptance commands run once on the final tree, unpiped, and
|
|
301
|
-
|
|
302
|
-
without
|
|
303
|
-
|
|
325
|
+
own acceptance commands run once on the final tree, unpiped, and ONE review
|
|
326
|
+
of the plan's whole diff, recorded with `review` - `close NNN done` refuses
|
|
327
|
+
without an approve. Round one fans out one reviewer per dimension, round two
|
|
328
|
+
reads only the delta since the sha round one recorded, and REVIEW_ROUNDS is
|
|
329
|
+
the budget `review` enforces."""
|
|
304
330
|
fields = tt.frontmatter(text)
|
|
305
331
|
ident = fields.get("id") or os.path.basename(path).split("-")[0]
|
|
306
332
|
# every item, not only the ones the command reader recognises: `pnpm test`
|
|
@@ -311,27 +337,51 @@ def verification_steps(text, path):
|
|
|
311
337
|
for i in tt.acceptance_items(text)]
|
|
312
338
|
# the project, not the private repository `.tezgah` may hold
|
|
313
339
|
root = os.path.realpath(os.path.join(os.path.dirname(path), "..", "..", ".."))
|
|
314
|
-
reviewed = fields.get("reviewed_sha") or ""
|
|
340
|
+
reviewed = fields.get("reviewed_sha") or "(no commit)"
|
|
315
341
|
prior = fields.get("review_round") or ""
|
|
316
342
|
rounds = int(prior) if prior.isdigit() else 0
|
|
317
343
|
print("verification - required before this plan is done:")
|
|
318
344
|
print(" 1. prove the acceptance items ONCE, on the final tree - not again per "
|
|
319
345
|
"review round - every command run unpiped "
|
|
320
|
-
"(`cmd > /tmp/check.log 2>&1`, then read the file)
|
|
346
|
+
"(`cmd > /tmp/check.log 2>&1`, then read the file); items that share "
|
|
347
|
+
"no state go out as tool calls in ONE message, one suite run at a time:")
|
|
321
348
|
for item in items or ["(the plan has no Acceptance items)"]:
|
|
322
349
|
print(" - %s" % item)
|
|
323
350
|
if os.path.isfile(os.path.join(root, "tests", "impacted.py")):
|
|
324
351
|
print(" the one full run here is `python3 tests/impacted.py --all`")
|
|
325
|
-
if
|
|
326
|
-
print(" 2. review round %d
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
352
|
+
if rounds and review_verdict(text) == "approve":
|
|
353
|
+
print(" 2. the review approved in round %d at %s: no further round"
|
|
354
|
+
% (rounds, reviewed))
|
|
355
|
+
print(" 3. `tezgah-task close %s done` once the work lands" % ident)
|
|
356
|
+
return
|
|
357
|
+
if rounds >= REVIEW_ROUNDS:
|
|
358
|
+
print(" 2. the %d-round budget is spent: `tezgah-task review` refuses a "
|
|
359
|
+
"third round. A critical or major finding still confirmed goes to "
|
|
360
|
+
"the user (block); a minor or suggestion one is recorded fix-later "
|
|
361
|
+
"under the plan's ## State" % REVIEW_ROUNDS)
|
|
362
|
+
return
|
|
363
|
+
if rounds:
|
|
364
|
+
print(" 2. review round 2 (the last): each confirmed critical or major "
|
|
365
|
+
"finding is fixed with a regression test that failed first; one "
|
|
366
|
+
"fresh tezgah-reviewer reads only the delta `git diff %s..HEAD`, the "
|
|
367
|
+
"round-1 hunks and direct callers of the symbols it touches, and the "
|
|
368
|
+
"test output above, and marks every round-1 finding closed or open; "
|
|
369
|
+
"it does not re-run the suite" % reviewed)
|
|
332
370
|
else:
|
|
333
|
-
|
|
334
|
-
|
|
371
|
+
base = diff_base(root)
|
|
372
|
+
size = changed_lines(root, base)
|
|
373
|
+
print(" 2. one review of the whole plan, at most %d rounds: spawn fresh "
|
|
374
|
+
"tezgah-reviewer subagents in ONE message, each owning one dimension "
|
|
375
|
+
"- correctness and contract, security, tests and performance - or one "
|
|
376
|
+
"file group of a large diff, over `git diff %s..HEAD` and the test "
|
|
377
|
+
"output above, then merge their findings into one verdict. Under %d "
|
|
378
|
+
"changed lines one reviewer reads every dimension (this diff: %s), "
|
|
379
|
+
"unless the diff touches auth, secrets or a gate, which keeps a "
|
|
380
|
+
"security reviewer" % (REVIEW_ROUNDS, base, SMALL_DIFF,
|
|
381
|
+
"unknown" if size is None else size))
|
|
382
|
+
print(" only a confirmed critical or major finding is `changes` and "
|
|
383
|
+
"opens round 2; a minor or suggestion one is recorded fix-later "
|
|
384
|
+
"under ## State and the verdict is `approve`")
|
|
335
385
|
print(" 3. record its verdict: `tezgah-task review %s <reviewer> approve|changes`"
|
|
336
386
|
" (records the reviewed sha);"
|
|
337
387
|
" `tezgah-task close %s done` refuses without an approve" % (ident, ident))
|
|
@@ -495,6 +545,16 @@ def review_verdict(text):
|
|
|
495
545
|
return parts[1] if len(parts) >= 2 and parts[1] in VERDICTS else None
|
|
496
546
|
|
|
497
547
|
|
|
548
|
+
def budget_spent(path, ident):
|
|
549
|
+
"""The refusal once REVIEW_ROUNDS rounds are recorded: what is left belongs
|
|
550
|
+
to the user, never to a third round."""
|
|
551
|
+
return ("%s has spent its %d-round review budget: never a third round. A "
|
|
552
|
+
"critical or major finding still confirmed goes to the user, who "
|
|
553
|
+
"decides (block); a minor or suggestion one is recorded fix-later "
|
|
554
|
+
"under the plan's ## State; `tezgah-task close %s discarded` needs no "
|
|
555
|
+
"review" % (path, REVIEW_ROUNDS, ident))
|
|
556
|
+
|
|
557
|
+
|
|
498
558
|
def cmd_review(args):
|
|
499
559
|
if len(args) != 3 or args[2] not in VERDICTS:
|
|
500
560
|
return misuse("review takes <NNN|path> <reviewer> %s" % "|".join(VERDICTS))
|
|
@@ -503,6 +563,8 @@ def cmd_review(args):
|
|
|
503
563
|
return problems([problem])
|
|
504
564
|
fields = tt.frontmatter(read(path))
|
|
505
565
|
prior = fields.get("review_round") or ""
|
|
566
|
+
if prior.isdigit() and int(prior) >= REVIEW_ROUNDS:
|
|
567
|
+
return problems([budget_spent(path, os.path.basename(path).split("-")[0])])
|
|
506
568
|
# the project's HEAD, never the private `.tezgah` repository's: the next
|
|
507
569
|
# round's delta is `git diff <this>..HEAD` over the project
|
|
508
570
|
root = os.path.realpath(os.path.join(os.path.dirname(path), "..", "..", ".."))
|
|
@@ -535,6 +597,9 @@ def cmd_close(args):
|
|
|
535
597
|
return problems(["%s is the active task: the user stops it first "
|
|
536
598
|
"(`tezgah-task stop`)" % path])
|
|
537
599
|
if args[1] == "done" and review_verdict(text) != "approve":
|
|
600
|
+
prior = tt.frontmatter(text).get("review_round") or ""
|
|
601
|
+
if prior.isdigit() and int(prior) >= REVIEW_ROUNDS:
|
|
602
|
+
return problems([budget_spent(path, ident)])
|
|
538
603
|
return problems(["%s records no approving review: a fresh reviewer reads "
|
|
539
604
|
"the diff, then `tezgah-task review %s <reviewer> approve`"
|
|
540
605
|
% (path, ident)])
|
|
@@ -561,22 +626,50 @@ def cmd_status(args):
|
|
|
561
626
|
bad = []
|
|
562
627
|
for path in plans(root):
|
|
563
628
|
fields = tt.frontmatter(read(path))
|
|
629
|
+
ident = fields.get("id") or os.path.basename(path).split("-")[0]
|
|
564
630
|
phase = fields.get("phase") or ""
|
|
565
631
|
if phase and phase not in tt.TASK_PHASES:
|
|
566
|
-
bad.append({"id":
|
|
567
|
-
"
|
|
632
|
+
bad.append({"id": ident, "phase": phase, "path": path,
|
|
633
|
+
"problem": "phase %r is not one of %s, so the plan is ignored (%s)"
|
|
634
|
+
% (phase, ", ".join(tt.TASK_PHASES), path)})
|
|
635
|
+
status = fields.get("status") or ""
|
|
636
|
+
if status not in tt.OPEN_STATUSES:
|
|
637
|
+
bad.append({"id": ident, "path": path,
|
|
638
|
+
"problem": "status %r is not one of %s, which is what a plan "
|
|
639
|
+
"under .tezgah/plans/open carries (%s)"
|
|
640
|
+
% (status, ", ".join(tt.OPEN_STATUSES), path)})
|
|
641
|
+
rows = tt.queue(root)
|
|
642
|
+
for row in rows:
|
|
643
|
+
for ident, state in row["after"]:
|
|
644
|
+
if state == "missing":
|
|
645
|
+
bad.append({"id": row["id"], "path": row["path"],
|
|
646
|
+
"problem": "`after: %s` names no plan under .tezgah/plans "
|
|
647
|
+
"(%s)" % (ident, row["path"])})
|
|
648
|
+
together = tt.parallel(rows)
|
|
568
649
|
if task:
|
|
569
650
|
task["allowed_from"] = allow_source(read(task["path"]))[1]
|
|
570
651
|
if as_json:
|
|
571
|
-
print(json.dumps({"active": task, "malformed": bad
|
|
652
|
+
print(json.dumps({"active": task, "malformed": bad, "open": rows,
|
|
653
|
+
"parallel": together}, indent=2))
|
|
572
654
|
else:
|
|
573
655
|
if task:
|
|
574
656
|
show(task["path"])
|
|
575
657
|
else:
|
|
576
658
|
print("no active task")
|
|
659
|
+
if rows:
|
|
660
|
+
print("open plans: %d, %d ready" % (len(rows), sum(r["ready"] for r in rows)))
|
|
661
|
+
for row in rows:
|
|
662
|
+
waits = ["%s (%s)" % (ident, state) for ident, state in row["after"]
|
|
663
|
+
if state != "done"]
|
|
664
|
+
print(" %s %s, acceptance %d/%d checked, %s" % (
|
|
665
|
+
row["id"], row["status"] or "(no status)", row["checked"], row["items"],
|
|
666
|
+
"ready" if row["ready"] else
|
|
667
|
+
"waits on %s" % ", ".join(waits) if waits else row["status"]))
|
|
668
|
+
if together:
|
|
669
|
+
print("parallel: %s - ready, and no two share an allowed_paths prefix"
|
|
670
|
+
% ", ".join(together))
|
|
577
671
|
for row in bad:
|
|
578
|
-
print("FAIL %s:
|
|
579
|
-
% (row["id"], row["phase"], ", ".join(tt.TASK_PHASES), row["path"]))
|
|
672
|
+
print("FAIL %s: %s" % (row["id"], row["problem"]))
|
|
580
673
|
return 1 if bad else 0
|
|
581
674
|
|
|
582
675
|
|