@r1z4x/tezgah 1.13.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 (93) hide show
  1. package/CHANGELOG.md +175 -1
  2. package/MANIFEST +18 -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 +15 -8
  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 +157 -41
  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 +12 -6
  27. package/bin/tezgah-rollback +3 -0
  28. package/bin/tezgah-route +1 -1
  29. package/bin/tezgah-setup +596 -157
  30. package/bin/tezgah-status +11 -1
  31. package/bin/tezgah-task +3 -0
  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 +3 -3
  38. package/docs/contract.md +77 -31
  39. package/docs/environment.md +58 -24
  40. package/docs/evidence.md +49 -19
  41. package/docs/gate.md +71 -21
  42. package/docs/hosts.md +65 -52
  43. package/docs/layers.md +1 -1
  44. package/docs/operations.md +67 -24
  45. package/docs/research.md +1 -1
  46. package/docs/skills.md +31 -8
  47. package/docs/status-line.md +33 -23
  48. package/docs/testing.md +48 -17
  49. package/docs/vision.md +6 -2
  50. package/hooks/lang/en.json +5 -2
  51. package/hooks/lang/tr.json +3 -2
  52. package/hooks/projects-posttooluse.py +11 -3
  53. package/hooks/projects-stop.py +1 -1
  54. package/hooks/tezgah_context.py +40 -23
  55. package/hooks/tezgah_gate.py +404 -106
  56. package/hooks/tezgah_integrity.py +367 -97
  57. package/hooks/tezgah_lessons.py +5 -2
  58. package/hooks/tezgah_paths.py +17 -6
  59. package/hooks/tezgah_policy.py +13 -5
  60. package/hooks/tezgah_skill_pick.py +1 -1
  61. package/hooks/tezgah_snapshot.py +6 -3
  62. package/hooks/tezgah_stopspec.py +2 -1
  63. package/hosts/amp/hook.py +1 -1
  64. package/hosts/amp/tezgah-plugin.ts.in +13 -3
  65. package/hosts/antigravity/hook.py +1 -1
  66. package/hosts/codex/hook.py +1 -1
  67. package/hosts/commandcode/hook.py +1 -1
  68. package/hosts/copilot/hook.py +1 -1
  69. package/hosts/cursor/hook.py +2 -1
  70. package/hosts/droid/hook.py +1 -1
  71. package/hosts/gemini/hook.py +1 -1
  72. package/hosts/goose/hook.py +1 -1
  73. package/hosts/omp/hook.py +1 -1
  74. package/hosts/omp/tezgah-hook.ts.in +127 -35
  75. package/hosts/opencode/plugins/tezgah.js +209 -63
  76. package/hosts/qwen/hook.py +1 -1
  77. package/output-styles/tezgah.md +1 -1
  78. package/package.json +6 -3
  79. package/packaging/build.sh +4 -4
  80. package/packaging/install.sh +11 -0
  81. package/packaging/upgrade.sh +3 -0
  82. package/skills/ai-research/LICENSE +21 -0
  83. package/skills/design-contract/SKILL.md +27 -6
  84. package/skills/design-library/LICENSE +21 -0
  85. package/skills/plan-add/render_table.py +7 -0
  86. package/skills/pm-frameworks/LICENSE +21 -0
  87. package/skills/rl-env/LICENSE +201 -0
  88. package/skills/tezgah-contract/SKILL.md +2 -2
  89. package/third_party/open-code-review/LICENSE +201 -0
  90. package/third_party/open-code-review/README.md +15 -0
  91. package/third_party/open-code-review/prompts/main_task_system.md +26 -0
  92. package/third_party/open-code-review/prompts/review_filter_task_system.md +13 -0
  93. package/workflows/graph-map.js +14 -5
package/docs/hosts.md CHANGED
@@ -17,12 +17,12 @@ 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
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
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
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`) |
@@ -44,19 +44,21 @@ What each host can enforce, in the
44
44
  [enforcement capability](glossary.md#enforcement-capability) words. `gate`: the
45
45
  hook can refuse. `observe`: it records and never refuses. `partial`: only part
46
46
  of the surface fires. `unavailable`: the host has no event for it. A cell gets
47
- `unverified` when no driver on this machine can probe it. Fixture payloads test
48
- 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.
49
51
 
50
52
  | Host | Gate (PreToolUse) | Stop | Subagent |
51
53
  |---|---|---|---|
52
- | 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) |
53
- | 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 |
54
- | 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 |
55
- | opencode | gate, probed by `--report --live` through a node driver | unavailable: the plugin API has no end-of-turn event | unavailable |
56
- | 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 |
57
- | 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 |
58
- | qwen | gate, probed by `--report --live` through the hook command in `~/.qwen/settings.json`; that the qwen binary obeys the deny is 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 |
59
- | droid | gate, probed by `--report --live` through the hook command in `~/.factory/hooks.json`; that the droid binary obeys the deny is 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 |
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 |
60
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 |
61
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 |
62
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` |
@@ -120,7 +122,8 @@ shared config, contract hash and the CLI symlinks every host shell can call
120
122
  (`bin/tezgah-setup::write_user_agents`).
121
123
  - **cursor** - `install_cursor` (`bin/tezgah-setup::install_cursor`): `~/.cursor/hooks.json`
122
124
  (tezgah entries replaced, a user's Orca entries merged), `skills/*` symlinks,
123
- `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`).
124
127
  - **opencode** - `install_opencode` (`bin/tezgah-setup::install_opencode`): the plugin under
125
128
  both `plugins/` and `plugin/` (version drift), `skills/*` symlinks, the
126
129
  generated `~/.config/tezgah/opencode-contract.md` (`bin/tezgah-setup::opencode_contract`) and
@@ -160,7 +163,8 @@ shared config, contract hash and the CLI symlinks every host shell can call
160
163
  `~/.qwen/settings.json` (or `$QWEN_HOME`). It adds one hook group per event of
