task-pipeline-skill 1.97.1 → 1.98.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.
Files changed (41) hide show
  1. package/CHANGELOG.md +53 -0
  2. package/HOW-IT-WORKS.md +17 -13
  3. package/README.md +12 -9
  4. package/SKILL-CARD.md +1 -1
  5. package/cursor/rules/task-pipeline.mdc +22 -14
  6. package/evals/task-pipeline.evals.json +3 -3
  7. package/package.json +1 -1
  8. package/plugins/task-pipeline/.claude-plugin/plugin.json +2 -2
  9. package/plugins/task-pipeline/commands/task-pipeline.md +14 -219
  10. package/plugins/task-pipeline/skills/evidence-docs/SKILL.md +8 -1
  11. package/plugins/task-pipeline/skills/evidence-docs/references/GENERATED.md +1 -1
  12. package/plugins/task-pipeline/skills/evidence-docs/references/learned.md +24 -1
  13. package/plugins/task-pipeline/skills/evidence-docs/references/retrospective.md +21 -576
  14. package/plugins/task-pipeline/skills/project-audit/SKILL.md +8 -1
  15. package/plugins/task-pipeline/skills/task-pipeline/SKILL.md +162 -230
  16. package/plugins/task-pipeline/skills/task-pipeline/graph.schema.json +1 -1
  17. package/plugins/task-pipeline/skills/task-pipeline/pipeline.example.json +9 -9
  18. package/plugins/task-pipeline/skills/task-pipeline/pipeline.schema.json +10 -3
  19. package/plugins/task-pipeline/skills/task-pipeline/references/acceptance.md +2 -2
  20. package/plugins/task-pipeline/skills/task-pipeline/references/brainstorm.md +3 -3
  21. package/plugins/task-pipeline/skills/task-pipeline/references/build.md +7 -7
  22. package/plugins/task-pipeline/skills/task-pipeline/references/certification.md +49 -44
  23. package/plugins/task-pipeline/skills/task-pipeline/references/doctrine-map.md +1 -1
  24. package/plugins/task-pipeline/skills/task-pipeline/references/finish-line.md +13 -8
  25. package/plugins/task-pipeline/skills/task-pipeline/references/iteration.md +20 -23
  26. package/plugins/task-pipeline/skills/task-pipeline/references/knowledge-sources.md +5 -3
  27. package/plugins/task-pipeline/skills/task-pipeline/references/learned.md +24 -1
  28. package/plugins/task-pipeline/skills/task-pipeline/references/loop-guard.md +17 -7
  29. package/plugins/task-pipeline/skills/task-pipeline/references/retrospective.md +47 -1
  30. package/plugins/task-pipeline/skills/task-pipeline/references/spec.md +5 -4
  31. package/plugins/task-pipeline/skills/task-pipeline/references/stage-00-intake.md +39 -25
  32. package/plugins/task-pipeline/skills/task-pipeline/references/stage-05-dev.md +1 -1
  33. package/plugins/task-pipeline/skills/task-pipeline/references/stages.md +31 -20
  34. package/plugins/task-pipeline/skills/task-pipeline/references/waves.md +6 -4
  35. package/plugins/task-pipeline/skills/task-pipeline/scripts/graph.py +77 -21
  36. package/plugins/task-pipeline/skills/task-pipeline/scripts/retro_budget.py +305 -0
  37. package/plugins/task-pipeline/skills/task-pipeline/scripts/short_path.py +70 -0
  38. package/plugins/task-pipeline/skills/task-pipeline/scripts/waves.py +22 -10
  39. package/plugins/task-pipeline/skills/task-pipeline/templates/canvas.md +1 -1
  40. package/plugins/task-pipeline/skills/task-pipeline/templates/retro-archive.md +9 -0
  41. package/plugins/task-pipeline/skills/task-pipeline/templates/retro.md +10 -1
@@ -18,7 +18,7 @@ Writing the approved design down so a zero-context implementer — human or suba
18
18
  - Module dossier — when the run is one brick of a platform
19
19
  - Self-review — before showing it
20
20
  - Locked contracts are decisions — the register, not only the spec
21
- - GATE (manual)
21
+ - GATE (auto)
22
22
 
23
23
  ## Order of operations
24
24
 
@@ -280,13 +280,14 @@ spec section instead of restating the reasoning there.
280
280
  and the next one supersedes it; an id survives. If this stage settled something and
281
281
  no entry names it, the run has agreed to decide it again later.
282
282
 
283
- ## GATE (manual)
283
+ ## GATE (auto)
284
284
 
285
285
  > "Spec written and committed to `<path>`. Review it and tell me if anything should
