@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.
- package/cli/commands/init.sh +13 -1
- package/cli/commands/status.sh +13 -3
- package/cli/commands/update.sh +1 -0
- package/cli/internal/check.sh +22 -3
- package/cli/internal/convert-legacy.sh +5 -4
- package/cli/internal/package-list.sh +14 -4
- package/cli/internal/restore.sh +1 -14
- package/cli/lib/cli-common.sh +51 -4
- package/cli/lib/lockfile.sh +72 -28
- package/cli/lib/manifest.sh +30 -65
- package/cli/lib/qmap.awk +191 -0
- package/cli/lib/registry.sh +1 -3
- package/engine/ENGINE_SHA +1 -1
- package/engine/VERSION +1 -1
- package/engine/adapters/antigravity.sh +197 -0
- package/engine/lib/common.sh +5 -0
- package/package.json +1 -1
- package/packages/sync/references/adapters.md +3 -1
- package/packages/sync/references/conventions.md +12 -7
- package/packages/sync/rules/intelligence-authoring.md +2 -2
- package/packages/sync/skills/intelligence-add-agent/SKILL.md +1 -1
package/cli/commands/init.sh
CHANGED
|
@@ -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
|
-
|
|
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" ] \
|
package/cli/commands/status.sh
CHANGED
|
@@ -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
|
|
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="
|
|
25
|
-
if [
|
|
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)"
|
package/cli/commands/update.sh
CHANGED
package/cli/internal/check.sh
CHANGED
|
@@ -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="
|
|
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/*)
|
|
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
|
-
|
|
195
|
-
|
|
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="
|
|
17
|
-
|
|
18
|
-
|
|
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
|
-
|
|
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
|
package/cli/internal/restore.sh
CHANGED
|
@@ -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=""
|
package/cli/lib/cli-common.sh
CHANGED
|
@@ -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
|
|
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
|
-
|
|
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
|
package/cli/lib/lockfile.sh
CHANGED
|
@@ -10,38 +10,74 @@
|
|
|
10
10
|
LOCKFILE_VERSION=1
|
|
11
11
|
LOCK_SEP=$'\x1f'
|
|
12
12
|
|
|
13
|
-
#
|
|
14
|
-
#
|
|
15
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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) "\""
|
package/cli/lib/manifest.sh
CHANGED
|
@@ -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
|
|
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
|
-
#
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
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
|
-
#
|
|
175
|
-
|
|
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
|
-
#
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
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"
|
|
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"
|
|
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 }
|
package/cli/lib/qmap.awk
ADDED
|
@@ -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
|
+
}
|
package/cli/lib/registry.sh
CHANGED
|
@@ -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
|
-
|
|
1
|
+
b1242a74527d0295e9017fcd6b13ebfe00892710
|
package/engine/VERSION
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
0.
|
|
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
|
+
}
|
package/engine/lib/common.sh
CHANGED
|
@@ -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.
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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.
|