Skip to content

[Feature]: Add specify artifact command exposing composition stacks as JSON #4212

Description

@nicolehaugen

Problem Statement

PresetResolver.collect_all_layers() (presets/__init__.py:5460) and resolve_content() (presets/__init__.py:5688) already implement the composition math needed to expose an artifact's layer stack — ordered layer stack, per-layer strategy, winner selection, and shadowing — but nothing serializes it. speckit-wizard-canvas resorts to shelling specify preset resolve <id> and parsing text output one line at a time (composition/collect.mjs::batchResolveTemplates), and re-implements strategy resolution and shadowing itself in composition/assembler.mjs (~23 KB). The wizard's own header comment on composition/collect.mjs says:

"Once the CLI grows equivalent commands (e.g. specify composition list --json), this whole file goes away and the scanner calls those commands directly."

This is a direct in-code TODO for this issue.

Proposed Solution

Add a new specify artifact command group:

  • specify artifact list --json — every artifact SpecKit knows about (commands, templates, scripts), one row each:

    { "id": "", "name": "speckit.constitution", "kind": "command",
      "description": "" }
  • specify artifact info <name> --json — the full stack for one artifact:

    {
      "id": "", "name": "speckit.constitution", "kind": "command",
      "description": "",
      "stack": [
        { "layer": "preset", "presetId": "speckit.corp",
          "presetName": "Corp Preset",
          "strategy": "wrap", "active": true, "hidden": false,
          "manifestPath": ".specify/presets/speckit.corp/preset.yml",
          "lookupId": "preset:speckit.corp:command:speckit.constitution" },
        { "layer": "preset", "presetId": "speckit.core", "…": "",
          "strategy": "replace", "active": false, "hidden": true },
        { "layer": "core", "presetId": null, "presetName": null,
          "strategy": "replace", "active": false, "hidden": true,
          "manifestPath": null,
          "lookupId": "core:_:command:speckit.constitution" }
      ]
    }

Stack rules:

  • Ordered highest-priority-first (matches collect_all_layers today).
  • Exactly one layer has active: true (the winner from resolve_content).
  • Layers below a higher-priority replace get hidden: true (they stay in the stack for provenance/debug).
  • Each stack ends with a layer: "core" row when SpecKit ships a baseline version of that artifact (folds the baseline into every stack view without needing a separate scan).
  • lookupId uses the stable-id scheme from the companion "stable id / lookupId" issue so a UI can hop from the stack row to the full per-contribution detail from the preset info --json / extension info --json issue.

Implementation: wrap the existing collect_all_layers + resolve_content output; add a small enumerator for core baseline artifacts (command names come from CORE_COMMAND_NAMES at extensions/__init__.py:119; templates/scripts are enumerated via a frontmatter parser over the shipped .md/.sh/.ps1/.py files, reusing the existing _locate_core_pack() / _repo_root() canonical asset resolvers).

Alternatives Considered

  • Extend specify preset resolve <id> with --json. Rejected — that command answers "which file wins?", not "what does the stack look like?" Consumers would still have to compose multiple calls.
  • Fold the core inventory into a separate specify core list --json. Rejected as the primary surface — same data, splits consumers. Filed as a separate specify core inventory issue for the inventory case where a UI wants the baseline without walking every artifact stack.
  • Include the resolved content in info --json. Rejected — content can be large; a separate artifact resolve (already exists as preset resolve) covers that.

Component

Specify CLI (initialization, commands)

AI Agent (if applicable)

Not applicable

Use Cases

  1. speckit-wizard-canvas deletes composition/assembler.mjs and composition/collect.mjs::batchResolveTemplates, calling specify artifact info <name> --json for each artifact it needs to render.
  2. A debugger renders "why is my speckit.plan command not what I expect?" by walking the stack — the answer is the active: true row's presetId and strategy.
  3. A CI diff shows "before/after" of the artifact stack when a preset is added or its priority changes.

Acceptance Criteria

  • specify artifact list --json emits one row per artifact with id, name, kind (command|template|script), description.
  • specify artifact info <name> --json emits {id, name, kind, description, stack: [...]} where every stack entry has layer, presetId/presetName (or null for core), strategy, active, hidden, manifestPath, lookupId.
  • Exactly one stack entry has active: true; entries shadowed by a higher-priority replace have hidden: true.
  • Core baseline appears as a layer: "core" row at the bottom of every stack whose artifact ships with SpecKit itself.
  • lookupId values conform to the stable-id scheme and match the id values returned by the companion preset info --json / extension info --json issue.
  • Every stack row with layer: "extension" has strategy: "replace" (invariant already enforced at extensions/__init__.py:622-626, where any strategy key on an extension provides entry raises ValidationError).
  • Error handling: unknown artifact name → non-zero exit, stderr JSON {"error": "unknown artifact <name>"}.
  • Tests: composition parity against resolve_content (winner matches), shadow flag correctness, core-layer presence for baseline artifacts, absence for extension-only artifacts, lookupId round-trip with the companion info --json output.
  • Docs: new specify artifact section in the CLI reference with the full JSON shape and stack semantics.

Additional Context

Direct replacement for plugins/spec-kit-copilot-wizard/extensions/speckit-wizard-canvas/composition/assembler.mjs and composition/collect.mjs::batchResolveTemplates in github/spec-kit-copilot. Depends on the companion "stable id / lookupId" issue (for lookupId); benefits from the companion "structured source provenance" issue (source on layer rows) once available.

Scope note (out of scope). A separate WorkflowResolver exists in src/specify_cli/workflows/overlays/__init__.py with its own collect_all_layers() for .specify/workflows/overlays/* composition. That is a distinct system from preset artifact composition and is not covered by this issue — this issue is scoped to commands, templates, and scripts. A future specify workflow composition view would be a separate issue against the workflow resolver.

Metadata

Metadata

Assignees

Labels

enhancementfeature-assessRun the Spec Kit idea-assessment pipeline on this feature requestfeature-goFeature assessment verdict: go — ready to hand off to /speckit.specify

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions