@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/docs/research.md CHANGED
@@ -69,13 +69,15 @@ sections below. Rules 5 add the scope lock. Rules 6 add the selection warnings
69
69
  ("Held-out set, repeats and noise band" below). Rules 7 add the `exploratory`
70
70
  method ("Rules 7: exploratory lines" below). A bump never weakens an older set.
71
71
  Rules 8 (`EVIDENCE_RULES`) add captured evidence and a computed certainty per
72
- claim ("Rules 8: captured evidence and computed certainty" below). `init` writes 8
73
- now: `RULES` is `EVIDENCE_RULES` (`hooks/tezgah_research.py::EVIDENCE_RULES`).
72
+ claim ("Rules 8: captured evidence and computed certainty" below). Rules 9
73
+ (`OWNER_CHOICE_RULES`) split a decision's pick: the line writes `recommended:`,
74
+ and `chosen:` is the owner's ("Variants of the deliverable" below). `init` writes 9
75
+ now: `RULES` is `OWNER_CHOICE_RULES` (`hooks/tezgah_research.py::OWNER_CHOICE_RULES`).
74
76
 
75
- A `rules` above `EVIDENCE_RULES` means a later tezgah wrote the line. `check` then reports
77
+ A `rules` above `RULES` means a later tezgah wrote the line. `check` then reports
76
78
  one error that names both numbers and reads nothing else. The writers refuse the
77
79
  line too: `claim`, `predict`, `compare`, `source --run`, `verdict`, `conclude`,
78
- `close` and `migrate` (`newer_rules`). Upgrade tezgah to work on such a line.
80
+ `close`, `choose` and `migrate` (`newer_rules`). Upgrade tezgah to work on such a line.
79
81
 
80
82
  ## Protocol sections (rules 4)
81
83
 
@@ -170,7 +172,7 @@ One comparison lives in `decisions/<id>/`, and one loader reads its files
170
172
  | `criteria.json` | `criteria`: a list of `{id, kind, direction}`, with `kind` `measured` or `judged` and `direction` `max`, `min` or `pass`. `baseline_variant`: the id of the status-quo variant in `variants.jsonl` |
171
173
  | `variants.jsonl` | one `{id, status}` row per variant. A `produced` variant names its `artifact`: a file in the line or the repository, `git:<rev>` or `orx:<id>`. A `dropped` one gives `drop_reason` |
172
174
  | `comparison.jsonl` | one cell per variant and criterion, written by `compare` |
173
- | `decision.md` | the choice and why the others lost |
175
+ | `decision.md` | the line's pick (`recommended:`), the owner's (`chosen:` with `owner:`) and why the others lost |
174
176
 
175
177
  Write and commit `criteria.json` and `variants.jsonl` before the first cell. Then
176
178
  record each cell with `tezgah-research compare <slug> <decision>`, one JSON object
@@ -190,12 +192,27 @@ this same rule.
190
192
  - A judged cell with fewer than two distinct raters warns.
191
193
  - The criteria have to enter the history before the first cell, and
192
194
  `comparison.jsonl` only grows.
193
- - `decision.md` names a produced variant on a `chosen: <id>` line.
195
+ - `decision.md` names a produced variant on a `chosen: <id>` line, else on a
196
+ `recommended: <id>` line (`hooks/tezgah_research.py::decision_pick`).
197
+ - On a rules-9 line the line writes its own pick as `recommended:`. A `chosen:`
198
+ line with no `owner:` line fails with `names no owner choice`. Choosing a
199
+ variant or option a research, analysis or review recommends is the owner's
200
+ call. `tezgah-research choose <slug> <decision> <variant> --ack
201
+ "<the owner's words>"` writes `chosen:` and `owner: <date> "<words>"` and one
202
+ `log.md` line (`hooks/tezgah_research.py::choose_line`). It works on a done
203
+ line too: the order seal hashes the experiments alone. It keeps the line's
204
+ own pick. A pick a rules-8 line wrote as `chosen:` becomes `recommended:`. A
205
+ rules-8 line's `chosen:` is read as before.
206
+ - `conclude` prints `report to the owner: <id> is recommended, not chosen` for
207
+ each decision still waiting (`hooks/tezgah_research.py::owner_waits`).
208
+ - The three rules below judge the line's own pick, never the owner's
209
+ (`hooks/tezgah_research.py::line_pick`). An owner may take a dominated
210
+ variant, and that is no error of the line.
194
211
  - Each other produced variant shares a line of `decision.md` with a criterion id
195
212
  that ruled it out. Else: `rejects <id> on no criterion`.
196
213
  - `decision.md` has a `flip:` line, the result that would reverse the choice.
197
214
  - When every criterion is `max` or `min` and scored, no produced variant may
198
- dominate the chosen one. A dominating variant is at least as good on every
215
+ dominate the picked one. A dominating variant is at least as good on every
199
216
  criterion and better on one (`hooks/tezgah_research.py::dominated_by`).
200
217
 
201
218
  ## One line, one directory
