@ainova-systems/intelligence 0.18.1 → 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 +418 -50
- 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 +44 -7
- 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
|
|
|
@@ -831,16 +861,36 @@ enforce_authored_invocation_policies() {
|
|
|
831
861
|
# Print warnings to stderr; do not fail. Strict consumers (Codex CLI) reject
|
|
832
862
|
# these files with cryptic messages — catching them in sync gives better DX.
|
|
833
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).
|
|
834
870
|
# Usage: lint_frontmatter_files "a.md" "b.md" ...
|
|
835
871
|
lint_frontmatter_files() {
|
|
836
872
|
[ "$#" -gt 0 ] || return 0
|
|
837
|
-
awk '
|
|
838
|
-
|
|
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 }
|
|
839
883
|
{ sub(/\r$/, "") }
|
|
840
884
|
done { next }
|
|
841
885
|
FNR == 1 && $0 != "---" { done = 1; next }
|
|
842
886
|
FNR == 1 { in_fm = 1; next }
|
|
843
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
|
+
}
|
|
844
894
|
in_fm && /^\t/ {
|
|
845
895
|
printf " WARN: %s:%d leading tab in frontmatter (use spaces)\n", FILENAME, FNR > "/dev/stderr"
|
|
846
896
|
}
|
|
@@ -994,8 +1044,8 @@ get_model_default_var() {
|
|
|
994
1044
|
cursor:heavy) IS_MODEL_DEFAULT="inherit" ;;
|
|
995
1045
|
cursor:standard) IS_MODEL_DEFAULT="inherit" ;;
|
|
996
1046
|
cursor:light) IS_MODEL_DEFAULT="inherit" ;;
|
|
997
|
-
# GPT-6 has no mid model, so frontier and heavy share Astra
|
|
998
|
-
#
|
|
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.
|
|
999
1049
|
copilot:frontier) IS_MODEL_DEFAULT="gpt-6-astra" ;;
|
|
1000
1050
|
copilot:heavy) IS_MODEL_DEFAULT="gpt-6-astra" ;;
|
|
1001
1051
|
copilot:standard) IS_MODEL_DEFAULT="gpt-6.1-sol" ;;
|
|
@@ -1132,7 +1182,7 @@ load_model_tiers() {
|
|
|
1132
1182
|
# the tool has no default for and the manifest overrides nowhere — a typo, as a
|
|
1133
1183
|
# rule — still renders an empty model, but never silently: the run warns once
|
|
1134
1184
|
# per tool and tier, unindented on purpose because `sync --compact` keeps only
|
|
1135
|
-
# `WARNING:` lines.
|
|
1185
|
+
# `WARNING:` lines (decision 0016).
|
|
1136
1186
|
# shellcheck disable=SC2034 # IS_MODEL is the return channel read by adapters
|
|
1137
1187
|
resolve_model_var() {
|
|
1138
1188
|
case "$1" in
|
|
@@ -1242,6 +1292,64 @@ map_access_to_claude_disallowed() {
|
|
|
1242
1292
|
echo "$IS_CLAUDE_DISALLOWED"
|
|
1243
1293
|
}
|
|
1244
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
|
+
|
|
1245
1353
|
# --- Validation ---
|
|
1246
1354
|
|
|
1247
1355
|
# Lexically canonicalize a path: collapse `//`, `.` and `..` by pure string
|
|
@@ -1787,9 +1895,213 @@ report_context_source_sizes() {
|
|
|
1787
1895
|
|
|
1788
1896
|
# --- Config Parsing ---
|
|
1789
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
|
+
|
|
1790
2097
|
# Read a simple list from config.yaml
|
|
1791
2098
|
# Format: key:\n - "value1"\n - "value2"
|
|
1792
|
-
# 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.
|
|
1793
2105
|
#
|
|
1794
2106
|
# Consults the load_yaml_list cache first: sync reads the same sections from
|
|
1795
2107
|
# the same manifest dozens of times, and each awk spawn costs tens of
|
|
@@ -1813,27 +2125,67 @@ read_yaml_list() {
|
|
|
1813
2125
|
fi
|
|
1814
2126
|
;;
|
|
1815
2127
|
esac
|
|
1816
|
-
awk -v section="$section"
|
|
1817
|
-
|
|
1818
|
-
|
|
1819
|
-
|
|
1820
|
-
|
|
1821
|
-
|
|
1822
|
-
|
|
1823
|
-
|
|
1824
|
-
|
|
1825
|
-
|
|
1826
|
-
|
|
1827
|
-
|
|
1828
|
-
|
|
1829
|
-
|
|
1830
|
-
|
|
1831
|
-
|
|
1832
|
-
|
|
1833
|
-
|
|
1834
|
-
|
|
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
|
+
}
|
|
1835
2163
|
}
|
|
1836
|
-
' "$
|
|
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
|
|
1837
2189
|
}
|
|
1838
2190
|
|
|
1839
2191
|
# load_yaml_list <file> <section> — fill the global IS_YAML_LIST with the
|
|
@@ -1865,7 +2217,10 @@ load_yaml_list() {
|
|
|
1865
2217
|
|
|
1866
2218
|
# load_yaml_lists <file> <section>... — warm the load_yaml_list cache for several
|
|
1867
2219
|
# sections in one manifest pass. Each section runs read_yaml_list's own state
|
|
1868
|
-
# 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.
|
|
1869
2224
|
load_yaml_lists() {
|
|
1870
2225
|
local file="$1" out line s v j
|
|
1871
2226
|
shift
|
|
@@ -1875,9 +2230,9 @@ load_yaml_lists() {
|
|
|
1875
2230
|
*) return 1 ;;
|
|
1876
2231
|
esac
|
|
1877
2232
|
done
|
|
1878
|
-
out="$(awk -v sections="$*" '
|
|
2233
|
+
out="$(awk -v sections="$*" "$IS_PKG_REF_AWK$IS_YAML_ITEM_AWK"'
|
|
1879
2234
|
BEGIN { n = split(sections, want, " ") }
|
|
1880
|
-
{ sub(/\r$/, "") }
|
|
2235
|
+
{ sub(/\r$/, ""); pkr_collect($0) }
|
|
1881
2236
|
{
|
|
1882
2237
|
for (k = 1; k <= n; k++) {
|
|
1883
2238
|
s = want[k]
|
|
@@ -1886,19 +2241,26 @@ load_yaml_lists() {
|
|
|
1886
2241
|
if ($0 ~ "^" s ":") { cur[k] = s; dep[k] = 0; continue }
|
|
1887
2242
|
if ($0 ~ "^ " s ":") { cur[k] = s; dep[k] = 2; continue }
|
|
1888
2243
|
if (cur[k] == s && dep[k] == 0 && /^ - /) {
|
|
1889
|
-
|
|
1890
|
-
|
|
1891
|
-
gsub(/["\047]/, "", val)
|
|
1892
|
-
print s "\037" val
|
|
2244
|
+
got_s[++ng] = s
|
|
2245
|
+
got_v[ng] = yaml_item(substr($0, 5))
|
|
1893
2246
|
}
|
|
1894
2247
|
if (cur[k] == s && dep[k] == 2 && /^ - /) {
|
|
1895
|
-
|
|
1896
|
-
|
|
1897
|
-
gsub(/["\047]/, "", val)
|
|
1898
|
-
print s "\037" val
|
|
2248
|
+
got_s[++ng] = s
|
|
2249
|
+
got_v[ng] = yaml_item(substr($0, 7))
|
|
1899
2250
|
}
|
|
1900
2251
|
}
|
|
1901
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
|
+
}
|
|
1902
2264
|
' "$file")"
|
|
1903
2265
|
for s in "$@"; do
|
|
1904
2266
|
j=""
|
|
@@ -1911,6 +2273,12 @@ load_yaml_lists() {
|
|
|
1911
2273
|
printf -v "IS_YL_${s}_FILE" '%s' "$file"
|
|
1912
2274
|
printf -v "IS_YL_${s}_VAL" '%s' "$j"
|
|
1913
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"
|
|
1914
2282
|
}
|
|
1915
2283
|
|
|
1916
2284
|
# load_targets_cache <file> — parse the whole targets: section once into the
|
package/engine/sync.sh
CHANGED
|
@@ -102,6 +102,15 @@ echo ""
|
|
|
102
102
|
load_targets_cache "$CONFIG_FILE"
|
|
103
103
|
load_yaml_lists "$CONFIG_FILE" rules agents skills ignore submodules
|
|
104
104
|
|
|
105
|
+
# A package reference that resolves to nothing is left out of every list, so
|
|
106
|
+
# nothing renders from it — but it is a manifest error, not a directory that
|
|
107
|
+
# does not exist yet, and it is named rather than skipped in silence.
|
|
108
|
+
while IFS=$'\037' read -r section src state _dir alias holders; do
|
|
109
|
+
[ -n "$section" ] || continue
|
|
110
|
+
source_reference_problem_var "$state" "$alias" "$holders"
|
|
111
|
+
echo "WARNING: sources.$section '$src' $IS_SOURCE_PROBLEM — skipped; 'intelligence status --check' reports it" >&2
|
|
112
|
+
done <<< "${IS_YL_UNRESOLVED:-}"
|
|
113
|
+
|
|
105
114
|
# Lint frontmatter across all source files (rules, agents, skills).
|
|
106
115
|
# Catches issues like unquoted colons that strict YAML consumers reject.
|
|
107
116
|
LINT_FILES=()
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@ainova-systems/intelligence",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.19.0",
|
|
4
4
|
"description": "Build, version and distribute AI agent intelligence across your organization — one CLI, versioned Intelligence Packages, and a sync engine for Claude Code, Cursor, Copilot, Codex, Pi and OpenCode.",
|
|
5
5
|
"bin": {
|
|
6
6
|
"intelligence": "bin/intelligence.js"
|