task-pipeline-skill 1.101.0 → 1.102.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
@@ -1,3 +1,66 @@
1
+ ## v1.102.0 (2026-10-24)
2
+
3
+ Guards: 530 → **531**; property checks 15 → **15**. Negative 550 is new; the release impact
4
+ checks are `test/release_impact_test.py` (47 cases), wired into `npm test` and `test:all`.
5
+
6
+ - **B-155:** `_is_dated_record()` read a section as dated when its heading and the first two
7
+ blocks under it carried an ISO date; under `## Closed` the second block is the newest closed
8
+ item, so one dated item exempted every number in the section and negative 442 stopped firing.
9
+ The section stamp is now the heading and the one block beneath it. Negative 550 plants a
10
+ dated first Closed item beside a skewed fixture count and must fail.
11
+
12
+ **Every release in a project with users owes a release impact card** (operator, 2026-10-24):
13
+ before a release harms anyone, it says who it touches and how, how it is watched, what tells
14
+ someone it went wrong, and the way back.
15
+
16
+ - **`references/release-impact.md`** and **`templates/release-impact.md`**: the card
17
+ (`release-impact/1`) is owed at stage 7 before any outward act (tag, publish, deploy) when
18
+ `pipeline.json` → `release.users` is `external` or `internal`; absent is unknown and treated as
19
+ `external`; `none` owes no card and prints `users: none` beside the stage 7 verdict. Four
20
+ sections, each answered or `n/a: <reason>`: who is affected and how (segments, changes,
21
+ breakage, rollout, timing), tracking (signals, baseline, where, watch window), alerting
22
+ (condition, receiver, whether it was ever seen firing), rollback or hotfix (procedure, time,
23
+ data reversibility, kill switch, hotfix path, decision trigger). Worked examples for a package
24
+ release and for a web or mobile product.
25
+ - **`scripts/release_impact.py check`** refuses a missing, empty or duplicated section, `n/a`
26
+ without a reason, a `TODO`, tracking without a watch window, alerting that does not say
27
+ whether it ever fired, a rollback without a decision trigger, tracking, alerting or rollback
28
+ answered `n/a` on a release with users, and a card whose `users:` disagrees with the project;
29
+ no card where one is owed is NOT_RUN (exit 3). `--outcome` checks stage 8's record.
30
+ - **Stages:** the stage 7 GATE in `references/stages.md` owes the card; stage 8 watches its
31
+ signals for the window, records the outcome and acts on the decision trigger per the card.
32
+ `pipeline.schema.json` declares `release.users`; this repository's own `pipeline.json` says
33
+ `external`.
34
+
35
+ ## v1.101.2 (2026-10-24)
36
+
37
+ **Not tagged.** Its candidate failed the tagged documentation gate: the B-150 closure cited
38
+ the two fw07 branch heads by SHA, and after the branches were deleted a clone cannot follow
39
+ them. The citation now names the pull request refs; everything below ships in v1.102.0.
40
+
41
+ Guards: 530 → **530**; property checks 15 → **15**. No negative was added.
42
+
43
+ - **The family member gate** (sshlg-skills B-108, B-109): `test/family_gate.py`, a byte-for-byte
44
+ copy of the hub's `member-gate/family_gate.py` with its `# shared-mechanism: family-gate` and
45
+ `# diverges: none` declaration, runs with `--self-test` from
46
+ `test/audit_regressions/family_gate_member.py` (the board parses; `release.yml` keeps the
47
+ tag-ancestry check).
48
+ - **B-156:** `test/lessons_test.py` cleanup tolerates a file git's background maintenance
49
+ already removed (`_rm()`), the race that failed the v1.101.1 release run's first attempt.
50
+ - **B-150 closed:** the two kept fw07 branches were deleted; their heads stay reachable as
51
+ `refs/pull/123/head` and `refs/pull/124/head`.
52
+
53
+ ## v1.101.1 (2026-10-23)
54
+
55
+ Guards: 530 → **530**; property checks 15 → **15**. No negative was added: the new checks are
56
+ two cases in `test/release_gate_test.py` (65 to 67).
57
+
58
+ - **B-153:** when the release gate's python3 decision fails, its fallback refused any command
59
+ that spelled `tag`, `push`, `release` or `publish` anywhere, so reading
60
+ `.git/sshlg-release-candidates/` was refused as an outward act (2026-10-22). It now matches the
61
+ command shapes (`git … tag|push`, `gh … release`, `npm … publish`); every outward act it
62
+ refused before is still refused. Filed as B-152, an id v1.91.1 had already used.
63
+
1
64
  ## v1.101.0 (2026-10-22)
2
65
 
3
66
  Guards: 530 → **530**; property checks 15 → **15**. No negative was added: the new checks are
