@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,87 +1,93 @@
1
1
  #!/bin/bash
2
- # intelligence update [name] [--no-sync]
2
+ # intelligence update [@scope/name] [--preview|--apply]
3
3
  #
4
- # Re-resolve version ranges against the remotes and rewrite the lock. Pinned
5
- # packages (`ref:`) never move here — repin by editing the manifest or
6
- # re-adding. `update <name>` limits the pass to one package.
4
+ # One update surface: show the installed-CLI/project/package plan, then either
5
+ # stop, ask interactively, or apply without a prompt. Updating the global npm
6
+ # installation remains an explicit package-manager operation.
7
7
  set -euo pipefail
8
8
  source "$CLI_DIR/lib/cli-common.sh"
9
9
 
10
- only="" no_sync=0
10
+ only="" mode="ask" preview_seen=0 apply_seen=0
11
11
  while [ $# -gt 0 ]; do
12
12
  case "$1" in
13
- --no-sync) no_sync=1 ;;
14
- @*) only="$1" ;;
15
- *) die "unknown argument '$1'" ;;
13
+ --preview) preview_seen=1; mode="preview" ;;
14
+ --apply) apply_seen=1; mode="apply" ;;
15
+ @*) [ -z "$only" ] || die "only one package may be selected"; only="$1" ;;
16
+ *) die "usage: intelligence update [@scope/name] [--preview|--apply]" ;;
16
17
  esac
17
- shift || true
18
+ shift
18
19
  done
20
+ [ "$preview_seen" -eq 0 ] || [ "$apply_seen" -eq 0 ] || die "choose either --preview or --apply"
19
21
 
20
22
  require_v2
21
23
  manifest="$IP_ROOT/intelligence.yaml"
22
- lock="$IP_ROOT/intelligence.lock"
24
+ check_version_compat "$manifest"
25
+ eng="$(bundled_engine_version)"
26
+ stamp="$(read_schema_version "$manifest")"
27
+ project_change=0
23
28
 
24
- moved=0 found=0
25
- while IFS= read -r name; do
26
- [ -n "$name" ] || continue
27
- # Manifest keys are untrusted input on their way into store paths.
28
- assert_valid_pkg_name "$name"
29
- [ -n "$only" ] && [ "$name" != "$only" ] && continue
30
- # A named package that exists is "found" whatever happens next — a skip
31
- # message followed by "not in the manifest" would contradict itself.
32
- found=1
33
- if [ "$name" = "$SYNC_PKG_NAME" ]; then
34
- echo " $name: engine content moves with the engine — 'intelligence upgrade' owns it"
35
- continue
29
+ echo "Update plan"
30
+ echo ""
31
+ echo "CLI:"
32
+ if [ -n "${INTELLIGENCE_NPM_VERSION:-}" ] && [ "${IS_SKIP_NPM_CHECK:-0}" != "1" ] && command -v npm >/dev/null 2>&1; then
33
+ channel="latest"
34
+ case "$INTELLIGENCE_NPM_VERSION" in *-*) channel="next" ;; esac
35
+ available="$(npm view "@ainova-systems/intelligence" "dist-tags.$channel" 2>/dev/null || true)"
36
+ if [ -z "$available" ]; then
37
+ echo " installed: $INTELLIGENCE_NPM_VERSION; registry check unavailable"
38
+ elif [ "$available" = "$INTELLIGENCE_NPM_VERSION" ]; then
39
+ echo " $INTELLIGENCE_NPM_VERSION (up to date on npm $channel)"
40
+ else
41
+ echo " $INTELLIGENCE_NPM_VERSION -> $available (npm $channel)"
42
+ echo " run: npm install -g @ainova-systems/intelligence@$channel"
43
+ echo " then rerun: intelligence update --apply"
36
44
  fi
