@ainova-systems/intelligence 0.11.5 → 0.11.7

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.
@@ -124,7 +124,9 @@ case "$IP_MODE" in
124
124
  if [ "$preview" -eq 1 ]; then
125
125
  check_version_compat "$IP_ROOT/intelligence.yaml"
126
126
  echo "project: Intelligence CLI at $IP_ROOT"
127
- if project_needs_upgrade "$IP_ROOT"; then
127
+ if project_stamped_ahead "$IP_ROOT"; then
128
+ echo " project schema $(read_schema_version "$IP_ROOT/intelligence.yaml") is ahead of engine $(bundled_engine_version) — left as is (update the CLI)"
129
+ elif project_needs_upgrade "$IP_ROOT"; then
128
130
  echo " would align project lifecycle with engine $(bundled_engine_version)"
129
131
  else
130
132
  echo " schema/content already match engine $(bundled_engine_version)"
@@ -48,7 +48,9 @@ fi
48
48
 
49
49
  echo ""
50
50
  echo "Project:"
51
- if project_needs_upgrade "$IP_ROOT"; then
51
+ if project_stamped_ahead "$IP_ROOT"; then
52
+ echo " schema $stamp is ahead of engine $eng — left as is; update the CLI to align it"
53
+ elif project_needs_upgrade "$IP_ROOT"; then
52
54
  echo " lifecycle alignment required (stamp ${stamp:-unstamped}, engine $eng)"
53
55
  project_change=1
54
56
  else
@@ -24,8 +24,10 @@ legacy_key_count="$(awk '/^sync_version:[[:space:]]*/ { n++ } END { print n + 0
24
24
  eng="$(bundled_engine_version)"
25
25
  [ "$schema_key_count" -le 1 ] || die "manifest has duplicate schema_version keys"
26
26
  [ "$legacy_key_count" -le 1 ] || die "manifest has duplicate sync_version keys"
27
+ # Alignment only ever moves a project UP to this engine. A project stamped
28
+ # ahead is never restamped downward, whatever the gap.
27
29
  if [ -n "$stamp" ] && _ver_gt "$stamp" "$eng"; then
28
- die "manifest schema $stamp is newer than this CLI's engine $eng — update the global CLI first"
30
+ die "manifest schema $stamp is newer than this CLI's engine $eng — refusing to align it downward; update the global CLI first"
29
31
  fi
30
32
  if [ -n "$legacy_stamp" ] && _ver_gt "$legacy_stamp" "$eng"; then
31
33
  die "manifest schema $legacy_stamp is newer than this CLI's engine $eng — update the global CLI first"
@@ -5,6 +5,7 @@ source "$CLI_DIR/lib/cli-common.sh"
5
5
 
6
6
  problems=0
7
7
  warn() { echo " ✗ $*"; problems=$((problems + 1)); }
8
+ note() { echo " ! $*"; }
8
9
  ok() { echo " ✓ $*"; }
9
10
 
10
11
  echo "environment:"
@@ -36,7 +37,11 @@ eng="$(bundled_engine_version)"
36
37
  if [ -z "$stamp" ]; then
37
38
  warn "manifest has no schema_version — run 'intelligence init'"
38
39
  elif _ver_gt "$stamp" "$eng"; then
39
- warn "manifest schema $stamp is newer than this CLI's engine $eng — update the CLI: npm i -g @ainova-systems/intelligence@latest"
40
+ if [ "$(_ver_major "$stamp")" -gt "$(_ver_major "$eng")" ]; then
41
+ warn "manifest schema $stamp is a newer major than this CLI's engine $eng — update the CLI: npm i -g @ainova-systems/intelligence@latest"
42
+ else
43
+ 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"
44
+ fi
40
45
  elif _ver_gt "$eng" "$stamp"; then
41
46
  warn "manifest schema $stamp behind engine $eng — run 'intelligence init'"
42
47
  else
@@ -49,6 +54,8 @@ sync_locked="$(qmap_field "$lock" "packages" "$SYNC_PKG_NAME" "resolved")"
49
54
  if [ -n "$sync_locked" ]; then
