@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
package/docs/hosts.md CHANGED
@@ -1,7 +1,7 @@
1
1
  # Hosts
2
2
 
3
- tezgah is one Python core with six host adapters around it: `claude`, `codex`,
4
- `cursor`, `opencode`, `dsh`, `omp`. This page is for the maintainer wiring a new
3
+ tezgah is one Python core with fourteen host adapters around it: `claude`, `codex`,
4
+ `cursor`, `opencode`, `dsh`, `omp`, `qwen`, `droid`, `commandcode`, `copilot`, `gemini`, `antigravity`, `goose`, `amp`. This page is for the maintainer wiring a new
5
5
  host, adding a mark, or debugging a status line that claims something a host
6
6
  cannot see. Read it when a question starts with "does this host get X?" or "can
7
7
  this host observe Y?" - the answer is not uniform, and guessing it produces a
@@ -17,12 +17,20 @@ surface can honestly report this session; the two skill-read marks (`pony`,
17
17
 
18
18
  | Host | Hook events it fires | Always-on contract | Per-turn reminder | Status surface, how drawn | Measures |
19
19
  |---|---|---|---|---|---|
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`) |
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 (`orca_out`, `statusline.py:52-60`) | 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
- | 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:2372-2737`) - `tool.execute.before` carries the whole gate, `tool.execute.after` the channel on the row, the provenance label on the result and the taint notice on the next effect (`untrustedSource` `hosts/opencode/plugins/tezgah.js:1942`, `labelResult` `:1993`) | static only: generated `instructions` files, because there is no session-start injection point (`hosts/opencode/plugins/tezgah.js:3-6`, `bin/tezgah-setup::opencode_contract_text`) | chat.message pushes a synthetic part (`hosts/opencode/plugins/tezgah.js:2652-2703`) | TUI plugin `tezgah-tui.tsx` declared in `tui.json`, runs `tezgah-status` on the event bus (`hosts/opencode/tui/tezgah-tui.tsx:1-11`; wiring `bin/tezgah-setup::install_opencode`) | all seven: skill reads and kinds are classified in process (`hosts/opencode/plugins/tezgah.js:2227-2263`) |
24
- | dsh | SessionStart, SubagentStart, UserPromptSubmit, PreToolUse, PostToolUse (`hosts/dsh/hooks.json:2-19`); no Stop rule, because the bridge drops every call's outcome and no check there can show a pass | hook: the same Claude-family scripts, named by `configPath` (`bin/tezgah-setup::dsh_patch_block`) | UserPromptSubmit, through the bridge (`hosts/dsh/hooks.json:9-11`) | web-profile plugin: authenticated `GET /api/tezgah.status` -> `tezgah-status` (`hosts/dsh/statusline/lib/index.js:15,66-104`) | five tool-use: the route passes `--observable=consult,research,graph,orch,judge` (`hosts/dsh/statusline/lib/index.js:16-19,84,96`) |
25
- | omp | session_start, session_compact, session_switch, turn_end, before_agent_start, tool_call, tool_result, session_stop (`hosts/omp/tezgah-hook.ts.in:596,655,674,679,681,701,720,755`) | both: static `RULES.md` (`bin/tezgah-setup::STATIC_RULES`, `bin/tezgah-setup::rules_block`) and a session payload carrying only what a static file cannot know (`hosts/omp/hook.py::handle`) | before_agent_start returns a hidden message (`hosts/omp/tezgah-hook.ts.in:681-699`) | extension draws `ctx.ui.setWidget` below the editor, `setStatus` as fallback (`hosts/omp/tezgah-hook.ts.in:170-203`); ANSI line from `hosts/omp/hook.py::status_line` | all seven: the embedded runner filters skill reads before asking python (`hosts/omp/tezgah-hook.ts.in:257-264`) and no `observable` is passed (`hosts/omp/hook.py::status_line`) |
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::color`) | five tool-use (`statusline.py::observable`) |
23
+ | opencode | no lifecycle events; plugin hooks: config, permission.ask, tool.execute.before, event, shell.env, experimental.session.compacting, chat.message, tool.execute.after (the hook object `Tezgah` in `hosts/opencode/plugins/tezgah.js` returns) - `tool.execute.before` carries the whole gate, `event` writes a `spawned` row naming the parent on a child's `session.created`, `tool.execute.after` the channel on the row, the provenance label on the result and the taint notice on the next effect (`untrustedSource` and `labelResult` in the same file) | static only: generated `instructions` files, because there is no session-start injection point (the header comment of `hosts/opencode/plugins/tezgah.js`, `bin/tezgah-setup::opencode_contract_text`) | chat.message pushes a synthetic part (the `chat.message` hook of `Tezgah`) | TUI plugin `tezgah-tui.tsx` declared in `tui.json`, runs `tezgah-status` on the event bus (`TezgahBar` in `hosts/opencode/tui/tezgah-tui.tsx`; wiring `bin/tezgah-setup::install_opencode`) | all seven: skill reads and kinds are classified in process (`classify` in `hosts/opencode/plugins/tezgah.js`) |
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` (`STATUS_PATH` and `apply` in `hosts/dsh/statusline/lib/index.js`) | five tool-use: the route passes `--observable=consult,research,graph,orch,judge` (`OBSERVABLE` in `hosts/dsh/statusline/lib/index.js`) |
25
+ | omp | 13 events: agent_start, tool_execution_start, tool_execution_end, agent_end, session_shutdown, session_start, session_compact, session_switch, turn_end, before_agent_start, tool_call, tool_result, session_stop (the `pi.on` registrations in `hosts/omp/tezgah-hook.ts.in`); the first five drive the busy spinner, and `agent_end` also records a task subagent's end, since omp fires no `session_stop` for one | 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 (its `pi.on` handler in `hosts/omp/tezgah-hook.ts.in`) | extension draws `ctx.ui.setWidget` below the editor, `setStatus` as fallback (`draw` in `hosts/omp/tezgah-hook.ts.in`); ANSI line from `hosts/omp/hook.py::status_line` | all seven: the embedded runner filters skill reads before asking python (`skillRead` in `hosts/omp/tezgah-hook.ts.in`) and no `observable` is passed (`hosts/omp/hook.py::status_line`) |
26
+ | qwen | SessionStart, UserPromptSubmit, PreToolUse, PostToolUse, PostToolUseFailure, SubagentStart, SubagentStop (record-only, `subagent_end`), Stop (`bin/tezgah-setup::QWEN_HOOK_EVENTS`), all in `~/.qwen/settings.json` beside Orca's rows | the managed block in `~/.qwen/QWEN.md` (`bin/tezgah-setup::STATIC_RULES`); SessionStart drops the core while the block is there (`hosts/qwen/hook.py::rules_carry_core`) | UserPromptSubmit -> `additionalContext` (`hosts/qwen/hook.py::main`) | `ui.statusLine` command in `~/.qwen/settings.json` -> `tezgah-statusline --qwen`, ANSI kept by `respectUserColors` (`bin/tezgah-setup::install_qwen`) | five tool-use (`statusline.py::observable`) |
27
+ | droid | SessionStart, UserPromptSubmit, PreToolUse, PostToolUse, SubagentStop (record-only, `subagent_end`), Stop (`bin/tezgah-setup::DROID_HOOK_EVENTS`), all in `~/.factory/hooks.json` | the managed block in `~/.factory/AGENTS.md` (`bin/tezgah-setup::STATIC_RULES`); SessionStart drops the core while the block is there (`hosts/droid/hook.py::rules_carry_core`) | UserPromptSubmit -> `additionalContext` (`hosts/droid/hook.py::main`) | `statusLine` command in `~/.factory/settings.json` -> `tezgah-statusline --droid`, set in place with the file's comments kept (`bin/tezgah-setup::install_droid`, `bin/tezgah-setup::jsonc_put`) | five tool-use (`statusline.py::observable`) |
28
+ | commandcode | SessionStart, PreToolUse (matcher `shell\|write\|edit`), PostToolUse, Stop (`bin/tezgah-setup::COMMANDCODE_HOOK_EVENTS`), all in the `hooks` key of `~/.commandcode/settings.json` | the managed block in `~/.commandcode/AGENTS.md` (`bin/tezgah-setup::STATIC_RULES`); SessionStart drops the core while the block is there (`hosts/commandcode/hook.py::rules_carry_core`) | unavailable: Command Code has no prompt event | no custom status line - the segment rides `systemMessage`, plain, TUI only, at SessionStart and Stop (`hosts/commandcode/hook.py::status`) | five tool-use (`TOOL_USE_MEASURES` at `hosts/commandcode/hook.py::status`) |
29
+ | copilot | SessionStart, UserPromptSubmit, PreToolUse, PostToolUse, PostToolUseFailure, subagentStart, SubagentStop (record-only, `subagent_end`), Stop (`bin/tezgah-setup::COPILOT_HOOK_EVENTS`), all in tezgah's own `$COPILOT_HOME/hooks/tezgah.json` | the managed block in `$COPILOT_HOME/copilot-instructions.md` (`bin/tezgah-setup::STATIC_RULES`); SessionStart drops the core while the block is there (`hosts/copilot/hook.py::rules_carry_core`) | unavailable: Copilot drops a command hook's UserPromptSubmit output; the event still writes the turn row (`hosts/copilot/hook.py::main`) | `statusLine` command in `$COPILOT_HOME/settings.json` -> `tezgah-statusline --copilot`, written only while that file is plain JSON (`bin/tezgah-setup::install_copilot`) | five tool-use (`statusline.py::observable`) |
30
+ | gemini | SessionStart, BeforeAgent, BeforeTool, AfterTool, AfterAgent (`bin/tezgah-setup::GEMINI_HOOK_EVENTS`), all in tezgah's own extension `~/.gemini/extensions/tezgah/hooks/hooks.json` | the managed block in `~/.gemini/GEMINI.md` (`bin/tezgah-setup::STATIC_RULES`), shared with Antigravity; SessionStart drops the core while the block is there (`hosts/gemini/hook.py::rules_carry_core`) | BeforeAgent -> `hookSpecificOutput.additionalContext`, appended to the prompt for that turn (`hosts/gemini/hook.py::main`) | no custom status line (`ui.footer.items` is a closed list) - the segment rides `systemMessage`, plain, at SessionStart and AfterAgent (`hosts/gemini/hook.py::status`) | five tool-use (`TOOL_USE_MEASURES` at `hosts/gemini/hook.py::status`) |
31
+ | antigravity | SessionStart, PreInvocation, PreToolUse, PostToolUse, Stop (`bin/tezgah-setup::ANTIGRAVITY_HOOK_EVENTS`), all under the `tezgah` name of `~/.gemini/config/hooks.json`; the payload names no event, so each command passes it as its first argument | the managed block in `~/.gemini/GEMINI.md` (`bin/tezgah-setup::STATIC_RULES`), shared with Gemini CLI; SessionStart injects the core as a system message while the block is missing (`hosts/antigravity/hook.py::rules_carry_core`) | PreInvocation with `invocationNum` 0 -> an `injectSteps` `ephemeralMessage`; whether that number restarts each user turn is undocumented (`hosts/antigravity/hook.py::main`) | `statusLine` command in `~/.gemini/antigravity-cli/settings.json` -> `tezgah-statusline --antigravity`, written only while that file is plain JSON (`bin/tezgah-setup::install_antigravity`) | five tool-use (`statusline.py::observable`) |
32
+ | goose | SessionStart, UserPromptSubmit, PreToolUse, PostToolUse, PostToolUseFailure, Stop (`bin/tezgah-setup::GOOSE_HOOK_EVENTS`), all in tezgah's own plugin `~/.agents/plugins/tezgah/hooks/hooks.json`; the payload names the event in `event` | the managed block in `~/.config/goose/.goosehints` (`bin/tezgah-setup::STATIC_RULES`), which Goose adds to the system prompt of every request; no hook can inject it, because SessionStart is observation only | unavailable: Goose ignores UserPromptSubmit output; the event still writes the turn row (`hosts/goose/hook.py::main`) | unavailable: Goose has no status line and shows no hook output to the user | none drawn; the PostToolUse rows still record the five tool-use marks for `tezgah-status` (`hosts/goose/hook.py::post_tool`) |
33
+ | amp | no hook file: tezgah's TypeScript plugin `$XDG_CONFIG_HOME/amp/plugins/tezgah.ts` (else `~/.config/amp/plugins/tezgah.ts`) subscribes to `session.start`, `agent.start`, `tool.call`, `tool.result` and `agent.end` and forwards each to `hosts/amp/hook.py` (`hosts/amp/tezgah-plugin.ts.in`) | the managed block in `~/.config/amp/AGENTS.md` (`bin/tezgah-setup::STATIC_RULES`), which Amp always includes; `session.start` cannot inject, so the session brief rides the thread's next `agent.start`, with the core only while that block is missing or `AMP_IGNORE_GUIDANCE_FILES` is set (`hosts/amp/hook.py::handle`) | hook: `agent.start` returns the reminder as a hidden message (`display: false`); a prompt that starts with `[tezgah:stop]` is tezgah's own continue and sends no `user_prompt` | the experimental `createStatusItem`, plain text, for the thread the user is looking at only (owner-approved, plan 140 A-D15); without it, no status surface | none drawn beyond the status item; the `tool.result` rows record the five tool-use marks and a skill read |
26
34
 
