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

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 (50) hide show
  1. package/README.md +15 -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} +32 -14
  12. package/cli/{commands/migrate.sh → internal/migrate-v1.sh} +114 -25
  13. package/cli/{commands/add.sh → internal/package-add.sh} +3 -3
  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 +97 -0
  18. package/cli/{commands/install.sh → internal/restore.sh} +3 -3
  19. package/cli/internal/target-state.sh +57 -0
  20. package/cli/{commands/upgrade.sh → internal/upgrade-v2.sh} +48 -8
  21. package/cli/lib/cli-common.sh +133 -7
  22. package/cli/lib/lockfile.sh +1 -1
  23. package/cli/lib/manifest.sh +86 -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/packages/sync/docs/ADAPTERS.md +0 -214
  50. 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,11 +50,11 @@ 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
60
  # Manifest packages: names valid, locked, installed — and the lock's source
@@ -64,16 +64,34 @@ 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
69
+ m_ver="$(qmap_field "$manifest" "packages" "$name" "version")"
70
+ m_ref="$(qmap_field "$manifest" "packages" "$name" "ref")"
71
+ m_url="$(qmap_field "$manifest" "packages" "$name" "url")"
72
+ m_path="$(qmap_field "$manifest" "packages" "$name" "path")"
73
+ l_req="$(qmap_field "$lock" "packages" "$name" "requested")"
74
+ l_res="$(qmap_field "$lock" "packages" "$name" "resolved")"
75
+ l_url="$(qmap_field "$lock" "packages" "$name" "url")"
76
+ l_path="$(qmap_field "$lock" "packages" "$name" "path")"
77
+ [ -z "$m_ver" ] || [ "$m_ver" = "$l_req" ] \
78
+ || warn "$name requests '$m_ver' but the lock recorded '$l_req' — run 'intelligence update --preview'"
79
+ [ -z "$m_ref" ] || [ "$m_ref" = "$l_res" ] \
80
+ || warn "$name pins ref '$m_ref' but the lock resolved '$l_res'"
81
+ [ -z "$m_url" ] || [ "$m_url" = "$l_url" ] \
82
+ || warn "$name declares url '$m_url' but the lock recorded '$l_url'"
83
+ [ -z "$m_path" ] || [ "$m_path" = "$l_path" ] \
84
+ || warn "$name declares path '$m_path' but the lock recorded '$l_path'"
85
+ if [ ! -d "$IP_ROOT/.intelligence/packages/$name" ]; then
86
+ warn "$name is locked but not installed — run 'intelligence sync'"
87
+ continue
88
+ fi
71
89
  ok "$name @ $(qmap_field "$lock" "packages" "$name" "resolved")"
72
90
  if [ -z "$(qmap_field "$manifest" "packages" "$name" "url")" ]; then
73
91
  locked_url="$(qmap_field "$lock" "packages" "$name" "url")"
74
92
  resolve_package_source "$manifest" "$name" 2>/dev/null || true
75
93
  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'"
94
+ warn "$name now resolves to $RES_URL but the lock pins $locked_url — a registry re-pointed the name; verify with 'intelligence update --preview'"
77
95
  fi
78
96
  fi
79
97
  fi
@@ -87,7 +105,7 @@ if [ -f "$lock" ]; then
87
105
  while IFS= read -r m; do
88
106
  [ "$m" = "$name" ] && found=1
89
107
  done < <(qmap_keys "$manifest" "packages")
90
- [ "$found" -eq 1 ] || warn "$name is locked but absent from the manifest — 'intelligence remove $name' cleans it up"
108
+ [ "$found" -eq 1 ] || warn "$name is locked but absent from the manifest — 'intelligence package remove $name' cleans it up"
91
109
  done < <(qmap_keys "$lock" "packages")
92
110
  fi
93
111
 
@@ -102,7 +120,7 @@ for section in rules agents skills; do
102
120
  if [ ! -d "$IP_ROOT/$src" ]; then
103
121
  case "$src" in
104
122
  # Store content is restorable state: absent means un-installed.
105
- .intelligence/*) warn "sources.$section '$src' missing — run 'intelligence install'" ;;
123
+ .intelligence/*) warn "sources.$section '$src' missing — run 'intelligence sync'" ;;
106
124
  # A project-owned dir that does not exist yet is a legitimate
107
125
  # state — a project consuming only packages authors nothing of
108
126
  # 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,7 +192,7 @@ 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
130
197
  printf '%s\037%s\037%s\037%s\037%s\037%s\n' \
131
198
  "$name" "" "$url" "" "$ref" "$sha" >> "$lock_rows"
@@ -192,7 +259,7 @@ while IFS= read -r line || [ -n "$line" ]; do
192
259
  fi
193
260
  echo "$line"
194
261
  done < "$config" > "$manifest_stage"
195
- stamp_version "$manifest_stage" "$eng"
262
+ stamp_schema_version "$manifest_stage" "$eng"
196
263
 
197
264
  # packages: entries (pin preserved — no invented ranges).
198
265
  while IFS="$LOCK_SEP" read -r pack name url ref mirror; do
@@ -221,14 +288,24 @@ for section in rules agents skills; do
221
288
  esac
222
289
  done < <(read_yaml_list "$manifest_stage" "$section")
223
290
  done
224
- for adapter in agents claude cursor copilot codex pi opencode; do
291
+ target_keys() {
292
+ awk '
293
+ /^targets:[[:space:]]*$/ { in_targets=1; next }
294
+ in_targets && /^[^[:space:]#]/ { exit }
295
+ in_targets && /^ [A-Za-z_][A-Za-z0-9_]*:/ {
296
+ line=$0; sub(/^ /, "", line); sub(/:.*/, "", line); print line
297
+ }
298
+ ' "$1"
299
+ }
300
+ while IFS= read -r adapter; do
301
+ [ -n "$adapter" ] || continue
225
302
  old_e="$(is_target_enabled "$config" "$adapter")"