50
55
  if [ "${sync_locked#v}" = "$eng" ]; then
51
56
  ok "$SYNC_PKG_NAME at $sync_locked (matches the engine)"
57
+ elif [ "${sync_locked#v}" = "$stamp" ] && _ver_gt "$stamp" "$eng"; then
58
+ note "$SYNC_PKG_NAME at $sync_locked (matches the project schema; the engine is $eng)"
52
59
  else
53
60
  warn "$SYNC_PKG_NAME locked at $sync_locked but the engine is $eng — run 'intelligence init'"
54
61
  fi
@@ -82,7 +89,7 @@ while IFS= read -r name; do
82
89
  warn "$name is locked but not installed — run 'intelligence sync'"
83
90
  continue
84
91
  fi
85
- ok "$name @ $(qmap_field "$lock" "packages" "$name" "resolved")"
92
+ ok "$name @ $(pin_label "$m_ref" "$l_res" "$(qmap_field "$lock" "packages" "$name" "sha")")"
86
93
  fi
87
94
  done < <(qmap_keys "$manifest" "packages")
88
95
 
@@ -14,6 +14,7 @@ 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
+ sha="$(qmap_field "$lock" "packages" "$name" "sha")"
17
18
  if [ ! -f "$lock" ]; then
18
19
  state="lock missing — restore committed intelligence.lock"
19
20
  elif [ -d "$IP_ROOT/.intelligence/packages/$name" ]; then
@@ -24,7 +25,7 @@ while IFS= read -r name; do
24
25
  printf '%s %s locked:%s %s\n' \
25
26
  "$name" \
26
27
  "${ref:+ref:$ref}${ref:-${range:-*}}" \
27
- "${resolved:-<none>}" \
28
+ "$(pin_label "$ref" "$resolved" "$sha")" \
28
29
  "$state"
29
30
  done < <(qmap_keys "$manifest" "packages")
30
31
 
@@ -22,6 +22,20 @@ require_cli_project
22
22
  manifest="$IP_ROOT/intelligence.yaml"
23
23
  lock="$IP_ROOT/intelligence.lock"
24
24
 
25
+ # move_label <ref-or-empty> <from-resolved> <to-resolved> <from-sha> <to-sha>
26
+ # A version pin moves between tags, a ref pin between commits under one name —
27
+ # printing "main -> main" would report the move by hiding what moved.
28
+ move_label() {
29
+ local ref="$1" from="$2" to="$3" from_sha="$4" to_sha="$5"
30
+ if [ -z "$ref" ]; then
31
+ printf '%s -> %s' "${from:-<none>}" "$to"
32
+ elif [ -n "$from" ] && [ "$from" != "$ref" ]; then
33
+ printf '%s -> %s@%s' "$from" "$ref" "$(short_sha "$to_sha")"
34
+ else
35
+ printf '%s %s -> %s' "$ref" "$(short_sha "$from_sha")" "$(short_sha "$to_sha")"
36
+ fi
37
+ }
38
+
25
39
  moved=0 found=0
26
40
  while IFS= read -r name; do
27
41
  [ -n "$name" ] || continue
@@ -37,6 +51,7 @@ while IFS= read -r name; do
37
51
  fi
38
52
  current="$(qmap_field "$lock" "packages" "$name" "resolved")"
39
53
  locked_requested="$(qmap_field "$lock" "packages" "$name" "requested")"
54
+ locked_sha="$(qmap_field "$lock" "packages" "$name" "sha")"
40
55
  url="$(qmap_field "$lock" "packages" "$name" "url")"
41
56
  path="$(qmap_field "$lock" "packages" "$name" "path")"
42
57
  [ -n "$url" ] || die "$name has no source in intelligence.lock — restore the committed lock or re-add the package"
@@ -44,32 +59,69 @@ while IFS= read -r name; do
44
59
  range="$(qmap_field "$manifest" "packages" "$name" "version")"
45
60
  [ -n "$ref" ] || [ -n "$range" ] || die "$name has neither version nor ref in the manifest"
