Repository navigation
retire_capabilities is unusable when a scenario bullet wraps: the continuation line counts as unaccounted content #1780
Description
Summary
retire_capabilities: true — the first-class retirement path added for #1750 — is unusable on any
spec whose scenario bullets wrap onto a second line. The continuation line is classified as
"content the merge cannot safely account for", so retirement is refused.
The same classification silently suppresses the retirement hint on the plain abort, because
isRetirementCandidate gates it on unaccountedContent.length === 0. So an author whose specs wrap
their bullets gets the bare Spec must have at least one requirement abort from #1750 with no
mention that retire_capabilities exists, and if they find it anyway it does not work.
Observed on 1.9.0. Reproduced below.
Reproduction
Two projects, identical but for one line break.
openspec/specs/doomed/spec.md:
# doomed Specification
## Purpose
A capability being retired, whose scenario bullet is wrapped to the repository's column limit.
## Requirements
### Requirement: The doomed thing happens
The system SHALL do the doomed thing.
#### Scenario: It happens
- **WHEN** the trigger occurs
- **THEN** the outstanding count becomes zeroopenspec/changes/c/specs/doomed/spec.md:
## REMOVED Requirements
### Requirement: The doomed thing happens
**Reason**: Retired.
**Migration**: None.openspec/changes/c/.openspec.yaml:
schema: spec-driven
retire_capabilities: trueA — the THEN bullet on one line. Works as designed:
Specs to update:
doomed: update
Retiring openspec/specs/doomed/spec.md: all requirements removed.
Totals: + 0, ~ 0, - 1, → 0
Specs updated successfully.
B — the identical spec with that one bullet wrapped:
- **THEN** the outstanding count becomes zero and the completions are recorded rather than the
earned total being reducedValidation errors in rebuilt spec for doomed (will not write changes):
✗ Spec must have at least one requirement
→ 'doomed' declares retire_capabilities, but the spec holds content the merge cannot safely
account for and deleting the file would take with it: "earned total being reduced". Move it
into `## Purpose` or a canonical requirement, or delete the spec by hand.
Aborted. No files were changed.
earned total being reduced is the second line of the bullet directly above it, not stray content.
Why this bites
Wrapping prose at a column limit is the norm in a repo that lints for it — ours holds every file to
100 columns, so effectively every scenario bullet longer than a short sentence wraps, and every
capability in the repo is therefore unretirable through the supported path.
It also hides the feature. On a real change retiring a six-requirement capability we got only:
Validation errors in rebuilt spec for wheel-spins (will not write changes):
✗ Spec must have at least one requirement
Aborted. No files were changed.
with no hint line, because unaccountedContent was non-empty for exactly this reason. We concluded
the tool had no retirement path, hand-deleted the spec directory alongside --skip-specs-style
manual work, and only found retire_capabilities afterwards by reproducing on a smaller spec that
happened not to wrap.
Suggested fix
Treat a line that continues a list item as part of that item when computing unaccountedContent —
i.e. a line indented under a -/* bullet, up to the next blank line or next block-level
construct. The same normalization presumably already exists wherever requirement and scenario text
is parsed for the merge itself, since the merge handles these specs correctly in every other
operation.
Separately, it may be worth decoupling the hint from unaccountedContent: telling an author that
retire_capabilities exists is useful even when a second problem also has to be solved, and the
current coupling is what made the feature invisible in the case above.
Environment
@fission-ai/openspec1.9.0, installed globally via npm- Node 24, Windows 11
- Related: No first-class way to retire a capability: a delta removing every requirement fails archive validation #1750 (the retirement path this makes unreachable), validate: MODIFIED/REMOVED/RENAMED-from headers that don't exist in base spec aren't caught until archive (proposal: opt-in cross-change MODIFIED) #1112 (the other archive-time
surprise we hit on the same change: aMODIFIEDheader naming a requirement that does not exist
in the base spec is only caught at archive)
Activity
fix(archive): share one list-marker definition with the paragraph rule
fix(archive): read a wrapped scenario bullet as one bullet (Fission-A…