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

### Fixed

- **`/opsx:propose` and `/opsx:ff` no longer finish a change with no spec written.** The workflows listed only `proposal`/`design`/`tasks` and treated the apply phase's `tasks` artifact as the stop condition — but `status` marks an artifact `done` as soon as a matching file exists, so writing `tasks.md` early satisfied the loop while `specs/<capability>/spec.md` was never created (a spec-less change in a spec-driven tool). The loop now derives the full required set — every apply dependency plus everything it transitively `requires` — from a single `status` call, creates each missing artifact, and only skips one when its own `instruction` field marks it conditional. (#1260, #788)

### Changed

- **`openspec status --json` now reports each artifact's `requires` edges.** Every entry in the `artifacts` array carries a `requires` array of the ids it directly depends on, present for every status (including `done`) so agents can compute the transitive required set from `status` alone. Additive and backward-compatible — existing fields are unchanged.
2 changes: 1 addition & 1 deletion docs/agent-contract.md
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,7 @@ Change: `{ "id", "title", "deltaCount", "deltas": [...], "root" }`. Spec: `{ "id
`{ "items": [ { "id", "type": "change"|"spec", "valid", "issues": [ { "level", "path", "message", "line"?, "column"? } ], "durationMs" } ], "summary": { "totals": {items,passed,failed}, "byType": {...} }, "version": "1.0", "root" }`. Exit 1 when any item fails.

### 4.4 `status --json`
`{ "changeName", "schemaName", "planningHome"?: { "kind", "root", "changesDir", "defaultSchema" }, "changeRoot", "artifactPaths": { "<id>": {outputPath, resolvedOutputPath, existingOutputPaths} }, "nextSteps": ["..."], "actionContext": { "mode": "repo-local", "sourceOfTruth": "repo", "planningArtifacts", "linkedContext", "allowedEditRoots", "requiresAffectedAreaSelection", "constraints" }, "isComplete", "applyRequires", "artifacts": [ {id, outputPath, status: "done"|"ready"|"blocked", missingDeps?} ], "root" }`. No active changes: `{ "changes": [], "message", "root" }`, exit 0.
`{ "changeName", "schemaName", "planningHome"?: { "kind", "root", "changesDir", "defaultSchema" }, "changeRoot", "artifactPaths": { "<id>": {outputPath, resolvedOutputPath, existingOutputPaths} }, "nextSteps": ["..."], "actionContext": { "mode": "repo-local", "sourceOfTruth": "repo", "planningArtifacts", "linkedContext", "allowedEditRoots", "requiresAffectedAreaSelection", "constraints" }, "isComplete", "applyRequires", "artifacts": [ {id, outputPath, status: "done"|"ready"|"blocked", requires, missingDeps?} ], "root" }`. Each artifact's `requires` is its direct dependency ids (present for every status, so the transitive required set is computable even when the artifact is `done`); `missingDeps` appears only when `blocked`. No active changes: `{ "changes": [], "message", "root" }`, exit 0.

### 4.5 `instructions <artifact> --json`
`{ "changeName", "artifactId", "schemaName", "changeDir", "planningHome"?, "outputPath", "resolvedOutputPath", "existingOutputPaths", "description", "instruction"?, "context"?, "rules"?, "references"?: ReferenceIndexEntry[], "template", "dependencies": [{id,done,path,description}], "unlocks", "root" }`.
Expand Down
8 changes: 4 additions & 4 deletions docs/cli.md
Original file line number Diff line number Diff line change
Expand Up @@ -702,10 +702,10 @@ Progress: 2/4 artifacts complete
"isComplete": false,
"applyRequires": ["tasks"],
"artifacts": [
{"id": "proposal", "outputPath": "proposal.md", "status": "done"},
{"id": "design", "outputPath": "design.md", "status": "ready"},
{"id": "specs", "outputPath": "specs/**/*.md", "status": "done"},
{"id": "tasks", "outputPath": "tasks.md", "status": "blocked", "missingDeps": ["design"]}
{"id": "proposal", "outputPath": "proposal.md", "status": "done", "requires": []},
{"id": "design", "outputPath": "design.md", "status": "ready", "requires": ["proposal"]},
{"id": "specs", "outputPath": "specs/**/*.md", "status": "done", "requires": ["proposal"]},
{"id": "tasks", "outputPath": "tasks.md", "status": "blocked", "requires": ["specs", "design"], "missingDeps": ["design"]}
]
}
```
Expand Down
6 changes: 6 additions & 0 deletions openspec/specs/cli-artifact-workflow/spec.md
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,12 @@ The system SHALL display artifact completion status for a change, including scaf
- `changeName`, `schemaName`, `isComplete`, `artifacts` array
- `applyRequires`: array of artifact IDs needed for apply phase

#### Scenario: Status JSON exposes each artifact's dependency edges

