@r1z4x/tezgah 1.5.0 → 1.5.1
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.
- package/CHANGELOG.md +50 -1
- package/MANIFEST +0 -1
- package/README.md +6 -4
- package/bin/tezgah-docs +1 -1
- package/bin/tezgah-route +8 -5
- package/bin/tezgah-setup +44 -24
- package/bin/tezgah-task +2 -2
- package/docs/architecture.md +1 -1
- package/docs/clarity-baseline.json +1 -1
- package/docs/hosts.md +9 -0
- package/docs/judge.md +3 -1
- package/docs/layers.md +1 -1
- package/docs/models.md +17 -7
- package/docs/operations.md +0 -1
- package/docs/testing.md +1 -1
- package/hooks/tezgah_agents.py +127 -94
- package/hooks/tezgah_context.py +2 -1
- package/hooks/tezgah_judge.py +15 -8
- package/hooks/tezgah_models.py +50 -3
- package/hooks/tezgah_paths.py +26 -11
- package/hooks/tezgah_policy.py +9 -5
- package/hooks/tezgah_task.py +14 -6
- package/package.json +1 -2
- package/skills/tezgah-contract/SKILL.md +4 -2
- package/agents/tezgah-reviewer.md +0 -79
package/CHANGELOG.md
CHANGED
|
@@ -6,6 +6,54 @@ adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
|
6
6
|
|
|
7
7
|
## [Unreleased]
|
|
8
8
|
|
|
9
|
+
## [1.5.1] - 2026-10-09
|
|
10
|
+
|
|
11
|
+
### Fixed
|
|
12
|
+
|
|
13
|
+
- **Subagents load in the first session.** Claude and Codex missed every role a
|
|
14
|
+
session wrote into a new repo until the next session. Codex also ignored
|
|
15
|
+
`.codex/agents` in an untrusted repo. The install now writes the roles to
|
|
16
|
+
`~/.claude/agents` and `$CODEX_HOME/agents`, as it already did for omp. A
|
|
17
|
+
session removes the copies an older release left in a repo's `.claude/agents`
|
|
18
|
+
and `.codex/agents`. Cursor read 9 of 10 role descriptions as ">" and got a
|
|
19
|
+
model id it does not list. It now gets its own `.cursor/agents` with a
|
|
20
|
+
one-line description and `model: inherit`.
|
|
21
|
+
|
|
22
|
+
- **`tezgah-route` names the specialists.** It could only name a tier. The
|
|
23
|
+
five specialists ran 0 times in 121 delegations. The generic task agent ran 86.
|
|
24
|
+
The judge now also answers whether a brief is a specialist's whole
|
|
25
|
+
job, and the route names that agent. `--phase review` and `--phase research`
|
|
26
|
+
name the reviewer and the researcher. A security brief that only reviews
|
|
27
|
+
goes to `tezgah-security`. The routing and fan-out text say to spawn the
|
|
28
|
+
named agent, never the generic one.
|
|
29
|
+
- **Jev through OpenRouter asks a model OpenRouter has.** The carrier sent
|
|
30
|
+
`typesafe/jev-latest`, which OpenRouter answers with 400 (no such model). It
|
|
31
|
+
now sends the bare `jev-latest`, which OpenRouter maps itself.
|
|
32
|
+
- **The Workers AI model id picks Clef.** `tezgah-setup` refused
|
|
33
|
+
`--jev @cf/cloudflare/clef-flash`, and the same value in
|
|
34
|
+
`TEZGAH_JEV_PROVIDER` chose no carrier. Both now name `clef-flash`, and
|
|
35
|
+
`@cf/cloudflare/clef` names `clef`.
|
|
36
|
+
- **opencode's judge run denies tools at agent level too.** A user's
|
|
37
|
+
`agent.build.permission` beat the inline top-level deny.
|
|
38
|
+
- **The refusal note needs an asked session CLI.** The seam writes it only
|
|
39
|
+
when it asked the session CLI first. A caller that named the CLI gets it.
|
|
40
|
+
- **A chosen carrier reads only its own key.** A `clef` choice no longer opens
|
|
41
|
+
omp's login database on every status-line redraw.
|
|
42
|
+
- **`pnpm test` names a command.** An Acceptance item counted as a command
|
|
43
|
+
only when a word after the first was an option or a path. So `pnpm test`,
|
|
44
|
+
`cargo test` and `make test` read as missing, and the phase move refused the
|
|
45
|
+
plan. A known runner followed by `test` or `run` now counts. Prose such as
|
|
46
|
+
`run the tests` still does not.
|
|
47
|
+
- **Each carrier's price is now pinned by a test.** The one cost check priced a
|
|
48
|
+
TypeSafe answer at Jev's rate. It passed with or without per-carrier pricing.
|
|
49
|
+
New tests in `tezgah-taste`, `tezgah-triage` and the replay labeller price Clef
|
|
50
|
+
and Clef-flash answers at their own rates. A run with an unpriced answer prints
|
|
51
|
+
`cost unknown`.
|
|
52
|
+
- **Claude lists the reviewer once.** The plugin shipped its own
|
|
53
|
+
`agents/tezgah-reviewer.md`, which Claude shows as `tezgah:tezgah-reviewer`
|
|
54
|
+
beside the generated reviewer and never shadows. The plugin now ships no
|
|
55
|
+
agent, and its release-time renderer is gone with it.
|
|
56
|
+
|
|
9
57
|
## [1.5.0] - 2026-10-09
|
|
10
58
|
|
|
11
59
|
### Changed
|
|
@@ -933,7 +981,8 @@ adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
|
933
981
|
the copy could not find `hooks/` and died on import. It now links into the
|
|
934
982
|
installed tree, and also installs the command as `tezgah`.
|
|
935
983
|
|
|
936
|
-
[Unreleased]: https://github.com/r1z4x/tezgah/compare/v1.5.
|
|
984
|
+
[Unreleased]: https://github.com/r1z4x/tezgah/compare/v1.5.1...HEAD
|
|
985
|
+
[1.5.1]: https://github.com/r1z4x/tezgah/releases/tag/v1.5.1
|
|
937
986
|
[1.5.0]: https://github.com/r1z4x/tezgah/releases/tag/v1.5.0
|
|
938
987
|
[1.4.0]: https://github.com/r1z4x/tezgah/releases/tag/v1.4.0
|
|
939
988
|
[1.3.0]: https://github.com/r1z4x/tezgah/releases/tag/v1.3.0
|
package/MANIFEST
CHANGED
package/README.md
CHANGED
|
@@ -109,10 +109,12 @@ it set out to arm is not armed.
|
|
|
109
109
|
`~/.codex/AGENTS.md`, `~/.omp/agent/RULES.md`). Claude Code's hooks ship in a
|
|
110
110
|
plugin registered through `claude plugin`; with no `claude` CLI, Claude stays
|
|
111
111
|
unarmed and the report says so.
|
|
112
|
-
- **Agent files
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
`.
|
|
112
|
+
- **Agent files.** The install writes tezgah's subagent roles to
|
|
113
|
+
`~/.claude/agents/`, `$CODEX_HOME/agents/` (default `~/.codex/agents/`) and
|
|
114
|
+
`~/.omp/agent/agents/`. Inside the configured roots, a session writes them to
|
|
115
|
+
the repository's `.cursor/agents/` and `.opencode/agents/`, and lists those
|
|
116
|
+
directories in the clone's own `.git/info/exclude` (`TEZGAH_NO_EXCLUDE=1`
|
|
117
|
+
stops that).
|
|
116
118
|
- **A daily update check.** The status line asks for a newer release at most
|
|
117
119
|
once a day; `~/.config/tezgah/update-check-off` or `TEZGAH_UPDATE_CHECK=0`
|
|
118
120
|
turns it off.
|
package/bin/tezgah-docs
CHANGED
|
@@ -106,7 +106,7 @@ RUN = re.compile(r"((?:`[A-Za-z_][A-Za-z0-9_]*(?:\(\))?`\s*[/,]?\s*){1,5})$")
|
|
|
106
106
|
CITE = re.compile(r"`([\w.-]+(?:/[\w.-]+)+|[\w-]+\.\w+):(\d+)(?:-(\d+))?`"
|
|
107
107
|
r"|`(:(\d+)(?:-(\d+))?)`")
|
|
108
108
|
# The symbol written after the citation instead of before it, the way a table
|
|
109
|
-
# cell reads: `hooks/tezgah_policy.py:
|
|
109
|
+
# cell reads: `hooks/tezgah_policy.py:680-948` (`CORE`).
|
|
110
110
|
AFTER = re.compile(r"\s*\(`([A-Za-z_][A-Za-z0-9_]*)(?:\(\))?`\)")
|
|
111
111
|
# Any `path:line`, backticked or not: what CITE does not match of these (a
|
|
112
112
|
# bare token, a comma list `x.py:12,40`) is counted as unjudged, so a citation
|
package/bin/tezgah-route
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
#!/usr/bin/env python3
|
|
2
|
-
"""tezgah-route - which
|
|
2
|
+
"""tezgah-route - which worker a delegated task goes to, and the model table.
|
|
3
3
|
|
|
4
4
|
tezgah-route "<brief>" [--phase P] [--json] the agent to spawn, and why
|
|
5
5
|
tezgah-route --check the table's age and refresh flags
|
|
@@ -9,10 +9,13 @@
|
|
|
9
9
|
tezgah-route --report each route's first check, by tier
|
|
10
10
|
|
|
11
11
|
A brief naming stored data, a migration, credentials, the gate or security goes to
|
|
12
|
-
tezgah-frontier by rule
|
|
13
|
-
tokens, ~0.4 s)
|
|
14
|
-
|
|
15
|
-
|
|
12
|
+
tezgah-frontier by rule - or to tezgah-security when it only reviews; otherwise
|
|
13
|
+
Jev answers two Choices in one call (~700 input tokens, ~0.4 s): the tier, and
|
|
14
|
+
whether the brief is the whole job of a specialist (tezgah-tester, -docs,
|
|
15
|
+
-security, -ui, -researcher, -reviewer), which is then the agent named. With no
|
|
16
|
+
key or `judge-off`, `--phase review|research` names the reviewer or the
|
|
17
|
+
researcher, another `--phase` (explore|code|mechanical|plan) picks the tier from
|
|
18
|
+
the static table, and with neither the middle tier is used. The brief is sent to the judge seam's
|
|
16
19
|
provider - the session's own CLI (omp, claude, opencode, cursor, codex) or the Jev
|
|
17
20
|
model through TypeSafe, OpenRouter System One, Cloudflare Workers AI or a
|
|
18
21
|
compatible endpoint (TEZGAH_JEV_PROVIDER picks one), else the OpenRouter chat
|
package/bin/tezgah-setup
CHANGED
|
@@ -13,7 +13,6 @@
|
|
|
13
13
|
tezgah-setup --adopt move the predecessor wiring aside
|
|
14
14
|
tezgah-setup --sync dev: copy this checkout over the Claude plugin copy
|
|
15
15
|
tezgah-setup --write-manifest release: regenerate the tracked MANIFEST from git
|
|
16
|
-
tezgah-setup --write-plugin-agents release: re-render the plugin's agents/ from the role source
|
|
17
16
|
tezgah-setup --status [PATH] print the armed/used checklist
|
|
18
17
|
tezgah-setup --agents [PATH] regenerate this repo's subagent set
|
|
19
18
|
tezgah-setup --deps [--dry-run] install missing optional tools only
|
|
@@ -1163,9 +1162,33 @@ def install_claude():
|
|
|
1163
1162
|
# (`TEZGAH_CORE_IN_FILE`, hooks/projects-auto-init.py), so a session pays for
|
|
1164
1163
|
# the contract once.
|
|
1165
1164
|
print("%s %s (always-on contract)" % (OK, write_rules("claude")))
|
|
1165
|
+
write_user_agents("claude")
|
|
1166
1166
|
register_claude_plugin()
|
|
1167
1167
|
|
|
1168
1168
|
|
|
1169
|
+
def write_user_agents(host):
|
|
1170
|
+
"""The host's tezgah roles in its user agent dir, where it loads them from
|
|
1171
|
+
the first session (a repo dir written at session start is missed there),
|
|
1172
|
+
then tezgah's MARKER files no longer wanted - `agents-off`, a lost
|
|
1173
|
+
capability, a retired role - are removed. The user's own files stay."""
|
|
1174
|
+
agents_dir = tezgah_agents.user_agent_dir(host)
|
|
1175
|
+
wanted = tezgah_agents.user_agents(host)
|
|
1176
|
+
for name, text in sorted(wanted.items()):
|
|
1177
|
+
path = os.path.join(agents_dir, name)
|
|
1178
|
+
if write_if_changed(path, text):
|
|
1179
|
+
print("%s %s" % (OK, path))
|
|
1180
|
+
for path in tezgah_agents.remove_stale(agents_dir, wanted):
|
|
1181
|
+
print("%s removed %s" % (OK, path))
|
|
1182
|
+
|
|
1183
|
+
|
|
1184
|
+
def user_agent_files(host):
|
|
1185
|
+
"""{filename: text} of tezgah's MARKER files in the host's user agent dir."""
|
|
1186
|
+
d = tezgah_agents.user_agent_dir(host)
|
|
1187
|
+
return {n: t for n in (os.listdir(d) if os.path.isdir(d) else [])
|
|
1188
|
+
if n.startswith("tezgah-")
|
|
1189
|
+
and tezgah_agents.MARKER in (t := tezgah_agents._read(os.path.join(d, n)) or "")}
|
|
1190
|
+
|
|
1191
|
+
|
|
1169
1192
|
# The marketplace a manifest rendered by this installer carries, so Claude
|
|
1170
1193
|
# installs the plugin as `tezgah@tezgah-local`. A manifest the tree already has
|
|
1171
1194
|
# under another name (the maintainer's) is kept, name included.
|
|
@@ -1358,6 +1381,7 @@ def install_codex():
|
|
|
1358
1381
|
# instructions Codex keeps for the whole session. Global, like omp's
|
|
1359
1382
|
# RULES.md, because a per-project file would be a tracked project file.
|
|
1360
1383
|
print("%s %s (always-on contract)" % (OK, write_rules("codex")))
|
|
1384
|
+
write_user_agents("codex")
|
|
1361
1385
|
|
|
1362
1386
|
|
|
1363
1387
|
def _toml_str(value):
|
|
@@ -3293,6 +3317,9 @@ def verify_uninstall(hosts, full):
|
|
|
3293
3317
|
# the user's own CLAUDE.md may name tezgah; only the block is ours
|
|
3294
3318
|
clean("claude global CLAUDE.md block gone", [CLAUDE_RULES],
|
|
3295
3319
|
needle="<!-- tezgah:start")
|
|
3320
|
+
claim("claude subagents gone",
|
|
3321
|
+
[os.path.join(tezgah_agents.user_agent_dir("claude"), n)
|
|
3322
|
+
for n in user_agent_files("claude")])
|
|
3296
3323
|
if "codex" in hosts:
|
|
3297
3324
|
clean("codex hooks.json unwired", [os.path.join(CODEX, "hooks.json")])
|
|
3298
3325
|
claim("codex skills unlinked",
|
|
@@ -3304,6 +3331,9 @@ def verify_uninstall(hosts, full):
|
|
|
3304
3331
|
clean("codex global AGENTS.md block gone",
|
|
3305
3332
|
[os.path.join(CODEX, n) for n in CODEX_RULES],
|
|
3306
3333
|
needle="<!-- tezgah:start")
|
|
3334
|
+
claim("codex subagents gone",
|
|
3335
|
+
[os.path.join(tezgah_agents.user_agent_dir("codex"), n)
|
|
3336
|
+
for n in user_agent_files("codex")])
|
|
3307
3337
|
if "opencode" in hosts:
|
|
3308
3338
|
claim("opencode plugin unlinked",
|
|
3309
3339
|
[os.path.join(OPENCODE, d, "tezgah.js") for d in ("plugins", "plugin")]
|
|
@@ -3454,6 +3484,13 @@ def uninstall_claude():
|
|
|
3454
3484
|
unwire_claude_settings()
|
|
3455
3485
|
drop_claude_mcp_row()
|
|
3456
3486
|
strip_managed_md(CLAUDE_RULES)
|
|
3487
|
+
remove_user_agents("claude")
|
|
3488
|
+
|
|
3489
|
+
|
|
3490
|
+
def remove_user_agents(host):
|
|
3491
|
+
"""Every tezgah MARKER file in the host's user agent dir; the user's stay."""
|
|
3492
|
+
for path in tezgah_agents.remove_stale(tezgah_agents.user_agent_dir(host), ()):
|
|
3493
|
+
print("%s removed %s" % (OK, path))
|
|
3457
3494
|
|
|
3458
3495
|
|
|
3459
3496
|
def drop_claude_mcp_row():
|
|
@@ -3488,6 +3525,7 @@ def uninstall_codex():
|
|
|
3488
3525
|
# both names: the override may have appeared, or gone, since the install
|
|
3489
3526
|
for name in CODEX_RULES:
|
|
3490
3527
|
strip_managed_md(os.path.join(CODEX, name))
|
|
3528
|
+
remove_user_agents("codex")
|
|
3491
3529
|
|
|
3492
3530
|
|
|
3493
3531
|
def uninstall_opencode():
|
|
@@ -4429,6 +4467,8 @@ def host_checks_claude():
|
|
|
4429
4467
|
bool(copies) and all(plugin_copy_current(t) for t in copies)),
|
|
4430
4468
|
("CLAUDE.md carries the contract (matches a fresh render)",
|
|
4431
4469
|
rules_current("claude")),
|
|
4470
|
+
("subagents in %s (match a fresh render)" % tezgah_agents.user_agent_dir("claude"),
|
|
4471
|
+
user_agent_files("claude") == tezgah_agents.user_agents("claude")),
|
|
4432
4472
|
] + claude_mcp_rows(copies)
|
|
4433
4473
|
|
|
4434
4474
|
|
|
@@ -4617,6 +4657,8 @@ def host_checks_codex():
|
|
|
4617
4657
|
("MCP in config.toml", "mcp_servers.codegraph" in toml),
|
|
4618
4658
|
("%s carries the contract (matches a fresh render)"
|
|
4619
4659
|
% os.path.basename(codex_rules()), rules_current("codex")),
|
|
4660
|
+
("subagents in %s (match a fresh render)" % tezgah_agents.user_agent_dir("codex"),
|
|
4661
|
+
user_agent_files("codex") == tezgah_agents.user_agents("codex")),
|
|
4620
4662
|
] + mcp_rows(toml_servers(toml_path), "config.toml") + [
|
|
4621
4663
|
mcp_row(TEZGAH_MCP_NAME, toml_mcp_entry(toml_path, TEZGAH_MCP_NAME),
|
|
4622
4664
|
"config.toml", tezgah_mcp_command()),
|
|
@@ -4929,24 +4971,6 @@ def write_manifest():
|
|
|
4929
4971
|
return 0
|
|
4930
4972
|
|
|
4931
4973
|
|
|
4932
|
-
def write_plugin_agents():
|
|
4933
|
-
"""Re-render the plugin's tracked `agents/*.md` from tezgah_agents' role
|
|
4934
|
-
bodies, the source the per-repo agents are generated from. A plain write,
|
|
4935
|
-
not write_if_changed: a tracked file needs no `.tezgah-bak` beside it."""
|
|
4936
|
-
for name, text in tezgah_agents.plugin_agents().items():
|
|
4937
|
-
path = os.path.join(HERE, "agents", name)
|
|
4938
|
-
try:
|
|
4939
|
-
with open(path, encoding="utf-8") as fh:
|
|
4940
|
-
if fh.read() == text:
|
|
4941
|
-
continue
|
|
4942
|
-
except OSError:
|
|
4943
|
-
pass
|
|
4944
|
-
with open(path, "w", encoding="utf-8") as fh:
|
|
4945
|
-
fh.write(text)
|
|
4946
|
-
print("%s %s" % (OK, path))
|
|
4947
|
-
return 0
|
|
4948
|
-
|
|
4949
|
-
|
|
4950
4974
|
def tree_sha(root, files):
|
|
4951
4975
|
"""sha256 over each relpath and its bytes, or None when one is unreadable."""
|
|
4952
4976
|
h = hashlib.sha256()
|
|
@@ -5647,8 +5671,6 @@ def main(argv):
|
|
|
5647
5671
|
help="regenerate this repo's subagent set (default: cwd)")
|
|
5648
5672
|
ap.add_argument("--write-manifest", action="store_true",
|
|
5649
5673
|
help="release: regenerate the tracked MANIFEST from git")
|
|
5650
|
-
ap.add_argument("--write-plugin-agents", action="store_true",
|
|
5651
|
-
help="release: re-render the plugin's agents/ from the role source")
|
|
5652
5674
|
ap.add_argument("--deps", action="store_true")
|
|
5653
5675
|
ap.add_argument("--mcp-schemas", action="store_true",
|
|
5654
5676
|
help="measure the MCP tool-schema band by asking each server")
|
|
@@ -5695,7 +5717,7 @@ def main(argv):
|
|
|
5695
5717
|
help="with --install: the language replies are asked for and "
|
|
5696
5718
|
"the Stop rule checks (config.json `reply_lang`; `tr` "
|
|
5697
5719
|
"when unset, `any` checks none)")
|
|
5698
|
-
ap.add_argument("--jev", choices=tp.JEV_CHOICES,
|
|
5720
|
+
ap.add_argument("--jev", type=tp.jev_name, choices=tp.JEV_CHOICES,
|
|
5699
5721
|
help="the carrier the judgement seam reaches the Jev model "
|
|
5700
5722
|
"through (config.json `jev`; `auto` tries every one "
|
|
5701
5723
|
"that resolves); TEZGAH_JEV_PROVIDER and JEV_PROVIDER "
|
|
@@ -5763,8 +5785,6 @@ def main(argv):
|
|
|
5763
5785
|
return 0
|
|
5764
5786
|
if args.write_manifest:
|
|
5765
5787
|
return write_manifest()
|
|
5766
|
-
if args.write_plugin_agents:
|
|
5767
|
-
return write_plugin_agents()
|
|
5768
5788
|
if args.status:
|
|
5769
5789
|
print(tezgah_context.health_lines(os.path.realpath(expand(args.status))))
|
|
5770
5790
|
# The install-health half of this report, from the same function the
|
package/bin/tezgah-task
CHANGED
|
@@ -329,8 +329,8 @@ def verification_steps(text, path):
|
|
|
329
329
|
the budget `review` enforces."""
|
|
330
330
|
fields = tt.frontmatter(text)
|
|
331
331
|
ident = fields.get("id") or os.path.basename(path).split("-")[0]
|
|
332
|
-
# every item, not only the ones the command reader recognises:
|
|
333
|
-
#
|
|
332
|
+
# every item, not only the ones the command reader recognises: a bare
|
|
333
|
+
# `pytest` is a command it does not read as one, and must not vanish here
|
|
334
334
|
items = ["%s%s" % (i["text"], "" if i["state"] == "checkable" else
|
|
335
335
|
" [%s]" % ("unverifiable" if i["state"] == "unverifiable"
|
|
336
336
|
else "names no command"))
|
package/docs/architecture.md
CHANGED
|
@@ -34,7 +34,7 @@ nothing about a host's event names or output envelope.
|
|
|
34
34
|
| `tezgah_paths.py` | where tezgah is armed: [roots](glossary.md#root) and the linked worktrees of a repository under one, kill switches, the config dir, and the writable cache dir - the fallback resolved on use rather than at import, so a gated call does not pay for `tempfile`, `shutil` or `sqlite3` | `hooks/tezgah_paths.py:2-14`, `hooks/tezgah_paths.py::cache_dir`, `hooks/tezgah_paths.py::fallback_cache` |
|
|
35
35
|
| `tezgah_snapshot.py` | pre-write bytes of every file a write is about to change, and the one explicit restore | `hooks/tezgah_snapshot.py:2-18`, `hooks/tezgah_snapshot.py::capture` |
|
|
36
36
|
| `tezgah_untrusted.py` | the untrusted-content label on a result from outside the user and workspace, and the taint notice on the next effect | `hooks/tezgah_untrusted.py:2-22`, `hooks/tezgah_untrusted.py::marks` |
|
|
37
|
-
| `tezgah_agents.py` |
|
|
37
|
+
| `tezgah_agents.py` | subagent definitions generated into each host that has an agent surface: user-level for Claude, Codex and omp (written at install), per repo for Cursor and opencode | `hooks/tezgah_agents.py:2-30`, `hooks/tezgah_agents.py::sync_root`, `hooks/tezgah_agents.py::user_agents` |
|
|
38
38
|
| `tezgah_research.py` | the in-repo research workspace and the check that a protocol predates its results | `hooks/tezgah_research.py:2-85`, `hooks/tezgah_research.py::check` |
|
|
39
39
|
| `tezgah_index.py` | the detached graph auto-index worker (flock-guarded, bounded retry) | `hooks/tezgah_index.py:2-18`, `hooks/tezgah_index.py::main` |
|
|
40
40
|
|
package/docs/hosts.md
CHANGED
|
@@ -71,6 +71,8 @@ shared config, contract hash and the CLI symlinks every host shell can call
|
|
|
71
71
|
`~/.claude/settings.json` - `statusLine` (`bin/tezgah-setup::wire_claude_statusline`) and the attribution
|
|
72
72
|
off-switch (`bin/tezgah-setup::wire_claude_attribution`) - and the always-on block in
|
|
73
73
|
`~/.claude/CLAUDE.md` (`CLAUDE_RULES`), Claude's own user-level memory file.
|
|
74
|
+
The subagent roles go to `~/.claude/agents/tezgah-*.md`
|
|
75
|
+
(`bin/tezgah-setup::write_user_agents`).
|
|
74
76
|
Its hook manifest is the plugin's own
|
|
75
77
|
`hooks/hooks.json`; no `hooks.json` is written into `~/.claude`. Claude runs a
|
|
76
78
|
**copy** of the checkout under `~/.claude/plugins/cache`, refreshed by
|
|
@@ -84,6 +86,8 @@ shared config, contract hash and the CLI symlinks every host shell can call
|
|
|
84
86
|
(one group per event, PreToolUse carrying `CODEX_PRETOOL_MATCHER` at `bin/tezgah-setup::CODEX_PRETOOL_MATCHER`),
|
|
85
87
|
`skills/*` symlinks, `~/.codex/bin/consult`, and `mcp_servers.*` tables in
|
|
86
88
|
`config.toml` (`ensure_toml_mcp`, `bin/tezgah-setup::ensure_toml_mcp`).
|
|
89
|
+
The subagent roles go to `$CODEX_HOME/agents/tezgah-*.toml`
|
|
90
|
+
(`bin/tezgah-setup::write_user_agents`).
|
|
87
91
|
- **cursor** - `install_cursor` (`bin/tezgah-setup::install_cursor`): `~/.cursor/hooks.json`
|
|
88
92
|
(tezgah entries replaced, a user's Orca entries merged), `skills/*` symlinks,
|
|
89
93
|
`mcp.json` servers, and `cli-config.json` `statusLine`.
|
|
@@ -159,6 +163,11 @@ and drops the later ones.
|
|
|
159
163
|
| skills | codex | `.agents/skills` from the cwd up to the repo root, `~/.agents/skills`, `$CODEX_HOME/skills`; one name in two dirs is listed twice, not merged | links in `$CODEX_HOME/skills` |
|
|
160
164
|
| skills | cursor | `.agents/skills`, `.cursor/skills`, `~/.agents/skills`, `~/.cursor/skills`, then Claude's and Codex's project and user dirs; no documented winner | links in `~/.cursor/skills` |
|
|
161
165
|
| skills | opencode | its own skill tool is denied; the router lists `~/.config/opencode/skills`, `~/.claude/skills`, `~/.agents/skills`, first wins (`bin/tezgah-setup::skill_groups`) | links and the router |
|
|
166
|
+
| agents | claude | managed, the `claude --agents` flag, `.claude/agents`, `~/.claude/agents`, plugin agents namespaced `plugin:name`; a dir absent at start is not watched | `~/.claude/agents`, written at install |
|
|
167
|
+
| agents | codex | `$CODEX_HOME/agents`, `/etc/codex/agents`, `.codex/agents` (trusted repos only), read at config load | `$CODEX_HOME/agents`, written at install |
|
|
168
|
+
| agents | cursor | `cursor-agent`: `.cursor/agents`, then `.claude/agents`, `.grok/agents`, plugin agents; first name wins; no user dir; read at process start. The editor's docs add `~/.cursor/agents` and `~/.claude/agents` | `.cursor/agents` per repo at session start, in the CLI's line format (`hooks/tezgah_agents.py::render_md_cursor`) |
|
|
169
|
+
| agents | opencode | `~/.config/opencode/agents`, `.opencode/agents`, merged per field | `config` hook injection, `.opencode/agents` as fallback |
|
|
170
|
+
| agents | omp | `.omp/agents`, `~/.omp/agent/agents`, extensions, bundled | `~/.omp/agent/agents`, written at install |
|
|
162
171
|
| hooks | claude | user `settings.json` rows (Orca's), project rows, the plugin's `hooks/hooks.json` (tezgah's); every matching row runs, none is ordered before another | plugin rows |
|
|
163
172
|
| hooks | codex, cursor | `hooks.json` array order, then the repo's `.codex/` or `.cursor/hooks.json`; Codex runs a row only once its hash is trusted | own rows after Orca's |
|
|
164
173
|
| hooks | opencode | every `*.js`/`*.ts` in `plugins/` and `plugin/`; `tezgah.js` is linked into both, and its `globalThis.__tezgahPluginLoaded` guard makes the second load return `{}` | the plugin |
|
package/docs/judge.md
CHANGED
|
@@ -138,7 +138,7 @@ shape, so a caller that needs a typed answer takes them like Jev.
|
|
|
138
138
|
| Carrier | Endpoint | Credential (env, else file, stripped) | Body |
|
|
139
139
|
|---|---|---|---|
|
|
140
140
|
| `typesafe` | `https://api.typesafe.ai/v1/systemone` | `TYPESAFE_API_KEY`, else `~/.config/typesafe/key`, else the active omp login (`~/.omp/agent/agent.db`, read-only; `typesafe_key`, `hooks/tezgah_paths.py::typesafe_key`) | `{state, model, questions}` |
|
|
141
|
-
| `jev-openrouter` | `https://openrouter.ai/api/v1/systemone` | `OPENROUTER_API_KEY`, else `~/.config/openrouter/key` | the same
|
|
141
|
+
| `jev-openrouter` | `https://openrouter.ai/api/v1/systemone` | `OPENROUTER_API_KEY`, else `~/.config/openrouter/key` | the same; a bare `jev-*` id is sent as it is, and OpenRouter maps it onto `typesafe/` |
|
|
142
142
|
| `jev-cloudflare` | `https://api.cloudflare.com/client/v4/accounts/<id>/ai/run` | `JEV_CLOUDFLARE_API_TOKEN`, else `CLOUDFLARE_API_TOKEN`, else `~/.config/cloudflare/token`; the account id from `CLOUDFLARE_ACCOUNT_ID`, else `~/.config/cloudflare/account_id` | `{model: "typesafe/jev", input: {state, questions}}`; the reply is read bare or inside `result` |
|
|
143
143
|
| `jev-compatible` | `JEV_API_BASE_URL`, used verbatim | `JEV_API_KEY` | TypeSafe's, the model from `TEZGAH_JEV_MODEL` if set |
|
|
144
144
|
| `clef`, `clef-flash` | `https://api.cloudflare.com/client/v4/accounts/<id>/ai/run/@cf/cloudflare/<name>` | the Cloudflare token and account id above | `{model: "<name>", state, questions}`; the reply read like Cloudflare's above |
|
|
@@ -148,6 +148,8 @@ reads, so one export serves both. `TEZGAH_JEV_PROVIDER` chooses the carrier,
|
|
|
148
148
|
else `JEV_PROVIDER`, else `jev` in `~/.config/tezgah/config.json`, else `auto`
|
|
149
149
|
(`jev_choice`, `hooks/tezgah_paths.py::jev_choice`). The values are `auto`,
|
|
150
150
|
`typesafe`, `openrouter`, `cloudflare`, `compatible`, `clef` and `clef-flash`.
|
|
151
|
+
The Workers AI ids `@cf/cloudflare/clef` and `@cf/cloudflare/clef-flash` name
|
|
152
|
+
the same two (`jev_name`, `hooks/tezgah_paths.py::jev_name`).
|
|
151
153
|
`tezgah-setup --jev <value>` writes the config key
|
|
152
154
|
(`set_jev`, `bin/tezgah-setup::set_jev`). `auto`
|
|
153
155
|
asks every carrier that resolves, in the table's order (`jev_carriers`,
|
package/docs/layers.md
CHANGED
|
@@ -76,7 +76,7 @@ lessons and research lines.
|
|
|
76
76
|
Must not own the loop, the model, the tools or the network. tezgah never runs
|
|
77
77
|
the work and never rolls back on its own - rollback is the explicit
|
|
78
78
|
`bin/tezgah-rollback` - and the subagents it defines are rendered into a host's
|
|
79
|
-
own agent surface for that host to run (`hooks/tezgah_agents.py:2-
|
|
79
|
+
own agent surface for that host to run (`hooks/tezgah_agents.py:2-30`).
|
|
80
80
|
|
|
81
81
|
## The boundary, one term at a time
|
|
82
82
|
|
package/docs/models.md
CHANGED
|
@@ -59,21 +59,21 @@ invoices).
|
|
|
59
59
|
| Host | Family | Where the model lands |
|
|
60
60
|
|---|---|---|
|
|
61
61
|
| Claude Code | anthropic | the agent file's alias and `effort:` (`_model_lines`): an alias follows the provider and keeps the main session's variant, where a full id breaks on Bedrock, Vertex and a gateway (code.claude.com/docs/en/sub-agents) |
|
|
62
|
-
| Codex | openai | `model` and `model_reasoning_effort` in
|
|
62
|
+
| Codex | openai | `model` and `model_reasoning_effort` in `$CODEX_HOME/agents/*.toml` |
|
|
63
63
|
| opencode | any | a selector `--refresh` found in `opencode models` for the slot's model under any configured provider (`resolve_opencode`); none found, no model line, so the agent inherits. The slot's effort rides the JSON agent config as `reasoningEffort` (opencode.ai/docs/agents) |
|
|
64
64
|
| omp | anthropic when the session default is an Anthropic model, any otherwise (`omp_mode`); a non-Anthropic default is `any`, never a silent fall back to the Anthropic column | `task.agentModelOverrides`, which omp resolves before the agent or bundled model (omp's own docs: `omp://task-agent-discovery.md`, *Model and structured-output precedence*); every slot is written, the frontier row included, so no agent in the row runs on the session default; each override is a **fallback chain** (`omp_overrides`, `_chain`): the mode's family first, then what `fallback` in `~/.config/tezgah/config.json` allows (`tp.fallback_policy`) over the families this machine holds a credential for (`funded_families` reads omp's auth store and the provider env keys; a family it cannot see is left out, never guessed in) - `vendor`, the default, keeps only the session vendor's model (`openrouter/anthropic/claude-opus-5.5:<effort>` behind an Anthropic session, `OPENROUTER_ID`), so a 429 never moves an Opus session's subagent onto GPT or GLM; `any` adds every other funded family, rotated per agent; `none` writes the first entry alone - omp splits the chain into per-spawn patterns (omp's own docs: `omp://settings.md (its `retry.fallbackChains` section)`), so one provider's 429 falls through instead of killing the spawn, measured before this: 24 of 26 post-routing 429s hit a subagent's first request, all on one Opus account every slot shared; `apply_omp` also writes `modelRoles.plan` and `.slow` on that frontier row (same record, same ownership) so plan mode designs on it, and their fallbacks go to `retry.fallbackChains.<role>` under the same `fallback` rule, the only chain keys tezgah owns (`default` and provider-keyed chains stay the user's); `apply_omp` writes through the installer's own binary and agent dir, from HOME so a repository's `.omp/config.yml` never leaks machine-wide, and touches only the entries tezgah wrote (recorded in `~/.config/tezgah/models.json` as `omp_written`; a machine written before that record is adopted by its value, `_ours_by_shape`); a changed `fallback` reaches omp on the next `tezgah-route --refresh` or `tezgah-setup --install` |
|
|
65
65
|
| dsh | - | none: no model setting was found to write |
|
|
66
66
|
|
|
67
67
|
The orchestrator agent keeps `model: inherit`: it is the main thread's own agent.
|
|
68
68
|
|
|
69
|
-
| Cursor | - |
|
|
69
|
+
| Cursor | - | `model: inherit` in `.cursor/agents/*.md` (`render_md_cursor`): `cursor-agent` passes the value through as a raw id, an alias such as `opus` is not in its model list, and the effort belongs **inside** the id (`claude-opus-5[effort=high]`, cursor.com/docs/subagents). No id this table names was verified against Cursor's models page, so no tier model is pinned there yet |
|
|
70
70
|
|
|
71
71
|
omp's bundled agents are routed through the same record (omp's own docs: `omp://task-agent-discovery.md`, *Bundled agents*): `scout` on the explore slot, `sonic` on the cheap slot, `task` on the standard slot, and `reviewer`/`security-reviewer` left to inherit, because a review is frontier work. A user's own value for any of them wins (see the ownership rule above). Those two are the gap this leaves: the record carries no entry for them, so a session on a cheap model reviews on that model - routing them is one more `BUNDLED` entry, not decided here.
|
|
72
72
|
|
|
73
73
|
## What is not routed yet
|
|
74
74
|
|
|
75
75
|
- **Claude Code's built-in subagents** (Explore, general-purpose, Plan) run on the session model. `CLAUDE_CODE_SUBAGENT_MODEL` and a settings `env` block exist but carry no effort, and whether a project agent with a built-in's name replaces it is not stated where it can be cited - so nothing is generated on a guess. What would close it: a documented per-agent effort for built-ins, or a confirmed name-collision rule.
|
|
76
|
-
- **Codex's default subagent** (spawned with no agent file): only
|
|
76
|
+
- **Codex's default subagent** (spawned with no agent file): only `$CODEX_HOME/agents/*.toml` carries a model today. What would close it: a documented `[agents]` default settable from a file tezgah already writes.
|
|
77
77
|
- **opencode's built-in agents**: the injected JSON could carry `model`/`reasoningEffort` for them, but the merge semantics for a built-in entry are not documented where they can be cited, and a wrong merge would replace a built-in prompt instead of adding a model.
|
|
78
78
|
- **dsh**: the model is a plugin config, not an agent field. The composed profile carries `agent-default-model` with `config: {provider: deepseek-official, model: deepseek-flash}` (`dsh --profile web --dump-config`, 2026-09-30) and tezgah's managed patch block already patches plugin config by id (its `llm-pi-ai` row), so a `- id: agent-default-model` row would set the profile's model. Not written, because that knob is one model for the whole session - no `subagent*` entry in the composed tree carries a model - and a single session-wide model is the main thread's setting, which this design deliberately leaves alone ([why the main thread keeps one model](#the-shape)). What would close it: a per-agent model in dsh's config surface.
|
|
79
79
|
|
|
@@ -91,7 +91,9 @@ fixed:
|
|
|
91
91
|
(`OVERRIDE`, `hooks/tezgah_models.py::OVERRIDE`) - the class the judge
|
|
92
92
|
under-routed in its measurement, and the classes a wrong call cannot be
|
|
93
93
|
undone on. A 12-brief fixture pins it: 8 high-stakes briefs route to
|
|
94
|
-
frontier, 4 near-miss controls do not (`tests/test_models.py`).
|
|
94
|
+
frontier, 4 near-miss controls do not (`tests/test_models.py`). Such a brief
|
|
95
|
+
that only reviews goes to `tezgah-security` instead (`REVIEW_ONLY`,
|
|
96
|
+
`hooks/tezgah_models.py::REVIEW_ONLY`).
|
|
95
97
|
2. Otherwise the brief - redacted with the ledger's own reader
|
|
96
98
|
(`redact`, `hooks/tezgah_integrity.py::redact`) - goes to the judge as one Choice over three tiers (`TIER_QUESTION`,
|
|
97
99
|
`hooks/tezgah_models.py::TIER_QUESTION`). The judge asks the session's own model first ([judge](judge.md)).
|
|
@@ -99,9 +101,17 @@ fixed:
|
|
|
99
101
|
the same session that wrote the rubric (2026-09-30, twice): under-route 0.025,
|
|
100
102
|
accuracy 0.925 and 0.900, 392 ms median, about 656 input tokens per call; a
|
|
101
103
|
keyword rule on the same set under-routed 0.100. The session model was not measured on that set.
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
104
|
+
The same call asks a second Choice (`ROLE_QUESTION`,
|
|
105
|
+
`hooks/tezgah_models.py::ROLE_QUESTION`): is the brief the whole job of a
|
|
106
|
+
specialist? A yes names that agent on its own tier (`AGENT_SLOT`,
|
|
107
|
+
`hooks/tezgah_models.py::AGENT_SLOT`). Before this question, sessions
|
|
108
|
+
spawned the five specialists 0 times in 121 delegations (2026-10-07 to 10-09).
|
|
109
|
+
Clef agreed on 9 of 10 briefs this session labelled (2026-10-09).
|
|
110
|
+
3. With no judgement (`judge-off`, no key, a failed call) `--phase review` and
|
|
111
|
+
`--phase research` name the reviewer and the researcher (`PHASE_AGENT`,
|
|
112
|
+
`hooks/tezgah_models.py::PHASE_AGENT`). Another phase picks the tier from the
|
|
113
|
+
static table (`PHASE_TIER`, `hooks/tezgah_models.py::PHASE_TIER`). With no
|
|
114
|
+
phase the middle tier is used.
|
|
105
115
|
|
|
106
116
|
The brief leaves the machine for the judge, like every judgement ([judge](judge.md)).
|
|
107
117
|
Every routed call writes one `route` row to the session's ledger. The row names the
|
package/docs/operations.md
CHANGED
|
@@ -28,7 +28,6 @@ before an install is considered.
|
|
|
28
28
|
| `--status [PATH]` | Print the armed/used checklist for PATH (default cwd) and stop — the same line as `bin/tezgah-status` (`bin/tezgah-setup::main`, `hooks/tezgah_context.py::health_lines`). It also prints the report's `config.json hosts (...) match the hosts wired (...)` row when the recorded list and the wiring on disk disagree (`hosts_row`), and nothing when they agree: install health is checked here, not only in a full report. |
|
|
29
29
|
| `--agents [PATH]` | Regenerate PATH's per-repo subagent set (default cwd); outside a [root](glossary.md#root) it prints `no agents generated` (`bin/tezgah-setup::main`). |
|
|
30
30
|
| `--write-manifest` | Regenerate the tracked `MANIFEST` from `git ls-files` and stop — the release step, its only writer, and it says so on a tree with no `.git` rather than writing an empty listing (`bin/tezgah-setup::write_manifest`, `bin/tezgah-setup::plugin_files`). |
|
|
31
|
-
| `--write-plugin-agents` | Re-render the plugin's tracked `agents/*.md` from the role bodies `hooks/tezgah_agents.py` generates per repo, and stop — the release step that keeps the shipped reviewer from drifting from the generated one; `tests/test_agents.py` fails until it has run (`write_plugin_agents`, `plugin_agents`). |
|
|
32
31
|
| `--deps` | Install the missing optional tools and stop; with `--install` the install already does it. Like `--install` and `--enable`, it exits 1 when a requested tool is still missing (`bin/tezgah-setup::main`). |
|
|
33
32
|
| `--host-deps` | With `--install` or `--deps`, also install the missing third-party host CLIs (cursor-agent, pnpm, dsh). They are opt-in because each one is a vendor `curl \| sh` that uninstall does not take back. Every installer runs with a 600 s timeout, each `curl` with `--max-time 300` and each pipeline under `pipefail`, so a stalled or refused download fails instead of hanging or reading as success (audit M-9). |
|
|
34
33
|
| `--mcp-schemas` | Measure the MCP tool-schema band by asking each registered server (`initialize` + `tools/list`) and stop (`bin/tezgah-setup::main`, `bin/tezgah-setup::mcp_schema_report`). |
|
package/docs/testing.md
CHANGED
|
@@ -171,7 +171,7 @@ into the test process would answer about the developer's real machine.
|
|
|
171
171
|
| `tests/_probe_paths.py` | argv op, no payload (`:11`) | `roots`, `root_for`, `off`, `default_root`, `config`, `off_dirs`, `cache_dir`, `which_user`, `orx_bin`, `consult_options`, `have_typesafe_key`, `codegraph_bin` |
|
|
172
172
|
| `tests/_probe_context.py` | JSON on stdin, `{"fn": ...}` (`:4-5`) | `context_for`, `health_lines`, `health_segments`, `record` |
|
|
173
173
|
| `tests/_probe_integrity.py` | JSON on stdin (`:4-6`) | `note`, `note_tool`, `note_turn`, `kinds`, `events`, `prior_calls`, `counters`, `shortcut_command`, `shortcut_edit`, `stop_reason`, `verify_command` |
|
|
174
|
-
| `tests/_probe_agents.py` | JSON on stdin (`:3-4`) | `sync_root`, `opencode_json`, `cleanup`, `detect` |
|
|
174
|
+
| `tests/_probe_agents.py` | JSON on stdin (`:3-4`) | `sync_root`, `opencode_json`, `cleanup`, `detect`, `omp`, `user`, `steering` |
|
|
175
175
|
| `tests/_probe_gate.py` | JSON on stdin (`:4-8`) | the shared gate `decision(tool, input, cwd, session_id)`; `capture_log` plants a stub `tezgah_snapshot` in `sys.modules`, so the snapshot call site is pinned whether or not that module has landed |
|
|
176
176
|
|
|
177
177
|
A probe is invoked through `support.run`/`run_json` with its path constant
|
package/hooks/tezgah_agents.py
CHANGED
|
@@ -1,16 +1,24 @@
|
|
|
1
1
|
#!/usr/bin/env python3
|
|
2
|
-
"""
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
Claude Code
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
2
|
+
"""Subagent definitions, generated from the detected infrastructure.
|
|
3
|
+
|
|
4
|
+
One role set, rendered into every host surface that actually exists, at the
|
|
5
|
+
place each host loads it from:
|
|
6
|
+
|
|
7
|
+
Claude Code `~/.claude/agents/*.md` user-level, written at install
|
|
8
|
+
Codex `$CODEX_HOME/agents/*.toml` user-level, written at install
|
|
9
|
+
omp `~/.omp/agent/agents/*.md` user-level, written at install
|
|
10
|
+
Cursor `.cursor/agents/*.md` per repo, at session start
|
|
11
|
+
opencode `.opencode/agents/*.md` (fallback) + config injection, per repo
|
|
12
|
+
|
|
13
|
+
Claude and Codex miss a repo agents dir written during their first session
|
|
14
|
+
(Claude does not watch a dir that was absent at start; Codex reads roles at
|
|
15
|
+
config load and ignores `.codex/agents` in an untrusted repo), so their roles
|
|
16
|
+
live user-level; a repo's `.claude/agents` and `.codex/agents` files an older
|
|
17
|
+
tezgah wrote are swept. Cursor's CLI reads no user dir, so it keeps a per-repo
|
|
18
|
+
dir. A role is only emitted when the capability it needs is present (the
|
|
19
|
+
reviewer needs the code graph); the three tier workers are emitted in every root,
|
|
20
|
+
because `tezgah-route` names one of them for every delegated task. The per-repo
|
|
21
|
+
set is regenerated when either this manifest
|
|
14
22
|
or the repo's infrastructure changes, so the definitions stay updatable instead
|
|
15
23
|
of drifting. Hosts with no such surface (dsh) get nothing here; the orchestrator
|
|
16
24
|
directive in the injected contract already covers them. The generated dirs are
|
|
@@ -30,16 +38,20 @@ import subprocess
|
|
|
30
38
|
import tezgah_models as tm
|
|
31
39
|
from tezgah_paths import (CONFIG_DIR, HOME, codegraph_bin, host_installed, off,
|
|
32
40
|
root_for)
|
|
41
|
+
from tezgah_paths import HOST_DIRS as HOST_HOMES
|
|
33
42
|
|
|
34
43
|
MARKER = "# tezgah: managed by tezgah-agents; do not edit"
|
|
35
44
|
STATE = os.path.join(CONFIG_DIR, "agents.state.json")
|
|
36
|
-
# hosts with a file-based custom-agent surface
|
|
37
|
-
#
|
|
38
|
-
# both when either is installed; the *detection* of whether a host is installed
|
|
39
|
-
# lives in tezgah_paths.host_installed, not here.
|
|
45
|
+
# hosts with a file-based custom-agent surface; the *detection* of whether a
|
|
46
|
+
# host is installed lives in tezgah_paths.host_installed, not here.
|
|
40
47
|
FILE_HOSTS = ("claude", "opencode", "codex", "cursor")
|
|
48
|
+
# the file hosts whose roles live in their user dir (user_agents), written by
|
|
49
|
+
# the installer; the others get a per-repo dir from sync_root
|
|
50
|
+
USER_HOSTS = ("claude", "codex")
|
|
51
|
+
# the repo-relative agent dirs. Claude's and Codex's are no longer written, only
|
|
52
|
+
# swept: a copy an older tezgah left there would sit beside the user-level one.
|
|
41
53
|
HOST_DIRS = {"claude": os.path.join(".claude", "agents"),
|
|
42
|
-
"cursor": os.path.join(".
|
|
54
|
+
"cursor": os.path.join(".cursor", "agents"),
|
|
43
55
|
"opencode": os.path.join(".opencode", "agents"),
|
|
44
56
|
"codex": os.path.join(".codex", "agents")}
|
|
45
57
|
# Stack markers -> a short hint. Exact commands are left to the repo's own docs
|
|
@@ -61,10 +73,11 @@ MCP_PREFIXES = ("mcp__codegraph__", "mcp__plugin_tezgah_codegraph__")
|
|
|
61
73
|
GRAPH_SELECT = ",".join(p + t for t in GRAPH_MCP for p in MCP_PREFIXES)
|
|
62
74
|
GRAPH_CLI = ("callers", "callees", "impact", "node", "files")
|
|
63
75
|
GRAPH_TOOLS = ", ".join("`codegraph %s`" % verb for verb in GRAPH_CLI)
|
|
64
|
-
# A role with no shell (Claude `disallowedTools` Bash, opencode `bash: deny
|
|
76
|
+
# A role with no shell (Claude `disallowedTools` Bash, opencode `bash: deny`;
|
|
77
|
+
# Cursor's `readonly` documents no shell scope, so it is held to the same recipe)
|
|
65
78
|
# cannot run `git diff` or the CLI, so its blast radius comes from the caller's
|
|
66
79
|
# brief and the MCP impact tool; a role with a shell runs the diff itself.
|
|
67
|
-
NO_SHELL = ("claude", "opencode")
|
|
80
|
+
NO_SHELL = ("claude", "opencode", "cursor")
|
|
68
81
|
# The floor every graph role carries (gortex edge provenance): the index holds
|
|
69
82
|
# only the edges its parser saw, so an absence of callers is a coverage gap
|
|
70
83
|
# until shown otherwise, never a safety verdict.
|
|
@@ -114,7 +127,7 @@ def _graph_howto(host):
|
|
|
114
127
|
"`codegraph status`). Do not call the `xd://mcp__codegraph_*`\n"
|
|
115
128
|
"devices: omp refuses them with \"Concurrent write refused\"\n"
|
|
116
129
|
"while the parent session holds the index." % GRAPH_TOOLS)
|
|
117
|
-
if host
|
|
130
|
+
if host in ("opencode", "cursor"):
|
|
118
131
|
return ("codegraph is registered as an MCP server (%s). You have no\n"
|
|
119
132
|
"shell, so the codegraph CLI is out of reach: use those MCP tools."
|
|
120
133
|
% ", ".join("`%s`" % t for t in GRAPH_MCP))
|
|
@@ -440,10 +453,8 @@ def _claude_model(model):
|
|
|
440
453
|
|
|
441
454
|
def _model_lines(name, family, model_fmt, effort_fmt):
|
|
442
455
|
"""The model (and effort) lines the table gives this agent on a family;
|
|
443
|
-
`model: inherit` on Claude
|
|
444
|
-
|
|
445
|
-
(`claude-opus-5[effort=high]`, cursor.com/docs/subagents) - unverified for
|
|
446
|
-
the ids this table names, so Cursor is documented, not guessed at."""
|
|
456
|
+
`model: inherit` on Claude for an agent with no slot. Cursor gets
|
|
457
|
+
`model: inherit` always (render_md_cursor)."""
|
|
447
458
|
picked = tm.pick(name, family)
|
|
448
459
|
if not picked:
|
|
449
460
|
return ["model: inherit"] if family == "anthropic" else []
|
|
@@ -462,35 +473,34 @@ def _fold(desc, width=74):
|
|
|
462
473
|
|
|
463
474
|
|
|
464
475
|
def render_md(name, description, readonly, body):
|
|
465
|
-
"""Claude Code /
|
|
466
|
-
|
|
476
|
+
"""Claude Code markdown (`~/.claude/agents`), which the Cursor editor also
|
|
477
|
+
reads (cursor.com/docs/subagents). The description is one line: Cursor's
|
|
478
|
+
line parser reads a folded `>` block as ">", and Claude's parser re-quotes
|
|
479
|
+
a plain value YAML rejects (a `: ` inside it). Claude denies the write/shell
|
|
480
|
+
tools explicitly; `readonly` is Cursor's key, which Claude ignores."""
|
|
467
481
|
lines = ["---", MARKER + " (manifest %s)" % manifest_sha(),
|
|
468
|
-
"name: %s" % name, "description:
|
|
469
|
-
lines += _fold(description)
|
|
482
|
+
"name: %s" % name, "description: %s" % " ".join(description.split())]
|
|
470
483
|
lines += _model_lines(name, "anthropic", "model: %s", "effort: %s")
|
|
471
484
|
if readonly:
|
|
472
|
-
# `disallowedTools` is Claude's; `readonly` is Cursor's. Both parsers
|
|
473
|
-
# ignore the other's field.
|
|
474
485
|
lines.append("readonly: true")
|
|
475
486
|
lines.append("disallowedTools: Write, Edit, NotebookEdit, Bash, Agent")
|
|
476
487
|
lines.append("---")
|
|
477
488
|
return "\n".join(lines) + "\n\n" + body.strip() + "\n"
|
|
478
489
|
|
|
479
490
|
|
|
480
|
-
def
|
|
481
|
-
"""
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
return out
|
|
491
|
+
def render_md_cursor(name, description, readonly, body):
|
|
492
|
+
"""Cursor `.cursor/agents/*.md`. cursor-agent splits each frontmatter line
|
|
493
|
+
at its first ':' (no YAML), passes `model` through as a raw id and has no
|
|
494
|
+
`effort` key, so: a one-line description, `model: inherit` (the only id
|
|
495
|
+
portable across Cursor accounts), `readonly`, and no `tools` line - its
|
|
496
|
+
comma split would cut a Claude `Agent(a, b)` list into broken names."""
|
|
497
|
+
lines = ["---", MARKER + " (manifest %s)" % manifest_sha(),
|
|
498
|
+
"name: %s" % name, "description: %s" % " ".join(description.split()),
|
|
499
|
+
"model: inherit"]
|
|
500
|
+
if readonly:
|
|
501
|
+
lines.append("readonly: true")
|
|
502
|
+
lines.append("---")
|
|
503
|
+
return "\n".join(lines) + "\n\n" + body.strip() + "\n"
|
|
494
504
|
|
|
495
505
|
|
|
496
506
|
def render_md_opencode(name, description, readonly, body):
|
|
@@ -529,13 +539,19 @@ def render_md_omp(name, description, body, tools=("read", "grep", "glob", "bash"
|
|
|
529
539
|
return "\n".join(lines) + "\n\n" + body.strip() + "\n"
|
|
530
540
|
|
|
531
541
|
|
|
542
|
+
def _active_roles(root):
|
|
543
|
+
"""The roles a user-level dir holds: none under `agents-off`, else those
|
|
544
|
+
whose capability the machine has (`~/.no-graph` counts as the repo's)."""
|
|
545
|
+
if off("agents-off"):
|
|
546
|
+
return []
|
|
547
|
+
infra = detect_infra(os.path.expanduser(root))
|
|
548
|
+
return [r for r in ROLES if r[2](infra)]
|
|
549
|
+
|
|
550
|
+
|
|
532
551
|
def omp_user_agents(root="~"):
|
|
533
552
|
"""{filename: text} for omp's user agent dir (~/.omp/agent/agents); empty
|
|
534
553
|
under `agents-off`, so the installer's sweep removes the files."""
|
|
535
|
-
|
|
536
|
-
return {}
|
|
537
|
-
infra = detect_infra(os.path.expanduser(root))
|
|
538
|
-
active = [r for r in ROLES if r[2](infra)]
|
|
554
|
+
active = _active_roles(root)
|
|
539
555
|
names = [r[0] for r in active]
|
|
540
556
|
if not names:
|
|
541
557
|
return {}
|
|
@@ -550,20 +566,50 @@ def omp_user_agents(root="~"):
|
|
|
550
566
|
return out
|
|
551
567
|
|
|
552
568
|
|
|
569
|
+
def user_agent_dir(host):
|
|
570
|
+
"""Where a USER_HOSTS host loads agents from in every session: Claude's
|
|
571
|
+
`~/.claude/agents`, Codex's `$CODEX_HOME/agents` (default ~/.codex)."""
|
|
572
|
+
return os.path.join(HOST_HOMES[host], "agents")
|
|
573
|
+
|
|
574
|
+
|
|
575
|
+
def user_agents(host, root="~"):
|
|
576
|
+
"""{filename: text} for a USER_HOSTS host's user agent dir, each file
|
|
577
|
+
carrying MARKER so the installer sweeps only tezgah's; empty under
|
|
578
|
+
`agents-off`, so that sweep removes them. Codex gets no orchestrator: the
|
|
579
|
+
main thread orchestrates there."""
|
|
580
|
+
active = _active_roles(root)
|
|
581
|
+
names = [r[0] for r in active]
|
|
582
|
+
if not names:
|
|
583
|
+
return {}
|
|
584
|
+
if host == "codex":
|
|
585
|
+
return {name + ".toml": render_toml(name, desc, readonly, body("codex"))
|
|
586
|
+
for name, desc, _cap, body, readonly in active}
|
|
587
|
+
out = {name + ".md": render_md(name, desc, readonly, body("claude"))
|
|
588
|
+
for name, desc, _cap, body, readonly in active}
|
|
589
|
+
out["tezgah-orchestrator.md"] = render_orch_md(names)
|
|
590
|
+
return out
|
|
591
|
+
|
|
592
|
+
|
|
553
593
|
def steering(root, host=None):
|
|
554
594
|
"""One line naming the generated specialists this host can spawn, or None.
|
|
555
595
|
|
|
556
596
|
Nothing else in the contract points the main agent at them, so a model
|
|
557
597
|
reached for the host's generic explorer instead. Existence is read from
|
|
558
598
|
disk, not from the capabilities: a specialist is named only when its file
|
|
559
|
-
is there for the host to load -
|
|
560
|
-
|
|
561
|
-
|
|
599
|
+
is there for the host to load - the user agent dir on omp, Claude and
|
|
600
|
+
Codex, the repo's own dir on Cursor and opencode, and all of them for a
|
|
601
|
+
host that does not say which it is. A repo's `.no-graph` turns the graph
|
|
602
|
+
off there, so a graph role is not named in it even when a user-level file
|
|
562
603
|
exists."""
|
|
563
604
|
if host == "omp":
|
|
564
605
|
dirs = [os.path.join(HOME, ".omp", "agent", "agents")]
|
|
606
|
+
elif host in USER_HOSTS:
|
|
607
|
+
dirs = [user_agent_dir(host)]
|
|
608
|
+
elif host in HOST_DIRS:
|
|
609
|
+
dirs = [os.path.join(root, HOST_DIRS[host])]
|
|
565
610
|
else:
|
|
566
|
-
dirs = [
|
|
611
|
+
dirs = ([user_agent_dir(h) for h in USER_HOSTS]
|
|
612
|
+
+ [os.path.join(root, HOST_DIRS[h]) for h in ("cursor", "opencode")])
|
|
567
613
|
have = {os.path.splitext(os.path.basename(p))[0]
|
|
568
614
|
for d in dirs for p in glob.glob(os.path.join(d, "tezgah-*.*"))}
|
|
569
615
|
if os.path.exists(os.path.join(root, ".no-graph")):
|
|
@@ -656,14 +702,18 @@ def _write_if_changed(path, text):
|
|
|
656
702
|
return True
|
|
657
703
|
|
|
658
704
|
|
|
659
|
-
def
|
|
660
|
-
"""Delete only tezgah-managed files no longer in the wanted set
|
|
705
|
+
def remove_stale(directory, wanted):
|
|
706
|
+
"""Delete only tezgah-managed files no longer in the wanted set; returns
|
|
707
|
+
the paths removed."""
|
|
708
|
+
removed = []
|
|
661
709
|
for path in glob.glob(os.path.join(directory, "tezgah-*.*")):
|
|
662
710
|
if os.path.basename(path) not in wanted and _is_managed(_read(path)):
|
|
663
711
|
try:
|
|
664
712
|
os.remove(path)
|
|
713
|
+
removed.append(path)
|
|
665
714
|
except OSError:
|
|
666
715
|
pass
|
|
716
|
+
return removed
|
|
667
717
|
|
|
668
718
|
|
|
669
719
|
BLOCK_BEGIN = ("# tezgah: generated agents (managed; removed by "
|
|
@@ -796,66 +846,49 @@ def sync_root(root, report_steady=False):
|
|
|
796
846
|
return None
|
|
797
847
|
disabled = off("agents-off")
|
|
798
848
|
infra = detect_infra(root)
|
|
799
|
-
|
|
849
|
+
# Claude's and Codex's roles are user-level (user_agents): only Cursor and
|
|
850
|
+
# opencode get a dir here
|
|
851
|
+
hosts = set() if disabled else set(infra["hosts"]) - set(USER_HOSTS)
|
|
800
852
|
active = [] if disabled else [r for r in ROLES if r[2](infra)]
|
|
801
853
|
names = [r[0] for r in active]
|
|
802
854
|
|
|
803
|
-
|
|
804
|
-
md_dir = os.path.join(root, HOST_DIRS["claude"]) if hosts & {"claude", "cursor"} else None
|
|
855
|
+
cursor_dir = os.path.join(root, HOST_DIRS["cursor"]) if "cursor" in hosts else None
|
|
805
856
|
oc_dir = os.path.join(root, HOST_DIRS["opencode"]) if "opencode" in hosts else None
|
|
806
|
-
codex_dir = os.path.join(root, HOST_DIRS["codex"]) if "codex" in hosts else None
|
|
807
857
|
|
|
808
858
|
written, removed = 0, 0
|
|
809
859
|
paths = []
|
|
860
|
+
wanted = {h: set() for h in HOST_DIRS}
|
|
810
861
|
|
|
811
|
-
def emit(
|
|
862
|
+
def emit(host, name, text):
|
|
812
863
|
nonlocal written
|
|
813
|
-
wanted.add(name)
|
|
814
|
-
path = os.path.join(
|
|
864
|
+
wanted[host].add(name)
|
|
865
|
+
path = os.path.join(root, HOST_DIRS[host], name)
|
|
815
866
|
if _write_if_changed(path, text):
|
|
816
867
|
written += 1
|
|
817
868
|
paths.append(path)
|
|
818
869
|
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
nonlocal removed
|
|
823
|
-
before = set(os.path.basename(p) for p in glob.glob(os.path.join(directory, "tezgah-*.*")))
|
|
824
|
-
_remove_stale(directory, wanted)
|
|
825
|
-
after = set(os.path.basename(p) for p in glob.glob(os.path.join(directory, "tezgah-*.*")))
|
|
826
|
-
removed += len(before - after)
|
|
827
|
-
|
|
828
|
-
if md_dir and names: # Claude Code + Cursor
|
|
870
|
+
# Cursor gets no orchestrator: its files are subagents the main thread
|
|
871
|
+
# delegates to, and the orchestrator is a primary agent
|
|
872
|
+
if cursor_dir and names:
|
|
829
873
|
for name, desc, _cap, body, readonly in active:
|
|
830
|
-
emit(
|
|
831
|
-
|
|
874
|
+
emit("cursor", name + ".md",
|
|
875
|
+
render_md_cursor(name, desc, readonly, body("cursor")))
|
|
832
876
|
|
|
833
877
|
if oc_dir and names: # opencode
|
|
834
878
|
for name, desc, _cap, body, readonly in active:
|
|
835
|
-
emit(
|
|
879
|
+
emit("opencode", name + ".md",
|
|
836
880
|
render_md_opencode(name, desc, readonly, body("opencode")))
|
|
837
|
-
emit(
|
|
838
|
-
|
|
839
|
-
if codex_dir and names: # Codex (no orchestrator agent; the main thread orchestrates)
|
|
840
|
-
for name, desc, _cap, body, readonly in active:
|
|
841
|
-
emit(codex_dir, wanted_codex, name + ".toml",
|
|
842
|
-
render_toml(name, desc, readonly, body("codex")))
|
|
881
|
+
emit("opencode", "tezgah-orchestrator.md", render_orch_md_opencode(names))
|
|
843
882
|
|
|
844
|
-
# The sweep runs over every dir this module
|
|
845
|
-
# ones: a host dropped from the config,
|
|
846
|
-
#
|
|
847
|
-
|
|
848
|
-
|
|
849
|
-
|
|
883
|
+
# The sweep runs over every dir this module ever wrote, not only the
|
|
884
|
+
# selected ones: a host dropped from the config, a capability that
|
|
885
|
+
# disappeared, or a `.claude`/`.codex` copy from before the roles moved
|
|
886
|
+
# user-level must not stay behind for a host to keep loading.
|
|
887
|
+
for host, directory in HOST_DIRS.items():
|
|
888
|
+
removed += len(remove_stale(os.path.join(root, directory), wanted[host]))
|
|
850
889
|
|
|
851
890
|
if paths:
|
|
852
|
-
dirs =
|
|
853
|
-
if md_dir:
|
|
854
|
-
dirs.add(HOST_DIRS["claude"])
|
|
855
|
-
if oc_dir:
|
|
856
|
-
dirs.add(HOST_DIRS["opencode"])
|
|
857
|
-
if codex_dir:
|
|
858
|
-
dirs.add(HOST_DIRS["codex"])
|
|
891
|
+
dirs = {HOST_DIRS[h] for h in ("cursor", "opencode") if wanted[h]}
|
|
859
892
|
ex = ensure_exclude(root, dirs)
|
|
860
893
|
if ex:
|
|
861
894
|
paths.append(ex)
|
|
@@ -868,7 +901,7 @@ def sync_root(root, report_steady=False):
|
|
|
868
901
|
% (len(names), ", %d removed" % removed if removed else ""))
|
|
869
902
|
if removed:
|
|
870
903
|
return "%d stale agent file(s) removed" % removed
|
|
871
|
-
if report_steady and names and (
|
|
904
|
+
if report_steady and names and (cursor_dir or oc_dir):
|
|
872
905
|
return "%d agent(s) current" % len(names)
|
|
873
906
|
return None
|
|
874
907
|
|
package/hooks/tezgah_context.py
CHANGED
|
@@ -690,7 +690,8 @@ def autoindex(root):
|
|
|
690
690
|
|
|
691
691
|
|
|
692
692
|
def sync_agents(root):
|
|
693
|
-
"""Generate/refresh this repo's
|
|
693
|
+
"""Generate/refresh this repo's Cursor and opencode subagent files (best
|
|
694
|
+
effort); Claude's and Codex's roles are user-level, written at install."""
|
|
694
695
|
try:
|
|
695
696
|
from tezgah_agents import sync_root
|
|
696
697
|
return sync_root(root)
|
package/hooks/tezgah_judge.py
CHANGED
|
@@ -65,7 +65,8 @@ import tezgah_store
|
|
|
65
65
|
URL = "https://api.typesafe.ai/v1/systemone"
|
|
66
66
|
MODEL = "jev-latest"
|
|
67
67
|
# The other Jev carriers (`JEV_CARRIERS`): OpenRouter's System One endpoint,
|
|
68
|
-
# which takes TypeSafe's body and
|
|
68
|
+
# which takes TypeSafe's body and maps a bare `jev-*` id onto `typesafe/` itself
|
|
69
|
+
# (`typesafe/jev-latest` is no model there: a live 400 on 2026-10-09); and
|
|
69
70
|
# Cloudflare Workers AI, whose run route wraps the state and questions in
|
|
70
71
|
# `input` and serves the one alias `typesafe/jev`. A System One-compatible URL
|
|
71
72
|
# is the user's own (`JEV_API_BASE_URL`), so it has no constant here.
|
|
@@ -157,7 +158,7 @@ def _jev_call(provider, model, state, questions):
|
|
|
157
158
|
return CLOUDFLARE_MODEL, url, {"model": CLOUDFLARE_MODEL, "input": {
|
|
158
159
|
"state": state, "questions": questions}}
|
|
159
160
|
if provider == "jev-openrouter":
|
|
160
|
-
used = model
|
|
161
|
+
used = model
|
|
161
162
|
url = override("TEZGAH_JEV_OPENROUTER_URL", JEV_OPENROUTER_URL)
|
|
162
163
|
elif provider == "jev-compatible":
|
|
163
164
|
used = os.environ.get("TEZGAH_JEV_MODEL", "").strip() or model
|
|
@@ -215,12 +216,17 @@ SESSION_ARGV = {
|
|
|
215
216
|
# prompt asked for; the judge's state can be a screen's untrusted text, so the
|
|
216
217
|
# run gets an inline config (`OPENCODE_CONFIG_CONTENT`) that denies every tool -
|
|
217
218
|
# the same probe with it wrote nothing. cursor-agent's `--mode ask` refused the
|
|
218
|
-
# same shell call, and codex's `-s read-only` sandbox has no network.
|
|
219
|
+
# same shell call, and codex's `-s read-only` sandbox has no network. Inline
|
|
220
|
+
# config overrides the user's files only key by key, and an agent's own
|
|
221
|
+
# `permission` wins over the top-level one, so the deny is repeated for the two
|
|
222
|
+
# built-in agents `run` uses.
|
|
223
|
+
_DENY_ALL = {"*": "deny", "bash": "deny", "edit": "deny", "webfetch": "deny",
|
|
224
|
+
"external_directory": "deny"}
|
|
219
225
|
SESSION_ENV = {
|
|
220
226
|
"opencode": {"OPENCODE_CONFIG_CONTENT": json.dumps({
|
|
221
|
-
"permission":
|
|
222
|
-
|
|
223
|
-
|
|
227
|
+
"permission": _DENY_ALL, "tools": {"*": False},
|
|
228
|
+
"agent": {name: {"permission": _DENY_ALL, "tools": {"*": False}}
|
|
229
|
+
for name in ("build", "plan")}})},
|
|
224
230
|
}
|
|
225
231
|
NO_SESSION = ("no session CLI to ask (no omp, Claude Code, opencode, Cursor or Codex "
|
|
226
232
|
"session marker or TEZGAH_JUDGE_CLI/judge_cli pick, or that CLI missing)")
|
|
@@ -386,8 +392,9 @@ def ask(state, questions, *, model=MODEL, timeout=30, attempts=2, deadline=None,
|
|
|
386
392
|
return result
|
|
387
393
|
refused = None
|
|
388
394
|
session = tp.session_cli()
|
|
389
|
-
|
|
390
|
-
|
|
395
|
+
# only a session CLI that was asked can have failed before a refusal
|
|
396
|
+
if session in (p for p, _ in tried) and (openrouter_key()
|
|
397
|
+
or any(s for _, s in tp.jev_carriers())):
|
|
391
398
|
refused = ("fallback=%s refuses a third-party judge after %s failed"
|
|
392
399
|
% (tp.fallback_policy(), session))
|
|
393
400
|
_record(None, None, failed="; ".join(failed), refused=refused)
|
package/hooks/tezgah_models.py
CHANGED
|
@@ -709,6 +709,39 @@ TIER_QUESTION = {
|
|
|
709
709
|
"a hard-to-reverse decision",
|
|
710
710
|
},
|
|
711
711
|
}
|
|
712
|
+
# The second question in the same batched call: whether the brief is one of the
|
|
713
|
+
# specialists' jobs. Before it existed route() could only name a tier, and from
|
|
714
|
+
# 2026-10-07 to 10-09 the five specialists were spawned 0 times in 121
|
|
715
|
+
# delegations while the generic `task` agent took 86. Every specialist but the
|
|
716
|
+
# docs writer is read-only or check-only, so a brief that changes code is `none`.
|
|
717
|
+
ROLE_QUESTION = {
|
|
718
|
+
"type": "choice",
|
|
719
|
+
"instructions": (
|
|
720
|
+
"Is `task` the whole job of one of these specialist agents? Pick `none` "
|
|
721
|
+
"when the task writes or changes source code, or mixes a specialist job "
|
|
722
|
+
"with code changes."),
|
|
723
|
+
"criteria": {
|
|
724
|
+
"none": "writing or changing code, or anything not listed below",
|
|
725
|
+
"tester": "only running the checks or tests a change reaches and reporting "
|
|
726
|
+
"their output; no edits",
|
|
727
|
+
"docs": "writing or repairing documentation pages or their path:line "
|
|
728
|
+
"citations",
|
|
729
|
+
"security": "a read-only security review of a change or surface",
|
|
730
|
+
"ui": "inspecting a running web or mobile app's screens against a design "
|
|
731
|
+
"floor or accessibility",
|
|
732
|
+
"researcher": "a research line: hypotheses, literature, experiments, a report",
|
|
733
|
+
"reviewer": "an adversarial read-only review of a diff, branch or pull request",
|
|
734
|
+
},
|
|
735
|
+
}
|
|
736
|
+
ROLE_AGENT = {role: "tezgah-" + role for role in ROLE_QUESTION["criteria"] if role != "none"}
|
|
737
|
+
# Without a judgement, the phase names the specialist where it is one.
|
|
738
|
+
PHASE_AGENT = {"review": "tezgah-reviewer", "research": "tezgah-researcher"}
|
|
739
|
+
# An override brief never leaves the machine, so the rule decides the one
|
|
740
|
+
# specialist it can name: a security review that writes nothing.
|
|
741
|
+
REVIEW_ONLY = re.compile(r"\b(?:review|audit|assess)\w*\b", re.I)
|
|
742
|
+
# `change` and `update` are left out: in "review the auth change" they are nouns.
|
|
743
|
+
WRITES = re.compile(r"\b(?:fix|implement|edit|write|add|refactor|patch|migrate|"
|
|
744
|
+
r"rename|delete|remove|rotate)\w*\b", re.I)
|
|
712
745
|
|
|
713
746
|
|
|
714
747
|
def route(brief, phase=None, ask=None):
|
|
@@ -724,23 +757,37 @@ def route(brief, phase=None, ask=None):
|
|
|
724
757
|
reply is the same as no reply and this never raises."""
|
|
725
758
|
hit = OVERRIDE.search(brief or "")
|
|
726
759
|
if hit:
|
|
760
|
+
if REVIEW_ONLY.search(brief) and not WRITES.search(brief):
|
|
761
|
+
return _agent("tezgah-security", "override %r, review only" % hit.group(0),
|
|
762
|
+
"rule")
|
|
727
763
|
return _pick("frontier", "override: %r" % hit.group(0), "rule")
|
|
728
|
-
result = ask({"task": ti.redact(brief or "")},
|
|
764
|
+
result = ask({"task": ti.redact(brief or "")},
|
|
765
|
+
{"tier": TIER_QUESTION, "role": ROLE_QUESTION}) if ask else None
|
|
729
766
|
choice = tj.choice(result, "tier")
|
|
730
767
|
if choice in JEV_TIER:
|
|
731
768
|
chance = tj.noul(result, "tier", choice)
|
|
732
|
-
|
|
733
|
-
|
|
769
|
+
why = "jev %s%s" % (choice, "" if chance is None else " %.2f" % chance)
|
|
770
|
+
role = tj.choice(result, "role")
|
|
771
|
+
out = (_agent(ROLE_AGENT[role], "%s, role %s" % (why, role), "jev")
|
|
772
|
+
if role in ROLE_AGENT else _pick(JEV_TIER[choice], why, "jev"))
|
|
734
773
|
out["judge"] = "%s/%s" % (result.get("provider") or "-",
|
|
735
774
|
result.get("model") or "-")
|
|
736
775
|
out["judged"] = result
|
|
737
776
|
return out
|
|
777
|
+
if phase in PHASE_AGENT:
|
|
778
|
+
return _agent(PHASE_AGENT[phase], "no judgement; phase %s names it" % phase,
|
|
779
|
+
"phase")
|
|
738
780
|
if phase in PHASE_TIER:
|
|
739
781
|
return _pick(PHASE_TIER[phase], "no judgement; static table for phase %s" % phase,
|
|
740
782
|
"phase")
|
|
741
783
|
return _pick("standard", "no judgement and no phase; the middle tier", "default")
|
|
742
784
|
|
|
743
785
|
|
|
786
|
+
def _agent(agent, why, via):
|
|
787
|
+
"""A specialist, on the tier its model slot names (`AGENT_SLOT`)."""
|
|
788
|
+
return dict(_pick(AGENT_SLOT[agent], why, via), agent=agent)
|
|
789
|
+
|
|
790
|
+
|
|
744
791
|
def _pick(tier, why, via):
|
|
745
792
|
return {"agent": "tezgah-" + tier, "tier": tier, "why": why, "via": via,
|
|
746
793
|
"judge": None, "judged": None}
|
package/hooks/tezgah_paths.py
CHANGED
|
@@ -592,17 +592,25 @@ JEV_CHOICES = ("auto", "typesafe", "openrouter", "cloudflare", "compatible",
|
|
|
592
592
|
"clef", "clef-flash")
|
|
593
593
|
|
|
594
594
|
|
|
595
|
+
def jev_name(value):
|
|
596
|
+
"""A carrier choice as written by a user, lowercased and with the Workers AI
|
|
597
|
+
catalog prefix dropped: `@cf/cloudflare/clef-flash` is what Cloudflare's
|
|
598
|
+
dashboard and docs show, and it names the `clef-flash` carrier."""
|
|
599
|
+
value = value.strip().lower()
|
|
600
|
+
return value[len("@cf/cloudflare/"):] if value.startswith("@cf/cloudflare/") else value
|
|
601
|
+
|
|
602
|
+
|
|
595
603
|
def jev_choice():
|
|
596
604
|
"""TEZGAH_JEV_PROVIDER, else JEV_PROVIDER (jev-mcp's name), else config.json
|
|
597
|
-
`jev`, else `auto` -
|
|
598
|
-
no carrier instead of silently to another one."""
|
|
605
|
+
`jev`, else `auto` - through `jev_name`. An unknown value is kept, so it
|
|
606
|
+
resolves to no carrier instead of silently to another one."""
|
|
599
607
|
for env in ("TEZGAH_JEV_PROVIDER", "JEV_PROVIDER"):
|
|
600
|
-
value = os.environ.get(env, "")
|
|
608
|
+
value = jev_name(os.environ.get(env, ""))
|
|
601
609
|
if value:
|
|
602
610
|
return value
|
|
603
611
|
cfg = config()
|
|
604
612
|
value = cfg.get("jev") if isinstance(cfg, dict) else None
|
|
605
|
-
value = value
|
|
613
|
+
value = jev_name(value) if isinstance(value, str) else ""
|
|
606
614
|
return value or "auto"
|
|
607
615
|
|
|
608
616
|
|
|
@@ -610,18 +618,25 @@ def jev_carriers():
|
|
|
610
618
|
"""[(carrier, secret), ...] the chosen Jev carriers in the order the seam
|
|
611
619
|
asks them. `auto`: every carrier whose credential resolves. A named one: that
|
|
612
620
|
carrier alone, its secret None when it does not resolve - the seam records
|
|
613
|
-
why rather than substituting another carrier.
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
621
|
+
why rather than substituting another carrier. Only the chosen carrier's
|
|
622
|
+
credential is read: the status line asks this on every redraw, and the
|
|
623
|
+
TypeSafe channel ends in omp's login database."""
|
|
624
|
+
def compatible():
|
|
625
|
+
return (os.environ.get("JEV_API_KEY", "").strip() or None
|
|
626
|
+
if os.environ.get("JEV_API_BASE_URL", "").strip() else None)
|
|
627
|
+
|
|
628
|
+
def workers_ai():
|
|
629
|
+
return cloudflare_token() if cloudflare_account() else None
|
|
630
|
+
found = [("typesafe", typesafe_key), ("jev-openrouter", openrouter_key),
|
|
618
631
|
("jev-cloudflare", workers_ai), ("jev-compatible", compatible),
|
|
619
632
|
("clef", workers_ai), ("clef-flash", workers_ai)]
|
|
620
633
|
choice = jev_choice()
|
|
621
634
|
if choice == "auto":
|
|
622
|
-
return [(name, secret) for name, secret in
|
|
635
|
+
return [(name, secret) for name, secret in ((n, read()) for n, read in found)
|
|
636
|
+
if secret]
|
|
623
637
|
name = choice if choice in ("typesafe", "clef", "clef-flash") else "jev-" + choice
|
|
624
|
-
|
|
638
|
+
read = dict(found).get(name)
|
|
639
|
+
return [(name, read() if read else None)]
|
|
625
640
|
|
|
626
641
|
|
|
627
642
|
# How far a model call may leave the session's own vendor (config.json
|
package/hooks/tezgah_policy.py
CHANGED
|
@@ -303,8 +303,10 @@ active --json` and track it with `orca worktree ps --json` or `tezgah-status
|
|
|
303
303
|
status in Orca; outside Orca, `git worktree add`. Either way the brief names
|
|
304
304
|
absolute paths under that checkout.
|
|
305
305
|
Routing: `{ROUTE_BIN} "<brief>"` names the worker - tezgah-cheap, -standard or
|
|
306
|
-
-frontier
|
|
307
|
-
|
|
306
|
+
-frontier, or the specialist whose whole job the brief is (tezgah-tester,
|
|
307
|
+
-docs, -security, -ui, -researcher, -reviewer) - and the spawn uses that name,
|
|
308
|
+
never the host's generic task agent (overrides for stored data, credentials,
|
|
309
|
+
security first; then a Jev judgement; the static phase table with no key). A worker that answers
|
|
308
310
|
`ESCALATE:` is restarted on tezgah-frontier with the original brief, never
|
|
309
311
|
continued; the main thread never switches model (the cache is per model). A
|
|
310
312
|
code-discovery brief names codegraph's tools (`codegraph callers`, `callees`,
|
|
@@ -951,9 +953,11 @@ CONDITIONAL_KEYS = ("spec", "consult", "research", "product", "graph", "fanout")
|
|
|
951
953
|
# the session had to recall, and the second list of a session was worked one
|
|
952
954
|
# item at a time. It is the paragraph's own clause, cut to one sentence.
|
|
953
955
|
FANOUT_ACTION = ("count the independent items, spawn one subagent per item in "
|
|
954
|
-
"ONE message
|
|
955
|
-
"
|
|
956
|
-
"
|
|
956
|
+
"ONE message, each the agent `tezgah-route` names - a specialist "
|
|
957
|
+
"by name, never the generic task agent - (each writing slice in "
|
|
958
|
+
"its own worktree, absolute paths), and keep the main thread for "
|
|
959
|
+
"integrating and verifying; a subagent does its own slice and "
|
|
960
|
+
"never fans out again.")
|
|
957
961
|
|
|
958
962
|
# The always-on replacement for the conditional paragraphs: one line each so a
|
|
959
963
|
# host without a per-turn hook still knows the rule exists and where the full
|
package/hooks/tezgah_task.py
CHANGED
|
@@ -158,6 +158,12 @@ _MARKER = re.compile(r"unverifiable(?![-\w])", re.I)
|
|
|
158
158
|
# why") has no separator there and is not a declaration.
|
|
159
159
|
_SEPARATORS = ":,;(-"
|
|
160
160
|
_SPAN = re.compile(r"`([^`\n]+)`")
|
|
161
|
+
# A runner whose test entry point is a bare subcommand: `pnpm test`, `cargo
|
|
162
|
+
# test`, `uv run pytest` carry no option and no path, yet each is the command.
|
|
163
|
+
# The runner has to be the first word, so `run the tests` stays prose.
|
|
164
|
+
_RUNNERS = ("npm", "pnpm", "yarn", "bun", "deno", "cargo", "go", "make", "mix",
|
|
165
|
+
"dotnet", "gradle", "mvn", "poetry", "uv")
|
|
166
|
+
_SUBCOMMANDS = ("test", "run")
|
|
161
167
|
|
|
162
168
|
|
|
163
169
|
def _argument(token):
|
|
@@ -179,14 +185,16 @@ def _argument(token):
|
|
|
179
185
|
|
|
180
186
|
def names_command(text):
|
|
181
187
|
"""Does this acceptance item name the command that proves it? A command is a
|
|
182
|
-
backticked run of two or more words carrying an option, a path or a script
|
|
183
|
-
`bin/tezgah-docs --citations`, `python3 -m unittest tests/test_task.py
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
188
|
+
backticked run of two or more words carrying an option, a path or a script
|
|
189
|
+
(`bin/tezgah-docs --citations`, `python3 -m unittest tests/test_task.py`), or
|
|
190
|
+
a known runner followed by `test` or `run` (`pnpm test`, `npm run test`). A
|
|
191
|
+
citation (`hooks/tezgah_task.py`, `plan:line`) is not one, and any other
|
|
192
|
+
invocation with no argument is not recognised either - both are reported,
|
|
193
|
+
which is the safe direction for a report a person reads."""
|
|
187
194
|
for span in _SPAN.findall(text):
|
|
188
195
|
tokens = span.split()
|
|
189
|
-
if len(tokens) > 1 and any(_argument(token) for token in tokens[1:])
|
|
196
|
+
if len(tokens) > 1 and (any(_argument(token) for token in tokens[1:]) or
|
|
197
|
+
(tokens[0] in _RUNNERS and tokens[1] in _SUBCOMMANDS)):
|
|
190
198
|
return True
|
|
191
199
|
return False
|
|
192
200
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@r1z4x/tezgah",
|
|
3
|
-
"version": "1.5.
|
|
3
|
+
"version": "1.5.1",
|
|
4
4
|
"description": "One shared working contract for every AI coding assistant you run: Claude Code, Codex, Cursor, opencode, dsh and oh-my-pi (omp).",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"repository": {
|
|
@@ -19,7 +19,6 @@
|
|
|
19
19
|
"output-styles/",
|
|
20
20
|
"commands/",
|
|
21
21
|
"docs/",
|
|
22
|
-
"agents/",
|
|
23
22
|
"statusline.py",
|
|
24
23
|
"MANIFEST",
|
|
25
24
|
"CHANGELOG.md",
|
|
@@ -233,8 +233,10 @@ active --json` and track it with `orca worktree ps --json` or `tezgah-status
|
|
|
233
233
|
status in Orca; outside Orca, `git worktree add`. Either way the brief names
|
|
234
234
|
absolute paths under that checkout.
|
|
235
235
|
Routing: `~/.config/tezgah/bin/tezgah-route "<brief>"` names the worker - tezgah-cheap, -standard or
|
|
236
|
-
-frontier
|
|
237
|
-
|
|
236
|
+
-frontier, or the specialist whose whole job the brief is (tezgah-tester,
|
|
237
|
+
-docs, -security, -ui, -researcher, -reviewer) - and the spawn uses that name,
|
|
238
|
+
never the host's generic task agent (overrides for stored data, credentials,
|
|
239
|
+
security first; then a Jev judgement; the static phase table with no key). A worker that answers
|
|
238
240
|
`ESCALATE:` is restarted on tezgah-frontier with the original brief, never
|
|
239
241
|
continued; the main thread never switches model (the cache is per model). A
|
|
240
242
|
code-discovery brief names codegraph's tools (`codegraph callers`, `callees`,
|
|
@@ -1,79 +0,0 @@
|
|
|
1
|
-
---
|
|
2
|
-
name: tezgah-reviewer
|
|
3
|
-
description: >
|
|
4
|
-
Adversarial, read-only review of a diff, branch or PR: derives the blast
|
|
5
|
-
radius, checks correctness, contract, security, tests and performance, and
|
|
6
|
-
classifies every finding confirmed/refuted/unverified. One review per
|
|
7
|
-
plan, at most two rounds; spawn one per dimension in one message for a
|
|
8
|
-
large diff.
|
|
9
|
-
model: opus
|
|
10
|
-
effort: high
|
|
11
|
-
readonly: true
|
|
12
|
-
disallowedTools: Write, Edit, NotebookEdit, Bash, Agent
|
|
13
|
-
---
|
|
14
|
-
|
|
15
|
-
You are tezgah-reviewer, an adversarial code reviewer. Review a change,
|
|
16
|
-
not the whole repo, with the code graph as below.
|
|
17
|
-
|
|
18
|
-
Load the graph tools first: ToolSearch("select:mcp__codegraph__codegraph_explore,mcp__plugin_tezgah_codegraph__codegraph_explore,mcp__codegraph__codegraph_impact,mcp__plugin_tezgah_codegraph__codegraph_impact,mcp__codegraph__codegraph_callers,mcp__plugin_tezgah_codegraph__codegraph_callers,mcp__codegraph__codegraph_node,mcp__plugin_tezgah_codegraph__codegraph_node"). You have no
|
|
19
|
-
shell, so the codegraph CLI is out of reach: use those MCP tools.
|
|
20
|
-
|
|
21
|
-
Review the raw artifact, never a summary of it and never the author's
|
|
22
|
-
self-assessment. Format, schema, frontmatter and spec-compliance problems
|
|
23
|
-
live in the exact text, and a framing you were handed is a finding you will
|
|
24
|
-
not make: a reviewer given the implementer's view finds measurably fewer
|
|
25
|
-
defects. If you were handed a paraphrase, read the files instead.
|
|
26
|
-
|
|
27
|
-
You have no shell, so you cannot run `git diff`: the caller passes
|
|
28
|
-
the changed files and symbols of the target (a base branch, a ref or
|
|
29
|
-
the PR under review). Run the MCP `codegraph_impact` tool per changed
|
|
30
|
-
symbol - that is the blast radius. If the brief names no changed files,
|
|
31
|
-
say so and ask for them instead of guessing. Read the changed code and any caller you intend to accuse.
|
|
32
|
-
An empty or short caller list is not proof that a change is safe: the
|
|
33
|
-
index records only the edges its parser saw, so say what it cannot see
|
|
34
|
-
(dynamic dispatch, string or config lookups, unindexed files) instead of
|
|
35
|
-
implying the list is complete.
|
|
36
|
-
Look only for defects the change causes: correctness, contract/callers,
|
|
37
|
-
security, tests, concurrency and performance. Classify every candidate as
|
|
38
|
-
confirmed (concrete failure scenario + file:line), refuted (a guard,
|
|
39
|
-
caller contract, type or test already prevents it) or unverified (not
|
|
40
|
-
settled). Default to refuted when the evidence is unclear. Only the code
|
|
41
|
-
or a run you observed settles a candidate: a doc, a comment, a lessons
|
|
42
|
-
file, a memory note or an earlier report may raise a question but never
|
|
43
|
-
makes it confirmed or refuted on its own. Report only confirmed findings
|
|
44
|
-
as bugs; no style notes, no praise; empty is the correct answer for a
|
|
45
|
-
clean change.
|
|
46
|
-
|
|
47
|
-
Score the constraints separately from the defects. For every constraint
|
|
48
|
-
the task stated - keep this behaviour, touch no other file, preserve this
|
|
49
|
-
format - report kept or violated with the file:line that shows which. A
|
|
50
|
-
patch that passes the tests and breaks a stated constraint is not clean,
|
|
51
|
-
and a functional test will not notice it.
|
|
52
|
-
|
|
53
|
-
Every finding carries a severity - critical (the change cannot stand as
|
|
54
|
-
written), major (a real weakness that must be fixed), minor (noticeable),
|
|
55
|
-
suggestion (an improvement, not a flaw) - and a verbatim quote of the
|
|
56
|
-
code it accuses; a finding about an absence carries no quote. A severity
|
|
57
|
-
is lowered only by evidence: a check you could not complete never
|
|
58
|
-
downgrades a confirmed finding - keep its severity and mark it
|
|
59
|
-
provisional. Disclose the order you read the files in.
|
|
60
|
-
|
|
61
|
-
Scope and budget. One review per plan, over its whole diff in the
|
|
62
|
-
verification phase; at most two rounds - only a confirmed critical or
|
|
63
|
-
major finding opens round two, a minor or suggestion one is recorded
|
|
64
|
-
fix-later, and after round two what is left goes to the user or is
|
|
65
|
-
recorded fix-later, never a third round. A round may be a fan-out of
|
|
66
|
-
reviewers spawned at once, one per dimension (correctness and contract,
|
|
67
|
-
security, tests and performance) or per file group: when your brief
|
|
68
|
-
names a dimension or files, review only those - a sibling owns the rest.
|
|
69
|
-
Round two reads only the delta since the sha round one recorded
|
|
70
|
-
(`tezgah-task review` prints it), plus the round-one hunks and direct
|
|
71
|
-
callers of the symbols that delta touches, and marks every round-one
|
|
72
|
-
finding closed or open: re-reading the whole diff re-reports what was
|
|
73
|
-
already triaged. Test results are input, not a task - read the author's
|
|
74
|
-
output file; do not run the suite, and run at most one `-k` test when a
|
|
75
|
-
specific accusation needs it. The same defect family confirmed in both
|
|
76
|
-
rounds is a sign the design needs a pivot, not another patch.
|
|
77
|
-
|
|
78
|
-
You may use: the codegraph MCP tools, read, grep and glob. Read-only:
|
|
79
|
-
no writes, no edits, no shell.
|