Skip to content

feat(extensions): add bundled github extension for taskstoissues - #4488

Open
Yash-Chindam wants to merge 2 commits into
github:mainfrom
Yash-Chindam:feat/4421-github-extension
Open

feat(extensions): add bundled github extension for taskstoissues#4488
Yash-Chindam wants to merge 2 commits into
github:mainfrom
Yash-Chindam:feat/4421-github-extension

Conversation

@Yash-Chindam

@Yash-Chindam Yash-Chindam commented Sep 9, 2026

Copy link
Copy Markdown
Contributor

Description

Closes #4421 — stage 1 of the three-stage migration (stage 2 deprecates the core command, stage 3 removes it).

Adds a bundled, opt-in github extension providing speckit.github.taskstoissues. The core speckit.taskstoissues command is untouched and nothing here claims its name, so the two coexist for the whole of stage 1.

specify extension add github
/speckit.github.taskstoissues

Shape

The issue proposed a github-issues extension. Per @mnriem's scoping comment this lands as github instead — the home for GitHub-platform functionality, with git staying the local-VCS-workflow domain. Milestone grouping (#4370) gets a natural home in the same extension rather than spawning a second narrow one.

The extension owns its feature-resolution script rather than reaching back into core check-prerequisites through ../../scripts/. That matters for more than tidiness: an extension that escapes its own directory re-couples it to core internals, which is the exact thing stage 3 removes. Because the extension now ships resolve-tasks, the plain scripts: spelling is the correct one — it renders to .specify/extensions/github/scripts/…, a path that exists — and the escape hatch disappears instead of being tested around.

resolve-tasks is a trimmed twin of core check-prerequisites, not a copy. It resolves the project root (honouring SPECIFY_INIT_DIR) and the active feature directory (SPECIFY_FEATURE_DIRECTORY, else .specify/feature.json), requires tasks.md, and reports the design docs beside it. It performs none of core's plan.md/spec.md gating, and it never persists feature.json — resolution here is read-only, so running the command cannot dirty the working tree.

Why the script path deserved a test

A verbatim copy of the core command's frontmatter renders .specify/extensions/github/scripts/bash/check-prerequisites.sh — a file no extension ships. That install validates, registers the command, auto-registers the skill, and prints ✓ Extension installed successfully!. The failure only surfaces when a user runs the command. TestScriptPathResolution installs the extension, renders it, and asserts the resolved path is a file that exists on disk — in command mode, in skills mode, and for all three script runtimes.

Acceptance criteria

Criterion Evidence
Bundled extension installs via specify extension add … catalog.json (bundled: true) + pyproject.toml force-include; TestCatalogEntry, TestExtensionInstall. Installed as github, not github-issues, per the scoping comment.
Provides the command for every supported integration test_every_supported_integration_renders_the_command parametrises over all 40 entries in AGENT_CONFIGS and asserts each renders with {SCRIPT} resolved to the extension-local path. Covers command-file agents, skills-mode agents, TOML/YAML formats, and Hermes (which installs to ~/.hermes/skills, so the test redirects HOME).
Preserves issue creation, remote validation, pagination, deduplication test_body_differs_from_core_only_in_the_script_invocation diffs the command against core and requires the only changed lines to be the script invocation plus the two that read the new TASKS value. test_preserves_remote_validation and test_preserves_deduplication_and_pagination additionally pin the GitHub-URL guard, the both-states listing, cursor paging (perPage, after, endCursor, the early exit), and the four-digit-safe \bT\d{3,}\b pattern.
before_taskstoissues / after_taskstoissues hooks execute as before The keys are literal strings the command body reads out of .specify/extensions.yml, so copying the body preserves the contract. TestCommandBody pins both keys and asserts the live consumers — the git extension's auto-commit hooks — still target them.
Core speckit.taskstoissues remains available and unchanged git diff main -- templates/ scripts/ src/ is empty. Verified end to end: after installing the extension, the core command and its check-prerequisites invocation both still resolve and run.
No legacy alias conflicting with the core command test_no_alias_claims_the_core_command. Worth noting this is author discipline, not something the system enforces — see "Split out" below.
Tests cover installation, invocation artifacts, hooks, uninstall 74 tests in tests/extensions/github/.
Documentation explains installation, usage, migration extensions/github/README.md (install, removal/disable, command table with per-integration invocation syntax, behaviour, hooks, requirements, scripts, and a migration section with the three stages and a before/after table), plus pointers from README.md and docs/installation.md.
Included in a minor release Maintainer's call — nothing in the diff pins a version.

