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
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"|"skipped"|"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`. The `artifacts` array is in dependency order, with the schema's `artifacts:` declaration order breaking ties between artifacts that become ready at the same time (never alphabetical), so the first `ready` entry is the artifact to write next; `missingDeps` uses that same order. `"skipped"` marks an artifact whose `generates` path is under `specs/` in a change whose `.openspec.yaml` declares `skip_specs: true`; it satisfies dependencies but must not be created. 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" }, "isPlanningComplete", "isComplete", "applyRequires", "artifacts": [ {id, outputPath, status: "done"|"skipped"|"ready"|"blocked", requires, missingDeps?} ], "root" }`. `isPlanningComplete` means every non-skipped planning artifact exists; skipped artifacts count as satisfied without being created. It does not mean implementation tasks are complete. `isComplete` is retained as a compatibility alias with the same value. 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`. The `artifacts` array is in dependency order, with the schema's `artifacts:` declaration order breaking ties between artifacts that become ready at the same time (never alphabetical), so the first `ready` entry is the artifact to write next; `missingDeps` uses that same order. `"skipped"` marks an artifact whose `generates` path is under `specs/` in a change whose `.openspec.yaml` declares `skip_specs: true`; it satisfies dependencies but must not be created. 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[], "skipped"?, "warning"?, "template", "dependencies": [{id,done,path,description,skipped?}], "unlocks", "root" }`. `unlocks` lists the artifacts this one makes ready, in the schema's declaration order (the same order `status` recommends them). `"skipped": true` (with `"warning"`) appears when the change declares `skip_specs: true` and this artifact is skipped — do not create its files. A dependency entry with `skipped: true` is satisfied without files — do not try to read its paths.
Expand Down
6 changes: 6 additions & 0 deletions docs/cli.md
Original file line number Diff line number Diff line change
Expand Up @@ -750,6 +750,7 @@ A change that declares `skip_specs: true` shows its specs stage as `[~] specs (s
{
"changeName": "add-dark-mode",
"schemaName": "spec-driven",
"isPlanningComplete": false,
"isComplete": false,
"applyRequires": ["tasks"],
"artifacts": [
Expand All @@ -761,6 +762,11 @@ A change that declares `skip_specs: true` shows its specs stage as `[~] specs (s
}
```

`isPlanningComplete` reports whether every non-skipped planning artifact exists;
skipped artifacts count as satisfied without being created. It does not report
whether implementation tasks are complete. `isComplete` is retained as a
compatibility alias with the same value.

Artifacts are listed in dependency order - a dependency never appears after
something that requires it - and artifacts that become ready at the same time
(spec-driven's `specs` and `design` both need only `proposal`) keep the order the
Expand Down
7 changes: 5 additions & 2 deletions openspec/specs/cli-artifact-workflow/spec.md
Original file line number Diff line number Diff line change
Expand Up @@ -25,13 +25,16 @@ The system SHALL display artifact completion status for a change, including scaf
#### Scenario: Status JSON output

- **WHEN** user runs `openspec status --change <id> --json`
- **THEN** the system outputs JSON with changeName, schemaName, isComplete, and artifacts array
- **THEN** the system outputs JSON with changeName, schemaName, isPlanningComplete, isComplete, and artifacts array
- **AND** `isPlanningComplete` is true only when every non-skipped planning artifact exists
- **AND** a skipped artifact counts as satisfied without being created
- **AND** `isComplete` remains a compatibility alias with the same value

#### Scenario: Status JSON includes apply requirements

- **WHEN** user runs `openspec status --change <id> --json`
- **THEN** the system outputs JSON with:
- `changeName`, `schemaName`, `isComplete`, `artifacts` array
- `changeName`, `schemaName`, `isPlanningComplete`, `isComplete`, `artifacts` array
- `applyRequires`: array of artifact IDs needed for apply phase

#### Scenario: Status JSON exposes each artifact's dependency edges
Expand Down
6 changes: 3 additions & 3 deletions skills/openspec-continue-change/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -41,17 +41,17 @@ Continue working on a change by creating the next artifact.
Parse the JSON to understand current state. The response includes:
- `schemaName`: The workflow schema being used (e.g., "spec-driven")
- `artifacts`: Array of artifacts with their status ("done", "skipped", "ready", "blocked")
- `isComplete`: Boolean indicating if all artifacts are complete
- `isPlanningComplete`: Boolean indicating if all planning artifacts are complete. Older CLI versions expose the same value as `isComplete`.
- `planningHome`, `changeRoot`, `artifactPaths`, and `actionContext`: path and scope context. Use these instead of assuming repo-local paths.

3. **Act based on status**:

---

**If all artifacts are complete (`isComplete: true`)**:
**If all planning artifacts are complete (`isPlanningComplete: true`, or legacy `isComplete: true`)**:
- Congratulate the user
- Show final status including the schema used
- Suggest: "All artifacts created! You can now implement this change or archive it."
- Suggest: "Planning is complete! You can now implement this change. Once implementation and any tracked work are complete, archive it."
- STOP

---
Expand Down
2 changes: 1 addition & 1 deletion skills/openspec-update-change/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@ Revise a change's existing planning artifacts and keep them coherent. Never edit
Parse the JSON to understand current state. The response includes:
- `schemaName`: The workflow schema being used (e.g., "spec-driven")
- `artifacts`: Array of artifacts with their status ("done", "skipped", "ready", "blocked")
- `isComplete`: Boolean indicating if all artifacts are complete
- `isPlanningComplete`: Boolean indicating if all planning artifacts are complete. Older CLI versions expose the same value as `isComplete`.
- `planningHome`, `changeRoot`, `artifactPaths`, and `actionContext`: path and scope context. Use these instead of assuming repo-local paths.

The artifact ids and paths come from the active schema - do NOT assume them, and do NOT branch on hardcoded artifact names. Custom schemas must work unchanged.
Expand Down
4 changes: 2 additions & 2 deletions src/commands/workflow/status.ts
Original file line number Diff line number Diff line change
Expand Up @@ -151,8 +151,8 @@ export function printStatusText(status: ChangeStatus): void {
console.log(line);
}

if (status.isComplete) {
if (status.isPlanningComplete) {
console.log();
console.log(chalk.green('All artifacts complete!'));
console.log(chalk.green('All planning artifacts complete!'));
}
}
5 changes: 4 additions & 1 deletion src/core/artifact-graph/instruction-loader.ts
Original file line number Diff line number Diff line change
Expand Up @@ -175,7 +175,9 @@ export interface ChangeStatus {
nextSteps: string[];
/** Machine-readable action constraints for agents */
actionContext: ActionContext;
/** Whether all artifacts are complete */
/** Whether all planning artifacts are complete */
isPlanningComplete: boolean;
/** Compatibility alias for isPlanningComplete */
isComplete: boolean;
/** Artifact IDs required before apply phase (from schema's apply.requires) */
applyRequires: string[];
Expand Down Expand Up @@ -521,6 +523,7 @@ export function formatChangeStatus(
planningHome: summarizePlanningHome(context.planningHome),
changeRoot: context.changeDir,
artifactPaths,
isPlanningComplete: isComplete,
isComplete,
applyRequires,
nextSteps: buildNextSteps({
Expand Down
6 changes: 4 additions & 2 deletions src/core/change-status-policy.ts
Original file line number Diff line number Diff line change
Expand Up @@ -65,14 +65,16 @@ export function buildActionContext(input: ActionContextInput): ActionContext {
export function buildNextSteps(input: ChangeNextStepsInput): string[] {
const readyArtifact = input.artifactStatuses.find((artifact) => artifact.status === 'ready');
const steps: string[] = [];
const storeFlag = input.storeId ? ` --store ${input.storeId}` : '';

if (readyArtifact) {
const storeFlag = input.storeId ? ` --store ${input.storeId}` : '';
steps.push(
`Run openspec instructions ${readyArtifact.id} --change "${input.changeName}"${storeFlag} --json before writing that artifact.`
);
} else if (input.allArtifactsComplete) {
steps.push('All planning artifacts are complete; review tasks before implementation.');
steps.push(
`All planning artifacts are complete. Run openspec instructions apply --change "${input.changeName}"${storeFlag} --json to inspect implementation progress.`
);
}

return steps;
Expand Down
12 changes: 6 additions & 6 deletions src/core/templates/workflows/continue-change.ts
Original file line number Diff line number Diff line change
Expand Up @@ -43,17 +43,17 @@ ${STORE_SELECTION_GUIDANCE}
Parse the JSON to understand current state. The response includes:
- \`schemaName\`: The workflow schema being used (e.g., "spec-driven")
- \`artifacts\`: Array of artifacts with their status ("done", "skipped", "ready", "blocked")
- \`isComplete\`: Boolean indicating if all artifacts are complete
- \`isPlanningComplete\`: Boolean indicating if all planning artifacts are complete. Older CLI versions expose the same value as \`isComplete\`.
- \`planningHome\`, \`changeRoot\`, \`artifactPaths\`, and \`actionContext\`: path and scope context. Use these instead of assuming repo-local paths.

3. **Act based on status**:

---

**If all artifacts are complete (\`isComplete: true\`)**:
**If all planning artifacts are complete (\`isPlanningComplete: true\`, or legacy \`isComplete: true\`)**:
- Congratulate the user
- Show final status including the schema used
- Suggest: "All artifacts created! You can now implement this change or archive it."
- Suggest: "Planning is complete! You can now implement this change. Once implementation and any tracked work are complete, archive it."
- STOP

---
Expand Down Expand Up @@ -161,17 +161,17 @@ ${STORE_SELECTION_GUIDANCE}
Parse the JSON to understand current state. The response includes:
- \`schemaName\`: The workflow schema being used (e.g., "spec-driven")
- \`artifacts\`: Array of artifacts with their status ("done", "skipped", "ready", "blocked")
- \`isComplete\`: Boolean indicating if all artifacts are complete
- \`isPlanningComplete\`: Boolean indicating if all planning artifacts are complete. Older CLI versions expose the same value as \`isComplete\`.
- \`planningHome\`, \`changeRoot\`, \`artifactPaths\`, and \`actionContext\`: path and scope context. Use these instead of assuming repo-local paths.

3. **Act based on status**:

---

**If all artifacts are complete (\`isComplete: true\`)**:
**If all planning artifacts are complete (\`isPlanningComplete: true\`, or legacy \`isComplete: true\`)**:
- Congratulate the user
- Show final status including the schema used
- Suggest: "All artifacts created! You can now implement this change with \`/opsx:apply\` or archive it with \`/opsx:archive\`."
- Suggest: "Planning is complete! You can now implement this change with \`/opsx:apply\`. Once implementation and any tracked work are complete, archive it with \`/opsx:archive\`."
- STOP

---
Expand Down
4 changes: 2 additions & 2 deletions src/core/templates/workflows/update-change.ts
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@ ${STORE_SELECTION_GUIDANCE}
Parse the JSON to understand current state. The response includes:
- \`schemaName\`: The workflow schema being used (e.g., "spec-driven")
- \`artifacts\`: Array of artifacts with their status ("done", "skipped", "ready", "blocked")
- \`isComplete\`: Boolean indicating if all artifacts are complete
- \`isPlanningComplete\`: Boolean indicating if all planning artifacts are complete. Older CLI versions expose the same value as \`isComplete\`.
- \`planningHome\`, \`changeRoot\`, \`artifactPaths\`, and \`actionContext\`: path and scope context. Use these instead of assuming repo-local paths.

The artifact ids and paths come from the active schema - do NOT assume them, and do NOT branch on hardcoded artifact names. Custom schemas must work unchanged.
Expand Down Expand Up @@ -136,7 +136,7 @@ ${STORE_SELECTION_GUIDANCE}
Parse the JSON to understand current state. The response includes:
- \`schemaName\`: The workflow schema being used (e.g., "spec-driven")
- \`artifacts\`: Array of artifacts with their status ("done", "skipped", "ready", "blocked")
- \`isComplete\`: Boolean indicating if all artifacts are complete
- \`isPlanningComplete\`: Boolean indicating if all planning artifacts are complete. Older CLI versions expose the same value as \`isComplete\`.
- \`planningHome\`, \`changeRoot\`, \`artifactPaths\`, and \`actionContext\`: path and scope context. Use these instead of assuming repo-local paths.

The artifact ids and paths come from the active schema - do NOT assume them, and do NOT branch on hardcoded artifact names. Custom schemas must work unchanged.
Expand Down
9 changes: 8 additions & 1 deletion test/cli-e2e/store-lifecycle.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -423,7 +423,14 @@ describe('standalone store lifecycle journey', () => {
{ env: machineB, cwd: base }
);
expect(status.exitCode).toBe(0);
expect(status.stdout).toContain('All artifacts complete!');
expect(status.stdout).toContain('All planning artifacts complete!');

const statusJson = await runCLI(
['status', '--change', changeId, '--store', STORE_ID, '--json'],
{ env: machineB, cwd: base }
);
expect(statusJson.exitCode).toBe(0);
expect(JSON.parse(statusJson.stdout).nextSteps[0]).toContain(`--store ${STORE_ID}`);

const validated = await runCLI(
['validate', changeId, '--store', STORE_ID],
Expand Down
57 changes: 55 additions & 2 deletions test/commands/artifact-workflow.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -118,6 +118,7 @@ describe('artifact-workflow CLI commands', () => {
const json = JSON.parse(result.stdout);
expect(json.changeName).toBe('json-change');
expect(json.schemaName).toBe('spec-driven');
expect(json.isPlanningComplete).toBe(false);
expect(json.isComplete).toBe(false);
expect(Array.isArray(json.artifacts)).toBe(true);
expect(json.artifacts).toHaveLength(4);
Expand All @@ -139,13 +140,65 @@ describe('artifact-workflow CLI commands', () => {
expect(json.nextSteps[0]).toContain('openspec instructions specs');
});

it('shows complete status when all artifacts are done', async () => {
it('shows planning completion when all artifacts exist', async () => {
await createTestChange('complete-change', ['proposal', 'design', 'specs', 'tasks']);

const result = await runCLI(['status', '--change', 'complete-change'], { cwd: tempDir });
expect(result.exitCode).toBe(0);
expect(result.stdout).toContain('4/4 artifacts complete');
expect(result.stdout).toContain('All artifacts complete!');
expect(result.stdout).toContain('All planning artifacts complete!');
expect(result.stdout).not.toContain('All artifacts complete!');
});

it('distinguishes planning completion from implementation task completion', async () => {
await createTestChange('planned-change', ['proposal', 'design', 'specs', 'tasks']);

const statusResult = await runCLI(['status', '--change', 'planned-change', '--json'], {
cwd: tempDir,
});
const applyResult = await runCLI(
['instructions', 'apply', '--change', 'planned-change', '--json'],
{ cwd: tempDir }
);

expect(statusResult.exitCode).toBe(0);
expect(applyResult.exitCode).toBe(0);

const status = JSON.parse(statusResult.stdout);
const apply = JSON.parse(applyResult.stdout);
expect(status.isPlanningComplete).toBe(true);
expect(status.isComplete).toBe(true);
expect(status.nextSteps[0]).toContain(
'openspec instructions apply --change "planned-change" --json'
);
expect(status.nextSteps[0]).not.toContain('before implementation');
expect(apply.state).toBe('ready');
expect(apply.progress.remaining).toBe(1);
});

it('reports skipped planning artifacts as complete without creating them', async () => {
const changeDir = await createTestChange('skip-specs-change', [
'proposal',
'design',
'tasks',
]);
await fs.writeFile(
path.join(changeDir, '.openspec.yaml'),
'schema: spec-driven\nskip_specs: true\n'
);

const result = await runCLI(['status', '--change', 'skip-specs-change', '--json'], {
cwd: tempDir,
});

expect(result.exitCode).toBe(0);
const status = JSON.parse(result.stdout);
expect(status.isPlanningComplete).toBe(true);
expect(status.isComplete).toBe(status.isPlanningComplete);
expect(status.artifacts.find((artifact: any) => artifact.id === 'specs')?.status).toBe(
'skipped'
);
await expect(fs.stat(path.join(changeDir, 'specs'))).rejects.toMatchObject({ code: 'ENOENT' });
});

it('exits gracefully when no changes exist', async () => {
Expand Down
Loading
Loading