task-pipeline-skill 1.78.4 → 1.79.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -1,3 +1,108 @@
1
+ # Changelog
2
+
3
+ ## v1.79.1 — the probe that a healthy release disarmed
4
+
5
+ The first round of `v1.79.0` failed in the release job on the
6
+ mentioned-vs-declared probe — not because that guard regressed, but because
7
+ v1.79.0 was the first release in seven to carry a run stamp, which emptied the
8
+ trailing set the probe's mutation lived in. A plant that inherits its
9
+ precondition from repository history switches itself off the moment the history
10
+ improves; the healthiest possible release is exactly what disarmed it.
11
+
12
+ The plant now manufactures the guard's own refusal state rather than a proxy
13
+ for it: it computes the trailing set exactly the way the guard does — tag
14
+ ranges walked against the stamp-row commits, matching the stamp-row shape,
15
+ because one draft matched "any line naming the sha" and deleted a standing
16
+ instruction's `Fired at` cell instead — un-stamps the newest stamped release
17
+ when nothing trails, mentions every trailing release unbolded, and then
18
+ un-bolds the declarations. The proxy version died twice on the way here: first
19
+ on the inherited precondition, then on a bold span that truthfully named a
20
+ stamped release. Watched both ways on both trees: the correct guard refuses, a
21
+ deliberately regressed guard that reads mentions as declarations accepts.
22
+
23
+ **`v1.79.0` is a dead tag.** Its round shipped nothing — the suite failed before
24
+ the release step, publish was skipped, npm never saw it — and the tag ruleset
25
+ forbids deleting the tag, so it stays pointed at a tree whose own release suite
26
+ refuses it. The version is burned, not reused: this release carries the whole
27
+ v1.79.0 payload below, plus the probe repair.
28
+
29
+ Guards: 417 → **417** — the repaired plant changed shape, not count.
30
+
31
+ ## v1.79.0 — the gate that could not see, and the example that disarmed it
32
+
33
+ Four defects in the release machinery, each demonstrated live before it was
34
+ fixed, plus the two commits that had been sitting on `main` unreleased — one
35
+ version string was serving different trees through different channels.
36
+
37
+ **A blind release gate now fails closed.** `printf '' | release-gate.sh` exited
38
+ 0 with no output, with a run in flight — an empty or unparseable hook payload
39
+ was a silent skip, so the gate allowed every release for as long as its stdin
40
+ plumbing was broken. That is the exact shape this repository's own retro names:
41
+ a component that never receives its input fails open and is indistinguishable
42
+ from approval. With a ledger present the gate now refuses (exit 2), names the
43
+ payload failure and the next step, and appends an `event: gate-blind` line to
44
+ the ledger so the blindness survives the refusal scrolling away. With no run in
45
+ flight it stays silent, as before — installing the plugin still changes nothing
46
+ in an ungoverned repository.
47
+
48
+ **A green lint stage can no longer stand in for the tests stage.** The stage
49
+ scan took the first stage that was declared `tests` *or* merely carried a gate
50
+ command — so a lint stage declared before the tests stage captured the gate,
51
+ and its green observation released a tag with the suite never run. The scan is
52
+ two-pass now: a stage the project declared as `tests` outranks any stage that
53
+ happens to carry a command. The observer had the same first-match break — only
54
+ the first command-bearing stage was ever recorded — and now records every
55
+ declared command under its own stage id, so the real suite's runs leave the
56
+ trace the release gate corroborates against.
57
+
58
+ **`npm run publish` is a project's script, not the registry act.** `"publish"
59
+ anywhere in npm's argument list` classified it as `npm publish`; the subcommand
60
+ must now be the first non-flag token. Fail-closed overmatch is still overmatch —
61
+ a gate that fights an ordinary script daily is a gate that gets removed.
62
+
63
+ **The build gate now fires on the flow this plugin ships.** It matched
64
+ `state == "build"` while the shipped `pipeline.example.json` names its build
65
+ stage `dev` — the canonical config, copied verbatim per its own note, disarmed
66
+ the hook it ships beside. The declaration path now matches `build` or `dev`,
67
+ mirroring the hook's own ledger fallback, and the fixture loads the example
68
+ unmodified so the two cannot drift apart silently again.
69
+
70
+ **The example no longer arms a loop its own note calls off, and no longer
71
+ teaches the retro deadlock.** `run.loop.mode` was `dynamic` under a `_run_note`
72
+ saying "shipped explicitly OFF" — every project copying the example verbatim
73
+ armed the loop. It is `off` now, with queue, arm and command kept as
74
+ documentation of the shape. And stage 10's check ordered "prune before you add …
75
+ then stamp", which is the deadlock the retro doctrine records: the
76
+ cold-retirement trigger reads the stamp, so a prune placed ahead of it can never
77
+ run on real data. The check now orders stamp → prune → entry and carries the
78
+ sixty-day cold clause the doctrine already had.
79
+
80
+ **Three stage-0 surfaces stopped telling the run to read the uncapped log in
81
+ full.** `templates/retro.md`, `references/knowledge-sources.md` and
82
+ `templates/brief.md` each instructed reading the retro's Recent log end to end —
83
+ contradicting the retro's own header, measured at 74% of the file with nothing
84
+ capping it. All three now say what the command surface already said: standing
85
+ instructions and run stamps are read in full because they are bounded by
86
+ construction; the Recent log and the archive are queried by the task's nouns.
87
+
88
+ **Also in this release, previously on `main` with no section here:**
89
+ `references/prioritisation.md` — the impact ladder that dominates (a gate, not a
90
+ factor: confidence × ease orders within a rung and never across one) with the
91
+ operator above it, and `references/stages.md`'s note on what a stage that
92
+ produces user-facing text owes. Both shipped commits now share a heading with
93
+ the version that carries them.
94
+
95
+ **And this file itself is repaired:** the `# Changelog` H1 sat below five
96
+ version sections with an unheaded block attached — the split half of v1.78.0's
97
+ own entry, orphaned when the section was inserted above the title instead of
98
+ below it. The H1 leads the file again and the orphan is back in its section.
99
+
100
+ Guards: 413 → **417**. Four new plants, one per gate fix — a blind gate reverted
101
+ to skip, the two-pass scan collapsed to first-match, the npm overmatch restored,
102
+ the build-state match narrowed back — each watched failing against the fixtures
103
+ before it shipped. `test/negatives.py`'s floor moved with the count, in this
104
+ change.
105
+
1
106
  ## v1.78.4 — the channel that sends the installs, on npm too
2
107
 
3
108
  - The `skills.sh` badge and the canonical `homepage` reached GitHub in the previous cycle and stopped
