@r1z4x/tezgah 1.3.0 → 1.4.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 +33 -1
- package/MANIFEST +3 -0
- package/bin/tezgah-doctor +75 -18
- package/bin/tezgah-setup +6 -17
- package/bin/tezgah-taste +30 -29
- package/docs/architecture.md +4 -4
- package/docs/clarity-baseline.json +3 -3
- package/docs/contract.md +1 -1
- package/docs/environment.md +1 -0
- package/docs/evidence.md +67 -31
- package/docs/gate.md +21 -21
- package/docs/glossary.md +4 -4
- package/docs/hosts.md +8 -8
- package/docs/operations.md +48 -20
- package/docs/skills.md +1 -1
- package/docs/status-line.md +7 -7
- package/hooks/tezgah_attest.py +15 -22
- package/hooks/tezgah_context.py +44 -118
- 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 +25 -43
- package/hooks/tezgah_lessons.py +18 -63
- package/hooks/tezgah_paths.py +18 -28
- package/hooks/tezgah_replay.py +31 -18
- 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_taste.py +20 -17
- package/hooks/tezgah_taste_ledger.py +52 -86
- package/hooks/tezgah_update.py +6 -26
- package/hosts/cursor/hook.py +19 -31
- package/hosts/opencode/plugins/tezgah.js +309 -108
- package/package.json +1 -1
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
|
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.
|
|
@@ -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).
|
package/docs/hosts.md
CHANGED
|
@@ -20,7 +20,7 @@ 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:2349-2714`) - `tool.execute.before` carries the whole gate, `tool.execute.after` the channel on the row, the provenance label on the result and the taint notice on the next effect (`untrustedSource` `hosts/opencode/plugins/tezgah.js:1919`, `labelResult` `:1993`) | static only: generated `instructions` files, because there is no session-start injection point (`hosts/opencode/plugins/tezgah.js:3-6`, `bin/tezgah-setup::opencode_contract_text`) | chat.message pushes a synthetic part (`hosts/opencode/plugins/tezgah.js:2629-2680`) | TUI plugin `tezgah-tui.tsx` declared in `tui.json`, runs `tezgah-status` on the event bus (`hosts/opencode/tui/tezgah-tui.tsx:1-11`; wiring `bin/tezgah-setup::install_opencode`) | all seven: skill reads and kinds are classified in process (`hosts/opencode/plugins/tezgah.js:2204-2240`) |
|
|
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
25
|
| omp | session_start, session_compact, session_switch, turn_end, before_agent_start, tool_call, tool_result, session_stop (`hosts/omp/tezgah-hook.ts.in:541,588,607,612,614,634,653,688`) | both: static `RULES.md` (`bin/tezgah-setup::STATIC_RULES`, `bin/tezgah-setup::rules_block`) and a session payload carrying only what a static file cannot know (`hosts/omp/hook.py::handle`) | before_agent_start returns a hidden message (`hosts/omp/tezgah-hook.ts.in:614-632`) | extension draws `ctx.ui.setWidget` below the editor, `setStatus` as fallback (`hosts/omp/tezgah-hook.ts.in:170-203`); ANSI line from `hosts/omp/hook.py::status_line` | all seven: the embedded runner filters skill reads before asking python (`hosts/omp/tezgah-hook.ts.in:255-262`) and no `observable` is passed (`hosts/omp/hook.py::status_line`) |
|
|
26
26
|
|
|
@@ -216,7 +216,7 @@ and say which document it came from.
|
|
|
216
216
|
|
|
217
217
|
A skill read is observable only where the host gives a channel that does not cost
|
|
218
218
|
a process per read: Claude parses its transcript (`statusline.py::claude_used`),
|
|
219
|
-
opencode classifies in process (`hosts/opencode/plugins/tezgah.js:
|
|
219
|
+
opencode classifies in process (`hosts/opencode/plugins/tezgah.js:2204-2240`),
|
|
220
220
|
omp filters in its embedded runner before it asks python
|
|
221
221
|
(`hosts/omp/tezgah-hook.ts.in:255-262`). On codex, cursor and dsh a read is not
|
|
222
222
|
observable at that price, so their surfaces pass the five tool-use measures as
|
|
@@ -339,9 +339,9 @@ In order, each step verified by the one below it:
|
|
|
339
339
|
`hosts/dsh/hooks.json` until the bridge carries outcomes.
|
|
340
340
|
- **opencode carries the untrusted-content control in the plugin**, not by
|
|
341
341
|
importing `hooks/tezgah_untrusted.py`, which a plugin cannot do in process: the
|
|
342
|
-
channel is decided in JS (`untrustedSource` `hosts/opencode/plugins/tezgah.js:
|
|
343
|
-
the tier's argv reader at `:
|
|
344
|
-
result the hook is handed (`labelResult` `:
|
|
342
|
+
channel is decided in JS (`untrustedSource` `hosts/opencode/plugins/tezgah.js:1919`,
|
|
343
|
+
the tier's argv reader at `:1902-1911`), the label and the taint notice ride the
|
|
344
|
+
result the hook is handed (`labelResult` `:1993`). A shared corpus in
|
|
345
345
|
`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
|
|
346
346
|
fails if their answers differ, so neither can move without the other.
|
|
347
347
|
- **omp carries both untrusted-content marks through the shared core.** Its
|
|
@@ -373,16 +373,16 @@ In order, each step verified by the one below it:
|
|
|
373
373
|
- **opencode asks the core for the rules it cannot port, and counts the ask.** The
|
|
374
374
|
task rule's phase and allowlist and the language rule's word list are answered by
|
|
375
375
|
`bin/tezgah-gate decide` (the recording verb; `check` is the dry run) rather than copied into JavaScript (`IDENT_CMD`
|
|
376
|
-
`hosts/opencode/plugins/tezgah.js:585`, the call site `:
|
|
376
|
+
`hosts/opencode/plugins/tezgah.js:585`, the call site `:2492-2495`) - one spawn per write
|
|
377
377
|
and per identifier-creating command. The delegation fails open by design: a missing
|
|
378
378
|
CLI, a non-zero exit, a broken pipe or a 10 s timeout refuses nothing (`collect`
|
|
379
379
|
`hosts/opencode/plugins/tezgah.js:152-178`). One `delegation` row now records the class when the core could not
|
|
380
|
-
answer (`noteDelegation` `:
|
|
380
|
+
answer (`noteDelegation` `:2304-2310`), so a silent fail-open is countable instead
|
|
381
381
|
of invisible.
|
|
382
382
|
- **opencode has no session-start hook**, so its always-on file must carry the
|
|
383
383
|
pointer every hook host appends, and the per-session index, agent and contract
|
|
384
384
|
refreshes ride the first `chat.message` instead (`bin/tezgah-setup::refresh_contract`,
|
|
385
|
-
`hosts/opencode/plugins/tezgah.js:
|
|
385
|
+
`hosts/opencode/plugins/tezgah.js:2663-2678`). It denies the native `skill`
|
|
386
386
|
tool - the generated router replaces the injected skill list
|
|
387
387
|
(`bin/tezgah-setup::install_opencode`) - and `permission.ask` may never be emitted by a
|
|
388
388
|
given build, which is why `tool.execute.before` stays the enforcing half
|