Skip to content

feat(init): scaffold managed .specify/.gitignore - #4000

Merged
mnriem merged 4 commits into
github:mainfrom
mnriem:mnriem-feat-2304-scaffold-specify-gitignore
Aug 6, 2026
Merged

feat(init): scaffold managed .specify/.gitignore#4000
mnriem merged 4 commits into
github:mainfrom
mnriem:mnriem-feat-2304-scaffold-specify-gitignore

Conversation

@mnriem

@mnriem mnriem commented Aug 6, 2026

Copy link
Copy Markdown
Collaborator

Description

Addresses the recurring "what should I commit under .specify?" question (discussion #2304) by scaffolding a managed .specify/.gitignore at 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 in ConfigManager).

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_infra and 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, and specify init --here --force restores the managed content. Like the rest of .specify/'s shared scripts and templates, it is intentionally left in place by specify integration uninstall (which only removes the uninstalled agent's own files).

Why a nested .specify/.gitignore and not a root-level merge?

  • Functionally complete: git check-ignore confirms both target paths are ignored; root placement would only add discoverability, not correctness.
  • Keeps the manifest model clean: the installer tracks whole-file SHA-256 hashes, so a file we fully own fits that contract exactly. Merging into a user-owned root .gitignore would 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.
  • Consistent with existing behavior: the CLI today only suggests adding the agent folder to .gitignore (see init.py) and never auto-edits the root file. The only marker-block-in-a-user-file pattern is the deliberately opt-in agent-context extension.

Note (independent of file location): a .gitignore does not untrack files already committed — a feature.json committed before upgrading still needs a one-time git rm --cached .specify/feature.json.

Testing

  • Tested locally with uv run specify --help
  • Ran existing tests with uv sync && uv run pytest (ran the shared-infra / init / manifest suites: 130 passed, 2 skipped)
  • Tested with a sample project (if applicable)

New tests/test_shared_infra_gitignore.py covers: file creation + manifest tracking, real git check-ignore behaviour for the target paths (and that a shareable file like constitution.md is not ignored), user-edit preservation, and --force restore. Docs updated in docs/reference/core.md.

AI Disclosure

  • I did use AI assistance (describe below)

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.

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
Copilot AI balanced review requested due to automatic review settings August 6, 2026 13:26

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

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

Comment thread src/specify_cli/shared_infra.py Outdated
Comment thread docs/reference/core.md Outdated
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
Copilot AI review requested due to automatic review settings August 6, 2026 13:31
@mnriem

mnriem commented Aug 6, 2026

Copy link
Copy Markdown
Collaborator Author

Good catch — the uninstall claim was wrong. specify integration uninstall loads the per-integration manifest (<key>.manifest.json), not the shared-infra speckit.manifest.json, and shared infrastructure is deliberately preserved (per test_uninstall_preserves_shared_infra). So .specify/.gitignore is left in place, not removed.

Fixed in 1a3cf18: reworded the code comment (shared_infra.py) and the docs/reference/core.md note to state the actual behavior — tracked in the shared-infra manifest, --force refreshes it, edits preserved on re-init, and it's intentionally left in place by integration uninstall. The PR description was updated to match. No functional change.


Posted on behalf of @mnriem by GitHub Copilot (model: Claude Opus 4.8, autonomous). Commits carry an Assisted-by trailer.

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

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/, and extensions/.registry, and it recognizes *-config.local.yml as 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 the git check-ignore coverage 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
Copilot AI review requested due to automatic review settings August 6, 2026 13:38

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

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 .gitignore so 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)
Copilot AI review requested due to automatic review settings August 6, 2026 13:58
@mnriem

mnriem commented Aug 6, 2026

Copy link
Copy Markdown
Collaborator Author

Review round: CI failures fixed (commit f37accd)

The failing pytest jobs were the integration test_complete_file_inventory_sh/_ps tests, which assert an exact match of every file specify init produces. Adding the new managed .specify/.gitignore introduced an unexpected extra file. I updated the affected expected-inventory lists (base markdown/skills/toml/yaml builders plus the inline lists in the cline, generic, and copilot tests, covering both sh and ps script types) to include .specify/.gitignore.

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.

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

Suppressed comments (1)

src/specify_cli/shared_infra.py:29

  • A slashless .gitignore pattern 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 .gitignore directory (and ideally assert a nested feature.json is not ignored).
feature.json
  • Files reviewed: 10/10 changed files
  • Comments generated: 0 new
  • Review effort level: Balanced

@mnriem
mnriem requested a balanced review from Copilot August 6, 2026 14:08
@mnriem
mnriem merged commit 40037b1 into github:main Aug 6, 2026
14 checks passed
@mnriem
mnriem deleted the mnriem-feat-2304-scaffold-specify-gitignore branch August 6, 2026 14:08

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: 10/10 changed files
  • Comments generated: 0 new
  • Review effort level: Balanced

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.

2 participants