@ainova-systems/intelligence 0.12.1 → 0.14.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -34,7 +34,8 @@ intelligence sync
34
34
  |---|---|
35
35
  | `intelligence init [--preview\|--apply]` | Create, convert, restore or align a project |
36
36
  | `intelligence sync [adapter]` | Restore locked content if needed, then render all or one enabled adapter |
37
- | `intelligence update [@scope/name] [--preview\|--apply]` | Plan or apply project and ranged-package updates |
37
+ | `intelligence update [@scope/name] [--latest] [--preview\|--apply]` | Plan or apply project and ranged-package updates; `--latest` crosses one package's range |
38
+ | `intelligence upgrade [--next] [--preview\|--apply]` | Replace the installed CLI with the newest version on its npm channel |
38
39
  | `intelligence package add\|remove\|list\|search` | Manage versioned Intelligence Packages |
39
40
  | `intelligence adapter list\|create\|enable\|disable\|remove` | Manage render adapters |
40
41
  | `intelligence status [--check]` | Inspect project state and consistency |
@@ -47,10 +47,19 @@ function findBash() {
47
47
 
48
48
  const cli = path.join(__dirname, '..', 'cli', 'intelligence').replace(/\\/g, '/');
49
49
  const pkg = require(path.join(__dirname, '..', 'package.json'));
50
+ const binName = Object.keys(pkg.bin)[0];
50
51
 
51
52
  const result = spawnSync(findBash(), [cli].concat(process.argv.slice(2)), {
52
53
  stdio: 'inherit',
53
- env: Object.assign({}, process.env, { INTELLIGENCE_NPM_VERSION: pkg.version }),
54
+ // What npm installed, for `update` (channel check) and `upgrade` (replace
55
+ // exactly this package): its version and name, the command npm linked as
56
+ // its shim, and this launcher's path inside the package.
57
+ env: Object.assign({}, process.env, {
58
+ INTELLIGENCE_NPM_VERSION: pkg.version,
59
+ INTELLIGENCE_NPM_PACKAGE: pkg.name,
60
+ INTELLIGENCE_NPM_BIN: binName,
61
+ INTELLIGENCE_NPM_LAUNCHER: pkg.bin[binName],
62
+ }),
54
63
  });
55
64
  if (result.error) {
56
65
  console.error('intelligence: failed to launch bash: ' + result.error.message);
@@ -2,22 +2,28 @@
2
2
  # intelligence update [@scope/name] [--preview|--apply]
3
3
  #
4
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.
5
+ # stop, ask interactively, or apply without a prompt. Replacing the global npm
6
+ # installation is `intelligence upgrade`, which the plan names; it never
7
+ # happens inside a project command.
7
8
  set -euo pipefail
8
9
  source "$CLI_DIR/lib/cli-common.sh"
9
10
 
10
- only="" mode="ask" preview_seen=0 apply_seen=0
11
+ only="" mode="ask" preview_seen=0 apply_seen=0 latest=0
11
12
  while [ $# -gt 0 ]; do
12
13
  case "$1" in
13
14
  --preview) preview_seen=1; mode="preview" ;;
14
15
  --apply) apply_seen=1; mode="apply" ;;
16
+ --latest) latest=1 ;;
15
17
  @*) [ -z "$only" ] || die "only one package may be selected"; only="$1" ;;
16
- *) die "usage: intelligence update [@scope/name] [--preview|--apply]" ;;
18
+ *) die "usage: intelligence update [@scope/name] [--latest] [--preview|--apply]" ;;
17
19
  esac
18
20
  shift
19
21
  done
20
22
  [ "$preview_seen" -eq 0 ] || [ "$apply_seen" -eq 0 ] || die "choose either --preview or --apply"
23
+ # Crossing a range is one package's decision, and SemVer marks the boundary as
24
+ # the place a changelog has to be read.
25
+ [ "$latest" -eq 0 ] || [ -n "$only" ] \
26
+ || die "--latest needs the package to move: intelligence update @scope/name --latest"
21
27
 
22
28
  require_cli_project
23
29
  manifest="$IP_ROOT/intelligence.yaml"
@@ -30,18 +36,27 @@ project_change=0
30
36
  echo "Update plan"
31
37
  echo ""
32
38
  echo "CLI:"
33
- if [ -n "${INTELLIGENCE_NPM_VERSION:-}" ] && [ "${IS_SKIP_NPM_CHECK:-0}" != "1" ] && command -v npm >/dev/null 2>&1; then
34
- channel="latest"
35
- case "$INTELLIGENCE_NPM_VERSION" in *-*) channel="next" ;; esac
36
- available="$(npm view "@ainova-systems/intelligence" "dist-tags.$channel" 2>/dev/null || true)"
37
- if [ -z "$available" ]; then
38
- echo " installed: $INTELLIGENCE_NPM_VERSION; registry check unavailable"
39
- elif [ "$available" = "$INTELLIGENCE_NPM_VERSION" ]; then
40
- echo " $INTELLIGENCE_NPM_VERSION (up to date on npm $channel)"
39
+ if [ -n "${INTELLIGENCE_NPM_VERSION:-}" ] && [ -n "${INTELLIGENCE_NPM_PACKAGE:-}" ] \
40
+ && [ "${IS_SKIP_NPM_CHECK:-0}" != "1" ] && command -v npm >/dev/null 2>&1; then
41
+ channel="$(npm_channel_for "$INTELLIGENCE_NPM_VERSION")"
42
+ # Ask the way `upgrade` will install: in global mode at this tree's own
43
+ # prefix when npm made it, so the plan and the install read one config.
44
+ cli_install_classify "$(cd "$CLI_DIR/.." && pwd -P)" "$INTELLIGENCE_NPM_PACKAGE" "${INTELLIGENCE_NPM_BIN:-}"
45
+ npm_prefix=""
46
+ [ "$CLI_INSTALL_KIND" != "npm" ] || npm_prefix="$(native_path "$CLI_INSTALL_PREFIX")"
47
+ # An empty or non-version answer is "not checked", never a comparison.
48
+ if available="$(cli_registry_version "$INTELLIGENCE_NPM_PACKAGE" "$channel" "$npm_prefix")"; then
49
+ case "$(semver_cmp_full "$available" "$INTELLIGENCE_NPM_VERSION")" in
50
+ 1)
51
+ echo " $INTELLIGENCE_NPM_VERSION -> $available (npm $channel)"
52
+ echo " run: intelligence upgrade"
53
+ echo " then rerun: intelligence update --apply"
54
+ ;;
55
+ 0) echo " $INTELLIGENCE_NPM_VERSION (up to date on npm $channel)" ;;
56
+ *) echo " $INTELLIGENCE_NPM_VERSION (ahead of npm $channel $available)" ;;
57
+ esac
41
58
  else
42
- echo " $INTELLIGENCE_NPM_VERSION -> $available (npm $channel)"
43
- echo " run: npm install -g @ainova-systems/intelligence@$channel"
44
- echo " then rerun: intelligence update --apply"
59
+ echo " installed: $INTELLIGENCE_NPM_VERSION; registry check unavailable"
45
60
  fi
