@ainova-systems/intelligence 0.11.8 → 0.12.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.
@@ -123,6 +123,7 @@ case "$IP_MODE" in
123
123
  [ "$force" -eq 0 ] || die "--force applies only when converting a legacy Intelligence Sync project"
124
124
  if [ "$preview" -eq 1 ]; then
125
125
  check_version_compat "$IP_ROOT/intelligence.yaml"
126
+ validate_project_lock "$IP_ROOT"
126
127
  echo "project: Intelligence CLI at $IP_ROOT"
127
128
  if project_stamped_ahead "$IP_ROOT"; then
128
129
  echo " project schema $(read_schema_version "$IP_ROOT/intelligence.yaml") is ahead of engine $(bundled_engine_version) — left as is (update the CLI)"
@@ -223,9 +224,20 @@ if [ -n "$targets_arg" ]; then
223
224
  else
224
225
  if [ -d "$root/.claude" ] || [ -f "$root/CLAUDE.md" ]; then targets="$targets claude"; fi
225
226
  if [ -d "$root/.cursor" ] || [ -f "$root/.cursorrules" ]; then targets="$targets cursor"; fi
226
- if [ -d "$root/.codex" ] || [ -d "$root/.agents" ]; then targets="$targets codex"; fi
227
+ # `.agents/skills` is the only part of the shared root Codex reads, so it is
228
+ # the only part that may imply Codex. `.agents/` as a whole cannot: its
229
+ # `rules/`, `agents/` and `workflows/` siblings belong to Antigravity, and
230
+ # treating them as a Codex marker would invent a tool the project shows no
231
+ # trace of.
232
+ if [ -d "$root/.codex" ] || [ -d "$root/.agents/skills" ]; then targets="$targets codex"; fi
227
233
  [ -d "$root/.pi" ] && targets="$targets pi"
228
234
  [ -d "$root/.opencode" ] && targets="$targets opencode"
235
+ if [ -f "$root/GEMINI.md" ] \
236
+ || [ -d "$root/.agents/rules" ] \
237
+ || [ -d "$root/.agents/agents" ] \
238
+ || [ -d "$root/.agents/workflows" ]; then
239
+ targets="$targets antigravity"
240
+ fi
229
241
  if [ -f "$root/.github/copilot-instructions.md" ] \
230
242
  || [ -d "$root/.github/instructions" ] \
231
243
  || [ -d "$root/.github/agents" ] \
@@ -12,22 +12,32 @@ esac
12
12
  detect_project
13
13
  case "$IP_MODE" in
14
14
  cli)
15
+ lock_valid=1
15
16
  echo "Project: $IP_ROOT (Intelligence project)"
16
17
  echo "Manifest: intelligence.yaml (schema_version $(read_schema_version "$IP_ROOT/intelligence.yaml"))"
17
- if [ -f "$IP_ROOT/intelligence.lock" ]; then
18
+ if ! check_project_lock "$IP_ROOT"; then
19
+ lock_valid=0
20
+ echo "Lockfile: INVALID (locked state unchecked)"
21
+ elif [ -f "$IP_ROOT/intelligence.lock" ]; then
18
22
  echo "Lockfile: intelligence.lock"
19
23
  elif [ -n "$(qmap_keys "$IP_ROOT/intelligence.yaml" "packages")" ]; then
20
24
  echo "Lockfile: MISSING (manifest declares packages)"
21
25
  else
22
26
  echo "Lockfile: none (no packages added yet)"
23
27
  fi
24
- sync_locked="$(qmap_field "$IP_ROOT/intelligence.lock" "packages" "$SYNC_PKG_NAME" "resolved")"
25
- if [ -n "$sync_locked" ]; then
28
+ sync_locked=""
29
+ if [ "$lock_valid" -eq 1 ]; then
30
+ sync_locked="$(qmap_field "$IP_ROOT/intelligence.lock" "packages" "$SYNC_PKG_NAME" "resolved")"
31
+ fi
32
+ if [ "$lock_valid" -eq 0 ]; then
33
+ echo "Content: unchecked"
34
+ elif [ -n "$sync_locked" ]; then
26
35
  echo "Content: $SYNC_PKG_NAME at $sync_locked"
27
36
  else
28
37
  echo "Content: no $SYNC_PKG_NAME (bare setup)"
29
38
  fi
30
39
  echo "Engine: $(bundled_engine_version) (bundled with the CLI)"
40
+ [ "$lock_valid" -eq 1 ] || exit 1
31
41
  ;;
32
42
  legacy)
33
43
  echo "Project: $IP_ROOT (legacy Intelligence Sync)"
@@ -22,6 +22,7 @@ done
22
22
  require_cli_project
23
23
  manifest="$IP_ROOT/intelligence.yaml"
24
24
  check_version_compat "$manifest"
25
+ validate_project_lock "$IP_ROOT"
25
26
  eng="$(bundled_engine_version)"
26
27
  stamp="$(read_schema_version "$manifest")"
27
28
  project_change=0
@@ -31,6 +31,12 @@ esac
31
31
  echo "project: $IP_ROOT"
32
32
  manifest="$IP_ROOT/intelligence.yaml"
33
33
  lock="$IP_ROOT/intelligence.lock"
34
+ lock_valid=1
35
+ if ! check_project_lock "$IP_ROOT"; then
36
+ problems=$((problems + 1))
37
+ lock_valid=0
38
+ note "locked package identities cannot be inspected; continuing independent project checks"
39
+ fi
34
40
 
35
41
  stamp="$(read_schema_version "$manifest")"
36
42
  eng="$(bundled_engine_version)"
@@ -50,7 +56,10 @@ fi
50
56
 
51
57
  # The engine-content package must sit at the bundled engine version — the
52
58
  # content documents exactly the scripts the CLI executes.
53
- sync_locked="$(qmap_field "$lock" "packages" "$SYNC_PKG_NAME" "resolved")"
59
+ sync_locked=""
60
+ if [ "$lock_valid" -eq 1 ]; then
61
+ sync_locked="$(qmap_field "$lock" "packages" "$SYNC_PKG_NAME" "resolved")"
62
+ fi
54
63
  if [ -n "$sync_locked" ]; then
55
64
  if [ "${sync_locked#v}" = "$eng" ]; then
56
65
  ok "$SYNC_PKG_NAME at $sync_locked (matches the engine)"
@@ -70,6 +79,10 @@ fi
70
79
  while IFS= read -r name; do
71
80
  [ -n "$name" ] || continue
72
81
  assert_valid_pkg_name "$name"
82
+ if [ "$lock_valid" -eq 0 ]; then
83
+ note "$name: locked state unchecked"
84
+ continue
85
+ fi
73
86
  if [ -z "$(qmap_field "$lock" "packages" "$name" "url")" ]; then
74
87
  warn "$name is in the manifest but not in intelligence.lock — run 'intelligence init'"
75
88
  else
@@ -143,7 +156,7 @@ while IFS= read -r target; do
143
156
  done < <(target_names "$manifest")
144
157
 
145
158
  # Lock orphans.
146
- if [ -f "$lock" ]; then
159
+ if [ "$lock_valid" -eq 1 ] && [ -f "$lock" ]; then
147
160
  while IFS= read -r name; do
148
161
  [ -n "$name" ] || continue
149
162
  found=0
@@ -165,7 +178,13 @@ for section in rules agents skills; do
165
178
  if [ ! -d "$IP_ROOT/$src" ]; then
166
179
  case "$src" in
167
180
  # Store content is restorable state: absent means un-installed.
