@r1z4x/tezgah 1.8.2 → 1.9.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.
@@ -471,9 +471,10 @@ def parallel(rows):
471
471
 
472
472
 
473
473
  def active(cwd, base, held=False):
474
- """The active task as a dict, or None. A linked worktree on a `plan/NNN-...`
475
- branch answers plan NNN when that plan carries a phase (`branch_task`), so
476
- two plans can run at once, one per worktree. Anywhere else - the main
474
+ """The active task as a dict, or None. A linked worktree on a branch that
475
+ binds a plan - `plan/NNN-...` or the plan's declared `branch:` - answers that
476
+ plan when it carries a phase (`branch_task`), so two plans can run at once,
477
+ one per worktree. Anywhere else - the main
477
478
  checkout, or a worktree on another branch - it reads the main checkout's
478
479
  `.tezgah/plans/open/*.md` sorted by name: the first plan carrying a valid
479
480
  `phase` that no linked worktree holds (`plan_holders`). When every phased
@@ -498,8 +499,9 @@ def active(cwd, base, held=False):
498
499
  main = linked_main(root) or root
499
500
  records = _phased(main)
500
501
  branches = _worktree_branches(main) if records else {}
502
+ declared = declared_branches(main) if branches else {}
501
503
  for record in records:
502
- if not any(_binds(record["id"], b) for b in branches):
504
+ if not any(_binds(record["id"], b, declared) for b in branches):
503
505
  return record
504
506
  if not held or not records:
505
507
  return None
@@ -570,19 +572,50 @@ def worktree_branch(top):
570
572
  return head[len(prefix):] if head.startswith(prefix) else None
571
573
 
572
574
 
573
- def plan_of_branch(branch):
574
- """The plan id a `plan/NNN-slug` branch names, else None."""
575
+ def declared_branches(main):
576
+ """{branch: plan id} for every open plan under the main checkout `main` that
577
+ declares a `branch:` - Orca names a worktree's branch `<user>/<slug>`, and
578
+ the plan declaring it is the one that worktree holds (plan 105). Read from
579
+ the plan files' frontmatter, never from git: the gate asks on every write.
580
+ Two plans declaring one branch: the lowest id binds it, so the answer never
581
+ depends on which file a directory listing names first."""
582
+ out = {}
583
+ for name, _, text in _plan_files(main, "open"):
584
+ fields = frontmatter(text)
585
+ branch, ident = fields.get("branch", ""), _plan_id(fields, name)
586
+ if branch and (branch not in out or ident < out[branch]):
587
+ out[branch] = ident
588
+ return out
589
+
590
+
591
+ def plan_of_branch(branch, main=None):
592
+ """The plan id `branch` binds. Without `main` it is the bare parse: NNN for
593
+ a `plan/NNN-slug` branch, else None. Given the main checkout `main`, it is
594
+ an open plan's id only: NNN when plan NNN is open there, else the open plan
595
+ declaring `branch` (`declared_branches`), else None - so `plan/999-nope`
596
+ binds nothing (review of plan 105, F3). One pass over the open plans."""
575
597
  found = _PLAN_BRANCH.match(branch or "")
576
- return found.group(1) if found else None
598
+ if not main:
599
+ return found.group(1) if found else None
600
+ declared = None
601
+ for name, _, text in _plan_files(main, "open") if branch else ():
602
+ fields = frontmatter(text)
603
+ ident = _plan_id(fields, name)
604
+ if found and ident == found.group(1):
605
+ return ident
606
+ if fields.get("branch") == branch and (declared is None or ident < declared):
607
+ declared = ident # the lowest id, as `declared_branches` picks
608
+ return declared
577
609
 
578
610
 
579
- def _binds(ident, branch):
580
- """Does `branch` hold plan `ident`? It has to name the plan: `plan/NNN-...`.
581
- A plan's declared `branch:` is one of those, and the slices it fans out to
582
- run on others (`plan/097-records` beside `plan/097-workspace-integrity`):
583
- each slice is held to its plan's scope, never to another plan's, which is
584
- what a first-unheld-plan reading gave it (plan 097, D6)."""
585
- return bool(branch) and plan_of_branch(branch) == ident
611
+ def _binds(ident, branch, declared):
612
+ """Does `branch` hold plan `ident`? It has to name the plan: `plan/NNN-...`,
613
+ or be the branch the plan declares (`declared`, `declared_branches`'s map).
614
+ The slices a plan fans out to run on other `plan/NNN-...` branches
615
+ (`plan/097-records` beside `plan/097-workspace-integrity`): each slice is
616
+ held to its plan's scope, never to another plan's, which is what a
617
+ first-unheld-plan reading gave it (plan 097, D6)."""
618
+ return bool(branch) and (plan_of_branch(branch) or declared.get(branch)) == ident
586
619
 
