Version
@fission-ai/openspec v1.2.0, Windows 11, Node.js
Summary
openspec instructions apply --change <name> reports Missing artifacts: specs even when the spec file exists and openspec status correctly shows specs: done.
The root cause is that instructions.js uses a hand-written glob matcher that does not recurse into subdirectories for single * patterns (only for **), while state.js uses fast-glob which handles all patterns correctly.
Steps to Reproduce
-
Create a schema with an artifact that uses a glob generates pattern:
- id: specs
generates: specs/*/spec.md
-
Create the spec file at the expected location:
openspec/changes/my-change/specs/my-change/spec.md
-
Run status — reports correctly:
$ openspec status --change "my-change" --json
# specs artifact status: "done" ✓
-
Run apply — incorrectly blocked:
$ openspec instructions apply --change "my-change"
## Apply: my-change
### ⚠️ Blocked
Missing artifacts: specs
Root Cause
Two separate artifactOutputExists implementations with different behavior:
| File |
Method |
Glob support |
core/artifact-graph/state.js |
isArtifactComplete() → hasGlobMatches() |
Uses fast-glob (fg.sync) — correct |
commands/workflow/instructions.js |
artifactOutputExists() |
Hand-written: only recurses for **, skips subdirs for * — broken |
The hand-written matcher in instructions.js (lines ~167-219 of the original) does this:
// For ** patterns, recurse into subdirectories
if (generates.includes('**') && hasMatchingFiles(path.join(dir, entry.name))) {
return true;
}
For a pattern like specs/*/spec.md, the * matches at the directory level (specs/ is the dirPath), but the function only checks files in that directory — it never enters subdirectories because generates.includes('**') is false.
Secondary issue: unresolved glob in contextFiles
Even after fixing detection, contextFiles stores the raw glob path:
{
"contextFiles": {
"specs": "C:\\...\\specs/*/spec.md" // not a valid file path
}
}
This means downstream consumers (skills, agents) cannot read the specs file from the context.
Suggested Fix
Replace the hand-written glob logic in instructions.js with fast-glob, matching what state.js already does:
import fg from 'fast-glob';
function artifactOutputExists(changeDir, generates) {
const fullPattern = path.join(changeDir, generates);
if (generates.includes('*') || generates.includes('?') || generates.includes('[')) {
const normalizedPattern = fullPattern.split(path.sep).join('/');
const matches = fg.sync(normalizedPattern, { onlyFiles: true });
return matches.length > 0;
}
return fs.existsSync(fullPattern);
}
And resolve glob paths in contextFiles to actual file paths:
// Build context files from all existing artifacts in schema
const contextFiles = {};
for (const artifact of schema.artifacts) {
if (artifactOutputExists(changeDir, artifact.generates)) {
if (artifact.generates.includes('*') || artifact.generates.includes('?') || artifact.generates.includes('[')) {
const fullPattern = path.join(changeDir, artifact.generates).split(path.sep).join('/');
const matches = fg.sync(fullPattern, { onlyFiles: true });
if (matches.length > 0) {
contextFiles[artifact.id] = matches[0];
}
} else {
contextFiles[artifact.id] = path.join(changeDir, artifact.generates);
}
}
}
Ideal long-term fix
Extract a shared utility (e.g. in core/artifact-graph/) for glob-aware file existence checking and path resolution, so state.js and instructions.js use the same code path. This eliminates the duplication that caused this divergence.
Workaround
Manually patch node_modules/@fission-ai/openspec/dist/commands/workflow/instructions.js with the suggested fix above.
Version
@fission-ai/openspecv1.2.0, Windows 11, Node.jsSummary
openspec instructions apply --change <name>reportsMissing artifacts: specseven when the spec file exists andopenspec statuscorrectly showsspecs: done.The root cause is that
instructions.jsuses a hand-written glob matcher that does not recurse into subdirectories for single*patterns (only for**), whilestate.jsusesfast-globwhich handles all patterns correctly.Steps to Reproduce
Create a schema with an artifact that uses a glob
generatespattern:Create the spec file at the expected location:
Run status — reports correctly:
Run apply — incorrectly blocked:
Root Cause
Two separate
artifactOutputExistsimplementations with different behavior:core/artifact-graph/state.jsisArtifactComplete()→hasGlobMatches()fast-glob(fg.sync) — correctcommands/workflow/instructions.jsartifactOutputExists()**, skips subdirs for*— brokenThe hand-written matcher in
instructions.js(lines ~167-219 of the original) does this:For a pattern like
specs/*/spec.md, the*matches at the directory level (specs/is thedirPath), but the function only checks files in that directory — it never enters subdirectories becausegenerates.includes('**')isfalse.Secondary issue: unresolved glob in
contextFilesEven after fixing detection,
contextFilesstores the raw glob path:{ "contextFiles": { "specs": "C:\\...\\specs/*/spec.md" // not a valid file path } }This means downstream consumers (skills, agents) cannot read the specs file from the context.
Suggested Fix
Replace the hand-written glob logic in
instructions.jswithfast-glob, matching whatstate.jsalready does:And resolve glob paths in
contextFilesto actual file paths:Ideal long-term fix
Extract a shared utility (e.g. in
core/artifact-graph/) for glob-aware file existence checking and path resolution, sostate.jsandinstructions.jsuse the same code path. This eliminates the duplication that caused this divergence.Workaround
Manually patch
node_modules/@fission-ai/openspec/dist/commands/workflow/instructions.jswith the suggested fix above.