task-pipeline-skill 1.95.0 → 1.96.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 +33 -0
- package/README.md +2 -1
- package/SKILL-CARD.md +1 -1
- 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/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/stages.md +6 -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/inventory.schema.json +15 -1
- package/plugins/task-pipeline/skills/task-pipeline/templates/run.md +26 -2
|
@@ -97,7 +97,8 @@ This spans the whole run, not one stage:
|
|
|
97
97
|
finished.
|
|
98
98
|
|
|
99
99
|
It stops for exactly four things: an unmet `manual` gate, a block it cannot resolve, a
|
|
100
|
-
genuine ambiguity the brief does not answer, and completion
|
|
100
|
+
genuine ambiguity the brief does not answer, and completion — the iteration's finish line
|
|
101
|
+
reached ([`finish-line.md`](finish-line.md)), never a search that ran dry.
|
|
101
102
|
|
|
102
103
|
**A block or an ambiguity stops its node, not the run.** An ambiguity whose answer is
|
|
103
104
|
internal and reversible is not a stop at all: the run takes the recommended answer, records
|
|
@@ -112,6 +113,11 @@ recorded reasons: external waits go on watchers, human decisions go into the que
|
|
|
112
113
|
batch. Exit 4 does not prove a human gate or completion. A watcher keeps the recorded
|
|
113
114
|
next action alive while no independent node is runnable. **When the board is empty, say so once**, and do not invent work to fill it.
|
|
114
115
|
|
|
116
|
+
**Stop is not idle.** A finish line reached closes that iteration — stages 9–10, the
|
|
117
|
+
hand-back, the retro — and on an armed loop the highest-priority board row starts a new
|
|
118
|
+
iteration with its own contract. Re-opening the closed one to polish it is not continuing
|
|
119
|
+
([`finish-line.md`](finish-line.md) → *Stop is not idle*).
|
|
120
|
+
|
|
115
121
|
**This extends [`build.md`](build.md); it does not replace it.** That file's
|
|
116
122
|
*Continuous execution* rule governs the inside of a stage-5 execution — do not
|
|
117
123
|
check in between the tasks of a plan you were told to execute. It is unconditional
|
|
@@ -59,6 +59,7 @@ Custom profiles use their own declared stages and doctrine, with the kernel inta
|
|
|
59
59
|
| **first run in a project** (new or existing) | `references/adoption.md` |
|
|
60
60
|
| **first run · the entry audit** (offered once) | `references/setup.md` |
|
|
61
61
|
| **what travels with the bundle vs stays in a project** | `references/portability.md` |
|
|
62
|
+
| 0–10 · when an iteration stops — the contract fixed at intake, triage at discovery, the round budget | `references/finish-line.md` |
|
|
62
63
|
| any repeating loop | `references/loop-guard.md` |
|
|
63
64
|
| run-wide · what the run **leaves running and leaves behind** — every gate, and stage 10 | `references/residue.md` |
|
|
64
65
|
| run-wide · what the run **prints about itself** — the rail, the iteration line | `references/progress.md` |
|
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
# The finish line — when an iteration stops
|
|
2
|
+
|
|
3
|
+
An iteration stops when the work it promised is done **on the artefact the operator uses**.
|
|
4
|
+
It does not stop when a search runs dry, because a search never does: every reading finds
|
|
5
|
+
something, every fix is new surface for the next reading, and find → fix → find has no end of
|
|
6
|
+
its own. This file is that end. The verdict is computed by `scripts/waves.py finish`.
|
|
7
|
+
|
|
8
|
+
## Contents
|
|
9
|
+
|
|
10
|
+
- The contract, fixed at intake
|
|
11
|
+
- Triage at discovery
|
|
12
|
+
- Rounds and the stop
|
|
13
|
+
- Computing the verdict
|
|
14
|
+
- Stop is not idle
|
|
15
|
+
- The finish block
|
|
16
|
+
- Rationalizations
|
|
17
|
+
|
|
18
|
+
## The contract, fixed at intake
|
|
19
|
+
|
|
20
|
+
The brief's `## Finish line` ([`../templates/brief.md`](../templates/brief.md)) carries five
|
|
21
|
+
fields, written at stage 0 and confirmed with the brief:
|
|
22
|
+
|
|
23
|
+
- **Objective** — one sentence: what is true when this iteration is done.
|
|
24
|
+
- **Exit checks** — REQ ids, each with its command, run on the artefact the operator uses:
|
|
25
|
+
released, installed or deployed. A branch is not that artefact.
|
|
26
|
+
- **Scope** — the modules, paths and REQ ids this iteration may change, as a list.
|
|
27
|
+
- **Budget** — `run.finish.maxRounds` (default 3) and `run.finish.maxReleases` (default 2),
|
|
28
|
+
from `pipeline.json` ([`../pipeline.schema.json`](../pipeline.schema.json)).
|
|
29
|
+
- **Base** — the commit the iteration started from.
|
|
30
|
+
|
|
31
|
+
Widening the scope is a recorded revision of the brief, with the operator's go. A finding is
|
|
32
|
+
not one.
|
|
33
|
+
|
|
34
|
+
## Triage at discovery
|
|
35
|
+
|
|
36
|
+
Every finding that reaches a round is classed when it is found, before anyone fixes it. First
|
|
37
|
+
match wins. A defect in a node's own diff, caught before the node closes, follows
|
|
38
|
+
[`review.md`](review.md) instead: Critical and Important block that node.
|
|
39
|
+
|
|
40
|
+
**Blocking**, when:
|
|
41
|
+
- **B1** — an exit check is red.
|
|
42
|
+
- **B2** — it is, or once released would be, a hotfix-lane entry. The list's single home is [`iteration.md`](iteration.md)
|
|
43
|
+
→ *No fix travels alone*; restated here, kept equal by the validator:
|
|
44
|
+
- production is down
|
|
45
|
+
- data is being lost or corrupted
|
|
46
|
+
- a credential has leaked
|
|
47
|
+
- a security hole is open to someone outside
|
|
48
|
+
- money or a legal obligation is failing
|
|
49
|
+
- **B3** — the failing scenario passes at Base: this iteration broke it.
|
|
50
|
+
- **B4** — a REQ marked `verified`, or a claim this iteration publishes, is false on the
|
|
51
|
+
artefact. A published claim beyond the REQs may instead be narrowed in the same change.
|
|
52
|
+
|
|
53
|
+
**Deferred** — real, with a receipt, and no B test matches. It becomes a board row with its
|
|
54
|
+
evidence, its priority inputs ([`prioritisation.md`](prioritisation.md)) and
|
|
55
|
+
`found-in: <iteration>`. It is not fixed now.
|
|
56
|
+
|
|
57
|
+
**Noise** — no reproducible receipt; a duplicate (append its evidence to the existing row); a
|
|
58
|
+
Minor with no behavioural consequence; or an environment transient named by its cause. One
|
|
59
|
+
ledger line, nothing else.
|
|
60
|
+
|
|
61
|
+
Severity words, effort and "it is a one-line fix" never change the class. Each finding leaves
|
|
62
|
+
one line in the run ledger ([`../templates/run.md`](../templates/run.md)):
|
|
63
|
+
|
|
64
|
+
```
|
|
65
|
+
finding: <id> — blocking:B<n> | deferred:B-NNN | noise — round <N> — base-repro yes|no|n/a
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
## Rounds and the stop
|
|
69
|
+
|
|
70
|
+
A **round** is every exit check run once, plus one independent reading over the Scope: an
|
|
71
|
+
audit axis, an observer, or the post-release readback. Rotating the axis
|
|
72
|
+
([`audit.md`](audit.md)) never adds a round. Only new blocking findings count.
|
|
73
|
+
|
|
74
|
+
- **Done** — every exit check green on the artefact, and two consecutive rounds with no new
|
|
75
|
+
blocking finding. The second of them may read only the last fix's diff.
|
|
76
|
+
- **Diverging** — new blocking findings did not fall from one round to the next, or
|
|
77
|
+
self-inflicted ≥ new. Run [`loop-guard.md`](loop-guard.md)'s break protocol.
|
|
78
|
+
- **Budget spent** — `maxRounds` or `maxReleases` reached before done: a blocker open, or the
|
|
79
|
+
second clean round still owed. Stop and hand the iteration back: each open blocker becomes a
|
|
80
|
+
top board row marked `needs-operator`. No round runs past the budget without the operator's
|
|
81
|
+
go, given with the numbers; a done reached past it without that go is still `budget`.
|
|
82
|
+
|
|
83
|
+
The finish word is one of three: `done`; `budget`, the budget reached before done; `blocked`,
|
|
84
|
+
the rounds diverging, handed to the loop guard. Deferred findings never extend an iteration.
|
|
85
|
+
|
|
86
|
+
Each round leaves one ledger line, clean rounds included, because a clean round leaves no
|
|
87
|
+
finding to count:
|
|
88
|
+
|
|
89
|
+
```
|
|
90
|
+
round: <N> — <reading> — new blocking <k>
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
## Computing the verdict
|
|
94
|
+
|
|
95
|
+
```
|
|
96
|
+
python3 scripts/waves.py finish --inventory <inventory.json> --ledger .task-pipeline/run.md \
|
|
97
|
+
--max-rounds 3 --max-releases 2 --releases <n> --checks <k>/<m> --artefact <name>@<sha>
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
`--rounds N` may stand in for `--ledger`; with neither the count is unknown and the script
|
|
101
|
+
refuses. Exit codes:
|
|
102
|
+
|
|
103
|
+
- `0` — done: the last two rounds found no new blocking item, none is open, and every exit
|
|
104
|
+
check is green on the artefact.
|
|
105
|
+
- `3` — continue: a blocker open, a second clean round owed, or no `--checks` given.
|
|
106
|
+
- `5` — budget: reached before done, or done past it without `--operator-go`.
|
|
107
|
+
- `6` — blocked: diverging; run the loop guard's break protocol.
|
|
108
|
+
- `1` — malformed: the inventory, the ledger or the arguments, each reason named.
|
|
109
|
+
|
|
110
|
+
## Stop is not idle
|
|
111
|
+
|
|
112
|
+
Reaching the finish line closes the iteration. It does not end the work.
|
|
113
|
+
|
|
114
|
+
1. Close it: stages 9–10, the hand-back ([`progress.md`](progress.md) → *The hand-back*), the
|
|
115
|
+
retrospective.
|
|
116
|
+
2. On an armed loop ([`continuity.md`](continuity.md)), take the highest-priority board row as a
|
|
117
|
+
**new** iteration with its own contract. Choose by priority, not recency: the row found last
|
|
118
|
+
is not next because it is fresh.
|
|
119
|
+
3. Re-opening a closed iteration to polish it is the loop this rule ends.
|
|
120
|
+
|
|
121
|
+
## The finish block
|
|
122
|
+
|
|
123
|
+
Printed at stage 10, beside the verdict, and in the hand-back's `FINISH` field:
|
|
124
|
+
|
|
125
|
+
```
|
|
126
|
+
finish: done | budget | blocked — rounds <n>/<max> — releases <n>/<max>
|
|
127
|
+
exit checks: <k>/<m> green on <artefact>@<sha>
|
|
128
|
+
blocking: <n> open · <n> fixed
|
|
129
|
+
deferred: <n> → <ids, top priority first>
|
|
130
|
+
noise: <n>
|
|
131
|
+
known risks: <deferred rows at blast 3 or rung I4+, or none>
|
|
132
|
+
next: B-NNN (new iteration) | board empty
|
|
133
|
+
```
|
|
134
|
+
|
|
135
|
+
## Rationalizations
|
|
136
|
+
|
|
137
|
+
| Excuse | Reality |
|
|
138
|
+
|---|---|
|
|
139
|
+
| "One more pass, it keeps finding things" | Every reading finds something. Only a new blocking finding argues for another round |
|
|
140
|
+
| "It is a one-line fix, do it now" | Effort never changes the class. A deferred finding is a board row |
|
|
141
|
+
| "This one is critical" | A severity word is not a B test. Name B1–B4, or defer it |
|
|
142
|
+
| "Rotate the axis and keep going" | Rotation happens inside the round budget, never on top of it |
|
|
143
|
+
| "The budget is nearly enough, one more round" | A round past the budget needs the operator's go, with the numbers |
|
|
144
|
+
| "The deferred list is long, so we are not done" | Deferred findings never extend an iteration |
|
|
145
|
+
| "Re-open the last iteration, it is nearly perfect" | That is the loop this rule ends. Take the top board row as a new iteration |
|
|
@@ -36,7 +36,8 @@ A fix designed against today's state is the intermediate state that
|
|
|
36
36
|
seam, and the fix is then paid for twice. The finding gets an id in its owning register
|
|
37
37
|
and lands on a canvas node. Its wave decides when it is built.
|
|
38
38
|
|
|
39
|
-
**The hotfix lane is the only exception, and it is closed.**
|
|
39
|
+
**The hotfix lane is the only exception, and it is closed.** This list is its single home;
|
|
40
|
+
[`finish-line.md`](finish-line.md)'s blocking test B2 cites it. It has five entries:
|
|
40
41
|
- production is down;
|
|
41
42
|
- data is being lost or corrupted;
|
|
42
43
|
- a credential has leaked;
|
|
@@ -57,12 +58,12 @@ reason. "It is quick" is not one of the five entries.
|
|
|
57
58
|
| 4 | Question round | answers from the sources, then one batch to the operator | every question is answered, assumed, or blocks a named node |
|
|
58
59
|
| 5 | Plan | the work graph with waves and risk classes — `scripts/waves.py sync-graph` writes each node's `wave` and `risk` from the plan, so `graph.py close` enforces the rounds it owes — and one packet per node | every REQ lands on a node; [`planning.md`](planning.md)'s set comparison holds |
|
|
59
60
|
| 6 | Build | parallel agents, each owning disjoint files | each node is committed on its branch, with its own checks green |
|
|
60
|
-
| 7 | Verify | rounds per node, starting as each node lands | each node passes its risk class's rounds |
|
|
61
|
+
| 7 | Verify | rounds per node, starting as each node lands; each finding triaged at discovery ([`finish-line.md`](finish-line.md)) | each node passes its risk class's rounds |
|
|
61
62
|
| 8 | Seam check | cross-module checks per seam group | every seam the canvas names is exercised end to end |
|
|
62
63
|
| 9 | Docs | stage 9 over everything the release iteration shipped | the doc gate passes and the docs match the code |
|
|
63
64
|
| 10 | Release | versions, tags, pins, changelogs, in dependency order | the published artefacts resolve |
|
|
64
|
-
| 11 | Post-release check | the stage-8 trio on what was released | every item the release claims closed is checked on the released artefact |
|
|
65
|
-
| 12 | Next pool | the re-measured inventory and the proposal | the hand-back is sent |
|
|
65
|
+
| 11 | Post-release check | the stage-8 trio on what was released, read as a finish-line round | every item the release claims closed is checked on the released artefact, and `finish:` is computed |
|
|
66
|
+
| 12 | Next pool | the re-measured inventory and the proposal | the hand-back, with its `finish:` line, is sent |
|
|
66
67
|
| 13 | Retrospective | the project retro, plus the shared lesson store where one exists | written last ([`retrospective.md`](retrospective.md)) |
|
|
67
68
|
|
|
68
69
|
Steps 1–4 are stages 0–4 run once. Steps 6–8 are stages 5–8 per wave, and a wave's
|
|
@@ -218,7 +219,10 @@ actually published, installed or deployed — never on the branch. Each item the
|
|
|
218
219
|
claims closed is re-checked against the released artefact. For installed packages and
|
|
219
220
|
skills, follow [`deploy-targets.md`](deploy-targets.md) → *The verification trio*: update
|
|
220
221
|
within the authorized rollout and read back each claimed installed channel. A failure goes to the hotfix
|
|
221
|
-
lane if it is one of its five entries.
|
|
222
|
+
lane if it is one of its five entries. Every post-release failure is triaged at the finish
|
|
223
|
+
line ([`finish-line.md`](finish-line.md) → *Triage at discovery*): a blocking one is fixed in
|
|
224
|
+
this release iteration, and the corrective release counts against `run.finish.maxReleases`; a
|
|
225
|
+
deferred one goes to the next pool, with an id.
|
|
222
226
|
|
|
223
227
|
## The next pool
|
|
224
228
|
|
|
@@ -101,6 +101,11 @@ review cap reached — 3 rounds — artifact: test/validate.py
|
|
|
101
101
|
- **New > self-inflicted** — the reader is still paying. Continuing is then the
|
|
102
102
|
operator's call, made with the numbers in hand rather than out of fatigue.
|
|
103
103
|
|
|
104
|
+
At the level of a whole iteration only new **blocking** findings argue for continuing; a
|
|
105
|
+
deferred or noise finding is paid for on the board, not with another round
|
|
106
|
+
([`finish-line.md`](finish-line.md) → *Rounds and the stop*). The class is read from the
|
|
107
|
+
run ledger's `finding:` lines and the rounds from its `round:` lines, never from memory.
|
|
108
|
+
|
|
104
109
|
**The pair is the whole point.** A round count alone says how tired everyone is; the
|
|
105
110
|
pair says whether the loop still produces anything. Measured on one file once, the
|
|
106
111
|
guard's shapes and the run's own prose disagreed — one still paying, one exhausted —
|
|
@@ -64,6 +64,7 @@ a row pointing outside the bundle is the defect this file exists to catch.
|
|
|
64
64
|
| The entry audit and what it inspects | `references/setup.md` |
|
|
65
65
|
| The ladder, seams, axis rotation, ratchets | `references/audit.md` |
|
|
66
66
|
| Loop detection and its caps | `references/loop-guard.md` |
|
|
67
|
+
| **The finish line** — when an iteration stops: the contract, triage at discovery, the round budget | `references/finish-line.md` |
|
|
67
68
|
| **What the run prints about itself** — the header block, the rail, the iteration line | `references/progress.md` |
|
|
68
69
|
| **What the run leaves running and leaves behind** — the eight classes, the `holds:` field, the teardown | `references/residue.md` |
|
|
69
70
|
| **The run mode** — item-by-item pacing, default off, what it never collapses | `references/continuity.md` |
|
|
@@ -169,6 +169,9 @@ SCOPE what this claim covers: the commit, the environment, the REQ
|
|
|
169
169
|
NOT VERIFIED what was built and NOT checked, or could not be. The literal
|
|
170
170
|
`none within the stated scope` is a valid answer; an empty
|
|
171
171
|
field is not
|
|
172
|
+
FINISH finish: done | budget | blocked — the finish block, as
|
|
173
|
+
finish-line.md prints it; mid-iteration, the round and
|
|
174
|
+
the open blocking count
|
|
172
175
|
|
|
173
176
|
DECISIONS WAITING <n> each as a question with options, asked HERE
|
|
174
177
|
AMBIGUITIES <n> computed, below
|
|
@@ -189,6 +192,11 @@ And `none within the stated scope` is a claim with a subject — it says *nothin
|
|
|
189
192
|
what SCOPE names is unverified*. Leaving the field empty says nothing at all, which reads
|
|
190
193
|
as the same thing and is not: canon 9a, one artifact over.
|
|
191
194
|
|
|
195
|
+
**`FINISH` says why the iteration stopped** ([`finish-line.md`](finish-line.md) → *The
|
|
196
|
+
finish block*): `done` with its exit checks green on the artefact, `budget` with each open
|
|
197
|
+
blocker a `needs-operator` board row, or `blocked`. A hand-back that cannot fill it has not
|
|
198
|
+
reached a finish line, and says which round it is in.
|
|
199
|
+
|
|
192
200
|
**Where there is no brief** — `checkup`, `setup`, a short path — TASK quotes the
|
|
193
201
|
operator's own sentence instead, marked as such. An unquotable TASK is a run that cannot
|
|
194
202
|
say what it was asked, which is worth its own line.
|
|
@@ -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
|
|
|
@@ -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
|
|
@@ -379,7 +381,9 @@ never that the work was skipped quietly.
|
|
|
379
381
|
surfaced that nobody asked for, every decision still waiting **asked here with
|
|
380
382
|
options rather than listed**, and the ambiguity count computed from the four
|
|
381
383
|
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
|
|
384
|
+
that cannot say what happened has not finished, it has stopped; **the `finish:` line is
|
|
385
|
+
printed beside this verdict — `done`, `budget` or `blocked`, with its block**
|
|
386
|
+
([`finish-line.md`](finish-line.md) → *The finish block*); **the environment is
|
|
383
387
|
given back** — all eight classes enumerated, everything this run started ended and
|
|
384
388
|
verified by re-enumerating rather than by the teardown's reply, an earlier run of
|
|
385
389
|
this project ended only when **provably spent**, an item this project does not own
|
|
@@ -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:
|