@r1z4x/tezgah 1.5.1 → 1.7.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 (81) hide show
  1. package/CHANGELOG.md +126 -1
  2. package/MANIFEST +11 -0
  3. package/README.md +6 -3
  4. package/bin/consult +13 -10
  5. package/bin/tezgah-docs +18 -16
  6. package/bin/tezgah-doctor +152 -0
  7. package/bin/tezgah-research +286 -12
  8. package/bin/tezgah-route +48 -1
  9. package/bin/tezgah-setup +27 -14
  10. package/bin/tezgah-task +771 -46
  11. package/bin/tezgah-taste +48 -10
  12. package/docs/architecture.md +7 -4
  13. package/docs/citations-baseline.json +1 -1
  14. package/docs/clarity-baseline.json +5 -5
  15. package/docs/contract.md +112 -25
  16. package/docs/evidence.md +201 -28
  17. package/docs/feature-audit.md +25 -4
  18. package/docs/gate.md +70 -33
  19. package/docs/glossary.md +4 -4
  20. package/docs/hosts.md +8 -8
  21. package/docs/index.json +30 -10
  22. package/docs/judge.md +9 -3
  23. package/docs/models.md +35 -2
  24. package/docs/operations.md +103 -3
  25. package/docs/research.md +392 -13
  26. package/docs/skills.md +24 -6
  27. package/docs/status-line.md +5 -5
  28. package/docs/testing.md +2 -2
  29. package/docs/vision.md +12 -4
  30. package/hooks/hooks.json +2 -2
  31. package/hooks/lang/en.json +323 -0
  32. package/hooks/lang/tr.json +344 -0
  33. package/hooks/projects-posttooluse.py +24 -5
  34. package/hooks/projects-pretooluse.py +9 -4
  35. package/hooks/projects-stop.py +28 -16
  36. package/hooks/tezgah_agents.py +29 -9
  37. package/hooks/tezgah_clarity.py +7 -3
  38. package/hooks/tezgah_components.py +2 -1
  39. package/hooks/tezgah_context.py +460 -390
  40. package/hooks/tezgah_gate.py +640 -104
  41. package/hooks/tezgah_integrity.py +1480 -197
  42. package/hooks/tezgah_models.py +337 -15
  43. package/hooks/tezgah_paths.py +103 -10
  44. package/hooks/tezgah_policy.py +286 -130
  45. package/hooks/tezgah_product.py +107 -0
  46. package/hooks/tezgah_rank.py +29 -27
  47. package/hooks/tezgah_replay.py +200 -30
  48. package/hooks/tezgah_research.py +1297 -12
  49. package/hooks/tezgah_skill_pick.py +58 -17
  50. package/hooks/tezgah_stopspec.py +7 -0
  51. package/hooks/tezgah_store.py +74 -27
  52. package/hooks/tezgah_task.py +917 -8
  53. package/hosts/codex/hook.py +9 -10
  54. package/hosts/cursor/hook.py +21 -14
  55. package/hosts/dsh/hooks.json +2 -2
  56. package/hosts/omp/hook.py +44 -23
  57. package/hosts/omp/tezgah-hook.ts.in +93 -7
  58. package/hosts/opencode/plugins/tezgah.js +119 -39
  59. package/output-styles/tezgah.md +76 -42
  60. package/package.json +1 -1
  61. package/skills/analyze-app/SKILL.md +57 -0
  62. package/skills/design-contract/EVALS.md +24 -1
  63. package/skills/design-contract/SKILL.md +17 -1
  64. package/skills/design-contract/data-views.md +86 -0
  65. package/skills/feature-audit/SKILL.md +32 -3
  66. package/skills/i-have-adhd/EVALS.md +22 -2
  67. package/skills/i-have-adhd/SKILL.md +12 -5
  68. package/skills/plan-add/EVALS.md +8 -8
  69. package/skills/plan-add/SKILL.md +22 -21
  70. package/skills/plan-sync/EVALS.md +2 -2
  71. package/skills/plan-sync/SKILL.md +14 -8
  72. package/skills/pm-frameworks/SKILL.md +6 -0
  73. package/skills/ponytail/EVALS.md +21 -2
  74. package/skills/ponytail/SKILL.md +4 -4
  75. package/skills/product-analysis/EVALS.md +23 -1
  76. package/skills/product-analysis/SKILL.md +50 -11
  77. package/skills/product-flow/EVALS.md +81 -0
  78. package/skills/product-flow/SKILL.md +91 -0
  79. package/skills/research/SKILL.md +99 -2
  80. package/skills/tezgah-contract/EVALS.md +52 -7
  81. package/skills/tezgah-contract/SKILL.md +204 -85
package/docs/evidence.md CHANGED
@@ -36,7 +36,7 @@ family lose theirs (`hooks/tezgah_integrity.py::REDACTIONS`; audit L-6, which fo
36
36
  (`hooks/tezgah_integrity.py::LEDGER_FIELDS`) — `id`, `exit`, `out_bytes`, `fail_class`, `workspace`, `source`, `hash`, `changed`, `tool`,
37
37
  `target`, plus the reply-shape names `lines`, `chars`, `items`, `longest_list`, `tr_share`, `answer_first` and the compaction
38
38
  row's `summary_chars`, `summary_hash`, `constraint_found`, `constraint_expected`, the `attest`
39
- row's `host` and `switches` and a `claim` row's `harness` — and
39
+ row's `host` and `switches`, a `claim` row's `harness` and a Stop refusal row's `reask` (the key of its own text, which the `turn` row of omp's continuation carries, `hooks/tezgah_integrity.py::_turn_refusals`) — and
40
40
  a key outside that set is dropped, a `None` value left out, because every reader treats a missing key
41
41
  as `None` (`hooks/tezgah_integrity.py::note_path`). `tool` is the call's own name (`Bash`, `Write`, `mcp__codegen__status`),
42
42
  written by `note_tool` (`hooks/tezgah_integrity.py::note_tool`) because `classify` folds the name into
@@ -49,7 +49,7 @@ written path as an absolute real path, resolved against the call's cwd (`_abs_ta
49
49
  now compares `target`, and a row without one (written before the field landed) is ignored by that
50
50
  guard rather than matched by its spelling (audit M-6). Both writers write `tool` — `note_tool`, and opencode's
51
51
  plugin, whose row carries the same field under the same empty-means-absent rule
