@r1z4x/tezgah 1.4.0 → 1.5.1
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 +120 -1
- package/MANIFEST +0 -1
- package/README.md +6 -4
- package/bin/tezgah-docs +3 -3
- package/bin/tezgah-route +12 -6
- package/bin/tezgah-setup +67 -24
- package/bin/tezgah-status +3 -1
- package/bin/tezgah-task +132 -39
- package/bin/tezgah-taste +49 -31
- package/bin/tezgah-triage +12 -7
- package/docs/architecture.md +1 -1
- package/docs/clarity-baseline.json +7 -7
- 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/hosts.md +9 -0
- package/docs/judge.md +95 -27
- package/docs/layers.md +1 -1
- package/docs/models.md +17 -7
- package/docs/operations.md +23 -13
- package/docs/skills.md +1 -1
- package/docs/testing.md +1 -1
- package/docs/vision.md +10 -5
- package/hooks/tezgah_agents.py +149 -105
- package/hooks/tezgah_context.py +42 -7
- package/hooks/tezgah_judge.py +252 -79
- package/hooks/tezgah_models.py +50 -3
- package/hooks/tezgah_paths.py +180 -27
- package/hooks/tezgah_policy.py +41 -10
- package/hooks/tezgah_replay.py +26 -15
- package/hooks/tezgah_task.py +160 -46
- 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 -2
- 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 +29 -14
- package/agents/tezgah-reviewer.md +0 -70
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)
|
|
@@ -138,6 +158,12 @@ _MARKER = re.compile(r"unverifiable(?![-\w])", re.I)
|
|
|
138
158
|
# why") has no separator there and is not a declaration.
|
|
139
159
|
_SEPARATORS = ":,;(-"
|
|
140
160
|
_SPAN = re.compile(r"`([^`\n]+)`")
|
|
161
|
+
# A runner whose test entry point is a bare subcommand: `pnpm test`, `cargo
|
|
162
|
+
# test`, `uv run pytest` carry no option and no path, yet each is the command.
|
|
163
|
+
# The runner has to be the first word, so `run the tests` stays prose.
|
|
164
|
+
_RUNNERS = ("npm", "pnpm", "yarn", "bun", "deno", "cargo", "go", "make", "mix",
|
|
165
|
+
"dotnet", "gradle", "mvn", "poetry", "uv")
|
|
166
|
+
_SUBCOMMANDS = ("test", "run")
|
|
141
167
|
|
|
142
168
|
|
|
143
169
|
def _argument(token):
|
|
@@ -159,14 +185,16 @@ def _argument(token):
|
|
|
159
185
|
|
|
160
186
|
def names_command(text):
|
|
161
187
|
"""Does this acceptance item name the command that proves it? A command is a
|
|
162
|
-
backticked run of two or more words carrying an option, a path or a script
|
|
163
|
-
`bin/tezgah-docs --citations`, `python3 -m unittest tests/test_task.py
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
188
|
+
backticked run of two or more words carrying an option, a path or a script
|
|
189
|
+
(`bin/tezgah-docs --citations`, `python3 -m unittest tests/test_task.py`), or
|
|
190
|
+
a known runner followed by `test` or `run` (`pnpm test`, `npm run test`). A
|
|
191
|
+
citation (`hooks/tezgah_task.py`, `plan:line`) is not one, and any other
|
|
192
|
+
invocation with no argument is not recognised either - both are reported,
|
|
193
|
+
which is the safe direction for a report a person reads."""
|
|
167
194
|
for span in _SPAN.findall(text):
|
|
168
195
|
tokens = span.split()
|
|
169
|
-
if len(tokens) > 1 and any(_argument(token) for token in tokens[1:])
|
|
196
|
+
if len(tokens) > 1 and (any(_argument(token) for token in tokens[1:]) or
|
|
197
|
+
(tokens[0] in _RUNNERS and tokens[1] in _SUBCOMMANDS)):
|
|
170
198
|
return True
|
|
171
199
|
return False
|
|
172
200
|
|
|
@@ -191,10 +219,11 @@ def unverifiable_reason(text):
|
|
|
191
219
|
|
|
192
220
|
def acceptance_items(text):
|
|
193
221
|
"""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.
|
|
222
|
+
{"line", "text", "checked", "state"}, plus "reason" when the state is
|
|
223
|
+
"unverifiable". One field and not two - "checkable" (the item names a
|
|
224
|
+
command), "unverifiable" (it says so and why) or "missing" (neither) - so no
|
|
225
|
+
caller can read two answers about one item. "checked" is the box (`- [x]`),
|
|
226
|
+
which is the plan's progress and says nothing about the item's proof.
|
|
198
227
|
|
|
199
228
|
An item is one `- [ ]`/`- [x]` line plus the lines it wraps onto, up to the
|
|
200
229
|
next item or a blank line: the format wraps at 79 columns, so an item is more
|
|
@@ -204,8 +233,8 @@ def acceptance_items(text):
|
|
|
204
233
|
for number, line in section_lines(text, "Acceptance"):
|
|
205
234
|
found = _ITEM.match(line)
|
|
206
235
|
if found:
|
|
207
|
-
out.append({"line": number, "text": " ".join(found.group(
|
|
208
|
-
"state": "missing"})
|
|
236
|
+
out.append({"line": number, "text": " ".join(found.group(2).split()),
|
|
237
|
+
"checked": found.group(1) != " ", "state": "missing"})
|
|
209
238
|
elif out and line.strip():
|
|
210
239
|
out[-1]["text"] = " ".join((out[-1]["text"] + " " + line).split())
|
|
211
240
|
for item in out:
|
|
@@ -217,19 +246,27 @@ def acceptance_items(text):
|
|
|
217
246
|
return out
|
|
218
247
|
|
|
219
248
|
|
|
249
|
+
def unproven(items):
|
|
250
|
+
"""The items that name no command and declare no `unverifiable` - the one
|
|
251
|
+
predicate both the phase move (`acceptance_gap`) and `render_table
|
|
252
|
+
--acceptance --strict` refuse on, so the two cannot let through different
|
|
253
|
+
plans."""
|
|
254
|
+
return [item for item in items if item["state"] == "missing"]
|
|
255
|
+
|
|
256
|
+
|
|
220
257
|
def acceptance_gap(text):
|
|
221
|
-
"""The plan's Acceptance items
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
Acceptance items at all.
|
|
258
|
+
"""The plan's Acceptance items that nothing can check: each one names no
|
|
259
|
+
command and declares no `unverifiable`. [] when every item names its proof,
|
|
260
|
+
and [] when the plan has no Acceptance items at all.
|
|
225
261
|
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
262
|
+
Any such item is a gap, not only a section made of them: the format asks for
|
|
263
|
+
the proof per item, and the strict report refuses an open plan on one, so a
|
|
264
|
+
phase move that let one through would be refused later by the reader it was
|
|
265
|
+
meant to answer first. A plan with an empty or absent section is a different
|
|
266
|
+
defect - the format asks plan-add to write the items, and the report does not
|
|
267
|
+
gate that case either - so refusing it here would tax every plan that
|
|
268
|
+
predates the rule. Returns the items so a refusal can name them."""
|
|
269
|
+
return unproven(acceptance_items(text))
|
|
233
270
|
|
|
234
271
|
|
|
235
272
|
# The `checkpoint:` values a plan can carry: a commit sha (the tree was clean at
|
|
@@ -295,6 +332,30 @@ def spike_unanswered(text):
|
|
|
295
332
|
|
|
296
333
|
|
|
297
334
|
ACCEPTANCE_DIRS = ("open", "done")
|
|
335
|
+
# The `status:` a plan carries in each directory (plan-add's format): a plan
|
|
336
|
+
# under open/ is open or blocked, one under done/ is done or discarded.
|
|
337
|
+
OPEN_STATUSES = ("open", "blocked")
|
|
338
|
+
|
|
339
|
+
|
|
340
|
+
def _plan_files(root, sub):
|
|
341
|
+
"""(name, path, text) for every readable `*.md` under
|
|
342
|
+
`<root>/.tezgah/plans/<sub>`, sorted by name; an absent directory or an
|
|
343
|
+
unreadable file is skipped, so a reader built on it never raises."""
|
|
344
|
+
directory = os.path.join(root, ".tezgah", "plans", sub)
|
|
345
|
+
try:
|
|
346
|
+
names = sorted(os.listdir(directory))
|
|
347
|
+
except OSError:
|
|
348
|
+
return
|
|
349
|
+
for name in names:
|
|
350
|
+
if not name.endswith(".md"):
|
|
351
|
+
continue
|
|
352
|
+
path = os.path.join(directory, name)
|
|
353
|
+
try:
|
|
354
|
+
with open(path, "r", encoding="utf-8") as fh:
|
|
355
|
+
text = fh.read()
|
|
356
|
+
except OSError:
|
|
357
|
+
continue
|
|
358
|
+
yield name, path, text
|
|
298
359
|
|
|
299
360
|
|
|
300
361
|
def acceptance_report(root):
|
|
@@ -310,20 +371,7 @@ def acceptance_report(root):
|
|
|
310
371
|
items = []
|
|
311
372
|
plans = 0
|
|
312
373
|
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
|
|
374
|
+
for name, _, text in _plan_files(root, sub):
|
|
327
375
|
plans += 1
|
|
328
376
|
for item in acceptance_items(text):
|
|
329
377
|
item["plan"] = ".tezgah/plans/%s/%s" % (sub, name)
|
|
@@ -331,6 +379,72 @@ def acceptance_report(root):
|
|
|
331
379
|
return {"plans": plans, "items": items}
|
|
332
380
|
|
|
333
381
|
|
|
382
|
+
def _plan_id(fields, name):
|
|
383
|
+
return fields.get("id") or name.split("-")[0]
|
|
384
|
+
|
|
385
|
+
|
|
386
|
+
def _prefix(glob):
|
|
387
|
+
"""The literal head of a glob: everything before its first `*`."""
|
|
388
|
+
return glob.split("*", 1)[0]
|
|
389
|
+
|
|
390
|
+
|
|
391
|
+
def scopes_overlap(first, second):
|
|
392
|
+
"""Could two allowlists name one file? An empty list is every path, and two
|
|
393
|
+
globs can only meet when one literal head is a prefix of the other's - so
|
|
394
|
+
`hooks/a*.py` and `hooks/b*.py` never do, and `**/x` meets everything.
|
|
395
|
+
ponytail: prefix test, not glob intersection - it says "overlap" for
|
|
396
|
+
`hooks/*.py` against `hooks/x/y.md`, which only costs a missed pairing."""
|
|
397
|
+
if not first or not second:
|
|
398
|
+
return True
|
|
399
|
+
return any(a.startswith(b) or b.startswith(a)
|
|
400
|
+
for a in map(_prefix, first) for b in map(_prefix, second))
|
|
401
|
+
|
|
402
|
+
|
|
403
|
+
def queue(root):
|
|
404
|
+
"""Every open plan as a row: {"id", "status", "path", "checked", "items" (the
|
|
405
|
+
Acceptance boxes ticked, and their count), "after" ([id, state] per plan it
|
|
406
|
+
waits on: `done`, `discarded`, `open` or `missing` when no plan carries the
|
|
407
|
+
id), "ready" (not blocked, and everything it waits on is done), "overlaps"
|
|
408
|
+
(the other open plans whose allowed_paths can name one of its files)}.
|
|
409
|
+
|
|
410
|
+
The plans' progress and order had no reader: the boxes were counted nowhere
|
|
411
|
+
and nothing said which plans could run beside each other. Never raises."""
|
|
412
|
+
state = {}
|
|
413
|
+
for name, _, text in _plan_files(root, "done"):
|
|
414
|
+
fields = frontmatter(text)
|
|
415
|
+
state[_plan_id(fields, name)] = ("discarded" if fields.get("status") == "discarded"
|
|
416
|
+
else "done")
|
|
417
|
+
rows = []
|
|
418
|
+
scopes = {}
|
|
419
|
+
for name, path, text in _plan_files(root, "open"):
|
|
420
|
+
fields = frontmatter(text)
|
|
421
|
+
ident = _plan_id(fields, name)
|
|
422
|
+
state[ident] = "open"
|
|
423
|
+
items = acceptance_items(text)
|
|
424
|
+
scopes[ident] = allowed_paths(text)
|
|
425
|
+
rows.append({"id": ident, "status": fields.get("status", ""), "path": path,
|
|
426
|
+
"checked": sum(1 for item in items if item["checked"]),
|
|
427
|
+
"items": len(items), "after": after(text)})
|
|
428
|
+
for row in rows:
|
|
429
|
+
row["after"] = [[ident, state.get(ident, "missing")] for ident in row["after"]]
|
|
430
|
+
row["ready"] = (row["status"] != "blocked"
|
|
431
|
+
and all(dep == "done" for _, dep in row["after"]))
|
|
432
|
+
row["overlaps"] = [other["id"] for other in rows if other is not row
|
|
433
|
+
and scopes_overlap(scopes[row["id"]], scopes[other["id"]])]
|
|
434
|
+
return rows
|
|
435
|
+
|
|
436
|
+
|
|
437
|
+
def parallel(rows):
|
|
438
|
+
"""The ready plans that can run at once, in id order: each one shares no
|
|
439
|
+
allowed_paths prefix with any other picked (greedy, first id first). [] when
|
|
440
|
+
fewer than two qualify - one plan is not a fan-out."""
|
|
441
|
+
picked = []
|
|
442
|
+
for row in rows:
|
|
443
|
+
if row["ready"] and not any(other in row["overlaps"] for other in picked):
|
|
444
|
+
picked.append(row["id"])
|
|
445
|
+
return picked if len(picked) > 1 else []
|
|
446
|
+
|
|
447
|
+
|
|
334
448
|
def active(cwd, base):
|
|
335
449
|
"""The active task as a dict, or None. Scans `<repo_root>/.tezgah/plans/open/*.md`
|
|
336
450
|
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
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@r1z4x/tezgah",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.5.1",
|
|
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": {
|
|
@@ -19,7 +19,6 @@
|
|
|
19
19
|
"output-styles/",
|
|
20
20
|
"commands/",
|
|
21
21
|
"docs/",
|
|
22
|
-
"agents/",
|
|
23
22
|
"statusline.py",
|
|
24
23
|
"MANIFEST",
|
|
25
24
|
"CHANGELOG.md",
|
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
|