286
286
  > change before I write the implementation plan."
287
287
 
288
- Wait for the operator. Changes requested → apply, re-run the self-review, ask
289
- again.
288
+ Where the brief records a standing authorization (the default gate is `auto` since
289
+ 1.98.0), an independent reader reviews the spec and the run continues; otherwise wait
290
+ for the operator. Changes requested → apply, re-run the self-review, and review again.
290
291
 
291
292
  The gate is not only the operator's word: **every section carries `covers: REQ-…`
292
293
  and every REQ in the brief appears in at least one section.** For UI tasks it
@@ -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 is not optional and not skippable.** There is no "small enough task"
11
- exemption, no "the request was already clear" exemption, no starting stage 1
12
- "while the operator thinks". The only sanctioned bypass is the
13
- entry-from-super-ux short-circuit below, and even that still requires a scope
14
- confirmation and a record of what was adopted vs skipped. A run that reaches
15
- stage 1 without a committed, operator-confirmed brief is a **failed run** —
16
- stop and go back.
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** — ten standing rows and ten stamps, both bounded **by a cap**, and they bind this
44
- run; stamp each instruction as it fires. Its *Recent log* is **queried** by the
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: printed, proposed, never taken silently.** This
85
- pipeline's own boundary exempts a typo, a one-line fix and a mechanical rename — and
86
- nothing measured that until 2026-08-10, so the exemption depended on an agent
87
- remembering it while eleven stages and four manual gates stood in front of a
88
- one-paragraph edit. Three questions, each with something behind it:
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 request names, resolved git ls-files -- <paths> -> N
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 or a caller observes changes -> yes/no
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
- Few files, no contract, no observable change ⇒ **propose** the short path: stages 1,
100
- 2, 3 and 4 marked `⊘` with the triage answer as the reason, and 5→10 running
101
- unchanged. **Propose, never take.** The answer goes in the brief's autonomy sweep and
102
- silence takes the full flow — the same floor deploy authorization uses. The glyph is
103
- what makes it safe: a skipped stage is printed on the rail **with its reason**
104
- ([`progress.md`](progress.md)), and a skip nobody can see is indistinguishable from a
105
- stage that was never entered.
106
- - **How it runs: [`grill.md`](grill.md)** — the full doctrine, built into this
107
- skill (nothing to install). In short: close what the sources can, then ONE batch
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 five-round fix loop
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 this spine at entry, then the one detail
4
- linked in the current stage before work on every entry or resume, including wave
5
- iterations and approved short paths. Do not preload all stage details. Missing or
6
- unreadable detail means stop that stage and repair its route, never guess. A custom
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). These stages (0 intake + 1→10) are the plugin's
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
- - **GATE (manual):** shared understanding reached — **the source ledger is written
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 operator
111
- confirms the brief. Stop when a
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 (manual):** the user approves the design, the UI verdict is recorded, **the
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 (manual):** the **`## Self-review` section written and committed with
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** user-reviewed; **every section carries
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,10 +309,10 @@ 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 (manual):** lint clean (host linter **and**, for UI projects, the super-ux
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, and the operator is already present at this gate. **The
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
318
  and scope. Authorization to release does not silently authorize a separate deploy. If
@@ -376,7 +386,7 @@ never that the work was skipped quietly.
376
386
 
377
387
  **On entry or resume:** read [stage-10-acceptance.md](stage-10-acceptance.md) before stage work.
378
388
 
379
- - **GATE (manual):** the ladder walk ran and its absences became REQ rows before
389
+ - **GATE (auto):** the ladder walk ran and its absences became REQ rows before
380
390
  the table was written; **the hand-back is written** — the request quoted as it was
381
391
  GIVEN, where the run stands against it, what was solved with its evidence, what
382
392
  surfaced that nobody asked for, every decision still waiting **asked here with
@@ -422,10 +432,11 @@ never that the work was skipped quietly.
422
432
  carries evidence; every `partial` names what's missing and where it's tracked;
423
433
  every `deferred`/`dropped` has the operator's agreement and, for `deferred`, a
424
434
  tracker entry; no carry-over row left `unresolved`, and the ledger's counts are
425
- printed with the verdict; the operator answers the
426
- closing question and signs off. Manual by design — an automated check can prove
427
- the table is well-formed, only the person who asked can confirm it is what they
428
- asked for.
435
+ printed with the verdict; the hand-back asks the closing question and carries
436
+ the answer the operator can give at any time. `auto` by default since 1.98.0: a
437
+ check proves the table well-formed, and only the person who asked can confirm it is
438
+ what they asked for, so the run hands that question back rather than waiting on it.
439
+ A project that wants the wait sets this gate `manual`.
429
440
 
430
441
  ## The program loop — a platform, one brick at a time
431
442
 
@@ -486,8 +497,8 @@ cycle.
486
497
  a feeling, and the ledger is what survives compaction.
487
498
  - **Trips on:** revert-oscillation (A→B→A); the same file edited twice for the same
488
499
  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: 5 fix rounds per
490
- task, 2 re-entries per stage per artifact, 3 passes per module.
500
+ time for one artifact; two loops editing one file. Hard caps, each a stop: 3 fix
501
+ rounds per task, 2 re-entries per stage per artifact, 3 passes per module.
491
502
  - **On a trip: stop editing.** Name shapes A and B with their evidence, escalate to
492
503
  the layer that owns the conflict (rubric → operator → plan → spec → module map),
493
504
  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 manual gates of stages 0, 2 and 3 are
61
- presented together at the end of planning: the brief, the canvas, the wave plan and the
62
- question batch in one message. Each gate's criteria are still checked and printed; the
63
- operator is asked once, and the go names the gates it closes. Silence closes none.
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
 
@@ -35,6 +35,7 @@ conditional on the code, never merely sequenced after it):
35
35
  4 nothing runnable: an unclassified, running or parked dependency remains
