diff --git a/docs/commands.md b/docs/commands.md index fd4bb7fe1..f8def955c 100644 --- a/docs/commands.md +++ b/docs/commands.md @@ -22,6 +22,7 @@ For workflow patterns and when to use each command, see [Workflows](workflows.md | `/opsx:new` | Start a new change scaffold | | `/opsx:continue` | Create the next artifact based on dependencies | | `/opsx:ff` | Fast-forward: create all planning artifacts at once | +| `/opsx:refine` | Review artifact quality and cross-artifact consistency | | `/opsx:verify` | Validate implementation matches artifacts | | `/opsx:sync` | Merge delta specs into main specs | | `/opsx:bulk-archive` | Archive multiple changes at once | @@ -315,6 +316,64 @@ AI: Implementing add-dark-mode... --- +### `/opsx:refine` + +Review and refine change artifacts for cross-artifact consistency, best-practice alignment, and implementation readiness. Uses a scratchpad file to track issues per change. + +**Syntax:** +```text +/opsx:refine [change-name] +``` + +**Arguments:** + +| Argument | Required | Description | +|----------|----------|-------------| +| `change-name` | No | Which change to refine (prompted if not provided) | + +**What it does:** +- Cross-checks artifacts against each other (proposal ↔ specs ↔ design ↔ tasks) +- Validates best practices (acceptance criteria, measurable scenarios, rationale) +- Creates a prioritized scratchpad (`scratchpad.md`) in the change directory +- Walks through issues one at a time for user review +- Commits scratchpad + artifact updates together for convergence traceability + +**Example:** +```text +You: /opsx:refine add-dark-mode + +AI: Refining add-dark-mode... + + Loading artifacts: proposal.md, specs/, design.md, tasks.md + + Created scratchpad.md with 5 issues: + - P0(1): Spec requires "system preference detection" but design has no mention + - P1(1): Task 1.3 has no acceptance criteria + - P1(2): design.md says CSS variables, tasks reference Tailwind + - P2(1): No error state for localStorage unavailable + - P2(2): Scenario "first visit" lacks Given/When/Then + + Starting with P0(1)... + + Spec requirement "System Preference Detection" (specs/ui/spec.md:42) + has no corresponding section in design.md. + + Proposed fix: Add "System Preference Detection" section to design.md + with prefers-color-scheme media query approach. + + Apply this fix? +``` + +**Tips:** +- Run after `/opsx:propose` or `/opsx:ff` and before `/opsx:apply` +- The scratchpad persists across sessions — resume where you left off +- Committing the scratchpad with each fix creates a history of how specs converged +- Works with partial artifacts (graceful degradation) + +**Prior art:** Inspired by [@demianmnave's nhc-opsx-refine command](https://github.com/NaveHaus/NhcAgentConfig/blob/main/.opencode/command/nhc-opsx-refine.md) ([#783](https://github.com/Fission-AI/OpenSpec/issues/783)). + +--- + ### `/opsx:verify` Validate that implementation matches your change artifacts. Checks completeness, correctness, and coherence. diff --git a/src/core/profile-sync-drift.ts b/src/core/profile-sync-drift.ts index 782bdcc9f..e68ecaf72 100644 --- a/src/core/profile-sync-drift.ts +++ b/src/core/profile-sync-drift.ts @@ -21,6 +21,7 @@ export const WORKFLOW_TO_SKILL_DIR: Record = { 'archive': 'openspec-archive-change', 'bulk-archive': 'openspec-bulk-archive-change', 'verify': 'openspec-verify-change', + 'refine': 'openspec-refine-change', 'onboard': 'openspec-onboard', 'propose': 'openspec-propose', }; diff --git a/src/core/profiles.ts b/src/core/profiles.ts index f61215dfc..4aa1a820e 100644 --- a/src/core/profiles.ts +++ b/src/core/profiles.ts @@ -27,6 +27,7 @@ export const ALL_WORKFLOWS = [ 'archive', 'bulk-archive', 'verify', + 'refine', 'onboard', ] as const; diff --git a/src/core/shared/skill-generation.ts b/src/core/shared/skill-generation.ts index 898e7a25e..597ddc0cb 100644 --- a/src/core/shared/skill-generation.ts +++ b/src/core/shared/skill-generation.ts @@ -16,6 +16,8 @@ import { getVerifyChangeSkillTemplate, getOnboardSkillTemplate, getOpsxProposeSkillTemplate, + getRefineChangeSkillTemplate, + getOpsxRefineCommandTemplate, getOpsxExploreCommandTemplate, getOpsxNewCommandTemplate, getOpsxContinueCommandTemplate, @@ -66,6 +68,7 @@ export function getSkillTemplates(workflowFilter?: readonly string[]): SkillTemp { template: getVerifyChangeSkillTemplate(), dirName: 'openspec-verify-change', workflowId: 'verify' }, { template: getOnboardSkillTemplate(), dirName: 'openspec-onboard', workflowId: 'onboard' }, { template: getOpsxProposeSkillTemplate(), dirName: 'openspec-propose', workflowId: 'propose' }, + { template: getRefineChangeSkillTemplate(), dirName: 'openspec-refine-change', workflowId: 'refine' }, ]; if (!workflowFilter) return all; @@ -92,6 +95,7 @@ export function getCommandTemplates(workflowFilter?: readonly string[]): Command { template: getOpsxVerifyCommandTemplate(), id: 'verify' }, { template: getOpsxOnboardCommandTemplate(), id: 'onboard' }, { template: getOpsxProposeCommandTemplate(), id: 'propose' }, + { template: getOpsxRefineCommandTemplate(), id: 'refine' }, ]; if (!workflowFilter) return all; diff --git a/src/core/templates/skill-templates.ts b/src/core/templates/skill-templates.ts index ff687d900..8e2690d95 100644 --- a/src/core/templates/skill-templates.ts +++ b/src/core/templates/skill-templates.ts @@ -17,4 +17,5 @@ export { getBulkArchiveChangeSkillTemplate, getOpsxBulkArchiveCommandTemplate } export { getVerifyChangeSkillTemplate, getOpsxVerifyCommandTemplate } from './workflows/verify-change.js'; export { getOnboardSkillTemplate, getOpsxOnboardCommandTemplate } from './workflows/onboard.js'; export { getOpsxProposeSkillTemplate, getOpsxProposeCommandTemplate } from './workflows/propose.js'; +export { getRefineChangeSkillTemplate, getOpsxRefineCommandTemplate } from './workflows/refine-change.js'; export { getFeedbackSkillTemplate } from './workflows/feedback.js'; diff --git a/src/core/templates/workflows/refine-change.ts b/src/core/templates/workflows/refine-change.ts new file mode 100644 index 000000000..42b043e27 --- /dev/null +++ b/src/core/templates/workflows/refine-change.ts @@ -0,0 +1,163 @@ +/** + * Skill Template Workflow Modules + * + * This file is generated by splitting the legacy monolithic + * templates file into workflow-focused modules. + */ +import type { SkillTemplate, CommandTemplate } from '../types.js'; + +const SKILL_INPUT_LINE = 'Optionally specify a change name (e.g., `/opsx:refine add-auth`).'; +const COMMAND_INPUT_LINE = 'The argument after `/opsx:refine` is the change name (e.g., `/opsx:refine add-auth`).'; + +function buildInstructions(inputLine: string): string { + return `Review and refine change artifacts for cross-artifact consistency, best-practice alignment, and implementation readiness. Tracks issues in a scratchpad file scoped to the change. + +**Input**: ${inputLine} + +**Steps** + +1. **If no change name provided, prompt for selection** + + Run \`openspec list --json\` to get available changes. Present the list and ask the user which change to refine. + + Show only active changes (not already archived). + Include the schema used for each change if available. + + **IMPORTANT**: Do NOT guess or auto-select a change. Always let the user choose. + +2. **Load change artifacts** + \`\`\`bash + openspec status --change "" --json + \`\`\` + Read all existing artifacts (proposal.md, design.md, tasks.md, specs/) from the change directory. + +3. **Create the scratchpad** + + Create \`openspec/changes//scratchpad.md\` using the format in [Scratchpad Format](#scratchpad-format). + + **IMPORTANT**: The scratchpad MUST be used to track issues and working decisions throughout the refine process. + +4. **Cross-artifact consistency checks** + + Compare artifacts against each other for contradictions, gaps, and ambiguities: + + - **Proposal → Specs**: Does every goal in the proposal have corresponding requirements in specs? + - **Specs → Design**: Does the design address all requirements and scenarios? + - **Design → Tasks**: Do tasks cover all design decisions? Are implementation steps aligned? + - **Specs → Tasks**: Are all requirements traceable to at least one task? + + Record each issue found in the scratchpad with priority: + - **P0**: Contradiction between artifacts (e.g., spec says X, design says Y) + - **P1**: Gap in coverage (e.g., requirement with no corresponding task) + - **P2**: Ambiguity or unclear intent (e.g., vague acceptance criteria) + +5. **Best-practice validation** + + Check artifacts against common quality standards: + + - Tasks have clear acceptance criteria + - Specs define measurable scenarios (Given/When/Then or equivalent) + - Design decisions include rationale + - No circular or missing dependencies in task ordering + - Edge cases and error states are addressed + + Add findings to the scratchpad. + +6. **Address issues one at a time** + + For each issue in priority order (P0 first): + - Present the issue to the user with context + - Propose a fix (artifact update) + - Apply the fix after user approval + - Validate the updated artifacts: + \`\`\`bash + openspec validate --change "" --strict + \`\`\` + If validation fails, mark the issue as "Needs refinement" in the scratchpad, keep changes uncommitted, and ask the user for direction. + - If validation passes, update the scratchpad status and **commit the scratchpad and updated artifacts together** + + **IMPORTANT**: Address one issue at a time so the user can review each change. + +7. **Final status** + + When all issues are resolved (or the user decides to stop): + \`\`\`bash + openspec status --change "" + \`\`\` + + Show summary: how many issues were found, resolved, and remaining. + If issues remain, note them as open in the scratchpad for future sessions. + +**Scratchpad Rules** + +- Issue status MUST reflect the state of the openspec artifacts (not implementation status) +- Keep "Last updated" date current +- List issues in priority order: P0, P1, P2 +- Use format \`P(i)\` where L is level and i is index (e.g., P0(1), P1(2)) +- The scratchpad is scoped per-change and persists across refine sessions +- Commit the scratchpad with artifact updates to preserve convergence history + +**Scratchpad Format** + +\`\`\`markdown +## Refinement Scratchpad + +Tracks openspec-refine issues and working decisions for the \`\` change. +This is a working document, not a spec artifact. + +Last updated: YYYY-MM-DD + +### Status Legend +- **Open**: Not yet captured consistently in OpenSpec artifacts +- **Needs refinement**: Partially captured; artifacts still need work +- **Consistent**: Artifacts are aligned with current intended behavior + +### Key References +- (List any external references used to resolve issues) + +### Current Working Constraints / Decisions +- (List constraints or decisions affecting the current issue) + +### Issue List + +#### P0(1): +- **Status**: <status> +- **Notes**: <clarify the status> +- **Artifacts touched**: + - \`openspec/changes/<change-name>/...\` + +#### P1(1): <title> +... + +### Open Questions +- (Questions needing user clarification) +\`\`\` + +**Graceful Degradation** + +- If only proposal.md exists: check for completeness and clarity, skip cross-artifact checks +- If proposal + specs exist: check proposal-spec consistency, skip design/task checks +- If full artifacts: run all checks +- Always note which checks were skipped and why`; +} + +export function getRefineChangeSkillTemplate(): SkillTemplate { + return { + name: 'openspec-refine-change', + description: 'Refine change artifacts for cross-artifact consistency, best-practice alignment, and implementation readiness. Use when the user wants to review artifact quality before implementation.', + instructions: buildInstructions(SKILL_INPUT_LINE), + license: 'MIT', + compatibility: 'Requires openspec CLI.', + metadata: { author: 'openspec', version: '1.0' }, + }; +} + +export function getOpsxRefineCommandTemplate(): CommandTemplate { + return { + name: 'OPSX: Refine', + description: 'Refine change artifacts for consistency and quality before implementation', + category: 'Workflow', + tags: ['workflow', 'refine', 'quality'], + content: buildInstructions(COMMAND_INPUT_LINE), + }; +} diff --git a/test/core/profiles.test.ts b/test/core/profiles.test.ts index 4df8e66c0..bba33a1f8 100644 --- a/test/core/profiles.test.ts +++ b/test/core/profiles.test.ts @@ -20,14 +20,14 @@ describe('profiles', () => { }); describe('ALL_WORKFLOWS', () => { - it('should contain all 11 workflows', () => { - expect(ALL_WORKFLOWS).toHaveLength(11); + it('should contain all 12 workflows', () => { + expect(ALL_WORKFLOWS).toHaveLength(12); }); it('should contain expected workflow IDs', () => { const expected = [ 'propose', 'explore', 'new', 'continue', 'apply', - 'ff', 'sync', 'archive', 'bulk-archive', 'verify', 'onboard', + 'ff', 'sync', 'archive', 'bulk-archive', 'verify', 'refine', 'onboard', ]; expect([...ALL_WORKFLOWS]).toEqual(expected); }); diff --git a/test/core/shared/skill-generation.test.ts b/test/core/shared/skill-generation.test.ts index 6c755f51d..8c2fb6c97 100644 --- a/test/core/shared/skill-generation.test.ts +++ b/test/core/shared/skill-generation.test.ts @@ -8,9 +8,9 @@ import { describe('skill-generation', () => { describe('getSkillTemplates', () => { - it('should return all 11 skill templates', () => { + it('should return all 12 skill templates', () => { const templates = getSkillTemplates(); - expect(templates).toHaveLength(11); + expect(templates).toHaveLength(12); }); it('should have unique directory names', () => { @@ -35,6 +35,7 @@ describe('skill-generation', () => { expect(dirNames).toContain('openspec-verify-change'); expect(dirNames).toContain('openspec-onboard'); expect(dirNames).toContain('openspec-propose'); + expect(dirNames).toContain('openspec-refine-change'); }); it('should have valid template structure', () => { @@ -88,9 +89,9 @@ describe('skill-generation', () => { }); describe('getCommandTemplates', () => { - it('should return all 11 command templates', () => { + it('should return all 12 command templates', () => { const templates = getCommandTemplates(); - expect(templates).toHaveLength(11); + expect(templates).toHaveLength(12); }); it('should have unique IDs', () => { @@ -115,6 +116,7 @@ describe('skill-generation', () => { expect(ids).toContain('verify'); expect(ids).toContain('onboard'); expect(ids).toContain('propose'); + expect(ids).toContain('refine'); }); it('should filter by workflow IDs when provided', () => { @@ -142,9 +144,9 @@ describe('skill-generation', () => { }); describe('getCommandContents', () => { - it('should return all 11 command contents', () => { + it('should return all 12 command contents', () => { const contents = getCommandContents(); - expect(contents).toHaveLength(11); + expect(contents).toHaveLength(12); }); it('should have valid content structure', () => { diff --git a/test/core/templates/skill-templates-parity.test.ts b/test/core/templates/skill-templates-parity.test.ts index f8fb1307b..2420f70b6 100644 --- a/test/core/templates/skill-templates-parity.test.ts +++ b/test/core/templates/skill-templates-parity.test.ts @@ -23,7 +23,9 @@ import { getOpsxSyncCommandTemplate, getOpsxProposeCommandTemplate, getOpsxProposeSkillTemplate, + getOpsxRefineCommandTemplate, getOpsxVerifyCommandTemplate, + getRefineChangeSkillTemplate, getSyncSpecsSkillTemplate, getVerifyChangeSkillTemplate, } from '../../../src/core/templates/skill-templates.js'; @@ -52,6 +54,8 @@ const EXPECTED_FUNCTION_HASHES: Record<string, string> = { getOpsxVerifyCommandTemplate: '9b4d3ca422553b7534764eb3a009da87a051612c5238e9baab294c7b1233e9a2', getOpsxProposeSkillTemplate: 'd67f937d44650e9c61d2158c865309fbab23cb3f50a3d4868a640a97776e3999', getOpsxProposeCommandTemplate: '41ad59b37eafd7a161bab5c6e41997a37368f9c90b194451295ede5cd42e4d46', + getRefineChangeSkillTemplate: '61d79ed4d9673ea032d80d3ec88c617062433ff1a3ee39b1ee8b422ddc834e5b', + getOpsxRefineCommandTemplate: 'da0b28a867314bce78f2302ad11f64b8806687c97f554656969291a71fb9b45e', getFeedbackSkillTemplate: 'd7d83c5f7fc2b92fe8f4588a5bf2d9cb315e4c73ec19bcd5ef28270906319a0d', }; @@ -67,6 +71,7 @@ const EXPECTED_GENERATED_SKILL_CONTENT_HASHES: Record<string, string> = { 'openspec-verify-change': '30d07c6f7051965f624f5964db51844ec17c7dfd05f0da95281fe0ca73616326', 'openspec-onboard': 'dbce376cf895f3fe4f63b4bce66d258c35b7b8884ac746670e5e35fabcefd255', 'openspec-propose': '20e36dabefb90e232bad0667292bd5007ec280f8fc4fc995dbc4282bf45a22e7', + 'openspec-refine-change': 'cc84efb5d418561868bca76ac7d51cbd6f1f3a93a02746db9550c9d52db9f8dc', }; function stableStringify(value: unknown): string { @@ -114,6 +119,8 @@ describe('skill templates split parity', () => { getOpsxVerifyCommandTemplate, getOpsxProposeSkillTemplate, getOpsxProposeCommandTemplate, + getRefineChangeSkillTemplate, + getOpsxRefineCommandTemplate, getFeedbackSkillTemplate, }; @@ -139,6 +146,7 @@ describe('skill templates split parity', () => { ['openspec-verify-change', getVerifyChangeSkillTemplate], ['openspec-onboard', getOnboardSkillTemplate], ['openspec-propose', getOpsxProposeSkillTemplate], + ['openspec-refine-change', getRefineChangeSkillTemplate], ]; const actualHashes = Object.fromEntries(