feat(init): scaffold managed .specify/.gitignore - #4000
Conversation
Write a manifest-tracked `.specify/.gitignore` during shared-infra install so machine-local Spec Kit state stays out of version control while everything else under `.specify/` remains shareable: - `feature.json` — the current-feature pointer, rewritten on every feature switch (per-checkout state, not something to share). - `extensions/*/local-config.yml` — per-machine extension config overrides. The file is routed through the same overwrite/skip/preserve policy as shared templates: `--force` refreshes it, user edits are preserved on re-init, and uninstall removes it via the manifest. Addresses github#2304. Assisted-by: GitHub Copilot (model: Claude Opus 4.8, autonomous) Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: 98faefd1-9fc8-48fc-bd25-d4f3ccbb2ab9
There was a problem hiding this comment.
Pull request overview
Adds managed Git ignore defaults for machine-local Spec Kit state.
Changes:
- Scaffolds and manifest-tracks
.specify/.gitignore. - Tests ignore behavior and overwrite preservation.
- Documents the defaults and release change.
Show a summary per file
| File | Description |
|---|---|
src/specify_cli/shared_infra.py |
Installs the managed Git ignore file. |
tests/test_shared_infra_gitignore.py |
Tests creation, patterns, and refresh behavior. |
docs/reference/core.md |
Documents version-control behavior. |
CHANGELOG.md |
Records the feature. |
Review details
Tip
Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
- Files reviewed: 4/4 changed files
- Comments generated: 2
- Review effort level: Balanced
The file is tracked in the shared-infra manifest (speckit.manifest.json), not the per-integration manifest that `specify integration uninstall` loads. Shared infrastructure is deliberately preserved on uninstall (see test_uninstall_preserves_shared_infra), so `.specify/.gitignore` is left in place rather than removed. Reword the code comment and core.md note to state the actual behavior; keep the true benefits (force-refresh and preserve-on-edit). Assisted-by: GitHub Copilot (model: Claude Opus 4.8, autonomous) Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: 98faefd1-9fc8-48fc-bd25-d4f3ccbb2ab9
|
Good catch — the uninstall claim was wrong. Fixed in 1a3cf18: reworded the code comment ( Posted on behalf of @mnriem by GitHub Copilot (model: Claude Opus 4.8, autonomous). Commits carry an |
There was a problem hiding this comment.
Review details
Suppressed comments (1)
src/specify_cli/shared_infra.py:32
- This list is incomplete for the extension system’s machine-local state. The CLI also creates
extensions/.cache/,extensions/.backup/, andextensions/.registry, and it recognizes*-config.local.ymlas local overrides (src/specify_cli/extensions/__init__.py:632-670, 918-924, 2209-2213, 2785-2815, 3455-3459). The extension user guide explicitly marks all of these as “Don’t commit” (extensions/EXTENSION-USER-GUIDE.md:855-875). With the managed file as written, normal extension operations therefore expose caches, installation state, backups, and one supported local-config format to Git. Add ignore rules for those paths and extend thegit check-ignorecoverage accordingly.
# Per-machine extension config overrides.
extensions/*/local-config.yml
- Files reviewed: 4/4 changed files
- Comments generated: 0 new
- Review effort level: Balanced
CHANGELOG.md is auto-generated; do not hand-edit it. Assisted-by: GitHub Copilot (model: Claude Opus 4.8, autonomous) Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: 98faefd1-9fc8-48fc-bd25-d4f3ccbb2ab9
There was a problem hiding this comment.
Review details
Suppressed comments (1)
src/specify_cli/shared_infra.py:29
- This unanchored basename pattern applies at every depth below
.specify, so it also ignores shareable files such as.specify/extensions/<id>/feature.json, contrary to the stated “everything else” policy. Anchor it to the directory containing this.gitignoreso only the current-feature pointer is excluded.
feature.json
- Files reviewed: 3/3 changed files
- Comments generated: 0 new
- Review effort level: Balanced
The complete-file-inventory tests assert an exact match of every file produced by `specify init`. Now that shared infra scaffolds a managed `.specify/.gitignore`, add it to the expected inventories so the exact-match assertions pass on both sh and ps script types. Assisted-by: GitHub Copilot (model: Claude Opus 4.8, autonomous)
|
Review round: CI failures fixed (commit f37accd) The failing pytest jobs were the integration ruff was already green; only the inventory expectations needed updating. Full suite passes locally (2443 passed, 3 skipped). CHANGELOG.md is untouched (auto-generated). Posted on behalf of @mnriem by GitHub Copilot (model: Claude Opus 4.8), acting autonomously. |
There was a problem hiding this comment.
Review details
Suppressed comments (1)
src/specify_cli/shared_infra.py:29
- A slashless
.gitignorepattern matches that basename at every depth, so this also ignores paths such as.specify/extensions/example/feature.json, contrary to the stated policy that only the root current-feature pointer is local and everything else remains shareable. Anchor the pattern to this.gitignoredirectory (and ideally assert a nestedfeature.jsonis not ignored).
feature.json
- Files reviewed: 10/10 changed files
- Comments generated: 0 new
- Review effort level: Balanced
Description
Addresses the recurring "what should I commit under
.specify?" question (discussion #2304) by scaffolding a managed.specify/.gitignoreat init time. It excludes machine-local state while leaving everything shareable tracked, so teams get sensible defaults out of the box instead of reverse-engineering which files are session-local.Ignored (machine/session-local):
feature.json— the current-feature pointer, rewritten on every feature switch (per-checkout state; sharing it only causes churn/merge noise).extensions/*/local-config.yml— per-machine extension config overrides (already documented as gitignored inConfigManager).Everything else under
.specify/(constitution, templates, scripts,extensions.yml, project-level extension config) stays tracked so a team shares a consistent setup.Implementation. The file is written in
install_shared_infraand routed through the same overwrite/skip/preserve policy as shared templates: it is tracked in the shared-infrastructure manifest (speckit.manifest.json), user edits are preserved on plain re-init, andspecify init --here --forcerestores the managed content. Like the rest of.specify/'s shared scripts and templates, it is intentionally left in place byspecify integration uninstall(which only removes the uninstalled agent's own files).Why a nested
.specify/.gitignoreand not a root-level merge?git check-ignoreconfirms both target paths are ignored; root placement would only add discoverability, not correctness..gitignorewould need a delimited managed block plus custom idempotent-merge/surgical-removal logic, and any unrelated user edit would change the file hash and break the manifest's preserve/refresh behavior..gitignore(seeinit.py) and never auto-edits the root file. The only marker-block-in-a-user-file pattern is the deliberately opt-inagent-contextextension.Note (independent of file location): a
.gitignoredoes not untrack files already committed — afeature.jsoncommitted before upgrading still needs a one-timegit rm --cached .specify/feature.json.Testing
uv run specify --helpuv sync && uv run pytest(ran the shared-infra / init / manifest suites: 130 passed, 2 skipped)New
tests/test_shared_infra_gitignore.pycovers: file creation + manifest tracking, realgit check-ignorebehaviour for the target paths (and that a shareable file likeconstitution.mdis not ignored), user-edit preservation, and--forcerestore. Docs updated indocs/reference/core.md.AI Disclosure
Implemented autonomously by GitHub Copilot (model: Claude Opus 4.8) on behalf of @mnriem — code, tests, and docs generated by the agent and reviewed before submission.