task-pipeline-skill 1.67.0 → 1.69.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +856 -0
- package/README.md +25 -0
- package/SKILL-CARD.md +1 -1
- package/bin/task-pipeline.js +30 -0
- package/package.json +4 -3
- package/plugins/task-pipeline/.claude-plugin/plugin.json +1 -1
- package/plugins/task-pipeline/agents/verifier.md +88 -0
- package/plugins/task-pipeline/commands/task-pipeline.md +22 -0
- package/plugins/task-pipeline/skills/task-pipeline/SKILL.md +84 -136
- package/plugins/task-pipeline/skills/task-pipeline/graph.example.json +73 -0
- package/plugins/task-pipeline/skills/task-pipeline/graph.schema.json +253 -0
- package/plugins/task-pipeline/skills/task-pipeline/pipeline.schema.json +46 -3
- package/plugins/task-pipeline/skills/task-pipeline/references/acceptance.md +69 -0
- package/plugins/task-pipeline/skills/task-pipeline/references/audit.md +1 -1
- package/plugins/task-pipeline/skills/task-pipeline/references/continuity.md +9 -1
- package/plugins/task-pipeline/skills/task-pipeline/references/documentation.md +17 -0
- package/plugins/task-pipeline/skills/task-pipeline/references/gates.md +91 -1
- package/plugins/task-pipeline/skills/task-pipeline/references/portability.md +1 -0
- package/plugins/task-pipeline/skills/task-pipeline/references/progress.md +41 -0
- package/plugins/task-pipeline/skills/task-pipeline/references/stages.md +11 -1
- package/plugins/task-pipeline/skills/task-pipeline/references/verification.md +52 -0
- package/plugins/task-pipeline/skills/task-pipeline/references/work-graph.md +121 -0
- package/plugins/task-pipeline/skills/task-pipeline/scripts/graph.py +1113 -0
- package/plugins/task-pipeline/skills/task-pipeline/templates/README.md +1 -0
- package/plugins/task-pipeline/skills/task-pipeline/templates/carryover.md +1 -1
- package/plugins/task-pipeline/skills/task-pipeline/templates/convergence.sh +146 -0
- package/plugins/task-pipeline/skills/task-pipeline/templates/exposure.sh +104 -1
- package/plugins/task-pipeline/skills/task-pipeline/templates/hooks.example.json +13 -1
- package/plugins/task-pipeline/skills/task-pipeline/templates/run.md +32 -0
- package/plugins/task-pipeline/skills/task-pipeline/templates/verification.md +67 -5
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
{
|
|
2
|
+
"goal": "The pipeline advances a queue without a human between iterations, and says what it did.",
|
|
3
|
+
"requirements": [
|
|
4
|
+
"REQ-001",
|
|
5
|
+
"REQ-002",
|
|
6
|
+
"REQ-005",
|
|
7
|
+
"REQ-016"
|
|
8
|
+
],
|
|
9
|
+
"nodes": [
|
|
10
|
+
{
|
|
11
|
+
"id": "N-001",
|
|
12
|
+
"title": "The graph schema",
|
|
13
|
+
"owner": "implementer",
|
|
14
|
+
"status": "done",
|
|
15
|
+
"blocked_by": [],
|
|
16
|
+
"serves": "REQ-001",
|
|
17
|
+
"evidence": [
|
|
18
|
+
"npm test → PASS: task-pipeline structure valid, with graph.example.json validated against graph.schema.json"
|
|
19
|
+
]
|
|
20
|
+
},
|
|
21
|
+
{
|
|
22
|
+
"id": "N-002",
|
|
23
|
+
"title": "graph.py validate — the invariants a schema cannot state",
|
|
24
|
+
"owner": "implementer",
|
|
25
|
+
"status": "running",
|
|
26
|
+
"blocked_by": [
|
|
27
|
+
"N-001"
|
|
28
|
+
],
|
|
29
|
+
"serves": "REQ-002",
|
|
30
|
+
"evidence": null
|
|
31
|
+
},
|
|
32
|
+
{
|
|
33
|
+
"id": "N-003",
|
|
34
|
+
"title": "The verifier agent and its six-key verdict",
|
|
35
|
+
"owner": "verifier",
|
|
36
|
+
"status": "pending",
|
|
37
|
+
"blocked_by": [
|
|
38
|
+
"N-001"
|
|
39
|
+
],
|
|
40
|
+
"serves": "REQ-005",
|
|
41
|
+
"evidence": null
|
|
42
|
+
},
|
|
43
|
+
{
|
|
44
|
+
"id": "N-004",
|
|
45
|
+
"title": "Mockups for the run view, before any markup",
|
|
46
|
+
"owner": "ui",
|
|
47
|
+
"status": "parked",
|
|
48
|
+
"blocked_by": [],
|
|
49
|
+
"serves": "REQ-016",
|
|
50
|
+
"evidence": null,
|
|
51
|
+
"parked_reason": "Serves module 2, not this release's goal. Parked rather than dropped quietly — the reason is the point."
|
|
52
|
+
}
|
|
53
|
+
],
|
|
54
|
+
"edges": [
|
|
55
|
+
{
|
|
56
|
+
"from": "N-001",
|
|
57
|
+
"to": "N-002",
|
|
58
|
+
"payload": "the schema validate validates against"
|
|
59
|
+
},
|
|
60
|
+
{
|
|
61
|
+
"from": "N-001",
|
|
62
|
+
"to": "N-003",
|
|
63
|
+
"payload": "the node shape the verdict's `node` field references"
|
|
64
|
+
}
|
|
65
|
+
],
|
|
66
|
+
"revisions": [
|
|
67
|
+
{
|
|
68
|
+
"verb": "park",
|
|
69
|
+
"node": "N-004",
|
|
70
|
+
"why": "The verifier's re-plan: this serves module 2, and module 1's goal clause does not reach it. Parked rather than dropped so the decision survives."
|
|
71
|
+
}
|
|
72
|
+
]
|
|
73
|
+
}
|
|
@@ -0,0 +1,253 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "http://json-schema.org/draft-07/schema#",
|
|
3
|
+
"$id": "https://raw.githubusercontent.com/ssheleg/task-pipeline/main/plugins/task-pipeline/skills/task-pipeline/graph.schema.json",
|
|
4
|
+
"title": "task-pipeline work graph",
|
|
5
|
+
"description": "The queue the loop walks, written at stage 2 and re-planned by the verifier at every close. It lives at `.task-pipeline/graph.json` — a RUN artifact, never shipped, never committed by the skill. What ships is this schema and one example.\n\nThe design rule this file exists to serve: the model never reads the graph. `scripts/graph.py next` computes the frontier and prints it, so what enters a context each iteration is bounded by the frontier's width rather than by the programme's size. A graph of four hundred nodes and a graph of four cost the same to walk.\n\nOnly the invariants a schema CAN state live here — and the first draft of this file drew that line in the wrong place. It claimed `done` implying evidence was beyond JSON Schema; draft-07 `if`/`then` states it exactly, and it is stated below. What genuinely cannot be expressed is cross-DOCUMENT: whether an `owner` names a role that exists, whether `serves` resolves to a real REQ, and whether the edges form a cycle. Those are `graph.py validate`'s, and the split is now where the format actually puts it.",
|
|
6
|
+
"type": "object",
|
|
7
|
+
"additionalProperties": false,
|
|
8
|
+
"required": [
|
|
9
|
+
"goal",
|
|
10
|
+
"requirements",
|
|
11
|
+
"nodes",
|
|
12
|
+
"edges"
|
|
13
|
+
],
|
|
14
|
+
"properties": {
|
|
15
|
+
"goal": {
|
|
16
|
+
"type": "string",
|
|
17
|
+
"minLength": 1,
|
|
18
|
+
"description": "Echoed from the project's `pipeline.json` → `release.goal` when the graph is created, so the graph carries the thing it serves rather than pointing at a file that may have moved on. Every loop iteration prints it above the frontier: drift from the goal becomes visible rather than remembered."
|
|
19
|
+
},
|
|
20
|
+
"nodes": {
|
|
21
|
+
"type": "array",
|
|
22
|
+
"items": {
|
|
23
|
+
"$ref": "#/definitions/node"
|
|
24
|
+
},
|
|
25
|
+
"description": "Units of work. A node is what one role does in one dispatch and what one verdict closes."
|
|
26
|
+
},
|
|
27
|
+
"edges": {
|
|
28
|
+
"type": "array",
|
|
29
|
+
"items": {
|
|
30
|
+
"$ref": "#/definitions/edge"
|
|
31
|
+
},
|
|
32
|
+
"description": "Dependencies, each carrying what it hands over."
|
|
33
|
+
},
|
|
34
|
+
"revisions": {
|
|
35
|
+
"type": "array",
|
|
36
|
+
"items": {
|
|
37
|
+
"$ref": "#/definitions/revision"
|
|
38
|
+
},
|
|
39
|
+
"description": "Why the graph is not the graph stage 2 wrote. Every mutation appends one entry, and both verbs refuse without a reason — `park` always did, `add` did not, which left half the revision surface silent. A graph that changed for reasons nobody recorded can always explain its own completion by appealing to a plan that existed only at the end; this log is what makes that checkable. `next` never prints it — the frontier's width is what a loop pays for on every iteration, and this grows."
|
|
40
|
+
},
|
|
41
|
+
"requirements": {
|
|
42
|
+
"type": "array",
|
|
43
|
+
"minItems": 1,
|
|
44
|
+
"uniqueItems": true,
|
|
45
|
+
"items": {
|
|
46
|
+
"type": "string",
|
|
47
|
+
"pattern": "^REQ-[0-9]{3,}$"
|
|
48
|
+
},
|
|
49
|
+
"description": "The REQ ids the brief froze at stage 0, copied here so `serves` has something to resolve against. Until this existed, `serves` was a non-empty string and nothing more — `serves: \"REQ-999\"` and `serves: \"asdf\"` passed every gate identically, and that field is the ONE edge joining the intent graph to the execution graph. Required and non-empty: a graph whose intent side is empty cannot be checked against anything, and a queue that serves nothing is not a plan."
|
|
50
|
+
},
|
|
51
|
+
"goal_clauses": {
|
|
52
|
+
"type": "array",
|
|
53
|
+
"uniqueItems": true,
|
|
54
|
+
"items": {
|
|
55
|
+
"type": "string",
|
|
56
|
+
"minLength": 1,
|
|
57
|
+
"pattern": "\\S"
|
|
58
|
+
},
|
|
59
|
+
"description": "Clauses of the release goal a node may serve instead of a REQ — release work that no requirement names. Enumerated rather than matched against the goal's prose, because substring-matching a sentence is the kind of check that produces confidence without correctness."
|
|
60
|
+
}
|
|
61
|
+
},
|
|
62
|
+
"definitions": {
|
|
63
|
+
"node": {
|
|
64
|
+
"type": "object",
|
|
65
|
+
"additionalProperties": false,
|
|
66
|
+
"required": [
|
|
67
|
+
"id",
|
|
68
|
+
"title",
|
|
69
|
+
"owner",
|
|
70
|
+
"status",
|
|
71
|
+
"serves"
|
|
72
|
+
],
|
|
73
|
+
"properties": {
|
|
74
|
+
"id": {
|
|
75
|
+
"type": "string",
|
|
76
|
+
"pattern": "^N-[0-9]{3,}$",
|
|
77
|
+
"description": "Addressable, so a verdict, a board row and a commit can all cite the same node."
|
|
78
|
+
},
|
|
79
|
+
"title": {
|
|
80
|
+
"type": "string",
|
|
81
|
+
"minLength": 1
|
|
82
|
+
},
|
|
83
|
+
"owner": {
|
|
84
|
+
"type": "string",
|
|
85
|
+
"minLength": 1,
|
|
86
|
+
"description": "The role that runs it. Required by schema because a node with no owner is a node nobody dispatches — it sits in the frontier forever and the loop looks stalled for a reason no output explains. Whether the name is a role that EXISTS is `graph.py validate`'s question, not this file's."
|
|
87
|
+
},
|
|
88
|
+
"status": {
|
|
89
|
+
"enum": [
|
|
90
|
+
"pending",
|
|
91
|
+
"running",
|
|
92
|
+
"done",
|
|
93
|
+
"blocked",
|
|
94
|
+
"parked"
|
|
95
|
+
],
|
|
96
|
+
"description": "`blocked` is waiting on an edge; `parked` is the verifier's deliberate *«this is a blocker, continue around it»*. The two are different facts and collapsing them loses the one a person needs."
|
|
97
|
+
},
|
|
98
|
+
"blocked_by": {
|
|
99
|
+
"type": "array",
|
|
100
|
+
"items": {
|
|
101
|
+
"type": "string",
|
|
102
|
+
"pattern": "^N-[0-9]{3,}$"
|
|
103
|
+
},
|
|
104
|
+
"default": [],
|
|
105
|
+
"description": "Node ids. This is what makes the frontier computable at all: a node is runnable when every id here is `done`.",
|
|
106
|
+
"uniqueItems": true
|
|
107
|
+
},
|
|
108
|
+
"serves": {
|
|
109
|
+
"type": "string",
|
|
110
|
+
"minLength": 1,
|
|
111
|
+
"description": "A REQ id, or a clause of the goal. Required, because a node that serves neither is work nobody asked for — and the pipeline's answer to that is to park it WITH THAT AS THE REASON rather than to do it quietly."
|
|
112
|
+
},
|
|
113
|
+
"evidence": {
|
|
114
|
+
"type": [
|
|
115
|
+
"array",
|
|
116
|
+
"null"
|
|
117
|
+
],
|
|
118
|
+
"items": {
|
|
119
|
+
"type": "string"
|
|
120
|
+
},
|
|
121
|
+
"default": null,
|
|
122
|
+
"description": "The command and its output that proves this node is done. Null until closed. **A `done` node must carry at least one non-empty entry** — stated as `if`/`then` above, so the format refuses it before any script runs. The first draft only promised that `graph.py close` refuses a null, which by its own wording accepted `evidence: []` — the exact shape a script writing an empty list produces."
|
|
123
|
+
},
|
|
124
|
+
"parked_reason": {
|
|
125
|
+
"type": "string",
|
|
126
|
+
"minLength": 1,
|
|
127
|
+
"pattern": "\\S",
|
|
128
|
+
"description": "Why this node is parked, written for the person who will read it weeks later. Required when `status` is `parked` — REQ-012, and the same rule shape as `done` implying `evidence`. A park with no reason is indistinguishable from a node that was quietly dropped, and the whole point of parking rather than deleting is that the reason survives. `scripts/graph.py park` refuses without one and refuses to overwrite one already recorded."
|
|
129
|
+
},
|
|
130
|
+
"touches": {
|
|
131
|
+
"type": "array",
|
|
132
|
+
"uniqueItems": true,
|
|
133
|
+
"items": {
|
|
134
|
+
"type": "string",
|
|
135
|
+
"minLength": 1,
|
|
136
|
+
"pattern": "\\S"
|
|
137
|
+
},
|
|
138
|
+
"description": "What this node MUTATES — paths, register names, remote resource ids. `references/planning.md` states the rule the frontier needs: *distinct is not the same as independent, and the check is what they touch, never what they are called.* That rule lived in the markdown plan, and the graph replaced the plan as the thing deciding what runs next — so `next` could hand two agents two runnable nodes that write the same file, with nothing able to report it.\\n\\nOptional, and its absence is DISCLOSED rather than treated as «touches nothing»: `next` prints how many frontier nodes declared no targets, because a quiet run and a checked one must not look alike."
|
|
139
|
+
}
|
|
140
|
+
},
|
|
141
|
+
"allOf": [
|
|
142
|
+
{
|
|
143
|
+
"if": {
|
|
144
|
+
"properties": {
|
|
145
|
+
"status": {
|
|
146
|
+
"const": "done"
|
|
147
|
+
}
|
|
148
|
+
},
|
|
149
|
+
"required": [
|
|
150
|
+
"status"
|
|
151
|
+
]
|
|
152
|
+
},
|
|
153
|
+
"then": {
|
|
154
|
+
"required": [
|
|
155
|
+
"evidence"
|
|
156
|
+
],
|
|
157
|
+
"properties": {
|
|
158
|
+
"evidence": {
|
|
159
|
+
"type": "array",
|
|
160
|
+
"minItems": 1,
|
|
161
|
+
"items": {
|
|
162
|
+
"type": "string",
|
|
163
|
+
"minLength": 1,
|
|
164
|
+
"pattern": "\\S",
|
|
165
|
+
"description": "At least one non-whitespace character. `minLength: 1` alone accepts a single space, which is the one shape where this schema and `graph.py`'s verdict gate disagreed — the gate strips, the schema counted. A cross-check fixture comparing the two found it."
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
},
|
|
171
|
+
{
|
|
172
|
+
"if": {
|
|
173
|
+
"properties": {
|
|
174
|
+
"status": {
|
|
175
|
+
"const": "parked"
|
|
176
|
+
}
|
|
177
|
+
},
|
|
178
|
+
"required": [
|
|
179
|
+
"status"
|
|
180
|
+
]
|
|
181
|
+
},
|
|
182
|
+
"then": {
|
|
183
|
+
"required": [
|
|
184
|
+
"parked_reason"
|
|
185
|
+
],
|
|
186
|
+
"properties": {
|
|
187
|
+
"parked_reason": {
|
|
188
|
+
"type": "string",
|
|
189
|
+
"minLength": 1,
|
|
190
|
+
"pattern": "\\S"
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
]
|
|
196
|
+
},
|
|
197
|
+
"edge": {
|
|
198
|
+
"type": "object",
|
|
199
|
+
"additionalProperties": false,
|
|
200
|
+
"required": [
|
|
201
|
+
"from",
|
|
202
|
+
"to",
|
|
203
|
+
"payload"
|
|
204
|
+
],
|
|
205
|
+
"properties": {
|
|
206
|
+
"from": {
|
|
207
|
+
"type": "string",
|
|
208
|
+
"pattern": "^N-[0-9]{3,}$"
|
|
209
|
+
},
|
|
210
|
+
"to": {
|
|
211
|
+
"type": "string",
|
|
212
|
+
"pattern": "^N-[0-9]{3,}$"
|
|
213
|
+
},
|
|
214
|
+
"payload": {
|
|
215
|
+
"type": "string",
|
|
216
|
+
"minLength": 1,
|
|
217
|
+
"description": "What this edge carries. Required, and it is `references/planning.md`'s fake-edge test stated in the schema rather than remembered: an arrow whose payload nobody can name is not a dependency, it is a drawing. Naming it is also how the receiving node's brief gets written."
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
},
|
|
221
|
+
"revision": {
|
|
222
|
+
"type": "object",
|
|
223
|
+
"additionalProperties": false,
|
|
224
|
+
"required": [
|
|
225
|
+
"verb",
|
|
226
|
+
"node",
|
|
227
|
+
"why"
|
|
228
|
+
],
|
|
229
|
+
"properties": {
|
|
230
|
+
"verb": {
|
|
231
|
+
"type": "string",
|
|
232
|
+
"enum": [
|
|
233
|
+
"add",
|
|
234
|
+
"park",
|
|
235
|
+
"close"
|
|
236
|
+
],
|
|
237
|
+
"description": "Which mutation made this entry. `close` joined the two on 2026-08-17 with T-5 — and it was caught by a probe rather than by the fixture asserting *the graph after a close still validates*, because `violations()` never reaches an enum. The same disagreement B-084 records, one field over."
|
|
238
|
+
},
|
|
239
|
+
"node": {
|
|
240
|
+
"type": "string",
|
|
241
|
+
"pattern": "^N-[0-9]{3,}$",
|
|
242
|
+
"description": "The node the mutation touched."
|
|
243
|
+
},
|
|
244
|
+
"why": {
|
|
245
|
+
"type": "string",
|
|
246
|
+
"minLength": 1,
|
|
247
|
+
"pattern": "\\S",
|
|
248
|
+
"description": "The reason, written for a person reading it later. The non-whitespace pattern is required for the same reason `parked_reason` needs one: `minLength: 1` counts a space."
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
}
|
|
@@ -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
|
},
|
|
@@ -22,6 +22,7 @@ requirement.** It is what turns the pipeline from a funnel into a circle.
|
|
|
22
22
|
- A seam is not a deliverable, and REQ rows are written against deliverables
|
|
23
23
|
- GATE (manual)
|
|
24
24
|
- When the answer is "something's missing"
|
|
25
|
+
- A project of several repositories
|
|
25
26
|
|
|
26
27
|
## Why a stage and not a gate
|
|
27
28
|
|
|
@@ -187,6 +188,42 @@ A REQ whose evidence lives in an unpushed commit, or in a submodule the parent
|
|
|
187
188
|
doesn't point at yet, is `partial` — the evidence is not reachable by anyone but
|
|
188
189
|
you.
|
|
189
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
|
+
|
|
190
227
|
## The closing question
|
|
191
228
|
|
|
192
229
|
The table is preparation. The stage exists for the question that follows it, asked
|
|
@@ -361,3 +398,35 @@ or a tracked follow-up. Both are legitimate outcomes of this stage. Closing the
|
|
|
361
398
|
run with a known gap is fine **if the gap is written down** — closing it with the
|
|
362
399
|
gap only in someone's memory is the failure mode this whole spine exists to
|
|
363
400
|
prevent.
|
|
401
|
+
|
|
402
|
+
---
|
|
403
|
+
|
|
404
|
+
## A project of several repositories
|
|
405
|
+
|
|
406
|
+
**A submodule is finished when its parent says so.** A parent repository records each submodule as
|
|
407
|
+
a pointer to one commit, and moving the submodule does not move the pointer. So the work is
|
|
408
|
+
committed, pushed, its CI is green and its own roadmap says done — and anyone who clones the parent
|
|
409
|
+
gets the commit **before** the change. Nothing looks wrong in either repository on its own; the
|
|
410
|
+
disagreement exists only between them, which is why it survives every check that runs inside one.
|
|
411
|
+
|
|
412
|
+
Stage 10 does not close until:
|
|
413
|
+
|
|
414
|
+
```bash
|
|
415
|
+
git submodule status # no line begins with '+' (a '+' is the missing bump)
|
|
416
|
+
git -C <each repo> status --porcelain && git -C <each repo> log @{u}..HEAD --oneline
|
|
417
|
+
```
|
|
418
|
+
|
|
419
|
+
report nothing — for the parent as well as every submodule. Where
|
|
420
|
+
[agent-sync](https://github.com/ssheleg/agent-sync) is installed, `/agent-sync finish` runs
|
|
421
|
+
exactly this plus *no lease left held*, and `--gates` adds the project's own gate commands.
|
|
422
|
+
|
|
423
|
+
The fix, when it fails, is two commands and the second is the one that gets forgotten:
|
|
424
|
+
|
|
425
|
+
```bash
|
|
426
|
+
git -C <submodule> push
|
|
427
|
+
git add <submodule> && git commit -m "chore: bump <name> submodule — <why>"
|
|
428
|
+
```
|
|
429
|
+
|
|
430
|
+
Moved out of `SKILL.md` on 2026-08-16: the body was 6685 tokens against a
|
|
431
|
+
< 5000 budget, and stage-10 close-out is what this file is for.
|
|
432
|
+
|
|
@@ -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
|