T-I2ML-squash-legacy-feat-branch-references
Status: closed/done · Impact: medium · Complexity: small
The feat/<basename> branch namespace was renamed to task/<basename> (per
[T-UNUH-codify-task-branch-naming](/planning/tasks/codify-task-branch-naming/)) with a transition window in which readers
(count_inflight_tasks, task-close-out, reconcile, project_cleanup, the resume detector in task-work,
etc.) accepted either prefix. The transition window can now close: as of 2026-05-28, zero feat/*
branches exist locally or on origin, zero worktrees check out a feat/* branch, and zero open PRs
target a feat/* head. The only remaining references are historical merged PRs (#122, #123, #125)
and the now-dead-weight transition-window prose, regex matches, and tests scattered across the
plugin.
Remove all of them in one sweep. Future readers see a single canonical prefix (task/); future
contributors aren’t tempted to re-introduce the legacy form “for compatibility.”
| Location | Role today |
|---|---|
plugin/conventions/branch-naming.md | The canonical doc. Carries a “Migrating from feat/” section explaining the rename and a “Whether the feat/ fallback in readers stays forever (cheap, no harm) or gets removed once no in-flight feat/ branches remain” line. The whole section becomes historical with this task. |
plugin/scripts/count_inflight_tasks.py | Walks task/* AND feat/* branches when listing in-flight worktrees; emits a stderr deprecation warning for any feat/* match. Module docstring documents both prefixes. |
plugin/scripts/test_count_inflight_tasks.py | Three test cases specifically exercise the feat/-prefix code paths: case_feat_only_emits_warning, case_mixed_counts_both, plus the dual-prefix module docstring assertions. |
plugin/scripts/project_cleanup.py | Includes "feat/" in the branch-prefix lists for local-branch enumeration and stale-branch cleanup. Also explicitly builds feat/<basename> candidates when searching for a task’s branch. |
plugin/scripts/worktree_scope_guard.py | Module docstring references task/<basename> (or legacy feat/<basename> during the transition window) as the supported prefix. |
plugin/lib/lease/reconcile.py | _local_task_branches returns both task/<id> and feat/<id> short names; the orphan-branch reconciler treats them as equivalent. |
plugin/lib/lease/runtime.py | Comments reference legacy feat/<task_id> in two places (the discover_lease fallback paragraph and __init__.py’s module docstring via re-export). |
plugin/lib/lease/tests/test_migration.py | Has an explicit test fixture that creates a feat/has-feat-branch branch and asserts the migration’s discovery sequence includes the legacy-prefix follow-up. |
plugin/lib/lease/tests/test_runtime.py | Documents and tests “Legacy feat/ prefix is not lease-eligible” as an explicit case. |
plugin/conventions/lease-aware-skills.md | Two paragraphs mention legacy feat/<task_id> branches in the discover_lease fallback explanation. |
plugin/conventions/worktree-scope-guard.md | One reference in the canonical-form paragraph. |
plugin/conventions/sdlc-yaml.md | One reference inside the max_awaiting_review description. |
plugin/schemas/sdlc-yaml.schema.json | One JSON-string reference inside the max_awaiting_review schema field description (duplicates the convention doc). |
Proposed
Section titled “Proposed”Every reference to feat/<basename> as a current-or-legacy task branch prefix is removed. The
codebase reads as if task/<basename> were the only convention there ever was. The “Migrating from
feat/” section in branch-naming.md is deleted; the open question about when the fallback comes
out is resolved by this commit landing.
Tests that specifically exercised feat/-prefix code paths are deleted (not “skipped” or “marked
legacy”) — the code paths they covered are gone. Tests that incidentally created feat/ branches as
fixtures get rewritten to use a non-prefixed name (so they still test “an unrelated branch in the
repo doesn’t confuse the reader”) without referencing the legacy task-prefix.
Historical references in completed task files under docs/planning/tasks/closed/ (if any) stay —
those are historical artifacts, not active code or convention.
Approach
Section titled “Approach”- Sweep grep. Run
grep -rnE 'feat/' plugin/ docs/skills/and walk every match. Classify each:- Convention prose (
plugin/conventions/*.md): delete the references. - Script docstrings + comments (
plugin/scripts/*.py,plugin/lib/lease/*.py): delete references. - Code paths (regex matches, branch enumeration lists): remove the
feat/alternative. - Schema descriptions (
plugin/schemas/*.json): delete the legacy-prefix mention.
- Convention prose (
- Convention docs. In
plugin/conventions/branch-naming.md, delete the entire “Migrating fromfeat/” section (heading + body). Update the surrounding prose so the doc reads as a clean single-prefix convention. Inlease-aware-skills.md,worktree-scope-guard.md,sdlc-yaml.md— strike the parenthetical “(or legacyfeat/...)” inserts everywhere they appear. - count_inflight_tasks.py. Remove the
feat/<basename>candidate construction from the worktree-to-branch resolution. Update the--prefixargv default and the inner enumeration to drop"feat/*". Delete the deprecation-warning code path. Update the module docstring. - count_inflight_tasks test suite. Delete the three
feat/-specific test cases (case_feat_only_emits_warning,case_mixed_counts_both, plus the module-docstring deprecation-warning assertions if any). The remaining cases (cleantask/worktrees, stale categorization, PR detection) stay unchanged. - project_cleanup.py. Remove
"feat/"from the three branch-prefix lists. Remove the explicitf"feat/{t.basename}"andf"feat/{t.slug}"candidate construction in the per-task branch search. Update the user-facing message that says “(or legacyfeat/) branch, no worktree, no remote branch exists.” to drop the parenthetical. - worktree_scope_guard.py + worktree-scope-guard.md. Strike the parenthetical “(or legacy
feat/...)” from the docstring and convention doc. - plugin/lib/lease/reconcile.py. Remove
feat/<id>candidate construction from_local_task_branches. Update theOrphanBranchFindingdocstring and any related prose. The orphan reconciler now only seestask/<id>branches; that matches reality. - plugin/lib/lease/runtime.py. Strike the “legacy
feat/<task_id>” parenthetical comments. - plugin/lib/lease tests. In
test_migration.py, delete the_git(["branch", "feat/has-feat-branch"], ...)setup and the assertion that the migration’s discovery sequence includes thehead:feat/follow-up. Intest_runtime.py, delete the test case assertingfeat/is not lease-eligible (the test’s reason for existence — the legacy fallback — is gone). The remaining tests in both files cover thetask/-only flow and stay unchanged. - schemas. Remove the
(or the legacy feat/<basename> during the transition window)parenthetical frommax_awaiting_review’s description insdlc-yaml.schema.json. - Run quality checks against the baseline. Expect zero new drift. The lint_task_state_commit_origin linter from #167 should not flag anything (every commit on the task branch will be implementation diff, no task-state frontmatter changes).
Files to touch
Section titled “Files to touch”| Location | Kind | Change |
|---|---|---|
plugin/conventions/branch-naming.md | modify | Delete the “Migrating from feat/” H3 section and the open question about fallback longevity. |
plugin/conventions/lease-aware-skills.md | modify | Strike the two legacy-feat/ parentheticals. |
plugin/conventions/worktree-scope-guard.md | modify | Strike the legacy-feat/ parenthetical. |
plugin/conventions/sdlc-yaml.md | modify | Strike the legacy-feat/ parenthetical inside max_awaiting_review. |
plugin/scripts/count_inflight_tasks.py | modify | Remove feat/ candidate construction, deprecation warning, and module-docstring references. |
plugin/scripts/test_count_inflight_tasks.py | modify | Delete the three feat/-specific test cases. |
plugin/scripts/project_cleanup.py | modify | Remove "feat/" from three branch-prefix lists; remove feat/<basename> candidate construction; update the user-facing message. |
plugin/scripts/worktree_scope_guard.py | modify | Strike the legacy-feat/ parenthetical from the docstring. |
plugin/lib/lease/reconcile.py | modify | Remove feat/<id> from _local_task_branches; update OrphanBranchFinding docstring. |
plugin/lib/lease/runtime.py | modify | Strike the legacy-feat/ comments. |
plugin/lib/lease/tests/test_migration.py | modify | Delete the feat/has-feat-branch fixture and related assertions. |
plugin/lib/lease/tests/test_runtime.py | modify | Delete the legacy-feat/ test case. |
plugin/schemas/sdlc-yaml.schema.json | modify | Strike the legacy-feat/ parenthetical from max_awaiting_review’s description. |
Acceptance criteria
Section titled “Acceptance criteria”-
AC-1:
grep -rnE '\bfeat/' plugin/returns zero matches (excludingplugin/PRINCIPLES.mdif it carries unrelated text, and excluding any historical task files referenced from prose). The only acceptable residualfeat/references are indocs/planning/tasks/closed/(historical) andplugin/conventions/branch-naming.md’s commit-message-prefix table wherefeat:(with colon, conventional-commit prefix — different concept) may legitimately appear. -
AC-2:
plugin/scripts/test_count_inflight_tasks.pyruns to completion with zerofeat/-prefix test cases remaining. The remaining test cases all pass. -
AC-3:
plugin/lib/lease/tests/runs to completion (via pytest at the package root) with zerofeat/-prefix test cases remaining. The remaining tests all pass. -
AC-4: A live
/sdlc:task-workrun against a freshopen/readytask still succeeds end-to-end. (The prefix removal is a no-op fortask/-prefixed flows; this AC just guards against accidental over-removal.) -
AC-5:
/Users/sksizer2/.claude/plugins/sdlc/scripts/run_quality_checks.py --config /Users/sksizer2/Developer/dev/sdlc.yaml --diff-against-baseline <origin-main-sha> --baseline-dir /Users/sksizer2/Developer/dev/.sdlc/quality-baselines --linereports zero new drift introduced by this branch.
Out of scope
Section titled “Out of scope”- Updating historical task files under
docs/planning/tasks/closed/(or anyclosed/*status task file underdocs/planning/tasks/). Those are frozen artifacts of past work; rewriting them would erase context. The grep AC explicitly carves outclosed/content. - Updating commit-message conventions to remove
feat:as a conventional-commit prefix.feat:(with colon) is a separate concept fromfeat/<basename>(with slash); the colon form remains valid per Conventional Commits 1.0. - Migrating the historical merged PRs (#122, #123, #125) — they’re frozen in GitHub’s history.
- Adding migration tooling for users who still have local
feat/*branches outside the canonical project repo. As of 2026-05-28, zero such branches exist in this project; if a contributor surfaces a stale localfeat/*branch later, they can rename viagit branch -m.
Dependencies
Section titled “Dependencies”- none — the architectural fix
[T-SIHV-task-state-frontmatter-commits-on-main-not-worktree-branch](/planning/tasks/task-state-frontmatter-commits-on-main-not-worktree-branch/)shipped (PR #167 merged) and this task complies with it (no task-state frontmatter commits on the task branch).
Discovery context
Section titled “Discovery context”Surfaced 2026-05-28 after the count_inflight_tasks.py query returned no feat/ worktrees,
git branch --list 'feat/*' returned empty, git branch -r --list 'origin/feat/*' returned empty,
and gh pr list --state open had no feat/ heads. The transition window is closed; the
legacy-prefix accommodation throughout the codebase is dead weight.
Post-mortem
Section titled “Post-mortem”Captured by /sdlc:task-work on 2026-05-28. PR: pending.
Acceptance criteria coverage
Section titled “Acceptance criteria coverage”- AC-1: auto —
grep -rnE '\bfeat/' plugin/returns zero matches after the sweep (excluding__pycache__). - AC-2: auto —
plugin/scripts/test_count_inflight_tasks.pyreports1/1 passedwith nofeat/-prefix cases. - AC-3: auto —
pytest plugin/lib/lease/tests/test_migration.py test_runtime.py test_reconcile.pyreports96 passed; nofeat/test cases remain. - AC-4: agent-manual — this very
/sdlc:task-workrun succeeded end-to-end up through Step 7 with the canonicaltask/flow. - AC-5: auto —
run_quality_checks.py --diff-against-baseline bd6370fc...reportsOK 14/14 (baseline-gated; pre-existing findings ignored).
What worked
Section titled “What worked”- The task body’s “Files to touch” table mapped cleanly to grep hits; classifying each match against the three categories (convention prose / code paths / test fixtures) was straightforward.
- Baseline-gated quality checks made it easy to confirm the branch introduced zero new drift even though the corpus has 240 pre-existing findings.
- The lease CLI’s exit-code contract on
task acquireworked exactly as documented; one command, one verdict line, no parsing.
Friction and automation gaps
Section titled “Friction and automation gaps”- For grep-AC-driven mechanical-sweep tasks (where the AC is “grep returns zero matches”), the
canonical “Files to touch” table tends to under-specify reality — this task’s table listed 13
files but the AC-1 sweep touched ~25. A
/sdlc:task-defineextension that runs the AC’s grep command and auto-populates the Files-to-touch table from the result would close this gap. Future runs of similar tasks (the next time a transition-window prefix is squashed, a deprecated convention removed, etc.) would benefit.