Skip to content

feat: add specify bundle command for role-based project setup (dogfooding, scrub generated files before merge)#3070

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

feat: add specify bundle command for role-based project setup (dogfooding, scrub generated files before merge)#3070
mnriem wants to merge 35 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 (supports --refresh)
  • 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

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: 113/113 changed files
  • Comments generated: 2

Comment on lines +120 to +136
_validate_remote_url(source.id, url)
return _http_get_json(url)

raise BundlerError(f"Unsupported catalog URL scheme: {url}")

return fetch


def _http_get_json(url: str) -> dict:
import urllib.request

try:
with urllib.request.urlopen(url, timeout=HTTP_TIMEOUT_SECONDS) as response: # noqa: S310
raw = response.read().decode("utf-8")
except Exception as exc: # noqa: BLE001
raise BundlerError(f"Failed to fetch catalog from {url}: {exc}") from exc
return loads_json(raw, origin=url)
Comment on lines +40 to +42
data = load_yaml(path)
catalogs = data.get("catalogs") if isinstance(data, dict) else None
return list(catalogs) if catalogs else []
PascalThuet and others added 3 commits June 19, 2026 12:21
…he repo root (github#2892)

* feat(scripts): add SPECIFY_INIT_DIR to target a member project from the repo root

Resolve an explicit SPECIFY_INIT_DIR project override once in the core
get_repo_root / Get-RepoRoot, so a non-interactive / CI caller can target a
member project (the directory containing .specify/) from a monorepo root
without cd. Strict by design: the path must exist and contain .specify/,
otherwise it hard-errors with no silent fallback.

- Single resolver in core; the git feature-branch script inherits it by
  sourcing core, with no per-extension copies.
- PS resolver verifies the resolved path is a directory (Resolve-Path also
  succeeds for files) so a file value errors as "not an existing directory".
- get_feature_paths splits decl/assignment so a SPECIFY_INIT_DIR failure
  propagates instead of being masked by `local`.
- create-new-feature-branch: when core is absent (only git-common loaded) and
  SPECIFY_INIT_DIR is set, hard-error rather than silently using the git root.
- Document SPECIFY_INIT_DIR and SPECIFY_FEATURE_DIRECTORY in the core reference.
- Tests for valid/relative/trailing-slash/file/missing/no-.specify targets,
  feature-axis composition, the no-core guard, and a PowerShell mirror.

* fix: guard SPECIFY_INIT_DIR with stale core scripts

* docs: clarify SPECIFY_FEATURE_DIRECTORY precedence wording

* fix: normalize trailing slash in PowerShell SPECIFY_INIT_DIR resolver

Resolve-Path preserves a trailing separator from its input, so a
SPECIFY_INIT_DIR ending in a slash returned a root that didn't match the
bash resolver (whose `cd && pwd` strips it). That broke
test_ps_trailing_slash_tolerated on the CI runners, which do have pwsh.
Trim it with TrimEndingDirectorySeparator (no-op on a bare root or a path
with no trailing separator).

Also fix the misleading test comment: the PowerShell mirror runs on the
CI ubuntu/windows runners (they ship pwsh), it is not skipped there.

* test: normalize bash path expectations on Windows

* docs: clarify SPECIFY_INIT_DIR root helpers
Mirror the SPECIFY_INIT_DIR resolver (resolve_specify_init_dir in
common.sh) into the committed dogfooding .specify/scripts/bash copies so
the git extension's create-new-feature-branch.sh finds an up-to-date
common.sh instead of failing with "requires updated Spec Kit core
scripts". Fixes the test_init_dir.py CI failures.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
- adapters: route catalog HTTP fetches through the shared authenticated
  client (authentication.http.open_url) so auth.json tokens apply and the
  Authorization header is stripped on cross-host/downgrade redirects.
  Reject any redirect that leaves HTTPS via a redirect_validator and
  re-validate the final URL after redirects, closing the urlopen
  auto-redirect MITM/downgrade gap.
- catalog_config._read: raise an actionable BundlerError when the config
  top level is not a mapping, 'catalogs' is not a list, or an entry is
  not a mapping, instead of letting list(<str>) produce a downstream
  AttributeError.

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

mnriem commented Jun 19, 2026

Copy link
Copy Markdown
Collaborator Author

Addressed review 4534421867 plus the test_init_dir.py CI failures.

Review findings

  • adapters._http_get_json: catalog HTTP fetches now go through the shared authenticated client (authentication.http.open_url), so auth.json tokens apply and the Authorization header is stripped on cross-host/downgrade redirects. A redirect_validator rejects any redirect that leaves HTTPS, and the final URL is re-validated after redirects — closing the urlopen auto-redirect MITM/downgrade gap.
  • catalog_config._read: now raises an actionable BundlerError when the top level isn't a mapping, catalogs isn't a list, or an entry isn't a mapping, instead of letting list(<str>) cause a downstream AttributeError.

CI: test_init_dir.py failures
Root cause: the branch was based on an older upstream, so the dogfooded .specify/scripts/bash/common.sh lacked resolve_specify_init_dir. The git extension's create-new-feature-branch.sh (updated upstream) sources that copy first and failed with "requires updated Spec Kit core scripts". Cherry-picked the upstream SPECIFY_INIT_DIR commit (#2892) and synced the dogfooded .specify core scripts. tests/test_init_dir.py now passes (24/24).

Added regression tests for redirect downgrade rejection, non-HTTPS final URL, and malformed catalog-config shapes. Full bundler suite + test_init_dir.py: 178 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: 123/123 changed files
  • Comments generated: 6

Comment thread src/specify_cli/bundler/models/records.py
Comment thread src/specify_cli/bundler/commands_impl/catalog_config.py
Comment thread src/specify_cli/bundler/services/installer.py Outdated
Comment thread .gitattributes Outdated
Comment thread src/specify_cli/commands/bundle/__init__.py
Comment thread src/specify_cli/bundler/models/catalog.py
…dence

Addresses review 4534504799:

- records.load_records: confine the read via ensure_within(project_root,
  ...) so a symlinked/traversal-escaping .specify cannot read arbitrary
  files outside the project (matches the write path's within= guard).
- catalog_config._slug: lowercase so derived catalog ids are
  deterministic across platforms and case-variant duplicates can't slip
  past the case-sensitive dup check.
- installer.install_bundle: reword the docstring's misleading "atomic on
  failure" claim to describe the real scoped guarantee (record written
  only on full success; rollback limited to newly-installed components).
- bundle update: enforce the source install_policy like install, refusing
  to update from a discovery-only source (FR-025).
- catalog source precedence: the CLI now passes ~/.specify as the user
  config dir so project > user > built-in precedence is actually
  reachable (previously the user scope was silently ignored).
- .gitattributes: scope the specs whitespace exemption to the generated
  dogfooding feature dir (specs/001-spec-kit-bundler/**) instead of all
  of specs/**.

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

mnriem commented Jun 19, 2026

Copy link
Copy Markdown
Collaborator Author

Addressed review 4534504799 (commit 71f18d2).

  • records.load_records — Now confines the read with ensure_within(project_root, …), so a symlinked/traversal-escaping .specify can't read files outside the project (matches the write path's within= guard).
  • catalog_config._slug — Lowercases now, so derived catalog ids are deterministic across platforms and case-variant duplicates can't slip past the case-sensitive dup check.
  • installer.install_bundle docstring — Reworded the misleading "atomic on failure" claim to the real scoped guarantee: the provenance record is written only on full success, and rollback is limited to components newly installed during the call.
  • bundle update — Now enforces the source install_policy like install, refusing to update from a discovery-only source (FR-025).
  • Source precedence — The CLI now passes ~/.specify as the user config dir, so the documented project > user > built-in precedence is actually reachable (the user scope was previously ignored). Kept load_source_stack hermetic for tests by resolving the user dir at the CLI boundary.
  • .gitattributes — Scoped the specs whitespace exemption to the generated dogfooding dir (specs/001-spec-kit-bundler/**) instead of all of specs/**.

Added regressions: record-read symlink-escape rejection, slug lowercase determinism, and discovery-only update refusal. Bundler suite + test_init_dir.py: 181 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: 123/123 changed files
  • Comments generated: 3

Comment thread src/specify_cli/bundler/services/installer.py
Comment thread src/specify_cli/bundler/models/catalog.py
Comment thread src/specify_cli/commands/bundle/__init__.py Outdated
Addresses review 4534571362:

- installer: in refresh mode (bundle update) only re-apply already-
  installed components that this bundle (or a sibling) owns. Components
  installed independently and tracked by no bundle are now skipped, never
  refreshed, so update cannot make collateral changes (FR-022).
- catalog.load_catalog_payload: validate each entry's own id is present
  and matches its enclosing bundles key, rejecting catalogs that would
  otherwise list a spoofed or unresolvable id.
- bundle info: stop swallowing manifest download failures. If the
  manifest can't be resolved (e.g. --offline against an https download_url
  or a download failure), surface the error and exit non-zero instead of
  silently degrading to catalog `provides` counts, preserving the "info
  == what install applies" guarantee.

Added regressions: refresh leaves independently-installed components
untouched, catalog id key/field mismatch + missing id rejection, and
info exits non-zero when the manifest is unresolvable offline.

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

mnriem commented Jun 19, 2026

Copy link
Copy Markdown
Collaborator Author

Addressed review 4534571362 (commit c3fc064).

  • installer refresh modebundle update now only re-applies already-installed components that this bundle (or a sibling) owns. Components installed independently and tracked by no bundle are skipped, never refreshed, so update can't make collateral changes (FR-022).
  • catalog.load_catalog_payload — Validates each entry's own id is present and equals its enclosing bundles key, rejecting catalogs that would otherwise list a spoofed or unresolvable id.
  • bundle info — No longer swallows manifest download failures. If the manifest can't be resolved (e.g. --offline against an https:// download_url, or a download failure), it surfaces the error and exits non-zero instead of silently degrading to catalog provides counts — preserving the "info shows the same set install applies" guarantee.

Added regressions: refresh leaves independently-installed components untouched, catalog id key/field mismatch + missing-id rejection, and info exits non-zero when the manifest is unresolvable offline. Bundler suite + test_init_dir.py: 185 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: 123/123 changed files
  • Comments generated: 4

Comment thread src/specify_cli/bundler/commands_impl/catalog_config.py Outdated
Comment thread src/specify_cli/bundler/commands_impl/catalog_config.py
Comment thread src/specify_cli/bundler/lib/project.py Outdated
Comment thread src/specify_cli/bundler/lib/project.py
Addresses review 4534716790: two more state reads bypassed the
symlink/path-escape confinement that records and the write paths already
enforce.

- catalog_config._read: validate the config path with
  ensure_within(project_root, ...) before exists()/read, so a symlinked
  .specify resolving outside project_root is rejected instead of read.
- lib.project.active_integration: confine the .specify/integration.json
  read the same way; an out-of-tree escape is treated as "not
  determinable" (returns None) rather than followed.

Added regressions covering both via a symlinked .specify pointing
outside the project root.

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

mnriem commented Jun 19, 2026

Copy link
Copy Markdown
Collaborator Author

Addressed review 4534716790 (commit beb978b).

Both findings were the same class of issue — state reads that bypassed the symlink/path-escape confinement records and the write paths already enforce:

  • catalog_config._read — Now validates the config path with ensure_within(project_root, …) before exists()/read, so a symlinked .specify resolving outside the project root is rejected instead of read.
  • lib.project.active_integration — Confines the .specify/integration.json read the same way; an out-of-tree escape is treated as "not determinable" (returns None) rather than followed.

Added regressions for both, using a symlinked .specify pointing outside the project root. Bundler suite + test_init_dir.py: 187 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: 123/123 changed files
  • Comments generated: 4

Comment on lines +122 to +136
provides = data.get("provides") or {}
if not isinstance(provides, dict):
raise BundlerError("'provides' must be a mapping when present.")

manifest = cls(
schema_version=schema_version,
bundle=meta,
requires=requires,
integration=integration,
extensions=_parse_refs("extensions", provides.get("extensions")),
presets=_parse_refs("presets", provides.get("presets")),
steps=_parse_refs("steps", provides.get("steps")),
workflows=_parse_refs("workflows", provides.get("workflows")),
tags=tuple(str(t) for t in (data.get("tags") or [])),
)
Comment thread CHANGELOG.md
Comment on lines +5 to +6
- feat(scripts): add SPECIFY_INIT_DIR to target a member project from the repo root (#2892)

Comment thread .gitattributes
Comment on lines +12 to +14
# Scope the specs exemption to the generated dogfooding feature dir only, so
# any hand-authored specs elsewhere keep full whitespace validation.
specs/001-spec-kit-bundler/** -whitespace
Comment on lines +105 to +110
host = parsed.netloc.split("@")[-1].split(":")[0]
# Hostnames are case-insensitive; _slug() lowercases so 'Example.com'
# and 'example.com' derive the same, deterministic id.
host_label = Path(host).stem or host
path_stem = Path(parsed.path).stem if parsed.path else ""
parts = [p for p in (_slug(host_label), _slug(path_stem)) if p]
…l host

Addresses review 4534768419:

- manifest.from_dict: reject a non-list `tags` (e.g. a bare string) instead
  of splitting it character-by-character, matching the catalog parser and
  the schema contract (tags = list of strings).
- catalog_config._derive_id: derive ids from the full host (TLD included)
  so example.com and example.net no longer collide on the same id. Updated
  the affected id assertions.
- CHANGELOG: call out the new `specify bundle` command group in the
  unreleased section (the PR's headline user-facing feature).
- .gitattributes: clarify the specs whitespace exemption — the dogfooding
  feature dir is scrubbed before merge (not retained), so it doesn't weaken
  checks for kept docs.

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

mnriem commented Jun 19, 2026

Copy link
Copy Markdown
Collaborator Author

Addressed review 4534768419 (commit 1bf4e9a) and updated the PR description.

  • manifest.from_dict — Rejects a non-list tags (e.g. a bare string) instead of splitting it character-by-character, matching the catalog parser and the schema contract (tags = list of strings).
  • catalog_config._derive_id — Derives ids from the full host (TLD included), so example.com and example.net no longer collide on the same id. Updated the affected id assertions and added a TLD-disambiguation regression.
  • CHANGELOG — Added the new specify bundle command group to the unreleased section as the headline user-facing feature.
  • .gitattributes — The SDD artifacts under specs/001-spec-kit-bundler/** are now slated for scrub (not retained), so the whitespace exemption is grouped with the rest of the generated scaffolding and doesn't weaken checks for any kept docs. The PR description has been updated to move those specs (and memory/constitution.md) into the scrub-before-merge list.

Added regressions: string tags rejection and TLD-distinct derived ids. Bundler suite + test_init_dir.py: 189 passing; ruff clean.

The project constitution (.specify/memory/constitution.md) is the one
dogfooding artifact carried forward past the pre-merge scrub. Give it its
own standalone whitespace exemption so it survives removal of the broader
.specify/** generated-scaffolding exemption.

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

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: 123/123 changed files
  • Comments generated: 3

Comment on lines +161 to +170
for component in target.contributed_components:
key = (component.kind, component.id)
if key in still_needed:
result.skipped.append(component)
continue
if installer.is_installed(project_root, component):
installer.remove(project_root, component)
result.uninstalled.append(component)

save_records(project_root, remove_record(records, bundle_id))
Comment on lines +225 to +229
if user_config_dir is not None:
_merge_config(by_id, Path(user_config_dir) / CONFIG_FILENAME, Scope.USER)

_merge_config(by_id, Path(project_root) / ".specify" / CONFIG_FILENAME, Scope.PROJECT)

Comment on lines +51 to +54
out_dir = Path(output_dir).resolve() if output_dir else bundle_dir
out_dir.mkdir(parents=True, exist_ok=True)
artifact_name = f"{manifest.bundle.id}-{manifest.bundle.version}.zip"
artifact_path = out_dir / artifact_name
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