Skip to content

chore: merge upstream 0.15.2 + 13 post-release commits, bump to 0.15.2+adlc2 - #107

Merged
kanfil merged 31 commits into
mainfrom
chore/upstream-merge-0.15.2
Aug 5, 2026
Merged

chore: merge upstream 0.15.2 + 13 post-release commits, bump to 0.15.2+adlc2#107
kanfil merged 31 commits into
mainfrom
chore/upstream-merge-0.15.2

Conversation

@kanfil

@kanfil kanfil commented Aug 4, 2026

Copy link
Copy Markdown
Member

Combined upstream merge — 0.15.1 → 0.15.2 → post-0.15.2 tip

This PR covers two merge rounds (adlc1 was never tagged/released; the release tag after merge will be agentic-sdlc-v0.15.2+adlc2).

PR updated on behalf of @kanfil by opencode (model: glm-5.2, supervised).


Round 2 (new) — post-0.15.2 tip, bump to 0.15.2+adlc2

Merges upstream main (ab468c4d..03d71b33) — 13 commits, no new release tag (upstream on 0.15.3.dev0) → suffix bump only.

New upstream features

  • feat(copilot): default integration to skills github/spec-kit#3976feat(copilot): default integration to skills. Copilot now defaults to the skills layout (speckit-<name>/SKILL.md under .github/skills/); commands layout (.agent.md + .prompt.md + .vscode/settings.json) is opt-in via --integration-options="--commands". Fork adaptations: is_skills_mode() checks both spec-/speckit- prefixes (fork skills use spec-* when presets are active); 4 fork customizations (resolve_command_alias, run_and_tee, _get_command_prefix() ×2) re-applied onto upstream's rewritten module; build_command_invocation() now canonicalizes bare command names ("plan"speckit.plan) before alias resolution (fixes /plan/{prefix}-plan).
  • feat(events): context injection for opencode and JSON-envelope agent hooks github/spec-kit#3934feat(events): context injection for opencode and JSON-envelope agent hooks (authored by fork maintainer upstream; auto-merged clean). opencode session_start/user_prompt_submit injection via TS plugin; JSON-envelope wrapping for Gemini/Tabnine/Qwen/Devin/Copilot/Cursor.

Upstream fixes (all auto-merged clean)

github#3955 non-UTF-8 preset registry · github#3963 non-UTF-8 config.toml on hook install/teardown · github#3962 unreadable staged backup as conflict · github#3980 non-string requires.speckit_version · github#3960 reinstall rejected when kept config unreadable · github#3957 None for unparseable script command · migration target-options validation in _migrate_commands.py

Community catalog / docs

github#3982 TDD extension (note: fork has its own bundled tdd — separate) · github#3983 Charter v0.5.1 · github#3981 Archive v1.1.0 · github#3985 agent PR-review prioritization · github#3987 July newsletter

Conflict resolution — 5 conflicts

File Resolution
AGENTS.md Adopted upstream's "Optional overrides" section + "Opening pull requests" PR-prioritization subsection; preserved fork header/SPECKIT markers
integrations/copilot/__init__.py Took upstream github#3976 rewrite as base; re-applied 4 fork customizations + made is_skills_mode() prefix-aware
tests/integrations/test_cli.py Skills-default assertions adapted to fork naming
tests/integrations/test_integration_copilot.py build_command_invocation tests adapted to alias-aware naming
tests/integrations/test_integration_subcommand.py Copilot switch/upgrade tests adapted to fork naming

Auto-merged clean: events.py, extensions/__init__.py, presets/__init__.py, base.py, _migrate_commands.py, integration_runtime.py. Fork modules untouched. No templates/ changes → no preset porting.

Bonus fix (pre-existing, reported by user during this round)

  • commands/init.py: typer.confirm() could raise OSError (e.g. BrokenPipeError) escaping both Click's and the fork's except (typer.Abort, EOFError) handlers → crash on specify init .. Added OSError to the handler.

Verification

  • ✅ Ruff clean (ruff@0.15.0)
  • ✅ 175 tests: test_events.py + test_integration_copilot.py + copilot-related test_cli.py
  • ✅ 1200 tests: test_extensions.py, test_presets.py, test_extension_skills.py, test_extra_args.py, test_integration_state.py (13 skipped, 0 failures)
  • ✅ Targeted test_integration_subcommand.py switch tests (full file is slow; CI covers rest)

