@r1z4x/tezgah 1.11.0 → 1.14.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 (109) hide show
  1. package/CHANGELOG.md +507 -1
  2. package/MANIFEST +40 -0
  3. package/NOTICE +152 -0
  4. package/README.de.md +41 -20
  5. package/README.es.md +39 -19
  6. package/README.fr.md +41 -20
  7. package/README.md +18 -10
  8. package/README.tr.md +39 -21
  9. package/bin/codegen +9 -2
  10. package/bin/consult +6 -0
  11. package/bin/tezgah-adhd +3 -0
  12. package/bin/tezgah-agents +3 -0
  13. package/bin/tezgah-capture +3 -0
  14. package/bin/tezgah-design +329 -54
  15. package/bin/tezgah-docs +158 -42
  16. package/bin/tezgah-dsh +1 -1
  17. package/bin/tezgah-gate +7 -2
  18. package/bin/tezgah-import-ai-research +16 -4
  19. package/bin/tezgah-index +3 -0
  20. package/bin/tezgah-lessons +13 -4
  21. package/bin/tezgah-mcp +4 -0
  22. package/bin/tezgah-migrate +3 -0
  23. package/bin/tezgah-ocr-review +2 -1
  24. package/bin/tezgah-optimize +4 -1
  25. package/bin/tezgah-pony +3 -0
  26. package/bin/tezgah-research +51 -10
  27. package/bin/tezgah-rollback +3 -0
  28. package/bin/tezgah-route +1 -1
  29. package/bin/tezgah-setup +1741 -95
  30. package/bin/tezgah-status +11 -1
  31. package/bin/tezgah-task +58 -17
  32. package/bin/tezgah.js +10 -3
  33. package/commands/adhd.md +1 -1
  34. package/docs/README.md +19 -8
  35. package/docs/architecture.md +2 -2
  36. package/docs/citations-baseline.json +11 -17
  37. package/docs/clarity-baseline.json +12 -6
  38. package/docs/contract.md +83 -34
  39. package/docs/environment.md +59 -24
  40. package/docs/evidence.md +128 -25
  41. package/docs/gate.md +77 -23
  42. package/docs/glossary.md +1 -1
  43. package/docs/hosts.md +343 -48
  44. package/docs/layers.md +2 -2
  45. package/docs/models.md +6 -0
  46. package/docs/operations.md +83 -29
  47. package/docs/research.md +37 -11
  48. package/docs/skills.md +31 -8
  49. package/docs/status-line.md +39 -23
  50. package/docs/testing.md +50 -17
  51. package/docs/vision.md +6 -2
  52. package/hooks/hooks.json +1 -1
  53. package/hooks/lang/en.json +16 -2
  54. package/hooks/lang/tr.json +14 -2
  55. package/hooks/projects-posttooluse.py +11 -3
  56. package/hooks/projects-stop.py +3 -2
  57. package/hooks/tezgah_agents.py +27 -3
  58. package/hooks/tezgah_attest.py +79 -10
  59. package/hooks/tezgah_context.py +61 -31
  60. package/hooks/tezgah_gate.py +480 -126
  61. package/hooks/tezgah_integrity.py +577 -133
  62. package/hooks/tezgah_lessons.py +5 -2
  63. package/hooks/tezgah_paths.py +60 -8
  64. package/hooks/tezgah_policy.py +48 -18
  65. package/hooks/tezgah_replay.py +8 -2
  66. package/hooks/tezgah_research.py +167 -24
  67. package/hooks/tezgah_skill_pick.py +1 -1
  68. package/hooks/tezgah_snapshot.py +6 -3
  69. package/hooks/tezgah_stack.py +236 -1
  70. package/hooks/tezgah_stopspec.py +2 -1
  71. package/hooks/tezgah_task.py +2 -1
  72. package/hosts/amp/hook.py +217 -0
  73. package/hosts/amp/tezgah-plugin.ts.in +396 -0
  74. package/hosts/antigravity/hook.py +255 -0
  75. package/hosts/codex/hook.py +3 -2
  76. package/hosts/commandcode/hook.py +239 -0
  77. package/hosts/copilot/hook.py +284 -0
  78. package/hosts/cursor/hook.py +4 -2
  79. package/hosts/droid/hook.py +259 -0
  80. package/hosts/dsh/hooks.json +1 -1
  81. package/hosts/gemini/hook.py +213 -0
  82. package/hosts/goose/hook.py +145 -0
  83. package/hosts/omp/hook.py +5 -2
  84. package/hosts/omp/tezgah-hook.ts.in +157 -42
  85. package/hosts/opencode/plugins/tezgah.js +209 -63
  86. package/hosts/qwen/hook.py +206 -0
  87. package/output-styles/tezgah.md +11 -6
  88. package/package.json +6 -3
  89. package/packaging/build.sh +4 -4
  90. package/packaging/install.sh +11 -0
  91. package/packaging/upgrade.sh +3 -0
  92. package/skills/ai-research/LICENSE +21 -0
  93. package/skills/design-contract/SKILL.md +27 -6
  94. package/skills/design-library/LICENSE +21 -0
  95. package/skills/harness/SKILL.md +2 -0
  96. package/skills/i-have-adhd/SKILL.md +10 -1
  97. package/skills/plan-add/SKILL.md +5 -1
  98. package/skills/plan-add/render_table.py +7 -0
  99. package/skills/pm-frameworks/LICENSE +21 -0
  100. package/skills/research/SKILL.md +22 -9
  101. package/skills/rl-env/LICENSE +201 -0
  102. package/skills/tezgah-contract/SKILL.md +21 -8
  103. package/statusline.py +20 -9
  104. package/third_party/open-code-review/LICENSE +201 -0
  105. package/third_party/open-code-review/README.md +15 -0
  106. package/third_party/open-code-review/prompts/main_task_system.md +26 -0
  107. package/third_party/open-code-review/prompts/review_filter_task_system.md +13 -0
  108. package/workflows/graph-map.js +14 -5
  109. package/workflows/graph-review.js +14 -2
@@ -4,7 +4,9 @@ Every environment variable tezgah reads, in one table per concern: what reads it
4
4
  what it defaults to, and what setting it changes. For someone pinning an install
5
5
  in CI, repointing an endpoint in a test, or wondering why a hook ignores the
6
6
  Python on PATH. A variable not on this page is not read by tezgah; one read by a
