@ainova-systems/intelligence 0.18.1 → 0.20.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/registry.sh +1 -1
- package/cli/commands/source.sh +56 -17
- package/cli/commands/sync.sh +78 -6
- package/cli/intelligence +16 -3
- package/cli/internal/check.sh +64 -13
- package/cli/internal/convert-legacy.sh +1 -1
- 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-search.sh +1 -1
- package/cli/internal/package-update.sh +14 -6
- package/cli/internal/restore.sh +3 -2
- package/cli/lib/adapter-lifecycle.sh +5 -2
- package/cli/lib/cli-common.sh +149 -34
- package/cli/lib/gitignore.sh +128 -34
- package/cli/lib/lockfile.sh +14 -4
- package/cli/lib/manifest.sh +315 -21
- package/cli/lib/qmap.awk +97 -32
- package/cli/lib/registry.sh +19 -14
- package/cli/lib/sync-cache.sh +596 -142
- package/cli/lib/sync-lock.sh +19 -2
- package/engine/ENGINE_SHA +1 -1
- package/engine/VERSION +1 -1
- package/engine/adapters/_template.sh +6 -2
- package/engine/adapters/agents.sh +8 -2
- package/engine/adapters/antigravity.sh +3 -1
- package/engine/adapters/claude.sh +29 -12
- package/engine/adapters/codex.sh +11 -13
- package/engine/adapters/copilot.sh +54 -4
- package/engine/adapters/cursor.sh +13 -2
- package/engine/adapters/opencode.sh +7 -1
- package/engine/adapters/pi.sh +3 -1
- package/engine/lib/adapter-contract.sh +51 -8
- package/engine/lib/common.sh +1072 -160
- package/engine/lib/contract.sh +5 -0
- package/engine/sync.sh +128 -28
- package/package.json +1 -1
- package/packages/sync/references/adapters.md +44 -9
- package/packages/sync/references/conventions.md +48 -8
- 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.
|
|
@@ -451,8 +452,11 @@ copy_md_with_quoted_frontmatter() {
|
|
|
451
452
|
# ship support files beside SKILL.md, and SKILL.md bodies point at them by
|
|
452
453
|
# relative path, so dropping them breaks the skill at runtime). Markdown is
|
|
453
454
|
# normalized to LF; every other file is copied byte-for-byte so potentially
|
|
454
|
-
# binary assets survive.
|
|
455
|
-
#
|
|
455
|
+
# binary assets survive. A symlink never reaches the output: the plan
|
|
456
|
+
# skill_source_inventory made turns a link inside its source into the regular
|
|
457
|
+
# file or directory it points at and leaves any other out, and the flush
|
|
458
|
+
# removes a link no plan covered (a copy from outside every skills source), so
|
|
459
|
+
# no host file outside the source is ever copied in (decision 0017).
|
|
456
460
|
#
|
|
457
461
|
# SKILL.md frontmatter is quoted for EVERY consumer, not only the strict-YAML
|
|
458
462
|
# ones. `argument-hint: [pr-number]` is a YAML *flow sequence*, so an unquoted
|
|
@@ -460,35 +464,62 @@ copy_md_with_quoted_frontmatter() {
|
|
|
460
464
|
# "argument-hint must be a string" — the skill silently disappears from the
|
|
461
465
|
# picker. Quoting is idempotent: an already-quoted value passes through
|
|
462
466
|
# untouched.
|
|
467
|
+
#
|
|
468
|
+
# The same pass renders SKILL.md's `effort:` for the tool that reads the copy
|
|
469
|
+
# (map_effort_var): the first `effort:` line becomes the tool's level, and is
|
|
470
|
+
# dropped — with every later one — when the tool has no level for it. The
|
|
471
|
+
# plain forms render for the shared `open` tree, which keeps a neutral level as
|
|
472
|
+
# written and drops only an empty or off-scale one.
|
|
463
473
|
# Usage: copy_skill_bundle "src/skill/dir" "dest/skill/dir"
|
|
464
474
|
copy_skill_bundle() {
|
|
465
475
|
_skill_bundles_reset
|
|
466
|
-
_skill_bundle_stage "$1" "$2"
|
|
467
|
-
_skill_bundles_flush
|
|
476
|
+
_skill_bundle_stage "$1" "$2" || return 1
|
|
477
|
+
_skill_bundles_flush open
|
|
468
478
|
}
|
|
469
479
|
|
|
470
480
|
# copy_skill_bundle_dirs <dest_root> <src_dir>... — batch form: ONE cp -R
|
|
471
481
|
# copies every source skill directory into <dest_root>/<skill-name>, then one
|
|
472
482
|
# awk pass quotes and finalizes every bundled markdown file across all
|
|
473
483
|
# bundles. A later source with the same skill name overwrites file-by-file in
|
|
474
|
-
# order, exactly like the sequential per-bundle copies did.
|
|
484
|
+
# order, exactly like the sequential per-bundle copies did. A skill with links
|
|
485
|
+
# to resolve is copied from its materialized stage, in the same order.
|
|
475
486
|
copy_skill_bundle_dirs() {
|
|
476
|
-
|
|
477
|
-
|
|
487
|
+
copy_skill_bundle_dirs_for open "$@"
|
|
488
|
+
}
|
|
489
|
+
|
|
490
|
+
# copy_skill_bundle_dirs_for <tool> <dest_root> <src_dir>... — the batch form
|
|
491
|
+
# for a tree one tool reads, rendering `effort:` for that tool.
|
|
492
|
+
copy_skill_bundle_dirs_for() {
|
|
493
|
+
local effort_tool="$1" dest_root="$2"
|
|
494
|
+
shift 2
|
|
478
495
|
[ "$#" -gt 0 ] || return 0
|
|
479
|
-
local src dest
|
|
496
|
+
local src dest linked=0
|
|
480
497
|
local -a srcs=()
|
|
481
498
|
for src in "$@"; do
|
|
482
|
-
|
|
499
|
+
src="${src%/}"
|
|
500
|
+
srcs+=("$src")
|
|
501
|
+
! _ski_planned "$src" || linked=1
|
|
483
502
|
done
|
|
484
503
|
mkdir -p "$dest_root"
|
|
485
|
-
|
|
504
|
+
if [ "$linked" = 0 ]; then
|
|
505
|
+
cp -R "${srcs[@]}" "$dest_root/"
|
|
506
|
+
else
|
|
507
|
+
for src in "${srcs[@]}"; do
|
|
508
|
+
if _ski_planned "$src"; then
|
|
509
|
+
_skill_bundle_materialize "$src" || return 1
|
|
510
|
+
cp -R "$IS_SKI_STAGED" "$dest_root/" || return 1
|
|
511
|
+
rm -rf "${IS_SKI_STAGED%/*}"
|
|
512
|
+
else
|
|
513
|
+
cp -R "$src" "$dest_root/" || return 1
|
|
514
|
+
fi
|
|
515
|
+
done
|
|
516
|
+
fi
|
|
486
517
|
_skill_bundles_reset
|
|
487
518
|
for src in "${srcs[@]}"; do
|
|
488
519
|
dest="$dest_root/${src##*/}"
|
|
489
520
|
_skill_bundle_note "$dest"
|
|
490
521
|
done
|
|
491
|
-
_skill_bundles_flush
|
|
522
|
+
_skill_bundles_flush "$effort_tool"
|
|
492
523
|
}
|
|
493
524
|
|
|
494
525
|
_skill_bundles_reset() {
|
|
@@ -500,24 +531,63 @@ _skill_bundles_reset() {
|
|
|
500
531
|
_skill_bundle_stage() {
|
|
501
532
|
local src="${1%/}" dest="$2"
|
|
502
533
|
mkdir -p "$dest"
|
|
503
|
-
|
|
534
|
+
if _ski_planned "$src"; then
|
|
535
|
+
_skill_bundle_materialize "$src" || return 1
|
|
536
|
+
cp -R "$IS_SKI_STAGED/." "$dest/" || return 1
|
|
537
|
+
rm -rf "${IS_SKI_STAGED%/*}"
|
|
538
|
+
else
|
|
539
|
+
cp -R "$src/." "$dest/"
|
|
540
|
+
fi
|
|
504
541
|
_skill_bundle_note "$dest"
|
|
505
542
|
}
|
|
506
543
|
|
|
544
|
+
# _skill_bundle_materialize <skill_dir> — copy a skill whose plan holds links
|
|
545
|
+
# into a fresh stage and apply the plan there, parents first: each planned link
|
|
546
|
+
# is replaced by a copy of the file or directory it resolves to inside its
|
|
547
|
+
# source, or removed. Sets IS_SKI_STAGED to <stage>/<name>; the caller copies
|
|
548
|
+
# it on and removes the stage. Spawns per link, never per file — a source
|
|
549
|
+
# without links never comes here.
|
|
550
|
+
_skill_bundle_materialize() {
|
|
551
|
+
local src="$1" key stage out rec rel act phys first=1
|
|
552
|
+
_ski_key_var "$src"
|
|
553
|
+
key="$_SKI_KEY"
|
|
554
|
+
stage="$(mktemp -d -t intelligence-skill-XXXXXX)" || return 1
|
|
555
|
+
out="$stage/${key##*/}"
|
|
556
|
+
while IFS= read -r rec; do
|
|
557
|
+
case "$rec" in
|
|
558
|
+
"$key$IS_SKI_US"*) ;;
|
|
559
|
+
*) continue ;;
|
|
560
|
+
esac
|
|
561
|
+
rec="${rec#"$key$IS_SKI_US"}"
|
|
562
|
+
rel="${rec%%"$IS_SKI_US"*}"
|
|
563
|
+
rec="${rec#*"$IS_SKI_US"}"
|
|
564
|
+
act="${rec%%"$IS_SKI_US"*}"
|
|
565
|
+
phys="${rec#*"$IS_SKI_US"}"
|
|
566
|
+
if [ "$first" = 1 ]; then
|
|
567
|
+
first=0
|
|
568
|
+
if [ -z "$rel" ]; then
|
|
569
|
+
# The skill directory itself is the link.
|
|
570
|
+
mkdir "$out" && cp -R "$phys/." "$out/" || return 1
|
|
571
|
+
continue
|
|
572
|
+
fi
|
|
573
|
+
cp -R "$src" "$stage/" || return 1
|
|
574
|
+
fi
|
|
575
|
+
# The copy holds the link itself here; `rm -f` never follows it and
|
|
576
|
+
# refuses a directory, so a plan that no longer matches stops the run.
|
|
577
|
+
rm -f "$out/$rel" || return 1
|
|
578
|
+
case "$act" in
|
|
579
|
+
f) cp "$phys" "$out/$rel" || return 1 ;;
|
|
580
|
+
d) mkdir "$out/$rel" && cp -R "$phys/." "$out/$rel/" || return 1 ;;
|
|
581
|
+
esac
|
|
582
|
+
done <<< "$IS_SKI_PLAN"
|
|
583
|
+
IS_SKI_STAGED="$out"
|
|
584
|
+
}
|
|
585
|
+
|
|
507
586
|
# Record a staged bundle for the flush pass: mark its SKILL.md for
|
|
508
587
|
# frontmatter quoting and deduplicate the destination.
|
|
509
588
|
_skill_bundle_note() {
|
|
510
589
|
local dest="$1"
|
|
511
|
-
|
|
512
|
-
# symlink. `[ -f ]` follows links, so quoting it would read the link's
|
|
513
|
-
# TARGET and write that content into a real file, turning
|
|
514
|
-
# `skills/x/SKILL.md -> /etc/…` into a copy of a host file inside the
|
|
515
|
-
# output. That is the leak the symlink-preserving copy exists to
|
|
516
|
-
# prevent, so skip the rewrite and say so (the same reason
|
|
517
|
-
# `find -type f` in the flush never matches a symlink).
|
|
518
|
-
if [ -L "$dest/SKILL.md" ]; then
|
|
519
|
-
echo " WARN: ${dest##*/}/SKILL.md is a symlink — emitted as-is (frontmatter not quoted, tokens not expanded)" >&2
|
|
520
|
-
elif [ -f "$dest/SKILL.md" ]; then
|
|
590
|
+
if [ -f "$dest/SKILL.md" ]; then
|
|
521
591
|
_SB_QUOTE_LIST="$_SB_QUOTE_LIST$dest/SKILL.md"$'\n'
|
|
522
592
|
fi
|
|
523
593
|
# The same skill name from a later source overwrites the earlier copy;
|
|
@@ -531,27 +601,51 @@ _skill_bundle_note() {
|
|
|
531
601
|
esac
|
|
532
602
|
}
|
|
533
603
|
|
|
604
|
+
# _skill_bundles_flush <effort-tool>
|
|
534
605
|
_skill_bundles_flush() {
|
|
535
606
|
[ "${#_SB_DESTS[@]}" -gt 0 ] || return 0
|
|
536
|
-
local -a mds=()
|
|
537
|
-
local f quote_list="$_SB_QUOTE_LIST"
|
|
607
|
+
local -a mds=() links=()
|
|
608
|
+
local f listing quote_list="$_SB_QUOTE_LIST"
|
|
609
|
+
# Captured, not streamed: a listing cut short could leave a link behind.
|
|
610
|
+
listing="$(find "${_SB_DESTS[@]}" \( -type l -o -type f -name '*.md' \) -print)" || {
|
|
611
|
+
echo "ERROR: could not list the skill copies under ${_SB_DESTS[0]%/*}" >&2
|
|
612
|
+
return 1
|
|
613
|
+
}
|
|
538
614
|
while IFS= read -r f; do
|
|
539
|
-
[ -n "$f" ]
|
|
540
|
-
|
|
615
|
+
[ -n "$f" ] || continue
|
|
616
|
+
if [ -L "$f" ]; then links+=("$f"); else mds+=("$f"); fi
|
|
617
|
+
done <<< "$listing"
|
|
618
|
+
# A link only a copy from outside every skills source can bring: no plan
|
|
619
|
+
# resolved it, so it is not followed but removed.
|
|
620
|
+
if [ "${#links[@]}" -gt 0 ]; then
|
|
621
|
+
rm -f "${links[@]}" || return 1
|
|
622
|
+
for f in "${links[@]}"; do
|
|
623
|
+
echo "WARNING: ${f#"${REPO_ROOT:-}"/} was copied as a symlink from outside every skills source — removed; generated output never holds one" >&2
|
|
624
|
+
done
|
|
625
|
+
fi
|
|
541
626
|
_skill_bundles_reset
|
|
542
627
|
[ "${#mds[@]}" -gt 0 ] || return 0
|
|
628
|
+
effort_map_var "$1"
|
|
543
629
|
# One awk: quote free-text frontmatter fields in each top-level SKILL.md
|
|
544
630
|
# (strict-YAML consumers reject unquoted colons; `argument-hint:
|
|
545
631
|
# [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
|
|
632
|
+
# skill silently vanishes from the picker), render its `effort:` for the
|
|
633
|
+
# tool, and expand layout tokens in every bundled markdown file. Quoting is
|
|
634
|
+
# idempotent — already-quoted values pass through untouched. The quote list
|
|
635
|
+
# and the effort map travel through the environment, like
|
|
636
|
+
# emit_wrapped_bodies specs. The effort key is matched exactly as
|
|
637
|
+
# frontmatter_index reads it, so the line rewritten is the one the
|
|
638
|
+
# lint judged.
|
|
550
639
|
is_fin_awk_vars
|
|
551
|
-
IS_QUOTE_LIST="$quote_list" awk "${IS_FIN_V[@]}" "$IS_AWK_LIB"'
|
|
640
|
+
IS_QUOTE_LIST="$quote_list" IS_EFFORT_MAP="$IS_EFFORT_MAP" awk "${IS_FIN_V[@]}" "$IS_AWK_LIB"'
|
|
552
641
|
BEGIN {
|
|
553
642
|
qn = split(ENVIRON["IS_QUOTE_LIST"], QL, "\n")
|
|
554
643
|
for (qi = 1; qi <= qn; qi++) if (QL[qi] != "") QUOTE[QL[qi]] = 1
|
|
644
|
+
en = split(ENVIRON["IS_EFFORT_MAP"], EL, "\n")
|
|
645
|
+
for (ei = 1; ei <= en; ei++) {
|
|
646
|
+
eq = index(EL[ei], "=")
|
|
647
|
+
if (eq > 0) EFFORT[substr(EL[ei], 1, eq - 1)] = substr(EL[ei], eq + 1)
|
|
648
|
+
}
|
|
555
649
|
}
|
|
556
650
|
function yamlq(s, out, i, c) {
|
|
557
651
|
out = ""
|
|
@@ -572,12 +666,19 @@ _skill_bundles_flush() {
|
|
|
572
666
|
flush_file()
|
|
573
667
|
out_file = FILENAME; line_n = 0
|
|
574
668
|
state = (FILENAME in QUOTE) ? "before" : ""
|
|
669
|
+
effort_seen = 0
|
|
575
670
|
}
|
|
576
671
|
{ sub(/\r$/, "") }
|
|
577
672
|
state == "before" {
|
|
578
673
|
if (FNR == 1 && $0 == "---") state = "in_fm"
|
|
579
674
|
else state = "after"
|
|
580
675
|
}
|
|
676
|
+
state == "in_fm" && FNR > 1 && substr($0, 1, 7) == "effort:" {
|
|
677
|
+
if (effort_seen++) next
|
|
678
|
+
val = fm_value_strip(substr($0, 8))
|
|
679
|
+
if (!(val in EFFORT)) next
|
|
680
|
+
if (EFFORT[val] != val) $0 = "effort: " EFFORT[val]
|
|
681
|
+
}
|
|
581
682
|
state == "in_fm" && FNR > 1 {
|
|
582
683
|
if ($0 == "---") state = "after"
|
|
583
684
|
else {
|
|
@@ -660,7 +761,7 @@ sync_open_skill_dirs() {
|
|
|
660
761
|
# copy_skill_bundle_dirs owns the frontmatter-quoting pass, so every
|
|
661
762
|
# target gets it — not just this open-standard dir.
|
|
662
763
|
if [ "$count" -gt 0 ]; then
|
|
663
|
-
|
|
764
|
+
copy_skill_bundle_dirs_for open "$output_dir" "${skill_dirs[@]}"
|
|
664
765
|
open_skill_invocation_policies "$output_dir" || return 1
|
|
665
766
|
fi
|
|
666
767
|
|
|
@@ -681,19 +782,15 @@ sync_open_skill_dirs() {
|
|
|
681
782
|
# when the author's file sets none, and a file whose policy says otherwise, or
|
|
682
783
|
# that sync cannot read as saying it, refuses the render — an owner-only skill
|
|
683
784
|
# Codex can still select is the exact failure the field exists to prevent, so
|
|
684
|
-
# it never passes as a success.
|
|
785
|
+
# it never passes as a success. The tree holds no symlink — copy_skill_bundle_dirs
|
|
786
|
+
# renders a link inside its source as a regular file and leaves any other out —
|
|
787
|
+
# so a skill whose SKILL.md or agents/openai.yaml is a link in its source is
|
|
788
|
+
# read and enforced like any other.
|
|
685
789
|
open_skill_invocation_policies() {
|
|
686
790
|
local output_dir="$1" path dir flag rows refusals
|
|
687
791
|
local -a skill_mds=() policy_dirs=() policies=() authored=()
|
|
688
792
|
for path in "$output_dir"/*/SKILL.md; do
|
|
689
|
-
|
|
690
|
-
# A link is emitted as-is (see _skill_bundle_note): reading through it
|
|
691
|
-
# or writing beside it would reach outside the output tree.
|
|
692
|
-
if [ -L "$dir" ] || [ -L "$path" ]; then
|
|
693
|
-
echo " WARN: ${dir##*/}/SKILL.md is reached through a symlink — Codex gets no invocation policy derived for it" >&2
|
|
694
|
-
elif [ -f "$path" ]; then
|
|
695
|
-
skill_mds+=("$path")
|
|
696
|
-
fi
|
|
793
|
+
[ ! -f "$path" ] || skill_mds+=("$path")
|
|
697
794
|
done
|
|
698
795
|
[ "${#skill_mds[@]}" -gt 0 ] || return 0
|
|
699
796
|
# Captured, not streamed: a reader cut short must fail the render rather
|
|
@@ -706,10 +803,6 @@ open_skill_invocation_policies() {
|
|
|
706
803
|
[ "$flag" = "true" ] || continue
|
|
707
804
|
dir="${path%/SKILL.md}"
|
|
708
805
|
path="$dir/agents"
|
|
709
|
-
if [ -L "$path" ] || [ -L "$path/openai.yaml" ]; then
|
|
710
|
-
echo " WARN: ${dir##*/}/agents/openai.yaml is a symlink — left as-is, its invocation policy is not enforced" >&2
|
|
711
|
-
continue
|
|
712
|
-
fi
|
|
713
806
|
if [ -s "$path/openai.yaml" ]; then
|
|
714
807
|
authored+=("$path/openai.yaml")
|
|
715
808
|
continue
|
|
@@ -830,19 +923,41 @@ enforce_authored_invocation_policies() {
|
|
|
830
923
|
# Lint YAML frontmatter for common pitfalls (unquoted colons, leading tabs).
|
|
831
924
|
# Print warnings to stderr; do not fail. Strict consumers (Codex CLI) reject
|
|
832
925
|
# these files with cryptic messages — catching them in sync gives better DX.
|
|
926
|
+
# Each finding is a `WARNING:` line: the author has to edit the source, so
|
|
927
|
+
# `sync --compact` (the first sync `init` runs) must keep it.
|
|
833
928
|
# Batched: one awk process lints every file passed.
|
|
929
|
+
# The engine lints every source once, so this is also where an `effort:` off
|
|
930
|
+
# IS_EFFORT_LEVELS is reported — read with frontmatter_index's semantics,
|
|
931
|
+
# first occurrence only. Adapters render it as absent and say nothing, so the
|
|
932
|
+
# warning names the source exactly once however many tools render it. Unlike
|
|
933
|
+
# the lint notes it is an unindented `WARNING:` line, because `sync --compact`
|
|
934
|
+
# (and so `init`) keeps only those (decision 0015).
|
|
834
935
|
# Usage: lint_frontmatter_files "a.md" "b.md" ...
|
|
835
936
|
lint_frontmatter_files() {
|
|
836
937
|
[ "$#" -gt 0 ] || return 0
|
|
837
|
-
awk '
|
|
838
|
-
|
|
938
|
+
awk -v levels="$IS_EFFORT_LEVELS" "$IS_AWK_LIB"'
|
|
939
|
+
BEGIN {
|
|
940
|
+
nl = split(levels, LV, " ")
|
|
941
|
+
allowed = ""
|
|
942
|
+
for (li = 1; li <= nl; li++) {
|
|
943
|
+
LEVEL[LV[li]] = 1
|
|
944
|
+
allowed = allowed (li > 1 ? ", " : "") LV[li]
|
|
945
|
+
}
|
|
946
|
+
}
|
|
947
|
+
FNR == 1 { in_fm = 0; done = 0; effort_seen = 0 }
|
|
839
948
|
{ sub(/\r$/, "") }
|
|
840
949
|
done { next }
|
|
841
950
|
FNR == 1 && $0 != "---" { done = 1; next }
|
|
842
951
|
FNR == 1 { in_fm = 1; next }
|
|
843
952
|
in_fm && $0 == "---" { done = 1; next }
|
|
953
|
+
in_fm && substr($0, 1, 7) == "effort:" && !effort_seen++ {
|
|
954
|
+
effort = fm_value_strip(substr($0, 8))
|
|
955
|
+
if (effort != "" && !(effort in LEVEL)) {
|
|
956
|
+
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"
|
|
957
|
+
}
|
|
958
|
+
}
|
|
844
959
|
in_fm && /^\t/ {
|
|
845
|
-
printf "
|
|
960
|
+
printf "WARNING: %s:%d leading tab in frontmatter (use spaces)\n", FILENAME, FNR > "/dev/stderr"
|
|
846
961
|
}
|
|
847
962
|
in_fm && /^[a-zA-Z0-9_-]+:[[:space:]]+[^"\047|>[{]/ {
|
|
848
963
|
value_start = index($0, ":") + 1
|
|
@@ -850,10 +965,10 @@ lint_frontmatter_files() {
|
|
|
850
965
|
sub(/^[[:space:]]+/, "", value)
|
|
851
966
|
if (value ~ /:[[:space:]]/ || value ~ /:$/) {
|
|
852
967
|
col = index(value, ":") + value_start
|
|
853
|
-
printf "
|
|
968
|
+
printf "WARNING: %s:%d unquoted colon in value at column %d — wrap value in quotes\n", FILENAME, FNR, col > "/dev/stderr"
|
|
854
969
|
}
|
|
855
970
|
if (value ~ /"/) {
|
|
856
|
-
printf "
|
|
971
|
+
printf "WARNING: %s:%d literal double quote in unquoted value — wrap value in single quotes or escape as \\\" so strict-YAML targets accept it\n", FILENAME, FNR > "/dev/stderr"
|
|
857
972
|
}
|
|
858
973
|
}
|
|
859
974
|
# Field-length limits. Both Claude Code and the Agent Skills standard
|
|
@@ -872,7 +987,7 @@ lint_frontmatter_files() {
|
|
|
872
987
|
}
|
|
873
988
|
limit = (key == "name") ? 64 : 1024
|
|
874
989
|
if (length(val) > limit) {
|
|
875
|
-
printf "
|
|
990
|
+
printf "WARNING: %s:%d %s is %d chars — over the %d-char limit; the skill/agent will be REJECTED at load time\n", FILENAME, FNR, key, length(val), limit > "/dev/stderr"
|
|
876
991
|
}
|
|
877
992
|
}
|
|
878
993
|
' "$@"
|
|
@@ -994,8 +1109,8 @@ get_model_default_var() {
|
|
|
994
1109
|
cursor:heavy) IS_MODEL_DEFAULT="inherit" ;;
|
|
995
1110
|
cursor:standard) IS_MODEL_DEFAULT="inherit" ;;
|
|
996
1111
|
cursor:light) IS_MODEL_DEFAULT="inherit" ;;
|
|
997
|
-
# GPT-6 has no mid model, so frontier and heavy share Astra
|
|
998
|
-
#
|
|
1112
|
+
# GPT-6 has no mid model, so frontier and heavy share Astra and render
|
|
1113
|
+
# identically; reasoning effort comes from `effort:`, never the tier.
|
|
999
1114
|
copilot:frontier) IS_MODEL_DEFAULT="gpt-6-astra" ;;
|
|
1000
1115
|
copilot:heavy) IS_MODEL_DEFAULT="gpt-6-astra" ;;
|
|
1001
1116
|
copilot:standard) IS_MODEL_DEFAULT="gpt-6.1-sol" ;;
|
|
@@ -1132,7 +1247,7 @@ load_model_tiers() {
|
|
|
1132
1247
|
# the tool has no default for and the manifest overrides nowhere — a typo, as a
|
|
1133
1248
|
# rule — still renders an empty model, but never silently: the run warns once
|
|
1134
1249
|
# per tool and tier, unindented on purpose because `sync --compact` keeps only
|
|
1135
|
-
# `WARNING:` lines.
|
|
1250
|
+
# `WARNING:` lines (decision 0016).
|
|
1136
1251
|
# shellcheck disable=SC2034 # IS_MODEL is the return channel read by adapters
|
|
1137
1252
|
resolve_model_var() {
|
|
1138
1253
|
case "$1" in
|
|
@@ -1242,6 +1357,64 @@ map_access_to_claude_disallowed() {
|
|
|
1242
1357
|
echo "$IS_CLAUDE_DISALLOWED"
|
|
1243
1358
|
}
|
|
1244
1359
|
|
|
1360
|
+
# --- Effort Mapping ---
|
|
1361
|
+
|
|
1362
|
+
# `effort:` is the tool-neutral reasoning effort an agent or skill asks for,
|
|
1363
|
+
# lowest first (decision 0015). It is independent of `tier`, which selects the
|
|
1364
|
+
# model only. A value off this scale — matched exactly, case included — is an
|
|
1365
|
+
# absent effort: lint_frontmatter_files warns about it and no tool receives
|
|
1366
|
+
# one, because a typo in another source's package must not block a sync.
|
|
1367
|
+
IS_EFFORT_LEVELS="low medium high xhigh max ultra"
|
|
1368
|
+
|
|
1369
|
+
# effort_levels_var <tool> — set IS_EFFORT_TOOL_LEVELS to the levels the tool's
|
|
1370
|
+
# native effort field accepts, in scale order; empty for a tool without one.
|
|
1371
|
+
# `open` is the shared Agent Skills tree (.agents/skills/), which keeps the
|
|
1372
|
+
# neutral value for the tools that read it.
|
|
1373
|
+
effort_levels_var() {
|
|
1374
|
+
case "$1" in
|
|
1375
|
+
claude) IS_EFFORT_TOOL_LEVELS="low medium high xhigh max" ;;
|
|
1376
|
+
codex|open) IS_EFFORT_TOOL_LEVELS="$IS_EFFORT_LEVELS" ;;
|
|
1377
|
+
# Copilot, Cursor, OpenCode, Antigravity and Pi have no per-agent
|
|
1378
|
+
# effort field: the tool's own setting applies.
|
|
1379
|
+
*) IS_EFFORT_TOOL_LEVELS="" ;;
|
|
1380
|
+
esac
|
|
1381
|
+
}
|
|
1382
|
+
|
|
1383
|
+
# map_effort_var <tool> <effort> — set IS_EFFORT to the level the tool receives:
|
|
1384
|
+
# the same level, or the nearest lower one the tool has (`ultra` is `max` for
|
|
1385
|
+
# Claude Code). Empty when the effort is absent or off the scale, or the tool
|
|
1386
|
+
# has no field. Which levels a particular model supports is the tool's call: a
|
|
1387
|
+
# manifest may override the model, and the tools fall back on their own.
|
|
1388
|
+
map_effort_var() {
|
|
1389
|
+
local level best=""
|
|
1390
|
+
IS_EFFORT=""
|
|
1391
|
+
effort_levels_var "$1"
|
|
1392
|
+
for level in $IS_EFFORT_LEVELS; do
|
|
1393
|
+
case " $IS_EFFORT_TOOL_LEVELS " in *" $level "*) best="$level" ;; esac
|
|
1394
|
+
if [ "$level" = "${2-}" ]; then
|
|
1395
|
+
IS_EFFORT="$best"
|
|
1396
|
+
return 0
|
|
1397
|
+
fi
|
|
1398
|
+
done
|
|
1399
|
+
}
|
|
1400
|
+
|
|
1401
|
+
map_effort() {
|
|
1402
|
+
map_effort_var "$1" "$2"
|
|
1403
|
+
echo "$IS_EFFORT"
|
|
1404
|
+
}
|
|
1405
|
+
|
|
1406
|
+
# effort_map_var <tool> — set IS_EFFORT_MAP to one `<neutral>=<native>` line per
|
|
1407
|
+
# level the tool maps, so a batched awk pass rewrites `effort:` in place with
|
|
1408
|
+
# map_effort_var's answers instead of a second copy of the mapping.
|
|
1409
|
+
effort_map_var() {
|
|
1410
|
+
local level map=""
|
|
1411
|
+
for level in $IS_EFFORT_LEVELS; do
|
|
1412
|
+
map_effort_var "$1" "$level"
|
|
1413
|
+
[ -z "$IS_EFFORT" ] || map+="$level=$IS_EFFORT"$'\n'
|
|
1414
|
+
done
|
|
1415
|
+
IS_EFFORT_MAP="$map"
|
|
1416
|
+
}
|
|
1417
|
+
|
|
1245
1418
|
# --- Validation ---
|
|
1246
1419
|
|
|
1247
1420
|
# Lexically canonicalize a path: collapse `//`, `.` and `..` by pure string
|
|
@@ -1453,7 +1626,8 @@ warn_unsynced() {
|
|
|
1453
1626
|
load_yaml_list "$config_file" "$section"
|
|
1454
1627
|
while IFS= read -r src; do
|
|
1455
1628
|
[ -z "$src" ] && continue
|
|
1456
|
-
|
|
1629
|
+
normalize_path_var "$repo_root/$src"
|
|
1630
|
+
all_sources+=("${IS_NORM_PATH#"$repo_root/"}")
|
|
1457
1631
|
done <<< "$IS_YAML_LIST"
|
|
1458
1632
|
done
|
|
1459
1633
|
|
|
@@ -1547,11 +1721,7 @@ warn_unsynced() {
|
|
|
1547
1721
|
done
|
|
1548
1722
|
|
|
1549
1723
|
if [ "$matched" = false ]; then
|
|
1550
|
-
|
|
1551
|
-
echo ""
|
|
1552
|
-
echo "=== WARNING: Unsynced directories ==="
|
|
1553
|
-
fi
|
|
1554
|
-
echo " NOT SYNCED: $rel_dir"
|
|
1724
|
+
echo "WARNING: NOT SYNCED: $rel_dir" >&2
|
|
1555
1725
|
warnings=$((warnings + 1))
|
|
1556
1726
|
fi
|
|
1557
1727
|
done < <(find "$repo_root" \( -name ".git" -o -name "node_modules" -o -name "vendor" -o -name "dist" -o -name ".claude" -o -name ".cursor" -o -name ".github" -o -name ".codex" -o -name ".agents" -o -name ".intelligence" \) -prune -o -type d \( -name "rules" -o -name "agents" -o -name "skills" -o -name "Rules" -o -name "Agents" -o -name "Skills" \) -print 2>/dev/null)
|
|
@@ -1562,10 +1732,363 @@ warn_unsynced() {
|
|
|
1562
1732
|
# and hand-placing an entry in an ordered list decides which artifact
|
|
1563
1733
|
# wins. The engine may also run without the CLI around it.
|
|
1564
1734
|
if [ "${IS_CLI:-0}" = "1" ]; then
|
|
1565
|
-
echo "
|
|
1735
|
+
echo "WARNING: Wire one in: intelligence source add <rules|agents|skills> <path>" >&2
|
|
1566
1736
|
else
|
|
1567
|
-
echo "
|
|
1737
|
+
echo "WARNING: Add these paths to sources: in ${config_file##*/}" >&2
|
|
1738
|
+
fi
|
|
1739
|
+
fi
|
|
1740
|
+
}
|
|
1741
|
+
|
|
1742
|
+
# --- Skill sources and the symlinks inside them (decision 0017) ---
|
|
1743
|
+
#
|
|
1744
|
+
# A symlink in a skills source renders as the regular file or directory it
|
|
1745
|
+
# resolves to while that stays inside the same source, and is left out with a
|
|
1746
|
+
# WARNING: line when it resolves outside it, nowhere, or to a directory that
|
|
1747
|
+
# encloses it. Generated output never holds a link, and nothing is ever copied
|
|
1748
|
+
# from outside the source (decision 0006). Every consumer takes that decision
|
|
1749
|
+
# from here, once per run: the skill list read_source_artifact_files returns,
|
|
1750
|
+
# the membership the glob-listing adapters check (skill_source_rendered), and
|
|
1751
|
+
# the plan copy_skill_bundle_dirs applies to its copy. A skill rendered from a
|
|
1752
|
+
# link is an ordinary skill from then on — quoting, token expansion, the
|
|
1753
|
+
# effort rendering and the Codex invocation policy all run on the output copy.
|
|
1754
|
+
#
|
|
1755
|
+
# skill_source_inventory <repo_root> <config_file> fills, in the caller's
|
|
1756
|
+
# shell and fail-closed like read_source_artifact_files:
|
|
1757
|
+
# IS_SKI_LIST <source>/<skill>/SKILL.md of every rendered skill, one a line,
|
|
1758
|
+
# sources in manifest order, names in byte order;
|
|
1759
|
+
# IS_SKI_KEYS "\n<source>/<skill>\n…", the same skills as a membership set;
|
|
1760
|
+
# IS_SKI_PLAN "\n" + one record a line for every link in a rendered skill,
|
|
1761
|
+
# parents first: <source>/<skill> US <path in the skill copy>
|
|
1762
|
+
# US f|d|x US <physical target>. f and d materialize the file or
|
|
1763
|
+
# directory at that path, x leaves the link out; an empty path
|
|
1764
|
+
# is the skill directory itself.
|
|
1765
|
+
# The resolution spawns processes only for a source that holds a link, and
|
|
1766
|
+
# then per source and per link-chain hop, never per file.
|
|
1767
|
+
IS_SKI_US=$'\x1f'
|
|
1768
|
+
|
|
1769
|
+
skill_source_inventory() {
|
|
1770
|
+
local repo_root="$1" config_file="$2" memo_key src dir listing
|
|
1771
|
+
memo_key="$repo_root"$'\n'"$config_file"
|
|
1772
|
+
if [ "${IS_SOURCE_FILES_MEMO:-0}" = 1 ] && [ "${IS_SKI_KEY:-}" = "$memo_key" ]; then
|
|
1773
|
+
return 0
|
|
1774
|
+
fi
|
|
1775
|
+
IS_SKI_KEY=""
|
|
1776
|
+
IS_SKI_LIST=""
|
|
1777
|
+
IS_SKI_KEYS=$'\n'
|
|
1778
|
+
IS_SKI_PLAN=$'\n'
|
|
1779
|
+
load_yaml_list "$config_file" "skills"
|
|
1780
|
+
while IFS= read -r src; do
|
|
1781
|
+
[ -n "$src" ] || continue
|
|
1782
|
+
dir="$repo_root/$src"
|
|
1783
|
+
while [ "${dir%/}" != "$dir" ]; do dir="${dir%/}"; done
|
|
1784
|
+
[ -d "$dir" ] || continue
|
|
1785
|
+
# Every directory and link at any depth: the top level holds the
|
|
1786
|
+
# skills, and a link anywhere below one is part of its bundle. -H
|
|
1787
|
+
# lists a source that is itself a link through it, as a glob does.
|
|
1788
|
+
# Command substitution, so find or sort failing stops the run here
|
|
1789
|
+
# instead of yielding a short list.
|
|
1790
|
+
listing="$(find -H "$dir" -mindepth 1 \( -type d -o -type l \) -print | LC_ALL=C sort)" || {
|
|
1791
|
+
echo "ERROR: cannot enumerate skills under '$dir' — refusing to render a partial list." >&2
|
|
1792
|
+
exit 1
|
|
1793
|
+
}
|
|
1794
|
+
[ -n "$listing" ] || continue
|
|
1795
|
+
_ski_source "$repo_root" "$dir" "$listing"
|
|
1796
|
+
done <<< "$IS_YAML_LIST"
|
|
1797
|
+
[ "${IS_SOURCE_FILES_MEMO:-0}" != 1 ] || IS_SKI_KEY="$memo_key"
|
|
1798
|
+
}
|
|
1799
|
+
|
|
1800
|
+
# skill_source_rendered <skill_dir> — the inventory renders this skill. For
|
|
1801
|
+
# adapters that list a skills source with their own glob.
|
|
1802
|
+
skill_source_rendered() {
|
|
1803
|
+
_ski_key_var "$1"
|
|
1804
|
+
case "${IS_SKI_KEYS:-}" in
|
|
1805
|
+
*$'\n'"$_SKI_KEY"$'\n'*) return 0 ;;
|
|
1806
|
+
esac
|
|
1807
|
+
return 1
|
|
1808
|
+
}
|
|
1809
|
+
|
|
1810
|
+
# _ski_key_var <skill_dir> — the key a skill is listed and planned under, its
|
|
1811
|
+
# source and name joined by one slash however the caller spelled a trailing
|
|
1812
|
+
# slash on either.
|
|
1813
|
+
_ski_key_var() {
|
|
1814
|
+
local path="$1" parent
|
|
1815
|
+
while [ "${path%/}" != "$path" ]; do path="${path%/}"; done
|
|
1816
|
+
parent="${path%/*}"
|
|
1817
|
+
while [ -n "$parent" ] && [ "${parent%/}" != "$parent" ]; do parent="${parent%/}"; done
|
|
1818
|
+
_SKI_KEY="$parent/${path##*/}"
|
|
1819
|
+
}
|
|
1820
|
+
|
|
1821
|
+
# _ski_planned <skill_dir> — the skill has links the copy has to resolve.
|
|
1822
|
+
_ski_planned() {
|
|
1823
|
+
_ski_key_var "$1"
|
|
1824
|
+
case "${IS_SKI_PLAN:-}" in
|
|
1825
|
+
*$'\n'"$_SKI_KEY$IS_SKI_US"*) return 0 ;;
|
|
1826
|
+
esac
|
|
1827
|
+
return 1
|
|
1828
|
+
}
|
|
1829
|
+
|
|
1830
|
+
# _ski_source <repo_root> <source_dir> <listing> — inventory one skills source.
|
|
1831
|
+
_ski_source() {
|
|
1832
|
+
local repo_root="$1" f rel
|
|
1833
|
+
local -a tops=() links=()
|
|
1834
|
+
_SKI_DIR="$2"
|
|
1835
|
+
_SKI_ROOT_REL="${2#"$repo_root"/}"
|
|
1836
|
+
_SKI_REPO="$repo_root"
|
|
1837
|
+
while IFS= read -r f; do
|
|
1838
|
+
[ -n "$f" ] || continue
|
|
1839
|
+
rel="${f#"$_SKI_DIR"/}"
|
|
1840
|
+
case "$rel" in
|
|
1841
|
+
*/*) [ ! -L "$f" ] || links+=("$f") ;;
|
|
1842
|
+
*)
|
|
1843
|
+
tops+=("$f")
|
|
1844
|
+
[ ! -L "$f" ] || links+=("$f")
|
|
1845
|
+
;;
|
|
1846
|
+
esac
|
|
1847
|
+
done <<< "$3"
|
|
1848
|
+
_SKI_LREL=(); _SKI_LSTAT=(); _SKI_LKIND=(); _SKI_LTREL=(); _SKI_LPHYS=()
|
|
1849
|
+
[ "${#links[@]}" -eq 0 ] || _ski_classify_links "${links[@]}"
|
|
1850
|
+
for f in "${tops[@]+"${tops[@]}"}"; do
|
|
1851
|
+
_ski_skill "${f##*/}"
|
|
1852
|
+
done
|
|
1853
|
+
}
|
|
1854
|
+
|
|
1855
|
+
# _ski_classify_links <link>... — resolve every link of the current source and
|
|
1856
|
+
# record where it leads: ok (inside the source), outside, dangling, or other
|
|
1857
|
+
# (neither a file nor a directory). A link that cannot be resolved stops the
|
|
1858
|
+
# run, like any enumeration that cannot answer.
|
|
1859
|
+
_ski_classify_links() {
|
|
1860
|
+
local out root_phys rec kind phys i=0 link
|
|
1861
|
+
local -a recs=()
|
|
1862
|
+
out="$(_ski_physical "$_SKI_DIR" "$@")" || {
|
|
1863
|
+
echo "ERROR: cannot resolve the symlinks under '$_SKI_DIR' — refusing to render a partial list." >&2
|
|
1864
|
+
exit 1
|
|
1865
|
+
}
|
|
1866
|
+
while IFS= read -r rec; do recs+=("$rec"); done <<< "$out"
|
|
1867
|
+
if [ "${#recs[@]}" -ne $(($# + 1)) ]; then
|
|
1868
|
+
echo "ERROR: cannot resolve the symlinks under '$_SKI_DIR' — refusing to render a partial list." >&2
|
|
1869
|
+
exit 1
|
|
1870
|
+
fi
|
|
1871
|
+
root_phys="${recs[0]%/}"
|
|
1872
|
+
for link in "$@"; do
|
|
1873
|
+
rec="${recs[i + 1]}"
|
|
1874
|
+
kind="${rec%%"$IS_SKI_US"*}"
|
|
1875
|
+
phys="${rec#*"$IS_SKI_US"}"
|
|
1876
|
+
_SKI_LREL[i]="${link#"$_SKI_DIR"/}"
|
|
1877
|
+
_SKI_LKIND[i]="$kind"
|
|
1878
|
+
_SKI_LPHYS[i]="$phys"
|
|
1879
|
+
_SKI_LTREL[i]=""
|
|
1880
|
+
case "$kind" in
|
|
1881
|
+
d|f)
|
|
1882
|
+
case "$phys" in
|
|
1883
|
+
"$root_phys"/*) _SKI_LSTAT[i]=ok; _SKI_LTREL[i]="${phys#"$root_phys"/}" ;;
|
|
1884
|
+
"${root_phys:-/}") _SKI_LSTAT[i]=ok ;;
|
|
1885
|
+
*) _SKI_LSTAT[i]=outside ;;
|
|
1886
|
+
esac
|
|
1887
|
+
;;
|
|
1888
|
+
o) _SKI_LSTAT[i]=other ;;
|
|
1889
|
+
*) _SKI_LSTAT[i]=dangling ;;
|
|
1890
|
+
esac
|
|
1891
|
+
i=$((i + 1))
|
|
1892
|
+
done
|
|
1893
|
+
}
|
|
1894
|
+
|
|
1895
|
+
# _ski_physical <source_dir> <link>... — print the source's physical path,
|
|
1896
|
+
# then one "<kind> US <physical target>" line per link: d (directory), f
|
|
1897
|
+
# (regular file), o (anything else) or x (dangling, or a link loop). Runs in a
|
|
1898
|
+
# subshell so it can change directory: `cd -P` resolves a directory, and a
|
|
1899
|
+
# file's chain is read with one readlink per hop for all links at once (no
|
|
1900
|
+
# readlink -f on macOS).
|
|
1901
|
+
_ski_physical() (
|
|
1902
|
+
local dir="$1" link target targets i k n hops=0 parent
|
|
1903
|
+
local -a kind=() cur=() phys=() pend=() next=() paths=()
|
|
1904
|
+
shift
|
|
1905
|
+
n=$#
|
|
1906
|
+
cd -P -- "$dir" 2>/dev/null || exit 1
|
|
1907
|
+
printf '%s\n' "$PWD"
|
|
1908
|
+
i=0
|
|
1909
|
+
for link in "$@"; do
|
|
1910
|
+
kind[i]=x; phys[i]=""; cur[i]="$link"
|
|
1911
|
+
if [ -d "$link" ]; then
|
|
1912
|
+
cd -P -- "$link" 2>/dev/null || exit 1
|
|
1913
|
+
kind[i]=d; phys[i]="$PWD"
|
|
1914
|
+
elif [ -f "$link" ]; then
|
|
1915
|
+
kind[i]=f; pend+=("$i")
|
|
1916
|
+
elif [ -e "$link" ]; then
|
|
1917
|
+
kind[i]=o
|
|
1918
|
+
fi
|
|
1919
|
+
i=$((i + 1))
|
|
1920
|
+
done
|
|
1921
|
+
while [ "${#pend[@]}" -gt 0 ]; do
|
|
1922
|
+
hops=$((hops + 1))
|
|
1923
|
+
[ "$hops" -le 40 ] || exit 1
|
|
1924
|
+
paths=()
|
|
1925
|
+
for i in "${pend[@]}"; do paths+=("${cur[i]}"); done
|
|
1926
|
+
targets="$(readlink "${paths[@]}")" || exit 1
|
|
1927
|
+
next=(); k=0
|
|
1928
|
+
while IFS= read -r target; do
|
|
1929
|
+
# More lines than links: a target held a newline.
|
|
1930
|
+
[ "$k" -lt "${#pend[@]}" ] || exit 1
|
|
1931
|
+
i="${pend[k]}"
|
|
1932
|
+
case "$target" in
|
|
1933
|
+
/*) cur[i]="$target" ;;
|
|
1934
|
+
*) cur[i]="${cur[i]%/*}/$target" ;;
|
|
1935
|
+
esac
|
|
1936
|
+
[ ! -L "${cur[i]}" ] || next+=("$i")
|
|
1937
|
+
k=$((k + 1))
|
|
1938
|
+
done <<< "$targets"
|
|
1939
|
+
[ "$k" -eq "${#pend[@]}" ] || exit 1
|
|
1940
|
+
pend=("${next[@]+"${next[@]}"}")
|
|
1941
|
+
done
|
|
1942
|
+
for ((i = 0; i < n; i++)); do
|
|
1943
|
+
if [ "${kind[i]}" = f ]; then
|
|
1944
|
+
parent="${cur[i]%/*}"
|
|
1945
|
+
cd -P -- "${parent:-/}" 2>/dev/null || exit 1
|
|
1946
|
+
phys[i]="${PWD%/}/${cur[i]##*/}"
|
|
1947
|
+
fi
|
|
1948
|
+
printf '%s%s%s\n' "${kind[i]}" "$IS_SKI_US" "${phys[i]}"
|
|
1949
|
+
done
|
|
1950
|
+
)
|
|
1951
|
+
|
|
1952
|
+
# _ski_link_var <path in source> — set _SKI_K to the index of the link at that
|
|
1953
|
+
# path, or -1.
|
|
1954
|
+
_ski_link_var() {
|
|
1955
|
+
local k=0
|
|
1956
|
+
_SKI_K=-1
|
|
1957
|
+
while [ "$k" -lt "${#_SKI_LREL[@]}" ]; do
|
|
1958
|
+
if [ "${_SKI_LREL[k]}" = "$1" ]; then
|
|
1959
|
+
_SKI_K="$k"
|
|
1960
|
+
return 0
|
|
1961
|
+
fi
|
|
1962
|
+
k=$((k + 1))
|
|
1963
|
+
done
|
|
1964
|
+
}
|
|
1965
|
+
|
|
1966
|
+
# _ski_loops <target> <open> — materializing the directory at <target> would
|
|
1967
|
+
# copy one of the <open> paths (newline-separated) being materialized, the
|
|
1968
|
+
# link itself included, so it would never end.
|
|
1969
|
+
_ski_loops() {
|
|
1970
|
+
local target="$1" open
|
|
1971
|
+
[ -n "$target" ] || return 0
|
|
1972
|
+
while IFS= read -r open; do
|
|
1973
|
+
case "$open" in "$target"|"$target"/*) return 0 ;; esac
|
|
1974
|
+
done <<< "$2"
|
|
1975
|
+
return 1
|
|
1976
|
+
}
|
|
1977
|
+
|
|
1978
|
+
# _ski_skill <name> — decide one top-level entry of the current source: a
|
|
1979
|
+
# rendered skill (with its plan and warnings), a skill left out, or no skill.
|
|
1980
|
+
_ski_skill() {
|
|
1981
|
+
local name="$1" key content open="" state rel link reason nl=$'\n'
|
|
1982
|
+
key="$_SKI_DIR/$name"
|
|
1983
|
+
_SKI_ENTRIES="$nl"
|
|
1984
|
+
_SKI_DROPS=""
|
|
1985
|
+
_ski_link_var "$name"
|
|
1986
|
+
if [ "$_SKI_K" -ge 0 ]; then
|
|
1987
|
+
# The skill directory is itself a link. A test through one that
|
|
1988
|
+
# leaves the source only stats it; nothing is read from outside.
|
|
1989
|
+
case "${_SKI_LSTAT[_SKI_K]}:${_SKI_LKIND[_SKI_K]}" in
|
|
1990
|
+
ok:d) ;;
|
|
1991
|
+
outside:d)
|
|
1992
|
+
_ski_warn "$name" "" "$name" outside
|
|
1993
|
+
return 0
|
|
1994
|
+
;;
|
|
1995
|
+
dangling:*)
|
|
1996
|
+
_ski_warn "$name" "" "$name" dangling
|
|
1997
|
+
return 0
|
|
1998
|
+
;;
|
|
1999
|
+
*) return 0 ;;
|
|
2000
|
+
esac
|
|
2001
|
+
content="${_SKI_LTREL[_SKI_K]}"
|
|
2002
|
+
if _ski_loops "$content" ""; then
|
|
2003
|
+
[ ! -f "$_SKI_DIR/$name/SKILL.md" ] || _ski_warn "$name" "" "$name" loop
|
|
2004
|
+
return 0
|
|
1568
2005
|
fi
|
|
2006
|
+
_SKI_ENTRIES="$_SKI_ENTRIES$key$IS_SKI_US${IS_SKI_US}d$IS_SKI_US${_SKI_LPHYS[_SKI_K]}$nl"
|
|
2007
|
+
open="$nl"
|
|
2008
|
+
else
|
|
2009
|
+
content="$name"
|
|
2010
|
+
fi
|
|
2011
|
+
_ski_expand "$key" "$content" "" "$open"
|
|
2012
|
+
# A skill is a directory whose SKILL.md renders as a regular file.
|
|
2013
|
+
case "$_SKI_ENTRIES" in
|
|
2014
|
+
*"$nl$key${IS_SKI_US}SKILL.md${IS_SKI_US}f$IS_SKI_US"*) state=ok ;;
|
|
2015
|
+
*"$nl$key${IS_SKI_US}SKILL.md${IS_SKI_US}x$IS_SKI_US"*) state=dropped ;;
|
|
2016
|
+
*"$nl$key${IS_SKI_US}SKILL.md$IS_SKI_US"*) state=none ;;
|
|
2017
|
+
*)
|
|
2018
|
+
state=none
|
|
2019
|
+
[ ! -f "$_SKI_DIR/$content/SKILL.md" ] || state=ok
|
|
2020
|
+
;;
|
|
2021
|
+
esac
|
|
2022
|
+
case "$state" in
|
|
2023
|
+
none) return 0 ;;
|
|
2024
|
+
dropped)
|
|
2025
|
+
while IFS="$IS_SKI_US" read -r rel link reason; do
|
|
2026
|
+
[ "$rel" != "SKILL.md" ] || _ski_warn "$name" "" "$link" "$reason"
|
|
2027
|
+
done <<< "$_SKI_DROPS"
|
|
2028
|
+
return 0
|
|
2029
|
+
;;
|
|
2030
|
+
esac
|
|
2031
|
+
IS_SKI_LIST="$IS_SKI_LIST$_SKI_DIR/$name/SKILL.md$nl"
|
|
2032
|
+
IS_SKI_KEYS="$IS_SKI_KEYS$key$nl"
|
|
2033
|
+
IS_SKI_PLAN="$IS_SKI_PLAN${_SKI_ENTRIES#"$nl"}"
|
|
2034
|
+
while IFS="$IS_SKI_US" read -r rel link reason; do
|
|
2035
|
+
[ -z "$rel" ] || _ski_warn "$name" "$rel" "$link" "$reason"
|
|
2036
|
+
done <<< "$_SKI_DROPS"
|
|
2037
|
+
}
|
|
2038
|
+
|
|
2039
|
+
# _ski_expand <key> <content> <prefix> <open> — plan every link inside the
|
|
2040
|
+
# source directory <content> as it lands at <prefix> in the skill copy,
|
|
2041
|
+
# recursing into each directory a link materializes. <open> holds the
|
|
2042
|
+
# locations of the links already being materialized, for _ski_loops.
|
|
2043
|
+
_ski_expand() {
|
|
2044
|
+
local key="$1" content="$2" prefix="$3" open="$4" k=0 rel out location
|
|
2045
|
+
while [ "$k" -lt "${#_SKI_LREL[@]}" ]; do
|
|
2046
|
+
rel="${_SKI_LREL[k]}"
|
|
2047
|
+
case "$rel" in
|
|
2048
|
+
"$content"/*) ;;
|
|
2049
|
+
*) k=$((k + 1)); continue ;;
|
|
2050
|
+
esac
|
|
2051
|
+
out="${prefix:+$prefix/}${rel#"$content"/}"
|
|
2052
|
+
case "${_SKI_LSTAT[k]}:${_SKI_LKIND[k]}" in
|
|
2053
|
+
ok:f)
|
|
2054
|
+
_SKI_ENTRIES="$_SKI_ENTRIES$key$IS_SKI_US$out${IS_SKI_US}f$IS_SKI_US${_SKI_LPHYS[k]}"$'\n'
|
|
2055
|
+
;;
|
|
2056
|
+
ok:d)
|
|
2057
|
+
location="${rel%/*}"
|
|
2058
|
+
if _ski_loops "${_SKI_LTREL[k]}" "$open$location"; then
|
|
2059
|
+
_ski_drop "$key" "$out" "$rel" loop
|
|
2060
|
+
else
|
|
2061
|
+
_SKI_ENTRIES="$_SKI_ENTRIES$key$IS_SKI_US$out${IS_SKI_US}d$IS_SKI_US${_SKI_LPHYS[k]}"$'\n'
|
|
2062
|
+
_ski_expand "$key" "${_SKI_LTREL[k]}" "$out" "$open$location"$'\n'
|
|
2063
|
+
fi
|
|
2064
|
+
;;
|
|
2065
|
+
*) _ski_drop "$key" "$out" "$rel" "${_SKI_LSTAT[k]}" ;;
|
|
2066
|
+
esac
|
|
2067
|
+
k=$((k + 1))
|
|
2068
|
+
done
|
|
2069
|
+
}
|
|
2070
|
+
|
|
2071
|
+
# _ski_drop <key> <path in copy> <path in source> <reason> — leave a link out.
|
|
2072
|
+
_ski_drop() {
|
|
2073
|
+
_SKI_ENTRIES="$_SKI_ENTRIES$1$IS_SKI_US$2${IS_SKI_US}x$IS_SKI_US"$'\n'
|
|
2074
|
+
_SKI_DROPS="$_SKI_DROPS$2$IS_SKI_US$3$IS_SKI_US$4"$'\n'
|
|
2075
|
+
}
|
|
2076
|
+
|
|
2077
|
+
# _ski_warn <skill> <path in copy> <path in source> <reason> — one WARNING:
|
|
2078
|
+
# line naming the skill and the link by its own path. An empty <path in copy>
|
|
2079
|
+
# means the whole skill is left out.
|
|
2080
|
+
_ski_warn() {
|
|
2081
|
+
local link="$_SKI_ROOT_REL/$3" what
|
|
2082
|
+
case "$4" in
|
|
2083
|
+
outside) what="is a symlink that resolves outside $_SKI_ROOT_REL" ;;
|
|
2084
|
+
dangling) what="is a dangling symlink" ;;
|
|
2085
|
+
loop) what="is a symlink to a directory that encloses it" ;;
|
|
2086
|
+
*) what="is a symlink to neither a file nor a directory" ;;
|
|
2087
|
+
esac
|
|
2088
|
+
if [ -n "$2" ]; then
|
|
2089
|
+
echo "WARNING: skill '$1' renders without $2: $link $what" >&2
|
|
2090
|
+
else
|
|
2091
|
+
echo "WARNING: skill '$1' is left out of every output: $link $what" >&2
|
|
1569
2092
|
fi
|
|
1570
2093
|
}
|
|
1571
2094
|
|
|
@@ -1588,6 +2111,9 @@ warn_unsynced() {
|
|
|
1588
2111
|
# replay that answer. Only the engine sets it: a sync never writes into a
|
|
1589
2112
|
# source (validate_output_path refuses), so each caller would otherwise re-run
|
|
1590
2113
|
# the same find and sort for every source directory.
|
|
2114
|
+
#
|
|
2115
|
+
# Skills come from skill_source_inventory, which also decides what a symlink
|
|
2116
|
+
# inside a skills source renders as.
|
|
1591
2117
|
read_source_artifact_files() {
|
|
1592
2118
|
local repo_root="$1"
|
|
1593
2119
|
local config_file="$2"
|
|
@@ -1595,8 +2121,15 @@ read_source_artifact_files() {
|
|
|
1595
2121
|
local src f dir listing memo_key="" key_var="" list_var="" joined=""
|
|
1596
2122
|
|
|
1597
2123
|
IS_SOURCE_FILES=()
|
|
2124
|
+
if [ "$section" = "skills" ]; then
|
|
2125
|
+
skill_source_inventory "$repo_root" "$config_file"
|
|
2126
|
+
while IFS= read -r f; do
|
|
2127
|
+
[ -n "$f" ] && IS_SOURCE_FILES+=("$f")
|
|
2128
|
+
done <<< "$IS_SKI_LIST"
|
|
2129
|
+
return 0
|
|
2130
|
+
fi
|
|
1598
2131
|
case "${IS_SOURCE_FILES_MEMO:-0}:$section" in
|
|
1599
|
-
1:rules|1:agents
|
|
2132
|
+
1:rules|1:agents)
|
|
1600
2133
|
key_var="IS_SF_${section}_KEY"
|
|
1601
2134
|
list_var="IS_SF_${section}_LIST"
|
|
1602
2135
|
memo_key="$repo_root"$'\n'"$config_file"
|
|
@@ -1623,12 +2156,6 @@ read_source_artifact_files() {
|
|
|
1623
2156
|
exit 1
|
|
1624
2157
|
}
|
|
1625
2158
|
;;
|
|
1626
|
-
skills)
|
|
1627
|
-
listing="$(find "$dir" -mindepth 1 -maxdepth 1 -type d -print | LC_ALL=C sort)" || {
|
|
1628
|
-
echo "ERROR: cannot enumerate $section under '$dir' — refusing to render a partial list." >&2
|
|
1629
|
-
exit 1
|
|
1630
|
-
}
|
|
1631
|
-
;;
|
|
1632
2159
|
*) continue ;;
|
|
1633
2160
|
esac
|
|
1634
2161
|
[ -n "$listing" ] || continue
|
|
@@ -1636,10 +2163,6 @@ read_source_artifact_files() {
|
|
|
1636
2163
|
# array it fills survives and any failure inside it is this shell's.
|
|
1637
2164
|
while IFS= read -r f; do
|
|
1638
2165
|
[ -n "$f" ] || continue
|
|
1639
|
-
if [ "$section" = "skills" ]; then
|
|
1640
|
-
[ -f "$f/SKILL.md" ] || continue
|
|
1641
|
-
f="$f/SKILL.md"
|
|
1642
|
-
fi
|
|
1643
2166
|
IS_SOURCE_FILES+=("$f")
|
|
1644
2167
|
joined="$joined$f"$'\n'
|
|
1645
2168
|
done <<< "$listing"
|
|
@@ -1787,15 +2310,220 @@ report_context_source_sizes() {
|
|
|
1787
2310
|
|
|
1788
2311
|
# --- Config Parsing ---
|
|
1789
2312
|
|
|
2313
|
+
# --- Package references in sources -------------------------------------------
|
|
2314
|
+
# A `sources:` entry names a directory inside an installed package in one of
|
|
2315
|
+
# three spellings (decision 0019):
|
|
2316
|
+
#
|
|
2317
|
+
# .intelligence/packages/@scope/name/<dir> the store path: an ordinary
|
|
2318
|
+
# repository path, as the CLI writes it
|
|
2319
|
+
# @scope/name/<dir> by full name: a reference only while
|
|
2320
|
+
# `packages:` declares @scope/name,
|
|
2321
|
+
# otherwise an ordinary path as before
|
|
2322
|
+
# <alias>:<dir> by the alias that package's
|
|
2323
|
+
# `packages:` entry declares
|
|
2324
|
+
#
|
|
2325
|
+
# A reference renders exactly as the store path it stands for. The list parser
|
|
2326
|
+
# below expands it inside the awk pass that already reads the manifest; that
|
|
2327
|
+
# pass is the one point every reader goes through — adapters, the engine's own
|
|
2328
|
+
# loops and the CLI — so expansion costs no process, and the CLI sees exactly
|
|
2329
|
+
# what the engine renders (decision 0009). A reference that resolves to nothing
|
|
2330
|
+
# is left out of the list, so no reader can render it as a path: `sync` names it
|
|
2331
|
+
# in a WARNING: line and `status --check` reports it.
|
|
2332
|
+
#
|
|
2333
|
+
# An alias is two or more of A-Z a-z 0-9 . _ - and starts with a letter or digit:
|
|
2334
|
+
# no `/`, `:` or `@`, no whitespace or quotes, and never one letter that reads
|
|
2335
|
+
# like a drive. It is a lookup key, never part of a path. <dir> is one or more
|
|
2336
|
+
# `/`-separated segments, none of them empty, `.` or `..`, without a backslash,
|
|
2337
|
+
# so a reference never leaves its package.
|
|
2338
|
+
#
|
|
2339
|
+
# These functions are the only definition. The CLI's editors (lib/manifest.sh),
|
|
2340
|
+
# `source`, `package alias` and `status --check` load this same string:
|
|
2341
|
+
#
|
|
2342
|
+
# pkr_collect(line) feed every manifest line; records the names `packages:`
|
|
2343
|
+
# declares and the alias each one states
|
|
2344
|
+
# pkr_parse(entry) classify one entry: path, ok, unknown (an alias no
|
|
2345
|
+
# package declares), ambiguous (an alias several declare)
|
|
2346
|
+
# or invalid (a reference whose <dir> is malformed); sets
|
|
2347
|
+
# PKR_NAME, PKR_DIR, PKR_ALIAS and, when ambiguous,
|
|
2348
|
+
# PKR_HOLDERS
|
|
2349
|
+
# pkr_expand(entry) the directory the entry renders as, "" when it renders
|
|
2350
|
+
# nothing; leaves the state in PKR_STATE
|
|
2351
|
+
# pkr_alias_ok(a) whether <a> is a well-formed alias
|
|
2352
|
+
IS_PKG_REF_AWK='
|
|
2353
|
+
function pkr_seg_ok(s) { return s != "" && s != "." && s != ".." }
|
|
2354
|
+
# The CLI refuses any other package name (assert_valid_pkg_name): a name
|
|
2355
|
+
# becomes a store path, so one that is not a plain @scope/name is never
|
|
2356
|
+
# declared.
|
|
2357
|
+
function pkr_name_ok(name, p) {
|
|
2358
|
+
if (name !~ /^@[@A-Za-z0-9._-]*\/[@A-Za-z0-9._-]*$/) return 0
|
|
2359
|
+
p = index(name, "/")
|
|
2360
|
+
return pkr_seg_ok(substr(name, 2, p - 2)) && pkr_seg_ok(substr(name, p + 1))
|
|
2361
|
+
}
|
|
2362
|
+
# Enumerated, not a range: a range follows the locale in some awks.
|
|
2363
|
+
function pkr_alias_ok(a, i, c) {
|
|
2364
|
+
if (length(a) < 2) return 0
|
|
2365
|
+
for (i = 1; i <= length(a); i++) {
|
|
2366
|
+
c = substr(a, i, 1)
|
|
2367
|
+
if (index("ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789", c)) continue
|
|
2368
|
+
if (i > 1 && index("._-", c)) continue
|
|
2369
|
+
return 0
|
|
2370
|
+
}
|
|
2371
|
+
return 1
|
|
2372
|
+
}
|
|
2373
|
+
function pkr_dir_ok(dir, n, i, seg) {
|
|
2374
|
+
if (dir == "" || index(dir, "\\")) return 0
|
|
2375
|
+
n = split(dir, seg, "/")
|
|
2376
|
+
for (i = 1; i <= n; i++) if (!pkr_seg_ok(seg[i])) return 0
|
|
2377
|
+
return 1
|
|
2378
|
+
}
|
|
2379
|
+
# The quoted keys of the top-level packages: block and their alias fields,
|
|
2380
|
+
# read the way lib/qmap.awk reads what the CLI writes: a duplicated key is
|
|
2381
|
+
# one package, and the first alias a package states is the one it has.
|
|
2382
|
+
function pkr_collect(line, s, q, v) {
|
|
2383
|
+
if (line ~ /^packages:[ \t]*$/) { pkr_in = 1; pkr_cur = ""; return }
|
|
2384
|
+
if (!pkr_in || line ~ /^[ \t]*(#.*)?$/) return
|
|
2385
|
+
if (line ~ /^[^ \t]/) { pkr_in = 0; pkr_cur = ""; return }
|
|
2386
|
+
if (substr(line, 1, 3) == " \"") {
|
|
2387
|
+
pkr_cur = ""
|
|
2388
|
+
s = substr(line, 4)
|
|
2389
|
+
q = index(s, "\"")
|
|
2390
|
+
if (q < 2) return
|
|
2391
|
+
v = substr(s, 1, q - 1)
|
|
2392
|
+
if (!pkr_name_ok(v)) return
|
|
2393
|
+
if (!(v in pkr_seen)) { pkr_seen[v] = 1; PKR_NAMES[++PKR_N] = v }
|
|
2394
|
+
pkr_cur = v
|
|
2395
|
+
return
|
|
2396
|
+
}
|
|
2397
|
+
if (substr(line, 1, 4) != " ") { pkr_cur = ""; return }
|
|
2398
|
+
if (pkr_cur == "" || (pkr_cur in PKR_ALIAS_RAW) || substr(line, 5, 6) != "alias:") return
|
|
2399
|
+
v = substr(line, 11)
|
|
2400
|
+
sub(/^[ \t]+/, "", v)
|
|
2401
|
+
if (substr(v, 1, 1) == "\"") {
|
|
2402
|
+
v = substr(v, 2)
|
|
2403
|
+
q = index(v, "\"")
|
|
2404
|
+
if (q == 0) return
|
|
2405
|
+
v = substr(v, 1, q - 1)
|
|
2406
|
+
} else {
|
|
2407
|
+
sub(/[ \t]+#.*$/, "", v)
|
|
2408
|
+
sub(/[ \t]+$/, "", v)
|
|
2409
|
+
}
|
|
2410
|
+
PKR_ALIAS_RAW[pkr_cur] = v
|
|
2411
|
+
if (!pkr_alias_ok(v)) return
|
|
2412
|
+
pkr_holders[v] = (v in pkr_holders) ? pkr_holders[v] ", " pkr_cur : pkr_cur
|
|
2413
|
+
pkr_count[v]++
|
|
2414
|
+
pkr_alias[v] = pkr_cur
|
|
2415
|
+
}
|
|
2416
|
+
function pkr_parse(entry, c, p, i, rest, name) {
|
|
2417
|
+
PKR_NAME = ""; PKR_DIR = ""; PKR_ALIAS = ""; PKR_HOLDERS = ""
|
|
2418
|
+
if (substr(entry, 1, 1) == "@") {
|
|
2419
|
+
p = index(entry, "/")
|
|
2420
|
+
if (p == 0) return "path"
|
|
2421
|
+
rest = substr(entry, p + 1)
|
|
2422
|
+
i = index(rest, "/")
|
|
2423
|
+
name = i ? substr(entry, 1, p + i - 1) : entry
|
|
2424
|
+
if (!(name in pkr_seen)) return "path"
|
|
2425
|
+
PKR_NAME = name
|
|
2426
|
+
PKR_DIR = i ? substr(rest, i + 1) : ""
|
|
2427
|
+
return pkr_dir_ok(PKR_DIR) ? "ok" : "invalid"
|
|
2428
|
+
}
|
|
2429
|
+
c = index(entry, ":")
|
|
2430
|
+
if (c == 0 || !pkr_alias_ok(substr(entry, 1, c - 1))) return "path"
|
|
2431
|
+
PKR_ALIAS = substr(entry, 1, c - 1)
|
|
2432
|
+
PKR_DIR = substr(entry, c + 1)
|
|
2433
|
+
if (!pkr_dir_ok(PKR_DIR)) return "invalid"
|
|
2434
|
+
if (!(PKR_ALIAS in pkr_count)) return "unknown"
|
|
2435
|
+
if (pkr_count[PKR_ALIAS] > 1) { PKR_HOLDERS = pkr_holders[PKR_ALIAS]; return "ambiguous" }
|
|
2436
|
+
PKR_NAME = pkr_alias[PKR_ALIAS]
|
|
2437
|
+
return "ok"
|
|
2438
|
+
}
|
|
2439
|
+
function pkr_expand(entry) {
|
|
2440
|
+
PKR_STATE = pkr_parse(entry)
|
|
2441
|
+
if (PKR_STATE == "path") return entry
|
|
2442
|
+
if (PKR_STATE == "ok") return ".intelligence/packages/" PKR_NAME "/" PKR_DIR
|
|
2443
|
+
return ""
|
|
2444
|
+
}
|
|
2445
|
+
'
|
|
2446
|
+
|
|
2447
|
+
# The value of one block-sequence item in the manifest — the only reading of a
|
|
2448
|
+
# list entry. Every list reader below and the CLI's `sources:` editors
|
|
2449
|
+
# (lib/manifest.sh) load this string, so an entry the engine renders and the
|
|
2450
|
+
# entry an editor compares are the same text (decision 0009, point 7).
|
|
2451
|
+
#
|
|
2452
|
+
# yaml_item(text) <text> is what follows the item's `-`. Returns the value:
|
|
2453
|
+
# whitespace around it and a trailing `# comment` dropped,
|
|
2454
|
+
# then every quote removed, as the readers always did. A
|
|
2455
|
+
# comment starts at a `#` that follows whitespace outside
|
|
2456
|
+
# quotes; inside a quoted scalar a `#` is part of the value,
|
|
2457
|
+
# and so is one with no whitespace before it (`a#b`).
|
|
2458
|
+
IS_YAML_ITEM_AWK='
|
|
2459
|
+
function yaml_item(s, q, e, rest) {
|
|
2460
|
+
sub(/^[ \t]+/, "", s)
|
|
2461
|
+
q = substr(s, 1, 1)
|
|
2462
|
+
rest = s
|
|
2463
|
+
s = ""
|
|
2464
|
+
if (q == "\"" || q == "\047") {
|
|
2465
|
+
e = index(substr(rest, 2), q)
|
|
2466
|
+
if (e == 0) { s = rest; rest = "" }
|
|
2467
|
+
else { s = substr(rest, 1, e + 1); rest = substr(rest, e + 2) }
|
|
2468
|
+
}
|
|
2469
|
+
if (s == "" && substr(rest, 1, 1) == "#") rest = ""
|
|
2470
|
+
else if (match(rest, /[ \t]#/)) rest = substr(rest, 1, RSTART - 1)
|
|
2471
|
+
s = s rest
|
|
2472
|
+
sub(/[ \t]+$/, "", s)
|
|
2473
|
+
gsub(/["\047]/, "", s)
|
|
2474
|
+
return s
|
|
2475
|
+
}
|
|
2476
|
+
'
|
|
2477
|
+
|
|
2478
|
+
# The manifest list reader. `mode` selects what it prints for each entry:
|
|
2479
|
+
# expand the entry as the engine renders it: a reference becomes its store
|
|
2480
|
+
# path, one that resolves to nothing is left out
|
|
2481
|
+
# raw the entry as written — what an editor compares and rewrites
|
|
2482
|
+
# classify raw, pkr_parse state, expansion, alias and the packages sharing
|
|
2483
|
+
# it, separated by \037 — what `status --check` and `source` judge
|
|
2484
|
+
# Only rules, agents and skills hold sources; every other list reads raw.
|
|
2485
|
+
# Entries are held until the end of the file because `packages:` may follow
|
|
2486
|
+
# `sources:`.
|
|
2487
|
+
IS_YAML_LIST_AWK='
|
|
2488
|
+
{ sub(/\r$/, ""); pkr_collect($0) }
|
|
2489
|
+
/^[a-z]/ { current_section = ""; depth = 0 }
|
|
2490
|
+
/^ [a-z]/ { current_section = ""; depth = 0 }
|
|
2491
|
+
$0 ~ "^" section ":" { current_section = section; depth = 0; next }
|
|
2492
|
+
$0 ~ "^ " section ":" { current_section = section; depth = 2; next }
|
|
2493
|
+
current_section == section && depth == 0 && /^ - / {
|
|
2494
|
+
vals[++nv] = yaml_item(substr($0, 5))
|
|
2495
|
+
}
|
|
2496
|
+
current_section == section && depth == 2 && /^ - / {
|
|
2497
|
+
vals[++nv] = yaml_item(substr($0, 7))
|
|
2498
|
+
}
|
|
2499
|
+
END {
|
|
2500
|
+
sources = section == "rules" || section == "agents" || section == "skills"
|
|
2501
|
+
n = nv + 0
|
|
2502
|
+
for (i = 1; i <= n; i++) {
|
|
2503
|
+
val = vals[i]
|
|
2504
|
+
if (!sources || mode == "raw") { print val; continue }
|
|
2505
|
+
dir = pkr_expand(val)
|
|
2506
|
+
if (mode == "classify") print val "\037" PKR_STATE "\037" dir "\037" PKR_ALIAS "\037" PKR_HOLDERS
|
|
2507
|
+
else if (PKR_STATE == "path" || PKR_STATE == "ok") print dir
|
|
2508
|
+
}
|
|
2509
|
+
}
|
|
2510
|
+
'
|
|
2511
|
+
|
|
1790
2512
|
# Read a simple list from config.yaml
|
|
1791
2513
|
# Format: key:\n - "value1"\n - "value2"
|
|
1792
|
-
# Usage:
|
|
2514
|
+
# Usage: while IFS= read -r src; do ...; done < <(read_yaml_list "intelligence.yaml" "rules")
|
|
2515
|
+
#
|
|
2516
|
+
# A source list arrives with its package references expanded to store paths —
|
|
2517
|
+
# the spelling every renderer resolves as $REPO_ROOT/<entry> — and without the
|
|
2518
|
+
# ones that resolve to nothing. Editors that rewrite the manifest read it with
|
|
2519
|
+
# read_yaml_list_raw instead.
|
|
1793
2520
|
#
|
|
1794
2521
|
# Consults the load_yaml_list cache first: sync reads the same sections from
|
|
1795
2522
|
# the same manifest dozens of times, and each awk spawn costs tens of
|
|
1796
2523
|
# milliseconds on Windows. The cache is only ever populated by
|
|
1797
|
-
# load_yaml_list
|
|
1798
|
-
#
|
|
2524
|
+
# load_yaml_list and load_yaml_lists. The engine never mutates its manifest;
|
|
2525
|
+
# CLI writers invalidate it through qmap_memo_reset; load_yaml_lists and the
|
|
2526
|
+
# project preloader also populate it.
|
|
1799
2527
|
read_yaml_list() {
|
|
1800
2528
|
local file="$1"
|
|
1801
2529
|
local section="$2"
|
|
@@ -1813,27 +2541,67 @@ read_yaml_list() {
|
|
|
1813
2541
|
fi
|
|
1814
2542
|
;;
|
|
1815
2543
|
esac
|
|
1816
|
-
awk -v section="$section"
|
|
1817
|
-
|
|
1818
|
-
|
|
1819
|
-
|
|
1820
|
-
|
|
1821
|
-
|
|
1822
|
-
|
|
1823
|
-
|
|
1824
|
-
|
|
1825
|
-
|
|
1826
|
-
|
|
1827
|
-
|
|
1828
|
-
|
|
1829
|
-
|
|
1830
|
-
|
|
1831
|
-
|
|
1832
|
-
|
|
1833
|
-
|
|
1834
|
-
|
|
2544
|
+
awk -v section="$section" -v mode=expand "$IS_PKG_REF_AWK$IS_YAML_ITEM_AWK$IS_YAML_LIST_AWK" "$file"
|
|
2545
|
+
}
|
|
2546
|
+
|
|
2547
|
+
# read_yaml_list_raw <file> <section> — the entries exactly as written,
|
|
2548
|
+
# references included and never cached. The CLI's sources editors compare and
|
|
2549
|
+
# rewrite these, so an entry is matched as the user spelled it.
|
|
2550
|
+
read_yaml_list_raw() {
|
|
2551
|
+
awk -v section="$2" -v mode=raw "$IS_PKG_REF_AWK$IS_YAML_ITEM_AWK$IS_YAML_LIST_AWK" "$1"
|
|
2552
|
+
}
|
|
2553
|
+
|
|
2554
|
+
# read_source_entries <file> <section> — one \037-separated line per entry: as
|
|
2555
|
+
# written, its pkr_parse state (path, ok, unknown, ambiguous or invalid), the
|
|
2556
|
+
# directory it renders as ("" for none), the alias it names and, when several
|
|
2557
|
+
# packages declare that alias, their names. One pass of the parser the engine
|
|
2558
|
+
# renders through, so a verdict can never disagree with the render.
|
|
2559
|
+
read_source_entries() {
|
|
2560
|
+
awk -v section="$2" -v mode=classify "$IS_PKG_REF_AWK$IS_YAML_ITEM_AWK$IS_YAML_LIST_AWK" "$1"
|
|
2561
|
+
}
|
|
2562
|
+
|
|
2563
|
+
# read_package_aliases <file> — one \037-separated line per declared package that
|
|
2564
|
+
# states an alias: its name, the alias, and ok, ambiguous (another package states
|
|
2565
|
+
# it too) or invalid (not a well-formed alias, so it resolves nothing). An
|
|
2566
|
+
# invalid alias is manifest input that may hold anything, so its field is empty.
|
|
2567
|
+
read_package_aliases() {
|
|
2568
|
+
[ -f "$1" ] || return 0
|
|
2569
|
+
awk "$IS_PKG_REF_AWK"'
|
|
2570
|
+
{ sub(/\r$/, ""); pkr_collect($0) }
|
|
2571
|
+
END {
|
|
2572
|
+
for (i = 1; i <= PKR_N; i++) {
|
|
2573
|
+
name = PKR_NAMES[i]
|
|
2574
|
+
if (!(name in PKR_ALIAS_RAW)) continue
|
|
2575
|
+
a = PKR_ALIAS_RAW[name]
|
|
2576
|
+
if (!pkr_alias_ok(a)) { print name "\037\037invalid"; continue }
|
|
2577
|
+
print name "\037" a "\037" (pkr_count[a] > 1 ? "ambiguous" : "ok")
|
|
2578
|
+
}
|
|
1835
2579
|
}
|
|
1836
|
-
' "$
|
|
2580
|
+
' "$1"
|
|
2581
|
+
}
|
|
2582
|
+
|
|
2583
|
+
# What pkr_alias_ok accepts, in the words every message that refuses an alias
|
|
2584
|
+
# uses.
|
|
2585
|
+
# shellcheck disable=SC2034
|
|
2586
|
+
IS_PKG_ALIAS_RULE="two or more of A-Z a-z 0-9 . _ -, starting with a letter or digit"
|
|
2587
|
+
|
|
2588
|
+
# pkg_alias_valid <alias> — true when <alias> is well formed (pkr_alias_ok). The
|
|
2589
|
+
# value travels through the environment: `awk -v` would interpret backslashes.
|
|
2590
|
+
pkg_alias_valid() {
|
|
2591
|
+
IS_PKR_CANDIDATE="$1" awk "$IS_PKG_REF_AWK"'BEGIN { exit !pkr_alias_ok(ENVIRON["IS_PKR_CANDIDATE"]) }'
|
|
2592
|
+
}
|
|
2593
|
+
|
|
2594
|
+
# source_reference_problem_var <state> <alias> <holders> — set IS_SOURCE_PROBLEM
|
|
2595
|
+
# to why a reference in that state renders nothing, "" for path and ok. The one
|
|
2596
|
+
# wording of `sync`'s WARNING: line and of `status --check`.
|
|
2597
|
+
# shellcheck disable=SC2034
|
|
2598
|
+
source_reference_problem_var() {
|
|
2599
|
+
case "$1" in
|
|
2600
|
+
unknown) IS_SOURCE_PROBLEM="names alias '$2', which no package in packages: declares" ;;
|
|
2601
|
+
ambiguous) IS_SOURCE_PROBLEM="names alias '$2', which several packages declare ($3) — an alias names one package" ;;
|
|
2602
|
+
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 '..'" ;;
|
|
2603
|
+
*) IS_SOURCE_PROBLEM="" ;;
|
|
2604
|
+
esac
|
|
1837
2605
|
}
|
|
1838
2606
|
|
|
1839
2607
|
# load_yaml_list <file> <section> — fill the global IS_YAML_LIST with the
|
|
@@ -1863,21 +2631,19 @@ load_yaml_list() {
|
|
|
1863
2631
|
esac
|
|
1864
2632
|
}
|
|
1865
2633
|
|
|
1866
|
-
#
|
|
1867
|
-
#
|
|
1868
|
-
#
|
|
1869
|
-
|
|
1870
|
-
|
|
1871
|
-
|
|
1872
|
-
|
|
1873
|
-
|
|
1874
|
-
|
|
1875
|
-
|
|
1876
|
-
|
|
1877
|
-
done
|
|
1878
|
-
out="$(awk -v sections="$*" '
|
|
2634
|
+
# The manifest readers below share their awk programs, so the combined pass in
|
|
2635
|
+
# load_manifest_view reads each shape exactly as its single-purpose reader does.
|
|
2636
|
+
# Rule order matters when they are joined: the targets program ends its rules
|
|
2637
|
+
# with `next`, so it runs last and never hides a line from the others.
|
|
2638
|
+
#
|
|
2639
|
+
# The lists program carries the package-reference parser with it: it sees every
|
|
2640
|
+
# line, so `packages:` is collected wherever it stands, and each source list is
|
|
2641
|
+
# expanded exactly as read_yaml_list expands it. A reference that resolves to
|
|
2642
|
+
# nothing comes out as a `!` row, which _yaml_lists_store keeps in
|
|
2643
|
+
# IS_YL_UNRESOLVED.
|
|
2644
|
+
_IS_AWK_LISTS="$IS_PKG_REF_AWK$IS_YAML_ITEM_AWK"'
|
|
1879
2645
|
BEGIN { n = split(sections, want, " ") }
|
|
1880
|
-
{ sub(/\r$/, "") }
|
|
2646
|
+
{ sub(/\r$/, ""); pkr_collect($0) }
|
|
1881
2647
|
{
|
|
1882
2648
|
for (k = 1; k <= n; k++) {
|
|
1883
2649
|
s = want[k]
|
|
@@ -1886,49 +2652,53 @@ load_yaml_lists() {
|
|
|
1886
2652
|
if ($0 ~ "^" s ":") { cur[k] = s; dep[k] = 0; continue }
|
|
1887
2653
|
if ($0 ~ "^ " s ":") { cur[k] = s; dep[k] = 2; continue }
|
|
1888
2654
|
if (cur[k] == s && dep[k] == 0 && /^ - /) {
|
|
1889
|
-
|
|
1890
|
-
|
|
1891
|
-
gsub(/["\047]/, "", val)
|
|
1892
|
-
print s "\037" val
|
|
2655
|
+
got_s[++ng] = s
|
|
2656
|
+
got_v[ng] = yaml_item(substr($0, 5))
|
|
1893
2657
|
}
|
|
1894
2658
|
if (cur[k] == s && dep[k] == 2 && /^ - /) {
|
|
1895
|
-
|
|
1896
|
-
|
|
1897
|
-
gsub(/["\047]/, "", val)
|
|
1898
|
-
print s "\037" val
|
|
2659
|
+
got_s[++ng] = s
|
|
2660
|
+
got_v[ng] = yaml_item(substr($0, 7))
|
|
1899
2661
|
}
|
|
1900
2662
|
}
|
|
1901
2663
|
}
|
|
1902
|
-
|
|
1903
|
-
|
|
1904
|
-
|
|
1905
|
-
|
|
1906
|
-
|
|
1907
|
-
|
|
1908
|
-
|
|
1909
|
-
|
|
1910
|
-
|
|
1911
|
-
|
|
1912
|
-
|
|
1913
|
-
|
|
1914
|
-
}
|
|
2664
|
+
END {
|
|
2665
|
+
m = ng + 0
|
|
2666
|
+
for (i = 1; i <= m; i++) {
|
|
2667
|
+
s = got_s[i]
|
|
2668
|
+
val = got_v[i]
|
|
2669
|
+
if (s != "rules" && s != "agents" && s != "skills") { print s "\037" val; continue }
|
|
2670
|
+
dir = pkr_expand(val)
|
|
2671
|
+
if (PKR_STATE == "path" || PKR_STATE == "ok") print s "\037" dir
|
|
2672
|
+
else print "!\037" s "\037" val "\037" PKR_STATE "\037" dir "\037" PKR_ALIAS "\037" PKR_HOLDERS
|
|
2673
|
+
}
|
|
2674
|
+
}
|
|
2675
|
+
'
|
|
1915
2676
|
|
|
1916
|
-
#
|
|
1917
|
-
#
|
|
1918
|
-
|
|
1919
|
-
|
|
1920
|
-
|
|
1921
|
-
|
|
1922
|
-
|
|
1923
|
-
|
|
1924
|
-
|
|
1925
|
-
|
|
1926
|
-
|
|
1927
|
-
|
|
2677
|
+
# get_yaml_field: the first ` fkey:` inside the first `fsection:` block. A
|
|
2678
|
+
# top-level key closes the block; ftag prefixes the printed value.
|
|
2679
|
+
_IS_AWK_FIELD='
|
|
2680
|
+
{ sub(/\r$/, "") }
|
|
2681
|
+
!fdone {
|
|
2682
|
+
if ($0 ~ "^" fsection ":") fin = 1
|
|
2683
|
+
else if (fin && /^[a-zA-Z]/) fdone = 1
|
|
2684
|
+
else if (fin && $0 ~ "^ " fkey ":") {
|
|
2685
|
+
fval = $0
|
|
2686
|
+
sub(/.*:[[:space:]]*["\047]?/, "", fval)
|
|
2687
|
+
sub(/["\047]?[[:space:]]*$/, "", fval)
|
|
2688
|
+
ffound = 1
|
|
2689
|
+
fdone = 1
|
|
2690
|
+
}
|
|
2691
|
+
}
|
|
2692
|
+
END { if (ffound) print ftag fval }
|
|
2693
|
+
'
|
|
2694
|
+
|
|
2695
|
+
# load_targets_cache: one `name<TAB>enabled<TAB>output` row per target, each
|
|
2696
|
+
# prefixed with ttag.
|
|
2697
|
+
_IS_AWK_TARGETS='
|
|
1928
2698
|
function flush_target(at_sibling) {
|
|
1929
2699
|
if (name == "") return
|
|
1930
2700
|
if (enabled == "" && at_sibling) enabled = 0
|
|
1931
|
-
printf "%s\t%s\t%s\n", name, enabled, output
|
|
2701
|
+
printf "%s%s\t%s\t%s\n", ttag, name, enabled, output
|
|
1932
2702
|
name = ""
|
|
1933
2703
|
}
|
|
1934
2704
|
{ sub(/\r$/, "") }
|
|
@@ -1964,10 +2734,147 @@ load_targets_cache() {
|
|
|
1964
2734
|
output = val
|
|
1965
2735
|
}
|
|
1966
2736
|
END { flush_target(0) }
|
|
1967
|
-
|
|
2737
|
+
'
|
|
2738
|
+
|
|
2739
|
+
# _yaml_lists_store <file> <awk-output> <section>... — fill the load_yaml_list
|
|
2740
|
+
# cache for each section from `<section><US><value>` rows, and IS_YL_UNRESOLVED
|
|
2741
|
+
# from the `!` rows: one read_source_entries-shaped line each, the section in
|
|
2742
|
+
# front.
|
|
2743
|
+
_yaml_lists_store() {
|
|
2744
|
+
local file="$1" out="$2" line v s k saved_ifs="$IFS"
|
|
2745
|
+
local -a lines=() names=() vals=()
|
|
2746
|
+
shift 2
|
|
2747
|
+
names=("$@")
|
|
2748
|
+
IFS=$'\n'
|
|
2749
|
+
set -f
|
|
2750
|
+
# shellcheck disable=SC2206 # rows are `<section><US><value>`, never empty
|
|
2751
|
+
lines=($out)
|
|
2752
|
+
set +f
|
|
2753
|
+
IFS="$saved_ifs"
|
|
2754
|
+
IS_YL_UNRESOLVED=""
|
|
2755
|
+
# One pass over the rows in this shell: reading a here-string again per
|
|
2756
|
+
# section costs a system call per byte on Git Bash.
|
|
2757
|
+
for line in "${lines[@]+"${lines[@]}"}"; do
|
|
2758
|
+
s="${line%%$'\037'*}"
|
|
2759
|
+
v="${line#*$'\037'}"
|
|
2760
|
+
if [ "$s" = "!" ]; then
|
|
2761
|
+
IS_YL_UNRESOLVED="$IS_YL_UNRESOLVED$v"$'\n'
|
|
2762
|
+
continue
|
|
2763
|
+
fi
|
|
2764
|
+
for ((k = 0; k < ${#names[@]}; k++)); do
|
|
2765
|
+
[ "${names[k]}" = "$s" ] || continue
|
|
2766
|
+
vals[k]="${vals[k]-}$v"$'\n'
|
|
2767
|
+
done
|
|
2768
|
+
done
|
|
2769
|
+
for ((k = 0; k < ${#names[@]}; k++)); do
|
|
2770
|
+
v="${vals[k]-}"
|
|
2771
|
+
while [[ "$v" == *$'\n' ]]; do v="${v%$'\n'}"; done
|
|
2772
|
+
printf -v "IS_YL_${names[k]}_FILE" '%s' "$file"
|
|
2773
|
+
printf -v "IS_YL_${names[k]}_VAL" '%s' "$v"
|
|
2774
|
+
done
|
|
2775
|
+
}
|
|
2776
|
+
|
|
2777
|
+
# load_yaml_lists <file> <section>... — warm the load_yaml_list cache for several
|
|
2778
|
+
# sections in one manifest pass. Each section runs read_yaml_list's own state
|
|
2779
|
+
# machine and its reference expansion, so every cached value is what
|
|
2780
|
+
# load_yaml_list would have stored. The same pass leaves the references that
|
|
2781
|
+
# resolve to nothing in IS_YL_UNRESOLVED, one read_source_entries-shaped line
|
|
2782
|
+
# each with the section in front, for the WARNING: lines `sync` prints.
|
|
2783
|
+
load_yaml_lists() {
|
|
2784
|
+
local file="$1" out s
|
|
2785
|
+
shift
|
|
2786
|
+
for s in "$@"; do
|
|
2787
|
+
case "$s" in
|
|
2788
|
+
rules|agents|skills|ignore|submodules) ;;
|
|
2789
|
+
*) return 1 ;;
|
|
2790
|
+
esac
|
|
2791
|
+
done
|
|
2792
|
+
out="$(awk -v sections="$*" "$_IS_AWK_LISTS" "$file")"
|
|
2793
|
+
_yaml_lists_store "$file" "$out" "$@"
|
|
2794
|
+
}
|
|
2795
|
+
|
|
2796
|
+
# load_targets_cache <file> — parse the whole targets: section once into the
|
|
2797
|
+
# global IS_TGT_TSV (one `name<TAB>enabled<TAB>output` row per target),
|
|
2798
|
+
# matching is_target_enabled and get_target_output semantics: inline and block
|
|
2799
|
+
# forms, first occurrence wins, `enabled` defaults to 0 when the block ends
|
|
2800
|
+
# without one and to empty at end of file. The engine warms this once;
|
|
2801
|
+
# both readers consult it before spawning awk.
|
|
2802
|
+
load_targets_cache() {
|
|
2803
|
+
local file="$1"
|
|
2804
|
+
if [ "${IS_TGT_FILE:-}" = "$file" ]; then
|
|
2805
|
+
return 0
|
|
2806
|
+
fi
|
|
2807
|
+
IS_TGT_TSV="$(awk "$_IS_AWK_TARGETS" "$file")"
|
|
2808
|
+
IS_TGT_FILE="$file"
|
|
2809
|
+
}
|
|
2810
|
+
|
|
2811
|
+
# load_manifest_view <file> <field-section> <field-key> <section>... — one
|
|
2812
|
+
# awk pass for what the CLI's preflight reads of a manifest: the
|
|
2813
|
+
# load_yaml_lists cache for each <section>, the targets cache, and the one
|
|
2814
|
+
# get_yaml_field answer that get_yaml_field_var then returns without a process.
|
|
2815
|
+
# Each reader's program is the one it runs alone, so the answers cannot differ.
|
|
2816
|
+
# A caller that overlaps the pass with other work runs manifest_view_run (it
|
|
2817
|
+
# prints) and hands the output to manifest_view_store itself.
|
|
2818
|
+
load_manifest_view() {
|
|
2819
|
+
local out
|
|
2820
|
+
out="$(manifest_view_run "$@")" || return 1
|
|
2821
|
+
manifest_view_store "$out" "$@"
|
|
2822
|
+
}
|
|
2823
|
+
|
|
2824
|
+
manifest_view_run() {
|
|
2825
|
+
local file="$1" fsection="$2" fkey="$3" s
|
|
2826
|
+
shift 3
|
|
2827
|
+
for s in "$@"; do
|
|
2828
|
+
case "$s" in
|
|
2829
|
+
rules|agents|skills|ignore|submodules) ;;
|
|
2830
|
+
*) return 1 ;;
|
|
2831
|
+
esac
|
|
2832
|
+
done
|
|
2833
|
+
awk -v sections="$*" -v fsection="$fsection" -v fkey="$fkey" \
|
|
2834
|
+
-v ftag=$'\036F' -v ttag=$'\036T' \
|
|
2835
|
+
"$_IS_AWK_LISTS$_IS_AWK_FIELD$_IS_AWK_TARGETS" "$file"
|
|
2836
|
+
}
|
|
2837
|
+
|
|
2838
|
+
# manifest_view_store <output> <file> <field-section> <field-key> <section>...
|
|
2839
|
+
manifest_view_store() {
|
|
2840
|
+
local out="$1" file="$2" fsection="$3" fkey="$4" line lists="" targets="" field="" found=0
|
|
2841
|
+
local saved_ifs="$IFS"
|
|
2842
|
+
local -a lines=()
|
|
2843
|
+
shift 4
|
|
2844
|
+
IFS=$'\n'
|
|
2845
|
+
set -f
|
|
2846
|
+
# shellcheck disable=SC2206 # every row is tagged or `<section><US>`, never empty
|
|
2847
|
+
lines=($out)
|
|
2848
|
+
set +f
|
|
2849
|
+
IFS="$saved_ifs"
|
|
2850
|
+
for line in "${lines[@]+"${lines[@]}"}"; do
|
|
2851
|
+
case "$line" in
|
|
2852
|
+
$'\036T'*) targets="$targets${line#??}"$'\n' ;;
|
|
2853
|
+
$'\036F'*) field="${line#??}"; found=1 ;;
|
|
2854
|
+
*) lists="$lists$line"$'\n' ;;
|
|
2855
|
+
esac
|
|
2856
|
+
done
|
|
2857
|
+
while [[ "$targets" == *$'\n' ]]; do targets="${targets%$'\n'}"; done
|
|
2858
|
+
_yaml_lists_store "$file" "$lists" "$@"
|
|
2859
|
+
IS_TGT_TSV="$targets"
|
|
1968
2860
|
IS_TGT_FILE="$file"
|
|
2861
|
+
IS_YF_KEY="$file"$'\037'"$fsection"$'\037'"$fkey"
|
|
2862
|
+
IS_YF_VALUE="$field"
|
|
2863
|
+
IS_YF_FOUND="$found"
|
|
2864
|
+
}
|
|
2865
|
+
|
|
2866
|
+
# manifest_view_reset — forget every manifest answer cached in this shell. A
|
|
2867
|
+
# process that has the manifest rewritten (by a child it ran) calls it before
|
|
2868
|
+
# reading again.
|
|
2869
|
+
manifest_view_reset() {
|
|
2870
|
+
local s
|
|
2871
|
+
for s in rules agents skills ignore submodules; do
|
|
2872
|
+
unset "IS_YL_${s}_FILE" "IS_YL_${s}_VAL"
|
|
2873
|
+
done
|
|
2874
|
+
unset IS_TGT_FILE IS_TGT_TSV IS_YF_KEY IS_YF_VALUE IS_YF_FOUND IS_YL_UNRESOLVED
|
|
1969
2875
|
}
|
|
1970
2876
|
|
|
2877
|
+
|
|
1971
2878
|
# _targets_cache_row <target> — scan the cached TSV; sets IS_TGT_ROW_ENABLED /
|
|
1972
2879
|
# IS_TGT_ROW_OUTPUT. Returns 1 when the target has no row (reader falls back
|
|
1973
2880
|
# to empty, same as the awk scan finding nothing).
|
|
@@ -2189,18 +3096,23 @@ get_yaml_field() {
|
|
|
2189
3096
|
local file="$1"
|
|
2190
3097
|
local section="$2"
|
|
2191
3098
|
local key="$3"
|
|
2192
|
-
|
|
2193
|
-
|
|
2194
|
-
|
|
2195
|
-
|
|
2196
|
-
|
|
2197
|
-
|
|
2198
|
-
|
|
2199
|
-
|
|
2200
|
-
|
|
2201
|
-
|
|
2202
|
-
|
|
2203
|
-
|
|
3099
|
+
if [ "${IS_YF_KEY:-}" = "$file"$'\037'"$section"$'\037'"$key" ]; then
|
|
3100
|
+
[ "$IS_YF_FOUND" != 1 ] || printf '%s\n' "$IS_YF_VALUE"
|
|
3101
|
+
return 0
|
|
3102
|
+
fi
|
|
3103
|
+
awk -v fsection="$section" -v fkey="$key" "$_IS_AWK_FIELD" "$file"
|
|
3104
|
+
}
|
|
3105
|
+
|
|
3106
|
+
# get_yaml_field_var <file> <section> <key> — get_yaml_field into
|
|
3107
|
+
# IS_YAML_FIELD, without a process when load_manifest_view read it.
|
|
3108
|
+
# shellcheck disable=SC2034 # IS_YAML_FIELD is the return channel
|
|
3109
|
+
get_yaml_field_var() {
|
|
3110
|
+
if [ "${IS_YF_KEY:-}" = "$1"$'\037'"$2"$'\037'"$3" ]; then
|
|
3111
|
+
IS_YAML_FIELD=""
|
|
3112
|
+
[ "$IS_YF_FOUND" != 1 ] || IS_YAML_FIELD="$IS_YF_VALUE"
|
|
3113
|
+
return 0
|
|
3114
|
+
fi
|
|
3115
|
+
IS_YAML_FIELD="$(get_yaml_field "$@")"
|
|
2204
3116
|
}
|
|
2205
3117
|
|
|
2206
3118
|
# Get project name from config.yaml (project.name)
|