feat(wizard): consume native CLI hook artifacts and remove legacy hook parser - #28
nicolehaugen wants to merge 9 commits into
Conversation
There was a problem hiding this comment.
Copilot wasn't able to review any files in this pull request.
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
…tifacts Remove hooks.mjs and update artifact-cli.mjs to consume native kind: 'hook' artifacts from specify artifact list --json. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
This reverts commit 5e020ad.
There was a problem hiding this comment.
Copilot review overview
🟡 Changes recommended
Runtime normalization corrupts native hook targets and metadata, while the unrelated product rename conflicts with documented branding.
Once you've addressed the issues Copilot identified, you can request another Copilot review.
Review tier: Balanced
Findings: 1
New issues introduced by this change (3)
| Severity | Finding |
|---|---|
plugins/spec-kit-copilot-wizard/extensions/speckit-wizard-canvas/composition/artifact-cli.mjs — Keep hook IDs aligned with their target commands |
|
plugins/spec-kit-copilot-wizard/extensions/speckit-wizard-canvas/composition/artifact-cli.mjs — Preserve additive strategy through composition application |
|
plugins/spec-kit-copilot-wizard/extensions/speckit-wizard-canvas/composition/artifact-cli.mjs — Carry optionality onto inline hook attributions |
…guess normalizeHookArtifactsInComposition() derived a hook's own command from its id by stripping a 'commands/' prefix, but native hook ids are shaped as 'hooks/<event>:<target>' (cliIdToWizardId), so the prefix never matched and the comparison always failed. This forced every native hook through the commandByProvider fallback, which silently rewrote the hook's id/targetCommand to whichever command from that provider was indexed first -- wrong for any provider with 2+ commands. Use the artifact's own authoritative targetCommand field (set at shape time from the CLI) instead, falling back to the old id-derived guess only when that field is absent. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
… allowlist artifact-cli.mjs shapes native hook layers with strategy "additive" (hooks stack alongside a command rather than replace/wrap/prepend/ append it), and its own VALID_STRATEGIES set already includes it. But composition-apply.mjs's normalizeArtifact() kept a separate, stale allowlist without "additive", so runFastComposition() -> applyComposition() silently coerced every native hook layer to "replace" on the way into the cached/UI composition, contradicting what the CLI actually reported. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
…ntry applyNativeHookAttributions() builds a synthesized `hooks[]` entry on the parent command artifact for display on the phase card, but omitted `optional` even though shapeArtifact() already computed it (as `layer.optional`) for the standalone hook artifact's `hookBindings`. resolveHooksForCommand() processes the inline entry first and de-duplicates the standalone binding on the same key, so an optional hook's `optional: true` was silently dropped and it rendered as Required on the phase card, even though the Composition/Artifacts tab (which reads the standalone hookBindings directly) showed it correctly as Optional. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
There was a problem hiding this comment.
Copilot review overview
🔵 Needs a closer look
Native hook IDs and multi-contributor stacks are not handled correctly by several existing UI and attribution paths.
Review tier: Balanced
Findings: 1
Pre-existing issues (1)
| Severity | Finding |
|---|---|
plugins/spec-kit-copilot-wizard/extensions/speckit-wizard-canvas/composition/artifact-cli.mjs — Keep hook IDs aligned with their target commands View comment |
Issues resolved since last review (2)
| Severity | Finding |
|---|---|
plugins/spec-kit-copilot-wizard/extensions/speckit-wizard-canvas/composition/artifact-cli.mjs — Carry optionality onto inline hook attributions View resolved comment |
|
plugins/spec-kit-copilot-wizard/extensions/speckit-wizard-canvas/composition/artifact-cli.mjs — Preserve additive strategy through composition application View resolved comment |
Previously missed findings (3)
In code that hasn't changed since last review
plugins/spec-kit-copilot-wizard/extensions/speckit-wizard-canvas/composition/artifact-cli.mjs:124
- This creates one binding per stack layer, but
ui/composition.js:163-184treatshookBindings.lengthas an event count and applies that full weight to every layer. For a native row shared by two providers, each provider is consequently reported as contributing two hooks instead of one. Update the contribution calculation to associate/count each binding with its matching provider.
plugins/spec-kit-copilot-wizard/extensions/speckit-wizard-canvas/composition/artifact-cli.mjs:216 accumulateProvidesCountsnow computes hooks for both preset and extension layers, but this assignment remains inside the extension-only branch. A preset that contributes a native hook therefore omitsprovides.hooks, contrary to the stated extension/preset summary support. Populate the hook count for preset summaries as well.
plugins/spec-kit-copilot-wizard/extensions/speckit-wizard-canvas/composition/artifact-cli.mjs:234- Native hook stacks are additive, so multiple entries may be active at once. Selecting only the first active layer records just one provider in the parent command's
hooks[]; other enabled declarations for the same event/target disappear from phase attribution and lose their own optional/registration state. Emit one inline entry per stack layer and deriveregisteredfrom that layer'sactiveflag.
|
Re: the "previously missed finding" that native hook stacks with 2+ active layers only surface one contributor in the parent command's inline Why: a hook artifact only gets 2+ stack layers when two different extensions declare a hook with the exact same
None of the 6 hook artifacts currently produced by this repo have more than 1 stack layer, so this doesn't affect current behavior. Fixing it properly would touch both Happy to revisit if/when a real extension combination actually exercises this. |
- Note why preset hook counts are always zero (hooks are extension-only by design, per artifactOrigin()). - Note the additive-hook-stack limitation in applyNativeHookAttributions() where only the first active layer is captured for a command's inline hooks[]. - Note that the hookBindings-length weight used to tally per-provider and header hook counts in ui/composition.js is a local stand-in that should be replaced once hook counts are reported directly by the CLI. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
There was a problem hiding this comment.
Copilot review overview
🟡 Changes recommended
Native hook IDs remain incompatible with command-ID-based UI lookups, causing incorrect hook rendering and command behavior.
Once you've addressed the issues Copilot identified, you can request another Copilot review.
Review tier: Balanced
Findings: 1
New issues introduced by this change (2)
| Severity | Finding |
|---|---|
plugins/spec-kit-copilot-wizard/extensions/speckit-wizard-canvas/composition/artifact-cli.mjs — Key hook UI lookups by targetCommand |
|
plugins/spec-kit-copilot-wizard/extensions/speckit-wizard-canvas/composition/artifact-cli.mjs — Remove the stale legacy hook architecture note |
Issues resolved since last review (1)
| Severity | Finding |
|---|---|
plugins/spec-kit-copilot-wizard/extensions/speckit-wizard-canvas/composition/artifact-cli.mjs — Keep hook IDs aligned with their target commands View resolved comment |
| shaped.event = event; | ||
| shaped.targetCommand = targetCommand; | ||
| shaped.registered = cliArtifact.registered ?? true; |
| // Native CLI hook artifacts are consumed directly without manual extension | ||
| // manifest parsing. |
Hook artifacts' own id (hooks/<event>:<target>) no longer matches their target command's id (commands/<target>) after the native hook artifact migration. This broke two lookups: - composition-artifacts.js: hookByTargetId and the hook source-path/label resolution keyed off the hook's own id, so command rows never picked up their hook attribution chip and the hook target link could not resolve. - phase-runtime.js: hookBindingsByCommandId keyed off the hook's own id, so the command card never found its hook binding, and the 'items' filter fell back to rendering hook artifacts as their own standalone cards (duplicating the command with a malformed hooks/... id/label). Every hook artifact's targetCommand is guaranteed to resolve to an existing command artifact, so the 'hook with no command' fallback in phase-runtime.js is removed entirely rather than patched - items is now strictly commands, and hooks only ever surface via their command's binding. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>



Summary
This PR migrates the Spec Kit Wizard canvas extension off legacy manual YAML hook parsing (
hooks.mjs,.specify/extensions.yml, andextension.ymlmanifest scanning) and onto nativekind: "hook"artifacts supplied byspecify artifact list --json.Key Changes
Removed Legacy Hook Parser:
plugins/spec-kit-copilot-wizard/extensions/speckit-wizard-canvas/composition/hooks.mjs.js-yamlruntime dependency import from composition loader.Native CLI Hook Artifact Support:
shapeArtifactinartifact-cli.mjsto acceptkind: "hook".event,targetCommand,registered,hookBindings,hookBinding) for UI renderers (resolveHooksForCommand).accumulateProvidesCountsto count nativekind: "hook"artifacts towards extension/preset hook summaries (item.provides.hooks).applyNativeHookAttributionsto attach inlinehooks[]arrays onto parent phase command artifacts (commands/speckit.<phase>) directly in memory.Test Suite Updates:
artifact-cli.test.mjsto supply nativekind: "hook"artifacts in test fixtures instead of writing mock manifest files to disk.Stacked PR Context
nicolehaugen-lookupid-wizard-simplification-d12(PR Consume deterministic lookupId to simplify wizard provider resolution #21).