46
61
  else
47
62
  echo " source checkout/registry check skipped; engine $eng"
@@ -50,7 +65,7 @@ fi
50
65
  echo ""
51
66
  echo "Project:"
52
67
  if project_stamped_ahead "$IP_ROOT"; then
53
- echo " schema $stamp is ahead of engine $eng — left as is; update the CLI to align it"
68
+ echo " schema $stamp is ahead of engine $eng — left as is; update the CLI to align it: intelligence upgrade (--next when the stamp came from a prerelease line)"
54
69
  elif project_needs_upgrade "$IP_ROOT"; then
55
70
  echo " lifecycle alignment required (stamp ${stamp:-unstamped}, engine $eng)"
56
71
  project_change=1
@@ -64,6 +79,7 @@ fi
64
79
 
65
80
  pkg_args=(--preview)
66
81
  [ -z "$only" ] || pkg_args+=("$only")
82
+ [ "$latest" -eq 0 ] || pkg_args+=(--latest)
67
83
  pkg_plan="$(bash "$CLI_DIR/internal/package-update.sh" "${pkg_args[@]}")"
68
84
  echo ""
69
85
  echo "Packages:"
@@ -92,5 +108,6 @@ fi
92
108
  ensure_project_current "$IP_ROOT"
93
109
  apply_args=(--no-sync)
94
110
  [ -z "$only" ] || apply_args+=("$only")
111
+ [ "$latest" -eq 0 ] || apply_args+=(--latest)
95
112
  bash "$CLI_DIR/internal/package-update.sh" "${apply_args[@]}"
96
113
  exec bash "$CLI_DIR/commands/sync.sh"
@@ -0,0 +1,100 @@
1
+ #!/bin/bash
2
+ # intelligence upgrade [--next] [--preview|--apply]
3
+ #
4
+ # Replace the installed CLI with the newest version on its npm channel:
5
+ # `next` when the running version is a prerelease or --next is given,
6
+ # `latest` otherwise. `update` plans this step and points here; this is the
7
+ # one command that writes to an npm prefix, and it never reads or writes a
8
+ # project.
9
+ set -euo pipefail
10
+ source "$CLI_DIR/lib/cli-common.sh"
11
+
12
+ mode="ask" preview_seen=0 apply_seen=0 channel=""
13
+ while [ $# -gt 0 ]; do
14
+ case "$1" in
15
+ --preview) preview_seen=1; mode="preview" ;;
16
+ --apply) apply_seen=1; mode="apply" ;;
17
+ --next) channel="next" ;;
18
+ *) die "usage: intelligence upgrade [--next] [--preview|--apply]" ;;
19
+ esac
20
+ shift
21
+ done
22
+ [ "$preview_seen" -eq 0 ] || [ "$apply_seen" -eq 0 ] || die "choose either --preview or --apply"
23
+
24
+ pkg_dir="$(cd "$CLI_DIR/.." && pwd -P)"
25
+ installed="${INTELLIGENCE_NPM_VERSION:-}"
26
+ pkg="${INTELLIGENCE_NPM_PACKAGE:-}"
27
+ bin="${INTELLIGENCE_NPM_BIN:-}"
28
+ # All of it comes from the npm launcher; `bash cli/intelligence` in a checkout has none.
29
+ [ -n "$installed" ] && [ -n "$pkg" ] && [ -n "$bin" ] && [ -n "${INTELLIGENCE_NPM_LAUNCHER:-}" ] \
30
+ || die "this CLI runs from a source checkout ($pkg_dir), not an npm installation — pull the repository instead"
31
+ command -v npm >/dev/null 2>&1 || die "npm is not on PATH — it installed this CLI and is needed to replace it"
32
+ [ -n "$channel" ] || channel="$(npm_channel_for "$installed")"
33
+
34
+ # Where this tree lives is decided locally, before any network: a tree npm
35
+ # did not make is refused whatever the registry would say. The hint names
36
+ # the exact version when the registry answers and the channel otherwise.
37
+ cli_install_classify "$pkg_dir" "$pkg" "$bin"
38
+ if [ "$CLI_INSTALL_KIND" != "npm" ]; then
39
+ available="$(cli_registry_version "$pkg" "$channel" || true)"
40
+ die "this CLI ($pkg_dir) is not an npm global installation — $(cli_install_hint "$CLI_INSTALL_KIND" "$pkg" "${available:-$channel}")"
41
+ fi
42
+ prefix="$(native_path "$CLI_INSTALL_PREFIX")"
43
+ launcher="$(native_path "$pkg_dir/$INTELLIGENCE_NPM_LAUNCHER")"
44
+
45
+ available="$(cli_registry_version "$pkg" "$channel" "$prefix")" \
46
+ || die "npm could not report a version for $pkg dist-tag $channel — check the network or the registry and retry"
47
+
48
+ case "$(semver_cmp_full "$available" "$installed")" in
49
+ 0)
50
+ echo "CLI $installed is up to date (npm $channel)"
51
+ exit 0
52
+ ;;
53
+ -1)
54
+ # Never a downgrade: a channel moved back on purpose is a decision
55
+ # the user takes by hand, with the command that does it.
56
+ echo "CLI $installed is newer than npm $channel ($available); nothing to do"
57
+ echo " to move back deliberately: npm install -g --prefix $(quote_for_shell "$prefix") $pkg@$available"
58
+ exit 0
59
+ ;;
60
+ esac
61
+
62
+ echo "CLI upgrade: $installed -> $available (npm $channel)"
63
+ echo " npm install -g --prefix $(quote_for_shell "$prefix") $pkg@$available"
64
+ [ "$mode" != "preview" ] || exit 0
65
+ if [ "$mode" = "ask" ]; then
66
+ [ -t 0 ] || die "upgrade requires confirmation — rerun with --preview or --apply"
67
+ printf '\nInstall %s? [Y/n] ' "$available"
68
+ read -r answer
69
+ case "$answer" in
70
+ ""|y|Y|yes|YES) ;;
71
+ *) echo "upgrade cancelled"; exit 0 ;;
72
+ esac
73
+ fi
74
+ echo ""
75
+
76
+ # npm rewrites the directory this script lives in, and Windows refuses to
77
+ # delete a file another process still holds open. `bash -c` parses its whole
78
+ # program before running it, so the fresh shell reads nothing from the
79
+ # package while npm replaces it, and exec leaves no old process behind.
80
+ # The version installed is the one the plan showed, never a moving tag, and
81
+ # success is what the new launcher reports, not what npm returned.
82
+ exec "$BASH" -c '
83
+ set -euo pipefail
84
+ pkg="$1" from="$2" to="$3" launcher="$4" prefix="$5" bin="$6"
85
+ npm install -g --prefix "$prefix" "$pkg@$to"
86
+ echo ""
87
+ if [ -f "$launcher" ] && command -v node >/dev/null 2>&1; then
88
+ got="$(node "$launcher" version 2>/dev/null || true)"
89
+ case "$got" in
90
+ "$to"|"$to "*) echo "upgraded: $from -> $to"; echo "$got" ;;
91
+ *)
92
+ echo "ERROR: npm reported success but the installed launcher answers: ${got:-<nothing>}" >&2
93
+ echo " expected $to — inspect the installation at $prefix" >&2
94
+ exit 1
95
+ ;;
96
+ esac
97
+ else
98
+ echo "upgraded: $from -> $to (not verified — run: $bin version)"
99
+ fi
100
+ ' intelligence-upgrade "$pkg" "$installed" "$available" "$launcher" "$prefix" "$bin"
package/cli/intelligence CHANGED
@@ -19,7 +19,7 @@ if [ -f "$CLI_DIR/../engine/sync.sh" ] && [ -f "$CLI_DIR/../engine/VERSION" ]; t
19
19
  fi
20
20
  if [ -z "$IS_ENGINE_DIR" ]; then
21
21
  echo "ERROR: bundled sync engine not found next to the CLI ($CLI_DIR)." >&2
22
- echo " Reinstall: npm i -g @ainova-systems/intelligence" >&2
22
+ echo " Reinstall: npm i -g ${INTELLIGENCE_NPM_PACKAGE:-<the CLI package>}" >&2
23
23
  exit 1
24
24
  fi
25
25
  export IS_ENGINE_DIR
@@ -51,6 +51,9 @@ Usage: intelligence <command> [args]
51
51
  update [@scope/name] Show an update plan; ask before applying it
52
52
  --preview Show the plan without asking or writing
53
53
  --apply Apply the plan without asking
54
+ --latest Move one named package past its range to the newest version
55
+ upgrade [--next] [--preview|--apply] Replace the installed CLI with the newest npm version
56
+ --next Follow the prerelease line instead of the stable one
54
57
  package <command> add | remove | list | search
55
58
  adapter <command> list | create | enable | disable | remove
56
59
  status [--check] Project state; --check runs deep consistency checks
@@ -44,9 +44,9 @@ if [ -z "$stamp" ]; then
44
44
  warn "manifest has no schema_version — run 'intelligence init'"
45
45
  elif _ver_gt "$stamp" "$eng"; then
46
46
  if [ "$(_ver_major "$stamp")" -gt "$(_ver_major "$eng")" ]; then
47
- warn "manifest schema $stamp is a newer major than this CLI's engine $eng — update the CLI: npm i -g @ainova-systems/intelligence@latest"
47
+ warn "manifest schema $stamp is a newer major than this CLI's engine $eng — update the CLI: intelligence upgrade (--next for a prerelease line)"
48
48
  else
49
- note "manifest schema $stamp is newer than this CLI's engine $eng — the project uses a newer CLI; update it: npm i -g @ainova-systems/intelligence@latest"
49
+ note "manifest schema $stamp is newer than this CLI's engine $eng — the project uses a newer CLI; update it: intelligence upgrade (--next for a prerelease line)"
50
50
  fi
51
51
  elif _ver_gt "$eng" "$stamp"; then
52
52
  warn "manifest schema $stamp behind engine $eng — run 'intelligence init'"
@@ -7,16 +7,22 @@
7
7
  set -euo pipefail
8
8
  source "$CLI_DIR/lib/cli-common.sh"
9
9
 
10
- only="" no_sync=0 preview=0
10
+ only="" no_sync=0 preview=0 latest=0
11
11
  while [ $# -gt 0 ]; do
12
12
  case "$1" in
13
13
  --no-sync) no_sync=1 ;;
14
14
  --preview) preview=1 ;;
15
+ --latest) latest=1 ;;
15
16
  @*) only="$1" ;;
16
17
  *) die "unknown argument '$1'" ;;
17
18
  esac
18
19
  shift || true
19
20
  done
21
+ # Crossing a range is per package on purpose: each boundary SemVer marks is a
22
+ # changelog to read, so a blanket sweep would be one confirmation for several
23
+ # unrelated decisions.
24
+ [ "$latest" -eq 0 ] || [ -n "$only" ] \
25
+ || die "--latest needs the package to move: intelligence update @scope/name --latest"
20
26
 
21
27
  require_cli_project
22
28
  manifest="$IP_ROOT/intelligence.yaml"
@@ -36,7 +42,7 @@ move_label() {
36
42
  fi
37
43
  }
38
44
 
39
- moved=0 found=0
45
+ moved=0 found=0 outside=0
40
46
  while IFS= read -r name; do
41
47
  [ -n "$name" ] || continue
42
48
  # Manifest keys are untrusted input on their way into store paths.
@@ -46,6 +52,8 @@ while IFS= read -r name; do
46
52
  # message followed by "not in the manifest" would contradict itself.
47
53
  found=1
48
54
  if [ "$name" = "$SYNC_PKG_NAME" ]; then
55
+ [ "$latest" -eq 0 ] \
56
+ || die "$name is the engine content package — its pin follows the installed CLI; use 'intelligence upgrade'"
49
57
  echo " $name: engine content follows the installed CLI — skipped"
50
58
  continue
51
59
  fi
@@ -59,7 +67,9 @@ while IFS= read -r name; do
59
67
  range="$(qmap_field "$manifest" "packages" "$name" "version")"
60
68
  [ -n "$ref" ] || [ -n "$range" ] || die "$name has neither version nor ref in the manifest"
61
69
 
62
- ref_moved=0 remote_sha=""
70
+ ref_moved=0 remote_sha="" beyond="" range_move=""
71
+ [ "$latest" -eq 0 ] || [ -z "$ref" ] \
72
+ || die "$name is pinned to ref '$ref', not a version range — a ref pin is frozen by intent; re-add the package to change it"
63
73
  if [ -n "$ref" ]; then
64
74
  tag="$ref"
65
75
  requested=""
@@ -101,27 +111,56 @@ while IFS= read -r name; do
101
111
  fi
102
112
  [ -z "$remote_sha" ] || [ "$remote_sha" = "$locked_sha" ] || ref_moved=1
103
113
  else
104
- picked="$(list_remote_versions "$url" | semver_pick_highest "$range")"
114
+ # One remote read answers both questions: what the range selects, and
115
+ # what it excludes.
116
+ versions="$(list_remote_versions "$url")"
117
+ picked="$(printf '%s\n' "$versions" | semver_pick_highest "$range")"
105
118
  [ -n "$picked" ] || { echo " $name: nothing satisfies '$range' at $url" >&2; continue; }
106
119
  read -r tag _ <<< "$(remote_tag_for_version "$url" "$picked")"
107
120
  requested="$range"
