@ainova-systems/intelligence 0.11.0-rc.4 → 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.
Files changed (37) hide show
  1. package/cli/commands/add.sh +23 -1
  2. package/cli/commands/doctor.sh +28 -7
  3. package/cli/commands/init.sh +67 -21
  4. package/cli/commands/install.sh +55 -5
  5. package/cli/commands/migrate.sh +41 -9
  6. package/cli/commands/registry.sh +54 -35
  7. package/cli/commands/remove.sh +13 -1
  8. package/cli/commands/search.sh +12 -13
  9. package/cli/commands/status.sh +5 -4
  10. package/cli/commands/sync.sh +19 -1
  11. package/cli/commands/update.sh +23 -4
  12. package/cli/commands/upgrade.sh +50 -9
  13. package/cli/engine-package.yaml +14 -0
  14. package/cli/lib/cli-common.sh +37 -48
  15. package/cli/lib/manifest.sh +118 -0
  16. package/cli/lib/registry.sh +74 -39
  17. package/cli/lib/semver.sh +5 -2
  18. package/engine/agents/intelligence-operator.md +4 -4
  19. package/engine/docs/ADAPTERS.md +2 -1
  20. package/engine/docs/CLI.md +12 -13
  21. package/engine/docs/CONVENTIONS.md +5 -3
  22. package/engine/rules/intelligence-authoring.md +4 -4
  23. package/engine/scripts/ENGINE_SHA +1 -0
  24. package/engine/scripts/lib/common.sh +6 -2
  25. package/engine/scripts/sync.sh +1 -1
  26. package/engine/skills/intelligence-add-agent/SKILL.md +7 -7
  27. package/engine/skills/intelligence-add-rule/SKILL.md +5 -5
  28. package/engine/skills/intelligence-add-skill/SKILL.md +5 -5
  29. package/engine/skills/intelligence-extract-skill/SKILL.md +2 -2
  30. package/engine/skills/intelligence-install-adapter/SKILL.md +6 -6
  31. package/engine/skills/intelligence-learn-from-context/SKILL.md +1 -1
  32. package/engine/skills/intelligence-review-skills/SKILL.md +5 -5
  33. package/engine/skills/intelligence-sync/SKILL.md +5 -7
  34. package/engine/skills/intelligence-uninstall-adapter/SKILL.md +2 -2
  35. package/engine/skills/intelligence-update/SKILL.md +6 -0
  36. package/package.json +2 -3
  37. package/registry/index.yaml +0 -22
@@ -78,9 +78,27 @@ 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
 
94
+ # The source must exist before anything else is attempted — never die
95
+ # mid-pipeline with no message.
96
+ if ! GIT_TERMINAL_PROMPT=0 git ls-remote "$url" >/dev/null 2>&1; then
97
+ echo "ERROR: no reachable repository at $url" >&2
98
+ echo " Declared by ${RES_VIA:-the spec} — check the URL and your git access." >&2
99
+ exit 1
100
+ fi
101
+
84
102
  # Pick the version: an explicit ref pins; otherwise the highest stable tag
85
103
  # matching the range (npm's behaviour, `^highest` becoming the requested range
86
104
  # when none was given).
@@ -107,7 +125,11 @@ sha="$(fetch_package "$url" "${ref:-$resolved_tag}" "$path" "$IP_ROOT/$rel")"
107
125
  wire_package_sources "$manifest" "$name" "$rel" "$IP_ROOT"
108
126
 
109
127
  # Manifest entry: a registry package records its range; a direct one records
110
- # 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"
111
133
  if [ "$mode" = "registry" ]; then
112
134
  qmap_set "$manifest" "packages" "$name" "version" "$requested"
113
135
  else
@@ -43,14 +43,23 @@ else
43
43
  ok "sync_version $stamp matches the engine"
44
44
  fi
45
45
 
