diff --git a/.changeset/fix-template-archive-date-dedup.md b/.changeset/fix-template-archive-date-dedup.md new file mode 100644 index 0000000000..866661fd3b --- /dev/null +++ b/.changeset/fix-template-archive-date-dedup.md @@ -0,0 +1,7 @@ +--- +"@fission-ai/openspec": patch +--- + +### Bug Fixes + +- **Archive workflow templates no longer teach agents to stack a second date prefix** — the `openspec-archive-change` and `openspec-bulk-archive-change` skill/command templates (and the onboarding walkthrough's archived-path example) now mirror the `openspec archive` rule: a change whose name already starts with a `YYYY-MM-DD-` prefix is archived under its own name, while other names get the current date prepended as before. Previously an agent following the workflow instructions on a change named `2026-07-04-voice-copilot-v1` produced `archive/2026-07-07-2026-07-04-voice-copilot-v1`, whatever the CLI did. diff --git a/openspec/specs/opsx-archive-skill/spec.md b/openspec/specs/opsx-archive-skill/spec.md index 95ba9dc2d0..a6256b37b7 100644 --- a/openspec/specs/opsx-archive-skill/spec.md +++ b/openspec/specs/opsx-archive-skill/spec.md @@ -15,7 +15,7 @@ The system SHALL provide an `/opsx:archive` skill that archives completed change - **WHEN** agent executes `/opsx:archive` with a change name - **AND** all artifacts in the schema are complete - **AND** all tasks are complete -- **THEN** the agent moves the change to `openspec/changes/archive/YYYY-MM-DD-/` +- **THEN** the agent moves the change to `openspec/changes/archive//` - **AND** displays success message with archived location #### Scenario: Change selection prompt @@ -94,7 +94,7 @@ The skill SHALL move the change to the archive folder with date prefix. - **WHEN** archiving a change - **THEN** create `archive/` directory if it doesn't exist -- **AND** generate target name as `YYYY-MM-DD-` using current date +- **AND** generate target name as `YYYY-MM-DD-` using current date, keeping the name as-is when it already starts with a `YYYY-MM-DD-` prefix - **AND** move entire change directory to archive location - **AND** preserve `.openspec.yaml` file in archived change diff --git a/skills/openspec-archive-change/SKILL.md b/skills/openspec-archive-change/SKILL.md index e198c0099c..b531e87641 100644 --- a/skills/openspec-archive-change/SKILL.md +++ b/skills/openspec-archive-change/SKILL.md @@ -89,14 +89,14 @@ Archive a completed change in the experimental workflow. mkdir -p "/archive" ``` - Generate target name using current date: `YYYY-MM-DD-` + Generate the 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-`. Never stack a second date (same rule as `openspec archive`). **Check if target already exists:** - If yes: Fail with error, suggest renaming existing archive or using different date - If no: Move `changeRoot` to the archive directory ```bash - mv "" "/archive/YYYY-MM-DD-" + mv "" "/archive/" ``` 6. **Display summary** @@ -115,7 +115,7 @@ Archive a completed change in the experimental workflow. **Change:** **Schema:** -**Archived to:** the archive path derived from `planningHome.changesDir`/YYYY-MM-DD-/ +**Archived to:** the archive path derived from `planningHome.changesDir`// **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")> diff --git a/skills/openspec-bulk-archive-change/SKILL.md b/skills/openspec-bulk-archive-change/SKILL.md index 6076216ec9..8ef031b931 100644 --- a/skills/openspec-bulk-archive-change/SKILL.md +++ b/skills/openspec-bulk-archive-change/SKILL.md @@ -128,9 +128,12 @@ This skill allows you to batch-archive changes, handling spec conflicts intellig - Track if sync was done b. **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`). + ```bash mkdir -p "/archive" - mv "" "/archive/YYYY-MM-DD-" + mv "" "/archive/" ``` c. **Track outcome** for each change: @@ -203,8 +206,8 @@ then add-graphql specs (chronological order, newer takes precedence). ## Bulk Archive Complete Archived N changes: -- -> archive/YYYY-MM-DD-/ -- -> archive/YYYY-MM-DD-/ +- -> archive// +- -> archive// Spec sync summary: - N delta specs synced to main specs @@ -217,7 +220,7 @@ Spec sync summary: ## Bulk Archive Complete (partial) Archived N changes: -- -> archive/YYYY-MM-DD-/ +- -> archive// Skipped M changes: - (user chose not to archive incomplete) @@ -244,5 +247,5 @@ No active changes found. Create a new change to get started. - Use single confirmation for entire batch - Track and report all outcomes (success/skip/fail) - Preserve .openspec.yaml when moving to archive -- Archive directory target uses current date: YYYY-MM-DD- +- 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 diff --git a/skills/openspec-onboard/SKILL.md b/skills/openspec-onboard/SKILL.md index a06f0fd26b..7722c3414d 100644 --- a/skills/openspec-onboard/SKILL.md +++ b/skills/openspec-onboard/SKILL.md @@ -437,7 +437,7 @@ openspec archive "" **SHOW:** ``` -Archived to: `/archive/YYYY-MM-DD-/` +Archived to: `/archive//` (the target name prepends today's date, unless the name already starts with a `YYYY-MM-DD-` prefix — then it is kept as-is, no second date) The change is now part of your project's history. The code is in your codebase, the decision record is preserved. ``` diff --git a/src/core/templates/workflows/archive-change.ts b/src/core/templates/workflows/archive-change.ts index 564a7fe0c7..d0c1e6fd14 100644 --- a/src/core/templates/workflows/archive-change.ts +++ b/src/core/templates/workflows/archive-change.ts @@ -91,14 +91,14 @@ ${STORE_SELECTION_GUIDANCE} mkdir -p "/archive" \`\`\` - Generate target name using current date: \`YYYY-MM-DD-\` + Generate the 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-\`. Never stack a second date (same rule as \`openspec archive\`). **Check if target already exists:** - If yes: Fail with error, suggest renaming existing archive or using different date - If no: Move \`changeRoot\` to the archive directory \`\`\`bash - mv "" "/archive/YYYY-MM-DD-" + mv "" "/archive/" \`\`\` 6. **Display summary** @@ -117,7 +117,7 @@ ${STORE_SELECTION_GUIDANCE} **Change:** **Schema:** -**Archived to:** the archive path derived from \`planningHome.changesDir\`/YYYY-MM-DD-/ +**Archived to:** the archive path derived from \`planningHome.changesDir\`// **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")> @@ -224,14 +224,14 @@ ${STORE_SELECTION_GUIDANCE} mkdir -p "/archive" \`\`\` - Generate target name using current date: \`YYYY-MM-DD-\` + Generate the 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-\`. Never stack a second date (same rule as \`openspec archive\`). **Check if target already exists:** - If yes: Fail with error, suggest renaming existing archive or using different date - If no: Move \`changeRoot\` to the archive directory \`\`\`bash - mv "" "/archive/YYYY-MM-DD-" + mv "" "/archive/" \`\`\` 6. **Display summary** @@ -250,7 +250,7 @@ ${STORE_SELECTION_GUIDANCE} **Change:** **Schema:** -**Archived to:** the archive path derived from \`planningHome.changesDir\`/YYYY-MM-DD-/ +**Archived to:** the archive path derived from \`planningHome.changesDir\`// **Specs:** ✓ Synced to main specs All artifacts complete. All tasks complete. @@ -263,7 +263,7 @@ All artifacts complete. All tasks complete. **Change:** **Schema:** -**Archived to:** the archive path derived from \`planningHome.changesDir\`/YYYY-MM-DD-/ +**Archived to:** the archive path derived from \`planningHome.changesDir\`// **Specs:** No delta specs All artifacts complete. All tasks complete. @@ -276,7 +276,7 @@ All artifacts complete. All tasks complete. **Change:** **Schema:** -**Archived to:** the archive path derived from \`planningHome.changesDir\`/YYYY-MM-DD-/ +**Archived to:** the archive path derived from \`planningHome.changesDir\`// **Specs:** Sync skipped (user chose to skip) **Warnings:** @@ -293,7 +293,7 @@ Review the archive if this was not intentional. ## Archive Failed **Change:** -**Target:** the archive path derived from \`planningHome.changesDir\`/YYYY-MM-DD-/ +**Target:** the archive path derived from \`planningHome.changesDir\`// Target archive directory already exists. diff --git a/src/core/templates/workflows/bulk-archive-change.ts b/src/core/templates/workflows/bulk-archive-change.ts index 607796818f..3acc0b2add 100644 --- a/src/core/templates/workflows/bulk-archive-change.ts +++ b/src/core/templates/workflows/bulk-archive-change.ts @@ -130,9 +130,12 @@ ${STORE_SELECTION_GUIDANCE} - Track if sync was done b. **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\`). + \`\`\`bash mkdir -p "/archive" - mv "" "/archive/YYYY-MM-DD-" + mv "" "/archive/" \`\`\` c. **Track outcome** for each change: @@ -205,8 +208,8 @@ then add-graphql specs (chronological order, newer takes precedence). ## Bulk Archive Complete Archived N changes: -- -> archive/YYYY-MM-DD-/ -- -> archive/YYYY-MM-DD-/ +- -> archive// +- -> archive// Spec sync summary: - N delta specs synced to main specs @@ -219,7 +222,7 @@ Spec sync summary: ## Bulk Archive Complete (partial) Archived N changes: -- -> archive/YYYY-MM-DD-/ +- -> archive// Skipped M changes: - (user chose not to archive incomplete) @@ -246,7 +249,7 @@ No active changes found. Create a new change to get started. - Use single confirmation for entire batch - Track and report all outcomes (success/skip/fail) - Preserve .openspec.yaml when moving to archive -- Archive directory target uses current date: YYYY-MM-DD- +- 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`, license: 'MIT', compatibility: 'Requires openspec CLI.', @@ -379,9 +382,12 @@ ${STORE_SELECTION_GUIDANCE} - Track if sync was done b. **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\`). + \`\`\`bash mkdir -p "/archive" - mv "" "/archive/YYYY-MM-DD-" + mv "" "/archive/" \`\`\` c. **Track outcome** for each change: @@ -454,8 +460,8 @@ then add-graphql specs (chronological order, newer takes precedence). ## Bulk Archive Complete Archived N changes: -- -> archive/YYYY-MM-DD-/ -- -> archive/YYYY-MM-DD-/ +- -> archive// +- -> archive// Spec sync summary: - N delta specs synced to main specs @@ -468,7 +474,7 @@ Spec sync summary: ## Bulk Archive Complete (partial) Archived N changes: -- -> archive/YYYY-MM-DD-/ +- -> archive// Skipped M changes: - (user chose not to archive incomplete) @@ -495,7 +501,7 @@ No active changes found. Create a new change to get started. - Use single confirmation for entire batch - Track and report all outcomes (success/skip/fail) - Preserve .openspec.yaml when moving to archive -- Archive directory target uses current date: YYYY-MM-DD- +- 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` }; } diff --git a/src/core/templates/workflows/onboard.ts b/src/core/templates/workflows/onboard.ts index 96f1b943bc..d175b08322 100644 --- a/src/core/templates/workflows/onboard.ts +++ b/src/core/templates/workflows/onboard.ts @@ -447,7 +447,7 @@ openspec archive "" **SHOW:** \`\`\` -Archived to: \`/archive/YYYY-MM-DD-/\` +Archived to: \`/archive//\` (the target name prepends today's date, unless the name already starts with a \`YYYY-MM-DD-\` prefix — then it is kept as-is, no second date) The change is now part of your project's history. The code is in your codebase, the decision record is preserved. \`\`\` diff --git a/test/core/templates/skill-templates-parity.test.ts b/test/core/templates/skill-templates-parity.test.ts index 0d8b783852..ec3f5ca6aa 100644 --- a/test/core/templates/skill-templates-parity.test.ts +++ b/test/core/templates/skill-templates-parity.test.ts @@ -43,19 +43,19 @@ const EXPECTED_FUNCTION_HASHES: Record = { getApplyChangeSkillTemplate: '0f5a15fc7fb9ad6059a5643d0e01365d27642637a4aaebf182f9eabb45348197', getFfChangeSkillTemplate: '20ebb682ba89809a100cd4985c074908df5bada2bd649ca1b0f4059a63a1c728', getSyncSpecsSkillTemplate: 'dc07ea0312687f3edc602329c889dbbab737c6d79327eb7a723553d346b43433', - getOnboardSkillTemplate: 'e871d8ce172bb805ae62a7611aee7a3154d89414f427ad5ef31721c903f13002', + getOnboardSkillTemplate: 'bc2216b72724b01c3a733e63b8bf4aff457f561c0e9ff7288bdacc39780a37a7', getOpsxExploreCommandTemplate: '37e53590aae7ac6621d4393aa80a5b8af21881323887fa924ed329199fda27e0', getOpsxNewCommandTemplate: '57c600cce318d16b9b4308a18d0d983ea3c0673034e606a7cceec07b4c705e87', getOpsxContinueCommandTemplate: 'f63964fab7720ede097aa48808baff196c391b962930ca960459205c724800e5', getOpsxApplyCommandTemplate: 'daeb507206707169de73c828e199648dde5732cbc17791ef2a027adffd028574', getOpsxFfCommandTemplate: 'b859b1955cda6012877ae7f9ec6980e468f2e949a3838dfcdebc17209d133749', - getArchiveChangeSkillTemplate: 'a8f1d9cb06c20c7335ac35826dd09bfadead75ef6d624d359912734f74232cbc', - getBulkArchiveChangeSkillTemplate: 'f675122bce3ef583b245352abedecf50ff4043e45bea6bac091885f83c7b6362', + getArchiveChangeSkillTemplate: 'b04eccde2c57af4bc484fa7279fa873ad1d46474eb024467d68e784d8b985c18', + getBulkArchiveChangeSkillTemplate: 'f31d17602c274a3fc24d688fb368156618cd31e07762a267d2c506c63b4b4760', getOpsxSyncCommandTemplate: '98b20e00da5c588ff83ed6e6f0e959dfc540349090fb3f5792ea030d099b8169', getVerifyChangeSkillTemplate: 'cab4db01b5d2b1243d63d90c53747d8b39e488c60f76eba3fe8b994467f69267', - getOpsxArchiveCommandTemplate: '9d14e1ea23ae8be8971fafa1d6a4d4717a8a7b922b6e76c6fb07aa568a420632', - getOpsxOnboardCommandTemplate: '0673f34a0f81fd173bcfb8c3ac83e2b1c617f7b7564e24e5298d3bd5665a05a9', - getOpsxBulkArchiveCommandTemplate: 'd0d84040bcbd44e89ac525bb21100bee7befb3604e51095bfa65b8453d85290c', + getOpsxArchiveCommandTemplate: '8c113e2a8bca36fecd0e2152ae262fbfbef508e81378838e15d31308fb069b57', + getOpsxOnboardCommandTemplate: '9430a0fb6530791ab720e068f4b172bc3dfc4e96a1ae29102bee0b92c2afe7b5', + getOpsxBulkArchiveCommandTemplate: '22dde4864ec494eee774a46fe5c0c6a68f4ca9ff67272c3177a5d4f5c2be07b7', getOpsxVerifyCommandTemplate: 'f01c0c0cef53be0956de52363d955d4ace131b1b2d77adf902f35fead9a1486d', getOpsxProposeSkillTemplate: '59197064a46c53264b62925a1c725af4ebe7caf9f0eaed4101990b7c13a40db1', getOpsxProposeCommandTemplate: '04f808a36e850b9cdbc4f943ef324a9fd2b1b0cc59b92f127ab6cc452d66cc4e', @@ -71,10 +71,10 @@ const EXPECTED_GENERATED_SKILL_CONTENT_HASHES: Record = { 'openspec-apply-change': '09c0e1cdf5ccc82416d0969d6bd715cc70616bdbc3531358a5c36057f78be55a', 'openspec-ff-change': '0c82830cd9bc98f86eb56b63ddaabe2bf5d35fe25b6c40a7059311aee2c8acac', 'openspec-sync-specs': 'b3f694ab81956d05126b089fe82dea78dec21788978bb9651485f996aee96740', - 'openspec-archive-change': '4679a077d34016bf38f0d0aa5432b53ea83ae82c2c5fec6dcb7dc15571ee8ac6', - 'openspec-bulk-archive-change': '545b9528df52fbb0b4898405b42a2ce10416678d469d20cf597d022fa6e16e3b', + 'openspec-archive-change': 'b24d326662ef58809de4464960440713748b9a281323357facdca24af52014e7', + 'openspec-bulk-archive-change': '98c682899a6fd4c83e71b790b27d6d4ccf832e51c0e754119537992a469c75ec', 'openspec-verify-change': '57693d22940f06080c6cf8d590ac2f48240d4a5e9ce7074dacd0f8d3c9945afa', - 'openspec-onboard': 'b1b6fc9a1b3ff64dafe9b8c39a761ee1bd001b542d47b4e4deaf058e0aa21256', + 'openspec-onboard': '76225d10352454a304e56566997811d16f91de1b37653816f2bc5d8ec976febc', 'openspec-propose': '024db4bce28d9a4d7b25fa92525da6fc701a64ac07dfdcf777d286c95b5281b5', 'openspec-update-change': '77ff4d1f1cd08a57649cce1f25e0ebc4f55d6d032dfde5c301d1b479561b72fa', }; @@ -238,4 +238,33 @@ describe('skill templates split parity', () => { expect(content, variant).toContain('not only the ones the sync reports it touched'); } }); + + // The archive instructions must mirror `openspec archive`'s date-prefix + // rule (#1316): a change already named with a `YYYY-MM-DD-` prefix keeps + // its name, so archived names never stack dates. Guard the caveat, the + // literal `mv` target, and the success-summary examples an agent would + // copy verbatim (#1317). + it('never instructs stacking a date prefix on an already-dated change (#1317)', () => { + const archiveInstructions: Array<[string, string]> = [ + ['openspec-archive-change', getArchiveChangeSkillTemplate().instructions], + ['openspec-bulk-archive-change', getBulkArchiveChangeSkillTemplate().instructions], + ['openspec-onboard', getOnboardSkillTemplate().instructions], + ['opsx-archive', getOpsxArchiveCommandTemplate().content], + ['opsx-bulk-archive', getOpsxBulkArchiveCommandTemplate().content], + ['opsx-onboard', getOpsxOnboardCommandTemplate().content], + ]; + + for (const [id, text] of archiveInstructions) { + expect(text, id).toContain('already starts with a `YYYY-MM-DD-` prefix'); + + // Every archive path an agent reproduces must name the derived target, + // never a hardcoded date. + expect(text, id).toContain(''); + + // Discriminator: a `YYYY-MM-DD-` after a path separator belongs to a + // literal archive path the agent copies verbatim. The rule statements + // only name the prefix, never place it in a path, so they stay legal. + expect(text, id).not.toMatch(/\/YYYY-MM-DD-/); + } + }); });