27
35
  The middle five marks (`consult`, `research`, `graph`, `orch`, `judge`) are tool-use marks:
28
36
  any adapter that sees its host's tool calls can light them by calling
@@ -36,26 +44,50 @@ What each host can enforce, in the
36
44
  [enforcement capability](glossary.md#enforcement-capability) words. `gate`: the
37
45
  hook can refuse. `observe`: it records and never refuses. `partial`: only part
38
46
  of the surface fires. `unavailable`: the host has no event for it. A cell gets
39
- `unverified` when no driver on this machine can probe it. Fixture payloads test
40
- the code path there, but no run has shown the host obeying it.
47
+ `unverified` when no run has shown the host binary obeying it. Fixture payloads
48
+ test the code path there. `--report --live` goes one step further: it drives the
49
+ installed wiring, the hook command or the plugin loaded with node. No host
50
+ binary runs, so a live `ok` proves that the wiring answers, not that the host obeys.
41
51
 
42
52
  | Host | Gate (PreToolUse) | Stop | Subagent |
43
53
  |---|---|---|---|
44
- | claude | gate, probed by `--report --live` (`bin/tezgah-setup::live_probe`) | gate, unverified (`hooks/projects-stop.py::main`) | observe, unverified: a brief at SubagentStart, a record-only `subagent_end` row at SubagentStop (ADR 011) |
45
- | codex | gate, probed by `--report --live`; Codex runs only a trusted group | gate, unverified (`hosts/codex/hook.py::main`) | partial, unverified: a brief at SubagentStart, no subagent-end event |
46
- | cursor | gate, probed by `--report --live` | gate, unverified (`hosts/cursor/hook.py::dispatch`) | observe, unverified: a brief at subagentStart, a record-only `subagent_end` row at subagentStop |
47
- | opencode | gate, probed by `--report --live` through a node driver | unavailable: the plugin API has no end-of-turn event | unavailable |
48
- | dsh | gate, probed by `--report --live` through the bridge | unavailable: ADR 003 removed the Stop entry, because the bridge carries no outcome | partial, unverified: a brief at SubagentStart, no SubagentStop entry |
49
- | omp | gate, probed by `--report --live` (node 22.6+) | gate, unverified (`hosts/omp/hook.py::handle`) | partial, unverified: a brief at a task session's start; `session_stop` does not fire for task sessions |
54
+ | claude | gate, unverified: probed by `--report --live` (`bin/tezgah-setup::live_probe`), which runs the PreToolUse command of the plugin copy's `hooks/hooks.json` on a deny payload; no claude binary runs, so that Claude Code obeys the deny is not shown | gate, unverified (`hooks/projects-stop.py::main`) | observe, unverified: a brief at SubagentStart, a record-only `subagent_end` row at SubagentStop (ADR 011) |
55
+ | codex | gate, unverified: probed by `--report --live` through the PreToolUse command in `$CODEX_HOME/hooks.json`; no codex binary runs, and Codex runs only a trusted group | gate, unverified (`hosts/codex/hook.py::main`) | partial, unverified: a brief at SubagentStart, no subagent-end event |
56
+ | cursor | gate, unverified: probed by `--report --live` through the preToolUse command in `~/.cursor/hooks.json`; no cursor-agent binary runs | gate, unverified (`hosts/cursor/hook.py::dispatch`) | observe, unverified: a brief at subagentStart, a record-only `subagent_end` row at subagentStop |
57
+ | opencode | gate, unverified: probed by `--report --live`, which imports the installed plugin with node and fires its `tool.execute.before`; opencode's own loader is not what ran it | unavailable: the plugin API has no end-of-turn event | unavailable |
58
+ | dsh | gate, unverified: probed by `--report --live` through the bridge's PreToolUse command; no dsh binary runs | unavailable: ADR 003 removed the Stop entry, because the bridge carries no outcome | partial, unverified: a brief at SubagentStart, no SubagentStop entry |
59
+ | omp | gate, unverified: probed by `--report --live`, which loads the installed extension with node 22.6+; omp's own loader is not what ran it | gate, unverified (`hosts/omp/hook.py::handle`) | partial, unverified: a brief at a task session's start; `session_stop` does not fire for task sessions |
60
+ | qwen | gate, unverified: probed by `--report --live` through the hook command in `~/.qwen/settings.json`; that the qwen binary obeys the deny stays unverified until `TEZGAH_E2E_SPEND=1 python3 tests/e2e_qwen.py` runs (it spends a model call) | gate, unverified (`hosts/qwen/hook.py::main`, judged on `last_assistant_message`) | observe, unverified: a brief at SubagentStart, a record-only `subagent_end` row at SubagentStop |
61
+ | droid | gate, unverified: probed by `--report --live` through the hook command in `~/.factory/hooks.json`; that the droid binary obeys the deny stays unverified until `TEZGAH_E2E_SPEND=1 python3 tests/e2e_droid.py` runs (it spends a model call) | gate, unverified: Droid's Stop carries no reply, so the reply is read from `transcript_path` (`hosts/droid/hook.py::last_reply`), and a turn end whose reply is not there yet is a `reply_unread` row, never judged | partial, unverified: a record-only `subagent_end` row from SubagentStop's `task_result`; no SubagentStart, so no brief |
62
+ | commandcode | gate, unverified: probed by `--report --live` through the hook command in `~/.commandcode/settings.json`, but no run of the binary has shown it obeying the deny; only shell, write and edit reach the hook (read, MCP, subagent and web tools do not), and plan mode skips every hook | gate, unverified: Stop carries no reply, so the reply is read from `transcript_path` (`hosts/commandcode/hook.py::last_reply`) and a turn end with none there is a `reply_unread` row, never judged; the host caps a block at three retries | unavailable: the settings hooks have no subagent event |
63
+ | copilot | gate, unverified: probed by `--report --live` through the hook command in `$COPILOT_HOME/hooks/tezgah.json`, which answers with a top-level `permissionDecision: deny`, but no run of the binary has shown it obeying the deny. A PreToolUse hook that times out (10 s) lets the call through, by Copilot's own rule; one that crashes denies it. A repo's `.claude/settings.json` hooks run there too | gate, unverified: Stop carries no reply, so the reply is the last `assistant.message` in `transcript_path` (`hosts/copilot/hook.py::last_reply`), and a turn end with none there is a `reply_unread` row, never judged; the host ends the turn anyway after 8 blocks in a row | observe, unverified: a brief at subagentStart, a record-only `subagent_end` row at SubagentStop; the built-in `general-purpose` agent fires neither |
64
+ | gemini | gate, unverified: probed by `--report --live` through the hook command in `~/.gemini/extensions/tezgah/hooks/hooks.json`, which answers with a top-level `decision: deny`, but no run of the binary has shown it obeying the deny, and no run has shown Gemini loading an extension dir written without `gemini extensions install` (its loader reads every subdir; a non-empty `security.allowedExtensions` asks for install metadata tezgah does not write). A hook that crashes or prints bad JSON is a warning and the call goes on | gate, unverified: AfterAgent carries the reply as `prompt_response` and `stop_hook_active` (`hosts/gemini/hook.py::main`); a deny sends the reason back as a new prompt | unavailable: no subagent event; a subagent is a tool call (`codebase_investigator`, `generalist`, `invoke_agent`) the gate sees as `Task` |
65
+ | antigravity | gate, unverified: probed by `--report --live` through the hook command under the `tezgah` name of `~/.gemini/config/hooks.json`, fed Antigravity's camelCase envelope with the event as argv, which answers with a top-level `decision: deny` (an allow answers `decision: allow`, since the field is required), but no run of the binary has shown it obeying the deny. What Antigravity does when a hook crashes or times out is undocumented. The same file arms the Antigravity App and IDE | gate, unverified: Stop carries no reply, so the reply is read from `transcriptPath` (`hosts/antigravity/hook.py::last_reply`), and a turn end with none there is a `reply_unread` row, never judged; only an idle `model_stop` is judged, and `decision: continue` sends the model back into the loop with the reason as a system message. No block counter: the turn's refusal rows bound the re-asks | unavailable: no subagent event; a child conversation keeps its own ledger, and `invoke_subagent` reaches the gate as Task |
66
+ | goose | gate, unverified: probed by `--report --live` through the hook command in `~/.agents/plugins/tezgah/hooks/hooks.json`, which answers `{"decision": "block"}` on stdout, but no run of the binary has shown it obeying the block, nor loading a plugin dir written by hand rather than by `goose plugin install` (the plugins guide says both are discovered). `on_failure` is left at its default `allow`, so a hook that crashes, times out or prints no decision lets the call through: the gate fails open on a hook fault. Only `shell`, `write`, `edit` and `<extension>__<tool>` reach the gate | gate, unverified: Stop carries the reply as `last_assistant_message` (`hosts/goose/hook.py::main`), and a Stop with none is a `reply_unread` row, never judged. It carries no count of earlier blocks, so the turn's own refusal rows bound the re-asks; Goose ends the turn anyway after `GOOSE_STOP_HOOK_BLOCK_CAP` blocks in a row | unavailable: SubagentStart and SubagentStop are not emitted; the subagent tool is not mapped |
67
+ | amp | gate, unverified: `--report --live` loads the installed plugin with node 22.6+ (`--experimental-strip-types`) and fires its `tool.call` on a `Bash` call carrying `cmd`; the plugin answers `{action: "reject-and-continue"}`, but Amp's own Bun loader is not what ran it, so the row stays UNVERIFIED. A hook that is missing, crashes, times out or prints no JSON answers `{action: "allow"}`: the gate fails open. Only `Bash`, `shell_command`, `edit_file`, `create_file`, `apply_patch`, `Task` and an MCP effect name reach the gate | gate, unverified: `agent.end` with status `done` asks the Stop rule on the last assistant message and continues the turn with `[tezgah:stop] <reason>`; a turn that marker started is judged as the re-ask (`stop_hook_active`), and Amp's own cap of 5 continues backs the core's budget. Error and cancelled turns are not judged. A shell result passes only on an integer `exitCode` read off an output object (`hosts/amp/hook.py::call_failed`); Amp's docs do not show that its Bash result carries one, so until a run shows it no Amp check counts as passed | partial, unverified: a thread with a parent (`ctx.thread.parentThreadID()`) gets the subagent brief and a record-only `subagent_end` row at its `agent.end`; whether a Task subagent's events reach plugins is undocumented |
50
68
 
51
69
  Every host also attests its own hook entries once per session
52
- (`hooks/tezgah_attest.py::run`). Claude, dsh, Codex, Cursor and omp do it at
53
- session start. opencode does it at the first message, through `oncePerSession`.
70
+ (`hooks/tezgah_attest.py::run`). Every host but opencode does it at session
71
+ start. opencode does it at the first message, through `oncePerSession`.
54
72
  The `attest` row and the `drift` mark are
55
73
  [evidence](evidence.md#the-kinds-by-what-reads-them), never a block.
56
74
  `--report` prints the same comparison per host
57
75
  (`bin/tezgah-setup::attestation_rows`).
58
76
 
77
+ Every Stop adapter hands the core the host's permission mode for the turn
78
+ (`hooks/tezgah_integrity.py::read_only_turn`, plan 142). Claude, Command Code,
79
+ Qwen, Copilot, Gemini, Codex, Goose, Droid and Cursor send it as
80
+ `permission_mode` on the Stop payload (Antigravity's envelope is camelCase,
81
+ `permissionMode`). omp has no such field, so its extension reads
82
+ `ctx.sessionModes.getState().plan` and sends `plan` when it is active
83
+ (`hosts/omp/tezgah-hook.ts.in`). A host that sends nothing reads as `default`.
84
+ Only `plan` and Droid's `spec` change a verdict, and only two of them: the rule
85
+ lets the approval question such a turn ends on through, and refuses a
86
+ done/tested claim in it
87
+ ([evidence](evidence.md#the-stop-rule-end-to-end)). Claude's hooks reference
88
+ documents the field on its own Stop payload. Every other adapter reads whatever
89
+ its payload carries.
90
+
59
91
  ## What each host gets, and where it is written
60
92
 
61
93
  Every host gets the skill symlinks from `SKILLS` (`bin/tezgah-setup::SKILLS`)
@@ -90,7 +122,8 @@ shared config, contract hash and the CLI symlinks every host shell can call
90
122
  (`bin/tezgah-setup::write_user_agents`).
91
123
  - **cursor** - `install_cursor` (`bin/tezgah-setup::install_cursor`): `~/.cursor/hooks.json`
92
124
  (tezgah entries replaced, a user's Orca entries merged), `skills/*` symlinks,
93
- `mcp.json` servers, and `cli-config.json` `statusLine`.
125
+ `mcp.json` servers, and `cli-config.json` `statusLine`. The symlinks skip a
126
+ skill another dir Cursor reads already links (`bin/tezgah-setup::cursor_shared_copy`).
94
127
  - **opencode** - `install_opencode` (`bin/tezgah-setup::install_opencode`): the plugin under
95
128
  both `plugins/` and `plugin/` (version drift), `skills/*` symlinks, the
96
129
  generated `~/.config/tezgah/opencode-contract.md` (`bin/tezgah-setup::opencode_contract`) and
@@ -126,6 +159,131 @@ shared config, contract hash and the CLI symlinks every host shell can call
126
159
  its own: orx's `codex` target writes the shim to `~/.agents/skills/orx`,
127
160
  omp's native `agents` skill root, so `install_openresearch` maps omp onto it
128
161
  (`ORX_AGENTS`, `bin/tezgah-setup::ORX_AGENTS`).
162
+ - **qwen** - `install_qwen` (`bin/tezgah-setup::install_qwen`) writes into
163
+ `~/.qwen/settings.json` (or `$QWEN_HOME`). It adds one hook group per event of
164
+ `QWEN_HOOK_EVENTS`, each `timeout` in milliseconds. The PreToolUse group
165
+ carries `QWEN_PRETOOL_MATCHER` over Qwen's runtime tool ids. It also sets
166
+ `ui.statusLine`. A status line the user had comes back at uninstall. Orca's
167
+ groups and the provider keys in that file stay, and
168
+ the backup is owner-only (`bin/tezgah-setup::backup`). The install links
169
+ `skills/*` into `~/.qwen/skills`. It writes the always-on block in
170
+ `~/.qwen/QWEN.md`. tezgah writes no subagent roles, MCP rows or orx shim for
171
+ Qwen yet.
172
+ - **droid** - `install_droid` (`bin/tezgah-setup::install_droid`) writes
173
+ `~/.factory/hooks.json`, keyed by event at the top. It adds one hook group per
174
+ event of `DROID_HOOK_EVENTS`, each `timeout` in seconds, and keeps the user's
175
+ own groups. The PreToolUse group carries `DROID_PRETOOL_MATCHER` over Droid's
176
+ tool ids. Droid reads the `hooks` key of `settings.json` only while hooks.json
177
+ is absent. The install then writes no hooks.json and says why
178
+ (`bin/tezgah-setup::droid_hooks_blocked`). Droid's settings.json holds
179
+ comments, so the install edits it in place and keeps them
180
+ (`bin/tezgah-setup::jsonc_put`). It adds the `statusLine`. It sets
181
+ `includeCoAuthoredByDroid` to false, the switch for Droid's co-author trailer.
182
+ The uninstall puts that switch back to what the install found: the user's
183
+ value, or no key. A file it cannot edit safely is left alone and the run names
184
+ both keys. The install
185
+ links `skills/*` into `~/.factory/skills` beside the user's own links. It
186
+ writes the always-on block in `~/.factory/AGENTS.md`. The uninstall deletes a
187
+ hooks.json that held only tezgah's groups. An empty one would still hide
188
+ settings.json's hooks. tezgah writes no subagent roles, MCP rows or orx
189
+ shim for Droid yet.
190
+ - **commandcode** - `install_commandcode` (`bin/tezgah-setup::install_commandcode`)
191
+ writes into the `hooks` key of `~/.commandcode/settings.json`. It adds one
192
+ hook group per event of `COMMANDCODE_HOOK_EVENTS`, each `timeout` in seconds.
193
+ The user's own groups stay, and the backup is owner-only. The PreToolUse group
194
+ carries `COMMANDCODE_PRETOOL_MATCHER`, which Command Code tests against the
195
+ tool's display name. The same write sets `attribution.commit` to `""`. That
196
+ turns off the default `Co-authored-by: CommandCodeBot` commit trailer. A value
197
+ the user set stays. The uninstall restores the value the install found, from
198
+ the install record (`bin/tezgah-setup::restore_prior`). The install links `skills/*` into `~/.commandcode/skills`. It
199
+ writes the always-on block in `~/.commandcode/AGENTS.md`. tezgah writes no
200
+ subagent roles, MCP rows or orx shim for Command Code yet.
201
+ - **copilot** - `install_copilot` (`bin/tezgah-setup::install_copilot`) writes
202
+ `$COPILOT_HOME/hooks/tezgah.json`, a `{"version": 1, "hooks": ...}` file that
203
+ is wholly tezgah's. `COPILOT_HOME` defaults to `~/.copilot`. The file holds
204
+ one entry per event of `COPILOT_HOOK_EVENTS`, each `timeoutSec` in seconds.
205
+ The inline `hooks` key of settings.json is never used, since one bad item
206
+ there rejects every hook in it. The PreToolUse entry carries
207
+ `COPILOT_PRETOOL_MATCHER`. settings.json is JSON with comments. So the status
208
+ line and `includeCoAuthoredBy: false` go in only while it parses as plain
209
+ JSON. A file holding comments gets a `holds comments - left untouched` line,
210
+ and the install still exits 0. The install refuses an unreadable, non-UTF-8
211
+ or non-object file and fails. That setting turns off the default `Co-authored-by` trailer on the
212
+ agent's commits. A value the user set stays. The install links `skills/*`
213
+ into `$COPILOT_HOME/skills` and writes the always-on block in
214
+ `$COPILOT_HOME/copilot-instructions.md`. The uninstall deletes tezgah.json.
215
+ Each settings key goes back to what the install found, the status line
216
+ too. Copilot gets no subagent roles, MCP rows or orx shim from tezgah yet.
217
+ - **gemini** - `install_gemini` (`bin/tezgah-setup::install_gemini`) writes
218
+ tezgah's own extension, `~/.gemini/extensions/tezgah/`, which is wholly
219
+ tezgah's. `GEMINI_CLI_HOME` moves `~/.gemini` under it. The dir holds
220
+ `gemini-extension.json`, `hooks/hooks.json` with one group per event of
221
+ `GEMINI_HOOK_EVENTS` (each `timeout` in milliseconds) and `skills/*` links.
222
+ The BeforeTool group carries `GEMINI_PRETOOL_MATCHER`. settings.json is never
223
+ written: it is JSON with comments and holds the user's keys. Gemini's default
224
+ prompt adds no co-author trailer and no setting names one, so there is
225
+ nothing to turn off. The install writes the always-on block in
226
+ `~/.gemini/GEMINI.md`, which Antigravity reads too. The uninstall deletes the
227
+ extension dir. It strips the block only while no other host of
228
+ `GEMINI_MD_HOSTS` stays armed (`bin/tezgah-setup::gemini_md_kept`). Gemini
229
+ gets no subagent roles, MCP rows or orx shim from tezgah yet.
230
+ - **antigravity** - `install_antigravity` (`bin/tezgah-setup::install_antigravity`)
231
+ writes tezgah's hook set under the top-level `tezgah` name of
232
+ `~/.gemini/config/hooks.json`, beside the user's other names. Each event of
233
+ `ANTIGRAVITY_HOOK_EVENTS` gets one item, its `timeout` in seconds. PreToolUse
234
+ and PostToolUse take a matcher group, the others a bare handler. Antigravity
235
+ never loads hooks from `antigravity-cli/settings.json`. The Antigravity App
236
+ and IDE read the same global hooks.json, so the install arms them too. The
237
+ status line goes in `~/.gemini/antigravity-cli/settings.json` only while it
238
+ is plain JSON. The install names a file holding comments, leaves it alone and exits 0.
239
+ The skill links go in `antigravity-cli/skills`. Antigravity
240
+ adds no co-author trailer of its own, and no setting names one. An open
241
+ feature request asks for one (google-antigravity/antigravity-cli#230). The
242
+ always-on block is the `~/.gemini/GEMINI.md` one Gemini CLI shares
243
+ (`bin/tezgah-setup::gemini_md_kept`). The uninstall takes out the `tezgah`
244
+ name and the links, and puts the user's own statusLine back. It removes a file that held only
245
+ tezgah's keys. Antigravity gets no subagent roles, MCP rows or orx shim from
246
+ tezgah yet. Antigravity can never show a passing check. Its PostToolUse
247
+ carries no output. A shell call that went to the background also ends with an
248
+ empty `error`. So the hook records each shell call as ran, never a pass
249
+ (`hosts/antigravity/hook.py::post_tool`). The owner chose this: a done or
250
+ tested claim there always stops at the Stop rule's `no verify_ok` class.
251
+ - **goose** - `install_goose` (`bin/tezgah-setup::install_goose`) writes
252
+ tezgah's own plugin, `~/.agents/plugins/tezgah/`, which is wholly tezgah's.
253
+ It holds `plugin.json`, `hooks/hooks.json` and `skills/*` links. The hooks
254
+ file has one group per event of `GOOSE_HOOK_EVENTS`, each `timeout` in
255
+ seconds. Goose loads the skills as `tezgah:<skill>`. Goose tests a matcher
256
+ unanchored, so the PreToolUse group carries the anchored
257
+ `GOOSE_PRETOOL_MATCHER`. No hook sets `on_failure`, so it stays at Goose's
258
+ default `allow`, and a hook fault lets the call through. The always-on block
259
+ goes in `~/.config/goose/.goosehints`. Neither Goose's docs nor its system
260
+ prompt add a co-author trailer or name a setting for one. The uninstall
261
+ deletes the plugin dir and strips the block. The rest of `~/.agents` is the
262
+ user's and stays. The install ignores `GOOSE_PATH_ROOT`, since the docs
263
+ do not say where it puts the hints file. Goose gets no subagent roles, MCP
264
+ rows or orx shim from tezgah yet.
265
+ - **amp** - `install_amp` (`bin/tezgah-setup::install_amp`) writes one file,
266
+ `tezgah.ts`, into Amp's system plugin dir (`hooks/tezgah_paths.py::AMP_PLUGIN`,
267
+ under `XDG_CONFIG_HOME` when that is set). `hooks/tezgah_attest.py::amp_plugin`
268
+ renders it from `hosts/amp/tezgah-plugin.ts.in`. Amp loads every `.ts` there,
269
+ and nothing registers it. A running Amp needs `plugins: reload`. The plugin
270
+ spawns `hosts/amp/hook.py` without blocking, one JSON in and one out. One
271
+ plugin process serves many threads, so it keys all its state by thread id.
272
+ The install links the skills into `~/.config/amp/skills` and writes the
273
+ always-on block into `~/.config/amp/AGENTS.md`. By default Amp adds a
274
+ `Co-authored-by: Amp` and an `Amp-Thread-ID` trailer to its commits. The
275
+ install sets `amp.git.commit.coauthor.enabled` and
276
+ `amp.git.commit.ampThread.enabled` to false in `~/.config/amp/settings.json`,
277
+ or in `settings.jsonc` when that is the only one, unless the user set them.
278
+ It writes that file only while it is plain JSON. Install, uninstall and the
279
+ report resolve it the same way (`bin/tezgah-setup::amp_settings`). The
280
+ install names a file holding comments, leaves it alone and exits 0. It
281
+ refuses both files at once, or an unreadable one, and exits 1. Then the run
282
+ prints both keys to set by hand. The uninstall removes the plugin only when
283
+ it is a fresh render or starts with tezgah's header line
284
+ (`bin/tezgah-setup::amp_plugin_ours`). A trailer key still false goes back to
285
+ what the install found. Amp gets no subagent roles, MCP rows or orx shim from
286
+ tezgah.
129
287
 
130
288
  An adapter is responsible for its host's **envelope** and its **surface**: it
131
289
  translates the host's event and tool names into the shared vocabulary (one call
@@ -149,6 +307,14 @@ pays for it once.
149
307
  | opencode | no stdout-inject hook: `install_opencode` writes the generated `~/.config/tezgah/opencode-contract.md` and references it from `opencode.json` `instructions` |
150
308
  | omp | the managed block in `~/.omp/agent/RULES.md` |
151
309
  | dsh | the session's own hook context: `hosts/dsh/hooks.json` runs the Claude-family scripts through the bridge, and no dsh file carries the core, so dsh's `SessionStart` row declares nothing and still gets it here (`~/.dsh/cordis.patch.yml` mounts that bridge) |
310
+ | qwen | the managed block in `~/.qwen/QWEN.md`, which Qwen loads into every session; its `SessionStart` hook repeats the core only while that block is missing (`hosts/qwen/hook.py::rules_carry_core`) |
311
+ | droid | the managed block in `~/.factory/AGENTS.md`, which Droid loads as coding guidelines at startup; its `SessionStart` hook repeats the core only while that block is missing (`hosts/droid/hook.py::rules_carry_core`) |
312
+ | commandcode | the managed block in `~/.commandcode/AGENTS.md`, the user memory file Command Code puts in the system prompt; its `SessionStart` hook repeats the core only while that block is missing (`hosts/commandcode/hook.py::rules_carry_core`) |
313
+ | copilot | the managed block in `$COPILOT_HOME/copilot-instructions.md`, Copilot's personal instructions file, applied to every session; its `SessionStart` hook repeats the core only while that block is missing (`hosts/copilot/hook.py::rules_carry_core`) |
314
+ | gemini | the managed block in `~/.gemini/GEMINI.md`, the global context file Gemini puts in the system prompt and Antigravity reads too; its `SessionStart` hook repeats the core only while that block is missing (`hosts/gemini/hook.py::rules_carry_core`) |
315
+ | antigravity | the same managed block in `~/.gemini/GEMINI.md`, a standalone global rule Antigravity keeps always on (24,000-byte file limit); its `SessionStart` hook injects the core as a system message only while that block is missing (`hosts/antigravity/hook.py::rules_carry_core`) |
316
+ | goose | the managed block in `~/.config/goose/.goosehints`, the global hints file Goose adds to the system prompt of every request; no hook repeats it, because no Goose hook output reaches the model outside a block |
317
+ | amp | the managed block in `~/.config/amp/AGENTS.md`, which Amp always includes; the session brief on the thread's first turn repeats it only while that block is missing or `AMP_IGNORE_GUIDANCE_FILES` is set (`hosts/amp/hook.py::rules_carry_core`) |
152
318
 
153
319
  An installed host whose core line is missing is a health check's business
154
320
  (`host_checks_<host>`), not a second copy: two delivery paths for one host is the
@@ -167,11 +333,27 @@ and drops the later ones.
167
333
  | context | cursor | the `sessionStart` hook context, the repo's `AGENTS.md`, `.cursor/rules/*.mdc`, `.cursorrules` | the hook |
168
334
  | context | opencode | `~/.config/opencode/AGENTS.md`, `opencode.json` `instructions`, the repo's `AGENTS.md` | the contract and router files in `instructions` |
169
335
  | context | omp | `SYSTEM.md`, `APPEND_SYSTEM.md`, `RULES.md` (sticky), the repo's `.omp/AGENTS.md`, `AGENTS.md`, `CLAUDE.md`; another host's user files only with `enabledProviders` | block in `RULES.md`, the extension at session start |
336
+ | context | qwen | `~/.qwen/QWEN.md`, the repo's `QWEN.md` and `AGENTS.md`, then `.qwen/QWEN.local.md`; all load | block in the first |
337
+ | context | droid | from the cwd up to the git root, `AGENTS.md` (or `CLAUDE.md`) in each dir and its `.factory/`, `.agents/`, `.agent/`; then `~/.factory/AGENTS.md` and the other personal dirs; project files override personal ones | block in `~/.factory/AGENTS.md` |
338
+ | context | commandcode | `~/.commandcode/AGENTS.md`, the repo's `AGENTS.md` (else `.commandcode/AGENTS.md`), then each subdirectory's; all load | block in the first |
339
+ | context | copilot | `$COPILOT_HOME/copilot-instructions.md` and `instructions/`, then in the git root and the cwd `.github/copilot-instructions.md`, `.github/instructions/`, `AGENTS.md`, `CLAUDE.md`, `GEMINI.md` and `.claude/rules/`; all merge | block in the first |
340
+ | context | gemini | `~/.gemini/GEMINI.md`, then from the cwd up to the git root, then subdirectories (`context.fileName` renames the file); all concatenate | block in the first |
341
+ | context | antigravity | `~/.gemini/AGENTS.md` and `GEMINI.md`, `~/.gemini/config/rules/*.md`, `~/.gemini/antigravity-cli/rules/*.md`, then the workspace's `AGENTS.md`, `GEMINI.md` and `.agents/rules/`, and plugin rules; all combine, more specific rules win a conflict, and over 20,000 tokens the largest files become pointers | block in `~/.gemini/GEMINI.md` |
342
+ | context | goose | `CONTEXT_FILE_NAMES` (default `AGENTS.md`, `.goosehints`) in `~/.config/goose/`, then in the project root and each directory goose reads; all load, and a local hint wins over a global one | block in `~/.config/goose/.goosehints` |
343
+ | context | amp | `AGENTS.md` in the cwd and its parents, subtree files on a read, then `~/.config/amp/AGENTS.md` and `~/.config/AGENTS.md`; all load, and `AMP_IGNORE_GUIDANCE_FILES` skips matching files | block in `~/.config/amp/AGENTS.md` |
170
344
  | skills | omp | `skills.customDirectories`, then `.omp/skills` and `~/.omp/agent/skills`, Claude's project dir (user dir opt-in), `.agents/skills` and `~/.agents/skills`; first wins, identical copies collapse, a differing copy is renamed `<namespace>/<name>` | links in `~/.omp/agent/skills` |
171
345
  | skills | claude | `.claude/skills`, `~/.claude/skills`; plugin skills are namespaced `tezgah:<name>` | the plugin copy |
172
346
  | skills | codex | `.agents/skills` from the cwd up to the repo root, `~/.agents/skills`, `$CODEX_HOME/skills`; one name in two dirs is listed twice, not merged | links in `$CODEX_HOME/skills` |
173
- | skills | cursor | `.agents/skills`, `.cursor/skills`, `~/.agents/skills`, `~/.cursor/skills`, then Claude's and Codex's project and user dirs; no documented winner | links in `~/.cursor/skills` |
347
+ | skills | cursor | `.agents/skills`, `.cursor/skills`, `~/.agents/skills`, `~/.cursor/skills`, then Claude's and Codex's project and user dirs; no documented winner | links in `~/.cursor/skills`, only for the skills `~/.codex`, `~/.claude` or `~/.agents/skills` do not already link |
174
348
  | skills | opencode | its own skill tool is denied; the router lists `~/.config/opencode/skills`, `~/.claude/skills`, `~/.agents/skills`, first wins (`bin/tezgah-setup::skill_groups`) | links and the router |
349
+ | skills | qwen | `~/.qwen/skills`, `.qwen/skills`, extension and bundled skills; no documented winner | links in `~/.qwen/skills` |
350
+ | skills | droid | `.factory/skills` (project and folder), `~/.factory/skills`, `.agents/skills` and `~/.agents/skills`, plugin and built-in skills; no documented winner | links in `~/.factory/skills`, beside the user's own |
351
+ | skills | commandcode | `.commandcode/skills`, `.agents/skills`, `~/.commandcode/skills`, `~/.agents/skills`, extra locations, bundled; first wins | links in `~/.commandcode/skills` |
352
+ | skills | copilot | `.github/skills`, `.agents/skills`, `.claude/skills`, a parent's `.github/skills`, `$COPILOT_HOME/skills`, `~/.agents/skills`, plugins, `COPILOT_SKILLS_DIRS`, bundled; first wins | links in `$COPILOT_HOME/skills` |
353
+ | skills | gemini | workspace `.agents/skills` and `.gemini/skills`, user `~/.agents/skills` and `~/.gemini/skills`, extension skills, built-in; higher tier wins, `.agents/skills` wins within one | links in the extension's `skills/`, the lowest tier |
354
+ | skills | antigravity | the workspace's `.agents/skills`, `~/.gemini/antigravity-cli/skills`, plugin skills; no documented winner | links in `~/.gemini/antigravity-cli/skills` |
355
+ | skills | goose | `~/.agents/skills`, `.agents/skills`, plugin skills (namespaced `<plugin>:<name>`), then `.goose/skills`, `.claude/skills` and `~/.claude/skills` for backward compatibility; no documented winner | links in the plugin's `skills/`, loaded as `tezgah:<name>` |
356
+ | skills | amp | `~/.config/agents/skills`, `~/.agents/skills`, `~/.config/amp/skills`, `.agents/skills`, `.claude/skills`, `~/.claude/skills`, then plugin and configured paths; first name wins | links in `~/.config/amp/skills` |
175
357
  | agents | claude | managed, the `claude --agents` flag, `.claude/agents`, `~/.claude/agents`, plugin agents namespaced `plugin:name`; a dir absent at start is not watched | `~/.claude/agents`, written at install |
176
358
  | agents | codex | `$CODEX_HOME/agents`, `/etc/codex/agents`, `.codex/agents` (trusted repos only), read at config load | `$CODEX_HOME/agents`, written at install |
177
359
  | agents | cursor | `cursor-agent`: `.cursor/agents`, then `.claude/agents`, `.grok/agents`, plugin agents; first name wins; no user dir; read at process start. The editor's docs add `~/.cursor/agents` and `~/.claude/agents` | `.cursor/agents` per repo at session start, in the CLI's line format (`hooks/tezgah_agents.py::render_md_cursor`) |
@@ -181,7 +363,16 @@ and drops the later ones.
181
363
  | hooks | codex, cursor | `hooks.json` array order, then the repo's `.codex/` or `.cursor/hooks.json`; Codex runs a row only once its hash is trusted | own rows after Orca's |
182
364
  | hooks | opencode | every `*.js`/`*.ts` in `plugins/` and `plugin/`; `tezgah.js` is linked into both, and its `globalThis.__tezgahPluginLoaded` guard makes the second load return `{}` | the plugin |
183
365
  | hooks | omp | `~/.omp/agent/extensions/` first, then `config.yml` `extensions:`, one load per absolute path; per event in that order, and the first `block` ends a tool call | the bridge in `hooks/pre/` |
184
- | mcp | each | omp: project `.omp/mcp.json`, then `~/.omp/agent/mcp.json`, first name wins; claude: `~/.claude.json`, the repo's `.mcp.json`, the plugin's `.mcp.json`; codex: `$CODEX_HOME/config.toml`; cursor: `~/.cursor/mcp.json`; opencode: `opencode.json` `mcp` | one `tezgah` and one `codegraph` row per host |
366
+ | hooks | qwen | user `settings.json` rows (Orca's and tezgah's), then project `.qwen/settings.json` rows in trusted folders; groups run in parallel unless `sequential` | own rows after Orca's |
367
+ | hooks | droid | org-managed hooks, user `~/.factory/hooks.json`, project `.factory/hooks.json` and enabled plugins' `hooks/hooks.json`, merged with no documented order; a scope's settings.json `hooks` key counts only while its hooks.json is absent | own rows in hooks.json |
368
+ | hooks | commandcode | project `.commandcode/settings.json` rows, then user `~/.commandcode/settings.json` rows; PreToolUse runs them in order and stops at the first deny, the other events run in parallel | own rows in the user settings.json |
369
+ | hooks | copilot | policy files, then the repo's `.github/hooks/*.json`, user `$COPILOT_HOME/hooks/*.json`, the inline `hooks` of the repo's and the user's settings.json (and the repo's `.claude/settings.json`), then plugins; every entry runs, and any PreToolUse deny blocks | own file `hooks/tezgah.json` |
370
+ | hooks | gemini | project `.gemini/settings.json`, user `~/.gemini/settings.json`, system settings, then extensions' `hooks/hooks.json`; `hooksConfig.enabled: false` runs none | own extension `extensions/tezgah/hooks/hooks.json` |
371
+ | hooks | antigravity | workspace `.agents/hooks.json`, global `~/.gemini/config/hooks.json`, then installed plugins' `hooks.json`; each top-level name is a hook set, and `enabled: false` on a name loads none of it | the `tezgah` name in the global hooks.json |
372
+ | hooks | goose | user `~/.agents/plugins/<name>/hooks/hooks.json`, project `.agents/plugins/<name>/hooks/hooks.json` and installed plugins; a name in `disabledPlugins` of `~/.config/goose/settings.json` loads none of it; any PreToolUse block denies | own plugin `~/.agents/plugins/tezgah/hooks/hooks.json` |
373
+ | hooks | amp | plugins: personal and workspace ones from Amp's settings, the project's `.amp/plugins/`, the system `$XDG_CONFIG_HOME/amp/plugins/`; a project plugin wins over a system one of the same name, and every plugin gets every event in no defined order | the system plugin `tezgah.ts` |
374
+ | mcp | each | omp: project `.omp/mcp.json`, then `~/.omp/agent/mcp.json`, first name wins; claude: `~/.claude.json`, the repo's `.mcp.json`, the plugin's `.mcp.json`; codex: `$CODEX_HOME/config.toml`; cursor: `~/.cursor/mcp.json`; opencode: `opencode.json` `mcp`; qwen: `settings.json` `mcpServers`; droid: `~/.factory/mcp.json`; commandcode: `settings.json` `mcp.servers`, `~/.commandcode/mcp.json`, the repo's `.mcp.json`; copilot: `$COPILOT_HOME/mcp-config.json`, the repo's `.mcp.json` and `.github/mcp.json`, project first; gemini: `settings.json` `mcpServers` (user, project) and each extension's `gemini-extension.json`, settings.json first; antigravity: `~/.gemini/config/mcp_config.json`, the workspace's `.agents/mcp_config.json` and plugins, the running server keeps a shared name; goose: the `extensions:` map of `~/.config/goose/config.yaml`; tezgah writes no row for qwen, droid, commandcode, copilot, gemini, antigravity or goose yet | one `tezgah` and one `codegraph` row per host but qwen, droid, commandcode, copilot, gemini, antigravity and goose |
375
+ | mcp | amp | `amp.mcpServers` in `~/.config/amp/settings.json`, then the workspace's `.amp/settings.json`, which wins | none: tezgah writes no MCP row for Amp |
185
376
 
186
377
  Orca is a launcher, not a host. It starts omp with its own `--extension`. It
187
378
  gives each Codex account its own `CODEX_HOME` under
@@ -224,6 +415,14 @@ three harness configurations while swapping the model inside one harness moved i
224
415
  | opencode | a selector read from `opencode models` (`hooks/tezgah_models.py:12`) | the AI SDK: `@ai-sdk/openai-compatible` for `/v1/chat/completions`, `@ai-sdk/openai` for `/v1/responses`, beside its own Anthropic and Bedrock providers (opencode's provider docs) | vendor documentation |
225
416
  | omp | the session default's family, otherwise `task.agentModelOverrides` and `modelRoles.plan` (`hooks/tezgah_models.py:13-16`) | whichever provider the selected model names - Anthropic Messages, OpenAI Responses, OpenAI chat completions, Google, Bedrock - with text-based dialects where a native tool API is unavailable; the OpenRouter route dispatches to Responses unless `PI_OPENROUTER_RESPONSES=0` (omp's own `provider-compat-reference` documentation) | the tool's own documentation |
226
417
  | dsh | the Claude-family scripts through the bridge (`hosts/dsh/hooks.json:2-18`); `README.md:27-28` calls it "DeepSeek's dsh harness" | `unverified`: its own `--help` describes it as "an ordered stack of plugin-bundle patch layers" and names no model API, and its profile directory names a provider (DeepSeek) rather than a wire (checked 2026-10-01) | - |
418
+ | qwen | not named: tezgah writes no Qwen subagent roles | the provider `settings.json` `modelProviders` selects, keyed by auth type - `openai` (OpenAI-compatible), `anthropic`, `gemini`, `vertex-ai` - each with its own `baseUrl` (Qwen Code 0.21.13 `configuration/model-providers.md`) | the tool's own documentation |
419
+ | droid | not named: tezgah writes no Droid subagent roles | `unverified`: tezgah has not mapped Droid's providers to a wire | - |
420
+ | commandcode | not named: tezgah writes no Command Code subagent roles | `unverified`: tezgah has not mapped Command Code's providers to a wire | - |
421
+ | copilot | not named: tezgah writes no Copilot subagent roles | `unverified`: tezgah has not mapped Copilot's providers to a wire | - |
422
+ | gemini | not named: tezgah writes no Gemini subagent roles | `unverified`: tezgah has not mapped Gemini's providers to a wire | - |
423
+ | antigravity | not named: tezgah writes no Antigravity subagent roles | `unverified`: tezgah has not mapped Antigravity's models to a wire | - |
424
+ | goose | not named: tezgah writes no Goose subagent roles | `unverified`: tezgah has not mapped Goose's providers to a wire | - |
425
+ | amp | not named: tezgah writes no Amp subagent roles | `unverified`: tezgah has not mapped Amp's models to a wire | - |
227
426
 
228
427
  `unverified` is the point of the column rather than a gap in it. A wrong dialect
229
428
  here is worse than a blank one: a reader would use it to explain a score, and the
@@ -234,15 +433,16 @@ and say which document it came from.
234
433
 
235
434
  A skill read is observable only where the host gives a channel that does not cost
236
435
  a process per read: Claude parses its transcript (`statusline.py::claude_used`),
237
- opencode classifies in process (`hosts/opencode/plugins/tezgah.js:2227-2263`),
436
+ opencode classifies in process (`classify` in `hosts/opencode/plugins/tezgah.js`),
238
437
  omp filters in its embedded runner before it asks python
239
- (`hosts/omp/tezgah-hook.ts.in:257-264`). On codex, cursor and dsh a read is not
240
- observable at that price, so their surfaces pass the five tool-use measures as
241
- `observable` and the two skill marks render dim (`info`, no glyph) instead of
438
+ (`skillRead` in `hosts/omp/tezgah-hook.ts.in`), and Amp's plugin does the same for a
439
+ `Read` of a shipped skill (`hosts/amp/tezgah-plugin.ts.in`). On codex, cursor, qwen, droid, commandcode, copilot, gemini, antigravity, goose and dsh a read is not
440
+ observable at that price. Their surfaces pass the five tool-use measures as
441
+ `observable`, and the two skill marks render dim (`info`, no glyph) instead of
242
442
  claiming the skill was never opened (`hooks/tezgah_context.py::TOOL_USE_MEASURES`, and the
243
443
  `observable` branch at `hooks/tezgah_context.py::health_segments`). Callers that pass `TOOL_USE_MEASURES`:
244
- `statusline.py::observable`, `hosts/codex/hook.py::main`,
245
- `hosts/dsh/statusline/lib/index.js:16-19`. A kill switch is observable everywhere
444
+ `statusline.py::observable`, `hosts/codex/hook.py::main`, `hosts/commandcode/hook.py::status`,
445
+ `OBSERVABLE` in `hosts/dsh/statusline/lib/index.js`. A kill switch is observable everywhere
246
446
  and still renders `off` (`hooks/tezgah_context.py::health_segments`).
247
447
 
248
448
  ## Adding a host
@@ -252,7 +452,7 @@ In order, each step verified by the one below it:
252
452
  1. `HOST_DIRS` and, when the CLI can exist without a config dir, `HOST_BINS` in
253
453
  `hooks/tezgah_paths.py::HOST_DIRS`, `hooks/tezgah_paths.py::HOST_BINS`. These are shared by the installer, the report
254
454
  and agent generation, so "is this host installed?" has one answer
255
- (`hooks/tezgah_paths.py:66-69`). Add the name to `ALL_HOSTS` in the report
455
+ (`hooks/tezgah_paths.py::host_installed`). Add the name to `ALL_HOSTS` in the report
256
456
  order (`bin/tezgah-setup::ALL_HOSTS`).
257
457
  2. The hook adapter(s) under `hosts/<host>/` (an `install_*`-independent
258
458
  directory like `hosts/codex/hook.py`, `hosts/cursor/hook.py`) plus, for a
@@ -260,21 +460,37 @@ In order, each step verified by the one below it:
260
460
  (`hosts/omp/tezgah-hook.ts.in` rendered at `hooks/tezgah_attest.py::omp_bridge`).
261
461
  3. `install_<host>()` writing that host's files, registered in `INSTALLERS`
262
462
  (`bin/tezgah-setup::INSTALLERS`), and `uninstall_<host>()` removing only
263
- tezgah-managed links and blocks (`bin/tezgah-setup:3331-3463`).
463
+ tezgah-managed links and blocks, registered in `UNINSTALLERS` (`bin/tezgah-setup::UNINSTALLERS`).
264
464
  4. `host_checks_<host>()` returning `(label, bool)` rows over what was actually
265
465
  written, registered in `HOST_CHECKS` (`bin/tezgah-setup::HOST_CHECKS`); the
266
466
  `--report` output is that list (`bin/tezgah-setup::report`). Give the row a home-qualified
267
467
  label if the host's dir can be relocated (`host_checks_codex`, `bin/tezgah-setup::host_checks_codex`).
468
+ The first row is the host's "wired" row (`bin/tezgah-setup::hosts_row`), and a
469
+ row whose MISS means unarmed names a label `ARMING_ROWS` carries
470
+ (`bin/tezgah-setup::ARMING_ROWS`).
471
+ Every other table keyed by host grows with it, or the host crashes or drops
472
+ out there. `hooks/tezgah_stack.py::host_stack` raises on an unknown host, so
473
+ `tezgah-doctor --stack` fails. The other tables are
474
+ `hooks/tezgah_stack.py::tezgah_skill_dirs`, `hooks/tezgah_attest.py::registration`
475
+ and `CODE_TARGETS`, `bin/tezgah-setup::hook_entries` and
476
+ `bin/tezgah-setup::live_plan`. Without `live_plan` the live rows read
477
+ UNVERIFIED. The gate's control plane grows too: `HOOK_ENTRIES`, `HOOK_HOSTS`
478
+ and the shared files of `hooks/tezgah_gate.py::_registration`. The gate then
479
+ refuses a forged run of the new hook, and an edit of its registration, as it
480
+ refuses the others.
268
481
  5. Decide the surface: a `statusLine` command, a TUI/widget plugin, or the
269
482
  `systemMessage` fallback (`hosts/codex/hook.py:10-12`). Pass `observable` if
270
483
  the host cannot see a skill read, and `idx_override` on any redraw that must
271
484
  not fork git for a cosmetic glyph (`hooks/tezgah_context.py::health_segments`).
272
485
  6. Tests, in two tiers: a per-host class in `tests/test_setup.py` pinning the
273
- report rows and the written files (e.g. `OmpHost:960-1030`,
274
- `CodexHome:657-702`, `CursorMatcher:705-722`, `DshStatusline:872-957`), and an
486
+ report rows and the written files (e.g. `tests/test_setup.py::OmpHost`,
487
+ `tests/test_setup.py::CodexHome`, `tests/test_setup.py::CursorMatcher`,
488
+ `tests/test_setup.py::DshStatusline`), and an
275
489
  opt-in script under `tests/e2e_*.py` for the thing only a real host can prove
276
490
  - not collected by `unittest discover`, it exits 0 with `SKIP` when the host
277
- is absent (`tests/e2e_omp_statusline.py:13-16`).
491
+ is absent (`tests/e2e_omp_statusline.py:13-16`). A script that calls a model
492
+ also exits 0 with `SKIP` unless `TEZGAH_E2E_SPEND=1` is set
493
+ (`tests/e2e_qwen.py`, `tests/e2e_droid.py`).
278
494
 
279
495
  ## Host-specific limits worth knowing
280
496
 
@@ -287,6 +503,26 @@ In order, each step verified by the one below it:
287
503
  `gate✗` appears only in the SessionStart/Stop status message.
288
504
  - **cursor, dsh:** observed only if their prompt payload carries `transcript_path`
289
505
  (unverified on a live host).
506
+ - **qwen:** unverified. Its prompt payload carries `transcript_path`. The counter
507
+ reads only the Claude, omp and Codex row shapes (`hooks/tezgah_context.py::_transcript_calls`).
508
+ No run has captured Qwen's transcript format yet, so a disarmed gate there likely counts 0.
509
+ - **droid:** unverified on a live host. Its transcript rows carry `message.content`
510
+ `tool_use` blocks with a `timestamp`, the Claude shape the counter reads, and
511
+ `GATED_TOOLS` names `Execute`, `Create` and `ApplyPatch`
512
+ (`hooks/tezgah_context.py::GATED_TOOLS`).
513
+ - **commandcode:** unavailable: Command Code has no prompt event, so nothing runs the
514
+ counter there.
515
+ - **copilot:** unavailable: Copilot drops a command hook's UserPromptSubmit output,
516
+ so no reminder carries the counter's warning there.
517
+ - **gemini:** unverified. BeforeAgent carries `transcript_path`, but Gemini's
518
+ session file is a format the counter does not read
519
+ (`hooks/tezgah_context.py::_transcript_calls`), so a disarmed gate there likely counts 0.
520
+ - **antigravity:** unavailable: its `transcript.jsonl` is not a format the counter
521
+ reads, so the hook does not hand it over. Only a switch armed mid-session shows.
522
+ - **goose:** unavailable: Goose ignores UserPromptSubmit output, so no reminder
523
+ carries the counter's warning there.
524
+ - **amp:** unavailable: the plugin hands the hook no transcript path, so only a
525
+ switch armed mid-session shows.
290
526
  - **omp, opencode:** not applicable: the prompt hook and the gate live in one extension or
291
527
  plugin process, so one cannot run without the other.
292
528
  - Everywhere: a disarmed PreToolUse beside a working PostToolUse writes rows and is not
@@ -313,7 +549,7 @@ In order, each step verified by the one below it:
313
549
  (`hooks/projects-auto-init.py:28-38`, `hosts/codex/hook.py::main`).
314
550
  - **opencode**: `experimental.session.compacting` carries the builder's
315
551
  post-compact block, so the contract survives the summary
316
- (`hosts/opencode/plugins/tezgah.js:39-40`). It hands the builder the
552
+ (the `experimental.session.compacting` hook of `Tezgah` in `hosts/opencode/plugins/tezgah.js`). It hands the builder the
317
553
  session id. The builder then forgets the lessons and armed paragraphs
318
554
  that session saw, and restates its pinned user constraints. The call
319
555
  used to send `{}`, which keyed nothing on the session.
@@ -323,7 +559,7 @@ In order, each step verified by the one below it:
323
559
  `hosts/omp/hook.py` for `{"event": "post_compact"}`; the block is sent with
324
560
  `pi.sendMessage(..., {deliverAs: "nextTurn"})`, and the summary rides as
325
561
  `compact_summary` so `remember_compaction` writes the compaction row
326
- (`hosts/omp/tezgah-hook.ts.in:647-664`, `hosts/omp/hook.py::handle`).
562
+ (the `session_compact` handler in `hosts/omp/tezgah-hook.ts.in`, `hosts/omp/hook.py::handle`).
327
563
  Subagent sessions are skipped, because the post-compact block starts the
328
564
  indexer.
329
565
  - **cursor**: not re-injected - a limitation. Cursor's only compaction hook,
@@ -347,7 +583,7 @@ In order, each step verified by the one below it:
347
583
  SubagentStop and Stop, and silently drops anything else - which is why
348
584
  `configPath` names `hosts/dsh/hooks.json` and not the Claude manifest
349
585
  (`bin/tezgah-setup::dsh_patch_block`). Its status route goes through `tezgah-status`
350
- with the five tool-use measures (`hosts/dsh/statusline/lib/index.js:16-19`), it
586
+ with the five tool-use measures (`OBSERVABLE` in `hosts/dsh/statusline/lib/index.js`), it
351
587
  declares `TEZGAH_CALL_OUTCOME=none` because the bridge drops the outcome field
352
588
  (`hosts/dsh/hooks.json:16-18`, `hooks/projects-posttooluse.py::main`), and its
353
589
  statusline row lives in the web profile because the plugin injects the
@@ -357,9 +593,9 @@ In order, each step verified by the one below it:
357
593
  `hosts/dsh/hooks.json` until the bridge carries outcomes.
358
594
  - **opencode carries the untrusted-content control in the plugin**, not by
359
595
  importing `hooks/tezgah_untrusted.py`, which a plugin cannot do in process: the
360
- channel is decided in JS (`untrustedSource` `hosts/opencode/plugins/tezgah.js:1942`,
361
- the tier's argv reader at `:1902-1911`), the label and the taint notice ride the
362
- result the hook is handed (`labelResult` `:1993`). A shared corpus in
596
+ channel is decided in JS (`untrustedSource` in `hosts/opencode/plugins/tezgah.js`,
597
+ the tier's argv reader `tierRead`), the label and the taint notice ride the
598
+ result the hook is handed (`labelResult`). A shared corpus in
363
599
  `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
364
600
  fails if their answers differ, so neither can move without the other.
365
601
  - **omp carries both untrusted-content marks through the shared core.** Its
@@ -391,20 +627,20 @@ In order, each step verified by the one below it:
391
627
  - **opencode asks the core for the rules it cannot port, and counts the ask.** The
392
628
  task rule's phase and allowlist and the language rule's word list are answered by
393
629
  `bin/tezgah-gate decide` (the recording verb; `check` is the dry run) rather than copied into JavaScript (`IDENT_CMD`
394
- `hosts/opencode/plugins/tezgah.js:585`, the call site `:2492-2495`) - one spawn per write
630
+ in `hosts/opencode/plugins/tezgah.js`, tested in the `tool.execute.before` hook) - one spawn per write
395
631
  and per identifier-creating command. The delegation fails open by design: a missing
396
- CLI, a non-zero exit, a broken pipe or a 10 s timeout refuses nothing (`collect`
397
- `hosts/opencode/plugins/tezgah.js:152-178`). One `delegation` row now records the class when the core could not
398
- answer (`noteDelegation` `:2304-2310`), so a silent fail-open is countable instead
632
+ CLI, a non-zero exit, a broken pipe or a 10 s timeout refuses nothing
633
+ (`collect`). One `delegation` row now records the class when the core could not
634
+ answer (`noteDelegation`), so a silent fail-open is countable instead
399
635
  of invisible.
400
636
  - **opencode has no session-start hook**, so its always-on file must carry the
401
637
  pointer every hook host appends, and the per-session index, agent and contract
402
638
  refreshes ride the first `chat.message` instead (`bin/tezgah-setup::refresh_contract`,
403
- `hosts/opencode/plugins/tezgah.js:2686-2701`). It denies the native `skill`
639
+ the `oncePerSession` checks in the `chat.message` hook of `hosts/opencode/plugins/tezgah.js`). It denies the native `skill`
404
640
  tool - the generated router replaces the injected skill list
405
641
  (`bin/tezgah-setup::install_opencode`) - and `permission.ask` may never be emitted by a
406
642
  given build, which is why `tool.execute.before` stays the enforcing half
407
- (`hosts/opencode/plugins/tezgah.js:11-14`).
643
+ (the header comment of `hosts/opencode/plugins/tezgah.js`).
408
644
  - **Cursor only runs a PreToolUse hook for the tool types its matcher names**, so
409
645
  the matcher must list the write spellings or the gate's edit branches are
410
646
  unreachable there (`hosts/cursor/hook.py:33-42`,
@@ -414,7 +650,7 @@ In order, each step verified by the one below it:
414
650
  longer. A busy turn with every conditional rule armed measured 10288.
415
651
  `fit()` cuts the tail to `CONTEXT_MAX` and marks the cut, so the reminder and
416
652
  the first rules arrive (`hosts/cursor/hook.py::fit`, `hosts/cursor/hook.py::CONTEXT_MAX`).
417
- - **Claude, dsh, omp, codex and cursor run the gate only for the tool names
653
+ - **Claude, dsh, omp, codex, cursor, qwen, droid, commandcode, copilot, gemini, antigravity, goose and amp run the gate only for the tool names
418
654
  their matcher carries**, so a name the PostToolUse side carries and the
419
655
  PreToolUse side does not is recorded in the ledger and never refused.
420
656
  `PowerShell` and `pwsh` are both `BASH_TOOLS` members
@@ -425,14 +661,65 @@ In order, each step verified by the one below it:
425
661
  `hosts/cursor/hooks.json:8` - neither named them until 2026-09-19, so a
426
662
  PowerShell call on those two reached no hook at all while the shared rules
427
663
  already knew the spelling), and omp's `GATED` list
428
- (`hosts/omp/tezgah-hook.ts.in:270-273`). A host this list does not cover is a
429
- host whose shell the gate cannot see.
664
+ (`GATED` in `hosts/omp/tezgah-hook.ts.in`). Qwen Code has two shell tool ids,
665
+ `run_shell_command` and `monitor`, which `bin/tezgah-setup::QWEN_PRETOOL_MATCHER`
666
+ names beside `notebook_edit` and `mcp__.*`. Claude's and dsh's `Monitor` runs a
667
+ shell command too, so their matcher names it. Droid's shell is `Execute`, which `bin/tezgah-setup::DROID_PRETOOL_MATCHER`
668
+ names beside `Create`, `Edit`, `ApplyPatch`, `Task`, `Grep` and `mcp__.*`. The
669
+ adapter hands ApplyPatch's `input` to the gate as `patch`
670
+ (`hosts/droid/hook.py::gate_input`). Command Code's settings hooks see only
671
+ four tools and test the matcher against their display names (`SHELL`, `READ`,
672
+ `WRITE`, `EDIT`), so `bin/tezgah-setup::COMMANDCODE_PRETOOL_MATCHER` is
673
+ `shell|write|edit`. Its read, MCP, subagent and web tools never reach the
674
+ gate, and plan mode skips every hook. The adapter folds a shell call's `args`
675
+ into `command` and hands an edit's `old_value`/`new_value` over as
676
+ `old_string`/`new_string` (`hosts/commandcode/hook.py::gate_input`). Copilot's
677
+ PascalCase PreToolUse reports Claude tool names and anchors its matcher. So
678
+ `bin/tezgah-setup::COPILOT_PRETOOL_MATCHER` names `Bash`, `Write`, `Edit`,
679
+ `Agent`, `Task` and `Grep`, plus `.+-.+` for an MCP tool's `<server>-<tool>`.
680
+ The adapter maps the runtime spellings too (`bash`, `powershell`, `create`,
681
+ `edit`, `apply_patch`, `task`, `grep`, `rg`). An Edit that names no file but
682
+ carries a patch reaches the gate as `apply_patch`. `<server>-<tool>` becomes
683
+ `mcp__<server>__<tool>` for a server `mcp-config.json` names
684
+ (`hosts/copilot/hook.py::gate_name`). Gemini's BeforeTool matcher is a regex
685
+ over the tool name. `bin/tezgah-setup::GEMINI_PRETOOL_MATCHER` names the
686
+ shell, write, edit and grep tools, the subagent tools
687
+ (`codebase_investigator`, `generalist`, `invoke_agent`) and `mcp_.*`. A user's own subagents are not
688
+ named. `mcp_<server>_<tool>` marks no boundary between server and tool, so it
689
+ reaches the gate as `mcp__<server>_<tool>` (`hosts/gemini/hook.py::gate_name`).
690
+ Antigravity's matcher is a regex over the tool name, and
691
+ `bin/tezgah-setup::ANTIGRAVITY_PRETOOL_MATCHER` names `run_command`,
692
+ `write_to_file`, `replace_file_content`, `multi_replace_file_content`,
693
+ `invoke_subagent` and `grep_search`. The adapter renames their PascalCase
694
+ arguments into the gate's fields (`hosts/antigravity/hook.py::gate_input`).
695
+ `CommandLine` becomes `command`, `TargetFile` and `CodeContent` become
696
+ `file_path` and `content`. `TargetContent` and `ReplacementContent` become
697
+ `old_string` and `new_string`, and `ReplacementChunks` becomes `edits`. The
698
+ docs name no MCP tool spelling, so the matcher names no MCP tool. It names
699
+ neither `manage_task`'s `send_input` nor `schedule`.
700
+ Goose tests a matcher unanchored, so `bin/tezgah-setup::GOOSE_PRETOOL_MATCHER`
701
+ anchors `shell`, `write`, `edit` and `.+__.+`, an extension's
702
+ `<extension>__<tool>`, which reaches the gate as `mcp__<extension>__<tool>`
703
+ (`hosts/goose/hook.py::gate_name`). The adapter hands `write`'s and `edit`'s
704
+ `path` over as `file_path`, and `edit`'s `before`/`after` as
705
+ `old_string`/`new_string` (`hosts/goose/hook.py::gate_input`). Goose's own
706
+ `developer`, `analyze`, `summon` and `code_execution` tools other than these
707
+ three, and its subagent tool, are not named.
708
+ Amp's plugin sends the gate `Bash`, `shell_command`, `edit_file`,
709
+ `create_file`, `apply_patch` and `Task` (its `GATED` list) and an MCP effect
710
+ name. A shell call's text reaches the gate as `command`, from Amp's
711
+ `shellCommandFromToolCall` helper or the input's `cmd`
712
+ (`hosts/amp/hook.py::gate_input`). Read, search, web and `code_exec` calls
713
+ are not gated, and an MCP tool reached through `code_exec` or `tool_search`
714
+ is never seen.
715
+ A host this list does not cover is a host whose shell the gate cannot see.
430
716
  - **An MCP effect reaches the gate on every host that can route it**. Claude
431
717
  and dsh run it through a PreToolUse group of its own in `hooks/hooks.json` and
432
718
  `hosts/dsh/hooks.json`. That group stays apart from the literal one, because
433
719
  one regex character would end its exact-name dialect. Its pattern names the
434
720
  effect verbs of `hooks/tezgah_integrity.py::MCP_VERBS`, and omp's `tool_call`
435
- tests the same pattern (`MCP_EFFECT`, `hosts/omp/tezgah-hook.ts.in`). Both
721
+ tests the same pattern (`MCP_EFFECT`, `hosts/omp/tezgah-hook.ts.in`), as does
722
+ Amp's `tool.call` (`hosts/amp/tezgah-plugin.ts.in`). Both
436
723
  hosts read a matcher as an unanchored JavaScript `RegExp`, and the pattern
437
724
  uses no lookahead. A read-only MCP call, such as a code-graph query, spawns no
438
725
  hook there. Codex's matcher carries `mcp__.*` (`hosts/codex/hooks.json`).
@@ -446,7 +733,7 @@ In order, each step verified by the one below it:
446
733
  - **omp spawns the MCP `command` as one executable** and takes the rest in
447
734
  `args`; the whole argv in `command` fails with ENOENT (`bin/tezgah-setup::install_omp`),
448
735
  and `setStatus` strips ANSI, so only the widget path keeps the per-mark colors
449
- (`hosts/omp/tezgah-hook.ts.in:170-173`).
736
+ (the comment above `draw` in `hosts/omp/tezgah-hook.ts.in`).
450
737
  - **omp drops browser MCP servers while its native browser is on**: a server
451
738
  named `playwright`/`puppeteer`/... or running `@playwright/mcp`/... never
452
739
  connects there (omp 18.2.11 `/mcp` listed codegraph, mobile-mcp and tezgah
@@ -465,6 +752,14 @@ In order, each step verified by the one below it:
465
752
  - `hosts/dsh/hooks.json`, `hosts/dsh/statusline/lib/index.js`
466
753
  - `hosts/omp/hook.py`, `hosts/omp/tezgah-hook.ts.in`
467
754
  - `hosts/opencode/plugins/tezgah.js`, `hosts/opencode/tui/tezgah-tui.tsx`
755
+ - `hosts/qwen/hook.py`, `tests/e2e_qwen.py` (the opt-in run of the real binary)
756
+ - `hosts/droid/hook.py`, `tests/e2e_droid.py` (the opt-in run of the real binary)
757
+ - `hosts/commandcode/hook.py`
758
+ - `hosts/copilot/hook.py`
759
+ - `hosts/gemini/hook.py`
760
+ - `hosts/antigravity/hook.py`
761
+ - `hosts/goose/hook.py`
762
+ - `hosts/amp/hook.py`, `hosts/amp/tezgah-plugin.ts.in`
468
763
  - `statusline.py`, `bin/tezgah-status`, `hooks/tezgah_context.py`
469
764
  - `hooks/tezgah_models.py` - the per-host model surface in `## The model wire`
470
765
  - `hooks/tezgah_stack.py`, `bin/tezgah-doctor` `--stack` - the order in `## Precedence`