task-pipeline-skill 1.67.0 → 1.69.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 (30) hide show
  1. package/CHANGELOG.md +856 -0
  2. package/README.md +25 -0
  3. package/SKILL-CARD.md +1 -1
  4. package/bin/task-pipeline.js +30 -0
  5. package/package.json +4 -3
  6. package/plugins/task-pipeline/.claude-plugin/plugin.json +1 -1
  7. package/plugins/task-pipeline/agents/verifier.md +88 -0
  8. package/plugins/task-pipeline/commands/task-pipeline.md +22 -0
  9. package/plugins/task-pipeline/skills/task-pipeline/SKILL.md +84 -136
  10. package/plugins/task-pipeline/skills/task-pipeline/graph.example.json +73 -0
  11. package/plugins/task-pipeline/skills/task-pipeline/graph.schema.json +253 -0
  12. package/plugins/task-pipeline/skills/task-pipeline/pipeline.schema.json +46 -3
  13. package/plugins/task-pipeline/skills/task-pipeline/references/acceptance.md +69 -0
  14. package/plugins/task-pipeline/skills/task-pipeline/references/audit.md +1 -1
  15. package/plugins/task-pipeline/skills/task-pipeline/references/continuity.md +9 -1
  16. package/plugins/task-pipeline/skills/task-pipeline/references/documentation.md +17 -0
  17. package/plugins/task-pipeline/skills/task-pipeline/references/gates.md +91 -1
  18. package/plugins/task-pipeline/skills/task-pipeline/references/portability.md +1 -0
  19. package/plugins/task-pipeline/skills/task-pipeline/references/progress.md +41 -0
  20. package/plugins/task-pipeline/skills/task-pipeline/references/stages.md +11 -1
  21. package/plugins/task-pipeline/skills/task-pipeline/references/verification.md +52 -0
  22. package/plugins/task-pipeline/skills/task-pipeline/references/work-graph.md +121 -0
  23. package/plugins/task-pipeline/skills/task-pipeline/scripts/graph.py +1113 -0
  24. package/plugins/task-pipeline/skills/task-pipeline/templates/README.md +1 -0
  25. package/plugins/task-pipeline/skills/task-pipeline/templates/carryover.md +1 -1
  26. package/plugins/task-pipeline/skills/task-pipeline/templates/convergence.sh +146 -0
  27. package/plugins/task-pipeline/skills/task-pipeline/templates/exposure.sh +104 -1
  28. package/plugins/task-pipeline/skills/task-pipeline/templates/hooks.example.json +13 -1
  29. package/plugins/task-pipeline/skills/task-pipeline/templates/run.md +32 -0
  30. package/plugins/task-pipeline/skills/task-pipeline/templates/verification.md +67 -5
package/CHANGELOG.md CHANGED
@@ -1,5 +1,856 @@
1
1
  # Changelog
2
2
 
