@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.
@@ -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
- local cf="$1"
26
- [ -f "$cf" ] || return 0
27
- awk -v k="$IS_SCHEMA_VERSION_KEY" '
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 = scripts/VERSION next to this lib (BASH_SOURCE works when
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
- local vf
81
- vf="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." 2>/dev/null && pwd)/VERSION"
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
- stamp="$(read_schema_version "$cf")"
159
+ read_schema_version_var "$cf"
160
+ stamp="$IS_SCHEMA_VERSION"
122
161
  [ -n "$stamp" ] || return 0
123
- eng="$(engine_version)"
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
- _stamp="$(read_schema_version "$_cf")"
45
- _eng="$(engine_version)"
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
- CONFIG_FILE="$(cd "$(dirname "$CONFIG_FILE")" && pwd)/$(basename "$CONFIG_FILE")"
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
- for section in rules agents skills ignore submodules; do
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
- while IFS= read -r f; do
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="$(basename "$adapter_file" .sh)"
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
- cp -a "$src" "$SYNC_TX_DIR/data/$index"
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
- adapter_contract_rel_path "$(agents_output_path "${IS_TGT_OUTPUT:-.agents}")"
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
- synced=0
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
- # Source adapter and run.
428
+ # run_adapter <index> — source one adapter and render it.
429
+ run_adapter() {
333
430
  # shellcheck source=/dev/null
334
- source "$adapter_file"
335
- "sync_to_$adapter" "$REPO_ROOT" "$CONFIG_FILE" "$output_dir"
431
+ source "${RUN_FILES[$1]}"
432
+ "sync_to_${RUN_NAMES[$1]}" "$REPO_ROOT" "$CONFIG_FILE" "${RUN_DIRS[$1]}"
336
433
  echo ""
337
- synced=$((synced + 1))
338
- done
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
- # Report adapter-agnostic source context pressure on every successful sync.
358
- report_context_source_sizes "$REPO_ROOT" "$CONFIG_FILE"
359
-
360
- # Report model overrides that drift from intelligence-sync defaults
361
- # (helpful when defaults move forward — e.g., gpt-5.5 -> gpt-5.6).
362
- report_model_drift "$CONFIG_FILE"
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.1",
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 a missing store strictly from that lock before rendering; manifest/lock or SHA drift is refused. Re-run `package add` when deliberately changing a source.
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 a missing store 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.
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.