46
61
 
62
+ ref_moved=0 remote_sha=""
47
63
  if [ -n "$ref" ]; then
48
64
  tag="$ref"
49
65
  requested=""
66
+ # A ref is requested INTENT, and the lock's `resolved` column holds
67
+ # that same ref name — comparing the two asks a question no branch can
68
+ # ever answer differently. The sha column is the only record of where
69
+ # the ref actually pointed, so a ref pin is compared commit to commit:
70
+ # a moved branch, a re-cut tag and a `HEAD` pin all become visible,
71
+ # while a pin that IS a commit stays immutable by construction.
72
+ probe_rc=0
73
+ remote_sha="$(remote_sha_for_ref "$url" "$ref")" || probe_rc=$?
74
+ if [ "$probe_rc" -ne 0 ]; then
75
+ # No answer is not "no change": reporting up to date here would
76
+ # freeze the pin exactly as the ref-name comparison used to.
77
+ echo " WARN: $name: cannot reach $url — ref '$ref' was not checked" >&2
78
+ echo " $name: $ref (not checked — remote unreachable)"
79
+ continue
80
+ fi
81
+ # ls-remote advertises refs, not arbitrary objects, so a commit pin
82
+ # legitimately matches nothing — and cannot move. This verdict applies
83
+ # only once the lock already resolved to this ref; a manifest that now
84
+ # names a ref the lock never resolved is an ordinary move, and falls
85
+ # through to the fetch below.
86
+ if [ -z "$remote_sha" ] && [ "$ref" = "$current" ]; then
87
+ # The lock records the commit this ref resolved to, so a ref that
88
+ # prefixes it IS that commit. Asking the lock beats guessing from
89
+ # the ref's shape: a deleted branch named like a hex string would
90
+ # otherwise pass for a pin and swallow its own disappearance.
91
+ case "$locked_sha" in
92
+ "$ref"*)
93
+ echo " $name: $(short_sha "$ref") (pinned commit)"
94
+ ;;
95
+ *)
96
+ echo " WARN: $name pins ref '$ref', which $url no longer advertises" >&2
97
+ echo " $name: $ref (unresolvable — gone upstream)"
98
+ ;;
99
+ esac
100
+ continue
101
+ fi
102
+ [ -z "$remote_sha" ] || [ "$remote_sha" = "$locked_sha" ] || ref_moved=1
50
103
  else
51
104
  picked="$(list_remote_versions "$url" | semver_pick_highest "$range")"
52
105
  [ -n "$picked" ] || { echo " $name: nothing satisfies '$range' at $url" >&2; continue; }
53
106
  read -r tag _ <<< "$(remote_tag_for_version "$url" "$picked")"
54
107
  requested="$range"
55
108
  fi
56
- if [ "$tag" = "$current" ] && [ "$requested" != "$locked_requested" ]; then
109
+ if [ "$ref_moved" -eq 0 ] && [ "$tag" = "$current" ] && [ "$requested" != "$locked_requested" ]; then
57
110
  if [ "$preview" -eq 1 ]; then
58
111
  echo " $name: request ${locked_requested:-<none>} -> ${requested:-<ref>} (keeps $current)"
59
112
  else
60
- locked_sha="$(qmap_field "$lock" "packages" "$name" "sha")"
61
113
  lock_upsert "$lock" "$name" "$requested" "$url" "$path" "$current" "$locked_sha"
62
114
  echo " $name: request ${locked_requested:-<none>} -> ${requested:-<ref>} (kept $current)"
63
115
  fi
64
116
  moved=$((moved + 1))
65
117
  continue
66
118
  fi
67
- if [ "$tag" = "$current" ]; then
68
- echo " $name: ${current:-<none>} (up to date)"
119
+ if [ "$ref_moved" -eq 0 ] && [ "$tag" = "$current" ]; then
120
+ echo " $name: $(pin_label "$ref" "$current" "$locked_sha") (up to date)"
69
121
  continue
70
122
  fi
