Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
40 changes: 40 additions & 0 deletions bundles/assess/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
# Idea Assessment Bundle

A first-party GitHub Spec Kit bundle that installs an idea-triage pipeline before committing to Spec-Driven Development.

## What it provides

- **Assess extension** (`extensions/assess`) — the `speckit.assess.intake`, `speckit.assess.research`, `speckit.assess.define`, `speckit.assess.shape`, and `speckit.assess.decide` commands.
- **Assess workflow** (`workflows/assess`) — a guided, resumable pipeline:
1. `intake` the raw idea.
2. `research` the evidence.
3. `define` the problem.
4. `shape` the concept.
5. `decide` the verdict.
6. `review-verdict` gate — approve to complete the assessment; reject to abort. A `go` verdict is then handed off manually to `/speckit.specify`.

## Install

```bash
specify bundle install assess
# or
specify bundle add assess
```

## Run the workflow

```bash
specify workflow run assess \
--input idea="Let users work offline and sync when they reconnect" \
--input slug="offline-mode"
```

Inputs omitted from the command line are prompted interactively. The slug is used as the working directory under `.specify/assessments/<slug>/` for all artifacts.

## Remove

```bash
specify bundle remove assess
```

Removing the bundle uninstalls the workflow and the extension it contributed, unless they are still depended on by another installed bundle (FR-022). Components you installed independently are not attributed to this bundle and survive removal.
25 changes: 25 additions & 0 deletions bundles/assess/bundle.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
schema_version: "1.0"

bundle:
id: "assess"
name: "Idea Assessment Pipeline"
version: "1.0.0"
role: "developer"
description: "Idea triage before Spec-Driven Development: intake, research, define, shape, decide with a verdict review gate; surviving ideas hand off manually to the specify command."
author: "GitHub"
license: "MIT"

requires:
speckit_version: ">=0.9.0"
tools: []
mcp: []

provides:
extensions:
- id: "assess"
version: "1.0.0"
workflows:
- id: "assess"
version: "1.0.0"

tags: ["assessment", "discovery", "triage", "product"]
38 changes: 38 additions & 0 deletions bundles/bugfix/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
# Bug Fix Bundle

A first-party GitHub Spec Kit bundle that installs an orchestrated bug-fixing pipeline.

## What it provides

- **Bug extension** (`extensions/bug`) — the `speckit.bug.assess`, `speckit.bug.fix`, and `speckit.bug.test` commands.
- **Bugfix workflow** (`workflows/bugfix`) — a guided, resumable pipeline:
1. `assess` the bug report.
2. `review-assessment` gate — approve to proceed, reject to abort.
3. `fix` the bug.
4. `test` the fix.

## Install

```bash
specify bundle install bugfix
# or
specify bundle add bugfix
```

## Run the workflow

```bash
specify workflow run bugfix \
--input report="https://github.com/example/repo/issues/1234" \
--input slug="callback-token"
```

Inputs omitted from the command line are prompted interactively. The slug is used as the working directory under `.specify/bugs/<slug>/` for all artifacts.

## Remove

```bash
specify bundle remove bugfix
```

Removing the bundle uninstalls the workflow and the extension it contributed, unless they are still depended on by another installed bundle (FR-022). Components you installed independently are not attributed to this bundle and survive removal.
25 changes: 25 additions & 0 deletions bundles/bugfix/bundle.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
schema_version: "1.0"

bundle:
id: "bugfix"
name: "Guided Bug Fix"
version: "1.0.0"
role: "developer"
description: "Orchestrated bug triage: assess a bug report, review the assessment behind a human gate, apply the fix, and verify it with tests."
author: "GitHub"
license: "MIT"

requires:
speckit_version: ">=0.9.0"
tools: []
mcp: []

provides:
extensions:
- id: "bug"
version: "1.0.0"
workflows:
- id: "bugfix"
version: "1.0.0"

tags: ["bug", "triage", "workflow", "qa"]
51 changes: 51 additions & 0 deletions bundles/catalog.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
{
"schema_version": "1.0",
"updated_at": "2026-09-10T00:00:00Z",
"catalog_url": "https://raw.githubusercontent.com/github/spec-kit/main/bundles/catalog.json",
"bundles": {
"bugfix": {
"id": "bugfix",
"name": "Guided Bug Fix",
"version": "1.0.0",
"role": "developer",
"description": "Orchestrated bug triage: assess a bug report, review behind a human gate, apply the fix, and verify with tests.",
"author": "GitHub",
"license": "MIT",
"download_url": "https://raw.githubusercontent.com/github/spec-kit/main/bundles/bugfix/bundle.yml",
"repository": "https://github.com/github/spec-kit",
"requires": {
"speckit_version": ">=0.9.0"
},
"provides": {
"extensions": 1,
"presets": 0,
"steps": 0,
"workflows": 1
},
"tags": ["bug", "triage", "workflow", "qa"],
"verified": true
},
"assess": {
"id": "assess",
"name": "Idea Assessment Pipeline",
"version": "1.0.0",
"role": "developer",
"description": "Idea triage before Spec-Driven Development: intake, research, define, shape, decide with a verdict review gate; surviving ideas hand off manually to specify.",
"author": "GitHub",
"license": "MIT",
"download_url": "https://raw.githubusercontent.com/github/spec-kit/main/bundles/assess/bundle.yml",
"repository": "https://github.com/github/spec-kit",
"requires": {
"speckit_version": ">=0.9.0"
},
"provides": {
"extensions": 1,
"presets": 0,
"steps": 0,
"workflows": 1
},
"tags": ["assessment", "discovery", "triage", "product"],
"verified": true
}
}
}
23 changes: 22 additions & 1 deletion docs/reference/bundles.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,24 @@ Bundles compose existing Spec Kit components — extensions, presets, workflows,