@@ -263,7 +280,15 @@ rule names the open lines with their reason counts on every research turn
263
280
  "<reason>"`, which writes the reason into the new line's `log.md` - an explicit
264
281
  decision on the record instead of a silent second line - and which is itself
265
282
  refused while any open line has `check` errors (`broken_open_lines`): a hatch past
266
- broken lines is how they stayed broken. What is left of a line that will not be
283
+ broken lines is how they stayed broken. A success criterion whose verdict is
284
+ still `pending` is a line in flight, not a broken one. That error alone does not
285
+ block the hatch, and any other error does. A line another session is running is
286
+ not this session's to fix or close. Both refusals say so
287
+ (`hooks/tezgah_research.py::OWNER_ASK`). The session stops and asks the owner,
288
+ never a way around a tezgah refusal (another folder, a hidden dir). A hidden dir
289
+ under `research/`, `research/open/` or `research/done/` holds no line `check` can
290
+ see. The no-slug `check` and `status` fail it by its path
291
+ (`hooks/tezgah_research.py::stray_dirs`). What is left of a line that will not be
267
292
  finished is recorded with `tezgah-research close <slug> --limit "<reason>"`
268
293
  (`close_line`), which concludes it and writes the reasons it was still open into
269
294
  `state.json` `closed` and `log.md` and seals it ("the order seal", below). Such
@@ -292,7 +317,7 @@ through its seal.
292
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 |
293
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 |
294
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 |
295
- | `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) | 0 |
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 |
296
321
  | `tezgah-research --all` | every checkout of this repository (the main checkout and each linked `git worktree`, `tezgah_paths.worktrees`), one header per checkout, then ` <slug>: <phase>` per line with the reasons it is still open; a checkout with none says `no research line`. Read-only: each checkout keeps its own `.tezgah`, locks and private repository, and nothing here writes to any of them (`across`, `hooks/tezgah_research.py::across`) | 0, 2 misuse |
297
322
  | `tezgah-research claim <slug>` | reads one claim from stdin and either appends it under an exclusive lock or refuses it, printing one reason per problem | 0, 1 refused, 2 misuse |
298
323
  | `tezgah-research predict <slug>` | reads one prediction row from stdin and either appends it under the same lock or refuses it with one reason per problem; a row whose `commit` git cannot place is appended with the warning printed, which is the fail-open `check` uses, and a row written now has to name at least one component the manifest defines (`append_prediction`, `hooks/tezgah_research.py::append_prediction`) | 0, 1 refused, 2 misuse |
@@ -302,7 +327,8 @@ through its seal.
302
327
  | `tezgah-research compare <slug> <decision>` | reads one variants x criteria cell from stdin and appends it to `decisions/<decision>/comparison.jsonl` under the lock, or refuses it by the rule `check` applies (`append_comparison` and `comparison_problems`, `hooks/tezgah_research.py`); it notes when `criteria.json` is not committed yet | 0, 1 refused, 2 misuse |
303
328
  | `tezgah-research close <slug> --limit "<reason>"` | concludes the line as a deliberate limit, writing the reasons it was still open into `state.json` `closed` and `log.md`, and seals it (`close_line`) | 0, 1 unreadable state, 2 misuse |
304
329
  | `tezgah-research verdict <slug> <id> met\|not-met\|unanswerable --evidence "<ref>"` | records one success criterion's verdict and the evidence it rests on in the line's `state.json`, or refuses it with one `FAIL verdict:` line per problem (`verdict_problems`, `hooks/tezgah_research.py::verdict_problems`) | 0, 1 refused or unreadable state, 2 misuse |
305
- | `tezgah-research conclude <slug>` | concludes a line once every success criterion carries a verdict, seals it and moves it to `done/` (`conclude_line`, `hooks/tezgah_research.py::conclude_line`) | 0, 1 refused, 2 misuse |
330
+ | `tezgah-research conclude <slug>` | concludes a line once every success criterion carries a verdict, seals it and moves it to `done/` (`conclude_line`, `hooks/tezgah_research.py::conclude_line`); then prints `report to the owner: <id> is recommended, not chosen; ...` for each decision with no owner choice (`owner_waits`) | 0, 1 refused, 2 misuse |
331
+ | `tezgah-research choose <slug> <decision> <variant> --ack "<the owner's words>"` | records the owner's pick: `chosen:` and `owner: <date> "<words>"` in the decision's `decision.md`, one `log.md` line; refuses empty `--ack`, a missing decision and a variant the decision did not produce, on an open or a done line (`choose_line`, `hooks/tezgah_research.py::choose_line`) | 0, 1 refused, 2 misuse |
306
332
  | `tezgah-research migrate-layout` | moves flat `research/<slug>` directories into `open/` or `done/` by each line's state, and is idempotent (`migrate_layout`, `hooks/tezgah_research.py::migrate_layout`) | 0, 2 misuse |
307
333
  | `tezgah-research seal <slug> [--history-lost] --ack "<owner decision>"` | seals a line under `done/` that was concluded before seals existed: plainly, as `conclude` would, when every experiment's order still checks; with `--history-lost`, with that verdict for every experiment whose order the lost history left undecidable. Refuses a line with a real order violation, a plain seal on any order finding, and a `history-lost` seal with nothing lost (`retro_seal`, `hooks/tezgah_research.py::retro_seal`) | 0, 1 refused, 2 misuse |