36
36
  5 external execution lease contention
37
37
  6 nothing runnable: all remaining dependency leaves are recorded watched waits
38
+ 7 `certify` stopped at its ceiling: the node is parked with the evidence
38
39
 
39
40
  **The mutation verbs never leave a half-written queue.** `add` and `park` write to a
40
41
  temp file in the same directory and `os.replace` it into place, so a crash mid-write
@@ -654,10 +655,25 @@ ALL_TIERS = TIERS + (VISUAL_TIER,)
654
655
  SURFACE_CLASSES = ("flagship", "product", "internal", "ad")
655
656
  RISK_CLASSES = ("R1", "R2", "R3")
656
657
  # Passing certification rounds a node owes before `close` accepts it, by risk class
657
- # (`references/certification.md` → *Rounds owed by risk class*). R3's must include one
658
- # round recorded `--kind adversarial`.
659
- ROUNDS_OWED = {"R1": 1, "R2": 2, "R3": 3}
658
+ # (`references/certification.md` → *Rounds owed by risk class*). A clean round ends the
659
+ # node unless it is R3 or a flagship surface: those owe a second passing round, and it is
660
+ # the adversarial one, a different question rather than the same reading again. R2 owed
661
+ # two blind rounds until 1.98.0, and a clean round did not cancel the next one: the loop
662
+ # the 2026-10-20 cost audit measured.
663
+ ROUNDS_OWED = {"R1": 1, "R2": 1, "R3": 2}
660
664
  ROUND_KINDS = ("blind", "adversarial")
665
+ # An R1 node certifies with ONE blind tier (any of the three; `unit` is the default
666
+ # reading). Every other node owes all three, and a flagship or product surface the fourth.
667
+ ONE_TIER_RISK = ("R1",)
668
+
669
+
670
+ def owed_rounds(node):
671
+ """(rounds owed, adversarial owed) for a node, or (0, False) when it owes no count."""
672
+ owed = ROUNDS_OWED.get(node.get("risk"), 0)
673
+ adversarial = node.get("risk") == "R3" or node.get("surface_class") == "flagship"
674
+ if node.get("surface_class") == "flagship":
675
+ owed = max(owed, 2)
676
+ return owed, adversarial
661
677
 
662
678
 
663
679
  def passing_rounds(cert):
@@ -675,13 +691,13 @@ def passing_rounds(cert):
675
691
 
676
692
  def owed_shortfall(node):
677
693
  """None when the node's class is satisfied, else a sentence saying what it still owes."""
678
- owed = ROUNDS_OWED.get(node.get("risk"))
694
+ owed, adversarial = owed_rounds(node)
679
695
  if not owed:
680
696
  return None
681
697
  passed = passing_rounds(node.get("certification"))
682
698
  if len(passed) < owed:
683
699
  return "%d of %d owed passing rounds" % (len(passed), owed)
684
- if node.get("risk") == "R3" and "adversarial" not in passed:
700
+ if adversarial and "adversarial" not in passed:
685
701
  cert = node.get("certification") or {}
686
702
  kinds = list(cert.get("kinds") or [])
