fix(archive): make the Purpose carry-over safe and consistent

Three adversarial reviews of the carry-over found the guard added in
651b42c was too narrow and the guidance half-landed. Addressed:

Engine
- Replace the two-rule structural guard with a readability check against
  the parser validate/list/archive actually use. A Purpose body holding a
  heading or an unterminated code fence used to abort the archive, or
  write a spec with a duplicated `## Requirements` that its own validator
  rejects. Both now fall back to the placeholder and warn.
- Ignore markdown inside HTML comments when locating the Purpose, so a
  commented-out draft cannot beat the real section and an unfilled
  template placeholder counts as empty.
- Warn when a carried Purpose is under the strict-mode minimum: the old
  placeholder always cleared it, so this was the first way archive could
  leave a spec that `validate --strict` fails.
- Warn instead of silently dropping a delta Purpose when the main spec
  already exists.

Guidance, which disagreed with itself and with the agent path
- openspec-sync-specs told agents to write TBD, so `/openspec-archive`
  undid what the CLI now does. It carries the delta Purpose too.
- The specs artifact template and the instruction's own example had no
  `## Purpose` while the prose asked for one.
- Document the section in concepts, writing-specs, their website copies,
  openspec-conventions and specs-sync-skill; state the 50-character
  threshold and how to change an existing spec's Purpose.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
