Skip to content

feat(scripts): port create-new-feature, setup-plan and setup-tasks to Python#3386

Open
marcelsafin wants to merge 6 commits into
github:mainfrom
marcelsafin:feat/3280-port-core-scripts
Open

feat(scripts): port create-new-feature, setup-plan and setup-tasks to Python#3386
marcelsafin wants to merge 6 commits into
github:mainfrom
marcelsafin:feat/3280-port-core-scripts

Conversation

@marcelsafin

@marcelsafin marcelsafin commented Jul 6, 2026

Copy link
Copy Markdown
Contributor

Description

Ports the three core workflow scripts to Python, per #3280. Follows the pattern established by the check-prerequisites PoC (#3302): additive port, no template or runtime changes, parity tests that run the bash and Python versions side by side and compare output.

Scope:

  • scripts/python/create_new_feature.py — port of create-new-feature.sh, including branch-name generation (stop words, acronym detection, word limits), feature numbering (specs scan + timestamp-dir handling), the 244-char ref-length truncation, --allow-existing-branch mode, and feature.json persistence (byte-identical to the jq output).
  • scripts/python/setup_plan.py — port of setup-plan.sh, including plan template resolution and the status messages that go to stderr in --json mode.
  • scripts/python/setup_tasks.py — port of setup-tasks.sh, including plan/spec validation and the tasks-template requirement.
  • scripts/python/common.py — adds resolve_template() (overrides → presets via registry → extensions → core), shared by the three scripts above.
  • tests/parity_helpers.py — shared fixtures/helpers for parity tests, extracted from the pattern in test_check_prerequisites_python_parity.py.
  • 25 parity tests across three files: exact JSON equality against bash, normalized text output, PowerShell JSON parity (skipped when pwsh is unavailable).

