@ainova-systems/intelligence 0.18.1 → 0.20.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (48) hide show
  1. package/cli/commands/init.sh +6 -1
  2. package/cli/commands/package.sh +8 -3
  3. package/cli/commands/registry.sh +1 -1
  4. package/cli/commands/source.sh +56 -17
  5. package/cli/commands/sync.sh +78 -6
  6. package/cli/intelligence +16 -3
  7. package/cli/internal/check.sh +64 -13
  8. package/cli/internal/convert-legacy.sh +1 -1
  9. package/cli/internal/package-add.sh +25 -4
  10. package/cli/internal/package-alias.sh +51 -0
  11. package/cli/internal/package-list.sh +16 -2
  12. package/cli/internal/package-remove.sh +3 -1
  13. package/cli/internal/package-search.sh +1 -1
  14. package/cli/internal/package-update.sh +14 -6
  15. package/cli/internal/restore.sh +3 -2
  16. package/cli/lib/adapter-lifecycle.sh +5 -2
  17. package/cli/lib/cli-common.sh +149 -34
  18. package/cli/lib/gitignore.sh +128 -34
  19. package/cli/lib/lockfile.sh +14 -4
  20. package/cli/lib/manifest.sh +315 -21
  21. package/cli/lib/qmap.awk +97 -32
  22. package/cli/lib/registry.sh +19 -14
  23. package/cli/lib/sync-cache.sh +596 -142
  24. package/cli/lib/sync-lock.sh +19 -2
  25. package/engine/ENGINE_SHA +1 -1
  26. package/engine/VERSION +1 -1
  27. package/engine/adapters/_template.sh +6 -2
  28. package/engine/adapters/agents.sh +8 -2
  29. package/engine/adapters/antigravity.sh +3 -1
  30. package/engine/adapters/claude.sh +29 -12
  31. package/engine/adapters/codex.sh +11 -13
  32. package/engine/adapters/copilot.sh +54 -4
  33. package/engine/adapters/cursor.sh +13 -2
  34. package/engine/adapters/opencode.sh +7 -1
  35. package/engine/adapters/pi.sh +3 -1
  36. package/engine/lib/adapter-contract.sh +51 -8
  37. package/engine/lib/common.sh +1072 -160
  38. package/engine/lib/contract.sh +5 -0
  39. package/engine/sync.sh +128 -28
  40. package/package.json +1 -1
  41. package/packages/sync/references/adapters.md +44 -9
  42. package/packages/sync/references/conventions.md +48 -8
  43. package/packages/sync/references/onboarding-migration.md +3 -3
  44. package/packages/sync/skills/intelligence-learn-from-repository/SKILL.md +3 -2
  45. package/packages/sync/skills/intelligence-review-context/SKILL.md +2 -1
  46. package/packages/sync/skills/intelligence-update-context/SKILL.md +4 -2
  47. package/packages/sync/skills/intelligence-update-context/references/agents.md +7 -3
  48. package/packages/sync/skills/intelligence-update-context/references/skills.md +3 -1
@@ -9,6 +9,10 @@ lock="$IP_ROOT/intelligence.lock"
9
9
  lock_valid=1
10
10
  if ! check_project_lock "$IP_ROOT"; then lock_valid=0; fi
11
11
 
12
+ # Aliases come from the parser the engine resolves them with, in one pass; an
13
+ # alias it would not resolve is shown as such, never echoed.
14
+ aliases="$(read_package_aliases "$manifest")"
15
+
12
16
  count=0
13
17
  while IFS= read -r name; do
14
18
  [ -n "$name" ] || continue
@@ -29,11 +33,21 @@ while IFS= read -r name; do
29
33
  else
30
34
  state="store missing — run 'intelligence sync'"
31
35
  fi
32
- printf '%s %s locked:%s %s\n' \
36
+ alias_note=""
37
+ while IFS=$'\037' read -r a_name a_alias a_verdict; do
38
+ [ "$a_name" = "$name" ] || continue
39
+ case "$a_verdict" in
40
+ ok) alias_note=" alias:$a_alias" ;;
41
+ ambiguous) alias_note=" alias:$a_alias (another package declares it too)" ;;
42
+ *) alias_note=" alias:invalid — intelligence status --check" ;;
43
+ esac
44
+ done <<< "$aliases"
45
+ printf '%s %s locked:%s %s%s\n' \
33
46
  "$name" \
