From a471e384037e1f99a7ebdbb66f04dc77939114bc Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 26 Aug 2026 20:00:55 +0000 Subject: [PATCH 1/3] Initial plan From 50ab618f46c25a4707a4cba9a4371cb4773350b6 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 26 Aug 2026 20:04:36 +0000 Subject: [PATCH 2/3] Fix composition fast-path naming Co-authored-by: nicolehaugen <10600161+nicolehaugen@users.noreply.github.com> --- .../canvas-runtime/composition-apply.mjs | 25 ++++--- .../canvas-runtime/dispatch.mjs | 11 +-- .../canvas-runtime/wizard-phases.mjs | 4 +- .../composition/assembler.mjs | 38 +++++----- .../pipeline/canonical.mjs | 8 +-- .../prompts/composition.mjs | 8 +-- .../test/composition.test.mjs | 70 +++++++++---------- 7 files changed, 82 insertions(+), 82 deletions(-) diff --git a/plugins/spec-kit-copilot-wizard/extensions/speckit-wizard-canvas/canvas-runtime/composition-apply.mjs b/plugins/spec-kit-copilot-wizard/extensions/speckit-wizard-canvas/canvas-runtime/composition-apply.mjs index e864739..f65c785 100644 --- a/plugins/spec-kit-copilot-wizard/extensions/speckit-wizard-canvas/canvas-runtime/composition-apply.mjs +++ b/plugins/spec-kit-copilot-wizard/extensions/speckit-wizard-canvas/canvas-runtime/composition-apply.mjs @@ -10,7 +10,7 @@ import { PHASE_BY_ID } from "./wizard-phases.mjs"; import { applyPatch, writeState, readState, validateInferredPipeline, activeFingerprint, normalizeExecutionReports } from "../state/store.mjs"; -import { assembleComposition, computeStage2Necessity } from "../composition/assembler.mjs"; +import { assembleComposition, computePipelineFastPath } from "../composition/assembler.mjs"; import { fsDeps } from "./instances.mjs"; import { snapshot } from "./snapshot.mjs"; @@ -280,13 +280,12 @@ export async function applyComposition(inst, input) { // milliseconds by reading manifests directly — the LLM Stage 1 turn is // retired entirely. // -// Trivial Stage 2 shortcut: `computeStage2Necessity` inspects the freshly -// assembled composition and decides whether the LLM Stage 2 pipeline -// inference is actually needed. When it isn't (no new commands, no -// wraps/prepends/appends directives), we synthesize `inferredPipeline` -// from the canonical spine here. When it IS needed, we skip pipeline +// Pipeline fast path: `computePipelineFastPath` inspects the freshly +// assembled composition and decides whether it can synthesize +// `inferredPipeline` from the canonical spine. When it cannot (new commands +// or wraps/prepends/appends directives), we skip pipeline // synthesis and the prior `inferredPipeline` carries forward until the -// user clicks Refresh Now on the Composition tab to invoke the LLM path. +// user clicks Refresh Now on the Composition tab to invoke LLM inference. // // Runs silently on catalog changes — failures degrade to a warn log and // leave the composition slice alone. @@ -298,18 +297,18 @@ export async function runFastComposition(inst, { reason } = {}) { presetItems: inst.cachedPresetItems ?? [], extensionItems: inst.cachedExtensionItems ?? [], }); - // `_presetManifests` is a side channel used only for Stage 2 - // necessity detection — never persisted. + // `_presetManifests` is a side channel used only for the pipeline + // fast-path decision — never persisted. const presetManifests = payload._presetManifests ?? []; delete payload._presetManifests; - const stage2 = computeStage2Necessity(payload, presetManifests); - if (!stage2.needed && stage2.syntheticPipeline) { - payload.inferredPipeline = stage2.syntheticPipeline; + const fastPath = computePipelineFastPath(payload, presetManifests); + if (fastPath.pipelineFastPath && fastPath.syntheticPipeline) { + payload.inferredPipeline = fastPath.syntheticPipeline; } await applyComposition(inst, payload); - return { ok: true, reason, stage2Needed: stage2.needed }; + return { ok: true, reason, pipelineFastPath: fastPath.pipelineFastPath }; } catch (err) { return { ok: false, reason: String(err?.message ?? err) }; } diff --git a/plugins/spec-kit-copilot-wizard/extensions/speckit-wizard-canvas/canvas-runtime/dispatch.mjs b/plugins/spec-kit-copilot-wizard/extensions/speckit-wizard-canvas/canvas-runtime/dispatch.mjs index 58c5afc..d0415be 100644 --- a/plugins/spec-kit-copilot-wizard/extensions/speckit-wizard-canvas/canvas-runtime/dispatch.mjs +++ b/plugins/spec-kit-copilot-wizard/extensions/speckit-wizard-canvas/canvas-runtime/dispatch.mjs @@ -85,22 +85,23 @@ export async function dispatchKindPrompt(inst, kind, payload) { // Special path for the "Refresh Now" button on the Composition tab. // Historically this dispatched an LLM prompt that only completed on the - // next agent turn — if the agent was busy (or Stage 2 wasn't actually - // needed), the button's aria-busy spinner never cleared because the + // next agent turn — if the agent was busy (or the pipeline fast path + // applied), the button's aria-busy spinner never cleared because the // "composition" SSE broadcast that clears it never arrived. // // The fast composition assembler broadcasts `type: "composition"` // synchronously via applyComposition, which is exactly what the UI // listens for. So we run it eagerly here — the button clears within // milliseconds regardless of agent state — and only fall through to - // the LLM Stage 2 prompt when the assembler reports Stage 2 is needed + // LLM pipeline inference when the assembler cannot use the fast path // (novel commands, wraps/prepends/appends directives, etc.). if (kind === "composition.refresh") { const fast = await runFastComposition(inst, { reason: "refresh-button" }); - if (fast?.ok && !fast.stage2Needed) { + if (fast?.ok && fast.pipelineFastPath) { return { kind, fastComposition: true }; } - // Stage 2 needed — fall through and dispatch the LLM prompt below. + // The pipeline fast path could not synthesize — fall through and + // dispatch the non-fast LLM inference prompt below. // The fast path still broadcast the presets/extensions/artifacts // slice, so the button already cleared; the LLM turn will restamp // `inferredPipeline` when it responds. diff --git a/plugins/spec-kit-copilot-wizard/extensions/speckit-wizard-canvas/canvas-runtime/wizard-phases.mjs b/plugins/spec-kit-copilot-wizard/extensions/speckit-wizard-canvas/canvas-runtime/wizard-phases.mjs index 60d0447..4b96ccb 100644 --- a/plugins/spec-kit-copilot-wizard/extensions/speckit-wizard-canvas/canvas-runtime/wizard-phases.mjs +++ b/plugins/spec-kit-copilot-wizard/extensions/speckit-wizard-canvas/canvas-runtime/wizard-phases.mjs @@ -144,10 +144,10 @@ export const SKILL_BY_KIND = Object.freeze({ // `speckit-X`. ...Object.fromEntries(CANONICAL_PHASES.map((id) => [id, `speckit-${id}`])), - // Composition Stage 2 (inferPipeline). Stage 1 (extract) is now handled + // LLM pipeline inference. Composition extraction is now handled // by the deterministic fast assembler (composition-assembler.mjs) which // runs on install/boot — no LLM turn, no ACTION_KINDS entry. The two - // remaining kinds both route to the same Stage-2-only prompt body; the + // remaining kinds both route to the same non-fast inference prompt; the // wizard emits `composition.refresh` from the "Refresh Now" button as a // stable name for backwards-compat with older sessions. "composition.refresh": null, diff --git a/plugins/spec-kit-copilot-wizard/extensions/speckit-wizard-canvas/composition/assembler.mjs b/plugins/spec-kit-copilot-wizard/extensions/speckit-wizard-canvas/composition/assembler.mjs index e9be2ff..8cbe3f0 100644 --- a/plugins/spec-kit-copilot-wizard/extensions/speckit-wizard-canvas/composition/assembler.mjs +++ b/plugins/spec-kit-copilot-wizard/extensions/speckit-wizard-canvas/composition/assembler.mjs @@ -22,16 +22,16 @@ // • Standalone hook artifacts (one per extension hook binding) + inline // hook attributions on the target phase command. // -// What this ALSO covers (trivial Stage 2 shortcut): -// • When `computeStage2Necessity(...)` returns `needed: false`, the fast -// path can synthesize `inferredPipeline` directly from the canonical +// What this ALSO covers (pipeline fast path): +// • When `computePipelineFastPath(...)` returns `pipelineFastPath: true`, +// the assembler can synthesize `inferredPipeline` directly from the canonical // spine intersected with the active command set. This is emitted with // `synthetic: true` so consumers can distinguish it from an LLM-inferred // pipeline. Skipping the LLM turn is safe when no active command lies // outside the canonical spine AND no preset uses `wraps:`/`prepends:`/ // `appends:` on a canonical. // -// What this does NOT cover (LLM Stage 2 — inferPipeline): +// What this does NOT cover (LLM pipeline inference): // • Pipelines that require README-driven ordering — new commands whose // placement can only be inferred from prose, mermaid flowcharts, or // stack directives. `runFastComposition` leaves `inferredPipeline` @@ -227,8 +227,8 @@ export async function assembleComposition({ workspaceRoot, presetItems, extensio // both a `kind: "command"` artifact AND a `kind: "hook"` artifact for // the same id would create two entries in comp.artifacts sharing an // id, which downstream `find()`-by-id lookups can't disambiguate, and - // would cause computeStage2Necessity to treat the hook as a novel - // command (forcing an unnecessary LLM Stage 2 turn). + // would cause computePipelineFastPath to treat the hook as a novel + // command (forcing unnecessary LLM pipeline inference). for (const manifest of extensionManifests) { const hookCommandNames = new Set( (manifest.hooks ?? []) @@ -388,7 +388,7 @@ export async function assembleComposition({ workspaceRoot, presetItems, extensio presets: presetsOut, extensions: extensionsOut, artifacts: [...artifacts.values()], - // Side channel for downstream `computeStage2Necessity`. NOT part of + // Side channel for downstream `computePipelineFastPath`. NOT part of // the persisted composition — callers must strip before writing. _presetManifests: presetManifests, }; @@ -397,7 +397,7 @@ export async function assembleComposition({ workspaceRoot, presetItems, extensio // Canonical spine — the ordered list of command IDs (with `commands/` prefix) // the wizard treats as the augmented-canonical default pipeline. Mirrors // `ui/pipeline-items.mjs canonicalSpine()` but scoped to seeded phases only -// (the pipeline order Stage 2 would emit). +// (the pipeline order LLM inference would emit). // Fully-qualified command artifact ids for the canonical spine (nine // seeded phases) — sourced from `ui/canonical.mjs` so this file never // drifts from the wizard's authoritative phase list. @@ -411,15 +411,15 @@ const CANONICAL_COMMAND_ID_SET = new Set( // `augmented-canonical` pipeline (mirrors `REQUIRED_CANONICAL_PHASES` // consumed by `state/store.mjs validateInferredPipeline`). If any of // these is absent from the active command set, the synthesized pipeline -// would fail validation — in that case we defer to LLM Stage 2 instead. +// would fail validation — in that case we defer to LLM inference instead. const REQUIRED_CANONICAL_PIPELINE_IDS = Object.freeze(requiredCanonicalPipelineIds()); /** - * Decide whether LLM Stage 2 (`composition.inferPipeline`) is needed to - * derive a correct pipeline for the given composition, or whether the fast - * path can synthesize one from the canonical spine. + * Decide whether the pipeline fast path can synthesize a correct pipeline + * from the canonical spine, or whether `composition.inferPipeline` must use + * the non-fast LLM approach. * - * Stage 2 is needed when either condition holds: + * The fast path cannot synthesize when either condition holds: * 1. `newCommands` is non-empty — some active command has no canonical * placement, so ordering it requires README/prose reasoning. * 2. `hasStackDirectives` is true — at least one preset entry uses @@ -436,9 +436,9 @@ const REQUIRED_CANONICAL_PIPELINE_IDS = Object.freeze(requiredCanonicalPipelineI * Optional array of preset manifests (from `readPresetManifest`) — needed * to detect stack directives at the entry level. `assembleComposition` * doesn't expose these, so `runFastComposition` passes them separately. - * @returns {{ needed: boolean, newCommands: string[], hasStackDirectives: boolean, syntheticPipeline: object | null }} + * @returns {{ pipelineFastPath: boolean, newCommands: string[], hasStackDirectives: boolean, syntheticPipeline: object | null }} */ -export function computeStage2Necessity(composition, presetManifests = []) { +export function computePipelineFastPath(composition, presetManifests = []) { const artifacts = Array.isArray(composition?.artifacts) ? composition.artifacts : []; // Active command IDs (commands only, hooks excluded). const activeCommands = new Set( @@ -480,17 +480,17 @@ export function computeStage2Necessity(composition, presetManifests = []) { if (hasStackDirectives) break; } - const needed = newCommands.length > 0 || hasStackDirectives; + const requiresLlmInference = newCommands.length > 0 || hasStackDirectives; // Extra safety: augmented-canonical pipelines must contain every // REQUIRED_CANONICAL. If the active command set is missing one (e.g. // a preset dropped `implement` entirely), the synthesized pipeline // would be rejected by validateInferredPipeline. Defer to LLM - // Stage 2 in that case — it can emit a `standalone` shape instead. + // inference in that case — it can emit a `standalone` shape instead. const missingRequiredCanonicals = REQUIRED_CANONICAL_PIPELINE_IDS.filter( (id) => !activeCommands.has(id), ); - const canSynthesize = !needed && missingRequiredCanonicals.length === 0; + const canSynthesize = !requiresLlmInference && missingRequiredCanonicals.length === 0; let syntheticPipeline = null; if (canSynthesize) { @@ -511,7 +511,7 @@ export function computeStage2Necessity(composition, presetManifests = []) { } return { - needed: needed || missingRequiredCanonicals.length > 0, + pipelineFastPath: canSynthesize, newCommands, hasStackDirectives, syntheticPipeline, diff --git a/plugins/spec-kit-copilot-wizard/extensions/speckit-wizard-canvas/pipeline/canonical.mjs b/plugins/spec-kit-copilot-wizard/extensions/speckit-wizard-canvas/pipeline/canonical.mjs index 6fc8de2..9653df8 100644 --- a/plugins/spec-kit-copilot-wizard/extensions/speckit-wizard-canvas/pipeline/canonical.mjs +++ b/plugins/spec-kit-copilot-wizard/extensions/speckit-wizard-canvas/pipeline/canonical.mjs @@ -320,8 +320,8 @@ const CANONICAL_SET = new Set(CANONICAL_KEYS); * Single source of truth. Consumed by: * • `state/store.mjs validateInferredPipeline` — rejects a pipeline * missing any of these. - * • `composition-assembler.mjs computeStage2Necessity` — falls back to - * LLM Stage 2 when any of these are absent from the active command set, + * • `composition-assembler.mjs computePipelineFastPath` — falls back to + * LLM inference when any of these are absent from the active command set, * since the fast path can't synthesize a valid pipeline without them. */ export const REQUIRED_CANONICAL_PHASES = Object.freeze(CANONICAL_KEYS.filter((k) => CANONICAL[k].required)); @@ -343,7 +343,7 @@ export function canonicalSpine() { * Return the canonical spine as fully-qualified command artifact ids — * `commands/speckit.` — in seeded-pipeline order. Fresh array on each * call. Used by the fast-path pipeline synthesizer and any other consumer - * that needs the artifact-id form (Stage 2 prompt, validators, etc.). + * that needs the artifact-id form (LLM inference prompt, validators, etc.). */ export function canonicalPipelineIds() { return CANONICAL_PHASES.map((name) => `commands/speckit.${name}`); @@ -352,7 +352,7 @@ export function canonicalPipelineIds() { /** * Return the REQUIRED canonical anchors as fully-qualified command * artifact ids. Fresh array on each call. Used by - * `validateInferredPipeline` and `computeStage2Necessity`. + * `validateInferredPipeline` and `computePipelineFastPath`. */ export function requiredCanonicalPipelineIds() { return REQUIRED_CANONICAL_PHASES.map((name) => `commands/speckit.${name}`); diff --git a/plugins/spec-kit-copilot-wizard/extensions/speckit-wizard-canvas/prompts/composition.mjs b/plugins/spec-kit-copilot-wizard/extensions/speckit-wizard-canvas/prompts/composition.mjs index 15ada89..59beeef 100644 --- a/plugins/spec-kit-copilot-wizard/extensions/speckit-wizard-canvas/prompts/composition.mjs +++ b/plugins/spec-kit-copilot-wizard/extensions/speckit-wizard-canvas/prompts/composition.mjs @@ -37,8 +37,8 @@ export function buildCompositionPrompt(kind, payload, context, { workspacePath, switch (kind) { case "composition.refresh": case "composition.inferPipeline": { - // Stage 2 (inferPipeline) only. Stage 1 — building - // { presets, extensions, artifacts } — is now handled entirely + // LLM pipeline inference only. Building + // { presets, extensions, artifacts } is now handled entirely // by the deterministic fast assembler (composition-assembler.mjs) // which runs on install/boot. This prompt is the LLM path for // pipeline shape inference, which still needs README fetching @@ -48,8 +48,8 @@ export function buildCompositionPrompt(kind, payload, context, { workspacePath, // canonical spine (no new commands, no wraps/prepends/appends), // `runFastComposition` stamps `inferredPipeline` directly and // this prompt is never triggered. It only runs when the user - // explicitly clicks Refresh Now AND `computeStage2Necessity` - // returned `needed: true`. + // explicitly clicks Refresh Now AND `computePipelineFastPath` + // returned `pipelineFastPath: false`. // // Reads the composition slice from state.json (populated by the // fast path) and emits ONLY `inferredPipeline` — partial-merge diff --git a/plugins/spec-kit-copilot-wizard/extensions/speckit-wizard-canvas/test/composition.test.mjs b/plugins/spec-kit-copilot-wizard/extensions/speckit-wizard-canvas/test/composition.test.mjs index 914da68..8b7680c 100644 --- a/plugins/spec-kit-copilot-wizard/extensions/speckit-wizard-canvas/test/composition.test.mjs +++ b/plugins/spec-kit-copilot-wizard/extensions/speckit-wizard-canvas/test/composition.test.mjs @@ -8,7 +8,7 @@ import { import { platform, tmpdir } from "node:os"; import { join } from "node:path"; import { describe, test } from "node:test"; -import { assembleComposition, computeStage2Necessity } from "../composition/assembler.mjs"; +import { assembleComposition, computePipelineFastPath } from "../composition/assembler.mjs"; import { IS_CASE_INSENSITIVE_FS, parseHookDeclarations, @@ -42,7 +42,7 @@ test("canonicalSpine returns a fresh mutable copy each call", () => { test("isCanonical rejects non-canonical, empty, and non-string values", () => { // Positive predicate (every canonical is accepted) is exercised via the - // S1×catalog and S2 integration tests. This test guards only the + // catalog and pipeline integration tests. This test guards only the // branches those don't cover: type/case rejection. assert.equal(isCanonical("outline"), false); assert.equal(isCanonical("Specify"), false, "must be case-sensitive"); @@ -325,7 +325,7 @@ test("parseProvidesEntries: explicit `strategy:` field beats the `replaces:` sho // Real-world case: `copilot-sub-agents` uses `replaces: X` + `strategy: prepend` // to mean "prepend before X". Without the explicit-field override, the // shorthand-based inferStrategy would silently coerce this to "replace" and - // computeStage2Necessity would miss the stack directive. + // computePipelineFastPath would miss the stack directive. const parsed = parseProvidesEntries({ templates: [ { type: "command", name: "speckit.specify", replaces: "speckit.specify", strategy: "prepend" }, @@ -443,7 +443,7 @@ describe("composition-assembler", () => { // `assembleComposition({ workspaceRoot, presetItems, extensionItems })` and // asserts against small snapshot objects (not full JSON dumps) — verify only // the fields that matter for the case, so unrelated churn doesn't cascade -// into test edits. `computeStage2Necessity` is exercised at the same time. +// into test edits. `computePipelineFastPath` is exercised at the same time. // // Delete alongside composition-assembler.mjs when the speckit CLI exposes // the composition model natively. @@ -588,16 +588,16 @@ test("core-only workspace: no presets/extensions, synthesized canonical pipeline assert.ok(findArtifact(comp, "commands/speckit.constitution")); assert.ok(findArtifact(comp, "commands/speckit.specify")); - const s2 = computeStage2Necessity(comp, comp._presetManifests); - assert.equal(s2.needed, false, "core-only should not need Stage 2"); - assert.deepEqual(s2.newCommands, []); - assert.equal(s2.hasStackDirectives, false); - assert.ok(s2.syntheticPipeline, "synthesized pipeline should be produced"); - assert.equal(s2.syntheticPipeline.shape, "augmented-canonical"); - assert.equal(s2.syntheticPipeline.synthetic, true); + const fastPath = computePipelineFastPath(comp, comp._presetManifests); + assert.equal(fastPath.pipelineFastPath, true, "core-only should use the pipeline fast path"); + assert.deepEqual(fastPath.newCommands, []); + assert.equal(fastPath.hasStackDirectives, false); + assert.ok(fastPath.syntheticPipeline, "synthesized pipeline should be produced"); + assert.equal(fastPath.syntheticPipeline.shape, "augmented-canonical"); + assert.equal(fastPath.syntheticPipeline.synthetic, true); // Canonical anchors present in synthesized order. - assert.ok(s2.syntheticPipeline.pipeline.includes("commands/speckit.constitution")); - assert.ok(s2.syntheticPipeline.pipeline.includes("commands/speckit.implement")); + assert.ok(fastPath.syntheticPipeline.pipeline.includes("commands/speckit.constitution")); + assert.ok(fastPath.syntheticPipeline.pipeline.includes("commands/speckit.implement")); } finally { rmSync(root, { recursive: true, force: true }); } @@ -639,16 +639,16 @@ test("preset that replaces a template: stack has preset (active, replace) above assert.equal(spec.stack.length, 1); assert.equal(spec.stack[0].layer, "core"); - // No new commands, no stack directives → no Stage 2 needed. - const s2 = computeStage2Necessity(comp, comp._presetManifests); - assert.equal(s2.needed, false); - assert.ok(s2.syntheticPipeline); + // No new commands or stack directives → deterministic synthesis. + const fastPath = computePipelineFastPath(comp, comp._presetManifests); + assert.equal(fastPath.pipelineFastPath, true); + assert.ok(fastPath.syntheticPipeline); } finally { rmSync(root, { recursive: true, force: true }); } }); -test("preset adding a novel command: Stage 2 becomes required", async () => { +test("preset adding a novel command falls back to LLM pipeline inference", async () => { const root = makeWorkspace(); try { writePreset(root, "with-review", { @@ -668,10 +668,10 @@ test("preset adding a novel command: Stage 2 becomes required", async () => { assert.equal(review.stack[0].presetId, "with-review"); assert.equal(review.stack[0].active, true); - const s2 = computeStage2Necessity(comp, comp._presetManifests); - assert.equal(s2.needed, true, "novel command requires Stage 2"); - assert.deepEqual(s2.newCommands, ["commands/speckit.review"]); - assert.equal(s2.syntheticPipeline, null); + const fastPath = computePipelineFastPath(comp, comp._presetManifests); + assert.equal(fastPath.pipelineFastPath, false, "novel command requires LLM inference"); + assert.deepEqual(fastPath.newCommands, ["commands/speckit.review"]); + assert.equal(fastPath.syntheticPipeline, null); } finally { rmSync(root, { recursive: true, force: true }); } @@ -725,7 +725,7 @@ test("extension adds command + hook binding: standalone hook artifact + inline a } }); -test("preset with a wraps: directive on a canonical command forces Stage 2", async () => { +test("preset with a wraps: directive falls back to LLM pipeline inference", async () => { const root = makeWorkspace(); try { writePreset(root, "wrapper", { @@ -740,19 +740,19 @@ test("preset with a wraps: directive on a canonical command forces Stage 2", asy presetItems: [presetItem("wrapper")], extensionItems: [], }); - const s2 = computeStage2Necessity(comp, comp._presetManifests); - assert.equal(s2.hasStackDirectives, true, "wraps: directive detected"); - assert.equal(s2.needed, true); - assert.equal(s2.syntheticPipeline, null); + const fastPath = computePipelineFastPath(comp, comp._presetManifests); + assert.equal(fastPath.hasStackDirectives, true, "wraps: directive detected"); + assert.equal(fastPath.pipelineFastPath, false); + assert.equal(fastPath.syntheticPipeline, null); } finally { rmSync(root, { recursive: true, force: true }); } }); -test("preset using `replaces: X` + explicit `strategy: prepend` — Stage 2 sees the prepend", async () => { +test("preset using `replaces: X` + explicit `strategy: prepend` disables the pipeline fast path", async () => { // Regression test for the `copilot-sub-agents` shape: shorthand // `replaces:` combined with an explicit `strategy: prepend` field means - // "prepend before X", NOT "replace X". `computeStage2Necessity` must + // "prepend before X", NOT "replace X". `computePipelineFastPath` must // honor the explicit strategy so `hasStackDirectives` is true. const root = makeWorkspace(); try { @@ -779,9 +779,9 @@ test("preset using `replaces: X` + explicit `strategy: prepend` — Stage 2 sees const presetLayer = spec.stack.find((l) => l.layer === "preset"); assert.equal(presetLayer.strategy, "prepend"); - const s2 = computeStage2Necessity(comp, comp._presetManifests); - assert.equal(s2.hasStackDirectives, true, "explicit strategy: prepend detected"); - assert.equal(s2.needed, true); + const fastPath = computePipelineFastPath(comp, comp._presetManifests); + assert.equal(fastPath.hasStackDirectives, true, "explicit strategy: prepend detected"); + assert.equal(fastPath.pipelineFastPath, false); } finally { rmSync(root, { recursive: true, force: true }); } @@ -802,15 +802,15 @@ test("hook artifact IDs are excluded from synthesized pipeline", async () => { presetItems: [], extensionItems: [extensionItem("audit")], }); - const s2 = computeStage2Necessity(comp, comp._presetManifests); + const fastPath = computePipelineFastPath(comp, comp._presetManifests); // audit.check is a hook target — should be excluded from newCommands // for pipeline placement purposes. But because it appears as an // extension-provided command entry, it also lives in `artifacts` as a // command kind. The important thing is the synthesized pipeline (if // any) doesn't include it. - if (s2.syntheticPipeline) { + if (fastPath.syntheticPipeline) { assert.ok( - !s2.syntheticPipeline.pipeline.includes("commands/audit.check"), + !fastPath.syntheticPipeline.pipeline.includes("commands/audit.check"), "hook target excluded from synthesized pipeline", ); } From bed7f249e30fffcdc0f869c5a1f58f7ca5be19ad Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 26 Aug 2026 20:05:55 +0000 Subject: [PATCH 3/3] Remove stale composition stage comments Co-authored-by: nicolehaugen <10600161+nicolehaugen@users.noreply.github.com> --- .../canvas-runtime/composition-apply.mjs | 4 ++-- .../speckit-wizard-canvas/composition/assembler.mjs | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/plugins/spec-kit-copilot-wizard/extensions/speckit-wizard-canvas/canvas-runtime/composition-apply.mjs b/plugins/spec-kit-copilot-wizard/extensions/speckit-wizard-canvas/canvas-runtime/composition-apply.mjs index f65c785..2711cfd 100644 --- a/plugins/spec-kit-copilot-wizard/extensions/speckit-wizard-canvas/canvas-runtime/composition-apply.mjs +++ b/plugins/spec-kit-copilot-wizard/extensions/speckit-wizard-canvas/canvas-runtime/composition-apply.mjs @@ -277,8 +277,8 @@ export async function applyComposition(inst, input) { // Purpose: after any catalog change (preset/extension install, remove, // swap, priority change) the composition needs to be rebuilt. This // helper rebuilds `{ presets, extensions, artifacts }` locally in -// milliseconds by reading manifests directly — the LLM Stage 1 turn is -// retired entirely. +// milliseconds by reading manifests directly — LLM composition extraction +// is retired entirely. // // Pipeline fast path: `computePipelineFastPath` inspects the freshly // assembled composition and decides whether it can synthesize diff --git a/plugins/spec-kit-copilot-wizard/extensions/speckit-wizard-canvas/composition/assembler.mjs b/plugins/spec-kit-copilot-wizard/extensions/speckit-wizard-canvas/composition/assembler.mjs index 8cbe3f0..63cd87f 100644 --- a/plugins/spec-kit-copilot-wizard/extensions/speckit-wizard-canvas/composition/assembler.mjs +++ b/plugins/spec-kit-copilot-wizard/extensions/speckit-wizard-canvas/composition/assembler.mjs @@ -13,7 +13,7 @@ // install (or any other catalog change) without waiting for the slow // two-stage refresh. // -// What this covers (Stage 1 — extract): +// What this covers (deterministic composition extraction): // • presets[] with per-kind provides counts // • extensions[] with per-kind provides + hook counts // • artifacts[] — union of core inventory + every preset/extension entry,