52
- (`hosts/opencode/plugins/tezgah.js:1230-1241`) — so no live host leaves it out. A row written *before*
52
+ (`hosts/opencode/plugins/tezgah.js:1253-1264`) — so no live host leaves it out. A row written *before*
53
53
  the field landed names its tool only where the name survived in `detail` — an `unknown` row and an
54
54
  MCP `external` row — so the one cause of an under-count is the corpus's age, and it shrinks as
55
55
  sessions run. The append is one INSERT in autocommit (`hooks/tezgah_integrity.py::_append`,
@@ -63,7 +63,14 @@ audit L-6 found the old files `0644`.
63
63
  **The JSONL files it replaced.** Every read or write of a session first imports its old
64
64
  `<cache>/evidence/<slug>.jsonl` file (`hooks/tezgah_store.py::_sync`). The import costs one stat
65
65
  when nothing is new. It takes the database's write lock first, then the file's flock if it is free.
66
- It takes only the complete lines past the bytes the `imported` table already counts. It never
66
+ It takes only the complete lines past the bytes the `imported` table already counts for the file.
67
+ A volume remount can give the file a new device id. A store from before the fix then took the
68
+ file again from its start under the new id. So every `imported` row of one name and inode is one
69
+ import stream. The import resumes from the longest stream under any id, even when this id holds a
70
+ shorter one (`hooks/tezgah_store.py::_recorded`). That row and this id's own row swap ids, so each
71
+ stream keeps one row (`hooks/tezgah_store.py::_record`). A file cut below what went in goes on as
72
+ one stream. `tezgah-doctor --repair-evidence` counts the copies by those rows and removes them
73
+ ([operations](operations.md#health-pass-tezgah-doctor)). It never
67
74
  renames the file aside. An opencode process started before the store may still run the old plugin,
68
75
  which appends to it, and a refused row lands there. Session start launches the bulk import
69
76
  of every other file, detached and at most once a day
@@ -108,12 +115,16 @@ a write tool is `edit`, a shell call is `verify` when its command matches the ch
108
115
  | Kind | Written by | Read by |
109
116
  |---|---|---|
110
117
  | `turn` | `hooks/tezgah_integrity.py::note_turn`, from the prompt path | `hooks/tezgah_integrity.py::_turn_start`, scoping every turn rule; `hooks/tezgah_integrity.py::_claim_key` |
111
- | `run`, `edit`, `verify`, `verify_ok`, `verify_fail`, `interrupted` | `hooks/tezgah_integrity.py::note_tool`, the call's own name in `tool`, on opencode the plugin writes these same kinds and the same field (`hosts/opencode/plugins/tezgah.js:1192-1270`) | the Stop rule's `worked` set `hooks/tezgah_integrity.py::WORK_KINDS`; `counters.steps` `hooks/tezgah_integrity.py::_counts`; `last_verify`/`partial_state`; the `--trend` tool histogram |
118
+ | `run`, `edit`, `verify`, `verify_ok`, `verify_fail`, `interrupted` | `hooks/tezgah_integrity.py::note_tool`, the call's own name in `tool`, on opencode the plugin writes these same kinds and the same field (`hosts/opencode/plugins/tezgah.js:1215-1293`) | the Stop rule's `worked` set `hooks/tezgah_integrity.py::WORK_KINDS`; `counters.steps` `hooks/tezgah_integrity.py::_counts`; `last_verify`/`partial_state`; the `--trend` tool histogram |
112
119
  | `external`, `unknown` | `hooks/tezgah_integrity.py::note_tool` | the taint notice, via `source`; the `--trend` tool histogram; nothing counts them as work |
113
- | `began` | the gate's allow path for a write or shell call `hooks/tezgah_gate.py::decision`; on opencode the plugin's before-hook when the core was not asked (`hosts/opencode/plugins/tezgah.js:2549-2565`) | `hooks/tezgah_integrity.py::unanswered`, behind a claim in the Stop rule and in `counters.unanswered` |
120
+ | `todo` | `hooks/tezgah_integrity.py::note_todo`, from `note_tool` for a todo call: omp `todo` (the bridge sends the list from the result's `details`), Claude and dsh `TodoWrite` (their PostToolUse matcher), opencode `todowrite` (the plugin's own `recordTodo`), Cursor `todo_write` and Codex `update_plan`, which arrive with no matcher. `open`, `ready` and `blocked` count the list, and `detail` holds the ready items' heads, one per line | the `ready steps` Stop class (`hooks/tezgah_integrity.py::ready_left`) and the idle-wait warning (`hooks/tezgah_gate.py::ready_advice`) |
121
+ | `wait` | `hooks/tezgah_integrity.py::note_tool`, for omp's `wait` | `hooks/tezgah_gate.py::ready_advice`, which counts it as idle; nothing counts it as work |
122
+ | `ask` | `hooks/tezgah_gate.py::ask_advice`, for an omp `ask` or Claude AskUserQuestion call: `detail` holds the questions, cut; `questions` counts them and `defaults` the ones with a recommended option | nothing refuses on it; the row is the record of what the session asked |
123
+ | `grant` | `hooks/tezgah_context.py::context_for`, from each prompt's `user_grants` (`hooks/tezgah_context.py::user_grants`): `detail` holds the user's clause, `target` the outward class it covers (`push`, `release`, `publish`, `pr` or `all`), or `lift:<class>` when the prompt took it back | `hooks/tezgah_integrity.py::grants_in_force`, read by the `needless ask` Stop class and the ask-tool warning |
124
+ | `began` | the gate's allow path for a write or shell call `hooks/tezgah_gate.py::decision`; on opencode the plugin's before-hook when the core was not asked (`hosts/opencode/plugins/tezgah.js:2572-2588`) | `hooks/tezgah_integrity.py::unanswered`, behind a claim in the Stop rule and in `counters.unanswered` |
114
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` |
115
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 |
116
- | `after_block` | `hooks/tezgah_integrity.py::stop_reason`, on the reply after this rule's own block (`stop_hook_active` in a turn holding a `blocked:` claim or refusal row) on Claude, Codex 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 |
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 |
117
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 |
118
129
  | `stop_spec` | `hooks/tezgah_stopspec.py::shadow`, from `stop_reason`, one per reply that leaves a claim or a refusal row, keyed like the claim row. The `detail` is `agree <where>: <class>`, `disagree <where>: <imperative> -> <spec>` (`ok` for an allow) or `error: <type>`. `<where>` is `fold` when the selector reached the fold and `selector` when it decided alone | `tests/stop_spec_eval.py --shadow-rows`, for plan 063's live agreement bar, read on the `fold` rows. It never feeds the verdict. `NOT_TOOL_HOOK` lists it |
119
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` |
@@ -121,6 +132,7 @@ a write tool is `edit`, a shell call is `verify` when its command matches the ch
121
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) |
122
133
  | `lesson` | `hooks/tezgah_context.py::note_lesson`, one row per lesson the budget left in a session block or a per-turn block, with its 8-hex `key` and `block` (`session` or `turn`), inside the host's `safe()` like the rest of the prompt path | no counter; `hooks/tezgah_context.py::NOT_TOOL_HOOK` keeps it out of `hooks/tezgah_context.py::_ledger_since`, so a session of lesson rows still reads as a gate that never ran |
123
134
  | `lesson_tainted` | the gate's allow path for a write to a `.tezgah/lessons.md` (`hooks/tezgah_gate.py::lesson_taint`, from `decision` after every refusal) in a user turn that already holds a row carrying `source`, folded over `turn_rows` (that subagent's rows when the host names one). One row per lesson line the ledger will hold after the write and does not hold now, read off the post-write text, so a partial-line edit names the whole line; its 8-hex `key`, the channel in `source` and `detail`, the ledger in `target`. One row with no `key` when the gate cannot name the line (an `echo` append, an edit whose old text is not there, a removal). The keyed rows also go to the repository's index, which keeps only the ledger's current lines (`hooks/tezgah_lessons.py::write_taint`). Never a refusal (ADR 010); the dry run writes neither | the per-line data label on that lesson in both lesson blocks (`hooks/tezgah_context.py::LESSON_TAINTED`, read once per prompt through `hooks/tezgah_lessons.py::tainted`); no counter |
135
+ | `lesson_changed` | the same allow path (`hooks/tezgah_gate.py::lesson_taint`), in any turn whatever it read: one row per `.tezgah/lessons.md` line the write rewrites or removes, read off the post-write text, an `apply_patch` hunk's `-` lines (a `Delete File` drops every line), or a heredoc or plain `tee` that overwrites the ledger (an append, `>>` or `tee -a`, drops nothing and writes none); the old line's 8-hex `key`, its old text in `detail` (redacted and cut like every `detail`), the ledger in `target`. At most `LESSON_CHANGED_MAX` = 10 rows per call; the rest go into one keyless row whose `items` counts them, so a whole-ledger rewrite cannot push the refusal rules' fixed tails out of their window. No `new_key`: the row contract has no field for it, so a rewritten line's new key rides only its `lesson_tainted` row. A shell write the gate cannot read (`sed -i`, an `echo >` body) leaves none. Never a refusal (ADR 019) | nothing yet: the history a person or a replay reads to see which standing constraints were edited away |
124
136
  | `lesson_hit` | nothing yet: reserved for plan 061 Phase B, a compiled lesson check that matched a call in its observe stage, with the lesson's `key` and the call's `id` | nothing yet |
125
137
  | `ledger_damage` | `hooks/tezgah_integrity.py::_note_damage`, from `_parse`, once per damaged line, with its 12-hex `key` | the Stop rule's first evidence class, "evidence tampered" (`hooks/tezgah_integrity.py::_stop_block`) |
126
138
  | `disarm` | `hooks/tezgah_context.py::disarmed`, on the prompt a switch first appears on after the session's first prompt; `detail` is the switch name. Also the gate (`hooks/tezgah_gate.py::decision`), for a call it lets through that changes a `CONTROL_EVIDENCE` target - an open plan's delete or move, a forced `git add` of a `.tezgah/` path - with the call's `id` and `detail` `control: <what>` | the status line's `gate` mark ([status-line.md](status-line.md)) for the switch rows, set by the same check that writes them; the gate's rows are read by a person or a replay; no counter |
@@ -208,7 +220,7 @@ done` exits 0 whatever pytest found. `pytest && echo ok` and `cd x && pytest` ke
208
220
  trimmed form before it runs ([gate.md](gate.md), the `piped` rule). The `out_bytes` half
209
221
  bites only where the host reported a result size — Codex (`hosts/codex/hook.py::main`), Cursor
210
222
  (`hosts/cursor/hook.py::remember_answer,275`) and omp, whose bridge measures it and sends `result_len`
211
- (`hosts/omp/tezgah-hook.ts.in:670-676`), now do — and 6 of 1423 `verify_ok` rows across 1454 local ledgers carry the field
223
+ (`hosts/omp/tezgah-hook.ts.in:737-743`), now do — and 6 of 1423 `verify_ok` rows across 1454 local ledgers carry the field
212
224
  (measured 2026-09-19), so an absent field still passes: the guard narrows a check whose
213
225
  result was measured empty, it does not require the measurement. `last_verify`
214
226
  (`hooks/tezgah_integrity.py::last_verify`) folds the ordered rows to `ok`/`fail`/`ran`/`None`, because a set cannot tell a failure
@@ -283,17 +295,29 @@ The checker is `_stop_block` (`hooks/tezgah_integrity.py::_stop_block`), reached
283
295
  (`hosts/codex/hook.py::main`), Cursor (`hosts/cursor/hook.py::dispatch`), omp
284
296
  (`hosts/omp/hook.py::handle`) — all with `{"decision": "block", "reason": …}`, all inert outside a
285
297
  [root](glossary.md#root) and under the `verify-off` [kill switch](glossary.md#kill-switch)
286
- (`hooks/projects-stop.py::main`). On Claude, Codex and omp the rule judges the reply after a block
287
- (`stop_hook_active`) once more, in record-only mode. That reply writes one `after_block` row and
288
- never meets a second refusal. It writes nothing when the turn holds no refusal of this rule, since
289
- another Stop hook may have blocked. Cursor still skips it. Ten triggers, in order, each naming its reason class (`_stop_block`
298
+ (`hooks/projects-stop.py::main`). Each of these hosts judges the reply after a block again.
299
+ The re-ask budget is the core's (`_record_only`, `hooks/tezgah_integrity.py::_record_only`): at
300
+ most `STOP_REASKS` = 2 refusals in one user turn, and never two for one reason class. The count
301
+ 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
305
+ caps the continuations it starts itself, but never a chain of blocks, so tezgah's count bounds that
306
+ chain. So a reply that fixes the first class but adds a new one gets one more refusal. A blocker
307
+ list with no evidence is such a new class. A reply after a block that is not refused writes one
308
+ `after_block` row. A host may say a block came before (`stop_hook_active`, Cursor's `loop_count`)
309
+ while the turn holds no refusal of this rule. That reply gets no refusal and no row, since another
310
+ Stop hook may have blocked. The host's count rules only where the ledger cannot. A session with no
311
+ `turn` row allows one refusal per chain. A Cursor `loop_count` of 2 ends the chain. The opencode
312
+ 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`
290
314
  `hooks/tezgah_integrity.py::_stop_block`).
