@ainova-systems/intelligence 0.16.1 → 0.17.1

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,24 @@
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
- source "$CLI_DIR/lib/cli-common.sh"
5
4
 
5
+ # Print before library loading and lifecycle checks so a content verification
6
+ # never looks idle. Compact output remains a buffered machine-readable report.
6
7
  compact=0
8
+ for argument in "$@"; do
9
+ [ "$argument" != --compact ] || compact=1
10
+ done
11
+ [ "$compact" -ne 0 ] || echo 'Checking project...'
12
+ source "$CLI_DIR/lib/cli-common.sh"
13
+ force=0
7
14
  target=""
8
15
  while [ $# -gt 0 ]; do
9
16
  case "$1" in
10
17
  --compact) compact=1 ;;
18
+ --force) force=1 ;;
11
19
  -*) die "unknown option '$1'" ;;
12
20
  *)
13
- [ -z "$target" ] || die "usage: intelligence sync [adapter] [--compact]"
21
+ [ -z "$target" ] || die "usage: intelligence sync [adapter] [--compact] [--force]"
14
22
  target="$1"
15
23
  ;;
16
24
  esac
@@ -28,13 +36,10 @@ run_sync() {
28
36
  ensure_project_current "$IP_ROOT" || return $?
29
37
  restore_project_store_if_missing "$IP_ROOT" || return $?
30
38
  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
39
+ sync_with_cache "$target" "$force" || return $?
36
40
  ;;
37
41
  legacy)
42
+ [ "$force" -eq 0 ] || die "--force is unavailable for a legacy Intelligence Sync project - run 'intelligence init' to convert it first"
38
43
  [ "$compact" -eq 0 ] || die "--compact is unavailable for a legacy Intelligence Sync project - run 'intelligence init' to convert it first"
39
44
  # A vendored project syncs with its own engine: its pin is the
40
45
  # 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 —
