From 75db1ed1d7a65492ece4f9b25b261a30aeb85e5e Mon Sep 17 00:00:00 2001 From: lawsonoates Date: Wed, 24 Jun 2026 22:05:54 +1000 Subject: [PATCH 1/3] fix(apply): block instructions apply when required artifacts are missing Exit with code 1 when apply instructions are blocked, add a spec-driven hint when delta specs are absent, and extend tests plus the tmp-init fixture so blocked and ready apply paths are covered. --- src/commands/workflow/instructions.ts | 13 +++++++--- test/commands/artifact-workflow.test.ts | 26 ++++++++++++++----- .../tmp-init/openspec/changes/c1/tasks.md | 3 +++ 3 files changed, 33 insertions(+), 9 deletions(-) create mode 100644 test/fixtures/tmp-init/openspec/changes/c1/tasks.md 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 From 9315bd1cdb32447f15f9d735eed77a969eeec914 Mon Sep 17 00:00:00 2001 From: lawsonoates Date: Wed, 24 Jun 2026 22:19:18 +1000 Subject: [PATCH 2/3] test(apply): align blocked-apply tests with spec-driven apply.requires Expect only tasks in the missing-artifacts message and assert that missing delta specs do not block apply while the default schema still requires tasks alone. --- test/commands/artifact-workflow.test.ts | 15 +++++++-------- 1 file changed, 7 insertions(+), 8 deletions(-) diff --git a/test/commands/artifact-workflow.test.ts b/test/commands/artifact-workflow.test.ts index 01ddce3d6..3630ad416 100644 --- a/test/commands/artifact-workflow.test.ts +++ b/test/commands/artifact-workflow.test.ts @@ -434,7 +434,7 @@ describe('artifact-workflow CLI commands', () => { }); it('shows blocked state when required artifacts are missing', async () => { - // Only create proposal - missing specs and tasks (required by spec-driven apply block) + // Only create proposal - missing tasks (required by spec-driven apply block) await createTestChange('blocked-apply', ['proposal']); const result = await runCLI(['instructions', 'apply', '--change', 'blocked-apply'], { @@ -442,22 +442,21 @@ describe('artifact-workflow CLI commands', () => { }); expect(result.exitCode).toBe(1); expect(result.stdout).toContain('Blocked'); - expect(result.stdout).toContain('Missing artifacts: specs, tasks'); + expect(result.stdout).toContain('Missing artifacts: tasks'); }); - it('blocks apply when tasks exist but delta specs are missing', async () => { + it('does not block 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); + expect(result.exitCode).toBe(0); const json = JSON.parse(result.stdout); - expect(json.state).toBe('blocked'); - expect(json.missingArtifacts).toEqual(['specs']); - expect(json.instruction).toContain('Delta specs must exist'); + expect(json.state).toBe('ready'); + expect(json.missingArtifacts).toBeUndefined(); }); it('outputs JSON for apply instructions', async () => { @@ -583,7 +582,7 @@ apply: }); it('spec-driven schema uses apply block configuration', async () => { - // Verify that spec-driven schema uses its apply block (requires: [specs, tasks]) + // Verify that spec-driven schema uses its apply block (requires: [tasks]) await createTestChange('apply-config-test', ['proposal', 'design', 'specs', 'tasks']); const result = await runCLI( From f2d72fb2a6e84f275f65f9beb06bd0417d3b7ad8 Mon Sep 17 00:00:00 2001 From: lawsonoates Date: Thu, 25 Jun 2026 00:20:13 +1000 Subject: [PATCH 3/3] fix(spec-driven): require delta specs before apply Add specs to apply.requires so fast-path workflows cannot stop at tasks alone. Update apply instruction tests to expect blocked state when delta specs are missing. --- schemas/spec-driven/schema.yaml | 2 +- test/commands/artifact-workflow.test.ts | 15 ++++++++------- 2 files changed, 9 insertions(+), 8 deletions(-) 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/test/commands/artifact-workflow.test.ts b/test/commands/artifact-workflow.test.ts index 3630ad416..01ddce3d6 100644 --- a/test/commands/artifact-workflow.test.ts +++ b/test/commands/artifact-workflow.test.ts @@ -434,7 +434,7 @@ 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'], { @@ -442,21 +442,22 @@ describe('artifact-workflow CLI commands', () => { }); 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('does not block apply when tasks exist but delta specs are missing', async () => { + 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(0); + expect(result.exitCode).toBe(1); const json = JSON.parse(result.stdout); - expect(json.state).toBe('ready'); - expect(json.missingArtifacts).toBeUndefined(); + 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 () => { @@ -582,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(