@r1z4x/tezgah 1.11.0 → 1.13.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.
Files changed (60) hide show
  1. package/CHANGELOG.md +333 -1
  2. package/MANIFEST +22 -0
  3. package/README.md +4 -3
  4. package/bin/tezgah-docs +1 -1
  5. package/bin/tezgah-research +39 -4
  6. package/bin/tezgah-setup +1229 -22
  7. package/bin/tezgah-task +55 -17
  8. package/docs/citations-baseline.json +1 -1
  9. package/docs/clarity-baseline.json +9 -3
  10. package/docs/contract.md +6 -3
  11. package/docs/environment.md +3 -2
  12. package/docs/evidence.md +81 -8
  13. package/docs/gate.md +6 -2
  14. package/docs/glossary.md +1 -1
  15. package/docs/hosts.md +297 -15
  16. package/docs/layers.md +1 -1
  17. package/docs/models.md +6 -0
  18. package/docs/operations.md +16 -5
  19. package/docs/research.md +36 -10
  20. package/docs/status-line.md +6 -0
  21. package/docs/testing.md +3 -1
  22. package/hooks/hooks.json +1 -1
  23. package/hooks/lang/en.json +11 -0
  24. package/hooks/lang/tr.json +11 -0
  25. package/hooks/projects-stop.py +2 -1
  26. package/hooks/tezgah_agents.py +27 -3
  27. package/hooks/tezgah_attest.py +79 -10
  28. package/hooks/tezgah_context.py +21 -8
  29. package/hooks/tezgah_gate.py +79 -23
  30. package/hooks/tezgah_integrity.py +212 -38
  31. package/hooks/tezgah_paths.py +43 -2
  32. package/hooks/tezgah_policy.py +35 -13
  33. package/hooks/tezgah_replay.py +8 -2
  34. package/hooks/tezgah_research.py +167 -24
  35. package/hooks/tezgah_stack.py +236 -1
  36. package/hooks/tezgah_task.py +2 -1
  37. package/hosts/amp/hook.py +217 -0
  38. package/hosts/amp/tezgah-plugin.ts.in +386 -0
  39. package/hosts/antigravity/hook.py +255 -0
  40. package/hosts/codex/hook.py +2 -1
  41. package/hosts/commandcode/hook.py +239 -0
  42. package/hosts/copilot/hook.py +284 -0
  43. package/hosts/cursor/hook.py +2 -1
  44. package/hosts/droid/hook.py +259 -0
  45. package/hosts/dsh/hooks.json +1 -1
  46. package/hosts/gemini/hook.py +213 -0
  47. package/hosts/goose/hook.py +145 -0
  48. package/hosts/omp/hook.py +4 -1
  49. package/hosts/omp/tezgah-hook.ts.in +30 -7
  50. package/hosts/opencode/plugins/tezgah.js +2 -2
  51. package/hosts/qwen/hook.py +206 -0
  52. package/output-styles/tezgah.md +10 -5
  53. package/package.json +1 -1
  54. package/skills/harness/SKILL.md +2 -0
  55. package/skills/i-have-adhd/SKILL.md +10 -1
  56. package/skills/plan-add/SKILL.md +5 -1
  57. package/skills/research/SKILL.md +22 -9
  58. package/skills/tezgah-contract/SKILL.md +19 -6
  59. package/statusline.py +20 -9
  60. package/workflows/graph-review.js +14 -2
package/bin/tezgah-task CHANGED
@@ -6,7 +6,7 @@
6
6
  tezgah-task allow GLOB ... | --any-path --why REASON replace the allowlist, recording why
7
7
  tezgah-task stop deactivate (clear phase)
8
8
  tezgah-task status [--json] the active task, then every open plan: progress, what it waits on, what can run at once
9
- tezgah-task review <NNN|path> <reviewer> approve|changes record a fresh review's verdict
9
+ tezgah-task review <NNN|path> <reviewer> approve|changes [--no-slice WHY] record a fresh review's verdict; approve needs the reviewer's slice record, or WHY it has none
10
10
  tezgah-task close <NNN|path> done|discarded [--resolved TEXT] move a plan to .tezgah/plans/done, deactivate it, remove merged worktrees
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`
@@ -1082,7 +1082,7 @@ def budget_spent(path, ident):
1082
1082
  % (path, REVIEW_ROUNDS, ident, say()["decision"], ident))
1083
1083
 
1084
1084
 
1085
- def unfinished_review(root, reviewer, plan="", since=0):
1085
+ def unfinished_review(root, reviewer, plan="", since=0, no_slice="", ran_since=0):
1086
1086
  """The refusal of an `approve` while a slice journal record whose id or agent
1087
1087
  is `reviewer` is open, stopped or stale: a review slice that died on a
1088
1088
  provider limit had its approve recorded anyway, and a NameError it would have
@@ -1097,7 +1097,11 @@ def unfinished_review(root, reviewer, plan="", since=0):
1097
1097
  `tt.open_slice`) counts too. Under TEZGAH_SESSION only this session's
1098
1098
  records and unowned ones count, by id, agent or name alike: another
1099
1099
  session's review is not this approve's. The refusal names the exact id,
1100
- which `slice resume` and `slice done` take from any session."""
1100
+ which `slice resume` and `slice done` take from any session. No finished
1101
+ record changed at or after `ran_since` refuses too: a requested reviewer with
1102
+ no status did not run, and a plan-readiness review before the checkpoint, an
1103
+ earlier round's review or a `resumed` record with no re-run is not this
1104
+ review - unless `no_slice` says why the reviewer ran outside the journal."""
1101
1105
  from tezgah_paths import off
1102
1106
  if off("slices-off"):
1103
1107
  return []
@@ -1113,21 +1117,42 @@ def unfinished_review(root, reviewer, plan="", since=0):
1113
1117
  return rec["plan"] == plan
1114
1118
  return (_stamp(rec.get("updated")) or 0) >= since
1115
1119
 
1116
- return ["review slice %s is %s%s: let it finish, or resume it with `tezgah-task "
1117
- "slice resume %s` and wait for that run; if the review was finished "
1118
- "another way, record it with `tezgah-task slice done %s` - an approve "
1119
- "from a review that did not finish reads nothing"
1120
- % (rec["id"], rec.get("status"),
1121
- (" (%s)" % rec["failure"]) if rec.get("failure") else "",
1122
- rec["id"], rec["id"])
1123
- for rec in tt.slices(root)
1124
- if rec.get("status") in ("open", "stopped", "stale")
1125
- and named(rec) and ours(rec)]
1120
+ found = [rec for rec in tt.slices(root) if named(rec) and ours(rec)]
1121
+ refusals = ["review slice %s is %s%s: let it finish, or resume it with `tezgah-task "
1122
+ "slice resume %s` and wait for that run; if the review was finished "
1123
+ "another way, record it with `tezgah-task slice done %s` - an approve "
1124
+ "from a review that did not finish reads nothing"
1125
+ % (rec["id"], rec.get("status"),
1126
+ (" (%s)" % rec["failure"]) if rec.get("failure") else "",
1127
+ rec["id"], rec["id"])
1128
+ for rec in found if rec.get("status") in ("open", "stopped", "stale")]
1129
+ ran = [rec for rec in found if rec.get("status") == "done"
1130
+ and (_stamp(rec.get("updated")) or 0) >= ran_since]
1131
+ if not ran and not refusals and not no_slice:
1132
+ # a requested reviewer with no finished record since the work is a
1133
+ # review that did not run
1134
+ refusals.append(
1135
+ "no finished review slice names %s for this plan since its work began: "
1136
+ "a reviewer with no record did not run, and a readiness review, an "
1137
+ "earlier round or a resumed record is not this review. Spawn the fresh "
1138
+ "reviewer and approve once its slice is done; a host that keeps no "
1139
+ "slice journal records why with "
1140
+ "`--no-slice \"<why: the reviewer ran outside the slice journal>\"`"
1141
+ % reviewer)
1142
+ return refusals
1126
1143
 