308
334
  | `tezgah-research import <checkout> [<slug>] [--allow-open "<reason>"]` | brings a line - or every line this checkout lacks - from another checkout's `.tezgah` repository with its history; an open line arrives beside open ones only with `--allow-open` (`import_line`, `hooks/tezgah_research.py::import_line`) | 0, 1 refused, 2 misuse |
@@ -311,6 +311,12 @@ Every other surface reads the store.
311
311
  legend on hover, pins it on click (16-19,35,47-52,63-64).
312
312
  - **Codex** — hosts/codex/hook.py puts the plain line in `systemMessage`
313
313
  (182-184,195-198).
314
+ - **Amp** — the plugin draws the widest plain tier
315
+ (`hosts/amp/hook.py::status_line`) on Amp's experimental status item
316
+ (`createStatusItem`, TUI only, plain text). It draws only for the thread the
317
+ user is looking at. It redraws on `session.start`, `agent.end` and each
318
+ watched tool result, and shows `hook✗` while the hook fails. A build without
319
+ the API has no status surface (`hosts/amp/tezgah-plugin.ts.in`).
314
320
 
315
321
  ## Seeing and debugging the line
316
322
 
package/docs/testing.md CHANGED
@@ -66,7 +66,9 @@ conclude from either.
66
66
  `.github/workflows/ci.yml` runs on a push to `main` and on a pull request, so
67
67
  a branch commit runs once, through its pull request, not twice. A tag push does
68
68
  not start it. `release.yml` calls it (`ci.yml:13`) as its `ci` job, so npm and
69
- brew publish only after CI passed on the release commit. A newer push to the
69
+ brew publish only after CI passed on the release commit. Its first job,
70
+ `on-main`, fails the release when the tag's commit is not on `origin/main`.
71
+ Then no job builds or uploads anything. A newer push to the
70
72
  same ref cancels the run it replaces, except on a release (`concurrency`). Every
71
73
  job has a `timeout-minutes`, so a hung test cannot hold a runner for six hours.
72
74
  No matrix cancels its other legs when one fails (`fail-fast: false`).
package/hooks/hooks.json CHANGED
@@ -13,7 +13,7 @@
13
13
  { "hooks": [ { "type": "command", "command": "TEZGAH_HOST=claude \"${TEZGAH_PYTHON:-python3}\" \"${CLAUDE_PLUGIN_ROOT}/hooks/projects-auto-init.py\"", "timeout": 10 } ] }
14
14
  ],
15
15
  "PreToolUse": [
16
- { "matcher": "Agent|agent|Task|task|Grep|grep|Bash|bash|PowerShell|pwsh|Shell|shell|subagent|Edit|Write|MultiEdit|NotebookEdit|AskUserQuestion", "hooks": [ { "type": "command", "command": "\"${TEZGAH_PYTHON:-python3}\" \"${CLAUDE_PLUGIN_ROOT}/hooks/projects-pretooluse.py\"", "timeout": 5 } ] },
16
+ { "matcher": "Agent|agent|Task|task|Grep|grep|Bash|bash|PowerShell|pwsh|Shell|shell|Monitor|monitor|subagent|Edit|Write|MultiEdit|NotebookEdit|AskUserQuestion", "hooks": [ { "type": "command", "command": "\"${TEZGAH_PYTHON:-python3}\" \"${CLAUDE_PLUGIN_ROOT}/hooks/projects-pretooluse.py\"", "timeout": 5 } ] },
17
17
  { "matcher": "^mcp__(?:.*[^A-Za-z0-9])?(?:append|cancel|clear|click|close|comment|commit|copy|create|delete|deploy|drag|edit|email|evaluate|exec|execute|fill|insert|install|kill|launch|merge|message|mkdir|move|navigate|notify|patch|pause|post|press|print|publish|push|put|release|remove|rename|replace|reply|resume|review|run|save|send|set|share|skip|start|stop|submit|swipe|tag|tap|terminate|type|uninstall|update|upload|write)(?:[^A-Za-z0-9].*)?$", "hooks": [ { "type": "command", "command": "\"${TEZGAH_PYTHON:-python3}\" \"${CLAUDE_PLUGIN_ROOT}/hooks/projects-pretooluse.py\"", "timeout": 5 } ] }
18
18
  ],
