@ainova-systems/intelligence 0.16.0 → 0.17.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -33,7 +33,7 @@ intelligence sync
33
33
  | Command | What it does |
34
34
  |---|---|
35
35
  | `intelligence init [--preview\|--apply]` | Create, convert, restore or align a project |
36
- | `intelligence sync [adapter]` | Restore locked content if needed, then render all or one enabled adapter |
36
+ | `intelligence sync [adapter] [--compact] [--force]` | Restore locked content, then render or reuse verified unchanged output; `--force` renders and refreshes repository discovery |
37
37
  | `intelligence update [@scope/name] [--latest] [--preview\|--apply]` | Plan or apply project and ranged-package updates; `--latest` crosses one package's range |
38
38
  | `intelligence upgrade [--next] [--preview\|--apply]` | Replace the installed CLI with the newest version on its npm channel |
39
39
  | `intelligence package add\|remove\|list\|search` | Manage versioned Intelligence Packages |
@@ -233,6 +233,7 @@ else
233
233
  [ -d "$root/.pi" ] && targets="$targets pi"
234
234
  [ -d "$root/.opencode" ] && targets="$targets opencode"
235
235
  if [ -f "$root/GEMINI.md" ] \
236
+ || [ -f "$root/.antigravity.md" ] \
236
237
  || [ -d "$root/.agents/rules" ] \
237
238
  || [ -d "$root/.agents/agents" ] \
238
239
  || [ -d "$root/.agents/workflows" ]; then
@@ -1,16 +1,18 @@
1
1
  #!/bin/bash
2
- # intelligence sync [target] [--compact] - render intelligence to enabled tools.
2
+ # intelligence sync [target] [--compact] [--force] - render intelligence to enabled tools.
3
3
  set -euo pipefail
4
4
  source "$CLI_DIR/lib/cli-common.sh"
5
5
 
6
6
  compact=0
7
+ force=0
7
8
  target=""
8
9
  while [ $# -gt 0 ]; do
9
10
  case "$1" in
10
11
  --compact) compact=1 ;;
12
+ --force) force=1 ;;
11
13
  -*) die "unknown option '$1'" ;;
12
14
  *)
13
- [ -z "$target" ] || die "usage: intelligence sync [adapter] [--compact]"
15
+ [ -z "$target" ] || die "usage: intelligence sync [adapter] [--compact] [--force]"
14
16
  target="$1"
15
17
  ;;
16
18
  esac
@@ -28,13 +30,10 @@ run_sync() {
28
30
  ensure_project_current "$IP_ROOT" || return $?
29
31
  restore_project_store_if_missing "$IP_ROOT" || return $?
30
32
  export_engine_env "$IP_ROOT" || return $?
31
- if [ -n "$target" ]; then
32
- bash "$IS_ENGINE_DIR/sync.sh" "$target" || return $?
33
- else
34
- bash "$IS_ENGINE_DIR/sync.sh" || return $?
35
- fi
33
+ sync_with_cache "$target" "$force" || return $?
36
34
  ;;
37
35
  legacy)
36
+ [ "$force" -eq 0 ] || die "--force is unavailable for a legacy Intelligence Sync project - run 'intelligence init' to convert it first"
38
37
  [ "$compact" -eq 0 ] || die "--compact is unavailable for a legacy Intelligence Sync project - run 'intelligence init' to convert it first"
39
38
  # A vendored project syncs with its own engine: its pin is the
40
39
  # contract, and a newer bundled engine must not generate against an
package/cli/intelligence CHANGED
@@ -46,8 +46,9 @@ Usage: intelligence <command> [args]
46
46
  init [--preview|--apply] Initialize, convert, restore or align this project
47
47
  --targets a,b --dir name --bare --no-sync New-project options
48
48
  --force Accept a dirty worktree during legacy project conversion
49
- sync [adapter] [--compact] Restore locked content, then render enabled adapters
49
+ sync [adapter] [--compact] [--force] Restore locked content, then render enabled adapters
50
50
  --compact On success show context, warnings and final status; on failure show diagnostics
51
+ --force Render again and refresh discovery of unconfigured source directories
51
52
  update [@scope/name] Show an update plan; ask before applying it
52
53
  --preview Show the plan without asking or writing
53
54
  --apply Apply the plan without asking
@@ -110,6 +110,9 @@ done < <(qmap_keys "$manifest" "packages")
110
110
  # onboarding backup and Git policy. Validate every declared target, including
111
111
  # disabled ones, so a later enable cannot reveal a stale custom adapter.
112
112
  content_dir="$(manifest_intelligence_dir "$manifest")"
113
+ # Ownership across enabled adapters: two adapters claiming one owned path prune
114
+ # each other silently, and sync reports success for both.
115
+ adapter_claims_reset
113
116
  while IFS= read -r target; do
114
117
  [ -n "$target" ] || continue
115
118
  output="$(get_target_output "$manifest" "$target")"
@@ -124,6 +127,10 @@ while IFS= read -r target; do
124
127
  warn "adapter '$target' has an invalid ownership contract"
125
128
  continue
126
129
  fi
130
+ if [ "$(is_target_enabled "$manifest" "$target")" = "1" ]; then
131
+ adapter_claims_add_records "$target" "$records" \
132
+ || warn "$IS_ADAPTER_CLAIM_CONFLICT"
133
+ fi
127
134
  while IFS=$'\t' read -r kind value; do
128
135
  if [ "$kind" = "requires" ] \
129
136
  && [ "$(is_target_enabled "$manifest" "$target")" = "1" ] \
@@ -29,13 +29,32 @@ if [ "$action" = "enable" ]; then
29
29
  [ -n "$output" ] || output="$(default_target_output "$name")"
30
30
  validate_adapter_contract_for "$IP_ROOT" "$content_dir" "$name" "$output" \
31
31
  || die "adapter '$name' has an invalid ownership contract"
32
+ records="$(adapter_records_for "$IP_ROOT" "$content_dir" "$name" "$output")" \
33
+ || die "adapter '$name' has an invalid ownership contract"
32
34
  while IFS= read -r required; do
33
35
  [ -n "$required" ] || continue
34
36
  if [ "$(is_target_enabled "$manifest" "$required")" != "1" ]; then
35
37
  die "target '$name' requires target '$required' — enable it first"
36
38
  fi
37
- done < <(adapter_records_for "$IP_ROOT" "$content_dir" "$name" "$output" \
38
- | awk -F '\t' '$1 == "requires" { print $2 }')
39
+ done < <(awk -F '\t' '$1 == "requires" { print $2 }' <<< "$records")
40
+
41
+ # Two adapters claiming one owned path prune each other's output, and both
42
+ # syncs still report success. Refuse here, while the manifest is the only
43
+ # thing that would have to change.
44
+ adapter_claims_reset
45
+ while IFS= read -r enabled_target; do
46
+ [ -n "$enabled_target" ] || continue
47
+ [ "$enabled_target" = "$name" ] && continue
48
+ [ "$(is_target_enabled "$manifest" "$enabled_target")" = "1" ] || continue
49
+ enabled_output="$(get_target_output "$manifest" "$enabled_target")"
50
+ [ -n "$enabled_output" ] || enabled_output="$(default_target_output "$enabled_target")"
51
+ enabled_records="$(adapter_records_for "$IP_ROOT" "$content_dir" "$enabled_target" "$enabled_output" 2>/dev/null)" \
52
+ || continue
53
+ adapter_claims_add_records "$enabled_target" "$enabled_records" || true
54
+ done < <(target_names "$manifest")
55
+ adapter_claims_add_records "$name" "$records" \
56
+ || die "$IS_ADAPTER_CLAIM_CONFLICT"
57
+
39
58
  target_set_enabled "$manifest" "$name" true "$output"
40
59
  ensure_target_gitignore "$IP_ROOT" "$manifest" "$name"
41
60
  ensure_manifest_publisher_ignores "$IP_ROOT" "$manifest"
