@ainova-systems/intelligence 0.11.0-rc.5 → 0.11.0-rc.6

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.
@@ -78,23 +78,24 @@ assert_valid_pkg_name "$name"
78
78
  if [ "$mode" = "registry" ]; then
79
79
  resolve_package_source "$manifest" "$name"
80
80
  url="$RES_URL"; path="$RES_PATH"
81
+ if [ -z "$url" ]; then
82
+ echo "ERROR: no trusted registry declares '$name'." >&2
83
+ echo " A name installs only through a registry this project added explicitly" >&2
84
+ echo " ('intelligence registry list' shows them). Either add its registry:" >&2
85
+ echo " intelligence registry add <registry-repo-url>" >&2
86
+ echo " or install from an explicit source you name yourself:" >&2
87
+ echo " intelligence add github:org/repo | intelligence add git+<url>[@ref][#path]" >&2
88
+ suggest_similar "$manifest" "$name"
89
+ exit 1
90
+ fi
81
91
  echo " resolving $name via $RES_VIA -> $url${path:+ ($path)}"
82
92
  fi
83
93
 
84
- # The source must exist before anything else is attempted — a name that fell
85
- # through to the convention is often just a typo, and that must say so, not
86
- # die mid-pipeline with no message.
94
+ # The source must exist before anything else is attempted — never die
95
+ # mid-pipeline with no message.
87
96
  if ! GIT_TERMINAL_PROMPT=0 git ls-remote "$url" >/dev/null 2>&1; then
88
97
  echo "ERROR: no reachable repository at $url" >&2
89
- if [ "${RES_VIA:-}" = "convention" ]; then
90
- echo " No registry declares '$name', so it resolved by convention to github.com/${name#@}." >&2
91
- echo " Either the name is misspelled ('intelligence search' lists what registries offer)," >&2
92
- echo " the repo is private (check your git credentials), or its registry is not added yet:" >&2
93
- echo " intelligence registry add <registry-repo-url>" >&2
94
- suggest_similar "$manifest" "$name"
95
- else
96
- echo " Declared by ${RES_VIA:-the manifest} — check the URL and your git access." >&2
97
- fi
98
+ echo " Declared by ${RES_VIA:-the spec} — check the URL and your git access." >&2
98
99
  exit 1
99
100
  fi
100
101
 
@@ -124,7 +125,11 @@ sha="$(fetch_package "$url" "${ref:-$resolved_tag}" "$path" "$IP_ROOT/$rel")"
124
125
  wire_package_sources "$manifest" "$name" "$rel" "$IP_ROOT"
125
126
 
126
127
  # Manifest entry: a registry package records its range; a direct one records
127
- # its source so resolution never has to guess it again.
128
+ # its source so resolution never has to guess it again. The entry is replaced
129
+ # WHOLE — re-adding under a different spec must not leave the old spec's
130
+ # fields behind (a stale ref: would freeze update, a stale url: would pin the
131
+ # old source).
132
+ qmap_delete_key "$manifest" "packages" "$name"
128
133
  if [ "$mode" = "registry" ]; then
129
134
  qmap_set "$manifest" "packages" "$name" "version" "$requested"
130
135
  else
@@ -33,15 +33,24 @@ esac
33
33
  root="$(git rev-parse --show-toplevel 2>/dev/null || pwd)"
34
34
  root="$(cd "$root" && pwd)"
35
35
 
36
- targets="agents claude"
36
+ # Never invent a tool the project shows no trace of: every per-tool target is
37
+ # DETECTED from repo markers or named explicitly via --targets. Only `agents`
38
+ # (AGENTS.md, the tool-neutral standard every AGENTS-reading tool shares, and
39
+ # the invariant-required carrier once any of them is on) is unconditional.
40
+ targets="agents"
37
41
  if [ -n "$targets_arg" ]; then
38
42
  targets="agents $(printf '%s' "$targets_arg" | tr ',' ' ')"
39
43
  else
44
+ if [ -d "$root/.claude" ] || [ -f "$root/CLAUDE.md" ]; then targets="$targets claude"; fi
40
45
  [ -d "$root/.cursor" ] && targets="$targets cursor"
41
46
  [ -d "$root/.codex" ] && targets="$targets codex"
42
47
  [ -d "$root/.pi" ] && targets="$targets pi"
43
48
  [ -d "$root/.opencode" ] && targets="$targets opencode"
44
49
  [ -d "$root/.github" ] && [ -d "$root/.github/instructions" ] && targets="$targets copilot"
50
+ if [ "$targets" = "agents" ]; then
51
+ echo " NOTE: no tool markers found (.claude/, CLAUDE.md, .cursor/, …) — only AGENTS.md is enabled." >&2
52
+ echo " Name your tools explicitly: intelligence init --targets claude,cursor (or edit targets: later)" >&2
53
+ fi
45
54
  fi
46
55
 
47
56
  manifest="$root/intelligence.yaml"
@@ -88,14 +97,20 @@ manifest="$root/intelligence.yaml"
88
97
  echo "# \"@ainova-systems/core\":"
89
98
  echo "# version: \"^0.3.0\""
90
99
  echo ""