19
19
  "PostToolUse": [
@@ -224,6 +224,9 @@
224
224
  "plan_build": [
225
225
  "\\bplans?\\b[^.?!\\n]{0,40}\\b(?:is ready|ready|written|drafted)\\b|\\b(?:wrote|opened|drafted|added|created) (?:the |a |this )?plans?\\b|\\b(?:build|implement|execute|start on|start) (?:the |this |that )?plans?\\b"
226
226
  ],
227
+ "owner_choice": [
228
+ "\\bwhich (?:variant|option|approach|design)s?\\b|\\b(?:v\\d+|option [a-h1-9]|variant [a-z0-9]+),? or (?:v\\d+|option [a-h1-9]|variant [a-z0-9]+)\\b|\\b(?:another|other|the other) session\\w*|\\bsession'?s (?:line|plan|branch)\\b|\\bclos\\w*\\b[^?\\n]{0,40}\\bline\\b|--allow-open|--limit\\b|\\.staging\\b|\\bhidden (?:dir|directory|folder)\\b|\\b(?:another|a different) (?:folder|dir|directory)\\b|\\b(?:work ?)?around\\b[^?\\n]{0,40}\\brefusal\\b"
229
+ ],
227
230
  "spend": [
228
231
  "spend\\w*|\\bpaid\\b|billing|\\bcost\\w*|\\bbudget\\w*|\\busd\\b|\\$\\s?\\d"
229
232
  ],
@@ -236,6 +239,12 @@
236
239
  "cheap_answer": [
237
240
  "yes|yep|yeah|ok(?:ay)?|sure|go(?: on| ahead)?|continue|proceed|do it|carry on|keep going|approved"
238
241
  ],
242
+ "hand_over": [
243
+ "\\bplease\\s+(?:run|execute)\\b|\\byour\\s+(?:step|turn|move)\\b(?=\\s*[:.;,!]|\\s*$)|\\bover to you\\b"
244
+ ],
245
+ "ran_it": [
246
+ "\\bi (?:ran|tested|verified|checked|executed)\\b|\\bran the (?:tests?|suite|build|lint|checks?)\\b|\\bwe ran\\b"
247
+ ],
239
248
  "outward": {
240
249
  "push": "\\bpush\\w*",
241
250
  "release": "\\brelease\\w*|\\btag(?:s|ged|ging)?\\b|version bump|bump the version",
@@ -329,6 +338,8 @@
329
338
  "slice_lead": "",
330
339
  "ready_lead": "",
331
340
  "ask_lead": "",
341
+ "handed_lead": "",
342
+ "plan_lead": "",
332
343
  "approval_heading": "Approval questions",
333
344
  "recommended": "Recommended",
334
345
  "grant_examples": "\"push without asking\", \"I approve\""
@@ -268,6 +268,9 @@
268
268
  "plan_build": [
269
269
  "\\bplan\\w*[^.?!\\n]{0,40}(?:hazır|yazıldı|yazdım|oluşturdum|oluşturuldu|açtım|açıldı|eklendi|uygula\\w*|başla\\w*|kur\\w*)|(?:yazdığım|açtığım|hazırladığım|oluşturduğum) plan\\w*"
270
270
  ],
271
+ "owner_choice": [
272
+ "\\b(?:v\\d+|seçenek \\w+|varyant \\w+)(?:'\\w+)? m[ıiuü]\\b,? (?:v\\d+|(?:seçenek |varyant )?\\w+)(?:'\\w+)? m[ıiuü]\\b|\\bhangi (?:varyant|seçenek|opsiyon|yaklaşım|tasarım)\\w*|\\bhangisini seç\\w*|\\b(?:başka|diğer) (?:bir )?oturum\\w*|\\bhatt[ıi]\\w* kapat\\w*|--allow-open|--limit\\b|\\.staging\\b|\\bgizli (?:dizin|klasör)\\w*|\\b(?:başka|farklı) (?:bir )?(?:klasör|dizin)\\w*|\\bred\\w* (?:aş|etrafından)\\w*|\\bengel\\w* (?:aş|etrafından)\\w*"
273
+ ],
271
274
  "spend": [
272
275
  "ücret\\w*|ödeme\\w*|harca\\w*|bütçe\\w*|maliyet\\w*|\\bpara\\b|dolar\\w*|\\$\\s?\\d"
273
276
  ],
@@ -280,6 +283,12 @@
280
283
  "cheap_answer": [
281
284
  "evet|tamam|olur|devam(?: et)?|yap|başla|ilerle|okey|onay(?:lıyorum)?"
282
285
  ],
286
+ "hand_over": [
287
+ "\\b(?:sen|siz)\\b[^.!?\\n]{0,32}?\\bçalıştır(?:ın|ınız)?\\b|\\bsenin adımın\\b|\\badım(?:ı)? sende\\b|\\bsıradaki adım(?:ı)? sen\\b"
288
+ ],
289
+ "ran_it": [
290
+ "\\b(?:test(?:ler)?i|suite'?i|komutu|kontrolü|denetimi)?\\s*(?:çalıştırdım|koştum|çalıştırdık)\\b|\\btest ettim\\b|\\bdoğruladım\\b|\\bkontrol ettim\\b|\\bdenetledim\\b"
291
+ ],
283
292
  "outward": {
284
293
  "push": "\\bpush\\w*",
285
294
  "release": "versiyon\\w*|sürüm\\w*|\\brelease\\w*|etiket\\w*",
@@ -350,6 +359,8 @@
350
359
  "slice_lead": "Durmuş bir dilim var. ",
351
360
  "ready_lead": "Hazır adımlar bekliyor. ",
352
361
  "ask_lead": "Bu soruyu sorma: kararı ver ya da önerili onay sorusuna çevir. ",
362
+ "handed_lead": "Bu adımı sen çalıştır, kullanıcıya bırakma. ",
363
+ "plan_lead": "Plan modunda hiçbir şey koşmadı. ",
353
364
  "approval_heading": "Onay için sorular",
354
365
  "recommended": "Öneri",
355
366
  "grant_examples": "\"bana sormadan pushla\", \"onay veriyorum\""
@@ -64,7 +64,8 @@ def main():
64
64
  # a core that cannot answer has refused nothing, so the turn ends: the rule
65
65
  # never traps a session, and that holds for its own failure too
66
66
  reason = safe(session_id, stop_reason, p.get("last_assistant_message"),
67
- session_id, cwd=cwd, after_block=bool(p.get("stop_hook_active")))
67
+ session_id, cwd=cwd, after_block=bool(p.get("stop_hook_active")),
68
+ mode=p.get("permission_mode"))
68
69
  if reason:
69
70
  json.dump({"decision": "block", "reason": reason}, sys.stdout)
70
71
 
@@ -245,6 +245,15 @@ def _reviewer_body(host):
245
245
  "for those files: account for every listed file as reviewed or skipped\n"
246
246
  "with a reason, and still apply everything above - OCR output is input,\n"
247
247
  "never a verdict.\n\n"
248
+ "Plan readiness. When the target is a plan file\n"
249
+ "(`.tezgah/plans/open/NNN-*.md`) rather than a diff, review whether it\n"
250
+ "can be built without a question. First restate each section's intent\n"
251
+ "in one sentence of your own; a section you cannot restate lacks\n"
252
+ "context, which is a major finding. Then check that every choice the\n"
253
+ "build needs is made in the plan - schema, API path, dependency,\n"
254
+ "edge-case handling, the files to change; a choice left to the\n"
255
+ "implementer is a major finding. A file the plan will create is not a\n"
256
+ "missing reference. Classify and report exactly as for a diff.\n\n"
248
257
  "%s" % (_graph_howto(host), _blast_radius(host), EMPTY_IS_NOT_PROOF,
249
258
  decision, decision, _may_use(host)))
250
259
 
@@ -265,14 +274,26 @@ def _worker_body(tier):
265
274
  "report what changed and what each check printed, verbatim. Report\n"
266
275
  "progress, but never redefine the work. Never widen or narrow the brief's\n"
267
276
  "scope, budget or question. If the brief cannot be met as written, say\n"
268
- "so and stop rather than answering a different question.\n%s\n"
277
+ "so and stop rather than answering a different question.\n%s\n%s"
269
278
  "A brief that builds or changes UI: read the `design-contract` skill\n"
270
279
  "(and `design-library` for critique) and derive/check with\n"
271
280
  "`~/.config/tezgah/bin/tezgah-design` before editing.\n"
272
- "Never spawn subagents.\n%s" % (tier, tier, scope, WORKER_OUTPUT))
281
+ "Never spawn subagents.\n%s" % (tier, tier, scope, OWNER_ESCALATE,
282
+ WORKER_OUTPUT))
273
283
  return body
