First of two stacked PRs for the knowledge-retrieval arc (#520 + #521). This PR ships the machinery only; it does NOT close either issue — the full ~90-record migration + the #520 startup/kickoff rewrite land in PR2 (branched off this PR's merged head), which is what completes #520 and #521. So this is intentionally Refs, not fixes.
What this delivers
scripts/decisions_lib.py — parser for decision records + a visible metadata block (key/status/since/supersedes/superseded-by + Rule/Signals/Mechanics). An entry with no metadata is tolerated as legacy-unmigrated.
scripts/decisions_validate.py — the lifecycle guard that replaces the line-level append-only mechanic. Enforces: ≤1 active record per key; reciprocal supersession links resolve; no-vanish (a record leaving the active set must reappear under docs/decisions/archive/); rationale body-diff (a change to a record's prose without [decisions-edit] is blocked — closes the silent-rewrite hole); archive-placement (superseded/retired must be archived, active must not); metadata-demotion detection. Budget is a non-blocking warning. All git checks fail-open.
scripts/build_decisions_catalog.py — regenerates docs/decisions/README.md, the compact active catalog / task router (drift fails CI --check). Anchors match Gitea's real slugger (double-hyphen for —).
Exemplar migrations + archive:ci.runner-placement (#406), ci.peak-anon-measurement (#412) active; the append-only→lifecycle supersession dogfooded (docs.decision-lifecycle active supersedes archived docs.append-only-guard); the old H9/H3 bundle split so the still-active root-screenshot guard (ci.root-screenshot-guard) stays in the active wing. docs/decisions/migration-map.md records it. #390/#411 confirmed prose-only.
Header/docs:docs/decisions.md header + docs/ci-cd.md release ritual rewritten to lifecycle framing; stale "append-only" instructions in CLAUDE.md/docs/README.md corrected.
MemPalace coordination (server-management#642)
Records front-load key/status for the ErsatzTV-Decisions wing; supersession is a physical move to archive/ (enforced by the validator), which is the wing boundary #642's exporter keys off. #642 mines nothing until a "validator green on " note after PR2.
Verification (all local, pre-push)
decisions_validate.py --base origin/main --head HEAD (the CI check) — exit 0, incl. after rebasing onto #518.
Per-task reviews + a whole-branch review (found & fixed a catalog-anchor BLOCKER verified against the repo's own 64 working anchors, plus archive-guard/demotion/reciprocal holes) + a non-vacuity re-review of the guard logic.
Notes
#511 (merged via #518 while this was in flight) is carried forward as a legacy-unmigrated record — it migrates when next touched.
Design docs (docs/superpowers/plans + specs) are included as the design record.
One non-blocking follow-up carried to PR2: extend the reciprocity check to archive↔archive pairs (zero exposure here; first appears in PR2's migration).
## PR1 of 2 — decision-lifecycle machinery (#521)
First of two stacked PRs for the knowledge-retrieval arc (#520 + #521). **This PR ships the machinery only; it does NOT close either issue** — the full ~90-record migration + the #520 startup/kickoff rewrite land in **PR2** (branched off this PR's merged head), which is what completes #520 and #521. So this is intentionally `Refs`, not `fixes`.
### What this delivers
- **`scripts/decisions_lib.py`** — parser for decision records + a visible metadata block (`key`/`status`/`since`/`supersedes`/`superseded-by` + `Rule`/`Signals`/`Mechanics`). An entry with no metadata is tolerated as `legacy-unmigrated`.
- **`scripts/decisions_validate.py`** — the lifecycle guard that **replaces the line-level append-only mechanic**. Enforces: ≤1 active record per key; reciprocal supersession links resolve; **no-vanish** (a record leaving the active set must reappear under `docs/decisions/archive/`); **rationale body-diff** (a change to a record's prose without `[decisions-edit]` is blocked — closes the silent-rewrite hole); archive-placement (superseded/retired must be archived, active must not); metadata-demotion detection. Budget is a non-blocking warning. All git checks fail-open.
- **`scripts/build_decisions_catalog.py`** — regenerates `docs/decisions/README.md`, the compact active catalog / task router (drift fails CI `--check`). Anchors match Gitea's real slugger (double-hyphen for ` — `).
- **Guard rework:** `.claude/hooks/decisions-guard.sh` → fail-open validator shim; `.husky/commit-msg`/`pre-commit` rewired; CI `decisions-guard` job → `decisions lifecycle` (validator + catalog check, `setup-python`, 1800-line floor removed). `[decisions-edit]` **kept narrow** (rationale-prose edits only), not retired.
- **Exemplar migrations + archive:** `ci.runner-placement` (#406), `ci.peak-anon-measurement` (#412) active; the append-only→lifecycle supersession dogfooded (`docs.decision-lifecycle` active supersedes archived `docs.append-only-guard`); the old H9/H3 bundle **split** so the still-active root-screenshot guard (`ci.root-screenshot-guard`) stays in the active wing. `docs/decisions/migration-map.md` records it. #390/#411 confirmed prose-only.
- **Header/docs:** `docs/decisions.md` header + `docs/ci-cd.md` release ritual rewritten to lifecycle framing; stale "append-only" instructions in `CLAUDE.md`/`docs/README.md` corrected.
### MemPalace coordination (server-management#642)
Records front-load `key`/`status` for the `ErsatzTV-Decisions` wing; supersession is a physical move to `archive/` (enforced by the validator), which is the wing boundary #642's exporter keys off. #642 mines nothing until a "validator green on <main-sha>" note after PR2.
### Verification (all local, pre-push)
- `decisions_validate.py --base origin/main --head HEAD` (the CI check) — exit 0, incl. after rebasing onto #518.
- catalog `--check` in sync; `scripts/tests` 26 passed; ruff clean; husky token-free.
- Per-task reviews + a whole-branch review (found & fixed a catalog-anchor BLOCKER verified against the repo's own 64 working anchors, plus archive-guard/demotion/reciprocal holes) + a non-vacuity re-review of the guard logic.
### Notes
- `#511` (merged via #518 while this was in flight) is carried forward as a `legacy-unmigrated` record — it migrates when next touched.
- Design docs (`docs/superpowers/plans` + `specs`) are included as the design record.
- One non-blocking follow-up carried to PR2: extend the reciprocity check to archive↔archive pairs (zero exposure here; first appears in PR2's migration).
Refs #520 #521
Design for the decision-knowledge lifecycle schema/catalog/validator (#521) and
the #237-retirement / parallel-startup kickoff rewrite (#520), as one arc.
Refs #520#521
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Adds scripts/decisions_lib.py, the shared parser for ErsatzTV decision
records (docs/decisions.md + docs/decisions/*.md). Parses H2 sections
into Record dataclasses, distinguishing migrated records (visible
metadata block: key/status/since/supersedes/superseded-by + Rule/
Signals/Mechanics) from legacy-unmigrated ones with no metadata line.
scripts/ is now an importable package (scripts/__init__.py,
scripts/tests/__init__.py) so later tools can `import scripts.decisions_lib`.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Adds scripts/decisions_validate.py: lifecycle invariant checks (unique
active key, key format, reciprocal supersession, removed-without-archive,
rationale-rewrite-without-token, catalog staleness, corpus budget) plus
git-diff helpers for merge-base-based CI checks. Deviates from the task
brief in one spot: REQUIRED_META narrowed to (key, status) — the brief's
(key, status, since, supersedes, superseded_by) makes its own
test_clean_corpus_passes fail, since since/supersedes/superseded_by
default to None on bare Record() instances built without going through
the markdown parser.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Adds scripts/build_decisions_catalog.py, which renders docs/decisions/README.md
as a compact table of only 'active' decision records (sorted by key), and its
test scripts/tests/test_build_catalog.py. Supports --check for CI drift
detection. No decision records are migrated yet, so the generated catalog is
currently empty (banner + header only) — expected at this stage.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
_anchor() mapped each space/hyphen to its own '-' without collapsing runs,
so the standard heading separator " — " (space, em-dash, space) produced a
double hyphen in every generated anchor. Since nearly every real decision
record heading uses that separator, this made the catalog emit a dead link
for essentially every row. Fix: after building the char list, collapse
consecutive '-' into one and strip leading/trailing '-' via re.sub, matching
how Goldmark/GitHub/Gitea sluggers behave.
Also fixed main() writing an extra trailing newline (want already ends in
"\n", then "+ \n" appended a second one) so docs/decisions/README.md now
ends with exactly one trailing newline; --check still compares via .strip().
Added test_anchor_collapses_em_dash_and_keeps_underscore to pin the anchor
behavior against the reported iptv.base_url case.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Migrate #406 (ci.runner-placement) and #412 (ci.peak-anon-measurement) to the lifecycle
schema as exemplars; #390 and #411 confirmed prose-only (no standalone record). Dogfood the
append-only->lifecycle supersession: docs.append-only-guard moved to archive/, superseded by
new active docs.decision-lifecycle. Stand up docs/decisions/archive/ + migration-map.md.
Refs #521
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The archived docs.append-only-guard record bundled the genuinely-superseded
H9 (decisions.md append-only) with the independent, still-active H3
(root-screenshot pre-commit guard), relocating both into archive/ and
burying an active decision in the history wing. Split into two records:
docs.append-only-guard stays in archive/release-ci-governance.md,
status: superseded, H3 content removed and replaced with a forward
pointer; ci.root-screenshot-guard is a new status: active record in the
active docs/decisions/release-ci-governance.md, with the H3 rationale
paragraph lifted byte-identical from the original bundle. Updated
migration-map.md (new row + a "Migration conventions" note: never
relocate an active decision to archive/) and regenerated
docs/decisions/README.md.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Retire the line-level append-only mechanic (ersatztv#303 H9) in favor of the lifecycle validator
built in Tasks 1-5. .claude/hooks/decisions-guard.sh is now a thin fail-open shim around
scripts/decisions_validate.py; .husky/pre-commit calls it for the structural (working-tree) checks,
.husky/commit-msg drops the old staged/[decisions-edit]-deletion block and keeps only the
Co-Authored-By check. The Gitea decisions-guard job is renamed "decisions lifecycle" and now runs
decisions_validate.py --base/--head (structural + body-diff + no-vanish) and
build_decisions_catalog.py --check (active catalog drift), with actions/setup-python@v5 added since
the bare `small` lane doesn't guarantee python3; the old 1800-line consolidation-floor step is
removed (replaced by the validator's aggregate active-corpus budget). docs/decisions.md's header is
rewritten from append-only to lifecycle framing (metadata schema, statuses, generated catalog,
archive, same-PR supersession); [decisions-edit] is re-scoped (not removed) to rationale-prose
edits/factual corrections only. docs/ci-cd.md's release ritual and hook/job descriptions are
rewritten to match.
Also fixes a pre-existing validator false-positive surfaced while sanity-checking against
origin/main: Task 6's #303 H9/H3 split (commit d09be57e) renamed the archived record's heading
away from the pre-split original, which the validator's heading-based relocation check reads as
"removed without an archive copy." Restored the archived heading to match the original text
(functionally unchanged — still status: superseded, same key) and updated the two prose
cross-references (migration-map.md, release-ci-governance.md) that pointed at the old anchor.
PR1 scope only (per brief): does NOT wire the kickoff-guard CI step or touch the
kickoff/README/select-queue docs — that's Task 8/PR2.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Blocking a user prevents them from interacting with repositories, such as opening or commenting on pull requests or issues. Learn more about blocking a user.
PR1 of 2 — decision-lifecycle machinery (#521)
First of two stacked PRs for the knowledge-retrieval arc (#520 + #521). This PR ships the machinery only; it does NOT close either issue — the full ~90-record migration + the #520 startup/kickoff rewrite land in PR2 (branched off this PR's merged head), which is what completes #520 and #521. So this is intentionally
Refs, notfixes.What this delivers
scripts/decisions_lib.py— parser for decision records + a visible metadata block (key/status/since/supersedes/superseded-by+Rule/Signals/Mechanics). An entry with no metadata is tolerated aslegacy-unmigrated.scripts/decisions_validate.py— the lifecycle guard that replaces the line-level append-only mechanic. Enforces: ≤1 active record per key; reciprocal supersession links resolve; no-vanish (a record leaving the active set must reappear underdocs/decisions/archive/); rationale body-diff (a change to a record's prose without[decisions-edit]is blocked — closes the silent-rewrite hole); archive-placement (superseded/retired must be archived, active must not); metadata-demotion detection. Budget is a non-blocking warning. All git checks fail-open.scripts/build_decisions_catalog.py— regeneratesdocs/decisions/README.md, the compact active catalog / task router (drift fails CI--check). Anchors match Gitea's real slugger (double-hyphen for—)..claude/hooks/decisions-guard.sh→ fail-open validator shim;.husky/commit-msg/pre-commitrewired; CIdecisions-guardjob →decisions lifecycle(validator + catalog check,setup-python, 1800-line floor removed).[decisions-edit]kept narrow (rationale-prose edits only), not retired.ci.runner-placement(#406),ci.peak-anon-measurement(#412) active; the append-only→lifecycle supersession dogfooded (docs.decision-lifecycleactive supersedes archiveddocs.append-only-guard); the old H9/H3 bundle split so the still-active root-screenshot guard (ci.root-screenshot-guard) stays in the active wing.docs/decisions/migration-map.mdrecords it. #390/#411 confirmed prose-only.docs/decisions.mdheader +docs/ci-cd.mdrelease ritual rewritten to lifecycle framing; stale "append-only" instructions inCLAUDE.md/docs/README.mdcorrected.MemPalace coordination (server-management#642)
Records front-load
key/statusfor theErsatzTV-Decisionswing; supersession is a physical move toarchive/(enforced by the validator), which is the wing boundary #642's exporter keys off. #642 mines nothing until a "validator green on " note after PR2.Verification (all local, pre-push)
decisions_validate.py --base origin/main --head HEAD(the CI check) — exit 0, incl. after rebasing onto #518.--checkin sync;scripts/tests26 passed; ruff clean; husky token-free.Notes
#511(merged via #518 while this was in flight) is carried forward as alegacy-unmigratedrecord — it migrates when next touched.docs/superpowers/plans+specs) are included as the design record.Refs #520 #521