Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
27 changes: 23 additions & 4 deletions skills/openspec-propose/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 <schema-name>`
- 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 "<store-id>"`. 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 "<store-id>"` to that command and each later OpenSpec command shown below that accepts `--store`.

Using the configured default:
```bash
openspec new change "<name>"
```

Using an explicitly requested schema:
```bash
openspec new change "<name>" --schema "<schema-name>"
```
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 "<name>" --json
```
Expand All @@ -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.

Expand Down Expand Up @@ -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 "<name>"
```
Expand Down
54 changes: 46 additions & 8 deletions src/core/templates/workflows/propose.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 <schema-name>\`
- 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 "<store-id>"\`. 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 "<store-id>"\` to that command and each later OpenSpec command shown below that accepts \`--store\`.

Using the configured default:
\`\`\`bash
openspec new change "<name>"
\`\`\`

Using an explicitly requested schema:
\`\`\`bash
openspec new change "<name>" --schema "<schema-name>"
\`\`\`
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 "<name>" --json
\`\`\`
Expand All @@ -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.

Expand Down Expand Up @@ -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 "<name>"
\`\`\`
Expand Down Expand Up @@ -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 <schema-name>\`
- 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 "<store-id>"\`. 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 "<store-id>"\` to that command and each later OpenSpec command shown below that accepts \`--store\`.

Using the configured default:
\`\`\`bash
openspec new change "<name>"
\`\`\`

Using an explicitly requested schema:
\`\`\`bash
openspec new change "<name>" --schema "<schema-name>"
\`\`\`
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 "<name>" --json
\`\`\`
Expand All @@ -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.

Expand Down Expand Up @@ -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 "<name>"
\`\`\`
Expand Down
2 changes: 2 additions & 0 deletions test/commands/context.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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);
Expand Down
74 changes: 70 additions & 4 deletions test/core/templates/propose.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand All @@ -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.
Expand Down Expand Up @@ -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 "<name>"\s*$/m);
expect(createSection, label).toMatch(
/^\s*openspec new change "<name>" --schema "<schema-name>"\s*$/m
);
expect(createSection, label).toContain(
'If a registered store is selected, append `--store "<store-id>"` 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 "<store-id>"`'
);
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
Expand Down Expand Up @@ -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) {
Expand Down
6 changes: 3 additions & 3 deletions test/core/templates/skill-templates-parity.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -57,8 +57,8 @@ const EXPECTED_FUNCTION_HASHES: Record<string, string> = {
getOpsxOnboardCommandTemplate: 'e04e4ab6c2f25122e6840212b4c22708812c36ceff9ec529c2bb1d1d035429e3',
getOpsxBulkArchiveCommandTemplate: 'fbb4de58ed00861badd93cde9bdd3d7c52f966158a18a660152060076ea9723e',
getOpsxVerifyCommandTemplate: 'ce0ee05b7a6b332e29db2298b9d5a928a1932caf516e35fd88f163154ffd43f4',
getOpsxProposeSkillTemplate: 'e175316cc654f78fea4195ee3f5173e544cc3bae35585e200833f26abbb09bd7',
getOpsxProposeCommandTemplate: '1085c01d9ce9ca576eab43887a6700007f30001978b624f7e004df7beb577028',
getOpsxProposeSkillTemplate: '416200ae0277061405d17d5506243657ee26f7b883abe063844126c497d88f94',
getOpsxProposeCommandTemplate: '8de5ce5fe15c0b13ee1801b6b18cb86dc16ddea66c34223fafb4360232d8424d',
getFeedbackSkillTemplate: 'd7d83c5f7fc2b92fe8f4588a5bf2d9cb315e4c73ec19bcd5ef28270906319a0d',
getUpdateChangeSkillTemplate: 'f85fbfb3a175e949becbef08be0eccfab97de5e7ad45105e999d2900dfafbaba',
getOpsxUpdateCommandTemplate: '461edf06e92c0da3dab4f11d91d59d44b48ed30a0881c1f34a714b1813435af6',
Expand All @@ -75,7 +75,7 @@ const EXPECTED_GENERATED_SKILL_CONTENT_HASHES: Record<string, string> = {
'openspec-bulk-archive-change': 'c58e1d319a6587b52202434d5d769c94718aafc0f019276cef04cf8be473b6ce',
'openspec-verify-change': '7cd65897d126f7c948620c0672ca62418620dbcb82ee73d890f758fb666a4ff8',
'openspec-onboard': '80f39cf33a138aac8e508db25d7af2c9e9bd482f90e414770e806f966dd58c9c',
'openspec-propose': '37818ab54ffc8e60a51ec8cd9913eec8735645ea0c6c46a19e89de9b573dcf2c',
'openspec-propose': '48b06cf0fa53be06c84fc3e79729fb16b7b9d8549cbed6d89616eb6ba1f7e325',
'openspec-update-change': '95bb533105e49aee06c9ea164b63092de77644cf8f94fa38d3ee3c11b0ccb893',
};

Expand Down
Loading