@ainova-systems/intelligence 0.11.0-rc.5 → 0.11.0-rc.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.
Files changed (53) hide show
  1. package/README.md +2 -2
  2. package/cli/commands/add.sh +26 -20
  3. package/cli/commands/init.sh +24 -9
  4. package/cli/commands/install.sh +55 -4
  5. package/cli/commands/migrate.sh +62 -9
  6. package/cli/commands/registry.sh +2 -9
  7. package/cli/commands/search.sh +4 -6
  8. package/cli/commands/sync.sh +1 -1
  9. package/cli/commands/update.sh +29 -5
  10. package/cli/engine-package.yaml +14 -0
  11. package/cli/intelligence +27 -15
  12. package/cli/lib/cli-common.sh +52 -17
  13. package/cli/lib/lockfile.sh +14 -8
  14. package/cli/lib/manifest.sh +59 -1
  15. package/cli/lib/registry.sh +41 -55
  16. package/cli/lib/semver.sh +4 -2
  17. package/engine/ENGINE_SHA +1 -0
  18. package/engine/{scripts/adapters → adapters}/agents.sh +10 -15
  19. package/engine/{scripts/lib → lib}/common.sh +15 -519
  20. package/engine/lib/contract.sh +120 -0
  21. package/engine/sync.sh +233 -0
  22. package/package.json +5 -5
  23. package/engine/INIT.md +0 -500
  24. package/engine/docs/CLI.md +0 -94
  25. package/engine/scripts/ENGINE_SHA +0 -1
  26. package/engine/scripts/lib/layout.sh +0 -51
  27. package/engine/scripts/lib/migrations.sh +0 -708
  28. package/engine/scripts/sync.sh +0 -311
  29. package/engine/scripts/update.sh +0 -237
  30. package/registry/index.yaml +0 -26
  31. /package/engine/{scripts/VERSION → VERSION} +0 -0
  32. /package/engine/{scripts/adapters → adapters}/_template.sh +0 -0
  33. /package/engine/{scripts/adapters → adapters}/claude.sh +0 -0
  34. /package/engine/{scripts/adapters → adapters}/codex.sh +0 -0
  35. /package/engine/{scripts/adapters → adapters}/copilot.sh +0 -0
  36. /package/engine/{scripts/adapters → adapters}/cursor.sh +0 -0
  37. /package/engine/{scripts/adapters → adapters}/opencode.sh +0 -0
  38. /package/engine/{scripts/adapters → adapters}/pi.sh +0 -0
  39. /package/{engine → packages/sync}/agents/intelligence-architect.md +0 -0
  40. /package/{engine → packages/sync}/agents/intelligence-operator.md +0 -0
  41. /package/{engine → packages/sync}/docs/ADAPTERS.md +0 -0
  42. /package/{engine → packages/sync}/docs/CONVENTIONS.md +0 -0
  43. /package/{engine → packages/sync}/rules/intelligence-authoring.md +0 -0
  44. /package/{engine → packages/sync}/skills/intelligence-add-agent/SKILL.md +0 -0
  45. /package/{engine → packages/sync}/skills/intelligence-add-rule/SKILL.md +0 -0
  46. /package/{engine → packages/sync}/skills/intelligence-add-skill/SKILL.md +0 -0
  47. /package/{engine → packages/sync}/skills/intelligence-extract-skill/SKILL.md +0 -0
  48. /package/{engine → packages/sync}/skills/intelligence-install-adapter/SKILL.md +0 -0
  49. /package/{engine → packages/sync}/skills/intelligence-learn-from-context/SKILL.md +0 -0
  50. /package/{engine → packages/sync}/skills/intelligence-review-skills/SKILL.md +0 -0
  51. /package/{engine → packages/sync}/skills/intelligence-sync/SKILL.md +0 -0
  52. /package/{engine → packages/sync}/skills/intelligence-uninstall-adapter/SKILL.md +0 -0
  53. /package/{engine → packages/sync}/skills/intelligence-update/SKILL.md +0 -0