274
284
 
275
285
 
286
+ # A question only the owner can answer climbs past every tier to the user: a
287
+ # worker that settles it itself, frontier included, decided for the owner
288
+ # (plan 136: a line ran under a hidden dir and wrote `chosen:` with no owner
289
+ # choice). One text on every tier.
290
+ OWNER_ESCALATE = ("A question only the owner can answer - choosing a variant or option a\n"
291
+ "research, analysis or review recommends; a way around a tezgah\n"
292
+ "refusal; deciding about another session's work (its line, plan or\n"
293
+ "branch) - is never yours to decide, on any tier: stop and answer\n"
294
+ "`ESCALATE: owner` with the options and a recommended default.\n")
295
+
296
+
276
297
  # The report a consumer reads: the orchestrator re-briefs a report missing one
277
298
  # of these fields (_orch_body), so only a role with a reader gets them. One
278
299
  # text on every host, since a worker body ignores its host.
@@ -282,7 +303,8 @@ WORKER_OUTPUT = ("End the report, after any other shape asked for, with `Output
282
303
  "- commit: the sha\n"
283
304
  "- checks: each command, its exit code and the last lines of its\n"
284
305
  " output file, verbatim\n"
285
- "- escalate: the `ESCALATE:` reason (always `none` on tezgah-frontier)")
306
+ "- escalate: the `ESCALATE:` reason (on tezgah-frontier only\n"
307
+ " `ESCALATE: owner`, else `none`)")
286
308
 
287
309
 
288
310
  # The specialists beside the tier workers: name -> (what it does, the skills it
@@ -483,6 +505,8 @@ def _orch_body(names):
483
505
  "or an ESCALATE, gets one narrower re-brief of the same role naming the\n"
484
506
  "missing field or the escalated judgement; only a second miss is\n"
485
507
  "restarted on tezgah-frontier with the original brief, never continued.\n"
508
+ "An `ESCALATE: owner` is neither: its options and default go to the user\n"
509
+ "in the approval batch, and the orchestrator never answers it itself.\n"
486
510
  "Never delegate a task a specialist is not\n"
