Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,12 @@ All notable changes to Intelligence are recorded here.

Legacy Intelligence Sync history remains in its [archive](https://github.com/ainova-systems/intelligence-sync/blob/main/CHANGELOG.md).

## [0.19.0]

### Fixed

- `status --check` no longer reports `.gitignore` patterns as missing when the file has CRLF line endings on Linux and macOS; it decides presence the way the `.gitignore` writer does, so `intelligence init` clears the finding (#45).

## [0.18.1]

### Changed
Expand Down
7 changes: 5 additions & 2 deletions cli/internal/check.sh
Original file line number Diff line number Diff line change
Expand Up @@ -150,14 +150,17 @@ while IFS= read -r target; do
&& [ "$(is_target_enabled "$manifest" "$value")" != "1" ]; then
warn "enabled adapter '$target' requires enabled adapter '$value'"
fi
# Presence is the writer's own test: a line `init` treats as present
# and leaves alone must not be reported missing here, or the warning
# can never clear — a CRLF .gitignore on Linux and macOS did exactly that.
if [ "$(is_target_enabled "$manifest" "$target")" = "1" ]; then
case "$kind" in
ignore)
grep -Fqx -- "$value" "$IP_ROOT/.gitignore" 2>/dev/null \
ignore_file_has_line "$IP_ROOT/.gitignore" "$value" \
|| warn "adapter '$target' Git policy is missing '$value'"
;;
include)
if ! grep -Fqx -- "!$value" "$IP_ROOT/.gitignore" 2>/dev/null; then
if ! ignore_file_has_line "$IP_ROOT/.gitignore" "!$value"; then
warn "adapter '$target' Git policy is missing '!$value'"
elif git -C "$IP_ROOT" rev-parse --is-inside-work-tree >/dev/null 2>&1; then
if git -C "$IP_ROOT" check-ignore -q --no-index -- "$value"; then
Expand Down
32 changes: 20 additions & 12 deletions cli/lib/gitignore.sh
Original file line number Diff line number Diff line change
Expand Up @@ -13,12 +13,13 @@ IS_CR=$'\r'
IS_LF=$'\n'
IS_CRLF=$'\r\n'

# An ignore file checked out on Windows can hold CRLF, and a CR belongs to the
# pattern Git matches — so these two helpers decide presence ignoring it, and
# append in whatever ending the file already uses. Comparing with the CR
# attached made every existing line look absent on Linux and macOS (where it is
# part of the line, unlike under MSYS), so each alignment appended an LF copy of
# a line that was already there and the file grew forever.
# An ignore file checked out on Windows can hold CRLF. Git drops one trailing
# CR from each line before matching, so `foo` and `foo<CR>` are the same rule:
# these helpers decide presence ignoring that CR, and append in whatever ending
# the file already uses. Comparing with the CR attached made every existing
# line look absent on Linux and macOS (where it is part of the line, unlike
# under MSYS), so each alignment appended an LF copy of a line that was already
# there and the file grew forever.

