task-pipeline-skill 1.93.3 → 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 +105 -0
  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 +94 -25
  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 +76 -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 +54 -13
  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 +108 -11
  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
@@ -4,8 +4,9 @@ For each stage: what it does, what to invoke, artifacts, and the **GATE** that
4
4
  must pass before advancing. Each gate is tagged with its **type** — `auto` (the
5
5
  orchestrator verifies the check itself, pass/fail), `judgment` (a named judge
6
6
  rules where no complete deterministic check exists — [`gates.md`](gates.md) →
7
- *The judgment gate*) or `manual` (wait for the
8
- operator's explicit go). These stages (0 intake + 1→10) are the plugin's
7
+ *The judgment gate*) or `manual` (requires the operator's explicit go, including a
8
+ specific standing authorization naming the act, cited with its source and scope;
9
+ otherwise wait). These stages (0 intake + 1→10) are the plugin's
9
10
  **example** flow, encoded in `pipeline.example.json` against the universal contract
10
11
  `pipeline.schema.json`; a host project replaces it with its own
11
12
  stages/agents/types (see SKILL.md → *Bring your own skills*).
@@ -108,7 +109,10 @@ never that the work was skipped quietly.
108
109
  read **in full** — ten standing rows and ten stamps, both bounded **by a cap**, and they bind this
109
110
  run; stamp each instruction as it fires. Its *Recent log* is **queried** by the
110
111
  task's nouns, not read: uncapped narrative inside a binding source is what makes the
111
- capped part get skimmed, [`retrospective.md`](retrospective.md)), the **knowledge wiki** if one is
112
+ capped part get skimmed, [`retrospective.md`](retrospective.md)), **lessons from other
113
+ projects** (source 7b, [`knowledge-sources.md`](knowledge-sources.md): `scripts/lessons.py
114
+ query` over the local index, then a shared store where the session lists one — each
115
+ layer's state printed in the source ledger), the **knowledge wiki** if one is
112
116
  installed
113
117
  ([obsidian-wiki](https://github.com/ar9av/obsidian-wiki) — recommended,
114
118
  never required), and any **other repo or hosted doc system the project names as
@@ -180,8 +184,10 @@ never that the work was skipped quietly.
180
184
  names the target and the preconditions.
181
185
  - **The run mode is settled here too** ([`continuity.md`](continuity.md)): does
182
186
  the run advance item-by-item with no check-in between items, and on what
183
- interval? Read `pipeline.json` → `run.loop` first — a recorded mode is the
184
- answer and is not re-asked; **absent, it is off**. It buys pacing, never
187
+ interval? Exactly three sources arm it: `pipeline.json` → `run.loop` (read
188
+ first — a recorded mode is the answer and is not re-asked), the operator's answer
189
+ here, or a standing instruction the host already carries. The ledger names which, once:
190
+ `loop: armed by <source:line>`. **Absent all three, it is off.** It buys pacing, never
185
191
  authorization: no loop mode collapses a `manual` gate or an outward act. The
186
192
  same file carries the other half of pacing — the context budget, which fires
187
193
  only on a harness signal or the operator's word, never on an estimate.
@@ -251,7 +257,10 @@ never that the work was skipped quietly.
251
257
  own churn detection mechanical and reads the `touch:` lines, and
252
258
  [`progress.md`](progress.md) derives the rail and the iteration counter from the
253
259
  `stage:` and `iter:` lines. The header goes out before the interview because a run
254
- that announces its position only at the end announced it to nobody.
260
+ that announces its position only at the end announced it to nobody. A new ledger starts a new
261
+ run's memory state: a previous run's "memory unavailable" marker is honoured only while
262
+ its own ledger line is in the ledger, so a reseeded ledger — even the same day — is nudged
263
+ again, and the stale marker is cleared at the next `stage_checkpoint.py record`.
255
264
  - **The board is read, or seeded.** `<artifacts>/backlog.md` ([`backlog.md`](backlog.md)) — its **open count is quoted in the brief**, measured by a command at the top of the run rather than inherited from the last run's report. Absent ⇒ seeded from the template and said so; an empty board and no board are the same thing to work on, and only one of them can be appended to.
256
265
 
257
266
  ## 1 — Docs study
@@ -304,8 +313,11 @@ never that the work was skipped quietly.
304
313
  can name is a node the seam and product tiers cannot scope, so `touches` and `serves` are
305
314
  what make the two outer readings possible at all.
306
315
  - **The queue exists here, so the loop arms here** ([`continuity.md`](continuity.md) →
307
- *Part 1a*). Where `run.loop.arm` is `after-decomposition` and the map holds more than
308
- one module, arm the mode at the close of this stage and print one line: the mode, and
316
+ *Part 1a*). Where the loop is armed — `run.loop` with `arm: after-decomposition`, the
317
+ operator's preflight answer, or a standing instruction the host carries (which arms here
318
+ unless it says otherwise) — and the map holds more than one module, arm the mode at the
319
+ close of this stage, cite the source as `loop: armed by <source:line>`, and print one
320
+ line: the mode, and
309
321
  either the job id and its cancel command (`interval`) or the delay chosen and why
