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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (68) hide show
  1. package/README.md +15 -16
  2. package/cli/commands/adapter.sh +153 -0
  3. package/cli/commands/init.sh +113 -22
  4. package/cli/commands/package.sh +30 -0
  5. package/cli/commands/registry.sh +5 -2
  6. package/cli/commands/status.sh +12 -4
  7. package/cli/commands/sync.sh +8 -21
  8. package/cli/commands/update.sh +76 -61
  9. package/cli/engine-package.yaml +4 -4
  10. package/cli/intelligence +37 -27
  11. package/cli/{commands/doctor.sh → internal/check.sh} +32 -14
  12. package/cli/{commands/migrate.sh → internal/migrate-v1.sh} +161 -42
  13. package/cli/{commands/add.sh → internal/package-add.sh} +11 -10
  14. package/cli/{commands/list.sh → internal/package-list.sh} +9 -4
  15. package/cli/{commands/remove.sh → internal/package-remove.sh} +3 -3
  16. package/cli/{commands/search.sh → internal/package-search.sh} +4 -4
  17. package/cli/internal/package-update.sh +97 -0
  18. package/cli/{commands/install.sh → internal/restore.sh} +11 -4
  19. package/cli/internal/target-state.sh +57 -0
  20. package/cli/{commands/upgrade.sh → internal/upgrade-v2.sh} +48 -8
  21. package/cli/lib/cli-common.sh +167 -14
  22. package/cli/lib/lockfile.sh +15 -9
  23. package/cli/lib/manifest.sh +106 -1
  24. package/cli/lib/registry.sh +29 -15
  25. package/cli/lib/semver.sh +4 -2
  26. package/engine/ENGINE_SHA +1 -0
  27. package/engine/{scripts/adapters → adapters}/_template.sh +10 -9
  28. package/engine/{scripts/adapters → adapters}/agents.sh +20 -25
  29. package/engine/{scripts/adapters → adapters}/opencode.sh +1 -1
  30. package/engine/{scripts/lib → lib}/common.sh +29 -541
  31. package/engine/lib/contract.sh +120 -0
  32. package/engine/sync.sh +238 -0
  33. package/package.json +6 -5
  34. package/{engine → packages/sync}/agents/intelligence-architect.md +5 -3
  35. package/{engine → packages/sync}/agents/intelligence-operator.md +10 -13
  36. package/packages/sync/references/adapters.md +252 -0
  37. package/packages/sync/references/conventions.md +385 -0
  38. package/{engine → packages/sync}/rules/intelligence-authoring.md +6 -6
  39. package/{engine → packages/sync}/skills/intelligence-add-agent/SKILL.md +5 -5
  40. package/{engine → packages/sync}/skills/intelligence-add-rule/SKILL.md +3 -3
  41. package/{engine → packages/sync}/skills/intelligence-add-skill/SKILL.md +3 -3
  42. package/{engine → packages/sync}/skills/intelligence-extract-skill/SKILL.md +2 -2
  43. package/packages/sync/skills/intelligence-install-adapter/SKILL.md +38 -0
  44. package/{engine → packages/sync}/skills/intelligence-learn-from-context/SKILL.md +3 -3
  45. package/packages/sync/skills/intelligence-learn-from-repository/SKILL.md +55 -0
  46. package/{engine → packages/sync}/skills/intelligence-review-skills/SKILL.md +6 -6
  47. package/packages/sync/skills/intelligence-sync/SKILL.md +20 -0
  48. package/packages/sync/skills/intelligence-uninstall-adapter/SKILL.md +24 -0
  49. package/packages/sync/skills/intelligence-update/SKILL.md +43 -0
  50. package/engine/INIT.md +0 -500
  51. package/engine/docs/ADAPTERS.md +0 -214
  52. package/engine/docs/CLI.md +0 -90
  53. package/engine/docs/CONVENTIONS.md +0 -456
  54. package/engine/scripts/ENGINE_SHA +0 -1
  55. package/engine/scripts/lib/layout.sh +0 -51
  56. package/engine/scripts/lib/migrations.sh +0 -708
  57. package/engine/scripts/sync.sh +0 -311
  58. package/engine/scripts/update.sh +0 -237
  59. package/engine/skills/intelligence-install-adapter/SKILL.md +0 -31
  60. package/engine/skills/intelligence-sync/SKILL.md +0 -16
  61. package/engine/skills/intelligence-uninstall-adapter/SKILL.md +0 -42
  62. package/engine/skills/intelligence-update/SKILL.md +0 -165
  63. /package/engine/{scripts/VERSION → VERSION} +0 -0
  64. /package/engine/{scripts/adapters → adapters}/claude.sh +0 -0
  65. /package/engine/{scripts/adapters → adapters}/codex.sh +0 -0
  66. /package/engine/{scripts/adapters → adapters}/copilot.sh +0 -0
  67. /package/engine/{scripts/adapters → adapters}/cursor.sh +0 -0
  68. /package/engine/{scripts/adapters → adapters}/pi.sh +0 -0