91
- echo "# Registries are a TRUST LIST: git repos holding an index.yaml, consulted"
92
- echo "# in order — the first to declare a name wins, then the CLI's bundled index,"
93
- echo "# then the @org/name -> github.com/org/name convention. Adding one is a"
94
- echo "# committed, reviewable act of trust in the names it declares; sources are"
95
- echo "# pinned by url+sha in intelligence.lock, and overriding a bundled name"
96
- echo "# warns loudly. Managed with 'intelligence registry add|remove'."
97
- echo "# registries:"
98
- echo "# - \"https://github.com/acme/intelligence-registry.git\""
100
+ echo "# Registries are this project's TRUST LIST — the ONLY way a package name"
101
+ echo "# resolves (there is no built-in catalog and no name->github guessing;"
102
+ echo "# registry-less installs are always explicit: github:org/repo, git+<url>)."
103
+ echo "# Git repos holding an index.yaml, consulted in order; first to declare a"
104
+ echo "# name wins; sources are then pinned by url+sha in intelligence.lock."
105
+ echo "# Managed with 'intelligence registry add|remove'. Delete a line to"
106
+ echo "# distrust that catalog — including the default seeded below."
107
+ if [ "$bare" -eq 1 ] || [ -z "$DEFAULT_REGISTRY_URL" ]; then
108
+ echo "# registries:"
109
+ echo "# - \"https://github.com/acme/intelligence-registry.git\""
110
+ else
111
+ echo "registries:"
112
+ echo " - \"$DEFAULT_REGISTRY_URL\""
113
+ fi
99
114
  } > "$manifest"
100
115
 
101
116
  # The package store is restorable state, never committed — the npm model.
@@ -22,8 +22,44 @@ require_v2
22
22
  manifest="$IP_ROOT/intelligence.yaml"
23
23
  lock="$IP_ROOT/intelligence.lock"
24
24
 
25
+ # --frozen is the reproducibility contract: the lock must agree with the
26
+ # manifest on the package SET and on every requested-source field before a
27
+ # single byte is restored. Anything less lets a manifest edit ride on a stale
28
+ # lock — the exact drift the flag exists to refuse.
29
+ if [ "$frozen" -eq 1 ]; then
30
+ while IFS= read -r name; do
31
+ [ -n "$name" ] || continue
32
+ [ -n "$(qmap_field "$lock" "packages" "$name" "url")" ] \
33
+ || die "--frozen: $name is in the manifest but not in intelligence.lock"
34
+ m_ver="$(qmap_field "$manifest" "packages" "$name" "version")"
35
+ m_ref="$(qmap_field "$manifest" "packages" "$name" "ref")"
36
+ m_url="$(qmap_field "$manifest" "packages" "$name" "url")"
37
+ m_path="$(qmap_field "$manifest" "packages" "$name" "path")"
38
+ l_req="$(qmap_field "$lock" "packages" "$name" "requested")"
39
+ l_res="$(qmap_field "$lock" "packages" "$name" "resolved")"
40
+ l_url="$(qmap_field "$lock" "packages" "$name" "url")"
41
+ l_path="$(qmap_field "$lock" "packages" "$name" "path")"
42
+ [ -z "$m_ver" ] || [ "$m_ver" = "$l_req" ] \
43
+ || die "--frozen: $name requests '$m_ver' but the lock recorded '$l_req' — run 'intelligence install' without --frozen"
44
+ [ -z "$m_ref" ] || [ "$m_ref" = "$l_res" ] \
45
+ || die "--frozen: $name pins ref '$m_ref' but the lock resolved '$l_res'"
46
+ [ -z "$m_url" ] || [ "$m_url" = "$l_url" ] \
47
+ || die "--frozen: $name declares url '$m_url' but the lock recorded '$l_url'"
48
+ [ -z "$m_path" ] || [ "$m_path" = "$l_path" ] \
49
+ || die "--frozen: $name declares path '$m_path' but the lock recorded '$l_path'"
50
+ done < <(qmap_keys "$manifest" "packages")
51
+ while IFS= read -r name; do
52
+ [ -n "$name" ] || continue
53
+ found=0
54
+ while IFS= read -r m; do
55
+ [ "$m" = "$name" ] && found=1
56
+ done < <(qmap_keys "$manifest" "packages")
57
+ [ "$found" -eq 1 ] || die "--frozen: $name is locked but absent from the manifest"
58
+ done < <(qmap_keys "$lock" "packages")
59
+ fi
60
+
25
61
  # Manifest packages missing from the lock: resolve them now (npm install
26
- # semantics). Under --frozen that is an error instead.
62
+ # semantics). Under --frozen the drift gate above has already refused.
27
63
  missing=""
28
64
  while IFS= read -r name; do
29
65
  [ -n "$name" ] || continue
@@ -32,7 +68,6 @@ while IFS= read -r name; do
32
68
  fi
33
69
  done < <(qmap_keys "$manifest" "packages")
34
70
  if [ -n "$missing" ]; then
35
- [ "$frozen" -eq 1 ] && die "lockfile is missing packages:$missing — run 'intelligence install' without --frozen"
36
71
  for name in $missing; do
37
72
  # The engine-content package needs no remote resolution: its pin IS
38
73
  # the bundled engine version and it seeds from the bundle, offline.
@@ -70,16 +105,25 @@ if [ -f "$lock" ]; then
70
105
  if [ -d "$IP_ROOT/$rel" ] && [ "$force" -eq 0 ]; then
71
106
  continue
72
107
  fi