291
315
  The first four judge how the reply is written (`_shape_block`, `hooks/tezgah_integrity.py::_shape_block`); the rest judge its evidence:
292
316
 
293
317
  1. **placating opener** — the reply opens by agreeing or apologising (`hooks/tezgah_integrity.py::SYCOPHANT`).
294
318
  2. **forbidden closer** — the reply's last prose line is one of the sign-offs the output contract bans (`CLOSER`, `hooks/tezgah_integrity.py`): the same rule read at the other end, with its own class so the ledger says which end fired.
295
319
  3. **list cap** — a contiguous list runs past `LIST_CAP` = 5 items (rule 8 of the `i-have-adhd` skill). `longest_list` (`hooks/tezgah_integrity.py::longest_list`) counts per run of column-0 items of one kind: a heading, a prose line, a table row, a fence, a marker switch or a numbered list restarting at 1 ends the run, and blank or indented continuation lines keep it. Two headed groups of four pass; a 5+ enumeration that must stay whole goes under headings or into a table, which the block text says.
296
- 4. **reply language** — the reply's prose is not Turkish: at least `LANG_MIN_WORDS` = 25 prose words and under `LANG_MIN_SHARE` = 6% of them Turkish. `prose_words` drops fences, table rows, inline code, URLs, paths and identifiers first; `turkish_share` counts a word with a Turkish letter or one of `TR_WORDS` (`hooks/tezgah_integrity.py::TR_WORDS`).
320
+ 4. **reply language** — the reply's prose is not in the configured language. That means at least `LANG_MIN_WORDS` = 25 prose words, under `LANG_MIN_SHARE` = 6% of them marked as that language. `prose_words` drops fences, table rows, inline code, URLs, paths and identifiers first; `turkish_share` counted a word with a Turkish letter or one of `TR_WORDS`. `reply_share` counts the language pack's marker letters and words now (`hooks/tezgah_integrity.py::reply_share`). Only `tr` ships markers. They no longer hold de, da, en, ne, ya, her or mi, the words Spanish, French and Dutch share with Turkish.
297
321
 
