@ainova-systems/intelligence 0.17.0 → 0.17.2
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/adapter.sh +4 -0
- package/cli/commands/sync.sh +10 -1
- package/cli/commands/update.sh +3 -0
- package/cli/intelligence +10 -4
- package/cli/internal/align-project.sh +1 -0
- package/cli/internal/convert-legacy.sh +47 -1
- package/cli/internal/package-add.sh +1 -0
- package/cli/internal/package-remove.sh +1 -0
- package/cli/internal/package-update.sh +1 -0
- package/cli/internal/restore.sh +1 -0
- package/cli/internal/target-state.sh +1 -0
- package/cli/lib/cli-common.sh +56 -23
- package/cli/lib/lockfile.sh +2 -1
- package/cli/lib/qmap.awk +16 -1
- package/cli/lib/sync-cache.sh +56 -26
- package/cli/lib/sync-lock.sh +148 -0
- package/engine/ENGINE_SHA +1 -1
- package/engine/VERSION +1 -1
- package/engine/adapters/claude.sh +3 -5
- package/engine/adapters/cursor.sh +3 -5
- package/engine/lib/adapter-contract.sh +1 -5
- package/engine/lib/common.sh +228 -59
- package/engine/lib/contract.sh +57 -17
- package/engine/sync.sh +315 -29
- package/package.json +1 -1
- package/packages/sync/references/adapters.md +7 -0
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
#!/bin/bash
|
|
2
|
+
# One writer per project at a time. The engine clears and rewrites every
|
|
3
|
+
# adapter's output, the store restore and package operations replace package
|
|
4
|
+
# directories, and target and alignment edits rewrite the manifest. Two of these
|
|
5
|
+
# overlapping interleave their writes and both report success over a corrupted
|
|
6
|
+
# tree, so every script that writes shared project state holds this lock while it
|
|
7
|
+
# does: `project_lock_hold` at its start.
|
|
8
|
+
#
|
|
9
|
+
# `mkdir` is the lock: it creates or fails atomically on every filesystem the
|
|
10
|
+
# CLI supports. The owner file names the holder — pid, host, start time, process
|
|
11
|
+
# group and a token — so a refusal says who it is, a child of the holder can
|
|
12
|
+
# join instead of refusing, and a holder that died can be told from one whose
|
|
13
|
+
# writers still run. Taking over a dead holder's lock happens under a second
|
|
14
|
+
# lock, so two runs that judged the same lock stale cannot both take it.
|
|
15
|
+
|
|
16
|
+
SYNC_LOCK_HELD=""
|
|
17
|
+
SYNC_LOCK_TOKEN=""
|
|
18
|
+
|
|
19
|
+
sync_lock_now() {
|
|
20
|
+
if [ -n "${EPOCHSECONDS:-}" ]; then
|
|
21
|
+
IS_NOW="$EPOCHSECONDS"
|
|
22
|
+
else
|
|
23
|
+
IS_NOW="$(date +%s)"
|
|
24
|
+
fi
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
# sync_lock_pgid — set IS_PGID to this process's group, "" when unknown. The
|
|
28
|
+
# engine and its jobs inherit the group, so it outlives a killed wrapper for as
|
|
29
|
+
# long as any writer it started still runs.
|
|
30
|
+
sync_lock_pgid() {
|
|
31
|
+
local stat
|
|
32
|
+
local -a fields
|
|
33
|
+
IS_PGID=""
|
|
34
|
+
if [ -r "/proc/$$/stat" ] && IFS= read -r stat < "/proc/$$/stat"; then
|
|
35
|
+
# pid (comm) state ppid pgrp …: comm may hold spaces, so cut after it.
|
|
36
|
+
read -r -a fields <<< "${stat##*) }"
|
|
37
|
+
IS_PGID="${fields[2]:-}"
|
|
38
|
+
else
|
|
39
|
+
IS_PGID="$(ps -o pgid= -p "$$" 2>/dev/null || true)"
|
|
40
|
+
IS_PGID="${IS_PGID//[[:space:]]/}"
|
|
41
|
+
fi
|
|
42
|
+
case "$IS_PGID" in ''|*[!0-9]*|0|1) IS_PGID="" ;; esac
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
sync_lock_write_owner() {
|
|
46
|
+
sync_lock_now
|
|
47
|
+
sync_lock_pgid
|
|
48
|
+
SYNC_LOCK_TOKEN="$$.$IS_NOW.$RANDOM$RANDOM"
|
|
49
|
+
printf '%s %s %s %s %s\n' "$$" "${HOSTNAME:-unknown}" "$IS_NOW" "${IS_PGID:--}" "$SYNC_LOCK_TOKEN" > "$1/owner"
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
# sync_lock_read_owner <lock> — set IS_LOCK_PID / _HOST / _SINCE / _PGID /
|
|
53
|
+
# _TOKEN; returns 1 when the folder has no readable owner yet.
|
|
54
|
+
sync_lock_read_owner() {
|
|
55
|
+
IS_LOCK_PID="" IS_LOCK_HOST="" IS_LOCK_SINCE="" IS_LOCK_PGID="" IS_LOCK_TOKEN=""
|
|
56
|
+
[ -f "$1/owner" ] || return 1
|
|
57
|
+
read -r IS_LOCK_PID IS_LOCK_HOST IS_LOCK_SINCE IS_LOCK_PGID IS_LOCK_TOKEN < "$1/owner" \
|
|
58
|
+
|| [ -n "$IS_LOCK_PID" ] || return 1
|
|
59
|
+
case "$IS_LOCK_PID" in ''|*[!0-9]*) return 1 ;; esac
|
|
60
|
+
case "$IS_LOCK_PGID" in ''|*[!0-9]*|0|1) IS_LOCK_PGID="" ;; esac
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
# sync_lock_holder_gone — true only when no process of the holder remains: its
|
|
64
|
+
# whole process group when the owner recorded one, else its pid. Only "No such
|
|
65
|
+
# process" proves that; a permission refusal is another user's live process,
|
|
66
|
+
# and any other answer keeps the lock.
|
|
67
|
+
sync_lock_holder_gone() {
|
|
68
|
+
local target="$IS_LOCK_PID" answer
|
|
69
|
+
[ -z "$IS_LOCK_PGID" ] || target="-$IS_LOCK_PGID"
|
|
70
|
+
answer="$(LC_ALL=C kill -0 -- "$target" 2>&1)" && return 1
|
|
71
|
+
case "$answer" in *"No such process"*) return 0 ;; esac
|
|
72
|
+
return 1
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
# sync_lock_is_stale <lock> — its holder can no longer write: gone from this
|
|
76
|
+
# machine, or a folder that never got an owner and is past a minute old. A
|
|
77
|
+
# holder on another machine (a shared filesystem) is never judged from here.
|
|
78
|
+
sync_lock_is_stale() {
|
|
79
|
+
if sync_lock_read_owner "$1"; then
|
|
80
|
+
[ "$IS_LOCK_HOST" = "${HOSTNAME:-unknown}" ] || return 1
|
|
81
|
+
sync_lock_holder_gone
|
|
82
|
+
return
|
|
83
|
+
fi
|
|
84
|
+
[ -d "$1" ] && [ -n "$(find "$1" -maxdepth 0 -mmin +1 2>/dev/null)" ]
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
# sync_lock_acquire <root> — take <root>/.intelligence/sync.lock or refuse.
|
|
88
|
+
sync_lock_acquire() {
|
|
89
|
+
local root="$1" lock="$1/.intelligence/sync.lock" took=0 age=""
|
|
90
|
+
mkdir -p "$root/.intelligence" || die "cannot create $root/.intelligence"
|
|
91
|
+
if mkdir "$lock" 2>/dev/null; then
|
|
92
|
+
took=1
|
|
93
|
+
elif mkdir "$lock.takeover" 2>/dev/null; then
|
|
94
|
+
# Judge again under the takeover lock: another run may have taken
|
|
95
|
+
# it over since this one looked.
|
|
96
|
+
if sync_lock_is_stale "$lock"; then
|
|
97
|
+
rm -rf "$lock"
|
|
98
|
+
mkdir "$lock" 2>/dev/null && took=1
|
|
99
|
+
fi
|
|
100
|
+
rmdir "$lock.takeover"
|
|
101
|
+
elif [ -n "$(find "$lock.takeover" -maxdepth 0 -mmin +1 2>/dev/null)" ]; then
|
|
102
|
+
# A takeover runs for milliseconds. Removing an abandoned one is itself
|
|
103
|
+
# a check-then-delete race, so it is left to a person.
|
|
104
|
+
die "a takeover of $lock was abandoned; if no intelligence command is running in this project, remove $lock.takeover"
|
|
105
|
+
fi
|
|
106
|
+
if [ "$took" = 1 ]; then
|
|
107
|
+
sync_lock_write_owner "$lock" || { rm -rf "$lock"; die "cannot write $lock/owner"; }
|
|
108
|
+
SYNC_LOCK_HELD="$lock"
|
|
109
|
+
return 0
|
|
110
|
+
fi
|
|
111
|
+
if sync_lock_read_owner "$lock"; then
|
|
112
|
+
sync_lock_now
|
|
113
|
+
case "$IS_LOCK_SINCE" in ''|*[!0-9]*) ;; *) age=", for $((IS_NOW - IS_LOCK_SINCE))s" ;; esac
|
|
114
|
+
die "another intelligence sync is running in this project (pid $IS_LOCK_PID on $IS_LOCK_HOST$age). Wait for it to finish; if no such process exists, remove $lock"
|
|
115
|
+
fi
|
|
116
|
+
die "another intelligence sync is starting in this project. Wait for it to finish; if none is running, remove $lock"
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
# project_lock_hold <root> — hold the project lock for the rest of this script.
|
|
120
|
+
# A script started by the holder joins it instead (INTELLIGENCE_SYNC_LOCK names
|
|
121
|
+
# the lock and its token): the holder releases it, unless `exec` made this
|
|
122
|
+
# process the holder, which then releases it on exit.
|
|
123
|
+
project_lock_hold() {
|
|
124
|
+
local root="$1" lock="$1/.intelligence/sync.lock" held="${INTELLIGENCE_SYNC_LOCK:-}"
|
|
125
|
+
if [ -n "$held" ] && [ "${held%%|*}" = "$lock" ] && sync_lock_read_owner "$lock" \
|
|
126
|
+
&& [ -n "$IS_LOCK_TOKEN" ] && [ "$IS_LOCK_TOKEN" = "${held#*|}" ]; then
|
|
127
|
+
if [ "$IS_LOCK_PID" = "$$" ]; then
|
|
128
|
+
SYNC_LOCK_HELD="$lock"
|
|
129
|
+
SYNC_LOCK_TOKEN="$IS_LOCK_TOKEN"
|
|
130
|
+
trap 'sync_lock_release' EXIT
|
|
131
|
+
trap 'exit 130' INT TERM
|
|
132
|
+
fi
|
|
133
|
+
return 0
|
|
134
|
+
fi
|
|
135
|
+
sync_lock_acquire "$root"
|
|
136
|
+
export INTELLIGENCE_SYNC_LOCK="$lock|$SYNC_LOCK_TOKEN"
|
|
137
|
+
trap 'sync_lock_release' EXIT
|
|
138
|
+
trap 'exit 130' INT TERM
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
# sync_lock_release — drop the lock this process holds, and only that one.
|
|
142
|
+
sync_lock_release() {
|
|
143
|
+
[ -n "$SYNC_LOCK_HELD" ] || return 0
|
|
144
|
+
if sync_lock_read_owner "$SYNC_LOCK_HELD" && [ "$IS_LOCK_TOKEN" = "$SYNC_LOCK_TOKEN" ]; then
|
|
145
|
+
rm -rf "$SYNC_LOCK_HELD"
|
|
146
|
+
fi
|
|
147
|
+
SYNC_LOCK_HELD=""
|
|
148
|
+
}
|
package/engine/ENGINE_SHA
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
|
|
1
|
+
97484ac7ccc2e093642b833edfa8cae7bfe47c9e
|
package/engine/VERSION
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
0.17.
|
|
1
|
+
0.17.2
|
|
@@ -173,9 +173,7 @@ sync_to_claude() {
|
|
|
173
173
|
sync_claude_skills "$repo_root" "$config_file" "$output_dir"
|
|
174
174
|
sync_claude_agents "$repo_root" "$config_file" "$output_dir"
|
|
175
175
|
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
agents_count=$(find "$output_dir/agents" -name "*.md" 2>/dev/null | wc -l)
|
|
180
|
-
echo " -> Rules: $rules_count, Skills: $skills_count, Agents: $agents_count"
|
|
176
|
+
count_matching_files "$output_dir/rules" "*.md" \
|
|
177
|
+
"$output_dir/skills" "SKILL.md" "$output_dir/agents" "*.md"
|
|
178
|
+
echo " -> Rules: ${IS_FILE_COUNTS[0]}, Skills: ${IS_FILE_COUNTS[1]}, Agents: ${IS_FILE_COUNTS[2]}"
|
|
181
179
|
}
|
|
@@ -204,9 +204,7 @@ sync_to_cursor() {
|
|
|
204
204
|
sync_cursor_skills "$repo_root" "$config_file" "$output_dir"
|
|
205
205
|
sync_cursor_agents "$repo_root" "$config_file" "$output_dir"
|
|
206
206
|
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
agents_count=$(find "$output_dir/agents" -name "*.md" 2>/dev/null | wc -l)
|
|
211
|
-
echo " -> Rules: $rules_count (.mdc), Skills: $skills_count, Agents: $agents_count"
|
|
207
|
+
count_matching_files "$output_dir/rules" "*.mdc" \
|
|
208
|
+
"$output_dir/skills" "SKILL.md" "$output_dir/agents" "*.md"
|
|
209
|
+
echo " -> Rules: ${IS_FILE_COUNTS[0]} (.mdc), Skills: ${IS_FILE_COUNTS[1]}, Agents: ${IS_FILE_COUNTS[2]}"
|
|
212
210
|
}
|
|
@@ -15,10 +15,6 @@ adapter_contract_preserve() { printf 'preserve\t%s\n' "$1"; }
|
|
|
15
15
|
adapter_contract_ignore() { printf 'ignore\t%s\n' "$1"; }
|
|
16
16
|
adapter_contract_include() { printf 'include\t%s\n' "$1"; }
|
|
17
17
|
|
|
18
|
-
adapter_contract_function() {
|
|
19
|
-
printf 'adapter_contract_%s' "$1"
|
|
20
|
-
}
|
|
21
|
-
|
|
22
18
|
# --- cross-adapter ownership -------------------------------------------------
|
|
23
19
|
#
|
|
24
20
|
# `owned` is exclusive: one adapter writes and prunes that path. `managed` is
|
|
@@ -118,7 +114,7 @@ adapter_contract_records() (
|
|
|
118
114
|
local name="$1" file="$2" output="$3" fn line kind value saw_version=0
|
|
119
115
|
# shellcheck source=/dev/null
|
|
120
116
|
source "$file"
|
|
121
|
-
fn
|
|
117
|
+
printf -v fn 'adapter_contract_%s' "$name"
|
|
122
118
|
declare -F "$fn" >/dev/null 2>&1 || {
|
|
123
119
|
echo "ERROR: adapter '$name' has no $fn contract" >&2
|
|
124
120
|
return 1
|
package/engine/lib/common.sh
CHANGED
|
@@ -820,34 +820,38 @@ has_paths() {
|
|
|
820
820
|
# When you bump these, re-run sync in projects; any project whose config.yaml
|
|
821
821
|
# `models:` section diverges from these will print a drift warning so users
|
|
822
822
|
# know their override is now stale.
|
|
823
|
-
get_model_default
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
claude:
|
|
828
|
-
claude:
|
|
829
|
-
|
|
830
|
-
cursor:
|
|
831
|
-
cursor:
|
|
832
|
-
|
|
833
|
-
copilot:
|
|
834
|
-
copilot:
|
|
835
|
-
|
|
836
|
-
codex:
|
|
837
|
-
codex:
|
|
838
|
-
codex:light) echo "gpt-5.6-luna" ;;
|
|
823
|
+
# get_model_default_var sets IS_MODEL_DEFAULT; get_model_default prints it.
|
|
824
|
+
get_model_default_var() {
|
|
825
|
+
case "$1:$2" in
|
|
826
|
+
claude:heavy) IS_MODEL_DEFAULT="opus" ;;
|
|
827
|
+
claude:standard) IS_MODEL_DEFAULT="sonnet" ;;
|
|
828
|
+
claude:light) IS_MODEL_DEFAULT="haiku" ;;
|
|
829
|
+
cursor:heavy) IS_MODEL_DEFAULT="inherit" ;;
|
|
830
|
+
cursor:standard) IS_MODEL_DEFAULT="inherit" ;;
|
|
831
|
+
cursor:light) IS_MODEL_DEFAULT="fast" ;;
|
|
832
|
+
copilot:heavy) IS_MODEL_DEFAULT="gpt-5.6-sol" ;;
|
|
833
|
+
copilot:standard) IS_MODEL_DEFAULT="gpt-5.6-terra" ;;
|
|
834
|
+
copilot:light) IS_MODEL_DEFAULT="gpt-5.6-luna" ;;
|
|
835
|
+
codex:heavy) IS_MODEL_DEFAULT="gpt-5.6-sol" ;;
|
|
836
|
+
codex:standard) IS_MODEL_DEFAULT="gpt-5.6-terra" ;;
|
|
837
|
+
codex:light) IS_MODEL_DEFAULT="gpt-5.6-luna" ;;
|
|
839
838
|
# Antigravity accepts only inherit|flash|pro, so standard and light
|
|
840
839
|
# share `flash` — there is no third tier to map onto.
|
|
841
|
-
antigravity:heavy)
|
|
842
|
-
antigravity:standard)
|
|
843
|
-
antigravity:light)
|
|
844
|
-
opencode:heavy)
|
|
845
|
-
opencode:standard)
|
|
846
|
-
opencode:light)
|
|
847
|
-
*)
|
|
840
|
+
antigravity:heavy) IS_MODEL_DEFAULT="pro" ;;
|
|
841
|
+
antigravity:standard) IS_MODEL_DEFAULT="flash" ;;
|
|
842
|
+
antigravity:light) IS_MODEL_DEFAULT="flash" ;;
|
|
843
|
+
opencode:heavy) IS_MODEL_DEFAULT="anthropic/claude-opus-4-8" ;;
|
|
844
|
+
opencode:standard) IS_MODEL_DEFAULT="anthropic/claude-sonnet-5" ;;
|
|
845
|
+
opencode:light) IS_MODEL_DEFAULT="anthropic/claude-haiku-4-5-20251001" ;;
|
|
846
|
+
*) IS_MODEL_DEFAULT="" ;;
|
|
848
847
|
esac
|
|
849
848
|
}
|
|
850
849
|
|
|
850
|
+
get_model_default() {
|
|
851
|
+
get_model_default_var "$1" "$2"
|
|
852
|
+
echo "$IS_MODEL_DEFAULT"
|
|
853
|
+
}
|
|
854
|
+
|
|
851
855
|
# Read a nested key from config.yaml: section -> sub -> key.
|
|
852
856
|
# Resolves `models.<ide>.<tier>` overrides and `packs.<name>.<field>`.
|
|
853
857
|
#
|
|
@@ -859,12 +863,16 @@ get_model_default() {
|
|
|
859
863
|
# The sub-key is matched LITERALLY (`index(...) == 1`), never interpolated into
|
|
860
864
|
# a regex: a pack name may contain `.`, which as a pattern is any character, so
|
|
861
865
|
# `packs.a.b` would happily read a pack named `axb`.
|
|
862
|
-
|
|
863
|
-
|
|
864
|
-
|
|
865
|
-
|
|
866
|
-
|
|
867
|
-
|
|
866
|
+
#
|
|
867
|
+
# _nested_yaml_values <plain|tagged> <file> <section> <sub> <key>... — one pass
|
|
868
|
+
# for any number of keys, first occurrence of each. `plain` prints the value
|
|
869
|
+
# alone (one key); `tagged` prints `key<US>value`, US being \037.
|
|
870
|
+
_nested_yaml_values() {
|
|
871
|
+
local tagged=0 file="$2" section="$3" sub="$4"
|
|
872
|
+
[ "$1" = tagged ] && tagged=1
|
|
873
|
+
shift 4
|
|
874
|
+
awk -v section="$section" -v subname="$sub" -v keys="$*" -v tagged="$tagged" '
|
|
875
|
+
BEGIN { n = split(keys, want, " "); left = n }
|
|
868
876
|
{ sub(/\r$/, "") }
|
|
869
877
|
$0 ~ "^" section ":[[:space:]]*$" { in_section=1; in_sub=0; next }
|
|
870
878
|
in_section && /^[a-zA-Z]/ && $0 !~ "^" section ":" { in_section=0; in_sub=0 }
|
|
@@ -872,24 +880,34 @@ get_nested_yaml_value() {
|
|
|
872
880
|
if (substr($0, length(subname) + 4) ~ /^[[:space:]]*$/) { in_sub=1; next }
|
|
873
881
|
}
|
|
874
882
|
in_section && in_sub && /^ [A-Za-z0-9_]/ { in_sub=0 }
|
|
875
|
-
in_section && in_sub
|
|
876
|
-
|
|
877
|
-
|
|
878
|
-
|
|
879
|
-
|
|
880
|
-
val
|
|
881
|
-
|
|
882
|
-
|
|
883
|
-
|
|
884
|
-
|
|
885
|
-
|
|
883
|
+
in_section && in_sub {
|
|
884
|
+
for (k = 1; k <= n; k++) {
|
|
885
|
+
if ((k in done) || index($0, " " want[k] ":") != 1) continue
|
|
886
|
+
val = $0
|
|
887
|
+
sub(/^[[:space:]]*[^:]*:[[:space:]]*/, "", val)
|
|
888
|
+
if (val ~ /^"/ || val ~ /^\047/) {
|
|
889
|
+
q = substr(val, 1, 1)
|
|
890
|
+
val = substr(val, 2)
|
|
891
|
+
i = index(val, q)
|
|
892
|
+
if (i > 0) val = substr(val, 1, i - 1)
|
|
893
|
+
} else {
|
|
894
|
+
sub(/[[:space:]]+#.*$/, "", val)
|
|
895
|
+
sub(/[[:space:]]+$/, "", val)
|
|
896
|
+
}
|
|
897
|
+
if (tagged) printf "%s\037%s\n", want[k], val
|
|
898
|
+
else print val
|
|
899
|
+
done[k] = 1
|
|
900
|
+
if (--left == 0) exit
|
|
901
|
+
break
|
|
886
902
|
}
|
|
887
|
-
print val
|
|
888
|
-
exit
|
|
889
903
|
}
|
|
890
904
|
' "$file"
|
|
891
905
|
}
|
|
892
906
|
|
|
907
|
+
get_nested_yaml_value() {
|
|
908
|
+
_nested_yaml_values plain "$1" "$2" "$3" "$4"
|
|
909
|
+
}
|
|
910
|
+
|
|
893
911
|
# Resolve a model: config.yaml `models:` override wins, otherwise default.
|
|
894
912
|
# Usage: get_model "$CONFIG_FILE" "claude" "$tier"
|
|
895
913
|
get_model() {
|
|
@@ -911,14 +929,27 @@ get_model() {
|
|
|
911
929
|
# once per adapter run (IS_MODEL_HEAVY / IS_MODEL_STANDARD / IS_MODEL_LIGHT)
|
|
912
930
|
# so per-file loops map tier -> model without forking. resolve_model_var
|
|
913
931
|
# consumes them; a non-standard tier value still goes through get_model so a
|
|
914
|
-
# `models:` override for it keeps working.
|
|
932
|
+
# `models:` override for it keeps working. One manifest pass reads all three.
|
|
915
933
|
load_model_tiers() {
|
|
916
|
-
local config_file="$1" ide="$2"
|
|
934
|
+
local config_file="$1" ide="$2" overrides="" tier val heavy="" standard="" light=""
|
|
917
935
|
IS_MODEL_CFG="$config_file"
|
|
918
936
|
IS_MODEL_IDE="$ide"
|
|
919
|
-
|
|
920
|
-
|
|
921
|
-
|
|
937
|
+
if [ -n "$config_file" ] && [ -f "$config_file" ]; then
|
|
938
|
+
overrides="$(_nested_yaml_values tagged "$config_file" "models" "$ide" heavy standard light)"
|
|
939
|
+
fi
|
|
940
|
+
while IFS=$'\037' read -r tier val; do
|
|
941
|
+
case "$tier" in
|
|
942
|
+
heavy) heavy="$val" ;;
|
|
943
|
+
standard) standard="$val" ;;
|
|
944
|
+
light) light="$val" ;;
|
|
945
|
+
esac
|
|
946
|
+
done <<< "$overrides"
|
|
947
|
+
get_model_default_var "$ide" heavy
|
|
948
|
+
IS_MODEL_HEAVY="${heavy:-$IS_MODEL_DEFAULT}"
|
|
949
|
+
get_model_default_var "$ide" standard
|
|
950
|
+
IS_MODEL_STANDARD="${standard:-$IS_MODEL_DEFAULT}"
|
|
951
|
+
get_model_default_var "$ide" light
|
|
952
|
+
IS_MODEL_LIGHT="${light:-$IS_MODEL_DEFAULT}"
|
|
922
953
|
}
|
|
923
954
|
|
|
924
955
|
# resolve_model_var <tier> — set IS_MODEL from the tiers load_model_tiers
|
|
@@ -968,7 +999,8 @@ report_model_drift() {
|
|
|
968
999
|
while IFS=$'\t' read -r ide tier from_config; do
|
|
969
1000
|
[ -z "$from_config" ] && continue
|
|
970
1001
|
local default
|
|
971
|
-
|
|
1002
|
+
get_model_default_var "$ide" "$tier"
|
|
1003
|
+
default="$IS_MODEL_DEFAULT"
|
|
972
1004
|
[ "$from_config" = "$default" ] && continue
|
|
973
1005
|
if [ $printed_header -eq 0 ]; then
|
|
974
1006
|
echo ""
|
|
@@ -1361,13 +1393,32 @@ warn_unsynced() {
|
|
|
1361
1393
|
# its writer's exit status entirely. Assembling in the caller's shell leaves no
|
|
1362
1394
|
# writer whose failure can be lost, and the remaining pipeline is checked. An
|
|
1363
1395
|
# enumeration that cannot answer must stop the run, never shorten the answer.
|
|
1396
|
+
#
|
|
1397
|
+
# With IS_SOURCE_FILES_MEMO=1 a section is enumerated once and later calls
|
|
1398
|
+
# replay that answer. Only the engine sets it: a sync never writes into a
|
|
1399
|
+
# source (validate_output_path refuses), so each caller would otherwise re-run
|
|
1400
|
+
# the same find and sort for every source directory.
|
|
1364
1401
|
read_source_artifact_files() {
|
|
1365
1402
|
local repo_root="$1"
|
|
1366
1403
|
local config_file="$2"
|
|
1367
1404
|
local section="$3"
|
|
1368
|
-
local src f dir listing
|
|
1405
|
+
local src f dir listing memo_key="" key_var="" list_var="" joined=""
|
|
1369
1406
|
|
|
1370
1407
|
IS_SOURCE_FILES=()
|
|
1408
|
+
case "${IS_SOURCE_FILES_MEMO:-0}:$section" in
|
|
1409
|
+
1:rules|1:agents|1:skills)
|
|
1410
|
+
key_var="IS_SF_${section}_KEY"
|
|
1411
|
+
list_var="IS_SF_${section}_LIST"
|
|
1412
|
+
memo_key="$repo_root"$'\n'"$config_file"
|
|
1413
|
+
if [ "${!key_var:-}" = "$memo_key" ]; then
|
|
1414
|
+
listing="${!list_var:-}"
|
|
1415
|
+
while IFS= read -r f; do
|
|
1416
|
+
[ -n "$f" ] && IS_SOURCE_FILES+=("$f")
|
|
1417
|
+
done <<< "$listing"
|
|
1418
|
+
return 0
|
|
1419
|
+
fi
|
|
1420
|
+
;;
|
|
1421
|
+
esac
|
|
1371
1422
|
load_yaml_list "$config_file" "$section"
|
|
1372
1423
|
while IFS= read -r src; do
|
|
1373
1424
|
[ -n "$src" ] || continue
|
|
@@ -1400,18 +1451,28 @@ read_source_artifact_files() {
|
|
|
1400
1451
|
f="$f/SKILL.md"
|
|
1401
1452
|
fi
|
|
1402
1453
|
IS_SOURCE_FILES+=("$f")
|
|
1454
|
+
joined="$joined$f"$'\n'
|
|
1403
1455
|
done <<< "$listing"
|
|
1404
1456
|
done <<< "$IS_YAML_LIST"
|
|
1457
|
+
if [ -n "$memo_key" ]; then
|
|
1458
|
+
printf -v "$list_var" '%s' "$joined"
|
|
1459
|
+
printf -v "$key_var" '%s' "$memo_key"
|
|
1460
|
+
fi
|
|
1405
1461
|
}
|
|
1406
1462
|
|
|
1407
1463
|
# Apply the agents adapter's lexical output rule without consulting filesystem
|
|
1408
1464
|
# state. An omitted output uses the generic adapter default directory.
|
|
1409
|
-
|
|
1465
|
+
agents_output_path_var() {
|
|
1410
1466
|
local output="${1:-.agents}"
|
|
1411
1467
|
if [[ "$output" == */ ]] || [[ "$output" != *.md ]]; then
|
|
1412
1468
|
output="${output%/}/AGENTS.md"
|
|
1413
1469
|
fi
|
|
1414
|
-
|
|
1470
|
+
IS_AGENTS_OUTPUT_PATH="$output"
|
|
1471
|
+
}
|
|
1472
|
+
|
|
1473
|
+
agents_output_path() {
|
|
1474
|
+
agents_output_path_var "$@"
|
|
1475
|
+
printf '%s\n' "$IS_AGENTS_OUTPUT_PATH"
|
|
1415
1476
|
}
|
|
1416
1477
|
|
|
1417
1478
|
# Report source prompt pressure independently of adapter formats. "Always-on"
|
|
@@ -1420,6 +1481,43 @@ agents_output_path() {
|
|
|
1420
1481
|
# them only when a skill explicitly reads them. When the shared agents target
|
|
1421
1482
|
# exists, also report its rendered size; policy for any tool-specific limit
|
|
1422
1483
|
# stays in that tool's adapter.
|
|
1484
|
+
# count_matching_files <dir> <name-glob> [<dir> <name-glob>]... — fill
|
|
1485
|
+
# IS_FILE_COUNTS with one count per pair, what `find <dir> -name <glob> | wc -l`
|
|
1486
|
+
# reports for each, from a single find. The directories must be siblings, not
|
|
1487
|
+
# nested in one another; a missing one fails the call, as find would.
|
|
1488
|
+
count_matching_files() {
|
|
1489
|
+
local -a dirs=() globs=() expr=()
|
|
1490
|
+
local listing path base i n
|
|
1491
|
+
while [ "$#" -ge 2 ]; do
|
|
1492
|
+
dirs+=("$1")
|
|
1493
|
+
globs+=("$2")
|
|
1494
|
+
shift 2
|
|
1495
|
+
done
|
|
1496
|
+
n=${#dirs[@]}
|
|
1497
|
+
IS_FILE_COUNTS=()
|
|
1498
|
+
for ((i = 0; i < n; i++)); do
|
|
1499
|
+
IS_FILE_COUNTS[i]=0
|
|
1500
|
+
[ "$i" -eq 0 ] || expr+=(-o)
|
|
1501
|
+
expr+=(-name "${globs[i]}")
|
|
1502
|
+
done
|
|
1503
|
+
[ "$n" -gt 0 ] || return 0
|
|
1504
|
+
listing="$(find "${dirs[@]}" \( "${expr[@]}" \) -print 2>/dev/null)" || return 1
|
|
1505
|
+
[ -n "$listing" ] || return 0
|
|
1506
|
+
while IFS= read -r path; do
|
|
1507
|
+
base="${path##*/}"
|
|
1508
|
+
for ((i = 0; i < n; i++)); do
|
|
1509
|
+
case "$path" in
|
|
1510
|
+
"${dirs[i]}"|"${dirs[i]}"/*) ;;
|
|
1511
|
+
*) continue ;;
|
|
1512
|
+
esac
|
|
1513
|
+
# shellcheck disable=SC2254 # the glob is the pattern being matched
|
|
1514
|
+
case "$base" in
|
|
1515
|
+
${globs[i]}) IS_FILE_COUNTS[i]=$((IS_FILE_COUNTS[i] + 1)) ;;
|
|
1516
|
+
esac
|
|
1517
|
+
done
|
|
1518
|
+
done <<< "$listing"
|
|
1519
|
+
}
|
|
1520
|
+
|
|
1423
1521
|
context_files_bytes() {
|
|
1424
1522
|
if [ "$#" -eq 0 ]; then
|
|
1425
1523
|
echo 0
|
|
@@ -1453,22 +1551,43 @@ report_context_source_sizes() {
|
|
|
1453
1551
|
read_source_artifact_files "$repo_root" "$config_file" "skills"
|
|
1454
1552
|
[ "${#IS_SOURCE_FILES[@]}" -eq 0 ] || skill_files=("${IS_SOURCE_FILES[@]}")
|
|
1455
1553
|
|
|
1456
|
-
local always_bytes custom_bytes agents_output agents_bytes=0 agents_status="disabled"
|
|
1457
|
-
|
|
1458
|
-
|
|
1459
|
-
|
|
1460
|
-
|
|
1461
|
-
|
|
1554
|
+
local always_bytes=0 custom_bytes=0 agents_output agents_bytes=0 agents_status="disabled"
|
|
1555
|
+
local always_n custom_n listing size rest idx=0
|
|
1556
|
+
local -a measured=()
|
|
1557
|
+
always_n=${#always_on_rules[@]}
|
|
1558
|
+
custom_n=$(( ${#scoped_rules[@]} + ${#agent_files[@]} + ${#skill_files[@]} ))
|
|
1559
|
+
measured=(
|
|
1560
|
+
"${always_on_rules[@]+"${always_on_rules[@]}"}"
|
|
1561
|
+
"${scoped_rules[@]+"${scoped_rules[@]}"}"
|
|
1562
|
+
"${agent_files[@]+"${agent_files[@]}"}"
|
|
1563
|
+
"${skill_files[@]+"${skill_files[@]}"}")
|
|
1462
1564
|
target_enabled_var "$config_file" "agents"
|
|
1463
1565
|
if [ "$IS_TGT_ENABLED" = "1" ]; then
|
|
1464
1566
|
agents_status="not-generated"
|
|
1465
1567
|
target_output_var "$config_file" "agents"
|
|
1466
|
-
|
|
1568
|
+
agents_output_path_var "${IS_TGT_OUTPUT:-.agents}"
|
|
1569
|
+
agents_output="$IS_AGENTS_OUTPUT_PATH"
|
|
1467
1570
|
if [ -f "$repo_root/$agents_output" ]; then
|
|
1468
|
-
|
|
1571
|
+
measured+=("$repo_root/$agents_output")
|
|
1469
1572
|
agents_status="generated"
|
|
1470
1573
|
fi
|
|
1471
1574
|
fi
|
|
1575
|
+
# One wc for every group: its per-file lines come back in argument order,
|
|
1576
|
+
# and each group's sum is what a separate `wc` total would have reported.
|
|
1577
|
+
if [ "${#measured[@]}" -gt 0 ]; then
|
|
1578
|
+
listing="$(LC_ALL=C wc -c "${measured[@]}")"
|
|
1579
|
+
while read -r size rest; do
|
|
1580
|
+
[ "$idx" -lt "${#measured[@]}" ] || break
|
|
1581
|
+
if [ "$idx" -lt "$always_n" ]; then
|
|
1582
|
+
always_bytes=$((always_bytes + size))
|
|
1583
|
+
elif [ "$idx" -lt $((always_n + custom_n)) ]; then
|
|
1584
|
+
custom_bytes=$((custom_bytes + size))
|
|
1585
|
+
else
|
|
1586
|
+
agents_bytes=$((agents_bytes + size))
|
|
1587
|
+
fi
|
|
1588
|
+
idx=$((idx + 1))
|
|
1589
|
+
done <<< "$listing"
|
|
1590
|
+
fi
|
|
1472
1591
|
|
|
1473
1592
|
printf 'CONTEXT: always-on=%s bytes (%s rules); custom=%s bytes (%s scoped rules, %s agents, %s skills); agents-md=%s bytes; agents-md-status=%s\n' \
|
|
1474
1593
|
"$always_bytes" "${#always_on_rules[@]}" "$custom_bytes" \
|
|
@@ -1554,6 +1673,56 @@ load_yaml_list() {
|
|
|
1554
1673
|
esac
|
|
1555
1674
|
}
|
|
1556
1675
|
|
|
1676
|
+
# load_yaml_lists <file> <section>... — warm the load_yaml_list cache for several
|
|
1677
|
+
# sections in one manifest pass. Each section runs read_yaml_list's own state
|
|
1678
|
+
# machine, so every cached value is what load_yaml_list would have stored.
|
|
1679
|
+
load_yaml_lists() {
|
|
1680
|
+
local file="$1" out line s v j
|
|
1681
|
+
shift
|
|
1682
|
+
for s in "$@"; do
|
|
1683
|
+
case "$s" in
|
|
1684
|
+
rules|agents|skills|ignore|submodules) ;;
|
|
1685
|
+
*) return 1 ;;
|
|
1686
|
+
esac
|
|
1687
|
+
done
|
|
1688
|
+
out="$(awk -v sections="$*" '
|
|
1689
|
+
BEGIN { n = split(sections, want, " ") }
|
|
1690
|
+
{ sub(/\r$/, "") }
|
|
1691
|
+
{
|
|
1692
|
+
for (k = 1; k <= n; k++) {
|
|
1693
|
+
s = want[k]
|
|
1694
|
+
if ($0 ~ /^[a-z]/) { cur[k] = ""; dep[k] = 0 }
|
|
1695
|
+
if ($0 ~ /^ [a-z]/) { cur[k] = ""; dep[k] = 0 }
|
|
1696
|
+
if ($0 ~ "^" s ":") { cur[k] = s; dep[k] = 0; continue }
|
|
1697
|
+
if ($0 ~ "^ " s ":") { cur[k] = s; dep[k] = 2; continue }
|
|
1698
|
+
if (cur[k] == s && dep[k] == 0 && /^ - /) {
|
|
1699
|
+
val = $0
|
|
1700
|
+
sub(/^ - /, "", val)
|
|
1701
|
+
gsub(/["\047]/, "", val)
|
|
1702
|
+
print s "\037" val
|
|
1703
|
+
}
|
|
1704
|
+
if (cur[k] == s && dep[k] == 2 && /^ - /) {
|
|
1705
|
+
val = $0
|
|
1706
|
+
sub(/^ - /, "", val)
|
|
1707
|
+
gsub(/["\047]/, "", val)
|
|
1708
|
+
print s "\037" val
|
|
1709
|
+
}
|
|
1710
|
+
}
|
|
1711
|
+
}
|
|
1712
|
+
' "$file")"
|
|
1713
|
+
for s in "$@"; do
|
|
1714
|
+
j=""
|
|
1715
|
+
while IFS= read -r line; do
|
|
1716
|
+
[ "${line%%$'\037'*}" = "$s" ] || continue
|
|
1717
|
+
v="${line#*$'\037'}"
|
|
1718
|
+
j="$j$v"$'\n'
|
|
1719
|
+
done <<< "$out"
|
|
1720
|
+
while [[ "$j" == *$'\n' ]]; do j="${j%$'\n'}"; done
|
|
1721
|
+
printf -v "IS_YL_${s}_FILE" '%s' "$file"
|
|
1722
|
+
printf -v "IS_YL_${s}_VAL" '%s' "$j"
|
|
1723
|
+
done
|
|
1724
|
+
}
|
|
1725
|
+
|
|
1557
1726
|
# load_targets_cache <file> — parse the whole targets: section once into the
|
|
1558
1727
|
# global IS_TGT_TSV (one `name<TAB>enabled<TAB>output` row per target),
|
|
1559
1728
|
# matching is_target_enabled and get_target_output semantics: inline and block
|