Clay Good
2026-07-23 09:23:28 -05:00
co-authored by Claude Opus 4.8
parent 651b42c6eb
commit fff5fb2256
13 changed files with 314 additions and 35 deletions
+4 -2
View File
@@ -1,5 +1,7 @@
---
'@fission-ai/openspec': patch
"@fission-ai/openspec": patch
---
`openspec archive` now carries a delta spec's `## Purpose` into the main spec it creates for a brand-new capability, instead of always writing the `TBD - created by archiving change <name>. Update Purpose after archive.` placeholder over it. The section body is copied verbatim, fenced code blocks included. The placeholder still appears when the delta has no `## Purpose` header outside a code fence, when the section body is empty, or when carrying the body over would put a requirement header outside `## Requirements` (that last case warns and still completes the archive). The Purpose of an existing main spec is never touched.
A delta spec that introduces a brand-new capability can now open with a `## Purpose`, and `openspec archive` uses it as the Purpose of the main spec it creates instead of always writing the `TBD - created by archiving change <name>. Update Purpose after archive.` placeholder over it. The body is copied trimmed but otherwise verbatim, fenced code blocks included. The `specs` artifact instruction, its example, the delta template and the `openspec-sync-specs` skill all now tell authors and agents to write one, so the CLI and agent-driven sync paths produce the same main spec.
Archive keeps the placeholder, and says why, when the delta has no `## Purpose` outside a code fence or HTML comment, when the section body is empty, or when carrying the body over would leave a spec the main spec parser cannot read. A carried Purpose shorter than the strict-mode minimum is kept but warned about, since `openspec validate --strict` reports it as too brief. The Purpose of an existing main spec is never touched, and archive now warns instead of dropping a delta's Purpose silently in that case.
+1
View File
@@ -393,6 +393,7 @@ The system MUST expire sessions after 15 minutes of inactivity.
| `## ADDED Requirements` | New behavior | Appended to main spec |
| `## MODIFIED Requirements` | Changed behavior | Replaces existing requirement |
| `## REMOVED Requirements` | Deprecated behavior | Deleted from main spec |
| `## Purpose` | What a brand-new capability is for | Seeds the Purpose of the main spec being created; ignored when the spec already exists |
### Why Deltas Instead of Full Specs
+2
View File
@@ -58,6 +58,8 @@ A change describes its edits to the specs with three section types. Using the ri
On archive, ADDED gets appended to the main spec, MODIFIED replaces the old version, and REMOVED is deleted. If you mark a real change as ADDED, you end up with two competing requirements; if you describe new behavior as MODIFIED, there's nothing to replace. When in doubt, open the current spec and see whether the requirement is already there.
One more section is worth knowing about. When your delta creates a capability that doesn't exist yet, open it with `## Purpose` — a sentence or two on what the capability is for. Archive uses it as the Purpose of the main spec it creates; skip it and you get a `TBD` placeholder to fill in by hand. An existing spec already has a Purpose, so a delta's is ignored there — edit `openspec/specs/<capability>/spec.md` directly to change one.
## Right-size the change
The single most common authoring mistake isn't a badly worded requirement — it's a change that's trying to be three changes.
+18 -6
View File
@@ -93,23 +93,35 @@ Before moving the change to archive, the command SHALL apply delta changes to ma
#### Scenario: New main spec inherits the delta's Purpose
- **WHEN** a delta creates a main spec that does not exist yet
- **AND** the delta spec has a `## Purpose` header that is not itself inside a fenced code block
- **AND** that section's body is not empty
- **THEN** write the section body into the new main spec verbatim, fenced code blocks included
- **AND** the delta spec has a line-initial `## Purpose` header that is not inside a fenced code block or an HTML comment
- **AND** the section body, ignoring fenced blocks and HTML comments, is not empty
- **THEN** write the section body into the new main spec, trimmed but otherwise verbatim, fenced code blocks and comments included
- **AND** the section body runs to the next `## ` heading outside a fenced block
#### Scenario: New main spec without an authored Purpose
- **WHEN** a delta creates a main spec that does not exist yet
- **AND** the delta spec has no `## Purpose` header outside a fenced code block, or the section body is empty
- **AND** the delta spec has no such `## Purpose` header, or that section's body is empty once fenced blocks and HTML comments are ignored
- **THEN** write the TBD placeholder Purpose naming the change to update after archive
#### Scenario: Delta Purpose that would invalidate the new main spec
#### Scenario: Delta Purpose that would leave the new main spec unreadable
- **WHEN** a delta creates a main spec that does not exist yet
- **AND** carrying its `## Purpose` body over would place a requirement header outside the `## Requirements` section
- **AND** carrying its `## Purpose` body over would leave a spec the main spec parser cannot read - a heading or requirement header that truncates a section, or an unterminated code fence that swallows one
- **THEN** write the TBD placeholder Purpose instead and warn that the delta Purpose was ignored
- **AND** complete the archive rather than aborting it
#### Scenario: Carried Purpose shorter than the strict-mode minimum
- **WHEN** a carried Purpose is shorter than the minimum Purpose length strict validation enforces
- **THEN** carry it over unchanged and warn that `openspec validate --strict` reports it as too brief
#### Scenario: Delta Purpose for a capability that already has a main spec
- **WHEN** a delta carries a `## Purpose` and the target main spec already exists
- **THEN** leave the existing Purpose untouched
- **AND** warn that the delta Purpose was ignored, naming the main spec to edit directly
### Requirement: Confirmation Behavior
The spec update confirmation SHALL provide clear visibility into changes before they are applied.
@@ -150,10 +150,18 @@ Change proposals SHALL store only the additions, modifications, and removals to
The `changes/[name]/specs/` directory SHALL contain:
- Delta files showing only what changes
- Sections for ADDED, MODIFIED, REMOVED, and RENAMED requirements
- An optional `## Purpose` section on deltas that introduce a new capability
- Normalized header matching for requirement identification
- Complete requirements using the structured format
- Clear indication of change type for each requirement
#### Scenario: Introducing a new capability
- **WHEN** a delta introduces a capability that has no main spec yet
- **THEN** the delta MAY open with a `## Purpose` section describing the capability
- **AND** that Purpose SHALL seed the main spec created for it
- **AND** a delta for a capability that already has a main spec SHALL NOT carry a `## Purpose`, because the existing Purpose is authoritative
#### Scenario: Using standard output symbols
- **WHEN** displaying delta operations in CLI output
+2
View File
@@ -55,6 +55,8 @@ The agent SHALL reconcile main specs with delta specs using the delta operation
#### Scenario: New capability spec
- **WHEN** delta spec exists for a capability not in main specs
- **THEN** create new main spec file at `openspec/specs/<capability>/spec.md`
- **AND** copy the delta's `## Purpose` body into it when the delta has one, matching what `openspec archive` does
- **AND** write a brief TBD placeholder Purpose only when the delta has none
#### Scenario: Merged main spec keeps canonical structure
- **WHEN** the agent writes a main spec during sync
+13 -6
View File
@@ -82,11 +82,14 @@ artifacts:
- Every requirement MUST have at least one scenario.
New capabilities only: start the delta spec with a `## Purpose` section -
one or two sentences describing what the capability is for. Archive copies
it into the main spec it creates; without it the new main spec is left with
a `TBD ... Update Purpose after archive` placeholder to fill in by hand.
Do NOT add `## Purpose` to a delta for an existing capability - that spec
already has one and the delta's is ignored.
one or two sentences (50+ characters, or `openspec validate --strict`
reports it as too brief) describing what the capability is for. Archive
copies it into the main spec it creates; without it the new main spec is
left with a `TBD ... Update Purpose after archive` placeholder to fill in
by hand. Do NOT add `## Purpose` to a delta for an existing capability -
that spec already has one and the delta's is ignored. To change an
existing capability's Purpose - including a leftover `TBD` placeholder -
edit `openspec/specs/<capability>/spec.md` directly.
MODIFIED requirements workflow:
1. Locate the existing requirement in openspec/specs/<capability>/spec.md
@@ -97,8 +100,12 @@ artifacts:
Common pitfall: Using MODIFIED with partial content loses detail at archive time.
If adding new concerns without changing existing behavior, use ADDED instead.
Example:
Example (a new capability, so it opens with `## Purpose`):
```
## Purpose
Lets users take their data out of the product in a portable format.
## ADDED Requirements
### Requirement: User can export data
+3
View File
@@ -1,3 +1,6 @@
## Purpose
<!-- New capabilities only: one or two sentences (50+ characters) on what this capability is for. Delete this section for an existing capability. -->
## ADDED Requirements
### Requirement: <!-- requirement name -->
+2 -1
View File
@@ -78,7 +78,8 @@ This is an **agent-driven** operation - you will read delta specs and directly e
d. **Create new main spec** if capability doesn't exist yet:
- Create `<planningHome.root>/openspec/specs/<capability>/spec.md`
- Add Purpose section (can be brief, mark as TBD)
- Add Purpose section: copy the delta's `## Purpose` body verbatim when it has one
(this is what `openspec archive` does); only write a brief TBD placeholder when it does not
- Add Requirements section with the ADDED requirements
- Follow the **Main Spec Format Reference** below
+69 -14
View File
@@ -16,7 +16,9 @@ import {
} from './parsers/requirement-blocks.js';
import { findMainSpecStructureIssues } from './parsers/spec-structure.js';
import { buildCodeFenceMask } from './parsers/code-fence.js';
import { MarkdownParser } from './parsers/markdown-parser.js';
import { Validator } from './validation/validator.js';
import { MIN_PURPOSE_LENGTH } from './validation/constants.js';
import { discoverSpecFiles } from '../utils/spec-discovery.js';
// -----------------------------------------------------------------------------
@@ -201,10 +203,22 @@ export async function buildUpdatedSpec(
}
// Load or create base target content
const deltaPurpose = extractPurposeSection(changeContent);
let targetContent: string;
let isNewSpec = false;
try {
targetContent = await fs.readFile(update.target, 'utf-8');
// A delta Purpose only seeds a spec that does not exist yet. Say so rather
// than dropping it silently - the specs instruction tells authors to write
// one for new capabilities, and the delta file looks identical either way.
if (deltaPurpose && !options.silent) {
console.log(
chalk.yellow(
`⚠️ Warning: ${specName} - delta Purpose ignored; ${specName} already has one. ` +
`Edit openspec/specs/${specName}/spec.md directly to change it.`
)
);
}
} catch {
// Target spec does not exist; MODIFIED and RENAMED are not allowed for new specs
// REMOVED will be ignored with a warning since there's nothing to remove
@@ -222,19 +236,27 @@ export async function buildUpdatedSpec(
);
}
isNewSpec = true;
targetContent = buildSpecSkeleton(specName, changeName, extractPurposeSection(changeContent));
// A carried Purpose that hides a requirement header would make the new main
// spec structurally invalid and abort an archive that succeeded before
// #1413. Keep the placeholder rather than turning a warning into a failure.
if (findMainSpecStructureIssues(targetContent).length > 0) {
targetContent = buildSpecSkeleton(specName, changeName, deltaPurpose);
// Keep the placeholder rather than turning this into a failure: these
// deltas archived cleanly before the Purpose carry-over existed.
if (!isSkeletonReadable(targetContent, specName)) {
targetContent = buildSpecSkeleton(specName, changeName);
if (!options.silent) {
console.log(
chalk.yellow(
`⚠️ Warning: ${specName} - delta Purpose ignored (it contains a requirement header); wrote the placeholder Purpose instead.`
`⚠️ Warning: ${specName} - delta Purpose ignored (it would leave the new spec unreadable); wrote the placeholder Purpose instead.`
)
);
}
} else if (deltaPurpose && deltaPurpose.length < MIN_PURPOSE_LENGTH && !options.silent) {
// The placeholder always cleared this threshold, so a carried Purpose is
// the first way archive can leave a spec that `validate --strict` fails.
console.log(
chalk.yellow(
`⚠️ Warning: ${specName} - carried Purpose is under ${MIN_PURPOSE_LENGTH} characters; ` +
`openspec validate --strict reports it as too brief.`
)
);
}
}
@@ -413,28 +435,61 @@ export async function writeUpdatedSpec(
if (counts.renamed) console.log(` → ${counts.renamed} renamed`);
}
/** Blank out `<!-- ... -->` spans, preserving line count so indices stay aligned. */
function maskHtmlComments(content: string): string {
return content.replace(/<!--[\s\S]*?-->/g, comment => comment.replace(/[^\n]/g, ' '));
}
/**
* Read the body of a `## Purpose` section, ignoring fenced code blocks.
* Returns undefined when the section is absent or empty.
* Read the body of a `## Purpose` section, ignoring markdown that only appears
* inside fenced code blocks or HTML comments. Returns undefined when the
* section is absent or its body is empty.
*/
function extractPurposeSection(content: string): string | undefined {
const lines = content.replace(/\r\n?/g, '\n').split('\n');
const mask = buildCodeFenceMask(lines);
const start = lines.findIndex((line, i) => !mask[i] && /^##\s+Purpose\s*$/i.test(line));
const normalized = content.replace(/\r\n?/g, '\n');
const lines = normalized.split('\n');
// Structure is read from the masked copy so a commented-out or fenced
// `## Purpose` is not mistaken for the real one; the body is returned from
// the original lines so an author's own comments and fences survive intact.
const masked = maskHtmlComments(normalized).split('\n');
const fenceMask = buildCodeFenceMask(masked);
const isStructural = (i: number) => !fenceMask[i];
const start = masked.findIndex((line, i) => isStructural(i) && /^##\s+Purpose\s*$/i.test(line));
if (start === -1) return undefined;
let end = lines.length;
for (let i = start + 1; i < lines.length; i++) {
if (!mask[i] && /^##\s+/.test(lines[i])) {
let end = masked.length;
for (let i = start + 1; i < masked.length; i++) {
if (isStructural(i) && /^##\s+/.test(masked[i])) {
end = i;
break;
}
}
// Emptiness is judged on the masked body so a comment-only Purpose (an
// unfilled template placeholder) falls back to the TBD placeholder.
if (!masked.slice(start + 1, end).join('\n').trim()) return undefined;
const body = lines.slice(start + 1, end).join('\n').trim();
return body || undefined;
}
/**
* A carried Purpose must leave the new main spec readable by the same parser
* that `validate`, `list` and a later `archive` use. A body containing a
* heading, a stray requirement header, or an unterminated code fence silently
* swallows or truncates the sections around it, so archive would abort or
* write a spec its own validator rejects (#1413).
*/
function isSkeletonReadable(skeleton: string, specName: string): boolean {
if (findMainSpecStructureIssues(skeleton).length > 0) return false;
try {
return new MarkdownParser(skeleton).parseSpec(specName).overview.trim().length > 0;
} catch {
return false;
}
}
/**
* Build a skeleton spec for new capabilities. When the delta spec authored a
* `## Purpose`, carry it over instead of the TBD placeholder (#1413) - archive
+4 -2
View File
@@ -80,7 +80,8 @@ ${STORE_SELECTION_GUIDANCE}
d. **Create new main spec** if capability doesn't exist yet:
- Create \`<planningHome.root>/openspec/specs/<capability>/spec.md\`
- Add Purpose section (can be brief, mark as TBD)
- Add Purpose section: copy the delta's \`## Purpose\` body verbatim when it has one
(this is what \`openspec archive\` does); only write a brief TBD placeholder when it does not
- Add Requirements section with the ADDED requirements
- Follow the **Main Spec Format Reference** below
@@ -252,7 +253,8 @@ ${STORE_SELECTION_GUIDANCE}
d. **Create new main spec** if capability doesn't exist yet:
- Create \`<planningHome.root>/openspec/specs/<capability>/spec.md\`
- Add Purpose section (can be brief, mark as TBD)
- Add Purpose section: copy the delta's \`## Purpose\` body verbatim when it has one
(this is what \`openspec archive\` does); only write a brief TBD placeholder when it does not
- Add Requirements section with the ADDED requirements
- Follow the **Main Spec Format Reference** below
+185 -1
View File
@@ -670,7 +670,7 @@ The system SHALL handle widgets.
expect(updatedContent).not.toContain('### Requirement: Stray header');
expect(updatedContent).toContain('### Requirement: Real Requirement');
expect(console.log).toHaveBeenCalledWith(
expect.stringContaining(`Warning: widgets - delta Purpose ignored (it contains a requirement header)`)
expect.stringContaining('Warning: widgets - delta Purpose ignored (it would leave the new spec unreadable)')
);
// The archive still completed rather than aborting.
@@ -678,6 +678,186 @@ The system SHALL handle widgets.
expect(archives.some(a => a.includes(changeName))).toBe(true);
});
it('should fall back to the placeholder when the delta Purpose contains a heading (issue #1413)', async () => {
const changeName = 'new-spec-with-heading-in-purpose';
const changeSpecDir = path.join(tempDir, 'openspec', 'changes', changeName, 'specs', 'gadgets');
await fs.mkdir(changeSpecDir, { recursive: true });
// An `#` heading truncates the Purpose section when the spec is read back,
// leaving a spec whose own validator rejects it for having no Purpose.
const specContent = `## Purpose
# Not a spec title
Some body text that is comfortably longer than the strict-mode minimum length.
## ADDED Requirements
### Requirement: Handle Gadget
The system SHALL handle gadgets.
#### Scenario: Gadget handled
- **WHEN** a gadget arrives
- **THEN** it is handled
`;
await fs.writeFile(path.join(changeSpecDir, 'spec.md'), specContent);
await archiveCommand.execute(changeName, { yes: true, noValidate: true });
const updatedContent = await fs.readFile(
path.join(tempDir, 'openspec', 'specs', 'gadgets', 'spec.md'),
'utf-8'
);
expect(updatedContent).toContain(
`TBD - created by archiving change ${changeName}. Update Purpose after archive.`
);
expect(updatedContent).not.toContain('# Not a spec title');
// The rebuilt spec must still satisfy the validator archive itself runs.
const report = await new Validator().validateSpecContent('gadgets', updatedContent);
expect(report.issues.filter(i => i.level === 'ERROR')).toHaveLength(0);
});
it('should fall back to the placeholder when the delta Purpose has an unterminated fence (issue #1413)', async () => {
const changeName = 'new-spec-with-unterminated-fence';
const changeSpecDir = path.join(tempDir, 'openspec', 'changes', changeName, 'specs', 'mesh-config');
await fs.mkdir(changeSpecDir, { recursive: true });
// The open fence masks everything after it, so the Purpose body would
// swallow the skeleton's own ## Requirements header.
const specContent = `## ADDED Requirements
### Requirement: Normalize Mesh Config
The system SHALL normalize mesh config.
#### Scenario: Config normalized
- **WHEN** config is loaded
- **THEN** it is normalized
## Purpose
Normalizes configuration for every service in the mesh. Canonical shape:
\`\`\`yaml
retries: 3
`;
await fs.writeFile(path.join(changeSpecDir, 'spec.md'), specContent);
await archiveCommand.execute(changeName, { yes: true, noValidate: true });
const updatedContent = await fs.readFile(
path.join(tempDir, 'openspec', 'specs', 'mesh-config', 'spec.md'),
'utf-8'
);
expect(updatedContent).toContain(
`TBD - created by archiving change ${changeName}. Update Purpose after archive.`
);
// Exactly one Requirements section, and the requirement is still visible.
expect(updatedContent.match(/^## Requirements$/gm)).toHaveLength(1);
const report = await new Validator().validateSpecContent('mesh-config', updatedContent);
expect(report.issues.filter(i => i.level === 'ERROR')).toHaveLength(0);
});
it('should ignore a commented-out Purpose in favor of the real one (issue #1413)', async () => {
const changeName = 'new-spec-with-commented-purpose';
const changeSpecDir = path.join(tempDir, 'openspec', 'changes', changeName, 'specs', 'loyalty-v2');
await fs.mkdir(changeSpecDir, { recursive: true });
const specContent = `<!--
## Purpose
Draft purpose the author commented out while rewriting the section.
-->
## Purpose
Manages the loyalty program end to end across the storefront and admin console.
## ADDED Requirements
### Requirement: Earn Points
The system SHALL award loyalty points.
#### Scenario: Points earned
- **WHEN** an order completes
- **THEN** points are credited
`;
await fs.writeFile(path.join(changeSpecDir, 'spec.md'), specContent);
await archiveCommand.execute(changeName, { yes: true, noValidate: true });
const updatedContent = await fs.readFile(
path.join(tempDir, 'openspec', 'specs', 'loyalty-v2', 'spec.md'),
'utf-8'
);
expect(updatedContent).toContain('Manages the loyalty program end to end');
expect(updatedContent).not.toContain('Draft purpose the author commented out');
expect(updatedContent).not.toContain('-->');
});
it('should keep the placeholder when the delta Purpose is only an HTML comment (issue #1413)', async () => {
const changeName = 'new-spec-with-unfilled-template';
const changeSpecDir = path.join(tempDir, 'openspec', 'changes', changeName, 'specs', 'unfilled');
await fs.mkdir(changeSpecDir, { recursive: true });
// This is the shipped delta template left unfilled.
const specContent = `## Purpose
<!-- New capabilities only: one or two sentences on what this capability is for. -->
## ADDED Requirements
### Requirement: Do Thing
The system SHALL do the thing.
#### Scenario: Thing done
- **WHEN** asked
- **THEN** done
`;
await fs.writeFile(path.join(changeSpecDir, 'spec.md'), specContent);
await archiveCommand.execute(changeName, { yes: true, noValidate: true });
const updatedContent = await fs.readFile(
path.join(tempDir, 'openspec', 'specs', 'unfilled', 'spec.md'),
'utf-8'
);
expect(updatedContent).toContain(
`TBD - created by archiving change ${changeName}. Update Purpose after archive.`
);
expect(updatedContent).not.toContain('New capabilities only');
});
it('should warn when a carried Purpose is under the strict-mode minimum (issue #1413)', async () => {
const changeName = 'new-spec-with-brief-purpose';
const changeSpecDir = path.join(tempDir, 'openspec', 'changes', changeName, 'specs', 'points');
await fs.mkdir(changeSpecDir, { recursive: true });
const specContent = `## Purpose
Tracks loyalty points.
## ADDED Requirements
### Requirement: Track Points
The system SHALL track points.
#### Scenario: Points tracked
- **WHEN** an order completes
- **THEN** points are tracked
`;
await fs.writeFile(path.join(changeSpecDir, 'spec.md'), specContent);
await archiveCommand.execute(changeName, { yes: true, noValidate: true });
const updatedContent = await fs.readFile(
path.join(tempDir, 'openspec', 'specs', 'points', 'spec.md'),
'utf-8'
);
// The author's words are kept - the warning exists so the strict-mode
// failure is not a surprise later.
expect(updatedContent).toContain('Tracks loyalty points.');
expect(console.log).toHaveBeenCalledWith(
expect.stringContaining('carried Purpose is under 50 characters')
);
});
it('should not overwrite the Purpose of an existing main spec (issue #1413)', async () => {
const changeName = 'existing-spec-with-purpose';
const changeSpecDir = path.join(tempDir, 'openspec', 'changes', changeName, 'specs', 'billing');
@@ -726,6 +906,10 @@ The system SHALL refund the card on file.
expect(updatedContent).toContain('The established purpose that must survive archiving.');
expect(updatedContent).not.toContain('A purpose written in the delta that must be ignored');
expect(updatedContent).toContain('### Requirement: Refund Card');
// Dropping it silently would be indistinguishable from it having worked.
expect(console.log).toHaveBeenCalledWith(
expect.stringContaining('billing - delta Purpose ignored; billing already has one')
);
});
it('should still error on MODIFIED when creating new spec file', async () => {
@@ -42,7 +42,7 @@ const EXPECTED_FUNCTION_HASHES: Record<string, string> = {
getContinueChangeSkillTemplate: '5cc6cf74c055ae67b08373421d934ece65dacbccafbc7452ab5636df3eb9e862',
getApplyChangeSkillTemplate: '0f5a15fc7fb9ad6059a5643d0e01365d27642637a4aaebf182f9eabb45348197',
getFfChangeSkillTemplate: '097a9ff9533900f227cac0523289eae4e19f06a081e5f355a8374dbecf3ff55d',
getSyncSpecsSkillTemplate: '32c3169e1ee0345a174c0bacb8fd16db73477cc006d8cedbedc6077233c5461b',
getSyncSpecsSkillTemplate: '7479bfc91d28d86d3e791bf5bec905be1dae58dba3c922e8e74f97c92041d0a7',
getOnboardSkillTemplate: 'bc2216b72724b01c3a733e63b8bf4aff457f561c0e9ff7288bdacc39780a37a7',
getOpsxExploreCommandTemplate: 'eef1f8b4fd90ade6d70be46f0f8c3e6722f221fed175a6f9cf626287ef504a94',
getOpsxNewCommandTemplate: '57c600cce318d16b9b4308a18d0d983ea3c0673034e606a7cceec07b4c705e87',
@@ -51,7 +51,7 @@ const EXPECTED_FUNCTION_HASHES: Record<string, string> = {
getOpsxFfCommandTemplate: '264b514cc4849f91fb4414f639484c4181f1e5850d0d788ef276c851efa92859',
getArchiveChangeSkillTemplate: '206a22b6778e97c30da9145ef51fdad449b8c995538f6fc25752ef551a37b675',
getBulkArchiveChangeSkillTemplate: '2b74b1f73380ff32e35f580734780d843c6161a2748c39edb07f1e00453771b4',
getOpsxSyncCommandTemplate: '68dc44c9be2ec1ef719a4ed59830e5a0bc74c3ba6113070650266e1b0d153071',
getOpsxSyncCommandTemplate: '48f2c5171e9b86db1f418723ddfb9215ce7dfb494de34a21ec969eac1fb3de8c',
getVerifyChangeSkillTemplate: 'cab4db01b5d2b1243d63d90c53747d8b39e488c60f76eba3fe8b994467f69267',
getOpsxArchiveCommandTemplate: '7dea65d0e2e17db366bb666ba6ae5e205ea02707b8c5c7707565200875c78916',
getOpsxOnboardCommandTemplate: '9430a0fb6530791ab720e068f4b172bc3dfc4e96a1ae29102bee0b92c2afe7b5',
@@ -70,7 +70,7 @@ const EXPECTED_GENERATED_SKILL_CONTENT_HASHES: Record<string, string> = {
'openspec-continue-change': '02ec4de061ad6277866b877497a1e66142ba364e12b83dd7dedb838579ea88db',
'openspec-apply-change': '09c0e1cdf5ccc82416d0969d6bd715cc70616bdbc3531358a5c36057f78be55a',
'openspec-ff-change': 'ff3bd3eac427a1e50071ad7c70f73b556cffa3db43e90da2726e96849c3fc886',
'openspec-sync-specs': 'd1bcd420bf8fb55a13f58a2857e6ebde58eb6f9e721a3bf6876bd9f640a63859',
'openspec-sync-specs': 'e08e40eae3bd55bf825adcda301676b690dcc018c56e3786bd62bbd2bfb81342',
'openspec-archive-change': '64b1611dd7aee04ca268820d1b193e8bf0a39ff3672ec6ba21fb0a1bcb1786c2',
'openspec-bulk-archive-change': '49d410bda408c0411decd584be9c2355335e3b3db760fc6a0adcd82c172a280f',
'openspec-verify-change': '57693d22940f06080c6cf8d590ac2f48240d4a5e9ce7074dacd0f8d3c9945afa',