package/README.md CHANGED
@@ -187,6 +187,7 @@ and the inline fallback satisfy the same [provider contract](plugins/task-pipeli
187
187
  | 5–6 TDD | [`tdd.md`](plugins/task-pipeline/skills/task-pipeline/references/tdd.md) — the iron law, red/green/refactor, the suite gate |
188
188
  | 5, 6, 8 The browser | [`browser.md`](plugins/task-pipeline/skills/task-pipeline/references/browser.md) — the ref model both channels share, the four commands the look is made of, sessions, and the three different things *"tested in a browser"* means |
189
189
  | 3, 6 The design review | [`design-review.md`](plugins/task-pipeline/skills/task-pipeline/references/design-review.md): the `design-tester` agent walks the ten-portrait test panel through the changed scenarios over the Figma frames or mockups before build and in the recorded browser after it, judges every step on a fixed checklist, and closes everything it started (`scripts/design_gate.py`, `scripts/resource_guard.py`) |
190
+ | 7, 8 Release impact | [`release-impact.md`](plugins/task-pipeline/skills/task-pipeline/references/release-impact.md): a release in a project with users owes a card before the tag, the publish or the deploy (who is affected and how, the tracking and its watch window, the alerting and whether it ever fired, the rollback or hotfix and its decision trigger), and stage 8 watches it and acts on the trigger (`scripts/release_impact.py`) |
190
191
  | 10 Acceptance | [`acceptance.md`](plugins/task-pipeline/skills/task-pipeline/references/acceptance.md) — REQ coverage table, evidence rules, the closing question |
191
192
  | 10 + any audit | [`audit.md`](plugins/task-pipeline/skills/task-pipeline/references/audit.md) — the L0→L7 ladder and its seams, axis rotation, ratchets, proven checks |
192
193
  | any loop | [`loop-guard.md`](plugins/task-pipeline/skills/task-pipeline/references/loop-guard.md) — churn detection, caps, the break protocol |
@@ -642,7 +643,7 @@ must say so.
642
643
  ### Held to Anthropic's own Skill authoring guidance
643
644
 
644
645
  Audited against the four Agent Skills pages. Most of it already held — `name`
645
- 13/64 chars, `description` 386/1024 chars, `SKILL.md` 207/500 lines, all 54 references
646
+ 13/64 chars, `description` 386/1024 chars, `SKILL.md` 207/500 lines, all 55 references
646
647
  reachable from `SKILL.md` through explicit reference routes, and the bundle far under the 30 MB ceiling. What
647
648
  did not, now does:
648
649
 
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.101.0 |
15
+ | **Version** | 1.102.0 |
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. The engine is WebKit (Safari) first; `chrome-devtools` is Chrome-only, a stated exception. Every stage's doctrine ships in-repo; user-facing stage 3 requires a valid scenario artifact; super-ux is preferred, with equivalent-provider and inline-fallback paths under the provider contract |
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,13 +1,13 @@
1
1
  {
2
2
  "name": "task-pipeline-skill",
3
- "version": "1.101.0",
3
+ "version": "1.102.0",
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"
7
7
  },
8
8
  "scripts": {
9
- "test": "python3 test/validate.py && python3 test/graph_test.py && python3 test/project_audit_test.py && python3 test/packet_schema_test.py && python3 test/browser_claims_test.py && python3 test/visual_gate_test.py && python3 test/stage_checkpoint_test.py && python3 test/backlog_sources_test.py && python3 test/release_approval_test.py && python3 test/memory_nudge_test.py && python3 test/lessons_test.py && python3 test/waves_test.py && python3 test/design_gate_test.py && python3 test/continuity_doc_test.py && bash test/install_test.sh && npm run test:audit-regressions",
10
- "test:all": "python3 test/validate.py && python3 test/graph_test.py && python3 test/project_audit_test.py && python3 test/packet_schema_test.py && python3 test/browser_claims_test.py && python3 test/visual_gate_test.py && python3 test/stage_checkpoint_test.py && python3 test/backlog_sources_test.py && python3 test/release_approval_test.py && python3 test/memory_nudge_test.py && python3 test/lessons_test.py && python3 test/waves_test.py && python3 test/design_gate_test.py && python3 test/continuity_doc_test.py && bash test/install_test.sh && python3 test/negatives.py && npm run test:certify && npm run test:exposure && npm run test:probe && npm run test:anchors && npm run test:runner && npm run test:hooks && npm run test:artifacts && npm run test:docs && npm run test:audit-regressions",
9
+ "test": "python3 test/validate.py && python3 test/graph_test.py && python3 test/project_audit_test.py && python3 test/packet_schema_test.py && python3 test/browser_claims_test.py && python3 test/visual_gate_test.py && python3 test/stage_checkpoint_test.py && python3 test/backlog_sources_test.py && python3 test/release_approval_test.py && python3 test/memory_nudge_test.py && python3 test/lessons_test.py && python3 test/waves_test.py && python3 test/design_gate_test.py && python3 test/release_impact_test.py && python3 test/continuity_doc_test.py && bash test/install_test.sh && npm run test:audit-regressions",
10
+ "test:all": "python3 test/validate.py && python3 test/graph_test.py && python3 test/project_audit_test.py && python3 test/packet_schema_test.py && python3 test/browser_claims_test.py && python3 test/visual_gate_test.py && python3 test/stage_checkpoint_test.py && python3 test/backlog_sources_test.py && python3 test/release_approval_test.py && python3 test/memory_nudge_test.py && python3 test/lessons_test.py && python3 test/waves_test.py && python3 test/design_gate_test.py && python3 test/release_impact_test.py && python3 test/continuity_doc_test.py && bash test/install_test.sh && python3 test/negatives.py && npm run test:certify && npm run test:exposure && npm run test:probe && npm run test:anchors && npm run test:runner && npm run test:hooks && npm run test:artifacts && npm run test:docs && npm run test:audit-regressions",
11
11
  "test:negatives": "python3 test/negatives.py",
12
12
  "test:exposure": "python3 test/exposure_test.py",
13
13
  "test:probe": "python3 test/probe.py --self-test",
@@ -3,7 +3,7 @@
3
3
  "name": "task-pipeline",
4
4
  "displayName": "Task Pipeline",
5
5
  "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/judgment/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 proportional short path for small changes, a loop guard whose ceilings stop and record a blocker, 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); a third, `waves`, takes the whole board and closes it in waves that each finish in one pass. Retro insights can publish upstream as issues, opt-in and redacted.",
6
- "version": "1.101.0",
6
+ "version": "1.102.0",
7
7
  "author": {
8
8
  "name": "ssheleg",
9
9
  "url": "https://x.com/sshlg93"
@@ -252,10 +252,13 @@ status=$?
252
252
  if [ $status -ne 0 ] || [ -z "$verdict" ]; then
253
253
  [ -f "$ledger" ] || exit 0
254
254
  # B-152: without python3 the command cannot be classified, and refusing EVERY
255
- # Bash call took the whole session down on a full disk. A word-level pre-filter
256
- # keeps fail-closed exactly where it matters: anything that could tag, push,
257
- # release or publish is still refused; `ls` is not.
258
- if ! printf '%s' "$input" | grep -Eq '(^|[^[:alnum:]_])(tag|tags|push|release|publish)([^[:alnum:]_]|$)'; then
255
+ # Bash call took the whole session down on a full disk. A pre-filter keeps
256
+ # fail-closed exactly where it matters: anything that could tag, push, release or
257
+ # publish is still refused; `ls` is not.
258
+ # B-153: it matched the WORDS, so `ls .git/sshlg-release-candidates/` was refused
259
+ # as an outward act. It now matches the command shapes: `git … tag|push`,
260
+ # `gh … release`, `npm … publish`, each word standing alone as an argument.
261
+ if ! printf '%s' "$input" | grep -Eq '(^|[^[:alnum:]_./-])(git([[:space:]]+[^[:space:];&|]+)*[[:space:]]+(tag|push)|gh([[:space:]]+[^[:space:];&|]+)*[[:space:]]+release|npm([[:space:]]+[^[:space:];&|]+)*[[:space:]]+publish)([[:space:]"]|$)'; then
259
262
  exit 0
260
263
  fi
261
264
  echo "task-pipeline: the stage-7 gate could not run (python3 missing or failed), and this project has a run in flight ($ledger). Refusing an outward act rather than failing open." >&2
@@ -188,6 +188,7 @@
188
188
  "enabled": true,
189
189
  "trigger": "tag",
190
190
  "approval": "checks",
191
+ "users": "external",
191
192
  "steps": [
192
193
  "create a GitHub release from the matching CHANGELOG section",
193
194
  "npm publish (task-pipeline-skill) — a second job in the same workflow, armed per repository by the PUBLISH_NPMJS variable (NPM_TOKEN granular automation token, or trusted publishing via OIDC); unarmed it falls back to a human 2FA step"
@@ -310,6 +310,14 @@
310
310
  },
311
311
  "description": "Optional single-line source locator (for example docs/DECISIONS.md:42) for the standing authorization covering the tag and its publication. A locator, not a grant: how it is read and verified lives in references/deploy-targets.md, When the release waits for an approval."
312
312
  },
313
+ "users": {
314
+ "enum": [
315
+ "external",
316
+ "internal",
317
+ "none"
318
+ ],
319
+ "description": "Who uses what this project releases: `external` (people or systems outside the team install, call or open it), `internal` (only the team or its own agents), `none` (nothing ships to anyone). `external` and `internal` owe a release impact card at stage 7 before any outward act (scripts/release_impact.py check); `none` owes no card and the run prints `users: none` beside the stage 7 verdict. Absent means unknown and is treated as `external` (references/release-impact.md)."
320
+ },
313
321
  "approval": {
314
322
  "enum": [
315
323
  "checks",
@@ -36,6 +36,7 @@ docs/
36
36
  YYYY-MM-DD-<topic>-inventory.json # wave mode, stage 0 — every open item by id, verified
37
37
  YYYY-MM-DD-<topic>-canvas.md # wave mode, stages 2–3 — the target state over the current logic
38
38
  YYYY-MM-DD-<topic>-design.md # stage 3 — the spec / module dossier (locks shared contracts)
39
+ YYYY-MM-DD-<topic>-release-impact.md # stage 7 card (users, tracking, alerting, rollback); 8 adds its outcome
39
40
  YYYY-MM-DD-<topic>-acceptance.md # stage 10 — REQ coverage table + evidence
40
41
  plans/
41
42
  YYYY-MM-DD-<topic>.md # stage 4 — the implementation plan
@@ -177,6 +178,7 @@ them is a finding, not a tie-break ([`knowledge-sources.md`](knowledge-sources.m
177
178
  | 3 Spec | `specs/<topic>-design.md` — module dossier for a decomposed platform (+ links `docs/ux/*` for UI) | stage 4 |
178
179
  | 4 Plan | `plans/<topic>.md` | stage 5 |
179
180
  | 3 UX track | `docs/ux/{foundation,flows,screens,scenarios}.md` | stages 4–9 + `/ux-lint` |
181
+ | 7 Release impact | `specs/<topic>-release-impact.md`: the card owed before an outward act when the project has users ([`release-impact.md`](release-impact.md)) | stage 8 watches it and writes its `## Outcome`; stage 10 reads it |
180
182
  | 8 Post-deploy | log/health notes (in the run, not a committed file) | stage 9 |
181
183
  | 9 Docs+wiki | host module docs + wiki pages + the refreshed `graphify-out/graph.json` (`knowledge-graph.md`) | the **next** run's stage-0 harvest |
182
184
 
@@ -38,6 +38,7 @@ Custom profiles use their own declared stages and doctrine, with the kernel inta
38
38
  | any stage · The canons, and where each is enforced | [`evidence-docs`](../../evidence-docs/SKILL.md) — the sibling skill in this plugin |
39
39
  | 6–10 · Gates | `references/gates.md` |
40
40
  | 7–8 · Deploy targets | `references/deploy-targets.md` |
41
+ | 7–8 · Release impact: who a release with users touches, its tracking, alerting and way back, the card owed before the outward act and the watch after it | `references/release-impact.md` |
41
42
  | any stage · Hooks | `references/hooks.md` |
42
43
  | 0 Knowledge harvest (pre-grill) | `references/knowledge-sources.md` |
43
44
  | 0, 9 The code graph (graphify — recommended, never required) | `references/knowledge-graph.md` |
@@ -59,6 +59,7 @@ a row pointing outside the bundle is the defect this file exists to catch.
59
59
  | The documentation system, the Doc Loop, supersede semantics | `references/documentation.md` |
60
60
  | Gate types, the enforcement ladder, degrees of freedom, probing | `references/gates.md` |
61
61
  | Deploy runbook template, per-platform verbs, the verification trio | `references/deploy-targets.md` |
62
+ | **Release impact**: who has users, the card owed before an outward act (affected, tracking, alerting, rollback), the stage 8 watch and its decision trigger | `references/release-impact.md`, `templates/release-impact.md`, `scripts/release_impact.py` |
62
63
  | The Claude Code hook contract | `references/hooks.md` |
63
64
  | First run in a project: greenfield and brownfield | `references/adoption.md` |
64
65
  | The entry audit and what it inspects | `references/setup.md` |
@@ -0,0 +1,154 @@
1
+ # Release impact: who a release touches, how it is watched, and the way back
2
+
3
+ Load this at stage 7, before any outward act of a release (a tag, a publish, a deploy), and at
4
+ stage 8, while the release is watched. Its gate is `scripts/release_impact.py`; its card is
5
+ `templates/release-impact.md`. The rule is the operator's (2026-10-24): whenever a release is
6
+ decided in a project that has users, the release first makes sure the current users are not
7
+ harmed and says how they are affected; and there is always tracking that shows it goes as it
8
+ should, an alert when it does not, and a plan to roll back or ship an urgent fix.
9
+
10
+ ## Contents
11
+
12
+ - Who has users
13
+ - When the card is owed
14
+ - The card
15
+ - What the checker refuses
16
+ - Stage 8: the watch and the decision
17
+ - Worked example: a skill or package release
18
+ - Worked example: a web or mobile product
19
+ - Companions and single homes
20
+ - Rationalizations
21
+
22
+ ## Who has users
23
+
24
+ `pipeline.json` → `release.users` says it, once per project:
25
+
26
+ | Value | Means | The card |
27
+ |---|---|---|
28
+ | `external` | people or systems outside the team install, call or open what ships | owed |
29
+ | `internal` | only the team or the operator's own agents use it | owed (the segments are small, the sections are the same) |
30
+ | `none` | nothing ships to anyone: a scratch repository, a spike, an archive | not owed; the run writes `users: none` beside the stage 7 verdict |
31
+ | absent | **unknown**, treated as `external` for safety | owed, and the run says the value was unknown, asks the operator once and records the answer |
32
+
33
+ An answer is a property of the project, not of one release. A release that ships nothing to
34
+ anyone in a project with users (a docs-only commit that is never tagged) is not a release here.
35
+
36
+ ## When the card is owed
37
+
38
+ Stage 7, before the first outward act, whenever `release.users` is not `none`. The card lives
39
+ beside the run's other records: `<artifacts>/specs/YYYY-MM-DD-<topic>-release-impact.md`, seeded
40
+ from `templates/release-impact.md`, one per release (a release iteration writes one for the
41
+ release it cuts, not one per wave). The stage 7 gate is:
42
+
43
+ ```bash
44
+ python3 scripts/release_impact.py check <card.md> --pipeline pipeline.json
45
+ ```
46
+
47
+ Exit 0 is PASS, 1 FAIL, 2 usage or an unreadable input, 3 NOT_RUN (no card where one is owed).
48
+ NOT_RUN stops the release exactly as FAIL does: it is never green.
49
+
50
+ ## The card
51
+
52
+ Schema `release-impact/1`. A header (`schema`, `release`, `users`) and four sections, each
53
+ **answered** or `n/a: <reason>`. Every line the template marks `TODO` is replaced.
54
+
55
+ 1. **Who is affected and how.** The user segments and a rough count of each; what changes for
56
+ them (behaviour, data, API or file format compatibility, migrations); whether anything breaks
57
+ for an existing user, and for whom; the rollout shape (all at once, a staged percentage, a
58
+ canary, a feature flag); the timing (when, and why then).
59
+ 2. **Tracking.** The signals that show the release is going well (metrics, dashboards, logs,
60
+ error rates, install counts); their current **baseline**, so a change can be read as one;
61
+ where each is read (a URL, a command, a query); and the **watch window**, how long after the
62
+ release the signals are watched.
63
+ 3. **Alerting.** What fires when it goes wrong (the threshold or condition), who receives it and
64
+ where; and whether that alert has **ever been seen firing**. An alert nobody has seen fire is
65
+ written as `never`: it is a hypothesis about an alert, and the card says so rather than
66
+ letting it read as a tested one.
67
+ 4. **Rollback or hotfix.** The exact rollback procedure and how long it takes; whether data
68
+ changes are reversible (a backup, a down migration) or not, said plainly; any kill switch or
69
+ flag; the hotfix path; and the **decision trigger**: `roll back if <signal crosses X> within
70
+ <Y>`. The project's runbook owns the procedure itself
71
+ ([`deploy-targets.md`](deploy-targets.md) → *Runbook template*, its `Rollback` section):
72
+ the card links it and adds what is specific to this release, never a second copy.
73
+
74
+ ## What the checker refuses
75
+
76
+ - No card where one is owed: NOT_RUN. A card for `users: none` is checked if it exists.
77
+ - A header without `schema: release-impact/1`, or a `users:` value outside the three, or a card
78
+ whose `users:` disagrees with `pipeline.json` (or `--users`).
79
+ - A missing section, a section that appears twice, or an empty one (an HTML comment is empty).
80
+ - `n/a` with no reason, in a section or on any field line: `n/a: <reason>` or nothing.
81
+ - A `TODO` left anywhere in the card.
82
+ - Section 2 without a `Watch window:` value. Tracking cannot be `n/a` on a release with users.
83
+ - Section 3 without a `Seen firing:` line, or answered `n/a`: something must say it went wrong.
84
+ - Section 4 without a `Decision trigger:` that states its condition (`if` or `when`), or
85
+ answered `n/a`: there is always a way back, even when the way back is a hotfix.
86
+ - At stage 8 (`--outcome`): no `## Outcome` section, or a `Result:` that is not one of `held`,
87
+ `rolled back`, `hotfixed`, `watching`.
88
+
89
+ ## Stage 8: the watch and the decision
90
+
91
+ Stage 8 reads the card's signals for its watch window and records what it read in the card's
92
+ `## Outcome` section: the signals with their values against the baseline, whether any alert
93
+ fired, and a `Result:`. When the decision trigger fires, the run rolls back or ships the hotfix
94
+ **per the card**, without a new debate: the trigger was agreed before the release precisely so
95
+ that the moment it fires is not the moment it is argued about. A rollback is itself outward and
96
+ is verified like a deploy ([`deploy-targets.md`](deploy-targets.md) → *The verification trio*).
97
+
98
+ A watch window longer than the session is a wait: `Result: watching`, the window's end, and the
99
+ watcher that reads the signals then (a scheduled job, a monitor, a named person). With no watcher
100
+ available the card says so; it never claims a watch that nobody runs.
101
+ `python3 scripts/release_impact.py check <card.md> --outcome` gates the record.
102
+
103
+ ## Worked example: a skill or package release
104
+
105
+ The users are installers. Section 1 names them (`npm` weekly downloads, plugin marketplace
106
+ installs, the operator's own machines) and what changes for them (a renamed command, a new
107
+ required field, a dropped Node version); the rollout is all at once on the tag. Section 2 tracks
108
+ the registry readback (`npm view <pkg> version` equals the tag), a clean-directory install, the
109
+ installed version read back on the operator's machine, and new issues; baseline: open issues
110
+ today; watch window: 72 h. Section 3: a failed install in the release workflow's smoke test and a
111
+ new issue on the repository, received by the maintainer by mail; seen firing: yes, the smoke test
112
+ failed a release once. Section 4: re-pin the previous version in every consumer, `npm deprecate
113
+ <pkg>@<bad>` with a message pointing at the fixed one, then a patch release; minutes; nothing to
114
+ reverse in data; decision trigger: roll back if an install fails or a breaking issue is filed
115
+ within 72 h.
116
+
117
+ ## Worked example: a web or mobile product
118
+
119
+ Section 1: signed-in customers on the web app (about 12,000 weekly), checkout changes to a new
120
+ payment form; existing saved cards keep working, a stored draft order is migrated; rollout behind
121
+ a flag at 5%, then 50%, then 100% over three days, starting on a Tuesday morning. Section 2:
122
+ checkout conversion, payment error rate and p95 latency on the product dashboard, baselines from
123
+ the last 14 days; the error tracker's release view; watch window: 24 h per step. Section 3: the
124
+ error tracker alerts on a new issue class or an error rate over twice the baseline for 10
125
+ minutes, to the on-call channel; the payment error alert has fired before, the latency one never
126
+ has. Section 4: switch the flag off (seconds); the migration has a down step tested on a staging
127
+ copy; the hotfix path is a patch through the normal pipeline with the flag held at its last good
128
+ step; decision trigger: roll back if payment errors exceed 2% or conversion falls 10% below
129
+ baseline within any step's window. A mobile release adds the store's staged rollout percentage,
130
+ the halt button as the kill switch, and the fact that a shipped binary cannot be recalled: its
131
+ way back is a hotfix build, so its section 4 names the server-side flag that disables the
132
+ feature for builds already installed.
133
+
134
+ ## Companions and single homes
135
+
136
+ - Wiring the alerts in section 3 for a service: sheleg-dev's `error-tracking` skill (releases
137
+ tied to commits, the release view the watch reads). Recommended, never required; absent, the
138
+ card names what fires on the project's own tooling.
139
+ - The rollback procedure: the project's runbook ([`deploy-targets.md`](deploy-targets.md) →
140
+ *If there is no runbook, write one first*). No runbook means stage 7 writes one first, and the
141
+ card points at its rollback.
142
+ - The release's authority and approval mode: [`deploy-targets.md`](deploy-targets.md) → *When
143
+ the release waits for an approval*. The card does not authorise anything; it is owed before
144
+ the act the authority allows.
145
+
146
+ ## Rationalizations
147
+
148
+ | The excuse | Why it fails |
149
+ |---|---|
150
+ | "It is a small release, nobody is affected." | Then section 1 says so in one line, with the reason. The card is short when the release is; it is never absent. |
151
+ | "We will notice if it breaks." | Noticing is section 3. If nothing is named that fires, nobody notices until a user writes. |
152
+ | "Rollback is obvious." | Obvious to the person who shipped it, at the moment they shipped it. The trigger and the procedure are written before the release so the person at the incident does not reconstruct them. |
153
+ | "The alert exists, it just never fired." | Then `Seen firing: never`. An alert nobody has seen fire is untested, and the card says that instead of implying the opposite. |
154
+ | "We do not know if there are users." | Unknown is treated as `external`. Ask once and record the answer in `pipeline.json`. |
@@ -13,3 +13,9 @@ The full gate and freedom level remain in [stages.md](stages.md).
13
13
  from inference about the project is one nobody can repeat or roll back, and the
14
14
  operator is standing at this gate anyway. That reference also carries the CLI
15
15
  verbs per target for when the runbook names a platform you have to recall.
16
+ - **Before the tag, the publish or the deploy, write the release impact card** when the
17
+ project has users (`pipeline.json` → `release.users` is not `none`; absent counts as
18
+ `external`): seed `templates/release-impact.md` to
19
+ `<artifacts>/specs/YYYY-MM-DD-<topic>-release-impact.md`, answer its four sections, link
20
+ the runbook's rollback rather than copy it, and run `python3 scripts/release_impact.py
21
+ check <card.md> --pipeline pipeline.json` to exit 0 ([`release-impact.md`](release-impact.md)).
@@ -28,3 +28,9 @@ The full gate and freedom level remain in [stages.md](stages.md).
28
28
  search and answer-engine visibility with `seo-aeo-audit`
29
29
  ([`companion-skills.md`](companion-skills.md)) — recommended, never a gate;
30
30
  absent, visibility ships unaudited and the close-out says so.
31
+ - **Watch the release impact card** ([`release-impact.md`](release-impact.md) → *Stage 8:
32
+ the watch and the decision*): read each signal against its baseline for the card's watch
33
+ window, write the card's `## Outcome`, and run `release_impact.py check <card.md>
34
+ --outcome`. A decision trigger that fires is acted on per the card, not debated; a window
35
+ that outlives the session is `Result: watching` with the watcher named, or the absence of
36
+ one said.
@@ -337,6 +337,14 @@ never that the work was skipped quietly.
337
337
  tagging, the CI verdict for what was just pushed is READ, not assumed**
338
338
  ([`conventions.md`](conventions.md) → *The CI verdict*) — a tag on a commit whose
339
339
  run nobody read is how a red `main` ships.
340
+ - **A release with users owes its impact card before the outward act** (since the
341
+ operator's rule of 2026-10-24): who is affected and how, the tracking with its watch
342
+ window, the alerting and whether it was ever seen firing, the rollback or hotfix with its
343
+ decision trigger. `python3 scripts/release_impact.py check <card.md> --pipeline
344
+ pipeline.json` exits 0 before the tag, the publish or the deploy; NOT_RUN (no card) stops
345
+ the release like FAIL. `release.users: none` owes no card and prints `users: none` beside
346
+ this verdict; absent is unknown and treated as `external`
347
+ ([`release-impact.md`](release-impact.md)).
340
348
  - **The independent reader is dispatched by this stage, and read by its output**
341
349
  ([`review.md`](review.md) → *The independent reader*). On any change that adds or
342
350
  widens a check, the run dispatches a reader — a subagent it can watch, a bot whose
@@ -364,6 +372,11 @@ never that the work was skipped quietly.
364
372
  steps — never silent success. **The CI verdict is one of the reported facts, with
365
373
  its run id** — "CI is green" written without a command behind it prints the same
366
374
  whether it looked or not ([`gates.md`](gates.md) → *False success*).
375
+ - **Watch the release impact card's signals for its window** and record what they read in
376
+ the card's `## Outcome` (`Result:` held, rolled back, hotfixed, or watching with its
377
+ watcher); `release_impact.py check <card.md> --outcome` exits 0. When the card's decision
378
+ trigger fires, roll back or ship the hotfix **per the card**, then verify that act as a
379
+ deploy ([`release-impact.md`](release-impact.md) → *Stage 8: the watch and the decision*).
367
380
  - **Write the verification row.** One line per REQ this run shipped, into
368
381
  `<artifacts>/verification.md` ([`verification.md`](verification.md)) — the run,
369
382
  the tag or commit it went out in, what the gate said, and `Human: never` unless the
@@ -1597,7 +1597,7 @@ def cmd_producer(graph, args):
1597
1597
  def cmd_doctrine(graph, args):
1598
1598
  """Which doctrine this run actually read — B-061.
1599
1599
 
1600
- The bundle is 54 reference files. A run reads some subset and nothing recorded which,
1600
+ The bundle is 55 reference files. A run reads some subset and nothing recorded which,
1601
1601
  so **a skipped file and a read one were indistinguishable** — the class every guard in
1602
1602
  this repository exists to catch, left standing over the doctrine itself.
1603
1603
 
@@ -0,0 +1,278 @@
1
+ #!/usr/bin/env python3
2
+ """The release impact card: who a release touches, how it is watched, and the way back, as a
3
+ command with an exit code. Stdlib only.
4
+
5
+ release_impact.py check <card.md> [--users external|internal|none]
6
+ [--pipeline pipeline.json] [--outcome] [--json]
7
+ stage 7, before any outward act: the card (`release-impact/1`) answers its four
8
+ sections (who is affected and how, tracking, alerting, rollback or hotfix), each
9
+ answered or `n/a: <reason>`. Tracking names its watch window, alerting says whether it
10
+ was ever seen firing, and rollback states its decision trigger; none of the three is
11
+ `n/a` on a release with users. `--outcome` is stage 8: the card also carries its
12
+ `## Outcome` with a `Result:` of held, rolled back, hotfixed or watching.
13
+
14
+ Who has users: `--users`, else `pipeline.json` → `release.users`, else the card's own `users:`
15
+ line. Absent everywhere means unknown, treated as `external` and said so. On `none` the card
16
+ is not owed; a card that exists is still checked.
17
+
18
+ **A check that could not run says NOT_RUN, and NOT_RUN is never PASS.** No card where one is
19
+ owed is NOT_RUN.
20
+
21
+ Exit codes: 0 PASS · 1 FAIL · 2 usage, or an input that cannot be read · 3 NOT_RUN.
22
+ The doctrine is `references/release-impact.md`.
23
+ """
24
+ from __future__ import annotations
25
+
26
+ import argparse
27
+ import json
28
+ import os
29
+ import re
30
+ import sys
31
+
32
+ SCHEMA = "release-impact/1"
33
+ USERS = ("external", "internal", "none")
34
+ RESULTS = ("held", "rolled back", "hotfixed", "watching")
35
+ # The four sections, in order, by the words their heading opens with (after an optional number).
36
+ SECTIONS = (
37
+ ("who", "who is affected"),
38
+ ("tracking", "tracking"),
39
+ ("alerting", "alerting"),
40
+ ("rollback", "rollback"),
41
+ )
42
+ TITLES = {"who": "1. Who is affected and how", "tracking": "2. Tracking",
43
+ "alerting": "3. Alerting", "rollback": "4. Rollback or hotfix"}
44
+ # Sections the operator's rule never lets be n/a on a release with users.
45
+ NEVER_NA = ("tracking", "alerting", "rollback")
46
+ COMMENT = re.compile(r"<!--.*?-->", re.S)
47
+ HEADING = re.compile(r"^##\s+(.+?)\s*$")
48
+ HEADER = re.compile(r"^(schema|release|users|date)\s*:\s*(.*?)\s*$", re.I)
49
+ # A field line: `- **Label:** value`, `**Label**: value` or `Label: value`.
50
+ FIELD = re.compile(r"^\s*(?:[-*]\s+)?(?:\*\*([^*:]+?)\s*:?\s*\*\*\s*:?|([A-Za-z][A-Za-z /]*?)\s*:)\s*(.*?)\s*$")
51
+ NA = re.compile(r"^n/?a\b\.?\s*[:;,(-]?\s*(.*?)\)?\s*$", re.I)
52
+
53
+
54
+ def _usage(msg):
55
+ print("release_impact: %s" % msg, file=sys.stderr)
56
+ return 2
57
+
58
+
59
+ def read_text(path):
60
+ with open(path, encoding="utf-8") as fh:
61
+ return fh.read()
62
+
63
+
64
+ def _is_na(value):
65
+ """(True, reason) when the value is an n/a answer; (False, None) otherwise. A reason is a
66
+ word of three letters or more, so `n/a`, `n/a:` and `n/a: -` carry none."""
67
+ m = NA.match(value.strip())
68
+ if not m:
69
+ return False, None
70
+ reason = m.group(1).strip()
71
+ return True, (reason if re.search(r"[^\W\d_]{3,}", reason) else "")
72
+
73
+
74
+ def _section_key(title):
75
+ t = re.sub(r"^\d+[.)]?\s*", "", title.strip()).lower()
76
+ for key, opener in SECTIONS:
77
+ if t.startswith(opener):
78
+ return key
79
+ if t.startswith("outcome"):
80
+ return "outcome"
81
+ return None
82
+
83
+
84
+ def parse(text):
85
+ """{'header': {...}, 'sections': {key: [body lines]}, 'duplicates': [...], 'todo': [...]}.
86
+ HTML comments are removed first: guidance in a comment is never an answer."""
87
+ text = COMMENT.sub("", text)
88
+ header, sections, dups, todo = {}, {}, [], []
89
+ cur, infence = None, False
90
+ for n, line in enumerate(text.splitlines(), 1):
91
+ if re.match(r"^\s*(```|~~~)", line):
92
+ infence = not infence
93
+ if "TODO" in line:
94
+ todo.append(n)
95
+ m = None if infence else HEADING.match(line)
96
+ if m:
97
+ cur = _section_key(m.group(1))
98
+ if cur is not None:
99
+ if cur in sections:
100
+ dups.append(cur)
101
+ sections[cur] = []
102
+ continue
103
+ if cur is None:
104
+ h = HEADER.match(line)
105
+ if h and h.group(1).lower() not in header:
106
+ header[h.group(1).lower()] = h.group(2)
107
+ continue
108
+ sections[cur].append(line)
109
+ return {"header": header, "sections": sections, "duplicates": dups, "todo": todo}
110
+
111
+
112
+ def fields(lines):
113
+ """[(label lowercased, value)] for every field-shaped line of a section body."""
114
+ out = []
115
+ for line in lines:
116
+ m = FIELD.match(line)
117
+ if m:
118
+ out.append(((m.group(1) or m.group(2)).strip().lower(), m.group(3).strip()))
119
+ return out
120
+
121
+
122
+ def field(lines, label):
123
+ for k, v in fields(lines):
124
+ if k == label:
125
+ return v
126
+ return None
127
+
128
+
129
+ def section_state(lines):
130
+ """'empty', ('na', reason) or 'answered'."""
131
+ body = [l.strip() for l in lines if l.strip()]
132
+ if not body:
133
+ return "empty", None
134
+ na, reason = _is_na(re.sub(r"^[-*]\s+", "", body[0]))
135
+ if na and len(body) == 1:
136
+ return "na", reason
137
+ return "answered", None
138
+
139
+
140
+ def check(text, users=None, outcome=False):
141
+ """(verdict, problems, notes, states, users). `users` is the project's value, or None."""
142
+ doc = parse(text)
143
+ problems, notes = [], []
144
+ head = doc["header"]
145
+ if head.get("schema", "").strip() != SCHEMA:
146
+ problems.append("the card is not %s: its header has no `schema: %s` line" % (SCHEMA, SCHEMA))
147
+ card_users = head.get("users", "").strip().lower() or None
148
+ if card_users and card_users not in USERS:
149
+ problems.append("users is one of %s, not %r" % (", ".join(USERS), card_users))
150
+ card_users = None
151
+ if users and card_users and users != card_users:
152
+ problems.append("the card says users: %s and the project says %s; one of them is wrong"
153
+ % (card_users, users))
154
+ if not users:
155
+ users = card_users
156
+ if not users:
157
+ users = "external"
158
+ notes.append("who has users is unknown (no --users, no release.users, no users: line): "
159
+ "treated as external; record the answer in pipeline.json")
160
+ for d in sorted(set(doc["duplicates"])):
161
+ problems.append("section %r appears twice; the card holds one of each" % TITLES.get(d, d))
162
+ if doc["todo"]:
163
+ problems.append("TODO left on line(s) %s: every placeholder is replaced"
164
+ % ", ".join(map(str, doc["todo"][:10])))
165
+ states = {}
166
+ secs = doc["sections"]
167
+ for key, _ in SECTIONS:
168
+ title = TITLES[key]
169
+ if key not in secs:
170
+ states[key] = "missing"
171
+ problems.append("no section %r" % title)
172
+ continue
173
+ st, reason = section_state(secs[key])
174
+ states[key] = st
175
+ if st == "empty":
176
+ problems.append("section %r is empty: answer it, or `n/a: <reason>`" % title)
177
+ elif st == "na":
178
+ if not reason:
179
+ problems.append("section %r is n/a with no reason: `n/a: <reason>`" % title)
180
+ elif key in NEVER_NA and users != "none":
181
+ problems.append("section %r cannot be n/a on a release with users: there is always "
182
+ "tracking, an alert and a way back" % title)
183
+ else:
184
+ notes.append("%s: n/a: %s" % (title, reason))
185
+ # n/a on any field line carries its reason too.
186
+ for label, value in fields(secs[key]):
187
+ na, why = _is_na(value) if value else (False, None)
188
+ if na and not why:
189
+ problems.append("%s: `%s` is n/a with no reason: `n/a: <reason>`" % (title, label))
190
+ if states.get("tracking") == "answered":
191
+ w = field(secs["tracking"], "watch window")
192
+ if not w or _is_na(w)[0]:
193
+ problems.append("section %r names no watch window (`Watch window: <how long>`)"
194
+ % TITLES["tracking"])
195
+ if states.get("alerting") == "answered":
196
+ s = field(secs["alerting"], "seen firing")
197
+ if not s:
198
+ problems.append("section %r does not say whether the alert was ever seen firing "
199
+ "(`Seen firing: yes (when)` or `never`)" % TITLES["alerting"])
200
+ elif re.match(r"^never\b", s, re.I):
201
+ notes.append("the alert has never been seen firing: it is untested")
202
+ if states.get("rollback") == "answered":
203
+ t = field(secs["rollback"], "decision trigger")
204
+ if not t or _is_na(t)[0] or not re.search(r"\b(if|when|если|когда)\b", t, re.I):
205
+ problems.append("section %r states no decision trigger (`Decision trigger: roll back "
206
+ "if <X> within <Y>`)" % TITLES["rollback"])
207
+ if outcome:
208
+ if "outcome" not in secs:
209
+ problems.append("stage 8: no `## Outcome` section recording what the watch read")
210
+ else:
211
+ r = (field(secs["outcome"], "result") or "").strip().lower()
212
+ if not any(r.startswith(x) for x in RESULTS):
213
+ problems.append("stage 8: `Result:` is one of %s, not %r" % (", ".join(RESULTS), r))
214
+ elif r.startswith("watching"):
215
+ notes.append("the watch window is still open: %s" % r)
216
+ return ("FAIL" if problems else "PASS"), problems, notes, states, users
217
+
218
+
219
+ def pipeline_users(path):
220
+ """release.users from pipeline.json; None when absent. Raises ValueError on bad input."""
221
+ cfg = json.loads(read_text(path))
222
+ rel = cfg.get("release") if isinstance(cfg, dict) else None
223
+ u = rel.get("users") if isinstance(rel, dict) else None
224
+ if u is not None and u not in USERS:
225
+ raise ValueError("release.users is one of %s, not %r" % (", ".join(USERS), u))
226
+ return u
227
+
228
+
229
+ def emit(verdict, problems, notes, as_json, extra=None):
230
+ if as_json:
231
+ print(json.dumps(dict(verdict=verdict, problems=problems, notes=notes, **(extra or {})), indent=2))
232
+ else:
233
+ print("release_impact: %s" % verdict)
234
+ for p in problems:
235
+ print(" - %s" % p)
236
+ for n in notes:
237
+ print(" note: %s" % n)
238
+ return {"PASS": 0, "FAIL": 1, "NOT_RUN": 3}[verdict]
239
+
240
+
241
+ def main(argv=None):
242
+ ap = argparse.ArgumentParser(description=__doc__.split("\n")[0])
243
+ sub = ap.add_subparsers(dest="cmd", required=True)
244
+ p = sub.add_parser("check")
245
+ p.add_argument("path")
246
+ p.add_argument("--users", choices=USERS)
247
+ p.add_argument("--pipeline", help="read release.users from this pipeline.json (--users wins)")
248
+ p.add_argument("--outcome", action="store_true", help="stage 8: the card carries its Outcome")
249
+ p.add_argument("--json", action="store_true")
250
+ try:
251
+ a = ap.parse_args(argv)
252
+ except SystemExit as exc:
253
+ return 2 if exc.code else 0
254
+ users = a.users
255
+ if not users and a.pipeline:
256
+ try:
257
+ users = pipeline_users(a.pipeline)
258
+ except OSError as exc:
259
+ return _usage("cannot read %s (%s)" % (a.pipeline, exc.strerror or exc))
260
+ except ValueError as exc:
261
+ return _usage("%s: %s" % (a.pipeline, exc))
262
+ if not os.path.exists(a.path):
263
+ if users == "none":
264
+ return emit("PASS", [], ["users: none: the card is not owed; write `users: none` "
265
+ "beside the stage 7 verdict"], a.json, {"users": "none"})
266
+ why = "" if users else " (who has users is unknown: treated as external)"
267
+ return emit("NOT_RUN", ["no release impact card at %s; one is owed before the outward "
268
+ "act%s" % (a.path, why)], [], a.json, {"users": users or "external"})
269
+ try:
270
+ text = read_text(a.path)
271
+ except (OSError, UnicodeDecodeError) as exc:
272
+ return _usage("cannot read %s (%s)" % (a.path, getattr(exc, "strerror", None) or exc))
273
+ verdict, probs, notes, states, resolved = check(text, users, a.outcome)
274
+ return emit(verdict, probs, notes, a.json, {"users": resolved, "sections": states})
275
+
276
+
277
+ if __name__ == "__main__":
278
+ sys.exit(main())
@@ -27,6 +27,7 @@ from `super-ux`.
27
27
  | `hooks.example.json` | the project's `.claude/settings.json` | 0 — offered, never installed silently |
28
28
  | `browser-claims.json` | stage 6, beside the look — one row per browser claim | REQ/scenario id + state + kind (look/suite/library); validated by `test/browser_claims_test.py`, stdlib only |
29
29
  | `finding-evidence.json` | one record per audit finding, wherever the finding lives | the five-axes evidence schema: mechanism / reproduction / exposure / incidence / impact uncertainty, observations apart from assumptions |
30
+ | `release-impact.md` | `docs/evidence/specs/YYYY-MM-DD-<topic>-release-impact.md`, one per release | 7 writes it before any outward act when the project has users, checked by `scripts/release_impact.py check`; 8 adds its `## Outcome` (`--outcome`) |
30
31
  | `routing-rule.md` | the operator's `CLAUDE.md` — **offered by `setup`, never written silently** | 0 / `setup` |
31
32
  | `retro.md` | `docs/evidence/retro.md` — **one per project, not per run** | 10 writes (stamp → prune → entry), 0 reads it in full |
32
33
  | `retro-archive.md` | `docs/evidence/retro/YYYY-QN.md` | 10 rotates into it, 0 **queries** it |
@@ -0,0 +1,50 @@
1
+ # Release impact: TODO release name or version
2
+
3
+ schema: release-impact/1
4
+ release: TODO the tag or version this card is for
5
+ users: TODO external | internal | none (the value in pipeline.json, release.users)
6
+ date: TODO YYYY-MM-DD
7
+
8
+ <!-- Seeded to <artifacts>/specs/YYYY-MM-DD-<topic>-release-impact.md at stage 7, before the
9
+ first outward act. Replace every TODO. A section or a field that does not apply says
10
+ `n/a: <reason>`; a bare `n/a` is refused. Gate:
11
+ python3 scripts/release_impact.py check <this file> --pipeline pipeline.json
12
+ The doctrine is references/release-impact.md in the task-pipeline skill. -->
13
+
14
+ ## 1. Who is affected and how
15
+
16
+ - **Segments:** TODO who uses what ships, with a rough count of each
17
+ - **What changes:** TODO behaviour, data, API or file format compatibility, migrations
18
+ - **Breaks for existing users:** TODO no, or what breaks and for whom
19
+ - **Rollout:** TODO all at once, staged percentage, canary or feature flag
20
+ - **Timing:** TODO when it goes out, and why then
21
+
22
+ ## 2. Tracking
23
+
24
+ - **Signals:** TODO the metrics, dashboards, logs or error rates that show it going well
25
+ - **Baseline:** TODO each signal's value now
26
+ - **Where:** TODO the URL, command or query that reads each signal
27
+ - **Watch window:** TODO how long after the release the signals are watched
28
+
29
+ ## 3. Alerting
30
+
31
+ - **Fires when:** TODO the threshold or condition
32
+ - **Receives:** TODO who gets it, and where
33
+ - **Seen firing:** TODO yes (when), or never
34
+
35
+ ## 4. Rollback or hotfix
36
+
37
+ - **Rollback:** TODO the exact procedure, or a link to the runbook's rollback section
38
+ - **Time to roll back:** TODO minutes or hours
39
+ - **Data reversible:** TODO yes (backup, down migration), or no and what is lost
40
+ - **Kill switch:** TODO the flag or switch, or n/a: <reason>
41
+ - **Hotfix path:** TODO how an urgent fix ships
42
+ - **Decision trigger:** TODO roll back if <signal crosses X> within <Y>
43
+
44
+ <!-- Stage 8 adds the section below once the watch window is read; check it with --outcome.
45
+
46
+ ## Outcome
47
+
48
+ - **Read:** the signals against their baselines, and any alert that fired
49
+ - **Result:** held | rolled back | hotfixed | watching (until <when>, watcher <who or what>)
50
+ -->
@@ -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 54 reference files and nothing recorded which of them a run read, so **a
23
+ The bundle is 55 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 54 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 55 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