PENDING annotation position¶
Context¶
The project uses > ⚠️ **PENDING ...** blockquotes in
openspec/specs/<capability>/spec.md to warn readers that an
in-flight change is about to MODIFY or REMOVE a landed requirement.
The annotation is inserted at propose time and disappears
automatically when the owning change is archived.
The natural place to put such an annotation is directly under the
### Requirement: heading — the reader's first eye-catch on the
requirement. CLAUDE.md's original "Format" block prescribed exactly
that position.
Constraint¶
The openspec CLI (@fission-ai/openspec v1.4.x) parses each
requirement into a text field, taken from the FIRST non-empty,
non-metadata line after the ### Requirement: header
(parseRequirements in
node_modules/@fission-ai/openspec/dist/core/parsers/markdown-parser.js).
Its Zod schema (RequirementSchema in
node_modules/@fission-ai/openspec/dist/core/schemas/base.schema.js)
then requires text to contain the substring SHALL or MUST.
When the annotation sits under the heading, that > ⚠️ **PENDING ...**
blockquote line IS the first non-empty content line — and it contains
no SHALL/MUST. Every archive from that point onward re-parses the full
capability spec after applying the delta and fails validation with:
x Requirement must contain SHALL or MUST keyword
This is not scoped to the change that owns the annotation — the same
error breaks openspec archive for every unrelated change touching
the same capability. In practice, 4 in-flight annotations in
openspec/specs/dashboard/spec.md blocked archive of 4 unrelated
changes until we discovered the pattern and used --no-validate.
Training the team to reach for --no-validate as the default is
worse than the annotation position, so we address the root cause.
Decision¶
Place the PENDING annotation immediately after the requirement's
SHALL/MUST body paragraph, before any #### Scenario: header:
### Requirement: <name>
<existing SHALL/MUST body — stays first non-empty line>
> ⚠️ **PENDING <ADDED|MODIFIED|REMOVED>** by [<change-id>](../../changes/<change-id>/): <one-line reason>.
<remaining body / #### Scenario: blocks>
The annotation still lives inside the requirement's own block and remains visible to human readers scanning the requirement; only the first-line slot is preserved for the SHALL/MUST body.
CI enforces this via server/openspec-annotation.test.ts, which
walks every openspec/specs/**/spec.md and asserts the first
non-empty content line of each requirement contains SHALL or MUST.
Alternatives considered¶
- Fork the openspec CLI parser to skip blockquotes when
extracting
text. Rejected — creates a version-drift maintenance burden for a project convention. - Use an HTML comment
<!-- PENDING ... -->instead of a blockquote. Rejected — invisible to human readers scanning the spec, which defeats the annotation's purpose (warning in-flight status). - Move the annotation to a separate top-of-file section like
## In-Flight Annotations. Rejected — decouples the annotation from the requirement it annotates; a reader scanning the requirement block wouldn't see it. - Rely on
--no-validateat archive time. Rejected — hides real validation errors and trains the team into a bad default.
Consequences¶
- CLAUDE.md's hard-rule section is updated with the new format and a rationale pointer to this ADR.
.claude/skills/opsx-revert/SKILL.mdstep 8 is updated to insert in the new position..claude/skills/openspec-flow/SKILL.md"PENDING annotation" section updated to match.- All 4 existing annotations in
openspec/specs/dashboard/spec.mdare repositioned to the new slot (seefix-pending-annotation-parser-compatdiff). - Future
openspec archiveruns no longer need--no-validatefor this cause — the rebuild validator can be trusted again. - CI regression test at
server/openspec-annotation.test.tscatches any future regression at PR review time.
Escape hatch¶
If a future requirement's SHALL/MUST body is genuinely two-paragraph
(rare), place the annotation after the LAST SHALL/MUST-bearing
paragraph, still before the first #### Scenario: header. The parser
only reads the first non-empty line, so anything after that is safe.