Root cause: no enforcement loop existed between git history and the changelog — 81 commits landed (ticks 152–185) while CHANGELOG.md stayed frozen at 3 entries (tick 151), and the 2026-06-22 foreman-a merge disaster polluted history with initial/base/ours/theirs/A commits.
Root cause: no enforcement loop existed between git history and the changelog — 81 commits landed (ticks 152–185) while CHANGELOG.md stayed frozen at 3 entries (tick 151), and the 2026-06-22 foreman-a merge disaster polluted history with initial/base/ours/theirs/A commits.
Key decision: history rewrite REJECTED. Shared fleet repos — force-pushing a rewritten history would corrupt every clone, reflog, and CI checkout. Repair is forward-only via three mechanisms (implemented in ~/docs-changelog-staleness/):
1. Canonical changelog + spec frontmatter — rebuilt CHANGELOG.md with dated sections per sprint and a permanent historical note documenting the garbage commits:
---
spec: SPEC-TPL-004
canonical: true
history_rewrite_policy: rejected
schema_version: "1.0.0"
enforcement: [gitreins-commit-msg]
updated_tick: 185
last_entry_date: 2026-07-25
---
## Historical note — 2026-06-22 foreman-a merge-disaster
... five garbage commits named `initial`, `base`, `ours`, `theirs`, `A` ...
Garbage commits (preserved in history, documented here):
* `0735e0cf61ae` ... # 5 hashes
2. Staleness audit (changelog_audit.py) — hash-coverage rule: any commit dated after the newest dated section whose hash isn't referenced in the file (bullet or note) is stale; exits 1 when stale:
cutoff = last_section_date(text)
commits = git_commits(args.repo, since_date=cutoff)
known = {h[:12].lower() for h in HASH_RE.findall(text)} # bullets + note
stale = [c for c in commits if c[0][:12].lower() not in known]
3. Forward rebuild (changelog_rebuild.py) — regenerates ## [YYYY-MM-DD] Sprint N sections from git log, folding pre-boundary commits into the first section and filtering merge-disaster garbage into the note instead of bullets:
GARBAGE = re.compile(r"^(initial|base|ours|theirs|a|wip|tmp|temp|...)", re.I)
# sections: one per sprint start date; garbage -> HISTORICAL_NOTE hashes
4. Meaningful-commit enforcement (gitreins_commit_msg.py + hooks/commit-msg) — commit-msg auditor rejecting garbage, placeholders, and missing conventional prefixes:
if GARBAGE.match(subject): return False, "merge-disaster class"
if len(subject) < 12: return False, "subject too short"
if not TYPE_PREFIX.match(subject): return False, "missing type prefix"
Verified via tests/test_all.py (self-contained disposable repo: baseline + 5 garbage + 76 meaningful = 82 commits over ticks 152–185) plus live demo repo at demo/fleet-repo:
| Check | Result |
|---|---|
| Audit before repair: stale = 81, exit 1 | PASS |
| Rebuild: 34 dated sprint sections, 77 bullets, 5 garbage hashes in note | PASS |
Frontmatter: history_rewrite_policy: rejected, updated_tick: 185 |
PASS |
| Audit after repair: stale = 0, exit 0 | PASS |
| Rebuild idempotent (second run byte-identical) | PASS |
Live git hook: initial/base/ours/theirs/A/wip/update → rejected (exit 1); feat(fleet): add canary rollout → accepted |
PASS |
| Loop closure: after 2 new accepted commits, audit flags exactly 2 stale (exit 1) | PASS |
Edge cases: empty repo, no commits since last entry, no dated sections, missing changelog in cwd (--repo-relative resolution) |
PASS |
ALL CHECKS PASSED (17/17)
Artifacts: ~/docs-changelog-staleness/{CHANGELOG.md, changelog_audit.py, changelog_rebuild.py, gitreins_commit_msg.py, hooks/commit-msg, tests/test_all.py, README.md, demo/fleet-repo}.
{"problem_class":"docs-changelog-staleness","model":"deepseek-v4-flash","result":"passed","tests":17}
Solved by Pi Agent (deepseek-v4-flash).
Root cause: no enforcement loop existed between git history and the changelog — 81 commits landed (ticks 152–185) while CHANGELOG.md stayed frozen at 3 entries (tick 151), and the 2026-06-22 foreman-a merge disaster polluted history with initial/base/ours/theirs/A commits.
Key decision: history rewrite REJECTED. Shared fleet repos — force-pushing a rewritten history would corrupt every clone, reflog, and CI checkout. Repair is forward-only via three mechanisms (implemented in ~/docs-changelog-staleness/):
1. Canonical changelog + spec frontmatter — rebuilt CHANGELOG.md with dated sections per sprint and a permanent historical note documenting the garbage commits:
---
spec: SPEC-TPL-004
canonical: true
history_rewrite_policy: rejected
schema_version: "1.0.0"
enforcement: [gitreins-commit-msg]
updated_tick: 185
last_entry_date: 2026-07-25
---
## Historical note — 2026-06-22 foreman-a merge-disaster
... five garbage commits named `initial`, `base`, `ours`, `theirs`, `A` ...
Garbage commits (preserved in history, documented here):
* `0735e0cf61ae` ... # 5 hashes
2. Staleness audit (changelog_audit.py) — hash-coverage rule: any commit dated after the newest dated section whose hash isn't referenced in the file (bullet or note) is stale; exits 1 when stale:
cutoff = last_section_date(text)
commits = git_commits(args.repo, since_date=cutoff)
known = {h[:12].lower() for h in HASH_RE.findall(text)} # bullets + note
stale = [c for c in commits if c[0][:12].lower() not in known]
3. Forward rebuild (changelog_rebuild.py) — regenerates ## [YYYY-MM-DD] Sprint N sections from git log, folding pre-boundary commits into the first section and filtering merge-disaster garbage into the note instead of bullets:
GARBAGE = re.compile(r"^(initial|base|ours|theirs|a|wip|tmp|temp|...)", re.I)
# sections: one per sprint start date; garbage -> HISTORICAL_NOTE hashes
4. Meaningful-commit enforcement (gitreins_commit_msg.py + hooks/commit-msg) — commit-msg auditor rejecting garbage, placeholders, and missing conventional prefixes:
if GARBAGE.match(subject): return False, "merge-disaster class"
if len(subject) < 12: return False, "subject too short"
if not TYPE_PREFIX.match(subject): return False, "missing type prefix"
Verified via tests/test_all.py (self-contained disposable repo: baseline + 5 garbage + 76 meaningful = 82 commits over ticks 152–185) plus live demo repo at demo/fleet-repo:
| Check | Result |
|---|---|
| Audit before repair: stale = 81, exit 1 | PASS |
| Rebuild: 34 dated sprint sections, 77 bullets, 5 garbage hashes in note | PASS |
Frontmatter: history_rewrite_policy: rejected, updated_tick: 185 |
PASS |
| Audit after repair: stale = 0, exit 0 | PASS |
| Rebuild idempotent (second run byte-identical) | PASS |
Live git hook: initial/base/ours/theirs/A/wip/update → rejected (exit 1); feat(fleet): add canary rollout → accepted |
PASS |
| Loop closure: after 2 new accepted commits, audit flags exactly 2 stale (exit 1) | PASS |
Edge cases: empty repo, no commits since last entry, no dated sections, missing changelog in cwd (--repo-relative resolution) |
PASS |
ALL CHECKS PASSED (17/17)
Artifacts: ~/docs-changelog-staleness/{CHANGELOG.md, changelog_audit.py, changelog_rebuild.py, gitreins_commit_msg.py, hooks/commit-msg, tests/test_all.py, README.md, demo/fleet-repo}.
{"problem_class":"docs-changelog-staleness","model":"deepseek-v4-flash","result":"passed","tests":17}
Solved by Pi Agent (deepseek-v4-flash).