A bundle is described by a `bundle.yml` manifest and is discovered through the same catalog stack as other components. Installing a bundle resolves its declared components against pinned versions, checks for the single cross-bundle conflict point (the active integration), and applies each component idempotently with full provenance tracking so it can be cleanly removed or refreshed later.

## First-party Bundles

Spec Kit ships a first-party bundle catalog in `bundles/catalog.json`. These bundles are curated, marked `verified: true`, and resolve through the built-in `builtin://default` catalog source.

| Bundle | Role | Components | Use case |
| --------- | ----------- | ----------------------------------------------------- | --------------------------------------- |
| `bugfix` | `developer` | `bug` extension + `bugfix` workflow | Guided assess → gate → fix → test |
| `assess` | `developer` | `assess` extension + `assess` workflow | Idea triage before Spec-Driven Development |

Install a first-party bundle the same way you install any bundle (`add` is an alias for `install`):

```bash
specify bundle install bugfix
specify bundle add assess
```

The first-party catalog is fetched from the repository online and falls back to the packaged wheel snapshot offline so discovery works without network access. A local bundle manifest can install bundled extensions and workflows with `--offline`. Catalog-discovered bundle manifests still resolve from their `download_url`, so `specify bundle add <id>` requires network today; fully offline catalog installation is tracked as follow-up work.

## Search Available Bundles

```bash
Expand Down Expand Up @@ -128,7 +146,10 @@ If your bundle references components from non-default catalogs, document those c

## Manage Catalog Sources

Bundles are discovered through a priority-ordered stack of catalog sources (project, user, and built-in scopes).
Bundles are discovered through a priority-ordered stack of catalog sources (project, user, and built-in scopes). The built-in sources are:

- `builtin://default` — first-party bundles shipped in `bundles/catalog.json` (`bugfix`, `assess`, ...), install-allowed.
- `builtin://community` — community submissions in `bundles/catalog.community.json`, discovery-only.

### List the Catalog Stack

Expand Down
7 changes: 5 additions & 2 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -45,12 +45,15 @@ packages = ["src/specify_cli"]
"extensions/agent-context" = "specify_cli/core_pack/extensions/agent-context"
"extensions/assess" = "specify_cli/core_pack/extensions/assess"
"extensions/bug" = "specify_cli/core_pack/extensions/bug"
# Bundled workflows (auto-installed during `specify init`)
# Bundled workflows (auto-installed during `specify init` or via first-party bundles)
"workflows/speckit" = "specify_cli/core_pack/workflows/speckit"
"workflows/bugfix" = "specify_cli/core_pack/workflows/bugfix"
"workflows/assess" = "specify_cli/core_pack/workflows/assess"
Comment thread
markuswondrak marked this conversation as resolved.
# Bundled presets (installable via `specify preset add <name>` or `specify init --preset <name>`)
"presets/lean" = "specify_cli/core_pack/presets/lean"
"presets/constitution-sync" = "specify_cli/core_pack/presets/constitution-sync"
# Community bundle catalog snapshot (used for offline discovery)
# Bundle catalog snapshots (used for offline discovery)
"bundles/catalog.json" = "specify_cli/core_pack/bundles/catalog.json"
"bundles/catalog.community.json" = "specify_cli/core_pack/bundles/catalog.community.json"

[project.optional-dependencies]
Expand Down
45 changes: 25 additions & 20 deletions src/specify_cli/bundler/services/adapters.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,16 +26,22 @@
"https://raw.githubusercontent.com/github/spec-kit/main/"
"bundles/catalog.community.json"
)
FIRSTPARTY_CATALOG_URL = (
"https://raw.githubusercontent.com/github/spec-kit/main/"
"bundles/catalog.json"
)