1127
1144
 
1128
1145
  def cmd_review(args):
1146
+ no_slice = ""
1147
+ if "--no-slice" in args:
1148
+ i = args.index("--no-slice")
1149
+ no_slice = " ".join((args[i + 1:i + 2] or [""])[0].split())
1150
+ args = args[:i] + args[i + 2:]
1151
+ if not no_slice:
1152
+ return misuse("--no-slice takes why the reviewer ran outside the slice journal")
1129
1153
  if len(args) != 3 or args[2] not in VERDICTS:
1130
- return misuse("review takes <NNN|path> <reviewer> %s" % "|".join(VERDICTS))
1154
+ return misuse("review takes <NNN|path> <reviewer> %s [--no-slice WHY]"
1155
+ % "|".join(VERDICTS))
1131
1156
  path, problem = open_plan(args[0])
1132
1157
  if problem:
1133
1158
  return problems([problem])
@@ -1144,9 +1169,21 @@ def cmd_review(args):
1144
1169
  mark = (fields.get("checkpoint") or "").split()
1145
1170
  since = (git_out(root, "log", "-1", "--format=%ct", mark[-1])
1146
1171
  if mark and mark[-1] != tt.CHECKPOINT_PENDING else None)
1172
+ since = int(since) if (since or "").isdigit() else 0
1173
+ # this round's review finished after the first commit of the work it
1174
+ # reads: past the checkpoint in round one (a plan-readiness review ran
1175
+ # before it), past the commit round one read in round two (round one's
1176
+ # reviewer finished before it); with no such commit, after the base itself
1147
1177
  ident = fields.get("id") or os.path.basename(path).split("-")[0]
1148
- bad = unfinished_review(root, args[1], ident,
1149
- int(since) if (since or "").isdigit() else 0)
1178
+ base = fields.get("reviewed_sha") or (
1179
+ mark[-1] if mark and mark[-1] != tt.CHECKPOINT_PENDING else "")
1180
+ first = ""
1181
+ if base:
1182
+ after = (git_out(tt.plan_holders(root).get(ident) or root, "log",
1183
+ "--reverse", "--format=%ct", base + "..HEAD") or "").split()
1184
+ first = after[0] if after else git_out(root, "log", "-1", "--format=%ct", base)
1185
+ ran_since = max(since, int(first) if (first or "").isdigit() else 0)
1186
+ bad = unfinished_review(root, args[1], ident, since, no_slice, ran_since)
1150
1187
  if bad:
1151
1188
  return problems(bad)
1152
1189
  # a plan held by its own worktree is built on that branch: its HEAD is the
@@ -1154,7 +1191,8 @@ def cmd_review(args):
1154
1191
  ident = fields.get("id") or os.path.basename(path).split("-")[0]
1155
1192
  where = tt.plan_holders(root).get(ident) or root
1156
1193
  sha = (git_out(where, "rev-parse", "HEAD") or "")[:12]
1157
- record = {"review": "%s %s %s" % (args[1], args[2], today()),
1194
+ record = {"review": "%s %s %s%s" % (args[1], args[2], today(),
1195
+ " no-slice: " + no_slice if no_slice else ""),
1158
1196
  "review_round": str(int(prior) + 1 if prior.isdigit() else 1),
1159
1197
  "updated": today()}
1160
1198
  if sha:
@@ -27,7 +27,7 @@
27
27
  "unjudged": 12
28
28
  },
29
29
  "docs/hosts.md": {
30
- "unjudged": 62
30
+ "unjudged": 61
31
31
  },