37
- ref="$(qmap_field "$manifest" "packages" "$name" "ref")"
38
- [ -n "$ref" ] && { echo " $name: pinned to ref '$ref' — skipped"; continue; }
39
- range="$(qmap_field "$manifest" "packages" "$name" "version")"
40
- # An exact version is a pin, same as ref: — update never moves pins.
41
- if [ -n "$range" ] && semver_is_stable "$range"; then
42
- echo " $name: pinned to $range — edit the manifest to move it"
43
- continue
44
- fi
45
- url="$(qmap_field "$manifest" "packages" "$name" "url")"
46
- path="$(qmap_field "$manifest" "packages" "$name" "path")"
47
- if [ -z "$url" ]; then
48
- resolve_package_source "$manifest" "$name"
49
- url="$RES_URL"; path="$RES_PATH"
50
- fi
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
- current="$(qmap_field "$lock" "packages" "$name" "resolved")"
55
- cur_url="$(qmap_field "$lock" "packages" "$name" "url")"
56
- cur_path="$(qmap_field "$lock" "packages" "$name" "path")"
57
- # Same tag is not enough: a registry may re-point a name's source while
58
- # keeping the version — the move doctor warns about must be actionable
59
- # here, so url/path changes refetch too.
60
- if [ "$tag" = "$current" ] && [ "$url" = "$cur_url" ] && [ "$path" = "$cur_path" ]; then
61
- echo " $name: ${current:-<none>} (up to date)"
62
- continue
63
- fi
64
- rel=".intelligence/packages/$name"
65
- # Fetch into staging FIRST: a failed clone must leave the current install
66
- # fully wired and intact. Only after success is the old shape unwired
67
- # (the new version may have dropped a section dir), the store swapped,
68
- # and the new shape wired.
69
- staging="$IP_ROOT/.intelligence/.staging-$$"
70
- rm -rf "$staging"
71
- sha="$(fetch_package "$url" "$tag" "$path" "$staging")"
72
- unwire_package_sources "$manifest" "$rel"
73
- rm -rf "${IP_ROOT:?}/$rel"
74
- mkdir -p "$(dirname "$IP_ROOT/$rel")"
75
- mv "$staging" "$IP_ROOT/$rel"
76
- wire_package_sources "$manifest" "$name" "$rel" "$IP_ROOT"
77
- lock_upsert "$lock" "$name" "$range" "$url" "$path" "$tag" "$sha"
78
- echo " $name: ${current:-<none>} -> $tag"
79
- moved=$((moved + 1))
80
- done < <(qmap_keys "$manifest" "packages")
45
+ else
46
+ echo " source checkout/registry check skipped; engine $eng"
47
+ fi
48
+
49
+ echo ""
50
+ echo "Project:"
51
+ if project_needs_upgrade "$IP_ROOT"; then
52
+ echo " lifecycle alignment required (stamp ${stamp:-unstamped}, engine $eng)"
53
+ project_change=1
54
+ else
55
+ echo " schema/content $eng (up to date)"
56
+ fi
81
57
 
82
- [ -n "$only" ] && [ "$found" -eq 0 ] && die "package '$only' is not in the manifest"
83
- echo "updated: $moved package(s)"
58
+ if project_has_packages "$IP_ROOT" && [ ! -f "$IP_ROOT/intelligence.lock" ]; then
59
+ die "manifest declares packages but intelligence.lock is absent — restore the committed lock before planning or applying updates"
60
+ fi
61
+
62
+ pkg_args=(--preview)
63
+ [ -z "$only" ] || pkg_args+=("$only")
64
+ pkg_plan="$(bash "$CLI_DIR/internal/package-update.sh" "${pkg_args[@]}")"
65
+ echo ""
66
+ echo "Packages:"
67
+ printf '%s\n' "$pkg_plan"
68
+ package_changes="$(printf '%s\n' "$pkg_plan" | awk '/^updates available:/ {print $3; exit}')"
69
+ package_changes="${package_changes:-0}"
84
70
 
85
- if [ "$moved" -gt 0 ] && [ "$no_sync" -eq 0 ]; then
86
- exec bash "$CLI_DIR/commands/sync.sh"
71
+ if [ "$mode" = "preview" ]; then
72
+ exit 0
73
+ fi
74
+ if [ "$project_change" -eq 0 ] && [ "$package_changes" -eq 0 ]; then
75
+ echo ""
76
+ echo "Nothing to apply with the currently installed CLI."
77
+ exit 0
87
78
  fi
79
+ if [ "$mode" = "ask" ]; then
80
+ [ -t 0 ] || die "update requires confirmation — rerun with --preview or --apply"
81
+ printf '\nApply this plan? [Y/n] '
82
+ read -r answer
83
+ case "$answer" in
84
+ ""|y|Y|yes|YES) ;;
85
+ *) echo "update cancelled"; exit 0 ;;
86
+ esac
87
+ fi
88
+
89
+ ensure_project_current "$IP_ROOT"
90
+ apply_args=(--no-sync)
91
+ [ -z "$only" ] || apply_args+=("$only")
92
+ bash "$CLI_DIR/internal/package-update.sh" "${apply_args[@]}"
93
+ exec bash "$CLI_DIR/commands/sync.sh"
@@ -6,8 +6,8 @@
6
6
  # The engine-content package is the one package whose content documents the