@@ -0,0 +1,219 @@
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
+ # Keep list descriptors open across the walk; reopening each line is expensive
20
+ # on Windows and provides no additional content verification.
21
+ sync_cache_fingerprint() (
22
+ local work="$1" path kind executable seen=$'\n'
23
+ local -a existing
24
+ existing=()
25
+ shift
26
+ : > "$work/entries" || return 1
27
+ : > "$work/files" || return 1
28
+ : > "$work/list" || return 1
29
+ for path in "$@"; do
30
+ sync_cache_safe_path "$path" || return 1
31
+ # Shared managed destinations are declared by several adapters. Walking
32
+ # the same tree again only multiplies filesystem probes on Windows.
33
+ case "$seen" in *$'\n'"$path"$'\n'*) continue ;; esac
34
+ seen="$seen$path"$'\n'
35
+ if [ -e "$path" ]; then
36
+ existing+=("$path")
37
+ else
38
+ printf 'missing\t%s\n' "$path" >> "$work/entries" || return 1
39
+ fi
40
+ done
41
+ if [ "${#existing[@]}" -gt 0 ]; then
42
+ find "${existing[@]}" -print0 > "$work/list" || return 1
43
+ fi
44
+ while IFS= read -r -d '' path; do
45
+ # Starting-path ancestors were checked above. find does not follow
46
+ # links, so checking each returned entry once covers the entire tree.
47
+ case "$path" in *[[:cntrl:]]*|*\\*|*\"*) return 1 ;; esac
48
+ [ ! -L "$path" ] || return 1
49
+ executable=0
50
+ if [ -f "$path" ]; then
51
+ kind='file'
52
+ [ ! -x "$path" ] || executable=1
53
+ printf '%s\n' "$path" >&4 || return 1
54
+ elif [ -d "$path" ]; then
55
+ kind=directory
56
+ else
57
+ return 1
58
+ fi
59
+ printf '%s\t%s\t%s\n' "$kind" "$executable" "$path" >&3 || return 1
60
+ done < "$work/list" 3>> "$work/entries" 4> "$work/files" || return 1
61
+ # find order need not be stable. Sort both streams; preserve path-to-hash
62
+ # association by feeding the sorted file list into a single git process.
63
+ LC_ALL=C sort -u "$work/entries" > "$work/sorted" || return 1
64
+ LC_ALL=C sort -u "$work/files" > "$work/paths" || return 1
65
+ if command -v cygpath >/dev/null 2>&1; then
66
+ # Git for Windows does not translate POSIX paths received on stdin.
67
+ cygpath -m -f "$work/paths" > "$work/native" || return 1
68
+ else
69
+ cp "$work/paths" "$work/native" || return 1
70
+ fi
71
+ git hash-object --no-filters --stdin-paths < "$work/native" > "$work/hashes" || return 1
72
+ cat "$work/sorted" "$work/paths" "$work/hashes" | git hash-object --stdin || return 1
73
+ )
74
+
75
+ sync_cache_dependencies() {
76
+ local src section adapter file output records kind value
77
+ SC_INPUTS=("$CONFIG_FILE" "$REPO_ROOT/intelligence.lock"
78
+ "$CLI_DIR/intelligence" "$CLI_DIR/engine-package.yaml"
79
+ "$CLI_DIR/commands" "$CLI_DIR/internal" "$CLI_DIR/lib" "$IS_ENGINE_DIR"
80
+ "$REPO_ROOT/$IS_CONTENT_REL/adapters")
81
+ SC_OUTPUTS=()
82
+ # Project adapters can read arbitrary files or environment and execute any
83
+ # code. Their dependencies cannot be inferred from their write contract.
84
+ for file in "$REPO_ROOT/$IS_CONTENT_REL/adapters"/*.sh; do
85
+ [ ! -e "$file" ] && [ ! -L "$file" ] || return 1
86
+ done
87
+ for section in rules agents skills; do
88
+ load_yaml_list "$CONFIG_FILE" "$section"
89
+ while IFS= read -r src; do
90
+ [ -n "$src" ] || continue
91
+ adapter_contract_safe_concrete_path "$src" || return 1
92
+ case "$src" in .|./|.intelligence|.intelligence/|.intelligence/sync-cache*) return 1 ;; esac
93
+ SC_INPUTS+=("$REPO_ROOT/$src")
94
+ done <<< "$IS_YAML_LIST"
95
+ done
96
+ load_targets_cache "$CONFIG_FILE"
97
+ for file in "$IS_ENGINE_DIR/adapters"/*.sh; do
98
+ adapter="${file##*/}"; adapter="${adapter%.sh}"
99
+ [ "$adapter" != _template ] || continue
100
+ target_enabled_var "$CONFIG_FILE" "$adapter"
101
+ [ "$IS_TGT_ENABLED" = 1 ] || continue
102
+ case "$adapter" in agents|antigravity|claude|codex|copilot|cursor|opencode|pi) ;; *) return 1 ;; esac
103
+ target_output_var "$CONFIG_FILE" "$adapter"
104
+ output="${IS_TGT_OUTPUT:-.$adapter}"
105
+ records="$(adapter_contract_records "$adapter" "$file" "$output")" || return 1
106
+ while IFS=$'\t' read -r kind value; do
107
+ case "$kind" in owned|managed) SC_OUTPUTS+=("$REPO_ROOT/$value") ;; esac
108
+ done <<< "$records"
109
+ done
110
+ [ "${#SC_OUTPUTS[@]}" -gt 0 ]
111
+ }
112
+
113
+ sync_cache_input_hash() {
114
+ local fingerprint
115
+ fingerprint="$(sync_cache_fingerprint "$1" "${SC_INPUTS[@]}")" || return 1
116
+ # Explicit environment values that affect rendering, discovery or parsing.
117
+ # No secrets or inherited environment dump are persisted.
118
+ printf '%s\n' "$fingerprint" "$2" "$REPO_ROOT" "$CONFIG_FILE" \
119
+ "$IS_CONTENT_REL" "$IS_MODULE_REL" "$IS_SYNC_CMD" "$IS_MANIFEST_NAME" \
120
+ "$IS_PROTECTED_DIRS" "$BASH_VERSION" "${OSTYPE:-}" "${LANG:-}" \
121
+ "${LC_ALL:-}" "${LC_CTYPE:-}" "${LC_COLLATE:-}" "${PATH:-}" "$(umask)" | git hash-object --stdin
122
+ }
123
+
124
+ sync_cache_directory() {
125
+ local path="$REPO_ROOT/.intelligence/sync-cache" physical root_physical probe expected
126
+ sync_cache_safe_path "$path/state" || return 1
127
+ root_physical="$(cd "$REPO_ROOT" && pwd -P)" || return 1
128
+ probe="$path"; expected="$root_physical/.intelligence/sync-cache"
129
+ while [ ! -e "$probe" ]; do probe="${probe%/*}"; expected="${expected%/*}"; done
130
+ physical="$(cd "$probe" && pwd -P)" || return 1
131
+ [ "$physical" = "$expected" ] || return 1
132
+ if [ "${1:-}" = create ]; then mkdir -p "$path" || return 1; fi
133
+ SC_DIRECTORY="$path"
134
+ }
135
+
136
+ sync_cache_read() {
137
+ local file="$1" input="$2" output="$3" work="$4" header report_hash actual
138
+ [ -f "$file" ] && [ ! -L "$file" ] || return 1
139
+ IFS= read -r header < "$file" || return 1
140
+ case "$header" in "intelligence-sync-cache-v1 $input $output "*) ;; *) return 1 ;; esac
141
+ report_hash="${header##* }"
142
+ case "$report_hash" in ''|*[!a-f0-9]*) return 1 ;; esac
143
+ [ "${#report_hash}" -eq 40 ] || [ "${#report_hash}" -eq 64 ] || return 1
144
+ tail -n +2 "$file" > "$work/report" || return 1
145
+ actual="$(git hash-object --stdin < "$work/report")" || return 1
146
+ [ "$actual" = "$report_hash" ] || return 1
147
+ grep -q '^IS_STATUS=ok\($\| \)' "$work/report" || return 1
148
+ grep -q '^=== Done:' "$work/report" || return 1
149
+ }
150
+
151
+ # Keep actionable diagnostics, including the multi-line unsynced-directory
152
+ # report that compact mode intentionally omits. Never replay renderer progress.
153
+ sync_cache_report() {
154
+ awk '
155
+ /^(WARNING:| WARN:)/ { warning=1; print; next }
156
+ warning && /^ / { print; next }
157
+ { warning=0 }
158
+ /^(CONTEXT:|IS_STATUS=ok|=== Done:|=== WARNING:| NOT SYNCED:| Wire one in:)/ { print }
159
+ ' "$1"
160
+ }
161
+
162
+ sync_with_cache() (
163
+ local target="$1" force="$2" work before after outputs report_hash rc=0 cacheable=0
164
+ work="$(mktemp -d -t intelligence-cache-XXXXXX)" || return 1
165
+ trap 'rm -rf "$work"' EXIT
166
+ echo "Checking source files..."
167
+ if sync_cache_dependencies && sync_cache_directory; then
168
+ before="$(sync_cache_input_hash "$work" "$target")" || before=""
169
+ [ -z "$before" ] || cacheable=1
170
+ fi
171
+ if [ "$cacheable" = 1 ] && [ "$force" = 0 ]; then
172
+ echo "Checking generated files..."
173
+ outputs="$(sync_cache_fingerprint "$work" "${SC_OUTPUTS[@]}")" || outputs=""
174
+ if [ -n "$outputs" ] && sync_cache_read "$SC_DIRECTORY/state" "$before" "$outputs" "$work"; then
175
+ echo 'Unchanged: generated files are up to date.'
176
+ printf '\n'
177
+ cat "$work/report"
178
+ printf '\n=== Unchanged: no files needed updating ===\n'
179
+ return 0
180
+ fi
181
+ fi
182
+ # A failed or interrupted attempt must not leave a success cache reusable.
183
+ if [ "$cacheable" = 1 ]; then
184
+ rm -f "$SC_DIRECTORY/state" || cacheable=0
185
+ fi
186
+ # Stream progress while retaining diagnostics for the next cache hit. Check
187
+ # both processes: a failed capture must never publish a success record.
188
+ local -a pipeline_status
189
+ if bash "$IS_ENGINE_DIR/sync.sh" "$target" 2>&1 | tee "$work/log"; then
190
+ pipeline_status=("${PIPESTATUS[@]}")
191
+ else
192
+ pipeline_status=("${PIPESTATUS[@]}")
193
+ fi
194
+ rc="${pipeline_status[0]}"
195
+ [ "$rc" = 0 ] || return "$rc"
196
+ [ "${pipeline_status[1]}" = 0 ] || return "${pipeline_status[1]}"
197
+ if [ "$cacheable" = 1 ]; then
198
+ (
199
+ echo "Checking sync results..."
200
+ after="$(sync_cache_input_hash "$work" "$target")" || return 0
201
+ [ "$before" = "$after" ] || return 0
202
+ outputs="$(sync_cache_fingerprint "$work" "${SC_OUTPUTS[@]}")" || return 0
203
+ sync_cache_report "$work/log" > "$work/report" || return 0
204
+ report_hash="$(git hash-object --stdin < "$work/report")" || return 0
205
+ # Recheck containment before publishing. mktemp+rename avoids following a
206
+ # pre-existing state-file link, and readers see only complete records.
207
+ sync_cache_directory create || return 0
208
+ local staged
209
+ staged="$(mktemp "$SC_DIRECTORY/state-XXXXXX")" || return 0
210
+ if { printf 'intelligence-sync-cache-v1 %s %s %s\n' "$after" "$outputs" "$report_hash"; cat "$work/report"; } > "$staged"; then
211
+ mv -f "$staged" "$SC_DIRECTORY/state" || rm -f "$staged"
212
+ else
213
+ rm -f "$staged"
214
+ fi
215
+ )
216
+ fi
217
+ echo "=== Sync complete ==="
218
+ return 0
219
+ )
package/engine/ENGINE_SHA CHANGED
@@ -1 +1 @@
1
- 365ec21e2f2d4d9ef323ed9f1387b3c3c4bef845
1
+ 3580413cb402a47d3defc33afbdd190ef23cc430
package/engine/VERSION CHANGED
@@ -1 +1 @@
1
- 0.16.1
1
+ 0.17.1
@@ -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.1",
3
+ "version": "0.17.1",
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