diff --git a/schemas/spec-driven/schema.yaml b/schemas/spec-driven/schema.yaml index 45f61e222..ebf17b41e 100644 --- a/schemas/spec-driven/schema.yaml +++ b/schemas/spec-driven/schema.yaml @@ -146,7 +146,7 @@ artifacts: - design apply: - requires: [tasks] + requires: [specs, tasks] tracks: tasks.md instruction: | Read context files, work through pending tasks, mark complete as you go. diff --git a/src/commands/workflow/instructions.ts b/src/commands/workflow/instructions.ts index 10a5fac16..a28b3e32b 100644 --- a/src/commands/workflow/instructions.ts +++ b/src/commands/workflow/instructions.ts @@ -395,7 +395,11 @@ export async function generateApplyInstructions( if (missingArtifacts.length > 0) { state = 'blocked'; - instruction = `Cannot apply this change yet. Missing artifacts: ${missingArtifacts.join(', ')}.\nUse the openspec-continue-change skill to create the missing artifacts first.`; + const specsHint = + missingArtifacts.includes('specs') && context.schemaName === 'spec-driven' + ? '\nDelta specs must exist under changes//specs/ before implementation.' + : ''; + instruction = `Cannot apply this change yet. Missing artifacts: ${missingArtifacts.join(', ')}.${specsHint}\nUse the openspec-continue-change skill to create the missing artifacts first.`; } else if (tracksFile && !tracksFileExists) { // Tracking file configured but doesn't exist yet const tracksFilename = path.basename(tracksFile); @@ -467,10 +471,13 @@ export async function applyInstructionsCommand(options: ApplyInstructionsOptions if (options.json) { console.log(JSON.stringify({ ...instructions, root: toRootOutput(root) }, null, 2)); - return; + } else { + printApplyInstructionsText(instructions); } - printApplyInstructionsText(instructions); + if (instructions.state === 'blocked') { + process.exitCode = 1; + } } catch (error) { spinner?.stop(); throw error; diff --git a/test/commands/artifact-workflow.test.ts b/test/commands/artifact-workflow.test.ts index a286422a8..01ddce3d6 100644 --- a/test/commands/artifact-workflow.test.ts +++ b/test/commands/artifact-workflow.test.ts @@ -434,15 +434,30 @@ describe('artifact-workflow CLI commands', () => { }); it('shows blocked state when required artifacts are missing', async () => { - // Only create proposal - missing tasks (required by spec-driven apply block) + // Only create proposal - missing specs and tasks (required by spec-driven apply block) await createTestChange('blocked-apply', ['proposal']); const result = await runCLI(['instructions', 'apply', '--change', 'blocked-apply'], { cwd: tempDir, }); - expect(result.exitCode).toBe(0); + expect(result.exitCode).toBe(1); expect(result.stdout).toContain('Blocked'); - expect(result.stdout).toContain('Missing artifacts: tasks'); + expect(result.stdout).toContain('Missing artifacts: specs, tasks'); + }); + + it('blocks apply when tasks exist but delta specs are missing', async () => { + await createTestChange('missing-specs-apply', ['proposal', 'design', 'tasks']); + + const result = await runCLI( + ['instructions', 'apply', '--change', 'missing-specs-apply', '--json'], + { cwd: tempDir } + ); + expect(result.exitCode).toBe(1); + + const json = JSON.parse(result.stdout); + expect(json.state).toBe('blocked'); + expect(json.missingArtifacts).toEqual(['specs']); + expect(json.instruction).toContain('Delta specs must exist'); }); it('outputs JSON for apply instructions', async () => { @@ -568,7 +583,7 @@ apply: }); it('spec-driven schema uses apply block configuration', async () => { - // Verify that spec-driven schema uses its apply block (requires: [tasks]) + // Verify that spec-driven schema uses its apply block (requires: [specs, tasks]) await createTestChange('apply-config-test', ['proposal', 'design', 'specs', 'tasks']); const result = await runCLI( @@ -578,7 +593,6 @@ apply: expect(result.exitCode).toBe(0); const json = JSON.parse(result.stdout); - // spec-driven schema has apply block with requires: [tasks], so should be ready expect(json.schemaName).toBe('spec-driven'); expect(json.state).toBe('ready'); }); @@ -624,7 +638,7 @@ artifacts: env: { XDG_DATA_HOME: userDataDir }, } ); - expect(result.exitCode).toBe(0); + expect(result.exitCode).toBe(1); const json = JSON.parse(result.stdout); // Without apply block, fallback requires ALL artifacts - second is missing diff --git a/test/fixtures/tmp-init/openspec/changes/c1/tasks.md b/test/fixtures/tmp-init/openspec/changes/c1/tasks.md new file mode 100644 index 000000000..de1871bff --- /dev/null +++ b/test/fixtures/tmp-init/openspec/changes/c1/tasks.md @@ -0,0 +1,3 @@ +## 1. Implementation + +- [ ] 1.1 Complete the change \ No newline at end of file