@r1z4x/tezgah 1.3.0 → 1.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +103 -1
- package/MANIFEST +3 -0
- package/agents/tezgah-reviewer.md +19 -10
- package/bin/tezgah-docs +3 -3
- package/bin/tezgah-doctor +75 -18
- package/bin/tezgah-route +5 -2
- package/bin/tezgah-setup +30 -18
- package/bin/tezgah-status +3 -1
- package/bin/tezgah-task +130 -37
- package/bin/tezgah-taste +74 -55
- package/bin/tezgah-triage +12 -7
- package/docs/architecture.md +4 -4
- package/docs/clarity-baseline.json +9 -9
- package/docs/contract.md +22 -12
- package/docs/environment.md +4 -1
- package/docs/evidence.md +71 -34
- package/docs/gate.md +21 -21
- package/docs/glossary.md +5 -5
- package/docs/hosts.md +8 -8
- package/docs/judge.md +93 -27
- package/docs/operations.md +69 -30
- package/docs/skills.md +2 -2
- package/docs/status-line.md +7 -7
- package/docs/vision.md +10 -5
- package/hooks/tezgah_agents.py +22 -11
- package/hooks/tezgah_attest.py +15 -22
- package/hooks/tezgah_context.py +84 -124
- package/hooks/tezgah_gate.py +26 -23
- package/hooks/tezgah_guard.py +9 -11
- package/hooks/tezgah_integrity.py +77 -178
- package/hooks/tezgah_judge.py +270 -122
- package/hooks/tezgah_lessons.py +18 -63
- package/hooks/tezgah_paths.py +183 -55
- package/hooks/tezgah_policy.py +35 -8
- package/hooks/tezgah_replay.py +57 -33
- package/hooks/tezgah_shapes.py +4 -4
- package/hooks/tezgah_skill_pick.py +6 -35
- package/hooks/tezgah_snapshot.py +25 -46
- package/hooks/tezgah_store.py +1268 -0
- package/hooks/tezgah_task.py +146 -40
- package/hooks/tezgah_taste.py +21 -18
- package/hooks/tezgah_taste_ledger.py +63 -88
- package/hooks/tezgah_update.py +6 -26
- package/hosts/codex/hook.py +5 -0
- package/hosts/cursor/hook.py +19 -31
- package/hosts/opencode/plugins/tezgah.js +309 -108
- package/package.json +1 -1
- package/skills/harness/SKILL.md +4 -0
- package/skills/plan-add/SKILL.md +23 -7
- package/skills/plan-add/render_table.py +3 -2
- package/skills/plan-status/SKILL.md +12 -4
- package/skills/plan-sync/SKILL.md +19 -9
- package/skills/tezgah-contract/SKILL.md +25 -12
|
@@ -18,10 +18,10 @@
|
|
|
18
18
|
"semicolons": 9
|
|
19
19
|
},
|
|
20
20
|
"docs/contract.md": {
|
|
21
|
-
"long_sentences":
|
|
22
|
-
"passive":
|
|
21
|
+
"long_sentences": 42,
|
|
22
|
+
"passive": 25,
|
|
23
23
|
"replace_words": 0,
|
|
24
|
-
"semicolons":
|
|
24
|
+
"semicolons": 30
|
|
25
25
|
},
|
|
26
26
|
"docs/environment.md": {
|
|
27
27
|
"long_sentences": 3,
|
|
@@ -30,10 +30,10 @@
|
|
|
30
30
|
"semicolons": 3
|
|
31
31
|
},
|
|
32
32
|
"docs/evidence.md": {
|
|
33
|
-
"long_sentences":
|
|
34
|
-
"passive":
|
|
33
|
+
"long_sentences": 108,
|
|
34
|
+
"passive": 55,
|
|
35
35
|
"replace_words": 1,
|
|
36
|
-
"semicolons":
|
|
36
|
+
"semicolons": 39
|
|
37
37
|
},
|
|
38
38
|
"docs/feature-audit.md": {
|
|
39
39
|
"long_sentences": 20,
|
|
@@ -61,9 +61,9 @@
|
|
|
61
61
|
},
|
|
62
62
|
"docs/judge.md": {
|
|
63
63
|
"long_sentences": 22,
|
|
64
|
-
"passive":
|
|
64
|
+
"passive": 14,
|
|
65
65
|
"replace_words": 0,
|
|
66
|
-
"semicolons":
|
|
66
|
+
"semicolons": 13
|
|
67
67
|
},
|
|
68
68
|
"docs/layers.md": {
|
|
69
69
|
"long_sentences": 20,
|
|
@@ -78,7 +78,7 @@
|
|
|
78
78
|
"semicolons": 9
|
|
79
79
|
},
|
|
80
80
|
"docs/operations.md": {
|
|
81
|
-
"long_sentences":
|
|
81
|
+
"long_sentences": 66,
|
|
82
82
|
"passive": 21,
|
|
83
83
|
"replace_words": 3,
|
|
84
84
|
"semicolons": 42
|
package/docs/contract.md
CHANGED
|
@@ -38,12 +38,14 @@ rules and all.
|
|
|
38
38
|
The skill suggestion is the one surface a judgement writes rather than a constant.
|
|
39
39
|
The roster reaches a session as an index of host-truncated one-liners, so which
|
|
40
40
|
entry to look at first is the turn's own weak spot; one batched judgement request
|
|
41
|
-
answers it -
|
|
41
|
+
answers it - the session's own CLI, else the Jev model through whichever carrier
|
|
42
|
+
resolves (TypeSafe, OpenRouter System One, Cloudflare Workers AI, a compatible
|
|
43
|
+
endpoint), else the OpenRouter chat fallback
|
|
42
44
|
(`credential()`, `hooks/tezgah_judge.py::credential`) - a Choice across the installed
|
|
43
45
|
skill names plus `none`, and one Noul
|
|
44
46
|
asking whether the turn wants a skill at all - and the winner is appended after
|
|
45
47
|
the armed paragraphs as a hint to look at first, never as an instruction to load
|
|
46
|
-
(`hooks/tezgah_skill_pick.py:41-
|
|
48
|
+
(`hooks/tezgah_skill_pick.py:41-76` is the whole configuration, wired into
|
|
47
49
|
`context_for` at `hooks/tezgah_context.py::context_for`). Measured cost of the line
|
|
48
50
|
itself: 455-523 characters, about 115-130 tokens of prompt (the section address
|
|
49
51
|
added ~150 characters over the name-only line; `bin/tezgah-skill` is the same
|
|
@@ -158,11 +160,14 @@ never told this; and a ledger with no such row costs no bytes. `verify-off` drop
|
|
|
158
160
|
it with the integrity rule it restates.
|
|
159
161
|
|
|
160
162
|
The on-demand contract also carries detail the core has no budget for. One is
|
|
161
|
-
the review rule
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
163
|
+
the review rule. A plan gets one review over its whole diff, in at most two
|
|
164
|
+
rounds. Round one fans out one reviewer per dimension. Only a confirmed critical
|
|
165
|
+
or major finding opens round two, and `tezgah-task review` refuses a third. A
|
|
166
|
+
change with an executable oracle - a checker, a gate rule, a parser - spends the
|
|
167
|
+
same budget. Its round one is four direction-led reviewers, and each claim
|
|
168
|
+
becomes a fixture that runs before it counts. That population is a tentative
|
|
169
|
+
default, measured on two research lines with a written flip
|
|
170
|
+
(`skills/tezgah-contract/SKILL.md:519-550`).
|
|
166
171
|
|
|
167
172
|
## Composition order of the always-on core
|
|
168
173
|
|
|
@@ -272,12 +277,17 @@ sentence of the rule.
|
|
|
272
277
|
`PROMPT_HINTS` (`hooks/tezgah_context.py::PROMPT_HINTS`) is one compiled pattern per key
|
|
273
278
|
— `spec`, `consult`, `research`, `product`, `graph`, `fanout` — and `classify_prompt()` returns the keys
|
|
274
279
|
a prompt matches (`hooks/tezgah_context.py::classify_prompt`). `fanout` arms on the
|
|
275
|
-
shape of the request, not a word. Two
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
280
|
+
shape of the request, not a word. Two list entries on their own lines arm it.
|
|
281
|
+
An entry starts with `1.`, `1)`, `1 -`, `1:`, `a)` or a bullet. An inline
|
|
282
|
+
`1. ... 2. ...` arms it too. So do three files named as one list, "First, ...
|
|
283
|
+
Second, ..." and three imperative clauses in one sentence. An explicit
|
|
284
|
+
subagent, parallel or worktree ask arms it as well. The owner no longer has to
|
|
285
|
+
say "do it with subagents, in parallel". On that turn only, the matching
|
|
286
|
+
paragraphs are appended after the reminder
|
|
279
287
|
(`hooks/tezgah_context.py::context_for`); a session that never asks such a question
|
|
280
|
-
pays the one-line pointer instead.
|
|
288
|
+
pays the one-line pointer instead. A later match in the same session pays one
|
|
289
|
+
line. The fan-out line still names the action
|
|
290
|
+
(`hooks/tezgah_context.py::armed_again`). The patterns carry Turkish stems because the
|
|
281
291
|
user writes Turkish, and a plain prompt arms nothing. The same prompt arms the
|
|
282
292
|
same rules on every host (`tests/test_context.py::ArmingConformance.test_all_hosts_arm_the_same_rules`) and each advisory
|
|
283
293
|
rule keeps a pointer line in the always-on text (`tests/test_context.py::ArmingConformance.test_every_advisory_rule_has_an_always_on_pointer`).
|
package/docs/environment.md
CHANGED
|
@@ -56,6 +56,7 @@ the kill switches are files, not variables, and live in
|
|
|
56
56
|
| `TEZGAH_ORCA_BIN` | `hooks/tezgah_orca.py::cli` | `ORCA_CLI_COMMAND`, then `$ORCA_CLI_BIN_DIR/orca`, then `orca` on PATH (`orca-ide` on Linux outside Orca), then the macOS app's CLI | The Orca CLI `tezgah-status --orca` and `tezgah-setup --status` ask. A missing path is no CLI, never a fallback: the test suite points it at one. |
|
|
57
57
|
| `TEZGAH_OMP_BIN` | `hooks/tezgah_paths.py::omp_bin` | `omp` on PATH | The omp binary the installer, the model table, consult's session-model lookup and the judgement seam's session call run; tests point it at a stand-in. |
|
|
58
58
|
| `TEZGAH_CLAUDE_BIN` | `claude_bin`, `hooks/tezgah_paths.py::claude_bin` | `claude` on PATH | The Claude Code CLI `--install` registers the plugin with (`claude plugin`) and the judgement seam asks in a Claude Code session; the test suite points it at a path that does not exist, or at a stand-in. |
|
|
59
|
+
| `TEZGAH_OPENCODE_BIN`, `TEZGAH_CURSOR_BIN`, `TEZGAH_CODEX_BIN` | `hooks/tezgah_paths.py::opencode_bin`, `hooks/tezgah_paths.py::cursor_bin`, `hooks/tezgah_paths.py::codex_bin` | `opencode`, `cursor-agent`, `codex` on PATH or a per-user bin dir | The CLI the judgement seam asks in that host's session; tests point them at stand-ins. |
|
|
59
60
|
| `TEZGAH_DSH_BIN` | `bin/tezgah-setup::reconcile_json_servers` | the profile-local entry, then `bin/tezgah-dsh`, then npx | How the installer invokes the dsh CLI. |
|
|
60
61
|
| `TEZGAH_INDEX_BIN` | `bin/tezgah-dsh:19`, `bin/tezgah-dsh.cmd` | `~/.config/tezgah/bin/tezgah-index` | The index worker `tezgah-dsh` warms before it starts dsh. |
|
|
61
62
|
| `TEZGAH_STATUS_BIN` | `hosts/dsh/statusline/lib/index.js:24`, `hosts/opencode/tui/tezgah-tui.tsx` | `~/.config/tezgah/bin/tezgah-status` | The status renderer the dsh status line and the opencode TUI call. |
|
|
@@ -73,7 +74,7 @@ the kill switches are files, not variables, and live in
|
|
|
73
74
|
|
|
74
75
|
| Variable | Read by | Default | Effect |
|
|
75
76
|
|---|---|---|---|
|
|
76
|
-
| `OMPCODE`, `CLAUDECODE` | `hooks/tezgah_paths.py::session_cli
|
|
77
|
+
| `OMPCODE`, `CLAUDECODE`, `OPENCODE`, `CURSOR_AGENT`, `CURSOR_VERSION`, `CODEX_THREAD_ID` | `hooks/tezgah_paths.py::session_cli` (all), `bin/consult`, `hooks/tezgah_models.py::worker_model` (the first two) | set by the host | Name the host session: the judgement seam asks that host's CLI first, in the order listed (omp sets both of the first two, so `OMPCODE` wins; Cursor sets `CURSOR_AGENT` in its shell tool and `CURSOR_VERSION` in its hooks; Codex sets `CODEX_THREAD_ID` in its shell tool only). The test suite unsets all of them. |
|
|
77
78
|
| `TYPESAFE_API_KEY` | `hooks/tezgah_judge.py::key`, `hooks/tezgah_paths.py` | `~/.config/typesafe/key` | The judgement seam's first third-party credential, used as `fallback` in `config.json` allows ([judge](judge.md)); omp also reads it for its own judge. |
|
|
78
79
|
| `OPENROUTER_API_KEY` | `hooks/tezgah_judge.py::openrouter_key`, `bin/consult`, `bin/codegen`, `hooks/tezgah_models.py` | `~/.config/openrouter/key` | The seam's chat fallback, consult's and codegen's `openrouter` provider, and omp's `any` family. |
|
|
79
80
|
| `DEEPSEEK_API_KEY`, `INCEPTION_API_KEY` | `bin/consult`, `bin/codegen`, `hooks/tezgah_replay.py::label_model` | `~/.config/deepseek/key`, `~/.config/inception/key` | The `deepseek` and `inception` providers of consult and codegen; `DEEPSEEK_API_KEY` alone is also the replay sheet's `--provider deepseek` rater. |
|
|
@@ -82,6 +83,7 @@ the kill switches are files, not variables, and live in
|
|
|
82
83
|
| `TEZGAH_OPENROUTER_URL` | `hooks/tezgah_judge.py::openrouter_url` | OpenRouter's chat-completions URL | Repoints the seam's chat fallback (tests). Plain `http` is refused unless the host is this machine. |
|
|
83
84
|
| `TEZGAH_DEEPSEEK_URL` | `hooks/tezgah_replay.py::_deepseek` | `https://api.deepseek.com/chat/completions` | Repoints the replay sheet's `--provider deepseek` rater (tests). Plain `http` is refused unless the host is this machine. |
|
|
84
85
|
| `TEZGAH_JUDGE_MODEL` | `hooks/tezgah_judge.py::fallback_model` | the cheap `any` row of the models table | The model the seam's chat fallback asks. |
|
|
86
|
+
| `TEZGAH_JUDGE_CLI` | `hooks/tezgah_paths.py::session_cli` | `judge_cli` in `config.json`, else `auto` | The session CLI the judgement seam asks: `omp`, `claude`, `opencode`, `cursor` or `codex`, or `auto` to read the host's marker. A named CLI that is not installed means no session CLI, never another one. |
|
|
85
87
|
|
|
86
88
|
## consult and codegen
|
|
87
89
|
|
|
@@ -122,6 +124,7 @@ the kill switches are files, not variables, and live in
|
|
|
122
124
|
| Variable | Read by | Default | Effect |
|
|
123
125
|
|---|---|---|---|
|
|
124
126
|
| `TEZGAH_E2E_STRICT` | `tests/e2e_*.py`, `.github/workflows/ci.yml` | unset (a missing prerequisite is a SKIP) | `1` turns an end-to-end script's SKIP into a FAIL; CI sets it. |
|
|
127
|
+
| `TEZGAH_OPENCODE_NO_SQLITE` | `hosts/opencode/plugins/tezgah.js` | unset (node:sqlite when the runtime has it) | `1` makes the opencode plugin read and write the evidence ledger through `python3 hooks/tezgah_store.py evidence ...`, the path a runtime without node:sqlite takes; a test sets it. |
|
|
125
128
|
|
|
126
129
|
## Debugging
|
|
127
130
|
|
package/docs/evidence.md
CHANGED
|
@@ -7,9 +7,11 @@ undone from. Read it before changing a rule that records or reads evidence, or w
|
|
|
7
7
|
|
|
8
8
|
## The ledger
|
|
9
9
|
|
|
10
|
-
One append-only
|
|
11
|
-
|
|
12
|
-
|
|
10
|
+
One append-only ledger per session, stored as the `evidence` rows of `<cache>/tezgah.db`
|
|
11
|
+
(`hooks/tezgah_store.py::EVIDENCE_SCHEMA`). A row keeps the JSON text its writer produced, with the
|
|
12
|
+
session, `kind` and `ts` in columns beside it. A caller names a ledger by its old file path,
|
|
13
|
+
`<cache>/evidence/<slug>.jsonl`. The stem is the session key: a readable prefix plus a hash of the
|
|
14
|
+
session id, which nothing can turn back into the id (`hooks/tezgah_integrity.py::_slug`). Every writer goes through `note_path` (`hooks/tezgah_integrity.py::note_path`): `note()`
|
|
13
15
|
derives the path from the session id (`hooks/tezgah_integrity.py::note`). The row is built in exactly one place (`hooks/tezgah_integrity.py::note_path`), and its shape is
|
|
14
16
|
`{"kind": …, "ts": …, "v": …, "detail": …}` plus whatever `LEDGER_FIELDS` keys the writer knew — for a check
|
|
15
17
|
the host reported passing, `{"kind": "verify_ok", "ts": 1758000000, "v": 3, "detail": "pytest -q",
|
|
@@ -50,17 +52,47 @@ plugin, whose row carries the same field under the same empty-means-absent rule
|
|
|
50
52
|
(`hosts/opencode/plugins/tezgah.js:1230-1241`) — so no live host leaves it out. A row written *before*
|
|
51
53
|
the field landed names its tool only where the name survived in `detail` — an `unknown` row and an
|
|
52
54
|
MCP `external` row — so the one cause of an under-count is the corpus's age, and it shrinks as
|
|
53
|
-
sessions run. The append is one
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
55
|
+
sessions run. The append is one INSERT in autocommit (`hooks/tezgah_integrity.py::_append`,
|
|
56
|
+
`hooks/tezgah_store.py::append_evidence`), so a row is whole or absent and two writers queue on the
|
|
57
|
+
database's lock. A row the database refuses (busy past its timeout, a damaged file) goes to the
|
|
58
|
+
session's old JSONL file instead, marked `unlocked`, and the next import takes it. It is best
|
|
59
|
+
effort: a write failure is never the caller's. The store creates the
|
|
60
|
+
database `0600` in a `0700` directory. Its rows name the paths and commands of the user's work, and
|
|
61
|
+
audit L-6 found the old files `0644`.
|
|
62
|
+
|
|
63
|
+
**The JSONL files it replaced.** Every read or write of a session first imports its old
|
|
64
|
+
`<cache>/evidence/<slug>.jsonl` file (`hooks/tezgah_store.py::_sync`). The import costs one stat
|
|
65
|
+
when nothing is new. It takes the database's write lock first, then the file's flock if it is free.
|
|
66
|
+
It takes only the complete lines past the bytes the `imported` table already counts. It never
|
|
67
|
+
renames the file aside. An opencode process started before the store may still run the old plugin,
|
|
68
|
+
which appends to it, and a refused row lands there. Session start launches the bulk import
|
|
69
|
+
of every other file, detached and at most once a day
|
|
70
|
+
(`hooks/tezgah_store.py::import_later`). `python3 hooks/tezgah_store.py import-evidence` and
|
|
71
|
+
`tezgah-doctor --import-evidence` run it on demand. A reader that spans every session imports first
|
|
72
|
+
(`hooks/tezgah_store.py::sessions`).
|
|
73
|
+
|
|
74
|
+
**opencode's plugin** opens the same database through node:sqlite. It uses the store's busy
|
|
75
|
+
timeout, WAL switch and schema (`evidenceDb` in `hosts/opencode/plugins/tezgah.js`), so either side
|
|
76
|
+
may create it. `tests/test_opencode_plugin.py::OpenCodePlugin.test_the_plugin_creates_the_store_python_creates`
|
|
77
|
+
holds the two schemas equal. Before a read or write of a session it asks the store to import that
|
|
78
|
+
session's old file. It asks again only when the file's size or mtime changed (`importLegacy`). A
|
|
79
|
+
row the store refuses goes to that file marked `unlocked`, as the Python writer's does
|
|
80
|
+
(`legacyAppend`). A runtime without node:sqlite, which is node before 22.5, runs
|
|
81
|
+
`python3 hooks/tezgah_store.py evidence append|tail|first|kind|import`
|
|
82
|
+
(`hooks/tezgah_store.py::_evidence_cli`) for each read or write instead. `TEZGAH_OPENCODE_NO_SQLITE=1`
|
|
83
|
+
forces that path. Two limits are known. node:sqlite is synchronous, so a busy database can block
|
|
84
|
+
the host's event loop for up to the 5-second busy timeout. Second, the plugin's deadline can kill a
|
|
85
|
+
CLI append after the CLI committed the row. The plugin then writes the row to the old file too,
|
|
86
|
+
and the ledger holds it twice.
|
|
87
|
+
|
|
88
|
+
Every reader skips a line that ends in a newline and does not parse (`_parse`
|
|
89
|
+
`hooks/tezgah_integrity.py::_parse`). Such a line reaches the database only through an imported file,
|
|
90
|
+
which stores it as written. The reader used to raise there, and the guard then failed the Stop rule
|
|
91
|
+
open for the turn, so corrupting a `verify_fail` row let a claim through. Now the reader writes one
|
|
92
|
+
`ledger_damage` row per damaged line, keyed on a digest of the line (`_note_damage`
|
|
61
93
|
`hooks/tezgah_integrity.py::_note_damage`). The Stop rule refuses a done-claim in a turn that holds one
|
|
62
|
-
as "evidence tampered". A tail with no newline is a fragment, not damage:
|
|
63
|
-
|
|
94
|
+
as "evidence tampered". A tail with no newline is a fragment, not damage: the import waits for its
|
|
95
|
+
newline, and the file append cuts it before its own write. Any other read error in another session's
|
|
64
96
|
ledger still costs only that ledger's rows (`_foreign_rows`; audit M-7).
|
|
65
97
|
|
|
66
98
|
What never reaches the ledger: tool result bodies (only `out_bytes`, a size,
|
|
@@ -78,7 +110,7 @@ a write tool is `edit`, a shell call is `verify` when its command matches the ch
|
|
|
78
110
|
| `turn` | `hooks/tezgah_integrity.py::note_turn`, from the prompt path | `hooks/tezgah_integrity.py::_turn_start`, scoping every turn rule; `hooks/tezgah_integrity.py::_claim_key` |
|
|
79
111
|
| `run`, `edit`, `verify`, `verify_ok`, `verify_fail`, `interrupted` | `hooks/tezgah_integrity.py::note_tool`, the call's own name in `tool`, on opencode the plugin writes these same kinds and the same field (`hosts/opencode/plugins/tezgah.js:1192-1270`) | the Stop rule's `worked` set `hooks/tezgah_integrity.py::WORK_KINDS`; `counters.steps` `hooks/tezgah_integrity.py::_counts`; `last_verify`/`partial_state`; the `--trend` tool histogram |
|
|
80
112
|
| `external`, `unknown` | `hooks/tezgah_integrity.py::note_tool` | the taint notice, via `source`; the `--trend` tool histogram; nothing counts them as work |
|
|
81
|
-
| `began` | the gate's allow path for a write or shell call `hooks/tezgah_gate.py::decision`; on opencode the plugin's before-hook when the core was not asked (`hosts/opencode/plugins/tezgah.js:
|
|
113
|
+
| `began` | the gate's allow path for a write or shell call `hooks/tezgah_gate.py::decision`; on opencode the plugin's before-hook when the core was not asked (`hosts/opencode/plugins/tezgah.js:2549-2565`) | `hooks/tezgah_integrity.py::unanswered`, behind a claim in the Stop rule and in `counters.unanswered` |
|
|
82
114
|
| `claim` | `hooks/tezgah_integrity.py::stop_reason`, only for a reply in the claim vocabulary - a completion or verification word (`DONE`/`VERIFIED`), blocked or allowed, or a refused claim about an external system's state (`_external_claim`) | `hooks/tezgah_integrity.py::counters` |
|
|
83
115
|
| `refusal` | `stop_reason`, for a blocked reply that claimed nothing - a work-only or a shape refusal - with the same `blocked: <class>` detail (row version 3) | `counters.refusals`, and `shape_blocked` for a shape class; never a claim; `NOT_TOOL_HOOK` lists it |
|
|
84
116
|
| `after_block` | `hooks/tezgah_integrity.py::stop_reason`, on the reply after this rule's own block (`stop_hook_active` in a turn holding a `blocked:` claim or refusal row) on Claude, Codex and omp; `detail` is `would block: <class>`, `ok` or `no claim` | nothing yet: a record for the after-block observation window, not a claim and not a second `shape` row, so `counters` counts it neither as a claim nor as a reply; `NOT_TOOL_HOOK` lists it |
|
|
@@ -92,7 +124,7 @@ a write tool is `edit`, a shell call is `verify` when its command matches the ch
|
|
|
92
124
|
| `lesson_hit` | nothing yet: reserved for plan 061 Phase B, a compiled lesson check that matched a call in its observe stage, with the lesson's `key` and the call's `id` | nothing yet |
|
|
93
125
|
| `ledger_damage` | `hooks/tezgah_integrity.py::_note_damage`, from `_parse`, once per damaged line, with its 12-hex `key` | the Stop rule's first evidence class, "evidence tampered" (`hooks/tezgah_integrity.py::_stop_block`) |
|
|
94
126
|
| `disarm` | `hooks/tezgah_context.py::disarmed`, on the prompt a switch first appears on after the session's first prompt; `detail` is the switch name. Also the gate (`hooks/tezgah_gate.py::decision`), for a call it lets through that changes a `CONTROL_EVIDENCE` target - an open plan's delete or move, a forced `git add` of a `.tezgah/` path - with the call's `id` and `detail` `control: <what>` | the status line's `gate` mark ([status-line.md](status-line.md)) for the switch rows, set by the same check that writes them; the gate's rows are read by a person or a replay; no counter |
|
|
95
|
-
| `attest` | `hooks/tezgah_attest.py::run`, once per session start: Claude and dsh (`hooks/projects-auto-init.py::main`), Codex, Cursor and omp from their session-start events, opencode at the first message through `oncePerSession` (`bin/tezgah-context::main`). `detail` is `ok`, `drifted: <entries>` (an entry added, removed or changed since install, a hook file any user can write, a stale omp bridge, a changed entry script on a release install) or `unverified: <why>`; `host` names the host, `switches` the kill switches present. The module is imported only inside that call, so a broken one costs the row and never a gate | the `drift` status mark and the `harness` field of the session's later `claim` rows, read from one
|
|
127
|
+
| `attest` | `hooks/tezgah_attest.py::run`, once per session start: Claude and dsh (`hooks/projects-auto-init.py::main`), Codex, Cursor and omp from their session-start events, opencode at the first message through `oncePerSession` (`bin/tezgah-context::main`). `detail` is `ok`, `drifted: <entries>` (an entry added, removed or changed since install, a hook file any user can write, a stale omp bridge, a changed entry script on a release install) or `unverified: <why>`; `host` names the host, `switches` the kill switches present. The module is imported only inside that call, so a broken one costs the row and never a gate | the `drift` status mark and the `harness` field of the session's later `claim` rows, read from one `harness_drift` row of `<cache>/tezgah.db` per session and host (`hooks/tezgah_attest.py::drift_key`, so one host's clean start cannot clear another's) - an annotation, never a Stop block; `NOT_TOOL_HOOK` lists it, so a session start after a compaction does not read as a gate that ran |
|
|
96
128
|
| `crash` | `hooks/tezgah_guard.py::safe` for a core call that raised, and `hooks/tezgah_guard.py::import_failed` for an entry point whose core imports raised (`detail` `import: <class>: <message>`). An import failure also writes one stderr line and the status line's `crash` mark, always; when `tezgah_integrity` or `tezgah_paths` is the module that failed, the row is the part that cannot be written. Hooks fail open with exit 0; `tezgah-gate check` exits 3 | `NOT_TOOL_HOOK` lists it; no counter |
|
|
97
129
|
|
|
98
130
|
**`compact` is what a compaction kept, from the record.** When the host hands the PostCompact
|
|
@@ -148,8 +180,8 @@ or its call's rows went missing. The reader marks it in memory and never writes
|
|
|
148
180
|
`scratch_evidence` and `tezgah-route --report` all read it as a check nobody saw pass. The Stop rule
|
|
149
181
|
refuses a done-claim in its turn as "evidence tampered", and `counters.orphans` counts it. A false
|
|
150
182
|
block refuses an honest turn, so the reader marks nothing in five cases. These are a ledger with no
|
|
151
|
-
`began` row before the pass, a turn with a `crash` row, and `pretooluse-off`. The others are
|
|
152
|
-
append
|
|
183
|
+
`began` row before the pass, a turn with a `crash` row, and `pretooluse-off`. The others are a row
|
|
184
|
+
a JSONL append wrote without the lock (it carries `unlocked`) and the sandbox fallback cache.
|
|
153
185
|
A row with no outcome, such as Cursor's afterShellExecution `verify`, does not take a pass's `began`.
|
|
154
186
|
`turn_rows` seeds the waiting `began` rows from the 200 rows before the turn. So a call that straddles a
|
|
155
187
|
prompt pairs there as it does in `events`. A forged `began` row plus its pass stays invisible:
|
|
@@ -423,8 +455,9 @@ is. Every denial is itself a `deny` row.
|
|
|
423
455
|
|
|
424
456
|
## The session store for the status marks
|
|
425
457
|
|
|
426
|
-
`<cache>/
|
|
427
|
-
by `used()` (`hooks/tezgah_context.py::used`). A row is
|
|
458
|
+
The `used` rows of `<cache>/tezgah.db`, written by `record()` (`hooks/tezgah_context.py::record`) and read
|
|
459
|
+
by `used()` (`hooks/tezgah_context.py::used`). A row is the session's slug, one kind and the time it was last
|
|
460
|
+
recorded (`hooks/tezgah_store.py::use`) — no outcome, one row per session and kind —
|
|
428
461
|
and the kinds are the used-tool marks [status-line.md](status-line.md) lights up (`graph`, `consult`,
|
|
429
462
|
`research`, `judge`), plus one kind per shipped skill a read opened. It is separate from the
|
|
430
463
|
[ledger](glossary.md#ledger) because it is display state, not
|
|
@@ -449,7 +482,7 @@ line never asked. Three surfaces recognise a read, and they have to agree on the
|
|
|
449
482
|
bridge calls (`hosts/omp/hook.py::classify`), omp's own filter, which decides whether python hears about the
|
|
450
483
|
read at all (`hosts/omp/tezgah-hook.ts.in:225-262`, its catalogue the checkout `@HOOK@` names), and
|
|
451
484
|
opencode's plugin, which classifies in process because that host has no Python hook
|
|
452
|
-
(`hosts/opencode/plugins/tezgah.js:
|
|
485
|
+
(`hosts/opencode/plugins/tezgah.js:2162-2219`, its catalogue the host's own installed skill dir
|
|
453
486
|
(`dirname(CONFIG)/opencode/skills`), which `tezgah-setup` fills with every shipped skill).
|
|
454
487
|
|
|
455
488
|
Claude loads a skill through its `Skill` tool, never a read, so a Claude load used to leave no row.
|
|
@@ -463,14 +496,15 @@ proves the hook side only.
|
|
|
463
496
|
recorded sessions opened it, and names the ones none did: a skill is a dependency that has to keep
|
|
464
497
|
earning the context lines it costs, and accretion is invisible from reading the skill itself. It is a
|
|
465
498
|
measurement, not a gate - it names the candidates for retirement, and deleting one stays the owner's
|
|
466
|
-
call. Three ceilings are honest parts of the number, not bugs: the window is the
|
|
467
|
-
`FITNESS_WINDOW = 200`
|
|
499
|
+
call. Three ceilings are honest parts of the number, not bugs: the window is the
|
|
500
|
+
`FITNESS_WINDOW = 200` sessions that recorded last, by the time each last recorded a kind
|
|
501
|
+
(`hooks/tezgah_store.py::used_sessions`), which says when the session last did anything this store sees; a session counts
|
|
468
502
|
as having opened a skill when it recorded that skill's kind, so reads recorded before `skill:` existed
|
|
469
503
|
still count through the two marks; and only the hosts that classify a read in process record one
|
|
470
504
|
here, so the count is a floor, not a census. Which host records what, since this store is the
|
|
471
505
|
report's only source: **omp** (the extension's filter lets the read through,
|
|
472
506
|
`hosts/omp/tezgah-hook.ts.in:255-262`, and the hook classifies it, `hosts/omp/hook.py::classify`) and
|
|
473
|
-
**opencode** (the plugin classifies it, `hosts/opencode/plugins/tezgah.js:
|
|
507
|
+
**opencode** (the plugin classifies it, `hosts/opencode/plugins/tezgah.js:2204-2219`) record the mark
|
|
474
508
|
or `skill:<name>` for every shipped skill, in either form; **Claude** sees a read, but only in its
|
|
475
509
|
transcript, which feeds the status line and writes no row here (`statusline.py::claude_used`), so a Claude
|
|
476
510
|
session reads in this report as one that opened nothing - marks included; **codex**, **cursor** and
|
|
@@ -488,8 +522,9 @@ deliberately not here.
|
|
|
488
522
|
## Snapshots and rollback
|
|
489
523
|
|
|
490
524
|
Before a write is allowed, `capture` copies the current bytes of every file the call names into
|
|
491
|
-
`<cache>/snapshots/<id>/file
|
|
492
|
-
`hooks/tezgah_snapshot.py::_capture_one`),
|
|
525
|
+
`<cache>/snapshots/<id>/file` (`hooks/tezgah_snapshot.py::_store`, `:74-79`,
|
|
526
|
+
`hooks/tezgah_snapshot.py::_capture_one`), indexes it with its manifest as a `snapshots` row of
|
|
527
|
+
`<cache>/tezgah.db` (`hooks/tezgah_store.py::add_snapshot`), and appends one `snapshot` row whose `detail` is the resolved path, `id` the 12-hex
|
|
493
528
|
snapshot id, `hash` the sha256 and `out_bytes` the size (`hooks/tezgah_snapshot.py::_capture_one`). The row is written last, so it
|
|
494
529
|
never names a copy that is not on disk. A credential file (`SECRET_FILE`: `.env*`, `*.env`, key and
|
|
495
530
|
certificate files, `id_rsa`-style keys, `credentials.json`, `.netrc`, `.npmrc`, `.pypirc`, `.pgpass`,
|
|
@@ -499,11 +534,11 @@ are written `0600` in `0700` directories. Every host calls `capture` in process
|
|
|
499
534
|
JavaScript plugin reaches it through `bin/tezgah-capture` (`bin/tezgah-capture:1-20`).
|
|
500
535
|
The core captures a call put to `tezgah-gate decide` itself, so the plugin skips it.
|
|
501
536
|
|
|
502
|
-
The store is capped twice (`hooks/tezgah_snapshot.py:
|
|
503
|
-
capture crosses it (`hooks/tezgah_snapshot.py::CAP`, `hooks/
|
|
537
|
+
The store is capped twice (`hooks/tezgah_snapshot.py:35-56`): `CAP = 200` snapshots, oldest evicted in capture order when a
|
|
538
|
+
capture crosses it (`hooks/tezgah_snapshot.py::CAP`, `hooks/tezgah_store.py::add_snapshot`), and `MAX_BYTES = 2 MiB` per file, above which
|
|
504
539
|
**nothing is captured at all** — an over-large file is skipped because `capture` runs synchronously in
|
|
505
540
|
the gate on the path of the very write it protects, so copying gigabytes would stall that call, and a
|
|
506
|
-
refused capture writes no row and so claims no copy (`hooks/tezgah_snapshot.py:
|
|
541
|
+
refused capture writes no row and so claims no copy (`hooks/tezgah_snapshot.py:49-56`, `hooks/tezgah_snapshot.py::_capture_one`).
|
|
507
542
|
|
|
508
543
|
`restore` (`hooks/tezgah_snapshot.py::restore`) has exactly one caller, `bin/tezgah-rollback`, which a user runs:
|
|
509
544
|
`tezgah-rollback <snapshot-id> [--force]` (`bin/tezgah-rollback::main`). It refuses an unknown or
|
|
@@ -586,8 +621,8 @@ empty stand-in for the result body its bridge never sends, so a subagent's repor
|
|
|
586
621
|
cannot import the core in process and mirrors the control in JavaScript instead — the channel on the
|
|
587
622
|
call's own row, the label and the taint notice in front of the result the hook is handed, including
|
|
588
623
|
the `external` row an MCP answer or a fetched page earns (`untrustedSource`
|
|
589
|
-
`hosts/opencode/plugins/tezgah.js:
|
|
590
|
-
`tierRead` at `:
|
|
624
|
+
`hosts/opencode/plugins/tezgah.js:1919`, `labelResult` `:1993`, with the tier's argv reader
|
|
625
|
+
`tierRead` at `:1902`). The two halves are pinned against each other over a shared corpus, so neither
|
|
591
626
|
can move without failing the other's test (`tests/test_opencode_plugin.py::OpenCodePlugin.test_the_classifier_agrees_with_the_python_half_on_a_shared_corpus`). One difference is deliberate:
|
|
592
627
|
opencode's plugin writes that `external` row itself and carries the channel alone, since the UI rule
|
|
593
628
|
`_screen_read` belongs to the Python half and does not run there.
|
|
@@ -693,7 +728,8 @@ the corpus ran under).
|
|
|
693
728
|
No counter above says whether a refusal was right. The ledger stores the verdict, never the truth.
|
|
694
729
|
`tezgah-gate replay` (`hooks/tezgah_replay.py`) builds the instrument that can. It is a report and
|
|
695
730
|
changes no gate behaviour. Every file it writes stays under `~/.cache/tezgah/replay/<run>/`, owner
|
|
696
|
-
only, and never leaves the machine.
|
|
731
|
+
only, and never leaves the machine. The run's summary is its row of the cache's `tezgah.db`
|
|
732
|
+
(`hooks/tezgah_store.py::add_replay`), and the newest row is the latest run. Every figure it prints names its cutoff.
|
|
697
733
|
|
|
698
734
|
- **Corpus.** The run reads every ledger under the cache and writes none. It drops a whole ledger
|
|
699
735
|
when `fixture_ledger` calls it a fixture. It also drops one when the session id behind the file
|
|
@@ -749,11 +785,12 @@ one rater's totals would show the other rater those labels.
|
|
|
749
785
|
|
|
750
786
|
`replay --label-model --prompt <file>` is rater 2 when that rater is a model. It sends each sheet
|
|
751
787
|
row to the judge seam (`hooks/tezgah_judge.py`) as one `choice` question and writes
|
|
752
|
-
`labels-model.jsonl` beside the sheet. It redacts the row's text first (`ti.redact`)
|
|
753
|
-
only when
|
|
788
|
+
`labels-model.jsonl` beside the sheet. It redacts the row's text first (`ti.redact`). It runs
|
|
789
|
+
only when a Jev carrier answers: TypeSafe, OpenRouter System One, Cloudflare Workers AI or a
|
|
790
|
+
compatible endpoint. `--provider jev` names it, and the old name `typesafe` still works. The prompt file holds each set's instructions and criteria. A reply
|
|
754
791
|
with no label from those criteria leaves its row unlabelled. The command writes each label as it
|
|
755
792
|
arrives, so a run that stops early keeps what it paid for. A second run asks only the unlabelled rows. Every run prints the input tokens and their cost. The sheet rows leave the machine for
|
|
756
|
-
|
|
793
|
+
the Jev carrier's endpoint. The key and the corpus stay on it. Kappa against a model rater
|
|
757
794
|
measures human-model agreement, not inter-rater reliability.
|
|
758
795
|
|
|
759
796
|
`--provider deepseek --model deepseek-v4-pro` asks the same questions from the same prompt file
|
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:2406` | `new Error(deny)` thrown at `hosts/opencode/plugins/tezgah.js:2567` |
|
|
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.
|
|
@@ -78,7 +78,7 @@ it first after the root check (`decision` `hooks/tezgah_gate.py::decision`). The
|
|
|
78
78
|
|---|---|---|
|
|
79
79
|
| kill switches and opt-in markers | every name in `SWITCHES` (`hooks/tezgah_paths.py::SWITCHES`) under `~/.config/tezgah` or `~/.claude` | any write |
|
|
80
80
|
| tezgah's configuration | all of `~/.config/tezgah` (`config.json`, `bin/`, `ponytail.level`, the `hook:<host>:...` rows of `contract.sha256`, ...) | any write |
|
|
81
|
-
| evidence and session state | the cache's `evidence/`, `sessions/`, `switches/`, `gate-inactive/`, `harness-drift
|
|
81
|
+
| evidence and session state | the cache's `tezgah.db` (with its `-wal` and `-shm` files), `evidence/`, the `sessions/`, `switches/`, `gate-inactive/`, `harness-drift/` and `import-crash/` directories that database imports once, and `workspace-index.json`, in the global cache and the temp fallback (`hooks/tezgah_gate.py::CONTROL_CACHE`) | any write |
|
|
82
82
|
| hook wiring | the opencode plugin file, the omp hook, the Claude plugin copy, the install prefix, and the tree the hooks run from unless the session works inside it (`hooks/tezgah_gate.py::_registration`, `hooks/tezgah_gate.py::_install_trees`) | any write |
|
|
83
83
|
| shared registration files | `.claude/settings.json` and `settings.local.json`, `~/.claude/plugins/installed_plugins.json` and `known_marketplaces.json`, Codex's and Cursor's `hooks.json`, Codex's `config.toml` | a shell write; a write tool only when it changes an entry that is tezgah's - `statusLine`, `attribution`, `disableAllHooks`, a key or item naming tezgah (the user's own hook beside tezgah's in one matcher group stays theirs), or any `[hooks...]` line of `config.toml`, where the trust entries live (`hooks/tezgah_gate.py::_shared_change`) |
|
|
84
84
|
| repository marks and hooks | a `REPO_MARKS` name under a root, the session checkout's `.git/hooks` (a `.sample` file and a clone elsewhere pass), `git config core.hooksPath` set or unset there or `--global` | any write |
|
|
@@ -89,7 +89,7 @@ it first after the root check (`decision` `hooks/tezgah_gate.py::decision`). The
|
|
|
89
89
|
| the hook entries | `projects-auto-init`, `projects-pretooluse`, `projects-posttooluse`, `projects-stop`, `hook.py` under `hosts/codex`, `hosts/cursor` or `hosts/omp`, and the `tezgah-codex-hook`/`tezgah-cursor-hook` launchers (`hooks/tezgah_gate.py::HOOK_ENTRIES`, `hooks/tezgah_gate.py::_host_hook`): fed a forged payload, one writes genuine rows, the switch latch's `authorized` row among them | always |
|
|
90
90
|
|
|
91
91
|
The rule reads the shell word by word, one simple command at a time (`shell_control` `hooks/tezgah_gate.py::shell_control`). It reads each redirect target.
|
|
92
|
-
It reads the arguments of `touch`, `rm`, `rmdir`, `unlink`, `mv`, `chmod`, `mkdir`, `tee`, `truncate`, the copiers' destination and `sed -i`/`perl -i`.
|
|
92
|
+
It reads the arguments of `touch`, `rm`, `rmdir`, `unlink`, `mv`, `chmod`, `mkdir`, `tee`, `truncate`, `sqlite3`, the copiers' destination and `sed -i`/`perl -i`.
|
|
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
|
|
@@ -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: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`).
|
|
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: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…
|
|
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:2492-2496`).
|
|
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
|
|
|
@@ -166,7 +166,7 @@ Trigger: a write tool, or a shell command that writes a file, while an active ta
|
|
|
166
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
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
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:
|
|
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
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
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
172
|
never by the agent, so this rule refuses on a decision the user made rather than one the gate inferred.
|
|
@@ -253,7 +253,7 @@ The search is the pattern of a `grep`, `search` or `ast_grep` tool, a term of om
|
|
|
253
253
|
omp's bridge sends `find`, `search` and `ast_grep` to the gate, because omp's own prompt sends code discovery to `find` first (`hosts/omp/tezgah-hook.ts.in`).
|
|
254
254
|
Told: the host's graph tool (`hooks/tezgah_gate.py::graph_tool`) and the runnable command built from the denied search (`codegraph explore <X>`, `codegraph callers <X>`, `codegraph impact <X>`).
|
|
255
255
|
An omp subagent uses this CLI, because omp's MCP device refuses concurrent writes (`hooks/tezgah_gate.py::nudge_reason`).
|
|
256
|
-
Once-only: the mark
|
|
256
|
+
Once-only: the mark is a `nudged` row of the cache's `tezgah.db`, keyed `sha1(session)[:16]`. It is written *before* the refusal, so re-issuing the search passes (`hooks/tezgah_gate.py::first_nudge`). opencode's `oncePerSession` writes the same key (`hooks/tezgah_gate.py::nudge_key`).
|
|
257
257
|
After that, every later symbol search passes with one line naming the graph tool, while the index stamp equals HEAD (`hooks/tezgah_gate.py::graph_advice`).
|
|
258
258
|
Claude reads the line as PreToolUse `additionalContext` (`hooks/projects-pretooluse.py`), and omp reads it with the call's `tool_result` (`hosts/omp/tezgah-hook.ts.in`).
|
|
259
259
|
Codex and Cursor read it with their PostToolUse result (`hosts/codex/hook.py`, `hosts/cursor/hook.py`).
|
|
@@ -275,7 +275,7 @@ Read this before filing a security-ish issue; each is a decision, not an oversig
|
|
|
275
275
|
outbound effect carrying only the taint notice, with no refusal in between. The notice itself stays (`hooks/tezgah_untrusted.py`).
|
|
276
276
|
- 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
277
|
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:
|
|
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`).
|
|
279
279
|
- 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
280
|
`target`, because that row names no file (`hooks/tezgah_integrity.py::writers_elsewhere`).
|
|
281
281
|
- 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 +320,17 @@ widen a list a regex already covers.
|
|
|
320
320
|
## The mirror: the opencode plugin
|
|
321
321
|
|
|
322
322
|
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:
|
|
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`,
|
|
324
324
|
`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
325
|
`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
326
|
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
327
|
|
|
328
328
|
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
329
|
`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:
|
|
331
|
-
ordering obligation (`COMMIT_CMD` `hosts/opencode/plugins/tezgah.js:
|
|
332
|
-
`hosts/opencode/plugins/tezgah.js:
|
|
333
|
-
`hosts/opencode/plugins/tezgah.js:
|
|
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
|
|
334
334
|
`fail`, and a row that narrowing would refuse reads `ok`/`ran` on both sides, so the outcome is identical.
|
|
335
335
|
|
|
336
336
|
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 +357,16 @@ rule's triggers are the classes `_stop_block` returns, the four `_shape_block` a
|
|
|
357
357
|
| `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
358
|
| `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
359
|
| `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:
|
|
361
|
-
| `race` | two sessions on one file - a parent and its subagent, or two worktrees - landing on a stale read | `hooks/tezgah_gate.py:
|
|
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` |
|
|
362
362
|
| `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
363
|
| `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:
|
|
365
|
-
| `plan` | a turn wrote four product files - a hook, its tests, its docs and the changelog - straight onto `main` | `hooks/tezgah_gate.py:
|
|
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` |
|
|
366
366
|
| `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
367
|
| `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
368
|
| `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:
|
|
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` |
|
|
370
370
|
| `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
371
|
| `check failed` | designed | the newest check failing is its own class; no incident recorded | `tests/test_integrity.py::test_failed_check_blocks` |
|
|
372
372
|
| `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` |
|
|
@@ -400,10 +400,10 @@ rule's triggers are the classes `_stop_block` returns, the four `_shape_block` a
|
|
|
400
400
|
|
|
401
401
|
## Source of truth
|
|
402
402
|
|
|
403
|
-
- `hooks/tezgah_gate.py` — `hooks/tezgah_gate.py::decision`; every rule constant and refusal text `hooks/tezgah_gate.py:
|
|
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`
|
|
404
404
|
`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
405
|
`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:
|
|
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)
|
|
407
407
|
- `hooks/projects-pretooluse.py` — the Claude and dsh envelope; `hooks/tezgah_paths.py` — `off`, `root_for`, `cache_dir`
|
|
408
408
|
- `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
409
|
`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
|
@@ -22,7 +22,7 @@ The per-session check of tezgah's own hook entries against the install record. `
|
|
|
22
22
|
The byte ceiling one injected event may spend: `CONTEXT_BUDGET` per event with `DEFAULT_BUDGET` behind it (`hooks/tezgah_context.py::CONTEXT_BUDGET`), enforced by `budgeted()` (`hooks/tezgah_context.py::budgeted`), which gives up blocks lowest value first along `DROP_ORDER` (`hooks/tezgah_context.py::DROP_ORDER`) - the skill hint among the first, the task phase, the delta and the pointer last - shrinking a block in `SHRINK` (the per-turn relevant lessons, to their first lesson) before it drops it (`hooks/tezgah_context.py::SHRINK`). The core, the reminder, an armed paragraph and the pinned user constraints sit outside `DROP_ORDER`. An event can therefore stay over its budget, and its note says so. The ceiling counts bytes, never tokens, because the module has no tokenizer. Not the [metadata band](#metadata-band), which is the installer's measurement of the same text rather than a runtime bound.
|
|
23
23
|
|
|
24
24
|
### capture
|
|
25
|
-
The pre-write copy the gate takes of every file a write is about to change, `capture()` (`hooks/tezgah_snapshot.py::capture`), called on the gate's allow path (`hooks/tezgah_gate.py::decision`) and capped at `CAP` snapshots and `MAX_BYTES` per file (`hooks/tezgah_snapshot.py:
|
|
25
|
+
The pre-write copy the gate takes of every file a write is about to change, `capture()` (`hooks/tezgah_snapshot.py::capture`), called on the gate's allow path (`hooks/tezgah_gate.py::decision`) and capped at `CAP` snapshots and `MAX_BYTES` per file (`hooks/tezgah_snapshot.py:55-56`); a file too large or unreadable is left uncaptured and writes no row, so nothing claims a copy that is not there. Not a transaction: the copy is bytes on disk, not a rollback that runs itself.
|
|
26
26
|
|
|
27
27
|
### claim
|
|
28
28
|
A ledger [row](#row) recording the [Stop rule](#stop-rule)'s verdict on one reply, `detail` being `ok` or `blocked: <reason class>`, written by `stop_reason()` (`hooks/tezgah_integrity.py::stop_reason`, row at `hooks/tezgah_integrity.py::stop_reason`). Not a deny: nothing ran, the turn simply may not end.
|
|
@@ -47,7 +47,7 @@ What one host can do with one rule surface: the gate, the Stop rule or a subagen
|
|
|
47
47
|
|
|
48
48
|
<a id="ledger"></a>
|
|
49
49
|
### evidence ledger
|
|
50
|
-
One
|
|
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
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.
|
|
@@ -98,7 +98,7 @@ A mark consumed by its own first use, so the thing it protects happens once: the
|
|
|
98
98
|
An opt-out file in the repo tree - `.no-ponytail`, `.no-adhd`, `.no-graph`, `.no-lessons`, `.no-taste` - collected by `repo_marks()` walking up to the enclosing root (`hooks/tezgah_context.py::repo_marks`). Not a [kill switch](#kill-switch): a mark travels with the repo, a switch with the machine.
|
|
99
99
|
|
|
100
100
|
### plan
|
|
101
|
-
A piece of work spanning sessions, one markdown file with frontmatter under `<repo root>/.tezgah/plans/open/`, moved to `.tezgah/plans/done/` when closed, up to three of them injected by `open_plans()` (`hooks/tezgah_context.py::open_plans`) and its work happening on a `plan/NNN-slug` branch. Not a TODO in code.
|
|
101
|
+
A piece of work spanning sessions, one markdown file with frontmatter under `<repo root>/.tezgah/plans/open/`, moved to `.tezgah/plans/done/` when closed, up to three of them injected by `open_plans()` (`hooks/tezgah_context.py::open_plans`) and its work happening on a `plan/NNN-slug` branch. An optional `after:` names the plans it waits on, and `tezgah-task status` reads it with the ticked Acceptance boxes (`hooks/tezgah_task.py::queue`). Not a TODO in code, and not the host's session todo list, which no reader syncs with it.
|
|
102
102
|
|
|
103
103
|
### plan table
|
|
104
104
|
The status table in `.tezgah/plans/README.md` (`.tezgah/plans/README.md:9`), owned by the `plan-status` skill (`skills/plan-status/SKILL.md:1`) and among the first blocks the [budget](#budget) gives up because it lives on disk (`hooks/tezgah_context.py::DROP_ORDER`). Not the injected list of open plans.
|
|
@@ -125,7 +125,7 @@ A directory tezgah is armed over, from `TEZGAH_ROOTS`, the config file or `~/Pro
|
|
|
125
125
|
The edge of a root: outside every root the gate returns nothing (`hooks/tezgah_gate.py::decision`), the context builder returns nothing (`hooks/tezgah_context.py::context_for`) and the per-repo extras are omitted from the status line (`hooks/tezgah_context.py::health_segments`). Not a repository boundary.
|
|
126
126
|
|
|
127
127
|
### row
|
|
128
|
-
One
|
|
128
|
+
One entry of an [evidence ledger](#evidence-ledger), stored as its JSON text: `{"kind","ts","detail"}` plus whatever the writer knew of `LEDGER_FIELDS` (`id`, `exit`, `out_bytes`, `fail_class`, `workspace`, `source`, `hash`, `changed`, `tool`), written by `note_path()` (`hooks/tezgah_integrity.py::note_path`, `hooks/tezgah_integrity.py::_stored_text`) with `detail` redacted and cut to `DETAIL_MAX` (`hooks/tezgah_integrity.py::DETAIL_MAX`). A missing key means the writer did not know it, never null. `tool` is the call's own name where the host reports one, which is what makes a firing histogram possible: `classify` folds the name into `run`/`edit`, so a row that predates the field carries none.
|
|
129
129
|
|
|
130
130
|
### rule
|
|
131
131
|
One bold-labelled paragraph of the contract, the unit a [kill switch](#kill-switch) drops and a test pins: `CORE_RULES` pairs each key with the label it must start with (`hooks/tezgah_context.py::CORE_RULES`) and `core_split()` drops exactly the paragraph whose label matches (`hooks/tezgah_context.py::core_split`). Not a [skill](#skill), which the model chooses to read.
|
|
@@ -134,7 +134,7 @@ One bold-labelled paragraph of the contract, the unit a [kill switch](#kill-swit
|
|
|
134
134
|
One host conversation, identified by the id `session_of()` reads from a payload under whichever name the host uses (`hooks/tezgah_context.py::session_of`), keying the [ledger](#evidence-ledger) filename (`hooks/tezgah_integrity.py::_slug`), the [session store](#session-store) and the per-turn markers (`note_turn()`, `hooks/tezgah_integrity.py::note_turn`). Not a [turn](#turn), of which one session has many.
|
|
135
135
|
|
|
136
136
|
### session store
|
|
137
|
-
The used-tool marks one session wrote, `<cache>/
|
|
137
|
+
The used-tool marks one session wrote, the `used` rows of `<cache>/tezgah.db`, one per kind, written by `record()` (`hooks/tezgah_context.py::record`) and read by `used()` (`hooks/tezgah_context.py::used`) to feed the "used" half of the status marks, with a missing kind recorded nowhere at all. Not the [evidence ledger](#evidence-ledger).
|
|
138
138
|
|
|
139
139
|
### skill
|
|
140
140
|
`skills/<name>/SKILL.md` - frontmatter with `name` and `description`, then instructions the model reads by choice; [skills](skills.md) owns the layer. Not a [rule](#rule), which is injected, and not a [slash command](#slash-command).
|