@r1z4x/tezgah 1.8.3 → 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 +35 -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 +8 -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 +31 -19
- 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 +84 -28
- 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
package/bin/tezgah-task
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
#!/usr/bin/env python3
|
|
2
2
|
"""tezgah-task - the active task: one plan's phase and its path allowlist.
|
|
3
3
|
|
|
4
|
-
tezgah-task start <NNN|path> --phase P [--allow GLOB ... | --any-path] activate a plan
|
|
4
|
+
tezgah-task start <NNN|path> --phase P [--allow GLOB ... | --any-path] [--branch B] activate a plan; --branch binds the worktree on B to it
|
|
5
5
|
tezgah-task phase P [--any-path] change the active task's phase
|
|
6
6
|
tezgah-task allow GLOB ... | --any-path --why REASON replace the allowlist, recording why
|
|
7
7
|
tezgah-task stop deactivate (clear phase)
|
|
@@ -11,6 +11,7 @@
|
|
|
11
11
|
tezgah-task tick <NNN|path> <n> [--undo] tick (or untick) Acceptance item n, counted from 1
|
|
12
12
|
tezgah-task note <NNN|path> state|next "<text>" replace the body of `## State` (the newest one) or `## Next`
|
|
13
13
|
tezgah-task spike <NNN|path> "<answer>" record the spike's answer (`spike_recorded:`)
|
|
14
|
+
tezgah-task unmet <NNN|path> <n> "<reason, who accepted it>" annotate Acceptance item n with `UNMET:`, the form close accepts
|
|
14
15
|
tezgah-task slice open --id ID [--kind agent|job] [--host H] [--agent A] [--brief TEXT|--brief-file F] [--parent P] [--cwd PATH]
|
|
15
16
|
record a delegated slice (a re-open counts one more attempt)
|
|
16
17
|
tezgah-task slice done ID | slice stop ID [--class C] close a slice record
|
|
@@ -19,6 +20,7 @@
|
|
|
19
20
|
tezgah-task slice stats [--window DAYS] [--json] counts per status, failure class and host, and the RSI lever candidates
|
|
20
21
|
tezgah-task slice board [--parent P] the open sibling slices: what each owns, its worktree, branch and last commit
|
|
21
22
|
tezgah-task slice merge-plan [--base REF] the done slice branches in merge order, with their forecast conflicts
|
|
23
|
+
tezgah-task sweep the last session's leftovers: pending slices, landed worktrees and branches, an orphan phase
|
|
22
24
|
|
|
23
25
|
The record is the plan file itself: `phase:` activates it, `allowed_paths:` is
|
|
24
26
|
the scope a write has to stay inside, and `start` clears every other plan's
|
|
@@ -37,7 +39,7 @@ and this CLI is the record's only writer: the gate refuses a write to the plan
|
|
|
37
39
|
file, because the checks below are made here and a retyped frontmatter would
|
|
38
40
|
skip them. `phase implementation` (or `verification`) is what lets a write
|
|
39
41
|
through, so the phase changes when the work reaches that point and not before.
|
|
40
|
-
`tick`, `note` and `
|
|
42
|
+
`tick`, `note`, `spike` and `unmet` are the record's body writers for the same reason:
|
|
41
43
|
they write an Acceptance box, the State/Next text or the spike's answer on any
|
|
42
44
|
open plan, active or not, keep the rest of the frontmatter but `updated:`, and
|
|
43
45
|
commit nothing.
|
|
@@ -134,7 +136,7 @@ import time
|
|
|
134
136
|
HERE = os.path.dirname(os.path.dirname(os.path.realpath(__file__)))
|
|
135
137
|
sys.path.insert(0, os.path.join(HERE, "hooks"))
|
|
136
138
|
import tezgah_task as tt # noqa: E402
|
|
137
|
-
from tezgah_paths import project_top, say # noqa: E402
|
|
139
|
+
from tezgah_paths import project_top, say, work_tree # noqa: E402
|
|
138
140
|
|
|
139
141
|
|
|
140
142
|
def repo_root(path):
|
|
@@ -198,6 +200,22 @@ def active_here(root):
|
|
|
198
200
|
return tt.active(root, root, held=True)
|
|
199
201
|
|
|
200
202
|
|
|
203
|
+
def own_task(root):
|
|
204
|
+
"""(task, problem) for a command that writes the active plan's record. In a
|
|
205
|
+
linked worktree on a branch that binds a plan - `plan/NNN-...` or the plan's
|
|
206
|
+
declared `branch:` - that is plan NNN or a refusal, never the main
|
|
207
|
+
checkout's first phased plan: `allow` there replaced another plan's
|
|
208
|
+
allowlist (plan 102). The gate keeps that fallback, which holds the
|
|
209
|
+
worktree's writes; only the record write is refused."""
|
|
210
|
+
task = active_here(root)
|
|
211
|
+
ident = tt.plan_of_branch(tt.worktree_branch(root), main_root(root))
|
|
212
|
+
if ident is not None and (not task or task["id"] != ident):
|
|
213
|
+
return None, ("this worktree is plan %s's and plan %s is not the active task "
|
|
214
|
+
"here: start it with `tezgah-task start %s --phase P`; no other "
|
|
215
|
+
"plan's record moves from this worktree" % (ident, ident, ident))
|
|
216
|
+
return task, None
|
|
217
|
+
|
|
218
|
+
|
|
201
219
|
def problems(items):
|
|
202
220
|
for problem in items:
|
|
203
221
|
print("FAIL %s" % problem)
|
|
@@ -271,7 +289,7 @@ def checkpoint_state(value, path):
|
|
|
271
289
|
# a plan held by its own worktree waits on that checkout's commit, the
|
|
272
290
|
# one the gate reads (tezgah_gate.checkpoint_reason)
|
|
273
291
|
holder = holder_of(path)
|
|
274
|
-
root = holder or repo_root(os.path.dirname(path) or ".")
|
|
292
|
+
root = holder or work_tree(repo_root(os.path.dirname(path) or "."))
|
|
275
293
|
head = git_out(root, "rev-parse", "HEAD")
|
|
276
294
|
listed = git_out(root, "status", "--porcelain")
|
|
277
295
|
if not head or head != sha or not listed:
|
|
@@ -451,7 +469,7 @@ def verification_steps(text, path):
|
|
|
451
469
|
print(" - %s" % item)
|
|
452
470
|
# the deferred tier: never inside a turn, run once here at the merge step
|
|
453
471
|
deferred = [cmd for rel, cmd in MERGE_CHECKS
|
|
454
|
-
if os.path.isfile(os.path.join(
|
|
472
|
+
if os.path.isfile(os.path.join(work_tree(where), rel))]
|
|
455
473
|
if deferred:
|
|
456
474
|
print(" the merge step's one run of the deferred checks: %s"
|
|
457
475
|
% ", ".join("`%s`" % c for c in deferred))
|
|
@@ -518,6 +536,7 @@ def cmd_start(args):
|
|
|
518
536
|
root = main_root(repo_root(os.getcwd()))
|
|
519
537
|
path, problem = open_plan(args[0])
|
|
520
538
|
phase = None
|
|
539
|
+
branch = None
|
|
521
540
|
allow = []
|
|
522
541
|
any_path = False
|
|
523
542
|
rest = args[1:]
|
|
@@ -529,6 +548,12 @@ def cmd_start(args):
|
|
|
529
548
|
phase = rest[i + 1]
|
|
530
549
|
i += 2
|
|
531
550
|
continue
|
|
551
|
+
if rest[i] == "--branch":
|
|
552
|
+
if i + 1 >= len(rest) or rest[i + 1].startswith("--"):
|
|
553
|
+
return misuse("--branch needs the branch name to bind the plan to")
|
|
554
|
+
branch = rest[i + 1]
|
|
555
|
+
i += 2
|
|
556
|
+
continue
|
|
532
557
|
if rest[i] == ANY_PATH:
|
|
533
558
|
any_path = True
|
|
534
559
|
i += 1
|
|
@@ -553,6 +578,8 @@ def cmd_start(args):
|
|
|
553
578
|
return problems([problem])
|
|
554
579
|
text = read(path)
|
|
555
580
|
fields = {"phase": phase, "updated": today()}
|
|
581
|
+
if branch:
|
|
582
|
+
fields["branch"] = branch # binds the worktree on it (plan 105)
|
|
556
583
|
if allow or any_path:
|
|
557
584
|
fields.update(allowed_paths=allow, allowed_from="--allow" if allow else ANY_PATH)
|
|
558
585
|
if tt.frontmatter(text).get("allowed_why"):
|
|
@@ -567,9 +594,11 @@ def cmd_start(args):
|
|
|
567
594
|
return problems(bad)
|
|
568
595
|
holders = tt.plan_holders(root)
|
|
569
596
|
# the plan's own worktree, when one holds it: the gate reads the checkpoint
|
|
570
|
-
# against that checkout's HEAD and tree, so it is recorded from there
|
|
571
|
-
|
|
572
|
-
|
|
597
|
+
# against that checkout's HEAD and tree, so it is recorded from there - the
|
|
598
|
+
# one on the branch this start binds, before the record names it
|
|
599
|
+
here = (holders.get(tt._plan_id(tt.frontmatter(text), os.path.basename(path)))
|
|
600
|
+
or branch and tt._worktree_branches(root).get(branch))
|
|
601
|
+
fields.update(checkpoint_fields(here or work_tree(root), phase))
|
|
573
602
|
for other in plans(root):
|
|
574
603
|
if other == path:
|
|
575
604
|
continue
|
|
@@ -602,9 +631,9 @@ def cmd_phase(args):
|
|
|
602
631
|
if phase not in tt.TASK_PHASES:
|
|
603
632
|
return problems([bad_phase(phase)])
|
|
604
633
|
root = repo_root(os.getcwd())
|
|
605
|
-
task =
|
|
606
|
-
if not task:
|
|
607
|
-
return problems(["no active task: start one with "
|
|
634
|
+
task, bad = own_task(root)
|
|
635
|
+
if bad or not task:
|
|
636
|
+
return problems([bad or "no active task: start one with "
|
|
608
637
|
"`tezgah-task start <id> --phase %s`" % phase])
|
|
609
638
|
text = read(task["path"])
|
|
610
639
|
fields = {"phase": phase, "updated": today()}
|
|
@@ -614,9 +643,13 @@ def cmd_phase(args):
|
|
|
614
643
|
fields["allowed_why"] = ""
|
|
615
644
|
granted = (any_path or tt.frontmatter(text).get("allowed_from")
|
|
616
645
|
== ANY_PATH)
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
646
|
+
# one pre-flight list, not one refusal per attempt: a plan that owes the
|
|
647
|
+
# scope, the Acceptance and the spike learns all three from one run
|
|
648
|
+
# (plan 108)
|
|
649
|
+
bad = list(scope_problem(phase, task["allowed_paths"], granted) or [])
|
|
650
|
+
bad += phase_problems(phase, text, task["path"])
|
|
651
|
+
if any_path:
|
|
652
|
+
bad += held_overlap(root, task["id"], [])
|
|
620
653
|
if bad:
|
|
621
654
|
return problems(bad)
|
|
622
655
|
where = task.get("worktree") or tt.plan_holders(main_root(root)).get(task["id"])
|
|
@@ -645,9 +678,9 @@ def cmd_allow(args):
|
|
|
645
678
|
if not why:
|
|
646
679
|
return misuse("allow takes --why \"<what in the plan reaches these paths>\"")
|
|
647
680
|
root = repo_root(os.getcwd())
|
|
648
|
-
task =
|
|
649
|
-
if not task:
|
|
650
|
-
return problems(["no active task to widen: start one with "
|
|
681
|
+
task, bad = own_task(root)
|
|
682
|
+
if bad or not task:
|
|
683
|
+
return problems([bad or "no active task to widen: start one with "
|
|
651
684
|
"`tezgah-task start <id> --phase P --allow GLOB`"])
|
|
652
685
|
bad = held_overlap(root, task["id"], args)
|
|
653
686
|
if bad:
|
|
@@ -664,12 +697,58 @@ def cmd_allow(args):
|
|
|
664
697
|
return show(task["path"])
|
|
665
698
|
|
|
666
699
|
|
|
700
|
+
def cmd_unmet(args):
|
|
701
|
+
"""Annotate one Acceptance item with `UNMET: <reason, who accepted it>` - the
|
|
702
|
+
form `close NNN done` accepts in place of a ticked box, and until now the one
|
|
703
|
+
the CLI could not write: the gate refuses a write to the record, so an item
|
|
704
|
+
the owner accepted as not met could only be annotated by hand and the plan
|
|
705
|
+
could not close (plan 110). The record's fifth body writer beside `tick`,
|
|
706
|
+
`note`, `spike` and `sweep`; it commits nothing."""
|
|
707
|
+
if len(args) != 3:
|
|
708
|
+
return misuse('unmet takes <NNN|path> <n> "<reason, who accepted it>"')
|
|
709
|
+
ref, number, reason = args
|
|
710
|
+
if not number.isdigit() or int(number) < 1:
|
|
711
|
+
return misuse("unmet takes the item's number, counted from 1")
|
|
712
|
+
reason = " ".join(reason.split())
|
|
713
|
+
if not reason:
|
|
714
|
+
return misuse("unmet takes the reason, and who accepted it")
|
|
715
|
+
# the same resolver `tick` and `note` take: `plan_path` accepts any existing
|
|
716
|
+
# file, which let `unmet README.md 1 x` rewrite a README, and it resolved the
|
|
717
|
+
# wrong checkout from a linked worktree (review of plan 110, F2)
|
|
718
|
+
path, problem = open_plan(ref)
|
|
719
|
+
if problem:
|
|
720
|
+
return problems([problem])
|
|
721
|
+
n = int(number)
|
|
722
|
+
items = tt.acceptance_items(read(path))
|
|
723
|
+
if n > len(items):
|
|
724
|
+
return problems(["%s has %d Acceptance item(s), so item %d names none"
|
|
725
|
+
% (path, len(items), n)])
|
|
726
|
+
if items[n - 1]["checked"]:
|
|
727
|
+
return problems(["%s item %d is ticked already: `UNMET:` is for an item the "
|
|
728
|
+
"owner accepted as not met, so untick it first "
|
|
729
|
+
"(`tezgah-task tick %s %d --undo`)" % (path, n, ref, n)])
|
|
730
|
+
if tt._UNMET.search(items[n - 1]["text"]):
|
|
731
|
+
return problems(["%s item %d carries an `UNMET:` line already (%s)"
|
|
732
|
+
% (path, n, items[n - 1]["text"][:64])])
|
|
733
|
+
try:
|
|
734
|
+
item = tt.set_unmet(path, n, reason)
|
|
735
|
+
except OSError as exc:
|
|
736
|
+
return problems(["cannot write %s: %s" % (path, exc)])
|
|
737
|
+
if item is None:
|
|
738
|
+
return problems(["%s has no Acceptance item %d" % (path, n)])
|
|
739
|
+
bad = write_fields(path, updated=today())
|
|
740
|
+
if bad:
|
|
741
|
+
return problems(bad)
|
|
742
|
+
print("%s: item %d -> UNMET: %s" % (path, n, reason))
|
|
743
|
+
return 0
|
|
744
|
+
|
|
745
|
+
|
|
667
746
|
def cmd_stop(args):
|
|
668
747
|
if args:
|
|
669
748
|
return misuse("stop takes no arguments")
|
|
670
|
-
task =
|
|
671
|
-
if not task:
|
|
672
|
-
return problems(["no active task to stop"])
|
|
749
|
+
task, bad = own_task(repo_root(os.getcwd()))
|
|
750
|
+
if bad or not task:
|
|
751
|
+
return problems([bad or "no active task to stop"])
|
|
673
752
|
bad = write_fields(task["path"], phase="", updated=today())
|
|
674
753
|
if bad:
|
|
675
754
|
return problems(bad)
|
|
@@ -955,8 +1034,18 @@ def cmd_close(args):
|
|
|
955
1034
|
print(line)
|
|
956
1035
|
# the checkout this runs in stays, even when it is a landed plan worktree:
|
|
957
1036
|
# removing it would pull the session's own cwd from under it
|
|
958
|
-
|
|
1037
|
+
pruning = prune_worktrees(root, keep=repo_root(os.getcwd()))
|
|
1038
|
+
for line in pruning:
|
|
959
1039
|
print(line)
|
|
1040
|
+
kept = [line for line in pruning
|
|
1041
|
+
if line.startswith(("worktree kept:", "branch kept:"))]
|
|
1042
|
+
if kept:
|
|
1043
|
+
# the reason a checkout stayed goes on the record: printed once, the
|
|
1044
|
+
# next session re-derived the same decision. Written after the move -
|
|
1045
|
+
# the record is in done/ now - and, like the other body writers, this
|
|
1046
|
+
# one commits nothing (plan 108)
|
|
1047
|
+
for line in write_fields(target, worktrees_kept=kept, updated=today()):
|
|
1048
|
+
print(line)
|
|
960
1049
|
return 0 if committed else 1
|
|
961
1050
|
|
|
962
1051
|
|
|
@@ -1012,7 +1101,41 @@ def commit_close(root, name, message):
|
|
|
1012
1101
|
# a test holds a cache.
|
|
1013
1102
|
REGENERABLE = frozenset(("__pycache__", ".pytest_cache", ".ruff_cache",
|
|
1014
1103
|
".mypy_cache", "node_modules", ".venv", "venv",
|
|
1015
|
-
".DS_Store"))
|
|
1104
|
+
".DS_Store", ".codegraph"))
|
|
1105
|
+
|
|
1106
|
+
|
|
1107
|
+
def _only_copy(path, rel):
|
|
1108
|
+
"""The first file under the ignored entry `rel` of worktree `path` that
|
|
1109
|
+
nothing regenerates, or None. tezgah's own agent files (`tezgah-*`, carrying
|
|
1110
|
+
the managed marker) are written again at session start (plan 102); a file
|
|
1111
|
+
beside them is the user's."""
|
|
1112
|
+
from tezgah_agents import MARKER
|
|
1113
|
+
full = os.path.join(path, rel)
|
|
1114
|
+
unread = [] # a directory the walk cannot list holds what it cannot tell
|
|
1115
|
+
walk = ([(os.path.dirname(full), [], [os.path.basename(full)])]
|
|
1116
|
+
if not os.path.isdir(full) or os.path.islink(full)
|
|
1117
|
+
else os.walk(full, onerror=lambda exc: unread.append(exc.filename)))
|
|
1118
|
+
# lazily, stopping at the first file nobody regenerates: an ignored build
|
|
1119
|
+
# tree is walked only as far as its first entry (review of plan 102)
|
|
1120
|
+
for d, dirs, names in walk:
|
|
1121
|
+
dirs.sort()
|
|
1122
|
+
# a link to a directory is a file os.walk does not descend: keep it
|
|
1123
|
+
for name in sorted(names) + [x for x in dirs if os.path.islink(os.path.join(d, x))]:
|
|
1124
|
+
entry = os.path.join(d, name)
|
|
1125
|
+
managed = False
|
|
1126
|
+
# the type before any open: a FIFO blocks open() until a writer comes
|
|
1127
|
+
if name.startswith("tezgah-") and os.path.isfile(entry) \
|
|
1128
|
+
and not os.path.islink(entry):
|
|
1129
|
+
try:
|
|
1130
|
+
with open(entry, encoding="utf-8", errors="replace") as fh:
|
|
1131
|
+
managed = MARKER in fh.read(4096)
|
|
1132
|
+
except OSError:
|
|
1133
|
+
pass
|
|
1134
|
+
if not managed:
|
|
1135
|
+
return os.path.relpath(entry, path)
|
|
1136
|
+
if unread:
|
|
1137
|
+
return os.path.relpath(unread[0], path)
|
|
1138
|
+
return os.path.relpath(unread[0], path) if unread else None
|
|
1016
1139
|
|
|
1017
1140
|
|
|
1018
1141
|
def _landed(root, branch, base):
|
|
@@ -1041,10 +1164,16 @@ def _keeps(root, path, branch, bases, agents):
|
|
|
1041
1164
|
return "uncommitted changes"
|
|
1042
1165
|
for line in (git_out(path, "status", "--porcelain", "-unormal", "--ignored") or "").splitlines():
|
|
1043
1166
|
rel = line[3:].strip('"').rstrip("/")
|
|
1044
|
-
if rel
|
|
1045
|
-
|
|
1167
|
+
if rel == ".tezgah" or REGENERABLE & set(rel.split("/")):
|
|
1168
|
+
continue
|
|
1169
|
+
kept = _only_copy(path, rel)
|
|
1170
|
+
if kept:
|
|
1171
|
+
return "ignored file %s may be the only copy" % kept
|
|
1046
1172
|
ws = os.path.join(path, ".tezgah")
|
|
1047
|
-
|
|
1173
|
+
# a link to the repository's one workspace (plans 098 and 100) is no data of
|
|
1174
|
+
# the worktree's own: removing the worktree drops only the link
|
|
1175
|
+
if os.path.isdir(ws) and os.path.realpath(ws) != os.path.realpath(
|
|
1176
|
+
os.path.join(root, ".tezgah")):
|
|
1048
1177
|
if set(os.listdir(ws)) - {".git"}:
|
|
1049
1178
|
return "its .tezgah holds data"
|
|
1050
1179
|
if os.path.isdir(os.path.join(ws, ".git")) and git_out(ws, "rev-parse", "-q", "--verify", "HEAD"):
|
|
@@ -1081,7 +1210,10 @@ def drop_branch(root, branch):
|
|
|
1081
1210
|
return None
|
|
1082
1211
|
done = subprocess.run(["git", "-C", root, "branch", "-D", branch],
|
|
1083
1212
|
capture_output=True, text=True)
|
|
1084
|
-
|
|
1213
|
+
# Orca deletes the branch of a checkout it removes: one gone by the time
|
|
1214
|
+
# `-D` ran was deleted, not kept (review of plan 104)
|
|
1215
|
+
if done.returncode == 0 or not git_out(root, "rev-parse", "-q", "--verify",
|
|
1216
|
+
"refs/heads/" + branch):
|
|
1085
1217
|
return None
|
|
1086
1218
|
return ("branch kept: %s (`git branch -D %s` refused: %s)"
|
|
1087
1219
|
% (branch, branch, ((done.stderr or "").strip().splitlines() or ["?"])[0]))
|
|
@@ -1127,10 +1259,18 @@ def prune_worktrees(root, keep=None):
|
|
|
1127
1259
|
gone = tezgah_orca.run(["worktree", "rm", "--worktree", "path:" + path],
|
|
1128
1260
|
timeout=60) is not None
|
|
1129
1261
|
how = "orca worktree rm --worktree path:%s" % path
|
|
1262
|
+
# Orca answers before the directory is gone, and git refuses to
|
|
1263
|
+
# drop a branch a checkout on disk still holds (plan 104)
|
|
1264
|
+
deadline = time.monotonic() + 10
|
|
1265
|
+
while gone and os.path.exists(path) and time.monotonic() < deadline:
|
|
1266
|
+
time.sleep(0.1)
|
|
1130
1267
|
else:
|
|
1131
1268
|
gone = git_out(root, "worktree", "remove", path) is not None
|
|
1132
1269
|
how = "git worktree remove %s" % path
|
|
1133
1270
|
why = None if gone else "`%s` failed; run it and report its error" % how
|
|
1271
|
+
if gone and os.path.exists(path):
|
|
1272
|
+
why = ("`%s` answered but the checkout is still on disk after 10 s; "
|
|
1273
|
+
"run close again" % how)
|
|
1134
1274
|
out.append("worktree %s: %s (%s)" % ("kept" if why else "removed", path,
|
|
1135
1275
|
why or branch))
|
|
1136
1276
|
if why is None:
|
|
@@ -1601,6 +1741,74 @@ def cmd_slice(args):
|
|
|
1601
1741
|
return misuse(usage)
|
|
1602
1742
|
|
|
1603
1743
|
|
|
1744
|
+
def orphan_phases(root):
|
|
1745
|
+
"""[(path, id, branch)] for phased open plans whose checkout branch is gone.
|
|
1746
|
+
Plan-add declares a branch before creating it, so absence alone is not an
|
|
1747
|
+
orphan: checkpoint, landed or reviewed_sha must show the phase ran in a
|
|
1748
|
+
checkout. A failed git query proves nothing. Query the current refs rather
|
|
1749
|
+
than this sweep's deletions: a checkout may have been removed by hand."""
|
|
1750
|
+
listed = git_out(root, "for-each-ref", "--format=%(refname)", "refs/heads")
|
|
1751
|
+
if listed is None:
|
|
1752
|
+
return []
|
|
1753
|
+
branches = set(listed.splitlines())
|
|
1754
|
+
out = []
|
|
1755
|
+
for path in plans(root):
|
|
1756
|
+
fields = tt.frontmatter(read(path))
|
|
1757
|
+
phase, branch = fields.get("phase") or "", fields.get("branch") or ""
|
|
1758
|
+
if not phase or not branch or not any(fields.get(key) for key in
|
|
1759
|
+
("checkpoint", "landed", "reviewed_sha")):
|
|
1760
|
+
continue
|
|
1761
|
+
if "refs/heads/" + branch not in branches:
|
|
1762
|
+
out.append((path, fields.get("id") or os.path.basename(path).split("-")[0],
|
|
1763
|
+
branch))
|
|
1764
|
+
return out
|
|
1765
|
+
|
|
1766
|
+
|
|
1767
|
+
def cmd_sweep(args):
|
|
1768
|
+
"""One idempotent pass over what a session left behind: every pending slice
|
|
1769
|
+
as one line naming its `slice resume` command, the landed worktrees and
|
|
1770
|
+
branches `reconcile --prune` removes, and a `phase:` whose branch is gone. It
|
|
1771
|
+
prints one line per change and closes with the count, so a second run over the
|
|
1772
|
+
same tree reports `0 change(s)`. The pending slices are printed, never
|
|
1773
|
+
touched: resuming one is the session's move, and closing it here would be the
|
|
1774
|
+
forgetting this exists to stop."""
|
|
1775
|
+
if args:
|
|
1776
|
+
return misuse("sweep takes nothing")
|
|
1777
|
+
# the shared checkout: the worktrees and branches this prunes live in the
|
|
1778
|
+
# repository, not in whichever worktree the command ran from
|
|
1779
|
+
root = main_root(repo_root(os.getcwd()))
|
|
1780
|
+
rows = tt.pending(root)
|
|
1781
|
+
for rec in rows:
|
|
1782
|
+
# one line each: the full brief is `slice resume <id>`'s, and a session
|
|
1783
|
+
# coming back to ten dead slices wants the list, not ten briefs
|
|
1784
|
+
print(tt.slice_line(rec))
|
|
1785
|
+
if not rows:
|
|
1786
|
+
print("no open or stopped slice")
|
|
1787
|
+
changed = 0
|
|
1788
|
+
bases = landing_bases(root)
|
|
1789
|
+
for line in prune_worktrees(root, keep=repo_root(os.getcwd())) + prune_branches(root, bases):
|
|
1790
|
+
print(line)
|
|
1791
|
+
# a "kept" line is a report, not a change: a tree holding one long-lived
|
|
1792
|
+
# worktree still has to reach `0 change(s)` on the second run. A slice a
|
|
1793
|
+
# removed worktree closed is one too (review of plan 108).
|
|
1794
|
+
if line.startswith(("worktree removed", "branch deleted", "slice ")):
|
|
1795
|
+
changed += 1
|
|
1796
|
+
for path, ident, branch in orphan_phases(root):
|
|
1797
|
+
# the same empty-value shape `stop` writes, so one parser reads both, and
|
|
1798
|
+
# the same wrapper the other record writers use: a plan that cannot be
|
|
1799
|
+
# written is a problem with the plan, not a traceback (review of plan 108)
|
|
1800
|
+
bad = write_fields(path, phase="", updated=today())
|
|
1801
|
+
if bad:
|
|
1802
|
+
for problem in bad:
|
|
1803
|
+
print("FAIL %s" % problem)
|
|
1804
|
+
continue
|
|
1805
|
+
print("phase cleared: %s (%s no longer exists, so no worktree can move it)"
|
|
1806
|
+
% (ident, branch))
|
|
1807
|
+
changed += 1
|
|
1808
|
+
print("swept: %d change(s)" % changed)
|
|
1809
|
+
return 0
|
|
1810
|
+
|
|
1811
|
+
|
|
1604
1812
|
def main(argv):
|
|
1605
1813
|
if len(argv) < 2:
|
|
1606
1814
|
print(__doc__, file=sys.stderr)
|
|
@@ -1626,10 +1834,14 @@ def main(argv):
|
|
|
1626
1834
|
return cmd_note(rest)
|
|
1627
1835
|
if cmd == "spike":
|
|
1628
1836
|
return cmd_spike(rest)
|
|
1837
|
+
if cmd == "unmet":
|
|
1838
|
+
return cmd_unmet(rest)
|
|
1629
1839
|
if cmd == "slice":
|
|
1630
1840
|
return cmd_slice(rest)
|
|
1841
|
+
if cmd == "sweep":
|
|
1842
|
+
return cmd_sweep(rest)
|
|
1631
1843
|
return misuse("unknown command %r (start, phase, allow, stop, status, review, "
|
|
1632
|
-
"close, tick, note, spike, slice)" % cmd)
|
|
1844
|
+
"close, tick, note, spike, unmet, slice, sweep)" % cmd)
|
|
1633
1845
|
|
|
1634
1846
|
|
|
1635
1847
|
class ReaderMayLeave:
|
package/bin/tezgah-taste
CHANGED
|
@@ -305,7 +305,9 @@ def mine_session(events):
|
|
|
305
305
|
|
|
306
306
|
|
|
307
307
|
def cmd_mine(args):
|
|
308
|
-
|
|
308
|
+
# sessions run in checkouts, never in a bare repository's git directory
|
|
309
|
+
main = tezgah_paths.linked_main(os.getcwd())
|
|
310
|
+
here = ((main and tezgah_paths.work_tree(main)) or repo_root(os.getcwd())
|
|
309
311
|
or os.getcwd())
|
|
310
312
|
root = os.path.abspath(os.path.expanduser(
|
|
311
313
|
args.root or ("~/Projects" if args.all_projects else here)))
|
package/docs/architecture.md
CHANGED
|
@@ -187,6 +187,14 @@ the `tezgah-research import` that moves its lines (`hooks/tezgah_context.py::sib
|
|
|
187
187
|
Session start in a linked worktree makes its `.tezgah` a link to the main one
|
|
188
188
|
(`hooks/tezgah_paths.py::link_workspace`). A relative `.tezgah/...` write lands
|
|
189
189
|
there, and `/.tezgah` goes to the repository's `info/exclude`.
|
|
190
|
+
A bare repository (`app.git`, or `app/.bare` with its worktrees beside it) has no
|
|
191
|
+
main checkout. Its workspace is `<bare git dir>/.tezgah`, a private repository in
|
|
192
|
+
the git directory itself, with no `.gitignore` written there
|
|
193
|
+
(`hooks/tezgah_paths.py::linked_main`, `hooks/tezgah_paths.py::ensure_workspace`).
|
|
194
|
+
Every worktree's `.tezgah` links to it. No `git worktree remove`, detach or
|
|
195
|
+
switch in any worktree takes it, and a plan worktree reads its plan there.
|
|
196
|
+
The git directory holds no project file. A checkpoint, a cited file or an arena
|
|
197
|
+
evaluator is read in the worktree the command runs in (`hooks/tezgah_paths.py::work_tree`).
|
|
190
198
|
A second clone is another repository, with a workspace of its own.
|
|
191
199
|
A path inside `.tezgah` belongs to the repository around it, never to `.tezgah`.
|
|
192
200
|
The workspace keeps its own `.git`, so git's top-level answers `.tezgah` there.
|
package/docs/environment.md
CHANGED
|
@@ -78,7 +78,7 @@ the kill switches are files, not variables, and live in
|
|
|
78
78
|
| `TYPESAFE_API_KEY` | `hooks/tezgah_judge.py::key`, `hooks/tezgah_paths.py` | `~/.config/typesafe/key` | The judgement seam's first third-party credential, used as `fallback` in `config.json` allows ([judge](judge.md)); omp also reads it for its own judge. |
|
|
79
79
|
| `OPENROUTER_API_KEY` | `hooks/tezgah_judge.py::openrouter_key`, `bin/consult`, `bin/codegen`, `hooks/tezgah_models.py` | `~/.config/openrouter/key` | The seam's chat fallback, consult's and codegen's `openrouter` provider, and omp's `any` family. |
|
|
80
80
|
| `DEEPSEEK_API_KEY`, `INCEPTION_API_KEY` | `bin/consult`, `bin/codegen`, `hooks/tezgah_replay.py::label_model` | `~/.config/deepseek/key`, `~/.config/inception/key` | The `deepseek` and `inception` providers of consult and codegen; `DEEPSEEK_API_KEY` alone is also the replay sheet's `--provider deepseek` rater. |
|
|
81
|
-
| `ANTHROPIC_OAUTH_TOKEN`, `ANTHROPIC_API_KEY`, `ZAI_API_KEY` | `hooks/tezgah_models.py::OMP_AUTH` | omp's own auth store | Read as presence only: whether omp can run the `anthropic` or `
|
|
81
|
+
| `ANTHROPIC_OAUTH_TOKEN`, `ANTHROPIC_API_KEY`, `ZAI_API_KEY`, `OPENAI_CODEX_OAUTH_TOKEN` | `hooks/tezgah_models.py::OMP_AUTH` | omp's own auth store | Read as presence only: whether omp can run the `anthropic`, `zai` or `openai` model family, so the model table can pick a row omp will run. |
|
|
82
82
|
| `TEZGAH_TYPESAFE_URL` | `hooks/tezgah_judge.py::endpoint` | `https://api.typesafe.ai/v1/systemone` | Repoints the seam's TypeSafe endpoint (tests). Plain `http` is refused unless the host is this machine; a cross-host redirect is refused. |
|
|
83
83
|
| `TEZGAH_OPENROUTER_URL` | `hooks/tezgah_judge.py::openrouter_url` | OpenRouter's chat-completions URL | Repoints the seam's chat fallback (tests). Plain `http` is refused unless the host is this machine. |
|
|
84
84
|
| `TEZGAH_DEEPSEEK_URL` | `hooks/tezgah_replay.py::_deepseek` | `https://api.deepseek.com/chat/completions` | Repoints the replay sheet's `--provider deepseek` rater (tests). Plain `http` is refused unless the host is this machine. |
|
package/docs/evidence.md
CHANGED
|
@@ -126,7 +126,7 @@ a write tool is `edit`, a shell call is `verify` when its command matches the ch
|
|
|
126
126
|
| `refusal` | `stop_reason`, for a blocked reply that claimed nothing - a work-only or a shape refusal - with the same `blocked: <class>` detail (row version 3) | `counters.refusals`, and `shape_blocked` for a shape class; never a claim; `NOT_TOOL_HOOK` lists it |
|
|
127
127
|
| `after_block` | `hooks/tezgah_integrity.py::stop_reason`, on a reply after this rule's own block (a turn holding a `blocked:` claim or refusal row) that is not refused again (`_record_only`) on Claude, Codex, Cursor and omp; `detail` is `would block: <class>`, `ok` or `no claim` | nothing yet: a record for the after-block observation window, not a claim and not a second `shape` row, so `counters` counts it neither as a claim nor as a reply; `NOT_TOOL_HOOK` lists it |
|
|
128
128
|
| `subagent_end` | `stop_reason(..., subagent=True)`, from Claude's SubagentStop (`hooks/hooks.json`) and Cursor's subagentStop `summary`; the evidence half only, never a block (ADR 011); `detail` as `after_block`'s | nothing yet: the rows plan 055 replays before a subagent end may block; `NOT_TOOL_HOOK` lists it. On Claude a subagent's tool rows sit in the parent's ledger, so until the agent key (plan 050) lands the row judges the parent's turn |
|
|
129
|
-
| `stop_spec` |
|
|
129
|
+
| `stop_spec` | nothing since plan 101. Plan 063's shadow wrote one per reply that left a claim or a refusal row: `agree`, `disagree` or `error`, and `fold` or `selector`. The fold it compared against is gone | nothing. `NOT_TOOL_HOOK` still lists it and the replay keeps it beside its claim (`hooks/tezgah_replay.py::SIDE_KINDS`), so an old ledger reads as it did |
|
|
130
130
|
| `deny`, `nudge` | the [gate](gate.md)'s `hooks/tezgah_gate.py::_deny`, first-nudge `hooks/tezgah_gate.py::decision` | `hooks/tezgah_integrity.py::counters` |
|
|
131
131
|
| `snapshot`, `rollback` | `hooks/tezgah_snapshot.py::_capture_one`, `hooks/tezgah_snapshot.py::restore` | `hooks/tezgah_integrity.py::_snapshot_hash`; no counter |
|
|
132
132
|
| `compact` | `hooks/tezgah_integrity.py::note_compaction`, from the post-compaction path (`tezgah_context.remember_compaction` `hooks/tezgah_context.py::remember_compaction`) | `hooks/tezgah_integrity.py::_counts` (what `counters` folds with) |
|
|
@@ -321,7 +321,7 @@ The first four judge how the reply is written (`_shape_block`, `hooks/tezgah_int
|
|
|
321
321
|
|
|
322
322
|
The four shape classes share their switches with the text they enforce: `adhd-off` or a repo's `.no-adhd` lifts 1–3 (`_adhd_armed`, `hooks/tezgah_integrity.py::_adhd_armed`), `exec-mode.off` lifts 4, and a session whose environment carries `TEZGAH_NESTED` (an agent CLI consult started, whose English answer is read by code) is not judged for shape at all. A subagent's reply is never refused: omp's `session_stop` does not fire for task sessions, and a Claude SubagentStop or Cursor subagentStop leaves a record-only `subagent_end` row judged on the evidence half alone (ADR 011). The thresholds were set on the owner's omp transcripts (2026-09-27: 1,495 assistant replies with at least 25 prose words): English prose scored 0.000–0.026, the most English-heavy Turkish reply 0.077 and the bulk 0.3–0.7, and 24 of the 1,495 fall under the cut, all English or Chinese prose. The trade-off is on the Turkish side: a Turkish reply that is mostly quoted English outside backticks can fall under 6%, and the block text tells the model to put the English in backticks or a fence. `tests/test_integrity.py` `ReplyShapeCorpus` pins which realistic replies pass and which block. A lead that announces what follows (`preamble-open`) stays report-only: "Sonuç:" over a list is an answer label and reads like a preamble to any regex.
|
|
323
323
|
|
|
324
|
-
5. **check failed** — the newest check in the session failed (`hooks/
|
|
324
|
+
5. **check failed** — the newest check in the session failed (`hooks/tezgah_stopspec.py::verdict`).
|
|
325
325
|
6. **partial failure** — this turn recorded a `verify_fail` and nothing passed since (`hooks/tezgah_integrity.py::_partial_state`).
|
|
326
326
|
7. **no ui_ok** — this turn changed a UI source (`hooks/tezgah_integrity.py::UI_PATH`) and the
|
|
327
327
|
check that passed was not one that sees the screen: a unit run never does. A browser/e2e/visual
|
|
@@ -330,7 +330,7 @@ The four shape classes share their switches with the text they enforce: `adhd-of
|
|
|
330
330
|
CLI capture) is the evidence this class asks for, and it has to be newer than
|
|
331
331
|
the UI write it is about - not than the newest write of anything, so an unrelated file written
|
|
332
332
|
after the screen read does not stale that read (`hooks/tezgah_integrity.py::_ui_evidence`,
|
|
333
|
-
branch `hooks/
|
|
333
|
+
branch `hooks/tezgah_stopspec.py::verdict`). The write kinds are the freshness fold's own (`_change_row`: a write tool's
|
|
334
334
|
`edit` row or a shell call's `run` row the gate saw change the tree), so a UI source written
|
|
335
335
|
through a redirect owes the same proof. The MCP half of that proof is read off the row's own
|
|
336
336
|
tool name (`hooks/tezgah_integrity.py::_screen_read`), which is the only field that
|
|
@@ -342,7 +342,7 @@ The four shape classes share their switches with the text they enforce: `adhd-of
|
|
|
342
342
|
under a component/views/widgets
|
|
343
343
|
directory, or a name that carries the convention on its own), a fresh read of the screen is not
|
|
344
344
|
enough by itself and a `tezgah-design check` row has to be newer than the component write it
|
|
345
|
-
judges (`hooks/tezgah_integrity.py::_design_evidence`, branch `hooks/
|
|
345
|
+
judges (`hooks/tezgah_integrity.py::_design_evidence`, branch `hooks/tezgah_stopspec.py::verdict`) - and it has
|
|
346
346
|
to be a check whose pass was seen (`passing_check`, `hooks/tezgah_integrity.py::passing_check`), because
|
|
347
347
|
a screenshot says what a component looks like and the contract is the only thing that says whether
|
|
348
348
|
it is on the repository's floor. The refusal names the command to run. Both of the checker's verbs
|
|
@@ -351,7 +351,7 @@ The four shape classes share their switches with the text they enforce: `adhd-of
|
|
|
351
351
|
it. A screen is unchanged: the page a person looks at owes the read, not the contract.
|
|
352
352
|
8. **stale evidence** — the newest check that passed ran before the newest write the gate saw change
|
|
353
353
|
the tree, so it verified an earlier revision of it (`_last_pass`/`_last_change`/`_stale_paths`
|
|
354
|
-
`hooks/tezgah_integrity.py::_last_pass`, `hooks/tezgah_integrity.py::_last_change`, `hooks/tezgah_integrity.py::_stale_paths`, branch `hooks/
|
|
354
|
+
`hooks/tezgah_integrity.py::_last_pass`, `hooks/tezgah_integrity.py::_last_change`, `hooks/tezgah_integrity.py::_stale_paths`, branch `hooks/tezgah_stopspec.py::verdict`). A write counts as a change whether the gate saw it as a
|
|
355
355
|
write tool or as a shell command that redirected into the file - `_change_row` (`hooks/tezgah_integrity.py::_change_row`) reads
|
|
356
356
|
an `edit` row, a `run` row whose captured target moved, or a formatter's write mode - while a `verify*` row never does, so
|
|
357
357
|
a check redirecting its own log cannot stale itself. A write *outside* the workspace is not one
|
|
@@ -365,10 +365,14 @@ The four shape classes share their switches with the text they enforce: `adhd-of
|
|
|
365
365
|
change here. A row with no `repo` binds to nothing. That covers an old row and a host that sent
|
|
366
366
|
no cwd. A change with no `target`, such as a shell write, binds to nothing too.
|
|
367
367
|
9. **no verify_ok** — this turn recorded a step (`edit`, `verify`, `verify_fail`, `run`,
|
|
368
|
-
`interrupted`) and no check passed in the session (`hooks/
|
|
368
|
+
`interrupted`) and no check passed in the session (`hooks/tezgah_stopspec.py::verdict`). A turn with no step at all
|
|
369
369
|
has only its words as the trigger. There a claim word inside a question or under a negation in
|
|
370
370
|
its own clause is no claim (`asserted_claims`). Examples: "testler geçti mi?", "is it done?",
|
|
371
371
|
"not tested yet", "tamamlandı değil". "Tamamlandı, push edeyim mi?" still claims.
|
|
372
|
+
A done or tested claim over a check nobody saw pass owes this class too, as a lost call does.
|
|
373
|
+
That is a `verify_ok` row that is no `passing_check`, such as exit 0 with nothing returned. A
|
|
374
|
+
work-free turn of one such row used to end on "Tamamlandı, tüm testler geçti" (plan 101,
|
|
375
|
+
`hooks/tezgah_integrity.py::_stop_block`). A reply with no claim word still ends there.
|
|
372
376
|
A prose edit is no step and no change (`hooks/tezgah_integrity.py::_prose_edit`,
|
|
373
377
|
`hooks/tezgah_integrity.py::_work_row`). Such an `edit` row has a real `target` ending in
|
|
374
378
|
`.md`, `.markdown`, `.rst` or `.adoc`. It owes no check and does not stale a pass. An agent
|
|
@@ -379,7 +383,7 @@ The four shape classes share their switches with the text they enforce: `adhd-of
|
|
|
379
383
|
10. **no external read** — the reply states the state of a system tezgah does not own — a registry,
|
|
380
384
|
a release, a tag, a formula, a CI run — and no read of that system ran in the same turn
|
|
381
385
|
(`hooks/tezgah_integrity.py::_external_claim`, over `EXTERNAL_SYSTEM`/`EXTERNAL_STATE`
|
|
382
|
-
`hooks/tezgah_integrity.py::EXTERNAL_SYSTEM`, `hooks/tezgah_integrity.py::EXTERNAL_STATE` and the CI pair `EXTERNAL_CI`/`EXTERNAL_CI_STATE` `hooks/tezgah_integrity.py::EXTERNAL_CI`, `hooks/tezgah_integrity.py::EXTERNAL_CI_STATE`; branch `hooks/
|
|
386
|
+
`hooks/tezgah_integrity.py::EXTERNAL_SYSTEM`, `hooks/tezgah_integrity.py::EXTERNAL_STATE` and the CI pair `EXTERNAL_CI`/`EXTERNAL_CI_STATE` `hooks/tezgah_integrity.py::EXTERNAL_CI`, `hooks/tezgah_integrity.py::EXTERNAL_CI_STATE`; branch `hooks/tezgah_stopspec.py::verdict`).
|
|
383
387
|
The two halves have to sit within `EXTERNAL_GAP` = 45 characters of each other on one line
|
|
384
388
|
(`hooks/tezgah_integrity.py::_external_pair`), and both are read on the reply's prose with inline code, paths,
|
|
385
389
|
URLs and identifiers blanked: `brew tap` inside a code span, the `.github/workflows/...` in a
|
|
@@ -525,27 +529,26 @@ The four shape classes share their switches with the text they enforce: `adhd-of
|
|
|
525
529
|
transcripts. It refuses once per turn and counts like `residual work`
|
|
526
530
|
(`hooks/tezgah_integrity.py::LIST_BLOCKS`).
|
|
527
531
|
|
|
528
|
-
|
|
532
|
+
A past-time temporal spec decides classes 5–10 (`hooks/tezgah_stopspec.py::FORMULAS`,
|
|
529
533
|
plan 063). Seven formulas read ten row atoms (`ROW_ATOMS`) and the turn marker. The module compiles
|
|
530
534
|
each formula to a one-pass monitor over the rows (`hooks/tezgah_stopspec.py::compile_table`), and the
|
|
531
|
-
order above is its decision list (`ORDER`). The spec
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
refuses "evidence tampered". The admission below and class 11
|
|
537
|
-
|
|
535
|
+
order above is its decision list (`ORDER`, walked by `hooks/tezgah_stopspec.py::verdict`). The spec
|
|
536
|
+
ran in shadow beside the imperative fold until it agreed on every Stop that reached the fold.
|
|
537
|
+
Plan 101 then made it the fold and deleted the imperative one. `_stop_block` calls it as its
|
|
538
|
+
evidence fold (`hooks/tezgah_stopspec.py::fold`), and `hooks/tezgah_integrity.py::_evidence_text`
|
|
539
|
+
words the class it picks with the refusal text. `_stop_block` stays the selector. It picks the
|
|
540
|
+
rows, runs the lost-began path and refuses "evidence tampered". The admission below and class 11
|
|
541
|
+
belong to the selector too. `ESCAPES` names the three predicates that read beyond one row: the
|
|
538
542
|
began-row join, the repository binding and the lost-began set. Plan 063 fixed that list before
|
|
539
|
-
the first shadow row. `
|
|
540
|
-
|
|
541
|
-
the live rows, the added latency, and generated mutants of the table.
|
|
543
|
+
the first shadow row. `tests/stop_spec_eval.py --mutants` mutates the table and runs the in-process
|
|
544
|
+
Stop tests against each mutant.
|
|
542
545
|
|
|
543
546
|
The trigger for 5–9 is the turn's own evidence, not its words: a turn that did work and never saw a
|
|
544
547
|
check pass is refused whatever the reply says (`hooks/tezgah_integrity.py::_stop_block`). Two cases are judged against the
|
|
545
548
|
whole session instead of the turn:
|
|
546
549
|
|
|
547
550
|
- **A claim in a turn with no work.** A done/tested claim made in a turn that recorded no work row
|
|
548
|
-
is judged by the same fold as a turn (
|
|
551
|
+
is judged by the same fold as a turn (the spec's), run over the session rows before
|
|
549
552
|
this turn (`_before_turn`): every class the turn fold has - `check failed`, `no verify_ok`,
|
|
550
553
|
`stale evidence`, a pending UI or design check, a partial failure - applies to the session too.
|
|
551
554
|
A session that did no work at all still ends, and a turn that ran its own passing check is
|
|
@@ -575,7 +578,7 @@ whole session instead of the turn:
|
|
|
575
578
|
|
|
576
579
|
What lets the turn end is the absence
|
|
577
580
|
of both — no work row and no claim vocabulary (`hooks/tezgah_integrity.py::_stop_block`), the one shape class 10 is exempt from
|
|
578
|
-
— or a `passing_check` row newer than the newest write seen to change the tree (`hooks/
|
|
581
|
+
— or a `passing_check` row newer than the newest write seen to change the tree (`hooks/tezgah_stopspec.py::verdict`), where
|
|
579
582
|
a fresh screen proof stands in the same
|
|
580
583
|
place as a unit pass; when that write was a UI source, the passing row has to be a UI proof — a
|
|
581
584
|
check that renders, or a read of the screen — and when it was a component, the design check has to be
|