298
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.
299
323
 
@@ -368,7 +392,7 @@ The four shape classes share their switches with the text they enforce: `adhd-of
368
392
  about what a registry publishes. The read is the evidence, and it is taken by the row's command
369
393
  and not by `passing_check` — `npm view` exiting non-zero is the answer for "this version is
370
394
  missing" (`hooks/tezgah_integrity.py::_external_read_row`) — so a turn whose only work was the read ends. It is
371
- last of the ten by the branch order above and in its strongest form: it is asked only where the
395
+ last of the evidence classes by the branch order above and in its strongest form: it is asked only where the
372
396
  fold would otherwise let the turn end, so it turns an allow into a refusal and never changes the
373
397
  class another branch refused the same turn under. It is the one class a turn with no work in it
374
398
  can make, which is why the "no work, no claim word" exit exempts it (`hooks/tezgah_integrity.py::_stop_block`) — the two turns it
@@ -377,6 +401,121 @@ The four shape classes share their switches with the text they enforce: `adhd-of
377
401
  fold returned `(None, None)` over both and neither was judged at all. Measured over this machine's
378
402
  own 2,092 final assistant replies, 2 are read as claims, both in review text about someone else's
379
403
  tooling.
404
+ 11. **residual work** — a turn that did work ends on a list of what is left. Any step row counts as
405
+ work, a prose edit included. The list starts at a line that opens with a residual label. That
406
+ is a markdown heading, a bold or italic lead, or a few words before a colon. A line of up to four
407
+ words right over a list needs no colon. Up to three words, a quote marker or an emoji may lead it.
408
+ Examples are "Açık kalanlar", "Doğrulanmadı:", "Sıradaki adım", "Bekleyen tek iş", "Kapsam
409
+ dışı", "TODO" and "Next steps". First-person negatives ("Yapamadım", "Test etmedim"), "Yarım
410
+ kalanlar", "Henüz kontrol edilmeyen", "Unresolved" and "Still open" count too (plan 086, S4).
411
+ `hooks/tezgah_integrity.py::RESIDUAL_LABEL` holds the words, and
412
+ `hooks/tezgah_integrity.py::residual_sections` reads them. "Kalan" is also an adjective, as in
413
+ "Kalan riskler". It opens a list only as the first word, on the work it names: "Kalan:", "Kalan
414
+ iş", "Kalan adımlar" (`hooks/tezgah_integrity.py::RESIDUAL_FIRST`). A heading holds the lines up
415
+ to the next heading of its level, or up to prose after a blank line. Some labels start no list.
416
+ Their own words close it ("Düzeltilen eksik", "Doğrulanmayan kalmadı"), or a few words say that
417
+ nothing is left ("Eksik yok"). "Doğrulanmadı, kanıt yok" still starts one. The class refuses an item that nothing accounts
418
+ for (`hooks/tezgah_integrity.py::unblocked_items`). Two things do. One is a capitalised KARAR or
419
+ GERİ ALINAMAZ token outside code, a Turkish suffix allowed and a negation not: a choice or a
420
+ consent leaves no row to check. A KARAR item also names its options or a recommended default
421
+ (`hooks/tezgah_integrity.py::OPTIONS_MARK`, `hooks/tezgah_integrity.py::DEFAULT_MARK`): a
422
+ choice with neither is one the agent can make (plan 086, S3). The other is a capitalised YETKİ or ORTAM token with its
423
+ evidence (`hooks/tezgah_integrity.py::_tried_holds`). The item names an error of the type's
424
+ kind. For ORTAM that is a missing tool, file, module, setting or quota. For YETKİ it is a denied
425
+ permission, a failed login or a 401/403. The item also quotes, in backticks or a fenced line, a
426
+ command that failed in the turn. A quote cut with "…" counts when its words open that command.
427
+ A grep alone that matched nothing and a failed write tool call are no failed command
428
+ (`hooks/tezgah_integrity.py::_failed_commands`). For an item that names a subagent, an agent, a
429
+ provider or a model, a failed spawn call can stand in for the quote. Its row names a provider
430
+ failure (`hooks/tezgah_integrity.py::_spawn_failed`). omp sends a spawn's outcome as a
431
+ flag with no text. So its row carries no class, and the rule refuses such an item there.
432
+ A YETKİ or ORTAM item that quotes a model provider's quota or rate limit is refused while
433
+ another family is up that the routing lets a chain fall back to. The item names a family by
434
+ its provider id or model stem, or the turn's spawn failed at the provider. The words "agent"
435
+ and "provider" alone tie nothing, so a `user-agent` or a registry's rate limit stays a blocker.
436
+ The journal first closes ended omp children, so a child's limit already holds its family
437
+ (`hooks/tezgah_integrity.py::_settle_slices`). The refusal names the family to re-dispatch to
438
+ (`hooks/tezgah_integrity.py::_limit_move`), never one the item names.
439
+ Neither the type nor the quote counts alone. "Doğrulanmadı: `pytest` ..." with no type is still
440
+ refused, and so is "ORTAM" with no quote. The rule refuses "ORTAM: `pytest` AssertionError" too,
441
+ because an assertion is work, not a missing tool. "YETKİ yok" is the type, not its negation. Only
442
+ "değil" negates it. A fragment of a command does not count. A type in the heading covers the
443
+ items under it. "Engeller: yok" and an empty "Engeller" heading hold no item. An item under a
444
+ blocker label that reports the blocker gone holds none either. It has a sentence in the past,
445
+ then a last sentence that opens on "Artık" with a present verb ("… yoktu. Artık var."). A
446
+ negative past ("güncellenmedi") or a last sentence with a residual word or a future keeps it
447
+ open (`hooks/tezgah_integrity.py::_now_closed`). The refusal names the blocker tokens of the
448
+ configured language. Under `tr` it opens with "Bu maddeleri şimdi yap veya engel türü ve
449
+ kanıtla yaz.", and under `en` it opens on the English instruction. It also says that a step
450
+ that cannot be undone never clears the list.
451
+ `_stop_block` asks this class at every exit that would end the turn
452
+ (`hooks/tezgah_integrity.py::_residual_block`). So it never takes a turn that another class
453
+ refused. A turn with no step, a ledger with no turn marker and a nested session skip it. A
454
+ subagent's end is never refused (ADR 011). Its `subagent_end` row carries the count of its
455
+ untyped items as `residual`. The parent's Stop then refuses a reply that types no item of its
456
+ own. Claude and Cursor write that row from their subagent-stop events. omp fires none for a
457
+ task session. Its bridge sends the child's last reply from `agent_end`. The row lands in the
458
+ ledger of the session that spawned the child, read from the child's own session header, and
459
+ counts once per child. Codex, opencode and dsh write no such row. On omp the reply after a
460
+ refusal sits in a turn of its own.
461
+ `hooks/tezgah_integrity.py::_turn_refusals` hands the class the rows of every turn in that
462
+ re-ask chain, so a failure quoted from the refused turn
463
+ counts. On Cursor, whose store keeps a long reply's head and tail, the sections of the
464
+ cut middle are kept between them (`hooks/tezgah_integrity.py::residual_text`). The words leave
465
+ out the labels other rules ask for: a spec's assumptions and alternatives, a measurement's
466
+ limits, risks and a bare recommendation. Suggested improvements are work left and stay in.
467
+ Before the class, 503 of 1876 final replies carried a residual label. They came from the owner's
468
+ omp and Claude transcripts, and 467 of them from turns that called a non-read tool. The word list
469
+ is precise rather than complete. A doable item typed KARAR with options still passes.
470
+ `tezgah-gate replay` counts the replies that end on a question to watch for that, beside the
471
+ residual sections per turn ([the replay corpus](#the-replay-corpus)). A residual refusal counts in `blocked_claims`
472
+ but not as a false completion: a check may back the claim beside the list.
473
+ 12. **ready steps** — a turn that did work ends while the newest todo list still holds a ready
474
+ step. The host's todo list is the orchestrator's step queue, and a todo call leaves a `todo` row
475
+ (`hooks/tezgah_integrity.py::note_todo`). A `pending` item is ready. Someone holds an
476
+ `in_progress` item. An item omp marks `blocked`, or whose own text types a blocker, waits on
477
+ something named. Done items leave the list. The row keeps every ready head in `heads`. The
478
+ class reads the newest `todo` row of the session's own list. A subagent's row carries its
479
+ `agent`, and the class skips it (`hooks/tezgah_integrity.py::newest_todo`). The row may come from an
480
+ earlier turn on purpose: the owner asked that unfinished steps carry over to the next turn
481
+ (`hooks/tezgah_integrity.py::ready_left`). It scans the rows back by their serialized shape and
482
+ parses them until the newest parent row. An open slice of this session takes its step off the
483
+ ready list (`hooks/tezgah_integrity.py::ready_heads`). The slice's id names the head, or the
484
+ slice's own `task` holds the whole head as words. An omp batch's shared context does not count.
485
+ On omp, `todo start` puts every other started item
486
+ back to pending, so a running slice's item would read as ready without this. A ready item
487
+ passes when one line of the reply names its first 24 characters, inline code kept, beside a
488
+ blocker token (any pack's spelling: DECISION, ACCESS, IRREVERSIBLE, ENV, or KARAR, YETKİ, GERİ
489
+ ALINAMAZ, ORTAM) outside code. Under `tr` the refusal opens with "Hazır adımlar bekliyor." It
490
+ names up to three heads and tells the turn to dispatch them or mark them blocked.
491
+ `_residual_block` asks it after `residual work` and before
492
+ `stopped slice`. The scope matches `residual work`. A turn with no step skips it. So do a
493
+ ledger with no turn marker, a subagent's end, a nested session and `orchestrate-off`. The rule
494
+ refuses it once per turn (`hooks/tezgah_integrity.py::_record_only`). It counts in
495
+ `blocked_claims` but not as a false completion (`hooks/tezgah_integrity.py::LIST_BLOCKS`).
496
+ The research line `orchestrator-flow-audit` measured the gap before the class. Of 451 omp user
497
+ turns, 13.1% were a bare "devam". Of the 98 continue or status prompts, 18 came after a reply
498
+ that handed back a step the agent could take. On omp, `wait` gets a warning, never a refusal
499
+ (`hooks/tezgah_gate.py::ready_advice`). The warning needs a spawn with nothing after it but
500
+ waits and todo updates, and a ready item no running slice names. It rides back with the wait's
501
+ result.
502
+ 13. **needless ask** — the reply ends on a question the agent can decide (plan 086, S3). The
503
+ last line asks: it ends on "?" or a pack's question particle
504
+ (`hooks/tezgah_integrity.py::QUESTION_END`). Its paragraph names no recommended default, and
505
+ it names no step the ask policy keeps from a default (`hooks/tezgah_integrity.py::never_default`).
506
+ Such steps are a force push, a delete, production, a credential, a third party, or a spend
507
+ over `spend_cap`. The class then refuses a process question: the packs' `ask.process`
508
+ shapes, such as "Başlayayım mı?", "shall I proceed?" and "which first?"
509
+ (`hooks/tezgah_integrity.py::PROCESS_ASK`). It also refuses a question about an outward
510
+ step the user granted this session (`hooks/tezgah_integrity.py::grants_in_force`). A question
511
+ about an outward step the user has not granted is the one the policy asks, so it passes. The
512
+ refusal says to take the step, or to move it into the approval batch with a default. Under
513
+ `tr` it opens with "Bu soruyu sorma". `_residual_block` asks it after `residual work` and
514
+ before `ready steps` (`hooks/tezgah_integrity.py::needless_ask`). Unlike those two, it needs
515
+ no work in the turn, because a plan proposal that ends "Başlayayım mı?" is its measured case.
516
+ The research line counted 33 process questions among 48 question endings in the owner's omp
517
+ transcripts. It refuses once per turn and counts like `residual work`
518
+ (`hooks/tezgah_integrity.py::LIST_BLOCKS`).
380
519
 
381
520
  Classes 5–10 also exist as a past-time temporal spec (`hooks/tezgah_stopspec.py::FORMULAS`,
382
521
  plan 063). Seven formulas read ten row atoms (`ROW_ATOMS`) and the turn marker. The module compiles
@@ -386,7 +525,8 @@ fold, inside its own `try`, when the reply leaves a claim or a refusal row. It w
386
525
  per reply. The row says `agree`, `disagree` or `error`. It also says `fold` when the selector reached
387
526
  the fold and `selector` when it decided alone. The verdict stays the imperative one. Both folds share one selector,
388
527
  `_stop_block`, through its `fold` argument. It picks the rows, runs the lost-began path and
389
- refuses "evidence tampered". `ESCAPES` names the three predicates that read beyond one row: the
528
+ refuses "evidence tampered". The admission below and class 11 belong to the selector too. The
529
+ spec's verdict passes through them. `ESCAPES` names the three predicates that read beyond one row: the
390
530
  began-row join, the repository binding and the lost-began set. Plan 063 fixed that list before
391
531
  the first shadow row. `TEZGAH_STOPSPEC_STRICT=1` makes a disagreement raise, for the tests.
392
532
  `tests/stop_spec_eval.py` measures the bet: agreement on plan 055's replayed Stop events and on
@@ -403,6 +543,13 @@ whole session instead of the turn:
403
543
  A session that did no work at all still ends, and a turn that ran its own passing check is
404
544
  judged by the turn fold alone. Before this, the claim could be moved to the turn
405
545
  after the work, where the turn fold saw nothing to judge (audit M-2, INT-01).
546
+ - **A turn of reads.** The rule treats a turn of known reads as a turn with no work (plan 086,
547
+ S4). Each command in it is a known read: a program in `READ_ONLY_PROGRAMS` or a git read in
548
+ `GIT_READS`, with no option in `WRITING_OPTIONS`. `sed`, `awk`, `uniq`, `gh` and `codegraph`
549
+ count with no writing script, one operand or a read verb. A `sqlite3` read-only open counts,
550
+ from the option or the database URI, and so does an `ssh` whose remote command is
551
+ such a read (`hooks/tezgah_integrity.py::_bookkeeping_command`). Any other command, a commit included,
552
+ keeps the turn worked and owes the check.
406
553
  - **A bookkeeping turn after a pass.** A turn whose only rows are read-only or VCS-bookkeeping
407
554
  commands ends without a fresh check, as long as nothing but bookkeeping ran after the session's
408
555
  newest pass, the session fold above allows it, and the reply makes no external-state claim
@@ -425,8 +572,21 @@ a fresh screen proof stands in the same
425
572
  place as a unit pass; when that write was a UI source, the passing row has to be a UI proof — a
426
573
  check that renders, or a read of the screen — and when it was a component, the design check has to be
427
574
  newer than that write as well. An explicit admission (`doğrulanmadı`,
428
- `unverified`, `not verified`, `couldn't verify`; `hooks/tezgah_integrity.py::NEGATED`) clears the rule (`hooks/tezgah_integrity.py::_stop_block`),
429
- checked after the shape branches and before the evidence triggers. The block text is the string the
575
+ `unverified`, `not verified`, `couldn't verify`; `hooks/tezgah_integrity.py::NEGATED`) clears a
576
+ class whose repair is a check (`ADMITTED_CLASSES`). Those are `check failed`, `partial failure`,
577
+ `stale evidence` and `no verify_ok`. It clears them only when a check ran and failed after the
578
+ turn's newest change (`hooks/tezgah_integrity.py::_failed_since_change`, applied by
579
+ `hooks/tezgah_integrity.py::_admitted`). Until plan 080 the word alone cleared every evidence class.
580
+ Writing it was cheaper than verifying. The word is read on the reply's prose, so a test selector
581
+ in code is no admission. After it clears the failure, the fold runs again without the failed
582
+ checks. A screen or design proof the turn still owes comes back as `no ui_ok`, and an external
583
+ claim with no read comes back as `no external read`. Otherwise a failed unit check would end a
584
+ turn that its passing twin cannot. One failure is that proof's own attempt. When the check that
585
+ failed is the one that sees the screen, after the newest UI write, the word clears `no ui_ok` too
586
+ (`hooks/tezgah_integrity.py::_proof_attempt_failed`). That covers the design check as well, which
587
+ measures the screen that did not render. A failed design check does the same for a component with
588
+ a proven screen. The selector refuses `evidence tampered` first, because the
589
+ admission now rests on the ledger's own rows. The block text is the string the
430
590
  refusing branch returns; it names the failed