487
511
  "listed for, and never let a subagent spawn its own subagents. Verify each\n"
488
512
  "returned claim against the code before acting." % (listed, say()["blocker_tokens"]))
@@ -11,8 +11,9 @@ One store, not two: the install record is the per-artifact sha file the contract
11
11
  artifacts already use (`contract.sha256`, `bin/tezgah-setup::record_contract`),
12
12
  one line per entry under a `hook:<host>:<entry>` key. An entry is one hook event's
13
13
  tezgah-owned groups (Claude, dsh, Codex, Cursor - so a removed PreToolUse is named
14
- while PostToolUse stays), opencode's plugin links, and nothing for omp: its bridge
15
- is compared to a fresh render, the way `rules_current` compares a rules file. The
14
+ while PostToolUse stays), opencode's plugin links, and nothing for omp and Amp:
15
+ their bridge or plugin is compared to a fresh render, the way `rules_current`
16
+ compares a rules file. The
16
17
  code an entry runs is hashed only on a packaged install (a tree with no `.git`):
17
18
  a checkout install points into a tree its owner edits on purpose. A hook file
18
19
  any user can write (0666) is drift too; group write is left alone, because a
@@ -43,10 +44,22 @@ CODE_TARGETS = {
43
44
  "cursor": (os.path.join("hosts", "cursor", "hook.py"),),
44
45
  "opencode": (os.path.join("hosts", "opencode", "plugins", "tezgah.js"),),
45
46
  "omp": (os.path.join("hosts", "omp", "hook.py"),),
47
+ "qwen": (os.path.join("hosts", "qwen", "hook.py"),),
48
+ "droid": (os.path.join("hosts", "droid", "hook.py"),),
49
+ "commandcode": (os.path.join("hosts", "commandcode", "hook.py"),),
50
+ "copilot": (os.path.join("hosts", "copilot", "hook.py"),),
51
+ "gemini": (os.path.join("hosts", "gemini", "hook.py"),),
52
+ "antigravity": (os.path.join("hosts", "antigravity", "hook.py"),),
53
+ "goose": (os.path.join("hosts", "goose", "hook.py"),),
54
+ "amp": (os.path.join("hosts", "amp", "hook.py"),
55
+ os.path.join("hosts", "amp", "tezgah-plugin.ts.in")),
46
56
  }
47
57
  OPENCODE_DIRS = ("plugins", "plugin")
48
58
  OMP_BRIDGE = os.path.join(tp.HOST_DIRS["omp"], "agent", "hooks", "pre",
49
59
  "tezgah-hook.ts")
60
+ # The first line of every Amp plugin tezgah renders: an uninstall removes a
61
+ # file at AMP_PLUGIN only when it equals a fresh render or starts with this.
62
+ AMP_HEADER = "// tezgah: Amp plugin rendered by tezgah-setup"
50
63
 
51
64
 
52
65
  def sha(text):
@@ -82,10 +95,15 @@ def own_groups(data, own=False):
82
95
  return out
83
96
 
84
97
 
85
- def _manifest(path, own):
98
+ def _manifest(path, own, bare=False, key=None):
99
+ """`bare`: the file is keyed by event at the top, as Droid's hooks.json is;
100
+ `key`: the events sit under that top-level name, as Antigravity's do."""
86
101
  try:
87
102
  with open(path, encoding="utf-8") as fh:
88
- return own_groups(json.load(fh), own)
103
+ data = json.load(fh)
104
+ if key:
105
+ data, bare = (data.get(key) if isinstance(data, dict) else None) or {}, True
106
+ return own_groups({"hooks": data} if bare else data, own)
89
107
  except (OSError, ValueError):
90
108
  return None
91
109
 
@@ -97,11 +115,31 @@ def omp_bridge(root=ROOT):
97
115
  with open(os.path.join(root, "hosts", "omp", "tezgah-hook.ts.in"),
98
116
  encoding="utf-8") as fh:
99
117
  return fh.read().replace(
100
- "@HOOK@", os.path.join(root, "hosts", "omp", "hook.py"))
118
+ "@HOOK@", js_string(os.path.join(root, "hosts", "omp", "hook.py")))
101
119
  except OSError:
102
120
  return ""
103
121
 
104
122
 
123
+ def amp_plugin(root=ROOT):
124
+ """The Amp plugin as an install writes it: the template with this tree's
125
+ hook path, or "" when the template is unreadable. The install, the report
126
+ row, the live probe and the attestation all compare against this."""
127
+ try:
128
+ with open(os.path.join(root, "hosts", "amp", "tezgah-plugin.ts.in"),
129
+ encoding="utf-8") as fh:
130
+ return fh.read().replace(
131
+ "@HOOK@", js_string(os.path.join(root, "hosts", "amp", "hook.py")))
132
+ except OSError:
133
+ return ""
134
+
135
+
136
+ def js_string(path):
137
+ """`path` as the inside of a JS double-quoted string: a Windows checkout's
138
+ `\\t`, `\\u` or `\\x` would otherwise become a control character or a
139
+ SyntaxError, and the bridge would not load (review of plan 140)."""
140
+ return json.dumps(path)[1:-1]
141
+
142
+
105
143
  def registration(host, root=ROOT):
