You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
feat(bundles): allow bundles to reference other bundles - #5067
Implements #5047: a bundle.yml can reference an exact version of another bundle via provides.bundles. References are expanded into the parent's component plan before installation (manifest expansion; no child-bundle records), reusing existing catalog precedence, release verification, install policy, conflict checks and primitive installers.
Validation, bundle info, install, update and refresh resolve the same expanded plan.
bundle info shows referenced bundles, versions and the components they introduce.
Cycles, duplicate direct references, incompatible component requirements and incompatible Spec Kit version constraints are rejected with actionable errors; compatible shared components are installed once.
Updates follow the pins in the selected manifest and never advance a referenced bundle independently.
Manifests without bundle references behave as before.
Deviations from the issue text
Field name and expansion-only/install-time design follow the proposed resolutions that @mnriem approved. Beyond or different from the issue text:
Integration of referenced bundles warns instead of erroring. The issue asks for incompatible integration constraints to error. This is intentional: an installation has one project integration, and only the parent (root) bundle establishes or validates that boundary. Referenced bundles are expanded into the parent's component plan; they cannot select or switch the project's integration. A root integration mismatch—or a root pin when the project's integration cannot be determined without explicit confirmation—remains a hard error. A referenced bundle's differing or unknown integration is instead reported as a warning with its reference chain. In other words, references do not need to be independently installable for the selected project integration; their declared target remains visible to the user. The Spec Kit version requirement stays a hard error for every bundle in the graph.
Parent declaration wins over references. The issue says to reject incompatible requirements. Here, a component declared in the parent overrides the same component from any reference, with a warning naming the discarded value and its origin. Conflicts between two referenced bundles remain errors.
Added limit: at most 50 distinct referenced bundles per expansion (besides the depth limit of 10 and cycle detection).
Manifest schema: a non-empty provides.bundles requires schema_version: "1.1"; reference entries also accept an optional source (expected winning catalog name).
Testing
Tested locally with uv run specify --help
Ran existing tests with uv sync && uv run pytest
Tested with a sample project (if applicable)
.venv/bin/python -m pytest tests/specify_cli/bundles → 1104 passed. New positive/negative coverage in test_expansion.py, test_manifest.py, test_sources.py, test_command_info.py, test_command_validate.py, and install/update/conflict/installer tests.
AI Disclosure
I did use AI assistance
AI disclosure: Implemented with pi coding agent (model: claude-sonnet-5.5 via GitHub Copilot, reasoning effort: high) on behalf of @markuswondrak. AI generated the code, tests and docs; commit carries an Assisted-by: trailer.
Add `provides.bundles` so a bundle.yml can reference an exact version of
another bundle. References are expanded into the parent's component plan
before installation, using existing catalog precedence, install policy,
conflict checks and primitive installers. Validation, `bundle info`,
install, update and refresh resolve the same expanded plan.
Closesgithub#5047
Assisted-by: pi (model: claude-sonnet-5.5, autonomous)
Implements #5047 by allowing bundles to reference exact versions of other bundles, expanding their components into the existing installation and lifecycle flows without creating child-bundle records.
Changes:
Adds schema 1.1 references, recursive expansion, conflict detection, and provenance.
Connects expanded plans to validation, preview, installation, updates, and installed records.
Adds graph and lifecycle tests, plus documentation of resolution rules and limitations.
Validation was limited to targeted, dependency-free checks; the project test suite was not run.
AI disclosure: GitHub Copilot (model unknown; autonomous review mode) generated this review for the requester.
File
Description
tests/specify_cli/bundles/test_validator.py
Tests expanded-component validation.
tests/specify_cli/bundles/test_sources.py
Tests reference loading and catalog policies.
tests/specify_cli/bundles/test_resolver.py
Tests expanded plans and requirement reporting.
tests/specify_cli/bundles/test_records.py
Tests reference serialization and corrupt records.
tests/specify_cli/bundles/test_packager.py
Tests reference-only packaging and structural rejection.
Treat extension priority as an install-affecting compatibility setting, with sibling-conflict and parent-override coverage.
Preserve deduplicated override provenance from agreeing reference paths while retaining the shortest component origin.
Clarify offline catalog/local-root behavior, report an offline reference-resolution limitation instead of a misleading catalog miss, and make local bundle info preview unsupported explicitly.
Document that bundle validate does not evaluate the runtime Spec Kit version gate.
Validation: .venv/bin/python -m pytest tests/specify_cli/bundles -q → 1104 passed; ruff check and git diff --check passed. The sample-project test was reported by the contributor.
AI disclosure: Posted on behalf of @markuswondrak by pi coding agent (GitHub Copilot; model: gpt-5.6-terra; reasoning: high; autonomous). The agent implemented the changes, tests, documentation, and this summary; the contributor directed the review dispositions and supplied the sample-project test result.
Remove the local-path guard from bundle info so a bare catalog ID is not
rejected when a same-named file or directory exists. Add a regression.
Assisted-by: GitHub Copilot CLI (model: Claude Sonnet 5.5, autonomous)
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Identify an override by the discarded component and its declaring bundle
and keep the shortest chain, so shared graphs no longer grow override
histories exponentially. Applies to merged and inherited histories.
Assisted-by: GitHub Copilot CLI (model: Claude Sonnet 5.5, autonomous)
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
bundle info: removed the local-path guard; a bare catalog ID is no longer rejected when a same-named path exists (9d538b7). Regression added for info and info --versions.
Overrides: records are now keyed by the discarded component and its declaring bundle, keeping the shortest chain, for both merged and inherited histories (5d7501e). Layered-sharing regression (1/4/7 layers, both sibling orders) fails before and passes after; distinct declarations stay separate.
Extension priority: the installed-pin check compares priority for presets only, as before; reference expansion stays strict (bba1038). Installer-level tests cover matching, None vs 10, and differing priorities, with and without --refresh; source mismatch is still rejected.
AI disclosure: GitHub Copilot CLI (model: Claude Sonnet 5.5, supervised step by step by the contributor) posted this on behalf of @markuswondrak; analysis, code, tests and this summary are AI-generated. Commits carry Assisted-by: trailers.
The reason will be displayed to describe this comment to others. Learn more.
🔵 Needs a closer look
Recursive catalog resolution and ownership-sensitive lifecycle changes need final maintainer validation beyond the isolated verification performed here.
No open issues 🎉
1 previously missed issue
In code that hasn't changed since last review
Normalize omitted and default extension priorities before comparison
src/specify_cli/bundles/conflict.py:36
Compare effective extension priorities rather than their raw declarations. _ExtensionKindManager._do_install in primitives.py:424 maps an omitted priority to DEFAULT_PRIORITY (10), but this comparison treats None and 10 as different. Two sibling references declaring the same extension and version with omitted versus explicit default priority therefore fail expansion, although they install identically. A parent declaration also produces a spurious override warning. Normalize missing extension priorities before comparing, preserve preset comparisons and extension_priority=False, and add regression coverage for both reference orders and the parent-override case.
An omitted extension priority installs as DEFAULT_PRIORITY (10), but
component_setting_differences compared the raw None against 10. Sibling
references declaring the same extension with omitted vs. explicit default
priority failed expansion, and a parent declaration emitted a spurious
override warning. Preset comparison and extension_priority=False are
unchanged.
Assisted-by: GitHub Copilot CLI (model: Claude Sonnet 5.5, autonomous)
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Addressed the previously missed finding from review 5625099694 in 7462c92.
component_setting_differences now treats an omitted extension priority as DEFAULT_PRIORITY (10) before comparing, so sibling references with omitted vs. explicit default priority no longer fail expansion and a parent declaration no longer produces a spurious override warning. Preset comparison and extension_priority=False are unchanged. New tests cover both reference orders, the parent-override case, and negatives (non-default priority still conflicts; presets still compare raw). The new tests fail without the change; tests/specify_cli/bundles passes (1133).
No threads were resolved on the reviewer's behalf.
AI disclosure: this comment and the commit were authored by GitHub Copilot CLI (Claude Sonnet 5.5) on behalf of the PR author.
The reason will be displayed to describe this comment to others. Learn more.
🔵 Needs a closer look
Recursive catalog resolution and ownership-sensitive lifecycle changes need final human review and confirmation of lifecycle test results.
No open issues 🎉
1 previously missed issue
In code that hasn't changed since last review
Align bundle info settings comparison with effective component values
src/specify_cli/bundles/command_info.py:294
bundle info compares raw settings here, while expansion compares their effective, kind-specific values. For example, if a child declares extension style@1.0.0 without a priority and the parent overrides it with style@2.0.0, priority: 10, the preview reports priority=<unset> as overridden even though both priorities resolve to 10. It also reports extension strategy changes, although strategy only affects presets.
Match component_setting_differences: normalize omitted extension priorities and ignore settings that do not apply to the component kind. Preserve the existing comparison behavior for dictionaries without kind, and cover omitted/default priorities in both directions with regression tests.
bundle info compared raw priority/strategy values, so an omitted
extension priority showed as overridden by the default 10, and extension
strategy changes were reported though only presets use strategy.
Move the kind-specific rules into conflict.effective_setting and use it
from both component_setting_differences and bundle info. Provenance
without a kind keeps the raw comparison.
Assisted-by: GitHub Copilot CLI (model: Claude Sonnet 5.5, autonomous)
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Addressed the previously missed finding from review 5625522768 in 5570973.
The kind-specific setting rules now live in one place, conflict.effective_setting, which both component_setting_differences and bundle info use. In bundle info, an omitted extension priority equals the default (10), extension strategy is ignored, and presets still compare raw values. Provenance without kind keeps the raw comparison. New tests cover omitted/default priority in both directions, a non-default extension priority, extension strategy, presets, and agreement between bundle info and component_setting_differences; the behavioral ones fail without the change. tests/specify_cli/bundles passes (1153).
No threads were resolved on the reviewer's behalf.
AI disclosure: this comment and the commit were authored by GitHub Copilot CLI (Claude Sonnet 5.5) on behalf of the PR author.
A verified root could reference a community bundle with no indication in
`bundle info`. Carry the catalog entry's `verified` flag through
LoadedBundle and ResolvedReference, and show it per reference in both
the text label and JSON (`verified`, `trust`). Root trust fields stay
scoped to the root.
Assisted-by: GitHub Copilot CLI (model: Claude Sonnet 5.5, autonomous)
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
AI-generated comment (GitHub Copilot CLI, Claude Sonnet 5.5), posted on behalf of @markuswondrak.
Addressed review 5625791400 (trust status of referenced bundles) in the latest commit on this branch.
LoadedBundle and ResolvedReference now carry the catalog entry's verified flag, including when a shorter route replaces a cached reference.
bundle info JSON references[] entries gain verified and trust. The text label shows community or verified per reference. The root's trust fields are unchanged.
Docs updated in docs/reference/bundles.md.
Tests: a verified root with a verified: false reference, checked in both text and JSON. Without the source change it fails. The existing reference-provenance test now also covers a verified reference. Test fakes for catalog entries gained verified.
The reason will be displayed to describe this comment to others. Learn more.
🔵 Needs a closer look
Recursive expansion changes catalog-policy enforcement and shared-component lifecycle behavior, warranting final maintainer validation.
No open issues 🎉
1 previously missed issue
In code that hasn't changed since last review
Clarify that presets require an integer priority
docs/reference/bundles.md:156
An omitted preset priority is invalid: BundleManifest.structural_errors() rejects it, including in downloaded referenced manifests. Describing omission as a comparable setting suggests bundle authors can leave it out. State that presets must declare an integer priority; only extensions use the default value of 10.
Only extensions may omit priority (default 10). Drop the sentence that
described an omitted preset priority as a comparable setting.
Assisted-by: GitHub Copilot CLI (model: Claude Sonnet 5.5, autonomous)
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
AI-generated comment (GitHub Copilot CLI, Claude Sonnet 5.5), posted on behalf of @markuswondrak.
Addressed the wording finding from review 5628500172 (preset priority) in the latest commit. Docs only, no behavior change.
docs/reference/bundles.md now says extensions may omit priority (an omitted value means the default, 10, and agrees with an explicit 10) and presets must declare an integer priority. The previous sentence described an omitted preset priority as a comparable setting, which suggested authors could leave it out. structural_errors() rejects that.
The reason will be displayed to describe this comment to others. Learn more.
🔵 Needs a closer look
Recursive expansion spans catalog protections, provenance, and shared-component lifecycle behavior that warrants final human validation.
No open issues 🎉
1 previously missed issue
In code that hasn't changed since last review
Clarify component JSON overrides and source fields
docs/reference/bundles.md:58
Please clarify the component JSON fields here. overrides belongs to the winning entry, including a parent's own declaration that has no origin, as covered by test_info_renders_reference_provenance. Also, components[].source is included when set only for bundles with references; flat bundles still omit it. Document both conditions so consumers do not assume overrides requires origin or expect source on flat entries.
Document that component `source` appears only for bundles with
references, and that `overrides` does not require `origin`.
Assisted-by: GitHub Copilot CLI (model: Claude Sonnet 5.5, autonomous)
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
AI-generated comment (GitHub Copilot CLI, Claude Sonnet 5.5), posted on behalf of @markuswondrak.
Addressed the wording finding from review 5629015873 (component JSON fields) in the latest commit. Docs only, no behavior change.
docs/reference/bundles.md now states that, for bundles with references, a component entry includes source when the manifest declares one (flat bundles omit it), origin only when the component comes from a referenced bundle, and overrides whenever the entry replaced other declarations, including a parent's own declaration, which has no origin. It also lists the fields each overrides item carries. Checked against _manifest_component_view and _component_provenance in command_info.py.
The reason will be displayed to describe this comment to others. Learn more.
🔵 Needs a closer look
Recursive catalog resolution and shared-component lifecycle changes need final maintainer review and runtime validation.
No open issues 🎉
1 previously missed issue
In code that hasn't changed since last review
Preserve declaring bundles in override provenance
src/specify_cli/bundles/expansion.py:391
Merging agreeing entries here loses distinct declaring bundles before a later override can record them. For example, if root references a and b, both declare e@1.0.0, and root declares e@2.0.0, the final version is correct, but the override warning and bundle info override history name only a. The discarded declaration from b is missing. This differs from deduplicating two routes to the same shared declaration. Preserve each distinct declaring bundle until override provenance is built, keeping the shortest route per declaration, and add a regression case for agreeing siblings followed by a parent override.
Agreeing siblings were merged into one entry that kept a single origin,
so a later parent override named only one of them. Entries now carry a
record per distinct declaring bundle, merged by one shared rule
(_merge_declarations) that also governs override history, and one
function (_declare_own) builds an own-declaration entry and its notice.
Assisted-by: GitHub Copilot CLI (model: Claude Sonnet 5.5, autonomous)
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
AI-generated comment (GitHub Copilot CLI, Claude Sonnet 5.5), posted on behalf of @markuswondrak.
Addressed review 5629523384 (declaring bundles lost when agreeing siblings are merged) in the latest commit, by moving the rule into one place instead of patching another site.
Cause: "one declaration = one component + one declaring bundle, shortest route kept" was encoded only in _merge_overrides. The agreeing-copy merge in _merge_candidate kept a single origin, so a later parent override could only record a, not b.
Change: an _Entry now carries declarations, one record per distinct declaring bundle whose copy agrees. _merge_overrides became _merge_declarations and is the only merge, used for entry declarations and override history alike (order-independent tie-break). A new _declare_own builds the entry, the recorded overrides and the warning notice for a bundle's own declaration, and _OverrideNotice.replaced holds declarations. _prefix_* share one helper. Routes to the same declaring bundle still collapse, so the earlier exponential-growth fix holds.
Result: for root → a, b (both e@1.0.0) with root declaring e@2.0.0, the override history and the warning name both a and b.
Tests: agreeing siblings then parent override (both orders), order independence, nested routes (mid → a, b), and a negative case where two routes reach one declaring bundle and stay one record. The four tests for the dropped declarer fail without the change; the negative case guards the earlier fix and passes either way.
The reason will be displayed to describe this comment to others. Learn more.
🔵 Needs a closer look
Recursive catalog expansion affects compatibility and shared-component lifecycle guarantees across several commands and warrants final human validation.
Compare equivalent expanded requirements during reinstall checks
src/specify_cli/bundles/expansion.py:375
[P2] Compatible reference reordering can incorrectly require refresh. If A provides e@1.0.0 with no priority and B provides the same extension with priority 10, [A, B] keeps priority=None, while [B, A] keeps priority=10. Expansion treats these settings as equivalent, but the exact ComponentRef comparison in installer.py:123–129 rejects reinstalling the same-version parent after the reorder and demands --refresh. Compare equivalent expanded requirements semantically at the reinstall guard, while still requiring refresh for real changes. Add an install → reorder references → reinstall regression.
AI disclosure: GitHub Copilot generated this finding for the requesting reviewer (identity, model, and settings unavailable; automated mode).
…ases
Offline, a pin missing from the built-in snapshot may still resolve online,
so advise re-running without --offline instead of updating the reference.
The advice depends on the winning source. CatalogSource.is_builtin is now
the single definition of a built-in source, shared with the adapter dispatch.
Assisted-by: GitHub Copilot CLI (model: Claude Sonnet 5.5, autonomous)
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Offline, a pin missing from the builtin:// snapshot now advises "the built-in catalog may be outdated, so the pin may resolve online. Re-run without --offline." instead of "Update the reference".
The advice depends on the source that won the lookup, matched by source object, not by id. A project catalog named default keeps the "update the reference" advice.
CatalogSource.is_builtin is now the single definition of a built-in source. The adapter dispatch and the loader both use it.
Tests: new offline built-in, non-built-in (offline and online) and winning-source cases, plus parametrized is_builtin cases. The new loader test and the is_builtin tests fail without the fix.
Two existing tests moved from offline=True to offline=False, since they assert the non-built-in advice.
A separate subagent review (gpt-6-astra) found no significant issues.
The paragraph now covers both component and bundle references, but the warning rule applies only to component references. Online _validate() adds bundle-expansion failures to errors, including an unreachable bundle catalog or a failed child-manifest download, so validation exits 1. Qualify the warning rule and state that bundle-reference fetch failures fail online validation.
AI disclosure: GitHub Copilot generated this comment and suggested fix for the requester (identity and model unavailable; automated review mode).
Include bundle origin chains in inherited-component errors
Optional: When a referenced bundle contributes an unresolved component, validation reports the component label and checker error but not the bundle that introduced it. For example, the existing parent → child test reports child-extension without identifying child. This makes failures harder to trace, especially because local parents cannot be previewed with bundle info. Append the shortest chain from expanded.origins to inherited-component errors, while leaving errors for the parent's own declarations unchanged.
AI disclosure: GitHub Copilot generated this comment and suggested fix for the requester (identity and model unavailable; automated review mode).
Please address Copilot feedback, both open and previously missed. If not applicable, please explain why
This branch has not been deployed
No deployments
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Description
Implements #5047: a
bundle.ymlcan reference an exact version of another bundle viaprovides.bundles. References are expanded into the parent's component plan before installation (manifest expansion; no child-bundle records), reusing existing catalog precedence, release verification, install policy, conflict checks and primitive installers.bundle info, install, update and refresh resolve the same expanded plan.bundle infoshows referenced bundles, versions and the components they introduce.Deviations from the issue text
Field name and expansion-only/install-time design follow the proposed resolutions that @mnriem approved. Beyond or different from the issue text:
provides.bundlesrequiresschema_version: "1.1"; reference entries also accept an optionalsource(expected winning catalog name).Testing
uv run specify --helpuv sync && uv run pytest.venv/bin/python -m pytest tests/specify_cli/bundles→ 1104 passed. New positive/negative coverage intest_expansion.py,test_manifest.py,test_sources.py,test_command_info.py,test_command_validate.py, and install/update/conflict/installer tests.AI Disclosure
AI disclosure: Implemented with pi coding agent (model: claude-sonnet-5.5 via GitHub Copilot, reasoning effort: high) on behalf of @markuswondrak. AI generated the code, tests and docs; commit carries an
Assisted-by:trailer.Closes #5047