From eaa14e03c731acdcf1fb2d02fb1f4fd6f8d7ecad Mon Sep 17 00:00:00 2001 From: Clay Good Date: Mon, 20 Jul 2026 12:24:11 -0500 Subject: [PATCH 1/5] fix(templates): wait for the spec sync before archiving a change MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The generated openspec-archive-change skill dispatched the spec sync to a subagent via the Task tool and then moved changeRoot in the very next step, with nothing requiring it to wait. Where subagents run asynchronously, the archive relocates the delta specs out from under the running sync, so the change is archived while openspec/specs/ is never updated — and the success summary still reports "Specs: ✓ Synced". Step 4 now requires waiting for the dispatched sync to return, verifying the synced requirements are present in the main spec, and stopping without archiving if either check fails. Adds a matching guardrail bullet and a parity assertion so the gate cannot silently disappear again. Fixes #1393 Co-Authored-By: Claude Opus 4.8 (1M context) --- .changeset/archive-waits-for-spec-sync.md | 7 +++++++ skills/openspec-archive-change/SKILL.md | 3 ++- src/core/templates/workflows/archive-change.ts | 6 ++++-- .../templates/skill-templates-parity.test.ts | 16 +++++++++++++--- 4 files changed, 26 insertions(+), 6 deletions(-) create mode 100644 .changeset/archive-waits-for-spec-sync.md diff --git a/.changeset/archive-waits-for-spec-sync.md b/.changeset/archive-waits-for-spec-sync.md new file mode 100644 index 0000000000..a973ef33de --- /dev/null +++ b/.changeset/archive-waits-for-spec-sync.md @@ -0,0 +1,7 @@ +--- +"@fission-ai/openspec": patch +--- + +### Fixes + +- **Archive no longer races the spec sync** — the generated `openspec-archive-change` skill (and the matching `opsx:archive` command) dispatched the spec sync to a subagent and then moved the change folder immediately, without waiting. In harnesses where subagents run asynchronously, the archive could move the delta specs out from under the running sync, so the change ended up archived while `openspec/specs/` was never updated — and the summary still reported `Specs: ✓ Synced`. The instructions now require waiting for the sync to return, verifying the synced requirements landed in the main spec, and stopping without archiving if either check fails. diff --git a/skills/openspec-archive-change/SKILL.md b/skills/openspec-archive-change/SKILL.md index 3c5cd0fadc..5c6c66df9a 100644 --- a/skills/openspec-archive-change/SKILL.md +++ b/skills/openspec-archive-change/SKILL.md @@ -66,7 +66,7 @@ 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 ''. Delta spec analysis: "). If the user chooses "Cancel", stop — do not archive. For any other choice, proceed to archive. + If user chooses sync, use Task tool (subagent_type: "general-purpose", prompt: "Use Skill tool to invoke openspec-sync-specs for change ''. Delta spec analysis: "). Wait for that subagent to return before continuing — do not start step 5 while a sync is still running, because moving `changeRoot` out from under it loses the delta specs and leaves the main specs unsynced. Once it returns, confirm the synced requirements are present in `/openspec/specs//spec.md`. If the sync failed or those requirements are missing, report that and stop — do not archive. If the user chooses "Cancel", stop — do not archive. For any other choice, proceed to archive. 5. **Perform the archive** @@ -114,4 +114,5 @@ Archive a completed change in the experimental workflow. - 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) +- Never archive while a spec sync is still in flight — wait for it to finish and verify the main specs first - If delta specs exist, always run the sync assessment and show the combined summary before prompting diff --git a/src/core/templates/workflows/archive-change.ts b/src/core/templates/workflows/archive-change.ts index 36dc43b403..22df92c91a 100644 --- a/src/core/templates/workflows/archive-change.ts +++ b/src/core/templates/workflows/archive-change.ts @@ -68,7 +68,7 @@ ${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 ''. Delta spec analysis: "). If the user chooses "Cancel", stop — do not archive. For any other choice, proceed to archive. + If user chooses sync, use Task tool (subagent_type: "general-purpose", prompt: "Use Skill tool to invoke openspec-sync-specs for change ''. Delta spec analysis: "). Wait for that subagent to return before continuing — do not start step 5 while a sync is still running, because moving \`changeRoot\` out from under it loses the delta specs and leaves the main specs unsynced. Once it returns, confirm the synced requirements are present in \`/openspec/specs//spec.md\`. If the sync failed or those requirements are missing, report that and stop — do not archive. If the user chooses "Cancel", stop — do not archive. For any other choice, proceed to archive. 5. **Perform the archive** @@ -116,6 +116,7 @@ ${STORE_SELECTION_GUIDANCE} - 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) +- Never archive while a spec sync is still in flight — wait for it to finish and verify the main specs first - If delta specs exist, always run the sync assessment and show the combined summary before prompting`, license: 'MIT', compatibility: 'Requires openspec CLI.', @@ -186,7 +187,7 @@ ${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 ''. Delta spec analysis: "). If the user chooses "Cancel", stop — do not archive. For any other choice, proceed to archive. + If user chooses sync, use Task tool (subagent_type: "general-purpose", prompt: "Use Skill tool to invoke openspec-sync-specs for change ''. Delta spec analysis: "). Wait for that subagent to return before continuing — do not start step 5 while a sync is still running, because moving \`changeRoot\` out from under it loses the delta specs and leaves the main specs unsynced. Once it returns, confirm the synced requirements are present in \`/openspec/specs//spec.md\`. If the sync failed or those requirements are missing, report that and stop — do not archive. If the user chooses "Cancel", stop — do not archive. For any other choice, proceed to archive. 5. **Perform the archive** @@ -281,6 +282,7 @@ Target archive directory already exists. - 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) +- Never archive while a spec sync is still in flight — wait for it to finish and verify the main specs first - If delta specs exist, always run the sync assessment and show the 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 90cc5707ac..d24b9955d9 100644 --- a/test/core/templates/skill-templates-parity.test.ts +++ b/test/core/templates/skill-templates-parity.test.ts @@ -49,11 +49,11 @@ const EXPECTED_FUNCTION_HASHES: Record = { getOpsxContinueCommandTemplate: 'f63964fab7720ede097aa48808baff196c391b962930ca960459205c724800e5', getOpsxApplyCommandTemplate: 'daeb507206707169de73c828e199648dde5732cbc17791ef2a027adffd028574', getOpsxFfCommandTemplate: 'b859b1955cda6012877ae7f9ec6980e468f2e949a3838dfcdebc17209d133749', - getArchiveChangeSkillTemplate: '81c0ef6794bc0e0b79342ea2a1814efb0d9bc8c7ebc9d7d63a16714d781ee804', + getArchiveChangeSkillTemplate: '525f5f5e4879fd924eac9e2e3e03352c4bf78fa981c5a0557301c0ef51283af7', getBulkArchiveChangeSkillTemplate: 'f675122bce3ef583b245352abedecf50ff4043e45bea6bac091885f83c7b6362', getOpsxSyncCommandTemplate: '98b20e00da5c588ff83ed6e6f0e959dfc540349090fb3f5792ea030d099b8169', getVerifyChangeSkillTemplate: 'cab4db01b5d2b1243d63d90c53747d8b39e488c60f76eba3fe8b994467f69267', - getOpsxArchiveCommandTemplate: '871d9909e0e465fc98f07826c29183f4739c1d9fb79bd268ac5f8685f37f872d', + getOpsxArchiveCommandTemplate: 'd3d52f456541a8b312fe8ed37ced5188793dc9b9601eaf059b1fa38d0eadc7ca', getOpsxOnboardCommandTemplate: '0673f34a0f81fd173bcfb8c3ac83e2b1c617f7b7564e24e5298d3bd5665a05a9', getOpsxBulkArchiveCommandTemplate: 'd0d84040bcbd44e89ac525bb21100bee7befb3604e51095bfa65b8453d85290c', getOpsxVerifyCommandTemplate: 'f01c0c0cef53be0956de52363d955d4ace131b1b2d77adf902f35fead9a1486d', @@ -71,7 +71,7 @@ const EXPECTED_GENERATED_SKILL_CONTENT_HASHES: Record = { 'openspec-apply-change': '09c0e1cdf5ccc82416d0969d6bd715cc70616bdbc3531358a5c36057f78be55a', 'openspec-ff-change': '0c82830cd9bc98f86eb56b63ddaabe2bf5d35fe25b6c40a7059311aee2c8acac', 'openspec-sync-specs': 'b3f694ab81956d05126b089fe82dea78dec21788978bb9651485f996aee96740', - 'openspec-archive-change': '5efd666d9b13e3cb41346bc65829026325daaf0b8eaa0e924e12e7021f2ff15a', + 'openspec-archive-change': '8d9e6b6f2b0e773249351ddcd37fa75ae714e9acabbf5560960039e60b07a25d', 'openspec-bulk-archive-change': '545b9528df52fbb0b4898405b42a2ce10416678d469d20cf597d022fa6e16e3b', 'openspec-verify-change': '57693d22940f06080c6cf8d590ac2f48240d4a5e9ce7074dacd0f8d3c9945afa', 'openspec-onboard': 'b1b6fc9a1b3ff64dafe9b8c39a761ee1bd001b542d47b4e4deaf058e0aa21256', @@ -209,4 +209,14 @@ 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]) { + expect(content).toContain('Wait for that subagent to return before continuing'); + expect(content).toContain('Never archive while a spec sync is still in flight'); + } + }); }); From 2095c16d13549d8b66e691e563e743f7dbd0c5c6 Mon Sep 17 00:00:00 2001 From: Clay Good Date: Mon, 20 Jul 2026 12:34:12 -0500 Subject: [PATCH 2/5] fix(templates): run the spec sync inline and verify it before archiving MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Addresses review on #1394. The first pass asked the agent to "wait" for a dispatched subagent, but subagents run in the background by default and the wait is not reliably expressible in prose — the race survived. It also gated the archive on the synced requirements being *present*, which a correct REMOVED-only or RENAMED-only sync does not satisfy, turning a successful sync into a hard block. The sync now runs inline via the Skill tool, with a synchronous-subagent fallback for harnesses that need one. Verification follows delta semantics: ADDED/MODIFIED present, REMOVED gone, RENAMED under the new name, checked across every capability the sync touched. Also resolves the opsx command variant's contradiction with its own guardrail, stops the summary reporting a checkmark that step 4 never verified, and updates openspec/specs/opsx-archive-skill/spec.md, which still said the skill proceeds with the archive regardless of the sync choice. Co-Authored-By: Claude Opus 4.8 (1M context) --- .changeset/archive-waits-for-spec-sync.md | 4 +-- openspec/specs/opsx-archive-skill/spec.md | 6 +++-- skills/openspec-archive-change/SKILL.md | 14 +++++++--- .../templates/workflows/archive-change.ts | 26 ++++++++++++++----- .../templates/skill-templates-parity.test.ts | 16 +++++++++--- 5 files changed, 47 insertions(+), 19 deletions(-) diff --git a/.changeset/archive-waits-for-spec-sync.md b/.changeset/archive-waits-for-spec-sync.md index a973ef33de..1ea97cbe0a 100644 --- a/.changeset/archive-waits-for-spec-sync.md +++ b/.changeset/archive-waits-for-spec-sync.md @@ -2,6 +2,6 @@ "@fission-ai/openspec": patch --- -### Fixes +### Bug Fixes -- **Archive no longer races the spec sync** — the generated `openspec-archive-change` skill (and the matching `opsx:archive` command) dispatched the spec sync to a subagent and then moved the change folder immediately, without waiting. In harnesses where subagents run asynchronously, the archive could move the delta specs out from under the running sync, so the change ended up archived while `openspec/specs/` was never updated — and the summary still reported `Specs: ✓ Synced`. The instructions now require waiting for the sync to return, verifying the synced requirements landed in the main spec, and stopping without archiving if either check fails. +- **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. diff --git a/openspec/specs/opsx-archive-skill/spec.md b/openspec/specs/opsx-archive-skill/spec.md index 2dbb04c529..a67e31146b 100644 --- a/openspec/specs/opsx-archive-skill/spec.md +++ b/openspec/specs/opsx-archive-skill/spec.md @@ -74,8 +74,10 @@ 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 confirms, execute `/opsx:sync` logic inline and wait for it to complete +- **AND** verify every synced capability matches its delta before archiving +- **AND** stop without archiving if the user cancels, the sync fails, or a capability does not match +- **AND** otherwise proceed with the archive #### Scenario: No delta specs diff --git a/skills/openspec-archive-change/SKILL.md b/skills/openspec-archive-change/SKILL.md index 5c6c66df9a..e83798fd62 100644 --- a/skills/openspec-archive-change/SKILL.md +++ b/skills/openspec-archive-change/SKILL.md @@ -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 ''. Delta spec analysis: "). Wait for that subagent to return before continuing — do not start step 5 while a sync is still running, because moving `changeRoot` out from under it loses the delta specs and leaves the main specs unsynced. Once it returns, confirm the synced requirements are present in `/openspec/specs//spec.md`. If the sync failed or those requirements are missing, report that and stop — do not archive. 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 '', 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 `/openspec/specs//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** @@ -102,7 +108,7 @@ Archive a completed change in the experimental workflow. **Change:** **Schema:** **Archived to:** the archive path derived from `planningHome.changesDir`/YYYY-MM-DD-/ -**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")> ``` @@ -113,6 +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) -- Never archive while a spec sync is still in flight — wait for it to finish and verify the main specs first +- 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 diff --git a/src/core/templates/workflows/archive-change.ts b/src/core/templates/workflows/archive-change.ts index 22df92c91a..0e6e39e79b 100644 --- a/src/core/templates/workflows/archive-change.ts +++ b/src/core/templates/workflows/archive-change.ts @@ -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 ''. Delta spec analysis: "). Wait for that subagent to return before continuing — do not start step 5 while a sync is still running, because moving \`changeRoot\` out from under it loses the delta specs and leaves the main specs unsynced. Once it returns, confirm the synced requirements are present in \`/openspec/specs//spec.md\`. If the sync failed or those requirements are missing, report that and stop — do not archive. 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 '', 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 \`/openspec/specs//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** @@ -104,7 +110,7 @@ ${STORE_SELECTION_GUIDANCE} **Change:** **Schema:** **Archived to:** the archive path derived from \`planningHome.changesDir\`/YYYY-MM-DD-/ -**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")> \`\`\` @@ -115,8 +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) -- Never archive while a spec sync is still in flight — wait for it to finish and verify the main specs first +- 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.', @@ -187,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 ''. Delta spec analysis: "). Wait for that subagent to return before continuing — do not start step 5 while a sync is still running, because moving \`changeRoot\` out from under it loses the delta specs and leaves the main specs unsynced. Once it returns, confirm the synced requirements are present in \`/openspec/specs//spec.md\`. If the sync failed or those requirements are missing, report that and stop — do not archive. 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 '', 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 \`/openspec/specs//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** @@ -281,8 +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) -- Never archive while a spec sync is still in flight — wait for it to finish and verify the main specs first +- 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` }; } diff --git a/test/core/templates/skill-templates-parity.test.ts b/test/core/templates/skill-templates-parity.test.ts index d24b9955d9..c2847597b9 100644 --- a/test/core/templates/skill-templates-parity.test.ts +++ b/test/core/templates/skill-templates-parity.test.ts @@ -49,11 +49,11 @@ const EXPECTED_FUNCTION_HASHES: Record = { getOpsxContinueCommandTemplate: 'f63964fab7720ede097aa48808baff196c391b962930ca960459205c724800e5', getOpsxApplyCommandTemplate: 'daeb507206707169de73c828e199648dde5732cbc17791ef2a027adffd028574', getOpsxFfCommandTemplate: 'b859b1955cda6012877ae7f9ec6980e468f2e949a3838dfcdebc17209d133749', - getArchiveChangeSkillTemplate: '525f5f5e4879fd924eac9e2e3e03352c4bf78fa981c5a0557301c0ef51283af7', + getArchiveChangeSkillTemplate: '63bcf56a53eee2550d12e80aaafbe37640c432f3a16baee003d903052dbf000f', getBulkArchiveChangeSkillTemplate: 'f675122bce3ef583b245352abedecf50ff4043e45bea6bac091885f83c7b6362', getOpsxSyncCommandTemplate: '98b20e00da5c588ff83ed6e6f0e959dfc540349090fb3f5792ea030d099b8169', getVerifyChangeSkillTemplate: 'cab4db01b5d2b1243d63d90c53747d8b39e488c60f76eba3fe8b994467f69267', - getOpsxArchiveCommandTemplate: 'd3d52f456541a8b312fe8ed37ced5188793dc9b9601eaf059b1fa38d0eadc7ca', + getOpsxArchiveCommandTemplate: 'c13e6491780273f59014a0177258b4b794323169900e0245799e8a5ea95fb5ea', getOpsxOnboardCommandTemplate: '0673f34a0f81fd173bcfb8c3ac83e2b1c617f7b7564e24e5298d3bd5665a05a9', getOpsxBulkArchiveCommandTemplate: 'd0d84040bcbd44e89ac525bb21100bee7befb3604e51095bfa65b8453d85290c', getOpsxVerifyCommandTemplate: 'f01c0c0cef53be0956de52363d955d4ace131b1b2d77adf902f35fead9a1486d', @@ -71,7 +71,7 @@ const EXPECTED_GENERATED_SKILL_CONTENT_HASHES: Record = { 'openspec-apply-change': '09c0e1cdf5ccc82416d0969d6bd715cc70616bdbc3531358a5c36057f78be55a', 'openspec-ff-change': '0c82830cd9bc98f86eb56b63ddaabe2bf5d35fe25b6c40a7059311aee2c8acac', 'openspec-sync-specs': 'b3f694ab81956d05126b089fe82dea78dec21788978bb9651485f996aee96740', - 'openspec-archive-change': '8d9e6b6f2b0e773249351ddcd37fa75ae714e9acabbf5560960039e60b07a25d', + 'openspec-archive-change': '52020d1b337a0bbf9ba9a7c3a2368e94a23dca62b89e85c4b4fc7256e4330769', 'openspec-bulk-archive-change': '545b9528df52fbb0b4898405b42a2ce10416678d469d20cf597d022fa6e16e3b', 'openspec-verify-change': '57693d22940f06080c6cf8d590ac2f48240d4a5e9ce7074dacd0f8d3c9945afa', 'openspec-onboard': 'b1b6fc9a1b3ff64dafe9b8c39a761ee1bd001b542d47b4e4deaf058e0aa21256', @@ -215,8 +215,16 @@ describe('skill templates split parity', () => { const commandContent = getOpsxArchiveCommandTemplate().content; for (const content of [generatedSkill, commandContent]) { - expect(content).toContain('Wait for that subagent to return before continuing'); + // 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'); } }); }); From 07109bd7cade40fbf635a45d939c27294472e7dd Mon Sep 17 00:00:00 2001 From: Clay Good Date: Mon, 20 Jul 2026 12:40:14 -0500 Subject: [PATCH 3/5] docs(specs): encode delta verification semantics in the archive skill spec MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The scenario said the agent verifies each capability "matches its delta", which is ambiguous about what a match means — and a REMOVED-only sync correctly leaves requirements absent. Spell out the predicate the template implements, and separate an explicit "Archive without syncing" choice from a requested sync that failed or could not be verified: only the former may skip verification, the latter must stop. Co-Authored-By: Claude Opus 4.8 (1M context) --- openspec/specs/opsx-archive-skill/spec.md | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/openspec/specs/opsx-archive-skill/spec.md b/openspec/specs/opsx-archive-skill/spec.md index a67e31146b..f1179f3e2f 100644 --- a/openspec/specs/opsx-archive-skill/spec.md +++ b/openspec/specs/opsx-archive-skill/spec.md @@ -74,10 +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 cancels, stop without archiving - **AND** if user confirms, execute `/opsx:sync` logic inline and wait for it to complete -- **AND** verify every synced capability matches its delta before archiving -- **AND** stop without archiving if the user cancels, the sync fails, or a capability does not match -- **AND** otherwise proceed with the archive +- **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 From 8c6f2a4bec0329abcdd83d635ed0ea54a659f9bb Mon Sep 17 00:00:00 2001 From: Clay Good Date: Mon, 20 Jul 2026 13:19:27 -0500 Subject: [PATCH 4/5] fix(templates): close verification holes and drop Claude-only tool names MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Second review pass on #1394. The gate was weaker than it looked. "MODIFIED requirements present" is vacuous — a MODIFIED requirement exists in the main spec before the sync runs, so a no-op sync passed the check for the most common delta shape, which is the exact symptom #1393 reports. "RENAMED under their new name" passed a sync that copied rather than renamed, leaving both names behind. And scoping the re-check to "every capability it touched" derived the verification set from the artifact being verified, so a silently skipped capability escaped it. Verification is now bound to the delta specs in artifactPaths.specs, covers the changes each MODIFIED delta names, and requires RENAMED requirements to be gone from the old name. Separately, the previous pass named the Claude Code "Skill tool" and run_in_background in a template that is also the slash-command source for ~28 other tools, where skills are removed entirely for commands-only delivery. Both variants now use the runtime-neutral phrasing bulk-archive-change already uses. Also: route the prompt options explicitly instead of defaulting unknown answers to archive, tell the user a stopped archive is recoverable, and mark the summary line as a conditional rather than literal text to copy. Co-Authored-By: Claude Opus 4.8 (1M context) --- .changeset/archive-waits-for-spec-sync.md | 2 +- openspec/specs/opsx-archive-skill/spec.md | 4 +- skills/openspec-archive-change/SKILL.md | 20 +++++++--- .../templates/workflows/archive-change.ts | 38 ++++++++++++------ .../templates/skill-templates-parity.test.ts | 39 ++++++++++++------- 5 files changed, 69 insertions(+), 34 deletions(-) diff --git a/.changeset/archive-waits-for-spec-sync.md b/.changeset/archive-waits-for-spec-sync.md index 1ea97cbe0a..21e6f663b2 100644 --- a/.changeset/archive-waits-for-spec-sync.md +++ b/.changeset/archive-waits-for-spec-sync.md @@ -4,4 +4,4 @@ ### 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. +- **Archive no longer races the spec sync, or reports a sync that never landed** — the generated `openspec-archive-change` skill (and the matching `opsx:archive` command) handed the spec sync to a background task and then moved the change folder immediately. 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 capability with a delta spec has been checked against it — ADDED present, MODIFIED changes applied, REMOVED gone, RENAMED under the new name and not the old. If the sync fails or a capability doesn't match, the archive stops and reports what differs instead of claiming success; nothing has moved, so you can fix it and retry. diff --git a/openspec/specs/opsx-archive-skill/spec.md b/openspec/specs/opsx-archive-skill/spec.md index f1179f3e2f..95ba9dc2d0 100644 --- a/openspec/specs/opsx-archive-skill/spec.md +++ b/openspec/specs/opsx-archive-skill/spec.md @@ -76,9 +76,9 @@ The skill SHALL prompt to sync delta specs before archiving if specs exist. - **THEN** prompt user: "This change has delta specs. Would you like to sync them to main specs before archiving?" - **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** verify every capability that has a delta spec, not only those the sync reports it touched: ADDED requirements present, MODIFIED requirements carrying the changes named in the delta, REMOVED requirements absent, RENAMED requirements present under the new name and absent under the old one - **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 +- **AND** archive only after verification passes, or when the user explicitly chose to archive without syncing or to archive already-synced specs #### Scenario: No delta specs diff --git a/skills/openspec-archive-change/SKILL.md b/skills/openspec-archive-change/SKILL.md index e83798fd62..e54de30556 100644 --- a/skills/openspec-archive-change/SKILL.md +++ b/skills/openspec-archive-change/SKILL.md @@ -66,13 +66,21 @@ 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 the user chooses "Cancel", stop — do not archive. + 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) + - Anything else — ask again rather than archiving - If the user chooses sync, run it inline: use the Skill tool to invoke `openspec-sync-specs` for change '', 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.) + To sync, run the `openspec-sync-specs` workflow inline (agent-driven intelligent merge) for change '', passing the delta spec analysis from above, and wait for it to finish. 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. - When the sync finishes, re-check every capability it touched at `/openspec/specs//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. + Then re-check every capability that has a delta spec in `artifactPaths.specs.existingOutputPaths` — not only the ones the sync reports it touched — against `/openspec/specs//spec.md`: + - ADDED requirements present + - MODIFIED requirements carrying the scenario and description changes named in the delta, with their other scenarios intact + - REMOVED requirements gone + - RENAMED requirements present under the new name and absent under the old one - For any other choice, proceed to archive. + If the sync failed, or any capability does not match, report what differs and stop — do not archive. Nothing has moved and `changeRoot` is intact, so the user can fix the mismatch or re-run the sync and start the archive again. 5. **Perform the archive** @@ -108,7 +116,7 @@ Archive a completed change in the experimental workflow. **Change:** **Schema:** **Archived to:** the archive path derived from `planningHome.changesDir`/YYYY-MM-DD-/ -**Specs:** ✓ Synced to main specs — only if the step 4 verification passed (otherwise "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")> ``` @@ -119,6 +127,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 the Skill tool to invoke `openspec-sync-specs` (inline, agent-driven) +- If sync is requested, run the `openspec-sync-specs` workflow 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 diff --git a/src/core/templates/workflows/archive-change.ts b/src/core/templates/workflows/archive-change.ts index 0e6e39e79b..5019c53a40 100644 --- a/src/core/templates/workflows/archive-change.ts +++ b/src/core/templates/workflows/archive-change.ts @@ -68,13 +68,21 @@ ${STORE_SELECTION_GUIDANCE} - If changes needed: "Sync now (recommended)", "Archive without syncing" - If already synced: "Archive now", "Sync anyway", "Cancel" - If the user chooses "Cancel", stop — do not archive. + 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) + - Anything else — ask again rather than archiving - If the user chooses sync, run it inline: use the Skill tool to invoke \`openspec-sync-specs\` for change '', 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.) + To sync, run the \`openspec-sync-specs\` workflow inline (agent-driven intelligent merge) for change '', passing the delta spec analysis from above, and wait for it to finish. 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. - When the sync finishes, re-check every capability it touched at \`/openspec/specs//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. + Then re-check every capability that has a delta spec in \`artifactPaths.specs.existingOutputPaths\` — not only the ones the sync reports it touched — against \`/openspec/specs//spec.md\`: + - ADDED requirements present + - MODIFIED requirements carrying the scenario and description changes named in the delta, with their other scenarios intact + - REMOVED requirements gone + - RENAMED requirements present under the new name and absent under the old one - For any other choice, proceed to archive. + If the sync failed, or any capability does not match, report what differs and stop — do not archive. Nothing has moved and \`changeRoot\` is intact, so the user can fix the mismatch or re-run the sync and start the archive again. 5. **Perform the archive** @@ -110,7 +118,7 @@ ${STORE_SELECTION_GUIDANCE} **Change:** **Schema:** **Archived to:** the archive path derived from \`planningHome.changesDir\`/YYYY-MM-DD-/ -**Specs:** ✓ Synced to main specs — only if the step 4 verification passed (otherwise "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")> \`\`\` @@ -121,7 +129,7 @@ ${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 the Skill tool to invoke \`openspec-sync-specs\` (inline, agent-driven) +- If sync is requested, run the \`openspec-sync-specs\` workflow 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', @@ -193,13 +201,21 @@ ${STORE_SELECTION_GUIDANCE} - If changes needed: "Sync now (recommended)", "Archive without syncing" - If already synced: "Archive now", "Sync anyway", "Cancel" - If the user chooses "Cancel", stop — do not archive. + 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) + - Anything else — ask again rather than archiving - If the user chooses sync, run it inline: use the Skill tool to invoke \`openspec-sync-specs\` for change '', 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.) + To sync, run the \`openspec-sync-specs\` workflow inline (agent-driven intelligent merge) for change '', passing the delta spec analysis from above, and wait for it to finish. 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. - When the sync finishes, re-check every capability it touched at \`/openspec/specs//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. + Then re-check every capability that has a delta spec in \`artifactPaths.specs.existingOutputPaths\` — not only the ones the sync reports it touched — against \`/openspec/specs//spec.md\`: + - ADDED requirements present + - MODIFIED requirements carrying the scenario and description changes named in the delta, with their other scenarios intact + - REMOVED requirements gone + - RENAMED requirements present under the new name and absent under the old one - For any other choice, proceed to archive. + If the sync failed, or any capability does not match, report what differs and stop — do not archive. Nothing has moved and \`changeRoot\` is intact, so the user can fix the mismatch or re-run the sync and start the archive again. 5. **Perform the archive** @@ -293,7 +309,7 @@ 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\` (inline, agent-driven) +- If sync is requested, run the \`openspec-sync-specs\` workflow 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` }; diff --git a/test/core/templates/skill-templates-parity.test.ts b/test/core/templates/skill-templates-parity.test.ts index c2847597b9..2bfc17272e 100644 --- a/test/core/templates/skill-templates-parity.test.ts +++ b/test/core/templates/skill-templates-parity.test.ts @@ -49,11 +49,11 @@ const EXPECTED_FUNCTION_HASHES: Record = { getOpsxContinueCommandTemplate: 'f63964fab7720ede097aa48808baff196c391b962930ca960459205c724800e5', getOpsxApplyCommandTemplate: 'daeb507206707169de73c828e199648dde5732cbc17791ef2a027adffd028574', getOpsxFfCommandTemplate: 'b859b1955cda6012877ae7f9ec6980e468f2e949a3838dfcdebc17209d133749', - getArchiveChangeSkillTemplate: '63bcf56a53eee2550d12e80aaafbe37640c432f3a16baee003d903052dbf000f', + getArchiveChangeSkillTemplate: '9fcf7aa65543042b39ea77b9cb62586e345729ee131e91bf7d0565c58c5a3852', getBulkArchiveChangeSkillTemplate: 'f675122bce3ef583b245352abedecf50ff4043e45bea6bac091885f83c7b6362', getOpsxSyncCommandTemplate: '98b20e00da5c588ff83ed6e6f0e959dfc540349090fb3f5792ea030d099b8169', getVerifyChangeSkillTemplate: 'cab4db01b5d2b1243d63d90c53747d8b39e488c60f76eba3fe8b994467f69267', - getOpsxArchiveCommandTemplate: 'c13e6491780273f59014a0177258b4b794323169900e0245799e8a5ea95fb5ea', + getOpsxArchiveCommandTemplate: 'c31724e302184795ace242af0c93a1331b86b2fc77bbcebfeaa61af43a747c32', getOpsxOnboardCommandTemplate: '0673f34a0f81fd173bcfb8c3ac83e2b1c617f7b7564e24e5298d3bd5665a05a9', getOpsxBulkArchiveCommandTemplate: 'd0d84040bcbd44e89ac525bb21100bee7befb3604e51095bfa65b8453d85290c', getOpsxVerifyCommandTemplate: 'f01c0c0cef53be0956de52363d955d4ace131b1b2d77adf902f35fead9a1486d', @@ -71,7 +71,7 @@ const EXPECTED_GENERATED_SKILL_CONTENT_HASHES: Record = { 'openspec-apply-change': '09c0e1cdf5ccc82416d0969d6bd715cc70616bdbc3531358a5c36057f78be55a', 'openspec-ff-change': '0c82830cd9bc98f86eb56b63ddaabe2bf5d35fe25b6c40a7059311aee2c8acac', 'openspec-sync-specs': 'b3f694ab81956d05126b089fe82dea78dec21788978bb9651485f996aee96740', - 'openspec-archive-change': '52020d1b337a0bbf9ba9a7c3a2368e94a23dca62b89e85c4b4fc7256e4330769', + 'openspec-archive-change': 'fb39fd7cfa407e26dcc9d029eba3e6972ec1d253384bab5e47459a1c99750483', 'openspec-bulk-archive-change': '545b9528df52fbb0b4898405b42a2ce10416678d469d20cf597d022fa6e16e3b', 'openspec-verify-change': '57693d22940f06080c6cf8d590ac2f48240d4a5e9ce7074dacd0f8d3c9945afa', 'openspec-onboard': 'b1b6fc9a1b3ff64dafe9b8c39a761ee1bd001b542d47b4e4deaf058e0aa21256', @@ -214,17 +214,28 @@ describe('skill templates split parity', () => { 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'); + const variants: Array<[string, string]> = [ + ['skill', generatedSkill], + ['opsx command', commandContent], + ]; + + for (const [variant, content] of variants) { + // The sync must run inline: delegating it to a background task lets step 5 + // move changeRoot out from under a sync that is still reading it. + expect(content, variant).toContain('run the `openspec-sync-specs` workflow inline'); + expect(content, variant).toContain('Do not delegate it to a background task'); + expect(content, variant).toContain('Never archive while a spec sync is still in flight'); + + // Verification must follow delta semantics. Asserting presence alone would + // read a correct REMOVED-only sync as a failure, and would pass a no-op + // sync for a MODIFIED-only delta (those requirements already exist). + expect(content, variant).toContain('MODIFIED requirements carrying the scenario and description changes'); + expect(content, variant).toContain('REMOVED requirements gone'); + expect(content, variant).toContain('RENAMED requirements present under the new name and absent under the old one'); + + // Verification is bound to the delta specs on disk, not to whatever the + // sync reports it touched — a silently skipped capability must not escape. + expect(content, variant).toContain('not only the ones the sync reports it touched'); } }); }); From 035039f0f1775afd84b5360f5cd145cfe7802b44 Mon Sep 17 00:00:00 2001 From: Clay Good Date: Mon, 20 Jul 2026 13:32:24 -0500 Subject: [PATCH 5/5] fix(templates): verify the sync by re-running step 4's own comparison The verification predicate restated delta semantics in its own words, which could drift from what openspec-sync-specs actually does. Anchor it instead to the comparison step 4 already performs before prompting: a successful sync leaves nothing to apply, so every capability must read as already synced. The explicit ADDED/MODIFIED/REMOVED/RENAMED bullets stay as the definition of what "nothing left to apply" means. Co-Authored-By: Claude Opus 4.8 (1M context) --- skills/openspec-archive-change/SKILL.md | 2 +- src/core/templates/workflows/archive-change.ts | 4 ++-- test/core/templates/skill-templates-parity.test.ts | 6 +++--- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/skills/openspec-archive-change/SKILL.md b/skills/openspec-archive-change/SKILL.md index e54de30556..e198c0099c 100644 --- a/skills/openspec-archive-change/SKILL.md +++ b/skills/openspec-archive-change/SKILL.md @@ -74,7 +74,7 @@ Archive a completed change in the experimental workflow. To sync, run the `openspec-sync-specs` workflow inline (agent-driven intelligent merge) for change '', passing the delta spec analysis from above, and wait for it to finish. 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-check every capability that has a delta spec in `artifactPaths.specs.existingOutputPaths` — not only the ones the sync reports it touched — against `/openspec/specs//spec.md`: + 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: - ADDED requirements present - MODIFIED requirements carrying the scenario and description changes named in the delta, with their other scenarios intact - REMOVED requirements gone diff --git a/src/core/templates/workflows/archive-change.ts b/src/core/templates/workflows/archive-change.ts index 5019c53a40..564a7fe0c7 100644 --- a/src/core/templates/workflows/archive-change.ts +++ b/src/core/templates/workflows/archive-change.ts @@ -76,7 +76,7 @@ ${STORE_SELECTION_GUIDANCE} To sync, run the \`openspec-sync-specs\` workflow inline (agent-driven intelligent merge) for change '', passing the delta spec analysis from above, and wait for it to finish. 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-check every capability that has a delta spec in \`artifactPaths.specs.existingOutputPaths\` — not only the ones the sync reports it touched — against \`/openspec/specs//spec.md\`: + 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: - ADDED requirements present - MODIFIED requirements carrying the scenario and description changes named in the delta, with their other scenarios intact - REMOVED requirements gone @@ -209,7 +209,7 @@ ${STORE_SELECTION_GUIDANCE} To sync, run the \`openspec-sync-specs\` workflow inline (agent-driven intelligent merge) for change '', passing the delta spec analysis from above, and wait for it to finish. 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-check every capability that has a delta spec in \`artifactPaths.specs.existingOutputPaths\` — not only the ones the sync reports it touched — against \`/openspec/specs//spec.md\`: + 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: - ADDED requirements present - MODIFIED requirements carrying the scenario and description changes named in the delta, with their other scenarios intact - REMOVED requirements gone diff --git a/test/core/templates/skill-templates-parity.test.ts b/test/core/templates/skill-templates-parity.test.ts index 2bfc17272e..0d8b783852 100644 --- a/test/core/templates/skill-templates-parity.test.ts +++ b/test/core/templates/skill-templates-parity.test.ts @@ -49,11 +49,11 @@ const EXPECTED_FUNCTION_HASHES: Record = { getOpsxContinueCommandTemplate: 'f63964fab7720ede097aa48808baff196c391b962930ca960459205c724800e5', getOpsxApplyCommandTemplate: 'daeb507206707169de73c828e199648dde5732cbc17791ef2a027adffd028574', getOpsxFfCommandTemplate: 'b859b1955cda6012877ae7f9ec6980e468f2e949a3838dfcdebc17209d133749', - getArchiveChangeSkillTemplate: '9fcf7aa65543042b39ea77b9cb62586e345729ee131e91bf7d0565c58c5a3852', + getArchiveChangeSkillTemplate: 'a8f1d9cb06c20c7335ac35826dd09bfadead75ef6d624d359912734f74232cbc', getBulkArchiveChangeSkillTemplate: 'f675122bce3ef583b245352abedecf50ff4043e45bea6bac091885f83c7b6362', getOpsxSyncCommandTemplate: '98b20e00da5c588ff83ed6e6f0e959dfc540349090fb3f5792ea030d099b8169', getVerifyChangeSkillTemplate: 'cab4db01b5d2b1243d63d90c53747d8b39e488c60f76eba3fe8b994467f69267', - getOpsxArchiveCommandTemplate: 'c31724e302184795ace242af0c93a1331b86b2fc77bbcebfeaa61af43a747c32', + getOpsxArchiveCommandTemplate: '9d14e1ea23ae8be8971fafa1d6a4d4717a8a7b922b6e76c6fb07aa568a420632', getOpsxOnboardCommandTemplate: '0673f34a0f81fd173bcfb8c3ac83e2b1c617f7b7564e24e5298d3bd5665a05a9', getOpsxBulkArchiveCommandTemplate: 'd0d84040bcbd44e89ac525bb21100bee7befb3604e51095bfa65b8453d85290c', getOpsxVerifyCommandTemplate: 'f01c0c0cef53be0956de52363d955d4ace131b1b2d77adf902f35fead9a1486d', @@ -71,7 +71,7 @@ const EXPECTED_GENERATED_SKILL_CONTENT_HASHES: Record = { 'openspec-apply-change': '09c0e1cdf5ccc82416d0969d6bd715cc70616bdbc3531358a5c36057f78be55a', 'openspec-ff-change': '0c82830cd9bc98f86eb56b63ddaabe2bf5d35fe25b6c40a7059311aee2c8acac', 'openspec-sync-specs': 'b3f694ab81956d05126b089fe82dea78dec21788978bb9651485f996aee96740', - 'openspec-archive-change': 'fb39fd7cfa407e26dcc9d029eba3e6972ec1d253384bab5e47459a1c99750483', + 'openspec-archive-change': '4679a077d34016bf38f0d0aa5432b53ea83ae82c2c5fec6dcb7dc15571ee8ac6', 'openspec-bulk-archive-change': '545b9528df52fbb0b4898405b42a2ce10416678d469d20cf597d022fa6e16e3b', 'openspec-verify-change': '57693d22940f06080c6cf8d590ac2f48240d4a5e9ce7074dacd0f8d3c9945afa', 'openspec-onboard': 'b1b6fc9a1b3ff64dafe9b8c39a761ee1bd001b542d47b4e4deaf058e0aa21256',