Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions .changeset/fix-template-archive-date-dedup.md
Original file line number Diff line number Diff line change
@@ -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.
4 changes: 2 additions & 2 deletions openspec/specs/opsx-archive-skill/spec.md
Original file line number Diff line number Diff line change
Expand Up @@ -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-<name>/`
- **THEN** the agent moves the change to `openspec/changes/archive/<target-name>/`
- **AND** displays success message with archived location

#### Scenario: Change selection prompt
Expand Down Expand Up @@ -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-<change-name>` using current date
- **AND** generate target name as `YYYY-MM-DD-<change-name>` using current date, keeping the name as-is when it already starts with a `YYYY-MM-DD-` prefix
Comment thread
coderabbitai[bot] marked this conversation as resolved.
- **AND** move entire change directory to archive location
- **AND** preserve `.openspec.yaml` file in archived change

Expand Down
6 changes: 3 additions & 3 deletions skills/openspec-archive-change/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -89,14 +89,14 @@ Archive a completed change in the experimental workflow.
mkdir -p "<planningHome.changesDir>/archive"
```

Generate target name using current date: `YYYY-MM-DD-<change-name>`
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-<change-name>`. 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 "<changeRoot>" "<planningHome.changesDir>/archive/YYYY-MM-DD-<name>"
mv "<changeRoot>" "<planningHome.changesDir>/archive/<target-name>"
```

6. **Display summary**
Expand All @@ -115,7 +115,7 @@ Archive a completed change in the experimental workflow.

**Change:** <change-name>
**Schema:** <schema-name>
**Archived to:** the archive path derived from `planningHome.changesDir`/YYYY-MM-DD-<name>/
**Archived to:** the archive path derived from `planningHome.changesDir`/<target-name>/
**Specs:** <"✓ Synced to main specs" only if the step 4 verification passed; otherwise "No delta specs" or "Sync skipped">

<"All artifacts complete. All tasks complete." — or, if archived with warnings, list them instead (e.g. "Archived with 2 incomplete tasks")>
Expand Down
13 changes: 8 additions & 5 deletions skills/openspec-bulk-archive-change/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -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-<name>` (same rule as `openspec archive`).

```bash
mkdir -p "<planningHome.changesDir>/archive"
mv "<changeRoot>" "<planningHome.changesDir>/archive/YYYY-MM-DD-<name>"
mv "<changeRoot>" "<planningHome.changesDir>/archive/<target-name>"
```

c. **Track outcome** for each change:
Expand Down Expand Up @@ -203,8 +206,8 @@ then add-graphql specs (chronological order, newer takes precedence).
## Bulk Archive Complete

Archived N changes:
- <change-1> -> archive/YYYY-MM-DD-<change-1>/
- <change-2> -> archive/YYYY-MM-DD-<change-2>/
- <change-1> -> archive/<target-name-1>/
- <change-2> -> archive/<target-name-2>/

Spec sync summary:
- N delta specs synced to main specs
Expand All @@ -217,7 +220,7 @@ Spec sync summary:
## Bulk Archive Complete (partial)

Archived N changes:
- <change-1> -> archive/YYYY-MM-DD-<change-1>/
- <change-1> -> archive/<target-name-1>/

Skipped M changes:
- <change-2> (user chose not to archive incomplete)
Expand All @@ -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-<name>
- Archive directory target uses current date: YYYY-MM-DD-<name>; 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
2 changes: 1 addition & 1 deletion skills/openspec-onboard/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -437,7 +437,7 @@ openspec archive "<name>"

