-
Notifications
You must be signed in to change notification settings - Fork 4.7k
feat(sync): fold delta specs without archiving the change #1813
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Draft
clay-good
wants to merge
6
commits into
main
Choose a base branch
from
feat/standalone-spec-sync
base: main
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
Draft
Changes from all commits
Commits
Show all changes
6 commits
Select commit
Hold shift + click to select a range
fe4de02
feat(sync): fold delta specs without archiving the change
clay-good de03d4c
docs(sync): document openspec sync and the CI gate
clay-good 7b542c2
fix(sync): address review findings
clay-good 51b0318
fix(sync): stamp the lifecycle field last, after the specs are correct
clay-good eda3dd7
fix(sync): write the lifecycle field atomically
clay-good 679d3f7
fix(sync): fold each change against the live tree, and gate the check
clay-good File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,13 @@ | ||
| --- | ||
| "@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](https://github.com/ixxie) in [#1683](https://github.com/Fission-AI/OpenSpec/issues/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. |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,2 @@ | ||
| schema: spec-driven | ||
| created: 2026-09-07 |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,74 @@ | ||
| # Let a change's specs be folded before it is archived | ||
|
|
||
| ## Why | ||
|
|
||
| `archive` does two separable jobs in one command. It folds a change's deltas into | ||
| `openspec/specs/`, and it declares the change finished by moving its directory. | ||
| Welding them means the fold can only happen at the moment the move happens, which | ||
| on a team that reviews before merging is after the pull request closes. | ||
|
|
||
| So a team that wants CI to assert "the specs describe what shipped" has nothing to | ||
| assert during review. The only property expressible today is "nothing is left | ||
| unarchived", and that is violated by design for the entire life of every open PR: | ||
| the change sits in `changes/`, unarchived, precisely because it is not finished. | ||
| A gate that is red as its resting state is one everyone learns to ignore, and it | ||
| masks the real failures underneath (#1683). | ||
|
|
||
| The fix is to make the check conditional on the change's own claim — not "is | ||
| everything archived?" but "does anything claiming to be shipped still have deltas | ||
| missing from the specs?" A proposed change passes for free, so green is the | ||
| resting state and red means a real mistake. | ||
|
|
||
| ## What Changes | ||
|
|
||
| - **`openspec sync [change]`** folds delta specs into the main specs without | ||
| archiving. The merge engine already supports this: re-applying a folded delta | ||
| is a no-op it names the "early-sync pattern", so `archive` afterwards behaves | ||
| exactly as it always did. | ||
| - **`openspec sync --check`** asserts `shipped ⇒ folded` over the working tree and | ||
| exits 1 with the offending changes named. A pure function of files on disk, so | ||
| a pre-commit hook, a pre-push hook and CI run one command and agree. | ||
| - **`status: proposed | shipped`** becomes an optional field in a change's | ||
| `.openspec.yaml`. Absent means `proposed`, which is what a change under | ||
| `changes/` has always meant. Nothing writes it: not `new change`, not `archive`. | ||
| - **`openspec sync <change> --ship`** sets the field and folds in one working-tree | ||
| diff, so no intermediate commit claims a change is shipped while the specs say | ||
| otherwise. | ||
| - **`openspec list --status <state>`** filters by the field, and renders a | ||
| lifecycle column only when some change in the root declares one. | ||
|
|
||
| Two deliberate limits, both to keep this additive rather than a second lifecycle: | ||
|
|
||
| - **Sync never deletes a spec.** Retiring a capability is the one irreversible | ||
| operation in the system; it stays with `archive`, behind the | ||
| `retire_capabilities` marker and its rollback-safe deletion. Sync reports the | ||
| case and names archive. | ||
| - **Sync never examines archived changes.** Their deltas are history and later | ||
| changes supersede them; re-applying a months-old delta over everything that | ||
| came after is a merge conflict, not a drift check. The checked set is the | ||
| active changes declaring `shipped`, which drains itself as they archive. | ||
|
|
||
| "Folded" is decided by running the merge builder and seeing that it applied zero | ||
| operations — the same predicate `archive` uses to decide it has nothing to write. | ||
| Not a byte-comparison of the rebuilt output: the rebuild normalizes blank lines, | ||
| so a hand-formatted main spec would compare unequal while being perfectly in | ||
| sync. Sharing archive's own predicate is also what stops the checker and the doer | ||
| from drifting apart (#1112). | ||
|
|
||
| ## Impact | ||
|
|
||
| - Affected specs: `cli-sync` (ADDED), `cli-list` (MODIFIED: filtering) | ||
| - Affected code: `src/core/sync.ts` (new), `src/core/list.ts`, | ||
| `src/utils/change-metadata.ts`, `src/core/change-metadata/schema.ts`, | ||
| `src/cli/index.ts`, `src/core/completions/command-registry.ts`, | ||
| `src/core/archive.ts` (two helpers exported, no behavior change) | ||
| - Affected docs: `docs/cli.md`, `docs/team-workflow.md`, | ||
| `docs-lab/reference/cli.md` | ||
|
|
||
| Credit: the design is Matan Bendix Shenhav's, from #1683 and his implementation | ||
| #1684. His: the diagnosis, `shipped ⇒ folded` as a tree predicate (V), the | ||
| checker-versus-doer argument (IV), the standalone idempotent `sync` (III), status | ||
| as data (I and II), and shipping in one working-tree diff (VI). This change takes | ||
| a smaller, additive subset — no mode, no layout change, no migration — and | ||
| decides folded-ness by archive's zero-operations predicate rather than his | ||
| byte-identical regeneration. |
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.