feat(scripts): add SPECIFY_NO_PERSIST env var to suppress feature.json writes (#4128) - #4129
feat(scripts): add SPECIFY_NO_PERSIST env var to suppress feature.json writes (#4128)#4129chelsealong wants to merge 2 commits into
Conversation
…n writes (github#4128) Multi-agent setups running several Spec Kit script invocations concurrently against the same checkout each set their own SPECIFY_FEATURE_DIRECTORY. Every invocation that omits --no-persist (e.g. setup-plan, setup-tasks) still writes that value to the shared .specify/feature.json, so agents can clobber each other's pinned feature directory. SPECIFY_NO_PERSIST=1|true is the environment-level equivalent of --no-persist, letting an orchestrator suppress that write across every call in the process tree without patching each call site. Assisted-by: Claude Code (model: claude-sonnet-5, autonomous)
|
Issue author here. Reviewed the diff — the implementation across all three language variants (bash/ps/python) matches the proposal in #4128 exactly, and the clobber-prevention test ( We backported the bash change locally and ran 8 end-to-end scenarios against
Happy to contribute a follow-up |
- Apply upstream PR #4129 patch to common.sh: add SPECIFY_NO_PERSIST env var as process-level equivalent of --no-persist - Update speckit-multiagent.md rule with NO_PERSIST documentation, isolation diagram, and 8/8 local verification results - Ref: github/spec-kit#4128 (our RFC), github/spec-kit#4129 (impl)
There was a problem hiding this comment.
Pull request overview
Adds an environment-level opt-out for feature-context persistence across Bash, PowerShell, and Python scripts.
Changes:
- Implements
SPECIFY_NO_PERSIST. - Adds cross-script regression tests.
- Documents the environment variable.
Show a summary per file
| File | Description |
|---|---|
scripts/bash/common.sh |
Adds Bash persistence suppression. |
scripts/powershell/common.ps1 |
Adds PowerShell persistence suppression. |
scripts/python/common.py |
Adds Python persistence suppression. |
tests/test_specify_no_persist.py |
Tests default, suppression, and pin-preservation behavior. |
docs/reference/core.md |
Documents the new variable. |
Review details
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
- Files reviewed: 5/5 changed files
- Comments generated: 1
- Review effort level: Balanced
| | `SPECIFY_INIT_DIR` | Target a member project from outside its directory (e.g. a monorepo root) without `cd`, for non-interactive / CI use. Set it to the **project root** — the directory *containing* `.specify/` (relative paths resolve against the current directory). The path must exist and contain `.specify/`, otherwise the command errors and does **not** fall back to the current directory. Resolved once in the core root helper (`get_repo_root` in Bash, `Get-RepoRoot` in PowerShell), so it is honored by the core feature scripts (`/speckit.plan`, `/speckit.tasks`, …) and the Git extension's feature-branch creation, which inherit it. The `specify` CLI applies the **same** validation rules to every project-scoped subcommand (`specify integration …`, `specify extension …`, `specify workflow …`, `specify preset …`, and the rest that operate on a `.specify/` project), so those can target a member project too. When unset, Bash/PowerShell helpers keep their existing upward search; the `specify` CLI keeps its project-scoped resolver cwd-only unless a command explicitly defines broader detection (for example, bundle commands). | | ||
| | `SPECIFY_FEATURE_DIRECTORY` | Override the active feature directory *within* the resolved project (takes precedence over `.specify/feature.json`). Relative paths resolve under the project root. Combine with `SPECIFY_INIT_DIR` to pick both the project and the feature non-interactively. | | ||
| | `SPECIFY_FEATURE` | Override feature detection for non-Git repositories. Set to the feature directory name (e.g., `001-photo-albums`) to work on a specific feature when not using Git branches. Must be set in the context of the agent prior to using `/speckit.plan` or follow-up commands. | | ||
| | `SPECIFY_NO_PERSIST` | Set to `1` or `true` to stop every core script from writing `.specify/feature.json`, even when it would otherwise persist `SPECIFY_FEATURE_DIRECTORY` on read. Useful when multiple agents run concurrently against the same checkout, each with its own `SPECIFY_FEATURE_DIRECTORY`: without it, each invocation's persist step can overwrite another agent's pinned feature directory. | |
… feature.json write The three create-new-feature variants write .specify/feature.json directly rather than through get_feature_paths(), so they ignored SPECIFY_NO_PERSIST entirely, leaving the "stops every core script from writing feature.json" guarantee in docs/reference/core.md broader than the implementation.
|
Fixed: |
mnriem
left a comment
There was a problem hiding this comment.
Please address Copilot feedback and adjust the variable name to SPECIFY_FEATURE_NO_PERSIST
Problem
Fixes #4128.
When multiple Spec Kit script invocations run concurrently against the same
checkout (e.g. multiple agents/subagents each working on a different
feature), each one typically sets its own
SPECIFY_FEATURE_DIRECTORY. Mostcore scripts (
setup-plan,setup-tasks) callget_feature_paths()without--no-persist, so every invocation persists itsSPECIFY_FEATURE_DIRECTORYto the shared
.specify/feature.json— the last writer wins, silentlyoverwriting another process's pinned feature directory.
--no-persist(added for #3025) already exists as a per-call opt-out, butit's a parameter each call site has to pass — scripts that don't know they
need it (like
setup-plan/setup-tasks) never do.Fix
Add
SPECIFY_NO_PERSISTas the environment-level equivalent of--no-persist/-NoPersist/no_persist=True. When set to1ortrue,it suppresses the
.specify/feature.jsonwrite inget_feature_paths()regardless of whether the calling script passes the per-call flag. An
orchestrator can set it once for a whole process tree so no script
invocation in that tree can write
feature.json, even ones that don't knowto opt out themselves.
Implemented identically in all three script variants per the project's
parity rule (
AGENTS.md):scripts/bash/common.sh—get_feature_paths()scripts/powershell/common.ps1—Get-FeaturePathsEnvscripts/python/common.py—get_feature_paths()Also documented the new variable in
docs/reference/core.md's environmentvariables table, next to
SPECIFY_FEATURE_DIRECTORY.Fully backward compatible: existing behavior when
SPECIFY_NO_PERSISTisunset is unchanged in every scenario.
Test plan
Added
tests/test_specify_no_persist.py, covering all three scriptvariants via
setup-plan(which callsget_feature_paths()without--no-persist, so it's a script that previously had no way to opt out):SPECIFY_FEATURE_DIRECTORYset, noSPECIFY_NO_PERSIST→feature.jsonis written (existing behavior, unchanged).SPECIFY_NO_PERSIST=1/true→feature.jsonwrite is suppressed, forbash, PowerShell, and Python.
specs/001-a, agent B runs withSPECIFY_FEATURE_DIRECTORY=specs/002-bandSPECIFY_NO_PERSIST=1→feature.jsonstill points at agent A'sspecs/001-a(the pin isn'tclobbered).
Confirmed the new tests fail without the fix (
git checkout HEAD~1 -- scripts/bash/common.sh scripts/powershell/common.ps1 scripts/python/common.py,then re-ran):
And pass with the fix restored:
Also ran:
shellcheck --severity=erroron all tracked.shfiles — clean.markdownlint-cli2ondocs/reference/core.md— same 2 pre-existingMD028findings as on unmodifiedmain(unrelated blockquotes furtherdown the file, confirmed by diffing lint output before/after).
pytest tests/ -q(6944 tests): 6921 passed, 9 skipped, 4 pre-existingfailures unrelated to this change (
test_presets.py,test_resolve_template_python_parity.py— template-composition/localeedge cases), confirmed present on
mainbefore this change too.No manual agent-driven testing was done for this change since it's a
core-script/docs-only change with no slash-command template modifications.
AI disclosure
This PR was written primarily by an autonomous AI coding agent (Claude Code,
Claude Sonnet 5), including the code change, tests, and this description.