package/README.md CHANGED
@@ -38,9 +38,9 @@ intelligence install
38
38
  | `intelligence update [pkg]` | Re-resolve version ranges, rewrite the lock |
39
39
  | `intelligence sync [target]` | Render intelligence to every enabled tool |
40
40
  | `intelligence list` / `doctor` / `status` | Inspect and verify |
41
- | `intelligence registry add @scope <url>` | Bind a scope to a private registry index |
41
+ | `intelligence registry add <repo-url>` | Trust a registry (a git repo with an `index.yaml`) |
42
42
  | `intelligence migrate` | Convert a vendored intelligence-sync setup to the CLI |
43
43
 
44
- Any git repo with `rules/`, `agents/` or `skills/` at its root is already an Intelligence Package: `intelligence add @your-org/your-repo` resolves it straight from GitHub — versions come from its `x.y.z` git tags. A monorepo of packages needs one line in a registry index.
44
+ Any git repo with `rules/`, `agents/` or `skills/` at its root is already an Intelligence Package: install it from an explicit source — `intelligence add github:your-org/your-repo` — with versions from its `x.y.z` git tags. Names (`@scope/name`) resolve only through registries this project explicitly trusts; a monorepo of packages needs one line in such an index.
45
45
 
46
46
  Requires `git` (and on Windows the bash it ships with — Git for Windows). Engine and docs: [ainova-systems/intelligence-sync](https://github.com/ainova-systems/intelligence-sync).
@@ -2,9 +2,9 @@
2
2
  # intelligence add <spec> [--name @scope/name] [--no-sync]
3
3
  #
4
4
  # Specs:
5
- # @scope/name[@range] resolved via registries / index / convention
6
- # github:org/repo[#path] direct from GitHub, no registry involved
7
- # git+<url>[@ref][#path] direct from any git host
5
+ # @scope/name[@range] resolved ONLY through the manifest's trusted registries
6
+ # github:org/repo[#path] explicit source: direct from GitHub, no registry involved
7
+ # git+<url>[@ref][#path] explicit source: direct from any git host
8
8
  set -euo pipefail
9
9
  source "$CLI_DIR/lib/cli-common.sh"
10
10
 
@@ -43,14 +43,15 @@ case "$spec" in
43
43
  rest="${spec#git+}"
44
44
  case "$rest" in *#*) path="${rest#*#}"; rest="${rest%%#*}" ;; esac
45
45
  # ref = the part after the last `@` in the post-scheme URL, unless it
46
- # contains `/` (then it is a user@host, not a ref) — the engine's own
47
- # git+ grammar.
46
+ # contains `/` or `:` — either marks a user@host part (scp-like
47
+ # `git@host:repo.git` included), not a ref. A branch name containing
48
+ # `/` cannot be expressed here; pin it via `ref:` in the manifest.
48
49
  base="${rest#*://}"
49
50
  case "$base" in
50
51
  *@*)
51
52
  tail="${base##*@}"
52
53
  case "$tail" in
53
- */*) ;;
54
+ */*|*:*) ;;
54
55
  *) ref="$tail"; rest="${rest%@"$tail"}" ;;
55
56
  esac
56
57
  ;;
@@ -78,23 +79,24 @@ assert_valid_pkg_name "$name"
78
79
  if [ "$mode" = "registry" ]; then
79
80
  resolve_package_source "$manifest" "$name"
80
81
  url="$RES_URL"; path="$RES_PATH"
82
+ if [ -z "$url" ]; then
83
+ echo "ERROR: no trusted registry declares '$name'." >&2
84
+ echo " A name installs only through a registry this project added explicitly" >&2
85
+ echo " ('intelligence registry list' shows them). Either add its registry:" >&2
86
+ echo " intelligence registry add <registry-repo-url>" >&2
87
+ echo " or install from an explicit source you name yourself:" >&2
88
+ echo " intelligence add github:org/repo | intelligence add git+<url>[@ref][#path]" >&2
89
+ suggest_similar "$manifest" "$name"
90
+ exit 1
91
+ fi
81
92
  echo " resolving $name via $RES_VIA -> $url${path:+ ($path)}"
