Skip to content

feat: add specify bundle command#3070

Open
mnriem wants to merge 47 commits into
github:mainfrom
mnriem:mnriem/feat-bundler-spec-dogfood
Open

feat: add specify bundle command#3070
mnriem wants to merge 47 commits into
github:mainfrom
mnriem:mnriem/feat-bundler-spec-dogfood

Conversation

@mnriem

@mnriem mnriem commented Jun 19, 2026

Copy link
Copy Markdown
Collaborator

Summary

Adds a specify bundle subcommand group that composes existing Spec Kit primitives — extensions, presets, workflows, and steps — into versioned, installable units ("bundles") for role-based project setup.

The command group follows a thin-CLI-over-services design (all logic lives in src/specify_cli/bundler/services/, the CLI layer just wires arguments and rendering), is offline-first, and ships with a catalog stack for discovery.

Commands

  • specify bundle search — discover bundles in the catalog (with a verified/community trust indicator)
  • specify bundle info <name> — inspect a bundle's full component set + trust level
  • specify bundle install <name> — install/compose a bundle into a project
  • specify bundle update <name> — re-resolve and refresh installed components
  • specify bundle remove <name> — cleanly uninstall
  • specify bundle validate / specify bundle build — author-side validation and packaging

Highlights: semantic-version resolution, conflict detection, reference/security-path checking, local-artifact installs, and a verified-vs-community trust badge surfaced across search and info.

Dogfooding — submitted in flight

This feature was built using Spec Kit on itself (specify init --integration copilot, then the full specify → clarify → plan → tasks → constitution → analyze → implement → converge loop). We're intentionally opening the PR with the generated scaffolding still present so the dogfooding is visible while the PR is in review.

To scrub before merge (generated — do not merge)

