@ainova-systems/intelligence 0.18.0 → 0.19.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 +6 -1
- package/cli/commands/package.sh +8 -3
- package/cli/commands/source.sh +55 -16
- package/cli/intelligence +1 -1
- package/cli/internal/check.sh +64 -13
- package/cli/internal/package-add.sh +25 -4
- package/cli/internal/package-alias.sh +51 -0
- package/cli/internal/package-list.sh +16 -2
- package/cli/internal/package-remove.sh +3 -1
- package/cli/internal/package-update.sh +12 -4
- package/cli/lib/adapter-lifecycle.sh +5 -2
- package/cli/lib/cli-common.sh +4 -1
- package/cli/lib/gitignore.sh +128 -34
- package/cli/lib/manifest.sh +168 -20
- package/cli/lib/registry.sh +18 -13
- package/engine/ENGINE_SHA +1 -1
- package/engine/VERSION +1 -1
- package/engine/adapters/_template.sh +6 -2
- package/engine/adapters/agents.sh +3 -0
- package/engine/adapters/claude.sh +21 -10
- package/engine/adapters/codex.sh +8 -12
- package/engine/adapters/copilot.sh +46 -2
- package/engine/adapters/cursor.sh +6 -1
- package/engine/lib/adapter-contract.sh +17 -6
- package/engine/lib/common.sh +559 -59
- package/engine/sync.sh +9 -0
- package/package.json +1 -1
- package/packages/sync/references/adapters.md +39 -9
- package/packages/sync/references/conventions.md +46 -9
- package/packages/sync/references/onboarding-migration.md +3 -3
- package/packages/sync/skills/intelligence-learn-from-repository/SKILL.md +3 -2
- package/packages/sync/skills/intelligence-review-context/SKILL.md +2 -1
- package/packages/sync/skills/intelligence-update-context/SKILL.md +4 -2
- package/packages/sync/skills/intelligence-update-context/references/agents.md +7 -3
- package/packages/sync/skills/intelligence-update-context/references/skills.md +3 -1
package/engine/lib/common.sh
CHANGED
|
@@ -293,9 +293,10 @@ repo_rel_dir() {
|
|
|
293
293
|
done
|
|
294
294
|
}
|
|
295
295
|
|
|
296
|
-
# Resolve a single source
|
|
297
|
-
# a repo-relative path: the CLI resolves, fetches and
|
|
298
|
-
#
|
|
296
|
+
# Resolve a single source entry to an absolute local directory. Every entry is
|
|
297
|
+
# a repo-relative path by the time it gets here: the CLI resolves, fetches and
|
|
298
|
+
# pins packages, so a package is just a directory under the store, and the list
|
|
299
|
+
# parser has already expanded a package reference to that directory.
|
|
299
300
|
# ALWAYS returns 0 (echoes nothing on failure) so `set -e` callers using
|
|
300
301
|
# `dir="$(resolve_source_dir ...)"` never abort; the caller's existing
|
|
301
302
|
# `[ -d "$dir" ] || continue` guard then skips an unresolved source.
|
|
@@ -460,11 +461,17 @@ copy_md_with_quoted_frontmatter() {
|
|
|
460
461
|
# "argument-hint must be a string" — the skill silently disappears from the
|
|
461
462
|
# picker. Quoting is idempotent: an already-quoted value passes through
|
|
462
463
|
# untouched.
|
|
464
|
+
#
|
|
465
|
+
# The same pass renders SKILL.md's `effort:` for the tool that reads the copy
|
|
466
|
+
# (map_effort_var): the first `effort:` line becomes the tool's level, and is
|
|
467
|
+
# dropped — with every later one — when the tool has no level for it. The
|
|
468
|
+
# plain forms render for the shared `open` tree, which keeps a neutral level as
|
|
469
|
+
# written and drops only an empty or off-scale one.
|
|
463
470
|
# Usage: copy_skill_bundle "src/skill/dir" "dest/skill/dir"
|
|
464
471
|
copy_skill_bundle() {
|
|
465
472
|
_skill_bundles_reset
|
|
466
473
|
_skill_bundle_stage "$1" "$2"
|
|
467
|
-
_skill_bundles_flush
|
|
474
|
+
_skill_bundles_flush open
|
|
468
475
|
}
|
|
469
476
|
|
|
470
477
|
# copy_skill_bundle_dirs <dest_root> <src_dir>... — batch form: ONE cp -R
|
|
@@ -473,8 +480,14 @@ copy_skill_bundle() {
|
|
|
473
480
|
# bundles. A later source with the same skill name overwrites file-by-file in
|
|
474
481
|
# order, exactly like the sequential per-bundle copies did.
|
|
475
482
|
copy_skill_bundle_dirs() {
|
|
476
|
-
|
|
477
|
-
|
|
483
|
+
copy_skill_bundle_dirs_for open "$@"
|
|
484
|
+
}
|
|
485
|
+
|
|
486
|
+
# copy_skill_bundle_dirs_for <tool> <dest_root> <src_dir>... — the batch form
|
|
487
|
+
# for a tree one tool reads, rendering `effort:` for that tool.
|
|
488
|
+
copy_skill_bundle_dirs_for() {
|
|
489
|
+
local effort_tool="$1" dest_root="$2"
|
|
490
|
+
shift 2
|
|
478
491
|
[ "$#" -gt 0 ] || return 0
|
|
479
492
|
local src dest
|
|
480
493
|
local -a srcs=()
|
|
@@ -488,7 +501,7 @@ copy_skill_bundle_dirs() {
|
|
|
488
501
|
dest="$dest_root/${src##*/}"
|
|
489
502
|
_skill_bundle_note "$dest"
|
|
490
503
|
done
|
|
491
|
-
_skill_bundles_flush
|
|
504
|
+
_skill_bundles_flush "$effort_tool"
|
|
492
505
|
}
|
|
493
506
|
|
|
494
507
|
_skill_bundles_reset() {
|
|
@@ -531,6 +544,7 @@ _skill_bundle_note() {
|
|
|
531
544
|
esac
|
|
532
545
|
}
|
|
533
546
|
|
|
547
|
+
# _skill_bundles_flush <effort-tool>
|
|
534
548
|
_skill_bundles_flush() {
|
|
535
549
|
[ "${#_SB_DESTS[@]}" -gt 0 ] || return 0
|
|
536
550
|
local -a mds=()
|
|
@@ -540,18 +554,27 @@ _skill_bundles_flush() {
|
|
|
540
554
|
done < <(find "${_SB_DESTS[@]}" -type f -name '*.md')
|
|
541
555
|
_skill_bundles_reset
|
|
542
556
|
[ "${#mds[@]}" -gt 0 ] || return 0
|
|
557
|
+
effort_map_var "$1"
|
|
543
558
|
# One awk: quote free-text frontmatter fields in each top-level SKILL.md
|
|
544
559
|
# (strict-YAML consumers reject unquoted colons; `argument-hint:
|
|
545
560
|
# [pr-number]` would otherwise arrive as a YAML flow sequence and the
|
|
546
|
-
# skill silently vanishes from the picker)
|
|
547
|
-
# every bundled markdown file. Quoting is
|
|
548
|
-
# values pass through untouched. The quote list
|
|
549
|
-
# environment, like
|
|
561
|
+
# skill silently vanishes from the picker), render its `effort:` for the
|
|
562
|
+
# tool, and expand layout tokens in every bundled markdown file. Quoting is
|
|
563
|
+
# idempotent — already-quoted values pass through untouched. The quote list
|
|
564
|
+
# and the effort map travel through the environment, like
|
|
565
|
+
# emit_wrapped_bodies specs. The effort key is matched exactly as
|
|
566
|
+
# frontmatter_index reads it, so the line rewritten is the one the
|
|
567
|
+
# lint judged.
|
|
550
568
|
is_fin_awk_vars
|
|
551
|
-
IS_QUOTE_LIST="$quote_list" awk "${IS_FIN_V[@]}" "$IS_AWK_LIB"'
|
|
569
|
+
IS_QUOTE_LIST="$quote_list" IS_EFFORT_MAP="$IS_EFFORT_MAP" awk "${IS_FIN_V[@]}" "$IS_AWK_LIB"'
|
|
552
570
|
BEGIN {
|
|
553
571
|
qn = split(ENVIRON["IS_QUOTE_LIST"], QL, "\n")
|
|
554
572
|
for (qi = 1; qi <= qn; qi++) if (QL[qi] != "") QUOTE[QL[qi]] = 1
|
|
573
|
+
en = split(ENVIRON["IS_EFFORT_MAP"], EL, "\n")
|
|
574
|
+
for (ei = 1; ei <= en; ei++) {
|
|
575
|
+
eq = index(EL[ei], "=")
|
|
576
|
+
if (eq > 0) EFFORT[substr(EL[ei], 1, eq - 1)] = substr(EL[ei], eq + 1)
|
|
577
|
+
}
|
|
555
578
|
}
|
|
556
579
|
function yamlq(s, out, i, c) {
|
|
557
580
|
out = ""
|
|
@@ -572,12 +595,19 @@ _skill_bundles_flush() {
|
|
|
572
595
|
flush_file()
|
|
573
596
|
out_file = FILENAME; line_n = 0
|
|
574
597
|
state = (FILENAME in QUOTE) ? "before" : ""
|
|
598
|
+
effort_seen = 0
|
|
575
599
|
}
|
|
576
600
|
{ sub(/\r$/, "") }
|
|
577
601
|
state == "before" {
|
|
578
602
|
if (FNR == 1 && $0 == "---") state = "in_fm"
|
|
579
603
|
else state = "after"
|
|
580
604
|
}
|
|
605
|
+
state == "in_fm" && FNR > 1 && substr($0, 1, 7) == "effort:" {
|
|
606
|
+
if (effort_seen++) next
|
|
607
|
+
val = fm_value_strip(substr($0, 8))
|
|
608
|
+
if (!(val in EFFORT)) next
|
|
609
|
+
if (EFFORT[val] != val) $0 = "effort: " EFFORT[val]
|
|
610
|
+
}
|
|
581
611
|
state == "in_fm" && FNR > 1 {
|
|
582
612
|
if ($0 == "---") state = "after"
|
|
583
613
|
else {
|
|
@@ -660,7 +690,7 @@ sync_open_skill_dirs() {
|
|
|
660
690
|
# copy_skill_bundle_dirs owns the frontmatter-quoting pass, so every
|
|
661
691
|
# target gets it — not just this open-standard dir.
|
|
662
692
|
if [ "$count" -gt 0 ]; then
|
|
663
|
-
|
|
693
|
+
copy_skill_bundle_dirs_for open "$output_dir" "${skill_dirs[@]}"
|
|
664
694
|
open_skill_invocation_policies "$output_dir" || return 1
|
|
665
695
|
fi
|
|
666
696
|
|
|
@@ -677,12 +707,23 @@ sync_open_skill_dirs() {
|
|
|
677
707
|
# `agents/openai.yaml` beside SKILL.md. Source skills stay tool-neutral: the
|
|
678
708
|
# policy file is derived here, in the open-standard tree Codex reads, so every
|
|
679
709
|
# adapter sharing that tree writes the same bytes. A skill that ships its own
|
|
680
|
-
# `agents/openai.yaml` keeps
|
|
710
|
+
# `agents/openai.yaml` keeps the rest of it: the output copy gains the policy
|
|
711
|
+
# when the author's file sets none, and a file whose policy says otherwise, or
|
|
712
|
+
# that sync cannot read as saying it, refuses the render — an owner-only skill
|
|
713
|
+
# Codex can still select is the exact failure the field exists to prevent, so
|
|
714
|
+
# it never passes as a success.
|
|
681
715
|
open_skill_invocation_policies() {
|
|
682
|
-
local output_dir="$1" path flag rows
|
|
683
|
-
local -a skill_mds=() policy_dirs=() policies=()
|
|
716
|
+
local output_dir="$1" path dir flag rows refusals
|
|
717
|
+
local -a skill_mds=() policy_dirs=() policies=() authored=()
|
|
684
718
|
for path in "$output_dir"/*/SKILL.md; do
|
|
685
|
-
|
|
719
|
+
dir="${path%/SKILL.md}"
|
|
720
|
+
# A link is emitted as-is (see _skill_bundle_note): reading through it
|
|
721
|
+
# or writing beside it would reach outside the output tree.
|
|
722
|
+
if [ -L "$dir" ] || [ -L "$path" ]; then
|
|
723
|
+
echo " WARN: ${dir##*/}/SKILL.md is reached through a symlink — Codex gets no invocation policy derived for it" >&2
|
|
724
|
+
elif [ -f "$path" ]; then
|
|
725
|
+
skill_mds+=("$path")
|
|
726
|
+
fi
|
|
686
727
|
done
|
|
687
728
|
[ "${#skill_mds[@]}" -gt 0 ] || return 0
|
|
688
729
|
# Captured, not streamed: a reader cut short must fail the render rather
|
|
@@ -693,13 +734,32 @@ open_skill_invocation_policies() {
|
|
|
693
734
|
}
|
|
694
735
|
while IFS=$'\x1f' read -r path flag; do
|
|
695
736
|
[ "$flag" = "true" ] || continue
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
[ -L "$path" ]
|
|
699
|
-
|
|
737
|
+
dir="${path%/SKILL.md}"
|
|
738
|
+
path="$dir/agents"
|
|
739
|
+
if [ -L "$path" ] || [ -L "$path/openai.yaml" ]; then
|
|
740
|
+
echo " WARN: ${dir##*/}/agents/openai.yaml is a symlink — left as-is, its invocation policy is not enforced" >&2
|
|
741
|
+
continue
|
|
742
|
+
fi
|
|
743
|
+
if [ -s "$path/openai.yaml" ]; then
|
|
744
|
+
authored+=("$path/openai.yaml")
|
|
745
|
+
continue
|
|
746
|
+
fi
|
|
700
747
|
policy_dirs+=("$path")
|
|
701
748
|
policies+=("$path/openai.yaml")
|
|
702
749
|
done <<< "$rows"
|
|
750
|
+
if [ "${#authored[@]}" -gt 0 ]; then
|
|
751
|
+
refusals="$(enforce_authored_invocation_policies "${authored[@]}")" || {
|
|
752
|
+
echo "ERROR: could not read or rewrite agents/openai.yaml under $output_dir" >&2
|
|
753
|
+
return 1
|
|
754
|
+
}
|
|
755
|
+
if [ -n "$refusals" ]; then
|
|
756
|
+
while IFS=$'\t' read -r path flag; do
|
|
757
|
+
path="${path%/agents/openai.yaml}"
|
|
758
|
+
echo "ERROR: ${path##*/} sets disable-model-invocation: true, but its agents/openai.yaml $flag — make them agree, or Codex can still select the skill" >&2
|
|
759
|
+
done <<< "$refusals"
|
|
760
|
+
return 1
|
|
761
|
+
fi
|
|
762
|
+
fi
|
|
703
763
|
[ "${#policies[@]}" -gt 0 ] || return 0
|
|
704
764
|
mkdir -p "${policy_dirs[@]}"
|
|
705
765
|
for path in "${policies[@]}"; do
|
|
@@ -709,20 +769,128 @@ open_skill_invocation_policies() {
|
|
|
709
769
|
finalize_output_files "${policies[@]}"
|
|
710
770
|
}
|
|
711
771
|
|
|
772
|
+
# enforce_authored_invocation_policies <openai.yaml>... — one awk pass over
|
|
773
|
+
# author-owned Codex files of skills that set `disable-model-invocation: true`.
|
|
774
|
+
# A file already setting `policy.allow_implicit_invocation: false` is left
|
|
775
|
+
# alone; one without that key gains it, inside an existing `policy:` block or
|
|
776
|
+
# as a new block, every other line kept. Only the plain boolean on a direct
|
|
777
|
+
# child of `policy:` counts — the key Codex reads. A file that sets it
|
|
778
|
+
# otherwise, or whose `policy:` sync cannot read or extend safely, is printed
|
|
779
|
+
# as `<file>\t<reason>` and left untouched for the caller to refuse.
|
|
780
|
+
enforce_authored_invocation_policies() {
|
|
781
|
+
awk '
|
|
782
|
+
function reset() {
|
|
783
|
+
file = ""; n = 0; haspol = 0; inpol = 0; polline = 0
|
|
784
|
+
inline = ""; child = ""; val = ""; hasval = 0; bad = ""
|
|
785
|
+
}
|
|
786
|
+
# A one-line policy counts only as a flat flow mapping of plain
|
|
787
|
+
# scalars holding the key itself; a quoted or nested value is not read.
|
|
788
|
+
function inline_ok(s, body, parts, k, i, p) {
|
|
789
|
+
if (s !~ /^\{.*\}$/) return 0
|
|
790
|
+
body = substr(s, 2, length(s) - 2)
|
|
791
|
+
if (body ~ /[{}"\047]/ || index(body, "[") || index(body, "]")) return 0
|
|
792
|
+
k = split(body, parts, ",")
|
|
793
|
+
for (i = 1; i <= k; i++) {
|
|
794
|
+
p = parts[i]
|
|
795
|
+
gsub(/^[[:space:]]+|[[:space:]]+$/, "", p)
|
|
796
|
+
if (p ~ /^allow_implicit_invocation[[:space:]]*:[[:space:]]*false$/) return 1
|
|
797
|
+
}
|
|
798
|
+
return 0
|
|
799
|
+
}
|
|
800
|
+
function finish( i, ind) {
|
|
801
|
+
if (file == "") return
|
|
802
|
+
if (bad == "" && inline != "" && !inline_ok(inline))
|
|
803
|
+
bad = "has policy: " inline ", which does not plainly set allow_implicit_invocation: false"
|
|
804
|
+
if (bad == "" && hasval && val != "false")
|
|
805
|
+
bad = "sets policy.allow_implicit_invocation: " (val == "" ? "null" : val)
|
|
806
|
+
if (bad != "") { printf "%s\t%s\n", file, bad; reset(); return }
|
|
807
|
+
if (inline != "" || hasval) { reset(); return }
|
|
808
|
+
ind = (child != "") ? child : " "
|
|
809
|
+
for (i = 1; i <= n; i++) {
|
|
810
|
+
print L[i] > file
|
|
811
|
+
if (haspol && i == polline) print ind "allow_implicit_invocation: false" > file
|
|
812
|
+
}
|
|
813
|
+
if (!haspol) {
|
|
814
|
+
print "policy:" > file
|
|
815
|
+
print " allow_implicit_invocation: false" > file
|
|
816
|
+
}
|
|
817
|
+
close(file)
|
|
818
|
+
reset()
|
|
819
|
+
}
|
|
820
|
+
BEGIN { reset() }
|
|
821
|
+
FNR == 1 { finish(); file = FILENAME }
|
|
822
|
+
{ sub(/\r$/, ""); L[++n] = $0 }
|
|
823
|
+
/^["\047]?policy["\047]?[[:space:]]*:/ {
|
|
824
|
+
if (haspol) bad = "has more than one policy: key"
|
|
825
|
+
haspol = 1; polline = n; inpol = 1
|
|
826
|
+
rest = $0
|
|
827
|
+
sub(/^[^:]*:[[:space:]]*/, "", rest)
|
|
828
|
+
sub(/[[:space:]]+#.*$/, "", rest)
|
|
829
|
+
sub(/^#.*$/, "", rest)
|
|
830
|
+
sub(/[[:space:]]+$/, "", rest)
|
|
831
|
+
if (rest != "") { inline = rest; inpol = 0 }
|
|
832
|
+
next
|
|
833
|
+
}
|
|
834
|
+
inpol && /^[^[:space:]#]/ { inpol = 0 }
|
|
835
|
+
# The first indented line fixes the depth of the direct children of
|
|
836
|
+
# policy:, and a deeper line belongs to another key, not the one Codex
|
|
837
|
+
# reads.
|
|
838
|
+
inpol && /^[[:space:]]+[^[:space:]#]/ {
|
|
839
|
+
match($0, /^[[:space:]]+/)
|
|
840
|
+
cur = substr($0, 1, RLENGTH)
|
|
841
|
+
if (child == "") {
|
|
842
|
+
child = cur
|
|
843
|
+
if ($0 ~ /^[[:space:]]+-([[:space:]]|$)/) bad = "has a policy: that is not a mapping"
|
|
844
|
+
} else if (length(cur) < length(child)) {
|
|
845
|
+
bad = "has a policy: block indented inconsistently"
|
|
846
|
+
}
|
|
847
|
+
if (cur == child && $0 ~ /^[[:space:]]+["\047]?allow_implicit_invocation["\047]?[[:space:]]*:/) {
|
|
848
|
+
v = $0
|
|
849
|
+
sub(/^[^:]*:[[:space:]]*/, "", v)
|
|
850
|
+
sub(/[[:space:]]+#.*$/, "", v)
|
|
851
|
+
sub(/^#.*$/, "", v)
|
|
852
|
+
sub(/[[:space:]]+$/, "", v)
|
|
853
|
+
hasval = 1; val = v
|
|
854
|
+
}
|
|
855
|
+
}
|
|
856
|
+
END { finish() }
|
|
857
|
+
' "$@"
|
|
858
|
+
}
|
|
859
|
+
|
|
712
860
|
# Lint YAML frontmatter for common pitfalls (unquoted colons, leading tabs).
|
|
713
861
|
# Print warnings to stderr; do not fail. Strict consumers (Codex CLI) reject
|
|
714
862
|
# these files with cryptic messages — catching them in sync gives better DX.
|
|
715
863
|
# Batched: one awk process lints every file passed.
|
|
864
|
+
# The engine lints every source once, so this is also where an `effort:` off
|
|
865
|
+
# IS_EFFORT_LEVELS is reported — read with frontmatter_index's semantics,
|
|
866
|
+
# first occurrence only. Adapters render it as absent and say nothing, so the
|
|
867
|
+
# warning names the source exactly once however many tools render it. Unlike
|
|
868
|
+
# the lint notes it is an unindented `WARNING:` line, because `sync --compact`
|
|
869
|
+
# (and so `init`) keeps only those (decision 0015).
|
|
716
870
|
# Usage: lint_frontmatter_files "a.md" "b.md" ...
|
|
717
871
|
lint_frontmatter_files() {
|
|
718
872
|
[ "$#" -gt 0 ] || return 0
|
|
719
|
-
awk '
|
|
720
|
-
|
|
873
|
+
awk -v levels="$IS_EFFORT_LEVELS" "$IS_AWK_LIB"'
|
|
874
|
+
BEGIN {
|
|
875
|
+
nl = split(levels, LV, " ")
|
|
876
|
+
allowed = ""
|
|
877
|
+
for (li = 1; li <= nl; li++) {
|
|
878
|
+
LEVEL[LV[li]] = 1
|
|
879
|
+
allowed = allowed (li > 1 ? ", " : "") LV[li]
|
|
880
|
+
}
|
|
881
|
+
}
|
|
882
|
+
FNR == 1 { in_fm = 0; done = 0; effort_seen = 0 }
|
|
721
883
|
{ sub(/\r$/, "") }
|
|
722
884
|
done { next }
|
|
723
885
|
FNR == 1 && $0 != "---" { done = 1; next }
|
|
724
886
|
FNR == 1 { in_fm = 1; next }
|
|
725
887
|
in_fm && $0 == "---" { done = 1; next }
|
|
888
|
+
in_fm && substr($0, 1, 7) == "effort:" && !effort_seen++ {
|
|
889
|
+
effort = fm_value_strip(substr($0, 8))
|
|
890
|
+
if (effort != "" && !(effort in LEVEL)) {
|
|
891
|
+
printf "WARNING: %s:%d effort \"%s\" is not one of %s — ignored, so no tool receives an effort from this file\n", FILENAME, FNR, effort, allowed > "/dev/stderr"
|
|
892
|
+
}
|
|
893
|
+
}
|
|
726
894
|
in_fm && /^\t/ {
|
|
727
895
|
printf " WARN: %s:%d leading tab in frontmatter (use spaces)\n", FILENAME, FNR > "/dev/stderr"
|
|
728
896
|
}
|
|
@@ -876,8 +1044,8 @@ get_model_default_var() {
|
|
|
876
1044
|
cursor:heavy) IS_MODEL_DEFAULT="inherit" ;;
|
|
877
1045
|
cursor:standard) IS_MODEL_DEFAULT="inherit" ;;
|
|
878
1046
|
cursor:light) IS_MODEL_DEFAULT="inherit" ;;
|
|
879
|
-
# GPT-6 has no mid model, so frontier and heavy share Astra
|
|
880
|
-
#
|
|
1047
|
+
# GPT-6 has no mid model, so frontier and heavy share Astra and render
|
|
1048
|
+
# identically; reasoning effort comes from `effort:`, never the tier.
|
|
881
1049
|
copilot:frontier) IS_MODEL_DEFAULT="gpt-6-astra" ;;
|
|
882
1050
|
copilot:heavy) IS_MODEL_DEFAULT="gpt-6-astra" ;;
|
|
883
1051
|
copilot:standard) IS_MODEL_DEFAULT="gpt-6.1-sol" ;;
|
|
@@ -1010,7 +1178,11 @@ load_model_tiers() {
|
|
|
1010
1178
|
}
|
|
1011
1179
|
|
|
1012
1180
|
# resolve_model_var <tier> — set IS_MODEL from the tiers load_model_tiers
|
|
1013
|
-
# resolved. An empty tier resolves to heavy, like get_model's default.
|
|
1181
|
+
# resolved. An empty tier resolves to heavy, like get_model's default. A tier
|
|
1182
|
+
# the tool has no default for and the manifest overrides nowhere — a typo, as a
|
|
1183
|
+
# rule — still renders an empty model, but never silently: the run warns once
|
|
1184
|
+
# per tool and tier, unindented on purpose because `sync --compact` keeps only
|
|
1185
|
+
# `WARNING:` lines (decision 0016).
|
|
1014
1186
|
# shellcheck disable=SC2034 # IS_MODEL is the return channel read by adapters
|
|
1015
1187
|
resolve_model_var() {
|
|
1016
1188
|
case "$1" in
|
|
@@ -1018,7 +1190,17 @@ resolve_model_var() {
|
|
|
1018
1190
|
heavy|"") IS_MODEL="$IS_MODEL_HEAVY" ;;
|
|
1019
1191
|
standard) IS_MODEL="$IS_MODEL_STANDARD" ;;
|
|
1020
1192
|
light) IS_MODEL="$IS_MODEL_LIGHT" ;;
|
|
1021
|
-
*)
|
|
1193
|
+
*)
|
|
1194
|
+
IS_MODEL="$(get_model "$IS_MODEL_CFG" "$IS_MODEL_IDE" "$1")"
|
|
1195
|
+
[ -n "$IS_MODEL" ] && return 0
|
|
1196
|
+
case "${IS_MODEL_WARNED:-|}" in
|
|
1197
|
+
*"|$IS_MODEL_IDE:$1|"*) ;;
|
|
1198
|
+
*)
|
|
1199
|
+
IS_MODEL_WARNED="${IS_MODEL_WARNED:-|}$IS_MODEL_IDE:$1|"
|
|
1200
|
+
echo "WARNING: no $IS_MODEL_IDE model for tier '$1' — agents with it get an empty model; use frontier, heavy, standard or light, or set models.$IS_MODEL_IDE.$1" >&2
|
|
1201
|
+
;;
|
|
1202
|
+
esac
|
|
1203
|
+
;;
|
|
1022
1204
|
esac
|
|
1023
1205
|
}
|
|
1024
1206
|
|
|
@@ -1110,6 +1292,64 @@ map_access_to_claude_disallowed() {
|
|
|
1110
1292
|
echo "$IS_CLAUDE_DISALLOWED"
|
|
1111
1293
|
}
|
|
1112
1294
|
|
|
1295
|
+
# --- Effort Mapping ---
|
|
1296
|
+
|
|
1297
|
+
# `effort:` is the tool-neutral reasoning effort an agent or skill asks for,
|
|
1298
|
+
# lowest first (decision 0015). It is independent of `tier`, which selects the
|
|
1299
|
+
# model only. A value off this scale — matched exactly, case included — is an
|
|
1300
|
+
# absent effort: lint_frontmatter_files warns about it and no tool receives
|
|
1301
|
+
# one, because a typo in another source's package must not block a sync.
|
|
1302
|
+
IS_EFFORT_LEVELS="low medium high xhigh max ultra"
|
|
1303
|
+
|
|
1304
|
+
# effort_levels_var <tool> — set IS_EFFORT_TOOL_LEVELS to the levels the tool's
|
|
1305
|
+
# native effort field accepts, in scale order; empty for a tool without one.
|
|
1306
|
+
# `open` is the shared Agent Skills tree (.agents/skills/), which keeps the
|
|
1307
|
+
# neutral value for the tools that read it.
|
|
1308
|
+
effort_levels_var() {
|
|
1309
|
+
case "$1" in
|
|
1310
|
+
claude) IS_EFFORT_TOOL_LEVELS="low medium high xhigh max" ;;
|
|
1311
|
+
codex|open) IS_EFFORT_TOOL_LEVELS="$IS_EFFORT_LEVELS" ;;
|
|
1312
|
+
# Copilot, Cursor, OpenCode, Antigravity and Pi have no per-agent
|
|
1313
|
+
# effort field: the tool's own setting applies.
|
|
1314
|
+
*) IS_EFFORT_TOOL_LEVELS="" ;;
|
|
1315
|
+
esac
|
|
1316
|
+
}
|
|
1317
|
+
|
|
1318
|
+
# map_effort_var <tool> <effort> — set IS_EFFORT to the level the tool receives:
|
|
1319
|
+
# the same level, or the nearest lower one the tool has (`ultra` is `max` for
|
|
1320
|
+
# Claude Code). Empty when the effort is absent or off the scale, or the tool
|
|
1321
|
+
# has no field. Which levels a particular model supports is the tool's call: a
|
|
1322
|
+
# manifest may override the model, and the tools fall back on their own.
|
|
1323
|
+
map_effort_var() {
|
|
1324
|
+
local level best=""
|
|
1325
|
+
IS_EFFORT=""
|
|
1326
|
+
effort_levels_var "$1"
|
|
1327
|
+
for level in $IS_EFFORT_LEVELS; do
|
|
1328
|
+
case " $IS_EFFORT_TOOL_LEVELS " in *" $level "*) best="$level" ;; esac
|
|
1329
|
+
if [ "$level" = "${2-}" ]; then
|
|
1330
|
+
IS_EFFORT="$best"
|
|
1331
|
+
return 0
|
|
1332
|
+
fi
|
|
1333
|
+
done
|
|
1334
|
+
}
|
|
1335
|
+
|
|
1336
|
+
map_effort() {
|
|
1337
|
+
map_effort_var "$1" "$2"
|
|
1338
|
+
echo "$IS_EFFORT"
|
|
1339
|
+
}
|
|
1340
|
+
|
|
1341
|
+
# effort_map_var <tool> — set IS_EFFORT_MAP to one `<neutral>=<native>` line per
|
|
1342
|
+
# level the tool maps, so a batched awk pass rewrites `effort:` in place with
|
|
1343
|
+
# map_effort_var's answers instead of a second copy of the mapping.
|
|
1344
|
+
effort_map_var() {
|
|
1345
|
+
local level map=""
|
|
1346
|
+
for level in $IS_EFFORT_LEVELS; do
|
|
1347
|
+
map_effort_var "$1" "$level"
|
|
1348
|
+
[ -z "$IS_EFFORT" ] || map+="$level=$IS_EFFORT"$'\n'
|
|
1349
|
+
done
|
|
1350
|
+
IS_EFFORT_MAP="$map"
|
|
1351
|
+
}
|
|
1352
|
+
|
|
1113
1353
|
# --- Validation ---
|
|
1114
1354
|
|
|
1115
1355
|
# Lexically canonicalize a path: collapse `//`, `.` and `..` by pure string
|
|
@@ -1655,9 +1895,213 @@ report_context_source_sizes() {
|
|
|
1655
1895
|
|
|
1656
1896
|
# --- Config Parsing ---
|
|
1657
1897
|
|
|
1898
|
+
# --- Package references in sources -------------------------------------------
|
|
1899
|
+
# A `sources:` entry names a directory inside an installed package in one of
|
|
1900
|
+
# three spellings (decision 0019):
|
|
1901
|
+
#
|
|
1902
|
+
# .intelligence/packages/@scope/name/<dir> the store path: an ordinary
|
|
1903
|
+
# repository path, as the CLI writes it
|
|
1904
|
+
# @scope/name/<dir> by full name: a reference only while
|
|
1905
|
+
# `packages:` declares @scope/name,
|
|
1906
|
+
# otherwise an ordinary path as before
|
|
1907
|
+
# <alias>:<dir> by the alias that package's
|
|
1908
|
+
# `packages:` entry declares
|
|
1909
|
+
#
|
|
1910
|
+
# A reference renders exactly as the store path it stands for. The list parser
|
|
1911
|
+
# below expands it inside the awk pass that already reads the manifest; that
|
|
1912
|
+
# pass is the one point every reader goes through — adapters, the engine's own
|
|
1913
|
+
# loops and the CLI — so expansion costs no process, and the CLI sees exactly
|
|
1914
|
+
# what the engine renders (decision 0009). A reference that resolves to nothing
|
|
1915
|
+
# is left out of the list, so no reader can render it as a path: `sync` names it
|
|
1916
|
+
# in a WARNING: line and `status --check` reports it.
|
|
1917
|
+
#
|
|
1918
|
+
# An alias is two or more of A-Z a-z 0-9 . _ - and starts with a letter or digit:
|
|
1919
|
+
# no `/`, `:` or `@`, no whitespace or quotes, and never one letter that reads
|
|
1920
|
+
# like a drive. It is a lookup key, never part of a path. <dir> is one or more
|
|
1921
|
+
# `/`-separated segments, none of them empty, `.` or `..`, without a backslash,
|
|
1922
|
+
# so a reference never leaves its package.
|
|
1923
|
+
#
|
|
1924
|
+
# These functions are the only definition. The CLI's editors (lib/manifest.sh),
|
|
1925
|
+
# `source`, `package alias` and `status --check` load this same string:
|
|
1926
|
+
#
|
|
1927
|
+
# pkr_collect(line) feed every manifest line; records the names `packages:`
|
|
1928
|
+
# declares and the alias each one states
|
|
1929
|
+
# pkr_parse(entry) classify one entry: path, ok, unknown (an alias no
|
|
1930
|
+
# package declares), ambiguous (an alias several declare)
|
|
1931
|
+
# or invalid (a reference whose <dir> is malformed); sets
|
|
1932
|
+
# PKR_NAME, PKR_DIR, PKR_ALIAS and, when ambiguous,
|
|
1933
|
+
# PKR_HOLDERS
|
|
1934
|
+
# pkr_expand(entry) the directory the entry renders as, "" when it renders
|
|
1935
|
+
# nothing; leaves the state in PKR_STATE
|
|
1936
|
+
# pkr_alias_ok(a) whether <a> is a well-formed alias
|
|
1937
|
+
IS_PKG_REF_AWK='
|
|
1938
|
+
function pkr_seg_ok(s) { return s != "" && s != "." && s != ".." }
|
|
1939
|
+
# The CLI refuses any other package name (assert_valid_pkg_name): a name
|
|
1940
|
+
# becomes a store path, so one that is not a plain @scope/name is never
|
|
1941
|
+
# declared.
|
|
1942
|
+
function pkr_name_ok(name, p) {
|
|
1943
|
+
if (name !~ /^@[@A-Za-z0-9._-]*\/[@A-Za-z0-9._-]*$/) return 0
|
|
1944
|
+
p = index(name, "/")
|
|
1945
|
+
return pkr_seg_ok(substr(name, 2, p - 2)) && pkr_seg_ok(substr(name, p + 1))
|
|
1946
|
+
}
|
|
1947
|
+
# Enumerated, not a range: a range follows the locale in some awks.
|
|
1948
|
+
function pkr_alias_ok(a, i, c) {
|
|
1949
|
+
if (length(a) < 2) return 0
|
|
1950
|
+
for (i = 1; i <= length(a); i++) {
|
|
1951
|
+
c = substr(a, i, 1)
|
|
1952
|
+
if (index("ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789", c)) continue
|
|
1953
|
+
if (i > 1 && index("._-", c)) continue
|
|
1954
|
+
return 0
|
|
1955
|
+
}
|
|
1956
|
+
return 1
|
|
1957
|
+
}
|
|
1958
|
+
function pkr_dir_ok(dir, n, i, seg) {
|
|
1959
|
+
if (dir == "" || index(dir, "\\")) return 0
|
|
1960
|
+
n = split(dir, seg, "/")
|
|
1961
|
+
for (i = 1; i <= n; i++) if (!pkr_seg_ok(seg[i])) return 0
|
|
1962
|
+
return 1
|
|
1963
|
+
}
|
|
1964
|
+
# The quoted keys of the top-level packages: block and their alias fields,
|
|
1965
|
+
# read the way lib/qmap.awk reads what the CLI writes: a duplicated key is
|
|
1966
|
+
# one package, and the first alias a package states is the one it has.
|
|
1967
|
+
function pkr_collect(line, s, q, v) {
|
|
1968
|
+
if (line ~ /^packages:[ \t]*$/) { pkr_in = 1; pkr_cur = ""; return }
|
|
1969
|
+
if (!pkr_in || line ~ /^[ \t]*(#.*)?$/) return
|
|
1970
|
+
if (line ~ /^[^ \t]/) { pkr_in = 0; pkr_cur = ""; return }
|
|
1971
|
+
if (substr(line, 1, 3) == " \"") {
|
|
1972
|
+
pkr_cur = ""
|
|
1973
|
+
s = substr(line, 4)
|
|
1974
|
+
q = index(s, "\"")
|
|
1975
|
+
if (q < 2) return
|
|
1976
|
+
v = substr(s, 1, q - 1)
|
|
1977
|
+
if (!pkr_name_ok(v)) return
|
|
1978
|
+
if (!(v in pkr_seen)) { pkr_seen[v] = 1; PKR_NAMES[++PKR_N] = v }
|
|
1979
|
+
pkr_cur = v
|
|
1980
|
+
return
|
|
1981
|
+
}
|
|
1982
|
+
if (substr(line, 1, 4) != " ") { pkr_cur = ""; return }
|
|
1983
|
+
if (pkr_cur == "" || (pkr_cur in PKR_ALIAS_RAW) || substr(line, 5, 6) != "alias:") return
|
|
1984
|
+
v = substr(line, 11)
|
|
1985
|
+
sub(/^[ \t]+/, "", v)
|
|
1986
|
+
if (substr(v, 1, 1) == "\"") {
|
|
1987
|
+
v = substr(v, 2)
|
|
1988
|
+
q = index(v, "\"")
|
|
1989
|
+
if (q == 0) return
|
|
1990
|
+
v = substr(v, 1, q - 1)
|
|
1991
|
+
} else {
|
|
1992
|
+
sub(/[ \t]+#.*$/, "", v)
|
|
1993
|
+
sub(/[ \t]+$/, "", v)
|
|
1994
|
+
}
|
|
1995
|
+
PKR_ALIAS_RAW[pkr_cur] = v
|
|
1996
|
+
if (!pkr_alias_ok(v)) return
|
|
1997
|
+
pkr_holders[v] = (v in pkr_holders) ? pkr_holders[v] ", " pkr_cur : pkr_cur
|
|
1998
|
+
pkr_count[v]++
|
|
1999
|
+
pkr_alias[v] = pkr_cur
|
|
2000
|
+
}
|
|
2001
|
+
function pkr_parse(entry, c, p, i, rest, name) {
|
|
2002
|
+
PKR_NAME = ""; PKR_DIR = ""; PKR_ALIAS = ""; PKR_HOLDERS = ""
|
|
2003
|
+
if (substr(entry, 1, 1) == "@") {
|
|
2004
|
+
p = index(entry, "/")
|
|
2005
|
+
if (p == 0) return "path"
|
|
2006
|
+
rest = substr(entry, p + 1)
|
|
2007
|
+
i = index(rest, "/")
|
|
2008
|
+
name = i ? substr(entry, 1, p + i - 1) : entry
|
|
2009
|
+
if (!(name in pkr_seen)) return "path"
|
|
2010
|
+
PKR_NAME = name
|
|
2011
|
+
PKR_DIR = i ? substr(rest, i + 1) : ""
|
|
2012
|
+
return pkr_dir_ok(PKR_DIR) ? "ok" : "invalid"
|
|
2013
|
+
}
|
|
2014
|
+
c = index(entry, ":")
|
|
2015
|
+
if (c == 0 || !pkr_alias_ok(substr(entry, 1, c - 1))) return "path"
|
|
2016
|
+
PKR_ALIAS = substr(entry, 1, c - 1)
|
|
2017
|
+
PKR_DIR = substr(entry, c + 1)
|
|
2018
|
+
if (!pkr_dir_ok(PKR_DIR)) return "invalid"
|
|
2019
|
+
if (!(PKR_ALIAS in pkr_count)) return "unknown"
|
|
2020
|
+
if (pkr_count[PKR_ALIAS] > 1) { PKR_HOLDERS = pkr_holders[PKR_ALIAS]; return "ambiguous" }
|
|
2021
|
+
PKR_NAME = pkr_alias[PKR_ALIAS]
|
|
2022
|
+
return "ok"
|
|
2023
|
+
}
|
|
2024
|
+
function pkr_expand(entry) {
|
|
2025
|
+
PKR_STATE = pkr_parse(entry)
|
|
2026
|
+
if (PKR_STATE == "path") return entry
|
|
2027
|
+
if (PKR_STATE == "ok") return ".intelligence/packages/" PKR_NAME "/" PKR_DIR
|
|
2028
|
+
return ""
|
|
2029
|
+
}
|
|
2030
|
+
'
|
|
2031
|
+
|
|
2032
|
+
# The value of one block-sequence item in the manifest — the only reading of a
|
|
2033
|
+
# list entry. Every list reader below and the CLI's `sources:` editors
|
|
2034
|
+
# (lib/manifest.sh) load this string, so an entry the engine renders and the
|
|
2035
|
+
# entry an editor compares are the same text (decision 0009, point 7).
|
|
2036
|
+
#
|
|
2037
|
+
# yaml_item(text) <text> is what follows the item's `-`. Returns the value:
|
|
2038
|
+
# whitespace around it and a trailing `# comment` dropped,
|
|
2039
|
+
# then every quote removed, as the readers always did. A
|
|
2040
|
+
# comment starts at a `#` that follows whitespace outside
|
|
2041
|
+
# quotes; inside a quoted scalar a `#` is part of the value,
|
|
2042
|
+
# and so is one with no whitespace before it (`a#b`).
|
|
2043
|
+
IS_YAML_ITEM_AWK='
|
|
2044
|
+
function yaml_item(s, q, e, rest) {
|
|
2045
|
+
sub(/^[ \t]+/, "", s)
|
|
2046
|
+
q = substr(s, 1, 1)
|
|
2047
|
+
rest = s
|
|
2048
|
+
s = ""
|
|
2049
|
+
if (q == "\"" || q == "\047") {
|
|
2050
|
+
e = index(substr(rest, 2), q)
|
|
2051
|
+
if (e == 0) { s = rest; rest = "" }
|
|
2052
|
+
else { s = substr(rest, 1, e + 1); rest = substr(rest, e + 2) }
|
|
2053
|
+
}
|
|
2054
|
+
if (s == "" && substr(rest, 1, 1) == "#") rest = ""
|
|
2055
|
+
else if (match(rest, /[ \t]#/)) rest = substr(rest, 1, RSTART - 1)
|
|
2056
|
+
s = s rest
|
|
2057
|
+
sub(/[ \t]+$/, "", s)
|
|
2058
|
+
gsub(/["\047]/, "", s)
|
|
2059
|
+
return s
|
|
2060
|
+
}
|
|
2061
|
+
'
|
|
2062
|
+
|
|
2063
|
+
# The manifest list reader. `mode` selects what it prints for each entry:
|
|
2064
|
+
# expand the entry as the engine renders it: a reference becomes its store
|
|
2065
|
+
# path, one that resolves to nothing is left out
|
|
2066
|
+
# raw the entry as written — what an editor compares and rewrites
|
|
2067
|
+
# classify raw, pkr_parse state, expansion, alias and the packages sharing
|
|
2068
|
+
# it, separated by \037 — what `status --check` and `source` judge
|
|
2069
|
+
# Only rules, agents and skills hold sources; every other list reads raw.
|
|
2070
|
+
# Entries are held until the end of the file because `packages:` may follow
|
|
2071
|
+
# `sources:`.
|
|
2072
|
+
IS_YAML_LIST_AWK='
|
|
2073
|
+
{ sub(/\r$/, ""); pkr_collect($0) }
|
|
2074
|
+
/^[a-z]/ { current_section = ""; depth = 0 }
|
|
2075
|
+
/^ [a-z]/ { current_section = ""; depth = 0 }
|
|
2076
|
+
$0 ~ "^" section ":" { current_section = section; depth = 0; next }
|
|
2077
|
+
$0 ~ "^ " section ":" { current_section = section; depth = 2; next }
|
|
2078
|
+
current_section == section && depth == 0 && /^ - / {
|
|
2079
|
+
vals[++nv] = yaml_item(substr($0, 5))
|
|
2080
|
+
}
|
|
2081
|
+
current_section == section && depth == 2 && /^ - / {
|
|
2082
|
+
vals[++nv] = yaml_item(substr($0, 7))
|
|
2083
|
+
}
|
|
2084
|
+
END {
|
|
2085
|
+
sources = section == "rules" || section == "agents" || section == "skills"
|
|
2086
|
+
n = nv + 0
|
|
2087
|
+
for (i = 1; i <= n; i++) {
|
|
2088
|
+
val = vals[i]
|
|
2089
|
+
if (!sources || mode == "raw") { print val; continue }
|
|
2090
|
+
dir = pkr_expand(val)
|
|
2091
|
+
if (mode == "classify") print val "\037" PKR_STATE "\037" dir "\037" PKR_ALIAS "\037" PKR_HOLDERS
|
|
2092
|
+
else if (PKR_STATE == "path" || PKR_STATE == "ok") print dir
|
|
2093
|
+
}
|
|
2094
|
+
}
|
|
2095
|
+
'
|
|
2096
|
+
|
|
1658
2097
|
# Read a simple list from config.yaml
|
|
1659
2098
|
# Format: key:\n - "value1"\n - "value2"
|
|
1660
|
-
# Usage:
|
|
2099
|
+
# Usage: while IFS= read -r src; do ...; done < <(read_yaml_list "intelligence.yaml" "rules")
|
|
2100
|
+
#
|
|
2101
|
+
# A source list arrives with its package references expanded to store paths —
|
|
2102
|
+
# the spelling every renderer resolves as $REPO_ROOT/<entry> — and without the
|
|
2103
|
+
# ones that resolve to nothing. Editors that rewrite the manifest read it with
|
|
2104
|
+
# read_yaml_list_raw instead.
|
|
1661
2105
|
#
|
|
1662
2106
|
# Consults the load_yaml_list cache first: sync reads the same sections from
|
|
1663
2107
|
# the same manifest dozens of times, and each awk spawn costs tens of
|
|
@@ -1681,27 +2125,67 @@ read_yaml_list() {
|
|
|
1681
2125
|
fi
|
|
1682
2126
|
;;
|
|
1683
2127
|
esac
|
|
1684
|
-
awk -v section="$section"
|
|
1685
|
-
|
|
1686
|
-
|
|
1687
|
-
|
|
1688
|
-
|
|
1689
|
-
|
|
1690
|
-
|
|
1691
|
-
|
|
1692
|
-
|
|
1693
|
-
|
|
1694
|
-
|
|
1695
|
-
|
|
1696
|
-
|
|
1697
|
-
|
|
1698
|
-
|
|
1699
|
-
|
|
1700
|
-
|
|
1701
|
-
|
|
1702
|
-
|
|
2128
|
+
awk -v section="$section" -v mode=expand "$IS_PKG_REF_AWK$IS_YAML_ITEM_AWK$IS_YAML_LIST_AWK" "$file"
|
|
2129
|
+
}
|
|
2130
|
+
|
|
2131
|
+
# read_yaml_list_raw <file> <section> — the entries exactly as written,
|
|
2132
|
+
# references included and never cached. The CLI's sources editors compare and
|
|
2133
|
+
# rewrite these, so an entry is matched as the user spelled it.
|
|
2134
|
+
read_yaml_list_raw() {
|
|
2135
|
+
awk -v section="$2" -v mode=raw "$IS_PKG_REF_AWK$IS_YAML_ITEM_AWK$IS_YAML_LIST_AWK" "$1"
|
|
2136
|
+
}
|
|
2137
|
+
|
|
2138
|
+
# read_source_entries <file> <section> — one \037-separated line per entry: as
|
|
2139
|
+
# written, its pkr_parse state (path, ok, unknown, ambiguous or invalid), the
|
|
2140
|
+
# directory it renders as ("" for none), the alias it names and, when several
|
|
2141
|
+
# packages declare that alias, their names. One pass of the parser the engine
|
|
2142
|
+
# renders through, so a verdict can never disagree with the render.
|
|
2143
|
+
read_source_entries() {
|
|
2144
|
+
awk -v section="$2" -v mode=classify "$IS_PKG_REF_AWK$IS_YAML_ITEM_AWK$IS_YAML_LIST_AWK" "$1"
|
|
2145
|
+
}
|
|
2146
|
+
|
|
2147
|
+
# read_package_aliases <file> — one \037-separated line per declared package that
|
|
2148
|
+
# states an alias: its name, the alias, and ok, ambiguous (another package states
|
|
2149
|
+
# it too) or invalid (not a well-formed alias, so it resolves nothing). An
|
|
2150
|
+
# invalid alias is manifest input that may hold anything, so its field is empty.
|
|
2151
|
+
read_package_aliases() {
|
|
2152
|
+
[ -f "$1" ] || return 0
|
|
2153
|
+
awk "$IS_PKG_REF_AWK"'
|
|
2154
|
+
{ sub(/\r$/, ""); pkr_collect($0) }
|
|
2155
|
+
END {
|
|
2156
|
+
for (i = 1; i <= PKR_N; i++) {
|
|
2157
|
+
name = PKR_NAMES[i]
|
|
2158
|
+
if (!(name in PKR_ALIAS_RAW)) continue
|
|
2159
|
+
a = PKR_ALIAS_RAW[name]
|
|
2160
|
+
if (!pkr_alias_ok(a)) { print name "\037\037invalid"; continue }
|
|
2161
|
+
print name "\037" a "\037" (pkr_count[a] > 1 ? "ambiguous" : "ok")
|
|
2162
|
+
}
|
|
1703
2163
|
}
|
|
1704
|
-
' "$
|
|
2164
|
+
' "$1"
|
|
2165
|
+
}
|
|
2166
|
+
|
|
2167
|
+
# What pkr_alias_ok accepts, in the words every message that refuses an alias
|
|
2168
|
+
# uses.
|
|
2169
|
+
# shellcheck disable=SC2034
|
|
2170
|
+
IS_PKG_ALIAS_RULE="two or more of A-Z a-z 0-9 . _ -, starting with a letter or digit"
|
|
2171
|
+
|
|
2172
|
+
# pkg_alias_valid <alias> — true when <alias> is well formed (pkr_alias_ok). The
|
|
2173
|
+
# value travels through the environment: `awk -v` would interpret backslashes.
|
|
2174
|
+
pkg_alias_valid() {
|
|
2175
|
+
IS_PKR_CANDIDATE="$1" awk "$IS_PKG_REF_AWK"'BEGIN { exit !pkr_alias_ok(ENVIRON["IS_PKR_CANDIDATE"]) }'
|
|
2176
|
+
}
|
|
2177
|
+
|
|
2178
|
+
# source_reference_problem_var <state> <alias> <holders> — set IS_SOURCE_PROBLEM
|
|
2179
|
+
# to why a reference in that state renders nothing, "" for path and ok. The one
|
|
2180
|
+
# wording of `sync`'s WARNING: line and of `status --check`.
|
|
2181
|
+
# shellcheck disable=SC2034
|
|
2182
|
+
source_reference_problem_var() {
|
|
2183
|
+
case "$1" in
|
|
2184
|
+
unknown) IS_SOURCE_PROBLEM="names alias '$2', which no package in packages: declares" ;;
|
|
2185
|
+
ambiguous) IS_SOURCE_PROBLEM="names alias '$2', which several packages declare ($3) — an alias names one package" ;;
|
|
2186
|
+
invalid) IS_SOURCE_PROBLEM="names no directory inside its package — the part after the package must be one or more '/'-separated segments, none empty, '.' or '..'" ;;
|
|
2187
|
+
*) IS_SOURCE_PROBLEM="" ;;
|
|
2188
|
+
esac
|
|
1705
2189
|
}
|
|
1706
2190
|
|
|
1707
2191
|
# load_yaml_list <file> <section> — fill the global IS_YAML_LIST with the
|
|
@@ -1733,7 +2217,10 @@ load_yaml_list() {
|
|
|
1733
2217
|
|
|
1734
2218
|
# load_yaml_lists <file> <section>... — warm the load_yaml_list cache for several
|
|
1735
2219
|
# sections in one manifest pass. Each section runs read_yaml_list's own state
|
|
1736
|
-
# machine, so every cached value is what
|
|
2220
|
+
# machine and its reference expansion, so every cached value is what
|
|
2221
|
+
# load_yaml_list would have stored. The same pass leaves the references that
|
|
2222
|
+
# resolve to nothing in IS_YL_UNRESOLVED, one read_source_entries-shaped line
|
|
2223
|
+
# each with the section in front, for the WARNING: lines `sync` prints.
|
|
1737
2224
|
load_yaml_lists() {
|
|
1738
2225
|
local file="$1" out line s v j
|
|
1739
2226
|
shift
|
|
@@ -1743,9 +2230,9 @@ load_yaml_lists() {
|
|
|
1743
2230
|
*) return 1 ;;
|
|
1744
2231
|
esac
|
|
1745
2232
|
done
|
|
1746
|
-
out="$(awk -v sections="$*" '
|
|
2233
|
+
out="$(awk -v sections="$*" "$IS_PKG_REF_AWK$IS_YAML_ITEM_AWK"'
|
|
1747
2234
|
BEGIN { n = split(sections, want, " ") }
|
|
1748
|
-
{ sub(/\r$/, "") }
|
|
2235
|
+
{ sub(/\r$/, ""); pkr_collect($0) }
|
|
1749
2236
|
{
|
|
1750
2237
|
for (k = 1; k <= n; k++) {
|
|
1751
2238
|
s = want[k]
|
|
@@ -1754,19 +2241,26 @@ load_yaml_lists() {
|
|
|
1754
2241
|
if ($0 ~ "^" s ":") { cur[k] = s; dep[k] = 0; continue }
|
|
1755
2242
|
if ($0 ~ "^ " s ":") { cur[k] = s; dep[k] = 2; continue }
|
|
1756
2243
|
if (cur[k] == s && dep[k] == 0 && /^ - /) {
|
|
1757
|
-
|
|
1758
|
-
|
|
1759
|
-
gsub(/["\047]/, "", val)
|
|
1760
|
-
print s "\037" val
|
|
2244
|
+
got_s[++ng] = s
|
|
2245
|
+
got_v[ng] = yaml_item(substr($0, 5))
|
|
1761
2246
|
}
|
|
1762
2247
|
if (cur[k] == s && dep[k] == 2 && /^ - /) {
|
|
1763
|
-
|
|
1764
|
-
|
|
1765
|
-
gsub(/["\047]/, "", val)
|
|
1766
|
-
print s "\037" val
|
|
2248
|
+
got_s[++ng] = s
|
|
2249
|
+
got_v[ng] = yaml_item(substr($0, 7))
|
|
1767
2250
|
}
|
|
1768
2251
|
}
|
|
1769
2252
|
}
|
|
2253
|
+
END {
|
|
2254
|
+
m = ng + 0
|
|
2255
|
+
for (i = 1; i <= m; i++) {
|
|
2256
|
+
s = got_s[i]
|
|
2257
|
+
val = got_v[i]
|
|
2258
|
+
if (s != "rules" && s != "agents" && s != "skills") { print s "\037" val; continue }
|
|
2259
|
+
dir = pkr_expand(val)
|
|
2260
|
+
if (PKR_STATE == "path" || PKR_STATE == "ok") print s "\037" dir
|
|
2261
|
+
else print "!\037" s "\037" val "\037" PKR_STATE "\037" dir "\037" PKR_ALIAS "\037" PKR_HOLDERS
|
|
2262
|
+
}
|
|
2263
|
+
}
|
|
1770
2264
|
' "$file")"
|
|
1771
2265
|
for s in "$@"; do
|
|
1772
2266
|
j=""
|
|
@@ -1779,6 +2273,12 @@ load_yaml_lists() {
|
|
|
1779
2273
|
printf -v "IS_YL_${s}_FILE" '%s' "$file"
|
|
1780
2274
|
printf -v "IS_YL_${s}_VAL" '%s' "$j"
|
|
1781
2275
|
done
|
|
2276
|
+
IS_YL_UNRESOLVED=""
|
|
2277
|
+
while IFS= read -r line; do
|
|
2278
|
+
case "$line" in
|
|
2279
|
+
"!"$'\037'*) IS_YL_UNRESOLVED="$IS_YL_UNRESOLVED${line#"!"$'\037'}"$'\n' ;;
|
|
2280
|
+
esac
|
|
2281
|
+
done <<< "$out"
|
|
1782
2282
|
}
|
|
1783
2283
|
|
|
1784
2284
|
# load_targets_cache <file> — parse the whole targets: section once into the
|