task-pipeline-skill 1.71.1 → 1.72.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 CHANGED
@@ -1,5 +1,56 @@
1
1
  # Changelog
2
2
 
3
+ ## v1.72.0 — a node says how it will be closed
4
+
5
+ **B-080 closed, and with it the last of four requirements this pack's own manifesto named
6
+ as unbuilt.** The Proof of Done manifesto cites this repository as its reference
7
+ implementation and filed four gaps against it by id; three closed on 2026-08-17, and this
8
+ is the fourth. The public document now says all four are built and names the commit for
9
+ each.
10
+
11
+ ### `check` — the per-node completion test
12
+
13
+ A node could not say how it would be closed, while shipped doctrine told the verifier to
14
+ "run the checks the task named". The doctrine read a field the schema did not have.
15
+
16
+ `check` is a string, **required on every node whose status is not `parked`** — the parked
17
+ node being the one nobody will close, where a placeholder would be confidence without
18
+ correctness. Never a list: a node needing two unrelated checks is a node doing two jobs, and
19
+ one gate made of two commands is `a && b`, which is still one gate. Stated twice on purpose,
20
+ in the schema and in `violations()`, because the schema never runs against a live graph.
21
+ `add --check` is required like `--why`, and a replan entry naming no check is refused before
22
+ the close writes anything.
23
+
24
+ ### The record the manifesto opens with now reads as one event
25
+
26
+ `residue.md`'s opening record is quoted by an external document and permalinked from a public
27
+ site. It said the inventory was queried "one minute later" beside a `ps` line showing the
28
+ process at 3:12 — two accurate observations of different things, framed so a reader had to
29
+ reconcile them by hand. Both moments are now stated, the interval between them named, and
30
+ **what the record never carried is named too**: the wall-clock times, which `git log --follow`
31
+ proves were never in it. A disambiguation, not a correction — no number changed.
32
+
33
+ The E4 sentence claimed more than its own block: "that **they** have been up for three days"
34
+ from a measurement of the oldest one. Now "the oldest of them".
35
+
36
+ ### Three plants that could not run, and one row that could not be parsed
37
+
38
+ CI failed twice on this release's work, and neither failure was noise.
39
+
40
+ A ledger row wrote a pytest filter as a backticked `-k a|b|c`; markdown does not care about
41
+ backticks, so two pipes split one cell into three and that row carried ten cells against a
42
+ header of eight. Locally the damage sat in a column nothing read, so every gate run passed.
43
+ Every `REQ` row must now match the header's cell count, with the remedy named in the refusal.
44
+
45
+ Then three schema plants died on `KeyError` before reaching their own `PLANT DID NOT LAND`
46
+ assert, because the new `check` rule added an `allOf` branch keyed on `not: {const: parked}`
47
+ and each plant indexed the const directly. All three traverse tolerantly now. **The assert
48
+ catches a plant that does not land; nothing catches a plant that cannot run** — the third
49
+ instance of that class in three days, and it is filed family-wide rather than here.
50
+
51
+ Guards: 389 → **390**. The new rule arrived without a plant, which this repository's own standard refuses; that is the plant. `graph.py` 114 → 129 cases.
52
+
53
+
3
54
 
4
55
  ## v1.71.1 — four shapes of a green that measured nothing
5
56
 
@@ -55,8 +106,20 @@ only reader that checks.
55
106
  and what makes each self-checking. Where a claim genuinely cannot be derived, **say so in the
56
107
  row**; correcting it silently a second time teaches readers that somebody else is checking.
57
108
 
58
- Guards: 376 → **376**. No guard was added or removed this release is doctrine, and each of the
59
- four rules is cited from the reference that owns it rather than restated.
109
+ > **After the tag, on the same tree.** `v1.71.1` is cut, and the guard-count claim
110
+ > `test/validate.py` reads is the newest `## vX.Y.Z` section's which post-tag is this one,
111
+ > with no version heading yet open for the work that follows. So the live figure is stated
112
+ > here rather than inside a released sentence, and the released sentence below is not edited:
113
+ > B-080 closed on 2026-08-19 with eight plants and TP-02 with five on the same day, so
114
+ > Guards: 376 → **389**. The guard has no home for a count between a tag and the next bump;
115
+ > filed as `B-104`, and the note is what keeps the number true meanwhile. **The bold belongs
116
+ > on the number alone**: the plant that corrupts this count reads `Guards: N → **M**` out of
117
+ > the raw section, so bolding the whole phrase left the validator reading a count no plant
118
+ > could still corrupt — which is the form this note was introduced with, and no second
119
+ > count-shaped figure belongs in this section for the same reason.
120
+
121
+ Guards at the tag: 376 → 376. No guard was added or removed — that release is doctrine, and
122
+ each of the four rules is cited from the reference that owns it rather than restated.
60
123
 
