Skip to content
Merged
Show file tree
Hide file tree
Changes from 3 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
7 changes: 7 additions & 0 deletions .changeset/archive-waits-for-spec-sync.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
---
"@fission-ai/openspec": patch
---

### Bug Fixes

- **Archive no longer races the spec sync** — the generated `openspec-archive-change` skill (and the matching `opsx:archive` command) handed the spec sync to a subagent and then moved the change folder immediately. Subagents run in the background by default, so the archive could move the delta specs out from under the running sync: the change ended up archived, `openspec/specs/` was never updated, and the summary still reported `Specs: ✓ Synced`. The sync now runs inline, and the archive only proceeds once every touched capability has been re-checked against its delta — ADDED and MODIFIED requirements present, REMOVED gone, RENAMED under the new name. If the sync fails or a capability does not match, the archive stops instead of reporting success.
7 changes: 5 additions & 2 deletions openspec/specs/opsx-archive-skill/spec.md
Original file line number Diff line number Diff line change
Expand Up @@ -74,8 +74,11 @@ The skill SHALL prompt to sync delta specs before archiving if specs exist.
- **WHEN** agent checks for delta specs
- **AND** `specs/` directory exists in the change with spec files
- **THEN** prompt user: "This change has delta specs. Would you like to sync them to main specs before archiving?"
- **AND** if user confirms, execute `/opsx:sync` logic
- **AND** proceed with archive regardless of sync choice
- **AND** if user cancels, stop without archiving
- **AND** if user confirms, execute `/opsx:sync` logic inline and wait for it to complete
- **AND** verify each affected capability against its delta: ADDED and MODIFIED requirements present, REMOVED requirements absent, RENAMED requirements under their new name
- **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

#### Scenario: No delta specs

Expand Down
13 changes: 10 additions & 3 deletions skills/openspec-archive-change/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -66,7 +66,13 @@ Archive a completed change in the experimental workflow.
- If changes needed: "Sync now (recommended)", "Archive without syncing"
- If already synced: "Archive now", "Sync anyway", "Cancel"

If user chooses sync, use Task tool (subagent_type: "general-purpose", prompt: "Use Skill tool to invoke openspec-sync-specs for change '<name>'. Delta spec analysis: <include the analyzed delta spec summary>"). If the user chooses "Cancel", stop — do not archive. For any other choice, proceed to archive.
If the user chooses "Cancel", stop — do not archive.

If the user chooses sync, run it inline: use the Skill tool to invoke `openspec-sync-specs` for change '<name>', passing the delta spec analysis from above. Do not hand the sync to a background subagent — 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 harness can only run this as a subagent, dispatch it synchronously — for example `run_in_background: false` — and wait for its result.)

When the sync finishes, re-check every capability it touched at `<planningHome.root>/openspec/specs/<capability>/spec.md`: ADDED and MODIFIED requirements present, REMOVED requirements gone, RENAMED requirements under their new name. If the sync failed, or any capability does not match its delta, report it and stop — do not archive.

For any other choice, proceed to archive.

5. **Perform the archive**

Expand Down Expand Up @@ -102,7 +108,7 @@ Archive a completed change in the experimental workflow.
**Change:** <change-name>
**Schema:** <schema-name>
**Archived to:** the archive path derived from `planningHome.changesDir`/YYYY-MM-DD-<name>/
**Specs:** ✓ Synced to main specs (or "No delta specs" or "Sync skipped")
**Specs:** ✓ Synced to main specs — only if the step 4 verification passed (otherwise "No delta specs" or "Sync skipped")

