Skip to content
Open
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 schemas/spec-driven/schema.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
13 changes: 10 additions & 3 deletions src/commands/workflow/instructions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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/<name>/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);
Expand Down Expand Up @@ -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;
Expand Down
26 changes: 20 additions & 6 deletions test/commands/artifact-workflow.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 () => {
Expand Down Expand Up @@ -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(
Expand All @@ -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');
});
Expand Down Expand Up @@ -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
Expand Down
3 changes: 3 additions & 0 deletions test/fixtures/tmp-init/openspec/changes/c1/tasks.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
## 1. Implementation

- [ ] 1.1 Complete the change