226
303
  new_e="$(is_target_enabled "$manifest_stage" "$adapter")"
227
304
  [ "$old_e" = "$new_e" ] || { echo " TARGET DRIFT: $adapter enabled '$old_e' -> '$new_e'"; fail=1; }
228
305
  old_o="$(get_target_output "$config" "$adapter")"
229
306
  new_o="$(get_target_output "$manifest_stage" "$adapter")"
230
307
  [ "$old_o" = "$new_o" ] || { echo " TARGET DRIFT: $adapter output '$old_o' -> '$new_o'"; fail=1; }
231
- done
308
+ done < <({ target_keys "$config"; target_keys "$manifest_stage"; } | sort -u)
232
309
  [ "$fail" -eq 0 ] || die "staged state failed verification — nothing changed"
233
310
 
234
311
  if [ "$dry_run" -eq 1 ]; then
@@ -250,40 +327,51 @@ if [ -f "$root/.gitignore" ]; then
250
327
  gitignore_existed=1
251
328
  cp "$root/.gitignore" "$stage/gitignore.orig"
252
329
  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
330
+ commit_active=1
260
331
  rollback() {
332
+ [ "$commit_active" -eq 1 ] || return 0
333
+ commit_active=0
334
+ set +e
261
335
  rm -rf "$root/.intelligence" "$root/intelligence.yaml" "$root/intelligence.lock"
262
336
  if [ "$gitignore_existed" -eq 1 ]; then
263
337
  cp "$stage/gitignore.orig" "$root/.gitignore"
264
338
  else
265
339
  rm -f "$root/.gitignore"
266
340
  fi
341
+ rm -rf "$stage"
267
342
  echo "rolled back — the vendored setup is untouched" >&2
268
343
  }
269
- # From the first moved file until success, an interrupt must roll back too.
344
+ rollback_on_exit() {
345
+ local rc=$?
346
+ rollback
347
+ exit "$rc"
348
+ }
349
+ # Every live write through verified sync is covered by rollback, including a
350
+ # plain set -e/EXIT failure rather than only an explicit sync refusal.
351
+ trap rollback_on_exit EXIT
270
352
  trap 'rollback; exit 130' INT TERM
353
+ if [ ! -f "$root/.gitignore" ] || ! grep -q '^\.intelligence/' "$root/.gitignore"; then
354
+ {
355
+ [ -f "$root/.gitignore" ] && [ -n "$(tail -c 1 "$root/.gitignore" 2>/dev/null)" ] && echo ""
356
+ echo "# intelligence CLI package store (restored automatically by 'intelligence sync')"
357
+ echo ".intelligence/"
358
+ } >> "$root/.gitignore"
359
+ fi
271
360
  mv "$stage/.intelligence" "$root/.intelligence"
272
361
  cp "$manifest_stage" "$root/intelligence.yaml"
273
362
  lock_write_from_tsv "$root/intelligence.lock" "$lock_rows"
274
363
 
275
364
  sync_out="$(cd "$root" && IS_SUPPRESS_CLI_NOTE=1 bash "$CLI_DIR/commands/sync.sh" 2>&1)" || {
276
365
  echo "$sync_out"
277
- rollback
278
- die "sync of the migrated state failed — rolled back"
366
+ die "sync of the migrated state failed"
279
367
  }
280
368
  echo "$sync_out" | grep -q '^IS_STATUS=ok' || {
281
369
  echo "$sync_out"
282
- rollback
283
- die "sync of the migrated state did not report ok — rolled back"
370
+ die "sync of the migrated state did not report ok"
284
371
  }
285
372
  # Point of no return: the new state is verified. From here an interrupt must
286
373
  # NOT roll back (the old setup is being removed) — only clean the stage.
374
+ commit_active=0
287
375
  trap cleanup_stage EXIT INT TERM
288
376
 
289
377
  # Only now is anything of the old setup removed — and config.yaml is kept as
@@ -298,7 +386,7 @@ while IFS="$LOCK_SEP" read -r pack name url ref mirror; do
298
386
  esac
299
387
  if [ -d "$root/$mirror" ]; then
300
388
  rm -rf "${root:?}/$mirror"
301
- # An emptied mirror parent (e.g. <umbrella>/external/) goes with it.
389
+ # An emptied v1 mirror parent (for example intelligence/external/) goes with it.
302
390
  rmdir "$(dirname "$root/$mirror")" 2>/dev/null || true
303
391
  fi
304
392
  done < "$pack_rows"
@@ -310,4 +398,5 @@ rmdir "$umbrella" 2>/dev/null || true
310
398
 
311
399
  echo ""
312
400
  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."
401
+ echo "From now on: intelligence sync | package | update | status."
402
+ echo "Next: ask your agent to run /intelligence-learn-from-repository to review the migrated project context."
@@ -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
@@ -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