106
144
  """({entry: text}, [files]) for the tezgah-owned hook entries this host has
107
145
  on disk; the dict is None when the host's file cannot be read."""
@@ -109,9 +147,30 @@ def registration(host, root=ROOT):
109
147
  path = os.path.join(root, *(("hooks",) if host == "claude"
110
148
  else ("hosts", "dsh")), "hooks.json")
111
149
  return _manifest(path, own=True), [path]
112
- if host in ("codex", "cursor"):
113
- path = os.path.join(tp.HOST_DIRS[host], "hooks.json")
114
- return _manifest(path, own=False), [path]
150
+ if host in ("codex", "cursor", "qwen", "droid", "commandcode"):
151
+ # Qwen's and Command Code's hooks live in a shared settings.json
152
+ path = os.path.join(tp.HOST_DIRS[host], "settings.json"
153
+ if host in ("qwen", "commandcode") else "hooks.json")
154
+ return _manifest(path, own=False, bare=host == "droid"), [path]
155
+ if host == "copilot":
156
+ # tezgah's own file in Copilot's user hooks dir: every entry is ours
157
+ path = os.path.join(tp.HOST_DIRS["copilot"], "hooks", "tezgah.json")
158
+ return _manifest(path, own=True), [path]
159
+ if host == "gemini":
160
+ # tezgah's own extension: every entry in its hooks.json is ours
161
+ path = os.path.join(tp.HOST_DIRS["gemini"], "extensions", "tezgah", "hooks",
162
+ "hooks.json")
163
+ return _manifest(path, own=True), [path]
164
+ if host == "antigravity":
165
+ # the shared ~/.gemini/config/hooks.json: every entry under its
166
+ # top-level `tezgah` name is ours
167
+ path = os.path.join(os.path.dirname(tp.HOST_DIRS["antigravity"]), "config",
168
+ "hooks.json")
169
+ return _manifest(path, own=True, key="tezgah"), [path]
170
+ if host == "goose":
171
+ # tezgah's own plugin: every entry in its hooks.json is ours
172
+ path = os.path.join(tp.GOOSE_PLUGIN, "hooks", "hooks.json")
173
+ return _manifest(path, own=True), [path]
115
174
  if host == "opencode":
116
175
  links = (os.path.join(tp.HOST_DIRS["opencode"], d, "tezgah.js")
117
176
  for d in OPENCODE_DIRS)
@@ -124,6 +183,12 @@ def registration(host, root=ROOT):
124
183
  return {"bridge": fh.read()}, [OMP_BRIDGE]
125
184
  except OSError:
126
185
  return None, [OMP_BRIDGE]
186
+ if host == "amp":
187
+ try:
188
+ with open(tp.AMP_PLUGIN, encoding="utf-8") as fh:
189
+ return {"plugin": fh.read()}, [tp.AMP_PLUGIN]
190
+ except OSError:
191
+ return None, [tp.AMP_PLUGIN]
127
192
  return None, []
128
193
 
129
194
 
@@ -148,7 +213,7 @@ def record_texts(host, root=ROOT):
148
213
  """{key: text} an install records for one host (`bin/tezgah-setup::record_hooks`)."""
149
214
  entries, _files = registration(host, root)
150
215
  out = {}
151
- if host != "omp":
216
+ if host not in ("omp", "amp"):
152
217
  out.update((KEY % (host, name), text)
153
218
  for name, text in (entries or {}).items())
154
219
  code = code_text(host, root) if packaged(root) else None
@@ -173,10 +238,14 @@ def drift(host, root=ROOT, shas=None):
173
238
  if mode & 0o002:
174
239
  found.append("%s mode %04o" % (path, mode))
175
240
  if entries is None:
176
- found.append("%s unreadable or missing" % files[0])
241
+ # a host `registration` does not know names no file: say so, never IndexError
242
+ found.append("%s unreadable or missing" % (files[0] if files else host))
177
243
  elif host == "omp":
178
244
  if entries["bridge"] != omp_bridge(root):
179
245
  found.append("omp bridge %s differs from a fresh render" % OMP_BRIDGE)
246
+ elif host == "amp":
247
+ if entries["plugin"] != amp_plugin(root):
248
+ found.append("amp plugin %s differs from a fresh render" % tp.AMP_PLUGIN)
180
249
  else:
181
250
  names = set(recorded) - {"code"}
182
251
  if not names:
@@ -1354,11 +1354,17 @@ def session_of(payload):
1354
1354
  # again on 2026-10-22 (plan 126, second pass): a research ask arms the Research
1355
1355
  # and Product paragraphs (6239 B on Claude, S4Remeasure p08) and the skill hint
1356
1356
  # that names the research skill (about 330 B with its path) was the block given