73
- got="$(fetch_package "$url" "$resolved" "$path" "$IP_ROOT/$rel")"
108
+ # Fetch into staging and commit to the store only after the integrity
109
+ # verdict: content that violates the lock must never be left where a
110
+ # re-run (whose dir-exists check skips verification) would sync it.
111
+ staging="$IP_ROOT/.intelligence/.staging-$$"
112
+ rm -rf "$staging"
113
+ got="$(fetch_package "$url" "$resolved" "$path" "$staging")"
74
114
  # An empty sha (bundle-seeded content on a dev build) means unknown,
75
115
  # not moved — only two known-and-different shas are a finding.
76
116
  if [ -n "$sha" ] && [ -n "$got" ] && [ "$got" != "$sha" ]; then
77
117
  if [ "$frozen" -eq 1 ]; then
118
+ rm -rf "$staging"
78
119
  die "$name: fetched $got but the lock pins $sha — the ref moved; refusing under --frozen"
79
120
  fi
80
121
  echo " WARN: $name resolved to $got, lock had $sha — updating the lock" >&2
81
122
  lock_upsert "$lock" "$name" "$requested" "$url" "$path" "$resolved" "$got"
82
123
  fi
124
+ rm -rf "${IP_ROOT:?}/$rel"
125
+ mkdir -p "$(dirname "$IP_ROOT/$rel")"
126
+ mv "$staging" "$IP_ROOT/$rel"
83
127
  wire_package_sources "$manifest" "$name" "$rel" "$IP_ROOT"
84
128
  echo "= $name@${resolved:-HEAD}"
85
129
  done < <(lock_to_tsv "$lock")
@@ -44,13 +44,6 @@ stamp="$(read_engine_stamp "$config")"
44
44
  eng="$(bundled_engine_version)"
45
45
  [ -n "$stamp" ] && _ver_gt "$stamp" "$eng" && die "project schema $stamp is newer than this CLI's engine $eng — update the CLI first"
46
46
 
47
- # A schema gap is closed by the engine's own proven chain before conversion.
48
- if [ "$dry_run" -eq 0 ] && { [ -z "$stamp" ] || _ver_gt "$eng" "$stamp"; }; then
49
- echo "== applying the engine migration chain ($stamp -> $eng) =="
50
- run_migrations "$umbrella" "$(basename "$module_dir")" "" || die "engine migration chain failed (IS_STATUS above) — nothing converted"
51
- stamp_version "$config" "$eng"
52
- fi
53
-
54
47
  # ---- Stage ----------------------------------------------------------------
55
48
  stage="$root/.intelligence-migrate-stage.$$"
56
49
  [ "$dry_run" -eq 1 ] && stage="$(mktemp -d -t intelligence-migrate-XXXXXX 2>/dev/null || mktemp -d)"
@@ -58,6 +51,24 @@ cleanup_stage() { rm -rf "$stage"; }
58
51
  trap cleanup_stage EXIT INT TERM
59
52
  mkdir -p "$stage/.intelligence/packages"
60
53
 
54
+ # A schema gap is closed by the engine's own proven chain before conversion.
55
+ # Dry-run must preview the SAME pipeline, so it runs the chain too — against
56
+ # a staged copy of the umbrella, leaving the project untouched.
57
+ if [ -z "$stamp" ] || _ver_gt "$eng" "$stamp"; then
58
+ if [ "$dry_run" -eq 1 ]; then
59
+ echo "== applying the engine migration chain ($stamp -> $eng) to a staged copy =="
60
+ cp -R "$umbrella" "$stage/umbrella"
61
+ run_migrations "$stage/umbrella" "$(basename "$module_dir")" "" > "$stage/chain.log" 2>&1 \
62
+ || { tail -5 "$stage/chain.log" >&2; die "engine migration chain failed in the staged copy — a real migrate would fail the same way"; }
63
+ stamp_version "$stage/umbrella/config.yaml" "$eng"
64
+ config="$stage/umbrella/config.yaml"
65
+ else
66
+ echo "== applying the engine migration chain ($stamp -> $eng) =="
67
+ run_migrations "$umbrella" "$(basename "$module_dir")" "" || die "engine migration chain failed (IS_STATUS above) — nothing converted"
68
+ stamp_version "$config" "$eng"
69
+ fi
70
+ fi
71
+
61
72
  echo "== staging =="
62
73
  # Engine content arrives as the @ainova-systems/sync package, seeded from the
63
74
  # bundle (fetch_package's guard — no network).
@@ -212,6 +223,13 @@ fi
212
223
 
213
224
  # ---- Commit ---------------------------------------------------------------
214
225
  echo "== committing =="
226
+ # .gitignore is part of the transaction: rollback must leave it exactly as
227
+ # found, absent included.
228
+ gitignore_existed=0
229
+ if [ -f "$root/.gitignore" ]; then
230
+ gitignore_existed=1
231
+ cp "$root/.gitignore" "$stage/gitignore.orig"
232
+ fi
215
233
  if [ ! -f "$root/.gitignore" ] || ! grep -q '^\.intelligence/' "$root/.gitignore"; then
