task-pipeline-skill 1.97.1 → 1.99.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +283 -0
- package/HOW-IT-WORKS.md +17 -13
- package/README.md +13 -10
- package/SKILL-CARD.md +1 -1
- package/cursor/rules/task-pipeline.mdc +22 -14
- package/evals/task-pipeline.evals.json +3 -3
- package/package.json +1 -1
- package/plugins/task-pipeline/.claude-plugin/plugin.json +2 -2
- package/plugins/task-pipeline/commands/task-pipeline.md +14 -219
- package/plugins/task-pipeline/skills/evidence-docs/SKILL.md +8 -1
- package/plugins/task-pipeline/skills/evidence-docs/references/GENERATED.md +1 -1
- package/plugins/task-pipeline/skills/evidence-docs/references/learned.md +24 -1
- package/plugins/task-pipeline/skills/evidence-docs/references/retrospective.md +21 -576
- package/plugins/task-pipeline/skills/project-audit/SKILL.md +8 -1
- package/plugins/task-pipeline/skills/task-pipeline/SKILL.md +162 -230
- package/plugins/task-pipeline/skills/task-pipeline/graph.schema.json +1 -1
- package/plugins/task-pipeline/skills/task-pipeline/pipeline.example.json +9 -9
- package/plugins/task-pipeline/skills/task-pipeline/pipeline.schema.json +11 -4
- package/plugins/task-pipeline/skills/task-pipeline/references/acceptance.md +2 -2
- package/plugins/task-pipeline/skills/task-pipeline/references/brainstorm.md +3 -3
- package/plugins/task-pipeline/skills/task-pipeline/references/build.md +22 -7
- package/plugins/task-pipeline/skills/task-pipeline/references/certification.md +49 -44
- package/plugins/task-pipeline/skills/task-pipeline/references/continuity.md +6 -3
- package/plugins/task-pipeline/skills/task-pipeline/references/doctrine-map.md +1 -1
- package/plugins/task-pipeline/skills/task-pipeline/references/finish-line.md +13 -8
- package/plugins/task-pipeline/skills/task-pipeline/references/iteration.md +188 -32
- package/plugins/task-pipeline/skills/task-pipeline/references/knowledge-sources.md +5 -3
- package/plugins/task-pipeline/skills/task-pipeline/references/learned.md +24 -1
- package/plugins/task-pipeline/skills/task-pipeline/references/loop-guard.md +17 -7
- package/plugins/task-pipeline/skills/task-pipeline/references/retrospective.md +49 -2
- package/plugins/task-pipeline/skills/task-pipeline/references/spec.md +5 -4
- package/plugins/task-pipeline/skills/task-pipeline/references/stage-00-intake.md +39 -25
- package/plugins/task-pipeline/skills/task-pipeline/references/stage-05-dev.md +1 -1
- package/plugins/task-pipeline/skills/task-pipeline/references/stages.md +36 -28
- package/plugins/task-pipeline/skills/task-pipeline/references/waves.md +8 -5
- package/plugins/task-pipeline/skills/task-pipeline/scripts/graph.py +79 -22
- package/plugins/task-pipeline/skills/task-pipeline/scripts/lessons.py +1229 -110
- package/plugins/task-pipeline/skills/task-pipeline/scripts/retro_budget.py +305 -0
- package/plugins/task-pipeline/skills/task-pipeline/scripts/short_path.py +70 -0
- package/plugins/task-pipeline/skills/task-pipeline/scripts/waves.py +22 -10
- package/plugins/task-pipeline/skills/task-pipeline/templates/canvas.md +1 -1
- package/plugins/task-pipeline/skills/task-pipeline/templates/lesson.schema.json +38 -10
- package/plugins/task-pipeline/skills/task-pipeline/templates/retro-archive.md +9 -0
- package/plugins/task-pipeline/skills/task-pipeline/templates/retro.md +10 -1
|
@@ -7,13 +7,13 @@ The full gate and freedom level remain in [stages.md](stages.md).
|
|
|
7
7
|
- 0 — Intake grill — MANDATORY
|
|
8
8
|
|
|
9
9
|
## 0 — Intake grill — MANDATORY
|
|
10
|
-
- **Stage 0
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
10
|
+
- **Stage 0 always runs, and its depth is proportional.** The gap check, the
|
|
11
|
+
harvest and the brief are never skipped; the interview, the committed brief file
|
|
12
|
+
and stages 1-4 are what a small change does without (Phase 1d below, the
|
|
13
|
+
*proportional run*). A run that starts building without a brief and its REQ rows
|
|
14
|
+
(in the brief file, or on the short path in the run ledger) is a **failed run**:
|
|
15
|
+
stop and go back. The entry-from-super-ux short-circuit below is the other
|
|
16
|
+
sanctioned shortening.
|
|
17
17
|
- **Entry-from-super-ux short-circuit (check FIRST).** task-pipeline is often
|
|
18
18
|
launched *from* super-ux — its `/ux` action menu offers "execute autonomously
|
|
19
19
|
via the task-pipeline plugin" once the UX chain (and often a
|
|
@@ -40,8 +40,9 @@ The full gate and freedom level remain in [stages.md](stages.md).
|
|
|
40
40
|
`affected` / `god-nodes` answer *reach*, which is what grep cannot), `CLAUDE.md`,
|
|
41
41
|
`CONTEXT.md`/ADRs, `docs/` + `docs/ux/`, past pipeline briefs and carry-over
|
|
42
42
|
ledgers, **the retro's standing instructions and run stamps** (`<artifacts>/retro.md`,
|
|
43
|
-
read **in full
|
|
44
|
-
|
|
43
|
+
read **in full**: ten standing rows and ten stamps, bounded by a cap and by a byte
|
|
44
|
+
budget; print `scripts/retro_budget.py check` first, which states the floor's bytes and
|
|
45
|
+
bytes/4 tokens. They bind this run; stamp each instruction as it fires. Its *Recent log* is **queried** by the
|
|
45
46
|
task's nouns, not read: uncapped narrative inside a binding source is what makes the
|
|
46
47
|
capped part get skimmed, [`retrospective.md`](retrospective.md)), **lessons from other
|
|
47
48
|
projects** (source 7b, [`knowledge-sources.md`](knowledge-sources.md): `scripts/lessons.py
|
|
@@ -81,30 +82,43 @@ The full gate and freedom level remain in [stages.md](stages.md).
|
|
|
81
82
|
or they genuinely disagree and that is a decision. There is no fourth option, and
|
|
82
83
|
starting on an unresolved divergence means building against a system that does
|
|
83
84
|
not exist.
|
|
84
|
-
- **Phase 1d — the short-path triage
|
|
85
|
-
pipeline's
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
85
|
+
- **Phase 1d — the short-path triage, and the proportional run: a small change
|
|
86
|
+
takes the short path by default.** This pipeline's boundary exempts a typo and a one-line edit; until 1.98.0
|
|
87
|
+
the next size up still met eleven stages and five manual gates, because the short
|
|
88
|
+
path was only *proposed* and silence took the full flow. Under a standing "decide and
|
|
89
|
+
act" instruction silence is the normal answer, so a proposal was the full flow every
|
|
90
|
+
time. The triage now **decides**, from five answers with something behind each:
|
|
89
91
|
|
|
90
92
|
```
|
|
91
|
-
1. files the
|
|
93
|
+
1. files the change touches, resolved git ls-files -- <paths> -> N
|
|
92
94
|
2. any of them a public contract the surfaces the host's release
|
|
93
95
|
syncs, its schema or config
|
|
94
96
|
contracts, a shipped command,
|
|
95
97
|
its README -> yes/no
|
|
96
|
-
3. behaviour a user
|
|
98
|
+
3. behaviour a user observes changes a UI, a message, an output -> yes/no
|
|
99
|
+
4. a deploy target in the brief stage 7's target exists -> yes/no
|
|
100
|
+
5. risk class work-graph.md R1-R3 -> R1/R2/R3
|
|
97
101
|
```
|
|
98
102
|
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
103
|
+
**Small** means all of: the gap check left zero material unknowns, N ≤ 5, no public
|
|
104
|
+
contract, no user-observable change, no deploy, and risk **R1**. Small ⇒ the run takes
|
|
105
|
+
the short path and prints one line:
|
|
106
|
+
`short path: R1 · N files · no contract · no user-facing change · no deploy`.
|
|
107
|
+
`scripts/short_path.py` computes the decision from the five answers (exit 0 short,
|
|
108
|
+
3 full) and prints that line.
|
|
109
|
+
The short path is: the brief and its REQ rows written into the run ledger (no grill,
|
|
110
|
+
no committed brief file) → stage 5, build with tests → stage 6, the suite plus **one**
|
|
111
|
+
verification (one blind tier, [`certification.md`](certification.md)) → stage 9, the
|
|
112
|
+
docs the change makes false and the handoff → stage 10, the hand-back and a run stamp;
|
|
113
|
+
a retro entry only when the run taught a new lesson. Stages 1-4, 7 and 8 print `⊘`
|
|
114
|
+
with the triage line as their reason ([`progress.md`](progress.md)).
|
|
115
|
+
**Triggers that bring a skipped stage back:** a deploy target appears (7, 8); a wiki or
|
|
116
|
+
code graph exists and the change touched what it indexes (9 in full); a check reveals a
|
|
117
|
+
contract or a user-facing effect (leave the short path, resume at stage 2); the
|
|
118
|
+
operator asks for the full cycle. Any answer outside *small* takes the full flow,
|
|
119
|
+
printed the same way with the failing answer as its reason.
|
|
120
|
+
- **How it runs: [`grill.md`](grill.md)**, read only when the gap check leaves a
|
|
121
|
+
material unknown (a complete brief asks nothing). In short: close what the sources can, then ONE batch
|
|
108
122
|
of the material unknowns, ordered by dependency, a recommended answer with each —
|
|
109
123
|
one at a time only where answers depend on each other or the operator asks;
|
|
110
124
|
contradictions reconciled on the spot; **every answer that touches
|
|
@@ -7,7 +7,7 @@ The full gate and freedom level remain in [stages.md](stages.md).
|
|
|
7
7
|
workspace (native worktree tool first, git fallback, baseline tests), keep a
|
|
8
8
|
ledger under `.task-pipeline/build/<plan>/` so a compacted context can resume,
|
|
9
9
|
then one fresh implementer subagent per task with a file-based brief and report,
|
|
10
|
-
a review after every task ([`review.md`](review.md)), and a
|
|
10
|
+
a review after every task ([`review.md`](review.md)), and a three-round fix loop
|
|
11
11
|
with an explicit breaker. TDD per task ([`tdd.md`](tdd.md)): failing test →
|
|
12
12
|
watch it fail → minimal impl → watch it pass → commit. Pin subagents to the
|
|
13
13
|
run's confirmed model (`model-tiering.md`). The plan's parallel groups fan out
|
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
# Stages — detail & gates
|
|
2
2
|
|
|
3
|
-
For the selected **default profile**, read
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
3
|
+
For the selected **default profile**, read **one stage section of this file when that
|
|
4
|
+
stage starts** (the stage table in `SKILL.md` is the index), then the one detail it
|
|
5
|
+
links, on every entry or resume, including wave iterations and approved short paths
|
|
6
|
+
(the triage approves the short path itself since 1.98.0). Never read this file end to
|
|
7
|
+
end. Do not preload all stage details. Missing or unreadable detail means stop that stage and repair its route, never guess. A custom
|
|
7
8
|
profile uses its own declared doctrine and stages; numeric ids do not select these
|
|
8
9
|
default details, and the kernel still applies.
|
|
9
10
|
|
|
@@ -14,7 +15,10 @@ orchestrator verifies the check itself, pass/fail), `judgment` (a named judge
|
|
|
14
15
|
rules where no complete deterministic check exists — [`gates.md`](gates.md) →
|
|
15
16
|
*The judgment gate*) or `manual` (requires the operator's explicit go, including a
|
|
16
17
|
specific standing authorization naming the act, cited with its source and scope;
|
|
17
|
-
otherwise wait).
|
|
18
|
+
otherwise wait). **This example ships every gate `auto`** (since 1.98.0): where it
|
|
19
|
+
names an approval, the operator's answer or the standing authorization recorded in the
|
|
20
|
+
brief satisfies it without a pause. A project makes a gate `manual` in its own
|
|
21
|
+
`pipeline.json`. These stages (0 intake + 1→10) are the plugin's
|
|
18
22
|
**example** flow, encoded in `pipeline.example.json` against the universal contract
|
|
19
23
|
`pipeline.schema.json`; a host project replaces it with its own
|
|
20
24
|
stages/agents/types (see SKILL.md → *Bring your own skills*).
|
|
@@ -84,7 +88,11 @@ never that the work was skipped quietly.
|
|
|
84
88
|
|
|
85
89
|
**On entry or resume:** read [stage-00-intake.md](stage-00-intake.md) before stage work.
|
|
86
90
|
|
|
87
|
-
- **
|
|
91
|
+
- **On the short path** (`stage-00-intake.md` → Phase 1d) this gate
|
|
92
|
+
is: the triage line printed, the brief and its REQ rows in the run ledger, the retro
|
|
93
|
+
floor read with its byte count, and the source ledger. Everything else below applies
|
|
94
|
+
to the full flow.
|
|
95
|
+
- **GATE (auto):** shared understanding reached — **the source ledger is written
|
|
88
96
|
(every source consulted, or an explicit "none found")** and **its `Contradictions:`
|
|
89
97
|
line is written under it** — the harvest is a fan-out that converges on one brief, and
|
|
90
98
|
nothing else compares the sources *with each other*; `Contradictions: none` is the
|
|
@@ -107,8 +115,8 @@ never that the work was skipped quietly.
|
|
|
107
115
|
is filled — objective, exit checks on the artefact the operator uses, scope, budget and
|
|
108
116
|
base** ([`finish-line.md`](finish-line.md)), the carry-over ledger is
|
|
109
117
|
seeded, **`.task-pipeline/run.md` exists and the header block has been printed**
|
|
110
|
-
([`progress.md`](progress.md)), the model decision is recorded, and the
|
|
111
|
-
|
|
118
|
+
([`progress.md`](progress.md)), the model decision is recorded, and the brief is
|
|
119
|
+
confirmed: the operator's answer, or the standing authorization recorded in it. Stop when a
|
|
112
120
|
re-scan surfaces no new branches (don't grill past diminishing returns;
|
|
113
121
|
reversible calls can be deferred with a note). Only then start stage 1.
|
|
114
122
|
- **The verification ledger is read.** `<artifacts>/verification.md` — the harvest
|
|
@@ -140,7 +148,8 @@ never that the work was skipped quietly.
|
|
|
140
148
|
|
|
141
149
|
**On entry or resume:** read [stage-02-brainstorm.md](stage-02-brainstorm.md) before stage work.
|
|
142
150
|
|
|
143
|
-
- **GATE (
|
|
151
|
+
- **GATE (auto):** the design is approved (the operator's go, or the brief's standing
|
|
152
|
+
authorization), the UI verdict is recorded, **the
|
|
144
153
|
queue is an artifact rather than a recollection** — where it is a work graph,
|
|
145
154
|
`graph.py validate` exits 0 and `graph.py coverage` names any requirement no node serves,
|
|
146
155
|
**every REQ is answered by the design** — a requirement the design doesn't
|
|
@@ -156,10 +165,11 @@ never that the work was skipped quietly.
|
|
|
156
165
|
|
|
157
166
|
**On entry or resume:** read [stage-03-spec.md](stage-03-spec.md) before stage work.
|
|
158
167
|
|
|
159
|
-
- **GATE (
|
|
168
|
+
- **GATE (auto):** the **`## Self-review` section written and committed with
|
|
160
169
|
computed values** — every check the spec names resolving or marked `review`, the
|
|
161
170
|
brief's decisions and stage 2's rejected options read back with no unresolved
|
|
162
|
-
contradiction, and the cost delta printed; spec committed **and**
|
|
171
|
+
contradiction, and the cost delta printed; spec committed **and** reviewed (by the
|
|
172
|
+
operator, or under the brief's standing authorization by an independent reader); **every section carries
|
|
163
173
|
`covers: REQ-…` and every REQ appears in at least one section**; for UI tasks
|
|
164
174
|
additionally: the super-ux chain (foundation → flows → screens → scenarios) is
|
|
165
175
|
designed, validated and approved; scenarios validated in `docs/ux/scenarios.md`;
|
|
@@ -299,20 +309,17 @@ never that the work was skipped quietly.
|
|
|
299
309
|
|
|
300
310
|
**On entry or resume:** read [stage-07-lint-deploy.md](stage-07-lint-deploy.md) before stage work.
|
|
301
311
|
|
|
302
|
-
- **GATE (
|
|
312
|
+
- **GATE (auto):** lint clean (host linter **and**, for UI projects, the super-ux
|
|
303
313
|
linter) **and** suite green **before** deploy, **and no REQ is still `open`** — a
|
|
304
314
|
`partial` ships only with the operator's explicit acceptance. A gap is cheapest to
|
|
305
|
-
close before it ships
|
|
315
|
+
close before it ships. **The
|
|
306
316
|
carry-over count is printed beside this verdict.** Deploy is outward → explicit
|
|
307
317
|
operator go, or a specific standing authorization naming that act, cited with its source
|
|
308
|
-
and scope.
|
|
309
|
-
`
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
the release's approval mode** (`pipeline.json` → `release.approval`; then
|
|
314
|
-
[`deploy-targets.md`](deploy-targets.md) → *When the release waits for an approval*):
|
|
315
|
-
`checks` — push the tag, watch the run selected by the tag, report; `reviewers` — the
|
|
318
|
+
and scope. How a release's authority is established, `release.authorized_by` included,
|
|
319
|
+
has one home: [`deploy-targets.md`](deploy-targets.md) → *When the release waits for an
|
|
320
|
+
approval*. **Before the tag, read the release's approval mode** (`pipeline.json` →
|
|
321
|
+
`release.approval`; the same section): `checks` — push the tag, put the run selected by
|
|
322
|
+
the tag on a watcher and take independent work until it reports; `reviewers` — the
|
|
316
323
|
direct link to the run that is actually `waiting`, never a request without it. Respect deploy-from-main rules if the project mandates them. **Before
|
|
317
324
|
tagging, the CI verdict for what was just pushed is READ, not assumed**
|
|
318
325
|
([`conventions.md`](conventions.md) → *The CI verdict*) — a tag on a commit whose
|
|
@@ -376,7 +383,7 @@ never that the work was skipped quietly.
|
|
|
376
383
|
|
|
377
384
|
**On entry or resume:** read [stage-10-acceptance.md](stage-10-acceptance.md) before stage work.
|
|
378
385
|
|
|
379
|
-
- **GATE (
|
|
386
|
+
- **GATE (auto):** the ladder walk ran and its absences became REQ rows before
|
|
380
387
|
the table was written; **the hand-back is written** — the request quoted as it was
|
|
381
388
|
GIVEN, where the run stands against it, what was solved with its evidence, what
|
|
382
389
|
surfaced that nobody asked for, every decision still waiting **asked here with
|
|
@@ -422,10 +429,11 @@ never that the work was skipped quietly.
|
|
|
422
429
|
carries evidence; every `partial` names what's missing and where it's tracked;
|
|
423
430
|
every `deferred`/`dropped` has the operator's agreement and, for `deferred`, a
|
|
424
431
|
tracker entry; no carry-over row left `unresolved`, and the ledger's counts are
|
|
425
|
-
printed with the verdict; the
|
|
426
|
-
|
|
427
|
-
the table
|
|
428
|
-
asked for.
|
|
432
|
+
printed with the verdict; the hand-back asks the closing question and carries
|
|
433
|
+
the answer the operator can give at any time. `auto` by default since 1.98.0: a
|
|
434
|
+
check proves the table well-formed, and only the person who asked can confirm it is
|
|
435
|
+
what they asked for, so the run hands that question back rather than waiting on it.
|
|
436
|
+
A project that wants the wait sets this gate `manual`.
|
|
429
437
|
|
|
430
438
|
## The program loop — a platform, one brick at a time
|
|
431
439
|
|
|
@@ -486,8 +494,8 @@ cycle.
|
|
|
486
494
|
a feeling, and the ledger is what survives compaction.
|
|
487
495
|
- **Trips on:** revert-oscillation (A→B→A); the same file edited twice for the same
|
|
488
496
|
reason; a finding already ADDRESSED or parked coming back; a stage entered a third
|
|
489
|
-
time for one artifact; two loops editing one file. Hard caps:
|
|
490
|
-
task, 2 re-entries per stage per artifact, 3 passes per module.
|
|
497
|
+
time for one artifact; two loops editing one file. Hard caps, each a stop: 3 fix
|
|
498
|
+
rounds per task, 2 re-entries per stage per artifact, 3 passes per module.
|
|
491
499
|
- **On a trip: stop editing.** Name shapes A and B with their evidence, escalate to
|
|
492
500
|
the layer that owns the conflict (rubric → operator → plan → spec → module map),
|
|
493
501
|
re-plan the check as an ordered one-item-per-line checklist, then go through it in
|
|
@@ -57,10 +57,12 @@ on real agents, tasks and users rather than on the first guess.
|
|
|
57
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
|
-
**One planning gate, not three prompts.** The
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
60
|
+
**One planning gate, not three prompts.** The gates of stages 0, 2 and 3 are presented
|
|
61
|
+
together at the end of planning: the brief, the canvas, the wave plan and the question
|
|
62
|
+
batch in one message. Each gate's criteria are still checked and printed. They are `auto`
|
|
63
|
+
by default (since 1.98.0): the standing authorization recorded in the brief closes them
|
|
64
|
+
and the run continues; where the project made them `manual`, the operator is asked once,
|
|
65
|
+
the go names the gates it closes, and silence closes none.
|
|
64
66
|
|
|
65
67
|
## Wave 0 — the survey and the inventory
|
|
66
68
|
|
|
@@ -261,7 +263,8 @@ The work graph ([`work-graph.md`](work-graph.md)) carries `wave` on a node, an i
|
|
|
261
263
|
- `graph.py validate` refuses an edge from a later wave's node into an earlier wave's
|
|
262
264
|
node: an earlier wave that waits on a later one cannot close in its own pass.
|
|
263
265
|
- `graph.py next --wave N` prints wave N's frontier only. Exit `3` says the wave is done,
|
|
264
|
-
`4` that nothing in it is runnable
|
|
266
|
+
`4` that nothing in it is runnable behind a mixed or unclassified barrier, `6` that only
|
|
267
|
+
recorded watched waits hold it, `1` that no node is in wave N.
|
|
265
268
|
- `graph.py wave N-007 --to 3 --why "<reason>"` moves a node and writes the revision.
|
|
266
269
|
It refuses a move that would break the order above.
|
|
267
270
|
|
|
@@ -31,10 +31,12 @@ conditional on the code, never merely sequenced after it):
|
|
|
31
31
|
0 the verb succeeded; for `next`, runnable nodes were printed
|
|
32
32
|
1 a refusal — an invariant violated, a malformed verdict, a missing reason
|
|
33
33
|
2 usage
|
|
34
|
-
3 nothing left to do: every node is done
|
|
34
|
+
3 nothing left to do: every node in scope is terminal — `done` or `parked`
|
|
35
|
+
(a parked node is finished for the walk; its reason stays on the node)
|
|
35
36
|
4 nothing runnable: an unclassified, running or parked dependency remains
|
|
36
37
|
5 external execution lease contention
|
|
37
38
|
6 nothing runnable: all remaining dependency leaves are recorded watched waits
|
|
39
|
+
7 `certify` stopped at its ceiling: the node is parked with the evidence
|
|
38
40
|
|
|
39
41
|
**The mutation verbs never leave a half-written queue.** `add` and `park` write to a
|
|
40
42
|
temp file in the same directory and `os.replace` it into place, so a crash mid-write
|
|
@@ -654,10 +656,25 @@ ALL_TIERS = TIERS + (VISUAL_TIER,)
|
|
|
654
656
|
SURFACE_CLASSES = ("flagship", "product", "internal", "ad")
|
|
655
657
|
RISK_CLASSES = ("R1", "R2", "R3")
|
|
656
658
|
# Passing certification rounds a node owes before `close` accepts it, by risk class
|
|
657
|
-
# (`references/certification.md` → *Rounds owed by risk class*).
|
|
658
|
-
# round
|
|
659
|
-
|
|
659
|
+
# (`references/certification.md` → *Rounds owed by risk class*). A clean round ends the
|
|
660
|
+
# node unless it is R3 or a flagship surface: those owe a second passing round, and it is
|
|
661
|
+
# the adversarial one, a different question rather than the same reading again. R2 owed
|
|
662
|
+
# two blind rounds until 1.98.0, and a clean round did not cancel the next one: the loop
|
|
663
|
+
# the 2026-10-20 cost audit measured.
|
|
664
|
+
ROUNDS_OWED = {"R1": 1, "R2": 1, "R3": 2}
|
|
660
665
|
ROUND_KINDS = ("blind", "adversarial")
|
|
666
|
+
# An R1 node certifies with ONE blind tier (any of the three; `unit` is the default
|
|
667
|
+
# reading). Every other node owes all three, and a flagship or product surface the fourth.
|
|
668
|
+
ONE_TIER_RISK = ("R1",)
|
|
669
|
+
|
|
670
|
+
|
|
671
|
+
def owed_rounds(node):
|
|
672
|
+
"""(rounds owed, adversarial owed) for a node, or (0, False) when it owes no count."""
|
|
673
|
+
owed = ROUNDS_OWED.get(node.get("risk"), 0)
|
|
674
|
+
adversarial = node.get("risk") == "R3" or node.get("surface_class") == "flagship"
|
|
675
|
+
if node.get("surface_class") == "flagship":
|
|
676
|
+
owed = max(owed, 2)
|
|
677
|
+
return owed, adversarial
|
|
661
678
|
|
|
662
679
|
|
|
663
680
|
def passing_rounds(cert):
|
|
@@ -675,13 +692,13 @@ def passing_rounds(cert):
|
|
|
675
692
|
|
|
676
693
|
def owed_shortfall(node):
|
|
677
694
|
"""None when the node's class is satisfied, else a sentence saying what it still owes."""
|
|
678
|
-
owed =
|
|
695
|
+
owed, adversarial = owed_rounds(node)
|
|
679
696
|
if not owed:
|
|
680
697
|
return None
|
|
681
698
|
passed = passing_rounds(node.get("certification"))
|
|
682
699
|
if len(passed) < owed:
|
|
683
700
|
return "%d of %d owed passing rounds" % (len(passed), owed)
|
|
684
|
-
if
|
|
701
|
+
if adversarial and "adversarial" not in passed:
|
|
685
702
|
cert = node.get("certification") or {}
|
|
686
703
|
kinds = list(cert.get("kinds") or [])
|
|
687
704
|
failed_adv = any(k == "adversarial" and not all(v == "pass" for v in (h or {}).values())
|
|
@@ -1661,10 +1678,14 @@ def cmd_certify(graph, args):
|
|
|
1661
1678
|
the number the ceiling below reads. The node stays `pending` on a failure; the
|
|
1662
1679
|
round count is the trail.
|
|
1663
1680
|
|
|
1664
|
-
**The ceiling
|
|
1665
|
-
|
|
1666
|
-
|
|
1667
|
-
|
|
1681
|
+
**The ceiling stops** (`references/loop-guard.md`, since 1.98.0). A round that fails
|
|
1682
|
+
with the node's failing rounds at the ceiling parks the node with the evidence as its
|
|
1683
|
+
reason (exit 7): the run records a blocker row and moves on. It used to measure
|
|
1684
|
+
only, and a measured loop is still a loop.
|
|
1685
|
+
|
|
1686
|
+
**No re-run on unchanged input.** A round of the same kind on the same tree as the
|
|
1687
|
+
round before it is refused before anything is written: a tier's answer about a tree
|
|
1688
|
+
it already read is the answer it gave.
|
|
1668
1689
|
"""
|
|
1669
1690
|
guard(graph, args.graph)
|
|
1670
1691
|
|
|
@@ -1721,10 +1742,14 @@ def cmd_certify(graph, args):
|
|
|
1721
1742
|
die("the tier reports are malformed — nothing was written:\n " + "\n ".join(bad))
|
|
1722
1743
|
|
|
1723
1744
|
missing = [x for x in TIERS if x not in reports]
|
|
1724
|
-
if
|
|
1745
|
+
if node.get("risk") in ONE_TIER_RISK:
|
|
1746
|
+
if len(missing) == len(TIERS):
|
|
1747
|
+
die("certification of an %s node needs one blind tier report (unit, seam or "
|
|
1748
|
+
"product); none was given" % node.get("risk"))
|
|
1749
|
+
elif missing:
|
|
1725
1750
|
die("certification is missing the %s report(s) — all three are required, because "
|
|
1726
|
-
"the level nobody read is the level the defect survives at"
|
|
1727
|
-
% ", ".join("`%s`" % m for m in missing))
|
|
1751
|
+
"the level nobody read is the level the defect survives at (an R1 node "
|
|
1752
|
+
"certifies with one)" % ", ".join("`%s`" % m for m in missing))
|
|
1728
1753
|
sclass = node.get("surface_class")
|
|
1729
1754
|
if sclass in VISUAL_REQUIRED and VISUAL_TIER not in reports:
|
|
1730
1755
|
die("certification is missing the `%s` report — %s is a %s surface, and on one the "
|
|
@@ -1743,6 +1768,18 @@ def cmd_certify(graph, args):
|
|
|
1743
1768
|
head = ""
|
|
1744
1769
|
|
|
1745
1770
|
prior = node.get("certification") or {}
|
|
1771
|
+
try:
|
|
1772
|
+
_t = subprocess.run(["git", "rev-parse", "HEAD^{tree}"], capture_output=True, text=True)
|
|
1773
|
+
tree = _t.stdout.strip() if _t.returncode == 0 else ""
|
|
1774
|
+
except OSError:
|
|
1775
|
+
tree = ""
|
|
1776
|
+
prior_kinds = list(prior.get("kinds") or [])
|
|
1777
|
+
if (tree and prior.get("tree") == tree
|
|
1778
|
+
and (prior_kinds[-1] if prior_kinds else "blind") == args.kind):
|
|
1779
|
+
die("%s: round %d already read tree %s as a %s round: re-running a tier on "
|
|
1780
|
+
"unchanged input repeats its answer. Change the code, run the other kind of round, "
|
|
1781
|
+
"or record a blocker (references/loop-guard.md). Nothing was written."
|
|
1782
|
+
% (nid, int(prior.get("round") or 0), tree[:12], args.kind))
|
|
1746
1783
|
round_no = int(prior.get("round") or 0) + 1
|
|
1747
1784
|
tiers_now = {x: reports[x]["verdict"] for x in tiers}
|
|
1748
1785
|
history = list(prior.get("history") or []) + [tiers_now]
|
|
@@ -1754,6 +1791,8 @@ def cmd_certify(graph, args):
|
|
|
1754
1791
|
"history": history,
|
|
1755
1792
|
"kinds": kinds,
|
|
1756
1793
|
}
|
|
1794
|
+
if tree:
|
|
1795
|
+
node["certification"]["tree"] = tree
|
|
1757
1796
|
|
|
1758
1797
|
failed = [x for x in tiers if tiers_now[x] == "fail"]
|
|
1759
1798
|
|
|
@@ -1761,6 +1800,20 @@ def cmd_certify(graph, args):
|
|
|
1761
1800
|
# operator needs named; counting it here is what makes the loop visible.
|
|
1762
1801
|
churning = [x for x in tiers
|
|
1763
1802
|
if len(history) >= 2 and all(h.get(x) == "fail" for h in history)]
|
|
1803
|
+
failed_rounds = sum(1 for h in history if any(v == "fail" for v in h.values()))
|
|
1804
|
+
stop = bool(failed) and failed_rounds >= args.ceiling
|
|
1805
|
+
if stop:
|
|
1806
|
+
first = next((f for x in failed for f in reports[x]["findings"]
|
|
1807
|
+
if f.get("severity") == "breaks"), {})
|
|
1808
|
+
reason = ("certification ceiling: %d failing round(s), ceiling %d; failing tier(s) %s; "
|
|
1809
|
+
"%s; first break %s: %s; check: %s. Blocker row owed on the board"
|
|
1810
|
+
% (failed_rounds, args.ceiling, ", ".join(failed),
|
|
1811
|
+
("stuck on " + ", ".join(churning)) if churning else "churn across levels",
|
|
1812
|
+
first.get("where", "?"), first.get("what", "?"), first.get("check", "?")))
|
|
1813
|
+
reason = " ".join(reason.split())
|
|
1814
|
+
node["status"] = "parked"
|
|
1815
|
+
node["parked_reason"] = reason
|
|
1816
|
+
revise(graph, "park", nid, reason)
|
|
1764
1817
|
|
|
1765
1818
|
save(args.graph, graph)
|
|
1766
1819
|
|
|
@@ -1774,9 +1827,10 @@ def cmd_certify(graph, args):
|
|
|
1774
1827
|
print(" [%s] %s — %s" % (tier, f["where"], f["what"]), file=sys.stderr)
|
|
1775
1828
|
print(" fix: %s" % f.get("fix", "(not stated)"), file=sys.stderr)
|
|
1776
1829
|
print(" check: %s" % f["check"], file=sys.stderr)
|
|
1777
|
-
if
|
|
1778
|
-
print("\n%s has
|
|
1779
|
-
|
|
1830
|
+
if stop:
|
|
1831
|
+
print("\n%s has failed %d round(s), at the ceiling of %d. STOPPED: the node is "
|
|
1832
|
+
"parked with this evidence as its reason; record a blocker row and move on."
|
|
1833
|
+
% (nid, failed_rounds, args.ceiling), file=sys.stderr)
|
|
1780
1834
|
if churning:
|
|
1781
1835
|
print("The same tier has failed every round: %s. That is not a fix "
|
|
1782
1836
|
"away — the level itself is being misread, or the node is the "
|
|
@@ -1785,6 +1839,8 @@ def cmd_certify(graph, args):
|
|
|
1785
1839
|
else:
|
|
1786
1840
|
print("No single tier is failing every round, so this is churn across "
|
|
1787
1841
|
"levels rather than one stuck level.", file=sys.stderr)
|
|
1842
|
+
if stop:
|
|
1843
|
+
return 7
|
|
1788
1844
|
print("\nThe node stays open. Round %d is recorded on it." % round_no,
|
|
1789
1845
|
file=sys.stderr)
|
|
1790
1846
|
return 1
|
|
@@ -1843,7 +1899,7 @@ def cmd_certify(graph, args):
|
|
|
1843
1899
|
fh.write("\n")
|
|
1844
1900
|
os.replace(tmp, out)
|
|
1845
1901
|
print("%s: certified at %s in round %d" % (nid, ", ".join(tiers), round_no))
|
|
1846
|
-
owed =
|
|
1902
|
+
owed, _adv = owed_rounds(node)
|
|
1847
1903
|
short = owed_shortfall(node)
|
|
1848
1904
|
if short:
|
|
1849
1905
|
print("round %d: %s — not closable yet. The next round reads the node's whole diff at "
|
|
@@ -1964,7 +2020,7 @@ def cmd_close(graph, args):
|
|
|
1964
2020
|
if short and not exc:
|
|
1965
2021
|
die("%s is %s: %s — not closable yet. Run the next round with `graph.py certify`, "
|
|
1966
2022
|
"or record an authorized exception with `waive`. Nothing was written."
|
|
1967
|
-
% (nid, node
|
|
2023
|
+
% (nid, node.get("risk") or "a %s surface" % node.get("surface_class"), short))
|
|
1968
2024
|
if cert:
|
|
1969
2025
|
tiers = cert.get("tiers") or {}
|
|
1970
2026
|
failing = sorted(k for k, val in tiers.items() if val != "pass")
|
|
@@ -2073,8 +2129,8 @@ VERBS = {
|
|
|
2073
2129
|
"wait": (cmd_wait, "record a watched external wait, without executing its locator"),
|
|
2074
2130
|
"resume": (cmd_wait, "resolve the exact wait identity and return to pending"),
|
|
2075
2131
|
"wave": (cmd_wave, "move a node to another wave, carrying the reason"),
|
|
2076
|
-
"certify": (cmd_certify, "require three independent tier reports (
|
|
2077
|
-
"flagship or product surface), then emit the verdict "
|
|
2132
|
+
"certify": (cmd_certify, "require three independent tier reports (one on an R1 node, "
|
|
2133
|
+
"four on a flagship or product surface), then emit the verdict "
|
|
2078
2134
|
"`close` consumes"),
|
|
2079
2135
|
"close": (cmd_close, "consume a verdict, close one node and re-plan"),
|
|
2080
2136
|
}
|
|
@@ -2128,8 +2184,9 @@ def main(argv=None):
|
|
|
2128
2184
|
help="the round's kind; R3 owes one `adversarial` round "
|
|
2129
2185
|
"(references/certification.md → *Rounds owed by risk class*)")
|
|
2130
2186
|
p_cert.add_argument("--ceiling", type=int, default=3,
|
|
2131
|
-
help="rounds after which
|
|
2132
|
-
"
|
|
2187
|
+
help="failing rounds after which certify STOPS: the node is parked "
|
|
2188
|
+
"with the evidence and the run records a blocker row and moves "
|
|
2189
|
+
"on, exit 7 (references/loop-guard.md)")
|
|
2133
2190
|
|
|
2134
2191
|
for verb in ("claim", "release", "recover", "complete"):
|
|
2135
2192
|
made[verb].add_argument("--authority", required=True,
|