* fix(scripts): portable uppercase for branch-name acronym retention
Branch-name generation keeps short uppercase acronyms (e.g. "AI") by re-checking
the lowercased word against the original description with ${word^^}. That
parameter expansion is bash 4+ only; on macOS's default bash 3.2 it errors with
"bad substitution", so the acronym/short-word retention branch never matches and
those words are dropped ("go AI now" yields 001-now instead of 001-ai-now). Use
tr '[:lower:]' '[:upper:]' instead, which is portable.
Applies to both the core create-new-feature.sh and the git extension's
create-new-feature-branch.sh. The existing
test_branch_name_short_word_case_sensitivity / test_short_word_retention tests
cover this and now pass on bash 3.2 (CI runs on bash 4+/Linux, so they passed
there already).
(Disclosure: an AI coding agent surfaced the failure while running the suite on
macOS and pinned the root cause; fix written and reviewed by me.)
* fix(scripts): portability follow-ups from code review
- core create-new-feature.sh: match the acronym with `grep -qw` (POSIX
whole-word) instead of `\b...\b` (GNU/BSD-only), matching the git extension
and dropping a non-POSIX construct.
- lint: add a CI guard rejecting bash 4+ case-modification expansions in *.sh.
shellcheck assumes bash 4+ from the shebang and can't flag them, and CI has no
bash-3.2 lane, so this prevents silently re-shipping the macOS regression this
PR fixes.
- update a stale PowerShell extension comment that cited the removed bash idiom.
(Disclosure: prompted by an AI code review of the PR; written and reviewed by me.)
* chore: align CI Python matrix with devguide release lifecycle
Run the pytest matrix only on the bugfix (maintenance) releases — 3.13
and 3.14 — instead of 3.11/3.12/3.13, and point the ruff lint job at the
latest interpreter (3.14). The supported floor stays at requires-python
>= 3.11 (oldest non-EOL security release): older security versions are
supported by claim and fixed reactively rather than gated on a wide
per-commit matrix. Also add macos-latest to the OS matrix so macOS
regressions are caught.
Assisted-by: GitHub Copilot (model: Claude Opus 4.8, autonomous)
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* fix: make bash scripts portable to bash 3.2 (macOS system /bin/bash)
Adding macos-latest to the CI matrix surfaced two pre-existing bash 3.2
incompatibilities (macOS ships bash 3.2 as /bin/bash):
1. update-agent-context.sh embedded Python heredocs inside $(...) command
substitution. bash 3.2 mis-parses an apostrophe in a heredoc body
nested in $(...), failing with "unexpected EOF while looking for
matching `''". Removed the apostrophes from the affected $()-nested
heredoc body and documented the constraint to prevent regressions.
2. create-new-feature-branch.sh and create-new-feature.sh used the
bash 4+ ${word^^} uppercase parameter expansion, which errors as a
"bad substitution" on bash 3.2 and caused short uppercase acronyms
(e.g. "GO") to be dropped from derived branch names. Replaced with a
portable `tr '[:lower:]' '[:upper:]'` pipeline.
Verified the full test suite passes under bash 3.2.57 and shellcheck
(--severity=error) is clean.
Assisted-by: GitHub Copilot (model: Claude Opus 4.8, autonomous)
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* fix: address review feedback on bash 3.2 portability changes
- create-new-feature.sh: replace the non-portable `\b...\b` grep
word-boundary (BSD grep treats `\b` as a backspace, so the acronym
branch could silently fail) with `grep -qw`, matching its twin
create-new-feature-branch.sh, and pipe the description via
`printf '%s'` instead of `echo`.
- create-new-feature-branch.sh: switch the acronym check to
`printf '%s'` as well so both twins are identical and avoid `echo`
on user-provided text.
- update-agent-context.sh: reword the apostrophe-free self-seeding
comment to be clearer and less easy to misread.
Verified under bash 3.2.57 (full bash-script suite green) and
shellcheck --severity=error.
Assisted-by: GitHub Copilot (model: Claude Opus 4.8, autonomous)
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
---------
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* feat(scripts): add SPECIFY_INIT_DIR to target a member project from the repo root
Resolve an explicit SPECIFY_INIT_DIR project override once in the core
get_repo_root / Get-RepoRoot, so a non-interactive / CI caller can target a
member project (the directory containing .specify/) from a monorepo root
without cd. Strict by design: the path must exist and contain .specify/,
otherwise it hard-errors with no silent fallback.
- Single resolver in core; the git feature-branch script inherits it by
sourcing core, with no per-extension copies.
- PS resolver verifies the resolved path is a directory (Resolve-Path also
succeeds for files) so a file value errors as "not an existing directory".
- get_feature_paths splits decl/assignment so a SPECIFY_INIT_DIR failure
propagates instead of being masked by `local`.
- create-new-feature-branch: when core is absent (only git-common loaded) and
SPECIFY_INIT_DIR is set, hard-error rather than silently using the git root.
- Document SPECIFY_INIT_DIR and SPECIFY_FEATURE_DIRECTORY in the core reference.
- Tests for valid/relative/trailing-slash/file/missing/no-.specify targets,
feature-axis composition, the no-core guard, and a PowerShell mirror.
* fix: guard SPECIFY_INIT_DIR with stale core scripts
* docs: clarify SPECIFY_FEATURE_DIRECTORY precedence wording
* fix: normalize trailing slash in PowerShell SPECIFY_INIT_DIR resolver
Resolve-Path preserves a trailing separator from its input, so a
SPECIFY_INIT_DIR ending in a slash returned a root that didn't match the
bash resolver (whose `cd && pwd` strips it). That broke
test_ps_trailing_slash_tolerated on the CI runners, which do have pwsh.
Trim it with TrimEndingDirectorySeparator (no-op on a bare root or a path
with no trailing separator).
Also fix the misleading test comment: the PowerShell mirror runs on the
CI ubuntu/windows runners (they ship pwsh), it is not skipped there.
* test: normalize bash path expectations on Windows
* docs: clarify SPECIFY_INIT_DIR root helpers
* feat(init)!: make git extension opt-in and remove --no-git at v0.10.0
- Remove --no-git parameter from specify init command
- Remove git extension auto-installation from init flow
- Git repository initialization (git init) still runs when git is available
- Remove --no-git from all test invocations across the test suite
- Update docs to reflect opt-in git extension behavior
- Replace TestGitExtensionAutoInstall with TestGitExtensionOptIn tests
BREAKING CHANGE: specify init no longer auto-installs the git extension.
Use `specify extension add git` to install it explicitly.
The --no-git flag has been removed.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* refactor(scripts): remove git operations from core scripts
Git functionality is now entirely managed by the git extension.
Core scripts only handle directory-based feature creation and numbering.
- Remove has_git(), check_feature_branch(), git branch creation from core
- Simplify number detection to use only spec directory scanning
- Remove HAS_GIT output from get_feature_paths()
- Remove git remote fetching and branch querying
- Keep BRANCH_NAME output key for backward compatibility
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* refactor: remove all git operations from core
- Remove is_git_repo() and init_git_repo() dead code from _utils.py
- Remove --branch-numbering from init command
- Remove git from 'specify check' (now extension-only)
- Update docs: git is optional prerequisite, check command description
- Fix tests to reflect no-git-in-core reality (fallback to main)
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* refactor(scripts): remove directory scanning and branch fallback from core
Core scripts now resolve feature context exclusively from:
1. SPECIFY_FEATURE env var (set by git extension)
2. .specify/feature.json (persisted by specify command)
Removed find_feature_dir_by_prefix() and directory scanning heuristics —
these are the git extension's responsibility. Scripts error clearly when
no feature context is available.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* feat: introduce feature_numbering, deprecate branch_numbering in init-options
- specify command template now reads feature_numbering (preferred) with
fallback to branch_numbering (deprecated) from init-options.json
- Git extension reads git-config.yml > feature_numbering > branch_numbering
- init now writes feature_numbering: sequential to init-options.json
- Deprecation warning emitted when branch_numbering is used as fallback
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* fix: remove trailing whitespace in common.ps1
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* feat(scripts): persist SPECIFY_FEATURE_DIRECTORY env var to feature.json
When SPECIFY_FEATURE_DIRECTORY is set, get_feature_paths() now writes the
value to .specify/feature.json so future sessions without the env var can
still resolve the feature directory. The write is idempotent — it skips
when the file already contains the same value.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* fix: address review feedback — error messages and docs
- Update error messages in common.sh and common.ps1 to reference
SPECIFY_FEATURE_DIRECTORY instead of SPECIFY_FEATURE (which no longer
resolves feature directories)
- Fix get_current_branch comment (returns empty string, not error)
- Update upgrade.md to reference SPECIFY_FEATURE_DIRECTORY with correct
example paths
- Update local-development.md troubleshooting: replace stale 'Git step
skipped' row with actionable git extension guidance
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* fix(scripts): harden feature.json persistence
- Use json_escape in printf fallback when jq is unavailable (common.sh)
- Replace utf8NoBOM encoding with UTF8Encoding($false) for PowerShell
5.1 compatibility (common.ps1)
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* refactor(scripts): remove dead feature_json_matches_feature_dir functions
These guards are no longer needed since the branch-name validation they
protected against has been removed from check-prerequisites.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* refactor(git-ext): rename create-new-feature to create-new-feature-branch
The git extension's script only creates the git branch — rename it to
reflect that responsibility. The core create-new-feature.sh/.ps1 handles
feature directory creation and feature.json persistence.
Also includes fixes from review feedback:
- common.sh: _persist_feature_json uses json_escape fallback
- common.ps1: Save-FeatureJson uses UTF8Encoding for PS 5.1 compat
- common.ps1: case-sensitive path stripping on non-Windows
- create-new-feature.sh/ps1: output both SPECIFY_FEATURE and
SPECIFY_FEATURE_DIRECTORY
- setup-tasks.sh: fix stale 'Validate branch' comment
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* fix(tests): update references to renamed git extension scripts
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* fix(tests): remove duplicate EXT_CREATE_FEATURE assignments
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
---------
Co-authored-by: Manfred Riem <mnriem@users.noreply.github.com>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
xargs re-parses stdin as shell tokens, causing 'unterminated quote'
errors when feature descriptions contain apostrophes, double quotes,
or backslashes. Replace with sed-based whitespace trim that preserves
input verbatim.
Add regression tests for special characters in descriptions (core and
extension scripts), plus a negative test for whitespace-only input.
Fixes#2339
* feat(scripts): add --dry-run flag to create-new-feature scripts
Add a --dry-run / -DryRun flag to both bash and PowerShell
create-new-feature scripts that computes the next branch name,
spec file path, and feature number without creating any branches,
directories, or files. This enables external tools to query the
next available name before running the full specify workflow.
When combined with --json, the output includes a DRY_RUN field.
Without --dry-run, behavior is completely unchanged.
Closes#1931
Assisted-By: 🤖 Claude Code
* fix(scripts): gate specs/ dir creation behind dry-run check
Dry-run was unconditionally creating the root specs/ directory via
mkdir -p / New-Item before the dry-run guard. This violated the
documented contract of zero side effects. Also adds returncode
assertion on git branch --list in tests and adds PowerShell dry-run
test coverage (skipped when pwsh unavailable).
Addresses review comments on #1998.
Assisted-By: 🤖 Claude Code
* fix: address PR review feedback
- Gate `mkdir -p $SPECS_DIR` behind DRY_RUN check (bash + PowerShell)
so dry-run creates zero directories
- Add returncode assertion on `git branch --list` in test
- Strengthen spec dir test to verify root `specs/` is not created
- Add PowerShell dry-run test class (5 tests, skipped without pwsh)
- Fix run_ps_script to use temp repo copy instead of project root
Assisted-By: 🤖 Claude Code
* fix: use git ls-remote for remote-aware dry-run numbering
Dry-run now queries remote branches via `git ls-remote --heads`
(read-only, no fetch) to account for remote-only branches when
computing the next sequential number. This prevents dry-run from
returning a number that already exists on a remote.
Added test verifying dry-run sees remote-only higher-numbered
branches and adjusts numbering accordingly.
Assisted-By: 🤖 Claude Code
* fix(scripts): deduplicate number extraction and branch scanning logic
Extract shared _extract_highest_number helper (bash) and
Get-HighestNumberFromNames (PowerShell) to eliminate duplicated
number extraction patterns between local branch and remote ref
scanning.
Add SkipFetch/skip_fetch parameter to check_existing_branches /
Get-NextBranchNumber so dry-run reuses the same function instead
of inlining duplicate max-of-branches-and-specs logic.
Assisted-By: 🤖 Claude Code
* fix(tests): use isolated paths for remote branch test
Move remote.git and second_clone directories under git_repo
instead of git_repo.parent to prevent path collisions with
parallel test workers.
Assisted-By: 🤖 Claude Code
* fix: address PR review feedback
- Set GIT_TERMINAL_PROMPT=0 for git ls-remote calls to prevent
credential prompts from blocking dry-run in automation scenarios
- Add returncode assertion to test_dry_run_with_timestamp git
branch --list check
Assisted-By: 🤖 Claude Code
* feat(scripts): add --allow-existing-branch flag to create-new-feature
Add an --allow-existing-branch / -AllowExistingBranch flag to both
bash and PowerShell create-new-feature scripts. When the target branch
already exists, the script switches to it instead of failing. The spec
directory and template are still created if missing, but existing
spec.md files are not overwritten (prevents data loss on re-runs).
The flag is opt-in, so existing behavior is completely unchanged
without it. This enables worktree-based workflows and CI/CD pipelines
that create branches externally before running speckit.specify.
Relates to #1931. Also addresses #1680, #841, #1921.
Assisted-By: 🤖 Claude Code
* fix: address PR review feedback for allow-existing-branch
- Make checkout failure fatal instead of suppressing with || true (bash)
- Check $LASTEXITCODE after git checkout in PowerShell
- Use Test-Path -PathType Leaf for spec file existence check (PS)
- Add PowerShell static assertion test for -AllowExistingBranch flag
Assisted-By: 🤖 Claude Code
* fix(scripts): prioritize .specify over git for repo root detection
When spec-kit is initialized in a subdirectory that doesn't have its
own .git, but a parent directory does, spec-kit was incorrectly using
the parent's git repository root. This caused specs to be created in
the wrong location.
The fix changes repo root detection to prioritize .specify directory
over git rev-parse, ensuring spec-kit respects its own initialization
boundary rather than inheriting a parent git repo.
Fixes#1932
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
* fix: address code review feedback
- Normalize paths in find_specify_root to prevent infinite loop with relative paths
- Use -PathType Container in PowerShell to only match .specify directories
- Improve has_git/Test-HasGit to check git command availability and validate work tree
- Handle git worktrees/submodules where .git can be a file
- Remove dead fallback code in create-new-feature scripts
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
* fix: check .specify before termination in find_specify_root
Fixes edge case where project root is at filesystem root (common in
containers). The loop now checks for .specify before checking the
termination condition.
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
* fix: scope git operations to spec-kit root & remove unused helpers
- get_current_branch now uses has_git check and runs git with -C to
prevent using parent git repo branch names in .specify-only projects
- Same fix applied to PowerShell Get-CurrentBranch
- Removed unused find_repo_root() from create-new-feature.sh
- Removed unused Find-RepositoryRoot from create-new-feature.ps1
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
* fix: use cd -- to handle paths starting with dash
Prevents cd from interpreting directory names like -P or -L as options.
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
* fix: check git command exists before calling get_repo_root in has_git
Avoids unnecessary work when git isn't installed since get_repo_root
may internally call git rev-parse.
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
* fix(powershell): use LiteralPath and check git before Get-RepoRoot
- Use -LiteralPath in Find-SpecifyRoot to handle paths with wildcard
characters ([, ], *, ?)
- Check Get-Command git before calling Get-RepoRoot in Test-HasGit to
avoid unnecessary work when git isn't installed
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
* fix(powershell): use LiteralPath for .git check in Test-HasGit
Prevents Test-Path from treating wildcard characters in paths as globs.
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
* fix(powershell): use LiteralPath in Get-RepoRoot fallback
Prevents Resolve-Path from treating wildcard characters as patterns.
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
---------
Co-authored-by: iamaeroplane <michal.bachorik@gmail.com>
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
In multi-remote environments, `git fetch --all` outputs messages like
"Fetching origin" to stdout. Since `check_existing_branches()` only
redirected stderr (`2>/dev/null`), the stdout output was captured by
the `$(...)` command substitution calling this function, contaminating
the branch number return value and causing arithmetic errors like
`$((10#Fetching...))`.
Fix: redirect both stdout and stderr to /dev/null (`>/dev/null 2>&1`).
* fix(scripts): harden bash scripts with escape, compat, and cleanup fixes
- common.sh: complete RFC 8259 JSON escape (\b, \f, strip control chars)
- common.sh: distinguish python3 success-empty vs failure in resolve_template
- check-prerequisites.sh: escape doc names through json_escape in fallback path
- create-new-feature.sh: remove duplicate json_escape (already in common.sh)
- create-new-feature.sh: warn on stderr when spec template is not found
- update-agent-context.sh: move nested function to top-level for bash 3.2 compat
* fix(scripts): explicit resolve_template return code and best-effort agent updates
- common.sh: resolve_template now returns 1 when no template is found,
making the "not found" case explicit instead of relying on empty stdout
- setup-plan.sh, create-new-feature.sh: add || true to resolve_template
calls so set -e does not abort on missing templates (non-fatal)
- update-agent-context.sh: accumulate errors in update_all_existing_agents
instead of silently discarding them — all agents are attempted and the
composite result is returned, matching the PowerShell equivalent behavior
* style(scripts): add clarifying comment in resolve_template preset branch
* fix(scripts): wrap python3 call in if-condition to prevent set -e abort
Move the python3 command substitution in resolve_template into an
if-condition so that a non-zero exit (e.g. invalid .registry JSON)
does not abort the function under set -e. The fallback directory
scan now executes as intended regardless of caller errexit settings.
* fix(scripts): track agent file existence before update and avoid top-level globals
- _update_if_new now records the path and sets _found_agent before calling
update_agent_file, so that failures do not cause duplicate attempts on
aliased paths (AMP/KIRO/BOB -> AGENTS_FILE) or false "no agent files
found" fallback triggers
- Remove top-level initialisation of _updated_paths and _found_agent;
they are now created exclusively inside update_all_existing_agents,
keeping the script side-effect free when sourced
- Replace eval of unquoted get_feature_paths output with safe pattern:
capture into variable, check return code, then eval quoted result
- Use printf '%q' in get_feature_paths to safely emit shell assignments,
preventing injection via paths containing quotes or metacharacters
- Add json_escape() helper for printf JSON fallback paths, handling
backslash, double-quote, and control characters when jq is unavailable
- Use jq -cn for safe JSON construction with proper escaping when
available, with printf + json_escape() fallback
- Replace declare -A (bash 4+) with indexed array for bash 3.2
compatibility (macOS default)
- Use inline command -v jq check in create-new-feature.sh since it
does not source common.sh
- Guard trap cleanup against re-entrant invocation by disarming traps
at entry
- Use printf '%q' for shell-escaped branch names in user-facing output
- Return failure instead of silently returning wrong path on ambiguous
spec directory matches
- Deduplicate agent file updates via realpath to prevent multiple writes
to the same file (e.g. AGENTS.md aliased by multiple variables)
When --number 027 was passed, printf '%03d' interpreted it as octal,
converting 027 (octal) to 23 (decimal). Now forces base-10 with 10# prefix.
Bug: User passes --number 027, gets feature 023 instead of 027
Root cause: printf %03d treats leading zeros as octal notation
Fix: Use $((10#$BRANCH_NUMBER)) to force decimal interpretation
Example:
- Before: --number 027 → octal 027 → decimal 23 → feature 023
- After: --number 027 → decimal 27 → feature 027
Note: PowerShell version does not have this bug because [int] type
conversion in PowerShell does not treat leading zeros as octal.
The check_existing_branches (bash) and Get-NextBranchNumber (PowerShell)
functions no longer use the short_name parameter since they now find the
global maximum across ALL features. This commit:
1. Removes the unused parameter from function signatures
2. Updates all call sites to not pass the parameter
This prevents the scripts from failing when the function is called with
the wrong number of arguments.
The check_existing_branches (bash) and Get-NextBranchNumber (PowerShell)
functions were only looking for branches/specs matching the SAME short name
when determining the next feature number. This caused collisions where
multiple features could be assigned the same number if they had different
short names.
For example, if feature 023-ci-optimization existed, creating a new feature
with a different short name would incorrectly use 001 instead of 024.
This fix changes both functions to:
1. Use get_highest_from_branches() / Get-HighestNumberFromBranches to find
the highest number across ALL branches globally
2. Use get_highest_from_specs() / Get-HighestNumberFromSpecs to find the
highest number across ALL spec directories globally
3. Return the maximum of both + 1
The helper functions already existed but were not being used. This fix
properly utilizes them to ensure features are numbered sequentially
regardless of their short names.
Issue: Branch number collisions when creating features with different names
Impact: Prevents multiple features from sharing the same number prefix
Refactored both Bash and PowerShell create-new-feature scripts to modularize and deduplicate logic for determining the next feature number, including new helper functions for extracting the highest number from specs and branches. Improved branch name cleaning and generation. In update-agent-context scripts, removed redundant updates to AGENTS.md for Copilot, streamlining agent update logic.
- Use git ls-remote for more reliable remote branch detection
- Check remote branches, local branches, AND specs directories
- Match exact short-name pattern to avoid false positives
- Ensures no duplicate numbers across all sources
- Add --number parameter to create-new-feature scripts (bash & PowerShell)
- Add check_existing_branches() function to fetch and scan remote branches
- Update branch numbering logic to check remotes before creating new branches
- Update /speckit.specify command to document remote branch checking workflow
- Prevents duplicate branch numbers when branches exist on remotes but not locally
- Maintains backward compatibility with existing workflows
- Falls back to local directory scanning when Git is not available
Fix two critical bugs in the argument parsing logic that caused incorrect
behavior when --short-name parameter was used:
1. **Index offset bug**: The loop started at i=0 and used i < $#, which
incorrectly processed $0 (script name) as the first argument and
skipped the last actual parameter. Changed to i=1 and i <= $# to
properly iterate through actual command-line arguments ($1 to $#).
2. **Boundary condition bug**: The condition `[ $((i + 1)) -ge $# ]`
incorrectly flagged valid arguments as missing. When --short-name was
at position $#-1, the next position $# was valid but treated as
out-of-bounds. Changed to `[ $((i + 1)) -gt $# ]` for correct validation.
3. **Enhanced validation**: Added check to ensure --short-name value is
not another option (doesn't start with --).
**Before**:
- `script --json "desc" --short-name "test"` → Error: requires a value
- `script --json "desc1" "desc2" --short-name` → Generated wrong branch name
**After**:
- `script --json "desc" --short-name "test"` → Works correctly
- `script --json "desc1" "desc2" --short-name` → Proper error message
This ensures the script correctly supports both parameter orders:
- `[--json] [--short-name <name>] <feature_description>`
- `[--json] <feature_description> [--short-name <name>]`