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.
Files changed (49) hide show
  1. package/CHANGELOG.md +151 -1
  2. package/README.md +42 -9
  3. package/SKILL-CARD.md +1 -1
  4. package/bin/task-pipeline.js +64 -33
  5. package/cursor/rules/task-pipeline.mdc +10 -5
  6. package/evals/RESULTS.md +1 -1
  7. package/evals/task-pipeline.evals.json +16 -0
  8. package/package.json +3 -3
  9. package/plugins/task-pipeline/.claude-plugin/plugin.json +1 -1
  10. package/plugins/task-pipeline/agents/verifier.md +4 -1
  11. package/plugins/task-pipeline/commands/task-pipeline.md +22 -8
  12. package/plugins/task-pipeline/hooks/memory-nudge.sh +45 -3
  13. package/plugins/task-pipeline/skills/evidence-docs/references/gates.md +6 -3
  14. package/plugins/task-pipeline/skills/evidence-docs/references/hooks.md +2 -1
  15. package/plugins/task-pipeline/skills/evidence-docs/references/retrospective.md +26 -1
  16. package/plugins/task-pipeline/skills/task-pipeline/SKILL.md +13 -11
  17. package/plugins/task-pipeline/skills/task-pipeline/graph.schema.json +89 -4
  18. package/plugins/task-pipeline/skills/task-pipeline/pipeline.schema.json +7 -0
  19. package/plugins/task-pipeline/skills/task-pipeline/references/backlog.md +25 -0
  20. package/plugins/task-pipeline/skills/task-pipeline/references/certification.md +45 -1
  21. package/plugins/task-pipeline/skills/task-pipeline/references/continuity.md +95 -26
  22. package/plugins/task-pipeline/skills/task-pipeline/references/deploy-targets.md +30 -4
  23. package/plugins/task-pipeline/skills/task-pipeline/references/doctrine-map.md +1 -0
  24. package/plugins/task-pipeline/skills/task-pipeline/references/gates.md +6 -3
  25. package/plugins/task-pipeline/skills/task-pipeline/references/grill.md +1 -1
  26. package/plugins/task-pipeline/skills/task-pipeline/references/hooks.md +2 -1
  27. package/plugins/task-pipeline/skills/task-pipeline/references/iteration.md +387 -0
  28. package/plugins/task-pipeline/skills/task-pipeline/references/knowledge-sources.md +1 -0
  29. package/plugins/task-pipeline/skills/task-pipeline/references/loop-guard.md +4 -1
  30. package/plugins/task-pipeline/skills/task-pipeline/references/portability.md +1 -0
  31. package/plugins/task-pipeline/skills/task-pipeline/references/retrospective.md +26 -1
  32. package/plugins/task-pipeline/skills/task-pipeline/references/stages.md +35 -11
  33. package/plugins/task-pipeline/skills/task-pipeline/references/waves.md +80 -36
  34. package/plugins/task-pipeline/skills/task-pipeline/references/work-graph.md +54 -5
  35. package/plugins/task-pipeline/skills/task-pipeline/scripts/backlog_sources.py +212 -0
  36. package/plugins/task-pipeline/skills/task-pipeline/scripts/context_packets.py +3 -3
  37. package/plugins/task-pipeline/skills/task-pipeline/scripts/graph.py +205 -22
  38. package/plugins/task-pipeline/skills/task-pipeline/scripts/lessons.py +1137 -0
  39. package/plugins/task-pipeline/skills/task-pipeline/scripts/stage_checkpoint.py +120 -15
  40. package/plugins/task-pipeline/skills/task-pipeline/scripts/waves.py +546 -0
  41. package/plugins/task-pipeline/skills/task-pipeline/templates/README.md +6 -0
  42. package/plugins/task-pipeline/skills/task-pipeline/templates/backlog-sources.schema.json +96 -0
  43. package/plugins/task-pipeline/skills/task-pipeline/templates/canvas.md +97 -0
  44. package/plugins/task-pipeline/skills/task-pipeline/templates/dossier.md +82 -0
  45. package/plugins/task-pipeline/skills/task-pipeline/templates/inventory.schema.json +94 -0
  46. package/plugins/task-pipeline/skills/task-pipeline/templates/lesson.schema.json +221 -0
  47. package/plugins/task-pipeline/skills/task-pipeline/templates/questions.md +45 -0
  48. package/plugins/task-pipeline/skills/task-pipeline/templates/retro.md +4 -0
  49. 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` — the contract is owned by the Fabric workspace's
76
- `knowledge/backlog.md` — the harvest reads exactly the sources it lists, as the board,
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 entry per open item:
84
-
85
- | Field | Carries |
86
- |---|---|
87
- | `id` | the item's id in its owning register; an item without one gets one there, in the same change |
88
- | `source` | where it was found, as `file:line` |
89
- | `status` | as measured, not as the register last said |
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.** A deterministic script
103
- reads the inventory and a short wave table and writes `node`, `wave` and `release` onto
104
- every entry, then prints the counts per wave and per node. A re-cut is then a diff to a
105
- table and a re-run, which a reviewer can read; seven hundred hand-edited rows are not.
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 at a wave boundary, in the
121
- hand-back's DECISIONS WAITING ([`progress.md`](progress.md)).
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
- without a specific standing authorization, sending to a person, spending money,
125
- deleting data: each waits for the operator's explicit go, asked with a verified direct
126
- link. A recommendation is not an authorization.
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)). A genuine ambiguity parks its node `needs-operator`
169
- and the wave goes on with the nodes it does not block; the run stops only when the whole
170
- frontier is blocked. When the board is empty, it says so once.
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
- Every finding is fixed in the wave or parked with a board row and its reason; nothing
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, sending to people, and every outward or irreversible act.
204
- The mode removes *"shall I continue?"* between waves and *"what did you mean?"* where a
205
- source already says; it never removes a person from a decision that is theirs. A
206
- `manual` gate still waits for its go ([`continuity.md`](continuity.md) → *The limit,
207
- before the capability*).
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. Ask for the specific go once, in the batch |
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 done · `4` nothing runnable · `1` with `--wave`, a wave no node is in |
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 `done` 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) |
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
- # Fabric's content-addressed packet was also `execution-packet/1` until Fabric renamed its
196
- # format `session-handoff/1` (fabric-agent-contract R-01). The namespace keeps the two from
197
- # being read as each other. The bare id is still READ, until no live packet carries it:
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",)