@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.
Files changed (53) hide show
  1. package/CHANGELOG.md +103 -1
  2. package/MANIFEST +3 -0
  3. package/agents/tezgah-reviewer.md +19 -10
  4. package/bin/tezgah-docs +3 -3
  5. package/bin/tezgah-doctor +75 -18
  6. package/bin/tezgah-route +5 -2
  7. package/bin/tezgah-setup +30 -18
  8. package/bin/tezgah-status +3 -1
  9. package/bin/tezgah-task +130 -37
  10. package/bin/tezgah-taste +74 -55
  11. package/bin/tezgah-triage +12 -7
  12. package/docs/architecture.md +4 -4
  13. package/docs/clarity-baseline.json +9 -9
  14. package/docs/contract.md +22 -12
  15. package/docs/environment.md +4 -1
  16. package/docs/evidence.md +71 -34
  17. package/docs/gate.md +21 -21
  18. package/docs/glossary.md +5 -5
  19. package/docs/hosts.md +8 -8
  20. package/docs/judge.md +93 -27
  21. package/docs/operations.md +69 -30
  22. package/docs/skills.md +2 -2
  23. package/docs/status-line.md +7 -7
  24. package/docs/vision.md +10 -5
  25. package/hooks/tezgah_agents.py +22 -11
  26. package/hooks/tezgah_attest.py +15 -22
  27. package/hooks/tezgah_context.py +84 -124
  28. package/hooks/tezgah_gate.py +26 -23
  29. package/hooks/tezgah_guard.py +9 -11
  30. package/hooks/tezgah_integrity.py +77 -178
  31. package/hooks/tezgah_judge.py +270 -122
  32. package/hooks/tezgah_lessons.py +18 -63
  33. package/hooks/tezgah_paths.py +183 -55
  34. package/hooks/tezgah_policy.py +35 -8
  35. package/hooks/tezgah_replay.py +57 -33
  36. package/hooks/tezgah_shapes.py +4 -4
  37. package/hooks/tezgah_skill_pick.py +6 -35
  38. package/hooks/tezgah_snapshot.py +25 -46
  39. package/hooks/tezgah_store.py +1268 -0
  40. package/hooks/tezgah_task.py +146 -40
  41. package/hooks/tezgah_taste.py +21 -18
  42. package/hooks/tezgah_taste_ledger.py +63 -88
  43. package/hooks/tezgah_update.py +6 -26
  44. package/hosts/codex/hook.py +5 -0
  45. package/hosts/cursor/hook.py +19 -31
  46. package/hosts/opencode/plugins/tezgah.js +309 -108
  47. package/package.json +1 -1
  48. package/skills/harness/SKILL.md +4 -0
  49. package/skills/plan-add/SKILL.md +23 -7
  50. package/skills/plan-add/render_table.py +3 -2
  51. package/skills/plan-status/SKILL.md +12 -4
  52. package/skills/plan-sync/SKILL.md +19 -9
  53. package/skills/tezgah-contract/SKILL.md +25 -12
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:2328-2681`) - `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:1898`, `labelResult` `:1972`) | 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:2596-2647`) | 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:2183-2219`) |
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:2183-2219`),
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:1898`,
343
- the tier's argv reader at `:1881-1890`), the label and the taint notice ride the
344
- result the hook is handed (`labelResult` `:1972`). A shared corpus in
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 `:2459-2462`) - one spawn per write
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` `:2283-2289`), so a silent fail-open is countable instead
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:2630-2645`). It denies the native `skill`
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
package/docs/judge.md CHANGED
@@ -14,9 +14,10 @@ the kill-switch paragraph and nothing else (`CORE`,
14
14
  One seam, five callers, one provider order, one egress boundary, one price, one
15
15
  redirect guard. The module is stdlib only and total - a failure is a `None`, never
16
16
  an exception, because a hook imports it and a hook that raises takes a session
17
- down (`_request`, `hooks/tezgah_judge.py::_request`). One endpoint and one key path
18
- are module constants (`URL`, `hooks/tezgah_judge.py::URL`; `KEY_FILE`,
19
- `hooks/tezgah_judge.py::KEY_FILE`), one opener is shared by every call (`OPENER`,
17
+ down (`_request`, `hooks/tezgah_judge.py::_request`). The carriers' endpoints are
18
+ module constants: `URL` (`hooks/tezgah_judge.py::URL`), `JEV_OPENROUTER_URL`
19
+ (`hooks/tezgah_judge.py::JEV_OPENROUTER_URL`) and `CLOUDFLARE_URL`
20
+ (`hooks/tezgah_judge.py::CLOUDFLARE_URL`). One opener serves every call (`OPENER`,
20
21
  `hooks/tezgah_judge.py::OPENER`), and it refuses a redirect that leaves the
21
22
  endpoint's host, so a `Location` cannot carry the bearer (`guarded_opener`,
22
23
  `hooks/tezgah_paths.py`, the one copy `bin/consult` and `bin/codegen` use too). A
@@ -25,15 +26,18 @@ attempt. A 4xx, a session CLI's non-zero exit and a malformed reply never do
25
26
  (`_transient`, `hooks/tezgah_judge.py::_transient`). A hook caller asks for one
26
27
  attempt and a wall-clock `deadline` instead (`ask`, `hooks/tezgah_judge.py::ask`).
27
28
  A session CLI answers in 4-10 s (measured 2026-10-07), so the skill hint's
28
- 4 s deadline usually ends first. `TEZGAH_TYPESAFE_URL`, `TEZGAH_OPENROUTER_URL`
29
- and `TEZGAH_OMP_BIN`/`TEZGAH_CLAUDE_BIN` are the test seams.
29
+ 4 s deadline usually ends first. `TEZGAH_TYPESAFE_URL`, `TEZGAH_JEV_OPENROUTER_URL`,
30
+ `TEZGAH_CLOUDFLARE_URL`, `TEZGAH_OPENROUTER_URL` and
31
+ `TEZGAH_OMP_BIN`/`TEZGAH_CLAUDE_BIN` are the test seams. Each URL override goes
32
+ through `override` (`hooks/tezgah_judge.py::override`), which refuses plain http
33
+ off this machine.
30
34
 
31
35
  Every caller reads an answer through the same accessors rather than reaching
32
36
  into the raw reply, so a Choice and a Noul are read one way for all five
33
37
  (`choice`, `hooks/tezgah_judge.py::choice`; `noul`,
34
38
  `hooks/tezgah_judge.py::noul`). A `text` question is the one shape that asks
35
39
  for prose. `bin/tezgah-taste` asks it for a learning's readable line, and only
36
- a generative provider answers one. TypeSafe is never sent one (`text`,
40
+ a generative provider answers one. No Jev carrier is sent one (`text`,
37
41
  `hooks/tezgah_judge.py::text`). All three are total: a missing or
38
42
  wrongly-typed answer is a `None`, never an exception.
39
43
 
@@ -44,9 +48,11 @@ are untouched, so a model answer can never license a "done" (`STEP_KINDS`,
44
48
 
45
49
  The state leaves the machine. By default it goes to the session's own vendor,
46
50
  through the session's own CLI and credential ([below](#the-providers-and-their-order)).
47
- A third party - `api.typesafe.ai` or `openrouter.ai` (`OPENROUTER_URL`,
48
- `hooks/tezgah_judge.py::OPENROUTER_URL`) - reads it only as the `fallback`
49
- setting allows. For the triage the state is the screen's own text, so whoever
51
+ A third party reads it only as the `fallback` setting allows. The third parties
52
+ are the Jev carriers' hosts - `api.typesafe.ai`, `openrouter.ai`,
53
+ `api.cloudflare.com` or a user's `JEV_API_BASE_URL` - and the chat endpoint
54
+ (`OPENROUTER_URL`, `hooks/tezgah_judge.py::OPENROUTER_URL`).
55
+ For the triage the state is the screen's own text, so whoever
50
56
  answers reads any personal data on it. For the docs fallback it is the query.
51
57
  Nothing else goes: no session id, no
52
58
  workspace path, no environment. The seam does not rewrite the state, because
@@ -89,18 +95,74 @@ background `learn` that armed taste starts.
89
95
 
90
96
  `providers()` (`hooks/tezgah_judge.py::providers`) is the order `ask()` tries.
91
97
  The next one is asked only when the previous failed. First comes the session's own
92
- CLI (`tp.session_cli`, `hooks/tezgah_paths.py::session_cli`): `omp` when
93
- `OMPCODE` is set, else `claude` when `CLAUDECODE` is. It runs headless with no
94
- tools, rules, skills, extensions, MCP servers or settings (`SESSION_ARGV`,
95
- `hooks/tezgah_judge.py::SESSION_ARGV`). Measured 2026-10-07, a call cost $0.007
96
- on claude and $0.015 on omp. The session's own credential pays, OAuth subscription
97
- included. The model and usage are read from the CLI's JSON output
98
- (`_session_request`, `hooks/tezgah_judge.py::_session_request`). Then come the
99
- third parties. TypeSafe reads `TYPESAFE_API_KEY`, else `~/.config/typesafe/key`
100
- (`key`, `hooks/tezgah_judge.py::key`). OpenRouter reads `OPENROUTER_API_KEY`, else
101
- `~/.config/openrouter/key` (`openrouter_key`, `hooks/tezgah_judge.py::openrouter_key`).
98
+ CLI (`tp.session_cli`, `hooks/tezgah_paths.py::session_cli`). The host's marker
99
+ names it, in this order:
100
+
101
+ | CLI | Marker | Set by |
102
+ |---|---|---|
103
+ | `omp` | `OMPCODE` | omp, which sets `CLAUDECODE` too |
104
+ | `claude` | `CLAUDECODE` | Claude Code |
105
+ | `opencode` | `OPENCODE` | opencode, in its own process |
106
+ | `cursor` (runs `cursor-agent`) | `CURSOR_AGENT`, `CURSOR_VERSION` | Cursor: the first in its shell tool, the second in its hooks |
107
+ | `codex` | `CODEX_THREAD_ID` | Codex in its shell tool; tezgah's Codex hook sets it from the session id |
108
+
109
+ A marked host whose binary is missing has no session CLI. No other CLI stands
110
+ in. `TEZGAH_JUDGE_CLI`, else `judge_cli` in `~/.config/tezgah/config.json`,
111
+ names one of the five instead. `auto`, the default, reads the marker. A named CLI
112
+ that is not installed is no session CLI either.
113
+
114
+ The CLI runs headless in an empty temp dir. Its flags are `SESSION_ARGV`
115
+ (`hooks/tezgah_judge.py::SESSION_ARGV`). The omp and claude runs drop tools,
116
+ rules, skills, extensions, MCP servers and settings. The other three have no flag
117
+ that drops their tools or takes a system prompt. They run with the least their
118
+ `--help` offers: opencode `--pure`, cursor-agent `--mode ask`, codex
119
+ `-s read-only --ephemeral`. Their prompt starts with the system prompt.
120
+ `opencode run` ran a shell command its prompt asked for in a probe on
121
+ 2026-10-09. So it also gets an inline config that denies every tool
122
+ (`SESSION_ENV`, `hooks/tezgah_judge.py::SESSION_ENV`). With it, the same probe
123
+ wrote nothing. cursor-agent in ask mode refused the same shell call.
124
+ Measured 2026-10-07, a call cost $0.007 on claude and $0.015 on omp. Measured
125
+ 2026-10-09 on one choice question, opencode read 28,646 input tokens ($0.004 by
126
+ its own report), cursor-agent 17,095 and codex 20,919. The session's own
127
+ credential pays, OAuth subscription included. The answer and usage come from the
128
+ CLI's JSON output (`_session_request`, `hooks/tezgah_judge.py::_session_request`).
129
+ `_cli_answer` (`hooks/tezgah_judge.py::_cli_answer`) reads the last three. Only
130
+ omp and claude name the model. Then come the
131
+ third parties: first the chosen Jev carriers, each a real System One transport
132
+ (`_jev_call`, `hooks/tezgah_judge.py::_jev_call`), then the OpenRouter chat
133
+ fallback. `JEV_CARRIERS` (`hooks/tezgah_judge.py::JEV_CARRIERS`) names the six.
134
+ Two of them are not Jev. Clef and Clef-flash are Cloudflare's own decision
135
+ models. They take the same System One request. They answer in the same typed
136
+ shape, so a caller that needs a typed answer takes them like Jev.
137
+
138
+ | Carrier | Endpoint | Credential (env, else file, stripped) | Body |
139
+ |---|---|---|---|
140
+ | `typesafe` | `https://api.typesafe.ai/v1/systemone` | `TYPESAFE_API_KEY`, else `~/.config/typesafe/key`, else the active omp login (`~/.omp/agent/agent.db`, read-only; `typesafe_key`, `hooks/tezgah_paths.py::typesafe_key`) | `{state, model, questions}` |
141
+ | `jev-openrouter` | `https://openrouter.ai/api/v1/systemone` | `OPENROUTER_API_KEY`, else `~/.config/openrouter/key` | the same, a bare `jev-*` id sent as `typesafe/<id>` |
142
+ | `jev-cloudflare` | `https://api.cloudflare.com/client/v4/accounts/<id>/ai/run` | `JEV_CLOUDFLARE_API_TOKEN`, else `CLOUDFLARE_API_TOKEN`, else `~/.config/cloudflare/token`; the account id from `CLOUDFLARE_ACCOUNT_ID`, else `~/.config/cloudflare/account_id` | `{model: "typesafe/jev", input: {state, questions}}`; the reply is read bare or inside `result` |
143
+ | `jev-compatible` | `JEV_API_BASE_URL`, used verbatim | `JEV_API_KEY` | TypeSafe's, the model from `TEZGAH_JEV_MODEL` if set |
144
+ | `clef`, `clef-flash` | `https://api.cloudflare.com/client/v4/accounts/<id>/ai/run/@cf/cloudflare/<name>` | the Cloudflare token and account id above | `{model: "<name>", state, questions}`; the reply read like Cloudflare's above |
145
+
146
+ `JEV_API_BASE_URL` and `JEV_API_KEY` are the names the public jev-mcp server
147
+ reads, so one export serves both. `TEZGAH_JEV_PROVIDER` chooses the carrier,
148
+ else `JEV_PROVIDER`, else `jev` in `~/.config/tezgah/config.json`, else `auto`
149
+ (`jev_choice`, `hooks/tezgah_paths.py::jev_choice`). The values are `auto`,
150
+ `typesafe`, `openrouter`, `cloudflare`, `compatible`, `clef` and `clef-flash`.
151
+ `tezgah-setup --jev <value>` writes the config key
152
+ (`set_jev`, `bin/tezgah-setup::set_jev`). `auto`
153
+ asks every carrier that resolves, in the table's order (`jev_carriers`,
154
+ `hooks/tezgah_paths.py::jev_carriers`). The seam asks a named carrier without
155
+ its credential nothing, and nothing stands in for it: the call records
156
+ `no credential (jev=<value>)` as a failure. `tezgah-status --judge` prints the
157
+ choice and the carriers it resolves to (`jev_summary`,
158
+ `hooks/tezgah_judge.py::jev_summary`). A cost row prices the carrier that
159
+ answered (`input_price`, `hooks/tezgah_judge.py::input_price`). Per 1M input
160
+ tokens: $0.042 for Jev, $0.24 for Clef, $0.09 for Clef-flash. Output is free.
161
+
162
+ OpenRouter's chat fallback reads the OpenRouter key (`openrouter_key`,
163
+ `hooks/tezgah_judge.py::openrouter_key`).
102
164
  It asks the table's cheap row unless `TEZGAH_JUDGE_MODEL` names another