310
322
  (`dynamic`). Arming collapses no gate and authorizes no outward act; it decides only
311
323
  that the run does not stop to ask *"shall I take the next one?"*. Single-module work
@@ -589,7 +601,12 @@ never that the work was skipped quietly.
589
601
  `partial` ships only with the operator's explicit acceptance. A gap is cheapest to
590
602
  close before it ships, and the operator is already present at this gate. **The
591
603
  carry-over count is printed beside this verdict.** Deploy is outward → explicit
592
- operator go — unless the operator already authorised releases. **Before the tag, read
604
+ operator go, or a specific standing authorization naming that act, cited with its source
605
+ and scope. Authorization to release does not silently authorize a separate deploy. If
606
+ `release.authorized_by` is present, read that source and verify its authority,
607
+ currentness and scope for the tag and publication; the locator is not authorization.
608
+ If absent, inspect existing recorded authority before asking for a missing grant.
609
+ **Before the tag, read
593
610
  the release's approval mode** (`pipeline.json` → `release.approval`; then
594
611
  [`deploy-targets.md`](deploy-targets.md) → *When the release waits for an approval*):
595
612
  `checks` — push the tag, watch the run selected by the tag, report; `reviewers` — the
@@ -790,7 +807,11 @@ never that the work was skipped quietly.
790
807
  commit that retired it**), then **write an entry only if
791
808
  the run diverged** (symptom · the stage it surfaced at · the stage that *owned* it
792
809
  · root cause · fix, mechanical before instruction before expiring note · the check
793
- that catches it next time). Every run stamps and prunes; a retro left empty after
810
+ that catches it next time). An entry whose class would be true in another project
811
+ carries a `**Lesson:**` line, and **`scripts/lessons.py add --retro <retro> --entry
812
+ <anchor>` puts it in the local lesson index** — and, where the session lists a shared
813
+ store, `emit-observatory` → the tool → `record` ([`iteration.md`](iteration.md) → *The
814
+ learning loop*). Every run stamps and prunes; a retro left empty after
794
815
  a messy run is the failure the file exists to stop. Stage 0 reads the standing
795
816
  instructions in full next time, which is why the cap is not negotiable.
796
817
  - **GATE (manual):** the ladder walk ran and its absences became REQ rows before
@@ -814,7 +835,10 @@ never that the work was skipped quietly.
814
835
  the same archive, and both counts print beside the verdict** (the stamp table is read
815
836
  in full at stage 0, so *one line per run* is a slope the prune has to stop), the run
816
837
  stamped with its commit, every SHA in either file resolvable, and the
817
- counts printed beside this verdict**; **where `pipeline.json` → `retro.publish` is
838
+ counts printed beside this verdict**; **every entry with a `**Lesson:**` line is in the
839
+ local index (`lessons.py add` printed its id) or its refusal is printed, and the shared
840
+ store's state is one ledger line** (`recorded`, `store refused — <code>`, or `shared
841
+ lessons: absent`); **where `pipeline.json` → `retro.publish` is
818
842
  set, the skill-level insight is published as an issue on the skill's repository —
819
843
  its body printed in full first, the printed string and the sent string being one
820
844
  string, and the five redaction rules applied
@@ -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,8 +72,10 @@ 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
@@ -84,29 +86,37 @@ prints the sources the harvest reads, refuses a manifest the harvest could not f
84
86
  No manifest is discovery, said aloud (exit 0).
85
87
 
86
88
  **One machine-readable inventory, nothing outside it.** A single committed file,
87
- `<artifacts>/specs/YYYY-MM-DD-<topic>-inventory.json`, one entry per open item:
88
-
89
- | Field | Carries |
90
- |---|---|
91
- | `id` | the item's id in its owning register; an item without one gets one there, in the same change |
92
- | `source` | where it was found, as `file:line` |
93
- | `status` | as measured, not as the register last said |
94
- | `area` | the part of the system it touches |
95
- | `node` | the canvas node it lands on |
96
- | `wave` | the wave that closes it, or the release it is carried to |
97
- | `release` | this release, or `program` |
98
-
99
- `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.
100
96
 