61
124
  Closes #39, #40, #48, #49.
62
125
 
package/CONTRIBUTING.md CHANGED
@@ -518,6 +518,19 @@ runs.
518
518
  *(guard: `does not say` and `rule 1 of the list it is printed beside`
519
519
  and `no stage names`)*
520
520
 
521
+ **56. A node says how it will be closed, and the doctrine that reads that field names
522
+ one the node has.** `agents/verifier.md` ordered the verifier to run *the checks the task
523
+ named* while `graph.schema.json`'s node carried no field in which a task could name one —
524
+ two files shipped on one day, and the instruction pointed at an absence that left the
525
+ verifier the two options the same paragraph forbids: invent a check, or run everything.
526
+ `check` is now a node property, **required on every node except a `parked` one** — the one
527
+ node nobody will close, where a placeholder would be worse than the gap. The rule is
528
+ stated twice on purpose, in the schema and in `violations()`, because the schema is never
529
+ applied to a live graph; both are RUN against a planted graph rather than inspected. And
530
+ the two homes are compared directly: whatever the verifier is told to read off the node
531
+ must be a property the schema declares.
532
+ *(guard: `node declares no` and `rule that can fire` and `off the node, and`)*
533
+
521
534
  ## Adding or changing doctrine
522
535
 
523
536
  - **Change one idea per PR.** These files are read by agents under load; a PR that
package/SKILL-CARD.md CHANGED
@@ -12,7 +12,7 @@ harmless.
12
12
  |---|---|
13
13
  | **Purpose** | Runs a substantial task through ten gated delivery stages — intake grill, docs study, brainstorm, spec, plan, subagent build, tests, lint/deploy, post-deploy, docs+registers, acceptance — refusing to advance until each gate passes |
