From 824eea00a0981aa823c66224f6c2532338e54c3f Mon Sep 17 00:00:00 2001 From: Manfred Riem <15701806+mnriem@users.noreply.github.com> Date: Wed, 21 Oct 2026 10:50:23 -0500 Subject: [PATCH 1/5] feat!: remove taskstoissues from core Make the opt-in github extension the sole owner of taskstoissues, including a deprecated compatibility alias. Preserve hook behavior, register aliases in skills mode, restore extension-backed preset overrides, and document the breaking migration for the next minor release. Refs github/spec-kit#4423 Assisted-by: GitHub Copilot (model: GPT-6.1 Sol, autonomous) Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --- CHANGELOG.md | 15 ++ README.md | 5 + docs/community/presets.md | 6 + docs/installation.md | 15 +- docs/reference/agentic-sdd.md | 10 +- docs/reference/extensions.md | 2 +- docs/reference/integrations.md | 13 +- docs/upgrade.md | 46 ++++++ extensions/EXTENSION-API-REFERENCE.md | 8 +- extensions/EXTENSION-DEVELOPMENT-GUIDE.md | 6 +- extensions/EXTENSION-USER-GUIDE.md | 3 +- extensions/catalog.json | 2 +- extensions/github/README.md | 90 ++++++----- .../commands/speckit.github.taskstoissues.md | 8 + extensions/github/extension.yml | 11 +- src/specify_cli/__init__.py | 1 - src/specify_cli/extensions/__init__.py | 26 ++-- .../integrations/alquimia/__init__.py | 1 - src/specify_cli/integrations/base.py | 1 - .../integrations/claude/__init__.py | 1 - .../integrations/copilot/__init__.py | 1 - src/specify_cli/presets/_manager_skills.py | 16 +- templates/commands/taskstoissues.md | 114 -------------- .../github/test_github_extension.py | 142 +++++++++--------- .../github/test_taskstoissues_deprecation.py | 99 ------------ .../github/test_taskstoissues_removal.py | 89 +++++++++++ .../test_integration_base_markdown.py | 2 +- .../test_integration_base_skills.py | 4 +- .../test_integration_base_toml.py | 1 - .../test_integration_base_yaml.py | 1 - tests/integrations/test_integration_bob.py | 4 +- .../integrations/test_integration_copilot.py | 10 +- .../integrations/test_integration_generic.py | 59 ++++++-- .../integrations/test_command_upgrade.py | 61 ++++++++ .../presets/test_manager_skills.py | 101 +++++++++---- tests/test_extensions.py | 14 +- 36 files changed, 545 insertions(+), 443 deletions(-) delete mode 100644 templates/commands/taskstoissues.md delete mode 100644 tests/extensions/github/test_taskstoissues_deprecation.py create mode 100644 tests/extensions/github/test_taskstoissues_removal.py diff --git a/CHANGELOG.md b/CHANGELOG.md index c6aa580d01..ef98604852 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,21 @@ +## Unreleased - next minor release + +### Breaking + +- Remove `taskstoissues` from core commands and integration scaffolding after + the replacement shipped in 1.0.13 and core deprecation shipped in 1.1.3 + (github/spec-kit#4423). The opt-in `github` extension is the sole owner of + `speckit.github.taskstoissues` and its deprecated `speckit.taskstoissues` + compatibility alias. Run `specify integration upgrade `, then + `specify extension add github`; for an already-installed + extension, use `specify extension update github` instead of `add`. + See the [migration guide](docs/upgrade.md#breaking-change-taskstoissues-removed-from-core). + This breaking removal must ship in a minor release, not the current patch + development line. + ## [1.2.2] - 2026-10-19 ### Changed diff --git a/README.md b/README.md index 3f50156526..887b64b20d 100644 --- a/README.md +++ b/README.md @@ -94,6 +94,11 @@ For other setups: - [Other agents](https://github.github.io/spec-kit/reference/integrations.html) for integration keys and [invocation syntax](https://github.github.io/spec-kit/reference/integrations.html#command-invocation). - [Upgrade](https://github.github.io/spec-kit/upgrade.html) to update an existing installation. +**Breaking change:** tasks-to-issues conversion is no longer installed by core. +Opt in with `specify extension add github`, then use +`speckit.github.taskstoissues`. Existing projects must also refresh their +integration; see the [migration recipe](https://github.github.io/spec-kit/upgrade.html#breaking-change-taskstoissues-removed-from-core). + diff --git a/docs/community/presets.md b/docs/community/presets.md index cbd7e356e4..06aa2cdbed 100644 --- a/docs/community/presets.md +++ b/docs/community/presets.md @@ -5,6 +5,12 @@ The following community-contributed presets customize how Spec Kit behaves — overriding templates, commands, and terminology without changing any tooling. Presets are available in [`catalog.community.json`](https://github.com/github/spec-kit/blob/main/presets/catalog.community.json): +**Migration note:** `speckit.taskstoissues` is no longer a core command. +Listings below retain their authors' descriptions; presets targeting that old +name need migration review by their authors. GitHub conversion now belongs to +the opt-in `github` extension's `speckit.github.taskstoissues`; other trackers +should use provider-specific extensions. See the [upgrade guide](../upgrade.md#breaking-change-taskstoissues-removed-from-core). + | Preset | Purpose | Provides | Requires | URL | |--------|---------|----------|----------|-----| | A11Y Governance | Adds WCAG 2.2 AA, accessible status output, bilingual CEFR-B2 delivery, inclusive-content and didactic-comment governance, and provider-neutral model routing. | 11 templates, 3 commands | — | [spec-kit-preset-a11y-governance](https://github.com/hindermath/spec-kit-preset-a11y-governance) | diff --git a/docs/installation.md b/docs/installation.md index 3abd5919a0..76bd8784a1 100644 --- a/docs/installation.md +++ b/docs/installation.md @@ -155,15 +155,18 @@ After initialization, you should see the following commands available in your co - `/speckit.checklist` - Generate quality checklists - `/speckit.constitution` - Create or update project principles - `/speckit.converge` - Assess codebase against artifacts and append remaining tasks -- `/speckit.taskstoissues` - Convert tasks to issues (deprecated; warns on invocation; replaced by the bundled `github` extension's - `/speckit.github.taskstoissues`; install it with `specify extension add github`) -The `generic` integration also registers extension commands in its configured +**Breaking change:** tasks-to-issues conversion is no longer a core command. +Fresh projects receive no issue-tracker commands unless you explicitly run +`specify extension add github`. Use `/speckit.github.taskstoissues`; the +extension also supplies the deprecated `/speckit.taskstoissues` alias. +See the [upgrade migration recipe](upgrade.md#breaking-change-taskstoissues-removed-from-core). + +The `generic` integration registers extension commands in its configured `--commands-dir`. Installing `github` makes `/speckit.github.taskstoissues` available there as a command file, or as `/speckit-github-taskstoissues` when -`--skills` is enabled. The core `/speckit.taskstoissues` (or -`/speckit-taskstoissues` with `--skills`) remains available during migration, warns on invocation, -and will be removed in a future minor release. +`--skills` is enabled. Removing the extension removes its unchanged commands, +including its compatibility alias. See the [GitHub extension's installation notes](https://github.com/github/spec-kit/blob/main/extensions/github/README.md#installation). Scripts are installed into a variant subdirectory matching the chosen script type: diff --git a/docs/reference/agentic-sdd.md b/docs/reference/agentic-sdd.md index ea60858f30..b2f80d4554 100644 --- a/docs/reference/agentic-sdd.md +++ b/docs/reference/agentic-sdd.md @@ -30,7 +30,6 @@ skills by default. | `/speckit.tasks` | `speckit-tasks` | Break the plan into actionable tasks | | `/speckit.implement` | `speckit-implement` | Execute the tasks | | `/speckit.converge` | `speckit-converge` | Assess implementation against the artifacts and append remaining work | -| `/speckit.taskstoissues` | `speckit-taskstoissues` | **Deprecated core command**; use the `github` extension for GitHub issues | | `/speckit.clarify` | `speckit-clarify` | Resolve ambiguity before planning (optional quality gate; formerly `/quizme`) | | `/speckit.analyze` | `speckit-analyze` | Check artifact consistency after tasks and before implementation (optional quality gate) | | `/speckit.checklist` | `speckit-checklist` | Generate requirements-quality checklists (optional quality gate) | @@ -158,11 +157,10 @@ It first prints a severity-graded findings summary, then resolves to one of two - **Converged** — no gaps found. `tasks.md` is left byte-for-byte unchanged and you'll see a clean result like `✅ Converged — the implementation satisfies the spec, plan, and tasks.` You're done; proceed to review or open a PR. - **Tasks appended** — gaps found. Converge appends them as new tasks under a Convergence section in `tasks.md` and tells you how many. Run `/speckit.implement` again to complete them, then `/speckit.converge` once more. Each pass finds fewer items; repeat until it reports converged. -## `/speckit.taskstoissues` (deprecated core command) +## `/speckit.github.taskstoissues` (opt-in extension) -The core command is deprecated and will be removed in a future minor release. -It warns when invoked, then continues its existing workflow without installing -or enabling the extension automatically. +**Breaking change:** tasks-to-issues conversion is removed from core. +It is not installed by `specify init`; neither is the compatibility alias. The recommended extension command optionally converts an existing `tasks.md` into actionable GitHub issues. Install the extension from your project root: @@ -184,4 +182,4 @@ identified by that remote and checks existing task IDs to avoid duplicates. It is not required to implement tasks or converge on a feature. > [!NOTE] -> GitHub issue tracking uses the bundled, opt-in [`github` extension](https://github.com/github/spec-kit/blob/main/extensions/github/README.md). Its recommended command is `/speckit.github.taskstoissues` (slash-hyphen integrations: `/speckit-github-taskstoissues`; Codex, ZCode, and Command Code skills: `$speckit-github-taskstoissues`; Kimi: `/skill:speckit-github-taskstoissues`); the deprecated core `/speckit.taskstoissues` remains available during migration. The `generic` integration registers the extension under its configured command directory in commands or skills mode. See the [installation and migration notes](https://github.com/github/spec-kit/blob/main/extensions/github/README.md#installation) for all integration-specific invocation syntaxes. +> GitHub issue tracking uses the bundled, opt-in [`github` extension](https://github.com/github/spec-kit/blob/main/extensions/github/README.md). Its canonical command is `/speckit.github.taskstoissues` (slash-hyphen integrations: `/speckit-github-taskstoissues`; Codex, ZCode, and Command Code skills: `$speckit-github-taskstoissues`; Kimi: `/skill:speckit-github-taskstoissues`). The extension also supplies the deprecated `/speckit.taskstoissues` alias. The `generic` integration registers these commands under its configured command directory in commands or skills mode. Existing projects should follow the [upgrade migration recipe](../upgrade.md#breaking-change-taskstoissues-removed-from-core). diff --git a/docs/reference/extensions.md b/docs/reference/extensions.md index 4c93f94dbe..0b98659f20 100644 --- a/docs/reference/extensions.md +++ b/docs/reference/extensions.md @@ -37,7 +37,7 @@ specify extension add | `--force` | Overwrite if the extension is already installed | | `--priority `| Resolution priority (default: 10; lower = higher precedence) | -Installs an extension from the catalog, a URL, or a local directory. Extension commands are registered with the active AI coding agent integration. For `generic`, invocations use the configured `--commands-dir`: flat command files by default, or `speckit-/SKILL.md` with `--skills`. The deprecated core `speckit.taskstoissues` command remains available during migration and will be removed in a future minor release. It warns on invocation and continues its existing workflow without installing or enabling the extension automatically. For GitHub issue tracking, run `specify extension add github` and use the recommended `/speckit.github.taskstoissues` command (slash-hyphen integrations: `/speckit-github-taskstoissues`; Codex, ZCode, and Command Code skills: `$speckit-github-taskstoissues`; Kimi: `/skill:speckit-github-taskstoissues`). +Installs an extension from the catalog, a URL, or a local directory. Extension commands are registered with the active AI coding agent integration. For `generic`, invocations use the configured `--commands-dir`: flat command files by default, or `speckit-/SKILL.md` with `--skills`. Core no longer provides tasks-to-issues conversion. Run `specify extension add github` and use the canonical `/speckit.github.taskstoissues` command (slash-hyphen integrations: `/speckit-github-taskstoissues`; Codex, ZCode, and Command Code skills: `$speckit-github-taskstoissues`; Kimi: `/skill:speckit-github-taskstoissues`). Only this opt-in extension supplies the deprecated `speckit.taskstoissues` compatibility alias. Existing projects should follow the [breaking-change migration recipe](../upgrade.md#breaking-change-taskstoissues-removed-from-core). If a generic integration refresh cannot produce every extension invocation (for example, because a command or skill is user-modified or its source is missing), it warns and restores that extension's prior registered artifacts. Other extensions can still refresh. diff --git a/docs/reference/integrations.md b/docs/reference/integrations.md index 2480695630..24ce8a8637 100644 --- a/docs/reference/integrations.md +++ b/docs/reference/integrations.md @@ -455,14 +455,17 @@ Once `generic` is the active integration, `specify extension add` registers extension commands in its configured `--commands-dir` (as command files or skills according to `--skills`). `specify extension remove` removes unchanged extension-owned artifacts while leaving core commands, user files, and edited -extension files intact. The deprecated core `speckit.taskstoissues` command -remains available, warns on invocation, and will be removed in a future minor -release. To migrate, run `specify extension add github` and use the recommended +extension files intact. Core no longer installs `speckit.taskstoissues`. +To migrate, run `specify integration upgrade ` to remove obsolete managed +core files and refresh enabled extension commands for the default integration, +then `specify extension add github`. +Use the canonical `/speckit.github.taskstoissues` command for dot-command integrations. Slash-hyphen integrations use `/speckit-github-taskstoissues`; Codex, ZCode, and Command Code skills use `$speckit-github-taskstoissues`; Kimi uses -`/skill:speckit-github-taskstoissues`. The core command continues its existing -workflow and does not install or enable the extension automatically. +`/skill:speckit-github-taskstoissues`. The extension also owns the deprecated +`speckit.taskstoissues` compatibility alias; core never installs it or enables +the extension automatically. See the [breaking-change migration](../upgrade.md#breaking-change-taskstoissues-removed-from-core). ### Claude: user-invocable-only skills diff --git a/docs/upgrade.md b/docs/upgrade.md index 45e664c0a4..5df203c20b 100644 --- a/docs/upgrade.md +++ b/docs/upgrade.md @@ -280,6 +280,52 @@ preset/extension-managed — see the "Interaction with the resolution stack" sec --- +## Breaking change: taskstoissues removed from core + +Tasks-to-issues conversion now belongs exclusively to the opt-in `github` +extension. The replacement shipped in 1.0.13, and core deprecation shipped in +1.1.3; removal is intended for the next minor release, not a patch release. +New projects receive no tasks-to-issues command unless the extension is installed. + +After upgrading to the CLI containing the removal, migrate from your project root: + +```bash +specify integration upgrade +specify extension add github +``` + +Use the installed integration key in place of ``, or omit it to +upgrade the default integration. If `github` is already installed, use +`specify extension update github` instead of `add`. The delivered extension +is named `github`, not the originally proposed `github-issues`. +Refresh the integration first so old core artifacts cannot collide with the +extension alias, especially in the ownership-preserving `generic` integration. + +Use the canonical `speckit.github.taskstoissues` command with your +[integration's invocation syntax](reference/integrations.md#command-invocation). +The extension also owns a deprecated `speckit.taskstoissues` alias, which warns +and runs the same workflow once. Both preserve `before_taskstoissues` and +`after_taskstoissues`. No extension is installed or enabled automatically. + +Integration upgrade removes obsolete manifest-tracked core command files and +re-registers enabled extensions for the default integration. If local edits +block the upgrade, inspect them before using `--force`. CLI upgrade alone does +not update generated commands. Without a generated-file manifest, upgrade +reports that there is nothing to upgrade: review and clean up old commands +manually before reinstalling the integration. For multiple integrations, +upgrade each; `specify integration use ` registers extension +commands when making one the default. + +For projects that do not want GitHub issue tracking, skip extension installation +and run only the integration upgrade; neither tasks-to-issues invocation remains. +Audit presets, dependent extensions, workflow steps, handoffs, and automation: +use `speckit.github.taskstoissues` and +`__SPECKIT_COMMAND_GITHUB_TASKSTOISSUES__` for GitHub conversion and explicitly +require extension installation. Preset overrides of the removed core name do +not automatically customize the canonical extension command. Port them or use +a provider-specific extension for another issue tracker. See the +[extension migration guide](https://github.com/github/spec-kit/blob/main/extensions/github/README.md#migrating-from-the-core-taskstoissues-command). + ## Common Scenarios ### Scenario 1: "I just want new slash commands" diff --git a/extensions/EXTENSION-API-REFERENCE.md b/extensions/EXTENSION-API-REFERENCE.md index 2be2f43123..0d1fe515d0 100644 --- a/extensions/EXTENSION-API-REFERENCE.md +++ b/extensions/EXTENSION-API-REFERENCE.md @@ -44,7 +44,7 @@ provides: - name: string # Required, pattern: ^speckit\.[a-z0-9-]+\.[a-z0-9-]+$ file: string # Required, relative path to command file description: string # Required - aliases: [string] # Optional, same pattern as name; namespace must match extension.id and must not shadow core or installed extension commands + aliases: [string] # Optional safe relative names; must not shadow core or installed extension commands templates: # Optional, array of declared templates. Always resolve # as "replace" -- 'strategy' is not an authorable field here. @@ -633,6 +633,12 @@ Standard events (defined by core): - `after_clarify` - After spec clarification - `before_constitution` - Before constitution update - `after_constitution` - After constitution update +#### Bundled `github` extension events (available when the extension is installed) + +The canonical `speckit.github.taskstoissues` command and its deprecated +`speckit.taskstoissues` compatibility alias consume the same hook keys. +Core no longer installs an issue-tracking command. + - `before_taskstoissues` - Before tasks-to-issues conversion - `after_taskstoissues` - After tasks-to-issues conversion diff --git a/extensions/EXTENSION-DEVELOPMENT-GUIDE.md b/extensions/EXTENSION-DEVELOPMENT-GUIDE.md index a263a7bbf1..0109d377be 100644 --- a/extensions/EXTENSION-DEVELOPMENT-GUIDE.md +++ b/extensions/EXTENSION-DEVELOPMENT-GUIDE.md @@ -41,7 +41,7 @@ provides: - name: "speckit.my-ext.hello" # Must follow pattern: speckit.{ext-id}.{cmd} file: "commands/hello.md" description: "Say hello" - aliases: ["speckit.my-ext.hi"] # Optional aliases, same pattern + aliases: ["speckit.my-ext.hi"] # Optional alternative invocation names config: # Optional: Config files - name: "my-ext-config.yml" @@ -193,7 +193,7 @@ of `provides.commands`, `provides.templates`, `provides.scripts`, `hooks`, or - `name`: Command name (must match `speckit.{ext-id}.{command}`) - `file`: Path to command file (relative to extension root) - `description`: Command description (optional) -- `aliases`: Alternative command names (optional, array; each must match `speckit.{ext-id}.{command}`) +- `aliases`: Alternative command names (optional string array). Names must be safe relative output paths and must not shadow core or installed extension commands. An alias may use a retired core name, as the opt-in `github` extension does for `speckit.taskstoissues`. **Template object**: @@ -226,7 +226,7 @@ Available hook points: - `before_checklist` / `after_checklist`: Before/after checklist generation - `before_clarify` / `after_clarify`: Before/after spec clarification - `before_constitution` / `after_constitution`: Before/after constitution update -- `before_taskstoissues` / `after_taskstoissues`: Before/after tasks-to-issues conversion +- `before_taskstoissues` / `after_taskstoissues`: Before/after tasks-to-issues conversion (opt-in `github` extension, including its deprecated alias) - `before_bug_assess` / `after_bug_assess`: Before/after bug assessment (bundled `bug` extension) - `before_bug_fix` / `after_bug_fix`: Before/after bug remediation (bundled `bug` extension) - `before_bug_test` / `after_bug_test`: Before/after bug verification (bundled `bug` extension) diff --git a/extensions/EXTENSION-USER-GUIDE.md b/extensions/EXTENSION-USER-GUIDE.md index 04b5e1edc0..a0ee6e6527 100644 --- a/extensions/EXTENSION-USER-GUIDE.md +++ b/extensions/EXTENSION-USER-GUIDE.md @@ -407,7 +407,8 @@ settings: # Available events: before_specify, after_specify, before_plan, after_plan, # before_tasks, after_tasks, before_implement, after_implement, # before_analyze, after_analyze, before_checklist, after_checklist, -# before_clarify, after_clarify, before_constitution, after_constitution, +# before_clarify, after_clarify, before_constitution, after_constitution +# Bundled github extension events (when installed): # before_taskstoissues, after_taskstoissues # Bundled bug extension events (when installed): # before_bug_assess, after_bug_assess, before_bug_fix, after_bug_fix, diff --git a/extensions/catalog.json b/extensions/catalog.json index 30e6dd92ac..dd13966c40 100644 --- a/extensions/catalog.json +++ b/extensions/catalog.json @@ -66,7 +66,7 @@ "github": { "name": "GitHub Integration", "id": "github", - "version": "1.0.2", + "version": "1.1.0", "description": "GitHub platform integration for Spec Kit - create GitHub issues from a feature's task list", "author": "spec-kit-core", "repository": "https://github.com/github/spec-kit", diff --git a/extensions/github/README.md b/extensions/github/README.md index 8aaf46a5c3..05b33db4b3 100644 --- a/extensions/github/README.md +++ b/extensions/github/README.md @@ -23,9 +23,9 @@ specify extension add github The `generic` (bring your own agent) integration registers this command under its configured `--commands-dir` in both commands and skills layouts. Installing `github` creates `speckit.github.taskstoissues.md` in commands mode or -`speckit-github-taskstoissues/SKILL.md` in skills mode. The core -`speckit.taskstoissues` command is deprecated but remains available during migration. -Use this extension's recommended command instead. See +`speckit-github-taskstoissues/SKILL.md` in skills mode. It also installs the +deprecated `speckit.taskstoissues` compatibility alias. Neither invocation is +installed by core. Use the namespaced command for new workflows. See [integration-specific options](../../docs/reference/integrations.md#integration-specific-options). ## Removal @@ -39,6 +39,7 @@ specify extension remove github | Command | Description | | ------------------------------ | -------------------------------------------------------------------- | | `speckit.github.taskstoissues` | Recommended: convert tasks from `tasks.md` into dependency-ordered GitHub issues. | +| `speckit.taskstoissues` | Deprecated compatibility alias; warns and runs the same workflow and hooks once. | > NOTE: The command ID above is canonical. Invoke it using the syntax for your integration: `/speckit.github.taskstoissues` for dot-command integrations; `/speckit-github-taskstoissues` for slash-hyphen integrations (including Forge and Cline); `$speckit-github-taskstoissues` for Codex, ZCode, or Command Code in skills mode; or `/skill:speckit-github-taskstoissues` for Kimi. @@ -55,10 +56,9 @@ This extension consumes the existing `before_taskstoissues` and `after_taskstois ## Requirements -- Spec Kit **0.12.17 or newer**. Extension-local `scripts/...` path rewriting arrived in - 0.12.6, but auto-registered skills did not resolve `__SPECKIT_COMMAND_*__` references - until 0.12.17. Earlier releases cannot render this command correctly in every supported - layout, so `specify extension add github` refuses to install below 0.12.17. +- Spec Kit **1.2.3.dev0 or newer** from the core-removal development line. + Earlier hosts still own `speckit.taskstoissues` and reject the compatibility + alias. Extension 1.0.2 remains the pre-removal replacement for those hosts. - A Git remote pointing at GitHub. - The **GitHub MCP server** available to your coding agent, providing the `list_issues` and `issue_write` tools. @@ -80,50 +80,56 @@ Prerequisite errors name the Spec Kit command without assuming an integration's ## Migrating from the core `taskstoissues` command -Spec Kit is moving GitHub issue tracking out of core in three stages: +**Breaking change:** `taskstoissues` has been removed from core after the +replacement shipped in 1.0.13 and deprecation shipped in 1.1.3. This removal +is intended for the next minor release, not a patch release. New projects do +not receive either invocation unless this extension is explicitly installed. -1. **Initial stage (completed)** — this extension shipped with the core `/speckit.taskstoissues` command still available. -2. **Next (current stage)** — the replacement has been available for at least one release, so the core command is now deprecated. It displays a migration warning and continues its existing workflow. It does not install or enable the extension automatically or run the replacement command. -3. **Later** — the core command will be removed in a future minor release. - -For existing projects, upgrading the CLI alone does not refresh the generated -core command files. After upgrading the CLI, run this from the project root: - -```bash -specify integration upgrade -``` - -Replace `` with the installed integration key, or omit it to upgrade the -default integration. This refreshes the core command with the deprecation -warning. If locally modified files block the upgrade, inspect those changes -before deciding whether to overwrite them. See [Upgrade an Integration](../../docs/reference/integrations.md#upgrade-an-integration). - -To migrate, install the extension and use the namespaced command instead: +After upgrading to a CLI containing the removal, run from the project root: ```bash +specify integration upgrade specify extension add github ``` -The `generic` integration supports this migration in both commands and skills -layouts under its configured `--commands-dir`. The core command remains -available during this deprecation period until the removal stage. - -| Deprecated core command | Recommended extension command | +Replace `` with the installed integration key, or omit it to +refresh the default integration. Upgrade first to retire old core artifacts +before installing the alias; generic registration refuses unowned collisions. +If `github` is already installed, run +`specify extension update github` instead of `add`. Integration upgrade +removes manifest-tracked obsolete core files, then re-registers enabled +extensions for the default integration, including the compatibility alias. +Upgrading the CLI alone does not refresh generated files. Inspect local edits +before deciding whether to use `--force`. Projects without an integration +manifest need manual review and cleanup of old generated commands; `upgrade` +reports that it cannot refresh them. + +The `generic` integration supports both commands and skills layouts under its +configured `--commands-dir`. For multiple installed integrations, upgrade each +one; use `specify integration use ` to register extension commands +when changing the default. See [Upgrade an Integration](../../docs/reference/integrations.md#upgrade-an-integration). + +| Deprecated compatibility alias | Canonical extension command | | ------------------------- | --------------------------------- | | `/speckit.taskstoissues` | `/speckit.github.taskstoissues` | -Apart from the core command's deprecation warning, conversion behavior is unchanged: the same feature resolution, the same `plan.md` and `tasks.md` prerequisites, the same remote validation, the same deduplication across open and closed issues, the same issue titles, and the same hook contract. This extension does **not** register `speckit.taskstoissues` as an alias, so the two commands coexist without shadowing each other while the core command still exists. +The extension owns both invocations. The alias warns and executes the same +workflow once; it does not invoke the canonical command a second time. +Conversion behavior is unchanged: feature resolution, `plan.md` and `tasks.md` +prerequisites, GitHub remote validation, open/closed issue deduplication, +issue titles, and hooks are preserved. Removing the extension removes its +unchanged canonical and alias artifacts without affecting core SDD commands. ### Presets and dependent extensions -Before the core command is removed, audit references to `speckit.taskstoissues` +Audit references to `speckit.taskstoissues` in presets, extension prompts, workflow steps, handoffs, and project automation. For GitHub issue conversion, use the canonical `speckit.github.taskstoissues` command ID and render it with the agent-specific syntax under [Commands](#commands). Replace `__SPECKIT_COMMAND_TASKSTOISSUES__` cross-command references with `__SPECKIT_COMMAND_GITHUB_TASKSTOISSUES__` where template rendering is supported. -Document `specify extension add github` as a setup prerequisite; the deprecation -warning does not install this dependency. +Document `specify extension add github` as a setup prerequisite; core never +installs this dependency. If an extension proposal or external dependency declaration uses `requires.commands: [speckit.taskstoissues]`, update its target to @@ -138,10 +144,16 @@ command. Review and port any needed customization, then verify the installed command in a sample project before retiring the old override. A preset that uses another issue tracker should retain its own provider-specific workflow rather than redirecting it to the GitHub command. Existing overrides may also -hide the core deprecation warning, so communicate the migration to their users. +hide the compatibility warning, so communicate the migration to their users. Keep the `before_taskstoissues` and `after_taskstoissues` hook keys unchanged; -both commands consume them. Only update a hook's command reference if it invokes -the deprecated command. Do not invoke both core and replacement commands for -the same conversion, since that also runs their hooks twice. Publish and test -updated preset/extension versions before the later core-removal release. +the canonical command and alias consume them. Only update a hook's command +reference if it invokes the deprecated alias. Do not invoke both names for +the same conversion, since that also runs their hooks twice. + +Related proposals such as [milestone grouping (github/spec-kit#4370)](https://github.com/github/spec-kit/issues/4370) +should target `speckit.github.taskstoissues`, not a core command. +[Jira support (github/spec-kit#2223)](https://github.com/github/spec-kit/issues/2223) +belongs in a provider-specific extension rather than an override of a +GitHub-specific core command. Existing third-party presets targeting the old +core name need their authors to port and verify those overrides. diff --git a/extensions/github/commands/speckit.github.taskstoissues.md b/extensions/github/commands/speckit.github.taskstoissues.md index 6e587abe75..a421cccf8d 100644 --- a/extensions/github/commands/speckit.github.taskstoissues.md +++ b/extensions/github/commands/speckit.github.taskstoissues.md @@ -7,6 +7,14 @@ scripts: py: scripts/python/resolve_tasks.py --json --- +## Compatibility Alias + +Only when invoked as `__SPECKIT_COMMAND_TASKSTOISSUES__`, display: + +> `__SPECKIT_COMMAND_TASKSTOISSUES__` is a deprecated compatibility alias provided by the `github` extension. Use `__SPECKIT_COMMAND_GITHUB_TASKSTOISSUES__` instead. + +Do not display this warning for the canonical command. Continue with the workflow below, including its existing hooks and safety checks. Do not invoke the canonical command again, since that would run the conversion and hooks twice. + ## User Input ```text diff --git a/extensions/github/extension.yml b/extensions/github/extension.yml index c6df8f19a1..426e0a35cd 100644 --- a/extensions/github/extension.yml +++ b/extensions/github/extension.yml @@ -3,18 +3,15 @@ schema_version: "1.0" extension: id: github name: "GitHub Integration" - version: "1.0.2" + version: "1.1.0" description: "GitHub platform integration for Spec Kit - create GitHub issues from a feature's task list" author: spec-kit-core repository: https://github.com/github/spec-kit license: MIT requires: - # 0.12.17 is the first release that both rewrites extension-local - # "scripts/..." paths (#3364) and resolves __SPECKIT_COMMAND_*__ tokens in - # auto-registered skills (#3544). Earlier versions cannot render this - # command correctly in every registerable layout. - speckit_version: ">=0.12.17" + # Earlier hosts still own taskstoissues and reject its compatibility alias. + speckit_version: ">=1.2.3.dev0" tools: - name: git required: true @@ -29,6 +26,8 @@ provides: - name: speckit.github.taskstoissues file: commands/speckit.github.taskstoissues.md description: "Convert tasks from tasks.md into dependency-ordered GitHub issues" + aliases: + - speckit.taskstoissues tags: - "github" diff --git a/src/specify_cli/__init__.py b/src/specify_cli/__init__.py index 181b0feec1..402a7fe564 100644 --- a/src/specify_cli/__init__.py +++ b/src/specify_cli/__init__.py @@ -384,7 +384,6 @@ def _print_cli_warning( "clarify": "Structured clarification workflow for underspecified requirements.", "constitution": "Create or update project governing principles and development guidelines.", "checklist": "Generate custom quality checklists for validating requirements completeness and clarity.", - "taskstoissues": "Deprecated: Convert tasks from tasks.md into GitHub issues.", } diff --git a/src/specify_cli/extensions/__init__.py b/src/specify_cli/extensions/__init__.py index d03cb85770..90072ce7c1 100644 --- a/src/specify_cli/extensions/__init__.py +++ b/src/specify_cli/extensions/__init__.py @@ -67,7 +67,6 @@ "plan", "specify", "tasks", - "taskstoissues", } ) EXTENSION_COMMAND_NAME_PATTERN = re.compile(r"^speckit\.([a-z0-9-]+)\.([a-z0-9-]+)$") @@ -1206,8 +1205,8 @@ def _normalize_shadow_name(name: str) -> str: Agent integrations (Cline, Forge, Junie) and the SKILL.md output-name computation (``CommandRegistrar._compute_output_name``) all collapse dots to hyphens and prefix a bare name with ``speckit-``, so - ``speckit.taskstoissues``, ``taskstoissues``, and - ``speckit-taskstoissues`` are distinct alias spellings that land on + ``speckit.tasks``, ``tasks``, and + ``speckit-tasks`` are distinct alias spellings that land on the same on-disk command name. Normalize before comparing so all of them are caught, not just the exact dotted spelling. """ @@ -1586,7 +1585,7 @@ def _register_extension_skills( ) -> List[str]: """Generate SKILL.md files for extension commands as agent skills. - For every command in the extension manifest, creates a SKILL.md + For every command and alias in the extension manifest, creates a SKILL.md file in the agent's skills directory following the agentskills.io specification. This is only done when skills mode was used during project initialisation. @@ -1652,8 +1651,11 @@ def _replacement(match: re.Match[str]) -> str: r"__SPECKIT_COMMAND_([A-Z][A-Z0-9_-]*)__", _replacement, body ) - for cmd_info in manifest.commands: - cmd_name = cmd_info["name"] + for cmd_info, cmd_name in ( + (command, name) + for command in manifest.commands + for name in [command["name"], *(command.get("aliases") or [])] + ): cmd_file_rel = cmd_info["file"] # Guard against path traversal: reject absolute paths and ensure @@ -2015,7 +2017,7 @@ def _snapshot_generic_refresh_artifacts( if isinstance(registered, dict): command_names.update(self._valid_name_list(registered.get("generic"))) skill_names = ( - {self._skill_name_for_command(command["name"]) for command in manifest.commands} + {self._skill_name_for_command(name) for name in self._collect_manifest_command_names(manifest)} if manifest is not None and skills_mode_active else set() ) skill_names.update(self._valid_name_list(metadata.get("registered_skills"))) @@ -2178,8 +2180,8 @@ def _remove_generic_artifact_paths( for name in [command["name"], *(command.get("aliases") or [])] ) skill_names.update( - self._skill_name_for_command(command["name"]) - for command in manifest.commands + self._skill_name_for_command(name) + for name in self._collect_manifest_command_names(manifest) ) hashes = metadata.get("generic_artifact_hashes", {}) if not isinstance(hashes, dict): @@ -2540,7 +2542,7 @@ def install_from_directory( ) from exc skills = is_ai_skills_enabled(active_options) names = ( - {self._skill_name_for_command(command["name"]) for command in manifest.commands} + {self._skill_name_for_command(name) for name in self._collect_manifest_command_names(manifest)} if skills else self._collect_manifest_command_names(manifest) ) owned = ( @@ -4066,8 +4068,8 @@ def register_enabled_extensions_for_agent(self, agent_name: str, *, force: bool ext_id, metadata, [ - self._skill_name_for_command(cmd["name"]) - for cmd in manifest.commands + self._skill_name_for_command(name) + for name in self._collect_manifest_command_names(manifest) ], registered_skills, skills=True, diff --git a/src/specify_cli/integrations/alquimia/__init__.py b/src/specify_cli/integrations/alquimia/__init__.py index 54ea47f486..271451749d 100644 --- a/src/specify_cli/integrations/alquimia/__init__.py +++ b/src/specify_cli/integrations/alquimia/__init__.py @@ -18,7 +18,6 @@ "clarify": "Optional areas to clarify in the spec", "constitution": "Principles or values for the project constitution", "checklist": "Domain or focus area for the checklist", - "taskstoissues": "Optional filter or label for GitHub issues", } diff --git a/src/specify_cli/integrations/base.py b/src/specify_cli/integrations/base.py index b3dd0abc34..c20924f840 100644 --- a/src/specify_cli/integrations/base.py +++ b/src/specify_cli/integrations/base.py @@ -53,7 +53,6 @@ "checklist", "specify", "tasks", - "taskstoissues", ) _CORE_COMMAND_TEMPLATE_RANK = { command: index for index, command in enumerate(_CORE_COMMAND_TEMPLATE_ORDER) diff --git a/src/specify_cli/integrations/claude/__init__.py b/src/specify_cli/integrations/claude/__init__.py index 03d428bc51..e2de919b8c 100644 --- a/src/specify_cli/integrations/claude/__init__.py +++ b/src/specify_cli/integrations/claude/__init__.py @@ -20,7 +20,6 @@ "clarify": "Optional areas to clarify in the spec", "constitution": "Principles or values for the project constitution", "checklist": "Domain or focus area for the checklist", - "taskstoissues": "Optional filter or label for GitHub issues", } # Per-command frontmatter overrides for skills that should run in a forked diff --git a/src/specify_cli/integrations/copilot/__init__.py b/src/specify_cli/integrations/copilot/__init__.py index c41f55e87a..05fa6d657f 100644 --- a/src/specify_cli/integrations/copilot/__init__.py +++ b/src/specify_cli/integrations/copilot/__init__.py @@ -43,7 +43,6 @@ "plan", "specify", "tasks", - "taskstoissues", } diff --git a/src/specify_cli/presets/_manager_skills.py b/src/specify_cli/presets/_manager_skills.py index 5a4a4fb739..4adbdf2a61 100644 --- a/src/specify_cli/presets/_manager_skills.py +++ b/src/specify_cli/presets/_manager_skills.py @@ -611,10 +611,12 @@ def _build_extension_skill_restore_index(self) -> Dict[str, Dict[str, Any]]: "extension_id": manifest.id, "extension_dir": ext_root, } - modern_skill_name, legacy_skill_name = self._skill_names_for_command(cmd_name) - restore_index.setdefault(modern_skill_name, restore_info) - if legacy_skill_name != modern_skill_name: - restore_index.setdefault(legacy_skill_name, restore_info) + for name in [cmd_name, *(cmd_info.get("aliases") or [])]: + modern_skill_name, legacy_skill_name = self._skill_names_for_command(name) + named_restore_info = {**restore_info, "command_name": name} + restore_index.setdefault(modern_skill_name, named_restore_info) + if legacy_skill_name != modern_skill_name: + restore_index.setdefault(legacy_skill_name, named_restore_info) return restore_index @@ -1496,6 +1498,9 @@ def _unregister_skills_in_dir( ) continue frontmatter, body = registrar.parse_frontmatter(content) + frontmatter = registrar._adjust_script_paths( + frontmatter, extension_id=extension_restore["extension_id"] + ) # Mirror the register-time rewrite (#2101): resolve # extension-relative subdir references (agents/, # knowledge-base/, etc.) to their installed location before @@ -1509,7 +1514,8 @@ def _unregister_skills_in_dir( ) if isinstance(selected_ai, str): body = registrar.resolve_skill_placeholders( - selected_ai, frontmatter, body, self.project_root + selected_ai, frontmatter, body, self.project_root, + extension_id=extension_restore["extension_id"], ) body = self._resolve_skill_command_refs( body, registrar, selected_ai, self.project_root diff --git a/templates/commands/taskstoissues.md b/templates/commands/taskstoissues.md deleted file mode 100644 index dea7e7cfe5..0000000000 --- a/templates/commands/taskstoissues.md +++ /dev/null @@ -1,114 +0,0 @@ ---- -description: "Deprecated: Convert existing tasks into actionable, dependency-ordered GitHub issues for the feature based on available design artifacts." -tools: ['github/github-mcp-server/list_issues', 'github/github-mcp-server/issue_write'] -scripts: - sh: scripts/bash/check-prerequisites.sh --json --require-tasks --include-tasks - ps: scripts/powershell/check-prerequisites.ps1 -Json -RequireTasks -IncludeTasks - py: scripts/python/check_prerequisites.py --json --require-tasks --include-tasks ---- - -## Deprecation Notice - -You **MUST display the following concise warning first**, as the first step of this command and before the User Input and Pre-Execution Checks sections below: - -> ⚠️ `__SPECKIT_COMMAND_TASKSTOISSUES__` is deprecated and will be removed in a future minor release. Use the bundled `github` extension instead: run `specify extension add github`, then invoke `__SPECKIT_COMMAND_GITHUB_TASKSTOISSUES__`. - -Continue with the existing workflow unchanged. Do not install or enable the extension automatically, do not stop, and do not run the replacement command. - -## User Input - -```text -$ARGUMENTS -``` - -You **MUST** consider the user input before proceeding (if not empty). - -## Pre-Execution Checks - -**Check for extension hooks (before tasks-to-issues conversion)**: -- Check if `.specify/extensions.yml` exists in the project root. -- If it exists, read it and look for entries under the `hooks.before_taskstoissues` key -- If the YAML cannot be parsed or is invalid, do not skip silently: tell the user that `.specify/extensions.yml` could not be read (include the parser error) and that no hooks were checked, including any mandatory (`optional: false`) hooks registered there, then continue normally -- Filter out hooks where `enabled` is explicitly `false`. Treat hooks without an `enabled` field as enabled by default. -- For each remaining hook, do **not** attempt to interpret or evaluate hook `condition` expressions: - - If the hook has no `condition` field, or it is null/empty, treat the hook as executable - - If the hook defines a non-empty `condition`, skip the hook and leave condition evaluation to the HookExecutor implementation -- For each executable hook, output the following based on its `optional` flag: - - **Optional hook** (`optional: true`): - ``` - ## Extension Hooks - - **Optional Pre-Hook**: {extension} - Command: `/{command}` - Description: {description} - - Prompt: {prompt} - To execute: `/{command}` - ``` - - **Mandatory hook** (`optional: false`): - ``` - ## Extension Hooks - - **Automatic Pre-Hook**: {extension} - Executing: `/{command}` - EXECUTE_COMMAND: {command} - - Wait for the result of the hook command before proceeding to the Outline. - ``` - After emitting the block above you MUST actually invoke the hook and wait for it to finish before continuing. Run it the same way you would run the command yourself in this agent/session (the invocation may differ from the literal `{command}` id shown above, e.g. a skills-mode agent runs it as `/skill:speckit-...` or `$speckit-...`). Emitting the block alone does not run the hook. -- If no hooks are registered or `.specify/extensions.yml` does not exist, skip silently - -## Outline - -1. Run `{SCRIPT}` from repo root and parse FEATURE_DIR and AVAILABLE_DOCS list. All paths must be absolute. For single quotes in args like "I'm Groot", use escape syntax: e.g 'I'\''m Groot' (or double-quote if possible: "I'm Groot"). -1. **IF EXISTS**: Load `/memory/constitution.md` for project principles and governance constraints. -1. From the executed script, extract the path to **tasks**. -1. Get the Git remote by running: - -```bash -git config --get remote.origin.url -``` - -> [!CAUTION] -> ONLY PROCEED TO NEXT STEPS IF THE REMOTE IS A GITHUB URL - -1. **Fetch existing issues for deduplication**: Before creating anything, build the set of task IDs you are about to process from `tasks.md` (each is a `T` followed by **at least** three digits, e.g. `T001` — `__SPECKIT_COMMAND_CONVERGE__` assigns new IDs with `T{M+1:03d}`, which is a floor rather than a cap, so once a file has more than 999 tasks the IDs are four digits or longer). Then use the GitHub MCP server's `list_issues` tool to look for issues that already cover those IDs. Do not pass a `state` value, since omitting it makes the tool return both open and closed issues. Request `perPage: 100` to keep the number of calls down, and since the tool uses cursor-based pagination, request pages with the `after` parameter (using the `endCursor` from the previous response). For each issue title, match it against the task ID pattern `\bT\d{3,}\b` (the `{3,}` accepts four-digit and longer IDs — with `\d{3}` a title containing `T1000` would not match at all, because the trailing `\b` cannot fall between two digits, so that task would be silently neither deduplicated nor created; word boundaries still stop a token like `ST001` from matching, and force the whole digit run to be consumed so `T100` can never match inside `T1000`; this also recognises titles written as `T001 ...`, `T001: ...` or `[T001] ...`) and, when it matches one of your task IDs, mark that ID as already having an issue. Stop paginating as soon as every task ID has been matched, or when there are no more pages, so you do not keep fetching the whole repository's issue history once all task IDs are accounted for. This bounds the number of calls on repos with large issue histories and still prevents duplicates when the command is re-run after `tasks.md` is regenerated or the skill is re-invoked. -1. For each task in the list, use the GitHub MCP server to create a new issue in the repository that is representative of the Git remote. Task lines in `tasks.md` start with a markdown checkbox, so first strip the leading `- [ ]` (and any `[P]` / `[US#]` markers) to recover the task ID and its description. Create the issue with a single canonical title of the form `T001: `, with the ID written once followed by the task description (for example, the line `- [ ] T001 Create project structure` becomes the title `T001: Create project structure`). - - **Skip** any task whose ID is already present in the set of existing issues from the previous step, and report it (for example, `T001 already has an issue, skipping`). - - Only create issues for tasks that do not yet have a matching issue. - -> [!CAUTION] -> UNDER NO CIRCUMSTANCES EVER CREATE ISSUES IN REPOSITORIES THAT DO NOT MATCH THE REMOTE URL - -## Post-Execution Checks - -**Check for extension hooks (after tasks-to-issues conversion)**: -Check if `.specify/extensions.yml` exists in the project root. -- If it exists, read it and look for entries under the `hooks.after_taskstoissues` key -- If the YAML cannot be parsed or is invalid, do not skip silently: tell the user that `.specify/extensions.yml` could not be read (include the parser error) and that no hooks were checked, including any mandatory (`optional: false`) hooks registered there, then continue normally -- Filter out hooks where `enabled` is explicitly `false`. Treat hooks without an `enabled` field as enabled by default. -- For each remaining hook, do **not** attempt to interpret or evaluate hook `condition` expressions: - - If the hook has no `condition` field, or it is null/empty, treat the hook as executable - - If the hook defines a non-empty `condition`, skip the hook and leave condition evaluation to the HookExecutor implementation -- For each executable hook, output the following based on its `optional` flag: - - **Optional hook** (`optional: true`): - ``` - ## Extension Hooks - - **Optional Hook**: {extension} - Command: `/{command}` - Description: {description} - - Prompt: {prompt} - To execute: `/{command}` - ``` - - **Mandatory hook** (`optional: false`): - ``` - ## Extension Hooks - - **Automatic Hook**: {extension} - Executing: `/{command}` - EXECUTE_COMMAND: {command} - ``` - After emitting the block above you MUST actually invoke the hook and wait for it to finish before continuing. Run it the same way you would run the command yourself in this agent/session (the invocation may differ from the literal `{command}` id shown above, e.g. a skills-mode agent runs it as `/skill:speckit-...` or `$speckit-...`). Emitting the block alone does not run the hook. -- If no hooks are registered or `.specify/extensions.yml` does not exist, skip silently diff --git a/tests/extensions/github/test_github_extension.py b/tests/extensions/github/test_github_extension.py index 718c1c8421..2b23fb4c70 100644 --- a/tests/extensions/github/test_github_extension.py +++ b/tests/extensions/github/test_github_extension.py @@ -3,7 +3,7 @@ Validates: - Bundled layout (manifest, README, command file, script twins) - Catalog registration and wheel/source resolution via ``_locate_bundled_extension`` -- Manifest validation, including that no alias claims the core command name +- Manifest validation, including the retired core command's compatibility alias - Install/uninstall through ``ExtensionManager`` - Rendered command artifacts across command mode and skills mode, and in particular that ``{SCRIPT}`` resolves to a script the extension actually @@ -40,10 +40,9 @@ # A released Spec Kit version that satisfies the manifest floor. Installs are # refused below it, so this cannot be an arbitrary synthetic value. -INSTALL_SPECKIT_VERSION = "1.0.12" -# The floor exists because auto-registered skills did not resolve command -# reference tokens until 0.12.17 (#3544). -MIN_SPECKIT_VERSION = "0.12.17" +INSTALL_SPECKIT_VERSION = "1.2.3.dev0" +# Earlier hosts still own taskstoissues and cannot accept the alias. +MIN_SPECKIT_VERSION = "1.2.3.dev0" COMMAND_NAME = "speckit.github.taskstoissues" COMMAND_FILE = EXT_DIR / "commands" / f"{COMMAND_NAME}.md" @@ -158,7 +157,7 @@ def test_manifest_validates(self): m = ExtensionManifest(EXT_DIR / "extension.yml") assert m.id == "github" - assert m.version == "1.0.2" + assert m.version == "1.1.0" assert [c["name"] for c in m.commands] == [COMMAND_NAME] def test_manifest_command_files_exist(self): @@ -168,18 +167,13 @@ def test_manifest_command_files_exist(self): for cmd in m.commands: assert (EXT_DIR / cmd["file"]).is_file() - def test_no_alias_claims_the_core_command(self): - """Stage 1 keeps the core command; nothing here may shadow it. - - Alias names are not pattern-checked and the core-namespace guard - applies to primary names only, so this is author discipline that a - test has to hold in place. - """ + def test_alias_claims_only_the_retired_core_command(self): + """The legacy spelling is owned exclusively by the opt-in extension.""" from specify_cli.extensions import ExtensionManifest m = ExtensionManifest(EXT_DIR / "extension.yml") aliases = [a for cmd in m.commands for a in cmd.get("aliases", []) or []] - assert aliases == [] + assert aliases == ["speckit.taskstoissues"] def test_declares_its_mandatory_external_tools(self): """The command runs ``git config`` and needs the GitHub MCP tools.""" @@ -188,32 +182,41 @@ def test_declares_its_mandatory_external_tools(self): assert all(t["required"] is True for t in tools.values()) def test_version_floor_covers_all_rendering_prerequisites(self): - """The floor must exclude releases that cannot render this command. - - Extension-local ``scripts/...`` rewriting landed in 0.12.6 (#3364), - but auto-registered skills did not resolve ``__SPECKIT_COMMAND_*__`` - tokens until 0.12.17 (#3544). Versions through 0.12.16 therefore - accept the extension but leak a raw command token in skills mode. - """ + """Hosts retaining core ownership must not accept this extension.""" from packaging.specifiers import SpecifierSet floor = _manifest_dict()["requires"]["speckit_version"] spec = SpecifierSet(floor) assert "0.12.5" not in spec, floor assert "0.12.16" not in spec, floor + assert "1.2.2" not in spec, floor assert MIN_SPECKIT_VERSION in spec, floor # And the version the install tests use must satisfy it. assert INSTALL_SPECKIT_VERSION in spec, floor - def test_core_command_remains_unchanged(self): - """Stage 1 is additive: the core command still ships.""" - assert CORE_COMMAND.is_file() + def test_core_command_no_longer_ships(self): + assert not CORE_COMMAND.exists() # -- Install / uninstall ------------------------------------------------------ class TestExtensionInstall: + @pytest.mark.parametrize("host_version", ["0.12.17", "1.0.13", "1.1.3", "1.2.2"]) + def test_install_refuses_hosts_that_still_own_the_core_command( + self, tmp_path, host_version, + ): + from specify_cli.extensions import CompatibilityError, ExtensionManager + + (tmp_path / ".specify").mkdir() + manager = ExtensionManager(tmp_path) + with pytest.raises(CompatibilityError): + manager.install_from_directory( + EXT_DIR, host_version, register_commands=False, + ) + assert not manager.registry.is_installed("github") + assert not (tmp_path / ".specify/extensions/github").exists() + @pytest.mark.parametrize("skills_mode", [False, True], ids=["commands", "skills"]) def test_generic_registers_and_removes_artifact_in_configured_directory( self, tmp_path: Path, skills_mode: bool @@ -249,8 +252,8 @@ def test_generic_registers_and_removes_artifact_in_configured_directory( commands_dir = project / ".myagent" / "commands" commands_dir.mkdir(parents=True) core = commands_dir / ( - "speckit-taskstoissues/SKILL.md" - if skills_mode else "speckit.taskstoissues.md" + "speckit-tasks/SKILL.md" + if skills_mode else "speckit.tasks.md" ) core.parent.mkdir(parents=True, exist_ok=True) core.write_text("core command remains unchanged\n", encoding="utf-8") @@ -269,6 +272,14 @@ def test_generic_registers_and_removes_artifact_in_configured_directory( if skills_mode else f"{COMMAND_NAME}.md" ) assert artifact.is_file() + alias = commands_dir / ( + "speckit-taskstoissues/SKILL.md" + if skills_mode else "speckit.taskstoissues.md" + ) + assert alias.is_file() + alias_content = alias.read_text(encoding="utf-8") + assert "deprecated compatibility alias" in alias_content + assert "__SPECKIT_COMMAND_" not in alias_content content = artifact.read_text(encoding="utf-8") assert ".specify/extensions/github/scripts/bash/resolve-tasks.sh" in content assert "{SCRIPT}" not in content @@ -277,6 +288,7 @@ def test_generic_registers_and_removes_artifact_in_configured_directory( ).exists() assert manager.remove("github") assert not artifact.exists() + assert not alias.exists() assert core.read_text(encoding="utf-8") == "core command remains unchanged\n" def test_install_copies_command_and_scripts(self, tmp_path: Path): @@ -318,13 +330,18 @@ def test_remove_uninstalls_cleanly(self, tmp_path: Path): [ f".github/agents/{COMMAND_NAME}.agent.md", f".github/prompts/{COMMAND_NAME}.prompt.md", + ".github/agents/speckit.taskstoissues.agent.md", + ".github/prompts/speckit.taskstoissues.prompt.md", ], - ".github/agents/speckit.taskstoissues.agent.md", + ".github/agents/speckit.tasks.agent.md", ), ( "claude", - [".claude/skills/speckit-github-taskstoissues/SKILL.md"], - ".claude/skills/speckit-taskstoissues/SKILL.md", + [ + ".claude/skills/speckit-github-taskstoissues/SKILL.md", + ".claude/skills/speckit-taskstoissues/SKILL.md", + ], + ".claude/skills/speckit-tasks/SKILL.md", ), ], ids=["command-mode", "skills-mode"], @@ -344,7 +361,7 @@ def test_remove_deletes_registered_artifacts_and_keeps_core( ) core = tmp_path / core_artifact core.parent.mkdir(parents=True) - core.write_text("core taskstoissues\n", encoding="utf-8") + core.write_text("core tasks\n", encoding="utf-8") manager = ExtensionManager(tmp_path) manager.install_from_directory( @@ -357,7 +374,7 @@ def test_remove_deletes_registered_artifacts_and_keeps_core( for rel_path in extension_artifacts: assert not (tmp_path / rel_path).exists(), f"Left behind: {rel_path}" - assert core.read_text(encoding="utf-8") == "core taskstoissues\n" + assert core.read_text(encoding="utf-8") == "core tasks\n" # -- Rendered command artifacts ----------------------------------------------- @@ -440,8 +457,8 @@ def test_every_extension_registrar_integration_renders_the_command( Covers both layouts in one sweep — command-file agents, skills-mode agents, and Hermes, which installs to ``~/.hermes/skills`` rather than - a project-local directory (hence the redirected home). Generic has no - static registrar config and does not register extension add-ons. + a project-local directory (hence the redirected home). Generic resolves + persisted output options and is covered separately. """ from specify_cli.extensions import CommandRegistrar, ExtensionManager @@ -474,6 +491,12 @@ def test_every_extension_registrar_integration_renders_the_command( and "taskstoissues" in p.as_posix().lower() ] assert artifacts, f"{agent} produced no command artifact" + assert any("github" in p.name or "github" in p.parent.name for p in artifacts) + aliases = [ + p for p in artifacts + if "github" not in p.name and "github" not in p.parent.name + ] + assert aliases, f"{agent} produced no compatibility alias" expected = f".specify/extensions/github/{SCRIPT_TWINS['sh']}" bodies = [p.read_text(encoding="utf-8") for p in artifacts] @@ -481,6 +504,11 @@ def test_every_extension_registrar_integration_renders_the_command( # No artifact may leak an unresolved placeholder... for artifact, content in zip(artifacts, bodies): assert "{SCRIPT}" not in content, artifact + assert "__SPECKIT_COMMAND_" not in content, artifact + assert any( + "deprecated compatibility alias" in path.read_text(encoding="utf-8") + for path in aliases + ) # ...and the command body must carry the resolved extension-local path. # Some integrations also emit a thin companion file (e.g. Copilot's # prompt shim, which only points at the agent), so this is "at least @@ -524,7 +552,7 @@ def test_rendered_skill_points_at_a_script_that_ships(self, tmp_path: Path): assert (project / expected).is_file() -# -- Behaviour parity with the core command ----------------------------------- +# -- Preserved conversion contracts ------------------------------------------ class TestCommandBody: @@ -598,44 +626,14 @@ def test_preserves_deduplication_and_pagination(self): # Four-digit and longer task IDs must still match. assert r"\bT\d{3,}\b" in body - def test_body_differs_from_core_only_in_deprecation_and_script_invocation(self): - """Behaviour parity, enforced as a diff rather than as spot checks. - - Apart from core-only deprecation metadata and notice, everything except - the ``scripts:`` frontmatter and the two lines that read the new ``TASKS`` - value must match verbatim, so the two cannot silently drift while both exist. - """ - import difflib - - core_text = CORE_COMMAND.read_text(encoding="utf-8") - before_notice, notice_and_workflow = core_text.split("## Deprecation Notice\n", 1) - _, workflow = notice_and_workflow.split("## User Input", 1) - core = (before_notice + "## User Input" + workflow).splitlines() - ext = COMMAND_FILE.read_text(encoding="utf-8").splitlines() - core_description = yaml.safe_load(core[1])["description"] - ext_description = yaml.safe_load(ext[1])["description"] - assert core_description == "Deprecated: " + ext_description - core[1] = ext[1] - changed = [ - line - for line in difflib.unified_diff(core, ext, n=0) - if line.startswith(("+", "-")) and not line.startswith(("+++", "---")) - ] - - # 3 script lines + 2 outline lines, each as one removal and one addition. - assert len(changed) == 10, "\n".join(changed) - - markers = ( - "check-prerequisites", - "check_prerequisites", - "resolve-tasks", - "resolve_tasks", - "AVAILABLE_DOCS", - "path to **tasks**", - ) - assert all( - any(marker in line for marker in markers) for line in changed - ), "\n".join(changed) + def test_body_uses_only_extension_local_prerequisites(self): + """The sole implementation cannot depend on the removed core command.""" + text = COMMAND_FILE.read_text(encoding="utf-8") + assert "parse FEATURE_DIR, TASKS and the AVAILABLE_DOCS list" in text + assert "extract the path to **tasks** (the `TASKS` value)" in text + assert "check-prerequisites" not in text + assert "check_prerequisites" not in text + assert set(_command_frontmatter()["scripts"]) == set(SCRIPT_TWINS) def test_uses_the_portable_command_reference_token(self): """A literal invocation would be correct for exactly one agent.""" diff --git a/tests/extensions/github/test_taskstoissues_deprecation.py b/tests/extensions/github/test_taskstoissues_deprecation.py deleted file mode 100644 index b18324863b..0000000000 --- a/tests/extensions/github/test_taskstoissues_deprecation.py +++ /dev/null @@ -1,99 +0,0 @@ -"""Text contracts for the core command's deprecation transition.""" - -from pathlib import Path - -import pytest -import yaml - - -ROOT = Path(__file__).resolve().parents[3] -CORE = ROOT / "templates/commands/taskstoissues.md" -EXTENSION = ROOT / "extensions/github/commands/speckit.github.taskstoissues.md" - - -def test_core_deprecation_warning_and_continuation(): - text = CORE.read_text(encoding="utf-8") - assert "## Deprecation Notice" in text - assert "## Pre-Execution Checks" in text - notice = text.split("## Deprecation Notice\n", 1)[1].split("## User Input", 1)[0] - assert text.index("## Deprecation Notice") < text.index("## User Input") < text.index("## Pre-Execution Checks") - for phrase in ( - "MUST display the following concise warning first", - "`__SPECKIT_COMMAND_TASKSTOISSUES__` is deprecated", - "will be removed in a future minor release", - "bundled `github` extension", - "specify extension add github", - "__SPECKIT_COMMAND_GITHUB_TASKSTOISSUES__", - "Continue with the existing workflow unchanged", - "Do not install or enable the extension automatically", - "do not stop", - "do not run the replacement command", - ): - assert phrase in notice - - -def test_core_frontmatter_preserves_execution_metadata(): - metadata = yaml.safe_load(CORE.read_text(encoding="utf-8").split("---", 2)[1]) - assert metadata["description"].startswith("Deprecated: ") - assert metadata["tools"] == [ - "github/github-mcp-server/list_issues", - "github/github-mcp-server/issue_write", - ] - assert metadata["scripts"] == { - "sh": "scripts/bash/check-prerequisites.sh --json --require-tasks --include-tasks", - "ps": "scripts/powershell/check-prerequisites.ps1 -Json -RequireTasks -IncludeTasks", - "py": "scripts/python/check_prerequisites.py --json --require-tasks --include-tasks", - } - - -def test_recommended_extension_is_not_deprecated(): - text = EXTENSION.read_text(encoding="utf-8") - assert "## Deprecation Notice" not in text - assert "future minor release" not in text - assert not yaml.safe_load(text.split("---", 2)[1])["description"].startswith("Deprecated:") - - -@pytest.mark.parametrize("command", [CORE, EXTENSION], ids=["core", "github"]) -def test_taskstoissues_hooks_and_safety_contracts_remain(command): - text = command.read_text(encoding="utf-8") - assert "hooks.before_taskstoissues" in text - assert "hooks.after_taskstoissues" in text - assert "ONLY PROCEED TO NEXT STEPS IF THE REMOTE IS A GITHUB URL" in text - assert "**Fetch existing issues for deduplication**" in text - assert "**Skip** any task whose ID is already present" in text - assert "Only create issues for tasks that do not yet have a matching issue." in text - assert "UNDER NO CIRCUMSTANCES EVER CREATE ISSUES IN REPOSITORIES THAT DO NOT MATCH THE REMOTE URL" in text - - -@pytest.mark.parametrize( - ("integration_key", "core_invocation", "replacement_invocation"), - [ - ("opencode", "/speckit.taskstoissues", "/speckit.github.taskstoissues"), - ("agy", "/speckit-taskstoissues", "/speckit-github-taskstoissues"), - ], -) -def test_rendered_core_warning_uses_integration_invocations( - tmp_path, integration_key, core_invocation, replacement_invocation -): - from specify_cli.integrations import get_integration - from specify_cli.integrations.manifest import IntegrationManifest - - integration = get_integration(integration_key) - created = integration.setup( - tmp_path, IntegrationManifest(integration_key, tmp_path), script_type="sh" - ) - command = next( - path for path in created - if path.name == "speckit.taskstoissues.md" - or path.parent.name == "speckit-taskstoissues" - ) - text = command.read_text(encoding="utf-8") - assert "## Deprecation Notice" in text - assert "## Pre-Execution Checks" in text - notice = text.split("## Deprecation Notice", 1)[1].split("## User Input", 1)[0] - assert f"`{core_invocation}` is deprecated" in notice - assert f"invoke `{replacement_invocation}`" in notice - assert "__SPECKIT_COMMAND_" not in notice - assert "specify extension add github" in notice - assert "Continue with the existing workflow unchanged" in notice - assert "Do not install or enable the extension automatically" in notice diff --git a/tests/extensions/github/test_taskstoissues_removal.py b/tests/extensions/github/test_taskstoissues_removal.py new file mode 100644 index 0000000000..ab371cc51a --- /dev/null +++ b/tests/extensions/github/test_taskstoissues_removal.py @@ -0,0 +1,89 @@ +"""Contracts for opt-in issue tracking after removal from core.""" + +from pathlib import Path + +import pytest +import yaml + + +ROOT = Path(__file__).resolve().parents[3] +CORE = ROOT / "templates/commands/taskstoissues.md" +EXTENSION = ROOT / "extensions/github/commands/speckit.github.taskstoissues.md" + + +def test_core_template_is_removed(): + assert not CORE.exists() + + +def test_extension_frontmatter_preserves_execution_metadata(): + metadata = yaml.safe_load(EXTENSION.read_text(encoding="utf-8").split("---", 2)[1]) + assert metadata["tools"] == [ + "github/github-mcp-server/list_issues", + "github/github-mcp-server/issue_write", + ] + assert metadata["scripts"] == { + "sh": "scripts/bash/resolve-tasks.sh --json", + "ps": "scripts/powershell/resolve-tasks.ps1 -Json", + "py": "scripts/python/resolve_tasks.py --json", + } + + +def test_recommended_extension_is_not_deprecated(): + text = EXTENSION.read_text(encoding="utf-8") + assert "## Deprecation Notice" not in text + assert "future minor release" not in text + assert not yaml.safe_load(text.split("---", 2)[1])["description"].startswith("Deprecated:") + + +def test_taskstoissues_hooks_and_safety_contracts_remain(): + text = EXTENSION.read_text(encoding="utf-8") + assert "hooks.before_taskstoissues" in text + assert "hooks.after_taskstoissues" in text + assert "ONLY PROCEED TO NEXT STEPS IF THE REMOTE IS A GITHUB URL" in text + assert "**Fetch existing issues for deduplication**" in text + assert "**Skip** any task whose ID is already present" in text + assert "Only create issues for tasks that do not yet have a matching issue." in text + assert "UNDER NO CIRCUMSTANCES EVER CREATE ISSUES IN REPOSITORIES THAT DO NOT MATCH THE REMOTE URL" in text + + +@pytest.mark.parametrize( + "integration_key", ["opencode", "agy", "claude", "copilot", "gemini", "goose"], +) +def test_fresh_integration_has_no_issue_tracking(tmp_path, integration_key): + from specify_cli.integrations import get_integration + from specify_cli.integrations.manifest import IntegrationManifest + + integration = get_integration(integration_key) + created = integration.setup( + tmp_path, IntegrationManifest(integration_key, tmp_path), script_type="sh" + ) + assert created + assert not any("taskstoissues" in path.as_posix() for path in created) + assert not (tmp_path / ".specify/extensions/github").exists() + + +def test_core_inventories_release_the_legacy_name(): + from specify_cli import SKILL_DESCRIPTIONS + from specify_cli.extensions import CORE_COMMAND_NAMES, _FALLBACK_CORE_COMMAND_NAMES + from specify_cli.integrations.base import _CORE_COMMAND_TEMPLATE_ORDER + from specify_cli.integrations.copilot import _COPILOT_CORE_COMMANDS + from specify_cli.integrations.claude import ARGUMENT_HINTS as claude_hints + from specify_cli.integrations.alquimia import ARGUMENT_HINTS as alquimia_hints + + for inventory in ( + SKILL_DESCRIPTIONS, CORE_COMMAND_NAMES, _FALLBACK_CORE_COMMAND_NAMES, + _CORE_COMMAND_TEMPLATE_ORDER, _COPILOT_CORE_COMMANDS, + claude_hints, alquimia_hints, + ): + assert "taskstoissues" not in inventory + + +def test_compatibility_alias_is_deprecated_without_deprecating_canonical_command(): + text = EXTENSION.read_text(encoding="utf-8") + notice = text.split("## Compatibility Alias\n", 1)[1].split("## User Input", 1)[0] + assert "Only when invoked" in notice + assert "__SPECKIT_COMMAND_TASKSTOISSUES__" in notice + assert "__SPECKIT_COMMAND_GITHUB_TASKSTOISSUES__" in notice + assert "deprecated compatibility alias" in notice + assert "Continue with the workflow below" in notice + assert "Do not invoke the canonical command again" in notice diff --git a/tests/integrations/test_integration_base_markdown.py b/tests/integrations/test_integration_base_markdown.py index e94f58c6ff..16ee0bd49d 100644 --- a/tests/integrations/test_integration_base_markdown.py +++ b/tests/integrations/test_integration_base_markdown.py @@ -220,7 +220,7 @@ def test_integration_flag_creates_files(self, tmp_path): COMMAND_STEMS = [ "analyze", "clarify", "constitution", "converge", "implement", - "plan", "checklist", "specify", "tasks", "taskstoissues", + "plan", "checklist", "specify", "tasks", ] def _expected_files(self, script_variant: str) -> list[str]: diff --git a/tests/integrations/test_integration_base_skills.py b/tests/integrations/test_integration_base_skills.py index 015152be01..f190508382 100644 --- a/tests/integrations/test_integration_base_skills.py +++ b/tests/integrations/test_integration_base_skills.py @@ -95,7 +95,7 @@ def test_skill_directory_structure(self, tmp_path): expected_commands = { "analyze", "clarify", "constitution", "converge", "implement", - "plan", "checklist", "specify", "tasks", "taskstoissues", + "plan", "checklist", "specify", "tasks", } # Derive command names from the skill directory names @@ -466,7 +466,7 @@ def test_options_include_skills_flag(self): _SKILL_COMMANDS = [ "analyze", "clarify", "constitution", "converge", "implement", - "plan", "checklist", "specify", "tasks", "taskstoissues", + "plan", "checklist", "specify", "tasks", ] def _expected_files(self, script_variant: str) -> list[str]: diff --git a/tests/integrations/test_integration_base_toml.py b/tests/integrations/test_integration_base_toml.py index 1b1bb18807..c7950b85fb 100644 --- a/tests/integrations/test_integration_base_toml.py +++ b/tests/integrations/test_integration_base_toml.py @@ -470,7 +470,6 @@ def test_integration_flag_creates_files(self, tmp_path): "checklist", "specify", "tasks", - "taskstoissues", ] def _expected_files(self, script_variant: str) -> list[str]: diff --git a/tests/integrations/test_integration_base_yaml.py b/tests/integrations/test_integration_base_yaml.py index 01914c5988..68c921ed23 100644 --- a/tests/integrations/test_integration_base_yaml.py +++ b/tests/integrations/test_integration_base_yaml.py @@ -384,7 +384,6 @@ def test_integration_flag_creates_files(self, tmp_path): "checklist", "specify", "tasks", - "taskstoissues", ] def _expected_files(self, script_variant: str) -> list[str]: diff --git a/tests/integrations/test_integration_bob.py b/tests/integrations/test_integration_bob.py index 752d7a42bc..8e85c66559 100644 --- a/tests/integrations/test_integration_bob.py +++ b/tests/integrations/test_integration_bob.py @@ -246,7 +246,7 @@ def test_skill_directory_structure(self, tmp_path): expected_commands = { "analyze", "clarify", "constitution", "converge", "implement", - "plan", "checklist", "specify", "tasks", "taskstoissues", + "plan", "checklist", "specify", "tasks", } actual_commands = {f.parent.name.removeprefix("speckit-") for f in created} assert actual_commands == expected_commands @@ -411,7 +411,7 @@ def test_init_default_complete_file_inventory_sh(self, tmp_path): commands = [ "analyze", "clarify", "constitution", "converge", "implement", - "plan", "checklist", "specify", "tasks", "taskstoissues", + "plan", "checklist", "specify", "tasks", ] for cmd in commands: assert (project / ".bob" / "skills" / f"speckit-{cmd}" / "SKILL.md").exists(), ( diff --git a/tests/integrations/test_integration_copilot.py b/tests/integrations/test_integration_copilot.py index 921b023be6..0044007b7d 100644 --- a/tests/integrations/test_integration_copilot.py +++ b/tests/integrations/test_integration_copilot.py @@ -176,10 +176,10 @@ def test_directory_structure(self, tmp_path): agents_dir = tmp_path / ".github" / "agents" assert agents_dir.is_dir() agent_files = sorted(agents_dir.glob("speckit.*.agent.md")) - assert len(agent_files) == 10 + assert len(agent_files) == 9 expected_commands = { "analyze", "clarify", "constitution", "converge", "implement", - "plan", "checklist", "specify", "tasks", "taskstoissues", + "plan", "checklist", "specify", "tasks", } actual_commands = {f.name.removeprefix("speckit.").removesuffix(".agent.md") for f in agent_files} assert actual_commands == expected_commands @@ -289,7 +289,6 @@ def test_complete_file_inventory_sh(self, tmp_path): ".github/agents/speckit.plan.agent.md", ".github/agents/speckit.specify.agent.md", ".github/agents/speckit.tasks.agent.md", - ".github/agents/speckit.taskstoissues.agent.md", ".github/prompts/speckit.analyze.prompt.md", ".github/prompts/speckit.checklist.prompt.md", ".github/prompts/speckit.clarify.prompt.md", @@ -299,7 +298,6 @@ def test_complete_file_inventory_sh(self, tmp_path): ".github/prompts/speckit.plan.prompt.md", ".github/prompts/speckit.specify.prompt.md", ".github/prompts/speckit.tasks.prompt.md", - ".github/prompts/speckit.taskstoissues.prompt.md", ".vscode/settings.json", ".specify/integration.json", ".specify/init-options.json", @@ -354,7 +352,6 @@ def test_complete_file_inventory_ps(self, tmp_path): ".github/agents/speckit.plan.agent.md", ".github/agents/speckit.specify.agent.md", ".github/agents/speckit.tasks.agent.md", - ".github/agents/speckit.taskstoissues.agent.md", ".github/prompts/speckit.analyze.prompt.md", ".github/prompts/speckit.checklist.prompt.md", ".github/prompts/speckit.clarify.prompt.md", @@ -364,7 +361,6 @@ def test_complete_file_inventory_ps(self, tmp_path): ".github/prompts/speckit.plan.prompt.md", ".github/prompts/speckit.specify.prompt.md", ".github/prompts/speckit.tasks.prompt.md", - ".github/prompts/speckit.taskstoissues.prompt.md", ".vscode/settings.json", ".specify/integration.json", ".specify/init-options.json", @@ -397,7 +393,7 @@ class TestCopilotSkillsMode: _SKILL_COMMANDS = [ "analyze", "clarify", "constitution", "converge", "implement", - "plan", "checklist", "specify", "tasks", "taskstoissues", + "plan", "checklist", "specify", "tasks", ] def _make_copilot(self): diff --git a/tests/integrations/test_integration_generic.py b/tests/integrations/test_integration_generic.py index c2eabfcaa7..2fae2c6f77 100644 --- a/tests/integrations/test_integration_generic.py +++ b/tests/integrations/test_integration_generic.py @@ -71,7 +71,7 @@ def test_generic_extension_lifecycle(tmp_path, generic_extension, skills): commands_dir=".custom/skills" if skills else ".custom/commands", ) output_dir = project / ".custom" / ("skills" if skills else "commands") - core = output_dir / ("speckit-taskstoissues/SKILL.md" if skills else "speckit.taskstoissues.md") + core = output_dir / ("speckit-tasks/SKILL.md" if skills else "speckit.tasks.md") core_content = core.read_bytes() unrelated = output_dir / ("user-skill/SKILL.md" if skills else "user.md") unrelated.parent.mkdir(parents=True, exist_ok=True) @@ -668,25 +668,32 @@ def test_generic_upgrade_warns_and_rolls_back_partial_extension_refresh( assert ExtensionManager(project).registry.get("sample") == previous -def test_generic_command_refreshes_owned_artifact_and_missing_alias( - tmp_path, generic_extension, +@pytest.mark.parametrize("skills", [False, True]) +def test_generic_refreshes_owned_artifact_and_missing_alias( + tmp_path, generic_extension, skills, ): manifest_path = generic_extension / "extension.yml" manifest = yaml.safe_load(manifest_path.read_text(encoding="utf-8")) manifest["provides"]["commands"][0]["aliases"] = ["speckit.sample.alias"] manifest_path.write_text(yaml.safe_dump(manifest), encoding="utf-8") - project = generic_project(tmp_path) + project = generic_project(tmp_path, skills=skills) manager = ExtensionManager(project) manager.install_from_directory(generic_extension, "1.0.0") output = project / ".custom/commands" - primary = output / "speckit.sample.run.md" - alias = output / "speckit.sample.alias.md" + primary = output / ( + "speckit-sample-run/SKILL.md" if skills else "speckit.sample.run.md" + ) + alias = output / ( + "speckit-sample-alias/SKILL.md" if skills else "speckit.sample.alias.md" + ) assert primary.is_file() and alias.is_file() alias.unlink() + if skills: + alias.parent.rmdir() source = manager.extensions_dir / "sample/commands/run.md" source.write_text(source.read_text(encoding="utf-8") + "\nnew content\n", encoding="utf-8") - manager.register_enabled_extensions_for_agent("generic") + manager.register_enabled_extensions_for_agent("generic", force=skills) assert "new content" in primary.read_text(encoding="utf-8") assert "new content" in alias.read_text(encoding="utf-8") @@ -699,12 +706,13 @@ def test_generic_command_refreshes_owned_artifact_and_missing_alias( @pytest.mark.parametrize("skills", [False, True]) @pytest.mark.parametrize("collision_owned", [False, True]) +@pytest.mark.parametrize("use_alias", [False, True]) def test_generic_refresh_collision_rolls_back_and_warns( - tmp_path, generic_extension, skills, collision_owned, capsys, + tmp_path, generic_extension, skills, collision_owned, use_alias, capsys, ): manifest_path = generic_extension / "extension.yml" manifest = yaml.safe_load(manifest_path.read_text(encoding="utf-8")) - if skills and collision_owned: + if skills and collision_owned and not use_alias: manifest["provides"]["commands"].append({ "name": "speckit.sample.other", "file": "commands/other.md", @@ -724,12 +732,13 @@ def test_generic_refresh_collision_rolls_back_and_warns( "speckit-sample-run/SKILL.md" if skills else "speckit.sample.run.md" ) collision = output / ( - "speckit-sample-other/SKILL.md" if skills else "speckit.sample.alias.md" + ("speckit-sample-alias/SKILL.md" if use_alias else "speckit-sample-other/SKILL.md") + if skills else "speckit.sample.alias.md" ) if not collision_owned: installed_manifest = manager.extensions_dir / "sample/extension.yml" installed = yaml.safe_load(installed_manifest.read_text(encoding="utf-8")) - if skills: + if skills and not use_alias: installed["provides"]["commands"].append({ "name": "speckit.sample.other", "file": "commands/other.md", @@ -741,6 +750,8 @@ def test_generic_refresh_collision_rolls_back_and_warns( collision.parent.mkdir() else: installed["provides"]["commands"][0]["aliases"] = ["speckit.sample.alias"] + if skills: + collision.parent.mkdir() installed_manifest.write_text(yaml.safe_dump(installed), encoding="utf-8") original = primary.read_bytes() collision.write_text("user-edited invocation", encoding="utf-8") @@ -766,6 +777,29 @@ def test_generic_refresh_collision_rolls_back_and_warns( assert manager.remove("sample") +@pytest.mark.parametrize("skills", [False, True]) +def test_generic_install_refuses_user_owned_compatibility_alias( + tmp_path, generic_extension, skills, +): + manifest_path = generic_extension / "extension.yml" + manifest = yaml.safe_load(manifest_path.read_text(encoding="utf-8")) + manifest["provides"]["commands"][0]["aliases"] = ["speckit.taskstoissues"] + manifest_path.write_text(yaml.safe_dump(manifest), encoding="utf-8") + project = generic_project(tmp_path, skills=skills) + alias = project / ".custom/commands" / ( + "speckit-taskstoissues/SKILL.md" if skills else "speckit.taskstoissues.md" + ) + alias.parent.mkdir(parents=True, exist_ok=True) + alias.write_text("user-owned issue workflow\n", encoding="utf-8") + manager = ExtensionManager(project) + + with pytest.raises(ExtensionError, match="cannot be replaced safely"): + manager.install_from_directory(generic_extension, "1.0.0") + + assert alias.read_text(encoding="utf-8") == "user-owned issue workflow\n" + assert not manager.registry.is_installed("sample") + + @pytest.mark.parametrize("skills", [False, True]) @pytest.mark.parametrize("retained", [False, True]) def test_generic_refresh_missing_source_requires_output_or_prior_ownership( @@ -2535,7 +2569,6 @@ def test_implement_loads_constitution_context(self, tmp_path): "checklist", "specify", "tasks", - "taskstoissues", ], ) def test_command_loads_constitution_context(self, tmp_path, command_stem): @@ -2604,7 +2637,6 @@ def test_complete_file_inventory_sh(self, tmp_path): ".myagent/commands/speckit.plan.md", ".myagent/commands/speckit.specify.md", ".myagent/commands/speckit.tasks.md", - ".myagent/commands/speckit.taskstoissues.md", ".specify/init-options.json", ".specify/integration.json", ".specify/integrations/generic.manifest.json", @@ -2752,7 +2784,6 @@ def test_complete_file_inventory_ps(self, tmp_path): ".myagent/commands/speckit.plan.md", ".myagent/commands/speckit.specify.md", ".myagent/commands/speckit.tasks.md", - ".myagent/commands/speckit.taskstoissues.md", ".specify/init-options.json", ".specify/integration.json", ".specify/integrations/generic.manifest.json", diff --git a/tests/specify_cli/integrations/test_command_upgrade.py b/tests/specify_cli/integrations/test_command_upgrade.py index 42d5ea0a6c..840e60d44e 100644 --- a/tests/specify_cli/integrations/test_command_upgrade.py +++ b/tests/specify_cli/integrations/test_command_upgrade.py @@ -27,6 +27,67 @@ ) class TestIntegrationUpgradeDetailed: + @pytest.mark.parametrize( + ("key", "options", "legacy_paths"), + [ + ("copilot", "--commands", [ + ".github/agents/speckit.taskstoissues.agent.md", + ".github/prompts/speckit.taskstoissues.prompt.md", + ]), + ("copilot", "--skills", [".github/skills/speckit-taskstoissues/SKILL.md"]), + ("claude", None, [".claude/skills/speckit-taskstoissues/SKILL.md"]), + ("gemini", None, [".gemini/commands/speckit.taskstoissues.toml"]), + ("generic", "--commands-dir .custom/commands", [ + ".custom/commands/speckit.taskstoissues.md", + ]), + ("generic", "--commands-dir .custom/commands --skills", [ + ".custom/commands/speckit-taskstoissues/SKILL.md", + ]), + ], + ) + @pytest.mark.parametrize("with_extension", [False, True]) + def test_upgrade_retires_core_taskstoissues_and_restores_only_opt_in_alias( + self, tmp_path, key, options, legacy_paths, with_extension, + ): + from specify_cli.extensions import ExtensionManager + from specify_cli.integrations.manifest import IntegrationManifest + + project = _init_project(tmp_path, key, integration_options=options) + manifest = IntegrationManifest.load(key, project) + for relative in legacy_paths: + assert not (project / relative).exists() + manifest.record_file(relative, "old core taskstoissues\n") + manifest.save() + user_file = project / "user-notes.md" + user_file.write_text("keep user content\n", encoding="utf-8") + + upgraded = _run_in_project(project, ["integration", "upgrade", key]) + assert upgraded.exit_code == 0, upgraded.output + assert not any((project / path).exists() for path in legacy_paths) + if with_extension: + installed = _run_in_project(project, ["extension", "add", "github"]) + assert installed.exit_code == 0, installed.output + + result = _run_in_project(project, ["integration", "upgrade", key, "--force"]) + + assert result.exit_code == 0, result.output + updated = IntegrationManifest.load(key, project) + assert not any("taskstoissues" in path for path in updated.files) + assert user_file.read_text(encoding="utf-8") == "keep user content\n" + for relative in legacy_paths: + artifact = project / relative + assert artifact.exists() is with_extension + if with_extension and "prompts/" not in relative: + content = artifact.read_text(encoding="utf-8") + assert "deprecated compatibility alias" in content + assert ".specify/extensions/github/scripts/" in content + assert "old core taskstoissues" not in content + manager = ExtensionManager(project) + assert manager.registry.is_installed("github") is with_extension + if with_extension: + assert manager.remove("github") + assert not any((project / path).exists() for path in legacy_paths) + def test_upgrade_invalid_manifest_reports_cli_error(self, tmp_path): project = _init_project(tmp_path, "claude") _write_invalid_manifest(project, "claude") diff --git a/tests/specify_cli/presets/test_manager_skills.py b/tests/specify_cli/presets/test_manager_skills.py index 7ae833bfb9..059d8b7d7f 100644 --- a/tests/specify_cli/presets/test_manager_skills.py +++ b/tests/specify_cli/presets/test_manager_skills.py @@ -461,12 +461,12 @@ def test_core_command_override_skill_uses_preset_command_description(self, proje """Preset skill overrides for core commands should keep preset frontmatter descriptions.""" self._write_init_options(project_dir, ai="claude") skills_dir = project_dir / ".claude" / "skills" - self._create_skill(skills_dir, "speckit-taskstoissues") + self._create_skill(skills_dir, "speckit-tasks") - preset_dir = temp_dir / "taskstoissues-description" + preset_dir = temp_dir / "tasks-description" preset_dir.mkdir() (preset_dir / "commands").mkdir() - (preset_dir / "commands" / "speckit.repro.taskstoissues.md").write_text( + (preset_dir / "commands" / "speckit.repro.tasks.md").write_text( "---\n" "description: COMMAND-FRONTMATTER-DESCRIPTION\n" "---\n\n" @@ -475,8 +475,8 @@ def test_core_command_override_skill_uses_preset_command_description(self, proje manifest_data = { "schema_version": "1.0", "preset": { - "id": "taskstoissues-description", - "name": "Taskstoissues Description", + "id": "tasks-description", + "name": "Tasks Description", "version": "1.0.0", "description": "Test", }, @@ -485,10 +485,10 @@ def test_core_command_override_skill_uses_preset_command_description(self, proje "templates": [ { "type": "command", - "name": "speckit.taskstoissues", - "file": "commands/speckit.repro.taskstoissues.md", + "name": "speckit.tasks", + "file": "commands/speckit.repro.tasks.md", "description": "MANIFEST-DESCRIPTION", - "replaces": "speckit.taskstoissues", + "replaces": "speckit.tasks", "strategy": "replace", } ] @@ -500,76 +500,76 @@ def test_core_command_override_skill_uses_preset_command_description(self, proje manager = PresetManager(project_dir) manager.install_from_directory(preset_dir, "0.1.5") - skill_file = skills_dir / "speckit-taskstoissues" / "SKILL.md" + skill_file = skills_dir / "speckit-tasks" / "SKILL.md" content = skill_file.read_text() assert "description: COMMAND-FRONTMATTER-DESCRIPTION" in content - assert "Convert tasks from tasks.md into GitHub issues." not in content - assert "source: preset:taskstoissues-description" in content + assert "Break down implementation plans into actionable task lists." not in content + assert "source: preset:tasks-description" in content def test_core_skill_restore_uses_core_command_description(self, project_dir, temp_dir): """Core skill restore should keep core command frontmatter descriptions.""" self._write_init_options(project_dir, ai="claude") skills_dir = project_dir / ".claude" / "skills" - self._create_skill(skills_dir, "speckit-taskstoissues") + self._create_skill(skills_dir, "speckit-tasks") core_cmds = project_dir / ".specify" / "templates" / "commands" core_cmds.mkdir(parents=True, exist_ok=True) - (core_cmds / "taskstoissues.md").write_text( + (core_cmds / "tasks.md").write_text( "---\n" "description: CORE-FRONTMATTER-DESCRIPTION\n" "---\n\n" - "core taskstoissues body\n" + "core tasks body\n" ) preset_dir = self._create_command_preset( temp_dir, - "taskstoissues-restore", - "speckit.taskstoissues", + "tasks-restore", + "speckit.tasks", "PRESET-FRONTMATTER-DESCRIPTION", - "preset taskstoissues body\n", + "preset tasks body\n", ) manager = PresetManager(project_dir) manager.install_from_directory(preset_dir, "0.1.5") - manager.remove("taskstoissues-restore") + manager.remove("tasks-restore") - skill_file = skills_dir / "speckit-taskstoissues" / "SKILL.md" + skill_file = skills_dir / "speckit-tasks" / "SKILL.md" content = skill_file.read_text() assert "description: CORE-FRONTMATTER-DESCRIPTION" in content - assert "Convert tasks from tasks.md into GitHub issues." not in content - assert "source: templates/commands/taskstoissues.md" in content - assert "core taskstoissues body" in content + assert "Break down implementation plans into actionable task lists." not in content + assert "source: templates/commands/tasks.md" in content + assert "core tasks body" in content def test_override_skill_reconcile_uses_override_command_description(self, project_dir, temp_dir): """Override skill reconciliation should keep override frontmatter descriptions.""" self._write_init_options(project_dir, ai="claude") skills_dir = project_dir / ".claude" / "skills" - self._create_skill(skills_dir, "speckit-taskstoissues") + self._create_skill(skills_dir, "speckit-tasks") overrides_dir = project_dir / ".specify" / "templates" / "overrides" overrides_dir.mkdir(parents=True) - (overrides_dir / "speckit.taskstoissues.md").write_text( + (overrides_dir / "speckit.tasks.md").write_text( "---\n" "description: OVERRIDE-FRONTMATTER-DESCRIPTION\n" "---\n\n" - "override taskstoissues body\n" + "override tasks body\n" ) preset_dir = self._create_command_preset( temp_dir, - "taskstoissues-reconcile", - "speckit.taskstoissues", + "tasks-reconcile", + "speckit.tasks", "PRESET-FRONTMATTER-DESCRIPTION", - "preset taskstoissues body\n", + "preset tasks body\n", ) manager = PresetManager(project_dir) manager.install_from_directory(preset_dir, "0.1.5") - skill_file = skills_dir / "speckit-taskstoissues" / "SKILL.md" + skill_file = skills_dir / "speckit-tasks" / "SKILL.md" content = skill_file.read_text() assert "description: OVERRIDE-FRONTMATTER-DESCRIPTION" in content - assert "Convert tasks from tasks.md into GitHub issues." not in content - assert "source: override:speckit.taskstoissues" in content - assert "override taskstoissues body" in content + assert "Break down implementation plans into actionable task lists." not in content + assert "source: override:speckit.tasks" in content + assert "override tasks body" in content def test_skill_not_updated_when_ai_skills_disabled(self, project_dir, temp_dir): """When skills mode was NOT used, preset install should not touch skills.""" @@ -843,6 +843,43 @@ def test_extension_skill_override_matches_hyphenated_multisegment_name(self, pro metadata = manager.registry.get("ext-skill-override") assert "speckit-fakeext-cmd" in metadata.get("registered_skills", {}).get("codex", []) + @pytest.mark.parametrize( + "command", ["speckit.github.taskstoissues", "speckit.taskstoissues"], + ) + def test_github_command_override_restores_opt_in_extension_not_core( + self, project_dir, temp_dir, command, + ): + from specify_cli.extensions import ExtensionManager + + self._write_init_options(project_dir, ai="claude") + source = Path(__file__).resolve().parents[3] / "extensions/github" + extensions = ExtensionManager(project_dir) + extensions.install_from_directory(source, "1.2.3.dev0") + skills = project_dir / ".claude/skills" + canonical = skills / "speckit-github-taskstoissues/SKILL.md" + alias = skills / "speckit-taskstoissues/SKILL.md" + target, other = (canonical, alias) if command == "speckit.github.taskstoissues" else (alias, canonical) + other_before = other.read_bytes() + preset_dir = self._create_command_preset( + temp_dir, "github-override", command, + "Custom issue tracking", "custom GitHub workflow\n", + ) + manager = PresetManager(project_dir) + manager.install_from_directory(preset_dir, "1.2.3.dev0") + assert "custom GitHub workflow" in target.read_text(encoding="utf-8") + assert other.read_bytes() == other_before + + manager.remove("github-override") + + content = target.read_text(encoding="utf-8") + assert "source: extension:github" in content + assert ".specify/extensions/github/scripts/" in content + assert "custom GitHub workflow" not in content + assert other.read_bytes() == other_before + assert extensions.remove("github") + assert not canonical.exists() + assert not alias.exists() + def test_extension_skill_restored_on_preset_remove(self, project_dir, temp_dir): """Preset removal should restore an extension-backed skill instead of deleting it.""" self._write_init_options(project_dir, ai="codex") diff --git a/tests/test_extensions.py b/tests/test_extensions.py index b512742389..295f271841 100644 --- a/tests/test_extensions.py +++ b/tests/test_extensions.py @@ -3131,7 +3131,7 @@ def test_install_rejects_alias_shadowing_core_command(self, temp_dir, project_di Regression test for #4555: a primary name is namespace-checked against CORE_COMMAND_NAMES, but aliases are intentionally free-form and previously went unchecked against core commands entirely, so an - extension could claim e.g. 'speckit.taskstoissues' as an alias and + extension could claim e.g. 'speckit.tasks' as an alias and shadow the core command of the same name. """ import yaml @@ -3152,9 +3152,9 @@ def test_install_rejects_alias_shadowing_core_command(self, temp_dir, project_di "provides": { "commands": [ { - "name": "speckit.probe.taskstoissues", + "name": "speckit.probe.tasks", "file": "commands/cmd.md", - "aliases": ["speckit.taskstoissues"], + "aliases": ["speckit.tasks"], } ] }, @@ -3167,7 +3167,7 @@ def test_install_rejects_alias_shadowing_core_command(self, temp_dir, project_di with pytest.raises(ValidationError, match="conflicts with core command"): manager.install_from_directory(ext_dir, "0.1.0", register_commands=False) - @pytest.mark.parametrize("alias", ["taskstoissues", "speckit-taskstoissues"]) + @pytest.mark.parametrize("alias", ["tasks", "speckit-tasks"]) def test_install_rejects_equivalent_alias_shadowing_core_command( self, temp_dir, project_dir, alias ): @@ -3175,8 +3175,8 @@ def test_install_rejects_equivalent_alias_shadowing_core_command( Regression test for the reviewer follow-up on #4555: agent-specific name transformation (``CommandRegistrar._compute_output_name`` and the - Cline/Forge/Junie formatters) collapses ``speckit.taskstoissues``, - ``taskstoissues``, and ``speckit-taskstoissues`` to the same on-disk + Cline/Forge/Junie formatters) collapses ``speckit.tasks``, + ``tasks``, and ``speckit-tasks`` to the same on-disk command name, so all three spellings must be rejected, not just the exact dotted one. """ @@ -3198,7 +3198,7 @@ def test_install_rejects_equivalent_alias_shadowing_core_command( "provides": { "commands": [ { - "name": "speckit.probe.taskstoissues", + "name": "speckit.probe.tasks", "file": "commands/cmd.md", "aliases": [alias], } From e6e904e34dbcd4311c27da810b474b557950b5c3 Mon Sep 17 00:00:00 2001 From: Manfred Riem <15701806+mnriem@users.noreply.github.com> Date: Wed, 21 Oct 2026 11:37:17 -0500 Subject: [PATCH 2/5] fix: narrow core taskstoissues removal scope Drop the conditional compatibility alias and revert its shared registration, cleanup, and preset-restoration support. Keep the existing GitHub extension unchanged and cover core-command retirement and explicit opt-in migration without an alias. Assisted-by: GitHub Copilot (model: GPT-6.1 Sol, autonomous) Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --- CHANGELOG.md | 8 +-- docs/installation.md | 7 ++- docs/reference/agentic-sdd.md | 4 +- docs/reference/extensions.md | 2 +- docs/reference/integrations.md | 5 +- docs/upgrade.md | 17 +++--- extensions/EXTENSION-API-REFERENCE.md | 6 +-- extensions/EXTENSION-DEVELOPMENT-GUIDE.md | 6 +-- extensions/catalog.json | 2 +- extensions/github/README.md | 47 ++++++++-------- .../commands/speckit.github.taskstoissues.md | 8 --- extensions/github/extension.yml | 11 ++-- src/specify_cli/extensions/__init__.py | 21 ++++---- src/specify_cli/presets/_manager_skills.py | 16 ++---- .../github/test_github_extension.py | 51 +++++------------- .../github/test_taskstoissues_removal.py | 13 ++--- .../integrations/test_integration_generic.py | 54 ++++--------------- .../integrations/test_command_upgrade.py | 12 +++-- .../presets/test_manager_skills.py | 37 ------------- 19 files changed, 105 insertions(+), 222 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index ef98604852..e515e56837 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,10 +9,10 @@ - Remove `taskstoissues` from core commands and integration scaffolding after the replacement shipped in 1.0.13 and core deprecation shipped in 1.1.3 (github/spec-kit#4423). The opt-in `github` extension is the sole owner of - `speckit.github.taskstoissues` and its deprecated `speckit.taskstoissues` - compatibility alias. Run `specify integration upgrade `, then - `specify extension add github`; for an already-installed - extension, use `specify extension update github` instead of `add`. + `speckit.github.taskstoissues`. No legacy alias is installed; consistent + alias lifecycle support is deferred rather than expanding shared runtime. + Run `specify integration upgrade `, then + `specify extension add github` (skip `add` if already installed). See the [migration guide](docs/upgrade.md#breaking-change-taskstoissues-removed-from-core). This breaking removal must ship in a minor release, not the current patch development line. diff --git a/docs/installation.md b/docs/installation.md index 76bd8784a1..46d46881b9 100644 --- a/docs/installation.md +++ b/docs/installation.md @@ -158,15 +158,14 @@ After initialization, you should see the following commands available in your co **Breaking change:** tasks-to-issues conversion is no longer a core command. Fresh projects receive no issue-tracker commands unless you explicitly run -`specify extension add github`. Use `/speckit.github.taskstoissues`; the -extension also supplies the deprecated `/speckit.taskstoissues` alias. +`specify extension add github`. Use `/speckit.github.taskstoissues`; +the removed `/speckit.taskstoissues` invocation has no compatibility alias. See the [upgrade migration recipe](upgrade.md#breaking-change-taskstoissues-removed-from-core). The `generic` integration registers extension commands in its configured `--commands-dir`. Installing `github` makes `/speckit.github.taskstoissues` available there as a command file, or as `/speckit-github-taskstoissues` when -`--skills` is enabled. Removing the extension removes its unchanged commands, -including its compatibility alias. +`--skills` is enabled. Removing the extension removes its unchanged commands. See the [GitHub extension's installation notes](https://github.com/github/spec-kit/blob/main/extensions/github/README.md#installation). Scripts are installed into a variant subdirectory matching the chosen script type: diff --git a/docs/reference/agentic-sdd.md b/docs/reference/agentic-sdd.md index b2f80d4554..85876108c9 100644 --- a/docs/reference/agentic-sdd.md +++ b/docs/reference/agentic-sdd.md @@ -160,7 +160,7 @@ It first prints a severity-graded findings summary, then resolves to one of two ## `/speckit.github.taskstoissues` (opt-in extension) **Breaking change:** tasks-to-issues conversion is removed from core. -It is not installed by `specify init`; neither is the compatibility alias. +It is not installed by `specify init`, and no legacy alias is provided. The recommended extension command optionally converts an existing `tasks.md` into actionable GitHub issues. Install the extension from your project root: @@ -182,4 +182,4 @@ identified by that remote and checks existing task IDs to avoid duplicates. It is not required to implement tasks or converge on a feature. > [!NOTE] -> GitHub issue tracking uses the bundled, opt-in [`github` extension](https://github.com/github/spec-kit/blob/main/extensions/github/README.md). Its canonical command is `/speckit.github.taskstoissues` (slash-hyphen integrations: `/speckit-github-taskstoissues`; Codex, ZCode, and Command Code skills: `$speckit-github-taskstoissues`; Kimi: `/skill:speckit-github-taskstoissues`). The extension also supplies the deprecated `/speckit.taskstoissues` alias. The `generic` integration registers these commands under its configured command directory in commands or skills mode. Existing projects should follow the [upgrade migration recipe](../upgrade.md#breaking-change-taskstoissues-removed-from-core). +> GitHub issue tracking uses the bundled, opt-in [`github` extension](https://github.com/github/spec-kit/blob/main/extensions/github/README.md). Its canonical command is `/speckit.github.taskstoissues` (slash-hyphen integrations: `/speckit-github-taskstoissues`; Codex, ZCode, and Command Code skills: `$speckit-github-taskstoissues`; Kimi: `/skill:speckit-github-taskstoissues`). The `generic` integration registers it under its configured command directory in commands or skills mode. Existing projects should follow the [upgrade migration recipe](../upgrade.md#breaking-change-taskstoissues-removed-from-core). diff --git a/docs/reference/extensions.md b/docs/reference/extensions.md index 0b98659f20..6553b4edec 100644 --- a/docs/reference/extensions.md +++ b/docs/reference/extensions.md @@ -37,7 +37,7 @@ specify extension add | `--force` | Overwrite if the extension is already installed | | `--priority `| Resolution priority (default: 10; lower = higher precedence) | -Installs an extension from the catalog, a URL, or a local directory. Extension commands are registered with the active AI coding agent integration. For `generic`, invocations use the configured `--commands-dir`: flat command files by default, or `speckit-/SKILL.md` with `--skills`. Core no longer provides tasks-to-issues conversion. Run `specify extension add github` and use the canonical `/speckit.github.taskstoissues` command (slash-hyphen integrations: `/speckit-github-taskstoissues`; Codex, ZCode, and Command Code skills: `$speckit-github-taskstoissues`; Kimi: `/skill:speckit-github-taskstoissues`). Only this opt-in extension supplies the deprecated `speckit.taskstoissues` compatibility alias. Existing projects should follow the [breaking-change migration recipe](../upgrade.md#breaking-change-taskstoissues-removed-from-core). +Installs an extension from the catalog, a URL, or a local directory. Extension commands are registered with the active AI coding agent integration. For `generic`, invocations use the configured `--commands-dir`: flat command files by default, or `speckit-/SKILL.md` with `--skills`. Core no longer provides tasks-to-issues conversion. Run `specify extension add github` and use the canonical `/speckit.github.taskstoissues` command (slash-hyphen integrations: `/speckit-github-taskstoissues`; Codex, ZCode, and Command Code skills: `$speckit-github-taskstoissues`; Kimi: `/skill:speckit-github-taskstoissues`). No legacy `speckit.taskstoissues` alias is installed. Existing projects should follow the [breaking-change migration recipe](../upgrade.md#breaking-change-taskstoissues-removed-from-core). If a generic integration refresh cannot produce every extension invocation (for example, because a command or skill is user-modified or its source is missing), it warns and restores that extension's prior registered artifacts. Other extensions can still refresh. diff --git a/docs/reference/integrations.md b/docs/reference/integrations.md index 24ce8a8637..bd0e222609 100644 --- a/docs/reference/integrations.md +++ b/docs/reference/integrations.md @@ -463,9 +463,8 @@ Use the canonical `/speckit.github.taskstoissues` command for dot-command integrations. Slash-hyphen integrations use `/speckit-github-taskstoissues`; Codex, ZCode, and Command Code skills use `$speckit-github-taskstoissues`; Kimi uses -`/skill:speckit-github-taskstoissues`. The extension also owns the deprecated -`speckit.taskstoissues` compatibility alias; core never installs it or enables -the extension automatically. See the [breaking-change migration](../upgrade.md#breaking-change-taskstoissues-removed-from-core). +`/skill:speckit-github-taskstoissues`. No legacy alias is provided, and core +never enables the extension automatically. See the [breaking-change migration](../upgrade.md#breaking-change-taskstoissues-removed-from-core). ### Claude: user-invocable-only skills diff --git a/docs/upgrade.md b/docs/upgrade.md index 5df203c20b..918c082e70 100644 --- a/docs/upgrade.md +++ b/docs/upgrade.md @@ -295,17 +295,16 @@ specify extension add github ``` Use the installed integration key in place of ``, or omit it to -upgrade the default integration. If `github` is already installed, use -`specify extension update github` instead of `add`. The delivered extension -is named `github`, not the originally proposed `github-issues`. -Refresh the integration first so old core artifacts cannot collide with the -extension alias, especially in the ownership-preserving `generic` integration. +upgrade the default integration. Skip `extension add` if `github` is already +installed; its implementation and version are unchanged. The delivered +extension is named `github`, not the originally proposed `github-issues`. Use the canonical `speckit.github.taskstoissues` command with your [integration's invocation syntax](reference/integrations.md#command-invocation). -The extension also owns a deprecated `speckit.taskstoissues` alias, which warns -and runs the same workflow once. Both preserve `before_taskstoissues` and -`after_taskstoissues`. No extension is installed or enabled automatically. +There is no legacy `speckit.taskstoissues` alias: consistent alias lifecycle +support would require separate shared-runtime changes. The namespaced command +preserves `before_taskstoissues` and `after_taskstoissues`. No extension is +installed or enabled automatically. Integration upgrade removes obsolete manifest-tracked core command files and re-registers enabled extensions for the default integration. If local edits @@ -317,7 +316,7 @@ upgrade each; `specify integration use ` registers extension commands when making one the default. For projects that do not want GitHub issue tracking, skip extension installation -and run only the integration upgrade; neither tasks-to-issues invocation remains. +and run only the integration upgrade; no core tasks-to-issues invocation remains. Audit presets, dependent extensions, workflow steps, handoffs, and automation: use `speckit.github.taskstoissues` and `__SPECKIT_COMMAND_GITHUB_TASKSTOISSUES__` for GitHub conversion and explicitly diff --git a/extensions/EXTENSION-API-REFERENCE.md b/extensions/EXTENSION-API-REFERENCE.md index 0d1fe515d0..2284067d3c 100644 --- a/extensions/EXTENSION-API-REFERENCE.md +++ b/extensions/EXTENSION-API-REFERENCE.md @@ -44,7 +44,7 @@ provides: - name: string # Required, pattern: ^speckit\.[a-z0-9-]+\.[a-z0-9-]+$ file: string # Required, relative path to command file description: string # Required - aliases: [string] # Optional safe relative names; must not shadow core or installed extension commands + aliases: [string] # Optional, same pattern as name; namespace must match extension.id and must not shadow core or installed extension commands templates: # Optional, array of declared templates. Always resolve # as "replace" -- 'strategy' is not an authorable field here. @@ -633,10 +633,10 @@ Standard events (defined by core): - `after_clarify` - After spec clarification - `before_constitution` - Before constitution update - `after_constitution` - After constitution update + #### Bundled `github` extension events (available when the extension is installed) -The canonical `speckit.github.taskstoissues` command and its deprecated -`speckit.taskstoissues` compatibility alias consume the same hook keys. +The existing `speckit.github.taskstoissues` command consumes these hook keys. Core no longer installs an issue-tracking command. - `before_taskstoissues` - Before tasks-to-issues conversion diff --git a/extensions/EXTENSION-DEVELOPMENT-GUIDE.md b/extensions/EXTENSION-DEVELOPMENT-GUIDE.md index 0109d377be..1bdd755956 100644 --- a/extensions/EXTENSION-DEVELOPMENT-GUIDE.md +++ b/extensions/EXTENSION-DEVELOPMENT-GUIDE.md @@ -41,7 +41,7 @@ provides: - name: "speckit.my-ext.hello" # Must follow pattern: speckit.{ext-id}.{cmd} file: "commands/hello.md" description: "Say hello" - aliases: ["speckit.my-ext.hi"] # Optional alternative invocation names + aliases: ["speckit.my-ext.hi"] # Optional aliases, same pattern config: # Optional: Config files - name: "my-ext-config.yml" @@ -193,7 +193,7 @@ of `provides.commands`, `provides.templates`, `provides.scripts`, `hooks`, or - `name`: Command name (must match `speckit.{ext-id}.{command}`) - `file`: Path to command file (relative to extension root) - `description`: Command description (optional) -- `aliases`: Alternative command names (optional string array). Names must be safe relative output paths and must not shadow core or installed extension commands. An alias may use a retired core name, as the opt-in `github` extension does for `speckit.taskstoissues`. +- `aliases`: Alternative command names (optional, array; each must match `speckit.{ext-id}.{command}`) **Template object**: @@ -226,7 +226,7 @@ Available hook points: - `before_checklist` / `after_checklist`: Before/after checklist generation - `before_clarify` / `after_clarify`: Before/after spec clarification - `before_constitution` / `after_constitution`: Before/after constitution update -- `before_taskstoissues` / `after_taskstoissues`: Before/after tasks-to-issues conversion (opt-in `github` extension, including its deprecated alias) +- `before_taskstoissues` / `after_taskstoissues`: Before/after tasks-to-issues conversion (opt-in `github` extension) - `before_bug_assess` / `after_bug_assess`: Before/after bug assessment (bundled `bug` extension) - `before_bug_fix` / `after_bug_fix`: Before/after bug remediation (bundled `bug` extension) - `before_bug_test` / `after_bug_test`: Before/after bug verification (bundled `bug` extension) diff --git a/extensions/catalog.json b/extensions/catalog.json index dd13966c40..30e6dd92ac 100644 --- a/extensions/catalog.json +++ b/extensions/catalog.json @@ -66,7 +66,7 @@ "github": { "name": "GitHub Integration", "id": "github", - "version": "1.1.0", + "version": "1.0.2", "description": "GitHub platform integration for Spec Kit - create GitHub issues from a feature's task list", "author": "spec-kit-core", "repository": "https://github.com/github/spec-kit", diff --git a/extensions/github/README.md b/extensions/github/README.md index 05b33db4b3..f40020ba70 100644 --- a/extensions/github/README.md +++ b/extensions/github/README.md @@ -23,9 +23,8 @@ specify extension add github The `generic` (bring your own agent) integration registers this command under its configured `--commands-dir` in both commands and skills layouts. Installing `github` creates `speckit.github.taskstoissues.md` in commands mode or -`speckit-github-taskstoissues/SKILL.md` in skills mode. It also installs the -deprecated `speckit.taskstoissues` compatibility alias. Neither invocation is -installed by core. Use the namespaced command for new workflows. See +`speckit-github-taskstoissues/SKILL.md` in skills mode. Core no longer installs +`speckit.taskstoissues`; use this extension's namespaced command. See [integration-specific options](../../docs/reference/integrations.md#integration-specific-options). ## Removal @@ -39,7 +38,6 @@ specify extension remove github | Command | Description | | ------------------------------ | -------------------------------------------------------------------- | | `speckit.github.taskstoissues` | Recommended: convert tasks from `tasks.md` into dependency-ordered GitHub issues. | -| `speckit.taskstoissues` | Deprecated compatibility alias; warns and runs the same workflow and hooks once. | > NOTE: The command ID above is canonical. Invoke it using the syntax for your integration: `/speckit.github.taskstoissues` for dot-command integrations; `/speckit-github-taskstoissues` for slash-hyphen integrations (including Forge and Cline); `$speckit-github-taskstoissues` for Codex, ZCode, or Command Code in skills mode; or `/skill:speckit-github-taskstoissues` for Kimi. @@ -56,9 +54,10 @@ This extension consumes the existing `before_taskstoissues` and `after_taskstois ## Requirements -- Spec Kit **1.2.3.dev0 or newer** from the core-removal development line. - Earlier hosts still own `speckit.taskstoissues` and reject the compatibility - alias. Extension 1.0.2 remains the pre-removal replacement for those hosts. +- Spec Kit **0.12.17 or newer**. Extension-local `scripts/...` path rewriting arrived in + 0.12.6, but auto-registered skills did not resolve `__SPECKIT_COMMAND_*__` references + until 0.12.17. Earlier releases cannot render this command correctly in every supported + layout, so `specify extension add github` refuses to install below 0.12.17. - A Git remote pointing at GitHub. - The **GitHub MCP server** available to your coding agent, providing the `list_issues` and `issue_write` tools. @@ -83,7 +82,7 @@ Prerequisite errors name the Spec Kit command without assuming an integration's **Breaking change:** `taskstoissues` has been removed from core after the replacement shipped in 1.0.13 and deprecation shipped in 1.1.3. This removal is intended for the next minor release, not a patch release. New projects do -not receive either invocation unless this extension is explicitly installed. +not receive a tasks-to-issues command unless this extension is explicitly installed. After upgrading to a CLI containing the removal, run from the project root: @@ -93,12 +92,10 @@ specify extension add github ``` Replace `` with the installed integration key, or omit it to -refresh the default integration. Upgrade first to retire old core artifacts -before installing the alias; generic registration refuses unowned collisions. -If `github` is already installed, run -`specify extension update github` instead of `add`. Integration upgrade -removes manifest-tracked obsolete core files, then re-registers enabled -extensions for the default integration, including the compatibility alias. +refresh the default integration. Skip `extension add` if `github` is already +installed; the extension's implementation and version are unchanged. +Integration upgrade removes manifest-tracked obsolete core files and +re-registers enabled extensions for the default integration. Upgrading the CLI alone does not refresh generated files. Inspect local edits before deciding whether to use `--force`. Projects without an integration manifest need manual review and cleanup of old generated commands; `upgrade` @@ -109,16 +106,16 @@ configured `--commands-dir`. For multiple installed integrations, upgrade each one; use `specify integration use ` to register extension commands when changing the default. See [Upgrade an Integration](../../docs/reference/integrations.md#upgrade-an-integration). -| Deprecated compatibility alias | Canonical extension command | +| Removed core command | Canonical extension command | | ------------------------- | --------------------------------- | | `/speckit.taskstoissues` | `/speckit.github.taskstoissues` | -The extension owns both invocations. The alias warns and executes the same -workflow once; it does not invoke the canonical command a second time. -Conversion behavior is unchanged: feature resolution, `plan.md` and `tasks.md` -prerequisites, GitHub remote validation, open/closed issue deduplication, -issue titles, and hooks are preserved. Removing the extension removes its -unchanged canonical and alias artifacts without affecting core SDD commands. +No legacy alias is installed: its lifecycle is not supported consistently +across commands and skills, so compatibility is deferred rather than expanding +shared registration and preset behavior in this removal. +The existing namespaced command preserves feature resolution, `plan.md` and +`tasks.md` prerequisites, GitHub remote validation, open/closed issue +deduplication, issue titles, and hooks. ### Presets and dependent extensions @@ -143,13 +140,11 @@ Preset command overrides are separate from command references: an override of command. Review and port any needed customization, then verify the installed command in a sample project before retiring the old override. A preset that uses another issue tracker should retain its own provider-specific workflow -rather than redirecting it to the GitHub command. Existing overrides may also -hide the compatibility warning, so communicate the migration to their users. +rather than redirecting it to the GitHub command. Keep the `before_taskstoissues` and `after_taskstoissues` hook keys unchanged; -the canonical command and alias consume them. Only update a hook's command -reference if it invokes the deprecated alias. Do not invoke both names for -the same conversion, since that also runs their hooks twice. +the namespaced command still consumes them. Update a hook's command reference +if it invokes the removed core command. Related proposals such as [milestone grouping (github/spec-kit#4370)](https://github.com/github/spec-kit/issues/4370) should target `speckit.github.taskstoissues`, not a core command. diff --git a/extensions/github/commands/speckit.github.taskstoissues.md b/extensions/github/commands/speckit.github.taskstoissues.md index a421cccf8d..6e587abe75 100644 --- a/extensions/github/commands/speckit.github.taskstoissues.md +++ b/extensions/github/commands/speckit.github.taskstoissues.md @@ -7,14 +7,6 @@ scripts: py: scripts/python/resolve_tasks.py --json --- -## Compatibility Alias - -Only when invoked as `__SPECKIT_COMMAND_TASKSTOISSUES__`, display: - -> `__SPECKIT_COMMAND_TASKSTOISSUES__` is a deprecated compatibility alias provided by the `github` extension. Use `__SPECKIT_COMMAND_GITHUB_TASKSTOISSUES__` instead. - -Do not display this warning for the canonical command. Continue with the workflow below, including its existing hooks and safety checks. Do not invoke the canonical command again, since that would run the conversion and hooks twice. - ## User Input ```text diff --git a/extensions/github/extension.yml b/extensions/github/extension.yml index 426e0a35cd..c6df8f19a1 100644 --- a/extensions/github/extension.yml +++ b/extensions/github/extension.yml @@ -3,15 +3,18 @@ schema_version: "1.0" extension: id: github name: "GitHub Integration" - version: "1.1.0" + version: "1.0.2" description: "GitHub platform integration for Spec Kit - create GitHub issues from a feature's task list" author: spec-kit-core repository: https://github.com/github/spec-kit license: MIT requires: - # Earlier hosts still own taskstoissues and reject its compatibility alias. - speckit_version: ">=1.2.3.dev0" + # 0.12.17 is the first release that both rewrites extension-local + # "scripts/..." paths (#3364) and resolves __SPECKIT_COMMAND_*__ tokens in + # auto-registered skills (#3544). Earlier versions cannot render this + # command correctly in every registerable layout. + speckit_version: ">=0.12.17" tools: - name: git required: true @@ -26,8 +29,6 @@ provides: - name: speckit.github.taskstoissues file: commands/speckit.github.taskstoissues.md description: "Convert tasks from tasks.md into dependency-ordered GitHub issues" - aliases: - - speckit.taskstoissues tags: - "github" diff --git a/src/specify_cli/extensions/__init__.py b/src/specify_cli/extensions/__init__.py index 90072ce7c1..a9ba5e6b34 100644 --- a/src/specify_cli/extensions/__init__.py +++ b/src/specify_cli/extensions/__init__.py @@ -1585,7 +1585,7 @@ def _register_extension_skills( ) -> List[str]: """Generate SKILL.md files for extension commands as agent skills. - For every command and alias in the extension manifest, creates a SKILL.md + For every command in the extension manifest, creates a SKILL.md file in the agent's skills directory following the agentskills.io specification. This is only done when skills mode was used during project initialisation. @@ -1651,11 +1651,8 @@ def _replacement(match: re.Match[str]) -> str: r"__SPECKIT_COMMAND_([A-Z][A-Z0-9_-]*)__", _replacement, body ) - for cmd_info, cmd_name in ( - (command, name) - for command in manifest.commands - for name in [command["name"], *(command.get("aliases") or [])] - ): + for cmd_info in manifest.commands: + cmd_name = cmd_info["name"] cmd_file_rel = cmd_info["file"] # Guard against path traversal: reject absolute paths and ensure @@ -2017,7 +2014,7 @@ def _snapshot_generic_refresh_artifacts( if isinstance(registered, dict): command_names.update(self._valid_name_list(registered.get("generic"))) skill_names = ( - {self._skill_name_for_command(name) for name in self._collect_manifest_command_names(manifest)} + {self._skill_name_for_command(command["name"]) for command in manifest.commands} if manifest is not None and skills_mode_active else set() ) skill_names.update(self._valid_name_list(metadata.get("registered_skills"))) @@ -2180,8 +2177,8 @@ def _remove_generic_artifact_paths( for name in [command["name"], *(command.get("aliases") or [])] ) skill_names.update( - self._skill_name_for_command(name) - for name in self._collect_manifest_command_names(manifest) + self._skill_name_for_command(command["name"]) + for command in manifest.commands ) hashes = metadata.get("generic_artifact_hashes", {}) if not isinstance(hashes, dict): @@ -2542,7 +2539,7 @@ def install_from_directory( ) from exc skills = is_ai_skills_enabled(active_options) names = ( - {self._skill_name_for_command(name) for name in self._collect_manifest_command_names(manifest)} + {self._skill_name_for_command(command["name"]) for command in manifest.commands} if skills else self._collect_manifest_command_names(manifest) ) owned = ( @@ -4068,8 +4065,8 @@ def register_enabled_extensions_for_agent(self, agent_name: str, *, force: bool ext_id, metadata, [ - self._skill_name_for_command(name) - for name in self._collect_manifest_command_names(manifest) + self._skill_name_for_command(cmd["name"]) + for cmd in manifest.commands ], registered_skills, skills=True, diff --git a/src/specify_cli/presets/_manager_skills.py b/src/specify_cli/presets/_manager_skills.py index 4adbdf2a61..5a4a4fb739 100644 --- a/src/specify_cli/presets/_manager_skills.py +++ b/src/specify_cli/presets/_manager_skills.py @@ -611,12 +611,10 @@ def _build_extension_skill_restore_index(self) -> Dict[str, Dict[str, Any]]: "extension_id": manifest.id, "extension_dir": ext_root, } - for name in [cmd_name, *(cmd_info.get("aliases") or [])]: - modern_skill_name, legacy_skill_name = self._skill_names_for_command(name) - named_restore_info = {**restore_info, "command_name": name} - restore_index.setdefault(modern_skill_name, named_restore_info) - if legacy_skill_name != modern_skill_name: - restore_index.setdefault(legacy_skill_name, named_restore_info) + modern_skill_name, legacy_skill_name = self._skill_names_for_command(cmd_name) + restore_index.setdefault(modern_skill_name, restore_info) + if legacy_skill_name != modern_skill_name: + restore_index.setdefault(legacy_skill_name, restore_info) return restore_index @@ -1498,9 +1496,6 @@ def _unregister_skills_in_dir( ) continue frontmatter, body = registrar.parse_frontmatter(content) - frontmatter = registrar._adjust_script_paths( - frontmatter, extension_id=extension_restore["extension_id"] - ) # Mirror the register-time rewrite (#2101): resolve # extension-relative subdir references (agents/, # knowledge-base/, etc.) to their installed location before @@ -1514,8 +1509,7 @@ def _unregister_skills_in_dir( ) if isinstance(selected_ai, str): body = registrar.resolve_skill_placeholders( - selected_ai, frontmatter, body, self.project_root, - extension_id=extension_restore["extension_id"], + selected_ai, frontmatter, body, self.project_root ) body = self._resolve_skill_command_refs( body, registrar, selected_ai, self.project_root diff --git a/tests/extensions/github/test_github_extension.py b/tests/extensions/github/test_github_extension.py index 2b23fb4c70..45bd8c6200 100644 --- a/tests/extensions/github/test_github_extension.py +++ b/tests/extensions/github/test_github_extension.py @@ -3,7 +3,7 @@ Validates: - Bundled layout (manifest, README, command file, script twins) - Catalog registration and wheel/source resolution via ``_locate_bundled_extension`` -- Manifest validation, including the retired core command's compatibility alias +- Manifest validation, including the absence of a legacy compatibility alias - Install/uninstall through ``ExtensionManager`` - Rendered command artifacts across command mode and skills mode, and in particular that ``{SCRIPT}`` resolves to a script the extension actually @@ -40,9 +40,10 @@ # A released Spec Kit version that satisfies the manifest floor. Installs are # refused below it, so this cannot be an arbitrary synthetic value. -INSTALL_SPECKIT_VERSION = "1.2.3.dev0" -# Earlier hosts still own taskstoissues and cannot accept the alias. -MIN_SPECKIT_VERSION = "1.2.3.dev0" +INSTALL_SPECKIT_VERSION = "1.0.12" +# The floor exists because auto-registered skills did not resolve command +# reference tokens until 0.12.17 (#3544). +MIN_SPECKIT_VERSION = "0.12.17" COMMAND_NAME = "speckit.github.taskstoissues" COMMAND_FILE = EXT_DIR / "commands" / f"{COMMAND_NAME}.md" @@ -157,7 +158,7 @@ def test_manifest_validates(self): m = ExtensionManifest(EXT_DIR / "extension.yml") assert m.id == "github" - assert m.version == "1.1.0" + assert m.version == "1.0.2" assert [c["name"] for c in m.commands] == [COMMAND_NAME] def test_manifest_command_files_exist(self): @@ -167,13 +168,13 @@ def test_manifest_command_files_exist(self): for cmd in m.commands: assert (EXT_DIR / cmd["file"]).is_file() - def test_alias_claims_only_the_retired_core_command(self): - """The legacy spelling is owned exclusively by the opt-in extension.""" + def test_no_legacy_compatibility_alias_is_registered(self): + """Removal does not expand the extension's alias lifecycle surface.""" from specify_cli.extensions import ExtensionManifest m = ExtensionManifest(EXT_DIR / "extension.yml") aliases = [a for cmd in m.commands for a in cmd.get("aliases", []) or []] - assert aliases == ["speckit.taskstoissues"] + assert aliases == [] def test_declares_its_mandatory_external_tools(self): """The command runs ``git config`` and needs the GitHub MCP tools.""" @@ -182,14 +183,13 @@ def test_declares_its_mandatory_external_tools(self): assert all(t["required"] is True for t in tools.values()) def test_version_floor_covers_all_rendering_prerequisites(self): - """Hosts retaining core ownership must not accept this extension.""" + """The extension retains its existing rendering compatibility floor.""" from packaging.specifiers import SpecifierSet floor = _manifest_dict()["requires"]["speckit_version"] spec = SpecifierSet(floor) assert "0.12.5" not in spec, floor assert "0.12.16" not in spec, floor - assert "1.2.2" not in spec, floor assert MIN_SPECKIT_VERSION in spec, floor # And the version the install tests use must satisfy it. assert INSTALL_SPECKIT_VERSION in spec, floor @@ -202,21 +202,6 @@ def test_core_command_no_longer_ships(self): class TestExtensionInstall: - @pytest.mark.parametrize("host_version", ["0.12.17", "1.0.13", "1.1.3", "1.2.2"]) - def test_install_refuses_hosts_that_still_own_the_core_command( - self, tmp_path, host_version, - ): - from specify_cli.extensions import CompatibilityError, ExtensionManager - - (tmp_path / ".specify").mkdir() - manager = ExtensionManager(tmp_path) - with pytest.raises(CompatibilityError): - manager.install_from_directory( - EXT_DIR, host_version, register_commands=False, - ) - assert not manager.registry.is_installed("github") - assert not (tmp_path / ".specify/extensions/github").exists() - @pytest.mark.parametrize("skills_mode", [False, True], ids=["commands", "skills"]) def test_generic_registers_and_removes_artifact_in_configured_directory( self, tmp_path: Path, skills_mode: bool @@ -276,10 +261,7 @@ def test_generic_registers_and_removes_artifact_in_configured_directory( "speckit-taskstoissues/SKILL.md" if skills_mode else "speckit.taskstoissues.md" ) - assert alias.is_file() - alias_content = alias.read_text(encoding="utf-8") - assert "deprecated compatibility alias" in alias_content - assert "__SPECKIT_COMMAND_" not in alias_content + assert not alias.exists() content = artifact.read_text(encoding="utf-8") assert ".specify/extensions/github/scripts/bash/resolve-tasks.sh" in content assert "{SCRIPT}" not in content @@ -330,8 +312,6 @@ def test_remove_uninstalls_cleanly(self, tmp_path: Path): [ f".github/agents/{COMMAND_NAME}.agent.md", f".github/prompts/{COMMAND_NAME}.prompt.md", - ".github/agents/speckit.taskstoissues.agent.md", - ".github/prompts/speckit.taskstoissues.prompt.md", ], ".github/agents/speckit.tasks.agent.md", ), @@ -339,7 +319,6 @@ def test_remove_uninstalls_cleanly(self, tmp_path: Path): "claude", [ ".claude/skills/speckit-github-taskstoissues/SKILL.md", - ".claude/skills/speckit-taskstoissues/SKILL.md", ], ".claude/skills/speckit-tasks/SKILL.md", ), @@ -457,7 +436,7 @@ def test_every_extension_registrar_integration_renders_the_command( Covers both layouts in one sweep — command-file agents, skills-mode agents, and Hermes, which installs to ``~/.hermes/skills`` rather than - a project-local directory (hence the redirected home). Generic resolves + a project-local directory (hence the redirected home). Generic resolves persisted output options and is covered separately. """ from specify_cli.extensions import CommandRegistrar, ExtensionManager @@ -496,7 +475,7 @@ def test_every_extension_registrar_integration_renders_the_command( p for p in artifacts if "github" not in p.name and "github" not in p.parent.name ] - assert aliases, f"{agent} produced no compatibility alias" + assert not aliases, f"{agent} unexpectedly installed a legacy alias" expected = f".specify/extensions/github/{SCRIPT_TWINS['sh']}" bodies = [p.read_text(encoding="utf-8") for p in artifacts] @@ -505,10 +484,6 @@ def test_every_extension_registrar_integration_renders_the_command( for artifact, content in zip(artifacts, bodies): assert "{SCRIPT}" not in content, artifact assert "__SPECKIT_COMMAND_" not in content, artifact - assert any( - "deprecated compatibility alias" in path.read_text(encoding="utf-8") - for path in aliases - ) # ...and the command body must carry the resolved extension-local path. # Some integrations also emit a thin companion file (e.g. Copilot's # prompt shim, which only points at the agent), so this is "at least diff --git a/tests/extensions/github/test_taskstoissues_removal.py b/tests/extensions/github/test_taskstoissues_removal.py index ab371cc51a..dd91873969 100644 --- a/tests/extensions/github/test_taskstoissues_removal.py +++ b/tests/extensions/github/test_taskstoissues_removal.py @@ -78,12 +78,9 @@ def test_core_inventories_release_the_legacy_name(): assert "taskstoissues" not in inventory -def test_compatibility_alias_is_deprecated_without_deprecating_canonical_command(): +def test_legacy_alias_is_not_installed_by_the_extension(): text = EXTENSION.read_text(encoding="utf-8") - notice = text.split("## Compatibility Alias\n", 1)[1].split("## User Input", 1)[0] - assert "Only when invoked" in notice - assert "__SPECKIT_COMMAND_TASKSTOISSUES__" in notice - assert "__SPECKIT_COMMAND_GITHUB_TASKSTOISSUES__" in notice - assert "deprecated compatibility alias" in notice - assert "Continue with the workflow below" in notice - assert "Do not invoke the canonical command again" in notice + manifest = yaml.safe_load((ROOT / "extensions/github/extension.yml").read_text(encoding="utf-8")) + assert "## Compatibility Alias" not in text + assert "__SPECKIT_COMMAND_TASKSTOISSUES__" not in text + assert all(not command.get("aliases") for command in manifest["provides"]["commands"]) diff --git a/tests/integrations/test_integration_generic.py b/tests/integrations/test_integration_generic.py index 2fae2c6f77..3ea10f7b11 100644 --- a/tests/integrations/test_integration_generic.py +++ b/tests/integrations/test_integration_generic.py @@ -668,32 +668,25 @@ def test_generic_upgrade_warns_and_rolls_back_partial_extension_refresh( assert ExtensionManager(project).registry.get("sample") == previous -@pytest.mark.parametrize("skills", [False, True]) -def test_generic_refreshes_owned_artifact_and_missing_alias( - tmp_path, generic_extension, skills, +def test_generic_command_refreshes_owned_artifact_and_missing_alias( + tmp_path, generic_extension, ): manifest_path = generic_extension / "extension.yml" manifest = yaml.safe_load(manifest_path.read_text(encoding="utf-8")) manifest["provides"]["commands"][0]["aliases"] = ["speckit.sample.alias"] manifest_path.write_text(yaml.safe_dump(manifest), encoding="utf-8") - project = generic_project(tmp_path, skills=skills) + project = generic_project(tmp_path) manager = ExtensionManager(project) manager.install_from_directory(generic_extension, "1.0.0") output = project / ".custom/commands" - primary = output / ( - "speckit-sample-run/SKILL.md" if skills else "speckit.sample.run.md" - ) - alias = output / ( - "speckit-sample-alias/SKILL.md" if skills else "speckit.sample.alias.md" - ) + primary = output / "speckit.sample.run.md" + alias = output / "speckit.sample.alias.md" assert primary.is_file() and alias.is_file() alias.unlink() - if skills: - alias.parent.rmdir() source = manager.extensions_dir / "sample/commands/run.md" source.write_text(source.read_text(encoding="utf-8") + "\nnew content\n", encoding="utf-8") - manager.register_enabled_extensions_for_agent("generic", force=skills) + manager.register_enabled_extensions_for_agent("generic") assert "new content" in primary.read_text(encoding="utf-8") assert "new content" in alias.read_text(encoding="utf-8") @@ -706,13 +699,12 @@ def test_generic_refreshes_owned_artifact_and_missing_alias( @pytest.mark.parametrize("skills", [False, True]) @pytest.mark.parametrize("collision_owned", [False, True]) -@pytest.mark.parametrize("use_alias", [False, True]) def test_generic_refresh_collision_rolls_back_and_warns( - tmp_path, generic_extension, skills, collision_owned, use_alias, capsys, + tmp_path, generic_extension, skills, collision_owned, capsys, ): manifest_path = generic_extension / "extension.yml" manifest = yaml.safe_load(manifest_path.read_text(encoding="utf-8")) - if skills and collision_owned and not use_alias: + if skills and collision_owned: manifest["provides"]["commands"].append({ "name": "speckit.sample.other", "file": "commands/other.md", @@ -732,13 +724,12 @@ def test_generic_refresh_collision_rolls_back_and_warns( "speckit-sample-run/SKILL.md" if skills else "speckit.sample.run.md" ) collision = output / ( - ("speckit-sample-alias/SKILL.md" if use_alias else "speckit-sample-other/SKILL.md") - if skills else "speckit.sample.alias.md" + "speckit-sample-other/SKILL.md" if skills else "speckit.sample.alias.md" ) if not collision_owned: installed_manifest = manager.extensions_dir / "sample/extension.yml" installed = yaml.safe_load(installed_manifest.read_text(encoding="utf-8")) - if skills and not use_alias: + if skills: installed["provides"]["commands"].append({ "name": "speckit.sample.other", "file": "commands/other.md", @@ -750,8 +741,6 @@ def test_generic_refresh_collision_rolls_back_and_warns( collision.parent.mkdir() else: installed["provides"]["commands"][0]["aliases"] = ["speckit.sample.alias"] - if skills: - collision.parent.mkdir() installed_manifest.write_text(yaml.safe_dump(installed), encoding="utf-8") original = primary.read_bytes() collision.write_text("user-edited invocation", encoding="utf-8") @@ -777,29 +766,6 @@ def test_generic_refresh_collision_rolls_back_and_warns( assert manager.remove("sample") -@pytest.mark.parametrize("skills", [False, True]) -def test_generic_install_refuses_user_owned_compatibility_alias( - tmp_path, generic_extension, skills, -): - manifest_path = generic_extension / "extension.yml" - manifest = yaml.safe_load(manifest_path.read_text(encoding="utf-8")) - manifest["provides"]["commands"][0]["aliases"] = ["speckit.taskstoissues"] - manifest_path.write_text(yaml.safe_dump(manifest), encoding="utf-8") - project = generic_project(tmp_path, skills=skills) - alias = project / ".custom/commands" / ( - "speckit-taskstoissues/SKILL.md" if skills else "speckit.taskstoissues.md" - ) - alias.parent.mkdir(parents=True, exist_ok=True) - alias.write_text("user-owned issue workflow\n", encoding="utf-8") - manager = ExtensionManager(project) - - with pytest.raises(ExtensionError, match="cannot be replaced safely"): - manager.install_from_directory(generic_extension, "1.0.0") - - assert alias.read_text(encoding="utf-8") == "user-owned issue workflow\n" - assert not manager.registry.is_installed("sample") - - @pytest.mark.parametrize("skills", [False, True]) @pytest.mark.parametrize("retained", [False, True]) def test_generic_refresh_missing_source_requires_output_or_prior_ownership( diff --git a/tests/specify_cli/integrations/test_command_upgrade.py b/tests/specify_cli/integrations/test_command_upgrade.py index 840e60d44e..6048feedca 100644 --- a/tests/specify_cli/integrations/test_command_upgrade.py +++ b/tests/specify_cli/integrations/test_command_upgrade.py @@ -46,7 +46,7 @@ class TestIntegrationUpgradeDetailed: ], ) @pytest.mark.parametrize("with_extension", [False, True]) - def test_upgrade_retires_core_taskstoissues_and_restores_only_opt_in_alias( + def test_upgrade_retires_core_taskstoissues_and_preserves_only_opt_in_command( self, tmp_path, key, options, legacy_paths, with_extension, ): from specify_cli.extensions import ExtensionManager @@ -74,12 +74,17 @@ def test_upgrade_retires_core_taskstoissues_and_restores_only_opt_in_alias( updated = IntegrationManifest.load(key, project) assert not any("taskstoissues" in path for path in updated.files) assert user_file.read_text(encoding="utf-8") == "keep user content\n" - for relative in legacy_paths: + canonical_paths = [ + relative.replace("speckit.taskstoissues", "speckit.github.taskstoissues") + .replace("speckit-taskstoissues", "speckit-github-taskstoissues") + for relative in legacy_paths + ] + assert not any((project / path).exists() for path in legacy_paths) + for relative in canonical_paths: artifact = project / relative assert artifact.exists() is with_extension if with_extension and "prompts/" not in relative: content = artifact.read_text(encoding="utf-8") - assert "deprecated compatibility alias" in content assert ".specify/extensions/github/scripts/" in content assert "old core taskstoissues" not in content manager = ExtensionManager(project) @@ -87,6 +92,7 @@ def test_upgrade_retires_core_taskstoissues_and_restores_only_opt_in_alias( if with_extension: assert manager.remove("github") assert not any((project / path).exists() for path in legacy_paths) + assert not any((project / path).exists() for path in canonical_paths) def test_upgrade_invalid_manifest_reports_cli_error(self, tmp_path): project = _init_project(tmp_path, "claude") diff --git a/tests/specify_cli/presets/test_manager_skills.py b/tests/specify_cli/presets/test_manager_skills.py index 059d8b7d7f..a0176936d2 100644 --- a/tests/specify_cli/presets/test_manager_skills.py +++ b/tests/specify_cli/presets/test_manager_skills.py @@ -843,43 +843,6 @@ def test_extension_skill_override_matches_hyphenated_multisegment_name(self, pro metadata = manager.registry.get("ext-skill-override") assert "speckit-fakeext-cmd" in metadata.get("registered_skills", {}).get("codex", []) - @pytest.mark.parametrize( - "command", ["speckit.github.taskstoissues", "speckit.taskstoissues"], - ) - def test_github_command_override_restores_opt_in_extension_not_core( - self, project_dir, temp_dir, command, - ): - from specify_cli.extensions import ExtensionManager - - self._write_init_options(project_dir, ai="claude") - source = Path(__file__).resolve().parents[3] / "extensions/github" - extensions = ExtensionManager(project_dir) - extensions.install_from_directory(source, "1.2.3.dev0") - skills = project_dir / ".claude/skills" - canonical = skills / "speckit-github-taskstoissues/SKILL.md" - alias = skills / "speckit-taskstoissues/SKILL.md" - target, other = (canonical, alias) if command == "speckit.github.taskstoissues" else (alias, canonical) - other_before = other.read_bytes() - preset_dir = self._create_command_preset( - temp_dir, "github-override", command, - "Custom issue tracking", "custom GitHub workflow\n", - ) - manager = PresetManager(project_dir) - manager.install_from_directory(preset_dir, "1.2.3.dev0") - assert "custom GitHub workflow" in target.read_text(encoding="utf-8") - assert other.read_bytes() == other_before - - manager.remove("github-override") - - content = target.read_text(encoding="utf-8") - assert "source: extension:github" in content - assert ".specify/extensions/github/scripts/" in content - assert "custom GitHub workflow" not in content - assert other.read_bytes() == other_before - assert extensions.remove("github") - assert not canonical.exists() - assert not alias.exists() - def test_extension_skill_restored_on_preset_remove(self, project_dir, temp_dir): """Preset removal should restore an extension-backed skill instead of deleting it.""" self._write_init_options(project_dir, ai="codex") From ee1bfd42538482f5efc178c0e6d2d68b5b747dcf Mon Sep 17 00:00:00 2001 From: Manfred Riem <15701806+mnriem@users.noreply.github.com> Date: Wed, 21 Oct 2026 12:44:14 -0500 Subject: [PATCH 3/5] fix: limit taskstoissues change to core removal Remove added migration documentation and replacement tests. Delete obsolete core-only and core-parity tests while retaining existing extension and shared prerequisite-script coverage. Restrict documentation edits to removing obsolete core content; leave release tooling and Hermes lifecycle behavior unchanged. Assisted-by: GitHub Copilot (model: GPT-6.1 Sol, autonomous) Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --- CHANGELOG.md | 15 ---- README.md | 5 -- docs/community/presets.md | 6 -- docs/installation.md | 12 --- docs/reference/agentic-sdd.md | 27 ------ docs/reference/extensions.md | 2 +- docs/reference/integrations.md | 11 +-- docs/upgrade.md | 45 ---------- extensions/EXTENSION-API-REFERENCE.md | 8 -- extensions/EXTENSION-DEVELOPMENT-GUIDE.md | 1 - extensions/EXTENSION-USER-GUIDE.md | 4 +- extensions/github/README.md | 63 ++++---------- src/specify_cli/extensions/__init__.py | 6 +- .../github/test_github_extension.py | 53 +++--------- .../github/test_taskstoissues_removal.py | 86 ------------------- .../integrations/test_command_upgrade.py | 67 --------------- 16 files changed, 35 insertions(+), 376 deletions(-) delete mode 100644 tests/extensions/github/test_taskstoissues_removal.py diff --git a/CHANGELOG.md b/CHANGELOG.md index e515e56837..c6aa580d01 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,21 +2,6 @@ -## Unreleased - next minor release - -### Breaking - -- Remove `taskstoissues` from core commands and integration scaffolding after - the replacement shipped in 1.0.13 and core deprecation shipped in 1.1.3 - (github/spec-kit#4423). The opt-in `github` extension is the sole owner of - `speckit.github.taskstoissues`. No legacy alias is installed; consistent - alias lifecycle support is deferred rather than expanding shared runtime. - Run `specify integration upgrade `, then - `specify extension add github` (skip `add` if already installed). - See the [migration guide](docs/upgrade.md#breaking-change-taskstoissues-removed-from-core). - This breaking removal must ship in a minor release, not the current patch - development line. - ## [1.2.2] - 2026-10-19 ### Changed diff --git a/README.md b/README.md index 887b64b20d..3f50156526 100644 --- a/README.md +++ b/README.md @@ -94,11 +94,6 @@ For other setups: - [Other agents](https://github.github.io/spec-kit/reference/integrations.html) for integration keys and [invocation syntax](https://github.github.io/spec-kit/reference/integrations.html#command-invocation). - [Upgrade](https://github.github.io/spec-kit/upgrade.html) to update an existing installation. -**Breaking change:** tasks-to-issues conversion is no longer installed by core. -Opt in with `specify extension add github`, then use -`speckit.github.taskstoissues`. Existing projects must also refresh their -integration; see the [migration recipe](https://github.github.io/spec-kit/upgrade.html#breaking-change-taskstoissues-removed-from-core). - diff --git a/docs/community/presets.md b/docs/community/presets.md index 06aa2cdbed..cbd7e356e4 100644 --- a/docs/community/presets.md +++ b/docs/community/presets.md @@ -5,12 +5,6 @@ The following community-contributed presets customize how Spec Kit behaves — overriding templates, commands, and terminology without changing any tooling. Presets are available in [`catalog.community.json`](https://github.com/github/spec-kit/blob/main/presets/catalog.community.json): -**Migration note:** `speckit.taskstoissues` is no longer a core command. -Listings below retain their authors' descriptions; presets targeting that old -name need migration review by their authors. GitHub conversion now belongs to -the opt-in `github` extension's `speckit.github.taskstoissues`; other trackers -should use provider-specific extensions. See the [upgrade guide](../upgrade.md#breaking-change-taskstoissues-removed-from-core). - | Preset | Purpose | Provides | Requires | URL | |--------|---------|----------|----------|-----| | A11Y Governance | Adds WCAG 2.2 AA, accessible status output, bilingual CEFR-B2 delivery, inclusive-content and didactic-comment governance, and provider-neutral model routing. | 11 templates, 3 commands | — | [spec-kit-preset-a11y-governance](https://github.com/hindermath/spec-kit-preset-a11y-governance) | diff --git a/docs/installation.md b/docs/installation.md index 46d46881b9..666ad7bd53 100644 --- a/docs/installation.md +++ b/docs/installation.md @@ -156,18 +156,6 @@ After initialization, you should see the following commands available in your co - `/speckit.constitution` - Create or update project principles - `/speckit.converge` - Assess codebase against artifacts and append remaining tasks -**Breaking change:** tasks-to-issues conversion is no longer a core command. -Fresh projects receive no issue-tracker commands unless you explicitly run -`specify extension add github`. Use `/speckit.github.taskstoissues`; -the removed `/speckit.taskstoissues` invocation has no compatibility alias. -See the [upgrade migration recipe](upgrade.md#breaking-change-taskstoissues-removed-from-core). - -The `generic` integration registers extension commands in its configured -`--commands-dir`. Installing `github` makes `/speckit.github.taskstoissues` -available there as a command file, or as `/speckit-github-taskstoissues` when -`--skills` is enabled. Removing the extension removes its unchanged commands. -See the [GitHub extension's installation notes](https://github.com/github/spec-kit/blob/main/extensions/github/README.md#installation). - Scripts are installed into a variant subdirectory matching the chosen script type: - `.specify/scripts/bash/` — contains `.sh` scripts (default on Linux/macOS) diff --git a/docs/reference/agentic-sdd.md b/docs/reference/agentic-sdd.md index 85876108c9..c99f417b8d 100644 --- a/docs/reference/agentic-sdd.md +++ b/docs/reference/agentic-sdd.md @@ -156,30 +156,3 @@ It first prints a severity-graded findings summary, then resolves to one of two - **Converged** — no gaps found. `tasks.md` is left byte-for-byte unchanged and you'll see a clean result like `✅ Converged — the implementation satisfies the spec, plan, and tasks.` You're done; proceed to review or open a PR. - **Tasks appended** — gaps found. Converge appends them as new tasks under a Convergence section in `tasks.md` and tells you how many. Run `/speckit.implement` again to complete them, then `/speckit.converge` once more. Each pass finds fewer items; repeat until it reports converged. - -## `/speckit.github.taskstoissues` (opt-in extension) - -**Breaking change:** tasks-to-issues conversion is removed from core. -It is not installed by `specify init`, and no legacy alias is provided. - -The recommended extension command optionally converts an existing `tasks.md` -into actionable GitHub issues. Install the extension from your project root: - -```bash -specify extension add github -``` - -Then invoke it in your agent after generating tasks when you want to track -execution in GitHub: - -```text -/speckit.github.taskstoissues -``` - -This command requires a GitHub `origin` remote and access to the GitHub MCP tools -for listing and creating issues. It creates issues only in the repository -identified by that remote and checks existing task IDs to avoid duplicates. -It is not required to implement tasks or converge on a feature. - -> [!NOTE] -> GitHub issue tracking uses the bundled, opt-in [`github` extension](https://github.com/github/spec-kit/blob/main/extensions/github/README.md). Its canonical command is `/speckit.github.taskstoissues` (slash-hyphen integrations: `/speckit-github-taskstoissues`; Codex, ZCode, and Command Code skills: `$speckit-github-taskstoissues`; Kimi: `/skill:speckit-github-taskstoissues`). The `generic` integration registers it under its configured command directory in commands or skills mode. Existing projects should follow the [upgrade migration recipe](../upgrade.md#breaking-change-taskstoissues-removed-from-core). diff --git a/docs/reference/extensions.md b/docs/reference/extensions.md index 6553b4edec..72ca64d3dc 100644 --- a/docs/reference/extensions.md +++ b/docs/reference/extensions.md @@ -37,7 +37,7 @@ specify extension add | `--force` | Overwrite if the extension is already installed | | `--priority `| Resolution priority (default: 10; lower = higher precedence) | -Installs an extension from the catalog, a URL, or a local directory. Extension commands are registered with the active AI coding agent integration. For `generic`, invocations use the configured `--commands-dir`: flat command files by default, or `speckit-/SKILL.md` with `--skills`. Core no longer provides tasks-to-issues conversion. Run `specify extension add github` and use the canonical `/speckit.github.taskstoissues` command (slash-hyphen integrations: `/speckit-github-taskstoissues`; Codex, ZCode, and Command Code skills: `$speckit-github-taskstoissues`; Kimi: `/skill:speckit-github-taskstoissues`). No legacy `speckit.taskstoissues` alias is installed. Existing projects should follow the [breaking-change migration recipe](../upgrade.md#breaking-change-taskstoissues-removed-from-core). +Installs an extension from the catalog, a URL, or a local directory. Extension commands are registered with the active AI coding agent integration. For `generic`, invocations use the configured `--commands-dir`: flat command files by default, or `speckit-/SKILL.md` with `--skills`. If a generic integration refresh cannot produce every extension invocation (for example, because a command or skill is user-modified or its source is missing), it warns and restores that extension's prior registered artifacts. Other extensions can still refresh. diff --git a/docs/reference/integrations.md b/docs/reference/integrations.md index bd0e222609..8af7054af1 100644 --- a/docs/reference/integrations.md +++ b/docs/reference/integrations.md @@ -455,16 +455,7 @@ Once `generic` is the active integration, `specify extension add` registers extension commands in its configured `--commands-dir` (as command files or skills according to `--skills`). `specify extension remove` removes unchanged extension-owned artifacts while leaving core commands, user files, and edited -extension files intact. Core no longer installs `speckit.taskstoissues`. -To migrate, run `specify integration upgrade ` to remove obsolete managed -core files and refresh enabled extension commands for the default integration, -then `specify extension add github`. -Use the canonical -`/speckit.github.taskstoissues` command for dot-command integrations. Slash-hyphen -integrations use `/speckit-github-taskstoissues`; Codex, ZCode, and Command Code -skills use `$speckit-github-taskstoissues`; Kimi uses -`/skill:speckit-github-taskstoissues`. No legacy alias is provided, and core -never enables the extension automatically. See the [breaking-change migration](../upgrade.md#breaking-change-taskstoissues-removed-from-core). +extension files intact. ### Claude: user-invocable-only skills diff --git a/docs/upgrade.md b/docs/upgrade.md index 918c082e70..45e664c0a4 100644 --- a/docs/upgrade.md +++ b/docs/upgrade.md @@ -280,51 +280,6 @@ preset/extension-managed — see the "Interaction with the resolution stack" sec --- -## Breaking change: taskstoissues removed from core - -Tasks-to-issues conversion now belongs exclusively to the opt-in `github` -extension. The replacement shipped in 1.0.13, and core deprecation shipped in -1.1.3; removal is intended for the next minor release, not a patch release. -New projects receive no tasks-to-issues command unless the extension is installed. - -After upgrading to the CLI containing the removal, migrate from your project root: - -```bash -specify integration upgrade -specify extension add github -``` - -Use the installed integration key in place of ``, or omit it to -upgrade the default integration. Skip `extension add` if `github` is already -installed; its implementation and version are unchanged. The delivered -extension is named `github`, not the originally proposed `github-issues`. - -Use the canonical `speckit.github.taskstoissues` command with your -[integration's invocation syntax](reference/integrations.md#command-invocation). -There is no legacy `speckit.taskstoissues` alias: consistent alias lifecycle -support would require separate shared-runtime changes. The namespaced command -preserves `before_taskstoissues` and `after_taskstoissues`. No extension is -installed or enabled automatically. - -Integration upgrade removes obsolete manifest-tracked core command files and -re-registers enabled extensions for the default integration. If local edits -block the upgrade, inspect them before using `--force`. CLI upgrade alone does -not update generated commands. Without a generated-file manifest, upgrade -reports that there is nothing to upgrade: review and clean up old commands -manually before reinstalling the integration. For multiple integrations, -upgrade each; `specify integration use ` registers extension -commands when making one the default. - -For projects that do not want GitHub issue tracking, skip extension installation -and run only the integration upgrade; no core tasks-to-issues invocation remains. -Audit presets, dependent extensions, workflow steps, handoffs, and automation: -use `speckit.github.taskstoissues` and -`__SPECKIT_COMMAND_GITHUB_TASKSTOISSUES__` for GitHub conversion and explicitly -require extension installation. Preset overrides of the removed core name do -not automatically customize the canonical extension command. Port them or use -a provider-specific extension for another issue tracker. See the -[extension migration guide](https://github.com/github/spec-kit/blob/main/extensions/github/README.md#migrating-from-the-core-taskstoissues-command). - ## Common Scenarios ### Scenario 1: "I just want new slash commands" diff --git a/extensions/EXTENSION-API-REFERENCE.md b/extensions/EXTENSION-API-REFERENCE.md index 2284067d3c..d1f53ccf84 100644 --- a/extensions/EXTENSION-API-REFERENCE.md +++ b/extensions/EXTENSION-API-REFERENCE.md @@ -634,14 +634,6 @@ Standard events (defined by core): - `before_constitution` - Before constitution update - `after_constitution` - After constitution update -#### Bundled `github` extension events (available when the extension is installed) - -The existing `speckit.github.taskstoissues` command consumes these hook keys. -Core no longer installs an issue-tracking command. - -- `before_taskstoissues` - Before tasks-to-issues conversion -- `after_taskstoissues` - After tasks-to-issues conversion - #### Bundled `bug` extension events (available when the extension is installed) Hooks are prompts run in the same session; they reuse the resolved `BUG_SLUG` and `BUG_DIR` (and, for post-hooks, the written report) from the conversation, and no variables are injected. Pre-hooks run after slug resolution and prerequisites; post-hooks run after the report is written and before completion is reported. diff --git a/extensions/EXTENSION-DEVELOPMENT-GUIDE.md b/extensions/EXTENSION-DEVELOPMENT-GUIDE.md index 1bdd755956..e3514b2679 100644 --- a/extensions/EXTENSION-DEVELOPMENT-GUIDE.md +++ b/extensions/EXTENSION-DEVELOPMENT-GUIDE.md @@ -226,7 +226,6 @@ Available hook points: - `before_checklist` / `after_checklist`: Before/after checklist generation - `before_clarify` / `after_clarify`: Before/after spec clarification - `before_constitution` / `after_constitution`: Before/after constitution update -- `before_taskstoissues` / `after_taskstoissues`: Before/after tasks-to-issues conversion (opt-in `github` extension) - `before_bug_assess` / `after_bug_assess`: Before/after bug assessment (bundled `bug` extension) - `before_bug_fix` / `after_bug_fix`: Before/after bug remediation (bundled `bug` extension) - `before_bug_test` / `after_bug_test`: Before/after bug verification (bundled `bug` extension) diff --git a/extensions/EXTENSION-USER-GUIDE.md b/extensions/EXTENSION-USER-GUIDE.md index a0ee6e6527..4e9d7fa564 100644 --- a/extensions/EXTENSION-USER-GUIDE.md +++ b/extensions/EXTENSION-USER-GUIDE.md @@ -407,9 +407,7 @@ settings: # Available events: before_specify, after_specify, before_plan, after_plan, # before_tasks, after_tasks, before_implement, after_implement, # before_analyze, after_analyze, before_checklist, after_checklist, -# before_clarify, after_clarify, before_constitution, after_constitution -# Bundled github extension events (when installed): -# before_taskstoissues, after_taskstoissues +# before_clarify, after_clarify, before_constitution, after_constitution, # Bundled bug extension events (when installed): # before_bug_assess, after_bug_assess, before_bug_fix, after_bug_fix, # before_bug_test, after_bug_test diff --git a/extensions/github/README.md b/extensions/github/README.md index f40020ba70..63d793b95b 100644 --- a/extensions/github/README.md +++ b/extensions/github/README.md @@ -23,8 +23,7 @@ specify extension add github The `generic` (bring your own agent) integration registers this command under its configured `--commands-dir` in both commands and skills layouts. Installing `github` creates `speckit.github.taskstoissues.md` in commands mode or -`speckit-github-taskstoissues/SKILL.md` in skills mode. Core no longer installs -`speckit.taskstoissues`; use this extension's namespaced command. See +`speckit-github-taskstoissues/SKILL.md` in skills mode. See [integration-specific options](../../docs/reference/integrations.md#integration-specific-options). ## Removal @@ -73,49 +72,37 @@ The extension ships its own feature-resolution script in all three supported run | PowerShell | `scripts/powershell/resolve-tasks.ps1` | | Python | `scripts/python/resolve_tasks.py` | -Once installed they live under `.specify/extensions/github/scripts/`. The script is a trimmed twin of core's `check-prerequisites`: it resolves the project root and active feature directory, requires `plan.md` and `tasks.md`, and reports the design docs alongside them. It matches the invocation the core command makes (`--require-tasks --include-tasks`), so `spec.md` stays optional. By default, a `SPECIFY_FEATURE_DIRECTORY` override is persisted to `.specify/feature.json` so a later run without the variable resolves to the same feature. When `SPECIFY_FEATURE_NO_PERSIST=1` or `true`, the override still selects the feature for this run, but the script does not create or modify `feature.json`. +Once installed they live under `.specify/extensions/github/scripts/`. The script is a trimmed twin of core's `check-prerequisites`: it resolves the project root and active feature directory, requires `plan.md` and `tasks.md`, and reports the design docs alongside them. `spec.md` stays optional. By default, a `SPECIFY_FEATURE_DIRECTORY` override is persisted to `.specify/feature.json` so a later run without the variable resolves to the same feature. When `SPECIFY_FEATURE_NO_PERSIST=1` or `true`, the override still selects the feature for this run, but the script does not create or modify `feature.json`. Prerequisite errors name the Spec Kit command without assuming an integration's invocation syntax. Use the syntax described under [Commands](#commands) for your integration. ## Migrating from the core `taskstoissues` command -**Breaking change:** `taskstoissues` has been removed from core after the -replacement shipped in 1.0.13 and deprecation shipped in 1.1.3. This removal -is intended for the next minor release, not a patch release. New projects do -not receive a tasks-to-issues command unless this extension is explicitly installed. +For existing projects, upgrading the CLI alone does not refresh the generated +core command files. After upgrading the CLI, run this from the project root: -After upgrading to a CLI containing the removal, run from the project root: +```bash +specify integration upgrade +``` + +Replace `` with the installed integration key, or omit it to upgrade the +default integration. If locally modified files block the upgrade, inspect those changes +before deciding whether to overwrite them. See [Upgrade an Integration](../../docs/reference/integrations.md#upgrade-an-integration). + +To migrate, install the extension and use the namespaced command instead: ```bash -specify integration upgrade specify extension add github ``` -Replace `` with the installed integration key, or omit it to -refresh the default integration. Skip `extension add` if `github` is already -installed; the extension's implementation and version are unchanged. -Integration upgrade removes manifest-tracked obsolete core files and -re-registers enabled extensions for the default integration. -Upgrading the CLI alone does not refresh generated files. Inspect local edits -before deciding whether to use `--force`. Projects without an integration -manifest need manual review and cleanup of old generated commands; `upgrade` -reports that it cannot refresh them. - -The `generic` integration supports both commands and skills layouts under its -configured `--commands-dir`. For multiple installed integrations, upgrade each -one; use `specify integration use ` to register extension commands -when changing the default. See [Upgrade an Integration](../../docs/reference/integrations.md#upgrade-an-integration). - -| Removed core command | Canonical extension command | +The `generic` integration supports this migration in both commands and skills +layouts under its configured `--commands-dir`. + +| Core command | Recommended extension command | | ------------------------- | --------------------------------- | | `/speckit.taskstoissues` | `/speckit.github.taskstoissues` | -No legacy alias is installed: its lifecycle is not supported consistently -across commands and skills, so compatibility is deferred rather than expanding -shared registration and preset behavior in this removal. -The existing namespaced command preserves feature resolution, `plan.md` and -`tasks.md` prerequisites, GitHub remote validation, open/closed issue -deduplication, issue titles, and hooks. +Conversion behavior is unchanged: the same feature resolution, the same `plan.md` and `tasks.md` prerequisites, the same remote validation, the same deduplication across open and closed issues, the same issue titles, and the same hook contract. This extension does **not** register `speckit.taskstoissues` as an alias. ### Presets and dependent extensions @@ -125,8 +112,7 @@ For GitHub issue conversion, use the canonical `speckit.github.taskstoissues` command ID and render it with the agent-specific syntax under [Commands](#commands). Replace `__SPECKIT_COMMAND_TASKSTOISSUES__` cross-command references with `__SPECKIT_COMMAND_GITHUB_TASKSTOISSUES__` where template rendering is supported. -Document `specify extension add github` as a setup prerequisite; core never -installs this dependency. +Document `specify extension add github` as a setup prerequisite. If an extension proposal or external dependency declaration uses `requires.commands: [speckit.taskstoissues]`, update its target to @@ -141,14 +127,3 @@ command. Review and port any needed customization, then verify the installed command in a sample project before retiring the old override. A preset that uses another issue tracker should retain its own provider-specific workflow rather than redirecting it to the GitHub command. - -Keep the `before_taskstoissues` and `after_taskstoissues` hook keys unchanged; -the namespaced command still consumes them. Update a hook's command reference -if it invokes the removed core command. - -Related proposals such as [milestone grouping (github/spec-kit#4370)](https://github.com/github/spec-kit/issues/4370) -should target `speckit.github.taskstoissues`, not a core command. -[Jira support (github/spec-kit#2223)](https://github.com/github/spec-kit/issues/2223) -belongs in a provider-specific extension rather than an override of a -GitHub-specific core command. Existing third-party presets targeting the old -core name need their authors to port and verify those overrides. diff --git a/src/specify_cli/extensions/__init__.py b/src/specify_cli/extensions/__init__.py index a9ba5e6b34..6fcd2a0d6a 100644 --- a/src/specify_cli/extensions/__init__.py +++ b/src/specify_cli/extensions/__init__.py @@ -1204,10 +1204,8 @@ def _normalize_shadow_name(name: str) -> str: Agent integrations (Cline, Forge, Junie) and the SKILL.md output-name computation (``CommandRegistrar._compute_output_name``) all collapse - dots to hyphens and prefix a bare name with ``speckit-``, so - ``speckit.tasks``, ``tasks``, and - ``speckit-tasks`` are distinct alias spellings that land on - the same on-disk command name. Normalize before comparing so all of + dots to hyphens and prefix a bare name with ``speckit-``. + Normalize before comparing so all of them are caught, not just the exact dotted spelling. """ hyphenated = name.replace(".", "-") diff --git a/tests/extensions/github/test_github_extension.py b/tests/extensions/github/test_github_extension.py index 45bd8c6200..ad3306c678 100644 --- a/tests/extensions/github/test_github_extension.py +++ b/tests/extensions/github/test_github_extension.py @@ -3,7 +3,7 @@ Validates: - Bundled layout (manifest, README, command file, script twins) - Catalog registration and wheel/source resolution via ``_locate_bundled_extension`` -- Manifest validation, including the absence of a legacy compatibility alias +- Manifest validation - Install/uninstall through ``ExtensionManager`` - Rendered command artifacts across command mode and skills mode, and in particular that ``{SCRIPT}`` resolves to a script the extension actually @@ -36,7 +36,6 @@ PROJECT_ROOT = Path(__file__).resolve().parent.parent.parent.parent EXT_DIR = PROJECT_ROOT / "extensions" / "github" -CORE_COMMAND = PROJECT_ROOT / "templates" / "commands" / "taskstoissues.md" # A released Spec Kit version that satisfies the manifest floor. Installs are # refused below it, so this cannot be an arbitrary synthetic value. @@ -168,14 +167,6 @@ def test_manifest_command_files_exist(self): for cmd in m.commands: assert (EXT_DIR / cmd["file"]).is_file() - def test_no_legacy_compatibility_alias_is_registered(self): - """Removal does not expand the extension's alias lifecycle surface.""" - from specify_cli.extensions import ExtensionManifest - - m = ExtensionManifest(EXT_DIR / "extension.yml") - aliases = [a for cmd in m.commands for a in cmd.get("aliases", []) or []] - assert aliases == [] - def test_declares_its_mandatory_external_tools(self): """The command runs ``git config`` and needs the GitHub MCP tools.""" tools = {t["name"]: t for t in _manifest_dict()["requires"]["tools"]} @@ -183,7 +174,13 @@ def test_declares_its_mandatory_external_tools(self): assert all(t["required"] is True for t in tools.values()) def test_version_floor_covers_all_rendering_prerequisites(self): - """The extension retains its existing rendering compatibility floor.""" + """The floor must exclude releases that cannot render this command. + + Extension-local ``scripts/...`` rewriting landed in 0.12.6 (#3364), + but auto-registered skills did not resolve ``__SPECKIT_COMMAND_*__`` + tokens until 0.12.17 (#3544). Versions through 0.12.16 therefore + accept the extension but leak a raw command token in skills mode. + """ from packaging.specifiers import SpecifierSet floor = _manifest_dict()["requires"]["speckit_version"] @@ -194,10 +191,6 @@ def test_version_floor_covers_all_rendering_prerequisites(self): # And the version the install tests use must satisfy it. assert INSTALL_SPECKIT_VERSION in spec, floor - def test_core_command_no_longer_ships(self): - assert not CORE_COMMAND.exists() - - # -- Install / uninstall ------------------------------------------------------ @@ -257,11 +250,6 @@ def test_generic_registers_and_removes_artifact_in_configured_directory( if skills_mode else f"{COMMAND_NAME}.md" ) assert artifact.is_file() - alias = commands_dir / ( - "speckit-taskstoissues/SKILL.md" - if skills_mode else "speckit.taskstoissues.md" - ) - assert not alias.exists() content = artifact.read_text(encoding="utf-8") assert ".specify/extensions/github/scripts/bash/resolve-tasks.sh" in content assert "{SCRIPT}" not in content @@ -270,7 +258,6 @@ def test_generic_registers_and_removes_artifact_in_configured_directory( ).exists() assert manager.remove("github") assert not artifact.exists() - assert not alias.exists() assert core.read_text(encoding="utf-8") == "core command remains unchanged\n" def test_install_copies_command_and_scripts(self, tmp_path: Path): @@ -317,9 +304,7 @@ def test_remove_uninstalls_cleanly(self, tmp_path: Path): ), ( "claude", - [ - ".claude/skills/speckit-github-taskstoissues/SKILL.md", - ], + [".claude/skills/speckit-github-taskstoissues/SKILL.md"], ".claude/skills/speckit-tasks/SKILL.md", ), ], @@ -436,8 +421,8 @@ def test_every_extension_registrar_integration_renders_the_command( Covers both layouts in one sweep — command-file agents, skills-mode agents, and Hermes, which installs to ``~/.hermes/skills`` rather than - a project-local directory (hence the redirected home). Generic resolves - persisted output options and is covered separately. + a project-local directory (hence the redirected home). Generic has no + static registrar config and does not register extension add-ons. """ from specify_cli.extensions import CommandRegistrar, ExtensionManager @@ -470,12 +455,6 @@ def test_every_extension_registrar_integration_renders_the_command( and "taskstoissues" in p.as_posix().lower() ] assert artifacts, f"{agent} produced no command artifact" - assert any("github" in p.name or "github" in p.parent.name for p in artifacts) - aliases = [ - p for p in artifacts - if "github" not in p.name and "github" not in p.parent.name - ] - assert not aliases, f"{agent} unexpectedly installed a legacy alias" expected = f".specify/extensions/github/{SCRIPT_TWINS['sh']}" bodies = [p.read_text(encoding="utf-8") for p in artifacts] @@ -483,7 +462,6 @@ def test_every_extension_registrar_integration_renders_the_command( # No artifact may leak an unresolved placeholder... for artifact, content in zip(artifacts, bodies): assert "{SCRIPT}" not in content, artifact - assert "__SPECKIT_COMMAND_" not in content, artifact # ...and the command body must carry the resolved extension-local path. # Some integrations also emit a thin companion file (e.g. Copilot's # prompt shim, which only points at the agent), so this is "at least @@ -601,15 +579,6 @@ def test_preserves_deduplication_and_pagination(self): # Four-digit and longer task IDs must still match. assert r"\bT\d{3,}\b" in body - def test_body_uses_only_extension_local_prerequisites(self): - """The sole implementation cannot depend on the removed core command.""" - text = COMMAND_FILE.read_text(encoding="utf-8") - assert "parse FEATURE_DIR, TASKS and the AVAILABLE_DOCS list" in text - assert "extract the path to **tasks** (the `TASKS` value)" in text - assert "check-prerequisites" not in text - assert "check_prerequisites" not in text - assert set(_command_frontmatter()["scripts"]) == set(SCRIPT_TWINS) - def test_uses_the_portable_command_reference_token(self): """A literal invocation would be correct for exactly one agent.""" body = COMMAND_FILE.read_text(encoding="utf-8") diff --git a/tests/extensions/github/test_taskstoissues_removal.py b/tests/extensions/github/test_taskstoissues_removal.py deleted file mode 100644 index dd91873969..0000000000 --- a/tests/extensions/github/test_taskstoissues_removal.py +++ /dev/null @@ -1,86 +0,0 @@ -"""Contracts for opt-in issue tracking after removal from core.""" - -from pathlib import Path - -import pytest -import yaml - - -ROOT = Path(__file__).resolve().parents[3] -CORE = ROOT / "templates/commands/taskstoissues.md" -EXTENSION = ROOT / "extensions/github/commands/speckit.github.taskstoissues.md" - - -def test_core_template_is_removed(): - assert not CORE.exists() - - -def test_extension_frontmatter_preserves_execution_metadata(): - metadata = yaml.safe_load(EXTENSION.read_text(encoding="utf-8").split("---", 2)[1]) - assert metadata["tools"] == [ - "github/github-mcp-server/list_issues", - "github/github-mcp-server/issue_write", - ] - assert metadata["scripts"] == { - "sh": "scripts/bash/resolve-tasks.sh --json", - "ps": "scripts/powershell/resolve-tasks.ps1 -Json", - "py": "scripts/python/resolve_tasks.py --json", - } - - -def test_recommended_extension_is_not_deprecated(): - text = EXTENSION.read_text(encoding="utf-8") - assert "## Deprecation Notice" not in text - assert "future minor release" not in text - assert not yaml.safe_load(text.split("---", 2)[1])["description"].startswith("Deprecated:") - - -def test_taskstoissues_hooks_and_safety_contracts_remain(): - text = EXTENSION.read_text(encoding="utf-8") - assert "hooks.before_taskstoissues" in text - assert "hooks.after_taskstoissues" in text - assert "ONLY PROCEED TO NEXT STEPS IF THE REMOTE IS A GITHUB URL" in text - assert "**Fetch existing issues for deduplication**" in text - assert "**Skip** any task whose ID is already present" in text - assert "Only create issues for tasks that do not yet have a matching issue." in text - assert "UNDER NO CIRCUMSTANCES EVER CREATE ISSUES IN REPOSITORIES THAT DO NOT MATCH THE REMOTE URL" in text - - -@pytest.mark.parametrize( - "integration_key", ["opencode", "agy", "claude", "copilot", "gemini", "goose"], -) -def test_fresh_integration_has_no_issue_tracking(tmp_path, integration_key): - from specify_cli.integrations import get_integration - from specify_cli.integrations.manifest import IntegrationManifest - - integration = get_integration(integration_key) - created = integration.setup( - tmp_path, IntegrationManifest(integration_key, tmp_path), script_type="sh" - ) - assert created - assert not any("taskstoissues" in path.as_posix() for path in created) - assert not (tmp_path / ".specify/extensions/github").exists() - - -def test_core_inventories_release_the_legacy_name(): - from specify_cli import SKILL_DESCRIPTIONS - from specify_cli.extensions import CORE_COMMAND_NAMES, _FALLBACK_CORE_COMMAND_NAMES - from specify_cli.integrations.base import _CORE_COMMAND_TEMPLATE_ORDER - from specify_cli.integrations.copilot import _COPILOT_CORE_COMMANDS - from specify_cli.integrations.claude import ARGUMENT_HINTS as claude_hints - from specify_cli.integrations.alquimia import ARGUMENT_HINTS as alquimia_hints - - for inventory in ( - SKILL_DESCRIPTIONS, CORE_COMMAND_NAMES, _FALLBACK_CORE_COMMAND_NAMES, - _CORE_COMMAND_TEMPLATE_ORDER, _COPILOT_CORE_COMMANDS, - claude_hints, alquimia_hints, - ): - assert "taskstoissues" not in inventory - - -def test_legacy_alias_is_not_installed_by_the_extension(): - text = EXTENSION.read_text(encoding="utf-8") - manifest = yaml.safe_load((ROOT / "extensions/github/extension.yml").read_text(encoding="utf-8")) - assert "## Compatibility Alias" not in text - assert "__SPECKIT_COMMAND_TASKSTOISSUES__" not in text - assert all(not command.get("aliases") for command in manifest["provides"]["commands"]) diff --git a/tests/specify_cli/integrations/test_command_upgrade.py b/tests/specify_cli/integrations/test_command_upgrade.py index 6048feedca..42d5ea0a6c 100644 --- a/tests/specify_cli/integrations/test_command_upgrade.py +++ b/tests/specify_cli/integrations/test_command_upgrade.py @@ -27,73 +27,6 @@ ) class TestIntegrationUpgradeDetailed: - @pytest.mark.parametrize( - ("key", "options", "legacy_paths"), - [ - ("copilot", "--commands", [ - ".github/agents/speckit.taskstoissues.agent.md", - ".github/prompts/speckit.taskstoissues.prompt.md", - ]), - ("copilot", "--skills", [".github/skills/speckit-taskstoissues/SKILL.md"]), - ("claude", None, [".claude/skills/speckit-taskstoissues/SKILL.md"]), - ("gemini", None, [".gemini/commands/speckit.taskstoissues.toml"]), - ("generic", "--commands-dir .custom/commands", [ - ".custom/commands/speckit.taskstoissues.md", - ]), - ("generic", "--commands-dir .custom/commands --skills", [ - ".custom/commands/speckit-taskstoissues/SKILL.md", - ]), - ], - ) - @pytest.mark.parametrize("with_extension", [False, True]) - def test_upgrade_retires_core_taskstoissues_and_preserves_only_opt_in_command( - self, tmp_path, key, options, legacy_paths, with_extension, - ): - from specify_cli.extensions import ExtensionManager - from specify_cli.integrations.manifest import IntegrationManifest - - project = _init_project(tmp_path, key, integration_options=options) - manifest = IntegrationManifest.load(key, project) - for relative in legacy_paths: - assert not (project / relative).exists() - manifest.record_file(relative, "old core taskstoissues\n") - manifest.save() - user_file = project / "user-notes.md" - user_file.write_text("keep user content\n", encoding="utf-8") - - upgraded = _run_in_project(project, ["integration", "upgrade", key]) - assert upgraded.exit_code == 0, upgraded.output - assert not any((project / path).exists() for path in legacy_paths) - if with_extension: - installed = _run_in_project(project, ["extension", "add", "github"]) - assert installed.exit_code == 0, installed.output - - result = _run_in_project(project, ["integration", "upgrade", key, "--force"]) - - assert result.exit_code == 0, result.output - updated = IntegrationManifest.load(key, project) - assert not any("taskstoissues" in path for path in updated.files) - assert user_file.read_text(encoding="utf-8") == "keep user content\n" - canonical_paths = [ - relative.replace("speckit.taskstoissues", "speckit.github.taskstoissues") - .replace("speckit-taskstoissues", "speckit-github-taskstoissues") - for relative in legacy_paths - ] - assert not any((project / path).exists() for path in legacy_paths) - for relative in canonical_paths: - artifact = project / relative - assert artifact.exists() is with_extension - if with_extension and "prompts/" not in relative: - content = artifact.read_text(encoding="utf-8") - assert ".specify/extensions/github/scripts/" in content - assert "old core taskstoissues" not in content - manager = ExtensionManager(project) - assert manager.registry.is_installed("github") is with_extension - if with_extension: - assert manager.remove("github") - assert not any((project / path).exists() for path in legacy_paths) - assert not any((project / path).exists() for path in canonical_paths) - def test_upgrade_invalid_manifest_reports_cli_error(self, tmp_path): project = _init_project(tmp_path, "claude") _write_invalid_manifest(project, "claude") From 491a895a220afd25ddfa69d10f19a5b9809247c8 Mon Sep 17 00:00:00 2001 From: Manfred Riem <15701806+mnriem@users.noreply.github.com> Date: Thu, 29 Oct 2026 16:05:31 -0500 Subject: [PATCH 4/5] fix: document and cover taskstoissues removal migration Assisted-by: GitHub Copilot (model: GPT-6.1 Sol, autonomous) Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --- CHANGELOG.md | 17 +++++ docs/upgrade.md | 31 +++++++++ extensions/EXTENSION-API-REFERENCE.md | 9 +++ extensions/EXTENSION-DEVELOPMENT-GUIDE.md | 1 + extensions/EXTENSION-USER-GUIDE.md | 2 + extensions/catalog.json | 2 +- extensions/github/README.md | 24 +++++-- extensions/github/extension.yml | 2 +- .../github/test_github_extension.py | 10 ++- .../integrations/test_command_upgrade.py | 68 +++++++++++++++++++ 10 files changed, 158 insertions(+), 8 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index c6aa580d01..73c94223cd 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,23 @@ +## [Unreleased] + +### Removed + +- **Breaking:** Remove the core `speckit.taskstoissues` command. GitHub issue + conversion is now opt-in through the bundled `github` extension and its + canonical `speckit.github.taskstoissues` command; no legacy alias is provided. + Upgrade each installed integration with `specify integration upgrade ` + before running `specify extension add github` (skip installation if already + installed). Port customizations before forcing the upgrade: old manifest-tracked + command and skill artifacts are deleted, not replaced. See the + [migration guide](https://github.github.io/spec-kit/upgrade/#breaking-core-taskstoissues-removal). + Per [#4423](https://github.com/github/spec-kit/issues/4423), ship this removal + only in a later minor release, after the extension and core deprecation from + #4421 and #4422 have shipped in earlier minor releases. This entry does not + change or select a release version. + ## [1.2.2] - 2026-10-19 ### Changed diff --git a/docs/upgrade.md b/docs/upgrade.md index 45e664c0a4..182abfef24 100644 --- a/docs/upgrade.md +++ b/docs/upgrade.md @@ -17,6 +17,37 @@ --- +## Breaking: core `taskstoissues` removal + +The core `speckit.taskstoissues` command has been removed. GitHub issue conversion +is now opt-in through the bundled `github` extension and its canonical +`speckit.github.taskstoissues` command. Installing the extension does **not** +restore the old invocation or create a compatibility alias. + +After upgrading the CLI, migrate from the project root in this order: + +1. Review and port any customizations in the old command or skill to the + namespaced extension command before forcing an integration upgrade. The + upgrade deletes old manifest-tracked `taskstoissues` artifacts; `--force` + authorizes deletion even when those files were locally customized. +2. Run `specify integration upgrade ` for each installed integration. + Replace `` with its key, such as `copilot` or `claude`. +3. Run `specify extension add github` only if `github` is not already installed. + An already-installed, enabled extension is re-registered during the integration + upgrade; it does not need to be reinstalled. +4. Invoke `speckit.github.taskstoissues` using your integration's syntax and update + references in presets, handoffs, extension prompts, and automation. + +See the [extension migration guide](https://github.com/github/spec-kit/blob/main/extensions/github/README.md#migrating-from-the-core-taskstoissues-command) +for invocation syntax, customization migration, and dependent extensions. +Projects that do not opt in no longer receive a tasks-to-issues command. + +Release sequencing follows [#4423](https://github.com/github/spec-kit/issues/4423): +the removal must ship in a later minor release, after the bundled extension +and core deprecation from #4421 and #4422 have shipped in earlier minor releases. + +--- + ## Part 1: Upgrade the CLI Tool The CLI tool (`specify`) is separate from your project files. Upgrade it to get the latest features and bug fixes. diff --git a/extensions/EXTENSION-API-REFERENCE.md b/extensions/EXTENSION-API-REFERENCE.md index d1f53ccf84..50e3b81b28 100644 --- a/extensions/EXTENSION-API-REFERENCE.md +++ b/extensions/EXTENSION-API-REFERENCE.md @@ -645,6 +645,15 @@ Hooks are prompts run in the same session; they reuse the resolved `BUG_SLUG` an - `before_bug_test` - Before running verification - `after_bug_test` - After writing `test.md` +#### Bundled `github` extension events (available when the extension is installed) + +The `speckit.github.taskstoissues` command consumes these events from +`.specify/extensions.yml`. They are extension-owned, not core events; existing +registrations, including the `git` extension's hooks, keep the same keys. + +- `before_taskstoissues` - Before tasks-to-issues conversion +- `after_taskstoissues` - After tasks-to-issues conversion + ### Hook Configuration **In `.specify/extensions.yml`**: diff --git a/extensions/EXTENSION-DEVELOPMENT-GUIDE.md b/extensions/EXTENSION-DEVELOPMENT-GUIDE.md index e3514b2679..63db3823a0 100644 --- a/extensions/EXTENSION-DEVELOPMENT-GUIDE.md +++ b/extensions/EXTENSION-DEVELOPMENT-GUIDE.md @@ -229,6 +229,7 @@ Available hook points: - `before_bug_assess` / `after_bug_assess`: Before/after bug assessment (bundled `bug` extension) - `before_bug_fix` / `after_bug_fix`: Before/after bug remediation (bundled `bug` extension) - `before_bug_test` / `after_bug_test`: Before/after bug verification (bundled `bug` extension) +- `before_taskstoissues` / `after_taskstoissues`: Before/after tasks-to-issues conversion by `speckit.github.taskstoissues` (bundled `github` extension, available when installed; not core events) Each event accepts a single hook object or a list of hook objects (multiple commands on one event). diff --git a/extensions/EXTENSION-USER-GUIDE.md b/extensions/EXTENSION-USER-GUIDE.md index 4e9d7fa564..d0e37c9544 100644 --- a/extensions/EXTENSION-USER-GUIDE.md +++ b/extensions/EXTENSION-USER-GUIDE.md @@ -411,6 +411,8 @@ settings: # Bundled bug extension events (when installed): # before_bug_assess, after_bug_assess, before_bug_fix, after_bug_fix, # before_bug_test, after_bug_test +# Bundled github extension events (when installed; not core events): +# before_taskstoissues, after_taskstoissues hooks: after_tasks: - extension: jira diff --git a/extensions/catalog.json b/extensions/catalog.json index 30e6dd92ac..c20cec03e6 100644 --- a/extensions/catalog.json +++ b/extensions/catalog.json @@ -66,7 +66,7 @@ "github": { "name": "GitHub Integration", "id": "github", - "version": "1.0.2", + "version": "1.0.3", "description": "GitHub platform integration for Spec Kit - create GitHub issues from a feature's task list", "author": "spec-kit-core", "repository": "https://github.com/github/spec-kit", diff --git a/extensions/github/README.md b/extensions/github/README.md index 63d793b95b..137637dbe6 100644 --- a/extensions/github/README.md +++ b/extensions/github/README.md @@ -78,23 +78,37 @@ Prerequisite errors name the Spec Kit command without assuming an integration's ## Migrating from the core `taskstoissues` command +**Breaking change:** the core command has been removed. Integration upgrades +delete old manifest-tracked `taskstoissues` command and skill files, rather than +replacing them with an updated prompt. Review and port local customizations to +the namespaced extension command **before** using `--force`; forcing the upgrade +also authorizes deletion of customized legacy files. Installing this extension +does not restore the old invocation or provide a compatibility alias. + For existing projects, upgrading the CLI alone does not refresh the generated core command files. After upgrading the CLI, run this from the project root: ```bash -specify integration upgrade +specify integration upgrade ``` -Replace `` with the installed integration key, or omit it to upgrade the -default integration. If locally modified files block the upgrade, inspect those changes -before deciding whether to overwrite them. See [Upgrade an Integration](../../docs/reference/integrations.md#upgrade-an-integration). +Replace `` with the installed integration key, or omit it to upgrade +the default integration. Repeat for each installed integration. If locally +modified files block the upgrade, port those changes before deciding whether to +delete the legacy files with `--force`. See +[Upgrade an Integration](https://github.github.io/spec-kit/reference/integrations/#upgrade-an-integration). -To migrate, install the extension and use the namespaced command instead: +Then install the extension, **skipping this step if `github` is already +installed**, and use the namespaced command instead: ```bash specify extension add github ``` +An already-installed, enabled extension is re-registered during the integration +upgrade. See the [public upgrade guide](https://github.github.io/spec-kit/upgrade/#breaking-core-taskstoissues-removal) +for the complete migration sequence. + The `generic` integration supports this migration in both commands and skills layouts under its configured `--commands-dir`. diff --git a/extensions/github/extension.yml b/extensions/github/extension.yml index c6df8f19a1..158824e823 100644 --- a/extensions/github/extension.yml +++ b/extensions/github/extension.yml @@ -3,7 +3,7 @@ schema_version: "1.0" extension: id: github name: "GitHub Integration" - version: "1.0.2" + version: "1.0.3" description: "GitHub platform integration for Spec Kit - create GitHub issues from a feature's task list" author: spec-kit-core repository: https://github.com/github/spec-kit diff --git a/tests/extensions/github/test_github_extension.py b/tests/extensions/github/test_github_extension.py index ad3306c678..b0277452bb 100644 --- a/tests/extensions/github/test_github_extension.py +++ b/tests/extensions/github/test_github_extension.py @@ -157,9 +157,17 @@ def test_manifest_validates(self): m = ExtensionManifest(EXT_DIR / "extension.yml") assert m.id == "github" - assert m.version == "1.0.2" + assert m.version == "1.0.3" assert [c["name"] for c in m.commands] == [COMMAND_NAME] + def test_manifest_does_not_restore_legacy_alias(self): + from specify_cli.extensions import ExtensionManifest + + manifest = ExtensionManifest(EXT_DIR / "extension.yml") + for command in manifest.commands: + assert command["name"] != "speckit.taskstoissues" + assert "speckit.taskstoissues" not in command.get("aliases", []) + def test_manifest_command_files_exist(self): from specify_cli.extensions import ExtensionManifest diff --git a/tests/specify_cli/integrations/test_command_upgrade.py b/tests/specify_cli/integrations/test_command_upgrade.py index 42d5ea0a6c..e2efbea24b 100644 --- a/tests/specify_cli/integrations/test_command_upgrade.py +++ b/tests/specify_cli/integrations/test_command_upgrade.py @@ -27,6 +27,74 @@ ) class TestIntegrationUpgradeDetailed: + @pytest.mark.parametrize("skills_mode", [False, True], ids=["commands", "skills"]) + @pytest.mark.parametrize("github_installed", [False, True], ids=["opt-in", "installed"]) + @pytest.mark.parametrize("customized", [False, True], ids=["unchanged", "customized"]) + def test_upgrade_removes_legacy_taskstoissues_without_restoring_alias( + self, tmp_path, skills_mode, github_installed, customized + ): + from specify_cli.extensions import ExtensionManager + from specify_cli.integrations.manifest import IntegrationManifest + + project = _init_project( + tmp_path, + "generic", + integration_options="--commands-dir .myagent/commands" + + (" --skills" if skills_mode else ""), + ) + command_root = project / ".myagent" / "commands" + legacy = command_root / ( + "speckit-taskstoissues/SKILL.md" + if skills_mode else "speckit.taskstoissues.md" + ) + canonical = command_root / ( + "speckit-github-taskstoissues/SKILL.md" + if skills_mode else "speckit.github.taskstoissues.md" + ) + assert not legacy.exists() + assert not canonical.exists() + assert not ExtensionManager(project).registry.is_installed("github") + + manifest = IntegrationManifest.load("generic", project) + legacy_relative = legacy.relative_to(project).as_posix() + manifest.record_file(legacy_relative, "# Legacy core taskstoissues\n") + manifest.save() + if customized: + legacy.write_text("# User-customized taskstoissues\n", encoding="utf-8") + user_file = command_root / "user-notes.md" + user_file.write_text("Keep my notes\n", encoding="utf-8") + + if github_installed: + result = _run_in_project(project, ["extension", "add", "github"]) + assert result.exit_code == 0, result.output + assert canonical.is_file() + + upgrade = ["integration", "upgrade", "generic"] + if customized: + manifest_before = manifest.manifest_path.read_bytes() + result = _run_in_project(project, upgrade) + assert result.exit_code != 0, result.output + assert "--force" in result.output + assert legacy.read_text(encoding="utf-8") == "# User-customized taskstoissues\n" + assert manifest.manifest_path.read_bytes() == manifest_before + upgrade.append("--force") + + result = _run_in_project(project, upgrade) + assert result.exit_code == 0, result.output + assert not legacy.exists() + assert legacy_relative not in IntegrationManifest.load("generic", project).files + assert user_file.read_text(encoding="utf-8") == "Keep my notes\n" + assert canonical.is_file() == github_installed, result.output + assert ExtensionManager(project).registry.is_installed("github") == github_installed + + if not github_installed: + result = _run_in_project(project, ["extension", "add", "github"]) + assert result.exit_code == 0, result.output + assert canonical.is_file() + assert not legacy.exists() + assert legacy_relative not in IntegrationManifest.load("generic", project).files + assert user_file.read_text(encoding="utf-8") == "Keep my notes\n" + def test_upgrade_invalid_manifest_reports_cli_error(self, tmp_path): project = _init_project(tmp_path, "claude") _write_invalid_manifest(project, "claude") From 9a067879abf6ff542ab2e9ac5ee9870c656e0fe8 Mon Sep 17 00:00:00 2001 From: Manfred Riem <15701806+mnriem@users.noreply.github.com> Date: Fri, 30 Oct 2026 12:09:07 -0500 Subject: [PATCH 5/5] fix: limit core removal to scrubbing and extension documentation Remove command-specific migration guidance and hook references from core docs and drop added legacy-artifact migration checks. Keep extension documentation in its README without changing the existing backup warning. Assisted-by: GitHub Copilot (model: GPT-6.1 Sol, autonomous) Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --- CHANGELOG.md | 18 ----- docs/extensions/api.md | 9 --- docs/extensions/developing.md | 1 - docs/extensions/overview.md | 2 - docs/integrations/cli.md | 8 --- docs/upgrade.md | 36 ---------- extensions/github/README.md | 3 +- .../integrations/test_command_upgrade.py | 68 ------------------- 8 files changed, 1 insertion(+), 144 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 98d5befe26..e6b76318a3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,24 +2,6 @@ -## [Unreleased] - -### Removed - -- **Breaking:** Remove the core `speckit.taskstoissues` command. GitHub issue - conversion is now opt-in through the bundled `github` extension and its - canonical `speckit.github.taskstoissues` command; no legacy alias is provided. - Upgrade each installed integration with `specify integration upgrade ` - before running `specify extension add github` (skip installation if already - installed). Back up customizations before forcing the upgrade and reapply them - after migration: old manifest-tracked command and skill artifacts are deleted, - not replaced. See the - [migration guide](https://github.github.io/spec-kit/upgrade.html#breaking-core-taskstoissues-removal). - Per [#4423](https://github.com/github/spec-kit/issues/4423), ship this removal - only in a later minor release, after the extension and core deprecation from - #4421 and #4422 have shipped in earlier minor releases. This entry does not - change or select a release version. - ## [1.2.5] - 2026-10-28 ### Changed diff --git a/docs/extensions/api.md b/docs/extensions/api.md index 40b8073e63..682221c4f4 100644 --- a/docs/extensions/api.md +++ b/docs/extensions/api.md @@ -644,15 +644,6 @@ Hooks are prompts run in the same session; they reuse the resolved `BUG_SLUG` an - `before_bug_test` - Before running verification - `after_bug_test` - After writing `test.md` -#### Bundled `github` extension events (available when the extension is installed) - -The `speckit.github.taskstoissues` command consumes these events from -`.specify/extensions.yml`. They are extension-owned, not core events; existing -registrations, including the `git` extension's hooks, keep the same keys. - -- `before_taskstoissues` - Before tasks-to-issues conversion -- `after_taskstoissues` - After tasks-to-issues conversion - ### Hook Configuration **In `.specify/extensions.yml`**: diff --git a/docs/extensions/developing.md b/docs/extensions/developing.md index 8dc96dffe5..ed95e2c2c3 100644 --- a/docs/extensions/developing.md +++ b/docs/extensions/developing.md @@ -240,7 +240,6 @@ Available hook points: - `before_bug_assess` / `after_bug_assess`: Before/after bug assessment (bundled `bug` extension) - `before_bug_fix` / `after_bug_fix`: Before/after bug remediation (bundled `bug` extension) - `before_bug_test` / `after_bug_test`: Before/after bug verification (bundled `bug` extension) -- `before_taskstoissues` / `after_taskstoissues`: Before/after tasks-to-issues conversion by `speckit.github.taskstoissues` (bundled `github` extension, available when installed; not core events) Each event accepts a single hook object or a list of hook objects (multiple commands on one event). diff --git a/docs/extensions/overview.md b/docs/extensions/overview.md index 07d6d713c3..033e5c2f4b 100644 --- a/docs/extensions/overview.md +++ b/docs/extensions/overview.md @@ -391,8 +391,6 @@ settings: # Bundled bug extension events (when installed): # before_bug_assess, after_bug_assess, before_bug_fix, after_bug_fix, # before_bug_test, after_bug_test -# Bundled github extension events (when installed; not core events): -# before_taskstoissues, after_taskstoissues hooks: after_tasks: - extension: jira diff --git a/docs/integrations/cli.md b/docs/integrations/cli.md index 0da4965707..8605e691f8 100644 --- a/docs/integrations/cli.md +++ b/docs/integrations/cli.md @@ -486,14 +486,6 @@ Docker Agent currently exposes structured agent/safety configuration. Headless approval flags, model forwarding, and structured-output support are also adapter-specific, not universal guarantees. -## GitHub Issue Creation - -The core `speckit.taskstoissues` command has been removed. Install the opt-in GitHub extension -with `specify extension add github` and use `speckit.github.taskstoissues` -instead. See the [extension's migration guide](https://github.com/github/spec-kit/blob/main/extensions/github/README.md#migrating-from-the-core-taskstoissues-command) -for invocation syntax and migration details. The extension is not installed -automatically and does not restore the old invocation. - ## FAQ ### Can I install multiple integrations in the same project? diff --git a/docs/upgrade.md b/docs/upgrade.md index 9b4afaeb42..943d9f41a1 100644 --- a/docs/upgrade.md +++ b/docs/upgrade.md @@ -17,42 +17,6 @@ --- -## Breaking: core `taskstoissues` removal - -The core `speckit.taskstoissues` command has been removed. GitHub issue conversion -is now opt-in through the bundled `github` extension and its canonical -`speckit.github.taskstoissues` command. Installing the extension does **not** -restore the old invocation or create a compatibility alias. - -After upgrading the CLI, migrate from the project root in this order: - -1. Back up customized legacy and extension command or skill files outside the - generated directories before upgrading. The upgrade deletes old - manifest-tracked `taskstoissues` artifacts; `--force` authorizes deletion even - when those files were locally customized. Generated extension files can also - be overwritten, so do not port edits into them before the upgrade. -2. Run `specify integration upgrade ` for each installed integration. - Replace `` with its key, such as `copilot` or `claude`. -3. Run `specify extension add github` only if `github` is not already installed. - An already-installed, enabled extension is re-registered when upgrading the - default integration only; it does not need to be reinstalled. -4. Before using the replacement in a non-default integration, run - `specify integration use ` to select it and register the extension. -5. After upgrade, extension setup, and integration selection, reapply saved - customizations to the namespaced extension command. Invoke - `speckit.github.taskstoissues` using your integration's syntax and update - references in presets, handoffs, extension prompts, and automation. - -See the [extension migration guide](https://github.com/github/spec-kit/blob/main/extensions/github/README.md#migrating-from-the-core-taskstoissues-command) -for invocation syntax, customization migration, and dependent extensions. -Projects that do not opt in no longer receive a tasks-to-issues command. - -Release sequencing follows [#4423](https://github.com/github/spec-kit/issues/4423): -the removal must ship in a later minor release, after the bundled extension -and core deprecation from #4421 and #4422 have shipped in earlier minor releases. - ---- - ## Part 1: Upgrade the CLI Tool The CLI tool (`specify`) is separate from your project files. Upgrade it to get the latest features and bug fixes. diff --git a/extensions/github/README.md b/extensions/github/README.md index 9fa48aa8bb..e2a6cd0f71 100644 --- a/extensions/github/README.md +++ b/extensions/github/README.md @@ -112,8 +112,7 @@ default integration only. Before using the replacement in a non-default integration, run `specify integration use ` to select it and register the extension there. After upgrading, installing if needed, and selecting the integration, reapply customizations from your separate backup to the namespaced -extension command. See the [public upgrade guide](https://github.github.io/spec-kit/upgrade.html#breaking-core-taskstoissues-removal) -for the complete migration sequence. +extension command. The `generic` integration supports this migration in both commands and skills layouts under its configured `--commands-dir`. diff --git a/tests/specify_cli/integrations/test_command_upgrade.py b/tests/specify_cli/integrations/test_command_upgrade.py index 44b2223c34..b6c3b0690a 100644 --- a/tests/specify_cli/integrations/test_command_upgrade.py +++ b/tests/specify_cli/integrations/test_command_upgrade.py @@ -27,74 +27,6 @@ ) class TestIntegrationUpgradeDetailed: - @pytest.mark.parametrize("skills_mode", [False, True], ids=["commands", "skills"]) - @pytest.mark.parametrize("github_installed", [False, True], ids=["opt-in", "installed"]) - @pytest.mark.parametrize("customized", [False, True], ids=["unchanged", "customized"]) - def test_upgrade_removes_legacy_taskstoissues_without_restoring_alias( - self, tmp_path, skills_mode, github_installed, customized - ): - from specify_cli.extensions import ExtensionManager - from specify_cli.integrations.manifest import IntegrationManifest - - project = _init_project( - tmp_path, - "generic", - integration_options="--commands-dir .myagent/commands" - + (" --skills" if skills_mode else ""), - ) - command_root = project / ".myagent" / "commands" - legacy = command_root / ( - "speckit-taskstoissues/SKILL.md" - if skills_mode else "speckit.taskstoissues.md" - ) - canonical = command_root / ( - "speckit-github-taskstoissues/SKILL.md" - if skills_mode else "speckit.github.taskstoissues.md" - ) - assert not legacy.exists() - assert not canonical.exists() - assert not ExtensionManager(project).registry.is_installed("github") - - manifest = IntegrationManifest.load("generic", project) - legacy_relative = legacy.relative_to(project).as_posix() - manifest.record_file(legacy_relative, "# Legacy core taskstoissues\n") - manifest.save() - if customized: - legacy.write_text("# User-customized taskstoissues\n", encoding="utf-8") - user_file = command_root / "user-notes.md" - user_file.write_text("Keep my notes\n", encoding="utf-8") - - if github_installed: - result = _run_in_project(project, ["extension", "add", "github"]) - assert result.exit_code == 0, result.output - assert canonical.is_file() - - upgrade = ["integration", "upgrade", "generic"] - if customized: - manifest_before = manifest.manifest_path.read_bytes() - result = _run_in_project(project, upgrade) - assert result.exit_code != 0, result.output - assert "--force" in result.output - assert legacy.read_text(encoding="utf-8") == "# User-customized taskstoissues\n" - assert manifest.manifest_path.read_bytes() == manifest_before - upgrade.append("--force") - - result = _run_in_project(project, upgrade) - assert result.exit_code == 0, result.output - assert not legacy.exists() - assert legacy_relative not in IntegrationManifest.load("generic", project).files - assert user_file.read_text(encoding="utf-8") == "Keep my notes\n" - assert canonical.is_file() == github_installed, result.output - assert ExtensionManager(project).registry.is_installed("github") == github_installed - - if not github_installed: - result = _run_in_project(project, ["extension", "add", "github"]) - assert result.exit_code == 0, result.output - assert canonical.is_file() - assert not legacy.exists() - assert legacy_relative not in IntegrationManifest.load("generic", project).files - assert user_file.read_text(encoding="utf-8") == "Keep my notes\n" - @pytest.mark.parametrize( "bad_handler", [