7
- host and only passed through (such as `HOME` or `PATH`) is left out.
7
+ host and only passed through (such as `HOME` or `PATH`) is left out. Vendored
8
+ upstream skill scripts (under `skills/`, such as hallmark's) are out of scope:
9
+ the variables they read are theirs, not tezgah's.
8
10
 
9
11
  Nothing loads a `.env` file: a variable reaches tezgah only through the
10
12
  environment of the process that started it - the shell, the host, or the host's
@@ -19,32 +21,40 @@ the kill switches are files, not variables, and live in
19
21
 
20
22
  | Variable | Read by | Default | Effect |
21
23
  |---|---|---|---|
22
- | `TEZGAH_PREFIX` | `packaging/install.sh:30`, `packaging/upgrade.sh`, `packaging/install.ps1`, `bin/tezgah-setup::default_prefix` | `$XDG_DATA_HOME/tezgah`, else `%LOCALAPPDATA%\tezgah` on Windows, else `~/.local/share/tezgah` | Where the artifact install unpacks its versions and the `current` link; `tezgah-setup` treats it as the install prefix. |
23
- | `TEZGAH_VERSION` | `packaging/install.sh`, `packaging/upgrade.sh`, `packaging/install.ps1`, `packaging/build.sh:70` | the newest GitHub release; `build.sh` falls back to `VERSION`/the CHANGELOG | The version to install, or the version `build.sh` stamps when `--version` is not given. |
24
- | `TEZGAH_REPO` | `packaging/install.sh:13`, `packaging/upgrade.sh`, `packaging/install.ps1` | `r1z4x/tezgah` | The GitHub repository releases are fetched from. |
24
+ | `TEZGAH_PREFIX` | `packaging/install.sh`, `packaging/upgrade.sh`, `packaging/install.ps1`, `bin/tezgah-setup::default_prefix`, `hooks/tezgah_gate.py::_install_trees` | `$XDG_DATA_HOME/tezgah`, else `%LOCALAPPDATA%\tezgah` on Windows, else `~/.local/share/tezgah` | Where the artifact install unpacks its versions and the `current` link; `tezgah-setup` treats it as the install prefix. |
25
+ | `TEZGAH_VERSION` | `packaging/upgrade.sh`, `packaging/install.ps1`, `packaging/build.sh:70` | the newest GitHub release; `build.sh` falls back to `VERSION`/the CHANGELOG | The version to install, or the version `build.sh` stamps when `--version` is not given. `install.sh` does not read it: the `upgrade.sh` it runs does. |
26
+ | `TEZGAH_REPO` | `packaging/install.sh:13`, `packaging/upgrade.sh`, `packaging/install.ps1`, `hooks/tezgah_update.py::REPO` | `r1z4x/tezgah` | The GitHub repository releases are fetched from, and the one the daily release check asks. |
27
+ | `TEZGAH_GITHUB_TOKEN`, `GITHUB_TOKEN`, `GH_TOKEN` | `packaging/install.sh`, `packaging/upgrade.sh`, `packaging/install.ps1` | unset | The first one set is sent as the bearer token for a private repository's release download; unset, the download is anonymous. |
25
28
  | `TEZGAH_UPDATE_URL` | `hooks/tezgah_update.py` | `https://github.com/<TEZGAH_REPO>/releases/latest` | Where the daily release check reads the newest tag: from the redirect, or from a JSON body's `tag_name`. Tests point it at a `file://` fixture. |
26
29
  | `TEZGAH_UPDATE_CHECK` | `hooks/tezgah_update.py` | unset | `0` turns off the update chip and the daily check. The test suite sets it, so no test reaches the network. |
27
- | `TEZGAH_DIST` | `packaging/upgrade.sh:105`, `packaging/install.ps1` | unset: the release URL | A directory holding `tezgah-<V>.tar.gz` and its `.sha256` (what `build.sh --out` writes) used instead of the release download; CI exercises the real fetch-verify-unpack path with it. |
30
+ | `TEZGAH_DIST` | `packaging/upgrade.sh:109`, `packaging/install.ps1` | unset: the release URL | A directory holding `tezgah-<V>.tar.gz` and its `.sha256` (what `build.sh --out` writes) used instead of the release download; CI exercises the real fetch-verify-unpack path with it. |
28
31
  | `TEZGAH_GH` | `packaging/upgrade.sh` | `gh` on PATH | The `gh` that checks the tarball's build provenance (`gh attestation verify`). A path that does not exist reads as no `gh`: the upgrade prints `provenance not checked` and goes on. Tests point it at a stand-in. |
29
32
  | `TEZGAH_NO_DEPS` | `bin/tezgah-setup::main` | unset | Any value but empty or `0` is `--no-deps`: the optional tools (codegraph, orx, host CLIs) are not installed. |
30
33
  | `TEZGAH_NO_SYMLINK` | `bin/tezgah-setup::symlinks_refused` | unset | Any value but empty or `0` writes the copy layout a machine without symlinks gets, on a machine that has them. |
31
- | `TEZGAH_PYTHON` | `hooks/tezgah_paths.py::python_cmd`, `bin/tezgah.js`, `bin/tezgah-dsh.cmd`, `hooks/hooks.json`, `hosts/*/hooks.json`, `hosts/omp/tezgah-hook.ts.in`, `hosts/opencode/plugins/tezgah.js`, `packaging/build.sh` | this process's interpreter, then `python3`/`python`/`py` on PATH | The interpreter every host hook and the npm shim start; pin it when the right Python is not first on PATH. |
34
+ | `TEZGAH_PYTHON` | `hooks/tezgah_paths.py::python_cmd`, `bin/tezgah.js`, `bin/tezgah-dsh.cmd`, `hooks/hooks.json`, `hosts/cursor/hooks.json`, `hosts/dsh/hooks.json`, `hosts/omp/tezgah-hook.ts.in`, `hosts/amp/tezgah-plugin.ts.in`, `hosts/opencode/plugins/tezgah.js`, `packaging/build.sh` | this process's interpreter, then `python3`/`python`/`py` on PATH | The interpreter every host hook and the npm shim start; pin it when the right Python is not first on PATH. Codex's hook command is rendered at install (`bin/tezgah-setup::codex_hook_groups`) rather than read from a shipped file. |
32
35
 
33
36
  ## Where tezgah and the hosts keep state
34
37
 
35
38
  | Variable | Read by | Default | Effect |
36
39
  |---|---|---|---|
37
40
  | `TEZGAH_ROOTS` | `hooks/tezgah_paths.py::roots`, `bin/tezgah-setup`, `hosts/opencode/plugins/tezgah.js` | `roots` in `~/.config/tezgah/config.json`, else `~/Projects` | The directories tezgah is armed over, `os.pathsep`-separated (`;` on Windows, the opencode plugin included); it outranks the config file. |
38
- | `XDG_CONFIG_HOME` | `hooks/tezgah_paths.py::CONFIG_DIR`, `bin/tezgah-doctor`, `bin/tezgah-dsh.cmd`, `hosts/opencode/plugins/tezgah.js` | `~/.config` | Parent of tezgah's config dir (kill switches, `bin/` links, config) and of opencode's. |
39
- | `XDG_DATA_HOME` | `bin/tezgah-setup::default_prefix`, `packaging/install.sh`, `packaging/upgrade.sh` | `~/.local/share` | Parent of the default install prefix. |
41
+ | `XDG_CONFIG_HOME` | `hooks/tezgah_paths.py::CONFIG_DIR`, `hooks/tezgah_paths.py::AMP_PLUGIN`, `hooks/tezgah_skill_pick.py::_host_roots`, `bin/tezgah-doctor`, `bin/tezgah-dsh.cmd`, `hosts/opencode/plugins/tezgah.js` | `~/.config` | Parent of tezgah's config dir (kill switches, `bin/` links, config), of opencode's, and of Amp's system plugin dir (`amp/plugins/tezgah.ts`). |
42
+ | `XDG_DATA_HOME` | `bin/tezgah-setup::default_prefix`, `packaging/install.sh`, `packaging/upgrade.sh`, `hooks/tezgah_gate.py::_install_trees` | `~/.local/share` | Parent of the default install prefix. |
40
43
  | `XDG_CACHE_HOME` | `hooks/tezgah_apps.py::ARTIFACTS` | `~/.cache` | Parent of the app-analysis artifact dir. |
41
- | `TEZGAH_DEBUG` | `hooks/tezgah_guard.py` | unset | `1` makes every hook process append one line to `<cache>/debug.log` (host, script, each guarded call's outcome or exception class, elapsed ms; no prompt or tool text), created 0600 and swept by `tezgah-doctor --clean`. Off, it costs one environment read. |
42
44
  | `TEZGAH_FALLBACK_CACHE` | `hooks/tezgah_paths.py::fallback_cache` | `<tempdir>/tezgah` | The cache a sandboxed hook writes when its normal cache dir is denied; a test knob. |
43
45
  | `TEZGAH_ARTIFACTS` | `hooks/tezgah_apps.py::ARTIFACTS` | `$XDG_CACHE_HOME/tezgah/apps` | Where `analyze-app` screenshots, traces and tree dumps land. |
44
46
  | `TEZGAH_NO_EXCLUDE` | `hooks/tezgah_agents.py::ensure_exclude` | unset | `1` stops tezgah adding its generated-agent dirs to the repository's `.git/info/exclude`. |
45
- | `CODEX_HOME` | `hooks/tezgah_paths.py::HOST_DIRS` | `~/.codex` | The Codex home tezgah installs into and checks; the same variable Codex itself reads. |
47
+ | `CODEX_HOME` | `hooks/tezgah_paths.py::HOST_DIRS`, `hooks/tezgah_skill_pick.py::_host_roots` | `~/.codex` | The Codex home tezgah installs into and checks; the same variable Codex itself reads. |
46
48
  | `DSH_HOME` | `hooks/tezgah_paths.py::HOST_DIRS`, `bin/tezgah-dsh`, `bin/tezgah-dsh.cmd` | `~/.dsh` | The dsh home tezgah installs into, and where `tezgah-dsh` looks for the CLI. |
49
+ | `QWEN_HOME`, `COPILOT_HOME` | `hooks/tezgah_paths.py::HOST_DIRS` | `~/.qwen`, `~/.copilot` | The Qwen Code and Copilot CLI homes tezgah installs into and checks; the same variables those CLIs read to relocate their home. |
50
+ | `GEMINI_CLI_HOME` | `hooks/tezgah_paths.py::HOST_DIRS` | the home dir | The directory holding Gemini CLI's `.gemini`, where tezgah installs its extension; the variable Gemini CLI itself reads. |
51
+ | `CLAUDE_CONFIG_DIR` | `hooks/tezgah_stack.py::host_stack` | unset | Set (Claude Code's own relocation variable), omp's skill stack counts `~/.claude/skills` as loaded even when omp's `enabledProviders` does not list `claude`. The test suite unsets it. |
47
52
  | `ORCA_WORKTREE_ID`, `ORCA_TERMINAL_HANDLE`, `ORCA_PANE_KEY`, `TERM_PROGRAM=Orca` | `hooks/tezgah_orca.py::session` | unset | Exported by an Orca terminal. Any one marks the session as inside Orca: the session-start block gains the `Orca:` line and `tezgah-setup --status` an `orca:` line ([orca](orca.md)). tezgah never writes them. |
53
+ | `ORCA_APP_VERSION` | `hooks/tezgah_orca.py::session` | unset | Exported by an Orca terminal: the Orca version the `Orca:` line reports. |
54
+ | `ORCA_CLI_COMMAND`, `ORCA_CLI_BIN_DIR` | `hooks/tezgah_orca.py::cli` | unset | Exported by Orca: the command (WSL) or the bin dir of its CLI, tried after `TEZGAH_ORCA_BIN` and before PATH. The test suite unsets both. |
55
+ | `TEZGAH_ENGAGEMENTS_ROOT` | `hooks/tezgah_extension.py::ENGAGEMENTS_ENV` | unset | A domain repository's engagement root: a session anywhere under `<root>/<slug>` reads that engagement's `.tezgah/extension.json` even when the walk up ran out first. |
56
+ | `GIT_DIR`, `GIT_WORK_TREE` | `hooks/tezgah_agents.py::_exclude_path` | unset | Either one set moves the repository away from `.git`, so the generated-agent excludes are written where git says, not to `.git/info/exclude`. |
57
+ | `LANGUAGE`, `LC_ALL`, `LC_MESSAGES`, `LANG` | `hooks/tezgah_paths.py::env_language` | unset | A first install takes the reply language from the first one set and not empty (`LANGUAGE`'s first `:` entry); `en` when it names no shipped pack. |
48
58
  | `TEZGAH_OPENCODE_DATA` | `bin/tezgah-setup::OPENCODE_DATA`, `bin/tezgah-doctor` | `~/.local/share/opencode` | opencode's data dir: its session database for `tezgah-doctor` and the installer. |
49
59
 
50
60
  ## Which binary runs
@@ -54,9 +64,11 @@ the kill switches are files, not variables, and live in
54
64
  | `TEZGAH_CODEGRAPH_BIN` | `hooks/tezgah_paths.py::codegraph_bin` | the config file, then `codegraph` on PATH | The codegraph executable the index and the graph marks use. |
55
65
  | `TEZGAH_ORX_BIN` | `hooks/tezgah_paths.py::orx_bin` | `orx` on PATH | The OpenResearch CLI the research rule routes to. |
56
66
  | `TEZGAH_OCR_BIN` | `hooks/tezgah_paths.py::ocr_bin` | `ocr` on PATH | The open-code-review CLI the opt-in OCR review spec runs ([operations](operations.md#opt-in-ocr-review-spec-ocr)). |
67
+ | `TEZGAH_OCR_REVIEW_READY_TIMEOUT` | `bin/tezgah-ocr-review::run` | `30` (seconds) | How long `tezgah-ocr-review` waits for its model gateway to answer before it reports a setup failure. Must be a number. |
68
+ | `GIT_CONFIG_GLOBAL`, `GIT_CONFIG_COUNT` | `bin/tezgah-ocr-review::run` | unset | OCR runs with a private HOME, so `tezgah-ocr-review` pins git to the user's `~/.gitconfig` and global excludes: an explicit `GIT_CONFIG_GLOBAL` wins over that pin, and the excludes entry is appended after the `GIT_CONFIG_COUNT` entries already set. |
57
69
  | `TEZGAH_ORCA_BIN` | `hooks/tezgah_orca.py::cli` | `ORCA_CLI_COMMAND`, then `$ORCA_CLI_BIN_DIR/orca`, then `orca` on PATH (`orca-ide` on Linux outside Orca), then the macOS app's CLI | The Orca CLI `tezgah-status --orca` and `tezgah-setup --status` ask. A missing path is no CLI, never a fallback: the test suite points it at one. |
58
70
  | `TEZGAH_OMP_BIN` | `hooks/tezgah_paths.py::omp_bin` | `omp` on PATH | The omp binary the installer, the model table, consult's session-model lookup and the judgement seam's session call run; tests point it at a stand-in. |
59
- | `TEZGAH_CLAUDE_BIN` | `claude_bin`, `hooks/tezgah_paths.py::claude_bin` | `claude` on PATH | The Claude Code CLI `--install` registers the plugin with (`claude plugin`) and the judgement seam asks in a Claude Code session; the test suite points it at a path that does not exist, or at a stand-in. |
71
+ | `TEZGAH_CLAUDE_BIN` | `hooks/tezgah_paths.py::claude_bin` | `claude` on PATH | The Claude Code CLI `--install` registers the plugin with (`claude plugin`) and the judgement seam asks in a Claude Code session; the test suite points it at a path that does not exist, or at a stand-in. |
60
72
  | `TEZGAH_OPENCODE_BIN`, `TEZGAH_CURSOR_BIN`, `TEZGAH_CODEX_BIN` | `hooks/tezgah_paths.py::opencode_bin`, `hooks/tezgah_paths.py::cursor_bin`, `hooks/tezgah_paths.py::codex_bin` | `opencode`, `cursor-agent`, `codex` on PATH or a per-user bin dir | The CLI the judgement seam asks in that host's session; tests point them at stand-ins. |
61
73
  | `TEZGAH_DSH_BIN` | `bin/tezgah-setup::reconcile_json_servers` | the profile-local entry, then `bin/tezgah-dsh`, then npx | How the installer invokes the dsh CLI. |
62
74
  | `TEZGAH_INDEX_BIN` | `bin/tezgah-dsh:19`, `bin/tezgah-dsh.cmd` | `~/.config/tezgah/bin/tezgah-index` | The index worker `tezgah-dsh` warms before it starts dsh. |
@@ -75,11 +87,18 @@ the kill switches are files, not variables, and live in
75
87
 
76
88
  | Variable | Read by | Default | Effect |
77
89
  |---|---|---|---|
78
- | `OMPCODE`, `CLAUDECODE`, `OPENCODE`, `CURSOR_AGENT`, `CURSOR_VERSION`, `CODEX_THREAD_ID` | `hooks/tezgah_paths.py::session_cli` (all), `bin/consult`, `hooks/tezgah_models.py::worker_model` (the first two) | set by the host | Name the host session: the judgement seam asks that host's CLI first, in the order listed (omp sets both of the first two, so `OMPCODE` wins; Cursor sets `CURSOR_AGENT` in its shell tool and `CURSOR_VERSION` in its hooks; Codex sets `CODEX_THREAD_ID` in its shell tool only). The test suite unsets all of them. |
90
+ | `OMPCODE`, `CLAUDECODE`, `OPENCODE`, `CURSOR_AGENT`, `CURSOR_VERSION`, `CODEX_THREAD_ID` | `hooks/tezgah_paths.py::session_cli` (all), `bin/consult`, `hooks/tezgah_models.py::worker_model` (the first two), `bin/tezgah-task::print_ocr_spec` (the first) | set by the host | Name the host session: the judgement seam asks that host's CLI first, in the order listed (omp sets both of the first two, so `OMPCODE` wins; Cursor sets `CURSOR_AGENT` in its shell tool and `CURSOR_VERSION` in its hooks; Codex sets `CODEX_THREAD_ID` in its shell tool only). The test suite unsets all of them. |
79
91
  | `TYPESAFE_API_KEY` | `hooks/tezgah_judge.py::key`, `hooks/tezgah_paths.py` | `~/.config/typesafe/key` | The judgement seam's first third-party credential, used as `fallback` in `config.json` allows ([judge](judge.md)); omp also reads it for its own judge. |
80
- | `OPENROUTER_API_KEY` | `hooks/tezgah_judge.py::openrouter_key`, `bin/consult`, `bin/codegen`, `hooks/tezgah_models.py` | `~/.config/openrouter/key` | The seam's chat fallback, consult's and codegen's `openrouter` provider, and omp's `any` family. |
92
+ | `OPENROUTER_API_KEY` | `hooks/tezgah_judge.py::openrouter_key`, `bin/consult`, `bin/codegen`, `hooks/tezgah_models.py`, `hooks/tezgah_research_evidence.py::cmd_label` | `~/.config/openrouter/key` | The seam's chat fallback, consult's and codegen's `openrouter` provider, omp's `any` family, and the `--provider openrouter` rater of `tezgah-research label`. |
81
93
  | `DEEPSEEK_API_KEY`, `INCEPTION_API_KEY` | `bin/consult`, `bin/codegen`, `hooks/tezgah_replay.py::label_model` | `~/.config/deepseek/key`, `~/.config/inception/key` | The `deepseek` and `inception` providers of consult and codegen; `DEEPSEEK_API_KEY` alone is also the replay sheet's `--provider deepseek` rater. |
82
- | `ANTHROPIC_OAUTH_TOKEN`, `ANTHROPIC_API_KEY`, `ZAI_API_KEY`, `OPENAI_CODEX_OAUTH_TOKEN` | `hooks/tezgah_models.py::OMP_AUTH` | omp's own auth store | Read as presence only: whether omp can run the `anthropic`, `zai` or `openai` model family, so the model table can pick a row omp will run. |
94
+ | `ANTHROPIC_OAUTH_TOKEN`, `ANTHROPIC_API_KEY`, `ZAI_API_KEY`, `OPENAI_CODEX_OAUTH_TOKEN`, `EXPERIMENTALLABS_API_KEY` | `hooks/tezgah_models.py::OMP_AUTH` | omp's own auth store | Read as presence only: whether omp can run the `anthropic`, `zai`, `openai` or `experientiallabs` model family, so the model table can pick a row omp will run. |
95
+ | `TEZGAH_HEALTH_CHECK` | `hooks/tezgah_models.py::health_due`, `hooks/tezgah_models.py::note_limit`, `hooks/tezgah_models.py::other_family_up` | unset | `0` turns off the provider health probe and the limit notes that read credentials; the test suite sets it, so no test asks a model or reads this machine's keys. |
96
+ | `TEZGAH_JEV_PROVIDER`, `JEV_PROVIDER` | `hooks/tezgah_paths.py::jev_choice` | `jev` in `config.json`, else `auto` | The Jev carrier the judgement seam asks (`JEV_PROVIDER` is jev-mcp's own name); `auto` asks every carrier whose credential resolves, an unknown name resolves to none ([judge](judge.md)). |
97
+ | `JEV_API_BASE_URL`, `JEV_API_KEY` | `hooks/tezgah_paths.py::jev_carriers`, `hooks/tezgah_judge.py::_jev_call` | unset | The `jev-compatible` carrier: any System One-compatible endpoint and its key. Plain `http` is refused unless the host is this machine. |
98
+ | `TEZGAH_JEV_MODEL` | `hooks/tezgah_judge.py::_jev_call` | the seam's model | The model the `jev-compatible` carrier asks. |
99
+ | `CLOUDFLARE_ACCOUNT_ID` | `hooks/tezgah_paths.py::cloudflare_account` | `~/.config/cloudflare/account_id` | The Workers AI account the `jev-cloudflare` and `clef` carriers call. |
100
+ | `JEV_CLOUDFLARE_API_TOKEN`, `CLOUDFLARE_API_TOKEN` | `hooks/tezgah_paths.py::cloudflare_token` | `~/.config/cloudflare/token` | The Workers AI token, the first one set. |
101
+ | `TEZGAH_CLOUDFLARE_URL`, `TEZGAH_JEV_OPENROUTER_URL` | `hooks/tezgah_judge.py::_jev_call` | Cloudflare's API base, OpenRouter's System One URL | Repoint the Workers AI and `jev-openrouter` carriers (tests). Plain `http` is refused unless the host is this machine. |
83
102
  | `TEZGAH_TYPESAFE_URL` | `hooks/tezgah_judge.py::endpoint` | `https://api.typesafe.ai/v1/systemone` | Repoints the seam's TypeSafe endpoint (tests). Plain `http` is refused unless the host is this machine; a cross-host redirect is refused. |
84
103
  | `TEZGAH_OPENROUTER_URL` | `hooks/tezgah_judge.py::openrouter_url` | OpenRouter's chat-completions URL | Repoints the seam's chat fallback (tests). Plain `http` is refused unless the host is this machine. |
85
104
  | `TEZGAH_DEEPSEEK_URL` | `hooks/tezgah_replay.py::_deepseek` | `https://api.deepseek.com/chat/completions` | Repoints the replay sheet's `--provider deepseek` rater (tests). Plain `http` is refused unless the host is this machine. |
@@ -99,6 +118,13 @@ the kill switches are files, not variables, and live in
99
118
  | `CODEGEN_URL` | `bin/codegen::ask` | the provider's URL | Repoints codegen's endpoint. Plain `http` is refused unless the host is this machine (`127.0.0.1`, `localhost`); a redirect to another host is refused. |
100
119
  | `CODEGEN_MODEL` | `bin/codegen::MODEL` | the cheap `any` row of the models table | The model codegen drafts with. |
101
120
 
121
+ ## Research capture
122
+
123
+ | Variable | Read by | Default | Effect |
124
+ |---|---|---|---|
125
+ | `TEZGAH_SCOPUS_KEY` | `hooks/tezgah_research_capture.py::cmd_search` | unset | The Elsevier API key the `scopus` search connector sends; without it that connector refuses. |
126
+ | `TEZGAH_CAPTURE_MAP` | `hooks/tezgah_research_capture.py::fetch_url` | unset | A JSON map that answers the capture commands' fetches instead of the network; tests set it, and `hooks/tezgah_research_evidence.py::cmd_release_check` sets it to its fixtures' `capture-map.json`. |
127
+
102
128
  ## Status line
103
129
 
104
130
  | Variable | Read by | Default | Effect |
@@ -106,8 +132,9 @@ the kill switches are files, not variables, and live in
106
132
  | `NO_COLOR` | `hooks/tezgah_context.py::color_default`, `hosts/omp/tezgah-hook.ts.in` | unset | Any value renders the status line without ANSI colour. |
107
133
  | `TEZGAH_STATUS_COLOR` | `hooks/tezgah_context.py::color_default`, `hosts/omp/tezgah-hook.ts.in` | unset | `0` is the same opt-out as `NO_COLOR`, for tezgah alone. |
108
134
  | `TEZGAH_STATUS_ANIMATE` | `hosts/omp/tezgah-hook.ts.in:65` | unset (animated) | `0` keeps omp's status colours and drops the motion. |
109
- | `TEZGAH_STATUS_HOST` | `statusline.py::HOST` | `claude` | `cursor` renders for Cursor (the same as `--cursor`). |
110
- | `TEZGAH_STATUS_LEGEND` | `statusline.py:127` | unset | `1` prints the mark legend under the line. |
135
+ | `TEZGAH_STATUS_HOST` | `statusline.py::HOST` | `claude` | `cursor`, `qwen`, `droid`, `copilot` or `antigravity` renders for that host (the same as `--cursor` and the like). |
136
+ | `TEZGAH_STATUS_LEGEND` | `statusline.py:138` | unset | `1` prints the mark legend under the line. |
137
+ | `COLORFGBG` | `hooks/tezgah_context.py::light_background` | unset (dark) | The terminal's reported colours: a light background picks the light palette. |
111
138
 
112
139
  ## Set by tezgah or the host, not by a user
113
140
 
@@ -119,15 +146,16 @@ the kill switches are files, not variables, and live in
119
146
  | `TEZGAH_CORE_IN_FILE` | `hooks/projects-auto-init.py::core_is_in_a_file`, `hooks/hooks.json` | unset | `1` (set by the Claude plugin's hook rows) drops the core rules from the injection when the host's static file really carries the tezgah block; a file without it falls back to the hook. |
120
147
  | `TEZGAH_HOST` | `hooks/tezgah_context.py::context_for`, `hooks/hooks.json` | unset | `claude` (set by the Claude plugin's UserPromptSubmit row; the payload names no host) makes the skill hint name the Skill tool's `tezgah:<name>` beside the path instead of a `skill://` address (`hooks/tezgah_skill_pick.py::_emit`). A host that sends `host` in the payload (omp, opencode) is not read from here. |
121
148
  | `TEZGAH_NESTED` | `hooks/tezgah_integrity.py::_shape_block`, `bin/consult` | unset | Set by consult on the agent CLIs it runs: that session's Stop rule does not fire. |
122
- | `TEZGAH_CALL_OUTCOME` | `hooks/projects-posttooluse.py::main`, `hosts/dsh/hooks.json` | unset | `none` (dsh's Task rows) records the call's outcome as unknown instead of as a pass. |
149
+ | `TEZGAH_CALL_OUTCOME` | `hooks/projects-posttooluse.py::main`, `hooks/tezgah_integrity.py::_hook_host`, `hosts/dsh/hooks.json` | unset | `none` (dsh's Task rows) records the call's outcome as unknown instead of as a pass. |
123
150
  | `CLAUDE_PROJECT_DIR` | `hooks/projects-auto-init.py::main` | the process cwd | The project dir when the hook payload carries no `cwd`. |
124
- | `PLAYWRIGHT_BROWSERS_PATH`, `LOCALAPPDATA` | `hooks/tezgah_apps.py::_browser_caches`, `bin/tezgah-setup::default_prefix` (`LOCALAPPDATA` only) | Playwright's own cache dirs | Where `analyze-app` looks for an installed browser before it reports one missing; on Windows `LOCALAPPDATA` is also the parent of the default install prefix. |
151
+ | `PLAYWRIGHT_BROWSERS_PATH`, `LOCALAPPDATA` | `hooks/tezgah_apps.py::_browser_caches`, `bin/tezgah-setup::default_prefix` and `packaging/install.ps1` (LOCALAPPDATA only) | Playwright's own cache dirs | Where `analyze-app` looks for an installed browser before it reports one missing; on Windows `LOCALAPPDATA` is also the parent of the default install prefix. |
152
+ | `AMP_IGNORE_GUIDANCE_FILES` | `hosts/amp/tezgah-plugin.ts.in` | set by the user for Amp | Amp's own list of guidance files to skip; when it is set the plugin asks for the session brief with the core in it, since `~/.config/amp/AGENTS.md` may be skipped. |
125
153
 
126
154
  ## Test knob
127
155
 
128
156
  | Variable | Read by | Default | Effect |
129
157
  |---|---|---|---|
130
- | `TEZGAH_E2E_STRICT` | `tests/e2e_*.py`, `.github/workflows/ci.yml` | unset (a missing prerequisite is a SKIP) | `1` turns an end-to-end script's SKIP into a FAIL; CI sets it. |
158
+ | `TEZGAH_E2E_STRICT` | `tests/e2e_analyze_wiring.py`, `tests/e2e_docker_cycle.py`, `tests/e2e_packaged_install.py`, `tests/e2e_plan_flow.py`, `tests/e2e_tezgah_mcp.py`, `tests/neuter_matrix.py`, `.github/workflows/ci.yml`, `.github/workflows/neuter.yml` | unset (a missing prerequisite is a SKIP) | `1` turns a SKIP into a FAIL in the six scripts listed. CI sets it on `e2e_plan_flow`, `e2e_analyze_wiring` and `e2e_packaged_install` (ci.yml) and on `neuter_matrix` (neuter.yml). `e2e_docker_cycle` and `e2e_tezgah_mcp` read it but are opt-in: set it by hand to make their missing prerequisite a failure. The other end-to-end scripts (`e2e_qwen`, `e2e_droid`, `e2e_omp_statusline`, `e2e_dsh_statusline`, `e2e_analyze_web`, `e2e_analyze_mobile`) ignore it: they need a host CLI or paid model calls a CI run does not have, and still SKIP. |
131
159
  | `TEZGAH_OPENCODE_NO_SQLITE` | `hosts/opencode/plugins/tezgah.js` | unset (node:sqlite when the runtime has it) | `1` makes the opencode plugin read and write the evidence ledger through `python3 hooks/tezgah_store.py evidence ...`, the path a runtime without node:sqlite takes; a test sets it. |
132
160
 
133
161
  ## Debugging
@@ -135,17 +163,24 @@ the kill switches are files, not variables, and live in
135
163
  | Variable | Read by | Default | Effect |
136
164
  |---|---|---|---|
137
165
  | `TEZGAH_DEBUG` | `hooks/tezgah_guard.py` | unset (off) | Any value but empty or `0` makes every hook process append one line to `<cache>/debug.log` (created 0600): `<epoch> host=<codex\|cursor\|omp\|claude/dsh> script=<entry script> calls=<core fn>:<answer\|none\|ExceptionClass>,... ms=<elapsed>`. `answer` means the core returned something (a deny reason, a context block, a block decision). No prompt or tool text is written. Off, it costs one environment read. `tezgah-doctor --clean` removes the file once it has gone untouched for `--retention-days` (default 30). |
166
+ | `TEZGAH_REPO_RULE` | `hooks/tezgah_gate.py::REPO_RULE_VALVE`, `hosts/opencode/plugins/tezgah.js` | unset | `0` skips a repository's own `.tezgah/gate.py` rule ([gate](gate.md#a-repositorys-own-rule)): the off valve for a refusal the repository, not tezgah, wrote. |
138
167
 
139
168
  ## Source of truth
140
169
 
141
170
  `hooks/tezgah_paths.py`, `hooks/tezgah_judge.py`, `hooks/tezgah_models.py`,
142
171
  `hooks/tezgah_index.py`, `hooks/tezgah_apps.py`, `hooks/tezgah_agents.py`,
143
172
  `hooks/tezgah_context.py`, `hooks/tezgah_integrity.py`,
173
+ `hooks/tezgah_orca.py`, `hooks/tezgah_extension.py`, `hooks/tezgah_stack.py`,
174
+ `hooks/tezgah_update.py`, `hooks/tezgah_guard.py`, `hooks/tezgah_gate.py`,
175
+ `hooks/tezgah_skill_pick.py`, `hooks/tezgah_research_capture.py`,
176
+ `hooks/tezgah_research_evidence.py`,
144
177
  `hooks/projects-auto-init.py`, `hooks/projects-posttooluse.py`, `statusline.py`,
145
178
  `bin/tezgah-setup`, `bin/consult`, `bin/codegen`, `bin/tezgah-status`,
146
- `bin/tezgah-doctor`, `bin/tezgah-dsh`, `bin/tezgah.js`, `packaging/install.sh`,
179
+ `bin/tezgah-doctor`, `bin/tezgah-dsh`, `bin/tezgah-ocr-review`, `bin/tezgah-task`,
180
+ `bin/tezgah.js`, `packaging/install.sh`,
147
181
  `packaging/upgrade.sh`, `packaging/install.ps1`, `packaging/build.sh`,
148
- `hosts/omp/tezgah-hook.ts.in`, `hosts/opencode/plugins/tezgah.js`,
149
- `hosts/dsh/statusline/lib/index.js`. The list was taken by searching the shipped
150
- code for `os.environ`, `getenv`, `process.env`, `$env:` and `${...}` reads; re-run
151
- that search after adding a variable and add its row here.
182
+ `hosts/omp/tezgah-hook.ts.in`, `hosts/amp/tezgah-plugin.ts.in`,
183
+ `hosts/opencode/plugins/tezgah.js`, `hosts/dsh/statusline/lib/index.js`. The
184
+ list covers the shipped code's `os.environ`, `getenv`, `process.env`, `$env:` and
185
+ `${...}` reads. `tests/test_docs.py::ContractAndEnvironment` re-runs that search
186
+ and fails on a name with no row here.
package/docs/evidence.md CHANGED
@@ -11,8 +11,12 @@ One append-only ledger per session, stored as the `evidence` rows of `<cache>/te
11
11
  (`hooks/tezgah_store.py::EVIDENCE_SCHEMA`). A row keeps the JSON text its writer produced, with the
12
12
  session, `kind` and `ts` in columns beside it. A caller names a ledger by its old file path,
13
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()`
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
+ session id, which nothing can turn back into the id (`hooks/tezgah_integrity.py::_slug`). Every Python writer but one goes through `note_path` (`hooks/tezgah_integrity.py::note_path`): `note()`
15
+ derives the path from the session id (`hooks/tezgah_integrity.py::note`). The exception is the pre-write snapshot writer
16
+ (`hooks/tezgah_snapshot.py::_append`). Its `snapshot` and `rollback` rows keep a path longer than `DETAIL_MAX` and a `forced`
17
+ key, which `note_path` would cut and drop. So they go straight to the same `_append` and the same ledger. They carry the same
18
+ `v`, but their `detail` is neither redacted nor cut. The opencode plugin builds its rows in JavaScript and inserts them itself
19
+ (`appendRow` in `hosts/opencode/plugins/tezgah.js`), with no `v`. Every other row is built in one place (`hooks/tezgah_integrity.py::note_path`). Its shape is
16
20
  `{"kind": …, "ts": …, "v": …, "detail": …}` plus whatever `LEDGER_FIELDS` keys the writer knew — for a check
17
21
  the host reported passing, `{"kind": "verify_ok", "ts": 1758000000, "v": 3, "detail": "pytest -q",
18
22
  "id": "a1b2c3d4e5f6", "exit": 0, "out_bytes": 4312, "tool": "Bash", "repo": "/home/u/proj"}`. `v` names the shape the row was written under
@@ -33,11 +37,21 @@ JSON key (`"password": "x"`) and as a flag with its value after a space (`--pass
33
37
  `--api-key X`, `mysql -pPASS`) - an `Authorization` scheme (`Bearer`, `Basic`, `Token`, `Digest`),
34
38
  the password of URL userinfo (`scheme://user:pass@`) and of `-u user:pass`, and a prefixed token
35
39
  family lose theirs (`hooks/tezgah_integrity.py::REDACTIONS`; audit L-6, which found each of those stored verbatim). The optional fields are exactly `LEDGER_FIELDS`
36
- (`hooks/tezgah_integrity.py::LEDGER_FIELDS`) — `id`, `exit`, `out_bytes`, `fail_class`, `workspace`, `source`, `hash`, `changed`, `tool`,
37
- `target`, plus the reply-shape names `lines`, `chars`, `items`, `longest_list`, `tr_share`, `answer_first` and the compaction
38
- row's `summary_chars`, `summary_hash`, `constraint_found`, `constraint_expected`, the `attest`
39
- row's `host` and `switches`, a `claim` row's `harness` and a Stop refusal row's `reask` (the key of its own text, which the `turn` row of omp's continuation carries, `hooks/tezgah_integrity.py::_turn_refusals`) — and
40
- a key outside that set is dropped, a `None` value left out, because every reader treats a missing key
40
+ (`hooks/tezgah_integrity.py::LEDGER_FIELDS`):
41
+
42
+ - a call's `id`, `exit`, `out_bytes`, `fail_class`, `workspace`, `source`, `hash`, `changed`, `tool`, `target`, `repo`,
43
+ `empty_run`, and the began row's `check`
44
+ - the reply-shape names `lines`, `chars`, `items`, `longest_list`, `tr_share`, `answer_first`, and a read-only turn's `mode`
45
+ - the compaction row's `summary_chars`, `summary_hash`, `constraint_found` and `constraint_expected`
46
+ - the attest row's `host` and `switches`, a claim row's `harness`, and a refusal's `cause`
47
+ - a Stop refusal row's `reask`: the key of its own text, which the turn row of omp's continuation carries
48
+ (`hooks/tezgah_integrity.py::_turn_refusals`)
49
+ - the lineage names `parent`, `child` and `agent`, on the spawned, forked, spawn, subagent_start and subagent_end rows
50
+ - a lesson row's `key` and `block`, the authorized row's `authorized`, and a turn row's `inflight`
51
+ - a todo row's `open`, `ready`, `blocked`, `heads`, `taken` and `partial`
52
+ - a subagent_end row's `residual`, and an ask row's `questions` and `defaults`
53
+
54
+ Any key outside that set is dropped, a `None` value left out, because every reader treats a missing key
41
55
  as `None` (`hooks/tezgah_integrity.py::note_path`). `tool` is the call's own name (`Bash`, `Write`, `mcp__codegen__status`),
42
56
  written by `note_tool` (`hooks/tezgah_integrity.py::note_tool`) because `classify` folds the name into
43
57
  a kind and drops it; it is what `_counts`' tool histogram counts and what the retirement report on
@@ -121,14 +135,15 @@ a write tool is `edit`, a shell call is `verify` when its command matches the ch
121
135
  | `wait` | `hooks/tezgah_integrity.py::note_tool`, for omp's `wait` | `hooks/tezgah_gate.py::ready_advice`, which counts it as idle; nothing counts it as work |
122
136
  | `ask` | `hooks/tezgah_gate.py::ask_advice`, for an omp `ask` or Claude AskUserQuestion call: `detail` holds the questions, cut; `questions` counts them and `defaults` the ones with a recommended option | nothing refuses on it; the row is the record of what the session asked |
123
137
  | `grant` | `hooks/tezgah_context.py::context_for`, from each prompt's `user_grants` (`hooks/tezgah_context.py::user_grants`): `detail` holds the user's clause, `target` the outward class it covers (`push`, `release`, `publish`, `pr` or `all`), or `lift:<class>` when the prompt took it back | `hooks/tezgah_integrity.py::grants_in_force`, read by the `needless ask` Stop class and the ask-tool warning |
124
- | `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:2602-2618`) | `hooks/tezgah_integrity.py::unanswered`, behind a claim in the Stop rule and in `counters.unanswered` |
138
+ | `began` | the gate's allow path for a write or shell call `hooks/tezgah_gate.py::decision`; on opencode the plugin's before-hook too, ahead of its own refusals and on its allow path, once per call: a call the core already wrote a row for keeps that one (`hosts/opencode/plugins/tezgah.js::noteBegan`) | `hooks/tezgah_integrity.py::unanswered`, behind a claim in the Stop rule and in `counters.unanswered` |
125
139
  | `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` |
126
140
  | `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 |
127
141
  | `after_block` | `hooks/tezgah_integrity.py::stop_reason`, on a reply after this rule's own block (a turn holding a `blocked:` claim or refusal row) that is not refused again (`_record_only`) on Claude, Codex, Cursor 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 |
128
- | `subagent_end` | `stop_reason(..., subagent=True)`, from Claude's SubagentStop (`hooks/hooks.json`) and Cursor's subagentStop `summary`; the evidence half only, never a block (ADR 011); `detail` as `after_block`'s | nothing yet: the rows plan 055 replays before a subagent end may block; `NOT_TOOL_HOOK` lists it. On Claude a subagent's tool rows sit in the parent's ledger, so until the agent key (plan 050) lands the row judges the parent's turn |
142
+ | `subagent_end` | `stop_reason(..., subagent=True)`, from Claude's SubagentStop (`hooks/projects-stop.py::main`), Cursor's subagentStop `summary` (`hosts/cursor/hook.py::dispatch`), Droid's SubagentStop `task_result` (`hosts/droid/hook.py::main`), Qwen's and Copilot's SubagentStop (`hosts/qwen/hook.py::main`, `hosts/copilot/hook.py::main`), and Amp's and omp's `subagent_stop`, which their bridges send from the child's end with its `parent` (`hosts/amp/hook.py::handle`, `hosts/omp/hook.py::handle`); the evidence half only, never a block (ADR 011); `detail` as `after_block`'s | nothing yet: the rows plan 055 replays before a subagent end may block; `NOT_TOOL_HOOK` lists it. On Claude a subagent's tool rows sit in the parent's ledger, so until the agent key (plan 050) lands the row judges the parent's turn |
143
+ | `reply_unread` | Droid's, Command Code's, Copilot's, Antigravity's and Goose's Stop (`hosts/droid/hook.py::main`, `hosts/commandcode/hook.py::main`, `hosts/copilot/hook.py::main`, `hosts/antigravity/hook.py::main`, `hosts/goose/hook.py::main`) when the transcript or payload yields no closing assistant text (`hosts/droid/hook.py::last_reply`, `hosts/copilot/hook.py::last_reply`); `detail` names the transcript, or says there was none. Such a Stop event carries no reply, or carries it empty, and an empty reply judged would pass any claim as "no claim", so the turn end is recorded and not judged | nothing yet; `NOT_TOOL_HOOK` lists it |
129
144
  | `stop_spec` | nothing since plan 101. Plan 063's shadow wrote one per reply that left a claim or a refusal row: `agree`, `disagree` or `error`, and `fold` or `selector`. The fold it compared against is gone | nothing. `NOT_TOOL_HOOK` still lists it and the replay keeps it beside its claim (`hooks/tezgah_replay.py::SIDE_KINDS`), so an old ledger reads as it did |
130
145
  | `deny`, `nudge` | the [gate](gate.md)'s `hooks/tezgah_gate.py::_deny`, first-nudge `hooks/tezgah_gate.py::decision` | `hooks/tezgah_integrity.py::counters` |
131
- | `snapshot`, `rollback` | `hooks/tezgah_snapshot.py::_capture_one`, `hooks/tezgah_snapshot.py::restore` | `hooks/tezgah_integrity.py::_snapshot_hash`; no counter |
146
+ | `snapshot`, `rollback` | `hooks/tezgah_snapshot.py::_capture_one`, `hooks/tezgah_snapshot.py::restore`, through the module's own `hooks/tezgah_snapshot.py::_append` rather than `note_path` (the Python exception, above): the row carries `v` but keeps its full path, and a forced rollback's `forced` | `hooks/tezgah_integrity.py::_snapshot_hash`; no counter |
132
147
  | `compact` | `hooks/tezgah_integrity.py::note_compaction`, from the post-compaction path (`tezgah_context.remember_compaction` `hooks/tezgah_context.py::remember_compaction`) | `hooks/tezgah_integrity.py::_counts` (what `counters` folds with) |
133
148
  | `lesson` | `hooks/tezgah_context.py::note_lesson`, one row per lesson the budget left in a session block or a per-turn block, with its 8-hex `key` and `block` (`session` or `turn`), inside the host's `safe()` like the rest of the prompt path | no counter; `hooks/tezgah_context.py::NOT_TOOL_HOOK` keeps it out of `hooks/tezgah_context.py::_ledger_since`, so a session of lesson rows still reads as a gate that never ran |
134
149
  | `lesson_tainted` | the gate's allow path for a write to a `.tezgah/lessons.md` (`hooks/tezgah_gate.py::lesson_taint`, from `decision` after every refusal) in a user turn that already holds a row carrying `source`, folded over `turn_rows` (that subagent's rows when the host names one). One row per lesson line the ledger will hold after the write and does not hold now, read off the post-write text, so a partial-line edit names the whole line; its 8-hex `key`, the channel in `source` and `detail`, the ledger in `target`. One row with no `key` when the gate cannot name the line (an `echo` append, an edit whose old text is not there, a removal). The keyed rows also go to the repository's index, which keeps only the ledger's current lines (`hooks/tezgah_lessons.py::write_taint`). Never a refusal (ADR 010); the dry run writes neither | the per-line data label on that lesson in both lesson blocks (`hooks/tezgah_context.py::LESSON_TAINTED`, read once per prompt through `hooks/tezgah_lessons.py::tainted`); no counter |
@@ -138,6 +153,16 @@ a write tool is `edit`, a shell call is `verify` when its command matches the ch
138
153
  | `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 |
139
154
  | `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 |
140
155
  | `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 |
156
+ | `shape` | `hooks/tezgah_integrity.py::stop_reason`, one row per judged reply, keyed like the claim row: `detail` is the report-only shape flags or `ok`, with the reply-shape fields and, in a read-only turn, `mode` | `hooks/tezgah_integrity.py::_counts` (`replies`, `shape`); `hooks/tezgah_integrity.py::_in_flight` |
157
+ | `authorized` | `hooks/tezgah_integrity.py::note_turn`, before the `turn` row of a prompt that names a kill switch while no call is in flight; `authorized` lists the names | the switch latch (`hooks/tezgah_paths.py::_authorized`), which honors a switch made mid-session only when such a row names it |
158
+ | `drift` | `hooks/tezgah_gate.py::drift_reason`, once per turn, before the refusal that re-states the rules once the turn passed `DRIFT_STEPS` steps; `detail` is the step count | `drift_reason` itself (one per turn); `hooks/tezgah_shapes.py::GATE_SIDE_KINDS` and the replay's `hooks/tezgah_replay.py::SIDE_KINDS` |
159
+ | `spawned`, `forked` | the child's session start, in the child's ledger: `spawned` on omp and Amp for a subagent whose bridge names its `parent` (`hosts/omp/hook.py::handle`, `hosts/amp/hook.py::handle`), on Cursor's subagentStart (`hosts/cursor/hook.py::dispatch`) and on opencode's `session.created` of a child (the plugin's `event` hook in `hosts/opencode/plugins/tezgah.js`), with its `agent`; `forked` on omp for a fork, `parent` holding the session it was forked from | the race rule's relation (`hooks/tezgah_integrity.py::_relation`, `hooks/tezgah_integrity.py::spawn_of`); `tezgah-route --report` (`hooks/tezgah_models.py::route_report`); the replay corpus (`hooks/tezgah_replay.py::build_corpus`) |
160
+ | `spawn` | omp's post-tool path for a `task` call (`hosts/omp/hook.py::handle`), one row per named task in the parent's ledger: `child` is the task name, `agent` the agent it runs (`task` when none is given) | `tezgah-route --report`, joining a route to its worker (`hooks/tezgah_models.py::route_report`) |
161
+ | `subagent_start` | the SubagentStart event in the parent's ledger: Claude's (`hooks/projects-auto-init.py::note_child`), Codex's (`hosts/codex/hook.py::main`) and Qwen's (`hosts/qwen/hook.py::main`); `detail` is the agent type, `agent` the agent id | `tezgah-route --report` (`hooks/tezgah_models.py::route_report`) |
162
+ | `orch` | `hooks/tezgah_context.py::record`, for a subagent event (Claude's and dsh's SubagentStart, Codex, Cursor, Droid, Qwen and Copilot subagent events) or a call a host classifies as a delegation | `counters.fanout` (`hooks/tezgah_integrity.py::_counts`) |
163
+ | `judge` | each model judgement's caller: the skill pick (`hooks/tezgah_skill_pick.py::judge`), `tezgah-route` (`bin/tezgah-route.py::main`), `tezgah-docs` and `tezgah-triage` (`bin/tezgah-docs.py::record`, `bin/tezgah-triage.py::record`); `detail` names the caller, the model and the token counts | `counters.judge` (`hooks/tezgah_integrity.py::_counts`) |
164
+ | `route` | `bin/tezgah-route.py::main`, when the call knows its session: the routing verdict (`hooks/tezgah_models.py::route_detail`) | `tezgah-route --report` (`hooks/tezgah_models.py::route_fields`, `hooks/tezgah_models.py::route_report`); the replay's `hooks/tezgah_replay.py::TEST_ROW_KINDS` |
165
+ | `delegation` | opencode's plugin only (`noteDelegation` in `hosts/opencode/plugins/tezgah.js`), when the Python core could not be asked about a call: `detail` is `gate: <failure>`, `id` the session. The call still passes | no reader by name; `counters`' kind histogram counts it (`hooks/tezgah_integrity.py::_counts`) |
141
166
 
142
167
  **`compact` is what a compaction kept, from the record.** When the host hands the PostCompact
143
168
  payload the text the model is about to receive — Claude's `compact_summary` — the shared path
@@ -296,12 +321,31 @@ The checker is `_stop_block` (`hooks/tezgah_integrity.py::_stop_block`), reached
296
321
  (`hosts/omp/hook.py::handle`) — all with `{"decision": "block", "reason": …}`, all inert outside a
297
322
  [root](glossary.md#root) and under the `verify-off` [kill switch](glossary.md#kill-switch)
298
323
  (`hooks/projects-stop.py::main`). Each of these hosts judges the reply after a block again.
324
+ The reply the rule judges is read through `reply_prose`
325
+ (`hooks/tezgah_integrity.py::reply_prose`). A string passes through. A host that hands over
326
+ a message object yields its `text` blocks, joined the way the model wrote them. A thinking
327
+ or tool block is left out. omp's `session_stop` carries `last_assistant_message` as the object
328
+ its `getLastAssistantMessage()` returns, and the bridge passed it through, so the rule judged
329
+ `str(dict)`: one line, no list items, diluted prose. Measured on
330
+ the owner's evidence database (2026-10-29): 1643 of 1656 omp `shape` rows of 45 days carried
331
+ `lines: 1`. Every other host wrote 209 multi-line rows of 231. Of the three days' refusals, 24
332
+ were `reply language` over such a row, and every line-shaped class read a text that had no
333
+ lines. The bridge now sends the text too
334
+ (`hosts/omp/tezgah-hook.ts.in`), but the core is where a host cannot repeat it (plan 143).
299
335
  The re-ask budget is the core's (`_record_only`, `hooks/tezgah_integrity.py::_record_only`): at
300
336
  most `STOP_REASKS` = 2 refusals in one user turn, and never two for one reason class. The count
301
337
  comes from the turn's `blocked:` claim and refusal rows, not from the host's payload. The omp host
302
- hands a block back as a queued message. Its prompt hook writes that text as a new `turn` row. A
303
- refusal row stores the key of its text as `reask`. A `turn` row with that key starts no new user
304
- turn for the count (`_turn_refusals`, `hooks/tezgah_integrity.py::_turn_refusals`). The omp host
338
+ hands a block back as a queued message. Its prompt hook writes that text as a new `turn` row.
339
+ Gemini does the same: an AfterAgent deny comes back through BeforeAgent as the next prompt
340
+ (`hosts/gemini/hook.py::main`). A refusal row stores the key of its text as `reask`. A `turn` row with that key starts no new user
341
+ turn for the count (`_turn_refusals`, `hooks/tezgah_integrity.py::_turn_refusals`). Antigravity's
342
+ `continue` re-enters the loop with no prompt, so its first-invocation hook writes no `turn` row
343
+ while the ledger's last row is that refusal (`hosts/antigravity/hook.py::reentered`). A Goose Stop
344
+ block keeps the turn going with no new prompt and no host count, so its turn rows alone decide
345
+ (`hosts/goose/hook.py::main`). Amp's plugin continues a blocked turn with a user message that
346
+ starts with `[tezgah:stop]`. The plugin never sends that prompt as a `user_prompt`. So it writes
347
+ no `turn` row and grants nothing. The Stop rule then judges that turn with `stop_hook_active`
348
+ (`hosts/amp/tezgah-plugin.ts.in`). The omp host
305
349
  caps the continuations it starts itself, but never a chain of blocks, so tezgah's count bounds that
306
350
  chain. So a reply that fixes the first class but adds a new one gets one more refusal. A blocker
307
351
  list with no evidence is such a new class. A reply after a block that is not refused writes one
@@ -310,7 +354,7 @@ while the turn holds no refusal of this rule. That reply gets no refusal and no
310
354
  Stop hook may have blocked. The host's count rules only where the ledger cannot. A session with no
311
355
  `turn` row allows one refusal per chain. A Cursor `loop_count` of 2 ends the chain. The opencode
312
356
  host has no Stop rule and so no budget. Its plugin API has no end-of-turn hook with an output. The
313
- `event` hook of `hosts/opencode/plugins/tezgah.js` only observes. Twelve triggers, in order, each naming its reason class (`_stop_block`
357
+ `event` hook of `hosts/opencode/plugins/tezgah.js` only observes. Fifteen triggers, in order, each naming its reason class (`_stop_block`
314
358
  `hooks/tezgah_integrity.py::_stop_block`).
315
359
  The first four judge how the reply is written (`_shape_block`, `hooks/tezgah_integrity.py::_shape_block`); the rest judge its evidence:
316
360
 
@@ -339,8 +383,9 @@ The four shape classes share their switches with the text they enforce: `adhd-of
339
383
  are read at a command position on the masked text, `re.M` so that a command on its own later
340
384
  line of the same call counts. A component turn owes one thing more: when a changed UI source is
341
385
  a component rather than a screen (`hooks/tezgah_integrity.py::DESIGN_COMPONENT` - a file
342
- under a component/views/widgets
343
- directory, or a name that carries the convention on its own), a fresh read of the screen is not
386
+ under a component/views/widgets/templates directory, a name that carries the convention on its
387
+ own, a single-file `.vue`/`.svelte`/`.astro` component, or the route file a framework with no
388
+ `components/` convention renders from: `page.tsx`, `layout.tsx`, `+page.svelte`), a fresh read of the screen is not
344
389
  enough by itself and a `tezgah-design check` row has to be newer than the component write it
345
390
  judges (`hooks/tezgah_integrity.py::_design_evidence`, branch `hooks/tezgah_stopspec.py::verdict`) - and it has
346
391
  to be a check whose pass was seen (`passing_check`, `hooks/tezgah_integrity.py::passing_check`), because
@@ -376,8 +421,12 @@ The four shape classes share their switches with the text they enforce: `adhd-of
376
421
  A prose edit is no step and no change (`hooks/tezgah_integrity.py::_prose_edit`,
377
422
  `hooks/tezgah_integrity.py::_work_row`). Such an `edit` row has a real `target` ending in
378
423
  `.md`, `.markdown`, `.rst` or `.adoc`. It owes no check and does not stale a pass. An agent
379
- instruction file is not prose: `SKILL.md`, `AGENTS.md`, `CLAUDE.md` and anything under
380
- `skills/`, `prompts/`, `agents/`, `commands/` or a host directory stay a change. A reply that
424
+ instruction file is not prose (`hooks/tezgah_integrity.py::INSTRUCTION_NAMES`,
425
+ `hooks/tezgah_integrity.py::INSTRUCTION_DIRS`, matched without case): a file named `SKILL.md`, `AGENTS.md`,
426
+ `CLAUDE.md`, `GEMINI.md`, `copilot-instructions.md` or `rules.md`, and anything with `skills/`,
427
+ `prompts/`, `templates/`, `agents/`, `commands/`, `output-styles/`, `hooks/`, `.claude/`,
428
+ `.cursor/`, `.codex/`, `.omp/` or `.github/` among its directories, stay a change. A repo's own
429
+ `hosts/<host>` directory is not in the set. A reply that
381
430
  claims a test ran ("tested", "tüm testler geçti") still needs a pass after a prose-only turn.
382
431
  A comment-only edit to code is still a change: the row carries no text to tell it apart.
383
432
  10. **no external read** — the reply states the state of a system tezgah does not own — a registry,
@@ -457,7 +506,8 @@ The four shape classes share their switches with the text they enforce: `adhd-of
457
506
  refused. A turn with no step, a ledger with no turn marker and a nested session skip it. A
458
507
  subagent's end is never refused (ADR 011). Its `subagent_end` row carries the count of its
459
508
  untyped items as `residual`. The parent's Stop then refuses a reply that types no item of its
460
- own. Claude and Cursor write that row from their subagent-stop events. omp fires none for a
509
+ own. Claude, Cursor, Droid, Qwen and Copilot write that row from their subagent-stop events,
510
+ and Amp's bridge from the child's end. omp fires none for a
461
511
  task session. Its bridge sends the child's last reply from `agent_end`. The row lands in the
462
512
  ledger of the session that spawned the child, read from the child's own session header, and
463
513
  counts once per child. Codex, opencode and dsh write no such row. On omp the reply after a
@@ -516,18 +566,71 @@ The four shape classes share their switches with the text they enforce: `adhd-of
516
566
  (`hooks/tezgah_integrity.py::QUESTION_END`). Its paragraph names no recommended default, and
517
567
  it names no step the ask policy keeps from a default (`hooks/tezgah_integrity.py::never_default`).
518
568
  Such steps are a force push, a delete, production, a credential, a third party, or a spend
519
- over `spend_cap`. The class then refuses a process question: the packs' `ask.process`
569
+ over `spend_cap`. Building a plan the reply wrote is one too
570
+ (`hooks/tezgah_integrity.py::PLAN_BUILD`). So is a last line that asks the owner to pick a
571
+ variant ("V4 mü V1 mi?"), to take a way around a tezgah refusal, or to decide about another
572
+ session's work (`hooks/tezgah_integrity.py::OWNER_CHOICE`, plan 136). The
573
+ class then refuses a process question: the packs' `ask.process`
520
574
  shapes, such as "Başlayayım mı?", "shall I proceed?" and "which first?"
521
575
  (`hooks/tezgah_integrity.py::PROCESS_ASK`). It also refuses a question about an outward
522
576
  step the user granted this session (`hooks/tezgah_integrity.py::grants_in_force`). A question
523
577
  about an outward step the user has not granted is the one the policy asks, so it passes. The
524
- refusal says to take the step, or to move it into the approval batch with a default. Under
578
+ refusal says to take the step, or to move a taste or intent fork into the approval batch
579
+ with a default. A class-F step waits for the answer. Under
525
580
  `tr` it opens with "Bu soruyu sorma". `_residual_block` asks it after `residual work` and
526
581
  before `ready steps` (`hooks/tezgah_integrity.py::needless_ask`). Unlike those two, it needs
527
- no work in the turn, because a plan proposal that ends "Başlayayım mı?" is its measured case.
582
+ no work in the turn, because a reply that ends on a bare "Başlayayım mı?" is its measured case.
528
583
  The research line counted 33 process questions among 48 question endings in the owner's omp
529
584
  transcripts. It refuses once per turn and counts like `residual work`
530
585
  (`hooks/tezgah_integrity.py::LIST_BLOCKS`).
586
+ 14. **handed step** — a turn that ran a command of its own ends by telling the reader to run
587
+ one (plan 142). The line names a command in backticks and addresses the reader as the one
588
+ who runs it: the packs' `ask.hand_over` shapes, "komutunu sen çalıştır", "çalıştırman
589
+ gerekiyor", "senin adımın", "please run", "your step:"
590
+ (`hooks/tezgah_integrity.py::handed_steps`). An answer about how something is done is not
591
+ one of them. "you can run `git config --global ...`" answers the question the reader
592
+ asked, and the review of this plan caught the wider wording refusing it. The turn must
593
+ hold a
594
+ `run`/`verify*` row (`hooks/tezgah_integrity.py::COMMAND_KINDS`). A turn that ran nothing
595
+ could not have run this either, which keeps the class inert on a host that sends no mode. A
596
+ read-only mode closes it outright: there every step waits for the approval. A table row and
597
+ a quoted line are not the
598
+ reply speaking, so the class skips them. The line stays the user's when it names an
599
+ outward step (`hooks/tezgah_integrity.py::outward_classes`), a class-F step
600
+ (`hooks/tezgah_integrity.py::never_default`), the owner's consent, tezgah's own install or a
601
+ `brew` step (`hooks/tezgah_integrity.py::OWNER_STEP`), or when it types its own blocker. The
602
+ refusal says to run each one and report what it printed. `_residual_block` asks it after
603
+ `needless ask` and before `ready steps`. A handover under a residual label ("Sonraki adım:")
604
+ belongs to `residual work`, which comes first. This class is the plain sentence that one
605
+ never saw. It refuses once per turn and counts like `residual work`
606
+ (`hooks/tezgah_integrity.py::LIST_BLOCKS`). Measured on the owner's top-level omp and Claude
607
+ transcripts (2026-10-29): of 3307 user turns, 27 replies handed the reader a command and no
608
+ class refused any of them. The shipped shapes refuse 9 of those turns, 8 of them a step the
609
+ session could have taken. Those eight are four `tezgah-task phase` moves, a local merge,
610
+ two "the next step is yours" and a server command that owes an ACCESS line. The ninth
611
+ quotes the gate's own refusal. The wider wording of the first two review rounds refused 40
612
+ turns and then 14, an answer about how a command is run among them.
613
+ 15. **plan claim** — a first-person claim that a command or check ran, in a turn the host ran
614
+ in a read-only mode (plan 142).
615
+ The host sends the mode with the Stop payload: `permission_mode` on Claude, Command Code,
616
+ Qwen and Droid, and omp's extension sends `plan` from `ctx.sessionModes`. `plan` and Droid's
617
+ `spec` are the read-only ones (`hooks/tezgah_integrity.py::READ_ONLY_MODES`). Nothing could
618
+ run in such a turn, so the claim says what cannot have happened. `_stop_block` asks it right
619
+ after the tamper classes and before the
620
+ admission, since "doğrulanmadı" repairs an unverified claim and here there was nothing to
621
+ check. A check that passed in the turn anyway clears it, like any other
622
+ evidence: a host may allow a read-only command in the mode. The trigger is the packs'
623
+ `ask.ran_it` vocabulary (`hooks/tezgah_integrity.py::RAN_IT`) - "test ettim", "i ran the
624
+ suite". It is read through the question and negation filter a claim is read through
625
+ (`hooks/tezgah_integrity.py::_asserted`). The done and verified vocabularies are not the
626
+ trigger: a plan states its acceptance in those words ("Done when all tests pass", "testler
627
+ yeşil olunca"), and the review of this plan caught both refused. It counts
628
+ as a false completion. The same mode exempts `needless ask` and closes `handed step`
629
+ (`hooks/tezgah_integrity.py::_residual_block`): the approval question is what plan mode asks
630
+ for, and every step it names waits for that approval. Every row of such a turn carries
631
+ `mode`, so a replay judges it as the mode it ran in
632
+ (`hooks/tezgah_replay.py::build_corpus`). A host that sends no
633
+ mode reads as `default`, which is what every rule did before the field.
531
634
 
532
635
  A past-time temporal spec decides classes 5–10 (`hooks/tezgah_stopspec.py::FORMULAS`,
533
636
  plan 063). Seven formulas read ten row atoms (`ROW_ATOMS`) and the turn marker. The module compiles
@@ -810,9 +913,9 @@ Cursor (`hosts/cursor/hook.py::dispatch`), omp (`hosts/omp/hook.py::handle`, whi
810
913
  empty stand-in for the result body its bridge never sends, so a subagent's report keeps its label), and opencode, whose plugin
811
914
  cannot import the core in process and mirrors the control in JavaScript instead — the channel on the
812
915
  call's own row, the label and the taint notice in front of the result the hook is handed, including
813
- the `external` row an MCP answer or a fetched page earns (`untrustedSource`
814
- `hosts/opencode/plugins/tezgah.js:1971`, `labelResult` `:2022`, with the tier's argv reader
815
- `tierRead` at `:1902`). The two halves are pinned against each other over a shared corpus, so neither
916
+ the `external` row an MCP answer or a fetched page earns (`hosts/opencode/plugins/tezgah.js::untrustedSource`,
917
+ `hosts/opencode/plugins/tezgah.js::labelResult`, with the tier's argv reader
918
+ `hosts/opencode/plugins/tezgah.js::tierRead`). The two halves are pinned against each other over a shared corpus, so neither
816
919
  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:
817
920
  opencode's plugin writes that `external` row itself and carries the channel alone, since the UI rule
818
921
  `_screen_read` belongs to the Python half and does not run there.