@r1z4x/tezgah 1.4.0 → 1.5.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.
@@ -21,14 +21,19 @@ before it happens is decided once. This module is on the gate's hot path for
21
21
  every write, so it imports nothing from tezgah_context and shells out to
22
22
  nothing.
23
23
 
24
- A plan's body is read here too, and only here: the acceptance report
24
+ A plan's body is read here too: the acceptance report
25
25
  (`tezgah-render-table --acceptance`) counts the items that name no command, so
26
26
  the record the gate reads and the items the report counts have one reader. Two
27
27
  readers of the body were added beside it and read in the same place - the
28
- `## Acceptance` items a plan cannot enter a writing phase without
29
- (`acceptance_gap`), and the spike a plan declares and has not answered
30
- (`spike_unanswered`) - for the same reason: the CLI refuses the phase move and
31
- the CI report counts the same items, and one module keeps them agreeing.
28
+ `## Acceptance` items a plan cannot enter a writing phase with while any of them
29
+ names no command (`acceptance_gap`, the predicate `--strict` refuses on too),
30
+ and the spike a plan declares and has not answered (`spike_unanswered`) - for
31
+ the same reason: the CLI refuses the phase move and the CI report counts the
32
+ same items, and one module keeps them agreeing. The open plans' progress (the
33
+ ticked boxes), the plans each one waits on (`after:`) and which ready plans can
34
+ run at once (`queue`, `parallel`) are read here for `tezgah-task status`. The
35
+ injected open-plans block keeps its own small reader
36
+ (`hooks/tezgah_context.py::_plan_row`: id, title, the first `## Next` line).
32
37
 
33
38
  The `checkpoint:` key is read here for the same reason again: bin/tezgah-task
34
39
  writes it when the phase moves to `implementation` - the pre-work commit's sha
@@ -100,8 +105,9 @@ def frontmatter(text):
100
105
  return out
101
106
 
102
107
 
103
- def allowed_paths(text):
104
- """The `allowed_paths:` list as a list of globs, [] when absent."""
108
+ def _list_field(text, key):
109
+ """One frontmatter key's `- item` block as a list of strings, [] when the key
110
+ is absent or carries no block."""
105
111
  out = []
106
112
  collecting = False
107
113
  for line in _frontmatter_lines(text):
@@ -110,10 +116,24 @@ def allowed_paths(text):
110
116
  out.append(stripped[2:].strip())
111
117
  continue
112
118
  collecting = (not stripped.startswith("- ") and ":" in stripped
113
- and stripped.partition(":")[0].strip() == "allowed_paths")
119
+ and stripped.partition(":")[0].strip() == key)
114
120
  return out
115
121
 
116
122
 
123
+ def allowed_paths(text):
124
+ """The `allowed_paths:` list as a list of globs, [] when absent."""
125
+ return _list_field(text, "allowed_paths")
126
+
127
+
128
+ def after(text):
129
+ """The plan ids this plan waits on (`after:`), in the order written: the
130
+ `- NNN` block `set_fields` writes, or the inline `after: 012, 015` a person
131
+ types. [] when the plan waits on nothing - which is what lets it run beside
132
+ the others."""
133
+ return (_list_field(text, "after")
134
+ or frontmatter(text).get("after", "").replace(",", " ").split())
135
+
136
+
117
137
  def section_lines(text, heading):
118
138
  """The lines of one `## <heading>` section with their 1-based numbers, []
119
139
  when the file has no such heading: the lines after the heading up to the next
@@ -129,7 +149,7 @@ def section_lines(text, heading):
129
149
  return out
130
150
 
131
151
 
132
- _ITEM = re.compile(r"^\s*- \[[ xX]\]\s*(.*)$")
152
+ _ITEM = re.compile(r"^\s*- \[([ xX])\]\s*(.*)$")
133
153
  # The word `unverifiable`, as a word: the lookahead is a boundary a hyphen and a
134
154
  # letter both fail, so `unverifiable-ness` is not a marker.
135
155
  _MARKER = re.compile(r"unverifiable(?![-\w])", re.I)
@@ -191,10 +211,11 @@ def unverifiable_reason(text):
191
211
 
192
212
  def acceptance_items(text):
193
213
  """One plan file's `## Acceptance` items: a list of
194
- {"line", "text", "state"}, plus "reason" when the state is "unverifiable".
195
- One field and not two - "checkable" (the item names a command),
196
- "unverifiable" (it says so and why) or "missing" (neither) - so no caller can
197
- read two answers about one item.
214
+ {"line", "text", "checked", "state"}, plus "reason" when the state is
215
+ "unverifiable". One field and not two - "checkable" (the item names a
216
+ command), "unverifiable" (it says so and why) or "missing" (neither) - so no
217
+ caller can read two answers about one item. "checked" is the box (`- [x]`),
218
+ which is the plan's progress and says nothing about the item's proof.
198
219
 