@@ -1,9 +1,9 @@
1
1
  #!/bin/bash
2
- # intelligence migrate [--dry-run] [--force] — convert a vendored (v1) setup
2
+ # Internal v1-to-v2 migration operation.
3
3
  # to the CLI setup: root intelligence.yaml, .intelligence/ store,
4
4
  # intelligence.lock, no vendored engine.
5
5
  #
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.
@@ -24,7 +24,7 @@ done
24
24
  detect_project
25
25
  case "$IP_MODE" in
26
26
  legacy) ;;
27
- v2) die "already on the CLI setup ($IP_ROOT/intelligence.yaml)" ;;
27
+ v2) die "already on the v2 setup ($IP_ROOT/intelligence.yaml)" ;;
28
28
  *) die "no vendored setup found here — 'intelligence init' starts a fresh project" ;;
29
29
  esac
30
30
  root="$IP_ROOT"
@@ -32,41 +32,101 @@ umbrella="$IP_UMBRELLA"
32
32
  module_dir="$IP_MODULE_DIR"
33
33
  config="$umbrella/config.yaml"
34
34
  module_rel="${module_dir#"$root"/}"
35
+ umbrella_rel="${umbrella#"$root"/}"
35
36
 
36
- [ -f "$root/intelligence.yaml" ] && die "intelligence.yaml already exists at the root — half-migrated state; resolve it manually"
37
+ for existing in intelligence.yaml intelligence.lock .intelligence; do
38
+ if [ -e "$root/$existing" ] || [ -L "$root/$existing" ]; then
39
+ die "$existing already exists at the root — conflicting v2 state; move it aside or restore the project before migrating"
40
+ fi
41
+ done
37
42
  if [ "$dry_run" -eq 0 ] && [ "$force" -eq 0 ]; then
38
43
  if [ -n "$(git -C "$root" status --porcelain 2>/dev/null)" ]; then
39
44
  die "working tree is not clean — commit or stash first (or --force); migrate wants a one-commit diff you can review and revert"
40
45
  fi
41
46
  fi
42
47
 
43
- stamp="$(read_engine_stamp "$config")"
48
+ # `sync_version` belongs to the archived v1 format. The v2 contract uses
49
+ # `schema_version`, so conversion reads the legacy scalar explicitly.
50
+ stamp="$(top_scalar "$config" "sync_version")"
44
51
  eng="$(bundled_engine_version)"
45
52
  [ -n "$stamp" ] && _ver_gt "$stamp" "$eng" && die "project schema $stamp is newer than this CLI's engine $eng — update the CLI first"
46
53
 
