Move wizard composition to specify artifact CLI (blocked by spec-kit#4305) - #18
Move wizard composition to specify artifact CLI (blocked by spec-kit#4305)#18nicolehaugen wants to merge 18 commits into
Conversation
Complements artifact-cli.test.mjs (fixture round-trip) with three real-shape guards: * Live-CLI test invokes real `specify artifact list/info` on a scaffolded workspace and asserts wizard-contract fields (id/kind/stack, layer vocabulary, exactly-one-active). Skips when `specify` isn't on PATH. * Fixture-drift tests replay a committed snapshot of real CLI output and assert the field set the shape mapper reads is present. Guards against silent CLI shape changes without needing the binary in CI. * fixtures/README.md documents regeneration. Full suite: 211/211 pass.
On a warm cache the server's `bootAsync` reaches `phase: ready` before
the browser's first paint. The old flow relied on JS to (a) populate the
overlay content and (b) hide `main.app-body`, then almost immediately
flipped the overlay to `is-hidden` in the same microtask cycle. The
browser composited populate + hide into one frame and the user saw a
blank body flip straight to the loaded app with no boot indicator.
Three-part fix so the overlay is guaranteed to paint:
* Static markup in `index.html` — pre-render the overlay panel with
title + subtitle so it is visible from the very first paint, before
any module fetch/parse.
* CSS-level `main.app-body { visibility: hidden }` — no longer
depends on JS running to keep the app body hidden underneath.
* JS-side minimum visible time (`MIN_OVERLAY_MS = 450`) — even when
the state fetch resolves in a single frame, the hide is deferred via
`setTimeout` so the overlay stays up long enough to register.
Boot's `hydrateCatalogs` used to walk preset → extension → bundle serially, and each hydrator walked its 2–3 source URLs serially inside `hydrateFromCatalogSources`. That's ~8 GitHub GETs strictly serial on a cold cache, plus 3 sequential `specify <kind> list` shell-outs, for what is entirely disjoint state. Two changes: * Run `hydratePresetsForSources` / `hydrateExtensionsForSources` / `hydrateBundlesForSources` via `Promise.all` — they touch independent cache slices. * Inside `hydrateFromCatalogSources`, `Promise.all` the per-source `fetchCatalogJson` calls before folding into the items array. Order of items is preserved because we still iterate the resolved array in source order. All 211 tests pass.
This reverts commit bb5edf2.
The catalog boot step called specify artifact info once per artifact via �xecFileSync in a serial loop. With a real workspace stack (~70 artifacts across 4 presets + 1 extension), that's ~70 shell-outs, each one blocking the Node event loop for its full duration. Impact: /api/state and SSE could not be answered during boot, so the UI sat on 'Loading catalogs' for the full 108s wall time of the loop, even though the HTTP server was up. From the user's perspective the wizard 'hung'. Fix: - Swap the default runner to a promisified execFile so each shell-out yields the event loop instead of hard-blocking it. - Fan the info-per-id calls out with Promise.all — safe now that spawn is non-blocking. - Await the runner return so injected sync test runners (which return a Buffer/string) still work unchanged. Measured on the current workspace (68 artifacts): before: 108s serial sync, HTTP frozen throughout after: 13s parallel async, HTTP responsive throughout (~8x faster). All 211 tests still pass. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: 2d44fd06-6323-4654-8a80-b756561ef669
The catalog boot step was fragile in three separate ways beyond the
sync exec loop already fixed in the artifact-info fan-out:
1. fetchCatalogJson had NO timeout. A stalled socket (slow DNS, TCP
loss, CDN outage) would block the fetch forever and freeze boot on
'Loading catalogs' with no recovery path. Added AbortSignal.timeout
(15s per fetch).
2. specifyRun had NO timeout either. A wedged CLI (uv resolver stuck,
PATH resolution hang) had the same failure mode. Added a 20s kill
timer; on expiry we resolve with the partial stdout (callers already
tolerate empty output).
3. hydrateCatalogs awaited the three groups (presets, extensions,
bundles) serially, and hydrateFromCatalogSources awaited each source
inside a group serially. Both are independent I/O — swapped for
Promise.all at each level so total time is bounded by the slowest
single call, not the sum.
Measured on the current workspace (7 catalogs, 68 artifacts):
before: 108s serial sync, HTTP frozen throughout
after: ~14s parallel async, HTTP responsive throughout (~8x faster,
and now failure-bounded instead of unbounded)
All 211 tests still pass.
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 2d44fd06-6323-4654-8a80-b756561ef669
Boot was making 68 sequential `specify artifact info` shell-outs per artifact to reconstruct composition stacks — the flow that regressed catalog-loading to 27-60s on Windows. spec-kit PR #4305 makes `artifact list --json` return the full per-row stack, so the wizard needs exactly ONE shell-out to build composition.
Wizard-side changes:
- `buildCompositionFromCli` now consumes a single `list --json` call and feeds rows directly through `shapeArtifact`. Presets/extensions summaries are folded from artifact stacks (accepted edge case: a preset that contributes zero currently-active artifacts won't appear — living with it until upstream ships `preset list --json`).
- Removed `specifyArtifactInfo` — dead post-refactor. Don't reintroduce a per-artifact fan-out on the boot critical path.
- Deleted `project-scanner.mjs::scanComposition` and its `.specify/{presets,extensions}.json` reads. Neither file is written by any CLI version; the code was dead. Removed the now-unused `readBoundedJson` import.
- Governing principle: CLI is the source of truth for composition. No direct fs reads of `.registry`/`.yml` from the wizard, ever.
Test-side changes:
- Unit fixtures flattened: list rows now embed `stack` (no separate `info` map). `fakeRunner` simplified to only handle `list`.
- Fixture-drift test rewritten to require `stack` on list rows. Skips gracefully when the on-disk snapshot is pre-#4305 (regen once upstream ships).
- Live-CLI test skips when the installed CLI's `list --json` doesn't yet emit `stack` — same rationale.
- Deleted obsolete `live-cli-info.json` fixture; updated README.
- Deleted `scanWorkspace drops malformed composition entries` test — it exercised the deleted `scanComposition` path.
208 tests: 207 pass, 1 skip (drift, until fixture regen).
Real `specify artifact list --json` output now carries per-row `stack` (spec-kit#4305 has landed). Fixture-drift and live-CLI tests are now actively guarding (208/208 pass, 0 skips) instead of skipping under the pre-#4305 detector.
Boot overlay previously bundled two unrelated phases into one 'catalog' step: remote catalog JSON fetches (~150ms parallel) AND the composition CLI build (specify artifact list --json, ~2s cold). When boot felt slow, you couldn't tell which side was blocked. Split them so each shows independently in the overlay: - `catalog` (Loading catalogs) \u2014 hydratePresets/Extensions/Bundles, remote HTTPS + `specify <group> list` per group, all parallel. - `composition` (Building composition) \u2014 single `specify artifact list --json` call + shape mapping. Now a hang on either side is visible at a glance without log scraping.
Comments now describe current behavior only. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: 2d44fd06-6323-4654-8a80-b756561ef669
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: 2d44fd06-6323-4654-8a80-b756561ef669
- project-scanner.mjs: rewrite composition-block comment to describe current behavior only; drop 'used to look at' language and the fabricated AGENTS.md citation. - artifact-cli.mjs: drop the phantom 'AGENTS.md: CLI is the source of truth for composition.' line from the doc header. - pipeline-fast-path.mjs: rename 'LLM Stage 2' → 'LLM path' (there was no Stage 1) and 'Fast path' → 'Deterministic path' to match the actual code. - ui/index.html, ui/boot.js: drop '- Dev' suffix from the wizard title in all four spots to prepare for check-in. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: 2d44fd06-6323-4654-8a80-b756561ef669
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
There was a problem hiding this comment.
Pull request overview
Moves wizard composition from filesystem inspection to the forthcoming specify artifact list --json API. This remains blocked by spec-kit#4305 and its CLI release.
Changes:
- Adds CLI-backed composition mapping, hook enrichment, and pipeline fast-path logic.
- Separates catalog/composition boot phases and parallelizes bounded catalog hydration.
- Updates boot UX, dependencies, and unit tests while removing the legacy assembler.
Show a summary per file
| File | Description |
|---|---|
ui/styles/boot.css |
Hides app content during boot. |
ui/index.html |
Adds initial boot markup. |
ui/boot.js |
Adds composition progress and minimum display time. |
test/state-and-scanner.test.mjs |
Removes obsolete scanner test. |
test/composition.test.mjs |
Removes legacy composition tests. |
test/boot-progress.test.mjs |
Covers the new boot step. |
test/artifact-cli.test.mjs |
Tests CLI composition mapping. |
project-scanner.mjs |
Removes filesystem composition scanning. |
package.json |
Updates js-yaml. |
package-lock.json |
Locks updated dependency. |
extension.mjs |
Splits catalog and composition boot work. |
composition/pipeline-fast-path.mjs |
Adds deterministic pipeline selection. |
composition/hooks.mjs |
Extracts hook metadata. |
composition/collect.mjs |
Removes legacy filesystem collector. |
composition/assembler.mjs |
Removes legacy assembler. |
composition/artifact-cli.mjs |
Adds CLI-backed composition source. |
catalog/sources.mjs |
Adds fetch timeouts. |
catalog/shared.mjs |
Parallelizes hydration and bounds CLI calls. |
canvas-runtime/composition-apply.mjs |
Integrates CLI composition and fast path. |
canvas-runtime/boot-progress.mjs |
Registers composition boot progress. |
Review details
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
Files not reviewed (1)
- plugins/spec-kit-copilot-wizard/extensions/speckit-wizard-canvas/package-lock.json: Generated file
- Files reviewed: 19/20 changed files
- Comments generated: 4
- Review effort level: Balanced
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
There was a problem hiding this comment.
Review details
Files not reviewed (1)
- plugins/spec-kit-copilot-wizard/extensions/speckit-wizard-canvas/package-lock.json: Generated file
Suppressed comments (3)
Previously missed (1) — in code that hasn't changed since the last review.
plugins/spec-kit-copilot-wizard/extensions/speckit-wizard-canvas/composition/artifact-cli.mjs:105
projectis a valid CLI layer, but the wizard renderers only recognizecore,preset, andextension. Preserving it here causes an active project override to be shown as Core/unchanged (for example,artifactPillOriginfalls through tocoreand contributor rows omit it). Add project-origin handling across the composition UI before accepting this layer.
return {
// CLI `null` layer = built-in; wizard code expects "core".
layer: layer.layer == null ? "core" : layer.layer,
plugins/spec-kit-copilot-wizard/extensions/speckit-wizard-canvas/canvas-runtime/composition-apply.mjs:302
dispatchKindPromptstill checks!fast.stage2Needed, so this renamed return field is never consumed. On every successful refreshstage2Neededisundefined, making the caller return early even whenpipelineFastPathis false; novel commands and stack directives therefore never reachinferPipeline. Update that caller to branch onfast.pipelineFastPathas part of this rename.
return { ok: true, reason, pipelineFastPath: fastPath.canSynthesize };
plugins/spec-kit-copilot-wizard/extensions/speckit-wizard-canvas/composition/artifact-cli.mjs:108
- The upstream CLI contract sets
presetId/presetNameto null for extension layers and identifies the extension throughsourceId. Passing those fields through unchanged breaks this file's own extension folds and ownership checks (accumulateProvidesCounts,activeExtensionIds, and hook-command suppression), all of which key extensions bypresetId; uncatalogued extensions disappear and hook commands can be duplicated. Add the wizard compatibility alias when normalizing extension rows.
return {
// CLI `null` layer = built-in; wizard code expects "core".
layer: layer.layer == null ? "core" : layer.layer,
presetId: layer.presetId ?? null,
presetName: layer.presetName ?? null,
sourceId: layer.sourceId ?? null,
- Files reviewed: 19/20 changed files
- Comments generated: 1
- Review effort level: Balanced
Co-authored-by: nicolehaugen <10600161+nicolehaugen@users.noreply.github.com>
There was a problem hiding this comment.
Review details
Files not reviewed (1)
- plugins/spec-kit-copilot-wizard/extensions/speckit-wizard-canvas/package-lock.json: Generated file
Suppressed comments (3)
Previously missed (1) — in code that hasn't changed since the last review.
plugins/spec-kit-copilot-wizard/extensions/speckit-wizard-canvas/composition/artifact-cli.mjs:432
- The new CLI-backed path's hook enrichment is untested: the replacement tests cover core/preset mapping and pipeline decisions, while the deleted suite contained the only assertions for inline hook attribution, standalone hook artifacts, registration flags, and hook-command suppression. Add a temporary extension manifest plus
extensions.ymlfixture and verify this output throughbuildCompositionFromCli.
const { extensionHookInfo, hooksMap } = await collectHookMetadata(
workspaceRoot,
activeExtensionIds,
);
const artifacts = applyHookAttributions(artifactsRaw, extensionHookInfo, hooksMap);
plugins/spec-kit-copilot-wizard/extensions/speckit-wizard-canvas/canvas-runtime/composition-apply.mjs:302
dispatchKindPromptstill checks!fast.stage2Neededatcanvas-runtime/dispatch.mjs:100. Because this result now omits that property, the condition is true for every successful build, includingpipelineFastPath: false, so Refresh never falls through toinferPipelinefor novel commands or stack directives. Update that caller to branch onfast.pipelineFastPathand remove its stale Stage 2 naming.
return { ok: true, reason, pipelineFastPath: fastPath.canSynthesize };
plugins/spec-kit-copilot-wizard/extensions/speckit-wizard-canvas/extension.mjs:219
- A composition failure calls
tracker.fail, but this unconditionalready()immediately overwritesboot.phasewith"ready".ui/boot.js:112then hides the overlay, and generic composition failures are not rendered throughdepsError, so an unsupported CLI produces empty composition without the error the PR description promises to surface. Preserve the failed phase or surface a persistent in-app error before marking boot ready.
tracker.ready();
- Files reviewed: 19/20 changed files
- Comments generated: 0 new
- Review effort level: Balanced
The composition refresh flow still used the retired stage2Needed contract, incorrectly bypassing LLM pipeline inference. This aligns the runtime on pipelineFastPath while retaining LLM inference as the non-fast fallback.
Fast-path contract
Rename the deterministic decision helper to computePipelineFastPath.
Return pipelineFastPath: true only when inferredPipeline can be synthesized.
Refresh dispatch
Bypass LLM inference only for a successful deterministic pipeline:
if (fast?.ok && fast.pipelineFastPath) {
return { kind, fastComposition: true };
}
Fall through to LLM inference for novel commands, stack directives, or missing canonical anchors.
Terminology and coverage
Replace obsolete Stage 1/2 naming with “pipeline fast path” and “LLM inference.”
Update tests for deterministic synthesis and LLM fallback behavior.
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
There was a problem hiding this comment.
Review details
Files not reviewed (1)
- plugins/spec-kit-copilot-wizard/extensions/speckit-wizard-canvas/package-lock.json: Generated file
Suppressed comments (5)
Previously missed (3) — in code that hasn't changed since the last review.
plugins/spec-kit-copilot-wizard/extensions/speckit-wizard-canvas/test/composition.test.mjs:1
- Deleting this file wholesale also removes the still-applicable regression tests for
pipeline/canonical.mjs,pipeline/effective-phases.mjs, andui/phase-runtime.js; those modules remain in use, and no equivalent tests exist elsewhere intest/. Move those suites to focused test files while deleting only the obsolete assembler/collector coverage.
plugins/spec-kit-copilot-wizard/extensions/speckit-wizard-canvas/composition/artifact-cli.mjs:318 - This rebuilds each hook artifact from scratch even though
byIdcontains the CLI-provided command row, and the original row is filtered out below. As a result the hook loses its CLI description and full stack provenance (includingsourcePath). Seed the hook artifact from the existing command artifact, changing only its kind and binding metadata.
const hookArtifactId = `commands/${hookCommand}`;
let hookArtifact = hookArtifactsById.get(hookArtifactId);
if (!hookArtifact) {
hookArtifact = {
id: hookArtifactId,
kind: "hook",
description: "",
stack: [],
hookBindings: [],
};
hookArtifactsById.set(hookArtifactId, hookArtifact);
plugins/spec-kit-copilot-wizard/extensions/speckit-wizard-canvas/catalog/shared.mjs:61
- Returning partial stdout on timeout is not safely malformed: a timed-out
preset list/extension listcan end after complete early rows, so the parsers accept that prefix and incorrectly mark every later installed item inactive. Treat a timeout as a failed query (null) rather than as a complete listing.
const timer = setTimeout(() => {
try { child.kill(); } catch { /* best-effort */ }
done(stdout || null);
}, timeoutMs);
plugins/spec-kit-copilot-wizard/extensions/speckit-wizard-canvas/canvas-runtime/composition-apply.mjs:306
- The prior review decision was to replace the Stage 2 terminology with
pipelineFastPath, but this exposes both names anddispatch.mjsstill consumes the legacy inverse flag. Complete the rename atomically: return onlypipelineFastPath, and have the refresh dispatcher fast-exit when that flag is true.
pipelineFastPath: fastPath.canSynthesize,
stage2Needed: !fastPath.canSynthesize,
plugins/spec-kit-copilot-wizard/extensions/speckit-wizard-canvas/composition/artifact-cli.mjs:114
sourcePathis part of the blocked upstream list-row stack contract, but this mapper drops it. The UI uses the active layer'ssourcePathto open the actual preset/extension artifact (composition-artifacts.js:600-601andphase-runtime.js:890-898); without it, overridden commands fall back to guessed core paths or become non-clickable. Preserve the field here and add it to the contract fixture.
manifestPath: layer.manifestPath ?? null,
lookupId: layer.lookupId ?? null,
};
- Files reviewed: 19/20 changed files
- Comments generated: 1
- Review effort level: Balanced
| "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-5.2.3.tgz", | ||
| "integrity": "sha512-n+mUVyUX5bVv7G/G2zyIHOhdxfuU1dY2NOFzTQUWiMUbFss8b57NFlgCCaggU78wSw5KVS9cllzeLyzyR+n5nw==", | ||
| "version": "5.3.0", | ||
| "resolved": "https://ms-feed-17.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/js-yaml/-/js-yaml-5.3.0.tgz", |
The issue is actually addressed by this: github/spec-kit#4305 (comment) => please review to confirm if it does address the issue and report back your conclusion. don't make any changes. Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
There was a problem hiding this comment.
Review details
Files not reviewed (1)
- plugins/spec-kit-copilot-wizard/extensions/speckit-wizard-canvas/package-lock.json: Generated file
Suppressed comments (4)
Previously missed (4) — in code that hasn't changed since the last review.
plugins/spec-kit-copilot-wizard/extensions/speckit-wizard-canvas/composition/artifact-cli.mjs:105
- The upstream contract includes an active
projectlayer for project-local overrides, but the wizard consumers only recognizecore,preset, andextension: project overrides fall back to a Core pill, a Preset band, or no contributor label. Add a Project origin/label across the composition and phase renderers (or adapt this layer to an explicit supported UI representation) before passing it through.
return {
// CLI `null` layer = built-in; wizard code expects "core".
layer: layer.layer == null ? "core" : layer.layer,
plugins/spec-kit-copilot-wizard/extensions/speckit-wizard-canvas/composition/hooks.mjs:118
- The extension manifest contract allows a hook event to contain a list of mappings, but spreading an array here creates numeric properties and no
command, so every hook in that supported form is filtered out. Flatten each event's value asArray.isArray(cfg) ? cfg : [cfg]before addingphase.
if (hooks && typeof hooks === "object" && !Array.isArray(hooks)) {
hooks = Object.entries(hooks).map(([phase, cfg]) => ({
phase,
...(cfg && typeof cfg === "object" ? cfg : {}),
}));
plugins/spec-kit-copilot-wizard/extensions/speckit-wizard-canvas/test/artifact-cli.test.mjs:132
- This replacement suite removes coverage for still-live
pipeline/canonical.mjs,pipeline/effective-phases.mjs, andui/phase-runtime.js; repository search finds no remaining tests forcanonicalTemplateIds,effectivePipelinePhases, orresolvePipelineEntry. Only the assembler/collector cases became obsolete, so move the unaffected tests to focused suites rather than dropping them.
describe("buildCompositionFromCli", () => {
plugins/spec-kit-copilot-wizard/extensions/speckit-wizard-canvas/composition/hooks.mjs:73
- Spec Kit extension metadata lives under the top-level
extension:mapping, so readingdoc.name,doc.description, anddoc.versionalways falls back for current manifests. Hook artifacts consequently show the directory id instead of the declared display name and lose version/description metadata. Read these fields fromdoc.extension(with a legacy top-level fallback).
name: doc.name ?? id,
description: doc.description ?? "",
version: doc.version ?? null,
priority: typeof doc.priority === "number" ? doc.priority : null,
- Files reviewed: 19/20 changed files
- Comments generated: 1
- Review effort level: Balanced
| if (kind === "command") { | ||
| const activeLayer = stack.find((layer) => layer.active); | ||
| const skillName = wizardId.slice("commands/".length).replace(/\./g, "-"); | ||
| if (activeLayer && skillName) { | ||
| activeLayer.sourcePath = `.github/skills/${skillName}/SKILL.md`; |
There was a problem hiding this comment.
Fixed in 3ba4d51. The normalizer now preserves each layer’s authoritative CLI sourcePath, and the guessed active-command skill path has been removed. Added contract-shaped coverage for extension command, template, and script paths.
Co-authored-by: nicolehaugen <10600161+nicolehaugen@users.noreply.github.com>
There was a problem hiding this comment.
Review details
Files not reviewed (1)
- plugins/spec-kit-copilot-wizard/extensions/speckit-wizard-canvas/package-lock.json: Generated file
Suppressed comments (4)
Previously missed (2) — in code that hasn't changed since the last review.
plugins/spec-kit-copilot-wizard/extensions/speckit-wizard-canvas/catalog/shared.mjs:61
- On timeout, the captured stdout is necessarily incomplete but can still look like valid list output. The downstream parsers then mark only that prefix as installed, causing remaining installed presets/extensions/bundles to render as absent. Treat a timeout as a failed query instead of returning partial data.
const timer = setTimeout(() => {
try { child.kill(); } catch { /* best-effort */ }
done(stdout || null);
plugins/spec-kit-copilot-wizard/extensions/speckit-wizard-canvas/test/artifact-cli.test.mjs:6
- The replacement suite only covers the CLI mapper and fast path, while deleting
composition.test.mjsalso removes all tests for still-livecanonical.mjs,effective-phases.mjs,phase-runtime.resolvePipelineEntry, and hook parsing behavior. Preserve those regression tests in focused test files rather than deleting them with the retired assembler tests.
import {
buildCompositionFromCli,
} from "../composition/artifact-cli.mjs";
import { computePipelineFastPath } from "../composition/pipeline-fast-path.mjs";
plugins/spec-kit-copilot-wizard/extensions/speckit-wizard-canvas/canvas-runtime/composition-apply.mjs:307
- This preserves the deprecated
stage2NeededAPI even though this flow now exposespipelineFastPath;dispatch.mjsconsequently still uses the old Stage 2 terminology. The developer resolution on the prior thread explicitly requested replacing all Stage 2 references, so returnpipelineFastPathonly and update the dispatch condition to check it directly.
return {
ok: true,
reason,
pipelineFastPath: fastPath.canSynthesize,
stage2Needed: !fastPath.canSynthesize,
};
plugins/spec-kit-copilot-wizard/extensions/speckit-wizard-canvas/package-lock.json:23
- The lockfile now pins this public dependency to an Azure Artifacts mirror instead of the npm registry and downgrades integrity metadata from SHA-512 to SHA-1. Consumers will be forced through that mirror regardless of their configured registry, reducing portability and verification strength. Regenerate the lockfile against
registry.npmjs.org.
- Files reviewed: 19/20 changed files
- Comments generated: 1
- Review effort level: Balanced
| const owns = artifact.stack.some( | ||
| (l) => l.layer === "extension" && l.presetId === extensionId, | ||
| ); |
Warning
Blocked by github/spec-kit#4305.
Do not merge until #4305 lands and a
specify-clirelease containingspecify artifact list --jsonis published to PyPI. Once that ships, bumpthe version floor note in
skills/speckit-cli-setup/SKILL.mdand un-draft.Replaces #17 — same commits, but branched directly in
github/spec-kit-copilotoffmaininstead of coming from a fork.Summary
Move the wizard's Composition tab off direct filesystem inspection and onto a
single
specify artifact list --jsoncall. The CLI returns one row perartifact carrying the full composition
stack: [...], which is everything thewizard needs to render commands, templates, scripts, and hooks.
Depends on
specify artifactcommand spec-kit#4305 — addsspecify artifact(list/info) with deterministiccontribution IDs and per-row stack. This PR does not work at runtime
without it.
What changes
composition/artifact-cli.mjs(new sole source of composition): callsspecify artifact list --jsononce at boot, maps rows into the wizard'sartifact / preset / extension shapes, and produces the composition summary.
project-scanner.mjs: no longer reads.specify/{presets,extensions}.jsonfor composition. Starts empty and lets
overlayCachedCompositionapply theCLI-derived data after the scan.
composition/pipeline-fast-path.mjs: decides between the deterministicpipeline (canonical spine +
replace-only overrides) and the LLM pipeline(
prompts/composition.mjs::inferPipeline) needed when an extension adds anon-canonical command or uses
wrap/prepend/append.tracker steps so the user sees which phase is running.
test/artifact-cli.integration.test.mjs)and its fixture — that layer belongs in the spec-kit repo alongside the CLI
it exercises. Unit tests here inject a fake runner so CI is green regardless
of which
specify-cliversion is installed.Verification
207/207 unit tests pass (unit tests inject a fake
specifyrunner).Playwright DOM-diff matrix — captured the wizard side panel from a main-branch
plugin variant vs. this branch across five scenarios:
copilot-sub-agentspresetpirate-full-presetagent-contextextension onlyEach scenario snapshots five surfaces (Composition → Commands / Templates /
Scripts / Hooks, plus top-level Phases). 25/25 pairs are byte-identical
after normalizing internal
[ref=...]handles. No user-visible regressions.Runtime behavior when the CLI is too old
Right now the wizard will surface an error and empty composition if the
installed
specify-clilacksartifact list --json. That's acceptable whilethis PR is draft; when we un-draft, the floor version bump in
speckit-cli-setupguarantees users get a compatible CLI.