@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/cli/lib/gitignore.sh
CHANGED
|
@@ -13,12 +13,13 @@ IS_CR=$'\r'
|
|
|
13
13
|
IS_LF=$'\n'
|
|
14
14
|
IS_CRLF=$'\r\n'
|
|
15
15
|
|
|
16
|
-
# An ignore file checked out on Windows can hold CRLF
|
|
17
|
-
#
|
|
18
|
-
#
|
|
19
|
-
#
|
|
20
|
-
#
|
|
21
|
-
#
|
|
16
|
+
# An ignore file checked out on Windows can hold CRLF. Git drops one trailing
|
|
17
|
+
# CR from each line before matching, so `foo` and `foo<CR>` are the same rule:
|
|
18
|
+
# these helpers decide presence ignoring that CR, and append in whatever ending
|
|
19
|
+
# the file already uses. Comparing with the CR attached made every existing
|
|
20
|
+
# line look absent on Linux and macOS (where it is part of the line, unlike
|
|
21
|
+
# under MSYS), so each alignment appended an LF copy of a line that was already
|
|
22
|
+
# there and the file grew forever.
|
|
22
23
|
|
|
23
24
|
# ignore_file_eol_var <file> — set IS_IGNORE_EOL from the file's first line.
|
|
24
25
|
ignore_file_eol_var() {
|
|
@@ -32,12 +33,19 @@ ignore_file_eol_var() {
|
|
|
32
33
|
}
|
|
33
34
|
|
|
34
35
|
# ignore_file_has_line <file> <line> — true when the file lists exactly this
|
|
35
|
-
# line, with or without a trailing CR.
|
|
36
|
+
# line, with or without a trailing CR. The one presence test for an ignore
|
|
37
|
+
# file: the writer and `status --check` both ask it, so a line one appends is a
|
|
38
|
+
# line the other reports present. Decided in bash, because an exact-line grep
|
|
39
|
+
# answers per platform — MSYS grep never sees the CR, Linux and macOS grep keep
|
|
40
|
+
# it as part of the line.
|
|
36
41
|
ignore_file_has_line() {
|
|
37
|
-
local file="$1" line="$2"
|
|
42
|
+
local file="$1" line="$2" entry=""
|
|
38
43
|
[ -f "$file" ] || return 1
|
|
39
|
-
|
|
40
|
-
|
|
44
|
+
while IFS= read -r entry || [ -n "$entry" ]; do
|
|
45
|
+
[ "${entry%"$IS_CR"}" = "$line" ] && return 0
|
|
46
|
+
entry=""
|
|
47
|
+
done < "$file"
|
|
48
|
+
return 1
|
|
41
49
|
}
|
|
42
50
|
|
|
43
51
|
# ignore_file_append_line <file> <line> — append in the file's own ending.
|
|
@@ -88,29 +96,17 @@ gitignore_collapse_duplicates() {
|
|
|
88
96
|
local root="$1" probe="$2"; shift 2
|
|
89
97
|
local file="$root/.gitignore" tmp backup before=0 after=0
|
|
90
98
|
local -a lines=() managed=("$@") last_idx=()
|
|
91
|
-
local
|
|
99
|
+
local key i mi n hdr keep final_newline
|
|
92
100
|
[ -f "$file" ] || return 0
|
|
93
101
|
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
# CRLF file as LF. A line's CR is significant to Git, and either way the
|
|
97
|
-
# lines this function does not remove must survive byte-for-byte.
|
|
98
|
-
line=""
|
|
99
|
-
while IFS= read -r line; do
|
|
100
|
-
lines[${#lines[@]}]="$line"
|
|
101
|
-
line=""
|
|
102
|
-
done < "$file"
|
|
103
|
-
# `read` returns 1 at EOF but still fills `line` when the file's last line
|
|
104
|
-
# carries no newline; preserve that ending instead of adding one.
|
|
105
|
-
if [ -n "$line" ]; then
|
|
106
|
-
lines[${#lines[@]}]="$line"
|
|
107
|
-
final_newline=0
|
|
108
|
-
fi
|
|
109
|
-
n=${#lines[@]}
|
|
110
|
-
for ((i = 0; i < n; i++)); do
|
|
111
|
-
if [ "${lines[i]%"$IS_CR"}" = "$IS_GITIGNORE_HEADER" ]; then hdr=$i; break; fi
|
|
112
|
-
done
|
|
102
|
+
gitignore_read_var "$file"
|
|
103
|
+
hdr=$IS_GI_HEADER
|
|
113
104
|
[ "$hdr" -ge 0 ] || return 0
|
|
105
|
+
# A header was found, so the array is not empty — Bash 3.2 under `set -u`
|
|
106
|
+
# cannot expand an empty one.
|
|
107
|
+
lines=("${IS_GI_LINES[@]}")
|
|
108
|
+
final_newline=$IS_GI_FINAL_NEWLINE
|
|
109
|
+
n=${#lines[@]}
|
|
114
110
|
|
|
115
111
|
# Where each managed line last occurs below the header. Indexed arrays and
|
|
116
112
|
# arithmetic `for` only — Bash 3.2 has no associative arrays.
|
|
@@ -161,6 +157,97 @@ gitignore_collapse_duplicates() {
|
|
|
161
157
|
rm -f "$backup"
|
|
162
158
|
}
|
|
163
159
|
|
|
160
|
+
# gitignore_read_var <file> — load the file's lines as stored, CRs included, into
|
|
161
|
+
# IS_GI_LINES. IS_GI_HEADER is the index of this policy's header (-1 when the
|
|
162
|
+
# file has none) and IS_GI_FINAL_NEWLINE is 0 when the last line ends without
|
|
163
|
+
# one. Every writer that rewrites .gitignore line by line reads it here.
|
|
164
|
+
#
|
|
165
|
+
# `read`, never awk: on Windows awk reads in text mode and would hand back every
|
|
166
|
+
# line stripped of its CR, rewriting a CRLF file as LF. The lines a writer does
|
|
167
|
+
# not remove must survive byte-for-byte, whatever their ending.
|
|
168
|
+
gitignore_read_var() {
|
|
169
|
+
local file="$1" line i
|
|
170
|
+
IS_GI_LINES=()
|
|
171
|
+
IS_GI_HEADER=-1
|
|
172
|
+
IS_GI_FINAL_NEWLINE=1
|
|
173
|
+
line=""
|
|
174
|
+
while IFS= read -r line; do
|
|
175
|
+
IS_GI_LINES[${#IS_GI_LINES[@]}]="$line"
|
|
176
|
+
line=""
|
|
177
|
+
done < "$file"
|
|
178
|
+
# `read` returns 1 at EOF but still fills `line` when the file's last line
|
|
179
|
+
# carries no newline; preserve that ending instead of adding one.
|
|
180
|
+
if [ -n "$line" ]; then
|
|
181
|
+
IS_GI_LINES[${#IS_GI_LINES[@]}]="$line"
|
|
182
|
+
IS_GI_FINAL_NEWLINE=0
|
|
183
|
+
fi
|
|
184
|
+
for ((i = 0; i < ${#IS_GI_LINES[@]}; i++)); do
|
|
185
|
+
if [ "${IS_GI_LINES[i]%"$IS_CR"}" = "$IS_GITIGNORE_HEADER" ]; then
|
|
186
|
+
IS_GI_HEADER=$i
|
|
187
|
+
break
|
|
188
|
+
fi
|
|
189
|
+
done
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
# gitignore_managed_has_line <root> <line> — true when the region this policy
|
|
193
|
+
# owns (its header to end of file) lists the line, with or without a CR.
|
|
194
|
+
gitignore_managed_has_line() {
|
|
195
|
+
local file="$1/.gitignore" want="$2" i
|
|
196
|
+
[ -f "$file" ] || return 1
|
|
197
|
+
gitignore_read_var "$file"
|
|
198
|
+
[ "$IS_GI_HEADER" -ge 0 ] || return 1
|
|
199
|
+
for ((i = IS_GI_HEADER + 1; i < ${#IS_GI_LINES[@]}; i++)); do
|
|
200
|
+
if [ "${IS_GI_LINES[i]%"$IS_CR"}" = "$want" ]; then
|
|
201
|
+
return 0
|
|
202
|
+
fi
|
|
203
|
+
done
|
|
204
|
+
return 1
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
# gitignore_remove_managed_lines <root> <line…> — take every copy of these lines
|
|
208
|
+
# out of the region this policy owns. A contract names them with `unignore` when
|
|
209
|
+
# the project's configuration keeps tracked what the default policy ignores
|
|
210
|
+
# (targets.copilot.commit_output), so the ignore an earlier alignment wrote is
|
|
211
|
+
# withdrawn by the writer that wrote it, not by a hand edit. Lines above the
|
|
212
|
+
# header predate Intelligence and stay — a hand-written copy of the same
|
|
213
|
+
# pattern included — and every kept line keeps its bytes and its own ending:
|
|
214
|
+
# only the file's last line can lack a newline, and none is added to it.
|
|
215
|
+
gitignore_remove_managed_lines() {
|
|
216
|
+
local root="$1"; shift
|
|
217
|
+
local file="$root/.gitignore" tmp key i mi n dropped=0
|
|
218
|
+
local -a remove=("$@") keep=()
|
|
219
|
+
[ "$#" -gt 0 ] && [ -f "$file" ] || return 0
|
|
220
|
+
gitignore_read_var "$file"
|
|
221
|
+
[ "$IS_GI_HEADER" -ge 0 ] || return 0
|
|
222
|
+
n=${#IS_GI_LINES[@]}
|
|
223
|
+
for ((i = 0; i < n; i++)); do
|
|
224
|
+
keep[i]=1
|
|
225
|
+
[ "$i" -gt "$IS_GI_HEADER" ] || continue
|
|
226
|
+
key="${IS_GI_LINES[i]%"$IS_CR"}"
|
|
227
|
+
for ((mi = 0; mi < ${#remove[@]}; mi++)); do
|
|
228
|
+
if [ "$key" = "${remove[mi]}" ]; then
|
|
229
|
+
keep[i]=0
|
|
230
|
+
dropped=1
|
|
231
|
+
break
|
|
232
|
+
fi
|
|
233
|
+
done
|
|
234
|
+
done
|
|
235
|
+
[ "$dropped" -eq 1 ] || return 0
|
|
236
|
+
|
|
237
|
+
tmp="$file.cli.tmp"
|
|
238
|
+
{
|
|
239
|
+
for ((i = 0; i < n; i++)); do
|
|
240
|
+
[ "${keep[i]}" -eq 1 ] || continue
|
|
241
|
+
if [ "$i" -eq $((n - 1)) ] && [ "$IS_GI_FINAL_NEWLINE" -eq 0 ]; then
|
|
242
|
+
printf '%s' "${IS_GI_LINES[i]}"
|
|
243
|
+
else
|
|
244
|
+
printf '%s\n' "${IS_GI_LINES[i]}"
|
|
245
|
+
fi
|
|
246
|
+
done
|
|
247
|
+
} > "$tmp"
|
|
248
|
+
mv -- "$tmp" "$file"
|
|
249
|
+
}
|
|
250
|
+
|
|
164
251
|
# Git cannot re-include a child of an excluded directory, so an include needs an
|
|
165
252
|
# explicit negation for every parent. A negation only wins when it comes after
|
|
166
253
|
# the rule excluding the parent, and the only repair an append can make is to
|
|
@@ -233,18 +320,25 @@ ensure_base_gitignore() {
|
|
|
233
320
|
|
|
234
321
|
ensure_target_gitignore() {
|
|
235
322
|
local root="$1" manifest="$2" target="$3" content_dir output kind value records
|
|
323
|
+
local -a unignored=()
|
|
236
324
|
ensure_gitignore_header "$root"
|
|
237
325
|
content_dir="$(manifest_intelligence_dir "$manifest")"
|
|
238
326
|
output="$(get_target_output "$manifest" "$target")"
|
|
239
327
|
[ -n "$output" ] || output="$(default_target_output "$target")"
|
|
240
|
-
records="$(adapter_records_for "$root" "$content_dir" "$target" "$output")" \
|
|
328
|
+
records="$(adapter_records_for "$root" "$content_dir" "$target" "$output" "$manifest")" \
|
|
241
329
|
|| die "adapter '$target' has an invalid ownership contract"
|
|
242
330
|
while IFS=$'\t' read -r kind value; do
|
|
243
331
|
case "$kind" in
|
|
244
|
-
ignore)
|
|
245
|
-
include)
|
|
332
|
+
ignore) gitignore_add_line "$root" "$value" ;;
|
|
333
|
+
include) gitignore_add_effective_include "$root" "$value" ;;
|
|
334
|
+
unignore) unignored+=("$value") ;;
|
|
246
335
|
esac
|
|
247
336
|
done <<< "$records"
|
|
337
|
+
# One rewrite for all of them; Bash 3.2 cannot expand an empty array under
|
|
338
|
+
# `set -u`, hence the count guard.
|
|
339
|
+
if [ "${#unignored[@]}" -gt 0 ]; then
|
|
340
|
+
gitignore_remove_managed_lines "$root" "${unignored[@]}"
|
|
341
|
+
fi
|
|
248
342
|
}
|
|
249
343
|
|
|
250
344
|
ensure_manifest_gitignore() {
|
|
@@ -331,7 +425,7 @@ managed_gitignore_patterns() {
|
|
|
331
425
|
[ "$(is_target_enabled "$manifest" "$target")" = "1" ] || continue
|
|
332
426
|
output="$(get_target_output "$manifest" "$target")"
|
|
333
427
|
[ -n "$output" ] || output="$(default_target_output "$target")"
|
|
334
|
-
records="$(adapter_records_for "$root" "$content_dir" "$target" "$output")" \
|
|
428
|
+
records="$(adapter_records_for "$root" "$content_dir" "$target" "$output" "$manifest")" \
|
|
335
429
|
|| return 1
|
|
336
430
|
while IFS=$'\t' read -r kind value; do
|
|
337
431
|
case "$kind" in
|
package/cli/lib/manifest.sh
CHANGED
|
@@ -2,9 +2,10 @@
|
|
|
2
2
|
# The CLI-owned YAML shapes: quoted-key maps (`packages:`, `registries:` in
|
|
3
3
|
# the manifest, `packages:` in the lock and in registry indexes).
|
|
4
4
|
#
|
|
5
|
-
# The engine
|
|
6
|
-
#
|
|
7
|
-
#
|
|
5
|
+
# The engine reads none of these blocks beyond the names and aliases in
|
|
6
|
+
# `packages:`, which resolve package references in `sources:`
|
|
7
|
+
# (engine/lib/common.sh), and its list readers cannot hold `@scope/name` keys —
|
|
8
|
+
# so this is the CLI's single, deliberate parser, scoped to exactly one shape:
|
|
8
9
|
#
|
|
9
10
|
# block:
|
|
10
11
|
# "@scope/name": # 2-space indent, key always quoted
|
|
@@ -407,12 +408,14 @@ registries_remove() {
|
|
|
407
408
|
# meaning: a later entry overrides a same-named artifact from an earlier one.
|
|
408
409
|
# Reading goes through the engine's own list parser, so the CLI sees exactly
|
|
409
410
|
# what the engine will render instead of a second reading of the same file.
|
|
411
|
+
# The editors read it raw — a package reference as written, which is what they
|
|
412
|
+
# compare and rewrite — and read_source_entries says what each one renders.
|
|
410
413
|
|
|
411
|
-
# sources_list_entries <file> <section> — entries of sources.<section
|
|
412
|
-
# line, in manifest order.
|
|
414
|
+
# sources_list_entries <file> <section> — entries of sources.<section> as
|
|
415
|
+
# written, one per line, in manifest order.
|
|
413
416
|
sources_list_entries() {
|
|
414
417
|
[ -f "$1" ] || return 0
|
|
415
|
-
|
|
418
|
+
read_yaml_list_raw "$1" "$2"
|
|
416
419
|
}
|
|
417
420
|
|
|
418
421
|
# sources_has_entry <file> <section> <entry> — true (0) when that section lists
|
|
@@ -439,7 +442,7 @@ sources_has_entry() {
|
|
|
439
442
|
sources_add_entry() {
|
|
440
443
|
local file="$1" section="$2" entry="$3" pos="${4:-last}" anchor="${5:-}"
|
|
441
444
|
sources_has_entry "$file" "$section" "$entry" && return 0
|
|
442
|
-
_qmap_stage "$file" -v section="$section" -v entry="$entry" -v pos="$pos" -v anchor="$anchor" '
|
|
445
|
+
_qmap_stage "$file" -v section="$section" -v entry="$entry" -v pos="$pos" -v anchor="$anchor" "$IS_YAML_ITEM_AWK"'
|
|
443
446
|
function line() { return " - \"" entry "\"" }
|
|
444
447
|
function place() { print line(); done = 1 }
|
|
445
448
|
# Blank lines inside sources: are held back so an insert lands next to
|
|
@@ -447,13 +450,11 @@ sources_add_entry() {
|
|
|
447
450
|
# one section from the next.
|
|
448
451
|
function flush_tail( i) { for (i = 1; i <= ntail; i++) print tail[i]; blanks = ntail; ntail = 0 }
|
|
449
452
|
function open_section() { flush_tail(); print " " section ":"; place(); if (blanks) print "" }
|
|
453
|
+
# The entry as the engine reads it (yaml_item, engine/lib/common.sh).
|
|
450
454
|
function value(s, v) {
|
|
451
455
|
v = s
|
|
452
|
-
sub(/^[ \t]
|
|
453
|
-
|
|
454
|
-
sub(/[ \t]+#.*$/, "", v)
|
|
455
|
-
sub(/[ \t]+$/, "", v)
|
|
456
|
-
return v
|
|
456
|
+
sub(/^[ \t]*-/, "", v)
|
|
457
|
+
return yaml_item(v)
|
|
457
458
|
}
|
|
458
459
|
BEGIN { anchored = (pos == "before" || pos == "after") }
|
|
459
460
|
{ sub(/\r$/, "") }
|
|
@@ -509,24 +510,171 @@ sources_add_entry_first() {
|
|
|
509
510
|
}
|
|
510
511
|
|
|
511
512
|
# sources_remove_entry <file> <section> <entry> — remove `- "entry"` from
|
|
512
|
-
# sources.<section
|
|
513
|
+
# sources.<section>, matching each item as the engine reads it (yaml_item).
|
|
513
514
|
sources_remove_entry() {
|
|
514
515
|
local file="$1" section="$2" entry="$3"
|
|
515
516
|
[ -f "$file" ] || return 0
|
|
516
|
-
_qmap_stage "$file" -v section="$section" -v entry="$entry" '
|
|
517
|
+
_qmap_stage "$file" -v section="$section" -v entry="$entry" "$IS_YAML_ITEM_AWK"'
|
|
517
518
|
{ sub(/\r$/, "") }
|
|
518
519
|
/^sources:[ \t]*$/ { ins = 1; print; next }
|
|
519
520
|
ins && /^[^ #]/ { ins = 0; insec = 0 }
|
|
520
521
|
ins && $0 ~ "^ " section ":[ \t]*$" { insec = 1; print; next }
|
|
521
522
|
ins && /^ [A-Za-z_]/ { insec = 0 }
|
|
522
|
-
insec {
|
|
523
|
+
insec && /^[ \t]*-/ {
|
|
523
524
|
line = $0
|
|
524
|
-
sub(/^[ \t]
|
|
525
|
-
|
|
526
|
-
sub(/[ \t]+#.*$/, "", line)
|
|
527
|
-
sub(/[ \t]+$/, "", line)
|
|
528
|
-
if (line == entry) next
|
|
525
|
+
sub(/^[ \t]*-/, "", line)
|
|
526
|
+
if (yaml_item(line) == entry) next
|
|
529
527
|
}
|
|
530
528
|
{ print }
|
|
531
529
|
'
|
|
532
530
|
}
|
|
531
|
+
|
|
532
|
+
# --- package references and aliases ------------------------------------------
|
|
533
|
+
# `sources:` may name a package's directory by its store path, by the package's
|
|
534
|
+
# full name or by the alias its `packages:` entry declares (decision 0019). The
|
|
535
|
+
# engine's list parser (IS_PKG_REF_AWK in engine/lib/common.sh) is the only
|
|
536
|
+
# definition of those spellings; everything below reads through it, so the CLI
|
|
537
|
+
# never judges an entry differently from the way the engine renders it. The CLI
|
|
538
|
+
# itself only ever writes store paths, and writes an alias only through
|
|
539
|
+
# `package add --alias` and `package alias`.
|
|
540
|
+
|
|
541
|
+
# assert_valid_alias <alias> — refuse an alias the engine would never resolve.
|
|
542
|
+
assert_valid_alias() {
|
|
543
|
+
pkg_alias_valid "$1" \
|
|
544
|
+
|| die "invalid alias '$1' — expected $IS_PKG_ALIAS_RULE"
|
|
545
|
+
}
|
|
546
|
+
|
|
547
|
+
# package_alias_of <file> <@scope/name> — the alias that package declares, when it
|
|
548
|
+
# is well formed; nothing otherwise (an invalid one resolves nothing).
|
|
549
|
+
package_alias_of() {
|
|
550
|
+
local name alias verdict
|
|
551
|
+
while IFS=$'\037' read -r name alias verdict; do
|
|
552
|
+
[ "$name" = "$2" ] || continue
|
|
553
|
+
[ "$verdict" = invalid ] || printf '%s' "$alias"
|
|
554
|
+
return 0
|
|
555
|
+
done < <(read_package_aliases "$1")
|
|
556
|
+
}
|
|
557
|
+
|
|
558
|
+
# assert_alias_free <file> <@scope/name> <alias> — refuse an alias another package
|
|
559
|
+
# already declares: one alias names exactly one package.
|
|
560
|
+
assert_alias_free() {
|
|
561
|
+
local file="$1" self="$2" want="$3" name alias verdict
|
|
562
|
+
while IFS=$'\037' read -r name alias verdict; do
|
|
563
|
+
[ "$name" != "$self" ] && [ "$alias" = "$want" ] || continue
|
|
564
|
+
die "alias '$want' already names $name — one alias names one package; choose another"
|
|
565
|
+
done < <(read_package_aliases "$file")
|
|
566
|
+
}
|
|
567
|
+
|
|
568
|
+
# sources_alias_users <file> <alias> — every sources entry spelled <alias>:<dir>,
|
|
569
|
+
# one `sources.<section> '<entry>'` line each.
|
|
570
|
+
sources_alias_users() {
|
|
571
|
+
local file="$1" alias="$2" section entry
|
|
572
|
+
for section in rules agents skills; do
|
|
573
|
+
while IFS= read -r entry; do
|
|
574
|
+
case "$entry" in
|
|
575
|
+
"$alias":*) printf "sources.%s '%s'\n" "$section" "$entry" ;;
|
|
576
|
+
esac
|
|
577
|
+
done < <(sources_list_entries "$file" "$section")
|
|
578
|
+
done
|
|
579
|
+
}
|
|
580
|
+
|
|
581
|
+
# assert_alias_unused <file> <@scope/name> <alias> <remove|replace> — refuse to
|
|
582
|
+
# take away an alias `sources:` still spells entries with: they would stop
|
|
583
|
+
# resolving, and the package's content would leave the outputs in silence.
|
|
584
|
+
assert_alias_unused() {
|
|
585
|
+
local file="$1" name="$2" alias="$3" verb="$4" users
|
|
586
|
+
users="$(sources_alias_users "$file" "$alias")"
|
|
587
|
+
[ -n "$users" ] || return 0
|
|
588
|
+
echo "ERROR: cannot $verb alias '$alias' of $name — sources: still uses it:" >&2
|
|
589
|
+
printf '%s\n' "$users" | sed 's/^/ /' >&2
|
|
590
|
+
echo " Spell those entries $name/<dir> instead, then $verb the alias." >&2
|
|
591
|
+
exit 1
|
|
592
|
+
}
|
|
593
|
+
|
|
594
|
+
# sources_classify_entry <file> <entry> — `<state>\037<directory>` for an entry
|
|
595
|
+
# that is not in the manifest (yet): what the engine would render it as against
|
|
596
|
+
# the packages the manifest declares. States are pkr_parse's.
|
|
597
|
+
sources_classify_entry() {
|
|
598
|
+
local file="$1"
|
|
599
|
+
[ -f "$file" ] || file=/dev/null
|
|
600
|
+
IS_PKR_CANDIDATE="$2" awk "$IS_PKG_REF_AWK"'
|
|
601
|
+
{ sub(/\r$/, ""); pkr_collect($0) }
|
|
602
|
+
END { dir = pkr_expand(ENVIRON["IS_PKR_CANDIDATE"]); print PKR_STATE "\037" dir }
|
|
603
|
+
' "$file"
|
|
604
|
+
}
|
|
605
|
+
|
|
606
|
+
# sources_find_entry <file> <section> <entry> — print the entry of
|
|
607
|
+
# sources.<section>, as written, that names the same directory as <entry> in any
|
|
608
|
+
# spelling; fail when the section names it nowhere. An entry that renders
|
|
609
|
+
# nothing matches only itself.
|
|
610
|
+
sources_find_entry() {
|
|
611
|
+
local file="$1" section="$2" entry="$3" want _state raw _st dir _rest
|
|
612
|
+
[ -f "$file" ] || return 1
|
|
613
|
+
IFS=$'\037' read -r _state want <<< "$(sources_classify_entry "$file" "$entry")"
|
|
614
|
+
while IFS=$'\037' read -r raw _st dir _rest; do
|
|
615
|
+
if [ "$raw" = "$entry" ] || { [ -n "$want" ] && [ "$dir" = "$want" ]; }; then
|
|
616
|
+
printf '%s' "$raw"
|
|
617
|
+
return 0
|
|
618
|
+
fi
|
|
619
|
+
done < <(read_source_entries "$file" "$section")
|
|
620
|
+
return 1
|
|
621
|
+
}
|
|
622
|
+
|
|
623
|
+
# package_source_listed <file> <section> <store-dir> — true when sources.<section>
|
|
624
|
+
# already names <store-dir>, in whichever spelling.
|
|
625
|
+
package_source_listed() {
|
|
626
|
+
local _raw _st dir _rest
|
|
627
|
+
[ -f "$1" ] || return 1
|
|
628
|
+
while IFS=$'\037' read -r _raw _st dir _rest; do
|
|
629
|
+
[ "$dir" = "$3" ] && return 0
|
|
630
|
+
done < <(read_source_entries "$1" "$2")
|
|
631
|
+
return 1
|
|
632
|
+
}
|
|
633
|
+
|
|
634
|
+
# sources_remove_package <file> <@scope/name> [section...] — drop the package's
|
|
635
|
+
# entries from sources:, in every spelling. With sections, only the entry of
|
|
636
|
+
# sources.<section> that names the package's own <section> directory goes;
|
|
637
|
+
# without, every rules/agents/skills entry naming a directory inside the
|
|
638
|
+
# package. The references resolve through `packages:`, so call it while the
|
|
639
|
+
# package and its alias are still declared. Every other line survives byte for
|
|
640
|
+
# byte, and an unchanged file is not rewritten.
|
|
641
|
+
sources_remove_package() {
|
|
642
|
+
local file="$1" name="$2" tmp rc=0
|
|
643
|
+
shift 2
|
|
644
|
+
[ -f "$file" ] || return 0
|
|
645
|
+
tmp="$file.cli.tmp"
|
|
646
|
+
awk -v name="$name" -v only=" $* " "$IS_PKG_REF_AWK$IS_YAML_ITEM_AWK"'
|
|
647
|
+
BEGIN { store = ".intelligence/packages/" name }
|
|
648
|
+
FNR == 1 { pass++ }
|
|
649
|
+
pass == 1 { sub(/\r$/, ""); pkr_collect($0); next }
|
|
650
|
+
{ sub(/\r$/, "") }
|
|
651
|
+
/^sources:[ \t]*$/ { ins = 1; print; next }
|
|
652
|
+
ins && /^[^ #]/ { ins = 0; sec = "" }
|
|
653
|
+
ins && /^ [A-Za-z_]/ {
|
|
654
|
+
sec = $0
|
|
655
|
+
sub(/^ /, "", sec)
|
|
656
|
+
sub(/:.*$/, "", sec)
|
|
657
|
+
if (sec != "rules" && sec != "agents" && sec != "skills") sec = ""
|
|
658
|
+
print
|
|
659
|
+
next
|
|
660
|
+
}
|
|
661
|
+
ins && sec != "" && /^[ \t]*-/ {
|
|
662
|
+
v = $0
|
|
663
|
+
sub(/^[ \t]*-/, "", v)
|
|
664
|
+
dir = pkr_expand(yaml_item(v))
|
|
665
|
+
if (only == " ") drop = dir == store || index(dir, store "/") == 1
|
|
666
|
+
else drop = index(only, " " sec " ") && dir == store "/" sec
|
|
667
|
+
if (drop) { changed = 1; next }
|
|
668
|
+
}
|
|
669
|
+
{ print }
|
|
670
|
+
END { exit (changed ? 0 : 10) }
|
|
671
|
+
' "$file" "$file" > "$tmp" || rc=$?
|
|
672
|
+
case "$rc" in
|
|
673
|
+
0)
|
|
674
|
+
[ -s "$tmp" ] || { rm -f "$tmp"; die "internal: sources edit produced an empty file for $file"; }
|
|
675
|
+
mv "$tmp" "$file"
|
|
676
|
+
;;
|
|
677
|
+
10) rm -f "$tmp" ;;
|
|
678
|
+
*) rm -f "$tmp"; die "internal: sources edit refused (awk exit $rc) for $file" ;;
|
|
679
|
+
esac
|
|
680
|
+
}
|
package/cli/lib/registry.sh
CHANGED
|
@@ -200,25 +200,30 @@ fetch_package() {
|
|
|
200
200
|
printf '%s' "$sha"
|
|
201
201
|
}
|
|
202
202
|
|
|
203
|
-
# wire_package_sources <manifest> <@scope/name> <store-rel-dir>
|
|
203
|
+
# wire_package_sources <manifest> <@scope/name> <store-rel-dir> <root>
|
|
204
204
|
# The provides convention: whichever of rules/agents/skills the installed
|
|
205
|
-
# package has becomes a sources entry — inserted FIRST in its
|
|
206
|
-
# project-owned entries come later and win on same-named artifacts.
|
|
205
|
+
# package has becomes a sources entry — its store path, inserted FIRST in its
|
|
206
|
+
# section, so project-owned entries come later and win on same-named artifacts.
|
|
207
|
+
# A section that already names that directory, in any spelling, keeps the entry
|
|
208
|
+
# where it stands: the person who wrote `@scope/name/<dir>` or `<alias>:<dir>`
|
|
209
|
+
# chose both the spelling and the position. Call it once `packages:` declares
|
|
210
|
+
# the package and its alias, which is what resolves those spellings.
|
|
207
211
|
wire_package_sources() {
|
|
208
|
-
local manifest="$1"
|
|
212
|
+
local manifest="$1" rel="$3" root="$4"
|
|
209
213
|
local section
|
|
210
214
|
for section in rules agents skills; do
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
215
|
+
[ -d "$root/$rel/$section" ] || continue
|
|
216
|
+
package_source_listed "$manifest" "$section" "$rel/$section" && continue
|
|
217
|
+
sources_add_entry_first "$manifest" "$section" "$rel/$section"
|
|
214
218
|
done
|
|
215
219
|
}
|
|
216
220
|
|
|
217
|
-
# unwire_package_sources <manifest>
|
|
221
|
+
# unwire_package_sources <manifest> <@scope/name> [section...]
|
|
222
|
+
# Drop the package's sources entries in every spelling (sources_remove_package):
|
|
223
|
+
# every one without sections, as `package remove` does; only the named
|
|
224
|
+
# sections' own directories with them, as `update` does for the directories a
|
|
225
|
+
# new version no longer has. Call it while `packages:` still declares the
|
|
226
|
+
# package and its alias.
|
|
218
227
|
unwire_package_sources() {
|
|
219
|
-
|
|
220
|
-
local section
|
|
221
|
-
for section in rules agents skills; do
|
|
222
|
-
sources_remove_entry "$manifest" "$section" "$rel/$section"
|
|
223
|
-
done
|
|
228
|
+
sources_remove_package "$@"
|
|
224
229
|
}
|
package/engine/ENGINE_SHA
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
|
|
1
|
+
b1113b0658f6fddb23017e845a4ba79401caedbf
|
package/engine/VERSION
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
0.
|
|
1
|
+
0.19.0
|
|
@@ -27,9 +27,11 @@
|
|
|
27
27
|
# has_paths(file) — check if file has paths: field
|
|
28
28
|
# get_model(config, ide, tier) — resolve model from config or default
|
|
29
29
|
# get_model_default(ide, tier) — hardcoded default for ide:tier
|
|
30
|
+
# map_effort(tool, effort) — the tool's level for a neutral effort; "" when none
|
|
30
31
|
# map_access_to_claude_tools(access) — full->"" (no tools list; inherits all), readonly->restricted
|
|
31
32
|
# map_access_to_claude_disallowed(access) — readonly->"Write, Edit", full->""
|
|
32
|
-
# read_yaml_list(manifest, section) — read a source list from intelligence.yaml
|
|
33
|
+
# read_yaml_list(manifest, section) — read a source list from intelligence.yaml,
|
|
34
|
+
# package references as their store paths
|
|
33
35
|
# get_target_field(config, target, field) — read a target's config field
|
|
34
36
|
|
|
35
37
|
# Project adapters are sourced by engine/sync.sh after the shared library is
|
|
@@ -40,7 +42,9 @@
|
|
|
40
42
|
# configured target output passed as $1. `owned` paths are exclusively managed;
|
|
41
43
|
# `managed` paths share a directory with another adapter or hand-authored files.
|
|
42
44
|
# Add `legacy` inputs to the initial backup, `preserve` for settings that must
|
|
43
|
-
# never be replaced, and explicit ignore/include records for Git policy.
|
|
45
|
+
# never be replaced, and explicit ignore/include records for Git policy. $2, when
|
|
46
|
+
# given, is the manifest: read it only to choose Git policy (ignore versus
|
|
47
|
+
# unignore), never ownership — callers that read ownership alone omit it.
|
|
44
48
|
adapter_contract_<name>() {
|
|
45
49
|
local output="${1%/}"
|
|
46
50
|
adapter_contract_version 1
|
|
@@ -216,6 +216,9 @@ adapter_contract_agents() {
|
|
|
216
216
|
adapter_contract_version 1
|
|
217
217
|
adapter_contract_owned "$output"
|
|
218
218
|
adapter_contract_legacy "AGENTS.md"
|
|
219
|
+
# No ignore record, deliberately: AGENTS.md is the tool-neutral fallback
|
|
220
|
+
# every tool reads, and a fresh clone has it only if it is committed —
|
|
221
|
+
# before anyone has run a sync.
|
|
219
222
|
}
|
|
220
223
|
|
|
221
224
|
sync_to_agents() {
|
|
@@ -3,8 +3,9 @@
|
|
|
3
3
|
# Transforms source prompts to .claude/ format
|
|
4
4
|
#
|
|
5
5
|
# Rules: copy as-is (paths: frontmatter preserved)
|
|
6
|
-
# Skills: copy skill directories in full (SKILL.md + bundled resources)
|
|
7
|
-
#
|
|
6
|
+
# Skills: copy skill directories in full (SKILL.md + bundled resources),
|
|
7
|
+
# effort -> Claude's effort level
|
|
8
|
+
# Agents: tier -> model, effort -> effort, access -> tools/disallowedTools
|
|
8
9
|
#
|
|
9
10
|
# Every per-file loop batches its work into one awk process (see the batched
|
|
10
11
|
# helpers in lib/common.sh): process spawns dominate sync time on Windows.
|
|
@@ -73,7 +74,7 @@ sync_claude_skills() {
|
|
|
73
74
|
done
|
|
74
75
|
done <<< "$list"
|
|
75
76
|
[ "${#skill_dirs[@]}" -gt 0 ] || return 0
|
|
76
|
-
|
|
77
|
+
copy_skill_bundle_dirs_for claude "$output_dir/skills" "${skill_dirs[@]}"
|
|
77
78
|
for d in "${skill_dirs[@]}"; do
|
|
78
79
|
skill_name="${d%/}"
|
|
79
80
|
echo " skill: ${skill_name##*/}"
|
|
@@ -81,7 +82,7 @@ sync_claude_skills() {
|
|
|
81
82
|
}
|
|
82
83
|
|
|
83
84
|
# Sync all agents to Claude format: one frontmatter pass resolves
|
|
84
|
-
# tier/access for every file, one transform pass writes every output.
|
|
85
|
+
# tier/effort/access for every file, one transform pass writes every output.
|
|
85
86
|
sync_claude_agents() {
|
|
86
87
|
local repo_root="$1"
|
|
87
88
|
local config_file="$2"
|
|
@@ -104,19 +105,22 @@ sync_claude_agents() {
|
|
|
104
105
|
|
|
105
106
|
load_model_tiers "$config_file" "claude"
|
|
106
107
|
|
|
107
|
-
local path tier access spec="" report=""
|
|
108
|
-
while IFS=$'\x1f' read -r path tier access; do
|
|
108
|
+
local path tier access effort spec="" report=""
|
|
109
|
+
while IFS=$'\x1f' read -r path tier access effort; do
|
|
109
110
|
[ -n "$path" ] || continue
|
|
110
111
|
resolve_model_var "$tier"
|
|
112
|
+
map_effort_var claude "$effort"
|
|
111
113
|
map_access_to_claude_tools_var "$access"
|
|
112
114
|
map_access_to_claude_disallowed_var "$access"
|
|
113
|
-
spec+="$path"$'\x1f'"$IS_MODEL"$'\x1f'"$IS_CLAUDE_TOOLS"$'\x1f'"$IS_CLAUDE_DISALLOWED"$'\n'
|
|
115
|
+
spec+="$path"$'\x1f'"$IS_MODEL"$'\x1f'"$IS_CLAUDE_TOOLS"$'\x1f'"$IS_CLAUDE_DISALLOWED"$'\x1f'"$IS_EFFORT"$'\n'
|
|
114
116
|
report+=" agent: ${path##*/} (tier=$tier -> model=$IS_MODEL, access=$access)"$'\n'
|
|
115
|
-
done < <(frontmatter_index "tier,access" "${files[@]}")
|
|
117
|
+
done < <(frontmatter_index "tier,access,effort" "${files[@]}")
|
|
116
118
|
|
|
117
119
|
# The transform used to run per file with the values injected via -v;
|
|
118
120
|
# the batched form carries them per file through the environment (awk -v
|
|
119
|
-
# would reprocess backslash escapes).
|
|
121
|
+
# would reprocess backslash escapes). The first frontmatter `effort:` line
|
|
122
|
+
# becomes Claude's level in place, or goes when there is none; a later
|
|
123
|
+
# one goes too, since frontmatter_index read only the first.
|
|
120
124
|
is_fin_awk_vars
|
|
121
125
|
IS_MAP_SPEC="$spec" awk "${IS_FIN_V[@]}" -v dst="$output_dir/agents" "$IS_AWK_LIB"'
|
|
122
126
|
BEGIN {
|
|
@@ -125,18 +129,25 @@ sync_claude_agents() {
|
|
|
125
129
|
for (i = 1; i <= n; i++) {
|
|
126
130
|
if (recs[i] == "") continue
|
|
127
131
|
split(recs[i], f, US)
|
|
128
|
-
MODEL[f[1]] = f[2]; TOOLS[f[1]] = f[3]; EXTRA[f[1]] = f[4]
|
|
132
|
+
MODEL[f[1]] = f[2]; TOOLS[f[1]] = f[3]; EXTRA[f[1]] = f[4]; EFFORT[f[1]] = f[5]
|
|
129
133
|
}
|
|
130
134
|
}
|
|
131
135
|
FNR == 1 {
|
|
132
136
|
if (out != "") close(out)
|
|
133
137
|
out = dst "/" base_name(FILENAME)
|
|
134
138
|
count = 0
|
|
139
|
+
fm = 0
|
|
140
|
+
effort_seen = 0
|
|
135
141
|
}
|
|
136
142
|
/^tier:/ { next }
|
|
137
143
|
/^access:/ { next }
|
|
138
144
|
{ sub(/\r$/, "") }
|
|
145
|
+
FNR == 1 && $0 == "---" { fm = 1 }
|
|
139
146
|
/^---$/ { count++ }
|
|
147
|
+
fm && count == 1 && substr($0, 1, 7) == "effort:" {
|
|
148
|
+
if (!effort_seen++ && EFFORT[FILENAME] != "") print fin_line("effort: " EFFORT[FILENAME]) > out
|
|
149
|
+
next
|
|
150
|
+
}
|
|
140
151
|
count == 2 && /^---$/ {
|
|
141
152
|
if (TOOLS[FILENAME] != "") print fin_line("tools: " TOOLS[FILENAME]) > out
|
|
142
153
|
if (EXTRA[FILENAME] != "") print fin_line("disallowedTools: " EXTRA[FILENAME]) > out
|