# ignore_file_eol_var <file> — set IS_IGNORE_EOL from the file's first line.
ignore_file_eol_var() {
Expand All @@ -32,12 +33,19 @@ ignore_file_eol_var() {
}

# ignore_file_has_line <file> <line> — true when the file lists exactly this
# line, with or without a trailing CR.
# line, with or without a trailing CR. The one presence test for an ignore
# file: the writer and `status --check` both ask it, so a line one appends is a
# line the other reports present. Decided in bash, because an exact-line grep
# answers per platform — MSYS grep never sees the CR, Linux and macOS grep keep
# it as part of the line.
ignore_file_has_line() {
local file="$1" line="$2"
local file="$1" line="$2" entry=""
[ -f "$file" ] || return 1
grep -Fqx -- "$line" "$file" 2>/dev/null && return 0
grep -Fqx -- "$line$IS_CR" "$file" 2>/dev/null
while IFS= read -r entry || [ -n "$entry" ]; do
[ "${entry%"$IS_CR"}" = "$line" ] && return 0
entry=""
done < "$file"
return 1
}

# ignore_file_append_line <file> <line> — append in the file's own ending.
Expand Down Expand Up @@ -93,8 +101,8 @@ gitignore_collapse_duplicates() {

# Read and write the file's own bytes, without awk: on Windows it reads in
# text mode and would hand back every line stripped of its CR, rewriting a
# CRLF file as LF. A line's CR is significant to Git, and either way the
# lines this function does not remove must survive byte-for-byte.
# CRLF file as LF. The lines this function does not remove must survive
# byte-for-byte, whatever their ending.
line=""
while IFS= read -r line; do
lines[${#lines[@]}]="$line"
Expand Down
43 changes: 43 additions & 0 deletions cli/tests/e2e-negative.sh
Original file line number Diff line number Diff line change
Expand Up @@ -909,6 +909,49 @@ xok "" "$PROJ" init --no-sync
chk cmp -s "$OUT/gitignore-collapsed" "$PROJ/.gitignore"
xok "all good" "$PROJ" status --check

# A Windows checkout can hold the .gitignore as CRLF. Git drops each line's
# trailing CR and so does the writer's presence test, but `status --check`
# compared whole lines with grep: on Linux and macOS it reported every present
# pattern missing while `init` found nothing to add, so the warning never
# cleared. The fixture is written and inspected in bash — under MSYS grep never
# sees a CR, and a grep-based assertion here would pass measuring nothing.
CR=$'\r'
# gi_crlf_shape — "crlf <n>" when every line of the project .gitignore ends in
# CR, "mixed <n>" otherwise; <n> counts the lines that are CLAUDE.md.
gi_crlf_shape() {
local l="" shape=crlf n=0
while IFS= read -r l || [ -n "$l" ]; do
case "$l" in *"$CR") ;; *) shape=mixed ;; esac
if [ "${l%"$CR"}" = 'CLAUDE.md' ]; then n=$((n + 1)); fi
l=""
done < "$PROJ/.gitignore"
printf '%s %s' "$shape" "$n"
}
cp "$PROJ/.gitignore" "$OUT/gitignore-lf"
gi_line=""
while IFS= read -r gi_line || [ -n "$gi_line" ]; do
# One managed pattern left out, so the check has a real absence to report.
[ "$gi_line" = 'CLAUDE.md' ] || printf '%s\r\n' "$gi_line"
gi_line=""
done < "$OUT/gitignore-lf" > "$PROJ/.gitignore"
[ "$(gi_crlf_shape)" = "crlf 0" ] \
|| { echo "FAIL: the CRLF .gitignore fixture is '$(gi_crlf_shape)', want 'crlf 0'"; fail=1; }
xfail "Git policy is missing 'CLAUDE.md'" "$PROJ" status --check
gi_missing="$(printf '%s\n' "$OUTPUT" | grep -cF 'Git policy is missing' || true)"
if [ "$gi_missing" != 1 ]; then
echo "FAIL: status --check reports $gi_missing patterns missing from a CRLF .gitignore, want only 'CLAUDE.md'"
printf '%s\n' "$OUTPUT" | grep -F 'Git policy is missing' | head -6 || true
fail=1
fi
xok "" "$PROJ" init --no-sync
[ "$(gi_crlf_shape)" = "crlf 1" ] \
|| { echo "FAIL: init left the CRLF .gitignore '$(gi_crlf_shape)', want 'crlf 1'"; fail=1; }
xok "all good" "$PROJ" status --check
cp "$PROJ/.gitignore" "$OUT/gitignore-crlf"
xok "" "$PROJ" init --no-sync
chk cmp -s "$OUT/gitignore-crlf" "$PROJ/.gitignore"
cp "$OUT/gitignore-lf" "$PROJ/.gitignore"

# AGENTS.md-dependent targets cannot be enabled into a manifest the engine
# would reject, and agents cannot be disabled while one remains enabled.
xok "disabled: claude" "$PROJ" adapter disable claude
Expand Down
24 changes: 21 additions & 3 deletions cli/tests/unit-gitignore.sh
Original file line number Diff line number Diff line change
Expand Up @@ -131,9 +131,9 @@ ignore_file_eol_var "$D/.gitignore"
# Compared through a variable: `$'\r\n'` written inside a command substitution
# is not expanded by bash, so the check would silently always fail.
[ "$IS_IGNORE_EOL" = "$CRLF" ] || { echo "FAIL: CRLF ending not detected"; fail=1; }
# A CR belongs to the pattern Git matches, so presence must ignore it while the
# append keeps it: comparing with the CR attached made every existing line look
# absent on Linux and macOS, and alignment appended an LF copy every run.
# Git drops a line's trailing CR, so presence must ignore it while the append
# keeps it: comparing with the CR attached made every existing line look absent
# on Linux and macOS, and alignment appended an LF copy every run.
chk ignore_file_has_line "$D/.gitignore" '!.claude/'
chk ignore_file_has_line "$D/.gitignore" "$HEADER"
gitignore_add_line "$D" '!.claude/'
Expand All @@ -142,6 +142,24 @@ gitignore_add_line "$D" '.codex/*'
is "new line appended as CRLF" "5" "$(crlf_lines "$D/.gitignore")"
is "no LF-only line introduced" "5" "$(total_lines "$D/.gitignore")"

echo "== presence is one exact line, ignoring only its trailing CR =="
D="$(fixture presence)"
printf '.claude/*\r\n!.claude/settings.json\r\nnested/dir/\r\r\nlast-line\r' > "$D/.gitignore"
chk ignore_file_has_line "$D/.gitignore" '.claude/*'
chk ignore_file_has_line "$D/.gitignore" '!.claude/settings.json'
# A final line without a newline is still a line, with or without its CR.
chk ignore_file_has_line "$D/.gitignore" 'last-line'
# Exact and literal: no prefix, substring or glob reading of either side.
chknot ignore_file_has_line "$D/.gitignore" '.claude/'
chknot ignore_file_has_line "$D/.gitignore" '!.claude/settings'
chknot ignore_file_has_line "$D/.gitignore" '.c*'
chknot ignore_file_has_line "$D/.gitignore" '.claude/x'
# Git drops exactly one CR, so a second one is part of that rule.
chknot ignore_file_has_line "$D/.gitignore" 'nested/dir/'
chknot ignore_file_has_line "$OUT/absent/.gitignore" '.claude/*'
printf 'last-line' > "$D/.gitignore"
chk ignore_file_has_line "$D/.gitignore" 'last-line'

echo "== repeated alignment leaves a CRLF file alone =="
D="$(fixture crlf-stable)"
printf '%s\r\n.claude/*\r\n!.claude/\r\n!.claude/settings.json\r\n' "$HEADER" > "$D/.gitignore"
Expand Down
2 changes: 1 addition & 1 deletion engine/VERSION
Original file line number Diff line number Diff line change
@@ -1 +1 @@
0.18.1
0.19.0
4 changes: 2 additions & 2 deletions examples/cli-project/intelligence.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@
project:
name: cli-project

schema_version: "0.18.1"
schema_version: "0.19.0"

# Package sources come first, project sources last: adapters copy in order
# and the last write wins, so a project file overrides a same-named package
Expand All @@ -38,7 +38,7 @@ targets:
# CLI-owned blocks — the engine never reads below this line.
packages:
"@ainova-systems/sync":
version: "0.18.1"
version: "0.19.0"
"@ainova-systems/core":
version: "^0.3.0"

Expand Down
6 changes: 3 additions & 3 deletions examples/dotnet-api-with-react-frontend/intelligence.lock
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
# Generated by the intelligence CLI. Do not edit.
lockfile_version: 1
engine_version: "0.18.1"
engine_version: "0.19.0"
packages:
"@ainova-systems/sync":
requested: "0.18.1"
requested: "0.19.0"
url: "https://github.com/ainova-systems/intelligence.git"
path: "packages/sync"
resolved: "v0.18.1"
resolved: "v0.19.0"
sha: ""
4 changes: 2 additions & 2 deletions examples/dotnet-api-with-react-frontend/intelligence.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
project:
name: my-platform

schema_version: "0.18.1"
schema_version: "0.19.0"

sources:
rules:
Expand Down Expand Up @@ -50,4 +50,4 @@ ignore:
# from the CLI bundle.
packages:
"@ainova-systems/sync":
version: "0.18.1"
version: "0.19.0"
6 changes: 3 additions & 3 deletions examples/go-api-with-antigravity/intelligence.lock
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
# Generated by the intelligence CLI. Do not edit.
lockfile_version: 1
engine_version: "0.18.1"
engine_version: "0.19.0"
packages:
"@ainova-systems/sync":
requested: "0.18.1"
requested: "0.19.0"
url: "https://github.com/ainova-systems/intelligence.git"
path: "packages/sync"
resolved: "v0.18.1"
resolved: "v0.19.0"
sha: ""
4 changes: 2 additions & 2 deletions examples/go-api-with-antigravity/intelligence.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
project:
name: my-api

schema_version: "0.18.1"
schema_version: "0.19.0"

sources:
rules:
Expand Down Expand Up @@ -36,4 +36,4 @@ targets:
# from the CLI bundle.
packages:
"@ainova-systems/sync":
version: "0.18.1"
version: "0.19.0"
6 changes: 3 additions & 3 deletions examples/go-api-with-opencode/intelligence.lock
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
# Generated by the intelligence CLI. Do not edit.
lockfile_version: 1
engine_version: "0.18.1"
engine_version: "0.19.0"
packages:
"@ainova-systems/sync":
requested: "0.18.1"
requested: "0.19.0"
url: "https://github.com/ainova-systems/intelligence.git"
path: "packages/sync"
resolved: "v0.18.1"
resolved: "v0.19.0"
sha: ""
4 changes: 2 additions & 2 deletions examples/go-api-with-opencode/intelligence.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
project:
name: my-api

schema_version: "0.18.1"
schema_version: "0.19.0"

sources:
rules:
Expand Down Expand Up @@ -35,4 +35,4 @@ targets:
# from the CLI bundle.
packages:
"@ainova-systems/sync":
version: "0.18.1"
version: "0.19.0"
6 changes: 3 additions & 3 deletions examples/go-api-with-pi-and-codex/intelligence.lock
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
# Generated by the intelligence CLI. Do not edit.
lockfile_version: 1
engine_version: "0.18.1"
engine_version: "0.19.0"
packages:
"@ainova-systems/sync":
requested: "0.18.1"
requested: "0.19.0"
url: "https://github.com/ainova-systems/intelligence.git"
path: "packages/sync"
resolved: "v0.18.1"
resolved: "v0.19.0"
sha: ""
4 changes: 2 additions & 2 deletions examples/go-api-with-pi-and-codex/intelligence.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
project:
name: my-api

schema_version: "0.18.1"
schema_version: "0.19.0"

sources:
rules:
Expand Down Expand Up @@ -36,4 +36,4 @@ targets:
# from the CLI bundle.
packages:
"@ainova-systems/sync":
version: "0.18.1"
version: "0.19.0"
6 changes: 3 additions & 3 deletions examples/go-api/intelligence.lock
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
# Generated by the intelligence CLI. Do not edit.
lockfile_version: 1
engine_version: "0.18.1"
engine_version: "0.19.0"
packages:
"@ainova-systems/sync":
requested: "0.18.1"
requested: "0.19.0"
url: "https://github.com/ainova-systems/intelligence.git"
path: "packages/sync"
resolved: "v0.18.1"
resolved: "v0.19.0"
sha: ""
4 changes: 2 additions & 2 deletions examples/go-api/intelligence.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
project:
name: my-api

schema_version: "0.18.1"
schema_version: "0.19.0"

sources:
rules:
Expand Down Expand Up @@ -35,4 +35,4 @@ targets:
# from the CLI bundle.
packages:
"@ainova-systems/sync":
version: "0.18.1"
version: "0.19.0"
6 changes: 3 additions & 3 deletions examples/platform-with-submodules/intelligence.lock
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
# Generated by the intelligence CLI. Do not edit.
lockfile_version: 1
engine_version: "0.18.1"
engine_version: "0.19.0"
packages:
"@ainova-systems/sync":
requested: "0.18.1"
requested: "0.19.0"
url: "https://github.com/ainova-systems/intelligence.git"
path: "packages/sync"
resolved: "v0.18.1"
resolved: "v0.19.0"
sha: ""
4 changes: 2 additions & 2 deletions examples/platform-with-submodules/intelligence.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
project:
name: my-platform

schema_version: "0.18.1"
schema_version: "0.19.0"

sources:
rules:
Expand Down Expand Up @@ -53,4 +53,4 @@ submodules:
# from the CLI bundle.
packages:
"@ainova-systems/sync":
version: "0.18.1"
version: "0.19.0"
4 changes: 2 additions & 2 deletions examples/with-remote-skills/intelligence.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
project:
name: my-app

schema_version: "0.18.1"
schema_version: "0.19.0"

# Package sources come first, project sources last: adapters copy in order and
# the last write wins, so a project file overrides a same-named package file.
Expand Down Expand Up @@ -43,7 +43,7 @@ targets:
# fresh clone restores exactly this state with `intelligence sync`.
packages:
"@ainova-systems/sync":
version: "0.18.1"
version: "0.19.0"
# A package resolved through the trust list below — the name is all the
# manifest needs, the registry supplies the url.
"@org/shared-intel":
Expand Down
Loading
Loading