@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/gate.md CHANGED
@@ -34,7 +34,7 @@ writes stays under `~/.cache/tezgah/replay`. The corpus, the exclusions and the
34
34
  |---|---|---|
35
35
  | claude, dsh | `hooks/projects-pretooluse.py::main` (`hosts/dsh/hooks.json:13-14`) | `hookSpecificOutput.permissionDecision: "deny"` |
36
36
  | codex, cursor, omp | `hosts/codex/hook.py::main`, `hosts/cursor/hook.py::dispatch`, `hosts/omp/hook.py::handle` | that host's own envelope — [hosts.md](hosts.md) |
37
- | opencode | `hosts/opencode/plugins/tezgah.js:2406` | `new Error(deny)` thrown at `hosts/opencode/plugins/tezgah.js:2567` |
37
+ | opencode | `hosts/opencode/plugins/tezgah.js:2429` | `new Error(deny)` thrown at `hosts/opencode/plugins/tezgah.js:2590` |
38
38
 
39
39
  Every refusal is also recorded before it is returned: `_deny` appends a ledger row `deny` whose `detail` is `"<rule>: <reason, first 80 chars>"`, plus any
40
40
  `extra` (`hooks/tezgah_gate.py::_deny`). That row, not the reason wording, is where "why was this denied" is answered, and `<rule>` is the name used below.