@@ -46,8 +151,6 @@ with rows still open prints the attribution and drops the request. Asking to be
46
151
  endorsed while something is still open is asking to be judged on manner rather
47
152
  than on evidence.
48
153
 
49
- # Changelog
50
-
51
154
  **The flow diagram now reads top to bottom, and cannot trip GitHub's renderer.** It
52
155
  was reported failing with *"Could not find a suitable point for the given
53
156
  distance"* — the error mermaid's edge routing raises on long curved edges, which is
package/README.md CHANGED
@@ -193,6 +193,7 @@ until it is installed.
193
193
  | run-wide reporting | [`progress.md`](plugins/task-pipeline/skills/task-pipeline/references/progress.md) — the header block at task start and the one-line iteration close: the stage rail computed from the project's own `pipeline.json`, every number borrowed from the gate that measured it |
194
194
  | run-wide pacing | [`continuity.md`](plugins/task-pipeline/skills/task-pipeline/references/continuity.md) — the loop mode (`run.loop`, default off, never collapses a manual gate) and the context budget (fires on a harness signal, never on an estimate) |
195
195
  | the board between runs | [`backlog.md`](plugins/task-pipeline/skills/task-pipeline/references/backlog.md) — the work-list that outlives a run: seeded at stage 0 or picked up, priority **computed** from three stated inputs rather than assigned, and the seam where the carry-over ledger's `backlog` pointer finally resolves |
196
+ | what to do next | [`prioritisation.md`](plugins/task-pipeline/skills/task-pipeline/references/prioritisation.md) — an impact ladder that dominates, confidence × ease inside the rung, researchers dispatched **before** scoring, and the operator's word overriding all of it |
196
197
  | what a human confirmed | [`verification.md`](plugins/task-pipeline/skills/task-pipeline/references/verification.md) — one row per shipped REQ and the one column a machine may not fill: the date somebody **looked**, or `never`. No floor, no target — `never` is a fact, and a count driven down is a column that has started lying |
197
198
  | how much is unconfirmed | [`exposure.md`](plugins/task-pipeline/skills/task-pipeline/references/exposure.md) — the unconfirmed count as a **vector with its components named**, never a percentage: `P(defect)` is not computable from these inputs and a single score invites a threshold, which is a target on `never`. Plus the check-list, oldest first, and the `/task-pipeline checkup` mode that prints it with no task in flight |
198
199
  | 0 + 9 + any settled decision | [`documentation.md`](plugins/task-pipeline/skills/task-pipeline/references/documentation.md) — the inventory, registers and ids, SSOT, the Doc Loop, supersede semantics, the propagation matrix, intent vs as-built |
@@ -586,7 +587,7 @@ must say so.
586
587
  ### Held to Anthropic's own Skill authoring guidance
587
588
 
588
589
  Audited against the four Agent Skills pages. Most of it already held — `name`
589
- 13/64 chars, `description` inside 1024, `SKILL.md` 334/500 lines, all 36 references
590
+ 13/64 chars, `description` inside 1024, `SKILL.md` 334/500 lines, all 37 references
590
591
  linked **directly** from `SKILL.md`, 436 KB against a 30 MB ceiling. What did not,
591
592
  now does:
592
593
 
package/SKILL-CARD.md CHANGED
@@ -12,7 +12,7 @@ harmless.
12
12
  |---|---|
13
13
  | **Purpose** | Runs a substantial task through ten gated delivery stages — intake grill, docs study, brainstorm, spec, plan, subagent build, tests, lint/deploy, post-deploy, docs+registers, acceptance — refusing to advance until each gate passes |
