diff --git a/skills/openspec-update-change/SKILL.md b/skills/openspec-update-change/SKILL.md index 10ac7dd24..24c9f8836 100644 --- a/skills/openspec-update-change/SKILL.md +++ b/skills/openspec-update-change/SKILL.md @@ -15,7 +15,7 @@ Revise a change's existing planning artifacts and keep them coherent. Never edit **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. -`/openspec-continue-change` is an expanded-profile workflow and may not be installed. Before suggesting it anywhere below, verify that it is available. If it is unavailable, `openspec status --change "" --json` shows the next artifact and `openspec instructions "" --change "" --json` explains how to create it. +`/openspec-continue-change` is an optional workflow and may not be installed. Before suggesting it anywhere below, verify that it is available. If it is unavailable, `openspec status --change "" --json` shows the next artifact and `openspec instructions "" --change "" --json` explains how to create it. **Steps** @@ -87,4 +87,4 @@ After each invocation, show: - Edit only the concrete files in `existingOutputPaths`; never write to a glob `resolvedOutputPath`. - Do not advance the build frontier: no new artifacts, no new files under glob artifacts - that is `/openspec-continue-change`'s job. - Confirm every edit with the user before writing. -- If the request changes the change's *intent* rather than refining it, first verify whether the expanded-profile `/openspec-new-change` workflow is available. If it is, recommend starting fresh with `/openspec-new-change` (the "Update vs. Start Fresh" heuristic). If it is unavailable, ask for a distinct unused change name and recommend `openspec new change ""` instead. +- If the request changes the change's *intent* rather than refining it, first verify whether the optional `/openspec-new-change` workflow is available. If it is, recommend starting fresh with `/openspec-new-change` (the "Update vs. Start Fresh" heuristic). If it is unavailable, ask for a distinct unused change name and recommend `openspec new change ""` instead. diff --git a/src/commands/config.ts b/src/commands/config.ts index e594583e7..4a3382b95 100644 --- a/src/commands/config.ts +++ b/src/commands/config.ts @@ -44,7 +44,7 @@ interface WorkflowPromptMeta { description: string; } -const WORKFLOW_PROMPT_META: Record = { +export const WORKFLOW_PROMPT_META: Record = { propose: { name: 'Propose change', description: 'Create proposal, design, and tasks from a request', @@ -65,6 +65,10 @@ const WORKFLOW_PROMPT_META: Record = { name: 'Apply tasks', description: 'Implement tasks from the current change', }, + update: { + name: 'Update change', + description: 'Revise the planning artifacts of an existing change', + }, ff: { name: 'Fast-forward', description: 'Run a faster implementation workflow', diff --git a/src/core/templates/workflows/update-change.ts b/src/core/templates/workflows/update-change.ts index a716ec47b..7700cd8d7 100644 --- a/src/core/templates/workflows/update-change.ts +++ b/src/core/templates/workflows/update-change.ts @@ -17,7 +17,7 @@ ${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. -\`/opsx:continue\` is an expanded-profile workflow and may not be installed. Before suggesting it anywhere below, verify that it is available. If it is unavailable, \`openspec status --change "" --json\` shows the next artifact and \`openspec instructions "" --change "" --json\` explains how to create it. +\`/opsx:continue\` is an optional workflow and may not be installed. Before suggesting it anywhere below, verify that it is available. If it is unavailable, \`openspec status --change "" --json\` shows the next artifact and \`openspec instructions "" --change "" --json\` explains how to create it. **Steps** @@ -89,7 +89,7 @@ After each invocation, show: - Edit only the concrete files in \`existingOutputPaths\`; never write to a glob \`resolvedOutputPath\`. - Do not advance the build frontier: no new artifacts, no new files under glob artifacts - that is \`/opsx:continue\`'s job. - Confirm every edit with the user before writing. -- If the request changes the change's *intent* rather than refining it, first verify whether the expanded-profile \`/opsx:new\` workflow is available. If it is, recommend starting fresh with \`/opsx:new\` (the "Update vs. Start Fresh" heuristic). If it is unavailable, ask for a distinct unused change name and recommend \`openspec new change ""\` instead.`, +- If the request changes the change's *intent* rather than refining it, first verify whether the optional \`/opsx:new\` workflow is available. If it is, recommend starting fresh with \`/opsx:new\` (the "Update vs. Start Fresh" heuristic). If it is unavailable, ask for a distinct unused change name and recommend \`openspec new change ""\` instead.`, license: 'MIT', compatibility: 'Requires openspec CLI.', metadata: { author: 'openspec', version: '1.0' }, @@ -108,7 +108,7 @@ ${STORE_SELECTION_GUIDANCE} **Input**: Optionally specify a change name after \`/opsx:update\` (e.g., \`/opsx:update add-auth\`). If omitted, check if it can be inferred from conversation context. If vague or ambiguous you MUST prompt for available changes. -\`/opsx:continue\` is an expanded-profile workflow and may not be installed. Before suggesting it anywhere below, verify that it is available. If it is unavailable, \`openspec status --change "" --json\` shows the next artifact and \`openspec instructions "" --change "" --json\` explains how to create it. +\`/opsx:continue\` is an optional workflow and may not be installed. Before suggesting it anywhere below, verify that it is available. If it is unavailable, \`openspec status --change "" --json\` shows the next artifact and \`openspec instructions "" --change "" --json\` explains how to create it. **Steps** @@ -180,6 +180,6 @@ After each invocation, show: - Edit only the concrete files in \`existingOutputPaths\`; never write to a glob \`resolvedOutputPath\`. - Do not advance the build frontier: no new artifacts, no new files under glob artifacts - that is \`/opsx:continue\`'s job. - Confirm every edit with the user before writing. -- If the request changes the change's *intent* rather than refining it, first verify whether the expanded-profile \`/opsx:new\` workflow is available. If it is, recommend starting fresh with \`/opsx:new\` (the "Update vs. Start Fresh" heuristic). If it is unavailable, ask for a distinct unused change name and recommend \`openspec new change ""\` instead.` +- If the request changes the change's *intent* rather than refining it, first verify whether the optional \`/opsx:new\` workflow is available. If it is, recommend starting fresh with \`/opsx:new\` (the "Update vs. Start Fresh" heuristic). If it is unavailable, ask for a distinct unused change name and recommend \`openspec new change ""\` instead.` }; } diff --git a/test/commands/config.test.ts b/test/commands/config.test.ts index 92096d266..d7dd73335 100644 --- a/test/commands/config.test.ts +++ b/test/commands/config.test.ts @@ -395,3 +395,24 @@ describe('config profile command', () => { expect(result.success).toBe(false); }); }); + +describe('workflow picker labels', () => { + it('gives every workflow a friendly label instead of the raw-id fallback', async () => { + const { WORKFLOW_PROMPT_META } = await import('../../src/commands/config.js'); + const { ALL_WORKFLOWS } = await import('../../src/core/profiles.js'); + + for (const workflow of ALL_WORKFLOWS) { + const meta = WORKFLOW_PROMPT_META[workflow]; + // A missing entry is exactly what made `update` render as its raw id + // with a `Workflow: update` placeholder in the config picker (#1627). + expect(meta, `missing picker metadata for "${workflow}"`).toBeDefined(); + expect(meta!.name, `label for "${workflow}" must not be the raw id`).not.toBe(workflow); + expect(meta!.name.length, `label for "${workflow}" must be non-empty`).toBeGreaterThan(0); + expect( + meta!.description.startsWith('Workflow:'), + `description for "${workflow}" must not be the placeholder` + ).toBe(false); + expect(meta!.description.length, `description for "${workflow}" must be non-empty`).toBeGreaterThan(0); + } + }); +}); diff --git a/test/core/init.test.ts b/test/core/init.test.ts index c73d1e7d7..42a60a79f 100644 --- a/test/core/init.test.ts +++ b/test/core/init.test.ts @@ -302,7 +302,7 @@ describe('InitCommand', () => { for (const [content, continueReference] of updateVariants) { const availabilityGuidance = content.indexOf( - `${continueReference} is an expanded-profile workflow and may not be installed` + `${continueReference} is an optional workflow and may not be installed` ); const nextReference = content.indexOf( continueReference, diff --git a/test/core/templates/skill-templates-parity.test.ts b/test/core/templates/skill-templates-parity.test.ts index 05c8db134..decad7b0b 100644 --- a/test/core/templates/skill-templates-parity.test.ts +++ b/test/core/templates/skill-templates-parity.test.ts @@ -61,8 +61,8 @@ const EXPECTED_FUNCTION_HASHES: Record = { getOpsxProposeSkillTemplate: '24623c066f97e34b957d448d1f9a9e8b8a13da3dfce45d45671f6226a2534848', getOpsxProposeCommandTemplate: 'e67ba591efb0fecacb2229d06dfa84af18b825fab8a7b01377279e4f09a06ce4', getFeedbackSkillTemplate: 'd7d83c5f7fc2b92fe8f4588a5bf2d9cb315e4c73ec19bcd5ef28270906319a0d', - getUpdateChangeSkillTemplate: '7aa7351aca25fe2d8d29df4339ca2c768e32c6a59596316bb451831447fa15fc', - getOpsxUpdateCommandTemplate: 'fdc8ba0502910f74e85044e450906f189e939ffd70d5e85b7307a6ec33805148', + getUpdateChangeSkillTemplate: '7dc8abc6f64c58bf34d7581ed4ab095a3b7a53cb372349bee2d840db58622819', + getOpsxUpdateCommandTemplate: 'e2388521b22f92f74561df9a0c2f98e1fa4d265af93b5ba26f42fb47a6c5bfed', }; const EXPECTED_GENERATED_SKILL_CONTENT_HASHES: Record = { @@ -77,7 +77,7 @@ const EXPECTED_GENERATED_SKILL_CONTENT_HASHES: Record = { 'openspec-verify-change': 'af9be013dcbe8c6d8f6d9ab10c893fbd03f4c62933c384d82f63894dd0ceb84f', 'openspec-onboard': 'd53403b4910ab64307862ccf97e70bd8f7174ee44508088fb239c880f0939331', 'openspec-propose': '25d08ed4f031770cea219604167d76bca9f3e89fe0c2f545263674482c6f13f0', - 'openspec-update-change': '5ee000a8bf5507a553fbb9ed666625d11986f9c0bf14edb495648554a4d1c53c', + 'openspec-update-change': '586547406aca94422dfeb3ffedce6c01049429b743f57ce829baa79ebc714d51', }; // Intentionally excludes getFeedbackSkillTemplate: this list only models templates diff --git a/test/core/templates/update-change.test.ts b/test/core/templates/update-change.test.ts index 94f52736b..cf68f234f 100644 --- a/test/core/templates/update-change.test.ts +++ b/test/core/templates/update-change.test.ts @@ -80,7 +80,7 @@ describe('update-change templates', () => { it('explains the optional continue workflow before suggesting it', () => { for (const [label, body] of bodies) { const availabilityGuidance = body.indexOf( - '`/opsx:continue` is an expanded-profile workflow and may not be installed' + '`/opsx:continue` is an optional workflow and may not be installed' ); const firstSuggestion = body.indexOf( '`/opsx:continue`', @@ -110,7 +110,7 @@ describe('update-change templates', () => { expect(body, label).not.toContain('openspec new change ""'); const newAvailabilityCheck = body.indexOf( - 'first verify whether the expanded-profile `/opsx:new` workflow is available' + 'first verify whether the optional `/opsx:new` workflow is available' ); const newRecommendation = body.indexOf('recommend starting fresh with `/opsx:new`'); expect(newAvailabilityCheck, label).toBeGreaterThanOrEqual(0);