54
+ # The v1 `packs:` block is the one v1 shape migrate still has to read, and the
55
+ # engine no longer knows it — the reader lives here, with its only consumer.
56
+ get_pack_field() {
57
+ get_nested_yaml_value "$1" "packs" "$2" "$3"
58
+ }
59
+
60
+ paths_overlap() {
61
+ case "$1" in "$2"|"$2"/*) return 0 ;; esac
62
+ case "$2" in "$1"|"$1"/*) return 0 ;; esac
63
+ return 1
64
+ }
65
+
66
+ validate_mirror_path() {
67
+ local pack="$1" mirror="$2" canon rel src src_canon src_rel target output protected mirror_phys
68
+ [ -n "$mirror" ] || return 0
69
+ case "$mirror" in
70
+ .|/*|*..*|*\\*|[A-Za-z]:*) die "pack '$pack' has unsafe mirror path '$mirror'" ;;
71
+ esac
72
+ canon="$(normalize_path "$root/$mirror")"
73
+ case "$canon" in "$root"/*) ;; *) die "pack '$pack' mirror escapes the repository: '$mirror'" ;; esac
74
+ rel="${canon#"$root"/}"
75
+
76
+ # A v1 mirror may live below the content directory (the normal location is
77
+ # intelligence/external/<pack>), but it must never own the directory itself,
78
+ # the vendored module, CLI state, authored sources, or generated outputs.
79
+ case "$umbrella_rel" in "$rel"|"$rel"/*) die "pack '$pack' mirror '$mirror' contains the v1 content directory" ;; esac
80
+ for protected in .git .intelligence "$module_rel"; do
81
+ paths_overlap "$rel" "$protected" && die "pack '$pack' mirror '$mirror' overlaps protected path '$protected'"
82
+ done
83
+ for section in rules agents skills; do
84
+ while IFS= read -r src; do
85
+ case "$src" in ""|git+*|@*) continue ;; esac
86
+ src_canon="$(normalize_path "$root/$src")"
87
+ case "$src_canon" in "$root"/*) src_rel="${src_canon#"$root"/}" ;; *) continue ;; esac
88
+ paths_overlap "$rel" "$src_rel" \
89
+ && die "pack '$pack' mirror '$mirror' overlaps configured source '$src'"
90
+ done < <(read_yaml_list "$config" "$section")
91
+ done
92
+ while IFS= read -r target; do
93
+ [ -n "$target" ] || continue
94
+ output="$(get_target_output "$config" "$target")"
95
+ [ -n "$output" ] || continue
96
+ case "$output" in /*|*..*) continue ;; esac
97
+ paths_overlap "$rel" "${output%/}" \
98
+ && die "pack '$pack' mirror '$mirror' overlaps target '$target' output '$output'"
99
+ done < <(read_yaml_keys "$config" "targets")
100
+
101
+ if [ -d "$canon" ]; then
102
+ mirror_phys="$(cd "$canon" && pwd -P)"
103
+ case "$mirror_phys" in "$root"|"$root"/*) ;; *) die "pack '$pack' mirror resolves outside the repository: '$mirror'" ;; esac
104
+ [ -f "$canon/.pack" ] || die "pack '$pack' mirror '$mirror' has no .pack ownership stamp — refusing to copy or delete project-owned content"
105
+ fi
106
+ }
107
+
47
108
  # ---- Stage ----------------------------------------------------------------
48
- stage="$root/.intelligence-migrate-stage.$$"
49
- [ "$dry_run" -eq 1 ] && stage="$(mktemp -d -t intelligence-migrate-XXXXXX 2>/dev/null || mktemp -d)"
109
+ if [ "$dry_run" -eq 1 ]; then
110
+ stage="$(mktemp -d -t intelligence-migrate-XXXXXX 2>/dev/null || mktemp -d)"
111
+ else
112
+ stage="$(mktemp -d "$root/.intelligence-migrate-stage.XXXXXX")"
113
+ fi
50
114
  cleanup_stage() { rm -rf "$stage"; }
51
115
  trap cleanup_stage EXIT INT TERM
52
116
  mkdir -p "$stage/.intelligence/packages"
53
117
 
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
118
+ # v2 carries no v1 migration chain: v1 is archived, and the archived engine is
119
+ # what knows how to walk an old project forward. So migrate accepts only a
120
+ # project already at the final v1 schema and names the exact remedy otherwise —
121
+ # a remedy that keeps working indefinitely, because the archive stays at the
122
+ # URL the project's own update.sh already clones.
123
+ V1_FINAL_SCHEMA="0.10.0"
124
+ if [ -z "$stamp" ] || _ver_gt "$V1_FINAL_SCHEMA" "$stamp"; then
125
+ echo "ERROR: project schema is ${stamp:-absent (pre-0.3.1)}, older than the final v1 schema $V1_FINAL_SCHEMA." >&2
126
+ echo " Bring it forward with its own engine first — that still works:" >&2
127
+ echo " bash $module_rel/scripts/update.sh --yes" >&2
128
+ echo " then re-run: intelligence init" >&2
129
+ exit 1
70
130
  fi
71
131
 
72
132
  echo "== staging =="
@@ -78,12 +138,22 @@ sync_pkg_sha="$(fetch_package "$SYNC_PKG_URL" "v$eng" "$SYNC_PKG_PATH" "$stage/$
78
138
  # per-pack source facts. Recorded as rows for the config rewrite and the lock.
79
139
  pack_rows="$stage/packs.rows"
80
140
  : > "$pack_rows"
141
+ seen_names=" "
142
+ seen_mirrors=" "
81
143
  while IFS= read -r pack; do
82
144
  [ -n "$pack" ] || continue
83
145
  url="$(get_pack_field "$config" "$pack" "url")"
84
146
  ref="$(get_pack_field "$config" "$pack" "ref")"
85
147
  mirror="$(get_pack_field "$config" "$pack" "mirror")"
86
148
  [ -n "$url" ] || die "pack '$pack' has no url in config.yaml"
149
+ assert_safe_source_url "$url"
150
+ assert_safe_ref "$ref"
151
+ validate_mirror_path "$pack" "$mirror"
152
+ if [ -n "$mirror" ]; then
153
+ mirror_key="$(normalize_path "$root/$mirror")"
154
+ case "$seen_mirrors" in *" $mirror_key "*) die "pack '$pack' shares mirror '$mirror' with another pack" ;; esac
155
+ seen_mirrors="$seen_mirrors$mirror_key "
156
+ fi
87
157
  base="${url%.git}"; base="${base%/}"
88
158
  repo="$(basename "$base")"
89
159
  org="$(basename "$(dirname "$base")")"
@@ -91,6 +161,12 @@ while IFS= read -r pack; do
91
161
  case "$repo" in ""|*[!A-Za-z0-9._-]*) repo="$(printf '%s' "$repo" | tr -c 'A-Za-z0-9._-' '-')" ;; esac
92
162
  name="@$org/$repo"
93
163
  assert_valid_pkg_name "$name"
164
+ # Two packs deriving the same @org/repo would overwrite one store dir and
165
+ # duplicate the lock key — fail closed, the user renames a source.
166
+ case "$seen_names" in
167
+ *" $name "*) die "packs '$pack' and another both derive package name '$name' — sources must map to distinct names" ;;
168
+ esac
169
+ seen_names="$seen_names $name "
94
170
  printf '%s\n' "$pack$LOCK_SEP$name$LOCK_SEP$url$LOCK_SEP$ref$LOCK_SEP$mirror"
95
171
  done < <(read_yaml_keys "$config" "packs") >> "$pack_rows"
96
172
 
@@ -116,7 +192,7 @@ while IFS="$LOCK_SEP" read -r pack name url ref mirror; do
116
192
  echo " pack '$pack' -> $name (fetched)"
117
193
  fi
118
194
  if [ -z "$sha" ] || [ "$sha" = "unknown" ]; then
119
- sha="$(GIT_TERMINAL_PROMPT=0 git ls-remote "$url" "$ref" 2>/dev/null | awk '{print $1; exit}')"
195
+ sha="$(GIT_TERMINAL_PROMPT=0 git ls-remote -- "$url" "$ref" 2>/dev/null | awk '{print $1; exit}')"
120
196
  fi
121
197
  printf '%s\037%s\037%s\037%s\037%s\037%s\n' \
122
198
  "$name" "" "$url" "" "$ref" "$sha" >> "$lock_rows"
@@ -164,15 +240,26 @@ while IFS= read -r line || [ -n "$line" ]; do
164
240
  *"- "*)
165
241
  indent="${line%%-*}"
166
242
  v="${line#*- }"
243
+ # A trailing inline comment survives the rewrite — split it
244
+ # off before touching quotes, or it would be mangled into the
245
+ # value.
246
+ trail=""
247
+ case "$v" in
248
+ *[[:space:]]"#"*)
249
+ trail=" #${v#*[[:space:]]#}"
250
+ v="${v%%[[:space:]]#*}"
251
+ ;;
252
+ esac
253
+ v="${v%"${v##*[![:space:]]}"}"
167
254
  v="${v%\"}"; v="${v#\"}"; v="${v%\'}"; v="${v#\'}"
168
- echo "$indent- \"$(rewrite_source_value "$v")\""
255
+ echo "$indent- \"$(rewrite_source_value "$v")\"$trail"
169
256
  continue
170
257
  ;;
171
258
  esac
172
259
  fi
173
260
  echo "$line"
174
261
  done < "$config" > "$manifest_stage"
175
- stamp_version "$manifest_stage" "$eng"
262
+ stamp_schema_version "$manifest_stage" "$eng"
176
263
 
177
264
  # packages: entries (pin preserved — no invented ranges).
178
265
  while IFS="$LOCK_SEP" read -r pack name url ref mirror; do
@@ -201,14 +288,24 @@ for section in rules agents skills; do
201
288
  esac
202
289
  done < <(read_yaml_list "$manifest_stage" "$section")
203
290
  done
204
- for adapter in agents claude cursor copilot codex pi opencode; do
291
+ target_keys() {
292
+ awk '
293
+ /^targets:[[:space:]]*$/ { in_targets=1; next }
294
+ in_targets && /^[^[:space:]#]/ { exit }
295
+ in_targets && /^ [A-Za-z_][A-Za-z0-9_]*:/ {
296
+ line=$0; sub(/^ /, "", line); sub(/:.*/, "", line); print line
297
+ }
298
+ ' "$1"
299
+ }
300
+ while IFS= read -r adapter; do
301
+ [ -n "$adapter" ] || continue
205
302
  old_e="$(is_target_enabled "$config" "$adapter")"