14
14
  | **Owner** | ssheleg ([github.com/ssheleg/task-pipeline](https://github.com/ssheleg/task-pipeline)) |
15
- | **Version** | 1.78.4 |
15
+ | **Version** | 1.79.1 |
16
16
  | **Surface** | Claude Code (filesystem skill + plugin) and the vercel `skills` CLI. **Not** uploaded to the Skills API; custom Skills do not sync across surfaces |
17
17
  | **Dependencies** | None required. Optional: `context7` (MCP), `figma` (MCP), super-ux, agent-sync, graphify, obsidian-wiki, and **one of two browser channels** — `playwright` (CLI or MCP) or `chrome-devtools` (MCP); either satisfies the browser step and neither is required. Every stage's doctrine ships in-repo; the one conditional requirement is super-ux for the stage-3 UX track on a user-facing task |
18
18
  | **Evaluation status** | Suite authored, 5 categories. One recorded run, **self-observed by the author**; **zero blind runs on zero of three models** — the split, and the numbers, live in [`evals/RESULTS.md`](evals/RESULTS.md) and are computed by `evals/run.py` |
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "task-pipeline-skill",
3
- "version": "1.78.4",
3
+ "version": "1.79.1",
4
4
  "description": "Full-cycle delivery pipeline for coding agents: a mandatory built-in intake grill, then 10 gated stages (docs, brainstorm+decompose, spec, plan, build, tests, lint/deploy, post-deploy, docs/wiki, acceptance). Every stage's doctrine ships inside the skill — no companion plugin required. This package is the installer CLI.",
5
5
  "bin": {
6
6
  "task-pipeline": "bin/task-pipeline.js"
@@ -2,7 +2,7 @@
2
2
  "name": "task-pipeline",
3
3
  "displayName": "Task Pipeline",
4
4
  "description": "Runs a substantial task through a mandatory built-in intake grill, then 10 gated stages (docs, brainstorm+decompose, spec, plan, subagent build, tests, lint/deploy, post-deploy, docs/wiki, acceptance). Every stage's doctrine is built into the skill — no companion plugin required — with typed auto/manual gates, a frozen requirement spine that closes with evidence, a work board and a verification ledger that outlive a run, an exposure line naming what shipped unconfirmed, a progress rail computed from the project's own config, a loop guard whose review ceiling measures rather than stops, and stage-3 tracks for what a product does, how it sounds and how it looks. Two modes need no task: `checkup` (what is unverified) and `setup` (audit existing docs). Retro insights can publish upstream as issues, opt-in and redacted.",
5
- "version": "1.78.4",
5
+ "version": "1.79.1",
6
6
  "author": {
7
7
  "name": "ssheleg",
8
8
  "url": "https://x.com/sshlg93"
@@ -64,7 +64,12 @@ def build_stage_id():
64
64
  try:
65
65
  cfg = json.load(open(os.path.join(project, "pipeline.json"), encoding="utf-8"))
66
66
  for s in cfg.get("stages") or []:
67
- if isinstance(s, dict) and s.get("state") == "build":
67
+ # `build` OR `dev` — mirroring this hook's own ledger fallback
68
+ # (`build|dev`) below. Matching only `build` meant the shipped
69
+ # pipeline.example.json, whose build stage is `state: "dev"`, never
70
+ # armed this gate: the canonical config disarmed the hook it ships
71
+ # beside.
72
+ if isinstance(s, dict) and s.get("state") in ("build", "dev"):
68
73
  return str(s.get("id"))
69
74
  except Exception:
70
75
  pass
@@ -49,23 +49,25 @@ try:
49
49
  except Exception:
50
50
  raise SystemExit(0)
51
51
 
52
- target = None
53
- for s in cfg.get("stages") or []:
54
- if isinstance(s, dict) and (s.get("gate") or {}).get("command"):
55
- target = (str(s.get("id")), (s.get("gate") or {})["command"].strip())
56
- break
57
- if not target:
52
+ # EVERY stage that declares a command, not the first. A first-match scan meant
53
+ # a lint stage declared before the tests stage was the only one ever observed,
54
+ # so the real suite's runs left no trace for the release gate to corroborate —
55
+ # the same first-match break the release gate's own stage scan shipped with,
56
+ # fixed together (sweep the class, not the instance).
57
+ declared = [(str(s.get("id")), (s.get("gate") or {})["command"].strip())
58
+ for s in cfg.get("stages") or []
59
+ if isinstance(s, dict) and (s.get("gate") or {}).get("command")]
60
+ if not declared:
58
61
  raise SystemExit(0)
59
62
 
60
- stage_id, declared = target
61
-
62
63
  # Compared on the normalised command line, not on a substring: `echo "npm test"`
63
64
  # and `npm test --watch` are not the project's gate, and treating them as one puts
64
65
  # a fabricated observation in the ledger the release gate trusts.
65
66
  def norm(s):
66
67
  return " ".join(s.split())
67
68
 
68
- if norm(cmd) != norm(declared):
69
+ matches = [(sid, d) for sid, d in declared if norm(cmd) == norm(d)]
70
+ if not matches:
69
71
  raise SystemExit(0)
70
72
 
71
73
  # PostToolUse fires on success; PostToolUseFailure carries the error. Both are
@@ -78,11 +80,13 @@ else:
78
80
  code = 1 if failed else 0
79
81
 
80
82
  stamp = datetime.datetime.now(datetime.timezone.utc).replace(microsecond=0).isoformat().replace("+00:00", "Z")
81
- line = 'gate: %s — command "%s" — exit %d — %s\n' % (stage_id, norm(declared), code, stamp)
82
83
 
83
84
  # Append-only, like every other line in this file. A ledger that is rewritten is a
84
- # ledger whose history can be edited to say the suite passed.
85
+ # ledger whose history can be edited to say the suite passed. One line per stage
86
+ # that declared this exact command: two stages declaring the same command is two
87
+ # observations, because each stage's gate is corroborated separately.
85
88
  with open(ledger, "a", encoding="utf-8") as fh:
86
- fh.write(line)
89
+ for stage_id, decl in matches:
90
+ fh.write('gate: %s — command "%s" — exit %d — %s\n' % (stage_id, norm(decl), code, stamp))
87
91
  PY
88
92
  exit 0
@@ -54,13 +54,36 @@ decide() {
54
54
  # JSON from there. Watched failing — the gate allowed every release, silently,
55
55
  # because `sys.stdin.read()` came back empty and an empty payload is a skip.
56
56
  HOOK_INPUT="$input" python3 - "$ledger" "$project" <<'PY'
57
- import json, shlex, sys, os, re
57
+ import datetime, json, shlex, sys, os, re
58
58
 
59
59
  ledger, project = sys.argv[1], sys.argv[2]
60
60
  raw = os.environ.get("HOOK_INPUT", "")
61
61
  try:
62
62
  data = json.loads(raw)
63
- except Exception:
63
+ if not isinstance(data, dict):
64
+ raise ValueError("payload is not a JSON object")
65
+ except Exception as exc:
66
+ # An empty or unreadable payload is the gate NOT SEEING — it says nothing
67
+ # about whether a release is happening. With no run in flight that is
68
+ # nobody's business, so stay silent. With a run in flight, a silent skip
69
+ # here is the fail-open this hook exists to close: watched happening —
70
+ # `printf '' | release-gate.sh` exited 0 with no output, and the gate
71
+ # allowed every release for as long as its stdin plumbing was broken. A
72
+ # component that never receives its input is indistinguishable from
73
+ # approval, so it fails CLOSED, and the blindness is written into the
74
+ # ledger (append-only, the lifecycle grammar) so the run can see the gate
75
+ # went blind even after the refusal scrolls away.
76
+ if os.path.exists(ledger):
77
+ why = ("empty payload" if not raw.strip()
78
+ else "unparseable payload (%s)" % type(exc).__name__)
79
+ try:
80
+ stamp = (datetime.datetime.now(datetime.timezone.utc)
81
+ .replace(microsecond=0).isoformat().replace("+00:00", "Z"))
82
+ with open(ledger, "a", encoding="utf-8") as fh:
83
+ fh.write("event: gate-blind — %s — %s\n" % (why, stamp))
84
+ except Exception:
85
+ pass # recording failed; the block below still stands
86
+ print("blind\t%s" % why); raise SystemExit(0)
64
87
  print("skip"); raise SystemExit(0)
65
88
 
66
89
  cmd = ((data.get("tool_input") or {}).get("command") or "")
@@ -99,8 +122,22 @@ def outward(tokens):
99
122
  return "git push <tag>"
100
123
  if t.endswith("gh") and rest[:2] == ["release", "create"]:
101
124
  return "gh release create"
102
- if t.endswith("npm") and "publish" in rest:
103
- return "npm publish"
125
+ if t.endswith("npm"):
126
+ # `publish` must be npm's SUBCOMMAND — the first non-flag token after
127
+ # npm. `"publish" in rest` matched anywhere in the argument list, so a
128
+ # project's own `npm run publish` script was gated as the registry
129
+ # act; fail-closed overmatch is still overmatch, and a gate that
130
+ # fights an ordinary script daily is a gate that gets removed. Known
131
+ # narrowness, stated rather than discovered: a flag whose value is a
132
+ # separate token (`npm --loglevel silent publish`) hides the
133
+ # subcommand from this scan; the `--flag=value` spelling is read
134
+ # correctly.
135
+ for a in rest:
136
+ if a.startswith("-"):
137
+ continue
138
+ if a == "publish":
139
+ return "npm publish"
140
+ break
104
141
  return None
105
142
 
106
143
 
@@ -122,16 +159,25 @@ stage_lines = [l.strip() for l in text.splitlines() if l.strip().startswith("sta
122
159
 
123
160
 
124
161
  def declared_test_stage():
125
- """The tests stage, from the project's own flow. `(id, command)` or None."""
162
+ """The tests stage, from the project's own flow. `(id, command)` or None.
163
+
164
+ TWO PASSES, and the order is the fix. One pass took the first stage that was
165
+ declared `tests` OR merely carried a gate command — so a lint stage declared
166
+ before the tests stage became "the tests stage", and its green observation
167
+ released a tag with the suite never run. A stage the project DECLARED as
168
+ tests outranks any stage that happens to carry a command; the command-bearing
169
+ fallback remains for flows that declare no `tests` state at all."""
126
170
  try:
127
171
  cfg = json.load(open(os.path.join(project, "pipeline.json"), encoding="utf-8"))
128
172
  except Exception:
129
173
  return None
130
- for s in cfg.get("stages") or []:
131
- if not isinstance(s, dict):
132
- continue
174
+ stages = [s for s in cfg.get("stages") or [] if isinstance(s, dict)]
175
+ for s in stages:
176
+ if s.get("state") == "tests":
177
+ return (str(s.get("id")), (s.get("gate") or {}).get("command"))
178
+ for s in stages:
133
179
  gate = s.get("gate") or {}
134
- if s.get("state") == "tests" or gate.get("command"):
180
+ if gate.get("command"):
135
181
  return (str(s.get("id")), gate.get("command"))
136
182
  return None
137
183
 
@@ -207,6 +253,21 @@ why=$(printf '%s' "$verdict" | cut -f3)
207
253
 
208
254
  case "$state" in
209
255
  skip|ok) exit 0 ;;
256
+ blind)
257
+ # act holds the reason here — the payload never carried a command to name.
258
+ cat >&2 <<EOF
259
+ task-pipeline: this project has a run in flight ($ledger) and the release gate
260
+ received an unreadable hook payload ($act), so it cannot tell whether this
261
+ command is an outward act. A gate that cannot see fails CLOSED — a blind gate
262
+ that waves things through is indistinguishable from approval, and that is the
263
+ exact shape it once shipped with.
264
+
265
+ An \`event: gate-blind\` line was appended to the ledger. Check the hook wiring
266
+ (stdin must carry the PreToolUse JSON), then re-run the command. To work without
267
+ the pipeline, remove the ledger ($ledger) or say «без пайплайна» and take the
268
+ route by hand.
269
+ EOF
270
+ exit 2 ;;
210
271
  block)
211
272
  cat >&2 <<EOF
212
273
  task-pipeline: \`$act\` is an outward, irreversible act and the tests gate has not
@@ -62,8 +62,38 @@ way to teach an agent to route around the rule where it actually protects someth
62
62
  pass through here: do it directly and **say out loud** that the claim is unbacked, rather
63
63
  than presenting an estimate as a measurement (canon 8).
64
64
 
65
+ ## Whom the canons serve
66
+
67
+ **The operator's intent is the point. Evidence is how it survives contact with reality —
68
+ not a licence to refuse it.**
69
+
70
+ Everything above exists so the operator gets the result they actually wanted and nothing
71
+ breaks quietly on the way. It does not exist to decide *what they are allowed to say*. A
72
+ skill that reads canon 8 as "I may not write an unverified claim" and refuses the work has
73
+ inverted its own purpose: it protected a rule and lost the person the rule was for.
74
+
75
+ The line, and it is not the same line:
76
+
77
+ | The operator may | The operator may not, and this is refusal ground |
78
+ |---|---|
79
+ | assert something not yet true — a landing page describing the product they are building, a roadmap, a pitch | make a **measurement** say something it did not: a test that passed, a benchmark, a count, a citation |
80
+ | ship an unbacked claim knowingly, after being told once | have an estimate **presented as** a measurement (canon 8), which is the one thing no intent authorises |
81
+
82
+ **So the sequence on an unproven claim is: say it once, label it, do the work.** Not a
83
+ negotiation, not a second warning, and never a silent refusal dressed as a question. If
84
+ the operator confirms they know, the claim ships with a marker naming it as forward-looking
85
+ — that marker is the whole of what this skill owes here, and the task gets done.
86
+
87
+ **Then do the harder half.** Following intent is the floor, not the service. The service is
88
+ making that intent *better* — more structural, more predictable, easier to maintain and
89
+ extend than the operator asked for — while still being the thing they asked for. An agent
90
+ that only obeys is a slower keyboard.
91
+
65
92
  ## The one test
66
93
 
67
94
  Before a document ships, read it for the sentence that would embarrass you if someone
68
- asked *"how do you know?"* — and either give that sentence its address, or delete it.
69
- Everything above is that question, made mechanical.
95
+ asked *"how do you know?"* — then do one of **three** things: give that sentence its
96
+ address, delete it, or **mark it as an unbacked claim the operator chose to make and ship
97
+ it**. The third option is not a loophole; it is the reason the other two are worth
98
+ anything. A rule with no way to proceed under it becomes a rule people route around, and
99
+ then nothing carries an address.
@@ -73,7 +73,7 @@ gate stops until it is installed.
73
73
  | run-wide · what the run **leaves running and leaves behind** — every gate, and stage 10 | `references/residue.md` |
74
74
  | run-wide · what the run **prints about itself** — the rail, the iteration line | `references/progress.md` |
75
75
  | run-wide · how a run keeps going (the loop mode + the context budget) | `references/continuity.md` |
76
- | run-wide · the work-list **between** runs, and how its priority is computed | `references/backlog.md` |
76
+ | run-wide · the work-list **between** runs, and the order it comes off | `references/backlog.md` + `references/prioritisation.md` |
77
77
  | run-wide · whether a **human** ever confirmed what shipped, and when | `references/verification.md` |
78
78
  | run-wide · how much unconfirmed work has piled up, and what to look at first | `references/exposure.md` |
79
79
  | any stage · Where each artifact belongs | `references/artifacts.md` |
@@ -167,14 +167,14 @@
167
167
  ],
168
168
  "gate": {
169
169
  "type": "manual",
170
- "check": "Close the circle. FIRST the LADDER WALK (references/audit.md), because the REQ table can only find what was named and lost — a comparison needs two sides and an absence has one: walk each REQ bottom-up through its rungs (decision -> spec section -> contract AND its failure behavior -> plan task -> change -> executed test -> surface/docs), check the seam at each step, order findings BY SEAM not by file, and turn every absence into a new REQ row with its check BEFORE the table is written; findings belonging to a lower layer go back to that layer (spec -> stage 3, plan -> stage 4); record the pass's two counts (new findings vs findings caused by this run's own fixes) so the next pass can tell whether the axis is exhausted. THEN the coverage table: every REQ has a status (verified / partial / deferred / dropped) — none unknown; every verified carries evidence (a passing test name, file:line, a command and its output, or a scenario ID) — 'done' without evidence is downgraded to partial, not upgraded, and a green from a check nobody has watched fail against a planted defect is not evidence at all; every partial names what is missing and where it is tracked; every deferred/dropped has the operator's agreement and, for deferred, a tracker entry; no carry-over row is left unresolved and the ledger's counts are printed beside this verdict, so 'green' never reads as 'verified'; EVERY REPOSITORY IS CLOSED, THE PARENT INCLUDED — a submodule is finished only when its parent points at it, so 'git submodule status' shows no line starting with '+' and every repo is clean and pushed ('git -C <repo> status --porcelain' and 'git -C <repo> log @{u}..HEAD' both empty), because a parent records a submodule as a pointer to one commit and moving the submodule does not move the pointer: neither repo looks wrong alone and the disagreement survives every check that runs inside one; and the operator answers the closing question — here is what you asked for, here is what shipped, here is what is deferred, what is missing? — and signs off. LAST ACT, THE RETROSPECTIVE (references/retrospective.md, written to docs/evidence/retro.md — one file per project, not per run, because every gate in this flow is good at THIS run and blind across runs): PRUNE BEFORE YOU ADD — every standing instruction checked against its three retirement triggers (it became a check; every path/command/stage it names is gone; it has not fired in the last five run stamps), the list held to its hard cap of ten (at eleven the oldest never-fired row goes — 'they all matter' is the state in which the list stopped being read), and EVERY DELETION LOGGED as one line, never silent; THEN stamp the run (date, topic, verdict, counts); THEN, only if the run diverged, write the entry — symptom with evidence, the stage it surfaced at, the stage that OWNED it, the root cause ('the agent was careless' is not one), the fix by grade (mechanical check > standing instruction with its retire-when written at birth > a note that expires in two runs), and the check that catches it the first time from now on. A retro left empty after a messy run is the failure this file exists to stop, and the retro counts are printed beside this gate's verdict like the carry-over ledger's, so a list that quietly grew back is visible where it happened. EVERY LESSON CARRIES ITS COMMIT: each standing instruction has the SHA that introduced it and the SHA of the run in which it last fired, each log entry and each retirement carries one, the run stamp carries the run's own — a file:line rots at the next edit while 'git show <sha>' reconstructs the whole incident two months later — and every SHA must resolve, which the documentation gate checks with 'git rev-parse --verify'. ROTATION: entries older than the last five run stamps MOVE into docs/evidence/retro/YYYY-QN.md, which is append-only and QUERIED rather than read, so the in-force file stays short enough to be read in full and pruning costs no knowledge. AND THE GATE ITSELF IS PROVEN: every check this close-out leans on — the documentation gate included — has been seen failing once against a planted defect, with the probe recorded, and its ratchet counts are printed beside this verdict (references/gates.md). THE HAND-BACK IS WRITTEN — the request quoted as GIVEN, progress against it, what was solved with evidence, what surfaced unasked, every waiting decision ASKED here with options, and the ambiguity count computed from the four registers; zero prints as zero."
170
+ "check": "Close the circle. FIRST the LADDER WALK (references/audit.md), because the REQ table can only find what was named and lost — a comparison needs two sides and an absence has one: walk each REQ bottom-up through its rungs (decision -> spec section -> contract AND its failure behavior -> plan task -> change -> executed test -> surface/docs), check the seam at each step, order findings BY SEAM not by file, and turn every absence into a new REQ row with its check BEFORE the table is written; findings belonging to a lower layer go back to that layer (spec -> stage 3, plan -> stage 4); record the pass's two counts (new findings vs findings caused by this run's own fixes) so the next pass can tell whether the axis is exhausted. THEN the coverage table: every REQ has a status (verified / partial / deferred / dropped) — none unknown; every verified carries evidence (a passing test name, file:line, a command and its output, or a scenario ID) — 'done' without evidence is downgraded to partial, not upgraded, and a green from a check nobody has watched fail against a planted defect is not evidence at all; every partial names what is missing and where it is tracked; every deferred/dropped has the operator's agreement and, for deferred, a tracker entry; no carry-over row is left unresolved and the ledger's counts are printed beside this verdict, so 'green' never reads as 'verified'; EVERY REPOSITORY IS CLOSED, THE PARENT INCLUDED — a submodule is finished only when its parent points at it, so 'git submodule status' shows no line starting with '+' and every repo is clean and pushed ('git -C <repo> status --porcelain' and 'git -C <repo> log @{u}..HEAD' both empty), because a parent records a submodule as a pointer to one commit and moving the submodule does not move the pointer: neither repo looks wrong alone and the disagreement survives every check that runs inside one; and the operator answers the closing question — here is what you asked for, here is what shipped, here is what is deferred, what is missing? — and signs off. LAST ACT, THE RETROSPECTIVE (references/retrospective.md, written to docs/evidence/retro.md — one file per project, not per run, because every gate in this flow is good at THIS run and blind across runs): STAMP THE RUN FIRST (date, topic, verdict, counts) the order is load-bearing, not style: the cold-retirement trigger reads the stamp this stage writes, so a prune placed ahead of the stamp can never run on real data; THEN PRUNE — every standing instruction checked against its retirement triggers (it became a check; every path/command/stage it names is gone; it has not fired in the last five run stamps, or in the last sixty days), the list held to its hard cap of ten (at eleven the oldest never-fired row goes — 'they all matter' is the state in which the list stopped being read), and EVERY DELETION LOGGED as one line, never silent; THEN, only if the run diverged, write the entry — symptom with evidence, the stage it surfaced at, the stage that OWNED it, the root cause ('the agent was careless' is not one), the fix by grade (mechanical check > standing instruction with its retire-when written at birth > a note that expires in two runs), and the check that catches it the first time from now on. A retro left empty after a messy run is the failure this file exists to stop, and the retro counts are printed beside this gate's verdict like the carry-over ledger's, so a list that quietly grew back is visible where it happened. EVERY LESSON CARRIES ITS COMMIT: each standing instruction has the SHA that introduced it and the SHA of the run in which it last fired, each log entry and each retirement carries one, the run stamp carries the run's own — a file:line rots at the next edit while 'git show <sha>' reconstructs the whole incident two months later — and every SHA must resolve, which the documentation gate checks with 'git rev-parse --verify'. ROTATION: entries older than the last five run stamps MOVE into docs/evidence/retro/YYYY-QN.md, which is append-only and QUERIED rather than read, so the in-force file stays short enough to be read in full and pruning costs no knowledge. AND THE GATE ITSELF IS PROVEN: every check this close-out leans on — the documentation gate included — has been seen failing once against a planted defect, with the probe recorded, and its ratchet counts are printed beside this verdict (references/gates.md). THE HAND-BACK IS WRITTEN — the request quoted as GIVEN, progress against it, what was solved with evidence, what surfaced unasked, every waiting decision ASKED here with options, and the ambiguity count computed from the four registers; zero prints as zero."
171
171
  }
172
172
  }
173
173
  ],
174
- "_run_note": "Run-wide pacing. Shipped explicitly OFF so this example DEMONSTRATES the default rather than relying on its absence — see references/continuity.md. Set mode to 'interval' and a project stops being asked whether to run item-by-item; it still stops at every manual gate. There is no context-budget field here on purpose: the threshold is not measurable, so it is unconditional doctrine, not config.",
174
+ "_run_note": "Run-wide pacing. Shipped explicitly OFF so this example DEMONSTRATES the default rather than relying on its absence — see references/continuity.md. (It shipped saying this while mode was 'dynamic' — an example whose note and config disagree arms a loop in every project that copies it verbatim, which is the opposite of demonstrating the default.) The queue, arm and command keys stay filled in as documentation of the shape a project switches on; only mode arms anything. Set mode to 'interval' or 'dynamic' and a project stops being asked whether to run item-by-item; it still stops at every manual gate. There is no context-budget field here on purpose: the threshold is not measurable, so it is unconditional doctrine, not config.",
175
175
  "run": {
176
176
  "loop": {
177
- "mode": "dynamic",
177
+ "mode": "off",
178
178
  "queue": "module-map",
179
179
  "arm": "after-decomposition",
180
180
  "command": "/loop"
@@ -122,6 +122,12 @@ Two consequences worth stating, because both are the point:
122
122
  `sev` and `blast` are judgement, and they are written down *as* judgement: two small
123
123
  integers a reader can disagree with, rather than a ranking they can only accept.
124
124
 
125
+ **This column ranks findings against each other.** When findings and product work compete
126
+ in one list — which is what happens the moment somebody asks *what next* — the order comes
127
+ from [`prioritisation.md`](prioritisation.md): an impact ladder that dominates, then
128
+ confidence × ease inside the rung. `sev × blast` is evidence for the rung there, not a
129
+ substitute for it.
130
+
125
131
  ## The shape is not fixed, so nothing may assume it
126
132
 
127
133
  This template ships one shape. Projects change it, and they are right to — the family
@@ -50,7 +50,7 @@ makes the grill's answers *checkable* instead of merely confident.
50
50
  | 4b | **The task register, for its *state*** | `docs/ROADMAP.md`, a board, a backlog, the tracker `CLAUDE.md` names | **what is open right now** — read with a command, never from memory; see *Carried-in claims* |
51
51
  | 5 | **Product/UX docs** | `docs/ux/` (super-ux chain), `README`, runbooks | user-facing behavior that is already specified |
52
52
  | 6 | **Pipeline history** | `<artifacts>/specs/`, `plans/`, past `-carryover.md` | what a previous run of this pipeline decided or deferred |
53
- | 7 | **The retro, in force** | `<artifacts>/retro.md` ([`retrospective.md`](retrospective.md)) | what previous runs got wrong here — **read in full**: standing instructions (capped at ten), run stamps and the recent-log window, all bounded by construction |
53
+ | 7 | **The retro, in force** | `<artifacts>/retro.md` ([`retrospective.md`](retrospective.md)) | what previous runs got wrong here — **read in full**: standing instructions (capped at ten) and run stamps (one line each), both bounded by construction; the *Recent log* is **queried** by the task's nouns, like the archive — nothing caps it |
54
54
  | 7a | **The retro archive** | `<artifacts>/retro/YYYY-QN.md` | *have we been bitten by this class before?* — **queried** by the task's nouns, never read end to end |
55
55
  | 8 | **The knowledge wiki** | see below | distilled cross-project knowledge, prior sessions, why decisions were made |
56
56
  | 9 | **Other doc repos the project names** | a docs repo URL or submodule in `CLAUDE.md`/`README`, a sibling checkout, a `docs/` monorepo package | specs, contracts and runbooks that live outside this repo |
@@ -57,6 +57,7 @@ a row pointing outside the bundle is the defect this file exists to catch.
57
57
  | **The run mode** — item-by-item pacing, default off, what it never collapses | `references/continuity.md` |
58
58
  | **The context budget** — the evidence rule and what a flush actually updates | `references/continuity.md` |
59
59
  | **The board** — the work-list between runs, its computed priority, and the ledger seam it resolves | `references/backlog.md` |
60
+ | **The order** — the impact ladder, confidence and ease, research before scoring, and where the human goes | `references/prioritisation.md` |
60
61
  | **The verification ledger** — what shipped, and whether a human ever confirmed it | `references/verification.md` |
61
62
  | **Exposure** — the unconfirmed count as a named vector, never a probability, and the `checkup` mode | `references/exposure.md` |
62
63
  | The retro: prune, cap, commits, archive | `references/retrospective.md` |
@@ -0,0 +1,165 @@
1
+ # Prioritisation — what to do next, and why that and not the other thing
2
+
3
+ **Load this when** the task was not named, when the next row comes off the backlog, when
4
+ the operator asks what to work on, or when a run is about to spend a day on something and
5
+ nobody has checked that it is the most valuable day available.
6
+
7
+ `backlog.md` owns the **board** — what a row is, where it lives, how it closes. This owns
8
+ the **order** the rows come off it.
9
+
10
+ ## Contents
11
+
12
+ - The default, and its one exception
13
+ - 1. The impact ladder
14
+ - 2. Confidence and Ease
15
+ - 3. The order, and why it is not a product
16
+ - 4. Research before scoring, not after
17
+ - 5. Where the human goes
18
+ - 6. Two models in one family, and why both are right
19
+
20
+ ## The default, and its one exception
21
+
22
+ **Every run assumes a backlog exists and that this task competes with it.** A task arriving
23
+ with no stated priority is not priority-free; it is unranked, which is a different and
24
+ worse thing.
25
+
26
+ **The exception is the operator, and it is absolute.** When they name the task, that is the
27
+ task. The most this file authorises is **one line** — *"`BL-14` scores higher: a checkout
28
+ error affecting paying users. Say the word and I switch; otherwise starting yours now"* —
29
+ and then starting theirs. Not a second ask, not a hedge, not a silent substitution. An
30
+ operator who knows what they are doing is the most reliable prioritiser in the system, and
31
+ an agent that argues with them twice has made itself expensive to use.
32
+
33
+ ## 1. The impact ladder
34
+
35
+ Impact is the rung, not a feeling. The ladder exists so a bug and a feature can be compared
36
+ at all — without it, "impact" is scored per-item by whoever is looking, and every item is
37
+ somebody's priority.
38
+
39
+ | Rung | What it means | Test |
40
+ |---|---|---|
41
+ | **I5** | the product does not work for someone **right now** | data loss, an outage, a broken purchase, a blocked signup |
42
+ | **I4** | a product or business metric moves | revenue, activation, retention, traffic, conversion |
43
+ | **I3** | it works, and the experience degrades | crash-free rate, error rate, latency, a rough edge people hit |
44
+ | **I2** | our own speed | debt, tooling, tests, anything that changes how fast the next thing ships |
45
+ | **I1** | polish | nobody's metric moves; it is better and that is all |
46
+
47
+ Two rules that carry most of the value:
48
+
49
+ - **An unmeasured claim of I4 is I3 until it is measured.** "This will lift conversion" is
50
+ a hypothesis; the rung is what the evidence supports, and the gap goes in Confidence.
51
+ - **Blocking is not a rung, it is a multiplier on someone else.** A task nobody can proceed
52
+ past inherits the highest rung it blocks. State whose work it blocks, or it is not
53
+ blocking — it is just old.
54
+
55
+ ## 2. Confidence and Ease
56
+
57
+ Both are 1–3, both are judgement, and both are written down **as** judgement — small
58
+ integers a reader can disagree with, in the row, next to the rung.
59
+
60
+ **Confidence** — how sure are we that doing this produces the effect claimed?
61
+
62
+ | | |
63
+ |---|---|
64
+ | **C3** | measured, or the mechanism is obvious and the change is local |
65
+ | **C2** | reasoned from something real — a trace, a ticket, one user |
66
+ | **C1** | a guess we believe. Fine to hold, not fine to hide |
67
+
68
+ **Ease** — how cheap is it, including the parts nobody counts?
69
+
70
+ | | |
71
+ |---|---|
72
+ | **E3** | within a run, one surface, reversible |
73
+ | **E2** | a few surfaces, or one that needs coordination |
74
+ | **E1** | crosses repositories, needs a migration or a credential, or cannot be undone cheaply |
75
+
76
+ Ease counts the **whole** cost: the migration, the review, the rollback, the second
77
+ repository. An E3 that turns out to need a credential from a person who is asleep was an E1
78
+ and the estimate was the defect.
79
+
80
+ ## 3. The order, and why it is not a product
81
+
82
+ ```
83
+ sort by I descending the ladder dominates
84
+ then by C × E descending 1..9 within the rung
85
+ then by age descending the tie-break, oldest first
86
+ ```
87
+
88
+ **Classic ICE multiplies I × C × E, and this deliberately does not.** Multiplying lets a
89
+ trivial certain easy win outrank a hard uncertain critical one — `I1×C3×E3 = 9` beats
90
+ `I5×C1×E1 = 5` — and shipping the 9 while the 5 waits is precisely the failure this file
91
+ exists to prevent. It is also the failure that feels most productive from inside: a
92
+ stream of small completed things while the thing that matters sits.
93
+
94
+ So the ladder is a **gate**, not a factor. `C × E` orders within a rung and never across
95
+ one.
96
+
97
+ **What age may and may not do.** It breaks ties, and past **30 days** the row is reported
98
+ as one the queue has been lying about — surfaced in the run's output, not silently
99
+ promoted. Age never lifts a row across a rung: a polish item that has waited a year is
100
+ still polish, and letting it climb is how a queue ends up sorted by patience.
101
+
102
+ **`prio` in `backlog.md` is not replaced.** That formula (`sev × blast + age_bonus`) ranks
103
+ *findings against each other* and stays the board's own column. This file's ordering is
104
+ what runs when findings and product work compete in **one** list — the finding's rung comes
105
+ from the ladder above, and `sev × blast` is evidence for it, not a substitute.
106
+
107
+ ## 4. Research before scoring, not after
108
+
109
+ **A row is scored from what it touches, never from its title.** "Fix the login redirect"
110
+ is E3 by its name and E1 once you find it crosses an auth library, a cookie domain and a
111
+ cached edge rule. Scoring first and discovering second produces an order that was wrong
112
+ before the first task started.
113
+
114
+ So, before an order is emitted:
115
+
116
+ 1. **Fan out one researcher per candidate row** — same rules as `build.md` §4.2: fresh
117
+ context each, given the row and nothing else, no shared working tree because they write
118
+ nothing.
119
+ 2. **Each returns four things and no opinion**: what it touches (`file:line`, services,
120
+ external systems), what it depends on and what depends on it, what is already
121
+ *measured* about the claimed effect, and what would have to be true for it to be wrong.
122
+ 3. **Score after they return.** The rung comes from the evidence, `C` from what the
123
+ researcher could and could not confirm, `E` from the surfaces they found.
124
+ 4. **Emit the ordered list once**, with the three numbers and one line of why per row.
125
+
126
+ A researcher that comes back with "seems fine, medium effort" has returned nothing. The
127
+ four fields are the contract; anything else is a subagent that spent context to guess.
128
+
129
+ **Do not fan out to score two rows.** The dispatch costs more than reading them.
130
+
131
+ ## 5. Where the human goes
132
+
133
+ The operator is a **scarce, high-value input**, and the failure mode is spending them one
134
+ question at a time.
135
+
136
+ - **Ask at the start, or in a batch at a boundary. Never after each task.** If the whole
137
+ queue needs one decision, ask it before the queue starts.
138
+ - **A blocked row does not stop the run.** When something needs a person: first satisfy
139
+ yourself that you are actually right and it is actually blocked — most "I need input"
140
+ is an unread file — then write the question **into the row**, mark it `needs-operator`,
141
+ and take the next row that is not blocked by the same answer.
142
+ - **The question in the row is answerable without context.** What was tried, what is
143
+ needed, and what happens under each answer. A question that requires re-reading the run
144
+ to understand is a question that waits another day.
145
+ - **Batch the answers back.** The operator returns to a list of questions with their
146
+ consequences, not to a transcript.
147
+
148
+ The measure of this section: **how much of the queue moved while nobody was watching**, and
149
+ whether the things that stopped genuinely could not proceed.
150
+
151
+ ## 6. Two models in one family, and why both are right
152
+
153
+ `seo-aeo-audit` triages on impact, irreversibility, uncertainty and coordination, and states
154
+ that **effort is recorded and never ranks**.
155
+ This file makes Ease rank. That is not a contradiction and the difference is the queue:
156
+
157
+ | | an **audit** | a **backlog** |
158
+ |---|---|---|
159
+ | the list is | everything that is wrong | everything that could be done |
160
+ | letting effort rank means | a blocker gets skipped for being hard, and the audit lies by omission | the queue self-selects toward value per day |
161
+ | so effort | is recorded and never ranks | ranks, inside the rung |
162
+
163
+ An audit must be complete before it is ordered; a backlog is ordered because it will never
164
+ be complete. Reach for the wrong one and the symptom is recognisable: an audit that
165
+ quietly dropped its hardest finding, or a backlog where nothing small ever ships.
@@ -66,6 +66,7 @@ never that the work was skipped quietly.
66
66
  - Cross-cutting — the Doc Loop
67
67
  - Cross-cutting — the loop guard
68
68
  - Cross-cutting — the audit
69
+ - A stage that produces text a user will read
69
70
 
70
71
  ## 0 — Intake grill — MANDATORY
71
72
  - **Freedom: medium** — the interview adapts to the answers; its two phases and their order do not ([`gates.md`](gates.md) → *Axis C*).
@@ -860,3 +861,22 @@ nothing.
860
861
  - **Whatever can't be fixed now becomes a ratchet** — a named, counted set that may
861
862
  only shrink, printed beside every gate verdict, so "green" never reads as
862
863
  "verified".
864
+
865
+ ## A stage that produces text a user will read
866
+
867
+ The framework has no opinion on which skills run where — `pipeline.json` is the contract —
868
+ but two facts about copy are worth stating once, because a stage author rediscovers them
869
+ expensively.
870
+
871
+ **Route it through the copy skill, not through the stage's own prose.** A stage that writes
872
+ interface strings, a landing page or a changelog entry for users is producing brand surface;
873
+ the registers, terminology and canonical facts that constrain it live in `docs/brand/`, and
874
+ a stage writing directly does not read them.
875
+
876
+ **The humanization pass is decided once, in the brand pack, not per run.**
877
+ `docs/brand/voice.md` carries an optional `Humanization pass:` field; absent means nobody
878
+ has been asked, and the copy skill asks once and records the answer. A pipeline that asks
879
+ every run has turned a settled decision into a prompt, and a pipeline that gates on a
880
+ marker count has turned a writing-quality signal into a verdict — the false positives fall
881
+ hardest on people writing in a second language, and nothing here fails a stage for them.
882
+ `npx sshlg-skills humanizers` lists what is installed.
@@ -1042,7 +1042,7 @@ def cmd_producer(graph, args):
1042
1042
  def cmd_doctrine(graph, args):
1043
1043
  """Which doctrine this run actually read — B-061.
1044
1044
 
1045
- The bundle is 36 reference files. A run reads some subset and nothing recorded which,
1045
+ The bundle is 37 reference files. A run reads some subset and nothing recorded which,
1046
1046
  so **a skipped file and a read one were indistinguishable** — the class every guard in
1047
1047
  this repository exists to catch, left standing over the doctrine itself.
1048
1048
 
@@ -59,9 +59,10 @@ source is a recorded decision, an unquoted one is an undetected divergence.
59
59
  - **Knowledge wiki:** installed / not installed
60
60
  ([obsidian-wiki](https://github.com/ar9av/obsidian-wiki); recommended, never a gate)
61
61
  - **Retro, in force:** `docs/evidence/retro.md` — none / N standing instructions
62
- (read **in full**, together with the run stamps and the recent-log window; list
63
- which ones bind this run, and stamp each as it fires **with the commit** — that
64
- stamp is the only evidence behind stage 10's cold-retirement rule)
62
+ (read **in full**, together with the run stamps both bounded by construction;
63
+ the recent log is **queried** by this task's nouns, never read end to end; list
64
+ which instructions bind this run, and stamp each as it fires **with the commit**
65
+ — that stamp is the only evidence behind stage 10's cold-retirement rule)
65
66
  - **Retro archive:** `docs/evidence/retro/` — **queried** by this task's nouns;
66
67
  what it returned: … (or `nothing`)
67
68
  - **Code graph:** built / installed-not-built / not installed
@@ -6,10 +6,12 @@ The order is load-bearing: the cold-retirement trigger reads the stamp this stag
6
6
  writes, so a prune ahead of it can never run on real data (`learned.md` rule 21).
7
7
  Doctrine: `references/retrospective.md`.
8
8
 
9
- **What stage 0 reads in full:** *Standing instructions*, *Run stamps* and *Recent
10
- log* — all three are bounded by construction, which is why the cap is not
11
- negotiable. The **archive** (`docs/evidence/retro/YYYY-QN.md`) is *queried* by
12
- the task's nouns and never read end to end.
9
+ **What stage 0 reads in full:** *Standing instructions* and *Run stamps* both
10
+ are bounded by construction (ten rows, one line per run), which is why the cap is
11
+ not negotiable. The **Recent log** and the **archive**
12
+ (`docs/evidence/retro/YYYY-QN.md`) are *queried* by the task's nouns and never
13
+ read end to end: nothing caps the log's length, and an uncapped section read in
14
+ full is the volume that stops the ten binding rows being read.
13
15
 
14
16
  ## Standing instructions (max 10 — in force right now)
15
17
 
@@ -20,7 +20,7 @@ Run: `<topic>` · started `<YYYY-MM-DD>` · module map: `<path or "none">`
20
20
 
21
21
  ## `read:` — which doctrine this run actually opened
22
22
 
23
- The bundle is 36 reference files and nothing recorded which of them a run read, so **a
23
+ The bundle is 37 reference files and nothing recorded which of them a run read, so **a
24
24
  skipped file and a read one were indistinguishable** — the class every guard in this
25
25
  pipeline exists to catch, left standing over the doctrine itself.
26
26
 
@@ -48,7 +48,7 @@ same claim one level down.
48
48
  |---|---|---|
49
49
  | `unmeasured — no run ledger` | there is no ledger | nothing to read from |
50
50
  | `unmeasured — the ledger carries no read: lines` | the hook is absent, **or** the run opened no doctrine | two opposite facts, and the ledger cannot separate them, so neither is claimed |
51
- | `N of 36 reference files read — unattested`, then each unread one | the hook is installed and fired | the count alone says there is a gap, not where — and `unattested` says the ledger cannot name who wrote the lines |
51
+ | `N of 37 reference files read — unattested`, then each unread one | the hook is installed and fired | the count alone says there is a gap, not where — and `unattested` says the ledger cannot name who wrote the lines |
52
52
 
53
53
  **It is a disclosure: no floor, no direction, never a target.** A run that needs four files
54
54
  and reads four is not worse than one that reads thirty — and the moment the number becomes