71
123
  if [ "$preview" -eq 1 ]; then
72
- echo " $name: ${current:-<none>} -> $tag"
124
+ echo " $name: $(move_label "$ref" "$current" "$tag" "$locked_sha" "$remote_sha")"
73
125
  moved=$((moved + 1))
74
126
  continue
75
127
  fi
@@ -87,7 +139,7 @@ while IFS= read -r name; do
87
139
  mv "$staging" "$IP_ROOT/$rel"
88
140
  wire_package_sources "$manifest" "$name" "$rel" "$IP_ROOT"
89
141
  lock_upsert "$lock" "$name" "$requested" "$url" "$path" "$tag" "$sha"
90
- echo " $name: ${current:-<none>} -> $tag"
142
+ echo " $name: $(move_label "$ref" "$current" "$tag" "$locked_sha" "$sha")"
91
143
  moved=$((moved + 1))
92
144
  done < <(qmap_keys "$manifest" "packages")
93
145
 
@@ -47,6 +47,27 @@ assert_safe_ref() {
47
47
  esac
48
48
  }
49
49
 
50
+ # --- Resolved-pin display ------------------------------------------------
51
+ # A `ref:` pin's resolved column holds the ref NAME, which never changes: the
52
+ # sha is the only field that says which commit is installed. Every surface
53
+ # reporting such a pin prints both, so a branch that stopped moving is visible
54
+ # without asking the remote.
55
+
56
+ short_sha() {
57
+ [ -n "$1" ] || { printf '<none>'; return 0; }
58
+ printf '%s' "${1:0:7}"
59
+ }
60
+
61
+ # pin_label <ref-or-empty> <resolved> <sha>
62
+ pin_label() {
63
+ local ref="$1" resolved="$2" sha="$3"
64
+ if [ -n "$ref" ]; then
65
+ printf '%s@%s' "${resolved:-$ref}" "$(short_sha "$sha")"
66
+ else
67
+ printf '%s' "${resolved:-<none>}"
68
+ fi
69
+ }
70
+
50
71
  source "$CLI_DIR/lib/manifest.sh"
51
72
  source "$CLI_DIR/lib/semver.sh"
52
73
  source "$CLI_DIR/lib/registry.sh"
@@ -218,12 +239,26 @@ assert_safe_content_dir() {
218
239
  done
219
240
  }
220
241
 
242
+ # project_stamped_ahead <root> — the manifest is stamped newer than this CLI's
243
+ # engine, at any SemVer level. Every caller runs check_version_compat first,
244
+ # which has already refused a newer major, so what reaches this predicate is
245
+ # the admitted minor/patch gap; the predicate itself does not re-judge the gap.
246
+ project_stamped_ahead() {
247
+ local stamp
248
+ stamp="$(read_schema_version "$1/intelligence.yaml")"
249
+ [ -n "$stamp" ] && _ver_gt "$stamp" "$(bundled_engine_version)"
250
+ }
251
+
221
252
  project_needs_upgrade() {
222
253
  local root="$1" manifest="$1/intelligence.yaml" stamp eng pinned locked name
223
254
  [ -f "$manifest" ] || return 1
224
255
  stamp="$(read_schema_version "$manifest")"
225
256
  eng="$(bundled_engine_version)"
226
257
  [ -z "$stamp" ] && return 0
258
+ # A project stamped ahead belongs to a newer CLI. Aligning it here would
259
+ # restamp the schema and re-pin the engine content DOWNWARD, and the next
260
+ # teammate on the current CLI would move both back: leave it as found.
261
+ project_stamped_ahead "$root" && return 1
227
262
  [ -n "$(top_scalar "$manifest" "sync_version")" ] && return 0
228
263
  _ver_gt "$eng" "$stamp" && return 0
229
264
  [ -d "$root/.intelligence/engine" ] && return 0
package/cli/lib/semver.sh CHANGED
@@ -120,6 +120,39 @@ remote_tag_for_version() {
120
120
  }'
121
121
  }
122
122
 