431
591
  command (`hooks/tezgah_integrity.py::_failed_check`) and tells the model to report the failure with its exact error
432
592
  line, or fix it and re-run.
@@ -480,9 +640,9 @@ therefore invisible to the line and its legend - no mark's measure is spelled th
480
640
  line never asked. Three surfaces recognise a read, and they have to agree on the two forms
481
641
  (`skills/<name>/SKILL.md` and `skill://<name>`) and on which name is shipped: the Python hook omp's
482
642
  bridge calls (`hosts/omp/hook.py::classify`), omp's own filter, which decides whether python hears about the
483
- read at all (`hosts/omp/tezgah-hook.ts.in:225-262`, its catalogue the checkout `@HOOK@` names), and
643
+ read at all (`hosts/omp/tezgah-hook.ts.in:227-264`, its catalogue the checkout `@HOOK@` names), and
484
644
  opencode's plugin, which classifies in process because that host has no Python hook
485
- (`hosts/opencode/plugins/tezgah.js:2162-2219`, its catalogue the host's own installed skill dir
645
+ (`hosts/opencode/plugins/tezgah.js:2185-2242`, its catalogue the host's own installed skill dir
486
646
  (`dirname(CONFIG)/opencode/skills`), which `tezgah-setup` fills with every shipped skill).
487
647
 
488
648
  Claude loads a skill through its `Skill` tool, never a read, so a Claude load used to leave no row.
@@ -503,8 +663,8 @@ as having opened a skill when it recorded that skill's kind, so reads recorded b
503
663
  still count through the two marks; and only the hosts that classify a read in process record one
504
664
  here, so the count is a floor, not a census. Which host records what, since this store is the
505
665
  report's only source: **omp** (the extension's filter lets the read through,
506
- `hosts/omp/tezgah-hook.ts.in:255-262`, and the hook classifies it, `hosts/omp/hook.py::classify`) and
507
- **opencode** (the plugin classifies it, `hosts/opencode/plugins/tezgah.js:2204-2219`) record the mark
666
+ `hosts/omp/tezgah-hook.ts.in:257-264`, and the hook classifies it, `hosts/omp/hook.py::classify`) and
667
+ **opencode** (the plugin classifies it, `hosts/opencode/plugins/tezgah.js:2227-2242`) record the mark
508
668
  or `skill:<name>` for every shipped skill, in either form; **Claude** sees a read, but only in its