587
620
 
588
621
  def _worktree_branches(main):
@@ -606,28 +639,30 @@ def plan_holders(main):
606
639
  out = {}
607
640
  if not branches:
608
641
  return out
642
+ declared = declared_branches(main)
609
643
  for name, _, text in _plan_files(main, "open"):
610
644
  fields = frontmatter(text)
611
645
  ident = _plan_id(fields, name)
612
- held = sorted(b for b in branches if _binds(ident, b))
646
+ held = sorted(b for b in branches if _binds(ident, b, declared))
613
647
  if held:
614
- declared = fields.get("branch", "")
615
- out[ident] = branches[declared if declared in held else held[0]]
648
+ own = fields.get("branch", "")
649
+ out[ident] = branches[own if own in held else held[0]]
616
650
  return out
617
651
 
618
652
 
619
653
  def branch_task(top):
620
654
  """Plan NNN's record (active()'s dict, plus "worktree": `top`, the checkout
621
655
  whose HEAD and tree its checkpoint is read against) when `top` is a linked
622
- worktree on a `plan/NNN-...` branch (`_binds`) and plan NNN, open under the
623
- main checkout, carries a phase. None otherwise - another branch, a plan with
624
- no phase, an unknown mapping - and the caller reads the main checkout's task
656
+ worktree on a branch that binds plan NNN (`plan_of_branch`: `plan/NNN-...`
657
+ or the plan's declared `branch:`) and plan NNN, open under the main
658
+ checkout, carries a phase. None otherwise - another branch, a plan with no
659
+ phase, an unknown mapping - and the caller reads the main checkout's task
625
660
  (plan 093)."""
626
- ident = plan_of_branch(worktree_branch(top))
627
- if ident is None:
628
- return None
629
661
  from tezgah_paths import linked_main
630
662
  main = linked_main(top)
663
+ ident = plan_of_branch(worktree_branch(top), main)
664
+ if ident is None:
665
+ return None
631
666
  for record in _phased(main, ident) if main else ():
632
667
  record["worktree"] = top
633
668
  return record
@@ -636,7 +671,7 @@ def branch_task(top):
636
671
 
637
672
  def worktree_task(path, cwd, base):
638
673
  """The record a write to `path` is held to when it lands in a sibling linked
639
- worktree on a plan branch (`branch_task`), else None. A path under cwd's own
674
+ worktree on a plan's branch (`branch_task`), else None. A path under cwd's own
640
675
  root is active()'s to answer, so it pays one realpath and nothing more."""
641
676
  root = repo_root(cwd, base)
642
677
  path = os.path.expanduser(path)
@@ -883,6 +918,35 @@ def set_box(path, n, checked):
883
918
  return items[n - 1]
884
919
 
885
920
 
921
+ def set_unmet(path, n, reason):
922
+ """Append `UNMET: <reason>` to the last line of Acceptance item `n` (1-based,
923
+ as `tick` counts) and return the item, or None when the plan has no item `n`.
924
+
925
+ The marker has to land inside the item's own text - `open_items` reads it
926
+ there, and it is the predicate `close NNN done` refuses on - and an item
927
+ wraps onto the lines below its box, up to the next item or a blank line, so
928
+ the anchor is the item's last line and not its first (plan 110). Raises
929
+ OSError."""
930
+ with plans_lock(path):
931
+ lines = _lines(path)
932
+ items = acceptance_items("\n".join(lines))
933
+ if not 1 <= n <= len(items):
934
+ return None
935
+ end = items[n - 1]["line"] - 1
936
+ for i in range(end + 1, len(lines)):
937
+ # the section ends at the next `## ` heading as well, which is where
938
+ # `acceptance_items` stops: without it an item that ended straight at
939
+ # a heading swallowed the line under it into the item's own text
940
+ # (review of plan 110, F1)
941
+ if (not lines[i].strip() or _ITEM.match(lines[i])
942
+ or lines[i].lstrip().startswith("## ")):
943
+ break
944
+ end = i
945
+ lines[end] = lines[end].rstrip() + " UNMET: " + reason
946
+ _write(path, "\n".join(lines))
947
+ return acceptance_items("\n".join(lines))[n - 1]
948
+
949
+
886
950
  def set_section(path, heading, body):
