Test-FileExists / Test-DirHasFiles report their line with Write-Output and
ALSO return $true/$false — both on the Success stream. The callers piped
the whole call to `| Out-Null` to discard the boolean, which discarded the
report line with it, so text mode printed the header and nothing under it:
BEFORE (measured, powershell.exe -NoProfile -File ... -IncludeTasks):
FEATURE_DIR:...\specs\001-f
AVAILABLE_DOCS:
(2 lines)
AFTER:
FEATURE_DIR:...\specs\001-f
AVAILABLE_DOCS:
[OK] research.md
[FAIL] data-model.md
[FAIL] contracts/
[FAIL] quickstart.md
[FAIL] tasks.md
(7 lines)
The bash and Python twins both list every document under that header, so
the PowerShell variant silently returned less information for the same
inputs.
Filter out only the boolean, keeping the report lines. Adds the first
PowerShell text-mode test in this file (every existing PS test is -Json).
File stays ASCII-only (verified 0 non-ASCII bytes).
Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
* feat(presets): resolve constitutions at command time
Gate install-time constitution materialization behind the constitution-sync preset while preserving one-time init seeding and authored-file safeguards.
Assisted-by: GitHub Copilot (model: GPT-5.6 Sol, autonomous)
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 7dbce70f-80c6-4e14-a30d-78cb358bcb84
* fix(presets): emit composed template content
Add a machine-readable preset resolve mode backed by PresetResolver.resolve_content and require the constitution command to consume it.
Assisted-by: GitHub Copilot (model: GPT-5.6 Sol, autonomous)
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 7dbce70f-80c6-4e14-a30d-78cb358bcb84
* fix(presets): unify runtime template composition
Assisted-by: GitHub Copilot (model: GPT-5.6 Sol, autonomous)
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 7dbce70f-80c6-4e14-a30d-78cb358bcb84
* fix(presets): secure runtime template resolution
Align runtime resolution across script variants, validate registry path components, and honor canonical extension ordering and convention paths.
Assisted-by: GitHub Copilot (model: GPT-5.6 Sol, autonomous)
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
* fix(presets): align runtime priority semantics
Normalize and tie-break preset priorities consistently across script variants, and preserve template bytes when Python materializes generated files.
Assisted-by: GitHub Copilot (model: GPT-5.6 Sol, autonomous)
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
* fix(presets): stop at effective template base
Avoid parsing irrelevant lower layers once resolution reaches a replace base, and decode raw bytes so Python preserves source line endings.
Assisted-by: GitHub Copilot (model: GPT-5.6 Sol, autonomous)
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
* fix(presets): align extension template resolution
Support root-level extension templates across runtime resolvers, fail safely when Bash cannot parse an extension registry, and validate requested templates in every prerequisite output mode.
Assisted-by: GitHub Copilot (model: GPT-5.6 Sol, autonomous)
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
* fix(presets): resolve dotted command identifiers
Route safe dotted names through command resolution, correct traversal coverage, and make Windows CI text decoding explicit.
Assisted-by: GitHub Copilot (model: GPT-5.6 Sol, autonomous)
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
* Avoid orphan feature directories on template errors
Assisted-by: GitHub Copilot (model: GPT-5.6 Sol, autonomous)
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
* Align malformed preset manifest handling
Assisted-by: GitHub Copilot (model: GPT-5.6 Sol, autonomous)
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 3158e06f-95df-4e3a-843f-f159a35aa30c
* Complete runtime resolver parity
Assisted-by: GitHub Copilot (model: GPT-5.6 Sol, autonomous)
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 3158e06f-95df-4e3a-843f-f159a35aa30c
* Fail closed on resolver input errors
Assisted-by: GitHub Copilot (model: GPT-5.6 Sol, autonomous)
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 3158e06f-95df-4e3a-843f-f159a35aa30c
* Force UTF-8 and full manifest validation
Force UTF-8 decoding for registry and manifest reads in the Bash and
PowerShell embedded-Python parsers so resolution no longer depends on the
process locale, and validate every manifest template entry's required
fields, type, and strategy consistent with the canonical PresetManifest.
Assisted-by: GitHub Copilot (model: claude-opus-4.8, autonomous)
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 3158e06f-95df-4e3a-843f-f159a35aa30c
* Fail closed on empty manifests and corrupt registries
Reject manifests missing the provides/templates sections or declaring an
empty template list in all three runtime resolvers, matching the canonical
PresetManifest which treats those as invalid instead of silently degrading a
composing layer to a convention `replace` lookup.
Make a corrupt or unreadable extension registry fail closed in Bash,
PowerShell, and Python instead of swallowing the error and treating every
on-disk extension directory as unregistered-and-enabled, which could activate
a disabled extension.
Read the preset and extension registries as explicit UTF-8 in the PowerShell
resolver so priority/enabled-state decoding no longer depends on the process
code page under Windows PowerShell 5.1.
Assisted-by: GitHub Copilot (model: claude-opus-4.8, autonomous)
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 3158e06f-95df-4e3a-843f-f159a35aa30c
* fix(presets): fail closed when extension registry is not a regular file
The Bash and Python resolvers used is_file()/`-f` to gate reading the
extension `.registry`, which returns false for a directory or a broken
symlink at that path. In those cases the resolvers treated the registry
as absent and scanned every on-disk extension directory as unregistered
and enabled — a fail-open path. Detect any filesystem entry at the
registry path (including broken symlinks) and reject unless it is a
readable regular file. PowerShell now rejects a non-leaf entry explicitly
for parity. Adds directory- and broken-symlink parity regressions.
Assisted-by: GitHub Copilot (model: claude-opus-4.8, autonomous)
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 3158e06f-95df-4e3a-843f-f159a35aa30c
* fix(presets): fail closed on corrupt registry in canonical resolver and PowerShell
Two remaining fail-open paths for an invalid extension registry:
- The canonical PresetResolver enumerated extensions through
ExtensionRegistry, whose _load() normalizes a corrupt or unreadable
registry to an empty mapping. The directory scan then admitted every
on-disk extension directory as unregistered-and-enabled, so a corrupt
registry could still supply constitution content at init and through
constitution-sync materialization. Add a non-invasive is_corrupt()
probe (recovery behavior for install/enable/disable is unchanged) and
raise from _get_all_extensions_by_priority() when the registry exists
but is invalid. _load() now also recovers from OSError/UnicodeDecodeError
so a directory or unreadable registry no longer crashes construction.
- The PowerShell resolver gated the registry read with Test-Path, which
returns false for a dangling symlink on Windows, letting a broken
.registry symlink bypass the guard and enable every on-disk extension.
Detect the entry via directory enumeration (which observes a broken
symlink) and reject it unless it is a readable regular file.
Adds canonical corrupt/directory-registry regressions and extends the
broken-symlink parity test to PowerShell.
Assisted-by: GitHub Copilot (model: claude-opus-4.8, autonomous)
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 3158e06f-95df-4e3a-843f-f159a35aa30c
* fix(presets): detect dangling registry symlink in ExtensionRegistry.is_corrupt
is_corrupt() gated on Path.exists(), which follows symlinks and returns
False for a dangling .registry symlink — so the canonical PresetResolver
treated it as an absent registry and fell back to scanning every on-disk
extension directory as unregistered-and-enabled, reopening the fail-open
path this guard closes. Detect lexical existence with os.path.lexists and
require a regular file before parsing, so a broken symlink (or directory)
is reported corrupt and resolution fails closed. Adds a canonical
broken-symlink regression alongside the directory case.
Assisted-by: GitHub Copilot (model: claude-opus-4.8, autonomous)
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 3158e06f-95df-4e3a-843f-f159a35aa30c
---------
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 7dbce70f-80c6-4e14-a30d-78cb358bcb84
Copilot-Session: 3158e06f-95df-4e3a-843f-f159a35aa30c
* fix: stop check-prerequisites --paths-only from writing feature.json (#3025)
check-prerequisites --paths-only / -PathsOnly is documented as pure,
read-only path resolution, but when SPECIFY_FEATURE_DIRECTORY was set it
called the persist routine and rewrote .specify/feature.json. That dirtied
the working tree and overwrote a pinned feature directory during what should
be a no-op.
Add an explicit opt-out at the resolver boundary instead of a global env
back-channel:
- bash: get_feature_paths accepts a leading --no-persist flag that skips
_persist_feature_json; check-prerequisites.sh passes it in --paths-only mode.
- PowerShell: Get-FeaturePathsEnv gains a -NoPersist switch that skips
Save-FeatureJson; check-prerequisites.ps1 passes it in -PathsOnly mode.
Normal (non-paths-only) invocations are unchanged and still persist the
override, so future sessions without the env var keep working.
Add regression tests asserting --paths-only/-PathsOnly leaves a pinned
feature.json untouched even when the env override differs, plus a guard that
normal mode still persists.
* fix: use ASCII hyphen in common.ps1 comment for PS 5.1 compatibility
The em-dash in the persist comment introduced non-ASCII bytes, failing
test_ps1_file_is_ascii_only which enforces ASCII-only PowerShell sources
for Windows PowerShell 5.1 compatibility.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* test: add PowerShell normal-mode persistence guard (#3025)
Addresses Copilot review feedback on #3190: the bash side had a
`test_normal_mode_still_persists_feature_json` guard, but there was no
symmetric PowerShell test asserting that running check-prerequisites.ps1
*without* -PathsOnly still persists the SPECIFY_FEATURE_DIRECTORY override
into .specify/feature.json.
Add test_ps_normal_mode_still_persists_feature_json, which guards against
accidentally passing -NoPersist unconditionally (or flipping the default)
in a future refactor. Verified it fails when -NoPersist is passed in the
non -PathsOnly branch and passes with the current conditional.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* fix(scripts): send check-prerequisites.ps1 errors to stderr
The validation errors and run-hints in check-prerequisites.ps1 were
written with Write-Output, so they went to stdout. This script is
usually run with -Json and its stdout parsed by the agent, so an error
(e.g. missing plan.md) leaves the parser with an error string instead
of JSON. The bash counterpart already writes these to stderr (>&2), as
do the sibling PowerShell scripts (setup-tasks.ps1, common.ps1's
Get-FeaturePathsEnv). Switch the six error/hint lines to
[Console]::Error.WriteLine so stdout stays clean and the two shells
match.
* test(scripts): assert check-prerequisites errors stay on stderr
Per the #3122 bug assessment, tighten the failure-path tests so they
verify stdout stays clean (empty / valid JSON) and the error text only
appears on stderr, instead of checking the combined stdout+stderr
string. Covers all three PowerShell validation paths (missing feature
dir, missing plan.md, missing tasks.md with -RequireTasks) and the bash
counterpart. The two new error-routing tests fail on the pre-fix
script (errors on stdout) and pass after it.
* 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>
* fix: paths-only skips branch validation, setup-plan preserves existing plan (#2653)
- check-prerequisites.sh/ps1: move branch validation after --paths-only
early exit so --paths-only returns paths without requiring a spec branch
- setup-plan.sh/ps1: skip template copy when plan.md already exists to
prevent overwriting user-authored plans on reruns
- setup-plan.sh: send status messages to stderr in --json mode so stdout
remains parseable JSON
- Add tests for both fixes (bash + PowerShell)
* fix: remove trailing whitespace in PowerShell scripts
* fix: route PS skip message to stderr in -Json mode, add PS JSON assertions
Address review: setup-plan.ps1 Write-Output polluted stdout in -Json
mode when plan.md already existed. Use [Console]::Error.WriteLine()
when -Json is set. Add json.loads + stderr assertions to the PS rerun
test to catch regressions.
* fix: use Test-Path -PathType Leaf for plan existence check
Bare Test-Path matches directories too, which would silently skip plan
creation if a directory existed at the plan.md path.