@ainova-systems/intelligence 0.17.1 → 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 +3 -0
- 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 +20 -7
- 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
|
@@ -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
|
package/engine/lib/contract.sh
CHANGED
|
@@ -20,19 +20,40 @@
|
|
|
20
20
|
# schema is this?" before parsing the rest.
|
|
21
21
|
IS_SCHEMA_VERSION_KEY="schema_version"
|
|
22
22
|
|
|
23
|
+
# read_schema_version_var <config_file> — set IS_SCHEMA_VERSION to the applied
|
|
24
|
+
# version, "" if absent; IS_SCHEMA_VERSION_FOUND=1 when the key exists. Read in
|
|
25
|
+
# this shell, not by awk: every lifecycle command and the engine ask before
|
|
26
|
+
# anything else, and on Git Bash each spawned reader costs tens of milliseconds.
|
|
27
|
+
# The value is the text after the first colon, minus surrounding whitespace, one
|
|
28
|
+
# leading quote and one trailing quote.
|
|
29
|
+
read_schema_version_var() {
|
|
30
|
+
local cf="$1" line v
|
|
31
|
+
IS_SCHEMA_VERSION=""
|
|
32
|
+
IS_SCHEMA_VERSION_FOUND=0
|
|
33
|
+
[ -f "$cf" ] || return 0
|
|
34
|
+
while IFS= read -r line || [ -n "$line" ]; do
|
|
35
|
+
line="${line%$'\r'}"
|
|
36
|
+
case "$line" in
|
|
37
|
+
"$IS_SCHEMA_VERSION_KEY:"*) ;;
|
|
38
|
+
*) continue ;;
|
|
39
|
+
esac
|
|
40
|
+
v="${line#*:}"
|
|
41
|
+
v="${v#"${v%%[![:space:]]*}"}"
|
|
42
|
+
case "$v" in \"*|\'*) v="${v#?}" ;; esac
|
|
43
|
+
v="${v%"${v##*[![:space:]]}"}"
|
|
44
|
+
case "$v" in *\"|*\') v="${v%?}" ;; esac
|
|
45
|
+
v="${v%"${v##*[![:space:]]}"}"
|
|
46
|
+
IS_SCHEMA_VERSION="$v"
|
|
47
|
+
IS_SCHEMA_VERSION_FOUND=1
|
|
48
|
+
return 0
|
|
49
|
+
done < "$cf"
|
|
50
|
+
}
|
|
51
|
+
|
|
23
52
|
# read_schema_version <config_file> → applied version, or "" if absent.
|
|
24
53
|
read_schema_version() {
|
|
25
|
-
|
|
26
|
-
[
|
|
27
|
-
|
|
28
|
-
{ sub(/\r$/, "") }
|
|
29
|
-
$0 ~ "^" k ":" {
|
|
30
|
-
v = $0; sub(/^[^:]*:[[:space:]]*/, "", v)
|
|
31
|
-
gsub(/^["\047]|["\047][[:space:]]*$/, "", v)
|
|
32
|
-
sub(/[[:space:]]+$/, "", v)
|
|
33
|
-
print v; exit
|
|
34
|
-
}
|
|
35
|
-
' "$cf"
|
|
54
|
+
read_schema_version_var "$1"
|
|
55
|
+
[ "$IS_SCHEMA_VERSION_FOUND" = 1 ] || return 0
|
|
56
|
+
printf '%s\n' "$IS_SCHEMA_VERSION"
|
|
36
57
|
}
|
|
37
58
|
|
|
38
59
|
# stamp_schema_version <config_file> <version> — idempotent, transactional upsert of
|
|
@@ -74,12 +95,29 @@ is_status() {
|
|
|
74
95
|
fi
|
|
75
96
|
}
|
|
76
97
|
|
|
77
|
-
# Engine version =
|
|
98
|
+
# Engine version = VERSION one level above this lib (BASH_SOURCE works when
|
|
78
99
|
# sourced). Empty if unreadable — callers treat empty as "no guard".
|
|
100
|
+
# engine_version_var sets IS_ENGINE_VERSION, and IS_ENGINE_VERSION_FOUND=1 when
|
|
101
|
+
# the file exists; it reads the file once per process.
|
|
102
|
+
engine_version_var() {
|
|
103
|
+
[ -z "${IS_ENGINE_VERSION_FOUND:-}" ] || return 0
|
|
104
|
+
local lib="${BASH_SOURCE[0]}" vf content=""
|
|
105
|
+
case "$lib" in
|
|
106
|
+
*/*) lib="${lib%/*}" ;;
|
|
107
|
+
*) lib="." ;;
|
|
108
|
+
esac
|
|
109
|
+
vf="$lib/../VERSION"
|
|
110
|
+
IS_ENGINE_VERSION=""
|
|
111
|
+
IS_ENGINE_VERSION_FOUND=0
|
|
112
|
+
[ -f "$vf" ] || return 0
|
|
113
|
+
IFS= read -r -d '' content < "$vf" || true
|
|
114
|
+
IS_ENGINE_VERSION="${content//[$' \t\r\n']/}"
|
|
115
|
+
IS_ENGINE_VERSION_FOUND=1
|
|
116
|
+
}
|
|
117
|
+
|
|
79
118
|
engine_version() {
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
[ -f "$vf" ] && tr -d ' \t\r\n' < "$vf"
|
|
119
|
+
engine_version_var
|
|
120
|
+
[ "$IS_ENGINE_VERSION_FOUND" = 1 ] && printf '%s' "$IS_ENGINE_VERSION"
|
|
83
121
|
}
|
|
84
122
|
|
|
85
123
|
# _ver_gt A B → true if semver A is strictly greater than B (numeric x.y.z;
|
|
@@ -118,9 +156,11 @@ _ver_major() {
|
|
|
118
156
|
# a project stamped ahead exactly as it found it (project_needs_upgrade).
|
|
119
157
|
check_version_compat() {
|
|
120
158
|
local cf="$1" stamp eng
|
|
121
|
-
|
|
159
|
+
read_schema_version_var "$cf"
|
|
160
|
+
stamp="$IS_SCHEMA_VERSION"
|
|
122
161
|
[ -n "$stamp" ] || return 0
|
|
123
|
-
|
|
162
|
+
engine_version_var
|
|
163
|
+
eng="$IS_ENGINE_VERSION"
|
|
124
164
|
[ -n "$eng" ] || return 0
|
|
125
165
|
_ver_gt "$stamp" "$eng" || return 0
|
|
126
166
|
if [ "$(_ver_major "$stamp")" -gt "$(_ver_major "$eng")" ]; then
|