121
+ # A range is a ceiling as much as a floor, and on a 0.x package the
122
+ # caret stops at the minor: a project sits on 0.4.x while 0.6.1 ships
123
+ # and every plan still reads "up to date". Name what the range leaves
124
+ # out. Crossing it edits requested intent, which is a manifest change
125
+ # and never this command's to make.
126
+ newest="$(printf '%s\n' "$versions" | semver_pick_highest "latest")"
127
+ if [ -n "$newest" ] && [ "$(semver_cmp "$newest" "$picked")" = "1" ]; then
128
+ if [ "$latest" -eq 1 ]; then
129
+ # Asked for by name: take the newest and widen the recorded
130
+ # intent to match, keeping the caret so the next boundary is
131
+ # still a decision. The manifest keeps saying what the project
132
+ # asked for — that is what makes the move reviewable.
133
+ picked="$newest"
134
+ read -r tag _ <<< "$(remote_tag_for_version "$url" "$picked")"
135
+ requested="^$newest"
136
+ range_move=" (range $range -> $requested)"
137
+ else
138
+ beyond=" — $newest available outside '$range'"
139
+ beyond="$beyond
140
+ follow it: intelligence update $name --latest"
141
+ outside=$((outside + 1))
142
+ fi
143
+ fi
144
+ # When the newest version is already inside the range there is nothing
145
+ # to cross: `--latest` falls through to the ordinary comparison, which
146
+ # still installs a move the lock is behind on and still widens nothing.
108
147
  fi
109
148
  if [ "$ref_moved" -eq 0 ] && [ "$tag" = "$current" ] && [ "$requested" != "$locked_requested" ]; then
110
149
  if [ "$preview" -eq 1 ]; then
111
- echo " $name: request ${locked_requested:-<none>} -> ${requested:-<ref>} (keeps $current)"
150
+ echo " $name: request ${locked_requested:-<none>} -> ${requested:-<ref>} (keeps $current)$beyond"
112
151
  else
113
152
  lock_upsert "$lock" "$name" "$requested" "$url" "$path" "$current" "$locked_sha"
114
- echo " $name: request ${locked_requested:-<none>} -> ${requested:-<ref>} (kept $current)"
153
+ echo " $name: request ${locked_requested:-<none>} -> ${requested:-<ref>} (kept $current)$beyond"
115
154
  fi
116
155
  moved=$((moved + 1))
117
156
  continue
118
157
  fi
119
158
  if [ "$ref_moved" -eq 0 ] && [ "$tag" = "$current" ]; then
120
- echo " $name: $(pin_label "$ref" "$current" "$locked_sha") (up to date)"
159
+ echo " $name: $(pin_label "$ref" "$current" "$locked_sha") (up to date)$beyond"
121
160
  continue
122
161
  fi
123
162
  if [ "$preview" -eq 1 ]; then
124
- echo " $name: $(move_label "$ref" "$current" "$tag" "$locked_sha" "$remote_sha")"
163
+ echo " $name: $(move_label "$ref" "$current" "$tag" "$locked_sha" "$remote_sha")$range_move$beyond"
125
164
  moved=$((moved + 1))
126
165
  continue
127
166
  fi
@@ -139,7 +178,11 @@ while IFS= read -r name; do
139
178
  mv "$staging" "$IP_ROOT/$rel"
140
179
  wire_package_sources "$manifest" "$name" "$rel" "$IP_ROOT"
141
180
  lock_upsert "$lock" "$name" "$requested" "$url" "$path" "$tag" "$sha"
142
- echo " $name: $(move_label "$ref" "$current" "$tag" "$locked_sha" "$sha")"
181
+ # The widened intent is recorded only once its content is installed and
182
+ # wired: a manifest saying ^0.6.1 over a failed fetch would describe a
183
+ # state the project never reached.
184
+ [ -z "$range_move" ] || qmap_set "$manifest" "packages" "$name" "version" "$requested"
185
+ echo " $name: $(move_label "$ref" "$current" "$tag" "$locked_sha" "$sha")$range_move$beyond"
143
186
  moved=$((moved + 1))
144
187
  done < <(qmap_keys "$manifest" "packages")
145
188
 
@@ -149,6 +192,11 @@ if [ "$preview" -eq 1 ]; then
149
192
  else
150
193
  echo "updated: $moved package(s)"
151
194
  fi
195
+ # Counted apart from the movable ones: no mode of this command installs these,
196
+ # so folding them into "updates available" would promise work --apply skips.
197
+ if [ "$outside" -gt 0 ]; then
198
+ echo "outside the requested range: $outside package(s) — read the changelog, then run the 'follow it' command above"
199
+ fi
152
200
 
153
201
  if [ "$preview" -eq 0 ] && [ "$moved" -gt 0 ] && [ "$no_sync" -eq 0 ]; then
154
202
  exec bash "$CLI_DIR/commands/sync.sh"