46
- if [ -f "$IP_ROOT/.intelligence/engine/.version" ]; then
47
- staged="$(tr -d ' \t\r\n' < "$IP_ROOT/.intelligence/engine/.version")"
48
- [ "$staged" = "$eng" ] && ok "engine content staged ($staged)" || warn "staged engine content is $staged, bundled is $eng — run 'intelligence install'"
49
- else
50
- warn "engine content not staged — run 'intelligence install'"
46
+ # The engine-content package must sit at the bundled engine version — the
47
+ # content documents exactly the scripts the CLI executes.
48
+ sync_locked="$(qmap_field "$lock" "packages" "$SYNC_PKG_NAME" "resolved")"
49
+ if [ -n "$sync_locked" ]; then
50
+ if [ "${sync_locked#v}" = "$eng" ]; then
51
+ ok "$SYNC_PKG_NAME at $sync_locked (matches the engine)"
52
+ else
53
+ warn "$SYNC_PKG_NAME locked at $sync_locked but the engine is $eng — run 'intelligence upgrade'"
54
+ fi
55
+ fi
56
+ if [ -d "$IP_ROOT/.intelligence/engine" ]; then
57
+ warn "stale .intelligence/engine directory (pre-package layout) — run 'intelligence upgrade'"
51
58
  fi
52
59
 
53
- # Manifest packages: names valid, locked, installed.
60
+ # Manifest packages: names valid, locked, installed — and the lock's source
61
+ # still the one resolution would pick today (a registry silently re-pointing
62
+ # an installed name is the dependency-confusion tail this check cuts off).
54
63
  while IFS= read -r name; do
55
64
  [ -n "$name" ] || continue
56
65
  assert_valid_pkg_name "$name"
@@ -60,6 +69,13 @@ while IFS= read -r name; do
60
69
  warn "$name is locked but not installed — run 'intelligence install'"
61
70
  else
62
71
  ok "$name @ $(qmap_field "$lock" "packages" "$name" "resolved")"
72
+ if [ -z "$(qmap_field "$manifest" "packages" "$name" "url")" ]; then
73
+ locked_url="$(qmap_field "$lock" "packages" "$name" "url")"
74
+ resolve_package_source "$manifest" "$name" 2>/dev/null || true
75
+ if [ -n "${RES_URL:-}" ] && [ -n "$locked_url" ] && [ "$RES_URL" != "$locked_url" ]; then
76
+ warn "$name now resolves to $RES_URL but the lock pins $locked_url — a registry re-pointed the name; verify before 'intelligence update'"
77
+ fi
78
+ fi
63
79
  fi
64
80
  done < <(qmap_keys "$manifest" "packages")
65
81
 
@@ -85,8 +101,13 @@ for section in rules agents skills; do
85
101
  esac
86
102
  if [ ! -d "$IP_ROOT/$src" ]; then
87
103
  case "$src" in