216
234
  {
217
235
  [ -f "$root/.gitignore" ] && [ -n "$(tail -c 1 "$root/.gitignore" 2>/dev/null)" ] && echo ""
@@ -221,6 +239,11 @@ if [ ! -f "$root/.gitignore" ] || ! grep -q '^\.intelligence/' "$root/.gitignore
221
239
  fi
222
240
  rollback() {
223
241
  rm -rf "$root/.intelligence" "$root/intelligence.yaml" "$root/intelligence.lock"
242
+ if [ "$gitignore_existed" -eq 1 ]; then
243
+ cp "$stage/gitignore.orig" "$root/.gitignore"
244
+ else
245
+ rm -f "$root/.gitignore"
246
+ fi
224
247
  echo "rolled back — the vendored setup is untouched" >&2
225
248
  }
226
249
  mv "$stage/.intelligence" "$root/.intelligence"
@@ -28,15 +28,7 @@ case "$sub" in
28
28
  done < <(registries_list "$IP_ROOT/intelligence.yaml")
29
29
  [ "$found" -eq 0 ] && echo " (none)"
30
30
  fi
31
- bundled="$(default_index_file)"
32
- if [ -n "$bundled" ]; then
33
- echo "Bundled index:"
34
- while IFS= read -r name; do
35
- [ -n "$name" ] || continue
36
- echo " $name -> $(qmap_field "$bundled" "packages" "$name" "url")"
37
- done < <(qmap_keys "$bundled" "packages")
38
- fi
39
- echo "Fallback: @org/name -> https://github.com/org/name.git"
31
+ echo "Names resolve ONLY through the list above. Registry-less installs are explicit: github:org/repo, git+<url>."
40
32
  ;;
41
33
  add)
42
34
  url="${2:-}"
@@ -4,7 +4,7 @@
4
4
  # "what does this project depend on", `search` answers "what could it".
5
5
  #
6
6
  # Sources, in resolution order (a name found earlier wins, exactly as `add`
7
- # resolves it): every scope bound in the manifest, then the bundled index.
7
+ # resolves it): the manifest's trusted registries, in order.
8
8
  set -euo pipefail
9
9
  source "$CLI_DIR/lib/cli-common.sh"
10
10
 
@@ -69,15 +69,13 @@ if [ -n "$manifest" ]; then
69
69
  emit_index "$index" "$url"
70
70
  done < <(registries_list "$manifest")
71
71
  fi
72
- emit_index "$(default_index_file)" "bundled"
73
-
74
72
  if [ "$rows" -eq 0 ]; then
75
73
  if [ -n "$term" ]; then
76
- echo "Nothing matching '$term' in the configured registries."
74
+ echo "Nothing matching '$term' in this project's trusted registries."
77
75
  else
78
- echo "No registries hold any packages."
76
+ echo "No trusted registries (or they offer nothing) — intelligence registry add <repo-url>."
79
77
  fi
80
- echo "Any git repo with rules/, agents/ or skills/ is a package: intelligence add @org/repo"
78
+ echo "Explicit installs need no registry: intelligence add github:org/repo"
81
79
  exit 0
82
80
  fi
83
81
 
@@ -21,10 +21,13 @@ require_v2
21
21
  manifest="$IP_ROOT/intelligence.yaml"
22
22
  lock="$IP_ROOT/intelligence.lock"
23
23
 
24
- moved=0 checked=0
24
+ moved=0 found=0
25
25
  while IFS= read -r name; do
26
26
  [ -n "$name" ] || continue
27
27
  [ -n "$only" ] && [ "$name" != "$only" ] && continue
28
+ # A named package that exists is "found" whatever happens next — a skip
29
+ # message followed by "not in the manifest" would contradict itself.
30
+ found=1
28
31
  if [ "$name" = "$SYNC_PKG_NAME" ]; then
29
32
  echo " $name: engine content moves with the engine — 'intelligence upgrade' owns it"
30
33
  continue
@@ -32,22 +35,34 @@ while IFS= read -r name; do
32
35
  ref="$(qmap_field "$manifest" "packages" "$name" "ref")"
33
36
  [ -n "$ref" ] && { echo " $name: pinned to ref '$ref' — skipped"; continue; }
34
37
  range="$(qmap_field "$manifest" "packages" "$name" "version")"
38
+ # An exact version is a pin, same as ref: — update never moves pins.
39
+ if [ -n "$range" ] && semver_is_stable "$range"; then
40
+ echo " $name: pinned to $range — edit the manifest to move it"
41
+ continue
42
+ fi
35
43
  url="$(qmap_field "$manifest" "packages" "$name" "url")"
36
44
  path="$(qmap_field "$manifest" "packages" "$name" "path")"
37
45
  if [ -z "$url" ]; then
38
46
  resolve_package_source "$manifest" "$name"
39
47
  url="$RES_URL"; path="$RES_PATH"
40
48
  fi
41
- checked=$((checked + 1))
42
49
  picked="$(list_remote_versions "$url" | semver_pick_highest "$range")"
43
50
  [ -n "$picked" ] || { echo " $name: nothing satisfies '${range:-*}' at $url" >&2; continue; }
44
51
  read -r tag _ <<< "$(remote_tag_for_version "$url" "$picked")"
45
52
  current="$(qmap_field "$lock" "packages" "$name" "resolved")"
