task-pipeline-skill 1.72.0 → 1.74.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.
Files changed (25) hide show
  1. package/CHANGELOG.md +176 -0
  2. package/CONTRIBUTING.md +61 -0
  3. package/README.md +1 -0
  4. package/SKILL-CARD.md +1 -1
  5. package/package.json +4 -3
  6. package/plugins/task-pipeline/.claude-plugin/plugin.json +1 -1
  7. package/plugins/task-pipeline/agents/verifier-product.md +96 -0
  8. package/plugins/task-pipeline/agents/verifier-seam.md +98 -0
  9. package/plugins/task-pipeline/agents/verifier-unit.md +86 -0
  10. package/plugins/task-pipeline/agents/verifier.md +9 -0
  11. package/plugins/task-pipeline/skills/task-pipeline/SKILL.md +1 -0
  12. package/plugins/task-pipeline/skills/task-pipeline/graph.schema.json +167 -72
  13. package/plugins/task-pipeline/skills/task-pipeline/references/acceptance.md +23 -0
  14. package/plugins/task-pipeline/skills/task-pipeline/references/audit.md +15 -6
  15. package/plugins/task-pipeline/skills/task-pipeline/references/certification.md +146 -0
  16. package/plugins/task-pipeline/skills/task-pipeline/references/gates.md +9 -0
  17. package/plugins/task-pipeline/skills/task-pipeline/references/portability.md +1 -0
  18. package/plugins/task-pipeline/skills/task-pipeline/references/progress.md +9 -3
  19. package/plugins/task-pipeline/skills/task-pipeline/references/retrospective.md +1 -1
  20. package/plugins/task-pipeline/skills/task-pipeline/references/stages.md +6 -0
  21. package/plugins/task-pipeline/skills/task-pipeline/references/work-graph.md +3 -0
  22. package/plugins/task-pipeline/skills/task-pipeline/scripts/graph.py +326 -6
  23. package/plugins/task-pipeline/skills/task-pipeline/templates/retro.md +1 -1
  24. package/plugins/task-pipeline/skills/task-pipeline/templates/run.md +22 -8
  25. package/plugins/task-pipeline/skills/task-pipeline/templates/verification.md +38 -5
@@ -117,7 +117,7 @@
117
117
  "not": {
118
118
  "pattern": "[\\n\\r]"
119
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."
120
+ "description": "**How this node will be closed** 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 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 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 *n/a, parked* 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
121
  },
