@r1z4x/tezgah 1.6.0 → 1.8.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +140 -1
- package/MANIFEST +7 -0
- package/README.md +6 -3
- package/bin/tezgah-decisions +6 -14
- package/bin/tezgah-design +21 -5
- package/bin/tezgah-docs +18 -16
- package/bin/tezgah-doctor +152 -0
- package/bin/tezgah-lessons +4 -1
- package/bin/tezgah-research +249 -17
- package/bin/tezgah-route +70 -8
- package/bin/tezgah-setup +26 -13
- package/bin/tezgah-task +877 -60
- package/bin/tezgah-taste +56 -15
- package/docs/architecture.md +28 -11
- package/docs/clarity-baseline.json +2 -2
- package/docs/contract.md +108 -24
- package/docs/environment.md +3 -1
- package/docs/evidence.md +209 -28
- package/docs/feature-audit.md +1 -1
- package/docs/gate.md +78 -23
- package/docs/glossary.md +4 -4
- package/docs/hosts.md +8 -8
- package/docs/index.json +11 -5
- package/docs/judge.md +9 -3
- package/docs/models.md +75 -14
- package/docs/operations.md +133 -3
- package/docs/research.md +349 -15
- package/docs/skills.md +1 -1
- package/docs/status-line.md +5 -5
- package/docs/testing.md +20 -13
- package/docs/vision.md +12 -4
- package/hooks/hooks.json +2 -2
- package/hooks/lang/en.json +323 -0
- package/hooks/lang/tr.json +344 -0
- package/hooks/projects-auto-init.py +29 -1
- package/hooks/projects-posttooluse.py +24 -5
- package/hooks/projects-pretooluse.py +9 -4
- package/hooks/projects-stop.py +28 -16
- package/hooks/tezgah_agents.py +53 -17
- package/hooks/tezgah_clarity.py +7 -3
- package/hooks/tezgah_context.py +489 -438
- package/hooks/tezgah_gate.py +996 -80
- package/hooks/tezgah_integrity.py +1547 -197
- package/hooks/tezgah_models.py +427 -45
- package/hooks/tezgah_paths.py +147 -16
- package/hooks/tezgah_policy.py +261 -129
- package/hooks/tezgah_rank.py +29 -27
- package/hooks/tezgah_replay.py +200 -30
- package/hooks/tezgah_research.py +1303 -14
- package/hooks/tezgah_stopspec.py +7 -0
- package/hooks/tezgah_store.py +74 -27
- package/hooks/tezgah_task.py +1201 -40
- package/hooks/tezgah_taste.py +8 -4
- package/hosts/codex/hook.py +32 -13
- package/hosts/cursor/hook.py +32 -14
- package/hosts/dsh/hooks.json +2 -2
- package/hosts/omp/hook.py +44 -23
- package/hosts/omp/tezgah-hook.ts.in +93 -7
- package/hosts/opencode/plugins/tezgah.js +273 -6
- package/output-styles/tezgah.md +76 -42
- package/package.json +1 -1
- package/skills/i-have-adhd/EVALS.md +22 -2
- package/skills/i-have-adhd/SKILL.md +12 -5
- package/skills/plan-add/SKILL.md +27 -11
- package/skills/plan-add/render_table.py +8 -3
- package/skills/plan-sync/SKILL.md +5 -3
- package/skills/ponytail/EVALS.md +21 -2
- package/skills/ponytail/SKILL.md +4 -4
- package/skills/research/SKILL.md +89 -1
- package/skills/tezgah-contract/EVALS.md +52 -7
- package/skills/tezgah-contract/SKILL.md +154 -59
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:
|
|
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.
|
|
@@ -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:
|
|
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:
|
|
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:
|
|
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,19 +155,40 @@ 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 file a git checkout holds is never scratch, even when the checkout lives under the temp dir (`hooks/tezgah_gate.py::_scratch`). 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`).
|
|
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.
|
|
164
|
+
|
|
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. A checkout under the temp dir is not scratch here either (`hooks/tezgah_gate.py::_scratch`).
|
|
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.
|
|
161
178
|
|
|
162
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
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
|
|
167
|
-
(`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:
|
|
184
|
+
(`hooks/tezgah_gate.py::TASK_RECORD_DENY`) - the active task's record and any other open plan that carries a phase (`hooks/tezgah_task.py::phased_record`), matched without case and read against the main checkout, so a session in a worktree meets the same rule - because only `bin/tezgah-task` writes it and that CLI makes the checks a retyped frontmatter would skip. Told:
|
|
168
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
|
|
169
|
-
the move until the plan's Acceptance, spike and checkpoint are in order, and records the reason for a widened scope (`allowed_why:`). 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.
|
|
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, run from the main checkout or from any of its worktrees. `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`). `tezgah-task spike NNN "<answer>"` writes `spike_recorded:`, the answer `implementation` waits for (`bin/tezgah-task::cmd_spike`). None of them 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. So does a file in the repo or its worktrees when the checkout lives under the temp dir, as CI's does.
|
|
188
|
+
The task rule holds repository paths only. A target under no git checkout and no configured root, such as `~/.config/runpod/key`, is no plan's work (`hooks/tezgah_gate.py::_no_repo`). Neither the phase nor the allowlist refuses it, while every other rule, the secret rule among them, still reads that write. The gate reads a relative target in the call's own directory. Any doubt keeps the target held. A line that moves the directory stays held, however it spells the `cd`, `pushd` or `popd`. So does a line that reads `$PWD` back. A line that makes a link stays held: `ln`, `install`, `cp -s`, `cp -l`. So does a target whose parent directory does not exist yet. So does a file with a second hard link. Host agent and instruction files and the shell rc files stay held too, in any letter case (`hooks/tezgah_gate.py::HOME_HELD`). Under `~/.claude` those are `agents/**`, `hooks/**`, `commands/**`, `skills/**` and `CLAUDE.md`. Elsewhere they are `~/.omp/agent/**`, `~/.codex/AGENTS.md`, `~/.codex/prompts/**`, `~/.cursor/**` and `~/.config/opencode/**`. The rc files are `~/.zshrc`, `~/.zshenv`, `~/.zprofile`, `~/.zlogin`, `~/.bashrc`, `~/.bash_profile`, `~/.bash_login` and `~/.profile`. A target in another repository stays refused as outside this one.
|
|
189
|
+
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 every shell write into `.tezgah/plans/open/`, because such a file could make itself the active task. A plan file stays held too when its text, once the write lands, carries a `phase:` line (`hooks/tezgah_gate.py::_text_after`). The gate judges the whole text, so two edits cannot each pass on their own. The record rule still refuses the active record there.
|
|
190
|
+
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. The phase, allowlist and checkpoint rules read a sibling. The record rule reads the main checkout, and the workspace rule keeps the cwd's own root. The worktree's admin directory must point back at it, and a worktree of another repository stays outside.
|
|
191
|
+
Two plans can run at once, one per worktree. A linked worktree holds plan NNN when its branch is `plan/NNN-...` (`hooks/tezgah_task.py::branch_task`). That covers the plan's declared `branch:` and every slice branch the plan fans out to. Of several, the declared branch's worktree is the one the checkpoint and review read (`hooks/tezgah_task.py::plan_holders`). It then answers to plan NNN's phase, allowlist and checkpoint while plan NNN carries a phase. A session whose cwd is that worktree reads plan NNN as the active task. A write from anywhere else into that worktree meets plan NNN too (`hooks/tezgah_task.py::worktree_task`). A shell line meets the plan of every redirect and `tee` target it names (`hooks/tezgah_gate.py::_owners`). It meets the cwd's task as well when it writes where no target was read. That is a positional writer such as `sed -i`, a `tee` with more than one file, or more write shapes than targets. The checkpoint's sha is read against that worktree's HEAD and tree, and the refusal names `git -C <worktree>`. Every other checkout reads the main checkout's plans. Its task is the first phased plan no worktree holds. When every phased plan is held, it has no scope of its own, and a write outside the plan worktrees meets no plan (`hooks/tezgah_task.py::active`). The record rule still refuses a direct write to each held record. `tezgah-task start` keeps another plan's phase in one case only (`bin/tezgah-task::cmd_start`). Both plans then run in their own worktrees, and their allowlists share no prefix. Otherwise it clears that phase. `allow`, and `phase --any-path`, refuse a scope that shares a prefix with such a held plan (`bin/tezgah-task::held_overlap`). Off: `task-off`
|
|
171
192
|
(`hooks/tezgah_gate.py::decision`). It fails open wherever the question cannot be answered: no active task, no readable record.
|
|
172
193
|
|
|
173
194
|
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
|
|
@@ -189,6 +210,20 @@ Three triggers, one per shape a credential takes on its way into a file (`hooks/
|
|
|
189
210
|
- **Shell text.** One simple command (split on `&&`, `||`, `;`, newline — `hooks/tezgah_gate.py::SEGMENT`) carrying both a token (`hooks/tezgah_gate.py::SECRET_TOKEN` — an
|
|
190
211
|
`Authorization: Bearer` header, or a `name=value` assignment for `api_key`/`token`/`secret`/`password`/...) and a sink (`hooks/tezgah_gate.py::SECRET_SINK` — `>`/`>>`,
|
|
191
212
|
`| tee`, a curl `--trace`, or `git add`). Told: record the name, length or a fingerprint instead, and pass the value through the tool's environment (`hooks/tezgah_gate.py::SECRET_DENY`).
|
|
213
|
+
A token only in the leading `NAME=value` words of a program reaches that program's environment, not the file.
|
|
214
|
+
So `API_KEY="$(cat key)" python3 tool.py > run.log` passes (`hooks/tezgah_gate.py::_env_prefix_only`).
|
|
215
|
+
An allowlist is the ceiling of this case (`hooks/tezgah_gate.py::_secret_runner`). Only a script runner named by itself passes: `python`, `node`, `bun`, `deno run`, `bash`, `sh` or `zsh`.
|
|
216
|
+
It needs a script file ending in `.py`, `.js`, `.mjs`, `.ts` or `.sh`.
|
|
217
|
+
No option before the file may run inline text, stdin, a preload, interactive mode or a trace.
|
|
218
|
+
Those are `-c`, `-e`, `-p`, `-`, `-r`, `-i`, `-x`, `-v` and `-o` (`hooks/tezgah_gate.py::SECRET_INLINE`).
|
|
219
|
+
Every other program holds, because it may print its environment: `env`, `echo`, `xargs env`, `node -pe`, a bare `prog`, a program named by its path.
|
|
220
|
+
It also holds when the sink is a `git add` or a curl trace (`hooks/tezgah_gate.py::SECRET_OUTPUT`).
|
|
221
|
+
A line that reads `/proc/*/environ`, defines a function or runs `set -x` holds too.
|
|
222
|
+
So does a line that sets an interpreter's startup or trace variable (`hooks/tezgah_gate.py::SECRET_STARTUP`: `BASH_ENV`, `NODE_OPTIONS`, `PYTHONSTARTUP`, `LD_PRELOAD`, ...).
|
|
223
|
+
The rule cannot read the script itself: an allowlisted script that prints its own environment still writes the key.
|
|
224
|
+
A name an earlier command on the line set to a token stays carried. A printer that reads it into a file still denies, as in `API_KEY=x; echo $API_KEY > f` (`hooks/tezgah_gate.py::_printer_owns_sink`).
|
|
225
|
+
That check reads the pipeline stage that owns the sink, so `echo "$TOKEN" | docker login --password-stdin > log` passes. A `> /dev/null` is not a file there.
|
|
226
|
+
The rule reads token shapes, not file names: `echo "$KEY" > f` and `cat key > f` carry no token and pass.
|
|
192
227
|
- **A credential file staged.** A `git add` that names a credential file (`hooks/tezgah_snapshot.py::SECRET_FILE`: `.env`, `.env.local`, a key, `id_rsa`, `credentials.json`).
|
|
193
228
|
The command is read word by word (`hooks/tezgah_gate.py::_secret_add`). `.env.example`, `.env.sample`, `.env.template`, `.env.dist` and `.env.defaults` carry names without values and pass (`hooks/tezgah_gate.py::SECRET_TEMPLATES`).
|
|
194
229
|
The rule does not read `git add -A` or `git add .` (`hooks/tezgah_gate.py::SECRET_ADD_DENY`).
|
|
@@ -263,6 +298,18 @@ Trigger: 25 work rows (`hooks/tezgah_gate.py::DRIFT_STEPS`) in the current user
|
|
|
263
298
|
`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`,
|
|
264
299
|
`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`).
|
|
265
300
|
|
|
301
|
+
### Ask — an ask-tool question with no recommended option
|
|
302
|
+
|
|
303
|
+
Not a rule of `decision`: the hosts ask for this warning after `decision` lets the call through, as they ask for the graph line.
|
|
304
|
+
Trigger: an omp `ask` call or a Claude AskUserQuestion call inside a root (`hooks/tezgah_gate.py::ask_advice`).
|
|
305
|
+
The gate counts the questions and the ones that carry a recommended option (`hooks/tezgah_integrity.py::undefaulted`).
|
|
306
|
+
omp marks one with a question's `recommended` index. Claude marks one with an option label such as "(Recommended)" or "(önerilen)".
|
|
307
|
+
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`).
|
|
308
|
+
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`).
|
|
309
|
+
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`.
|
|
310
|
+
Claude routes the tool through its PreToolUse matcher (`hooks/hooks.json`), and omp through its `GATED` list (`hosts/omp/tezgah-hook.ts.in`).
|
|
311
|
+
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)).
|
|
312
|
+
|
|
266
313
|
## What the gate deliberately does not catch
|
|
267
314
|
|
|
268
315
|
Read this before filing a security-ish issue; each is a decision, not an oversight.
|
|
@@ -272,9 +319,11 @@ Read this before filing a security-ish issue; each is a decision, not an oversig
|
|
|
272
319
|
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
|
|
273
320
|
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
|
|
274
321
|
outbound effect carrying only the taint notice, with no refusal in between. The notice itself stays (`hooks/tezgah_untrusted.py`).
|
|
322
|
+
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.
|
|
323
|
+
A grant the user writes in chat ("bana sormadan pushla") lifts the ask for its class. Outward commands still run with no refusal.
|
|
275
324
|
- 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
|
|
276
325
|
the snapshot.
|
|
277
|
-
- `grep -A 3 foo`: a flag with its own value shifts the token, so the search passes unnudged (`hooks/tezgah_gate.py:
|
|
326
|
+
- `grep -A 3 foo`: a flag with its own value shifts the token, so the search passes unnudged (`hooks/tezgah_gate.py:166-167`).
|
|
278
327
|
- 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
|
|
279
328
|
`target`, because that row names no file (`hooks/tezgah_integrity.py::writers_elsewhere`).
|
|
280
329
|
- Which rule the user meant when a turn drifts: "a guess about intent wearing a check's clothes" (`hooks/tezgah_gate.py::drift_reason`).
|
|
@@ -319,17 +368,17 @@ widen a list a regex already covers.
|
|
|
319
368
|
## The mirror: the opencode plugin
|
|
320
369
|
|
|
321
370
|
opencode cannot run Python hooks, so `hosts/opencode/plugins/tezgah.js` is an independent JavaScript re-implementation of the same rules, dispatched from
|
|
322
|
-
`tool.execute.before` (hosts/opencode/plugins/tezgah.js:
|
|
371
|
+
`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`,
|
|
323
372
|
`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:
|
|
324
373
|
`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
|
|
325
374
|
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`).
|
|
326
375
|
|
|
327
376
|
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`,
|
|
328
377
|
`heredocBodies` `hosts/opencode/plugins/tezgah.js:985`, `shellWriteBody` `hosts/opencode/plugins/tezgah.js:1018`, the shortcut and attribution twins at
|
|
329
|
-
`hosts/opencode/plugins/tezgah.js:
|
|
330
|
-
ordering obligation (`COMMIT_CMD` `hosts/opencode/plugins/tezgah.js:
|
|
331
|
-
`hosts/opencode/plugins/tezgah.js:
|
|
332
|
-
`hosts/opencode/plugins/tezgah.js:
|
|
378
|
+
`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
|
|
379
|
+
ordering obligation (`COMMIT_CMD` `hosts/opencode/plugins/tezgah.js:1558`, `ORDER_DENY`
|
|
380
|
+
`hosts/opencode/plugins/tezgah.js:1560`, `lastVerify` `hosts/opencode/plugins/tezgah.js:1571`, `orderReason` `hosts/opencode/plugins/tezgah.js:1596`, dispatched at
|
|
381
|
+
`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
|
|
333
382
|
`fail`, and a row that narrowing would refuse reads `ok`/`ran` on both sides, so the outcome is identical.
|
|
334
383
|
|
|
335
384
|
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
|
|
@@ -356,16 +405,17 @@ rule's triggers are the classes `_stop_block` returns, the four `_shape_block` a
|
|
|
356
405
|
| `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` |
|
|
357
406
|
| `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` |
|
|
358
407
|
| `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` |
|
|
359
|
-
| `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:
|
|
360
|
-
| `race` | two sessions on one file - a parent and its subagent, or two worktrees - landing on a stale read | `hooks/tezgah_gate.py:
|
|
408
|
+
| `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` |
|
|
409
|
+
| `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` |
|
|
410
|
+
| `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` |
|
|
361
411
|
| `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` |
|
|
362
412
|
| `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` |
|
|
363
|
-
| `workspace` | sessions wrote root `plans/`, `research/` and `analysis/` (observed across several unrelated projects on one machine) | `hooks/tezgah_gate.py:
|
|
364
|
-
| `plan` | a turn wrote four product files - a hook, its tests, its docs and the changelog - straight onto `main` | `hooks/tezgah_gate.py:
|
|
413
|
+
| `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` |
|
|
414
|
+
| `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` |
|
|
365
415
|
| `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` |
|
|
366
416
|
| `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` |
|
|
367
417
|
| `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` |
|
|
368
|
-
| `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:
|
|
418
|
+
| `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` |
|
|
369
419
|
| `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` |
|
|
370
420
|
| `check failed` | designed | the newest check failing is its own class; no incident recorded | `tests/test_integrity.py::test_failed_check_blocks` |
|
|
371
421
|
| `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` |
|
|
@@ -373,6 +423,11 @@ rule's triggers are the classes `_stop_block` returns, the four `_shape_block` a
|
|
|
373
423
|
| `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` |
|
|
374
424
|
| `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` |
|
|
375
425
|
| `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` |
|
|
426
|
+
| `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` |
|
|
427
|
+
| `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` |
|
|
428
|
+
| `open todo` | the owner's report (2026-10-12, plan 097 item 2): `ready_left` counted only `pending` items, so a done claim over an `in_progress` todo item ended the turn | `hooks/tezgah_integrity.py::open_left` | `tests/test_integrity.py::test_a_done_claim_over_an_in_progress_todo_is_refused_once` |
|
|
429
|
+
| `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` |
|
|
430
|
+
| `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` |
|
|
376
431
|
| `placating opener` | designed | output rule 10 read at the Stop event; no incident recorded | `tests/test_integrity.py::test_sycophantic_opener_blocks` |
|
|
377
432
|
| `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` |
|
|
378
433
|
| `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` |
|
|
@@ -388,7 +443,7 @@ rule's triggers are the classes `_stop_block` returns, the four `_shape_block` a
|
|
|
388
443
|
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
|
|
389
444
|
[Rule provenance](#rule-provenance), or `bin/tezgah-docs --citations` refuses the rule for having none.
|
|
390
445
|
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
|
|
391
|
-
standing in for `tezgah_gate.capture` on the allow path (`tests/_probe_gate.py:
|
|
446
|
+
standing in for `tezgah_gate.capture` on the allow path (`tests/_probe_gate.py:21-31`). Only if the refusal envelope changes, touch
|
|
392
447
|
`tests/test_claude_adapters.py::PreToolUse` and the codex/cursor/omp hook tests.
|
|
393
448
|
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
|
|
394
449
|
compared and where a shared-vocabulary drift fails.
|
|
@@ -399,10 +454,10 @@ rule's triggers are the classes `_stop_block` returns, the four `_shape_block` a
|
|
|
399
454
|
|
|
400
455
|
## Source of truth
|
|
401
456
|
|
|
402
|
-
- `hooks/tezgah_gate.py` — `hooks/tezgah_gate.py::decision`; every rule constant and refusal text `hooks/tezgah_gate.py:
|
|
457
|
+
- `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`
|
|
403
458
|
`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`,
|
|
404
459
|
`hooks/tezgah_gate.py::RETRY_CEILING`, `hooks/tezgah_gate.py::retry_reason`; `hooks/tezgah_gate.py::secret_command`, `hooks/tezgah_gate.py::race_reason`,
|
|
405
|
-
`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:
|
|
460
|
+
`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)
|
|
406
461
|
- `hooks/projects-pretooluse.py` — the Claude and dsh envelope; `hooks/tezgah_paths.py` — `off`, `root_for`, `cache_dir`
|
|
407
462
|
- `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`
|
|
408
463
|
`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
|
|
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:
|
|
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,
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
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
|
@@ -268,7 +268,8 @@
|
|
|
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
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"
|
|
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)"
|
|
272
273
|
],
|
|
273
274
|
"answers_tr": [
|
|
274
275
|
"bir araştırma hattı nerede yaşar ve ne tutar",
|
|
@@ -277,7 +278,8 @@
|
|
|
277
278
|
"bir oturum bir araştırma hattının bozuk olduğunu nasıl öğrenir",
|
|
278
279
|
"araştırma işaretini ne yakar ve katman nasıl kapatılır",
|
|
279
280
|
"başka biri bitmemişken neden yeni bir araştırma hattı açılamaz",
|
|
280
|
-
"tezgah-research next bir ürün akışı için ne yazdırır ve bir karar ne zaman yeniden ölçülmeli"
|
|
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)"
|
|
281
283
|
],
|
|
282
284
|
"sources": [
|
|
283
285
|
"hooks/tezgah_research.py",
|
|
@@ -383,7 +385,8 @@
|
|
|
383
385
|
"where does an installed version unpack and how do I roll back",
|
|
384
386
|
"something is broken what do I check first",
|
|
385
387
|
"how do I turn on taste signal capture and measure my corrections",
|
|
386
|
-
"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"
|
|
387
390
|
],
|
|
388
391
|
"answers_tr": [
|
|
389
392
|
"bunu bir makineye nasıl kurar, doğrular ve yükseltirim",
|
|
@@ -392,7 +395,8 @@
|
|
|
392
395
|
"kurulu bir sürüm nereye açılır ve nasıl geri alırım",
|
|
393
396
|
"bir şey bozuk, önce neyi kontrol etmeliyim",
|
|
394
397
|
"zevk sinyali kaydını nasıl açar ve düzeltmelerimi nasıl ölçerim",
|
|
395
|
-
"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"
|
|
396
400
|
],
|
|
397
401
|
"sources": [
|
|
398
402
|
"bin/tezgah-setup",
|
|
@@ -407,7 +411,9 @@
|
|
|
407
411
|
"hooks/tezgah_embed.py",
|
|
408
412
|
"hooks/tezgah_taste.py",
|
|
409
413
|
"hooks/tezgah_taste_ledger.py",
|
|
410
|
-
"bin/tezgah-taste"
|
|
414
|
+
"bin/tezgah-taste",
|
|
415
|
+
"hooks/tezgah_task.py",
|
|
416
|
+
"bin/tezgah-task"
|
|
411
417
|
]
|
|
412
418
|
},
|
|
413
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.
|
|
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
|
@@ -25,8 +25,11 @@ generated in every tezgah root because routing needs no capability (`ROLES`,
|
|
|
25
25
|
`tezgah-researcher` (`hooks/tezgah_agents.py::SPECIALISTS`). A specialist's
|
|
26
26
|
description names its slot from `AGENT_SLOT`. The tier a host shows and the
|
|
27
27
|
model line it runs therefore cannot disagree, and `tezgah-agents --list` prints
|
|
28
|
-
both. A cheaper worker that meets work above its tier
|
|
29
|
-
|
|
28
|
+
both. A cheaper worker that meets work above its tier answers `ESCALATE: <why>`,
|
|
29
|
+
and a report missing an `Output to orchestrator` field
|
|
30
|
+
(`hooks/tezgah_agents.py::WORKER_OUTPUT`) counts the same: the router sends one
|
|
31
|
+
narrower re-brief of the same role naming that
|
|
32
|
+
field or judgement, and only a second miss restarts the task on `tezgah-frontier`
|
|
30
33
|
with the original brief rather than handing the failed trajectory up: continuing a
|
|
31
34
|
cheap trajectory on a frontier model was the most expensive option measured
|
|
32
35
|
(arXiv 2608.24358, Table 1: 1.61 cost units against 0.72 for frontier-only).
|
|
@@ -70,6 +73,37 @@ The orchestrator agent keeps `model: inherit`: it is the main thread's own agent
|
|
|
70
73
|
|
|
71
74
|
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
75
|
|
|
76
|
+
A credential is not a balance, so the chain is live. A family whose provider
|
|
77
|
+
answered a limit leaves every chain until its reset (`funded_families`,
|
|
78
|
+
`hooks/tezgah_models.py::down_families`). The record is `model-health.json`
|
|
79
|
+
in the cache dir. The reset comes from the provider's own answer. That is a
|
|
80
|
+
named `reset at` time or a `retry-after-ms`, else six hours (`limit_until`).
|
|
81
|
+
A named reset already past also holds six hours, since the provider's time zone
|
|
82
|
+
is not stated.
|
|
83
|
+
`tezgah-route --health` probes each other family with a credential. OpenRouter
|
|
84
|
+
answers through its key endpoint, which costs no tokens. A family omp runs gets
|
|
85
|
+
one short prompt on its cheap row (`probe_family`). The command then records the
|
|
86
|
+
downs and re-applies omp's chain. Session start and each prompt start it in the
|
|
87
|
+
background when it is due (`health_due`, `start_health`). It is due when it was
|
|
88
|
+
never checked, when it is six hours old, or when a recorded reset has passed. The mode's own family is never
|
|
89
|
+
probed, since it leads the chain anyway. `tezgah-route --fallback any` lets a
|
|
90
|
+
chain fall back to another vendor's family once it is up. A family omp runs on
|
|
91
|
+
an account, such as zai, comes before OpenRouter, which bills per token
|
|
92
|
+
(`hooks/tezgah_models.py::_chain`). The probe uses the consult argv, so it loads
|
|
93
|
+
no tools, rules, skills or extensions.
|
|
94
|
+
Measured 2026-10-11: zai answered `429 Weekly/Monthly Limit Exhausted. Your limit
|
|
95
|
+
will reset at 2026-10-12 02:02:35`, so it stays out of the chain until then.
|
|
96
|
+
A subagent slice that stops on a quota or rate limit pushes the same record at
|
|
97
|
+
once (`hooks/tezgah_models.py::note_limit`). Its close calls it
|
|
98
|
+
(`hooks/tezgah_task.py::close_slice`). omp's transcript names the provider. A
|
|
99
|
+
Claude subagent runs on Anthropic unless its model names another vendor. A
|
|
100
|
+
quota is held until the answer's reset, else six hours. A rate limit is held
|
|
101
|
+
until its reset or retry-after, else ten minutes. Then the push re-applies omp's
|
|
102
|
+
chain. The mode's own family stays first in every chain, so a hold moves
|
|
103
|
+
only the fallbacks after it. Both writers re-read the record under a lock
|
|
104
|
+
before they write (`hooks/tezgah_models.py::_health_locked`), so neither drops
|
|
105
|
+
the other's hold.
|
|
106
|
+
|
|
73
107
|
## What is not routed yet
|
|
74
108
|
|
|
75
109
|
- **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 +131,7 @@ fixed:
|
|
|
97
131
|
2. Otherwise the brief - redacted with the ledger's own reader
|
|
98
132
|
(`redact`, `hooks/tezgah_integrity.py::redact`) - goes to the judge as one Choice over three tiers (`TIER_QUESTION`,
|
|
99
133
|
`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
|
|
134
|
+
Measured with Jev (model `jev-latest`, carrier not recorded) on 40 English briefs labelled by
|
|
101
135
|
the same session that wrote the rubric (2026-09-30, twice): under-route 0.025,
|
|
102
136
|
accuracy 0.925 and 0.900, 392 ms median, about 656 input tokens per call; a
|
|
103
137
|
keyword rule on the same set under-routed 0.100. The session model was not measured on that set.
|
|
@@ -106,7 +140,9 @@ fixed:
|
|
|
106
140
|
specialist? A yes names that agent on its own tier (`AGENT_SLOT`,
|
|
107
141
|
`hooks/tezgah_models.py::AGENT_SLOT`). Before this question, sessions
|
|
108
142
|
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
|
|
143
|
+
Clef agreed on 9 of 10 briefs this session labelled (2026-10-09, model not
|
|
144
|
+
recorded). Each judge figure on this page measures only the provider and model
|
|
145
|
+
it names, and stays unmeasured for any other default.
|
|
110
146
|
3. With no judgement (`judge-off`, no key, a failed call) `--phase review` and
|
|
111
147
|
`--phase research` name the reviewer and the researcher (`PHASE_AGENT`,
|
|
112
148
|
`hooks/tezgah_models.py::PHASE_AGENT`). Another phase picks the tier from the
|
|
@@ -123,16 +159,41 @@ an entry the user owns, and `mode` when omp holds none and the session model run
|
|
|
123
159
|
otherwise. `via` keeps the word `jev` even when the chat fallback answered,
|
|
124
160
|
because the report groups history by it. `judge` is where the two differ.
|
|
125
161
|
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
162
|
+
On omp and opencode a routed worker runs in its own session and its own ledger. That
|
|
163
|
+
ledger opens with a `spawned` row that names the parent session. On opencode the
|
|
164
|
+
child's own title carries its type. On omp the parent's task call records which type
|
|
165
|
+
each named child runs. On Claude Code and Codex the worker runs under its parent's
|
|
166
|
+
session id, and its rows carry its own `agent` id. There the SubagentStart hook writes
|
|
167
|
+
a `subagent_start` row into the parent's ledger with that id and the child's type.
|
|
168
|
+
Both hosts' payloads carry `agent_id` and `agent_type`. Claude's SessionStart hook
|
|
169
|
+
exports `TEZGAH_SESSION` through `CLAUDE_ENV_FILE`. A Codex shell carries
|
|
170
|
+
`CODEX_THREAD_ID`, the root thread's id. So the `route` row lands in the same ledger.
|
|
171
|
+
A Cursor subagent runs under its own conversation id. Each Cursor agent shell carries
|
|
172
|
+
its conversation's id as `CURSOR_CONVERSATION_ID`, so the route row lands in the
|
|
173
|
+
parent's ledger. Cursor's
|
|
174
|
+
subagentStart hook opens the child's ledger under `subagent_id` with a `spawned` row
|
|
175
|
+
that names `parent_conversation_id`. Cursor's docs do not say that the child's own
|
|
176
|
+
calls carry `subagent_id`, so that link counts only once a row of the child lands.
|
|
177
|
+
A host started from another host's agent shell inherits that host's id. When two of
|
|
178
|
+
these ids differ, no variable tells which host runs the call, so `tezgah-route`
|
|
179
|
+
writes no row.
|
|
180
|
+
|
|
181
|
+
`tezgah-route --report` joins by type when any child has a type. A route owns the
|
|
182
|
+
first fan-out of its type after it and before the next route of that type. A fan-out
|
|
183
|
+
is the children of one task call, each started within 2 seconds of the one before
|
|
184
|
+
(`FAN_OUT_GAP`). One failed first check in it fails the route, else a pass passes it
|
|
185
|
+
(`OUTCOME_ORDER`). Later spawns of that type in the window had no route, so the
|
|
186
|
+
report leaves them out. A route gets nothing when the route of its type before it
|
|
187
|
+
had no worker yet: both routes' workers may sit in one fan-out. It also gets nothing
|
|
188
|
+
when an untyped child started inside its span. It gets nothing when its fan-out
|
|
189
|
+
started more than 30 minutes after it (`ROUTE_GAP`). With no typed child, order is the
|
|
190
|
+
fallback. It joins only when one child sits between each route and the next, with
|
|
191
|
+
no two spawns in one second. The report then reads each child's first check: `pass`,
|
|
192
|
+
`fail`, `ran` (no outcome seen) or `none`. A route it cannot prove counts as
|
|
193
|
+
`unjoined`: a tie, an unrouted spawn, or a host with no child row. Two cases write
|
|
194
|
+
none. `cursor-agent` 2026.10.01 fires no subagentStart, and its child's calls name no
|
|
195
|
+
parent. The dsh bridge runs Claude's hook, and no dsh SubagentStart payload on
|
|
196
|
+
record shows `agent_id`. The parent's own next check never counts for the worker.
|
|
136
197
|
One line per `via` and tier gives the first-attempt pass rate. The `jev-below-static`
|
|
137
198
|
line counts Jev routes under the static tier and how many of them failed. That is the
|
|
138
199
|
measurement that decides whether Jev routing becomes the default or the static table
|