From 89b71ed0084644165ac0781e0093494436e31c72 Mon Sep 17 00:00:00 2001 From: Clay Good Date: Mon, 3 Aug 2026 19:48:21 -0500 Subject: [PATCH 1/6] fix(status): clarify planning completion --- docs/agent-contract.md | 2 +- docs/cli.md | 5 ++++ openspec/specs/cli-artifact-workflow/spec.md | 6 +++-- skills/openspec-continue-change/SKILL.md | 4 ++-- skills/openspec-update-change/SKILL.md | 2 +- src/core/artifact-graph/instruction-loader.ts | 5 +++- .../templates/workflows/continue-change.ts | 8 +++---- src/core/templates/workflows/update-change.ts | 4 ++-- test/commands/artifact-workflow.test.ts | 23 +++++++++++++++++++ .../artifact-graph/instruction-loader.test.ts | 5 +++- 10 files changed, 50 insertions(+), 14 deletions(-) diff --git a/docs/agent-contract.md b/docs/agent-contract.md index 17cec31135..f889a83c74 100644 --- a/docs/agent-contract.md +++ b/docs/agent-contract.md @@ -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": { "": {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": { "": {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 planning artifact exists; 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 --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. diff --git a/docs/cli.md b/docs/cli.md index 271ca5c8c7..ef282063e3 100644 --- a/docs/cli.md +++ b/docs/cli.md @@ -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": [ @@ -761,6 +762,10 @@ A change that declares `skip_specs: true` shows its specs stage as `[~] specs (s } ``` +`isPlanningComplete` reports whether all planning artifacts exist; 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 diff --git a/openspec/specs/cli-artifact-workflow/spec.md b/openspec/specs/cli-artifact-workflow/spec.md index ee9fe6138e..e2b75cf102 100644 --- a/openspec/specs/cli-artifact-workflow/spec.md +++ b/openspec/specs/cli-artifact-workflow/spec.md @@ -25,13 +25,15 @@ The system SHALL display artifact completion status for a change, including scaf #### Scenario: Status JSON output - **WHEN** user runs `openspec status --change --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 planning artifact exists +- **AND** `isComplete` remains a compatibility alias with the same value #### Scenario: Status JSON includes apply requirements - **WHEN** user runs `openspec status --change --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 diff --git a/skills/openspec-continue-change/SKILL.md b/skills/openspec-continue-change/SKILL.md index 2f4650bd02..262ec967a7 100644 --- a/skills/openspec-continue-change/SKILL.md +++ b/skills/openspec-continue-change/SKILL.md @@ -41,14 +41,14 @@ 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." diff --git a/skills/openspec-update-change/SKILL.md b/skills/openspec-update-change/SKILL.md index 084baa0062..77d2ed27b3 100644 --- a/skills/openspec-update-change/SKILL.md +++ b/skills/openspec-update-change/SKILL.md @@ -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. diff --git a/src/core/artifact-graph/instruction-loader.ts b/src/core/artifact-graph/instruction-loader.ts index 1c89dd92d7..3f12670016 100644 --- a/src/core/artifact-graph/instruction-loader.ts +++ b/src/core/artifact-graph/instruction-loader.ts @@ -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[]; @@ -521,6 +523,7 @@ export function formatChangeStatus( planningHome: summarizePlanningHome(context.planningHome), changeRoot: context.changeDir, artifactPaths, + isPlanningComplete: isComplete, isComplete, applyRequires, nextSteps: buildNextSteps({ diff --git a/src/core/templates/workflows/continue-change.ts b/src/core/templates/workflows/continue-change.ts index bef2147200..b09335e1a3 100644 --- a/src/core/templates/workflows/continue-change.ts +++ b/src/core/templates/workflows/continue-change.ts @@ -43,14 +43,14 @@ ${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." @@ -161,14 +161,14 @@ ${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\`." diff --git a/src/core/templates/workflows/update-change.ts b/src/core/templates/workflows/update-change.ts index aea08ab676..a716ec47b1 100644 --- a/src/core/templates/workflows/update-change.ts +++ b/src/core/templates/workflows/update-change.ts @@ -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. @@ -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. diff --git a/test/commands/artifact-workflow.test.ts b/test/commands/artifact-workflow.test.ts index 6abfb4b8e1..4170f93683 100644 --- a/test/commands/artifact-workflow.test.ts +++ b/test/commands/artifact-workflow.test.ts @@ -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); @@ -148,6 +149,28 @@ describe('artifact-workflow CLI commands', () => { expect(result.stdout).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(apply.state).toBe('ready'); + expect(apply.progress.remaining).toBe(1); + }); + it('exits gracefully when no changes exist', async () => { const result = await runCLI(['status'], { cwd: tempDir }); expect(result.exitCode).toBe(0); diff --git a/test/core/artifact-graph/instruction-loader.test.ts b/test/core/artifact-graph/instruction-loader.test.ts index 6d2412523f..5ab33fdbb8 100644 --- a/test/core/artifact-graph/instruction-loader.test.ts +++ b/test/core/artifact-graph/instruction-loader.test.ts @@ -640,6 +640,7 @@ rules: expect(status.changeName).toBe('my-change'); expect(status.schemaName).toBe('spec-driven'); + expect(status.isPlanningComplete).toBe(false); expect(status.isComplete).toBe(false); // proposal has no deps, should be ready @@ -679,7 +680,7 @@ rules: expect(specs?.outputPath).toBe('specs/**/*.md'); }); - it('should report isComplete true when all done', () => { + it('should report planning completion without removing the compatibility alias', () => { const changeDir = path.join(tempDir, 'openspec', 'changes', 'my-change'); fs.mkdirSync(changeDir, { recursive: true }); fs.mkdirSync(path.join(changeDir, 'specs'), { recursive: true }); @@ -693,7 +694,9 @@ rules: const context = loadChangeContext(tempDir, 'my-change'); const status = formatChangeStatus(context); + expect(status.isPlanningComplete).toBe(true); expect(status.isComplete).toBe(true); + expect(status.isComplete).toBe(status.isPlanningComplete); expect(status.artifacts.every(a => a.status === 'done')).toBe(true); }); From cd811bc175e50da01e2a5a5db1ba2651592922f6 Mon Sep 17 00:00:00 2001 From: Clay Good Date: Mon, 3 Aug 2026 19:57:58 -0500 Subject: [PATCH 2/6] test(status): cover skipped planning artifacts --- docs/agent-contract.md | 2 +- docs/cli.md | 7 ++++--- openspec/specs/cli-artifact-workflow/spec.md | 3 ++- .../artifact-graph/instruction-loader.test.ts | 20 +++++++++++++++++++ 4 files changed, 27 insertions(+), 5 deletions(-) diff --git a/docs/agent-contract.md b/docs/agent-contract.md index f889a83c74..63e469e48f 100644 --- a/docs/agent-contract.md +++ b/docs/agent-contract.md @@ -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": { "": {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 planning artifact exists; 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. +`{ "changeName", "schemaName", "planningHome"?: { "kind", "root", "changesDir", "defaultSchema" }, "changeRoot", "artifactPaths": { "": {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 --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. diff --git a/docs/cli.md b/docs/cli.md index ef282063e3..c83bfd25e4 100644 --- a/docs/cli.md +++ b/docs/cli.md @@ -762,9 +762,10 @@ A change that declares `skip_specs: true` shows its specs stage as `[~] specs (s } ``` -`isPlanningComplete` reports whether all planning artifacts exist; it does not -report whether implementation tasks are complete. `isComplete` is retained as -a compatibility alias with the same value. +`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 diff --git a/openspec/specs/cli-artifact-workflow/spec.md b/openspec/specs/cli-artifact-workflow/spec.md index e2b75cf102..2b82647027 100644 --- a/openspec/specs/cli-artifact-workflow/spec.md +++ b/openspec/specs/cli-artifact-workflow/spec.md @@ -26,7 +26,8 @@ The system SHALL display artifact completion status for a change, including scaf - **WHEN** user runs `openspec status --change --json` - **THEN** the system outputs JSON with changeName, schemaName, isPlanningComplete, isComplete, and artifacts array -- **AND** `isPlanningComplete` is true only when every planning artifact exists +- **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 diff --git a/test/core/artifact-graph/instruction-loader.test.ts b/test/core/artifact-graph/instruction-loader.test.ts index 5ab33fdbb8..134e52716e 100644 --- a/test/core/artifact-graph/instruction-loader.test.ts +++ b/test/core/artifact-graph/instruction-loader.test.ts @@ -700,6 +700,26 @@ rules: expect(status.artifacts.every(a => a.status === 'done')).toBe(true); }); + it('should count skipped artifacts as planning-complete without creating them', () => { + const changeDir = path.join(tempDir, 'openspec', 'changes', 'my-change'); + fs.mkdirSync(changeDir, { recursive: true }); + fs.writeFileSync( + path.join(changeDir, '.openspec.yaml'), + 'schema: spec-driven\nskip_specs: true\n' + ); + fs.writeFileSync(path.join(changeDir, 'proposal.md'), '# Proposal'); + fs.writeFileSync(path.join(changeDir, 'design.md'), '# Design'); + fs.writeFileSync(path.join(changeDir, 'tasks.md'), '# Tasks'); + + const context = loadChangeContext(tempDir, 'my-change'); + const status = formatChangeStatus(context); + + expect(status.isPlanningComplete).toBe(true); + expect(status.isComplete).toBe(true); + expect(status.artifacts.find(a => a.id === 'specs')?.status).toBe('skipped'); + expect(fs.existsSync(path.join(changeDir, 'specs'))).toBe(false); + }); + it('should show blocked artifacts with missing dependencies', () => { const context = loadChangeContext(tempDir, 'my-change'); const status = formatChangeStatus(context); From 4808fe711c21641f39c84d701095bc7728587f5d Mon Sep 17 00:00:00 2001 From: Clay Good Date: Tue, 4 Aug 2026 05:26:56 -0500 Subject: [PATCH 3/6] fix(workflows): gate archive guidance on implementation --- skills/openspec-continue-change/SKILL.md | 2 +- .../templates/workflows/continue-change.ts | 4 +-- test/commands/artifact-workflow.test.ts | 25 +++++++++++++++++++ .../templates/skill-templates-parity.test.ts | 15 +++++++++++ 4 files changed, 43 insertions(+), 3 deletions(-) diff --git a/skills/openspec-continue-change/SKILL.md b/skills/openspec-continue-change/SKILL.md index 262ec967a7..a5a92bc212 100644 --- a/skills/openspec-continue-change/SKILL.md +++ b/skills/openspec-continue-change/SKILL.md @@ -51,7 +51,7 @@ Continue working on a change by creating the next artifact. **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: "All artifacts created! You can now implement this change. Once implementation and tasks are complete, archive it." - STOP --- diff --git a/src/core/templates/workflows/continue-change.ts b/src/core/templates/workflows/continue-change.ts index b09335e1a3..40f63bcf8c 100644 --- a/src/core/templates/workflows/continue-change.ts +++ b/src/core/templates/workflows/continue-change.ts @@ -53,7 +53,7 @@ ${STORE_SELECTION_GUIDANCE} **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: "All artifacts created! You can now implement this change. Once implementation and tasks are complete, archive it." - STOP --- @@ -171,7 +171,7 @@ ${STORE_SELECTION_GUIDANCE} **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: "All artifacts created! You can now implement this change with \`/opsx:apply\`. Once implementation and tasks are complete, archive it with \`/opsx:archive\`." - STOP --- diff --git a/test/commands/artifact-workflow.test.ts b/test/commands/artifact-workflow.test.ts index 4170f93683..3aa6609fef 100644 --- a/test/commands/artifact-workflow.test.ts +++ b/test/commands/artifact-workflow.test.ts @@ -171,6 +171,31 @@ describe('artifact-workflow CLI commands', () => { 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 () => { const result = await runCLI(['status'], { cwd: tempDir }); expect(result.exitCode).toBe(0); diff --git a/test/core/templates/skill-templates-parity.test.ts b/test/core/templates/skill-templates-parity.test.ts index 30b12eb662..a706da7441 100644 --- a/test/core/templates/skill-templates-parity.test.ts +++ b/test/core/templates/skill-templates-parity.test.ts @@ -256,6 +256,21 @@ describe('skill templates split parity', () => { } }); + it('does not suggest archiving when only planning is complete', () => { + const variants: Array<[string, string]> = [ + [ + 'skill', + generateSkillContent(getContinueChangeSkillTemplate(), 'PARITY-BASELINE'), + ], + ['opsx command', getOpsxContinueCommandTemplate().content], + ]; + + for (const [variant, content] of variants) { + expect(content, variant).toContain('Once implementation and tasks are complete, archive it'); + expect(content, variant).not.toContain('or archive it'); + } + }); + it('gates the archive on a completed spec sync (#1393)', () => { const generatedSkill = generateSkillContent(getArchiveChangeSkillTemplate(), 'PARITY-BASELINE'); const commandContent = getOpsxArchiveCommandTemplate().content; From fff5defd956f8cd006228aac4e096c2a3f9ae897 Mon Sep 17 00:00:00 2001 From: Clay Good Date: Tue, 4 Aug 2026 08:59:17 -0500 Subject: [PATCH 4/6] fix(status): clarify human completion message --- src/commands/workflow/status.ts | 4 ++-- test/cli-e2e/store-lifecycle.test.ts | 2 +- test/commands/artifact-workflow.test.ts | 5 +++-- 3 files changed, 6 insertions(+), 5 deletions(-) diff --git a/src/commands/workflow/status.ts b/src/commands/workflow/status.ts index 2a09b48edb..32f5950716 100644 --- a/src/commands/workflow/status.ts +++ b/src/commands/workflow/status.ts @@ -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!')); } } diff --git a/test/cli-e2e/store-lifecycle.test.ts b/test/cli-e2e/store-lifecycle.test.ts index 4f0acd99c4..3eec644602 100644 --- a/test/cli-e2e/store-lifecycle.test.ts +++ b/test/cli-e2e/store-lifecycle.test.ts @@ -423,7 +423,7 @@ 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 validated = await runCLI( ['validate', changeId, '--store', STORE_ID], diff --git a/test/commands/artifact-workflow.test.ts b/test/commands/artifact-workflow.test.ts index 3aa6609fef..e36a28872f 100644 --- a/test/commands/artifact-workflow.test.ts +++ b/test/commands/artifact-workflow.test.ts @@ -140,13 +140,14 @@ 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 () => { From 723f912c4b249caede0660687ce85de460c7254a Mon Sep 17 00:00:00 2001 From: Clay Good Date: Tue, 4 Aug 2026 09:16:03 -0500 Subject: [PATCH 5/6] fix(status): make completion guidance stage-neutral --- skills/openspec-continue-change/SKILL.md | 2 +- src/core/change-status-policy.ts | 6 ++++-- src/core/templates/workflows/continue-change.ts | 4 ++-- test/cli-e2e/store-lifecycle.test.ts | 7 +++++++ test/commands/artifact-workflow.test.ts | 4 ++++ test/core/templates/skill-templates-parity.test.ts | 6 +++++- 6 files changed, 23 insertions(+), 6 deletions(-) diff --git a/skills/openspec-continue-change/SKILL.md b/skills/openspec-continue-change/SKILL.md index a5a92bc212..37201adf90 100644 --- a/skills/openspec-continue-change/SKILL.md +++ b/skills/openspec-continue-change/SKILL.md @@ -51,7 +51,7 @@ Continue working on a change by creating the next artifact. **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. Once implementation and tasks are complete, archive it." + - Suggest: "Planning is complete! You can now implement this change. Once implementation and any tracked work are complete, archive it." - STOP --- diff --git a/src/core/change-status-policy.ts b/src/core/change-status-policy.ts index aac089fbef..f922bd1f1e 100644 --- a/src/core/change-status-policy.ts +++ b/src/core/change-status-policy.ts @@ -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; diff --git a/src/core/templates/workflows/continue-change.ts b/src/core/templates/workflows/continue-change.ts index 40f63bcf8c..14b3109e43 100644 --- a/src/core/templates/workflows/continue-change.ts +++ b/src/core/templates/workflows/continue-change.ts @@ -53,7 +53,7 @@ ${STORE_SELECTION_GUIDANCE} **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. Once implementation and tasks are complete, archive it." + - Suggest: "Planning is complete! You can now implement this change. Once implementation and any tracked work are complete, archive it." - STOP --- @@ -171,7 +171,7 @@ ${STORE_SELECTION_GUIDANCE} **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\`. Once implementation and tasks are complete, 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 --- diff --git a/test/cli-e2e/store-lifecycle.test.ts b/test/cli-e2e/store-lifecycle.test.ts index 3eec644602..39c79da9d8 100644 --- a/test/cli-e2e/store-lifecycle.test.ts +++ b/test/cli-e2e/store-lifecycle.test.ts @@ -425,6 +425,13 @@ describe('standalone store lifecycle journey', () => { expect(status.exitCode).toBe(0); 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], { env: machineB, cwd: base } diff --git a/test/commands/artifact-workflow.test.ts b/test/commands/artifact-workflow.test.ts index e36a28872f..1d5000c7f6 100644 --- a/test/commands/artifact-workflow.test.ts +++ b/test/commands/artifact-workflow.test.ts @@ -168,6 +168,10 @@ describe('artifact-workflow CLI commands', () => { 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); }); diff --git a/test/core/templates/skill-templates-parity.test.ts b/test/core/templates/skill-templates-parity.test.ts index a706da7441..bcdcbdc3e0 100644 --- a/test/core/templates/skill-templates-parity.test.ts +++ b/test/core/templates/skill-templates-parity.test.ts @@ -266,7 +266,11 @@ describe('skill templates split parity', () => { ]; for (const [variant, content] of variants) { - expect(content, variant).toContain('Once implementation and tasks are complete, archive it'); + expect(content, variant).toContain('Planning is complete!'); + expect(content, variant).toContain( + 'Once implementation and any tracked work are complete, archive it' + ); + expect(content, variant).not.toContain('All artifacts created!'); expect(content, variant).not.toContain('or archive it'); } }); From 9fc15225214bac330a5c5754e1c66fe93f1ec281 Mon Sep 17 00:00:00 2001 From: Clay Good Date: Tue, 4 Aug 2026 15:55:01 -0500 Subject: [PATCH 6/6] test(status): align parity hashes after rebase --- test/core/templates/skill-templates-parity.test.ts | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/test/core/templates/skill-templates-parity.test.ts b/test/core/templates/skill-templates-parity.test.ts index bcdcbdc3e0..8adb04a051 100644 --- a/test/core/templates/skill-templates-parity.test.ts +++ b/test/core/templates/skill-templates-parity.test.ts @@ -39,14 +39,14 @@ import { STORE_SELECTION_GUIDANCE } from '../../../src/core/templates/workflows/ const EXPECTED_FUNCTION_HASHES: Record = { getExploreSkillTemplate: 'fd45923f8d9eecb8896c17d5ce6d309302132e289132c680d5b3b4d6490501e8', getNewChangeSkillTemplate: '935f6335e2d4b7d1bd4f0538c88386350c25e8b16e11b627556262229583ca51', - getContinueChangeSkillTemplate: '1354a92b54d8b3c0e6979c46e3bd3b0fb4e619c4a775ae9d33c1e4dc809d709d', + getContinueChangeSkillTemplate: 'ed41e2356af7aad6ef760f60fad19c6843cefe436d8f90084dcba4dbc6bf7272', getApplyChangeSkillTemplate: 'e5fc093637d3100a61acf934553002a5e9f5bccab5110136d7680af4133f7351', getFfChangeSkillTemplate: 'fc2a45a08533ee9c7ab30fdab5f832b7d440070048e2a153f03db1620dc379bb', getSyncSpecsSkillTemplate: 'f90032dbeb3a647b451139e12624753057018986df000159499dadc2c3d0965a', getOnboardSkillTemplate: '0b0f9559e21e73a7acfb7e61b403b20080f10ba169d2330c6d55618ce1759a42', getOpsxExploreCommandTemplate: '0f9af4120cfa7a8f273eebe7c0ddb56fd7c8705b28d1b1d48e1964a26b91d02f', getOpsxNewCommandTemplate: '08e784e52ac2c146975a874257c589d88e93efbd83dc4d79253c8525f5c3064f', - getOpsxContinueCommandTemplate: 'a00664d4338219e85002f568756998ac4b7b53785d8fad2ff0c1261f3374ec44', + getOpsxContinueCommandTemplate: 'ae964cd00f6ca332fd7f9428a577ade75be279f50431d5f60ece8172e8d1a4b1', getOpsxApplyCommandTemplate: 'd879b0430f756b9dbc5a1a1348a34409b2fcd453eeae7add4bf9f421616c2ad1', getOpsxFfCommandTemplate: '012610f85576a7055dfec2aaabba6bfc245454ce91fb6214587ae9316dc2b864', getArchiveChangeSkillTemplate: 'b6dac476db882d5e2afea237e298c2aa98ed9f9cacbcf1a5000f00e67e8ca524', @@ -60,14 +60,14 @@ const EXPECTED_FUNCTION_HASHES: Record = { getOpsxProposeSkillTemplate: '416200ae0277061405d17d5506243657ee26f7b883abe063844126c497d88f94', getOpsxProposeCommandTemplate: '8de5ce5fe15c0b13ee1801b6b18cb86dc16ddea66c34223fafb4360232d8424d', getFeedbackSkillTemplate: 'd7d83c5f7fc2b92fe8f4588a5bf2d9cb315e4c73ec19bcd5ef28270906319a0d', - getUpdateChangeSkillTemplate: 'f85fbfb3a175e949becbef08be0eccfab97de5e7ad45105e999d2900dfafbaba', - getOpsxUpdateCommandTemplate: '461edf06e92c0da3dab4f11d91d59d44b48ed30a0881c1f34a714b1813435af6', + getUpdateChangeSkillTemplate: 'e50b6cd5d38f0d8974172fd7ebd6e2139f3fe3782c71584d8a61cfdb54edff8e', + getOpsxUpdateCommandTemplate: '4f1530486fbe118d9d7d469083c5517b8ec341ed8e92282e0b6c5155fb945bfe', }; const EXPECTED_GENERATED_SKILL_CONTENT_HASHES: Record = { 'openspec-explore': '87a93d0d748c071982ed2199719f00b2885db94d4ac11ae9f12f79909777660c', 'openspec-new-change': '579d432771703f947a331a6ed288bf9c6660ca015fcd376d76f19b6ac7683082', - 'openspec-continue-change': '06a8e9df0c34de6e90e067d6d17e8e361d48ec08adb57786bc63c41dd03529e8', + 'openspec-continue-change': '5c34be8194cdb4c5158335e47aece71143e8a22bfb4179dba47fd8aaf436d395', 'openspec-apply-change': '1726319cd4305a47f9c827acaeb84a9de57f7e44aba9ed60869c1758338e18ae', 'openspec-ff-change': '19315644df7c582d920acfb67f3c500ca4e06fccc900265b3ac39621d85f7cdb', 'openspec-sync-specs': 'dbdc0528c5d59c1a9b3c8b3df01ab2bcf325ad2cb5d47e061c7a65106c058a3e', @@ -76,7 +76,7 @@ const EXPECTED_GENERATED_SKILL_CONTENT_HASHES: Record = { 'openspec-verify-change': '7cd65897d126f7c948620c0672ca62418620dbcb82ee73d890f758fb666a4ff8', 'openspec-onboard': '80f39cf33a138aac8e508db25d7af2c9e9bd482f90e414770e806f966dd58c9c', 'openspec-propose': '48b06cf0fa53be06c84fc3e79729fb16b7b9d8549cbed6d89616eb6ba1f7e325', - 'openspec-update-change': '95bb533105e49aee06c9ea164b63092de77644cf8f94fa38d3ee3c11b0ccb893', + 'openspec-update-change': '8654fc3ea1eb2f03e1dba3eaf1e8c884b1c71cc949294a070c2f966fb13c8e2a', }; // Intentionally excludes getFeedbackSkillTemplate: this list only models templates