Skip to content

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

Open
Yash-Chindam wants to merge 3 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 3 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": {
Comment thread extensions/github/README.md Outdated
| ------------------------------ | -------------------------------------------------------------------- |
| `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:
@mnriem mnriem added the author-awaiting Waiting on author response label Sep 9, 2026
@mnriem

mnriem commented Sep 9, 2026

Copy link
Copy Markdown
Collaborator

Thanks — the shape is exactly right (github namespace, vendored scripts, plain frontmatter). The re-review found real bugs in the vendored scripts to fix before merge: (1) resolve-tasks.sh json_escape doesn't escape backslashes and collapses newlines/tabs to letters instead of JSON escapes; (2) resolve_tasks.py emits U+2713 which raises UnicodeEncodeError under Windows cp1252 — use an ASCII marker or force UTF-8; (3) catalog.json top-level updated_at is stale; (4) the README invocation guide omits Command Code (also $speckit- skills syntax). Re-request once addressed.

… scripts

Four review findings on github#4488, all confirmed against the code before
changing it.

resolve-tasks.sh json_escape had lost one level of backslash quoting
when the file was authored, so the escape table read `${s//\/\}` rather
than core's `${s//\/\\}`. Backslashes passed through unescaped and
\n/\t/\r collapsed to the bare letters n/t/r. A Windows feature path
such as C:\Users\dev\specs emitted JSON the agent cannot parse
("Invalid \escape"). Replaced with core's implementation from
scripts/bash/common.sh verbatim, which also adds \b, \f and the
\uXXXX control-character pass, and is now asserted equal to core's
output case by case.

resolve_tasks.py printed U+2713 unconditionally. On Windows stdout falls
back to the ANSI code page whenever it is not a console -- a pipe or a
redirect, which is how agents invoke these scripts -- so the report
died with UnicodeEncodeError right after "AVAILABLE_DOCS:". Adopted
core's _status_marker idiom: emit the glyph when stdout can encode it,
fall back to "[OK]", which is what the PowerShell twin already prints.

catalog.json updated_at was left at the assess extension's date; bumped
to this change's date, matching what the bug and assess additions did.

The README invocation guide named only Codex and ZCode for the
`$speckit-` syntax, omitting Command Code. A test now derives the
expected set from DOLLAR_SKILLS_AGENTS so a newly added agent fails
until the guide is updated.

Regression tests added for each: the json_escape cases round-trip
through a JSON parser and are compared against core's output, an
end-to-end run covers a feature directory containing a backslash, and
the text-mode tests pin both the cp1252 fallback and the UTF-8 glyph.
All were confirmed to fail against the pre-fix scripts.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@Yash-Chindam

Copy link
Copy Markdown
Contributor Author

All four confirmed and fixed in 64915df. I reproduced each against the code before changing anything, and every fix has a regression test that I verified fails against the pre-fix scripts.

1. json_escape — you're right, and it's worse than a style issue.

The escape table had lost one level of backslash quoting when I authored the file. What shipped was:

s="${s//\/\\}"          # pattern is an escaped nothing; replacement collapses to \
s="${s//$'\n'/\n}"      # replacement \n means literal "n"

against core's:

s="${s//\\/\\\\}"
s="${s//$'\n'/\\n}"

So backslashes passed through unescaped and \n/\t/\r became the bare letters. The user-visible failure is a Windows feature path:

C:\Users\dev\specs  ->  "C:\Users\dev\specs"   json.JSONDecodeError: Invalid \escape

The agent gets unparseable JSON from step 1 of the command. I'd missed it because every fixture I tested used an MSYS forward-slash path, so no input ever contained a backslash.

Replaced with core's implementation from scripts/bash/common.sh verbatim — which also handles \b, \f and the \uXXXX control-character pass I had omitted entirely. The tests now assert each case round-trips through a JSON parser and is byte-identical to core's output, so the two can't drift.

Worth noting for the record: I had "verified" this function earlier by eyeballing a Python repr() of the source line and reading '\\' as two backslashes when it means one. That's what let a real bug through a check that looked like it passed.

2. U+2713 under cp1252 — confirmed, and core had already solved it.

print(f" \u2713 {doc}") dies with UnicodeEncodeError whenever stdout isn't a console, which is exactly how agents invoke these scripts:

$ PYTHONIOENCODING=cp1252 python resolve_tasks.py | cat
AVAILABLE_DOCS:
UnicodeEncodeError: 'charmap' codec can't encode character '\u2713'

The report dies right after AVAILABLE_DOCS:. I adopted core's _status_marker idiom from scripts/python/check_prerequisites.py — emit the glyph when stdout can encode it, fall back to [OK] otherwise. That also aligns the Python twin with the PowerShell one, which was already printing [OK]. Both branches are pinned by tests.

3. updated_at — bumped to 2026-09-10T00:00:00Z. I checked the history: the bug and assess additions each bumped it to their own date, so this matches the convention rather than guessing.

4. Command Code — right, DOLLAR_SKILLS_AGENTS is {codex, zcode, command-code} and the guide named only two. Fixed. Rather than just adding the string, the test now derives the expected set from DOLLAR_SKILLS_AGENTS, so if a fourth dollar-skills agent is added the test fails until the guide is updated.

One adjacent thing I did not touch, to avoid scope creep: extensions/agent-context/README.md:43 has the identical omission — my note was copied from it. Happy to fix it here if you'd prefer, or leave it for a separate change.


Verification. 92 tests in tests/extensions/github/. Extension suites (tests/extensions, test_ps1_encoding, test_extension_registration, test_extension_skills, test_extensions) → 806 passed, 237 skipped, plus the two test_scaffold_config_rejects_symlink* failures that also fail on main here (they need a symlink privilege this box doesn't grant).

One caveat I want to be explicit about rather than let CI discover: the 17 bash-gated tests skip on my machine and I could not execute them under pytest. Bare bash resolves to the WSL launcher through a Windows App Execution Alias that wins over PATH, so requires_bash correctly skips them. I verified those assertions by running the identical logic against Git Bash directly — all json_escape cases round-trip and match core, and the end-to-end backslash-path run produces parseable JSON — but their first run as pytest tests will be in CI.

Re-requesting review.

Disclosure: I used an AI assistant (Claude Code, model Claude Opus 5) to investigate and fix these and to draft this reply. Each finding was reproduced against the pre-fix code and each fix re-verified, including confirming the new tests fail without the fixes.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

author-awaiting Waiting on author response 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