887
951
  """Replace the body of the last `## <heading>` section - the newest, since
888
952
  a plan appends a dated `## State (...)` - with `body` and one blank line,
@@ -915,10 +979,8 @@ def set_section(path, heading, body):
915
979
 
916
980
  SLICE_KINDS = ("agent", "job")
917
981
  SLICE_HOSTS = ("omp", "claude", "codex", "cursor", "opencode", "dsh")
918
- # `stale`: no terminal status was observed within SLICE_IDLE_SECONDS - a host
919
- # that reports none (Codex `spawn_agent` returns at once and tezgah sees no child
920
- # event) or a child that died without writing its end. It leaves the pending
921
- # list without being read as finished or as failed.
982
+ # `stale`: an idle background job whose result stays in its parent transcript.
983
+ # An agent with no observed end stays owed as stopped with no failure class.
922
984
  SLICE_STATUSES = ("open", "done", "stopped", "resumed", "stale")
923
985
  SLICE_CLOSED = ("done", "stopped", "resumed", "stale")
924
986
  SLICE_IDLE_SECONDS = 6 * 3600
@@ -1372,11 +1434,12 @@ def open_slice(root, sid, *, kind="agent", host="", agent="", brief="", parent="
1372
1434
  # prompt and Stop - never globs (40 ms on 1393 project dirs, measured)
1373
1435
  rec["transcript_dir"] = _omp_session_dir(rec["parent"]) or ""
1374
1436
  # the slice's own plan: the review guard reads its own plan's records only.
1375
- # Its worktree's plan branch, else the plan its task or shared brief names,
1376
- # else the tag a re-open already carries, else the task the main checkout
1377
- # reads - with two plans running that is only one of them
1437
+ # Its worktree's plan branch (`plan/NNN-...` or a plan's declared one), else
1438
+ # the plan its task or shared brief names, else the tag a re-open already
1439
+ # carries, else the task the main checkout reads - with two plans running
1440
+ # that is only one of them
1378
1441
  named = _BRIEF_PLAN.search(own) or _BRIEF_PLAN.search(brief or "")
1379
- rec["plan"] = (plan_of_branch(rec.get("branch")) or (named and named.group(1))
1442
+ rec["plan"] = (plan_of_branch(rec.get("branch"), main) or (named and named.group(1))
1380
1443
  or rec.get("plan") or (active(main, main) or {}).get("id") or "")
1381
1444
  rec.update(base=_git(main, "rev-parse", "HEAD") or "", status="open",
1382
1445
  failure="", updated=now)
@@ -1649,8 +1712,9 @@ def pending(root):
1649
1712
  omp record still open whose child transcript has ended is closed first:
1650
1713
  stopped with its failure class, or done when it ended normally. An open
1651
1714
  record nothing has moved for SLICE_IDLE_SECONDS (an omp child's transcript
1652
- included) - or a `job` past SLICE_JOB_SECONDS - closes `stale`. One stat and
1653
- at most one SLICE_TAIL read per open omp record."""
1715
+ included) closes stopped with no failure class; only a `job` past
1716
+ SLICE_JOB_SECONDS closes stale. One stat and at most one SLICE_TAIL read per
1717
+ open omp record."""
1654
1718
  out = []
1655
1719
  for rec in slices(root):
1656
1720
  if rec.get("status") != "open":
@@ -1680,7 +1744,12 @@ def pending(root):
1680
1744
  except OSError:
1681
1745
  pass
1682
1746
  if rec.get("status") == "open" and idle is not None and idle > limit:
1683
- rec = close_slice(root, rec["id"], "stale") or rec
1747
+ # A job's result stays in its parent transcript; an agent with no
1748
+ # observed end stays stopped and offered for resume. Unknown is not
1749
+ # cut_off: slice_stats skips a class it cannot name, so signature
1750
+ # counts go quiet for hosts whose ends tezgah cannot observe.
1751
+ job = rec.get("kind") == "job"
1752
+ rec = close_slice(root, rec["id"], "stale" if job else "stopped", "") or rec
1684
1753
  if rec.get("status") in ("open", "stopped"):
1685
1754
  out.append(rec)
1686
1755
  return out
@@ -3201,3 +3201,139 @@ export const Tezgah = async ({ directory }) => {
3201
3201
  // (TEZGAH_STATUS_BIN) is the safe path today.
3202
3202
  }
3203
3203
  }
3204
+
3205
+ // opencode 2 validates a plugin module's DEFAULT export: an object with a string
3206
+ // `id` and a `setup(ctx)` that registers hooks through the v2 context - it calls
3207
+ // nothing else, so a module with only the callable above is rejected at load
3208
+ // with `PluginModule.LoadError: Plugin must export a default definition with an
3209
+ // id and an effect or setup function` (measured on opencode v2.0.30, and the
3210
+ // reason the host showed both entrypoints as failed plugins). opencode 1 calls
3211
+ // the exported callable and uses the hook map it returns. One file serves both,
3212
+ // as the sibling plugin on this machine does
3213
+ // (~/.config/opencode/plugins/orca-opencode-status.js).
3214
+ //
3215
+ // v2 merged the v1 `input` and `output` objects into ONE mutable event
3216
+ // (services/www/src/docs/content/build/plugins/migrate-v1.mdx), so the adapter
3217
+ // passes the event as the v1 `input` and rebuilds the v1 `output` field the v1
3218
+ // callback reads (`args` from `output.args`). The fields below are the ones a
3219
+ // probe plugin measured on opencode v2.0.30: `execute.before` carries
3220
+ // {tool, sessionID, agent, messageID, id, input}. A refusal is a thrown Error in
3221
+ // both generations and the v1 callback already throws it, so the deny path is
3222
+ // the same object and no decision is rewritten here.
3223
+ //
3224
+ // Registered in this piece: the tool envelopes (the enforcement path), the shell
3225
+ // env hook, the session prompt and the event subscription. permission/evaluate,
3226
+ // the compaction hook and the TUI plugin still need their own probe of the v2
3227
+ // event before they are registered - guessing a field there would silently drop a
3228
+ // decision.
3229
+ export default {
3230
+ id: "tezgah",
3231
+
3232
+ async setup(ctx) {
3233
+ const directory = (ctx.location && ctx.location.directory) || process.cwd()
3234
+ const hooks = await Tezgah({ directory })
3235
+ // The same double-load guard the callable keeps: opencode 1 and 2 both scan
3236
+ // `plugin/` and `plugins/`, and the second instance returns no hook map. A
3237
+ // registration without a hook would raise at load and fail the whole plugin,
3238
+ // which is the one state this file must never reach.
3239
+ if (!hooks || typeof hooks !== "object") return
3240
+
3241
+ const before = hooks["tool.execute.before"]
3242
+ if (typeof before === "function") {
3243
+ await ctx.tool.hook("execute.before", (event) =>
3244
+ before(event, { args: event && event.input }))
3245
+ }
3246
+
3247
+ const after = hooks["tool.execute.after"]
3248
+ if (typeof after === "function") {
3249
+ await ctx.tool.hook("execute.after", (event) => {
3250
+ // On the "error" status the v2 event carries a Tool.Error, not a
3251
+ // result object, so the v1 labels (labelResult) and rows
3252
+ // (recordEvidence) have no carrier for a failed call: skipped, not
3253
+ // faked. On "completed" it translates onto the v1 pair: the callback
3254
+ // reads `tool` and `sessionID` from its input, `args` from
3255
+ // `input.args` and the result from its second argument, which it
3256
+ // labels in place.
3257
+ if (event && event.status === "completed")
3258
+ after({ sessionID: event.sessionID, tool: event.tool,
3259
+ args: event.input }, event.result)
3260
+ })
3261
+ }
3262
+
3263
+ const chatMessage = hooks["chat.message"]
3264
+ if (typeof chatMessage === "function" && ctx.session && ctx.session.hook) {
3265
+ // v2 hands the plugin `{sessionID, messageID, prompt: {text, files}}` and
3266
+ // gives it no parts array, while the v1 callback reaches the model by
3267
+ // pushing a synthetic text part. So the adapter builds the v1 `parts`
3268
+ // array from the prompt text, runs the same callback, and appends whatever
3269
+ // it pushed to the v2 prompt text: the reminder lands in the message either
3270
+ // way, and the decision (which rule it arms) is the callback's own.
3271
+ await ctx.session.hook("prompt", async (event) => {
3272
+ if (!event) return
3273
+ const prompt = event.prompt || (event.prompt = {})
3274
+ const parts = [{ type: "text", text: String(prompt.text || "") }]
3275
+ await chatMessage({ sessionID: event.sessionID, messageID: event.messageID },
3276
+ { parts, message: { id: event.messageID, sessionID: event.sessionID } })
3277
+ const extra = parts.slice(1).map((p) => p && p.text).filter(Boolean).join("\n")
3278
+ if (extra) prompt.text = (prompt.text ? prompt.text + "\n" : "") + extra
3279
+ })
3280
+ }
3281
+
3282
+ const shellEnv = hooks["shell.env"]
3283
+ if (typeof shellEnv === "function") {
3284
+ // v2's shell event carries `env` in place, and v1's callback fills
3285
+ // `output.env`, so one object serves both roles. The session id sits under
3286
+ // `metadata` at runtime rather than on the event, and the v1 callback reads
3287
+ // `input.sessionID`, so it is lifted where it exists (review of plan 111,
3288
+ // F3): a shell event with neither skips the session variable, as it did
3289
+ // before.
3290
+ await ctx.shell.hook("create.before", (event) => shellEnv(
3291
+ Object.assign({}, event,
3292
+ { sessionID: event.sessionID || (event.metadata && event.metadata.sessionID) }),
3293
+ event))
3294
+ }
3295
+
3296
+ // The v1 `event` hook joins a subagent's ledger to its parent's on
3297
+ // `session.created`, and v2 delivers host events through a subscription
3298
+ // rather than a callback, so this block consumes it.
3299
+ if (typeof hooks.event === "function" && ctx.event && ctx.event.subscribe) {
3300
+ // v2's subscription returns an async iterable of {type, data} and never
3301
+ // calls a callback - the sibling plugin consumes it with `for await` - so
3302
+ // this reads it the same way and hands the v1 callback the shape it reads
3303
+ // (`input.event`, with the v1 `properties`). The loop runs for the life of
3304
+ // the plugin; a failed subscription costs the v1 event hook and nothing
3305
+ // else. Review of plan 111, F1.
3306
+ const consume = async () => {
3307
+ // `await` because the subscription may hand back a promise of the
3308
+ // iterable: `for await` takes the iterable itself, and on a promise it
3309
+ // throws where a bare `catch` would hide it (review of plan 111, F1).
3310
+ for await (const input of await ctx.event.subscribe({})) {
3311
+ if (!input || !input.type) continue
3312
+ try {
3313
+ await hooks.event({ event: { type: input.type, properties: input.data } })
3314
+ } catch {}
3315
+ }
3316
+ }
3317
+ consume().catch(() => {})
3318
+ }
3319
+
3320
+ // Not registered on v2, each with its reason rather than a stub:
3321
+ //
3322
+ // `permission.ask` - the v1 callback decides from the call's tool and its
3323
+ // args (`permissionToolArgs`: metadata.tool, metadata.pattern, the pattern
3324
+ // list), and v2's evaluate event carries `action`, `resources`, `metadata`
3325
+ // and `source` instead; whether the host fills `metadata` with the tool and
3326
+ // the pattern is unverified, and a wrong reading there would deny a call the
3327
+ // before-hook allows. The same decisions are already enforced by
3328
+ // `tool.execute.before`, which is registered above and runs on every call,
3329
+ // so nothing is left unenforced - only the redundancy is dropped.
3330
+ // `experimental.session.compacting` - v1 reaches the model by mutating
3331
+ // `output.parts`; v2's compaction event carries `messages` and a
3332
+ // `result = {summary}` to set, so this is a different hook, not a rename.
3333
+ // `config` - v1 mutates one global config object to register the repository's
3334
+ // generated subagents; v2 has no global config and registers them through
3335
+ // `ctx.agent.transform`, which needs the agents' own shape.
3336
+ // The TUI plugin (`hosts/opencode/tui/`) has its own v2 definition to port.
3337
+ },
3338
+ }
3339
+
@@ -66,7 +66,9 @@ examples. Off: `tezgah-adhd off`, or the repo's `.no-adhd`.
66
66
 
67
67
  **Deliver the whole ask; never the shortcut.** The request defines the
68
68
  deliverable: every named item is in scope until the user says otherwise, and the
69
- ask is a floor, not a ceiling. Ponytail shrinks the solution, never the request.
69
+ ask is a floor, not a ceiling - its kind is one: a research, analysis, review or
70
+ plan ask ends at the written plan, and building it waits for the user's answer.
71
+ Ponytail shrinks the solution, never the request.
70
72
  FORBIDDEN: swapping in a cheaper, deferred or partial stand-in for what was
71
73
  asked; silently narrowing scope; deciding a requested item is
72
74
  "unnecessary"/"YAGNI" and dropping it; a token gesture reported as done; stopping
@@ -110,7 +112,8 @@ lines: item - blocker - evidence - what unblocks it. No blockers: no heading.
110
112
  Work and process - the order, "shall I go on", a check, a plan, a consult
111
113
  member, a spend within `spend_cap`, a local merge of the session's branch,
112
114
  removing its worktree - are taken and stated in one line, never asked; a taste
113
- or intent fork goes in the batch with a default taken this turn. An outward step
115
+ or intent fork goes in the batch with a default taken this turn, but building a
116
+ plan this turn wrote waits for the answer. An outward step
114
117
  on the user's own remote (push, PR, release, their package) runs on a grant
115
118
  ("push without asking", "I approve", "bana sormadan pushla", "onay veriyorum"), else is asked once. Never on a
116
119
  default - ask and wait for an answer naming action and target: a force push,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@r1z4x/tezgah",
3
- "version": "1.8.2",
3
+ "version": "1.9.0",
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": {
@@ -96,14 +96,18 @@ Input: `$ARGUMENTS` (free text describing the work). Run these steps in order.
96
96
  the scope with `tezgah-task allow <glob> ... --why "<reason>"`, which records the reason. Never retype the record:
97
97
  the gate refuses a write to it, because the CLI's checks would be skipped.
98
98
  Tick an Acceptance box with `tezgah-task tick NNN <n>` (`--undo` unticks),
99
- rewrite State or Next with `tezgah-task note NNN state|next "<text>"`, and
99
+ rewrite State or Next with `tezgah-task note NNN state|next "<text>"`,
100
100
  record the spike's answer with `tezgah-task spike NNN "<answer>"`, which
101
- writes `spike_recorded:` on the plan, active or not. These three work from
102
- the main checkout or from any of its worktrees.
101
+ writes `spike_recorded:` on the plan, active or not, and annotate an item the
102
+ owner accepted as not met with `tezgah-task unmet NNN <n> "<reason, who
103
+ accepted it>"`, which writes `UNMET:` and is the form `close done` takes in
104
+ place of a tick. These four work from the main checkout or from any of its
105
+ worktrees.
103
106
  After the merge, `tezgah-task close NNN done` deactivates the plan,
104
107
  moves it to done and removes every merged worktree. It refuses while an
105
108
  Acceptance box is open: tick it, or, only when the owner accepted the item
106
- as not met, end it with `UNMET: <reason, who accepted it>`.
109
+ as not met, end it with `tezgah-task unmet NNN <n> "<reason, who accepted
110
+ it>"`, which writes the `UNMET:` line and leaves the box open.
107
111
 
108
112
  10. If the work settles a decision that is hard to reverse, spans components, or
109
113
  keeps being re-explained, record it as an ADR under `.tezgah/decisions/` with
@@ -266,6 +266,20 @@ code-discovery brief names codegraph's tools (`codegraph callers`, `callees`,
266
266
  the graph, grep only for literal text"; never a grep-only explorer (observed).
267
267
  Subagents never orchestrate: no nested harnesses, no sub-subagents.
268
268
 
269
+ ### The verification batch: `tezgah-optimize`
270
+ `~/.config/tezgah/bin/tezgah-optimize next [--files PATH ...]` answers, per command
271
+ the active plan's Acceptance names, one of `run now`, `defer` or `already green` -
272
+ the first from no measurement or a median below the in-turn bar, the second from a
273
+ median at or above it, the third from a green row recorded for the same sha. The
274
+ router calls it at a phase move and before it spends a check the same change
275
+ already passed, and calls `push` once at the end of a step instead of pushing
276
+ every iteration: a build or a CI run paid once per attempt is the cost this
277
+ decides. `record <command> --seconds N --exit N` is how a measurement gets in, so
278
+ the session that runs a check hands the number back. The tiers are measured, not
279
+ declared, so a project that never adopted this repository's `AGENTS.md` still gets
280
+ a batch. See `docs/testing.md` and, for the decision behind it, the research line
281
+ `tezgah-optimization`.
282
+
269
283
  ### Tier 2 - codegen (cheap model via OpenRouter)
270
284
  `~/.config/tezgah/bin/codegen "task" --files a.py b.py` drafts a bounded edit on a cheap model
271
285
  (override with --model or CODEGEN_MODEL) for a fraction of the cost. On Claude