161
164
  `QWEN_HOOK_EVENTS`, each `timeout` in milliseconds. The PreToolUse group
162
165
  carries `QWEN_PRETOOL_MATCHER` over Qwen's runtime tool ids. It also sets
163
- `ui.statusLine`. Orca's groups and the provider keys in that file stay, and
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
164
168
  the backup is owner-only (`bin/tezgah-setup::backup`). The install links
165
169
  `skills/*` into `~/.qwen/skills`. It writes the always-on block in
166
170
  `~/.qwen/QWEN.md`. tezgah writes no subagent roles, MCP rows or orx shim for
@@ -175,7 +179,9 @@ shared config, contract hash and the CLI symlinks every host shell can call
175
179
  comments, so the install edits it in place and keeps them
176
180
  (`bin/tezgah-setup::jsonc_put`). It adds the `statusLine`. It sets
177
181
  `includeCoAuthoredByDroid` to false, the switch for Droid's co-author trailer.
178
- A file it cannot edit safely is left alone and the run names both keys. The install
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
179
185
  links `skills/*` into `~/.factory/skills` beside the user's own links. It
180
186
  writes the always-on block in `~/.factory/AGENTS.md`. The uninstall deletes a
181
187
  hooks.json that held only tezgah's groups. An empty one would still hide
@@ -188,8 +194,8 @@ shared config, contract hash and the CLI symlinks every host shell can call
188
194
  carries `COMMANDCODE_PRETOOL_MATCHER`, which Command Code tests against the
189
195
  tool's display name. The same write sets `attribution.commit` to `""`. That
190
196
  turns off the default `Co-authored-by: CommandCodeBot` commit trailer. A value
191
- the user set stays. The uninstall removes the key only while it holds what the
192
- install wrote. The install links `skills/*` into `~/.commandcode/skills`. It
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
193
199
  writes the always-on block in `~/.commandcode/AGENTS.md`. tezgah writes no
194
200
  subagent roles, MCP rows or orx shim for Command Code yet.
195
201
  - **copilot** - `install_copilot` (`bin/tezgah-setup::install_copilot`) writes
@@ -200,12 +206,14 @@ shared config, contract hash and the CLI symlinks every host shell can call
200
206
  there rejects every hook in it. The PreToolUse entry carries
201
207
  `COPILOT_PRETOOL_MATCHER`. settings.json is JSON with comments. So the status
202
208
  line and `includeCoAuthoredBy: false` go in only while it parses as plain
203
- JSON. That setting turns off the default `Co-authored-by` trailer on the
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
204
212
  agent's commits. A value the user set stays. The install links `skills/*`
205
213
  into `$COPILOT_HOME/skills` and writes the always-on block in
206
- `$COPILOT_HOME/copilot-instructions.md`. The uninstall deletes tezgah.json
207
- and takes back only tezgah's settings keys. tezgah writes no subagent roles,
208
- MCP rows or orx shim for Copilot yet.
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.
209
217
  - **gemini** - `install_gemini` (`bin/tezgah-setup::install_gemini`) writes
210
218
  tezgah's own extension, `~/.gemini/extensions/tezgah/`, which is wholly
211
219
  tezgah's. `GEMINI_CLI_HOME` moves `~/.gemini` under it. The dir holds
@@ -227,12 +235,13 @@ shared config, contract hash and the CLI symlinks every host shell can call
227
235
  never loads hooks from `antigravity-cli/settings.json`. The Antigravity App
228
236
  and IDE read the same global hooks.json, so the install arms them too. The
229
237
  status line goes in `~/.gemini/antigravity-cli/settings.json` only while it
230
- is plain JSON. The skill links go in `antigravity-cli/skills`. Antigravity
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
231
240
  adds no co-author trailer of its own, and no setting names one. An open