@@ -93,7 +93,7 @@ It reads the arguments of `touch`, `rm`, `rmdir`, `unlink`, `mv`, `chmod`, `mkdi
93
93
  It reads the value of `cp -t`, `curl -o`, `wget -O`, `tar -C` on extraction and `unzip -d` (`OPTION_TARGETS`). It expands `~`, `$HOME` and the XDG
94
94
  variables first. It follows a `cd` within the line and reads past `if`/`then`/`do`/`!`/`{`. It opens `eval` and `bash -c`, and any flag cluster holding
95
95
  `c` is `-c`. A path counts as written and with its last link resolved. A quoted word is a word, so a grep or a commit message that names a switch passes. `SHELL_WRITE` stays as it was: it feeds the
96
- task phase rule and the opencode mirror. The task CLI stays the task rule's (below), so `task-off` still removes all of that rule. Told: what the call would
96
+ task phase rule and the opencode mirror. The task CLI is not a control-plane target: the session runs it itself (see the task rule below). Told: what the call would
97
97
  change, and that the user changes it in their own terminal (`CONTROL_DENY` `hooks/tezgah_gate.py::CONTROL_DENY`). The text names no command. Standing. No switch
98
98
  but `pretooluse-off` removes the rule, and that switch is a protected file. The rule cannot tell a change the user asked for from one the agent chose. It
99
99
  refuses both inside the session, and it never sees the user's own terminal. Plan 050's replay of three weeks of real ledgers found the two evidence-only
@@ -130,7 +130,7 @@ Passes: a line that opens with `set -o pipefail` (`hooks/tezgah_integrity.py::PI
130
130
  (`verify_ok`/`verify_fail`, [evidence.md](evidence.md)); a redirect to a file; a pipe of anything that is not a check (`git log | head`). The
131
131
  remedy is one sentence, `hooks/tezgah_integrity.py::PIPED_REMEDY`, which the always-on core and the contract skill quote too. Told: keep the check last with its output in a file, then read the file in a separate call, or open the line with `set -o pipefail;`. A `; tail` on the same line hands the status to `tail`. The refusal names the check as it was typed. Standing, under `verify-off`
132
132
  (`hooks/tezgah_gate.py::decision`). opencode asks the core for this rule on any shell line that pipes a check
133
- (`hosts/opencode/plugins/tezgah.js:2492-2496`) and records such a line as `verify` when a pipe or a later command owns its status (`statusHidden`, `hosts/opencode/plugins/tezgah.js:445`).
133
+ (`hosts/opencode/plugins/tezgah.js:2515-2519`) and records such a line as `verify` when a pipe or a later command owns its status (`statusHidden`, `hosts/opencode/plugins/tezgah.js:445`).
134
134
 
135
135
  ### Attribution — an AI/model credit on its way into an artifact
136
136
 
@@ -144,9 +144,9 @@ a `git`/`gh` write for it to be this rule's. An MCP `write` or `publish` call me
144
144
 
145
145
  ### Language — an identifier or message that is not English
146
146
 
147
- Trigger, shell: a command that would create an identifier - `git checkout -b`/`git switch -c`, `git branch` with a new name, a commit subject (`git commit -m`/`--message`/`-F`), or a `gh pr|issue create --title` (the language section `hooks/tezgah_gate.py:354-467`, `hooks/tezgah_gate.py::created_texts`; the text is read from the raw command, because `mask()` blanks quoted strings and a commit subject is one) - whose name or subject is **not English**: a letter outside ASCII anywhere (`hooks/tezgah_lang.py::non_english` - a predicate rather than one language's letter list, so Russian, Greek, Arabic, Chinese and accented Latin hit it exactly as Turkish does) or an ASCII-folded Turkish word from the curated list (`hooks/tezgah_lang.py::ENGLISH`, wi…
147
+ Trigger, shell: a command that would create an identifier - `git checkout -b`/`git switch -c`, `git branch` with a new name, a commit subject (`git commit -m`/`--message`/`-F`), or a `gh pr|issue create --title` (the language section `hooks/tezgah_gate.py:355-468`, `hooks/tezgah_gate.py::created_texts`; the text is read from the raw command, because `mask()` blanks quoted strings and a commit subject is one) - whose name or subject is **not English**: a letter outside ASCII anywhere (`hooks/tezgah_lang.py::non_english` - a predicate rather than one language's letter list, so Russian, Greek, Arabic, Chinese and accented Latin hit it exactly as Turkish does) or an ASCII-folded Turkish word from the curated list (`hooks/tezgah_lang.py::ENGLISH`, wi…
148
148
 
149
- The rule is a **heuristic** and its own text says so: a word list is not a language detector, so it cannot tell a proper noun or a product name in another language from a word that ought to be English, and a listed word may be the user's own term. A command that only names the words - `echo`, `grep`, `git log --grep` - creates nothing and passes. An identifier outlives the session that typed it, which is the reason the rule exists at all: `plan/004-admin-durum-onarimi` is in a public history from the moment it exists. On **opencode** the rule is not ported: the plugin asks the core for it through `bin/tezgah-gate check`, one spawn for a command its pre-test reads as identifier-creating (`IDENT_CMD` `hosts/opencode/plugins/tezgah.js:585`, the call site `hosts/opencode/plugins/tezgah.js:2492-2496`).
149
+ The rule is a **heuristic** and its own text says so: a word list is not a language detector, so it cannot tell a proper noun or a product name in another language from a word that ought to be English, and a listed word may be the user's own term. A command that only names the words - `echo`, `grep`, `git log --grep` - creates nothing and passes. An identifier outlives the session that typed it, which is the reason the rule exists at all: `plan/004-admin-durum-onarimi` is in a public history from the moment it exists. On **opencode** the rule is not ported: the plugin asks the core for it through `bin/tezgah-gate check`, one spawn for a command its pre-test reads as identifier-creating (`IDENT_CMD` `hosts/opencode/plugins/tezgah.js:585`, the call site `hosts/opencode/plugins/tezgah.js:2515-2519`).
150
150
 
151
151
  Off: `lang-off`, the rule's own switch (`hooks/tezgah_gate.py::decision`), and `pretooluse-off` drops it with the whole gate.
152
152
 
@@ -155,26 +155,44 @@ Off: `lang-off`, the rule's own switch (`hooks/tezgah_gate.py::decision`), and `
155
155
  Trigger: another session recorded a write of this call's file inside `RACE_WINDOW_MIN = 10` minutes (`hooks/tezgah_gate.py::RACE_WINDOW_MIN`). The call's files are
156
156
  its path field, its `apply_patch` headers, or a shell redirect or `tee` target (`hooks/tezgah_gate.py::write_paths`, `hooks/tezgah_gate.py::race_reason`).
157
157
  The reader counts an `edit` row and a shell `run` row that carries a `target` (`hooks/tezgah_integrity.py::writers_elsewhere`).
158
- Scratch is skipped: a temp file or a device (`hooks/tezgah_integrity.py::scratch_target`). A `run` row that writes only scratch carries no `target`.
158
+ Scratch is skipped: a temp file or a device (`hooks/tezgah_integrity.py::scratch_target`). A `run` row that writes only scratch carries no `target`. The rule also skips the harness's own channels (`hooks/tezgah_gate.py::URI_CHANNEL`), such as `agent://`, `xd://` and `conflict://`. It skips a shell target with an unknown variable too.
159
+ It skips this session's own subagent once its slice record has closed after the subagent's newest write of the file (`hooks/tezgah_gate.py::ended_child`). The writer ledger's `spawned` row names the child, and the record is read after `pending` closes an ended omp child. A child with no record, an open one, or a write newer than the close stays refused.
159
160
  Told: re-read the file and re-apply the change to what is on disk now (`RACE_DENY`
160
- `hooks/tezgah_gate.py::RACE_DENY`). Standing (`RACE_REFUSE = True` `hooks/tezgah_gate.py::RACE_REFUSE`) — a notice would not close a silent overwrite — and it lifts only by time.
161
+ `hooks/tezgah_gate.py::RACE_DENY`). The text names each writer by its relation to this session (`hooks/tezgah_gate.py::RACE_WHO`, `hooks/tezgah_integrity.py::_relation`).
162
+ The relations are your subagent, your parent, a sibling subagent, an omp fork, the fork's origin and an unrelated session. The omp hook writes a fork's `forked` row from its header's `parentSession` (`hosts/omp/hook.py::handle`).
163
+ Standing (`RACE_REFUSE = True` `hooks/tezgah_gate.py::RACE_REFUSE`) — a notice would not close a silent overwrite — and it lifts only by time.
161
164
 
162
- ### Task — the user's own phase and path allowlist
165
+ A write into another repository than cwd's gets one notice per session and target repository, never a refusal (`hooks/tezgah_gate.py::cross_project_note`). It says that repository's plans, lessons and slice journal do not apply to this session. A worktree counts as its main checkout, and a repository's own `.tezgah` as that repository. Claude, dsh, Codex, Cursor and omp carry it on their post-tool channel. opencode does not.
166
+
167
+ ### Lease — a sibling slice owns this file
168
+
169
+ Trigger: a write into a file an open sibling slice owns, or into the main checkout while the writer's own brief declares a worktree (`hooks/tezgah_gate.py::lease_reason`).
170
+ A brief's `Owns:` line holds the globs, split on commas and spaces, or lists them as `- glob` items under it. The slice record stores them as `owns` (`hooks/tezgah_task.py::owned_globs`, `hooks/tezgah_task.py::open_slice`). The owner is the open record whose globs match (`hooks/tezgah_task.py::owner_of`). A reader, an agent named for a read-only role such as a reviewer, holds no lease.
171
+ The main-checkout refusal reads the worktree a `Worktree:`, `Checkout` or `Cwd:` line declares, the slice's own task first (`hooks/tezgah_task.py::_declared_in`). A path the brief only names declares nothing, and a declared main checkout means the slice works there. The journal still keeps the named worktree for reconcile, resume and the board.
172
+ A shell target resolves against the call's own `cwd` and every `cd` or `pushd` before it (`hooks/tezgah_gate.py::_shell_dir`). A redirect to `/dev/*` or a descriptor writes no file.
173
+ A slice may write a path its own `Owns:` names. The `.tezgah` directory and scratch stay free, and a closed owner frees its files.
174
+ On omp, the ledger's `spawned` row maps the writing session to its record (`hooks/tezgah_integrity.py::spawn_of`, `hooks/tezgah_task.py::slice_of`). A Claude background child maps through the `agent_id` its calls carry, which the spawn's result named (`hooks/projects-posttooluse.py`). Its SubagentStop closes the record (`hooks/tezgah_task.py::close_child`). Everything else fails open. A Claude foreground agent, dsh, Cursor and opencode open the record only once the child has ended, and Codex writes no `spawned` row.
175
+ Told: the owner, its globs, how to reach it - omp `write agent://<name>`, Claude `SendMessage`, else the parent - and the `tezgah-task slice done <id>` that frees it (`hooks/tezgah_gate.py::LEASE_DENY`, `hooks/tezgah_gate.py::MAIN_CHECKOUT_DENY`). Standing while the owner's record runs.
176
+ Off: `orchestrate-off`, whose fan-out rule it backs, and `slices-off`, which keeps no record to read (`hooks/tezgah_gate.py::decision`).
177
+ A spawn whose writing slices name one worktree gets a warning, never a refusal, since a reviewer panel shares a checkout freely (`hooks/tezgah_gate.py::spawn_advice`, `hooks/tezgah_task.py::worktree_clash`). The warning covers two slices in the call and one beside an open sibling. Omp, Claude and dsh carry it with the spawn's result.
178
+
179
+ ### Task — the active plan's phase and path allowlist
163
180
 
164
181
  Trigger: a write tool, or a shell command that writes a file, while an active task is set — a plan file under `.tezgah/plans/open` whose frontmatter carries a valid
165
182
  `phase` (`tezgah_task.active`). The phase refuses a write in a reading phase (`hooks/tezgah_gate.py::TASK_PHASE_DENY`) and a shell command that writes one (`hooks/tezgah_gate.py::TASK_SHELL_DENY`,
166
- against `hooks/tezgah_gate.py::SHELL_WRITE`); the allowlist refuses a target outside it (`hooks/tezgah_gate.py::TASK_SCOPE_DENY`) — a write tool's path, and a shell command's redirect or `tee` target (`hooks/tezgah_gate.py::write_paths`); and the record itself is refused twice over — a write whose target IS it, by a write tool or a shell redirect/`tee`
167
- (`hooks/tezgah_gate.py::TASK_RECORD_DENY`) and a shell command that would change it through the CLI, named by its basename with or without `.py` (`hooks/tezgah_gate.py::TASK_UNLOCK_DENY`, matched on the masked text by `hooks/tezgah_gate.py::TASK_CHANGE`). Told:
168
- none of them names the command that lifts it, on purpose — with the unlock printed, an armed arm removed or disabled the gate in 25 of 25 runs and obeyed it in none
169
- (`hooks/tezgah_gate.py:882-886`) — so each names the phase or the allowlist as the user's to change. Standing: only the record changing lifts it.
170
- Scratch meets no allowlist: a temp file or a device, such as `pytest > /tmp/check.log` (`hooks/tezgah_gate.py::task_reason`). Build output inside the repo stays held. Off: `task-off`
171
- (`hooks/tezgah_gate.py::decision`). It fails open wherever the question cannot be answered: no active task, no readable record; the record is written by `bin/tezgah-task` and
172
- never by the agent, so this rule refuses on a decision the user made rather than one the gate inferred.
173
-
174
- The fifth refusal under the same rule is the checkpoint. Trigger: a write, or a shell command that writes a file (`hooks/tezgah_gate.py::SHELL_WRITE`), while the task is in
183
+ against `hooks/tezgah_gate.py::SHELL_WRITE`); the allowlist refuses a target outside it (`hooks/tezgah_gate.py::TASK_SCOPE_DENY`) — a write tool's path, and a shell command's redirect or `tee` target (`hooks/tezgah_gate.py::write_paths`); and a write whose target IS the record, by a write tool or a shell redirect/`tee`, is refused
184
+ (`hooks/tezgah_gate.py::TASK_RECORD_DENY`), because only `bin/tezgah-task` writes it and that CLI makes the checks a retyped frontmatter would skip. Told:
185
+ the move that lifts it, made by the session itself — `tezgah-task phase implementation` or `tezgah-task allow <glob> ... --why "<reason>"` — since the orchestrator owns the plan lifecycle and the CLI refuses
186
+ the move until the plan's Acceptance, spike and checkpoint are in order, and records the reason for a widened scope (`allowed_why:`). The record's body has its own writers, on any open plan. `tezgah-task tick NNN <n> [--undo]` ticks Acceptance item n (`bin/tezgah-task::cmd_tick`). `tezgah-task note NNN state|next "<text>"` replaces the newest `## State` or the `## Next` body (`bin/tezgah-task::cmd_note`). Neither commits. Standing: only the record changing lifts it.
187
+ Scratch meets no allowlist: a temp file or a device, such as `pytest > /tmp/check.log` (`hooks/tezgah_gate.py::task_reason`). Build output inside the repo stays held.
188
+ Three targets are no project file, so neither the phase nor the allowlist refuses them (`hooks/tezgah_gate.py::unnamed_target`). A URI is one: a tool device (`xd://<tool>`), an agent message (`agent://X`) or a conflict resolution (`conflict://2`). A `file://` URI names a checkout file and stays held. A shell target with an unknown variable is another. `write_paths` first expands a variable from an assignment earlier on the line. So the allowlist holds `B=src; git diff > $B/x.patch` as `src/x.patch`. A variable the same line set to the checkout, such as `R=$(git rev-parse --show-toplevel)`, is read as the checkout (`hooks/tezgah_gate.py::known_tail`). The third is tezgah's own workspace under `.tezgah/`, such as research lines, lessons and briefs (`hooks/tezgah_gate.py::workspace_free`). The slice journal stays held. So does a plan file whose text sets `phase:` and every shell write into `.tezgah/plans/open/`, because such a file could make itself the active task. The record rule still refuses the active record there.
189
+ The gate matches a file in a linked worktree of the same repository relative to that worktree's top (`hooks/tezgah_task.py::relative`). Each writing slice gets its own worktree, so `hooks/a.py` there is in scope when `hooks/a.py` is. Only the allowlist reads a sibling: the record and workspace rules keep the main root. The worktree's admin directory must point back at it, and a worktree of another repository stays outside. Off: `task-off`
190
+ (`hooks/tezgah_gate.py::decision`). It fails open wherever the question cannot be answered: no active task, no readable record.
191
+
192
+ The fourth refusal under the same rule is the checkpoint. Trigger: a write, or a shell command that writes a file (`hooks/tezgah_gate.py::SHELL_WRITE`), while the task is in
175
193
  `phase: implementation`, its record reads `checkpoint: pending <sha>`, that sha is still the checkout's HEAD and the tree is dirty (`checkpoint_reason()`
176
194
  `hooks/tezgah_gate.py::checkpoint_reason`, `hooks/tezgah_gate.py::CHECKPOINT_DENY`). Told: the task it read, that the record holds a `pending` checkpoint, and the one command