82
93
  fi
83
94
 
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.
87
- if ! GIT_TERMINAL_PROMPT=0 git ls-remote "$url" >/dev/null 2>&1; then
95
+ # The source must exist before anything else is attempted — never die
96
+ # mid-pipeline with no message.
97
+ if ! GIT_TERMINAL_PROMPT=0 git ls-remote -- "$url" >/dev/null 2>&1; then
88
98
  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
99
+ echo " Declared by ${RES_VIA:-the spec} — check the URL and your git access." >&2
98
100
  exit 1
99
101
  fi
100
102
 
@@ -124,7 +126,11 @@ sha="$(fetch_package "$url" "${ref:-$resolved_tag}" "$path" "$IP_ROOT/$rel")"
124
126
  wire_package_sources "$manifest" "$name" "$rel" "$IP_ROOT"
125
127
 
126
128
  # Manifest entry: a registry package records its range; a direct one records
127
- # its source so resolution never has to guess it again.
129
+ # its source so resolution never has to guess it again. The entry is replaced
130
+ # WHOLE — re-adding under a different spec must not leave the old spec's
131
+ # fields behind (a stale ref: would freeze update, a stale url: would pin the
132
+ # old source).
133
+ qmap_delete_key "$manifest" "packages" "$name"
128
134
  if [ "$mode" = "registry" ]; then
129
135
  qmap_set "$manifest" "packages" "$name" "version" "$requested"
130
136
  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,17 +22,53 @@ 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
66
+ assert_valid_pkg_name "$name"
30
67
  if [ ! -f "$lock" ] || [ -z "$(qmap_field "$lock" "packages" "$name" "url")" ]; then
31
68
  missing="$missing $name"
32
69
  fi
33
70
  done < <(qmap_keys "$manifest" "packages")
34
71
  if [ -n "$missing" ]; then
35
- [ "$frozen" -eq 1 ] && die "lockfile is missing packages:$missing — run 'intelligence install' without --frozen"
36
72
  for name in $missing; do
37
73
  # The engine-content package needs no remote resolution: its pin IS
38
74
  # the bundled engine version and it seeds from the bundle, offline.
@@ -66,20 +102,35 @@ fi
66
102
  if [ -f "$lock" ]; then
67
103
  while IFS="$LOCK_SEP" read -r name requested url path resolved sha; do
68
104
  [ -n "$name" ] || continue
105
+ # The lock arrives with a cloned repo — its keys are untrusted input
106
+ # on their way into rm -rf paths, and its urls into git argv.
107
+ assert_valid_pkg_name "$name"
69
108
  rel=".intelligence/packages/$name"
70
- if [ -d "$IP_ROOT/$rel" ] && [ "$force" -eq 0 ]; then
109
+ # --frozen never trusts what is already on disk: a pre-existing
110
+ # (possibly substituted) store dir would otherwise be synced with no
111
+ # sha verification at all.
112
+ if [ -d "$IP_ROOT/$rel" ] && [ "$force" -eq 0 ] && [ "$frozen" -eq 0 ]; then
71
113
  continue
72
114
  fi
73
- got="$(fetch_package "$url" "$resolved" "$path" "$IP_ROOT/$rel")"
115
+ # Fetch into staging and commit to the store only after the integrity
116
+ # verdict: content that violates the lock must never be left where a
117
+ # re-run (whose dir-exists check skips verification) would sync it.
118
+ staging="$IP_ROOT/.intelligence/.staging-$$"
119
+ rm -rf "$staging"
120
+ got="$(fetch_package "$url" "$resolved" "$path" "$staging")"
74
121
  # An empty sha (bundle-seeded content on a dev build) means unknown,
75
122
  # not moved — only two known-and-different shas are a finding.
76
123
  if [ -n "$sha" ] && [ -n "$got" ] && [ "$got" != "$sha" ]; then