**SHOW:**
```
Archived to: `<planningHome.changesDir>/archive/YYYY-MM-DD-<name>/`
Archived to: `<planningHome.changesDir>/archive/<target-name>/` (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.
```
Expand Down
18 changes: 9 additions & 9 deletions src/core/templates/workflows/archive-change.ts
Original file line number Diff line number Diff line change
Expand Up @@ -91,14 +91,14 @@ ${STORE_SELECTION_GUIDANCE}
mkdir -p "<planningHome.changesDir>/archive"
\`\`\`

Generate target name using current date: \`YYYY-MM-DD-<change-name>\`
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-<change-name>\`. 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 "<changeRoot>" "<planningHome.changesDir>/archive/YYYY-MM-DD-<name>"
mv "<changeRoot>" "<planningHome.changesDir>/archive/<target-name>"
\`\`\`

6. **Display summary**
Expand All @@ -117,7 +117,7 @@ ${STORE_SELECTION_GUIDANCE}

**Change:** <change-name>
**Schema:** <schema-name>
**Archived to:** the archive path derived from \`planningHome.changesDir\`/YYYY-MM-DD-<name>/
**Archived to:** the archive path derived from \`planningHome.changesDir\`/<target-name>/
**Specs:** <"✓ Synced to main specs" only if the step 4 verification passed; otherwise "No delta specs" or "Sync skipped">

<"All artifacts complete. All tasks complete." — or, if archived with warnings, list them instead (e.g. "Archived with 2 incomplete tasks")>
Expand Down Expand Up @@ -224,14 +224,14 @@ ${STORE_SELECTION_GUIDANCE}
mkdir -p "<planningHome.changesDir>/archive"
\`\`\`

Generate target name using current date: \`YYYY-MM-DD-<change-name>\`
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-<change-name>\`. 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 "<changeRoot>" "<planningHome.changesDir>/archive/YYYY-MM-DD-<name>"
mv "<changeRoot>" "<planningHome.changesDir>/archive/<target-name>"
\`\`\`

6. **Display summary**
Expand All @@ -250,7 +250,7 @@ ${STORE_SELECTION_GUIDANCE}

**Change:** <change-name>
**Schema:** <schema-name>
**Archived to:** the archive path derived from \`planningHome.changesDir\`/YYYY-MM-DD-<name>/
**Archived to:** the archive path derived from \`planningHome.changesDir\`/<target-name>/
**Specs:** ✓ Synced to main specs

All artifacts complete. All tasks complete.
Expand All @@ -263,7 +263,7 @@ All artifacts complete. All tasks complete.

**Change:** <change-name>
**Schema:** <schema-name>
**Archived to:** the archive path derived from \`planningHome.changesDir\`/YYYY-MM-DD-<name>/
**Archived to:** the archive path derived from \`planningHome.changesDir\`/<target-name>/
**Specs:** No delta specs

All artifacts complete. All tasks complete.
Expand All @@ -276,7 +276,7 @@ All artifacts complete. All tasks complete.

**Change:** <change-name>
**Schema:** <schema-name>
**Archived to:** the archive path derived from \`planningHome.changesDir\`/YYYY-MM-DD-<name>/
**Archived to:** the archive path derived from \`planningHome.changesDir\`/<target-name>/
**Specs:** Sync skipped (user chose to skip)

**Warnings:**
Expand All @@ -293,7 +293,7 @@ Review the archive if this was not intentional.
## Archive Failed

**Change:** <change-name>
**Target:** the archive path derived from \`planningHome.changesDir\`/YYYY-MM-DD-<name>/
**Target:** the archive path derived from \`planningHome.changesDir\`/<target-name>/

Target archive directory already exists.

Expand Down
26 changes: 16 additions & 10 deletions src/core/templates/workflows/bulk-archive-change.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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-<name>\` (same rule as \`openspec archive\`).

\`\`\`bash
mkdir -p "<planningHome.changesDir>/archive"
mv "<changeRoot>" "<planningHome.changesDir>/archive/YYYY-MM-DD-<name>"
mv "<changeRoot>" "<planningHome.changesDir>/archive/<target-name>"
\`\`\`

c. **Track outcome** for each change:
Expand Down Expand Up @@ -205,8 +208,8 @@ then add-graphql specs (chronological order, newer takes precedence).
## Bulk Archive Complete

Archived N changes:
- <change-1> -> archive/YYYY-MM-DD-<change-1>/
- <change-2> -> archive/YYYY-MM-DD-<change-2>/
- <change-1> -> archive/<target-name-1>/
- <change-2> -> archive/<target-name-2>/

Spec sync summary:
- N delta specs synced to main specs
Expand All @@ -219,7 +222,7 @@ Spec sync summary:
## Bulk Archive Complete (partial)

Archived N changes:
- <change-1> -> archive/YYYY-MM-DD-<change-1>/
- <change-1> -> archive/<target-name-1>/

Skipped M changes:
- <change-2> (user chose not to archive incomplete)
Expand All @@ -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-<name>
- Archive directory target uses current date: YYYY-MM-DD-<name>; 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.',
Expand Down Expand Up @@ -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-<name>\` (same rule as \`openspec archive\`).

\`\`\`bash
mkdir -p "<planningHome.changesDir>/archive"
mv "<changeRoot>" "<planningHome.changesDir>/archive/YYYY-MM-DD-<name>"
mv "<changeRoot>" "<planningHome.changesDir>/archive/<target-name>"
\`\`\`

c. **Track outcome** for each change:
Expand Down Expand Up @@ -454,8 +460,8 @@ then add-graphql specs (chronological order, newer takes precedence).
## Bulk Archive Complete

Archived N changes:
- <change-1> -> archive/YYYY-MM-DD-<change-1>/
- <change-2> -> archive/YYYY-MM-DD-<change-2>/
- <change-1> -> archive/<target-name-1>/
- <change-2> -> archive/<target-name-2>/

Spec sync summary:
- N delta specs synced to main specs
Expand All @@ -468,7 +474,7 @@ Spec sync summary:
## Bulk Archive Complete (partial)

Archived N changes:
- <change-1> -> archive/YYYY-MM-DD-<change-1>/
- <change-1> -> archive/<target-name-1>/

Skipped M changes:
- <change-2> (user chose not to archive incomplete)
Expand All @@ -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-<name>
- Archive directory target uses current date: YYYY-MM-DD-<name>; 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`
};
}
2 changes: 1 addition & 1 deletion src/core/templates/workflows/onboard.ts
Original file line number Diff line number Diff line change
Expand Up @@ -447,7 +447,7 @@ openspec archive "<name>"

**SHOW:**
\`\`\`
Archived to: \`<planningHome.changesDir>/archive/YYYY-MM-DD-<name>/\`
Archived to: \`<planningHome.changesDir>/archive/<target-name>/\` (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.
\`\`\`
Expand Down
47 changes: 38 additions & 9 deletions test/core/templates/skill-templates-parity.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -43,19 +43,19 @@ const EXPECTED_FUNCTION_HASHES: Record<string, string> = {
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',
Expand All @@ -71,10 +71,10 @@ const EXPECTED_GENERATED_SKILL_CONTENT_HASHES: Record<string, string> = {
'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',
};
Expand Down Expand Up @@ -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('<target-name>');

// 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-/);
}
});
});
Loading