@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.
- package/CHANGELOG.md +71 -1
- package/agents/tezgah-reviewer.md +19 -10
- package/bin/tezgah-docs +3 -3
- package/bin/tezgah-route +5 -2
- package/bin/tezgah-setup +24 -1
- package/bin/tezgah-status +3 -1
- package/bin/tezgah-task +130 -37
- package/bin/tezgah-taste +49 -31
- package/bin/tezgah-triage +12 -7
- package/docs/clarity-baseline.json +6 -6
- package/docs/contract.md +21 -11
- package/docs/environment.md +3 -1
- package/docs/evidence.md +4 -3
- package/docs/glossary.md +1 -1
- package/docs/judge.md +93 -27
- package/docs/operations.md +23 -12
- package/docs/skills.md +1 -1
- package/docs/vision.md +10 -5
- package/hooks/tezgah_agents.py +22 -11
- package/hooks/tezgah_context.py +40 -6
- package/hooks/tezgah_judge.py +245 -79
- package/hooks/tezgah_paths.py +165 -27
- package/hooks/tezgah_policy.py +35 -8
- package/hooks/tezgah_replay.py +26 -15
- package/hooks/tezgah_task.py +146 -40
- package/hooks/tezgah_taste.py +2 -2
- package/hooks/tezgah_taste_ledger.py +13 -4
- package/hosts/codex/hook.py +5 -0
- package/package.json +1 -1
- package/skills/harness/SKILL.md +4 -0
- package/skills/plan-add/SKILL.md +23 -7
- package/skills/plan-add/render_table.py +3 -2
- package/skills/plan-status/SKILL.md +12 -4
- package/skills/plan-sync/SKILL.md +19 -9
- package/skills/tezgah-contract/SKILL.md +25 -12
package/hooks/tezgah_task.py
CHANGED
|
@@ -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
|
|
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
|
|
29
|
-
(`acceptance_gap
|
|
30
|
-
|
|
31
|
-
the
|
|
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
|
|
104
|
-
"""
|
|
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() ==
|
|
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
|
|
195
|
-
One field and not two - "checkable" (the item names a
|
|
196
|
-
"unverifiable" (it says so and why) or "missing" (neither) - so no
|
|
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(
|
|
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
|
|
222
|
-
|
|
223
|
-
|
|
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
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
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
|
-
|
|
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`.
|
package/hooks/tezgah_taste.py
CHANGED
|
@@ -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
|
-
|
|
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(("
|
|
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
|
|
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",
|
|
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
|
|
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",
|
|
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
|
package/hosts/codex/hook.py
CHANGED
|
@@ -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
package/skills/harness/SKILL.md
CHANGED
|
@@ -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
|
|
package/skills/plan-add/SKILL.md
CHANGED
|
@@ -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).
|
|
44
|
-
writing phase
|
|
45
|
-
check, not a report read later. State:
|
|
46
|
-
|
|
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
|
|
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
|
-
|
|
41
|
-
|
|
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
|
|
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
|
|
32
|
-
|
|
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` -
|
|
37
|
-
`tezgah-reviewer` subagent
|
|
38
|
-
`tezgah-task review NNN <reviewer> approve`
|
|
39
|
-
the active task. A refused plan stays open:
|
|
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
|
|
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.
|
|
225
|
-
|
|
226
|
-
|
|
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
|
|
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
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
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
|
|
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.
|
|
538
|
-
|
|
539
|
-
fix's delta,
|
|
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,
|