34
47
  "${ref:+ref:$ref}${ref:-${range:-*}}" \
35
48
  "$(pin_label "$ref" "$resolved" "$sha")" \
36
- "$state"
49
+ "$state" \
50
+ "$alias_note"
37
51
  done < <(qmap_keys "$manifest" "packages")
38
52
 
39
53
  if [ "$count" -eq 0 ]; then
@@ -39,7 +39,9 @@ while IFS= read -r k; do
39
39
  done < <(qmap_keys "$manifest" "packages")
40
40
  [ "$known" -eq 1 ] || die "package '$name' is not in the manifest"
41
41
 
42
- unwire_package_sources "$manifest" "$rel"
42
+ # Every entry naming the package, in every spelling, goes while `packages:`
43
+ # still declares it and its alias: that is what resolves the references.
44
+ unwire_package_sources "$manifest" "$name"
43
45
  qmap_delete_key "$manifest" "packages" "$name"
44
46
  lock_remove "$lock" "$name"
45
47
  rm -rf "${IP_ROOT:?}/$rel"
@@ -63,7 +63,7 @@ if [ -n "$manifest" ]; then
63
63
  [ -n "$url" ] || continue
64
64
  index="$(_fetch_index "${url#git+}")"
65
65
  if [ -z "$index" ]; then
66
- echo " WARN: registry unreachable or missing index.yaml: $url" >&2
66
+ echo "WARNING: registry unreachable or missing index.yaml: $url" >&2
67
67
  continue
68
68
  fi
69
69
  emit_index "$index" "$url"
@@ -85,7 +85,7 @@ while IFS= read -r name; do
85
85
  if [ "$probe_rc" -ne 0 ]; then
86
86
  # No answer is not "no change": reporting up to date here would
87
87
  # freeze the pin exactly as the ref-name comparison used to.
88
- echo " WARN: $name: cannot reach $url — ref '$ref' was not checked" >&2
88
+ echo "WARNING: $name: cannot reach $url — ref '$ref' was not checked" >&2
89
89
  echo " $name: $ref (not checked — remote unreachable)"
90
90
  continue
91
91
  fi
@@ -104,7 +104,7 @@ while IFS= read -r name; do
104
104
  echo " $name: $(short_sha "$ref") (pinned commit)"
105
105
  ;;
106
106
  *)
107
- echo " WARN: $name pins ref '$ref', which $url no longer advertises" >&2
107
+ echo "WARNING: $name pins ref '$ref', which $url no longer advertises" >&2
108
108
  echo " $name: $ref (unresolvable — gone upstream)"
109
109
  ;;
110
110
  esac
@@ -167,13 +167,21 @@ while IFS= read -r name; do
167
167
  fi
168
168
  rel=".intelligence/packages/$name"
169
169
  # Fetch into staging FIRST: a failed clone must leave the current install
170
- # fully wired and intact. Only after success is the old shape unwired
171
- # (the new version may have dropped a section dir), the store swapped,
172
- # and the new shape wired.
170
+ # fully wired and intact. Only after success are the sections the new
171
+ # version dropped unwired, in every spelling, the store swapped, and the
172
+ # sections it gained wired. A section it keeps stays where it stands in
173
+ # the list: position is the override order between packages, and an
174
+ # update changes content, not which package wins.
173
175
  staging="$IP_ROOT/.intelligence/.staging-$$"
174
176
  rm -rf "$staging"
175
177
  sha="$(fetch_package "$url" "$tag" "$path" "$staging")"
176
- unwire_package_sources "$manifest" "$rel"
178
+ dropped=()
179
+ for section in rules agents skills; do
180
+ [ -d "$staging/$section" ] || dropped+=("$section")
181
+ done
182
+ if [ "${#dropped[@]}" -gt 0 ]; then
183
+ unwire_package_sources "$manifest" "$name" "${dropped[@]}"
184
+ fi
177
185
  store_record_remove "$IP_ROOT" "$name"
178
186
  rm -rf "${IP_ROOT:?}/$rel"
179
187
  mkdir -p "$(dirname "$IP_ROOT/$rel")"
@@ -7,12 +7,13 @@
7
7
  set -euo pipefail
8
8
  source "$CLI_DIR/lib/cli-common.sh"
9
9
 
10
- frozen=0 force=0 no_sync=0
10
+ frozen=0 force=0 no_sync=0 keep_sources=0
11
11
  while [ $# -gt 0 ]; do
12
12
  case "$1" in
13
13
  --frozen) frozen=1 ;;
14
14
  --force) force=1 ;;
15
15
  --no-sync) no_sync=1 ;;
16
+ --keep-sources) keep_sources=1 ;;
16
17
  *) die "unknown option '$1'" ;;
17
18
  esac
18
19
  shift || true
@@ -117,7 +118,7 @@ if [ -f "$lock" ]; then
117
118
  mkdir -p "$(dirname "$IP_ROOT/$rel")"
118
119
  mv "$staging" "$IP_ROOT/$rel"
119
120
  store_record_set "$IP_ROOT" "$name" "$url" "$path" "$resolved" "$sha"
120
- wire_package_sources "$manifest" "$name" "$rel" "$IP_ROOT"
121
+ [ "$keep_sources" = 1 ] || wire_package_sources "$manifest" "$name" "$rel" "$IP_ROOT"
121
122
  echo "= $name@${resolved:-HEAD}"
122
123
  done < <(lock_to_tsv "$lock")
123
124
  fi
@@ -12,13 +12,16 @@ adapter_file_for() {
12
12
  fi
13
13
  }
14
14
 
15
+ # adapter_records_for <root> <content-dir> <name> <output> [manifest] — pass the
16
+ # manifest whenever the caller applies or checks Git policy; ownership callers
17
+ # may omit it (engine/lib/adapter-contract.sh).
15
18
  adapter_records_for() {
16
- local root="$1" content_dir="$2" name="$3" output="$4" file
19
+ local root="$1" content_dir="$2" name="$3" output="$4" manifest="${5:-}" file
17
20
  file="$(adapter_file_for "$root" "$content_dir" "$name")" || {
18
21
  echo "ERROR: adapter '$name' not found" >&2
19
22
  return 1
20
23
  }
21
- adapter_contract_records "$name" "$file" "$output"
24
+ adapter_contract_records "$name" "$file" "$output" "$manifest"
22
25
  }
23
26
 
24
27
  validate_adapter_contract_for() {
@@ -1,11 +1,12 @@
1
1
  #!/bin/bash
2
- # Shared plumbing for every CLI command. Commands are separate processes (the
3
- # dispatcher execs them), so each one sources this file first.
2
+ # Shared plumbing for every CLI command. Each command is its own script (the
3
+ # dispatcher execs it, or sources `sync`), so each one sources this file first.
4
4
  #
5
5
  # All YAML reading goes through the engine's lib/common.sh — the CLI never
6
6
  # grows a parallel parser for shapes the engine can already read. The only
7
7
  # CLI-owned parsing lives in lib/manifest.sh (quoted-key `packages:` /
8
- # `registries:` blocks, which the engine deliberately never reads).
8
+ # `registries:` blocks, which the engine never reads beyond the package names
9
+ # and aliases its list parser resolves `sources:` references against).
9
10
 
10
11
  # Engine libraries (readers, is_status, IS_RC_*, engine_version). CLI_DIR /
11
12
  # IS_ENGINE_DIR come exported from the dispatcher.
@@ -98,23 +99,32 @@ source "$CLI_DIR/lib/sync-lock.sh"
98
99
  # Its identity is DATA shipped with the distribution (cli/engine-package.yaml),
99
100
  # never a name compiled into cli code — a fork edits the file.
100
101
  _EPKG="$CLI_DIR/engine-package.yaml"
101
- SYNC_PKG_NAME="" SYNC_PKG_URL="" SYNC_PKG_PATH=""
102
+ _EPKG_FIELDS='name url path default_registry'
103
+ SYNC_PKG_NAME="" SYNC_PKG_URL="" SYNC_PKG_PATH="" SYNC_PKG_STORE=""
102
104
  # Read by commands (init seeds it) — per-file shellcheck cannot see that.
103
105
  # shellcheck disable=SC2034
104
106
  DEFAULT_REGISTRY_URL=""
105
- # Every command loads this file first: read the four fields in one pass.
107
+
108
+ # load_sync_package_identity — read the four fields in one pass. Every command
109
+ # loads this file first and gets them at once; `sync` sets CLI_DEFER_SYNC_PKG
110
+ # before sourcing and reads them in the same awk pass as the project's manifest
111
+ # and lock (project_reads_preload), once it holds the project lock.
106
112
  # 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
116
- [ -n "$SYNC_PKG_NAME" ] || die "corrupt CLI installation: $_EPKG is missing or has no 'name'"
117
- SYNC_PKG_STORE=".intelligence/packages/$SYNC_PKG_NAME"
113
+ load_sync_package_identity() {
114
+ local key value
115
+ _qmap_read_var tops "$_EPKG" '' "$_EPKG_FIELDS" || true
116
+ while IFS="$LOCK_SEP" read -r key value; do
117
+ case "$key" in
118
+ name) SYNC_PKG_NAME="$value" ;;
119
+ url) SYNC_PKG_URL="$value" ;;
120
+ path) SYNC_PKG_PATH="$value" ;;
121
+ default_registry) DEFAULT_REGISTRY_URL="$value" ;;
122
+ esac
123
+ done <<< "$IS_QMAP_OUT"
124
+ [ -n "$SYNC_PKG_NAME" ] || die "corrupt CLI installation: $_EPKG is missing or has no 'name'"
125
+ SYNC_PKG_STORE=".intelligence/packages/$SYNC_PKG_NAME"
126
+ }
127
+ [ "${CLI_DEFER_SYNC_PKG:-0}" = 1 ] || load_sync_package_identity
118
128
 
119
129
  # --- Project detection ---------------------------------------------------
120
130
  # Sets: IP_MODE (cli|legacy|none), IP_ROOT, IP_UMBRELLA, IP_MODULE_DIR.
@@ -256,9 +266,14 @@ source_entry_problem() {
256
266
  }
257
267
 
258
268
  manifest_intelligence_dir() {
259
- local manifest="$1" v
260
- v="$(get_yaml_field "$manifest" "project" "intelligence_dir")"
261
- printf '%s' "${v:-intelligence}"
269
+ manifest_intelligence_dir_var "$1"
270
+ printf '%s' "$IS_CONTENT_DIR"
271
+ }
272
+
273
+ # manifest_intelligence_dir_var <manifest> — the same answer in IS_CONTENT_DIR.
274
+ manifest_intelligence_dir_var() {
275
+ get_yaml_field_var "$1" "project" "intelligence_dir"
276
+ IS_CONTENT_DIR="${IS_YAML_FIELD:-intelligence}"
262
277
  }
263
278
 
264
279
  default_target_output() {
@@ -316,7 +331,8 @@ sync_pkg_install() {
316
331
  export_engine_env() {
317
332
  local root="$1"
318
333
  local content_rel
319
- content_rel="$(manifest_intelligence_dir "$root/intelligence.yaml")"
334
+ manifest_intelligence_dir_var "$root/intelligence.yaml"
335
+ content_rel="$IS_CONTENT_DIR"
320
336
  export IS_CLI=1
321
337
  export CONFIG_FILE="$root/intelligence.yaml"
322
338
  export REPO_ROOT="$root"
@@ -381,6 +397,7 @@ project_stamped_ahead() {
381
397
 
382
398
  project_needs_upgrade() {
383
399
  local root="$1" manifest="$1/intelligence.yaml" stamp eng pinned locked name url path rows
400
+ local lname _requested _url _path resolved _sha
384
401
  [ -f "$manifest" ] || return 1
385
402
  read_schema_version_var "$manifest"
386
403
  stamp="$IS_SCHEMA_VERSION"
@@ -391,12 +408,14 @@ project_needs_upgrade() {
391
408
  # restamp the schema and re-pin the engine content DOWNWARD, and the next
392
409
  # teammate on the current CLI would move both back: leave it as found.
393
410
  project_stamped_ahead "$root" && return 1
394
- [ -n "$(top_scalar "$manifest" "sync_version")" ] && return 0
411
+ _qmap_read_var top "$manifest" '' sync_version || true
412
+ [ -z "$IS_QMAP_OUT" ] || return 0
395
413
  _ver_gt "$eng" "$stamp" && return 0
396
414
  [ -d "$root/.intelligence/engine" ] && return 0
397
415
 
398
416
  # One manifest pass for every package's fields, not a reader per field.
399
- rows="$(_qmap_read fieldrows "$manifest" packages '' 'url path version')"
417
+ _qmap_read_var fieldrows "$manifest" packages '' 'url path version' || true
418
+ rows="$IS_QMAP_OUT"
400
419
  while IFS="$LOCK_SEP" read -r name url path pinned; do
401
420
  [ -n "$name" ] || continue
402
421
  # Early RC manifests mixed requested intent with resolved source
@@ -404,7 +423,14 @@ project_needs_upgrade() {
404
423
  [ -n "$url" ] && return 0
405
424
  [ -n "$path" ] && return 0
406
425
  if [ "$name" = "$SYNC_PKG_NAME" ]; then
407
- locked="$(qmap_field "$root/intelligence.lock" "packages" "$name" "resolved")"
426
+ # The first lock row for the name: what qmap_field would decode.
427
+ locked=""
428
+ _qmap_read_var rows "$root/intelligence.lock" packages || true
429
+ while IFS="$LOCK_SEP" read -r lname _requested _url _path resolved _sha; do
430
+ [ "$lname" = "$name" ] || continue
431
+ locked="$resolved"
432
+ break
433
+ done <<< "$IS_QMAP_OUT"
408
434
  [ "$pinned" = "$eng" ] || return 0
409
435
  if [ -f "$root/intelligence.lock" ]; then
410
436
  [ -n "$locked" ] || return 0
@@ -419,21 +445,30 @@ project_needs_upgrade() {
419
445
  # which lifecycle alignment can proceed. Never manufacture a partial lock.
420
446
  project_has_packages() {
421
447
  local root="$1" name found=1
448
+ _qmap_read_var keys "$root/intelligence.yaml" "packages" || true
422
449
  while IFS= read -r name; do
423
450
  [ -n "$name" ] || continue
424
451
  assert_valid_pkg_name "$name"
425
452
  found=0
426
- done < <(qmap_keys "$root/intelligence.yaml" "packages")
453
+ done <<< "$IS_QMAP_OUT"
427
454
  return "$found"
428
455
  }
429
456
 
430
- ensure_project_current() {
431
- local root="$1" explicit="${2:-}" manifest="$1/intelligence.yaml" stamp eng
432
- check_version_compat "$manifest" || return $?
457
+ # project_preflight <root> — what every project-aware command checks before it
458
+ # may align or write: schema compatibility (a newer major refuses with 4), a
459
+ # valid lock, and a lock for whatever packages the manifest declares.
460
+ project_preflight() {
461
+ local root="$1"
462
+ check_version_compat "$root/intelligence.yaml" || return $?
433
463
  validate_project_lock "$root" || return $?
434
464
  if project_has_packages "$root" && [ ! -f "$root/intelligence.lock" ]; then
435
465
  die "manifest declares packages but intelligence.lock is absent — restore the committed lock before running project lifecycle commands"
436
466
  fi
467
+ }
468
+
469
+ ensure_project_current() {
470
+ local root="$1" explicit="${2:-}" manifest="$1/intelligence.yaml" stamp eng
471
+ project_preflight "$root" || return $?
437
472
  project_needs_upgrade "$root" || return 0
438
473
  read_schema_version_var "$manifest"
439
474
  stamp="$IS_SCHEMA_VERSION"
@@ -443,7 +478,8 @@ ensure_project_current() {
443
478
  die "project lifecycle requires alignment (stamp ${stamp:-unstamped}, engine $eng) — run 'intelligence init --apply' locally, review and commit the diff"
444
479
  fi
445
480
  echo " project alignment: stamp ${stamp:-unstamped}, engine $eng"
446
- bash "$CLI_DIR/internal/align-project.sh" --no-sync
481
+ bash "$CLI_DIR/internal/align-project.sh" --no-sync || return $?
482
+ project_reads_refresh "$root"
447
483
  }
448
484
 
449
485
  # A present lock is validated even when there is no missing store to restore.
@@ -451,7 +487,8 @@ ensure_project_current() {
451
487
  validate_project_lock() {
452
488
  local lock="$1/intelligence.lock" output line rc=0
453
489
  if [ -e "$lock" ] || [ -L "$lock" ]; then
454
- output="$(lock_validate "$lock" "${2---metadata}" "$1/intelligence.yaml" 2>&1)" || rc=$?
490
+ # The capture is the subshell that contains the guards' `die`.
491
+ output="$(lock_validate_checks "$lock" "${2---metadata}" "$1/intelligence.yaml" 2>&1)" || rc=$?
455
492
  while IFS= read -r line; do
456
493
  [ -n "$line" ] || continue
457
494
  case "$line" in
@@ -471,6 +508,74 @@ validate_project_lock() {
471
508
  fi
472
509
  }
473
510
 
511
+ # --- Reads shared by one command's preflight -----------------------------
512
+ # A sync reads the CLI's package descriptor, the manifest and the lock several
513
+ # times on its way to the engine. Once it holds the project lock it answers all
514
+ # of those reads with one qmap pass and one pass of the engine's manifest
515
+ # readers; a child that may rewrite either document (alignment, restore) is
516
+ # followed by project_reads_refresh. Without a preload every reader takes its
517
+ # ordinary path.
518
+ IS_PROJECT_READS_ROOT=""
519
+
520
+ project_reads_preload() {
521
+ local root="$1" manifest="$1/intelligence.yaml" lock="$1/intelligence.lock" s=$'\037'
522
+ local qmap=0 view=0 out qmap_pid="" view_pid=""
523
+ local -a view_args=("$manifest" project intelligence_dir rules agents skills)
524
+ project_reads_reset
525
+ qmap_preload_plan \
526
+ "tops$s$_EPKG$s$s$_EPKG_FIELDS$s$s" \
527
+ "keys$s$manifest${s}packages$s$s$s" \
528
+ "fieldrows$s$manifest${s}packages$s${s}url path version$s" \
529
+ "top$s$manifest$s${s}sync_version$s$s" \
530
+ "lock$s$lock${s}packages$s$s$s$LOCKFILE_VERSION" \
531
+ "rows$s$lock${s}packages$s$s$s" && qmap=1
532
+ [ ! -f "$manifest" ] || [ ! -r "$manifest" ] || view=1
533
+ if project_lock_scratch "$root"; then
534
+ # Both passes at once: starting their processes is most of their cost.
535
+ if [ "$qmap" = 1 ]; then
536
+ qmap_preload_run > "$IS_LOCK_SCRATCH.qmap" &
537
+ qmap_pid=$!
538
+ fi
539
+ if [ "$view" = 1 ]; then
540
+ manifest_view_run "${view_args[@]}" > "$IS_LOCK_SCRATCH.view" &
541
+ view_pid=$!
542
+ fi
543
+ if [ "$qmap" = 1 ] && wait "$qmap_pid"; then
544
+ qmap_preload_store "$(< "$IS_LOCK_SCRATCH.qmap")"
545
+ fi
546
+ if [ "$view" = 1 ]; then
547
+ if wait "$view_pid"; then
548
+ manifest_view_store "$(< "$IS_LOCK_SCRATCH.view")" "${view_args[@]}"
549
+ else
550
+ manifest_view_reset
551
+ fi
552
+ fi
553
+ else
554
+ if [ "$qmap" = 1 ] && out="$(qmap_preload_run)"; then
555
+ qmap_preload_store "$out"
556
+ fi
557
+ [ "$view" = 0 ] || load_manifest_view "${view_args[@]}" || manifest_view_reset
558
+ fi
559
+ IS_PROJECT_READS_ROOT="$root"
560
+ }
561
+
562
+ project_reads_reset() {
563
+ qmap_memo_reset
564
+ manifest_view_reset
565
+ IS_PROJECT_READS_ROOT=""
566
+ }
567
+
568
+ # project_reads_refresh <root> — after a child may have rewritten the manifest
569
+ # or the lock: read them again if this process preloaded them.
570
+ project_reads_refresh() {
571
+ [ "$IS_PROJECT_READS_ROOT" = "$1" ] || return 0
572
+ project_reads_preload "$1"
573
+ }
574
+
575
+ # CLI and engine share one list-cache format. Writers invalidate both the
576
+ # qmap answers and the engine view through qmap_memo_reset.
577
+ _yaml_list_var() { load_yaml_list "$1" "$2"; }
578
+
474
579
  # Read-only diagnosis must distinguish usable installed metadata from metadata
475
580
  # which could actually restore a missing store.
476
581
  check_project_lock() {
@@ -539,26 +644,31 @@ _store_record_write() {
539
644
  # project_store_missing <root> — a package the manifest or lock names is absent
540
645
  # from the store, or holds something other than its lock row pins.
541
646
  project_store_missing() {
542
- local root="$1" manifest="$1/intelligence.yaml" name src
647
+ local root="$1" manifest="$1/intelligence.yaml" name src section
648
+ _qmap_read_var keys "$manifest" "packages" || true
543
649
  while IFS= read -r name; do
544
650
  [ -n "$name" ] || continue
545
651
  assert_valid_pkg_name "$name"
546
652
  [ -d "$root/.intelligence/packages/$name" ] || return 0
547
- done < <(qmap_keys "$manifest" "packages")
653
+ done <<< "$IS_QMAP_OUT"
654
+ # The parser hands a package reference over as the store directory it
655
+ # names, so every spelling is checked here as that directory.
548
656
  for section in rules agents skills; do
657
+ _yaml_list_var "$manifest" "$section"
549
658
  while IFS= read -r src; do
550
659
  case "$src" in
551
660
  .intelligence/packages/*)
552
661
  [ -d "$root/$src" ] || return 0
553
662
  ;;
554
663
  esac
555
- done < <(read_yaml_list "$manifest" "$section")
664
+ done <<< "$IS_YAML_LIST"
556
665
  done
557
666
  # Present is not enough: a `git pull` moves the lock and leaves the ignored
558
667
  # store on the commit it held before.
559
668
  local rows _requested url path resolved sha
560
669
  [ -f "$root/intelligence.lock" ] || return 1
561
- rows="$(lock_to_tsv "$root/intelligence.lock")"
670
+ _qmap_read_var rows "$root/intelligence.lock" packages || true
671
+ rows="$IS_QMAP_OUT"
562
672
  while IFS="$LOCK_SEP" read -r name _requested url path resolved sha; do
563
673
  [ -n "$name" ] || continue
564
674
  assert_valid_pkg_name "$name"
@@ -573,5 +683,10 @@ restore_project_store_if_missing() {
573
683
  project_store_missing "$root" || return 0
574
684
  [ -f "$root/intelligence.lock" ] || die "package store is missing and intelligence.lock is absent — run 'intelligence init'"
575
685
  echo " restoring package store from intelligence.lock"
576
- bash "$CLI_DIR/internal/restore.sh" --frozen --no-sync
686
+ if [ "${2:-}" = keep-sources ]; then
687
+ bash "$CLI_DIR/internal/restore.sh" --frozen --no-sync --keep-sources || return $?
688
+ else
689
+ bash "$CLI_DIR/internal/restore.sh" --frozen --no-sync || return $?
690
+ fi
691
+ project_reads_refresh "$root"
577
692
  }