All generated dogfooding scaffolding is removed before merge:

  • .specify/** (runtime scripts, templates, workflows, integrations, extensions, feature.json, init-options.json, integration.json, extensions.yml) — except .specify/memory/constitution.md, see below
  • .github/agents/speckit.*, .github/prompts/speckit.*, .github/copilot-instructions.md
  • specs/001-spec-kit-bundler/** (spec, plan, tasks, research, data-model, contracts, quickstart)
  • The temporary .gitattributes / markdownlint exemptions that cover the above generated paths

Retained

  • .specify/memory/constitution.md — the one dogfooding artifact we carry forward, as the project constitution governing this and future Spec-Driven work.

What lands in the tool

src/specify_cli/bundler/**, src/specify_cli/commands/bundle/__init__.py, the bundler test suites (tests/contract, tests/unit, tests/integration, tests/bundler_helpers.py), examples/bundles/**, and docs (docs/reference/bundles.md, overview.md).

Testing

Full suite green; the bundler subset (tests/contract tests/unit tests/integration) passes locally on every push. Verified self-contained in a fresh worktree + venv.

mnriem and others added 17 commits June 19, 2026 08:09
Scaffold Spec Kit (--integration copilot) and run the full SDD workflow
against the `specify bundle` subcommand feature:

- spec.md (4 user stories, 31 FRs, 8 success criteria) + clarifications
- plan.md, research.md, data-model.md, contracts/, quickstart.md
- tasks.md (43 dependency-ordered tasks, organized by user story)
- Spec Kit Constitution v1.0.0 (code quality, testing, UX, performance,
  dependency/security principles) derived from deep codebase analysis
- plan Constitution Check + tasks grounded against the ratified principles

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Implements the Spec Kit Bundler as a `specify bundle ...` subcommand group
that calls existing primitive machinery in-process with zero new dependencies,
per the v1.0.0 constitution (Principles I-V).

Adds the `specify_cli.bundler` package (models, services, lib helpers) and the
`commands/bundle` Typer group wiring search, info, list, install, update,
remove, validate, build, init, and catalog list/add/remove (with --json and
--offline). Includes manifest/catalog schemas, version + integration-clash
gating, discovery-only refusal, idempotent install with atomic rollback,
non-collateral removal, and offline-first catalog resolution.

Ships an 82-test suite (contract/unit/integration), four sample role bundles
(product-manager, business-analyst, security-researcher, developer), README
"Bundles" docs, and an AGENTS.md pitfall on the test-venv gotcha. Marks
tasks T001-T043 complete and records follow-ups T044 (live in-process
primitive dispatch) and T045 (install from a local artifact path).

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
….venv

Add a "Running the full test suite" subsection under Automated checks covering
`uv pip install -e ".[test]"` + `.venv/bin/python -m pytest`, with the
shared/global editable-install contamination caveat that mirrors the AGENTS.md
pitfall.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…t install

Closes the two follow-ups left after the initial bundler landing.

T044 — DefaultPrimitiveInstaller now performs real installs through existing
machinery instead of raising "use the primitive command" errors:
- presets/extensions install via their reusable managers
  (install_from_directory / install_from_zip); bundled assets install fully
  offline, catalog assets are fetched only when the network is allowed.
- workflows/steps delegate to the existing `workflow add` / `workflow step add`
  command callables in-process (project root as cwd), avoiding any duplicated
  download/validation logic (Principle I).
- `--offline` is threaded through DefaultPrimitiveInstaller(allow_network=…) so
  network-only kinds refuse with an actionable message rather than silently
  reaching out.

T045 — `specify bundle install` now accepts a local path (a built .zip
artifact, a bundle directory, or a bundle.yml) and installs directly without
consulting the catalog stack; bundle-ids still resolve via the stack.

Adds 13 tests (routing, offline gating, local-source resolution, and an
end-to-end offline build → install → list → remove of the bundled
agent-context extension). Bundler suite: 95 passing; ruff clean. Marks T044
and T045 complete in tasks.md.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Ran the converge command: assessed the codebase against spec.md, plan.md,
tasks.md, and the v1.0.0 constitution. Appended 7 traceable gap-closure tasks
(T046–T052) as a new "Phase 8: Convergence" section. Append-only — no existing
tasks were modified and no application code was changed.

Findings: 1 CRITICAL (Constitution III — bundle group undocumented under
docs/reference/), 3 HIGH (FR-005/SC-007 validate references; FR-009/SC-002 info
expansion; FR-012 install-time init), 3 MEDIUM (FR-013 integration precedence;
FR-020 surface overlaps; FR-028 update refresh).

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Close the gaps the converge command found between the bundler spec/plan/
constitution and the code:

- T046: add docs/reference/bundles.md documenting the full `specify bundle`
  command group; link it from docs/reference/overview.md (Constitution III).
- T047: wire a reference checker into `bundle validate` (services/references.py);
  online runs fail and name unresolved component references, offline runs warn.
- T048: expand `bundle info` to enumerate the full component set (versions,
  preset priority/strategy) plus the bundle integration — info == install.
- T049/T050: `bundle install`/`bundle init` now scaffold an uninitialized
  project via the existing `specify init` machinery, choosing the integration by
  precedence (override → bundle-declared → Copilot + OS default script type).
- T051: surface foreseeable component overlaps during info and install.
- T052: `bundle update` refreshes already-installed components via a new
  refresh path in install_bundle, preserving primitive-level overrides.

Adds unit/contract/integration coverage (107 tests pass).

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Re-run of converge after Phase 8. The seven Phase 8 tasks are verified closed.
One residual partial gap remains: the `verified`/trust indicator (FR-010,
FR-027) is exposed only in `bundle info --json`, absent from `bundle search`
(the primary discovery surface) and `bundle info` text. Appended as a single
new task for implement to complete. Append-only; no code changed.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
`bundle search` (text + JSON) and `bundle info` (text + JSON) now expose each
catalog entry's verification/trust level (verified vs community), so users can
judge a bundle's trust before installing, per FR-010 / FR-027. Previously
`verified` was only present in `bundle info --json`.

Adds contract coverage; 108 tests pass.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Scaffold Spec Kit (--integration copilot) and run the full SDD workflow
against the `specify bundle` subcommand feature:

- spec.md (4 user stories, 31 FRs, 8 success criteria) + clarifications
- plan.md, research.md, data-model.md, contracts/, quickstart.md
- tasks.md (43 dependency-ordered tasks, organized by user story)
- Spec Kit Constitution v1.0.0 (code quality, testing, UX, performance,
  dependency/security principles) derived from deep codebase analysis
- plan Constitution Check + tasks grounded against the ratified principles

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Implements the Spec Kit Bundler as a `specify bundle ...` subcommand group
that calls existing primitive machinery in-process with zero new dependencies,
per the v1.0.0 constitution (Principles I-V).

Adds the `specify_cli.bundler` package (models, services, lib helpers) and the
`commands/bundle` Typer group wiring search, info, list, install, update,
remove, validate, build, init, and catalog list/add/remove (with --json and
--offline). Includes manifest/catalog schemas, version + integration-clash
gating, discovery-only refusal, idempotent install with atomic rollback,
non-collateral removal, and offline-first catalog resolution.

Ships an 82-test suite (contract/unit/integration), four sample role bundles
(product-manager, business-analyst, security-researcher, developer), README
"Bundles" docs, and an AGENTS.md pitfall on the test-venv gotcha. Marks
tasks T001-T043 complete and records follow-ups T044 (live in-process
primitive dispatch) and T045 (install from a local artifact path).

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
….venv

Add a "Running the full test suite" subsection under Automated checks covering
`uv pip install -e ".[test]"` + `.venv/bin/python -m pytest`, with the
shared/global editable-install contamination caveat that mirrors the AGENTS.md
pitfall.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…t install

Closes the two follow-ups left after the initial bundler landing.

T044 — DefaultPrimitiveInstaller now performs real installs through existing
machinery instead of raising "use the primitive command" errors:
- presets/extensions install via their reusable managers
  (install_from_directory / install_from_zip); bundled assets install fully
  offline, catalog assets are fetched only when the network is allowed.
- workflows/steps delegate to the existing `workflow add` / `workflow step add`
  command callables in-process (project root as cwd), avoiding any duplicated
  download/validation logic (Principle I).
- `--offline` is threaded through DefaultPrimitiveInstaller(allow_network=…) so
  network-only kinds refuse with an actionable message rather than silently
  reaching out.

T045 — `specify bundle install` now accepts a local path (a built .zip
artifact, a bundle directory, or a bundle.yml) and installs directly without
consulting the catalog stack; bundle-ids still resolve via the stack.

Adds 13 tests (routing, offline gating, local-source resolution, and an
end-to-end offline build → install → list → remove of the bundled
agent-context extension). Bundler suite: 95 passing; ruff clean. Marks T044
and T045 complete in tasks.md.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Ran the converge command: assessed the codebase against spec.md, plan.md,
tasks.md, and the v1.0.0 constitution. Appended 7 traceable gap-closure tasks
(T046–T052) as a new "Phase 8: Convergence" section. Append-only — no existing
tasks were modified and no application code was changed.

Findings: 1 CRITICAL (Constitution III — bundle group undocumented under
docs/reference/), 3 HIGH (FR-005/SC-007 validate references; FR-009/SC-002 info
expansion; FR-012 install-time init), 3 MEDIUM (FR-013 integration precedence;
FR-020 surface overlaps; FR-028 update refresh).

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Close the gaps the converge command found between the bundler spec/plan/
constitution and the code:

- T046: add docs/reference/bundles.md documenting the full `specify bundle`
  command group; link it from docs/reference/overview.md (Constitution III).
- T047: wire a reference checker into `bundle validate` (services/references.py);
  online runs fail and name unresolved component references, offline runs warn.
- T048: expand `bundle info` to enumerate the full component set (versions,
  preset priority/strategy) plus the bundle integration — info == install.
- T049/T050: `bundle install`/`bundle init` now scaffold an uninitialized
  project via the existing `specify init` machinery, choosing the integration by
  precedence (override → bundle-declared → Copilot + OS default script type).
- T051: surface foreseeable component overlaps during info and install.
- T052: `bundle update` refreshes already-installed components via a new
  refresh path in install_bundle, preserving primitive-level overrides.

Adds unit/contract/integration coverage (107 tests pass).

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Re-run of converge after Phase 8. The seven Phase 8 tasks are verified closed.
One residual partial gap remains: the `verified`/trust indicator (FR-010,
FR-027) is exposed only in `bundle info --json`, absent from `bundle search`
(the primary discovery surface) and `bundle info` text. Appended as a single
new task for implement to complete. Append-only; no code changed.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
`bundle search` (text + JSON) and `bundle info` (text + JSON) now expose each
catalog entry's verification/trust level (verified vs community), so users can
judge a bundle's trust before installing, per FR-010 / FR-027. Previously
`verified` was only present in `bundle info --json`.

Adds contract coverage; 108 tests pass.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot AI review requested due to automatic review settings June 19, 2026 14:30

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Adds a new Spec Kit Bundler subsystem and wires it into the specify CLI as specify bundle ..., enabling role-based project setup via versioned “bundle” artifacts that compose existing primitives (extensions/presets/steps/workflows). The PR also includes a substantial dogfooding trail (spec artifacts + generated runtime scaffolding) and a comprehensive bundler-focused test suite.

Changes:

  • Introduces src/specify_cli/bundler/** (models/services/lib) implementing manifest validation, catalog stacking, install planning, installation/removal with provenance records, and artifact packaging.
  • Adds extensive unit/contract/integration tests for offline behavior, security path confinement, and install/update/remove lifecycle.
  • Updates documentation and examples to describe/illustrate bundle authoring, discovery, and installation.
Show a summary per file
File Description
tests/unit/test_bundler_versioning.py Unit tests for version parsing/constraints
tests/unit/test_bundler_resolver.py Resolver tests (version gate, integration compatibility)
tests/unit/test_bundler_references.py Reference-checker tests (online vs offline behavior)
tests/unit/test_bundler_records.py Installed-bundle record persistence/removal tests
tests/unit/test_bundler_primitives.py Primitive installer routing/offline gating tests
tests/unit/test_bundler_packager.py Bundle artifact packaging tests
tests/unit/test_bundler_conflict.py Conflict detection tests (integration + overlaps)
tests/integration/test_bundler_security_paths.py Path traversal/symlink confinement security tests
tests/integration/test_bundler_offline.py Offline-first catalog resolution tests
tests/integration/test_bundler_local_install.py Local path/zip install tests + end-to-end offline install
tests/integration/test_bundler_install_flow.py Install/record/remove lifecycle integration tests
tests/integration/test_bundler_init_install.py Init-on-install + integration precedence tests
tests/integration/test_bundler_catalog_stack.py Catalog stack precedence/policy/search integration tests
tests/contract/test_manifest_schema.py Manifest “schema contract” tests via model validation
tests/contract/test_catalog_schema.py Catalog “schema contract” tests + default stack shape
tests/bundler_helpers.py Shared helpers/fakes for bundler tests
src/specify_cli/bundler/services/validator.py Manifest structural + reference validation service
src/specify_cli/bundler/services/resolver.py Manifest → ordered install plan resolver
src/specify_cli/bundler/services/references.py Offline-first component reference resolution
src/specify_cli/bundler/services/packager.py Build bundle zip artifacts from bundle directories
src/specify_cli/bundler/services/installer.py Apply install plans; rollback; provenance record write
src/specify_cli/bundler/services/conflict.py Cross-bundle conflict detection (integration + overlaps)
src/specify_cli/bundler/services/catalog_stack.py Multi-source catalog stack resolution + search
src/specify_cli/bundler/services/adapters.py Concrete fetch/install adapters (network + primitives)
src/specify_cli/bundler/services/init.py Bundler services package init
src/specify_cli/bundler/models/records.py Installed bundle record model + persistence helpers
src/specify_cli/bundler/models/manifest.py Bundle manifest parsing + structural validation
src/specify_cli/bundler/models/catalog.py Catalog source/entry models + stack merge logic
src/specify_cli/bundler/models/init.py Bundler models package init
src/specify_cli/bundler/lib/yamlio.py Confined YAML/JSON IO helpers
src/specify_cli/bundler/lib/versioning.py SemVer + constraint evaluation helpers
src/specify_cli/bundler/lib/project.py Project root detection + active integration lookup
src/specify_cli/bundler/lib/init.py Bundler lib package init
src/specify_cli/bundler/commands_impl/catalog_config.py Project catalog config persistence helpers
src/specify_cli/bundler/commands_impl/init.py Bundler commands-impl package init
src/specify_cli/bundler/init.py Bundler package + BundlerError
src/specify_cli/init.py Registers the new bundle command group on the root CLI
specs/001-spec-kit-bundler/research.md Dogfooding research artifact
specs/001-spec-kit-bundler/quickstart.md Dogfooding quickstart/validation artifact
specs/001-spec-kit-bundler/plan.md Dogfooding implementation plan artifact
specs/001-spec-kit-bundler/data-model.md Dogfooding data model artifact
specs/001-spec-kit-bundler/contracts/cli-commands.md Bundler CLI behavior contract doc
specs/001-spec-kit-bundler/contracts/bundle-manifest.schema.md Bundle manifest contract doc
specs/001-spec-kit-bundler/contracts/bundle-catalog.schema.md Bundle catalog contract doc
specs/001-spec-kit-bundler/checklists/requirements.md Dogfooding requirements checklist
README.md Adds “Bundles” overview + usage examples
examples/bundles/security-researcher/README.md Example bundle documentation
examples/bundles/security-researcher/bundle.yml Example manifest
examples/bundles/product-manager/README.md Example bundle documentation
examples/bundles/product-manager/bundle.yml Example manifest
examples/bundles/developer/README.md Example bundle documentation
examples/bundles/developer/bundle.yml Example manifest
examples/bundles/business-analyst/README.md Example bundle documentation
examples/bundles/business-analyst/bundle.yml Example manifest
docs/reference/overview.md Adds “Bundles” reference entry
docs/reference/bundles.md New Bundles reference documentation
CONTRIBUTING.md Adds guidance for running tests in a local venv reliably
AGENTS.md Markdown formatting fixes + adds test-env gotcha
.specify/workflows/workflow-registry.json Generated workflow registry (dogfooding/runtime)
.specify/workflows/speckit/workflow.yml Generated workflow definition (dogfooding/runtime)
.specify/templates/spec-template.md Generated template (dogfooding/runtime)
.specify/templates/plan-template.md Generated template (dogfooding/runtime)
.specify/templates/constitution-template.md Generated template (dogfooding/runtime)
.specify/templates/checklist-template.md Generated template (dogfooding/runtime)
.specify/scripts/bash/setup-tasks.sh Generated script (dogfooding/runtime)
.specify/scripts/bash/setup-plan.sh Generated script (dogfooding/runtime)
.specify/scripts/bash/check-prerequisites.sh Generated script (dogfooding/runtime)
.specify/integrations/speckit.manifest.json Generated integration manifest (dogfooding/runtime)
.specify/integrations/copilot.manifest.json Generated integration manifest (dogfooding/runtime)
.specify/integration.json Generated runtime integration state (dogfooding/runtime)
.specify/init-options.json Generated init options (dogfooding/runtime)
.specify/feature.json Generated feature pointer (dogfooding/runtime)
.specify/extensions/agent-context/scripts/bash/update-agent-context.sh Generated extension script (dogfooding/runtime)
.specify/extensions/agent-context/README.md Generated extension docs (dogfooding/runtime)
.specify/extensions/agent-context/extension.yml Generated extension manifest (dogfooding/runtime)
.specify/extensions/agent-context/commands/speckit.agent-context.update.md Generated extension command (dogfooding/runtime)
.specify/extensions/agent-context/agent-context-config.yml Generated extension config (dogfooding/runtime)
.specify/extensions/.registry Generated extensions registry (dogfooding/runtime)
.specify/extensions.yml Generated extensions config (dogfooding/runtime)
.github/prompts/speckit.taskstoissues.prompt.md Generated Copilot prompt scaffold (dogfooding/runtime)
.github/prompts/speckit.tasks.prompt.md Generated Copilot prompt scaffold (dogfooding/runtime)
.github/prompts/speckit.specify.prompt.md Generated Copilot prompt scaffold (dogfooding/runtime)
.github/prompts/speckit.plan.prompt.md Generated Copilot prompt scaffold (dogfooding/runtime)
.github/prompts/speckit.implement.prompt.md Generated Copilot prompt scaffold (dogfooding/runtime)
.github/prompts/speckit.constitution.prompt.md Generated Copilot prompt scaffold (dogfooding/runtime)
.github/prompts/speckit.clarify.prompt.md Generated Copilot prompt scaffold (dogfooding/runtime)
.github/prompts/speckit.checklist.prompt.md Generated Copilot prompt scaffold (dogfooding/runtime)
.github/prompts/speckit.analyze.prompt.md Generated Copilot prompt scaffold (dogfooding/runtime)
.github/prompts/speckit.agent-context.update.prompt.md Generated Copilot prompt scaffold (dogfooding/runtime)
.github/copilot-instructions.md Generated Copilot context pointer (dogfooding/runtime)
.github/agents/speckit.taskstoissues.agent.md Generated Copilot agent scaffold (dogfooding/runtime)
.github/agents/speckit.plan.agent.md Generated Copilot agent scaffold (dogfooding/runtime)
.github/agents/speckit.constitution.agent.md Generated Copilot agent scaffold (dogfooding/runtime)
.github/agents/speckit.agent-context.update.agent.md Generated Copilot agent scaffold (dogfooding/runtime)

Copilot's findings

Tip

Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

  • Files reviewed: 110/110 changed files
  • Comments generated: 8

Comment thread src/specify_cli/bundler/services/validator.py
Comment thread src/specify_cli/bundler/services/adapters.py Outdated
Comment thread src/specify_cli/bundler/services/adapters.py
Comment thread src/specify_cli/bundler/models/manifest.py Outdated
Comment thread src/specify_cli/bundler/services/adapters.py Outdated
Comment thread src/specify_cli/bundler/services/adapters.py Outdated
Comment thread tests/unit/test_bundler_packager.py
Comment thread .specify/integration.json
…errors, reproducible builds

Resolves automated review feedback on github#3070:

- validator: drop redundant string-quoting on ReferenceChecker's
  `str | None` return so the annotation evaluates as a real union under
  `from __future__ import annotations`.
- adapters: normalize Windows drive-letter paths (e.g. C:\...) to the
  local-file branch so offline file catalogs resolve on Windows.
- adapters: enforce HTTPS (HTTP only for localhost) and require a host on
  remote catalog URLs before any network call, mirroring
  specify_cli.catalogs URL validation (MITM/downgrade protection).
- adapters: pass `origin` to loads_json for local files and HTTP payloads
  so JSON parse errors name the real source instead of <string>.
- manifest: parse component `priority` defensively, raising an actionable
  BundlerError on non-integer values instead of a raw ValueError.
- packager: write zip members with a fixed timestamp + permissions so
  identical inputs yield byte-for-byte identical artifacts (genuinely
  reproducible builds), and strengthen the determinism test accordingly.

Adds regression tests for priority validation, plain-HTTP/host rejection,
and byte-level artifact reproducibility (111 bundler tests pass; ruff clean).

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@mnriem

mnriem commented Jun 19, 2026

Copy link
Copy Markdown
Collaborator Author

Thanks for the review. Addressed all seven code findings in b762e4b:

  • validator.py — removed the redundant string-quoting on ReferenceChecker's str | None return so it evaluates as a real union under from __future__ import annotations.
  • adapters.py (Windows paths) — drive-letter paths like C:\catalog.json now normalize to the local-file branch instead of falling through to "unsupported scheme".
  • adapters.py (HTTPS) — remote catalog URLs are now validated up front (HTTPS required, HTTP only for localhost, host required) before any network call, mirroring specify_cli.catalogs._validate_catalog_url.
  • adapters.py (origin)loads_json now receives the real file path / URL as origin, so JSON parse errors name the actual source instead of <string> (both local and HTTP paths).
  • manifest.py (priority)priority is parsed defensively and raises an actionable BundlerError on non-integer input instead of a raw ValueError.
  • packager.py (determinism) — members are now written with a fixed timestamp + permissions, so identical inputs produce byte-for-byte identical artifacts; the test now asserts true byte-level reproducibility rather than just member ordering.

Added regression tests for priority validation, plain-HTTP/missing-host rejection, and byte-level artifact reproducibility. 111 bundler tests pass; ruff clean.

The remaining comment on .specify/integration.json is expected — see the inline reply.

Posted by GitHub Copilot (model: Claude Opus 4.8) on behalf of @mnriem.

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot's findings

  • Files reviewed: 110/110 changed files
  • Comments generated: 2

Comment thread src/specify_cli/bundler/services/packager.py Outdated
Comment thread src/specify_cli/bundler/services/adapters.py Outdated
… URLs

- packager: when --output points inside the bundle directory, exclude the
  whole output subtree from collection so previously-built artifacts are
  never re-packaged (prevents broken reproducibility and unbounded growth).
- adapters: resolve file:// catalog URLs via url2pathname and preserve
  netloc, so Windows file URLs (file:///C:/...) and UNC shares
  (file://server/share) resolve correctly instead of dropping the host or
  producing /C:/x.

Adds regression tests for nested-output exclusion and file:// resolution
(113 bundler tests pass; ruff clean).

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@mnriem

mnriem commented Jun 19, 2026

Copy link
Copy Markdown
Collaborator Author

Addressed both findings in 9f5a542:

  • packager.py (nested output dir) — when --output points to a directory inside the bundle (e.g. dist/), the whole output subtree is now excluded from collection, so previously-built artifacts are never re-packaged. This keeps builds reproducible and bounded.
  • adapters.py (file:// URLs)file:// catalog sources now resolve via url2pathname and preserve netloc, so Windows file URLs (file:///C:/...) and UNC shares (file://server/share) resolve correctly instead of dropping the host or yielding /C:/x.

Added regression tests for nested-output exclusion and file:// resolution. 113 bundler tests pass; ruff clean.

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot's findings

  • Files reviewed: 110/110 changed files
  • Comments generated: 5

Comment thread src/specify_cli/bundler/services/packager.py Outdated
Comment thread src/specify_cli/bundler/models/records.py
Comment thread src/specify_cli/bundler/services/installer.py
Comment thread README.md
Comment thread specs/001-spec-kit-bundler/quickstart.md
Addresses review 4535084048:

- versioning.is_semver: enforce a full MAJOR.MINOR.PATCH SemVer (with optional
  pre-release/build) via a dedicated regex, instead of accepting any
  packaging.version.Version-parseable string (e.g. "1", "1.0"). This makes
  BundleManifest.structural_errors() reject non-semver versions.
- packager: narrow the prior-artifact skip pattern to semver-named zips
  (<id>-<x.y.z>.zip) so legitimate assets like <id>-assets.zip are still
  packaged.
- primitives (preset + extension install): use an explicit `is None` check so
  an intentional priority of 0 is preserved instead of being replaced by the
  default.

Adds regressions: non-semver rejection ("1"/"1.0"/"1.2.3.4"), asset-not-
excluded vs semver-artifact-excluded, and priority-0 pass-through.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@mnriem

mnriem commented Jun 19, 2026

Copy link
Copy Markdown
Collaborator Author

Addressed review 4535084048 (commit 654e1da).

  • versioning.is_semver — Now enforces a full MAJOR.MINOR.PATCH SemVer (with optional pre-release/build) via a dedicated regex, rather than accepting any packaging.version.Version-parseable string. Partial versions like 1 / 1.0 (and 1.2.3.4) are rejected, so BundleManifest.structural_errors() correctly flags non-semver bundle.version / pinned component versions. Test matrix extended to cover these.
  • packager — Narrowed the prior-artifact skip pattern to only semver-named zips (<id>-<x.y.z>.zip), so a legitimate asset such as <id>-assets.zip is no longer over-filtered out of the bundle.
  • primitives (preset + extension install) — Replaced component.priority or DEFAULT_PRIORITY with an explicit is None check so an intentional priority of 0 (a meaningful, lowest-wins value) is preserved instead of silently defaulted.

Added regressions for each. Ran the entire suite in the worktree venv: 4152 passed, 3 skipped; ruff clean.

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot's findings

  • Files reviewed: 114/114 changed files
  • Comments generated: 3

Comment thread src/specify_cli/bundler/services/packager.py
Comment thread docs/reference/bundles.md Outdated
Comment thread examples/bundles/security-researcher/README.md Outdated
…n/priority docs

Addresses review 4535132279:

- packager: the prior-artifact skip regex now matches semver names carrying
  both a prerelease and build-metadata segment (e.g. 1.0.0-rc1+build5), so such
  an existing artifact is excluded rather than re-packaged — keeping builds
  bounded/deterministic, consistent with is_semver().
- docs/reference/bundles.md: correct the install integration wording.
  --integration selects the integration when initializing a new project and
  confirms the target when a pinned bundle's active integration can't be
  determined; it does NOT override a bundle that targets a specific integration
  (a mismatch aborts with no changes).
- examples/security-researcher README: reword the preset priority note in terms
  of the numeric comparison (ascending priority order) to avoid inverting the
  meaning.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@mnriem

mnriem commented Jun 19, 2026

Copy link
Copy Markdown
Collaborator Author

Addressed review 4535132279 (commit 70b9292).

  • packager — The prior-artifact skip regex now matches semver names that carry both a prerelease and build-metadata segment (e.g. 1.0.0-rc1+build5), so such an existing artifact is excluded rather than re-packaged — keeping builds bounded/deterministic and consistent with is_semver(). Added a regression.
  • docs/reference/bundles.md — Corrected the install integration wording. --integration selects the integration when initializing a new project and confirms the target when a pinned bundle's active integration can't be determined; it does not override a bundle that targets a specific integration (a mismatch aborts with no changes). Integration-agnostic bundles inherit the project's active integration.
  • examples/bundles/security-researcher/README.md — Reworded the preset priority note in terms of the numeric comparison (presets apply in ascending priority order, so the low number 5 sorts ahead) to avoid inverting the meaning.

Bundler suite: 188 passing; ruff clean.

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot's findings

  • Files reviewed: 114/114 changed files
  • Comments generated: 2

Comment thread src/specify_cli/commands/bundle/__init__.py
Comment thread docs/reference/bundles.md Outdated
… docs

Addresses review 4535159341:

- bundle install: for an already-initialized project, the project's recorded
  active integration is now authoritative. --integration no longer overrides it
  (which let a copilot project install a claude-pinned bundle via
  `--integration claude`, bypassing the FR-019 clash guard). The override still
  selects the integration at init time and confirms the target only when the
  active integration cannot be determined.
- docs/reference/bundles.md: reword the install guarantee to match the
  implementation — no provenance record is written unless the install fully
  succeeds, and rollback of this run's components is best-effort (removal errors
  are swallowed, so partial on-disk state may remain). Dropped the inaccurate
  "atomic / rolls back everything" claim.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@mnriem

mnriem commented Jun 19, 2026

Copy link
Copy Markdown
Collaborator Author

Addressed review 4535159341 (commit 3c44029).

  • bundle install — For an already-initialized project, the project's recorded active integration is now authoritative. An explicit --integration no longer overrides it (previously a copilot project could install a claude-pinned bundle via --integration claude, bypassing the FR-019 clash guard). The override still selects the integration at init time and confirms the target only when the active integration can't be determined. Added a regression test asserting the override can't bypass the clash.
  • docs/reference/bundles.md — Reworded the install guarantee to match the implementation: no provenance record is written unless the install fully succeeds, and rollback of the run's components is best-effort (removal errors are swallowed, so partial on-disk state may remain). Dropped the inaccurate "atomic / rolls back everything" claim.

Bundler suite: 189 passing; ruff clean.

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot's findings

  • Files reviewed: 114/114 changed files
  • Comments generated: 1

Comment thread src/specify_cli/bundler/models/records.py
Addresses review 4535194606: _component_from_dict now rejects a contributed
component whose 'kind' is not a supported component kind or whose 'id' is
empty, raising a BundlerError that explicitly flags the records file as
corrupt. Previously such a record loaded successfully and only failed later
(e.g. in primitive_manager() during bundle remove/update) with a less
actionable error.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@mnriem

mnriem commented Jun 19, 2026

Copy link
Copy Markdown
Collaborator Author

Addressed review 4535194606 (commit 139ef20).

  • records._component_from_dict — Now validates each contributed component when loading the records file: kind must be one of the supported component kinds and id must be non-empty, otherwise it raises a BundlerError that explicitly flags the records file as corrupt. Previously a corrupt record loaded successfully and only failed later (e.g. in primitive_manager() during bundle remove/update) with a less actionable error.

Added regressions for unknown kind and empty id. Bundler suite: 191 passing; ruff clean.

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot's findings

  • Files reviewed: 114/114 changed files
  • Comments generated: 7

Comment thread src/specify_cli/bundler/lib/versioning.py
Comment thread src/specify_cli/bundler/lib/versioning.py
Comment thread src/specify_cli/bundler/services/validator.py
Comment thread src/specify_cli/bundler/services/validator.py
Comment thread src/specify_cli/commands/bundle/__init__.py
Comment thread src/specify_cli/commands/bundle/__init__.py
Comment thread src/specify_cli/commands/bundle/__init__.py Outdated
- versioning: tolerate an uppercase `V` prefix in `_normalize_semver` and
  `is_semver`, mirroring specify_cli._version tag normalization (V -> v) so
  `V1.2.3` parses and validates consistently.
- validator: import BundlerError and narrow the speckit_version constraint
  except clause to `BundlerError` only, so programming errors are no longer
  masked behind an "invalid constraint" message.
- bundle update: accept `--integration` and thread it through
  resolve_install_plan the same way `bundle install` does (override used only
  when the active integration can't be auto-detected), so integration-pinned
  bundles can be updated where `.specify/integration.json` is missing/unreadable.
- bundle validate: fold reference warnings into `report.warnings` so the
  ValidationReport is the single warning channel at the CLI layer.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@mnriem

mnriem commented Jun 19, 2026

Copy link
Copy Markdown
Collaborator Author

Addressed review 4535234003 (commit 54b37b2).

  • versioning._normalize_semver / is_semver — Now tolerate an uppercase V prefix in addition to lowercase v, mirroring specify_cli._version tag normalization (Vv), so V1.2.3 parses and validates consistently.
  • validator.validate_manifest — Imported BundlerError and narrowed the requires.speckit_version except clause to BundlerError only. parse_constraint() already wraps parse failures in BundlerError, so unexpected programming errors now propagate instead of being masked behind an "invalid constraint" message.
  • bundle update — Added a --integration option, threaded through resolve_install_plan() the same way bundle install does: the project's detected active integration is authoritative, and the override is used only when detection is indeterminate. This makes the resolver's "re-run with --integration" guidance actionable and unblocks updating integration-pinned bundles where .specify/integration.json is missing/unreadable.
  • bundle validate — Reference warnings are now folded into report.warnings, so ValidationReport is the single warning channel at the CLI layer instead of maintaining two.

Added regressions (uppercase-V semver, validator BundlerError-only catch + non-BundlerError propagation, update --integration presence). Bundler suite: 195 passing; ruff clean.

Rich can split the "--integration" option label with ANSI escape codes
between the two leading dashes, so the literal substring check failed under
CI's terminal settings. Match the un-split option word instead, mirroring how
test_bundle_help_lists_all_commands checks bare command names.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@mnriem mnriem changed the title feat: add specify bundle command for role-based project setup (dogfooding, scrub generated files before merge) feat: add specify bundle command Jun 19, 2026

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot's findings

  • Files reviewed: 115/115 changed files
  • Comments generated: 1

Comment on lines +69 to +76
# FR-016: SpecKit version gate — refuse incompatible installs.
if enforce_version and manifest.requires.speckit_version:
if not satisfies(speckit_version, manifest.requires.speckit_version):
raise BundlerError(
f"Bundle '{manifest.bundle.id}' requires Spec Kit "
f"{manifest.requires.speckit_version}, but this project uses "
f"{speckit_version}. Update Spec Kit or choose a compatible bundle."
)

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot's findings

  • Files reviewed: 115/115 changed files
  • Comments generated: 2

Comment on lines +89 to +92
info = zipfile.ZipInfo(filename=arcname, date_time=_FIXED_TIMESTAMP)
info.compress_type = zipfile.ZIP_DEFLATED
info.external_attr = 0o644 << 16
archive.writestr(info, file_path.read_bytes())
Comment on lines +104 to +120
for component in plan.components:
key = (component.kind, component.id)
if installer.is_installed(project_root, component):
# A component is "ours" only when this bundle (or a sibling
# bundle) already owns it. Independently-installed components
# are never attributed and — crucially — never refreshed, so
# ``bundle update`` cannot make collateral changes to things it
# does not own (FR-022).
owned = key in prior_ours or key in other_tracked
if refresh and owned:
_refresh_component(project_root, installer, component)
result.refreshed.append(component)
else:
result.skipped.append(component)
if owned:
contributed.append(component)
continue
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants