@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
@@ -13,12 +13,13 @@ case "$IP_MODE" in
13
13
  else
14
14
  echo "Lockfile: none (no packages added yet)"
15
15
  fi
16
- if [ -f "$IP_ROOT/.intelligence/engine/.version" ]; then
17
- echo "Store: .intelligence/engine at $(tr -d ' \t\r\n' < "$IP_ROOT/.intelligence/engine/.version")"
16
+ sync_locked="$(qmap_field "$IP_ROOT/intelligence.lock" "packages" "$SYNC_PKG_NAME" "resolved")"
17
+ if [ -n "$sync_locked" ]; then
18
+ echo "Content: $SYNC_PKG_NAME at $sync_locked"
18
19
  else
19
- echo "Store: not staged — run 'intelligence install'"
20
+ echo "Content: no $SYNC_PKG_NAME (bare setup)"
20
21
  fi
21
- echo "Engine: $(bundled_engine_version) (bundled)"
22
+ echo "Engine: $(bundled_engine_version) (bundled with the CLI)"
22
23
  ;;
23
24
  legacy)
24
25
  echo "Project: $IP_ROOT (vendored v1 setup)"
@@ -6,7 +6,25 @@ source "$CLI_DIR/lib/cli-common.sh"
6
6
  detect_project
7
7
  case "$IP_MODE" in
8
8
  v2)
9
- ensure_engine_staged "$IP_ROOT"
9
+ # Fail closed, never skip silently: a store-backed source that is not
10
+ # on disk means the project is not installed (or predates the package
11
+ # model) — syncing would quietly drop whole rule sets.
12
+ missing="" stale=0
13
+ for section in rules agents skills; do
14
+ while IFS= read -r src; do
15
+ [ -n "$src" ] || continue
16
+ case "$src" in
17
+ .intelligence/engine/*) stale=1 ;;
18
+ .intelligence/*) [ -d "$IP_ROOT/$src" ] || missing="$missing $src" ;;
19
+ esac
20
+ done < <(read_yaml_list "$IP_ROOT/intelligence.yaml" "$section")
21
+ done
22
+ if [ "$stale" -eq 1 ]; then
23
+ die "this manifest predates the engine-content package (.intelligence/engine sources) — run 'intelligence upgrade'"
24
+ fi
25
+ if [ -n "$missing" ]; then
26
+ die "installed content missing on disk:$missing — run 'intelligence install'"
27
+ fi
10
28
  export_engine_env "$IP_ROOT"
11
29
  exec bash "$IS_ENGINE_DIR/scripts/sync.sh" "$@"
12
30
  ;;
@@ -21,29 +21,48 @@ require_v2
21
21
  manifest="$IP_ROOT/intelligence.yaml"
22
22
  lock="$IP_ROOT/intelligence.lock"
23
23
 
24
- moved=0 checked=0
24
+ moved=0 found=0
25
25
  while IFS= read -r name; do
26
26
  [ -n "$name" ] || continue
27
27
  [ -n "$only" ] && [ "$name" != "$only" ] && continue
28
+ # A named package that exists is "found" whatever happens next — a skip
29
+ # message followed by "not in the manifest" would contradict itself.
30
+ found=1
31
+ if [ "$name" = "$SYNC_PKG_NAME" ]; then
32
+ echo " $name: engine content moves with the engine — 'intelligence upgrade' owns it"
33
+ continue
34
+ fi
28
35
  ref="$(qmap_field "$manifest" "packages" "$name" "ref")"
29
36
  [ -n "$ref" ] && { echo " $name: pinned to ref '$ref' — skipped"; continue; }
30
37
  range="$(qmap_field "$manifest" "packages" "$name" "version")"
38
+ # An exact version is a pin, same as ref: — update never moves pins.
39
+ if [ -n "$range" ] && semver_is_stable "$range"; then
40
+ echo " $name: pinned to $range — edit the manifest to move it"
41
+ continue
42
+ fi
31
43
  url="$(qmap_field "$manifest" "packages" "$name" "url")"
32
44
  path="$(qmap_field "$manifest" "packages" "$name" "path")"
33
45
  if [ -z "$url" ]; then
34
46
  resolve_package_source "$manifest" "$name"
35
47
  url="$RES_URL"; path="$RES_PATH"
36
48
  fi
37
- checked=$((checked + 1))
38
49
  picked="$(list_remote_versions "$url" | semver_pick_highest "$range")"
39
50
  [ -n "$picked" ] || { echo " $name: nothing satisfies '${range:-*}' at $url" >&2; continue; }
40
51
  read -r tag _ <<< "$(remote_tag_for_version "$url" "$picked")"
41
52
  current="$(qmap_field "$lock" "packages" "$name" "resolved")"
42
- if [ "$tag" = "$current" ]; then
53
+ cur_url="$(qmap_field "$lock" "packages" "$name" "url")"
54
+ cur_path="$(qmap_field "$lock" "packages" "$name" "path")"
55
+ # Same tag is not enough: a registry may re-point a name's source while
56
+ # keeping the version — the move doctor warns about must be actionable
57
+ # here, so url/path changes refetch too.
58
+ if [ "$tag" = "$current" ] && [ "$url" = "$cur_url" ] && [ "$path" = "$cur_path" ]; then
43
59
  echo " $name: ${current:-<none>} (up to date)"
44
60
  continue
45
61
  fi
46
62
  rel=".intelligence/packages/$name"
63
+ # The new version may have dropped a top-level section dir: unwire the old
64
+ # shape first, or a dangling store-backed source bricks the next sync.
65
+ unwire_package_sources "$manifest" "$rel"
47
66
  sha="$(fetch_package "$url" "$tag" "$path" "$IP_ROOT/$rel")"
48
67
  wire_package_sources "$manifest" "$name" "$rel" "$IP_ROOT"
49
68
  lock_upsert "$lock" "$name" "$range" "$url" "$path" "$tag" "$sha"
@@ -51,7 +70,7 @@ while IFS= read -r name; do
51
70
  moved=$((moved + 1))
52
71
  done < <(qmap_keys "$manifest" "packages")
53
72
 
54
- [ -n "$only" ] && [ "$checked" -eq 0 ] && die "package '$only' is not in the manifest"
73
+ [ -n "$only" ] && [ "$found" -eq 0 ] && die "package '$only' is not in the manifest"
55
74
  echo "updated: $moved package(s)"
56
75
 
57
76
  if [ "$moved" -gt 0 ] && [ "$no_sync" -eq 0 ]; then
@@ -1,7 +1,7 @@
1
1
  #!/bin/bash
2
- # intelligence upgrade — bring the project to this CLI's engine: restage the
3
- # engine content, apply v2 schema migrations (none yet — the chain starts
4
- # empty by design), restamp sync_version, sync.
2
+ # intelligence upgrade — bring the project to this CLI's engine: apply v2
3
+ # schema migrations, align the engine-content package to the bundled version,
4
+ # restamp sync_version, sync.
5
5
  set -euo pipefail
6
6
  source "$CLI_DIR/lib/cli-common.sh"
7
7
 
@@ -14,13 +14,54 @@ if [ -n "$stamp" ] && _ver_gt "$stamp" "$eng"; then
14
14
  die "manifest schema $stamp is newer than this CLI's engine $eng — update the CLI first: npm i -g @ainova-systems/intelligence@latest"
15
15
  fi
16
16
 
17
- # v2 schema migrations live HERE, not in the engine's chain (that one owns
18
- # vendored layouts). Same discipline: ascending, append-only, each migration
19
- # self-detects and no-ops when already applied.
20
- # V2_MIGRATIONS=( ) — first entry arrives with the first breaking v2 change.
17
+ # --- v2 schema migrations -------------------------------------------------
18
+ # The engine's own chain owns vendored layouts; this one owns the manifest.
19
+ # Same discipline: ascending, append-only, each migration self-detects and
20
+ # no-ops when already applied.
21
+
22
+ # v2 migration 1: staged engine content -> the @ainova-systems/sync package.
23
+ # Pre-package manifests listed `.intelligence/engine/{rules,agents,skills}`
24
+ # as sources fed by a CLI-staged copy; that copy is now an ordinary package
25
+ # entry seeded from the bundle. Post-condition: no `.intelligence/engine`
26
+ # source entries, the package present in manifest+lock+store, stale dir gone.
27
+ migrated=0
28
+ for section in rules agents skills; do
29
+ while IFS= read -r src; do
30
+ case "$src" in
31
+ .intelligence/engine/*)
32
+ sources_remove_entry "$manifest" "$section" "$src"
33
+ migrated=1
34
+ ;;
35
+ esac
36
+ done < <(read_yaml_list "$manifest" "$section")
37
+ done
38
+ if [ "$migrated" -eq 1 ]; then
39
+ echo " migrating: staged engine content -> $SYNC_PKG_NAME package"
40
+ rm -rf "$IP_ROOT/.intelligence/engine"
41
+ sync_pkg_entry "$manifest"
42
+ sync_pkg_install "$IP_ROOT"
43
+ fi
44
+
45
+ # --- steady state ---------------------------------------------------------
46
+ # Align the engine-content package with the bundled engine. Only a manifest
47
+ # that HAS the entry is touched — a --bare project opted out, and upgrade
48
+ # respects that.
49
+ have_pkg=0
50
+ while IFS= read -r name; do
51
+ [ "$name" = "$SYNC_PKG_NAME" ] && have_pkg=1
52
+ done < <(qmap_keys "$manifest" "packages")
53
+ if [ "$have_pkg" -eq 1 ] && [ "$migrated" -eq 0 ]; then
54
+ pinned="$(qmap_field "$manifest" "packages" "$SYNC_PKG_NAME" "version")"
55
+ if [ "$pinned" != "$eng" ]; then
56
+ echo " $SYNC_PKG_NAME: $pinned -> $eng"
57
+ fi
58
+ sync_pkg_entry "$manifest"
59
+ sync_pkg_install "$IP_ROOT"
60
+ fi
61
+ if [ "$have_pkg" -eq 0 ] && [ "$migrated" -eq 0 ]; then
62
+ echo " NOTE: $SYNC_PKG_NAME is not in this manifest (bare setup) — engine meta-skills stay uninstalled; 'intelligence add $SYNC_PKG_NAME' opts back in." >&2
63
+ fi
21
64
 
22
- stage_engine_content "$IP_ROOT/.intelligence/engine"
23
- echo " engine content restaged ($eng)"
24
65
  stamp_version "$manifest" "$eng"
25
66
  echo " sync_version -> $eng"
26
67
 
@@ -0,0 +1,14 @@
1
+ # What THIS CLI distribution ships as its engine-content package, and which
2
+ # registry its init seeds into new manifests. Data, not code: a fork or an
3
+ # enterprise mirror edits this file (or its copy in the npm build) — nothing
4
+ # in cli/ names a vendor.
5
+ #
6
+ # The engine-content package is the one package whose content documents the
7
+ # engine scripts bundled with the CLI: init writes this entry into the
8
+ # manifest (visible, removable), its pin always equals the engine VERSION,
9
+ # `upgrade` moves it, and at the matching version it materializes from the
10
+ # bundle without network.
11
+ name: "@ainova-systems/sync"
12
+ url: "https://github.com/ainova-systems/intelligence-sync.git"
13
+ path: "intelligence/sync"
14
+ default_registry: "https://github.com/ainova-systems/intelligence-registry.git"
@@ -24,10 +24,23 @@ source "$CLI_DIR/lib/semver.sh"
24
24
  source "$CLI_DIR/lib/registry.sh"
25
25
  source "$CLI_DIR/lib/lockfile.sh"
26
26
 
27
- # Meta-skills that the CLI replaces with first-class commands — staging skips
28
- # them so a v2 project never carries a skill telling the agent to run a flow
29
- # the CLI already owns.
30
- CLI_OBSOLETE_SKILLS="intelligence-update intelligence-sync"
27
+ # The engine-content package: OPTIONAL but auto-selected at init. Package by
28
+ # UX (manifest entry, lockfile row, list/search/remove), bundle by mechanics —
29
+ # at the version the CLI ships, it materializes from the npm bundle without
30
+ # network; only a cross-version install reaches git. The pin is held exactly
31
+ # at the bundled engine version and moved only by `upgrade`.
32
+ #
33
+ # Its identity is DATA shipped with the distribution (cli/engine-package.yaml),
34
+ # never a name compiled into cli code — a fork edits the file.
35
+ _EPKG="$CLI_DIR/engine-package.yaml"
36
+ SYNC_PKG_NAME="$(top_scalar "$_EPKG" "name")"
37
+ SYNC_PKG_URL="$(top_scalar "$_EPKG" "url")"
38
+ SYNC_PKG_PATH="$(top_scalar "$_EPKG" "path")"
39
+ # Read by commands (init seeds it) — per-file shellcheck cannot see that.
40
+ # shellcheck disable=SC2034
41
+ DEFAULT_REGISTRY_URL="$(top_scalar "$_EPKG" "default_registry")"
42
+ [ -n "$SYNC_PKG_NAME" ] || die "corrupt CLI installation: $_EPKG is missing or has no 'name'"
43
+ SYNC_PKG_STORE=".intelligence/packages/$SYNC_PKG_NAME"
31
44
 
32
45
  # --- Project detection ---------------------------------------------------
33
46
  # Sets: IP_MODE (v2|legacy|none), IP_ROOT, IP_UMBRELLA, IP_MODULE_DIR.
@@ -90,51 +103,26 @@ bundled_engine_version() {
90
103
  tr -d ' \t\r\n' < "$IS_ENGINE_DIR/scripts/VERSION"
91
104
  }
92
105
 
93
- # --- Engine content staging ----------------------------------------------
94
- # The v2 project vendors no engine code; the engine's own rules / agents /
95
- # meta-skills / docs are staged into the package store so sources can reach
96
- # them as ordinary repo-relative dirs. Re-staged whenever the bundled engine
97
- # version differs from the `.version` stamp.
98
- # stage_engine_content <dest-dir> — unconditional copy of the engine's own
99
- # content into <dest-dir> plus the `.version` stamp.
100
- stage_engine_content() {
101
- local store="$1"
102
- rm -rf "$store"
103
- mkdir -p "$store"
104
- local d s name skip
105
- # `scripts` travels too: engine-shipped skills reference `<module>/scripts/…`
106
- # (adapters, docs) and `<module>` resolves here, so the paths they hand an
107
- # agent must exist. Running that sync.sh directly is harmless — outside CLI
108
- # mode it fails closed rather than generating against the wrong layout.
109
- for d in rules agents docs scripts; do
110
- [ -d "$IS_ENGINE_DIR/$d" ] && cp -R "$IS_ENGINE_DIR/$d" "$store/$d"
111
- done
112
- if [ -d "$IS_ENGINE_DIR/skills" ]; then
113
- mkdir -p "$store/skills"
114
- for s in "$IS_ENGINE_DIR"/skills/*/; do
115
- [ -d "$s" ] || continue
116
- name="$(basename "$s")"
117
- skip=0
118
- for d in $CLI_OBSOLETE_SKILLS; do
119
- [ "$name" = "$d" ] && skip=1
120
- done
121
- [ "$skip" = "1" ] && continue
122
- cp -R "$s" "$store/skills/$name"
123
- done
124
- fi
125
- bundled_engine_version > "$store/.version"
106
+ # --- The sync package's manifest/lock plumbing ---------------------------
107
+ # sync_pkg_entry <manifest> — write/refresh the package's manifest entry:
108
+ # name + exact pin + explicit url/path, so resolution never depends on any
109
+ # registry (a project registry shadowing the name cannot brick the engine).
110
+ sync_pkg_entry() {
111
+ local manifest="$1"
112
+ qmap_set "$manifest" "packages" "$SYNC_PKG_NAME" "version" "$(bundled_engine_version)"
113
+ qmap_set "$manifest" "packages" "$SYNC_PKG_NAME" "url" "$SYNC_PKG_URL"
114
+ qmap_set "$manifest" "packages" "$SYNC_PKG_NAME" "path" "$SYNC_PKG_PATH"
126
115
  }
127
116
 
128
- ensure_engine_staged() {
129
- local root="$1"
130
- local store="$root/.intelligence/engine"
131
- local bundled_ver staged_ver
132
- bundled_ver="$(bundled_engine_version)"
133
- staged_ver=""
134
- [ -f "$store/.version" ] && staged_ver="$(tr -d ' \t\r\n' < "$store/.version")"
135
- [ "$staged_ver" = "$bundled_ver" ] && return 0
136
- stage_engine_content "$store"
137
- echo " engine content staged: .intelligence/engine ($bundled_ver)"
117
+ # sync_pkg_install <root> — materialize the package into the store and lock
118
+ # it (offline at the bundled version — fetch_package's bundle-seed guard).
119
+ sync_pkg_install() {
120
+ local root="$1" ver sha
121
+ ver="$(bundled_engine_version)"
122
+ sha="$(fetch_package "$SYNC_PKG_URL" "v$ver" "$SYNC_PKG_PATH" "$root/$SYNC_PKG_STORE")"
123
+ wire_package_sources "$root/intelligence.yaml" "$SYNC_PKG_NAME" "$SYNC_PKG_STORE" "$root"
124
+ lock_upsert "$root/intelligence.lock" "$SYNC_PKG_NAME" "$ver" "$SYNC_PKG_URL" "$SYNC_PKG_PATH" "v$ver" "$sha"
125
+ echo " engine content installed: $SYNC_PKG_STORE (v$ver)"
138
126
  }
139
127
 
140
128
  # --- The engine env contract ---------------------------------------------
@@ -147,7 +135,8 @@ export_engine_env() {
147
135
  export CONFIG_FILE="$root/intelligence.yaml"
148
136
  export REPO_ROOT="$root"
149
137
  export IS_UMBRELLA_REL="$umbrella"
150
- export IS_MODULE_REL=".intelligence/engine"
138
+ export IS_MODULE_REL="$SYNC_PKG_STORE"
151
139
  export IS_SYNC_CMD="intelligence sync"
140
+ export IS_MANIFEST_NAME="intelligence.yaml"
152
141
  export IS_PROTECTED_DIRS="$umbrella:.intelligence"
153
142
  }
@@ -24,6 +24,16 @@ assert_valid_pkg_name() {
24
24
  case "$1" in
25
25
  *[!@/A-Za-z0-9._-]*|*/*/*) die "invalid package name '$1' — allowed: letters, digits, . _ -, one '/'" ;;
26
26
  esac
27
+ # Both segments become store path components fed to rm -rf: an empty one
28
+ # ('@scope/') resolves to the whole scope dir, and a dot segment walks the
29
+ # tree — either would turn one bad --name into deleting other packages.
30
+ local scope="${1%%/*}" short="${1#*/}"
31
+ case "${scope#@}" in
32
+ ""|.|..) die "invalid package name '$1' — empty or dot scope" ;;
33
+ esac
34
+ case "$short" in
35
+ ""|.|..) die "invalid package name '$1' — empty or dot name" ;;
36
+ esac
27
37
  }