7
7
  # engine scripts bundled with the CLI: init writes this entry into the
8
8
  # manifest (visible, removable), its pin always equals the engine VERSION,
9
- # `upgrade` moves it, and at the matching version it materializes from the
10
- # bundle without network.
9
+ # lifecycle preflight aligns it with the installed CLI, and at the matching
10
+ # version it materializes from the bundle without network.
11
11
  name: "@ainova-systems/sync"
12
12
  url: "https://github.com/ainova-systems/intelligence.git"
13
13
  path: "packages/sync"
package/cli/intelligence CHANGED
@@ -43,19 +43,17 @@ intelligence — build, version and distribute AI agent intelligence
43
43
 
44
44
  Usage: intelligence <command> [args]
45
45
 
46
- init Set up this project (manifest, content skeleton, targets)
47
- add <spec> Add a package: @scope/name[@range] | github:org/repo[#path] | git+<url>[@ref][#path]
48
- remove <name> Remove a package
49
- install [--frozen] Restore .intelligence/ exactly from intelligence.lock
50
- update [name] Re-resolve version ranges and rewrite the lockfile
51
- upgrade Upgrade the project to this CLI's engine (restage + restamp)
52
- list Installed packages with requested/locked versions
53
- search [term] What the registries offer, and what this project has
54
- sync [target] Render intelligence to every enabled tool (.claude, AGENTS.md, ...)
46
+ init [--preview|--apply] Initialize, migrate, restore or align this project
47
+ --targets a,b --dir name --bare --no-sync New-project options
48
+ --force Accept a dirty worktree during v1 conversion
49
+ sync [adapter] Restore locked content if needed, then render enabled adapters
50
+ update [@scope/name] Show an update plan; ask before applying it
51
+ --preview Show the plan without asking or writing
52
+ --apply Apply the plan without asking
53
+ package <command> add | remove | list | search
54
+ adapter <command> list | create | enable | disable | remove
55
+ status [--check] Project state; --check runs deep consistency checks
55
56
  registry <list|add|remove> Manage the trusted registry list (add <repo-url>)
56
- migrate [--dry-run] Convert a vendored (v1) setup to the CLI setup
57
- doctor Check manifest, lockfile, store and engine consistency
58
- status Project state at a glance
59
57
  version Print CLI and engine versions
60
58
  EOF
61
59
  exit 0
@@ -1,5 +1,5 @@
1
1
  #!/bin/bash
2
- # intelligence doctor — consistency checks, no writes. Exit 1 on problems.
2
+ # Internal deep consistency check. No writes; exits 1 on problems.
3
3
  set -euo pipefail
4
4
  source "$CLI_DIR/lib/cli-common.sh"
5
5
 
@@ -22,7 +22,7 @@ case "$IP_MODE" in
22
22
  legacy)
23
23
  echo "project: vendored (v1) at $IP_ROOT"
24
24
  ok "vendored engine $(tr -d ' \t\r\n' < "$IP_MODULE_DIR/scripts/VERSION")"
25
- echo " → 'intelligence migrate' converts it to the CLI setup"
25
+ echo " → 'intelligence init' converts it to the CLI setup"
26
26
  exit 0
27
27
  ;;
28
28
  esac
@@ -31,16 +31,16 @@ echo "project: $IP_ROOT"
31
31
  manifest="$IP_ROOT/intelligence.yaml"
32
32
  lock="$IP_ROOT/intelligence.lock"
33
33
 
34
- stamp="$(read_engine_stamp "$manifest")"
34
+ stamp="$(read_schema_version "$manifest")"
35
35
  eng="$(bundled_engine_version)"
36
36
  if [ -z "$stamp" ]; then
37
- warn "manifest has no sync_version — run 'intelligence upgrade'"
37
+ warn "manifest has no schema_version — run 'intelligence init'"
38
38
  elif _ver_gt "$stamp" "$eng"; then
39
39
  warn "manifest schema $stamp is newer than this CLI's engine $eng — update the CLI: npm i -g @ainova-systems/intelligence@latest"
40
40
  elif _ver_gt "$eng" "$stamp"; then
41
- warn "manifest schema $stamp behind engine $eng — run 'intelligence upgrade'"
41
+ warn "manifest schema $stamp behind engine $eng — run 'intelligence init'"
42
42
  else
43
- ok "sync_version $stamp matches the engine"
43
+ ok "schema_version $stamp matches the engine"
44
44
  fi
45
45
 
46
46
  # The engine-content package must sit at the bundled engine version — the
@@ -50,32 +50,39 @@ if [ -n "$sync_locked" ]; then
50
50
  if [ "${sync_locked#v}" = "$eng" ]; then