104
+ # Store content is restorable state: absent means un-installed.
88
105
  .intelligence/*) warn "sources.$section '$src' missing — run 'intelligence install'" ;;
89
- *) warn "sources.$section '$src' does not exist" ;;
106
+ # A project-owned dir that does not exist yet is a legitimate
107
+ # state — a project consuming only packages authors nothing of
108
+ # its own — and the engine simply skips it. Declared up front so
109
+ # authoring later needs no config edit.
110
+ *) ok "sources.$section '$src' — not created yet (optional)" ;;
90
111
  esac
91
112
  fi
92
113
  done < <(read_yaml_list "$manifest" "$section")
@@ -8,15 +8,21 @@
8
8
  set -euo pipefail
9
9
  source "$CLI_DIR/lib/cli-common.sh"
10
10
 
11
- targets_arg="" no_sync=0
11
+ targets_arg="" no_sync=0 content_dir="intelligence" bare=0
12
12
  while [ $# -gt 0 ]; do
13
13
  case "$1" in
14
14
  --targets) shift; targets_arg="${1:-}" ;;
15
+ --dir) shift; content_dir="${1:-}" ;;
15
16
  --no-sync) no_sync=1 ;;
17
+ --bare) bare=1 ;;
16
18
  *) die "unknown option '$1'" ;;
17
19
  esac
18
20
  shift || true
19
21
  done
22
+ [ -n "$content_dir" ] || die "--dir needs a directory name"
23
+ case "$content_dir" in
24
+ /*|*..*|.intelligence|.intelligence/*) die "--dir must be a plain directory inside the repo, and not the package store" ;;
25
+ esac
20
26
 
21
27
  detect_project
22
28
  case "$IP_MODE" in
@@ -27,37 +33,55 @@ esac
27
33
  root="$(git rev-parse --show-toplevel 2>/dev/null || pwd)"
28
34
  root="$(cd "$root" && pwd)"
29
35
 
30
- 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"
31
41
  if [ -n "$targets_arg" ]; then
32
42
  targets="agents $(printf '%s' "$targets_arg" | tr ',' ' ')"
33
43
  else
44
+ if [ -d "$root/.claude" ] || [ -f "$root/CLAUDE.md" ]; then targets="$targets claude"; fi
34
45
  [ -d "$root/.cursor" ] && targets="$targets cursor"
35
46
  [ -d "$root/.codex" ] && targets="$targets codex"
36
47
  [ -d "$root/.pi" ] && targets="$targets pi"
37
48
  [ -d "$root/.opencode" ] && targets="$targets opencode"
38
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
39
54
  fi
40
55
 
41
56
  manifest="$root/intelligence.yaml"
42
57
  {
43
58
  echo "# Intelligence manifest — what this project's AI tooling knows and where it goes."
44
- echo "# Managed by the intelligence CLI (https://github.com/ainova-systems/intelligence-sync)."
59
+ echo "# Managed by the intelligence CLI. Reference: https://github.com/ainova-systems/intelligence-sync"
45
60
  echo "project:"
46
61
  echo " name: \"$(basename "$root")\""
62
+ [ "$content_dir" = "intelligence" ] || echo " intelligence_dir: \"$content_dir\""
47
63
  echo ""
64
+ echo "# Schema version of this manifest. The CLI keeps it in step with its engine."
48
65
  echo "sync_version: \"$(bundled_engine_version)\""
49
66
  echo ""
67
+ echo "# Where content comes from. Nothing here has to exist: a directory that is"
68
+ echo "# absent is skipped, so a project consuming only packages authors nothing of"
69
+ echo "# its own. To write your own, create the directory and sync — no config edit:"
70
+ echo "# $content_dir/rules/context.md an always-on rule; describe the project,"
71
+ echo "# its stack, and the constraints to respect"
72
+ echo "# $content_dir/agents/<name>.md a persona (frontmatter: name, description,"
73
+ echo "# tier: heavy|standard|light, access: full|readonly)"
74
+ echo "# $content_dir/skills/<name>/SKILL.md a procedure invoked as /<name>"
75
+ echo "# '.intelligence/' is CLI-managed (packages + engine content) — never edit it."
50
76
  echo "sources:"
51
77
  echo " rules:"
52
- echo " - \"intelligence/rules\""
53
- echo " - \".intelligence/engine/rules\""
78
+ echo " - \"$content_dir/rules\""
54
79
  echo " agents:"
55
- echo " - \"intelligence/agents\""
56
- echo " - \".intelligence/engine/agents\""
80
+ echo " - \"$content_dir/agents\""
57
81
  echo " skills:"
58
- echo " - \"intelligence/skills\""
59
- echo " - \".intelligence/engine/skills\""
82
+ echo " - \"$content_dir/skills\""
60
83
  echo ""
84
+ echo "# Generated output, one entry per tool. Add or drop tools freely."
61
85
  echo "targets:"
62
86
  for t in $targets; do
63
87
  case "$t" in
@@ -65,18 +89,30 @@ manifest="$root/intelligence.yaml"
65
89
  *) echo " $t: { enabled: true, output: \".$t\" }" ;;
66
90
  esac
67
91
  done
92
+ echo ""
93
+ echo "# Packages are managed with 'intelligence add' / 'search' / 'list':"
94
+ echo "# intelligence add @ainova-systems/core from the registry"
95
+ echo "# intelligence add github:org/repo any repo with rules|agents|skills"
96
+ echo "# packages:"
97
+ echo "# \"@ainova-systems/core\":"
98
+ echo "# version: \"^0.3.0\""
99
+ echo ""
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
68
114
  } > "$manifest"
69
115
 
70
- mkdir -p "$root/intelligence/rules" "$root/intelligence/agents" "$root/intelligence/skills"
71
- if [ ! -f "$root/intelligence/rules/context.md" ]; then
72
- cat > "$root/intelligence/rules/context.md" <<EOF
73
- # Project Context
74
-
75
- <!-- Always-on context every AI tool receives. Describe what this project is,
76
- its stack, and the constraints an agent must respect. -->
77
- EOF
78
- fi
79
-
80
116
  # The package store is restorable state, never committed — the npm model.
81
117
  if [ ! -f "$root/.gitignore" ] || ! grep -q '^\.intelligence/' "$root/.gitignore"; then
82
118
  {
@@ -86,9 +122,19 @@ if [ ! -f "$root/.gitignore" ] || ! grep -q '^\.intelligence/' "$root/.gitignore
86
122
  } >> "$root/.gitignore"
87
123
  fi
88
124
 
89
- ensure_engine_staged "$root"
125
+ # The engine's own content (meta-skills, authoring rule, engine agents) is an
126
+ # ordinary package: auto-selected, opted out with --bare, removable later with
127
+ # 'intelligence remove @ainova-systems/sync --force'.
128
+ if [ "$bare" -eq 0 ]; then
129
+ sync_pkg_entry "$manifest"
130
+ sync_pkg_install "$root"
131
+ fi
132
+
90
133
  echo "initialized: intelligence.yaml (targets:$(printf ' %s' $targets))"
91
- echo "next: add intelligence packages — e.g. 'intelligence add @ainova-systems/core'"
134
+ [ "$bare" -eq 1 ] && echo " bare setup: no packages — engine meta-skills not installed"
135
+ echo " add packages: intelligence add @ainova-systems/core (browse: intelligence search)"
136
+ echo " write your own: create $content_dir/rules/context.md, then intelligence sync"
137
+ echo " docs: https://github.com/ainova-systems/intelligence-sync#readme"
92
138
 
93
139
  if [ "$no_sync" -eq 0 ]; then
94
140
  cd "$root"
@@ -22,10 +22,44 @@ require_v2
22
22
  manifest="$IP_ROOT/intelligence.yaml"
23
23
  lock="$IP_ROOT/intelligence.lock"
24
24
 
25
- ensure_engine_staged "$IP_ROOT"
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
26
60
 
27
61
  # Manifest packages missing from the lock: resolve them now (npm install
28
- # semantics). Under --frozen that is an error instead.
62
+ # semantics). Under --frozen the drift gate above has already refused.
29
63
  missing=""
30
64
  while IFS= read -r name; do
31
65
  [ -n "$name" ] || continue
@@ -34,8 +68,13 @@ while IFS= read -r name; do
34
68
  fi
35
69
  done < <(qmap_keys "$manifest" "packages")
36
70
  if [ -n "$missing" ]; then
37
- [ "$frozen" -eq 1 ] && die "lockfile is missing packages:$missing — run 'intelligence install' without --frozen"
38
71
  for name in $missing; do
72
+ # The engine-content package needs no remote resolution: its pin IS
73
+ # the bundled engine version and it seeds from the bundle, offline.
74
+ if [ "$name" = "$SYNC_PKG_NAME" ]; then
75
+ sync_pkg_install "$IP_ROOT"
76
+ continue
77
+ fi
39
78
  url="$(qmap_field "$manifest" "packages" "$name" "url")"
40
79
  path="$(qmap_field "$manifest" "packages" "$name" "path")"
41
80
  ref="$(qmap_field "$manifest" "packages" "$name" "ref")"
@@ -66,14 +105,25 @@ if [ -f "$lock" ]; then
66
105
  if [ -d "$IP_ROOT/$rel" ] && [ "$force" -eq 0 ]; then
67
106
  continue
68
107
  fi
69
- got="$(fetch_package "$url" "$resolved" "$path" "$IP_ROOT/$rel")"
70
- if [ -n "$sha" ] && [ "$got" != "$sha" ]; then
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")"
114
+ # An empty sha (bundle-seeded content on a dev build) means unknown,
115
+ # not moved — only two known-and-different shas are a finding.
116
+ if [ -n "$sha" ] && [ -n "$got" ] && [ "$got" != "$sha" ]; then
71
117
  if [ "$frozen" -eq 1 ]; then
118
+ rm -rf "$staging"
72
119
  die "$name: fetched $got but the lock pins $sha — the ref moved; refusing under --frozen"
73
120
  fi
74
121
  echo " WARN: $name resolved to $got, lock had $sha — updating the lock" >&2
75
122
  lock_upsert "$lock" "$name" "$requested" "$url" "$path" "$resolved" "$got"
76
123
  fi
124
+ rm -rf "${IP_ROOT:?}/$rel"
125
+ mkdir -p "$(dirname "$IP_ROOT/$rel")"
126
+ mv "$staging" "$IP_ROOT/$rel"
77
127
  wire_package_sources "$manifest" "$name" "$rel" "$IP_ROOT"
78
128
  echo "= $name@${resolved:-HEAD}"
79
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,8 +51,28 @@ 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
- stage_engine_content "$stage/.intelligence/engine"
73
+ # Engine content arrives as the @ainova-systems/sync package, seeded from the
74
+ # bundle (fetch_package's guard — no network).
75
+ sync_pkg_sha="$(fetch_package "$SYNC_PKG_URL" "v$eng" "$SYNC_PKG_PATH" "$stage/$SYNC_PKG_STORE")"
63
76
 
64
77
  # Legacy pack name -> v2 package name (@org/repo derived from the url), plus
65
78
  # per-pack source facts. Recorded as rows for the config rewrite and the lock.
@@ -117,7 +130,7 @@ rewrite_source_value() {
117
130
  local v="$1" pack name rest
118
131
  case "$v" in
119
132
  "$module_rel"/rules|"$module_rel"/agents|"$module_rel"/skills)
120
- printf '.intelligence/engine/%s' "${v##*/}"
133
+ printf '%s/%s' "$SYNC_PKG_STORE" "${v##*/}"
121
134
  return 0 ;;