122
122
  "evidence": {
123
123
  "type": [
@@ -145,87 +145,100 @@
145
145
  "pattern": "\\S"
146
146
  },
147
147
  "description": "What this node MUTATES — paths, register names, remote resource ids. `references/planning.md` states the rule the frontier needs: *distinct is not the same as independent, and the check is what they touch, never what they are called.* That rule lived in the markdown plan, and the graph replaced the plan as the thing deciding what runs next — so `next` could hand two agents two runnable nodes that write the same file, with nothing able to report it.\\n\\nOptional, and its absence is DISCLOSED rather than treated as «touches nothing»: `next` prints how many frontier nodes declared no targets, because a quiet run and a checked one must not look alike."
148
- }
149
- },
150
- "allOf": [
151
- {
152
- "if": {
153
- "properties": {
154
- "status": {
155
- "not": {
156
- "const": "parked"
148
+ },
149
+ "certification": {
150
+ "type": "object",
151
+ "description": "What the three-tier certification recorded for this node. Written by `graph.py certify` on every round, pass or fail: a failing round that wrote nothing would erase the only evidence that a node is churning, which is the number the loop-guard ceiling reads.",
152
+ "additionalProperties": false,
153
+ "required": [
154
+ "round",
155
+ "tiers",
156
+ "at",
157
+ "history"
158
+ ],
159
+ "properties": {
160
+ "round": {
161
+ "type": "integer",
162
+ "minimum": 1
163
+ },
164
+ "tiers": {
165
+ "type": "object",
166
+ "additionalProperties": false,
167
+ "required": [
168
+ "unit",
169
+ "seam",
170
+ "product"
171
+ ],
172
+ "properties": {
173
+ "unit": {
174
+ "enum": [
175
+ "pass",
176
+ "fail"
177
+ ]
178
+ },
179
+ "seam": {
180
+ "enum": [
181
+ "pass",
182
+ "fail"
183
+ ]
184
+ },
185
+ "product": {
186
+ "enum": [
187
+ "pass",
188
+ "fail"
189
+ ]
157
190
  }
158
191
  }
159
192
  },
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
- },
177
- {
178
- "if": {
179
- "properties": {
180
- "status": {
181
- "const": "done"
182
- }
193
+ "at": {
194
+ "type": "string",
195
+ "minLength": 1
183
196
  },
184
- "required": [
185
- "status"
186
- ]
187
- },
188
- "then": {
189
- "required": [
190
- "evidence"
191
- ],
192
- "properties": {
193
- "evidence": {
194
- "type": "array",
195
- "minItems": 1,
196
- "items": {
197
- "type": "string",
198
- "minLength": 1,
199
- "pattern": "\\S",
200
- "description": "At least one non-whitespace character. `minLength: 1` alone accepts a single space, which is the one shape where this schema and `graph.py`'s verdict gate disagreed — the gate strips, the schema counted. A cross-check fixture comparing the two found it."
197
+ "history": {
198
+ "type": "array",
199
+ "minItems": 1,
200
+ "items": {
201
+ "type": "object",
202
+ "additionalProperties": false,
203
+ "required": [
204
+ "unit",
205
+ "seam",
206
+ "product"
207
+ ],
208
+ "properties": {
209
+ "unit": {
210
+ "enum": [
211
+ "pass",
212
+ "fail"
213
+ ]
214
+ },
215
+ "seam": {
216
+ "enum": [
217
+ "pass",
218
+ "fail"
219
+ ]
220
+ },
221
+ "product": {
222
+ "enum": [
223
+ "pass",
224
+ "fail"
225
+ ]
226
+ }
201
227
  }
202
228
  }
203
229
  }
204
230
  }
231
+ }
232
+ },
233
+ "allOf": [
234
+ {
235
+ "$ref": "#/definitions/rule_check_unless_parked"
205
236
  },
206
237
  {
207
- "if": {
208
- "properties": {
209
- "status": {
210
- "const": "parked"
211
- }
212
- },
213
- "required": [
214
- "status"
215
- ]
216
- },
217
- "then": {
218
- "required": [
219
- "parked_reason"
220
- ],
221
- "properties": {
222
- "parked_reason": {
223
- "type": "string",
224
- "minLength": 1,
225
- "pattern": "\\S"
226
- }
227
- }
228
- }
238
+ "$ref": "#/definitions/rule_done_needs_evidence"
239
+ },
240
+ {
241
+ "$ref": "#/definitions/rule_parked_needs_reason"
229
242
  }
230
243
  ]
231
244
  },
@@ -283,6 +296,88 @@
283
296
  "description": "The reason, written for a person reading it later. The non-whitespace pattern is required for the same reason `parked_reason` needs one: `minLength: 1` counts a space."
284
297
  }
285
298
  }
299
+ },
300
+ "rule_check_unless_parked": {
301
+ "description": "B-080's rule, factored out of `node.allOf` so it is stated once and referenced. Factoring it exposed B-079: `test/validate.py`'s `_conditionals()` recursed into `allOf` and never followed a `$ref`, so a schema that shares a rule this way read as a schema with NO conditionals and both REQ-006 and REQ-012 were reported missing from a schema that states them. The shipped schema now uses the `$ref` form, so the deref branch is exercised by every run of the gate rather than by one fixture.",
302
+ "if": {
303
+ "properties": {
304
+ "status": {
305
+ "not": {
306
+ "const": "parked"
307
+ }
308
+ }
309
+ },
310
+ "required": [
311
+ "status"
312
+ ]
313
+ },
314
+ "then": {
315
+ "required": [
316
+ "check"
317
+ ],
318
+ "properties": {
319
+ "check": {
320
+ "type": "string",
321
+ "minLength": 1,
322
+ "pattern": "\\S"
323
+ }
324
+ }
325
+ }
326
+ },
327
+ "rule_done_needs_evidence": {
328
+ "description": "REQ-006 as draft-07 states it: `done` implies at least one non-whitespace evidence string. Referenced from `node.allOf`, never inlined, so the rule has one home.",
329
+ "if": {
330
+ "properties": {
331
+ "status": {
332
+ "const": "done"
333
+ }
334
+ },
335
+ "required": [
336
+ "status"
337
+ ]
338
+ },
339
+ "then": {
340
+ "required": [
341
+ "evidence"
342
+ ],
343
+ "properties": {
344
+ "evidence": {
345
+ "type": "array",
346
+ "minItems": 1,
347
+ "items": {
348
+ "type": "string",
349
+ "minLength": 1,
350
+ "pattern": "\\S",
351
+ "description": "At least one non-whitespace character. `minLength: 1` alone accepts a single space, which is the one shape where this schema and `graph.py`'s verdict gate disagreed — the gate strips, the schema counted. A cross-check fixture comparing the two found it."
352
+ }
353
+ }
354
+ }
355
+ }
356
+ },
357
+ "rule_parked_needs_reason": {
358
+ "description": "REQ-012: a parked node names why it is parked. Referenced from `node.allOf`.",
359
+ "if": {
360
+ "properties": {
361
+ "status": {
362
+ "const": "parked"
363
+ }
364
+ },
365
+ "required": [
366
+ "status"
367
+ ]
368
+ },
369
+ "then": {
370
+ "required": [
371
+ "parked_reason"
372
+ ],
373
+ "properties": {
374
+ "parked_reason": {
375
+ "type": "string",
376
+ "minLength": 1,
377
+ "pattern": "\\S"
378
+ }
379
+ }
380
+ }
286
381
  }
287
382
  }
288
383
  }
@@ -319,6 +319,29 @@ spends its length refusing.
319
319
 
320
320
  ## GATE (manual)
321
321
 
322
+ > **Acceptance policy `AP-1` · owner: the operator of the project running this pipeline ·
323
+ > in force since 2026-08-20 · supersedes: the unversioned ladder that stood before it.**
324
+ >
325
+ > This block is the policy, and naming it that closes a contradiction that stood in the
326
+ > shipped doctrine. [`gates.md`](gates.md) → *Axis A — the stage gate type* says which stages are
327
+ > manual is the operator's decision and that **the framework fixes no stage count and no
328
+ > gate assignment** — while the ladder below fixes twelve criteria and the statuses they
329
+ > may take. Both were true and neither was scoped, so a reader could take either as the
330
+ > rule.
331
+ >
332
+ > **The scope, stated once:** `gates.md`'s sentence governs the **pipeline's shape** — how
333
+ > many stages a project runs and which of them are `auto`, `judgment` or `manual`, all of
334
+ > it in the project's own `pipeline.json`. `AP-1` governs **what stage 10's manual gate
335
+ > asks when a project runs one.** A project may drop stage 10, or make it `auto` for a
336
+ > class of work, and `AP-1` then does not apply to it; a project that keeps it manual gets
337
+ > these criteria and this vocabulary, not a per-run selection from them.
338
+ >
339
+ > **Changing it is a decision, not an edit.** An amended criterion moves the version to
340
+ > `AP-2` and lands with a row in the project's decision register, because a policy that can
341
+ > be edited silently is the *«recorded in the slot reserved for what a machine
342
+ > established»* failure one level up: an acceptance standard nobody can cite by version is
343
+ > one every run re-negotiates.
344
+
322
345
  All of:
323
346
 
324
347
  1. **Every shipped REQ has a verification row, and every row names a REQ its own run
@@ -47,12 +47,21 @@ The method most audits use is **horizontal**: compare the documents against each
47
47
  other, then do it again. It works, and then it fails in a way that is invisible
48
48
  from inside it. Measured over seven passes on a production repository:
49
49
 
50
- | Pass | Findings | …of which the previous pass's own fixes caused |
51
- |---|---|---|
52
- | 4 | 12 | 5 |
53
- | 5 | 17 | 9 |
54
- | 6 | 13 | 10 |
55
- | 7 | 19 | 4 |
50
+ | Pass | Findings | …of which the previous pass's own fixes caused | Self-inflicted share |
51
+ |---|---|---|---|
52
+ | 4 | 12 | 5 | 42% |
53
+ | 5 | 17 | 9 | 53% |
54
+ | 6 | 13 | 10 | 77% |
55
+ | 7 | 19 | 4 | 21% — see the note below |
56
+
57
+ **The trend above is measured over passes four to six, and pass seven is not part of
58
+ it.** 42% → 53% → 77% is the decay this section teaches; the seventh pass fell back to
59
+ 4 of 19 and **the record does not say what that pass did differently.** The row stays,
60
+ with the gap named, for two reasons. Deleting a measured row to protect a claim is the
61
+ opposite of what this file asks of every gate it describes — and the vertical pass
62
+ described below cannot be credited for the drop, because it is a separate pass over the
63
+ same repository, not the seventh. Whatever pass seven did, it is unrecorded, and an
64
+ unrecorded cause is what this table has to say about it.
56
65
 
57
66
  By pass six the audit was **mostly repairing itself**. Not fatigue — arithmetic.
58
67
  Each pass edits the corpus the next pass reads, so the newest edits are always the
@@ -0,0 +1,146 @@
1
+ # Certification — three readings at three distances, and all three must pass
2
+
3
+ A node is not closed by one agent's opinion. It is closed by **three independent
4
+ readings at escalating visibility**, and the run may not advance until all three
5
+ pass.
6
+
7
+ ## Contents
8
+
9
+ - Why one verifier is not enough, stated as the failure it produces
10
+ - The three tiers
11
+ - Blind, and it is the whole design
12
+ - A pass has to mean something, so two rules have teeth
13
+ - The report, and where each field lands
14
+ - The commands
15
+ - The fix cycle, and its ceiling
16
+ - What this costs, said out loud
17
+ - Rationalizations
18
+
19
+ ## Why one verifier is not enough, stated as the failure it produces
20
+
21
+ A verifier reads the diff it was handed. That is not a shortcoming of the agent; it
22
+ is the definition of its context. And it means a whole class of defect is invisible
23
+ to it by construction:
24
+
25
+ - the change is correct where it was made, and a **caller's contract moved** under it
26
+ - a **second implementation of the same rule** did not get the fix
27
+ - a **documented behaviour** is now false, and the document still reads as true
28
+ - **another feature** reaches the same path and nobody considered the interaction
29
+
30
+ None of these is a bug in the changed lines. All of them ship. Each is found by
31
+ looking one level further out than the change — which is a different reading, not a
32
+ longer one, because the context that finds it is the context that excludes the diff.
33
+
34
+ ## The three tiers
35
+
36
+ | Tier | Subject | Characteristic finding |
37
+ |---|---|---|
38
+ | `unit` | the changed functions, classes and branches, plus the node's own `check` | a branch nothing exercises; a boundary that moved |
39
+ | `seam` | everything that can reach the change — callers, callees, implementors, shared state, the neighbours' tests | a contract that moved under a dependent; the duplicate that did not get the fix |
40
+ | `product` | documentation, scenarios, user-visible strings, and the neighbouring features that share this path | a documented behaviour that is now false; an interaction nobody listed |
41
+
42
+ Agents: [`../../../agents/verifier-unit.md`](../../../agents/verifier-unit.md),
43
+ [`verifier-seam.md`](../../../agents/verifier-seam.md),
44
+ [`verifier-product.md`](../../../agents/verifier-product.md).
45
+
46
+ ## Blind, and it is the whole design
47
+
48
+ **The three run in parallel and no tier reads another's report.** Three readings
49
+ that inform each other are one opinion with three signatures — and the failure mode
50
+ is specific: an agent that has just read a convincing account of the implementation
51
+ will paraphrase it back as product truth. The disagreement between blind readings is
52
+ the instrument, so `graph.py certify` refuses a report whose prose cites another
53
+ tier's verdict.
54
+
55
+ Dispatch all three in one message so they run concurrently. Give each the node id,
56
+ its `serves`, and the diff — nothing else, and never another tier's output.
57
+
58
+ ## A pass has to mean something, so two rules have teeth
59
+
60
+ **A tier cannot pass on an empty `scope`.** `scope` is what the tier actually
61
+ opened, with `file:line`. A report that names nothing it read is a rubber stamp, and
62
+ three rubber stamps cost three times one verifier while reading as three times the
63
+ assurance — strictly worse than the single verdict it replaced. `certify` refuses it
64
+ by name.
65
+
66
+ **A tier cannot pass while carrying a `breaks` finding.** There are two severities
67
+ and no third, because a certification that admits a maybe admits everything:
68
+
69
+ - **`breaks`** — the node is not done. Carries a `check`: the command or judgement
70
+ that will prove the fix, for the same reason `replan.add` does. The finding
71
+ becomes a node the next round has to close, and handing that node the absence is
72
+ how the defect returns one round later.
73
+ - **`risk`** — found, judged survivable, and named. It ships, and it reaches the
74
+ closing verdict as a blocker with `can_continue_around: true`, which is exactly
75
+ what a named survivable finding is.
76
+
77
+ ## The report, and where each field lands
78
+
79
+ Eight keys, all required, `[]` a valid answer and silence not one. On a pass
80
+ `certify` assembles the canonical seven-key verdict that
81
+ [`work-graph.md`](work-graph.md) already specifies, and no field is used for
82
+ something it does not mean:
83
+
84
+ | Tier field | Becomes | Because |
85
+ |---|---|---|
86
+ | `confirms` | `done` | asked for, and now true |
87
+ | `not_examined` | `not_verified` | present, and no check touched it |
88
+ | `findings` at `risk` | `blockers`, `can_continue_around: true` | found, judged survivable, named |
89
+ | `evidence` | `evidence`, prefixed with the tier | the command and what it printed |
90
+
91
+ `certify` runs the assembled verdict through the same `verdict_violations` gate
92
+ `close` will apply, so a certification cannot hand the run a verdict its own
93
+ consumer refuses.
94
+
95
+ ## The commands
96
+
97
+ ```bash
98
+ # three reports in, one verdict out — exits 1 if any tier failed
99
+ graph.py certify --node N-007 \
100
+ --tier unit.json --tier seam.json --tier product.json
101
+
102
+ # unchanged, and still the only thing that moves the graph
103
+ graph.py close --verdict .task-pipeline/verdict-N-007.json
104
+ ```
105
+
106
+ ## The fix cycle, and its ceiling
107
+
108
+ A failing round **records itself and leaves the node open.** `certification` on the
109
+ node carries the round number, this round's three verdicts and the history of every
110
+ round — written on failure too, because a failing round that wrote nothing would
111
+ erase the only evidence that a node is churning.
112
+
113
+ The cycle is: `certify` → fail → the `breaks` findings become nodes (each already
114
+ carrying its `check`) → fix → `certify` again, round `N+1`. Same three tiers, same
115
+ blind dispatch. A tier that passed in an earlier round is **re-run**, because the
116
+ fix is a new change and the level it passed on is not the level it now faces.
117
+
118
+ **At the ceiling the gate measures rather than stops** —
119
+ [`loop-guard.md`](loop-guard.md). `--ceiling` defaults to 3. At or over it, `certify`
120
+ still runs and still reports the tiers; what it adds is the name of the tier that
121
+ has failed **every** round. A run spinning on one level needs the operator to see
122
+ *which* level:
123
+
124
+ - the same tier every round → the level is being misread, or the node is the wrong
125
+ shape. Not one fix away. Re-plan the node, do not attempt round four.
126
+ - different tiers each round → churn across levels. Usually one requirement that
127
+ was never decided, surfacing at whichever distance looks at it.
128
+
129
+ ## What this costs, said out loud
130
+
131
+ Three agents per node instead of one. That is the price of the visibility, and it is
132
+ paid per node rather than per run. The three are dispatched in parallel, so the
133
+ wall-clock cost is roughly one reading; the token cost is three. A node whose
134
+ `check` is mechanical and whose blast radius is genuinely nil still pays it — and a
135
+ tier with nothing to find says so in `scope` and `not_examined` rather than being
136
+ skipped, because **a skipped level and a clean level are indistinguishable
137
+ afterwards**, and only one of them is evidence.
138
+
139
+ ## Rationalizations
140
+
141
+ | Temptation | Why it is wrong |
142
+ |---|---|
143
+ | «All three would say the same thing» | Then all three say it, at a cost you already know, and the run has three signatures instead of one guess about what the other two would have found |
144
+ | «The seam tier can read the unit report first — it saves tokens» | It saves tokens by removing the second opinion. The reports are cheap; the independence is the product |
145
+ | «Tier 3 passed last round, skip it» | The fix is a new change. A tier's pass is about the tree it read, and that tree moved |
146
+ | «Round 4 will get it» | Read the ceiling's output. The same tier failing three times is a planning defect wearing a verification failure's clothes |
@@ -58,6 +58,15 @@ not the operator confirming it is what they asked for, and no amount of checking
58
58
  makes it one. Which stages are manual is the **operator's** decision, recorded in
59
59
  their `pipeline.json`; the framework fixes no stage count and no gate assignment.
60
60
 
61
+ **That sentence is about the pipeline's SHAPE, and nothing else.** It says a project
62
+ chooses how many stages it runs and which of them wait for a person. It does not say the
63
+ criteria inside a gate are per-run negotiable: where a project keeps stage 10 manual, what
64
+ that gate asks is [`acceptance.md`](acceptance.md)'s policy **`AP-1`**, which is versioned
65
+ and has an owner. The two rules stood side by side unscoped until 2026-08-20 (`B-091`), and
66
+ a reader could take either as the whole rule — *the framework fixes nothing* and *the ladder
67
+ is fixed* are both in the shipped doctrine, which is how an acceptance standard becomes
68
+ something every run re-argues.
69
+
61
70
  ## The judgment gate — a ruling is not a measurement
62
71
 
63
72
  Two types were not enough, and the gap was not cosmetic. A reviewer's ruling, a check that
@@ -71,6 +71,7 @@ a row pointing outside the bundle is the defect this file exists to catch.
71
71
  | What a spec must lock, the UX-track order, the module dossier | `references/spec.md` |
72
72
  | The zero-context plan format, parallel groups, set equality | `references/planning.md` |
73
73
  | The work graph: its fields, the verbs and their exit codes, and the three invariants a schema cannot state | `references/work-graph.md`, `scripts/graph.py`, `graph.schema.json` |
74
+ | How a node is CLOSED: three blind readings at escalating visibility, all three required, and the round ledger the ceiling reads | `references/certification.md`, `agents/verifier-{unit,seam,product}.md`, `scripts/graph.py certify` |
74
75
  | Workspace isolation, the subagent loop, who may write the register | `references/build.md` |
75
76
  | The review rubric, diff packages, the three verdicts | `references/review.md` |
76
77
  | **False success** — the class, its known shapes and its two rules | `references/gates.md` |
@@ -369,12 +369,18 @@ deduplicated, and always exiting 0 — a hook that can fail a `Read` breaks ever
369
369
  every session. `scripts/graph.py doctrine` reports how many of the bundle's reference files
370
370
  a run opened and lists the rest.
371
371
 
372
- Why it is hook-written is the same reason as above: a claim about what somebody read,
373
- written by the party the claim is about, is not evidence. And why the verb prints
372
+ Why a hook appends it is the same reason as above: a claim about what somebody read,
373
+ written by the party the claim is about, is not evidence. **And that is an intent rather
374
+ than a proof, so the verb reports `unattested`:** the ledger is the file the agent appends
375
+ to at every stage and carries no writer field, so nothing in it separates a hook-written
376
+ line from one an agent typed. Saying *never agent-written* was a provenance claim the
377
+ format cannot support — B-014's class, in the mechanism built to close it.
378
+
379
+ And why the verb prints
374
380
  `unmeasured` rather than `0` when there are no such lines is the same reason again — the
375
381
  hook being absent and the run reading nothing are **opposite facts** the ledger cannot
376
382
  separate, so it claims neither. A `0` there would be the reassuring answer to a question
377
- nobody asked, over 34 files nobody checked.
383
+ nobody asked, over 35 files nobody checked.
378
384
 
379
385
  It is a disclosure: no floor, no direction, never a target. The moment the number becomes
380
386
  something to raise, a run will open files to raise it.
@@ -19,7 +19,7 @@ justifies reading it protects one section while the file below it doubles.
19
19
  | Artifact | Parts | How it is read |
20
20
  |---|---|---|
21
21
  | `docs/evidence/retro.md` — **one per project** | **Standing instructions** (max **10**) · **Run stamps** (max **10**, oldest rotate out) | stage 0, **in full** — both are bounded by a **cap**, which *one line each* never was |
22
- | the same file's **Recent log** | entries from the last five run stamps narrative, and capped by nothing | stage 0, **queried** by the task's nouns. It said *in full* until 2026-08-10, when it measured **74%** of the file: an uncapped section inside a binding source is what makes the capped part get skimmed |
22
+ | the same file's **Recent log** | narrative entries, **uncapped by design** — the heading said *entries from the last five run stamps* until 2026-08-20 while the section held 25 going back nine days, a bound in a heading that nothing enforced | stage 0, **queried** by the task's nouns. It said *in full* until 2026-08-10, when it measured **74%** of the file: an uncapped section inside a binding source is what makes the capped part get skimmed |
23
23
  | `docs/evidence/retro/YYYY-QN.md` — the archive | every entry and every retirement ever written, append-only | **queried** by the task's nouns; never read end to end |
24
24
 
25
25
  Seed the archive from [`../templates/retro-archive.md`](../templates/retro-archive.md).
@@ -273,6 +273,12 @@ never that the work was skipped quietly.
273
273
  `next` refuses to walk one. The reason to prefer it over a prose plan is measured, not
274
274
  aesthetic: a 400-node graph and a 4-node graph produce the same 27-byte frontier, so the
275
275
  cost of knowing what is next does not grow with the programme.
276
+ - **And the check is what THREE readings will run, not one**
277
+ ([`certification.md`](certification.md)). A node is closed by `unit`, `seam` and `product`
278
+ reports, dispatched blind and in parallel, and `certify` refuses the close until all three
279
+ pass. That is a planning fact, not only a verification one: a node whose blast radius nobody
280
+ can name is a node the seam and product tiers cannot scope, so `touches` and `serves` are
281
+ what make the two outer readings possible at all.
276
282
  - **The queue exists here, so the loop arms here** ([`continuity.md`](continuity.md) →
277
283
  *Part 1a*). Where `run.loop.arm` is `after-decomposition` and the map holds more than
278
284
  one module, arm the mode at the close of this stage and print one line: the mode, and
@@ -59,6 +59,7 @@ conditional on the code, never merely sequenced after it.**
59
59
  | `goal` | the release goal | `0` · `3` unstated |
60
60
  | `add` | the id it allocated | `0` · `1` refused |
61
61
  | `park` | the id and the reason | `0` · `1` refused |
62
+ | `certify` | the round, and on a failure every `breaks` finding with its fix and its check | `0` all three tiers passed · `1` a tier failed, or a report is malformed |
62
63
  | `close` | the goal, the new frontier count, and what was not verified | `0` · `1` refused **or the verdict stops the run** |
63
64
  | `producer` | what produced this proof — actor, model, runtime, skill, config, commit, trace | `0` |
64
65
  | `doctrine` | how many of the bundle's reference files this run opened | `0` |
@@ -77,6 +78,8 @@ A **`parked`** node is the single exemption: it is the one node nobody will clos
77
78
  *n/a — parked* in that field is confidence without correctness. `park` never removes what
78
79
  the node said it would run.
79
80
 
81
+ **A node is closed by three readings, not one.** `certify` takes one tier report from each of `unit`, `seam` and `product` — dispatched blind and in parallel — requires all three to pass, and assembles the seven-key verdict `close` consumes. `close`'s contract is unchanged; what changed is that the verdict is now built from three readings at different distances instead of written from one, because a change can be correct where it was made and wrong one level out. A failing round records itself and leaves the node open. Doctrine: [`certification.md`](certification.md).
82
+
80
83
  **`close` stamps the commit; the verifier never supplies it.** A verdict written after the
81
84
  tree moved is evidence about a different tree, and an agent cannot name the wrong commit if
82
85
  it is never the one naming one.