One deliberate deviation: --number abc produces a clean error message in Python where bash surfaces a raw arithmetic error (10#abc: value too great for base). The parity test asserts exit code and empty stdout for that case, not the stderr text.

Testing

  • Tested locally with uv run specify --help
  • Ran existing tests with uv sync && uv run pytest (3801 passed, 110 skipped)
  • Manual side-by-side runs of bash vs Python on a sample repo (23 cases: numbering, stop words, acronyms, truncation, collision handling, JSON/text modes)

AI Disclosure

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

Ported and tested with GitHub Copilot CLI. I reviewed the diff and verified parity behavior manually.

Fixes #3280

… Python

Ports the three core workflow scripts to Python as part of github#3280,
following the check-prerequisites PoC pattern from github#3302. Adds
resolve_template() to the shared common.py module and parity tests
that run bash and Python side by side.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@marcelsafin marcelsafin requested a review from mnriem as a code owner July 6, 2026 23:55
Copilot AI review requested due to automatic review settings July 6, 2026 23:55

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.

Pull request overview

Ports three core Spec Kit workflow scripts (create-new-feature, setup-plan, setup-tasks) to Python while preserving the existing shell implementations, and adds parity tests to ensure the Python ports match Bash/PowerShell output and exit behavior.

Changes:

  • Added Python implementations for create-new-feature, setup-plan, and setup-tasks, plus shared resolve_template() logic in scripts/python/common.py.
  • Added shared parity-test utilities (tests/parity_helpers.py) to run Bash/PowerShell/Python twins in temporary repos with normalized output comparisons.
  • Added parity test suites for the three newly-ported scripts, including JSON/text output parity and PowerShell JSON parity where available.

Reviewed changes

Copilot reviewed 8 out of 8 changed files in this pull request and generated 1 comment.

Show a summary per file
File Description
tests/test_setup_tasks_python_parity.py Adds Bash/Python + PowerShell/Python parity tests for setup-tasks.
tests/test_setup_plan_python_parity.py Adds Bash/Python + PowerShell/Python parity tests for setup-plan.
tests/test_create_new_feature_python_parity.py Adds parity tests for create-new-feature including numbering, truncation, errors, and persistence.
tests/parity_helpers.py Introduces reusable helpers for parity tests (repo scaffolding, command builders, normalization, subprocess runner).
scripts/python/setup_tasks.py Python port of setup-tasks including prereq checks + template resolution and JSON/text output.
scripts/python/setup_plan.py Python port of setup-plan including template copy behavior and JSON-mode stderr status messages.
scripts/python/create_new_feature.py Python port of create-new-feature including naming rules, numbering/timestamp modes, truncation, and feature.json persistence.
scripts/python/common.py Adds resolve_template() and preset ordering helper to mirror Bash template resolution priority.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread tests/parity_helpers.py
Comment on lines +84 to +94
def run(
cmd: list[str], repo: Path, env: dict[str, str] | None = None
) -> subprocess.CompletedProcess[str]:
return subprocess.run(
cmd,
cwd=repo,
capture_output=True,
text=True,
check=False,
env=env or clean_env(),
)

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Fixed in 83a5dca: env if env is not None else clean_env().

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

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.

Review details

  • Files reviewed: 8/8 changed files
  • Comments generated: 1
  • Review effort level: Low

Comment thread scripts/python/common.py
Comment on lines +185 to +208
def _sorted_preset_ids(presets_dir: Path) -> list[str]:
registry = presets_dir / ".registry"
if registry.is_file():
try:
data = json.loads(registry.read_text(encoding="utf-8"))
except (OSError, json.JSONDecodeError):
data = None
if isinstance(data, dict):
presets = data.get("presets", {})
if isinstance(presets, dict):
return [
pid
for pid, meta in sorted(
presets.items(),
key=lambda kv: kv[1].get("priority", 10)
if isinstance(kv[1], dict)
else 10,
)
if isinstance(meta, dict) and meta.get("enabled", True) is not False
]
try:
return sorted(p.name for p in presets_dir.iterdir() if p.is_dir())
except OSError:
return []

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Fixed in d4e4c5e: the whole registry read/sort is now wrapped in a broad except that falls back to the directory scan, and the scan skips hidden dirs, matching the bash behavior. Added a parity test with an unorderable priority value plus a hidden preset dir.

… hidden preset dirs

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Ships with the scripts they reference; the remaining templates got
their py: lines in github#3403.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot AI review requested due to automatic review settings July 8, 2026 06:59

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.

Pull request overview

Copilot reviewed 10 out of 10 changed files in this pull request and generated 3 comments.

Comment thread tests/parity_helpers.py
Comment on lines +66 to +73
def ps_cmd(repo: Path, script: str, *args: str) -> list[str]:
return [
POWERSHELL_EXE,
"-NoProfile",
"-File",
str(repo / ".specify" / "scripts" / "powershell" / f"{script}.ps1"),
*args,
]

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

All ps_cmd call sites are guarded by HAS_POWERSHELL skips, but the failure mode was obscure. Added an assert with a clear message so a missing guard fails loudly.

Comment on lines 11 to +14
scripts:
sh: scripts/bash/setup-plan.sh --json
ps: scripts/powershell/setup-plan.ps1 -Json
py: scripts/python/setup_plan.py --json

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Good catch. resolve_skill_placeholders now accepts py and prefixes the resolved interpreter (quoted when the path contains spaces), matching process_template. Covered in tests/test_skill_placeholder_py.py.

Comment on lines 12 to +15
scripts:
sh: scripts/bash/setup-tasks.sh --json
ps: scripts/powershell/setup-tasks.ps1 -Json
py: scripts/python/setup_tasks.py --json

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Fixed together with the plan.md thread: the skills resolver handles py with interpreter prefixing now.

resolve_skill_placeholders only accepted sh/ps, so a py init option
fell into the fallback path and {SCRIPT} rendered without an
interpreter prefix. Accept py and prefix the resolved interpreter,
matching process_template. Also guard ps_cmd against a missing
PowerShell with a clear assert.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot AI review requested due to automatic review settings July 8, 2026 07:09

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.

Pull request overview

Copilot reviewed 12 out of 12 changed files in this pull request and generated 1 comment.

Comment on lines +252 to +272
@requires_bash
@pytest.mark.parametrize(
"args",
[
(),
(" ",),
("--short-name",),
("--number",),
],
ids=["missing_description", "whitespace_description", "short_name_no_value", "number_no_value"],
)
def test_python_argument_errors_match_bash(repo: Path, args: tuple[str, ...]) -> None:
bash = run(bash_cmd(repo, SCRIPT, *args), repo)
py = run(py_cmd(repo, SCRIPT, *args), repo)

assert py.returncode == bash.returncode == 1
assert py.stdout == bash.stdout == ""
assert normalize_script_names(py.stderr, repo, SCRIPT) == normalize_script_names(
bash.stderr, repo, SCRIPT
)

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Added test_python_invalid_number_fails_cleanly: exit code 1, empty stdout, and the exact error message are pinned, with a comment explaining the deliberate deviation from the bash arithmetic error.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot AI review requested due to automatic review settings July 8, 2026 07:15

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.

Pull request overview

Copilot reviewed 12 out of 12 changed files in this pull request and generated 2 comments.

Comment on lines 11 to +14
scripts:
sh: scripts/bash/setup-plan.sh --json
ps: scripts/powershell/setup-plan.ps1 -Json
py: scripts/python/setup_plan.py --json

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

The installer gap is fixed in #3403 (commit aa7e627): install_shared_infra maps py to the python variant dir and a test runs the installer with --script py and asserts the scripts land. Keeping that fix in one PR avoids conflicting duplicate patches; both PRs together complete the --script py flow.

Comment on lines 12 to +15
scripts:
sh: scripts/bash/setup-tasks.sh --json
ps: scripts/powershell/setup-tasks.ps1 -Json
py: scripts/python/setup_tasks.py --json

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Same as the plan.md thread: covered by the shared-infra fix in #3403.

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.

Review details

  • Files reviewed: 12/12 changed files
  • Comments generated: 1
  • Review effort level: Low

Comment on lines +44 to +54
def _help_text(argv0: str) -> str:
return f"""{_usage(argv0)}

Options:
--json Output in JSON format
--dry-run Compute feature name and paths without creating directories or files
--allow-existing-branch Reuse an existing feature directory if it already exists
--short-name <name> Provide a custom short name (2-4 words) for the feature
--number N Specify branch number manually (overrides auto-detection)
--timestamp Use timestamp prefix (YYYYMMDD-HHMMSS) instead of sequential numbering
--help, -h Show this help message

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Right, --no-git does not exist in any variant. That was a description error on my part, not missing code: the flag list should have said --allow-existing-branch only. PR description fixed. The bash twin has no --no-git either, so implementing it here would break parity scope.

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Port core scripts to Python (common, create-new-feature, setup-plan, setup-tasks)

3 participants