168
- .intelligence/*) warn "sources.$section '$src' missing — run 'intelligence sync'" ;;
181
+ .intelligence/*)
182
+ if [ "$lock_valid" -eq 1 ]; then
183
+ warn "sources.$section '$src' missing — run 'intelligence sync'"
184
+ else
185
+ warn "sources.$section '$src' missing — resolve the lock problem first"
186
+ fi
187
+ ;;
169
188
  # A project-owned dir that does not exist yet is a legitimate
170
189
  # state — a project consuming only packages authors nothing of
171
190
  # its own — and the engine simply skips it. Declared up front so
@@ -183,7 +183,9 @@ while IFS="$LOCK_SEP" read -r pack name url ref mirror; do
183
183
  mkdir -p "$dest"
184
184
  cp -R "$root/$mirror/." "$dest/"
185
185
  if [ -f "$dest/.pack" ]; then
186
- sha="$(awk -F= '$1 == "sha" {print $2; exit}' "$dest/.pack")"
186
+ sha="$(awk -F= '{ sub(/\r$/, "") } $1 == "sha" {print $2; exit}' "$dest/.pack")"
187
+ [[ "$sha" =~ ^[0-9a-f]{40}$ || "$sha" =~ ^[0-9a-f]{64}$ ]] \
188
+ || die "pack '$pack' mirror '$mirror' has a missing or invalid recorded commit SHA in .pack — legacy project unchanged; restore its trusted ownership stamp from history or refresh the mirror with its archived engine, then rerun 'intelligence init'"
187
189
  rm -f "$dest/.pack"
188
190
  fi
189
191
  echo " pack '$pack' -> $name (from mirror $mirror)"
@@ -191,9 +193,8 @@ while IFS="$LOCK_SEP" read -r pack name url ref mirror; do
191
193
  sha="$(fetch_package "$url" "$ref" "" "$dest")"
192
194
  echo " pack '$pack' -> $name (fetched)"
193
195
  fi
194
- if [ -z "$sha" ] || [ "$sha" = "unknown" ]; then
195
- sha="$(GIT_TERMINAL_PROMPT=0 git ls-remote -- "$url" "$ref" 2>/dev/null | awk '{print $1; exit}')"
196
- fi
196
+ # A ref currently advertised by the remote cannot prove which commit
197
+ # produced an existing mirror. Only its recorded identity can do that.
197
198
  [ -n "$ref" ] || ref="HEAD"
198
199
  printf '%s\037%s\037%s\037%s\037%s\037%s\n' \
199
200
  "$name" "" "$url" "" "$ref" "$sha" >> "$lock_rows"
@@ -6,6 +6,8 @@ source "$CLI_DIR/lib/cli-common.sh"
6
6
  require_cli_project
7
7
  manifest="$IP_ROOT/intelligence.yaml"
8
8
  lock="$IP_ROOT/intelligence.lock"
9
+ lock_valid=1
10
+ if ! check_project_lock "$IP_ROOT"; then lock_valid=0; fi
9
11
 
10
12
  count=0
11
13
  while IFS= read -r name; do
@@ -13,9 +15,14 @@ while IFS= read -r name; do
13
15
  count=$((count + 1))
14
16
  range="$(qmap_field "$manifest" "packages" "$name" "version")"
15
17
  ref="$(qmap_field "$manifest" "packages" "$name" "ref")"
16
- resolved="$(qmap_field "$lock" "packages" "$name" "resolved")"
17
- sha="$(qmap_field "$lock" "packages" "$name" "sha")"
18
- if [ ! -f "$lock" ]; then
18
+ resolved="" sha=""
19
+ if [ "$lock_valid" -eq 1 ]; then
20
+ resolved="$(qmap_field "$lock" "packages" "$name" "resolved")"
21
+ sha="$(qmap_field "$lock" "packages" "$name" "sha")"
22
+ fi
23
+ if [ "$lock_valid" -eq 0 ]; then
24
+ state="invalid lock — locked state unchecked"
25
+ elif [ ! -f "$lock" ]; then
19
26
  state="lock missing — restore committed intelligence.lock"
20
27
  elif [ -d "$IP_ROOT/.intelligence/packages/$name" ]; then
21
28
  state="installed"
@@ -30,5 +37,8 @@ while IFS= read -r name; do
30
37
  done < <(qmap_keys "$manifest" "packages")
31
38
 
32
39
  if [ "$count" -eq 0 ]; then
33
- echo "No packages. Add one: intelligence package add @ainova-systems/core"
40
+ if [ "$lock_valid" -eq 1 ]; then
41
+ echo "No packages. Add one: intelligence package add @ainova-systems/core"
42
+ fi
34
43
  fi
44
+ [ "$lock_valid" -eq 1 ] || exit 1
@@ -21,6 +21,7 @@ done
21
21
  require_cli_project
22
22
  manifest="$IP_ROOT/intelligence.yaml"
23
23
  lock="$IP_ROOT/intelligence.lock"
24
+ validate_project_lock "$IP_ROOT" --restore
24
25
 
25
26
  # --frozen is the reproducibility contract: the lock must agree with the
26
27
  # manifest on the package SET and on every requested-intent field before a
@@ -50,20 +51,6 @@ if [ "$frozen" -eq 1 ]; then
50
51
  done < <(qmap_keys "$lock" "packages")
51
52
  fi
52
53
 
53
- # Check every required commit identity before restoring any package. Only the
54
- # matching bundled content may carry the historical empty development-build SHA.
55
- while IFS="$LOCK_SEP" read -r name _requested url path resolved sha; do
56
- [ -n "$name" ] || continue
57
- assert_valid_pkg_name "$name"
58
- assert_safe_source_url "$url"
59
- assert_safe_ref "$resolved"
60
- if [ -z "$sha" ] && is_bundle_source "$url" "$resolved" "$path"; then
61
- continue
62
- fi
63
- [[ "$sha" =~ ^[0-9a-f]{40}$ || "$sha" =~ ^[0-9a-f]{64}$ ]] \
64
- || die "locked restore: $name has a missing or invalid commit SHA — restore a valid intelligence.lock"
65
- done < <(lock_to_tsv "$lock")
66
-
67
54
  # Manifest packages missing from the lock: resolve them now (npm install
68
55
  # semantics). Under --frozen the drift gate above has already refused.
69
56
  missing=""
@@ -14,7 +14,7 @@ source "$IS_ENGINE_DIR/lib/contract.sh"
14
14
  source "$IS_ENGINE_DIR/lib/adapter-contract.sh"
15
15
 
16
16
  die() {
17
- echo "ERROR: $*" >&2
17
+ echo "ERROR: ${CLI_INPUT_CONTEXT:-}$*" >&2
18
18
  exit 1
19
19
  }
20
20
 
@@ -30,7 +30,7 @@ assert_safe_source_url() {
30
30
  local url="$1"
31
31
  case "$url" in
32
32
  ""|-*) die "unsafe source url '$url' — option-shaped or empty" ;;
33
- *[\"\'\ ]*) die "unsafe source url '$url' — quotes or spaces" ;;
33
+ *[\"\'\ ]*|*[[:cntrl:]]*) die "unsafe source url '$url' — quotes or whitespace" ;;
34
34
  https://*|http://*|ssh://*|git://*|file://*) ;;
35
35
  *@*:*) ;;
36
36
  *) die "unsafe source url '$url' — allowed: https, http, ssh, git, file, or user@host:path" ;;
@@ -43,7 +43,17 @@ assert_safe_ref() {
43
43
  [ -z "$ref" ] && return 0
44
44
  case "$ref" in
45
45
  -*) die "unsafe git ref '$ref' — option-shaped" ;;
46
- *[\"\'\ \\]*) die "unsafe git ref '$ref'" ;;
46
+ *[\"\'\ \\]*|*[[:cntrl:]]*) die "unsafe git ref '$ref'" ;;
47
+ esac
48
+ }
49
+
50
+ # Lexical package-subdirectory validation shared by acquisition and lock
51
+ # preflight. Physical containment of acquired content is a separate boundary.
52
+ assert_safe_package_path() {
53
+ # Preserve literal Git paths, but also guard Windows separator semantics.
54
+ local normalized="${1//\\//}"
55
+ case "$normalized" in
56
+ *..*|/*|[A-Za-z]:*|*[\"\']*|*[[:cntrl:]]*) die "unsafe path '$1'" ;;
47
57
  esac
48
58
  }
49
59
 
@@ -154,13 +164,16 @@ manifest_intelligence_dir() {
154
164
  default_target_output() {
155
165
  case "$1" in
156
166
  agents) printf '%s' "AGENTS.md" ;;
167
+ antigravity) printf '%s' ".agents" ;;
157
168
  copilot) printf '%s' ".github" ;;
158
169
  *) printf '.%s' "$1" ;;
159
170
  esac
160
171
  }
161
172
 
162
173
  bundled_engine_version() {
163
- tr -d ' \t\r\n' < "$IS_ENGINE_DIR/VERSION"
174
+ local version
175
+ version="$(< "$IS_ENGINE_DIR/VERSION")"
176
+ printf '%s' "${version//[$' \t\r\n']/}"
164
177
  }
165
178
 
166
179
  # --- The sync package's manifest/lock plumbing ---------------------------
@@ -297,6 +310,7 @@ project_has_packages() {
297
310
  ensure_project_current() {
298
311
  local root="$1" explicit="${2:-}" manifest="$1/intelligence.yaml" stamp eng
299
312
  check_version_compat "$manifest" || return $?
313
+ validate_project_lock "$root" || return $?
300
314
  if project_has_packages "$root" && [ ! -f "$root/intelligence.lock" ]; then
301
315
  die "manifest declares packages but intelligence.lock is absent — restore the committed lock before running project lifecycle commands"
302
316
  fi
@@ -310,6 +324,39 @@ ensure_project_current() {
310
324
  bash "$CLI_DIR/internal/align-project.sh" --no-sync
311
325
  }
312
326
 
327
+ # A present lock is validated even when there is no missing store to restore.
328
+ # Keep this before alignment: repairing the sync pin must not rewrite bad input.
329
+ validate_project_lock() {
330
+ local lock="$1/intelligence.lock" output line rc=0
331
+ if [ -e "$lock" ] || [ -L "$lock" ]; then
332
+ output="$(lock_validate "$lock" "${2---metadata}" "$1/intelligence.yaml" 2>&1)" || rc=$?
333
+ while IFS= read -r line; do
334
+ [ -n "$line" ] || continue
335
+ case "$line" in
336
+ WARNING:*)
337
+ case "${IS_LOCK_WARNINGS:-}" in *"$line"*) continue ;; esac
338
+ # Suppress only duplicate messages across lifecycle children;
339
+ # the lock itself is always parsed and validated again.
340
+ export IS_LOCK_WARNINGS="${IS_LOCK_WARNINGS:-}$line"$'\n'
341
+ ;;
342
+ esac
343
+ echo "$line" >&2
344
+ done <<< "$output"
345
+ if [ "$rc" -ne 0 ]; then
346
+ echo "ERROR: invalid intelligence.lock — recovery: https://github.com/ainova-systems/intelligence/blob/main/docs/cli.md#recovering-a-lock" >&2
347
+ return 1
348
+ fi
349
+ fi
350
+ }
351
+
352
+ # Read-only diagnosis must distinguish usable installed metadata from metadata
353
+ # which could actually restore a missing store.
354
+ check_project_lock() {
355
+ local mode=--metadata
356
+ if project_store_missing "$1"; then mode=--restore; fi
357
+ validate_project_lock "$1" "$mode"
358
+ }
359
+
313
360
  project_store_missing() {
314
361
  local root="$1" manifest="$1/intelligence.yaml" name src
315
362
  while IFS= read -r name; do
@@ -10,38 +10,74 @@
10
10
  LOCKFILE_VERSION=1
11
11
  LOCK_SEP=$'\x1f'
12
12
 
13
- # top_scalar <file> <key> — one top-level scalar (`key: "value"`), quotes and
14
- # trailing comment stripped. The declared exception to "no new parsing": the
15
- # engine's read_schema_version is hardwired to its own key.
13
+ # lock_validate <lock> [--metadata|--restore] [manifest] — read-only preflight. A subshell
14
+ # contains the fail-fast input guards so status can report a failed verdict too.
15
+ lock_validate() (
16
+ local lock="$1" mode="${2---metadata}" manifest="${3:-}" count=0
17
+ local kind name requested url path resolved sha line rows current
18
+ local -x CLI_INPUT_CONTEXT=""
19
+ [ "$#" -le 3 ] || die "internal: too many lock validation arguments"
20
+ case "$mode" in
21
+ --metadata|--restore) ;;
22
+ *) die "internal: unknown lock validation mode '$mode'" ;;
23
+ esac
24
+ # The same tokenizer supplies ordinary lock rows and strict metadata. Buffer
25
+ # before consumption: a failed parser must never yield a partial package set.
26
+ rows="$(_qmap_read lock "$lock" packages '' '' "$LOCKFILE_VERSION")" || return $?
27
+ current="$(bundled_engine_version)"
28
+ while IFS="$LOCK_SEP" read -r kind name requested url path resolved sha line; do
29
+ case "$kind" in
30
+ V)
31
+ [[ "$requested" =~ ^[0-9]+\.[0-9]+\.[0-9]+$ ]] \
32
+ || die "$lock has missing or invalid engine_version"
33
+ continue
34
+ ;;
35
+ R) ;;
36
+ *) die "internal: unexpected lock record '$kind'" ;;
37
+ esac
38
+ count=$((count + 1))
39
+ CLI_INPUT_CONTEXT="$lock:$line: package $name, field name: "
40
+ assert_valid_pkg_name "$name"
41
+ CLI_INPUT_CONTEXT="$lock:$line: package $name, field url: "
42
+ assert_safe_source_url "$url"
43
+ CLI_INPUT_CONTEXT="$lock:$line: package $name, field resolved: "
44
+ [ -n "$resolved" ] || die "locked restore: $name has a missing resolved ref"
45
+ assert_safe_ref "$resolved"
46
+ CLI_INPUT_CONTEXT="$lock:$line: package $name, field path: "
47
+ assert_safe_package_path "$path"
48
+ CLI_INPUT_CONTEXT="$lock:$line: package $name, field sha: "
49
+ if [ -z "$sha" ]; then
50
+ if [ "$url" = "$SYNC_PKG_URL" ] && [ "$path" = "$SYNC_PKG_PATH" ] \
51
+ && [ "$resolved" = "v$current" ]; then
52
+ continue
53
+ fi
54
+ # engine_version records the writer CLI, which can differ from the
55
+ # preserved bundle pin after a same-major older CLI adds a package.
56
+ # Versioned bundle metadata admits alignment/installed content only;
57
+ # it must never authorize an unpinned cross-version acquisition.
58
+ if [ "$mode" = "--metadata" ] && [ "$url" = "$SYNC_PKG_URL" ] \
59
+ && [ "$path" = "$SYNC_PKG_PATH" ] && [[ "$resolved" =~ ^v[0-9]+\.[0-9]+\.[0-9]+$ ]] \
60
+ && [ "${resolved%%.*}" = "v${current%%.*}" ]; then
61
+ echo "WARNING: $name at $resolved has no locked commit SHA; commit verification is unavailable for this development-bundle metadata" >&2
62
+ continue
63
+ fi
64
+ fi
65
+ [[ "$sha" =~ ^[0-9a-f]{40}$ || "$sha" =~ ^[0-9a-f]{64}$ ]] \
66
+ || die "locked restore: $name has a missing or invalid commit SHA — restore a valid intelligence.lock"
67
+ done <<< "$rows"
68
+ if [ "$count" -eq 0 ] && [ -n "$manifest" ] && [ -n "$(qmap_keys "$manifest" packages)" ]; then
69
+ die "$lock: packages is empty but the manifest declares packages — restore their recorded identities"
70
+ fi
71
+ )
72
+
73
+ # top_scalar <file> <key> — one top-level scalar through the shared tokenizer.
16
74
  top_scalar() {
17
- [ -f "$1" ] || return 0
18
- awk -v key="$2" '
19
- { sub(/\r$/, "") }
20
- $0 ~ "^" key ":" {
21
- v = substr($0, length(key) + 2)
22
- sub(/^[ \t]+/, "", v)
23
- if (v ~ /^"/) { v = substr(v, 2); q = index(v, "\""); if (q > 0) v = substr(v, 1, q - 1) }
24
- else { sub(/[ \t]+#.*$/, "", v); sub(/[ \t]+$/, "", v) }
25
- print v
26
- exit
27
- }
28
- ' "$1"
75
+ _qmap_read top "$1" '' "$2"
29
76
  }
30
77
 
31
78
  # lock_to_tsv <lock> — every locked package as one row.
32
79
  lock_to_tsv() {
33
- local lock="$1" name
34
- [ -f "$lock" ] || return 0
35
- while IFS= read -r name; do
36
- [ -n "$name" ] || continue
37
- printf '%s\037%s\037%s\037%s\037%s\037%s\n' \
38
- "$name" \
39
- "$(qmap_field "$lock" "packages" "$name" "requested")" \
40
- "$(qmap_field "$lock" "packages" "$name" "url")" \
41
- "$(qmap_field "$lock" "packages" "$name" "path")" \
42
- "$(qmap_field "$lock" "packages" "$name" "resolved")" \
43
- "$(qmap_field "$lock" "packages" "$name" "sha")"
44
- done < <(qmap_keys "$lock" "packages")
80
+ _qmap_read rows "$1" packages
45
81
  }
46
82
 
47
83
  # lock_write_from_tsv <lock> <tsv-file> — rebuild the lock. Entries are
@@ -58,7 +94,15 @@ lock_write_from_tsv() {
58
94
  # them (registry-supplied url/path included) must be escaped, or it
59
95
  # would break or alias the neighbouring field.
60
96
  awk -F"$LOCK_SEP" '
61
- function esc(s) { gsub(/\\/, "\\\\", s); gsub(/"/, "\\\"", s); return s }
97
+ function esc(s, i, c, out) {
98
+ out = ""
99
+ for (i = 1; i <= length(s); i++) {
100
+ c = substr(s, i, 1)
101
+ if (c == "\\" || c == "\"") out = out "\\"
102
+ out = out c
103
+ }
104
+ return out
105
+ }
62
106
  NF >= 6 {
63
107
  print " \"" esc($1) "\":"
64
108
  if ($2 != "") print " requested: \"" esc($2) "\""
@@ -87,7 +87,8 @@ target_set_enabled() {
87
87
  case "$enabled" in true|false) ;; *) die "internal: invalid target state '$enabled'" ;; esac
88
88
  target_exists "$file" "$target" && target_missing=0
89
89
  output="$(_yq_esc "$output")"
90
- _qmap_stage "$file" -v target="$target" -v enabled="$enabled" -v output="$output" -v target_missing="$target_missing" '
90
+ QMAP_EDIT_VALUE="$output" _qmap_stage "$file" -v target="$target" -v enabled="$enabled" -v target_missing="$target_missing" '
91
+ BEGIN { output = ENVIRON["QMAP_EDIT_VALUE"] }
91
92
  function entry() { return " " target ": { enabled: " enabled ", output: \"" output "\" }" }
92
93
  function insert_missing_target() {
93
94
  if (in_targets && target_missing && !target_seen) { print entry(); target_seen = 1 }
@@ -154,71 +155,33 @@ target_set_enabled() {
154
155
  '
155
156
  }
156
157
 
157
- # qmap_keys <file> <block> — quoted keys, one per line.
158
- qmap_keys() {
159
- [ -f "$1" ] || return 0
160
- awk -v block="$2" '
161
- { sub(/\r$/, "") }
162
- $0 ~ "^" block ":[ \t]*$" { inb = 1; next }
163
- inb && /^[^ #]/ { inb = 0 }
164
- inb {
165
- if ($0 ~ /^ "/) {
166
- s = substr($0, 4)
167
- q = index(s, "\"")
168
- if (q > 0) print substr(s, 1, q - 1)
169
- }
170
- }
171
- ' "$1"
158
+ # _qmap_read <mode> <file> <block> [key] [field] [expected-lock-version]
159
+ # All scalar reads and strict lock records share one tokenizer. Environment
160
+ # transport preserves literal backslashes that awk -v would interpret again.
161
+ _qmap_read() {
162
+ local mode="$1" file="$2"
163
+ if [ ! -f "$file" ] || [ ! -r "$file" ]; then
164
+ case "$mode" in
165
+ lock|validate) echo "cannot read $file" >&2; return 1 ;;
166
+ *) return 0 ;;
167
+ esac
168
+ fi
169
+ QMAP_MODE="$mode" QMAP_FILE="$file" QMAP_BLOCK="${3:-}" \
170
+ QMAP_KEY="${4:-}" QMAP_FIELD="${5:-}" QMAP_EXPECTED_VERSION="${6:-}" \
171
+ LC_ALL=C awk -f "${BASH_SOURCE[0]%/*}/qmap.awk" < "$file"
172
172
  }
173
173
 
174
- # qmap_field <file> <block> <key> <field> — 4-indent field value under a
175
- # quoted key; strips surrounding quotes and a trailing unquoted comment.
176
- qmap_field() {
177
- [ -f "$1" ] || return 0
178
- awk -v block="$2" -v key="$3" -v field="$4" '
179
- { sub(/\r$/, "") }
180
- $0 ~ "^" block ":[ \t]*$" { inb = 1; next }
181
- inb && /^[^ #]/ { inb = 0; ink = 0 }
182
- inb && /^ "/ {
183
- s = substr($0, 4); q = index(s, "\"")
184
- ink = (q > 0 && substr(s, 1, q - 1) == key)
185
- next
186
- }
187
- inb && ink && /^ [A-Za-z_]/ {
188
- line = $0
189
- sub(/^ /, "", line)
190
- c = index(line, ":")
191
- if (c == 0) next
192
- if (substr(line, 1, c - 1) != field) next
193
- v = substr(line, c + 1)
194
- sub(/^[ \t]+/, "", v)
195
- if (v ~ /^"/) { v = substr(v, 2); q2 = index(v, "\""); if (q2 > 0) v = substr(v, 1, q2 - 1) }
196
- else { sub(/[ \t]+#.*$/, "", v); sub(/[ \t]+$/, "", v) }
197
- print v
198
- exit
199
- }
200
- ' "$1"
201
- }
174
+ # qmap_validate_document <file> <block> — structural validation only.
175
+ qmap_validate_document() { _qmap_read validate "$1" "$2"; }
202
176
 
203
- # qmap_value <file> <block> <key> — flat form: ` "key": "value"`.
204
- qmap_value() {
205
- [ -f "$1" ] || return 0
206
- awk -v block="$2" -v key="$3" '
207
- { sub(/\r$/, "") }
208
- $0 ~ "^" block ":[ \t]*$" { inb = 1; next }
209
- inb && /^[^ #]/ { inb = 0 }
210
- inb && /^ "/ {
211
- s = substr($0, 4); q = index(s, "\"")
212
- if (q == 0 || substr(s, 1, q - 1) != key) next
213
- v = substr(s, q + 1)
214
- sub(/^:[ \t]*/, "", v)
215
- if (v ~ /^"/) { v = substr(v, 2); q2 = index(v, "\""); if (q2 > 0) v = substr(v, 1, q2 - 1) }
216
- else { sub(/[ \t]+#.*$/, "", v); sub(/[ \t]+$/, "", v) }
217
- if (v != "") print v
218
- exit
219
- }
220
- ' "$1"
221
- }
177
+ # qmap_keys <file> <block> — quoted keys in document order.
178
+ qmap_keys() { _qmap_read keys "$1" "$2"; }
179
+
180
+ # qmap_field <file> <block> <key> <field> — decoded nested scalar.
181
+ qmap_field() { _qmap_read field "$1" "$2" "$3" "$4"; }
182
+
183
+ # qmap_value <file> <block> <key> — decoded flat scalar.
184
+ qmap_value() { _qmap_read value "$1" "$2" "$3"; }
222
185
 