51
51
  ok "$SYNC_PKG_NAME at $sync_locked (matches the engine)"
52
52
  else
53
- warn "$SYNC_PKG_NAME locked at $sync_locked but the engine is $eng — run 'intelligence upgrade'"
53
+ warn "$SYNC_PKG_NAME locked at $sync_locked but the engine is $eng — run 'intelligence init'"
54
54
  fi
55
55
  fi
56
56
  if [ -d "$IP_ROOT/.intelligence/engine" ]; then
57
- warn "stale .intelligence/engine directory (pre-package layout) — run 'intelligence upgrade'"
57
+ warn "stale .intelligence/engine directory (pre-package layout) — run 'intelligence init'"
58
58
  fi
59
59
 
60
- # Manifest packages: names valid, locked, installed — and the lock's source
61
- # still the one resolution would pick today (a registry silently re-pointing
62
- # an installed name is the dependency-confusion tail this check cuts off).
60
+ # Manifest packages: names valid, requested intent only, locked and installed.
61
+ # The lock is authoritative for resolved source URL/path until an explicit
62
+ # package re-add changes that trust decision.
63
63
  while IFS= read -r name; do
64
64
  [ -n "$name" ] || continue
65
65
  assert_valid_pkg_name "$name"
66
66
  if [ -z "$(qmap_field "$lock" "packages" "$name" "url")" ]; then
67
- warn "$name is in the manifest but not in intelligence.lock — run 'intelligence install'"
68
- elif [ ! -d "$IP_ROOT/.intelligence/packages/$name" ]; then
69
- warn "$name is locked but not installed — run 'intelligence install'"
67
+ warn "$name is in the manifest but not in intelligence.lock — run 'intelligence init'"
70
68
  else
71
- ok "$name @ $(qmap_field "$lock" "packages" "$name" "resolved")"
72
- if [ -z "$(qmap_field "$manifest" "packages" "$name" "url")" ]; then
73
- locked_url="$(qmap_field "$lock" "packages" "$name" "url")"
74
- resolve_package_source "$manifest" "$name" 2>/dev/null || true
75
- if [ -n "${RES_URL:-}" ] && [ -n "$locked_url" ] && [ "$RES_URL" != "$locked_url" ]; then
76
- warn "$name now resolves to $RES_URL but the lock pins $locked_url — a registry re-pointed the name; verify before 'intelligence update'"
77
- fi
69
+ m_ver="$(qmap_field "$manifest" "packages" "$name" "version")"
70
+ m_ref="$(qmap_field "$manifest" "packages" "$name" "ref")"
71
+ l_req="$(qmap_field "$lock" "packages" "$name" "requested")"
72
+ l_res="$(qmap_field "$lock" "packages" "$name" "resolved")"
73
+ [ -z "$m_ver" ] || [ "$m_ver" = "$l_req" ] \
74
+ || warn "$name requests '$m_ver' but the lock recorded '$l_req' — run 'intelligence update --preview'"
75
+ [ -z "$m_ref" ] || [ "$m_ref" = "$l_res" ] \
76
+ || warn "$name pins ref '$m_ref' but the lock resolved '$l_res'"
77
+ if [ -n "$(qmap_field "$manifest" "packages" "$name" "url")" ] \
78
+ || [ -n "$(qmap_field "$manifest" "packages" "$name" "path")" ]; then
79
+ warn "$name stores source details in the manifest — run 'intelligence init --apply' to move them to the lock-only model"
80
+ fi
81
+ if [ ! -d "$IP_ROOT/.intelligence/packages/$name" ]; then
82
+ warn "$name is locked but not installed — run 'intelligence sync'"
83
+ continue
78
84
  fi
85
+ ok "$name @ $(qmap_field "$lock" "packages" "$name" "resolved")"
79
86
  fi
80
87
  done < <(qmap_keys "$manifest" "packages")
81
88
 
@@ -87,7 +94,7 @@ if [ -f "$lock" ]; then
87
94
  while IFS= read -r m; do
88
95
  [ "$m" = "$name" ] && found=1
89
96
  done < <(qmap_keys "$manifest" "packages")
90
- [ "$found" -eq 1 ] || warn "$name is locked but absent from the manifest — 'intelligence remove $name' cleans it up"
97
+ [ "$found" -eq 1 ] || warn "$name is locked but absent from the manifest — 'intelligence package remove $name' cleans it up"
91
98
  done < <(qmap_keys "$lock" "packages")
92
99
  fi
93
100
 