122
135
  @*)
123
136
  rest="${v#@}"
@@ -168,6 +181,13 @@ while IFS="$LOCK_SEP" read -r pack name url ref mirror; do
168
181
  [ -n "$ref" ] && qmap_set "$manifest_stage" "packages" "$name" "ref" "$ref"
169
182
  done < "$pack_rows"
170
183
 
184
+ # The engine's own content rides along as a package, pinned to the engine.
185
+ qmap_set "$manifest_stage" "packages" "$SYNC_PKG_NAME" "version" "$eng"
186
+ qmap_set "$manifest_stage" "packages" "$SYNC_PKG_NAME" "url" "$SYNC_PKG_URL"
187
+ qmap_set "$manifest_stage" "packages" "$SYNC_PKG_NAME" "path" "$SYNC_PKG_PATH"
188
+ printf '%s\037%s\037%s\037%s\037%s\037%s\n' \
189
+ "$SYNC_PKG_NAME" "$eng" "$SYNC_PKG_URL" "$SYNC_PKG_PATH" "v$eng" "$sync_pkg_sha" >> "$lock_rows"
190
+
171
191
  # ---- Verify ---------------------------------------------------------------
172
192
  echo "== verifying staged state =="
173
193
  fail=0
@@ -203,6 +223,13 @@ fi
203
223
 
