Skip to content

Collapsed the duplicate gitignore negations an older CLI left behind - #30

Merged
dzykovic merged 4 commits into
mainfrom
fix/gitignore-duplicate-negations
Sep 13, 2026
Merged

dzykovic merged 4 commits into
mainfrom
fix/gitignore-duplicate-negations

Conversation

@dzykovic

Copy link
Copy Markdown
Contributor

Summary

Closes #24.

0.12.1 stopped .gitignore growing by one negation chain per alignment, but nothing removed the chains the defect had already written: a project initialized under 0.12.0 or earlier keeps one copy of !<dir>/ per run it made. The issue measured five copies surviving three further alignments untouched.

Clearing them is safe by construction — Git applies the last matching rule, so identical earlier copies of a line decide nothing. Alignment now keeps only the last occurrence of each line of the chain, and only below the policy's own header: a line the project wrote before Intelligence touched the file is never rewritten. Afterwards the include path is re-probed, and a collapse that somehow changed what Git ignores is rolled back rather than committed.

Measured on the issue's fixture (five !.claude/, plus a hand-written copy in the project's own section above the header):

before:       15 lines, 5 x '!.claude/'
after align:  10 lines, 2 x '!.claude/'   (one managed, one hand-written)
again:        10 lines, 2 x '!.claude/'   (idempotent)

The rewrite is plain Bash rather than awk. On Windows awk reads in text mode and handed back every line stripped of its CR, which would have rewritten a CRLF .gitignore as LF; a CR is significant to Git. Lines the collapse does not remove keep their bytes, a missing final newline included.

Type of change

  • CLI lifecycle or package behavior
  • Sync engine or adapter
  • Rule, agent, or skill content
  • Documentation
  • Refactor with no behavior change

Public CLI check

  • Uses only the established groups: init, sync, update, upgrade, package, source, adapter, status, registry
  • Keeps preview/apply behavior explicit for planned writes
  • Covers automatic project alignment and CI refusal when project schema/content can change
  • Keeps fresh-clone package restoration behind intelligence sync

Verification

  • bash cli/tests/verify.sh reports verify ok and skipped nothing this change needed
  • intelligence status --check succeeds in the relevant fixture/project
  • A second intelligence sync produces no unexpected diff
  • Adapter changes were checked against the real tool format and preserve hand-authored sibling files
  • User-facing docs and examples use the current public command groups

Versioned artifacts

  • The PR records its change under one concrete CHANGELOG.md version; there is no [Unreleased] section or duplicated release date
  • The current engine/VERSION release state was checked: append while pending, or select the next SemVer after publication
  • If a new version was selected, every example's schema_version and exact @ainova-systems/sync pin changed with it
  • No stable release is implied or published without owner approval

Notes for reviewers

Version. v0.16.0 is published, so this selects 0.16.1 and moves engine/VERSION, every example's schema_version and every exact sync pin with it.

Why the header is the boundary. Everything this policy appends lands after its header line, so that line is also the limit of what it may rewrite. A hand-written !.claude/ above it stays exactly where the project put it — the fixture in both suites asserts this.

Why not awk. The first draft used a two-pass awk and turned a CRLF fixture into LF. Bash read/printf round-trips the bytes, stays inside the Bash 3.2 floor shell.md records (indexed arrays and arithmetic for, no associative arrays), and the file is small enough that the loop costs nothing measurable.

Coverage. New cli/tests/unit-gitignore.sh (registered in SUITES) covers the collapse itself: residue onto the last occurrence, the hand-written line above the header, a file with nothing to collapse left byte-identical, a file without the header left alone, CRLF endings, a missing final newline, and the full repair converging through gitignore_add_effective_include. e2e-negative gains the issue's own scenario end to end through init --no-sync, next to the existing idempotence assertions. Against main that repair case leaves 3 copies where the fix leaves 1.

No adapter changes, and no behavior change for a project that never ran 0.12.0 — its file has one copy already, and the collapse then writes nothing.

Deployment notes

None.

0.12.1 stopped `.gitignore` growing by one negation chain per alignment, but
nothing removed the chains already written: a project initialized under 0.12.0
or earlier keeps one copy per run it made. Identical ignore lines decide
nothing — Git applies the last match — so this is residue, and clearing it is
safe by construction.

Alignment now keeps only the last occurrence of each line of the chain, and
only below the policy's own header: a line the project wrote before
Intelligence touched the file is never rewritten, and the probe path is
re-checked afterwards so a collapse that somehow changed what Git ignores is
rolled back instead of committed.

The rewrite is plain Bash rather than awk, which reads in text mode on Windows
and would have rewritten a CRLF file as LF. Lines it does not remove keep their
bytes, including a missing final newline.
Copilot AI lite review requested due to automatic review settings September 13, 2026 14:39

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Changes recommended

Unresolved CRLF handling, macOS test portability, and stale test-suite documentation findings remain.

Get a fresh assessment by requesting another Copilot review.