77
124
  if [ "$frozen" -eq 1 ]; then
125
+ rm -rf "$staging"
78
126
  die "$name: fetched $got but the lock pins $sha — the ref moved; refusing under --frozen"
79
127
  fi
80
128
  echo " WARN: $name resolved to $got, lock had $sha — updating the lock" >&2
81
129
  lock_upsert "$lock" "$name" "$requested" "$url" "$path" "$resolved" "$got"
82
130
  fi
131
+ rm -rf "${IP_ROOT:?}/$rel"
132
+ mkdir -p "$(dirname "$IP_ROOT/$rel")"
133
+ mv "$staging" "$IP_ROOT/$rel"
83
134
  wire_package_sources "$manifest" "$name" "$rel" "$IP_ROOT"
84
135
  echo "= $name@${resolved:-HEAD}"
85
136
  done < <(lock_to_tsv "$lock")
@@ -3,7 +3,7 @@
3
3
  # to the CLI setup: root intelligence.yaml, .intelligence/ store,
4
4
  # intelligence.lock, no vendored engine.
5
5
  #
6
- # Transactional in the migrations.sh spirit: STAGE everything next to the
6
+ # Transactional: STAGE everything next to the
7
7
  # project, VERIFY the staged state, then COMMIT in an order where every
8
8
  # destructive step happens only after a real sync of the new state reported
9
9
  # IS_STATUS=ok. Any earlier failure leaves the project untouched.
@@ -44,12 +44,11 @@ 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
47
+ # The v1 `packs:` block is the one v1 shape migrate still has to read, and the
48
+ # engine no longer knows it — the reader lives here, with its only consumer.
49
+ get_pack_field() {
50
+ get_nested_yaml_value "$1" "packs" "$2" "$3"
51
+ }
53
52
 
54
53
  # ---- Stage ----------------------------------------------------------------
55
54
  stage="$root/.intelligence-migrate-stage.$$"
@@ -58,6 +57,20 @@ cleanup_stage() { rm -rf "$stage"; }
58
57
  trap cleanup_stage EXIT INT TERM
59
58
  mkdir -p "$stage/.intelligence/packages"
60
59
 
60
+ # v2 carries no v1 migration chain: v1 is archived, and the archived engine is
61
+ # what knows how to walk an old project forward. So migrate accepts only a
62
+ # project already at the final v1 schema and names the exact remedy otherwise —
63
+ # a remedy that keeps working indefinitely, because the archive stays at the
64
+ # URL the project's own update.sh already clones.
65
+ V1_FINAL_SCHEMA="0.10.0"
66
+ if [ -z "$stamp" ] || _ver_gt "$V1_FINAL_SCHEMA" "$stamp"; then
67
+ echo "ERROR: project schema is ${stamp:-absent (pre-0.3.1)}, older than the final v1 schema $V1_FINAL_SCHEMA." >&2
68
+ echo " Bring it forward with its own engine first — that still works:" >&2
69
+ echo " bash $module_rel/scripts/update.sh --yes" >&2
70
+ echo " then re-run: intelligence migrate" >&2
71
+ exit 1
72
+ fi
73
+
61
74
  echo "== staging =="
62
75
  # Engine content arrives as the @ainova-systems/sync package, seeded from the
63
76
  # bundle (fetch_package's guard — no network).
@@ -67,6 +80,7 @@ sync_pkg_sha="$(fetch_package "$SYNC_PKG_URL" "v$eng" "$SYNC_PKG_PATH" "$stage/$
67
80
  # per-pack source facts. Recorded as rows for the config rewrite and the lock.
68
81
  pack_rows="$stage/packs.rows"
69
82
  : > "$pack_rows"
83
+ seen_names=" "
70
84
  while IFS= read -r pack; do
71
85
  [ -n "$pack" ] || continue
72
86
  url="$(get_pack_field "$config" "$pack" "url")"