509
669
  transcript, which feeds the status line and writes no row here (`statusline.py::claude_used`), so a Claude
510
670
  session reads in this report as one that opened nothing - marks included; **codex**, **cursor** and
@@ -621,7 +781,7 @@ empty stand-in for the result body its bridge never sends, so a subagent's repor
621
781
  cannot import the core in process and mirrors the control in JavaScript instead — the channel on the
622
782
  call's own row, the label and the taint notice in front of the result the hook is handed, including
623
783
  the `external` row an MCP answer or a fetched page earns (`untrustedSource`
624
- `hosts/opencode/plugins/tezgah.js:1919`, `labelResult` `:1993`, with the tier's argv reader
784
+ `hosts/opencode/plugins/tezgah.js:1942`, `labelResult` `:1993`, with the tier's argv reader
625
785
  `tierRead` at `:1902`). The two halves are pinned against each other over a shared corpus, so neither
626
786
  can move without failing the other's test (`tests/test_opencode_plugin.py::OpenCodePlugin.test_the_classifier_agrees_with_the_python_half_on_a_shared_corpus`). One difference is deliberate:
627
787
  opencode's plugin writes that `external` row itself and carries the channel alone, since the UI rule
@@ -648,14 +808,16 @@ Each key, as both readers produce it:
648
808
  - `tool_error_rate` — non-zero `exit` values over every row carrying an `exit` (`hooks/tezgah_integrity.py::_counts`);
