@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/skills.md CHANGED
@@ -217,8 +217,8 @@ inside the repository (`hooks/tezgah_skill_pick.py::section_hint`,
217
217
  Measured on this machine on 2026-10-07: 7 502 sections and a 9.5 MB index. The
218
218
  one cold rebuild took 1.36 s, and a warm prompt takes 6-15 ms. On a turn it
219
219
  fires the line costs 196-239 bytes on omp and 245-320 elsewhere. It is not a
220
- judgement, so `judge-off` leaves it on. `reminder-off` drops it with the rest
221
- of the per-turn text (`tests/test_skill_search.py::LocalHint`,
220
+ judgement, so `judge-off` leaves it on, and so does `reminder-off`, which drops
221
+ only the reminder line (`tests/test_skill_search.py::LocalHint`,
222
222
  `OmpPromptPath`).
223
223
 
224
224
 
@@ -226,10 +226,31 @@ of the per-turn text (`tests/test_skill_search.py::LocalHint`,
226
226
 
227
227
  `SKILLS` in `bin/tezgah-setup::SKILLS` is the shipped list, and it is the single
228
228
  definition of "a tezgah skill": the router, the per-host linking, the uninstall
229
- and the context budget all read it. Each install function links those
230
- directories into the host's own skill directory - Codex
231
- (`bin/tezgah-setup::install_codex`), opencode (`bin/tezgah-setup::install_opencode`), Cursor (`bin/tezgah-setup::install_cursor`), dsh
232
- (`bin/tezgah-setup::install_dsh`), omp (`bin/tezgah-setup::install_omp`). Claude is the exception: it installs from a
229
+ and the context budget all read it. Every host but Claude has an install function
230
+ that links those directories into the host's own skill directory:
231
+
232
+ | Host | Install function | Skill directory |
233
+ |---|---|---|
234
+ | Codex | `bin/tezgah-setup::install_codex` | `$CODEX_HOME/skills` |
235
+ | opencode | `bin/tezgah-setup::install_opencode` | `~/.config/opencode/skills` |
236
+ | Cursor | `bin/tezgah-setup::install_cursor` | `~/.cursor/skills`, only for a skill no other dir Cursor reads already links |
237
+ | dsh | `bin/tezgah-setup::install_dsh` | `$DSH_HOME/skills` |
238
+ | omp | `bin/tezgah-setup::install_omp` | `~/.omp/agent/skills` |
239
+ | Qwen Code | `bin/tezgah-setup::install_qwen` | `$QWEN_HOME/skills` |
240
+ | Droid | `bin/tezgah-setup::install_droid` | `~/.factory/skills` |
241
+ | Command Code | `bin/tezgah-setup::install_commandcode` | `~/.commandcode/skills` |
242
+ | Copilot CLI | `bin/tezgah-setup::install_copilot` | `$COPILOT_HOME/skills` |
243
+ | Gemini CLI | `bin/tezgah-setup::install_gemini` | `~/.gemini/extensions/tezgah/skills` |
244
+ | Antigravity | `bin/tezgah-setup::install_antigravity` | `~/.gemini/antigravity-cli/skills` |
245
+ | Goose | `bin/tezgah-setup::install_goose` | `~/.agents/plugins/tezgah/skills` |
246
+ | Amp | `bin/tezgah-setup::install_amp` | `~/.config/amp/skills` |
247
+
248
+ Cursor reads `~/.codex/skills`, `~/.claude/skills` and `~/.agents/skills` too,
249
+ and names no winner for a skill found twice. So a skill one of those already
250
+ links to this tree is not linked into `~/.cursor/skills` again
251
+ (`bin/tezgah-setup::cursor_shared_copy`, `bin/tezgah-setup::CURSOR_SHARED_SKILLS`). The install prints
252
+ `<n> skills reach Cursor through <dir>, not linked twice`. Cursor installs after
253
+ Codex, so it sees Codex's links. Claude is the exception: it installs from a
233
254
  plugin, so the skills arrive in the COPY at
234
255
  `~/.claude/plugins/cache/<marketplace>/tezgah/<version>/` that `--sync` refreshes
235
256
  and `--install` re-refreshes when it is stale (`bin/tezgah-setup:26-29`,
@@ -238,8 +259,10 @@ and `--install` re-refreshes when it is stale (`bin/tezgah-setup:26-29`,
238
259
 
239
260
  The install report checks the file, not the link. `skills_linked()` requires
240
261
  every name in `SKILLS` to resolve to a readable `SKILL.md` under the host
241
- directory (`skills_linked()`, `bin/tezgah-setup::skills_linked`), and it is used for every host row
242
- (`bin/tezgah-setup::host_checks_codex`, `bin/tezgah-setup::host_checks_opencode`, `bin/tezgah-setup::host_checks_cursor`, `bin/tezgah-setup::host_checks_dsh`, `bin/tezgah-setup::host_checks_omp`). It was
262
+ directory (`skills_linked()`, `bin/tezgah-setup::skills_linked`), and the `skills linked` row of
263
+ every host but Claude and Cursor calls it (`host_checks_<host>`, for example
264
+ `bin/tezgah-setup::host_checks_codex`). Cursor's row accepts either a readable
265
+ `SKILL.md` under `~/.cursor/skills` or a shared copy (`bin/tezgah-setup::host_checks_cursor`). It was
243
266
  `islink()` once, and a link to nothing is a link: a name whose `SKILL.md` was
244
267
  never written reported as linked on every host at once while no host could read
245
268
  it (`tests/test_skills.py::SkillStandards`).
@@ -15,8 +15,8 @@ host-neutrally by `health_segments()` (`hooks/tezgah_context.py::health_segments
15
15
  groups (`hooks/tezgah_context.py::render_line`). The group is *data on each segment*: a renderer building its
16
16
  own line from `--json` separates on `seg.group` and matches `render_line()`
17
17
  without a second copy of the partition (`hooks/tezgah_context.py::health_segments`) — opencode and dsh do exactly
18
- that (hosts/opencode/tui/tezgah-tui.tsx:108-111,
19
- hosts/dsh/statusline/lib/client.js:86-90).
18
+ that (`TezgahBar` in `hosts/opencode/tui/tezgah-tui.tsx`,
19
+ `TezgahStatusLine` in `hosts/dsh/statusline/lib/client.js`).
20
20
 
21
21
  | group | members | why together |
22
22
  |---|---|---|
@@ -173,7 +173,12 @@ group separators stay identical for every extension. An extension never has to
173
173
  know the gutter. It must name `text` and `state`. tezgah fills a missing `glyph`
174
174
  from the state's own mark, so a chip reads like a built-in. The per-extension cap
175
175
  (`MAX_SEGMENTS`) and the hard timeout (`timeout_ms`, default
176
- `DEFAULT_TIMEOUT_MS`) bound one row.
176
+ `DEFAULT_TIMEOUT_MS`) bound one row. The command prints one JSON object whose
177
+ `segments` key holds that list. tezgah reads a bare list on stdout as malformed output:
178
+
179
+ ```json
180
+ {"segments": [{"key": "cyber-eng", "state": "on", "text": "eng"}]}
181
+ ```
177
182
 
178
183
  It is a subprocess and not an import on purpose. The extension is the user's
179
184
  repo, in whatever language, and may be slow or broken. A bounded child keeps a
@@ -229,10 +234,13 @@ python is asked (`hooks/tezgah_context.py::skill_read_kind`). A surface that can
229
234
  was never opened", so it declares what it can see and those marks render `info`
230
235
  (dim, no glyph) instead of `ready` (`observable`, `hooks/tezgah_context.py::health_segments`; the set constant
231
236
  `TOOL_USE_MEASURES`, `hooks/tezgah_context.py::TOOL_USE_MEASURES`). The `--observable=` flag carries it on the CLI
232
- (`bin/tezgah-status::main`). Two callers pass the tool-use set: Cursor's
233
- status line (statusline.py:117-121) and Codex, which renders the line plain into
234
- `systemMessage` (`hosts/codex/hook.py::main`); dsh passes the literal flag
235
- string (hosts/dsh/statusline/lib/index.js:16-19,84,96), which names the five marks its
237
+ (`bin/tezgah-status::main`). Every host whose line reads the store and cannot see
238
+ a skill read passes the tool-use set: the status line for Cursor, Qwen, Droid, Copilot and Antigravity
239
+ (`statusline.py::HOST`, `statusline.py::observable`), and Codex, Command Code
240
+ and Gemini, which render the line plain into `systemMessage`
241
+ (`hosts/codex/hook.py::main`, `hosts/commandcode/hook.py::status`,
242
+ `hosts/gemini/hook.py::status`); dsh passes the literal flag
243
+ string (`OBSERVABLE` in `hosts/dsh/statusline/lib/index.js`), which names the five marks its
236
244
  own hook can record - `judge` included, since dsh wires the same PostToolUse hook
237
245
  every other host does. The set is what a host can write, not what it happens to
238
246
  have written: a mark the literal omits reads `info` there rather than a claim.
@@ -248,15 +256,15 @@ the five kinds are ever written (`hooks/tezgah_context.py::record`).
248
256
 
249
257
  Writers: the shared PostToolUse hook `hooks/projects-posttooluse.py` — `graph` from
250
258
  an MCP tool name, else the shell tokenizer's answer, which counts a tool only when
251
- the command really ran it (34-38,84) and is the one writer that can earn `judge`
259
+ the command really ran it (`hooks/projects-posttooluse.py::used_kind`) and is the one writer that can earn `judge`
252
260
  (a run of `bin/tezgah-triage` or `bin/tezgah-docs`, `shell_kind`, `hooks/tezgah_context.py::shell_kind`)
253
261
  — plus `hooks/projects-auto-init.py::main`
254
262
  (`orch` on subagent start), `hosts/codex/hook.py::main`,
255
- `hosts/cursor/hook.py::dispatch`, hosts/opencode/plugins/tezgah.js:2167-2174,
256
- hosts/opencode/plugins/tezgah.js:2725, `hosts/omp/hook.py::handle`. Both Claude (hooks/hooks.json) and dsh
263
+ `hosts/cursor/hook.py::dispatch`, `record` in `hosts/opencode/plugins/tezgah.js`,
264
+ `hosts/omp/hook.py::handle`. Both Claude (hooks/hooks.json) and dsh
257
265
  (hosts/dsh/hooks.json) wire PostToolUse to that one shared hook, which is how dsh
258
266
  gets any used mark at all: with no local transcript, the store is the only channel
259
- its line has (hooks/projects-posttooluse.py:45-49).
267
+ its line has (`hooks/projects-posttooluse.py::used_kind`).
260
268
 
261
269
  Claude's line never reads the store: it parses `tool_use` blocks out of
262
270
  `payload["transcript_path"]` plus the subagent transcripts under it, which is how
@@ -269,15 +277,15 @@ Every other surface reads the store.
269
277
  picks the host from `--cursor` or `TEZGAH_STATUS_HOST`, renders
270
278
  `render_line(segs, color=color_default())`, and joins Orca's own status line to
271
279
  the tezgah segment with ` | `; `TEZGAH_STATUS_LEGEND=1` appends the legend
272
- (31-34,40-48,116-122). ANSI is dropped under `NO_COLOR` or
280
+ (`statusline.py::HOST`, `statusline.py::seg`, `statusline.py::out`). ANSI is dropped under `NO_COLOR` or
273
281
  `TEZGAH_STATUS_COLOR=0` (`color_default()`, `hooks/tezgah_context.py::color_default`).
274
282
  - **omp** — hosts/omp/tezgah-hook.ts.in: `draw()` prefers
275
283
  `ctx.ui.setWidget("tezgah", [line], {placement: "belowEditor"})`, which renders
276
284
  the colored string as-is; `setStatus` is the fallback for a build without
277
- `setWidget`, and it is plain because omp strips ANSI (12-29). `status_line()`
285
+ `setWidget`, and it is plain because omp strips ANSI (`draw` in `hosts/omp/tezgah-hook.ts.in`). `status_line()`
278
286
  builds the line and carries `idx_override`, so a per-tool redraw forks no git
279
287
  (`hosts/omp/hook.py::status_line`); it redraws on session start, session switch, turn end
280
- and each watched tool result (hosts/omp/tezgah-hook.ts.in:596-636,674-679,720-745).
288
+ and each watched tool result (`refresh` and `WATCHED` in `hosts/omp/tezgah-hook.ts.in`).
281
289
  **Motion:** while the agent runs (`agent_start` until an `agent_end` whose
282
290
  `isTerminal` is not `false`) the widget leads with a braille spinner, the tool
283
291
  in flight (`tool_execution_start`/`_end`, "thinking" between tools) and the
@@ -297,20 +305,20 @@ Every other surface reads the store.
297
305
  (`tui.json`'s `plugin` array, not the server plugin) registering an `app_bottom`
298
306
  slot, shelling out to `tezgah-status <dir> --json <sessionID>`, coloring each
299
307
  segment from the active theme and rebuilding the separator from `seg.group`
300
- (56-88,hosts/opencode/tui/tezgah-tui.tsx:108-111). Its `tezgah: status legend` command opens a dialog running
301
- `--legend` (141-147).
308
+ (`TezgahBar` in `hosts/opencode/tui/tezgah-tui.tsx`). Its `tezgah: status legend` command opens a dialog running
309
+ `--legend` (`LegendDialog` in `hosts/opencode/tui/tezgah-tui.tsx`).
302
310
  On opencode 2 the same line comes from hosts/opencode/tui/cli/tui.tsx, a
303
311
  `Plugin.define` CLI plugin listed in `cli.json`. It reuses the v1 file's `run`.
304
312
  - **dsh** — hosts/dsh/statusline/lib/index.js serves `GET /api/tezgah.status`
305
313
  behind the Web UI's own authenticated fetch fence, so the browser fetches it
306
- same-origin with no extra token handling (14,34-38); it always passes
307
- `--observable=consult,research,graph,orch,judge` (18,47-48) and returns `{segments,
308
- legend}` for `?format=json`, plain text otherwise (49-56,63). Its client half
314
+ same-origin with no extra token handling (`apply` in `hosts/dsh/statusline/lib/index.js`); it always passes
315
+ `--observable=consult,research,graph,orch,judge` (`OBSERVABLE` in `hosts/dsh/statusline/lib/index.js`) and returns `{segments,
316
+ legend}` for `?format=json`, plain text otherwise (`statusText` in `hosts/dsh/statusline/lib/index.js`). Its client half
309
317
  (hosts/dsh/statusline/lib/client.js) renders the marks in the session header in
310
318
  fixed light/dark colors, polls every 10 s while the tab is visible, and shows the
311
- legend on hover, pins it on click (16-19,35,47-52,63-64).
319
+ legend on hover, pins it on click (`TezgahStatusLine` in `hosts/dsh/statusline/lib/client.js`).
312
320
  - **Codex** — hosts/codex/hook.py puts the plain line in `systemMessage`
313
- (182-184,195-198).
321
+ (`hosts/codex/hook.py::main`).
314
322
  - **Amp** — the plugin draws the widest plain tier
315
323
  (`hosts/amp/hook.py::status_line`) on Amp's experimental status item
316
324
  (`createStatusItem`, TUI only, plain text). It draws only for the thread the
@@ -336,7 +344,8 @@ tezgah-status --judge # what the judge used last, and why
336
344
 
337
345
  The session id is the second positional argument or `TEZGAH_SESSION`; without it
338
346
  the used marks cannot light up (`bin/tezgah-status::main`). `--color`/`--no-color` force ANSI or plain,
339
- `--observable=` narrows the measures as above, and an unknown flag exits 2 (`bin/tezgah-status::main`).
347
+ `--observable=` narrows the measures as above, and an unknown flag exits 2. So does
348
+ an `--observable=` measure outside `TOOL_USE_MEASURES` plus `pony` and `adhd` (`bin/tezgah-status::main`).
340
349
 
341
350
  `--counters` is not the marks: it prints `tezgah_integrity.counters(session)` —
342
351
  `steps`, `tool_error_rate`, `claims`, `false_completion`, `shape_blocked`,
@@ -413,7 +422,8 @@ could not be read - so the mark refuses to say either fresh or stale, and
413
422
  `index_notice()` says on the turn that the graph's age is unknown. On a host that
414
423
  sandboxes hook writes (dsh) the stamp is never written, and before that state
415
424
  existed the mark read green there for good. That is why `observable` exists (`hooks/tezgah_context.py::health_segments`),
416
- why the skill-read check is documented as unavailable on Codex, Cursor and dsh
425
+ why the skill-read check is documented as unavailable on every host that passes
426
+ the tool-use set or the dsh literal (see `--observable=` above)
417
427
  (`hooks/tezgah_context.py::skill_read_kind`), why omp ignores any `idx` value that is not one of
418
428
  its five glyphs (`hosts/omp/hook.py::status_line`) - a value outside that set re-probes
419
429
  instead of being reused, one fork and never a wrong mark - and
package/docs/testing.md CHANGED
@@ -47,7 +47,7 @@ baseline on the next run. Only `--clarity --update` raises it.
47
47
  A pin is a claim about what ran, not a guarantee about what will. This repository
48
48
  has two, and both are about the language rather than about dependencies: the
49
49
  floor `py310` (`pyproject.toml:5`) and the CI matrix that proves it
50
- (`.github/workflows/ci.yml:114`). There is no dependency lockfile to pin
50
+ (the `test` job's `python-version` matrix in `.github/workflows/ci.yml`). There is no dependency lockfile to pin
51
51
  because there is no package: tezgah is a set of scripts run in place, and no
52
52
  `[project]` or build system is declared (`pyproject.toml:1-2`). The only
53
53
  third-party tool the checks name is ruff, at a version in
@@ -65,10 +65,15 @@ conclude from either.
65
65
 
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
- not start it. `release.yml` calls it (`ci.yml:13`) as its `ci` job, so npm and
68
+ not start it. `release.yml` calls it (its `workflow_call` trigger) as its `ci` job, so npm and
69
69
  brew publish only after CI passed on the release commit. Its first job,
70
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
71
+ Then no job builds or uploads anything. Next, `green` looks for a finished push
72
+ run of `ci.yml` on the same commit. It waits for a running one and counts only
73
+ `success`, and then `ci` does not run: a release does not run the suite twice. No
74
+ such run, or any other outcome, runs `ci` as before. `npm-publish` and
75
+ `brew-formula` need `green` and `ci`, and publish when either answered green.
76
+ A newer push to the
72
77
  same ref cancels the run it replaces, except on a release (`concurrency`). Every
73
78
  job has a `timeout-minutes`, so a hung test cannot hold a runner for six hours.
74
79
  No matrix cancels its other legs when one fails (`fail-fast: false`).
@@ -79,33 +84,58 @@ No matrix cancels its other legs when one fails (`fail-fast: false`).
79
84
  empty diff, a base it cannot read, a release or a manual run runs everything.
80
85
  - `docs`, prose only: the citations audit, the clarity audit and
81
86
  `tests/impacted.py --ref <base>`, which maps a docs page to the docs modules.
82
- - `test` (`ci.yml:100`) on Python 3.10 and 3.14, the floor and the ceiling.
83
- It byte-compiles, lints with ruff from `requirements-dev.txt` (pip cached),
84
- audits the docs citations and clarity and runs `TEZGAH_E2E_STRICT=1 python3
85
- tests/e2e_plan_flow.py`. A release passes the `release` input and runs one
86
- 3.10 leg.
87
- - `test-sharded` (`ci.yml:142`) on Python 3.10 and 3.14 with node 22 (the
87
+ - `test` runs on Python 3.10 and 3.14, the floor and the ceiling. Its first steps
88
+ are "Byte-compile every script", "Install ruff" (from `requirements-dev.txt`,
89
+ pip cached) and "Lint". Then come "Every docs citation lands in the symbol it
90
+ names" and "The docs prose audit". The last two are "The plan and decision
91
+ rules, in a real project" (`TEZGAH_E2E_STRICT=1 python3 tests/e2e_plan_flow.py`)
92
+ and "The research checks against their known-truth set" (`python3
93
+ bin/tezgah-research release-check`). A release passes the `release` input and
94
+ runs one 3.10 leg.
95
+ - `test-sharded` on Python 3.10 and 3.14 with node 22 (the
88
96
  first line that loads the omp `.ts` hook without a flag). On `main` and a
89
- release it runs `python3 tests/impacted.py --all`. On a pull request it runs
90
- `--ref <base>`: the modules the change maps to, and the whole suite when the
91
- map does not know a path. It replaced the serial `unittest discover` step on
97
+ release its step "Run the suite, sharded" runs `python3 tests/impacted.py --all`.
98
+ On a pull request "Run the impacted modules, sharded (pull request)" runs
99
+ `--ref <base>` instead. That runs the modules the change maps to, or the whole
100
+ suite when the map does not know a path. It replaced the serial `unittest discover` step on
92
101
  2026-10-13: both caught the same failures, and the serial step took about 30
93
102
  minutes.
94
- - `apps-e2e` (`ci.yml:175`) on Python 3.12 and node 20: one step,
103
+ - `apps-e2e` on Python 3.12 and node 20: one step, "App MCP handshake
104
+ (playwright + mobile-mcp)",
95
105
  `TEZGAH_E2E_STRICT=1 python3 tests/e2e_analyze_wiring.py`. It completes the
96
106
  app-MCP handshake only - no browser, no device - so it stays deterministic;
97
107
  strict mode makes a server that cannot start a failure rather than a skip, so
98
108
  a broken wire cannot hide behind a skip (`e2e_analyze_wiring.py:4-12`).
99
- - `artifact-install` (`ci.yml:200`) on Python 3.10 and 3.12: builds the
109
+ - `artifact-install` on Python 3.10 and 3.12: builds the
100
110
  tarball, installs from it under a temp prefix, and runs the artifact smoke on
101
111
  the installed tree instead of the checkout.
102
- - `artifact-install-windows` (`ci.yml:243`) on `windows-latest`: the same
112
+ - `artifact-install-windows` on `windows-latest`: the same
103
113
  build, install and smoke through `packaging/install.ps1` - the twin a Windows
104
114
  user runs - so it is the only place the Windows claim is proven. It then runs
105
115
  the opencode plugin's path tests natively, `tezgah update --dry-run`, the
106
116
  opencode live probe and `--uninstall` from the installed tree. Any residue
107
117
  under the prefix fails it.
108
118
 
119
+ Four node harnesses run JavaScript and TypeScript the suite cannot import. Their
120
+ Python modules drive them, so they run wherever those modules run: in
121
+ `test-sharded`, which installs node 22, and locally under `tests/impacted.py`.
122
+
123
+ - `tests/_opencode_plugin_harness.mjs` loads the opencode plugin
124
+ (`tests/test_opencode_plugin.py`).
125
+ - `tests/_omp_extension_harness.mjs` loads the omp extension
126
+ (`tests/test_omp_extension.py`) and `tests/_amp_plugin_harness.mjs` the Amp
127
+ plugin (`tests/test_amp_plugin.py`). Both need a node that strips types
128
+ (22.18+).
129
+ - `tests/_workflow_harness.mjs` is a stub of the Workflow runtime. It runs
130
+ `workflows/graph-review.js`, `workflows/graph-impact.js` and
131
+ `workflows/graph-map.js` as async function
132
+ bodies. It answers each `agent()` from a fixture, and `null` is a failed agent.
133
+ `python3 -m pytest tests/test_graph_workflows.py` runs it.
134
+
135
+ Without node, or with a node too old for `.ts`, these modules skip rather than
136
+ fail, so a leg with no node passes them silently. `test-sharded` is the leg
137
+ that proves them.
138
+
109
139
  `.github/workflows/neuter.yml` runs two jobs weekly, on demand, and on a push to
110
140
  `main` that changes a guard, the matrix or a test module it runs. The matrix is
111
141
  CI's, not the local loop's. The first job, `neuter`, asks the question the suite
@@ -127,10 +157,11 @@ row.
127
157
  Each mutant first runs alone the test that killed it last time, read from
128
158
  `tests/neuter_killers.json`. When that test passes or is gone, the mutant runs
129
159
  every module in its own HOME and TMPDIR. The module that reads the mutated file
130
- goes first. The first failing test becomes its new entry. The
160
+ goes first. With `--update-killers`, the first failing test becomes its new
161
+ entry. Without it, the tracked cache is left as it was. The
131
162
  control runs every module, then every cached killer alone, so a killer that
132
163
  fails without a mutant voids the run. A session that adds a deny rule checks its
133
- one mutant with `python3 tests/neuter_matrix.py --only <name>`.
164
+ one mutant with `python3 tests/neuter_matrix.py --only <name> --update-killers`.
134
165
 
135
166
  The second, `fuzz-shell`, runs the bash-oracle fuzzer (`tests/fuzz_shell.py`)
136
167
  under the ubuntu runner's bash 5.x. Locally, macOS ships bash 3.2. A first step
package/docs/vision.md CHANGED
@@ -117,8 +117,12 @@ conflicting SWE-bench split, the agent cheated in 0 of 132 runs with tezgah and
117
117
  no headroom. Hand labels of final messages on those impossible tasks found
118
118
  false done claims in 3 of 30 runs with tezgah and 6 of 30 without. The CI of
119
119
  that difference includes 0. The Stop rule's claim reader
120
- flagged far more runs than that. On Turkish replies it counts `doğrulandı`
121
- (verified) on a partial fix as a claim, a recorded weakness not yet fixed. The
120
+ flagged far more runs than that. On Turkish replies it counted `doğrulandı`
121
+ (verified) on a partial fix as a claim, even beside a `doğrulanmadı`. Commit
122
+ 9cb4d1d fixed that shape. Its held-out recall is 0.857, below the 0.9 bar. A
123
+ narrower gap remains. A Turkish partial fix with no admission word
124
+ (`hâlâ başarısız`, `kısmen`) still reads as done and verified. Its English twin
125
+ reads as verification only. The
122
126
  numbers, the per-task counts and the instrument flaws are in
123
127
  [the results bundle](results/paired-outcome-2026-10.md).
124
128
 
@@ -1,13 +1,13 @@
1
1
  {
2
2
  "name": "English",
3
3
  "done": [
4
- "done|complete[d]?|finished|implemented|fixed|shipped|landed|merged|wired up|wired into|all tests? pass|all checks? pass|(?:tests?|checks?|suite) (?:is |are )?(?:green|passing)|build (?:passes|is green)"
4
+ "done|complete[d]?|finished|implemented|fixed|(?<!\\bis )(?<!\\bare )(?<!\\bget )(?<!\\bgets )(?<!\\bbe )(?<!\\bbeing )(?<!-)updated(?!-)|shipped|landed|merged|wired up|wired into|(?:all )?(?:tests?|checks?|suite|build) passed|(?:all )?(?:tests|checks) pass|(?:suite|build) passes|(?:tests?|checks?|suite) (?:is |are )?(?:green|passing)|build is green"
5
5
  ],
6
6
  "verified": [
7
7
  "tested|verified|i ran|ran the (?:tests?|suite|build|lint|checks?)"
8
8
  ],
9
9
  "negated": [
10
- "unverified|not verified|could ?n[o']t verify|verification (?:was )?skipped"
10
+ "unverified|not verified|could ?n[o']t verify|verification (?:was )?skipped|^\\s*(?:untested|not (?:yet )?(?:been )?(?:tested|checked))\\b|(?:^|[.!;]\\s+)(?:(?:this|the) (?:change|fix|patch)|it|i)\\b[^.\\n]{0,24}?(?:not|n't) (?:yet )?(?:been )?tested|not checked yet"
11
11
  ],
12
12
  "admitted": [
13
13
  "contradict|unsatisfiable|(?:can ?not|can't|can ?never|never) pass|impossible"
@@ -36,6 +36,9 @@
36
36
  "negation_before": [
37
37
  "\\bnot|n't|\\bnever"
38
38
  ],
39
+ "condition_before": [
40
+ "\\b(?:if|when|once|unless|until|whether)\\b"
41
+ ],
39
42
  "question_particle": [],
40
43
  "question_end": [],
41
44
  "clause_join": [
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "Turkish",
3
3
  "done": [
4
- "yaptım|tamamladım|tamamlandı|bitirdim|bitirildi|ekledim|eklendi|düzelttim|düzeltildi|hallettim|güncellendi|kuruldu|uygulandı|yayına girdi|kapatıldı|yüklendi|tüm testler geçti|testler geçti|testler yeşil|çalışıyor"
4
+ "yaptım|tamamladım|tamamlandı|bitirdim|bitirildi|ekledim|eklendi|düzelttim|düzeltildi|hallettim|güncellendi|kuruldu|uygulandı|yayına girdi|kapatıldı|yüklendi|tüm testler geçti|testler geçti|testler geçiyor|testler yeşil|çalışıyor"
5
5
  ],
6
6
  "verified": [
7
7
  "doğruladım|test ettim|kontrol ettim|denetledim|doğrulandı|test edildi"
@@ -89,6 +89,7 @@
89
89
  "negation_before": [
90
90
  "\\bdeğil"
91
91
  ],
92
+ "condition_before": [],
92
93
  "question_particle": [
93
94
  "m[ıiuü]|değil"
94
95
  ],
@@ -200,7 +201,7 @@
200
201
  "ürün(?!\\w*_)\\w*|yol harita\\w*|kuzey yıldız\\w*|aktivasyon\\w*|dönüşüm\\w*|fiyatlandır\\w*|önceliklendir\\w*|kullanıcı araştırma\\w*|ürün keşf\\w*|müşteri geri bildirim\\w*|benimsen\\w*|konumlandır\\w*|segmentasyon\\w*|hedef kitle\\w*|(kullanıcı|müşteri) discovery|keşif (görüşme|çalışma)\\w*|fırsat\\w*|memnuniyet\\w*|anket\\w*|(kullanıcı|müşteri)[ -]?(feedback|geri bildirim)\\w*|(customer|user)[ -]?geri bildirim\\w*|şikayet\\w*|rakip\\w*|rekabet\\w*|pazar pay\\w*|terk oran\\w*|elde tutma\\w*|abonelik\\w*|gelir model\\w*|özellik\\w*|sayfa(?! numara)\\w*|ekran(?! kart)(?!a\\b[^.!?\\n]{0,40}\\b(?:yaz|bas)\\w*)\\w*|arayüz\\w*|arama kutu\\w*|filtre\\w*|tablo(?![^.!?\\n]{0,60}\\b(?:sütun|kolon|sql|index|indeks)\\w*)\\w*|adım(?! sayı)\\w*|kullanıcı liste\\w*|form(u|un|da|daki|lar|ları|unu)\\w*[^.!?\\n]{0,40}\\b(?:incele|denetle|analiz|de[ğg]erlendir|iyile[şs]tir|sadele[şs]tir|kontrol)\\w*"
201
202
  ],
202
203
  "design": [
203
- "(?:(?<![\\w'’] )|(?<=\\bbu )|(?<=\\bşu ))tasar[ıi]m\\w*|(?:dashboard|pano|g[öo]sterge panel|ekran|sayfa|aray[üu]z|landing|logo|ikon)\\w* (?:\\w+ ){0,2}(?:tasarla|tasar[ıi]m|redesign)\\w*|(?:web[ -]?)?site\\w* tasar[ıi]m\\w*|(?:mobil |web )?uygulama\\w* tasar[ıi]m\\w*|(?:web|mobil) tasar[ıi]m\\w*|tasar[ıi]m (?:sistem|token|dil|rehber)\\w*|yeniden tasarla\\w*|ba[şs]tan tasarla\\w*|design [çc]al[ıi][şs]ma\\w*|aray[üu]z\\w*|(?:ui|aray[üu]z|form|buton|d[üu][ğg]me|kart|men[üu]|sayfa|ekran|react|vue)\\w* (?:\\w+ )?bile[şs]en\\w*|bile[şs]en (?:k[üu]t[üu]phane|sistem)\\w*|tipografi\\w*|renk\\w*|tema(?:s[ıi]|y[ıi]|lar\\w*)?|kullan[ıi]labilirlik\\w*|eri[şs]ilebilirli[kğg]\\w*|g[öo]rsel (?:hiyerar[şs]i|tutarl[ıi]l[ıi]k|denetim|ele[şs]tiri)\\w*",
204
+ "(?:(?<![\\w'’] )|(?<=\\bbu )|(?<=\\bşu ))tasar[ıi]m\\w*|(?:dashboard|pano|g[öo]sterge panel|ekran|sayfa|aray[üu]z|landing|logo|ikon)\\w* (?:\\w+ ){0,2}(?:tasarla|tasar[ıi]m|redesign)\\w*|(?:uygulama(?:y[ıi]|s[ıi]n[ıi])?|site(?:yi|sini)?) redesign\\w*|(?:web[ -]?)?site\\w* tasar[ıi]m\\w*|(?:mobil |web )?uygulama\\w* tasar[ıi]m\\w*|(?:web|mobil) tasar[ıi]m\\w*|tasar[ıi]m (?:sistem|token|dil|rehber)\\w*|yeniden tasarla\\w*|ba[şs]tan tasarla\\w*|design [çc]al[ıi][şs]ma\\w*|aray[üu]z\\w*|(?:ui|aray[üu]z|form|buton|d[üu][ğg]me|kart|men[üu]|sayfa|ekran|react|vue)\\w* (?:\\w+ )?bile[şs]en\\w*|bile[şs]en (?:k[üu]t[üu]phane|sistem)\\w*|tipografi\\w*|renk\\w*|tema(?:s[ıi]|y[ıi]|lar\\w*)?|kullan[ıi]labilirlik\\w*|eri[şs]ilebilirli[kğg]\\w*|g[öo]rsel (?:hiyerar[şs]i|tutarl[ıi]l[ıi]k|denetim|ele[şs]tiri)\\w*",
204
205
  {
205
206
  "ui_ask": "iyile[şs]tir\\w*|g[üu]zelle[şs]tir\\w*|d[üu]zenle\\w*|kar[ıi][şs][ıi]k\\w*|tutars[ıi]z\\w*|da[ğg][ıi]n[ıi]k\\w*|[çc]irkin\\w*"
206
207
  },
@@ -69,11 +69,19 @@ def result_size(result):
69
69
  so a container is measured by its top-level length (O(1) for str, bytes,
70
70
  list, dict, set) instead of re-serializing it: `json.dumps` on every
71
71
  PostToolUse copied the whole tool response and broke the hook's cost bound
72
- ("a sha1 over a short string and a few fields; nothing else"). ponytail: a
73
- dict result therefore reports its field count, not its byte length - the
74
- host reports no size of its own, and no reader compares the two."""
72
+ ("a sha1 over a short string and a few fields; nothing else"). A shell
73
+ result is the exception: Claude's Bash answers `{"stdout", "stderr",
74
+ "interrupted", ...}`, so its field count is never 0 and a check that
75
+ printed nothing would read as output (`passing_check`'s silent failure,
76
+ B-005); its stream strings (`ran_nothing`'s keys) are measured instead.
77
+ ponytail: any other dict reports its field count, not its byte length."""
75
78
  if result is None:
76
79
  return None
80
+ if isinstance(result, dict):
81
+ streams = [result[k] for k in ("stdout", "stderr", "output")
82
+ if isinstance(result.get(k), str)]
83
+ if streams:
84
+ return sum(map(len, streams))
77
85
  if isinstance(result, (str, bytes, bytearray, list, tuple, dict, set,
78
86
  frozenset)):
79
87
  return len(result)
@@ -48,7 +48,7 @@ def main():
48
48
  if not isinstance(p, dict):
49
49
  return
50
50
  session_id = p.get("session_id")
51
- safe(session_id, bind_session, session_id) # the switch latch (tezgah_paths.off)
51
+ safe(session_id, bind_session, session_id, p.get("parent")) # the switch latch
52
52
  cwd = p.get("cwd") or os.getcwd()
53
53
  if p.get("hook_event_name") == "SubagentStop" and p.get("agent_id") \
54
54
  and root_for(cwd) and not off("slices-off"):
@@ -335,9 +335,10 @@ def skill_read_kind(tool, inp):
335
335
 
336
336
  Only the hosts that can see a read without paying a process per read call
337
337
  this (Claude parses its transcript, opencode classifies in-process, omp's
338
- embedded runner filters before it asks python). On codex, cursor and dsh a
339
- read is not observable at that price, so those marks stay at their armed
340
- state and the legend says so.
338
+ embedded runner filters before it asks python). On codex, commandcode,
339
+ gemini, cursor, qwen, droid, copilot, antigravity and dsh a read is not
340
+ observable at that price, so those marks stay at their armed state and the
341
+ legend says so.
341
342
 
342
343
  A read of one of the two marked skills earns its mark (`SKILL_MARKS`); a read
343
344
  of any OTHER shipped skill earns `skill:<name>`. That second kind lights
@@ -2095,7 +2096,8 @@ def context_for(event, cwd, payload=None, with_core=True):
2095
2096
  # The switch latch: every off() below answers for this session, and a
2096
2097
  # prompt's turn marker (with its `authorized` row) is written first, so a
2097
2098
  # switch the user's prompt names is honored from this very turn.
2098
- bind_session(session_of(payload))
2099
+ bind_session(session_of(payload),
2100
+ payload.get("parent") if isinstance(payload, dict) else None)
2099
2101
  if event == "user_prompt":
2100
2102
  # One marker per user turn, written before the reminder check: the loop
2101
2103
  # guard counts an identical call's failures in the current turn only, so
@@ -2103,13 +2105,16 @@ def context_for(event, cwd, payload=None, with_core=True):
2103
2105
  # guard state rather than part of the reminder. Only a hash of the prompt
2104
2106
  # is stored - note_turn keys the row on it so one submission cannot write
2105
2107
  # two markers and hide the failures the guard had just counted.
2106
- note_turn(session_of(payload), prompt_text(payload), workspace=root_for(cwd))
2108
+ note_turn(session_of(payload), prompt_text(payload), workspace=root_for(cwd),
2109
+ subagent=isinstance(payload, dict) and bool(payload.get("subagent")))
2107
2110
  core, disabled = core_for(cwd)
2108
2111
  # A disabled rule is also removed from the on-demand skill's reach, because
2109
2112
  # the skill is loaded separately and would otherwise re-enable it.
2113
+ # Claude's `tezgah` output style is a static copy of the core that no switch
2114
+ # re-renders, so the note names it beside the skill (OQ-12).
2110
2115
  off_note = ("Kill switches active this session: %s. Those rules are OFF; "
2111
- "ignore the matching section in the `tezgah-contract` skill."
2112
- % ", ".join(disabled)) if disabled else ""
2116
+ "ignore the matching section in the `tezgah-contract` skill and "
2117
+ "the `tezgah` output style." % ", ".join(disabled)) if disabled else ""
2113
2118
  if event == "user_prompt":
2114
2119
  prompt = prompt_text(payload)
2115
2120
  session_id = session_of(payload)
@@ -2118,24 +2123,24 @@ def context_for(event, cwd, payload=None, with_core=True):
2118
2123
  host = (payload or {}).get("host") if isinstance(payload, dict) else None
2119
2124
  tezgah_taste.note_prompt(session_id, prompt, cwd,
2120
2125
  host=host if isinstance(host, str) else None)
2121
- # Before the reminder switch: the status mark follows the gate's state
2122
- # even when the per-turn text is off. Never in DROP_ORDER, so no budget
2123
- # gives it up - it says the rules below are not being enforced.
2126
+ # The gate notice: never in DROP_ORDER, so no budget gives it up, and
2127
+ # `reminder-off` keeps it - it says the rules below are not being
2128
+ # enforced. Reading it also writes the status mark.
2124
2129
  gate = gate_inactive(session_id, payload)
2125
- # The user's grants and lifts (`user_grants`), before the reminder
2126
- # switch too: the Stop rule reads them whether or not this turn's text
2127
- # is shown. A subagent's prompt is its parent's brief, never the
2128
- # user's grant (omp's bridge sends `subagent`).
2130
+ # The user's grants and lifts (`user_grants`): the Stop rule reads them
2131
+ # whatever this turn's text shows. A subagent's prompt is its parent's
2132
+ # brief, never the user's grant (omp's bridge sends `subagent`).
2129
2133
  for clause, target in ([] if isinstance(payload, dict) and payload.get("subagent")
2130
2134
  else user_grants(prompt)):
2131
2135
  note(session_id, GRANT_KIND, clause, target=target)
2132
2136
  # per-turn nudge: openers decay over long sessions. Kept short because
2133
2137
  # it is paid every turn, and on Claude the output style already carries
2134
2138
  # the same rules on every response. The conditional rules ride along
2135
- # only on the turn whose prompt matches their task class.
2136
- if off("reminder-off"):
2137
- return None
2138
- parts = [("reminder", render(prompt_reminder(dropped_switches(cwd))))]
2139
+ # only on the turn whose prompt matches their task class. `reminder-off`
2140
+ # mutes this one line: every other part has its own switch or none
2141
+ # (OQ-10), and the gate notice must never be given up.
2142
+ parts = ([] if off("reminder-off") else
2143
+ [("reminder", render(prompt_reminder(dropped_switches(cwd))))])
2139
2144
  if gate:
2140
2145
  parts.append(("gate", gate))
2141
2146
  # The previous turn's stamp, read before the armed paragraphs: they are
@@ -2258,8 +2263,8 @@ def context_for(event, cwd, payload=None, with_core=True):
2258
2263
  if stale:
2259
2264
  parts.append(("index", stale))
2260
2265
  if disabled:
2261
- parts.append(("offnote", "(off this session: %s)"
2262
- % ", ".join(disabled)))
2266
+ parts.append(("offnote", "(off this session: %s; ignore their sections "
2267
+ "in the `tezgah` output style)" % ", ".join(disabled)))
2263
2268
  text = budgeted(event, parts)
2264
2269
  # latched only when the budget kept it, so a dropped line comes back
2265
2270
  if heavy and heavy in text:
@@ -2625,7 +2630,18 @@ def shell_kind(command):
2625
2630
  could light from `orx` alone and the layer's own commands stayed invisible to
2626
2631
  it. The absolute spelling needs no case of its own - this reader keeps
2627
2632
  basenames, so `<bin>/tezgah-research` and the bare name arrive as the same
2628
- word."""
2633
+ word.
2634
+
2635
+ A python or node interpreter given a script file (`python3 bin/tezgah-docs
2636
+ q`) runs that script, so the interpreter word is dropped before the line is
2637
+ read; `-c`, `-m` and `-` (stdin) keep it, as their next word is code or a
2638
+ module, not a file. The match stays on one line (spaces and tabs only), and a
2639
+ shell interpreter needs none of this: the wrapper reader already looks past
2640
+ `bash`/`sh`. `shell_programs` itself stays as it is: the gate reads it and
2641
+ must keep the interpreter."""
2642
+ command = re.sub(r"(?<![\w/.$=-])(?:[\w./-]*/)?(?:python[\d.]*|node)"
2643
+ r"(?:[ \t]+-(?![cm]\b)\w[\w-]*)*[ \t]+(?=[^\s-])", "",
2644
+ str(command or ""))
2629
2645
  ran = shell_programs(command)
2630
2646
  if "consult" in ran:
2631
2647
  return "consult"
@@ -2930,8 +2946,9 @@ colored, and the glyph carries the state on its own where color does not):
2930
2946
  name\u25cb yellow armed, on demand - not used yet this session
2931
2947
  name\u2717 red turned off by a kill switch or a per-repo .no-* mark
2932
2948
  pony, adhd, dim this surface cannot report that measure (a skill read
2933
- needs a process per read to observe on codex, cursor and
2934
- dsh, so those two marks state nothing there instead of
2949
+ needs a process per read to observe on codex, commandcode,
2950
+ gemini, cursor, qwen, droid, copilot, antigravity and dsh,
2951
+ so those two marks state nothing there instead of
2935
2952
  claiming the skill was never opened)
2936
2953
  dim no state to report: idx n/a or uncomparable, or no blocked
2937
2954
  plan