@r1z4x/tezgah 1.0.0 → 1.2.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 +92 -1
  2. package/MANIFEST +10 -0
  3. package/bin/tezgah-agents +10 -1
  4. package/bin/tezgah-docs +1 -1
  5. package/bin/tezgah-doctor +11 -0
  6. package/bin/tezgah-research +31 -14
  7. package/bin/tezgah-setup +154 -29
  8. package/bin/tezgah-skill +59 -0
  9. package/bin/tezgah-skill.py +59 -0
  10. package/bin/tezgah-status +62 -1
  11. package/bin/tezgah-taste +504 -5
  12. package/bin/tezgah-triage +5 -3
  13. package/docs/README.md +1 -0
  14. package/docs/architecture.md +5 -2
  15. package/docs/citations-baseline.json +1 -1
  16. package/docs/clarity-baseline.json +9 -9
  17. package/docs/contract.md +68 -26
  18. package/docs/environment.md +6 -3
  19. package/docs/evidence.md +7 -0
  20. package/docs/gate.md +13 -7
  21. package/docs/glossary.md +1 -1
  22. package/docs/hosts.md +48 -1
  23. package/docs/index.json +34 -3
  24. package/docs/judge.md +75 -60
  25. package/docs/models.md +14 -8
  26. package/docs/operations.md +80 -25
  27. package/docs/orca.md +126 -0
  28. package/docs/research.md +38 -3
  29. package/docs/skills.md +74 -0
  30. package/docs/status-line.md +3 -1
  31. package/docs/testing.md +33 -21
  32. package/docs/vision.md +12 -8
  33. package/hooks/projects-posttooluse.py +7 -0
  34. package/hooks/projects-pretooluse.py +17 -5
  35. package/hooks/tezgah_agents.py +135 -13
  36. package/hooks/tezgah_components.py +3 -0
  37. package/hooks/tezgah_context.py +84 -22
  38. package/hooks/tezgah_gate.py +133 -17
  39. package/hooks/tezgah_integrity.py +60 -13
  40. package/hooks/tezgah_judge.py +288 -66
  41. package/hooks/tezgah_models.py +41 -5
  42. package/hooks/tezgah_orca.py +144 -0
  43. package/hooks/tezgah_paths.py +40 -6
  44. package/hooks/tezgah_policy.py +109 -35
  45. package/hooks/tezgah_research.py +104 -8
  46. package/hooks/tezgah_skill_pick.py +459 -8
  47. package/hooks/tezgah_stack.py +382 -0
  48. package/hooks/tezgah_stopspec.py +2 -2
  49. package/hooks/tezgah_taste.py +35 -9
  50. package/hooks/tezgah_taste_ledger.py +628 -0
  51. package/hosts/codex/hook.py +12 -1
  52. package/hosts/cursor/hook.py +12 -4
  53. package/hosts/omp/hook.py +18 -6
  54. package/hosts/omp/tezgah-hook.ts.in +20 -7
  55. package/hosts/opencode/plugins/tezgah.js +28 -15
  56. package/output-styles/tezgah.md +34 -19
  57. package/package.json +1 -1
  58. package/skills/i-have-adhd/SKILL.md +2 -2
  59. package/skills/research/SKILL.md +60 -17
  60. package/skills/tezgah-contract/SKILL.md +55 -13
package/docs/research.md CHANGED
@@ -65,7 +65,7 @@ nothing recorded the ask and `close --limit` accepted any string.
65
65
  `state.json` `rules` is the line's format version. `init` writes `RULES`, the
66
66
  newest set (`hooks/tezgah_research.py::RULES`). Each set keeps its own gate. Rules 2
67
67
  are the standards rules. Rules 3 add the ask contract. Rules 4 add the protocol
68
- sections below. A bump never weakens an older set.
68
+ sections below. Rules 5 add the scope lock. A bump never weakens an older set.
69
69
 
70
70
  A `rules` above `RULES` means a later tezgah wrote the line. `check` then reports
71
71
  one error that names both numbers and reads nothing else. The writers refuse the
@@ -110,14 +110,49 @@ it. The rules come from the gortex/SoL-Pi adoption's ideas 3 and 4 and from the
110
110
  layer audit's P2 probe (`_check_protocol_sections`, `_token_claim_problem`).
111
111
  Older lines keep the word rule.
112
112
 