46
- if [ "$tag" = "$current" ]; then
53
+ cur_url="$(qmap_field "$lock" "packages" "$name" "url")"
54
+ cur_path="$(qmap_field "$lock" "packages" "$name" "path")"
55
+ # Same tag is not enough: a registry may re-point a name's source while
56
+ # keeping the version — the move doctor warns about must be actionable
57
+ # here, so url/path changes refetch too.
58
+ if [ "$tag" = "$current" ] && [ "$url" = "$cur_url" ] && [ "$path" = "$cur_path" ]; then
47
59
  echo " $name: ${current:-<none>} (up to date)"
48
60
  continue
49
61
  fi
50
62
  rel=".intelligence/packages/$name"
63
+ # The new version may have dropped a top-level section dir: unwire the old
64
+ # shape first, or a dangling store-backed source bricks the next sync.
65
+ unwire_package_sources "$manifest" "$rel"
51
66
  sha="$(fetch_package "$url" "$tag" "$path" "$IP_ROOT/$rel")"
52
67
  wire_package_sources "$manifest" "$name" "$rel" "$IP_ROOT"
53
68
  lock_upsert "$lock" "$name" "$range" "$url" "$path" "$tag" "$sha"
@@ -55,7 +70,7 @@ while IFS= read -r name; do
55
70
  moved=$((moved + 1))
56
71
  done < <(qmap_keys "$manifest" "packages")
57
72
 
58
- [ -n "$only" ] && [ "$checked" -eq 0 ] && die "package '$only' is not in the manifest"
73
+ [ -n "$only" ] && [ "$found" -eq 0 ] && die "package '$only' is not in the manifest"
59
74
  echo "updated: $moved package(s)"
60
75
 
61
76
  if [ "$moved" -gt 0 ] && [ "$no_sync" -eq 0 ]; then
@@ -0,0 +1,14 @@
1
+ # What THIS CLI distribution ships as its engine-content package, and which
2
+ # registry its init seeds into new manifests. Data, not code: a fork or an
3
+ # enterprise mirror edits this file (or its copy in the npm build) — nothing
4
+ # in cli/ names a vendor.
5
+ #
6
+ # The engine-content package is the one package whose content documents the
7
+ # engine scripts bundled with the CLI: init writes this entry into the
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.
11
+ name: "@ainova-systems/sync"
12
+ url: "https://github.com/ainova-systems/intelligence-sync.git"
13
+ path: "intelligence/sync"
14
+ default_registry: "https://github.com/ainova-systems/intelligence-registry.git"
@@ -24,15 +24,23 @@ source "$CLI_DIR/lib/semver.sh"
24
24
  source "$CLI_DIR/lib/registry.sh"
25
25
  source "$CLI_DIR/lib/lockfile.sh"
26
26
 
27
- # The engine's own content as a package: OPTIONAL but auto-selected at init.
28
- # Package by UX (manifest entry, lockfile row, list/search/remove), bundle by
29
- # mechanics — at the version the CLI ships, it materializes from the npm
30
- # bundle without network; only a cross-version install reaches git. The pin
31
- # is held exactly at the bundled engine version and moved only by `upgrade`.
32
- SYNC_PKG_NAME="@ainova-systems/sync"
33
- SYNC_PKG_URL="https://github.com/ainova-systems/intelligence-sync.git"
34
- SYNC_PKG_PATH="intelligence/sync"
35
- SYNC_PKG_STORE=".intelligence/packages/@ainova-systems/sync"
27
+ # The engine-content package: OPTIONAL but auto-selected at init. Package by
28
+ # UX (manifest entry, lockfile row, list/search/remove), bundle by mechanics —
29
+ # at the version the CLI ships, it materializes from the npm bundle without
30
+ # network; only a cross-version install reaches git. The pin is held exactly
31
+ # at the bundled engine version and moved only by `upgrade`.
32
+ #
33
+ # Its identity is DATA shipped with the distribution (cli/engine-package.yaml),
34
+ # never a name compiled into cli code — a fork edits the file.
35
+ _EPKG="$CLI_DIR/engine-package.yaml"
36
+ SYNC_PKG_NAME="$(top_scalar "$_EPKG" "name")"
37
+ SYNC_PKG_URL="$(top_scalar "$_EPKG" "url")"
38
+ SYNC_PKG_PATH="$(top_scalar "$_EPKG" "path")"
39
+ # Read by commands (init seeds it) — per-file shellcheck cannot see that.
40
+ # shellcheck disable=SC2034
41
+ DEFAULT_REGISTRY_URL="$(top_scalar "$_EPKG" "default_registry")"
42
+ [ -n "$SYNC_PKG_NAME" ] || die "corrupt CLI installation: $_EPKG is missing or has no 'name'"
43
+ SYNC_PKG_STORE=".intelligence/packages/$SYNC_PKG_NAME"
36
44
 
37
45
  # --- Project detection ---------------------------------------------------
38
46
  # Sets: IP_MODE (v2|legacy|none), IP_ROOT, IP_UMBRELLA, IP_MODULE_DIR.
