task-pipeline-skill 1.67.0 → 1.69.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (30) hide show
  1. package/CHANGELOG.md +856 -0
  2. package/README.md +25 -0
  3. package/SKILL-CARD.md +1 -1
  4. package/bin/task-pipeline.js +30 -0
  5. package/package.json +4 -3
  6. package/plugins/task-pipeline/.claude-plugin/plugin.json +1 -1
  7. package/plugins/task-pipeline/agents/verifier.md +88 -0
  8. package/plugins/task-pipeline/commands/task-pipeline.md +22 -0
  9. package/plugins/task-pipeline/skills/task-pipeline/SKILL.md +84 -136
  10. package/plugins/task-pipeline/skills/task-pipeline/graph.example.json +73 -0
  11. package/plugins/task-pipeline/skills/task-pipeline/graph.schema.json +253 -0
  12. package/plugins/task-pipeline/skills/task-pipeline/pipeline.schema.json +46 -3
  13. package/plugins/task-pipeline/skills/task-pipeline/references/acceptance.md +69 -0
  14. package/plugins/task-pipeline/skills/task-pipeline/references/audit.md +1 -1
  15. package/plugins/task-pipeline/skills/task-pipeline/references/continuity.md +9 -1
  16. package/plugins/task-pipeline/skills/task-pipeline/references/documentation.md +17 -0
  17. package/plugins/task-pipeline/skills/task-pipeline/references/gates.md +91 -1
  18. package/plugins/task-pipeline/skills/task-pipeline/references/portability.md +1 -0
  19. package/plugins/task-pipeline/skills/task-pipeline/references/progress.md +41 -0
  20. package/plugins/task-pipeline/skills/task-pipeline/references/stages.md +11 -1
  21. package/plugins/task-pipeline/skills/task-pipeline/references/verification.md +52 -0
  22. package/plugins/task-pipeline/skills/task-pipeline/references/work-graph.md +121 -0
  23. package/plugins/task-pipeline/skills/task-pipeline/scripts/graph.py +1113 -0
  24. package/plugins/task-pipeline/skills/task-pipeline/templates/README.md +1 -0
  25. package/plugins/task-pipeline/skills/task-pipeline/templates/carryover.md +1 -1
  26. package/plugins/task-pipeline/skills/task-pipeline/templates/convergence.sh +146 -0
  27. package/plugins/task-pipeline/skills/task-pipeline/templates/exposure.sh +104 -1
  28. package/plugins/task-pipeline/skills/task-pipeline/templates/hooks.example.json +13 -1
  29. package/plugins/task-pipeline/skills/task-pipeline/templates/run.md +32 -0
  30. package/plugins/task-pipeline/skills/task-pipeline/templates/verification.md +67 -5
@@ -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
@@ -40,6 +41,7 @@ elsewhere and is not restated here:
40
41
  - Where a gate runs
41
42
  - Adding a check to an existing gate
42
43
  - Rationalizations
44
+ - Cross-cutting, at every stage
43
45
 
44
46
  ## Axis A — the stage gate type
45
47
 
@@ -48,6 +50,7 @@ From [`../pipeline.schema.json`](../pipeline.schema.json), one per stage:
48
50
  | Type | Meaning | Failure to respect it |
49
51
  |---|---|---|
50
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 |
51
54
  | `manual` | wait for the operator's **explicit** go | treating an auto verification as the approval |
52
55
 
53
56
  **An auto gate never substitutes for a required manual approval.** A green table is
@@ -55,6 +58,49 @@ not the operator confirming it is what they asked for, and no amount of checking
55
58
  makes it one. Which stages are manual is the **operator's** decision, recorded in
56
59
  their `pipeline.json`; the framework fixes no stage count and no gate assignment.
57
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
+
58
104
  ## Axis B — the enforcement mechanism
59
105
 
60
106
  Where a rule actually lives. A rule climbs this ladder; it does not start at the top.
@@ -447,7 +493,7 @@ A **ratchet** is a named, counted set that may only shrink, printed on every run
447
493
 
448
494
  ```
449
495
  GATE 9 docs: PASS — propagation backlog: 121 (was 162) · unmarked residue: 0
450
- 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
451
497
  ```
452
498
 
453
499
  A ratchet nobody prints is a TODO with a better name.
@@ -589,3 +635,47 @@ of is deleted in the next refactor by someone who assumed it was dead.
589
635
  | "The gate would be red on day one, so I'll add it later" | Make the section dormant instead. Dormant is visible and green; "later" is neither. |
590
636
  | "I raised the floor to get the build green" | Then say so in the log, in the same commit. A floor raised silently is a ratchet running backwards. |
591
637
  | "A hook is overkill, CI catches it" | CI catches it after the edit, the commit and the push. If the point is to stop the edit, CI is the wrong rung — and if it is not, do not pay the latency. |
638
+
639
+ ---
640
+
641
+ ## Cross-cutting, at every stage
642
+
643
+ 5. Cross-cutting, every stage: **when anything is settled — scope, a contract, a
644
+ name, a policy, a vocabulary — run the Doc Loop
645
+ (`references/documentation.md`) before the run moves on**: reserve the id,
646
+ record it, resolve the question it answers, propagate by the matrix, commit
647
+ with the ids. A decision that lives only in the spec dies with the spec, and one
648
+ that lives only in the conversation was never made;
649
+ **answer from the brief's autonomy section rather
650
+ than asking again** — it was grilled precisely so you wouldn't have to;
651
+ **anything deferred, dropped or left half-done goes into the carry-over ledger
652
+ the moment it's said** — deferred out loud is forgotten; **never narrow the task
653
+ silently** — the REQ list is frozen, adding is free, removing needs the
654
+ operator's explicit agreement; **when a loop starts undoing an earlier pass —
655
+ the same file edited twice for the same reason, a closed finding coming back, a
656
+ third entry into one stage — stop and run the loop guard**
657
+ (`references/loop-guard.md`): name the two shapes, escalate to the layer that
658
+ owns the conflict, re-plan the check as an ordered list, then go through it one
659
+ item at a time; **when a pass is *searching* rather than editing and starts
660
+ finding mostly what the previous pass's own fixes broke, the axis is exhausted —
661
+ rotate it, don't look harder** (`references/audit.md`); **every gate
662
+ prints `holds: N` — what this run left running** across the eight classes
663
+ (background shells, monitors, scheduled loops, coordination leases, worktrees,
664
+ containers, scratch files, remote state), enumerated **by class and never by a
665
+ single tool**, and stage 10 does not close while this run's residue is live and
666
+ unaccounted (`references/residue.md`); and remember that a
667
+ green from a check nobody has watched fail is not evidence; task
668
+ tracker + conventional commits per host conventions; worktree isolation for the
669
+ build, integrated back per the brief's branch policy before stage 7; honest
670
+ degradation (never claim a failed/skipped step succeeded);
671
+ outward/irreversible actions (deploy, publish, repo create, opening a PR,
672
+ **editing a shared design file — frames are read by designers and stakeholders,
673
+ so drawing in one is publishing — and above all *creating* one, which needs a
674
+ named team and never happens while a recorded file resolves**) need explicit
675
+ operator go — or a **specific** standing authorization recorded in the brief
676
+ (named target + preconditions; a vague "do everything" is not one).
677
+
678
+ Moved out of `SKILL.md` on 2026-08-16 for the same budget reason as the
679
+ multi-repository block: these fire at any stage, so they belong with the gate
680
+ doctrine rather than inside step 5 of the run order.
681
+
@@ -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 |