@@ -102,7 +109,7 @@ for section in rules agents skills; do
102
109
  if [ ! -d "$IP_ROOT/$src" ]; then
103
110
  case "$src" in
104
111
  # Store content is restorable state: absent means un-installed.
105
- .intelligence/*) warn "sources.$section '$src' missing — run 'intelligence install'" ;;
112
+ .intelligence/*) warn "sources.$section '$src' missing — run 'intelligence sync'" ;;
106
113
  # A project-owned dir that does not exist yet is a legitimate
107
114
  # state — a project consuming only packages authors nothing of
108
115
  # its own — and the engine simply skips it. Declared up front so
@@ -1,5 +1,5 @@
1
1
  #!/bin/bash
2
- # intelligence migrate [--dry-run] [--force] — convert a vendored (v1) setup
2
+ # Internal v1-to-v2 migration operation.
3
3
  # to the CLI setup: root intelligence.yaml, .intelligence/ store,
4
4
  # intelligence.lock, no vendored engine.
5
5
  #
@@ -24,7 +24,7 @@ done
24
24
  detect_project
25
25
  case "$IP_MODE" in
26
26
  legacy) ;;
27
- v2) die "already on the CLI setup ($IP_ROOT/intelligence.yaml)" ;;
27
+ v2) die "already on the v2 setup ($IP_ROOT/intelligence.yaml)" ;;
28
28
  *) die "no vendored setup found here — 'intelligence init' starts a fresh project" ;;
29
29
  esac
30
30
  root="$IP_ROOT"
@@ -32,15 +32,22 @@ umbrella="$IP_UMBRELLA"
32
32
  module_dir="$IP_MODULE_DIR"
33
33
  config="$umbrella/config.yaml"
34
34
  module_rel="${module_dir#"$root"/}"
35
+ umbrella_rel="${umbrella#"$root"/}"
35
36
 
36
- [ -f "$root/intelligence.yaml" ] && die "intelligence.yaml already exists at the root — half-migrated state; resolve it manually"
37
+ for existing in intelligence.yaml intelligence.lock .intelligence; do
38
+ if [ -e "$root/$existing" ] || [ -L "$root/$existing" ]; then
39
+ die "$existing already exists at the root — conflicting v2 state; move it aside or restore the project before migrating"
40
+ fi
41
+ done
37
42
  if [ "$dry_run" -eq 0 ] && [ "$force" -eq 0 ]; then
38
43
  if [ -n "$(git -C "$root" status --porcelain 2>/dev/null)" ]; then
39
44
  die "working tree is not clean — commit or stash first (or --force); migrate wants a one-commit diff you can review and revert"
40
45
  fi
41
46
  fi
42
47
 
43
- stamp="$(read_engine_stamp "$config")"
48
+ # `sync_version` belongs to the archived v1 format. The v2 contract uses
49
+ # `schema_version`, so conversion reads the legacy scalar explicitly.
50
+ stamp="$(top_scalar "$config" "sync_version")"
44
51
  eng="$(bundled_engine_version)"
45
52
  [ -n "$stamp" ] && _ver_gt "$stamp" "$eng" && die "project schema $stamp is newer than this CLI's engine $eng — update the CLI first"
46
53
 
@@ -50,9 +57,60 @@ get_pack_field() {
50
57
  get_nested_yaml_value "$1" "packs" "$2" "$3"
51
58
  }
52
59
 
60
+ paths_overlap() {
61
+ case "$1" in "$2"|"$2"/*) return 0 ;; esac
62
+ case "$2" in "$1"|"$1"/*) return 0 ;; esac
63
+ return 1
64
+ }
65
+
66
+ validate_mirror_path() {
67
+ local pack="$1" mirror="$2" canon rel src src_canon src_rel target output protected mirror_phys
68
+ [ -n "$mirror" ] || return 0
69
+ case "$mirror" in
70
+ .|/*|*..*|*\\*|[A-Za-z]:*) die "pack '$pack' has unsafe mirror path '$mirror'" ;;
71
+ esac
72
+ canon="$(normalize_path "$root/$mirror")"
73
+ case "$canon" in "$root"/*) ;; *) die "pack '$pack' mirror escapes the repository: '$mirror'" ;; esac
74
+ rel="${canon#"$root"/}"
75
+
76
+ # A v1 mirror may live below the content directory (the normal location is
77
+ # intelligence/external/<pack>), but it must never own the directory itself,
78
+ # the vendored module, CLI state, authored sources, or generated outputs.
79
+ case "$umbrella_rel" in "$rel"|"$rel"/*) die "pack '$pack' mirror '$mirror' contains the v1 content directory" ;; esac
80
+ for protected in .git .intelligence "$module_rel"; do
81
+ paths_overlap "$rel" "$protected" && die "pack '$pack' mirror '$mirror' overlaps protected path '$protected'"
82
+ done
83
+ for section in rules agents skills; do
84
+ while IFS= read -r src; do
85
+ case "$src" in ""|git+*|@*) continue ;; esac
86
+ src_canon="$(normalize_path "$root/$src")"
87
+ case "$src_canon" in "$root"/*) src_rel="${src_canon#"$root"/}" ;; *) continue ;; esac
88
+ paths_overlap "$rel" "$src_rel" \
89
+ && die "pack '$pack' mirror '$mirror' overlaps configured source '$src'"
90
+ done < <(read_yaml_list "$config" "$section")
91
+ done
92
+ while IFS= read -r target; do
93
+ [ -n "$target" ] || continue
94
+ output="$(get_target_output "$config" "$target")"
95
+ [ -n "$output" ] || continue
96
+ case "$output" in /*|*..*) continue ;; esac
97
+ paths_overlap "$rel" "${output%/}" \
98
+ && die "pack '$pack' mirror '$mirror' overlaps target '$target' output '$output'"
99
+ done < <(read_yaml_keys "$config" "targets")
100
+
101
+ if [ -d "$canon" ]; then
102
+ mirror_phys="$(cd "$canon" && pwd -P)"
103
+ case "$mirror_phys" in "$root"|"$root"/*) ;; *) die "pack '$pack' mirror resolves outside the repository: '$mirror'" ;; esac
104
+ [ -f "$canon/.pack" ] || die "pack '$pack' mirror '$mirror' has no .pack ownership stamp — refusing to copy or delete project-owned content"
105
+ fi
106
+ }
107
+
53
108
  # ---- Stage ----------------------------------------------------------------
54
- stage="$root/.intelligence-migrate-stage.$$"
55
- [ "$dry_run" -eq 1 ] && stage="$(mktemp -d -t intelligence-migrate-XXXXXX 2>/dev/null || mktemp -d)"
109
+ if [ "$dry_run" -eq 1 ]; then
110
+ stage="$(mktemp -d -t intelligence-migrate-XXXXXX 2>/dev/null || mktemp -d)"
111
+ else
112
+ stage="$(mktemp -d "$root/.intelligence-migrate-stage.XXXXXX")"
113
+ fi
56
114
  cleanup_stage() { rm -rf "$stage"; }
57
115
  trap cleanup_stage EXIT INT TERM
58
116
  mkdir -p "$stage/.intelligence/packages"
@@ -67,7 +125,7 @@ if [ -z "$stamp" ] || _ver_gt "$V1_FINAL_SCHEMA" "$stamp"; then
67
125
  echo "ERROR: project schema is ${stamp:-absent (pre-0.3.1)}, older than the final v1 schema $V1_FINAL_SCHEMA." >&2
68
126
  echo " Bring it forward with its own engine first — that still works:" >&2
69
127
  echo " bash $module_rel/scripts/update.sh --yes" >&2
70
- echo " then re-run: intelligence migrate" >&2
128
+ echo " then re-run: intelligence init" >&2
71
129
  exit 1
72
130
  fi
73
131
 
@@ -81,12 +139,21 @@ sync_pkg_sha="$(fetch_package "$SYNC_PKG_URL" "v$eng" "$SYNC_PKG_PATH" "$stage/$
81
139
  pack_rows="$stage/packs.rows"
82
140
  : > "$pack_rows"
83
141
  seen_names=" "
142
+ seen_mirrors=" "
84
143
  while IFS= read -r pack; do
85
144
  [ -n "$pack" ] || continue
86
145
  url="$(get_pack_field "$config" "$pack" "url")"
87
146
  ref="$(get_pack_field "$config" "$pack" "ref")"
88
147
  mirror="$(get_pack_field "$config" "$pack" "mirror")"
89
148
  [ -n "$url" ] || die "pack '$pack' has no url in config.yaml"
149
+ assert_safe_source_url "$url"
150
+ assert_safe_ref "$ref"
151
+ validate_mirror_path "$pack" "$mirror"
152
+ if [ -n "$mirror" ]; then
153
+ mirror_key="$(normalize_path "$root/$mirror")"
154
+ case "$seen_mirrors" in *" $mirror_key "*) die "pack '$pack' shares mirror '$mirror' with another pack" ;; esac
155
+ seen_mirrors="$seen_mirrors$mirror_key "
156
+ fi
90
157
  base="${url%.git}"; base="${base%/}"
91
158
  repo="$(basename "$base")"
92
159
  org="$(basename "$(dirname "$base")")"
@@ -125,8 +192,9 @@ while IFS="$LOCK_SEP" read -r pack name url ref mirror; do
125
192
  echo " pack '$pack' -> $name (fetched)"
126
193
  fi
127
194
  if [ -z "$sha" ] || [ "$sha" = "unknown" ]; then
128
- sha="$(GIT_TERMINAL_PROMPT=0 git ls-remote "$url" "$ref" 2>/dev/null | awk '{print $1; exit}')"
195
+ sha="$(GIT_TERMINAL_PROMPT=0 git ls-remote -- "$url" "$ref" 2>/dev/null | awk '{print $1; exit}')"
129
196
  fi
197
+ [ -n "$ref" ] || ref="HEAD"
130
198
  printf '%s\037%s\037%s\037%s\037%s\037%s\n' \
131
199
  "$name" "" "$url" "" "$ref" "$sha" >> "$lock_rows"
132
200
  done < "$pack_rows"
@@ -192,19 +260,16 @@ while IFS= read -r line || [ -n "$line" ]; do
192
260
  fi
193
261
  echo "$line"
194
262
  done < "$config" > "$manifest_stage"
195
- stamp_version "$manifest_stage" "$eng"
263
+ stamp_schema_version "$manifest_stage" "$eng"
196
264
 
197
- # packages: entries (pin preserved — no invented ranges).
265
+ # packages: requested pins only; resolved source details live in the lock.
198
266
  while IFS="$LOCK_SEP" read -r pack name url ref mirror; do
199
267
  [ -n "$pack" ] || continue
200
- qmap_set "$manifest_stage" "packages" "$name" "url" "$url"
201
- [ -n "$ref" ] && qmap_set "$manifest_stage" "packages" "$name" "ref" "$ref"
268
+ qmap_set "$manifest_stage" "packages" "$name" "ref" "${ref:-HEAD}"
202
269
  done < "$pack_rows"
203
270
 
204
271
  # The engine's own content rides along as a package, pinned to the engine.
205
272
  qmap_set "$manifest_stage" "packages" "$SYNC_PKG_NAME" "version" "$eng"
206
- qmap_set "$manifest_stage" "packages" "$SYNC_PKG_NAME" "url" "$SYNC_PKG_URL"
207
- qmap_set "$manifest_stage" "packages" "$SYNC_PKG_NAME" "path" "$SYNC_PKG_PATH"
208
273
  printf '%s\037%s\037%s\037%s\037%s\037%s\n' \
209
274
  "$SYNC_PKG_NAME" "$eng" "$SYNC_PKG_URL" "$SYNC_PKG_PATH" "v$eng" "$sync_pkg_sha" >> "$lock_rows"
210
275
 
@@ -221,14 +286,24 @@ for section in rules agents skills; do
221
286
  esac
222
287
  done < <(read_yaml_list "$manifest_stage" "$section")
223
288
  done
224
- for adapter in agents claude cursor copilot codex pi opencode; do
289
+ target_keys() {
290
+ awk '
291
+ /^targets:[[:space:]]*$/ { in_targets=1; next }
292
+ in_targets && /^[^[:space:]#]/ { exit }
293
+ in_targets && /^ [A-Za-z_][A-Za-z0-9_]*:/ {
294
+ line=$0; sub(/^ /, "", line); sub(/:.*/, "", line); print line
295
+ }
296
+ ' "$1"
297
+ }
298
+ while IFS= read -r adapter; do
299
+ [ -n "$adapter" ] || continue
225
300
  old_e="$(is_target_enabled "$config" "$adapter")"