<"All artifacts complete. All tasks complete." — or, if archived with warnings, list them instead (e.g. "Archived with 2 incomplete tasks")>
```
Expand All @@ -113,5 +119,6 @@ Archive a completed change in the experimental workflow.
- Don't block archive on warnings - just inform and confirm
- Preserve .openspec.yaml when moving to archive (it moves with the directory)
- Show clear summary of what happened
- If sync is requested, use openspec-sync-specs approach (agent-driven)
- If sync is requested, use the Skill tool to invoke `openspec-sync-specs` (inline, agent-driven)
- Never archive while a spec sync is still in flight — run the sync inline and verify the main specs before moving `changeRoot`
- If delta specs exist, always run the sync assessment and show the combined summary before prompting
24 changes: 19 additions & 5 deletions src/core/templates/workflows/archive-change.ts
Original file line number Diff line number Diff line change
Expand Up @@ -68,7 +68,13 @@ ${STORE_SELECTION_GUIDANCE}
- If changes needed: "Sync now (recommended)", "Archive without syncing"
- If already synced: "Archive now", "Sync anyway", "Cancel"

If user chooses sync, use Task tool (subagent_type: "general-purpose", prompt: "Use Skill tool to invoke openspec-sync-specs for change '<name>'. Delta spec analysis: <include the analyzed delta spec summary>"). If the user chooses "Cancel", stop — do not archive. For any other choice, proceed to archive.
If the user chooses "Cancel", stop — do not archive.

If the user chooses sync, run it inline: use the Skill tool to invoke \`openspec-sync-specs\` for change '<name>', passing the delta spec analysis from above. Do not hand the sync to a background subagent — 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 harness can only run this as a subagent, dispatch it synchronously — for example \`run_in_background: false\` — and wait for its result.)

When the sync finishes, re-check every capability it touched at \`<planningHome.root>/openspec/specs/<capability>/spec.md\`: ADDED and MODIFIED requirements present, REMOVED requirements gone, RENAMED requirements under their new name. If the sync failed, or any capability does not match its delta, report it and stop — do not archive.

For any other choice, proceed to archive.

5. **Perform the archive**

Expand Down Expand Up @@ -104,7 +110,7 @@ ${STORE_SELECTION_GUIDANCE}
**Change:** <change-name>
**Schema:** <schema-name>
**Archived to:** the archive path derived from \`planningHome.changesDir\`/YYYY-MM-DD-<name>/
**Specs:** ✓ Synced to main specs (or "No delta specs" or "Sync skipped")
**Specs:** ✓ Synced to main specs — only if the step 4 verification passed (otherwise "No delta specs" or "Sync skipped")

<"All artifacts complete. All tasks complete." — or, if archived with warnings, list them instead (e.g. "Archived with 2 incomplete tasks")>
\`\`\`
Expand All @@ -115,7 +121,8 @@ ${STORE_SELECTION_GUIDANCE}
- Don't block archive on warnings - just inform and confirm
- Preserve .openspec.yaml when moving to archive (it moves with the directory)
- Show clear summary of what happened
- If sync is requested, use openspec-sync-specs approach (agent-driven)
- If sync is requested, use the Skill tool to invoke \`openspec-sync-specs\` (inline, agent-driven)
- Never archive while a spec sync is still in flight — run the sync inline and verify the main specs before moving \`changeRoot\`
- If delta specs exist, always run the sync assessment and show the combined summary before prompting`,
license: 'MIT',
compatibility: 'Requires openspec CLI.',
Expand Down Expand Up @@ -186,7 +193,13 @@ ${STORE_SELECTION_GUIDANCE}
- If changes needed: "Sync now (recommended)", "Archive without syncing"
- If already synced: "Archive now", "Sync anyway", "Cancel"

If user chooses sync, use Task tool (subagent_type: "general-purpose", prompt: "Use Skill tool to invoke openspec-sync-specs for change '<name>'. Delta spec analysis: <include the analyzed delta spec summary>"). If the user chooses "Cancel", stop — do not archive. For any other choice, proceed to archive.
If the user chooses "Cancel", stop — do not archive.

