@r1z4x/tezgah 1.11.0 → 1.13.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 (60) hide show
  1. package/CHANGELOG.md +333 -1
  2. package/MANIFEST +22 -0
  3. package/README.md +4 -3
  4. package/bin/tezgah-docs +1 -1
  5. package/bin/tezgah-research +39 -4
  6. package/bin/tezgah-setup +1229 -22
  7. package/bin/tezgah-task +55 -17
  8. package/docs/citations-baseline.json +1 -1
  9. package/docs/clarity-baseline.json +9 -3
  10. package/docs/contract.md +6 -3
  11. package/docs/environment.md +3 -2
  12. package/docs/evidence.md +81 -8
  13. package/docs/gate.md +6 -2
  14. package/docs/glossary.md +1 -1
  15. package/docs/hosts.md +297 -15
  16. package/docs/layers.md +1 -1
  17. package/docs/models.md +6 -0
  18. package/docs/operations.md +16 -5
  19. package/docs/research.md +36 -10
  20. package/docs/status-line.md +6 -0
  21. package/docs/testing.md +3 -1
  22. package/hooks/hooks.json +1 -1
  23. package/hooks/lang/en.json +11 -0
  24. package/hooks/lang/tr.json +11 -0
  25. package/hooks/projects-stop.py +2 -1
  26. package/hooks/tezgah_agents.py +27 -3
  27. package/hooks/tezgah_attest.py +79 -10
  28. package/hooks/tezgah_context.py +21 -8
  29. package/hooks/tezgah_gate.py +79 -23
  30. package/hooks/tezgah_integrity.py +212 -38
  31. package/hooks/tezgah_paths.py +43 -2
  32. package/hooks/tezgah_policy.py +35 -13
  33. package/hooks/tezgah_replay.py +8 -2
  34. package/hooks/tezgah_research.py +167 -24
  35. package/hooks/tezgah_stack.py +236 -1
  36. package/hooks/tezgah_task.py +2 -1
  37. package/hosts/amp/hook.py +217 -0
  38. package/hosts/amp/tezgah-plugin.ts.in +386 -0
  39. package/hosts/antigravity/hook.py +255 -0
  40. package/hosts/codex/hook.py +2 -1
  41. package/hosts/commandcode/hook.py +239 -0
  42. package/hosts/copilot/hook.py +284 -0
  43. package/hosts/cursor/hook.py +2 -1
  44. package/hosts/droid/hook.py +259 -0
  45. package/hosts/dsh/hooks.json +1 -1
  46. package/hosts/gemini/hook.py +213 -0
  47. package/hosts/goose/hook.py +145 -0
  48. package/hosts/omp/hook.py +4 -1
  49. package/hosts/omp/tezgah-hook.ts.in +30 -7
  50. package/hosts/opencode/plugins/tezgah.js +2 -2
  51. package/hosts/qwen/hook.py +206 -0
  52. package/output-styles/tezgah.md +10 -5
  53. package/package.json +1 -1
  54. package/skills/harness/SKILL.md +2 -0
  55. package/skills/i-have-adhd/SKILL.md +10 -1
  56. package/skills/plan-add/SKILL.md +5 -1
  57. package/skills/research/SKILL.md +22 -9
  58. package/skills/tezgah-contract/SKILL.md +19 -6
  59. package/statusline.py +20 -9
  60. package/workflows/graph-review.js +14 -2
package/CHANGELOG.md CHANGED
@@ -6,6 +6,335 @@ adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
6
6
 
7
7
  ## [Unreleased]
8
8
 