226
301
  new_e="$(is_target_enabled "$manifest_stage" "$adapter")"
227
302
  [ "$old_e" = "$new_e" ] || { echo " TARGET DRIFT: $adapter enabled '$old_e' -> '$new_e'"; fail=1; }
228
303
  old_o="$(get_target_output "$config" "$adapter")"
229
304
  new_o="$(get_target_output "$manifest_stage" "$adapter")"
230
305
  [ "$old_o" = "$new_o" ] || { echo " TARGET DRIFT: $adapter output '$old_o' -> '$new_o'"; fail=1; }
231
- done
306
+ done < <({ target_keys "$config"; target_keys "$manifest_stage"; } | sort -u)
232
307
  [ "$fail" -eq 0 ] || die "staged state failed verification — nothing changed"
233
308
 
234
309
  if [ "$dry_run" -eq 1 ]; then
@@ -250,40 +325,51 @@ if [ -f "$root/.gitignore" ]; then
250
325
  gitignore_existed=1
251
326
  cp "$root/.gitignore" "$stage/gitignore.orig"
252
327
  fi
253
- if [ ! -f "$root/.gitignore" ] || ! grep -q '^\.intelligence/' "$root/.gitignore"; then
254
- {
255
- [ -f "$root/.gitignore" ] && [ -n "$(tail -c 1 "$root/.gitignore" 2>/dev/null)" ] && echo ""
256
- echo "# intelligence CLI package store (restored by 'intelligence install')"
257
- echo ".intelligence/"
258
- } >> "$root/.gitignore"
259
- fi
328
+ commit_active=1
260
329
  rollback() {
330
+ [ "$commit_active" -eq 1 ] || return 0
331
+ commit_active=0
332
+ set +e
261
333
  rm -rf "$root/.intelligence" "$root/intelligence.yaml" "$root/intelligence.lock"
262
334
  if [ "$gitignore_existed" -eq 1 ]; then
263
335
  cp "$stage/gitignore.orig" "$root/.gitignore"
264
336
  else
265
337
  rm -f "$root/.gitignore"
266
338
  fi
339
+ rm -rf "$stage"
267
340
  echo "rolled back — the vendored setup is untouched" >&2
268
341
  }