649
809
  a host reporting no outcome contributes to neither half, so every row with an `exit` counts.
650
810
  - `claims` and `false_completion` — the `claim` rows, which are replies in the claim vocabulary,
651
- and those whose detail starts with `blocked` (`hooks/tezgah_integrity.py::_counts`). A shape class (`SHAPE_BLOCKS`) is
652
- the exception. A reply refused for its list or its language made no false claim, so it counts
653
- as `shape_blocked` instead.
811
+ and those whose detail starts with `blocked` (`hooks/tezgah_integrity.py::_counts`). Two classes
812
+ are the exception. A shape class (`SHAPE_BLOCKS`) refuses a reply for its list or its language,
813
+ which made no false claim, so it counts as `shape_blocked` instead. `residual work`
814
+ (`LIST_BLOCKS`) refuses a reply for the work it leaves, not for its claim.
654
815
  - `refusals` — the `refusal` rows: refused replies that claimed nothing, work-only or shape. A
655
816
  shape one also counts in `shape_blocked`. Before row version 3 these were `claim` rows, so a
656
817
  corpus folded across the boundary counts them in `claims`.
657
- - `blocked_claims` — `false_completion` split by its Stop class, the text after `blocked: `
818
+ - `blocked_claims` — the refused claims split by their Stop class, the text after `blocked: `
658
819
  (`no verify_ok`, `stale evidence`, `no external read`, …), so each class has its own count.
820
+ Every class but `residual work` (`LIST_BLOCKS`) is also a `false_completion`.
659
821
  - `replies` and `shape` — the `shape` rows, written for every judged reply, and those carrying a
660
822
  report-only flag (`hooks/tezgah_integrity.py::_counts`).
661
823
  - `denies` — `deny` rows grouped by the text before the first colon (`hooks/tezgah_integrity.py::_counts`), which is the rule
@@ -702,7 +864,7 @@ disagree with the number it is drawn from; both are off unless the flag asks, so
702
864
  name at all - `classify` folded it into a kind - so an old corpus under-counts, which is the
703
865
  honest ceiling and the reason the field exists; every row written since carries it, because both
704
866
  writers write it (`hooks/tezgah_integrity.py::note_tool`, and opencode's plugin
705
- `hosts/opencode/plugins/tezgah.js:1230-1241` - before that second one did, an opencode work row was
867
+ `hosts/opencode/plugins/tezgah.js:1253-1264` - before that second one did, an opencode work row was
706
868
  the other half of this under-count, with its name unrecoverable in the command `detail`). A read
707
869
  tool is not in it either: reads record no row by design.
708
870
  - **The shipped programs** — `programs`, the `bin/` programs a command row really ran, read with the
@@ -763,6 +925,16 @@ only, and never leaves the machine. The run's summary is its row of the cache's
763
925
  - **Denial budget** (`tezgah_shapes.deny_runs`) = per rule, runs of 3 or more consecutive denies