@@ -80,6 +80,7 @@ pin_label() {
80
80
 
81
81
  source "$CLI_DIR/lib/manifest.sh"
82
82
  source "$CLI_DIR/lib/semver.sh"
83
+ source "$CLI_DIR/lib/cli-install.sh"
83
84
  source "$CLI_DIR/lib/registry.sh"
84
85
  source "$CLI_DIR/lib/lockfile.sh"
85
86
  source "$CLI_DIR/lib/adapter-lifecycle.sh"
@@ -0,0 +1,131 @@
1
+ #!/bin/bash
2
+ # The installed CLI's own npm installation — shared by `upgrade`, which
3
+ # replaces the tree it runs from, and `update`, which plans that step.
4
+ #
5
+ # npm is never asked where its global root is: `npm root -g` answers for the
6
+ # current configuration rather than for the running tree (nvm, a custom
7
+ # prefix), and npm redacts UUID-like path segments from everything it prints.
8
+ # The tree's own location and the shim npm linked for it say where it lives.
9
+
10
+ cli_on_windows() {
11
+ case "${OSTYPE:-}" in msys*|cygwin*) return 0 ;; esac
12
+ return 1
13
+ }
14
+
15
+ # native_path <posix-path> — the spelling a native program (npm, node) needs:
16
+ # Windows form under Git Bash, unchanged elsewhere.
17
+ native_path() {
18
+ if cli_on_windows; then cygpath -w "$1"; else printf '%s' "$1"; fi
19
+ }
20
+
21
+ # quote_for_shell <string> — one shell word for a command printed to be
22
+ # copied. Windows paths carry no `$` or backtick and cmd.exe reads single
23
+ # quotes literally, so they get double quotes; elsewhere single quotes with
24
+ # an embedded quote as '\''. sed rather than ${var//…}: Bash 3.2 (macOS)
25
+ # expands a backslash-quote replacement differently.
26
+ quote_for_shell() {
27
+ if cli_on_windows; then
28
+ printf '"%s"' "$1"
29
+ else
30
+ printf "'%s'" "$(printf '%s' "$1" | sed "s/'/'\\\\''/g")"
31
+ fi
32
+ }
33
+
34
+ # cli_install_classify <pkg_dir> <pkg> <bin> — where the running tree lives.
35
+ # Sets CLI_INSTALL_KIND to npm | npx | volta | pnpm | bun | yarn | nix |
36
+ # dependency | unknown, and CLI_INSTALL_PREFIX (POSIX spelling) for npm only.
37
+ #
38
+ # npm's global layout is `<prefix>/lib/node_modules/<pkg>` on POSIX and
39
+ # `<prefix>/node_modules/<pkg>` on Windows, and a project's own node_modules
40
+ # looks exactly like the latter. The shim npm links at the prefix root is
41
+ # what proves a prefix: `<prefix>/bin/<bin>` pointing into this tree, or
42
+ # `<prefix>/<bin>.cmd` naming it. Known foreign stores are named first
43
+ # because Volta's package images are laid out like an npm prefix without
44
+ # being one.
45
+ cli_install_classify() {
46
+ local pkg_dir="$1" pkg="$2" bin="$3" prefix="" kind="unknown" shim target pkg_bs
47
+ CLI_INSTALL_KIND=""; CLI_INSTALL_PREFIX=""
48
+ case "$pkg_dir" in
49
+ */_npx/*) kind="npx" ;;
50
+ */.volta/*|*/Volta/*) kind="volta" ;;
51
+ */.pnpm/*) kind="pnpm" ;;
52
+ */.bun/install/global/*) kind="bun" ;;
53
+ */yarn/global/*|*/Yarn/Data/global/*) kind="yarn" ;;
54
+ /nix/store/*|/gnu/store/*) kind="nix" ;;
55
+ *)
56
+ if cli_on_windows; then
57
+ case "$pkg_dir" in
58
+ */node_modules/"$pkg")
59
+ prefix="${pkg_dir%/node_modules/"$pkg"}"
60
+ prefix="${prefix:-/}"
61
+ shim="$prefix/$bin.cmd"
62
+ # The cmd shim spells its target with backslashes.
63
+ pkg_bs="$(printf '%s' "$pkg" | tr '/' '\\')"
64
+ if [ ! -f "$shim" ] || ! grep -Fq "node_modules\\$pkg_bs\\bin\\" "$shim"; then
65
+ prefix=""
66
+ fi
67
+ ;;
68
+ esac
69
+ else
70
+ case "$pkg_dir" in
71
+ */lib/node_modules/"$pkg")
72
+ prefix="${pkg_dir%/lib/node_modules/"$pkg"}"
73
+ prefix="${prefix:-/}"
74
+ shim="$prefix/bin/$bin"
75
+ target=""
76
+ if [ -L "$shim" ]; then
77
+ target="$(readlink "$shim")"
78
+ case "$target" in /*) ;; *) target="$prefix/bin/$target" ;; esac
79
+ target="$(cd "$(dirname "$target")" 2>/dev/null && pwd -P || true)"
80
+ fi
81
+ [ "$target" = "$pkg_dir/bin" ] || prefix=""
82
+ ;;
83
+ esac
84
+ fi
85
+ if [ -n "$prefix" ]; then
86
+ kind="npm"
87
+ else
88
+ case "$pkg_dir" in */node_modules/"$pkg") kind="dependency" ;; esac
89
+ fi
90
+ ;;
91
+ esac
92
+ # Read by upgrade and update after the call — per-file shellcheck cannot see that.
93
+ # shellcheck disable=SC2034
94
+ CLI_INSTALL_KIND="$kind"
95
+ # shellcheck disable=SC2034
96
+ CLI_INSTALL_PREFIX="$prefix"
97
+ }
98
+
99
+ # cli_install_hint <kind> <pkg> <version-or-channel> — the command that
100
+ # upgrades a tree npm did not make, for the refusal message.
101
+ cli_install_hint() {
102
+ local kind="$1" spec="$2@$3"
103
+ case "$kind" in
104
+ npx) printf 'nothing is installed globally (this run came through npx) — install it: npm install -g %s' "$spec" ;;
105
+ volta) printf 'volta install %s' "$spec" ;;
106
+ pnpm) printf 'pnpm add -g %s' "$spec" ;;
107
+ bun) printf 'bun add -g %s' "$spec" ;;
108
+ yarn) printf 'yarn global add %s' "$spec" ;;
109
+ nix) printf 'this tree is a Nix/Guix store path — upgrade it with that package manager (%s)' "$spec" ;;
110
+ dependency) printf 'this is a project dependency — move it in package.json: npm install -D %s' "$spec" ;;
111
+ *) printf 'upgrade it with the tool that installed it (ask for %s), or pull it if it is a checkout' "$spec" ;;
112
+ esac
113
+ }
114
+
115
+ # cli_registry_version <pkg> <channel> [native-prefix] — the version the
116
+ # registry serves on that dist-tag, validated; prints nothing and fails when
117
+ # npm does not answer with one. With a prefix the lookup runs in global mode
118
+ # at that prefix — the configuration `npm install -g --prefix` will use, so
119
+ # the plan and the install cannot read different registries. `--no-json`
120
+ # overrides a `json=true` in any npmrc, which would quote the answer.
121
+ cli_registry_version() {
122
+ local pkg="$1" channel="$2" prefix="${3:-}" out
123
+ if [ -n "$prefix" ]; then
124
+ out="$(npm view --global --prefix "$prefix" --no-json "$pkg" "dist-tags.$channel" 2>/dev/null || true)"
125
+ else
126
+ out="$(npm view --no-json "$pkg" "dist-tags.$channel" 2>/dev/null || true)"
127
+ fi
128
+ out="${out//[$' \t\r\n']/}"
129
+ is_npm_version "$out" || return 1
130
+ printf '%s' "$out"
131
+ }
@@ -136,7 +136,7 @@ fetch_package() {
136
136
  if is_bundle_source "$url" "$ref" "$subpath" \
137
137
  && { [ -z "$locked_sha" ] || [ -z "$bundle_sha" ] || [ "$locked_sha" = "$bundle_sha" ]; }; then
138
138
  [ -n "${IS_BUNDLED_PKG_DIR:-}" ] && [ -d "$IS_BUNDLED_PKG_DIR" ] \
139
- || die "bundled engine content not found next to the CLI — reinstall @ainova-systems/intelligence"
139
+ || die "bundled engine content not found next to the CLI — reinstall ${INTELLIGENCE_NPM_PACKAGE:-the CLI package}"
140
140
  rm -rf "$dest"
141
141
  mkdir -p "$dest"
142
142
  cp -R "$IS_BUNDLED_PKG_DIR/." "$dest/"
package/cli/lib/semver.sh CHANGED
@@ -21,6 +21,72 @@ semver_cmp() {
21
21
  }'
22
22
  }
23
23
 