3
+ ## v1.69.0 — the work graph, and a check that mentions is not a check that binds
4
+
5
+ **Module 1 of the role-agent programme, complete** — T-1 through T-7, briefed in
6
+ `docs/evidence/specs/2026-08-17-role-agent-graph-brief.md`. The graph is on disk, a script
7
+ walks it, a verifier closes one node at a time against a seven-key verdict, and the loop
8
+ reads a queue rather than its own recollection.
9
+
10
+ **Counted at the close, not carried from a section above:** 376 guards · 114 graph fixtures · 24 exposure fixtures · 9 verbs on `graph.py` · 35 reference files · `npm run test:all` exits 0 over eight suites. The figures in the sub-sections below are each true at the moment that sub-section landed, which is why this line exists.
11
+
12
+
13
+ **`graph.schema.json` and `graph.example.json` ship**, and `test/validate.py` reads
14
+ them. `.task-pipeline/graph.json` is the queue the loop walks — a run artifact, never
15
+ committed by the skill, so what ships is the schema and one example that exercises it.
16
+
17
+ **The first draft of the check asserted membership in `required` and nothing else,
18
+ and an independent reader defeated every requirement it claimed to enforce.** Standing
19
+ instruction `R-005` exists for exactly that — *your own reading of your own check is
20
+ the reading that missed it* — and this is the first time it has been run on a check
21
+ this repository added. Eight bypasses, all now refused and each watched refusing:
22
+
23
+ | Bypass | Why it worked |
24
+ |---|---|
25
+ | `nodes` declared an object map, `items` left as decoration | `items` constrains arrays only, so every element check was vacuous — REQ-001, 002 and 003 defeated at once |
26
+ | `owner` in `required`, `minLength` dropped | a node whose owner is `""` satisfies `required` and dispatches to nobody |
27
+ | `owner` typed `["string", "null"]` | the same, with `null` |
28
+ | `edges` requiring `payload` and neither endpoint | an edge is from, to, and what it carries |
29
+ | `items` given as a tuple | binds element 0, frees the rest — and crashed the check rather than failing it |
30
+ | a name in `required` that `properties` never declares | constrains nothing at all |
31
+ | a two-hop `$ref` | reported five fields missing that were not missing |
32
+ | an example of `{"nodes": [], "edges": []}` | validates against any schema and demonstrates none of it |
33
+
34
+ **And one claim in the schema's own prose was false.** It said `done` implying
35
+ evidence was beyond JSON Schema. Draft-07 `if`/`then` states it exactly, and now does
36
+ — so a node called done by assertion is refused **by the format**, before any script
37
+ runs. The line between "the schema's job" and "the script's job" moved to where the
38
+ format actually puts it: what remains for `graph.py` is cross-document — whether an
39
+ owner names a role that exists, whether `serves` resolves, whether the edges cycle.
40
+
41
+ **A NameError, found by the reader and not by the author.** The skip path appended to
42
+ `_skips`, which exists in a **sibling repository's** validator and not in this one. On
43
+ any machine without `jsonschema` the run died on a bare traceback and the ~250 checks
44
+ below it never ran; CI could not see it, because CI installs `jsonschema` first. The
45
+ accumulator here is `_UNLOOKED`, and the one-line fix that defines `_skips` would have
46
+ been worse — a silent skip, which `test/validate.py:395` forbids by name.
47
+
48
+ ### T-2 — the walk, and the promise it exists to keep
49
+
50
+ `scripts/graph.py` ships: `validate`, `next`, `goal`. Stdlib only, verified by
51
+ parsing its own imports — `references/portability.md` makes `scripts/` the one
52
+ Claude-Code capability that travels, and a dependency here would have made the
53
+ graph Claude-Code-shaped.
54
+
55
+ **The design's central claim is now a measurement.** A 400-node graph is 51 KB on
56
+ disk and produces a **27-byte** frontier; a 4-node graph produces the same 27
57
+ bytes. Context cost is **flat in graph size**, which is the property every other
58
+ part of this programme rests on — and it is why `next` prints the frontier and
59
+ nothing else. That line enters a context on every iteration of every loop.
60
+
61
+ It checks the three things a schema cannot reach, and only those: whether `owner`
62
+ names a role that **exists** (with the misspelt near-miss caught separately from
63
+ the absent one, per `R-008`'s enumerate-the-shapes rule), whether `blocked_by` and
64
+ the edges name nodes that exist, and whether the edges **cycle** — the one failure
65
+ of this design that looks exactly like slow progress.
66
+
67
+ Exit codes are the contract per `R-004`: `3` is *nothing left to do* and `4` is
68
+ *nothing runnable*, because a finished graph and a stalled one are different facts
69
+ and a caller that cannot tell them apart will wait on the wrong one.
70
+
71
+ `test/graph_test.py`, **14 cases**, joins `npm test`. This is also the first
72
+ `scripts/` in this repository, so `CLAUDE.md`'s sentence about the only executable
73
+ code being two installers and the validator was false the moment it landed, and is
74
+ corrected in the same change.
75
+
76
+ Guards: 351 → **376**. Twenty-three plants across the module, structurally distinct rather than variations,
77
+ each asserting it landed before the validator runs.
78
+
79
+ ### Stage 9 — the third artifact, and one false alarm I raised myself
80
+
81
+ The code graph was **33 commits behind** and its report described a *different* graph: 1787
82
+ nodes and 1847 edges in `GRAPH_REPORT.md` against 1535 nodes in the `graph.json` beside it,
83
+ with the report the older of the two. `graphify update .` re-extracts without an LLM call, so
84
+ the refresh cost nothing but time: **1866 nodes · 1983 edges · 231 communities**, stamped at
85
+ `26ac6dd`, and the report now agrees with the graph exactly.
86
+
87
+ **The hubs are seven doctrine sections and three test helpers, and no undocumented code
88
+ seam.** `project()`, `exposure()` and `row()` are hubs because twenty-four fixtures call them
89
+ — scaffolding, not architecture. That `graph.py`'s own functions are *not* hubs is the
90
+ informative part: nine verbs with little internal coupling is what the design intended.
91
+
92
+ **And I raised a false alarm on the way, which is worth recording because of how it read.**
93
+ The first measurement said *1535 nodes and zero edges* — a graph that answers no reach
94
+ question at all, which is exactly the failure `references/knowledge-graph.md` warns of, since
95
+ a wrong graph carries the authority of a machine. It was wrong: edges live under `links` in
96
+ this format, and there were 1585 of them. A check that reads the wrong field reports the most
97
+ alarming possible state with total confidence — the same shape as a check that reads the
98
+ wrong subject, one axis over, and the reason the second measurement was taken before anything
99
+ was filed.
100
+
101
+ **Disclosed rather than skipped in silence:** 231 communities now carry 156 saved labels, 154
102
+ of them renamed by their hub. Refreshing the names needs an LLM call and was not made, so the
103
+ community names in the report are hub-derived and not semantic.
104
+
105
+ ### Stage 6 — the full suite, and the thing it found was the suite itself
106
+
107
+ `npm run test:all` ran six suites and **`graph_test.py` was not one of them.** 114 fixtures —
108
+ the whole of module 1 — lived in `npm test` and outside the command named *all*. Every command
109
+ in `test:all` passed, so *the full suite is green* had been a true sentence about a smaller set
110
+ than it names. `exposure_test.py` was worse off: **24 fixtures in no script at all**, testing
111
+ the very file this release extended with the staleness section.
112
+
113
+ Both are in now, and a guard **discovers** the suites rather than listing them — every
114
+ `test/*_test.py` and `negatives.py` must be reachable from `test:all`, resolving one level of
115
+ `npm run`. A list there would drift exactly the way the thing it checks drifted.
116
+
117
+ **Its own first run was wrong, and said so.** Substituting script names in declaration order
118
+ made `npm run test` a prefix of `npm run test:probe`, so four suites were reported absent that
119
+ the chain reaches. Longest name first.
120
+
121
+ **Then the full suite found two rotted CI plants — both rotted by edits made in this release.**
122
+
123
+ - *coverage stops refusing a requirement nothing serves* replaced the first
124
+ `return 1 if bad else 0`, and that line **stopped being unique** the day `cmd_close` landed:
125
+ the plant disarmed `cmd_validate` instead and the guard, correctly, stayed green. This is a
126
+ new shape of an old class — a plant pinned to a literal usually rots because the literal
127
+ disappears; this one rotted because the literal **multiplied**. It anchors inside
128
+ `cmd_coverage` now.
129
+ - *a worked GATE verdict that prints no disclosures* matched a sentence that B-064 appended
130
+ `· holds: 0` to, hours earlier. It matches the line's **shape** now.
131
+
132
+ Both were watched landing and firing before the suite was re-run. `test:all` → **exit 0**
133
+ across eight suites: 376 guards, 114 graph fixtures, 24 exposure fixtures, 9 property checks,
134
+ 7 + 7 artifact fixtures, the release-gate harness and the documentation gate.
135
+
136
+ ### T-7 — the doctrine that names the graph, and module 1 closes
137
+
138
+ `scripts/graph.py`, `graph.schema.json` and `.task-pipeline/graph.json` had shipped and **no
139
+ doctrine file named any of them.** The schema disclosed it about itself: its `queue`
140
+ description said `continuity.md` did not yet know about `work-graph`. A capability with no
141
+ doctrine is one an agent meets by accident, and the run that meets it by accident is the run
142
+ that reads the graph itself — which is the one thing the design exists to prevent.
143
+
144
+ `references/work-graph.md` ships: what each field is for and the failure it prevents, the
145
+ nine verbs with their exit codes, the three invariants a schema cannot state and the fourth
146
+ reason `violations()` restates the ones it can (**the schema is never applied to a live
147
+ graph** — `graph.py` is stdlib by design, so a rule checked only against the shipped example
148
+ is a rule the run does not have), and what the graph deliberately does not do.
149
+
150
+ **Stage 2 now writes it and its gate reads it.** The queue was already declared there — *the
151
+ queue exists here, so the loop arms here* — and the graph is where that declaration becomes
152
+ walkable: the frozen REQ ids so `serves` resolves, one node per unit of work with its owner
153
+ and what it touches, an edge per dependency **naming what it hands over**, then
154
+ `graph.py validate`. A graph that does not validate is not a queue, and `next` refuses to
155
+ walk one. `continuity.md` prefers it over the module map and the task list for a measured
156
+ reason rather than a taste: 400 nodes and 4 produce the same 27-byte frontier.
157
+
158
+ **The verb list is discovered from the script, not typed into the doctrine.** Two homes for
159
+ one list is the class B-084 recorded twice in a day, and the plant is a tenth verb shipped
160
+ without a doctrine row — refused.
161
+
162
+ **And the position hole appeared a third time.** `graph.py validate` is named in stage 2's
163
+ body and in stage 2's gate, so a file-wide search was satisfied by either: removing it from
164
+ the body left the gate to cover for it. Body and gate are checked separately now, as are
165
+ `SKILL.md`'s stage-table row and `stages.md`'s prose — the stage list is compared across
166
+ three surfaces, so a criterion on one is a criterion the others quietly drop.
167
+
168
+ Six planted defects watched refused. **Module 1 of the role-agent programme is complete:
169
+ T-1 through T-7.**
170
+
171
+ ### T-5 — `close` consumes a verdict, and the verdict grew its seventh key
172
+
173
+ `verdict_violations()` had **no CLI verb**: the gate this module's own docstring calls *the
174
+ thing `close` consumes* was reachable only from the test suite, while `agents/verifier.md`
175
+ told an agent to run `graph.py close`. Shipped doctrine pointing at an absence — the class
176
+ B-080 is about, in the file that names it.
177
+
178
+ `close <id> --verdict <path>` checks the verdict, closes the node, applies `replan.add` and
179
+ `replan.park`, records a revision, and prints the goal with the new frontier count.
180
+
181
+ **A stop closes the node and refuses the next step.** `replan.possible: false` means the run
182
+ cannot continue around what it found — not that the work just verified did not happen.
183
+ Exiting 0 there would let the loop carry on past a stop; discarding the close would throw
184
+ away a verdict somebody earned. Both directions are fixtured, and the CI plant is the first
185
+ of them.
186
+
187
+ **`close` stamps the commit; the verifier never supplies it.** Evidence is prose, and a
188
+ verdict written after the tree moved is evidence about a different tree. An agent cannot name
189
+ the wrong commit if it is never the one naming one. Outside a checkout the stamp says
190
+ `unavailable` and why — canon 9a.
191
+
192
+ **The seventh key is `not_verified`, and it is the one people collapse into `not_done`.**
193
+ `not_done` is *asked for and absent*; `not_verified` is *present and unchecked* — the second
194
+ ships and the first does not. `npm test` has printed `unlooked: N` for releases, so the
195
+ pipeline named the concept everywhere except in the verdict that closes work with it. An
196
+ empty list is a valid answer; silence is not.
197
+
198
+ **And it walked straight into B-084's class again.** `close` wrote `verb: "close"` into the
199
+ revision log while the schema enumerated only `add` and `park` — so the first `close` wrote a
200
+ graph its own shipped schema rejects. The fixture asserting *the graph after a close still
201
+ validates* **passed**, because `violations()` never reaches an enum; a `jsonschema` probe
202
+ caught it. Both ends now agree from one place, the runtime enforces the set, and a fixture
203
+ compares the two homes directly rather than trusting either.
204
+
205
+ `test/graph_test.py` → **114 cases**.
206
+
207
+ ### B-092 — the report an operator actually reads
208
+
209
+ Every gate computes exactly what a not-verified field needs: `abstained` for claims the run
210
+ declined to make, `unlooked` for checks that did not look. **None of it reached the
211
+ hand-back** — four sections and two counters, none of which said what the claim covers or
212
+ what was never checked. So a run could hand back a report honest sentence by sentence and
213
+ still be **indistinguishable from a run whose checks never looked**, which is the failure
214
+ `references/progress.md` names three separate times about other things.
215
+
216
+ `SCOPE` and `NOT VERIFIED` are in the block now, and in the `hand:` ledger shape beside it —
217
+ the block is transient and the ledger is what survives a compaction, so a field in one and
218
+ not the other is lost exactly when it is needed.
219
+
220
+ `NOT VERIFIED` is **populated from the disclosures rather than composed**: the `abstained`
221
+ and `unlooked` sets in words, plus anything built this iteration that no check touched.
222
+ Composed by hand it becomes a summary of the parts somebody remembered. And the literal
223
+ `none within the stated scope` is required for the empty case, because an empty field and
224
+ *nothing inside what SCOPE names is unverified* read the same and are not the same — canon
225
+ 9a, one artifact over.
226
+
227
+ **Three of the five plants defeated the guard first, all by the same hole: either side
228
+ satisfying a check meant for both.** A search for the words anywhere in `progress.md` passed
229
+ a block that carried neither, since the doctrine discusses them in prose throughout — it
230
+ reads **inside the block** now. And a search for `scope` among `run.md`'s `hand:` lines was
231
+ satisfied by the *example* while the *shape* had lost it, and vice versa — each `hand:` line
232
+ is now checked against its own continuation, shape and worked example alike, because an
233
+ example that omits what the shape mandates teaches the omission.
234
+
235
+ One miss was mine rather than the guard's: the plant harness filtered failures for `B-092`
236
+ while that check cited only canon 9a, so a working guard read as a hole. The attribution now
237
+ names both.
238
+
239
+ ### Canon 9a — a measured zero and an unmeasured quantity may not print the same
240
+
241
+ This arrived **three times under three names** in one programme before anyone named it:
242
+ *State zero out loud* for the code graph, `unanchored`/`unresolvable` for the verification
243
+ ledger, and `unmeasured` for `graph.py doctrine` — joined this release by `next` reporting
244
+ how many runnable nodes declared no `touches`. Four sites, one rule, and
245
+ `references/audit.md` is explicit that a class seen twice becomes a mechanism rather than
246
+ another paragraph.
247
+
248
+ Canon 9 already said *carry the absence*. 9a says **refuse the number**: `0 of 34 files
249
+ read` and *the recorder was never installed* are opposite facts, and a `0` claims the first
250
+ while meaning the second — the most reassuring answer available, derived from an instrument
251
+ nobody switched on.
252
+
253
+ **The check is over the shape, not the four sites.** Any verb of `graph.py` that prints a
254
+ count must carry, in the same function, a word for the case where nothing measured it. A
255
+ list of the four would not catch the fifth, which is the whole reason the rule is written
256
+ down — and the plant is exactly that fifth: a new counting verb, added and refused.
257
+
258
+ **It also caught the difference between a word being present and a word being said.** The
259
+ first version searched the whole function body, so a site that kept its `undeclared`
260
+ variable and printed `note:` instead passed. It reads **printed text only** now — the same
261
+ lesson as four substring failures earlier in this release, arriving once more in a new
262
+ costume.
263
+
264
+ Measured before writing, and it changed the work: `templates/stage-coverage.sh` prints three
265
+ counts and no absence word, which looked like a fourth instance — and is not. It
266
+ **enumerates** every unaccounted stage by name, so its `accounted for 0` is a measurement
267
+ rather than a claim. The check was scoped to what actually has the defect.
268
+
269
+ ### B-093 — two runnable nodes, one mutable target
270
+
271
+ `references/planning.md` states the rule with the right teeth — *distinct is not the same as
272
+ independent, and the check is what they touch, never what they are called* — and it lived
273
+ **entirely in the markdown plan**. The role-agent design replaced that plan with
274
+ `graph.json` as the thing deciding what runs next, and the node had no field for what it
275
+ mutates. So `frontier()` ranked by `blocked_by` alone and could hand two agents two runnable
276
+ nodes that write the same file, with nothing able to report it.
277
+
278
+ `touches` ships on the node — paths, register names, remote resource ids — and `next` reports
279
+ a pair of **simultaneously-runnable** nodes sharing one. Only simultaneously: a pair where
280
+ one waits on the other never holds the target at once, and reporting it would be a warning
281
+ nobody can act on, which is how a warning becomes noise.
282
+
283
+ **Both reports go to stderr, and that is a contract rather than a preference.** The frontier
284
+ rows are parsed one per node and are the one line paid for on every iteration of every loop
285
+ — a warning among them reads as a node.
286
+
287
+ **And the third state is the one that matters: nobody declared anything.** A frontier whose
288
+ nodes carry no `touches` produces no pairs, which looks exactly like a frontier that was
289
+ checked and found clean. So `next` prints how many runnable nodes said nothing — the same
290
+ shape `doctrine` refuses to print `0` for, one axis over.
291
+
292
+ **Three existing fixtures went red, and they were right to.** They asserted *the frontier
293
+ and nothing else* by reading stdout and stderr merged, so a disclosure written to stderr
294
+ looked like a violation of the width contract. The contract is about stdout; the helpers
295
+ `run_out` and `run_at_out` read that stream alone, and a helper that merges the two cannot
296
+ tell the contract from its breach. Six planted defects watched refused, including both
297
+ disclosures relocated to stdout.
298
+
299
+ `test/graph_test.py` → **101 cases**.
300
+
301
+ ### B-065 — what the invariants bind together, coordination must guard together
302
+
303
+ Two halves of this row had gone stale and the third could not be mechanised, so it was
304
+ closed by measuring all three rather than by taking the easy one.
305
+
306
+ **Stale, and the measurement says so.** *«six registers under lease»* — `idRegisters` is
307
+ deliberately **empty**: the `fs` backend cannot reserve an id safely, and a declaration that
308
+ cannot be served reads as a capability nobody then writes the procedure for. *«the same
309
+ config in the other projects»* — measured: **all eight** family repositories carry one.
310
+
311
+ **Genuinely open, and now closed.** The version-sync invariant names **five** surfaces that
312
+ must move together; four were lease-guarded. The fifth is `SKILL-CARD.md` — whose omission
313
+ had already surfaced once on a release bump, from the validator rather than from a reader.
314
+ Two agents bumping a version collided there with no lease, which is not hypothetical: this
315
+ project lost four version numbers and a `files[]` entry to exactly that. `SKILL-CARD.md` and
316
+ the carry-over ledgers are guarded now.
317
+
318
+ **The surfaces are discovered, not listed.** A file *declaring* the current version — JSON
319
+ `"version": "x"` or the card's `| **Version** | x |` row — is a surface a bump touches, and
320
+ each must match a `guardedFiles` glob. A list here would drift from the invariant exactly
321
+ the way the last one did; watched catching a `registry.json` created for the test and never
322
+ mentioned to the check.
323
+
324
+ **And the habit was promoted rather than left as a row nobody can close.** *Take the lease
325
+ before the edit, not after the collision* is `R-009` now, with the retirement condition the
326
+ doctrine requires. It is a standing instruction and not a mechanism because whether a write
327
+ is *about to* happen is not a state a script can read — the guard refuses an unleased edit
328
+ *at* the edit, which is already too late to have avoided the race. B-75 is the evidence: a
329
+ second session committed to the umbrella with no leases and **invisible to `agent_sync
330
+ status`**, so the config being present is not the habit being held.
331
+
332
+ Five planted defects watched refused.
333
+
334
+ ### B-061 — which doctrine a run actually read, and the one number it must refuse to print
335
+
336
+ The bundle is **34 reference files**. A run reads some subset and nothing recorded which,
337
+ so **a skipped file and a read one were indistinguishable** — the class every guard in this
338
+ repository exists to catch, left standing over the doctrine itself.
339
+
340
+ A `PostToolUse` hook on `Read` now appends `read: references/<file>.md` to the run ledger,
341
+ deduplicated, and **always exits 0**: a hook that can fail a `Read` breaks every turn in
342
+ every session, including sessions of packs that never asked for this one. It is
343
+ hook-written for the same reason `gate:` is — a claim about what somebody read, written by
344
+ the party the claim is about, is not evidence.
345
+
346
+ `scripts/graph.py doctrine` reports it, and **the state that matters is the one where it
347
+ must not print a number.** No ledger, or a ledger with no `read:` lines, prints
348
+ `unmeasured` and says why: the hook being absent and the run having opened no doctrine are
349
+ **opposite facts**, the ledger cannot separate them, so neither is claimed. `0 of 34` there
350
+ would be the reassuring answer to a question nobody asked, over 34 files nobody checked —
351
+ and that is precisely the shape that went unnoticed for a whole bundle.
352
+
353
+ Where the hook did fire, it prints the count **and every unread file**, because a number
354
+ says there is a gap and not where. It is a disclosure — no floor, no direction, never a
355
+ target: a run that needs four files and reads four is not worse than one that reads thirty,
356
+ and the moment the number becomes something to raise, a run will open files to raise it.
357
+
358
+ **No per-file reading floor was invented.** Stage 0's mandatory items are the floor that
359
+ exists and they are not per-file; declaring one inside a measurement would be a doctrine
360
+ decision smuggled in as a count.
361
+
362
+ Two existing guards caught this change as it landed, both correctly: a relative link in a
363
+ seeded template (which resolves from `templates/` and nowhere it is seeded to), and **a
364
+ ledger shape with no reader** — `read:` had to be named in the doctrine that consumes it
365
+ before the template could declare it. Seven planted defects watched refused.
366
+
367
+ ### B-064 — a worked example is the executable half of doctrine, and now something checks one
368
+
369
+ Three times in one release a rule moved and its own example did not. An agent copies the
370
+ example literally and paraphrases the prose, so **the example is what ships** — and nothing
371
+ compared one against the rule it illustrates.
372
+
373
+ Now something does. Every `GATE <n> <name>: PASS|FAIL` block across `references/` and
374
+ `templates/` is read and required to carry the `holds:` line `gates.md` says every gate
375
+ prints. Three of the seven did not; they do now. And the page that **states** a mandate must
376
+ carry a conforming example of its own — the prose gets paraphrased and the example gets
377
+ copied, so the page stating a rule is the page that most needs one.
378
+
379
+ **The unit is the block, and that is not a detail — it is the whole finding.** A verdict is
380
+ its `GATE …` line plus the indented continuation beneath it. Measuring by *line* said five
381
+ examples lacked `holds:`. Measuring by *block* says three did: two carried it on a
382
+ continuation line all along.
383
+
384
+ **So the first version of this fix was wrong, and this check caught it ten minutes later.**
385
+ Reading line-wise, `holds: 0` was appended to two blocks that already said
386
+ `holds: 10 — none — enumerated 8/8 classes`. Two values for one disclosure in one verdict is
387
+ **worse than none**, because a reader picks one and copies whichever they picked. Both
388
+ duplicates are reverted, and the guard now refuses a repeated disclosure as well as a
389
+ missing one — a rule it learned from being broken by the change that introduced it.
390
+
391
+ Six planted defects watched refused: a continuation-line disclosure removed, an inline one
392
+ removed, the disclosure renamed inside an example, a **new** example added without it, a
393
+ second contradicting value, and the stating page losing its own example.
394
+
395
+ ### B-076 — a ruling is not a measurement
396
+
397
+ Gate types were `auto` and `manual`, and that was one short. A reviewer's ruling, a check
398
+ that the scenarios are coherent, a verdict that a mockup is good — none has a complete
399
+ deterministic check, and all three rode in `auto`, **indistinguishable from an exit code**.
400
+ A coverage table then cannot tell a measured row from an opinion, and the role-agent
401
+ programme multiplies it: `reviewer`, `ux`, `ui` and `market-analyst` produce judgement by
402
+ design.
403
+
404
+ `judgment` ships. `auto` now means only what a machine established, and a judgment gate
405
+ **must name its `judge`** — the schema refuses it otherwise. That obligation is not
406
+ bookkeeping: a ruling with no author cannot be weighed for independence, and independence
407
+ is not a property of *having* a reviewer. This pipeline's own `R-005` reader shares a
408
+ model, instructions and repository with the author it reviews, differing only in context.
409
+ It is a real second reading and it is **not** a deterministic runner, a contract at another
410
+ boundary, or an external system. Naming the judge is what makes that difference visible
411
+ instead of assumed.
412
+
413
+ **It generalises a rule this repository already had in one place.**
414
+ `templates/verification.md` turns a coverage verdict of `review` into `none` in the `Auto`
415
+ column, because that column records what a machine established. That is the `judgment` type
416
+ applied to one column, and it has been sitting there being right.
417
+
418
+ **Which of this pipeline's own gates are judgement is deliberately not decided.**
419
+ `references/gates.md` says gate assignment is the operator's call and the framework fixes
420
+ none — so shipping a reclassified stage list would contradict the sentence above it.
421
+
422
+ **Eight planted defects, all refused on the first attempt — including the two shapes that
423
+ defeated every guard before this one.** Renaming the doctrine row to `judgement` and the
424
+ section to *About judgment gates* both fail now, because the checks anchor on a line's
425
+ opening cell rather than searching for a word. That was the session's repeated lesson —
426
+ four guards had been beaten by a substring — and this is the first one written with it in
427
+ hand.
428
+
429
+ ### B-081 — proof expires, and the ledger had only one end of it
430
+
431
+ The verification ledger tracked rows nobody had **ever** confirmed and had no notion of a
432
+ row whose confirmation the tree has since **overtaken**. A row verified at commit A read
433
+ `verified` after commit B, forever. Those are the same failure from two ends, and only one
434
+ end was instrumented — so a ledger could read fully green over a tree where every check ran
435
+ against code that has since moved.
436
+
437
+ **This is a port, not a design.** `references/knowledge-graph.md` already gives the code
438
+ graph a stamp, a distance, three states, and a marker on every non-current one. The same
439
+ contract, applied to the ledger: `Observed at` is the commit the check ran against, and
440
+ `exposure.sh` reports **current · behind · unresolvable · unanchored** — a disclosure with
441
+ no floor, no direction and never a target, exactly like the `never` column beside it.
442
+
443
+ `behind` means **unproven for this tree, never wrong.** The section knows the distance and
444
+ does not know whether the commits between touched anything the row covers; claiming more
445
+ would be the estimate-printed-as-measurement this pipeline refuses elsewhere. And
446
+ **invalidation is not deletion** — an overtaken row is true about the tree it observed and
447
+ stays; re-observing appends.
448
+
449
+ **Where it prints turned out to matter as much as what it prints.** The first placement put
450
+ the section after the check-list, and `exposure.sh` exits early when nothing is unverified
451
+ — so the counts were invisible in exactly the state where they matter most. `0 unverified`
452
+ is the sentence most likely to be read as *nothing to look at*.
453
+
454
+ **Three of the eight plants defeated the guard on the first attempt, and one of them for the
455
+ fourth time this session.** Checking `"staleness" not in output` passed a section renamed to
456
+ `was-staleness`, because the old string is a substring of the new one; the guard anchors on a
457
+ line *beginning* `staleness —` now. Checking `"not trusted" not in output` passed a plant
458
+ that stripped the marker from the `behind` row only, because the unresolvable row still
459
+ carried one — it is checked **per state** now. And nothing asserted the **shipped** template
460
+ carried the column at all, so every project seeding it would have got a section dormant
461
+ forever, and dormant is green. All eight refused now.
462
+
463
+ ### B-087 — the pointer is not the path
464
+
465
+ Stage 10 already required `git submodule status` with no `+` and every repository clean
466
+ and pushed. That is a statement about **commits**: the parent points at the child's newest
467
+ one. It proves nothing about whether the two versions work *together*. A parent can point
468
+ at a green submodule whose contract the parent's own code calls with the previous
469
+ signature, and every check passes — the child's suite ran against the child, the parent's
470
+ against the parent, and no check ran across the pointer. Neither repository looks wrong
471
+ alone, which is how this survived being written down twice.
472
+
473
+ `templates/convergence.sh` ships, and the criterion fires **only where a component
474
+ pointer moved in the range being accepted** — a range that crossed no boundary has no seam
475
+ to prove, and demanding a record for it is how a gate becomes noise. Where one moved, the
476
+ acceptance owes a named cross-component path, the exact versions it observed, and the
477
+ observation to the same standard a single REQ meets.
478
+
479
+ It also checks the thing `git submodule status` **cannot see: whether the pinned commit is
480
+ published at all.** Measured here on 2026-08-16 — a release tag failed CI at checkout
481
+ because the parent pinned a commit that existed only on one machine, and `submodule
482
+ status` showed no `+` because the pointer matched the *local* head.
483
+
484
+ **Two things happened on its first live run, and both are the point.** It found a real,
485
+ current defect in the umbrella: the parent's pointer and the child's HEAD disagree, so a
486
+ clone would get a different tree than the one tested. And it found a defect **in itself** —
487
+ the published-pin section read `git -C <c> rev-parse HEAD`, the *child's* HEAD, where it
488
+ needed `git rev-parse HEAD:<path>`, the parent's pointer. Those are the same fact only
489
+ while they agree, and they disagree in precisely the case the section exists for. So its
490
+ first live run reported about a commit the parent does not pin.
491
+
492
+ **The gate does not read the script; it runs it over four shapes built from real git
493
+ repositories** — a repository pinning nothing (dormant and green, because a gate that
494
+ starts red teaches its project the gate is noise), a range touching no component, a moved
495
+ pointer with no record, and a record that names no version. Five planted defects watched
496
+ refused, including a verdict block that prints FAIL and returns 0.
497
+
498
+ ### B-086 — what produced the proof
499
+
500
+ Every artifact here recorded what was done, what proved it, and whether a person looked.
501
+ None recorded what **produced** it. Two runs six months apart, one under v1.40 doctrine
502
+ and one under v1.69, leave indistinguishable coverage tables — so a defect traced to a
503
+ doctrine change cannot be scoped to the runs that carried it.
504
+
505
+ `graph.py producer` prints seven fields, and **needs no graph**, because it is wanted
506
+ beside an acceptance artifact rather than inside a run. Three resolve from the tree —
507
+ the skill version from the plugin manifest, a digest of the project's `pipeline.json`,
508
+ and `git rev-parse HEAD`. Four belong to the harness (`actor`, `model`, `runtime`,
509
+ `trace`) and are read from named environment variables a project wires once.
510
+
511
+ **A field that cannot be resolved prints anyway and says why.** An omitted field is
512
+ indistinguishable from one that was checked and found empty — the rule every disclosure
513
+ in this pipeline already follows, applied to the one artifact that had no disclosures at
514
+ all. And `model` is deliberately **not inferred**: naming a vendor id in a shipped skill
515
+ is forbidden here, and inferring the wrong one is worse than saying nothing.
516
+
517
+ `templates/verification.md` carries the block above its rows, with the command that
518
+ computes it, so it is pasted rather than typed.
519
+
520
+ **Two harness defects surfaced while building this, and both were worth more than the
521
+ feature.** A fixture raising anything but `AssertionError` used to abort the whole suite —
522
+ one `KeyError` hid every case after it, and a harness that stops at the first crash
523
+ reports fewer failures than exist. It reports a `CRASH` line now and keeps going: the
524
+ count went from 1 visible failure to 4. And the guard could not observe the
525
+ no-manifest branch, because this repository always has a manifest — so a version *guessed*
526
+ as `task-pipeline@unknown` passed. That branch has its own fixture now, copying the bundle
527
+ alone, which is exactly what a plain-skill install is; watched failing against the guess.
528
+
529
+ `test/graph_test.py` → **93 cases**.
530
+
531
+ ### B-085 and B-077 — the one edge between intent and execution, and the relation over it
532
+
533
+ `serves` was a non-empty string and nothing more, so `serves: "REQ-999"` and
534
+ `serves: "asdf"` passed every gate identically — and that field is the **only** edge
535
+ joining the intent graph to the execution graph. T-2's own DoD claimed *«every `serves`
536
+ resolves»* and nothing did.
537
+
538
+ The graph now carries `requirements`: the REQ ids the brief froze, **required and
539
+ non-empty**, plus optional `goal_clauses` for release work no requirement names.
540
+ Enumerated rather than substring-matched against the goal's prose, because matching a
541
+ sentence is the kind of check that produces confidence without correctness. A `serves`
542
+ resolving to neither is refused, with a near-miss hint.
543
+
544
+ **And `add` refuses to invent a requirement.** The REQ table is frozen at stage 0 —
545
+ adding to it is free and the *brief* does it, not a node. The refusal says so and lists
546
+ what is available, because an agent told only «no» will try a synonym.
547
+
548
+ **`graph.py coverage` computes the relation, and says which quarter of it it cannot
549
+ see.** `references/acceptance.md` defines the path a requirement takes and an agent
550
+ walked it from a checklist, one REQ at a time — the pipeline's own definition of a rule
551
+ that should have been a mechanism. Three directions are now computed: a requirement no
552
+ node serves, a requirement whose every node is **parked** (covered on paper and by
553
+ nothing that will run), and each requirement with the nodes and statuses serving it. The
554
+ fourth — an evidence row closing no requirement — lives in `docs/evidence/verification.md`,
555
+ which this script does not read, and **the report says that out loud**, because a report
556
+ silent about its own blind spot reads as the whole relation.
557
+
558
+ **Two of the guards for this were defeated on their first attempt, and both by shapes
559
+ this file has now met three times.** A source scan for `cmd_coverage` passed a
560
+ renamed-and-unwired `_cmd_coverage_disabled`, because the old name is a substring of the
561
+ new one. And the guard ran `coverage` only against the shipped example, which is fully
562
+ covered on paper — so a `return 0` that had stopped refusing anything passed. Both are
563
+ behavioural now: the example supplies the **failing** control (it has a parked-only
564
+ requirement, and refusing it is correct), a copy with the parked node removed supplies
565
+ the passing one, and the subparsers are **built from the dispatch table**, so a verb
566
+ argparse accepts and the dispatch lacks cannot exist — it used to raise `KeyError`, which
567
+ is a traceback where a named refusal belongs. Seven planted defects watched refused.
568
+
569
+ `test/graph_test.py` → **85 cases**.
570
+
571
+ ### B-084 — the mutation verb was drawing chronology
572
+
573
+ The graph stored one fact in two unlinked places. `blocked_by` is what `frontier()`
574
+ obeys; `edges` carries the `payload` the schema requires — and nothing read it past a
575
+ from/to existence check. So `references/planning.md`'s fake-edge test, stated for the
576
+ markdown plan, was **unenforceable on the artifact that replaced the plan**, and
577
+ `graph.py add` wrote the first field and never the second. Every node added mid-run
578
+ therefore created a dependency whose payload was unnamed *by construction*. Measured by
579
+ the four-way manifesto audit: adding a node to the shipped example gave 5 nodes, 2
580
+ edges, `validate` exit 0.
581
+
582
+ Four things move together, because separately each leaves a hole the others cover:
583
+
584
+ - **`violations()` refuses an edge whose `payload` is missing or blank**, and refuses a
585
+ `blocked_by` with no payload-bearing edge **in the blocker→blocked direction** — a
586
+ backwards edge no longer satisfies a dependency.
587
+ - **`title` and `serves` must be non-empty at runtime.** Both were schema-only, and the
588
+ schema has never run against a live graph, so `serves: ""` passed the gate while the
589
+ format forbade it.
590
+ - **`add` takes `--carries`**, one per `--blocked-by`, pairing in the order written, and
591
+ writes the edge **with** the node. A count mismatch is refused and names both counts.
592
+ - **`add` takes `--why`, and there is now a revision log.** `park` demanded a reason
593
+ from the start and `add` demanded nothing, which left half the graph's revision
594
+ surface silent — and a graph that changed for reasons nobody recorded can always
595
+ explain its own completion by appealing to a plan that existed only at the end. Both
596
+ verbs append `{verb, node, why}`; the schema requires all three and requires `why` to
597
+ hold a non-whitespace character; `next` never prints the log, because the frontier's
598
+ width is what a loop pays for on every iteration and this grows.
599
+
600
+ **Tightening the rule invalidated the fixtures that had relied on it being loose**,
601
+ which is the clearest evidence it bites: the test helper now *derives* an edge for every
602
+ `blocked_by` it builds, and the one fixture that needs a dependency with no edge asks
603
+ for it explicitly. Four planted defects were watched being refused, including a
604
+ `why` pattern of `^.*$` and a nullable `why` — the two shapes that defeated this file
605
+ twice already today.
606
+
607
+ `test/graph_test.py` → **75 cases**.
608
+
609
+ ### The npx install path lost the verifier without saying so
610
+
611
+ `agents/` is a Claude Code plugin capability, and `install.sh` and
612
+ `bin/task-pipeline.js` copy the skill directory and the command and nothing else. That
613
+ absence is the **design** — the brief chose plugin agents with honest degradation. It
614
+ was silent, which is the part that was not: an operator on the npx path reads doctrine
615
+ naming `task-pipeline:verifier`, finds a name that resolves to nothing, and nothing they
616
+ ran ever mentioned it.
617
+
618
+ Both paths now print what they are not installing, how many files it is, that **every
619
+ role still runs** — on the main thread rather than in its own context, which costs
620
+ context and speed and not doctrine — and the two commands that get the agent-backed
621
+ version.
622
+
623
+ **The guard RUNS the installers against a throwaway `HOME` rather than reading them**,
624
+ and that decision was forced twice. The first version scanned the source for the printed
625
+ string — and the first draft of this very fix defined `discloseAgents()` and never
626
+ called it, which satisfies a source scan exactly. The second was defeated by a
627
+ substring: `bin/task-pipeline.js` already prints *"Any agent (70+): npx skills add…"*,
628
+ about the seventy agent products this skill installs into, and a check for the word
629
+ `agent` passed it while the real gap stood untouched. It matches `agents/` with the
630
+ slash, in output, from a real run. Three planted defects watched being refused, the
631
+ dead-code one included.
632
+
633
+ ### The R-005 read of T-3 — fourteen findings, and two of them were critical
634
+
635
+ The reader that standing instruction `R-005` requires was given the wave and told to
636
+ defeat it. It did, and the two worst were in checks written that same hour:
637
+
638
+ **The new schema check read the rule's shape and never its behaviour.** It asserted
639
+ that `parked_reason` carried a `pattern` — and `"^.*$"` is a pattern. Swap it in, drop
640
+ `minLength`, and the whole gate stays green over a schema that accepts `parked_reason:
641
+ ""`. This is the fourth time this file has been defeated by the same class: a name in
642
+ `required` constraining nothing, a nullable type, a decorative `items`, and now a
643
+ pattern that matches everything. **The check now RUNS the regex** — it must reject
644
+ `""` and `" "` and accept ordinary text — because presence has never once been
645
+ behaviour here.
646
+
647
+ **And the same field was left nullable.** `pattern` and `minLength` are string-only
648
+ assertions, so `type: ["string", "null"]` satisfies both vacuously and `parked_reason:
649
+ null` sailed through. The check three screens above tests `owner`'s type for exactly
650
+ this reason; the new field did not inherit it. It does now, at both ends of the rule.
651
+
652
+ **The third was worse than either, because it disarmed both rules at once.** Add one
653
+ impossible name to each `if.required` and, under `additionalProperties: false`, no node
654
+ can ever match — `done → evidence` and `parked → reason` both go inert while every key
655
+ the check reads is still in place, and `npm test` exits 0. A conditional is now accepted
656
+ only when its `if` constrains the status and **nothing else**.
657
+
658
+ **Then the finding that made a claim in this repository false.** Nothing ever validated
659
+ a *live* `.task-pipeline/graph.json` against `graph.schema.json` — only the shipped
660
+ example, at build time. So both conditional rules rested entirely on the scripts
661
+ behaving, which is precisely what the validator's own new message said had stopped being
662
+ true. `graph.py validate` now enforces what the schema states: `done` implies readable
663
+ evidence, `parked` implies a reason, the `goal` exists, ids match their shape, and
664
+ `blocked_by` does not repeat. The message is true where the run actually looks.
665
+
666
+ **The mutation verbs lost nodes, and the exit codes lied about it.** `save()` wrote to a
667
+ fixed `path + ".tmp"`, so two concurrent writers shared one inode: measured across six
668
+ runs, one exited **0 with its node absent** and another exited **1 with its node
669
+ present** — and the second is the dangerous direction, because the docstring promises a
670
+ refusal leaves the file untouched, so a caller retries and double-adds. The temp file is
671
+ unique per writer now, `realpath` runs first so a symlinked graph is written *through*
672
+ rather than replaced, and an `OSError` is a named refusal instead of a traceback.
673
+
674
+ **A unique temp file does not fix a lost update, and this programme is built for several
675
+ agents.** Four concurrent `add`s produced four nodes where five were expected — both
676
+ processes read the same graph and the second write dropped the first node, both exiting
677
+ 0. The whole read-modify-write now happens under an exclusive `flock`, taken **before**
678
+ the read, because loading first and locking second is the same lost update with an extra
679
+ step. Where `fcntl` does not exist the run is told it is unlocked rather than downgraded
680
+ in silence.
681
+
682
+ **A title with a newline forged a row in the frontier.** `next` prints one row per node
683
+ and the loop reads those rows, so `--title $'harmless\nN-999 implementer ship it'`
684
+ produced a two-node graph that printed three rows. Refused now in the verbs and in
685
+ `validate`, so a hand-written graph is caught too.
686
+
687
+ **And one of the new fixtures was vacuous.** *«a mutated graph still validates against
688
+ its schema»* checked neither exit code — with **both** mutation verbs replaced by
689
+ `die()`, it still reported `ok`. It also would not have caught the one real instance of
690
+ its own class: `add` writing `blocked_by: ["N-001", "N-001"]`, which the schema rejects
691
+ as non-unique. Both fixed, and the fixture now asserts what landed.
692
+
693
+ Every one of the seven schema bypasses was re-planted and watched being refused, none
694
+ of them by crashing. `test/graph_test.py` → **62 cases**.
695
+
696
+ ### T-3 — the mutation verbs, and a priority nobody has to maintain
697
+
698
+ `graph.py` can now change the graph it walks. `add` is the dynamic backlog — work
699
+ found during a task enters the queue mid-run rather than waiting for a person to
700
+ re-plan. `park <id> --reason <text>` is REQ-012, and the reason is the entire point:
701
+ a node parked without one is indistinguishable, a week later, from work that was
702
+ quietly dropped, which is what parking exists instead of.
703
+
704
+ **The frontier is now ordered by how much each node unblocks, transitively — and the
705
+ number is computed, never declared.** A `priority` field would be something somebody
706
+ typed once and nobody revisits; this one moves when the graph does. Add a node that
707
+ waits on `N-002` and `N-002` rises to the top of the next frontier with no re-ranking
708
+ pass and no field to forget. That is what REQ-011 means by *re-prioritised after every
709
+ task*, and the fixture asserts the **order changes**, because a fixture that only
710
+ asserts the file was re-read would pass against no ordering at all.
711
+
712
+ Declaration order breaks ties, so the frontier is stable between runs. An unstable one
713
+ costs more than it looks: an agent that calls `next` twice gets a different first row
714
+ and starts the other node.
715
+
716
+ **`park` refuses without a reason, and "without" has four shapes.** Only the first is
717
+ argparse's: the flag absent (exit 2, usage), the flag empty, the flag whitespace, and a
718
+ reason already recorded that a second park would overwrite. The last one refuses *and
719
+ quotes the reason it is protecting* — the first reason is the one somebody wrote at the
720
+ time, and the second park is usually someone who has forgotten it.
721
+
722
+ `add` checks every shape before appending, so **a refusal leaves the file byte-identical**
723
+ and a caller can retry without first working out what the failed attempt did. Ids are
724
+ allocated from the **maximum in use, never the count** — ids stop being contiguous the
725
+ first time anything is renumbered, and from that moment counting hands out one that
726
+ already exists. Both verbs refuse outright on a graph that was *already* invalid and say
727
+ so in those words: a mutation that reports pre-existing damage as though the caller
728
+ caused it sends the next fix to the wrong place.
729
+
730
+ `save()` writes to a temp file beside the graph and `os.replace`s it. A crash mid-write
731
+ now loses the mutation instead of the queue. This repository has destroyed a file by
732
+ writing it in place twice, and both times what saved it was a copy somebody had made by
733
+ hand.
734
+
735
+ **REQ-012 moved from the script into the format.** The reason used to live in `note` —
736
+ a free-text field with no description and no rule, which made a park carrying a reason
737
+ and a park carrying an unrelated remark the same shape to every reader and every check.
738
+ It is `parked_reason` now, **required by the schema when the status is `parked`**,
739
+ exactly as `evidence` is required when the status is `done`, with the same
740
+ non-whitespace `pattern` the wave-2 convergence check taught this file to write.
741
+
742
+ **And that broke a guard, which is the guard working.** draft-07 allows one `if`/`then`
743
+ per schema object, so the second rule went into an `allOf` beside the first — and
744
+ `test/validate.py` read `node["if"]` literally and went red immediately. It walks `allOf`
745
+ recursively now, so a schema stating both rules inline, both in `allOf`, or one of each
746
+ reads the same. Six planted defects were watched refusing, including the inverse: the
747
+ old inline shape is still accepted, which is what a widening has to prove it did not
748
+ break. Two of the six are now CI plants; the section's running count is at the top.
749
+
750
+ `test/graph_test.py` → **52 cases**.
751
+
752
+ ### Wave 2 — T-4 and T-6, and the check `build.md` puts over a fan-out
753
+
754
+ `agents/verifier.md` ships — the first agent this plugin has. It closes one node and
755
+ returns a six-key verdict, and `graph.py`'s `verdict_violations()` refuses one that
756
+ omits a key or claims `done` with no evidence. The agent file says what it cannot do
757
+ and why that matters: **it cannot ask the operator anything**, so a verdict meaning
758
+ *«I need a decision»* has to say so in `replan.why` rather than end in a question
759
+ nobody will see.
760
+
761
+ `pipeline.json` moves to `mode: dynamic` — `interval` dropped, because the schema
762
+ calls it meaningless there — and records `release.goal`.
763
+
764
+ **Then the convergence check `references/build.md` §4.2a requires over a fanned-out
765
+ group, and it earned its place.** Nine contradictions, every one of them invisible to
766
+ the three per-task reviews that had already passed:
767
+
768
+ | Found | Between |
769
+ |---|---|
770
+ | the verdict gate accepted `evidence: ["", " "]` that the **schema refuses** — `close` would write a node its own shipped schema rejects | `graph.py` ↔ `graph.schema.json` |
771
+ | `release.goal` was undeclared in the schema, so the guard T-6 shipped **could not see the field T-6 shipped** — `additionalProperties` is true, and renaming it away kept every gate green | `pipeline.json` ↔ `pipeline.schema.json` |
772
+ | `ROLES` held ten of the brief's thirteen — and its own refusal message **named the manager while the set rejected it** | `graph.py` ↔ the brief |
773
+ | `verifier.md` told an agent to run `graph.py close`, which is T-5 and does not exist | the agent ↔ the script |
774
+ | `_goal_note` claimed the goal is *"printed above the frontier every iteration"*; nothing prints them together | the config ↔ the script |
775
+ | the schema's `queue` cited `continuity.md`, which still describes a two-item queue set that does not include `work-graph` | the schema ↔ the doctrine |
776
+ | the brief's REQ-005 required `plugin.json` to **declare** `agents`, and declaring it fails `--strict` | the brief ↔ the platform |
777
+ | *«five keys»* over a six-key object, in five places | everywhere at once |
778
+ | T-6's new guard shipped with **no negative self-test**, against this repo's own stage-6 gate | the change ↔ the gate |
779
+
780
+ Every one is fixed. Two are worth naming for the shape rather than the fix:
781
+
782
+ **`ROLES` conflated two different axes.** Whether a role ships as a subagent and
783
+ whether it may **own a node** are separate questions, and the first draft answered
784
+ the second with the first. `manager` and `business-analyst` are main-thread doctrine
785
+ *because* their job is asking the operator — that is precisely why they cannot be
786
+ agents, and it says nothing about whether work can belong to them. Both own nodes
787
+ now; `project` still cannot, because the brief defers it for having no stated job,
788
+ and a role that cannot say what it does cannot own work either.
789
+
790
+ **And the fix for the evidence bug was itself incomplete.** A cross-check fixture —
791
+ asking the gate and the schema the same question and requiring the same answer —
792
+ caught `[" "]` surviving one and not the other: the gate strips, and `minLength: 1`
793
+ counts a space. The schema now requires a non-whitespace character, and the fixture
794
+ that found it is in the suite.
795
+
796
+ `test/graph_test.py` → **29 cases**. Two of them were added here.
797
+ ## v1.68.0 — the worst body in the family, and the rule that was wrong about it
798
+
799
+ **6685 tokens against a 5000 budget → 4735**, under the 4750 working limit, by
800
+ splitting rather than trimming. This was the largest `SKILL.md` body in the
801
+ ssheleg family and the furthest over — 34% — and the body loads on every turn of
802
+ every session that resolves the skill.
803
+
804
+ Most of the overrun sat in the **stage table's Gate column**, which restated
805
+ `references/stages.md` under a heading that literally says *(detail in
806
+ `references/stages.md`)*. The table is the index and the run order now; the
807
+ reference is what you read while standing in the stage. Nothing was deleted:
808
+
809
+ | Moved | To | Why there |
810
+ |---|---|---|
811
+ | Stage 10 in a project of several repositories | `references/acceptance.md` | it owns stage-10 close-out |
812
+ | Step 5's cross-cutting rules | `references/gates.md` | they fire at any stage, not inside step 5 |
813
+
814
+ and five *Prerequisites* paragraphs that restated a reference in full were cut to
815
+ the rule plus the failure it prevents — which is what a body is for — with the
816
+ procedure left in the file that owns it.
817
+
818
+ **All 38 routed trigger phrases across both skills survive verbatim**
819
+ (`node test/advertised_check.js`), and the stage list still matches across the
820
+ three surfaces the validator compares mechanically.
821
+
822
+ ### The description rule was wrong, and this repository was already right
823
+
824
+ The family's shared auditor demands a description **start** with `Use when …`.
825
+ This repository's own validator refuses exactly that, and its comment says why:
826
+ Anthropic's guidance asks for **both** halves — what the skill does and when to
827
+ use it — and their own example leads with the capability (*"Extracts text and
828
+ tables from PDF files… Use when working with PDF files."*). Demanding `Use when`
829
+ at position 0 enforces the WHEN half and leaves the WHAT half optional.
830
+
831
+ So the 2026-08-16 audit's finding that this description *"does not open with Use
832
+ when, against the house rule its sibling obeys"* is **withdrawn — the house rule
833
+ is the one that is wrong**, and this repository had corrected its own copy of it
834
+ already. Applying the corrected rule to the family measures **22 of 24 skills**
835
+ opening with the trigger, so flipping it rewrites 22 descriptions that carry live
836
+ routing phrases. That is a family decision rather than a member's, and it is
837
+ filed as umbrella `B-76` rather than taken here.
838
+
839
+ ### Fixed
840
+
841
+ - A negative self-test was pinned to a literal containing a **line break**, so it
842
+ stopped landing the moment the paragraph reflowed — the guard then read green
843
+ while proving nothing. Matched by regex now. Same class as the two that refused
844
+ `seo-aeo-audit`'s release earlier the same day, and the reason the local gate
845
+ there learned to catch it before the tag.
846
+
847
+ Guards: 351 → **351**. No guard was added or removed — one plant was repaired, and
848
+ the suite that reports `all 351 guards provably reject their planted defect` was
849
+ red until it was, which is the whole point of counting them.
850
+
851
+ Found by the nine-repository audit of 2026-08-16 (umbrella `B-66`;
852
+ `F-task-pipeline-01`, and `F-task-pipeline-02` withdrawn).
853
+
3
854
  ## v1.67.0 — a ledger records two different things, and most record only one
4
855
 
5
856
  **A ledger records two different things, and most record only one.** *What confirmed it*
@@ -4175,6 +5026,11 @@ this class of defect surfaces:
4175
5026
 
4176
5027
  ## v1.5.0 — 2026-08-01
4177
5028
 
5029
+ > **Never released on its own.** There is no `v1.5.0` tag and no `1.5.0` on npm,
5030
+ > so `npm install task-pipeline-skill@1.5.0` and `git checkout v1.5.0` both fail. This section
5031
+ > describes work that shipped inside a later version. The note is here because
5032
+ > the section reads as a release (2026-08-17, umbrella `B-71`).
5033
+
4178
5034
  ### `references/knowledge-graph.md` — the code graph as a source, and as a second opinion
4179
5035
 
4180
5036
  A grep finds a **name**. The questions that actually stop a run are *what calls this*