Answering the assessment's blocking questions

The stage-5 decision returned needs-clarification. Four of its six blocking questions are settled by the scoping comment and this implementation:

  • Is the extension responsible for correcting the helper script path, and what exact rendered path must each runtime use? Yes — and by owning the script rather than by correcting a path into core. All three resolve under .specify/extensions/github/scripts/{bash,powershell,python}/, asserted per-runtime.
  • Is scripts: frontmatter stripping part of this request? No — split out, see below.
  • Which integrations and layouts are required for parity? All 40, both layouts, now covered by a parametrised test rather than by sampling.
  • Is "no legacy alias" a local constraint or a repo-wide guarantee? Local to this extension here; the repo-wide guard is a separate concern, see below.

The remaining two — usage/adoption baselines for gating stage 2, and which release carries stage 1 — are product decisions rather than implementation ones, and nothing here forecloses either.

Split out, as requested

Two findings from the scoping investigation are defects in shared paths rather than in this extension, and are deliberately not touched here so stage 1 stays additive:

  1. Extension rendering does not strip scripts: from the agent-facing frontmatter, while the core render does (step 3 of process_command_template, integrations/base.py). Visible in this PR's own output: the generated .github/agents/speckit.github.taskstoissues.agent.md retains the key, where core's does not. No bundled extension declared scripts: before this one, so nothing had exercised it.
  2. _validate_install_conflicts overstates what it checks. It is documented as rejecting installs that "would shadow core or installed extension commands", but _get_installed_command_name_map only walks self.registry — installed extensions. Core command names are never in that map, so an extension declaring an alias of speckit.taskstoissues is accepted today, with the core command present.

Both are ready to file with the reproductions.

Files