687
703
  failed_adv = any(k == "adversarial" and not all(v == "pass" for v in (h or {}).values())
@@ -1661,10 +1677,14 @@ def cmd_certify(graph, args):
1661
1677
  the number the ceiling below reads. The node stays `pending` on a failure; the
1662
1678
  round count is the trail.
1663
1679
 
1664
- **The ceiling measures rather than stops** — `references/loop-guard.md`. At the
1665
- ceiling `certify` still runs and still tells the truth about the tiers; what it
1666
- adds is the name of the tier that keeps failing, because a run spinning on one
1667
- level needs the operator to see WHICH level, not to be halted.
1680
+ **The ceiling stops** (`references/loop-guard.md`, since 1.98.0). A round that fails
1681
+ with the node's failing rounds at the ceiling parks the node with the evidence as its
1682
+ reason (exit 7): the run records a blocker row and moves on. It used to measure
1683
+ only, and a measured loop is still a loop.
1684
+
1685
+ **No re-run on unchanged input.** A round of the same kind on the same tree as the
1686
+ round before it is refused before anything is written: a tier's answer about a tree
1687
+ it already read is the answer it gave.
1668
1688
  """
1669
1689
  guard(graph, args.graph)
1670
1690
 
@@ -1721,10 +1741,14 @@ def cmd_certify(graph, args):
1721
1741
  die("the tier reports are malformed — nothing was written:\n " + "\n ".join(bad))
1722
1742
 
1723
1743
  missing = [x for x in TIERS if x not in reports]
1724
- if missing:
1744
+ if node.get("risk") in ONE_TIER_RISK:
1745
+ if len(missing) == len(TIERS):
1746
+ die("certification of an %s node needs one blind tier report (unit, seam or "
1747
+ "product); none was given" % node.get("risk"))
1748
+ elif missing:
1725
1749
  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))
1750
+ "the level nobody read is the level the defect survives at (an R1 node "
1751
+ "certifies with one)" % ", ".join("`%s`" % m for m in missing))
1728
1752
  sclass = node.get("surface_class")
1729
1753
  if sclass in VISUAL_REQUIRED and VISUAL_TIER not in reports:
1730
1754
  die("certification is missing the `%s` report — %s is a %s surface, and on one the "
@@ -1743,6 +1767,18 @@ def cmd_certify(graph, args):
1743
1767
  head = ""
1744
1768
 
1745
1769
  prior = node.get("certification") or {}
1770
+ try:
1771
+ _t = subprocess.run(["git", "rev-parse", "HEAD^{tree}"], capture_output=True, text=True)
1772
+ tree = _t.stdout.strip() if _t.returncode == 0 else ""
1773
+ except OSError:
1774
+ tree = ""
1775
+ prior_kinds = list(prior.get("kinds") or [])
1776
+ if (tree and prior.get("tree") == tree
1777
+ and (prior_kinds[-1] if prior_kinds else "blind") == args.kind):
1778
+ die("%s: round %d already read tree %s as a %s round: re-running a tier on "
1779
+ "unchanged input repeats its answer. Change the code, run the other kind of round, "
1780
+ "or record a blocker (references/loop-guard.md). Nothing was written."
1781
+ % (nid, int(prior.get("round") or 0), tree[:12], args.kind))
1746
1782
  round_no = int(prior.get("round") or 0) + 1
1747
1783
  tiers_now = {x: reports[x]["verdict"] for x in tiers}
1748
1784
  history = list(prior.get("history") or []) + [tiers_now]
@@ -1754,6 +1790,8 @@ def cmd_certify(graph, args):
1754
1790
  "history": history,
1755
1791
  "kinds": kinds,
1756
1792
  }
1793
+ if tree:
1794
+ node["certification"]["tree"] = tree
1757
1795
 
1758
1796
  failed = [x for x in tiers if tiers_now[x] == "fail"]
1759
1797
 
@@ -1761,6 +1799,20 @@ def cmd_certify(graph, args):
1761
1799
  # operator needs named; counting it here is what makes the loop visible.
1762
1800
  churning = [x for x in tiers
1763
1801
  if len(history) >= 2 and all(h.get(x) == "fail" for h in history)]
1802
+ failed_rounds = sum(1 for h in history if any(v == "fail" for v in h.values()))
1803
+ stop = bool(failed) and failed_rounds >= args.ceiling
1804
+ if stop:
1805
+ first = next((f for x in failed for f in reports[x]["findings"]
1806
+ if f.get("severity") == "breaks"), {})
1807
+ reason = ("certification ceiling: %d failing round(s), ceiling %d; failing tier(s) %s; "
1808
+ "%s; first break %s: %s; check: %s. Blocker row owed on the board"
1809
+ % (failed_rounds, args.ceiling, ", ".join(failed),
1810
+ ("stuck on " + ", ".join(churning)) if churning else "churn across levels",
1811
+ first.get("where", "?"), first.get("what", "?"), first.get("check", "?")))
1812
+ reason = " ".join(reason.split())
1813
+ node["status"] = "parked"
1814
+ node["parked_reason"] = reason
1815
+ revise(graph, "park", nid, reason)
1764
1816
 
1765
1817
  save(args.graph, graph)
1766
1818
 
@@ -1774,9 +1826,10 @@ def cmd_certify(graph, args):
1774
1826
  print(" [%s] %s — %s" % (tier, f["where"], f["what"]), file=sys.stderr)
1775
1827
  print(" fix: %s" % f.get("fix", "(not stated)"), file=sys.stderr)
1776
1828
  print(" check: %s" % f["check"], file=sys.stderr)
1777
- if round_no >= args.ceiling:
1778
- print("\n%s has been certified %d time(s), at or over the ceiling of %d."
1779
- % (nid, round_no, args.ceiling), file=sys.stderr)
1829
+ if stop:
1830
+ print("\n%s has failed %d round(s), at the ceiling of %d. STOPPED: the node is "
1831
+ "parked with this evidence as its reason; record a blocker row and move on."
1832
+ % (nid, failed_rounds, args.ceiling), file=sys.stderr)
1780
1833
  if churning:
1781
1834
  print("The same tier has failed every round: %s. That is not a fix "
1782
1835
  "away — the level itself is being misread, or the node is the "
@@ -1785,6 +1838,8 @@ def cmd_certify(graph, args):
1785
1838
  else:
1786
1839
  print("No single tier is failing every round, so this is churn across "
1787
1840
  "levels rather than one stuck level.", file=sys.stderr)
1841
+ if stop:
1842
+ return 7
1788
1843
  print("\nThe node stays open. Round %d is recorded on it." % round_no,
1789
1844
  file=sys.stderr)
1790
1845
  return 1
@@ -1843,7 +1898,7 @@ def cmd_certify(graph, args):
1843
1898
  fh.write("\n")
1844
1899
  os.replace(tmp, out)
1845
1900
  print("%s: certified at %s in round %d" % (nid, ", ".join(tiers), round_no))
1846
- owed = ROUNDS_OWED.get(node.get("risk"))
1901
+ owed, _adv = owed_rounds(node)
1847
1902
  short = owed_shortfall(node)
1848
1903
  if short:
1849
1904
  print("round %d: %s — not closable yet. The next round reads the node's whole diff at "
@@ -1964,7 +2019,7 @@ def cmd_close(graph, args):
1964
2019
  if short and not exc:
1965
2020
  die("%s is %s: %s — not closable yet. Run the next round with `graph.py certify`, "
1966
2021
  "or record an authorized exception with `waive`. Nothing was written."
1967
- % (nid, node["risk"], short))
2022
+ % (nid, node.get("risk") or "a %s surface" % node.get("surface_class"), short))
1968
2023
  if cert:
1969
2024
  tiers = cert.get("tiers") or {}
1970
2025
  failing = sorted(k for k, val in tiers.items() if val != "pass")
@@ -2073,8 +2128,8 @@ VERBS = {
2073
2128
  "wait": (cmd_wait, "record a watched external wait, without executing its locator"),
2074
2129
  "resume": (cmd_wait, "resolve the exact wait identity and return to pending"),
2075
2130
  "wave": (cmd_wave, "move a node to another wave, carrying the reason"),
2076
- "certify": (cmd_certify, "require three independent tier reports (four on a "
2077
- "flagship or product surface), then emit the verdict "
2131
+ "certify": (cmd_certify, "require three independent tier reports (one on an R1 node, "
2132
+ "four on a flagship or product surface), then emit the verdict "
2078
2133
  "`close` consumes"),
2079
2134
  "close": (cmd_close, "consume a verdict, close one node and re-plan"),
2080
2135
  }
@@ -2128,8 +2183,9 @@ def main(argv=None):
2128
2183
  help="the round's kind; R3 owes one `adversarial` round "
2129
2184
  "(references/certification.md → *Rounds owed by risk class*)")
2130
2185
  p_cert.add_argument("--ceiling", type=int, default=3,
2131
- help="rounds after which the output names the churning tier; it "
2132
- "measures rather than stops (references/loop-guard.md)")
2186
+ help="failing rounds after which certify STOPS: the node is parked "
2187
+ "with the evidence and the run records a blocker row and moves "
2188
+ "on, exit 7 (references/loop-guard.md)")
2133
2189
 
2134
2190
  for verb in ("claim", "release", "recover", "complete"):
2135
2191
  made[verb].add_argument("--authority", required=True,