task-pipeline-skill 1.93.2 → 1.94.1
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 +151 -1
- package/README.md +42 -9
- package/SKILL-CARD.md +1 -1
- package/bin/task-pipeline.js +64 -33
- package/cursor/rules/task-pipeline.mdc +10 -5
- package/evals/RESULTS.md +1 -1
- package/evals/task-pipeline.evals.json +16 -0
- package/package.json +3 -3
- package/plugins/task-pipeline/.claude-plugin/plugin.json +1 -1
- package/plugins/task-pipeline/agents/verifier.md +4 -1
- package/plugins/task-pipeline/commands/task-pipeline.md +22 -8
- package/plugins/task-pipeline/hooks/memory-nudge.sh +45 -3
- package/plugins/task-pipeline/skills/evidence-docs/references/gates.md +6 -3
- package/plugins/task-pipeline/skills/evidence-docs/references/hooks.md +2 -1
- package/plugins/task-pipeline/skills/evidence-docs/references/retrospective.md +26 -1
- package/plugins/task-pipeline/skills/task-pipeline/SKILL.md +13 -11
- package/plugins/task-pipeline/skills/task-pipeline/graph.schema.json +89 -4
- package/plugins/task-pipeline/skills/task-pipeline/pipeline.schema.json +7 -0
- package/plugins/task-pipeline/skills/task-pipeline/references/backlog.md +25 -0
- package/plugins/task-pipeline/skills/task-pipeline/references/certification.md +45 -1
- package/plugins/task-pipeline/skills/task-pipeline/references/continuity.md +95 -26
- package/plugins/task-pipeline/skills/task-pipeline/references/deploy-targets.md +30 -4
- package/plugins/task-pipeline/skills/task-pipeline/references/doctrine-map.md +1 -0
- package/plugins/task-pipeline/skills/task-pipeline/references/gates.md +6 -3
- package/plugins/task-pipeline/skills/task-pipeline/references/grill.md +1 -1
- package/plugins/task-pipeline/skills/task-pipeline/references/hooks.md +2 -1
- package/plugins/task-pipeline/skills/task-pipeline/references/iteration.md +387 -0
- package/plugins/task-pipeline/skills/task-pipeline/references/knowledge-sources.md +1 -0
- package/plugins/task-pipeline/skills/task-pipeline/references/loop-guard.md +4 -1
- package/plugins/task-pipeline/skills/task-pipeline/references/portability.md +1 -0
- package/plugins/task-pipeline/skills/task-pipeline/references/retrospective.md +26 -1
- package/plugins/task-pipeline/skills/task-pipeline/references/stages.md +35 -11
- package/plugins/task-pipeline/skills/task-pipeline/references/waves.md +80 -36
- package/plugins/task-pipeline/skills/task-pipeline/references/work-graph.md +54 -5
- package/plugins/task-pipeline/skills/task-pipeline/scripts/backlog_sources.py +212 -0
- package/plugins/task-pipeline/skills/task-pipeline/scripts/context_packets.py +3 -3
- package/plugins/task-pipeline/skills/task-pipeline/scripts/graph.py +205 -22
- package/plugins/task-pipeline/skills/task-pipeline/scripts/lessons.py +1137 -0
- package/plugins/task-pipeline/skills/task-pipeline/scripts/stage_checkpoint.py +120 -15
- package/plugins/task-pipeline/skills/task-pipeline/scripts/waves.py +546 -0
- package/plugins/task-pipeline/skills/task-pipeline/templates/README.md +6 -0
- package/plugins/task-pipeline/skills/task-pipeline/templates/backlog-sources.schema.json +96 -0
- package/plugins/task-pipeline/skills/task-pipeline/templates/canvas.md +97 -0
- package/plugins/task-pipeline/skills/task-pipeline/templates/dossier.md +82 -0
- package/plugins/task-pipeline/skills/task-pipeline/templates/inventory.schema.json +94 -0
- package/plugins/task-pipeline/skills/task-pipeline/templates/lesson.schema.json +221 -0
- package/plugins/task-pipeline/skills/task-pipeline/templates/questions.md +45 -0
- package/plugins/task-pipeline/skills/task-pipeline/templates/retro.md +4 -0
- package/plugins/task-pipeline/skills/task-pipeline/templates/run.md +2 -2
|
@@ -54,7 +54,7 @@ on real agents, tasks and users rather than on the first guess.
|
|
|
54
54
|
| 2 Brainstorm + decompose | once: the canvas extends the module map ([`decomposition.md`](decomposition.md)), and the work graph carries each node's wave |
|
|
55
55
|
| 3 Spec | once: the canvas is the committed target-state spec; a wave's dossier may deepen a section, never contradict it |
|
|
56
56
|
| 4 Plan | once: every wave's nodes, its exit criteria and the question batch. Re-cut at each wave boundary, as a recorded revision |
|
|
57
|
-
| 5–8 Build → post-deploy | per wave, then the close pass |
|
|
57
|
+
| 5–8 Build → post-deploy | per wave, then the close pass. A wave's stage 7 integrates to the main branch **without tagging**; the release's tag, its approval mode and its go are stage 7's full gate, run once at the release ([`iteration.md`](iteration.md) → *The cycle*, step 10), and stage 8's trio follows on what was released (step 11) |
|
|
58
58
|
| 9 Docs + wiki · 10 Acceptance | once, to close the release, over everything the waves shipped |
|
|
59
59
|
|
|
60
60
|
**One planning gate, not three prompts.** The manual gates of stages 0, 2 and 3 are
|
|
@@ -72,37 +72,51 @@ product has users, walk their real path in the built app on a fresh profile with
|
|
|
72
72
|
disposable data. A finding is a fact with a receipt, never an impression.
|
|
73
73
|
|
|
74
74
|
**A declared source list outranks discovery.** Where the repository carries
|
|
75
|
-
`docs/backlog-sources.json` —
|
|
76
|
-
`
|
|
75
|
+
`docs/backlog-sources.json` — its shape is
|
|
76
|
+
[`templates/backlog-sources.schema.json`](../templates/backlog-sources.schema.json)
|
|
77
|
+
([`backlog.md`](backlog.md) → *A declared source list*) — the harvest reads exactly the
|
|
78
|
+
sources it lists, as the board,
|
|
77
79
|
with the id, title and status columns it names. Status changes land only in those owning
|
|
78
80
|
files, under a lease. A new source joins the manifest in the same change. A generated
|
|
79
81
|
view of them, such as a published common backlog, is never edited. A source of open
|
|
80
82
|
work the manifest does not list is reported as a finding, not silently added.
|
|
83
|
+
`scripts/backlog_sources.py --root . [--found <path> ...]` reads the manifest first: it
|
|
84
|
+
prints the sources the harvest reads, refuses a manifest the harvest could not follow
|
|
85
|
+
(exit 1, every reason named) and reports each `--found` source it does not list (exit 3).
|
|
86
|
+
No manifest is discovery, said aloud (exit 0).
|
|
81
87
|
|
|
82
88
|
**One machine-readable inventory, nothing outside it.** A single committed file,
|
|
83
|
-
`<artifacts>/specs/YYYY-MM-DD-<topic>-inventory.json`, one
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
| `area` | the part of the system it touches |
|
|
91
|
-
| `node` | the canvas node it lands on |
|
|
92
|
-
| `wave` | the wave that closes it, or the release it is carried to |
|
|
93
|
-
| `release` | this release, or `program` |
|
|
94
|
-
|
|
95
|
-
`blocked_by`, `closable_in_one_pass` and `notes` are worth carrying beside them.
|
|
89
|
+
`<artifacts>/specs/YYYY-MM-DD-<topic>-inventory.json`, one item per open item. Its shape is
|
|
90
|
+
[`templates/inventory.schema.json`](../templates/inventory.schema.json) (inventory/1): every
|
|
91
|
+
item carries its `id` in the owning register, `title`, `area`, `kind`, `severity`, the
|
|
92
|
+
`evidence` that is its receipt and a `proposed_fix`; `status` is as measured, not as the
|
|
93
|
+
register last said; `module`, `node`, `wave` and `release` are written by the script below,
|
|
94
|
+
never by hand. `scripts/waves.py merge <dir>` builds it from the survey outputs and refuses
|
|
95
|
+
an item without evidence or an id seen twice.
|
|
96
96
|
|
|
97
97
|
**Verify each open item against the code before trusting it.** A stale item is closed
|
|
98
98
|
in its owning register with the receipt that shows it done; duplicates are clustered,
|
|
99
99
|
both ids kept. A register that says *open* about shipped work is the commonest thing a
|
|
100
100
|
survey finds.
|
|
101
101
|
|
|
102
|
-
**Assign nodes and waves with a committed script, not by hand.**
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
102
|
+
**Assign nodes and waves with a committed script, not by hand.** `scripts/waves.py assign
|
|
103
|
+
--modules <modules.json> --plan <plan.json> --write` reads two short tables — module → item
|
|
104
|
+
ids, and the plan's nodes with their `wave` and the items each `closes` — writes `module`,
|
|
105
|
+
`node`, `wave` and `release` onto every item, and prints the counts. `waves.py validate`
|
|
106
|
+
exits 1 when an item lands on no module or an open item on no node: either the canvas has a
|
|
107
|
+
gap or the item is closed with its receipt or carried (`status: carried`, `release:
|
|
108
|
+
program`). An incomplete `assign --write` prints the gaps but preserves the prior file;
|
|
109
|
+
preview and repair the tables before retrying. Plan ids and effective `graph_id` targets
|
|
110
|
+
must be unique. `waves.py sync-graph` writes the plan's waves and risk classes onto the
|
|
111
|
+
work graph with graph.py's lock and revision log — one `wave` or `risk` revision per
|
|
112
|
+
change, a lowered risk only with `--lower-why` — and judges the re-cut as
|
|
113
|
+
a whole with graph.py's order rule, so moving a chain up a wave is never refused halfway.
|
|
114
|
+
A missing graph target refuses before any write; `--only-present` explicitly opts into a
|
|
115
|
+
plan spanning several graphs. A re-cut is then a diff to a table
|
|
116
|
+
and a re-run, which a reviewer can read; seven hundred hand-edited rows are not. The canvas,
|
|
117
|
+
the question queue and each dossier start from [`templates/canvas.md`](../templates/canvas.md),
|
|
118
|
+
[`templates/questions.md`](../templates/questions.md) and
|
|
119
|
+
[`templates/dossier.md`](../templates/dossier.md).
|
|
106
120
|
|
|
107
121
|
## The questions — closed from sources, then one batch
|
|
108
122
|
|
|
@@ -117,13 +131,16 @@ answer and the consequence of each answer, ordered so prerequisite decisions com
|
|
|
117
131
|
([`grill.md`](grill.md)). A go that leaves a question unanswered takes the
|
|
118
132
|
recommendation, recorded as **assumed** in the decisions table, and every wave that
|
|
119
133
|
depends on an assumption names it — so a different answer later re-plans exactly those
|
|
120
|
-
waves and no others. After planning, a question is asked only
|
|
121
|
-
|
|
134
|
+
waves and no others. After planning, a question is asked only in a question round
|
|
135
|
+
([`iteration.md`](iteration.md) → *Question rounds*): a wave boundary, a blocked frontier
|
|
136
|
+
or five open questions calls one, and its batch lands in the hand-back's DECISIONS
|
|
137
|
+
WAITING ([`progress.md`](progress.md)). Never one question at a time.
|
|
122
138
|
|
|
123
|
-
**An outward or irreversible act is never assumed.** Publishing, tagging, deploying
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
link. A recommendation
|
|
139
|
+
**An outward or irreversible act is never assumed.** Publishing, tagging, deploying,
|
|
140
|
+
sending to a person, spending money or deleting data needs authorization naming that act.
|
|
141
|
+
Cite a specific standing authorization within its scope; otherwise ask for the operator's
|
|
142
|
+
explicit go with the relevant verified direct link. A recommendation or generic loop mode
|
|
143
|
+
is not authorization ([`continuity.md`](continuity.md) → *The limit, before the capability*).
|
|
127
144
|
|
|
128
145
|
## The canvas — the target state over the current logic
|
|
129
146
|
|
|
@@ -165,21 +182,39 @@ marked `needs-operator`, out of every wave until it is unblocked.
|
|
|
165
182
|
|
|
166
183
|
Each wave runs stages 5–8 as [`stages.md`](stages.md) states them, then the close pass.
|
|
167
184
|
Between waves the run does not ask whether to continue: it takes the next wave
|
|
168
|
-
([`continuity.md`](continuity.md)).
|
|
169
|
-
|
|
170
|
-
|
|
185
|
+
([`continuity.md`](continuity.md)). An unresolved ambiguity parks its node with its
|
|
186
|
+
actual owner and reason; only a human-owned question is `needs-operator`. Project
|
|
187
|
+
asks follow [`iteration.md`](iteration.md), and the wave continues on independent
|
|
188
|
+
nodes. A frontier blocked by CI, review
|
|
189
|
+
or another external wait goes on watchers, with the waits and their next actions recorded
|
|
190
|
+
([`continuity.md`](continuity.md) → *Waits outlive turns*); it is not completion or a new
|
|
191
|
+
request for permission. An unresolved human gate stops its dependent work. When the board
|
|
192
|
+
is empty, the run says so once. `graph.py next --wave N` returns 6 when only recorded
|
|
193
|
+
watched waits hold that wave, following prerequisite leaves across wave boundaries.
|
|
194
|
+
Keep their watchers armed; `wait` records their locators and `resume --wait-id`
|
|
195
|
+
refuses stale events. Exit 4 retains mixed or unclassified barriers: read the reasons
|
|
196
|
+
before choosing the next action. Neither code is proof of completion or authority
|
|
197
|
+
([`work-graph.md`](work-graph.md) → *Watched external waits*).
|
|
171
198
|
|
|
172
199
|
Research, the audit and the blind readings run in parallel agents; builds stay sequential
|
|
173
200
|
wherever two of them share a seam ([`planning.md`](planning.md) → file ownership).
|
|
174
201
|
|
|
202
|
+
**Verification does not wait for the wave.** A node is certified as soon as it lands, in
|
|
203
|
+
the number of rounds its risk class sets, and a seam is checked once the last node on it
|
|
204
|
+
lands ([`iteration.md`](iteration.md) → *Verification starts when a node lands*). The
|
|
205
|
+
close pass below then re-measures what those rounds already proved.
|
|
206
|
+
|
|
175
207
|
## The close pass, after every wave
|
|
176
208
|
|
|
177
209
|
In this order, inside the wave rather than after it:
|
|
178
210
|
|
|
179
211
|
1. **Verify.** Every node the wave closes is certified ([`certification.md`](certification.md)),
|
|
212
|
+
most of it already, as each node landed,
|
|
180
213
|
and [`audit.md`](audit.md)'s exit is re-measured over what the wave touched.
|
|
181
214
|
2. **Finish and fix — one fix round**, inside [`loop-guard.md`](loop-guard.md)'s caps.
|
|
182
|
-
|
|
215
|
+
Findings against this wave's nodes are fixed here or parked with a board row and reason.
|
|
216
|
+
Findings elsewhere join the inventory for a planned node; only the closed hotfix lane
|
|
217
|
+
bypasses that design ([`iteration.md`](iteration.md) → *No fix travels alone*). Nothing
|
|
183
218
|
is dropped silently.
|
|
184
219
|
3. **Document** — the Doc Loop and the stage-9 propagation matrix
|
|
185
220
|
([`documentation.md`](documentation.md)), scoped to the wave: scenarios, decisions,
|
|
@@ -193,6 +228,9 @@ In this order, inside the wave rather than after it:
|
|
|
193
228
|
|
|
194
229
|
## A board bigger than one release
|
|
195
230
|
|
|
231
|
+
**The loop around the waves — release, post-release check, next pool, and the next
|
|
232
|
+
iteration — is [`iteration.md`](iteration.md).**
|
|
233
|
+
|
|
196
234
|
**It becomes a program.** The release takes the first waves; the rest stays on the
|
|
197
235
|
canvas by node, `release: program`, never dropped. The next release resumes from the
|
|
198
236
|
canvas: it re-measures the inventory rather than re-running wave 0 from nothing, and its
|
|
@@ -200,11 +238,17 @@ waves are cut from the nodes still open.
|
|
|
200
238
|
|
|
201
239
|
## What stays human
|
|
202
240
|
|
|
203
|
-
Sign-ins, consents, payments
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
241
|
+
Sign-ins, consents, operator payments and typed secrets remain human. Record the
|
|
242
|
+
required action and continue independent work; a release or loop grant cannot
|
|
243
|
+
perform these actions for the person.
|
|
244
|
+
|
|
245
|
+
Other outward or irreversible acts, including sending to people, require explicit
|
|
246
|
+
authority. A specific standing authorization naming the act counts within its
|
|
247
|
+
recorded source and scope ([`continuity.md`](continuity.md) → *The limit, before the
|
|
248
|
+
capability*). A generic instruction to continue does not. A `manual` gate waits
|
|
249
|
+
without such authority. The mode removes *"shall I continue?"* between waves and
|
|
250
|
+
*"what did you mean?"* where a source already says; it never removes a person from
|
|
251
|
+
a decision that is theirs.
|
|
208
252
|
|
|
209
253
|
## The graph carries the wave
|
|
210
254
|
|
|
@@ -229,5 +273,5 @@ said to be checked by hand.
|
|
|
229
273
|
| "Ask each question as it comes up" | Each one is a human round. Close it from the sources, or put it in the batch |
|
|
230
274
|
| "The board is too big to inventory" | The inventory is the plan. Without it the waves are cut from memory |
|
|
231
275
|
| "Re-cutting waves makes the pipeline dynamic" | A re-cut is a committed revision and a canvas edit. The static-graph decision in [`planning.md`](planning.md) holds |
|
|
232
|
-
| "The recommendation is obviously right, so assume the deploy go" | An outward act is never assumed.
|
|
276
|
+
| "The recommendation is obviously right, so assume the deploy go" | An outward act is never assumed. Cite the standing authorization naming it, or ask for the specific go once, in the batch |
|
|
233
277
|
| "Wave 3 can stub what wave 5 will build properly" | Only at a declared contract, with wave 5 named as the stub's removal |
|
|
@@ -41,6 +41,7 @@ turn of every loop.
|
|
|
41
41
|
| `nodes[].blocked_by` | what must PRODUCE first. The frontier obeys a satisfaction predicate, and only `done` satisfies — a PARKED producer blocks its consumer, because a park is a decision not to produce, not a production. `next` names each held consumer with the park's reason; a valid alternative producer is an explicit, versioned edge change, never an implicit unblock |
|
|
42
42
|
| `nodes[].touches` | what it **mutates**. Two runnable nodes writing one file is the false parallelism [`planning.md`](planning.md) refuses — *distinct is not the same as independent, and the check is what they touch, never what they are called* |
|
|
43
43
|
| `nodes[].check` | **how this node will be closed** — one command, or the named judgement where no command can decide it. Required on every node except a `parked` one. The certification's `unit` tier runs it and reports its output as the evidence row ([`certification.md`](certification.md) — three blind tiers close a node, not one reader); before this field existed that instruction pointed at an absence, leaving a verifier the two things it forbids — invent a check, or run everything (B-080) |
|
|
44
|
+
| `nodes[].risk` | wave mode or a release iteration: the node's risk class, `R1`, `R2` or `R3` ([`iteration.md`](iteration.md) → *Verification starts when a node lands*): how many passing certification rounds the node owes — one, two, or three with one adversarial ([`certification.md`](certification.md) → *Rounds owed by risk class*). `validate` refuses any other value, because a class nobody recognises owes no known number of rounds, and `close` refuses the node until its rounds are done |
|
|
44
45
|
| `nodes[].wave` | wave mode only ([`waves.md`](waves.md)): the pass of stages 5–8 that closes the node, from 1. **An earlier wave may not wait on a later one** — `validate` refuses the edge, because a wave holds only what closes in its own pass |
|
|
45
46
|
| `nodes[].evidence` | required when `status` is `done`. A node called done by assertion is what evidence exists to prevent |
|
|
46
47
|
| `nodes[].parked_reason` | required when `status` is `parked`. A park with no reason is indistinguishable, a week later, from work quietly dropped |
|
|
@@ -55,22 +56,62 @@ conditional on the code, never merely sequenced after it.**
|
|
|
55
56
|
| Verb | Prints | Codes |
|
|
56
57
|
|---|---|---|
|
|
57
58
|
| `validate` | every violation, in a stable order | `0` clean · `1` any |
|
|
58
|
-
| `next` | the frontier, ordered by how much each node unblocks — **and nothing else**. `--wave N` narrows it to one wave, and `3` then means *this wave is done* | `0` printed · `3` all
|
|
59
|
+
| `next` | the frontier, ordered by how much each node unblocks — **and nothing else**. `--wave N` narrows it to one wave, and `3` then means *this wave is done* | `0` printed · `3` all terminal · `4` nothing runnable with another barrier · `6` only recorded watched waits hold remaining dependency leaves · `1` with `--wave`, a wave no node is in |
|
|
59
60
|
| `coverage` | each requirement with the nodes serving it | `0` covered · `1` a gap, named |
|
|
60
61
|
| `goal` | the release goal | `0` · `3` unstated |
|
|
61
62
|
| `add` | the id it allocated | `0` · `1` refused |
|
|
62
63
|
| `park` | the id and the reason | `0` · `1` refused |
|
|
64
|
+
| `wait` | the node and new wait identity; records kind, watcher locator and reason, never executes the locator | `0` recorded · `1` refused · `2` usage |
|
|
65
|
+
| `resume` | the node and resolved wait identity; returns to pending and archives the wait in revisions | `0` resolved · `1` stale identity or refused · `2` usage |
|
|
63
66
|
| `wave` | wave mode's re-cut: the node, its old and new wave, and the reason, which enters the revision log. Refuses a move that would leave an earlier wave waiting on a later one, and a `done` node | `0` · `1` refused |
|
|
64
|
-
| `certify` | the round, and on a failure every `breaks` finding with its fix and its check | `0` every owed tier passed — three, or four with `visual` on a flagship or product node · `1` a tier failed, a required one is missing, or a report is malformed |
|
|
65
|
-
| `close` | the goal, the new frontier count, and what was not verified | `0` · `1` refused **or the verdict stops the run** |
|
|
67
|
+
| `certify` | the round — with a risk class, `round N: k of M owed passing rounds — not closable yet` or `k of M owed passing rounds — closable`; `--kind adversarial` records an adversarial round — and on a failure every `breaks` finding with its fix and its check | `0` every owed tier passed — three, or four with `visual` on a flagship or product node · `1` a tier failed, a required one is missing, or a report is malformed |
|
|
68
|
+
| `close` | the goal, the new frontier count, and what was not verified | `0` · `1` refused — including a node whose risk class owes rounds not yet passed — **or the verdict stops the run** |
|
|
66
69
|
| `producer` | what produced this proof — actor, model, runtime, skill, config, commit, trace | `0` |
|
|
67
70
|
| `doctrine` | how many of the bundle's reference files this run opened | `0` |
|
|
68
71
|
| `claim` | **external mode** — arbitrate one runnable node to a single owner through the durable coordinator (`scripts/execution_authority.py`); prints the grant. `next` says what COULD run, `claim` says who MAY | `0` won · `5` lost the race · `4` not runnable · `1` authority unavailable — **and `1` means no work starts** (fail-closed) |
|
|
69
72
|
| `release` | give back a hold this run owns, matching its fence; a mismatch is a no-op, never a way to steal a live node | `0` released · `5` not held at that fence · `1` authority unavailable |
|
|
70
|
-
| `recover` | **external mode** — reclaim an EXPIRED node for a new owner, minting a higher fence; a still-live claim is not recoverable (that is stealing a working node) | `0` recovered · `5` still live · `1` authority unavailable |
|
|
73
|
+
| `recover` | **external mode** — reclaim an EXPIRED node for a new owner, minting a higher fence; a still-live claim is not recoverable (that is stealing a working node) | `0` recovered · `5` still live · `4` recorded wait unresolved · `1` invalid graph or authority unavailable |
|
|
71
74
|
| `complete` | **external mode** — record completion from the CURRENT fence-holder only; a late/superseded worker (stale fence) is refused, and the current holder completing twice is idempotent | `0` completed · `5` not the current holder · `1` authority unavailable |
|
|
72
75
|
| `waive` | record an AUTHORIZED EXCEPTION — reason + identity, its own disposition: the node is never marked certified, a failed certification stays visible, and `close` stamps the exception into the evidence | `0` waived · `1` refused (no reason, no identity, unknown node) |
|
|
73
|
-
| `invalidate` | a REQ/interface/brief change supersedes one node: a superseding revision is recorded, and the node plus every
|
|
76
|
+
| `invalidate` | a REQ/interface/brief change supersedes one node: a superseding revision is recorded, and the node plus every non-parked DESCENDANT resets to pending with its evidence/proof/certification cleared — a proof certified against a contract that moved is a proof about a tree that no longer exists. Unrelated nodes keep their proofs; invalidation flows downstream only | `0` invalidated · `1` refused (no `--why`, unknown node, or the write would break the graph) |
|
|
77
|
+
|
|
78
|
+
### Watched external waits
|
|
79
|
+
|
|
80
|
+
A node waiting on CI, review, a peer, release, time or a lease uses `status: waiting`
|
|
81
|
+
and `waiting_on: {id, kind, watcher}`. The closed `kind` vocabulary is `ci`, `review`,
|
|
82
|
+
`peer`, `release`, `time`, `lease`; `watcher` is a nonblank single-line durable
|
|
83
|
+
trigger locator. The generated 32-hex identity distinguishes repeated waits even
|
|
84
|
+
when the locator is reused. Runtime validation and the schema reject unknown
|
|
85
|
+
statuses, malformed waits and live wait metadata on another status.
|
|
86
|
+
|
|
87
|
+
```bash
|
|
88
|
+
python3 scripts/graph.py wait N-001 --kind ci --watcher '<durable trigger locator>' --why 'candidate checks pending'
|
|
89
|
+
python3 scripts/graph.py resume N-001 --wait-id '<identity returned by wait>' --why 'exact candidate checks resolved'
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
These transitions take the same exclusive graph lock as other mutations. `wait`
|
|
93
|
+
accepts only pending/blocked nodes without a recorded execution claim; `resume`
|
|
94
|
+
requires the current identity. Running, terminal and already-waiting nodes refuse
|
|
95
|
+
a new wait. The prior record survives resume, park and invalidation in revisions.
|
|
96
|
+
Close and certification refuse an unresolved wait; resume does not imply success
|
|
97
|
+
or completion. It only allows the existing dependency and verification rules to
|
|
98
|
+
consider the node again.
|
|
99
|
+
|
|
100
|
+
`next` excludes waits from runnable stdout; external `claim` and `recover` therefore refuse
|
|
101
|
+
one. Exit **6** means every remaining dependency leaf is a recorded wait, including
|
|
102
|
+
leaves outside `--wave`. Wait identity/locator diagnostics go to stderr. A ready
|
|
103
|
+
sibling still returns 0; a running, parked dependency or unclassified barrier keeps
|
|
104
|
+
4. Exit 3 retains its existing all-terminal meaning, including intentionally parked
|
|
105
|
+
nodes; it is not proof every requirement was delivered.
|
|
106
|
+
|
|
107
|
+
Recording a locator does not start a watcher, prove it is alive, or authorize its
|
|
108
|
+
action. Arm the actual watcher through the host and retain its receipt as described
|
|
109
|
+
in [`continuity.md`](continuity.md). Human-only actions retain their boundary;
|
|
110
|
+
resuming a graph node cannot grant approval. Graph-local state and the separate
|
|
111
|
+
SQLite execution authority are not one transaction: this command refuses recorded
|
|
112
|
+
graph claims but cannot discover an unrecorded external grant. In external mode,
|
|
113
|
+
the coordinator must stop/release that grant before recording the wait and prevent
|
|
114
|
+
concurrent dispatch. No cross-store fencing guarantee is claimed here.
|
|
74
115
|
|
|
75
116
|
**`next` is ordered by what each node unblocks, transitively, and the number is computed.**
|
|
76
117
|
A `priority` field is something somebody typed once and nobody revisits; this one moves when
|
|
@@ -95,6 +136,14 @@ it is never the one naming one.
|
|
|
95
136
|
**A stop closes its node and refuses the next step.** `replan.possible: false` says the run
|
|
96
137
|
cannot continue *around* what it found — not that the work just verified did not happen.
|
|
97
138
|
|
|
139
|
+
**A mutation needs a working file lock.** The lock covers loading, checking and
|
|
140
|
+
replacing the graph. If `fcntl` is unavailable or opening/acquiring the lock fails,
|
|
141
|
+
the command exits 1 before writing; it no longer warns and continues unlocked.
|
|
142
|
+
Read-only verbs remain available on those platforms. An atomic file replacement
|
|
143
|
+
alone cannot prevent concurrent writers from losing each other's updates. This
|
|
144
|
+
is a local graph-file guarantee, not a transaction with the separate execution
|
|
145
|
+
claim store.
|
|
146
|
+
|
|
98
147
|
## The three things a schema cannot state
|
|
99
148
|
|
|
100
149
|
The split is where the format actually puts it, which is not where the first draft drew it.
|
|
@@ -0,0 +1,212 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""The declared task sources a wave-0 harvest reads (`references/waves.md`, R13) — stdlib only.
|
|
3
|
+
|
|
4
|
+
Where a repository carries `docs/backlog-sources.json`, the harvest reads exactly the sources
|
|
5
|
+
it lists, with the id, title and status columns each one names, and a source of open work the
|
|
6
|
+
manifest does not list is a finding, not a silent addition. This script is the mechanical half
|
|
7
|
+
of that sentence: it reads the manifest, refuses one the harvest could not follow, and prints
|
|
8
|
+
the list the harvest works from.
|
|
9
|
+
|
|
10
|
+
backlog_sources.py [--root DIR] [--found PATH ...]
|
|
11
|
+
|
|
12
|
+
The rule a user of this skill reads is the shipped schema template,
|
|
13
|
+
`templates/backlog-sources.schema.json` (backlog-sources/1), with `references/backlog.md` →
|
|
14
|
+
*A declared source list*; this checker enforces it, plus the two rules a schema cannot state
|
|
15
|
+
(a regular file reached through no symlink, no duplicate declaration). The Fabric workspace's
|
|
16
|
+
`knowledge/backlog.md` (section *Source contract*; read at fabric-workspace d49b6f58,
|
|
17
|
+
2026-10-08) is its provenance: the template is compatible with it, with one deliberate
|
|
18
|
+
widening — `repository` may be any `https://github.com/<owner>/<repo>`, because this skill runs
|
|
19
|
+
outside one organisation.
|
|
20
|
+
|
|
21
|
+
- `schema` is 1; `repository` is a canonical GitHub URL; `sources` holds 1–100 entries.
|
|
22
|
+
- Each `path` is relative, inside the repository, with no hidden, `.` or `..` segment and no
|
|
23
|
+
symlink on the way; it exists as a regular file — `.md`, or `.json` for `json-tasks`.
|
|
24
|
+
- `goal` is one of the five anchors; `kind` is `tasks` (the default) or `context`.
|
|
25
|
+
- A `tasks` source is `table`, `checklist` or `json-tasks`. A table names non-empty
|
|
26
|
+
`idColumn` and `titleColumn` and exactly one of `statusColumn` or `defaultStatus: "open"`.
|
|
27
|
+
- `excludeIds` is a unique list of strings; `idPattern` is a literal prefix, optionally with
|
|
28
|
+
`[0-9]+` or `\\d+`, anchored at both ends. Two identical declarations are refused.
|
|
29
|
+
- A key the schema does not name is refused, at the top and on a source; `heading` is a
|
|
30
|
+
string; `idColumn`, `titleColumn`, `statusColumn`, `defaultStatus`, `excludeIds` and
|
|
31
|
+
`idPattern` are checked on every
|
|
32
|
+
source, whatever its kind. Where `jsonschema` is installed the shipped schema is applied too.
|
|
33
|
+
|
|
34
|
+
Output, on success: one JSON object — `repository`, `tasks` (each declaration as given, the
|
|
35
|
+
fields the harvest reads), `context` (paths), `undeclared` (each `--found` path the manifest
|
|
36
|
+
does not list, as `file:line`-free repository paths).
|
|
37
|
+
|
|
38
|
+
Exit codes: 0 the manifest is readable and nothing undeclared was found, or there is no
|
|
39
|
+
manifest (discovery then, as `references/knowledge-sources.md` describes — printed, not
|
|
40
|
+
silent); 1 the manifest is malformed, every reason printed; 3 the manifest is fine and a
|
|
41
|
+
`--found` source is not in it — the finding the doctrine asks for.
|
|
42
|
+
"""
|
|
43
|
+
from __future__ import annotations
|
|
44
|
+
|
|
45
|
+
import argparse
|
|
46
|
+
import json
|
|
47
|
+
import pathlib
|
|
48
|
+
import re
|
|
49
|
+
import sys
|
|
50
|
+
|
|
51
|
+
MANIFEST = "docs/backlog-sources.json"
|
|
52
|
+
SCHEMA = pathlib.Path(__file__).resolve().parents[1] / "templates" / "backlog-sources.schema.json"
|
|
53
|
+
TOP_KEYS = ("schema", "repository", "sources")
|
|
54
|
+
SOURCE_KEYS = ("path", "goal", "kind", "format", "heading", "idColumn", "titleColumn",
|
|
55
|
+
"statusColumn", "defaultStatus", "idPattern", "excludeIds")
|
|
56
|
+
GOALS = ("agent-workplace", "workflow-control", "interoperability", "reliable-work", "shared-context")
|
|
57
|
+
FORMATS = ("table", "checklist", "json-tasks")
|
|
58
|
+
REPOSITORY = re.compile(r"^https://github\.com/[A-Za-z0-9][A-Za-z0-9-]{0,38}/\.?[A-Za-z0-9][A-Za-z0-9._-]{0,99}$")
|
|
59
|
+
ID_PATTERN = re.compile(r"^\^[A-Za-z0-9._/-]+(?:\[0-9\]\+|\\d\+)?\$$")
|
|
60
|
+
UNSAFE = re.compile(r"[\\\x00-\x1f\x7f?#%:]")
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def _safe(path: object) -> bool:
|
|
64
|
+
if not isinstance(path, str) or not path or len(path) > 700 or UNSAFE.search(path):
|
|
65
|
+
return False
|
|
66
|
+
if path.startswith("/"):
|
|
67
|
+
return False
|
|
68
|
+
return all(seg and seg not in (".", "..") and not seg.startswith(".") for seg in path.split("/"))
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
def _regular(root: pathlib.Path, path: str) -> bool:
|
|
72
|
+
"""The file exists and nothing on the way to it is a symlink."""
|
|
73
|
+
cur = root
|
|
74
|
+
for seg in path.split("/"):
|
|
75
|
+
cur = cur / seg
|
|
76
|
+
if cur.is_symlink():
|
|
77
|
+
return False
|
|
78
|
+
return cur.is_file()
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
def _nonempty(v: object) -> bool:
|
|
82
|
+
return isinstance(v, str) and bool(v.strip())
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
def _schema_errors(doc: dict) -> list[str]:
|
|
86
|
+
"""The shipped schema, applied when `jsonschema` is installed — the authority this
|
|
87
|
+
checker hand-implements. Without the library the hand checks above stand alone, and
|
|
88
|
+
they cover every rule the schema states."""
|
|
89
|
+
try:
|
|
90
|
+
import jsonschema
|
|
91
|
+
schema = json.loads(SCHEMA.read_text(encoding="utf-8"))
|
|
92
|
+
except (ImportError, OSError, ValueError):
|
|
93
|
+
return []
|
|
94
|
+
out = []
|
|
95
|
+
for e in sorted(jsonschema.Draft202012Validator(schema).iter_errors(doc), key=lambda e: list(e.path)):
|
|
96
|
+
where = "/".join(str(p) for p in e.path) or "(top)"
|
|
97
|
+
out.append(f"{MANIFEST}: schema: {where}: {e.message[:200]}")
|
|
98
|
+
return out
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
def check(root: pathlib.Path, found: list[str]) -> tuple[int, dict | None, list[str]]:
|
|
102
|
+
"""(exit code, the harvest list or None, reasons)."""
|
|
103
|
+
mf = root / MANIFEST
|
|
104
|
+
if not mf.exists():
|
|
105
|
+
return 0, None, [f"no {MANIFEST} — the harvest discovers its sources"]
|
|
106
|
+
try:
|
|
107
|
+
doc = json.loads(mf.read_text(encoding="utf-8"))
|
|
108
|
+
except (OSError, ValueError) as exc:
|
|
109
|
+
return 1, None, [f"{MANIFEST}: not readable JSON ({type(exc).__name__})"]
|
|
110
|
+
if not isinstance(doc, dict):
|
|
111
|
+
return 1, None, [f"{MANIFEST}: not an object"]
|
|
112
|
+
errs: list[str] = []
|
|
113
|
+
errs += [f"{MANIFEST}: unknown key `{k}` — backlog-sources/1 names {', '.join(TOP_KEYS)}"
|
|
114
|
+
for k in doc if k not in TOP_KEYS]
|
|
115
|
+
if doc.get("schema") != 1:
|
|
116
|
+
errs.append(f"{MANIFEST}: `schema` is {doc.get('schema')!r}, not 1")
|
|
117
|
+
if not REPOSITORY.match(str(doc.get("repository") or "")):
|
|
118
|
+
errs.append(f"{MANIFEST}: `repository` {doc.get('repository')!r} is not a canonical "
|
|
119
|
+
"https://github.com/<owner>/<repo> URL")
|
|
120
|
+
sources = doc.get("sources")
|
|
121
|
+
if not isinstance(sources, list) or not 1 <= len(sources) <= 100:
|
|
122
|
+
errs.append(f"{MANIFEST}: `sources` must list 1–100 entries — a context-only "
|
|
123
|
+
"repository still declares one")
|
|
124
|
+
sources = sources if isinstance(sources, list) else []
|
|
125
|
+
tasks, context, seen = [], [], set()
|
|
126
|
+
for i, s in enumerate(sources):
|
|
127
|
+
where = f"{MANIFEST}: sources[{i}]"
|
|
128
|
+
if not isinstance(s, dict):
|
|
129
|
+
errs.append(f"{where}: not an object")
|
|
130
|
+
continue
|
|
131
|
+
path, kind, fmt = s.get("path"), s.get("kind", "tasks"), s.get("format")
|
|
132
|
+
where = f"{where} ({path})"
|
|
133
|
+
errs += [f"{where}: unknown key `{k}` — a source names only {', '.join(SOURCE_KEYS)}"
|
|
134
|
+
for k in s if k not in SOURCE_KEYS]
|
|
135
|
+
if "heading" in s and not isinstance(s["heading"], str):
|
|
136
|
+
errs.append(f"{where}: `heading` is not a string")
|
|
137
|
+
# These hold on EVERY source, whatever its kind or format: the schema says so, and
|
|
138
|
+
# a rule checked only on a task table is a rule a context source can break.
|
|
139
|
+
for col in ("idColumn", "titleColumn", "statusColumn"):
|
|
140
|
+
if col in s and not _nonempty(s.get(col)):
|
|
141
|
+
errs.append(f"{where}: `{col}` is empty")
|
|
142
|
+
if "defaultStatus" in s and s.get("defaultStatus") != "open":
|
|
143
|
+
errs.append(f"{where}: `defaultStatus` may only be \"open\"")
|
|
144
|
+
ex = s.get("excludeIds")
|
|
145
|
+
if ex is not None and (not isinstance(ex, list) or not all(_nonempty(x) for x in ex)
|
|
146
|
+
or len(set(ex)) != len(ex)):
|
|
147
|
+
errs.append(f"{where}: `excludeIds` is a unique list of stable ids")
|
|
148
|
+
if "idPattern" in s and not ID_PATTERN.match(str(s.get("idPattern"))):
|
|
149
|
+
errs.append(f"{where}: `idPattern` {s.get('idPattern')!r} is not a literal "
|
|
150
|
+
"prefix with an optional [0-9]+, anchored at both ends")
|
|
151
|
+
if kind not in ("tasks", "context"):
|
|
152
|
+
errs.append(f"{where}: `kind` {kind!r} is neither tasks nor context")
|
|
153
|
+
if s.get("goal") not in GOALS:
|
|
154
|
+
errs.append(f"{where}: `goal` {s.get('goal')!r} is not one of {', '.join(GOALS)}")
|
|
155
|
+
ext = ".json" if (kind == "tasks" and fmt == "json-tasks") else ".md"
|
|
156
|
+
if not _safe(path) or not str(path).endswith(ext):
|
|
157
|
+
errs.append(f"{where}: `path` must be a relative {ext} path with no hidden, `.` "
|
|
158
|
+
"or `..` segment")
|
|
159
|
+
elif not _regular(root, path):
|
|
160
|
+
errs.append(f"{where}: the declared source is missing, or reached through a symlink")
|
|
161
|
+
if kind == "tasks":
|
|
162
|
+
if fmt not in FORMATS:
|
|
163
|
+
errs.append(f"{where}: `format` {fmt!r} is not one of {', '.join(FORMATS)} — "
|
|
164
|
+
"an unsupported source is migrated or declared as context")
|
|
165
|
+
if fmt == "table":
|
|
166
|
+
has_status, has_default = "statusColumn" in s, "defaultStatus" in s
|
|
167
|
+
if not (_nonempty(s.get("idColumn")) and _nonempty(s.get("titleColumn"))):
|
|
168
|
+
errs.append(f"{where}: a table names non-empty `idColumn` and `titleColumn`")
|
|
169
|
+
if has_status == has_default or (has_status and not _nonempty(s.get("statusColumn"))) \
|
|
170
|
+
or (has_default and s.get("defaultStatus") != "open"):
|
|
171
|
+
errs.append(f"{where}: a table names exactly one of a non-empty "
|
|
172
|
+
"`statusColumn` or `defaultStatus: \"open\"`")
|
|
173
|
+
key = json.dumps([path, s.get("heading", ""), s.get("idPattern", ""), fmt or "",
|
|
174
|
+
s.get("idColumn", ""), s.get("titleColumn", ""),
|
|
175
|
+
s.get("statusColumn", ""), s.get("excludeIds", [])])
|
|
176
|
+
if key in seen:
|
|
177
|
+
errs.append(f"{where}: a duplicate declaration")
|
|
178
|
+
seen.add(key)
|
|
179
|
+
if kind == "tasks":
|
|
180
|
+
tasks.append({k: s[k] for k in ("path", "format", "heading", "idColumn", "titleColumn",
|
|
181
|
+
"statusColumn", "defaultStatus", "idPattern",
|
|
182
|
+
"excludeIds") if k in s})
|
|
183
|
+
elif isinstance(path, str):
|
|
184
|
+
context.append(path)
|
|
185
|
+
errs += _schema_errors(doc)
|
|
186
|
+
if errs:
|
|
187
|
+
return 1, None, list(dict.fromkeys(errs))
|
|
188
|
+
declared = {t["path"] for t in tasks} | set(context)
|
|
189
|
+
undeclared = sorted({f.strip().lstrip("./") for f in found if f.strip()} - declared)
|
|
190
|
+
out = {"repository": doc["repository"], "tasks": tasks, "context": context,
|
|
191
|
+
"undeclared": undeclared}
|
|
192
|
+
return (3 if undeclared else 0), out, [f"finding: {u} holds open work and is not declared "
|
|
193
|
+
f"in {MANIFEST}" for u in undeclared]
|
|
194
|
+
|
|
195
|
+
|
|
196
|
+
def main(argv: list[str]) -> int:
|
|
197
|
+
p = argparse.ArgumentParser(prog="backlog_sources.py",
|
|
198
|
+
description="The declared task sources a wave-0 harvest reads.")
|
|
199
|
+
p.add_argument("--root", default=".", help="the repository root (default: .)")
|
|
200
|
+
p.add_argument("--found", nargs="*", default=[],
|
|
201
|
+
help="sources of open work discovery found, as repository paths")
|
|
202
|
+
a = p.parse_args(argv)
|
|
203
|
+
code, out, notes = check(pathlib.Path(a.root), a.found)
|
|
204
|
+
for n in notes:
|
|
205
|
+
print(f"backlog-sources: {n}", file=sys.stderr)
|
|
206
|
+
if out is not None:
|
|
207
|
+
print(json.dumps(out, ensure_ascii=False))
|
|
208
|
+
return code
|
|
209
|
+
|
|
210
|
+
|
|
211
|
+
if __name__ == "__main__":
|
|
212
|
+
sys.exit(main(sys.argv[1:]))
|
|
@@ -192,9 +192,9 @@ def verify_parents(report, plan):
|
|
|
192
192
|
|
|
193
193
|
|
|
194
194
|
LEAF_SCHEMA = "task-pipeline/execution-packet/1"
|
|
195
|
-
#
|
|
196
|
-
#
|
|
197
|
-
#
|
|
195
|
+
# The namespace separates this packet from other systems using `execution-packet/1`;
|
|
196
|
+
# it does not depend on a claimed Fabric format rename or deployed version.
|
|
197
|
+
# The bare id is still READ, until no live packet carries it:
|
|
198
198
|
# 19 packets in one consumer repository did on 2026-10-12 (backlog B-135); refusing it
|
|
199
199
|
# before they are migrated would break them.
|
|
200
200
|
LEGACY_LEAF_SCHEMAS = ("execution-packet/1",)
|