123
+ # remote_sha_for_ref <git-url> <ref> — the commit a branch/tag/HEAD pin points
124
+ # at on the remote right now; empty when the remote advertises no such ref
125
+ # (a pin that IS a commit, or a ref deleted upstream). Exit 2 means the remote
126
+ # could not be asked at all: a check that produced no answer must never be
127
+ # read by the caller as "nothing changed".
128
+ #
129
+ # Three git behaviours this mirrors deliberately, because a sha that is not the
130
+ # one `fetch_package` would land on is worse than no answer:
131
+ # - `git ls-remote <url> main` also matches `refs/heads/feature/main`
132
+ # (patterns match at slash boundaries), so candidates are selected by full
133
+ # ref name, never by the pattern that fetched them;
134
+ # - an annotated tag advertises the tag OBJECT on `refs/tags/<t>` and the
135
+ # commit on the peeled `refs/tags/<t>^{}`, while the lock records the
136
+ # commit — so peeled wins, and the peel line only appears when its own
137
+ # pattern is asked for;
138
+ # - on a name shared by a branch and a tag, `git clone --branch <ref>` takes
139
+ # the branch, so `refs/heads` outranks `refs/tags` here too.
140
+ remote_sha_for_ref() {
141
+ local url="$1" ref="$2" out
142
+ assert_safe_source_url "$url"
143
+ assert_safe_ref "$ref"
144
+ [ -n "$ref" ] || return 0
145
+ out="$(GIT_TERMINAL_PROMPT=0 git ls-remote -- "$url" \
146
+ "$ref" "$ref^{}" "refs/heads/$ref" "refs/tags/$ref" "refs/tags/$ref^{}" 2>/dev/null)" \
147
+ || return 2
148
+ printf '%s\n' "$out" | awk -v ref="$ref" '
149
+ { sub(/\r$/, ""); if ($2 != "") seen[$2] = $1 }
150
+ END {
151
+ split("refs/heads/" ref "\037refs/tags/" ref "^{}\037refs/tags/" ref "\037" ref "^{}\037" ref, C, "\037")
152
+ for (i = 1; i <= 5; i++) if (C[i] in seen) { print seen[C[i]]; exit }
153
+ }'
154
+ }
155
+
123
156
  # semver_pick_highest <range> — reads versions on stdin, prints the highest
124
157
  # one matching the range (empty output when nothing matches).
