diff --git a/skills/openspec-propose/SKILL.md b/skills/openspec-propose/SKILL.md index 3b852b58d3..29677ac457 100644 --- a/skills/openspec-propose/SKILL.md +++ b/skills/openspec-propose/SKILL.md @@ -40,13 +40,32 @@ When the user is ready to implement, they must start the apply workflow explicit If the request contains ambiguity that would materially affect scope, externally observable behavior, compatibility, or acceptance criteria, ask the user before creating the change. For minor details, make a reasonable assumption and record it in the planning artifacts. -2. **Create the change directory** +2. **Determine the workflow schema** + + Use the configured default schema unless the user explicitly requests a different workflow. + + **Use a different schema only if the user:** + - Explicitly requests a specific schema by name → use `--schema ` + - Asks to "show workflows" or asks "what workflows" exist → resolve the authoritative root by running `openspec context --json` from the current working directory. If the user explicitly selected a registered store, use `openspec context --json --store ""`. Then run `openspec schemas --json` with its working directory set to the returned `root.path` and let them choose. This preserves roots selected by a local `store:` pointer or the global `defaultStore`; `schemas` does not accept `--store`. If context reports only `no_openspec_root`, run `openspec schemas --json` from the current working directory instead. Do not use this fallback for invalid or unavailable stores. + + Otherwise, omit `--schema` to preserve the configured default. + +3. **Create the change directory** + + Choose one schema form below. If a registered store is selected, append `--store ""` to that command and each later OpenSpec command shown below that accepts `--store`. + + Using the configured default: ```bash openspec new change "" ``` + + Using an explicitly requested schema: + ```bash + openspec new change "" --schema "" + ``` This creates a scaffolded change in the planning home resolved by the CLI with `.openspec.yaml`. -3. **Get the artifact build order** +4. **Get the artifact build order** ```bash openspec status --change "" --json ``` @@ -55,7 +74,7 @@ When the user is ready to implement, they must start the apply workflow explicit - `artifacts`: list of all artifacts, each with its `status` and its `requires` edges (the artifact IDs it directly depends on) - `planningHome`, `changeRoot`, `artifactPaths`, and `actionContext`: path and scope context. Use these instead of assuming repo-local paths. -4. **Create every artifact in the required set** +5. **Create every artifact in the required set** Use a todo list to track progress through the artifacts. @@ -94,7 +113,7 @@ When the user is ready to implement, they must start the apply workflow explicit - Ask the user to clarify - Then continue with creation -5. **Show final status** +6. **Show final status** ```bash openspec status --change "" ``` diff --git a/src/core/templates/workflows/propose.ts b/src/core/templates/workflows/propose.ts index c8f54c14a1..e82594906a 100644 --- a/src/core/templates/workflows/propose.ts +++ b/src/core/templates/workflows/propose.ts @@ -42,13 +42,32 @@ ${STORE_SELECTION_GUIDANCE} If the request contains ambiguity that would materially affect scope, externally observable behavior, compatibility, or acceptance criteria, ask the user before creating the change. For minor details, make a reasonable assumption and record it in the planning artifacts. -2. **Create the change directory** +2. **Determine the workflow schema** + + Use the configured default schema unless the user explicitly requests a different workflow. + + **Use a different schema only if the user:** + - Explicitly requests a specific schema by name → use \`--schema \` + - Asks to "show workflows" or asks "what workflows" exist → resolve the authoritative root by running \`openspec context --json\` from the current working directory. If the user explicitly selected a registered store, use \`openspec context --json --store ""\`. Then run \`openspec schemas --json\` with its working directory set to the returned \`root.path\` and let them choose. This preserves roots selected by a local \`store:\` pointer or the global \`defaultStore\`; \`schemas\` does not accept \`--store\`. If context reports only \`no_openspec_root\`, run \`openspec schemas --json\` from the current working directory instead. Do not use this fallback for invalid or unavailable stores. + + Otherwise, omit \`--schema\` to preserve the configured default. + +3. **Create the change directory** + + Choose one schema form below. If a registered store is selected, append \`--store ""\` to that command and each later OpenSpec command shown below that accepts \`--store\`. + + Using the configured default: \`\`\`bash openspec new change "" \`\`\` + + Using an explicitly requested schema: + \`\`\`bash + openspec new change "" --schema "" + \`\`\` This creates a scaffolded change in the planning home resolved by the CLI with \`.openspec.yaml\`. -3. **Get the artifact build order** +4. **Get the artifact build order** \`\`\`bash openspec status --change "" --json \`\`\` @@ -57,7 +76,7 @@ ${STORE_SELECTION_GUIDANCE} - \`artifacts\`: list of all artifacts, each with its \`status\` and its \`requires\` edges (the artifact IDs it directly depends on) - \`planningHome\`, \`changeRoot\`, \`artifactPaths\`, and \`actionContext\`: path and scope context. Use these instead of assuming repo-local paths. -4. **Create every artifact in the required set** +5. **Create every artifact in the required set** Use a todo list to track progress through the artifacts. @@ -96,7 +115,7 @@ ${STORE_SELECTION_GUIDANCE} - Ask the user to clarify - Then continue with creation -5. **Show final status** +6. **Show final status** \`\`\`bash openspec status --change "" \`\`\` @@ -170,13 +189,32 @@ ${STORE_SELECTION_GUIDANCE} If the request contains ambiguity that would materially affect scope, externally observable behavior, compatibility, or acceptance criteria, ask the user before creating the change. For minor details, make a reasonable assumption and record it in the planning artifacts. -2. **Create the change directory** +2. **Determine the workflow schema** + + Use the configured default schema unless the user explicitly requests a different workflow. + + **Use a different schema only if the user:** + - Explicitly requests a specific schema by name → use \`--schema \` + - Asks to "show workflows" or asks "what workflows" exist → resolve the authoritative root by running \`openspec context --json\` from the current working directory. If the user explicitly selected a registered store, use \`openspec context --json --store ""\`. Then run \`openspec schemas --json\` with its working directory set to the returned \`root.path\` and let them choose. This preserves roots selected by a local \`store:\` pointer or the global \`defaultStore\`; \`schemas\` does not accept \`--store\`. If context reports only \`no_openspec_root\`, run \`openspec schemas --json\` from the current working directory instead. Do not use this fallback for invalid or unavailable stores. + + Otherwise, omit \`--schema\` to preserve the configured default. + +3. **Create the change directory** + + Choose one schema form below. If a registered store is selected, append \`--store ""\` to that command and each later OpenSpec command shown below that accepts \`--store\`. + + Using the configured default: \`\`\`bash openspec new change "" \`\`\` + + Using an explicitly requested schema: + \`\`\`bash + openspec new change "" --schema "" + \`\`\` This creates a scaffolded change in the planning home resolved by the CLI with \`.openspec.yaml\`. -3. **Get the artifact build order** +4. **Get the artifact build order** \`\`\`bash openspec status --change "" --json \`\`\` @@ -185,7 +223,7 @@ ${STORE_SELECTION_GUIDANCE} - \`artifacts\`: list of all artifacts, each with its \`status\` and its \`requires\` edges (the artifact IDs it directly depends on) - \`planningHome\`, \`changeRoot\`, \`artifactPaths\`, and \`actionContext\`: path and scope context. Use these instead of assuming repo-local paths. -4. **Create every artifact in the required set** +5. **Create every artifact in the required set** Use a todo list to track progress through the artifacts. @@ -224,7 +262,7 @@ ${STORE_SELECTION_GUIDANCE} - Ask the user to clarify - Then continue with creation -5. **Show final status** +6. **Show final status** \`\`\`bash openspec status --change "" \`\`\` diff --git a/test/commands/context.test.ts b/test/commands/context.test.ts index ed1551fd13..ccb7064d58 100644 --- a/test/commands/context.test.ts +++ b/test/commands/context.test.ts @@ -103,6 +103,7 @@ describe('openspec context (4.1)', () => { fs.writeFileSync(path.join(pointerRepo, 'openspec', 'config.yaml'), 'store: team-context\n'); const declared = await runCLI(['context', '--json'], { cwd: pointerRepo, env }); expect(parseJson(declared).root.source).toBe('declared'); + expect(parseJson(declared).root.path).toBe(storeRoot); expect(parseJson(declared).members).toHaveLength(2); // Global-default session: no root, no pointer — provenance must name @@ -116,6 +117,7 @@ describe('openspec context (4.1)', () => { fs.mkdirSync(scratch, { recursive: true }); const fallback = await runCLI(['context', '--json'], { cwd: scratch, env }); expect(parseJson(fallback).root.source).toBe('global_default'); + expect(parseJson(fallback).root.path).toBe(storeRoot); expect(parseJson(fallback).root.store_id).toBe('team-context'); expect(parseJson(fallback).members).toHaveLength(2); }, CONTEXT_MATRIX_TIMEOUT_MS); diff --git a/test/core/templates/propose.test.ts b/test/core/templates/propose.test.ts index 429c6e89d2..e88c8d7786 100644 --- a/test/core/templates/propose.test.ts +++ b/test/core/templates/propose.test.ts @@ -8,6 +8,7 @@ import { getFfChangeSkillTemplate, getOpsxFfCommandTemplate, } from '../../../src/core/templates/skill-templates.js'; +import { generateSkillContent } from '../../../src/core/shared/skill-generation.js'; import { loadSchema } from '../../../src/core/artifact-graph/schema.js'; import { CommandAdapterRegistry } from '../../../src/core/command-generation/registry.js'; import { generateCommand } from '../../../src/core/command-generation/generator.js'; @@ -20,8 +21,8 @@ import { getCommandContents } from '../../../src/core/shared/skill-generation.js const proposeSkillBody = getOpsxProposeSkillTemplate().instructions; const proposeCommandBody = getOpsxProposeCommandTemplate().content; const proposeBodies: Array<[string, string]> = [ - ['propose skill', proposeSkillBody], - ['propose command', proposeCommandBody], + ['propose skill', generateSkillContent(getOpsxProposeSkillTemplate(), 'TEST')], + ['propose command', getOpsxProposeCommandTemplate().content], ]; // ff runs the byte-identical artifact loop, so it carries the identical guards. @@ -153,6 +154,70 @@ describe('propose implementation boundary', () => { }); }); +describe('propose schema selection', () => { + // #770: the CLI and new workflow already accept an explicit schema, but + // propose used to discard that request and always create with the default. + it('shows both concrete creation forms after an explicit schema choice (#770)', () => { + for (const [label, body] of proposeBodies) { + const schemaStep = body.indexOf('**Determine the workflow schema**'); + const createStep = body.indexOf('**Create the change directory**'); + const statusStep = body.indexOf('**Get the artifact build order**'); + + expect(schemaStep, `${label} is missing schema selection`).toBeGreaterThanOrEqual(0); + expect(createStep, `${label} is missing change creation`).toBeGreaterThan(schemaStep); + expect(statusStep, `${label} is missing status lookup`).toBeGreaterThan(createStep); + + const createSection = body.slice(createStep, statusStep); + expect(createSection, label).toMatch(/^\s*openspec new change ""\s*$/m); + expect(createSection, label).toMatch( + /^\s*openspec new change "" --schema ""\s*$/m + ); + expect(createSection, label).toContain( + 'If a registered store is selected, append `--store ""` to that command and each later OpenSpec command shown below that accepts `--store`' + ); + expect(createSection, label).not.toContain('every follow-up command'); + } + }); + + it('discovers schemas from the authoritative project or store root', () => { + for (const [label, body] of proposeBodies) { + const schemaStep = body.indexOf('**Determine the workflow schema**'); + const createStep = body.indexOf('**Create the change directory**'); + const schemaSection = body.slice(schemaStep, createStep); + + expect(schemaSection, label).toContain('Use the configured default schema'); + expect(schemaSection, label).toContain('Explicitly requests a specific schema by name'); + const contextCommand = schemaSection.indexOf('`openspec context --json`'); + const schemasCommand = schemaSection.indexOf('`openspec schemas --json`'); + expect(contextCommand, `${label} is missing root resolution`).toBeGreaterThanOrEqual(0); + expect(schemasCommand, `${label} lists schemas before resolving the root`).toBeGreaterThan( + contextCommand + ); + expect(schemaSection, label).toContain('from the current working directory'); + expect(schemaSection, label).toContain( + '`openspec context --json --store ""`' + ); + expect(schemaSection, label).toContain( + 'run `openspec schemas --json` with its working directory' + ); + expect(schemaSection, label).toContain('returned `root.path`'); + expect(schemaSection, label).toContain('local `store:` pointer'); + expect(schemaSection, label).toContain('global `defaultStore`'); + expect(schemaSection, label).toContain('`schemas` does not accept `--store`'); + expect(schemaSection, label).toContain('context reports only `no_openspec_root`'); + expect(schemaSection, label).toContain( + 'run `openspec schemas --json` from the current working directory instead' + ); + expect(schemaSection, label).toContain( + 'Do not use this fallback for invalid or unavailable stores' + ); + expect(schemaSection, label).toContain( + 'Otherwise, omit `--schema` to preserve the configured default' + ); + } + }); +}); + describe('artifact loop guards (propose and ff)', () => { // `status` is file-existence based (detectCompleted), so writing tasks.md before // specs flips tasks to done and satisfies a bare applyRequires stop condition @@ -258,8 +323,9 @@ describe('artifact loop guards (propose and ff)', () => { } }); - // The step-4 TITLE must not use "apply-ready" either: in the prewritten-tasks - // case the change is already apply-ready when step 4 begins, so a title of + // The artifact-creation TITLE must not use "apply-ready" either: in the + // prewritten-tasks case the change is already apply-ready when this step + // begins, so a title of // "create ... until apply-ready" invites the exact early-stop this PR kills. it('titles the create step around the required set, not "apply-ready"', () => { for (const [label, body] of loopBodies) { diff --git a/test/core/templates/skill-templates-parity.test.ts b/test/core/templates/skill-templates-parity.test.ts index 7a17b6ed55..30b12eb662 100644 --- a/test/core/templates/skill-templates-parity.test.ts +++ b/test/core/templates/skill-templates-parity.test.ts @@ -57,8 +57,8 @@ const EXPECTED_FUNCTION_HASHES: Record = { getOpsxOnboardCommandTemplate: 'e04e4ab6c2f25122e6840212b4c22708812c36ceff9ec529c2bb1d1d035429e3', getOpsxBulkArchiveCommandTemplate: 'fbb4de58ed00861badd93cde9bdd3d7c52f966158a18a660152060076ea9723e', getOpsxVerifyCommandTemplate: 'ce0ee05b7a6b332e29db2298b9d5a928a1932caf516e35fd88f163154ffd43f4', - getOpsxProposeSkillTemplate: 'e175316cc654f78fea4195ee3f5173e544cc3bae35585e200833f26abbb09bd7', - getOpsxProposeCommandTemplate: '1085c01d9ce9ca576eab43887a6700007f30001978b624f7e004df7beb577028', + getOpsxProposeSkillTemplate: '416200ae0277061405d17d5506243657ee26f7b883abe063844126c497d88f94', + getOpsxProposeCommandTemplate: '8de5ce5fe15c0b13ee1801b6b18cb86dc16ddea66c34223fafb4360232d8424d', getFeedbackSkillTemplate: 'd7d83c5f7fc2b92fe8f4588a5bf2d9cb315e4c73ec19bcd5ef28270906319a0d', getUpdateChangeSkillTemplate: 'f85fbfb3a175e949becbef08be0eccfab97de5e7ad45105e999d2900dfafbaba', getOpsxUpdateCommandTemplate: '461edf06e92c0da3dab4f11d91d59d44b48ed30a0881c1f34a714b1813435af6', @@ -75,7 +75,7 @@ const EXPECTED_GENERATED_SKILL_CONTENT_HASHES: Record = { 'openspec-bulk-archive-change': 'c58e1d319a6587b52202434d5d769c94718aafc0f019276cef04cf8be473b6ce', 'openspec-verify-change': '7cd65897d126f7c948620c0672ca62418620dbcb82ee73d890f758fb666a4ff8', 'openspec-onboard': '80f39cf33a138aac8e508db25d7af2c9e9bd482f90e414770e806f966dd58c9c', - 'openspec-propose': '37818ab54ffc8e60a51ec8cd9913eec8735645ea0c6c46a19e89de9b573dcf2c', + 'openspec-propose': '48b06cf0fa53be06c84fc3e79729fb16b7b9d8549cbed6d89616eb6ba1f7e325', 'openspec-update-change': '95bb533105e49aee06c9ea164b63092de77644cf8f94fa38d3ee3c11b0ccb893', };