From f0221c5de9eed13c40b82b5c4b82942b59554b6b Mon Sep 17 00:00:00 2001 From: Henry Su Date: Sat, 25 Jul 2026 20:02:14 -0500 Subject: [PATCH 1/2] fix(templates): make archive sync guidance profile-agnostic when sync skill is uninstalled (#913) --- skills/openspec-archive-change/SKILL.md | 4 ++-- skills/openspec-bulk-archive-change/SKILL.md | 2 +- src/core/templates/workflows/archive-change.ts | 4 ++-- src/core/templates/workflows/bulk-archive-change.ts | 4 ++-- test/core/templates/skill-templates-parity.test.ts | 10 +++++----- 5 files changed, 12 insertions(+), 12 deletions(-) diff --git a/skills/openspec-archive-change/SKILL.md b/skills/openspec-archive-change/SKILL.md index 35c3e66cf5..d453d1dbe0 100644 --- a/skills/openspec-archive-change/SKILL.md +++ b/skills/openspec-archive-change/SKILL.md @@ -72,7 +72,7 @@ Archive a completed change in the experimental workflow. - "Sync now" or "Sync anyway" — sync, then verify (below) - Anything else — ask again rather than archiving - 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. + To sync, run the `openspec-sync-specs` workflow inline (agent-driven intelligent merge) for change '' (if `openspec-sync-specs` is not installed, perform the spec sync directly by merging delta specs into main specs under `openspec/specs/`), 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-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 @@ -127,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, run the `openspec-sync-specs` workflow inline (agent-driven) +- If sync is requested, run the `openspec-sync-specs` workflow inline (agent-driven), or perform the spec sync directly if that workflow is not installed - 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/skills/openspec-bulk-archive-change/SKILL.md b/skills/openspec-bulk-archive-change/SKILL.md index 87dd2205a0..0b2060489f 100644 --- a/skills/openspec-bulk-archive-change/SKILL.md +++ b/skills/openspec-bulk-archive-change/SKILL.md @@ -130,7 +130,7 @@ This skill allows you to batch-archive changes, handling spec conflicts intellig Process changes in the determined order (respecting conflict resolution): a. **Sync specs** if delta specs exist: - - Use the openspec-sync-specs approach (agent-driven intelligent merge) + - Use the openspec-sync-specs approach (agent-driven intelligent merge, or merge delta specs directly into main specs if sync workflow is not installed) - For conflicts, apply in resolved order - Track if sync was done diff --git a/src/core/templates/workflows/archive-change.ts b/src/core/templates/workflows/archive-change.ts index 85441751d0..24e68ac5a6 100644 --- a/src/core/templates/workflows/archive-change.ts +++ b/src/core/templates/workflows/archive-change.ts @@ -74,7 +74,7 @@ ${STORE_SELECTION_GUIDANCE} - "Sync now" or "Sync anyway" — sync, then verify (below) - Anything else — ask again rather than archiving - 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. + To sync, run the \`openspec-sync-specs\` workflow inline (agent-driven intelligent merge) for change '' (if \`openspec-sync-specs\` is not installed, perform the spec sync directly by merging delta specs into main specs under \`openspec/specs/\`), 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-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 @@ -129,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, run the \`openspec-sync-specs\` workflow inline (agent-driven) +- If sync is requested, run the \`openspec-sync-specs\` workflow inline (agent-driven), or perform the spec sync directly if that workflow is not installed - 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', diff --git a/src/core/templates/workflows/bulk-archive-change.ts b/src/core/templates/workflows/bulk-archive-change.ts index 3cca28b022..0045c3a8b8 100644 --- a/src/core/templates/workflows/bulk-archive-change.ts +++ b/src/core/templates/workflows/bulk-archive-change.ts @@ -132,7 +132,7 @@ ${STORE_SELECTION_GUIDANCE} Process changes in the determined order (respecting conflict resolution): a. **Sync specs** if delta specs exist: - - Use the openspec-sync-specs approach (agent-driven intelligent merge) + - Use the openspec-sync-specs approach (agent-driven intelligent merge, or merge delta specs directly into main specs if sync workflow is not installed) - For conflicts, apply in resolved order - Track if sync was done @@ -392,7 +392,7 @@ ${STORE_SELECTION_GUIDANCE} Process changes in the determined order (respecting conflict resolution): a. **Sync specs** if delta specs exist: - - Use the openspec-sync-specs approach (agent-driven intelligent merge) + - Use the openspec-sync-specs approach (agent-driven intelligent merge, or merge delta specs directly into main specs if sync workflow is not installed) - For conflicts, apply in resolved order - Track if sync was done diff --git a/test/core/templates/skill-templates-parity.test.ts b/test/core/templates/skill-templates-parity.test.ts index a9b2e6fdb2..039b56022c 100644 --- a/test/core/templates/skill-templates-parity.test.ts +++ b/test/core/templates/skill-templates-parity.test.ts @@ -49,13 +49,13 @@ const EXPECTED_FUNCTION_HASHES: Record = { getOpsxContinueCommandTemplate: '5c3968174001c20737ba39d2473ecec0f3b76591a80f7e2fc3974904d3da9dcd', getOpsxApplyCommandTemplate: '147408d7085b468981a400cc725804252c3fd84e519c57c5f6f83562e32606ee', getOpsxFfCommandTemplate: '264b514cc4849f91fb4414f639484c4181f1e5850d0d788ef276c851efa92859', - getArchiveChangeSkillTemplate: '206a22b6778e97c30da9145ef51fdad449b8c995538f6fc25752ef551a37b675', - getBulkArchiveChangeSkillTemplate: '2b74b1f73380ff32e35f580734780d843c6161a2748c39edb07f1e00453771b4', + getArchiveChangeSkillTemplate: 'fd45f7f6f4b33549763fbb6a9bd1f2fd9c61b10f5976a1a571037dde2ac2c3b5', + getBulkArchiveChangeSkillTemplate: '63a75524d151b1899d0de93a6bcfb8aae98086a3bb661296d181dac31e65f8f8', getOpsxSyncCommandTemplate: 'df0240a79f7b4943a54c7413ab088ee48f5bf5fe19f9347c170d695c8ec777a4', getVerifyChangeSkillTemplate: 'cab4db01b5d2b1243d63d90c53747d8b39e488c60f76eba3fe8b994467f69267', getOpsxArchiveCommandTemplate: '7dea65d0e2e17db366bb666ba6ae5e205ea02707b8c5c7707565200875c78916', getOpsxOnboardCommandTemplate: '16a68b8c9819e2a7bab013c3b49a3e49ea258b68c4e7f47f0d598e30815e0a80', - getOpsxBulkArchiveCommandTemplate: 'da7be1a7318f15b915f5aae8eb638797a8a24a31e5fc7fc0a2bad01bba137686', + getOpsxBulkArchiveCommandTemplate: '9008e579046d3f55821152948c5c73b958375c891c2e2eb18101506bc7429d94', getOpsxVerifyCommandTemplate: 'f01c0c0cef53be0956de52363d955d4ace131b1b2d77adf902f35fead9a1486d', getOpsxProposeSkillTemplate: '57fb556a060e2eb246b500922837af7573a6e100a6ed7dfaa7bd4ce0f5daffd3', getOpsxProposeCommandTemplate: '434cae3ee20835725bb1d2ccb9698310a850c5b95ed669ea15fc7a0125371c59', @@ -71,8 +71,8 @@ const EXPECTED_GENERATED_SKILL_CONTENT_HASHES: Record = { 'openspec-apply-change': '2f7a8e7a7528d9f8d89b508a8cbc909ba47bdff473db19317008d156b9ba5893', 'openspec-ff-change': 'ff3bd3eac427a1e50071ad7c70f73b556cffa3db43e90da2726e96849c3fc886', 'openspec-sync-specs': '74de778dd8a8fd4987a09621147358cc32505bb58110492ab2b4ffe7f35aa48f', - 'openspec-archive-change': '64b1611dd7aee04ca268820d1b193e8bf0a39ff3672ec6ba21fb0a1bcb1786c2', - 'openspec-bulk-archive-change': '49d410bda408c0411decd584be9c2355335e3b3db760fc6a0adcd82c172a280f', + 'openspec-archive-change': '23e1933afe94464f055dffb7bc8ee4f6f00764d8fcf249baa32ae04855b57fb9', + 'openspec-bulk-archive-change': '25a9e25e60740ad78c8a29b776992738db2f1ac45328d3948decaaa47e89a602', 'openspec-verify-change': '57693d22940f06080c6cf8d590ac2f48240d4a5e9ce7074dacd0f8d3c9945afa', 'openspec-onboard': '1d581c12d4928d751eb79de099e275dabe9c99fc15dc1f502abebd99ad7cb7d2', 'openspec-propose': '4638400113946f4f1ee9f0bd0e965aafb200bd89b64ec7f5406ef5e948e8e218', From 8c632a89a91982e41789ebc51b62e482a1bec8ec Mon Sep 17 00:00:00 2001 From: Henry Su Date: Mon, 27 Jul 2026 14:44:19 -0500 Subject: [PATCH 2/2] fix(archive): make command and bulk archive paths root-aware, synchronous, and verified --- skills/openspec-bulk-archive-change/SKILL.md | 29 ++++++--- .../templates/workflows/archive-change.ts | 4 +- .../workflows/bulk-archive-change.ts | 62 +++++++++++++------ .../templates/skill-templates-parity.test.ts | 53 ++++++++++++---- 4 files changed, 105 insertions(+), 43 deletions(-) diff --git a/skills/openspec-bulk-archive-change/SKILL.md b/skills/openspec-bulk-archive-change/SKILL.md index 0b2060489f..b5565b13ad 100644 --- a/skills/openspec-bulk-archive-change/SKILL.md +++ b/skills/openspec-bulk-archive-change/SKILL.md @@ -122,7 +122,7 @@ This skill allows you to batch-archive changes, handling spec conflicts intellig so match what the user picked rather than the wording above: - "Cancel" — stop, do not archive. Report that nothing was archived and skip the remaining steps. - The archive-everything option — proceed with every selected change - - The ready-only option — proceed with only the changes the step 6 table marks `Ready` or `Ready*`, and record the rest as Skipped in step 8c. If a `Ready*` change's conflict partner is skipped, re-derive that conflict's resolution using only the changes being archived. + - The ready-only option — proceed with only the changes the step 6 table marks `Ready` or `Ready*`, and record the rest as Skipped in step 8d. If a `Ready*` change's conflict partner is skipped, re-derive that conflict's resolution using only the changes being archived. - Anything else — ask again rather than archiving 8. **Execute archive for each confirmed change** @@ -130,11 +130,20 @@ This skill allows you to batch-archive changes, handling spec conflicts intellig Process changes in the determined order (respecting conflict resolution): a. **Sync specs** if delta specs exist: - - Use the openspec-sync-specs approach (agent-driven intelligent merge, or merge delta specs directly into main specs if sync workflow is not installed) - - For conflicts, apply in resolved order - - Track if sync was done + - Run the `openspec-sync-specs` workflow inline (agent-driven intelligent merge, or merge delta specs directly into main specs at `/openspec/specs//spec.md` if sync workflow is not installed) for each change, passing the delta spec analysis, and wait for it to finish. + - For conflicts, apply in resolved order. + - Do not delegate to a background task — step 8c would move `changeRoot` out from under a sync that is still reading it. - b. **Perform the archive**: + b. **Verify main specs before moving changeRoot**: + - Re-run the comparison against every capability that has a delta spec in `artifactPaths.specs.existingOutputPaths` against main spec at `/openspec/specs//spec.md` (use the store-aware `planningHome.root` from step 3 status JSON, not a hardcoded repo path). + - Verify that main specs are updated: + - ADDED requirements present + - MODIFIED requirements carrying 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 + - If sync failed or any capability does not match verification, report what differs and fail/skip moving that change's `changeRoot` — do not archive that change. `changeRoot` remains intact. + + c. **Perform the archive**: Target name: use the change name as-is when it already starts with a `YYYY-MM-DD-` prefix; otherwise prepend the current date as `YYYY-MM-DD-` (same rule as `openspec archive`). @@ -143,9 +152,9 @@ This skill allows you to batch-archive changes, handling spec conflicts intellig mv "" "/archive/" ``` - c. **Track outcome** for each change: + d. **Track outcome** for each change: - Success: archived successfully - - Failed: error during archive (record error) + - Failed: error during archive or spec verification (record error) - Skipped: user chose not to archive (if applicable) 9. **Display summary** @@ -178,7 +187,7 @@ This skill allows you to batch-archive changes, handling spec conflicts intellig Example 1: Only one implemented ```text -Conflict: specs/auth/spec.md touched by [add-oauth, add-jwt] +Conflict: /openspec/specs/auth/spec.md touched by [add-oauth, add-jwt] Checking add-oauth: - Delta adds "OAuth Provider Integration" requirement @@ -193,7 +202,7 @@ Resolution: Only add-oauth is implemented. Will sync add-oauth specs only. Example 2: Both implemented ```text -Conflict: specs/api/spec.md touched by [add-rest-api, add-graphql] +Conflict: /openspec/specs/api/spec.md touched by [add-rest-api, add-graphql] Checking add-rest-api (created 2026-01-10): - Delta adds "REST Endpoints" requirement @@ -257,3 +266,5 @@ No active changes found. Create a new change to get started. - Preserve .openspec.yaml when moving to archive - Archive directory target uses current date: YYYY-MM-DD-; a name that already starts with a `YYYY-MM-DD-` prefix is used as-is (never stack a second date) - If archive target exists, fail that change but continue with others +- If sync is requested, run the `openspec-sync-specs` workflow inline (agent-driven) for each change +- Never archive a change while a spec sync is still in flight — run the sync inline and verify main specs at `/openspec/specs//spec.md` before moving `changeRoot` diff --git a/src/core/templates/workflows/archive-change.ts b/src/core/templates/workflows/archive-change.ts index 24e68ac5a6..3fc6781814 100644 --- a/src/core/templates/workflows/archive-change.ts +++ b/src/core/templates/workflows/archive-change.ts @@ -207,7 +207,7 @@ ${STORE_SELECTION_GUIDANCE} - "Sync now" or "Sync anyway" — sync, then verify (below) - Anything else — ask again rather than archiving - 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. + To sync, run the \`/opsx:sync\` 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-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 @@ -309,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, run the \`openspec-sync-specs\` workflow inline (agent-driven) +- If sync is requested, run the \`/opsx:sync\` 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/bulk-archive-change.ts b/src/core/templates/workflows/bulk-archive-change.ts index 0045c3a8b8..e0b1c9dd01 100644 --- a/src/core/templates/workflows/bulk-archive-change.ts +++ b/src/core/templates/workflows/bulk-archive-change.ts @@ -124,7 +124,7 @@ ${STORE_SELECTION_GUIDANCE} so match what the user picked rather than the wording above: - "Cancel" — stop, do not archive. Report that nothing was archived and skip the remaining steps. - The archive-everything option — proceed with every selected change - - The ready-only option — proceed with only the changes the step 6 table marks \`Ready\` or \`Ready*\`, and record the rest as Skipped in step 8c. If a \`Ready*\` change's conflict partner is skipped, re-derive that conflict's resolution using only the changes being archived. + - The ready-only option — proceed with only the changes the step 6 table marks \`Ready\` or \`Ready*\`, and record the rest as Skipped in step 8d. If a \`Ready*\` change's conflict partner is skipped, re-derive that conflict's resolution using only the changes being archived. - Anything else — ask again rather than archiving 8. **Execute archive for each confirmed change** @@ -132,11 +132,20 @@ ${STORE_SELECTION_GUIDANCE} Process changes in the determined order (respecting conflict resolution): a. **Sync specs** if delta specs exist: - - Use the openspec-sync-specs approach (agent-driven intelligent merge, or merge delta specs directly into main specs if sync workflow is not installed) - - For conflicts, apply in resolved order - - Track if sync was done + - Run the \`openspec-sync-specs\` workflow inline (agent-driven intelligent merge, or merge delta specs directly into main specs at \`/openspec/specs//spec.md\` if sync workflow is not installed) for each change, passing the delta spec analysis, and wait for it to finish. + - For conflicts, apply in resolved order. + - Do not delegate to a background task — step 8c would move \`changeRoot\` out from under a sync that is still reading it. - b. **Perform the archive**: + b. **Verify main specs before moving changeRoot**: + - Re-run the comparison against every capability that has a delta spec in \`artifactPaths.specs.existingOutputPaths\` against main spec at \`/openspec/specs//spec.md\` (use the store-aware \`planningHome.root\` from step 3 status JSON, not a hardcoded repo path). + - Verify that main specs are updated: + - ADDED requirements present + - MODIFIED requirements carrying 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 + - If sync failed or any capability does not match verification, report what differs and fail/skip moving that change's \`changeRoot\` — do not archive that change. \`changeRoot\` remains intact. + + c. **Perform the archive**: Target name: use the change name as-is when it already starts with a \`YYYY-MM-DD-\` prefix; otherwise prepend the current date as \`YYYY-MM-DD-\` (same rule as \`openspec archive\`). @@ -145,9 +154,9 @@ ${STORE_SELECTION_GUIDANCE} mv "" "/archive/" \`\`\` - c. **Track outcome** for each change: + d. **Track outcome** for each change: - Success: archived successfully - - Failed: error during archive (record error) + - Failed: error during archive or spec verification (record error) - Skipped: user chose not to archive (if applicable) 9. **Display summary** @@ -180,7 +189,7 @@ ${STORE_SELECTION_GUIDANCE} Example 1: Only one implemented \`\`\`text -Conflict: specs/auth/spec.md touched by [add-oauth, add-jwt] +Conflict: /openspec/specs/auth/spec.md touched by [add-oauth, add-jwt] Checking add-oauth: - Delta adds "OAuth Provider Integration" requirement @@ -195,7 +204,7 @@ Resolution: Only add-oauth is implemented. Will sync add-oauth specs only. Example 2: Both implemented \`\`\`text -Conflict: specs/api/spec.md touched by [add-rest-api, add-graphql] +Conflict: /openspec/specs/api/spec.md touched by [add-rest-api, add-graphql] Checking add-rest-api (created 2026-01-10): - Delta adds "REST Endpoints" requirement @@ -258,7 +267,9 @@ No active changes found. Create a new change to get started. - Track and report all outcomes (success/skip/fail) - Preserve .openspec.yaml when moving to archive - Archive directory target uses current date: YYYY-MM-DD-; a name that already starts with a \`YYYY-MM-DD-\` prefix is used as-is (never stack a second date) -- If archive target exists, fail that change but continue with others`, +- If archive target exists, fail that change but continue with others +- If sync is requested, run the \`openspec-sync-specs\` workflow inline (agent-driven) for each change +- Never archive a change while a spec sync is still in flight — run the sync inline and verify main specs at \`/openspec/specs//spec.md\` before moving \`changeRoot\``, license: 'MIT', compatibility: 'Requires openspec CLI.', metadata: { author: 'openspec', version: '1.0' }, @@ -384,7 +395,7 @@ ${STORE_SELECTION_GUIDANCE} so match what the user picked rather than the wording above: - "Cancel" — stop, do not archive. Report that nothing was archived and skip the remaining steps. - The archive-everything option — proceed with every selected change - - The ready-only option — proceed with only the changes the step 6 table marks \`Ready\` or \`Ready*\`, and record the rest as Skipped in step 8c. If a \`Ready*\` change's conflict partner is skipped, re-derive that conflict's resolution using only the changes being archived. + - The ready-only option — proceed with only the changes the step 6 table marks \`Ready\` or \`Ready*\`, and record the rest as Skipped in step 8d. If a \`Ready*\` change's conflict partner is skipped, re-derive that conflict's resolution using only the changes being archived. - Anything else — ask again rather than archiving 8. **Execute archive for each confirmed change** @@ -392,11 +403,20 @@ ${STORE_SELECTION_GUIDANCE} Process changes in the determined order (respecting conflict resolution): a. **Sync specs** if delta specs exist: - - Use the openspec-sync-specs approach (agent-driven intelligent merge, or merge delta specs directly into main specs if sync workflow is not installed) - - For conflicts, apply in resolved order - - Track if sync was done + - Run the \`/opsx:sync\` workflow inline (agent-driven intelligent merge, or merge delta specs directly into main specs at \`/openspec/specs//spec.md\` if sync workflow is not installed) for each change, passing the delta spec analysis, and wait for it to finish. + - For conflicts, apply in resolved order. + - Do not delegate to a background task — step 8c would move \`changeRoot\` out from under a sync that is still reading it. + + b. **Verify main specs before moving changeRoot**: + - Re-run the comparison against every capability that has a delta spec in \`artifactPaths.specs.existingOutputPaths\` against main spec at \`/openspec/specs//spec.md\` (use the store-aware \`planningHome.root\` from step 3 status JSON, not a hardcoded repo path). + - Verify that main specs are updated: + - ADDED requirements present + - MODIFIED requirements carrying 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 + - If sync failed or any capability does not match verification, report what differs and fail/skip moving that change's \`changeRoot\` — do not archive that change. \`changeRoot\` remains intact. - b. **Perform the archive**: + c. **Perform the archive**: Target name: use the change name as-is when it already starts with a \`YYYY-MM-DD-\` prefix; otherwise prepend the current date as \`YYYY-MM-DD-\` (same rule as \`openspec archive\`). @@ -405,9 +425,9 @@ ${STORE_SELECTION_GUIDANCE} mv "" "/archive/" \`\`\` - c. **Track outcome** for each change: + d. **Track outcome** for each change: - Success: archived successfully - - Failed: error during archive (record error) + - Failed: error during archive or spec verification (record error) - Skipped: user chose not to archive (if applicable) 9. **Display summary** @@ -440,7 +460,7 @@ ${STORE_SELECTION_GUIDANCE} Example 1: Only one implemented \`\`\`text -Conflict: specs/auth/spec.md touched by [add-oauth, add-jwt] +Conflict: /openspec/specs/auth/spec.md touched by [add-oauth, add-jwt] Checking add-oauth: - Delta adds "OAuth Provider Integration" requirement @@ -455,7 +475,7 @@ Resolution: Only add-oauth is implemented. Will sync add-oauth specs only. Example 2: Both implemented \`\`\`text -Conflict: specs/api/spec.md touched by [add-rest-api, add-graphql] +Conflict: /openspec/specs/api/spec.md touched by [add-rest-api, add-graphql] Checking add-rest-api (created 2026-01-10): - Delta adds "REST Endpoints" requirement @@ -518,6 +538,8 @@ No active changes found. Create a new change to get started. - Track and report all outcomes (success/skip/fail) - Preserve .openspec.yaml when moving to archive - Archive directory target uses current date: YYYY-MM-DD-; a name that already starts with a \`YYYY-MM-DD-\` prefix is used as-is (never stack a second date) -- If archive target exists, fail that change but continue with others` +- If archive target exists, fail that change but continue with others +- If sync is requested, run the \`/opsx:sync\` workflow inline (agent-driven) for each change +- Never archive a change while a spec sync is still in flight — run the sync inline and verify main specs at \`/openspec/specs//spec.md\` before moving \`changeRoot\`` }; } diff --git a/test/core/templates/skill-templates-parity.test.ts b/test/core/templates/skill-templates-parity.test.ts index 039b56022c..706389c133 100644 --- a/test/core/templates/skill-templates-parity.test.ts +++ b/test/core/templates/skill-templates-parity.test.ts @@ -50,12 +50,12 @@ const EXPECTED_FUNCTION_HASHES: Record = { getOpsxApplyCommandTemplate: '147408d7085b468981a400cc725804252c3fd84e519c57c5f6f83562e32606ee', getOpsxFfCommandTemplate: '264b514cc4849f91fb4414f639484c4181f1e5850d0d788ef276c851efa92859', getArchiveChangeSkillTemplate: 'fd45f7f6f4b33549763fbb6a9bd1f2fd9c61b10f5976a1a571037dde2ac2c3b5', - getBulkArchiveChangeSkillTemplate: '63a75524d151b1899d0de93a6bcfb8aae98086a3bb661296d181dac31e65f8f8', + getBulkArchiveChangeSkillTemplate: '8ab1ee4a57d52d88f56e706a517af166e48b6d7016b57806b517f4750eb930f3', getOpsxSyncCommandTemplate: 'df0240a79f7b4943a54c7413ab088ee48f5bf5fe19f9347c170d695c8ec777a4', getVerifyChangeSkillTemplate: 'cab4db01b5d2b1243d63d90c53747d8b39e488c60f76eba3fe8b994467f69267', - getOpsxArchiveCommandTemplate: '7dea65d0e2e17db366bb666ba6ae5e205ea02707b8c5c7707565200875c78916', + getOpsxArchiveCommandTemplate: '2f337c6dfb5f988cf994e5637433c5a90d5d3b66ec20ecc1a2b723050a872efd', getOpsxOnboardCommandTemplate: '16a68b8c9819e2a7bab013c3b49a3e49ea258b68c4e7f47f0d598e30815e0a80', - getOpsxBulkArchiveCommandTemplate: '9008e579046d3f55821152948c5c73b958375c891c2e2eb18101506bc7429d94', + getOpsxBulkArchiveCommandTemplate: 'ec31cb4c5530131ab19c49396d8bc44143c11ae10098d340dfc7aa8596d45559', getOpsxVerifyCommandTemplate: 'f01c0c0cef53be0956de52363d955d4ace131b1b2d77adf902f35fead9a1486d', getOpsxProposeSkillTemplate: '57fb556a060e2eb246b500922837af7573a6e100a6ed7dfaa7bd4ce0f5daffd3', getOpsxProposeCommandTemplate: '434cae3ee20835725bb1d2ccb9698310a850c5b95ed669ea15fc7a0125371c59', @@ -72,7 +72,7 @@ const EXPECTED_GENERATED_SKILL_CONTENT_HASHES: Record = { 'openspec-ff-change': 'ff3bd3eac427a1e50071ad7c70f73b556cffa3db43e90da2726e96849c3fc886', 'openspec-sync-specs': '74de778dd8a8fd4987a09621147358cc32505bb58110492ab2b4ffe7f35aa48f', 'openspec-archive-change': '23e1933afe94464f055dffb7bc8ee4f6f00764d8fcf249baa32ae04855b57fb9', - 'openspec-bulk-archive-change': '25a9e25e60740ad78c8a29b776992738db2f1ac45328d3948decaaa47e89a602', + 'openspec-bulk-archive-change': 'd5005667213c56a800d685f5a1703c0823d95feeb2ffc45097d13b250757750e', 'openspec-verify-change': '57693d22940f06080c6cf8d590ac2f48240d4a5e9ce7074dacd0f8d3c9945afa', 'openspec-onboard': '1d581c12d4928d751eb79de099e275dabe9c99fc15dc1f502abebd99ad7cb7d2', 'openspec-propose': '4638400113946f4f1ee9f0bd0e965aafb200bd89b64ec7f5406ef5e948e8e218', @@ -224,28 +224,57 @@ describe('skill templates split parity', () => { const generatedSkill = generateSkillContent(getArchiveChangeSkillTemplate(), 'PARITY-BASELINE'); const commandContent = getOpsxArchiveCommandTemplate().content; + // The single archive skill references openspec-sync-specs; opsx command references /opsx:sync. + expect(generatedSkill, 'skill').toContain('run the `openspec-sync-specs` workflow inline'); + expect(commandContent, 'opsx command').toContain('run the `/opsx:sync` workflow inline'); + 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). + // Verification must follow delta semantics. 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. + // Verification is bound to the delta specs on disk, not to whatever the sync reports it touched. expect(content, variant).toContain('not only the ones the sync reports it touched'); + + // Main spec paths are store-root aware + expect(content, variant).toContain('/openspec/specs//spec.md'); + } + }); + + 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; + + // The bulk archive skill references openspec-sync-specs; opsx command references /opsx:sync. + expect(generatedSkill, 'bulk skill').toContain('run the `openspec-sync-specs` workflow inline'); + expect(commandContent, 'bulk opsx command').toContain('run the `/opsx:sync` workflow inline'); + + const variants: Array<[string, string]> = [ + ['bulk skill', generatedSkill], + ['bulk opsx command', commandContent], + ]; + + for (const [variant, content] of variants) { + expect(content, variant).toContain('Do not delegate to a background task'); + expect(content, variant).toContain('Never archive a change while a spec sync is still in flight'); + expect(content, variant).toContain('Verify main specs before moving changeRoot'); + + // Verification must follow delta semantics. + expect(content, variant).toContain('MODIFIED requirements carrying 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'); + + // Main spec paths are store-root aware + expect(content, variant).toContain('/openspec/specs//spec.md'); } });