@@ -86,6 +86,7 @@ source "$CLI_DIR/lib/lockfile.sh"
86
86
  source "$CLI_DIR/lib/adapter-lifecycle.sh"
87
87
  source "$CLI_DIR/lib/gitignore.sh"
88
88
  source "$CLI_DIR/lib/onboarding.sh"
89
+ source "$CLI_DIR/lib/sync-cache.sh"
89
90
 
90
91
  # The engine-content package: OPTIONAL but auto-selected at init. Package by
91
92
  # UX (manifest entry, lockfile row, list/search/remove), bundle by mechanics —
@@ -1,12 +1,56 @@
1
1
  #!/bin/bash
2
2
  # Generated-output policy derived from each adapter's ownership contract.
3
3
 
4
+ # The line that separates what this policy manages from what the project wrote
5
+ # before Intelligence touched the file: everything the CLI appends lands after
6
+ # it, so it is also the boundary of what the CLI may rewrite.
7
+ IS_GITIGNORE_HEADER='# Intelligence generated state and tool output'
8
+
9
+ # Line endings held in variables, never written as `$'\r'` at the point of use:
10
+ # inside a command substitution bash does not expand that form, so the very same
11
+ # expression stops stripping the CR depending on where it is called from.
12
+ IS_CR=$'\r'
13
+ IS_LF=$'\n'
14
+ IS_CRLF=$'\r\n'
15
+
16
+ # An ignore file checked out on Windows can hold CRLF, and a CR belongs to the
17
+ # pattern Git matches — so these two helpers decide presence ignoring it, and
18
+ # append in whatever ending the file already uses. Comparing with the CR
19
+ # attached made every existing line look absent on Linux and macOS (where it is
20
+ # part of the line, unlike under MSYS), so each alignment appended an LF copy of
21
+ # a line that was already there and the file grew forever.
22
+
23
+ # ignore_file_eol_var <file> — set IS_IGNORE_EOL from the file's first line.
24
+ ignore_file_eol_var() {
25
+ local file="$1" first=""
26
+ IS_IGNORE_EOL="$IS_LF"
27
+ [ -s "$file" ] || return 0
28
+ IFS= read -r first < "$file" || true
29
+ case "$first" in
30
+ *"$IS_CR") IS_IGNORE_EOL="$IS_CRLF" ;;
31
+ esac
32
+ }
33
+
34
+ # ignore_file_has_line <file> <line> — true when the file lists exactly this
35
+ # line, with or without a trailing CR.
36
+ ignore_file_has_line() {
37
+ local file="$1" line="$2"
38
+ [ -f "$file" ] || return 1
39
+ grep -Fqx -- "$line" "$file" 2>/dev/null && return 0
40
+ grep -Fqx -- "$line$IS_CR" "$file" 2>/dev/null
41
+ }
42
+
43
+ # ignore_file_append_line <file> <line> — append in the file's own ending.
44
+ ignore_file_append_line() {
45
+ local file="$1" line="$2"
46
+ ignore_file_eol_var "$file"
47
+ printf '%s%s' "$line" "$IS_IGNORE_EOL" >> "$file"
48
+ }
49
+
4
50
  gitignore_add_line() {
5
51
  local root="$1" line="$2" file="$1/.gitignore"
6
- if [ -f "$file" ] && grep -Fqx -- "$line" "$file"; then
7
- return 0
8
- fi
9
- printf '%s\n' "$line" >> "$file"
52
+ ignore_file_has_line "$file" "$line" && return 0
53
+ ignore_file_append_line "$file" "$line"
10
54
  }
11
55
 