- **WHEN** user runs `openspec status --change <id> --json`
- **THEN** every entry in the `artifacts` array includes `requires`: the array of artifact IDs it directly depends on
- **AND** `requires` is present regardless of the artifact's status, so a `done` artifact still reports its dependencies (letting agents compute the transitive required set from status alone)

#### Scenario: Status on scaffolded change

- **WHEN** user runs `openspec status --change <id>` on a change with no artifacts
Expand Down
22 changes: 13 additions & 9 deletions skills/openspec-ff-change/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -38,10 +38,10 @@ Fast-forward through artifact creation - generate everything needed to start imp
```
Parse the JSON to get:
- `applyRequires`: array of artifact IDs needed before implementation (e.g., `["tasks"]`)
- `artifacts`: list of all artifacts with their status and dependencies
- `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 artifacts in sequence until apply-ready**
4. **Create every artifact in the required set**

Use a todo list to track progress through the artifacts.

Expand All @@ -61,14 +61,18 @@ Fast-forward through artifact creation - generate everything needed to start imp
- `dependencies`: Completed artifacts to read for context
- Read any completed dependency files for context - always re-read them from disk, even if you saw them earlier in the conversation (the user may have edited them)
- If the `instruction` field delegates creation to a specific skill or command, invoke it to produce the artifact instead of writing the file yourself, then verify the artifact file exists at `resolvedOutputPath`
- Otherwise create the artifact file using `template` as the structure and write it to `resolvedOutputPath`
- Otherwise create the artifact file using `template` as the structure and write it to `resolvedOutputPath`. If `resolvedOutputPath` is a glob, follow `instruction` to choose the concrete file path
- Apply `context` and `rules` as constraints - but do NOT copy them into the file
- Show brief progress: "✓ Created <artifact-id>"

b. **Continue until all `applyRequires` artifacts are complete**
b. **Continue until every artifact in the required set exists (not just `apply.requires`)**
- After creating each artifact, re-run `openspec status --change "<name>" --json`
- Check if every artifact ID in `applyRequires` has `status: "done"` in the artifacts array
- Stop when all `applyRequires` artifacts are done
- The required set is `applyRequires` plus every artifact reachable from those by following the `requires` edges in `status --json` - walk them transitively (spec-driven closes over proposal, specs, design, tasks). Leave artifacts outside that set alone
- `status` is file-existence only, so an `applyRequires` artifact reading `done` does NOT mean its dependencies exist - writing `tasks.md` early marks `tasks` done while `specs` was never written. Use each artifact's `requires` edges, not its `status`, to build the required set: a `done` artifact still lists what it depends on
- Create every artifact in the required set that is missing, then re-check - creating one can unblock others
- Skip one only when its own `instruction` says it is conditional: run `openspec instructions <artifact-id> --change "<name>" --json` and skip only if its `instruction` field marks it optional (e.g. "create only if..."). Spec-driven's `design.md` qualifies; `specs` never does. Tell the user, and do not reconsider it
- Dependencies are enablers, not gates: if a required artifact is still `blocked` only because you skipped a conditional dependency, write it anyway
- Stop when every artifact in the required set is `done` or was deliberately skipped

c. **If an artifact requires user input** (unclear context):
- Use **AskUserQuestion tool** to clarify
Expand All @@ -83,8 +87,8 @@ Fast-forward through artifact creation - generate everything needed to start imp

After completing all artifacts, summarize:
- Change name and location
- List of artifacts created with brief descriptions
- What's ready: "All artifacts created! Ready for implementation."
- List of artifacts created with brief descriptions, plus any conditional artifact you skipped and why
- What's ready: "All artifacts needed for implementation are ready."
- Prompt: "Run `/openspec-apply-change` or ask me to implement to start working on the tasks."

**Artifact Creation Guidelines**
Expand All @@ -99,7 +103,7 @@ After completing all artifacts, summarize:
- These guide what you write, but should never appear in the output