9
+ ## [1.13.0] - 2026-10-29
10
+
11
+ ### Fixed
12
+
13
+ - **omp handed the Stop rule a message object, not the reply (plan 143).**
14
+ omp's `session_stop` carries `last_assistant_message` as the object its
15
+ `getLastAssistantMessage()` returns. The bridge passed it through, so the
16
+ rule judged `str(dict)`: one line, no list items, diluted prose.
17
+ - Every line-shaped class read a text with no lines: the list cap, the
18
+ residual sections, the question the reply ends on, the handed step. The
19
+ language check read a share that was not the reply's.
20
+ - Measured on the owner's evidence database: 1643 of 1656 omp `shape` rows
21
+ of 45 days carried `lines: 1`. Every other host wrote 209 multi-line of
22
+ 231. Of three days' refusals, 24 were `reply language` over such a row.
23
+ - `stop_reason` reads every reply through `reply_prose` now, so no host can
24
+ repeat it. The omp bridge sends the text blocks.
25
+
26
+ ### Added
27
+
28
+ - **The Stop rule knows the host's plan mode (plan 142).** Every Stop adapter
29
+ passes the host's `permission_mode` to the core now. omp's extension sends
30
+ `plan` from `ctx.sessionModes.getState()`. `plan` and Droid's `spec` are the
31
+ read-only ones.
32
+ - In such a turn the rule lets the approval question through. It used to
33
+ refuse it as a `needless ask` - the one question that mode allows.
34
+ - A first-person claim that a command or check ran, made in such a turn, is
35
+ the new `plan claim` class. Nothing could run, so the claim says what
36
+ cannot have happened. A check that passed anyway clears it, and a plan's
37
+ own acceptance line ("Done when all tests pass") is no claim.
38
+ - The new `handed step` class refuses a reply that tells the reader to run a
39
+ command. It asks only of a turn that ran commands of its own, outside plan
40
+ mode. An outward step, one no default may take, the owner's consent,
41
+ tezgah's own install and a `brew` step stay the user's. So does a line that
42
+ types its blocker, and an answer about how a command is run.
43
+ - Measured on the owner's transcripts: of 3307 user turns, 27 replies handed
44
+ back a command and no class refused any of them. The shipped shapes refuse
45
+ 9 turns, 8 of them a step the session could have taken.
46
+
47
+ ## [1.12.0] - 2026-10-29
48
+
49
+ ### Added
50
+
51
+ - **Amp is a host (`amp`, plan 140).** `tezgah-setup --install --hosts amp`
52
+ writes one TypeScript plugin, `tezgah.ts`, into Amp's system plugin dir.
53
+ That dir is `$XDG_CONFIG_HOME/amp/plugins/`, else `~/.config/amp/plugins/`.
54
+ The install renders it from `hosts/amp/tezgah-plugin.ts.in`. Amp loads every
55
+ `.ts` there, and a running Amp needs `plugins: reload`. The install links the
56
+ skills into `~/.config/amp/skills` and writes the always-on block into
57
+ `~/.config/amp/AGENTS.md`. It sets Amp's two commit trailers
58
+ (`amp.git.commit.coauthor.enabled`, `amp.git.commit.ampThread.enabled`) to
59
+ false in `settings.json` unless the user set them. It writes that file only
60
+ while it is plain JSON with no `settings.jsonc` beside it. The uninstall
61
+ removes the plugin only when it is tezgah's render. It drops a trailer key
62
+ only while that key is still false.
63
+ - The plugin spawns `hosts/amp/hook.py` without blocking. One plugin process
64
+ serves many threads, so it keys its state by thread id. A dead, slow or
65
+ crashing hook allows every call, and the plugin tells each thread once.
66
+ - `tool.call` refuses with `{action: "reject-and-continue"}`. It asks the gate
67
+ for `Bash`, `shell_command`, the edit tools, `Task` and MCP effect names. A
68
+ shell call's text reaches the gate as `command`, also when Amp sends `cmd`.
69
+ - A tool result counts as passed only with an integer `exitCode` on its
70
+ output object. `error` and `cancelled` are failures. A bare `done` shell
71
+ result is a run, never a pass.
72
+ - `agent.end` judges a `done` turn and continues it with `[tezgah:stop]
73
+ <reason>`. The plugin never sends that prompt as the user's. So it writes no
74
+ turn row, grants nothing and cannot reset the re-ask budget. A thread with
75
+ a parent thread is a subagent. It gets the subagent brief, and its end is a
76
+ record-only row.
77
+ - The session brief rides the thread's first `agent.start`. The status line
78
+ goes on Amp's experimental status item for the active thread.
79
+ - `--report` has Amp rows. `--report --live` loads the plugin with node and
80
+ drives its `tool.call` and `tool.result`. The row stays UNVERIFIED, since
81
+ Amp's own loader did not run it. `tezgah-doctor --stack` reads Amp's
82
+ guidance files, skills, plugins and `amp.mcpServers`. `read_web_page` is a
83
+ web channel for the untrusted label. README and docs name all fourteen
84
+ hosts.
85
+ - **Goose is a host (`goose`, plan 139 slice 5).** `tezgah-setup --install
86
+ --hosts goose` writes tezgah's own plugin dir, `~/.agents/plugins/tezgah/`.
87
+ It holds `plugin.json`, `hooks/hooks.json` and `skills/` links, and it is
88
+ wholly tezgah's. Goose loads the skills as `tezgah:<skill>`. The timeouts are in seconds: Stop 30, the
89
+ rest 5. `on_failure` stays at Goose's default `allow`, so the gate fails open
90
+ on a hook fault. The always-on block goes in `~/.config/goose/.goosehints`.
91
+ Goose adds no co-author trailer and names no setting for one, so there is
92
+ nothing to turn off. The uninstall deletes the plugin dir and the block and
93
+ leaves the rest of `~/.agents` alone.
94
+ - `hosts/goose/hook.py` handles SessionStart (attestation only),
95
+ UserPromptSubmit (the turn row only), PreToolUse, PostToolUse,
96
+ PostToolUseFailure and Stop. The event is the payload's `event`, the cwd its
97
+ `working_dir`, else the hook's own. `shell`, `write` and `edit` map to Bash,
98
+ Write and Edit, with `path` as `file_path` and `before`/`after` as
99
+ `old_string`/`new_string`. `<extension>__<tool>` becomes
100
+ `mcp__<extension>__<tool>`.
101
+ - PreToolUse and Stop block with `{"decision": "block", "reason": ...}` on
102
+ stdout. The outcome is the event: PostToolUse is a pass, PostToolUseFailure
103
+ a failure. Stop judges `last_assistant_message`, or writes a
104
+ `reply_unread` row. With no host block count, the turn rows bound the
105
+ re-asks.
106
+ - `--report` has Goose rows, including one for `disabledPlugins`. `--report
107
+ --live` probes the hook command with Goose's payload and block shape.
108
+ `tezgah-doctor --stack` reads Goose's hints, skills, plugins and the
109
+ `extensions:` map of `config.yaml`. No run has shown the binary obeying the
110
+ block. README and docs/layers.md now name all thirteen hosts.
111
+ - **Antigravity CLI is a host (`antigravity`, plan 139 slice 4).**
112
+ `tezgah-setup --install --hosts antigravity` writes tezgah's hook set under
113
+ the `tezgah` name of `~/.gemini/config/hooks.json`. The user's other names
114
+ stay. The Antigravity App and IDE load that file too. The timeouts are in
115
+ seconds: Stop 30, the rest 5. The status line goes in
116
+ `~/.gemini/antigravity-cli/settings.json` while it is plain JSON, and the skill
117
+ links go in `antigravity-cli/skills`. Antigravity adds no co-author
118
+ trailer of its own, so there is nothing to turn off. The always-on block is
119
+ the `~/.gemini/GEMINI.md` one Gemini CLI shares. An uninstall now keeps it only
120
+ while another armed host reads the same file, so with `GEMINI_CLI_HOME` set
121
+ the two blocks come and go apart.
122
+ - `hosts/antigravity/hook.py` handles SessionStart, PreInvocation,
123
+ PreToolUse, PostToolUse and Stop. The payload names no event, so each
124
+ command passes it as its first argument. `conversationId` is the session
125
+ and `workspacePaths[0]` the cwd. The adapter renames the PascalCase tool
126
+ arguments into the gate's fields: `CommandLine`, `TargetFile`, `CodeContent`,
127
+ `TargetContent`/`ReplacementContent` and `ReplacementChunks`.
128
+ - PreToolUse denies with a top-level `decision: deny`. An allowed call
129
+ answers `decision: allow`, since Antigravity requires the field. PostToolUse reads
130
+ the outcome off `error`: empty is a pass, anything else a failure. The first
131
+ invocation of a turn writes the turn row and carries the reminder as an
132
+ `ephemeralMessage`. A loop that a refusal re-entered writes no turn row, so
133
+ the refusal budget holds.
134
+ - Stop reads the reply from `transcriptPath`, or writes a `reply_unread` row.
135
+ It judges only an idle `model_stop`, and `decision: continue` sends the
136
+ model back into the loop with the reason.
137
+ - `--report` has Antigravity rows, including one for `enabled: false` on the
138
+ `tezgah` name. `--report --live` probes the hook command with Antigravity's
139
+ camelCase payload and deny shape. No run has shown the binary obeying the
140
+ deny.
141
+ - **Gemini CLI is a host (`gemini`, plan 139 slice 3).** `tezgah-setup
142
+ --install --hosts gemini` writes tezgah's own extension dir,
143
+ `~/.gemini/extensions/tezgah/`. It holds `gemini-extension.json`,
144
+ `hooks/hooks.json` and links to the skills.
145
+ `GEMINI_CLI_HOME` moves `~/.gemini` under it. The timeouts are in
146
+ milliseconds: AfterAgent 30000, the rest 5000. settings.json is never
147
+ written, since it is JSON with comments and holds the user's keys. Gemini's
148
+ default prompt adds no co-author trailer and no setting names one. The
149
+ always-on block goes in `~/.gemini/GEMINI.md`, which Antigravity reads too.
150
+ `--uninstall` deletes the extension dir and strips the block only while no
151
+ other host that reads it stays armed.
152
+ - `hosts/gemini/hook.py` handles SessionStart, BeforeAgent, BeforeTool,
153
+ AfterTool and AfterAgent. BeforeAgent writes the turn row and carries the
154
+ per-turn reminder. BeforeTool maps `run_shell_command`, `write_file`,
155
+ `replace`, the grep tools and the subagent tools into the gate's names, and
156
+ `mcp_<server>_<tool>` into `mcp__<server>_<tool>`. It denies with a
157
+ top-level `decision: deny` on exit 0.
158
+ - AfterTool reads the outcome off `tool_response`: an `error` is a failure,
159
+ and a shell result's `Exit Code: N` names a non-zero code. A shell run that
160
+ printed its `Output:` with no exit-code line is a pass, since Gemini writes
161
+ that line only for a non-zero code. AfterAgent judges `prompt_response` and
162
+ denies with `decision: deny`. Gemini sends the reason back through
163
+ BeforeAgent as the next prompt, and that turn row does not reset the
164
+ refusal budget. The status segment rides `systemMessage`.
165
+ - `--report` has Gemini rows, including one for `hooksConfig.enabled: false`
166
+ in settings.json, which runs no hook at all. `--report --live` probes the
167
+ hook command with Gemini's own payload and deny shape. No run has shown the
168
+ binary loading the extension or obeying the deny.
169
+ - **GitHub Copilot CLI is a host (`copilot`, plan 139 slice 2).** `tezgah-setup
170
+ --install --hosts copilot` writes `$COPILOT_HOME/hooks/tezgah.json`, a hooks
171
+ file that is wholly tezgah's. `COPILOT_HOME` defaults to `~/.copilot`. The
172
+ timeouts are in seconds: PreToolUse 10, Stop and SubagentStop 30, the rest 5.
173
+ The inline `hooks` key of settings.json is never used, since one bad item
174
+ there rejects every hook in it. The install sets two keys in settings.json
175
+ while it is plain JSON. The status line runs `tezgah-statusline --copilot`.
176
+ `includeCoAuthoredBy` is `false`, so the agent's commits carry no co-author
177
+ trailer. A value the user set stays. The install links the skills into
178
+ `$COPILOT_HOME/skills` and writes the always-on block in
179
+ `copilot-instructions.md`. `--uninstall` deletes tezgah.json and takes back
180
+ only tezgah's settings keys, links and block.
181
+ - `hosts/copilot/hook.py` uses the PascalCase events, so payloads carry
182
+ snake_case fields and Claude tool names. It maps the runtime spellings too.
183
+ An Edit that names no file but carries a patch reaches the gate as
184
+ `apply_patch`. An MCP tool's `<server>-<tool>` becomes
185
+ `mcp__<server>__<tool>` for a server `mcp-config.json` names. It denies
186
+ with a top-level `permissionDecision: deny` on exit 0.
187
+ - A PreToolUse hook that times out lets the call through, by Copilot's own
188
+ rule. One that crashes denies the call. A repo's `.claude/settings.json`
189
+ hooks run there too.
190
+ - PostToolUse records a pass and PostToolUseFailure a failure. A shell
191
+ result's `exit code N` overrides either. Stop carries no reply, so the hook
192
+ reads the last `assistant.message` from `transcript_path`. A turn end with
193
+ none there becomes a `reply_unread` row. Copilot drops a command hook's
194
+ UserPromptSubmit output, so there is no per-turn reminder. The event still
195
+ writes the turn row. subagentStart gives a delegate the brief. A
196
+ SubagentStop only records its verdict.
197
+ - `--report` has Copilot rows, and `--report --live` probes the hook command
198
+ with Copilot's own payload and deny shape. No run has shown the binary
199
+ obeying the deny.
200
+ - **Command Code is a host (`commandcode`, plan 139 slice 1).** `tezgah-setup
201
+ --install --hosts commandcode` merges tezgah's hook groups into the `hooks`
202
+ key of `~/.commandcode/settings.json` beside the user's. Their timeouts are in
203
+ seconds. The same write sets `attribution.commit` to `""`, so commits carry no
204
+ `CommandCodeBot` co-author trailer. A value the user set stays. The install
205
+ links the skills into `~/.commandcode/skills`. It writes the always-on block
206
+ in `~/.commandcode/AGENTS.md`. `--uninstall` takes back only tezgah's groups,
207
+ links and block. It drops the attribution key only while it holds what the
208
+ install wrote.
209
+ - Command Code's settings hooks see only shell, read, write and edit. So the
210
+ PreToolUse matcher is `shell|write|edit`. The gate there never sees read,
211
+ MCP, subagent or web calls, and plan mode skips every hook.
212
+ - `hosts/commandcode/hook.py` maps `shell_command`, `write_file` and
213
+ `edit_file` onto the gate. It folds a shell call's `args` into `command`.
214
+ It hands `old_value`/`new_value` over as `old_string`/`new_string`. It
215
+ denies with `permissionDecision: deny` on exit 0. It reads a shell call's
216
+ outcome off an `exit code N` in its result. A result with no code counts
217
+ as a run, never a pass.
218
+ - Stop carries no reply, so the hook reads it from `transcript_path`. A turn
219
+ end with none there becomes a `reply_unread` row. The status segment rides
220
+ `systemMessage` at SessionStart and Stop. There is no prompt event, so no
221
+ per-turn reminder.
222
+ - `--report` has Command Code rows, and `--report --live` probes the hook
223
+ command with Command Code's own payload. No run has shown the binary
224
+ obeying the deny.
225
+ - **Factory Droid is a host (`droid`, plan 138 wave 1).** `tezgah-setup --install
226
+ --hosts droid` merges tezgah's hook groups into `~/.factory/hooks.json` beside
227
+ the user's. Their timeouts are in seconds, as Droid reads them. The install
228
+ also links the skills into `~/.factory/skills` beside the user's own links.
229
+ It writes the always-on block in `~/.factory/AGENTS.md`. It sets the
230
+ `statusLine` command and `includeCoAuthoredByDroid: false` in `settings.json`.
231
+ The install edits a settings.json with comments in place and keeps its
232
+ comments (plan 141). `--uninstall` takes back only tezgah's groups, links and keys.
233
+ - Droid reads the `hooks` key of settings.json only while hooks.json is
234
+ absent. So the install creates no hooks.json beside such a key, and the
235
+ report row says why. Then settings.json stays untouched.
236
+ - `hosts/droid/hook.py` maps Droid's tool ids (`Execute`, `Create`,
237
+ `ApplyPatch`, `FetchUrl`) onto the gate and hands ApplyPatch's `input` over
238
+ as `patch`. It denies with `permissionDecision: deny` on exit 0 and reads a
239
+ shell call's outcome off its `[Process exited with code N]` line.
240
+ - Droid's Stop carries no reply, so the hook reads the reply from
241
+ `transcript_path`. A turn end whose reply is not there yet becomes a
242
+ `reply_unread` row and is not judged. SubagentStop records `task_result`
243
+ as a `subagent_end` row.
244
+ - `--report` has Droid rows, and `--report --live` probes the hook command
245
+ with Droid's own payload. `tests/e2e_droid.py` runs the real binary only
246
+ under `TEZGAH_E2E_SPEND=1`, because it spends a model call.
247
+
248
+ - **Qwen Code is a host (`qwen`, plan 138 wave 1).** `tezgah-setup --install
249
+ --hosts qwen` merges tezgah's hook groups into `~/.qwen/settings.json` beside
250
+ Orca's. Their timeouts are in milliseconds, as Qwen reads them. The install
251
+ also adds the `ui.statusLine` command, the skill links and the always-on
252
+ block in `~/.qwen/QWEN.md`. `--uninstall` takes back only tezgah's groups and keys.
253
+ - `hosts/qwen/hook.py` maps Qwen's tool ids (`run_shell_command`,
254
+ `write_file`, `edit`, `agent`, `grep_search`) onto the gate. It denies with
255
+ `permissionDecision: deny` on exit 0, blocks Stop on
256
+ `last_assistant_message`, briefs a subagent at SubagentStart and records
257
+ its end at SubagentStop.
258
+ - `--report` has Qwen rows, and `--report --live` probes the hook command
259
+ with Qwen's own payload. `tests/e2e_qwen.py` runs the real binary only
260
+ under `TEZGAH_E2E_SPEND=1`, because it spends a model call.
261
+ - `tezgah-doctor --stack` and the session attestation know the host. An
262
+ unknown host no longer crashes the attestation.
263
+ - A host-file backup is now owner-only, and a rewritten host config keeps
264
+ its file mode, because Qwen's settings.json holds provider keys.
265
+
266
+ - **Review rules taken from google/elephant-goldfish-model (plan 137).** The
267
+ owner chose them from research line `egm-lessons`. No EGM file ships:
268
+ - `tezgah-task review NNN <r> approve` refuses a plan with no review slice
269
+ record. A host with no slice journal passes `--no-slice "<why>"`, and the
270
+ reason lands on the review line (`bin/tezgah-task`).
271
+ - The contract says that no fresh subagent means no review verdict. It also
272
+ says the author contests a finding only with a file:line and never lowers
273
+ a severity (`hooks/tezgah_policy.py`).
274
+ - tezgah-reviewer has a plan-readiness mode for a plan file. A design choice
275
+ left to the implementer is a major finding, and so is a section it cannot
276
+ paraphrase. plan-add runs it before `phase implementation`
277
+ (`hooks/tezgah_agents.py`).
278
+ - graph-review merges findings with the same file, line and title before its
279
+ verification cap, and reports `caps.deduplicated`.
280
+ - `tests/test_links.py` resolves every relative markdown link the repository
281
+ owns.
282
+
283
+ ### Changed
284
+
285
+ - **The gate refuses a credit verb with "with" or "using" too.** The attribution
286
+ rule read `written by` and `built by`, so `Drafted with Claude` passed. The
287
+ credit verbs now include `drafted`, `produced` and `composed`, and each takes
288
+ `by`, `with` or `using` before a model or vendor name. Prose such as
289
+ `drafted with the owner` still passes (`hooks/tezgah_gate.py::_CREDIT`, plan
290
+ 137).
291
+
292
+ - **A release tag must sit on main.** A new `on-main` job in `release.yml`
293
+ fails the run on a tag off `origin/main`. Then no job builds or uploads
294
+ anything (plan 137).
295
+
296
+ - **An owner-only choice waits for the owner.** Class F now names three more
297
+ steps. One is picking a variant a research, analysis or review recommends.
298
+ The others are a way around a tezgah refusal and deciding about another
299
+ session's work. The core
300
+ Ask policy, the per-turn reminder and the ask warning carry them too. The Stop
301
+ rule's `needless ask` lets a question that asks the owner to pick between
302
+ options through (`OWNER_CHOICE`, `hooks/tezgah_integrity.py`). A worker returns
303
+ such a question as `ESCALATE: owner`, and the orchestrator relays it to the
304
+ user instead of answering it (plan 136).
305
+
306
+ - **A research line records the owner's pick apart from its own.** A rules-9
307
+ line writes its pick as `recommended:` in `decision.md`. A `chosen:` line
308
+ needs the owner's words, which `tezgah-research choose <slug> <decision>
309
+ <variant> --ack "<words>"` writes. The line's rules judge its own pick, never
310
+ the owner's, so an owner may take a dominated variant. `conclude` says when a
311
+ recommendation still waits for the owner (`hooks/tezgah_research.py`).
312
+
313
+ - **A line in flight no longer blocks `init --allow-open`.** A line whose only
314
+ `check` errors are pending verdicts is in flight, not broken. Both `init`
315
+ refusals now say that another session's line goes to the owner.
316
+
317
+ ### Fixed
318
+
319
+ - **Three new hosts no longer record a check that did not pass as a pass.**
320
+ Antigravity's PostToolUse has no output. A command that went to the
321
+ background also ends with an empty `error`. So the hook records each
322
+ Antigravity shell call as ran, never a pass, and Antigravity can never show
323
+ a passing check. A Copilot shell success with no `exit code N` text (a
324
+ background launch) is ran, never a pass. A Gemini result with a `Signal:`
325
+ line (a runner killed by a signal) is a failure.
326
+ - **The gate guards Gemini's and Goose's hook-off keys.** These are Gemini's
327
+ `hooksConfig` and Goose's `disabledPlugins`. Claude's `disableAllHooks` had
328
+ the same guard.
329
+ - **The Command Code uninstall removes tezgah's `attribution.commit: ""`
330
+ beside the user's other attribution keys**, not only when it stood alone.
331
+
332
+ - **A hidden research dir is no longer invisible.** A dot dir under
333
+ `.tezgah/research/` is an error in `status` and in `check` with no slug. The
334
+ gate refuses a write there (`research_hidden_reason`, `hooks/tezgah_gate.py`).
335
+ A session had run a whole line under `.tezgah/research/.staging/` after an
336
+ `init` refusal.
337
+
9
338
  ## [1.11.0] - 2026-10-23
10
339
 
11
340
  ### Added
@@ -1299,7 +1628,10 @@ adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
1299
1628
  the copy could not find `hooks/` and died on import. It now links into the
1300
1629
  installed tree, and also installs the command as `tezgah`.
1301
1630
 
1302
- [Unreleased]: https://github.com/r1z4x/tezgah/compare/v1.10.1...HEAD
1631
+ [Unreleased]: https://github.com/r1z4x/tezgah/compare/v1.13.0...HEAD
1632
+ [1.13.0]: https://github.com/r1z4x/tezgah/releases/tag/v1.13.0
1633
+ [1.12.0]: https://github.com/r1z4x/tezgah/releases/tag/v1.12.0
1634
+ [1.11.0]: https://github.com/r1z4x/tezgah/releases/tag/v1.11.0
1303
1635
  [1.10.1]: https://github.com/r1z4x/tezgah/releases/tag/v1.10.1
1304
1636
  [1.10.0]: https://github.com/r1z4x/tezgah/releases/tag/v1.10.0
1305
1637
  [1.9.2]: https://github.com/r1z4x/tezgah/releases/tag/v1.9.2
package/MANIFEST CHANGED
@@ -161,19 +161,28 @@ hooks/tezgah_taste.py
161
161
  hooks/tezgah_taste_ledger.py
162
162
  hooks/tezgah_untrusted.py
163
163
  hooks/tezgah_update.py
164
+ hosts/amp/hook.py
165
+ hosts/amp/tezgah-plugin.ts.in
166
+ hosts/antigravity/hook.py
164
167
  hosts/codex/hook.py
165
168
  hosts/codex/hooks.json
169
+ hosts/commandcode/hook.py
170
+ hosts/copilot/hook.py
166
171
  hosts/cursor/hook.py
167
172
  hosts/cursor/hooks.json
173
+ hosts/droid/hook.py
168
174
  hosts/dsh/hooks.json
169
175
  hosts/dsh/statusline/lib/client.js
170
176
  hosts/dsh/statusline/lib/index.js
171
177
  hosts/dsh/statusline/package.json
178
+ hosts/gemini/hook.py
179
+ hosts/goose/hook.py
172
180
  hosts/omp/hook.py
173
181
  hosts/omp/tezgah-hook.ts.in
174
182
  hosts/opencode/plugins/tezgah.js
175
183
  hosts/opencode/tui/cli/tui.tsx
176
184
  hosts/opencode/tui/tezgah-tui.tsx
185
+ hosts/qwen/hook.py
177
186
  output-styles/tezgah.md
178
187
  package.json
179
188
  packaging/build.sh
@@ -830,6 +839,7 @@ skills/rl-env/rl-env-from-description/references/verifiers.md
830
839
  skills/tezgah-contract/EVALS.md
831
840
  skills/tezgah-contract/SKILL.md
832
841
  statusline.py
842
+ tests/_amp_plugin_harness.mjs
833
843
  tests/_fuzz_shell_reader.mjs
834
844
  tests/_mcp_stdio.py
835
845
  tests/_omp_extension_harness.mjs
@@ -849,10 +859,12 @@ tests/e2e_analyze_mobile.py
849
859
  tests/e2e_analyze_web.py
850
860
  tests/e2e_analyze_wiring.py
851
861
  tests/e2e_docker_cycle.py
862
+ tests/e2e_droid.py
852
863
  tests/e2e_dsh_statusline.py
853
864
  tests/e2e_omp_statusline.py
854
865
  tests/e2e_packaged_install.py
855
866
  tests/e2e_plan_flow.py
867
+ tests/e2e_qwen.py
856
868
  tests/e2e_tezgah_mcp.py
857
869
  tests/fixtures/rules8-known-truth/bodies/arxiv-1.xml
858
870
  tests/fixtures/rules8-known-truth/bodies/arxiv-10.xml
@@ -906,6 +918,9 @@ tests/stop_spec_eval.py
906
918
  tests/support.py
907
919
  tests/test_agents.py
908
920
  tests/test_ai_research_library.py
921
+ tests/test_amp_hook.py
922
+ tests/test_amp_plugin.py
923
+ tests/test_antigravity_hook.py
909
924
  tests/test_apps_registry.py
910
925
  tests/test_attest.py
911
926
  tests/test_bash_agreement.py
@@ -916,12 +931,14 @@ tests/test_claude_adapters.py
916
931
  tests/test_claude_registration.py
917
932
  tests/test_codegen.py
918
933
  tests/test_codex_hook.py
934
+ tests/test_commandcode_hook.py
919
935
  tests/test_components.py
920
936
  tests/test_constraint_pinning.py
921
937
  tests/test_consult_arena.py
922
938
  tests/test_context.py
923
939
  tests/test_context_diet.py
924
940
  tests/test_control_plane.py
941
+ tests/test_copilot_hook.py
925
942
  tests/test_cursor_cap.py
926
943
  tests/test_cursor_hook.py
927
944
  tests/test_decisions.py
@@ -929,6 +946,7 @@ tests/test_design_cli.py
929
946
  tests/test_docs.py
930
947
  tests/test_docs_router.py
931
948
  tests/test_doctor.py
949
+ tests/test_droid_hook.py
932
950
  tests/test_dsh_hooks.py
933
951
  tests/test_dsh_launcher.py
934
952
  tests/test_dsh_statusline.py
@@ -940,6 +958,8 @@ tests/test_extension.py
940
958
  tests/test_fuzz_shell.py
941
959
  tests/test_gate.py
942
960
  tests/test_gate_vectors.py
961
+ tests/test_gemini_hook.py
962
+ tests/test_goose_hook.py
943
963
  tests/test_graph_verbs.py
944
964
  tests/test_graph_workflows.py
945
965
  tests/test_guard.py
@@ -961,6 +981,7 @@ tests/test_lesson_lineage.py
961
981
  tests/test_lessons.py
962
982
  tests/test_lessons_compile.py
963
983
  tests/test_lifecycle_sweep.py
984
+ tests/test_links.py
964
985
  tests/test_live_probe.py
965
986
  tests/test_mcp_features.py
966
987
  tests/test_mcp_server.py
@@ -981,6 +1002,7 @@ tests/test_platform.py
981
1002
  tests/test_product_flow_e2e.py
982
1003
  tests/test_product_next.py
983
1004
  tests/test_providers.py
1005
+ tests/test_qwen_hook.py
984
1006
  tests/test_rank.py
985
1007
  tests/test_replay.py
986
1008
  tests/test_replay_context.py
package/README.md CHANGED
@@ -35,8 +35,9 @@ reaches every host the same way.
35
35
 
36
36
  ## What tezgah enforces and records
37
37
 
38
- - **One contract, six hosts.** omp, Claude Code, Codex, Cursor, opencode and
39
- dsh see the same rules, because every host is a thin adapter over one shared
38
+ - **One contract, fourteen hosts.** omp, Claude Code, Codex, Cursor, opencode,
39
+ dsh, Qwen Code, Factory Droid, Command Code, GitHub Copilot CLI, Gemini CLI,
40
+ Antigravity CLI, Goose and Amp see the same rules, because every host is a thin adapter over one shared
40
41
  core — change a rule once and all of them pick it up.
41
42
  - **A done claim needs a passing check on record.** A gate of 15 refusals stops
42
43
  the neutered check — `--no-verify`, `|| true`, a test piped into `tail`, a
@@ -154,7 +155,7 @@ and the optional integrations are in the [docs](docs/README.md).
154
155
 
155
156
  ## Supported hosts
156
157
 
157
- **omp** (the primary host, developed and verified against), **Claude Code**, **Codex**, **Cursor**, **opencode**, **dsh** — thin adapters over one shared core.
158
+ **omp** (the primary host, developed and verified against), **Claude Code**, **Codex**, **Cursor**, **opencode**, **dsh**, **Qwen Code**, **Factory Droid**, **Command Code**, **GitHub Copilot CLI**, **Gemini CLI**, **Antigravity CLI**, **Goose**, **Amp** — thin adapters over one shared core. The last six are wired from their vendors' docs alone: no run of their binaries has shown them obeying a deny yet ([hosts](docs/hosts.md)).
158
159
 
159
160
  ## Where the depth lives
160
161
 
package/bin/tezgah-docs CHANGED
@@ -106,7 +106,7 @@ RUN = re.compile(r"((?:`[A-Za-z_][A-Za-z0-9_]*(?:\(\))?`\s*[/,]?\s*){1,5})$")
106
106
  CITE = re.compile(r"`([\w.-]+(?:/[\w.-]+)+|[\w-]+\.\w+):(\d+)(?:-(\d+))?`"
107
107
  r"|`(:(\d+)(?:-(\d+))?)`")
108
108
  # The symbol written after the citation instead of before it, the way a table
109
- # cell reads: `hooks/tezgah_policy.py:806-1135` (`CORE`).
109
+ # cell reads: `hooks/tezgah_policy.py:819-1158` (`CORE`).
110
110
  AFTER = re.compile(r"\s*\(`([A-Za-z_][A-Za-z0-9_]*)(?:\(\))?`\)")
111
111
  # Any `path:line`, backticked or not: what CITE does not match of these (a
112
112
  # bare token, a comma list `x.py:12,40`) is counted as unjudged, so a citation
@@ -25,6 +25,8 @@
25
25
  tezgah-research verdict <slug> <id> met|not-met|unanswerable --evidence "<ref>"
26
26
  record one success criterion's verdict
27
27
  tezgah-research conclude <slug> conclude a line whose criteria all carry a verdict
28
+ tezgah-research choose <slug> <decision> <variant> --ack "<the owner's words>"
29
+ record the owner's pick of a recommended variant
28
30
  tezgah-research migrate-layout move flat research/<slug> into open/ or done/
29
31
  tezgah-research close <slug> --limit "<reason>" conclude a line as a deliberate limit
30
32
  tezgah-research seal <slug> [--history-lost] --ack "<owner decision>"
@@ -121,14 +123,20 @@ no status, a design/plan/analysis/code deliverable with no decision comparing it
121
123
  variants - printing every open slug with its reasons. `--allow-open "<reason>"`
122
124
  opens the new line anyway and writes the reason into its `log.md` as the first
123
125
  entry; an empty reason is misuse, and the hatch is refused while any open line has
124
- `check` errors. `close <slug> --limit "<reason>"` concludes a line on purpose,
126
+ `check` errors other than a criterion whose verdict is still `pending` (a line in
127
+ flight). Both refusals say that a line another session is running goes to the
128
+ owner, not to this session. `close <slug> --limit "<reason>"` concludes a line on purpose,
125
129
  writing the reasons it was still open into `state.json` `closed` and `log.md`.
126
130
  `--supersedes <old>` opens a new version of an existing line, which then carries
127
131
  the old deliverable as a variant; a line asking an existing line's exact question
128
132
  without it is refused. `compare` appends one cell to
129
133
  `decisions/<decision>/comparison.jsonl` under the rule `check` applies to it.
134
+ A rules-9 decision.md writes the line's pick as `recommended:`; `choose` records
135
+ the owner's `chosen:` with an `owner:` line holding their words, and `conclude`
136
+ names each recommendation still waiting for it.
130
137
  `check` prints the same list of open lines after the per-line report, and so does
131
- `status`, so neither state has to be discovered by being refused.
138
+ `status`, so neither state has to be discovered by being refused. Both also fail
139
+ a hidden dir under `research/`, which no line may live in.
132
140
 
133
141
  `init --method exploratory` (rules 7) opens a campaign instead of a question:
134
142
  `--arena` and `--budget` (and, for a measurable arena, `--evaluator` with its
@@ -286,6 +294,7 @@ def cmd_init(args):
286
294
  print(" %s: %s" % (name, "; ".join(reasons)))
287
295
  print('to open %s anyway, say why: tezgah-research init %s '
288
296
  '--allow-open "<reason>"' % (slug, slug))
297
+ print(tr.OWNER_ASK)
289
298
  return 1
290
299
  broken = tr.broken_open_lines(repo) if unfinished else []
291
300
  if broken:
@@ -297,6 +306,7 @@ def cmd_init(args):
297
306
  print(" %s: %d error(s), first: %s" % (name, len(errors), errors[0]))
298
307
  print('fix them, or record what is left: tezgah-research close <slug> '
299
308
  '--limit "<reason>"')
309
+ print(tr.OWNER_ASK)
300
310
  return 1
301
311
  twins = [t for t in tr.serial_twins(repo, slug, {"question": question})
302
312
  if t != supersedes]
@@ -490,7 +500,7 @@ def cmd_check(args):
490
500
  return 1
491
501
  problems = 0
492
502
  report = {}
493
- if known:
503
+ if known or tr.stray_dirs(repo):
494
504
  report = tr.check(repo, rest[0] if rest else None, strict=strict,
495
505
  all_lines=all_lines, code=checkout_root(os.getcwd()),
496
506
  flags={"online": online, "orx": ask_orx})
@@ -1067,6 +1077,28 @@ def cmd_conclude(args):
1067
1077
  print("FAIL conclude: %s" % problem)
1068
1078
  return 1
1069
1079
  print("concluded %s; it moved to research/done/%s" % (slug, slug))
1080
+ for decision, variant in tr.owner_waits(repo, slug):
1081
+ print("report to the owner: %s is recommended, not chosen; building it waits "
1082
+ "for the owner's answer (tezgah-research choose %s %s %s --ack "
1083
+ "\"<their words>\")" % (variant, slug, decision, variant))
1084
+ return 0
1085
+
1086
+
1087
+ def cmd_choose(args):
1088
+ """Record the owner's pick among a decision's produced variants."""
1089
+ usage = 'choose <slug> <decision> <variant> --ack "<the owner\'s words>"'
1090
+ if len(args) != 5 or args[3] != "--ack":
1091
+ return misuse(usage)
1092
+ slug, decision, variant, _flag, ack = args
1093
+ repo = repo_root(os.getcwd())
1094
+ if slug not in tr.slugs(repo):
1095
+ return misuse("no such research line: %s" % slug)
1096
+ problem = tr.choose_line(repo, slug, decision, variant, ack,
1097
+ time.strftime("%Y-%m-%d"))
1098
+ if problem:
1099
+ print("FAIL choose: %s" % problem)
1100
+ return 1
1101
+ print("decisions/%s: the owner chose %s" % (decision, variant))
1070
1102
  return 0
1071
1103
 
1072
1104
 
@@ -1212,6 +1244,8 @@ def main(argv):
1212
1244
  return cmd_verdict(rest)
1213
1245
  if cmd == "conclude":
1214
1246
  return cmd_conclude(rest)
1247
+ if cmd == "choose":
1248
+ return cmd_choose(rest)
1215
1249
  if cmd == "migrate-layout":
1216
1250
  return cmd_migrate_layout(rest)
1217
1251
  if cmd == "close":
@@ -1237,7 +1271,8 @@ def main(argv):
1237
1271
  if cmd in commands:
1238
1272
  return commands[cmd](repo_root(os.getcwd()), rest)
1239
1273
  return misuse("unknown command %r (init, commit, check, status, --all, claim, predict, "
1240
- "components, migrate, source, compare, verdict, conclude, migrate-layout, "
1274
+ "components, migrate, source, compare, verdict, conclude, choose, "
1275
+ "migrate-layout, "
1241
1276
  "close, seal, import, next, idea, stage, match, board, dedup, yield, cite, "
1242
1277
  "fetch, search, instrument, gold, label, switch, release-check, "
1243
1278
  "calibration)" % cmd)