204
224
  # ---- Commit ---------------------------------------------------------------
205
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
206
233
  if [ ! -f "$root/.gitignore" ] || ! grep -q '^\.intelligence/' "$root/.gitignore"; then
207
234
  {
208
235
  [ -f "$root/.gitignore" ] && [ -n "$(tail -c 1 "$root/.gitignore" 2>/dev/null)" ] && echo ""
@@ -212,6 +239,11 @@ if [ ! -f "$root/.gitignore" ] || ! grep -q '^\.intelligence/' "$root/.gitignore
212
239
  fi
213
240
  rollback() {
214
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
215
247
  echo "rolled back — the vendored setup is untouched" >&2
216
248
  }
217
249
  mv "$stage/.intelligence" "$root/.intelligence"
@@ -1,8 +1,10 @@
1
1
  #!/bin/bash
2
- # intelligence registry <list | add @scope <registry-repo-url> | remove @scope>
2
+ # intelligence registry <list | add <registry-repo-url> [--force] | remove <url>>
3
3
  #
4
- # A registry binding is rare, per-scope configuration (npm's @scope:registry
5
- # analog): the URL of a git repo whose index.yaml maps names to sources.
4
+ # The registries block is a TRUST LIST: git repos holding an index.yaml, in
5
+ # trust order. Adding one is a committed, reviewable act — after it, every
6
+ # name that registry declares installs like a bundled one. No scope labels:
7
+ # names carry their own scope, and the first registry to declare a name wins.
6
8
  set -euo pipefail
7
9
  source "$CLI_DIR/lib/cli-common.sh"
8
10
 
@@ -11,54 +13,71 @@ case "$sub" in
11
13
  list)