12
56
  gitignore_path_is_ignored() {
@@ -21,10 +65,102 @@ gitignore_tail_is() {
21
65
  [ -f "$file" ] || return 1
22
66
  local want tail_lines
23
67
  want="$(printf '%s\n' "$@")"
24
- tail_lines="$(tail -n "$#" -- "$file")"
68
+ # CR stripped for the comparison only — the chain is the same chain whether
69
+ # the file is LF or CRLF.
70
+ tail_lines="$(tail -n "$#" -- "$file" | tr -d '\r')"
25
71
  [ "$tail_lines" = "$want" ]
26
72
  }
27
73
 
74
+ # gitignore_collapse_duplicates <root> <probe-path> <line…> — keep only the LAST
75
+ # occurrence of each given line inside the region this policy owns (its header
76
+ # to end of file).
77
+ #
78
+ # This removes residue, never meaning: Git applies the last matching rule, so
79
+ # identical earlier copies of a line decide nothing. The copies exist because
80
+ # the 0.12.0 repair appended the whole negation chain on every alignment — a
81
+ # project carries one chain per run it made, and 0.12.1 only stopped the growth.
82
+ #
83
+ # Lines above the header predate Intelligence and are left untouched, including
84
+ # a hand-written copy of a line this policy also writes. The probe path is
85
+ # re-checked afterwards: a collapse that somehow changed what Git ignores is
86
+ # rolled back rather than committed.
87
+ gitignore_collapse_duplicates() {
88
+ local root="$1" probe="$2"; shift 2
89
+ local file="$root/.gitignore" tmp backup before=0 after=0
90
+ local -a lines=() managed=("$@") last_idx=()
91
+ local line key i mi n hdr=-1 keep final_newline=1
92
+ [ -f "$file" ] || return 0
93
+
94
+ # Read and write the file's own bytes, without awk: on Windows it reads in
95
+ # text mode and would hand back every line stripped of its CR, rewriting a
96
+ # CRLF file as LF. A line's CR is significant to Git, and either way the
97
+ # lines this function does not remove must survive byte-for-byte.
98
+ line=""
99
+ while IFS= read -r line; do
100
+ lines[${#lines[@]}]="$line"
101
+ line=""
102
+ done < "$file"
103
+ # `read` returns 1 at EOF but still fills `line` when the file's last line
104
+ # carries no newline; preserve that ending instead of adding one.
105
+ if [ -n "$line" ]; then
106
+ lines[${#lines[@]}]="$line"
107
+ final_newline=0
108
+ fi
109
+ n=${#lines[@]}
110
+ for ((i = 0; i < n; i++)); do
111
+ if [ "${lines[i]%"$IS_CR"}" = "$IS_GITIGNORE_HEADER" ]; then hdr=$i; break; fi
112
+ done
113
+ [ "$hdr" -ge 0 ] || return 0
114
+
115
+ # Where each managed line last occurs below the header. Indexed arrays and
116
+ # arithmetic `for` only — Bash 3.2 has no associative arrays.
117
+ for ((mi = 0; mi < ${#managed[@]}; mi++)); do
118
+ last_idx[mi]=-1
119
+ for ((i = hdr + 1; i < n; i++)); do
120
+ [ "${lines[i]%"$IS_CR"}" = "${managed[mi]}" ] && last_idx[mi]=$i
121
+ done
122
+ done
123
+
124
+ tmp="$file.cli.tmp"
125
+ {
126
+ for ((i = 0; i < n; i++)); do
127
+ keep=1
128
+ if [ "$i" -gt "$hdr" ]; then
129
+ key="${lines[i]%"$IS_CR"}"
130
+ for ((mi = 0; mi < ${#managed[@]}; mi++)); do
131
+ if [ "$key" = "${managed[mi]}" ] && [ "$i" -ne "${last_idx[mi]}" ]; then
132
+ keep=0
133
+ break
134
+ fi
135
+ done
136
+ fi
137
+ [ "$keep" -eq 1 ] || continue
138
+ if [ "$i" -eq $((n - 1)) ] && [ "$final_newline" -eq 0 ]; then
139
+ printf '%s' "${lines[i]}"
140
+ else
141
+ printf '%s\n' "${lines[i]}"
142
+ fi
143
+ done
144
+ } > "$tmp"
145
+ if [ ! -s "$tmp" ] || cmp -s "$tmp" "$file"; then
146
+ rm -f "$tmp"
147
+ return 0
148
+ fi
149
+
150
+ before=0
151
+ gitignore_path_is_ignored "$root" "$probe" || before=$?
152
+ backup="$file.cli.bak"
153
+ cp -- "$file" "$backup"
154
+ mv -- "$tmp" "$file"
155
+ after=0
156
+ gitignore_path_is_ignored "$root" "$probe" || after=$?
157
+ if [ "$after" != "$before" ]; then
158
+ mv -- "$backup" "$file"
159
+ return 0
160
+ fi
161
+ rm -f "$backup"
162
+ }
163
+
28
164
  # Git cannot re-include a child of an excluded directory, so an include needs an
29
165
  # explicit negation for every parent. A negation only wins when it comes after
30
166
  # the rule excluding the parent, and the only repair an append can make is to
@@ -59,7 +195,7 @@ gitignore_add_effective_include() {
59
195
  gitignore_path_is_ignored "$root" "$value" || rc=$?
60
196
  case "$rc" in
61
197
  0) ;;
62
- 1|2) return 0 ;;
198
+ 1|2) gitignore_collapse_duplicates "$root" "$value" "${negations[@]}"; return 0 ;;
63
199
  *) return "$rc" ;;
64
200
  esac
65
201
 
@@ -67,17 +203,25 @@ gitignore_add_effective_include() {
67
203
  # chain last. Already last means appending cannot help — leave it, and let
68
204
  # `status --check` report the re-inclusion this policy could not make
69
205
  # effective rather than growing the file forever.
70
- gitignore_tail_is "$root/.gitignore" "${negations[@]}" && return 0
71
- printf '%s\n' "${negations[@]}" >> "$root/.gitignore"
206
+ if ! gitignore_tail_is "$root/.gitignore" "${negations[@]}"; then
207
+ for line in "${negations[@]}"; do
208
+ ignore_file_append_line "$root/.gitignore" "$line"
209
+ done
210
+ fi
211
+ # Either way the chain now stands last, so any earlier copy of it is the
212
+ # residue an older CLI appended; collapsing runs on both paths so a project
213
+ # converges on one copy whether or not this run had to move anything.
214
+ gitignore_collapse_duplicates "$root" "$value" "${negations[@]}"
72
215
  }
73
216
 
74
217
  ensure_gitignore_header() {
75
218
  local root="$1" file="$1/.gitignore"
76
- if [ ! -f "$file" ] || ! grep -Fqx '# Intelligence generated state and tool output' "$file"; then
219
+ if ! ignore_file_has_line "$file" "$IS_GITIGNORE_HEADER"; then
77
220
  if [ -f "$file" ] && [ -s "$file" ] && [ -n "$(tail -c 1 "$file" 2>/dev/null)" ]; then
78
- printf '\n' >> "$file"
221
+ ignore_file_eol_var "$file"
222
+ printf '%s' "$IS_IGNORE_EOL" >> "$file"
79
223
  fi
80
- printf '%s\n' '# Intelligence generated state and tool output' >> "$file"
224
+ ignore_file_append_line "$file" "$IS_GITIGNORE_HEADER"
81
225
  fi
82
226
  }
83
227
 
@@ -125,10 +269,8 @@ publisher_ignore_file_names() {
125
269
 
126
270
  publisher_ignore_add_line() {
127
271
  local file="$1" line="$2"
128
- if grep -Fqx -- "$line" "$file" 2>/dev/null; then
129
- return 0
130
- fi
131
- printf '%s\n' "$line" >> "$file"
272
+ ignore_file_has_line "$file" "$line" && return 0
273
+ ignore_file_append_line "$file" "$line"
132
274
  PUBLISH_IGNORE_FILE_CHANGED=1
133
275
  }
134
276
 
@@ -148,11 +290,12 @@ ensure_manifest_publisher_ignores() {
148
290
  file="$root/$rel"
149
291
  [ -f "$file" ] || continue
150
292
  PUBLISH_IGNORE_FILE_CHANGED=0
151
- if ! grep -Fqx '# Intelligence development context and generated output' "$file" 2>/dev/null; then
293
+ if ! ignore_file_has_line "$file" '# Intelligence development context and generated output'; then
152
294
  if [ -s "$file" ] && [ -n "$(tail -c 1 "$file" 2>/dev/null)" ]; then
153
- printf '\n' >> "$file"
295
+ ignore_file_eol_var "$file"
296
+ printf '%s' "$IS_IGNORE_EOL" >> "$file"
154
297
  fi
155
- printf '%s\n' '# Intelligence development context and generated output' >> "$file"
298
+ ignore_file_append_line "$file" '# Intelligence development context and generated output'
156
299
  PUBLISH_IGNORE_FILE_CHANGED=1
157
300
  fi
158
301
  publisher_ignore_add_path "$file" '.intelligence'
@@ -0,0 +1,202 @@
1
+ #!/bin/bash
2
+ # Optional CLI optimization. Cache data never supplies paths or executable code:
3
+ # every dependency is derived again from the manifest and built-in contracts.
4
+
5
+ sync_cache_safe_path() {
6
+ local path="$1" probe="$1"
7
+ case "$path" in *[[:cntrl:]]*|*\\*|*\"*) return 1 ;; esac
8
+ # Include ancestors: find does not report a link above its starting path.
9
+ while [ -n "$probe" ] && [ "$probe" != / ]; do
10
+ case "$probe" in "$REPO_ROOT"|"$CLI_DIR"|"$IS_ENGINE_DIR") break ;; esac
11
+ [ ! -L "$probe" ] || return 1
12
+ probe="${probe%/*}"
13
+ done
14
+ }
15
+
16
+ # One checked enumeration and one batch hash, including names, directory/file
17
+ # types and executable bits. Timestamps cannot prove content is unchanged.
18
+ # Unsupported paths/types are a cache miss, never an incomplete fingerprint.
19
+ sync_cache_fingerprint() (
20
+ local work="$1" path kind executable seen=$'\n'
21
+ local -a existing
22
+ existing=()
23
+ shift
24
+ : > "$work/entries" || return 1
25
+ : > "$work/files" || return 1
26
+ : > "$work/list" || return 1
27
+ for path in "$@"; do
28
+ sync_cache_safe_path "$path" || return 1
29
+ # Shared managed destinations are declared by several adapters. Walking
30
+ # the same tree again only multiplies filesystem probes on Windows.
31
+ case "$seen" in *$'\n'"$path"$'\n'*) continue ;; esac
32
+ seen="$seen$path"$'\n'
33
+ if [ -e "$path" ]; then
34
+ existing+=("$path")
35
+ else
36
+ printf 'missing\t%s\n' "$path" >> "$work/entries" || return 1
37
+ fi
38
+ done
39
+ if [ "${#existing[@]}" -gt 0 ]; then
40
+ find "${existing[@]}" -print0 > "$work/list" || return 1
41
+ fi
42
+ while IFS= read -r -d '' path; do
43
+ # Starting-path ancestors were checked above. find does not follow
44
+ # links, so checking each returned entry once covers the entire tree.
45
+ case "$path" in *[[:cntrl:]]*|*\\*|*\"*) return 1 ;; esac
46
+ [ ! -L "$path" ] || return 1
47
+ executable=0
48
+ if [ -f "$path" ]; then
49
+ kind='file'
50
+ [ ! -x "$path" ] || executable=1
51
+ printf '%s\n' "$path" >> "$work/files" || return 1
52
+ elif [ -d "$path" ]; then
53
+ kind=directory
54
+ else
55
+ return 1
56
+ fi
57
+ printf '%s\t%s\t%s\n' "$kind" "$executable" "$path" >> "$work/entries" || return 1
58
+ done < "$work/list"
59
+ # find order need not be stable. Sort both streams; preserve path-to-hash
60
+ # association by feeding the sorted file list into a single git process.
61
+ LC_ALL=C sort -u "$work/entries" > "$work/sorted" || return 1
62
+ LC_ALL=C sort -u "$work/files" > "$work/paths" || return 1
63
+ if command -v cygpath >/dev/null 2>&1; then
64
+ # Git for Windows does not translate POSIX paths received on stdin.
65
+ cygpath -m -f "$work/paths" > "$work/native" || return 1
66
+ else
67
+ cp "$work/paths" "$work/native" || return 1
68
+ fi
69
+ git hash-object --no-filters --stdin-paths < "$work/native" > "$work/hashes" || return 1
70
+ cat "$work/sorted" "$work/paths" "$work/hashes" | git hash-object --stdin || return 1
71
+ )
72
+
73
+ sync_cache_dependencies() {
74
+ local src section adapter file output records kind value
75
+ SC_INPUTS=("$CONFIG_FILE" "$REPO_ROOT/intelligence.lock"
76
+ "$CLI_DIR/intelligence" "$CLI_DIR/engine-package.yaml"
77
+ "$CLI_DIR/commands" "$CLI_DIR/internal" "$CLI_DIR/lib" "$IS_ENGINE_DIR"
78
+ "$REPO_ROOT/$IS_CONTENT_REL/adapters")
79
+ SC_OUTPUTS=()
80
+ # Project adapters can read arbitrary files or environment and execute any
81
+ # code. Their dependencies cannot be inferred from their write contract.
82
+ for file in "$REPO_ROOT/$IS_CONTENT_REL/adapters"/*.sh; do
83
+ [ ! -e "$file" ] && [ ! -L "$file" ] || return 1
84
+ done
85
+ for section in rules agents skills; do
86
+ load_yaml_list "$CONFIG_FILE" "$section"
87
+ while IFS= read -r src; do
88
+ [ -n "$src" ] || continue
89
+ adapter_contract_safe_concrete_path "$src" || return 1
90
+ case "$src" in .|./|.intelligence|.intelligence/|.intelligence/sync-cache*) return 1 ;; esac
91
+ SC_INPUTS+=("$REPO_ROOT/$src")
92
+ done <<< "$IS_YAML_LIST"
93
+ done
94
+ load_targets_cache "$CONFIG_FILE"
95
+ for file in "$IS_ENGINE_DIR/adapters"/*.sh; do
96
+ adapter="${file##*/}"; adapter="${adapter%.sh}"
97
+ [ "$adapter" != _template ] || continue
98
+ target_enabled_var "$CONFIG_FILE" "$adapter"
99
+ [ "$IS_TGT_ENABLED" = 1 ] || continue
100
+ case "$adapter" in agents|antigravity|claude|codex|copilot|cursor|opencode|pi) ;; *) return 1 ;; esac
101
+ target_output_var "$CONFIG_FILE" "$adapter"
102
+ output="${IS_TGT_OUTPUT:-.$adapter}"
103
+ records="$(adapter_contract_records "$adapter" "$file" "$output")" || return 1
104
+ while IFS=$'\t' read -r kind value; do
105
+ case "$kind" in owned|managed) SC_OUTPUTS+=("$REPO_ROOT/$value") ;; esac
106
+ done <<< "$records"
107
+ done
108
+ [ "${#SC_OUTPUTS[@]}" -gt 0 ]
109
+ }
110
+
111
+ sync_cache_input_hash() {
112
+ local fingerprint
113
+ fingerprint="$(sync_cache_fingerprint "$1" "${SC_INPUTS[@]}")" || return 1
114
+ # Explicit environment values that affect rendering, discovery or parsing.
115
+ # No secrets or inherited environment dump are persisted.
116
+ printf '%s\n' "$fingerprint" "$2" "$REPO_ROOT" "$CONFIG_FILE" \
117
+ "$IS_CONTENT_REL" "$IS_MODULE_REL" "$IS_SYNC_CMD" "$IS_MANIFEST_NAME" \
118
+ "$IS_PROTECTED_DIRS" "$BASH_VERSION" "${OSTYPE:-}" "${LANG:-}" \
119
+ "${LC_ALL:-}" "${LC_CTYPE:-}" "${LC_COLLATE:-}" "${PATH:-}" "$(umask)" | git hash-object --stdin
120
+ }
121
+
122
+ sync_cache_directory() {
123
+ local path="$REPO_ROOT/.intelligence/sync-cache" physical root_physical probe expected
124
+ sync_cache_safe_path "$path/state" || return 1
125
+ root_physical="$(cd "$REPO_ROOT" && pwd -P)" || return 1
126
+ probe="$path"; expected="$root_physical/.intelligence/sync-cache"
127
+ while [ ! -e "$probe" ]; do probe="${probe%/*}"; expected="${expected%/*}"; done
128
+ physical="$(cd "$probe" && pwd -P)" || return 1
129
+ [ "$physical" = "$expected" ] || return 1
130
+ if [ "${1:-}" = create ]; then mkdir -p "$path" || return 1; fi
131
+ SC_DIRECTORY="$path"
132
+ }
133
+
134
+ sync_cache_read() {
135
+ local file="$1" input="$2" output="$3" work="$4" header report_hash actual
136
+ [ -f "$file" ] && [ ! -L "$file" ] || return 1
137
+ IFS= read -r header < "$file" || return 1
138
+ case "$header" in "intelligence-sync-cache-v1 $input $output "*) ;; *) return 1 ;; esac
139
+ report_hash="${header##* }"
140
+ case "$report_hash" in ''|*[!a-f0-9]*) return 1 ;; esac
141
+ [ "${#report_hash}" -eq 40 ] || [ "${#report_hash}" -eq 64 ] || return 1
142
+ tail -n +2 "$file" > "$work/report" || return 1
143
+ actual="$(git hash-object --stdin < "$work/report")" || return 1
144
+ [ "$actual" = "$report_hash" ] || return 1
145
+ grep -q '^IS_STATUS=ok\($\| \)' "$work/report" || return 1
146
+ grep -q '^=== Done:' "$work/report" || return 1
147
+ }
148
+
149
+ # Keep actionable diagnostics, including the multi-line unsynced-directory
150
+ # report that compact mode intentionally omits. Never replay renderer progress.
151
+ sync_cache_report() {
152
+ awk '
153
+ /^(WARNING:| WARN:)/ { warning=1; print; next }
154
+ warning && /^ / { print; next }
155
+ { warning=0 }
156
+ /^(CONTEXT:|IS_STATUS=ok|=== Done:|=== WARNING:| NOT SYNCED:| Wire one in:)/ { print }
157
+ ' "$1"
158
+ }
159
+
160
+ sync_with_cache() (
161
+ local target="$1" force="$2" work before after outputs report_hash rc=0 cacheable=0
162
+ work="$(mktemp -d -t intelligence-cache-XXXXXX)" || return 1
163
+ trap 'rm -rf "$work"' EXIT
164
+ if sync_cache_dependencies && sync_cache_directory; then
165
+ before="$(sync_cache_input_hash "$work" "$target")" || before=""
166
+ [ -z "$before" ] || cacheable=1
167
+ fi
168
+ if [ "$cacheable" = 1 ] && [ "$force" = 0 ]; then
169
+ outputs="$(sync_cache_fingerprint "$work" "${SC_OUTPUTS[@]}")" || outputs=""
170
+ if [ -n "$outputs" ] && sync_cache_read "$SC_DIRECTORY/state" "$before" "$outputs" "$work"; then
171
+ echo ' Unchanged: verified sources and generated output; skipped rendering (use --force to refresh discovery).'
172
+ cat "$work/report"
173
+ return 0
174
+ fi
175
+ fi
176
+ # A failed or interrupted attempt must not leave a success cache reusable.
177
+ if [ "$cacheable" = 1 ]; then
178
+ rm -f "$SC_DIRECTORY/state" || cacheable=0
179
+ fi
180
+ # Buffer once so success diagnostics can be replayed without asking the
181
+ # engine to know about persistent CLI state. Preserve the engine exit code.
182
+ bash "$IS_ENGINE_DIR/sync.sh" "$target" > "$work/log" 2>&1 || rc=$?
183
+ cat "$work/log"
184
+ [ "$rc" = 0 ] || return "$rc"
185
+ [ "$cacheable" = 1 ] || return 0
186
+ after="$(sync_cache_input_hash "$work" "$target")" || return 0
187
+ [ "$before" = "$after" ] || return 0
188
+ outputs="$(sync_cache_fingerprint "$work" "${SC_OUTPUTS[@]}")" || return 0
189
+ sync_cache_report "$work/log" > "$work/report" || return 0
190
+ report_hash="$(git hash-object --stdin < "$work/report")" || return 0
191
+ # Recheck containment before publishing. mktemp+rename avoids following a
192
+ # pre-existing state-file link, and readers see only complete records.
193
+ sync_cache_directory create || return 0
194
+ local staged
195
+ staged="$(mktemp "$SC_DIRECTORY/state-XXXXXX")" || return 0
196
+ if { printf 'intelligence-sync-cache-v1 %s %s %s\n' "$after" "$outputs" "$report_hash"; cat "$work/report"; } > "$staged"; then
197
+ mv -f "$staged" "$SC_DIRECTORY/state" || rm -f "$staged"
198
+ else
199
+ rm -f "$staged"
200
+ fi
201
+ return 0
202
+ )
package/engine/ENGINE_SHA CHANGED
@@ -1 +1 @@
1
- 179b1e89ade2ac7d833750458de1babe0c7a6d90
1
+ f23e407da080869b07481730d81b6ebce73a1d2e
package/engine/VERSION CHANGED
@@ -1 +1 @@
1
- 0.16.0
1
+ 0.17.0
@@ -2,18 +2,32 @@
2
2
  # intelligence-sync: Google Antigravity adapter
3
3
  # Transforms source prompts to Antigravity's native resource model.
4
4
  #
5
+ # Vendor sources checked 2026-09-20: antigravity.google/docs/{rules-workflows,
6
+ # skills,subagents} and /docs/cli/{best-practices,gcli-migration}. Where this
7
+ # adapter goes past what they state, the comment says so — the vendor documents
8
+ # less than the product implements, and an unmarked guess is what drifts.
9
+ #
5
10
  # Rules:
6
- # - Always-on (no `paths:`) -> SKIPPED here. Antigravity reads AGENTS.md
7
- # natively at the workspace root, and the `agents` adapter inlines
8
- # always-on rule content into it. Emitting copies under .agents/rules/
9
- # would double-load the same text.
11
+ # - Always-on (no `paths:`) -> SKIPPED here. The CLI docs tell a project to
12
+ # "Create a GEMINI.md or AGENTS.md file at your workspace root", and the
13
+ # `agents` adapter inlines always-on rule content into AGENTS.md. Emitting
14
+ # copies under .agents/rules/ would double-load the same text.
10
15
  # - Path-scoped (with `paths:`) -> .agents/rules/<name>.md carrying
11
- # `trigger: glob` and `globs:` (activation modes: always_on, manual,
12
- # model_decision, glob). Antigravity documents a 12,000-character limit
13
- # per rule file, so scoped rules stay short.
14
- # - GEMINI.md outranks AGENTS.md in Antigravity's rule precedence, so the
15
- # contract quarantines it at onboarding: left in place it would silently
16
- # override every synced rule.
16
+ # `trigger: glob` and `globs:`. NOT vendor-documented: the rules page names
17
+ # four activation modes in prose (manual, always on, model decision, glob
18
+ # pattern) and no frontmatter key, format or example anywhere. These keys
19
+ # follow the Windsurf-lineage convention Antigravity inherits; re-check
20
+ # them whenever that page grows a real schema.
21
+ # - "Rules files are limited to 12,000 characters each" is documented, so a
22
+ # generated scoped rule above the limit warns (targets.antigravity.
23
+ # warn_rule_limit).
24
+ # - Workspace-root instruction files outrank AGENTS.md where both are read:
25
+ # GEMINI.md over AGENTS.md, and .antigravity.md over GEMINI.md in the CLI.
26
+ # The docs host states neither order — nor AGENTS.md support on its rules
27
+ # page — so both come from the CLI pages above plus third-party reports of
28
+ # IDE 1.20.3. The contract quarantines both files at onboarding either way:
29
+ # left in place, either silently overrides every synced rule, and a file
30
+ # the project does not have costs nothing to declare.
17
31
  # Skills:
18
32
  # - Copied to .agents/skills/ (Agent Skills open standard, shared with
19
33
  # Codex, Pi and opencode). Antigravity reads that fixed workspace path,
@@ -38,17 +52,47 @@ adapter_contract_antigravity() {
38
52
  adapter_contract_owned "$output/agents"
39
53
  adapter_contract_managed ".agents/skills"
40
54
  adapter_contract_legacy "GEMINI.md"
55
+ adapter_contract_legacy ".antigravity.md"
41
56
  adapter_contract_ignore "GEMINI.md"
57
+ adapter_contract_ignore ".antigravity.md"
42
58
  adapter_contract_ignore "$output/rules/"
43
59
  adapter_contract_ignore "$output/agents/"
44
60
  adapter_contract_ignore ".agents/skills/"
45
61
  }
46
62
 
63
+ # Documented Antigravity limit: "Rules files are limited to 12,000 characters
64
+ # each." A rule over it is not truncated visibly — the tool decides what to do
65
+ # with it — so sync reports the overflow instead of guessing. A project may
66
+ # match another effective limit or turn the warning off.
67
+ # The count is awk's `length()`, which is characters where the awk build is
68
+ # locale-aware and bytes where it is not; the two agree on ASCII rules.
69
+ # Sets IS_AG_RULE_LIMIT (0 disables the check).
70
+ antigravity_rule_limit() {
71
+ local config_file="$1"
72
+ local setting
73
+ setting="$(get_target_field "$config_file" "antigravity" "warn_rule_limit")"
74
+ case "$setting" in
75
+ ''|true) IS_AG_RULE_LIMIT=12000 ;;
76
+ false) IS_AG_RULE_LIMIT=0 ;;
77
+ *[!0-9]*|0|0*)
78
+ echo "ERROR: targets.antigravity.warn_rule_limit must be true, false, or a positive character count without leading zeros." >&2
79
+ return 1
80
+ ;;
81
+ *) IS_AG_RULE_LIMIT="$setting" ;;
82
+ esac
83
+ }
84
+
47
85
  sync_antigravity_rules() {
48
86
  local repo_root="$1"
49
87
  local config_file="$2"
50
88
  local output_dir="$3"
51
89
 
90
+ # Read before the early return: a malformed setting is a manifest error
91
+ # whether or not this project happens to have a scoped rule today.
92
+ local rule_limit
93
+ antigravity_rule_limit "$config_file" || return 1
94
+ rule_limit="$IS_AG_RULE_LIMIT"
95
+
52
96
  local src f
53
97
  local -a files=()
54
98
  load_yaml_list "$config_file" "rules"
@@ -78,25 +122,50 @@ sync_antigravity_rules() {
78
122
  return 0
79
123
  fi
80
124
 
81
- # Path-scoped rule -> Glob activation (paths -> globs)
125
+ # Path-scoped rule -> Glob activation (paths -> globs). The rewrite is
126
+ # anchored inside the frontmatter block: a body line may legitimately start
127
+ # with `paths:` (a rule documenting rule syntax does exactly that), and
128
+ # rewriting it would corrupt the text it teaches.
129
+ #
130
+ # stdout carries nothing but the character-count overflow report, so the
131
+ # limit check costs no extra process.
132
+ local overflow
82
133
  is_fin_awk_vars
83
- awk "${IS_FIN_V[@]}" -v dst="$output_dir/rules" "$IS_AWK_LIB"'
134
+ overflow="$(awk "${IS_FIN_V[@]}" -v dst="$output_dir/rules" -v limit="$rule_limit" "$IS_AWK_LIB"'
135
+ function emit(line, rendered) {
136
+ rendered = fin_line(line)
137
+ print rendered > out
138
+ chars[out] += length(rendered) + 1
139
+ }
84
140
  FNR == 1 {
85
141
  if (out != "") close(out)
86
142
  out = dst "/" base_name(FILENAME)
143
+ chars[out] = 0
144
+ fm = 0
87
145
  }
88
- {
89
- sub(/\r$/, "")
90
- sub(/^paths:/, "globs:")
91
- }
146
+ { sub(/\r$/, "") }
92
147
  FNR == 1 {
93
- print fin_line($0) > out
94
- print fin_line("trigger: glob") > out
148
+ if ($0 == "---") fm = 1
149
+ emit($0)
150
+ emit("trigger: glob")
95
151
  next
96
152
  }
97
- { print fin_line($0) > out }
98
- END { if (out != "") close(out) }
99
- ' "${scoped[@]}"
153
+ fm == 1 && $0 == "---" { fm = 2; emit($0); next }
154
+ fm == 1 { sub(/^paths:/, "globs:") }
155
+ { emit($0) }
156
+ END {
157
+ if (out != "") close(out)
158
+ if (limit > 0)
159
+ for (f in chars)
160
+ if (chars[f] > limit) printf "%s\t%d\n", f, chars[f]
161
+ }
162
+ ' "${scoped[@]}" | sort)"
163
+
164
+ local over_file over_chars
165
+ while IFS=$'\t' read -r over_file over_chars; do
166
+ [ -n "$over_file" ] || continue
167
+ echo "WARNING: Antigravity limits a rule file to $rule_limit characters: ${over_file#"$repo_root/"} renders $over_chars; split the rule, shorten it, set targets.antigravity.warn_rule_limit to the effective limit, or set it to false to disable this warning." >&2
168
+ done <<< "$overflow"
100
169
 
101
170
  for f in "${scoped[@]}"; do
102
171
  base="${f##*/}"
@@ -146,18 +215,20 @@ sync_antigravity_agents() {
146
215
  fi
147
216
  header+="${LS}subagent: true"
148
217
  header+="${LS}mainAgent: false"
149
- # A readonly agent gets an explicit allowlist. It holds only tool
150
- # identifiers the official docs name, because an unknown entry in
151
- # an allowlist silently removes a capability rather than failing:
152
- # a directory-listing tool is left out until one is documented.
218
+ # A readonly agent gets an explicit allowlist, and it may hold only
219
+ # names the vendor documents: "Specifying an unmapped or misspelled
220
+ # tool name in the `tools` list may cause the subagent process to
221
+ # hang during execution." The subagents page names four —
222
+ # view_file, replace_file_content, grep_search, run_command — and
223
+ # publishes no master list, so readonly takes the two that read.
224
+ # Web and directory tools appear in leaked prompt dumps only; a
225
+ # hang is too expensive a bet on an unpublished name.
153
226
  # An unrestricted agent declares no list and inherits the main
154
227
  # agent's tools, which is Antigravity's default.
155
228
  if [ "$access" = "readonly" ]; then
156
229
  header+="${LS}tools:"
157
230
  header+="${LS} - view_file"
158
231
  header+="${LS} - grep_search"
159
- header+="${LS} - search_web"
160
- header+="${LS} - read_url_content"
161
232
  fi
162
233
  header+="${LS}---"
163
234
  header+="${LS}"
@@ -176,6 +247,31 @@ sync_antigravity_agents() {
176
247
  echo " -> Agents: $count"
177
248
  }
178
249
 
250
+ # Antigravity's resource paths are fixed, not configured: the docs call
251
+ # .agents/ a default with backward compatibility for .agent/, and name no
252
+ # setting that moves it. A configured output anywhere else therefore renders
253
+ # rules and agents the tool will never load, and leaves whatever the default
254
+ # path already holds behind as the only thing it does read. Sync still writes
255
+ # the requested path — rendering elsewhere for inspection is legitimate — but
256
+ # it says what the tool will actually see.
257
+ antigravity_warn_unread_output() {
258
+ local repo_root="$1"
259
+ local output_dir="$2"
260
+
261
+ # Compare the resolved path, not its spelling: `.agents`, `./.agents` and
262
+ # `.agents//` are the same directory, and the writer reaches it either way.
263
+ adapter_contract_rel_path "${output_dir#"$repo_root"/}"
264
+ local rel="$IS_ADAPTER_REL_PATH"
265
+ case "$rel" in
266
+ .agents|.agent) return 0 ;;
267
+ esac
268
+
269
+ echo "WARNING: Antigravity reads .agents/rules and .agents/agents (legacy .agent/), never a configured output: targets.antigravity.output '$rel' renders rules and agents no Antigravity surface loads." >&2
270
+ if [ -d "$repo_root/.agents/rules" ] || [ -d "$repo_root/.agents/agents" ]; then
271
+ echo " .agents/ still holds output from an earlier run; that stale copy is what Antigravity loads. Remove it, or set the output back to .agents." >&2
272
+ fi
273
+ }
274
+
179
275
  sync_to_antigravity() {
180
276
  local repo_root="$1"
181
277
  local config_file="$2"
@@ -183,6 +279,8 @@ sync_to_antigravity() {
183
279
 
184
280
  echo "=== Antigravity ==="
185
281
 
282
+ antigravity_warn_unread_output "$repo_root" "$output_dir"
283
+
186
284
  rm -rf "$output_dir/rules" "$output_dir/agents"
187
285
  mkdir -p "$output_dir/rules" "$output_dir/agents"
188
286
 
@@ -62,7 +62,10 @@ sync_cursor_rules() {
62
62
  done < <(frontmatter_index "paths#" "${files[@]}")
63
63
  [ "${#scoped[@]}" -gt 0 ] || return 0
64
64
 
65
- # Path-scoped rule -> Auto Attached (paths -> globs)
65
+ # Path-scoped rule -> Auto Attached (paths -> globs). The rewrite is
66
+ # anchored inside the frontmatter block: a body line may legitimately start
67
+ # with `paths:` (a rule documenting rule syntax does exactly that), and
68
+ # rewriting it would corrupt the text it teaches.
66
69
  is_fin_awk_vars
67
70
  awk "${IS_FIN_V[@]}" -v dst="$output_dir/rules" "$IS_AWK_LIB"'
68
71
  FNR == 1 {
@@ -70,12 +73,17 @@ sync_cursor_rules() {
70
73
  nm = base_name(FILENAME)
71
74
  sub(/\.md$/, ".mdc", nm)
72
75
  out = dst "/" nm
76
+ fm = 0
73
77
  }
74
- {
75
- sub(/\r$/, "")
76
- sub(/^paths:/, "globs:")
78
+ { sub(/\r$/, "") }
79
+ FNR == 1 {
80
+ if ($0 == "---") fm = 1
81
+ print fin_line($0) > out
82
+ print fin_line("alwaysApply: false") > out
83
+ next
77
84
  }
78
- FNR == 1 { print fin_line($0) > out; print fin_line("alwaysApply: false") > out; next }
85
+ fm == 1 && $0 == "---" { fm = 2; print fin_line($0) > out; next }
86
+ fm == 1 { sub(/^paths:/, "globs:") }
79
87
  { print fin_line($0) > out }
80
88
  ' "${scoped[@]}"
81
89
  for f in "${scoped[@]}"; do
@@ -19,6 +19,79 @@ adapter_contract_function() {
19
19
  printf 'adapter_contract_%s' "$1"
20
20
  }
21
21
 
22
+ # --- cross-adapter ownership -------------------------------------------------
23
+ #
24
+ # `owned` is exclusive: one adapter writes and prunes that path. `managed` is
25
+ # shared on purpose — .agents/skills is filled by Antigravity, Codex, Pi and
26
+ # opencode from identical content — so two `managed` claims on one path are
27
+ # legal. Any other collision means two adapters prune each other: whichever
28
+ # runs last wins, the earlier adapter's output is gone, and both report
29
+ # success. `intelligence sync codex` with codex output set to `.agents` did
30
+ # exactly that to Antigravity's agents.
31
+ #
32
+ # Claims are compared by resolved path, never by spelling: `.agents`,
33
+ # `./.agents` and `.agents//` name one directory, `validate_output_path`
34
+ # resolves them alike, and a lexical comparison would miss the collision that
35
+ # actually happens on disk. (normalize_path_var comes from lib/common.sh, which
36
+ # both the engine and the CLI source before this file.)
37
+ #
38
+ # State is one "adapter<TAB>kind<TAB>path" record per line in IS_ADAPTER_CLAIMS.
39
+ # A conflict is reported through IS_ADAPTER_CLAIM_CONFLICT, never printed and
40
+ # never captured in a subshell: the accumulator has to survive the call.
41
+ # shellcheck disable=SC2034 # IS_ADAPTER_CLAIM_CONFLICT is read by callers in the engine and the CLI
42
+ adapter_claims_reset() {
43
+ IS_ADAPTER_CLAIMS=""
44
+ IS_ADAPTER_CLAIM_CONFLICT=""
45
+ }
46
+
47
+ # adapter_contract_rel_path <path> — the repo-relative spelling every claim and
48
+ # every output comparison uses. Sets IS_ADAPTER_REL_PATH ("" for the repo root).
49
+ adapter_contract_rel_path() {
50
+ normalize_path_var "$1"
51
+ IS_ADAPTER_REL_PATH="${IS_NORM_PATH#/}"
52
+ }
53
+
54
+ # adapter_claims_add <adapter> <kind> <path>
55
+ # Registers the claim, or sets IS_ADAPTER_CLAIM_CONFLICT and returns 1.
56
+ # shellcheck disable=SC2034 # the conflict text is consumed by the caller
57
+ adapter_claims_add() {
58
+ local adapter="$1" kind="$2" path
59
+ adapter_contract_rel_path "$3"
60
+ path="$IS_ADAPTER_REL_PATH"
61
+ local prev_adapter prev_kind prev_path
62
+ while IFS=$'\t' read -r prev_adapter prev_kind prev_path; do
63
+ [ -n "$prev_adapter" ] || continue
64
+ [ "$prev_adapter" = "$adapter" ] && continue
65
+ case "$path" in
66
+ "$prev_path"|"$prev_path"/*) ;;
67
+ *) case "$prev_path" in "$path"/*) ;; *) continue ;; esac ;;
68
+ esac
69
+ # Two `managed` claims agree by design; anything else prunes.
70
+ [ "$kind" = "owned" ] || [ "$prev_kind" = "owned" ] || continue
71
+ if [ "$path" = "$prev_path" ]; then
72
+ IS_ADAPTER_CLAIM_CONFLICT="adapters '$prev_adapter' ($prev_kind) and '$adapter' ($kind) both claim '$path'; an owned path belongs to one adapter, so whichever syncs last prunes the other's output"
73
+ else
74
+ IS_ADAPTER_CLAIM_CONFLICT="adapter '$adapter' claims '$path' ($kind) nested in '$prev_path' ($prev_kind) claimed by '$prev_adapter'; whichever syncs last prunes the other's output"
75
+ fi
76
+ return 1
77
+ done <<< "$IS_ADAPTER_CLAIMS"
78
+ IS_ADAPTER_CLAIMS="$IS_ADAPTER_CLAIMS$adapter"$'\t'"$kind"$'\t'"$path"$'\n'
79
+ }
80
+
81
+ # adapter_claims_add_records <adapter> <records>
82
+ # Feeds every owned/managed record of one adapter through adapter_claims_add.
83
+ # Returns 1 on the first conflict, which stays in IS_ADAPTER_CLAIM_CONFLICT.
84
+ adapter_claims_add_records() {
85
+ local adapter="$1" records="$2" kind value
86
+ while IFS=$'\t' read -r kind value; do
87
+ case "$kind" in
88
+ owned|managed) ;;
89
+ *) continue ;;
90
+ esac
91
+ adapter_claims_add "$adapter" "$kind" "$value" || return 1
92
+ done <<< "$records"
93
+ }
94
+
22
95
  # Reject records that could address anything outside the repository. Contract
23
96
  # paths are always repo-relative; ignore/include records may contain globs.
24
97
  adapter_contract_safe_path() {
package/engine/sync.sh CHANGED
@@ -214,23 +214,50 @@ finish_sync_transaction() {
214
214
  trap finish_sync_transaction EXIT
215
215
  trap 'exit 130' INT TERM
216
216
 
217
+ # Ownership is checked across EVERY enabled adapter, not only the selected one:
218
+ # `intelligence sync codex` still prunes a path another adapter owns, so a
219
+ # filtered run that skipped the check would be the one that destroys output.
220
+ adapter_claims_reset
221
+ agents_dependents=""
222
+
223
+ # note_agents_dependent <adapter> — collect the AGENTS.md consumers for the
224
+ # root-path warning below. The list is built with its separators so no pattern
225
+ # substitution is needed: Bash 3.2 is the floor, and its ${v//} keeps the
226
+ # backslash of an escaped separator.
227
+ note_agents_dependent() {
228
+ [ -n "$agents_dependents" ] && agents_dependents="$agents_dependents, "
229
+ agents_dependents="$agents_dependents$1"
230
+ }
217
231
  preflight_idx=0
218
232
  while [ "$preflight_idx" -lt "${#ADAPTERS[@]}" ]; do
219
233
  adapter="${ADAPTERS[$preflight_idx]}"
220
234
  adapter_file="${ADAPTER_FILES[$preflight_idx]}"
221
235
  preflight_idx=$((preflight_idx + 1))
222
- if [ -n "$TARGET_FILTER" ] && [ "$adapter" != "$TARGET_FILTER" ]; then
223
- continue
224
- fi
225
236
  target_enabled_var "$CONFIG_FILE" "$adapter"
226
237
  [ "$IS_TGT_ENABLED" = "1" ] || continue
227
238
  target_output_var "$CONFIG_FILE" "$adapter"
228
239
  output="$IS_TGT_OUTPUT"
229
240
  [ -n "$output" ] || output=".$adapter"
230
- validate_output_path "$REPO_ROOT" "$CONFIG_FILE" "$adapter" "$REPO_ROOT/$output"
231
241
  records="$(adapter_contract_records "$adapter" "$adapter_file" "$output")" || exit 1
242
+
243
+ if ! adapter_claims_add_records "$adapter" "$records"; then
244
+ echo "ERROR: $IS_ADAPTER_CLAIM_CONFLICT." >&2
245
+ echo " Give each adapter its own output: intelligence adapter list" >&2
246
+ exit 1
247
+ fi
248
+
249
+ if [ -n "$TARGET_FILTER" ] && [ "$adapter" != "$TARGET_FILTER" ]; then
250
+ # Not selected: its claims are registered, its output is not touched.
251
+ while IFS=$'\t' read -r kind value; do
252
+ [ "$kind" = "requires" ] && [ "$value" = "agents" ] && note_agents_dependent "$adapter"
253
+ done <<< "$records"
254
+ continue
255
+ fi
256
+
257
+ validate_output_path "$REPO_ROOT" "$CONFIG_FILE" "$adapter" "$REPO_ROOT/$output"
232
258
  while IFS=$'\t' read -r kind value; do
233
259
  [ "$kind" = "requires" ] || continue
260
+ [ "$value" = "agents" ] && note_agents_dependent "$adapter"
234
261
  target_enabled_var "$CONFIG_FILE" "$value"
235
262
  if [ "$IS_TGT_ENABLED" != "1" ]; then
236
263
  echo "ERROR: targets.$adapter requires enabled target '$value'." >&2
@@ -244,6 +271,21 @@ while [ "$preflight_idx" -lt "${#ADAPTERS[@]}" ]; do
244
271
  esac
245
272
  done <<< "$records"
246
273
  done
274
+
275
+ # Every adapter that requires `agents` skips always-on rules because AGENTS.md
276
+ # carries them — and each of those tools reads AGENTS.md at the workspace root
277
+ # only. Rendered anywhere else, the always-on rules reach no tool at all, and a
278
+ # stale root copy from an earlier run is what they keep reading.
279
+ if [ -n "$agents_dependents" ]; then
280
+ target_output_var "$CONFIG_FILE" "agents"
281
+ # Resolved, not lexical: `./` renders `./AGENTS.md`, which IS the
282
+ # workspace-root file every dependent adapter reads.
283
+ adapter_contract_rel_path "$(agents_output_path "${IS_TGT_OUTPUT:-.agents}")"
284
+ agents_rel="$IS_ADAPTER_REL_PATH"
285
+ if [ "$agents_rel" != "AGENTS.md" ]; then
286
+ echo "WARNING: targets.agents.output renders '$agents_rel', not the workspace-root AGENTS.md. These adapters skip always-on rules because AGENTS.md carries them, and they read it at the root only: $agents_dependents. No tool loads those rules from '$agents_rel'." >&2
287
+ fi
288
+ fi
247
289
  SYNC_TX_ACTIVE=1
248
290
 
249
291
  synced=0
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ainova-systems/intelligence",
3
- "version": "0.16.0",
3
+ "version": "0.17.0",
4
4
  "description": "Build, version and distribute AI agent intelligence across your organization — one CLI, versioned Intelligence Packages, and a sync engine for Claude Code, Cursor, Copilot, Codex, Pi and OpenCode.",
5
5
  "bin": {
6
6
  "intelligence": "bin/intelligence.js"
@@ -89,6 +89,15 @@ or unsupported contracts before enabling or syncing an adapter. `owned` and
89
89
  `managed` paths form the transactional write-set: if any adapter fails, the
90
90
  engine restores every selected adapter path to its pre-sync state.
91
91
 
92
+ `owned` is exclusive across enabled adapters. Two adapters may declare the same
93
+ `managed` path — that is how Antigravity, Codex, Pi and OpenCode share
94
+ `.agents/skills` — but an `owned` path that another enabled adapter also claims,
95
+ or that nests inside one, is refused by `sync`, `adapter enable` and
96
+ `status --check`. Without that refusal the adapter that syncs last prunes the
97
+ other's output and both runs report success. A filtered `sync <adapter>` is
98
+ checked the same way: it prunes the same paths while never loading the adapter
99
+ whose output it destroys.
100
+
92
101
  For an existing `.vscodeignore`, `.npmignore`, or `.dockerignore`, enable/init
93
102
  also excludes the configured adapter output plus its `owned`, `managed`, and
94
103
  `legacy` paths from published or build artifacts. This packaging policy is
@@ -177,6 +186,21 @@ Every successful sync prints an adapter-agnostic `CONTEXT:` summary with source
177
186
 
178
187
  The Codex adapter checks its `project_doc_max_bytes` default (32 KiB). When generated `AGENTS.md` exceeds it, sync prints the byte count and a sufficient Codex setting. The warning says "may truncate" because a developer can already have a larger override. `targets.codex.warn_project_doc_limit` accepts `true` or omission for the default threshold, a positive byte count to match another effective limit, or `false` to disable the Intelligence warning. Both inline and block target forms work, and the field does not change Codex configuration. The warning remains visible in `sync --compact`.
179
188
 
189
+ The Antigravity adapter checks the 12,000-character limit its documentation
190
+ states for a rule file. A generated scoped rule above it is reported with its
191
+ rendered character count, because the tool decides what to do with the excess
192
+ and never says so. `targets.antigravity.warn_rule_limit` accepts `true` or
193
+ omission for the documented limit, a positive character count to match another
194
+ effective limit, or `false` to disable the warning.
195
+
196
+ Antigravity also reads `.agents/rules` and `.agents/agents` only — its paths are
197
+ fixed, not configurable — so a `targets.antigravity.output` anywhere else warns
198
+ that nothing will load the result, and names an earlier `.agents/` copy still on
199
+ disk. The same applies to `AGENTS.md`: every adapter that requires the `agents`
200
+ target skips always-on rules because `AGENTS.md` carries them, and each reads it
201
+ at the workspace root, so a `targets.agents.output` elsewhere warns and lists the
202
+ adapters whose always-on rules then reach no tool.
203
+
180
204
  ### Skills
181
205
 
182
206
  Skills follow the [Agent Skills standard](https://agentskills.io). Copy each skill directory as a complete bundle—not only `SKILL.md`—because its body may reference `references/`, `scripts/` or `assets/` beside it.
@@ -333,6 +333,7 @@ By default, commit the manifest, lock, project-owned content, `AGENTS.md`, and s
333
333
  CLAUDE.md
334
334
  .cursorrules
335
335
  GEMINI.md
336
+ .antigravity.md
336
337
 
337
338
  # Generated Claude and Cursor content; shared settings remain trackable
338
339
  .claude/*
@@ -415,7 +416,7 @@ The permanent applied-schema key is the top-level scalar `schema_version` in `in
415
416
  The public lifecycle is deliberately compact:
416
417
 
417
418
  - `intelligence init [--preview|--apply]` is universal: it creates a new setup, aligns an existing Intelligence project, or plans/applies conversion of an eligible legacy Intelligence Sync project.
418
- - `intelligence sync [adapter] [--compact]` first aligns an existing Intelligence project with the installed CLI, restores a missing store strictly from `intelligence.lock`, then renders. Compact mode shows context sizes, actionable warnings and final status on success, and all diagnostics on failure. In CI it refuses an alignment that would change tracked files and points to a local `intelligence init --apply` plus review/commit.
419
+ - `intelligence sync [adapter] [--compact] [--force]` first aligns an existing Intelligence project with the installed CLI and restores a missing store strictly from `intelligence.lock`. It skips rendering when local inputs and outputs match a previous successful run. Changes or missing outputs trigger rendering; `--force` always renders and refreshes the search for unconfigured source directories. An unchanged run replays the last full run's useful diagnostics. Compact mode shows context sizes, actionable warnings and final status on success, and all diagnostics on failure. In CI it refuses an alignment that would change tracked files and points to a local `intelligence init --apply` plus review/commit.
419
420
  - `intelligence update [@scope/name] [--preview|--apply]` is the only update surface. It prints the CLI/project/package plan; default mode prompts, `--preview` never writes, and `--apply` does not prompt. It never moves `ref:` pins.
420
421
  - `intelligence upgrade [--next] [--preview|--apply]` replaces the installed CLI with the newest version on its npm channel (`next` for a prerelease or with `--next`, otherwise `latest`) with the same modes. It touches no project, never downgrades, and refuses an installation that npm did not make.
421
422
  - `intelligence package add|remove|list|search` owns package inventory.
@@ -10,8 +10,10 @@ context: fork
10
10
  1. Run `intelligence sync` (or `intelligence sync <adapter>` when one adapter
11
11
  was requested). For Intelligence projects, this command first aligns project schema/content
12
12
  with the installed CLI and restores a missing package store strictly from
13
- `intelligence.lock`.
14
- 2. Require final `IS_STATUS=ok`; relay per-adapter counts and any integrity,
13
+ `intelligence.lock`. Unchanged inputs and outputs can reuse a successful run.
14
+ Use `--force` when a fresh render or a new search for unconfigured source
15
+ directories is requested; an unchanged run replays the previous diagnostics.
16
+ 2. Require final `IS_STATUS=ok`; relay reported counts and any integrity,
15
17
  model-drift or unsynced-source warnings.
16
18
  3. In CI, a required tracked project upgrade is intentionally refused. Report
17
19
  the instruction to run `intelligence init --apply` locally, review and