# The default catalog is reserved for first-party bundles. The community
# catalog is loaded from the repository online and from the packaged snapshot
# offline so discovery remains useful without network access.
_BUILTIN_CATALOGS: dict[str, dict] = {
"builtin://default": {
"schema_version": "1.0",
"catalog_url": "builtin://default",
"bundles": {},
},
# Built-in catalogs are resolved directly by URL. ``builtin://default`` is the
# repository-shipped first-party bundle catalog; ``builtin://community`` is the
# community catalog. Both are fetched from the repository online and fall back
# to the packaged wheel snapshot offline so discovery works without network.
_BUILTIN_REPOSITORY_URLS: dict[str, str] = {
"builtin://default": FIRSTPARTY_CATALOG_URL,
"builtin://community": COMMUNITY_CATALOG_URL,
}
_BUILTIN_PACKAGED_SNAPSHOTS: dict[str, str] = {
"builtin://default": "catalog.json",
"builtin://community": "catalog.community.json",
}

HTTP_TIMEOUT_SECONDS = 10
Expand Down Expand Up @@ -99,15 +105,16 @@ def _validate_remote_url(source_id: str, url: str) -> None:
)


def _load_packaged_community_catalog() -> dict:
def _load_packaged_catalog(filename: str) -> dict:
"""Load a packaged bundle catalog snapshot from the wheel or repo root."""
core_pack = _locate_core_pack()
path = (
core_pack / "bundles" / "catalog.community.json"
core_pack / "bundles" / filename
if core_pack is not None
else _repo_root() / "bundles" / "catalog.community.json"
else _repo_root() / "bundles" / filename
)
if not path.is_file():
raise BundlerError(f"Bundled community catalog not found: {path}")
raise BundlerError(f"Bundled catalog not found: {path}")
return loads_json(path.read_text(encoding="utf-8"), origin=str(path))


Expand All @@ -132,14 +139,12 @@ def fetch(source: CatalogSource) -> dict:
scheme = parsed.scheme.lower()

if scheme == "builtin":
if url == "builtin://community":
if allow_network:
return _http_get_json(source.id, COMMUNITY_CATALOG_URL)
return _load_packaged_community_catalog()
payload = _BUILTIN_CATALOGS.get(url)
if payload is None:
repository_url = _BUILTIN_REPOSITORY_URLS.get(url)
if repository_url is None:
raise BundlerError(f"Unknown built-in catalog '{url}'.")
return payload
if allow_network:
return _http_get_json(source.id, repository_url)
return _load_packaged_catalog(_BUILTIN_PACKAGED_SNAPSHOTS[url])

if scheme == "file":
path = _file_url_to_path(parsed)
Expand Down
40 changes: 32 additions & 8 deletions src/specify_cli/bundler/services/primitives.py
Original file line number Diff line number Diff line change
Expand Up @@ -327,7 +327,38 @@ def is_installed(self, component: ComponentRef) -> bool:
return False

def install(self, component: ComponentRef) -> None:
if not self._allow_network and not self._is_bundled(component.id):
from ..._assets import _locate_bundled_workflow

bundled = _locate_bundled_workflow(component.id)
if bundled is not None:
workflow_file = bundled / "workflow.yml"
try:
from ...workflows.engine import WorkflowDefinition

definition = WorkflowDefinition.from_yaml(workflow_file)
except (OSError, ValueError) as exc:
raise BundlerError(
f"Failed to load bundled workflow '{component.id}': {exc}"
) from exc
if definition.id != component.id:
raise BundlerError(
f"Bundled workflow at {workflow_file} declares ID "
f"'{definition.id}', expected '{component.id}'."
)
_assert_pinned_version(
"Workflow", component.id, component.version, definition.version
)
from ... import workflow_add

with _chdir(self._root):
_delegate_command(
"install",
f"workflow '{component.id}'",
lambda: workflow_add(str(workflow_file), dev=True, from_url=None),
)
return

if not self._allow_network:
raise BundlerError(
f"Workflow '{component.id}' installs from a catalog and network "
f"access is disabled; re-run without --offline or install it first "
Expand Down Expand Up @@ -361,13 +392,6 @@ def _assert_pinned_version(self, component: ComponentRef) -> None:
"Workflow", component.id, component.version, info.get("version")
)

@staticmethod
def _is_bundled(workflow_id: str) -> bool:
# A workflow that ships with Spec Kit installs fully offline.
from ..._assets import _locate_bundled_workflow

return _locate_bundled_workflow(workflow_id) is not None

def remove(self, component: ComponentRef) -> None:
from ... import workflow_remove

Expand Down
14 changes: 14 additions & 0 deletions src/specify_cli/commands/bundle/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -439,6 +439,20 @@ def bundle_install(
)


@bundle_app.command("add")
Comment thread
markuswondrak marked this conversation as resolved.
def bundle_add(
bundle_id: str = typer.Argument(
...,
help="Bundle id (from the catalog stack) or a local path to a .zip "
"artifact, bundle directory, or bundle.yml",
),
integration: str = typer.Option(None, "--integration", help="Override integration"),
offline: bool = typer.Option(False, "--offline", help="Do not access the network"),
) -> None:
"""Install a bundle's full component set (alias for install)."""
return bundle_install(bundle_id=bundle_id, integration=integration, offline=offline)


@bundle_app.command("update")
def bundle_update(
bundle_id: str = typer.Argument(None, help="Bundle id, or omit with --all"),
Expand Down
Loading