28
38
 
29
39
  # qmap_keys <file> <block> — quoted keys, one per line.
@@ -194,6 +204,114 @@ qmap_delete_key() {
194
204
  '
195
205
  }
196
206
 
207
+ # --- registries: a trust LIST, not a scope map -----------------------------
208
+ # The block holds registry repo URLs in trust order. Two shapes are read so
209
+ # early manifests keep working: the list form (`- "url"`) and the retired
210
+ # flat-map form (`"@scope": "url"` — the scope label is ignored, the URL is
211
+ # simply another registry).
212
+
213
+ # registries_list <file> — registry URLs, one per line, manifest order.
214
+ registries_list() {
215
+ [ -f "$1" ] || return 0
216
+ awk '
217
+ { sub(/\r$/, "") }
218
+ /^registries:[ \t]*$/ { inb = 1; next }
219
+ inb && /^[^ #]/ { inb = 0 }
220
+ inb {
221
+ line = $0
222
+ if (line ~ /^[ \t]*-[ \t]*/) {
223
+ sub(/^[ \t]*-[ \t]*/, "", line)
224
+ } else if (line ~ /^ "/) {
225
+ c = index(line, ":")
226
+ if (c == 0) next
227
+ line = substr(line, c + 1)
228
+ } else next
229
+ sub(/^[ \t]+/, "", line)
230
+ gsub(/["\x27]/, "", line)
231
+ sub(/[ \t]+#.*$/, "", line)
232
+ sub(/[ \t]+$/, "", line)
233
+ if (line != "") print line
234
+ }
235
+ ' "$1"
236
+ }
237
+
238
+ # registries_add <file> <url> — idempotent append in list form.
239
+ registries_add() {
240
+ local file="$1" url="$2" existing
241
+ while IFS= read -r existing; do
242
+ [ "$existing" = "$url" ] && return 0
243
+ done < <(registries_list "$file")
244
+ _qmap_stage "$file" -v url="$url" '
245
+ function entry() { return " - \"" url "\"" }
246
+ { sub(/\r$/, "") }
247
+ /^registries:[ \t]*$/ { blockseen = 1; inb = 1; print; next }
248
+ inb && /^[^ #]/ { if (!done) { print entry(); done = 1 }; inb = 0 }
249
+ { last = $0; print }
250
+ END {
251
+ if (inb && !done) { print entry(); done = 1 }
252
+ if (!blockseen) {
253
+ if (last != "") print ""
254
+ print "registries:"
255
+ print entry()
256
+ }
257
+ }
258
+ '
259
+ }
260
+
261
+ # registries_remove <file> <url> — drop the entry, either shape.
262
+ registries_remove() {
263
+ local file="$1" url="$2"
264
+ [ -f "$file" ] || return 0
265
+ _qmap_stage "$file" -v url="$url" '
266
+ { sub(/\r$/, "") }
267
+ /^registries:[ \t]*$/ { inb = 1; print; next }
268
+ inb && /^[^ #]/ { inb = 0 }
269
+ inb {
270
+ line = $0
271
+ v = ""
272
+ if (line ~ /^[ \t]*-[ \t]*/) { v = line; sub(/^[ \t]*-[ \t]*/, "", v) }
273
+ else if (line ~ /^ "/) { c = index(line, ":"); if (c > 0) v = substr(line, c + 1) }
274
+ if (v != "") {
275
+ sub(/^[ \t]+/, "", v)
276
+ gsub(/["\x27]/, "", v)
277
+ sub(/[ \t]+#.*$/, "", v)
278
+ sub(/[ \t]+$/, "", v)
279
+ if (v == url) next
280
+ }
281
+ }
282
+ { print }
283
+ '
284
+ }
285
+
286
+ # sources_add_entry_first <file> <section> <entry> — idempotent insert at the
287
+ # TOP of sources.<section> (creating sources:/section as needed). Package
288
+ # entries go through this so project-owned entries stay later in the list —
289
+ # adapters copy sources in order and the last write wins, which is exactly
290
+ # the documented "your file overrides the package's" behavior.
291
+ sources_add_entry_first() {
292
+ local file="$1" section="$2" entry="$3"
293
+ _mig_has_source "$file" "$entry" && return 0
294
+ _qmap_stage "$file" -v section="$section" -v entry="$entry" '
295
+ function line() { return " - \"" entry "\"" }
296
+ { sub(/\r$/, "") }
297
+ /^sources:[ \t]*$/ { ins = 1; sourceseen = 1; print; next }
298
+ ins && /^[^ #]/ {
299
+ if (!secseen && !done) { print " " section ":"; print line(); done = 1 }
300
+ ins = 0
301
+ }
302
+ ins && $0 ~ "^ " section ":[ \t]*$" { secseen = 1; print; print line(); done = 1; next }
303
+ { print }
304
+ END {
305
+ if (ins && !secseen && !done) { print " " section ":"; print line(); done = 1 }
306
+ if (!sourceseen) {
307
+ print "sources:"
308
+ print " " section ":"
309
+ print line()
310
+ }
311
+ }
312
+ '
313
+ }
314
+
197
315
  # sources_remove_entry <file> <section> <entry> — remove `- "entry"` from
198
316
  # sources.<section>. Inverse of the engine-side _mig_add_source.
199
317
  sources_remove_entry() {
@@ -1,23 +1,23 @@
1
1
  #!/bin/bash
2
2
  # Name -> source resolution and package fetching.
3
3
  #
4
- # Resolution order (first hit wins):
5
- # 1. project `registries:` — a scope bound to a registry repo (a git repo
6
- # holding index.yaml); private orgs override anything shipped
7
- # 2. the bundled default index (registry/index.yaml next to the CLI)
8
- # 3. convention: @org/name -> https://github.com/org/name.git, content at
9
- # the repo root — the zero-infrastructure default
4
+ # A name resolves ONLY through the manifest's `registries:` trust list (git
5
+ # repos holding index.yaml, consulted in order; the first to declare the name
6
+ # wins). There is deliberately no built-in catalog and no name->github
7
+ # convention: the CLI core knows no vendor, and a name nobody explicitly
8
+ # trusted must never silently turn into an install from a guessed URL.
9
+ # Sources without a registry are always EXPLICIT: github:org/repo, git+<url>.
10
+ # Vendor defaults exist only as lines `init` writes into the manifest —
11
+ # visible, reviewable, deletable.
12
+ #
13
+ # The NAME is the trust anchor a developer reasons with; source integrity is
14
+ # a separate mechanism: the lock pins url+sha, and doctor flags
15
+ # resolution/lock url drift.
10
16
  #
11
17
  # An index is itself fetched with git (never curl): auth, proxies and private
12
18
  # hosting all come for free, and the CLI keeps the engine's bash+awk+git-only
13
19
  # dependency footprint.
14
20
 
15
- default_index_file() {
16
- local f="$CLI_DIR/../registry/index.yaml"
17
- [ -f "$f" ] && printf '%s' "$f"
18
- return 0
19
- }
20
-
21
21
  # _fetch_index <registry-repo-url> — clones the registry repo (shallow) and
22
22
  # prints the path of its index.yaml; empty on failure. Cached per run+url.
23
23
  _fetch_index() {
@@ -43,38 +43,57 @@ _fetch_index() {
43
43
  # shellcheck disable=SC2034
44
44
  resolve_package_source() {
45
45
  local manifest="$1" name="$2"
46
- local scope="${name%%/*}" short="${name#*/}"
47
46
  RES_URL=""; RES_PATH=""; RES_VIA=""
48
47
 
48
+ local reg_url index
49
49
  if [ -n "$manifest" ] && [ -f "$manifest" ]; then
50
- local reg_url index
51
- reg_url="$(qmap_value "$manifest" "registries" "$scope")"
52
- if [ -n "$reg_url" ]; then
50
+ while IFS= read -r reg_url; do
51
+ [ -n "$reg_url" ] || continue
53
52
  index="$(_fetch_index "${reg_url#git+}")"
54
- [ -n "$index" ] || die "registry for scope '$scope' is unreachable or has no index.yaml: $reg_url"
53
+ if [ -z "$index" ]; then
54
+ echo " WARN: registry unreachable or missing index.yaml, skipped: $reg_url" >&2
55
+ continue
56
+ fi
55
57
  RES_URL="$(qmap_field "$index" "packages" "$name" "url")"
56
- RES_PATH="$(qmap_field "$index" "packages" "$name" "path")"
57
- [ -n "$RES_URL" ] || die "package '$name' not found in the '$scope' registry ($reg_url)"
58
- RES_VIA="registry:$reg_url"
59
- return 0
60
- fi
58
+ if [ -n "$RES_URL" ]; then
59
+ RES_PATH="$(qmap_field "$index" "packages" "$name" "path")"
60
+ RES_VIA="registry:$reg_url"
61
+ return 0
62
+ fi
63
+ done < <(registries_list "$manifest")
61
64
  fi
65
+ # No trusted registry declares the name. Deliberately NOT an invented
66
+ # URL: the caller reports it, with suggestions.
67
+ return 0
68
+ }
62
69
 
63
- local bundled
64
- bundled="$(default_index_file)"
65
- if [ -n "$bundled" ]; then
66
- RES_URL="$(qmap_field "$bundled" "packages" "$name" "url")"
67
- if [ -n "$RES_URL" ]; then
68
- RES_PATH="$(qmap_field "$bundled" "packages" "$name" "path")"
69
- RES_VIA="index"
70
- return 0
71
- fi
70
+ # suggest_similar <manifest-or-empty> <@scope/name> — "Did you mean" lines to
71
+ # stderr, matched on the short name (singular/plural tolerant) across every
72
+ # trusted registry.
73
+ suggest_similar() {
74
+ local manifest="$1" name="$2"
75
+ local want cand cshort reg_url index seen=" "
76
+ want="${name#*/}"; want="${want%s}"
77
+ _suggest_from() {
78
+ local idx="$1"
79
+ [ -n "$idx" ] && [ -f "$idx" ] || return 0
80
+ while IFS= read -r cand; do
81
+ [ -n "$cand" ] || continue
82
+ case "$seen" in *" $cand "*) continue ;; esac
83
+ cshort="${cand#*/}"; cshort="${cshort%s}"
84
+ if [ "$cshort" = "$want" ] && [ "$cand" != "$name" ]; then
85
+ seen="$seen$cand "
86
+ echo " Did you mean: intelligence add $cand" >&2
87
+ fi
88
+ done < <(qmap_keys "$idx" "packages")
89
+ }
90
+ if [ -n "$manifest" ] && [ -f "$manifest" ]; then
91
+ while IFS= read -r reg_url; do
92
+ [ -n "$reg_url" ] || continue
93
+ index="$(_fetch_index "${reg_url#git+}")"
94
+ _suggest_from "$index"
95
+ done < <(registries_list "$manifest")
72
96
  fi
73
-
74
- RES_URL="https://github.com/${scope#@}/$short.git"
75
- RES_PATH=""
76
- RES_VIA="convention"
77
- return 0
78
97
  }
79
98
 
80
99
  # fetch_package <url> <ref> <subpath> <dest-dir>
@@ -84,6 +103,22 @@ resolve_package_source() {
84
103
  # store holds the bytes the package published.
85
104
  fetch_package() {
86
105
  local url="$1" ref="$2" subpath="$3" dest="$4"
106
+ # Bundle seed: the engine-content package at the CLI's own version copies
107
+ # from the npm bundle — no network, which keeps init / fresh-clone install
108
+ # / migrate offline exactly like the staging they replace. Keyed on the
109
+ # full (url, path, ref) triple; any other version or source falls through
110
+ # to the normal clone.
111
+ if [ "$url" = "$SYNC_PKG_URL" ] && [ "$subpath" = "$SYNC_PKG_PATH" ] \
112
+ && [ "${ref#v}" = "$(bundled_engine_version)" ]; then
113
+ rm -rf "$dest"
114
+ mkdir -p "$dest"
115
+ cp -R "$IS_ENGINE_DIR/." "$dest/"
116
+ rm -rf "$dest/.git"
117
+ if [ -f "$IS_ENGINE_DIR/scripts/ENGINE_SHA" ]; then
118
+ tr -d ' \t\r\n' < "$IS_ENGINE_DIR/scripts/ENGINE_SHA"
119
+ fi
120
+ return 0
121
+ fi
87
122
  local tmp sha src
88
123
  local -a branch_arg=()
89
124
  [ -n "$ref" ] && branch_arg=(--branch "$ref")
@@ -119,14 +154,14 @@ fetch_package() {
119
154
 
120
155
  # wire_package_sources <manifest> <@scope/name> <store-rel-dir>
121
156
  # The provides convention: whichever of rules/agents/skills the installed
122
- # package has becomes a sources entry. Reuses the engine's idempotent,
123
- # comment-preserving _mig_add_source.
157
+ # package has becomes a sources entry — inserted FIRST in its section, so
158
+ # project-owned entries come later and win on same-named artifacts.
124
159
  wire_package_sources() {
125
160
  local manifest="$1" name="$2" rel="$3" root="$4"
126
161
  local section
127
162
  for section in rules agents skills; do
128
163
  if [ -d "$root/$rel/$section" ]; then
129
- _mig_add_source "$manifest" "$section" "$rel/$section"
164
+ sources_add_entry_first "$manifest" "$section" "$rel/$section"
130
165
  fi
131
166
  done
132
167
  }
package/cli/lib/semver.sh CHANGED
@@ -75,7 +75,10 @@ semver_match() {
75
75
  # onto their tag.
76
76
  list_remote_versions() {
77
77
  local url="$1"
78
- GIT_TERMINAL_PROMPT=0 git ls-remote --tags "$url" 2>/dev/null \
78
+ # `|| true` inside the pipeline: an unreachable repo must read as "no
79
+ # versions", not kill a pipefail caller mid-command-substitution —
80
+ # reachability is the CALLER's question (add probes it explicitly).
81
+ { GIT_TERMINAL_PROMPT=0 git ls-remote --tags "$url" 2>/dev/null || true; } \
79
82
  | awk '{
80
83
  sub(/\r$/, "")
81
84
  ref = $2
@@ -92,7 +95,7 @@ list_remote_versions() {
92
95
  # prints "<tag> <sha>". Peeled sha (the commit a tag object points at) wins.
93
96
  remote_tag_for_version() {
94
97
  local url="$1" ver="${2#v}"
95
- GIT_TERMINAL_PROMPT=0 git ls-remote --tags "$url" 2>/dev/null \
98
+ { GIT_TERMINAL_PROMPT=0 git ls-remote --tags "$url" 2>/dev/null || true; } \
96
99
  | awk -v want="$ver" '
97
100
  {
98
101
  sub(/\r$/, "")
@@ -19,10 +19,10 @@ that ships it.
19
19
 
20
20
  ## Expertise
21
21
 
22
- The bash-to-skill status contract (`docs/CONVENTIONS.md`, Migration & Module Contract). Every flow
23
- here is deterministic and fail-closed: `sync.sh` is a pure synchronizer that refuses across an
24
- un-applied schema, `update.sh` stages, verifies a sentinel and only then commits, and each skill
25
- branches on `IS_STATUS`. The work is running the right flow, reading the code it returns, and doing
22
+ The status contract (`<module>/docs/CONVENTIONS.md`, Migration & Module Contract). Every flow
23
+ here is deterministic and fail-closed: the sync is a pure synchronizer that refuses across an
24
+ un-applied schema, the update flow stages, verifies and only then commits, and every flow
25
+ reports `IS_STATUS`. The work is running the right flow, reading the code it returns, and doing
26
26
  what that code says - including stopping. The guards carry the decisions, which is why this agent
27
27
  runs on the standard tier: the failure mode is a loud refusal and a retry, not a plausible wrong
28
28
  answer.