If the user chooses sync, run it inline: use the Skill tool to invoke \`openspec-sync-specs\` for change '<name>', passing the delta spec analysis from above. Do not hand the sync to a background subagent — 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 harness can only run this as a subagent, dispatch it synchronously — for example \`run_in_background: false\` — and wait for its result.)

When the sync finishes, re-check every capability it touched at \`<planningHome.root>/openspec/specs/<capability>/spec.md\`: ADDED and MODIFIED requirements present, REMOVED requirements gone, RENAMED requirements under their new name. If the sync failed, or any capability does not match its delta, report it and stop — do not archive.

For any other choice, proceed to archive.

5. **Perform the archive**

Expand Down Expand Up @@ -280,7 +293,8 @@ Target archive directory already exists.
- Don't block archive on warnings - just inform and confirm
- Preserve .openspec.yaml when moving to archive (it moves with the directory)
- Show clear summary of what happened
- If sync is requested, use the Skill tool to invoke \`openspec-sync-specs\` (agent-driven)
- If sync is requested, use the Skill tool to invoke \`openspec-sync-specs\` (inline, agent-driven)
- Never archive while a spec sync is still in flight — run the sync inline and verify the main specs before moving \`changeRoot\`
- If delta specs exist, always run the sync assessment and show the combined summary before prompting`
};
}
24 changes: 21 additions & 3 deletions test/core/templates/skill-templates-parity.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -49,11 +49,11 @@ const EXPECTED_FUNCTION_HASHES: Record<string, string> = {
getOpsxContinueCommandTemplate: 'f63964fab7720ede097aa48808baff196c391b962930ca960459205c724800e5',
getOpsxApplyCommandTemplate: 'daeb507206707169de73c828e199648dde5732cbc17791ef2a027adffd028574',
getOpsxFfCommandTemplate: 'b859b1955cda6012877ae7f9ec6980e468f2e949a3838dfcdebc17209d133749',
getArchiveChangeSkillTemplate: '81c0ef6794bc0e0b79342ea2a1814efb0d9bc8c7ebc9d7d63a16714d781ee804',
getArchiveChangeSkillTemplate: '63bcf56a53eee2550d12e80aaafbe37640c432f3a16baee003d903052dbf000f',
getBulkArchiveChangeSkillTemplate: 'f675122bce3ef583b245352abedecf50ff4043e45bea6bac091885f83c7b6362',
getOpsxSyncCommandTemplate: '98b20e00da5c588ff83ed6e6f0e959dfc540349090fb3f5792ea030d099b8169',
getVerifyChangeSkillTemplate: 'cab4db01b5d2b1243d63d90c53747d8b39e488c60f76eba3fe8b994467f69267',
getOpsxArchiveCommandTemplate: '871d9909e0e465fc98f07826c29183f4739c1d9fb79bd268ac5f8685f37f872d',
getOpsxArchiveCommandTemplate: 'c13e6491780273f59014a0177258b4b794323169900e0245799e8a5ea95fb5ea',
getOpsxOnboardCommandTemplate: '0673f34a0f81fd173bcfb8c3ac83e2b1c617f7b7564e24e5298d3bd5665a05a9',
getOpsxBulkArchiveCommandTemplate: 'd0d84040bcbd44e89ac525bb21100bee7befb3604e51095bfa65b8453d85290c',
getOpsxVerifyCommandTemplate: 'f01c0c0cef53be0956de52363d955d4ace131b1b2d77adf902f35fead9a1486d',
Expand All @@ -71,7 +71,7 @@ const EXPECTED_GENERATED_SKILL_CONTENT_HASHES: Record<string, string> = {
'openspec-apply-change': '09c0e1cdf5ccc82416d0969d6bd715cc70616bdbc3531358a5c36057f78be55a',
'openspec-ff-change': '0c82830cd9bc98f86eb56b63ddaabe2bf5d35fe25b6c40a7059311aee2c8acac',
'openspec-sync-specs': 'b3f694ab81956d05126b089fe82dea78dec21788978bb9651485f996aee96740',
'openspec-archive-change': '5efd666d9b13e3cb41346bc65829026325daaf0b8eaa0e924e12e7021f2ff15a',
'openspec-archive-change': '52020d1b337a0bbf9ba9a7c3a2368e94a23dca62b89e85c4b4fc7256e4330769',
'openspec-bulk-archive-change': '545b9528df52fbb0b4898405b42a2ce10416678d469d20cf597d022fa6e16e3b',
'openspec-verify-change': '57693d22940f06080c6cf8d590ac2f48240d4a5e9ce7074dacd0f8d3c9945afa',
'openspec-onboard': 'b1b6fc9a1b3ff64dafe9b8c39a761ee1bd001b542d47b4e4deaf058e0aa21256',
Expand Down Expand Up @@ -209,4 +209,22 @@ describe('skill templates split parity', () => {
expect(content, dirName).not.toContain('Workspace guard');
}
});

it('gates the archive on a completed spec sync (#1393)', () => {
const generatedSkill = generateSkillContent(getArchiveChangeSkillTemplate(), 'PARITY-BASELINE');
const commandContent = getOpsxArchiveCommandTemplate().content;

for (const content of [generatedSkill, commandContent]) {
// The sync must run inline: a backgrounded subagent lets step 5 move
// changeRoot out from under a sync that is still reading it.
expect(content).toContain('run it inline: use the Skill tool to invoke');
expect(content).toContain('Do not hand the sync to a background subagent');
expect(content).toContain('Never archive while a spec sync is still in flight');

// The verification predicate must follow delta semantics, or a correct
// REMOVED-only sync would read as a failure and block the archive.
expect(content).toContain('REMOVED requirements gone');
expect(content).toContain('RENAMED requirements under their new name');
}
});
});
Loading