199
220
  An item is one `- [ ]`/`- [x]` line plus the lines it wraps onto, up to the
200
221
  next item or a blank line: the format wraps at 79 columns, so an item is more
@@ -204,8 +225,8 @@ def acceptance_items(text):
204
225
  for number, line in section_lines(text, "Acceptance"):
205
226
  found = _ITEM.match(line)
206
227
  if found:
207
- out.append({"line": number, "text": " ".join(found.group(1).split()),
208
- "state": "missing"})
228
+ out.append({"line": number, "text": " ".join(found.group(2).split()),
229
+ "checked": found.group(1) != " ", "state": "missing"})
209
230
  elif out and line.strip():
210
231
  out[-1]["text"] = " ".join((out[-1]["text"] + " " + line).split())
211
232
  for item in out:
@@ -217,19 +238,27 @@ def acceptance_items(text):
217
238
  return out
218
239
 
219
240
 
241
+ def unproven(items):
242
+ """The items that name no command and declare no `unverifiable` - the one
243
+ predicate both the phase move (`acceptance_gap`) and `render_table
244
+ --acceptance --strict` refuse on, so the two cannot let through different
245
+ plans."""
246
+ return [item for item in items if item["state"] == "missing"]
247
+
248
+
220
249
  def acceptance_gap(text):
221
- """The plan's Acceptance items when nothing in them can check the work: every
222
- item the section carries is `missing`, so none names a command and none
223
- declares itself `unverifiable`. [] otherwise, and [] when the plan has no
224
- Acceptance items at all.
250
+ """The plan's Acceptance items that nothing can check: each one names no
251
+ command and declares no `unverifiable`. [] when every item names its proof,
252
+ and [] when the plan has no Acceptance items at all.
225
253
 
226
- A plan with an empty or absent section is a different defect - the format asks
227
- plan-add to write the items, and the render_table report does not gate that
228
- case either - so refusing it here would tax every plan that predates the rule
229
- while the two readers still agreed about every item either of them can see.
230
- Returns the items so a refusal can count them."""
231
- items = acceptance_items(text)
232
- return items if items and all(item["state"] == "missing" for item in items) else []
254
+ Any such item is a gap, not only a section made of them: the format asks for
255
+ the proof per item, and the strict report refuses an open plan on one, so a
256
+ phase move that let one through would be refused later by the reader it was
257
+ meant to answer first. A plan with an empty or absent section is a different
258
+ defect - the format asks plan-add to write the items, and the report does not
259
+ gate that case either - so refusing it here would tax every plan that
260
+ predates the rule. Returns the items so a refusal can name them."""
261
+ return unproven(acceptance_items(text))
233
262
 
234
263
 
235
264
  # The `checkpoint:` values a plan can carry: a commit sha (the tree was clean at
@@ -295,6 +324,30 @@ def spike_unanswered(text):
295
324
 
296
325
 
297
326
  ACCEPTANCE_DIRS = ("open", "done")
327
+ # The `status:` a plan carries in each directory (plan-add's format): a plan
328
+ # under open/ is open or blocked, one under done/ is done or discarded.
329
+ OPEN_STATUSES = ("open", "blocked")
330
+
331
+
332
+ def _plan_files(root, sub):
333
+ """(name, path, text) for every readable `*.md` under
334
+ `<root>/.tezgah/plans/<sub>`, sorted by name; an absent directory or an
335
+ unreadable file is skipped, so a reader built on it never raises."""
336
+ directory = os.path.join(root, ".tezgah", "plans", sub)
337
+ try:
338
+ names = sorted(os.listdir(directory))
339
+ except OSError:
340
+ return
341
+ for name in names:
342
+ if not name.endswith(".md"):
343
+ continue
344
+ path = os.path.join(directory, name)
345
+ try:
346
+ with open(path, "r", encoding="utf-8") as fh:
347
+ text = fh.read()
348
+ except OSError:
349
+ continue
350
+ yield name, path, text
298
351
 
299
352
 
300
353
  def acceptance_report(root):
@@ -310,20 +363,7 @@ def acceptance_report(root):
310
363
  items = []
311
364
  plans = 0
312
365
  for sub in ACCEPTANCE_DIRS:
