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 SpecKit 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 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
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 presetId and strategy.
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).
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.
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 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:
collect_all_layerstoday).active: true(the winner fromresolve_content).replacegethidden: true(they stay in the stack for provenance/debug).layer: "core"row when SpecKit ships a baseline version of that artifact (folds the baseline into every stack view without needing a separate scan).lookupIduses 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 thepreset 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.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'spresetIdandstrategy.Acceptance Criteria
specify artifact list --jsonemits one row per artifact withid,name,kind(command|template|script),description.specify artifact info <name> --jsonemits{id, name, kind, description, stack: [...]}where every stack entry haslayer,presetId/presetName(ornullforcore),strategy,active,hidden,manifestPath,lookupId.active: true; entries shadowed by a higher-priorityreplacehavehidden: true.layer: "core"row at the bottom of every stack whose artifact ships with SpecKit itself.lookupIdvalues conform to the stable-id scheme and match theidvalues returned by the companionpreset info --json/extension info --jsonissue.layer: "extension"hasstrategy: "replace"(invariant already enforced atextensions/__init__.py:622-626, where anystrategykey on an extensionprovidesentry raisesValidationError).{"error": "unknown artifact <name>"}.resolve_content(winner matches), shadow flag correctness, core-layer presence for baseline artifacts, absence for extension-only artifacts,lookupIdround-trip with the companioninfo --jsonoutput.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 id / lookupId" issue (forlookupId); benefits from the companion "structured source provenance" issue (sourceon layer rows) 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.