1357
- # up, so the prompt budget moves to 7000 B. A
1357
+ # up, so the prompt budget moves to 7000 B. Moved again on 2026-10-28 (plan
1358
+ # 136): the subagent pointer says an owner-only question goes back as
1359
+ # `ESCALATE: owner` (+330 B), the fixture's subagent_start reached 5958 B and
1360
+ # gave up `consult` (205 B), so subagent_start moves to 6100 B. Moved again on
1361
+ # 2026-10-29 (plan 142): the output-shape paragraph says what the host's plan
1362
+ # mode changes (+168 B), and the worktree fixture's session_start gave up its
1363
+ # worktrees line again, so the session budgets move to 16200 B. A
1358
1364
  # budget in bytes, not tokens: this file has no tokenizer and a wrong estimate
1359
1365
  # would be worse than a bound.
1360
- CONTEXT_BUDGET = {"session_start": 16000, "post_compact": 16000,
1361
- "subagent_start": 5800, "user_prompt": 7000}
1366
+ CONTEXT_BUDGET = {"session_start": 16200, "post_compact": 16200,
1367
+ "subagent_start": 6100, "user_prompt": 7000}
1362
1368
  DEFAULT_BUDGET = 13000
1363
1369
  # The blocks in the order they are given up when the budget is exceeded, lowest
1364
1370
  # value first: text another surface already carries (the plan table lives in the
@@ -1520,12 +1526,13 @@ GATE_MIN_CALLS = 3
1520
1526
  # The tool names whose every call leaves a PreToolUse row (`began`,
1521
1527
  # tezgah_gate.decision's write and shell branch) and every refused one a `deny`:
1522
1528
  # Claude's write and shell tools, omp's `bash`/`edit`/`write` and Codex's
1523
- # `exec_command`/`apply_patch`. A
1529
+ # `exec_command`/`apply_patch`, Droid's `Execute`/`Create`/`ApplyPatch`. A
1524
1530
  # tool the gate may pass without a row (Read, Grep, Task, WebFetch, an MCP
1525
1531
  # call) is not counted, so its absence from the ledger is never read as a
1526
1532
  # disarmed gate.
1527
1533
  GATED_TOOLS = re.compile(r"(?i)^(?:bash|powershell|pwsh|edit|write|multiedit|"
1528
- r"notebookedit|exec_command|shell|apply_patch)$")
1534
+ r"notebookedit|exec_command|shell|apply_patch|"
1535
+ r"execute|create|applypatch)$")
1529
1536
  GATE_INACTIVE = (
1530
1537
  "tezgah gate inactive on this host: %d gated tool call(s) since this "
1531
1538
  "session's first turn and not one row from the gate, so the shortcut "
@@ -1651,7 +1658,8 @@ def context_nudge(payload, session_id):
1651
1658
  # The ledger kinds a hook other than the tool hooks writes: the prompt hook's
1652
1659
  # turn marker, judge row, lesson rows and disarm row, the Stop hook's claim,
1653
1660
  # refusal, after_block and shape rows and the stop_spec rows of plan 063's shadow
1654
- # (old ledgers only), the subagent-end hook's subagent_end row,
1661
+ # (old ledgers only), the subagent-end hook's subagent_end row, Droid's Stop
1662
+ # `reply_unread` row (a turn end whose reply the transcript did not yield),
1655
1663
  # compaction, the subagent mark and the `subagent_start` lineage row (both from
1656
1664
  # SubagentStart), the omp fork's `forked` row, the guard's crash
1657
1665
  # row (any hook), the damage row any reader writes and the session start's
@@ -1664,7 +1672,7 @@ NOT_TOOL_HOOK = frozenset((b"turn", b"judge", b"claim", b"refusal",
1664
1672
  b"after_block", b"subagent_end", b"stop_spec",
1665
1673
  b"shape", b"compact", b"orch", b"crash", b"route",
1666
1674
  b"spawned", b"forked", b"lesson", b"disarm", b"ledger_damage",
1667
- b"attest", b"subagent_start"))
1675
+ b"attest", b"subagent_start", b"reply_unread"))
1668
1676
  # The rows PostToolUse writes (`note_tool`): they prove the host ran the call,
1669
1677
  # not that the gate saw it - a PostToolUse hook keeps writing them while a
1670
1678
  # broken PreToolUse hook lets every call through - so they are no proof either.
@@ -2450,7 +2458,12 @@ def context_for(event, cwd, payload=None, with_core=True):
2450
2458
  "(each tagged DERIVED, you concluded it, or RETRIEVED, "
2451
2459
  "you read it), Evidence (the path:line or command behind "
2452
2460
  "each), Confidence, Unresolved, Disconfirming, Conflicts. "
2453
- "Full rules: the `tezgah-contract` skill."))
2461
+ "A question only the owner can answer - choosing a variant "
2462
+ "or option a research, analysis or review recommends; a way "
2463
+ "around a tezgah refusal; deciding about another session's "
2464
+ "work (its line, plan or branch) - is never yours: return "
2465
+ "`ESCALATE: owner` with the options and a recommended "
2466
+ "default. Full rules: the `tezgah-contract` skill."))
2454
2467
  else:
2455
2468
  parts.append(("pointer", POINTER_LINE))
2456
2469
  text = budgeted(event, [(key, render(text.strip())) for key, text in parts])