You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
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 Spec Kit knows about (commands, templates, scripts), one row each:
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 built-in baseline row when Spec Kit ships that artifact. Built-in rows are recognized by absent provenance: layer, sourceId, and lookupId are null.
The top-level and per-row id use the source-agnostic {kind}:{name} form and are the round-trip key accepted by specify artifact info, including for built-ins.
lookupId is present only on provenance-backed non-built-in layers. Manifest-declared preset and extension values match the contribution IDs exposed by the companion 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.
Emit synthetic core:_:... lookup IDs for built-ins. Rejected because built-ins have no originating manifest contribution; their source-agnostic public artifact id already provides a stable round-trip key.
Component
Specify CLI (initialization, commands)
AI Agent (if applicable)
Not applicable
Use Cases
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.
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 provider and strategy; a row with null provenance is the built-in fallback.
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, and stack.
specify artifact info <name> --json emits {id, name, kind, description, stack: [...]} where every stack entry has id, layer, sourceId, presetId, presetName, strategy, active, hidden, manifestPath, and lookupId; provenance fields may be null for built-ins.
Exactly one stack entry has active: true; entries shadowed by a higher-priority replace have hidden: true.
A built-in baseline row appears at the bottom of every stack whose artifact ships with Spec Kit itself; its layer, sourceId, presetId, presetName, manifestPath, and lookupId are null.
Every artifact and stack row carries the source-agnostic {kind}:{name}id, which round-trips through specify artifact info.
Manifest-declared preset and extension lookupId values conform to the stable contribution-ID scheme and match the IDs returned by the companion preset info --json / extension info --json issue.
Every stack row with layer: "extension" has strategy: "replace" (invariant already enforced by extension validation).
Tests: composition parity against resolve_content (winner matches), shadow flag correctness, built-in-row presence for baseline artifacts, absence for extension-only artifacts, manifest-backed lookupId joins, and built-in public-ID round-trip.
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 contribution ID / lookupId issue; benefits from structured source provenance 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.
Problem Statement
PresetResolver.collect_all_layers()(presets/__init__.py:5460) andresolve_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-canvasresorts to shellingspecify preset resolve <id>and parsing text output one line at a time (composition/collect.mjs::batchResolveTemplates), and re-implements strategy resolution and shadowing itself incomposition/assembler.mjs(~23 KB). The wizard's own header comment oncomposition/collect.mjssays:This is a direct in-code TODO for this issue.
Proposed Solution
Add a new
specify artifactcommand group:specify artifact list --json— every artifact Spec Kit knows about (commands, templates, scripts), one row each:{ "id": "command:speckit.constitution", "name": "speckit.constitution", "kind": "command", "description": "…" }specify artifact info <name> --json— the full stack for one artifact:{ "id": "command:speckit.constitution", "name": "speckit.constitution", "kind": "command", "description": "…", "stack": [ { "id": "command:speckit.constitution", "layer": "preset", "sourceId": "speckit.corp", "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" }, { "id": "command:speckit.constitution", "layer": "preset", "sourceId": "speckit.core", "presetId": "speckit.core", "presetName": "…", "strategy": "replace", "active": false, "hidden": true, "manifestPath": ".specify/presets/speckit.core/preset.yml", "lookupId": "preset:speckit.core:command:speckit.constitution" }, { "id": "command:speckit.constitution", "layer": null, "sourceId": null, "presetId": null, "presetName": null, "strategy": "replace", "active": false, "hidden": true, "manifestPath": null, "lookupId": null } ] }Stack rules:
collect_all_layerstoday).active: true(the winner fromresolve_content).replacegethidden: true(they stay in the stack for provenance/debug).layer,sourceId, andlookupIdare null.iduse the source-agnostic{kind}:{name}form and are the round-trip key accepted byspecify artifact info, including for built-ins.lookupIdis present only on provenance-backed non-built-in layers. Manifest-declared preset and extension values match the contribution IDs exposed by the companionpreset info --json/extension info --jsonissue.Implementation: wrap the existing
collect_all_layers+resolve_contentoutput; add a small enumerator for core baseline artifacts (command names come fromCORE_COMMAND_NAMESatextensions/__init__.py:119; templates/scripts are enumerated via a frontmatter parser over the shipped.md/.sh/.ps1/.pyfiles, reusing the existing_locate_core_pack()/_repo_root()canonical asset resolvers).Alternatives Considered
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.specify core list --json. Rejected as the primary surface — same data, splits consumers. Filed as a separatespecify core inventoryissue for the inventory case where a UI wants the baseline without walking every artifact stack.info --json. Rejected — content can be large; a separateartifact resolve(already exists aspreset resolve) covers that.core:_:...lookup IDs for built-ins. Rejected because built-ins have no originating manifest contribution; their source-agnostic public artifactidalready provides a stable round-trip key.Component
Specify CLI (initialization, commands)
AI Agent (if applicable)
Not applicable
Use Cases
speckit-wizard-canvasdeletescomposition/assembler.mjsandcomposition/collect.mjs::batchResolveTemplates, callingspecify artifact info <name> --jsonfor each artifact it needs to render.speckit.plancommand not what I expect?" by walking the stack — the answer is theactive: truerow's provider and strategy; a row with null provenance is the built-in fallback.Acceptance Criteria
specify artifact list --jsonemits one row per artifact withid,name,kind(command|template|script),description, andstack.specify artifact info <name> --jsonemits{id, name, kind, description, stack: [...]}where every stack entry hasid,layer,sourceId,presetId,presetName,strategy,active,hidden,manifestPath, andlookupId; provenance fields may be null for built-ins.active: true; entries shadowed by a higher-priorityreplacehavehidden: true.layer,sourceId,presetId,presetName,manifestPath, andlookupIdare null.{kind}:{name}id, which round-trips throughspecify artifact info.lookupIdvalues conform to the stable contribution-ID scheme and match the IDs returned by the companionpreset info --json/extension info --jsonissue.layer: "extension"hasstrategy: "replace"(invariant already enforced by extension validation).{"error": "unknown artifact <name>"}.resolve_content(winner matches), shadow flag correctness, built-in-row presence for baseline artifacts, absence for extension-only artifacts, manifest-backedlookupIdjoins, and built-in public-ID round-trip.specify artifactsection 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.mjsandcomposition/collect.mjs::batchResolveTemplatesingithub/spec-kit-copilot. Depends on the companion stable contribution ID /lookupIdissue; benefits from structured source provenance once available.Scope note (out of scope). A separate
WorkflowResolverexists insrc/specify_cli/workflows/overlays/__init__.pywith its owncollect_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 futurespecify workflowcomposition view would be a separate issue against the workflow resolver.