313
- directory = os.path.join(root, ".tezgah", "plans", sub)
314
- try:
315
- names = sorted(os.listdir(directory))
316
- except OSError:
317
- continue
318
- for name in names:
319
- if not name.endswith(".md"):
320
- continue
321
- path = os.path.join(directory, name)
322
- try:
323
- with open(path, "r", encoding="utf-8") as fh:
324
- text = fh.read()
325
- except OSError:
326
- continue
366
+ for name, _, text in _plan_files(root, sub):
327
367
  plans += 1
328
368
  for item in acceptance_items(text):
329
369
  item["plan"] = ".tezgah/plans/%s/%s" % (sub, name)
@@ -331,6 +371,72 @@ def acceptance_report(root):
331
371
  return {"plans": plans, "items": items}
332
372
 
333
373
 
374
+ def _plan_id(fields, name):
375
+ return fields.get("id") or name.split("-")[0]
376
+
377
+
378
+ def _prefix(glob):
379
+ """The literal head of a glob: everything before its first `*`."""
380
+ return glob.split("*", 1)[0]
381
+
382
+
383
+ def scopes_overlap(first, second):
384
+ """Could two allowlists name one file? An empty list is every path, and two
385
+ globs can only meet when one literal head is a prefix of the other's - so
386
+ `hooks/a*.py` and `hooks/b*.py` never do, and `**/x` meets everything.
387
+ ponytail: prefix test, not glob intersection - it says "overlap" for
388
+ `hooks/*.py` against `hooks/x/y.md`, which only costs a missed pairing."""
389
+ if not first or not second:
390
+ return True
391
+ return any(a.startswith(b) or b.startswith(a)
392
+ for a in map(_prefix, first) for b in map(_prefix, second))
393
+
394
+
395
+ def queue(root):
396
+ """Every open plan as a row: {"id", "status", "path", "checked", "items" (the
397
+ Acceptance boxes ticked, and their count), "after" ([id, state] per plan it
398
+ waits on: `done`, `discarded`, `open` or `missing` when no plan carries the
399
+ id), "ready" (not blocked, and everything it waits on is done), "overlaps"
400
+ (the other open plans whose allowed_paths can name one of its files)}.
401
+
402
+ The plans' progress and order had no reader: the boxes were counted nowhere
403
+ and nothing said which plans could run beside each other. Never raises."""
404
+ state = {}
405
+ for name, _, text in _plan_files(root, "done"):
406
+ fields = frontmatter(text)
407
+ state[_plan_id(fields, name)] = ("discarded" if fields.get("status") == "discarded"
408
+ else "done")
409
+ rows = []
410
+ scopes = {}
411
+ for name, path, text in _plan_files(root, "open"):
412
+ fields = frontmatter(text)
413
+ ident = _plan_id(fields, name)
414
+ state[ident] = "open"
415
+ items = acceptance_items(text)
416
+ scopes[ident] = allowed_paths(text)
417
+ rows.append({"id": ident, "status": fields.get("status", ""), "path": path,
418
+ "checked": sum(1 for item in items if item["checked"]),
419
+ "items": len(items), "after": after(text)})
420
+ for row in rows:
421
+ row["after"] = [[ident, state.get(ident, "missing")] for ident in row["after"]]
422
+ row["ready"] = (row["status"] != "blocked"
423
+ and all(dep == "done" for _, dep in row["after"]))
424
+ row["overlaps"] = [other["id"] for other in rows if other is not row
425
+ and scopes_overlap(scopes[row["id"]], scopes[other["id"]])]
426
+ return rows
427
+
428
+
429
+ def parallel(rows):
430
+ """The ready plans that can run at once, in id order: each one shares no
431
+ allowed_paths prefix with any other picked (greedy, first id first). [] when
432
+ fewer than two qualify - one plan is not a fan-out."""
433
+ picked = []
434
+ for row in rows:
435
+ if row["ready"] and not any(other in row["overlaps"] for other in picked):
436
+ picked.append(row["id"])
437
+ return picked if len(picked) > 1 else []
438
+
439
+
334
440
  def active(cwd, base):