206
303
  new_e="$(is_target_enabled "$manifest_stage" "$adapter")"
207
304
  [ "$old_e" = "$new_e" ] || { echo " TARGET DRIFT: $adapter enabled '$old_e' -> '$new_e'"; fail=1; }
208
305
  old_o="$(get_target_output "$config" "$adapter")"
209
306
  new_o="$(get_target_output "$manifest_stage" "$adapter")"
210
307
  [ "$old_o" = "$new_o" ] || { echo " TARGET DRIFT: $adapter output '$old_o' -> '$new_o'"; fail=1; }
211
- done
308
+ done < <({ target_keys "$config"; target_keys "$manifest_stage"; } | sort -u)
212
309
  [ "$fail" -eq 0 ] || die "staged state failed verification — nothing changed"
213
310
 
214
311
  if [ "$dry_run" -eq 1 ]; then
@@ -230,45 +327,66 @@ if [ -f "$root/.gitignore" ]; then
230
327
  gitignore_existed=1
231
328
  cp "$root/.gitignore" "$stage/gitignore.orig"
232
329
  fi
233
- if [ ! -f "$root/.gitignore" ] || ! grep -q '^\.intelligence/' "$root/.gitignore"; then
234
- {
235
- [ -f "$root/.gitignore" ] && [ -n "$(tail -c 1 "$root/.gitignore" 2>/dev/null)" ] && echo ""
236
- echo "# intelligence CLI package store (restored by 'intelligence install')"
237
- echo ".intelligence/"
238
- } >> "$root/.gitignore"
239
- fi
330
+ commit_active=1
240
331
  rollback() {
332
+ [ "$commit_active" -eq 1 ] || return 0
333
+ commit_active=0
334
+ set +e
241
335
  rm -rf "$root/.intelligence" "$root/intelligence.yaml" "$root/intelligence.lock"
242
336
  if [ "$gitignore_existed" -eq 1 ]; then
243
337
  cp "$stage/gitignore.orig" "$root/.gitignore"
244
338
  else
245
339
  rm -f "$root/.gitignore"
246
340
  fi
341
+ rm -rf "$stage"
247
342
  echo "rolled back — the vendored setup is untouched" >&2
248
343
  }
