Repository navigation
[Feature]: Add deterministic contribution IDs and stack lookup IDs for resolved artifacts #4210
Description
Problem Statement
Spec Kit identifies contributed commands, templates, scripts, and hooks primarily by name. Names can collide across artifact kinds and source layers, and a name alone cannot reliably link a resolved artifact-stack layer to the exact manifest contribution that supplied it. Consumers such as preset info --json, extension info --json, and specify artifact need stable contribution identifiers and a join key that works across reinstalls and machines.
Proposed Solution
Introduce computed, opaque id fields for every command, template, script, and hook returned by public preset/extension manifest and info APIs, plus a lookupId field on every provenance-backed non-built-in resolved artifact-stack layer.
For named preset, extension, and project-override contributions, derive IDs using:
{layer}:{sourceId}:{kind}:{name}
Use project, preset, or extension for layer; _ only for the project-override source ID; and the preset or extension manifest ID for manifest-declared sources. Use command, template, or script for kind. Examples include preset:speckit.core:command:speckit.plan, extension:speckit.git:template:pr-body, and project:_:template:spec-template.
For hooks, use {eventName}:{command} as the name component. Hooks are valid only for preset and extension layers. If duplicate event/command hook entries are valid, add an appropriate stable discriminator or reject duplicates; do not use array position.
Built-in artifacts do not have an originating manifest contribution and therefore do not receive a contribution lookupId. Every artifact, including built-ins, instead has a source-agnostic public ID of the form {kind}:{name}. Built-in stack rows are recognized by absent provenance fields (layer, sourceId, and lookupId are null), and round-trip through the public artifact ID.
Compute IDs at read, serialization, or resolution time rather than persisting them in authored or installed manifests. For manifest-declared preset and extension layers, lookupId must exactly match the originating contribution's id. Preserve existing name-based behavior and document IDs as opaque strings.
Alternatives Considered
Install-time UUIDs are unsuitable because they differ across machines and reinstalls. Existing names are insufficient because they collide across sources and kinds. Content hashes are unsuitable because IDs would change whenever artifact content is edited. Array indexes are unsuitable for hooks because reordering entries would change their IDs.
A synthetic core:_:... contribution ID was considered for built-in assets. It was rejected because built-ins have no originating preset or extension manifest contribution to join to; the source-agnostic {kind}:{name} artifact ID provides their stable round-trip key without overloading lookupId.
Do not use the existing integration manifest as the ID source; it tracks installed file hashes and paths rather than manifest contribution identity.
Component
Specify CLI (initialization, commands)
AI Agent (if applicable)
No response
Use Cases
- A wizard can read an artifact stack and follow each provenance-backed layer's
lookupIdto the full contribution detail without re-parsing manifests. - Developers on different machines can use identical IDs when reporting or diagnosing a contribution.
- Tooling can hash or cache resolved compositions using public artifact IDs plus the available layer
lookupIdvalues. - Future JSON output for
preset info,extension info, andspecify artifactcan expose consistent cross-references.
Acceptance Criteria
- Document the
layer:sourceId:kind:namecontribution grammar, the source-agnostickind:nameartifact ID, and thelookupIdrelationship inextensions/EXTENSION-API-REFERENCE.mdand the preset API/manifest reference. - Public preset/extension manifest and info representations expose computed
idvalues for commands, templates, scripts, and hooks. - Hook IDs are deterministic and collision-free without install paths or list indexes.
- Manifest-declared preset and extension artifact-stack layers expose
lookupIdequal to the corresponding contribution'sid. - Project-local override layers use the synthetic
project:_:{kind}:{name}lookup form and intentionally have no manifest contribution match. - Built-in artifact-stack layers expose no contribution provenance (
layer,sourceId, andlookupIdare null) and round-trip through the public{kind}:{name}artifact ID. - Identical manifest coordinates produce identical IDs across processes, machines, project locations, and reinstalls.
- Manifest-backed IDs do not depend on artifact contents, timestamps, manifest hashes, archive paths, or installation directories.
- Existing
namefields and name-based resolution remain unchanged. - Tests cover each provenance-backed layer and artifact kind, hook uniqueness, resolver repeatability, manifest lookup round-trips, built-in public-ID round-trips, and backward compatibility.
Additional Context
This request defines the ID contract only. Adding the JSON output surfaces themselves for preset info --json, extension info --json, or specify artifact is out of scope and will be handled separately. Changing precedence, resolution, installation, or uninstall behavior is also out of scope. Relevant implementation areas include the extension manifest schema and validation and PresetResolver composition handling. IDs must not expose install paths, secrets, or connection strings.
Activity
github-actions commentedon Aug 19, 2026
Feature assessment — contribution-ids-lookup · Stage 1/5: Intake
Idea Intake: Deterministic Contribution IDs and Stack Lookup IDs
Idea (as captured)
Restated
The Spec Kit CLI needs stable, computed identifiers for every artifact contribution (commands, templates, scripts, hooks) and for every layer of a resolved artifact stack, so that consumers such as
preset info --json,extension info --json, and futurespecify artifacttooling can reliably cross-reference contributions without relying on names that collide across sources and kinds.Origin & Context
First-Glance Unknowns
preset info --json,extension info --json,specify artifact) are already implemented vs. still planned?]sourcestring format in resolved layers that would need migration?]Posted on behalf of @nicolehaugen by GitHub Copilot (model: claude-sonnet-4.6, autonomous)
github-actions commentedon Aug 19, 2026
Feature assessment — contribution-ids-lookup · Stage 2/5: Research
Idea Research: Deterministic Contribution IDs and Stack Lookup IDs
Users & Demand
PresetResolver.resolve()returns aPathonly, no source attribution;WorkflowResolver.resolve_with_layers()returnsLayerobjects wheresourceis a freeform string, not a structured ID)Prior Art
Layer.sourcefield (src/specify_cli/workflows/overlays/layer_sources.py): existing layers carry asourcestring andtierbut no structured contribution ID; this is the closest current analog — (cited: codebase)idfields:ExtensionManifest.idandPresetManifest.idalready provide stable slug-like identifiers ([a-z0-9-]+) that the proposed grammar reuses assourceId— (cited:src/specify_cli/extensions/__init__.py,src/specify_cli/presets/__init__.py)lookupId,contribution_id,lookup_id— no matches)extensionId, NPM package identity, Cargo workspace crate IDs all use stable structured identifiers for provenance — (confidence: low, ASSUMPTION)Market & Context
Layer.source(freeform string), install paths, or by re-parsing manifest YAML — fragile across project locationsData & Constraints
namefields and name-based resolution must remain unchanged — (cited: issue acceptance criterion adding executable permission to the scripts so they execute when the … #7)preset info --json,extension info --json,specify artifactare explicitly deferred — (cited: issue "Additional Context")ExtensionManifest,PresetManifestschema/validation,PresetResolvercomposition — (cited: issue body)Evidence Against the Idea
id/lookupIdare explicitly deferred; risk of over- or under-specifying the schema before consumers exist — (confidence: medium, cited: issue "Additional Context")Gaps & Open Questions
preset info --json,extension info --json, andspecify artifactalready partially implemented or only planned?]PresetManifest,ExtensionManifest, orLayerthat would need migration guidance?]Sources
src/specify_cli/presets/__init__.py,src/specify_cli/extensions/__init__.py,src/specify_cli/workflows/overlays/layer_sources.py(policy: allowlisted — local repo)Posted on behalf of @nicolehaugen by GitHub Copilot (model: claude-sonnet-4.6, autonomous)
github-actions commentedon Aug 19, 2026
Feature assessment — contribution-ids-lookup · Stage 3/5: Problem
Problem Definition: Deterministic Contribution IDs and Stack Lookup IDs
Problem Statement
When Spec Kit resolves an artifact stack (commands, templates, scripts, hooks contributed by core, presets, and extensions), it returns layers identified only by freeform source strings and file paths — which differ across machines, project locations, and reinstalls — making it impossible for consumers to reliably cross-reference a resolved layer back to the exact manifest contribution that supplied it.
Affected Users & Stakeholders
Goals
id(grammar:{layer}:{sourceId}:{kind}:{name})lookupIdmatching the contributing manifest entry'sidEXTENSION-API-REFERENCE.mdand preset API/manifest referenceNon-Goals
id/lookupIdthroughpreset info --json,extension info --json, orspecify artifactJSON surfaces (deferred — handled separately)namefields or name-based resolutionSuccess Metrics
idfor commands, templates, scripts, and hooks (baseline: 0 computedidfields today)lookupIdequal to the corresponding contribution'sid(baseline:lookupIdfield does not exist)sourcestrings vary by install path)Cost of Inaction
Without this, any tooling that needs to join a resolved stack layer back to its manifest contribution must resort to path-string heuristics that break across machines and reinstalls. As
preset info --jsonand related surfaces are built out, each consumer must independently re-implement fragile origin reconstruction instead of using a stable, tested contract.Open Questions
preset info --json,extension info --json, andspecify artifactalready partially implemented or only planned?]PresetManifest,ExtensionManifest, orLayerthat need migration guidance?]Posted on behalf of @nicolehaugen by GitHub Copilot (model: claude-sonnet-4.6, autonomous)
github-actions commentedon Aug 19, 2026
Feature assessment — contribution-ids-lookup · Stage 4/5: Concept
Concept: Deterministic Contribution IDs and Stack Lookup IDs
Options
Option A — Computed ID properties on existing manifest and layer objects
idproperty toPresetManifest,ExtensionManifest, and the core contribution model. For each command, template, script, and hook entry read from a manifest, the ID is computed on-the-fly using{layer}:{sourceId}:{kind}:{name}(e.g.,extension:speckit.git:template:pr-body). Add alookupIdfield to resolvedLayer/stack-layer objects set during resolution to the matching contribution'sid. Document the grammar as opaque inEXTENSION-API-REFERENCE.md. Hook IDs use{eventName}:{command}as the name component; duplicate event+command hook entries are rejected at validation.idfields. Requires touchingPresetManifest,ExtensionManifest,PresetResolver, the workflow overlay layer model, and comprehensive test suites.Layerdataclass is shared across overlay and preset contexts; verifying no callsite depends onsource's string shape needs investigation.Option B — Central contribution-ID registry computed at install time
.specify/at install time; resolution looks up the cache.Option C — Standalone ID-computation utility, no class changes
contribution_id(layer, source_id, kind, name)utility function; callers opt in.idvalues — leaves consistency to callers.Recommendation
Option A. It is the only option satisfying all acceptance criteria without contradicting the issue's constraints. Option B is explicitly ruled out; Option C does not meet the public-API requirement.
The hook deduplication question must be resolved before implementation — recommend rejection at validation as the simpler, safer default.
Out of Scope (for the recommended option)
id/lookupIdthroughpreset info --json,extension info --json, orspecify artifactCLI outputs (deferred per issue)Assumptions to Validate
Layerdataclass can receive alookupIdfield without breaking existing callsitesLayer.sourcein a way thatlookupIdwould need to replace{layer}:{sourceId}:{kind}:{name}grammar is collision-free given[a-z0-9-]+constraints onsourceIdand[a-z0-9.-]+onnamePosted on behalf of @nicolehaugen by GitHub Copilot (model: claude-sonnet-4.6, autonomous)
github-actions commentedon Aug 19, 2026
Feature assessment — contribution-ids-lookup · Stage 5/5: Decision — verdict needs-clarification
Decision: Deterministic Contribution IDs and Stack Lookup IDs
Scorecard
PresetResolver.resolve()returns a path with no structured source attribution;Layer.sourceis a freeform string that varies by install path. Cross-referencing stack layers to manifest contributions is genuinely impossible today without heuristics.idfields onExtensionManifest/PresetManifestprovide thesourceIdbuilding block. Implementation areas are named in the issue.Layerdataclass callsite impact needs verification. Tractable, but must be resolved before a go.Verdict & Rationale
needs-clarification. The problem is valid and the concept is well-shaped (Option A is clearly the right approach). However, evidence strength is rated weak: there is no external demand signal, and the primary consumers of
id/lookupId(preset info --json,extension info --json,specify artifact) are explicitly deferred and may not yet exist. The decide command's rules require evidence strengthadequate+ for ago— thin evidence producesneeds-clarification, notgo.Two implementation-blocking questions are also unresolved: the status of the consumer surfaces, and the hook deduplication policy. Once those are answered and an active consumer is confirmed, this idea has all the properties of a
go.If needs-clarification
preset info --json,extension info --json, andspecify artifactalready partially implemented or only planned? An active consumer in development substantially strengthens the urgency for this contract.]PresetManifest,ExtensionManifest, orLayerthat would need migration guidance whenid/lookupIdfields are added?]Posted on behalf of @nicolehaugen by GitHub Copilot (model: claude-sonnet-4.6, autonomous)
nicolehaugen commentedon Aug 19, 2026
[NEEDS CLARIFICATION: Are preset info --json, extension info --json, and specify artifact already partially implemented or only planned? An active consumer in development substantially strengthens the urgency for this contract.] => The consumer is this Canvas app - currently, it is using it's own logic to obtain composition information on presets/extensions, but the goal is to remove this custom logic and get it directly from SpecKit:
https://github.com/github/spec-kit-copilot/tree/main/plugins/spec-kit-copilot-wizard => also see this issue: #4211
[NEEDS CLARIFICATION: Should duplicate hook entries (same event+command) be rejected at validation time, or should a stable discriminator be appended? Must be decided before implementation to avoid a later breaking schema change.] => Use descriminator.
[NEEDS CLARIFICATION: Are there external Python consumers of PresetManifest, ExtensionManifest, or Layer that would need migration guidance when id/lookupId fields are added?] =>My assumption is no.
github-actions commentedon Aug 19, 2026
Feature assessment — contribution-ids-lookup · Stage 1/5: Intake
Idea Intake: Deterministic Contribution IDs and Stack Lookup IDs
Idea (as captured)
Restated
The request proposes adding computed, stable
idfields (format:{layer}:{sourceId}:{kind}:{name}) to every command, template, script, and hook in Specify CLI's public manifest/info APIs, along with a matchinglookupIdon resolved artifact-stack layers, so that consumers can reliably join stack layers back to their contributing manifest entries across machines and reinstalls.Origin & Context
feature-assesson 2026-08-19; the author observed that name-only identification causes collisions across artifact kinds and source layers, making reliable cross-referencing impossible for consumers ofpreset info --json,extension info --json, andspecify artifact.First-Glance Unknowns
preset info --json/extension info --jsonthat would need to be updated simultaneously, or is this additive?]idfield actually surface today?]Posted on behalf of @nicolehaugen by GitHub Copilot (model: claude-sonnet-4.6, autonomous)
github-actions commentedon Aug 19, 2026
Feature assessment — contribution-ids-lookup · Stage 2/5: Research
Idea Research: Deterministic Contribution IDs and Stack Lookup IDs
Users & Demand
PresetResolver.collect_all_layers()returns layers as raw dicts withpath,source, andstrategykeys; noidorlookupIdfield exists — [source:src/specify_cli/presets/__init__.pylines 5460–5490] (confidence: high, cited from code)sourcefield already encodes partial origin info (e.g."extension:speckit.git v1.2.3","preset:speckit.core","core") but as an unstructured string, not a stable opaque key — [source:src/specify_cli/presets/__init__.pylines 1773, 5448–5458] (confidence: high, cited from code)Prior Art
manifest.id,manifest.commands, etc. — all accessed by name, no computedidon individual contributions — [source:src/specify_cli/extensions/__init__.pylines 668–724;EXTENSION-API-REFERENCE.md] (confidence: high, cited)pack["id"]at preset level but provide no per-command/template/hook contribution ID — [source:src/specify_cli/presets/__init__.pyline 490] (confidence: high, cited).specify/for a contribution ID system — [source: file system search] (confidence: high){registry}:{package}#{artifact}for lockfile references; this proposal follows a similar four-part grammar — [ASSUMPTION, no external citation; consistent with well-known pattern]Market & Context
sourcefield. This is already observable in_reconcile_composed_commands()which parsessource.startswith("extension:")— [source:src/specify_cli/presets/__init__.pylines 1776, 1898] (confidence: high, cited)Data & Constraints
preset info --json,extension info --json, orspecify artifactis out of scope — [source: issue [Feature]: Add deterministic contribution IDs and stack lookup IDs for resolved artifacts #4210, Additional Context]{eventName}:{command}pair, the proposed grammar produces colliding IDs — [source: issue [Feature]: Add deterministic contribution IDs and stack lookup IDs for resolved artifacts #4210;EXTENSION-API-REFERENCE.md] (confidence: high){layer}:{sourceId}:{kind}:{name}— four segments, no hashing, fully deterministic from manifest coordinates — [source: issue [Feature]: Add deterministic contribution IDs and stack lookup IDs for resolved artifacts #4210] (confidence: high)preset infoandextension infodo not currently emit--json— no--jsonflag on those commands today — [source:src/specify_cli/presets/_commands.pyline 462;extensions/_commands.pyline 1338] (confidence: medium)Evidence Against the Idea
sourcefield already partially captures whatidwould capture. Consumers tolerating an informal string could continue; no existing consumer is documented as broken today. The ID system adds formality but may be premature if JSON output surfaces are also deferred. (confidence: medium, ASSUMPTION)preset info --jsonorextension info --json, the only initial consumers are internal callers ofcollect_all_layers(). Benefit is real but incremental. (confidence: medium, cited from issue)Gaps & Open Questions
{eventName}:{command}hook entries be handled — reject duplicates or add a stable discriminator?]collect_all_layers()need alookupIdkey in each returned layer dict, or only at a higher API boundary?]Sources
src/specify_cli/presets/__init__.py(local codebase)src/specify_cli/extensions/__init__.py(local codebase)extensions/EXTENSION-API-REFERENCE.md(local codebase)Posted on behalf of @nicolehaugen by GitHub Copilot (model: claude-sonnet-4.6, autonomous)
github-actions commentedon Aug 19, 2026
Feature assessment — contribution-ids-lookup · Stage 3/5: Problem
Problem Definition: Deterministic Contribution IDs and Stack Lookup IDs
Problem Statement
Specify CLI's resolved artifact stack identifies contributions only by name and an unstructured
sourcestring, making it impossible for consumers (tooling, wizards, diagnostics) to reliably join a resolved stack layer back to the exact manifest contribution that supplied it — especially when names collide across artifact kinds or source layers, or when the stack is read on a different machine or after reinstall.Affected Users & Stakeholders
Goals
idof the form{layer}:{sourceId}:{kind}:{name}at read/resolution timelookupIdthat exactly matches the originating contribution'sidlookupIdrelationship are documented inextensions/EXTENSION-API-REFERENCE.mdand the preset API/manifest referenceNon-Goals
preset info --json,extension info --json, orspecify artifact(deferred; out of scope)Success Metrics
lookupIdon a resolved layer equals the contribution'sid(baseline: not tested)Cost of Inaction
If no IDs are added, consumers must continue parsing the informal
sourcestring with brittle substring logic — an approach already evident in production code (source.startswith("extension:")). As extensions and presets grow, name collisions across kinds will surface as hard-to-diagnose bugs, and tooling built on the stack (wizards, caching, diffing) will remain unreliable.Open Questions
{eventName}:{command}hook entries be handled — reject at validation time, or add a stable discriminator?]lookupIdadded to the dict returned bycollect_all_layers(), or exposed only at a higher API boundary?]Posted on behalf of @nicolehaugen by GitHub Copilot (model: claude-sonnet-4.6, autonomous)
github-actions commentedon Aug 19, 2026
Feature assessment — contribution-ids-lookup · Stage 4/5: Concept
Concept: Deterministic Contribution IDs and Stack Lookup IDs
lookupIdon layer dictsOptions
Option A — Computed IDs at resolution time,
lookupIdon layer dictscompute_contribution_id(layer, source_id, kind, name)helper that assembles{layer}:{sourceId}:{kind}:{name}at read time. Hook this intoExtensionManifest.commands/.templates/.scripts/.hooksproperty getters (returning enriched dicts with anidfield), and intoPresetManifestsimilarly. Add alookupIdkey to each dict returned byPresetResolver.collect_all_layers(), derived by calling the same helper. Document the grammar inEXTENSION-API-REFERENCE.md. Reject hook entries that would collide on{eventName}:{command}.Option B — ID computation only at serialization (lazy/on-demand helper)
contribution_id(contribution_dict)helper callers invoke when needed; do not enrich internal layer dicts or manifest property dicts.lookupIdis not co-located with the layer dict; every consumer must know to call the helper. Does not satisfy the acceptance criteria thatlookupIdappears on resolved layers.Option C — Full typed model with
ContributionIddataclassResolvedLayerdataclass carryingpath,source,strategy,lookupId, and a typedContributionobject. All callers migrated.collect_all_layers()and manifest property getters must be updated.Recommendation
Option A. It directly satisfies all acceptance criteria, is backward-compatible, and is proportionate to the scope. Option B fails the criteria; Option C is disproportionate.
Key design decision to resolve before implementation: hook collision policy — recommend rejecting duplicate
{eventName}:{command}pairs at manifest validation time (thecoerce_hook_entries()function is the right seam), but requires auditing existing extensions first.Out of Scope (for the recommended option)
preset info --json,extension info --json,specify artifact)Assumptions to Validate
collect_all_layers()is the right seam to addlookupId— called by all composition pathssourcestrings on layer dicts contain enough information to derivelayer,sourceId, andkindwithout additional resolver statelookupIdto the returned dict is additive-safe for all callersPosted on behalf of @nicolehaugen by GitHub Copilot (model: claude-sonnet-4.6, autonomous)
github-actions commentedon Aug 19, 2026
Feature assessment — contribution-ids-lookup · Stage 5/5: Decision — verdict go
Decision: Deterministic Contribution IDs and Stack Lookup IDs
Scorecard
_reconcile_composed_commands()already parsessource.startswith("extension:")as a brittle workaround. Name collisions across kinds are a documented limitation.preset info --jsonandextension info --jsonsurfaces cited in the issue. Aligns with the CLI's trajectory toward richer machine-readable outputs.lookupId) are identified and credibly bounded. Hook collision policy must be decided before implementation; auditing known extensions for existing duplicates is a concrete pre-step.Verdict & Rationale
go. Problem validity is strong and grounded in codebase evidence. Evidence strength is adequate — direct code inspection confirms the structural gap even without user telemetry. A shaped concept exists (Option A), the appetite is credible, and the change is backward-compatible by design. Strategic fit is strong: this is the foundational contract that unblocks the JSON output surfaces explicitly scoped to follow-on work. The one open design question (hook collision policy) is bounded and resoluble during specification — it is not a blocking unknown for the go verdict.
Handoff to
/speckit-specifysourcestring, making reliable cross-referencing impossible for consumers across machines and reinstalls.compute_contribution_id(layer, source_id, kind, name)helper producing{layer}:{sourceId}:{kind}:{name}; enrichExtensionManifestandPresetManifestproperty getters to return contribution dicts with anidfield; add alookupIdkey to each dict returned byPresetResolver.collect_all_layers(); document the grammar inEXTENSION-API-REFERENCE.mdand the preset API/manifest reference; enforce hook-collision policy at validation.idon manifest contribution dicts (commands, templates, scripts, hooks);lookupIdon resolved layer dicts; grammar documentation; hook-collision policy; tests across all source layers and artifact kinds.preset info --json,extension info --json,specify artifact); resolution/precedence/installation/uninstall changes; persisting IDs in authored manifests.{eventName}:{command}duplicates before deciding.]lookupIdthe right seam atcollect_all_layers(), or only at a higher public boundary?]Posted on behalf of @nicolehaugen by GitHub Copilot (model: claude-sonnet-4.6, autonomous)