fix(templates): deduplicate apply skill and command instructions (#1153)

* fix(templates): deduplicate apply skill and command instructions

Extract shared APPLY_INSTRUCTIONS constant so skill and command
templates reference the same string. Eliminates content drift
reported in #1139.

* fix(templates): update parity hashes after parameterizing apply instructions

* test(templates): add normalized body parity assertion for apply skill vs command

* docs(templates): add JSDoc to getApplyInstructions

* docs(templates): add JSDoc to all functions in apply-change

* fix(templates): parameterize /opsx:apply examples and add regression tests for skill /opsx: references

---------

Co-authored-by: Clay Good <hi@claygood.com>
This commit is contained in:
SHASHANK DWIVEDI
2026-08-05 00:48:30 +00:00
committed by GitHub
co-authored by Clay Good
parent 7a4a745d80
commit 0b233efb86
3 changed files with 35 additions and 197 deletions
+4 -4
View File
@@ -13,7 +13,7 @@ Implement tasks from an OpenSpec change.
**Store selection:** If the user names a store (a store is a standalone OpenSpec repo registered on this machine) or the work lives in one, run `openspec store list --json` to discover registered store ids, then pass `--store <id>` on the commands that read or write specs and changes (`new change`, `status`, `instructions`, `list`, `show`, `validate`, `archive`, `doctor`, `context`, `view`). Once selected, treat `--store <id>` as sticky for the rest of the workflow. Every unscoped example of those commands below is shorthand: before running it, append the flag. For example, run `openspec status --change "<name>" --json --store "<id>"`, not the unscoped form shown below. Other commands do not take the flag. Hints printed by commands already carry the flag; keep it on follow-ups. Without a store, commands act on the nearest local `openspec/` root.
**Input**: Optionally specify a change name. If omitted, check if it can be inferred from conversation context. If vague or ambiguous you MUST prompt for available changes.
**Input**: Optionally specify a change name (e.g., `/openspec-apply-change add-auth`). If omitted, check if it can be inferred from conversation context. If vague or ambiguous you MUST prompt for available changes.
**Steps**
@@ -42,7 +42,7 @@ Implement tasks from an OpenSpec change.
```
This returns:
- `contextFiles`: artifact ID -> array of concrete file paths (varies by schema - could be proposal/specs/design/tasks or spec/tests/implementation/docs)
- `contextFiles`: artifact ID -> array of concrete file paths (varies by schema)
- Progress (total, complete, remaining)
- Task list with status
- Dynamic instruction based on current state
@@ -50,7 +50,7 @@ Implement tasks from an OpenSpec change.
- Optional `operationGuidance`: current advisory guidance for apply
**Handle states:**
- If `state: "blocked"` (missing artifacts): show message, suggest using openspec-continue-change (if it is not installed, run `openspec status --change "<name>" --json` to see the next artifact and `openspec instructions <artifact-id> --change "<name>" --json` for how to create it)
- If `state: "blocked"` (missing artifacts): show message, suggest using `/openspec-continue-change` (if it is not installed, run `openspec status --change "<name>" --json` to see the next artifact and `openspec instructions <artifact-id> --change "<name>" --json` for how to create it)
- If `state: "all_done"`: congratulate, suggest archive
- Otherwise: proceed to implementation
@@ -138,7 +138,7 @@ Working on task 4/7: <task description>
- [x] Task 2
...
All tasks complete! Ready to archive this change.
All tasks complete! You can archive this change with `/openspec-archive-change`.
```
**Output On Pause (Issue Encountered)**
+23 -191
View File
@@ -7,196 +7,8 @@
import type { SkillTemplate, CommandTemplate } from '../types.js';
import { STORE_SELECTION_GUIDANCE } from './store-selection.js';
export function getApplyChangeSkillTemplate(): SkillTemplate {
return {
name: 'openspec-apply-change',
description: 'Implement tasks from an OpenSpec change. Use when the user wants to start implementing, continue implementation, or work through tasks.',
instructions: `Implement tasks from an OpenSpec change.
${STORE_SELECTION_GUIDANCE}
**Input**: Optionally specify a change name. If omitted, check if it can be inferred from conversation context. If vague or ambiguous you MUST prompt for available changes.
**Steps**
1. **Select the change**
If a name is provided, use it. Otherwise:
- Infer from conversation context if the user mentioned a change
- Auto-select if only one active change exists
- If ambiguous, run \`openspec list --json\` to get available changes and ask the user to select one
Always announce: "Using change: <name>" and how to override (e.g., \`/opsx:apply <other>\`).
2. **Check status to understand the schema**
\`\`\`bash
openspec status --change "<name>" --json
\`\`\`
Parse the JSON to understand:
- \`schemaName\`: The workflow being used (e.g., "spec-driven")
- \`planningHome\`, \`changeRoot\`, and \`actionContext\`: planning scope and edit constraints
- Which artifact contains the tasks (typically "tasks" for spec-driven, check status for others)
3. **Get apply instructions**
\`\`\`bash
openspec instructions apply --change "<name>" --json
\`\`\`
This returns:
- \`contextFiles\`: artifact ID -> array of concrete file paths (varies by schema - could be proposal/specs/design/tasks or spec/tests/implementation/docs)
- Progress (total, complete, remaining)
- Task list with status
- Dynamic instruction based on current state
- Optional \`context\`: current required project instruction input from the selected root
- Optional \`operationGuidance\`: current advisory guidance for apply
**Handle states:**
- If \`state: "blocked"\` (missing artifacts): show message, suggest using openspec-continue-change (if it is not installed, run \`openspec status --change "<name>" --json\` to see the next artifact and \`openspec instructions <artifact-id> --change "<name>" --json\` for how to create it)
- If \`state: "all_done"\`: congratulate, suggest archive
- Otherwise: proceed to implementation
Treat \`context\` as a required prompt-level input. Read and consider it, and
apply relevant project facts, conventions, and constraints while implementing.
Treat \`operationGuidance\` as optional additive advice. Read and consider every
entry, and follow entries that are applicable and compatible with the built-in
workflow.
Keep both fields separate from CLI-returned state, missing artifacts, tasks,
progress, \`contextFiles\`, and the built-in \`instruction\`. They are not
evidence of task completion, do not replace the built-in instruction, and do
not permit bypassing a blocked state. If context conflicts with the built-in
instruction, an explicit user choice, or a CLI-controlled value, report the
conflict and preserve the controlling value. If guidance is inapplicable or
conflicts with those controlling inputs, do not follow it and explain why.
These are prompt-level behavior contracts, not enforceable checks.
4. **Read context files**
Read every file path listed under \`contextFiles\` from the apply instructions output.
The files depend on the schema being used:
- **spec-driven**: proposal, specs, design, tasks
- Other schemas: follow the contextFiles from CLI output
Do not copy \`context\` or \`operationGuidance\` verbatim into implementation
files or planning artifacts unless the user separately asks for that content.
5. **Show current progress**
Display:
- Schema being used
- Progress: "N/M tasks complete"
- Remaining tasks overview
- Dynamic instruction from CLI
6. **Implement tasks (loop until done or blocked)**
For each pending task:
- Show which task is being worked on
- Make the code changes required
- Keep changes minimal and focused
- Mark task complete in the tasks file: \`- [ ]\` → \`- [x]\`
- Continue to next task
**Pause if:**
- Task is unclear → ask for clarification
- Implementation reveals a design issue → suggest updating artifacts
- Error or blocker encountered → report and wait for guidance
- User interrupts
7. **On completion or pause, show status**
Display:
- Tasks completed this session
- Overall progress: "N/M tasks complete"
- If all done: suggest archive
- If paused: explain why and wait for guidance
**Output During Implementation**
\`\`\`
## Implementing: <change-name> (schema: <schema-name>)
Working on task 3/7: <task description>
[...implementation happening...]
✓ Task complete
Working on task 4/7: <task description>
[...implementation happening...]
✓ Task complete
\`\`\`
**Output On Completion**
\`\`\`
## Implementation Complete
**Change:** <change-name>
**Schema:** <schema-name>
**Progress:** 7/7 tasks complete ✓
### Completed This Session
- [x] Task 1
- [x] Task 2
...
All tasks complete! Ready to archive this change.
\`\`\`
**Output On Pause (Issue Encountered)**
\`\`\`
## Implementation Paused
**Change:** <change-name>
**Schema:** <schema-name>
**Progress:** 4/7 tasks complete
### Issue Encountered
<description of the issue>
**Options:**
1. <option 1>
2. <option 2>
3. Other approach
What would you like to do?
\`\`\`
**Guardrails**
- Keep going through tasks until done or blocked
- Always read context files before starting (from the apply instructions output)
- If task is ambiguous, pause and ask before implementing
- If implementation reveals issues, pause and suggest artifact updates
- Keep code changes minimal and scoped to each task
- Update task checkbox immediately after completing each task
- Pause on errors, blockers, or unclear requirements - don't guess
- Use contextFiles from CLI output, don't assume specific file names
- Do not use context or operation guidance as proof that a task is complete
- Apply relevant project context; report conflicts with controlling workflow inputs
- Consider every guidance entry; explain any inapplicable or conflicting advice
- Do not copy runtime context or operation guidance into implementation files or planning artifacts
- Preserve CLI-controlled blocked/ready/all-done behavior and completion criteria
**Fluid Workflow Integration**
This skill supports the "actions on a change" model:
- **Can be invoked anytime**: Before all artifacts are done (if tasks exist), after partial implementation, interleaved with other actions
- **Allows artifact updates**: If implementation reveals design issues, suggest updating artifacts - not phase-locked, work fluidly`,
license: 'MIT',
compatibility: 'Requires openspec CLI.',
metadata: { author: 'openspec', version: '1.0' },
};
}
export function getOpsxApplyCommandTemplate(): CommandTemplate {
return {
name: 'OPSX: Apply',
description: 'Implement tasks from an OpenSpec change (Experimental)',
category: 'Workflow',
tags: ['workflow', 'artifacts', 'experimental'],
content: `Implement tasks from an OpenSpec change.
function getApplyInstructions(): string {
return `Implement tasks from an OpenSpec change.
${STORE_SELECTION_GUIDANCE}
@@ -368,6 +180,26 @@ What would you like to do?
This skill supports the "actions on a change" model:
- **Can be invoked anytime**: Before all artifacts are done (if tasks exist), after partial implementation, interleaved with other actions
- **Allows artifact updates**: If implementation reveals design issues, suggest updating artifacts - not phase-locked, work fluidly`
- **Allows artifact updates**: If implementation reveals design issues, suggest updating artifacts - not phase-locked, work fluidly`;
}
export function getApplyChangeSkillTemplate(): SkillTemplate {
return {
name: 'openspec-apply-change',
description: 'Implement tasks from an OpenSpec change. Use when the user wants to start implementing, continue implementation, or work through tasks.',
instructions: getApplyInstructions(),
license: 'MIT',
compatibility: 'Requires openspec CLI.',
metadata: { author: 'openspec', version: '1.0' },
};
}
export function getOpsxApplyCommandTemplate(): CommandTemplate {
return {
name: 'OPSX: Apply',
description: 'Implement tasks from an OpenSpec change (Experimental)',
category: 'Workflow',
tags: ['workflow', 'artifacts', 'experimental'],
content: getApplyInstructions(),
};
}
@@ -40,7 +40,7 @@ const EXPECTED_FUNCTION_HASHES: Record<string, string> = {
getExploreSkillTemplate: 'fec38ba01c5c20695aca0ec7eff78c26e278ead21459cab8ec1562af51053427',
getNewChangeSkillTemplate: '935f6335e2d4b7d1bd4f0538c88386350c25e8b16e11b627556262229583ca51',
getContinueChangeSkillTemplate: 'ed41e2356af7aad6ef760f60fad19c6843cefe436d8f90084dcba4dbc6bf7272',
getApplyChangeSkillTemplate: 'e5fc093637d3100a61acf934553002a5e9f5bccab5110136d7680af4133f7351',
getApplyChangeSkillTemplate: '18b19aec04e95cd4cce694a64cf84ac6a0fa522b69ace00390a55bb78df46778',
getFfChangeSkillTemplate: 'fc2a45a08533ee9c7ab30fdab5f832b7d440070048e2a153f03db1620dc379bb',
getSyncSpecsSkillTemplate: 'd43b112a3c74bc951b094d220c8e75cca26bb00640d404b78af0752af1ff7bd9',
getOnboardSkillTemplate: 'a9f6134b187ec4f3a5aa6c7c181e51a15fec11b7ac1044a076fdfe79b47fbc80',
@@ -68,7 +68,7 @@ const EXPECTED_GENERATED_SKILL_CONTENT_HASHES: Record<string, string> = {
'openspec-explore': '80109dec3abf1505ab1037f7196baac4fcdf175ca954411e8d439e5da881bf62',
'openspec-new-change': '579d432771703f947a331a6ed288bf9c6660ca015fcd376d76f19b6ac7683082',
'openspec-continue-change': '5c34be8194cdb4c5158335e47aece71143e8a22bfb4179dba47fd8aaf436d395',
'openspec-apply-change': '1726319cd4305a47f9c827acaeb84a9de57f7e44aba9ed60869c1758338e18ae',
'openspec-apply-change': '919db34873151b8a573fcb38631fd79a0b1256da1677b7608b2d8c2475227893',
'openspec-ff-change': '19315644df7c582d920acfb67f3c500ca4e06fccc900265b3ac39621d85f7cdb',
'openspec-sync-specs': '6e85521de10858bb020885eb657aa843e5746b2f09c846aa44545694f456cda9',
'openspec-archive-change': '019d580a13eee5892cc9233a899919b572a3abfc6a05c1f0aabf9c4ba9bf3d4d',
@@ -117,6 +117,12 @@ function hash(value: string): string {
}
describe('skill templates split parity', () => {
it('uses one canonical instruction body for apply skills and commands', () => {
expect(getApplyChangeSkillTemplate().instructions).toBe(
getOpsxApplyCommandTemplate().content
);
});
it('preserves all template function payloads exactly', () => {
const functionFactories: Record<string, () => unknown> = {
getExploreSkillTemplate,