335
441
  """The active task as a dict, or None. Scans `<repo_root>/.tezgah/plans/open/*.md`
336
442
  sorted by name, first file whose frontmatter carries a valid `phase`.
@@ -218,7 +218,7 @@ def learn_later(root):
218
218
  signals of `root` (the git root `enabled` returned) grew since the last
219
219
  start and an hour has passed; the stamp (a `taste_learn` document of the
220
220
  cache database) keeps the newest signal's append number. Only when
221
- TypeSafe resolves: the automatic run never pays a
221
+ the Jev model resolves through any carrier: the automatic run never pays a
222
222
  generative provider for a decision it may not apply. Not on a
223
223
  machine without flock (Windows), where two background runs in two
224
224
  repositories would race on the user ledger. ponytail: no Windows lock; add
@@ -243,7 +243,7 @@ def learn_later(root):
243
243
  import tezgah_judge
244
244
  import tezgah_taste_ledger
245
245
  if tezgah_taste_ledger.fcntl is None or tezgah_paths.off("judge-off") \
246
- or not tezgah_judge.named(("typesafe",)):
246
+ or not tezgah_judge.named(("jev",)):
247
247
  return
248
248
  if not store.put_doc("taste_learn", name, {"rows": count, "at": now}):
249
249
  return
@@ -135,6 +135,15 @@ def rows(root, table, where="", args=()):
135
135
  return store.rows(db, table, where, args) if db else []
136
136
 
137
137
 
138
+ def jev_rows():
139
+ """(where, args) for `rows`: the rows a Jev carrier (`tezgah_judge.JEV_CARRIERS`)
140
+ decided, whichever one answered - an old `typesafe` row included. Imported
141
+ here, not at the top: the write hook loads this module and never asks."""
142
+ import tezgah_judge
143
+ carriers = tuple(tezgah_judge.JEV_CARRIERS)
144
+ return "WHERE provider IN (%s)" % ", ".join("?" * len(carriers)), carriers
145
+
146
+
138
147
  def append(root, table, row, strict=False):
139
148
  """One row into a taste table. A hook drops a row it cannot write; `strict`