32
32
  "docs/layers.md": {
33
33
  "unjudged": 14
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "CHANGELOG.md": {
3
- "long_sentences": 0,
3
+ "long_sentences": 1,
4
4
  "passive": 2,
5
5
  "replace_words": 0,
6
6
  "semicolons": 0
@@ -30,7 +30,7 @@
30
30
  "semicolons": 3
31
31
  },
32
32
  "docs/evidence.md": {
33
- "long_sentences": 108,
33
+ "long_sentences": 112,
34
34
  "passive": 55,
35
35
  "replace_words": 1,
36
36
  "semicolons": 39
@@ -54,7 +54,7 @@
54
54
  "semicolons": 14
55
55
  },
56
56
  "docs/hosts.md": {
57
- "long_sentences": 36,
57
+ "long_sentences": 38,
58
58
  "passive": 19,
59
59
  "replace_words": 0,
60
60
  "semicolons": 18
@@ -83,6 +83,12 @@
83
83
  "replace_words": 3,
84
84
  "semicolons": 42
85
85
  },
86
+ "docs/orca.md": {
87
+ "long_sentences": 0,
88
+ "passive": 0,
89
+ "replace_words": 0,
90
+ "semicolons": 0
91
+ },
86
92
  "docs/research.md": {
87
93
  "long_sentences": 105,
88
94
  "passive": 59,
package/docs/contract.md CHANGED
@@ -208,7 +208,10 @@ default, measured on two research lines with a written flip
208
208
  one also gets an OCR review spec: OCR's file selection, its rule groups and a
209
209
  refuter pass. The spec is input to the review, never a verdict
210
210
  ([operations](operations.md#opt-in-ocr-review-spec-ocr),
211
- `bin/tezgah-task::print_ocr_spec`).
211
+ `bin/tezgah-task::print_ocr_spec`). A verdict comes only from a fresh
212
+ reviewer. A host that cannot start one says so and asks the owner. The author
213
+ contests a confirmed finding only with a file:line the reviewer could read. It
214
+ never lowers a severity (`hooks/tezgah_policy.py::EXEC`).
212
215
 
213
216
  ## Composition order of the always-on core
214
217
 
@@ -256,7 +259,7 @@ applied, and it also returns the names of the switches that fired.
256
259
  - **Ponytail (minimal code).** Take the laziest rung that holds (YAGNI → reuse → stdlib → platform → installed dependency → one line), never simplify away validation, error handling or security. A non-default level rides the reminder (`hooks/tezgah_context.py::_pony_level_line`).
257
260
  - **Output shape: ADHD-friendly.** The action or answer is the first line, multi-step work is a numbered list whose position is restated, an estimate is in concrete units. Two upstream rules were rewritten rather than imported verbatim because they collided with rules already in force: the state restatement points at the todo list instead of duplicating it, and a time estimate can no longer be read as a measurement.
258
261
  - **Deliver the whole ask; never the shortcut.** The request is a floor: no cheaper stand-in, no silent scope cut, no token gesture reported as done.
259
- - **Integrity: evidence, or a named blocker.** A done/tested claim holds only if the check ran in this session and its output was seen; "unverified" (tr: "doğrulanmadı") follows a check that ran and failed; leftover work is done, or stays as one of four blockers (DECISION, ACCESS, IRREVERSIBLE, ENV; tr: KARAR, YETKİ, GERİ ALINAMAZ, ORTAM) with its evidence. The mechanical half is enforced by [gate.md](gate.md), the residual half by the Stop rule's `residual work` class ([evidence.md](evidence.md)). Its **Ask policy** says what is asked at all. Work and process are never asked. An outward step runs on a grant. A never-default step waits for an answer. Every other question goes in one batch with a recommended default. The Stop rule's `needless ask` class refuses a reply that ends on a question the agent can decide, and the gate warns on an ask-tool question with no default ([gate.md](gate.md#ask-an-ask-tool-question-with-no-recommended-option)).
262
+ - **Integrity: evidence, or a named blocker.** A done/tested claim holds only if the check ran in this session and its output was seen; "unverified" (tr: "doğrulanmadı") follows a check that ran and failed; leftover work is done, or stays as one of four blockers (DECISION, ACCESS, IRREVERSIBLE, ENV; tr: KARAR, YETKİ, GERİ ALINAMAZ, ORTAM) with its evidence. The mechanical half is enforced by [gate.md](gate.md), the residual half by the Stop rule's `residual work` class ([evidence.md](evidence.md)). Its **Ask policy** says what is asked at all. Work and process are never asked. An outward step runs on a grant. A never-default step waits for an answer. Plan 136 added three to that list after a session ran another session's research line under a hidden dir and wrote a `chosen:` variant the owner never picked: choosing a variant or option a research, analysis or review recommends; a way around a tezgah refusal (another folder, a hidden dir, a hatch the owner did not grant); deciding about another session's work (its line, plan or branch). A taste or intent fork still goes in the batch with a default taken this turn; these do not. A subagent's question of that kind is relayed to the user, never answered by the orchestrator or escalated to a stronger model. Every other question goes in one batch with a recommended default. The Stop rule's `needless ask` class refuses a reply that ends on a question the agent can decide, and passes one that asks the owner to pick a variant or to decide one of those steps (`hooks/tezgah_integrity.py::OWNER_CHOICE`); the gate warns on an ask-tool question with no default ([gate.md](gate.md#ask-an-ask-tool-question-with-no-recommended-option)).
260
263
  - **Loop discipline.** Never repeat an identical failing command; three attempts is the ceiling, then report what is still unknown.
261
264
  - **Lessons ledger: stop repeating mistakes.** `.tezgah/lessons.md` lines are standing constraints; the recent ones are injected at session start, and up to three older ones a prompt shares words with ride that turn, each once per session when the host sends a session id (a compaction resets that, because the compacted context no longer holds them; without an id every matching turn carries them). Write a line rule first, `<rule> - <incident>` (`LESSON_SEPARATOR`, `hooks/tezgah_context.py::LESSON_SEPARATOR`). The session block counts the shown lines that do not open with their rule. A line ending `|| enforced_by: <rule|test>` leaves both blocks while that gate rule or test is on (`hooks/tezgah_lessons.py::lines`). That one reader serves the context, the gate and the CLI. It cuts that clause, a `|| check: ...` clause and an `@<sha> <path>` stamp off each line (`hooks/tezgah_lessons.py::parse`). None of them changes the key, the per-turn digest, the cut or the ranking. The gate records a line written after untrusted text in the same turn as a `lesson_tainted` row ([evidence.md](evidence.md)). That line still rides both blocks, behind a per-line data label (`hooks/tezgah_context.py::LESSON_TAINTED`). ADR 010 makes it a row, never a refusal. `bin/tezgah-lessons` proposes rewrites, merges and retirements, and never writes the file.
262
265
  - **No AI attribution, ever, on any host.** Nothing persisted or published may name a model, vendor or "AI" as author, co-author, generator or helper; one already in pushed history is reported.
@@ -543,7 +546,7 @@ brief carries the same caller-list floor (`hooks/tezgah_agents.py`), and
543
546
  ## When the injected text grows too large
544
547
 
545
548
  Each event has a byte budget: `session_start` and `post_compact` 16000,
546
- `user_prompt` 7000, `subagent_start` 5800, anything else 13000
549
+ `user_prompt` 7000, `subagent_start` 6100, anything else 13000
547
550
  (`hooks/tezgah_context.py::CONTEXT_BUDGET`). The rationale and the measured
548
551
  sizes sit beside it. In an empty fixture repo, `session_start` with the core
549
552
  builds 9842 B before the ponytail and ADHD rules went inline and 10884 B after
@@ -28,14 +28,14 @@ the kill switches are files, not variables, and live in
28
28
  | `TEZGAH_GH` | `packaging/upgrade.sh` | `gh` on PATH | The `gh` that checks the tarball's build provenance (`gh attestation verify`). A path that does not exist reads as no `gh`: the upgrade prints `provenance not checked` and goes on. Tests point it at a stand-in. |
29
29
  | `TEZGAH_NO_DEPS` | `bin/tezgah-setup::main` | unset | Any value but empty or `0` is `--no-deps`: the optional tools (codegraph, orx, host CLIs) are not installed. |
30
30
  | `TEZGAH_NO_SYMLINK` | `bin/tezgah-setup::symlinks_refused` | unset | Any value but empty or `0` writes the copy layout a machine without symlinks gets, on a machine that has them. |
31
- | `TEZGAH_PYTHON` | `hooks/tezgah_paths.py::python_cmd`, `bin/tezgah.js`, `bin/tezgah-dsh.cmd`, `hooks/hooks.json`, `hosts/*/hooks.json`, `hosts/omp/tezgah-hook.ts.in`, `hosts/opencode/plugins/tezgah.js`, `packaging/build.sh` | this process's interpreter, then `python3`/`python`/`py` on PATH | The interpreter every host hook and the npm shim start; pin it when the right Python is not first on PATH. |
31
+ | `TEZGAH_PYTHON` | `hooks/tezgah_paths.py::python_cmd`, `bin/tezgah.js`, `bin/tezgah-dsh.cmd`, `hooks/hooks.json`, `hosts/*/hooks.json`, `hosts/omp/tezgah-hook.ts.in`, `hosts/amp/tezgah-plugin.ts.in`, `hosts/opencode/plugins/tezgah.js`, `packaging/build.sh` | this process's interpreter, then `python3`/`python`/`py` on PATH | The interpreter every host hook and the npm shim start; pin it when the right Python is not first on PATH. |
32
32
 
33
33
  ## Where tezgah and the hosts keep state
34
34
 
35
35
  | Variable | Read by | Default | Effect |
36
36
  |---|---|---|---|
37
37
  | `TEZGAH_ROOTS` | `hooks/tezgah_paths.py::roots`, `bin/tezgah-setup`, `hosts/opencode/plugins/tezgah.js` | `roots` in `~/.config/tezgah/config.json`, else `~/Projects` | The directories tezgah is armed over, `os.pathsep`-separated (`;` on Windows, the opencode plugin included); it outranks the config file. |
38
- | `XDG_CONFIG_HOME` | `hooks/tezgah_paths.py::CONFIG_DIR`, `bin/tezgah-doctor`, `bin/tezgah-dsh.cmd`, `hosts/opencode/plugins/tezgah.js` | `~/.config` | Parent of tezgah's config dir (kill switches, `bin/` links, config) and of opencode's. |
38
+ | `XDG_CONFIG_HOME` | `hooks/tezgah_paths.py::CONFIG_DIR`, `hooks/tezgah_paths.py::AMP_PLUGIN`, `bin/tezgah-doctor`, `bin/tezgah-dsh.cmd`, `hosts/opencode/plugins/tezgah.js` | `~/.config` | Parent of tezgah's config dir (kill switches, `bin/` links, config), of opencode's, and of Amp's system plugin dir (`amp/plugins/tezgah.ts`). |
39
39
  | `XDG_DATA_HOME` | `bin/tezgah-setup::default_prefix`, `packaging/install.sh`, `packaging/upgrade.sh` | `~/.local/share` | Parent of the default install prefix. |
40
40
  | `XDG_CACHE_HOME` | `hooks/tezgah_apps.py::ARTIFACTS` | `~/.cache` | Parent of the app-analysis artifact dir. |
41
41
  | `TEZGAH_DEBUG` | `hooks/tezgah_guard.py` | unset | `1` makes every hook process append one line to `<cache>/debug.log` (host, script, each guarded call's outcome or exception class, elapsed ms; no prompt or tool text), created 0600 and swept by `tezgah-doctor --clean`. Off, it costs one environment read. |
@@ -122,6 +122,7 @@ the kill switches are files, not variables, and live in
122
122
  | `TEZGAH_CALL_OUTCOME` | `hooks/projects-posttooluse.py::main`, `hosts/dsh/hooks.json` | unset | `none` (dsh's Task rows) records the call's outcome as unknown instead of as a pass. |
123
123
  | `CLAUDE_PROJECT_DIR` | `hooks/projects-auto-init.py::main` | the process cwd | The project dir when the hook payload carries no `cwd`. |
124
124
  | `PLAYWRIGHT_BROWSERS_PATH`, `LOCALAPPDATA` | `hooks/tezgah_apps.py::_browser_caches`, `bin/tezgah-setup::default_prefix` (`LOCALAPPDATA` only) | Playwright's own cache dirs | Where `analyze-app` looks for an installed browser before it reports one missing; on Windows `LOCALAPPDATA` is also the parent of the default install prefix. |
125
+ | `AMP_IGNORE_GUIDANCE_FILES` | `hosts/amp/tezgah-plugin.ts.in` | set by the user for Amp | Amp's own list of guidance files to skip; when it is set the plugin asks for the session brief with the core in it, since `~/.config/amp/AGENTS.md` may be skipped. |
125
126
 
126
127
  ## Test knob
127
128
 
package/docs/evidence.md CHANGED
@@ -125,7 +125,8 @@ a write tool is `edit`, a shell call is `verify` when its command matches the ch
125
125
  | `claim` | `hooks/tezgah_integrity.py::stop_reason`, only for a reply in the claim vocabulary - a completion or verification word (`DONE`/`VERIFIED`), blocked or allowed, or a refused claim about an external system's state (`_external_claim`) | `hooks/tezgah_integrity.py::counters` |
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
- | `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 |
128
+ | `subagent_end` | `stop_reason(..., subagent=True)`, from Claude's SubagentStop (`hooks/hooks.json`), Cursor's subagentStop `summary` and Droid's SubagentStop `task_result`; 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
+ | `reply_unread` | Droid's, Command Code's and Copilot's Stop (`hosts/droid/hook.py::main`, `hosts/commandcode/hook.py::main`, `hosts/copilot/hook.py::main`) when the transcript yields no closing assistant text (`hosts/droid/hook.py::last_reply`, `hosts/copilot/hook.py::last_reply`); `detail` names the transcript, or says there was none. No such Stop event carries the reply, and an empty reply judged would pass any claim as "no claim", so the turn end is recorded and not judged | nothing yet; `NOT_TOOL_HOOK` lists it |
129
130
  | `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
131
  | `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
132
  | `snapshot`, `rollback` | `hooks/tezgah_snapshot.py::_capture_one`, `hooks/tezgah_snapshot.py::restore` | `hooks/tezgah_integrity.py::_snapshot_hash`; no counter |
@@ -296,12 +297,31 @@ The checker is `_stop_block` (`hooks/tezgah_integrity.py::_stop_block`), reached
296
297
  (`hosts/omp/hook.py::handle`) — all with `{"decision": "block", "reason": …}`, all inert outside a
297
298
  [root](glossary.md#root) and under the `verify-off` [kill switch](glossary.md#kill-switch)
298
299
  (`hooks/projects-stop.py::main`). Each of these hosts judges the reply after a block again.
300
+ The reply the rule judges is read through `reply_prose`
301
+ (`hooks/tezgah_integrity.py::reply_prose`). A string passes through. A host that hands over
302
+ a message object yields its `text` blocks, joined the way the model wrote them. A thinking
303
+ or tool block is left out. omp's `session_stop` carries `last_assistant_message` as the object
304
+ its `getLastAssistantMessage()` returns, and the bridge passed it through, so the rule judged
305
+ `str(dict)`: one line, no list items, diluted prose. Measured on
306
+ the owner's evidence database (2026-10-29): 1643 of 1656 omp `shape` rows of 45 days carried
307
+ `lines: 1`. Every other host wrote 209 multi-line rows of 231. Of the three days' refusals, 24
308
+ were `reply language` over such a row, and every line-shaped class read a text that had no
309
+ lines. The bridge now sends the text too
310
+ (`hosts/omp/tezgah-hook.ts.in`), but the core is where a host cannot repeat it (plan 143).
299
311
  The re-ask budget is the core's (`_record_only`, `hooks/tezgah_integrity.py::_record_only`): at
300
312
  most `STOP_REASKS` = 2 refusals in one user turn, and never two for one reason class. The count
301
313
  comes from the turn's `blocked:` claim and refusal rows, not from the host's payload. The omp host
302
- hands a block back as a queued message. Its prompt hook writes that text as a new `turn` row. A
303
- refusal row stores the key of its text as `reask`. A `turn` row with that key starts no new user
304
- turn for the count (`_turn_refusals`, `hooks/tezgah_integrity.py::_turn_refusals`). The omp host
314
+ hands a block back as a queued message. Its prompt hook writes that text as a new `turn` row.
315
+ Gemini does the same: an AfterAgent deny comes back through BeforeAgent as the next prompt
316
+ (`hosts/gemini/hook.py::main`). A refusal row stores the key of its text as `reask`. A `turn` row with that key starts no new user
317
+ turn for the count (`_turn_refusals`, `hooks/tezgah_integrity.py::_turn_refusals`). Antigravity's
318
+ `continue` re-enters the loop with no prompt, so its first-invocation hook writes no `turn` row
319
+ while the ledger's last row is that refusal (`hosts/antigravity/hook.py::reentered`). A Goose Stop
320
+ block keeps the turn going with no new prompt and no host count, so its turn rows alone decide
321
+ (`hosts/goose/hook.py::main`). Amp's plugin continues a blocked turn with a user message that
322
+ starts with `[tezgah:stop]`. The plugin never sends that prompt as a `user_prompt`. So it writes
323
+ no `turn` row and grants nothing. The Stop rule then judges that turn with `stop_hook_active`
324
+ (`hosts/amp/tezgah-plugin.ts.in`). The omp host
305
325
  caps the continuations it starts itself, but never a chain of blocks, so tezgah's count bounds that
306
326
  chain. So a reply that fixes the first class but adds a new one gets one more refusal. A blocker
307
327
  list with no evidence is such a new class. A reply after a block that is not refused writes one
@@ -310,7 +330,7 @@ while the turn holds no refusal of this rule. That reply gets no refusal and no
310
330
  Stop hook may have blocked. The host's count rules only where the ledger cannot. A session with no
311
331
  `turn` row allows one refusal per chain. A Cursor `loop_count` of 2 ends the chain. The opencode
312
332
  host has no Stop rule and so no budget. Its plugin API has no end-of-turn hook with an output. The
313
- `event` hook of `hosts/opencode/plugins/tezgah.js` only observes. Twelve triggers, in order, each naming its reason class (`_stop_block`
333
+ `event` hook of `hosts/opencode/plugins/tezgah.js` only observes. Fifteen triggers, in order, each naming its reason class (`_stop_block`
314
334
  `hooks/tezgah_integrity.py::_stop_block`).
315
335
  The first four judge how the reply is written (`_shape_block`, `hooks/tezgah_integrity.py::_shape_block`); the rest judge its evidence:
316
336
 
@@ -516,18 +536,71 @@ The four shape classes share their switches with the text they enforce: `adhd-of
516
536
  (`hooks/tezgah_integrity.py::QUESTION_END`). Its paragraph names no recommended default, and
517
537
  it names no step the ask policy keeps from a default (`hooks/tezgah_integrity.py::never_default`).
518
538
  Such steps are a force push, a delete, production, a credential, a third party, or a spend
519
- over `spend_cap`. The class then refuses a process question: the packs' `ask.process`
539
+ over `spend_cap`. Building a plan the reply wrote is one too
540
+ (`hooks/tezgah_integrity.py::PLAN_BUILD`). So is a last line that asks the owner to pick a
541
+ variant ("V4 mü V1 mi?"), to take a way around a tezgah refusal, or to decide about another
542
+ session's work (`hooks/tezgah_integrity.py::OWNER_CHOICE`, plan 136). The
543
+ class then refuses a process question: the packs' `ask.process`
520
544
  shapes, such as "Başlayayım mı?", "shall I proceed?" and "which first?"
521
545
  (`hooks/tezgah_integrity.py::PROCESS_ASK`). It also refuses a question about an outward
522
546
  step the user granted this session (`hooks/tezgah_integrity.py::grants_in_force`). A question
523
547
  about an outward step the user has not granted is the one the policy asks, so it passes. The
524
- refusal says to take the step, or to move it into the approval batch with a default. Under
548
+ refusal says to take the step, or to move a taste or intent fork into the approval batch
549
+ with a default. A class-F step waits for the answer. Under
525
550
  `tr` it opens with "Bu soruyu sorma". `_residual_block` asks it after `residual work` and
526
551
  before `ready steps` (`hooks/tezgah_integrity.py::needless_ask`). Unlike those two, it needs
527
- no work in the turn, because a plan proposal that ends "Başlayayım mı?" is its measured case.
552
+ no work in the turn, because a reply that ends on a bare "Başlayayım mı?" is its measured case.
528
553
  The research line counted 33 process questions among 48 question endings in the owner's omp
529
554
  transcripts. It refuses once per turn and counts like `residual work`
530
555
  (`hooks/tezgah_integrity.py::LIST_BLOCKS`).
556
+ 14. **handed step** — a turn that ran a command of its own ends by telling the reader to run
557
+ one (plan 142). The line names a command in backticks and addresses the reader as the one
558
+ who runs it: the packs' `ask.hand_over` shapes, "komutunu sen çalıştır", "çalıştırman
559
+ gerekiyor", "senin adımın", "please run", "your step:"
560
+ (`hooks/tezgah_integrity.py::handed_steps`). An answer about how something is done is not
561
+ one of them. "you can run `git config --global ...`" answers the question the reader
562
+ asked, and the review of this plan caught the wider wording refusing it. The turn must
563
+ hold a
564
+ `run`/`verify*` row (`hooks/tezgah_integrity.py::COMMAND_KINDS`). A turn that ran nothing
565
+ could not have run this either, which keeps the class inert on a host that sends no mode. A
566
+ read-only mode closes it outright: there every step waits for the approval. A table row and
567
+ a quoted line are not the
568
+ reply speaking, so the class skips them. The line stays the user's when it names an
569
+ outward step (`hooks/tezgah_integrity.py::outward_classes`), a class-F step
570
+ (`hooks/tezgah_integrity.py::never_default`), the owner's consent, tezgah's own install or a
571
+ `brew` step (`hooks/tezgah_integrity.py::OWNER_STEP`), or when it types its own blocker. The
572
+ refusal says to run each one and report what it printed. `_residual_block` asks it after
573
+ `needless ask` and before `ready steps`. A handover under a residual label ("Sonraki adım:")
574
+ belongs to `residual work`, which comes first. This class is the plain sentence that one
575
+ never saw. It refuses once per turn and counts like `residual work`
576
+ (`hooks/tezgah_integrity.py::LIST_BLOCKS`). Measured on the owner's top-level omp and Claude
577
+ transcripts (2026-10-29): of 3307 user turns, 27 replies handed the reader a command and no
578
+ class refused any of them. The shipped shapes refuse 9 of those turns, 8 of them a step the
579
+ session could have taken. Those eight are four `tezgah-task phase` moves, a local merge,
580
+ two "the next step is yours" and a server command that owes an ACCESS line. The ninth
581
+ quotes the gate's own refusal. The wider wording of the first two review rounds refused 40
582
+ turns and then 14, an answer about how a command is run among them.
583
+ 15. **plan claim** — a first-person claim that a command or check ran, in a turn the host ran
584
+ in a read-only mode (plan 142).
585
+ The host sends the mode with the Stop payload: `permission_mode` on Claude, Command Code,
586
+ Qwen and Droid, and omp's extension sends `plan` from `ctx.sessionModes`. `plan` and Droid's
587
+ `spec` are the read-only ones (`hooks/tezgah_integrity.py::READ_ONLY_MODES`). Nothing could
588
+ run in such a turn, so the claim says what cannot have happened. `_stop_block` asks it right
589
+ after the tamper classes and before the
590
+ admission, since "doğrulanmadı" repairs an unverified claim and here there was nothing to
591
+ check. A check that passed in the turn anyway clears it, like any other
592
+ evidence: a host may allow a read-only command in the mode. The trigger is the packs'
593
+ `ask.ran_it` vocabulary (`hooks/tezgah_integrity.py::RAN_IT`) - "test ettim", "i ran the
594
+ suite". It is read through the question and negation filter a claim is read through
595
+ (`hooks/tezgah_integrity.py::_asserted`). The done and verified vocabularies are not the
596
+ trigger: a plan states its acceptance in those words ("Done when all tests pass", "testler
597
+ yeşil olunca"), and the review of this plan caught both refused. It counts
598
+ as a false completion. The same mode exempts `needless ask` and closes `handed step`
599
+ (`hooks/tezgah_integrity.py::_residual_block`): the approval question is what plan mode asks
600
+ for, and every step it names waits for that approval. Every row of such a turn carries
601
+ `mode`, so a replay judges it as the mode it ran in
602
+ (`hooks/tezgah_replay.py::build_corpus`). A host that sends no
603
+ mode reads as `default`, which is what every rule did before the field.
531
604
 
532
605
  A past-time temporal spec decides classes 5–10 (`hooks/tezgah_stopspec.py::FORMULAS`,
533
606
  plan 063). Seven formulas read ten row atoms (`ROW_ATOMS`) and the turn marker. The module compiles
package/docs/gate.md CHANGED
@@ -83,7 +83,7 @@ it first after the root check (`decision` `hooks/tezgah_gate.py::decision`). The
83
83
  | tezgah's configuration | all of `~/.config/tezgah` (`config.json`, `bin/`, `ponytail.level`, the `hook:<host>:...` rows of `contract.sha256`, ...) | any write |
84
84
  | evidence and session state | the cache's `tezgah.db` (with its `-wal` and `-shm` files), `evidence/`, the `sessions/`, `switches/`, `gate-inactive/`, `harness-drift/` and `import-crash/` directories that database imports once, and `workspace-index.json`, in the global cache and the temp fallback (`hooks/tezgah_gate.py::CONTROL_CACHE`) | any write |
85
85
  | hook wiring | the opencode plugin file, the omp hook, the Claude plugin copy, the install prefix, and the tree the hooks run from unless the session works inside it (`hooks/tezgah_gate.py::_registration`, `hooks/tezgah_gate.py::_install_trees`) | any write |
86
- | shared registration files | `.claude/settings.json` and `settings.local.json`, `~/.claude/plugins/installed_plugins.json` and `known_marketplaces.json`, Codex's and Cursor's `hooks.json`, Codex's `config.toml` | a shell write; a write tool only when it changes an entry that is tezgah's - `statusLine`, `attribution`, `disableAllHooks`, a key or item naming tezgah (the user's own hook beside tezgah's in one matcher group stays theirs), or any `[hooks...]` line of `config.toml`, where the trust entries live (`hooks/tezgah_gate.py::_shared_change`) |
86
+ | shared registration files | `.claude/settings.json` and `settings.local.json`, `~/.claude/plugins/installed_plugins.json` and `known_marketplaces.json`, Codex's and Cursor's `hooks.json`, Codex's `config.toml`, Gemini's `settings.json`, Goose's `~/.config/goose/settings.json` | a shell write; a write tool only when it changes an entry that is tezgah's - `statusLine`, `attribution`, a key that turns hooks off (Claude's `disableAllHooks`, Gemini's `hooksConfig`; `hooks/tezgah_gate.py::OWNED_KEYS`), a key or item naming tezgah, such as `tezgah` in Goose's `disabledPlugins` (the user's own hook beside tezgah's in one matcher group stays theirs), or any `[hooks...]` line of `config.toml`, where the trust entries live (`hooks/tezgah_gate.py::_shared_change`) |
87
87
  | repository marks and hooks | a `REPO_MARKS` name under a root, the session checkout's `.git/hooks` (a `.sample` file and a clone elsewhere pass), `git config core.hooksPath` set or unset there or `--global` | any write |
88
88
  | | `.husky` | a delete, a chmod that takes a permission away, or a move |
89
89
  | | a directory holding any protected path (`rm -rf ~/.cache`, `mv .git /tmp/x`), also through a link (`rm -rf link/`), including `.tezgah/plans/open/` itself, every ancestor of it (`rm -rf .tezgah`, `mv .tezgah/plans /tmp/x`) and a glob or brace directly under it (`rm -rf .tezgah/plans/open/*`) | a delete or move |
@@ -139,7 +139,7 @@ remedy is one sentence, `hooks/tezgah_integrity.py::PIPED_REMEDY`, which the alw
139
139
 
140
140
  Trigger, shell: a write command (`hooks/tezgah_gate.py::WRITE_CMD` — `git commit|merge|tag|notes`, `gh api`,
141
141
  `gh pr|issue|release create|edit|comment|review|merge|close`) carrying a credit form (`hooks/tezgah_gate.py::ATTRIB` — `co-authored-by:`, `generated with`, `made with`,
142
- `built by`, `assisted by`, `authored by`, `noreply@anthropic`, a robot emoji). Trigger, edit/write: one of the content fields `EDIT_TEXT` (`hooks/tezgah_gate.py::EDIT_TEXT`)
142
+ a credit verb, `noreply@anthropic`, a robot emoji). A credit verb is `built`, `written`, `drafted` or a like verb, then `by`, `with` or `using`, then a model or vendor name. Trigger, edit/write: one of the content fields `EDIT_TEXT` (`hooks/tezgah_gate.py::EDIT_TEXT`)
143
143
  containing a line that *starts* with a credit (`hooks/tezgah_gate.py::ATTRIB_LINE`). Told: remove it and re-run; naming a tool in order to use it is fine, crediting it
144
144
  as author is not (`hooks/tezgah_gate.py::ATTRIB_DENY`). Standing. The line anchor is why prose that merely names the banned forms passes (`hooks/tezgah_gate.py::_CREDIT`).
145
145
  The same twin as the shortcut rule's: a credit a heredoc writes into a file is refused from the body (`hooks/tezgah_gate.py::decision`), and no command has to be
@@ -207,6 +207,8 @@ checkpoint that is a plain sha (the boundary is already real), a `pending` with
207
207
 
208
208
  Trigger: a write tool, or a shell redirect/`tee` (`write_paths`), whose target relative to the repo root starts with `plans/`, `research/` or `analysis/` while the project tracks no file under that directory (`workspace_reason`, `hooks/tezgah_gate.py`). The refusal names `.tezgah/<kind>/` as the place to write. It also names the commit form for the workspace's shape (`hooks/tezgah_research.py::folded`). An own repository gets `git -C .tezgah` (`hooks/tezgah_gate.py::WORKSPACE_OWN`). A folded one the project tracks gets its own `git add`/`git commit` (`hooks/tezgah_gate.py::WORKSPACE_FOLDED`). A directory the project already tracks is its own and passes. `mkdir` and positional shell targets (`cp`, `mv`) are not read - the same ceiling as `write_paths`. Off: `workspace-off`.
209
209
 
210
+ A second trigger under the same switch: a write under a dot dir directly below `.tezgah/research/`, such as `.tezgah/research/.staging/x/state.json` (`hooks/tezgah_gate.py::research_hidden_reason`). It is read against the main checkout and against the session's own root. `slugs()` skips dot dirs, so a line there is invisible to `status` and `check`. The refusal says a refused line goes to the owner (`hooks/tezgah_gate.py::RESEARCH_HIDDEN_DENY`). `open/`, `done/` and a dot file directly in `research/` pass.
211
+
210
212
  ### Secret — a credential on its way into a file
211
213
 
212
214
  Three triggers, one per shape a credential takes on its way into a file (`hooks/tezgah_gate.py::secret_command`, `hooks/tezgah_gate.py::secret_edit`, read at `hooks/tezgah_gate.py::decision`):
@@ -443,6 +445,8 @@ rule's triggers are the classes `_stop_block` returns, the four `_shape_block` h
443
445
  | `open todo` | the owner's report (2026-10-12, plan 097 item 2): `ready_left` counted only `pending` items, so a done claim over an `in_progress` todo item ended the turn | `hooks/tezgah_integrity.py::open_left` | `tests/test_integrity.py::test_a_done_claim_over_an_in_progress_todo_is_refused_once` |
444
446
  | `needless ask` | research line `orchestrator-flow-audit` S3: 33 of 48 final replies that ended on a question in the owner's omp transcripts were process questions ("Başlayayım mı?"), and 9 of 47 next prompts were a bare "devam" or "evet" | `hooks/tezgah_integrity.py::needless_ask` | `tests/test_integrity.py::test_a_process_question_at_the_end_is_refused_once` |
445
447
  | `stopped slice` | measured 2026-10-11 (research line `subagent-resume-orchestration`): 118 of 1413 omp subagent runs ended stopped, and in the newest 20 the parent absorbed the work inline 16 times instead of resuming it | `hooks/tezgah_integrity.py::_stopped_slice` | `tests/test_integrity.py::test_a_stopped_slice_of_this_session_refuses_the_turn_once` |
448
+ | `handed step` | measured 2026-10-29 on the owner's top-level omp and Claude transcripts: of 3307 user turns, 27 replies handed the reader a command in backticks and no class refused any of them; the `needless ask` refusal pushes the same step out of a question and into prose. The shipped shapes refuse 9 of those turns, 8 a real handover; the two review rounds of plan 142 cut the wordings that refused an answer about how a command is run | `hooks/tezgah_integrity.py::handed_steps` | `tests/test_integrity.py::test_a_handed_step_is_refused_once` |
449
+ | `plan claim` | probe 2026-10-29: a read-only plan turn whose reply claimed "test ettim" ended the turn, because every evidence class needs a work row and plan mode produces none; the same mode's approval question was refused as `needless ask`. The trigger is the first-person run claim alone: review round 2 found the done and verified vocabularies refusing a plan's own acceptance line | `hooks/tezgah_integrity.py::RAN_IT` | `tests/test_integrity.py::test_a_done_claim_in_plan_mode_is_refused` |
446
450
  | `placating opener` | designed | output rule 10 read at the Stop event; no incident recorded | `tests/test_integrity.py::test_sycophantic_opener_blocks` |
447
451
  | `forbidden closer` | designed | output rule 10 read at the Stop event; no incident recorded | `tests/test_integrity.py::test_a_closer_from_the_skills_own_list_blocks` |
448
452
  | `list cap` | designed | output rule 8 read at the Stop event; no incident recorded | `tests/test_integrity.py::test_a_list_over_the_cap_blocks_with_its_size` |
package/docs/glossary.md CHANGED
@@ -164,7 +164,7 @@ The value of one status [mark](#mark): `on`, `ready`, `off` or `info` (`hooks/te
164
164
  One of the row kinds that count as a step of work - `run`, `edit`, `verify`, `verify_ok`, `verify_fail`, `interrupted` (`STEP_KINDS`, `hooks/tezgah_integrity.py::STEP_KINDS`), summed into `steps` and into the drift threshold, while `deny`, `nudge`, `claim` and `turn` are the machinery around the work (`hooks/tezgah_integrity.py:1420-1422`). `interrupted` is the one that is not a failure: the host said the call was stopped, so the row carries no `exit` and no `fail_class`, no reader folds it as a failed check and no attempt is spent on it (`hooks/tezgah_integrity.py:1426-1429`). Not any row.
165
165
 
166
166
  ### Stop rule
167
- The end-of-turn refusal, `stop_reason()` (`hooks/tezgah_integrity.py::stop_reason`), run by the Stop hooks of Claude, Codex, Cursor and omp (`hooks/projects-stop.py::main`, `hosts/codex/hook.py::main`, `hosts/cursor/hook.py::dispatch`, `hosts/omp/hook.py::handle`): it blocks on a placating opener or a banned sign-off on the last prose line, on a completion claim the newest check does not support, on a check that passed before the newest write the gate saw change the tree, on a turn that recorded work with no passing check, or on a working turn that ends on a list of what is left with an item no blocker accounts for (`_stop_block()`, `hooks/tezgah_integrity.py::_stop_block`). An explicit "doğrulanmadı" clears a check-repair refusal only after a check that ran and failed since the newest change (`hooks/tezgah_integrity.py::_admitted`). A claim in a turn with no work is judged against the whole session, and a read-only or VCS-bookkeeping turn after a pass on an unchanged tree is exempt ([evidence](evidence.md#the-stop-rule-end-to-end)). Not the [gate](#gate), which runs before a call ([evidence](evidence.md)).
167
+ The end-of-turn refusal, `stop_reason()` (`hooks/tezgah_integrity.py::stop_reason`), run by the Stop hooks of Claude, Codex, Cursor and omp (`hooks/projects-stop.py::main`, `hosts/codex/hook.py::main`, `hosts/cursor/hook.py::dispatch`, `hosts/omp/hook.py::handle`): it blocks on a placating opener or a banned sign-off on the last prose line, on a completion claim the newest check does not support, on a check that passed before the newest write the gate saw change the tree, on a turn that recorded work with no passing check, on a working turn that ends on a list of what is left with an item no blocker accounts for, or on a reply that hands the reader a command the turn ran commands beside (`handed_steps()`, `hooks/tezgah_integrity.py::handed_steps`) (`_stop_block()`, `hooks/tezgah_integrity.py::_stop_block`). An explicit "doğrulanmadı" clears a check-repair refusal only after a check that ran and failed since the newest change (`hooks/tezgah_integrity.py::_admitted`). A claim in a turn with no work is judged against the whole session, and a read-only or VCS-bookkeeping turn after a pass on an unchanged tree is exempt ([evidence](evidence.md#the-stop-rule-end-to-end)). In a host plan mode (`read_only_turn()`, `hooks/tezgah_integrity.py::read_only_turn`) the approval question is not refused and a done/tested claim is. Not the [gate](#gate), which runs before a call ([evidence](evidence.md)).
168
168
 
169
169
  ### surface
170
170
  A place a host can show tezgah's output - a status line, a TUI widget, a `systemMessage` - and therefore the reason the same [marks](#mark) render differently per host (`hooks/tezgah_context.py::health_segments`, `hooks/tezgah_context.py::render_line`); each host's surfaces are [hosts](hosts.md)'s. Not the host itself.