Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/create-main-spec-for-new-capability.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@fission-ai/openspec": patch
---

Agent-driven archive and sync workflows now create a missing main spec from `ADDED` requirements instead of treating it as already synced. They block sync rather than inventing `MODIFIED` or `RENAMED` requirements or writing an empty spec for a `REMOVED`-only delta, while preserving the user's explicit choice to archive without syncing. A REMOVED-only delta with `retire_capabilities: true` remains already synced when its main spec is gone. Fixes #1222 and #1264.
2 changes: 1 addition & 1 deletion docs-lab/reference/glossary.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ OpenSpec reuses words that mean something else in git, CI, and agent tooling. Ea
| **Fast-forward** | Create a change proposal with every planning artifact in one pass, ready to implement. Skill: `openspec-ff-change`. Not a git fast-forward. | [Skills](skills.md) |
| **Legacy workflow** | The pre-OPSX `/openspec:*` commands. | [Migration](../help/legacy/migration.md) |
| **Loop** | The cycle a change proposal moves through: explore, propose, review, apply, archive. | [Quickstart](../start/quickstart.md) |
| **Main specs** | The `openspec/specs/` tree: the current, agreed behavior of your system. Archiving merges deltas into it. | [Concepts](../guides/concepts.md) |
| **Main specs** | The `openspec/specs/` tree: the current, agreed behavior of your system. Archiving merges deltas into it. A capability with no spec yet gets one from its `ADDED` requirements. | [Concepts](../guides/concepts.md) |
| **OpenSpec root** | The `openspec/` tree a command resolves to and operates on: your repo's, or a store's. | [Stores](../multi-repo/stores.md#where-artifacts-get-created-when-using-stores) |
| **OPSX** | The current OpenSpec workflow system, and the command prefix it installs (`/opsx:`). | [Architecture](architecture/index.md) |
| **Profile** | Which workflows init installs: `core` or `custom`. | [Profiles](../customize/profiles.md) |
Expand Down
46 changes: 46 additions & 0 deletions openspec/specs/opsx-archive-skill/spec.md
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,52 @@ The skill SHALL prompt to sync delta specs before archiving if specs exist.
- **AND** stop without archiving if the sync fails or any capability does not verify
- **AND** archive only after verification passes, or when the user explicitly chose to archive without syncing or to archive already-synced specs

#### Scenario: Applicable ADDED delta whose main spec does not exist yet

- **WHEN** agent compares a delta spec against its main spec at `openspec/specs/<capability-path>/spec.md`
- **AND** that main spec does not exist yet
- **AND** the delta has `## ADDED Requirements`
- **AND** the delta has no `## MODIFIED Requirements` or `## RENAMED Requirements`
- **THEN** count that capability as needing sync rather than as already synced
- **AND** name it in the summary as a main spec the sync will create
- **AND** never treat the missing main spec as nothing to apply
- **AND** if the delta also has `## REMOVED Requirements`, warn that they will be ignored because there is no main spec to remove them from
- **AND** create the main spec from only the delta's `## ADDED Requirements`

#### Scenario: Unsupported delta operation whose main spec does not exist yet

- **WHEN** a delta targets a capability whose main spec does not exist yet
- **AND** the delta has `## MODIFIED Requirements` or `## RENAMED Requirements`
- **THEN** report that only ADDED requirements can create a new main spec
- **AND** mark the capability as sync-blocked without writing a main spec

#### Scenario: Explicitly retired capability whose main spec is missing

- **WHEN** a delta contains only `## REMOVED Requirements` and its main spec is missing
- **AND** the change's `.openspec.yaml` declares `retire_capabilities: true`
- **THEN** count that capability as already synced and report that it is already retired
- **AND** warn that there is nothing left to remove and do not recreate the main spec
- **AND** apply the same rule when verifying a completed sync, so retiring a capability does not block archiving

#### Scenario: Nothing to put in a missing main spec without a declared retirement

- **WHEN** a delta targets a capability whose main spec does not exist yet
- **AND** the delta has no `## ADDED Requirements`
- **AND** it is not a REMOVED-only delta with `retire_capabilities: true`
- **THEN** report that no sync is possible
- **AND** if the delta has only `## REMOVED Requirements`, warn that there is no main spec to remove them from and leave the main-spec tree unchanged
- **AND** mark the capability as sync-blocked, since the verification pass would re-read the same missing spec

#### Scenario: Sync-blocked capability during archive assessment

- **WHEN** any capability is sync-blocked during the initial assessment
- **THEN** assess the remaining capabilities and summarize the blockers before prompting
- **AND** offer only "Archive without syncing" and "Cancel"
- **AND** archive without writing main specs only if the user explicitly chooses "Archive without syncing"
- **AND** stop without archiving if the user cancels
- **AND** do not start any sync while a capability is blocked, even if other capabilities could sync
- **AND** a failed sync or post-sync verification still stops without archiving; do not silently fall back to skipping sync

Comment thread
coderabbitai[bot] marked this conversation as resolved.
#### Scenario: No delta specs

- **WHEN** agent checks for delta specs
Expand Down
17 changes: 17 additions & 0 deletions openspec/specs/specs-sync-skill/spec.md
Original file line number Diff line number Diff line change
Expand Up @@ -71,10 +71,27 @@ The agent SHALL reconcile main specs with delta specs using the delta operation

#### Scenario: New capability spec
- **WHEN** delta spec exists for a capability not in main specs
- **AND** it has ADDED requirements and no MODIFIED or RENAMED requirements
- **THEN** create new main spec file at `openspec/specs/<capability-path>/spec.md`, preserving the delta's path relative to `specs/`
- **AND** copy the delta's `## Purpose` body into it when the delta has one, matching what `openspec archive` does
- **AND** write a brief TBD placeholder Purpose only when the delta has none

#### Scenario: MODIFIED or RENAMED against a capability with no main spec
- **WHEN** delta contains `## MODIFIED Requirements` or `## RENAMED Requirements`
- **AND** the capability has no main spec yet
- **THEN** stop the sync for that capability and report that only ADDED requirements are allowed for a new spec, matching what `openspec archive` does
- **AND** never invent the missing requirement
- **AND** skip any `## REMOVED Requirements` with a warning, since there is nothing to remove

#### Scenario: Nothing to put in a new spec
- **WHEN** a delta targets a capability with no main spec
- **AND** the delta has no `## ADDED Requirements` to seed it with
- **THEN** create no main spec and leave the specs directory untouched
- **AND** for a REMOVED-only delta with `retire_capabilities: true` in the change's `.openspec.yaml`, report the capability as already retired and continue without recreating it
- **AND** without that marker, report a REMOVED-only sync as blocked, matching `openspec archive`, which aborts with `Spec must have at least one requirement`
- **AND** report an empty delta as blocked because it has no operations to sync
- **AND** never write an empty `## Requirements` section

#### Scenario: Merged main spec keeps canonical structure
- **WHEN** the agent writes a main spec during sync
- **THEN** every requirement lives under a single `## Requirements` section
Expand Down
16 changes: 11 additions & 5 deletions skills/openspec-archive-change/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -94,17 +94,23 @@ Archive a completed change in the experimental workflow.

**If delta specs exist:**
- Compare each delta spec with its corresponding main spec at `<planningHome.root>/openspec/specs/<capability-path>/spec.md` (use the store-aware `planningHome.root` from step 2, not a hardcoded repo path)
- A missing main spec is **not automatically** "already synced". For a new capability, the main spec is an *output* of the sync, not an input:
- If the delta has MODIFIED or RENAMED requirements, report that only ADDED requirements can create a new main spec and mark that capability as sync-blocked. Never invent a requirement that has no current version.
- Otherwise, if the delta has only REMOVED requirements and the change's `.openspec.yaml` declares `retire_capabilities: true`, the capability is already retired: count it as already synced, warn that there is nothing left to remove, and do not recreate the main spec. Apply this rule both now and when verifying a completed sync.
- Otherwise, if the delta has no ADDED requirements, report that no sync is possible and mark that capability as sync-blocked. For a REMOVED-only delta, warn that there is no main spec to remove from and leave the main-spec tree unchanged. `openspec archive` refuses the unmarked REMOVED-only case with `Spec must have at least one requirement`.
- Otherwise, count the capability as needing sync and name it in the summary (`<capability-path>: new main spec will be created`). If the delta also has REMOVED requirements, warn that they will be ignored because there is no main spec to remove from. The sync creates the main spec from only the delta's ADDED requirements, exactly as `openspec archive` does.
- Determine what changes would be applied (adds, modifications, removals, renames)
- Show a combined summary before prompting
- Continue assessing the remaining capabilities even when one is sync-blocked. Show a combined summary before prompting.

**Prompt options:**
- If changes needed: "Sync now (recommended)", "Archive without syncing"
- If already synced: "Archive now", "Sync anyway", "Cancel"
- If any capability is sync-blocked: explain why and offer only "Archive without syncing", "Cancel"
- Otherwise, if changes needed: "Sync now (recommended)", "Archive without syncing"
- Otherwise, if already synced: "Archive now", "Sync anyway", "Cancel"

Route on the answer:
- "Cancel" — stop, do not archive
- "Archive without syncing" or "Archive now" — proceed to archive
- "Sync now" or "Sync anyway" — sync, then verify (below)
- "Sync now" or "Sync anyway" — sync, then verify (below). Do not start any sync while a capability is sync-blocked; explain the blocker and repeat the available choices.
- Anything else — ask again rather than archiving

Before a selected sync writes any main spec, run
Expand All @@ -118,7 +124,7 @@ Archive a completed change in the experimental workflow.

Then run the `openspec-sync-specs` workflow inline (agent-driven intelligent merge) for change '<name>', passing the delta spec analysis and the fetched specs-rule snapshot from above, and wait for it to finish. The inline sync must reuse that snapshot without fetching `specs` instructions again. Do not delegate it to a background task — step 5 would move `changeRoot` out from under a sync that is still reading it, leaving the change archived and the main specs never updated. If your agent can only run it by delegation, delegate synchronously and wait for the result.

Then re-run the comparison from the top of this step against every capability that has a delta spec in `artifactPaths.specs.existingOutputPaths` — not only the ones the sync reports it touched. A successful sync leaves nothing left to apply, so each capability must now read as already synced:
Then re-run the comparison from the top of this step, including the explicitly retired, missing-spec case, against every capability that has a delta spec in `artifactPaths.specs.existingOutputPaths` — not only the ones the sync reports it touched. A successful sync leaves nothing left to apply, so each capability must now read as already synced:
- ADDED requirements present
- MODIFIED requirements carrying the scenario and description changes named in the delta, with their other scenarios intact
- REMOVED requirements gone — and where this sync retired a capability (removed its last requirement, leaving `## Requirements` empty), its main spec deleted rather than left empty; a spec the sync deliberately kept and reported is also a match
Expand Down
15 changes: 15 additions & 0 deletions skills/openspec-sync-specs/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -95,6 +95,13 @@ This is an **agent-driven** operation - you will read delta specs and directly e

b. **Read the main spec** at `<planningHome.root>/openspec/specs/<capability-path>/spec.md` (may not exist yet)

**If it does not exist yet** (a new capability), match what `openspec archive` does:
only ADDED requirements may be applied - step d creates the spec from them.
MODIFIED and RENAMED have no requirement to act on, so stop the sync for that
capability and report that its main spec does not exist and only ADDED is allowed
for a new spec; never invent the missing requirement. REMOVED has nothing to
remove - skip it and warn.

c. **Apply changes intelligently**:

**ADDED Requirements:**
Expand Down Expand Up @@ -142,6 +149,14 @@ This is an **agent-driven** operation - you will read delta specs and directly e
(this is what `openspec archive` does; it warns and moves on)

d. **Create new main spec** if capability doesn't exist yet:
- Only when the delta has ADDED requirements to put in it and no MODIFIED or
RENAMED requirements blocked this capability in step b. Otherwise create nothing
and leave the specs directory untouched. For a REMOVED-only delta, if the change's
`.openspec.yaml` declares `retire_capabilities: true`, report it as already retired
and continue without recreating the spec. Without that marker, report the sync as blocked:
`openspec archive` rejects it with `Spec must have at least one requirement`.
An empty delta has no operations to sync; report it as blocked too.
Never write an empty `## Requirements` section.
- Create `<planningHome.root>/openspec/specs/<capability-path>/spec.md`
- Add Purpose section: copy the delta's `## Purpose` body verbatim when it has one
(this is what `openspec archive` does); only write a brief TBD placeholder when it does not
Expand Down
Loading
Loading