@ainova-systems/intelligence 0.22.0 → 0.22.1
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/intelligence +6 -0
- package/cli/lib/sync-cache.sh +162 -38
- package/cli/lib/sync-lock.sh +40 -0
- package/engine/ENGINE_SHA +1 -1
- package/engine/VERSION +1 -1
- package/engine/sync.sh +6 -2
- package/package.json +1 -1
package/cli/intelligence
CHANGED
|
@@ -7,6 +7,12 @@
|
|
|
7
7
|
|
|
8
8
|
set -euo pipefail
|
|
9
9
|
|
|
10
|
+
# Bash keeps a PWD inherited from its parent when it names the current
|
|
11
|
+
# directory, so a Windows caller can hand over `D:/x` or `D:\x` for what every
|
|
12
|
+
# other caller sees as `/d/x`. Taken once here, one spelling gives the project
|
|
13
|
+
# one root and the CLI one location, whoever started it.
|
|
14
|
+
case "$PWD" in [A-Za-z]:*) cd -P . ;; esac
|
|
15
|
+
|
|
10
16
|
case "$0" in
|
|
11
17
|
*/*) CLI_DIR="${0%/*}"; [ -n "$CLI_DIR" ] || CLI_DIR="/" ;;
|
|
12
18
|
*) CLI_DIR="." ;;
|
package/cli/lib/sync-cache.sh
CHANGED
|
@@ -4,19 +4,19 @@
|
|
|
4
4
|
#
|
|
5
5
|
# The record keeps three fingerprints apart (decision 0018), so a check can say
|
|
6
6
|
# which side moved:
|
|
7
|
-
# tooling the CLI and engine code
|
|
8
|
-
#
|
|
9
|
-
#
|
|
10
|
-
# than answer "sync needed" from it.
|
|
7
|
+
# tooling the CLI and engine code and the adapter filter. A change here is
|
|
8
|
+
# not the project's: a check renders rather than answer "sync
|
|
9
|
+
# needed" from it.
|
|
11
10
|
# inputs the manifest, the lock, every configured source including skill
|
|
12
11
|
# resources and installed packages, and the project adapter folder.
|
|
13
12
|
# outputs every path the enabled built-in adapters own or manage.
|
|
14
|
-
# Each fingerprint lists names, entry kinds and executable state beside
|
|
15
|
-
# hashes; timestamps
|
|
13
|
+
# Each fingerprint lists relative names, entry kinds and executable state beside
|
|
14
|
+
# content hashes; timestamps, the project's location and the caller's
|
|
15
|
+
# environment never count. `.intelligence/sync-cache/state` holds the
|
|
16
16
|
# record and the report a hit replays; `seal` holds the state file's own hash,
|
|
17
17
|
# so a record is trusted only whole.
|
|
18
18
|
|
|
19
|
-
SC_FORMAT='intelligence-sync-cache-
|
|
19
|
+
SC_FORMAT='intelligence-sync-cache-v3'
|
|
20
20
|
SC_SAFE_SEEN=$'\n'
|
|
21
21
|
|
|
22
22
|
sync_cache_safe_path() {
|
|
@@ -155,6 +155,19 @@ sync_cache_dependencies() {
|
|
|
155
155
|
}
|
|
156
156
|
|
|
157
157
|
# --- Fingerprint ---------------------------------------------------------------
|
|
158
|
+
# sync_cache_label_var <path> — IS_SC_LABEL, the name a fingerprint records for
|
|
159
|
+
# a path: relative to the engine or CLI directory for tooling, to the project
|
|
160
|
+
# otherwise. Where the project or the CLI lives, and how that path is spelled,
|
|
161
|
+
# never decides whether the same files are current.
|
|
162
|
+
sync_cache_label_var() {
|
|
163
|
+
case "$1" in
|
|
164
|
+
"$IS_ENGINE_DIR"|"$IS_ENGINE_DIR"/*) IS_SC_LABEL="engine${1#"$IS_ENGINE_DIR"}" ;;
|
|
165
|
+
"$CLI_DIR"|"$CLI_DIR"/*) IS_SC_LABEL="cli${1#"$CLI_DIR"}" ;;
|
|
166
|
+
"$REPO_ROOT"/*) IS_SC_LABEL="${1#"$REPO_ROOT/"}" ;;
|
|
167
|
+
*) IS_SC_LABEL="$1" ;;
|
|
168
|
+
esac
|
|
169
|
+
}
|
|
170
|
+
|
|
158
171
|
# _sync_cache_starts <group> <path>... — queue a group's start points. A missing
|
|
159
172
|
# one is recorded as missing; a repeated one is walked once.
|
|
160
173
|
_sync_cache_starts() {
|
|
@@ -171,7 +184,8 @@ _sync_cache_starts() {
|
|
|
171
184
|
SC_STARTS+=("$path")
|
|
172
185
|
SC_START_GROUPS+=("$group")
|
|
173
186
|
else
|
|
174
|
-
|
|
187
|
+
sync_cache_label_var "$path"
|
|
188
|
+
SC_MISSING+=("$group missing $IS_SC_LABEL")
|
|
175
189
|
fi
|
|
176
190
|
done
|
|
177
191
|
}
|
|
@@ -190,7 +204,7 @@ _SC_AWK_LISTING='
|
|
|
190
204
|
starts = split(ENVIRON["SC_AWK_STARTS"], rows, "\n")
|
|
191
205
|
for (i = 1; i <= starts; i++) {
|
|
192
206
|
split(rows[i], field, "\t")
|
|
193
|
-
group[i] = field[1]; posix[i] = field[2]; native[i] = field[3]
|
|
207
|
+
group[i] = field[1]; posix[i] = field[2]; native[i] = field[3]; label[i] = field[4]
|
|
194
208
|
}
|
|
195
209
|
probe = ENVIRON["SC_AWK_EXEC_PROBE"]
|
|
196
210
|
stream = (ENVIRON["SC_AWK_STREAM"] == 1)
|
|
@@ -209,7 +223,7 @@ _SC_AWK_LISTING='
|
|
|
209
223
|
if (!last) { bad = 1; return }
|
|
210
224
|
}
|
|
211
225
|
g = group[last]
|
|
212
|
-
listing[g] = listing[g] kind " " entry "\037"
|
|
226
|
+
listing[g] = listing[g] kind " " label[last] substr(entry, length(posix[last]) + 1) "\037"
|
|
213
227
|
if (kind == "d") return
|
|
214
228
|
k = ++files[g]
|
|
215
229
|
path = native[last] substr(entry, length(posix[last]) + 1)
|
|
@@ -248,7 +262,7 @@ _SC_AWK_LISTING='
|
|
|
248
262
|
# the listing stage tells kinds without a stat per entry. git hashes the files
|
|
249
263
|
# while the walk is still running.
|
|
250
264
|
sync_cache_fingerprint() {
|
|
251
|
-
local target="$1" hashes i j n value ordered=0 starts_env="" state=""
|
|
265
|
+
local target="$1" hashes i j n value ordered=0 starts_env="" state=""
|
|
252
266
|
local tooling inputs outputs listing saved_ifs="$IFS"
|
|
253
267
|
local -a hash_lines=() expression=() keep=() starts=() groups=() natives=() parts=() count=()
|
|
254
268
|
SC_STARTS=() SC_START_GROUPS=() SC_MISSING=() SC_START_SEEN=$'\n'
|
|
@@ -299,7 +313,8 @@ sync_cache_fingerprint() {
|
|
|
299
313
|
for ((j = 0; j < ${#starts[@]}; j++)); do
|
|
300
314
|
value="${starts[j]}"
|
|
301
315
|
[ "$SC_TRANSLATE" != 1 ] || value="${natives[j]}"
|
|
302
|
-
|
|
316
|
+
sync_cache_label_var "${starts[j]}"
|
|
317
|
+
starts_env="$starts_env${groups[j]}"$'\t'"${starts[j]}"$'\t'"$value"$'\t'"$IS_SC_LABEL"$'\n'
|
|
303
318
|
done
|
|
304
319
|
starts_env="${starts_env%$'\n'}"
|
|
305
320
|
expression=(\( ! -type d ! -type f -exec false {} + \)
|
|
@@ -359,15 +374,11 @@ sync_cache_fingerprint() {
|
|
|
359
374
|
[ "${count[0]}" -eq 0 ] || { printf -v value '%s\037' "${hash_lines[@]:0:${count[0]}}"; tooling="$tooling$value"; }
|
|
360
375
|
[ "${count[1]}" -eq 0 ] || { printf -v value '%s\037' "${hash_lines[@]:${count[0]}:${count[1]}}"; inputs="$inputs$value"; }
|
|
361
376
|
[ "${count[2]}" -eq 0 ] || { printf -v value '%s\037' "${hash_lines[@]:$((count[0] + count[1])):${count[2]}}"; outputs="$outputs$value"; }
|
|
362
|
-
#
|
|
363
|
-
#
|
|
364
|
-
#
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
for value in "$target" "$REPO_ROOT" "$CONFIG_FILE" "$IS_CONTENT_REL" "$IS_MODULE_REL" \
|
|
368
|
-
"$IS_SYNC_CMD" "$IS_MANIFEST_NAME" "$IS_PROTECTED_DIRS" "$BASH_VERSION" "${OSTYPE:-}" \
|
|
369
|
-
"${LANG:-}" "${LC_ALL:-}" "${LC_CTYPE:-}" "${LC_COLLATE:-}" "${PATH:-}" "$umask_value" \
|
|
370
|
-
"exec-probe=$SC_EXEC_PROBE" "walk-ordered=$ordered"; do
|
|
377
|
+
# The adapter filter selects what renders; the two listing flags say how
|
|
378
|
+
# the listings above were made. Nothing else of the invocation counts:
|
|
379
|
+
# the code, the manifest and the files decide what sync writes, wherever
|
|
380
|
+
# the project lives and whoever starts the CLI.
|
|
381
|
+
for value in "filter=$target" "exec-probe=$SC_EXEC_PROBE" "walk-ordered=$ordered"; do
|
|
371
382
|
case "$value" in *[[:cntrl:]]*) return 1 ;; esac
|
|
372
383
|
tooling="${tooling}env $value"$'\037'
|
|
373
384
|
done
|
|
@@ -514,9 +525,15 @@ sync_with_cache() {
|
|
|
514
525
|
|
|
515
526
|
# The write contract marks links whose spelling must remain unchanged: they
|
|
516
527
|
# are compared, never dereferenced, and are not renderer read inputs.
|
|
528
|
+
#
|
|
529
|
+
# SC_CHECK_OUTPUTS holds the selected adapters' paths; SC_CHECK_ALL_OUTPUTS
|
|
530
|
+
# every enabled adapter's, since a filtered render still reads what the others
|
|
531
|
+
# generated. SC_CHECK_PROJECT_ADAPTER is 1 when an enabled adapter is the
|
|
532
|
+
# project's own code.
|
|
517
533
|
sync_check_write_paths() {
|
|
518
534
|
local filter="$1" dir file name i output adapter kind value
|
|
519
535
|
local -a names=() files=() batch=()
|
|
536
|
+
SC_CHECK_PROJECT_ADAPTER=0
|
|
520
537
|
for dir in "$IS_ENGINE_DIR/adapters" "$REPO_ROOT/$IS_CONTENT_REL/adapters"; do
|
|
521
538
|
for file in "$dir"/*.sh; do
|
|
522
539
|
[ -f "$file" ] || continue
|
|
@@ -530,41 +547,142 @@ sync_check_write_paths() {
|
|
|
530
547
|
i=0
|
|
531
548
|
while [ "$i" -lt "${#names[@]}" ]; do
|
|
532
549
|
name="${names[i]}"; file="${files[i]}"; i=$((i + 1))
|
|
533
|
-
[ -z "$filter" ] || [ "$name" = "$filter" ] || continue
|
|
534
550
|
target_enabled_var "$CONFIG_FILE" "$name"
|
|
535
551
|
[ "$IS_TGT_ENABLED" = 1 ] || continue
|
|
552
|
+
case "$file" in "$REPO_ROOT/$IS_CONTENT_REL/adapters/"*) SC_CHECK_PROJECT_ADAPTER=1 ;; esac
|
|
536
553
|
target_output_var "$CONFIG_FILE" "$name"
|
|
537
554
|
output="${IS_TGT_OUTPUT:-.$name}"
|
|
538
555
|
batch+=("$name" "$file" "$output")
|
|
539
556
|
done
|
|
540
|
-
SC_CHECK_OUTPUTS=()
|
|
557
|
+
SC_CHECK_OUTPUTS=() SC_CHECK_ALL_OUTPUTS=()
|
|
541
558
|
[ "${#batch[@]}" -gt 0 ] || return 0
|
|
542
559
|
adapter_contract_records_batch "$SC_SCRATCH.check-contract" "${batch[@]}" > "$SC_SCRATCH.check-records" || return 1
|
|
543
560
|
while IFS=$'\t' read -r adapter kind value; do
|
|
544
|
-
case "$kind" in
|
|
561
|
+
case "$kind" in
|
|
562
|
+
owned|managed)
|
|
563
|
+
SC_CHECK_ALL_OUTPUTS+=("$value")
|
|
564
|
+
[ -z "$filter" ] || [ "$adapter" = "$filter" ] || continue
|
|
565
|
+
SC_CHECK_OUTPUTS+=("$value")
|
|
566
|
+
;;
|
|
567
|
+
esac
|
|
545
568
|
done < "$SC_SCRATCH.check-records"
|
|
546
569
|
}
|
|
547
570
|
|
|
571
|
+
# sync_check_unlinked_path <rel> — true when no existing component of the
|
|
572
|
+
# project path, the last included, is a link. Copying such a path alone would
|
|
573
|
+
# leave its link pointing at something the copy does not hold.
|
|
574
|
+
sync_check_unlinked_path() {
|
|
575
|
+
local rest="$1" probe="$REPO_ROOT" part
|
|
576
|
+
while [ -n "$rest" ]; do
|
|
577
|
+
part="${rest%%/*}"
|
|
578
|
+
if [ "$part" = "$rest" ]; then rest=""; else rest="${rest#*/}"; fi
|
|
579
|
+
probe="$probe/$part"
|
|
580
|
+
[ ! -L "$probe" ] || return 1
|
|
581
|
+
[ -e "$probe" ] || return 0
|
|
582
|
+
done
|
|
583
|
+
}
|
|
584
|
+
|
|
585
|
+
# sync_check_copy_paths — SC_CHECK_COPY, the existing project paths a render
|
|
586
|
+
# by built-in adapters reads or writes, none inside another. Return 1 when only
|
|
587
|
+
# a copy of the whole project is faithful: a project adapter may read any file,
|
|
588
|
+
# and a link on the way to an input may point anywhere in the project.
|
|
589
|
+
# Everything else the engine reads in the project is a directory listing the
|
|
590
|
+
# check takes from the live tree (warn_unsynced).
|
|
591
|
+
sync_check_copy_paths() {
|
|
592
|
+
local section src rel other kept
|
|
593
|
+
local -a wanted=("${CONFIG_FILE#"$REPO_ROOT/"}" intelligence.lock .intelligence/packages
|
|
594
|
+
"$IS_CONTENT_REL/adapters" .agents/rules .agents/agents)
|
|
595
|
+
SC_CHECK_COPY=()
|
|
596
|
+
[ "$SC_CHECK_PROJECT_ADAPTER" = 0 ] || return 1
|
|
597
|
+
for section in rules agents skills; do
|
|
598
|
+
load_yaml_list "$CONFIG_FILE" "$section"
|
|
599
|
+
while IFS= read -r src; do
|
|
600
|
+
case "$src" in
|
|
601
|
+
''|/*|..|../*|*/..|*/../*) continue ;; # Outside the project: read where it is.
|
|
602
|
+
esac
|
|
603
|
+
wanted+=("$src")
|
|
604
|
+
done <<< "$IS_YAML_LIST"
|
|
605
|
+
done
|
|
606
|
+
wanted+=(${SC_CHECK_ALL_OUTPUTS[@]+"${SC_CHECK_ALL_OUTPUTS[@]}"})
|
|
607
|
+
for rel in "${wanted[@]}"; do
|
|
608
|
+
normalize_path_var "/$rel"
|
|
609
|
+
rel="${IS_NORM_PATH#/}"
|
|
610
|
+
case "$rel" in ''|.git|.git/*) return 1 ;; esac
|
|
611
|
+
sync_check_unlinked_path "$rel" || return 1
|
|
612
|
+
[ -e "$REPO_ROOT/$rel" ] || continue
|
|
613
|
+
kept=()
|
|
614
|
+
for other in ${SC_CHECK_COPY[@]+"${SC_CHECK_COPY[@]}"}; do
|
|
615
|
+
case "$rel" in "$other"|"$other"/*) continue 2 ;; esac
|
|
616
|
+
case "$other" in "$rel"/*) continue ;; esac
|
|
617
|
+
kept+=("$other")
|
|
618
|
+
done
|
|
619
|
+
SC_CHECK_COPY=(${kept[@]+"${kept[@]}"} "$rel")
|
|
620
|
+
done
|
|
621
|
+
}
|
|
622
|
+
|
|
623
|
+
# sync_check_holds <private-project> <rel> — true when the private copy holds
|
|
624
|
+
# the project path: inside a copied entry, never a directory made only as the
|
|
625
|
+
# parent of one. An empty SC_CHECK_COPY means the whole project was copied.
|
|
626
|
+
sync_check_holds() {
|
|
627
|
+
local entry
|
|
628
|
+
[ -e "$1/$2" ] || [ -L "$1/$2" ] || return 1
|
|
629
|
+
[ "${#SC_CHECK_COPY[@]}" -gt 0 ] || return 0
|
|
630
|
+
for entry in "${SC_CHECK_COPY[@]}"; do
|
|
631
|
+
case "$2" in "$entry"|"$entry"/*) return 0 ;; esac
|
|
632
|
+
done
|
|
633
|
+
return 1
|
|
634
|
+
}
|
|
635
|
+
|
|
548
636
|
# Check renders use a private project tree. A rollback in the live tree would
|
|
549
|
-
# overwrite unrelated edits made while the renderer was running.
|
|
637
|
+
# overwrite unrelated edits made while the renderer was running. The tree holds
|
|
638
|
+
# only what the render reads and writes: a copy of the whole project took
|
|
639
|
+
# minutes and gigabytes where dependency and build trees sit beside the sources.
|
|
550
640
|
sync_check_render() (
|
|
551
|
-
local target="$1" verdict="$2" staging entry link target_link manifest_rel rel output is_output original_target original_root="$REPO_ROOT" rc=0
|
|
552
|
-
local -a SC_CHECK_OUTPUTS=()
|
|
641
|
+
local target="$1" verdict="$2" staging entry link target_link manifest_rel rel output is_output original_target original_root="$REPO_ROOT" rc=0 parent
|
|
642
|
+
local -a SC_CHECK_OUTPUTS=() SC_CHECK_ALL_OUTPUTS=() SC_CHECK_COPY=() parents=() batch=()
|
|
643
|
+
local SC_CHECK_PROJECT_ADAPTER=0
|
|
553
644
|
sync_check_write_paths "$target" || return 1
|
|
554
645
|
manifest_rel="${CONFIG_FILE#"$REPO_ROOT/"}"
|
|
555
646
|
staging="$(mktemp -d -t intelligence-check-XXXXXX)" || return 1
|
|
556
647
|
trap 'rm -rf "$staging"' EXIT
|
|
557
648
|
trap 'exit 130' INT
|
|
558
649
|
trap 'exit 143' TERM
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
650
|
+
# A run killed outright never reaches its trap; the lock's release or
|
|
651
|
+
# takeover removes the copy it names here.
|
|
652
|
+
: > "$staging/.intelligence-check" || return 1
|
|
653
|
+
printf '%s\n' "$staging" > "$SC_SCRATCH.tmpdir" || return 1
|
|
654
|
+
mkdir "$staging/project" || return 1
|
|
655
|
+
if sync_check_copy_paths; then
|
|
656
|
+
# One copy per parent directory: on Git Bash each process costs tens
|
|
657
|
+
# of milliseconds.
|
|
658
|
+
for rel in ${SC_CHECK_COPY[@]+"${SC_CHECK_COPY[@]}"}; do
|
|
659
|
+
parent=.
|
|
660
|
+
case "$rel" in */*) parent="${rel%/*}" ;; esac
|
|
661
|
+
for entry in ${parents[@]+"${parents[@]}"}; do
|
|
662
|
+
[ "$entry" != "$parent" ] || continue 2
|
|
663
|
+
done
|
|
664
|
+
parents+=("$parent")
|
|
665
|
+
done
|
|
666
|
+
for parent in ${parents[@]+"${parents[@]}"}; do
|
|
667
|
+
batch=()
|
|
668
|
+
for rel in "${SC_CHECK_COPY[@]}"; do
|
|
669
|
+
entry=.
|
|
670
|
+
case "$rel" in */*) entry="${rel%/*}" ;; esac
|
|
671
|
+
[ "$entry" != "$parent" ] || batch+=("$REPO_ROOT/$rel")
|
|
672
|
+
done
|
|
673
|
+
mkdir -p "$staging/project/$parent" && cp -a "${batch[@]}" "$staging/project/$parent/" || { rc=1; break; }
|
|
674
|
+
done
|
|
675
|
+
else
|
|
676
|
+
SC_CHECK_COPY=()
|
|
677
|
+
for entry in "$REPO_ROOT"/* "$REPO_ROOT"/.[!.]* "$REPO_ROOT"/..?*; do
|
|
678
|
+
[ -e "$entry" ] || [ -L "$entry" ] || continue
|
|
679
|
+
# Git history is not a renderer input. The project lock contains the
|
|
680
|
+
# check's scratch buffers and must stay solely in the real project.
|
|
681
|
+
[ "${entry##*/}" != .git ] || continue
|
|
682
|
+
cp -a "$entry" "$staging/project/" || rc=$?
|
|
683
|
+
[ "$rc" = 0 ] || break
|
|
684
|
+
done
|
|
685
|
+
fi
|
|
568
686
|
if [ "$rc" = 0 ]; then
|
|
569
687
|
# Never remove or rewrite through a copied directory link: it may
|
|
570
688
|
# still point at the live project or an external source.
|
|
@@ -575,7 +693,9 @@ sync_check_render() (
|
|
|
575
693
|
while IFS= read -r -d '' link; do
|
|
576
694
|
target_link="$(readlink "$link")" || { rc=1; break; }
|
|
577
695
|
# Output links need no relocation: comparison never follows them.
|
|
578
|
-
# Internal absolute links still move with their copied project
|
|
696
|
+
# Internal absolute links still move with their copied project;
|
|
697
|
+
# one into a part of the project the copy does not hold keeps
|
|
698
|
+
# reading the live path, which a render only ever reads.
|
|
579
699
|
rel="${link#"$staging/project/"}"
|
|
580
700
|
is_output=0
|
|
581
701
|
for output in ${SC_CHECK_OUTPUTS[@]+"${SC_CHECK_OUTPUTS[@]}"}; do
|
|
@@ -586,6 +706,7 @@ sync_check_render() (
|
|
|
586
706
|
rm "$link" && ln -s "$staging/project" "$link" || { rc=1; break; }
|
|
587
707
|
;;
|
|
588
708
|
"$REPO_ROOT"/*)
|
|
709
|
+
sync_check_holds "$staging/project" "${target_link#"$REPO_ROOT/"}" || continue
|
|
589
710
|
rm "$link" && ln -s "$staging/project/${target_link#"$REPO_ROOT/"}" "$link" || { rc=1; break; }
|
|
590
711
|
;;
|
|
591
712
|
/*) ;; # Absolute external links already retain their resolution.
|
|
@@ -596,7 +717,10 @@ sync_check_render() (
|
|
|
596
717
|
original_target="$IS_NORM_PATH"
|
|
597
718
|
case "$original_target" in
|
|
598
719
|
"$REPO_ROOT") original_target="$staging/project" ;;
|
|
599
|
-
"$REPO_ROOT"/*)
|
|
720
|
+
"$REPO_ROOT"/*)
|
|
721
|
+
entry="${original_target#"$REPO_ROOT/"}"
|
|
722
|
+
if sync_check_holds "$staging/project" "$entry"; then original_target="$staging/project/$entry"; fi
|
|
723
|
+
;;
|
|
600
724
|
esac
|
|
601
725
|
normalize_path_var "${link%/*}/$target_link"
|
|
602
726
|
[ "$IS_NORM_PATH" != "$original_target" ] || continue
|
package/cli/lib/sync-lock.sh
CHANGED
|
@@ -117,6 +117,36 @@ sync_lock_held_by_check() {
|
|
|
117
117
|
[ "$kind" = check ]
|
|
118
118
|
}
|
|
119
119
|
|
|
120
|
+
# sync_lock_tmpdirs <lock> — SYNC_LOCK_TMPDIRS, the temporary directories
|
|
121
|
+
# outside the lock that its holder's runs named in `scratch.<pid>.tmpdir`
|
|
122
|
+
# files. A run killed outright never reaches the trap that removes its own, so
|
|
123
|
+
# whoever releases or takes over the lock removes them. A record is only a
|
|
124
|
+
# file in the project, so it names nothing to remove unless the directory is
|
|
125
|
+
# one the CLI made: an absolute, real `intelligence-check-*` directory holding
|
|
126
|
+
# the marker `.intelligence-check` that the check writes first.
|
|
127
|
+
sync_lock_tmpdirs() {
|
|
128
|
+
local record dir
|
|
129
|
+
SYNC_LOCK_TMPDIRS=()
|
|
130
|
+
for record in "$1"/scratch.*.tmpdir; do
|
|
131
|
+
[ -f "$record" ] || continue
|
|
132
|
+
dir=""
|
|
133
|
+
IFS= read -r dir < "$record" || [ -n "$dir" ] || continue
|
|
134
|
+
case "$dir" in /*) ;; *) continue ;; esac
|
|
135
|
+
case "$dir" in *[[:cntrl:]]*|*/./*|*/../*|*/.|*/..) continue ;; esac
|
|
136
|
+
case "${dir##*/}" in intelligence-check-?*) ;; *) continue ;; esac
|
|
137
|
+
[ -d "$dir" ] && [ ! -L "$dir" ] && [ -f "$dir/.intelligence-check" ] || continue
|
|
138
|
+
SYNC_LOCK_TMPDIRS+=("$dir")
|
|
139
|
+
done
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
# Removing an abandoned copy can take long; it never keeps the lock looking
|
|
143
|
+
# ownerless or a takeover in progress.
|
|
144
|
+
sync_lock_remove_tmpdirs() {
|
|
145
|
+
[ "${#SYNC_LOCK_TMPDIRS[@]}" -eq 0 ] || rm -rf "${SYNC_LOCK_TMPDIRS[@]}" 2>/dev/null || true
|
|
146
|
+
SYNC_LOCK_TMPDIRS=()
|
|
147
|
+
}
|
|
148
|
+
SYNC_LOCK_TMPDIRS=()
|
|
149
|
+
|
|
120
150
|
# sync_lock_acquire <root> — take <root>/.intelligence/sync.lock, or refuse.
|
|
121
151
|
# A writer waits for each check that holds it for up to SYNC_LOCK_CHECK_WAIT
|
|
122
152
|
# seconds; a check refuses at once.
|
|
@@ -127,6 +157,7 @@ sync_lock_acquire() {
|
|
|
127
157
|
[ -d "$root/.intelligence" ] || mkdir -p "$root/.intelligence" || die "cannot create $root/.intelligence"
|
|
128
158
|
while :; do
|
|
129
159
|
took=0
|
|
160
|
+
SYNC_LOCK_TMPDIRS=()
|
|
130
161
|
if mkdir "$lock" 2>/dev/null; then
|
|
131
162
|
took=1
|
|
132
163
|
elif sync_lock_is_stale "$lock" && mkdir "$lock.takeover" 2>/dev/null; then
|
|
@@ -136,10 +167,15 @@ sync_lock_acquire() {
|
|
|
136
167
|
# Judge again under it: another run may have taken the lock over
|
|
137
168
|
# since this one looked.
|
|
138
169
|
if sync_lock_is_stale "$lock"; then
|
|
170
|
+
sync_lock_tmpdirs "$lock"
|
|
139
171
|
rm -rf "$lock"
|
|
140
172
|
mkdir "$lock" 2>/dev/null && took=1
|
|
141
173
|
fi
|
|
142
174
|
rmdir "$lock.takeover"
|
|
175
|
+
# Their records went with the lock: remove them now even when
|
|
176
|
+
# another run took it first. A lock this run took gets its owner
|
|
177
|
+
# before any of that time is spent.
|
|
178
|
+
[ "$took" = 1 ] || sync_lock_remove_tmpdirs
|
|
143
179
|
elif [ -d "$lock.takeover" ] && [ -n "$(find "$lock.takeover" -maxdepth 0 -mmin +1 2>/dev/null)" ]; then
|
|
144
180
|
# A takeover runs for milliseconds. Removing an abandoned one is itself
|
|
145
181
|
# a check-then-delete race, so it is left to a person.
|
|
@@ -147,6 +183,7 @@ sync_lock_acquire() {
|
|
|
147
183
|
fi
|
|
148
184
|
if [ "$took" = 1 ]; then
|
|
149
185
|
sync_lock_write_owner "$lock" || { rm -rf "$lock"; die "cannot write $lock/owner"; }
|
|
186
|
+
sync_lock_remove_tmpdirs
|
|
150
187
|
SYNC_LOCK_HELD="$lock"
|
|
151
188
|
return 0
|
|
152
189
|
fi
|
|
@@ -224,6 +261,9 @@ project_lock_hold() {
|
|
|
224
261
|
sync_lock_release() {
|
|
225
262
|
[ -n "$SYNC_LOCK_HELD" ] || return 0
|
|
226
263
|
if sync_lock_read_owner "$SYNC_LOCK_HELD" && [ "$IS_LOCK_TOKEN" = "$SYNC_LOCK_TOKEN" ]; then
|
|
264
|
+
# While the owner still stands, so a waiter keeps waiting.
|
|
265
|
+
sync_lock_tmpdirs "$SYNC_LOCK_HELD"
|
|
266
|
+
sync_lock_remove_tmpdirs
|
|
227
267
|
# A check's owner goes first: `rm -rf` alone could expose a moment with
|
|
228
268
|
# the owner still there and the kind gone, and a command waiting for
|
|
229
269
|
# the check would be refused in it. Without the owner, a waiter sees a
|
package/engine/ENGINE_SHA
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
|
|
1
|
+
4a95c37c037291cd55c305b72571decae77d7767
|
package/engine/VERSION
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
0.22.
|
|
1
|
+
0.22.1
|
package/engine/sync.sh
CHANGED
|
@@ -742,10 +742,14 @@ SYNC_TX_ACTIVE=0
|
|
|
742
742
|
# defaults move forward — e.g., gpt-5.5 -> gpt-5.6).
|
|
743
743
|
# Concurrently, the scan runs beside the other two and beside removing the
|
|
744
744
|
# snapshots; the buffers print in the order above.
|
|
745
|
+
# A check renders in a copy that holds only the project's inputs and outputs; the
|
|
746
|
+
# scan lists directories and reads no bytes, so it looks at the live project.
|
|
747
|
+
UNSYNCED_ROOT="$REPO_ROOT"
|
|
748
|
+
[ -z "${IS_SYNC_CHECK:-}" ] || [ -z "${IS_SYNC_CHECK_ROOT:-}" ] || UNSYNCED_ROOT="$IS_SYNC_CHECK_ROOT"
|
|
745
749
|
if [ "$SYNC_PARALLEL" = 1 ]; then
|
|
746
750
|
rm -rf "$SYNC_TX_DIR/data" &
|
|
747
751
|
SYNC_BG_PIDS+=("$!")
|
|
748
|
-
warn_unsynced "$
|
|
752
|
+
warn_unsynced "$UNSYNCED_ROOT" "$CONFIG_FILE" > "$SYNC_TX_DIR/unsynced" 2>&1 &
|
|
749
753
|
SYNC_BG_PIDS+=("$!")
|
|
750
754
|
{
|
|
751
755
|
report_context_source_sizes "$REPO_ROOT" "$CONFIG_FILE"
|
|
@@ -758,7 +762,7 @@ if [ "$SYNC_PARALLEL" = 1 ]; then
|
|
|
758
762
|
else
|
|
759
763
|
rm -rf "$SYNC_TX_DIR"
|
|
760
764
|
trap - EXIT INT TERM
|
|
761
|
-
warn_unsynced "$
|
|
765
|
+
warn_unsynced "$UNSYNCED_ROOT" "$CONFIG_FILE"
|
|
762
766
|
report_context_source_sizes "$REPO_ROOT" "$CONFIG_FILE"
|
|
763
767
|
report_model_drift "$CONFIG_FILE"
|
|
764
768
|
fi
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@ainova-systems/intelligence",
|
|
3
|
-
"version": "0.22.
|
|
3
|
+
"version": "0.22.1",
|
|
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"
|