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.
Files changed (30) hide show
  1. package/CHANGELOG.md +799 -0
  2. package/README.md +25 -0
  3. package/SKILL-CARD.md +1 -1
  4. package/bin/task-pipeline.js +30 -0
  5. package/package.json +4 -3
  6. package/plugins/task-pipeline/.claude-plugin/plugin.json +1 -1
  7. package/plugins/task-pipeline/agents/verifier.md +88 -0
  8. package/plugins/task-pipeline/commands/task-pipeline.md +22 -0
  9. package/plugins/task-pipeline/skills/task-pipeline/SKILL.md +2 -1
  10. package/plugins/task-pipeline/skills/task-pipeline/graph.example.json +73 -0
  11. package/plugins/task-pipeline/skills/task-pipeline/graph.schema.json +253 -0
  12. package/plugins/task-pipeline/skills/task-pipeline/pipeline.schema.json +46 -3
  13. package/plugins/task-pipeline/skills/task-pipeline/references/acceptance.md +36 -0
  14. package/plugins/task-pipeline/skills/task-pipeline/references/audit.md +1 -1
  15. package/plugins/task-pipeline/skills/task-pipeline/references/continuity.md +9 -1
  16. package/plugins/task-pipeline/skills/task-pipeline/references/documentation.md +17 -0
  17. package/plugins/task-pipeline/skills/task-pipeline/references/gates.md +46 -1
  18. package/plugins/task-pipeline/skills/task-pipeline/references/portability.md +1 -0
  19. package/plugins/task-pipeline/skills/task-pipeline/references/progress.md +41 -0
  20. package/plugins/task-pipeline/skills/task-pipeline/references/stages.md +11 -1
  21. package/plugins/task-pipeline/skills/task-pipeline/references/verification.md +52 -0
  22. package/plugins/task-pipeline/skills/task-pipeline/references/work-graph.md +121 -0
  23. package/plugins/task-pipeline/skills/task-pipeline/scripts/graph.py +1113 -0
  24. package/plugins/task-pipeline/skills/task-pipeline/templates/README.md +1 -0
  25. package/plugins/task-pipeline/skills/task-pipeline/templates/carryover.md +1 -1
  26. package/plugins/task-pipeline/skills/task-pipeline/templates/convergence.sh +146 -0
  27. package/plugins/task-pipeline/skills/task-pipeline/templates/exposure.sh +104 -1
  28. package/plugins/task-pipeline/skills/task-pipeline/templates/hooks.example.json +13 -1
  29. package/plugins/task-pipeline/skills/task-pipeline/templates/run.md +32 -0
  30. package/plugins/task-pipeline/skills/task-pipeline/templates/verification.md +67 -5
@@ -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 (pass/fail); manual = wait for the operator's explicit go."
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
- **The queue is the module map** ([`decomposition.md`](decomposition.md)) when the brief
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 |