Files
OpenSpec/.changeset/sync-lifecycle-status.md
T
Clay GoodandClaude Opus 5 679d3f7bbb fix(sync): fold each change against the live tree, and gate the check
Hardening round. Four defects, all reproduced before being fixed.

**Two shipped changes touching one capability lost a fold.** `applyFolds`
evaluated every change against the pre-write baseline and then wrote them
all, so each rebuilt body was a whole file derived from the original spec
and the second write erased the first — silently, while the console
reported both as applied. The changes did not conflict; the batch read a
stale baseline. Archive never had this because it takes one change per
invocation. Sync now folds one change at a time, re-deriving each against
the specs as they are at that moment.

**Two capability ids resolving to one file overwrote each other.** A
capability directory may deliberately be a symlink, so this is a shape the
trust model allows rather than an accident. Archive refuses it outright;
sync wrote both and lost one. Archive's check is now shared by both, so
they cannot disagree about which trees they will write.

**`sync --check` was green for a change whose delta the writer refuses.**
The check only asked "is what was discovered folded?", and
`discoverSpecFiles` does not walk `specs/spec.md`, so a change whose only
delta sat there certified as clean while archive and the sync writer both
refused the same tree (#1385). Delta validation now runs inside the
evaluation, so it runs on the check path too — and it asks archive's own
question about whether a change has deltas at all, so a zero-delta change
gets the same answer from both commands.

**`--ship` could fold and then fail forever.** A change with no
`.openspec.yaml` had its specs written and its stamp refused, and the
rerun failed in the same place, so the ordering's usual self-correction
did not apply. Checked up front now.

Also: `--no-validate` requires `--yes`, matching archive's refusal to skip
validation without an explicit answer; the rollback no longer "restores"
targets it never wrote (a false data-loss alarm) and refuses to clobber a
file something else changed mid-run; and `test/core/sync.test.ts` restores
the process working directory before removing its temp tree, which Windows
locks.

Nineteen tests added, each mutation-verified.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-07 10:14:52 -05:00

1.2 KiB

@fission-ai/openspec
@fission-ai/openspec
minor

Add openspec sync, which folds a change's delta specs into the main specs without archiving it, and an optional status: proposed | shipped field in a change's .openspec.yaml.

openspec sync --check gates on one property: a change that claims to be shipped has its deltas in specs/. A proposed change passes for free, so the check is green as its resting state and red only on a real mistake — unlike a check for "is everything archived?", which is red for the whole life of every open pull request. It reads only files on disk, so a pre-commit hook, a pre-push hook and CI run the same command and agree.

openspec list --status <state> filters changes by that field.

Everything here is opt-in and inert by default. The status field is absent unless a project writes it, nothing generates it, and archive is unchanged.

Designed by @ixxie in #1683 — the diagnosis that archive welds a state transition to a text merge, shipped ⇒ folded as a predicate over the working tree, and the standalone sync that makes it checkable. This ships a smaller, additive subset of that proposal.