From 9f3dc0c6f11632d86675b556dcd7e356bd4cefff Mon Sep 17 00:00:00 2001 From: Clay Good Date: Wed, 19 Aug 2026 11:14:25 -0500 Subject: [PATCH 1/5] fix(workflows): create the main spec when a capability is new MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The agent-driven archive workflow told agents to "compare each delta spec with its corresponding main spec" and said nothing about the case where that main spec does not exist yet. Comparing against nothing reads as "already synced", so the agent took the archive branch and the new capability's main spec was never written — the change landed in changes/archive/ with openspec/specs/ still empty. `openspec archive` already handles this: buildUpdatedSpec creates the spec from the delta's ADDED requirements, rejects MODIFIED/RENAMED with "only ADDED requirements are allowed for new specs", and warns past REMOVED. The guidance now says the same thing, so the agent path and the CLI path agree: - archive-change: a missing main spec counts as changes needed and is named in the summary as a spec the sync will create — never as already synced. - sync-specs: MODIFIED and RENAMED have no requirement to act on when the main spec is absent, so the sync stops and reports rather than inventing one; REMOVED is skipped with a warning. Guidance text only — no CLI, parser, or archive behavior changes. Closes #1222 Closes #1264 Co-Authored-By: Claude Opus 5 --- .changeset/tidy-spiders-shave.md | 18 ++++++ openspec/specs/opsx-archive-skill/spec.md | 8 +++ openspec/specs/specs-sync-skill/spec.md | 7 +++ skills/openspec-archive-change/SKILL.md | 1 + skills/openspec-sync-specs/SKILL.md | 7 +++ .../templates/workflows/archive-change.ts | 2 + src/core/templates/workflows/sync-specs.ts | 14 +++++ .../templates/skill-templates-parity.test.ts | 63 +++++++++++++++++-- 8 files changed, 114 insertions(+), 6 deletions(-) create mode 100644 .changeset/tidy-spiders-shave.md diff --git a/.changeset/tidy-spiders-shave.md b/.changeset/tidy-spiders-shave.md new file mode 100644 index 0000000000..44a83420d4 --- /dev/null +++ b/.changeset/tidy-spiders-shave.md @@ -0,0 +1,18 @@ +--- +"@fission-ai/openspec": patch +--- + +fix(workflows): create the main spec when a capability is new + +The agent-driven archive workflow told agents to compare each delta spec with +its main spec, but said nothing about the case where that main spec does not +exist yet. Agents read "nothing to compare" as "already synced", archived the +change, and the capability's main spec was never written — the exact case +`openspec archive` handles by creating the spec from the delta's ADDED +requirements. The archive workflow now counts a missing main spec as changes +needed and names it as a spec the sync will create. + +The sync workflow gained the matching rule for the other half: MODIFIED and +RENAMED have no requirement to act on when the main spec does not exist, so it +stops and reports instead of inventing one, matching the CLI's +"only ADDED requirements are allowed for new specs" error. diff --git a/openspec/specs/opsx-archive-skill/spec.md b/openspec/specs/opsx-archive-skill/spec.md index 2c76461e54..90259b0217 100644 --- a/openspec/specs/opsx-archive-skill/spec.md +++ b/openspec/specs/opsx-archive-skill/spec.md @@ -82,6 +82,14 @@ 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: Delta spec whose main spec does not exist yet + +- **WHEN** agent compares a delta spec against its main spec at `openspec/specs//spec.md` +- **AND** that main spec does not exist yet +- **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 + #### Scenario: No delta specs - **WHEN** agent checks for delta specs diff --git a/openspec/specs/specs-sync-skill/spec.md b/openspec/specs/specs-sync-skill/spec.md index 3d14288802..a293caf477 100644 --- a/openspec/specs/specs-sync-skill/spec.md +++ b/openspec/specs/specs-sync-skill/spec.md @@ -75,6 +75,13 @@ The agent SHALL reconcile main specs with delta specs using the delta operation - **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: Merged main spec keeps canonical structure - **WHEN** the agent writes a main spec during sync - **THEN** every requirement lives under a single `## Requirements` section diff --git a/skills/openspec-archive-change/SKILL.md b/skills/openspec-archive-change/SKILL.md index 5f34ed53a7..42ca9146a9 100644 --- a/skills/openspec-archive-change/SKILL.md +++ b/skills/openspec-archive-change/SKILL.md @@ -94,6 +94,7 @@ Archive a completed change in the experimental workflow. **If delta specs exist:** - Compare each delta spec with its corresponding main spec at `/openspec/specs//spec.md` (use the store-aware `planningHome.root` from step 2, not a hardcoded repo path) + - A main spec that does not exist yet is **not** "already synced". For a new capability the main spec is an *output* of the sync, not an input: it gets created from the delta's ADDED requirements, exactly as `openspec archive` does. Count that capability as changes needed and name it in the summary (`: new main spec will be created`). Never read a missing main spec as nothing to apply. - Determine what changes would be applied (adds, modifications, removals, renames) - Show a combined summary before prompting diff --git a/skills/openspec-sync-specs/SKILL.md b/skills/openspec-sync-specs/SKILL.md index d12d56b857..7a532f6c89 100644 --- a/skills/openspec-sync-specs/SKILL.md +++ b/skills/openspec-sync-specs/SKILL.md @@ -95,6 +95,13 @@ This is an **agent-driven** operation - you will read delta specs and directly e b. **Read the main spec** at `/openspec/specs//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:** diff --git a/src/core/templates/workflows/archive-change.ts b/src/core/templates/workflows/archive-change.ts index 2dae74d436..cd238edcb8 100644 --- a/src/core/templates/workflows/archive-change.ts +++ b/src/core/templates/workflows/archive-change.ts @@ -96,6 +96,7 @@ ${STORE_SELECTION_GUIDANCE} **If delta specs exist:** - Compare each delta spec with its corresponding main spec at \`/openspec/specs//spec.md\` (use the store-aware \`planningHome.root\` from step 2, not a hardcoded repo path) + - A main spec that does not exist yet is **not** "already synced". For a new capability the main spec is an *output* of the sync, not an input: it gets created from the delta's ADDED requirements, exactly as \`openspec archive\` does. Count that capability as changes needed and name it in the summary (\`: new main spec will be created\`). Never read a missing main spec as nothing to apply. - Determine what changes would be applied (adds, modifications, removals, renames) - Show a combined summary before prompting @@ -278,6 +279,7 @@ ${STORE_SELECTION_GUIDANCE} **If delta specs exist:** - Compare each delta spec with its corresponding main spec at \`/openspec/specs//spec.md\` (use the store-aware \`planningHome.root\` from step 2, not a hardcoded repo path) + - A main spec that does not exist yet is **not** "already synced". For a new capability the main spec is an *output* of the sync, not an input: it gets created from the delta's ADDED requirements, exactly as \`openspec archive\` does. Count that capability as changes needed and name it in the summary (\`: new main spec will be created\`). Never read a missing main spec as nothing to apply. - Determine what changes would be applied (adds, modifications, removals, renames) - Show a combined summary before prompting diff --git a/src/core/templates/workflows/sync-specs.ts b/src/core/templates/workflows/sync-specs.ts index bedbaa7164..ca6ceddbcf 100644 --- a/src/core/templates/workflows/sync-specs.ts +++ b/src/core/templates/workflows/sync-specs.ts @@ -97,6 +97,13 @@ ${STORE_SELECTION_GUIDANCE} b. **Read the main spec** at \`/openspec/specs//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:** @@ -359,6 +366,13 @@ ${STORE_SELECTION_GUIDANCE} b. **Read the main spec** at \`/openspec/specs//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:** diff --git a/test/core/templates/skill-templates-parity.test.ts b/test/core/templates/skill-templates-parity.test.ts index decad7b0b5..713eca5a75 100644 --- a/test/core/templates/skill-templates-parity.test.ts +++ b/test/core/templates/skill-templates-parity.test.ts @@ -43,18 +43,18 @@ const EXPECTED_FUNCTION_HASHES: Record = { getContinueChangeSkillTemplate: '012136f6411a99c8fa228e2f9444cb64b0a89e0f56fdeac2fe03b2f5bee0c5d7', getApplyChangeSkillTemplate: 'd1e7d5ceb85193c0964057dbb88e9651526754bd33f84020e2440ff0621d5dbb', getFfChangeSkillTemplate: '5501740e7ec36ab23ab8c3a0d6dd0655a5e2f35433c7b90e82904fef5e7a326a', - getSyncSpecsSkillTemplate: 'b099e2ff31859c9b10d928066e662524f9aad9ecf2be12fceacb732d718c4146', + getSyncSpecsSkillTemplate: 'd09df989123f2e1fe3115910b449dda3cd423646365cac66851858a6099cd842', getOnboardSkillTemplate: '29b1d825179cff92fbc7b790694c1baef138575ea3de56848715e27d7e367946', getOpsxExploreCommandTemplate: 'd2f70d11588f902c15c1e5ce9908cc4124c6b82fe78dc766ac5c3599c9e2a6f1', getOpsxNewCommandTemplate: 'f2d30e569798a4c92ba932859d6ba4e0ad10e18feccbade1cfee0957597b3463', getOpsxContinueCommandTemplate: 'e50e50266efa1b8e64ff9b6274ee8254f0a240d6adc1b862d126e2f1c9d3a559', getOpsxApplyCommandTemplate: 'e3579ac78f2e2c75fa3d3a7ac7dc3e49c395e96f7323398f0f041d94f8de9bb0', getOpsxFfCommandTemplate: 'e603bc0996604e6c17a3140943ea642a32d0fc65565e25424bf956e124c55772', - getArchiveChangeSkillTemplate: '56bfada1a5f35a127791b70de9d428a75b5aedd1584d6c9803a1ecb1fd1b4a23', + getArchiveChangeSkillTemplate: 'fefb60130185c97761567f537ca7b91e53b4675c45634496f43545e0d614a825', getBulkArchiveChangeSkillTemplate: '93875998cade5322d95b43299fba794bc1da754e917dd63a770406386a6d295d', - getOpsxSyncCommandTemplate: '0d2427efb79986e8fff3f96bd075a739c80d45eb29159fae717e950030da8202', + getOpsxSyncCommandTemplate: '406cc9eb7818b8ba054ada2c67ce979bc312210b3e6c0070d1d7af7eef8ce8ea', getVerifyChangeSkillTemplate: '223b7ffd99299a7d430e13092b9a0a3421b39f0d3217232f46c39d79b5f619ff', - getOpsxArchiveCommandTemplate: '9f973c819b11620985b03322945f0e0a92a02a2ef455b94e74482f5e6292ac5d', + getOpsxArchiveCommandTemplate: 'a794e6c3da28a771e42c811b753e87ac6eff94865163545320e82c6398a626ee', getOpsxOnboardCommandTemplate: '7e251da66e2fdf539a09326463ee3ed0d01fe665ecb1d8f36f941fed00a01891', getOpsxBulkArchiveCommandTemplate: '9fa8cdebe2f5667ebfc37bdc023396762c59d5b038c771dac2d8fd2c19e2627b', getOpsxVerifyCommandTemplate: '1efcf7eff0671f48e9d9420f50865c563dd3079ee60f8c380bb7a90dd0102696', @@ -71,8 +71,8 @@ const EXPECTED_GENERATED_SKILL_CONTENT_HASHES: Record = { 'openspec-continue-change': 'bb6194a16c54891cdb253678e8f70ce53b2af86735243980f366ce551d37e42e', 'openspec-apply-change': '81ea96d9fa6ec8536cd23c1fe561ed28e1cc1cad0a8ceb700588e08974cc0e49', 'openspec-ff-change': '217c78da2b6e8358f609ac57dcd02266aaec3354ce26dc6ec2fc9c2174673ab4', - 'openspec-sync-specs': 'd933d8856584d6c1253de91e652e7aee9e85c77ad4d3531f6476f79d84e6e5e8', - 'openspec-archive-change': '7c65053d674ba4e1e20e2bf73ba7e5a7f94baef2eaa9b33cee48d4cadea51b7a', + 'openspec-sync-specs': '1ce9170b46b5205adb4a5abbb47c0eb96eb47fcf974a3a7b82b1ae999c06f25a', + 'openspec-archive-change': '698e8b45ab4ced57afaf96564488e595b3da6110d503c3f6780631ed69865e38', 'openspec-bulk-archive-change': '2039b9ecf6e64339dffe0e16272507a386d9fe326f419ff758315aa736fdd96c', 'openspec-verify-change': 'af9be013dcbe8c6d8f6d9ab10c893fbd03f4c62933c384d82f63894dd0ceb84f', 'openspec-onboard': 'd53403b4910ab64307862ccf97e70bd8f7174ee44508088fb239c880f0939331', @@ -447,6 +447,57 @@ describe('skill templates split parity', () => { } }); + it('treats a missing main spec as a sync that must run, not as already synced (#1222, #1264)', () => { + // `openspec archive` creates the main spec from the delta's ADDED requirements + // when it does not exist yet (`buildUpdatedSpec`, specs-apply.ts). The agent + // workflow only told the agent to "compare each delta spec with its + // corresponding main spec", so a capability with no main spec compared against + // nothing, read as "already synced", and the change archived with the spec + // never written. Assertions are scoped to the sync-assessment step so they + // cannot pass on unrelated text elsewhere in the body. + const archiveVariants: Array<[string, string]> = [ + ['archive skill', generateSkillContent(getArchiveChangeSkillTemplate(), 'PARITY-BASELINE')], + ['archive opsx command', getOpsxArchiveCommandTemplate().content], + ]; + + for (const [variant, content] of archiveVariants) { + const start = content.indexOf('**Assess delta spec sync state**'); + const end = content.indexOf('**Perform the archive**'); + expect(start, variant).toBeGreaterThan(-1); + expect(end, variant).toBeGreaterThan(start); + const assessStep = content.slice(start, end); + + expect(assessStep, variant).toContain( + 'A main spec that does not exist yet is **not** "already synced"' + ); + expect(assessStep, variant).toContain('is an *output* of the sync, not an input'); + expect(assessStep, variant).toContain("created from the delta's ADDED requirements"); + expect(assessStep, variant).toContain('Count that capability as changes needed'); + expect(assessStep, variant).toContain('Never read a missing main spec as nothing to apply'); + } + + // The sync itself must not invent a requirement that has no base to modify: + // the CLI throws "only ADDED requirements are allowed for new specs". + const syncVariants: Array<[string, string]> = [ + ['sync skill', getSyncSpecsSkillTemplate().instructions], + ['sync command', getOpsxSyncCommandTemplate().content], + ]; + + for (const [variant, content] of syncVariants) { + const start = content.indexOf('b. **Read the main spec**'); + const end = content.indexOf('c. **Apply changes intelligently**'); + expect(start, variant).toBeGreaterThan(-1); + expect(end, variant).toBeGreaterThan(start); + const readStep = content.slice(start, end); + + expect(readStep, variant).toContain('**If it does not exist yet** (a new capability)'); + expect(readStep, variant).toContain('only ADDED requirements may be applied'); + expect(readStep, variant).toContain('MODIFIED and RENAMED have no requirement to act on'); + expect(readStep, variant).toContain('never invent the missing requirement'); + expect(readStep, variant).toContain('REMOVED has nothing to'); + } + }); + it('gates bulk archive on inline synchronous spec sync and verification before moving change root', () => { const generatedSkill = generateSkillContent(getBulkArchiveChangeSkillTemplate(), 'PARITY-BASELINE'); const commandContent = getOpsxBulkArchiveCommandTemplate().content; From f55ba15e563cdd361438d3a86400a9a6ef2748e6 Mon Sep 17 00:00:00 2001 From: Clay Good Date: Wed, 19 Aug 2026 11:26:54 -0500 Subject: [PATCH 2/5] fix(sync): never create a main spec with nothing to put in it MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit CodeRabbit caught a gap in the previous commit: step 4b now tells the agent a REMOVED-only delta has nothing to remove, but step 4d still read as "create the main spec if the capability doesn't exist yet" unconditionally. Following both would write a spec whose `## Requirements` section is empty. Verified against the CLI on a REMOVED-only delta targeting a capability with no main spec: Specs to update: parking: create ⚠️ Warning: parking - 1 REMOVED requirement(s) ignored for new spec. Validation errors in rebuilt spec for parking (will not write changes): ✗ Spec must have at least one requirement Aborted. No files were changed. So step 4d is now gated on the delta having ADDED requirements to seed the spec with, and says what the CLI reports when it does not. Co-Authored-By: Claude Opus 5 --- openspec/specs/specs-sync-skill/spec.md | 7 ++++++ skills/openspec-sync-specs/SKILL.md | 5 +++++ src/core/templates/workflows/sync-specs.ts | 10 +++++++++ .../templates/skill-templates-parity.test.ts | 22 ++++++++++++++++--- 4 files changed, 41 insertions(+), 3 deletions(-) diff --git a/openspec/specs/specs-sync-skill/spec.md b/openspec/specs/specs-sync-skill/spec.md index a293caf477..34872ada57 100644 --- a/openspec/specs/specs-sync-skill/spec.md +++ b/openspec/specs/specs-sync-skill/spec.md @@ -82,6 +82,13 @@ The agent SHALL reconcile main specs with delta specs using the delta operation - **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** report it, matching `openspec archive`, which aborts the same case with `Spec must have at least one requirement` +- **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 diff --git a/skills/openspec-sync-specs/SKILL.md b/skills/openspec-sync-specs/SKILL.md index 7a532f6c89..9a254f463c 100644 --- a/skills/openspec-sync-specs/SKILL.md +++ b/skills/openspec-sync-specs/SKILL.md @@ -149,6 +149,11 @@ 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. With nothing to add - + a REMOVED-only delta against a capability that has no main spec - create nothing, + leave the specs directory untouched, and report it; `openspec archive` aborts the + same case with `Spec must have at least one requirement`. Never write an empty + `## Requirements` section. - Create `/openspec/specs//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 diff --git a/src/core/templates/workflows/sync-specs.ts b/src/core/templates/workflows/sync-specs.ts index ca6ceddbcf..0ac2fd497e 100644 --- a/src/core/templates/workflows/sync-specs.ts +++ b/src/core/templates/workflows/sync-specs.ts @@ -151,6 +151,11 @@ ${STORE_SELECTION_GUIDANCE} (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. With nothing to add - + a REMOVED-only delta against a capability that has no main spec - create nothing, + leave the specs directory untouched, and report it; \`openspec archive\` aborts the + same case with \`Spec must have at least one requirement\`. Never write an empty + \`## Requirements\` section. - Create \`/openspec/specs//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 @@ -420,6 +425,11 @@ ${STORE_SELECTION_GUIDANCE} (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. With nothing to add - + a REMOVED-only delta against a capability that has no main spec - create nothing, + leave the specs directory untouched, and report it; \`openspec archive\` aborts the + same case with \`Spec must have at least one requirement\`. Never write an empty + \`## Requirements\` section. - Create \`/openspec/specs//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 diff --git a/test/core/templates/skill-templates-parity.test.ts b/test/core/templates/skill-templates-parity.test.ts index 713eca5a75..f7abacc226 100644 --- a/test/core/templates/skill-templates-parity.test.ts +++ b/test/core/templates/skill-templates-parity.test.ts @@ -43,7 +43,7 @@ const EXPECTED_FUNCTION_HASHES: Record = { getContinueChangeSkillTemplate: '012136f6411a99c8fa228e2f9444cb64b0a89e0f56fdeac2fe03b2f5bee0c5d7', getApplyChangeSkillTemplate: 'd1e7d5ceb85193c0964057dbb88e9651526754bd33f84020e2440ff0621d5dbb', getFfChangeSkillTemplate: '5501740e7ec36ab23ab8c3a0d6dd0655a5e2f35433c7b90e82904fef5e7a326a', - getSyncSpecsSkillTemplate: 'd09df989123f2e1fe3115910b449dda3cd423646365cac66851858a6099cd842', + getSyncSpecsSkillTemplate: '5b6da3170398f6f8c2d6ab58e985d2771023e644e7d1bde74d2c29ea07f2b1bf', getOnboardSkillTemplate: '29b1d825179cff92fbc7b790694c1baef138575ea3de56848715e27d7e367946', getOpsxExploreCommandTemplate: 'd2f70d11588f902c15c1e5ce9908cc4124c6b82fe78dc766ac5c3599c9e2a6f1', getOpsxNewCommandTemplate: 'f2d30e569798a4c92ba932859d6ba4e0ad10e18feccbade1cfee0957597b3463', @@ -52,7 +52,7 @@ const EXPECTED_FUNCTION_HASHES: Record = { getOpsxFfCommandTemplate: 'e603bc0996604e6c17a3140943ea642a32d0fc65565e25424bf956e124c55772', getArchiveChangeSkillTemplate: 'fefb60130185c97761567f537ca7b91e53b4675c45634496f43545e0d614a825', getBulkArchiveChangeSkillTemplate: '93875998cade5322d95b43299fba794bc1da754e917dd63a770406386a6d295d', - getOpsxSyncCommandTemplate: '406cc9eb7818b8ba054ada2c67ce979bc312210b3e6c0070d1d7af7eef8ce8ea', + getOpsxSyncCommandTemplate: '382c724044c0532b5174b111cba220d20d4ff6519caad5b1849c1831530d3eb5', getVerifyChangeSkillTemplate: '223b7ffd99299a7d430e13092b9a0a3421b39f0d3217232f46c39d79b5f619ff', getOpsxArchiveCommandTemplate: 'a794e6c3da28a771e42c811b753e87ac6eff94865163545320e82c6398a626ee', getOpsxOnboardCommandTemplate: '7e251da66e2fdf539a09326463ee3ed0d01fe665ecb1d8f36f941fed00a01891', @@ -71,7 +71,7 @@ const EXPECTED_GENERATED_SKILL_CONTENT_HASHES: Record = { 'openspec-continue-change': 'bb6194a16c54891cdb253678e8f70ce53b2af86735243980f366ce551d37e42e', 'openspec-apply-change': '81ea96d9fa6ec8536cd23c1fe561ed28e1cc1cad0a8ceb700588e08974cc0e49', 'openspec-ff-change': '217c78da2b6e8358f609ac57dcd02266aaec3354ce26dc6ec2fc9c2174673ab4', - 'openspec-sync-specs': '1ce9170b46b5205adb4a5abbb47c0eb96eb47fcf974a3a7b82b1ae999c06f25a', + 'openspec-sync-specs': '5fcd207922b55a78b19fbc2872690c104ef83be09f1369de8b4b48778a234d75', 'openspec-archive-change': '698e8b45ab4ced57afaf96564488e595b3da6110d503c3f6780631ed69865e38', 'openspec-bulk-archive-change': '2039b9ecf6e64339dffe0e16272507a386d9fe326f419ff758315aa736fdd96c', 'openspec-verify-change': 'af9be013dcbe8c6d8f6d9ab10c893fbd03f4c62933c384d82f63894dd0ceb84f', @@ -495,6 +495,22 @@ describe('skill templates split parity', () => { expect(readStep, variant).toContain('MODIFIED and RENAMED have no requirement to act on'); expect(readStep, variant).toContain('never invent the missing requirement'); expect(readStep, variant).toContain('REMOVED has nothing to'); + + // ...and the creation step must not then write the empty spec the CLI refuses: + // a REMOVED-only delta against a capability with no main spec aborts with + // "Spec must have at least one requirement" and leaves the tree untouched. + const createStart = content.indexOf("d. **Create new main spec**"); + const createEnd = content.indexOf('**Validate updated main specs**'); + expect(createStart, variant).toBeGreaterThan(-1); + expect(createEnd, variant).toBeGreaterThan(createStart); + const createStep = content.slice(createStart, createEnd); + + expect(createStep, variant).toContain( + 'Only when the delta has ADDED requirements to put in it' + ); + expect(createStep, variant).toContain('create nothing'); + expect(createStep, variant).toContain('Spec must have at least one requirement'); + expect(createStep, variant).toContain('Never write an empty'); } }); From cce935bc03e1150b72762fe55ae8ca367f4fec58 Mon Sep 17 00:00:00 2001 From: Clay Good Date: Wed, 19 Aug 2026 12:06:48 -0500 Subject: [PATCH 3/5] docs: define "main spec" and close the no-ADDED archive loop MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Hardening pass over the two fixes in this branch. Guidance: the archive step's verification pass re-runs the same comparison the fix touched, so a delta that can create nothing — no ADDED requirements, no main spec to merge into — would have been reported as "still needs sync" after a sync that correctly created nothing, and an agent could loop on it. That case now short-circuits with the reason, matching `openspec archive`, which refuses it with "Spec must have at least one requirement". Docs: the glossary defined "delta spec" but never "main spec", which is half of #1647's terminology complaint. It now defines the term and says that for a new capability the main spec is created by the archive rather than written up front; concepts.md says the same in the delta-section table and the archive process. The docs site generates from docs/ at build time, so no website files change. Changeset rewritten in the house style (prose, no commit header; the changelog-github action supplies attribution) and renamed descriptively. All three CLI branches this guidance describes were verified end to end: ADDED against a greenfield repo creates the spec and carries its Purpose; MODIFIED reports "target spec does not exist; only ADDED requirements are allowed for new specs"; REMOVED-only aborts with "Spec must have at least one requirement" and writes nothing. Co-Authored-By: Claude Opus 5 --- .../create-main-spec-for-new-capability.md | 5 +++++ .changeset/tidy-spiders-shave.md | 18 ------------------ docs/concepts.md | 6 +++--- docs/glossary.md | 4 +++- openspec/specs/opsx-archive-skill/spec.md | 1 + skills/openspec-archive-change/SKILL.md | 2 +- src/core/templates/workflows/archive-change.ts | 4 ++-- .../templates/skill-templates-parity.test.ts | 17 ++++++++++++++--- 8 files changed, 29 insertions(+), 28 deletions(-) create mode 100644 .changeset/create-main-spec-for-new-capability.md delete mode 100644 .changeset/tidy-spiders-shave.md diff --git a/.changeset/create-main-spec-for-new-capability.md b/.changeset/create-main-spec-for-new-capability.md new file mode 100644 index 0000000000..1117475a7b --- /dev/null +++ b/.changeset/create-main-spec-for-new-capability.md @@ -0,0 +1,5 @@ +--- +"@fission-ai/openspec": patch +--- + +The agent-driven archive and sync workflows now create a capability's main spec when it does not exist yet. The archive workflow told agents to compare each delta spec against its main spec but said nothing about a main spec that is not there; comparing against nothing read as "already synced", so the agent archived the change and the new capability's main spec was never written — in a greenfield project every change archived with `openspec/specs/` still empty. A missing main spec now counts as work the sync must do and is named in the summary as a spec that will be created. The sync workflow gained the matching rules `openspec archive` has always enforced: `MODIFIED` and `RENAMED` have no requirement to act on when there is no main spec, so the sync stops and reports instead of inventing one, and a `REMOVED`-only delta creates nothing rather than writing a spec with an empty `## Requirements` section. The docs now define "main spec" and state that for a new capability it is created by the archive rather than written up front. Fixes #1222. Fixes #1264. diff --git a/.changeset/tidy-spiders-shave.md b/.changeset/tidy-spiders-shave.md deleted file mode 100644 index 44a83420d4..0000000000 --- a/.changeset/tidy-spiders-shave.md +++ /dev/null @@ -1,18 +0,0 @@ ---- -"@fission-ai/openspec": patch ---- - -fix(workflows): create the main spec when a capability is new - -The agent-driven archive workflow told agents to compare each delta spec with -its main spec, but said nothing about the case where that main spec does not -exist yet. Agents read "nothing to compare" as "already synced", archived the -change, and the capability's main spec was never written — the exact case -`openspec archive` handles by creating the spec from the delta's ADDED -requirements. The archive workflow now counts a missing main spec as changes -needed and names it as a spec the sync will create. - -The sync workflow gained the matching rule for the other half: MODIFIED and -RENAMED have no requirement to act on when the main spec does not exist, so it -stops and reports instead of inventing one, matching the CLI's -"only ADDED requirements are allowed for new specs" error. diff --git a/docs/concepts.md b/docs/concepts.md index 10106c5b78..23665ec37a 100644 --- a/docs/concepts.md +++ b/docs/concepts.md @@ -390,8 +390,8 @@ The system MUST expire sessions after 15 minutes of inactivity. | Section | Meaning | What Happens on Archive | |---------|---------|------------------------| -| `## ADDED Requirements` | New behavior | Appended to main spec | -| `## MODIFIED Requirements` | Changed behavior | Replaces existing requirement | +| `## ADDED Requirements` | New behavior | Appended to the main spec, which is created when the capability is new | +| `## MODIFIED Requirements` | Changed behavior | Replaces existing requirement; rejected when the capability has no main spec yet | | `## REMOVED Requirements` | Deprecated behavior | Deleted from main spec; removing the last requirement retires the capability and deletes its spec file, when the change declares `retire_capabilities: true` | | `## Purpose` | What a brand-new capability is for | Seeds the Purpose of the main spec being created; ignored when the spec already exists | @@ -542,7 +542,7 @@ openspec/ ### The Archive Process -1. **Merge deltas.** Each delta spec section (ADDED/MODIFIED/REMOVED) is applied to the corresponding main spec. +1. **Merge deltas.** Each delta spec section (ADDED/MODIFIED/REMOVED) is applied to the corresponding main spec. When the change introduces a capability that has no main spec yet, the archive creates it from the delta's ADDED requirements — the main spec is an output of this step, not something that must already exist. 2. **Move to archive.** The change folder moves to `changes/archive/` with a date prefix for chronological ordering. diff --git a/docs/glossary.md b/docs/glossary.md index 345125f38a..daa590d599 100644 --- a/docs/glossary.md +++ b/docs/glossary.md @@ -16,6 +16,8 @@ Terms are grouped by topic, then alphabetized within each group. **Delta spec.** A spec inside a change that describes only what's changing, using `ADDED`, `MODIFIED`, and `REMOVED` sections, rather than restating the entire spec. This is what lets OpenSpec edit existing systems cleanly. See [Concepts](concepts.md#delta-specs). +**Main spec.** The spec in `openspec/specs/` that a delta spec edits — the durable, current-state counterpart to a change's delta. For a capability that already exists, the main spec is the file the delta merges into. For a brand-new capability, there is nothing to merge into yet: the main spec is *created* by the archive from the delta's `ADDED` requirements. It is an output of finishing a change, not something you write up front. + **Domain.** A logical grouping for specs, like `auth/`, `payments/`, or `ui/`. You choose domains that match how you think about your system. ## Inside a spec @@ -38,7 +40,7 @@ Terms are grouped by topic, then alphabetized within each group. **Archive.** The act of finishing a change. Its delta specs merge into the main specs, and the change folder moves to `openspec/changes/archive/YYYY-MM-DD-/`. After archiving, your specs describe the new reality. See [Concepts](concepts.md#archive). -**Sync.** Merging a change's delta specs into the main specs *without* archiving the change. Usually automatic (archive offers to do it), but available on its own as `/opsx:sync` for long-running changes. See [Commands](commands.md#opsxsync). +**Sync.** Merging a change's delta specs into the main specs *without* archiving the change — creating a main spec from scratch when the change introduces a capability that has none yet. Usually automatic (archive offers to do it), but available on its own as `/opsx:sync` for long-running changes. See [Commands](commands.md#opsxsync). ## Workflow and commands diff --git a/openspec/specs/opsx-archive-skill/spec.md b/openspec/specs/opsx-archive-skill/spec.md index 90259b0217..d56ce5905f 100644 --- a/openspec/specs/opsx-archive-skill/spec.md +++ b/openspec/specs/opsx-archive-skill/spec.md @@ -89,6 +89,7 @@ The skill SHALL prompt to sync delta specs before archiving if specs exist. - **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** when that delta has no `## ADDED Requirements` to seed the new spec with, report that nothing can be created and stop instead of prompting to sync, since the verification pass would re-read the same missing spec #### Scenario: No delta specs diff --git a/skills/openspec-archive-change/SKILL.md b/skills/openspec-archive-change/SKILL.md index 42ca9146a9..ef6bea90cd 100644 --- a/skills/openspec-archive-change/SKILL.md +++ b/skills/openspec-archive-change/SKILL.md @@ -94,7 +94,7 @@ Archive a completed change in the experimental workflow. **If delta specs exist:** - Compare each delta spec with its corresponding main spec at `/openspec/specs//spec.md` (use the store-aware `planningHome.root` from step 2, not a hardcoded repo path) - - A main spec that does not exist yet is **not** "already synced". For a new capability the main spec is an *output* of the sync, not an input: it gets created from the delta's ADDED requirements, exactly as `openspec archive` does. Count that capability as changes needed and name it in the summary (`: new main spec will be created`). Never read a missing main spec as nothing to apply. + - A main spec that does not exist yet is **not** "already synced". For a new capability the main spec is an *output* of the sync, not an input: it gets created from the delta's ADDED requirements, exactly as `openspec archive` does. Count that capability as changes needed and name it in the summary (`: new main spec will be created`). Never read a missing main spec as nothing to apply. The one case a sync cannot fix is a delta with no ADDED requirements to seed the new spec with: nothing can be created, so report that and stop rather than prompting to sync and re-checking the same missing spec - `openspec archive` refuses it too, with `Spec must have at least one requirement`. - Determine what changes would be applied (adds, modifications, removals, renames) - Show a combined summary before prompting diff --git a/src/core/templates/workflows/archive-change.ts b/src/core/templates/workflows/archive-change.ts index cd238edcb8..2fa7b5ca01 100644 --- a/src/core/templates/workflows/archive-change.ts +++ b/src/core/templates/workflows/archive-change.ts @@ -96,7 +96,7 @@ ${STORE_SELECTION_GUIDANCE} **If delta specs exist:** - Compare each delta spec with its corresponding main spec at \`/openspec/specs//spec.md\` (use the store-aware \`planningHome.root\` from step 2, not a hardcoded repo path) - - A main spec that does not exist yet is **not** "already synced". For a new capability the main spec is an *output* of the sync, not an input: it gets created from the delta's ADDED requirements, exactly as \`openspec archive\` does. Count that capability as changes needed and name it in the summary (\`: new main spec will be created\`). Never read a missing main spec as nothing to apply. + - A main spec that does not exist yet is **not** "already synced". For a new capability the main spec is an *output* of the sync, not an input: it gets created from the delta's ADDED requirements, exactly as \`openspec archive\` does. Count that capability as changes needed and name it in the summary (\`: new main spec will be created\`). Never read a missing main spec as nothing to apply. The one case a sync cannot fix is a delta with no ADDED requirements to seed the new spec with: nothing can be created, so report that and stop rather than prompting to sync and re-checking the same missing spec - \`openspec archive\` refuses it too, with \`Spec must have at least one requirement\`. - Determine what changes would be applied (adds, modifications, removals, renames) - Show a combined summary before prompting @@ -279,7 +279,7 @@ ${STORE_SELECTION_GUIDANCE} **If delta specs exist:** - Compare each delta spec with its corresponding main spec at \`/openspec/specs//spec.md\` (use the store-aware \`planningHome.root\` from step 2, not a hardcoded repo path) - - A main spec that does not exist yet is **not** "already synced". For a new capability the main spec is an *output* of the sync, not an input: it gets created from the delta's ADDED requirements, exactly as \`openspec archive\` does. Count that capability as changes needed and name it in the summary (\`: new main spec will be created\`). Never read a missing main spec as nothing to apply. + - A main spec that does not exist yet is **not** "already synced". For a new capability the main spec is an *output* of the sync, not an input: it gets created from the delta's ADDED requirements, exactly as \`openspec archive\` does. Count that capability as changes needed and name it in the summary (\`: new main spec will be created\`). Never read a missing main spec as nothing to apply. The one case a sync cannot fix is a delta with no ADDED requirements to seed the new spec with: nothing can be created, so report that and stop rather than prompting to sync and re-checking the same missing spec - \`openspec archive\` refuses it too, with \`Spec must have at least one requirement\`. - Determine what changes would be applied (adds, modifications, removals, renames) - Show a combined summary before prompting diff --git a/test/core/templates/skill-templates-parity.test.ts b/test/core/templates/skill-templates-parity.test.ts index f7abacc226..055d28c808 100644 --- a/test/core/templates/skill-templates-parity.test.ts +++ b/test/core/templates/skill-templates-parity.test.ts @@ -50,11 +50,11 @@ const EXPECTED_FUNCTION_HASHES: Record = { getOpsxContinueCommandTemplate: 'e50e50266efa1b8e64ff9b6274ee8254f0a240d6adc1b862d126e2f1c9d3a559', getOpsxApplyCommandTemplate: 'e3579ac78f2e2c75fa3d3a7ac7dc3e49c395e96f7323398f0f041d94f8de9bb0', getOpsxFfCommandTemplate: 'e603bc0996604e6c17a3140943ea642a32d0fc65565e25424bf956e124c55772', - getArchiveChangeSkillTemplate: 'fefb60130185c97761567f537ca7b91e53b4675c45634496f43545e0d614a825', + getArchiveChangeSkillTemplate: '67ec0a3bbecaf583abe1a0827f62484e2a0d0e5b271107f8f673d921f29b17ff', getBulkArchiveChangeSkillTemplate: '93875998cade5322d95b43299fba794bc1da754e917dd63a770406386a6d295d', getOpsxSyncCommandTemplate: '382c724044c0532b5174b111cba220d20d4ff6519caad5b1849c1831530d3eb5', getVerifyChangeSkillTemplate: '223b7ffd99299a7d430e13092b9a0a3421b39f0d3217232f46c39d79b5f619ff', - getOpsxArchiveCommandTemplate: 'a794e6c3da28a771e42c811b753e87ac6eff94865163545320e82c6398a626ee', + getOpsxArchiveCommandTemplate: '5502c72166d36b506ce88de27b1ab4d8706ac44f5e0f3d222f028f08f6a0b9f6', getOpsxOnboardCommandTemplate: '7e251da66e2fdf539a09326463ee3ed0d01fe665ecb1d8f36f941fed00a01891', getOpsxBulkArchiveCommandTemplate: '9fa8cdebe2f5667ebfc37bdc023396762c59d5b038c771dac2d8fd2c19e2627b', getOpsxVerifyCommandTemplate: '1efcf7eff0671f48e9d9420f50865c563dd3079ee60f8c380bb7a90dd0102696', @@ -72,7 +72,7 @@ const EXPECTED_GENERATED_SKILL_CONTENT_HASHES: Record = { 'openspec-apply-change': '81ea96d9fa6ec8536cd23c1fe561ed28e1cc1cad0a8ceb700588e08974cc0e49', 'openspec-ff-change': '217c78da2b6e8358f609ac57dcd02266aaec3354ce26dc6ec2fc9c2174673ab4', 'openspec-sync-specs': '5fcd207922b55a78b19fbc2872690c104ef83be09f1369de8b4b48778a234d75', - 'openspec-archive-change': '698e8b45ab4ced57afaf96564488e595b3da6110d503c3f6780631ed69865e38', + 'openspec-archive-change': '2e8e6fbdcfd1e7c7305b89ebdaea5ffd0d3feb0480acd36a6df617f32a9c85f1', 'openspec-bulk-archive-change': '2039b9ecf6e64339dffe0e16272507a386d9fe326f419ff758315aa736fdd96c', 'openspec-verify-change': 'af9be013dcbe8c6d8f6d9ab10c893fbd03f4c62933c384d82f63894dd0ceb84f', 'openspec-onboard': 'd53403b4910ab64307862ccf97e70bd8f7174ee44508088fb239c880f0939331', @@ -474,6 +474,17 @@ describe('skill templates split parity', () => { expect(assessStep, variant).toContain("created from the delta's ADDED requirements"); expect(assessStep, variant).toContain('Count that capability as changes needed'); expect(assessStep, variant).toContain('Never read a missing main spec as nothing to apply'); + + // The verification pass re-runs this same comparison, so a delta that can + // create nothing must short-circuit here instead of prompting to sync and + // re-checking the still-missing spec forever. + expect(assessStep, variant).toContain( + 'The one case a sync cannot fix is a delta with no ADDED requirements' + ); + expect(assessStep, variant).toContain( + 'report that and stop rather than prompting to sync' + ); + expect(assessStep, variant).toContain('Spec must have at least one requirement'); } // The sync itself must not invent a requirement that has no base to modify: From e3577c375b5907e2bd364b6af29ba652462ef6b6 Mon Sep 17 00:00:00 2001 From: Clay Good Date: Wed, 19 Aug 2026 12:15:47 -0500 Subject: [PATCH 4/5] docs(glossary): a standalone sync creates the main spec too MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The "Main spec" entry said the spec is created "by the archive", but the "Sync" entry two sections down says /opsx:sync creates it as well, without archiving — and specs-sync-skill's "New capability spec" scenario is the sync's own behavior. Names both paths so the two entries agree. Co-Authored-By: Claude Opus 5 --- docs/glossary.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/glossary.md b/docs/glossary.md index daa590d599..3c9c26ed4b 100644 --- a/docs/glossary.md +++ b/docs/glossary.md @@ -16,7 +16,7 @@ Terms are grouped by topic, then alphabetized within each group. **Delta spec.** A spec inside a change that describes only what's changing, using `ADDED`, `MODIFIED`, and `REMOVED` sections, rather than restating the entire spec. This is what lets OpenSpec edit existing systems cleanly. See [Concepts](concepts.md#delta-specs). -**Main spec.** The spec in `openspec/specs/` that a delta spec edits — the durable, current-state counterpart to a change's delta. For a capability that already exists, the main spec is the file the delta merges into. For a brand-new capability, there is nothing to merge into yet: the main spec is *created* by the archive from the delta's `ADDED` requirements. It is an output of finishing a change, not something you write up front. +**Main spec.** The spec in `openspec/specs/` that a delta spec edits — the durable, current-state counterpart to a change's delta. For a capability that already exists, the main spec is the file the delta merges into. For a brand-new capability, there is nothing to merge into yet: the main spec is *created* from the delta's `ADDED` requirements — by the archive, or by a standalone sync before you archive. It is an output of the work, not something you write up front. **Domain.** A logical grouping for specs, like `auth/`, `payments/`, or `ui/`. You choose domains that match how you think about your system. From da4d4c837c757fb18691b1f31481a53f5b86f3a9 Mon Sep 17 00:00:00 2001 From: Clay Good Date: Mon, 24 Aug 2026 09:27:27 -0500 Subject: [PATCH 5/5] fix(workflows): clarify removed deltas for missing specs --- openspec/specs/opsx-archive-skill/spec.md | 5 ++++- skills/openspec-archive-change/SKILL.md | 4 ++-- src/core/templates/workflows/archive-change.ts | 8 ++++---- .../core/templates/skill-templates-parity.test.ts | 15 +++++++++++---- 4 files changed, 21 insertions(+), 11 deletions(-) diff --git a/openspec/specs/opsx-archive-skill/spec.md b/openspec/specs/opsx-archive-skill/spec.md index 0acf86ab42..17149960a3 100644 --- a/openspec/specs/opsx-archive-skill/spec.md +++ b/openspec/specs/opsx-archive-skill/spec.md @@ -91,6 +91,8 @@ The skill SHALL prompt to sync delta specs before archiving if specs exist. - **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 @@ -103,7 +105,8 @@ The skill SHALL prompt to sync delta specs before archiving if specs exist. - **WHEN** a delta targets a capability whose main spec does not exist yet - **AND** the delta has no `## ADDED Requirements` -- **THEN** report that nothing can be created +- **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** stop instead of prompting to sync, since the verification pass would re-read the same missing spec #### Scenario: No delta specs diff --git a/skills/openspec-archive-change/SKILL.md b/skills/openspec-archive-change/SKILL.md index b550c6e960..91072e6de3 100644 --- a/skills/openspec-archive-change/SKILL.md +++ b/skills/openspec-archive-change/SKILL.md @@ -96,8 +96,8 @@ Archive a completed change in the experimental workflow. - Compare each delta spec with its corresponding main spec at `/openspec/specs//spec.md` (use the store-aware `planningHome.root` from step 2, not a hardcoded repo path) - A main spec that does not exist yet is **not** "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 stop instead of prompting to sync. Never invent a requirement that has no current version. - - If the delta has no ADDED requirements, report that nothing can be created and stop instead of prompting to sync. `openspec archive` refuses the same case with `Spec must have at least one requirement`. - - Otherwise, count the capability as needing sync and name it in the summary (`: new main spec will be created`). The sync creates it from the delta's ADDED requirements, exactly as `openspec archive` does. + - If the delta has no ADDED requirements, report that no sync is possible and stop instead of prompting to sync. 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 same case with `Spec must have at least one requirement`. + - Otherwise, count the capability as needing sync and name it in the summary (`: 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 diff --git a/src/core/templates/workflows/archive-change.ts b/src/core/templates/workflows/archive-change.ts index 4eb49a953d..b4b2e79951 100644 --- a/src/core/templates/workflows/archive-change.ts +++ b/src/core/templates/workflows/archive-change.ts @@ -98,8 +98,8 @@ ${STORE_SELECTION_GUIDANCE} - Compare each delta spec with its corresponding main spec at \`/openspec/specs//spec.md\` (use the store-aware \`planningHome.root\` from step 2, not a hardcoded repo path) - A main spec that does not exist yet is **not** "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 stop instead of prompting to sync. Never invent a requirement that has no current version. - - If the delta has no ADDED requirements, report that nothing can be created and stop instead of prompting to sync. \`openspec archive\` refuses the same case with \`Spec must have at least one requirement\`. - - Otherwise, count the capability as needing sync and name it in the summary (\`: new main spec will be created\`). The sync creates it from the delta's ADDED requirements, exactly as \`openspec archive\` does. + - If the delta has no ADDED requirements, report that no sync is possible and stop instead of prompting to sync. 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 same case with \`Spec must have at least one requirement\`. + - Otherwise, count the capability as needing sync and name it in the summary (\`: 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 @@ -284,8 +284,8 @@ ${STORE_SELECTION_GUIDANCE} - Compare each delta spec with its corresponding main spec at \`/openspec/specs//spec.md\` (use the store-aware \`planningHome.root\` from step 2, not a hardcoded repo path) - A main spec that does not exist yet is **not** "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 stop instead of prompting to sync. Never invent a requirement that has no current version. - - If the delta has no ADDED requirements, report that nothing can be created and stop instead of prompting to sync. \`openspec archive\` refuses the same case with \`Spec must have at least one requirement\`. - - Otherwise, count the capability as needing sync and name it in the summary (\`: new main spec will be created\`). The sync creates it from the delta's ADDED requirements, exactly as \`openspec archive\` does. + - If the delta has no ADDED requirements, report that no sync is possible and stop instead of prompting to sync. 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 same case with \`Spec must have at least one requirement\`. + - Otherwise, count the capability as needing sync and name it in the summary (\`: 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 diff --git a/test/core/templates/skill-templates-parity.test.ts b/test/core/templates/skill-templates-parity.test.ts index d524329cdd..0d8fd8a21c 100644 --- a/test/core/templates/skill-templates-parity.test.ts +++ b/test/core/templates/skill-templates-parity.test.ts @@ -50,11 +50,11 @@ const EXPECTED_FUNCTION_HASHES: Record = { getOpsxContinueCommandTemplate: 'e50e50266efa1b8e64ff9b6274ee8254f0a240d6adc1b862d126e2f1c9d3a559', getOpsxApplyCommandTemplate: 'e3579ac78f2e2c75fa3d3a7ac7dc3e49c395e96f7323398f0f041d94f8de9bb0', getOpsxFfCommandTemplate: 'e603bc0996604e6c17a3140943ea642a32d0fc65565e25424bf956e124c55772', - getArchiveChangeSkillTemplate: 'ab9f73a319c73bf66168883ed766e97748460d41599eb8b6f54df4f404885624', + getArchiveChangeSkillTemplate: '5eac76fa2e9ae19136b8d4246bb5cfe4e8ac7925f2e253a93c86856b15ce9f16', getBulkArchiveChangeSkillTemplate: '93875998cade5322d95b43299fba794bc1da754e917dd63a770406386a6d295d', getOpsxSyncCommandTemplate: '382c724044c0532b5174b111cba220d20d4ff6519caad5b1849c1831530d3eb5', getVerifyChangeSkillTemplate: '223b7ffd99299a7d430e13092b9a0a3421b39f0d3217232f46c39d79b5f619ff', - getOpsxArchiveCommandTemplate: 'fced8b751cbe0654ed56f34c167b627a6113b9353b24bc424e9154549e06a82a', + getOpsxArchiveCommandTemplate: '5e754505e7baf80f8c6959a6fa5cacfabc6b50f8bf5ec94428e82095abf5d361', getOpsxOnboardCommandTemplate: 'ee99aa99252c602720fbb8c63fb3ac438a5bd4e952fd961ddf1ae956cbfc2c8f', getOpsxBulkArchiveCommandTemplate: '9fa8cdebe2f5667ebfc37bdc023396762c59d5b038c771dac2d8fd2c19e2627b', getOpsxVerifyCommandTemplate: '1efcf7eff0671f48e9d9420f50865c563dd3079ee60f8c380bb7a90dd0102696', @@ -72,7 +72,7 @@ const EXPECTED_GENERATED_SKILL_CONTENT_HASHES: Record = { 'openspec-apply-change': '81ea96d9fa6ec8536cd23c1fe561ed28e1cc1cad0a8ceb700588e08974cc0e49', 'openspec-ff-change': '217c78da2b6e8358f609ac57dcd02266aaec3354ce26dc6ec2fc9c2174673ab4', 'openspec-sync-specs': '5fcd207922b55a78b19fbc2872690c104ef83be09f1369de8b4b48778a234d75', - 'openspec-archive-change': 'f562fb03ce265fed7c27cbf73d452e12db67a963314820a5f403328b1651809a', + 'openspec-archive-change': '238c6e04cca29ab8b8635bafd39d836b40f8290ccec110a2e6ed1bf8618773bf', 'openspec-bulk-archive-change': '2039b9ecf6e64339dffe0e16272507a386d9fe326f419ff758315aa736fdd96c', 'openspec-verify-change': 'af9be013dcbe8c6d8f6d9ab10c893fbd03f4c62933c384d82f63894dd0ceb84f', 'openspec-onboard': 'f6f59476acaf5e4d65dbb180da4cef62432612f3cecf207d471a951295e2003a', @@ -509,10 +509,17 @@ describe('skill templates split parity', () => { expect(assessStep, variant).toContain('only ADDED requirements can create'); expect(assessStep, variant).toContain('Never invent a requirement'); expect(assessStep, variant).toContain('If the delta has no ADDED requirements'); + expect(assessStep, variant).toContain('report that no sync is possible'); + expect(assessStep, variant).toContain('For a REMOVED-only delta'); + expect(assessStep, variant).toContain('leave the main-spec tree unchanged'); expect(assessStep, variant).toContain('stop instead of prompting to sync'); expect(assessStep, variant).toContain('Spec must have at least one requirement'); expect(assessStep, variant).toContain('Otherwise, count the capability as needing sync'); - expect(assessStep, variant).toContain("creates it from the delta's ADDED requirements"); + expect(assessStep, variant).toContain('If the delta also has REMOVED requirements'); + expect(assessStep, variant).toContain('warn that they will be ignored'); + expect(assessStep, variant).toContain( + "creates the main spec from only the delta's ADDED requirements" + ); } // The sync itself must not invent a requirement that has no base to modify: