@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.
Files changed (55) hide show
  1. package/CHANGELOG.md +96 -1
  2. package/MANIFEST +6 -1
  3. package/README.md +6 -4
  4. package/bin/consult +13 -10
  5. package/bin/tezgah-docs +1 -1
  6. package/bin/tezgah-research +53 -3
  7. package/bin/tezgah-route +8 -5
  8. package/bin/tezgah-setup +45 -25
  9. package/bin/tezgah-task +194 -41
  10. package/docs/architecture.md +1 -1
  11. package/docs/citations-baseline.json +1 -1
  12. package/docs/clarity-baseline.json +4 -4
  13. package/docs/contract.md +4 -1
  14. package/docs/feature-audit.md +24 -3
  15. package/docs/gate.md +10 -11
  16. package/docs/hosts.md +9 -0
  17. package/docs/index.json +21 -7
  18. package/docs/judge.md +3 -1
  19. package/docs/layers.md +1 -1
  20. package/docs/models.md +17 -7
  21. package/docs/operations.md +0 -1
  22. package/docs/research.md +46 -0
  23. package/docs/skills.md +23 -5
  24. package/docs/testing.md +1 -1
  25. package/hooks/tezgah_agents.py +130 -96
  26. package/hooks/tezgah_components.py +2 -1
  27. package/hooks/tezgah_context.py +67 -15
  28. package/hooks/tezgah_gate.py +62 -76
  29. package/hooks/tezgah_judge.py +15 -8
  30. package/hooks/tezgah_models.py +50 -3
  31. package/hooks/tezgah_paths.py +26 -11
  32. package/hooks/tezgah_policy.py +47 -19
  33. package/hooks/tezgah_product.py +107 -0
  34. package/hooks/tezgah_research.py +3 -3
  35. package/hooks/tezgah_skill_pick.py +58 -17
  36. package/hooks/tezgah_task.py +16 -8
  37. package/hosts/opencode/plugins/tezgah.js +17 -37
  38. package/package.json +1 -2
  39. package/skills/analyze-app/SKILL.md +57 -0
  40. package/skills/design-contract/EVALS.md +24 -1
  41. package/skills/design-contract/SKILL.md +17 -1
  42. package/skills/design-contract/data-views.md +86 -0
  43. package/skills/feature-audit/SKILL.md +32 -3
  44. package/skills/plan-add/EVALS.md +8 -8
  45. package/skills/plan-add/SKILL.md +20 -21
  46. package/skills/plan-sync/EVALS.md +2 -2
  47. package/skills/plan-sync/SKILL.md +11 -7
  48. package/skills/pm-frameworks/SKILL.md +6 -0
  49. package/skills/product-analysis/EVALS.md +23 -1
  50. package/skills/product-analysis/SKILL.md +50 -11
  51. package/skills/product-flow/EVALS.md +81 -0
  52. package/skills/product-flow/SKILL.md +91 -0
  53. package/skills/research/SKILL.md +10 -1
  54. package/skills/tezgah-contract/SKILL.md +67 -41
  55. package/agents/tezgah-reviewer.md +0 -79