764
926
  inside one turn, and sessions with 20 or more denies. These are Claude Code auto mode's
765
927
  thresholds, not a tezgah measurement.
928
+ - **Ask policy** (`hooks/tezgah_replay.py::residual_turns`, plan 086 S3) = four counts over the
929
+ top-level omp and Claude transcripts, beside the residual sections. `process_questions` counts
930
+ final replies that the `needless ask` class reads as a process question. `cheap_answers` counts
931
+ prompts that only approve ("devam", "evet", "go on") right after a reply that ended on a
932
+ question. `asks_without_default` counts ask-tool questions with no recommended option.
933
+ `outward_without_grant` counts turns that ran `git push`, a package publish or a `gh` release
934
+ or PR write. Such a turn counts only when no earlier prompt of the session granted or named the
935
+ step. The transcripts
936
+ hold no grant rows, so the first count misses a re-ask after a grant. One outward turn without
937
+ a grant per 100 work turns over 14 days is the trigger for an outward-action gate (S3's V3).
766
938
 
767
939
  `replay --sheet` draws the blind label sample with a fixed seed. It fills per-stratum quotas,
768
940
  shuffles them, and writes `tezgah-taste measure`'s `{set, n, text}` format. No item shows its
@@ -841,8 +1013,9 @@ matched NEGATED and read as an admission:
841
1013
  The reader measures and does not decide. `claims` feeds only the benchmarks'
842
1014
  false-done counts (`benchmarks/arm-bench/bench.py::false_completion`,
843
1015
  `benchmarks/arm-bench/swe.py`), never the Stop decision. The Stop verdict and
844
- its `claim` row read the plain DONE/VERIFIED vocabulary with their own NEGATED
845
- exit, so no refusal changed. NEGATED and ADMITTED match anywhere in the reply,
1016
+ its `claim` row read the plain DONE/VERIFIED vocabulary. Their admission reads
1017
+ NEGATED after a check that failed since the newest change (plan 080). So the
1018
+ reader's change moved no refusal. NEGATED and ADMITTED match anywhere in the reply,
846
1019
  so a done claim beside an unrelated "no contradictions" or "never pass secrets"
847
1020
  is not counted. A false-done count taken before commit 9cb4d1d is not
848
1021
  comparable with one taken after it.
@@ -1,7 +1,8 @@
1
1
  # Feature audit: one feature, every layer
2
2
 
3
3
  Audience: the agent and the maintainer answering "why does this screen do that"
4
- or "make this feature better" on an existing product surface.
4
+ or "make this feature better" on an existing product surface. It also serves
5
+ whoever specifies a feature before the build.
5
6
 
6
7
  A screen review judges what is on the screen. Most of what goes wrong in a real
7
8
  feature is not on the screen: it is a disagreement between two layers - the surface
@@ -20,8 +21,8 @@ sourced to WAI-ARIA APG and WCAG 2.2.
20
21
  It is reached the same way `product-analysis` is: the `product` task class arms the
21
22
  rule (`hooks/tezgah_context.py::PROMPT_HINTS`), whose coherence paragraph
22
23
  (`hooks/tezgah_policy.py::CORE`) names the feature unit and the matrices, and
23
- `skills/product-analysis/SKILL.md:51` runs the coherence pass before its axes.
24
- `skills/tezgah-contract/SKILL.md:725-737` carries the long form for a session that
24
+ `skills/product-analysis/SKILL.md:74` runs the coherence pass before its axes.
25
+ `skills/tezgah-contract/SKILL.md:738-750` carries the long form for a session that
25
26
  reads the contract.
26
27
 
27
28
  The unit is the feature, not the screen, and every interface element and every data
@@ -38,6 +39,24 @@ step flow, a notification. It is not a form-only method.
38
39
  | Interaction dependency | the controls whose change alters another's valid set | dependents, required action on change, state owner, announcement | a stale dependent value; a silent reset; a filter showing a state the view no longer has |
39
40
  | Surface pattern | the controls, data views, flows and notifications | the decidable rule, with its source | a combobox with no expanded state; a results count announced nowhere; a destructive confirmation that hides what it destroys |
40
41
 
42
+ ## Forward mode: the spec before the build
43
+
44
+ For a feature that does not exist yet, fill the five matrices from the decision
45
+ first. They go into `.tezgah/analysis/<feature>/spec.md` before the build. Mark a
46
+ cell the decision leaves open, and ask it. Never guess it.
47
+
48
+ After the build, run the normal audit and compare it with `spec.md` row by row.
49
+ Every disagreement is a finding. Examples: a capability no layer delivers, a field
50
+ no view renders, a missing state, an unasked layer. The method lives
51
+ in `skills/feature-audit/SKILL.md`, "Forward mode".
52
+
53
+ The decision the spec builds from is the one written under `## decision` in
54
+ `.tezgah/analysis/<topic>/flow.md`. `spec.md` names it, and so does the audit
55
+ after the build. The `product-flow` skill orders the steps around the audit:
56
+ `tezgah-research next --topic <t>` reads the forward spec as done once `spec.md`
57
+ exists (`hooks/tezgah_product.py::next_steps`). See "The next step of a product
58
+ flow" in `docs/research.md`.
59
+
41
60
  ## The gate that makes it more than advice
42
61
 
43
62
  A finding whose fix names a layer that does not exist yet is not a UI finding. It
@@ -128,7 +147,7 @@ rubric, then three passes of the feature-audit skill:
128
147
  ## Source of truth
129
148
 
130
149
  - `skills/feature-audit/SKILL.md` - the method: the five matrices, the proposal
131
- gate, the per-surface rule table and the hand-check list
150
+ gate, the forward-mode spec, the per-surface rule table and the hand-check list
132
151
  - `skills/product-analysis/SKILL.md` - the coherence pass and the artifact element
133
152
  that carries the matrices
134
153
  - `hooks/tezgah_context.py` - `PROMPT_HINTS`, whose `product` alternation carries
@@ -137,3 +156,5 @@ rubric, then three passes of the feature-audit skill:
137
156
  `skills/tezgah-contract/SKILL.md` as its long form
138
157
  - `tests/test_context.py`, `tests/test_setup.py` - the arming table and the router
139
158
  line that pin the wiring
159
+ - `skills/product-flow/SKILL.md` and `hooks/tezgah_product.py::FLOWS` - the step
160
+ order around the forward spec and the audit, printed by `tezgah-research next`