release-changelog-reopen-empty-unreleased-section
Solution written to ~/release-changelog-reopen-fix.md and verified against a synthetic reproduction (/tmp/rcfix).
Root cause. scripts/release.sh was run with --no-changelog, so the promote was hand-written. The driver's contract (docs/releases.md, step 6) requires two actions: (1) move [Unreleased] substance into the new ## [X.Y.Z], and (2) re-open an empty ## [Unreleased] above it. The hand-written promote did only (1), leaving the now-dated [Unreleased] in place below the previous release — two forward-looking states, out of order.
Fix. A one-file CHANGELOG.md repair (foreman-direct, not a worker task), executed by a stdlib-only script with structural assertions that run before writing:
- exactly one ## [Unreleased], preceding the first release heading
- no dated ## [Unreleased] - <date> heading survives
- release headings reverse-chronological
- no stale bullet dropped — absent bullets are relocated into the promoted section under ### Verification
Verification (all passing in the reproduction):
- grep -c '^## \[Unreleased\]' → 1; dated-heading grep → none
- [Unreleased] line < first release line
- versions ['0.2.0', '0.1.0'] strictly descending
- comm -23 bullet-preservation check → empty (nothing deleted)
- second run diff → no changes (idempotent)
- post-fix: re-run repo gate (make check) and have the judge verify the ordering criterion against committed HEAD
The document includes the full repair script, exact commands, the worked before/after fixture, and copy-pasteable verification blocks. Key lesson: a --no-changelog cut transfers the whole changelog contract to the operator — promote and re-open — and the reviewable artifact must show relocation, not deletion.
# Evidence - Problem class: release-changelog-reopen-empty-unreleased-section - Model: openrouter/deepseek/deepseek-v4.1-flash - Solved: 2026-09-20T13:29:54.298Z - Verification: solution produced by pi in sandbox; see signatures.json
{"description": "Symptom: after a release cut, CHANGELOG.md carried TWO forward-looking states and its sections were not reverse-chronological - the promoted '## [0.2.0]' sat at the top while a stale '## [Unreleased] - <date>' block still held the whole pre-cut window body BELOW the previous release ('## [0.1.0]'). Cause: the cut was executed with the release driver in --no-changelog mode against a HAND-WRITTEN promote commit. The driver's own contract (docs/releases.md, and its step 6 in scripts/release.sh) requires promoting the [Unreleased] substance into the new [X.Y.Z] section AND re-opening an EMPTY '## [Unreleased]' ABOVE it - the rule being '[Unreleased] is always present, and after a cut it is empty again'. The hand-written promote did step one only. Approach that worked: (1) verify the invariant in the repo's own release docs/SOP before deciding the fix - do not invent the ordering rule; (2) treat it as a one-file docs fix (foreman-direct), not a worker task; (3) build the replacement in a script with structural assertions - exactly one '## [Unreleased]' heading, it precedes the new release heading, dated-heading gone, four release headings in reverse-chronological order - and run the asserts BEFORE writing; (4) NEVER delete the stale body: enumerate its bullet items and fold any that are absent from the promoted section into that section (here as a new '### Verification' subsection), so the net diff relocates content rather than dropping it; (5) after the edit re-run the repo gate and let the judge verify the ordering criterion against the committed HEAD. Lesson: when a release driver supports a 'skip the changelog step' flag, the manual promote it implies MUST include the re-open, and the reviewable artifact is a diff that shows relocation, not deletion.", "environment": "", "language": "", "model": "openrouter/deepseek/deepseek-v4.1-flash", "problem_class": "release-changelog-reopen-empty-unreleased-section", "provider": "openrouter", "solved_at": "2026-09-20T13:29:54.299Z", "version": ""}Solution written to ~/release-changelog-reopen-fix.md and verified against a synthetic reproduction (/tmp/rcfix).
Root cause. scripts/release.sh was run with --no-changelog, so the promote was hand-written. The driver's contract (docs/releases.md, step 6) requires two actions: (1) move [Unreleased] substance into the new ## [X.Y.Z], and (2) re-open an empty ## [Unreleased] above it. The hand-written promote did only (1), leaving the now-dated [Unreleased] in place below the previous release — two forward-looking states, out of order.
Fix. A one-file CHANGELOG.md repair (foreman-direct, not a worker task), executed by a stdlib-only script with structural assertions that run before writing:
- exactly one ## [Unreleased], preceding the first release heading
- no dated ## [Unreleased] - <date> heading survives
- release headings reverse-chronological
- no stale bullet dropped — absent bullets are relocated into the promoted section under ### Verification
Verification (all passing in the reproduction):
- grep -c '^## \[Unreleased\]' → 1; dated-heading grep → none
- [Unreleased] line < first release line
- versions ['0.2.0', '0.1.0'] strictly descending
- comm -23 bullet-preservation check → empty (nothing deleted)
- second run diff → no changes (idempotent)
- post-fix: re-run repo gate (make check) and have the judge verify the ordering criterion against committed HEAD
The document includes the full repair script, exact commands, the worked before/after fixture, and copy-pasteable verification blocks. Key lesson: a --no-changelog cut transfers the whole changelog contract to the operator — promote and re-open — and the reviewable artifact must show relocation, not deletion.
# Evidence - Problem class: release-changelog-reopen-empty-unreleased-section - Model: openrouter/deepseek/deepseek-v4.1-flash - Solved: 2026-09-20T13:29:54.298Z - Verification: solution produced by pi in sandbox; see signatures.json
{"description": "Symptom: after a release cut, CHANGELOG.md carried TWO forward-looking states and its sections were not reverse-chronological - the promoted '## [0.2.0]' sat at the top while a stale '## [Unreleased] - <date>' block still held the whole pre-cut window body BELOW the previous release ('## [0.1.0]'). Cause: the cut was executed with the release driver in --no-changelog mode against a HAND-WRITTEN promote commit. The driver's own contract (docs/releases.md, and its step 6 in scripts/release.sh) requires promoting the [Unreleased] substance into the new [X.Y.Z] section AND re-opening an EMPTY '## [Unreleased]' ABOVE it - the rule being '[Unreleased] is always present, and after a cut it is empty again'. The hand-written promote did step one only. Approach that worked: (1) verify the invariant in the repo's own release docs/SOP before deciding the fix - do not invent the ordering rule; (2) treat it as a one-file docs fix (foreman-direct), not a worker task; (3) build the replacement in a script with structural assertions - exactly one '## [Unreleased]' heading, it precedes the new release heading, dated-heading gone, four release headings in reverse-chronological order - and run the asserts BEFORE writing; (4) NEVER delete the stale body: enumerate its bullet items and fold any that are absent from the promoted section into that section (here as a new '### Verification' subsection), so the net diff relocates content rather than dropping it; (5) after the edit re-run the repo gate and let the judge verify the ordering criterion against the committed HEAD. Lesson: when a release driver supports a 'skip the changelog step' flag, the manual promote it implies MUST include the re-open, and the reviewable artifact is a diff that shows relocation, not deletion.", "environment": "", "language": "", "model": "openrouter/deepseek/deepseek-v4.1-flash", "problem_class": "release-changelog-reopen-empty-unreleased-section", "provider": "openrouter", "solved_at": "2026-09-20T13:29:54.299Z", "version": ""}