101
97
  **Verify each open item against the code before trusting it.** A stale item is closed
102
98
  in its owning register with the receipt that shows it done; duplicates are clustered,
103
99
  both ids kept. A register that says *open* about shipped work is the commonest thing a
104
100
  survey finds.
105
101
 
106
- **Assign nodes and waves with a committed script, not by hand.** A deterministic script
107
- reads the inventory and a short wave table and writes `node`, `wave` and `release` onto
108
- every entry, then prints the counts per wave and per node. A re-cut is then a diff to a
109
- 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).
110
120
 
111
121
  ## The questions — closed from sources, then one batch
112
122
 
@@ -121,13 +131,16 @@ answer and the consequence of each answer, ordered so prerequisite decisions com
121
131
  ([`grill.md`](grill.md)). A go that leaves a question unanswered takes the
122
132
  recommendation, recorded as **assumed** in the decisions table, and every wave that
123
133
  depends on an assumption names it — so a different answer later re-plans exactly those
124
- waves and no others. After planning, a question is asked only at a wave boundary, in the
125
- 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.
126
138
 
127
- **An outward or irreversible act is never assumed.** Publishing, tagging, deploying
128
- without a specific standing authorization, sending to a person, spending money,
129
- deleting data: each waits for the operator's explicit go, asked with a verified direct
130
- 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*).
131
144
 
132
145
  ## The canvas — the target state over the current logic
133
146
 
@@ -169,21 +182,39 @@ marked `needs-operator`, out of every wave until it is unblocked.
169
182
 
170
183
  Each wave runs stages 5–8 as [`stages.md`](stages.md) states them, then the close pass.
171
184
  Between waves the run does not ask whether to continue: it takes the next wave
172
- ([`continuity.md`](continuity.md)). A genuine ambiguity parks its node `needs-operator`
173
- and the wave goes on with the nodes it does not block; the run stops only when the whole
174
- 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*).
175
198
 
176
199
  Research, the audit and the blind readings run in parallel agents; builds stay sequential
177
200
  wherever two of them share a seam ([`planning.md`](planning.md) → file ownership).
178
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
+
179
207
  ## The close pass, after every wave
180
208
 
181
209
  In this order, inside the wave rather than after it:
182
210
 
183
211
  1. **Verify.** Every node the wave closes is certified ([`certification.md`](certification.md)),
212
+ most of it already, as each node landed,
184
213
  and [`audit.md`](audit.md)'s exit is re-measured over what the wave touched.
185
214
  2. **Finish and fix — one fix round**, inside [`loop-guard.md`](loop-guard.md)'s caps.
186
- 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
187
218
  is dropped silently.
188
219
  3. **Document** — the Doc Loop and the stage-9 propagation matrix
189
220
  ([`documentation.md`](documentation.md)), scoped to the wave: scenarios, decisions,
@@ -197,6 +228,9 @@ In this order, inside the wave rather than after it:
197
228
 
198
229
  ## A board bigger than one release
199
230
 
231
+ **The loop around the waves — release, post-release check, next pool, and the next
232
+ iteration — is [`iteration.md`](iteration.md).**
233
+
200
234
  **It becomes a program.** The release takes the first waves; the rest stays on the
201
235
  canvas by node, `release: program`, never dropped. The next release resumes from the
202
236
  canvas: it re-measures the inventory rather than re-running wave 0 from nothing, and its
@@ -204,11 +238,17 @@ waves are cut from the nodes still open.
204
238
 
205
239
  ## What stays human
206
240
 
