Skip to content

instructions apply fails to detect glob-pattern artifacts (e.g. specs/*/spec.md) #942

@Se7enLiu

Description

@Se7enLiu

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

  1. Create a schema with an artifact that uses a glob generates pattern:

    - id: specs
      generates: specs/*/spec.md
  2. Create the spec file at the expected location:

    openspec/changes/my-change/specs/my-change/spec.md
    
  3. Run status — reports correctly:

    $ openspec status --change "my-change" --json
    # specs artifact status: "done" ✓
    
  4. 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.

Metadata

Metadata

Assignees

Labels

No labels
No labels

Type

No type

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions