@ainova-systems/intelligence 0.11.0-rc.7 → 0.11.0-rc.9

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 (51) hide show
  1. package/README.md +19 -16
  2. package/cli/commands/adapter.sh +153 -0
  3. package/cli/commands/init.sh +113 -22
  4. package/cli/commands/package.sh +30 -0
  5. package/cli/commands/registry.sh +4 -2
  6. package/cli/commands/status.sh +12 -4
  7. package/cli/commands/sync.sh +7 -20
  8. package/cli/commands/update.sh +76 -70
  9. package/cli/engine-package.yaml +2 -2
  10. package/cli/intelligence +10 -12
  11. package/cli/{commands/doctor.sh → internal/check.sh} +30 -23
  12. package/cli/{commands/migrate.sh → internal/migrate-v1.sh} +117 -30
  13. package/cli/{commands/add.sh → internal/package-add.sh} +11 -16
  14. package/cli/{commands/list.sh → internal/package-list.sh} +9 -4
  15. package/cli/{commands/remove.sh → internal/package-remove.sh} +3 -3
  16. package/cli/{commands/search.sh → internal/package-search.sh} +4 -4
  17. package/cli/internal/package-update.sh +103 -0
  18. package/cli/{commands/install.sh → internal/restore.sh} +7 -18
  19. package/cli/internal/target-state.sh +57 -0
  20. package/cli/internal/upgrade-v2.sh +133 -0
  21. package/cli/lib/cli-common.sh +142 -12
  22. package/cli/lib/lockfile.sh +1 -1
  23. package/cli/lib/manifest.sh +109 -0
  24. package/cli/lib/registry.sh +4 -4
  25. package/engine/ENGINE_SHA +1 -1
  26. package/engine/adapters/_template.sh +10 -9
  27. package/engine/adapters/agents.sh +11 -11
  28. package/engine/adapters/opencode.sh +1 -1
  29. package/engine/lib/common.sh +14 -22
  30. package/engine/lib/contract.sh +14 -14
  31. package/engine/sync.sh +25 -20
  32. package/package.json +1 -1
  33. package/packages/sync/agents/intelligence-architect.md +5 -3
  34. package/packages/sync/agents/intelligence-operator.md +10 -13
  35. package/packages/sync/references/adapters.md +252 -0
  36. package/packages/sync/references/conventions.md +385 -0
  37. package/packages/sync/rules/intelligence-authoring.md +6 -6
  38. package/packages/sync/skills/intelligence-add-agent/SKILL.md +5 -5
  39. package/packages/sync/skills/intelligence-add-rule/SKILL.md +3 -3
  40. package/packages/sync/skills/intelligence-add-skill/SKILL.md +3 -3
  41. package/packages/sync/skills/intelligence-extract-skill/SKILL.md +2 -2
  42. package/packages/sync/skills/intelligence-install-adapter/SKILL.md +29 -22
  43. package/packages/sync/skills/intelligence-learn-from-context/SKILL.md +3 -3
  44. package/packages/sync/skills/intelligence-learn-from-repository/SKILL.md +55 -0
  45. package/packages/sync/skills/intelligence-review-skills/SKILL.md +6 -6
  46. package/packages/sync/skills/intelligence-sync/SKILL.md +13 -9
  47. package/packages/sync/skills/intelligence-uninstall-adapter/SKILL.md +19 -37
  48. package/packages/sync/skills/intelligence-update/SKILL.md +34 -156
  49. package/cli/commands/upgrade.sh +0 -68
  50. package/packages/sync/docs/ADAPTERS.md +0 -214
  51. package/packages/sync/docs/CONVENTIONS.md +0 -456
@@ -1,5 +1,5 @@
1
1
  #!/bin/bash
2
- # intelligence add <spec> [--name @scope/name] [--no-sync]
2
+ # Internal package add operation.
3
3
  #
4
4
  # Specs:
5
5
  # @scope/name[@range] resolved ONLY through the manifest's trusted registries
@@ -9,7 +9,7 @@ set -euo pipefail
9
9
  source "$CLI_DIR/lib/cli-common.sh"
10
10
 
11
11
  spec="${1:-}"
12
- [ -n "$spec" ] || die "usage: intelligence add <@scope/name[@range] | github:org/repo[#path] | git+<url>[@ref][#path]>"
12
+ [ -n "$spec" ] || die "usage: intelligence package add <@scope/name[@range] | github:org/repo[#path] | git+<url>[@ref][#path]>"
13
13
  shift || true
14
14
 
15
15
  no_sync=0
@@ -85,7 +85,7 @@ if [ "$mode" = "registry" ]; then
85
85
  echo " ('intelligence registry list' shows them). Either add its registry:" >&2
86
86
  echo " intelligence registry add <registry-repo-url>" >&2
87
87
  echo " or install from an explicit source you name yourself:" >&2
88
- echo " intelligence add github:org/repo | intelligence add git+<url>[@ref][#path]" >&2
88
+ echo " intelligence package add github:org/repo | intelligence package add git+<url>[@ref][#path]" >&2
89
89
  suggest_similar "$manifest" "$name"
90
90
  exit 1
91
91
  fi
@@ -117,6 +117,7 @@ if [ -z "$ref" ]; then
117
117
  die "'$name' has no stable version tags at $url — a registry package must be versioned; use github:/git+ with @<ref> to pin a branch or commit"
118
118
  fi
119
119
  echo " NOTE: no version tags at $url — pinning the default branch head" >&2
120
+ ref="HEAD"
120
121
  fi
121
122
  fi
122
123
 
@@ -125,22 +126,16 @@ sha="$(fetch_package "$url" "${ref:-$resolved_tag}" "$path" "$IP_ROOT/$rel")"
125
126
 
126
127
  wire_package_sources "$manifest" "$name" "$rel" "$IP_ROOT"
127
128
 
128
- # Manifest entry: a registry package records its range; a direct one records
129
- # its source so resolution never has to guess it again. The entry is replaced
130
- # WHOLE — re-adding under a different spec must not leave the old spec's
131
- # fields behind (a stale ref: would freeze update, a stale url: would pin the
132
- # old source).
129
+ # The manifest records requested intent only. The resolved source URL/path,
130
+ # tag/ref and SHA live in the required lock. Re-adding a package is the
131
+ # explicit operation for changing its trusted source.
133
132
  qmap_delete_key "$manifest" "packages" "$name"
134
- if [ "$mode" = "registry" ]; then
133
+ if [ -n "$ref" ]; then
134
+ qmap_set "$manifest" "packages" "$name" "ref" "$ref"
135
+ elif [ -n "$requested" ]; then
135
136
  qmap_set "$manifest" "packages" "$name" "version" "$requested"
136
137
  else
137
- qmap_set "$manifest" "packages" "$name" "url" "$url"
138
- [ -n "$path" ] && qmap_set "$manifest" "packages" "$name" "path" "$path"
139
- if [ -n "$ref" ]; then
140
- qmap_set "$manifest" "packages" "$name" "ref" "$ref"
141
- elif [ -n "$requested" ]; then
142
- qmap_set "$manifest" "packages" "$name" "version" "$requested"
143
- fi
138
+ die "internal: package '$name' has neither a requested version nor ref"
144
139
  fi
145
140
 
146
141
  lock_upsert "$lock" "$name" "$requested" "$url" "$path" "${ref:-$resolved_tag}" "$sha"
@@ -1,5 +1,5 @@
1
1
  #!/bin/bash
2
- # intelligence list — packages with requested / locked / installed state.
2
+ # Internal package list operation.
3
3
  set -euo pipefail
4
4
  source "$CLI_DIR/lib/cli-common.sh"
5
5
 
@@ -14,8 +14,13 @@ while IFS= read -r name; do
14
14
  range="$(qmap_field "$manifest" "packages" "$name" "version")"
15
15
  ref="$(qmap_field "$manifest" "packages" "$name" "ref")"
16
16
  resolved="$(qmap_field "$lock" "packages" "$name" "resolved")"
17
- state="missing — run 'intelligence install'"
18
- [ -d "$IP_ROOT/.intelligence/packages/$name" ] && state="installed"
17
+ if [ ! -f "$lock" ]; then
18
+ state="lock missing — restore committed intelligence.lock"
19
+ elif [ -d "$IP_ROOT/.intelligence/packages/$name" ]; then
20
+ state="installed"
21
+ else
22
+ state="store missing — run 'intelligence sync'"
23
+ fi
19
24
  printf '%s %s locked:%s %s\n' \
20
25
  "$name" \
21
26
  "${ref:+ref:$ref}${ref:-${range:-*}}" \
@@ -24,5 +29,5 @@ while IFS= read -r name; do
24
29
  done < <(qmap_keys "$manifest" "packages")
25
30
 
26
31
  if [ "$count" -eq 0 ]; then
27
- echo "No packages. Add one: intelligence add @ainova-systems/core"
32
+ echo "No packages. Add one: intelligence package add @ainova-systems/core"
28
33
  fi
@@ -1,10 +1,10 @@
1
1
  #!/bin/bash
2
- # intelligence remove <@scope/name> [--no-sync]
2
+ # Internal package remove operation.
3
3
  set -euo pipefail
4
4
  source "$CLI_DIR/lib/cli-common.sh"
5
5
 
6
6
  name="${1:-}"
7
- [ -n "$name" ] || die "usage: intelligence remove <@scope/name>"
7
+ [ -n "$name" ] || die "usage: intelligence package remove <@scope/name>"
8
8
  shift || true
9
9
  no_sync=0 force=0
10
10
  for a in "$@"; do
@@ -24,7 +24,7 @@ if [ "$name" = "$SYNC_PKG_NAME" ] && [ "$force" -eq 0 ]; then
24
24
  echo "ERROR: $SYNC_PKG_NAME is the engine's own content — removing it drops every" >&2
25
25
  echo " intelligence-* meta-skill, the authoring rule and the engine agents from" >&2
26
26
  echo " the outputs. The sync engine itself keeps working (it ships with the CLI)." >&2
27
- echo " If that is what you want: intelligence remove $SYNC_PKG_NAME --force" >&2
27
+ echo " If that is what you want: intelligence package remove $SYNC_PKG_NAME --force" >&2
28
28
  exit 1
29
29
  fi
30
30
  assert_valid_pkg_name "$name"
@@ -1,5 +1,5 @@
1
1
  #!/bin/bash
2
- # intelligence search [term] — what the registries offer, and what this project
2
+ # Internal package search operation: what the registries offer and what this project
3
3
  # already has. The catalogue view `list` deliberately is not: `list` answers
4
4
  # "what does this project depend on", `search` answers "what could it".
5
5
  #
@@ -27,7 +27,7 @@ pkg_state() {
27
27
  elif [ -d "$IP_ROOT/.intelligence/packages/$name" ]; then
28
28
  printf 'installed %s' "$(qmap_field "$IP_ROOT/intelligence.lock" "packages" "$name" "resolved")"
29
29
  else
30
- printf 'declared (run install)'
30
+ printf 'declared (run sync)'
31
31
  fi
32
32
  }
33
33
 
@@ -75,9 +75,9 @@ if [ "$rows" -eq 0 ]; then
75
75
  else
76
76
  echo "No trusted registries (or they offer nothing) — intelligence registry add <repo-url>."
77
77
  fi
78
- echo "Explicit installs need no registry: intelligence add github:org/repo"
78
+ echo "Explicit installs need no registry: intelligence package add github:org/repo"
79
79
  exit 0
80
80
  fi
81
81
 
82
82
  echo ""
83
- echo "add one: intelligence add <name> | private registry: intelligence registry add <repo-url>"
83
+ echo "add one: intelligence package add <name> | private registry: intelligence registry add <repo-url>"
@@ -0,0 +1,103 @@
1
+ #!/bin/bash
2
+ # Internal package range update operation.
3
+ #
4
+ # Resolve requested versions/refs against each package's locked source and
5
+ # rewrite the lock. `update <name>` limits the pass to one package; re-adding
6
+ # is the explicit operation for changing a source URL/path.
7
+ set -euo pipefail
8
+ source "$CLI_DIR/lib/cli-common.sh"
9
+
10
+ only="" no_sync=0 preview=0
11
+ while [ $# -gt 0 ]; do
12
+ case "$1" in
13
+ --no-sync) no_sync=1 ;;
14
+ --preview) preview=1 ;;
15
+ @*) only="$1" ;;
16
+ *) die "unknown argument '$1'" ;;
17
+ esac
18
+ shift || true
19
+ done
20
+
21
+ require_v2
22
+ manifest="$IP_ROOT/intelligence.yaml"
23
+ lock="$IP_ROOT/intelligence.lock"
24
+
25
+ moved=0 found=0
26
+ while IFS= read -r name; do
27
+ [ -n "$name" ] || continue
28
+ # Manifest keys are untrusted input on their way into store paths.
29
+ assert_valid_pkg_name "$name"
30
+ [ -n "$only" ] && [ "$name" != "$only" ] && continue
31
+ # A named package that exists is "found" whatever happens next — a skip
32
+ # message followed by "not in the manifest" would contradict itself.
33
+ found=1
34
+ if [ "$name" = "$SYNC_PKG_NAME" ]; then
35
+ echo " $name: engine content follows the installed CLI — skipped"
36
+ continue
37
+ fi
38
+ current="$(qmap_field "$lock" "packages" "$name" "resolved")"
39
+ locked_requested="$(qmap_field "$lock" "packages" "$name" "requested")"
40
+ url="$(qmap_field "$lock" "packages" "$name" "url")"
41
+ path="$(qmap_field "$lock" "packages" "$name" "path")"
42
+ [ -n "$url" ] || die "$name has no source in intelligence.lock — restore the committed lock or re-add the package"
43
+ ref="$(qmap_field "$manifest" "packages" "$name" "ref")"
44
+ range="$(qmap_field "$manifest" "packages" "$name" "version")"
45
+ [ -n "$ref" ] || [ -n "$range" ] || die "$name has neither version nor ref in the manifest"
46
+
47
+ if [ -n "$ref" ]; then
48
+ tag="$ref"
49
+ requested=""
50
+ else
51
+ picked="$(list_remote_versions "$url" | semver_pick_highest "$range")"
52
+ [ -n "$picked" ] || { echo " $name: nothing satisfies '$range' at $url" >&2; continue; }
53
+ read -r tag _ <<< "$(remote_tag_for_version "$url" "$picked")"
54
+ requested="$range"
55
+ fi
56
+ if [ "$tag" = "$current" ] && [ "$requested" != "$locked_requested" ]; then
57
+ if [ "$preview" -eq 1 ]; then
58
+ echo " $name: request ${locked_requested:-<none>} -> ${requested:-<ref>} (keeps $current)"
59
+ else
60
+ locked_sha="$(qmap_field "$lock" "packages" "$name" "sha")"
61
+ lock_upsert "$lock" "$name" "$requested" "$url" "$path" "$current" "$locked_sha"
62
+ echo " $name: request ${locked_requested:-<none>} -> ${requested:-<ref>} (kept $current)"
63
+ fi
64
+ moved=$((moved + 1))
65
+ continue
66
+ fi
67
+ if [ "$tag" = "$current" ]; then
68
+ echo " $name: ${current:-<none>} (up to date)"
69
+ continue
70
+ fi
71
+ if [ "$preview" -eq 1 ]; then
72
+ echo " $name: ${current:-<none>} -> $tag"
73
+ moved=$((moved + 1))
74
+ continue
75
+ fi
76
+ rel=".intelligence/packages/$name"
77
+ # Fetch into staging FIRST: a failed clone must leave the current install
78
+ # fully wired and intact. Only after success is the old shape unwired
79
+ # (the new version may have dropped a section dir), the store swapped,
80
+ # and the new shape wired.
81
+ staging="$IP_ROOT/.intelligence/.staging-$$"
82
+ rm -rf "$staging"
83
+ sha="$(fetch_package "$url" "$tag" "$path" "$staging")"
84
+ unwire_package_sources "$manifest" "$rel"
85
+ rm -rf "${IP_ROOT:?}/$rel"
86
+ mkdir -p "$(dirname "$IP_ROOT/$rel")"
87
+ mv "$staging" "$IP_ROOT/$rel"
88
+ wire_package_sources "$manifest" "$name" "$rel" "$IP_ROOT"
89
+ lock_upsert "$lock" "$name" "$requested" "$url" "$path" "$tag" "$sha"
90
+ echo " $name: ${current:-<none>} -> $tag"
91
+ moved=$((moved + 1))
92
+ done < <(qmap_keys "$manifest" "packages")
93
+
94
+ [ -n "$only" ] && [ "$found" -eq 0 ] && die "package '$only' is not in the manifest"
95
+ if [ "$preview" -eq 1 ]; then
96
+ echo "updates available: $moved package(s)"
97
+ else
98
+ echo "updated: $moved package(s)"
99
+ fi
100
+
101
+ if [ "$preview" -eq 0 ] && [ "$moved" -gt 0 ] && [ "$no_sync" -eq 0 ]; then
102
+ exec bash "$CLI_DIR/commands/sync.sh"
103
+ fi
@@ -1,5 +1,5 @@
1
1
  #!/bin/bash
2
- # intelligence install [--frozen] [--force] [--no-sync]
2
+ # Internal locked-store restore operation.
3
3
  #
4
4
  # Restore .intelligence/ from intelligence.lock — the fresh-clone command.
5
5
  # --frozen additionally refuses any manifest/lock divergence (CI mode) and
@@ -23,30 +23,22 @@ manifest="$IP_ROOT/intelligence.yaml"
23
23
  lock="$IP_ROOT/intelligence.lock"
24
24
 
25
25
  # --frozen is the reproducibility contract: the lock must agree with the
26
- # manifest on the package SET and on every requested-source field before a
26
+ # manifest on the package SET and on every requested-intent field before a
27
27
  # single byte is restored. Anything less lets a manifest edit ride on a stale
28
28
  # lock — the exact drift the flag exists to refuse.
29
29
  if [ "$frozen" -eq 1 ]; then
30
30
  while IFS= read -r name; do
31
31
  [ -n "$name" ] || continue
32
32
  [ -n "$(qmap_field "$lock" "packages" "$name" "url")" ] \
33
- || die "--frozen: $name is in the manifest but not in intelligence.lock"
33
+ || die "locked restore: $name is in the manifest but not in intelligence.lock"
34
34
  m_ver="$(qmap_field "$manifest" "packages" "$name" "version")"
35
35
  m_ref="$(qmap_field "$manifest" "packages" "$name" "ref")"
36
- m_url="$(qmap_field "$manifest" "packages" "$name" "url")"
37
- m_path="$(qmap_field "$manifest" "packages" "$name" "path")"
38
36
  l_req="$(qmap_field "$lock" "packages" "$name" "requested")"
39
37
  l_res="$(qmap_field "$lock" "packages" "$name" "resolved")"
40
- l_url="$(qmap_field "$lock" "packages" "$name" "url")"
41
- l_path="$(qmap_field "$lock" "packages" "$name" "path")"
42
38
  [ -z "$m_ver" ] || [ "$m_ver" = "$l_req" ] \
43
- || die "--frozen: $name requests '$m_ver' but the lock recorded '$l_req' — run 'intelligence install' without --frozen"
39
+ || die "locked restore: $name requests '$m_ver' but the lock recorded '$l_req' — reconcile the manifest and lock"
44
40
  [ -z "$m_ref" ] || [ "$m_ref" = "$l_res" ] \
45
41
  || die "--frozen: $name pins ref '$m_ref' but the lock resolved '$l_res'"
46
- [ -z "$m_url" ] || [ "$m_url" = "$l_url" ] \
47
- || die "--frozen: $name declares url '$m_url' but the lock recorded '$l_url'"
48
- [ -z "$m_path" ] || [ "$m_path" = "$l_path" ] \
49
- || die "--frozen: $name declares path '$m_path' but the lock recorded '$l_path'"
50
42
  done < <(qmap_keys "$manifest" "packages")
51
43
  while IFS= read -r name; do
52
44
  [ -n "$name" ] || continue
@@ -76,14 +68,11 @@ if [ -n "$missing" ]; then
76
68
  sync_pkg_install "$IP_ROOT"
77
69
  continue
78
70
  fi
79
- url="$(qmap_field "$manifest" "packages" "$name" "url")"
80
- path="$(qmap_field "$manifest" "packages" "$name" "path")"
81
71
  ref="$(qmap_field "$manifest" "packages" "$name" "ref")"
82
72
  range="$(qmap_field "$manifest" "packages" "$name" "version")"
83
- if [ -z "$url" ]; then
84
- resolve_package_source "$manifest" "$name"
85
- url="$RES_URL"; path="$RES_PATH"
86
- fi
73
+ resolve_package_source "$manifest" "$name"
74
+ url="$RES_URL"; path="$RES_PATH"
75
+ [ -n "$url" ] || die "no trusted registry declares '$name' and it is absent from intelligence.lock"
87
76
  resolved_tag=""
88
77
  if [ -z "$ref" ]; then
89
78
  picked="$(list_remote_versions "$url" | semver_pick_highest "$range")"
@@ -0,0 +1,57 @@
1
+ #!/bin/bash
2
+ # Internal adapter target-state operation.
3
+ #
4
+ # Change only the manifest. Syncing and deleting generated output are separate
5
+ # explicit actions: a generic target command cannot infer a custom adapter's
6
+ # ownership safely.
7
+ set -euo pipefail
8
+ source "$CLI_DIR/lib/cli-common.sh"
9
+
10
+ action="${1:-}"
11
+ name="${2:-}"
12
+ [ $# -le 2 ] || die "internal target state: expected <enable|disable> <name>"
13
+ case "$action" in enable|disable) ;; *) die "internal target state: expected <enable|disable> <name>" ;; esac
14
+ [ -n "$name" ] || die "internal target state: missing adapter name"
15
+ assert_valid_target_name "$name"
16
+
17
+ require_v2
18
+ manifest="$IP_ROOT/intelligence.yaml"
19
+ content_dir="$(manifest_intelligence_dir "$manifest")"
20
+
21
+ if [ "$action" = "enable" ]; then
22
+ project_adapter="$IP_ROOT/$content_dir/adapters/$name.sh"
23
+ bundled_adapter="$IS_ENGINE_DIR/adapters/$name.sh"
24
+ if [ ! -f "$project_adapter" ] && [ ! -f "$bundled_adapter" ]; then
25
+ die "adapter '$name' not found — create it first: intelligence adapter create $name"
26
+ fi
27
+
28
+ # These adapters intentionally omit always-on rules because their tools
29
+ # read AGENTS.md. Do not create a manifest that full sync must refuse.
30
+ case "$name" in
31
+ cursor|copilot|codex|pi|opencode)
32
+ if [ "$(is_target_enabled "$manifest" "agents")" != "1" ]; then
33
+ die "target '$name' requires target 'agents' — enable it first"
34
+ fi
35
+ ;;
36
+ esac
37
+
38
+ output="$(get_target_output "$manifest" "$name")"
39
+ [ -n "$output" ] || output="$(default_target_output "$name")"
40
+ target_set_enabled "$manifest" "$name" true "$output"
41
+ echo "enabled: $name (output: $output)"
42
+ exit 0
43
+ fi
44
+
45
+ target_exists "$manifest" "$name" || die "target '$name' is not in the manifest"
46
+ if [ "$name" = "agents" ]; then
47
+ for dependent in cursor copilot codex pi opencode; do
48
+ if [ "$(is_target_enabled "$manifest" "$dependent")" = "1" ]; then
49
+ die "target 'agents' is required by enabled target '$dependent' — disable '$dependent' first"
50
+ fi
51
+ done
52
+ fi
53
+ output="$(get_target_output "$manifest" "$name")"
54
+ [ -n "$output" ] || output="$(default_target_output "$name")"
55
+ target_set_enabled "$manifest" "$name" false "$output"
56
+ echo "disabled: $name"
57
+ echo " generated output was kept; remove only paths owned by this adapter"
@@ -0,0 +1,133 @@
1
+ #!/bin/bash
2
+ # Internal: bring the project to this CLI's engine: apply v2
3
+ # schema migrations, align the engine-content package to the bundled version,
4
+ # restamp schema_version, sync.
5
+ set -euo pipefail
6
+ source "$CLI_DIR/lib/cli-common.sh"
7
+
8
+ no_sync=0
9
+ while [ $# -gt 0 ]; do
10
+ case "$1" in
11
+ --no-sync) no_sync=1 ;;
12
+ *) die "internal upgrade: unknown option '$1'" ;;
13
+ esac
14
+ shift
15
+ done
16
+
17
+ require_v2
18
+ manifest="$IP_ROOT/intelligence.yaml"
19
+
20
+ stamp="$(read_schema_version "$manifest")"
21
+ legacy_stamp="$(top_scalar "$manifest" "sync_version")"
22
+ schema_key_count="$(awk '/^schema_version:[[:space:]]*/ { n++ } END { print n + 0 }' "$manifest")"
23
+ legacy_key_count="$(awk '/^sync_version:[[:space:]]*/ { n++ } END { print n + 0 }' "$manifest")"
24
+ eng="$(bundled_engine_version)"
25
+ [ "$schema_key_count" -le 1 ] || die "manifest has duplicate schema_version keys"
26
+ [ "$legacy_key_count" -le 1 ] || die "manifest has duplicate sync_version keys"
27
+ if [ -n "$stamp" ] && _ver_gt "$stamp" "$eng"; then
28
+ die "manifest schema $stamp is newer than this CLI's engine $eng — update the global CLI first"
29
+ fi
30
+ if [ -n "$legacy_stamp" ] && _ver_gt "$legacy_stamp" "$eng"; then
31
+ die "manifest schema $legacy_stamp is newer than this CLI's engine $eng — update the global CLI first"
32
+ fi
33
+
34
+ # --- v2 schema migrations -------------------------------------------------
35
+ # The engine's own chain owns vendored layouts; this one owns the manifest.
36
+ # Same discipline: ascending, append-only, each migration self-detects and
37
+ # no-ops when already applied.
38
+
39
+ # v2 migration 0: the original RC manifest called the permanent schema key
40
+ # `sync_version`. Rename it without changing its value; the final stamp below
41
+ # then aligns it with this engine. The archived v1 reader remains separate.
42
+ if [ -n "$legacy_stamp" ]; then
43
+ if [ -n "$stamp" ]; then
44
+ [ "$stamp" = "$legacy_stamp" ] || die "manifest has conflicting schema_version '$stamp' and sync_version '$legacy_stamp'"
45
+ _qmap_stage "$manifest" '$0 !~ /^sync_version:[[:space:]]*/'
46
+ else
47
+ _qmap_stage "$manifest" '{ sub(/^sync_version:/, "schema_version:"); print }'
48
+ fi
49
+ echo " migrating: sync_version -> schema_version"
50
+ fi
51
+
52
+ # v2 migration 1: package entries contain requested intent only. Early RCs
53
+ # also copied resolved url/path into the manifest. Preserve the existing lock
54
+ # as the trusted source and remove that duplicate representation. A direct
55
+ # unversioned package becomes an explicit HEAD pin so its manifest entry stays
56
+ # meaningful after the source fields are removed.
57
+ source_fields_migrated=0
58
+ while IFS= read -r name; do
59
+ [ -n "$name" ] || continue
60
+ m_url="$(qmap_field "$manifest" "packages" "$name" "url")"
61
+ m_path="$(qmap_field "$manifest" "packages" "$name" "path")"
62
+ [ -n "$m_url" ] || [ -n "$m_path" ] || continue
63
+ m_ver="$(qmap_field "$manifest" "packages" "$name" "version")"
64
+ m_ref="$(qmap_field "$manifest" "packages" "$name" "ref")"
65
+ if [ -z "$m_ver" ] && [ -z "$m_ref" ]; then
66
+ locked_ref="$(qmap_field "$IP_ROOT/intelligence.lock" "packages" "$name" "resolved")"
67
+ qmap_set "$manifest" "packages" "$name" "ref" "${locked_ref:-HEAD}"
68
+ fi
69
+ qmap_delete_field "$manifest" "packages" "$name" "url"
70
+ qmap_delete_field "$manifest" "packages" "$name" "path"
71
+ source_fields_migrated=1
72
+ done < <(qmap_keys "$manifest" "packages")
73
+ if [ "$source_fields_migrated" -eq 1 ]; then
74
+ echo " migrating: package url/path -> intelligence.lock"
75
+ fi
76
+
77
+ # v2 migration 2: staged engine content -> the @ainova-systems/sync package.
78
+ # Pre-package manifests listed `.intelligence/engine/{rules,agents,skills}`
79
+ # as sources fed by a CLI-staged copy; that copy is now an ordinary package
80
+ # entry seeded from the bundle. Post-condition: no `.intelligence/engine`
81
+ # source entries, the package present in manifest+lock+store, stale dir gone.
82
+ migrated=0
83
+ for section in rules agents skills; do
84
+ while IFS= read -r src; do
85
+ case "$src" in
86
+ .intelligence/engine/*)
87
+ sources_remove_entry "$manifest" "$section" "$src"
88
+ migrated=1
89
+ ;;
90
+ esac
91
+ done < <(read_yaml_list "$manifest" "$section")
92
+ done
93
+ if [ "$migrated" -eq 1 ]; then
94
+ echo " migrating: staged engine content -> $SYNC_PKG_NAME package"
95
+ rm -rf "$IP_ROOT/.intelligence/engine"
96
+ sync_pkg_entry "$manifest"
97
+ sync_pkg_install "$IP_ROOT"
98
+ fi
99
+
100
+ # --- steady state ---------------------------------------------------------
101
+ # Align the engine-content package with the bundled engine. Only a manifest
102
+ # that HAS the entry is touched — a --bare project opted out, and upgrade
103
+ # respects that.
104
+ have_pkg=0
105
+ while IFS= read -r name; do
106
+ [ "$name" = "$SYNC_PKG_NAME" ] && have_pkg=1
107
+ done < <(qmap_keys "$manifest" "packages")
108
+ if [ "$have_pkg" -eq 1 ] && [ "$migrated" -eq 0 ]; then
109
+ pinned="$(qmap_field "$manifest" "packages" "$SYNC_PKG_NAME" "version")"
110
+ if [ "$pinned" != "$eng" ]; then
111
+ echo " $SYNC_PKG_NAME: $pinned -> $eng"
112
+ fi
113
+ sync_pkg_entry "$manifest"
114
+ sync_pkg_install "$IP_ROOT"
115
+ fi
116
+ if [ "$have_pkg" -eq 0 ] && [ "$migrated" -eq 0 ]; then
117
+ echo " NOTE: $SYNC_PKG_NAME is not in this manifest (bare setup) — engine meta-skills stay uninstalled; 'intelligence package add $SYNC_PKG_NAME' opts back in." >&2
118
+ fi
119
+
120
+ # A previous RC could leave this managed directory even after its source
121
+ # entries were already removed. Its presence is itself an upgrade predicate,
122
+ # so always close that post-condition.
123
+ if [ -d "$IP_ROOT/.intelligence/engine" ]; then
124
+ rm -rf "$IP_ROOT/.intelligence/engine"
125
+ echo " removed stale .intelligence/engine"
126
+ fi
127
+
128
+ stamp_schema_version "$manifest" "$eng"
129
+ echo " schema_version -> $eng"
130
+
131
+ if [ "$no_sync" -eq 0 ]; then
132
+ exec bash "$CLI_DIR/commands/sync.sh"
133
+ fi