@@ -80,6 +94,12 @@ while IFS= read -r pack; do
80
94
  case "$repo" in ""|*[!A-Za-z0-9._-]*) repo="$(printf '%s' "$repo" | tr -c 'A-Za-z0-9._-' '-')" ;; esac
81
95
  name="@$org/$repo"
82
96
  assert_valid_pkg_name "$name"
97
+ # Two packs deriving the same @org/repo would overwrite one store dir and
98
+ # duplicate the lock key — fail closed, the user renames a source.
99
+ case "$seen_names" in
100
+ *" $name "*) die "packs '$pack' and another both derive package name '$name' — sources must map to distinct names" ;;
101
+ esac
102
+ seen_names="$seen_names $name "
83
103
  printf '%s\n' "$pack$LOCK_SEP$name$LOCK_SEP$url$LOCK_SEP$ref$LOCK_SEP$mirror"
84
104
  done < <(read_yaml_keys "$config" "packs") >> "$pack_rows"
85
105
 
@@ -153,8 +173,19 @@ while IFS= read -r line || [ -n "$line" ]; do
153
173
  *"- "*)
154
174
  indent="${line%%-*}"
155
175
  v="${line#*- }"
176
+ # A trailing inline comment survives the rewrite — split it
177
+ # off before touching quotes, or it would be mangled into the
178
+ # value.
179
+ trail=""
180
+ case "$v" in
181
+ *[[:space:]]"#"*)
182
+ trail=" #${v#*[[:space:]]#}"
183
+ v="${v%%[[:space:]]#*}"
184
+ ;;
185
+ esac
186
+ v="${v%"${v##*[![:space:]]}"}"
156
187
  v="${v%\"}"; v="${v#\"}"; v="${v%\'}"; v="${v#\'}"
157
- echo "$indent- \"$(rewrite_source_value "$v")\""
188
+ echo "$indent- \"$(rewrite_source_value "$v")\"$trail"
158
189
  continue
159
190
  ;;
160
191
  esac
@@ -212,6 +243,13 @@ fi
212
243
 
213
244
  # ---- Commit ---------------------------------------------------------------
214
245
  echo "== committing =="
246
+ # .gitignore is part of the transaction: rollback must leave it exactly as
247
+ # found, absent included.
248
+ gitignore_existed=0
249
+ if [ -f "$root/.gitignore" ]; then
250
+ gitignore_existed=1
251
+ cp "$root/.gitignore" "$stage/gitignore.orig"
252
+ fi
215
253
  if [ ! -f "$root/.gitignore" ] || ! grep -q '^\.intelligence/' "$root/.gitignore"; then
216
254
  {
217
255
  [ -f "$root/.gitignore" ] && [ -n "$(tail -c 1 "$root/.gitignore" 2>/dev/null)" ] && echo ""
@@ -221,8 +259,15 @@ if [ ! -f "$root/.gitignore" ] || ! grep -q '^\.intelligence/' "$root/.gitignore
221
259
  fi
222
260
  rollback() {
223
261
  rm -rf "$root/.intelligence" "$root/intelligence.yaml" "$root/intelligence.lock"
262
+ if [ "$gitignore_existed" -eq 1 ]; then
263
+ cp "$stage/gitignore.orig" "$root/.gitignore"
264
+ else
265
+ rm -f "$root/.gitignore"
266
+ fi
224
267
  echo "rolled back — the vendored setup is untouched" >&2
225
268
  }
269
+ # From the first moved file until success, an interrupt must roll back too.
270
+ trap 'rollback; exit 130' INT TERM
226
271
  mv "$stage/.intelligence" "$root/.intelligence"
227
272
  cp "$manifest_stage" "$root/intelligence.yaml"
228
273
  lock_write_from_tsv "$root/intelligence.lock" "$lock_rows"
@@ -237,13 +282,21 @@ echo "$sync_out" | grep -q '^IS_STATUS=ok' || {
237
282
  rollback
238
283
  die "sync of the migrated state did not report ok — rolled back"
239
284
  }
285
+ # Point of no return: the new state is verified. From here an interrupt must
286
+ # NOT roll back (the old setup is being removed) — only clean the stage.
287
+ trap cleanup_stage EXIT INT TERM
240
288
 
241
289
  # Only now is anything of the old setup removed — and config.yaml is kept as
242
290
  # a reviewable backup inside the store.
243
291
  mkdir -p "$root/.intelligence/backup"
244
292
  cp "$config" "$root/.intelligence/backup/config.yaml"
245
293
  while IFS="$LOCK_SEP" read -r pack name url ref mirror; do
246
- if [ -n "$mirror" ] && [ -d "$root/$mirror" ]; then
294
+ # Containment before deletion: a mirror value is config input, and rm -rf
295
+ # must never follow it out of the repository.
296
+ case "$mirror" in
297
+ ""|/*|*..*) [ -n "$mirror" ] && echo " WARN: mirror '$mirror' not repo-contained — left in place" >&2; continue ;;
298
+ esac
299
+ if [ -d "$root/$mirror" ]; then
247
300
  rm -rf "${root:?}/$mirror"
248
301
  # An emptied mirror parent (e.g. <umbrella>/external/) goes with it.
249
302
  rmdir "$(dirname "$root/$mirror")" 2>/dev/null || true
@@ -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:-}"
@@ -46,6 +38,7 @@ case "$sub" in
46
38
  case "$url" in
47
39
  @*) die "registries are added by URL — names carry their own scope. To install a package: intelligence add $url/<name>" ;;
48
40
  esac
41
+ assert_safe_source_url "${url#git+}"
49
42
  require_v2
50
43
  # Fail closed: a URL that cannot produce an index is not a registry,
51
44
  # and recording it would turn a typo here into a confusing failure at
@@ -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
 
@@ -26,7 +26,7 @@ case "$IP_MODE" in
26
26
  die "installed content missing on disk:$missing — run 'intelligence install'"
27
27
  fi
28
28
  export_engine_env "$IP_ROOT"
29
- exec bash "$IS_ENGINE_DIR/scripts/sync.sh" "$@"
29
+ exec bash "$IS_ENGINE_DIR/sync.sh" "$@"
30
30
  ;;
31
31
  legacy)
32
32
  # A vendored project syncs with ITS engine — its pin is the contract,
@@ -21,10 +21,15 @@ 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
+ # Manifest keys are untrusted input on their way into store paths.
28
+ assert_valid_pkg_name "$name"
27
29
  [ -n "$only" ] && [ "$name" != "$only" ] && continue
30
+ # A named package that exists is "found" whatever happens next — a skip
31
+ # message followed by "not in the manifest" would contradict itself.
32
+ found=1
28
33
  if [ "$name" = "$SYNC_PKG_NAME" ]; then
29
34
  echo " $name: engine content moves with the engine — 'intelligence upgrade' owns it"
30
35
  continue
@@ -32,30 +37,49 @@ while IFS= read -r name; do
32
37
  ref="$(qmap_field "$manifest" "packages" "$name" "ref")"
33
38
  [ -n "$ref" ] && { echo " $name: pinned to ref '$ref' — skipped"; continue; }
34
39
  range="$(qmap_field "$manifest" "packages" "$name" "version")"
40
+ # An exact version is a pin, same as ref: — update never moves pins.
41
+ if [ -n "$range" ] && semver_is_stable "$range"; then
42
+ echo " $name: pinned to $range — edit the manifest to move it"
43
+ continue
44
+ fi
35
45
  url="$(qmap_field "$manifest" "packages" "$name" "url")"
36
46
  path="$(qmap_field "$manifest" "packages" "$name" "path")"
37
47
  if [ -z "$url" ]; then
38
48
  resolve_package_source "$manifest" "$name"
39
49
  url="$RES_URL"; path="$RES_PATH"
40
50
  fi
41
- checked=$((checked + 1))
42
51
  picked="$(list_remote_versions "$url" | semver_pick_highest "$range")"
43
52
  [ -n "$picked" ] || { echo " $name: nothing satisfies '${range:-*}' at $url" >&2; continue; }
44
53
  read -r tag _ <<< "$(remote_tag_for_version "$url" "$picked")"
45
54
  current="$(qmap_field "$lock" "packages" "$name" "resolved")"
46
- if [ "$tag" = "$current" ]; then
55
+ cur_url="$(qmap_field "$lock" "packages" "$name" "url")"
56
+ cur_path="$(qmap_field "$lock" "packages" "$name" "path")"
57
+ # Same tag is not enough: a registry may re-point a name's source while
58
+ # keeping the version — the move doctor warns about must be actionable
59
+ # here, so url/path changes refetch too.
60
+ if [ "$tag" = "$current" ] && [ "$url" = "$cur_url" ] && [ "$path" = "$cur_path" ]; then
47
61
  echo " $name: ${current:-<none>} (up to date)"
48
62
  continue
49
63
  fi
50
64
  rel=".intelligence/packages/$name"
51
- sha="$(fetch_package "$url" "$tag" "$path" "$IP_ROOT/$rel")"
65
+ # Fetch into staging FIRST: a failed clone must leave the current install
66
+ # fully wired and intact. Only after success is the old shape unwired
67
+ # (the new version may have dropped a section dir), the store swapped,
68
+ # and the new shape wired.
69
+ staging="$IP_ROOT/.intelligence/.staging-$$"
70
+ rm -rf "$staging"
71
+ sha="$(fetch_package "$url" "$tag" "$path" "$staging")"
72
+ unwire_package_sources "$manifest" "$rel"
73
+ rm -rf "${IP_ROOT:?}/$rel"
74
+ mkdir -p "$(dirname "$IP_ROOT/$rel")"
75
+ mv "$staging" "$IP_ROOT/$rel"
52
76
  wire_package_sources "$manifest" "$name" "$rel" "$IP_ROOT"
53
77
  lock_upsert "$lock" "$name" "$range" "$url" "$path" "$tag" "$sha"
54
78
  echo " $name: ${current:-<none>} -> $tag"
55
79
  moved=$((moved + 1))
56
80
  done < <(qmap_keys "$manifest" "packages")
57
81
 
58
- [ -n "$only" ] && [ "$checked" -eq 0 ] && die "package '$only' is not in the manifest"
82
+ [ -n "$only" ] && [ "$found" -eq 0 ] && die "package '$only' is not in the manifest"
59
83
  echo "updated: $moved package(s)"
60
84
 
61
85
  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.git"
13
+ path: "packages/sync"
14
+ default_registry: "https://github.com/ainova-systems/intelligence-registry.git"
package/cli/intelligence CHANGED
@@ -3,25 +3,20 @@
3
3
  #
4
4
  # Dispatcher only: each command lives in commands/<name>.sh so commands can be
5
5
  # ported to Node one at a time without touching the others. The engine is the
6
- # vendored bash engine bundled next to the CLI (npm package) or, when running
7
- # from the intelligence-sync repo itself, the repo's own module.
6
+ # bash engine shipped next to the CLI.
8
7
 
9
8
  set -euo pipefail
10
9
 
11
10
  CLI_DIR="$(cd "$(dirname "$0")" && pwd)"
12
11
  export CLI_DIR
13
12
 
14
- # Engine discovery, by role (scripts/sync.sh + scripts/VERSION), never by
15
- # guessing names beyond the two shipped layouts:
16
- # 1. npm package: <pkg>/cli/intelligence + <pkg>/engine/scripts/
17
- # 2. this repo: <repo>/cli/intelligence + <repo>/intelligence/sync/scripts/
13
+ # Engine discovery, by role (sync.sh + VERSION). The repo and the npm package
14
+ # ship one layout — <root>/{cli,engine,packages/sync} — so there is a single
15
+ # candidate rather than a per-distribution list.
18
16
  IS_ENGINE_DIR=""
19
- for _cand in "$CLI_DIR/../engine" "$CLI_DIR/../intelligence/sync"; do
20
- if [ -f "$_cand/scripts/sync.sh" ] && [ -f "$_cand/scripts/VERSION" ]; then
21
- IS_ENGINE_DIR="$(cd "$_cand" && pwd)"
22
- break
23
- fi
24
- done
17
+ if [ -f "$CLI_DIR/../engine/sync.sh" ] && [ -f "$CLI_DIR/../engine/VERSION" ]; then
18
+ IS_ENGINE_DIR="$(cd "$CLI_DIR/../engine" && pwd)"
19
+ fi
25
20
  if [ -z "$IS_ENGINE_DIR" ]; then
26
21
  echo "ERROR: bundled sync engine not found next to the CLI ($CLI_DIR)." >&2
27
22
  echo " Reinstall: npm i -g @ainova-systems/intelligence" >&2
@@ -29,10 +24,19 @@ if [ -z "$IS_ENGINE_DIR" ]; then
29
24
  fi
30
25
  export IS_ENGINE_DIR
31
26
 
27
+ # The engine's own content, shipped beside the engine as the seed for the
28
+ # @ainova-systems/sync package at exactly this CLI's version. Separate from the
29
+ # engine dir: the store must receive rules/agents/skills, never engine scripts.
30
+ IS_BUNDLED_PKG_DIR=""
31
+ if [ -d "$CLI_DIR/../packages/sync" ]; then
32
+ IS_BUNDLED_PKG_DIR="$(cd "$CLI_DIR/../packages/sync" && pwd)"
33
+ fi
34
+ export IS_BUNDLED_PKG_DIR
35
+
32
36
  cmd="${1:-}"
33
37
  case "$cmd" in
34
38
  ""|help|-h|--help)
35
- engine_ver="$(tr -d ' \t\r\n' < "$IS_ENGINE_DIR/scripts/VERSION")"
39
+ engine_ver="$(tr -d ' \t\r\n' < "$IS_ENGINE_DIR/VERSION")"
36
40
  cat <<EOF
37
41
  intelligence — build, version and distribute AI agent intelligence
38
42
  (engine $engine_ver${INTELLIGENCE_NPM_VERSION:+, package $INTELLIGENCE_NPM_VERSION})
@@ -48,7 +52,7 @@ Usage: intelligence <command> [args]
48
52
  list Installed packages with requested/locked versions
49
53
  search [term] What the registries offer, and what this project has
50
54
  sync [target] Render intelligence to every enabled tool (.claude, AGENTS.md, ...)
51
- registry <list|add|remove> Bind a scope to a registry index
55
+ registry <list|add|remove> Manage the trusted registry list (add <repo-url>)
52
56
  migrate [--dry-run] Convert a vendored (v1) setup to the CLI setup
53
57
  doctor Check manifest, lockfile, store and engine consistency
54
58
  status Project state at a glance
@@ -57,13 +61,21 @@ EOF
57
61
  exit 0
58
62
  ;;
59
63
  version|-v|--version)
60
- engine_ver="$(tr -d ' \t\r\n' < "$IS_ENGINE_DIR/scripts/VERSION")"
64
+ engine_ver="$(tr -d ' \t\r\n' < "$IS_ENGINE_DIR/VERSION")"
61
65
  echo "${INTELLIGENCE_NPM_VERSION:-$engine_ver} (engine $engine_ver)"
62
66
  exit 0
63
67
  ;;
64
68
  esac
65
69
  shift
66
70
 
71
+ # The command word builds a path — allow exactly lowercase words, so
72
+ # `intelligence ../x` can never exec a neighbouring script.
73
+ case "$cmd" in
74
+ *[!a-z-]*|-*)
75
+ echo "ERROR: unknown command '$cmd'. Run 'intelligence help'." >&2
76
+ exit 1
77
+ ;;
78
+ esac
67
79
  cmd_file="$CLI_DIR/commands/$cmd.sh"
68
80
  if [ ! -f "$cmd_file" ]; then
69
81
  echo "ERROR: unknown command '$cmd'. Run 'intelligence help'." >&2