@ainova-systems/intelligence 0.17.1 → 0.17.2
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/adapter.sh +4 -0
- package/cli/commands/sync.sh +3 -0
- package/cli/commands/update.sh +3 -0
- package/cli/intelligence +10 -4
- package/cli/internal/align-project.sh +1 -0
- package/cli/internal/convert-legacy.sh +47 -1
- package/cli/internal/package-add.sh +1 -0
- package/cli/internal/package-remove.sh +1 -0
- package/cli/internal/package-update.sh +1 -0
- package/cli/internal/restore.sh +1 -0
- package/cli/internal/target-state.sh +1 -0
- package/cli/lib/cli-common.sh +56 -23
- package/cli/lib/lockfile.sh +2 -1
- package/cli/lib/qmap.awk +16 -1
- package/cli/lib/sync-cache.sh +20 -7
- package/cli/lib/sync-lock.sh +148 -0
- package/engine/ENGINE_SHA +1 -1
- package/engine/VERSION +1 -1
- package/engine/adapters/claude.sh +3 -5
- package/engine/adapters/cursor.sh +3 -5
- package/engine/lib/adapter-contract.sh +1 -5
- package/engine/lib/common.sh +228 -59
- package/engine/lib/contract.sh +57 -17
- package/engine/sync.sh +315 -29
- package/package.json +1 -1
- package/packages/sync/references/adapters.md +7 -0
package/cli/commands/adapter.sh
CHANGED
|
@@ -118,6 +118,8 @@ adapter_remove() {
|
|
|
118
118
|
read -r answer
|
|
119
119
|
case "$answer" in y|Y|yes|YES) ;; *) echo "removal cancelled"; exit 0 ;; esac
|
|
120
120
|
fi
|
|
121
|
+
# A render sources project adapters: never remove one under it.
|
|
122
|
+
project_lock_hold "$IP_ROOT"
|
|
121
123
|
rm -f "$file"
|
|
122
124
|
echo "removed: $content_dir/adapters/$name.sh"
|
|
123
125
|
echo " generated output was kept"
|
|
@@ -136,6 +138,8 @@ case "$action" in
|
|
|
136
138
|
[ -n "$name" ] && [ $# -eq 2 ] || die "usage: intelligence adapter <enable|disable> <name>"
|
|
137
139
|
assert_valid_target_name "$name"
|
|
138
140
|
require_cli_project
|
|
141
|
+
# Held through the final render: target-state and sync join it.
|
|
142
|
+
project_lock_hold "$IP_ROOT"
|
|
139
143
|
ensure_project_current "$IP_ROOT"
|
|
140
144
|
if [ "$action" = "enable" ]; then
|
|
141
145
|
state_stage="$(mktemp -d -t intelligence-adapter-state-XXXXXX)"
|
package/cli/commands/sync.sh
CHANGED
|
@@ -29,6 +29,9 @@ run_sync() {
|
|
|
29
29
|
detect_project || return $?
|
|
30
30
|
case "$IP_MODE" in
|
|
31
31
|
cli)
|
|
32
|
+
# One writer per project at a time: the store restore and the
|
|
33
|
+
# render below both rewrite shared state (sync-lock.sh).
|
|
34
|
+
project_lock_hold "$IP_ROOT"
|
|
32
35
|
# `sync` is the normal fresh-clone command. A newer globally installed
|
|
33
36
|
# CLI aligns the current Intelligence project first (except in CI, where a
|
|
34
37
|
# tracked migration must be reviewed and committed locally), and a
|
package/cli/commands/update.sh
CHANGED
|
@@ -105,6 +105,9 @@ if [ "$mode" = "ask" ]; then
|
|
|
105
105
|
esac
|
|
106
106
|
fi
|
|
107
107
|
|
|
108
|
+
# Held from alignment through the final render: the package pass and the sync
|
|
109
|
+
# it execs join this lock instead of taking their own.
|
|
110
|
+
project_lock_hold "$IP_ROOT"
|
|
108
111
|
ensure_project_current "$IP_ROOT"
|
|
109
112
|
apply_args=(--no-sync)
|
|
110
113
|
[ -z "$only" ] || apply_args+=("$only")
|
package/cli/intelligence
CHANGED
|
@@ -7,15 +7,21 @@
|
|
|
7
7
|
|
|
8
8
|
set -euo pipefail
|
|
9
9
|
|
|
10
|
-
|
|
10
|
+
case "$0" in
|
|
11
|
+
*/*) CLI_DIR="${0%/*}"; [ -n "$CLI_DIR" ] || CLI_DIR="/" ;;
|
|
12
|
+
*) CLI_DIR="." ;;
|
|
13
|
+
esac
|
|
14
|
+
CLI_DIR="$(cd "$CLI_DIR" && pwd)"
|
|
11
15
|
export CLI_DIR
|
|
12
16
|
|
|
13
17
|
# Engine discovery, by role (sync.sh + VERSION). The repo and the npm package
|
|
14
18
|
# ship one layout — <root>/{cli,engine,packages/sync} — so there is a single
|
|
15
|
-
# candidate rather than a per-distribution list.
|
|
19
|
+
# candidate rather than a per-distribution list. CLI_DIR is already the logical
|
|
20
|
+
# `pwd` spelling, so its textual parent is what `cd "$CLI_DIR/.."` would print.
|
|
21
|
+
CLI_ROOT="${CLI_DIR%/*}"
|
|
16
22
|
IS_ENGINE_DIR=""
|
|
17
23
|
if [ -f "$CLI_DIR/../engine/sync.sh" ] && [ -f "$CLI_DIR/../engine/VERSION" ]; then
|
|
18
|
-
IS_ENGINE_DIR="$
|
|
24
|
+
IS_ENGINE_DIR="$CLI_ROOT/engine"
|
|
19
25
|
fi
|
|
20
26
|
if [ -z "$IS_ENGINE_DIR" ]; then
|
|
21
27
|
echo "ERROR: bundled sync engine not found next to the CLI ($CLI_DIR)." >&2
|
|
@@ -29,7 +35,7 @@ export IS_ENGINE_DIR
|
|
|
29
35
|
# engine dir: the store must receive rules/agents/skills, never engine scripts.
|
|
30
36
|
IS_BUNDLED_PKG_DIR=""
|
|
31
37
|
if [ -d "$CLI_DIR/../packages/sync" ]; then
|
|
32
|
-
IS_BUNDLED_PKG_DIR="$
|
|
38
|
+
IS_BUNDLED_PKG_DIR="$CLI_ROOT/packages/sync"
|
|
33
39
|
fi
|
|
34
40
|
export IS_BUNDLED_PKG_DIR
|
|
35
41
|
|
|
@@ -227,7 +227,38 @@ rewrite_source_value() {
|
|
|
227
227
|
esac
|
|
228
228
|
printf '%s' "$v"
|
|
229
229
|
}
|
|
230
|
-
|
|
230
|
+
# The project's own content stays where the legacy layout kept it. When that
|
|
231
|
+
# directory is not `intelligence/`, the manifest names it: the content directory
|
|
232
|
+
# is where project adapters live, what no output may overwrite, and the source of
|
|
233
|
+
# truth AGENTS.md points readers at. Left at the default, all three name a
|
|
234
|
+
# directory that only a case-insensitive filesystem confuses with the real one.
|
|
235
|
+
legacy_content_dir=""
|
|
236
|
+
case "$umbrella_rel" in
|
|
237
|
+
"$umbrella"|intelligence|*/*|.intelligence*|*[\"\'\\]*|*[[:cntrl:]]*) ;;
|
|
238
|
+
*)
|
|
239
|
+
# Only content that stays counts: the vendored module and pack mirrors
|
|
240
|
+
# move into the store and leave, so a config-only umbrella is not one.
|
|
241
|
+
if [ -z "$(get_yaml_field "$config" "project" "intelligence_dir")" ]; then
|
|
242
|
+
for section in rules agents skills; do
|
|
243
|
+
while IFS= read -r src; do
|
|
244
|
+
case "$src" in ""|git+*|@*) continue ;; esac
|
|
245
|
+
src_canon="$(normalize_path "$root/$src")"
|
|
246
|
+
case "$src_canon" in "$root"/*) src_rel="${src_canon#"$root"/}" ;; *) continue ;; esac
|
|
247
|
+
case "$src_rel" in "$module_rel"|"$module_rel"/*) continue ;; esac
|
|
248
|
+
leaving=0
|
|
249
|
+
while IFS="$LOCK_SEP" read -r _ _ _ _ mirror; do
|
|
250
|
+
[ -n "$mirror" ] || continue
|
|
251
|
+
mirror="$(normalize_path "$root/$mirror")"
|
|
252
|
+
case "$src_canon" in "$mirror"|"$mirror"/*) leaving=1 ;; esac
|
|
253
|
+
done < "$pack_rows"
|
|
254
|
+
[ "$leaving" -eq 0 ] || continue
|
|
255
|
+
case "$src_rel" in "$umbrella_rel"|"$umbrella_rel"/*) legacy_content_dir="$umbrella_rel" ;; esac
|
|
256
|
+
done < <(read_yaml_list "$config" "$section")
|
|
257
|
+
done
|
|
258
|
+
fi
|
|
259
|
+
;;
|
|
260
|
+
esac
|
|
261
|
+
in_packs=0 in_sources=0 project_key=0 content_dir_written=0
|
|
231
262
|
while IFS= read -r line || [ -n "$line" ]; do
|
|
232
263
|
line="${line%$'\r'}"
|
|
233
264
|
case "$line" in
|
|
@@ -236,6 +267,18 @@ while IFS= read -r line || [ -n "$line" ]; do
|
|
|
236
267
|
sources:*) in_sources=1; in_packs=0; echo "$line"; continue ;;
|
|
237
268
|
[!\ \#]*) in_packs=0; in_sources=0 ;;
|
|
238
269
|
esac
|
|
270
|
+
case "$line" in project:*) project_key=1 ;; esac
|
|
271
|
+
if [ -n "$legacy_content_dir" ] && [ "$content_dir_written" -eq 0 ]; then
|
|
272
|
+
# Only the block form takes a child line; `project: {...}` does not.
|
|
273
|
+
key_part="${line%%#*}"
|
|
274
|
+
key_part="${key_part%"${key_part##*[![:space:]]}"}"
|
|
275
|
+
if [ "$key_part" = "project:" ]; then
|
|
276
|
+
echo "$line"
|
|
277
|
+
echo " intelligence_dir: \"$legacy_content_dir\""
|
|
278
|
+
content_dir_written=1
|
|
279
|
+
continue
|
|
280
|
+
fi
|
|
281
|
+
fi
|
|
239
282
|
[ "$in_packs" -eq 1 ] && continue
|
|
240
283
|
if [ "$in_sources" -eq 1 ]; then
|
|
241
284
|
case "$line" in
|
|
@@ -261,6 +304,9 @@ while IFS= read -r line || [ -n "$line" ]; do
|
|
|
261
304
|
fi
|
|
262
305
|
echo "$line"
|
|
263
306
|
done < "$config" > "$manifest_stage"
|
|
307
|
+
if [ -n "$legacy_content_dir" ] && [ "$content_dir_written" -eq 0 ] && [ "$project_key" -eq 0 ]; then
|
|
308
|
+
printf 'project:\n intelligence_dir: "%s"\n' "$legacy_content_dir" >> "$manifest_stage"
|
|
309
|
+
fi
|
|
264
310
|
stamp_schema_version "$manifest_stage" "$eng"
|
|
265
311
|
|
|
266
312
|
# packages: requested pins only; resolved source details live in the lock.
|
package/cli/internal/restore.sh
CHANGED
|
@@ -15,6 +15,7 @@ case "$action" in enable|disable) ;; *) die "internal target state: expected <en
|
|
|
15
15
|
assert_valid_target_name "$name"
|
|
16
16
|
|
|
17
17
|
require_cli_project
|
|
18
|
+
project_lock_hold "$IP_ROOT"
|
|
18
19
|
manifest="$IP_ROOT/intelligence.yaml"
|
|
19
20
|
content_dir="$(manifest_intelligence_dir "$manifest")"
|
|
20
21
|
|
package/cli/lib/cli-common.sh
CHANGED
|
@@ -87,6 +87,7 @@ source "$CLI_DIR/lib/adapter-lifecycle.sh"
|
|
|
87
87
|
source "$CLI_DIR/lib/gitignore.sh"
|
|
88
88
|
source "$CLI_DIR/lib/onboarding.sh"
|
|
89
89
|
source "$CLI_DIR/lib/sync-cache.sh"
|
|
90
|
+
source "$CLI_DIR/lib/sync-lock.sh"
|
|
90
91
|
|
|
91
92
|
# The engine-content package: OPTIONAL but auto-selected at init. Package by
|
|
92
93
|
# UX (manifest entry, lockfile row, list/search/remove), bundle by mechanics —
|
|
@@ -97,12 +98,21 @@ source "$CLI_DIR/lib/sync-cache.sh"
|
|
|
97
98
|
# Its identity is DATA shipped with the distribution (cli/engine-package.yaml),
|
|
98
99
|
# never a name compiled into cli code — a fork edits the file.
|
|
99
100
|
_EPKG="$CLI_DIR/engine-package.yaml"
|
|
100
|
-
SYNC_PKG_NAME="
|
|
101
|
-
SYNC_PKG_URL="$(top_scalar "$_EPKG" "url")"
|
|
102
|
-
SYNC_PKG_PATH="$(top_scalar "$_EPKG" "path")"
|
|
101
|
+
SYNC_PKG_NAME="" SYNC_PKG_URL="" SYNC_PKG_PATH=""
|
|
103
102
|
# Read by commands (init seeds it) — per-file shellcheck cannot see that.
|
|
104
103
|
# shellcheck disable=SC2034
|
|
105
|
-
DEFAULT_REGISTRY_URL="
|
|
104
|
+
DEFAULT_REGISTRY_URL=""
|
|
105
|
+
# Every command loads this file first: read the four fields in one pass.
|
|
106
|
+
# shellcheck disable=SC2034 # DEFAULT_REGISTRY_URL is read by commands, as above
|
|
107
|
+
while IFS="$LOCK_SEP" read -r _epkg_key _epkg_value; do
|
|
108
|
+
case "$_epkg_key" in
|
|
109
|
+
name) SYNC_PKG_NAME="$_epkg_value" ;;
|
|
110
|
+
url) SYNC_PKG_URL="$_epkg_value" ;;
|
|
111
|
+
path) SYNC_PKG_PATH="$_epkg_value" ;;
|
|
112
|
+
default_registry) DEFAULT_REGISTRY_URL="$_epkg_value" ;;
|
|
113
|
+
esac
|
|
114
|
+
done <<< "$(_qmap_read tops "$_EPKG" '' 'name url path default_registry')"
|
|
115
|
+
unset _epkg_key _epkg_value
|
|
106
116
|
[ -n "$SYNC_PKG_NAME" ] || die "corrupt CLI installation: $_EPKG is missing or has no 'name'"
|
|
107
117
|
SYNC_PKG_STORE=".intelligence/packages/$SYNC_PKG_NAME"
|
|
108
118
|
|
|
@@ -116,15 +126,18 @@ SYNC_PKG_STORE=".intelligence/packages/$SYNC_PKG_NAME"
|
|
|
116
126
|
# by role — scripts/sync.sh + scripts/VERSION — never by name.
|
|
117
127
|
detect_project() {
|
|
118
128
|
IP_MODE="none"; IP_ROOT=""; IP_UMBRELLA=""; IP_MODULE_DIR=""
|
|
119
|
-
|
|
129
|
+
# PWD is already the logical `pwd` spelling, and so is each textual parent.
|
|
130
|
+
local dir="$PWD" parent
|
|
120
131
|
while :; do
|
|
121
132
|
if [ -f "$dir/intelligence.yaml" ]; then
|
|
122
133
|
IP_MODE="cli"
|
|
123
|
-
IP_ROOT="$
|
|
134
|
+
IP_ROOT="$dir"
|
|
124
135
|
return 0
|
|
125
136
|
fi
|
|
126
|
-
|
|
127
|
-
|
|
137
|
+
parent="${dir%/*}"
|
|
138
|
+
[ -n "$parent" ] || parent="/"
|
|
139
|
+
[ "$dir" = "$parent" ] && break
|
|
140
|
+
dir="$parent"
|
|
128
141
|
done
|
|
129
142
|
local root
|
|
130
143
|
root="$(git rev-parse --show-toplevel 2>/dev/null || true)"
|
|
@@ -257,10 +270,23 @@ default_target_output() {
|
|
|
257
270
|
esac
|
|
258
271
|
}
|
|
259
272
|
|
|
273
|
+
# bundled_engine_version_var sets IS_BUNDLED_ENGINE_VERSION, reading VERSION
|
|
274
|
+
# once per process; lifecycle preflight compares against it several times.
|
|
275
|
+
bundled_engine_version_var() {
|
|
276
|
+
[ -z "${IS_BUNDLED_ENGINE_VERSION_READ:-}" ] || return 0
|
|
277
|
+
local version=""
|
|
278
|
+
[ -r "$IS_ENGINE_DIR/VERSION" ] || {
|
|
279
|
+
echo "ERROR: cannot read $IS_ENGINE_DIR/VERSION" >&2
|
|
280
|
+
return 1
|
|
281
|
+
}
|
|
282
|
+
IFS= read -r -d '' version < "$IS_ENGINE_DIR/VERSION" || true
|
|
283
|
+
IS_BUNDLED_ENGINE_VERSION="${version//[$' \t\r\n']/}"
|
|
284
|
+
IS_BUNDLED_ENGINE_VERSION_READ=1
|
|
285
|
+
}
|
|
286
|
+
|
|
260
287
|
bundled_engine_version() {
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
printf '%s' "${version//[$' \t\r\n']/}"
|
|
288
|
+
bundled_engine_version_var
|
|
289
|
+
printf '%s' "$IS_BUNDLED_ENGINE_VERSION"
|
|
264
290
|
}
|
|
265
291
|
|
|
266
292
|
# --- The sync package's manifest/lock plumbing ---------------------------
|
|
@@ -345,15 +371,19 @@ assert_safe_content_dir() {
|
|
|
345
371
|
# the admitted minor/patch gap; the predicate itself does not re-judge the gap.
|
|
346
372
|
project_stamped_ahead() {
|
|
347
373
|
local stamp
|
|
348
|
-
|
|
349
|
-
|
|
374
|
+
read_schema_version_var "$1/intelligence.yaml"
|
|
375
|
+
stamp="$IS_SCHEMA_VERSION"
|
|
376
|
+
bundled_engine_version_var
|
|
377
|
+
[ -n "$stamp" ] && _ver_gt "$stamp" "$IS_BUNDLED_ENGINE_VERSION"
|
|
350
378
|
}
|
|
351
379
|
|
|
352
380
|
project_needs_upgrade() {
|
|
353
|
-
local root="$1" manifest="$1/intelligence.yaml" stamp eng pinned locked name
|
|
381
|
+
local root="$1" manifest="$1/intelligence.yaml" stamp eng pinned locked name url path rows
|
|
354
382
|
[ -f "$manifest" ] || return 1
|
|
355
|
-
|
|
356
|
-
|
|
383
|
+
read_schema_version_var "$manifest"
|
|
384
|
+
stamp="$IS_SCHEMA_VERSION"
|
|
385
|
+
bundled_engine_version_var
|
|
386
|
+
eng="$IS_BUNDLED_ENGINE_VERSION"
|
|
357
387
|
[ -z "$stamp" ] && return 0
|
|
358
388
|
# A project stamped ahead belongs to a newer CLI. Aligning it here would
|
|
359
389
|
# restamp the schema and re-pin the engine content DOWNWARD, and the next
|
|
@@ -363,14 +393,15 @@ project_needs_upgrade() {
|
|
|
363
393
|
_ver_gt "$eng" "$stamp" && return 0
|
|
364
394
|
[ -d "$root/.intelligence/engine" ] && return 0
|
|
365
395
|
|
|
366
|
-
|
|
396
|
+
# One manifest pass for every package's fields, not a reader per field.
|
|
397
|
+
rows="$(_qmap_read fieldrows "$manifest" packages '' 'url path version')"
|
|
398
|
+
while IFS="$LOCK_SEP" read -r name url path pinned; do
|
|
367
399
|
[ -n "$name" ] || continue
|
|
368
400
|
# Early RC manifests mixed requested intent with resolved source
|
|
369
401
|
# details. Source URL/path now live only in the required lockfile.
|
|
370
|
-
[ -n "$
|
|
371
|
-
[ -n "$
|
|
402
|
+
[ -n "$url" ] && return 0
|
|
403
|
+
[ -n "$path" ] && return 0
|
|
372
404
|
if [ "$name" = "$SYNC_PKG_NAME" ]; then
|
|
373
|
-
pinned="$(qmap_field "$manifest" "packages" "$name" "version")"
|
|
374
405
|
locked="$(qmap_field "$root/intelligence.lock" "packages" "$name" "resolved")"
|
|
375
406
|
[ "$pinned" = "$eng" ] || return 0
|
|
376
407
|
if [ -f "$root/intelligence.lock" ]; then
|
|
@@ -378,7 +409,7 @@ project_needs_upgrade() {
|
|
|
378
409
|
[ "${locked#v}" = "$eng" ] || return 0
|
|
379
410
|
fi
|
|
380
411
|
fi
|
|
381
|
-
done
|
|
412
|
+
done <<< "$rows"
|
|
382
413
|
return 1
|
|
383
414
|
}
|
|
384
415
|
|
|
@@ -402,8 +433,10 @@ ensure_project_current() {
|
|
|
402
433
|
die "manifest declares packages but intelligence.lock is absent — restore the committed lock before running project lifecycle commands"
|
|
403
434
|
fi
|
|
404
435
|
project_needs_upgrade "$root" || return 0
|
|
405
|
-
|
|
406
|
-
|
|
436
|
+
read_schema_version_var "$manifest"
|
|
437
|
+
stamp="$IS_SCHEMA_VERSION"
|
|
438
|
+
bundled_engine_version_var
|
|
439
|
+
eng="$IS_BUNDLED_ENGINE_VERSION"
|
|
407
440
|
if is_ci_environment && [ "$explicit" != "--explicit" ]; then
|
|
408
441
|
die "project lifecycle requires alignment (stamp ${stamp:-unstamped}, engine $eng) — run 'intelligence init --apply' locally, review and commit the diff"
|
|
409
442
|
fi
|
package/cli/lib/lockfile.sh
CHANGED
|
@@ -24,7 +24,8 @@ lock_validate() (
|
|
|
24
24
|
# The same tokenizer supplies ordinary lock rows and strict metadata. Buffer
|
|
25
25
|
# before consumption: a failed parser must never yield a partial package set.
|
|
26
26
|
rows="$(_qmap_read lock "$lock" packages '' '' "$LOCKFILE_VERSION")" || return $?
|
|
27
|
-
|
|
27
|
+
bundled_engine_version_var
|
|
28
|
+
current="$IS_BUNDLED_ENGINE_VERSION"
|
|
28
29
|
while IFS="$LOCK_SEP" read -r kind name requested url path resolved sha line; do
|
|
29
30
|
case "$kind" in
|
|
30
31
|
V)
|
package/cli/lib/qmap.awk
CHANGED
|
@@ -177,7 +177,17 @@ END {
|
|
|
177
177
|
exit 1
|
|
178
178
|
}
|
|
179
179
|
if (mode == "lock") print "V" sep top_value["lockfile_version"] sep top_value["engine_version"]
|
|
180
|
-
if (mode == "
|
|
180
|
+
if (mode == "fieldrows") {
|
|
181
|
+
# One row per package key, as `keys` lists them: the name, then each
|
|
182
|
+
# field QMAP_FIELD names (space separated), as qmap_field decodes it.
|
|
183
|
+
count = split(wanted_field, fields, " ")
|
|
184
|
+
for (i = 1; i <= row_count; i++) {
|
|
185
|
+
name = row_name[i]
|
|
186
|
+
row = name
|
|
187
|
+
for (f = 1; f <= count; f++) row = row sep field_value[name SUBSEP fields[f]]
|
|
188
|
+
print row
|
|
189
|
+
}
|
|
190
|
+
} else if (mode == "rows" || mode == "lock" || mode == "keys") {
|
|
181
191
|
for (i = 1; i <= row_count; i++) {
|
|
182
192
|
name = row_name[i]
|
|
183
193
|
if (mode == "keys") print name
|
|
@@ -187,5 +197,10 @@ END {
|
|
|
187
197
|
else print row
|
|
188
198
|
}
|
|
189
199
|
}
|
|
200
|
+
} else if (mode == "tops") {
|
|
201
|
+
# Several top-level scalars in one pass: QMAP_KEY lists them, space
|
|
202
|
+
# separated; each present one prints as key<US>value.
|
|
203
|
+
count = split(wanted_key, tops, " ")
|
|
204
|
+
for (i = 1; i <= count; i++) if (tops[i] in top_value) print tops[i] sep top_value[tops[i]]
|
|
190
205
|
} else if (found) print result
|
|
191
206
|
}
|
package/cli/lib/sync-cache.sh
CHANGED
|
@@ -111,14 +111,17 @@ sync_cache_dependencies() {
|
|
|
111
111
|
}
|
|
112
112
|
|
|
113
113
|
sync_cache_input_hash() {
|
|
114
|
-
local fingerprint
|
|
114
|
+
local fingerprint payload
|
|
115
115
|
fingerprint="$(sync_cache_fingerprint "$1" "${SC_INPUTS[@]}")" || return 1
|
|
116
|
+
[ -n "${SC_UMASK:-}" ] || SC_UMASK="$(umask)"
|
|
116
117
|
# Explicit environment values that affect rendering, discovery or parsing.
|
|
117
118
|
# No secrets or inherited environment dump are persisted.
|
|
118
|
-
printf '%s\n' "$fingerprint" "$2" "$REPO_ROOT" "$CONFIG_FILE" \
|
|
119
|
+
printf -v payload '%s\n' "$fingerprint" "$2" "$REPO_ROOT" "$CONFIG_FILE" \
|
|
119
120
|
"$IS_CONTENT_REL" "$IS_MODULE_REL" "$IS_SYNC_CMD" "$IS_MANIFEST_NAME" \
|
|
120
121
|
"$IS_PROTECTED_DIRS" "$BASH_VERSION" "${OSTYPE:-}" "${LANG:-}" \
|
|
121
|
-
"${LC_ALL:-}" "${LC_CTYPE:-}" "${LC_COLLATE:-}" "${PATH:-}" "$
|
|
122
|
+
"${LC_ALL:-}" "${LC_CTYPE:-}" "${LC_COLLATE:-}" "${PATH:-}" "$SC_UMASK"
|
|
123
|
+
# A here-string appends the final newline the payload already ends with.
|
|
124
|
+
git hash-object --stdin <<< "${payload%$'\n'}"
|
|
122
125
|
}
|
|
123
126
|
|
|
124
127
|
sync_cache_directory() {
|
|
@@ -164,6 +167,8 @@ sync_with_cache() (
|
|
|
164
167
|
work="$(mktemp -d -t intelligence-cache-XXXXXX)" || return 1
|
|
165
168
|
trap 'rm -rf "$work"' EXIT
|
|
166
169
|
echo "Checking source files..."
|
|
170
|
+
# Read once here so both input hashes, one in a background job, reuse it.
|
|
171
|
+
SC_UMASK="$(umask)"
|
|
167
172
|
if sync_cache_dependencies && sync_cache_directory; then
|
|
168
173
|
before="$(sync_cache_input_hash "$work" "$target")" || before=""
|
|
169
174
|
[ -z "$before" ] || cacheable=1
|
|
@@ -197,11 +202,19 @@ sync_with_cache() (
|
|
|
197
202
|
if [ "$cacheable" = 1 ]; then
|
|
198
203
|
(
|
|
199
204
|
echo "Checking sync results..."
|
|
200
|
-
|
|
205
|
+
# The input recheck and the output fingerprint only read files, so
|
|
206
|
+
# they run side by side, each in its own scratch directory.
|
|
207
|
+
local after_pid after_rc=0
|
|
208
|
+
mkdir "$work/after" "$work/outputs" || return 0
|
|
209
|
+
sync_cache_input_hash "$work/after" "$target" > "$work/after/hash" &
|
|
210
|
+
after_pid=$!
|
|
211
|
+
outputs="$(sync_cache_fingerprint "$work/outputs" "${SC_OUTPUTS[@]}")" || outputs=""
|
|
212
|
+
sync_cache_report "$work/log" > "$work/report" || outputs=""
|
|
213
|
+
report_hash="$(git hash-object --stdin < "$work/report")" || outputs=""
|
|
214
|
+
wait "$after_pid" || after_rc=$?
|
|
215
|
+
[ "$after_rc" = 0 ] && [ -n "$outputs" ] || return 0
|
|
216
|
+
IFS= read -r after < "$work/after/hash" || return 0
|
|
201
217
|
[ "$before" = "$after" ] || return 0
|
|
202
|
-
outputs="$(sync_cache_fingerprint "$work" "${SC_OUTPUTS[@]}")" || return 0
|
|
203
|
-
sync_cache_report "$work/log" > "$work/report" || return 0
|
|
204
|
-
report_hash="$(git hash-object --stdin < "$work/report")" || return 0
|
|
205
218
|
# Recheck containment before publishing. mktemp+rename avoids following a
|
|
206
219
|
# pre-existing state-file link, and readers see only complete records.
|
|
207
220
|
sync_cache_directory create || return 0
|
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
#!/bin/bash
|
|
2
|
+
# One writer per project at a time. The engine clears and rewrites every
|
|
3
|
+
# adapter's output, the store restore and package operations replace package
|
|
4
|
+
# directories, and target and alignment edits rewrite the manifest. Two of these
|
|
5
|
+
# overlapping interleave their writes and both report success over a corrupted
|
|
6
|
+
# tree, so every script that writes shared project state holds this lock while it
|
|
7
|
+
# does: `project_lock_hold` at its start.
|
|
8
|
+
#
|
|
9
|
+
# `mkdir` is the lock: it creates or fails atomically on every filesystem the
|
|
10
|
+
# CLI supports. The owner file names the holder — pid, host, start time, process
|
|
11
|
+
# group and a token — so a refusal says who it is, a child of the holder can
|
|
12
|
+
# join instead of refusing, and a holder that died can be told from one whose
|
|
13
|
+
# writers still run. Taking over a dead holder's lock happens under a second
|
|
14
|
+
# lock, so two runs that judged the same lock stale cannot both take it.
|
|
15
|
+
|
|
16
|
+
SYNC_LOCK_HELD=""
|
|
17
|
+
SYNC_LOCK_TOKEN=""
|
|
18
|
+
|
|
19
|
+
sync_lock_now() {
|
|
20
|
+
if [ -n "${EPOCHSECONDS:-}" ]; then
|
|
21
|
+
IS_NOW="$EPOCHSECONDS"
|
|
22
|
+
else
|
|
23
|
+
IS_NOW="$(date +%s)"
|
|
24
|
+
fi
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
# sync_lock_pgid — set IS_PGID to this process's group, "" when unknown. The
|
|
28
|
+
# engine and its jobs inherit the group, so it outlives a killed wrapper for as
|
|
29
|
+
# long as any writer it started still runs.
|
|
30
|
+
sync_lock_pgid() {
|
|
31
|
+
local stat
|
|
32
|
+
local -a fields
|
|
33
|
+
IS_PGID=""
|
|
34
|
+
if [ -r "/proc/$$/stat" ] && IFS= read -r stat < "/proc/$$/stat"; then
|
|
35
|
+
# pid (comm) state ppid pgrp …: comm may hold spaces, so cut after it.
|
|
36
|
+
read -r -a fields <<< "${stat##*) }"
|
|
37
|
+
IS_PGID="${fields[2]:-}"
|
|
38
|
+
else
|
|
39
|
+
IS_PGID="$(ps -o pgid= -p "$$" 2>/dev/null || true)"
|
|
40
|
+
IS_PGID="${IS_PGID//[[:space:]]/}"
|
|
41
|
+
fi
|
|
42
|
+
case "$IS_PGID" in ''|*[!0-9]*|0|1) IS_PGID="" ;; esac
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
sync_lock_write_owner() {
|
|
46
|
+
sync_lock_now
|
|
47
|
+
sync_lock_pgid
|
|
48
|
+
SYNC_LOCK_TOKEN="$$.$IS_NOW.$RANDOM$RANDOM"
|
|
49
|
+
printf '%s %s %s %s %s\n' "$$" "${HOSTNAME:-unknown}" "$IS_NOW" "${IS_PGID:--}" "$SYNC_LOCK_TOKEN" > "$1/owner"
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
# sync_lock_read_owner <lock> — set IS_LOCK_PID / _HOST / _SINCE / _PGID /
|
|
53
|
+
# _TOKEN; returns 1 when the folder has no readable owner yet.
|
|
54
|
+
sync_lock_read_owner() {
|
|
55
|
+
IS_LOCK_PID="" IS_LOCK_HOST="" IS_LOCK_SINCE="" IS_LOCK_PGID="" IS_LOCK_TOKEN=""
|
|
56
|
+
[ -f "$1/owner" ] || return 1
|
|
57
|
+
read -r IS_LOCK_PID IS_LOCK_HOST IS_LOCK_SINCE IS_LOCK_PGID IS_LOCK_TOKEN < "$1/owner" \
|
|
58
|
+
|| [ -n "$IS_LOCK_PID" ] || return 1
|
|
59
|
+
case "$IS_LOCK_PID" in ''|*[!0-9]*) return 1 ;; esac
|
|
60
|
+
case "$IS_LOCK_PGID" in ''|*[!0-9]*|0|1) IS_LOCK_PGID="" ;; esac
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
# sync_lock_holder_gone — true only when no process of the holder remains: its
|
|
64
|
+
# whole process group when the owner recorded one, else its pid. Only "No such
|
|
65
|
+
# process" proves that; a permission refusal is another user's live process,
|
|
66
|
+
# and any other answer keeps the lock.
|
|
67
|
+
sync_lock_holder_gone() {
|
|
68
|
+
local target="$IS_LOCK_PID" answer
|
|
69
|
+
[ -z "$IS_LOCK_PGID" ] || target="-$IS_LOCK_PGID"
|
|
70
|
+
answer="$(LC_ALL=C kill -0 -- "$target" 2>&1)" && return 1
|
|
71
|
+
case "$answer" in *"No such process"*) return 0 ;; esac
|
|
72
|
+
return 1
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
# sync_lock_is_stale <lock> — its holder can no longer write: gone from this
|
|
76
|
+
# machine, or a folder that never got an owner and is past a minute old. A
|
|
77
|
+
# holder on another machine (a shared filesystem) is never judged from here.
|
|
78
|
+
sync_lock_is_stale() {
|
|
79
|
+
if sync_lock_read_owner "$1"; then
|
|
80
|
+
[ "$IS_LOCK_HOST" = "${HOSTNAME:-unknown}" ] || return 1
|
|
81
|
+
sync_lock_holder_gone
|
|
82
|
+
return
|
|
83
|
+
fi
|
|
84
|
+
[ -d "$1" ] && [ -n "$(find "$1" -maxdepth 0 -mmin +1 2>/dev/null)" ]
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
# sync_lock_acquire <root> — take <root>/.intelligence/sync.lock or refuse.
|
|
88
|
+
sync_lock_acquire() {
|
|
89
|
+
local root="$1" lock="$1/.intelligence/sync.lock" took=0 age=""
|
|
90
|
+
mkdir -p "$root/.intelligence" || die "cannot create $root/.intelligence"
|
|
91
|
+
if mkdir "$lock" 2>/dev/null; then
|
|
92
|
+
took=1
|
|
93
|
+
elif mkdir "$lock.takeover" 2>/dev/null; then
|
|
94
|
+
# Judge again under the takeover lock: another run may have taken
|
|
95
|
+
# it over since this one looked.
|
|
96
|
+
if sync_lock_is_stale "$lock"; then
|
|
97
|
+
rm -rf "$lock"
|
|
98
|
+
mkdir "$lock" 2>/dev/null && took=1
|
|
99
|
+
fi
|
|
100
|
+
rmdir "$lock.takeover"
|
|
101
|
+
elif [ -n "$(find "$lock.takeover" -maxdepth 0 -mmin +1 2>/dev/null)" ]; then
|
|
102
|
+
# A takeover runs for milliseconds. Removing an abandoned one is itself
|
|
103
|
+
# a check-then-delete race, so it is left to a person.
|
|
104
|
+
die "a takeover of $lock was abandoned; if no intelligence command is running in this project, remove $lock.takeover"
|
|
105
|
+
fi
|
|
106
|
+
if [ "$took" = 1 ]; then
|
|
107
|
+
sync_lock_write_owner "$lock" || { rm -rf "$lock"; die "cannot write $lock/owner"; }
|
|
108
|
+
SYNC_LOCK_HELD="$lock"
|
|
109
|
+
return 0
|
|
110
|
+
fi
|
|
111
|
+
if sync_lock_read_owner "$lock"; then
|
|
112
|
+
sync_lock_now
|
|
113
|
+
case "$IS_LOCK_SINCE" in ''|*[!0-9]*) ;; *) age=", for $((IS_NOW - IS_LOCK_SINCE))s" ;; esac
|
|
114
|
+
die "another intelligence sync is running in this project (pid $IS_LOCK_PID on $IS_LOCK_HOST$age). Wait for it to finish; if no such process exists, remove $lock"
|
|
115
|
+
fi
|
|
116
|
+
die "another intelligence sync is starting in this project. Wait for it to finish; if none is running, remove $lock"
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
# project_lock_hold <root> — hold the project lock for the rest of this script.
|
|
120
|
+
# A script started by the holder joins it instead (INTELLIGENCE_SYNC_LOCK names
|
|
121
|
+
# the lock and its token): the holder releases it, unless `exec` made this
|
|
122
|
+
# process the holder, which then releases it on exit.
|
|
123
|
+
project_lock_hold() {
|
|
124
|
+
local root="$1" lock="$1/.intelligence/sync.lock" held="${INTELLIGENCE_SYNC_LOCK:-}"
|
|
125
|
+
if [ -n "$held" ] && [ "${held%%|*}" = "$lock" ] && sync_lock_read_owner "$lock" \
|
|
126
|
+
&& [ -n "$IS_LOCK_TOKEN" ] && [ "$IS_LOCK_TOKEN" = "${held#*|}" ]; then
|
|
127
|
+
if [ "$IS_LOCK_PID" = "$$" ]; then
|
|
128
|
+
SYNC_LOCK_HELD="$lock"
|
|
129
|
+
SYNC_LOCK_TOKEN="$IS_LOCK_TOKEN"
|
|
130
|
+
trap 'sync_lock_release' EXIT
|
|
131
|
+
trap 'exit 130' INT TERM
|
|
132
|
+
fi
|
|
133
|
+
return 0
|
|
134
|
+
fi
|
|
135
|
+
sync_lock_acquire "$root"
|
|
136
|
+
export INTELLIGENCE_SYNC_LOCK="$lock|$SYNC_LOCK_TOKEN"
|
|
137
|
+
trap 'sync_lock_release' EXIT
|
|
138
|
+
trap 'exit 130' INT TERM
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
# sync_lock_release — drop the lock this process holds, and only that one.
|
|
142
|
+
sync_lock_release() {
|
|
143
|
+
[ -n "$SYNC_LOCK_HELD" ] || return 0
|
|
144
|
+
if sync_lock_read_owner "$SYNC_LOCK_HELD" && [ "$IS_LOCK_TOKEN" = "$SYNC_LOCK_TOKEN" ]; then
|
|
145
|
+
rm -rf "$SYNC_LOCK_HELD"
|
|
146
|
+
fi
|
|
147
|
+
SYNC_LOCK_HELD=""
|
|
148
|
+
}
|
package/engine/ENGINE_SHA
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
|
|
1
|
+
97484ac7ccc2e093642b833edfa8cae7bfe47c9e
|
package/engine/VERSION
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
0.17.
|
|
1
|
+
0.17.2
|
|
@@ -173,9 +173,7 @@ sync_to_claude() {
|
|
|
173
173
|
sync_claude_skills "$repo_root" "$config_file" "$output_dir"
|
|
174
174
|
sync_claude_agents "$repo_root" "$config_file" "$output_dir"
|
|
175
175
|
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
agents_count=$(find "$output_dir/agents" -name "*.md" 2>/dev/null | wc -l)
|
|
180
|
-
echo " -> Rules: $rules_count, Skills: $skills_count, Agents: $agents_count"
|
|
176
|
+
count_matching_files "$output_dir/rules" "*.md" \
|
|
177
|
+
"$output_dir/skills" "SKILL.md" "$output_dir/agents" "*.md"
|
|
178
|
+
echo " -> Rules: ${IS_FILE_COUNTS[0]}, Skills: ${IS_FILE_COUNTS[1]}, Agents: ${IS_FILE_COUNTS[2]}"
|
|
181
179
|
}
|
|
@@ -204,9 +204,7 @@ sync_to_cursor() {
|
|
|
204
204
|
sync_cursor_skills "$repo_root" "$config_file" "$output_dir"
|
|
205
205
|
sync_cursor_agents "$repo_root" "$config_file" "$output_dir"
|
|
206
206
|
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
agents_count=$(find "$output_dir/agents" -name "*.md" 2>/dev/null | wc -l)
|
|
211
|
-
echo " -> Rules: $rules_count (.mdc), Skills: $skills_count, Agents: $agents_count"
|
|
207
|
+
count_matching_files "$output_dir/rules" "*.mdc" \
|
|
208
|
+
"$output_dir/skills" "SKILL.md" "$output_dir/agents" "*.md"
|
|
209
|
+
echo " -> Rules: ${IS_FILE_COUNTS[0]} (.mdc), Skills: ${IS_FILE_COUNTS[1]}, Agents: ${IS_FILE_COUNTS[2]}"
|
|
212
210
|
}
|