24
+ # semver_cmp_full <a> <b> — prints -1 / 0 / 1 with SemVer prerelease
25
+ # precedence: 1.0.0-rc.1 < 1.0.0-rc.2 < 1.0.0; build metadata is ignored.
26
+ # Package ranges never see prereleases, so semver_cmp stays digits-only; this
27
+ # one is for the CLI's own npm versions, whose `next` channel is a prerelease
28
+ # line.
29
+ semver_cmp_full() {
30
+ awk -v a="${1#v}" -v b="${2#v}" '
31
+ # Numeric identifiers compare as numbers (no leading zeros, so length
32
+ # then digits — exact beyond what a double holds) and sort before
33
+ # alphanumeric ones; those compare as ASCII strings, never as numbers
34
+ # awk might read into them (1e5).
35
+ function idcmp(x, y, xn, yn) {
36
+ xn = (x ~ /^[0-9]+$/); yn = (y ~ /^[0-9]+$/)
37
+ if (xn && yn) {
38
+ if (length(x) != length(y)) return (length(x) < length(y)) ? -1 : 1
39
+ return (x "" < y "") ? -1 : (x "" > y "") ? 1 : 0
40
+ }
41
+ if (xn) return -1
42
+ if (yn) return 1
43
+ return (x "" < y "") ? -1 : (x "" > y "") ? 1 : 0
44
+ }
45
+ BEGIN {
46
+ sub(/\+.*$/, "", a); sub(/\+.*$/, "", b)
47
+ pa = ""; pb = ""
48
+ if (i = index(a, "-")) { pa = substr(a, i + 1); a = substr(a, 1, i - 1) }
49
+ if (i = index(b, "-")) { pb = substr(b, i + 1); b = substr(b, 1, i - 1) }
50
+ na = split(a, A, "."); nb = split(b, B, ".")
51
+ for (i = 1; i <= 3; i++) {
52
+ x = (i <= na) ? A[i] + 0 : 0
53
+ y = (i <= nb) ? B[i] + 0 : 0
54
+ if (x < y) { print -1; exit }
55
+ if (x > y) { print 1; exit }
56
+ }
57
+ # A release outranks every prerelease of the same core.
58
+ if (pa == "" && pb == "") { print 0; exit }
59
+ if (pa == "") { print 1; exit }
60
+ if (pb == "") { print -1; exit }
61
+ na = split(pa, PA, "."); nb = split(pb, PB, ".")
62
+ n = (na < nb) ? na : nb
63
+ for (i = 1; i <= n; i++) {
64
+ c = idcmp(PA[i], PB[i])
65
+ if (c != 0) { print c; exit }
66
+ }
67
+ r = (na < nb) ? -1 : (na > nb) ? 1 : 0
68
+ print r
69
+ }'
70
+ }
71
+
72
+ # npm_channel_for <version> — the npm dist-tag a CLI version came from:
73
+ # `next` for a prerelease, `latest` otherwise. `update` and `upgrade` both
74
+ # derive the channel from the running version; one rule here keeps them equal.
75
+ npm_channel_for() {
76
+ case "$1" in
77
+ *-*) printf 'next' ;;
78
+ *) printf 'latest' ;;
79
+ esac
80
+ }
81
+
82
+ # is_npm_version <string> — 0 iff the string is a version as npm publishes
83
+ # it: `x.y.z`, an optional `-prerelease`, an optional `+build`, nothing else.
84
+ # A registry answer becomes an npm argument, so anything else is refused.
85
+ is_npm_version() {
86
+ local re='^[0-9]+\.[0-9]+\.[0-9]+(-[0-9A-Za-z.-]+)?(\+[0-9A-Za-z.-]+)?$'
87
+ [[ "$1" =~ $re ]]
88
+ }
89
+
24
90
  # semver_is_stable <version> — 0 iff `[v]x.y.z` with nothing else.