@@ -127,7 +135,7 @@ export_engine_env() {
127
135
  export CONFIG_FILE="$root/intelligence.yaml"
128
136
  export REPO_ROOT="$root"
129
137
  export IS_UMBRELLA_REL="$umbrella"
130
- export IS_MODULE_REL=".intelligence/engine"
138
+ export IS_MODULE_REL="$SYNC_PKG_STORE"
131
139
  export IS_SYNC_CMD="intelligence sync"
132
140
  export IS_MANIFEST_NAME="intelligence.yaml"
133
141
  export IS_PROTECTED_DIRS="$umbrella:.intelligence"
@@ -24,6 +24,16 @@ assert_valid_pkg_name() {
24
24
  case "$1" in
25
25
  *[!@/A-Za-z0-9._-]*|*/*/*) die "invalid package name '$1' — allowed: letters, digits, . _ -, one '/'" ;;
26
26
  esac
27
+ # Both segments become store path components fed to rm -rf: an empty one
28
+ # ('@scope/') resolves to the whole scope dir, and a dot segment walks the
29
+ # tree — either would turn one bad --name into deleting other packages.
30
+ local scope="${1%%/*}" short="${1#*/}"
31
+ case "${scope#@}" in
32
+ ""|.|..) die "invalid package name '$1' — empty or dot scope" ;;
33
+ esac
34
+ case "$short" in
35
+ ""|.|..) die "invalid package name '$1' — empty or dot name" ;;
36
+ esac
27
37
  }
28
38
 
29
39
  # qmap_keys <file> <block> — quoted keys, one per line.
@@ -273,6 +283,35 @@ registries_remove() {
273
283
  '
274
284
  }
275
285
 
286
+ # sources_add_entry_first <file> <section> <entry> — idempotent insert at the
287
+ # TOP of sources.<section> (creating sources:/section as needed). Package
288
+ # entries go through this so project-owned entries stay later in the list —
289
+ # adapters copy sources in order and the last write wins, which is exactly
290
+ # the documented "your file overrides the package's" behavior.
291
+ sources_add_entry_first() {
292
+ local file="$1" section="$2" entry="$3"
293
+ _mig_has_source "$file" "$entry" && return 0
294
+ _qmap_stage "$file" -v section="$section" -v entry="$entry" '
295
+ function line() { return " - \"" entry "\"" }
296
+ { sub(/\r$/, "") }
297
+ /^sources:[ \t]*$/ { ins = 1; sourceseen = 1; print; next }
298
+ ins && /^[^ #]/ {
299
+ if (!secseen && !done) { print " " section ":"; print line(); done = 1 }
300
+ ins = 0
301
+ }
302
+ ins && $0 ~ "^ " section ":[ \t]*$" { secseen = 1; print; print line(); done = 1; next }
303
+ { print }
304
+ END {
305
+ if (ins && !secseen && !done) { print " " section ":"; print line(); done = 1 }
306
+ if (!sourceseen) {
307
+ print "sources:"
308
+ print " " section ":"
309
+ print line()
310
+ }
311
+ }
312
+ '
313
+ }
314
+
276
315
  # sources_remove_entry <file> <section> <entry> — remove `- "entry"` from
277
316
  # sources.<section>. Inverse of the engine-side _mig_add_source.