344
+ rollback_on_exit() {
345
+ local rc=$?
346
+ rollback
347
+ exit "$rc"
348
+ }
349
+ # Every live write through verified sync is covered by rollback, including a
350
+ # plain set -e/EXIT failure rather than only an explicit sync refusal.
351
+ trap rollback_on_exit EXIT
352
+ trap 'rollback; exit 130' INT TERM
353
+ if [ ! -f "$root/.gitignore" ] || ! grep -q '^\.intelligence/' "$root/.gitignore"; then
354
+ {
355
+ [ -f "$root/.gitignore" ] && [ -n "$(tail -c 1 "$root/.gitignore" 2>/dev/null)" ] && echo ""
356
+ echo "# intelligence CLI package store (restored automatically by 'intelligence sync')"
357
+ echo ".intelligence/"
358
+ } >> "$root/.gitignore"
359
+ fi
249
360
  mv "$stage/.intelligence" "$root/.intelligence"
250
361
  cp "$manifest_stage" "$root/intelligence.yaml"
251
362
  lock_write_from_tsv "$root/intelligence.lock" "$lock_rows"
252
363
 
253
364
  sync_out="$(cd "$root" && IS_SUPPRESS_CLI_NOTE=1 bash "$CLI_DIR/commands/sync.sh" 2>&1)" || {
254
365
  echo "$sync_out"
255
- rollback
256
- die "sync of the migrated state failed — rolled back"
366
+ die "sync of the migrated state failed"
257
367
  }
258
368
  echo "$sync_out" | grep -q '^IS_STATUS=ok' || {
259
369
  echo "$sync_out"
260
- rollback
261
- die "sync of the migrated state did not report ok — rolled back"
370
+ die "sync of the migrated state did not report ok"
262
371
  }
372
+ # Point of no return: the new state is verified. From here an interrupt must
373
+ # NOT roll back (the old setup is being removed) — only clean the stage.
374
+ commit_active=0
375
+ trap cleanup_stage EXIT INT TERM
263
376
 
264
377
  # Only now is anything of the old setup removed — and config.yaml is kept as
265
378
  # a reviewable backup inside the store.
266
379
  mkdir -p "$root/.intelligence/backup"
267
380
  cp "$config" "$root/.intelligence/backup/config.yaml"
268
381
  while IFS="$LOCK_SEP" read -r pack name url ref mirror; do