12
14
  detect_project
13
15
  if [ "$IP_MODE" = "v2" ] && [ -f "$IP_ROOT/intelligence.yaml" ]; then
14
- echo "Project bindings (intelligence.yaml):"
16
+ echo "Project registries (intelligence.yaml, trust order):"
15
17
  found=0
16
- while IFS= read -r scope; do
17
- [ -n "$scope" ] || continue
18
+ while IFS= read -r url; do
19
+ [ -n "$url" ] || continue
18
20
  found=1
19
- echo " $scope -> $(qmap_value "$IP_ROOT/intelligence.yaml" "registries" "$scope")"
20
- done < <(qmap_keys "$IP_ROOT/intelligence.yaml" "registries")
21
+ index="$(_fetch_index "${url#git+}")"
22
+ if [ -n "$index" ]; then
23
+ n="$(qmap_keys "$index" "packages" | grep -c . || true)"
24
+ echo " $url — $n package(s)"
25
+ else
26
+ echo " $url — UNREACHABLE or no index.yaml"
27
+ fi
28
+ done < <(registries_list "$IP_ROOT/intelligence.yaml")
21
29
  [ "$found" -eq 0 ] && echo " (none)"
22
30
  fi
23
- bundled="$(default_index_file)"
24
- if [ -n "$bundled" ]; then
25
- echo "Bundled index:"
26
- while IFS= read -r name; do
27
- [ -n "$name" ] || continue
28
- echo " $name -> $(qmap_field "$bundled" "packages" "$name" "url")"
29
- done < <(qmap_keys "$bundled" "packages")
30
- fi
31
- 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>."
32
32
  ;;
33
33
  add)