207
- Sign-ins, consents, payments, sending to people, and every outward or irreversible act.
208
- The mode removes *"shall I continue?"* between waves and *"what did you mean?"* where a
209
- source already says; it never removes a person from a decision that is theirs. A
210
- `manual` gate still waits for its go ([`continuity.md`](continuity.md) → *The limit,
211
- 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.
212
252
 
213
253
  ## The graph carries the wave
214
254
 
@@ -233,5 +273,5 @@ said to be checked by hand.
233
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 |
234
274
  | "The board is too big to inventory" | The inventory is the plan. Without it the waves are cut from memory |
235
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 |
236
- | "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 |
237
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.
@@ -9,11 +9,14 @@ the list the harvest works from.
9
9
 
10
10
  backlog_sources.py [--root DIR] [--found PATH ...]
11
11
 
12
- The contract is not this file's: it is owned by the Fabric workspace's `knowledge/backlog.md`
13
- (section *Source contract*; read at fabric-workspace d49b6f58, 2026-10-08), and its collector
14
- there is the authority. This checker refuses what that contract states in prose, with one
15
- deliberate widening: `repository` may be any `https://github.com/<owner>/<repo>`, because this
16
- skill runs outside one organisation.
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.
17
20
 
18
21
  - `schema` is 1; `repository` is a canonical GitHub URL; `sources` holds 1–100 entries.
19
22
  - Each `path` is relative, inside the repository, with no hidden, `.` or `..` segment and no
@@ -23,6 +26,10 @@ skill runs outside one organisation.
23
26
  `idColumn` and `titleColumn` and exactly one of `statusColumn` or `defaultStatus: "open"`.
24
27
  - `excludeIds` is a unique list of strings; `idPattern` is a literal prefix, optionally with
25
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.
26
33
 
27
34
  Output, on success: one JSON object — `repository`, `tasks` (each declaration as given, the
28
35
  fields the harvest reads), `context` (paths), `undeclared` (each `--found` path the manifest
@@ -42,6 +49,10 @@ import re
42
49
  import sys
43
50
 
44
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")
45
56
  GOALS = ("agent-workplace", "workflow-control", "interoperability", "reliable-work", "shared-context")
46
57
  FORMATS = ("table", "checklist", "json-tasks")
47
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}$")
@@ -71,6 +82,22 @@ def _nonempty(v: object) -> bool:
71
82
  return isinstance(v, str) and bool(v.strip())
72
83
 
73
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
+
74
101
  def check(root: pathlib.Path, found: list[str]) -> tuple[int, dict | None, list[str]]:
75
102
  """(exit code, the harvest list or None, reasons)."""
76
103
  mf = root / MANIFEST
@@ -83,6 +110,8 @@ def check(root: pathlib.Path, found: list[str]) -> tuple[int, dict | None, list[
83
110
  if not isinstance(doc, dict):
84
111
  return 1, None, [f"{MANIFEST}: not an object"]
85
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]
86
115
  if doc.get("schema") != 1:
87
116
  errs.append(f"{MANIFEST}: `schema` is {doc.get('schema')!r}, not 1")
88
117
  if not REPOSITORY.match(str(doc.get("repository") or "")):
@@ -101,6 +130,24 @@ def check(root: pathlib.Path, found: list[str]) -> tuple[int, dict | None, list[
101
130
  continue
102
131
  path, kind, fmt = s.get("path"), s.get("kind", "tasks"), s.get("format")
103
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")
104
151
  if kind not in ("tasks", "context"):
105
152
  errs.append(f"{where}: `kind` {kind!r} is neither tasks nor context")
106
153
  if s.get("goal") not in GOALS:
@@ -123,13 +170,6 @@ def check(root: pathlib.Path, found: list[str]) -> tuple[int, dict | None, list[
123
170
  or (has_default and s.get("defaultStatus") != "open"):
124
171
  errs.append(f"{where}: a table names exactly one of a non-empty "
125
172
  "`statusColumn` or `defaultStatus: \"open\"`")
126
- ex = s.get("excludeIds")
127
- if ex is not None and (not isinstance(ex, list) or not all(_nonempty(x) for x in ex)
128
- or len(set(ex)) != len(ex)):
129
- errs.append(f"{where}: `excludeIds` is a unique list of stable ids")
130
- if "idPattern" in s and not ID_PATTERN.match(str(s.get("idPattern"))):
131
- errs.append(f"{where}: `idPattern` {s.get('idPattern')!r} is not a literal "
132
- "prefix with an optional [0-9]+, anchored at both ends")
133
173
  key = json.dumps([path, s.get("heading", ""), s.get("idPattern", ""), fmt or "",
134
174
  s.get("idColumn", ""), s.get("titleColumn", ""),
135
175
  s.get("statusColumn", ""), s.get("excludeIds", [])])
@@ -142,8 +182,9 @@ def check(root: pathlib.Path, found: list[str]) -> tuple[int, dict | None, list[
142
182
  "excludeIds") if k in s})
143
183
  elif isinstance(path, str):
144
184
  context.append(path)
185
+ errs += _schema_errors(doc)
145
186
  if errs:
146
- return 1, None, errs
187
+ return 1, None, list(dict.fromkeys(errs))
147
188
  declared = {t["path"] for t in tasks} | set(context)
148
189
  undeclared = sorted({f.strip().lstrip("./") for f in found if f.strip()} - declared)
149
190
  out = {"repository": doc["repository"], "tasks": tasks, "context": context,
@@ -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",)