125
158
  semver_pick_highest() {
package/engine/ENGINE_SHA CHANGED
@@ -1 +1 @@
1
- bd688bcdb5443d808a72ed4e63805a65255a7d02
1
+ 00308bfbdcd92ebf37924bb1083cee142e655fd7
package/engine/VERSION CHANGED
@@ -1 +1 @@
1
- 0.11.5
1
+ 0.11.7
@@ -100,21 +100,41 @@ _ver_gt() {
100
100
  return 1
101
101
  }
102
102
 
103
- # check_version_compat <config_file> — refuse to operate on a project whose
104
- # config schema is stamped newer than this engine knows (a stale engine must
105
- # never rewrite/sync a newer schema). Emits status + returns IS_RC_AHEAD on
106
- # conflict, else 0.
103
+ # _ver_major V → the leading numeric field of a version ("0.11.6" → 0).
104
+ _ver_major() {
105
+ local m="${1%%.*}"
106
+ m="${m//[!0-9]/}"
107
+ printf '%s' "$((10#${m:-0}))"
108
+ }
109
+
110
+ # check_version_compat <config_file> — a stale engine on a project stamped
111
+ # NEWER than itself. The gap decides:
112
+ # * a newer MAJOR is refused: the manifest may carry shapes this engine
113
+ # cannot read, and rendering or rewriting it would destroy them. Emits
114
+ # status + returns IS_RC_AHEAD;
115
+ # * a newer minor or patch within the same major warns once and returns 0:
116
+ # the schema is additive there, so an older engine renders what it
117
+ # understands. It never restamps the project — the CLI's preflight leaves
118
+ # a project stamped ahead exactly as it found it (project_needs_upgrade).
107
119
  check_version_compat() {
108
120
  local cf="$1" stamp eng
109
121
  stamp="$(read_schema_version "$cf")"
110
122
  [ -n "$stamp" ] || return 0
111
123
  eng="$(engine_version)"
112
124
  [ -n "$eng" ] || return 0
113
- if _ver_gt "$stamp" "$eng"; then
125
+ _ver_gt "$stamp" "$eng" || return 0
126
+ if [ "$(_ver_major "$stamp")" -gt "$(_ver_major "$eng")" ]; then
114
127
  is_status ahead-of-engine "stamp=$stamp engine=$eng"
115
- echo " ERROR: project stamped $stamp but this engine is $eng — refusing." >&2
128
+ echo " ERROR: project stamped $stamp but this engine is $eng — a newer major schema; refusing." >&2
116
129
  echo " Update the CLI first: npm i -g @ainova-systems/intelligence@latest" >&2
117
130
  return "$IS_RC_AHEAD"
118
131
  fi
132
+ # One warning per command, however many processes re-check on the way to
133
+ # the engine (CLI preflight first, then the engine itself). The line has
134
+ # no indent on purpose: `sync --compact` keeps `WARNING:` lines.
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
137
+ export IS_SCHEMA_AHEAD_WARNED="$stamp"
138
+ fi
119
139
  return 0
120
140
  }
package/engine/sync.sh CHANGED
@@ -31,7 +31,8 @@ fi
31
31
  # Schema version lives in the manifest (the frozen contract key).
32
32
  _cf="$CONFIG_FILE"
33
33
 
34
- # Stale engine vs project schema stamped NEWER (ahead-of-engine) → refuse.
34
+ # Stale engine vs project schema stamped NEWER (ahead-of-engine): a newer
35
+ # major refuses, a newer minor/patch warns and renders (see contract.sh).
35
36
  _vc_rc=0
36
37
  check_version_compat "$_cf" || _vc_rc=$?
37
38
  if [ "$_vc_rc" -ne 0 ]; then exit "$_vc_rc"; fi
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ainova-systems/intelligence",
3
- "version": "0.11.5",
3
+ "version": "0.11.7",
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"
@@ -391,7 +391,7 @@ The public lifecycle is deliberately compact:
391
391
  - `intelligence adapter list|create|enable|disable|remove` owns adapter inventory and target state.
392
392
  - `intelligence status [--check]` reports state; `--check` runs deep consistency checks.
393
393
 
394
- Implement Intelligence schema changes as idempotent structural checks. Stage and verify replacement state before deleting or replacing prior state. A stale engine refuses a manifest whose `schema_version` is newer; normal project entry points close a behind-project gap through lifecycle preflight.
394
+ Implement Intelligence schema changes as idempotent structural checks. Stage and verify replacement state before deleting or replacing prior state. A stale engine refuses a manifest whose `schema_version` is a newer major; a newer minor or patch within the same major warns once and proceeds without restamping the project. Normal project entry points close a behind-project gap through lifecycle preflight.
395
395
 
396
396
  Breaking changelog entries use a `### Breaking` checklist of verifiable post-conditions. The update skill reads every release across the version gap, chooses the package/CLI/project command sequence and verifies those conditions after the deterministic command completes.
397
397
 
@@ -406,7 +406,7 @@ The engine emits one machine-readable line, `IS_STATUS=<code> [IS_DETAIL=...]`,
406
406
  | `error` | 1 | Generic failure |
407
407
  | `config-missing` | 2 | Required manifest is absent |
408
408
  | `ambiguous` | 3 | Conflicting state requiring agent/human judgment; reserved |
409
- | `ahead-of-engine` | 4 | Manifest schema is newer than the engine |
409
+ | `ahead-of-engine` | 4 | Manifest schema is a newer major than the engine (a newer minor or patch warns and continues) |
410
410
  | `aborted-incomplete` | 5 | Staged replacement was incomplete; prior state remains |
411
411
  | `needs-update` | 6 | Project schema is behind the engine; rerun through a public lifecycle command |
412
412