269
- # From the first moved file until success, an interrupt must roll back too.
342
+ rollback_on_exit() {
343
+ local rc=$?
344
+ rollback
345
+ exit "$rc"
346
+ }
347
+ # Every live write through verified sync is covered by rollback, including a
348
+ # plain set -e/EXIT failure rather than only an explicit sync refusal.
349
+ trap rollback_on_exit EXIT
270
350
  trap 'rollback; exit 130' INT TERM
351
+ if [ ! -f "$root/.gitignore" ] || ! grep -q '^\.intelligence/' "$root/.gitignore"; then
352
+ {
353
+ [ -f "$root/.gitignore" ] && [ -n "$(tail -c 1 "$root/.gitignore" 2>/dev/null)" ] && echo ""
354
+ echo "# intelligence CLI package store (restored automatically by 'intelligence sync')"
355
+ echo ".intelligence/"
356
+ } >> "$root/.gitignore"
357
+ fi
271
358
  mv "$stage/.intelligence" "$root/.intelligence"
272
359
  cp "$manifest_stage" "$root/intelligence.yaml"
273
360
  lock_write_from_tsv "$root/intelligence.lock" "$lock_rows"
274
361
 
275
362
  sync_out="$(cd "$root" && IS_SUPPRESS_CLI_NOTE=1 bash "$CLI_DIR/commands/sync.sh" 2>&1)" || {
276
363
  echo "$sync_out"
277
- rollback
278
- die "sync of the migrated state failed — rolled back"
364
+ die "sync of the migrated state failed"
279
365
  }
