@ainova-systems/intelligence 0.17.1 → 0.17.3
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/check.sh +14 -1
- package/cli/internal/convert-legacy.sh +52 -1
- package/cli/internal/package-add.sh +6 -0
- package/cli/internal/package-remove.sh +2 -0
- package/cli/internal/package-update.sh +3 -0
- package/cli/internal/restore.sh +11 -5
- package/cli/internal/target-state.sh +1 -0
- package/cli/lib/cli-common.sh +128 -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/packages/sync/references/conventions.md +2 -2
package/engine/lib/contract.sh
CHANGED
|
@@ -20,19 +20,40 @@
|
|
|
20
20
|
# schema is this?" before parsing the rest.
|
|
21
21
|
IS_SCHEMA_VERSION_KEY="schema_version"
|
|
22
22
|
|
|
23
|
+
# read_schema_version_var <config_file> — set IS_SCHEMA_VERSION to the applied
|
|
24
|
+
# version, "" if absent; IS_SCHEMA_VERSION_FOUND=1 when the key exists. Read in
|
|
25
|
+
# this shell, not by awk: every lifecycle command and the engine ask before
|
|
26
|
+
# anything else, and on Git Bash each spawned reader costs tens of milliseconds.
|
|
27
|
+
# The value is the text after the first colon, minus surrounding whitespace, one
|
|
28
|
+
# leading quote and one trailing quote.
|
|
29
|
+
read_schema_version_var() {
|
|
30
|
+
local cf="$1" line v
|
|
31
|
+
IS_SCHEMA_VERSION=""
|
|
32
|
+
IS_SCHEMA_VERSION_FOUND=0
|
|
33
|
+
[ -f "$cf" ] || return 0
|
|
34
|
+
while IFS= read -r line || [ -n "$line" ]; do
|
|
35
|
+
line="${line%$'\r'}"
|
|
36
|
+
case "$line" in
|
|
37
|
+
"$IS_SCHEMA_VERSION_KEY:"*) ;;
|
|
38
|
+
*) continue ;;
|
|
39
|
+
esac
|
|
40
|
+
v="${line#*:}"
|
|
41
|
+
v="${v#"${v%%[![:space:]]*}"}"
|
|
42
|
+
case "$v" in \"*|\'*) v="${v#?}" ;; esac
|
|
43
|
+
v="${v%"${v##*[![:space:]]}"}"
|
|
44
|
+
case "$v" in *\"|*\') v="${v%?}" ;; esac
|
|
45
|
+
v="${v%"${v##*[![:space:]]}"}"
|
|
46
|
+
IS_SCHEMA_VERSION="$v"
|
|
47
|
+
IS_SCHEMA_VERSION_FOUND=1
|
|
48
|
+
return 0
|
|
49
|
+
done < "$cf"
|
|
50
|
+
}
|
|
51
|
+
|
|
23
52
|
# read_schema_version <config_file> → applied version, or "" if absent.
|
|
24
53
|
read_schema_version() {
|
|
25
|
-
|
|
26
|
-
[
|
|
27
|
-
|
|
28
|
-
{ sub(/\r$/, "") }
|
|
29
|
-
$0 ~ "^" k ":" {
|
|
30
|
-
v = $0; sub(/^[^:]*:[[:space:]]*/, "", v)
|
|
31
|
-
gsub(/^["\047]|["\047][[:space:]]*$/, "", v)
|
|
32
|
-
sub(/[[:space:]]+$/, "", v)
|
|
33
|
-
print v; exit
|
|
34
|
-
}
|
|
35
|
-
' "$cf"
|
|
54
|
+
read_schema_version_var "$1"
|
|
55
|
+
[ "$IS_SCHEMA_VERSION_FOUND" = 1 ] || return 0
|
|
56
|
+
printf '%s\n' "$IS_SCHEMA_VERSION"
|
|
36
57
|
}
|
|
37
58
|
|
|
38
59
|
# stamp_schema_version <config_file> <version> — idempotent, transactional upsert of
|
|
@@ -74,12 +95,29 @@ is_status() {
|
|
|
74
95
|
fi
|
|
75
96
|
}
|
|
76
97
|
|
|
77
|
-
# Engine version =
|
|
98
|
+
# Engine version = VERSION one level above this lib (BASH_SOURCE works when
|
|
78
99
|
# sourced). Empty if unreadable — callers treat empty as "no guard".
|
|
100
|
+
# engine_version_var sets IS_ENGINE_VERSION, and IS_ENGINE_VERSION_FOUND=1 when
|
|
101
|
+
# the file exists; it reads the file once per process.
|
|
102
|
+
engine_version_var() {
|
|
103
|
+
[ -z "${IS_ENGINE_VERSION_FOUND:-}" ] || return 0
|
|
104
|
+
local lib="${BASH_SOURCE[0]}" vf content=""
|
|
105
|
+
case "$lib" in
|
|
106
|
+
*/*) lib="${lib%/*}" ;;
|
|
107
|
+
*) lib="." ;;
|
|
108
|
+
esac
|
|
109
|
+
vf="$lib/../VERSION"
|
|
110
|
+
IS_ENGINE_VERSION=""
|
|
111
|
+
IS_ENGINE_VERSION_FOUND=0
|
|
112
|
+
[ -f "$vf" ] || return 0
|
|
113
|
+
IFS= read -r -d '' content < "$vf" || true
|
|
114
|
+
IS_ENGINE_VERSION="${content//[$' \t\r\n']/}"
|
|
115
|
+
IS_ENGINE_VERSION_FOUND=1
|
|
116
|
+
}
|
|
117
|
+
|
|
79
118
|
engine_version() {
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
[ -f "$vf" ] && tr -d ' \t\r\n' < "$vf"
|
|
119
|
+
engine_version_var
|
|
120
|
+
[ "$IS_ENGINE_VERSION_FOUND" = 1 ] && printf '%s' "$IS_ENGINE_VERSION"
|
|
83
121
|
}
|
|
84
122
|
|
|
85
123
|
# _ver_gt A B → true if semver A is strictly greater than B (numeric x.y.z;
|
|
@@ -118,9 +156,11 @@ _ver_major() {
|
|
|
118
156
|
# a project stamped ahead exactly as it found it (project_needs_upgrade).
|
|
119
157
|
check_version_compat() {
|
|
120
158
|
local cf="$1" stamp eng
|
|
121
|
-
|
|
159
|
+
read_schema_version_var "$cf"
|
|
160
|
+
stamp="$IS_SCHEMA_VERSION"
|
|
122
161
|
[ -n "$stamp" ] || return 0
|
|
123
|
-
|
|
162
|
+
engine_version_var
|
|
163
|
+
eng="$IS_ENGINE_VERSION"
|
|
124
164
|
[ -n "$eng" ] || return 0
|
|
125
165
|
_ver_gt "$stamp" "$eng" || return 0
|
|
126
166
|
if [ "$(_ver_major "$stamp")" -gt "$(_ver_major "$eng")" ]; then
|
package/engine/sync.sh
CHANGED
|
@@ -41,8 +41,11 @@ if [ "$_vc_rc" -ne 0 ]; then exit "$_vc_rc"; fi
|
|
|
41
41
|
# so the CLI lifecycle preflight must align the project first. An ABSENT stamp
|
|
42
42
|
# means the same thing — a manifest with no
|
|
43
43
|
# `schema_version` must not silently sync past a schema change.
|
|
44
|
-
|
|
45
|
-
|
|
44
|
+
read_schema_version_var "$_cf"
|
|
45
|
+
_stamp="$IS_SCHEMA_VERSION"
|
|
46
|
+
engine_version_var
|
|
47
|
+
[ "$IS_ENGINE_VERSION_FOUND" = 1 ] || exit 1
|
|
48
|
+
_eng="$IS_ENGINE_VERSION"
|
|
46
49
|
if [ -z "$_stamp" ]; then
|
|
47
50
|
is_status needs-update "stamped= engine=$_eng (no schema_version)"
|
|
48
51
|
echo "ERROR: the manifest has no schema_version — schema un-applied." >&2
|
|
@@ -62,7 +65,12 @@ fi
|
|
|
62
65
|
REPO_ROOT_RAW="${REPO_ROOT:-$(git rev-parse --show-toplevel 2>/dev/null || dirname "$CONFIG_FILE")}"
|
|
63
66
|
REPO_ROOT="$(cd "$REPO_ROOT_RAW" && pwd)"
|
|
64
67
|
unset REPO_ROOT_RAW
|
|
65
|
-
|
|
68
|
+
case "$CONFIG_FILE" in
|
|
69
|
+
*/*) _cf_dir="${CONFIG_FILE%/*}"; [ -n "$_cf_dir" ] || _cf_dir="/" ;;
|
|
70
|
+
*) _cf_dir="." ;;
|
|
71
|
+
esac
|
|
72
|
+
CONFIG_FILE="$(cd "$_cf_dir" && pwd)/${CONFIG_FILE##*/}"
|
|
73
|
+
unset _cf_dir
|
|
66
74
|
|
|
67
75
|
# Layout tokens for generated output (see finalize_output_file in common.sh).
|
|
68
76
|
# Package-shipped rules/agents cannot hardcode the content dir's name — the
|
|
@@ -92,13 +100,12 @@ echo ""
|
|
|
92
100
|
# adapter process substitutions — hits the in-memory copy instead of
|
|
93
101
|
# spawning awk.
|
|
94
102
|
load_targets_cache "$CONFIG_FILE"
|
|
95
|
-
|
|
96
|
-
load_yaml_list "$CONFIG_FILE" "$section"
|
|
97
|
-
done
|
|
103
|
+
load_yaml_lists "$CONFIG_FILE" rules agents skills ignore submodules
|
|
98
104
|
|
|
99
105
|
# Lint frontmatter across all source files (rules, agents, skills).
|
|
100
106
|
# Catches issues like unquoted colons that strict YAML consumers reject.
|
|
101
107
|
LINT_FILES=()
|
|
108
|
+
LINT_SKILL_DIRS=()
|
|
102
109
|
for section in rules agents skills; do
|
|
103
110
|
load_yaml_list "$CONFIG_FILE" "$section"
|
|
104
111
|
while IFS= read -r src; do
|
|
@@ -106,9 +113,7 @@ for section in rules agents skills; do
|
|
|
106
113
|
src_dir="$REPO_ROOT/$src"
|
|
107
114
|
[ -d "$src_dir" ] || continue
|
|
108
115
|
if [ "$section" = "skills" ]; then
|
|
109
|
-
|
|
110
|
-
[ -n "$f" ] && LINT_FILES+=("$f")
|
|
111
|
-
done < <(find "$src_dir" -mindepth 2 -maxdepth 2 -name 'SKILL.md' 2>/dev/null)
|
|
116
|
+
LINT_SKILL_DIRS+=("$src_dir")
|
|
112
117
|
else
|
|
113
118
|
for f in "$src_dir"/*.md; do
|
|
114
119
|
[ -f "$f" ] && LINT_FILES+=("$f")
|
|
@@ -116,10 +121,27 @@ for section in rules agents skills; do
|
|
|
116
121
|
fi
|
|
117
122
|
done <<< "$IS_YAML_LIST"
|
|
118
123
|
done
|
|
124
|
+
# One find for every skills source: it walks its start points in order.
|
|
125
|
+
if [ "${#LINT_SKILL_DIRS[@]}" -gt 0 ]; then
|
|
126
|
+
while IFS= read -r f; do
|
|
127
|
+
[ -n "$f" ] && LINT_FILES+=("$f")
|
|
128
|
+
done < <(find "${LINT_SKILL_DIRS[@]}" -mindepth 2 -maxdepth 2 -name 'SKILL.md' 2>/dev/null)
|
|
129
|
+
fi
|
|
119
130
|
if [ "${#LINT_FILES[@]}" -gt 0 ]; then
|
|
120
131
|
lint_frontmatter_files "${LINT_FILES[@]}"
|
|
121
132
|
fi
|
|
122
133
|
|
|
134
|
+
# Sources stay read-only for the whole run (validate_output_path refuses an
|
|
135
|
+
# output inside one), so read_source_artifact_files enumerates each section
|
|
136
|
+
# once, in this shell, and its later callers — the agents adapter, the shared
|
|
137
|
+
# skill directory, the context report — replay it. Adapters that list sources
|
|
138
|
+
# with in-shell globs spawn nothing and keep their own, locale-ordered listing.
|
|
139
|
+
# shellcheck disable=SC2034 # read by read_source_artifact_files in lib/common.sh
|
|
140
|
+
IS_SOURCE_FILES_MEMO=1
|
|
141
|
+
for section in rules agents skills; do
|
|
142
|
+
read_source_artifact_files "$REPO_ROOT" "$CONFIG_FILE" "$section"
|
|
143
|
+
done
|
|
144
|
+
|
|
123
145
|
# Adapters come from two places, discovered by filename (minus `.sh`,
|
|
124
146
|
# `_template` excluded):
|
|
125
147
|
#
|
|
@@ -152,7 +174,8 @@ for adapters_dir in "$SCRIPT_DIR/adapters" "$INTELLIGENCE_DIR/adapters"; do
|
|
|
152
174
|
[ -d "$adapters_dir" ] || continue
|
|
153
175
|
for adapter_file in "$adapters_dir"/*.sh; do
|
|
154
176
|
[ -f "$adapter_file" ] || continue
|
|
155
|
-
adapter_name="$
|
|
177
|
+
adapter_name="${adapter_file##*/}"
|
|
178
|
+
adapter_name="${adapter_name%.sh}"
|
|
156
179
|
[ "$adapter_name" = "_template" ] && continue
|
|
157
180
|
register_adapter "$adapter_name" "$adapter_file"
|
|
158
181
|
done
|
|
@@ -170,6 +193,15 @@ SYNC_TX_ACTIVE=0
|
|
|
170
193
|
SYNC_TX_SEEN_LIST=$'\n'
|
|
171
194
|
SYNC_TX_COUNT=0
|
|
172
195
|
|
|
196
|
+
# Independent work runs concurrently unless INTELLIGENCE_SYNC_SERIAL=1 asks for
|
|
197
|
+
# the one-at-a-time order every earlier engine used (decision 0011). Every
|
|
198
|
+
# background job is listed in SYNC_BG_PIDS so an early exit stops it before
|
|
199
|
+
# anything is restored or removed.
|
|
200
|
+
SYNC_PARALLEL=1
|
|
201
|
+
[ "${INTELLIGENCE_SYNC_SERIAL:-0}" != "1" ] || SYNC_PARALLEL=0
|
|
202
|
+
SYNC_BG_PIDS=()
|
|
203
|
+
SYNC_COPY_INDEXES=()
|
|
204
|
+
|
|
173
205
|
snapshot_sync_path() {
|
|
174
206
|
local adapter_name="$1" rel="$2" src index present=0
|
|
175
207
|
case "$SYNC_TX_SEEN_LIST" in
|
|
@@ -180,7 +212,15 @@ snapshot_sync_path() {
|
|
|
180
212
|
index="$SYNC_TX_COUNT"
|
|
181
213
|
src="$REPO_ROOT/$rel"
|
|
182
214
|
if [ -e "$src" ] || [ -L "$src" ]; then
|
|
183
|
-
|
|
215
|
+
# Copies of distinct paths into distinct slots: they can overlap, and
|
|
216
|
+
# The snapshot wait before the render collects every status.
|
|
217
|
+
if [ "$SYNC_PARALLEL" = 1 ]; then
|
|
218
|
+
cp -a "$src" "$SYNC_TX_DIR/data/$index" 2> "$SYNC_TX_DIR/copy.$index.err" &
|
|
219
|
+
SYNC_BG_PIDS+=("$!")
|
|
220
|
+
SYNC_COPY_INDEXES+=("$index")
|
|
221
|
+
else
|
|
222
|
+
cp -a "$src" "$SYNC_TX_DIR/data/$index"
|
|
223
|
+
fi
|
|
184
224
|
present=1
|
|
185
225
|
fi
|
|
186
226
|
printf '%s\t%s\t%s\n' "$index" "$rel" "$present" >> "$SYNC_TX_INDEX"
|
|
@@ -200,10 +240,36 @@ restore_sync_snapshot() {
|
|
|
200
240
|
done < "$SYNC_TX_INDEX"
|
|
201
241
|
}
|
|
202
242
|
|
|
243
|
+
# stop_sync_jobs — reap every background job before restoring or removing
|
|
244
|
+
# anything, so no copy or adapter can write after that point. It waits rather
|
|
245
|
+
# than kills: killing a job's shell would leave its cp or awk still writing.
|
|
246
|
+
# Ctrl-C already reaches every job through the terminal's process group.
|
|
247
|
+
stop_sync_jobs() {
|
|
248
|
+
local pid
|
|
249
|
+
for pid in "${SYNC_BG_PIDS[@]+"${SYNC_BG_PIDS[@]}"}"; do
|
|
250
|
+
wait "$pid" 2>/dev/null
|
|
251
|
+
done
|
|
252
|
+
SYNC_BG_PIDS=()
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
# wait_sync_jobs — collect every background job's status; the first failure
|
|
256
|
+
# becomes this shell's, as it would have had the work run in the foreground.
|
|
257
|
+
wait_sync_jobs() {
|
|
258
|
+
local pid rc=0 first=0
|
|
259
|
+
for pid in "${SYNC_BG_PIDS[@]+"${SYNC_BG_PIDS[@]}"}"; do
|
|
260
|
+
rc=0
|
|
261
|
+
wait "$pid" || rc=$?
|
|
262
|
+
[ "$first" -ne 0 ] || first="$rc"
|
|
263
|
+
done
|
|
264
|
+
SYNC_BG_PIDS=()
|
|
265
|
+
return "$first"
|
|
266
|
+
}
|
|
267
|
+
|
|
203
268
|
finish_sync_transaction() {
|
|
204
269
|
local rc=$?
|
|
205
270
|
trap - EXIT INT TERM
|
|
206
271
|
set +e
|
|
272
|
+
stop_sync_jobs
|
|
207
273
|
if [ "${SYNC_TX_ACTIVE:-0}" = "1" ] && [ "$rc" -ne 0 ]; then
|
|
208
274
|
restore_sync_snapshot
|
|
209
275
|
echo "ERROR: sync failed; all adapter-owned paths were restored to their pre-sync state." >&2
|
|
@@ -228,6 +294,8 @@ note_agents_dependent() {
|
|
|
228
294
|
[ -n "$agents_dependents" ] && agents_dependents="$agents_dependents, "
|
|
229
295
|
agents_dependents="$agents_dependents$1"
|
|
230
296
|
}
|
|
297
|
+
SELECTED_RECORDS=()
|
|
298
|
+
RENDER_PARALLEL="$SYNC_PARALLEL"
|
|
231
299
|
preflight_idx=0
|
|
232
300
|
while [ "$preflight_idx" -lt "${#ADAPTERS[@]}" ]; do
|
|
233
301
|
adapter="${ADAPTERS[$preflight_idx]}"
|
|
@@ -255,6 +323,14 @@ while [ "$preflight_idx" -lt "${#ADAPTERS[@]}" ]; do
|
|
|
255
323
|
fi
|
|
256
324
|
|
|
257
325
|
validate_output_path "$REPO_ROOT" "$CONFIG_FILE" "$adapter" "$REPO_ROOT/$output"
|
|
326
|
+
# The scheduler below reads ownership and requirements from these records.
|
|
327
|
+
# A project adapter is executable code whose reads no contract declares, so
|
|
328
|
+
# its presence keeps the whole render serial.
|
|
329
|
+
SELECTED_RECORDS+=("$records")
|
|
330
|
+
case "$adapter_file" in
|
|
331
|
+
"$SCRIPT_DIR/adapters/"*) ;;
|
|
332
|
+
*) RENDER_PARALLEL=0 ;;
|
|
333
|
+
esac
|
|
258
334
|
while IFS=$'\t' read -r kind value; do
|
|
259
335
|
[ "$kind" = "requires" ] || continue
|
|
260
336
|
[ "$value" = "agents" ] && note_agents_dependent "$adapter"
|
|
@@ -280,15 +356,30 @@ if [ -n "$agents_dependents" ]; then
|
|
|
280
356
|
target_output_var "$CONFIG_FILE" "agents"
|
|
281
357
|
# Resolved, not lexical: `./` renders `./AGENTS.md`, which IS the
|
|
282
358
|
# workspace-root file every dependent adapter reads.
|
|
283
|
-
|
|
359
|
+
agents_output_path_var "${IS_TGT_OUTPUT:-.agents}"
|
|
360
|
+
adapter_contract_rel_path "$IS_AGENTS_OUTPUT_PATH"
|
|
284
361
|
agents_rel="$IS_ADAPTER_REL_PATH"
|
|
285
362
|
if [ "$agents_rel" != "AGENTS.md" ]; then
|
|
286
363
|
echo "WARNING: targets.agents.output renders '$agents_rel', not the workspace-root AGENTS.md. These adapters skip always-on rules because AGENTS.md carries them, and they read it at the root only: $agents_dependents. No tool loads those rules from '$agents_rel'." >&2
|
|
287
364
|
fi
|
|
288
365
|
fi
|
|
366
|
+
# Every snapshot copy has landed before any adapter writes. Their diagnostics
|
|
367
|
+
# print in path order once all have finished, so a failed copy reads the same
|
|
368
|
+
# whichever finished first; it reports after preflight checked every adapter,
|
|
369
|
+
# where the serial order stopped at that adapter (decision 0011).
|
|
370
|
+
snapshot_rc=0
|
|
371
|
+
wait_sync_jobs || snapshot_rc=$?
|
|
372
|
+
for index in "${SYNC_COPY_INDEXES[@]+"${SYNC_COPY_INDEXES[@]}"}"; do
|
|
373
|
+
[ ! -s "$SYNC_TX_DIR/copy.$index.err" ] || cat "$SYNC_TX_DIR/copy.$index.err" >&2
|
|
374
|
+
done
|
|
375
|
+
[ "$snapshot_rc" -eq 0 ] || exit "$snapshot_rc"
|
|
289
376
|
SYNC_TX_ACTIVE=1
|
|
290
377
|
|
|
291
|
-
|
|
378
|
+
# The run list: selected, enabled adapters in discovery order, refused with the
|
|
379
|
+
# same messages the one-at-a-time loop gave. Preflight validated every output.
|
|
380
|
+
RUN_NAMES=()
|
|
381
|
+
RUN_FILES=()
|
|
382
|
+
RUN_DIRS=()
|
|
292
383
|
adapter_count=${#ADAPTERS[@]}
|
|
293
384
|
adapter_idx=0
|
|
294
385
|
|
|
@@ -328,14 +419,192 @@ while [ "$adapter_idx" -lt "$adapter_count" ]; do
|
|
|
328
419
|
# output, and `agents` overwrites whatever single file it is handed. Both
|
|
329
420
|
# turn a bad config line into a destructive write.
|
|
330
421
|
validate_output_path "$REPO_ROOT" "$CONFIG_FILE" "$adapter" "$output_dir"
|
|
422
|
+
RUN_NAMES+=("$adapter")
|
|
423
|
+
RUN_FILES+=("$adapter_file")
|
|
424
|
+
RUN_DIRS+=("$output_dir")
|
|
425
|
+
done
|
|
426
|
+
synced=${#RUN_NAMES[@]}
|
|
331
427
|
|
|
332
|
-
|
|
428
|
+
# run_adapter <index> — source one adapter and render it.
|
|
429
|
+
run_adapter() {
|
|
333
430
|
# shellcheck source=/dev/null
|
|
334
|
-
source "$
|
|
335
|
-
"sync_to_$
|
|
431
|
+
source "${RUN_FILES[$1]}"
|
|
432
|
+
"sync_to_${RUN_NAMES[$1]}" "$REPO_ROOT" "$CONFIG_FILE" "${RUN_DIRS[$1]}"
|
|
336
433
|
echo ""
|
|
337
|
-
|
|
338
|
-
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
# managed_paths_meet <a> <b> — two resolved paths are one tree: equal, or one
|
|
437
|
+
# inside the other.
|
|
438
|
+
managed_paths_meet() {
|
|
439
|
+
[ "$1" = "$2" ] && return 0
|
|
440
|
+
case "$1" in "$2"/*) return 0 ;; esac
|
|
441
|
+
case "$2" in "$1"/*) return 0 ;; esac
|
|
442
|
+
return 1
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
# plan_concurrent_render — fill RUN_CHAIN (each adapter's chain, named by its
|
|
446
|
+
# first member) and CHAIN_WAVE (when that chain may start), from the contract
|
|
447
|
+
# records preflight kept. Adapters that manage one tree form a chain and run in
|
|
448
|
+
# list order inside one job: they share that tree, and sync_open_skill_dirs
|
|
449
|
+
# replays the first one's work in the same shell. An adapter that requires
|
|
450
|
+
# another starts in a later wave than the chain holding it — Codex reads the
|
|
451
|
+
# rendered AGENTS.md. Returns 1 when no safe plan exists; the render is then
|
|
452
|
+
# serial.
|
|
453
|
+
plan_concurrent_render() {
|
|
454
|
+
local n="$synced" i j k kind value old value_chain
|
|
455
|
+
local -a paths=() path_chain=()
|
|
456
|
+
[ "${#SELECTED_RECORDS[@]}" -eq "$n" ] || return 1
|
|
457
|
+
RUN_CHAIN=()
|
|
458
|
+
CHAIN_WAVE=()
|
|
459
|
+
for ((i = 0; i < n; i++)); do
|
|
460
|
+
RUN_CHAIN[i]=$i
|
|
461
|
+
CHAIN_WAVE[i]=0
|
|
462
|
+
done
|
|
463
|
+
for ((i = 0; i < n; i++)); do
|
|
464
|
+
while IFS=$'\t' read -r kind value; do
|
|
465
|
+
[ "$kind" = managed ] || continue
|
|
466
|
+
normalize_path_var "$REPO_ROOT/$value"
|
|
467
|
+
value="$IS_NORM_PATH"
|
|
468
|
+
for ((k = 0; k < ${#paths[@]}; k++)); do
|
|
469
|
+
managed_paths_meet "${paths[k]}" "$value" || continue
|
|
470
|
+
old="${RUN_CHAIN[i]}"
|
|
471
|
+
[ "$old" != "${path_chain[k]}" ] || continue
|
|
472
|
+
# Merge into the chain whose first member comes earlier.
|
|
473
|
+
if [ "$old" -lt "${path_chain[k]}" ]; then
|
|
474
|
+
old="${path_chain[k]}"
|
|
475
|
+
value_chain="${RUN_CHAIN[i]}"
|
|
476
|
+
else
|
|
477
|
+
value_chain="${path_chain[k]}"
|
|
478
|
+
fi
|
|
479
|
+
for ((j = 0; j < n; j++)); do
|
|
480
|
+
[ "${RUN_CHAIN[j]}" != "$old" ] || RUN_CHAIN[j]="$value_chain"
|
|
481
|
+
done
|
|
482
|
+
for ((j = 0; j < ${#paths[@]}; j++)); do
|
|
483
|
+
[ "${path_chain[j]}" != "$old" ] || path_chain[j]="$value_chain"
|
|
484
|
+
done
|
|
485
|
+
done
|
|
486
|
+
paths+=("$value")
|
|
487
|
+
path_chain+=("${RUN_CHAIN[i]}")
|
|
488
|
+
done <<< "${SELECTED_RECORDS[i]}"
|
|
489
|
+
done
|
|
490
|
+
# Relax requirement edges; a plan still moving after n rounds has a cycle.
|
|
491
|
+
local round changed want
|
|
492
|
+
for ((round = 0; round <= n; round++)); do
|
|
493
|
+
changed=0
|
|
494
|
+
for ((i = 0; i < n; i++)); do
|
|
495
|
+
while IFS=$'\t' read -r kind value; do
|
|
496
|
+
[ "$kind" = requires ] || continue
|
|
497
|
+
for ((j = 0; j < n; j++)); do
|
|
498
|
+
[ "${RUN_NAMES[j]}" = "$value" ] || continue
|
|
499
|
+
[ "${RUN_CHAIN[j]}" != "${RUN_CHAIN[i]}" ] || continue
|
|
500
|
+
want=$(( CHAIN_WAVE[RUN_CHAIN[j]] + 1 ))
|
|
501
|
+
if [ "${CHAIN_WAVE[RUN_CHAIN[i]]}" -lt "$want" ]; then
|
|
502
|
+
CHAIN_WAVE[RUN_CHAIN[i]]="$want"
|
|
503
|
+
changed=1
|
|
504
|
+
fi
|
|
505
|
+
done
|
|
506
|
+
done <<< "${SELECTED_RECORDS[i]}"
|
|
507
|
+
done
|
|
508
|
+
[ "$changed" = 1 ] || return 0
|
|
509
|
+
done
|
|
510
|
+
return 1
|
|
511
|
+
}
|
|
512
|
+
|
|
513
|
+
# render_concurrently — run each wave's chains as background jobs, one output
|
|
514
|
+
# buffer per adapter. Buffers print in list order as soon as every earlier
|
|
515
|
+
# adapter's has printed, so progress still streams adapter by adapter. A failed
|
|
516
|
+
# adapter ends the run with its own status after the buffers of the adapters
|
|
517
|
+
# that completed before it in the list; the EXIT handler then restores every
|
|
518
|
+
# snapshot, exactly as when the adapters ran one at a time.
|
|
519
|
+
render_concurrently() {
|
|
520
|
+
local n="$synced" buf="$SYNC_TX_DIR/out" wave last_wave=0 c k pid rc
|
|
521
|
+
local printed=0 failed=-1 failed_rc=0
|
|
522
|
+
local -a wave_pids=() wave_chains=() files=() job_rc=()
|
|
523
|
+
mkdir -p "$buf"
|
|
524
|
+
for ((c = 0; c < n; c++)); do
|
|
525
|
+
[ "${CHAIN_WAVE[c]}" -le "$last_wave" ] || last_wave="${CHAIN_WAVE[c]}"
|
|
526
|
+
done
|
|
527
|
+
for ((wave = 0; wave <= last_wave; wave++)); do
|
|
528
|
+
wave_pids=()
|
|
529
|
+
wave_chains=()
|
|
530
|
+
for ((c = 0; c < n; c++)); do
|
|
531
|
+
[ "${RUN_CHAIN[c]}" = "$c" ] && [ "${CHAIN_WAVE[c]}" = "$wave" ] || continue
|
|
532
|
+
(
|
|
533
|
+
for ((k = 0; k < n; k++)); do
|
|
534
|
+
[ "${RUN_CHAIN[k]}" = "$c" ] || continue
|
|
535
|
+
run_adapter "$k" > "$buf/$k" 2>&1
|
|
536
|
+
: > "$buf/$k.ok"
|
|
537
|
+
done
|
|
538
|
+
) &
|
|
539
|
+
wave_pids+=("$!")
|
|
540
|
+
wave_chains+=("$c")
|
|
541
|
+
SYNC_BG_PIDS+=("$!")
|
|
542
|
+
done
|
|
543
|
+
for ((k = 0; k < ${#wave_pids[@]}; k++)); do
|
|
544
|
+
rc=0
|
|
545
|
+
wait "${wave_pids[k]}" || rc=$?
|
|
546
|
+
job_rc[wave_chains[k]]="$rc"
|
|
547
|
+
# Stream while the wave runs: every buffer whose predecessors have
|
|
548
|
+
# all printed goes out now. Only completed adapters are marked, so
|
|
549
|
+
# nothing printed here can precede a failure in list order.
|
|
550
|
+
files=()
|
|
551
|
+
while [ "$printed" -lt "$n" ] && [ -f "$buf/$printed.ok" ]; do
|
|
552
|
+
files+=("$buf/$printed")
|
|
553
|
+
printed=$((printed + 1))
|
|
554
|
+
done
|
|
555
|
+
[ "${#files[@]}" -eq 0 ] || cat "${files[@]}"
|
|
556
|
+
done
|
|
557
|
+
SYNC_BG_PIDS=()
|
|
558
|
+
# The first adapter in list order that started and did not finish.
|
|
559
|
+
for ((k = 0; k < n; k++)); do
|
|
560
|
+
if [ -f "$buf/$k" ] && [ ! -f "$buf/$k.ok" ]; then
|
|
561
|
+
failed=$k
|
|
562
|
+
failed_rc="${job_rc[RUN_CHAIN[k]]:-1}"
|
|
563
|
+
[ "$failed_rc" != 0 ] || failed_rc=1
|
|
564
|
+
break
|
|
565
|
+
fi
|
|
566
|
+
done
|
|
567
|
+
# A job that failed before its adapter's buffer existed still fails
|
|
568
|
+
# the run, charged to the first member of that chain not marked done.
|
|
569
|
+
if [ "$failed" -lt 0 ]; then
|
|
570
|
+
for ((k = 0; k < ${#wave_chains[@]}; k++)); do
|
|
571
|
+
rc="${job_rc[wave_chains[k]]}"
|
|
572
|
+
[ "$rc" = 0 ] && continue
|
|
573
|
+
for ((c = 0; c < n; c++)); do
|
|
574
|
+
[ "${RUN_CHAIN[c]}" = "${wave_chains[k]}" ] && [ ! -f "$buf/$c.ok" ] || continue
|
|
575
|
+
failed=$c
|
|
576
|
+
break
|
|
577
|
+
done
|
|
578
|
+
[ "$failed" -ge 0 ] || failed="${wave_chains[k]}"
|
|
579
|
+
failed_rc="$rc"
|
|
580
|
+
break
|
|
581
|
+
done
|
|
582
|
+
fi
|
|
583
|
+
files=()
|
|
584
|
+
if [ "$failed" -ge 0 ]; then
|
|
585
|
+
for ((k = printed; k <= failed; k++)); do
|
|
586
|
+
[ -f "$buf/$k" ] && files+=("$buf/$k")
|
|
587
|
+
done
|
|
588
|
+
[ "${#files[@]}" -eq 0 ] || cat "${files[@]}"
|
|
589
|
+
exit "$failed_rc"
|
|
590
|
+
fi
|
|
591
|
+
while [ "$printed" -lt "$n" ] && [ -f "$buf/$printed.ok" ]; do
|
|
592
|
+
files+=("$buf/$printed")
|
|
593
|
+
printed=$((printed + 1))
|
|
594
|
+
done
|
|
595
|
+
[ "${#files[@]}" -eq 0 ] || cat "${files[@]}"
|
|
596
|
+
done
|
|
597
|
+
}
|
|
598
|
+
|
|
599
|
+
if [ "$RENDER_PARALLEL" = 1 ] && [ "$synced" -gt 1 ] && plan_concurrent_render; then
|
|
600
|
+
render_concurrently
|
|
601
|
+
else
|
|
602
|
+
adapter_idx=0
|
|
603
|
+
while [ "$adapter_idx" -lt "$synced" ]; do
|
|
604
|
+
run_adapter "$adapter_idx"
|
|
605
|
+
adapter_idx=$((adapter_idx + 1))
|
|
606
|
+
done
|
|
607
|
+
fi
|
|
339
608
|
|
|
340
609
|
if [ $synced -eq 0 ]; then
|
|
341
610
|
if [ -n "$TARGET_FILTER" ]; then
|
|
@@ -348,18 +617,35 @@ if [ $synced -eq 0 ]; then
|
|
|
348
617
|
fi
|
|
349
618
|
|
|
350
619
|
SYNC_TX_ACTIVE=0
|
|
351
|
-
rm -rf "$SYNC_TX_DIR"
|
|
352
|
-
trap - EXIT INT TERM
|
|
353
|
-
|
|
354
|
-
# Warn about unsynced directories
|
|
355
|
-
warn_unsynced "$REPO_ROOT" "$CONFIG_FILE"
|
|
356
620
|
|
|
357
|
-
#
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
#
|
|
361
|
-
#
|
|
362
|
-
|
|
621
|
+
# After rendering, three read-only reports, in this order:
|
|
622
|
+
# - directories that look like sources but are not wired in (a repository-wide
|
|
623
|
+
# scan, so it runs only once every output exists);
|
|
624
|
+
# - adapter-agnostic source context pressure;
|
|
625
|
+
# - model overrides that drift from intelligence-sync defaults (helpful when
|
|
626
|
+
# defaults move forward — e.g., gpt-5.5 -> gpt-5.6).
|
|
627
|
+
# Concurrently, the scan runs beside the other two and beside removing the
|
|
628
|
+
# snapshots; the buffers print in the order above.
|
|
629
|
+
if [ "$SYNC_PARALLEL" = 1 ]; then
|
|
630
|
+
rm -rf "$SYNC_TX_DIR/data" &
|
|
631
|
+
SYNC_BG_PIDS+=("$!")
|
|
632
|
+
warn_unsynced "$REPO_ROOT" "$CONFIG_FILE" > "$SYNC_TX_DIR/unsynced" 2>&1 &
|
|
633
|
+
SYNC_BG_PIDS+=("$!")
|
|
634
|
+
{
|
|
635
|
+
report_context_source_sizes "$REPO_ROOT" "$CONFIG_FILE"
|
|
636
|
+
report_model_drift "$CONFIG_FILE"
|
|
637
|
+
} > "$SYNC_TX_DIR/reports" 2>&1
|
|
638
|
+
wait_sync_jobs
|
|
639
|
+
cat "$SYNC_TX_DIR/unsynced" "$SYNC_TX_DIR/reports"
|
|
640
|
+
rm -rf "$SYNC_TX_DIR"
|
|
641
|
+
trap - EXIT INT TERM
|
|
642
|
+
else
|
|
643
|
+
rm -rf "$SYNC_TX_DIR"
|
|
644
|
+
trap - EXIT INT TERM
|
|
645
|
+
warn_unsynced "$REPO_ROOT" "$CONFIG_FILE"
|
|
646
|
+
report_context_source_sizes "$REPO_ROOT" "$CONFIG_FILE"
|
|
647
|
+
report_model_drift "$CONFIG_FILE"
|
|
648
|
+
fi
|
|
363
649
|
|
|
364
650
|
echo ""
|
|
365
651
|
# sync.sh never changes project schemas (the CLI preflight owns that), so
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@ainova-systems/intelligence",
|
|
3
|
-
"version": "0.17.
|
|
3
|
+
"version": "0.17.3",
|
|
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"
|
|
@@ -98,6 +98,13 @@ other's output and both runs report success. A filtered `sync <adapter>` is
|
|
|
98
98
|
checked the same way: it prunes the same paths while never loading the adapter
|
|
99
99
|
whose output it destroys.
|
|
100
100
|
|
|
101
|
+
The contract also orders rendering. Built-in adapters render concurrently:
|
|
102
|
+
adapters whose `managed` paths meet run one after another in list order, and an
|
|
103
|
+
adapter starts only after every target it `requires` has finished. An adapter
|
|
104
|
+
that reads another adapter's output while it renders therefore declares
|
|
105
|
+
`requires` on it. A project adapter keeps the whole render serial, because its
|
|
106
|
+
reads are not part of any contract.
|
|
107
|
+
|
|
101
108
|
For an existing `.vscodeignore`, `.npmignore`, or `.dockerignore`, enable/init
|
|
102
109
|
also excludes the configured adapter output plus its `owned`, `managed`, and
|
|
103
110
|
`legacy` paths from published or build artifacts. This packaging policy is
|
|
@@ -121,7 +121,7 @@ Practice:
|
|
|
121
121
|
- **`latest` and `*` are for a package you own and release in lockstep.** Anywhere else they hand an upstream author write access to your agents' behavior between two syncs.
|
|
122
122
|
- **Never pin a range to dodge a broken release.** Pin the exact version (`1.4.2`), record why, and remove the pin when the fix ships — a narrowed range hides the reason and outlives the incident.
|
|
123
123
|
|
|
124
|
-
Commit `intelligence.lock`. It records requested versions, source URLs and paths, resolved refs and commit SHAs. After cloning, `intelligence sync` restores
|
|
124
|
+
Commit `intelligence.lock`. It records requested versions, source URLs and paths, resolved refs and commit SHAs. After cloning, or after a pull that moved the lock, `intelligence sync` restores every package the store lacks or holds at another commit, strictly from that lock, before rendering; manifest/lock or SHA drift is refused. Re-run `package add` when deliberately changing a source.
|
|
125
125
|
|
|
126
126
|
`@ainova-systems/sync` is ordinary package content exact-pinned to the bundled engine version. `intelligence init` installs it unless `--bare` is used. Lifecycle preflight keeps that pin and `schema_version` aligned with the installed CLI; package-range updates never move it independently.
|
|
127
127
|
|
|
@@ -416,7 +416,7 @@ The permanent applied-schema key is the top-level scalar `schema_version` in `in
|
|
|
416
416
|
The public lifecycle is deliberately compact:
|
|
417
417
|
|
|
418
418
|
- `intelligence init [--preview|--apply]` is universal: it creates a new setup, aligns an existing Intelligence project, or plans/applies conversion of an eligible legacy Intelligence Sync project.
|
|
419
|
-
- `intelligence sync [adapter] [--compact] [--force]` first aligns an existing Intelligence project with the installed CLI and restores
|
|
419
|
+
- `intelligence sync [adapter] [--compact] [--force]` first aligns an existing Intelligence project with the installed CLI and restores every package the store lacks or holds at another commit, strictly from `intelligence.lock`. It skips rendering when local inputs and outputs match a previous successful run. Changes or missing outputs trigger rendering; `--force` always renders and refreshes the search for unconfigured source directories. An unchanged run replays the last full run's useful diagnostics. Compact mode shows context sizes, actionable warnings and final status on success, and all diagnostics on failure. In CI it refuses an alignment that would change tracked files and points to a local `intelligence init --apply` plus review/commit.
|
|
420
420
|
- `intelligence update [@scope/name] [--preview|--apply]` is the only update surface. It prints the CLI/project/package plan; default mode prompts, `--preview` never writes, and `--apply` does not prompt. It never moves `ref:` pins.
|
|
421
421
|
- `intelligence upgrade [--next] [--preview|--apply]` replaces the installed CLI with the newest version on its npm channel (`next` for a prerelease or with `--next`, otherwise `latest`) with the same modes. It touches no project, never downgrades, and refuses an installation that npm did not make.
|
|
422
422
|
- `intelligence package add|remove|list|search` owns package inventory.
|