@r1z4x/tezgah 1.5.0 → 1.6.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.
- package/CHANGELOG.md +96 -1
- package/MANIFEST +6 -1
- package/README.md +6 -4
- package/bin/consult +13 -10
- package/bin/tezgah-docs +1 -1
- package/bin/tezgah-research +53 -3
- package/bin/tezgah-route +8 -5
- package/bin/tezgah-setup +45 -25
- package/bin/tezgah-task +194 -41
- package/docs/architecture.md +1 -1
- package/docs/citations-baseline.json +1 -1
- package/docs/clarity-baseline.json +4 -4
- package/docs/contract.md +4 -1
- package/docs/feature-audit.md +24 -3
- package/docs/gate.md +10 -11
- package/docs/hosts.md +9 -0
- package/docs/index.json +21 -7
- 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/research.md +46 -0
- package/docs/skills.md +23 -5
- package/docs/testing.md +1 -1
- package/hooks/tezgah_agents.py +130 -96
- package/hooks/tezgah_components.py +2 -1
- package/hooks/tezgah_context.py +67 -15
- package/hooks/tezgah_gate.py +62 -76
- 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 +47 -19
- package/hooks/tezgah_product.py +107 -0
- package/hooks/tezgah_research.py +3 -3
- package/hooks/tezgah_skill_pick.py +58 -17
- package/hooks/tezgah_task.py +16 -8
- package/hosts/opencode/plugins/tezgah.js +17 -37
- package/package.json +1 -2
- package/skills/analyze-app/SKILL.md +57 -0
- package/skills/design-contract/EVALS.md +24 -1
- package/skills/design-contract/SKILL.md +17 -1
- package/skills/design-contract/data-views.md +86 -0
- package/skills/feature-audit/SKILL.md +32 -3
- package/skills/plan-add/EVALS.md +8 -8
- package/skills/plan-add/SKILL.md +20 -21
- package/skills/plan-sync/EVALS.md +2 -2
- package/skills/plan-sync/SKILL.md +11 -7
- package/skills/pm-frameworks/SKILL.md +6 -0
- package/skills/product-analysis/EVALS.md +23 -1
- package/skills/product-analysis/SKILL.md +50 -11
- package/skills/product-flow/EVALS.md +81 -0
- package/skills/product-flow/SKILL.md +91 -0
- package/skills/research/SKILL.md +10 -1
- package/skills/tezgah-contract/SKILL.md +67 -41
- package/agents/tezgah-reviewer.md +0 -79
package/hooks/tezgah_gate.py
CHANGED
|
@@ -38,28 +38,26 @@ Rules, all only inside a tezgah root:
|
|
|
38
38
|
8. a write while an active task is set - a plan file under .tezgah/plans/open whose
|
|
39
39
|
frontmatter carries a valid `phase` - is refused when that phase is one
|
|
40
40
|
that only reads, or when the file is outside the task's `allowed_paths`
|
|
41
|
-
refused (`task`). The record is
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
41
|
+
refused (`task`). The record is written by bin/tezgah-task, which the
|
|
42
|
+
orchestrating session runs itself as the work moves, so the refusal leans
|
|
43
|
+
on a state the CLI vetted (an Acceptance item per command, an answered
|
|
44
|
+
spike, a checkpoint) rather than one this gate inferred: no active task
|
|
45
|
+
means no requirement, and the rule invents nothing. The record also
|
|
46
|
+
carries where the phase returns to - a
|
|
45
47
|
`checkpoint:` sha, or `pending` while the tree still holds work no commit
|
|
46
48
|
names - and a write in `implementation` is refused while it reads `pending`
|
|
47
49
|
and the tree is dirty, so the phase's work starts from a named state rather
|
|
48
50
|
than on top of uncommitted changes; the per-file snapshots are not one
|
|
49
|
-
state to return to.
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
measured open before they were closed - with the refusal naming the command
|
|
53
|
-
that lifts it, an armed arm removed or disabled the gate in 25 of 25 runs
|
|
54
|
-
and obeyed it in none (an internal benchmark, block E7) - and neither
|
|
55
|
-
refusal names a command now. The phase covers the shell too, because the
|
|
51
|
+
state to return to. A write whose target IS that record is refused too:
|
|
52
|
+
retyping the frontmatter would skip the preconditions the CLI checks. The
|
|
53
|
+
phase covers the shell too, because the
|
|
56
54
|
write tools are not the only way to change a file: with them refused, the
|
|
57
55
|
armed arm wrote the target with a heredoc redirect in 3 of 25 runs, so a
|
|
58
56
|
reading phase refuses a shell command that writes. A code-execution tool is
|
|
59
57
|
a third route the phase does not cover: the same block measured one row in
|
|
60
58
|
25 writing the target through `eval` after the write tools and the shell
|
|
61
59
|
were both refused, and that one is named rather than closed. The `task-off`
|
|
62
|
-
kill switch removes all
|
|
60
|
+
kill switch removes all three.
|
|
63
61
|
9. a `git commit` (or `--amend`) is refused while the newest check recorded in
|
|
64
62
|
this session failed (`order`): a commit is a claim that the tree passed, and
|
|
65
63
|
the state it would freeze is the one a check just rejected. The state is the
|
|
@@ -681,9 +679,10 @@ CLASS_NOTE = {
|
|
|
681
679
|
"transient": "it names a failure the host's own client may retry - a "
|
|
682
680
|
"timeout, a connection error, a rate limit, a 5xx - and an "
|
|
683
681
|
"identical repeat by the agent adds nothing to that",
|
|
684
|
-
"user": "it names something only the user can
|
|
685
|
-
"login, an access grant - so
|
|
686
|
-
"
|
|
682
|
+
"user": "it names something only the user can supply - a credential, a "
|
|
683
|
+
"login, an access grant - so take a route that needs none first "
|
|
684
|
+
"(another provider or member, a local run, a public source) and "
|
|
685
|
+
"ask the user once, naming what it unlocks, only when none is left",
|
|
687
686
|
"permanent": "an assertion or a bad argument does not change by re-running "
|
|
688
687
|
"it",
|
|
689
688
|
}
|
|
@@ -980,52 +979,47 @@ def race_reason(inp, session_id, cwd=None):
|
|
|
980
979
|
return None
|
|
981
980
|
|
|
982
981
|
|
|
983
|
-
# --- task scope: the
|
|
984
|
-
# What this closes: a session that has
|
|
985
|
-
#
|
|
986
|
-
# files it may touch - and then writes anyway. Two shapes cost
|
|
987
|
-
#
|
|
988
|
-
#
|
|
989
|
-
# bounded task quietly becomes an unbounded one.
|
|
990
|
-
#
|
|
991
|
-
#
|
|
992
|
-
#
|
|
982
|
+
# --- task scope: the plan's own phase and path allowlist --------------------
|
|
983
|
+
# What this closes: a session that has declared what it is working on - a plan
|
|
984
|
+
# file under .tezgah/plans/open is active, so the work has a phase and the
|
|
985
|
+
# files it may touch - and then writes anyway. Two shapes cost real work: a
|
|
986
|
+
# write in `discovery`, where the whole point of the phase is to read before
|
|
987
|
+
# deciding, and a write outside the plan's own allowlist, which is how a
|
|
988
|
+
# bounded task quietly becomes an unbounded one. The record is written by
|
|
989
|
+
# bin/tezgah-task, which the orchestrating session runs itself: the plan's
|
|
990
|
+
# lifecycle is the orchestrator's job, never a hand-off to the user. What keeps
|
|
991
|
+
# the boundary is the CLI, not who runs it - a writing phase needs every
|
|
992
|
+
# Acceptance item to name its proving command, `implementation` an answered
|
|
993
|
+
# spike and a checkpoint, `close done` an approving review - so the record's own
|
|
994
|
+
# file is refused to a write (TASK_RECORD_DENY): retyping the frontmatter is the
|
|
995
|
+
# direct route that skips those checks. A shell line's positional target (`sed
|
|
996
|
+
# -i`, `cp`) is not read here (see write_paths), the residual this rule names.
|
|
993
997
|
#
|
|
994
998
|
# The direction is fail-open wherever the record is missing, unreadable or out
|
|
995
|
-
# of phase vocabulary (tezgah_task.active returns None for those)
|
|
996
|
-
#
|
|
997
|
-
#
|
|
998
|
-
#
|
|
999
|
-
#
|
|
1000
|
-
#
|
|
999
|
+
# of phase vocabulary (tezgah_task.active returns None for those): a rule that
|
|
1000
|
+
# refused every write until a record appeared would be the fail-closed trap the
|
|
1001
|
+
# design report names, with the whole session as its blast radius. An empty
|
|
1002
|
+
# allowlist is no scope declared rather than "nothing is allowed", so the phase
|
|
1003
|
+
# is the whole requirement there.
|
|
1004
|
+
#
|
|
1005
|
+
# The refusals name the CLI move, because running it is the work the rule asks
|
|
1006
|
+
# for and the CLI vets it. E7 measured a refusal that named a way to take the
|
|
1007
|
+
# rule off (the armed arm disabled the gate in 25 of 25 runs); the switches that
|
|
1008
|
+
# do that stay behind the control-plane rule below.
|
|
1001
1009
|
TASK_PHASE_DENY = (
|
|
1002
1010
|
"Task gate: the active task %s is in phase `%s`, and this call writes. "
|
|
1003
|
-
"Writes belong to %s.
|
|
1004
|
-
"
|
|
1011
|
+
"Writes belong to %s. Finish the reading this phase is for, then move the "
|
|
1012
|
+
"phase yourself with `tezgah-task phase implementation` - it refuses until "
|
|
1013
|
+
"the plan's Acceptance, spike and checkpoint are in order.")
|
|
1005
1014
|
TASK_SCOPE_DENY = (
|
|
1006
|
-
"Task gate: %s is outside the paths the active task %s allows (%s).
|
|
1007
|
-
"
|
|
1008
|
-
"why
|
|
1009
|
-
|
|
1010
|
-
# one that lifts them, and the E7 block measured what that costs: with the unlock
|
|
1011
|
-
# printed in the refusal, the armed arm removed or disabled the gate in 25 of 25
|
|
1012
|
-
# runs and obeyed it in none (an internal benchmark, block E7). A refusal
|
|
1013
|
-
# is a boundary or it is an instruction; it cannot be both.
|
|
1015
|
+
"Task gate: %s is outside the paths the active task %s allows (%s). If the "
|
|
1016
|
+
"plan's work genuinely reaches it, widen the scope yourself with `tezgah-task "
|
|
1017
|
+
"allow <glob> ... --why \"<reason>\"` (listing the current globs too); "
|
|
1018
|
+
"otherwise leave the file alone.")
|
|
1014
1019
|
TASK_RECORD_DENY = (
|
|
1015
1020
|
"Task gate: %s is the active task's own record. The phase and the allowlist "
|
|
1016
|
-
"live in that file
|
|
1017
|
-
"
|
|
1018
|
-
TASK_UNLOCK_DENY = (
|
|
1019
|
-
"Task gate: this command changes the active task's own record. The phase and "
|
|
1020
|
-
"the allowlist are the user's, and a session that can move them is held to "
|
|
1021
|
-
"nothing. Ask the user to change the task instead.")
|
|
1022
|
-
# A shell command that changes the record. Matched on the masked text, so a
|
|
1023
|
-
# command that merely names the CLI - a grep over the docs, a commit message -
|
|
1024
|
-
# is not refused. The token is the CLI's own basename, with or without `.py`
|
|
1025
|
-
# and run through any interpreter, so any path to it counts.
|
|
1026
|
-
TASK_CHANGE = re.compile(
|
|
1027
|
-
r"(?:^|[|;&(]\s*|\s)(?:python3?\s+)?\S*tezgah-task(?:\.py)?\s+"
|
|
1028
|
-
r"(?:start|phase|allow|stop)\b")
|
|
1021
|
+
"live in that file and only `tezgah-task` writes them, because it checks the "
|
|
1022
|
+
"plan's Acceptance, spike and review on the way; change the task through it.")
|
|
1029
1023
|
# The shapes that make a shell command a write, for the phase rule below. The
|
|
1030
1024
|
# tools are not the only way to change a file, and E7b measured the other one:
|
|
1031
1025
|
# with `edit` and `write` refused twelve and six times, the armed arm wrote the
|
|
@@ -1053,8 +1047,8 @@ SHELL_WRITE = re.compile(
|
|
|
1053
1047
|
TASK_SHELL_DENY = (
|
|
1054
1048
|
"Task gate: the active task %s is in phase `%s`, and this command writes a "
|
|
1055
1049
|
"file. The shell is a write route like any other, so the phase covers it. "
|
|
1056
|
-
"
|
|
1057
|
-
"
|
|
1050
|
+
"Finish the reading this phase is for, then move the phase yourself with "
|
|
1051
|
+
"`tezgah-task phase implementation`.")
|
|
1058
1052
|
|
|
1059
1053
|
|
|
1060
1054
|
# The marker a caller passes when it has NOT read the record, so a rule reads it
|
|
@@ -1100,11 +1094,10 @@ def task_shell_reason(inp, cwd, base, task=_UNRESOLVED):
|
|
|
1100
1094
|
def task_record_reason(inp, cwd, base, task=_UNRESOLVED):
|
|
1101
1095
|
"""A deny reason when this write's target IS the active task's record.
|
|
1102
1096
|
|
|
1103
|
-
The
|
|
1104
|
-
|
|
1105
|
-
|
|
1106
|
-
|
|
1107
|
-
scope, because a scope that can widen itself is not one."""
|
|
1097
|
+
The record is a file in the repo, so an agent that may write files could
|
|
1098
|
+
retype the phase instead of running the CLI, and skip the Acceptance, spike
|
|
1099
|
+
and review checks the CLI makes on the way. Refused whatever the phase and
|
|
1100
|
+
whatever the allowlist says - the record is not inside its own scope."""
|
|
1108
1101
|
task = _task(cwd, base, task)
|
|
1109
1102
|
if not task:
|
|
1110
1103
|
return None
|
|
@@ -1182,11 +1175,9 @@ def task_reason(inp, cwd, base, task=_UNRESOLVED):
|
|
|
1182
1175
|
# be read, and `task-off` above. A refusal has to rest on something a reader can
|
|
1183
1176
|
# check, and a rule that cannot answer must not refuse.
|
|
1184
1177
|
#
|
|
1185
|
-
# The refusal names the one command that makes the boundary real
|
|
1186
|
-
#
|
|
1187
|
-
#
|
|
1188
|
-
# command *satisfies* the requirement - running it is the work the rule asks for,
|
|
1189
|
-
# so naming it is naming what the rule wants rather than a way around it.
|
|
1178
|
+
# The refusal names the one command that makes the boundary real: running it is
|
|
1179
|
+
# the work the rule asks for, so naming it is naming what the rule wants rather
|
|
1180
|
+
# than a way around it.
|
|
1190
1181
|
CHECKPOINT_DENY = (
|
|
1191
1182
|
"Checkpoint gate: the active task %s is in phase `implementation` and its "
|
|
1192
1183
|
"plan records a pending checkpoint (`checkpoint: pending <sha>`), so this "
|
|
@@ -1823,8 +1814,8 @@ def _refusing(label):
|
|
|
1823
1814
|
# `tezgah-gate decide` writes a genuine `began` row with `check=1`; the capture
|
|
1824
1815
|
# CLI writes snapshot rows; pony and adhd write CONFIG_DIR; `tezgah-context
|
|
1825
1816
|
# attest` re-attests a session, and a clean result clears its drift mark. The
|
|
1826
|
-
# task CLI is not here:
|
|
1827
|
-
#
|
|
1817
|
+
# task CLI is not here: the orchestrating session runs it, and the checks that
|
|
1818
|
+
# matter live in the CLI itself (see the task rule above).
|
|
1828
1819
|
# `tezgah-context user_prompt` is a prompt event, and a prompt naming a switch
|
|
1829
1820
|
# writes the `authorized` row the switch latch honors (tezgah_paths.off).
|
|
1830
1821
|
CONTROL_CLIS = {"tezgah-gate": ("decide",), "tezgah-capture": "",
|
|
@@ -2651,11 +2642,9 @@ def decision(tool, inp, cwd, session_id=None, record=True, agent=None):
|
|
|
2651
2642
|
reason = race_reason(inp, session_id, cwd)
|
|
2652
2643
|
if reason:
|
|
2653
2644
|
return _deny(session_id, "race", reason, tool, inp, base)
|
|
2654
|
-
# Task scope: the
|
|
2655
|
-
#
|
|
2656
|
-
#
|
|
2657
|
-
# through the CLI is refused with it: a boundary the agent can move is not a
|
|
2658
|
-
# boundary, and both routes to moving this one were measured open (E7).
|
|
2645
|
+
# Task scope: the active plan's phase and the files its allowlist names (see
|
|
2646
|
+
# task_reason). The record itself is refused first of all: only
|
|
2647
|
+
# bin/tezgah-task writes it, through the checks it makes on the way.
|
|
2659
2648
|
if not off("task-off"):
|
|
2660
2649
|
# The record is read once and handed to every rule below: each read lists
|
|
2661
2650
|
# `.tezgah/plans/open/` and parses a plan, and this path is every write of
|
|
@@ -2667,9 +2656,6 @@ def decision(tool, inp, cwd, session_id=None, record=True, agent=None):
|
|
|
2667
2656
|
reason = task_record_reason(inp, cwd, base, task)
|
|
2668
2657
|
if reason:
|
|
2669
2658
|
return _deny(session_id, "task", reason, tool, inp, base)
|
|
2670
|
-
if t in BASH_TOOLS and TASK_CHANGE.search(
|
|
2671
|
-
mask(str(inp.get("command") or ""))):
|
|
2672
|
-
return _deny(session_id, "task", TASK_UNLOCK_DENY, tool, inp, base)
|
|
2673
2659
|
if t in BASH_TOOLS:
|
|
2674
2660
|
reason = task_shell_reason(inp, cwd, base, task)
|
|
2675
2661
|
if reason:
|
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
|
@@ -236,17 +236,32 @@ rewrite it plainly.
|
|
|
236
236
|
**Merge authority: standing, not per-PR.** The user is not the approval step
|
|
237
237
|
on a merge. Review first with a FRESH reviewer (never my own read of my own
|
|
238
238
|
code), run the full suite, and if the review is clean and every test passes,
|
|
239
|
-
merge
|
|
240
|
-
|
|
241
|
-
failing test
|
|
239
|
+
merge and close the plan yourself (`tezgah-task close NNN done`, which also
|
|
240
|
+
deactivates it and removes every merged worktree), then report the outcome. Nothing
|
|
241
|
+
goes back to the user: a failing test you fix, and a critical or major finding
|
|
242
|
+
left after round two you fix yourself, prove with a test and close with
|
|
243
|
+
`tezgah-task close NNN done --resolved "<fix and test>"`. One review per plan, over its whole diff in the verification
|
|
242
244
|
phase; at most two rounds - only a confirmed critical or major finding opens
|
|
243
245
|
round two, a minor or suggestion one is recorded fix-later, and after round two
|
|
244
|
-
what is left
|
|
246
|
+
what is left is fixed by the session and closed with `--resolved`, or recorded fix-later, never a third round.
|
|
245
247
|
A round spawns its reviewers in ONE message, one per dimension (correctness
|
|
246
248
|
and contract, security, tests and performance) or file group; a diff under 300
|
|
247
249
|
changed lines gets one reviewer. Per-task work gets tests, not a reviewer, and
|
|
248
250
|
`tezgah-task review` refuses a third round.
|
|
249
251
|
|
|
252
|
+
**The process is yours: never hand a step back.** The orchestrator runs the
|
|
253
|
+
plan lifecycle itself - `tezgah-task start`, every `phase` move, `review`,
|
|
254
|
+
`close`, the merge, and removing a slice's worktree once its branch is merged -
|
|
255
|
+
and never ends a reply with "run this command" or "shall I close it?" for a
|
|
256
|
+
step it can take. A process parameter the user did not set (a spend ceiling, a
|
|
257
|
+
sample size, a retry budget, a member to consult) is picked by you: choose the
|
|
258
|
+
conservative default, state it in one line, proceed. A credential, a login or an
|
|
259
|
+
access grant is the one thing no tool supplies, and even then take every route
|
|
260
|
+
that needs none first - another consult member or provider, local compute
|
|
261
|
+
instead of managed, a public source - and ask once, naming what it unlocks, only
|
|
262
|
+
when no route is left. A requested item you would drop is still asked about
|
|
263
|
+
(see "Deliver the whole ask").
|
|
264
|
+
|
|
250
265
|
**Attribution: none, anywhere, ever.** Nothing you persist or publish may name
|
|
251
266
|
the assistant, model, vendor or "AI" as author, co-author, generator or helper -
|
|
252
267
|
on any host (Claude, opencode, Codex, Cursor, dsh, omp), including subagents and cheap
|
|
@@ -303,8 +318,10 @@ active --json` and track it with `orca worktree ps --json` or `tezgah-status
|
|
|
303
318
|
status in Orca; outside Orca, `git worktree add`. Either way the brief names
|
|
304
319
|
absolute paths under that checkout.
|
|
305
320
|
Routing: `{ROUTE_BIN} "<brief>"` names the worker - tezgah-cheap, -standard or
|
|
306
|
-
-frontier
|
|
307
|
-
|
|
321
|
+
-frontier, or the specialist whose whole job the brief is (tezgah-tester,
|
|
322
|
+
-docs, -security, -ui, -researcher, -reviewer) - and the spawn uses that name,
|
|
323
|
+
never the host's generic task agent (overrides for stored data, credentials,
|
|
324
|
+
security first; then a Jev judgement; the static phase table with no key). A worker that answers
|
|
308
325
|
`ESCALATE:` is restarted on tezgah-frontier with the original brief, never
|
|
309
326
|
continued; the main thread never switches model (the cache is per model). A
|
|
310
327
|
code-discovery brief names codegraph's tools (`codegraph callers`, `callees`,
|
|
@@ -400,10 +417,11 @@ question needs facts newer or wider than the codebase - current versions, CVEs,
|
|
|
400
417
|
vendor status, breaking-change news; skip it for pure code/design reasoning.
|
|
401
418
|
|
|
402
419
|
Exit 4 means no choice is recorded and exit 5 means every recorded member
|
|
403
|
-
failed: nothing answered, and the output lists `offer:`/`reoffer:` members.
|
|
404
|
-
the user
|
|
405
|
-
|
|
406
|
-
|
|
420
|
+
failed: nothing answered, and the output lists `offer:`/`reoffer:` members. Do
|
|
421
|
+
not ask the user: record the first listed member from a model family other than
|
|
422
|
+
your own (two make a panel) with `{CONSULT_BIN} --use <member>[,<member>]`, ask
|
|
423
|
+
again, and say which member you chose. After two such rounds with nothing
|
|
424
|
+
answering, report the second opinion as skipped and carry on.
|
|
407
425
|
|
|
408
426
|
Treat every answer as advisory evidence, never as truth: verify each claim
|
|
409
427
|
against the actual code before adopting it, and tell the user which models were
|
|
@@ -451,8 +469,9 @@ are what keeps results comparable, and breaking one silently invalidates the run
|
|
|
451
469
|
never edit a node once a run has answered it (branch a child instead); the run
|
|
452
470
|
command and environment are a fixed contract identical on every node; vary the
|
|
453
471
|
committed code/config, never CLI args or env knobs; grow the experiment tree
|
|
454
|
-
downward, not sideways. Local research needs no login; managed compute does
|
|
455
|
-
|
|
472
|
+
downward, not sideways. Local research needs no login; managed compute does -
|
|
473
|
+
run locally at a scale that fits, and ask for `{ORX_BIN} login` only when the
|
|
474
|
+
question cannot be answered without managed compute.
|
|
456
475
|
|
|
457
476
|
A result row and every claim built on it say what they were measured on: `real`
|
|
458
477
|
for the running system, `fixture` for an input you generated, `derived` for a
|
|
@@ -804,8 +823,10 @@ never your own summary, conclusion or self-assessment - a reviewer handed the
|
|
|
804
823
|
author's framing finds measurably fewer defects. It answers with independent
|
|
805
824
|
models, then one referee: read back the referee's named fields (recommendation,
|
|
806
825
|
key disagreements, unchecked assumptions, what would change its mind, requested
|
|
807
|
-
evidence), not a paraphrase. On exit 4/5
|
|
808
|
-
member
|
|
826
|
+
evidence), not a paraphrase. On exit 4/5 record the first listed
|
|
827
|
+
`offer:`/`reoffer:` member of another model family yourself with `--use` and ask
|
|
828
|
+
again - never ask the user to choose; after two failed rounds say it was
|
|
829
|
+
skipped. Relay the failure
|
|
809
830
|
class and retry line it prints, verify every claim against the code, report
|
|
810
831
|
which models disagreed, and if nothing answered say the second opinion was
|
|
811
832
|
skipped. Skip trivial local edits.
|
|
@@ -869,7 +890,9 @@ evidence class - `user-verbatim`, `behaviour`, `ui-observed`, `code`, `external`
|
|
|
869
890
|
and a finding without one is a question, not a finding. Run it as a research line
|
|
870
891
|
(`{RESEARCH_BIN}`), say what it did not look at, and read the `product-analysis`
|
|
871
892
|
skill for the artifact shape and the scorecard, and `feature-audit` for the
|
|
872
|
-
layer-coherence matrices when the ask is one feature.
|
|
893
|
+
layer-coherence matrices when the ask is one feature. A new capability built from
|
|
894
|
+
data or an ask runs the `product-flow` skill: `{RESEARCH_BIN} next --topic <t>` is
|
|
895
|
+
the step source, and its first missing or due step is the next one. Off: `research-off`.
|
|
873
896
|
|
|
874
897
|
**No AI attribution, ever, on any host.** Nothing persisted or published may
|
|
875
898
|
name the assistant, model, vendor or "AI" as author/co-author/generator/helper:
|
|
@@ -920,7 +943,10 @@ its own branch and checkout - inside Orca `orca worktree create --name <slug>
|
|
|
920
943
|
--parent-worktree active --json`, else `git worktree add -b <branch> <absolute
|
|
921
944
|
path>`; its brief names every read, edit and shell `cwd` as an absolute path
|
|
922
945
|
under that checkout, never the main checkout, whose `git status --short` must
|
|
923
|
-
not grow. Read-only slices need no worktree.
|
|
946
|
+
not grow. Read-only slices need no worktree. Once a slice's branch is merged,
|
|
947
|
+
its worktree goes in the same turn - `tezgah-task close` removes every merged
|
|
948
|
+
one, and outside a plan `orca worktree rm --worktree path:<p>` (else `git
|
|
949
|
+
worktree remove <p>`) does it; never leave one for the user to find. Pick each agent by task class from
|
|
924
950
|
the specialists line; `{ROUTE_BIN}` names the tier for code work. Serialize
|
|
925
951
|
only a real dependency - one slice's output is another's input, or two slices
|
|
926
952
|
edit one file - and say which. Never wait idle on a spawned slice while
|
|
@@ -951,9 +977,11 @@ CONDITIONAL_KEYS = ("spec", "consult", "research", "product", "graph", "fanout")
|
|
|
951
977
|
# the session had to recall, and the second list of a session was worked one
|
|
952
978
|
# item at a time. It is the paragraph's own clause, cut to one sentence.
|
|
953
979
|
FANOUT_ACTION = ("count the independent items, spawn one subagent per item in "
|
|
954
|
-
"ONE message
|
|
955
|
-
"
|
|
956
|
-
"
|
|
980
|
+
"ONE message, each the agent `tezgah-route` names - a specialist "
|
|
981
|
+
"by name, never the generic task agent - (each writing slice in "
|
|
982
|
+
"its own worktree, absolute paths), and keep the main thread for "
|
|
983
|
+
"integrating and verifying; a subagent does its own slice and "
|
|
984
|
+
"never fans out again.")
|
|
957
985
|
|
|
958
986
|
# The always-on replacement for the conditional paragraphs: one line each so a
|
|
959
987
|
# host without a per-turn hook still knows the rule exists and where the full
|