25
91
  semver_is_stable() {
26
92
  case "${1#v}" in
package/engine/ENGINE_SHA CHANGED
@@ -1 +1 @@
1
- 9a4af90d8c755b5486a4a67af7c53c1be23f4641
1
+ a9cf448bc3f2944c988ce57581822df081195ca2
package/engine/VERSION CHANGED
@@ -1 +1 @@
1
- 0.12.1
1
+ 0.14.0
@@ -58,11 +58,9 @@ agents_md_append_agents_table() {
58
58
  local rows=""
59
59
  local count=0
60
60
 
61
- local f
62
61
  local -a files=()
63
- while IFS= read -r f; do
64
- [ -n "$f" ] && files+=("$f")
65
- done < <(source_artifact_files "$repo_root" "$config_file" "agents")
62
+ read_source_artifact_files "$repo_root" "$config_file" "agents"
63
+ [ "${#IS_SOURCE_FILES[@]}" -eq 0 ] || files=("${IS_SOURCE_FILES[@]}")
66
64
 
67
65
  if [ "${#files[@]}" -gt 0 ]; then
68
66
  local path tier access desc name
@@ -101,11 +99,10 @@ agents_md_append_skills_table() {
101
99
  local rows=""
102
100
  local count=0
103
101
 
104
- local f dirname
102
+ local dirname
105
103
  local -a skill_files=()
106
- while IFS= read -r f; do
107
- [ -n "$f" ] && skill_files+=("$f")
108
- done < <(source_artifact_files "$repo_root" "$config_file" "skills")
104
+ read_source_artifact_files "$repo_root" "$config_file" "skills"
105
+ [ "${#IS_SOURCE_FILES[@]}" -eq 0 ] || skill_files=("${IS_SOURCE_FILES[@]}")
109
106
 
110
107
  if [ "${#skill_files[@]}" -gt 0 ]; then
111
108
  local path desc
@@ -145,11 +142,9 @@ agents_md_append_rules_list() {
145
142
  local count=0
146
143
  local global_rule_files=()
147
144
 
148
- local f
149
145
  local -a files=()
150
- while IFS= read -r f; do
151
- [ -n "$f" ] && files+=("$f")
152
- done < <(source_artifact_files "$repo_root" "$config_file" "rules")
146
+ read_source_artifact_files "$repo_root" "$config_file" "rules"
147
+ [ "${#IS_SOURCE_FILES[@]}" -eq 0 ] || files=("${IS_SOURCE_FILES[@]}")
153
148
 
154
149
  if [ "${#files[@]}" -gt 0 ]; then
155
150
  local path hp name scope
@@ -643,14 +643,20 @@ sync_open_skill_dirs() {
643
643
 
644
644
  local count=0 log="" d skill_file skill_name
645
645
  local -a skill_dirs=()
646
- while IFS= read -r skill_file; do
647
- [ -n "$skill_file" ] || continue
648
- d="${skill_file%/SKILL.md}"
649
- skill_name="${d##*/}"
650
- skill_dirs+=("$d/")
651
- count=$((count + 1))
652
- log+=" skill: $skill_name"$'\n'
653
- done < <(source_artifact_files "$repo_root" "$config_file" "skills")
646
+ read_source_artifact_files "$repo_root" "$config_file" "skills"
647
+ # Guard on the count, then expand quoted: Bash 3.2 (macOS) errors on an
648
+ # empty array under `set -u`, and quoting keeps a path with a space or a
649
+ # glob character intact.
650
+ if [ "${#IS_SOURCE_FILES[@]}" -gt 0 ]; then
651
+ for skill_file in "${IS_SOURCE_FILES[@]}"; do
652
+ [ -n "$skill_file" ] || continue
653
+ d="${skill_file%/SKILL.md}"
654
+ skill_name="${d##*/}"
655
+ skill_dirs+=("$d/")
656
+ count=$((count + 1))
657
+ log+=" skill: $skill_name"$'\n'
658
+ done
659
+ fi
654
660
  # copy_skill_bundle_dirs owns the frontmatter-quoting pass, so every
655
661
  # target gets it — not just this open-standard dir.
656
662
  if [ "$count" -gt 0 ]; then
@@ -1335,29 +1341,58 @@ warn_unsynced() {
1335
1341
 
1336
1342
  # Enumerate one manifest source kind using the same depth and ordering
1337
1343
  # everywhere. Source-list order is significant; entries inside each directory
1338
- # use byte-order sorting for cross-platform determinism.
1339
- source_artifact_files() {
1344
+ # use byte-order sorting for cross-platform determinism. The result lands in
1345
+ # IS_SOURCE_FILES; callers read that array.
1346
+ #
1347
+ # It is deliberately not streamed out of a subshell. This list decides what
1348
+ # every adapter renders, and a truncated one is indistinguishable from a
1349
+ # smaller project: a short list renders an incomplete file and the run still
1350
+ # reports IS_STATUS=ok. Streaming had two ways to truncate silently — a pipe
1351
+ # write cut short (bash's printf surfaces EINTR as `write error: Interrupted
1352
+ # system call` and drops the line) and a process substitution, which discards
1353
+ # its writer's exit status entirely. Assembling in the caller's shell leaves no
1354
+ # writer whose failure can be lost, and the remaining pipeline is checked. An
1355
+ # enumeration that cannot answer must stop the run, never shorten the answer.
1356
+ read_source_artifact_files() {
1340
1357
  local repo_root="$1"
1341
1358
  local config_file="$2"
1342
1359
  local section="$3"
1343
- local src f dir
1360
+ local src f dir listing
1344
1361
 
1362
+ IS_SOURCE_FILES=()
1345
1363
  load_yaml_list "$config_file" "$section"
1346
1364
  while IFS= read -r src; do
1347
1365
  [ -n "$src" ] || continue
1348
1366
  dir="$repo_root/$src"
1349
1367
  [ -d "$dir" ] || continue
1368
+ # Command substitution, so the pipeline's status is the assignment's:
1369
+ # `find` or `sort` failing aborts here instead of yielding a short list.
1350
1370
  case "$section" in
1351
1371
  rules|agents)
1352
- find "$dir" -maxdepth 1 -type f -name '*.md' -print | LC_ALL=C sort
1372
+ listing="$(find "$dir" -maxdepth 1 -type f -name '*.md' -print | LC_ALL=C sort)" || {
1373
+ echo "ERROR: cannot enumerate $section under '$dir' — refusing to render a partial list." >&2
1374
+ exit 1
1375
+ }
1353
1376
  ;;
1354
1377
  skills)
1355
- while IFS= read -r f; do
1356
- [ -n "$f" ] || continue
1357
- [ -f "$f/SKILL.md" ] && printf '%s\n' "$f/SKILL.md"
1358
- done < <(find "$dir" -mindepth 1 -maxdepth 1 -type d -print | LC_ALL=C sort)
1378
+ listing="$(find "$dir" -mindepth 1 -maxdepth 1 -type d -print | LC_ALL=C sort)" || {
1379
+ echo "ERROR: cannot enumerate $section under '$dir' — refusing to render a partial list." >&2
1380
+ exit 1
1381
+ }
1359
1382
  ;;
1383
+ *) continue ;;
1360
1384
  esac
1385
+ [ -n "$listing" ] || continue
1386
+ # A here-string redirect keeps this loop in the current shell, so the
1387
+ # array it fills survives and any failure inside it is this shell's.
1388
+ while IFS= read -r f; do
1389
+ [ -n "$f" ] || continue
1390
+ if [ "$section" = "skills" ]; then
1391
+ [ -f "$f/SKILL.md" ] || continue
1392
+ f="$f/SKILL.md"
1393
+ fi
1394
+ IS_SOURCE_FILES+=("$f")
1395
+ done <<< "$listing"
1361
1396
  done <<< "$IS_YAML_LIST"
1362
1397
  }
1363
1398
 
@@ -1391,9 +1426,8 @@ report_context_source_sizes() {
1391
1426
  local f path has_paths
1392
1427
  local -a rule_files=() always_on_rules=() scoped_rules=() agent_files=() skill_files=()
1393
1428
 
1394
- while IFS= read -r f; do
1395
- [ -n "$f" ] && rule_files+=("$f")
1396
- done < <(source_artifact_files "$repo_root" "$config_file" "rules")
1429
+ read_source_artifact_files "$repo_root" "$config_file" "rules"
1430
+ [ "${#IS_SOURCE_FILES[@]}" -eq 0 ] || rule_files=("${IS_SOURCE_FILES[@]}")
1397
1431
  if [ "${#rule_files[@]}" -gt 0 ]; then
1398
1432
  while IFS=$'\x1f' read -r path has_paths; do
1399
1433
  [ -n "$path" ] || continue
@@ -1405,13 +1439,11 @@ report_context_source_sizes() {
1405
1439
  done < <(frontmatter_index "paths#" "${rule_files[@]}")
1406
1440
  fi
1407
1441
 
1408
- while IFS= read -r f; do
1409
- [ -n "$f" ] && agent_files+=("$f")
1410
- done < <(source_artifact_files "$repo_root" "$config_file" "agents")
1442
+ read_source_artifact_files "$repo_root" "$config_file" "agents"
1443
+ [ "${#IS_SOURCE_FILES[@]}" -eq 0 ] || agent_files=("${IS_SOURCE_FILES[@]}")
1411
1444
 
1412
- while IFS= read -r f; do
1413
- [ -n "$f" ] && skill_files+=("$f")
1414
- done < <(source_artifact_files "$repo_root" "$config_file" "skills")
1445
+ read_source_artifact_files "$repo_root" "$config_file" "skills"
1446
+ [ "${#IS_SOURCE_FILES[@]}" -eq 0 ] || skill_files=("${IS_SOURCE_FILES[@]}")
1415
1447
 
1416
1448
  local always_bytes custom_bytes agents_output agents_bytes=0 agents_status="disabled"
1417
1449
  always_bytes="$(context_files_bytes "${always_on_rules[@]+"${always_on_rules[@]}"}")"
@@ -126,14 +126,14 @@ check_version_compat() {
126
126
  if [ "$(_ver_major "$stamp")" -gt "$(_ver_major "$eng")" ]; then
127
127
  is_status ahead-of-engine "stamp=$stamp engine=$eng"
128
128
  echo " ERROR: project stamped $stamp but this engine is $eng — a newer major schema; refusing." >&2
129
- echo " Update the CLI first: npm i -g @ainova-systems/intelligence@latest" >&2
129
+ echo " Update the CLI first: intelligence upgrade (--next for a prerelease line)" >&2
130
130
  return "$IS_RC_AHEAD"
131
131
  fi
132
132
  # One warning per command, however many processes re-check on the way to
133
133
  # the engine (CLI preflight first, then the engine itself). The line has
134
134
  # no indent on purpose: `sync --compact` keeps `WARNING:` lines.
135
135
  if [ "${IS_SCHEMA_AHEAD_WARNED:-}" != "$stamp" ]; then
136
- echo "WARNING: project schema $stamp is newer than this CLI's engine $eng — the project uses a newer CLI; update it: npm i -g @ainova-systems/intelligence@latest" >&2
136
+ echo "WARNING: project schema $stamp is newer than this CLI's engine $eng — the project uses a newer CLI; update it: intelligence upgrade (--next for a prerelease line)" >&2
137
137
  export IS_SCHEMA_AHEAD_WARNED="$stamp"
138
138
  fi
139
139
  return 0
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ainova-systems/intelligence",
3
- "version": "0.12.1",
3
+ "version": "0.14.0",
4
4
  "description": "Build, version and distribute AI agent intelligence across your organization — one CLI, versioned Intelligence Packages, and a sync engine for Claude Code, Cursor, Copilot, Codex, Pi and OpenCode.",
5
5
  "bin": {
6
6
  "intelligence": "bin/intelligence.js"
@@ -96,6 +96,29 @@ Registries are an ordered trust list and the only resolver for a package name. T
96
96
 
97
97
  Stable Git tags provide package versions. Semver ranges select the highest matching stable tag; a `ref:` pin names a branch or commit and does not move during `intelligence update`. One package name has one version per project.
98
98
 
99
+ ### Choosing a range
100
+
101
+ `package add` without an explicit range writes `^<the version it installed>`, npm's caret. The caret holds the **leftmost non-zero** component, and that is the whole story:
102
+
103
+ | Requested | Follows automatically | Stops at |
104
+ |---|---|---|
105
+ | `^1.4.0` | every later `1.x` | `2.0.0` |
106
+ | `^0.4.0` | every later `0.4.x` | `0.5.0` |
107
+ | `~1.4.0` | every later `1.4.x` | `1.5.0` |
108
+ | `1.4.0` | nothing | it is a pin |
109
+ | `latest` | every stable tag | nothing |
110
+
111
+ **A pre-1.0 package therefore follows patches only.** SemVer treats a `0.x` minor as a major, so `^0.4.0` is a ceiling at `0.5.0` and the project stays on `0.4.x` while `0.5`, `0.6` and later ship. That is the correct reading of the range, not a defect — but it is the single most common surprise, because the same spelling behaves differently once the package reaches `1.0.0`.
112
+
113
+ Practice:
114
+
115
+ - **Keep the caret.** It is the right default at both stages: automatic patches, and a deliberate decision at every boundary that SemVer says may break.
116
+ - **Read `intelligence update --preview` as two answers.** The move is what the range allows; the `available outside '<range>'` note is what it excludes, and it prints the command that follows it. That note never joins the counter, because no ordinary mode of `update` installs it.
117
+ - **Cross a boundary deliberately, one at a time.** Read the package's changelog for every version crossed, then `intelligence update @scope/name --latest`: it takes the newest stable version and rewrites the requested range to `^<that version>`, so the manifest still records what the project asked for and the next boundary is still a decision. The flag needs the package named — one confirmation must not cover several unrelated changelogs — and it refuses a `ref:` pin, which is frozen by intent. Without it `update` never widens a range, because a command that granted itself permission to install would make the range meaningless.
118
+ - **Review the generated diff, not just the lock.** A package minor may rename or drop an artifact. A renamed skill breaks every `/skill-name` that invoked it, and a renamed artifact silently ends the override relationship a same-named project artifact had, so the project's version stops winning and the package's content appears instead.
119
+ - **`latest` and `*` are for a package you own and release in lockstep.** Anywhere else they hand an upstream author write access to your agents' behavior between two syncs.
120
+ - **Never pin a range to dodge a broken release.** Pin the exact version (`1.4.2`), record why, and remove the pin when the fix ships — a narrowed range hides the reason and outlives the incident.
121
+
99
122
  Commit `intelligence.lock`. It records requested versions, source URLs and paths, resolved refs and commit SHAs. After cloning, `intelligence sync` restores a missing store strictly from that lock before rendering; manifest/lock or SHA drift is refused. Re-run `package add` when deliberately changing a source.
100
123
 
101
124
  `@ainova-systems/sync` is ordinary package content exact-pinned to the bundled engine version. `intelligence init` installs it unless `--bare` is used. Lifecycle preflight keeps that pin and `schema_version` aligned with the installed CLI; package-range updates never move it independently.
@@ -392,6 +415,7 @@ The public lifecycle is deliberately compact:
392
415
  - `intelligence init [--preview|--apply]` is universal: it creates a new setup, aligns an existing Intelligence project, or plans/applies conversion of an eligible legacy Intelligence Sync project.
393
416
  - `intelligence sync [adapter] [--compact]` first aligns an existing Intelligence project with the installed CLI, restores a missing store strictly from `intelligence.lock`, then renders. Compact mode shows context sizes, actionable warnings and final status on success, and all diagnostics on failure. In CI it refuses an alignment that would change tracked files and points to a local `intelligence init --apply` plus review/commit.
394
417
  - `intelligence update [@scope/name] [--preview|--apply]` is the only update surface. It prints the CLI/project/package plan; default mode prompts, `--preview` never writes, and `--apply` does not prompt. It never moves `ref:` pins.
418
+ - `intelligence upgrade [--next] [--preview|--apply]` replaces the installed CLI with the newest version on its npm channel (`next` for a prerelease or with `--next`, otherwise `latest`) with the same modes. It touches no project, never downgrades, and refuses an installation that npm did not make.
395
419
  - `intelligence package add|remove|list|search` owns package inventory.
396
420
  - `intelligence adapter list|create|enable|disable|remove` owns adapter inventory and target state.
397
421
  - `intelligence status [--check]` reports state; `--check` runs deep consistency checks.
@@ -28,10 +28,15 @@ result.
28
28
  mode also shows the plan and prompts; after approval, use `--apply` for an
29
29
  unambiguous non-interactive execution.
30
30
 
31
- 4. If the plan reports a newer global CLI, run exactly the npm command it
32
- prints after approval, then rerun `intelligence update --preview` with the
33
- new executable. Apply the resulting plan with `intelligence update --apply`,
34
- or `intelligence update $ARGUMENTS --apply` when one package was requested.
31
+ 4. If the plan reports a newer global CLI, run `intelligence upgrade --apply`
32
+ after approval — it installs exactly the version the plan showed — then
33
+ rerun `intelligence update --preview` with the new executable. When the
34
+ project section reports a schema stamped ahead of the stable line, the CLI
35
+ that stamped it was a prerelease: use `intelligence upgrade --next --apply`.
36
+ If `upgrade` refuses because npm did not make this installation, run the
37
+ command its message names instead. Apply the resulting plan with
38
+ `intelligence update --apply`, or `intelligence update $ARGUMENTS --apply`
39
+ when one package was requested.
35
40
 
36
41
  5. An applied update that renders must finish with `IS_STATUS=ok`; preserve and
37
42
  stop on any other status. Then run `intelligence status --check`. Do not run