@antoneeo/kb-agentic-skill 1.5.1 → 1.7.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 CHANGED
@@ -2,6 +2,92 @@
2
2
 
3
3
  Every significant change to this skill is recorded here.
4
4
 
5
+ ## [1.7.0] - 2026-08-28
6
+
7
+ F-040 — the capture moment (second-brain unit 2): the day's decisions reach the
8
+ ledger before they evaporate.
9
+
10
+ ### Added
11
+ - **`SKILL.md` §The Capture Moment.** At a user-signed closing (phase 5 of a governed
12
+ unit, or any closing signal in an un-governed session), ONE sweep question carrying
13
+ its own inline search result: "today I recorded [...] — any other decisions from
14
+ today?". Scoped to TODAY, so it reaches decisions taken outside the session — facts
15
+ only the practitioner owns. A scheduled elicitation (the phase-3 round's family),
16
+ not a blocking question. The sweep is idempotent: a decision already noted at
17
+ phase 4 is verified, never re-captured. Channeling with explicit provenance
18
+ (elicited → ELICITED, synthesis → DERIVED, RULING only when resolving a contested
19
+ set per reconciliation §2); capture never creates topics (`unplaced.md` is never a
20
+ decision park); a no-topic decision is noted NOW with the placement declared as the
21
+ next kb unit. Declared residual: an unsignaled end captures nothing — the phase-4
22
+ note-when-said duty is the protection for sessions that never close.
23
+ - **Notes recency in the kb `orient`.** Whenever `corpus/notes/` exists: `newest
24
+ note: N days old` (or `no notes yet`), dated from note frontmatter `date:` with
25
+ mtime as fallback — named limit: mtime lies after clone/worktree, and the line
26
+ measures notes recency, not full ledger freshness. A missed capture is visible at
27
+ the next session's start.
28
+ - **`test_kb_capture.py`** — kb-only battery module (doctrine anchors + the five
29
+ recency states), outside the shared x3 manifest.
30
+ - Eval scenarios `capture_channels_the_days_decisions.md` and
31
+ `capture_stays_silent_with_nothing_to_record.md` (the negative branch: "niente"
32
+ writes nothing).
33
+
34
+ F-039 — the recall reflex (second-brain unit 1): the graph is read when answering,
35
+ not only written when distilling.
36
+
37
+ ### Added
38
+ - **`SKILL.md` §Topic Recall — the answer-side consult.** Whenever a reply would
39
+ assert facts about the project's domain, the topic index is scanned and the descent
40
+ (`taxonomy.md` §6, new) runs in answer mode — plausibility of coverage is judged ON
41
+ the scanned index, never before it. Four declared verdicts (claims cited / node
42
+ matched no claims, surfacing `gaps:` / no coverage / index absent + regenerate),
43
+ emitted only when the recall ran; never faked. Once per topic per session.
44
+ - **The anti-echo re-touch rule.** A DERIVED claim that grounds a decision has its
45
+ chain walked to non-DERIVED ground (GIVEN artifact, ELICITED or RULING note) and
46
+ that ground cited; an unresolvable ground marks the claim unverified for
47
+ decision-grounding — surfaced, never silently cited. IMPORTED grounding a decision
48
+ is named as un-re-ratified foreign authority.
49
+ - **By construction: kb `orient` appends the topic router.** The overlay special-cases
50
+ `orient` (raw argv forwarded to the spine, never re-parsed) and appends the top of
51
+ `topics/INDEX.md` — or `index absent (N node files)` with the regenerate command —
52
+ so every session opens with the graph in sight. Fails open; spine untouched.
53
+ - **`test_kb_recall.py`** — new kb-only battery module (doctrine anchors + orient
54
+ interception), deliberately outside the shared x3 manifest.
55
+ - **`run_behavioral.py` fenced-block seeding** — a scenario setup entry ending in `|`
56
+ takes its file body from the following fenced block (common-dedented), so claims
57
+ tables and frontmatter'd nodes can be seeded; one-line entries unchanged.
58
+ - Eval scenario `recall_descends_before_answering.md`.
59
+
60
+ ## [1.6.0] - 2026-08-26
61
+
62
+ F-038 — the gated rung: truthful log vocabulary and a mandated ask, no grant memory.
63
+
64
+ ### Fixed
65
+ - **A review rung that exists behind a permission policy is no longer logged as
66
+ "unavailable".** The ladder knew two states (the client supports the rung, or not)
67
+ and hard-coded rung 3's reason as "no subagent facility on this client" — so a gated
68
+ facility produced false log rows, and the ladder's own prohibition ("rung 3 is
69
+ illegitimate wherever rung 1 or 2 exists") was unsatisfiable on a gated client: the
70
+ agent could not obey it without asking, and no rule said to ask.
71
+
72
+ ### Added
73
+ - **The gated-rung stop.** At a due review gate (design; closure), when the best rung is
74
+ permission-gated (a standing policy — an interactive per-call prompt is NOT this) and
75
+ no ungated rung works (a working one-shot CLI pre-empts the stop; a gated rung 2 joins
76
+ the question instead), the gate stops and asks ONE question in the five-bullet blocking
77
+ form: the gated rung(s) vs the fallback, each higher rung's status, the quoted policy,
78
+ the cost (~130-175k subagent tokens per deep review, measured 2026-08) AND what
79
+ independence last bought, and what stays blocked. **No grant memory**: the answer holds
80
+ while the conversation does; an agent that cannot recall a grant asks again. Unattended
81
+ runs never emit the question — rung 2 is still owed a try, then rung 3 with its reason.
82
+ (Successor to a design CANCELLED at the review cap after grant memory failed three
83
+ ways; rulings r18/r19.)
84
+ - **The reason words.** A below-rung-1 row says why the rung(s) above did not run:
85
+ `absent` (the client has no such facility — a claim about the client, never a policy),
86
+ `gated, declined`, `gated, unattended`, `gated, pre-empted` (nothing asked — an
87
+ ungated lower rung ran the review). A rung-1 row owes nothing. For rung 3 this
88
+ re-words an existing duty; for rung 2 it is a new one-word duty, disclosed and
89
+ accepted with the rest.
90
+
5
91
  ## [1.5.1] - 2026-08-25
6
92
 
7
93
  ### Fixed
package/README.md CHANGED
@@ -20,6 +20,10 @@ Two axes are kept apart on purpose — **abstraction** (a topic made of topics:
20
20
 
21
21
  5. **Escalation in one batch at the end of the run**, in legal form (the claims, the reopenable sources, the dates, why the machine cannot decide). Ingestion never stops to interrogate you.
22
22
 
23
+ 6. **Recall — the graph is read when answering, not only written when distilling.** Whenever a reply would assert facts about the project's domain, the agent scans the topic index and descends before answering from model memory — plausibility of coverage is judged ON the scanned index, never before it. Answers cite claim ids; a superseded claim is never cited without its successor; the verdict is declared only when the recall ran (`kb: <slug> → N claims cited`, a matched node with no claims surfaces its `gaps:`, and a missing index is reported as index-absent — not as a missing graph — with the regenerate command). When a DERIVED claim grounds a decision, its chain is walked to non-DERIVED ground (the given artifact or the elicited/ruled note) and that ground is cited — re-reading the agent's own synthesis is the echo chamber this rule exists to break. By construction, not exhortation: the kb `orient` opens every session with the topic router in sight.
24
+
25
+ 7. **Capture — the day's decisions reach the ledger before they evaporate.** At a user-signed closing ("chiudiamo"), one sweep question — carrying its own inline result: "today I recorded X and Y — any other decisions from today?" — reaches even the decisions taken outside the session, which no transcript search could find. What surfaces lands on the existing rails (an elicited dated note, an L1 claim row) with explicit provenance; the sweep is idempotent (nothing captured twice), never creates topics, and stays silent when no one signed off. The kb `orient` shows notes recency at every session start — dated from note frontmatter, because file mtimes lie after a clone or worktree — so a missed capture is visible the next morning.
26
+
23
27
  Deliberately absent: any per-node coverage or completion state. `gaps:` says what a node lacks; nothing collects it into a dashboard. A **source** does record how far it has been read, but on its own sidecar and nowhere else — the corpus index prints that fact for every artifact, including the finished ones, because a list of only what is behind is the dashboard this method refuses.
24
28
 
25
29
  ## Key features
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "kb-agentic-skill",
3
- "version": "1.5.1",
3
+ "version": "1.7.0",
4
4
  "description": "Knowledge-Base & Document-First protocol with risk triage, Vision governance, signal distillation and optional devPNT integration.",
5
5
  "author": "Antonio Pinto (https://github.com/Antoneeo)"
6
6
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@antoneeo/kb-agentic-skill",
3
- "version": "1.5.1",
3
+ "version": "1.7.0",
4
4
  "description": "Knowledge-Base & Document-First protocol for Claude Code, Gemini CLI, Google Antigravity and Codex with risk triage, Vision governance, signal distillation and optional devPNT integration.",
5
5
  "keywords": [
6
6
  "claude-code",
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  name: kb-agentic
3
- version: 1.5.1
4
- description: Knowledge-Base & Document-First protocol with risk-proportional triage, Vision as a guide, Signal Distillation, a complete Standalone mode and optional symbiosis with devPNT. Use for user documentation, knowledge extraction, SOPs, research notes, decision logs and knowledge management.
3
+ version: 1.7.0
4
+ description: Knowledge-Base & Document-First protocol with risk-proportional triage, Vision as a guide, Signal Distillation, a complete Standalone mode and optional symbiosis with devPNT. Use for user documentation, knowledge extraction, SOPs, research notes, decision logs, knowledge management — and for answering project questions from the claim ledger and topic graph.
5
5
  author: Antonio Pinto (https://github.com/Antoneeo)
6
6
  copyright: (c) 2026 Antonio Pinto
7
7
  ---
@@ -15,7 +15,7 @@ This skill guides knowledge management and documentation with a Document-First p
15
15
  Support files in the skill directory:
16
16
  - `templates.md`: templates for Vision, Knowledge ANALYSIS, Research SPIKE, SOP GUIDE, audit plan, and handoff.
17
17
  - `portability.md`: carrying knowledge between projects — what a bundle is, why export is a closure rather than a selection, and the rule that knowledge crosses a project boundary while authority does not (`prov: IMPORTED`). Read before `export`/`import`.
18
- - `taxonomy.md`: placing a claim in the topic graph — descent over the generated index, the five verdicts (EXISTS / INADEQUATE / MISSING / GENERALIZES / UNPLACED), the sibling rule, guarded re-parenting, canonicalization. Run at L3 before drafting.
18
+ - `taxonomy.md`: placing a claim in the topic graph — descent over the generated index, the five verdicts (EXISTS / INADEQUATE / MISSING / GENERALIZES / UNPLACED), the sibling rule, guarded re-parenting, canonicalization — and the answer-mode descent (`§6`). Run at L3 before drafting.
19
19
  - `guides.md`: pipeline for distilling user-provided indications into `ai_docs/reference/GUIDE_[topic].md`.
20
20
  - `vision.md`: how to write a Vision a cold reviewer can actually apply — the properties that make a rule hold, the minimum operable sections, and the blind check.
21
21
  - `distillation.md`: from a source to claim rows — intake (content-addressed originals, stored canonical extraction, every provenance a real file), the claim table, extraction discipline, signal rules (symbiosis with `distill`).
@@ -35,7 +35,7 @@ Read these files only when needed. `SKILL.md` is the operating contract; the sup
35
35
  - **Signal & Distillation:** eliminate fluff, filler, and speculative statements; keep deterministic facts, constraints, decisions, and clear SOP steps (symbiosis with `distill`).
36
36
  - **Lifecycle & Freshness:** mark superseded knowledge (`status: SUPERSEDED` or `DEPRECATED`) so outdated information does not cause hallucination.
37
37
  - **Protect the Vision & User Style:** align all knowledge work with user strategic goals, operational preferences, and expected outcomes.
38
- - **Map-First Navigation:** rely on `ai_docs/README.md`, `INDEX.md`, and `reference/INDEX.md` router for targeted retrieval before creating new documents.
38
+ - **Map-First Navigation:** rely on `ai_docs/README.md`, `INDEX.md`, and `reference/INDEX.md` router for targeted retrieval before creating new documents — and before *answering from model memory* on the project's domain (`## Topic Recall`).
39
39
 
40
40
  ## Rule Zero: Triage
41
41
 
@@ -90,6 +90,7 @@ Triage decides IF documentation is due; this table decides WHICH document each e
90
90
  | a KB bundle (`export`) | Knowledge must leave this project. Export is L1 — it writes nothing into the corpus. **Importing one is L3**: a source enters the corpus and nodes are created, and a bundle is external input, so never L1 whatever its size (`portability.md`). | — |
91
91
  | `corpus/given/*` + sidecar | A source arrives: it becomes a content-addressed artifact with a sidecar carrying digest/date/`supersedes:`. A text source is copied verbatim. A non-text source yields its stored canonical extraction — copied **beside** the original when that is small enough to keep, or **instead of it** on a large binary corpus, where the original stays where it lives and is recorded as `original_path:`/`original_sha256:` (`distillation.md` §1, which owns this rule). Never edited after ingest — the digest check on whatever `given/` holds is what enforces it. | 4 |
92
92
  | `corpus/notes/*` | Something is said (`origin: elicited`), synthesised (`derived_from:`), or ruled (`basis:`). A note with none of the three is refused by the validator. | 4 / 5 |
93
+ | the day's decisions, at a user-signed closing (the Capture Moment sweep) | Verification that the phase-4 notes/claims exist; NEW `corpus/notes/*` + claim rows only for what the sweep surfaces un-captured. The event is the CLOSING, disjoint from the row above (which fires when something is SAID). | 5 / session end |
93
94
  | `solutions/PLAN_[topic].md` + its ledger | A source too long for one context is ingested: **one task per reading window** (30 pages by default; the plan states the window used), each ending at a declared `extracted_through:`. That ledger is the register the ingestion resumes from across sessions — ingestion never builds a second one (`distillation.md` §3, `templates.md`). | 4 |
94
95
  | `corpus/given/*.meta.md` — `extracted_through:` | Every reading window closes by advancing it (`p=<n>`, `L<n>`, `complete`). Required once any claim cites the artifact: unstated, "I am finished" cannot be falsified, which is how a sampled 200-page manual passes for an ingested one. | 4 |
95
96
  | `INDEX.md`, `reference/INDEX.md`, `topics/INDEX.md`, `corpus/INDEX.md` | Regenerated by `sdlc_check.py index` at closure — never by hand; `validate` fails on a hand-edited one. | 5 |
@@ -120,7 +121,7 @@ Use this mode when `devpnt_*` tools are available and point at the current proje
120
121
  - Run spec elicitation round (`elicitation.md`) before drafting analysis.
121
122
  - Run taxonomy pass (`taxonomy.md`): verify whether topics, categories, or SOPs already exist in `ai_docs/`. Avoid duplication.
122
123
  - Create or update `ai_docs/solutions/ANALYSIS_[topic].md`.
123
- - **Design review gate (end of Phase 3, before any drafting):** the analysis is reviewed by somebody other than its author — a subagent with fresh context, or a declared self-pass when none is available. Follow `review.md`; log the outcome in `audit/reviews/REVIEW_LOG.md`. A knowledge structure reviewed only by the person who chose it is not reviewed.
124
+ - **Design review gate (end of Phase 3, before any drafting):** the analysis is reviewed by somebody other than its author — a subagent with fresh context, or a declared self-pass when no higher rung is usable — a permission-gated rung is asked about, never silently skipped (`review.md`). Follow `review.md`; log the outcome in `audit/reviews/REVIEW_LOG.md`. A knowledge structure reviewed only by the person who chose it is not reviewed.
124
125
 
125
126
  ### 4. Knowledge Processing & Distillation
126
127
  - **Isolate the work (Branch/worktree hygiene).** Distillation rewrites existing notes: do it on a branch or a worktree, never directly on the shared corpus, so a half-finished reconciliation is never what the next reader finds.
@@ -142,6 +143,106 @@ Guides are consulted, created, and distilled per `guides.md`.
142
143
  - **Propose proactively**: when the user hands over indications that will govern future work, propose distilling them into a guide. A proposal, never a silent write, and never from model knowledge — a guide's whole value is that every claim traces to what the user actually provided.
143
144
  - Distill from user instructions into `ai_docs/reference/GUIDE_[topic].md`.
144
145
 
146
+ ## Topic Recall — the answer-side consult
147
+
148
+ The graph is only a second brain if it is READ when answering, not only written when
149
+ distilling. The descent (`taxonomy.md`) fires here with a new trigger — the answer-side
150
+ twin of the guide consult above.
151
+
152
+ - **Trigger — unconditional scan on domain assertions.** Whenever the reply would assert
153
+ facts about the PROJECT'S domain (its topics, decisions, sources, constraints), scan
154
+ `topics/INDEX.md`. Plausibility of coverage is judged ON the scanned top rows, never
155
+ before them — "the KB probably doesn't have this" is model memory talking, the exact
156
+ judgement this reflex removes. Exempt: pure mechanics (running a command, editing per
157
+ explicit instruction), general knowledge outside the project's domain, L1 gestures.
158
+ - **Once per topic per SESSION.** The dedup key is the matched slug (known ex post); a
159
+ question plainly within an already-descended branch reuses that consult, and a
160
+ placement descent already run this session counts. A compaction starts a new session.
161
+ - **The act.** Descend per `taxonomy.md` §6 (same doctrine, answer mode). On claims,
162
+ answer FROM them citing claim ids; a claim whose `state` is `SUPERSEDED <id>` is never
163
+ cited without naming its successor.
164
+ - **The declared verdict — only when the recall ran**, never a per-turn ritual. Four
165
+ legal values:
166
+ - `kb: <slug> → N claims cited`
167
+ - `kb: <slug> → node matched, no claims` — surface the node's `gaps:` when it answers
168
+ the question: "the KB knows it doesn't know" is a first-class answer.
169
+ - `kb: no coverage` — index read, nothing fits.
170
+ - `kb: index absent — regenerate (sdlc_check.py index)` — the INDEX is missing, not
171
+ necessarily the graph (nodes routinely exist while the index lags); regenerate and
172
+ rescan.
173
+ Never fake the verdict: an always-"no coverage" verdict certifies a lookup that did
174
+ not happen. The `router:` and `kb:` verdicts are independent and may co-occur, one
175
+ line each — a how-to question can match a guide AND carry claims.
176
+ - **Anti-echo — the re-touch rule.** When a DERIVED claim grounds a decision the user is
177
+ about to take, walk its chain to non-DERIVED ground — a GIVEN artifact, an ELICITED or
178
+ RULING note — and cite that ground beside the claim id; re-opening only the immediate
179
+ target may re-read the agent's own synthesis, which is the echo chamber itself. If the
180
+ ground does not resolve (imported bundle without originals, external `original_path:`,
181
+ tombstone): say so beside the claim id and treat the claim as **unverified for
182
+ decision-grounding** — surfaced, never silently cited. Per-provenance stance: GIVEN's
183
+ source column is already the direct citation; ELICITED/RULING carry the practitioner's
184
+ own authority; an IMPORTED claim grounding a decision is named as un-re-ratified
185
+ foreign authority.
186
+ - **The trace persists.** When the decision is recorded (a ruling note, a diary entry),
187
+ that record cites the claim id AND the re-touched ground — the trace survives in the
188
+ corpus, not in ephemeral chat.
189
+ - **Under dispatch**, recall is an orchestrator concern: the orchestrator runs the
190
+ descent itself; a context-free subagent never runs its own. The task schema has no
191
+ claims field today, so until a dispatch-focused unit adds one, the orchestrator
192
+ inlines the cited claim rows in the task's free text — an explicit interim.
193
+
194
+ ## The Capture Moment — the day's decisions reach the ledger
195
+
196
+ Capture today is what makes recall honest tomorrow: a captured elicited or ruled note
197
+ is exactly the non-DERIVED ground the re-touch rule above walks to. The rails all
198
+ exist — this section adds only the MOMENT.
199
+
200
+ - **When — user-signed closings, once per session.** Phase 5 of a governed unit, or
201
+ any closing signal from the user in an un-governed session ("chiudiamo", "we're
202
+ done"); the session-end pass that updates `handoff.md`/`project_notes.md` then runs
203
+ at that same closing. Never per-turn, never a standing interruption. **Declared
204
+ residual**: an unsignaled end (the user simply stops writing) captures nothing —
205
+ the phase-4 duty (a decision stated mid-session owes its note WHEN SAID) is the
206
+ protection for sessions that never close, and the orient's recency line makes the
207
+ miss visible next time. That line measures NOTES recency, not full ledger
208
+ freshness, and trusts frontmatter `date:` over file mtimes — mtimes lie after a
209
+ clone or worktree.
210
+ - **The sweep question.** At the closing, ONE question, carrying its own search
211
+ result inline: "today I recorded: [what the sweep found already on the rails] —
212
+ any other decisions from today to record?". Scoped to TODAY, so it reaches
213
+ decisions taken outside the session (a meeting, a call) that no transcript search
214
+ could find — facts only the practitioner owns. This is a **scheduled elicitation**
215
+ in the same family as the phase-3 round, not a blocking question (nothing stalls;
216
+ the session is ending), so the blocking form does not attach; the inline result
217
+ satisfies the search-first duty. "Niente" ends it.
218
+ - **The sweep is idempotent.** It verifies the day's decisions are on the rails; a
219
+ decision already noted at phase 4 is verified, never re-captured — no second note,
220
+ no duplicate claim row. The moment closes gaps; the phase-4 row stays the primary
221
+ channel.
222
+ - **What counts as a decision**: domain choices made or confirmed today — a value, a
223
+ threshold, a rule, a stated preference, a rejected alternative with its reason.
224
+ NOT mechanics (commands run, files edited), NOT process state (the handoff's job).
225
+ - **The channeling — existing rails, explicit provenance.**
226
+ - The user's words → `corpus/notes/*` with `origin: elicited` and a `date:`; a
227
+ claim row distilled from it carries `prov: ELICITED`, source = that note with a
228
+ legal `L<a>-<b>` locator.
229
+ - A synthesis of the day's discussion → a note with `derived_from:` → claim
230
+ `prov: DERIVED`.
231
+ - **RULING only when the decision resolves an existing contested set** — then
232
+ `reconciliation.md` §2 applies in full (mandatory `basis:`, supersession of the
233
+ whole prior set). A fresh conflict-free decision is captured elicited: a preference
234
+ is not a fact, and `basis:` is reserved for the fact the corpus lacks.
235
+ - A fact on an existing topic → claim row, L1 (`id` empty; `claim-id --fill`).
236
+ - No owning topic → the note is written NOW and the placement is declared as the
237
+ next kb unit. Capture never smuggles an L3 hierarchy change, and
238
+ `topics/unplaced.md` is never a decision park — UNPLACED is for topic-less
239
+ scraps and is invisible to recall.
240
+ - **Reconciliation is wired.** A captured claim meeting an existing claim on the
241
+ node classifies per `reconciliation.md` — capture can confirm, refine, coexist or
242
+ CONTEST, never silently overwrite.
243
+ - **Silence stays legal where no one signed off**: no closing signal → no question,
244
+ no ritual line.
245
+
145
246
  ## Mechanical Enforcement
146
247
  - `scripts/sdlc_check.py index`: updates `ai_docs/INDEX.md` and `ai_docs/reference/INDEX.md`.
147
248
  - `scripts/sdlc_check.py validate`: validates YAML frontmatter (`status`, `description`).
@@ -1,96 +1,98 @@
1
- # Subagent Execution Discipline
2
-
3
- Opt-in orchestration for L3 work with an approved design: the orchestrator
4
- drives a `PLAN_[feature].md` through subagents instead of implementing every
5
- task in the same session. Default stays same-session; this is an escalation,
6
- never a requirement.
7
-
8
- ## Trigger
9
-
10
- Only for L3 (an approved E-TDD in Hybrid, or an ANALYSIS Action Plan in
11
- Standalone). Never for L1/L2 — the plan/ledger machinery is overhead a small
12
- change does not need. The plan is always `derived-from` the accepted design:
13
- it is never independently authored, exactly like an E-TDD is never authored
14
- without an E-ISP.
15
-
16
- ## The loop
17
-
18
- 1. `sdlc_check.py plan validate PLAN_[feature].md` — zero-execution schema +
19
- confinement + ledger cross-check. Non-zero exit = **no dispatch**. This is
20
- the hard gate: "no valid plan, no dispatch."
21
- 2. For each task, in plan order:
22
- - Read the task's status from the sidecar ledger
23
- (`PLAN_[feature].ledger.json`). `status: done` (exact sentinel) → skip,
24
- never re-dispatch. Anything else (pending, failed, missing, or a
25
- corrupt-but-parseable entry) → treat as pending and dispatch.
26
- - `sdlc_check.py plan brief PLAN_[feature].md --task <id>` — prints the
27
- task block, the `produces` of prior-order tasks (interfaces), and
28
- `guides` pointers (paths, never pasted content) to stdout.
29
- - Spawn the subagent with that brief as its entire context window.
30
- - Run `task.verify` out of band (the orchestrator executes it — the
31
- validator only ever prints it, never runs it) plus the one-shot review
32
- below.
33
- - Write `{status, verify_result, timestamp}` back to the ledger. The
34
- validator never writes the ledger — single-writer, orchestrator-owned.
35
-
36
- **Guide consumption under dispatch.** Selecting each task's `guides` field IS the
37
- consult trigger (`guides.md` §0) applied at plan-authoring time: the orchestrator
38
- runs the router lookup (project router `ai_docs/reference/INDEX.md` + the agent-KB
39
- router) when populating `guides`. A dispatched context-free subagent does **NOT**
40
- run its own router consult — it reads the guide pointers handed to it in the
41
- brief. The router verdict (Rule Zero) is therefore declared ONCE, by the
42
- orchestrator, when it authors the plan; a dispatched subagent does not declare
43
- one. (Proactive guide-creation stays at closure — the same broad final pass
44
- below — so it needs no separate dispatch hook.)
45
-
46
- ## Model tiers (client-relative, no provider names)
47
-
48
- - Default dispatch: **economy** implementer tier.
49
- - After **two** consecutive `verify_result: fail` on the same task: escalate
50
- to the **deep** tier for the retry (ADR 2026-07-02). Do not escalate on the
51
- first failure — a single fail is often a brief or environment issue, not a
52
- capability gap.
53
-
54
- ## Review slots — one-shot, not iterative
55
-
56
- Exactly three review touches per task, never a loop:
57
-
58
- 1. Inline self-review by the implementer subagent before it reports done
59
- (the standard critical-review pass, not a separate call).
60
- 2. One reviewer pass per task (Hybrid: reuse the devPNT code-review gate;
61
- Standalone: the `review.md` discipline).
62
- 3. One broad final pass over the whole plan at closure, after all tasks are
63
- DONE — catches cross-task drift a per-task review cannot see.
64
-
65
- If a review FAILs, fix and re-run `verify` — that is a normal loop iteration
66
- via the ledger's fail path, not an extra review slot.
67
-
68
- **The scoped re-review is a round INSIDE slot 2 or 3, never a fourth slot.**
69
- `review.md` §Receiving requires every review-driven correction to be
70
- re-reviewed against the correction alone — that round belongs to the slot whose
71
- findings caused it, and is bounded by the same cap of 3 rounds. "Never a loop"
72
- bounds the SLOTS (no unbounded re-reviewing of a task), not the rounds inside
73
- one slot: a fix nobody looked at is the defect the round exists to catch. Slot 1
74
- is the implementer's own pass, not an independent review, so its fixes carry no
75
- re-review round. The ledger records the round on the task's fail path as usual.
76
-
77
- ## Ledger protocol summary
78
-
79
- Read → skip-if-done → dispatch-if-pending → write. The ledger is the only
80
- memory the loop needs across sessions or context compaction: a resumed
81
- orchestrator re-reads it and picks up exactly where it left off, never
82
- re-running a DONE task.
83
-
84
- ## Degradation
85
-
86
- No subagent-spawning tool available → the orchestrator runs each task in the
87
- same session, against the same plan and ledger, with the same one-shot review
88
- slots. No capability is lost, only the parallelism/isolation subagents would
89
- have added.
90
-
91
- ## Hybrid note
92
-
93
- The plan's `derived-from` points at the accepted E-TDD document key. Per-task
94
- review reuses the devPNT independent reviewers (§4.6 code review gate) rather
95
- than restating review doctrine — see `review.md` for the single definition
96
- both modes share.
1
+ # Subagent Execution Discipline
2
+
3
+ Opt-in orchestration for L3 work with an approved design: the orchestrator
4
+ drives a `PLAN_[feature].md` through subagents instead of implementing every
5
+ task in the same session. Default stays same-session; this is an escalation,
6
+ never a requirement.
7
+
8
+ ## Trigger
9
+
10
+ Only for L3 (an approved E-TDD in Hybrid, or an ANALYSIS Action Plan in
11
+ Standalone). Never for L1/L2 — the plan/ledger machinery is overhead a small
12
+ change does not need. The plan is always `derived-from` the accepted design:
13
+ it is never independently authored, exactly like an E-TDD is never authored
14
+ without an E-ISP.
15
+
16
+ ## The loop
17
+
18
+ 1. `sdlc_check.py plan validate PLAN_[feature].md` — zero-execution schema +
19
+ confinement + ledger cross-check. Non-zero exit = **no dispatch**. This is
20
+ the hard gate: "no valid plan, no dispatch."
21
+ 2. For each task, in plan order:
22
+ - Read the task's status from the sidecar ledger
23
+ (`PLAN_[feature].ledger.json`). `status: done` (exact sentinel) → skip,
24
+ never re-dispatch. Anything else (pending, failed, missing, or a
25
+ corrupt-but-parseable entry) → treat as pending and dispatch.
26
+ - `sdlc_check.py plan brief PLAN_[feature].md --task <id>` — prints the
27
+ task block, the `produces` of prior-order tasks (interfaces), and
28
+ `guides` pointers (paths, never pasted content) to stdout.
29
+ - Spawn the subagent with that brief as its entire context window.
30
+ - Run `task.verify` out of band (the orchestrator executes it — the
31
+ validator only ever prints it, never runs it) plus the one-shot review
32
+ below.
33
+ - Write `{status, verify_result, timestamp}` back to the ledger. The
34
+ validator never writes the ledger — single-writer, orchestrator-owned.
35
+
36
+ **Guide consumption under dispatch.** Selecting each task's `guides` field IS the
37
+ consult trigger (`guides.md` §0) applied at plan-authoring time: the orchestrator
38
+ runs the router lookup (project router `ai_docs/reference/INDEX.md` + the agent-KB
39
+ router) when populating `guides`. A dispatched context-free subagent does **NOT**
40
+ run its own router consult — it reads the guide pointers handed to it in the
41
+ brief. The router verdict (Rule Zero) is therefore declared ONCE, by the
42
+ orchestrator, when it authors the plan; a dispatched subagent does not declare
43
+ one. (Proactive guide-creation stays at closure — the same broad final pass
44
+ below — so it needs no separate dispatch hook.)
45
+
46
+ ## Model tiers (client-relative, no provider names)
47
+
48
+ - Default dispatch: **economy** implementer tier.
49
+ - After **two** consecutive `verify_result: fail` on the same task: escalate
50
+ to the **deep** tier for the retry (ADR 2026-07-02). Do not escalate on the
51
+ first failure — a single fail is often a brief or environment issue, not a
52
+ capability gap.
53
+
54
+ ## Review slots — one-shot, not iterative
55
+
56
+ Exactly three review touches per task, never a loop:
57
+
58
+ 1. Inline self-review by the implementer subagent before it reports done
59
+ (the standard critical-review pass, not a separate call).
60
+ 2. One reviewer pass per task (Hybrid: reuse the devPNT code-review gate;
61
+ Standalone: the `review.md` discipline).
62
+ 3. One broad final pass over the whole plan at closure, after all tasks are
63
+ DONE — catches cross-task drift a per-task review cannot see.
64
+
65
+ If a review FAILs, fix and re-run `verify` — that is a normal loop iteration
66
+ via the ledger's fail path, not an extra review slot.
67
+
68
+ **The scoped re-review is a round INSIDE slot 2 or 3, never a fourth slot.**
69
+ `review.md` §Receiving requires every review-driven correction to be
70
+ re-reviewed against the correction alone — that round belongs to the slot whose
71
+ findings caused it, and is bounded by the same cap of 3 rounds. "Never a loop"
72
+ bounds the SLOTS (no unbounded re-reviewing of a task), not the rounds inside
73
+ one slot: a fix nobody looked at is the defect the round exists to catch. Slot 1
74
+ is the implementer's own pass, not an independent review, so its fixes carry no
75
+ re-review round. The ledger records the round on the task's fail path as usual.
76
+
77
+ ## Ledger protocol summary
78
+
79
+ Read → skip-if-done → dispatch-if-pending → write. The ledger is the only
80
+ memory the loop needs across sessions or context compaction: a resumed
81
+ orchestrator re-reads it and picks up exactly where it left off, never
82
+ re-running a DONE task.
83
+
84
+ ## Degradation
85
+
86
+ No subagent-spawning tool available — absent, or permission-gated with no grant
87
+ in context (`review.md`'s gated-rung stop owns the asking; task dispatch never
88
+ asks; the broad final pass IS `review.md` moment 2 and asks there) → the
89
+ orchestrator runs each task in the same session, against the same plan and ledger,
90
+ with the same one-shot review slots. No capability is lost, only the parallelism/isolation subagents would
91
+ have added.
92
+
93
+ ## Hybrid note
94
+
95
+ The plan's `derived-from` points at the accepted E-TDD document key. Per-task
96
+ review reuses the devPNT independent reviewers (§4.6 code review gate) rather
97
+ than restating review doctrine — see `review.md` for the single definition
98
+ both modes share.
@@ -21,8 +21,10 @@ record is reviewable; a guess folded silently into a design is not.
21
21
  ## The question discipline
22
22
 
23
23
  A question to the practitioner spends their attention and stalls the work; the
24
- round below is the only place the process *plans* that cost. Everywhere, a
25
- question is legal only when BOTH hold:
24
+ process *plans* that cost in exactly two places — the round below, and the
25
+ Capture Moment sweep at a user-signed closing (`SKILL.md`), a scheduled
26
+ elicitation in the same family that carries its search result inline and stalls
27
+ nothing. Everywhere, a question is legal only when BOTH hold:
26
28
 
27
29
  1. **Searched first, and the search is named — with its result.** The answer is
28
30
  not on record and not derivable from the corpus, the topic graph, `ai_docs/`,
@@ -12,7 +12,7 @@ Two moments, and they review different things:
12
12
  |---|---|---|---|
13
13
  | **1. Design review** | End of Phase 3 — **before any implementation** | the ANALYSIS (Standalone) / the `E-ISP`+`E-TDD` (Hybrid) | L3 |
14
14
  | **1b. Late arrival** | Work that became L3 *after* code existed — an L1/L2 reclassified mid-flight, or a design increment on a feature already implemented — runs moment 1 **now**, before any further implementation, logged `design (late)` | same | L3 |
15
- | **2. Closure review** | Phase 5, before DONE | the actual diff, against that approved design | L2 / L3 — the L2 row is optional |
15
+ | **2. Closure review** | Phase 5, before DONE | the actual diff, against that approved design | L2 / L3 — the L2 closure review is optional; every review that runs logs its row |
16
16
 
17
17
  **Why the design review is its own moment, and not a nicety.** The closure review
18
18
  can only tell you the code matches the design; it cannot tell you the design was
@@ -30,17 +30,56 @@ blind to it, which is why independence, not effort, is what this gate buys.
30
30
  a SELF-CONTAINED prompt — the reviewer session has no other context, which is
31
31
  exactly what makes it independent.
32
32
  3. **A declared self-pass** — a separate, explicitly adversarial pass by you,
33
- against the same checklist. **Rung 3 is illegitimate wherever rung 1 or 2
34
- exists**: on a client with a subagent facility or a one-shot CLI, descending to
35
- it is choosing zero independence, which is the one thing this gate buys. When
36
- you do use it, the log row must carry *why* — `self-pass (declared; no subagent
37
- facility on this client)` — not merely that you did. A rung named without its
33
+ against the same checklist. **Rung 3 is illegitimate wherever a higher rung
34
+ is usable**: on a client with a working subagent facility or one-shot CLI,
35
+ descending to it is choosing zero independence, which is the one thing this
36
+ gate buys. A gated rung the user DECLINED is not usable — see *The gated
37
+ rung* below; a gated rung nobody asked about is not declined (unattended is
38
+ its own case below). When
39
+ you do use it, the log row must carry *why*, in the reason words below —
40
+ `self-pass (declared; absent — the client has no such facility)` — not merely
41
+ that you did. A rung named without its
38
42
  reason is indistinguishable from a rung chosen for convenience.
39
43
 
40
44
  Rung 3 stays in the ladder deliberately: it is what keeps the methodology
41
45
  completable with no network, no account and no subagent facility. It is a floor,
42
46
  never a default.
43
47
 
48
+ **The gated rung — present is not absent (F-038).** A rung that exists behind a
49
+ standing policy or instruction forbidding its use absent a user request is
50
+ **permission-gated**, not unavailable. An interactive per-call approval prompt is
51
+ NOT this: answering the prompt IS the grant. With an ungated rung usable — rung 1
52
+ free, or a one-shot CLI that works (try it, or show it failed) — the prohibition
53
+ above is already satisfiable and no stop fires (the row still carries
54
+ `gated, pre-empted` when a gated rung sat above the one that ran); a gated
55
+ rung 2 is not "usable" for this clause, it joins the question instead. When the best rung is gated and no
56
+ ungated rung works, the gate STOPS and asks: descending to rung 3 on silence is
57
+ illegal while a user is reachable. The stop is doctrine-mandated (legal by mandate,
58
+ `elicitation.md` §Blocking is reserved) and carries that file's five-bullet
59
+ blocking form — its closed-list exemption covers this file's round-cap hand-over
60
+ only, not this stop. The question offers the gated rung(s) against the fallback, with what each
61
+ buys; states each higher rung's status (tried / unusable / gated) with the
62
+ standing instruction quoted and "no grant visible in my current context"; says
63
+ why no assumption survives (assuming either way writes a false log row); says
64
+ why it is the USER'S call — their tokens (~130-175k per deep review, measured
65
+ 2026-08) against a benefit only they can price, stated with what independence
66
+ last bought; and names what stays blocked — which reviewer runs, nothing else.
67
+ **No grant memory exists**: the answer holds while the conversation does, and an
68
+ agent that cannot recall a grant asks again — one question per gate per intact
69
+ context; re-asking is a smaller defect than a false row. Unattended
70
+ (`elicitation.md`'s Unattended path — the user is not reachable), no question is
71
+ emitted: rung 2 is still owed a try, then rung 3 runs with its reason logged.
72
+
73
+ **The reason words.** A below-rung-1 row carries why the rung(s) above did not
74
+ run; a rung-1 row owes nothing. `absent` — the client has no such facility (a
75
+ claim about the client, never about a policy); `gated, declined` — usable only
76
+ with the user's assent at this gate, and the user withheld it (a standing policy
77
+ answered no, or a per-call prompt denied); `gated, unattended` — a standing policy
78
+ gates it and no user is reachable to ask; `gated, pre-empted` — gated, nothing
79
+ asked: an ungated lower rung ran the review instead. `gated` always appears with
80
+ its outcome. The trigger term "permission-gated" scopes the STOP; the row word
81
+ `gated` additionally covers a denied per-call prompt, which never triggers one.
82
+
44
83
  Use a different model from the author's where the client allows it.
45
84
 
46
85
  **Rounds are capped at 3.** FAIL → revise → re-review. If findings still stand
@@ -1491,6 +1491,113 @@ def kb_cmd_import(args):
1491
1491
  return 0
1492
1492
 
1493
1493
 
1494
+ def kb_cmd_orient(argv):
1495
+ """Forward `orient` to the spine untouched, then append the topic router.
1496
+
1497
+ The recall reflex (SKILL.md section "Topic Recall") needs the graph in
1498
+ sight at session start -- by construction, not exhortation (F-039). The
1499
+ spine's orient is never re-implemented here: the raw argv goes through
1500
+ (the --help pattern below), and orient's flags are never mirrored into an
1501
+ overlay argparse -- the drift class the forward-by-default comment warns
1502
+ about. parse_known_args reads ONLY the two root flags the overlay itself
1503
+ needs to locate topics/, and ignores everything else.
1504
+
1505
+ The append fails open: a broken graph read must never break orient itself.
1506
+ """
1507
+ try:
1508
+ rc = sdlc_core.main(argv)
1509
+ except SystemExit as e:
1510
+ rc = e.code if isinstance(e.code, int) else 1
1511
+ # Pre-bind so a throw INSIDE the router print (after root was validly
1512
+ # bound) does not erase it -- the recency block below must survive a
1513
+ # router failure with the bound root intact (closure R2 finding).
1514
+ root = name = None
1515
+ try:
1516
+ probe = argparse.ArgumentParser(add_help=False)
1517
+ probe.add_argument("--root")
1518
+ probe.add_argument("--docs-dir")
1519
+ known, _ = probe.parse_known_args(argv[1:])
1520
+ try:
1521
+ discovered, name = sdlc_core.resolve_docs_dir(known, known.root)
1522
+ except sdlc_core.AmbiguousDocsRoot:
1523
+ # Mirror the spine's degraded orient: fall back to the default
1524
+ # docs dir rather than losing the router on a half-migrated tree.
1525
+ discovered, name = None, sdlc_core.DEFAULT_DOCS_DIR
1526
+ root = (Path(known.root).resolve() if known.root
1527
+ else (discovered or sdlc_core.find_project_root()))
1528
+ topics = root / name / "topics"
1529
+ index = topics / "INDEX.md"
1530
+ if index.is_file():
1531
+ lines = sdlc_core.read_text(index).splitlines()
1532
+ print("\n## Topic router")
1533
+ cap = 30
1534
+ for line in lines[:cap]:
1535
+ print(line)
1536
+ if len(lines) > cap:
1537
+ print("(+%d more lines -- read topics/INDEX.md)" % (len(lines) - cap))
1538
+ elif topics.is_dir():
1539
+ nodes = [p for p in topics.glob("*.md") if p.name != "INDEX.md"]
1540
+ if nodes:
1541
+ print("\n## Topic router")
1542
+ print("index absent (%d node files) -- regenerate: sdlc_check.py index"
1543
+ % len(nodes))
1544
+ # no topics/ at all: the project has no graph; print nothing.
1545
+ except (Exception, SystemExit):
1546
+ # SystemExit included: the probe parser raises it on a dangling flag
1547
+ # value -- the spine already reported that usage error; the append must
1548
+ # never turn it into an escape. root/name keep whatever binding they
1549
+ # reached: pre-binding throws leave them None (recency skips), a throw
1550
+ # inside the router print leaves them valid (recency still runs).
1551
+ pass
1552
+ try:
1553
+ # Notes recency (F-040) -- its OWN try, genuinely independent of the
1554
+ # router: a router throw must not kill this line. It measures NOTES
1555
+ # recency, not full ledger freshness (an ingest feeding claims from
1556
+ # given/ leaves it unmoved). Date source: frontmatter `date:` first;
1557
+ # per-note mtime as fallback -- mtime LIES after clone/worktree (the
1558
+ # skill's own worktree hygiene makes that routine), which is why the
1559
+ # frontmatter date is primary.
1560
+ if root is None:
1561
+ return rc
1562
+ notes_dir = root / name / "corpus" / "notes"
1563
+ if notes_dir.is_dir():
1564
+ import datetime as _dt
1565
+ newest = None
1566
+ for p in notes_dir.glob("*.md"):
1567
+ stamp = None
1568
+ try:
1569
+ head = sdlc_core.read_text(p)[:600]
1570
+ # `date:` counts only INSIDE the frontmatter block: a
1571
+ # column-0 date: in a note's body must not misdate it.
1572
+ if head.startswith("---"):
1573
+ end = head.find("\n---", 3)
1574
+ if end != -1:
1575
+ m = re.search(r"^date:\s*(\d{4}-\d{2}-\d{2})",
1576
+ head[:end], re.M)
1577
+ if m:
1578
+ stamp = _dt.date.fromisoformat(m.group(1))
1579
+ except Exception:
1580
+ pass
1581
+ if stamp is None:
1582
+ try:
1583
+ stamp = _dt.date.fromtimestamp(p.stat().st_mtime)
1584
+ except Exception:
1585
+ continue
1586
+ if newest is None or stamp > newest:
1587
+ newest = stamp
1588
+ if newest is None:
1589
+ print("\nno notes yet (corpus/notes)")
1590
+ else:
1591
+ days = (_dt.date.today() - newest).days
1592
+ print("\nnewest note: %d days old (corpus/notes)" % max(days, 0))
1593
+ except (Exception, SystemExit):
1594
+ # Fail-open: nothing in the recency append may break orient. (The
1595
+ # probe parser lives in the router try above; this guard covers the
1596
+ # date/mtime walk and the print itself.)
1597
+ pass
1598
+ return rc
1599
+
1600
+
1494
1601
  def kb_cmd_help():
1495
1602
  """The spine's usage, then the overlay's own commands.
1496
1603
 
@@ -1525,6 +1632,10 @@ def main(argv=None):
1525
1632
  # 2026-08-02). Intercepted at argv[0] ONLY.
1526
1633
  if argv and argv[0] in ("-h", "--help"):
1527
1634
  return kb_cmd_help()
1635
+ # `orient` is special-cased BEFORE the overlay argparse, like --help: the
1636
+ # spine runs it on the raw argv, the overlay only appends the topic router.
1637
+ if argv and argv[0] == "orient":
1638
+ return kb_cmd_orient(argv)
1528
1639
  # Forward-by-default: anything not intercepted goes to the spine untouched.
1529
1640
  # Never a hand-copied command tuple - that is how a spine command gets
1530
1641
  # silently dropped (mkt_check.py ships that exact defect with `migrate`).
@@ -78,3 +78,12 @@ unreachable nodes as errors, but the refusal at write time is what prevents them
78
78
  A merged or renamed topic keeps its file: `status: SUPERSEDED`, `redirect_to: <slug>`,
79
79
  body emptied. Inbound references resolve through it; a claim re-placed from an old
80
80
  source lands on the survivor instead of resurrecting the dead slug.
81
+
82
+ ## 6. The same descent, answer mode
83
+
84
+ The descent above also serves ANSWERING (`SKILL.md` §Topic Recall): when a reply would
85
+ assert facts about the project's domain, descend before answering from model memory.
86
+ Differences only — no placement verdicts, no writes; the descent stops at reading the
87
+ matched node's claims; coverage (found / not found) replaces the five verdicts; UNPLACED
88
+ does not exist when reading. Everything else applies unchanged: descend don't scan,
89
+ follow every parent, synonyms, tombstone redirects resolve to the survivor.
@@ -448,7 +448,7 @@ adds values to the existing columns rather than a second table.
448
448
  | date | doc_key | tier | reviewer | findings_raised | findings_real | verdict | revise_rounds |
449
449
  |---|---|---|---|---|---|---|---|
450
450
  | 2026-06-11 | ANALYSIS_login_sso.md | design | subagent (opus, fresh ctx) | 4 | 3 | FAIL → PASS | 2 |
451
- | 2026-06-12 | diff feature/sso-login | closure | self-pass (declared; no subagent facility) | 2 | 2 | PASS with findings → corrections re-reviewed, PASS | 2 |
451
+ | 2026-06-12 | diff feature/sso-login | closure | self-pass (declared; absent) | 2 | 2 | PASS with findings → corrections re-reviewed, PASS | 2 |
452
452
 
453
453
  ## Notes
454
454
  <!-- One short paragraph per review that found something worth remembering: what
@@ -463,9 +463,17 @@ and `closure` (Standalone); `deep`, `light`, `code`, `guide`, `vision` (devPNT g
463
463
  and the Vision blind check). The validator reads this column by its header name, so
464
464
  extra or reordered columns are fine — but the header must say `tier`. `reviewer`
465
465
  records the realization actually used — fresh subagent, one-shot client run, or a
466
- **declared** self-pass. Writing `self-pass` where independence was unavailable is
467
- honest; writing nothing, or implying independence you did not have, is the failure
468
- this column exists to prevent. `findings_real` is how many raised findings survived
466
+ **declared** self-pass — and, for any rung below rung 1, WHY the rung(s) above did
467
+ not run, in the ladder's reason words (`review.md`): `absent` — the client has no
468
+ such facility (a claim about the client, never about a policy); `gated, declined` —
469
+ usable only with the user's assent at this gate, withheld (a standing policy
470
+ answered no, or a per-call prompt denied); `gated, unattended` — a standing policy
471
+ gates it, no user reachable; `gated, pre-empted` — gated, nothing asked, an ungated
472
+ lower rung ran the review. A rung-1 row owes nothing. `gated` here is the ROW
473
+ word — it also covers a denied per-call prompt, which never triggers `review.md`'s
474
+ stop. Writing `self-pass` with its
475
+ true reason is honest; writing nothing, or implying independence you did not have,
476
+ is the failure this column exists to prevent. `findings_real` is how many raised findings survived
469
477
  triage: over time it is the only evidence of whether the gate earns its cost.
470
478
  `revise_rounds` counts **review rounds**, not fix cycles: the first review is round 1
471
479
  and every scoped re-review adds one. A review that produced findings which were then