269
- if [ -n "$mirror" ] && [ -d "$root/$mirror" ]; then
382
+ # Containment before deletion: a mirror value is config input, and rm -rf
383
+ # must never follow it out of the repository.
384
+ case "$mirror" in
385
+ ""|/*|*..*) [ -n "$mirror" ] && echo " WARN: mirror '$mirror' not repo-contained — left in place" >&2; continue ;;
386
+ esac
387
+ if [ -d "$root/$mirror" ]; then
270
388
  rm -rf "${root:?}/$mirror"
271
- # An emptied mirror parent (e.g. <umbrella>/external/) goes with it.
389
+ # An emptied v1 mirror parent (for example intelligence/external/) goes with it.
272
390
  rmdir "$(dirname "$root/$mirror")" 2>/dev/null || true
273
391
  fi
274
392
  done < "$pack_rows"
@@ -280,4 +398,5 @@ rmdir "$umbrella" 2>/dev/null || true
280
398
 
281
399
  echo ""
282
400
  echo "migrated. Review the diff, then commit. The old config.yaml is kept at .intelligence/backup/config.yaml."
283
- echo "From now on: intelligence sync | add | install | doctor."
401
+ echo "From now on: intelligence sync | package | update | status."
402
+ echo "Next: ask your agent to run /intelligence-learn-from-repository to review the migrated project context."
@@ -1,15 +1,15 @@
1
1
  #!/bin/bash
2
- # intelligence add <spec> [--name @scope/name] [--no-sync]
2
+ # Internal package add operation.
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
 
11
11
  spec="${1:-}"
12
- [ -n "$spec" ] || die "usage: intelligence add <@scope/name[@range] | github:org/repo[#path] | git+<url>[@ref][#path]>"
12
+ [ -n "$spec" ] || die "usage: intelligence package add <@scope/name[@range] | github:org/repo[#path] | git+<url>[@ref][#path]>"
13
13
  shift || true
14
14
 
15
15
  no_sync=0
@@ -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
  ;;
@@ -84,7 +85,7 @@ if [ "$mode" = "registry" ]; then
84
85
  echo " ('intelligence registry list' shows them). Either add its registry:" >&2
85
86
  echo " intelligence registry add <registry-repo-url>" >&2
86
87
  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
+ echo " intelligence package add github:org/repo | intelligence package add git+<url>[@ref][#path]" >&2
88
89
  suggest_similar "$manifest" "$name"
89
90
  exit 1
90
91
  fi
@@ -93,7 +94,7 @@ fi
93
94
 
94
95
  # The source must exist before anything else is attempted — never die
95
96
  # mid-pipeline with no message.
96
- if ! GIT_TERMINAL_PROMPT=0 git ls-remote "$url" >/dev/null 2>&1; then
97
+ if ! GIT_TERMINAL_PROMPT=0 git ls-remote -- "$url" >/dev/null 2>&1; then
97
98
  echo "ERROR: no reachable repository at $url" >&2
98
99
  echo " Declared by ${RES_VIA:-the spec} — check the URL and your git access." >&2
99
100
  exit 1
@@ -1,5 +1,5 @@
1
1
  #!/bin/bash
2
- # intelligence list — packages with requested / locked / installed state.
2
+ # Internal package list operation.
3
3
  set -euo pipefail
4
4
  source "$CLI_DIR/lib/cli-common.sh"
5
5
 
@@ -14,8 +14,13 @@ while IFS= read -r name; do
14
14
  range="$(qmap_field "$manifest" "packages" "$name" "version")"
15
15
  ref="$(qmap_field "$manifest" "packages" "$name" "ref")"
16
16
  resolved="$(qmap_field "$lock" "packages" "$name" "resolved")"
17
- state="missing — run 'intelligence install'"
18
- [ -d "$IP_ROOT/.intelligence/packages/$name" ] && state="installed"
17
+ if [ ! -f "$lock" ]; then
18
+ state="lock missing — restore committed intelligence.lock"
19
+ elif [ -d "$IP_ROOT/.intelligence/packages/$name" ]; then
20
+ state="installed"
21
+ else
22
+ state="store missing — run 'intelligence sync'"
23
+ fi
19
24
  printf '%s %s locked:%s %s\n' \
20
25
  "$name" \
21
26
  "${ref:+ref:$ref}${ref:-${range:-*}}" \
@@ -24,5 +29,5 @@ while IFS= read -r name; do
24
29
  done < <(qmap_keys "$manifest" "packages")
25
30
 
26
31
  if [ "$count" -eq 0 ]; then
27
- echo "No packages. Add one: intelligence add @ainova-systems/core"
32
+ echo "No packages. Add one: intelligence package add @ainova-systems/core"
28
33
  fi
@@ -1,10 +1,10 @@
1
1
  #!/bin/bash
2
- # intelligence remove <@scope/name> [--no-sync]
2
+ # Internal package remove operation.
3
3
  set -euo pipefail
4
4
  source "$CLI_DIR/lib/cli-common.sh"
5
5
 
6
6
  name="${1:-}"
7
- [ -n "$name" ] || die "usage: intelligence remove <@scope/name>"
7
+ [ -n "$name" ] || die "usage: intelligence package remove <@scope/name>"
8
8
  shift || true
9
9
  no_sync=0 force=0
10
10
  for a in "$@"; do
@@ -24,7 +24,7 @@ if [ "$name" = "$SYNC_PKG_NAME" ] && [ "$force" -eq 0 ]; then
24
24
  echo "ERROR: $SYNC_PKG_NAME is the engine's own content — removing it drops every" >&2
25
25
  echo " intelligence-* meta-skill, the authoring rule and the engine agents from" >&2
26
26
  echo " the outputs. The sync engine itself keeps working (it ships with the CLI)." >&2
27
- echo " If that is what you want: intelligence remove $SYNC_PKG_NAME --force" >&2
27
+ echo " If that is what you want: intelligence package remove $SYNC_PKG_NAME --force" >&2
28
28
  exit 1
29
29
  fi
30
30
  assert_valid_pkg_name "$name"
@@ -1,5 +1,5 @@
1
1
  #!/bin/bash
2
- # intelligence search [term] — what the registries offer, and what this project
2
+ # Internal package search operation: what the registries offer and what this project
3
3
  # already has. The catalogue view `list` deliberately is not: `list` answers
4
4
  # "what does this project depend on", `search` answers "what could it".
5
5
  #
@@ -27,7 +27,7 @@ pkg_state() {
27
27
  elif [ -d "$IP_ROOT/.intelligence/packages/$name" ]; then
28
28
  printf 'installed %s' "$(qmap_field "$IP_ROOT/intelligence.lock" "packages" "$name" "resolved")"
29
29
  else
30
- printf 'declared (run install)'
30
+ printf 'declared (run sync)'
31
31
  fi
32
32
  }
33
33
 
@@ -75,9 +75,9 @@ if [ "$rows" -eq 0 ]; then
75
75
  else
76
76
  echo "No trusted registries (or they offer nothing) — intelligence registry add <repo-url>."
77
77
  fi
78
- echo "Explicit installs need no registry: intelligence add github:org/repo"
78
+ echo "Explicit installs need no registry: intelligence package add github:org/repo"
79
79
  exit 0
80
80
  fi
81
81
 
82
82
  echo ""
83
- echo "add one: intelligence add <name> | private registry: intelligence registry add <repo-url>"
83
+ echo "add one: intelligence package add <name> | private registry: intelligence registry add <repo-url>"
@@ -0,0 +1,97 @@
1
+ #!/bin/bash
2
+ # Internal package range update operation.
3
+ #
4
+ # Re-resolve version ranges against the remotes and rewrite the lock. Pinned
5
+ # packages (`ref:`) never move here — repin by editing the manifest or
6
+ # re-adding. `update <name>` limits the pass to one package.
7
+ set -euo pipefail
8
+ source "$CLI_DIR/lib/cli-common.sh"
9
+
10
+ only="" no_sync=0 preview=0
11
+ while [ $# -gt 0 ]; do
12
+ case "$1" in
13
+ --no-sync) no_sync=1 ;;
14
+ --preview) preview=1 ;;
15
+ @*) only="$1" ;;
16
+ *) die "unknown argument '$1'" ;;
17
+ esac
18
+ shift || true
19
+ done
20
+
21
+ require_v2
22
+ manifest="$IP_ROOT/intelligence.yaml"
23
+ lock="$IP_ROOT/intelligence.lock"
24
+
25
+ moved=0 found=0
26
+ while IFS= read -r name; do
27
+ [ -n "$name" ] || continue
28
+ # Manifest keys are untrusted input on their way into store paths.
29
+ assert_valid_pkg_name "$name"
30
+ [ -n "$only" ] && [ "$name" != "$only" ] && continue
31
+ # A named package that exists is "found" whatever happens next — a skip
32
+ # message followed by "not in the manifest" would contradict itself.
33
+ found=1
34
+ if [ "$name" = "$SYNC_PKG_NAME" ]; then
35
+ echo " $name: engine content follows the installed CLI — skipped"
36
+ continue
37
+ fi
38
+ ref="$(qmap_field "$manifest" "packages" "$name" "ref")"
39
+ [ -n "$ref" ] && { echo " $name: pinned to ref '$ref' — skipped"; continue; }
40
+ range="$(qmap_field "$manifest" "packages" "$name" "version")"
41
+ # An exact version is a pin, same as ref: — update never moves pins.
42
+ if [ -n "$range" ] && semver_is_stable "$range"; then
43
+ echo " $name: pinned to $range — edit the manifest to move it"
44
+ continue
45
+ fi
46
+ url="$(qmap_field "$manifest" "packages" "$name" "url")"
47
+ path="$(qmap_field "$manifest" "packages" "$name" "path")"
48
+ if [ -z "$url" ]; then
49
+ resolve_package_source "$manifest" "$name"
50
+ url="$RES_URL"; path="$RES_PATH"
51
+ fi
52
+ picked="$(list_remote_versions "$url" | semver_pick_highest "$range")"
53
+ [ -n "$picked" ] || { echo " $name: nothing satisfies '${range:-*}' at $url" >&2; continue; }
54
+ read -r tag _ <<< "$(remote_tag_for_version "$url" "$picked")"
55
+ current="$(qmap_field "$lock" "packages" "$name" "resolved")"
56
+ cur_url="$(qmap_field "$lock" "packages" "$name" "url")"
57
+ cur_path="$(qmap_field "$lock" "packages" "$name" "path")"
58
+ # Same tag is not enough: a registry may re-point a name's source while
59
+ # keeping the version — the move doctor warns about must be actionable
60
+ # here, so url/path changes refetch too.
61
+ if [ "$tag" = "$current" ] && [ "$url" = "$cur_url" ] && [ "$path" = "$cur_path" ]; then
62
+ echo " $name: ${current:-<none>} (up to date)"
63
+ continue
64
+ fi
65
+ if [ "$preview" -eq 1 ]; then
66
+ echo " $name: ${current:-<none>} -> $tag"
67
+ moved=$((moved + 1))
68
+ continue
69
+ fi
70
+ rel=".intelligence/packages/$name"
71
+ # Fetch into staging FIRST: a failed clone must leave the current install
72
+ # fully wired and intact. Only after success is the old shape unwired
73
+ # (the new version may have dropped a section dir), the store swapped,
74
+ # and the new shape wired.
75
+ staging="$IP_ROOT/.intelligence/.staging-$$"
76
+ rm -rf "$staging"
77
+ sha="$(fetch_package "$url" "$tag" "$path" "$staging")"
78
+ unwire_package_sources "$manifest" "$rel"
79
+ rm -rf "${IP_ROOT:?}/$rel"
80
+ mkdir -p "$(dirname "$IP_ROOT/$rel")"
81
+ mv "$staging" "$IP_ROOT/$rel"
82
+ wire_package_sources "$manifest" "$name" "$rel" "$IP_ROOT"
83
+ lock_upsert "$lock" "$name" "$range" "$url" "$path" "$tag" "$sha"
84
+ echo " $name: ${current:-<none>} -> $tag"
85
+ moved=$((moved + 1))
86
+ done < <(qmap_keys "$manifest" "packages")
87
+
88
+ [ -n "$only" ] && [ "$found" -eq 0 ] && die "package '$only' is not in the manifest"
89
+ if [ "$preview" -eq 1 ]; then
90
+ echo "updates available: $moved package(s)"
91
+ else
92
+ echo "updated: $moved package(s)"
93
+ fi
94
+
95
+ if [ "$preview" -eq 0 ] && [ "$moved" -gt 0 ] && [ "$no_sync" -eq 0 ]; then
96
+ exec bash "$CLI_DIR/commands/sync.sh"
97
+ fi
@@ -1,5 +1,5 @@
1
1
  #!/bin/bash
2
- # intelligence install [--frozen] [--force] [--no-sync]
2
+ # Internal locked-store restore operation.
3
3
  #
4
4
  # Restore .intelligence/ from intelligence.lock — the fresh-clone command.
5
5
  # --frozen additionally refuses any manifest/lock divergence (CI mode) and
@@ -30,7 +30,7 @@ if [ "$frozen" -eq 1 ]; then
30
30
  while IFS= read -r name; do
31
31
  [ -n "$name" ] || continue
32
32
  [ -n "$(qmap_field "$lock" "packages" "$name" "url")" ] \
33
- || die "--frozen: $name is in the manifest but not in intelligence.lock"
33
+ || die "locked restore: $name is in the manifest but not in intelligence.lock"
34
34
  m_ver="$(qmap_field "$manifest" "packages" "$name" "version")"
35
35
  m_ref="$(qmap_field "$manifest" "packages" "$name" "ref")"
36
36
  m_url="$(qmap_field "$manifest" "packages" "$name" "url")"
@@ -40,7 +40,7 @@ if [ "$frozen" -eq 1 ]; then
40
40
  l_url="$(qmap_field "$lock" "packages" "$name" "url")"
41
41
  l_path="$(qmap_field "$lock" "packages" "$name" "path")"
42
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"
43
+ || die "locked restore: $name requests '$m_ver' but the lock recorded '$l_req' — reconcile the manifest and lock"
44
44
  [ -z "$m_ref" ] || [ "$m_ref" = "$l_res" ] \
45
45
  || die "--frozen: $name pins ref '$m_ref' but the lock resolved '$l_res'"
46
46
  [ -z "$m_url" ] || [ "$m_url" = "$l_url" ] \
@@ -63,6 +63,7 @@ fi
63
63
  missing=""
64
64
  while IFS= read -r name; do
65
65
  [ -n "$name" ] || continue
66
+ assert_valid_pkg_name "$name"
66
67
  if [ ! -f "$lock" ] || [ -z "$(qmap_field "$lock" "packages" "$name" "url")" ]; then
67
68
  missing="$missing $name"
68
69
  fi
@@ -101,8 +102,14 @@ fi
101
102
  if [ -f "$lock" ]; then
102
103
  while IFS="$LOCK_SEP" read -r name requested url path resolved sha; do
103
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"
104
108
  rel=".intelligence/packages/$name"
105
- 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
106
113
  continue
107
114
  fi
108
115
  # Fetch into staging and commit to the store only after the integrity