**Guardrails**
- Create ALL artifacts needed for implementation (as defined by schema's `apply.requires`)
- Create every artifact the apply phase transitively depends on, not just the ids listed in `apply.requires`
- Always read dependency artifacts before creating a new one - re-read from disk, not from conversation memory (files may have changed since you last saw them)
- If context is critically unclear, ask the user - but prefer making reasonable decisions to keep momentum
- If a change with that name already exists, suggest continuing that change instead
Expand Down
25 changes: 15 additions & 10 deletions skills/openspec-propose/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,9 @@ metadata:

Propose a new change - create the change and generate all artifacts in one step.

I'll create a change with artifacts:
I'll create a change with the artifacts your schema defines. With the default spec-driven schema that is:
- proposal.md (what & why)
- `specs/<capability>/spec.md` (what the system must do - a delta, not the main spec)
- design.md (how)
- tasks.md (implementation steps)

Expand Down Expand Up @@ -47,10 +48,10 @@ When ready to implement, run /openspec-apply-change
```
Parse the JSON to get:
- `applyRequires`: array of artifact IDs needed before implementation (e.g., `["tasks"]`)
- `artifacts`: list of all artifacts with their status and dependencies
- `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 artifacts in sequence until apply-ready**
4. **Create every artifact in the required set**

Use a todo list to track progress through the artifacts.

Expand All @@ -70,14 +71,18 @@ When ready to implement, run /openspec-apply-change
- `dependencies`: Completed artifacts to read for context
- Read any completed dependency files for context - always re-read them from disk, even if you saw them earlier in the conversation (the user may have edited them)
- If the `instruction` field delegates creation to a specific skill or command, invoke it to produce the artifact instead of writing the file yourself, then verify the artifact file exists at `resolvedOutputPath`
- Otherwise create the artifact file using `template` as the structure and write it to `resolvedOutputPath`
- Otherwise create the artifact file using `template` as the structure and write it to `resolvedOutputPath`. If `resolvedOutputPath` is a glob, follow `instruction` to choose the concrete file path
- Apply `context` and `rules` as constraints - but do NOT copy them into the file
- Show brief progress: "Created <artifact-id>"

b. **Continue until all `applyRequires` artifacts are complete**
b. **Continue until every artifact in the required set exists (not just `apply.requires`)**
- After creating each artifact, re-run `openspec status --change "<name>" --json`
- Check if every artifact ID in `applyRequires` has `status: "done"` in the artifacts array
- Stop when all `applyRequires` artifacts are done
- The required set is `applyRequires` plus every artifact reachable from those by following the `requires` edges in `status --json` - walk them transitively (spec-driven closes over proposal, specs, design, tasks). Leave artifacts outside that set alone
- `status` is file-existence only, so an `applyRequires` artifact reading `done` does NOT mean its dependencies exist - writing `tasks.md` early marks `tasks` done while `specs` was never written. Use each artifact's `requires` edges, not its `status`, to build the required set: a `done` artifact still lists what it depends on
- Create every artifact in the required set that is missing, then re-check - creating one can unblock others
- Skip one only when its own `instruction` says it is conditional: run `openspec instructions <artifact-id> --change "<name>" --json` and skip only if its `instruction` field marks it optional (e.g. "create only if..."). Spec-driven's `design.md` qualifies; `specs` never does. Tell the user, and do not reconsider it
- Dependencies are enablers, not gates: if a required artifact is still `blocked` only because you skipped a conditional dependency, write it anyway
- Stop when every artifact in the required set is `done` or was deliberately skipped

c. **If an artifact requires user input** (unclear context):
- Use **AskUserQuestion tool** to clarify
Expand All @@ -92,8 +97,8 @@ When ready to implement, run /openspec-apply-change

After completing all artifacts, summarize:
- Change name and location
- List of artifacts created with brief descriptions
- What's ready: "All artifacts created! Ready for implementation."
- List of artifacts created with brief descriptions, plus any conditional artifact you skipped and why
- What's ready: "All artifacts needed for implementation are ready."
- Prompt: "Run `/openspec-apply-change` or ask me to implement to start working on the tasks."

**Artifact Creation Guidelines**
Expand All @@ -108,7 +113,7 @@ After completing all artifacts, summarize:
- These guide what you write, but should never appear in the output

**Guardrails**
- Create ALL artifacts needed for implementation (as defined by schema's `apply.requires`)
- Create every artifact the apply phase transitively depends on, not just the ids listed in `apply.requires`
- Always read dependency artifacts before creating a new one - re-read from disk, not from conversation memory (files may have changed since you last saw them)
- If context is critically unclear, ask the user - but prefer making reasonable decisions to keep momentum
- If a change with that name already exists, ask if user wants to continue it or create a new one
Expand Down
8 changes: 8 additions & 0 deletions src/core/artifact-graph/instruction-loader.ts
Original file line number Diff line number Diff line change
Expand Up @@ -124,6 +124,11 @@ export interface ArtifactStatus {
outputPath: string;
/** Status: done, ready, or blocked */
status: 'done' | 'ready' | 'blocked';
/** Artifact IDs this artifact directly requires (its `requires` edges).
* Present for every status so callers can compute the transitive required
* set even when the artifact is already `done` (file-existence status does
* not imply its dependencies exist). */
requires: string[];
/** Missing dependencies (only for blocked) */
missingDeps?: string[];
}
Expand Down Expand Up @@ -406,6 +411,7 @@ export function formatChangeStatus(
id: artifact.id,
outputPath: artifact.generates,
status: 'done' as const,
requires: artifact.requires,
};
}

Expand All @@ -414,13 +420,15 @@ export function formatChangeStatus(
id: artifact.id,
outputPath: artifact.generates,
status: 'ready' as const,
requires: artifact.requires,
};
}

return {
id: artifact.id,
outputPath: artifact.generates,
status: 'blocked' as const,
requires: artifact.requires,
missingDeps: blocked[artifact.id] ?? [],
};
});
Expand Down
Loading
Loading