232
241
  feature request asks for one (google-antigravity/antigravity-cli#230). The
233
242
  always-on block is the `~/.gemini/GEMINI.md` one Gemini CLI shares
234
243
  (`bin/tezgah-setup::gemini_md_kept`). The uninstall takes out the `tezgah`
235
- name, the statusLine and the links. It removes a file that held only
244
+ name and the links, and puts the user's own statusLine back. It removes a file that held only
236
245
  tezgah's keys. Antigravity gets no subagent roles, MCP rows or orx shim from
237
246
  tezgah yet. Antigravity can never show a passing check. Its PostToolUse
238
247
  carries no output. A shell call that went to the background also ends with an
@@ -264,13 +273,16 @@ shared config, contract hash and the CLI symlinks every host shell can call
264
273
  always-on block into `~/.config/amp/AGENTS.md`. By default Amp adds a
265
274
  `Co-authored-by: Amp` and an `Amp-Thread-ID` trailer to its commits. The
266
275
  install sets `amp.git.commit.coauthor.enabled` and
267
- `amp.git.commit.ampThread.enabled` to false in `~/.config/amp/settings.json`
268
- unless the user set them. It writes that file only while it is plain JSON
269
- with no `settings.jsonc` beside it (`bin/tezgah-setup::amp_settings`). Else it
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
270
282
  prints both keys to set by hand. The uninstall removes the plugin only when
271
283
  it is a fresh render or starts with tezgah's header line
272
- (`bin/tezgah-setup::amp_plugin_ours`). It drops a trailer key only while that
273
- key is still false. Amp gets no subagent roles, MCP rows or orx shim from
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
274
286
  tezgah.
275
287
 
276
288
  An adapter is responsible for its host's **envelope** and its **surface**: it
@@ -332,7 +344,7 @@ and drops the later ones.
332
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` |
333
345
  | skills | claude | `.claude/skills`, `~/.claude/skills`; plugin skills are namespaced `tezgah:<name>` | the plugin copy |
334
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` |
335
- | 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 |
336
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 |
337
349
  | skills | qwen | `~/.qwen/skills`, `.qwen/skills`, extension and bundled skills; no documented winner | links in `~/.qwen/skills` |
338
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 |
@@ -421,16 +433,16 @@ and say which document it came from.
421
433
 
422
434
  A skill read is observable only where the host gives a channel that does not cost
423
435
  a process per read: Claude parses its transcript (`statusline.py::claude_used`),
424
- opencode classifies in process (`hosts/opencode/plugins/tezgah.js:2227-2263`),
436
+ opencode classifies in process (`classify` in `hosts/opencode/plugins/tezgah.js`),
425
437
  omp filters in its embedded runner before it asks python
426
- (`hosts/omp/tezgah-hook.ts.in:257-264`), and Amp's plugin does the same for a
438
+ (`skillRead` in `hosts/omp/tezgah-hook.ts.in`), and Amp's plugin does the same for a
427
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
428
440
  observable at that price. Their surfaces pass the five tool-use measures as
429
441
  `observable`, and the two skill marks render dim (`info`, no glyph) instead of
430
442
  claiming the skill was never opened (`hooks/tezgah_context.py::TOOL_USE_MEASURES`, and the
431
443
  `observable` branch at `hooks/tezgah_context.py::health_segments`). Callers that pass `TOOL_USE_MEASURES`:
432
444
  `statusline.py::observable`, `hosts/codex/hook.py::main`, `hosts/commandcode/hook.py::status`,
433
- `hosts/dsh/statusline/lib/index.js:16-19`. A kill switch is observable everywhere
445
+ `OBSERVABLE` in `hosts/dsh/statusline/lib/index.js`. A kill switch is observable everywhere
434
446
  and still renders `off` (`hooks/tezgah_context.py::health_segments`).
435
447
 
436
448
  ## Adding a host
@@ -440,7 +452,7 @@ In order, each step verified by the one below it:
440
452
  1. `HOST_DIRS` and, when the CLI can exist without a config dir, `HOST_BINS` in
441
453
  `hooks/tezgah_paths.py::HOST_DIRS`, `hooks/tezgah_paths.py::HOST_BINS`. These are shared by the installer, the report
442
454
  and agent generation, so "is this host installed?" has one answer
443
- (`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
444
456
  order (`bin/tezgah-setup::ALL_HOSTS`).
445
457
  2. The hook adapter(s) under `hosts/<host>/` (an `install_*`-independent
446
458
  directory like `hosts/codex/hook.py`, `hosts/cursor/hook.py`) plus, for a
@@ -471,8 +483,9 @@ In order, each step verified by the one below it:
471
483
  the host cannot see a skill read, and `idx_override` on any redraw that must
472
484
  not fork git for a cosmetic glyph (`hooks/tezgah_context.py::health_segments`).
473
485
  6. Tests, in two tiers: a per-host class in `tests/test_setup.py` pinning the
474
- report rows and the written files (e.g. `OmpHost:960-1030`,
475
- `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
476
489
  opt-in script under `tests/e2e_*.py` for the thing only a real host can prove
477
490
  - not collected by `unittest discover`, it exits 0 with `SKIP` when the host
478
491
  is absent (`tests/e2e_omp_statusline.py:13-16`). A script that calls a model
@@ -536,7 +549,7 @@ In order, each step verified by the one below it:
536
549
  (`hooks/projects-auto-init.py:28-38`, `hosts/codex/hook.py::main`).
537
550
  - **opencode**: `experimental.session.compacting` carries the builder's
538
551
  post-compact block, so the contract survives the summary
539
- (`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
540
553
  session id. The builder then forgets the lessons and armed paragraphs
541
554
  that session saw, and restates its pinned user constraints. The call
542
555
  used to send `{}`, which keyed nothing on the session.
@@ -546,7 +559,7 @@ In order, each step verified by the one below it:
546
559
  `hosts/omp/hook.py` for `{"event": "post_compact"}`; the block is sent with
547
560
  `pi.sendMessage(..., {deliverAs: "nextTurn"})`, and the summary rides as
548
561
  `compact_summary` so `remember_compaction` writes the compaction row
549
- (`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`).
550
563
  Subagent sessions are skipped, because the post-compact block starts the
551
564
  indexer.
552
565
  - **cursor**: not re-injected - a limitation. Cursor's only compaction hook,
@@ -570,7 +583,7 @@ In order, each step verified by the one below it:
570
583
  SubagentStop and Stop, and silently drops anything else - which is why
571
584
  `configPath` names `hosts/dsh/hooks.json` and not the Claude manifest
572
585
  (`bin/tezgah-setup::dsh_patch_block`). Its status route goes through `tezgah-status`
573
- 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
574
587
  declares `TEZGAH_CALL_OUTCOME=none` because the bridge drops the outcome field
575
588
  (`hosts/dsh/hooks.json:16-18`, `hooks/projects-posttooluse.py::main`), and its
576
589
  statusline row lives in the web profile because the plugin injects the
@@ -580,9 +593,9 @@ In order, each step verified by the one below it:
580
593
  `hosts/dsh/hooks.json` until the bridge carries outcomes.
581
594
  - **opencode carries the untrusted-content control in the plugin**, not by
582
595
  importing `hooks/tezgah_untrusted.py`, which a plugin cannot do in process: the
583
- channel is decided in JS (`untrustedSource` `hosts/opencode/plugins/tezgah.js:1942`,
584
- the tier's argv reader at `:1902-1911`), the label and the taint notice ride the
585
- 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
586
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
587
600
  fails if their answers differ, so neither can move without the other.
588
601
  - **omp carries both untrusted-content marks through the shared core.** Its
@@ -614,20 +627,20 @@ In order, each step verified by the one below it:
614
627
  - **opencode asks the core for the rules it cannot port, and counts the ask.** The
615
628
  task rule's phase and allowlist and the language rule's word list are answered by
616
629
  `bin/tezgah-gate decide` (the recording verb; `check` is the dry run) rather than copied into JavaScript (`IDENT_CMD`
617
- `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
618
631
  and per identifier-creating command. The delegation fails open by design: a missing
619
- CLI, a non-zero exit, a broken pipe or a 10 s timeout refuses nothing (`collect`
620
- `hosts/opencode/plugins/tezgah.js:152-178`). One `delegation` row now records the class when the core could not
621
- 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
622
635
  of invisible.
623
636
  - **opencode has no session-start hook**, so its always-on file must carry the
624
637
  pointer every hook host appends, and the per-session index, agent and contract
625
638
  refreshes ride the first `chat.message` instead (`bin/tezgah-setup::refresh_contract`,
626
- `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`
627
640
  tool - the generated router replaces the injected skill list
628
641
  (`bin/tezgah-setup::install_opencode`) - and `permission.ask` may never be emitted by a
629
642
  given build, which is why `tool.execute.before` stays the enforcing half
630
- (`hosts/opencode/plugins/tezgah.js:11-14`).
643
+ (the header comment of `hosts/opencode/plugins/tezgah.js`).
631
644
  - **Cursor only runs a PreToolUse hook for the tool types its matcher names**, so
632
645
  the matcher must list the write spellings or the gate's edit branches are
633
646
  unreachable there (`hosts/cursor/hook.py:33-42`,
@@ -648,7 +661,7 @@ In order, each step verified by the one below it:
648
661
  `hosts/cursor/hooks.json:8` - neither named them until 2026-09-19, so a
649
662
  PowerShell call on those two reached no hook at all while the shared rules
650
663
  already knew the spelling), and omp's `GATED` list
651
- (`hosts/omp/tezgah-hook.ts.in:270-273`). Qwen Code has two shell tool ids,
664
+ (`GATED` in `hosts/omp/tezgah-hook.ts.in`). Qwen Code has two shell tool ids,
652
665
  `run_shell_command` and `monitor`, which `bin/tezgah-setup::QWEN_PRETOOL_MATCHER`
653
666
  names beside `notebook_edit` and `mcp__.*`. Claude's and dsh's `Monitor` runs a
654
667
  shell command too, so their matcher names it. Droid's shell is `Execute`, which `bin/tezgah-setup::DROID_PRETOOL_MATCHER`
@@ -720,7 +733,7 @@ In order, each step verified by the one below it:
720
733
  - **omp spawns the MCP `command` as one executable** and takes the rest in
721
734
  `args`; the whole argv in `command` fails with ENOENT (`bin/tezgah-setup::install_omp`),
722
735
  and `setStatus` strips ANSI, so only the widget path keeps the per-mark colors
723
- (`hosts/omp/tezgah-hook.ts.in:170-173`).
736
+ (the comment above `draw` in `hosts/omp/tezgah-hook.ts.in`).
724
737
  - **omp drops browser MCP servers while its native browser is on**: a server
725
738
  named `playwright`/`puppeteer`/... or running `@playwright/mcp`/... never
726
739
  connects there (omp 18.2.11 `/mcp` listed codegraph, mobile-mcp and tezgah
package/docs/layers.md CHANGED
@@ -94,7 +94,7 @@ frameworks.
94
94
  | agent framework | is it composed *from* elsewhere, or does it compose? A framework is composed from building blocks and stays a framework agent | tezgah plugs into its extension points, supplies none |
95
95
  | SDK | does it leave the iteration to its caller? A client library gives a call and stops | tezgah ships no client; it reads a host's event JSON on stdin (`hosts/codex/hook.py:1-2`) |
96
96
  | IDE plugin | is it a guest inside another program's surface? It draws and forwards and owns no task runtime | `hosts/opencode/plugins/tezgah.js:1-12` is one, and is a plugin |
97
- | eval harness | does it produce a score over a task set? It measures and enforces nothing | an eval scaffold is vendored as the `lm-evaluation-harness` skill body (`skills/ai-research/11-evaluation/lm-evaluation-harness/ENTRY.md:11`), and this project's measurements live apart from the layer (`docs/README.md:88-89`) |
97
+ | eval harness | does it produce a score over a task set? It measures and enforces nothing | an eval scaffold is vendored as the `lm-evaluation-harness` skill body (`skills/ai-research/11-evaluation/lm-evaluation-harness/ENTRY.md:11`), and this project's measurements live apart from the layer (`docs/README.md:100-101`) |
98
98
  | orchestrator | does it split one request across many runs and merge them? A harness runs one job to a report | the `harness` skill's route table (`skills/harness/SKILL.md:41-53`) and the graph workflows (`hooks/tezgah_policy.py::WORKFLOWS`) |
99
99
  | MCP | does it only carry a call and its result? Then it is transport, not a layer with policy or evidence | the section below |
100
100
 
@@ -52,14 +52,14 @@ A green `--report` says the wiring is on disk; `--report --live` says it ran. Wh
52
52
  What a user receives is a release tarball, not this checkout:
53
53
  `dist/tezgah-<version>.tar.gz` beside `dist/tezgah-<version>.tar.gz.sha256`,
54
54
  built by `packaging/build.sh` from the tracked `MANIFEST` plus a generated
55
- `VERSION`, both written into the payload (`packaging/build.sh:4`,
56
- `packaging/build.sh:61`, `packaging/build.sh:137`). A version unpacks at
55
+ `VERSION`, both written into the payload (`GENERATED`, `packaging/build.sh:61`,
56
+ and `VERSION`, `packaging/build.sh:137`). A version unpacks at
57
57
  `<prefix>/<version>` and `<prefix>/current` is a symlink to the active one, so a
58
58
  team can stay on one version and a rollback is one tree away: the older version is
59
59
  never removed, and going back is
60
- `ln -sfn <prefix>/<previous> <prefix>/current` (`packaging/upgrade.sh:9-11`,
61
- `packaging/upgrade.sh:176-196`). The prefix is `~/.local/share/tezgah` unless
62
- `TEZGAH_PREFIX` or `$XDG_DATA_HOME` says otherwise (`packaging/upgrade.sh:94`),
60
+ `ln -sfn <prefix>/<previous> <prefix>/current` (the header comment of
61
+ `packaging/upgrade.sh`, and its `ln -sfn "$dest"` flip). The prefix is `~/.local/share/tezgah` unless
62
+ `TEZGAH_PREFIX` or `$XDG_DATA_HOME` says otherwise (`PREFIX=` in `packaging/upgrade.sh`),
63
63
  and `bin/tezgah-setup --prefix DIR` wins over both (`bin/tezgah-setup::INSTALL_PREFIX`).
64
64
  A farm link counts as tezgah's when it resolves into the checkout **or** into the
65
65
  install root, which is what keeps an upgrade from orphaning the wiring
@@ -68,23 +68,23 @@ install root, which is what keeps an upgrade from orphaning the wiring
68
68
  | Step | Command |
69
69
  |---|---|
70
70
  | first install, POSIX | `curl -fsSL https://raw.githubusercontent.com/r1z4x/tezgah/v<version>/packaging/install.sh \| sh -s -- --version <version>` |
71
- | from a tarball you hold | unpack it, then `sh packaging/install.sh --version <version>` — it finds `upgrade.sh` beside itself (`packaging/install.sh:34-36`) |
71
+ | from a tarball you hold | unpack it, then `sh packaging/install.sh --version <version>` — it finds `upgrade.sh` beside itself (`here=` in `packaging/install.sh`) |
72
72
  | first install, Windows | `$env:TEZGAH_VERSION = '<version>'; irm https://raw.githubusercontent.com/r1z4x/tezgah/v<version>/packaging/install.ps1 \| iex` |
73
73
  | upgrade | `tezgah update` (any channel), `bin/tezgah-setup --upgrade [VERSION]` from the installed tree, or `sh packaging/upgrade.sh --version X.Y.Z` |
74
74
 
75
75
  `packaging/install.sh` is a bootstrap, never a second implementation: it runs
76
76
  `packaging/upgrade.sh` — the one place a version is fetched, checksum-verified,
77
77
  unpacked and made current — and then starts `bin/tezgah-setup --install` from the
78
- tree that just became current (`packaging/install.sh:54-66`). `upgrade.sh`
78
+ tree that just became current (the `sh "$up"` call in `packaging/install.sh`). `upgrade.sh`
79
79
  compares the digest it fetched against the artifact before unpacking anything and
80
- stops, printing both, on a mismatch (`packaging/upgrade.sh:148-152`). It then
80
+ stops, printing both, on a mismatch (`want=` and `got=` in `packaging/upgrade.sh`). It then
81
81
  checks the tarball's build provenance with `gh attestation verify` when `gh` is
82
82
  on PATH. Without `gh` it prints `provenance not checked`, and a `gh` that cannot
83
83
  answer is a warning: the checksum stays the gate.
84
84
  `TEZGAH_DIST` points `upgrade.sh` and `install.ps1` at a directory holding
85
85
  `tezgah-<version>.tar.gz` + `.sha256` instead of the release URL — the seam that
86
86
  installs a locally built artifact with no published release, which is what CI
87
- uses (`packaging/upgrade.sh:17-24`, `packaging/upgrade.sh:105-111`).
87
+ uses (the `TEZGAH_DIST` branch of `packaging/upgrade.sh`).
88
88
 
89
89
  **A private `TEZGAH_REPO` needs a token.** GitHub answers `404` to
90
90
  `releases/download/...` from a session without one. Measured 2026-10-17: the token
@@ -105,8 +105,8 @@ Windows goes through `packaging/install.ps1`, which assumes no `sh`, no `python3
105
105
  and no symlink privilege: it unpacks with the `tar.exe` that Windows 10 1803+
106
106
  ships, materialises `current` as a directory junction, copies the tree where a
107
107
  junction is refused, and starts the installer with the first of `py -3`,
108
- `python`, `python3` it finds, passing `--prefix` on (`packaging/install.ps1:12-23`,
109
- `packaging/install.ps1:179-195`). The installer knows both shapes of `current`
108
+ `python`, `python3` it finds, passing `--prefix` on (the header and the `$NoInstall`
109
+ block of `packaging/install.ps1`). The installer knows both shapes of `current`
110
110
  (`bin/tezgah-setup::running_prefix`). A junction answers to
111
111
  `os.path.isjunction`, or to its reparse tag below Python 3.12. A copy is the
112
112
  running tree at `<prefix>/current` with no `.git`, whose `VERSION` names a
@@ -140,11 +140,13 @@ cd ~/Projects/tezgah && bin/tezgah-setup --install
140
140
  install the missing optional tools (host CLIs only with `--host-deps`) and whether to wire the DevTools MCP, prints
141
141
  the plan, and writes only after a yes (`bin/tezgah-setup::wizard`). A pre-tezgah setup is
142
142
  named under `predecessor wiring still present` and retired by `--adopt`
143
- (`bin/tezgah-setup::adopt`); narrow it with `--hosts omp`, `--roots ~/work:~/oss`, `--dry-run`.
143
+ (`bin/tezgah-setup::adopt`); narrow an install with `--hosts omp` and `--roots ~/work:~/oss`.
144
+ On `--install`, `--dry-run` covers only the optional-tool installs (`bin/tezgah-setup::install_deps`).
145
+ `--install --dry-run` still writes every host file, the farm and `config.json`, so it is not a preview.
144
146
 
145
147
  An install from a checkout is a live symlink into that working tree, so a half-finished edit or a branch switch there crashes the sessions running on it (the `crash` ledger rows of 09-20..09-21 were names removed mid-edit) - do tezgah maintenance in a `git worktree`, never in the installed checkout.
146
148
 
147
- The `~/.config/tezgah/bin` farm carries `tezgah-mcp` and `tezgah-design` beside the other CLIs; two are left out on purpose: `tezgah-dsh` is wired as `~/.local/bin/dsh` (`dsh_launcher`), and `tezgah-import-ai-research` is dev-time only (its docstring). The report's `config.json hosts (...) match the hosts wired (...)` row (`hosts_row`) reads `MISS` when the recorded host list differs from the hosts whose wiring is on disk; `--upgrade`, `--enable`/`--disable` and a full uninstall act on the recorded list, so the row only reports it - `--install --hosts ...` records the list you choose.
149
+ The `~/.config/tezgah/bin` farm carries `tezgah-mcp` and `tezgah-design` beside the other CLIs; two are left out on purpose: `tezgah-dsh` is wired as `~/.local/bin/dsh` (`dsh_launcher`), and `tezgah-import-ai-research` is dev-time only (its docstring). It also carries one link per host adapter. A host's hook config runs that link by its farm path: `tezgah-codex-hook`, `tezgah-cursor-hook`, `tezgah-qwen-hook`, `tezgah-droid-hook`, `tezgah-commandcode-hook`, `tezgah-copilot-hook`, `tezgah-gemini-hook`, `tezgah-antigravity-hook` and `tezgah-goose-hook` (`bin/tezgah-setup::install_common`). The report's `config.json hosts (...) match the hosts wired (...)` row (`hosts_row`) reads `MISS` when the recorded host list differs from the hosts whose wiring is on disk; `--upgrade`, `--enable`/`--disable` and a full uninstall act on the recorded list, so the row only reports it - `--install --hosts ...` records the list you choose.
148
150
 
149
151
  A successful run prints, in order: `dependencies:`, a line per missing optional
150
152
  tool with the vendor command it runs — over the network, no sudo
@@ -172,7 +174,7 @@ or `default`), a common block (config, the code-graph binary, the consult option
172
174
  `orx`, `npx`, the artifacts dir, `git`, the manifest, a stale-contract line), one
173
175
  block per host, then the budget (`bin/tezgah-setup::report`). Each block is
174
176
  `host_checks_<host>` (`bin/tezgah-setup::HOST_CHECKS`), and those rows are the source of truth for
175
- "is this host armed" (`AGENTS.md:147-148`) — not the presence of a directory, and
177
+ "is this host armed" (`AGENTS.md:177-178`) — not the presence of a directory, and
176
178
  not the status line. A row can read ` MISS ` too: claude's `plugin copy current`
177
179
  row fails on a machine with no copy at all, because an absent copy is not a
178
180
  current one (`bin/tezgah-setup::host_checks_claude`).
@@ -195,7 +197,7 @@ a host's config, so a wiring row there would belong to a different tool.
195
197
 
196
198
  The version lives in `.claude-plugin/plugin.json` when that untracked local
197
199
  manifest is present, else in the newest `## [x.y.z]` heading of `CHANGELOG.md`
198
- (`hooks/tezgah_context.py::version`); `.gitignore:16-19` is why it is not in the repository, and
200
+ (`hooks/tezgah_context.py::version`); `.gitignore:28` is why it is not in the repository, and
199
201
  `RELEASING.md:19-24` is how a release bumps it. A `--install` that registers
200
202
  Claude renders the pair into its tree. The version comes from `VERSION` or that
201
203
  heading, never from the pair itself. A pair the install rendered follows the
@@ -239,7 +241,7 @@ After a change to the contract text (`hooks/tezgah_policy.py`,
239
241
  - `--refresh` does that without a reinstall, for every armed host's static
240
242
  block and not only opencode's (`refresh_contract`). opencode has no
241
243
  session-start hook, so its plugin runs it once per session
242
- (`hosts/opencode/plugins/tezgah.js:2689-2693`). Until 2026-10-02 it
244
+ (`oncePerSession` in the `chat.message` hook of `hosts/opencode/plugins/tezgah.js`). Until 2026-10-02 it
243
245
  re-rendered opencode's files alone and reset the one shared hash, so a rule
244
246
  edit left the other static files stale while `--report` went green (audit
245
247
  M-5).
@@ -278,7 +280,15 @@ materialised where a platform refuses symlinks — counts as tezgah's only when
278
280
  resolves inside this checkout or the install root, so a file you put in the same
279
281
  place is kept (`is_tezgah_entry()`, `bin/tezgah-setup::is_tezgah_entry`), and
280
282
  `~/.claude/settings.json` loses only the keys tezgah added
281
- (`bin/tezgah-setup::unwire_claude_settings`).
283
+ (`bin/tezgah-setup::unwire_claude_settings`). Every key tezgah replaced goes
284
+ back to what the install found, from the install record
285
+ (`bin/tezgah-setup::restore_prior`). A directory or host file the install
286
+ created goes once the uninstall leaves it empty. One that was there before
287
+ stays. The codegraph MCP row stays too (OQ-13). A partial run takes only the
288
+ directories under the removed hosts' own roots
289
+ (`bin/tezgah-setup::remove_created_dirs`). A JSON host file keeps its data,
290
+ indent, minified form and non-ASCII text. An inline `[1, 2]` or `{"a": 1}`
291
+ inside an indented file may come back expanded (`bin/tezgah-setup::write_json`).
282
292
 
283
293
  A **full** run also takes everything the wiring was serving from:
284
294
 
@@ -301,18 +311,25 @@ A **full** run also takes everything the wiring was serving from:
301
311
  Kept in every run: the user's own files, every host-file backup under
302
312
  `~/.config/tezgah/backups/` (the timestamped ones too), `~/.config/tezgah/adopted/` (the only copy of the
303
313
  predecessor wiring adopt moved aside), and every repository's `.tezgah/`
304
- research state. A **partial** run also keeps config.json and the install tree,
305
- because the hosts still armed read both at runtime, and says so. It also leaves
314
+ research state. A **partial** run also keeps config.json, both state caches
315
+ and the install tree. The hosts still armed read them at runtime, and the run says so. It also leaves
306
316
  the gate armed: only a full run writes `pretooluse-off` while it removes the
307
317
  hooks. A partial run used to write it too and never take it back, so the hosts
308
318
  still armed lost the whole gate (audit H-2, ENV-01). The orx skill
309
319
  shims come from orx's own installer, so they are never listed for removal
310
- (`bin/tezgah-setup::install_openresearch`).
320
+ (`bin/tezgah-setup::install_openresearch`). An uninstall keeps the `codegraph` MCP
321
+ row in Codex's `config.toml`, Cursor's `mcp.json` and opencode's `opencode.json`
322
+ on purpose. It is the graph server's row, not tezgah's, and a user may have
323
+ written the same row. So the uninstall takes only the `tezgah` row beside it
324
+ (`tests/test_tezgah_mcp_wiring.py::Rows.test_uninstall_removes_only_the_tezgah_row`).
311
325
 
312
326
  Every run ends in a verify pass that re-derives the removal from the filesystem
313
327
  alone — one line per claim, and the exit code is 1 while anything tezgah wrote
314
328
  survives (`verify_uninstall()`, `bin/tezgah-setup::verify_uninstall`), so a leftover can
315
- never read as a clean uninstall. It ends by naming what needs a restart and, in
329
+ never read as a clean uninstall. Each claim counts only tezgah's own links and
330
+ markers. Those are its farm path, raw or JSON-escaped, its MCP server names and
331
+ its managed-block markers. A user's own file that names tezgah is no
332
+ leftover (B-123). It ends by naming what needs a restart and, in
316
333
  a git checkout whose own `.mcp.json` registers the tezgah MCP server, saying that
317
334
  that file is the source tree's dev config, not an install artifact. An npm or
318
335
  artifact install does not ship `.mcp.json` (it is neither in package.json
@@ -414,6 +431,31 @@ repository a sync over the copy took 1 s, and a fresh `init` took 10 s.
414
431
  way (`bin/tezgah-task::cmd_start`). A copy that fails leaves the worktree to
415
432
  `init`, as before.
416
433
 
434
+ ## tezgah's own MCP server and the decision record: `tezgah-mcp`, `tezgah-decisions`
435
+
436
+ `bin/tezgah-mcp` is one stdio MCP server for every host: `initialize`,
437
+ `tools/list` and `tools/call` over newline-delimited JSON-RPC 2.0. Each host's
438
+ MCP config names it `tezgah` (`bin/tezgah-setup::TEZGAH_MCP_NAME`,
439
+ `bin/tezgah-setup::tezgah_mcp_command`), and the report's `MCP tezgah server wired
440
+ with this tree's argv` row checks that entry (`bin/tezgah-setup::mcp_row`). The server ports no rule. Each tool runs one
441
+ installed CLI and returns its output: one summary line, then the log line by
442
+ line. A CLI that fails comes back as `isError` with its own exit status
443
+ (`bin/tezgah-mcp::call`). A delegate gets 120 s, and `consult` gets 600 s
444
+ (`bin/tezgah-mcp::TIMEOUT`, `bin/tezgah-mcp::CONSULT_TIMEOUT`). The tools are
445
+ `bin/tezgah-mcp::SPECS`, the same list `tools/list` answers with.
446
+
447
+ | Tool | Inputs | Answers from |
448
+ |---|---|---|
449
+ | `tezgah_status` | `path` (default: the server's working directory) | `tezgah-status`: the armed, used and off contracts, one segment per line |
450
+ | `tezgah_gate_check` | `command` (required), `path` | `tezgah-gate check`: the refusal reason, or nothing when the call passes; nothing runs |
451
+ | `tezgah_features` | none | `tezgah-setup --features`: each feature id, its default, its selection and whether its dependency is installed |
452
+ | `tezgah_consult` | `question` (required), `use` (the members the user chose, e.g. `cli:codex,openrouter`) | `consult`; exit 4 or 5 lists the members to offer the user |
453
+ | `tezgah_research_check` | `path`, `slug` (one line in full; without it the open lines in full and each done line by its seal) | `tezgah-research check` |
454
+
455
+ | Command | What it does | Exit |
456
+ |---|---|---|
457
+ | `tezgah-decisions [check] [PATH] [--json]` | reads every ADR at `.tezgah/decisions/NNN-slug.md` in the repository at PATH (default: the working directory) and prints how many it read. It refuses three things: a record with no `status:`, a status outside `proposed`, `accepted`, `deprecated` and `superseded`, and a `supersedes:` naming no record (`bin/tezgah-decisions::problems`). `--json` prints the records and the problems | 0 every record whole or none, 1 a record refused (one `FAIL <path>: <what>` line each), 2 misuse |
458
+
417
459
  ## Opt-in taste: capture, learn, apply
418
460
 
419
461
  Taste learns the user's coding preferences from their own corrections. It is
@@ -440,8 +482,8 @@ and a session rollback reaches about half as far back. The after blob writes no
440
482
  ledger row. A rollback therefore never reads it as a pre-state (`capture_after`,
441
483
  `hooks/tezgah_snapshot.py::capture_after`). A file outside the repository gets no
442
484
  after blob.
443
- opencode reaches the same call through a process (`noteTaste`,
444
- `hosts/opencode/plugins/tezgah.js:2436-2446`).
485
+ opencode reaches the same call through a process (`noteTaste` in
486
+ `hosts/opencode/plugins/tezgah.js`).
445
487
 
446
488
  ```sh
447
489
  touch ~/.config/tezgah/taste-on # arm; rm it to disarm
@@ -727,7 +769,8 @@ OCR then reviews on omp's model with omp's own credentials.
727
769
  tracked, untracked and ignored files. In range or commit mode it covers the
728
770
  tree of the reviewed ref. `--allow-secrets` skips the check. The patterns are
729
771
  OCR's own (`bin/tezgah-ocr-review::SECRET_PATTERNS`).
730
- - A missing `ocr` or `omp` exits 2. A setup failure of the tool itself exits 4
772
+ - Exit 2 is misuse or a missing tool: an unknown flag, `--commit` beside
773
+ `--from`/`--to`, or no `ocr` or `omp` on PATH. A setup failure of the tool itself exits 4
731
774
  with one line (`bin/tezgah-ocr-review::SetupError`). Three failures count:
732
775
  no git repository, omp cannot start, the gateway never reports ready.
733
776
 
package/docs/research.md CHANGED
@@ -314,7 +314,7 @@ through its seal.
314
314
 
315
315
  | Command | What it does | Exit |
316
316
  |---|---|---|
317
- | `tezgah-research init <slug> [--question "..."] [--class R1..R12] [--allow-open "<reason>"] [--supersedes <slug>]` | scaffolds the line and makes sure `.tezgah/` is ignored by the project and has its own git repository (`tezgah_paths.ensure_workspace`). It refuses while another line is still open (see below), names each open line and its reasons one per line, and `--allow-open "<reason>"` is the way past unless an open line has `check` errors: the reason lands in the new line's `log.md` as its first entry, and an empty reason is misuse. A `--question` another line already asks is refused unless `--supersedes <that line>` says the new line is its next version (`serial_twins`). `--class` writes `state.question_class` and prints `obligations: <step>, ...` (`obligations`), the tier's `budget <slug>: sources a/b searches c/d runs e/f` (`budget`) and `overlap: <line>/<claim> <status> <similarity>` for each done claim close to the question (`overlaps`); `--class R0` exits 1 (`R0 gets no line`) before any other argument is read. Its next-step message names the commit loop: `tezgah-research commit` for the protocol, then again for the results in a later commit (`cmd_init`, `bin/tezgah-research`) | 0, 1 refused, 2 misuse |
317
+ | `tezgah-research init <slug> --ask "<the user's words>" [--tier quick\|study\|program] [--method qualitative\|measured\|exploratory] [--class R1..R12] [--in "<covered>"]... [--out "<left alone>"]... [--question "..."] [--allow-open "<reason>"] [--supersedes <slug>] [--arena "<problem>" --budget "<budget>" [--evaluator <path> --metric "<metric>"]]` | scaffolds the line and makes sure `.tezgah/` is ignored by the project and has its own git repository (`tezgah_paths.ensure_workspace`). `--ask` is required: the user's words, verbatim, and a run without it exits 1 (`no --ask`). `--tier`, `--method`, `--in` and `--out` are the scope lock above, and `--arena`/`--budget` (with `--evaluator`/`--metric` for a measurable one) open an exploratory line ("The arena lock"). It refuses while another line is still open (see below), names each open line and its reasons one per line, and `--allow-open "<reason>"` is the way past unless an open line has `check` errors: the reason lands in the new line's `log.md` as its first entry, and an empty reason is misuse. A `--question` another line already asks is refused unless `--supersedes <that line>` says the new line is its next version (`serial_twins`). `--class` writes `state.question_class` and prints `obligations: <step>, ...` (`obligations`), the tier's `budget <slug>: sources a/b searches c/d runs e/f` (`budget`) and `overlap: <line>/<claim> <status> <similarity>` for each done claim close to the question (`overlaps`); `--class R0` exits 1 (`R0 gets no line`) before any other argument is read. Its next-step message names the commit loop: `tezgah-research commit` for the protocol, then again for the results in a later commit (`cmd_init`, `bin/tezgah-research`) | 0, 1 refused, 2 misuse |
318
318
  | `tezgah-research commit <slug> "<message>"` | stages and commits only that line's path in `.tezgah`'s private repository - the commit the order rule reads (`cmd_commit`, `bin/tezgah-research`). With nothing left to commit it prints `nothing to commit: <paths> is already committed in <workspace>` and exits 0 | 0 (also with nothing to commit), 1 not a work tree or git failed, 2 misuse |
319
319
  | `tezgah-research check [<slug>] [--json] [--strict] [--orx] [--online] [--all-lines] [-- <slug>]` | the discipline checks below. With no slug it checks the open lines in full and a line under `done/` by its order seal alone (`done_seal`, `hooks/tezgah_research.py::done_seal`), naming it only when the seal fails or its `state.json` or seal cannot be read, then prints `research: <n> done line(s): <s> checked by their seal, <u> unsealed (not checked); ...`; `--all-lines` checks every line in full, and `check <slug>` always checks that line in full (ADR 015). `--json` prints the report (no-slug default: the open lines and any done line whose seal read fails), `--strict` turns the unverifiable class into a refusal, `--orx` adds the registry check that asks `orx project view` for this repository (`check_orx`, `hooks/tezgah_research.py::check_orx`) and, on a rules-8 line, the run census; `--online` re-fetches a rules-8 line's citation receipts; a slug after `--` is never read as a flag, which is how the MCP tool `tezgah_research_check` passes its validated `slug`. A rules-8 line (`_check_rules8`) also prints report lines that are never counted - `certainty`, `stats`, `effect`, `instrument`, `ppi`, `stopping`, `online`, `budget` - and `--json` adds its per-claim `certainty` | 0 clean, 1 a line failed a rule or names no line |
320
320
  | `tezgah-research status` | one line per line: an open line `ok` or a problem count, a line under `done/` read by its seal like the no-slug `check` - `done, seal ok`, `done, <n> seal problem(s)` or `done, unsealed (not checked)` (`summary`, `hooks/tezgah_research.py::summary`, plan 058 part 4); then one `FAIL` line per hidden dir under `research/` (`stray_dirs`) | 0 |