113
+ ## Scope lock (rules 5)
114
+
115
+ The owner reported on 2026-10-07 that research turned every question into
116
+ metrics. It also grew past the ask, stayed shallow, and drifted off topic. The
117
+ rule text pushed each of these. The injected rule sent every research task to
118
+ orx's experiment tree. The skill locked a metric at bootstrap. `check` refused a
119
+ line past `bootstrap` without one. An analysis owed three compared variants. The
120
+ ideation moves had no fence. Rules 5 lock a line to its question:
121
+
122
+ - `init --in "<covered>" --out "<left alone>"` (repeatable) write `state.json`
123
+ `scope`. The in-scope items are `S1`, `S2`, ... in order. `--method
124
+ qualitative|measured` writes `method`. `scope_problems`
125
+ (`hooks/tezgah_research.py::scope_problems`) names an empty `scope.in`, an
126
+ empty `scope.out` and a missing method. `check` refuses them past `bootstrap`
127
+ and warns at it.
128
+ - A claim's `trace` names the in-scope item it answers (`S<n>` or the item's
129
+ text). `claim_problems` and `_check_claims` both call `trace_problem`
130
+ (`hooks/tezgah_research.py::trace_problem`). The writer and the checker refuse
131
+ the same off-topic claim.
132
+ - A `qualitative` line owes no metric. `_check_evaluation`
133
+ (`hooks/tezgah_research.py::_check_evaluation`) skips it. `_check_scope`
134
+ refuses one that records `results.jsonl`. A measured question says `measured`
135
+ and locks its evaluation first.
136
+ - An `analysis` deliverable owes no variants (`deliverable_problems` takes the
137
+ line's rules). A design, plan or code still does. Older lines keep three.
138
+
139
+ The session reads the same lock in the research rule
140
+ (`hooks/tezgah_policy.py::RESEARCH` and the paragraph `**Research: route it to
141
+ OpenResearch.**`). Restate the question verbatim. Name what is in and out of
142
+ scope. Drop a finding that answers nothing asked. Pick the method from the
143
+ question. Answer a plain factual question in the reply from primary sources, with
144
+ no line. Cite each finding's source with a confidence. The host-subagent fallback
145
+ gets the question, its scope and its method in its brief. The research skill's
146
+ "Scope and method" section is the long form.
147
+
113
148
  ## One line, one directory
114
149
 
115
150
  | Path | Holds |
116
151
  |---|---|
117
- | `state.json` | the question, the phase (`bootstrap`/`inner`/`outer`/`concluded`), the direction, the locked evaluation and its optional second gate (`capability_tolerance`, `counter_metric`), the session events (`PHASES`, `hooks/tezgah_research.py::PHASES`), `rules` (the rule set `init` opened it under, `RULES`), `deliverable` (`kind`, `path`, `ask`, `min_variants`), `supersedes` for a new version of an older line, and `closed` once `close` records a deliberate limit. The phase is the author's declaration; `derived_phase` (`hooks/tezgah_research.py::derived_phase`) reads the *other* authority beside it from the line's own artifacts, and the checker names the two disagreeing |
152
+ | `state.json` | the question, the phase (`bootstrap`/`inner`/`outer`/`concluded`), the direction, `method` and `scope` (rules 5, see "Scope lock"), the locked evaluation and its optional second gate (`capability_tolerance`, `counter_metric`), the session events (`PHASES`, `hooks/tezgah_research.py::PHASES`), `rules` (the rule set `init` opened it under, `RULES`), `deliverable` (`kind`, `path`, `ask`, `min_variants`), `supersedes` for a new version of an older line, and `closed` once `close` records a deliberate limit. The phase is the author's declaration; `derived_phase` (`hooks/tezgah_research.py::derived_phase`) reads the *other* authority beside it from the line's own artifacts, and the checker names the two disagreeing |
118
153
  | `log.md` | the decision log, newest last: one line per decision, experiment, dead end or pivot, with the evidence that drove it |
119
154
  | `findings.md` | the four sections every line answers, named by `FINDINGS_SECTIONS` (`hooks/tezgah_research.py::FINDINGS_SECTIONS`) |
120
- | `claims.jsonl` | one JSON object per row: `statement`, `falsification`, `proof`, `provenance`, `status`, `kind`, `scope` (what the claim's numbers were measured on, which may not be wider than the rows it rests on), and `supersedes` when the row replaces an earlier claim |
155
+ | `claims.jsonl` | one JSON object per row: `statement`, `falsification`, `proof`, `provenance`, `status`, `kind`, `trace` (the in-scope item, rules 5), `scope` (what the claim's numbers were measured on, which may not be wider than the rows it rests on), and `supersedes` when the row replaces an earlier claim |
121
156
  | `predictions.jsonl` | one JSON object per row: `commit` (a 40-character sha the row is bound to), `claim` (an id the line holds, or empty when the prediction stands alone), `metric`, `value_before`, `value_after`, `falsifier`, `components` (the keys of the components the change touches, read from `hooks/tezgah_components.py`), and `granted_by` when a human granted what the frozen paths below forbid |
122
157
  | `experiments/<hypothesis>/protocol.md` | what the change is, what it predicts, what result would falsify it, why - committed before the run |
123
158
  | `experiments/<hypothesis>/results.jsonl` | the rows the run produced, one JSON object per non-blank line, each carrying a non-empty `source` and, once declared, a `scope` of `real`, `fixture` or `derived`, and - when that scope is `fixture` - a `fixture` description of what was generated |
package/docs/skills.md CHANGED
@@ -83,6 +83,66 @@ directory that host reads, and the host lists `name` + `description` itself -
83
83
  which is why the installer counts that metadata as an always-on cost
84
84
  (`bin/tezgah-setup::context_budget`).
85
85
 
86
+ ## Searching skills, and reading part of one
87
+
88
+ A session meets the roster as names (omp's list runs to hundreds once a
89
+ `skills.customDirectories` corpus is mounted - 284 on this machine), and no host
90
+ searches bodies by topic. `bin/tezgah-skill` does: it indexes every installed
91
+ skill root - the checkout's, omp's user and custom-directories corpora,
92
+ Claude's, `~/.agents`, Codex's, opencode's - into heading-delimited sections
93
+ (`hooks/tezgah_skill_pick.py::sections`) and ranks them for a query with the
94
+ lessons ledger's BM25 (`hooks/tezgah_rank.py::rank`, heading weighted over
95
+ body). Each hit is a section:
96
+
97
+ tezgah-skill "jwt alg none"
98
+ skill://attack-jwt:26-122 /Users/.../attack-jwt/SKILL.md JWT Token Attack
99
+
100
+ The `skill://<name>:<start>-<end>` address is a range omp's read tool resolves
101
+ (line selectors are part of the `read` contract; `skill://<name>/<file>` reads a
102
+ neighbour file, and `/skill:<name>` injects the whole body - a command, not a
103
+ range). The absolute path beside it is for hosts without a `skill://`
104
+ transport, and both are printed because a name that differs between roots keeps
105
+ only omp's precedence - the search reports the first root's copy rather than
106
+ resolving the collision. Measured on this machine: 7 roots, 10 482 sections,
107
+ 2.3-2.6 s cold. Fenced code is not indexed as headings, so a `# comment`
108
+ inside a block never masquerades as a section title.
109
+
110
+ The per-turn hint reuses the same ranking: when the armed judgement names a
111
+ skill (`skill-suggest-on`, [contract](contract.md)), the appended line points at
112
+ the matching SECTION of that skill - `Start at skill://ponytail:38-61` plus the
113
+ path - so the session opens the part that answers the turn instead of a name it
114
+ must skim (`hooks/tezgah_skill_pick.py::section_of`, pinned by
115
+ `tests/test_skill_pick.py::OmpHintPath` through omp's own dispatch). The search
116
+ CLI is local and unarmed: it costs nothing until it is run, and the judgement
117
+ stays opt-in exactly as before.
118
+
119
+ The always-on Session scope paragraph sends every session to it. Run
120
+ `tezgah-skill <words>` before working a topic a skill covers, then read only
121
+ the returned range (`hooks/tezgah_policy.py::CORE`). The installer links the
122
+ CLI into `~/.config/tezgah/bin` beside the other tezgah commands.
123
+
124
+ On a turn the judgement does not answer, the same search runs on its own,
125
+ default on and with no model (`hooks/tezgah_skill_pick.py::section_hint`). It
126
+ keeps its index as SQLite in the tezgah cache dir (`skill-search.sqlite`). It
127
+ rebuilds that index only when a SKILL.md's mtime or size moves
128
+ (`hooks/tezgah_skill_pick.py::_open_index`). A prompt reads its own terms'
129
+ postings and the length column, never the corpus.
130
+
131
+ The hook then appends one line, `A local skill search matched this request:
132
+ skill://<name>:<start>-<end>`, with the section title. Every host but omp also
133
+ gets the absolute path. The top section must carry 60% of the prompt's
134
+ distinct content terms that any skill contains, and at least three
135
+ (`hooks/tezgah_skill_pick.py::HINT_COVER`, `HINT_MIN_TERMS`). Each section
136
+ appears once per session.
137
+
138
+ Measured on this machine on 2026-10-07: 7 502 sections and a 9.5 MB index. The
139
+ one cold rebuild took 1.36 s, and a warm prompt takes 6-15 ms. On a turn it
140
+ fires the line costs 196-239 bytes on omp and 245-320 elsewhere. It is not a
141
+ judgement, so `judge-off` leaves it on. `reminder-off` drops it with the rest
142
+ of the per-turn text (`tests/test_skill_search.py::LocalHint`,
143
+ `OmpPromptPath`).
144
+
145
+
86
146
  ## How a skill reaches a host
87
147
 
88
148
  `SKILLS` in `bin/tezgah-setup::SKILLS` is the shipped list, and it is the single
@@ -127,6 +187,17 @@ it (`tests/test_skills.py::SkillStandards`).
127
187
  | `ai-research` | The vendored 98-skill library for AI/ML machinery - training and serving a model, benchmarks, interpretability, retrieval pipelines; read one entry, never the tree. |
128
188
  | `rl-env` | The vendored FineEnvs skill set for authoring an RL environment from a description - OpenEnv, OpenReward/ORS, Verifiers and NeMo Gym, plus the orchestrator that ports one description across all four; read one entry, never the tree. |
129
189
 
190
+ The generated specialists load skills first (`hooks/tezgah_agents.py::SPECIALISTS`).
191
+ `tezgah-agents --list` prints the roster with tiers.
192
+
193
+ | Specialist | Skills it loads |
194
+ |---|---|
195
+ | `tezgah-docs` | `no-ai-slop` |
196
+ | `tezgah-ui` | `analyze-app`, `design-contract`, `design-library`, `feature-audit` |
197
+ | `tezgah-researcher` | `research`, `ai-research` |
198
+ | `tezgah-security` | the host's `wstg-*`, `attack-*`, `ci-assessment` and `llm-security` skills, which tezgah does not ship |
199
+ | `tezgah-tester` | none |
200
+
130
201
  ## Vendored material
131
202
 
132
203
  `NOTICE` records every third-party skill and its terms: it opens by saying the
@@ -229,6 +300,9 @@ unlisted directory, and fails on a body that pairs 44x44 with Level AA again.
229
300
  its index and its three trap cases
230
301
  - `skills/rl-env/SOURCE` - the vendored FineEnvs environment-authoring set, its
231
302
  upstream revision and the per-file sha256
303
+ - `bin/tezgah-skill` - the section search CLI over every installed skill root;
304
+ `hooks/tezgah_skill_pick.py` - `sections`, `search`, `skill_roots`,
305
+ `section_of`, the hint that carries the address
232
306
  - `hooks/tezgah_policy.py` - the always-on rule text and the Claude skill name
233
307
  - `tests/test_setup.py`, `tests/test_skills.py` - the router-line, name
234
308
  resolution, frontmatter, collision and skill-standard tests
@@ -105,7 +105,8 @@ must use it": a missing key or binary reads the same red as a kill switch
105
105
  (`consult_options()`, `orx_bin()`, `have_judge_key()`: `hooks/tezgah_paths.py::consult_options`,
106
106
  `hooks/tezgah_paths.py::orx_bin`, `hooks/tezgah_paths.py::have_judge_key`). `judge` is the seam
107
107
  behind `bin/tezgah-triage`, `bin/tezgah-docs` and the skill picker
108
- ([judge](judge.md)), and its credential is the seam's own two channels, never
108
+ ([judge](judge.md)). Its credential is the session's own CLI, else a third-party
109
+ key `fallback` allows, never
109
110
  omp's login store. `graph` and `orch` are the two switch-only marks: nothing at
110
111
  arming time asks whether their toolchain is on the machine, because neither rule
111
112
  leaves the text when it is missing - the graph-first rule ships whether or not
@@ -229,6 +230,7 @@ tezgah-status /path/to/repo <id> --counters # the evidence ledger's counters
229
230
  tezgah-status --legend # what each mark means
230
231
  tezgah-status --rule-yield # live gate rules that stopped firing
231
232
  tezgah-status --enforcers # what enforces each always-on rule
233
+ tezgah-status --judge # what the judge used last, and why
232
234
  ```
233
235
 
234
236
  The session id is the second positional argument or `TEZGAH_SESSION`; without it
package/docs/testing.md CHANGED
@@ -26,7 +26,7 @@ request (`CONTRIBUTING.md:15-20`).
26
26
  `ruff` is not vendored: install it as a uv tool, run `uvx ruff check .` without
27
27
  installing it, or use the version CI installs from `requirements-dev.txt:3`
28
28
  (`ruff==0.16.7`). There is no other linter and no type checker
29
- (`AGENTS.md:44-45`).
29
+ (`AGENTS.md:51-52`).
30
30
 
31
31
  The clarity ratchet, `bin/tezgah-docs --clarity`, counts four prose rules per
32
32
  file in `docs/*.md` and `CHANGELOG.md`. The rules are sentences over 25 words,
@@ -41,7 +41,7 @@ baseline on the next run. Only `--clarity --update` raises it.
41
41
  A pin is a claim about what ran, not a guarantee about what will. This repository
42
42
  has two, and both are about the language rather than about dependencies: the
43
43
  floor `py310` (`pyproject.toml:5`) and the CI matrix that proves it
44
- (`.github/workflows/ci.yml:24`). There is no dependency lockfile to pin
44
+ (`.github/workflows/ci.yml:114`). There is no dependency lockfile to pin
45
45
  because there is no package: tezgah is a set of scripts run in place, and no
46
46
  `[project]` or build system is declared (`pyproject.toml:1-2`). The only
47
47
  third-party tool the checks name is ruff, at a version in
@@ -57,30 +57,42 @@ conclude from either.
57
57
 
58
58
  ## Continuous integration
59
59
 
60
- `.github/workflows/ci.yml` runs on every push and pull request. `release.yml`
61
- calls it (`ci.yml:8`) as its `ci` job, so npm and brew publish only after CI
62
- passed on the release commit. It runs five jobs. No matrix
63
- cancels its other legs when one fails (`fail-fast: false`).
64
-
65
- - `test` (`ci.yml:15-43`) on Python 3.10, 3.12, 3.13 and 3.14. 3.10 is the
66
- floor the project supports. The byte-compile step exercises it rather than
67
- asserts it. It byte-compiles, runs the suite with `-v`, lints with
68
- ruff from `requirements-dev.txt` (`ci.yml:37`), audits the docs citations and
69
- runs `TEZGAH_E2E_STRICT=1 python3 tests/e2e_plan_flow.py`.
70
- - `test-sharded` (`ci.yml:48-69`) on Python 3.10 and 3.14 with node 22 (the
71
- first line that loads the omp `.ts` hook without a flag):
72
- `python3 tests/impacted.py --all`. It is a shadow that started 2026-10-05.
73
- After four weeks it replaces the four-version `test` matrix only if it missed
74
- no failure that matrix caught.
75
- - `apps-e2e` (`ci.yml:71-88`) on Python 3.12 and node 20: one step,
60
+ `.github/workflows/ci.yml` runs on a push to `main` and on a pull request, so
61
+ a branch commit runs once, through its pull request, not twice. A tag push does
62
+ not start it. `release.yml` calls it (`ci.yml:13`) as its `ci` job, so npm and
63
+ brew publish only after CI passed on the release commit. A newer push to the
64
+ same ref cancels the run it replaces, except on a release (`concurrency`). Every
65
+ job has a `timeout-minutes`, so a hung test cannot hold a runner for six hours.
66
+ No matrix cancels its other legs when one fails (`fail-fast: false`).
67
+
68
+ - `changes` diffs against the commit the push replaced or the
69
+ pull request's base. A change only under `docs/`, `assets/`, a root `*.md`
70
+ page or the GitHub templates is prose: it runs `docs` and nothing else. An
71
+ empty diff, a base it cannot read, a release or a manual run runs everything.
72
+ - `docs`, prose only: the citations audit and
73
+ `tests/impacted.py --ref <base>`, which maps a docs page to the docs modules.
74
+ - `test` (`ci.yml:100`) on Python 3.10 and 3.14, the floor and the ceiling.
75
+ It byte-compiles, lints with ruff from `requirements-dev.txt` (pip cached),
76
+ audits the docs citations and runs `TEZGAH_E2E_STRICT=1 python3
77
+ tests/e2e_plan_flow.py`. On `main` it also runs the serial suite with `-v`.
78
+ A pull request skips that step, and so does a release, which passes the
79
+ `release` input and runs one 3.10 leg.
80
+ - `test-sharded` (`ci.yml:145`) on Python 3.10 and 3.14 with node 22 (the
81
+ first line that loads the omp `.ts` hook without a flag). On `main` and a
82
+ release it runs `python3 tests/impacted.py --all`. On a pull request it runs
83
+ `--ref <base>`: the modules the change maps to, and the whole suite when the
84
+ map does not know a path. It is a shadow that started 2026-10-05. After four
85
+ weeks it replaces the serial `test` step only if it missed no failure that
86
+ step caught.
87
+ - `apps-e2e` (`ci.yml:178`) on Python 3.12 and node 20: one step,
76
88
  `TEZGAH_E2E_STRICT=1 python3 tests/e2e_analyze_wiring.py`. It completes the
77
89
  app-MCP handshake only - no browser, no device - so it stays deterministic;
78
90
  strict mode makes a server that cannot start a failure rather than a skip, so
79
91
  a broken wire cannot hide behind a skip (`e2e_analyze_wiring.py:4-12`).
80
- - `artifact-install` (`ci.yml:90-128`) on Python 3.10 and 3.12: builds the
92
+ - `artifact-install` (`ci.yml:200`) on Python 3.10 and 3.12: builds the
81
93
  tarball, installs from it under a temp prefix, and runs the artifact smoke on
82
94
  the installed tree instead of the checkout.
83
- - `artifact-install-windows` (`ci.yml:130-214`) on `windows-latest`: the same
95
+ - `artifact-install-windows` (`ci.yml:243`) on `windows-latest`: the same
84
96
  build, install and smoke through `packaging/install.ps1` - the twin a Windows
85
97
  user runs - so it is the only place the Windows claim is proven. It then runs
86
98
  the opencode plugin's path tests natively, `tezgah update --dry-run`, the
@@ -233,7 +245,7 @@ a browser or a device) or to a `_probe_*.py` helper the tests call.
233
245
  - **No network.** Loopback stubs only, as above.
234
246
  - **Two e2e scripts stay opt-in and local**: `e2e_analyze_web.py` and
235
247
  `e2e_analyze_mobile.py`, behind `TEZGAH_E2E_APPS=1`, with the wiring handshake
236
- as their CI substitute (`AGENTS.md:108-114`).
248
+ as their CI substitute (`AGENTS.md:119-125`).
237
249
 
238
250
  ## Adding a test
239
251
 
package/docs/vision.md CHANGED
@@ -51,14 +51,18 @@ change that would silently run nothing.
51
51
 
52
52
  ## Model routing
53
53
 
54
- Every omp agent gets a **cross-family fallback chain** (`hooks/tezgah_models.py::_chain`, over `hooks/tezgah_models.py::funded_families`): the mode's family first,
55
- then every other family this machine holds a credential for. One pin per agent
56
- made one provider's 429 every subagent's failure, because the account is shared
57
- with the main thread (measured: 24 of 26 post-routing 429s hit a subagent's first
58
- request, all on one Opus account; four reviews and three implementation agents
59
- died on it in one day). A family whose credential cannot be detected is left out
60
- of the chain, never guessed in. The main thread's own model, and the provider-keyed
61
- chains, stay the user's settings - tezgah writes only the keys it owns.
54
+ Every omp agent gets a **fallback chain** (`hooks/tezgah_models.py::_chain`, over
55
+ `hooks/tezgah_models.py::funded_families`). The mode's family goes first. By
56
+ default the rest is the same vendor's model through another funded provider, such
57
+ as Opus through OpenRouter on an Anthropic session. `fallback: any` in
58
+ `config.json` adds every other funded family, and `none` keeps the first entry
59
+ alone. One pin per agent made one provider's 429 every subagent's failure, because
60
+ the account is shared with the main thread (measured: 24 of 26 post-routing 429s hit
61
+ a subagent's first request, all on one Opus account). A chain that left the vendor
62
+ ran an Opus session's subagents on GPT and GLM, so that step is opt-in. A family
63
+ whose credential cannot be detected is left out of the chain, never guessed in. The
64
+ main thread's own model, and the provider-keyed chains, stay the user's settings.
65
+ tezgah writes only the keys it owns.
62
66
 
63
67
  What tezgah cannot fix from inside: a provider that stalls on a very large
64
68
  context (`OpenAI completions stream stalled`, five minutes without an event on a
@@ -26,6 +26,7 @@ try:
26
26
  from tezgah_integrity import ( # noqa: E402
27
27
  SUBAGENT_CHANNEL, note_tool, ran_nothing, report_bytes, untrusted_source)
28
28
  from tezgah_paths import root_for # noqa: E402
29
+ from tezgah_taste import write_note # noqa: E402
29
30
  from tezgah_untrusted import marks # noqa: E402
30
31
  except Exception as exc:
31
32
  import_failed(exc)
@@ -139,6 +140,12 @@ def main():
139
140
  # empty-run contract (tezgah_integrity.EMPTY_RUN)
140
141
  empty_run=ran_nothing(result),
141
142
  agent=agent)
143
+ # the opt-in taste note: the learnings in scope for the first write to this
144
+ # file type in the session (hooks/tezgah_taste.write_note), beside the
145
+ # provenance line rather than instead of it
146
+ if event == "PostToolUse" and not failed:
147
+ notice = "\n".join(t for t in (notice, safe(session_id, write_note, session_id,
148
+ inp, cwd, tool)) if t)
142
149
  if notice:
143
150
  json.dump({"hookSpecificOutput": {
144
151
  "hookEventName": event,
@@ -2,8 +2,9 @@
2
2
  """Claude Code PreToolUse hook (matcher Agent|Task|Grep|Bash), inert outside roots.
3
3
 
4
4
  Claude's envelope around the shared gate (hooks/tezgah_gate.py): refuse the
5
- grep-only explorer subagent, and nudge the first identifier-shaped search of a
6
- session toward the code graph. Later searches pass.
5
+ grep-only explorer subagent, and nudge the first code-symbol search of a
6
+ session toward the code graph. Later symbol searches pass with the graph named
7
+ beside them (`additionalContext`, which Claude reads with the tool result).
7
8
  """
8
9
  import json
9
10
  import os
@@ -12,11 +13,17 @@ import sys
12
13
  sys.path.insert(0, os.path.dirname(os.path.realpath(__file__)))
13
14
  from tezgah_guard import import_failed, safe # noqa: E402
14
15
  try:
15
- from tezgah_gate import decision # noqa: E402
16
+ from tezgah_gate import decision, graph_advice # noqa: E402
16
17
  except Exception as exc:
17
18
  import_failed(exc)
18
19
 
19
20
 
21
+ def verdict(tool, inp, cwd, session_id, agent):
22
+ """(deny reason, graph line): the line only for a call the gate lets through."""
23
+ reason = decision(tool, inp, cwd, session_id, agent=agent)
24
+ return reason, None if reason else graph_advice(tool, inp, cwd, "claude")
25
+
26
+
20
27
  def main():
21
28
  try:
22
29
  p = json.load(sys.stdin)
@@ -27,14 +34,19 @@ def main():
27
34
  inp = p.get("tool_input") or {}
28
35
  # `agent_id` is set only when the call comes from a subagent (Claude's hook
29
36
  # reference, common input fields): the repeat ceilings key on it
30
- reason = safe(p.get("session_id"), decision, tool, inp, cwd,
31
- p.get("session_id"), agent=p.get("agent_id"))
37
+ reason, advice = safe(p.get("session_id"), verdict, tool, inp, cwd,
38
+ p.get("session_id"), p.get("agent_id")) or (None, None)
32
39
  if reason:
33
40
  json.dump({"hookSpecificOutput": {
34
41
  "hookEventName": "PreToolUse",
35
42
  "permissionDecision": "deny",
36
43
  "permissionDecisionReason": reason,
37
44
  }}, sys.stdout)
45
+ elif advice:
46
+ json.dump({"hookSpecificOutput": {
47
+ "hookEventName": "PreToolUse",
48
+ "additionalContext": advice,
49
+ }}, sys.stdout)
38
50
 
39
51
 
40
52
  main()
@@ -79,7 +79,12 @@ EMPTY_IS_NOT_PROOF = (
79
79
  STEER = (("mechanical edit (tier from `tezgah-route`)", "tezgah-cheap"),
80
80
  ("bounded code or tests", "tezgah-standard"),
81
81
  ("design, invariants, unknown-cause debugging", "tezgah-frontier"),
82
- ("review of a diff", "tezgah-reviewer"))
82
+ ("review of a diff", "tezgah-reviewer"),
83
+ ("running the impacted checks", "tezgah-tester"),
84
+ ("docs and path:line citations", "tezgah-docs"),
85
+ ("security review", "tezgah-security"),
86
+ ("running-app UI or design analysis", "tezgah-ui"),
87
+ ("a research line", "tezgah-researcher"))
83
88
 
84
89
 
85
90
  def manifest_sha():
@@ -235,6 +240,104 @@ def _worker_body(tier):
235
240
  return body
236
241
 
237
242
 
243
+ # The specialists beside the tier workers: name -> (what it does, the skills it
244
+ # loads first, its tools, the role text). The tier is not here: it is
245
+ # tezgah_models.AGENT_SLOT, the row every host's model line is rendered from, so
246
+ # the description cannot name one tier while the frontmatter runs another.
247
+ # A skill is shipped under skills/ unless HOST_SKILLS names it: those are the
248
+ # security families a host installs on its own, which tezgah does not ship.
249
+ HOST_SKILLS = ("wstg-*", "attack-*", "ci-assessment", "llm-security")
250
+ SPECIALISTS = {
251
+ "tezgah-tester": (
252
+ "Runs only the checks a change reaches and reports them verbatim: the "
253
+ "repo's impacted-test runner or the tests that import the changed "
254
+ "files, never the full suite unless the brief asks.",
255
+ (), "shell to run checks; no source or test edits",
256
+ "Take the changed files from the brief or `git diff --name-only`. Run\n"
257
+ "the repository's own impacted-test runner if it has one (for example\n"
258
+ "`python3 tests/impacted.py --run <files>`), else the test files that\n"
259
+ "import the changed modules. Keep each check last on its line with its\n"
260
+ "output in a file, then read the file in a separate call: never pipe a\n"
261
+ "check into tail or grep, never `|| true`, never `--no-verify`, never a\n"
262
+ "new skip. Set the shell timeout above 300 s for a long run. Report each\n"
263
+ "check as passed or failed with its counts and the exact failure text.\n"
264
+ "Never edit source or test files; a fix belongs to the caller."),
265
+ "tezgah-docs": (
266
+ "Writes and repairs docs pages and their `path:line` / `path::symbol` "
267
+ "citations after a code change, under the repo's own docs checks.",
268
+ ("no-ai-slop",), "read, edit and write docs; shell for the docs checks",
269
+ "Edit only documentation files the brief names. Cite code as\n"
270
+ "`path::symbol`; when a code change shifted a `path:line` citation,\n"
271
+ "re-anchor it by whole token (`:87` must never rewrite `:870`), map each\n"
272
+ "changed line to its replacement block, and run the repo's citation and\n"
273
+ "clarity checks (`bin/tezgah-docs --citations`, `--clarity` where they\n"
274
+ "exist) until they report zero. Plain sentences, one fact each."),
275
+ "tezgah-security": (
276
+ "Read-only security review of a change or surface: injection, authn/authz, "
277
+ "secrets, SSRF, deserialisation, supply chain; every finding with a "
278
+ "concrete exploit path and file:line.",
279
+ HOST_SKILLS, "read-only; shell only for read-only git and the codegraph CLI",
280
+ "Pick the skills that match the class under test: `wstg-injection`,\n"
281
+ "`wstg-auth-session`, `wstg-logic-client-api`, `wstg-recon-config`,\n"
282
+ "the matching `attack-*` entry (attack-ssrf, attack-jwt, ...),\n"
283
+ "`ci-assessment` for pipelines, `llm-security` for prompt handling.\n"
284
+ "Trace each untrusted input to its sink. Classify every candidate as\n"
285
+ "confirmed (exploit path + file:line), refuted (the guard that stops it)\n"
286
+ "or unverified. Never write, never run an exploit against a live system,\n"
287
+ "never print a secret you find - name its location only."),
288
+ "tezgah-ui": (
289
+ "Inspects a running web or mobile app and judges it against the design "
290
+ "floor: component x state coverage, layout, contrast and WCAG 2.2, with "
291
+ "the screen it read as evidence.",
292
+ ("analyze-app", "design-contract", "design-library", "feature-audit"),
293
+ "shell and browser/device tools; no source edits",
294
+ "Read the running app, never only its source: a UI claim inferred from\n"
295
+ "code is not evidence. Measure against `.tezgah/design-contract.md` when\n"
296
+ "it exists (`tezgah-design check`). Every finding names the screen,\n"
297
+ "breakpoint and state it came from and a 0-4 severity. Report findings;\n"
298
+ "do not edit source files."),
299
+ "tezgah-researcher": (
300
+ "Runs one research line through OpenResearch: hypotheses, experiments, "
301
+ "literature, a report - the protocol frozen before the run.",
302
+ ("research", "ai-research"),
303
+ "shell for `orx` and `tezgah-research`; writes only under .tezgah/research",
304
+ "Load the OpenResearch manual first (`orx skill`) and follow its\n"
305
+ "experiment-tree rules. Work inside the one line the brief names; never\n"
306
+ "open a second line while one is open (`tezgah-research status`). Freeze\n"
307
+ "the protocol and the criteria before any run; a correction after a run\n"
308
+ "goes in `amendments.md`. Run `tezgah-research check` before reporting.\n"
309
+ "If `orx` is not installed, say so and stop."),
310
+ }
311
+
312
+
313
+ def _specialist_desc(name):
314
+ what, skills, tools, _role = SPECIALISTS[name]
315
+ named = ", ".join(s + (" (host-installed)" if s in HOST_SKILLS else "")
316
+ for s in skills)
317
+ return "%s Skills: %s. Tier: %s (tezgah-route). Tools: %s." % (
318
+ what, named or "none", tm.AGENT_SLOT[name], tools)
319
+
320
+
321
+ def _specialist_body(name):
322
+ _what, skills, _tools, role = SPECIALISTS[name]
323
+
324
+ def body(_host):
325
+ load = ("Load these skills before anything else, those this host has: "
326
+ "%s.\n" % ", ".join("`%s`" % s for s in skills)) if skills else ""
327
+ return ("You are %s, a specialist on the %s model tier. Do exactly the\n"
328
+ "brief: stay inside the files and paths it names (absolute paths\n"
329
+ "under the worktree it gives, never the main checkout), and\n"
330
+ "report what you did and what each command printed, verbatim.\n"
331
+ "%s%s\nIf the brief cannot be met as written, say so and stop.\n"
332
+ "Never spawn subagents." % (name, tm.AGENT_SLOT[name], load, role))
333
+ return body
334
+
335
+
336
+ def _specialist(name, readonly=False):
337
+ return (name, _specialist_desc(name), lambda infra: True,
338
+ _specialist_body(name), readonly)
339
+
340
+
238
341
  # name, description, capability gate, body(host), read-only?
239
342
  ROLES = (
240
343
  ("tezgah-reviewer",
@@ -257,6 +360,11 @@ ROLES = (
257
360
  "unknown-cause debugging and hard-to-reverse changes on the strongest model "
258
361
  "tier; also where an ESCALATE from a cheaper worker is restarted.",
259
362
  lambda infra: True, _worker_body("frontier"), False),
363
+ _specialist("tezgah-tester"),
364
+ _specialist("tezgah-docs"),
365
+ _specialist("tezgah-security", readonly=True),
366
+ _specialist("tezgah-ui"),
367
+ _specialist("tezgah-researcher"),
260
368
  )
261
369
 
262
370
  # The roles gated on the code graph, which a repo's `.no-graph` turns off.
@@ -264,25 +372,38 @@ GRAPH_ROLES = ("tezgah-reviewer",)
264
372
  # Every agent name tezgah ever generated: the live roles, the orchestrator and
265
373
  # the retired roles. omp's files carry no MARKER, so this list - not the
266
374
  # `tezgah-` prefix - is what makes a file there tezgah's to sweep.
267
- RETIRED_ROLES = ("tezgah-explorer", "tezgah-verifier", "tezgah-researcher")
375
+ # tezgah-researcher was retired and came back as the OpenResearch specialist.
376
+ RETIRED_ROLES = ("tezgah-explorer", "tezgah-verifier")
268
377
  OWNED_NAMES = tuple(r[0] for r in ROLES) + ("tezgah-orchestrator",) + RETIRED_ROLES
269
378
 
270
379
  ORCH_DESC = ("Route work in this repo to the generated tezgah-* subagents. The "
271
- "main thread decides and verifies; delegate bounded, well-specified "
272
- "work and never let a subagent orchestrate another.")
380
+ "main thread decides and verifies; two or more independent items "
381
+ "fan out to parallel subagents in one message, one git worktree per "
382
+ "writing slice, and no subagent orchestrates another.")
273
383
 
274
384
 
275
385
  def _orch_body(names):
276
386
  listed = ", ".join(names) if names else "(none active yet)"
277
387
  return (
278
388
  "You are the tezgah orchestrator for this repository. You decide and\n"
279
- "verify; you delegate bounded, well-specified work and read the evidence\n"
280
- "back. Available specialists: %s.\n\n"
389
+ "verify; you delegate execution and read the evidence back. Available\n"
390
+ "specialists: %s.\n\n"
391
+ "Fan out by default: when the request or your todo list holds two or more\n"
392
+ "independent items, spawn one subagent per item in ONE message. Give\n"
393
+ "every writing slice its own branch and checkout - `orca worktree create\n"
394
+ "--name <slug> --parent-worktree active --json` when the session runs\n"
395
+ "inside Orca, else `git worktree add -b <branch> <absolute path>` - and\n"
396
+ "brief it with absolute paths under that checkout only, never the main\n"
397
+ "checkout. Serialize only a real\n"
398
+ "dependency, and never wait idle while independent work remains; you\n"
399
+ "integrate the branches and run the checks once on the merged tree.\n\n"
281
400
  "Route reviews to tezgah-reviewer, with the changed files and symbols in\n"
282
- "the brief: where it has no shell it cannot run `git diff` itself. Brief a\n"
283
- "code-discovery task to a tier worker with the codegraph tools; a second\n"
284
- "opinion (`consult`) and research (`orx`) stay with the main thread. Other\n"
285
- "work goes to the tier worker\n"
401
+ "the brief: where it has no shell it cannot run `git diff` itself. Checks\n"
402
+ "go to tezgah-tester, docs and citations to tezgah-docs, a security review\n"
403
+ "to tezgah-security, a running app to tezgah-ui, a research line to\n"
404
+ "tezgah-researcher. Brief a code-discovery task to a tier worker with the\n"
405
+ "codegraph tools; a second opinion (`consult`) stays with the main\n"
406
+ "thread. Other work goes to the tier worker\n"
286
407
  "`tezgah-route \"<brief>\"` names (tezgah-cheap, tezgah-standard,\n"
287
408
  "tezgah-frontier); an ESCALATE answer is restarted on tezgah-frontier with\n"
288
409
  "the same brief. Never delegate a task a specialist is not\n"
@@ -407,10 +528,11 @@ def omp_user_agents(root="~"):
407
528
  names = [r[0] for r in active]
408
529
  if not names:
409
530
  return {}
531
+ # A role that writes gets every tool the session has; a read-only one gets
532
+ # the read tools plus the shell its body bounds to read-only commands.
410
533
  out = {name + ".md": render_md_omp(
411
- name, desc, body("omp"),
412
- **({"tools": None} if name[len("tezgah-"):] in tm.TIERS else {}))
413
- for name, desc, _cap, body, _readonly in active}
534
+ name, desc, body("omp"), **({} if readonly else {"tools": None}))
535
+ for name, desc, _cap, body, readonly in active}
414
536
  out["tezgah-orchestrator.md"] = render_md_omp(
415
537
  "tezgah-orchestrator", ORCH_DESC, _orch_body(names),
416
538
  tools=("read", "grep", "glob", "bash", "task"))
@@ -114,6 +114,9 @@ COMPONENTS = [
114
114
  {"key": "workspace", "label": "workspace",
115
115
  "files": ["hooks/tezgah_policy.py", "hooks/tezgah_gate.py"],
116
116
  "switch": "workspace-off", "editable": False, "revert": "git"},
117
+ {"key": "fanout", "label": "fanout",
118
+ "files": ["hooks/tezgah_policy.py", "hooks/tezgah_agents.py"],
119
+ "switch": "orchestrate-off", "editable": True, "revert": "git"},
117
120
  {"key": "skills", "label": "skills",
118
121
  "files": ["skills/"],
119
122
  "switch": "none", "editable": True, "revert": "git"},