34
- scope="${2:-}"; url="${3:-}"
35
- { [ -n "$scope" ] && [ -n "$url" ]; } || die "usage: intelligence registry add @scope <registry-repo-url>"
36
- case "$scope" in @*/*) die "bind a scope (@acme), not a package name" ;; @*) ;; *) die "scope must start with @" ;; esac
34
+ url="${2:-}"
35
+ force=0
36
+ [ "${3:-}" = "--force" ] && force=1
37
+ [ -n "$url" ] || die "usage: intelligence registry add <registry-repo-url> [--force]"
38
+ case "$url" in
39
+ @*) die "registries are added by URL — names carry their own scope. To install a package: intelligence add $url/<name>" ;;
40
+ esac
37
41
  require_v2
38
- # Validate at bind time, not at the first `add`: a registry is a git
39
- # repo holding index.yaml, and a URL pointing at anything else (a pack
40
- # repo, a typo) is a mistake worth hearing about now. The binding is
41
- # still written — the registry may simply not be reachable yet.
42
+ # Fail closed: a URL that cannot produce an index is not a registry,
43
+ # and recording it would turn a typo here into a confusing failure at
44
+ # the first add. --force covers the one honest case: the registry is
45
+ # not published (or reachable) yet.
42
46
  index="$(_fetch_index "${url#git+}")"
43
47
  if [ -n "$index" ]; then
44
48
  n="$(qmap_keys "$index" "packages" | grep -c . || true)"
45
- echo " index.yaml reachable — $n package(s) offered"
49
+ echo " index.yaml reachable — $n package(s):"
50
+ qmap_keys "$index" "packages" | sed 's/^/ /'
51
+ elif [ "$force" -eq 1 ]; then
52
+ echo " WARN: no index.yaml at $url — recording anyway (--force)." >&2
46
53
  else
47
- echo " WARN: no index.yaml found at $url — 'intelligence add $scope/<name>' will fail until it has one." >&2
48
- echo " A registry is a git repo with index.yaml at its root (see the README of" >&2
49
- echo " https://github.com/ainova-systems/intelligence-registry for the format)." >&2
54
+ echo "ERROR: no index.yaml found at $url — not adding it." >&2
55
+ echo " A registry is a git repo with index.yaml at its root, mapping names to sources:" >&2
56
+ echo " packages:" >&2
57
+ echo " \"@acme/backend\":" >&2
58
+ echo " url: \"https://github.com/acme/intelligence.git\"" >&2
59
+ echo " path: \"packs/backend\" # omit when the repo IS the package" >&2
60
+ echo " Format and an example: https://github.com/ainova-systems/intelligence-registry" >&2
61
+ echo " A single repository needs no registry at all: intelligence add github:org/repo" >&2
62
+ echo " Recording it ahead of a registry that does not exist yet: --force" >&2
63
+ exit 1
50
64
  fi
51
- qmap_set_value "$IP_ROOT/intelligence.yaml" "registries" "$scope" "$url"
52
- echo "$scope -> $url"
65
+ registries_add "$IP_ROOT/intelligence.yaml" "$url"
66
+ echo "added: $url"
53
67
  ;;
54
68
  remove)
55
- scope="${2:-}"
56
- [ -n "$scope" ] || die "usage: intelligence registry remove @scope"
69
+ url="${2:-}"
70
+ [ -n "$url" ] || die "usage: intelligence registry remove <url>"
57
71
  require_v2
58
- qmap_delete_key "$IP_ROOT/intelligence.yaml" "registries" "$scope"
59
- echo "unbound $scope"
72
+ registries_remove "$IP_ROOT/intelligence.yaml" "$url"
73
+ # The retired flat-map form also stored scope keys — drop one if the
74
+ # argument names it, so old manifests clean up with the old command.
75
+ case "$url" in
76
+ @*) qmap_delete_key "$IP_ROOT/intelligence.yaml" "registries" "$url" ;;
77
+ esac
78
+ echo "removed: $url"
60
79
  ;;
61
80
  *)
62
- die "usage: intelligence registry <list | add @scope <url> | remove @scope>"
81
+ die "usage: intelligence registry <list | add <url> [--force] | remove <url>>"
63
82
  ;;
64
83
  esac
@@ -6,15 +6,27 @@ source "$CLI_DIR/lib/cli-common.sh"
6
6
  name="${1:-}"
7
7
  [ -n "$name" ] || die "usage: intelligence remove <@scope/name>"
8
8
  shift || true
9
- no_sync=0
9
+ no_sync=0 force=0
10
10
  for a in "$@"; do
11
11
  case "$a" in
12
12
  --no-sync) no_sync=1 ;;
13
+ --force) force=1 ;;
13
14
  *) die "unknown option '$a'" ;;
14
15
  esac
15
16
  done
16
17
 
17
18
  require_v2
19
+
20
+ # Removing the engine-content package guts the outputs (meta-skills, the
21
+ # authoring rule, both engine agents disappear) while everything still
22
+ # reports ok — legal, but never by accident.
23
+ if [ "$name" = "$SYNC_PKG_NAME" ] && [ "$force" -eq 0 ]; then
24
+ echo "ERROR: $SYNC_PKG_NAME is the engine's own content — removing it drops every" >&2
25
+ echo " intelligence-* meta-skill, the authoring rule and the engine agents from" >&2
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
28
+ exit 1
29
+ fi
18
30
  assert_valid_pkg_name "$name"
19
31
  manifest="$IP_ROOT/intelligence.yaml"
20
32
  lock="$IP_ROOT/intelligence.lock"
@@ -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
 
@@ -33,6 +33,9 @@ pkg_state() {
33
33
 
34
34
  seen=" "
35
35
  rows=0
36
+ # emit_index <index-file> <origin-label>
37
+ # Registries are consulted in trust order; the first to declare a name wins,
38
+ # so a name already seen is not repeated (that IS the resolution order).
36
39
  emit_index() {
37
40
  local index="$1" origin="$2" name desc state
38
41
  [ -n "$index" ] && [ -f "$index" ] || return 0
@@ -56,29 +59,25 @@ emit_index() {
56
59
  }
57
60
 
58
61
  if [ -n "$manifest" ]; then
59
- while IFS= read -r scope; do
60
- [ -n "$scope" ] || continue
61
- url="$(qmap_value "$manifest" "registries" "$scope")"
62
+ while IFS= read -r url; do
62
63
  [ -n "$url" ] || continue
63
64
  index="$(_fetch_index "${url#git+}")"
64
65
  if [ -z "$index" ]; then
65
- echo " WARN: registry for $scope is unreachable or has no index.yaml: $url" >&2
66
+ echo " WARN: registry unreachable or missing index.yaml: $url" >&2
66
67
  continue
67
68
  fi
68
- emit_index "$index" "$scope -> $url"
69
- done < <(qmap_keys "$manifest" "registries")
69
+ emit_index "$index" "$url"
70
+ done < <(registries_list "$manifest")
70
71
  fi
71
- emit_index "$(default_index_file)" "bundled"
72
-
73
72
  if [ "$rows" -eq 0 ]; then
74
73
  if [ -n "$term" ]; then
75
- echo "Nothing matching '$term' in the configured registries."
74
+ echo "Nothing matching '$term' in this project's trusted registries."
76
75
  else
77
- echo "No registries hold any packages."
76
+ echo "No trusted registries (or they offer nothing) — intelligence registry add <repo-url>."
78
77
  fi
79
- 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"
80
79
  exit 0
81
80
  fi
82
81
 
83
82
  echo ""
84
- echo "add one: intelligence add <name> | private registry: intelligence registry add @scope <repo-url>"
83
+ echo "add one: intelligence add <name> | private registry: intelligence registry add <repo-url>"