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.
- package/CHANGELOG.md +105 -0
- 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 +94 -25
- 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 +76 -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 +54 -13
- 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 +108 -11
- 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
|
@@ -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` (
|
|
8
|
-
|
|
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)),
|
|
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?
|
|
184
|
-
answer and is not re-asked
|
|
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
|
|
308
|
-
|
|
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
|
|
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).
|
|
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**; **
|
|
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` —
|
|
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
|
|
@@ -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
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
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.**
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
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
|
|
125
|
-
|
|
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
|
-
|
|
129
|
-
|
|
130
|
-
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*).
|
|
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)).
|
|
173
|
-
|
|
174
|
-
|
|
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
|
-
|
|
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
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
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.
|
|
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
|
|
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.
|
|
@@ -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
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
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
|
-
#
|
|
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",)
|