223
186
  # _qmap_stage <file> <awk-program> [awk args…] — run an editing pass, verify
224
187
  # it produced output, commit.
@@ -245,7 +208,8 @@ qmap_set() {
245
208
  # url/path values can carry a `"`; escape before it reaches the quoted
246
209
  # scalar the writer emits.
247
210
  value="$(_yq_esc "$value")"
248
- _qmap_stage "$file" -v block="$block" -v key="$key" -v field="$field" -v value="$value" '
211
+ QMAP_EDIT_VALUE="$value" _qmap_stage "$file" -v block="$block" -v key="$key" -v field="$field" '
212
+ BEGIN { value = ENVIRON["QMAP_EDIT_VALUE"] }
249
213
  function keyline() { return " \"" key "\":" }
250
214
  function fieldline() { return " " field ": \"" value "\"" }
251
215
  function flush_key() {
@@ -292,7 +256,8 @@ qmap_set_value() {
292
256
  local file="$1" block="$2" key="$3" value="$4"
293
257
  [ -f "$file" ] || die "no such file: $file"
294
258
  value="$(_yq_esc "$value")"
295
- _qmap_stage "$file" -v block="$block" -v key="$key" -v value="$value" '
259
+ QMAP_EDIT_VALUE="$value" _qmap_stage "$file" -v block="$block" -v key="$key" '
260
+ BEGIN { value = ENVIRON["QMAP_EDIT_VALUE"] }
296
261
  function entry() { return " \"" key "\": \"" value "\"" }
297
262
  { sub(/\r$/, "") }
298
263
  $0 ~ "^" block ":[ \t]*$" { blockseen = 1; inb = 1; print; next }
@@ -0,0 +1,191 @@
1
+ # Shared tokenizer for the CLI-owned quoted-key map and scalar document shape.
2
+ # Reader modes ignore unrelated YAML. Lock mode validates the whole document
3
+ # before publishing rows, so a bad later entry never yields a partial result.
4
+ BEGIN {
5
+ mode = ENVIRON["QMAP_MODE"]
6
+ block = ENVIRON["QMAP_BLOCK"]
7
+ wanted_key = ENVIRON["QMAP_KEY"]
8
+ wanted_field = ENVIRON["QMAP_FIELD"]
9
+ expected_version = ENVIRON["QMAP_EXPECTED_VERSION"]
10
+ source_file = ENVIRON["QMAP_FILE"]
11
+ strict = (mode == "lock" || mode == "validate")
12
+ sep = sprintf("%c", 31)
13
+ single_quote = sprintf("%c", 39)
14
+ }
15
+
16
+ function error_at(reason, line) {
17
+ if (problem == "") {
18
+ problem = reason
19
+ problem_line = line
20
+ }
21
+ }
22
+
23
+ # quoted() also parses keys. quote_end leaves the caller at the delimiter;
24
+ # decoded contains literal bytes, with the writer's two escapes decoded once.
25
+ function quoted(s, i, c, escaped) {
26
+ decoded = ""
27
+ quote_end = 0
28
+ for (i = 2; i <= length(s); i++) {
29
+ c = substr(s, i, 1)
30
+ if (c == "\"") {
31
+ quote_end = i
32
+ return 1
33
+ }
34
+ if (c == "\\") {
35
+ if (++i > length(s)) return 0
36
+ escaped = substr(s, i, 1)
37
+ if (escaped == "\\" || escaped == "\"") c = escaped
38
+ else {
39
+ if (strict) return 0
40
+ c = "\\" escaped
41
+ }
42
+ }
43
+ if (strict && c ~ /[[:cntrl:]]/) return 0
44
+ decoded = decoded c
45
+ }
46
+ return 0
47
+ }
48
+
49
+ function scalar(s, tail) {
50
+ decoded = ""
51
+ empty_scalar = 0
52
+ sub(/^[ \t]+/, "", s)
53
+ if (s == "" || s ~ /^#/) {
54
+ empty_scalar = 1
55
+ return 1
56
+ }
57
+ if (s ~ /^"/) {
58
+ if (!quoted(s)) return 0
59
+ tail = substr(s, quote_end + 1)
60
+ return !strict || tail ~ /^([ \t]*|[ \t]+#.*)$/
61
+ }
62
+ sub(/[ \t]+#.*$/, "", s)
63
+ sub(/[ \t]+$/, "", s)
64
+ if (strict) {
65
+ if (s ~ /[[:cntrl:]]/ || substr(s, 1, 1) == single_quote) return 0
66
+ if (s ~ /^[!&*\[\]{}>|%@`]/ || s ~ /^[?:-]([ \t]|$)/ || s ~ /"|:[ \t]/) return 0
67
+ if (s ~ /^(null|Null|NULL|~|true|True|TRUE|false|False|FALSE)$/) return 0
68
+ }
69
+ decoded = s
70
+ return 1
71
+ }
72
+
73
+ # Tokenization is shared by permissive reads and strict lock validation.
74
+ function token(line, text, colon, tail) {
75
+ kind = ""
76
+ key = ""
77
+ value = ""
78
+ empty = 0
79
+ if (line ~ /^[A-Za-z_]/) {
80
+ kind = "top"
81
+ text = line
82
+ } else if (line ~ /^ [A-Za-z_]/) {
83
+ kind = "field"
84
+ text = substr(line, 5)
85
+ } else if (line ~ /^ "/) {
86
+ kind = "package"
87
+ text = substr(line, 3)
88
+ if (!quoted(text)) return 0
89
+ key = decoded
90
+ tail = substr(text, quote_end + 1)
91
+ if (substr(tail, 1, 1) != ":") return 0
92
+ if (!scalar(substr(tail, 2))) return 0
93
+ value = decoded
94
+ empty = empty_scalar
95
+ return 1
96
+ } else return 0
97
+ colon = index(text, ":")
98
+ if (!colon) return 0
99
+ key = substr(text, 1, colon - 1)
100
+ if (strict && key !~ /^[A-Za-z_][A-Za-z0-9_]*$/) return 0
101
+ if (!scalar(substr(text, colon + 1))) return 0
102
+ value = decoded
103
+ empty = empty_scalar
104
+ return 1
105
+ }
106
+
107
+ {
108
+ sub(/\r$/, "")
109
+ if ($0 ~ /^[ \t]*(#.*)?$/) next
110
+ if (!token($0)) {
111
+ if (strict) {
112
+ context = ""
113
+ if (key ~ /^[A-Za-z_][A-Za-z0-9_]*$/) {
114
+ if (kind == "field" && package != "") context = "package " package ", field " key ": "
115
+ else if (kind == "top") context = "field " key ": "
116
+ }
117
+ error_at(context "malformed lock structure or unsupported scalar", NR)
118
+ }
119
+ if ($0 ~ /^[^ #\t]/) { in_block = 0; package = "" }
120
+ next
121
+ }
122
+ if (kind == "top") {
123
+ in_block = (key == block && empty)
124
+ package = ""
125
+ if (strict && top_seen[key]++) error_at("duplicate top-level field " key, NR)
126
+ if (!(key in top_value)) top_value[key] = value
127
+ if (mode == "top" && key == wanted_key && !found) { result = value; found = 1 }
128
+ if (strict) {
129
+ if (key == block) {
130
+ block_seen = 1
131
+ if (!empty) error_at(block " must be a block map", NR)
132
+ } else if (empty) error_at("expected scalar for " key, NR)
133
+ }
134
+ next
135
+ }
136
+ if (kind == "package" && in_block) {
137
+ package = key
138
+ row_count++
139
+ row_name[row_count] = key
140
+ row_line[row_count] = NR
141
+ if (strict) {
142
+ if (key == "") error_at("empty package key", NR)
143
+ if (package_seen[key]++) error_at("duplicate package " key, NR)
144
+ if (!empty) error_at("expected field map for " key, NR)
145
+ }
146
+ if (mode == "value" && key == wanted_key && !found) { result = value; found = 1 }
147
+ next
148
+ }
149
+ if (kind == "field" && in_block && package != "") {
150
+ identity = package SUBSEP key
151
+ if (strict && field_seen[identity]++) error_at("duplicate field " package "." key, NR)
152
+ if (!(identity in field_value)) field_value[identity] = value
153
+ if (strict && empty) error_at("expected scalar for " package "." key, NR)
154
+ if (mode == "field" && package == wanted_key && key == wanted_field && !found) {
155
+ result = value
156
+ found = 1
157
+ }
158
+ next
159
+ }
160
+ if (strict) error_at("malformed lock structure: expected top-level scalar or quoted package field", NR)
161
+ }
162
+
163
+ END {
164
+ # Read the format header before interpreting a future body's shape. Errors
165
+ # were retained during the single pass so header placement does not matter.
166
+ if (mode == "lock" && top_value["lockfile_version"] != expected_version) {
167
+ version = top_value["lockfile_version"]
168
+ print source_file " has unsupported or missing lockfile_version '" (version == "" ? "<missing>" : version) "' — expected " expected_version > "/dev/stderr"
169
+ exit 1
170
+ }
171
+ if (strict && problem != "") {
172
+ print source_file ":" problem_line ": " problem > "/dev/stderr"
173
+ exit 1
174
+ }
175
+ if (strict && !block_seen) {
176
+ print source_file ": missing " block " block" > "/dev/stderr"
177
+ exit 1
178
+ }
179
+ if (mode == "lock") print "V" sep top_value["lockfile_version"] sep top_value["engine_version"]
180
+ if (mode == "rows" || mode == "lock" || mode == "keys") {
181
+ for (i = 1; i <= row_count; i++) {
182
+ name = row_name[i]
183
+ if (mode == "keys") print name
184
+ else {
185
+ row = name sep field_value[name SUBSEP "requested"] sep field_value[name SUBSEP "url"] sep field_value[name SUBSEP "path"] sep field_value[name SUBSEP "resolved"] sep field_value[name SUBSEP "sha"]
186
+ if (mode == "lock") print "R" sep row sep row_line[i]
187
+ else print row
188
+ }
189
+ }
190
+ } else if (found) print result
191
+ }
@@ -122,6 +122,7 @@ fetch_package() {
122
122
  local url="$1" ref="$2" subpath="$3" dest="$4" locked_sha="${5:-}" bundle_sha=""
123
123
  assert_safe_source_url "$url"
124
124
  assert_safe_ref "$ref"
125
+ assert_safe_package_path "$subpath"
125
126
  # Bundle seed: the engine-content package at the CLI's own version copies
126
127
  # from the npm bundle — no network, which keeps init, sync restoration and
127
128
  # archived-project conversion offline. Keyed on the
@@ -186,9 +187,6 @@ fetch_package() {
186
187
  fi
187
188
  src="$tmp/clone"
188
189
  if [ -n "$subpath" ]; then
189
- case "$subpath" in
190
- *..*|/*|*[\"\']*) rm -rf "$tmp"; die "unsafe path '$subpath'" ;;
191
- esac
192
190
  src="$tmp/clone/$subpath"
193
191
  [ -d "$src" ] || { rm -rf "$tmp"; die "path '$subpath' not found in $url${ref:+@$ref}" ; }
194
192
  fi
package/engine/ENGINE_SHA CHANGED
@@ -1 +1 @@
1
- 3413f7d6f9ef181368b16b90e36d77360cb5f479
1
+ b1242a74527d0295e9017fcd6b13ebfe00892710
package/engine/VERSION CHANGED
@@ -1 +1 @@
1
- 0.11.8
1
+ 0.12.0
@@ -0,0 +1,197 @@
1
+ #!/bin/bash
2
+ # intelligence-sync: Google Antigravity adapter
3
+ # Transforms source prompts to Antigravity's native resource model.
4
+ #
5
+ # 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.
10
+ # - 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.
17
+ # Skills:
18
+ # - Copied to .agents/skills/ (Agent Skills open standard, shared with
19
+ # Codex, Pi and opencode). Antigravity reads that fixed workspace path,
20
+ # never the configured output, and selects skills by description.
21
+ # Agents:
22
+ # - Converted to .agents/agents/<name>.md. `name` and `description` are
23
+ # required; `model` accepts only inherit|flash|pro; `subagent: true` with
24
+ # `mainAgent: false` matches how every other adapter exposes Intelligence
25
+ # agents — as delegates, not as chat personas. A readonly agent gets an
26
+ # explicit `tools:` allowlist.
27
+ #
28
+ # Every per-file loop batches its work into one awk process (see the batched
29
+ # helpers in lib/common.sh): process spawns dominate sync time on Windows.
30
+
31
+ source "$(dirname "${BASH_SOURCE[0]}")/../lib/common.sh"
32
+
33
+ adapter_contract_antigravity() {
34
+ local output="${1%/}"
35
+ adapter_contract_version 1
36
+ adapter_contract_requires agents
37
+ adapter_contract_owned "$output/rules"
38
+ adapter_contract_owned "$output/agents"
39
+ adapter_contract_managed ".agents/skills"
40
+ adapter_contract_legacy "GEMINI.md"
41
+ adapter_contract_ignore "GEMINI.md"
42
+ adapter_contract_ignore "$output/rules/"
43
+ adapter_contract_ignore "$output/agents/"
44
+ adapter_contract_ignore ".agents/skills/"
45
+ }
46
+
47
+ sync_antigravity_rules() {
48
+ local repo_root="$1"
49
+ local config_file="$2"
50
+ local output_dir="$3"
51
+
52
+ local src f
53
+ local -a files=()
54
+ load_yaml_list "$config_file" "rules"
55
+ local list="$IS_YAML_LIST"
56
+ while IFS= read -r src; do
57
+ [ -z "$src" ] && continue
58
+ local dir="$repo_root/$src"
59
+ [ -d "$dir" ] || continue
60
+ for f in "$dir"/*.md; do
61
+ [ -f "$f" ] || continue
62
+ files+=("$f")
63
+ done
64
+ done <<< "$list"
65
+
66
+ # Skip always-on rules: AGENTS.md (canonical) carries them.
67
+ local path hp base
68
+ local -a scoped=()
69
+ if [ "${#files[@]}" -gt 0 ]; then
70
+ while IFS=$'\x1f' read -r path hp; do
71
+ [ -n "$path" ] || continue
72
+ [ "$hp" -eq 0 ] && continue
73
+ scoped+=("$path")
74
+ done < <(frontmatter_index "paths#" "${files[@]}")
75
+ fi
76
+ if [ "${#scoped[@]}" -eq 0 ]; then
77
+ echo " -> Rules: 0 scoped (AGENTS.md carries always-on)"
78
+ return 0
79
+ fi
80
+
81
+ # Path-scoped rule -> Glob activation (paths -> globs)
82
+ is_fin_awk_vars
83
+ awk "${IS_FIN_V[@]}" -v dst="$output_dir/rules" "$IS_AWK_LIB"'
84
+ FNR == 1 {
85
+ if (out != "") close(out)
86
+ out = dst "/" base_name(FILENAME)
87
+ }
88
+ {
89
+ sub(/\r$/, "")
90
+ sub(/^paths:/, "globs:")
91
+ }
92
+ FNR == 1 {
93
+ print fin_line($0) > out
94
+ print fin_line("trigger: glob") > out
95
+ next
96
+ }
97
+ { print fin_line($0) > out }
98
+ END { if (out != "") close(out) }
99
+ ' "${scoped[@]}"
100
+
101
+ for f in "${scoped[@]}"; do
102
+ base="${f##*/}"
103
+ echo " rule: $base (scoped)"
104
+ done
105
+ echo " -> Rules: ${#scoped[@]} scoped"
106
+ }
107
+
108
+ sync_antigravity_agents() {
109
+ local repo_root="$1"
110
+ local config_file="$2"
111
+ local agents_dir="$3"
112
+
113
+ local src f
114
+ local -a files=()
115
+ load_yaml_list "$config_file" "agents"
116
+ local list="$IS_YAML_LIST"
117
+ while IFS= read -r src; do
118
+ [ -z "$src" ] && continue
119
+ local dir="$repo_root/$src"
120
+ [ -d "$dir" ] || continue
121
+ for f in "$dir"/*.md; do
122
+ [ -f "$f" ] || continue
123
+ files+=("$f")
124
+ done
125
+ done <<< "$list"
126
+
127
+ local count=0
128
+ if [ "${#files[@]}" -gt 0 ]; then
129
+ load_model_tiers "$config_file" "antigravity"
130
+
131
+ local path tier access description name spec="" report="" LS=$'\x1e'
132
+ while IFS=$'\x1f' read -r path tier access description; do
133
+ [ -n "$path" ] || continue
134
+ name="${path##*/}"; name="${name%.md}"
135
+ resolve_model_var "$tier"
136
+
137
+ local header
138
+ yaml_dq_escape_var "$name"
139
+ header="${LS}---"
140
+ header+="${LS}name: \"$IS_YAML_ESCAPED\""
141
+ yaml_dq_escape_var "$description"
142
+ header+="${LS}description: \"$IS_YAML_ESCAPED\""
143
+ if [ -n "$IS_MODEL" ]; then
144
+ yaml_dq_escape_var "$IS_MODEL"
145
+ header+="${LS}model: \"$IS_YAML_ESCAPED\""
146
+ fi
147
+ header+="${LS}subagent: true"
148
+ 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.
153
+ # An unrestricted agent declares no list and inherits the main
154
+ # agent's tools, which is Antigravity's default.
155
+ if [ "$access" = "readonly" ]; then
156
+ header+="${LS}tools:"
157
+ header+="${LS} - view_file"
158
+ header+="${LS} - grep_search"
159
+ header+="${LS} - search_web"
160
+ header+="${LS} - read_url_content"
161
+ fi
162
+ header+="${LS}---"
163
+ header+="${LS}"
164
+ header+="${LS}<!-- Generated by intelligence-sync. Do not edit manually. -->"
165
+ header+="${LS}"
166
+
167
+ spec+="$path"$'\x1f'"$agents_dir/$name.md"$'\x1f'"strip"$'\x1f'"1"$'\x1f'"none"$'\x1f'"$header"$'\x1f\n'
168
+ report+=" agent: $name.md"$'\n'
169
+ count=$((count + 1))
170
+ done < <(frontmatter_index "tier,access,description" "${files[@]}")
171
+
172
+ emit_wrapped_bodies "$spec"
173
+ printf '%s' "$report"
174
+ fi
175
+
176
+ echo " -> Agents: $count"
177
+ }
178
+
179
+ sync_to_antigravity() {
180
+ local repo_root="$1"
181
+ local config_file="$2"
182
+ local output_dir="$3"
183
+
184
+ echo "=== Antigravity ==="
185
+
186
+ rm -rf "$output_dir/rules" "$output_dir/agents"
187
+ mkdir -p "$output_dir/rules" "$output_dir/agents"
188
+
189
+ sync_antigravity_rules "$repo_root" "$config_file" "$output_dir"
190
+
191
+ # Skills -> .agents/skills/ (open standard; Antigravity reads that fixed
192
+ # workspace path). `sync_open_skill_dirs` owns clean + populate of this
193
+ # shared dir, and replays it when Codex, Pi or opencode already filled it.
194
+ sync_open_skill_dirs "$repo_root" "$config_file" "$repo_root/.agents/skills"
195
+
196
+ sync_antigravity_agents "$repo_root" "$config_file" "$output_dir/agents"
197
+ }
@@ -830,6 +830,11 @@ get_model_default() {
830
830
  codex:heavy) echo "gpt-5.6-sol" ;;
831
831
  codex:standard) echo "gpt-5.6-terra" ;;
832
832
  codex:light) echo "gpt-5.6-luna" ;;
833
+ # Antigravity accepts only inherit|flash|pro, so standard and light
834
+ # share `flash` — there is no third tier to map onto.
835
+ antigravity:heavy) echo "pro" ;;
836
+ antigravity:standard) echo "flash" ;;
837
+ antigravity:light) echo "flash" ;;
833
838
  opencode:heavy) echo "anthropic/claude-opus-4-8" ;;
834
839
  opencode:standard) echo "anthropic/claude-sonnet-5" ;;
835
840
  opencode:light) echo "anthropic/claude-haiku-4-5-20251001" ;;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ainova-systems/intelligence",
3
- "version": "0.11.8",
3
+ "version": "0.12.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"
@@ -163,6 +163,7 @@ Intelligence routes always-on rules once through `AGENTS.md` for tools that cons
163
163
  | Built-in | Always-on rules | Scoped rules |
164
164
  |---|---|---|
165
165
  | `agents` | Inline in `AGENTS.md` | List with links |
166
+ | `antigravity` | Omit; reads `AGENTS.md` | `.agents/rules/*.md` with `trigger: glob` and `globs:` |
166
167
  | `claude` | Copy | Copy with `paths:` preserved |
167
168
  | `cursor` | Omit; reads `AGENTS.md` | `.mdc` with `globs:` |
168
169
  | `copilot` | Omit; reads `AGENTS.md` | `.instructions.md` with `applyTo:` |
@@ -186,7 +187,7 @@ copy_skill_bundle "$source_skill_dir" "$output_dir/skills/$skill_name"
186
187
 
187
188
  Do not use plain `cp` for skill bundles. `copy_skill_bundle` preserves non-Markdown assets, avoids materializing symlink targets, normalizes Markdown, expands layout tokens and quotes `description` and `argument-hint` where strict YAML readers require strings.
188
189
 
189
- Codex, Pi and OpenCode share `.agents/skills/`. Any adapter writing that open-standard directory must call `sync_open_skill_dirs`; it is the single lifecycle owner for immediate skill subdirectories.
190
+ Antigravity, Codex, Pi and OpenCode share `.agents/skills/`. Any adapter writing that open-standard directory must call `sync_open_skill_dirs`; it is the single lifecycle owner for immediate skill subdirectories.
190
191
 
191
192
  ### Agents
192
193
 
@@ -300,6 +301,7 @@ For a built-in adapter, add the same assertions to the repository's smoke or lif
300
301
  | Adapter | Primary output |
301
302
  |---|---|
302
303
  | `agents` | `AGENTS.md` |
304
+ | `antigravity` | `.agents/rules`, `.agents/agents`, `.agents/skills` |
303
305
  | `claude` | `.claude/rules`, `.claude/agents`, `.claude/skills` |
304
306
  | `cursor` | `.cursor/rules`, `.cursor/agents`, `.cursor/skills` |
305
307
  | `copilot` | `.github/instructions`, `.github/agents`, `.github/skills` |
@@ -156,7 +156,7 @@ Agent instructions in Markdown.
156
156
 
157
157
  An agent stays thin: **Expertise** → **Boundaries** → **Build & Verify**. Its role, limits and proof of completion belong here; reusable constraints belong in rules and reusable procedures belong in skills.
158
158
 
159
- Do not instruct an agent to read rules or restate their content. Claude loads its generated rules for its subagents, while Cursor, Copilot, Codex, Pi and OpenCode receive always-on rules through `AGENTS.md`. Duplicating a rule in an agent spends context twice and creates a copy that drifts.
159
+ Do not instruct an agent to read rules or restate their content. Claude loads its generated rules for its subagents, while Cursor, Copilot, Codex, Pi, OpenCode and Antigravity receive always-on rules through `AGENTS.md`. Duplicating a rule in an agent spends context twice and creates a copy that drifts.
160
160
 
161
161
  ### Tier mappings
162
162
 
@@ -196,12 +196,12 @@ Rule content in Markdown.
196
196
 
197
197
  ### Routing
198
198
 
199
- | Source rule | Claude | Cursor | Copilot | Codex / Pi / OpenCode / `AGENTS.md` |
200
- |---|---|---|---|---|
201
- | Scoped | copied with `paths:` | `.mdc` with `globs:` | `.instructions.md` with `applyTo:` | listed in `AGENTS.md`; Pi also gets on-demand files and an extension |
202
- | Always-on | copied | omitted | omitted | inlined once into `AGENTS.md` |
199
+ | Source rule | Claude | Cursor | Antigravity | Copilot | Codex / Pi / OpenCode / `AGENTS.md` |
200
+ |---|---|---|---|---|---|
201
+ | Scoped | copied with `paths:` | `.mdc` with `globs:` | `.md` with `trigger: glob` and `globs:` | `.instructions.md` with `applyTo:` | listed in `AGENTS.md`; Pi also gets on-demand files and an extension |
202
+ | Always-on | copied | omitted | omitted | omitted | inlined once into `AGENTS.md` |
203
203
 
204
- Cursor, Copilot, Codex, Pi and OpenCode consume `AGENTS.md`, so always-on rules are not duplicated in their tool-specific channels. Claude does not consume `AGENTS.md`, so it receives the full rule set. OpenCode and Codex have no generated path-scoped rule channel; OpenCode users may configure `instructions:` globs themselves.
204
+ Cursor, Copilot, Codex, Pi, OpenCode and Antigravity consume `AGENTS.md`, so always-on rules are not duplicated in their tool-specific channels. Claude does not consume `AGENTS.md`, so it receives the full rule set. OpenCode and Codex have no generated path-scoped rule channel; OpenCode users may configure `instructions:` globs themselves.
205
205
 
206
206
  Keep always-on rules small. Put narrow framework or component guidance behind `paths:` so unrelated tasks do not pay its context cost.
207
207
 
@@ -288,6 +288,7 @@ Every line enters a finite context budget. Prefer subtraction, consolidation and
288
288
  | Target | Rules | Skills | Agents |
289
289
  |---|---|---|---|
290
290
  | `agents` | Always-on inlined; scoped listed in `AGENTS.md` | Listed | Listed |
291
+ | Antigravity | `AGENTS.md` plus scoped `.agents/rules/*.md` | `.agents/skills/` | `.agents/agents/*.md` |
291
292
  | Claude | `.claude/rules/` | `.claude/skills/` | `.claude/agents/` |
292
293
  | Cursor | scoped `.cursor/rules/*.mdc` | `.cursor/skills/` | `.cursor/agents/` |
293
294
  | Copilot | scoped `.github/instructions/*.instructions.md` | `.github/skills/` | `.github/agents/` |
@@ -306,6 +307,7 @@ By default, commit the manifest, lock, project-owned content, `AGENTS.md`, and s
306
307
  # Local root instructions migrate into project rules; local preferences stay ignored
307
308
  CLAUDE.md
308
309
  .cursorrules
310
+ GEMINI.md
309
311
 
310
312
  # Generated Claude and Cursor content; shared settings remain trackable
311
313
  .claude/*
@@ -315,8 +317,11 @@ CLAUDE.md
315
317
  !.cursor/
316
318
  !.cursor/settings.json
317
319
 
318
- # Generated open-standard and Codex content
320
+ # Generated open-standard, Codex and Antigravity content. `.agents/` is a
321
+ # shared workspace root, so only the generated subdirectories are ignored.
319
322
  .agents/skills/
323
+ .agents/rules/
324
+ .agents/agents/
320
325
  .codex/agents/
321
326
 
322
327
  # Generated Pi content
@@ -25,7 +25,7 @@ Three ways to shorten, in order of what they are worth:
25
25
 
26
26
  ## Source of truth
27
27
 
28
- Edit the project-owned sources listed in `<manifest>` — the `rules/`, `agents/` and `skills/` directories it names — and `<manifest>` itself. An installed package source is replaced by CLI lifecycle/package operations; change it in its own repository. Everything else is derived: `.claude/`, `.cursor/`, `.github/{instructions,agents,skills}/`, `.codex/`, `.agents/skills/`, `.pi/`, `.opencode/` and `AGENTS.md` are **generated output**, and a hand edit there survives exactly until the next sync.
28
+ Edit the project-owned sources listed in `<manifest>` — the `rules/`, `agents/` and `skills/` directories it names — and `<manifest>` itself. An installed package source is replaced by CLI lifecycle/package operations; change it in its own repository. Everything else is derived: `.claude/`, `.cursor/`, `.github/{instructions,agents,skills}/`, `.codex/`, `.agents/{rules,agents,skills}/`, `.pi/`, `.opencode/` and `AGENTS.md` are **generated output**, and a hand edit there survives exactly until the next sync.
29
29
 
30
30
  `<module>/` is the installed sync package's content. Package operations replace it, so a local edit there is lost. Fix it upstream instead.
31
31
 
@@ -68,7 +68,7 @@ The mistakes that actually happen, in order of frequency:
68
68
 
69
69
  An agent is **thin**: who it is, where it stops, how it verifies. Nothing else.
70
70
 
71
- - **Do not list rules for an agent to read.** They load on their own — Claude Code loads `.claude/rules/` into every custom subagent's startup context, and Cursor, Copilot, Codex, Pi and opencode receive always-on rules inlined in `AGENTS.md`. Naming a rule is fine; copying it is duplication that drifts.
71
+ - **Do not list rules for an agent to read.** They load on their own — Claude Code loads `.claude/rules/` into every custom subagent's startup context, and Cursor, Copilot, Codex, Pi, OpenCode and Antigravity receive always-on rules inlined in `AGENTS.md`. Naming a rule is fine; copying it is duplication that drifts.
72
72
  - **Point at a rule, do not restate it.** Wanting to copy a rule into an agent means the rule is in the wrong place — move it.
73
73
  - **Do carry** what is genuinely the agent's own: its boundaries ("if the app will not start, stop — do not hand-write the output"), its verification commands, its definition of done.
74
74
 
@@ -52,7 +52,7 @@ argument-hint: <domain> [description]
52
52
 
53
53
  6. **Write body** with sections: **Expertise** -> **Boundaries** -> **Build & Verify**
54
54
  - An agent is **thin**: who it is, where it stops, how it verifies. Everything else already reaches it.
55
- - **Do not tell the agent to read the rules.** Rules load on their own: Claude Code loads `.claude/rules/` into every custom subagent's startup context alongside `CLAUDE.md` (*Subagents → What loads at startup*), and Cursor / Copilot / Codex / Pi / opencode receive always-on rules inlined in `AGENTS.md`. A `Read <content-dir>/rules/<domain>.md before starting` line duplicates content the agent already has — double the tokens, and a second copy that drifts from the rule it copied.
55
+ - **Do not tell the agent to read the rules.** Rules load on their own: Claude Code loads `.claude/rules/` into every custom subagent's startup context alongside `CLAUDE.md` (*Subagents → What loads at startup*), and Cursor / Copilot / Codex / Pi / opencode / Antigravity receive always-on rules inlined in `AGENTS.md`. A `Read <content-dir>/rules/<domain>.md before starting` line duplicates content the agent already has — double the tokens, and a second copy that drifts from the rule it copied.
56
56
  - **Point at a rule, never restate it.** If you want to copy a rule into the agent, the rule is in the wrong place — move it, do not clone it.
57
57
  - **Do carry** what is genuinely the agent's own: its boundaries ("if the app is not running, stop — do not hand-write the output"), its verification commands, its definition of done.
58
58
  - All content must come from actual codebase analysis.