177
- that makes the boundary real - unlike the refusals above it names that command, because running it *satisfies* the rule rather than taking it off. Standing: the recorded sha is the switch, not the tree - the commit
195
+ that makes the boundary real - it names that command because running it *satisfies* the rule rather than taking it off. Standing: the recorded sha is the switch, not the tree - the commit
178
196
  the refusal names moves HEAD off that sha, so the refusal clears itself, where keying on the word `pending` alone left the phase waiting on a record only
179
197
  `bin/tezgah-task` rewrites. Off, with the rest of the rule: `task-off`. It fails open wherever the question cannot be answered: no record, no `checkpoint:` key, a
180
198
  checkpoint that is a plain sha (the boundary is already real), a `pending` with no sha, a sha this checkout's HEAD does not name, and a repo whose HEAD cannot be read.
@@ -236,7 +254,7 @@ tail still shows that failure, under `verify-off` (`hooks/tezgah_gate.py::decisi
236
254
  ### Loop — the identical attempt that already failed twice
237
255
 
238
256
  Trigger: the same call (same `call_id`) failed `LOOP_ATTEMPTS = 2` times already in the current user turn (`hooks/tezgah_gate.py::LOOP_ATTEMPTS`, `hooks/tezgah_gate.py::loop_reason`). Told: attempt N
239
- of an identical call, the failure class read from the ledger (`transient`, `user`, `permanent`, `unknown`; the cap is the same for every class, and a `user` failure - a 401/403, a missing credential, a login - says to ask the user), "change the approach or stop" (`hooks/tezgah_gate.py::loop_reason`). Standing while the ledger tail still shows those
257
+ of an identical call, the failure class read from the ledger (`transient`, `user`, `permanent`, `unknown`; the cap is the same for every class, and a `user` failure - a 401/403, a missing credential, a login - says to take a route that needs no credential first and ask the user once only when none is left), "change the approach or stop" (`hooks/tezgah_gate.py::loop_reason`). Standing while the ledger tail still shows those
240
258
  failures. Under `verify-off` (`hooks/tezgah_gate.py::decision`).
241
259
 
242
260
  ### Retry — the session-wide ceiling
@@ -264,6 +282,18 @@ Trigger: 25 work rows (`hooks/tezgah_gate.py::DRIFT_STEPS`) in the current user
264
282
  `hooks/tezgah_gate.py::effectful`). An MCP call of any verb class counts as an effectful call too. Told: the standing constraints re-stated, or the delta since the turn began; "re-issue this call unchanged and carry on" (`hooks/tezgah_gate.py::DRIFT_DENY`,
265
283
  `hooks/tezgah_gate.py::drift_reason`). A refusal, last in `decision` after every other rule, and its only delivery: no host's PostToolUse side carries it, and opencode gets it from the core through `bin/tezgah-gate` on its write path. Once per turn: the `drift` mark is written *before* the refusal, so the identical call passes on the next attempt, and the count behind it is the turn's own — a bounded read (`hooks/tezgah_gate.py::DRIFT_TAIL`) falls back to the turn's own start (`hooks/tezgah_integrity.py::turn_rows`) when the turn outgrew the window. Governed by `reminder-off`, not `verify-off` (`hooks/tezgah_gate.py::drift_reason`).
266
284
 
285
+ ### Ask — an ask-tool question with no recommended option
286
+
287
+ Not a rule of `decision`: the hosts ask for this warning after `decision` lets the call through, as they ask for the graph line.
288
+ Trigger: an omp `ask` call or a Claude AskUserQuestion call inside a root (`hooks/tezgah_gate.py::ask_advice`).
289
+ The gate counts the questions and the ones that carry a recommended option (`hooks/tezgah_integrity.py::undefaulted`).
290
+ omp marks one with a question's `recommended` index. Claude marks one with an option label such as "(Recommended)" or "(önerilen)".
291
+ Told: how many questions carry no default, and that each question names one, unless the step waits on the user by nature (`hooks/tezgah_gate.py::ASK_NO_DEFAULT`).
292
+ A question about a step the user granted this session gets a second line: take the step, do not ask again (`hooks/tezgah_gate.py::ASK_GRANTED`).
293
+ A warning, never a deny: the line rides back with the call's result, as the graph line does. Every such call leaves an `ask` row with `questions` and `defaults`.
294
+ Claude routes the tool through its PreToolUse matcher (`hooks/hooks.json`), and omp through its `GATED` list (`hosts/omp/tezgah-hook.ts.in`).
295
+ Off under `verify-off` and `pretooluse-off`. The rest of the ask policy is the Stop rule's `needless ask` class ([evidence.md](evidence.md#the-stop-rule-end-to-end)).
296
+
267
297
  ## What the gate deliberately does not catch
268
298
 
269
299
  Read this before filing a security-ish issue; each is a decision, not an oversight.
@@ -273,9 +303,11 @@ Read this before filing a security-ish issue; each is a decision, not an oversig
273
303
  front of every server-side command cost a round-trip per effect, the user's chat approval could not lift it (only the deleted `bin/tezgah-consent` CLI
274
304
  could), and the sessions it actually gated were the user's own deploys. The untrusted-read half went with it: a turn that read web content may now run an
275
305
  outbound effect carrying only the taint notice, with no refusal in between. The notice itself stays (`hooks/tezgah_untrusted.py`).
306
+ The ask policy replaced it (plan 086, S3). It is a class table in the contract, the Stop rule's `needless ask` class and the ask-tool warning above.
307
+ A grant the user writes in chat ("bana sormadan pushla") lifts the ask for its class. Outward commands still run with no refusal.
276
308
  - A write inside the root after a fetch: left to the taint notice that `hooks/tezgah_untrusted.py` rides the first effect with, because it is recoverable from
277
309
  the snapshot.
278
- - `grep -A 3 foo`: a flag with its own value shifts the token, so the search passes unnudged (`hooks/tezgah_gate.py:165-166`).
310
+ - `grep -A 3 foo`: a flag with its own value shifts the token, so the search passes unnudged (`hooks/tezgah_gate.py:166-167`).
279
311
  - A foreign `apply_patch` write: the PostToolUse writer records no path for that row (`hooks/tezgah_gate.py::write_paths`); and the guard skips a row an older writer left with no
280
312
  `target`, because that row names no file (`hooks/tezgah_integrity.py::writers_elsewhere`).
281
313
  - Which rule the user meant when a turn drifts: "a guess about intent wearing a check's clothes" (`hooks/tezgah_gate.py::drift_reason`).
@@ -320,17 +352,17 @@ widen a list a regex already covers.
320
352
  ## The mirror: the opencode plugin
321
353
 
322
354
  opencode cannot run Python hooks, so `hosts/opencode/plugins/tezgah.js` is an independent JavaScript re-implementation of the same rules, dispatched from
323
- `tool.execute.before` (hosts/opencode/plugins/tezgah.js:2406, exported `hosts/opencode/plugins/tezgah.js:2342`) in the gate's own order (`hosts/opencode/plugins/tezgah.js:2406-2568`); its shortcut constants restate the same regexes (`NEUTER` `hosts/opencode/plugins/tezgah.js:270`,
355
+ `tool.execute.before` (hosts/opencode/plugins/tezgah.js:2429, exported `hosts/opencode/plugins/tezgah.js:2365`) in the gate's own order (`hosts/opencode/plugins/tezgah.js:2429-2591`); its shortcut constants restate the same regexes (`NEUTER` `hosts/opencode/plugins/tezgah.js:270`,
324
356
  `SKIP_ENV` hosts/opencode/plugins/tezgah.js:271, `NO_VERIFY` hosts/opencode/plugins/tezgah.js:272, `GITISH` hosts/opencode/plugins/tezgah.js:273, `SKIP_TEST` hosts/opencode/plugins/tezgah.js:515). What keeps the two halves honest is a test, not a shared module:
325
357
  `tests/test_opencode_plugin.py` drives the plugin's hooks through a node harness with a throwaway HOME, and imports the Python `tezgah_integrity.call_id` so a
326
358
  separator or canonical-form drift in the action id fails there instead of silently in a session (`tests/test_opencode_plugin.py:1-7`, `tests/test_opencode_plugin.py:24-27`, `tests/test_opencode_plugin.py::OpenCodePlugin.test_the_id_is_the_hash_the_python_writer_computes`).
327
359
 
328
360
  The shell rule kinds mirrored last are each pinned in both suites: the shell's write body (`SHELL_WRITE` `hosts/opencode/plugins/tezgah.js:979`,
329
361
  `heredocBodies` `hosts/opencode/plugins/tezgah.js:985`, `shellWriteBody` `hosts/opencode/plugins/tezgah.js:1018`, the shortcut and attribution twins at
330
- `hosts/opencode/plugins/tezgah.js:2458-2462` and `hosts/opencode/plugins/tezgah.js:1647`, the credential twin at `hosts/opencode/plugins/tezgah.js:1607-1611`) and the
331
- ordering obligation (`COMMIT_CMD` `hosts/opencode/plugins/tezgah.js:1535`, `ORDER_DENY`
332
- `hosts/opencode/plugins/tezgah.js:1537`, `lastVerify` `hosts/opencode/plugins/tezgah.js:1548`, `orderReason` `hosts/opencode/plugins/tezgah.js:1573`, dispatched at
333
- `hosts/opencode/plugins/tezgah.js:2507-2510`). `lastVerify` folds the tail exactly as `_last_verify` does, minus the `passing_check` narrowing - this rule reads only
362
+ `hosts/opencode/plugins/tezgah.js:2481-2485` and `hosts/opencode/plugins/tezgah.js:1670`, the credential twin at `hosts/opencode/plugins/tezgah.js:1630-1634`) and the
363
+ ordering obligation (`COMMIT_CMD` `hosts/opencode/plugins/tezgah.js:1558`, `ORDER_DENY`
364
+ `hosts/opencode/plugins/tezgah.js:1560`, `lastVerify` `hosts/opencode/plugins/tezgah.js:1571`, `orderReason` `hosts/opencode/plugins/tezgah.js:1596`, dispatched at
365
+ `hosts/opencode/plugins/tezgah.js:2530-2533`). `lastVerify` folds the tail exactly as `_last_verify` does, minus the `passing_check` narrowing - this rule reads only
334
366
  `fail`, and a row that narrowing would refuse reads `ok`/`ran` on both sides, so the outcome is identical.
335
367
 
336
368
  The control rule has no JavaScript copy. The plugin's `CONTROL_CMD` pre-test picks the shell lines that could touch the control plane. The plugin puts each
@@ -357,16 +389,17 @@ rule's triggers are the classes `_stop_block` returns, the four `_shape_block` a
357
389
  | `shortcut` | E7c: with the write tools refused, 3 of 25 armed runs wrote a test skip through a heredoc | an internal benchmark (block E7, not published) | `tests/test_gate.py::test_a_shell_write_that_disables_a_test_is_denied` |
358
390
  | `piped` | a real omp session: 13 checks ran piped and 3 of 6 completion claims were blocked | the Piped section above | `tests/test_gate.py::test_a_piped_check_is_refused_and_pipefail_passes` |
359
391
  | `attribution` | E7c: the same 3 of 25 runs, a credit written through a heredoc | an internal benchmark (block E7, not published) | `tests/test_gate.py::test_attribution_denies_a_credit_written_by_the_shell` |
360
- | `lang` | a plan branch was created as `plan/004-admin-durum-onarimi`, and its slug is in the public history for good | `hooks/tezgah_gate.py:354-363` | `tests/test_gate.py::test_a_turkish_branch_name_is_refused` |
361
- | `race` | two sessions on one file - a parent and its subagent, or two worktrees - landing on a stale read | `hooks/tezgah_gate.py:751-758` | `tests/test_gate.py::test_a_write_to_a_file_another_session_wrote_is_refused` |
392
+ | `lang` | a plan branch was created as `plan/004-admin-durum-onarimi`, and its slug is in the public history for good | `hooks/tezgah_gate.py:355-364` | `tests/test_gate.py::test_a_turkish_branch_name_is_refused` |
393
+ | `race` | two sessions on one file - a parent and its subagent, or two worktrees - landing on a stale read | `hooks/tezgah_gate.py:752-759` | `tests/test_gate.py::test_a_write_to_a_file_another_session_wrote_is_refused` |
394
+ | `lease` | plan 086 wave 1: two slices wrote into the main checkout through relative paths; 24 sibling race refusals in real transcripts, all in a shared worktree, tree or record | the Lease section above | `tests/test_gate.py::test_a_sibling_slices_lease_refuses_its_file_and_frees_the_owner` |
362
395
  | `task` | E7: with the unlocking command printed, an armed arm removed or disabled the gate in 25 of 25 runs; E7b: the heredoc redirect in 3 of 25 | the Task section above (the checkpoint refusal) | `tests/test_gate.py::test_a_reading_phase_refuses_a_write_inside_the_allowlist` |
363
396
  | `secret` | E7c: the same 3 of 25 runs, a key written through a heredoc | an internal benchmark (block E7, not published) | `tests/test_gate.py::test_a_credential_written_by_a_heredoc_is_denied` |
364
- | `workspace` | sessions wrote root `plans/`, `research/` and `analysis/` (observed across several unrelated projects on one machine) | `hooks/tezgah_gate.py:1110-1114` | `tests/test_gate.py::test_untracked_root_kinds_are_refused_for_tools_and_redirects` |
365
- | `plan` | a turn wrote four product files - a hook, its tests, its docs and the changelog - straight onto `main` | `hooks/tezgah_gate.py:1390-1398` | `tests/test_gate.py::test_the_third_product_write_on_main_is_refused` |
397
+ | `workspace` | sessions wrote root `plans/`, `research/` and `analysis/` (observed across several unrelated projects on one machine) | `hooks/tezgah_gate.py:1344-1348` | `tests/test_gate.py::test_untracked_root_kinds_are_refused_for_tools_and_redirects` |
398
+ | `plan` | a turn wrote four product files - a hook, its tests, its docs and the changelog - straight onto `main` | `hooks/tezgah_gate.py:1624-1632` | `tests/test_gate.py::test_the_third_product_write_on_main_is_refused` |
366
399
  | `order` | FAVA: the shape found in 90% of real agent-instruction projects ("do not commit before running the tests") | the Ordering section above | `tests/test_gate.py::test_a_commit_is_refused_while_the_newest_check_failed` |
367
400
  | `loop` | designed | the turn's own repeated failure; no incident recorded | `tests/test_gate.py::test_an_identical_failed_call_is_denied_after_the_ceiling` |
368
401
  | `retry` | designed | the session ceiling over the same guard; no incident recorded | `tests/test_gate.py::test_a_fourth_identical_call_is_refused_whatever_the_outcome` |
369
- | `drift` | an internal plan's flip rule on the 2026-09-20 move to a result-channel notice: at 27 sessions the blocked-claim rate in turns under 25 work rows read 112/140 = 0.8000, against 59/142 = 0.4155 before | `hooks/tezgah_gate.py:1283-1295` | `tests/test_gate.py::test_a_long_turn_restates_the_constraints_before_a_write` |
402
+ | `drift` | an internal plan's flip rule on the 2026-09-20 move to a result-channel notice: at 27 sessions the blocked-claim rate in turns under 25 work rows read 112/140 = 0.8000, against 59/142 = 0.4155 before | `hooks/tezgah_gate.py:1517-1529` | `tests/test_gate.py::test_a_long_turn_restates_the_constraints_before_a_write` |
370
403
  | `evidence tampered` | a terminated garbage line made `_parse` raise, and the guard then failed the Stop rule open for the turn; corrupting a `verify_fail` row was an allow route. A `verify_ok` appended with `python3 -c` licensed a claim (security-03), so a pass with no `began` row is an orphan (`hooks/tezgah_integrity.py::_pair`) | `hooks/tezgah_integrity.py::_parse` | `tests/test_integrity.py::test_a_damaged_ledger_blocks_a_done_claim_as_evidence_tampered` |
371
404
  | `check failed` | designed | the newest check failing is its own class; no incident recorded | `tests/test_integrity.py::test_failed_check_blocks` |
372
405
  | `partial failure` | designed | a failure the turn never resolved; no incident recorded | `tests/test_integrity.py::test_an_unresolved_failure_blocks_even_after_an_earlier_pass` |
@@ -374,6 +407,10 @@ rule's triggers are the classes `_stop_block` returns, the four `_shape_block` a
374
407
  | `stale evidence` | measured 2026-09-19: reading a rewrite as a change refused honest turns | `hooks/tezgah_integrity.py::_post_write` | `tests/test_integrity.py::test_a_write_after_the_check_makes_the_check_stale` |
375
408
  | `no verify_ok` | E2: 0 of 10 description-shaped claims refused before the `worked` trigger | `hooks/tezgah_integrity.py::_stop_block` | `tests/test_integrity.py::test_work_with_no_passing_check_is_refused_without_a_claim_word` |
376
409
  | `no external read` | two turns: "npm 0.22.0 is missing" off a stale client, and "make NPM_TOKEN an automation token" when it already was | `hooks/tezgah_integrity.py::_external_read_row` | `tests/test_integrity.py::test_an_external_claim_with_no_read_is_refused` |
410
+ | `residual work` | the owner's report (2026-10-11): turns end on "Açık kalanlar / Doğrulanmadı / Sıradaki adım" instead of the work; measured 503 of 1876 final replies with such a label at a line start, 467 in turns that called a non-read tool | `hooks/tezgah_integrity.py::_residual_block` | `tests/test_integrity.py::test_a_residual_list_is_refused_once_and_the_reply_after_it_recorded` |
411
+ | `ready steps` | measured 2026-10-11 (research line `orchestrator-flow-audit`): 13.1% of 451 omp user turns were a bare "devam", and 192 of 281 spawns were followed by `wait` with no other call between, at 82 of them with four or more todo items open | `hooks/tezgah_integrity.py::ready_left` | `tests/test_integrity.py::test_a_ready_todo_step_left_is_refused_once` |
412
+ | `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` |
413
+ | `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` |
377
414
  | `placating opener` | designed | output rule 10 read at the Stop event; no incident recorded | `tests/test_integrity.py::test_sycophantic_opener_blocks` |
378
415
  | `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` |
379
416
  | `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` |
@@ -389,7 +426,7 @@ rule's triggers are the classes `_stop_block` returns, the four `_shape_block` a
389
426
  to (`off(...)`), the way the shortcut and repeat rules use `verify-off` (`hooks/tezgah_gate.py::decision`). The rule name is now a record too: add its row to
390
427
  [Rule provenance](#rule-provenance), or `bin/tezgah-docs --citations` refuses the rule for having none.
391
428
  3. Extend `tests/test_gate.py`. It drives the real function through `tests/_probe_gate.py` (`tests/support.py::PROBE_GATE`), which also carries the `capture_log` stub
392
- standing in for `tezgah_gate.capture` on the allow path (`tests/_probe_gate.py:20-30`). Only if the refusal envelope changes, touch
429
+ standing in for `tezgah_gate.capture` on the allow path (`tests/_probe_gate.py:21-31`). Only if the refusal envelope changes, touch
393
430
  `tests/test_claude_adapters.py::PreToolUse` and the codex/cursor/omp hook tests.
394
431
  4. Mirror the rule in `hosts/opencode/plugins/tezgah.js` and extend `tests/test_opencode_plugin.py` with its cases; that suite is where the two halves are
395
432
  compared and where a shared-vocabulary drift fails.
@@ -400,10 +437,10 @@ rule's triggers are the classes `_stop_block` returns, the four `_shape_block` a
400
437
 
401
438
  ## Source of truth
402
439
 
403
- - `hooks/tezgah_gate.py` — `hooks/tezgah_gate.py::decision`; every rule constant and refusal text `hooks/tezgah_gate.py:164-2158`; `attribution`/`attribution_edit` `hooks/tezgah_gate.py::attribution`, `hooks/tezgah_gate.py::attribution_edit`; `explored`
440
+ - `hooks/tezgah_gate.py` — `hooks/tezgah_gate.py::decision`; every rule constant and refusal text `hooks/tezgah_gate.py:165-2434`; `attribution`/`attribution_edit` `hooks/tezgah_gate.py::attribution`, `hooks/tezgah_gate.py::attribution_edit`; `explored`
404
441
  `hooks/tezgah_gate.py::explored`; `hooks/tezgah_gate.py::searched_symbol`, `hooks/tezgah_gate.py::index_slug`, `hooks/tezgah_gate.py::first_nudge`, `hooks/tezgah_gate.py::nudge_reason`, `hooks/tezgah_gate.py::graph_advice`; `hooks/tezgah_gate.py::LOOP_ATTEMPTS`, `hooks/tezgah_gate.py::loop_reason`,
405
442
  `hooks/tezgah_gate.py::RETRY_CEILING`, `hooks/tezgah_gate.py::retry_reason`; `hooks/tezgah_gate.py::secret_command`, `hooks/tezgah_gate.py::race_reason`,
406
- `hooks/tezgah_gate.py::drift_reason`, `hooks/tezgah_gate.py::effectful`, `hooks/tezgah_gate.py::_deny`; the plan rule's own section `hooks/tezgah_gate.py:1390-1587` — `hooks/tezgah_gate.py::plan_reason`, `hooks/tezgah_gate.py::_branch`, `hooks/tezgah_gate.py::_checkout`, `hooks/tezgah_gate.py::_product`, `hooks/tezgah_gate.py::PLAN_DENY`; `hooks/tezgah_gate.py::write_paths`, `hooks/tezgah_gate.py::shell_target`, `hooks/tezgah_gate.py::SHELL_AS_WRITE`; the `capture` call `hooks/tezgah_gate.py::decision` (a write tool's target, and a shell write's)
443
+ `hooks/tezgah_gate.py::drift_reason`, `hooks/tezgah_gate.py::effectful`, `hooks/tezgah_gate.py::_deny`; the plan rule's own section `hooks/tezgah_gate.py:1624-1821` — `hooks/tezgah_gate.py::plan_reason`, `hooks/tezgah_gate.py::_branch`, `hooks/tezgah_gate.py::_checkout`, `hooks/tezgah_gate.py::_product`, `hooks/tezgah_gate.py::PLAN_DENY`; `hooks/tezgah_gate.py::write_paths`, `hooks/tezgah_gate.py::shell_target`, `hooks/tezgah_gate.py::SHELL_AS_WRITE`; the `capture` call `hooks/tezgah_gate.py::decision` (a write tool's target, and a shell write's)
407
444
  - `hooks/projects-pretooluse.py` — the Claude and dsh envelope; `hooks/tezgah_paths.py` — `off`, `root_for`, `cache_dir`
408
445
  - `hooks/tezgah_integrity.py` — `hooks/tezgah_integrity.py::WRITE_TOOLS`, `hooks/tezgah_integrity.py::BASH_TOOLS`; `NEUTER`/`SKIP_ENV`/`NO_VERIFY`/`GITISH` `hooks/tezgah_integrity.py::NEUTER`, `hooks/tezgah_integrity.py::SKIP_ENV`, `hooks/tezgah_integrity.py::NO_VERIFY`, `hooks/tezgah_integrity.py::GITISH`; `HOOKS_KEY`/`GIT_VALUE_OPTS`/`CONFIG_READS`/`ROUGH_WORDS` `hooks/tezgah_integrity.py::HOOKS_KEY`, `hooks/tezgah_integrity.py::GIT_VALUE_OPTS`, `hooks/tezgah_integrity.py::CONFIG_READS`, `hooks/tezgah_integrity.py::ROUGH_WORDS`; `hooks/tezgah_integrity.py::SKIP_TEST`; `TEST_PATH`
409
446
  `hooks/tezgah_integrity.py::TEST_PATH`; `hooks/tezgah_integrity.py::call_id`; `hooks/tezgah_integrity.py::_path`, `hooks/tezgah_integrity.py::note`, `hooks/tezgah_integrity.py::events`, `hooks/tezgah_integrity.py::writers_elsewhere`, `hooks/tezgah_integrity.py::mask`, `hooks/tezgah_integrity.py::shortcut_command`,
package/docs/glossary.md CHANGED
@@ -10,7 +10,7 @@ are definitions, not rules: the rule text belongs to [contract](contract.md),
10
10
  [gate](gate.md) and [evidence](evidence.md).
11
11
 
12
12
  ### after-block row
13
- A ledger [row](#row) of kind `after_block`: the [Stop rule](#stop-rule)'s record-only verdict on the reply that answers its own block. The host sets `stop_hook_active`, and the turn already holds a `blocked:` [claim](#claim) row. `stop_reason(..., record_only=True)` (`hooks/tezgah_integrity.py::stop_reason`) writes it on Claude, Codex and omp, with `detail` `would block: <reason class>`, `ok` or `no claim`. Not a [claim](#claim): the rule never refuses that reply a second time, and `counters()` counts it neither as a claim nor as a reply.
13
+ A ledger [row](#row) of kind `after_block`: the [Stop rule](#stop-rule)'s record-only verdict on a reply that answers its own block and is not refused again. The turn already holds a `blocked:` [claim](#claim) or refusal row. `_record_only()` (`hooks/tezgah_integrity.py::_record_only`) allows at most two refusals per [turn](#turn) and never two for one reason class. So the rule refuses the reply after a block once more only for a new class. Otherwise `stop_reason()` (`hooks/tezgah_integrity.py::stop_reason`) writes this row on Claude, Codex, Cursor and omp, with `detail` `would block: <reason class>`, `ok` or `no claim`. Not a [claim](#claim): `counters()` counts it neither as a claim nor as a reply.
14
14
 
15
15
  ### always-on
16
16
  The part of the contract every session pays for - the invariants plus the one-line [pointer](#pointer) - built by `always_on_core()` (`hooks/tezgah_context.py::always_on_core`) out of [CORE](#core) with the [conditional rules](#conditional-rule) removed; it is not the whole contract, whose long tail is the `tezgah-contract` [skill](#skill) ([contract](contract.md)).
@@ -50,7 +50,7 @@ What one host can do with one rule surface: the gate, the Stop rule or a subagen
50
50
  One session's record of what ran: the `evidence` rows of `<cache>/tezgah.db` for that session, keyed by the path its JSONL file had, `<cache>/evidence/<slug>.jsonl` (`hooks/tezgah_integrity.py::_path`, `hooks/tezgah_store.py::EVIDENCE_SCHEMA`), appended by `note()` (`hooks/tezgah_integrity.py::note`) and read by `events()` (`hooks/tezgah_integrity.py::events`), with its contract in the module docstring (`hooks/tezgah_integrity.py:2-24`) and every reader failing open. A read or write of the session first imports a JSONL file still at that path. Not the [session store](#session-store), which holds only used-tool marks.
51
51
 
52
52
  ### false completion
53
- The count of [claim](#claim) rows a stop refused, `false_completion` in `counters()`, read against `claims` so the rate is meaningful (`hooks/tezgah_integrity.py::_counts`, `hooks/tezgah_integrity.py::drift_series`). Not the count of [denies](#deny): a deny is the gate refusing a call, a false completion is the Stop rule refusing the end of a turn.
53
+ The count of [claim](#claim) rows a stop refused for their evidence, `false_completion` in `counters()`, read against `claims` so the rate is meaningful (`hooks/tezgah_integrity.py::_counts`, `hooks/tezgah_integrity.py::drift_series`). A refusal for the reply's shape or for the work it leaves (`residual work`) made no false claim, so it does not count. Not the count of [denies](#deny): a deny is the gate refusing a call, a false completion is the Stop rule refusing the end of a turn.
54
54
 
55
55
  ### gate
56
56
  The pre-tool refusal layer, `decision()` (`hooks/tezgah_gate.py::decision`), which runs only inside a [root](#root) (`hooks/tezgah_gate.py::decision`) and returns a reason or nothing ([gate](gate.md)). Not the [Stop rule](#stop-rule), which runs after the reply.
@@ -158,10 +158,10 @@ The ledger field naming the untrusted channel a result came through - `web`, `mc
158
158
  The value of one status [mark](#mark): `on`, `ready`, `off` or `info` (`hooks/tezgah_context.py::health_segments`), drawn as `✓ ○ ✗` or nothing by `GLYPHS` (`hooks/tezgah_context.py::GLYPHS`) and mapped to a colour by `COLORS` (`hooks/tezgah_context.py::COLORS`), with `off` winning over the observable test because a kill switch is visible everywhere ([status-line](status-line.md)). Not a glyph, which is only how the state is drawn.
159
159
 
160
160
  ### step kind
161
- 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:1350-1352`). `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:1356-1359`). Not any row.
161
+ 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.
162
162
 
163
163
  ### Stop rule
164
- 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, or on a turn that recorded work with no passing check (`_stop_block()`, `hooks/tezgah_integrity.py::_stop_block`), and an explicit "doğrulanmadı" clears it. 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)).
164
+ 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)).
165
165
 
166
166
  ### surface
167
167
  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.
package/docs/hosts.md CHANGED
@@ -20,9 +20,9 @@ surface can honestly report this session; the two skill-read marks (`pony`,
20
20
  | claude | SessionStart, SubagentStart, PostCompact, UserPromptSubmit, PreToolUse, PostToolUse, PostToolUseFailure, Stop, SubagentStop (record-only, `subagent_end`) - `hooks/hooks.json:2-32` | the managed block in `~/.claude/CLAUDE.md` (`CLAUDE_RULES`), Claude's global memory file; the SessionStart/PostCompact hook rows declare `TEZGAH_CORE_IN_FILE` (`hooks/hooks.json:4,10`) and the hook drops the core when the block is there, so the contract is paid for once (`hooks/projects-auto-init.py::core_is_in_a_file`; the static `output-styles/tezgah.md` mirror (`output-styles/tezgah.md:1`) is the hookless build's copy) | UserPromptSubmit -> `additionalContext` (`hooks/hooks.json:12-14`, `hooks/projects-auto-init.py::main`) | `statusLine` command in `~/.claude/settings.json` (`bin/tezgah-setup::wire_claude_statusline`), ANSI, forwards Orca first (`statusline.py:47-55`) | all seven: the transcript parse covers the two skill reads, `graph`, `orch` and the three shell kinds - `consult`, `research` and the judgement callers (`statusline.py::claude_used`, `observable=None` at `statusline.py::observable`) |
21
21
  | codex | SessionStart, UserPromptSubmit, PreToolUse, PostToolUse, SubagentStart, PostCompact, Stop (`bin/tezgah-setup::CODEX_HOOK_EVENTS`; named in `hosts/codex/hook.py:6-7`) | hook: the normalized events inject `context_for` (`hosts/codex/hook.py::main`) | UserPromptSubmit -> `additionalContext` (`hosts/codex/hook.py::main`) | no custom footer item (`tui.status_line` is a closed enum) - the line rides `systemMessage`, plain, at SessionStart and Stop (`hosts/codex/hook.py:10-12`, `hosts/codex/hook.py::main`) | five tool-use (`TOOL_USE_MEASURES` at `hosts/codex/hook.py::main`) |
22
22
  | cursor | 13 events: sessionStart, preToolUse, postToolUse, postToolUseFailure, subagentStart, subagentStop, beforeSubmitPrompt, stop, beforeMCPExecution, afterShellExecution, afterMCPExecution, afterFileEdit, afterAgentResponse (`bin/tezgah-setup::CURSOR_HOOK_EVENTS`) | hook: sessionStart -> `additional_context` (`hosts/cursor/hook.py::dispatch`) | beforeSubmitPrompt -> `additional_context` plus `{"continue": true}` (`hosts/cursor/hook.py::dispatch`) | `statusLine` in `~/.cursor/cli-config.json` -> `tezgah-statusline --cursor` (`bin/tezgah-setup::install_cursor`), ANSI (`statusline.py:122-124`) | five tool-use (`statusline.py::observable`) |
23
- | opencode | no lifecycle events; plugin hooks: config, permission.ask, tool.execute.before, shell.env, experimental.session.compacting, chat.message, tool.execute.after (`hosts/opencode/plugins/tezgah.js:2349-2714`) - `tool.execute.before` carries the whole gate, `tool.execute.after` the channel on the row, the provenance label on the result and the taint notice on the next effect (`untrustedSource` `hosts/opencode/plugins/tezgah.js:1919`, `labelResult` `:1993`) | static only: generated `instructions` files, because there is no session-start injection point (`hosts/opencode/plugins/tezgah.js:3-6`, `bin/tezgah-setup::opencode_contract_text`) | chat.message pushes a synthetic part (`hosts/opencode/plugins/tezgah.js:2629-2680`) | TUI plugin `tezgah-tui.tsx` declared in `tui.json`, runs `tezgah-status` on the event bus (`hosts/opencode/tui/tezgah-tui.tsx:1-11`; wiring `bin/tezgah-setup::install_opencode`) | all seven: skill reads and kinds are classified in process (`hosts/opencode/plugins/tezgah.js:2204-2240`) |
23
+ | opencode | no lifecycle events; plugin hooks: config, permission.ask, tool.execute.before, shell.env, experimental.session.compacting, chat.message, tool.execute.after (`hosts/opencode/plugins/tezgah.js:2372-2737`) - `tool.execute.before` carries the whole gate, `tool.execute.after` the channel on the row, the provenance label on the result and the taint notice on the next effect (`untrustedSource` `hosts/opencode/plugins/tezgah.js:1942`, `labelResult` `:1993`) | static only: generated `instructions` files, because there is no session-start injection point (`hosts/opencode/plugins/tezgah.js:3-6`, `bin/tezgah-setup::opencode_contract_text`) | chat.message pushes a synthetic part (`hosts/opencode/plugins/tezgah.js:2652-2703`) | TUI plugin `tezgah-tui.tsx` declared in `tui.json`, runs `tezgah-status` on the event bus (`hosts/opencode/tui/tezgah-tui.tsx:1-11`; wiring `bin/tezgah-setup::install_opencode`) | all seven: skill reads and kinds are classified in process (`hosts/opencode/plugins/tezgah.js:2227-2263`) |
24
24
  | dsh | SessionStart, SubagentStart, UserPromptSubmit, PreToolUse, PostToolUse (`hosts/dsh/hooks.json:2-19`); no Stop rule, because the bridge drops every call's outcome and no check there can show a pass | hook: the same Claude-family scripts, named by `configPath` (`bin/tezgah-setup::dsh_patch_block`) | UserPromptSubmit, through the bridge (`hosts/dsh/hooks.json:9-11`) | web-profile plugin: authenticated `GET /api/tezgah.status` -> `tezgah-status` (`hosts/dsh/statusline/lib/index.js:15,66-104`) | five tool-use: the route passes `--observable=consult,research,graph,orch,judge` (`hosts/dsh/statusline/lib/index.js:16-19,84,96`) |
25
- | omp | session_start, session_compact, session_switch, turn_end, before_agent_start, tool_call, tool_result, session_stop (`hosts/omp/tezgah-hook.ts.in:541,588,607,612,614,634,653,688`) | both: static `RULES.md` (`bin/tezgah-setup::STATIC_RULES`, `bin/tezgah-setup::rules_block`) and a session payload carrying only what a static file cannot know (`hosts/omp/hook.py::handle`) | before_agent_start returns a hidden message (`hosts/omp/tezgah-hook.ts.in:614-632`) | extension draws `ctx.ui.setWidget` below the editor, `setStatus` as fallback (`hosts/omp/tezgah-hook.ts.in:170-203`); ANSI line from `hosts/omp/hook.py::status_line` | all seven: the embedded runner filters skill reads before asking python (`hosts/omp/tezgah-hook.ts.in:255-262`) and no `observable` is passed (`hosts/omp/hook.py::status_line`) |
25
+ | omp | session_start, session_compact, session_switch, turn_end, before_agent_start, tool_call, tool_result, session_stop (`hosts/omp/tezgah-hook.ts.in:596,655,674,679,681,701,720,755`) | both: static `RULES.md` (`bin/tezgah-setup::STATIC_RULES`, `bin/tezgah-setup::rules_block`) and a session payload carrying only what a static file cannot know (`hosts/omp/hook.py::handle`) | before_agent_start returns a hidden message (`hosts/omp/tezgah-hook.ts.in:681-699`) | extension draws `ctx.ui.setWidget` below the editor, `setStatus` as fallback (`hosts/omp/tezgah-hook.ts.in:170-203`); ANSI line from `hosts/omp/hook.py::status_line` | all seven: the embedded runner filters skill reads before asking python (`hosts/omp/tezgah-hook.ts.in:257-264`) and no `observable` is passed (`hosts/omp/hook.py::status_line`) |
26
26
 
27
27
  The middle five marks (`consult`, `research`, `graph`, `orch`, `judge`) are tool-use marks:
28
28
  any adapter that sees its host's tool calls can light them by calling
@@ -225,9 +225,9 @@ and say which document it came from.
225
225
 
226
226
  A skill read is observable only where the host gives a channel that does not cost
227
227
  a process per read: Claude parses its transcript (`statusline.py::claude_used`),
228
- opencode classifies in process (`hosts/opencode/plugins/tezgah.js:2204-2240`),
228
+ opencode classifies in process (`hosts/opencode/plugins/tezgah.js:2227-2263`),
229
229
  omp filters in its embedded runner before it asks python
230
- (`hosts/omp/tezgah-hook.ts.in:255-262`). On codex, cursor and dsh a read is not
230
+ (`hosts/omp/tezgah-hook.ts.in:257-264`). On codex, cursor and dsh a read is not
231
231
  observable at that price, so their surfaces pass the five tool-use measures as
232
232
  `observable` and the two skill marks render dim (`info`, no glyph) instead of
233
233
  claiming the skill was never opened (`hooks/tezgah_context.py::TOOL_USE_MEASURES`, and the
@@ -314,7 +314,7 @@ In order, each step verified by the one below it:
314
314
  `hosts/omp/hook.py` for `{"event": "post_compact"}`; the block is sent with
315
315
  `pi.sendMessage(..., {deliverAs: "nextTurn"})`, and the summary rides as
316
316
  `compact_summary` so `remember_compaction` writes the compaction row
317
- (`hosts/omp/tezgah-hook.ts.in:580-597`, `hosts/omp/hook.py::handle`).
317
+ (`hosts/omp/tezgah-hook.ts.in:647-664`, `hosts/omp/hook.py::handle`).
318
318
  Subagent sessions are skipped, because the post-compact block starts the
319
319
  indexer.
320
320
  - **cursor**: not re-injected - a limitation. Cursor's only compaction hook,
@@ -348,7 +348,7 @@ In order, each step verified by the one below it:
348
348
  `hosts/dsh/hooks.json` until the bridge carries outcomes.
349
349
  - **opencode carries the untrusted-content control in the plugin**, not by
350
350
  importing `hooks/tezgah_untrusted.py`, which a plugin cannot do in process: the
351
- channel is decided in JS (`untrustedSource` `hosts/opencode/plugins/tezgah.js:1919`,
351
+ channel is decided in JS (`untrustedSource` `hosts/opencode/plugins/tezgah.js:1942`,
352
352
  the tier's argv reader at `:1902-1911`), the label and the taint notice ride the
353
353
  result the hook is handed (`labelResult` `:1993`). A shared corpus in
354
354
  `tests/test_opencode_plugin.py::OpenCodePlugin.test_the_classifier_agrees_with_the_python_half_on_a_shared_corpus` drives both halves over the same calls and
@@ -391,7 +391,7 @@ In order, each step verified by the one below it:
391
391
  - **opencode has no session-start hook**, so its always-on file must carry the
392
392
  pointer every hook host appends, and the per-session index, agent and contract
393
393
  refreshes ride the first `chat.message` instead (`bin/tezgah-setup::refresh_contract`,
394
- `hosts/opencode/plugins/tezgah.js:2663-2678`). It denies the native `skill`
394
+ `hosts/opencode/plugins/tezgah.js:2686-2701`). It denies the native `skill`
395
395
  tool - the generated router replaces the injected skill list
396
396
  (`bin/tezgah-setup::install_opencode`) - and `permission.ask` may never be emitted by a
397
397
  given build, which is why `tool.execute.before` stays the enforcing half
@@ -411,7 +411,7 @@ In order, each step verified by the one below it:
411
411
  `hosts/cursor/hooks.json:8` - neither named them until 2026-09-19, so a
412
412
  PowerShell call on those two reached no hook at all while the shared rules
413
413
  already knew the spelling), and omp's `GATED` list
414
- (`hosts/omp/tezgah-hook.ts.in:268-270`). A host this list does not cover is a
414
+ (`hosts/omp/tezgah-hook.ts.in:270-273`). A host this list does not cover is a
415
415
  host whose shell the gate cannot see.
416
416
  - **An MCP effect reaches the gate on every host that can route it**. Claude
417
417
  and dsh run it through a PreToolUse group of its own in `hooks/hooks.json` and
package/docs/index.json CHANGED
@@ -267,7 +267,9 @@
267
267
  "what do init, claim, migrate and source write",
268
268
  "how does a session learn that a research line is broken",
269
269
  "what lights the research mark and how is the layer switched off",
270
- "why may a new research line not be opened while another is unfinished"
270
+ "why may a new research line not be opened while another is unfinished",
271
+ "what does tezgah-research next print for a product flow, and when is a decision due for re-measure",
272
+ "how does an exploratory line run a campaign of new ideas (arena, stage funnel, tournament, board, dedup, yield)"
271
273
  ],
272
274
  "answers_tr": [
273
275
  "bir araştırma hattı nerede yaşar ve ne tutar",
@@ -275,11 +277,14 @@
275
277
  "init, claim, migrate ve source ne yazar",
276
278
  "bir oturum bir araştırma hattının bozuk olduğunu nasıl öğrenir",
277
279
  "araştırma işaretini ne yakar ve katman nasıl kapatılır",
278
- "başka biri bitmemişken neden yeni bir araştırma hattı açılamaz"
280
+ "başka biri bitmemişken neden yeni bir araştırma hattı açılamaz",
281
+ "tezgah-research next bir ürün akışı için ne yazdırır ve bir karar ne zaman yeniden ölçülmeli",
282
+ "keşif (exploratory) hattı yeni fikir kampanyasını nasıl yürütür (arena, aşama hunisi, turnuva, board, dedup, yield)"
279
283
  ],
280
284
  "sources": [
281
285
  "hooks/tezgah_research.py",
282
286
  "bin/tezgah-research",
287
+ "hooks/tezgah_product.py",
283
288
  "hooks/tezgah_policy.py",
284
289
  "hooks/tezgah_context.py",
285
290
  "skills/research/SKILL.md",
@@ -294,12 +299,18 @@
294
299
  "answers": [
295
300
  "what is a skill here and how does it reach a host",
296
301
  "which skills ship and what does each do",
297
- "how do I add a skill without breaking the router or the install report"
302
+ "how do I add a skill without breaking the router or the install report",
303
+ "which skill designs a metric dashboard and where do its data-view rules live",
304
+ "does the local skill search reach design-library's nested entries",
305
+ "which skill carries a new metric dashboard or feature from research to re-measure"
298
306
  ],
299
307
  "answers_tr": [
300
308
  "burada beceri nedir ve bir hosta nasıl ulaşır",
301
309
  "hangi beceriler gelir ve her biri ne yapar",
302
- "yönlendiriciyi veya kurulum raporunu bozmadan nasıl beceri eklerim"
310
+ "yönlendiriciyi veya kurulum raporunu bozmadan nasıl beceri eklerim",
311
+ "metrik dashboard tasarımını hangi beceri yapar ve veri görünümü kuralları nerede",
312
+ "yerel beceri araması design-library'nin iç girdilerine ulaşır mı",
313
+ "yeni bir metrik dashboard'u veya özelliği araştırmadan yeniden ölçüme hangi beceri taşır"
303
314
  ],
304
315
  "sources": [
305
316
  "bin/tezgah-setup",
@@ -309,7 +320,10 @@
309
320
  "commands/",
310
321
  "NOTICE",
311
322
  "tests/test_setup.py",
312
- "tests/test_skills.py"
323
+ "tests/test_skills.py",
324
+ "hooks/tezgah_skill_pick.py",
325
+ "skills/design-contract/data-views.md",
326
+ "skills/product-flow/SKILL.md"
313
327
  ]
314
328
  },
315
329
  {
@@ -320,12 +334,14 @@
320
334
  "answers": [
321
335
  "what does a feature-level audit cover that a screen review misses",
322
336
  "what are the five matrices and the capability-change proposal",
323
- "how is the feature-audit skill reached and what evidence backs it"
337
+ "how is the feature-audit skill reached and what evidence backs it",
338
+ "how do I specify a feature before it is built and audit it against that spec"
324
339
  ],
325
340
  "answers_tr": [
326
341
  "özellik düzeyinde bir denetim, ekran incelemesinin kaçırdığı neyi kapsar",
327
342
  "beş matris ve yetenek değişikliği önerisi nedir",
328
- "özellik denetimi becerisine nasıl ulaşılır ve onu hangi kanıt destekler"
343
+ "özellik denetimi becerisine nasıl ulaşılır ve onu hangi kanıt destekler",
344
+ "bir özelliği yapılmadan önce nasıl tanımlarım ve yapıldıktan sonra o tanıma göre nasıl denetlerim"
329
345
  ],
330
346
  "sources": [
331
347
  "skills/feature-audit/SKILL.md",
@@ -369,7 +385,8 @@
369
385
  "where does an installed version unpack and how do I roll back",
370
386
  "something is broken what do I check first",
371
387
  "how do I turn on taste signal capture and measure my corrections",
372
- "how does taste learn my coding preferences and inject them"
388
+ "how does taste learn my coding preferences and inject them",
389
+ "how do I resume or clean up a delegated slice after a crash or a quota stop"
373
390
  ],
374
391
  "answers_tr": [
375
392
  "bunu bir makineye nasıl kurar, doğrular ve yükseltirim",
@@ -378,7 +395,8 @@
378
395
  "kurulu bir sürüm nereye açılır ve nasıl geri alırım",
379
396
  "bir şey bozuk, önce neyi kontrol etmeliyim",
380
397
  "zevk sinyali kaydını nasıl açar ve düzeltmelerimi nasıl ölçerim",
381
- "taste kodlama tercihlerimi nasıl öğrenir ve oturuma nasıl ekler"
398
+ "taste kodlama tercihlerimi nasıl öğrenir ve oturuma nasıl ekler",
399
+ "bir çökme veya kota durmasından sonra devredilen bir dilimi nasıl sürdürür veya temizlerim"
382
400
  ],
383
401
  "sources": [
384
402
  "bin/tezgah-setup",
@@ -393,7 +411,9 @@
393
411
  "hooks/tezgah_embed.py",
394
412
  "hooks/tezgah_taste.py",
395
413
  "hooks/tezgah_taste_ledger.py",
396
- "bin/tezgah-taste"
414
+ "bin/tezgah-taste",
415
+ "hooks/tezgah_task.py",
416
+ "bin/tezgah-task"
397
417
  ]
398
418
  },
399
419
  {
package/docs/judge.md CHANGED
@@ -25,13 +25,16 @@ transient failure - a timeout, a connection error, a 5xx - gets exactly one more
25
25
  attempt. A 4xx, a session CLI's non-zero exit and a malformed reply never do
26
26
  (`_transient`, `hooks/tezgah_judge.py::_transient`). A hook caller asks for one
27
27
  attempt and a wall-clock `deadline` instead (`ask`, `hooks/tezgah_judge.py::ask`).
28
- A session CLI answers in 4-10 s (measured 2026-10-07), so the skill hint's
28
+ A session CLI answers in 4-10 s (measured 2026-10-07, provider and model not recorded), so the skill hint's
29
29
  4 s deadline usually ends first. `TEZGAH_TYPESAFE_URL`, `TEZGAH_JEV_OPENROUTER_URL`,
30
30
  `TEZGAH_CLOUDFLARE_URL`, `TEZGAH_OPENROUTER_URL` and
31
31
  `TEZGAH_OMP_BIN`/`TEZGAH_CLAUDE_BIN` are the test seams. Each URL override goes
32
32
  through `override` (`hooks/tezgah_judge.py::override`), which refuses plain http
33
33
  off this machine.
34
34
 
35
+ Each measured figure on this page measures only the provider and model it names,
36
+ and stays unmeasured for any other default.
37
+
35
38
  Every caller reads an answer through the same accessors rather than reaching
36
39
  into the raw reply, so a Choice and a Noul are read one way for all five
37
40
  (`choice`, `hooks/tezgah_judge.py::choice`; `noul`,
@@ -123,7 +126,8 @@ that drops their tools or takes a system prompt. They run with the least their
123
126
  wrote nothing. cursor-agent in ask mode refused the same shell call.
124
127
  Measured 2026-10-07, a call cost $0.007 on claude and $0.015 on omp. Measured
125
128
  2026-10-09 on one choice question, opencode read 28,646 input tokens ($0.004 by
126
- its own report), cursor-agent 17,095 and codex 20,919. The session's own
129
+ its own report), cursor-agent 17,095 and codex 20,919. These figures name the CLI
130
+ that answered, and no record names its model. The session's own
127
131
  credential pays, OAuth subscription included. The answer and usage come from the
128
132
  CLI's JSON output (`_session_request`, `hooks/tezgah_judge.py::_session_request`).
129
133
  `_cli_answer` (`hooks/tezgah_judge.py::_cli_answer`) reads the last three. Only
@@ -213,10 +217,12 @@ computes and prints the arithmetic it paid on every run (`PRICE_PER_MILLION`,
213
217
  unit-selection shape it ships was measured on two reproduced screens: 100% of the
214
218
  control lines at a 94% read on a 356-line table, and 100% at a 74% read on a
215
219
  31-line screen, against 73.5% at a 26% read for the per-line shape it replaced.
220
+ The record of these three figures names no provider and no model (provider not
221
+ recorded).
216
222
  The threshold those numbers were taken at is `SELECTED_AT`, and the tool prints
217
223
  the measurement with every run (`MEASURED`, `bin/tezgah-triage::MEASURED`;
218
224
  `SELECTED_AT`, `bin/tezgah-triage::SELECTED_AT`). The retry covers a rate that was
219
- measured rather than assumed - 0 of 184 live calls returned `None` - so the second
225
+ measured rather than assumed - 0 of 184 live calls returned `None` (provider not recorded) - so the second
220
226
  attempt costs a healthy call nothing (`_transient`,
221
227
  `hooks/tezgah_judge.py::_transient`); `docs/operations.md` records the run. Each
222
228
  caller also records one `judge` row of cost on the ledger when a session id is
package/docs/models.md CHANGED
@@ -70,6 +70,37 @@ The orchestrator agent keeps `model: inherit`: it is the main thread's own agent
70
70
 
71
71
  omp's bundled agents are routed through the same record (omp's own docs: `omp://task-agent-discovery.md`, *Bundled agents*): `scout` on the explore slot, `sonic` on the cheap slot, `task` on the standard slot, and `reviewer`/`security-reviewer` left to inherit, because a review is frontier work. A user's own value for any of them wins (see the ownership rule above). Those two are the gap this leaves: the record carries no entry for them, so a session on a cheap model reviews on that model - routing them is one more `BUNDLED` entry, not decided here.
72
72
 
73
+ A credential is not a balance, so the chain is live. A family whose provider
74
+ answered a limit leaves every chain until its reset (`funded_families`,
75
+ `hooks/tezgah_models.py::down_families`). The record is `model-health.json`
76
+ in the cache dir. The reset comes from the provider's own answer. That is a
77
+ named `reset at` time or a `retry-after-ms`, else six hours (`limit_until`).
78
+ A named reset already past also holds six hours, since the provider's time zone
79
+ is not stated.
80
+ `tezgah-route --health` probes each other family with a credential. OpenRouter
81
+ answers through its key endpoint, which costs no tokens. A family omp runs gets
82
+ one short prompt on its cheap row (`probe_family`). The command then records the
83
+ downs and re-applies omp's chain. Session start and each prompt start it in the
84
+ background when it is due (`health_due`, `start_health`). It is due when it was
85
+ never checked, when it is six hours old, or when a recorded reset has passed. The mode's own family is never
86
+ probed, since it leads the chain anyway. `tezgah-route --fallback any` lets a
87
+ chain fall back to another vendor's family once it is up. A family omp runs on
88
+ an account, such as zai, comes before OpenRouter, which bills per token
89
+ (`hooks/tezgah_models.py::_chain`). The probe uses the consult argv, so it loads
90
+ no tools, rules, skills or extensions.
91
+ Measured 2026-10-11: zai answered `429 Weekly/Monthly Limit Exhausted. Your limit
92
+ will reset at 2026-10-12 02:02:35`, so it stays out of the chain until then.
93
+ A subagent slice that stops on a quota or rate limit pushes the same record at
94
+ once (`hooks/tezgah_models.py::note_limit`). Its close calls it
95
+ (`hooks/tezgah_task.py::close_slice`). omp's transcript names the provider. A
96
+ Claude subagent runs on Anthropic unless its model names another vendor. A
97
+ quota is held until the answer's reset, else six hours. A rate limit is held
98
+ until its reset or retry-after, else ten minutes. Then the push re-applies omp's
99
+ chain. The mode's own family stays first in every chain, so a hold moves
100
+ only the fallbacks after it. Both writers re-read the record under a lock
101
+ before they write (`hooks/tezgah_models.py::_health_locked`), so neither drops
102
+ the other's hold.
103
+
73
104
  ## What is not routed yet
74
105
 
75
106
  - **Claude Code's built-in subagents** (Explore, general-purpose, Plan) run on the session model. `CLAUDE_CODE_SUBAGENT_MODEL` and a settings `env` block exist but carry no effort, and whether a project agent with a built-in's name replaces it is not stated where it can be cited - so nothing is generated on a guess. What would close it: a documented per-agent effort for built-ins, or a confirmed name-collision rule.
@@ -97,7 +128,7 @@ fixed:
97
128
  2. Otherwise the brief - redacted with the ledger's own reader
98
129
  (`redact`, `hooks/tezgah_integrity.py::redact`) - goes to the judge as one Choice over three tiers (`TIER_QUESTION`,
99
130
  `hooks/tezgah_models.py::TIER_QUESTION`). The judge asks the session's own model first ([judge](judge.md)).
100
- Measured with Jev on 40 English briefs labelled by
131
+ Measured with Jev (model `jev-latest`, carrier not recorded) on 40 English briefs labelled by
101
132
  the same session that wrote the rubric (2026-09-30, twice): under-route 0.025,
102
133
  accuracy 0.925 and 0.900, 392 ms median, about 656 input tokens per call; a
103
134
  keyword rule on the same set under-routed 0.100. The session model was not measured on that set.
@@ -106,7 +137,9 @@ fixed:
106
137
  specialist? A yes names that agent on its own tier (`AGENT_SLOT`,
107
138
  `hooks/tezgah_models.py::AGENT_SLOT`). Before this question, sessions
108
139
  spawned the five specialists 0 times in 121 delegations (2026-10-07 to 10-09).
109
- Clef agreed on 9 of 10 briefs this session labelled (2026-10-09).
140
+ Clef agreed on 9 of 10 briefs this session labelled (2026-10-09, model not
141
+ recorded). Each judge figure on this page measures only the provider and model
142
+ it names, and stays unmeasured for any other default.
110
143
  3. With no judgement (`judge-off`, no key, a failed call) `--phase review` and
111
144
  `--phase research` name the reviewer and the researcher (`PHASE_AGENT`,
112
145
  `hooks/tezgah_models.py::PHASE_AGENT`). Another phase picks the tier from the