task-pipeline-skill 1.68.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 +799 -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 +2 -1
- 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 +36 -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 +46 -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
|
@@ -82,9 +82,10 @@
|
|
|
82
82
|
"enum": [
|
|
83
83
|
"module-map",
|
|
84
84
|
"plan-tasks",
|
|
85
|
+
"work-graph",
|
|
85
86
|
"none"
|
|
86
87
|
],
|
|
87
|
-
"description": "What the loop walks. A loop with no queue is a timer: it says how often to continue and never what the next item is, so the run picks its next move by recollection. module-map = stage 2's decomposition (a platform); plan-tasks = stage 4's task list; none = the mode is armed for the stage boundaries only. Doctrine: references/continuity.md -> Part 1a."
|
|
88
|
+
"description": "What the loop walks. A loop with no queue is a timer: it says how often to continue and never what the next item is, so the run picks its next move by recollection. module-map = stage 2's decomposition (a platform); plan-tasks = stage 4's task list; none = the mode is armed for the stage boundaries only. Doctrine: references/continuity.md -> Part 1a. `work-graph` is `.task-pipeline/graph.json`, walked by `scripts/graph.py next`; doctrine: references/work-graph.md."
|
|
88
89
|
},
|
|
89
90
|
"arm": {
|
|
90
91
|
"enum": [
|
|
@@ -145,6 +146,15 @@
|
|
|
145
146
|
"minLength": 1
|
|
146
147
|
},
|
|
147
148
|
"description": "Post-release smoke checks that must pass after a release — this is the release's own post-deploy gate (stage 8 applied to shipping the package itself)."
|
|
149
|
+
},
|
|
150
|
+
"goal": {
|
|
151
|
+
"type": "string",
|
|
152
|
+
"minLength": 1,
|
|
153
|
+
"description": "What this release is for, in one sentence. Printed beside the queue on every loop iteration so drift from it is visible rather than remembered, and a node serving neither it nor a REQ is parked WITH THAT AS THE REASON. Declared here because the guard that validates this config could not otherwise see the field: `additionalProperties` is true, so an undeclared `goal` could be renamed away and every gate would stay green (found by the wave-2 convergence check)."
|
|
154
|
+
},
|
|
155
|
+
"_goal_note": {
|
|
156
|
+
"type": "string",
|
|
157
|
+
"description": "Free prose beside `goal`."
|
|
148
158
|
}
|
|
149
159
|
}
|
|
150
160
|
},
|
|
@@ -195,16 +205,49 @@
|
|
|
195
205
|
"type": {
|
|
196
206
|
"enum": [
|
|
197
207
|
"auto",
|
|
208
|
+
"judgment",
|
|
198
209
|
"manual"
|
|
199
210
|
],
|
|
200
|
-
"description": "auto = the orchestrator verifies `check` itself
|
|
211
|
+
"description": "auto = the orchestrator verifies `check` itself, pass/fail — a fact a MACHINE established. judgment = there is no complete deterministic check and someone rules on it; the ruling is recorded AS judgement and never as a measurement, and the gate must name its `judge`. manual = wait for the operator's explicit go. Two types were not enough: a reviewer's ruling, a check that scenarios are coherent and a verdict that a mockup is good all rode in `auto`, indistinguishable from an exit code — so a coverage table could not tell a measured row from an opinion."
|
|
201
212
|
},
|
|
202
213
|
"check": {
|
|
203
214
|
"type": "string",
|
|
204
215
|
"minLength": 1,
|
|
205
216
|
"description": "The gate condition, in prose."
|
|
217
|
+
},
|
|
218
|
+
"judge": {
|
|
219
|
+
"type": "string",
|
|
220
|
+
"minLength": 1,
|
|
221
|
+
"pattern": "\\S",
|
|
222
|
+
"description": "Who or what renders the judgement — a role, an agent, a person. Required when `type` is `judgment`. Without it the ruling has no author, and a judgement whose author is unknown cannot be weighed against its independence: this pipeline's own `R-005` reader shares a model and a repository with the author it reviews, which is a different kind of evidence from a deterministic runner. Naming the judge is what makes that difference visible."
|
|
206
223
|
}
|
|
207
|
-
}
|
|
224
|
+
},
|
|
225
|
+
"allOf": [
|
|
226
|
+
{
|
|
227
|
+
"if": {
|
|
228
|
+
"properties": {
|
|
229
|
+
"type": {
|
|
230
|
+
"const": "judgment"
|
|
231
|
+
}
|
|
232
|
+
},
|
|
233
|
+
"required": [
|
|
234
|
+
"type"
|
|
235
|
+
]
|
|
236
|
+
},
|
|
237
|
+
"then": {
|
|
238
|
+
"required": [
|
|
239
|
+
"judge"
|
|
240
|
+
],
|
|
241
|
+
"properties": {
|
|
242
|
+
"judge": {
|
|
243
|
+
"type": "string",
|
|
244
|
+
"minLength": 1,
|
|
245
|
+
"pattern": "\\S"
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
]
|
|
208
251
|
}
|
|
209
252
|
}
|
|
210
253
|
},
|
|
@@ -188,6 +188,42 @@ A REQ whose evidence lives in an unpushed commit, or in a submodule the parent
|
|
|
188
188
|
doesn't point at yet, is `partial` — the evidence is not reachable by anyone but
|
|
189
189
|
you.
|
|
190
190
|
|
|
191
|
+
### The pointer is not the path — and the pointer check alone was the whole gate
|
|
192
|
+
|
|
193
|
+
Everything above proves **commits**: the parent points at the child's newest one, every
|
|
194
|
+
repository is clean, nothing is unpushed. None of it proves anything *works* at those two
|
|
195
|
+
versions together. A parent can point at a green submodule whose contract the parent's own
|
|
196
|
+
code calls with the previous signature, and every check in this pipeline passes — the
|
|
197
|
+
child's suite ran against the child, the parent's against the parent, and **no check ran
|
|
198
|
+
across the pointer**. Neither repository looks wrong alone, which is why this survived
|
|
199
|
+
being written down twice.
|
|
200
|
+
|
|
201
|
+
So the criterion has a second half, and it fires **only where a component pointer moved in
|
|
202
|
+
the range being accepted**. A range that crossed no component boundary has no seam to
|
|
203
|
+
prove, and demanding a record for it is how a gate becomes noise.
|
|
204
|
+
|
|
205
|
+
Where one moved, the acceptance owes three things:
|
|
206
|
+
|
|
207
|
+
1. **One named cross-component path** — a command, a test, a request, a scenario that
|
|
208
|
+
traverses both sides. Not each side's suite.
|
|
209
|
+
2. **The exact versions it observed**, one per component: the commit the *parent pins*,
|
|
210
|
+
not whatever happens to be checked out. Those are the same fact only while they agree,
|
|
211
|
+
and they disagree in precisely the case this exists for.
|
|
212
|
+
3. **The observation, to the same standard a single REQ meets** — the command and what it
|
|
213
|
+
printed, in `docs/evidence/convergence.md`.
|
|
214
|
+
|
|
215
|
+
`templates/convergence.sh` mechanises the pointer half and requires the record for the
|
|
216
|
+
seam half. It also checks the one thing `git submodule status` cannot see: **whether the
|
|
217
|
+
pinned commit is published at all.** Measured here on 2026-08-16 — a release tag failed CI
|
|
218
|
+
at checkout because the parent pinned a commit that existed only on one machine, and
|
|
219
|
+
`git submodule status` showed no `+` because the pointer matched the *local* head.
|
|
220
|
+
|
|
221
|
+
| Rationalization | Why it is wrong |
|
|
222
|
+
|---|---|
|
|
223
|
+
| *"Both suites are green, so the release is green."* | Each suite ran against one side. The defect lives in the seam, and no suite has an opinion about it |
|
|
224
|
+
| *"The pointer is at the newest commit, so it is current."* | Current and *published* are different facts. A clone gets what the remote has |
|
|
225
|
+
| *"I ran the cross-component check, it passed."* | Then say which versions it observed. A record citing no version cannot say which composition it proved |
|
|
226
|
+
|
|
191
227
|
## The closing question
|
|
192
228
|
|
|
193
229
|
The table is preparation. The stage exists for the question that follows it, asked
|
|
@@ -329,7 +329,7 @@ beside the verdict**:
|
|
|
329
329
|
```
|
|
330
330
|
GATE 6 tests: PASS — full suite green (247 tests)
|
|
331
331
|
carry-over: 4 open (was 6) · unresolved: 0 · audit findings deferred: 2
|
|
332
|
-
abstained: 1 (1 cannot-verify) · unlooked: 2 dormant
|
|
332
|
+
abstained: 1 (1 cannot-verify) · unlooked: 2 dormant · holds: 1 (container: pg-test, this run)
|
|
333
333
|
```
|
|
334
334
|
|
|
335
335
|
The difference from a TODO is not bookkeeping. A TODO is invisible until somebody
|
|
@@ -70,7 +70,15 @@ often* to continue and never said *what the next item is*, so the mode could be
|
|
|
70
70
|
and still leave the run picking its next move by recollection — which is the failure
|
|
71
71
|
[`learned.md`](learned.md) rule 16 is about, running once per fire.
|
|
72
72
|
|
|
73
|
-
**
|
|
73
|
+
**Where a work graph exists it is the queue** ([`work-graph.md`](work-graph.md)):
|
|
74
|
+
`.task-pipeline/graph.json`, walked by `scripts/graph.py next`, which prints the runnable
|
|
75
|
+
nodes and nothing else. It is preferred over the two below for one measured reason — a
|
|
76
|
+
400-node graph and a 4-node graph produce the same 27-byte frontier, so the cost of knowing
|
|
77
|
+
what is next does not grow with the programme. `run.loop.queue: work-graph` records it, and
|
|
78
|
+
`next`'s exit codes are what the loop branches on: `3` is *every node is done*, `4` is
|
|
79
|
+
*what remains is blocked or parked*, and those are different endings.
|
|
80
|
+
|
|
81
|
+
**Otherwise the queue is the module map** ([`decomposition.md`](decomposition.md)) when the brief
|
|
74
82
|
was a platform, and the plan's task list otherwise. Both already exist and both are
|
|
75
83
|
already ordered; neither was ever named as the thing the loop walks.
|
|
76
84
|
|
|
@@ -89,6 +89,23 @@ side, so it never surfaces by comparison. Carry it as a named, counted set next
|
|
|
89
89
|
verdict — a ratchet, never a TODO — so `PASS` reads as *"green, and here is what nobody
|
|
90
90
|
looked at"*. → [`learned.md`](learned.md) rule 7; [`gates.md`](gates.md) → *Ratchets*.
|
|
91
91
|
|
|
92
|
+
**9a. A measured zero and an unmeasured quantity may not print the same.** Canon 9 says
|
|
93
|
+
carry the absence; this says **refuse the number** when nothing measured it. `0 of 34
|
|
94
|
+
files read` and *the recorder was never installed* are opposite facts, and a `0` claims
|
|
95
|
+
the first while meaning the second — the most reassuring answer available, derived from
|
|
96
|
+
an instrument nobody switched on. So the unmeasured case prints a word, and the word says
|
|
97
|
+
why it cannot be a number.
|
|
98
|
+
|
|
99
|
+
The rule is written down because it arrived three times under three names before anyone
|
|
100
|
+
named it: `references/knowledge-graph.md` → *State zero out loud* (a fresh graph must
|
|
101
|
+
still print `current, 0 commits behind`, or freshness is indistinguishable from a harvest
|
|
102
|
+
that never looked); `references/verification.md` → *Staleness* (`unanchored` and
|
|
103
|
+
`unresolvable` are states, not zeroes); and `scripts/graph.py` → `doctrine`, which prints
|
|
104
|
+
`unmeasured` and its reason rather than `0 of 34`, and `next`, which reports how many
|
|
105
|
+
runnable nodes declared no `touches` so that no collision is not read as no collision
|
|
106
|
+
found. Three sites, one rule, and the fourth will be written from this line rather than
|
|
107
|
+
from the same mistake.
|
|
108
|
+
|
|
92
109
|
**10. The document ships in the change that made it true.** Not in the next ticket —
|
|
93
110
|
documentation deferred is documentation that describes a system nobody is running. A
|
|
94
111
|
correction is **appended**, never edited over: a register that is rewritten loses the
|
|
@@ -22,6 +22,7 @@ elsewhere and is not restated here:
|
|
|
22
22
|
## Contents
|
|
23
23
|
|
|
24
24
|
- Axis A — the stage gate type
|
|
25
|
+
- The judgment gate — a ruling is not a measurement
|
|
25
26
|
- Axis B — the enforcement mechanism
|
|
26
27
|
- Axis C — degrees of freedom
|
|
27
28
|
- Progressive arming
|
|
@@ -49,6 +50,7 @@ From [`../pipeline.schema.json`](../pipeline.schema.json), one per stage:
|
|
|
49
50
|
| Type | Meaning | Failure to respect it |
|
|
50
51
|
|---|---|---|
|
|
51
52
|
| `auto` | the orchestrator verifies the `check` itself, pass/fail, and stops on fail | advancing on an unverified check |
|
|
53
|
+
| `judgment` | somebody **rules** on it, because no complete deterministic check exists; the gate names its `judge` | recording the ruling in the slot reserved for what a machine established |
|
|
52
54
|
| `manual` | wait for the operator's **explicit** go | treating an auto verification as the approval |
|
|
53
55
|
|
|
54
56
|
**An auto gate never substitutes for a required manual approval.** A green table is
|
|
@@ -56,6 +58,49 @@ not the operator confirming it is what they asked for, and no amount of checking
|
|
|
56
58
|
makes it one. Which stages are manual is the **operator's** decision, recorded in
|
|
57
59
|
their `pipeline.json`; the framework fixes no stage count and no gate assignment.
|
|
58
60
|
|
|
61
|
+
## The judgment gate — a ruling is not a measurement
|
|
62
|
+
|
|
63
|
+
Two types were not enough, and the gap was not cosmetic. A reviewer's ruling, a check that
|
|
64
|
+
the scenarios are coherent, a verdict that a mockup is good — none has a complete
|
|
65
|
+
deterministic check, and all three rode in `auto`, **indistinguishable from an exit code**.
|
|
66
|
+
A coverage table then cannot tell a measured row from an opinion, and the role-agent
|
|
67
|
+
programme multiplies the problem: `reviewer`, `ux`, `ui` and `market-analyst` produce
|
|
68
|
+
judgement by design.
|
|
69
|
+
|
|
70
|
+
`auto` now means only what a machine established.
|
|
71
|
+
|
|
72
|
+
**The precedent already existed in miniature, and this generalises it rather than
|
|
73
|
+
inventing it.** [`templates/verification.md`](../templates/verification.md) already turns a
|
|
74
|
+
coverage verdict of `review` into `none` in the `Auto` column — because that column records
|
|
75
|
+
what a machine established, and a review is not that. That rule, applied to one column, is
|
|
76
|
+
the `judgment` type in one instance.
|
|
77
|
+
|
|
78
|
+
Three obligations, and the third is the one that bites:
|
|
79
|
+
|
|
80
|
+
1. **It names its `judge`** — a role, an agent, a person. The schema refuses the gate
|
|
81
|
+
without one. A ruling with no author cannot be weighed for independence, and
|
|
82
|
+
independence is not a property of *having* a reviewer: this pipeline's own `R-005` reader
|
|
83
|
+
shares a model, instructions and repository with the author it reviews, differing only in
|
|
84
|
+
context. That is a real second reading and it is **not** a deterministic runner, a
|
|
85
|
+
contract at another boundary, or an external system. Naming the judge is what makes the
|
|
86
|
+
difference visible instead of assumed.
|
|
87
|
+
2. **The verdict is recorded as judgement**, in the artifact that quotes it — never
|
|
88
|
+
promoted to a pass in a column that means *a machine established this*.
|
|
89
|
+
3. **It may not stand in for a `manual` gate.** A judgement can be rendered by an agent; an
|
|
90
|
+
*authorisation* cannot. Anything outward, irreversible, or costing money stays `manual`
|
|
91
|
+
however confident the judge.
|
|
92
|
+
|
|
93
|
+
**Which of this pipeline's own gates are judgement is deliberately not decided here.**
|
|
94
|
+
Gate assignment is the operator's call and the framework fixes none — so shipping a
|
|
95
|
+
reclassified stage list would contradict the sentence above it. The type exists; the
|
|
96
|
+
project chooses where it applies.
|
|
97
|
+
|
|
98
|
+
| Rationalization | Why it is wrong |
|
|
99
|
+
|---|---|
|
|
100
|
+
| *"The reviewer approved it, so the gate passed."* | It did — as a judgement. Type it as one, or the table claims a machine agreed |
|
|
101
|
+
| *"A second agent checked it, so it is independent."* | Independence is a different **evidence path**, not a second reader. Name the judge and the difference is visible |
|
|
102
|
+
| *"There is no check for this, so it has to be `manual`."* | `manual` waits for a person's authority. `judgment` records a ruling. Collapsing them puts a human in the loop for everything that is merely hard to measure, which is how an operator learns to route around the pipeline |
|
|
103
|
+
|
|
59
104
|
## Axis B — the enforcement mechanism
|
|
60
105
|
|
|
61
106
|
Where a rule actually lives. A rule climbs this ladder; it does not start at the top.
|
|
@@ -448,7 +493,7 @@ A **ratchet** is a named, counted set that may only shrink, printed on every run
|
|
|
448
493
|
|
|
449
494
|
```
|
|
450
495
|
GATE 9 docs: PASS — propagation backlog: 121 (was 162) · unmarked residue: 0
|
|
451
|
-
abstained: 0 · unlooked: 4 (3 dormant · 1 skip — no submodules in this repo)
|
|
496
|
+
abstained: 0 · unlooked: 4 (3 dormant · 1 skip — no submodules in this repo) · holds: 0
|
|
452
497
|
```
|
|
453
498
|
|
|
454
499
|
A ratchet nobody prints is a TODO with a better name.
|
|
@@ -70,6 +70,7 @@ a row pointing outside the bundle is the defect this file exists to catch.
|
|
|
70
70
|
| Cutting a platform into modules, brick criteria, build order | `references/decomposition.md` |
|
|
71
71
|
| What a spec must lock, the UX-track order, the module dossier | `references/spec.md` |
|
|
72
72
|
| The zero-context plan format, parallel groups, set equality | `references/planning.md` |
|
|
73
|
+
| The work graph: its fields, the verbs and their exit codes, and the three invariants a schema cannot state | `references/work-graph.md`, `scripts/graph.py`, `graph.schema.json` |
|
|
73
74
|
| Workspace isolation, the subagent loop, who may write the register | `references/build.md` |
|
|
74
75
|
| The review rubric, diff packages, the three verdicts | `references/review.md` |
|
|
75
76
|
| **False success** — the class, its known shapes and its two rules | `references/gates.md` |
|
|
@@ -163,11 +163,31 @@ PROGRESS where the run stands against that request: gates passed,
|
|
|
163
163
|
DONE what was solved this iteration, each with its evidence
|
|
164
164
|
SURFACED what came up that nobody asked for — findings, corrections,
|
|
165
165
|
things that turned out to be other than assumed
|
|
166
|
+
SCOPE what this claim covers: the commit, the environment, the REQ
|
|
167
|
+
ids, the surfaces — the validity domain, not the ambition
|
|
168
|
+
NOT VERIFIED what was built and NOT checked, or could not be. The literal
|
|
169
|
+
`none within the stated scope` is a valid answer; an empty
|
|
170
|
+
field is not
|
|
166
171
|
|
|
167
172
|
DECISIONS WAITING <n> each as a question with options, asked HERE
|
|
168
173
|
AMBIGUITIES <n> computed, below
|
|
169
174
|
```
|
|
170
175
|
|
|
176
|
+
**`SCOPE` and `NOT VERIFIED` are the two the block lacked, and their absence had a
|
|
177
|
+
shape.** Every gate already computes exactly what `NOT VERIFIED` needs — `abstained` for
|
|
178
|
+
claims the run declined to make, `unlooked` for checks that did not look — and none of it
|
|
179
|
+
reached the artifact an operator actually reads. So a run could hand back a report honest
|
|
180
|
+
sentence by sentence and still be **indistinguishable from a run whose checks never
|
|
181
|
+
looked**, which is the failure this file names three separate times.
|
|
182
|
+
|
|
183
|
+
`NOT VERIFIED` is **populated from those disclosures rather than composed**: it is the
|
|
184
|
+
`abstained` and `unlooked` sets in words, plus anything built this iteration that no check
|
|
185
|
+
touched. Composing it by hand is how it becomes a summary of the parts somebody remembered.
|
|
186
|
+
|
|
187
|
+
And `none within the stated scope` is a claim with a subject — it says *nothing inside
|
|
188
|
+
what SCOPE names is unverified*. Leaving the field empty says nothing at all, which reads
|
|
189
|
+
as the same thing and is not: canon 9a, one artifact over.
|
|
190
|
+
|
|
171
191
|
**Where there is no brief** — `checkup`, `setup`, a short path — TASK quotes the
|
|
172
192
|
operator's own sentence instead, marked as such. An unquotable TASK is a run that cannot
|
|
173
193
|
say what it was asked, which is worth its own line.
|
|
@@ -338,6 +358,27 @@ of the command the project declared in `pipeline.json` → the tests stage's
|
|
|
338
358
|
`gate.command`. The rail does not read it — a glyph still comes from the verdict —
|
|
339
359
|
but the stage-7 release gate requires the claim and the observation to agree.
|
|
340
360
|
|
|
361
|
+
**The same shape, one axis over — `read:`.** Since v1.69.0 a `PostToolUse` hook on `Read`
|
|
362
|
+
appends
|
|
363
|
+
|
|
364
|
+
```
|
|
365
|
+
read: references/<file>.md
|
|
366
|
+
```
|
|
367
|
+
|
|
368
|
+
deduplicated, and always exiting 0 — a hook that can fail a `Read` breaks every turn in
|
|
369
|
+
every session. `scripts/graph.py doctrine` reports how many of the bundle's reference files
|
|
370
|
+
a run opened and lists the rest.
|
|
371
|
+
|
|
372
|
+
Why it is hook-written is the same reason as above: a claim about what somebody read,
|
|
373
|
+
written by the party the claim is about, is not evidence. And why the verb prints
|
|
374
|
+
`unmeasured` rather than `0` when there are no such lines is the same reason again — the
|
|
375
|
+
hook being absent and the run reading nothing are **opposite facts** the ledger cannot
|
|
376
|
+
separate, so it claims neither. A `0` there would be the reassuring answer to a question
|
|
377
|
+
nobody asked, over 34 files nobody checked.
|
|
378
|
+
|
|
379
|
+
It is a disclosure: no floor, no direction, never a target. The moment the number becomes
|
|
380
|
+
something to raise, a run will open files to raise it.
|
|
381
|
+
|
|
341
382
|
The reason is the one this whole file is about, arriving one level down. A rail
|
|
342
383
|
written from memory is a summary that is confidently wrong exactly when it matters;
|
|
343
384
|
a gate that reads a verdict typed by the agent it constrains is the same shape
|
|
@@ -262,6 +262,14 @@ never that the work was skipped quietly.
|
|
|
262
262
|
its status, in build order with the walking skeleton first. Single-module work
|
|
263
263
|
records `single module: <name>` in the design and moves on — a skipped
|
|
264
264
|
decomposition is a decision, never an omission.
|
|
265
|
+
- **Where the queue is a work graph, it is WRITTEN here**
|
|
266
|
+
([`work-graph.md`](work-graph.md)): `.task-pipeline/graph.json`, carrying the frozen REQ
|
|
267
|
+
ids so `serves` resolves, one node per unit of work with its owner and what it touches,
|
|
268
|
+
and an edge per dependency **naming what it hands over**. Then
|
|
269
|
+
`python3 scripts/graph.py validate` — a graph that does not validate is not a queue, and
|
|
270
|
+
`next` refuses to walk one. The reason to prefer it over a prose plan is measured, not
|
|
271
|
+
aesthetic: a 400-node graph and a 4-node graph produce the same 27-byte frontier, so the
|
|
272
|
+
cost of knowing what is next does not grow with the programme.
|
|
265
273
|
- **The queue exists here, so the loop arms here** ([`continuity.md`](continuity.md) →
|
|
266
274
|
*Part 1a*). Where `run.loop.arm` is `after-decomposition` and the map holds more than
|
|
267
275
|
one module, arm the mode at the close of this stage and print one line: the mode, and
|
|
@@ -269,7 +277,9 @@ never that the work was skipped quietly.
|
|
|
269
277
|
(`dynamic`). Arming collapses no gate and authorizes no outward act; it decides only
|
|
270
278
|
that the run does not stop to ask *"shall I take the next one?"*. Single-module work
|
|
271
279
|
arms nothing and says so — a loop with one item is a timer.
|
|
272
|
-
- **GATE (manual):** the user approves the design, the UI verdict is recorded,
|
|
280
|
+
- **GATE (manual):** the user approves the design, the UI verdict is recorded, **the
|
|
281
|
+
queue is an artifact rather than a recollection** — where it is a work graph,
|
|
282
|
+
`graph.py validate` exits 0 and `graph.py coverage` names any requirement no node serves,
|
|
273
283
|
**every REQ is answered by the design** — a requirement the design doesn't
|
|
274
284
|
address is either covered now or explicitly dropped by the operator, with the
|
|
275
285
|
drop recorded in the carry-over ledger — **and, for a platform, the module map is
|
|
@@ -22,6 +22,7 @@ the run*. Three things it does not say, and each is why this file exists:
|
|
|
22
22
|
|
|
23
23
|
- Why it keys to the brief, not to the coverage table
|
|
24
24
|
- `never` is a fact
|
|
25
|
+
- Staleness — the other end of `never`
|
|
25
26
|
- A ledger records two different things, and most record only one
|
|
26
27
|
- What stage 8 writes and what stage 10 refuses
|
|
27
28
|
- Rationalizations
|
|
@@ -54,6 +55,57 @@ So the count has **no floor, no direction, and may never be given a target**, ex
|
|
|
54
55
|
like the disclosures in [`gates.md`](gates.md). A project with forty `never` rows is not
|
|
55
56
|
failing; it is a project that now knows something it could not previously ask.
|
|
56
57
|
|
|
58
|
+
## Staleness — the other end of `never`
|
|
59
|
+
|
|
60
|
+
This file tracked rows nobody had ever confirmed and had **no notion of a row whose
|
|
61
|
+
confirmation the tree has overtaken.** A row verified at commit A read `verified` after
|
|
62
|
+
commit B, forever. Those are the same failure from two ends, and only one end was
|
|
63
|
+
instrumented — which is why a ledger could read fully green over a tree where every check
|
|
64
|
+
ran against code that has since moved.
|
|
65
|
+
|
|
66
|
+
`Observed at` is the commit the check ran against. `scripts/exposure.sh` reports four
|
|
67
|
+
states from it, and the whole thing is a **port**: `references/knowledge-graph.md` already
|
|
68
|
+
gives the code graph a stamp, a distance and three states, and every non-current one ends
|
|
69
|
+
in a marker. The same contract, applied to the ledger.
|
|
70
|
+
|
|
71
|
+
| State | Condition | Reads |
|
|
72
|
+
|---|---|---|
|
|
73
|
+
| **current** | 0 commits behind `HEAD` | still speaks about this tree |
|
|
74
|
+
| **behind** | N commits / M days behind | *not trusted for the current tree until re-observed* |
|
|
75
|
+
| **unresolvable** | the commit does not resolve here | rebase, squash, shallow clone — same marker |
|
|
76
|
+
| **unanchored** | no commit recorded | nothing can say what it saw |
|
|
77
|
+
|
|
78
|
+
**It is a disclosure: no floor, no direction, never a target** — the same contract the
|
|
79
|
+
`Human` column has, and for the same reason. A threshold on staleness teaches a run to
|
|
80
|
+
re-observe rows for the counter rather than for the question.
|
|
81
|
+
|
|
82
|
+
**`behind` means unproven for this tree, never wrong.** The section knows the distance and
|
|
83
|
+
does not know whether the commits in between touched anything the row covers. Claiming
|
|
84
|
+
more than that would be the estimate-printed-as-measurement this pipeline refuses
|
|
85
|
+
elsewhere.
|
|
86
|
+
|
|
87
|
+
**State zero prints out loud.** `current 12 · behind 0 · unresolvable 0 · unanchored 0` is
|
|
88
|
+
a measurement; printing nothing when everything is fresh makes freshness indistinguishable
|
|
89
|
+
from a check that never looked.
|
|
90
|
+
|
|
91
|
+
**Invalidation is not deletion.** An overtaken row is not wrong — it is true about the tree
|
|
92
|
+
it observed, and it stays. Re-observing **appends** a row; it never edits the old one. Four
|
|
93
|
+
things overtake a row, and the note says which applies: a **code** change in what it
|
|
94
|
+
covers, a **dependency** change, an **environment** change, and a **policy** change — the
|
|
95
|
+
rule under which the evidence was accepted.
|
|
96
|
+
|
|
97
|
+
**Where the section prints matters as much as what it prints.** It runs *before* the
|
|
98
|
+
check-list, because `exposure.sh` exits early when nothing is unverified — and that is
|
|
99
|
+
exactly the state where these counts matter most. `0 unverified` is the sentence most
|
|
100
|
+
likely to be read as *nothing to look at*, and a staleness section invisible behind it
|
|
101
|
+
would have been dormant precisely when it was needed.
|
|
102
|
+
|
|
103
|
+
| Rationalization | Why it is wrong |
|
|
104
|
+
|---|---|
|
|
105
|
+
| *"The row says `pass`, so it passes."* | It passed against a tree. Name which one, or the claim has no subject |
|
|
106
|
+
| *"Everything is behind, so the number is noise."* | Then it is a large honest number. A threshold would make it a small dishonest one |
|
|
107
|
+
| *"I re-ran the check, so I will update the row."* | Append. The old row is true about what it saw, and overwriting it destroys the only record of when the claim was earned |
|
|
108
|
+
|
|
57
109
|
## A ledger records two different things, and most record only one
|
|
58
110
|
|
|
59
111
|
**What confirmed it** and **whether a person looked** are separate facts. The first is
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
# The work graph — the queue a script walks so the model never reads it
|
|
2
|
+
|
|
3
|
+
**One job: hold what is left to do, in a form whose cost does not grow with the
|
|
4
|
+
programme.**
|
|
5
|
+
|
|
6
|
+
A plan is prose. A prose plan for a real release is hundreds of lines, and a model that
|
|
7
|
+
re-reads it every iteration spends its context on ground it has already walked — so the
|
|
8
|
+
cost of knowing *what is next* rises with the size of the work, which is exactly backwards.
|
|
9
|
+
|
|
10
|
+
`.task-pipeline/graph.json` is the same information as a typed graph, and
|
|
11
|
+
[`scripts/graph.py`](../scripts/graph.py) answers one question against it: **which nodes are
|
|
12
|
+
runnable right now.** What enters a context each iteration is then bounded by the
|
|
13
|
+
**frontier's width**, not by the graph's size.
|
|
14
|
+
|
|
15
|
+
Measured on this build: a 400-node graph (51 KB) and a 4-node graph produce the same
|
|
16
|
+
**27-byte** frontier. That property is the whole reason the file exists, and it is why
|
|
17
|
+
`next` prints the frontier and nothing else — anything else printed there is paid on every
|
|
18
|
+
turn of every loop.
|
|
19
|
+
|
|
20
|
+
---
|
|
21
|
+
|
|
22
|
+
## Contents
|
|
23
|
+
|
|
24
|
+
- What is in it, and what each field is for
|
|
25
|
+
- The verbs, and their exit codes
|
|
26
|
+
- The three things a schema cannot state
|
|
27
|
+
- What it deliberately does NOT do
|
|
28
|
+
- Rationalizations
|
|
29
|
+
|
|
30
|
+
## What is in it, and what each field is for
|
|
31
|
+
|
|
32
|
+
`graph.schema.json` is the contract; this is why each part is there.
|
|
33
|
+
|
|
34
|
+
| Field | Why |
|
|
35
|
+
|---|---|
|
|
36
|
+
| `goal` | echoed from `pipeline.json` → `release.goal`, so the graph carries the thing it serves rather than pointing at a file that may have moved on |
|
|
37
|
+
| `requirements` | the REQ ids the brief froze. **`serves` resolves against these** — without them it was a non-empty string and nothing more, and that field is the one edge joining the intent graph to this one |
|
|
38
|
+
| `goal_clauses` | release work no requirement names. Enumerated, never matched against the goal's prose: substring-matching a sentence produces confidence without correctness |
|
|
39
|
+
| `nodes[].owner` | which role does it. A node nobody can dispatch never leaves the frontier and nothing says why |
|
|
40
|
+
| `nodes[].serves` | the REQ or goal clause it exists for. A node serving neither is **parked with that as the reason** |
|
|
41
|
+
| `nodes[].blocked_by` | what must close first. This is what the frontier obeys |
|
|
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[].evidence` | required when `status` is `done`. A node called done by assertion is what evidence exists to prevent |
|
|
44
|
+
| `nodes[].parked_reason` | required when `status` is `parked`. A park with no reason is indistinguishable, a week later, from work quietly dropped |
|
|
45
|
+
| `edges[].payload` | what the dependency hands over. **An edge carrying no named artifact is chronology drawn as architecture** |
|
|
46
|
+
| `revisions` | why the graph is not the graph stage 2 wrote. A graph that changed for reasons nobody recorded can explain its own completion by appealing to a plan that existed only at the end |
|
|
47
|
+
|
|
48
|
+
## The verbs, and their exit codes
|
|
49
|
+
|
|
50
|
+
Exit codes are the contract, per standing instruction `R-004`: **the next command is
|
|
51
|
+
conditional on the code, never merely sequenced after it.**
|
|
52
|
+
|
|
53
|
+
| Verb | Prints | Codes |
|
|
54
|
+
|---|---|---|
|
|
55
|
+
| `validate` | every violation, in a stable order | `0` clean · `1` any |
|
|
56
|
+
| `next` | the frontier, ordered by how much each node unblocks — **and nothing else** | `0` printed · `3` all done · `4` nothing runnable |
|
|
57
|
+
| `coverage` | each requirement with the nodes serving it | `0` covered · `1` a gap, named |
|
|
58
|
+
| `goal` | the release goal | `0` · `3` unstated |
|
|
59
|
+
| `add` | the id it allocated | `0` · `1` refused |
|
|
60
|
+
| `park` | the id and the reason | `0` · `1` refused |
|
|
61
|
+
| `close` | the goal, the new frontier count, and what was not verified | `0` · `1` refused **or the verdict stops the run** |
|
|
62
|
+
| `producer` | what produced this proof — actor, model, runtime, skill, config, commit, trace | `0` |
|
|
63
|
+
| `doctrine` | how many of the bundle's reference files this run opened | `0` |
|
|
64
|
+
|
|
65
|
+
**`next` is ordered by what each node unblocks, transitively, and the number is computed.**
|
|
66
|
+
A `priority` field is something somebody typed once and nobody revisits; this one moves when
|
|
67
|
+
the graph does. Ties break on declaration order, so the frontier is stable between runs —
|
|
68
|
+
an unstable one costs more than it looks, because an agent calling `next` twice starts the
|
|
69
|
+
other node.
|
|
70
|
+
|
|
71
|
+
**`close` stamps the commit; the verifier never supplies it.** A verdict written after the
|
|
72
|
+
tree moved is evidence about a different tree, and an agent cannot name the wrong commit if
|
|
73
|
+
it is never the one naming one.
|
|
74
|
+
|
|
75
|
+
**A stop closes its node and refuses the next step.** `replan.possible: false` says the run
|
|
76
|
+
cannot continue *around* what it found — not that the work just verified did not happen.
|
|
77
|
+
|
|
78
|
+
## The three things a schema cannot state
|
|
79
|
+
|
|
80
|
+
The split is where the format actually puts it, which is not where the first draft drew it.
|
|
81
|
+
`graph.schema.json` states everything JSON Schema can, including `done → evidence` and
|
|
82
|
+
`parked → parked_reason` — the first version claimed those were beyond it and was wrong.
|
|
83
|
+
|
|
84
|
+
What genuinely cannot be expressed is cross-document and cross-node, and lives in
|
|
85
|
+
`violations()`:
|
|
86
|
+
|
|
87
|
+
1. **Whether `owner` names a role that exists** — with a near-miss hint, because an agent
|
|
88
|
+
told only *no* will try a synonym.
|
|
89
|
+
2. **Whether `serves` resolves**, and whether every `blocked_by` and edge end is in the
|
|
90
|
+
graph at all.
|
|
91
|
+
3. **Whether the edges cycle** — the one failure of this design that looks exactly like slow
|
|
92
|
+
progress: a frontier can be non-empty forever while nothing is runnable.
|
|
93
|
+
|
|
94
|
+
And one more that is not about expressiveness: **the schema is never applied to a live
|
|
95
|
+
graph.** `graph.py` is stdlib-only by design, so `violations()` also enforces the rules the
|
|
96
|
+
format states — because a rule checked only against the shipped example is a rule the run
|
|
97
|
+
does not have.
|
|
98
|
+
|
|
99
|
+
## What it deliberately does NOT do
|
|
100
|
+
|
|
101
|
+
- **It is not committed.** `.task-pipeline/graph.json` is a run artifact. That is a live
|
|
102
|
+
tension, not a settled question: a graph deleted with its run cannot be inspected
|
|
103
|
+
afterwards, and `docs/evidence/backlog.md` carries it rather than this file pretending it
|
|
104
|
+
is resolved.
|
|
105
|
+
- **It does not decide the module map.** Stage 2 does ([`decomposition.md`](decomposition.md));
|
|
106
|
+
the graph is where that decision becomes walkable.
|
|
107
|
+
- **It does not schedule.** `next` says what *may* run. Whether two runnable nodes are
|
|
108
|
+
dispatched together is the dispatcher's call, and the collision warning on stderr is what
|
|
109
|
+
that call is made against.
|
|
110
|
+
- **It does not read like a plan.** If you want to know why the work is shaped this way, the
|
|
111
|
+
brief and the spec say so. The graph says what is left.
|
|
112
|
+
|
|
113
|
+
## Rationalizations
|
|
114
|
+
|
|
115
|
+
| The excuse | Why it is wrong |
|
|
116
|
+
|---|---|
|
|
117
|
+
| *"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
|
+
| *"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
|
+
| *"`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 |
|
|
120
|
+
| *"I'll add the node now and record why later."* | Later is the run that cannot say why its plan changed, which is the run whose completion claim cannot be falsified |
|
|
121
|
+
| *"The graph validates, so the mutation was fine."* | `validate` is stdlib and the format is richer. Both were made to agree once, and the disagreement returned within a day — check the two against each other, not the result against one |
|