-
Notifications
You must be signed in to change notification settings - Fork 6.9k
Description
Summary
Extract all Git-related operations (repository detection, branch creation, branch numbering, branch validation, remote fetching, SPECIFY_FEATURE env var management) from core into a self-contained Spec Kit extension at extensions/git/. The extension auto-enables by default during the migration period (opt-out) to preserve backward compatibility. Before the 1.0.0 release, the extension will transition to opt-in — users will explicitly install it via specify extension add git or a preset that includes it.
Scope note: The
--no-gitflag onspecify initis out of scope for this issue and will be handled separately.
Problem
Git branching logic is deeply interwoven across ~815+ lines in 10+ files spanning 4 languages (Python, Bash, PowerShell, Markdown). Analysis of upstream issues reveals this coupling causes problems across five distinct categories — not just maintenance burden, but workflow lock-in, blocked extensibility, spec iteration friction, and contributor barriers:
The scope of the coupling
| File | Language | Git Lines | Operations |
|---|---|---|---|
src/specify_cli/__init__.py |
Python | ~80 | is_git_repo() (L634), init_git_repo() (L654), --branch-numbering validation (L1826), git init orchestration (L2100-2135), branch_numbering in init-options (L2146) |
scripts/bash/create-new-feature.sh |
Bash | ~130 | git branch -a, git fetch --all --prune, git checkout -b, git branch --list, branch number extraction, name generation, 244-byte validation |
scripts/bash/common.sh |
Bash | ~95 | get_repo_root(), get_current_branch(), has_git(), check_feature_branch(), find_feature_dir_by_prefix(), SPECIFY_FEATURE env var handling |
scripts/powershell/create-new-feature.ps1 |
PowerShell | ~120 | PowerShell equivalents of all bash git operations |
scripts/powershell/common.ps1 |
PowerShell | ~75 | PowerShell equivalents of all common.sh git operations |
templates/commands/specify.md |
Markdown | ~35 | Branch creation instructions, init-options.json reading for branch_numbering, script invocation with --timestamp/--short-name |
templates/commands/implement.md |
Markdown | 1 | git rev-parse --git-dir for .gitignore creation |
templates/commands/taskstoissues.md |
Markdown | 5 | git config --get remote.origin.url for GitHub issue creation |
tests/test_branch_numbering.py |
Python | 69 | 6 tests for branch numbering persistence and validation |
tests/test_timestamp_branches.py |
Python | 280 | 15 tests covering timestamp/sequential branching end-to-end |
Impact of this coupling — assessed against filed issues:
-
Workflow lock-in — users who don't want branching can't adopt spec-kit. Issue Feature Request: Support disabling automatic branch creation via parameter #841 (25 reactions) and [Feature]: Make branch creation configurable #1921 both request the ability to disable automatic branch creation. Teams using trunk-based development, monorepo strategies, or simply preferring manual branch management are currently forced into spec-kit's opinionated branching model with no opt-out. These aren't edge cases — Feature Request: Support disabling automatic branch creation via parameter #841 is the 4th most-reacted open issue.
-
Blocked extensibility — users who want different branching need core surgery. Issue [Feature request] Custom namespacing and branch naming conventions #1382 (18 reactions) wants domain-specific prefixes like
USR-001-profile-update. Issue [Feature]: Allow agent-namespaced branches (claude/**,copilot/**, etc.) to bypass numeric prefix requirement #1901 wants agent-namespaced branches (copilot/**,claude/**). PR feat: Add Git worktree-aware workflows #1547 (worktree-aware workflows) required changes to 6+ files across the core. None of these can be implemented without modifying core scripts and Python code — there is no extension point for branching variation. -
Spec iteration friction — the Add John Lam as contributor and release badge #1 most-reacted issue is partially caused by mandatory branching. Issue Spec-Driven Editing Flow: Can't Easily Update or Refine Existing Specs #1191 (107 reactions) reports that users can't easily update or refine existing specs because the workflow is optimized for net-new branch creation. When every spec requires a new branch, iterating on feedback, refining requirements, or making small changes becomes disproportionately expensive. Decoupling branching from spec creation directly unblocks lighter-weight iteration patterns.
-
Maintenance burden and persistent bugs — every branching enhancement touches 4+ files across 3 languages. PR feat: add timestamp-based branch naming option for
specify init#1911 (timestamp branching) needed changes to Python, Bash, PowerShell, and Markdown templates simultaneously. PR feat: Add Git worktree-aware workflows #1547 (worktree-aware workflows) modified 6+ files. The cross-platform coordination requirement causes bugs to linger and recur:- Bug bug: automatic branch naming repeats numbering prefix such as 001-second-branch after 001-first-branch #1066 (branch numbering repeats
001-, 13 reactions, open since Oct 2025) — the root cause was diagnosed separately in closed Potential bug with the create-new-feature.ps1 script that ignores existing feature branches when determining next feature number #975 (PowerShell only checkedspecs/, not branch names), fixed in closed PR fix: check all git branches for feature numbering to prevent duplicates #1023, then rediscovered as a prompt-level issue and fixed again in closed PR fix: use global branch numbering instead of per-short-name detection #1757 ("use global branch numbering instead of per-short-name detection"). The same bug required three separate fixes. - Closed PR fix(scripts): suppress git fetch stdout leak in multi-remote environments #1876 fixed
git fetchstdout leaking into branch number arithmetic in multi-remote environments — a bug only possible because git subprocess output parsing is scattered across scripts. - Closed bug [Bug]: specify command throws error saying branch already exists, when creating a new branch in antigravity and powershell combo #1791 reported
create-new-feature.ps1crashing becausegit checkout -berror handling behaved differently in PowerShell's$ErrorActionPreference='Stop'context. - Closed issue Specs Directory Created at Git Root Instead of Project Root #1151 reported specs directories being created at the git root instead of the project root because the script prioritized
git rev-parse --show-toplevelover.specifymarker location. - Open issue Different commands produce/expect different branch name syntax #1165 reported
check_feature_branch()rejecting branches named1-url-migration(single digit) because validation only accepted the001-three-digit pattern — a rigid assumption baked into corecommon.sh.
Bash and PowerShell maintain ~225 and ~195 lines of parallel git implementations that must be kept in lockstep — ripe for cross-platform drift.
- Bug bug: automatic branch naming repeats numbering prefix such as 001-second-branch after 001-first-branch #1066 (branch numbering repeats
-
Contributor barrier — PRs are large and complex because branching touches everything. PR Customizable Branch Naming Templates #1511 (customizable branch naming templates, 8 reactions) modifies
__init__.py, bothcommon.sh/.ps1, bothcreate-new-featurescripts, and adds a new TOML config — a large surface area for what is conceptually a naming convention change. PR feat: Add git workflow configuration to specify init #1483 (git workflow configuration) remains in draft. Closed PR feat(shell): add fish support tocreate-new-feature#1361 attempted to add fish shell support tocreate-new-feature— illustrating that N shell implementations means N×maintenance cost for every change. The friction discourages community contributions to the branching subsystem.
Proposed Solution
Create extensions/git/ following the established extension manifest schema (extension.yml, commands/, hooks):
extensions/git/
extension.yml
commands/
speckit.git.feature.md # Branch creation (wraps create-new-feature.sh/.ps1)
speckit.git.validate.md # Branch validation
speckit.git.remote.md # Remote URL detection (for taskstoissues)
scripts/
bash/
create-new-feature.sh # Moved from scripts/bash/
git-common.sh # Git-specific subset of common.sh
powershell/
create-new-feature.ps1 # Moved from scripts/powershell/
git-common.ps1 # Git-specific subset of common.ps1
Extension manifest:
schema_version: "1.0"
extension:
id: git
name: "Git Branching Workflow"
version: "1.0.0"
description: "Feature branch creation, numbering (sequential/timestamp), validation, and Git remote detection"
author: spec-kit-core
repository: https://github.com/github/spec-kit
license: MIT
requires:
speckit_version: ">=0.2.0"
tools:
- name: git
required: false # Extension degrades gracefully when git unavailable
provides:
commands:
- name: speckit.git.feature
file: commands/speckit.git.feature.md
description: "Create a feature branch with sequential or timestamp numbering"
- name: speckit.git.validate
file: commands/speckit.git.validate.md
description: "Validate current branch follows feature branch naming conventions"
- name: speckit.git.remote
file: commands/speckit.git.remote.md
description: "Detect Git remote URL for GitHub integration"
hooks:
before_specify:
command: speckit.git.feature
optional: false
description: "Create feature branch before specification"
after_implement:
command: speckit.git.validate
optional: true
prompt: "Verify feature branch naming?"
description: "Validate branch naming after implementation"
config:
defaults:
branch_numbering: sequential # sequential | timestamp
branch_template: "{number}-{short_name}"
auto_fetch: trueNote: The extension system supports
config.defaultsin manifests but does not currently supportconfig_schemavalidation. Configuration is stored in.specify/extensions/git/git-config.ymlwith layered precedence (extension defaults → project config → local overrides → env vars).
What Moves Out of Core
| From Core | To Extension | Details |
|---|---|---|
is_git_repo() in __init__.py |
scripts/bash/git-common.sh has_git() |
Already exists in scripts; remove Python duplicate |
init_git_repo() in __init__.py |
Remains in core (out of scope per --no-git exclusion) |
Git init during specify init stays in core for now |
--branch-numbering in __init__.py |
extension.yml defaults.branch_numbering + config |
Stored in .specify/extensions/git/git-config.yml |
create-new-feature.sh |
extensions/git/scripts/bash/create-new-feature.sh |
Unchanged logic, new home |
create-new-feature.ps1 |
extensions/git/scripts/powershell/create-new-feature.ps1 |
Unchanged logic, new home |
get_current_branch() in common.sh |
Split: git-specific logic to git-common.sh, env-var/dir-scan stays |
SPECIFY_FEATURE check + specs/ fallback remain in core |
check_feature_branch() in common.sh |
extensions/git/scripts/bash/git-common.sh |
Branch naming validation is git-specific |
get_highest_from_branches() |
extensions/git/scripts/bash/create-new-feature.sh |
Already there; stays |
Branch creation section of specify.md |
commands/speckit.git.feature.md |
Hook-based: before_specify triggers branch creation |
git rev-parse --git-dir in implement.md |
commands/speckit.git.validate.md hook |
.gitignore check triggered by hook |
git config remote.origin.url in taskstoissues.md |
commands/speckit.git.remote.md |
Remote detection as extension command |
test_branch_numbering.py |
tests/test_git_extension.py or extensions/git/tests/ |
Branch numbering tests move with the feature |
test_timestamp_branches.py |
tests/test_git_extension.py or extensions/git/tests/ |
All 15 branching tests move with the feature |
What Stays in Core
| Component | Reason |
|---|---|
--no-git flag |
Out of scope; handled separately |
init_git_repo() |
Part of specify init lifecycle, not feature branching |
is_git_repo() (during init) |
Guards init_git_repo(); stays until --no-git refactor |
SPECIFY_FEATURE env var check |
Core feature detection, not git-specific |
specs/ directory scanning fallback |
Non-git feature detection in common.sh |
find_feature_dir_by_prefix() |
Spec directory resolution (works without git) |
Auto-Enable / Opt-Out Behavior
Migration period (pre-1.0.0): The extension auto-installs during specify init (opt-out). Users who don't want branching disable it:
# Disable the git extension
specify extension disable git
# Re-enable it
specify extension enable gitAt 1.0.0: The extension becomes opt-in. It will no longer auto-install during specify init. Users who want branching explicitly add it:
# Opt-in to git branching
specify extension add git
# Or via a preset that includes it
specify init my-project --preset sdd-fullWhen the extension is absent or disabled, specify.md creates spec directories but skips branch creation — equivalent to the existing non-git fallback behavior.
Related Issues & PRs
| # | Title | Status | Relevance | Signal |
|---|---|---|---|---|
| #841 | Support disabling automatic branch creation via parameter | open | Direct: branching should be optional | 25 👍 |
| #1382 | Custom namespacing and branch naming conventions | open | Unblocked: extension config enables USR-001-* patterns |
18 👍 |
| #1066 | Branch numbering repeats 001- prefix |
open | Fix moves to extension, single-file change | 13 👍 |
| #1921 | Make branch creation configurable (branchStrategy) |
open | Resolved by extension config | 1 👍 |
| #1901 | Allow agent-namespaced branches (copilot/**) |
open | Extension can add agent prefix allowlist | — |
| #1165 | Different commands produce/expect different branch name syntax | open | Rigid branch validation in core common.sh |
— |
| #1151 | Specs directory created at git root instead of project root | open | Git root vs project root confusion in scripts | — |
| #1191 | Can't easily update or refine existing specs | open | Branch coupling makes spec iteration painful | 107 👍 |
| #1511 | PR: Customizable branch naming templates | open | Simplifies: template logic lives in one extension | 8 👍 |
| #1483 | PR: Add git workflow configuration to specify init | open/draft | Extension approach absorbs this cleanly | — |
| #1911 | PR: Timestamp-based branch naming | merged | Would have been extension-only change | 2 🚀 |
| #1757 | PR: Use global branch numbering instead of per-short-name | merged | Fixed #1066 root cause; illustrates template→script coupling | — |
| #1876 | PR: Suppress git fetch stdout leak in multi-remote envs | merged | Cross-platform git subprocess fragility | — |
| #1547 | PR: Git worktree-aware workflows | closed | Cleaner as extension with git-mode config |
2 👍 |
| #1361 | PR: Add fish shell support to create-new-feature | closed | N shells × N scripts = N² maintenance | — |
| #1023 | PR: Check all git branches for feature numbering | closed | PowerShell-only fix for #975; bash had same bug | — |
| #975 | Bug: create-new-feature.ps1 ignores existing feature branches | closed | PowerShell/bash parity bug | — |
| #1791 | Bug: specify throws "branch already exists" on PowerShell | closed | git checkout -b error handling differs across shells |
1 👍 |
| #1924 | Agent Catalog — self-bootstrapping agent packs | open | Related architectural decoupling effort | 1 👁️ |
| #1707 | Multi-catalog extension system | closed | Extension infrastructure this depends on | 3 👍 |
| #1049 | Automated testing for Bash and PowerShell scripts | open | Extension boundary simplifies test scope | 1 👍 |
Migration Strategy
-
Phase 1 — Create
extensions/git/with current logic (no core changes): Build the extension with existing scripts and commands. Validate it works viaspecify extension add git --from extensions/git/. -
Phase 2 — Wire auto-enable into
specify init: Add extension auto-install logic toinit(). Today,specify initdoes not auto-install extensions (only presets can install extensions via--preset). This phase requires new core code ininit()to callExtensionManager.install_from_directory()for the bundled git extension. Consider adding anauto_install: truemanifest field to distinguish bundled extensions from user-installed ones. -
Phase 3 — Remove git operations from core: Delete
is_git_repo()/ branch-numbering validation from__init__.py. Remove--branch-numberingCLI flag frominit()(configuration moves to.specify/extensions/git/git-config.yml). Remove branch creation section fromspecify.mdand rewrite it to conditionally include branching instructions based on whether the git extension is enabled (check.specify/extensions/.registry). Keepget_current_branch()intact as a single function incommon.sh/.ps1— only movecheck_feature_branch()validation to the extension. Move test files. -
Phase 4 — Verify non-git mode: Ensure
specify extension disable gitproduces identical behavior to currentHAS_GIT=falsein scripts (spec directories created, no branches).--no-gitoninitadditionally skipsinit_git_repo()and is handled separately.
Implementation Notes
The following design constraints were identified during feasibility analysis of the extension system:
-
No auto-install mechanism exists today.
specify initdoes not auto-install extensions — only presets do (via--preset). Phase 2 requires adding new code toinit()to callExtensionManager.install_from_directory()for the bundled git extension. -
Branching is AI-agent-mediated, not core-executed. The Python CLI never calls
create-new-feature.shdirectly. Today,specify.mdinstructs the AI agent to call the script; the agent chooses to follow this instruction. The extension hook model (before_specify) is equivalent — template instructions asking the AI to invoke the extension command. There is no change in execution guarantees. -
{SCRIPT}placeholder resolution is Codex-only. Extension commands cannot rely on{SCRIPT}body replacement (only_resolve_codex_skill_placeholders()does this). Extension command templates must include explicit script paths (e.g.,.specify/extensions/git/scripts/bash/create-new-feature.sh "{ARGS}"). -
get_current_branch()must remain a single function. Every downstream command (plan.md,tasks.md,implement.md,check-prerequisites.sh) callsget_current_branch(). Onlycheck_feature_branch()validation moves to the extension; the branch detection function stays whole in corecommon.sh/.ps1. -
--branch-numberingCLI flag ownership transfers. Phase 3 must remove--branch-numberingfromspecify initand document that users configure numbering via.specify/extensions/git/git-config.yml. -
specify.mdtemplate must be rewritten, not just stripped. Phase 3 must updatespecify.mdto check the extension registry (.specify/extensions/.registry) and conditionally include branching instructions when the git extension is enabled. -
Auto-enable is transitional, not permanent. All current extensions are opt-in. The git extension auto-enables during the migration period to preserve backward compatibility. Before 1.0.0, the auto-install code in
init()is removed and the extension becomes opt-in like all others. Noauto_install: truemanifest field is needed — this is a time-bounded migration strategy, not a permanent extension category. -
--no-gitvs extension disable boundary. When the git extension is disabled, behavior should be identical toHAS_GIT=falsein current scripts (spec directories created, no branches).--no-gitoninitadditionally skipsinit_git_repo()and is handled separately.
Acceptance Criteria
-
extensions/git/extension.ymlmanifest with commands, hooks, andconfig.defaults -
speckit.git.featurecommand creates branches with sequential or timestamp numbering -
speckit.git.validatecommand validates feature branch naming conventions -
speckit.git.remotecommand detects Git remote URL for GitHub integration -
before_specifyhook registered;specify.mdtemplate instructs AI agents to invoke it (consistent with existing AI-agent-mediated execution model) - Extension commands include explicit script paths (no reliance on
{SCRIPT}placeholder) - Extension reads
branch_numberingfrom its own config (.specify/extensions/git/git-config.yml) - Extension degrades gracefully when git is not installed (warnings, no errors)
-
specify extension disable gitstops all branch operations without breaking spec creation -
specify extension enable gitre-enables branch operations - Auto-install logic added to
specify initfor bundled git extension (migration period; removed before 1.0.0) -
--branch-numberingCLI flag removed frominit()in Phase 3; config moves to extension - All 21 existing branching tests pass when run against the extension
- Cross-platform: Bash and PowerShell scripts both move to extension
-
get_current_branch()remains intact in corecommon.sh/.ps1; onlycheck_feature_branch()moves -
SPECIFY_FEATUREenvironment variable continues to work (stays in core) -
specify.mdtemplate conditionally includes branching instructions based on extension registry - No regression in
--no-gitbehavior (remains in core, out of scope)