Skip to content

Commit 1384caa

Browse files
authored
Merge branch 'main' into add-contextforge-mcp-extension-313957706d8efa38
Assisted-by: GitHub Copilot (model: claude-sonnet-4.5, autonomous)
2 parents 2342d8a + 86d769b commit 1384caa

19 files changed

Lines changed: 554 additions & 54 deletions

File tree

CHANGELOG.md

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,21 @@
22

33
<!-- insert new changelog below this comment -->
44

5+
## [0.12.12] - 2026-07-13
6+
7+
### Changed
8+
9+
- fix(extensions): set-priority repairs corrupted boolean priority (#3268)
10+
- fix(presets): set-priority repairs corrupted boolean priority (#3269)
11+
- fix(workflows): engine loop cap ignores bool max_iterations (#3270)
12+
- docs(bundles): document --integration on 'bundle update' (#3271)
13+
- fix(workflows): harden catalog.py against mis-shaped registry & non-string fields (#3375)
14+
- Add Verify Review Ship extension to community catalog (#3450)
15+
- fix(bundle): reject file:// / local download_url — catalog URLs are HTTPS-only (#3344)
16+
- fix(extensions): handle prefix-colliding env vars in _get_env_config (#3350)
17+
- docs: document copilot skills mode (--skills) and markdown deprecation (#3313)
18+
- chore: release 0.12.11, begin 0.12.12.dev0 development (#3460)
19+
520
## [0.12.11] - 2026-07-10
621

722
### Changed

docs/community/extensions.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -149,6 +149,7 @@ The following community-contributed extensions are available in [`catalog.commun
149149
| Token Economy | Token routing, measured savings, and context audit workflows | `process` | Read+Write | [spec-kit-token-economy](https://github.com/formin/spec-kit-token-economy) |
150150
| V-Model Extension Pack | Enforces V-Model paired generation of development specs and test specs with full traceability | `docs` | Read+Write | [spec-kit-v-model](https://github.com/leocamello/spec-kit-v-model) |
151151
| Verify Extension | Post-implementation quality gate that validates implemented code against specification artifacts | `code` | Read-only | [spec-kit-verify](https://github.com/ismaelJimenez/spec-kit-verify) |
152+
| Verify Review Ship | Adds post-implementation verify, review, and ship readiness gates to Spec Kit workflows | `process` | Read-only | [spec-kit-verify-review-ship](https://github.com/cadugevaerd/spec-kit-verify-review-ship) |
152153
| Verify Tasks Extension | Detect phantom completions: tasks marked [X] in tasks.md with no real implementation | `code` | Read-only | [spec-kit-verify-tasks](https://github.com/datastone-inc/spec-kit-verify-tasks) |
153154
| Version Guard | Verify tech stack versions against live npm registries before planning and implementation | `process` | Read-only | [spec-kit-version-guard](https://github.com/KevinBrown5280/spec-kit-version-guard) |
154155
| What-if Analysis | Preview the downstream impact (complexity, effort, tasks, risks) of requirement changes before committing to them | `visibility` | Read-only | [spec-kit-whatif](https://github.com/DevAbdullah90/spec-kit-whatif) |

docs/reference/bundles.md

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -51,10 +51,11 @@ If the current directory is not yet a Spec Kit project, `install` initializes on
5151
specify bundle update [<bundle_id>]
5252
```
5353

54-
| Option | Description |
55-
| ------------ | ------------------------------------ |
56-
| `--all` | Update every installed bundle |
57-
| `--offline` | Do not access the network |
54+
| Option | Description |
55+
| ---------------- | --------------------------------------------------------------------------------------------------------------------- |
56+
| `--all` | Update every installed bundle |
57+
| `--integration` | Override the integration used when refreshing components; applied only when the project's active integration can't be determined |
58+
| `--offline` | Do not access the network |
5859

5960
Re-resolves a bundle and **refreshes** its components through each primitive's update path, bringing already-installed components up to the bundle's newly pinned versions while preserving primitive-level overrides (such as preset priority). Provide a bundle id, or use `--all` to update everything installed.
6061

docs/reference/integrations.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ The Specify CLI supports a wide range of AI coding agents. When you run `specify
1818
| [Firebender](https://firebender.com/) | `firebender` | IDE-based agent for Android Studio / IntelliJ |
1919
| [Forge](https://forgecode.dev/) | `forge` | |
2020
| [Gemini CLI](https://github.com/google-gemini/gemini-cli) | `gemini` | |
21-
| [GitHub Copilot](https://code.visualstudio.com/) | `copilot` | |
21+
| [GitHub Copilot](https://code.visualstudio.com/) | `copilot` | Defaults to legacy markdown mode: `.agent.md` command files under `.github/agents/`, companion `.prompt.md` files under `.github/prompts/`, and a `.vscode/settings.json` merge. Pass `--integration-options="--skills"` to scaffold skills as `speckit-<command>/SKILL.md` under `.github/skills/` instead. Legacy markdown mode is deprecated and will stop being the default in a future release. |
2222
| [Goose](https://goose-docs.ai/) | `goose` | Uses YAML recipe format in `.goose/recipes/` |
2323
| [Hermes](https://github.com/NousResearch/hermes-agent) | `hermes` | Skills-based integration; installs skills globally into `~/.hermes/skills/` |
2424
| [IBM Bob](https://www.ibm.com/products/bob) | `bob` | IDE-based agent |
@@ -219,6 +219,7 @@ Some integrations accept additional options via `--integration-options`:
219219
| ----------- | ------------------- | -------------------------------------------------------------- |
220220
| `generic` | `--commands-dir` | Required. Directory for command files |
221221
| `kimi` | `--migrate-legacy` | Migrate legacy `.kimi/skills/` installs to `.kimi-code/skills/` (including dotted→hyphenated skill naming, e.g. `speckit.xxx``speckit-xxx`) |
222+
| `copilot` | `--skills` | Scaffold commands as agent skills (`speckit-<command>/SKILL.md` under `.github/skills/`, invoked as `/speckit-<command>`) instead of the default legacy markdown mode (`.github/agents/*.agent.md` plus `.github/prompts/*.prompt.md` and a `.vscode/settings.json` merge). Without this flag, install warns that legacy markdown mode is deprecated. |
222223

223224
Example:
224225

extensions/catalog.community.json

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4391,6 +4391,40 @@
43914391
"created_at": "2026-03-03T00:00:00Z",
43924392
"updated_at": "2026-04-09T00:00:00Z"
43934393
},
4394+
"verify-review-ship": {
4395+
"name": "Verify Review Ship",
4396+
"id": "verify-review-ship",
4397+
"description": "Adds post-implementation verify, review, and ship readiness gates to Spec Kit workflows.",
4398+
"author": "Carlos Eduardo Gevaerd Araujo",
4399+
"version": "0.1.0",
4400+
"download_url": "https://github.com/cadugevaerd/spec-kit-verify-review-ship/archive/refs/tags/v0.1.0.zip",
4401+
"repository": "https://github.com/cadugevaerd/spec-kit-verify-review-ship",
4402+
"homepage": "https://github.com/cadugevaerd/spec-kit-verify-review-ship",
4403+
"documentation": "https://github.com/cadugevaerd/spec-kit-verify-review-ship/blob/main/README.md",
4404+
"changelog": "https://github.com/cadugevaerd/spec-kit-verify-review-ship/blob/main/CHANGELOG.md",
4405+
"license": "MIT",
4406+
"category": "process",
4407+
"effect": "read-only",
4408+
"requires": {
4409+
"speckit_version": ">=0.1.0"
4410+
},
4411+
"provides": {
4412+
"commands": 3,
4413+
"hooks": 1
4414+
},
4415+
"tags": [
4416+
"quality",
4417+
"review",
4418+
"shipping",
4419+
"workflow",
4420+
"testing"
4421+
],
4422+
"verified": false,
4423+
"downloads": 0,
4424+
"stars": 0,
4425+
"created_at": "2026-07-10T00:00:00Z",
4426+
"updated_at": "2026-07-10T00:00:00Z"
4427+
},
43944428
"verify-tasks": {
43954429
"name": "Verify Tasks Extension",
43964430
"id": "verify-tasks",

pyproject.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
[project]
22
name = "specify-cli"
3-
version = "0.12.12.dev0"
3+
version = "0.12.13.dev0"
44
description = "Specify CLI, part of GitHub Spec Kit. A tool to bootstrap your projects for Spec-Driven Development (SDD)."
55
readme = "README.md"
66
requires-python = ">=3.11"

src/specify_cli/commands/bundle/__init__.py

Lines changed: 37 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -746,11 +746,16 @@ def _resolve_manifest_path(path: Path | None) -> Path:
746746
def _download_manifest(resolved, *, offline: bool):
747747
"""Resolve a bundle's manifest from its catalog ``download_url``.
748748
749-
Local/``file://`` URLs always work offline and may point at a ``.zip``
750-
artifact, a bundle directory, or a ``bundle.yml`` (handled by
751-
:func:`_local_manifest_source`). Remote ``https://`` URLs are fetched with
752-
the shared authenticated, redirect-validated HTTP client, and only when not
753-
``--offline``.
749+
Catalog ``download_url``s are HTTPS-only (``http`` allowed for localhost),
750+
matching the extensions/presets/workflows catalog systems. Remote URLs are
751+
fetched with the shared authenticated, redirect-validated HTTP client, and
752+
only when not ``--offline``.
753+
754+
Local and ``file://`` sources are intentionally not resolved here: to
755+
install a bundle from disk, pass the path positionally
756+
(``specify bundle install ./path/to/bundle.yml`` — a bundle directory or a
757+
``.zip`` artifact also works), which :func:`_local_manifest_source` handles
758+
before catalog resolution and which never touches ``download_url``.
754759
"""
755760
from urllib.parse import urlparse
756761

@@ -763,26 +768,35 @@ def _download_manifest(resolved, *, offline: bool):
763768
parsed = urlparse(url)
764769
scheme = parsed.scheme.lower()
765770

766-
# On Windows an absolute path like ``C:\bundle.yml`` parses with a
767-
# single-letter ``scheme``; treat it as a local file, not a URL scheme.
771+
# ``file://`` URLs and bare filesystem paths (including Windows drive paths
772+
# like ``C:\bundle.yml``, which urlparse reads as a single-letter scheme)
773+
# are not valid catalog download URLs. Catalog URLs are HTTPS-only across
774+
# every catalog system; installing from disk is done by passing the path
775+
# positionally, which never reaches URL resolution. Give an actionable
776+
# error rather than accepting a scheme the rest of the codebase rejects.
768777
if scheme in ("", "file") or re.match(r"^[A-Za-z]:[\\/]", url):
769-
local = Path(parsed.path if scheme == "file" else url)
770-
manifest = _local_manifest_source(str(local))
771-
if manifest is None:
772-
raise BundlerError(f"Bundle manifest not found: {local}")
773-
return manifest
774-
775-
if scheme in ("http", "https"):
776-
if offline:
777-
raise BundlerError(
778-
f"Network access disabled; cannot download bundle '{resolved.entry.id}' "
779-
f"from {url}."
780-
)
781-
return _download_remote_manifest(resolved.entry.id, url)
778+
raise BundlerError(
779+
f"Catalog entry '{resolved.entry.id}' has a non-HTTP(S) download_url "
780+
f"({url}); catalog download URLs must be HTTPS (http for localhost) — "
781+
"a file:// URL, a local filesystem path, or a scheme-less value "
782+
"(e.g. 'example.com/bundle.zip') is not accepted. "
783+
"To install a bundle from disk, pass the path directly: "
784+
"'specify bundle install <path-to-bundle.yml | bundle-dir | .zip>'."
785+
)
782786

783-
raise BundlerError(
784-
f"Unsupported download_url scheme for bundle '{resolved.entry.id}': {url}"
785-
)
787+
# Validate the scheme/host *before* the offline gate so an invalid or
788+
# non-HTTPS download_url reports the real problem in every mode, rather
789+
# than a misleading "Network access disabled" under --offline.
790+
# (_download_remote_manifest re-checks this, but only once network access
791+
# is permitted.) HTTPS-only, http allowed for localhost.
792+
_require_https(f"bundle '{resolved.entry.id}'", url)
793+
794+
if offline:
795+
raise BundlerError(
796+
f"Network access disabled; cannot download bundle '{resolved.entry.id}' "
797+
f"from {url}."
798+
)
799+
return _download_remote_manifest(resolved.entry.id, url)
786800

787801

788802
def _require_https(label: str, url: str) -> None:

src/specify_cli/extensions/__init__.py

Lines changed: 20 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -2755,18 +2755,32 @@ def _get_env_config(self) -> Dict[str, Any]:
27552755
if not key.startswith(prefix):
27562756
continue
27572757

2758-
# Remove prefix and split into parts
2759-
config_path = key[len(prefix) :].lower().split("_")
2758+
# Remove prefix and split into parts. Drop empty components from a
2759+
# malformed name (e.g. ``SPECKIT_<EXT>_`` with no key, or
2760+
# consecutive underscores ``SPECKIT_X__Y``) so we never create an
2761+
# entry under an empty key.
2762+
config_path = [p for p in key[len(prefix) :].lower().split("_") if p]
2763+
if not config_path:
2764+
continue
27602765

2761-
# Build nested dict
2766+
# Build nested dict. Two env vars can collide on a prefix, e.g.
2767+
# SPECKIT_X_CONNECTION=a and SPECKIT_X_CONNECTION_URL=b. Guard the
2768+
# walk so a colliding scalar is replaced by a dict (deeper/more
2769+
# specific vars win) instead of being indexed into — which raised
2770+
# TypeError ('str' object does not support item assignment) — and
2771+
# guard the leaf so a scalar processed after the nested var does
2772+
# not clobber the nested dict. Order-independent: both insertion
2773+
# orders yield {'connection': {'url': ...}}. Nested-wins mirrors
2774+
# _merge_configs' dict-preserving semantics.
27622775
current = env_config
27632776
for part in config_path[:-1]:
2764-
if part not in current:
2777+
if not isinstance(current.get(part), dict):
27652778
current[part] = {}
27662779
current = current[part]
27672780

2768-
# Set the final value
2769-
current[config_path[-1]] = value
2781+
# Set the final value, unless a nested dict already occupies it.
2782+
if not isinstance(current.get(config_path[-1]), dict):
2783+
current[config_path[-1]] = value
27702784

27712785
return env_config
27722786

src/specify_cli/extensions/_commands.py

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1566,7 +1566,14 @@ def extension_set_priority(
15661566
raw_priority = metadata.get("priority")
15671567
# Only skip if the stored value is already a valid int equal to requested priority
15681568
# This ensures corrupted values (e.g., "high") get repaired even when setting to default (10)
1569-
if isinstance(raw_priority, int) and raw_priority == priority:
1569+
# A bool is an int in Python (isinstance(True, int) is True), so exclude it explicitly —
1570+
# mirroring normalize_priority's bool guard — otherwise a corrupted True/False priority
1571+
# equals 1/0 here and is never repaired.
1572+
if (
1573+
isinstance(raw_priority, int)
1574+
and not isinstance(raw_priority, bool)
1575+
and raw_priority == priority
1576+
):
15701577
console.print(f"[yellow]Extension '{_escape_markup(str(display_name))}' already has priority {priority}[/yellow]")
15711578
raise typer.Exit(0)
15721579

src/specify_cli/presets/_commands.py

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -469,7 +469,14 @@ def preset_set_priority(
469469
raw_priority = metadata.get("priority")
470470
# Only skip if the stored value is already a valid int equal to requested priority
471471
# This ensures corrupted values (e.g., "high") get repaired even when setting to default (10)
472-
if isinstance(raw_priority, int) and raw_priority == priority:
472+
# A bool is an int in Python (isinstance(True, int) is True), so exclude it explicitly —
473+
# mirroring normalize_priority's bool guard — otherwise a corrupted True/False priority
474+
# equals 1/0 here and is never repaired.
475+
if (
476+
isinstance(raw_priority, int)
477+
and not isinstance(raw_priority, bool)
478+
and raw_priority == priority
479+
):
473480
console.print(f"[yellow]Preset '{preset_id}' already has priority {priority}[/yellow]")
474481
raise typer.Exit(0)
475482

0 commit comments

Comments
 (0)