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