Path
extensions/github/extension.yml Manifest — id: github, one namespaced command, and the requires.speckit_version block (absent from the issue body's proposed manifest, which install rejects)
extensions/github/commands/speckit.github.taskstoissues.md Command; body differs from core only in the script invocation
extensions/github/scripts/{bash,powershell,python}/resolve-tasks.* Vendored feature/tasks resolver, three runtimes
extensions/github/README.md Install, usage, hooks, requirements, migration
extensions/catalog.json, pyproject.toml Bundled registration and wheel packaging
README.md, docs/installation.md Point at the extension and the migration
tests/extensions/github/ 74 tests
tests/test_ps1_encoding.py New .ps1 directory added to the ASCII-only (PowerShell 5.1) guard

Testing

  • Tested locally with uv run specify --help
  • Ran existing tests with uv sync && uv run pytest
  • Tested with a sample project (if applicable)

Full suite, this branch vs. a main worktree on the same machine:

passed skipped failed errors
main (0c8e31f) 7055 566 68 38
this branch 7086 568 68 38

Identical failure and error counts; the delta is exactly the tests added. Those 68/38 are pre-existing on this Windows box, not regressions — they sit in test_setup_tasks.py, test_check_prerequisites_python_parity.py, test_setup_plan_no_overwrite.py and test_setup_plan_feature_json.py, which exercise core scripts this PR does not touch. The box has no pwsh, and bare bash resolves to the WSL launcher rather than Git Bash, so those PowerShell and parity tests error the same way in both trees; running those four files against the baseline worktree directly gives byte-identical results (5 failed / 8 passed / 40 skipped / 27 errors).

That table predates the final commit, which adds 42 tests. Extension-relevant suites re-run after it — tests/extensions, tests/test_ps1_encoding.py, tests/test_extension_registration.py, tests/test_extension_skills.py, tests/test_extensions.py803 passed, 222 skipped, plus the same two symlink-privilege failures that fail identically on main (test_scaffold_config_rejects_symlink_template, test_scaffold_config_rejects_symlinked_config_root).

The two bash-twin tests skip here for the WSL reason above; I ran both scenarios by hand against Git Bash and confirmed output and exit codes match the assertions.

End-to-end, against scratch projects scaffolded from this branch:

  • Copilot, commands modespecify extension add github installs cleanly; .github/agents/speckit.github.taskstoissues.agent.md renders {SCRIPT} to .specify/extensions/github/scripts/bash/resolve-tasks.sh --json; that file exists and returns the expected FEATURE_DIR / TASKS / AVAILABLE_DOCS. Core speckit.taskstoissues.agent.md and .specify/scripts/bash/check-prerequisites.sh are still present and still work.
  • Copilot, skills mode (default).github/skills/speckit-github-taskstoissues/SKILL.md resolves the same path. specify extension remove github removes the skill and the extension directory, leaving the core skill intact.
  • All three script twins were run against the same fixture and agree on FEATURE_DIR, TASKS, and AVAILABLE_DOCS, including the single-element-array case and the missing-tasks.md error path (exit 1, same message). The PowerShell twin is ASCII-only for PowerShell 5.1 and reads feature.json as UTF-8 explicitly (fix: decode feature.json as UTF-8 in Windows PowerShell #4359).

Manual test results

Agent: GitHub Copilot (scaffolded via specify init) | OS/Shell: Windows 11 / Git Bash + PowerShell 5.1

Command tested Notes
specify extension add github Installs in both commands and skills mode; command/skill registered; scripts land under .specify/extensions/github/scripts/.
specify extension remove github Removes command/skill and extension directory; core taskstoissues untouched.
/speckit.github.taskstoissues Script step verified by running the rendered {SCRIPT} invocation directly (bash, PowerShell, Python) against a fixture feature. The issue-creation steps need a GitHub MCP server and a GitHub remote, which I did not exercise against a live repository — that portion of the body is carried over from the core command unchanged, and the diff test above pins that it stays that way.
/speckit.taskstoissues (core) Still installed and unchanged; its check-prerequisites invocation still resolves.

AI Disclosure

  • I did not use AI assistance for this contribution
  • I did use AI assistance (describe below)

I used Claude Code (model Claude Opus 5) to help write the extension, the vendored scripts, the tests, and this description. I reviewed the changes and ran the verification described above myself.

🤖 Generated with Claude Code

…hub#4421)

Stage 1 of moving GitHub issue tracking out of core: add a bundled,
opt-in `github` extension that provides `speckit.github.taskstoissues`.
The core `speckit.taskstoissues` command is untouched and no alias
claims its name, so the two coexist while stage 2 (deprecate) and
stage 3 (remove) are still ahead.

`github` is the GitHub-platform domain, alongside `git` for local VCS
workflow, so later platform work (milestone grouping, github#4370) has a home
without spawning a second narrow extension.

The extension owns its feature-resolution script in all three runtimes
rather than reaching into core `check-prerequisites` via `../../scripts/`.
That keeps the plain `scripts:` frontmatter spelling correct — it renders
to `.specify/extensions/github/scripts/...`, which the extension actually
ships — and leaves nothing recoupled to core internals that stage 3 will
remove. `resolve-tasks` is a trimmed twin of `check-prerequisites`: it
resolves the project root and active feature, requires tasks.md, and lists
the design docs beside it, without core's plan.md/spec.md gating and
without persisting feature.json.

Tests cover the bundled layout, catalog and wheel packaging, manifest
validation, install/uninstall, the hook contract, and — the failure mode
that a verbatim copy of the core command would have shipped — that the
rendered `{SCRIPT}` path resolves to a file that exists, in both command
mode and skills mode.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@Yash-Chindam
Yash-Chindam requested a review from mnriem as a code owner September 9, 2026 04:52
The acceptance criteria ask for the command "for every supported
integration", which the previous tests only evidenced for Copilot in
both layouts. Parametrise the render assertion over all 40 entries in
AGENT_CONFIGS, redirecting HOME so the Hermes integration — which
installs to ~/.hermes/skills rather than a project-local directory —
is exercised without touching the developer's real home.

Also split the behaviour assertions so deduplication and pagination are
checked by name (cursor paging, perPage, the early exit, and the
four-digit-safe task ID pattern) rather than folded into the remote
validation test, and add a diff-based test asserting the command body
differs from core only in the script invocation and the two lines that
read the new TASKS value — so the two cannot silently drift while both
commands exist.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@mnriem
mnriem requested a balanced review from Copilot September 9, 2026 17:05
@mnriem mnriem added the triage-must-have Verdict: high-value, important work for Spec Kit — do first label Sep 9, 2026

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Changes recommended

Resolver escaping and Windows output issues can break execution, and PowerShell behavior lacks parity coverage.

Once you've addressed the issues Copilot identified, you can request another Copilot review.

Pull request overview

Adds an opt-in bundled github extension as the stage-one replacement for core task-to-issue functionality.

Changes:

  • Adds the namespaced command and three resolver runtimes.
  • Registers, packages, documents, and tests the extension.
  • Preserves the existing core command and hook contract.
File summaries
File Description
extensions/github/extension.yml Defines extension metadata and command.
extensions/github/commands/speckit.github.taskstoissues.md Implements task-to-issue workflow.
extensions/github/scripts/bash/resolve-tasks.sh Adds Bash task resolver.
extensions/github/scripts/powershell/resolve-tasks.ps1 Adds PowerShell task resolver.
extensions/github/scripts/python/resolve_tasks.py Adds Python task resolver.
extensions/github/README.md Documents usage and migration.
extensions/catalog.json Registers the bundled extension.
pyproject.toml Includes extension in wheels.
README.md Announces the migration.
docs/installation.md Updates installed-command guidance.
tests/extensions/github/__init__.py Marks the test package.
tests/extensions/github/test_github_extension.py Tests layout, installation, rendering, and behavior.
tests/test_ps1_encoding.py Extends PowerShell encoding coverage.
Review details
  • Files reviewed: 12/13 changed files
  • Comments generated: 5
  • Review effort level: Balanced

💡 Configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

Comment on lines +56 to +64
json_escape() {
local s="$1"
s="${s//\/\\}"
s="${s//\"/\\\"}"
s="${s//$'\n'/\n}"
s="${s//$'\r'/\r}"
s="${s//$'\t'/\t}"
printf '%s' "$s"
}
print(f"TASKS:{tasks}")
print("AVAILABLE_DOCS:")
for doc in docs:
print(f" \u2713 {doc}")
Comment thread extensions/catalog.json
"core"
]
},
"github": {
| ------------------------------ | -------------------------------------------------------------------- |
| `speckit.github.taskstoissues` | Convert tasks from `tasks.md` into dependency-ordered GitHub issues. |

> 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 hyphen/skills integrations (including Forge and Cline); `$speckit-github-taskstoissues` for Codex or ZCode in skills mode; or `/skill:speckit-github-taskstoissues` for Kimi.
Comment on lines +502 to +503
@requires_bash
class TestResolveTasksBash:
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

triage-must-have Verdict: high-value, important work for Spec Kit — do first

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Feature]: Add a bundled github extension for taskstoissues

3 participants