278
317
  sources_remove_entry() {
@@ -1,29 +1,23 @@
1
1
  #!/bin/bash
2
2
  # Name -> source resolution and package fetching.
3
3
  #
4
- # Resolution order (first registry to DECLARE the name wins):
5
- # 1. project `registries:` — a trust LIST of registry repos (git repos
6
- # holding index.yaml), consulted in manifest order; adding one is an
7
- # explicit, committed act of trust in the names it declares
8
- # 2. the bundled default index (registry/index.yaml next to the CLI)
9
- # 3. convention: @org/name -> https://github.com/org/name.git, content at
10
- # the repo root — the zero-infrastructure default
4
+ # A name resolves ONLY through the manifest's `registries:` trust list (git
5
+ # repos holding index.yaml, consulted in order; the first to declare the name
6
+ # wins). There is deliberately no built-in catalog and no name->github
7
+ # convention: the CLI core knows no vendor, and a name nobody explicitly
8
+ # trusted must never silently turn into an install from a guessed URL.
9
+ # Sources without a registry are always EXPLICIT: github:org/repo, git+<url>.
10
+ # Vendor defaults exist only as lines `init` writes into the manifest —
11
+ # visible, reviewable, deletable.
11
12
  #
12
13
  # The NAME is the trust anchor a developer reasons with; source integrity is
13
- # a separate mechanism: the lock pins url+sha, a project-registry hit that
14
- # shadows a bundled name with a different url warns loudly, and doctor flags
14
+ # a separate mechanism: the lock pins url+sha, and doctor flags
15
15
  # resolution/lock url drift.
16
16
  #
17
17
  # An index is itself fetched with git (never curl): auth, proxies and private
18
18
  # hosting all come for free, and the CLI keeps the engine's bash+awk+git-only
19
19
  # dependency footprint.
20
20
 
21
- default_index_file() {
22
- local f="$CLI_DIR/../registry/index.yaml"
23
- [ -f "$f" ] && printf '%s' "$f"
24
- return 0
25
- }
26
-
27
21
  # _fetch_index <registry-repo-url> — clones the registry repo (shallow) and
28
22
  # prints the path of its index.yaml; empty on failure. Cached per run+url.
29
23
  _fetch_index() {
@@ -49,11 +43,9 @@ _fetch_index() {
49
43
  # shellcheck disable=SC2034
50
44
  resolve_package_source() {
51
45
  local manifest="$1" name="$2"
52
- local scope="${name%%/*}" short="${name#*/}"
53
46
  RES_URL=""; RES_PATH=""; RES_VIA=""
54
47
 
55
- local reg_url index bundled burl
56
- bundled="$(default_index_file)"
48
+ local reg_url index
57
49
  if [ -n "$manifest" ] && [ -f "$manifest" ]; then
58
50
  while IFS= read -r reg_url; do
59
51
  [ -n "$reg_url" ] || continue
@@ -66,37 +58,18 @@ resolve_package_source() {
66
58
  if [ -n "$RES_URL" ]; then
67
59
  RES_PATH="$(qmap_field "$index" "packages" "$name" "path")"
68
60
  RES_VIA="registry:$reg_url"
69
- # Shadowing a bundled name with a DIFFERENT source is legal
70
- # (that is what overriding means) but never silent.
71
- if [ -n "$bundled" ]; then
72
- burl="$(qmap_field "$bundled" "packages" "$name" "url")"
73
- if [ -n "$burl" ] && [ "$burl" != "$RES_URL" ]; then
74
- echo " WARN: $name from this registry overrides the bundled source ($burl)" >&2
75
- fi
76
- fi
77
61
  return 0
78
62
  fi
79
63
  done < <(registries_list "$manifest")
80
64
  fi
81
-
82
- if [ -n "$bundled" ]; then
83
- RES_URL="$(qmap_field "$bundled" "packages" "$name" "url")"
84
- if [ -n "$RES_URL" ]; then
85
- RES_PATH="$(qmap_field "$bundled" "packages" "$name" "path")"
86
- RES_VIA="index"
87
- return 0
88
- fi
89
- fi
90
-
91
- RES_URL="https://github.com/${scope#@}/$short.git"
92
- RES_PATH=""
93
- RES_VIA="convention"
65
+ # No trusted registry declares the name. Deliberately NOT an invented
66
+ # URL: the caller reports it, with suggestions.
94
67
  return 0
95
68
  }
96
69
 
97
70
  # suggest_similar <manifest-or-empty> <@scope/name> — "Did you mean" lines to
98
71
  # stderr, matched on the short name (singular/plural tolerant) across every
99
- # project registry and the bundled index.
72
+ # trusted registry.
100
73
  suggest_similar() {
101
74
  local manifest="$1" name="$2"
102
75
  local want cand cshort reg_url index seen=" "
@@ -121,7 +94,6 @@ suggest_similar() {
121
94
  _suggest_from "$index"
122
95
  done < <(registries_list "$manifest")
123
96
  fi
124
- _suggest_from "$(default_index_file)"
125
97
  }
126
98
 
127
99
  # fetch_package <url> <ref> <subpath> <dest-dir>
@@ -182,14 +154,14 @@ fetch_package() {
182
154
 
183
155
  # wire_package_sources <manifest> <@scope/name> <store-rel-dir>
184
156
  # The provides convention: whichever of rules/agents/skills the installed
185
- # package has becomes a sources entry. Reuses the engine's idempotent,
186
- # comment-preserving _mig_add_source.
157
+ # package has becomes a sources entry — inserted FIRST in its section, so
158
+ # project-owned entries come later and win on same-named artifacts.
187
159
  wire_package_sources() {
188
160
  local manifest="$1" name="$2" rel="$3" root="$4"
189
161
  local section
190
162
  for section in rules agents skills; do
191
163
  if [ -d "$root/$rel/$section" ]; then
192
- _mig_add_source "$manifest" "$section" "$rel/$section"
164
+ sources_add_entry_first "$manifest" "$section" "$rel/$section"
193
165
  fi
194
166
  done
195
167
  }
@@ -27,7 +27,7 @@ No engine code lives in the project. The engine's *scripts* ship inside the npm
27
27
 
28
28
  | Command | Does |
29
29
  |---|---|
30
- | `init [--targets a,b] [--dir d] [--bare] [--no-sync]` | Detect tools by their marker dirs, write a minimal self-documenting root manifest, `.gitignore` the store, install `@ainova-systems/sync` (unless `--bare`), first sync. No dirs are created — authoring one later needs no config edit. |
30
+ | `init [--targets a,b] [--dir d] [--bare] [--no-sync]` | Enable `agents` (the tool-neutral AGENTS.md) plus every tool DETECTED by repo markers (`.claude`/`CLAUDE.md`, `.cursor`, `.codex`, `.pi`, `.opencode`, `.github/instructions`) or named via `--targets` — a tool is never invented. Writes a minimal self-documenting root manifest, `.gitignore`s the store, installs `@ainova-systems/sync` (unless `--bare`), first sync. No dirs are created. |
31
31
  | `add <spec> [--name @s/n] [--no-sync]` | Resolve → fetch → wire sources → manifest entry → lock → sync. Specs: `@scope/name[@range]`, `github:org/repo[#path]`, `git+<url>[@ref][#path]`. |
32
32
  | `remove <name> [--force]` | Inverse of add: manifest, sources, lock, store. |
33
33
  | `install [--frozen] [--force]` | Restore the store exactly from the lock; resolve manifest packages the lock lacks (`--frozen` refuses instead, and fails on sha drift). |
@@ -44,11 +44,7 @@ No engine code lives in the project. The engine's *scripts* ship inside the npm
44
44
 
45
45
  **Names are global.** The name is the trust anchor a developer reasons with — the same `@scope/name` means the same package in every project, every lock and every conversation. A registry never renames anything, and two versions of one name cannot coexist in a project: intelligence artifacts land in each tool's flat namespace (`.claude/skills/<name>/`, the `AGENTS.md` tables), so duplicates would collide file-by-file. Need pieces of two versions? That is a different package — fork it under a different name. Need to override one artifact? Put a same-named file in your own content dir; project sources are listed after package sources.
46
46
 
47
- **Name → repo resolution** — the first registry to *declare* the name wins:
48
-
49
- 1. `registries:` in the manifest — a **trust list** of registry repos (git repos holding an `index.yaml`), consulted in order. Adding one is an explicit, committed, reviewable act of trust in the names it declares; private registries are just private repos, git auth covers access. A project-registry hit that shadows a bundled name with a *different* source warns loudly, and `doctor` flags a name whose current resolution url no longer matches the lock.
50
- 2. The bundled default index (`registry/index.yaml`) — needed exactly when a name is not a repo (monorepos: `@ainova-systems/core` → `intelligence-dev-packs` at `packs/core`).
51
- 3. Convention: `@org/name` → `https://github.com/org/name.git`, content at the repo root. Zero infrastructure: any repo with the three dirs is already a package.
47
+ **Name → repo resolution: the trust list is the ONLY resolver.** `registries:` in the manifest holds registry repos (git repos with an `index.yaml`), consulted in order — the first to declare the name wins. There is deliberately **no built-in catalog and no `@org/name` → github guessing**: the CLI core knows no vendor, and a name nobody explicitly trusted never turns into an install from an invented URL. A name no trusted registry declares is refused with suggestions. Installs without a registry are always **explicit sources**: `github:org/repo`, `git+<url>[@ref][#path]`. Vendor defaults exist only as lines `init` seeds into the manifest — visible, reviewable, deletable (`doctor` still flags a name whose current resolution url no longer matches the lock).
52
48
 
53
49
  **Versions are git tags.** Ranges (`^1.2.0`, `~1.2.0`, exact, `latest`) match stable `x.y.z` tags (optional `v` prefix) listed via `git ls-remote` — no clone to resolve. `add` without a range picks the highest stable tag and records `^that`. A branch or commit pin is `ref:`, the escape hatch that ranges never touch. Prerelease-suffixed tags are invisible to ranges by design.
54
50
 
@@ -1 +1 @@
1
- 40b7d3a7dd936c5ffb854f9e5f553720c48ef494
1
+ 3a5cd174c73bd3918ac0a01df8d54a6f3d98b485
@@ -94,7 +94,7 @@ if [ "${IS_CLI:-0}" = "1" ]; then
94
94
  # `/d/...`, and prefix comparisons downstream need one spelling.
95
95
  CONFIG_FILE="$(cd "$(dirname "$CONFIG_FILE")" && pwd)/$(basename "$CONFIG_FILE")"
96
96
  IS_UMBRELLA_REL="${IS_UMBRELLA_REL:-intelligence}"
97
- IS_MODULE_REL="${IS_MODULE_REL:-.intelligence/engine}"
97
+ IS_MODULE_REL="${IS_MODULE_REL:-.intelligence/packages/@ainova-systems/sync}"
98
98
  # Project-owned adapters keep their v1 home: <umbrella>/adapters/.
99
99
  INTELLIGENCE_DIR="$REPO_ROOT/$IS_UMBRELLA_REL"
100
100
  else
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ainova-systems/intelligence",
3
- "version": "0.11.0-rc.5",
3
+ "version": "0.11.0-rc.6",
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"
@@ -8,8 +8,7 @@
8
8
  "files": [
9
9
  "bin",
10
10
  "cli",
11
- "engine",
12
- "registry"
11
+ "engine"
13
12
  ],
14
13
  "engines": {
15
14
  "node": ">=18"
@@ -1,26 +0,0 @@
1
- # The default Intelligence Registry index, bundled with the CLI.
2
- #
3
- # Maps a package name to the git repo (and path inside it) the package lives
4
- # in. Needed exactly when a name is not the repo — monorepos like
5
- # intelligence-dev-packs. A name absent here falls through to the convention:
6
- # @org/name -> https://github.com/org/name.git, content at the repo root.
7
- # Organizations override or extend this per scope with `intelligence registry
8
- # add @scope <registry-repo-url>`.
9
- #
10
- # This copy travels inside the CLI package. The same index lives at
11
- # https://github.com/ainova-systems/intelligence-registry and can be bound
12
- # explicitly (`intelligence registry add @ainova-systems <url>`) to read the
13
- # newest entries without waiting for a CLI release.
14
- packages:
15
- "@ainova-systems/sync":
16
- url: "https://github.com/ainova-systems/intelligence-sync.git"
17
- path: "intelligence/sync"
18
- description: "The engine's own content: authoring rule, engine agents and the intelligence-* meta-skills. Auto-installed by init; pinned to the engine version."
19
- "@ainova-systems/core":
20
- url: "https://github.com/ainova-systems/intelligence-dev-packs.git"
21
- path: "packs/core"
22
- description: "Engineering discipline for AI-first development: context engineering, artifact-derived status, review and diagnosis loops."
23
- "@ainova-systems/spec":
24
- url: "https://github.com/ainova-systems/intelligence-dev-packs.git"
25
- path: "packs/spec"
26
- description: "Spec-driven delivery: specifications, decision records, sliced execution."