Round 1 — 0.15.1 → 0.15.2, bump to 0.15.2+adlc1

Merges github/spec-kit upstream main (tip ab468c4d) — 14 commits since the last merge base d1e86f63, including the 0.15.2 release (github#3953) and 3 post-release fixes (github#3899, github#3857, github#3897).

New upstream features

New upstream hardening

Conflict resolution

1 conflict (pyproject.toml) — kept fork name/description, version → 0.15.2+adlc1 (base reset, counter reset).

All semantic hotspots auto-merged cleanly: __init__.py, agents.py, extensions/__init__.py, extensions/_commands.py, presets/__init__.py, commands/init.py, commands/bundle/__init__.py, events.py, event.py, workflows/engine.py, workflows/steps/init/__init__.py, _agent_config.py. Fork modules untouched. No templates/ changes upstream → no preset command porting needed.

Verification (round 1)

  • ✅ Ruff clean (ruff@0.15.0)
  • specify --version0.15.2+adlc1
  • pytest --co collects 6578 tests cleanly
  • ⏳ Full test suite deferred to CI

Hygiene

  • CHANGELOG.md0.15.2+adlc1 + 0.15.2+adlc2 entries at top
  • FORK.md — version-history rows for both rounds

Note on the working tree

The fork's main has unrelated uncommitted changes under evals/ (stashed during merge, restored after) and an untracked .adlc/. Neither is part of this PR. A local agent-context-managed refresh of AGENTS.md's SPECKIT section is also uncommitted and not part of this PR.

marcelsafin and others added 29 commits August 3, 2026 08:40
Assisted-by: GitHub Copilot (model: GPT-5.6 Sol, autonomous)

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…ithub#2000)

* feat(extensions): scaffold config templates on extension add/enable

Deploy an extension's provides.config templates into .specify/ when the
extension is added or enabled. Existing files are never overwritten, so
user customizations are preserved.

Addresses the review on github#2000:
- ExtensionManifest.config returns [] unless provides.config is a list of
  dicts, so a malformed manifest cannot crash callers.
- scaffold_config returns a consistent (deployed, skipped_existing, failed)
  tuple on every path, including a missing manifest.
- Template paths must resolve inside the extension dir and targets inside
  .specify/; symlinks and non-regular files are rejected.
- Callers distinguish "already exists (preserved)" from "not scaffolded",
  and extension_enable no longer crashes on a corrupt manifest.
- Tests cover traversal, absolute paths, symlinks, directory templates,
  malformed provides.config, and the missing-manifest tuple shape.

Ported onto the extensions package introduced by github#3014: the manager and
manifest changes land in extensions/__init__.py and the CLI wiring in
extensions/_commands.py.

* fix(extensions): deploy config where it is read, and contain the write

Addresses @Copilot's review.

Config now lands in .specify/extensions/<id>/ rather than the .specify/
root. ConfigManager._get_project_config() reads
.specify/extensions/<id>/<id>-config.yml, and the bundled scripts and
READMEs use the same path, so a scaffolded git-config.yml was being
written somewhere the git extension never looks.

Containment is checked component by component before .specify is used as
the root. Resolving it first and trusting the result let a symlinked
component point outside the project, after which every target satisfied
relative_to and copy2 wrote externally. This matches the project
safe-write path in shared_infra.

mkdir moved inside the OSError handler. A nested target like
foo/config.yml raised out of scaffolding when its parent could not be
created, and on extension add that happened after the extension was
already installed.

The 'Configuration may be required' warning is now conditional. It ran
unconditionally after the scaffolding block, so it contradicted the
success output directly above it and fired for extensions with no
provides.config at all.

Tests cover the corrected location, a symlinked config root, and an
uncreatable nested target.

* fix(extensions): only scaffold config targets that removal preserves

remove(keep_config=True) rmtree's every subdirectory and keeps only top-level
-config.yml / -config.local.yml files; the backup path globs the same
top-level pattern. Scaffolding a nested or differently-named target therefore
handed the user a file that 'extension add --force' silently deleted and
replaced with the template default, losing customization.

Constrain scaffold targets to that convention rather than widening four
removal paths.

---------

Co-authored-by: Matt Van Horn <455140+mvanhorn@users.noreply.github.com>
Add adrkit extension submitted by @mbeacom to:
- extensions/catalog.community.json (alphabetical order)
- docs/community/extensions.md community extensions table

Closes github#3942


Assisted-by: GitHub Copilot (model: claude-sonnet-4.6, autonomous)

Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…ON_DEFAULT (github#3952)

* feat: allow overriding default init integration via SPECKIT_INTEGRATION_DEFAULT

Resolve the non-interactive/init default integration from the
SPECKIT_INTEGRATION_DEFAULT environment variable, fitting the existing
SPECKIT_INTEGRATION_* namespace. Falls back to the hardcoded "copilot"
default when unset, and warns to stderr (rather than silently falling
back) when the value is not a registered integration key.

Wires the resolver into specify init (interactive prompt default and
non-interactive fallback), the init workflow step, and the bundle init
default. Adds unit and CLI tests and documents the variable.

Closes github#3939

Assisted-by: GitHub Copilot (model: claude-opus-4.8, autonomous)
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Copilot-Session: eecda55f-fa13-42f7-99bf-bfb0bb8565a0

* test: cover env-var default wiring for picker, workflow step, and bundle

Address PR review: add regression tests so each SPECKIT_INTEGRATION_DEFAULT
wiring site cannot silently revert to the hardcoded constant.

- init.py: interactive picker receives the resolved key as default_key.
- workflow init step: no step/workflow default + env var drives output
  integration and argv.
- bundle _resolve_init_integration: env-var default applies when unspecified,
  while explicit override and manifest-declared integration still win.

Assisted-by: GitHub Copilot (model: claude-opus-4.8, autonomous)
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Copilot-Session: eecda55f-fa13-42f7-99bf-bfb0bb8565a0

---------

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Copilot-Session: eecda55f-fa13-42f7-99bf-bfb0bb8565a0
Assisted-by: GitHub Copilot (model: GPT-5.6 Sol, autonomous)

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Assisted-by: GitHub Copilot (model: GPT-5.6 Sol, autonomous)

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…fault on an explicit null (github#3889)

The step documents the default twice:

  class docstring: "Because workflows run unattended, the step defaults to
                    ``--ignore-agent-tools``"
  field docs:      "Skip checks for the coding agent CLI (defaults to ``true``)"

It implements that with `config.get("ignore_agent_tools", True)`, which
applies the default only when the key is ABSENT. A bare
`ignore_agent_tools:` in YAML parses to None, and `_resolve_bool(None)`
returns False:

  key ABSENT                -> True   flag emitted: YES
  bare ignore_agent_tools:  -> False  flag emitted: NO   <-- bug
  explicit true             -> True   flag emitted: YES
  explicit false            -> False  flag emitted: NO

So the flag is dropped, `specify init` re-runs the agent-CLI presence
check, and an unattended run fails with "Agent Detection Error" for any
integration whose CLI is not installed on the runner.

Normalize an explicit null to the default, mirroring the while/do-while
`max_iterations` handling.

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
…hub#3856)

* fix: narrow exception in invoke separator resolution and add regression test

Narrow 'except Exception' to 'except (ImportError, ValueError, KeyError)'
in register_commands() invoke separator resolution. Add regression test
that verifies TypeError propagates instead of being silently swallowed.

* fix: remove duplicate pass statement in agents.py

Remove redundant second pass statement in the except block
for invoke separator resolution. The narrowed exception handler
now has a single clean pass statement.

Assisted-by: GitHub Copilot (model: mimo-v2-free, supervised)
…ithub#3943)

* fix(manifests): reject non-string metadata instead of crashing on it

`ExtensionManifest` and `PresetManifest` checked only key PRESENCE for
`id`/`name`/`version`/`description`, then fed the values straight to
`re.match()` and `packaging.Version()`. Both raise a bare `TypeError` on a
non-string, which is neither `ValidationError` nor `PresetValidationError`,
so it escaped every caller that already handles a malformed manifest.

YAML makes this an easy authoring slip rather than a contrived one: an
unquoted `version: 1.0` parses as a float and `id: 2` as an int.

The user-visible symptom is the one the in-tree comment above the section
guards was written to prevent (github#3898 for presets, and its extension twin):
`list_installed()` degrades a bad manifest to "⚠️ Corrupted extension" but
catches only the domain error, so a single bad manifest made
`specify extension list` / `specify preset list` exit 1 with a raw
traceback and *no output at all* — hiding every healthy extension/preset
too, not just the broken one.

Also unguarded on the same path:
- extension `provides.commands[].name` → `TypeError` from the command-name
  pattern match. The sibling `file` field was already safe, since
  `relative_extension_path_violation()` rejects a non-string.
- preset `provides.templates[].name`/`.file` → `TypeError` from `re.match`
  and `os.path.normpath` respectively.

The third manifest twin, `IntegrationDescriptor`, is already hardened: it
type-checks the same four fields and catches `TypeError` alongside
`InvalidVersion`. This brings the other two in line with it.

Tests: 68 added across both suites, covering each field against float, int,
None, list, dict, and bool, plus an end-to-end guard per manifest type
asserting a healthy entry still lists while the bad one degrades. All 68
fail with the source change reverted.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* Potential fix for pull request finding

Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>

---------

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
…remove (github#3929)

* fix(presets): restore core skills instead of deleting them on preset remove

Skill restoration looked for core command templates under
.specify/templates/commands, a directory specify init never populates
in real projects. Since that lookup always missed, presets overriding
a core command (e.g. speckit.plan) had their skill deleted outright on
removal instead of restored — the actual core templates live in the
bundled core_pack (wheel install) or the repo-root templates/ tree.

Restoration now falls back to that bundled location, gated behind a
restore_from_bundled_core flag so the existing "retire a stale skill
superseded by a command-mode winner" path keeps deleting rather than
resurrecting a duplicate skill.

Fixes github#3928

* fix(tests): use explicit utf-8 encoding reading restored skill content

read_text() defaults to the platform locale encoding, which is cp1252
("charmap") on Windows. The bundled specify.md core template contains a
UTF-8 multi-byte emoji whose bytes aren't valid cp1252, so the Windows CI
job failed decoding the restored SKILL.md with UnicodeDecodeError.

* fix(presets): keep extension restore priority over bundled-core fallback

The bundled-core fallback added for github#3928 ran before the
extension_restore_index lookup, so a skill an installed extension owns
could be silently replaced by lower-priority bundled core content on
preset removal instead of preserving the extension's winning layer.
* chore: bump version to 0.15.2

* chore: begin 0.15.3.dev0 development

---------

Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
Assisted-by: GitHub Copilot (model: GPT-5.6 Sol, autonomous)

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* fix: cap stdin read at 1 MiB to prevent DoS

Unbounded sys.stdin.read() allowed a malicious caller to exhaust memory
by sending a multi-gigabyte payload. Cap at 1 MiB and raise typer.Exit
if truncated.

* fix: improve stdin payload limit error handling in event.py

- Rename _MAX_PAYLOAD to MAX_STDIN_BYTES (clearer constant naming)
- Improve error message to suggest truncation or smaller payload
- Better code formatting for readability

Assisted-by: GitHub Copilot (model: mimo-v2-free, supervised)
Assisted-by: GitHub Copilot (model: GPT-5.6 Sol, autonomous)

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Merges github/spec-kit upstream main (tip ab468c4) — 14 commits since the
last merge base d1e86f6, including the 0.15.2 release (github#3953) and 3
post-release fixes (github#3899, github#3857, github#3897).

New upstream features:
- scaffold extension config templates on extension add/enable (github#2000)
- SPECKIT_INTEGRATION_DEFAULT env-var for specify init / workflow / bundle (github#3952)
- adrkit community catalog entry (github#3947)

New upstream hardening:
- restore core skills (not delete) on preset remove (github#3929)
- reject non-string manifest metadata (github#3943)
- narrow bare except Exception in invoke-separator resolution (github#3856)
- workflow ignore_agent_tools explicit-null default (github#3889)
- reject mismatched workflow run state IDs (github#3899)
- cap stdin read at 1 MiB to prevent DoS (github#3857)
- non-UTF-8 tolerance: events/presets/extension manifests/event overrides
  (github#3900, github#3896, github#3895, github#3897)
- validate required preset manifest mappings (github#3898)

1 conflict resolved (pyproject.toml: kept fork name/description, version ->
0.15.2+adlc1). All semantic hotspots auto-merged cleanly; fork modules
untouched. No templates/ changes upstream -> no preset command porting.
Ruff clean (ruff@0.15.0). Tests deferred to CI.

Assisted-by: opencode (model: glm-5.2, autonomous)
* feat(copilot): default integration to skills

Make Copilot skills the default while retaining the commands layout behind --integration-options="--commands". Preserve historical project layouts and validate conflicting mode flags before switch teardown.

Assisted-by: GitHub Copilot (model: GPT-5.6 Sol, autonomous)

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

Copilot-Session: 930d846b-8921-44ef-9f45-3e77c036b6b5

* fix(copilot): preserve layout state during migration

Keep target integration options isolated from fallback state, prefer the Copilot manifest when resolving layouts, and update dispatch coverage for the skills-first default.

Assisted-by: GitHub Copilot (model: GPT-5.6 Sol, autonomous)

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

Copilot-Session: 930d846b-8921-44ef-9f45-3e77c036b6b5

---------

Copilot-Session: 930d846b-8921-44ef-9f45-3e77c036b6b5
Update archive extension submitted by @stn1slv:
- extensions/catalog.community.json (version, download_url, updated_at)
- docs/community/extensions.md community extensions table

Closes github#3977

Assisted-by: GitHub Copilot (model: claude-sonnet-4.6, autonomous)

Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Add tdd extension submitted by @d0whc3r to:
- extensions/catalog.community.json (alphabetical order)
- docs/community/extensions.md community extensions table

Closes github#3978

Assisted-by: GitHub Copilot (model: claude-sonnet-4.6, autonomous)

Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…hooks (github#3934)

* feat(events): context injection for opencode and JSON-envelope agent hooks

Adds first-class context injection to agent runtime events:

1. opencode: maps session_start to experimental.chat.system.transform (injects into system prompt) and user_prompt_submit to chat.message (injects synthetic TextPart). TS plugin captures runEvent stdout (stdio pipe, encoding utf-8) and pushes into output objects. Part IDs derive from output.parts[last].id to preserve OpenCode's prt_ brand and prevent session schema crashes.

2. JSON-envelope hook wrapping: adds events_context_envelope to IntegrationBase so agents that require JSON on stdout receive their target envelope via the dispatcher's 5th argument:
   - gemini, tabnine, qwen, devin: hookSpecificOutput.additionalContext on session_start/user_prompt_submit; suppress on non-injectable events (prevents systemMessage user-facing noise)
   - copilot: top-level additionalContext on session_start
   - cursor: top-level additional_context on session_start; suppress elsewhere
   - claude, codex: plain stdout passthrough (already injected)

3. Dispatcher template and resolve_and_run_event_command parse the 5th envelope arg and wrap stdout accordingly.

Tests added for opencode TextPart schema, part ID derivation, envelope command generation, and dispatcher output wrapping. All 162 events/integration tests pass.

* fix(events): address code review on github#3934

- Qwen/Gemini/Tabnine/Devin: include native hookEventName inside
  hookSpecificOutput envelope (required by Qwen's hooks spec). Thread
  the native event name from the integration's CANONICAL_TO_NATIVE
  through _dispatcher_command as a 6th dispatcher argument, through
  the dispatcher template's main()/_run_inline()/_emit(), and through
  resolve_and_run_event_command()/_emit_event_stdout().
- Copilot: map user_prompt_submit to additionalContext (previously
  unmapped, breaking per-prompt context injection despite Copilot CLI
  supporting it via userPromptSubmitted).
- OpenCode: guard experimental.chat.system.transform so canonical
  session_start handlers only run when input.sessionID is present —
  OpenCode fires this hook for non-session operations (e.g. agent
  generation) with no sessionID.

Assisted-by: opencode (model: glm-5.2, supervised)

* fix(events): address second Copilot review round on github#3934

- Positional arg alignment: always emit default timeout (60s) as the
  4th dispatcher argument even when timeout_seconds is omitted, so the
  envelope (5th) and native_event (6th) land in the correct argv slots.
  Previously, omitting timeout_seconds caused the envelope to be parsed
  as an invalid timeout, silently falling back to plain stdout.
- OpenCode session_start caching: cache handler output per sessionID in
  the generated TS plugin so non-idempotent handlers (setup, telemetry,
  file-mutating scripts) run once per session instead of on every LLM
  request. Cache is evicted on session.deleted.
- Updated PR description to reflect Copilot user_prompt_submit now maps
  to additionalContext (was documented as plain/unprocessed).

Assisted-by: opencode (model: glm-5.2, supervised)
_script_command() split the configured command with a bare
shlex.split(), so a command string with unbalanced quotes crashed event
dispatch with a raw ValueError. The dispatcher-template twin a few
lines up already wraps the same call in try/except ValueError and
returns None so dispatch falls back cleanly.

Wrap the split the same way and return None, restoring parity between
the two paths.

Assisted-by: GitHub Copilot (model: claude-fable-5, autonomous)

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* Update Charter extension to v0.5.1

Update charter extension submitted by @Huljo:
- extensions/catalog.community.json (version, download_url, updated_at)

Closes github#3944

Assisted-by: GitHub Copilot (model: claude-sonnet-4.6, autonomous)
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* Limit catalog diff to Charter fields and top-level timestamp

Assisted-by: GitHub Copilot (model: unknown, autonomous)

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

---------

Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
Co-authored-by: mnriem <15701806+mnriem@users.noreply.github.com>
…ithub#3960)

The keep-config rescue branch of install_from_directory() reads each
preserved config with bare read_bytes()/stat() calls, so a kept config
that cannot be read (permission or I/O error) crashed the reinstall
with a raw OSError. The sibling symlink guard four lines above already
rejects with a ValidationError and resolution guidance for the same
reason: bytes that cannot be safely rescued must not reach the rmtree
below.

Wrap the read and raise ValidationError with guidance, while dest_dir
is still untouched so the preserved bytes are never lost.

Assisted-by: GitHub Copilot (model: claude-fable-5, autonomous)

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
`requires.speckit_version` was presence-checked but never type-checked in
both the extension and preset manifest validators, so an unquoted YAML
`speckit_version: 1.0` (a float) passed validation and reached
`SpecifierSet(required)` in `check_compatibility()`.

That call is guarded by `except InvalidSpecifier` alone, which a
non-string escapes two different ways:

- a float/int/bool/None raises `TypeError: 'float' object is not
  iterable` from the `SpecifierSet` constructor;
- a list or dict is an *iterable*, so `SpecifierSet` accepts it and the
  failure surfaces much later as `AttributeError: 'str' object has no
  attribute 'filter'` from inside `.contains()`.

Neither is a `CompatibilityError`/`PresetCompatibilityError`, so both
bypass the CLI's "Compatibility Error" handler in `_commands.py` and exit
1 with a raw traceback that names no field, leaving the author with no
hint which manifest key is wrong.

Type-check the field in both validators, requiring a non-empty string,
and additionally guard `check_compatibility()` in both managers since
each is public and reachable with a hand-built or mutated manifest. This
mirrors the sibling `IntegrationDescriptor`, which already requires a
non-empty string for the same key, and completes the type-checking pass
started in github#3943 for the neighbouring `extension`/`preset` fields.

Adds 33 regression tests across both modules covering every escape path;
26 of them fail without this change.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

Assisted-by: Claude Code (model: Claude Opus 5, supervised)
…hub#3962)

The rescue-retry loop in install_from_directory() reads each staged
backup with bare stat()/read_bytes() calls, so a staged config that
cannot be read crashed the reinstall with a raw OSError. Every sibling
read in this path — the live twin four lines below, the packaged
baseline check, the mode sidecar — already catches OSError.

Treat an unreadable staged file like an uncomparable live config:
add it to the conflict set so both copies are preserved and the retry
aborts with the existing resolution guidance while dest_dir is still
untouched.

Assisted-by: GitHub Copilot (model: claude-fable-5, autonomous)

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

_merge_toml_fragment() and _remove_toml_entries() read the user's
config.toml with bare read_text() calls, so a non-UTF-8 (or otherwise
unreadable) file crashed install_integration_events() and
remove_integration_events() with a raw UnicodeDecodeError — and the
merge path regenerates the file from what it read, so it would have
discarded the user's bytes had it not crashed first. Every JSON
merge/remove path already goes through _load_user_json(), which skips
on an unreadable file to preserve user content (#22).

Abort the merge (returning False so the caller skips tracking, S5) and
skip the teardown cleanup with a warning, leaving the user's bytes
untouched in both directions.

Assisted-by: GitHub Copilot (model: claude-fable-5, autonomous)

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Assisted-by: GitHub Copilot (model: GPT-5.6 Sol, autonomous)

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

Copilot-Session: 31dc6b66-8484-46b5-a282-360029e14ff2
PresetRegistry._load() catches json.JSONDecodeError and
FileNotFoundError to start fresh on a corrupted or missing registry, but
a registry file with invalid UTF-8 bytes raised UnicodeDecodeError
before JSON parsing began, crashing every preset command.

Catch UnicodeDecodeError in the same clause: undecodable bytes are the
same corruption class as unparseable JSON. OSError stays uncaught on
purpose — the data may be intact on disk, and starting fresh would let a
later _save() wipe it (same fail-closed reasoning as the workflow
catalog cache loader).

Assisted-by: GitHub Copilot (model: claude-fable-5, autonomous)

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* Add July 2026 newsletter

* docs(newsletters): remove internal press-index figures from earlier editions

Replace article counts, volume superlatives, and discovery-methodology
references (derived from an internal press index) with qualitative phrasing
in the April, May, and June editions, keeping only publicly verifiable data.
Upstream merge (ab468c4..03d71b3): Copilot skills-default flip (github#3976),
events context injection for opencode + JSON-envelope hooks (github#3934, by fork
maintainer upstream), non-UTF-8/hardening fixes (github#3955/github#3957/github#3960/github#3962/
github#3963/github#3980), community catalog additions (github#3981/github#3982/github#3983), docs (github#3985/
github#3987).

5 conflicts resolved: AGENTS.md (optional-overrides section + PR-prioritization),
copilot/__init__.py (re-applied fork customizations + prefix-aware
is_skills_mode), test_cli.py/test_integration_copilot.py/test_integration_
subcommand.py (adapted to fork naming for skills-default).

Fork fix: build_command_invocation() bare-name canonicalization before alias
resolution. Ruff clean (ruff@0.15.0). 1375+ tests pass.

Assisted-by: opencode (model: glm-5.2, supervised)
@kanfil kanfil changed the title chore: merge upstream 0.15.2, bump to 0.15.2+adlc1 chore: merge upstream 0.15.2 + 13 post-release commits, bump to 0.15.2+adlc2 Aug 5, 2026
kanfil added 2 commits August 5, 2026 20:27
…er py3.14

Latest uv (setup-uv v9) resolves typing-extensions differently under
--universal + Python 3.14 than the committed snapshot — the dependency-audit
check failed on the pyproject.toml version bump in this PR.

Regenerated with: uvx uv@latest pip compile pyproject.toml --extra test
--universal --generate-hashes --quiet --no-header (under Python 3.14).

Assisted-by: opencode (model: glm-5.2, supervised)
…tion

Two CI failures from github#3976 copilot skills-default flip:

1. build_command_invocation: only canonicalize truly bare names (no dot).
   "git.commit" is a dotted alias-form name, not bare — prepending speckit.
   made it speckit.git.commit which resolves differently in clean envs (CI
   checkout has no .specify/ → no alias → /speckit-git-commit instead of
   /git-commit). Test documents the contract: "git.commit is not in the alias
   map as a key, so it's always unchanged".

2. test_upgrade_preserves_historical_copilot_commands_without_options:
   fork's _register_model_invocation_skills (since 0.12.8+adlc6) generates
   .github/skills/quick-* during init for commands-mode agents (agentic-quick
   is preinstalled with model-invocation:true commands). These skills exist
   before the upgrade runs — not an upgrade bug. Assert the migration signal
   (no command-derived speckit-plan/spec-plan skill dir) instead of skills-dir
   absence.

Assisted-by: opencode (model: glm-5.2, supervised)
@kanfil
kanfil merged commit 68daed8 into main Aug 5, 2026
14 checks passed
@kanfil
kanfil deleted the chore/upstream-merge-0.15.2 branch August 5, 2026 19:13
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.

8 participants