Skip to content

Commit 730dcf5

Browse files
markuswondrakMarkusCopilotCopilot
authored
feat(bundles): first-party bugfix and assess bundles with bundled workflows (#4504)
* feat(bundles): add first-party bugfix and assess bundles with bundled workflows Implements #4495. Two first-party bundles pairing the bug and assess extensions with orchestrated, resumable pipelines: - workflows/bugfix: bug assess -> human review gate -> bug fix -> bug test - workflows/assess: intake -> research -> define -> shape -> decide -> verdict review gate (specify handoff stays manual) Both workflows are registered in workflows/catalog.json (bundled: true) and force-included in the wheel's core_pack like speckit. The bundles live in bundles/{bugfix,assess} and are listed in a new repo-shipped first-party catalog bundles/catalog.json (verified: true). The reserved builtin://default catalog source now resolves from that catalog — online from the repository, offline from the packaged wheel snapshot — so `specify bundle add bugfix|assess` resolves by id. Also adds `bundle add` as an alias for `bundle install`. Tests: workflow validation and structure guards, wheel force-include contract (mirrors test_wheel_bundled_presets.py), first-party catalog/manifest/pin consistency, and workflows/catalog.json <-> workflow.yml id/version/url consistency. Assisted-by: opencode (model: glm-5.3, autonomous) * fix(bundles): install bundled workflows offline Install bundled workflow components from their packaged workflow.yml instead of falling through to the network-backed workflow catalog. Validate the bundled workflow ID and the bundle's pinned version before delegating to the local workflow install path. Add coverage for offline first-party bundle installs, bundle add alias execution, version pin rejection, and command/input workflow contracts. Assisted-by: opencode (model: glm-5.3, autonomous) * fix(bundles): address review findings for first-party bundles - Correct README wording for bundle remove behavior (FR-022). - Parenthesize full assertion message in first-party catalog consistency test. - Add contract coverage for `bundle add <id>` from an empty directory, mocking first-party catalog/manifest HTTP responses and verifying fresh-project init plus bundled extension + workflow installation. Assisted-by: opencode (model: kimi-k2.7-code, supervised) * docs(bundles): show required workflow inputs in bundle README run commands The run examples showed bare `specify workflow run bugfix|assess`, which fails immediately because both workflows declare required inputs (`report`/`idea` and `slug`). Show the repeatable --input flags with the same example values as the corresponding extension READMEs, and note that inputs omitted from the command line are prompted interactively. Assisted-by: opencode (model: glm-5.2, autonomous) * test(events): pin the PowerShell launcher in the ps-variant argv test test_ps_variant_prefixed_with_powershell_launcher asserted that a real pwsh/powershell launcher resolves on PATH, so it failed on machines with neither installed (a pre-existing, environment-specific failure since #4340 removed the fake "pwsh" fallback — not related to the bundles work, but it blocked a clean local full-suite run). The test's contract is argv construction (launcher, -File, script), not launcher discovery, so monkeypatch shutil.which to a synthetic pwsh path — the same pattern the no-launcher sibling test already uses — keeping the test running (not skipped) on every platform. Assisted-by: opencode (model: glm-5.2, autonomous) * docs(bundles): clarify required workflow inputs Assisted-by: GitHub Copilot (model: gpt-5.6-luna, autonomous) * Remove unused import of Path Remove unused import of Path from test_bundled_bugfix_assess_workflows.py Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> * fix(bundle): forward refresh flag in add alias Assisted-by: GitHub Copilot (model: gpt-5.6-luna, autonomous) * fix(bundler): fall back to packaged catalog snapshot Assisted-by: GitHub Copilot (model: gpt-5.6-luna, autonomous) * fix(bundler): only fall back to snapshot on transient catalog failures The builtin catalog fallback previously caught every BundlerError from the online fetch, silently masking malformed JSON, oversized responses, and redirect/URL validation failures with the packaged snapshot. Classify only transient failures (connection errors, timeouts, HTTP 5xx) as unavailable, preserve validation failures, warn when a snapshot is used, and go quiet for deliberate --offline use. Cover both builtin catalogs in unit and integration tests, and assert the first-party catalog wheel mapping. Assisted-by: opencode (model: deepseek-v4.1-flash, autonomous) * fix bundler catalog fallback handling Preserve redirect-policy failures and treat HTTP 408/429 as transient when falling back to the packaged catalog snapshot, per review on #4504. Co-authored-by: markuswondrak <245696895+markuswondrak@users.noreply.github.com> Assisted-by: opencode (model: deepseek-v4.1-flash, autonomous) * test(bundler): exercise real redirect handler and refresh fallback docstring Address review nits on #4504: update the _CatalogUnavailable docstring to mention 408/429 as transient, and drive the redirect-policy test through the real _StripAuthOnRedirect handler instead of injecting the exception, with a snapshot present to prove the fallback is not taken. Assisted-by: opencode (model: deepseek-v4.1-flash, autonomous) * fix(bundler): keep TLS certificate failures out of snapshot fallback Address review on #4504: urllib wraps ssl.SSLCertVerificationError in URLError(reason=...) and some paths raise it directly as an OSError. Detect both before the transient transport branches and raise a hard BundlerError so a certificate-verification failure is never masked by the packaged snapshot. Add regression cases for the wrapped and direct forms. Assisted-by: opencode (model: deepseek-v4.1-flash, autonomous) * fix(bundler): treat truncated chunked responses as catalog-unavailable Address review on #4504: http.client.IncompleteRead is neither URLError nor OSError, so an interrupted chunked response fell through to the generic branch and became a hard BundlerError, defeating the packaged-snapshot fallback. Classify it as _CatalogUnavailable and add a regression test that raises it from response.read(). Assisted-by: opencode (model: deepseek-v4.1-flash, autonomous) --------- Co-authored-by: Markus <markus@example.com> Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: markuswondrak <245696895+markuswondrak@users.noreply.github.com>
1 parent 4362335 commit 730dcf5

24 files changed

Lines changed: 1404 additions & 69 deletions

‎bundles/assess/README.md‎

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
# Idea Assessment Bundle
2+
3+
A first-party GitHub Spec Kit bundle that installs an idea-triage pipeline before committing to Spec-Driven Development.
4+
5+
## What it provides
6+
7+
- **Assess extension** (`extensions/assess`) — the `speckit.assess.intake`, `speckit.assess.research`, `speckit.assess.define`, `speckit.assess.shape`, and `speckit.assess.decide` commands.
8+
- **Assess workflow** (`workflows/assess`) — a guided, resumable pipeline:
9+
1. `intake` the raw idea.
10+
2. `research` the evidence.
11+
3. `define` the problem.
12+
4. `shape` the concept.
13+
5. `decide` the verdict.
14+
6. `review-verdict` gate — approve to complete the assessment; reject to abort. A `go` verdict is then handed off manually to `/speckit.specify`.
15+
16+
## Install
17+
18+
```bash
19+
specify bundle install assess
20+
# or
21+
specify bundle add assess
22+
```
23+
24+
## Run the workflow
25+
26+
```bash
27+
specify workflow run assess \
28+
--input idea="Let users work offline and sync when they reconnect" \
29+
--input slug="offline-mode"
30+
```
31+
32+
Required inputs must be supplied with `--input`: `idea` and `slug`. The slug is used as the working directory under `.specify/assessments/<slug>/` for all artifacts.
33+
34+
## Remove
35+
36+
```bash
37+
specify bundle remove assess
38+
```
39+
40+
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.

‎bundles/assess/bundle.yml‎

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
schema_version: "1.0"
2+
3+
bundle:
4+
id: "assess"
5+
name: "Idea Assessment Pipeline"
6+
version: "1.0.0"
7+
role: "developer"
8+
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."
9+
author: "GitHub"
10+
license: "MIT"
11+
12+
requires:
13+
speckit_version: ">=0.9.0"
14+
tools: []
15+
mcp: []
16+
17+
provides:
18+
extensions:
19+
- id: "assess"
20+
version: "1.0.0"
21+
workflows:
22+
- id: "assess"
23+
version: "1.0.0"
24+
25+
tags: ["assessment", "discovery", "triage", "product"]

‎bundles/bugfix/README.md‎

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
# Bug Fix Bundle
2+
3+
A first-party GitHub Spec Kit bundle that installs an orchestrated bug-fixing pipeline.
4+
5+
## What it provides
6+
7+
- **Bug extension** (`extensions/bug`) — the `speckit.bug.assess`, `speckit.bug.fix`, and `speckit.bug.test` commands.
8+
- **Bugfix workflow** (`workflows/bugfix`) — a guided, resumable pipeline:
9+
1. `assess` the bug report.
10+
2. `review-assessment` gate — approve to proceed, reject to abort.
11+
3. `fix` the bug.
12+
4. `test` the fix.
13+
14+
## Install
15+
16+
```bash
17+
specify bundle install bugfix
18+
# or
19+
specify bundle add bugfix
20+
```
21+
22+
## Run the workflow
23+
24+
```bash
25+
specify workflow run bugfix \
26+
--input report="https://github.com/example/repo/issues/1234" \
27+
--input slug="callback-token"
28+
```
29+
30+
Required inputs must be supplied with `--input`: `report` and `slug`. The slug is used as the working directory under `.specify/bugs/<slug>/` for all artifacts.
31+
32+
## Remove
33+
34+
```bash
35+
specify bundle remove bugfix
36+
```
37+
38+
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.

‎bundles/bugfix/bundle.yml‎

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
schema_version: "1.0"
2+
3+
bundle:
4+
id: "bugfix"
5+
name: "Guided Bug Fix"
6+
version: "1.0.0"
7+
role: "developer"
8+
description: "Orchestrated bug triage: assess a bug report, review the assessment behind a human gate, apply the fix, and verify it with tests."
9+
author: "GitHub"
10+
license: "MIT"
11+
12+
requires:
13+
speckit_version: ">=0.9.0"
14+
tools: []
15+
mcp: []
16+
17+
provides:
18+
extensions:
19+
- id: "bug"
20+
version: "1.0.0"
21+
workflows:
22+
- id: "bugfix"
23+
version: "1.0.0"
24+
25+
tags: ["bug", "triage", "workflow", "qa"]

‎bundles/catalog.json‎

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
{
2+
"schema_version": "1.0",
3+
"updated_at": "2026-09-10T00:00:00Z",
4+
"catalog_url": "https://raw.githubusercontent.com/github/spec-kit/main/bundles/catalog.json",
5+
"bundles": {
6+
"bugfix": {
7+
"id": "bugfix",
8+
"name": "Guided Bug Fix",
9+
"version": "1.0.0",
10+
"role": "developer",
11+
"description": "Orchestrated bug triage: assess a bug report, review behind a human gate, apply the fix, and verify with tests.",
12+
"author": "GitHub",
13+
"license": "MIT",
14+
"download_url": "https://raw.githubusercontent.com/github/spec-kit/main/bundles/bugfix/bundle.yml",
15+
"repository": "https://github.com/github/spec-kit",
16+
"requires": {
17+
"speckit_version": ">=0.9.0"
18+
},
19+
"provides": {
20+
"extensions": 1,
21+
"presets": 0,
22+
"steps": 0,
23+
"workflows": 1
24+
},
25+
"tags": ["bug", "triage", "workflow", "qa"],
26+
"verified": true
27+
},
28+
"assess": {
29+
"id": "assess",
30+
"name": "Idea Assessment Pipeline",
31+
"version": "1.0.0",
32+
"role": "developer",
33+
"description": "Idea triage before Spec-Driven Development: intake, research, define, shape, decide with a verdict review gate; surviving ideas hand off manually to specify.",
34+
"author": "GitHub",
35+
"license": "MIT",
36+
"download_url": "https://raw.githubusercontent.com/github/spec-kit/main/bundles/assess/bundle.yml",
37+
"repository": "https://github.com/github/spec-kit",
38+
"requires": {
39+
"speckit_version": ">=0.9.0"
40+
},
41+
"provides": {
42+
"extensions": 1,
43+
"presets": 0,
44+
"steps": 0,
45+
"workflows": 1
46+
},
47+
"tags": ["assessment", "discovery", "triage", "product"],
48+
"verified": true
49+
}
50+
}
51+
}

‎docs/reference/bundles.md‎

Lines changed: 22 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,24 @@ These demonstrate packaging a role-based setup, not filled generated feature
1111
specs; for end-to-end usage examples, see
1212
[community walkthroughs](../community/walkthroughs.md).
1313

14+
## First-party Bundles
15+
16+
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.
17+
18+
| Bundle | Role | Components | Use case |
19+
| --------- | ----------- | ----------------------------------------------------- | --------------------------------------- |
20+
| `bugfix` | `developer` | `bug` extension + `bugfix` workflow | Guided assess → gate → fix → test |
21+
| `assess` | `developer` | `assess` extension + `assess` workflow | Idea triage before Spec-Driven Development |
22+
23+
Install a first-party bundle the same way you install any bundle (`add` is an alias for `install`):
24+
25+
```bash
26+
specify bundle install bugfix
27+
specify bundle add assess
28+
```
29+
30+
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.
31+
1432
## Search Available Bundles
1533

1634
```bash
@@ -146,7 +164,10 @@ If your bundle references components from non-default catalogs, document those c
146164

147165
## Manage Catalog Sources
148166

149-
Bundles are discovered through a priority-ordered stack of catalog sources (project, user, and built-in scopes).
167+
Bundles are discovered through a priority-ordered stack of catalog sources (project, user, and built-in scopes). The built-in sources are:
168+
169+
- `builtin://default` — first-party bundles shipped in `bundles/catalog.json` (`bugfix`, `assess`, ...), install-allowed.
170+
- `builtin://community` — community submissions in `bundles/catalog.community.json`, discovery-only.
150171

151172
Each source has an install policy. `install-allowed` sources can be installed
152173
from; `discovery-only` sources appear in `search` and `info` but refuse

‎pyproject.toml‎

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -45,12 +45,15 @@ packages = ["src/specify_cli"]
4545
"extensions/agent-context" = "specify_cli/core_pack/extensions/agent-context"
4646
"extensions/assess" = "specify_cli/core_pack/extensions/assess"
4747
"extensions/bug" = "specify_cli/core_pack/extensions/bug"
48-
# Bundled workflows (auto-installed during `specify init`)
48+
# Bundled workflows (`specify init` installs only `speckit`; `bugfix`/`assess` are opt-in via first-party bundles)
4949
"workflows/speckit" = "specify_cli/core_pack/workflows/speckit"
50+
"workflows/bugfix" = "specify_cli/core_pack/workflows/bugfix"
51+
"workflows/assess" = "specify_cli/core_pack/workflows/assess"
5052
# Bundled presets (installable via `specify preset add <name>` or `specify init --preset <name>`)
5153
"presets/lean" = "specify_cli/core_pack/presets/lean"
5254
"presets/constitution-sync" = "specify_cli/core_pack/presets/constitution-sync"
53-
# Community bundle catalog snapshot (used for offline discovery)
55+
# Bundle catalog snapshots (used for offline discovery)
56+
"bundles/catalog.json" = "specify_cli/core_pack/bundles/catalog.json"
5457
"bundles/catalog.community.json" = "specify_cli/core_pack/bundles/catalog.community.json"
5558

5659
[project.optional-dependencies]

‎src/specify_cli/authentication/http.py‎

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -65,9 +65,13 @@ def _hostname_in_hosts(hostname: str, hosts: tuple[str, ...]) -> bool:
6565
RedirectValidator = Callable[[str, str], None]
6666

6767

68+
class RedirectPolicyError(urllib.error.URLError):
69+
"""A redirect rejected because it violates the client's security policy."""
70+
71+
6872
def _validate_strict_redirect(old_url: str, new_url: str) -> None:
6973
if not is_safe_download_redirect(old_url, new_url):
70-
raise urllib.error.URLError(
74+
raise RedirectPolicyError(
7175
f"unsafe redirect to {new_url}: target must use HTTPS with a hostname, "
7276
"must not enter a local target from a remote host, and may use HTTP only "
7377
"within loopback (for example localhost, 127.0.0.1, ::1)"
@@ -100,7 +104,7 @@ def redirect_request(self, req, fp, code, msg, headers, newurl):
100104
except ValueError as exc:
101105
# Malformed redirect target (e.g. unterminated IPv6 bracket).
102106
# Surface as URLError so callers' download error handling applies.
103-
raise urllib.error.URLError(f"malformed redirect URL: {exc}") from exc
107+
raise RedirectPolicyError(f"malformed redirect URL: {exc}") from exc
104108

105109
if self._redirect_validator is not None:
106110
self._redirect_validator(req.full_url, newurl)

0 commit comments

Comments
 (0)