14
14
  | **Owner** | ssheleg ([github.com/ssheleg/task-pipeline](https://github.com/ssheleg/task-pipeline)) |
15
- | **Version** | 1.71.1 |
15
+ | **Version** | 1.72.0 |
16
16
  | **Surface** | Claude Code (filesystem skill + plugin) and the vercel `skills` CLI. **Not** uploaded to the Skills API; custom Skills do not sync across surfaces |
17
17
  | **Dependencies** | None required. Optional: `context7` (MCP), `figma` (MCP), super-ux, agent-sync, graphify, obsidian-wiki, and **one of two browser channels** — `playwright` (CLI or MCP) or `chrome-devtools` (MCP); either satisfies the browser step and neither is required. Every stage's doctrine ships in-repo; the one conditional requirement is super-ux for the stage-3 UX track on a user-facing task |
18
18
  | **Evaluation status** | Suite authored, 5 categories. One recorded run, **self-observed by the author**; **zero blind runs on zero of three models** — the split, and the numbers, live in [`evals/RESULTS.md`](evals/RESULTS.md) and are computed by `evals/run.py` |
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "task-pipeline-skill",
3
- "version": "1.71.1",
3
+ "version": "1.72.0",
4
4
  "description": "Full-cycle delivery pipeline for coding agents: a mandatory built-in intake grill, then 10 gated stages (docs, brainstorm+decompose, spec, plan, build, tests, lint/deploy, post-deploy, docs/wiki, acceptance). Every stage's doctrine ships inside the skill — no companion plugin required. This package is the installer CLI.",
5
5
  "bin": {
6
6
  "task-pipeline": "bin/task-pipeline.js"
@@ -2,7 +2,7 @@
2
2
  "name": "task-pipeline",
3
3
  "displayName": "Task Pipeline",
4
4
  "description": "Runs a substantial task through a mandatory built-in intake grill, then 10 gated stages (docs, brainstorm+decompose, spec, plan, subagent build, tests, lint/deploy, post-deploy, docs/wiki, acceptance). Every stage's doctrine is built into the skill — no companion plugin required — with typed auto/manual gates, a frozen requirement spine that closes with evidence, a work board and a verification ledger that outlive a run, an exposure line naming what shipped unconfirmed, a progress rail computed from the project's own config, a loop guard whose review ceiling measures rather than stops, and stage-3 tracks for what a product does, how it sounds and how it looks. Two modes need no task: `checkup` (what is unverified) and `setup` (audit existing docs). Retro insights can publish upstream as issues, opt-in and redacted.",
5
- "version": "1.71.1",
5
+ "version": "1.72.0",
6
6
  "author": {
7
7
  "name": "ssheleg",
8
8
  "url": "https://x.com/sshlg93"
@@ -34,7 +34,10 @@ than ending in a question nobody will see.
34
34
  "not_done": ["what was asked and is not"],
35
35
  "not_verified": ["what was BUILT and no check touched"],
36
36
  "blockers": [{ "what": "…", "blocks": ["N-009"], "can_continue_around": true }],
37
- "replan": { "possible": true, "add": [], "park": ["N-009"], "why": "…" },
37
+ "replan": { "possible": true,
38
+ "add": [{ "title": "…", "owner": "implementer",
39
+ "serves": "REQ-004", "check": "…" }],
40
+ "park": ["N-009"], "why": "…" },
38
41
  "evidence": ["the command and the output that proves each `done` row"]
39
42
  }
40
43
  ```
@@ -58,12 +61,20 @@ ledger exists to catch.
58
61
 
59
62
  1. **Read the node's `serves`** — the REQ or the goal clause. That is the standard.
60
63
  Not what the diff does; what was asked.
61
- 2. **Run the checks the task named.** Not a check you invented, and not `npm test`
62
- alone if the task named something narrowera green from a check nobody watched
63
- fail against a planted defect is not evidence.
64
+ 2. **Run the node's `check`.** It is a field on the node — one command, or a named
65
+ judgement where no command can decide itand it is what closes this node. Not a
66
+ check you invented, and not `npm test` alone when the node named something narrower:
67
+ a green from a check nobody watched fail against a planted defect is not evidence.
68
+ Where the `check` is a judgement, record the verdict **as** judgement
69
+ (`references/gates.md`), never as an exit code.
70
+ **A node with no `check` is a refusal, not a judgement call.** `graph.py validate`
71
+ exits 1 and names it, because inventing a check and running everything are the two
72
+ things this step forbids — and until B-080 closed, this paragraph asked for a field
73
+ the schema did not have.
64
74
  3. **`done` takes one row per claim, and each needs a line in `evidence`** — the
65
75
  command and what it printed. Paraphrase is not evidence. If you cannot produce
66
- the output, the row belongs in `not_done`.
76
+ the output, the row belongs in `not_done`. The `check`'s own output is the first
77
+ row: the node said how it would be closed, so the proof it closed is that output.
67
78
  4. **`not_done` is not a failure report.** It is what the next iteration picks up,
68
79
  so write it as work rather than as blame.
69
80
  5. **Every blocker says what it `blocks` and whether the run `can_continue_around`
@@ -72,6 +83,9 @@ ledger exists to catch.
72
83
  6. **`replan.possible: false` needs a `why` a person can act on.** A stop with no
73
84
  reason is indistinguishable from a stall, and the operator is the one who has to
74
85
  tell them apart.
86
+ 7. **Every `replan.add` entry names its own `check`.** A node you create is a node the
87
+ next verifier has to close, and handing it the absence you were handed is how the
88
+ defect returns one iteration later. `close` refuses the verdict and names the key.
75
89
 
76
90
  ## Three ways this goes wrong
77
91
 
@@ -80,6 +94,7 @@ ledger exists to catch.
80
94
  | «The tests pass, so it is done» | The node serves a REQ, not a suite. A green suite that never exercised the requirement proves the suite ran |
81
95
  | «Close it and note the gap» | A `done` with a caveat is a `not_done` somebody will read as finished. Split the row |
82
96
  | «This blocker stops everything» | Say whether it does. `can_continue_around: true` is what keeps a run moving past one bad node, and guessing it wrong costs either the run or the correctness |
97
+ | «The node names no check, so I'll run the suite» | That is the choice this agent may not make. `validate` refuses the node before you are dispatched; a node whose completion test nobody wrote is a planning defect, and reporting it is worth more than a green suite |
83
98
 
84
99
  ## Where the doctrine is
85
100
 
@@ -14,6 +14,7 @@
14
14
  "status": "done",
15
15
  "blocked_by": [],
16
16
  "serves": "REQ-001",
17
+ "check": "npm test — graph.example.json validates against graph.schema.json",
17
18
  "evidence": [
18
19
  "npm test → PASS: task-pipeline structure valid, with graph.example.json validated against graph.schema.json"
19
20
  ]
@@ -27,6 +28,7 @@
27
28
  "N-001"
28
29
  ],
29
30
  "serves": "REQ-002",
31
+ "check": "python3 test/graph_test.py — the validate fixtures",
30
32
  "evidence": null
31
33
  },
32
34
  {
@@ -38,6 +40,7 @@
38
40
  "N-001"
39
41
  ],
40
42
  "serves": "REQ-005",
43
+ "check": "judgment: the reviewer rules the seven keys sufficient — recorded AS judgement, references/gates.md",
41
44
  "evidence": null
42
45
  },
43
46
  {
@@ -110,6 +110,15 @@
110
110
  "minLength": 1,
111
111
  "description": "A REQ id, or a clause of the goal. Required, because a node that serves neither is work nobody asked for — and the pipeline's answer to that is to park it WITH THAT AS THE REASON rather than to do it quietly."
112
112
  },
113
+ "check": {
114
+ "type": "string",
115
+ "minLength": 1,
116
+ "pattern": "\\S",
117
+ "not": {
118
+ "pattern": "[\\n\\r]"
119
+ },
120
+ "description": "**How this node will be closed** \u2014 the command a verifier runs, or the named judgement that stands in where no command can (`references/gates.md`'s third gate type: name the judge, record the verdict AS judgement). Its output becomes the node's `evidence` row.\n\nRequired on every node except a `parked` one, and that is the whole point of the field: `agents/verifier.md` tells the verifier to run *the check this node names*, so a node with none leaves it the two options that same paragraph forbids \u2014 invent a check, or run everything. Shipped doctrine read a field the schema did not have (B-080); the field the doctrine reads now exists.\n\nOne string, never a list, because the requirement it serves is singular throughout \u2014 one input, one job, one output, one owner, **one** completion test. A node needing two unrelated checks is a node doing two jobs, and the answer is to split it rather than to widen this field. One gate made of two commands is `a && b`, which is still one gate.\n\nA `parked` node is exempt because it is the one node nobody will close, and a placeholder there \u2014 *n/a, parked* \u2014 is the confidence-without-correctness this schema refuses everywhere else. The field survives a park: `park` sets the status and never removes what the node said it would run."
121
+ },
113
122
  "evidence": {
114
123
  "type": [
115
124
  "array",
@@ -139,6 +148,32 @@
139
148
  }
140
149
  },
141
150
  "allOf": [
151
+ {
152
+ "if": {
153
+ "properties": {
154
+ "status": {
155
+ "not": {
156
+ "const": "parked"
157
+ }
158
+ }
159
+ },
160
+ "required": [
161
+ "status"
162
+ ]
163
+ },
164
+ "then": {
165
+ "required": [
166
+ "check"
167
+ ],
168
+ "properties": {
169
+ "check": {
170
+ "type": "string",
171
+ "minLength": 1,
172
+ "pattern": "\\S"
173
+ }
174
+ }
175
+ }
176
+ },
142
177
  {
143
178
  "if": {
144
179
  "properties": {
@@ -27,15 +27,29 @@ Both are checked the same way and this file covers both.
27
27
 
28
28
  ## The measured reason this file exists
29
29
 
30
- On 2026-08-11, mid-run, a monitor was armed to watch CI. One minute later the
31
- harness task inventory was queried:
30
+ On 2026-08-11, mid-run, a monitor was armed to watch CI. **What follows is two
31
+ observations of two different things, taken at two different moments**, and each
32
+ one is stamped so that a reader is not left reconciling them. One minute after
33
+ the monitor was armed, the harness task inventory was queried; the process table
34
+ was read after that:
32
35
 
33
36
  ```
34
37
  TaskList → "No tasks found"
35
38
  ps -eo pid,etime,cmd → 52693 03:12 /bin/zsh -c … gh pr checks …
36
39
  ```
37
40
 
38
- The monitor was **alive and polling**, and the inventory tool reported nothing.
41
+ `etime` is elapsed life, so the `03:12` in that line dates its own reading:
42
+ three minutes and twelve seconds after the monitor was armed, a little over two
43
+ minutes after the inventory had come back empty. The monitor was **alive and
44
+ polling**, and the inventory tool reported nothing. Both readings were accurate.
45
+ They were readings of different things, made about two minutes apart.
46
+
47
+ **What this record does not carry, said plainly: the wall-clock times.** Neither
48
+ observation was stamped with the hour at which it was taken, so what was
49
+ observed is the two offsets above — one minute to the inventory, `03:12` of
50
+ process life at the `ps` reading — and the interval between them is derived from
51
+ those two. The absolute times are not recoverable from anything this record
52
+ kept, and nothing here reconstructs them.
39
53
 
40
54
  This is the class this whole doctrine exists to catch: **a check answered by
41
55
  something that is not its subject.** An inventory that does not enumerate the
@@ -201,9 +215,9 @@ judgement, the item is reported, not ended** — which puts it back under the ru
201
215
  above rather than creating an exception to it.
202
216
 
203
217
  **A foreign item never becomes spent.** The 18 containers above belong to other
204
- projects; that they have been up for three days is information for whoever owns them,
205
- not permission. The third owner state widens what a run may clean **inside its own
206
- project** and widens nothing at all outside it.
218
+ projects; that the oldest of them has been up for three days is information for
219
+ whoever owns them, not permission. The third owner state widens what a run may
220
+ clean **inside its own project** and widens nothing at all outside it.
207
221
 
208
222
  ## Rationalizations
209
223
 
@@ -264,8 +264,11 @@ never that the work was skipped quietly.
264
264
  decomposition is a decision, never an omission.
265
265
  - **Where the queue is a work graph, it is WRITTEN here**
266
266
  ([`work-graph.md`](work-graph.md)): `.task-pipeline/graph.json`, carrying the frozen REQ
267
- ids so `serves` resolves, one node per unit of work with its owner and what it touches,
268
- and an edge per dependency **naming what it hands over**. Then
267
+ ids so `serves` resolves, one node per unit of work with its owner, what it touches and
268
+ **the check that will close it**, and an edge per dependency **naming what it hands
269
+ over**. The check is the planner's job and not the verifier's: a node whose completion
270
+ test is decided by whoever happens to close it is a node two closes will disagree about,
271
+ and `validate` refuses one that names none. Then
269
272
  `python3 scripts/graph.py validate` — a graph that does not validate is not a queue, and
270
273
  `next` refuses to walk one. The reason to prefer it over a prose plan is measured, not
271
274
  aesthetic: a 400-node graph and a 4-node graph produce the same 27-byte frontier, so the
@@ -40,6 +40,7 @@ turn of every loop.
40
40
  | `nodes[].serves` | the REQ or goal clause it exists for. A node serving neither is **parked with that as the reason** |
41
41
  | `nodes[].blocked_by` | what must close first. This is what the frontier obeys |
42
42
  | `nodes[].touches` | what it **mutates**. Two runnable nodes writing one file is the false parallelism [`planning.md`](planning.md) refuses — *distinct is not the same as independent, and the check is what they touch, never what they are called* |
43
+ | `nodes[].check` | **how this node will be closed** — one command, or the named judgement where no command can decide it. Required on every node except a `parked` one. `agents/verifier.md` runs it and reports its output as the evidence row; before this field existed that instruction pointed at an absence, leaving the verifier the two things it forbids — invent a check, or run everything (B-080) |
43
44
  | `nodes[].evidence` | required when `status` is `done`. A node called done by assertion is what evidence exists to prevent |
44
45
  | `nodes[].parked_reason` | required when `status` is `parked`. A park with no reason is indistinguishable, a week later, from work quietly dropped |
45
46
  | `edges[].payload` | what the dependency hands over. **An edge carrying no named artifact is chronology drawn as architecture** |
@@ -68,6 +69,14 @@ the graph does. Ties break on declaration order, so the frontier is stable betwe
68
69
  an unstable one costs more than it looks, because an agent calling `next` twice starts the
69
70
  other node.
70
71
 
72
+ **One node, one completion test.** `check` is a string rather than a list, because the
73
+ requirement is singular throughout — one input, one job, one output, one owner, one
74
+ completion test. A node needing two unrelated checks is a node doing two jobs, and the
75
+ answer is to split it; one gate made of two commands is `a && b`, which is still one gate.
76
+ A **`parked`** node is the single exemption: it is the one node nobody will close, and
77
+ *n/a — parked* in that field is confidence without correctness. `park` never removes what
78
+ the node said it would run.
79
+
71
80
  **`close` stamps the commit; the verifier never supplies it.** A verdict written after the
72
81
  tree moved is evidence about a different tree, and an agent cannot name the wrong commit if
73
82
  it is never the one naming one.
@@ -114,6 +123,7 @@ does not have.
114
123
 
115
124
  | The excuse | Why it is wrong |
116
125
  |---|---|
126
+ | *"The verifier will work out what to run."* | Then the node's completion test is decided by whoever happens to close it, and two closes of one node disagree. The field is where the planner says it, and `validate` refuses a node that does not |
117
127
  | *"I'll just read the graph, it's only forty nodes."* | Forty is the number today. The property being protected is that four hundred costs the same, and it stops being true the first time the model reads the file |
118
128
  | *"The frontier is short; one extra line won't matter."* | It is paid on every iteration of every loop. That is what *the frontier and nothing else* means |
119
129
  | *"`blocked_by` already says the dependency, the edge is bookkeeping."* | The edge carries the **payload**. A dependency handing over nothing named is the fake edge with a field around it |
@@ -19,7 +19,10 @@ channel ships it. A dependency here would make the graph Claude-Code-shaped.
19
19
  states everything JSON Schema can — the required fields, `owner` non-empty, an edge's
20
20
  payload, and `done` implying non-empty evidence. What a schema cannot reach is
21
21
  cross-document and cross-node: whether an `owner` names a role that EXISTS, whether
22
- `serves` resolves, and whether the edges cycle. Those three are here. The split is
22
+ `serves` resolves, and whether the edges cycle. Those three are here. So is every rule
23
+ the format states, because the schema is never applied to a LIVE graph — including
24
+ B-080's `check`, without which `agents/verifier.md` instructs an agent to read a field
25
+ that does not exist. The split is
23
26
  where the format actually puts it, which is not where the first draft drew it.
24
27
 
25
28
  Exit codes are the contract (standing instruction R-004 — the next command is
@@ -238,6 +241,25 @@ def violations(graph):
238
241
  "nobody asked for is work the brief cannot account for, and the "
239
242
  "coverage relation cannot reach it")
240
243
 
244
+ # B-080 — the node says HOW it will be closed, and shipped doctrine reads it.
245
+ # `agents/verifier.md` told the verifier to run *the check the task named* while
246
+ # the schema had no field to name one, so the instruction pointed at an absence
247
+ # and the verifier's only options were the two that paragraph forbids: invent a
248
+ # check, or run everything. A `parked` node is the one exemption — it is the one
249
+ # node nobody will close, and a placeholder there would be worse than the gap.
250
+ if n.get("status") != "parked":
251
+ chk = n.get("check")
252
+ if not isinstance(chk, str) or not chk.strip():
253
+ out.append(f"{nid}: `check` is {chk!r} — B-080: a node that cannot say how "
254
+ "it will be closed leaves the verifier inventing a check or "
255
+ "running everything, and `agents/verifier.md` forbids both. Name "
256
+ "the command, or the judge where no command can decide it. Only a "
257
+ "`parked` node is exempt")
258
+ elif any(c in chk for c in "\n\r"):
259
+ out.append(f"{nid}: `check` contains a line break. A completion test that is "
260
+ "two commands cannot say which one closed the node, and the "
261
+ "verifier reports its output as one evidence row")
262
+
241
263
  if n.get("status") == "done":
242
264
  ev = n.get("evidence")
243
265
  if not isinstance(ev, list) or not [e for e in ev
@@ -480,6 +502,16 @@ def verdict_violations(v):
480
502
  for nid in rp.get("add") or []:
481
503
  if not isinstance(nid, dict) or "title" not in nid:
482
504
  out.append("replan.add entries must be nodes with at least a `title`")
505
+ continue
506
+ # B-080 again, at the one place a node is created by a verdict rather than by
507
+ # a person. Checked HERE so the refusal names the key before anything is
508
+ # written: leaving it to `violations()` after the close would abort the whole
509
+ # close over a field the verdict could have been asked for.
510
+ _c = nid.get("check")
511
+ if not isinstance(_c, str) or not _c.strip():
512
+ out.append(f"replan.add entry {nid.get('title')!r} names no `check` — the "
513
+ "node it creates has to say how IT will be closed, or the next "
514
+ "verifier faces the absence this one was told to read")
483
515
  return out
484
516
 
485
517
 
@@ -679,7 +711,14 @@ def cmd_add(graph, args):
679
711
  die("owner %r is not a role this pipeline ships%s — nothing was written"
680
712
  % (args.owner, (" — did you mean %s?" % near[0]) if near else ""))
681
713
 
682
- for name, val in (("--title", title), ("--serves", serves)):
714
+ check = (args.check or "").strip()
715
+ if not check:
716
+ die("`add` needs a --check with something in it: the command that closes this node, "
717
+ "or the judge where no command can decide it. A node that cannot say how it "
718
+ "will be closed leaves the verifier inventing a check or running everything — "
719
+ "B-080, and `agents/verifier.md` forbids both. Nothing was written")
720
+
721
+ for name, val in (("--title", title), ("--serves", serves), ("--check", check)):
683
722
  if any(c in val for c in "\n\r"):
684
723
  die("%s contains a line break. `next` prints one row per node and the loop "
685
724
  "reads those rows, so a break here forges a row for a node that does not "
@@ -729,7 +768,7 @@ def cmd_add(graph, args):
729
768
  "allocated from the highest in use" % nid)
730
769
 
731
770
  new = {"id": nid, "title": title, "owner": args.owner, "status": "pending",
732
- "blocked_by": blocked, "serves": serves, "evidence": None}
771
+ "blocked_by": blocked, "serves": serves, "check": check, "evidence": None}
733
772
  touches = list(dict.fromkeys(t.strip() for t in (args.touches or []) if t.strip()))
734
773
  if touches:
735
774
  new["touches"] = touches
@@ -993,14 +1032,17 @@ def cmd_close(graph, args):
993
1032
  title = str(spec.get("title", "")).strip()
994
1033
  owner = str(spec.get("owner", "implementer")).strip()
995
1034
  serves = str(spec.get("serves", node.get("serves"))).strip()
1035
+ check = str(spec.get("check", "")).strip()
996
1036
  if not title:
997
1037
  die("replan.add carries an entry with no title — nothing was written")
1038
+ if not check:
1039
+ die("replan.add carries an entry with no check — nothing was written")
998
1040
  if owner not in ROLES:
999
1041
  die("replan.add names owner %r, which is not a role this pipeline ships" % owner)
1000
1042
  new_id = next_id({n.get("id") for n in graph["nodes"]})
1001
1043
  graph["nodes"].append({"id": new_id, "title": title, "owner": owner,
1002
1044
  "status": "pending", "blocked_by": [], "serves": serves,
1003
- "evidence": None})
1045
+ "check": check, "evidence": None})
1004
1046
  revise(graph, "add", new_id, why or ("re-planned by the verdict on " + nid))
1005
1047
  added.append(new_id)
1006
1048
  for pid in rp.get("park") or []:
@@ -1074,6 +1116,10 @@ def main(argv=None):
1074
1116
  p_add.add_argument("--title", required=True)
1075
1117
  p_add.add_argument("--owner", required=True)
1076
1118
  p_add.add_argument("--serves", required=True)
1119
+ p_add.add_argument("--check", required=True,
1120
+ help="the command that closes this node, or the judge where no "
1121
+ "command can decide it — the verifier runs it and reports its "
1122
+ "output as the evidence row")
1077
1123
  p_add.add_argument("--blocked-by", dest="blocked_by", action="append", default=[])
1078
1124
  p_add.add_argument("--carries", action="append", default=[],
1079
1125
  help="what each --blocked-by hands over; pairs in the order written")