Skip to content

fix(archive): treat early-synced REMOVED deltas as no-ops, plus audit follow-ups#1437

Merged
clay-good merged 4 commits into
mainfrom
audit-followups
Jul 23, 2026
Merged

fix(archive): treat early-synced REMOVED deltas as no-ops, plus audit follow-ups#1437
clay-good merged 4 commits into
mainfrom
audit-followups

Conversation

@clay-good

@clay-good clay-good commented Jul 23, 2026

Copy link
Copy Markdown
Collaborator

Status: LGTM — full suite green (112 files / 2,251 tests, 14 new), lint clean, all 36 in-repo specs validate, every fix verified end-to-end against the built CLI. Hardened by two rounds of three independent adversarial reviews each, plus CodeRabbit; all confirmed findings addressed (details below).

What was wrong: a full audit of v1.6.0..main found no regressions, but a handful of pre-existing gaps — one workflow-blocking, the rest paper cuts:

Area Problem
archive A REMOVED delta already synced to the main spec aborted the archive (REMOVED failed ... not found), while ADDED (#1376) and RENAMED (#1386) got already-synced escapes. The sync skill steers users straight into this.
archive Totals reported removals that were skipped (- 2 removed next to "2 ignored").
archive The has-delta-specs gate matched ## ADDED Requirements case-sensitively while the parser is case-insensitive, so lowercase headers skipped delta validation and failed later with a worse error.
discovery A symlinked specs/<cap>/spec.md was invisible to discoverSpecFiles but counted as content by hasAnyFileUnder and the artifact graph — archive would silently drop the delta.
show Every openspec show <change> printed Warning: Ignoring flags not applicable to change: scenarios (commander defaults --no-scenarios to scenarios: true).
parsers buildCodeFenceMask existed twice (#1151 added a second copy); both claimed to be the single source of truth.
templates On the core profile, apply/update/onboard bodies sent users to /opsx:continue and /opsx:new, which core never installs (#1410 fixed only the init hints).
qwen/bob Command bodies (qwen) and skills (both) referenced /opsx:*, but their command files answer to /opsx-<id> — every cross-reference pointed at a nonexistent command.
specs-apply Dead applySpecs export with zero callers that bypassed store-aware roots.

How it was fixed:

  • REMOVED now warns and continues when the requirement is already gone (treating it as already removed) — analogous to the ADDED/RENAMED escapes, but with a warning since, unlike RENAMED, there is no signal separating early-sync from a typo. Totals count applied removals only. A missing MODIFIED target still aborts.
  • The delta gate regex gained /i, so lowercase headers get the same validateChangeDeltaSpecs errors validate shows.
  • Discovery stats symlinked spec.md entries (dangling links skipped, other errors still thrown); symlinked directories remain unfollowed, as before.
  • show counts scenarios as user-provided only when it is false.
  • One buildCodeFenceMask lives in code-fence.ts; requirement-text.ts re-exports it (no import-site churn).
  • Templates keep the /opsx:continue / /opsx:new suggestions but name the CLI fallback (openspec status --change / openspec instructions) for profiles that don't install them; onboard's extra-command tables say "only if installed."
  • qwen's adapter transforms bodies like bob's; both joined opencode/pi/oh-my-pi in the skill-side hyphen transform. Verified init --tools qwen,bob output has zero /opsx: references left.
  • applySpecs deleted (not reachable from the public entry point).

Adversarial-review round (second commit): three independent reviews (core logic, generation/templates, tests/compat) plus CodeRabbit produced these additional fixes, each with tests:

  • A delta that both RENAMEs and REMOVEs the same requirement is rejected explicitly by both validate and archive — previously it failed incidentally at apply time, and the new warn-and-continue path would have masked it.
  • archive --json carries spec-merge warnings in a new optional warnings array, so agent flows see the skipped-REMOVED signal (previously the silent path swallowed it entirely — including for typos).
  • Archive skips rewriting a spec whose operations were all already synced (no normalization churn; a REMOVED-only delta no longer materializes an empty skeleton spec).
  • Init's getting-started hint prints each tool's real invocation form (/opsx-propose for qwen/bob/opencode/pi/oh-my-pi, /skill:openspec-propose for Kimi).
  • Onboard's pause guidance names the CLI fallback when /opsx:continue isn't installed (CodeRabbit).
  • openspec-conventions/spec.md updated to state the idempotent archive semantics (direct spec edit, same precedent as fix(archive): keep the delta spec's Purpose in a new main spec #1431 — this also fixes the clause that fix(archive): stop failing on specs that were already synced before archiving #1376's idempotent-ADDED already contradicted), and a changeset was added.

Round-2 review (third commit): a second set of three adversarial reviews on the updated diff (second-round-delta, fresh-eyes with a mandatory attack set, and consumers/contract) all returned merge-safe, and produced two more hardening fixes: a REMOVED header that differs only in case or interior whitespace from an existing requirement now stays a hard abort naming the near-miss (a fold-insensitive match present in the spec means typo, not early-sync), and specsUpdated is true only when a spec file was actually written — a fully-already-synced change prints Specs already in sync; no files changed. and reports specsUpdated: false with its skips in warnings (CodeRabbit). The reviews also verified: no template or skill parses archive output, the deleted exports were never publicly reachable (exports map blocks deep imports, so the patch bump is right), CRLF deltas work through the new paths, error paths never leak partial warnings arrays, and all parity hashes were independently recomputed.

Replication / proof: repro before the fix — sync a REMOVED delta to the main spec, then openspec archive <change>REMOVED failed for header ... - not found, exit 1. After: archives with the warning, honest +0/−0 totals, and the main spec byte-untouched (verified live and pinned by test). New tests cover each fix: early-synced REMOVED (human + JSON), applied-only counts, RENAMED+REMOVED conflict (validate + archive), lowercase-header gate parity, symlinked/dangling spec.md, default vs explicit --no-scenarios, qwen hyphen transform, transformer selection, and the per-tool init hint. Parity hashes and skills/ regenerated via the documented recipe.

Notes / nits:

  • Other flat-filename adapters (cursor, auggie, iflow, junie, roocode, trae, …) have the same /opsx: cross-reference pattern; each tool's invocation convention needs individual verification, so this PR only fixes the two with a documented opsx-<id> surface (and matching in-repo precedent). Codex reference rewriting stays parked, per the existing comment.
  • The sync skill's "partial MODIFIED" guidance still conflicts with the archive drift guard (pre-existing tension, tightened by fix(archive): make scenario-drift check multiplicity-aware (#1246) #1391) — left for a dedicated discussion.

🤖 Generated with Claude Code

Summary by CodeRabbit

  • New Features
    • Improved “blocked” change guidance with CLI fallback when continue/new commands aren’t available.
    • Spec discovery now includes valid symlinked spec.md files and ignores dangling symlinks.
    • Generated command references use hyphenated names for supported tools.
  • Bug Fixes
    • openspec show --json scenario warnings now respect explicit --no-scenarios.
    • openspec archive now preserves non-fatal warnings in output, avoids unnecessary rewrites, and treats early-synced removals as warnings.
    • Validation now rejects deltas that combine rename+remove on the same requirement.
  • Documentation
    • Updated onboarding/workflow and archive --json contract to reflect optional command availability and warning output.

… follow-ups

Follow-ups from the post-v1.6.0 full-branch audit:

- archive: a REMOVED delta whose requirement is already gone from the main
  spec (early-sync pattern) now warns and continues instead of aborting,
  matching the ADDED (#1376) and RENAMED (#1386) escapes; spec-update totals
  now count applied removals only
- archive: the has-delta-specs gate matches section headers
  case-insensitively like the parser, so lowercase headers get the same
  delta validation errors validate reports
- discovery: a symlinked specs/<cap>/spec.md is resolved instead of being
  invisible (hasAnyFileUnder and the artifact graph already counted it);
  dangling links are skipped
- show: a plain `openspec show <change>` no longer warns about the
  never-passed `scenarios` flag (commander defaults --no-scenarios to true)
- parsers: buildCodeFenceMask now has a single implementation in
  code-fence.ts; requirement-text.ts re-exports it
- templates: apply/update/onboard no longer dead-end core-profile users on
  /opsx:continue and /opsx:new - they name the CLI fallback (openspec
  status/instructions) for profiles that do not install those workflows
- qwen/bob: command bodies and skills reference commands by the hyphen
  names their files actually answer to (/opsx-<id>), matching
  opencode/pi/oh-my-pi
- specs-apply: remove the dead applySpecs export (no callers, bypassed
  store-aware roots)

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@clay-good
clay-good requested a review from TabishB as a code owner July 23, 2026 20:47
@coderabbitai

coderabbitai Bot commented Jul 23, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: 4636feed-1895-4cb1-8a94-49a0069ba807

📥 Commits

Reviewing files that changed from the base of the PR and between 74a1026 and 33ebba5.

📒 Files selected for processing (7)
  • .changeset/archive-early-synced-removed.md
  • openspec/specs/openspec-conventions/spec.md
  • src/core/parsers/requirement-blocks.ts
  • src/core/specs-apply.ts
  • src/core/validation/validator.ts
  • test/core/archive.test.ts
  • test/core/validation.test.ts
🚧 Files skipped from review as they are similar to previous changes (4)
  • .changeset/archive-early-synced-removed.md
  • src/core/validation/validator.ts
  • openspec/specs/openspec-conventions/spec.md
  • src/core/specs-apply.ts

📝 Walkthrough

Walkthrough

The PR refines archive delta validation and warning propagation, adds hyphenated command handling for additional tools, supports symlinked spec discovery, corrects explicit flag warnings, relocates code-fence masking, and updates installation-aware workflow guidance.

Changes

Core behavior and workflow guidance

Layer / File(s) Summary
Delta application and archive validation
src/core/specs-apply.ts, src/core/archive.ts, src/core/validation/validator.ts, openspec/specs/openspec-conventions/spec.md
Already-removed requirements produce warnings without rewrites, JSON results expose warnings, lowercase headers are recognized, and conflicting rename/removal entries are rejected.
Archive validation coverage and contracts
test/core/archive.test.ts, test/core/validation.test.ts, docs/agent-contract.md, .changeset/*
Tests cover synchronized removals, conflicts, near matches, atomic failures, JSON output, and updated archive contract semantics.
Hyphenated command references
src/utils/command-references.ts, src/core/command-generation/adapters/qwen.ts, src/core/init.ts, test/utils/command-references.test.ts, test/core/command-generation/adapters.test.ts, test/core/init.test.ts
bob and qwen use hyphenated command transformations in generated content and initialization hints.
Spec discovery and parser organization
src/utils/spec-discovery.ts, test/utils/spec-discovery.test.ts, src/core/parsers/requirement-text.ts
Valid symlinked spec.md files are discovered, dangling links are skipped, and code-fence masking is re-exported from a dedicated module.
Show flags and workflow availability guidance
src/commands/show.ts, test/commands/show.test.ts, skills/openspec-*/SKILL.md, src/core/templates/workflows/*.ts, test/core/templates/skill-templates-parity.test.ts
Warnings distinguish explicit --no-scenarios; workflow instructions mark optional commands as installation-dependent and provide CLI fallbacks, with parity hashes updated.

Estimated code review effort: 4 (Complex) | ~45 minutes

Possibly related issues

Possibly related PRs

Suggested reviewers: tabishb, javigomez

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 33.33% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title matches the main archive change and accurately signals additional audit follow-ups.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch audit-followups

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@src/core/templates/workflows/apply-change.ts`:
- Line 53: Preserve selected store context in all blocked-state and update
fallback command examples by appending the conditional --store <id> flag only
when store selection exists, while leaving local-root examples unchanged. Apply
this consistently at src/core/templates/workflows/apply-change.ts lines 53 and
213, skills/openspec-apply-change/SKILL.md line 51,
src/core/templates/workflows/update-change.ts lines 87-88 and 174-175, and
skills/openspec-update-change/SKILL.md line 86.

In `@src/core/templates/workflows/onboard.ts`:
- Line 516: Add a CLI recovery fallback next to the /opsx:continue guidance in
src/core/templates/workflows/onboard.ts at lines 516-516, describing openspec
status --change "<name>" --json and openspec instructions <artifact-id> --change
"<name>" --json, then regenerate the committed skill content and parity hash.
Mirror the same fallback in skills/openspec-onboard/SKILL.md at lines 506-506;
both sites require the guidance update.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: 4cb877b6-4150-4e85-85d1-59a9b1af62cb

📥 Commits

Reviewing files that changed from the base of the PR and between 6a5171e and 457abf3.

📒 Files selected for processing (19)
  • skills/openspec-apply-change/SKILL.md
  • skills/openspec-onboard/SKILL.md
  • skills/openspec-update-change/SKILL.md
  • src/commands/show.ts
  • src/core/archive.ts
  • src/core/command-generation/adapters/qwen.ts
  • src/core/parsers/requirement-text.ts
  • src/core/specs-apply.ts
  • src/core/templates/workflows/apply-change.ts
  • src/core/templates/workflows/onboard.ts
  • src/core/templates/workflows/update-change.ts
  • src/utils/command-references.ts
  • src/utils/spec-discovery.ts
  • test/commands/show.test.ts
  • test/core/archive.test.ts
  • test/core/command-generation/adapters.test.ts
  • test/core/templates/skill-templates-parity.test.ts
  • test/utils/command-references.test.ts
  • test/utils/spec-discovery.test.ts

Comment thread src/core/templates/workflows/apply-change.ts
Comment thread src/core/templates/workflows/onboard.ts Outdated
…, skip no-op writes

Adversarial-review round for #1437:

- a delta that both RENAMEs and REMOVEs the same requirement is rejected
  explicitly by both validate and archive - the warn-and-continue REMOVED
  path would otherwise have masked the contradiction that previously
  failed incidentally at apply time
- buildUpdatedSpec collects its warnings and archive --json carries them
  in a new optional `warnings` array, so agent flows see the same
  skipped-REMOVED signal humans get on stdout
- archive skips rewriting a spec whose operations were all already
  synced, instead of churning normalization differences into the file
  (and no longer materializes an empty skeleton for a REMOVED-only new
  spec)
- init's getting-started hint uses each tool's real invocation form
  (/opsx-propose for qwen/bob/opencode/pi/oh-my-pi)
- onboard's pause guidance names the CLI fallback when /opsx:continue is
  not installed (CodeRabbit)
- openspec-conventions spec updated to state the idempotent archive
  semantics; changeset added

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 3

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In @.changeset/archive-early-synced-removed.md:
- Line 5: Update the wording in the changeset description to use the grammatical
form “Qwen- and Bob-generated files” instead of “qwen and bob generated files,”
preserving the rest of the description unchanged.

In `@src/core/archive.ts`:
- Around line 523-537: The archive update flow around writeUpdatedSpec must
track whether at least one file was actually written. Set specsUpdated and emit
the “Specs updated successfully” output only when writeUpdatedSpec runs, while
preserving zero-count skips and accurate JSON results for already-synced
changes.

In `@test/core/archive.test.ts`:
- Line 483: Remove the duplicate block-scoped logCalls declarations, retaining
only one declaration in test/core/archive.test.ts at lines 483-483 and one in
test/core/init.test.ts at lines 1071-1071; preserve each test’s existing use of
the retained constant.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: a65afc04-76c1-42e6-b1e6-84b275edea03

📥 Commits

Reviewing files that changed from the base of the PR and between 457abf3 and d5b55cc.

📒 Files selected for processing (12)
  • .changeset/archive-early-synced-removed.md
  • openspec/specs/openspec-conventions/spec.md
  • skills/openspec-onboard/SKILL.md
  • src/core/archive.ts
  • src/core/init.ts
  • src/core/specs-apply.ts
  • src/core/templates/workflows/onboard.ts
  • src/core/validation/validator.ts
  • test/core/archive.test.ts
  • test/core/init.test.ts
  • test/core/templates/skill-templates-parity.test.ts
  • test/core/validation.test.ts
🚧 Files skipped from review as they are similar to previous changes (3)
  • src/core/templates/workflows/onboard.ts
  • test/core/templates/skill-templates-parity.test.ts
  • src/core/specs-apply.ts

Comment thread .changeset/archive-early-synced-removed.md Outdated
Comment thread src/core/archive.ts
Comment thread test/core/archive.test.ts
…or no-op archives

Round-2 adversarial review for #1437:

- a REMOVED header that differs only in case or interior whitespace from
  an existing requirement is a typo, not an early sync - it stays a hard
  abort naming the near-miss, instead of degrading to warn-and-continue
- specsUpdated is true only when a spec file was actually written; a
  fully-already-synced change prints "Specs already in sync; no files
  changed." and reports specsUpdated: false in JSON (CodeRabbit)
- agent-contract documents the archive warnings field and specsUpdated
  semantics; changeset wording fixed (CodeRabbit)

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@cloudflare-workers-and-pages

cloudflare-workers-and-pages Bot commented Jul 23, 2026

Copy link
Copy Markdown

Deploying openspec-docs with  Cloudflare Pages  Cloudflare Pages

Latest commit: 74a1026
Status: ✅  Deploy successful!
Preview URL: https://9ea2eb5f.openspec-docs.pages.dev
Branch Preview URL: https://audit-followups.openspec-docs.pages.dev

View logs

@alfred-openspec alfred-openspec left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The new conflict guard still misses case/whitespace variants: RENAMED FROM: Old Name plus REMOVED: old name passes validate --strict --json, then archive exits 0, renames the requirement, reports the removal as already synced, and archives the change. Please use the same case/whitespace-folded identity for the RENAMED+REMOVED cross-section check in both validation and apply, with focused regressions for both paths.

…ce-insensitively

Addresses alfred's review on #1437: `RENAMED FROM: Old Name` plus
`REMOVED: old name` slipped past the exact-match cross-section guard,
so validate passed, archive renamed the requirement, reported the
removal as already synced, and archived the change.

Both the validator and the apply-side guard now compare the two
spellings with the shared foldRequirementName (lowercase, collapsed
whitespace), and the error names the variant spelling when it differs.
Focused regressions cover both paths; requirement matching everywhere
else stays case-sensitive.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@clay-good

Copy link
Copy Markdown
Collaborator Author

@alfred-openspec Fixed in 33ebba5. Both the validator and the apply-side guard now compare the RENAMED FROM header against REMOVED entries with a shared case- and whitespace-insensitive fold (foldRequirementName, exported from requirement-blocks.ts next to normalizeRequirementName), and the error names the variant spelling when the two differ.

Your exact scenario, re-run against the built CLI:

  • openspec validate <change> --strict --jsonvalid: false with Requirement present in both RENAMED and REMOVED: "Old Name" (REMOVED spells it "old name"), exit 1
  • openspec archive <change> --yes (and even --no-validate, which hits the apply-side guard) → aborts, exit 1, main spec byte-untouched, change not archived

Focused regressions for both paths: test/core/validation.test.ts ("rejects a case/whitespace variant of the renamed FROM header in REMOVED", using old name to cover whitespace folding too) and test/core/archive.test.ts ("should abort when REMOVED spells the renamed FROM header with different case"). Requirement matching everywhere else stays case-sensitive — the fold is used only for this conflict and the near-miss REMOVED typo check, both of which only ever reject. Full suite 2,253 green, openspec-conventions spec and the changeset updated to state the folded comparison.

@alfred-openspec alfred-openspec left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The follow-up closes the case/whitespace conflict gap in both validation and apply. I reproduced the original failure against the built CLI, confirmed both paths now fail safely without mutation, and the full 2,253-test suite passes at head 33ebba5.

@clay-good
clay-good added this pull request to the merge queue Jul 23, 2026
Merged via the queue into main with commit 19d4171 Jul 23, 2026
16 checks passed
@clay-good
clay-good deleted the audit-followups branch July 23, 2026 22:59
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants