fix: make onboarding guidance tool-aware

This commit is contained in:
alfred-openspec
2026-04-11 22:04:18 -07:00
parent a18d992fa1
commit bba45f3028
13 changed files with 355 additions and 44 deletions
+13 -2
View File
@@ -98,9 +98,20 @@ cd your-project
openspec init
```
Now tell your AI: `/opsx:propose <what-you-want-to-build>`
By default, OpenSpec installs the `core` workflow set only:
- `propose`
- `explore`
- `apply`
- `archive`
If you want the expanded workflow (`/opsx:new`, `/opsx:continue`, `/opsx:ff`, `/opsx:verify`, `/opsx:sync`, `/opsx:bulk-archive`, `/opsx:onboard`), select it with `openspec config profile` and apply with `openspec update`.
Then tell your AI using the syntax that matches your tool:
- **Claude Code and other colon-style tools:** `/opsx:propose <what-you-want-to-build>`
- **OpenCode, Cursor, Windsurf, Copilot IDE, and other hyphen-style tools:** `/opsx-propose <what-you-want-to-build>`
- **Skills-only delivery or skill-based tools:** `/openspec-propose <what-you-want-to-build>`
If command files are missing, your setup may be using `skills` delivery by design. See [Supported Tools](docs/supported-tools.md) for the full delivery and syntax model.
If you want the expanded workflow (`/opsx:new`, `/opsx:continue`, `/opsx:ff`, `/opsx:verify`, `/opsx:sync`, `/opsx:bulk-archive`, `/opsx:onboard`), run `openspec config profile` and then `openspec update`.
> [!NOTE]
> Not sure if your tool is supported? [View the full list](docs/supported-tools.md) – we support 25+ tools and growing.
+30 -26
View File
@@ -4,28 +4,42 @@ This is the reference for OpenSpec's slash commands. These commands are invoked
For workflow patterns and when to use each command, see [Workflows](workflows.md). For CLI commands, see [CLI](cli.md).
## First: Use the Syntax Your Tool Expects
OpenSpec command intent is shared, but the invocation syntax is tool-dependent. Check this before debugging a setup.
| Tool family | Syntax example |
|-------------|----------------|
| Claude Code and other colon-style integrations | `/opsx:propose`, `/opsx:apply` |
| OpenCode, Cursor, Windsurf, GitHub Copilot IDE, Bob Shell, Pi, and other hyphen-style integrations | `/opsx-propose`, `/opsx-apply` |
| Skill-based tools or skills-only delivery | `/openspec-propose`, `/openspec-apply-change` |
If you are not sure which surface your tool uses, check [Supported Tools](supported-tools.md).
## Quick Reference
By default, OpenSpec installs the `core` profile only: `propose`, `explore`, `apply`, and `archive`. Expanded workflows are opt-in.
### Default Quick Path (`core` profile)
| Command | Purpose |
| Command intent | Purpose |
|---------|---------|
| `/opsx:propose` | Create a change and generate planning artifacts in one step |
| `/opsx:explore` | Think through ideas before committing to a change |
| `/opsx:apply` | Implement tasks from the change |
| `/opsx:archive` | Archive a completed change |
| `propose` | Create a change and generate planning artifacts in one step |
| `explore` | Think through ideas before committing to a change |
| `apply` | Implement tasks from the change |
| `archive` | Archive a completed change |
### Expanded Workflow Commands (custom workflow selection)
### Expanded Workflow Commands (opt-in via `openspec config profile` + `openspec update`)
| Command | Purpose |
| Command intent | Purpose |
|---------|---------|
| `/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:verify` | Validate implementation matches artifacts |
| `/opsx:sync` | Merge delta specs into main specs |
| `/opsx:bulk-archive` | Archive multiple changes at once |
| `/opsx:onboard` | Guided tutorial through the complete workflow |
| `new` | Start a new change scaffold |
| `continue` | Create the next artifact based on dependencies |
| `ff` | Fast-forward: create all planning artifacts at once |
| `verify` | Validate implementation matches artifacts |
| `sync` | Merge delta specs into main specs |
| `bulk-archive` | Archive multiple changes at once |
| `onboard` | Guided tutorial through the complete workflow |
The default global profile is `core`. To enable expanded workflow commands, run `openspec config profile`, select workflows, then run `openspec update` in your project.
@@ -610,19 +624,9 @@ AI: Welcome to OpenSpec!
## Command Syntax by AI Tool
Different AI tools use slightly different command syntax. Use the format that matches your tool:
The syntax table now lives near the top of this page because it is usually the first thing to verify.
| Tool | Syntax Example |
|------|----------------|
| Claude Code | `/opsx:propose`, `/opsx:apply` |
| Cursor | `/opsx-propose`, `/opsx-apply` |
| Windsurf | `/opsx-propose`, `/opsx-apply` |
| Copilot (IDE) | `/opsx-propose`, `/opsx-apply` |
| Trae | Skill-based invocations such as `/openspec-propose`, `/openspec-apply-change` (no generated `opsx-*` command files) |
The intent is the same across tools, but how commands are surfaced can differ by integration.
> **Note:** GitHub Copilot commands (`.github/prompts/*.prompt.md`) are only available in IDE extensions (VS Code, JetBrains, Visual Studio). GitHub Copilot CLI does not currently support custom prompt files — see [Supported Tools](supported-tools.md) for details and workarounds.
> **Note:** GitHub Copilot commands (`.github/prompts/*.prompt.md`) are only available in IDE extensions (VS Code, JetBrains, Visual Studio). GitHub Copilot CLI does not currently support custom prompt files. See [Supported Tools](supported-tools.md) for details and workarounds.
---
+3
View File
@@ -20,6 +20,9 @@ OpenSpec helps you and your AI coding assistant agree on what to build before an
The default global profile is `core`, which includes `propose`, `explore`, `apply`, and `archive`. You can enable the expanded workflow commands with `openspec config profile` and then `openspec update`.
> [!NOTE]
> Examples on this page use Claude-style `/opsx:...` syntax for readability. If your tool expects `/opsx-...` or direct `openspec-*` skill invocations instead, check [Commands](commands.md#first-use-the-syntax-your-tool-expects).
## What OpenSpec Creates
After running `openspec init`, your project has this structure:
+41
View File
@@ -17,6 +17,20 @@ By default, OpenSpec uses the `core` profile, which includes:
You can enable expanded workflows (`new`, `continue`, `ff`, `verify`, `sync`, `bulk-archive`, `onboard`) via `openspec config profile`, then run `openspec update`.
## What to Expect After `openspec init`
- **Only 4 workflows installed?** That is the default `core` profile. You should expect `propose`, `explore`, `apply`, and `archive` unless you explicitly opt into a broader workflow set.
- **Missing command files?** Check your delivery mode. `skills` delivery installs only `openspec-*` skills, `commands` delivery installs only `opsx-*` command files, and `both` installs both surfaces when the tool supports both.
- **Wrong command syntax?** The same workflow may surface as `/opsx:propose`, `/opsx-propose`, or `/openspec-propose` depending on the tool and delivery mode.
## Command Syntax by Tool Family
| Tool family | Syntax example |
|-------------|----------------|
| Claude Code and other colon-style integrations | `/opsx:propose`, `/opsx:apply` |
| OpenCode, Cursor, Windsurf, GitHub Copilot IDE, Bob Shell, Pi, and other hyphen-style integrations | `/opsx-propose`, `/opsx-apply` |
| ForgeCode, Trae, or any project using skills-only delivery | `/openspec-propose`, `/openspec-apply-change` |
## Tool Directory Reference
| Tool (ID) | Skills path pattern | Command path pattern |
@@ -101,6 +115,33 @@ When selected by profile/workflow config, OpenSpec generates these skills:
See [Commands](commands.md) for command behavior and [CLI](cli.md) for `init`/`update` options.
## Troubleshooting
### Why did I only get 4 workflows?
Because OpenSpec defaults to the `core` profile. Run `openspec config profile`, choose the expanded workflows you want, then run `openspec update` in the project.
### Why are command files missing?
Two common reasons:
1. **Delivery is set to `skills`**. That installs only `openspec-*` skills, so missing `opsx-*` files are expected.
2. **Your tool is skill-based**. Tools such as ForgeCode and Trae do not currently get generated `opsx-*` command files, so use the installed skills instead.
### Which syntax should I use?
Use the syntax family that matches your tool:
- `/opsx:...` for colon-style integrations
- `/opsx-...` for hyphen-style integrations
- `/openspec-...` when you are using skills directly
If you are unsure, compare your generated file paths against the table above or check [Commands](commands.md) first.
### Why does OpenCode documentation sometimes mention `.opencode/command/` and sometimes `.opencode/commands/`?
Current OpenSpec releases generate `.opencode/commands/`. Older OpenCode setups and older docs may still mention `.opencode/command/`. If you see the singular path in older reports, treat it as version drift rather than a separate workflow model.
## Related
- [CLI Reference](cli.md) — Terminal commands
+11 -6
View File
@@ -45,6 +45,7 @@ import { getGlobalConfig, type Delivery, type Profile } from './global-config.js
import { getProfileWorkflows, CORE_WORKFLOWS, ALL_WORKFLOWS } from './profiles.js';
import { getAvailableTools } from './available-tools.js';
import { migrateIfNeeded } from './migration.js';
import { buildOnboardingGuidance } from './onboarding-guidance.js';
const require = createRequire(import.meta.url);
const { version: OPENSPEC_VERSION } = require('../../package.json');
@@ -696,17 +697,21 @@ export class InitCommand {
console.log(chalk.dim(`Config: skipped (non-interactive mode)`));
}
// Getting started (task 7.6: show propose if in profile)
// Getting started
const globalCfg = getGlobalConfig();
const activeProfile: Profile = (this.profileOverride as Profile) ?? globalCfg.profile ?? 'core';
const activeWorkflows = [...getProfileWorkflows(activeProfile, globalCfg.workflows)];
console.log();
if (activeWorkflows.includes('propose')) {
if (activeWorkflows.length > 0) {
console.log(chalk.bold('Getting started:'));
console.log(' Start your first change: /opsx:propose "your idea"');
} else if (activeWorkflows.includes('new')) {
console.log(chalk.bold('Getting started:'));
console.log(' Start your first change: /opsx:new "your idea"');
const onboardingLines = buildOnboardingGuidance({
workflows: activeWorkflows,
delivery: globalCfg.delivery ?? 'both',
toolIds: successfulTools.map((tool) => tool.value),
});
for (const line of onboardingLines) {
console.log(` ${line}`);
}
} else {
console.log("Done. Run 'openspec config profile' to configure your workflows.");
}
+9 -1
View File
@@ -10,6 +10,7 @@ import { getGlobalConfig, getGlobalConfigPath, saveGlobalConfig, type Delivery }
import { CommandAdapterRegistry } from './command-generation/index.js';
import { WORKFLOW_TO_SKILL_DIR } from './profile-sync-drift.js';
import { ALL_WORKFLOWS } from './profiles.js';
import { buildOnboardingGuidance } from './onboarding-guidance.js';
import path from 'path';
import * as fs from 'fs';
@@ -127,5 +128,12 @@ export function migrateIfNeeded(projectPath: string, tools: AIToolOption[]): voi
saveGlobalConfig(config);
console.log(`Migrated: custom profile with ${installedWorkflows.length} workflows`);
console.log("New in this version: /opsx:propose. Try 'openspec config profile core' for the streamlined experience.");
const onboardingLines = buildOnboardingGuidance({
workflows: installedWorkflows,
delivery: config.delivery ?? inferDelivery(artifacts),
});
for (const line of onboardingLines) {
console.log(line);
}
console.log("New projects default to the streamlined core profile. Run 'openspec config profile core', then 'openspec update' if you want that setup here.");
}
+144
View File
@@ -0,0 +1,144 @@
import path from 'path';
import { AI_TOOLS } from './config.js';
import { CommandAdapterRegistry } from './command-generation/index.js';
import type { Delivery } from './global-config.js';
const WORKFLOW_TO_SKILL_NAME: Record<string, string> = {
'explore': 'openspec-explore',
'new': 'openspec-new-change',
'continue': 'openspec-continue-change',
'apply': 'openspec-apply-change',
'ff': 'openspec-ff-change',
'sync': 'openspec-sync-specs',
'archive': 'openspec-archive-change',
'bulk-archive': 'openspec-bulk-archive-change',
'verify': 'openspec-verify-change',
'onboard': 'openspec-onboard',
'propose': 'openspec-propose',
};
const EXPANDED_WORKFLOW_IDS = ['new', 'continue', 'ff', 'verify', 'sync'];
export interface OnboardingGuidanceOptions {
workflows: string[];
delivery: Delivery;
toolIds?: string[];
maxExamples?: number;
}
function describeDelivery(delivery: Delivery): string {
if (delivery === 'skills') return 'skills only';
if (delivery === 'commands') return 'commands only';
return 'both (skills + commands)';
}
function getToolLabel(toolId: string): string {
const tool = AI_TOOLS.find((candidate) => candidate.value === toolId);
return tool?.successLabel ?? tool?.name ?? toolId;
}
function getPrimaryWorkflow(workflows: string[]): string | undefined {
if (workflows.includes('propose')) return 'propose';
if (workflows.includes('new')) return 'new';
return workflows[0];
}
function getSkillInvocation(workflowId: string): string | null {
const skillName = WORKFLOW_TO_SKILL_NAME[workflowId];
if (!skillName) return null;
return `/${skillName}`;
}
function getCommandInvocation(toolId: string, workflowId: string): string | null {
const adapter = CommandAdapterRegistry.get(toolId);
if (!adapter) return null;
const normalizedPath = adapter.getFilePath(workflowId).replace(/\\/g, '/');
const basename = path.posix.basename(normalizedPath);
if (normalizedPath.includes('/opsx/')) {
return `/opsx:${workflowId}`;
}
if (basename.startsWith('opsx-')) {
return `/opsx-${workflowId}`;
}
return `/opsx:${workflowId}`;
}
function getInvocationExample(toolId: string, workflowId: string, delivery: Delivery): string | null {
if (delivery !== 'skills') {
const commandInvocation = getCommandInvocation(toolId, workflowId);
if (commandInvocation) {
return `${commandInvocation} \"your idea\"`;
}
}
if (delivery !== 'commands' || !getCommandInvocation(toolId, workflowId)) {
const skillInvocation = getSkillInvocation(workflowId);
if (skillInvocation) {
return `${skillInvocation} \"your idea\"`;
}
}
return null;
}
function getSkillOnlyTools(toolIds: string[]): string[] {
return toolIds.filter((toolId) => CommandAdapterRegistry.get(toolId) == null);
}
function buildGroupedExamples(toolIds: string[], workflowId: string, delivery: Delivery): string[] {
const grouped = new Map<string, string[]>();
for (const toolId of toolIds) {
const example = getInvocationExample(toolId, workflowId, delivery);
if (!example) continue;
const labels = grouped.get(example) ?? [];
labels.push(getToolLabel(toolId));
grouped.set(example, labels);
}
return [...grouped.entries()].map(([example, labels]) => `${labels.join(', ')}: ${example}`);
}
export function buildOnboardingGuidance(options: OnboardingGuidanceOptions): string[] {
const workflows = [...options.workflows];
const delivery = options.delivery;
const toolIds = [...new Set(options.toolIds ?? [])];
const maxExamples = options.maxExamples ?? 3;
const lines: string[] = [];
if (workflows.length > 0) {
lines.push(`Available workflows: ${workflows.join(', ')}`);
}
lines.push(`Delivery: ${describeDelivery(delivery)}`);
const primaryWorkflow = getPrimaryWorkflow(workflows);
if (primaryWorkflow && toolIds.length > 0) {
const examples = buildGroupedExamples(toolIds, primaryWorkflow, delivery).slice(0, maxExamples);
if (examples.length > 0) {
lines.push('Try:');
for (const example of examples) {
lines.push(` ${example}`);
}
}
}
if (!EXPANDED_WORKFLOW_IDS.every((workflowId) => workflows.includes(workflowId))) {
lines.push('Want expanded workflows like /opsx:new and /opsx:continue? Run `openspec config profile`, then `openspec update`.');
}
if (delivery === 'skills') {
lines.push('Command files were not generated because delivery is set to skills-only.');
}
const skillOnlyTools = getSkillOnlyTools(toolIds);
if (skillOnlyTools.length > 0 && delivery !== 'skills') {
lines.push(`These tools use skill-based invocations here: ${skillOnlyTools.map(getToolLabel).join(', ')}.`);
}
return lines;
}
+9 -3
View File
@@ -37,6 +37,7 @@ import { isInteractive } from '../utils/interactive.js';
import { getGlobalConfig, type Delivery } from './global-config.js';
import { getProfileWorkflows, ALL_WORKFLOWS } from './profiles.js';
import { getAvailableTools } from './available-tools.js';
import { buildOnboardingGuidance } from './onboarding-guidance.js';
import {
WORKFLOW_TO_SKILL_DIR,
getCommandConfiguredTools,
@@ -268,9 +269,14 @@ export class UpdateCommand {
if (newlyConfiguredTools.length > 0) {
console.log();
console.log(chalk.bold('Getting started:'));
console.log(' /opsx:new Start a new change');
console.log(' /opsx:continue Create the next artifact');
console.log(' /opsx:apply Implement tasks');
const onboardingLines = buildOnboardingGuidance({
workflows: desiredWorkflows,
delivery,
toolIds: newlyConfiguredTools,
});
for (const line of onboardingLines) {
console.log(` ${line}`);
}
console.log();
console.log(`Learn more: ${chalk.cyan('https://github.com/Fission-AI/OpenSpec')}`);
}
+4 -4
View File
@@ -24,10 +24,10 @@ function getWelcomeText(): string[] {
chalk.dim(' • Agent Skills for AI tools'),
chalk.dim(' • /opsx:* slash commands'),
'',
chalk.white('Quick start after setup:'),
` ${chalk.yellow('/opsx:new')} ${chalk.dim('Create a change')}`,
` ${chalk.yellow('/opsx:continue')} ${chalk.dim('Next artifact')}`,
` ${chalk.yellow('/opsx:apply')} ${chalk.dim('Implement tasks')}`,
chalk.white('Default workflow after setup:'),
` ${chalk.yellow('/opsx:propose')} ${chalk.dim('Start a change on colon-style tools')}`,
` ${chalk.yellow('/opsx-propose')} ${chalk.dim('Start a change on hyphen-style tools')}`,
` ${chalk.yellow('core')} ${chalk.dim('Installs propose, explore, apply, archive')}`,
'',
chalk.cyan('Press Enter to select tools...'),
];
+25
View File
@@ -741,6 +741,31 @@ describe('InitCommand - profile and detection features', () => {
const skillFile = path.join(testDir, '.claude', 'skills', 'openspec-explore', 'SKILL.md');
expect(await fileExists(skillFile)).toBe(true);
});
it('should print tool-aware onboarding guidance after setup', async () => {
const initCommand = new InitCommand({ tools: 'opencode', force: true });
await initCommand.execute(testDir);
const logCalls = (console.log as unknown as { mock: { calls: unknown[][] } }).mock.calls.flat().map(String);
expect(logCalls.some((entry) => entry.includes('Available workflows: propose, explore, apply, archive'))).toBe(true);
expect(logCalls.some((entry) => entry.includes('OpenCode: /opsx-propose "your idea"'))).toBe(true);
expect(logCalls.some((entry) => entry.includes('Want expanded workflows like /opsx:new and /opsx:continue?'))).toBe(true);
});
it('should explain skills-only delivery in setup output', async () => {
saveGlobalConfig({
featureFlags: {},
profile: 'core',
delivery: 'skills',
});
const initCommand = new InitCommand({ tools: 'claude', force: true });
await initCommand.execute(testDir);
const logCalls = (console.log as unknown as { mock: { calls: unknown[][] } }).mock.calls.flat().map(String);
expect(logCalls.some((entry) => entry.includes('Delivery: skills only'))).toBe(true);
expect(logCalls.some((entry) => entry.includes('Command files were not generated because delivery is set to skills-only.'))).toBe(true);
});
});
async function fileExists(filePath: string): Promise<boolean> {
+17 -1
View File
@@ -1,4 +1,4 @@
import { describe, it, expect, beforeEach, afterEach } from 'vitest';
import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest';
import path from 'path';
import os from 'os';
import { randomUUID } from 'crypto';
@@ -135,6 +135,22 @@ describe('migration', () => {
expect(fs.existsSync(getGlobalConfigPath())).toBe(false);
});
it('prints workflow and delivery guidance during migration', async () => {
const consoleSpy = vi.spyOn(console, 'log').mockImplementation(() => undefined);
await writeSkill(projectDir, 'openspec-explore');
await writeManagedCommand(projectDir, 'apply');
migrateIfNeeded(projectDir, [ensureClaudeTool()]);
const logLines = consoleSpy.mock.calls.flat().map(String);
expect(logLines.some((line) => line.includes('Available workflows: explore, apply'))).toBe(true);
expect(logLines.some((line) => line.includes('Delivery: both (skills + commands)'))).toBe(true);
expect(logLines.some((line) => line.includes("Run 'openspec config profile core', then 'openspec update'"))).toBe(true);
consoleSpy.mockRestore();
});
it('ignores unknown custom skill and command files when scanning workflows', async () => {
await writeSkill(projectDir, 'my-custom-skill');
const customCommandPath = path.join(projectDir, '.claude', 'commands', 'opsx', 'my-custom.md');
+48
View File
@@ -0,0 +1,48 @@
import { describe, expect, it } from 'vitest';
import { buildOnboardingGuidance } from '../../src/core/onboarding-guidance.js';
describe('buildOnboardingGuidance', () => {
it('uses colon syntax for Claude Code command examples', () => {
const lines = buildOnboardingGuidance({
workflows: ['propose', 'explore', 'apply', 'archive'],
delivery: 'both',
toolIds: ['claude'],
});
expect(lines).toContain('Available workflows: propose, explore, apply, archive');
expect(lines).toContain('Delivery: both (skills + commands)');
expect(lines).toContain(' Claude Code: /opsx:propose "your idea"');
});
it('uses hyphen syntax for OpenCode command examples', () => {
const lines = buildOnboardingGuidance({
workflows: ['propose', 'explore', 'apply', 'archive'],
delivery: 'both',
toolIds: ['opencode'],
});
expect(lines).toContain(' OpenCode: /opsx-propose "your idea"');
});
it('falls back to skill invocations for skills-only delivery', () => {
const lines = buildOnboardingGuidance({
workflows: ['propose', 'explore', 'apply', 'archive'],
delivery: 'skills',
toolIds: ['claude'],
});
expect(lines).toContain(' Claude Code: /openspec-propose "your idea"');
expect(lines).toContain('Command files were not generated because delivery is set to skills-only.');
});
it('explains skill-based tools when no command adapter exists', () => {
const lines = buildOnboardingGuidance({
workflows: ['propose', 'explore', 'apply', 'archive'],
delivery: 'both',
toolIds: ['forgecode'],
});
expect(lines).toContain(' ForgeCode: /openspec-propose "your idea"');
expect(lines).toContain('These tools use skill-based invocations here: ForgeCode.');
});
});
+1 -1
View File
@@ -1142,7 +1142,7 @@ More user content after markers.
expect.stringContaining('Getting started')
);
expect(consoleSpy).toHaveBeenCalledWith(
expect.stringContaining('/opsx:new')
expect.stringContaining('/opsx:propose')
);
// Skills should be created