@@ -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 the user's own, written by bin/tezgah-task and
42
- never by the agent, so the refusal leans on a decision the user made rather
43
- than one this gate inferred: no active task means no requirement, and the
44
- rule invents nothing. The record also carries where the phase returns to - a
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. Two further refusals close the ways a session could
50
- move the boundary instead of respecting it: a write whose target IS that
51
- record, and a shell command that would change it through the CLI. Both were
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 four.
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 fix - a credential, a "
685
- "login, an access grant - so stop and ask the user for it rather "
686
- "than change the approach",
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 user's own phase and path allowlist --------------------
984
- # What this closes: a session that has been told what it is working on - the
985
- # user activated a plan file under .tezgah/plans/open, so the work has a phase and the
986
- # files it may touch - and then writes anyway. Two shapes cost the user real
987
- # work: a write in `discovery`, where the whole point of the phase is to read
988
- # before deciding, and a write outside the plan's own allowlist, which is how a
989
- # bounded task quietly becomes an unbounded one. Both are the user's own
990
- # decisions, which is why this rule may refuse at all: the record is written by
991
- # bin/tezgah-task, by hand, and never by the agent - a rule the agent could
992
- # satisfy by editing its own record would be a nudge wearing a gate's clothes.
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), because a
996
- # refusal has to rest on an ask the user actually made: a rule that refused
997
- # every write until a record appeared would be the fail-closed trap the design
998
- # report names, with the whole session as its blast radius. Nothing is named
999
- # that the user did not name either: an empty allowlist is no scope asked for
1000
- # rather than "nothing is allowed", so the phase is the whole requirement there.
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. The phase is the user's to move: ask them, or do the "
1004
- "reading this phase asks for and say what the write was for.")
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). The "
1007
- "allowlist is the user's to widen: ask them, or leave the file alone and say "
1008
- "why it looked in scope.")
1009
- # The two refusals above deliberately name no command. They used to end with the
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, so a session that edits it is editing the boundary it is "
1017
- "being held to. Ask the user to change the task; the record is theirs.")
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
- "The phase is the user's to move: ask them, or do the reading this phase "
1057
- "asks for and say what the write was for.")
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 other route to the same hole as TASK_UNLOCK_DENY, and the one a write
1104
- tool takes: the record is a file in the repo, so an agent that may write
1105
- files can retype the phase instead of running the CLI. Refused whatever the
1106
- phase and whatever the allowlist says - the record is not inside its own
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. That is not the
1186
- # mistake TASK_PHASE_DENY records: the E7 lesson is a refusal that names how to
1187
- # take the constraint off (the phase is the user's to move), while the checkpoint
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: it is the task rule's (TASK_CHANGE), which `task-off`
1827
- # removes, and that switch is one of the files this rule protects.
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 user's own record for this work - its phase and the files
2655
- # its allowlist names (see task_reason). The
2656
- # record itself is refused first of all, and a shell that would change it
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:
@@ -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 namespaces the model `typesafe/<id>`; 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 if "/" in model else "typesafe/" + 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": {"*": "deny", "bash": "deny", "edit": "deny", "webfetch": "deny",
222
- "external_directory": "deny"},
223
- "tools": {"*": False}})},
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
- if only is None and session and (openrouter_key()
390
- or any(s for _, s in tp.jev_carriers())):
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)
@@ -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 "")}, {"tier": TIER_QUESTION}) if ask else None
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
- out = _pick(JEV_TIER[choice], "jev %s%s" % (
733
- choice, "" if chance is None else " %.2f" % chance), "jev")
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}
@@ -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` - lowercased. An unknown value is kept, so it resolves to
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, "").strip().lower()
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.strip().lower() if isinstance(value, str) else ""
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
- compatible = (os.environ.get("JEV_API_KEY", "").strip() or None
615
- if os.environ.get("JEV_API_BASE_URL", "").strip() else None)
616
- workers_ai = cloudflare_token() if cloudflare_account() else None
617
- found = [("typesafe", typesafe_key()), ("jev-openrouter", openrouter_key()),
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 found if secret]
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
- return [(name, dict(found).get(name))]
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
@@ -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, move the plan to `.tezgah/plans/done/`, and report the outcome. Bring it back
240
- to the user only on a critical or major finding left after round two, or a
241
- failing test. One review per plan, over its whole diff in the verification
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 goes to the user or is recorded fix-later, never a third round.
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 (overrides for stored data, credentials, security first; then a Jev
307
- judgement; the static phase table with no key). A worker that answers
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. Ask
404
- the user which to use with exactly those options (prefer a model family other
405
- than your own), record the answer with `{CONSULT_BIN} --use <member>[,<member>]`,
406
- then ask the question again. Never pick a member for the user.
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
- - ask the user to run `{ORX_BIN} login`.
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 ask the user which `offer:`/`reoffer:`
808
- member to use and record it with `--use` before asking again. Relay the failure
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. Off: `research-off`.
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. Pick each agent by task class from
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 (each writing slice in its own worktree, absolute "
955
- "paths), and keep the main thread for integrating and verifying;"
956
- " a subagent does its own slice and never fans out again.")
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