280
366
  echo "$sync_out" | grep -q '^IS_STATUS=ok' || {
281
367
  echo "$sync_out"
282
- rollback
283
- die "sync of the migrated state did not report ok — rolled back"
368
+ die "sync of the migrated state did not report ok"
284
369
  }
285
370
  # Point of no return: the new state is verified. From here an interrupt must
286
371
  # NOT roll back (the old setup is being removed) — only clean the stage.
372
+ commit_active=0
287
373
  trap cleanup_stage EXIT INT TERM
288
374
 
289
375
  # Only now is anything of the old setup removed — and config.yaml is kept as
@@ -298,7 +384,7 @@ while IFS="$LOCK_SEP" read -r pack name url ref mirror; do
298
384
  esac
299
385
  if [ -d "$root/$mirror" ]; then
300
386
  rm -rf "${root:?}/$mirror"
301
- # An emptied mirror parent (e.g. <umbrella>/external/) goes with it.
387
+ # An emptied v1 mirror parent (for example intelligence/external/) goes with it.
302
388
  rmdir "$(dirname "$root/$mirror")" 2>/dev/null || true
303
389
  fi
304
390
  done < "$pack_rows"
@@ -310,4 +396,5 @@ rmdir "$umbrella" 2>/dev/null || true
310
396
 
311
397
  echo ""
312
398
  echo "migrated. Review the diff, then commit. The old config.yaml is kept at .intelligence/backup/config.yaml."
313
- echo "From now on: intelligence sync | add | install | doctor."
399
+ echo "From now on: intelligence sync | package | update | status."
400
+ echo "Next: ask your agent to run /intelligence-learn-from-repository to review the migrated project context."