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.
- package/CHANGELOG.md +856 -0
- package/README.md +25 -0
- package/SKILL-CARD.md +1 -1
- package/bin/task-pipeline.js +30 -0
- package/package.json +4 -3
- package/plugins/task-pipeline/.claude-plugin/plugin.json +1 -1
- package/plugins/task-pipeline/agents/verifier.md +88 -0
- package/plugins/task-pipeline/commands/task-pipeline.md +22 -0
- package/plugins/task-pipeline/skills/task-pipeline/SKILL.md +84 -136
- package/plugins/task-pipeline/skills/task-pipeline/graph.example.json +73 -0
- package/plugins/task-pipeline/skills/task-pipeline/graph.schema.json +253 -0
- package/plugins/task-pipeline/skills/task-pipeline/pipeline.schema.json +46 -3
- package/plugins/task-pipeline/skills/task-pipeline/references/acceptance.md +69 -0
- package/plugins/task-pipeline/skills/task-pipeline/references/audit.md +1 -1
- package/plugins/task-pipeline/skills/task-pipeline/references/continuity.md +9 -1
- package/plugins/task-pipeline/skills/task-pipeline/references/documentation.md +17 -0
- package/plugins/task-pipeline/skills/task-pipeline/references/gates.md +91 -1
- package/plugins/task-pipeline/skills/task-pipeline/references/portability.md +1 -0
- package/plugins/task-pipeline/skills/task-pipeline/references/progress.md +41 -0
- package/plugins/task-pipeline/skills/task-pipeline/references/stages.md +11 -1
- package/plugins/task-pipeline/skills/task-pipeline/references/verification.md +52 -0
- package/plugins/task-pipeline/skills/task-pipeline/references/work-graph.md +121 -0
- package/plugins/task-pipeline/skills/task-pipeline/scripts/graph.py +1113 -0
- package/plugins/task-pipeline/skills/task-pipeline/templates/README.md +1 -0
- package/plugins/task-pipeline/skills/task-pipeline/templates/carryover.md +1 -1
- package/plugins/task-pipeline/skills/task-pipeline/templates/convergence.sh +146 -0
- package/plugins/task-pipeline/skills/task-pipeline/templates/exposure.sh +104 -1
- package/plugins/task-pipeline/skills/task-pipeline/templates/hooks.example.json +13 -1
- package/plugins/task-pipeline/skills/task-pipeline/templates/run.md +32 -0
- 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*
|