103
- (`fallback_model`, `hooks/tezgah_judge.py::fallback_model`). The key file is
165
+ (`fallback_model`, `hooks/tezgah_judge.py::fallback_model`). The key files are
104
166
  read because a hook runs where `~/.zshenv` never did. A chat answer is asked for in prose
105
167
  (`CHAT_SYSTEM`, `hooks/tezgah_judge.py::CHAT_SYSTEM`) and filtered to the ids
106
168
  asked, a wrongly typed answer dropped (`_chat_answers`,
@@ -111,8 +173,8 @@ asked, a wrongly typed answer dropped (`_chat_answers`,
111
173
 
112
174
  | `fallback` | Session CLI present | No session CLI |
113
175
  |---|---|---|
114
- | `vendor` (default) | the session CLI only; when it fails, no judgement | TypeSafe, then OpenRouter |
115
- | `any` | the session CLI, then TypeSafe, then OpenRouter | TypeSafe, then OpenRouter |
176
+ | `vendor` (default) | the session CLI only; when it fails, no judgement | the Jev carriers, then the chat fallback |
177
+ | `any` | the session CLI, then the Jev carriers, then the chat fallback | the Jev carriers, then the chat fallback |
116
178
  | `none` | the session CLI only | nobody: `available()` is false |
117
179
 
118
180
  Nothing that stands in is silent. A result carries `provider`, the `model`
@@ -124,7 +186,7 @@ hint's cost row and the router's `route` row also carry `judge=<provider>/<model
124
186
 
125
187
  A caller that needs one provider in particular passes `only` (`named`,
126
188
  `hooks/tezgah_judge.py::named`). `learn`'s decision has to come from a typed
127
- model, so it passes `only=("typesafe",)`. It gets `None` rather than another
189
+ model, so it passes `only=("jev",)`, every Jev carrier that resolves. It gets `None` rather than another
128
190
  provider's answer. The caller named the provider, so the `vendor` order does not
129
191
  apply. `none` still keeps every third party out.
130
192
 
@@ -173,8 +235,9 @@ known (`note`, `hooks/tezgah_integrity.py::note`), counted by the row's kind
173
235
  costs a fraction of a cent, so a cache is not worth its state file, and a naive
174
236
  one would make two runs of the same command disagree. Beside the last-use
175
237
  record, the seam keeps a failure marker (`DOWN_FOR`, 300 s, in `hooks/tezgah_judge.py`). A
176
- call that ends on a 401, 402, 5xx or a session CLI's non-zero exit marks that
177
- provider, endpoint and credential down for five minutes. Until the marker
238
+ 401, 402, 403, 5xx or a session CLI's non-zero exit marks that
239
+ provider, endpoint and credential down for five minutes. A 403 is OpenRouter's
240
+ answer to a key past its limit. Until the marker
178
241
  expires, that provider is skipped and sent no request. A dead key or an empty account then costs
179
242
  one refusal per five minutes, not one per prompt. A rotated key gets a new
180
243
  marker, so the seam asks it at once.
@@ -193,8 +256,11 @@ known (`note`, `hooks/tezgah_integrity.py::note`), counted by the row's kind
193
256
 
194
257
  ## Source of truth
195
258
 
196
- - `hooks/tezgah_judge.py` — the seam: endpoint, key path, `available()`, `ask()`,
197
- the answer accessors, the retry class and the provider-down marker.
259
+ - `hooks/tezgah_judge.py` — the seam: the carriers' endpoints and bodies,
260
+ `available()`, `ask()`, the answer accessors, the retry class and the
261
+ provider-down marker.
262
+ - `hooks/tezgah_paths.py` — the key channels (`typesafe_key`, `jev_carriers`)
263
+ and the carrier choice (`jev_choice`).
198
264
  - `bin/tezgah-triage` — the snapshot triage and the per-state matrix; prints the
199
265
  unit it selected, the characters it saved and the cost it paid.
200
266
  - `bin/tezgah-docs` — the page router; its index match, then its one fallback
@@ -202,8 +202,8 @@ fetch, the flip and the re-arm and runs none of the three
202
202
 
203
203
  The status line says when a newer release is out. A `↑X.Y.Z` chip sits beside
204
204
  the logo, in the version's own group, on every surface (`notice_segments()`,
205
- `hooks/tezgah_update.py`). A redraw only reads `~/.cache/tezgah/update.json`.
206
- When that file is a day old, the redraw stamps it and starts one detached
205
+ `hooks/tezgah_update.py`). A redraw only reads the `update_check` row of
206
+ `~/.cache/tezgah/tezgah.db`. When that row is a day old, the redraw stamps it and starts one detached
207
207
  check against the GitHub releases endpoint. An offline check writes nothing,
208
208
  and the next day asks again. `tezgah update` is the command the chip points
209
209
  at, and it is the only thing that moves the install. The
@@ -225,7 +225,7 @@ After a change to the contract text (`hooks/tezgah_policy.py`,
225
225
  - `--refresh` does that without a reinstall, for every armed host's static
226
226
  block and not only opencode's (`refresh_contract`). opencode has no
227
227
  session-start hook, so its plugin runs it once per session
228
- (`hosts/opencode/plugins/tezgah.js:2633-2637`). Until 2026-10-02 it
228
+ (`hosts/opencode/plugins/tezgah.js:2666-2670`). Until 2026-10-02 it
229
229
  re-rendered opencode's files alone and reset the one shared hash, so a rule
230
230
  edit left the other static files stale while `--report` went green (audit
231
231
  M-5).
@@ -370,7 +370,8 @@ does not hold.
370
370
  | Invocation | What it does |
371
371
  |---|---|
372
372
  | `tezgah-doctor` | nothing: sizes, session and event counts, whether opencode is running, the two context-hygiene settings, the `.codegraph` bytes and index presence per repository, and the three host state dirs (`bin/tezgah-doctor::collect`, `bin/tezgah-doctor::report`) |
373
- | `tezgah-doctor --clean` | vacuums opencode's database, and only when opencode is not running (`bin/tezgah-doctor::vacuum_db`, `bin/tezgah-doctor::main`); and deletes tezgah's own hook state not modified for `--retention-days` (default 30) - the ledgers under `evidence/`, `turns/`, `sessions/`, `classify.log`, `context-drops.log` and `debug.log`, in the cache and its sandbox fallback - keeping the current session's (`$TEZGAH_SESSION`) files whatever their age (`sweep_state`) |
373
+ | `tezgah-doctor --clean` | vacuums opencode's database, and only when opencode is not running (`bin/tezgah-doctor::vacuum_db`, `bin/tezgah-doctor::main`); deletes every evidence ledger whose newest row is older than `--retention-days` (default 30), rows, import offsets and old JSONL file together (`hooks/tezgah_store.py::sweep_evidence`), and the turn stamps and used-tool marks last written before then (`hooks/tezgah_store.py::sweep_cache`); and deletes the files not modified for that long - `classify.log`, `context-drops.log`, `debug.log` and the `turns/` and `sessions/` files a running older release still writes - in the cache and its sandbox fallback, keeping the current session's (`$TEZGAH_SESSION`) ledger, stamp and marks whatever their age (`sweep_state`) |
374
+ | `tezgah-doctor --import-evidence` | imports every old `evidence/*.jsonl` file into the cache's `tezgah.db` now, in the cache and its sandbox fallback (`bin/tezgah-doctor::import_evidence`); session start runs the same import detached at most once a day (`hooks/tezgah_store.py::import_later`) |
374
375
  | `tezgah-doctor --coverage` | every tracked file the codegraph index does not hold, in two classes — a supported extension the index is missing, and a shebang-only script with no `.py` twin — with the file counts it read, so an empty result cannot read as "everything is covered" |
375
376
  | `tezgah-doctor --stack` | per host: the context files in load order, the skill dirs and which copy wins, the hook rows per event with their owner, the MCP servers, and every conflict; exits 1 on one ([hosts](hosts.md#precedence-what-each-host-loads-and-in-which-order), `hooks/tezgah_stack.py::report`) |
376
377
  | `tezgah-doctor --prune-sessions DAYS` | deletes sessions idle longer than DAYS through `opencode session delete`, then vacuums; skipped when opencode is running or its CLI is missing (`bin/tezgah-doctor::prune_sessions`, `bin/tezgah-doctor::main`) |
@@ -398,7 +399,7 @@ writes only into a repository that already has a `.tezgah/` directory. A
398
399
  (`hooks/tezgah_context.py::repo_marks`). A workspace of repo-provided data gets
399
400
  nothing.
400
401
 
401
- Rows go to `<repo>/.tezgah/taste/signals.jsonl` through the ledger's own writer
402
+ Rows go to the `signals` table of `<repo>/.tezgah/taste/taste.db`
402
403
  (`_write`, `hooks/tezgah_taste.py::_write`). A user turn adds a `prompt` row,
403
404
  redacted and cut to 2000 characters (`hooks/tezgah_taste.py::note_prompt`).
404
405
  A landed write adds an `edit` row with its redacted old and new text, cut to
@@ -412,7 +413,7 @@ ledger row. A rollback therefore never reads it as a pre-state (`capture_after`,
412
413
  `hooks/tezgah_snapshot.py::capture_after`). A file outside the repository gets no
413
414
  after blob.
414
415
  opencode reaches the same call through a process (`noteTaste`,
415
- `hosts/opencode/plugins/tezgah.js:2380-2390`).
416
+ `hosts/opencode/plugins/tezgah.js:2413-2423`).
416
417
 
417
418
  ```sh
418
419
  touch ~/.config/tezgah/taste-on # arm; rm it to disarm
@@ -434,25 +435,28 @@ builds it (`capture_signals`, `bin/tezgah-taste::capture_signals`). A signal
434
435
  holds the prompt, the files written, the agent's edits, and the user's own edit
435
436
  between two agent writes. That last one compares the after blob with the next
436
437
  write's pre-state, or with the file on disk (`user_diff`,
437
- `bin/tezgah-taste::user_diff`). `learn` asks TypeSafe for one typed decision per
438
- signal (`decide`, `bin/tezgah-taste::decide`, [judge](judge.md)). The decision
438
+ `bin/tezgah-taste::user_diff`). `learn` asks the Jev model for one typed decision
439
+ per signal (`decide`, `bin/tezgah-taste::decide`, [judge](judge.md)). Any Jev
440
+ carrier will do: TypeSafe, OpenRouter System One, Cloudflare Workers AI or a
441
+ compatible endpoint. The decision
439
442
  names preference, defect or none. It picks one of eleven fixed categories and a
440
443
  scope. It also says how the signal relates to each learning held: supports,
441
- contradicts, narrows or unrelated. When another provider answers, `learn` writes
442
- the decision to `decisions.jsonl` as `unverified` and never applies it. A defect
443
- goes to `defects.jsonl` and never into the ledger.
444
+ contradicts, narrows or unrelated. When a provider outside the Jev family answers,
445
+ `learn` records the decision in the `decisions` table as `unverified`. It never
446
+ applies it. The report counts the typed decisions under `jev`. A
447
+ defect goes to the `defects` table and never into the ledger.
444
448
 
445
449
  Armed taste also learns by itself. At session start, `learn` starts in the
446
- background when `signals.jsonl` grew since the last start, at most once an hour
447
- per repository (`learn_later`, `hooks/tezgah_taste.py::learn_later`). It runs
448
- with `--no-fallback` and only when a TypeSafe key resolves, so it never pays a
450
+ background when new signals arrived since the last start. It starts at most
451
+ once an hour per repository (`learn_later`, `hooks/tezgah_taste.py::learn_later`). It runs
452
+ with `--no-fallback` and only when a Jev carrier resolves, so it never pays a
449
453
  generative provider for a decision it could not apply. It does not run on
450
- Windows, which has no ledger lock yet. The stamp and the log live under the
451
- cache dir, in `taste-learn/`.
454
+ Windows, which has no ledger lock yet. The stamp is a `taste_learn` row of the
455
+ cache's `tezgah.db`. The log lives under the cache dir, in `taste-learn/`.
452
456
 
453
- The ledger is `<repo>/.tezgah/taste/ledger.json`, plus
454
- `~/.config/tezgah/taste/ledger.json` for user-scope learnings
455
- (`hooks/tezgah_taste_ledger.py`). Confidence is (r+1)/(r+s+2) over supporting
457
+ The repository's learnings live in the same `taste.db`. User-scope learnings live in
458
+ `~/.config/tezgah/taste/taste.db` (`hooks/tezgah_taste_ledger.py`). Confidence is
459
+ (r+1)/(r+s+2) over supporting
456
460
  and contradicting evidence. Each piece counts at its decision probability, and
457
461
  the ledger forgets it per day at its scope's rate (`confidence`,
458
462
  `hooks/tezgah_taste_ledger.py::confidence`). A learning turns active in its
@@ -465,8 +469,34 @@ ledgers. `learn`, `accept`, `reject` and `edit` rewrite them one at a time on
465
469
  this machine, under a lock on `~/.config/tezgah/taste/ledger.lock` (`locked`,
466
470
  `hooks/tezgah_taste_ledger.py::locked`). The lock spans every repository,
467
471
  because each command also rewrites the user ledger. A second one exits 2 and
468
- names the holder. Every taste row goes through the evidence ledger's locked
469
- writer (`append`, `hooks/tezgah_taste_ledger.py::append`).
472
+ names the holder.
473
+
474
+ One module owns both databases (`taste`, `hooks/tezgah_store.py::taste`). Each
475
+ runs SQLite in WAL mode with a 5-second busy timeout. The directory is 0700 and
476
+ the database file 0600. A row keeps its JSON fields as before. A hook drops a
477
+ row it cannot write, and the CLI's decision, defect and label rows raise
478
+ instead (`append`, `hooks/tezgah_taste_ledger.py::append`). An open of a
479
+ database imports the files it replaces (`_import`,
480
+ `hooks/tezgah_store.py::_import`). `ledger.json` and `gate.json` import only
481
+ into empty tables. The `.jsonl` row files (`signals`, `decisions`, `defects`,
482
+ `labels`, `injected`) import on every open that finds one, so rows an older
483
+ install wrote later still land. The import holds the old writer's flock on the
484
+ file. The `imported` table records how many bytes of each file went in, in the
485
+ same transaction as the rows. The next open therefore takes only the bytes a
486
+ writer added since, and a torn last line waits for its newline. After the commit
487
+ the import renames a fully read file to `<name>.imported`, or `.imported.N`
488
+ beside an earlier one, and never deletes one. A process killed before that
489
+ rename imports nothing twice. A file that does not read or parse stays in
490
+ place. The import retries it only once its size or mtime changes. The old
491
+ per-session `taste-notes/` files under the cache dir are not carried over: they
492
+ never named their repository. A session open during the upgrade can see one
493
+ write note again.
494
+
495
+ Every command that opens the store exits 2 when the repository's `.tezgah`
496
+ came with the clone (`store_or_exit`, `bin/tezgah-taste::store_or_exit`). It
497
+ then reads, imports and creates nothing there. The refusal also applies to a
498
+ repository whose `.tezgah` is a symlink or whose git index cannot be read. Run
499
+ the command from another checkout to manage the user-scope learnings.
470
500
 
471
501
  Session start injects the active learnings (`block`,
472
502
  `hooks/tezgah_taste_ledger.py::block`). A rule needs confidence 0.8 and a
@@ -475,11 +505,12 @@ labelled sample (`calibration`, `hooks/tezgah_taste_ledger.py::calibration`). A
475
505
  hint needs 0.6. A learning whose evidence paths are all gone is withheld. A
476
506
  write also carries the in-scope learnings it has not yet shown in the session,
477
507
  on every host's post-tool channel (`write_note`, `hooks/tezgah_taste.py::write_note`).
508
+ The repository's database keeps the learnings each session has already been shown.
478
509
  `gate` compares preference corrections per writing turn before and after the
479
510
  first injection, by when each turn happened. Once both arms hold 980 turns and
480
511
  the after-rate is not lower
481
- at one-sided p < 0.1, it writes `gate.json` and injection stops (`gate`,
482
- `hooks/tezgah_taste_ledger.py::gate`). `mine` reads omp's top-level session
512
+ at one-sided p < 0.1, injection stops (`gate`,
513
+ `hooks/tezgah_taste_ledger.py::gate`). The database keeps that stop. `mine` reads omp's top-level session
483
514
  files and Claude's top-level transcripts whose cwd is under `--root`
484
515
  (`cmd_mine`, `bin/tezgah-taste::cmd_mine`). `measure` and `rate` use a fixed
485
516
  labelling rule (`RULE`, `bin/tezgah-taste::RULE`).
@@ -633,7 +664,7 @@ or its optional tools and config, on its own initiative
633
664
 
634
665
  ## Triage: `tezgah-triage` and the `judge-off` switch
635
666
 
636
- One judgement seam serves five callers, each of them asking TypeSafe a batched
667
+ One judgement seam serves five callers, each of them asking the judge a batched
637
668
  question instead of paying an agent to read a page: the analyze-app loop's snapshot
638
669
  triage (`bin/tezgah-triage`), `bin/tezgah-docs` for a query its keyword index
639
670
  cannot place, and the prompt-path skill hint (`hooks/tezgah_skill_pick.py`), the
@@ -672,7 +703,10 @@ made - no credential, the switch below, or a failed call - and the loop reads th
672
703
  tree directly instead.
673
704
 
674
705
  The state leaves the machine. A judgement sends the state and the questions to
675
- `api.typesafe.ai` (`ask()`, `hooks/tezgah_judge.py::ask`) - for the triage that is
706
+ the host of whichever provider answers - the session's own CLI (omp, claude,
707
+ opencode, cursor, codex), or the chosen Jev
708
+ carrier's (`api.typesafe.ai`, `openrouter.ai`, `api.cloudflare.com` or a
709
+ `JEV_API_BASE_URL`, see [judge](judge.md)) (`ask()`, `hooks/tezgah_judge.py::ask`) - for the triage that is
676
710
  the snapshot's own text, so a screen carrying personal data is read by a third
677
711
  party, and for the docs fallback it is the reader's query. Nothing else goes: no
678
712
  session id, no workspace path, no credential beyond the bearer header, and the
@@ -682,11 +716,16 @@ request cannot drift. That is why the two shell callers are on-demand - an agent
682
716
  asks for a judgement, no gate does - while the skill hint asks only with its own
683
717
  marker armed, and why the switch below is the off button for the whole path.
684
718
 
685
- The credential resolves from `TYPESAFE_API_KEY`, else from
686
- `~/.config/typesafe/key` (`credential()`, `hooks/tezgah_judge.py::credential`), and when
687
- neither resolves the same questions go to the OpenRouter fallback, whose key rides
688
- `OPENROUTER_API_KEY` and then `~/.config/openrouter/key`
689
- (`openrouter_key()`, `hooks/tezgah_judge.py::openrouter_key`). The file is the channel that
719
+ With no session CLI, the third party is the Jev model through one of its carriers.
720
+ TypeSafe's credential resolves from `TYPESAFE_API_KEY`, else from
721
+ `~/.config/typesafe/key`, else from the active omp login (`typesafe_key()`,
722
+ `hooks/tezgah_paths.py::typesafe_key`). `jev` chooses which of the other Jev
723
+ carriers the seam asks - OpenRouter's System One endpoint, Cloudflare Workers AI
724
+ and any System One-compatible URL (`jev_carriers()`,
725
+ `hooks/tezgah_paths.py::jev_carriers`). The OpenRouter chat fallback's key rides
726
+ `OPENROUTER_API_KEY` and then `~/.config/openrouter/key` (`openrouter_key()`,
727
+ `hooks/tezgah_judge.py::openrouter_key`). When no carrier answers, the same
728
+ questions go to that chat fallback. The file is the channel that
690
729
  matters on a machine exporting the variable from `~/.zshenv`: a hook or a bin tool
691
730
  runs in a non-interactive shell, where that export never ran, so the file is what a
692
731
  judgement actually resolves. Cost is input tokens alone - $0.042 per
package/docs/skills.md CHANGED
@@ -32,7 +32,7 @@ skill is simply not read.
32
32
 
33
33
  opencode's plugin does inject prompt-time text - the builder's per-turn block,
34
34
  the conditional rules included
35
- (`hosts/opencode/plugins/tezgah.js:2596-2625`) - but that text names at most one
35
+ (`hosts/opencode/plugins/tezgah.js:2629-2658`) - but that text names at most one
36
36
  skill (the hint), never the roster, so the installer writes one:
37
37
  `~/.config/tezgah/opencode-skills.md` always-on and
38
38
  `opencode-skills.full.md` on demand (`bin/tezgah-setup::SKILL_ROUTER`, `bin/tezgah-setup::SKILL_ROUTER_FULL`), the first
@@ -176,7 +176,7 @@ it (`tests/test_skills.py::SkillStandards`).
176
176
  | `no-ai-slop` | Edits a draft sharper and more human without losing the voice, or flags slop; fires on a draft review, on a draft in a language other than English, on text that must keep terms, identifiers or unit symbols verbatim, and before publishing English prose tezgah writes itself. |
177
177
  | `analyze-app` | Inspects and drives a running web or mobile app from its accessibility / DOM / view tree, its rendered screen (screenshots, blur and grayscale tests, breakpoints) and measurements taken in the page; fires when a task must verify behaviour or read the UI, not just read source. |
178
178
  | `plan-add` | Creates a plan under `.tezgah/plans/open/` with its own `allowed_paths:` scope, bootstraps the `.tezgah/plans/` layer if missing, commits it in the private `.tezgah` repository (`git -C .tezgah`, never the project's git); fires on `/tezgah:plan-add`, "track this as a plan". |
179
- | `plan-status` | Reads every open plan, enriches rows with live PR state from `gh`, rewrites the README table (committed in the private `.tezgah` repository) and recommends the next plan; fires on `/tezgah:plan-status` or "what plans are open". |
179
+ | `plan-status` | Reads every open plan and `tezgah-task status` (each plan's Acceptance boxes ticked, the plans its `after:` waits on, the ready plans that can run at once), enriches rows with live PR state from `gh`, rewrites the README table (committed in the private `.tezgah` repository) and recommends the next plan; fires on `/tezgah:plan-status` or "what plans are open". |
180
180
  | `plan-sync` | Closes finished plans whose PR merged or closed through `tezgah-task close`, which moves a plan to `.tezgah/plans/done/` as done only when it records an approving `review:`; commits one message per plan in the private `.tezgah` repository; fires on `/tezgah:plan-sync`, "sync plans". |
181
181
  | `research` | Runs an auditable research line - two loops, protocol before run, six-dimension review, provenance; fires when the deliverable is evidence: a lit review, a hypothesis, a benchmark or ablation. |
182
182
  | `product-analysis` | Runs a product analysis across five axes - value (HEART), usability (heuristics, the cognitive walkthrough, WCAG 2.2, axe-core, the running app read through `analyze-app` down to component x state, tree and image both), feasibility (a cited `path:line`), competition (a dated teardown) and triage (keep/fix/cut/bet) - with one named evidence class per finding, executed as a research line; fires on a product, feature, retention or roadmap question rather than a code change. |
@@ -96,9 +96,9 @@ measure the surface cannot see, then armed-and-used or armed-not-used.
96
96
  | `graph` | a code-graph tool call (an `mcp__…codegraph…` tool) | `.no-graph` — a missing binary is `idx`'s report, not this mark's |
97
97
  | `orch` | a delegate call (`Task`/`Agent`/`spawn_agent`) or a subagent start | `orchestrate-off` |
98
98
  | `judge` | a shell command really ran `tezgah-triage` or `tezgah-docs` | `judge-off`, or no credential: no `TYPESAFE_API_KEY` and no non-empty `~/.config/typesafe/key`, and neither the fallback's `OPENROUTER_API_KEY` nor its `~/.config/openrouter/key` |
99
- | `gate` | never `on`: shown only as `gate✗`, right after the version, in two cases. One: the prompt hook found at least 3 gated calls in this session's transcript since its first turn: write and shell calls, every one of which the gate answers with a `began` or a `deny` row (Bash/PowerShell/Edit/Write/MultiEdit/NotebookEdit on Claude, `exec_command`/`shell`/`apply_patch` on Codex). Over the same span the ledger holds no row from the gate. A PostToolUse row is no proof: that hook keeps writing while a broken PreToolUse lets every call through. The same prompt carries the line "tezgah gate inactive on this host: ...". Two: a kill switch appeared after the session's first prompt; the prompt hook writes one `disarm` row naming it (`disarmed`, baseline `<cache>/switches/<session>.json`). Mark file `<cache>/gate-inactive/<session>` (`gate_inactive`, audit Phase 1.3) | a row from the gate appears and no switch armed mid-session is still armed |
100
- | `drift` | never `on`: shown as `drift✗` when a session start found tezgah's own hook entries changed since install (`hooks/tezgah_attest.py::run`). One mark per session and host, `<cache>/harness-drift/<session hash>.<host>`; the session's `attest` row names what drifted | the next clean start of that host in that session |
101
- | `crash` | never `on`: shown as `crash✗` when a hook of this session could not import its core and ran without tezgah (`hooks/tezgah_guard.py::import_failed`, mark `~/.cache/tezgah/import-crash/<session hash>`). When `tezgah_context` itself is the module that failed, `statusline.py` prints `tezgah ✗ core import failed` instead of the line | never by itself: the mark stays for the session |
99
+ | `gate` | never `on`: shown only as `gate✗`, right after the version, in two cases. One: the prompt hook found at least 3 gated calls in this session's transcript since its first turn: write and shell calls, every one of which the gate answers with a `began` or a `deny` row (Bash/PowerShell/Edit/Write/MultiEdit/NotebookEdit on Claude, `exec_command`/`shell`/`apply_patch` on Codex). Over the same span the ledger holds no row from the gate. A PostToolUse row is no proof: that hook keeps writing while a broken PreToolUse lets every call through. The same prompt carries the line "tezgah gate inactive on this host: ...". Two: a kill switch appeared after the session's first prompt; the prompt hook writes one `disarm` row naming it (`disarmed`, baseline a `switches` row of `<cache>/tezgah.db`). The mark is a `gate_inactive` row of the same database (`gate_inactive`, audit Phase 1.3) | a row from the gate appears and no switch armed mid-session is still armed |
100
+ | `drift` | never `on`: shown as `drift✗` when a session start found tezgah's own hook entries changed since install (`hooks/tezgah_attest.py::run`). One mark per session and host, a `harness_drift` row of `<cache>/tezgah.db` keyed `<session hash>.<host>`; the session's `attest` row names what drifted | the next clean start of that host in that session |
101
+ | `crash` | never `on`: shown as `crash✗` when a hook of this session could not import its core and ran without tezgah (`hooks/tezgah_guard.py::import_failed`, mark an `import_crash` row of `~/.cache/tezgah/tezgah.db` keyed `<session hash>`). When `tezgah_context` itself is the module that failed, `statusline.py` prints `tezgah ✗ core import failed` instead of the line | never by itself: the mark stays for the session |
102
102
 
103
103
  Read `on` for `consult`/`research`/`judge` as "installed and usable", not "you
104
104
  must use it": a missing key or binary reads the same red as a kill switch
@@ -147,9 +147,9 @@ have written: a mark the literal omits reads `info` there rather than a claim.
147
147
 
148
148
  ## The used-kinds channel
149
149
 
150
- A used measure is one append to the session store,
151
- `<cache_dir>/sessions/<slug(session_id)>.jsonl`, one `{"kind": …}` per line
150
+ A used measure is one row of the session store, keyed by `slug(session_id)` and kind
152
151
  (`record()`, `hooks/tezgah_context.py::record`; `used()` reads it back, `hooks/tezgah_context.py::used`).
152
+ The store is the `used` table of `<cache_dir>/tezgah.db`.
153
153
  `cache_dir()` is `~/.cache/tezgah` with a temp fallback for sandboxed hosts
154
154
  (`hooks/tezgah_paths.py::CACHE`, `hooks/tezgah_paths.py::fallback_cache`, `hooks/tezgah_paths.py::cache_dir`); a missing kind is not an event, so only
155
155
  the five kinds are ever written (`hooks/tezgah_context.py::record`).
@@ -160,8 +160,8 @@ the command really ran it (34-38,84) and is the one writer that can earn `judge`
160
160
  (a run of `bin/tezgah-triage` or `bin/tezgah-docs`, `shell_kind`, `hooks/tezgah_context.py::shell_kind`)
161
161
  — plus `hooks/projects-auto-init.py::main`
162
162
  (`orch` on subagent start), `hosts/codex/hook.py::main`,
163
- `hosts/cursor/hook.py::dispatch`, hosts/opencode/plugins/tezgah.js:2123-2130,
164
- hosts/opencode/plugins/tezgah.js:2669, `hosts/omp/hook.py::handle`. Both Claude (hooks/hooks.json) and dsh
163
+ `hosts/cursor/hook.py::dispatch`, hosts/opencode/plugins/tezgah.js:2144-2151,
164
+ hosts/opencode/plugins/tezgah.js:2702, `hosts/omp/hook.py::handle`. Both Claude (hooks/hooks.json) and dsh
165
165
  (hosts/dsh/hooks.json) wire PostToolUse to that one shared hook, which is how dsh
166
166
  gets any used mark at all: with no local transcript, the store is the only channel
167
167
  its line has (hooks/projects-posttooluse.py:33-37).
package/docs/vision.md CHANGED
@@ -22,11 +22,16 @@ the workspace's own records; every number below names where it came from.
22
22
  runs, 106 minutes, 10 of them green confirmations, in one session - the rule
23
23
  "run the full suite once, on the final tree" was already written down and
24
24
  `AGENTS.md` said "before every commit".)
25
- 3. **A review loop reads every fix.** A round that confirms a defect gets a
26
- round over the fix. The loop ends when a round confirms none
27
- (`hooks/tezgah_agents.py::_reviewer_body`). The same defect family three times means the
28
- design changes. (Measured: two of four severe defects in one internal plan came from
29
- fixes, after round two.)
25
+ 3. **One review per plan, two rounds, fanned out.** Reviewers read the plan's
26
+ whole diff once, in its verification phase. Round one spawns them together,
27
+ one per dimension. Only a confirmed critical or major finding opens round two,
28
+ and round two reads the fix's delta. After round two the user gets what is
29
+ left, or the plan lists it as fix-later. `tezgah-task review` refuses a third
30
+ round (`hooks/tezgah_agents.py::_reviewer_body`, `bin/tezgah-task::cmd_review`).
31
+ (Measured: the old loop opened a round on any finding and ended only on a
32
+ clean round. It reviewed one finished plan round after round. In another plan,
33
+ fixes brought two of four severe defects. So round two reads the fix, and a
34
+ severe finding left after it blocks.)
30
35
  4. **Doubt lowers nothing and hides nothing.** An unverifiable check never
31
36
  downgrades a finding, an unknown criterion never relaxes a cap, and a line
32
37
  whose own criteria are unmet is reported as unanswered rather than concluded.
@@ -206,15 +206,22 @@ def _reviewer_body(host):
206
206
  "is lowered only by evidence: a check you could not complete never\n"
207
207
  "downgrades a confirmed finding - keep its severity and mark it\n"
208
208
  "provisional. Disclose the order you read the files in.\n\n"
209
- "Scope and budget. Every round that confirms a defect is followed by a\n"
210
- "round over that fix's delta; the review ends when a round confirms none.\n"
211
- "A later round reads only the delta since the sha the previous round\n"
212
- "recorded (`tezgah-task review` prints it): re-reading the whole diff\n"
213
- "re-reports what was already triaged. Test results are input, not a task\n"
214
- "- read the author's output file; do not run the suite, and run at most one\n"
215
- "`-k` test when a specific accusation needs it. The same defect family\n"
216
- "confirmed three times is a sign the design needs a pivot, not another\n"
217
- "patch.\n\n"
209
+ "Scope and budget. One review per plan, over its whole diff in the\n"
210
+ "verification phase; at most two rounds - only a confirmed critical or\n"
211
+ "major finding opens round two, a minor or suggestion one is recorded\n"
212
+ "fix-later, and after round two what is left goes to the user or is\n"
213
+ "recorded fix-later, never a third round. A round may be a fan-out of\n"
214
+ "reviewers spawned at once, one per dimension (correctness and contract,\n"
215
+ "security, tests and performance) or per file group: when your brief\n"
216
+ "names a dimension or files, review only those - a sibling owns the rest.\n"
217
+ "Round two reads only the delta since the sha round one recorded\n"
218
+ "(`tezgah-task review` prints it), plus the round-one hunks and direct\n"
219
+ "callers of the symbols that delta touches, and marks every round-one\n"
220
+ "finding closed or open: re-reading the whole diff re-reports what was\n"
221
+ "already triaged. Test results are input, not a task - read the author's\n"
222
+ "output file; do not run the suite, and run at most one `-k` test when a\n"
223
+ "specific accusation needs it. The same defect family confirmed in both\n"
224
+ "rounds is a sign the design needs a pivot, not another patch.\n\n"
218
225
  "%s" % (_graph_howto(host), _blast_radius(host), EMPTY_IS_NOT_PROOF,
219
226
  _may_use(host)))
220
227
 
@@ -343,7 +350,8 @@ ROLES = (
343
350
  ("tezgah-reviewer",
344
351
  "Adversarial, read-only review of a diff, branch or PR: derives the blast "
345
352
  "radius, checks correctness, contract, security, tests and performance, and "
346
- "classifies every finding confirmed/refuted/unverified.",
353
+ "classifies every finding confirmed/refuted/unverified. One review per plan, "
354
+ "at most two rounds; spawn one per dimension in one message for a large diff.",
347
355
  lambda infra: infra["caps"]["graph"], _reviewer_body, True),
348
356
  ("tezgah-cheap",
349
357
  "Mechanical, fully specified edits on the cheapest model tier: renames, "
@@ -389,7 +397,10 @@ def _orch_body(names):
389
397
  "verify; you delegate execution and read the evidence back. Available\n"
390
398
  "specialists: %s.\n\n"
391
399
  "Fan out by default: when the request or your todo list holds two or more\n"
392
- "independent items, spawn one subagent per item in ONE message. Give\n"
400
+ "independent items, count them and spawn one subagent per item in ONE\n"
401
+ "message - at every phase: a plan's tasks, its acceptance commands and the\n"
402
+ "sources of a survey are items too, and checks that share no state run as\n"
403
+ "tool calls in one message, never a queue. Give\n"
393
404
  "every writing slice its own branch and checkout - `orca worktree create\n"
394
405
  "--name <slug> --parent-worktree active --json` when the session runs\n"
395
406
  "inside Orca, else `git worktree add -b <branch> <absolute path>` - and\n"