Pull request overview

This PR removes legacy duplicate .gitignore negation chains during CLI alignment, adds regression coverage, and synchronizes release artifacts for 0.16.1.

Changes:

  • Added header-scoped duplicate collapsing with ignore-state verification.
  • Added unit and end-to-end coverage.
  • Updated versions, locks, examples, and changelog.
File summaries
File Summary / final review note
intelligence.yaml Updated schema and sync package versions.
intelligence.lock Updated engine and package pins.
examples/with-remote-skills/intelligence.yaml Updated schema and sync pins.
examples/platform-with-submodules/intelligence.yaml Updated schema and sync pins.
examples/platform-with-submodules/intelligence.lock Updated engine and package pins.
examples/go-api/intelligence.yaml Updated schema and sync pins.
examples/go-api/intelligence.lock Updated engine and package pins.
examples/go-api-with-pi-and-codex/intelligence.yaml Updated schema and sync pins.
examples/go-api-with-pi-and-codex/intelligence.lock Updated engine and package pins.
examples/go-api-with-opencode/intelligence.yaml Updated schema and sync pins.
examples/go-api-with-opencode/intelligence.lock Updated engine and package pins.
examples/go-api-with-antigravity/intelligence.yaml Updated schema and sync pins.
examples/go-api-with-antigravity/intelligence.lock Updated engine and package pins.
examples/dotnet-api-with-react-frontend/intelligence.yaml Updated schema and sync pins.
examples/dotnet-api-with-react-frontend/intelligence.lock Updated engine and package pins.
examples/cli-project/intelligence.yaml Updated schema and sync pins.
engine/VERSION Bumped the release version to 0.16.1.
cli/tests/verify.sh Registered the new unit suite. Nit (1 vote): docs/cli.md:460 has a stale test-suite list.
cli/tests/unit-gitignore.sh Added .gitignore collapse, CRLF, and newline-preservation coverage.
cli/tests/e2e-negative.sh Added legacy-residue alignment coverage. Critical (3 votes): the GNU-specific sed insertion fails with macOS BSD sed.
cli/lib/gitignore.sh Added duplicate collapse and rollback logic. Moderate (3 votes): the public path can append LF copies to CRLF files instead of preserving line endings.
CHANGELOG.md Documented the fix in 0.16.1.
Review details

Suppressed comments (1)

cli/tests/verify.sh:24

  • Adding unit-gitignore to the runner makes docs/cli.md:460 stale: its enumerated tests scope still claims the runner executes a list that omits this suite. Please update that documented suite list in the same change so the new verification gate is discoverable and the documentation matches the runner.
SUITES=(unit-semver unit-manifest unit-gitignore unit-release unit-fetch unit-upgrade e2e-sources e2e-packages e2e-lifecycle e2e-negative e2e-lock-validation e2e-compat)
  • Files reviewed: 15/22 changed files
  • Comments generated: 2
  • Review effort level: Lite

💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

Comment thread cli/tests/e2e-negative.sh Outdated
Comment thread cli/lib/gitignore.sh
The new residue scenario used `sed -i.bak '1i <text>'`, which BSD sed on macOS
rejects — the hand-written line was never added there, so CI asserted against a
file the test had not actually built and reported the collapse as the failure.
This file already had the portable pattern a few hundred lines up; it now uses
it, and shell.md records the BSD userland trap next to the Bash 3.2 one.
A CR belongs to the line Git matches, and presence was tested with it attached:
on Linux and macOS every existing entry looked absent, so alignment appended an
LF copy of it on every run and a CRLF file grew forever — the growth 0.12.1 was
supposed to have ended. MSYS hid it, because its grep reads in text mode and
never sees a CR.

Presence now matches with or without the CR, an append uses the ending the file
already has, and the tail comparison ignores it. The header write and the
publisher ignore files go through the same helpers.

Line endings are held in variables rather than written as $'\r' where they are
used: bash does not expand that form inside a command substitution, so the same
expression silently stops stripping the CR depending on the caller. Two CR
assertions in the new suite were false-green for exactly that reason and now
count in bash, which is also the only way to see a CR under MSYS.
…where

`grep -Fxc` answered differently per platform for the same file: under MSYS it
reads in text mode and never sees a CR, while on Linux and macOS the CR is part
of the line, so the CRLF assertions passed locally and reported 0 on both CI
legs. The behaviour under test was already correct — only the measurement was
platform-dependent.

Counting now strips the CR the way the policy itself does, and shell.md records
that anything deciding on a CR belongs in Bash rather than a text-mode tool.
@dzykovic dzykovic added the ai:ready-to-merge CI green, reviews resolved, awaiting owner approval label Sep 13, 2026
@dzykovic
dzykovic merged commit 365ec21 into main Sep 13, 2026
21 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

ai:ready-to-merge CI green, reviews resolved, awaiting owner approval

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Alignment leaves the duplicate .gitignore negations 0.12.0 already wrote

2 participants