140
149
  (the CLI's decision, defect and label rows) raises instead, so `learn` stops
@@ -193,10 +202,10 @@ class locked:
193
202
  def calibration(root):
194
203
  """(x, n, bound): of the labelled signals the typed decision called a
195
204
  preference, how many the label agrees with, and the Clopper-Pearson lower
196
- bound on that precision. Only typed-provider decisions count; the labels are
205
+ bound on that precision. Only Jev decisions count; the labels are
197
206
  a sample drawn independently of what was injected (`tezgah-taste label`)."""
198
207
  x = n = 0
199
- for row in rows(root, "labels", "WHERE provider = ?", ("typesafe",)):
208
+ for row in rows(root, "labels", *jev_rows()):
200
209
  if row.get("decided") != "preference":
201
210
  continue
202
211
  n += 1
@@ -258,7 +267,7 @@ def apply(ledgers, sig, dec, day, applied=()):
258
267
 
259
268
  `dec` is the typed decision: `kind`, `category`, `scope` as (choice,
260
269
  probability), `relations` {learning id: (relation, probability)} and
261
- `verified`, True only when TypeSafe answered - any other provider's decision
270
+ `verified`, True only when the Jev model answered - any other provider's decision
262
271
  is never applied. `applied` are the ids injected in the signal's session."""
263
272
  if not dec.get("verified"):
264
273
  return "unverified"
@@ -571,7 +580,7 @@ def gate(root, need=980, alpha=0.1):
571
580
  lower at one-sided p < `alpha`. Returns the report."""
572
581
  first = first_injection(root)
573
582
  arms = {"before": [0, 0], "after": [0, 0]}
574
- for row in rows(root, "decisions", "WHERE provider = ?", ("typesafe",)):
583
+ for row in rows(root, "decisions", *jev_rows()):
575
584
  at = row.get("at")
576
585
  if not isinstance(at, (int, float)):
577
586
  continue
@@ -158,6 +158,11 @@ def main():
158
158
  event = payload.get("hook_event_name") or "SessionStart"
159
159
  cwd = payload.get("cwd") or os.getcwd()
160
160
  session_id = payload.get("session_id")
161
+ # codex hands its hooks its own env, which carries no codex marker (only the
162
+ # shell tool gets CODEX_THREAD_ID), so the judge seam's session_cli() could
163
+ # not tell this is a codex session without it.
164
+ if session_id:
165
+ os.environ.setdefault("CODEX_THREAD_ID", str(session_id))
161
166
  safe(session_id, bind_session, session_id) # the switch latch (tezgah_paths.off)
162
167
  if event == "SessionStart":
163
168
  safe(session_id, attest_session, "codex", session_id, cwd)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@r1z4x/tezgah",
3
- "version": "1.4.0",
3
+ "version": "1.5.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": {
@@ -60,6 +60,10 @@ is reported as such, and an empty list beside it is not evidence of absence.
60
60
  `graph-review` splits findings into `confirmed` (no refuter refuted it), `refuted` (do not report as bugs),
61
61
  and `unverified` (hit the cap, or every refuter failed - label them as such). Never promote a refuted or unverified finding.
62
62
 
63
+ `graph-review` is one review round, not a loop: inside a plan it is round one or round two of the
64
+ plan's single review (two rounds at most, `tezgah-task review` refuses a third), never one run per
65
+ task, and never re-run on a diff it already read.
66
+
63
67
  `graph-impact`'s `graph_blind_spots` are call sites the graph could not see - string dispatch, config,
64
68
  templates, generated code. They are usually where a migration actually breaks. Lead with them.
65
69
 
@@ -40,17 +40,23 @@ Input: `$ARGUMENTS` (free text describing the work). Run these steps in order.
40
40
  Acceptance: checkable criteria, each with the command that proves it - or the
41
41
  word `unverifiable` and why, since *when possible* is the loophole
42
42
  (`~/.config/tezgah/bin/tezgah-render-table --acceptance` reports the items that
43
- name no command). A plan whose every item names no command cannot enter a
44
- writing phase at all (`tezgah-task` refuses it), so this is the plan's own
45
- check, not a report read later. State: `not started` plus the findings from
46
- step 4. Next: the first concrete action. Dates: `date +%F`. `allowed_paths:`:
43
+ name no command). An item that names no command keeps the plan out of a
44
+ writing phase (`tezgah-task` refuses it, through the predicate `--strict`
45
+ refuses on), so this is the plan's own check, not a report read later. State:
46
+ `not started` plus the findings from
47
+ step 4. Next: the first concrete action - when the plan holds two or more
48
+ independent tasks, one parallel wave: one slice per task, each with its own
49
+ worktree and a subset of `allowed_paths:`, spawned in one message. Dates: `date +%F`. `allowed_paths:`:
47
50
  the globs the work will write, from the files step 4 found (`apps/admin/**`,
48
51
  `packages/ui/src/**`), narrow enough that a write outside them is a surprise
49
52
  - the plan names its own scope, and `tezgah-task start` without `--allow`
50
53
  holds the task to it. A plan that has to spike an assumption first (does the
51
54
  approach work at all?) writes the four `spike:` keys below: `implementation`
52
55
  is refused while `spike_recorded:` names no answer, so the answer is recorded
53
- before the build rather than after it.
56
+ before the build rather than after it. `after:`: the ids of the open plans
57
+ this one cannot start before (`after: 012, 015`), only when step 4 found a
58
+ real dependency - a plan with none can run beside the others, and
59
+ `tezgah-task status` names which ready plans share no `allowed_paths:` prefix.
54
60
  6. Rewrite the README status table: `~/.config/tezgah/bin/tezgah-render-table` (installed by `bin/tezgah-setup --install`; if it is missing, run that script)
55
61
  (one row per open plan, sorted by id; it prints the rows). A new plan has no PR
56
62
  state to enrich, so no `--pr-info` here; that enrichment belongs to plan-status.
@@ -72,7 +78,7 @@ Input: `$ARGUMENTS` (free text describing the work). Run these steps in order.
72
78
  user passes `--any-path`, so tell the user the exact command - the plan's
73
79
  scope included - and never suggest dropping it. Three more refusals land at
74
80
  the same moment, and the user fixes them in the plan, not the command: a
75
- writing phase whose every Acceptance item names no command, and
81
+ writing phase while any Acceptance item names no command, and
76
82
  `implementation` while the plan's spike is unanswered (both refused with the
77
83
  file and the reader that reported it); and `implementation` is refused while
78
84
  the tree is dirty, because the phase starts by recording `checkpoint:` - the
@@ -111,6 +117,7 @@ created: YYYY-MM-DD
111
117
  updated: YYYY-MM-DD
112
118
  allowed_paths:
113
119
  - <glob>
120
+ after:
114
121
  ---
115
122
  ## Goal
116
123
  <1-3 sentences>
@@ -134,6 +141,14 @@ acceptance commands run unpiped, and a fresh reviewer (the host's
134
141
  `tezgah-task review NNN <reviewer> approve|changes` records as `review:`.
135
142
  `tezgah-task close NNN done` (plan-sync) refuses without an `approve`.
136
143
 
144
+ `after:` names the plans that must be done before this one starts - a `- NNN`
145
+ block or `after: 012, 015`. `tezgah-task status` reads it: each open plan's
146
+ Acceptance boxes ticked (`acceptance 3/8 checked`), what it still waits on
147
+ (`waits on 012 (open)`), and a `parallel:` line naming the ready plans whose
148
+ `allowed_paths:` share no prefix, which can run at once in separate worktrees.
149
+ An `after:` id no plan carries, or an open plan whose `status:` is not open or
150
+ blocked, is a `FAIL` line there.
151
+
137
152
  `checkpoint:` is written by `tezgah-task` when the phase moves to
138
153
  `implementation`: the sha of the commit the work returns to, or `pending` while
139
154
  the tree still holds work no commit names - and while it reads `pending` the gate
@@ -163,7 +178,8 @@ Small self-contained edits do not get a plan.
163
178
  `allowed_paths` (the plan's scope), `phase` while it is the active task,
164
179
  `checkpoint` in the implementation phase (the commit the work returns to) and
165
180
  `review` once a fresh reviewer read its diff, plus the optional `spike` keys
166
- on a plan that answers one question before building,
181
+ on a plan that answers one question before building and `after` (the plan ids
182
+ it waits on),
167
183
  sections Goal, Acceptance (checkboxes: each item carries the command that proves
168
184
  it, or `unverifiable` and why), State (evidence), Next (one action or BLOCKED).
169
185
  - Work for a plan happens on branch `plan/NNN-slug`; a merged PR (or a branch merged
@@ -37,8 +37,9 @@ def acceptance(strict):
37
37
  count = lambda state: sum(1 for i in report["items"] if i["state"] == state)
38
38
  print("plans: %d read, acceptance items: %d read; %d name no command, %d declared unverifiable"
39
39
  % (report["plans"], len(report["items"]), count("missing"), count("unverifiable")))
40
- open_missing = [i for i in report["items"]
41
- if i["state"] == "missing" and i["plan"].startswith(".tezgah/plans/open/")]
40
+ # the predicate the phase move refuses on (tezgah-task), so the two agree
41
+ open_missing = tt.unproven([i for i in report["items"]
42
+ if i["plan"].startswith(".tezgah/plans/open/")])
42
43
  if strict and open_missing:
43
44
  print("strict: %d item(s) under .tezgah/plans/open name no command and declare no "
44
45
  "unverifiable; a done plan is reported, never gated" % len(open_missing))
@@ -15,7 +15,12 @@ description: >
15
15
  1. Resolve the root: `ROOT=$(git rev-parse --show-toplevel)`. If it fails, stop and
16
16
  say plans need a git repo. If `$ROOT/.tezgah/plans/open` is missing, say so and stop.
17
17
  2. For each `.tezgah/plans/open/*.md`: read the frontmatter (id, title, status, branch, pr)
18
- and the first line of the `## Next` section.
18
+ and the first line of the `## Next` section. Then run
19
+ `~/.config/tezgah/bin/tezgah-task status`: per open plan it prints the
20
+ Acceptance boxes ticked, the plans its `after:` still waits on, and a
21
+ `parallel:` line naming the ready plans that share no `allowed_paths:`
22
+ prefix; a `FAIL` line (a status outside open|blocked, an `after:` id no plan
23
+ carries, a phase no reader accepts) goes into the report as it printed.
19
24
  3. If `pr:` is set, run
20
25
  `gh pr view <pr> --json state,mergedAt,reviewDecision,statusCheckRollup -q '[.state, .reviewDecision, ([.statusCheckRollup[]?.conclusion] | unique | join(","))] | map(select(. != null and . != "")) | join(" | ")'`
21
26
  and append the result to that row's `pr` cell, e.g. `#12 (OPEN | APPROVED | SUCCESS)`.
@@ -37,16 +42,19 @@ description: >
37
42
  no "Generated with" / "Made with", no robot emoji, no Claude/Anthropic/OpenAI/
38
43
  GPT/Codex/Gemini/Cursor/Copilot credit.
39
44
  8. Print the table, then one line per plan with `status: blocked` quoting its
40
- `BLOCKED: <reason>`, then any gh failures.
45
+ `BLOCKED: <reason>`, then one line per plan that waits on another (`waits on
46
+ NNN (open)`), then any gh failures and `FAIL` lines.
41
47
  9. End with exactly one recommendation: "Work on NNN <slug> next: <reason>". No menu.
42
48
  Prefer plans with an approved PR and green CI, then open plans with no blocker,
43
- oldest first.
49
+ oldest first; never a plan that still waits on another. When the `parallel:`
50
+ line names it with others, add one sentence: those plans can run at once, one
51
+ worktree each.
44
52
 
45
53
  ## Format
46
54
 
47
55
  Plan file `.tezgah/plans/open/NNN-slug.md` (status open|blocked) or `.tezgah/plans/done/NNN-slug.md`
48
56
  (status done|discarded). Frontmatter: id, title, status, branch (`plan/NNN-slug`),
49
- pr, created, updated, allowed_paths, plus phase/allowed_from/review when set. Sections: `## Goal`, `## Acceptance` (checkboxes), `## State`
57
+ pr, created, updated, allowed_paths, plus after/phase/allowed_from/checkpoint/review/review_round/reviewed_sha and the spike keys when set. Sections: `## Goal`, `## Acceptance` (checkboxes), `## State`
50
58
  (evidence), `## Next` (one action or `BLOCKED: <reason>`). README table lives between
51
59
  `<!-- status:start -->` and `<!-- status:end -->` with columns
52
60
  `| id | title | status | branch | pr | next |`.
@@ -28,16 +28,23 @@ description: >
28
28
  - gh returned `[]`: fallback `git branch -r --merged origin/main | grep -q "origin/<branch>"`.
29
29
  If it matches, treat as done and append `merged without PR`.
30
30
  - otherwise: leave the plan untouched.
31
- 5. For every plan marked done: if `## Acceptance` still has `- [ ]` boxes, still move
32
- it but append the line `acceptance boxes unchecked at sync` to `## State`.
31
+ 5. For every plan marked done: if `## Acceptance` still has `- [ ]` boxes (the
32
+ `acceptance N/M checked` count `~/.config/tezgah/bin/tezgah-task status` prints
33
+ per open plan), still move it but append the line
34
+ `acceptance boxes unchecked at sync (N/M checked)` to `## State`.
33
35
  6. Move each done or discarded plan with
34
36
  `~/.config/tezgah/bin/tezgah-task close NNN done|discarded`: it sets `status:` and
35
37
  `updated:` and moves the file to `.tezgah/plans/done/`. It refuses `done` unless the
36
- plan records `review: <reviewer> approve` - a fresh reviewer (the host's
37
- `tezgah-reviewer` subagent) read `git diff <base>..HEAD` and
38
- `tezgah-task review NNN <reviewer> approve` wrote the verdict - and it refuses
39
- the active task. A refused plan stays open: report it with the refusal, never
40
- move it by hand.
38
+ plan records `review: <reviewer> approve` - fresh reviewers (the host's
39
+ `tezgah-reviewer` subagent, one per dimension in one message for a large diff)
40
+ read `git diff <base>..HEAD` and `tezgah-task review NNN <reviewer> approve`
41
+ wrote the verdict - and it refuses the active task. A refused plan stays open:
42
+ report it with the refusal, never move it by hand, and never start a review
43
+ round from here. One review per plan, over its whole diff in the verification
44
+ phase; at most two rounds - only a confirmed critical or major finding opens
45
+ round two, a minor or suggestion one is recorded fix-later, and after round two
46
+ what is left goes to the user or is recorded fix-later, never a third round.
47
+ A plan refused with a spent budget is the user's decision.
41
48
  7. Rewrite the README status table: `~/.config/tezgah/bin/tezgah-render-table` (installed by `bin/tezgah-setup --install`; if it is missing, run that script)
42
49
  (one row per open plan, sorted by id; it prints the rows). No `--pr-info` here:
43
50
  the row's `pr` cell comes from the plan files, and review/check enrichment
@@ -55,14 +62,17 @@ description: >
55
62
  no "Generated with" / "Made with", no robot emoji, no Claude/Anthropic/OpenAI/
56
63
  GPT/Codex/Gemini/Cursor/Copilot credit.
57
64
  9. Report four lists: moved plans (with done/discarded and the evidence line),
58
- plans `close` refused (with its reason), still-open plans, and gh failures. Never delete branches, never edit code outside
65
+ plans `close` refused (with its reason), still-open plans - each with what
66
+ `tezgah-task status` says it waits on, so a plan whose `after:` names one just
67
+ discarded (`waits on NNN (discarded)`) is named for the user to re-plan - and
68
+ gh failures. Never delete branches, never edit code outside
59
69
  `.tezgah/plans/`.
60
70
 
61
71
  ## Format
62
72
 
63
73
  Plan file `.tezgah/plans/open/NNN-slug.md` (status open|blocked) or `.tezgah/plans/done/NNN-slug.md`
64
74
  (status done|discarded). Frontmatter: id, title, status, branch (`plan/NNN-slug`),
65
- pr, created, updated, allowed_paths, plus phase/allowed_from/review when set.
75
+ pr, created, updated, allowed_paths, plus after/phase/allowed_from/checkpoint/review/review_round/reviewed_sha and the spike keys when set.
66
76
  Sections: `## Goal`, `## Acceptance` (checkboxes), `## State`
67
77
  (evidence), `## Next` (one action or `BLOCKED: <reason>`). README table lives between
68
78
  `<!-- status:start -->` and `<!-- status:end -->` with columns
@@ -221,9 +221,11 @@ a list, several files or areas, todo entries that share nothing - go to one
221
221
  subagent each, and every writing slice gets its own branch and checkout (see
222
222
  below), never the main checkout. The
223
223
  orchestrator is the main thread itself: it does not wait idle on a slice while
224
- independent work remains. Fan out only when the subtasks share no mutable file and no interface: if two of them would edit the same file, or one's answer decides the other's, keep them in one context or sequence them. The shared artifact is the coordination channel, not chatter - naming a lead coordinates nothing by itself. If the work
225
- cannot be split - one file, one bounded change, a strictly serial chain - do it
226
- directly. Never spawn a subagent whose briefing is bigger than the work.
224
+ independent work remains. Fan out only when the subtasks share no mutable file and no interface: if two of them would edit the same file, or one's answer decides the other's, keep them in one context or sequence them. The shared artifact is the coordination channel, not chatter - naming a lead coordinates nothing by itself. Only a single item - one file, one
225
+ bounded change, a strictly serial chain - is done directly; a list of
226
+ independent items is never worked one by one on the main thread, however small
227
+ each item is, and that holds at every phase: a plan's tasks, its acceptance
228
+ commands and the sources of a survey fan out the same way.
227
229
  A slice that needs its own checkout: inside Orca (the session-start `Orca:`
228
230
  line) create it with `orca worktree create --name <slug> --parent-worktree
229
231
  active --json` and track it with `orca worktree ps --json` or `tezgah-status
@@ -272,7 +274,8 @@ guards against.
272
274
 
273
275
  ### The gate, for anything either tier produces
274
276
  Read the diff yourself. Then tests, lint and type check must pass, and anything
275
- non-trivial gets an independent review before it lands. A delegate's output is
277
+ non-trivial gets an independent review before it lands - once per plan, not
278
+ per task or commit. A delegate's output is
276
279
  a draft until the router has evidence; "the agent said it works" is not
277
280
  evidence.
278
281
 
@@ -519,12 +522,20 @@ rewrite it plainly.
519
522
  on a merge. Review first with a FRESH reviewer (never my own read of my own
520
523
  code), run the full suite, and if the review is clean and every test passes,
521
524
  merge, move the plan to `.tezgah/plans/done/`, and report the outcome. Bring it back
522
- to the user only on a critical or high-severity finding, or a failing test.
523
-
524
- **Review population: a change with an executable oracle.** The single FRESH
525
- reviewer above stays the rule for every change except one whose correctness can
525
+ to the user only on a critical or major finding left after round two, or a
526
+ failing test. One review per plan, over its whole diff in the verification
527
+ phase; at most two rounds - only a confirmed critical or major finding opens
528
+ round two, a minor or suggestion one is recorded fix-later, and after round two
529
+ what is left goes to the user or is recorded fix-later, never a third round.
530
+ A round spawns its reviewers in ONE message, one per dimension (correctness
531
+ and contract, security, tests and performance) or file group; a diff under 300
532
+ changed lines gets one reviewer. Per-task work gets tests, not a reviewer, and
533
+ `tezgah-task review` refuses a third round.
534
+
535
+ **Review population: a change with an executable oracle.** The fresh review
536
+ above stays the rule for every change except one whose correctness can
526
537
  be executed - a checker, a gate rule, a parser - where each claim can be built as
527
- a fixture. There the fresh-review step becomes a population:
538
+ a fixture. There round one becomes a population:
528
539
  1. One agent that has seen no known defect writes four failure directions from
529
540
  the code alone.
530
541
  2. Four reviewers, one direction each, run at once; every claim names an exact
@@ -534,9 +545,11 @@ a fixture. There the fresh-review step becomes a population:
534
545
  kind - a false accept and a false refusal alike. Only a confirmed claim counts.
535
546
  4. Confirmed claims are grouped by root mechanism, and each group gets a
536
547
  failing-first test.
537
- 5. A population round reviews one revision and does not replace re-review of
538
- the fix: every round that confirms a defect is followed by a round over that
539
- fix's delta, until a round confirms none.
548
+ 5. The population is round one and spends the same two-round budget: a fix
549
+ for a confirmed critical or major claim carries its failing-first test, and
550
+ round two reads that fix's delta, the round-one hunks and the direct callers
551
+ of what it touches. After round two what is left goes to the user or is
552
+ recorded fix-later, never a third round.
540
553
 
541
554
  Status: a tentative default, measured on two changes. E1
542
555
  (an internal prove-verify experiment): directed reviewers found 3 of 3 ground-truth defects,