task-pipeline-skill 1.95.0 → 1.97.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 +70 -0
- package/README.md +13 -9
- package/SKILL-CARD.md +2 -2
- package/cursor/rules/task-pipeline.mdc +11 -7
- package/evals/RESULTS.md +55 -8
- package/evals/cases/task-pipeline.json +139 -0
- package/evals/fixtures/lessons/expect-match.sh +30 -0
- package/evals/fixtures/lessons/index.jsonl +2 -0
- package/evals/fixtures/waves-unclaimed/expect-refusal.sh +26 -0
- package/evals/fixtures/waves-unclaimed/inventory.json +33 -0
- package/evals/fixtures/waves-unclaimed/modules.json +4 -0
- package/evals/fixtures/waves-unclaimed/plan.json +5 -0
- package/evals/task-pipeline.evals.json +104 -0
- package/package.json +1 -1
- package/plugins/task-pipeline/.claude-plugin/plugin.json +1 -1
- package/plugins/task-pipeline/skills/task-pipeline/SKILL.md +5 -2
- package/plugins/task-pipeline/skills/task-pipeline/pipeline.example.json +4 -0
- package/plugins/task-pipeline/skills/task-pipeline/pipeline.schema.json +19 -0
- package/plugins/task-pipeline/skills/task-pipeline/references/audit.md +5 -2
- package/plugins/task-pipeline/skills/task-pipeline/references/browser.md +48 -11
- package/plugins/task-pipeline/skills/task-pipeline/references/companion-skills.md +32 -24
- package/plugins/task-pipeline/skills/task-pipeline/references/continuity.md +7 -1
- package/plugins/task-pipeline/skills/task-pipeline/references/doctrine-map.md +1 -0
- package/plugins/task-pipeline/skills/task-pipeline/references/finish-line.md +145 -0
- package/plugins/task-pipeline/skills/task-pipeline/references/iteration.md +9 -5
- package/plugins/task-pipeline/skills/task-pipeline/references/loop-guard.md +5 -0
- package/plugins/task-pipeline/skills/task-pipeline/references/portability.md +1 -0
- package/plugins/task-pipeline/skills/task-pipeline/references/progress.md +8 -0
- package/plugins/task-pipeline/skills/task-pipeline/references/review.md +7 -2
- package/plugins/task-pipeline/skills/task-pipeline/references/stage-05-dev.md +2 -2
- package/plugins/task-pipeline/skills/task-pipeline/references/stage-08-post-deploy.md +3 -2
- package/plugins/task-pipeline/skills/task-pipeline/references/stages.md +9 -4
- package/plugins/task-pipeline/skills/task-pipeline/references/tdd.md +3 -2
- package/plugins/task-pipeline/skills/task-pipeline/references/waves.md +9 -6
- package/plugins/task-pipeline/skills/task-pipeline/scripts/graph.py +1 -1
- package/plugins/task-pipeline/skills/task-pipeline/scripts/waves.py +219 -3
- package/plugins/task-pipeline/skills/task-pipeline/templates/brief.md +14 -0
- package/plugins/task-pipeline/skills/task-pipeline/templates/browser-claims.json +5 -5
- package/plugins/task-pipeline/skills/task-pipeline/templates/inventory.schema.json +15 -1
- package/plugins/task-pipeline/skills/task-pipeline/templates/run.md +26 -2
|
@@ -108,14 +108,19 @@ Review in this order; stop reading the diff only when you've covered all of it.
|
|
|
108
108
|
**Severities:**
|
|
109
109
|
|
|
110
110
|
- **Critical** — wrong behavior, data loss, security hole, a red or dishonest test
|
|
111
|
-
suite. Blocks.
|
|
111
|
+
suite. Blocks the node when the defect is in its diff or caused by it.
|
|
112
112
|
- **Important** — a real defect or spec gap that will bite: missing requirement,
|
|
113
|
-
unhandled failure path, a magic value the constraints named. Blocks
|
|
113
|
+
unhandled failure path, a magic value the constraints named. Blocks the node when the
|
|
114
|
+
defect is in its diff or caused by it.
|
|
114
115
|
- **Minor** — style, naming, a nit with no behavioral consequence. Never blocks;
|
|
115
116
|
goes to the ledger.
|
|
116
117
|
- **⚠️ Cannot verify from diff** — the requirement lives in unchanged code or spans
|
|
117
118
|
tasks. Not a blocker for the reviewer; the controller resolves it.
|
|
118
119
|
|
|
120
|
+
**A pre-existing defect the reviewer notices** — one the diff neither contains nor
|
|
121
|
+
causes — does not block the node, whatever its severity. It goes to finish-line triage
|
|
122
|
+
([`finish-line.md`](finish-line.md) → *Triage at discovery*).
|
|
123
|
+
|
|
119
124
|
Formatting nits that don't change meaning are not findings. Praise is not a
|
|
120
125
|
finding either.
|
|
121
126
|
|
|
@@ -19,8 +19,8 @@ The full gate and freedom level remain in [stages.md](stages.md).
|
|
|
19
19
|
contradiction that exists only between two of them.
|
|
20
20
|
- **Web front end? The task's own surface is checked in a browser, not in the diff.**
|
|
21
21
|
Where a browser channel is connected — `playwright` **or** `chrome-devtools`, either
|
|
22
|
-
one,
|
|
23
|
-
[`companion-skills.md`](companion-skills.md)): after a
|
|
22
|
+
one, on WebKit first with Chromium a stated exception (**how**, and the engine order:
|
|
23
|
+
[`browser.md`](browser.md); which: [`companion-skills.md`](companion-skills.md)): after a
|
|
24
24
|
task that changes a rendered surface, load it, take a snapshot and read the
|
|
25
25
|
console and the network log **before the task is marked DONE** — a component can be
|
|
26
26
|
correct and land under a fixed header, and a review of the diff cannot see that.
|
|
@@ -13,8 +13,9 @@ The full gate and freedom level remain in [stages.md](stages.md).
|
|
|
13
13
|
- **A deployed web target is opened, not curled.** A `200` proves the server
|
|
14
14
|
answered; it says nothing about whether the page rendered, whether a bundle 404'd,
|
|
15
15
|
or whether the console filled with errors on load — all three ship green past a
|
|
16
|
-
health check. Where a browser channel is connected — `playwright`
|
|
17
|
-
`chrome-devtools
|
|
16
|
+
health check. Where a browser channel is connected — `playwright` on WebKit first
|
|
17
|
+
**or** `chrome-devtools` as the stated Chromium exception, and a Chromium-only look
|
|
18
|
+
is reported as such; either one ([`browser.md`](browser.md)) — load the deployed URL and read
|
|
18
19
|
the console and the network log; quote what you read, not that you looked
|
|
19
20
|
([`companion-skills.md`](companion-skills.md)). Absent → say the check was an HTTP
|
|
20
21
|
response only, which is the honest name for it.
|
|
@@ -103,7 +103,9 @@ never that the work was skipped quietly.
|
|
|
103
103
|
is now stale), no open contradictions, **every
|
|
104
104
|
autonomy-sweep row is answered or explicitly marked "stop and ask here"**, **a
|
|
105
105
|
user-facing task's brief names its `surface_class`**, the
|
|
106
|
-
**REQ table is written and every row names its check**, the
|
|
106
|
+
**REQ table is written and every row names its check**, **the brief's `## Finish line`
|
|
107
|
+
is filled — objective, exit checks on the artefact the operator uses, scope, budget and
|
|
108
|
+
base** ([`finish-line.md`](finish-line.md)), the carry-over ledger is
|
|
107
109
|
seeded, **`.task-pipeline/run.md` exists and the header block has been printed**
|
|
108
110
|
([`progress.md`](progress.md)), the model decision is recorded, and the operator
|
|
109
111
|
confirms the brief. Stop when a
|
|
@@ -252,8 +254,9 @@ never that the work was skipped quietly.
|
|
|
252
254
|
page rendered — a component can be correct and land under a fixed header, a request
|
|
253
255
|
can 404 while every unit test mocks it, and a console error costs nothing at test
|
|
254
256
|
time. Where a browser channel is connected — `playwright` **or** `chrome-devtools`,
|
|
255
|
-
either one
|
|
256
|
-
[`
|
|
257
|
+
either one, on WebKit first with Chromium a stated exception
|
|
258
|
+
([`browser.md`](browser.md) is the four commands this sentence means and the engine
|
|
259
|
+
order; [`companion-skills.md`](companion-skills.md) is which channel):
|
|
257
260
|
load the surface, take a snapshot, and read **the console and the network log**
|
|
258
261
|
before calling it green. Absent, say the surface was verified **by reading the
|
|
259
262
|
diff** — that is a weaker claim and the close-out records it as one, rather than
|
|
@@ -379,7 +382,9 @@ never that the work was skipped quietly.
|
|
|
379
382
|
surfaced that nobody asked for, every decision still waiting **asked here with
|
|
380
383
|
options rather than listed**, and the ambiguity count computed from the four
|
|
381
384
|
registers with its ids ([`progress.md`](progress.md) → *The hand-back*) — a run
|
|
382
|
-
that cannot say what happened has not finished, it has stopped; **the
|
|
385
|
+
that cannot say what happened has not finished, it has stopped; **the `finish:` line is
|
|
386
|
+
printed beside this verdict — `done`, `budget` or `blocked`, with its block**
|
|
387
|
+
([`finish-line.md`](finish-line.md) → *The finish block*); **the environment is
|
|
383
388
|
given back** — all eight classes enumerated, everything this run started ended and
|
|
384
389
|
verified by re-enumerating rather than by the teardown's reply, an earlier run of
|
|
385
390
|
this project ended only when **provably spent**, an item this project does not own
|
|
@@ -102,8 +102,9 @@ database created ten seconds ago" are different claims, and only the second one
|
|
|
102
102
|
code does what its assertions say; it cannot see a component that renders correctly
|
|
103
103
|
and lands under a fixed header, a request that 404s while every unit test mocks it,
|
|
104
104
|
or a console error that costs nothing at test time. Where a browser channel is
|
|
105
|
-
connected — `playwright` **or** `chrome-devtools`, either one
|
|
106
|
-
([`companion-skills.md`](companion-skills.md)
|
|
105
|
+
connected — `playwright` **or** `chrome-devtools`, either one, on WebKit first with
|
|
106
|
+
Chromium a stated exception ([`companion-skills.md`](companion-skills.md);
|
|
107
|
+
[`browser.md`](browser.md) → *Engine order*) — open the surface and read
|
|
107
108
|
the console and the network log before calling it done — and **quote what you read**,
|
|
108
109
|
not that you looked. Absent, the honest sentence is *"verified by reading the diff"*,
|
|
109
110
|
which is a weaker claim and is recorded as one. Same family as a test that passes
|
|
@@ -211,11 +211,13 @@ In this order, inside the wave rather than after it:
|
|
|
211
211
|
1. **Verify.** Every node the wave closes is certified ([`certification.md`](certification.md)),
|
|
212
212
|
most of it already, as each node landed,
|
|
213
213
|
and [`audit.md`](audit.md)'s exit is re-measured over what the wave touched.
|
|
214
|
-
2. **Finish and fix — one fix round**, inside
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
is
|
|
214
|
+
2. **Finish and fix — one fix round, for blocking findings only**, inside
|
|
215
|
+
[`loop-guard.md`](loop-guard.md)'s caps. Every finding is triaged when found
|
|
216
|
+
([`finish-line.md`](finish-line.md) → *Triage at discovery*): blocking findings against
|
|
217
|
+
this wave's nodes are fixed here or parked with a board row and reason; deferred ones
|
|
218
|
+
become board rows; noise is one ledger line. Findings elsewhere join the inventory for a
|
|
219
|
+
planned node; only the closed hotfix lane bypasses that design
|
|
220
|
+
([`iteration.md`](iteration.md) → *No fix travels alone*). Nothing is dropped silently.
|
|
219
221
|
3. **Document** — the Doc Loop and the stage-9 propagation matrix
|
|
220
222
|
([`documentation.md`](documentation.md)), scoped to the wave: scenarios, decisions,
|
|
221
223
|
docs and registers in the same change as the code.
|
|
@@ -224,7 +226,8 @@ In this order, inside the wave rather than after it:
|
|
|
224
226
|
items, not only the board*). A re-plan is a committed `revisions` entry plus a canvas
|
|
225
227
|
edit, never a quiet change.
|
|
226
228
|
6. **Re-measure the inventory and re-cut the remaining waves** from the updated target —
|
|
227
|
-
never from the original guess. New findings join the inventory with ids
|
|
229
|
+
never from the original guess. New findings join the inventory with ids and their
|
|
230
|
+
triage class; a deferred one never adds a wave to this release.
|
|
228
231
|
|
|
229
232
|
## A board bigger than one release
|
|
230
233
|
|
|
@@ -1580,7 +1580,7 @@ def cmd_producer(graph, args):
|
|
|
1580
1580
|
def cmd_doctrine(graph, args):
|
|
1581
1581
|
"""Which doctrine this run actually read — B-061.
|
|
1582
1582
|
|
|
1583
|
-
The bundle is
|
|
1583
|
+
The bundle is 53 reference files. A run reads some subset and nothing recorded which,
|
|
1584
1584
|
so **a skipped file and a read one were indistinguishable** — the class every guard in
|
|
1585
1585
|
this repository exists to catch, left standing over the doctrine itself.
|
|
1586
1586
|
|
|
@@ -30,6 +30,22 @@ reviewed; a table and a re-run can. This script is that re-run:
|
|
|
30
30
|
refused halfway and a refused one writes nothing. Plan nodes the graph lacks are named
|
|
31
31
|
and exit 1 before any write, unless --only-present
|
|
32
32
|
(a plan that spans several repositories' graphs).
|
|
33
|
+
waves.py finish --inventory FILE --max-rounds N --max-releases M (--rounds K | --ledger RUN.md)
|
|
34
|
+
--checks K/M --artefact NAME@SHA [--releases R] [--operator-go]
|
|
35
|
+
the finish-line verdict (`references/finish-line.md`) from the triaged items, and the
|
|
36
|
+
finish block printed. Only `triage: blocking` items count, less those closed as
|
|
37
|
+
duplicate, stale or not-reproduced; deferred and noise never change the verdict. The
|
|
38
|
+
round count is explicit: --rounds, or the ledger's `round: <N> — <reading> — new
|
|
39
|
+
blocking <k>` lines, whose counts must match the inventory. Exit 0 done (the last two
|
|
40
|
+
rounds found no new blocking item, none is open, --checks is all green, and the rounds
|
|
41
|
+
are within --max-rounds or --operator-go is passed); 5 the budget reached before done
|
|
42
|
+
(rounds at --max-rounds, releases at --max-releases with a blocker open, or done past
|
|
43
|
+
the round budget without --operator-go); 6 diverging (the blocking count did not fall
|
|
44
|
+
from one round that found some to the next, or it did not fall and the last round's
|
|
45
|
+
self-inflicted >= new, from round 2 on); 3 continue (including no --checks); 1
|
|
46
|
+
malformed (a schema error, a blocking item without `test` or `round`, B3 with
|
|
47
|
+
base_repro yes, a round past the rounds run, no round count, a ledger that disagrees,
|
|
48
|
+
--max-rounds below 2, an artefact that is not NAME@SHA).
|
|
33
49
|
|
|
34
50
|
**The plan** is any JSON whose `nodes` list `{id, wave, closes}` — a work graph that carries
|
|
35
51
|
`closes` on its nodes is one, and so is a short hand-written table. A node may carry
|
|
@@ -42,7 +58,7 @@ described in `templates/inventory.schema.json` (`$defs.modules`, `$defs.plan`).
|
|
|
42
58
|
are listed in (wave, id) order, counts are sorted. A re-cut is a diff to a table and a
|
|
43
59
|
re-run, which a reviewer can read.
|
|
44
60
|
|
|
45
|
-
Exit codes: 0 done; 1 an item lands nowhere, or the tables and the inventory disagree, or a
|
|
61
|
+
Exit codes (every verb but `finish`, whose own are above): 0 done; 1 an item lands nowhere, or the tables and the inventory disagree, or a
|
|
46
62
|
graph move was refused; 2 a schema error, a table error (an id on two modules), usage, or an
|
|
47
63
|
unreadable file. Stdlib only (`references/portability.md`).
|
|
48
64
|
"""
|
|
@@ -62,11 +78,18 @@ SCHEMA = HERE.parent / "templates" / "inventory.schema.json"
|
|
|
62
78
|
GRAPH_PY = HERE / "graph.py"
|
|
63
79
|
REQUIRED = ("id", "title", "area", "kind", "severity", "evidence", "proposed_fix")
|
|
64
80
|
OPTIONAL = ("status", "source", "source_file", "repo", "module", "node", "wave", "release",
|
|
65
|
-
"register_id", "blocked_by", "closable_in_one_pass", "needs_operator", "notes"
|
|
81
|
+
"register_id", "blocked_by", "closable_in_one_pass", "needs_operator", "notes",
|
|
82
|
+
"triage", "test", "round", "base_repro", "blast", "rung")
|
|
66
83
|
SEVERITY = ("critical", "high", "medium", "low", "info")
|
|
67
84
|
STATUS = ("open", "changed", "closed", "fixed", "fixed-elsewhere", "duplicate", "moved", "stale",
|
|
68
85
|
"not-reproduced", "carried")
|
|
69
86
|
OPEN = ("open", "changed")
|
|
87
|
+
# The finish line's vocabulary (references/finish-line.md → Triage at discovery). The
|
|
88
|
+
# validator holds these to the doctrine, the run-ledger grammar and inventory/1's enums.
|
|
89
|
+
TRIAGE = ("blocking", "deferred", "noise")
|
|
90
|
+
TESTS = ("B1", "B2", "B3", "B4")
|
|
91
|
+
BASE_REPRO = ("yes", "no", "n/a")
|
|
92
|
+
RUNGS = ("I1", "I2", "I3", "I4", "I5")
|
|
70
93
|
ID = re.compile(r"^[A-Za-z][A-Za-z0-9._-]{0,63}$")
|
|
71
94
|
KIND = re.compile(r"^[a-z][a-z0-9-]*$")
|
|
72
95
|
EXT = re.compile(r"^x-[a-z0-9-]+$")
|
|
@@ -115,9 +138,34 @@ def item_errors(x: object) -> list[str]:
|
|
|
115
138
|
for k in ("closable_in_one_pass", "needs_operator"):
|
|
116
139
|
if k in x and not isinstance(x[k], bool):
|
|
117
140
|
errs.append(f"{iid}: `{k}` is not a boolean")
|
|
141
|
+
if "triage" in x and x["triage"] not in TRIAGE:
|
|
142
|
+
errs.append(f"{iid}: `triage` {x['triage']!r} is not one of {', '.join(TRIAGE)}")
|
|
143
|
+
if "test" in x and x["test"] not in TESTS:
|
|
144
|
+
errs.append(f"{iid}: `test` {x['test']!r} is not one of {', '.join(TESTS)}")
|
|
145
|
+
if "round" in x and not _posint(x["round"]):
|
|
146
|
+
errs.append(f"{iid}: `round` is not an integer from 1")
|
|
147
|
+
if "base_repro" in x and x["base_repro"] not in BASE_REPRO:
|
|
148
|
+
errs.append(f"{iid}: `base_repro` {x['base_repro']!r} is not one of {', '.join(BASE_REPRO)}")
|
|
149
|
+
if "blast" in x and not (_posint(x["blast"]) and x["blast"] <= 3):
|
|
150
|
+
errs.append(f"{iid}: `blast` is not 1, 2 or 3")
|
|
151
|
+
if "rung" in x and x["rung"] not in RUNGS:
|
|
152
|
+
errs.append(f"{iid}: `rung` {x['rung']!r} is not one of {', '.join(RUNGS)}")
|
|
153
|
+
if x.get("triage") == "blocking":
|
|
154
|
+
errs += [f"{iid}: blocking without `{k}` — a blocking finding names the B test that "
|
|
155
|
+
"matched and the round that found it" for k in ("test", "round") if k not in x]
|
|
156
|
+
if x.get("test") == "B3" and x.get("base_repro") == "yes":
|
|
157
|
+
errs.append(f"{iid}: B3 says it passes at Base, and `base_repro: yes` says it fails "
|
|
158
|
+
"there — one of the two is wrong")
|
|
159
|
+
elif "test" in x:
|
|
160
|
+
errs.append(f"{iid}: `test` on an item that is not blocking — a B test is what makes a "
|
|
161
|
+
"finding blocking; without one it is deferred or noise")
|
|
118
162
|
return errs
|
|
119
163
|
|
|
120
164
|
|
|
165
|
+
def _posint(v) -> bool:
|
|
166
|
+
return isinstance(v, int) and not isinstance(v, bool) and v >= 1
|
|
167
|
+
|
|
168
|
+
|
|
121
169
|
def schema_errors(items: list) -> list[str]:
|
|
122
170
|
"""inventory/1 itself, applied where `jsonschema` is installed. `item_errors` is the
|
|
123
171
|
hand copy every install has; test/waves_test.py holds the two to one verdict."""
|
|
@@ -504,6 +552,163 @@ def cmd_sync_graph(a) -> int:
|
|
|
504
552
|
return 0
|
|
505
553
|
|
|
506
554
|
|
|
555
|
+
# ---------------------------------------------------------------------------- the finish line
|
|
556
|
+
|
|
557
|
+
# A blocking item closed as one of these was never a blocker: it counts in no round.
|
|
558
|
+
NOT_COUNTED = ("duplicate", "stale", "not-reproduced")
|
|
559
|
+
ROUND_LINE = re.compile(r"^round:\s*(\d+)\s*[—-]\s*(.*?)\s*[—-]\s*new blocking\s*(\d+)\s*$")
|
|
560
|
+
ARTEFACT = re.compile(r"^[^@\s]+@[0-9a-f]{7,40}$")
|
|
561
|
+
|
|
562
|
+
|
|
563
|
+
def _priority(x: dict):
|
|
564
|
+
"""Top priority first: the impact rung, then blast, then severity, then the id. Where an
|
|
565
|
+
item was found is not an input — a fresh finding is not next because it is fresh."""
|
|
566
|
+
rung = RUNGS.index(x["rung"]) + 1 if x.get("rung") in RUNGS else 0
|
|
567
|
+
return (-rung, -(x.get("blast") or 0), SEVERITY.index(x["severity"]), _natural(x["id"]))
|
|
568
|
+
|
|
569
|
+
|
|
570
|
+
def _board_id(x: dict) -> str:
|
|
571
|
+
return x.get("register_id") or x["id"]
|
|
572
|
+
|
|
573
|
+
|
|
574
|
+
def ledger_rounds(path: pathlib.Path) -> dict[int, int]:
|
|
575
|
+
"""{round: new blocking} from the run ledger's `round:` lines (templates/run.md)."""
|
|
576
|
+
try:
|
|
577
|
+
text = path.read_text(encoding="utf-8")
|
|
578
|
+
except (OSError, UnicodeError) as exc:
|
|
579
|
+
raise Fail(1, [f"the run ledger {path} cannot be read ({type(exc).__name__})"])
|
|
580
|
+
out, errs = {}, []
|
|
581
|
+
for n, line in enumerate(text.splitlines(), 1):
|
|
582
|
+
if not line.startswith("round:"):
|
|
583
|
+
continue
|
|
584
|
+
m = ROUND_LINE.match(line.strip())
|
|
585
|
+
if not m:
|
|
586
|
+
errs.append(f"{path}:{n}: not `round: <N> — <reading> — new blocking <k>`")
|
|
587
|
+
continue
|
|
588
|
+
r = int(m[1])
|
|
589
|
+
if r < 1 or r in out:
|
|
590
|
+
errs.append(f"{path}:{n}: round {r} is below 1 or written twice")
|
|
591
|
+
out[r] = int(m[3])
|
|
592
|
+
if out and sorted(out) != list(range(1, max(out) + 1)):
|
|
593
|
+
errs.append(f"{path}: rounds {sorted(out)} are not 1..{max(out)} — a round line is missing")
|
|
594
|
+
if errs:
|
|
595
|
+
raise Fail(1, errs)
|
|
596
|
+
return out
|
|
597
|
+
|
|
598
|
+
|
|
599
|
+
def finish_verdict(items: list[dict], rounds: int, max_rounds: int, max_releases: int,
|
|
600
|
+
releases: int, checks: tuple[int, int] | None,
|
|
601
|
+
operator_go: bool = False) -> tuple[int, str, dict]:
|
|
602
|
+
"""(exit code, word, facts) — the finish line computed from the triaged items alone.
|
|
603
|
+
Deferred and noise items are counted for the block and never read for the verdict."""
|
|
604
|
+
triaged = [x for x in items if "triage" in x]
|
|
605
|
+
late = [f"{x['id']}: found in round {x['round']}, and only {rounds} round(s) ran"
|
|
606
|
+
for x in triaged if _posint(x.get("round")) and x["round"] > rounds]
|
|
607
|
+
if late:
|
|
608
|
+
raise Fail(1, late)
|
|
609
|
+
blocking = [x for x in triaged if x["triage"] == "blocking" and _status(x) not in NOT_COUNTED]
|
|
610
|
+
new = [sum(1 for x in blocking if x["round"] == r) for r in range(1, rounds + 1)]
|
|
611
|
+
selfinf = sum(1 for x in blocking if x["round"] == rounds and x.get("base_repro") == "no")
|
|
612
|
+
open_b = [x for x in blocking if _needs_node(x)]
|
|
613
|
+
facts = {"rounds": rounds, "new": new, "open": open_b, "fixed": len(blocking) - len(open_b),
|
|
614
|
+
"self_inflicted": selfinf}
|
|
615
|
+
clean = rounds >= 2 and new[-1] == 0 and new[-2] == 0 and not open_b
|
|
616
|
+
green = checks is not None and checks[0] == checks[1]
|
|
617
|
+
if clean and green:
|
|
618
|
+
if rounds <= max_rounds:
|
|
619
|
+
return 0, "done", facts
|
|
620
|
+
if operator_go:
|
|
621
|
+
return 0, "done", dict(facts, why=f"past budget — rounds {rounds}/{max_rounds} on the "
|
|
622
|
+
"operator's go")
|
|
623
|
+
return 5, "budget", dict(facts, why=f"past budget — operator's go required: done at round "
|
|
624
|
+
f"{rounds}/{max_rounds}")
|
|
625
|
+
if rounds >= max_rounds:
|
|
626
|
+
owed = (f"{len(open_b)} blocker(s) open" if open_b else
|
|
627
|
+
"exit checks not supplied" if checks is None else
|
|
628
|
+
"exit checks not all green" if not green else "the second clean round still owed")
|
|
629
|
+
return 5, "budget", dict(facts, why=f"rounds {rounds}/{max_rounds} reached before done — {owed}")
|
|
630
|
+
if open_b and releases >= max_releases:
|
|
631
|
+
return 5, "budget", dict(facts, why=f"releases {releases}/{max_releases} reached before "
|
|
632
|
+
f"done — {len(open_b)} blocker(s) open")
|
|
633
|
+
if rounds >= 2 and new[-1] >= new[-2] > 0:
|
|
634
|
+
return 6, "blocked", dict(facts, why=f"diverging — new blocking {new[-2]} → {new[-1]} "
|
|
635
|
+
"did not fall; run the loop guard's break protocol")
|
|
636
|
+
if rounds >= 2 and new[-1] >= new[-2] and selfinf and selfinf >= new[-1] - selfinf:
|
|
637
|
+
return 6, "blocked", dict(facts, why=f"diverging — round {rounds}: {selfinf} self-inflicted "
|
|
638
|
+
f"≥ {new[-1] - selfinf} new, and the count did not "
|
|
639
|
+
"fall; run the loop guard's break protocol")
|
|
640
|
+
why = (f"{len(open_b)} blocker(s) open" if open_b else
|
|
641
|
+
"exit checks not supplied — pass --checks K/M read on the artefact" if checks is None else
|
|
642
|
+
"exit checks not all green" if not green else
|
|
643
|
+
"a second round with no new blocking finding is owed")
|
|
644
|
+
return 3, "continue", dict(facts, why=why)
|
|
645
|
+
|
|
646
|
+
|
|
647
|
+
def cmd_finish(a) -> int:
|
|
648
|
+
try:
|
|
649
|
+
_, items = load_inventory(pathlib.Path(a.inventory))
|
|
650
|
+
except Fail as exc:
|
|
651
|
+
raise Fail(1, exc.reasons) # a malformed inventory is malformed here, never usage
|
|
652
|
+
for flag, v, least in (("--max-rounds", a.max_rounds, 2), ("--max-releases", a.max_releases, 1),
|
|
653
|
+
("--releases", a.releases, 0), ("--rounds", a.rounds, 0)):
|
|
654
|
+
if v is not None and v < least:
|
|
655
|
+
raise Fail(1, [f"{flag} {v} is below {least}" + (
|
|
656
|
+
" — done needs two clean rounds, so a budget of one can never be met"
|
|
657
|
+
if flag == "--max-rounds" else "")])
|
|
658
|
+
# THE ROUND COUNT IS EXPLICIT. A clean round leaves no item, so a count derived from the
|
|
659
|
+
# items can never see the two clean rounds `done` needs: the verdict would stay
|
|
660
|
+
# `continue` for ever. It comes from --rounds or from the ledger's `round:` lines.
|
|
661
|
+
led = ledger_rounds(pathlib.Path(a.ledger)) if a.ledger else None
|
|
662
|
+
if led is not None and not led and a.rounds is None:
|
|
663
|
+
raise Fail(1, [f"{a.ledger} carries no `round:` line and --rounds is absent — the round "
|
|
664
|
+
"count cannot be read"])
|
|
665
|
+
if led is None and a.rounds is None:
|
|
666
|
+
raise Fail(1, ["the round count is not given — pass --rounds N, or --ledger with the run "
|
|
667
|
+
"ledger's `round:` lines; clean rounds leave no item to count"])
|
|
668
|
+
rounds = a.rounds if a.rounds is not None else max(led)
|
|
669
|
+
if led and a.rounds is not None and a.rounds != max(led):
|
|
670
|
+
raise Fail(1, [f"--rounds {a.rounds} and the ledger's {max(led)} round(s) disagree"])
|
|
671
|
+
if led:
|
|
672
|
+
counted = {r: sum(1 for x in items if x.get("triage") == "blocking" and x.get("round") == r)
|
|
673
|
+
for r in led}
|
|
674
|
+
bad = [f"round {r}: the ledger says new blocking {k}, the inventory holds {counted[r]}"
|
|
675
|
+
for r, k in sorted(led.items()) if counted[r] != k]
|
|
676
|
+
if bad:
|
|
677
|
+
raise Fail(1, bad)
|
|
678
|
+
checks = None
|
|
679
|
+
if a.checks:
|
|
680
|
+
m = re.fullmatch(r"(\d+)/(\d+)", a.checks.strip())
|
|
681
|
+
if not m or int(m[1]) > int(m[2]) or int(m[2]) == 0:
|
|
682
|
+
raise Fail(1, [f"--checks {a.checks!r} is not K/M green of M exit checks"])
|
|
683
|
+
checks = (int(m[1]), int(m[2]))
|
|
684
|
+
if a.artefact and not ARTEFACT.match(a.artefact):
|
|
685
|
+
raise Fail(1, [f"--artefact {a.artefact!r} is not NAME@SHA (a commit of 7-40 hex digits)"])
|
|
686
|
+
code, word, f = finish_verdict(items, rounds, a.max_rounds, a.max_releases, a.releases, checks,
|
|
687
|
+
a.operator_go)
|
|
688
|
+
deferred = sorted((x for x in items if x.get("triage") == "deferred"), key=_priority)
|
|
689
|
+
noise = [x for x in items if x.get("triage") == "noise"]
|
|
690
|
+
risks = [x for x in deferred if x.get("blast") == 3 or x.get("rung") in ("I4", "I5")]
|
|
691
|
+
pool = sorted((x for x in items if x.get("triage") != "blocking" and _needs_node(x)
|
|
692
|
+
and x.get("triage") != "noise"), key=_priority)
|
|
693
|
+
budget = f"rounds {f['rounds']}/{a.max_rounds} — releases {a.releases}/{a.max_releases}"
|
|
694
|
+
if word == "continue":
|
|
695
|
+
print(f"next round: {f['rounds'] + 1}/{a.max_rounds} — {f['why']} — {budget}")
|
|
696
|
+
else:
|
|
697
|
+
print(f"finish: {word} — {budget}" + (f" — {f['why']}" if f.get("why") else ""))
|
|
698
|
+
print(" exit checks: " + (f"{checks[0]}/{checks[1]} green on {a.artefact or '<artefact not named>'}"
|
|
699
|
+
if checks else "not supplied — read them on the artefact (--checks K/M)"))
|
|
700
|
+
print(f" blocking: {len(f['open'])} open · {f['fixed']} fixed — new per round "
|
|
701
|
+
f"{' · '.join(str(n) for n in f['new']) or 'none'}"
|
|
702
|
+
+ (f" — open: {', '.join(x['id'] for x in f['open'])}" if f["open"] else ""))
|
|
703
|
+
top = ", ".join(_board_id(x) for x in deferred[:5]) + (" …" if len(deferred) > 5 else "")
|
|
704
|
+
print(f" deferred: {len(deferred)}" + (f" → {top}" if deferred else ""))
|
|
705
|
+
print(f" noise: {len(noise)}")
|
|
706
|
+
print(" known risks: " + (", ".join(_board_id(x) for x in risks) or "none"))
|
|
707
|
+
print(" next: " + (f"{_board_id(pool[0])} (new iteration)" if pool
|
|
708
|
+
else "highest-priority board row (read the board)"))
|
|
709
|
+
return code
|
|
710
|
+
|
|
711
|
+
|
|
507
712
|
def main(argv: list[str]) -> int:
|
|
508
713
|
p = argparse.ArgumentParser(prog="waves.py", description=__doc__.split("\n")[0])
|
|
509
714
|
sub = p.add_subparsers(dest="cmd", required=True)
|
|
@@ -531,9 +736,20 @@ def main(argv: list[str]) -> int:
|
|
|
531
736
|
"a lowering is refused")
|
|
532
737
|
s.add_argument("--only-present", action="store_true",
|
|
533
738
|
help="ignore plan nodes this graph does not hold")
|
|
739
|
+
f = sub.add_parser("finish", help="the finish-line verdict from the triaged items, and the block")
|
|
740
|
+
f.add_argument("--inventory", required=True)
|
|
741
|
+
f.add_argument("--max-rounds", type=int, required=True, help="run.finish.maxRounds (default 3, at least 2)")
|
|
742
|
+
f.add_argument("--max-releases", type=int, required=True, help="run.finish.maxReleases (default 2)")
|
|
743
|
+
f.add_argument("--releases", type=int, default=0, help="releases this iteration made so far")
|
|
744
|
+
f.add_argument("--rounds", type=int, help="rounds run, clean ones included")
|
|
745
|
+
f.add_argument("--ledger", help="the run ledger; its `round:` lines are the round count")
|
|
746
|
+
f.add_argument("--operator-go", action="store_true",
|
|
747
|
+
help="the operator's recorded go for rounds past --max-rounds")
|
|
748
|
+
f.add_argument("--checks", help="K/M — exit checks green on the artefact")
|
|
749
|
+
f.add_argument("--artefact", help="what they ran on, as NAME@SHA")
|
|
534
750
|
a = p.parse_args(argv)
|
|
535
751
|
verbs = {"merge": cmd_merge, "assign": cmd_assign, "count": cmd_count, "validate": cmd_validate,
|
|
536
|
-
"sync-graph": cmd_sync_graph}
|
|
752
|
+
"sync-graph": cmd_sync_graph, "finish": cmd_finish}
|
|
537
753
|
try:
|
|
538
754
|
return verbs[a.cmd](a)
|
|
539
755
|
except Fail as exc:
|
|
@@ -18,6 +18,7 @@
|
|
|
18
18
|
- Documentation (the phase-1b inventory — the four questions)
|
|
19
19
|
- Scope
|
|
20
20
|
- Requirements (the REQ spine — every later stage traces to these IDs)
|
|
21
|
+
- Finish line (when this iteration stops)
|
|
21
22
|
- Users & context
|
|
22
23
|
- Decisions locked (the grill's output)
|
|
23
24
|
- Autonomy (the sweep — stages 1→10 read this instead of asking)
|
|
@@ -101,6 +102,19 @@ not continuously): `open` → `planned` → `built` → `verified` \| `partial`
|
|
|
101
102
|
> task in smaller terms is the failure this table exists to prevent: every gate
|
|
102
103
|
> after it goes green on the shrunken task and nothing reports the loss.
|
|
103
104
|
|
|
105
|
+
## Finish line (when this iteration stops)
|
|
106
|
+
|
|
107
|
+
The contract the iteration stops against (the skill's `references/finish-line.md`). Written
|
|
108
|
+
here, confirmed with the brief, and revised only as a recorded revision — a finding is not one.
|
|
109
|
+
|
|
110
|
+
- **Objective:** <one sentence: what is true when this iteration is done>
|
|
111
|
+
- **Exit checks:** <REQ id → command, run on the artefact the operator uses: released /
|
|
112
|
+
installed / deployed — one line each>
|
|
113
|
+
- **Scope:** <the modules, paths and REQ ids this iteration may change, as a list>
|
|
114
|
+
- **Budget:** `maxRounds` 3 · `maxReleases` 2 (the defaults; `pipeline.json` →
|
|
115
|
+
`run.finish` overrides them)
|
|
116
|
+
- **Base:** <the commit this iteration started from>
|
|
117
|
+
|
|
104
118
|
## Users & context
|
|
105
119
|
|
|
106
120
|
- **Who / for what:** … (personas, the job being done)
|
|
@@ -75,7 +75,7 @@
|
|
|
75
75
|
"route": "/",
|
|
76
76
|
"motion": "reduced",
|
|
77
77
|
"captured_at": "2026-10-07T00:00:00Z",
|
|
78
|
-
"source": "the capability that took it, e.g.
|
|
78
|
+
"source": "the capability and engine that took it, e.g. playwright-cli screenshot (webkit)"
|
|
79
79
|
},
|
|
80
80
|
"figma_frame": "https://www.figma.com/design/<fileKey>/<file>?node-id=<frame>",
|
|
81
81
|
"baseline": null,
|
|
@@ -120,7 +120,7 @@
|
|
|
120
120
|
"route": "/",
|
|
121
121
|
"motion": "reduced",
|
|
122
122
|
"captured_at": "2026-10-07T00:00:00Z",
|
|
123
|
-
"source": "the capability that took it, e.g.
|
|
123
|
+
"source": "the capability and engine that took it, e.g. playwright-cli screenshot (webkit)"
|
|
124
124
|
},
|
|
125
125
|
"figma_frame": "https://www.figma.com/design/<fileKey>/<file>?node-id=<frame>",
|
|
126
126
|
"baseline": null,
|
|
@@ -165,7 +165,7 @@
|
|
|
165
165
|
"route": "/",
|
|
166
166
|
"motion": "reduced",
|
|
167
167
|
"captured_at": "2026-10-07T00:00:00Z",
|
|
168
|
-
"source": "the capability that took it, e.g.
|
|
168
|
+
"source": "the capability and engine that took it, e.g. playwright-cli screenshot (webkit)"
|
|
169
169
|
},
|
|
170
170
|
"figma_frame": "https://www.figma.com/design/<fileKey>/<file>?node-id=<frame>",
|
|
171
171
|
"baseline": null,
|
|
@@ -210,7 +210,7 @@
|
|
|
210
210
|
"route": "/",
|
|
211
211
|
"motion": "reduced",
|
|
212
212
|
"captured_at": "2026-10-07T00:00:00Z",
|
|
213
|
-
"source": "the capability that took it, e.g.
|
|
213
|
+
"source": "the capability and engine that took it, e.g. playwright-cli screenshot (webkit)"
|
|
214
214
|
},
|
|
215
215
|
"figma_frame": null,
|
|
216
216
|
"baseline": null,
|
|
@@ -251,7 +251,7 @@
|
|
|
251
251
|
"route": "/",
|
|
252
252
|
"motion": "reduced",
|
|
253
253
|
"captured_at": "2026-10-07T00:00:00Z",
|
|
254
|
-
"source": "the capability that took it, e.g.
|
|
254
|
+
"source": "the capability and engine that took it, e.g. playwright-cli screenshot (webkit)"
|
|
255
255
|
},
|
|
256
256
|
"figma_frame": null,
|
|
257
257
|
"baseline": null,
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
|
3
3
|
"$id": "https://github.com/ssheleg/task-pipeline/blob/main/plugins/task-pipeline/skills/task-pipeline/templates/inventory.schema.json",
|
|
4
4
|
"title": "inventory/1 — <artifacts>/specs/YYYY-MM-DD-<topic>-inventory.json",
|
|
5
|
-
"description": "Wave 0's single machine-readable inventory: one item per open item of the board, every survey and every harvested source (references/waves.md, Wave 0). scripts/waves.py merges audit outputs into it (`merge`), writes `module`, `node`, `wave` and `release` from two short tables (`assign`), counts it (`count`)
|
|
5
|
+
"description": "Wave 0's single machine-readable inventory: one item per open item of the board, every survey and every harvested source (references/waves.md, Wave 0). scripts/waves.py merges audit outputs into it (`merge`), writes `module`, `node`, `wave` and `release` from two short tables (`assign`), counts it (`count`), refuses an item that lands nowhere (`validate`) and computes the finish-line verdict from the triaged items (`finish`, references/finish-line.md). The document is an object carrying `schema` and `items`; a bare array of items is read as the items of one. Fields a project needs beyond these go under an `x-` prefix.",
|
|
6
6
|
"type": "object",
|
|
7
7
|
"additionalProperties": false,
|
|
8
8
|
"required": ["schema", "items"],
|
|
@@ -17,6 +17,14 @@
|
|
|
17
17
|
"additionalProperties": false,
|
|
18
18
|
"patternProperties": {"^x-[a-z0-9-]+$": {}},
|
|
19
19
|
"required": ["id", "title", "area", "kind", "severity", "evidence", "proposed_fix"],
|
|
20
|
+
"allOf": [
|
|
21
|
+
{"if": {"required": ["triage"], "properties": {"triage": {"const": "blocking"}}},
|
|
22
|
+
"then": {"required": ["test", "round"]}},
|
|
23
|
+
{"if": {"required": ["test"]},
|
|
24
|
+
"then": {"required": ["triage"], "properties": {"triage": {"const": "blocking"}}}},
|
|
25
|
+
{"if": {"required": ["test", "base_repro"], "properties": {"test": {"const": "B3"}}},
|
|
26
|
+
"then": {"properties": {"base_repro": {"not": {"const": "yes"}}}}}
|
|
27
|
+
],
|
|
20
28
|
"properties": {
|
|
21
29
|
"id": {
|
|
22
30
|
"description": "The item's id in its owning register. An item without one gets one there, in the same change. Unique across the inventory.",
|
|
@@ -60,6 +68,12 @@
|
|
|
60
68
|
"blocked_by": {"type": "array", "items": {"type": "string"}},
|
|
61
69
|
"closable_in_one_pass": {"type": "boolean"},
|
|
62
70
|
"needs_operator": {"type": "boolean"},
|
|
71
|
+
"triage": {"description": "The finish-line class, set when the finding is found (references/finish-line.md → Triage at discovery). Only blocking findings argue for another round; deferred and noise never affect `waves.py finish`.", "enum": ["blocking", "deferred", "noise"]},
|
|
72
|
+
"test": {"description": "The blocking test that matched, first match wins: B1 an exit check is red; B2 a hotfix-lane entry; B3 it passes at Base; B4 a verified REQ or a published claim is false on the artefact. Required on a blocking item, refused on any other.", "enum": ["B1", "B2", "B3", "B4"]},
|
|
73
|
+
"round": {"description": "The finish-line round that found it, from 1. Required on a blocking item.", "type": "integer", "minimum": 1},
|
|
74
|
+
"base_repro": {"description": "Does the failure reproduce at the iteration's Base? `no` is a finding this iteration caused (self-inflicted).", "enum": ["yes", "no", "n/a"]},
|
|
75
|
+
"blast": {"description": "Who it hurts, 1–3 (references/prioritisation.md, backlog `sev × blast`): a priority input of a deferred row. `waves.py finish` lists a deferred item at blast 3 as a known risk.", "type": "integer", "minimum": 1, "maximum": 3},
|
|
76
|
+
"rung": {"description": "The impact rung (references/prioritisation.md): a priority input of a deferred row. `waves.py finish` lists a deferred item at I4 or I5 as a known risk.", "enum": ["I1", "I2", "I3", "I4", "I5"]},
|
|
63
77
|
"notes": {"type": "string"}
|
|
64
78
|
}
|
|
65
79
|
},
|
|
@@ -20,7 +20,7 @@ Run: `<topic>` · started `<YYYY-MM-DD>` · module map: `<path or "none">`
|
|
|
20
20
|
|
|
21
21
|
## `read:` — which doctrine this run actually opened
|
|
22
22
|
|
|
23
|
-
The bundle is
|
|
23
|
+
The bundle is 53 reference files and nothing recorded which of them a run read, so **a
|
|
24
24
|
skipped file and a read one were indistinguishable** — the class every guard in this
|
|
25
25
|
pipeline exists to catch, left standing over the doctrine itself.
|
|
26
26
|
|
|
@@ -48,7 +48,7 @@ same claim one level down.
|
|
|
48
48
|
|---|---|---|
|
|
49
49
|
| `unmeasured — no run ledger` | there is no ledger | nothing to read from |
|
|
50
50
|
| `unmeasured — the ledger carries no read: lines` | the hook is absent, **or** the run opened no doctrine | two opposite facts, and the ledger cannot separate them, so neither is claimed |
|
|
51
|
-
| `N of
|
|
51
|
+
| `N of 53 reference files read — unattested`, then each unread one | the hook is installed and fired | the count alone says there is a gap, not where — and `unattested` says the ledger cannot name who wrote the lines |
|
|
52
52
|
|
|
53
53
|
**It is a disclosure: no floor, no direction, never a target.** A run that needs four files
|
|
54
54
|
and reads four is not worse than one that reads thirty — and the moment the number becomes
|
|
@@ -71,6 +71,9 @@ holds: <stage id> — <n> (<class: what, owner>; … or "none") — enumerated <
|
|
|
71
71
|
gate: <stage id> — command "<cmd>" — exit <N> — <ISO-8601>
|
|
72
72
|
event: <compact|session-end|subagent|memory> — <detail> — <ISO-8601>
|
|
73
73
|
review: <stage id> — surface <name> — rounds <N> — <returned|approved|unresolved> — <ISO-8601>
|
|
74
|
+
finding: <id> — blocking:B<n> | deferred:B-NNN | noise — round <N> — base-repro yes|no|n/a
|
|
75
|
+
round: <N> — <reading> — new blocking <k>
|
|
76
|
+
finish: done|budget|blocked — rounds <n>/<max> — releases <n>/<max>
|
|
74
77
|
read: references/<file>.md # hook-appended, deduped, UNATTESTED (no writer field)
|
|
75
78
|
```
|
|
76
79
|
|
|
@@ -118,6 +121,20 @@ read: references/<file>.md # hook-appended, deduped, UNATTESTED (no
|
|
|
118
121
|
into the checkpoint of the stage it names, and stage 10 copies the last one per surface
|
|
119
122
|
into the acceptance file. **It is a measurement, never a target**: a number of passes to
|
|
120
123
|
beat is an instruction to stop showing the sheet.
|
|
124
|
+
- **`finding:`** — one line per finding, written when it is found and before anyone fixes
|
|
125
|
+
it (`references/finish-line.md` → *Triage at discovery*, inside the skill). The class is
|
|
126
|
+
one of `blocking`, `deferred`, `noise`; a blocking line names its test, one of `B1`, `B2`,
|
|
127
|
+
`B3`, `B4`; a deferred line names its board row. `base-repro` says whether the failure
|
|
128
|
+
reproduces at the iteration's Base — `no` is a finding this iteration caused.
|
|
129
|
+
- **`round:`** — one line per finish-line round, **clean rounds included**: its number, the
|
|
130
|
+
independent reading it ran (an audit axis, an observer, the post-release readback) and how
|
|
131
|
+
many new blocking findings it produced. A clean round leaves no `finding:` line, so without
|
|
132
|
+
this line two clean rounds and no rounds look the same; `scripts/waves.py finish --ledger`
|
|
133
|
+
counts the rounds from it and refuses when its counts disagree with the inventory.
|
|
134
|
+
- **`finish:`** — one line when the iteration stops, beside stage 10's verdict: `done`,
|
|
135
|
+
`budget` or `blocked`, with the rounds and releases spent against their budget. The
|
|
136
|
+
block under it is `references/finish-line.md` → *The finish block*;
|
|
137
|
+
`scripts/waves.py finish` computes the verdict from the inventory.
|
|
121
138
|
- **`touch:`** — one line per file per pass, and the reason names **what forced the
|
|
122
139
|
edit**: a finding id, a failed gate item, an operator instruction. *"Cleanup"*,
|
|
123
140
|
*"polish"* and *"while I was there"* are not reasons; they are churn with better
|
|
@@ -141,11 +158,18 @@ event: compact — auto — 2026-08-10T11:58Z
|
|
|
141
158
|
event: subagent — general-purpose — 2026-08-10T12:00Z
|
|
142
159
|
gate: 6 — command "npm test" — exit 0 — 2026-08-10T12:02Z
|
|
143
160
|
review: 6 — surface landing — rounds 1 — returned — 2026-08-10T12:02Z
|
|
161
|
+
finding: F-021 — blocking:B3 — round 1 — base-repro no
|
|
162
|
+
finding: F-022 — deferred:B-118 — round 1 — base-repro yes
|
|
163
|
+
round: 1 — exit checks + audit axis seams — new blocking 1
|
|
164
|
+
finding: F-023 — noise — round 2 — base-repro n/a
|
|
165
|
+
round: 2 — exit checks + observer — new blocking 0
|
|
166
|
+
round: 3 — exit checks + post-release readback — new blocking 0
|
|
144
167
|
stage: 6 Tests — gate manual — verdict pass — 2026-08-10T12:03Z
|
|
145
168
|
hand: 3 — task "add CSV export to the orders table" — done 2 — surfaced 1 — decisions 1 — amb 2 (OQ-0007, ledger row 4)
|
|
146
169
|
scope 5f21ac3/node-24-linux/REQ-001,REQ-004 — unverified 1 (XLSX path: no fixture)
|
|
147
170
|
holds: 5 — 2 (worktree: build-csv-export, this run; container: pg-test, this run) — enumerated 8/8 classes
|
|
148
171
|
holds: 10 — none — enumerated 7/8 classes, unlooked: containers (no docker on this host)
|
|
172
|
+
finish: done — rounds 3/3 — releases 1/2
|
|
149
173
|
```
|
|
150
174
|
|
|
151
175
|
The last two lines are a **trip**: the same file, two consecutive passes, the same
|