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
7 changes: 7 additions & 0 deletions .changeset/profile-aware-workflow-references.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
---
"@fission-ai/openspec": patch
---

Generated skills and commands no longer point at workflows the active profile does not install. On the default `core` profile, the update workflow told agents to hand off to `/opsx:continue` for missing artifacts and to `/opsx:new` for a change of intent, neither of which `core` generates. Every cross-workflow handoff is now decided at generation time against the installed workflow set, and renders a concrete CLI fallback (`openspec status`, `openspec instructions`, `openspec archive`) when the workflow it would name is absent, rather than relying on a runtime availability check the agent had to perform. The onboarding tutorial's command tables are likewise built from the workflows you actually have.

Also folds in #1735, which fixed the same issue (#1734) by removing the optional handoffs outright. The CLI's own runtime instructions no longer name the `openspec-continue-change` skill either, since those strings are chosen at run time and cannot be resolved against a profile; and the blocked-state fallback now carries the full CLI recovery (select the next `ready` artifact from `openspec status`, read its rules with `openspec instructions`, keep the selected `--store`) rather than a one-line pointer.
6 changes: 4 additions & 2 deletions docs-lab/reference/skills.md
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,8 @@ The skills come in two sets:
| [openspec-bulk-archive-change](#openspec-bulk-archive-change) | Archive several change proposals at once | Optional |
| [openspec-onboard](#openspec-onboard) | Learn the workflow by doing one real change proposal end to end | Optional |

Each entry below names the skill that owns the next step. When your profile leaves that skill out, the installed files never name it: the handoff becomes the equivalent `openspec` command, or a plain request to you, and a line that exists only to point at a missing skill is not written at all. So the skills you have always hand off to skills you have. Which set you get is [Profiles](../customize/profiles.md).

## openspec-explore

Think through an idea before it becomes a change proposal.
Expand Down Expand Up @@ -82,7 +84,7 @@ Implement a change proposal's tasks, working through the list until done or bloc
|---|---|
| **Arguments** | A change proposal name (`add-auth`), optional. If the target is ambiguous it lists the active change proposals and asks you to pick. |
| **Creates** | Code: the minimal changes each task calls for, in your project files. In the change proposal it touches only the tasks file, checking off each finished task (`- [ ]` to `- [x]`). |
| **Response** | Progress per task, then an overall count (N/M tasks complete). All done: suggests `openspec-archive-change`. Blocked by missing artifacts: points to `openspec-continue-change`. Unclear tasks or errors: pauses and asks. |
| **Response** | Progress per task, then an overall count (N/M tasks complete). All done: suggests `openspec-archive-change`. Blocked by missing artifacts: points to `openspec-continue-change`, or to `openspec status` and `openspec instructions` when that skill is not installed (the core profile leaves it out). Unclear tasks or errors: pauses and asks. |

## openspec-update-change

Expand All @@ -92,7 +94,7 @@ other.
| Contract | Description |
|---|---|
| **Arguments** | A change proposal name, optional, plus the revision you want. With no revision stated it runs a coherence review: artifacts checked against each other for contradictions, gaps, and duplication. |
| **Creates** | Nothing new. Edits only artifact files that already exist. Missing artifacts are `openspec-continue-change`'s job. Never code. |
| **Creates** | Nothing new. Edits only artifact files that already exist. Missing artifacts are `openspec-continue-change`'s job. Without that skill (the core profile leaves it out), it points to `openspec status` and `openspec instructions` instead. Never code. |
| **Response** | Shows each proposed revision and writes it only after you confirm, one artifact at a time. Ends with what was revised and the next step; implementation waits for `openspec-apply-change`. |

## openspec-sync-specs
Expand Down
5 changes: 4 additions & 1 deletion skills/openspec-apply-change/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -48,9 +48,12 @@ Implement tasks from an OpenSpec change.
- Dynamic instruction based on current state
- Optional `context`: current required project instruction input from the selected root
- Optional `operationGuidance`: current advisory guidance for apply
- `missingArtifacts` (when present): required artifact ids with no output

**Handle states:**
- If `state: "blocked"` (missing artifacts): show message, suggest using `/openspec-continue-change` (if it is not installed, run `openspec status --change "<name>" --json` to see the next artifact and `openspec instructions <artifact-id> --change "<name>" --json` for how to create it)
- If `state: "blocked"`: show the message and pause implementation.
- If `missingArtifacts` is non-empty: suggest using `/openspec-continue-change` to create them.
- Otherwise, follow the CLI instruction to create or repair the schema-configured tracking file from existing planning artifacts. Do not assume another artifact is ready or start implementation while blocked.
- If `state: "all_done"`: congratulate, suggest archive
- Otherwise: proceed to implementation

Expand Down
46 changes: 18 additions & 28 deletions skills/openspec-onboard/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -472,23 +472,18 @@ This same rhythm works for any size change—a small fix or a major feature.

## Command Reference

**Core workflow:**
**The commands you have installed:**

| Command | What it does |
|-------------------|--------------------------------------------|
| Command | What it does |
|------------------|--------------------------------------------|
| `/openspec-propose` | Create a change and generate all artifacts |
| `/openspec-explore` | Think through problems before/during work |
| `/openspec-apply-change` | Implement tasks from a change |
| `/openspec-archive-change` | Archive a completed change |

**Additional commands** (only if installed - availability depends on your profile):

| Command | What it does |
|--------------------|----------------------------------------------------------|
| `/openspec-new-change` | Start a new change, step through artifacts one at a time |
| `/openspec-continue-change` | Continue working on an existing change |
| `/openspec-ff-change` | Fast-forward: create all artifacts at once |
| `/openspec-verify-change` | Verify implementation matches artifacts |
| `/openspec-new-change` | Start a new change, one artifact at a time |
| `/openspec-continue-change` | Continue working on an existing change |
| `/openspec-ff-change` | Fast-forward: create all artifacts at once |
| `/openspec-verify-change` | Verify implementation matches artifacts |

---

Expand All @@ -508,8 +503,8 @@ If the user says they need to stop, want to pause, or seem disengaged:
```
No problem! Your change is saved at the `changeRoot` reported by `openspec status --change "<name>" --json`.

To pick up where we left off later:
- `/openspec-continue-change <name>` - Resume artifact creation (if installed; otherwise `openspec status --change "<name>" --json` shows the next artifact)
To pick up where we left off later, `openspec status --change "<name>" --json` shows exactly where the change stands.
- `/openspec-continue-change <name>` - Resume artifact creation
- `/openspec-apply-change <name>` - Jump to implementation (if tasks exist)

The work won't be lost. Come back whenever you're ready.
Expand All @@ -524,23 +519,18 @@ If the user says they just want to see the commands or skip the tutorial:
```
## OpenSpec Quick Reference

**Core workflow:**
**The commands you have installed:**

| Command | What it does |
|--------------------------|--------------------------------------------|
| `/openspec-propose <name>` | Create a change and generate all artifacts |
| `/openspec-explore` | Think through problems (no code changes) |
| `/openspec-apply-change <name>` | Implement tasks |
| `/openspec-archive-change <name>` | Archive when done |

**Additional commands** (only if installed - availability depends on your profile):

| Command | What it does |
|---------------------------|-------------------------------------|
| `/openspec-new-change <name>` | Start a new change, step by step |
| `/openspec-continue-change <name>` | Continue an existing change |
| `/openspec-ff-change <name>` | Fast-forward: all artifacts at once |
| `/openspec-verify-change <name>` | Verify implementation |
| `/openspec-propose <name>` | Create a change and generate all artifacts |
| `/openspec-explore` | Think through problems (no code changes) |
| `/openspec-apply-change <name>` | Implement tasks |
| `/openspec-archive-change <name>` | Archive when done |
| `/openspec-new-change <name>` | Start a new change, step by step |
| `/openspec-continue-change <name>` | Continue an existing change |
| `/openspec-ff-change <name>` | Fast-forward: all artifacts at once |
| `/openspec-verify-change <name>` | Verify implementation |

Try `/openspec-propose` to start your first change.
```
Expand Down
4 changes: 2 additions & 2 deletions skills/openspec-update-change/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 optional workflow and may not be installed. Before suggesting it anywhere below, verify that it is available. If it is unavailable, `openspec status --change "<name>" --json` shows the next artifact and `openspec instructions "<artifact-id>" --change "<name>" --json` explains how to create it.
This workflow revises artifacts that already exist; `/openspec-continue-change` is what creates the ones that do not.

**Steps**

Expand Down Expand Up @@ -88,4 +88,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 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 "<new-change-name>"` instead.
- If the request changes the change's *intent* rather than refining it, recommend starting fresh with `/openspec-new-change` (the "Update vs. Start Fresh" heuristic).
6 changes: 6 additions & 0 deletions src/core/command-generation/generator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
import type { CommandContent, ToolCommandAdapter, GeneratedCommand } from './types.js';
import { getInvocationForAdapter, needsInvocationRewrite } from './invocation.js';
import { transformCommandInvocations } from '../../utils/command-references.js';
import { assertWorkflowConditionalsResolved } from '../templates/optional-workflow.js';

/**
* Generate a single command file using the provided adapter.
Expand All @@ -26,6 +27,11 @@ export function generateCommand(
content: CommandContent,
adapter: ToolCommandAdapter
): GeneratedCommand {
assertWorkflowConditionalsResolved(
content.body,
`Command '${content.id}' was generated without resolving its optional-workflow blocks`
);

const invocation = getInvocationForAdapter(adapter);
const formatted = needsInvocationRewrite(invocation)
? { ...content, body: transformCommandInvocations(content.body, invocation) }
Expand Down
52 changes: 46 additions & 6 deletions src/core/shared/skill-generation.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,8 +32,31 @@ import {
type SkillTemplate,
} from '../templates/skill-templates.js';
import type { CommandContent } from '../command-generation/index.js';
import {
assertWorkflowConditionalsResolved,
resolveOptionalWorkflows,
} from '../templates/optional-workflow.js';
import { ALL_WORKFLOWS } from '../profiles.js';
import { OPENSPEC_CLI_ALLOWED_TOOLS } from './allowed-tools.js';

/**
* The workflow set a template body is rendered against.
*
* `workflowFilter` is both the list of workflows to install and the set a
* template may refer to, so resolving optional-workflow conditionals here —
* the one place every generation path (init, update, migration, the skills.sh
* distribution) already funnels through — keeps a reference to an uninstalled
* workflow out of every generated file (#1734, umbrella #919).
*
* With no filter, every workflow is installed (that is what an unfiltered call
* means), so the installed branch is kept.
*/
function resolveInstalledWorkflows(
workflowFilter?: readonly string[]
): ReadonlySet<string> {
return new Set<string>(workflowFilter ?? ALL_WORKFLOWS);
}

/**
* Skill template with directory name and workflow ID mapping.
*/
Expand Down Expand Up @@ -72,10 +95,16 @@ export function getSkillTemplates(workflowFilter?: readonly string[]): SkillTemp
{ template: getOpsxProposeSkillTemplate(), dirName: 'openspec-propose', workflowId: 'propose' },
];

if (!workflowFilter) return all;
const installed = resolveInstalledWorkflows(workflowFilter);
const selected = workflowFilter ? all.filter(entry => installed.has(entry.workflowId)) : all;

const filterSet = new Set(workflowFilter);
return all.filter(entry => filterSet.has(entry.workflowId));
return selected.map(entry => ({
...entry,
template: {
...entry.template,
instructions: resolveOptionalWorkflows(entry.template.instructions, installed),
},
}));
}

/**
Expand All @@ -99,10 +128,16 @@ export function getCommandTemplates(workflowFilter?: readonly string[]): Command
{ template: getOpsxProposeCommandTemplate(), id: 'propose' },
];

if (!workflowFilter) return all;
const installed = resolveInstalledWorkflows(workflowFilter);
const selected = workflowFilter ? all.filter(entry => installed.has(entry.id)) : all;

const filterSet = new Set(workflowFilter);
return all.filter(entry => filterSet.has(entry.id));
return selected.map(entry => ({
...entry,
template: {
...entry.template,
content: resolveOptionalWorkflows(entry.template.content, installed),
},
}));
}

/**
Expand Down Expand Up @@ -138,6 +173,11 @@ export function generateSkillContent(
? transformInstructions(template.instructions)
: template.instructions;

assertWorkflowConditionalsResolved(
instructions,
`Skill '${template.name}' was generated without resolving its optional-workflow blocks`
);

return `---
name: ${template.name}
description: ${template.description}
Expand Down
Loading