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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (53) hide show
  1. package/README.md +2 -2
  2. package/cli/commands/add.sh +26 -20
  3. package/cli/commands/init.sh +24 -9
  4. package/cli/commands/install.sh +55 -4
  5. package/cli/commands/migrate.sh +62 -9
  6. package/cli/commands/registry.sh +2 -9
  7. package/cli/commands/search.sh +4 -6
  8. package/cli/commands/sync.sh +1 -1
  9. package/cli/commands/update.sh +29 -5
  10. package/cli/engine-package.yaml +14 -0
  11. package/cli/intelligence +27 -15
  12. package/cli/lib/cli-common.sh +52 -17
  13. package/cli/lib/lockfile.sh +14 -8
  14. package/cli/lib/manifest.sh +59 -1
  15. package/cli/lib/registry.sh +41 -55
  16. package/cli/lib/semver.sh +4 -2
  17. package/engine/ENGINE_SHA +1 -0
  18. package/engine/{scripts/adapters → adapters}/agents.sh +10 -15
  19. package/engine/{scripts/lib → lib}/common.sh +15 -519
  20. package/engine/lib/contract.sh +120 -0
  21. package/engine/sync.sh +233 -0
  22. package/package.json +5 -5
  23. package/engine/INIT.md +0 -500
  24. package/engine/docs/CLI.md +0 -94
  25. package/engine/scripts/ENGINE_SHA +0 -1
  26. package/engine/scripts/lib/layout.sh +0 -51
  27. package/engine/scripts/lib/migrations.sh +0 -708
  28. package/engine/scripts/sync.sh +0 -311
  29. package/engine/scripts/update.sh +0 -237
  30. package/registry/index.yaml +0 -26
  31. /package/engine/{scripts/VERSION → VERSION} +0 -0
  32. /package/engine/{scripts/adapters → adapters}/_template.sh +0 -0
  33. /package/engine/{scripts/adapters → adapters}/claude.sh +0 -0
  34. /package/engine/{scripts/adapters → adapters}/codex.sh +0 -0
  35. /package/engine/{scripts/adapters → adapters}/copilot.sh +0 -0
  36. /package/engine/{scripts/adapters → adapters}/cursor.sh +0 -0
  37. /package/engine/{scripts/adapters → adapters}/opencode.sh +0 -0
  38. /package/engine/{scripts/adapters → adapters}/pi.sh +0 -0
  39. /package/{engine → packages/sync}/agents/intelligence-architect.md +0 -0
  40. /package/{engine → packages/sync}/agents/intelligence-operator.md +0 -0
  41. /package/{engine → packages/sync}/docs/ADAPTERS.md +0 -0
  42. /package/{engine → packages/sync}/docs/CONVENTIONS.md +0 -0
  43. /package/{engine → packages/sync}/rules/intelligence-authoring.md +0 -0
  44. /package/{engine → packages/sync}/skills/intelligence-add-agent/SKILL.md +0 -0
  45. /package/{engine → packages/sync}/skills/intelligence-add-rule/SKILL.md +0 -0
  46. /package/{engine → packages/sync}/skills/intelligence-add-skill/SKILL.md +0 -0
  47. /package/{engine → packages/sync}/skills/intelligence-extract-skill/SKILL.md +0 -0
  48. /package/{engine → packages/sync}/skills/intelligence-install-adapter/SKILL.md +0 -0
  49. /package/{engine → packages/sync}/skills/intelligence-learn-from-context/SKILL.md +0 -0
  50. /package/{engine → packages/sync}/skills/intelligence-review-skills/SKILL.md +0 -0
  51. /package/{engine → packages/sync}/skills/intelligence-sync/SKILL.md +0 -0
  52. /package/{engine → packages/sync}/skills/intelligence-uninstall-adapter/SKILL.md +0 -0
  53. /package/{engine → packages/sync}/skills/intelligence-update/SKILL.md +0 -0
@@ -7,32 +7,67 @@
7
7
  # CLI-owned parsing lives in lib/manifest.sh (quoted-key `packages:` /
8
8
  # `registries:` blocks, which the engine deliberately never reads).
9
9
 
10
- # Engine libraries (readers, is_status, IS_RC_*, engine_version). Sourced in
11
- # the same order sync.sh uses. CLI_DIR / IS_ENGINE_DIR come exported from the
12
- # dispatcher.
13
- source "$IS_ENGINE_DIR/scripts/lib/common.sh"
14
- source "$IS_ENGINE_DIR/scripts/lib/layout.sh"
15
- source "$IS_ENGINE_DIR/scripts/lib/migrations.sh"
10
+ # Engine libraries (readers, is_status, IS_RC_*, engine_version). CLI_DIR /
11
+ # IS_ENGINE_DIR come exported from the dispatcher.
12
+ source "$IS_ENGINE_DIR/lib/common.sh"
13
+ source "$IS_ENGINE_DIR/lib/contract.sh"
16
14
 
17
15
  die() {
18
16
  echo "ERROR: $*" >&2
19
17
  exit 1
20
18
  }
21
19
 
20
+ # --- Untrusted-input guards ----------------------------------------------
21
+ # Manifest, lock and registry indexes arrive with a cloned repo: every value
22
+ # they carry is attacker-adjacent. URLs and refs reach git argv (a leading
23
+ # `-` would be an option — `--upload-pack=<cmd>` is code execution), names
24
+ # become store paths fed to rm -rf. Guard at the choke points, loudly.
25
+
26
+ # assert_safe_source_url <url> — scheme allowlist (the engine's own list) or
27
+ # scp-like user@host:path; never option-shaped, never quote-bearing.
28
+ assert_safe_source_url() {
29
+ local url="$1"
30
+ case "$url" in
31
+ ""|-*) die "unsafe source url '$url' — option-shaped or empty" ;;
32
+ *[\"\'\ ]*) die "unsafe source url '$url' — quotes or spaces" ;;
33
+ https://*|http://*|ssh://*|git://*|file://*) ;;
34
+ *@*:*) ;;
35
+ *) die "unsafe source url '$url' — allowed: https, http, ssh, git, file, or user@host:path" ;;
36
+ esac
37
+ }
38
+
39
+ # assert_safe_ref <ref-or-empty> — tags/branches/SHAs; never option-shaped.
40
+ assert_safe_ref() {
41
+ local ref="$1"
42
+ [ -z "$ref" ] && return 0
43
+ case "$ref" in
44
+ -*) die "unsafe git ref '$ref' — option-shaped" ;;
45
+ *[\"\'\ \\]*) die "unsafe git ref '$ref'" ;;
46
+ esac
47
+ }
48
+
22
49
  source "$CLI_DIR/lib/manifest.sh"
23
50
  source "$CLI_DIR/lib/semver.sh"
24
51
  source "$CLI_DIR/lib/registry.sh"
25
52
  source "$CLI_DIR/lib/lockfile.sh"
26
53
 
27
- # The engine's own content as a package: OPTIONAL but auto-selected at init.
28
- # Package by UX (manifest entry, lockfile row, list/search/remove), bundle by
29
- # mechanics — at the version the CLI ships, it materializes from the npm
30
- # bundle without network; only a cross-version install reaches git. The pin
31
- # is held exactly at the bundled engine version and moved only by `upgrade`.
32
- SYNC_PKG_NAME="@ainova-systems/sync"
33
- SYNC_PKG_URL="https://github.com/ainova-systems/intelligence-sync.git"
34
- SYNC_PKG_PATH="intelligence/sync"
35
- SYNC_PKG_STORE=".intelligence/packages/@ainova-systems/sync"
54
+ # The engine-content package: OPTIONAL but auto-selected at init. Package by
55
+ # UX (manifest entry, lockfile row, list/search/remove), bundle by mechanics —
56
+ # at the version the CLI ships, it materializes from the npm bundle without
57
+ # network; only a cross-version install reaches git. The pin is held exactly
58
+ # at the bundled engine version and moved only by `upgrade`.
59
+ #
60
+ # Its identity is DATA shipped with the distribution (cli/engine-package.yaml),
61
+ # never a name compiled into cli code — a fork edits the file.
62
+ _EPKG="$CLI_DIR/engine-package.yaml"
63
+ SYNC_PKG_NAME="$(top_scalar "$_EPKG" "name")"
64
+ SYNC_PKG_URL="$(top_scalar "$_EPKG" "url")"
65
+ SYNC_PKG_PATH="$(top_scalar "$_EPKG" "path")"
66
+ # Read by commands (init seeds it) — per-file shellcheck cannot see that.
67
+ # shellcheck disable=SC2034
68
+ DEFAULT_REGISTRY_URL="$(top_scalar "$_EPKG" "default_registry")"
69
+ [ -n "$SYNC_PKG_NAME" ] || die "corrupt CLI installation: $_EPKG is missing or has no 'name'"
70
+ SYNC_PKG_STORE=".intelligence/packages/$SYNC_PKG_NAME"
36
71
 
37
72
  # --- Project detection ---------------------------------------------------
38
73
  # Sets: IP_MODE (v2|legacy|none), IP_ROOT, IP_UMBRELLA, IP_MODULE_DIR.
@@ -92,7 +127,7 @@ manifest_intelligence_dir() {
92
127
  }
93
128
 
94
129
  bundled_engine_version() {
95
- tr -d ' \t\r\n' < "$IS_ENGINE_DIR/scripts/VERSION"
130
+ tr -d ' \t\r\n' < "$IS_ENGINE_DIR/VERSION"
96
131
  }
97
132
 
98
133
  # --- The sync package's manifest/lock plumbing ---------------------------
@@ -127,7 +162,7 @@ export_engine_env() {
127
162
  export CONFIG_FILE="$root/intelligence.yaml"
128
163
  export REPO_ROOT="$root"
129
164
  export IS_UMBRELLA_REL="$umbrella"
130
- export IS_MODULE_REL=".intelligence/engine"
165
+ export IS_MODULE_REL="$SYNC_PKG_STORE"
131
166
  export IS_SYNC_CMD="intelligence sync"
132
167
  export IS_MANIFEST_NAME="intelligence.yaml"
133
168
  export IS_PROTECTED_DIRS="$umbrella:.intelligence"
@@ -54,14 +54,18 @@ lock_write_from_tsv() {
54
54
  echo "lockfile_version: $LOCKFILE_VERSION"
55
55
  echo "engine_version: \"$(bundled_engine_version)\""
56
56
  echo "packages:"
57
+ # Values reach a double-quoted YAML scalar: a `"` or `\` in any of
58
+ # them (registry-supplied url/path included) must be escaped, or it
59
+ # would break or alias the neighbouring field.
57
60
  awk -F"$LOCK_SEP" '
61
+ function esc(s) { gsub(/\\/, "\\\\", s); gsub(/"/, "\\\"", s); return s }
58
62
  NF >= 6 {
59
- print " \"" $1 "\":"
60
- if ($2 != "") print " requested: \"" $2 "\""
61
- print " url: \"" $3 "\""
62
- if ($4 != "") print " path: \"" $4 "\""
63
- if ($5 != "") print " resolved: \"" $5 "\""
64
- print " sha: \"" $6 "\""
63
+ print " \"" esc($1) "\":"
64
+ if ($2 != "") print " requested: \"" esc($2) "\""
65
+ print " url: \"" esc($3) "\""
66
+ if ($4 != "") print " path: \"" esc($4) "\""
67
+ if ($5 != "") print " resolved: \"" esc($5) "\""
68
+ print " sha: \"" esc($6) "\""
65
69
  }
66
70
  ' "$tsv"
67
71
  } > "$tmp"
@@ -72,7 +76,8 @@ lock_write_from_tsv() {
72
76
  # Replace-or-append one entry, keeping the others.
73
77
  lock_upsert() {
74
78
  local lock="$1" name="$2" requested="$3" url="$4" path="$5" resolved="$6" sha="$7"
75
- local tsv="${TMPDIR:-/tmp}/intelligence-lock-$$.tsv"
79
+ local tsv
80
+ tsv="$(mktemp -t intelligence-lock-XXXXXX 2>/dev/null || mktemp)"
76
81
  {
77
82
  lock_to_tsv "$lock" | awk -F"$LOCK_SEP" -v n="$name" '$1 != n'
78
83
  printf '%s\037%s\037%s\037%s\037%s\037%s\n' \
@@ -86,7 +91,8 @@ lock_upsert() {
86
91
  lock_remove() {
87
92
  local lock="$1" name="$2"
88
93
  [ -f "$lock" ] || return 0
89
- local tsv="${TMPDIR:-/tmp}/intelligence-lock-$$.tsv"
94
+ local tsv
95
+ tsv="$(mktemp -t intelligence-lock-XXXXXX 2>/dev/null || mktemp)"
90
96
  lock_to_tsv "$lock" | awk -F"$LOCK_SEP" -v n="$name" '$1 != n' > "$tsv"
91
97
  if [ -s "$tsv" ]; then
92
98
  lock_write_from_tsv "$lock" "$tsv"
@@ -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.
@@ -104,9 +114,19 @@ _qmap_stage() {
104
114
 
105
115
  # qmap_set <file> <block> <key> <field> <value> — upsert one field, creating
106
116
  # the block and the key as needed.
117
+ # _yq_esc <string> — escape \ and " for a double-quoted YAML scalar.
118
+ _yq_esc() {
119
+ local s="$1"
120
+ s="${s//\\/\\\\}"
121
+ printf '%s' "${s//\"/\\\"}"
122
+ }
123
+
107
124
  qmap_set() {
108
125
  local file="$1" block="$2" key="$3" field="$4" value="$5"
109
126
  [ -f "$file" ] || die "no such file: $file"
127
+ # url/path values can carry a `"`; escape before it reaches the quoted
128
+ # scalar the writer emits.
129
+ value="$(_yq_esc "$value")"
110
130
  _qmap_stage "$file" -v block="$block" -v key="$key" -v field="$field" -v value="$value" '
111
131
  function keyline() { return " \"" key "\":" }
112
132
  function fieldline() { return " " field ": \"" value "\"" }
@@ -153,6 +173,7 @@ qmap_set() {
153
173
  qmap_set_value() {
154
174
  local file="$1" block="$2" key="$3" value="$4"
155
175
  [ -f "$file" ] || die "no such file: $file"
176
+ value="$(_yq_esc "$value")"
156
177
  _qmap_stage "$file" -v block="$block" -v key="$key" -v value="$value" '
157
178
  function entry() { return " \"" key "\": \"" value "\"" }
158
179
  { sub(/\r$/, "") }
@@ -272,9 +293,46 @@ registries_remove() {
272
293
  { print }
273
294
  '
274
295
  }
296
+ # True (0) if <config> already lists <entry> under any sources section. Quoted
297
+ # and bare spellings both count - a manifest may hold either.
298
+ _mig_has_source() {
299
+ local config="$1" entry="$2"
300
+ [ -f "$config" ] || return 1
301
+ grep -Fq -- "\"$entry\"" "$config" || grep -Fq -- "- $entry" "$config"
302
+ }
303
+
304
+
305
+ # sources_add_entry_first <file> <section> <entry> — idempotent insert at the
306
+ # TOP of sources.<section> (creating sources:/section as needed). Package
307
+ # entries go through this so project-owned entries stay later in the list —
308
+ # adapters copy sources in order and the last write wins, which is exactly
309
+ # the documented "your file overrides the package's" behavior.
310
+ sources_add_entry_first() {
311
+ local file="$1" section="$2" entry="$3"
312
+ _mig_has_source "$file" "$entry" && return 0
313
+ _qmap_stage "$file" -v section="$section" -v entry="$entry" '
314
+ function line() { return " - \"" entry "\"" }
315
+ { sub(/\r$/, "") }
316
+ /^sources:[ \t]*$/ { ins = 1; sourceseen = 1; print; next }
317
+ ins && /^[^ #]/ {
318
+ if (!secseen && !done) { print " " section ":"; print line(); done = 1 }
319
+ ins = 0
320
+ }
321
+ ins && $0 ~ "^ " section ":[ \t]*$" { secseen = 1; print; print line(); done = 1; next }
322
+ { print }
323
+ END {
324
+ if (ins && !secseen && !done) { print " " section ":"; print line(); done = 1 }
325
+ if (!sourceseen) {
326
+ print "sources:"
327
+ print " " section ":"
328
+ print line()
329
+ }
330
+ }
331
+ '
332
+ }
275
333
 
276
334
  # sources_remove_entry <file> <section> <entry> — remove `- "entry"` from
277
- # sources.<section>. Inverse of the engine-side _mig_add_source.
335
+ # sources.<section>.
278
336
  sources_remove_entry() {
279
337
  local file="$1" section="$2" entry="$3"
280
338
  [ -f "$file" ] || return 0
@@ -1,42 +1,46 @@
1
1
  #!/bin/bash
2
2
  # Name -> source resolution and package fetching.
3
3
  #
4
- # Resolution order (first registry to DECLARE the name wins):
5
- # 1. project `registries:` — a trust LIST of registry repos (git repos
6
- # holding index.yaml), consulted in manifest order; adding one is an
7
- # explicit, committed act of trust in the names it declares
8
- # 2. the bundled default index (registry/index.yaml next to the CLI)
9
- # 3. convention: @org/name -> https://github.com/org/name.git, content at
10
- # the repo root — the zero-infrastructure default
4
+ # A name resolves ONLY through the manifest's `registries:` trust list (git
5
+ # repos holding index.yaml, consulted in order; the first to declare the name
6
+ # wins). There is deliberately no built-in catalog and no name->github
7
+ # convention: the CLI core knows no vendor, and a name nobody explicitly
8
+ # trusted must never silently turn into an install from a guessed URL.
9
+ # Sources without a registry are always EXPLICIT: github:org/repo, git+<url>.
10
+ # Vendor defaults exist only as lines `init` writes into the manifest —
11
+ # visible, reviewable, deletable.
11
12
  #
12
13
  # The NAME is the trust anchor a developer reasons with; source integrity is
13
- # a separate mechanism: the lock pins url+sha, a project-registry hit that
14
- # shadows a bundled name with a different url warns loudly, and doctor flags
14
+ # a separate mechanism: the lock pins url+sha, and doctor flags
15
15
  # resolution/lock url drift.
16
16
  #
17
17
  # An index is itself fetched with git (never curl): auth, proxies and private
18
18
  # hosting all come for free, and the CLI keeps the engine's bash+awk+git-only
19
19
  # dependency footprint.
20
20
 
21
- default_index_file() {
22
- local f="$CLI_DIR/../registry/index.yaml"
23
- [ -f "$f" ] && printf '%s' "$f"
24
- return 0
25
- }
26
-
27
21
  # _fetch_index <registry-repo-url> — clones the registry repo (shallow) and
28
22
  # prints the path of its index.yaml; empty on failure. Cached per run+url.
29
23
  _fetch_index() {
30
- local url="$1" cache key tmp
31
- cache="${TMPDIR:-/tmp}/intelligence-cli-index-$$"
24
+ local url="$1" key tmp
25
+ assert_safe_source_url "$url"
26
+ # Per-run private cache dir under an UNPREDICTABLE mktemp path (a fixed
27
+ # /tmp/...-$$ would be trusted on cache hit and pre-creatable by others).
28
+ # No EXIT trap: _fetch_index is always called inside $( ), so a trap would
29
+ # fire in that subshell and delete the dir before the caller reads it. The
30
+ # dir is small and OS-reaped from the temp root; IS_REMOTE_CACHE-style
31
+ # persistence is exported so repeated calls in one command share it.
32
+ if [ -z "${IS_CLI_INDEX_CACHE:-}" ]; then
33
+ IS_CLI_INDEX_CACHE="$(mktemp -d -t intelligence-cli-index-XXXXXX 2>/dev/null || mktemp -d)"
34
+ export IS_CLI_INDEX_CACHE
35
+ fi
36
+ local cache="$IS_CLI_INDEX_CACHE"
32
37
  key="$(printf '%s' "$url" | cksum | awk '{print $1}')"
33
38
  if [ -f "$cache/$key/index.yaml" ]; then
34
39
  printf '%s' "$cache/$key/index.yaml"
35
40
  return 0
36
41
  fi
37
- mkdir -p "$cache"
38
42
  tmp="$cache/$key"
39
- if GIT_TERMINAL_PROMPT=0 git -c core.autocrlf=false clone --depth 1 --quiet "$url" "$tmp" 2>/dev/null \
43
+ if GIT_TERMINAL_PROMPT=0 git -c core.autocrlf=false clone --depth 1 --quiet -- "$url" "$tmp" 2>/dev/null \
40
44
  && [ -f "$tmp/index.yaml" ]; then
41
45
  printf '%s' "$tmp/index.yaml"
42
46
  fi
@@ -49,11 +53,9 @@ _fetch_index() {
49
53
  # shellcheck disable=SC2034
50
54
  resolve_package_source() {
51
55
  local manifest="$1" name="$2"
52
- local scope="${name%%/*}" short="${name#*/}"
53
56
  RES_URL=""; RES_PATH=""; RES_VIA=""
54
57
 
55
- local reg_url index bundled burl
56
- bundled="$(default_index_file)"
58
+ local reg_url index
57
59
  if [ -n "$manifest" ] && [ -f "$manifest" ]; then
58
60
  while IFS= read -r reg_url; do
59
61
  [ -n "$reg_url" ] || continue
@@ -66,37 +68,18 @@ resolve_package_source() {
66
68
  if [ -n "$RES_URL" ]; then
67
69
  RES_PATH="$(qmap_field "$index" "packages" "$name" "path")"
68
70
  RES_VIA="registry:$reg_url"
69
- # Shadowing a bundled name with a DIFFERENT source is legal
70
- # (that is what overriding means) but never silent.
71
- if [ -n "$bundled" ]; then
72
- burl="$(qmap_field "$bundled" "packages" "$name" "url")"
73
- if [ -n "$burl" ] && [ "$burl" != "$RES_URL" ]; then
74
- echo " WARN: $name from this registry overrides the bundled source ($burl)" >&2
75
- fi
76
- fi
77
71
  return 0
78
72
  fi
79
73
  done < <(registries_list "$manifest")
80
74
  fi
81
-
82
- if [ -n "$bundled" ]; then
83
- RES_URL="$(qmap_field "$bundled" "packages" "$name" "url")"
84
- if [ -n "$RES_URL" ]; then
85
- RES_PATH="$(qmap_field "$bundled" "packages" "$name" "path")"
86
- RES_VIA="index"
87
- return 0
88
- fi
89
- fi
90
-
91
- RES_URL="https://github.com/${scope#@}/$short.git"
92
- RES_PATH=""
93
- RES_VIA="convention"
75
+ # No trusted registry declares the name. Deliberately NOT an invented
76
+ # URL: the caller reports it, with suggestions.
94
77
  return 0
95
78
  }
96
79
 
97
80
  # suggest_similar <manifest-or-empty> <@scope/name> — "Did you mean" lines to
98
81
  # stderr, matched on the short name (singular/plural tolerant) across every
99
- # project registry and the bundled index.
82
+ # trusted registry.
100
83
  suggest_similar() {
101
84
  local manifest="$1" name="$2"
102
85
  local want cand cshort reg_url index seen=" "
@@ -121,7 +104,6 @@ suggest_similar() {
121
104
  _suggest_from "$index"
122
105
  done < <(registries_list "$manifest")
123
106
  fi
124
- _suggest_from "$(default_index_file)"
125
107
  }
126
108
 
127
109
  # fetch_package <url> <ref> <subpath> <dest-dir>
@@ -131,6 +113,8 @@ suggest_similar() {
131
113
  # store holds the bytes the package published.
132
114
  fetch_package() {
133
115
  local url="$1" ref="$2" subpath="$3" dest="$4"
116
+ assert_safe_source_url "$url"
117
+ assert_safe_ref "$ref"
134
118
  # Bundle seed: the engine-content package at the CLI's own version copies
135
119
  # from the npm bundle — no network, which keeps init / fresh-clone install
136
120
  # / migrate offline exactly like the staging they replace. Keyed on the
@@ -138,12 +122,14 @@ fetch_package() {
138
122
  # to the normal clone.
139
123
  if [ "$url" = "$SYNC_PKG_URL" ] && [ "$subpath" = "$SYNC_PKG_PATH" ] \
140
124
  && [ "${ref#v}" = "$(bundled_engine_version)" ]; then
125
+ [ -n "${IS_BUNDLED_PKG_DIR:-}" ] && [ -d "$IS_BUNDLED_PKG_DIR" ] \
126
+ || die "bundled engine content not found next to the CLI — reinstall @ainova-systems/intelligence"
141
127
  rm -rf "$dest"
142
128
  mkdir -p "$dest"
143
- cp -R "$IS_ENGINE_DIR/." "$dest/"
129
+ cp -R "$IS_BUNDLED_PKG_DIR/." "$dest/"
144
130
  rm -rf "$dest/.git"
145
- if [ -f "$IS_ENGINE_DIR/scripts/ENGINE_SHA" ]; then
146
- tr -d ' \t\r\n' < "$IS_ENGINE_DIR/scripts/ENGINE_SHA"
131
+ if [ -f "$IS_ENGINE_DIR/ENGINE_SHA" ]; then
132
+ tr -d ' \t\r\n' < "$IS_ENGINE_DIR/ENGINE_SHA"
147
133
  fi
148
134
  return 0
149
135
  fi
@@ -152,20 +138,20 @@ fetch_package() {
152
138
  [ -n "$ref" ] && branch_arg=(--branch "$ref")
153
139
  tmp="$(mktemp -d -t intelligence-pkg-XXXXXX 2>/dev/null || mktemp -d)"
154
140
  if ! GIT_TERMINAL_PROMPT=0 git -c core.symlinks=false -c core.autocrlf=false -c core.eol=lf \
155
- clone --depth 1 "${branch_arg[@]+"${branch_arg[@]}"}" --quiet "$url" "$tmp/clone" 2>/dev/null; then
141
+ clone --depth 1 "${branch_arg[@]+"${branch_arg[@]}"}" --quiet -- "$url" "$tmp/clone" 2>/dev/null; then
156
142
  # A SHA is not clonable with --branch — fall back to a full clone + checkout.
157
143
  rm -rf "$tmp/clone"
158
144
  GIT_TERMINAL_PROMPT=0 git -c core.symlinks=false -c core.autocrlf=false -c core.eol=lf \
159
- clone --quiet "$url" "$tmp/clone" 2>/dev/null \
145
+ clone --quiet -- "$url" "$tmp/clone" 2>/dev/null \
160
146
  || { rm -rf "$tmp"; die "cannot clone $url"; }
161
- [ -z "$ref" ] || git -C "$tmp/clone" checkout --quiet "$ref" \
147
+ [ -z "$ref" ] || git -C "$tmp/clone" checkout --quiet "$ref" -- \
162
148
  || { rm -rf "$tmp"; die "ref '$ref' not found in $url"; }
163
149
  fi
164
150
  sha="$(git -C "$tmp/clone" rev-parse HEAD)"
165
151
  src="$tmp/clone"
166
152
  if [ -n "$subpath" ]; then
167
153
  case "$subpath" in
168
- *..*) rm -rf "$tmp"; die "unsafe path '$subpath'" ;;
154
+ *..*|/*|*[\"\']*) rm -rf "$tmp"; die "unsafe path '$subpath'" ;;
169
155
  esac
170
156
  src="$tmp/clone/$subpath"
171
157
  [ -d "$src" ] || { rm -rf "$tmp"; die "path '$subpath' not found in $url${ref:+@$ref}" ; }
@@ -182,14 +168,14 @@ fetch_package() {
182
168
 
183
169
  # wire_package_sources <manifest> <@scope/name> <store-rel-dir>
184
170
  # The provides convention: whichever of rules/agents/skills the installed
185
- # package has becomes a sources entry. Reuses the engine's idempotent,
186
- # comment-preserving _mig_add_source.
171
+ # package has becomes a sources entry — inserted FIRST in its section, so
172
+ # project-owned entries come later and win on same-named artifacts.
187
173
  wire_package_sources() {
188
174
  local manifest="$1" name="$2" rel="$3" root="$4"
189
175
  local section
190
176
  for section in rules agents skills; do
191
177
  if [ -d "$root/$rel/$section" ]; then
192
- _mig_add_source "$manifest" "$section" "$rel/$section"
178
+ sources_add_entry_first "$manifest" "$section" "$rel/$section"
193
179
  fi
194
180
  done
195
181
  }
package/cli/lib/semver.sh CHANGED
@@ -75,10 +75,11 @@ semver_match() {
75
75
  # onto their tag.
76
76
  list_remote_versions() {
77
77
  local url="$1"
78
+ assert_safe_source_url "$url"
78
79
  # `|| true` inside the pipeline: an unreachable repo must read as "no
79
80
  # versions", not kill a pipefail caller mid-command-substitution —
80
81
  # reachability is the CALLER's question (add probes it explicitly).
81
- { GIT_TERMINAL_PROMPT=0 git ls-remote --tags "$url" 2>/dev/null || true; } \
82
+ { GIT_TERMINAL_PROMPT=0 git ls-remote --tags -- "$url" 2>/dev/null || true; } \
82
83
  | awk '{
83
84
  sub(/\r$/, "")
84
85
  ref = $2
@@ -95,7 +96,8 @@ list_remote_versions() {
95
96
  # prints "<tag> <sha>". Peeled sha (the commit a tag object points at) wins.
96
97
  remote_tag_for_version() {
97
98
  local url="$1" ver="${2#v}"
98
- { GIT_TERMINAL_PROMPT=0 git ls-remote --tags "$url" 2>/dev/null || true; } \
99
+ assert_safe_source_url "$url"
100
+ { GIT_TERMINAL_PROMPT=0 git ls-remote --tags -- "$url" 2>/dev/null || true; } \
99
101
  | awk -v want="$ver" '
100
102
  {
101
103
  sub(/\r$/, "")
@@ -0,0 +1 @@
1
+ 557cde6997dce612e0e8a5a11adc2472173dcec0
@@ -13,25 +13,20 @@
13
13
 
14
14
  source "$(dirname "${BASH_SOURCE[0]}")/../lib/common.sh"
15
15
 
16
- # Repo-relative paths to the umbrella folder and the engine module, DERIVED —
17
- # never hardcoded. The umbrella is named by the project (`intelligence/`,
18
- # `Intelligence/`, a codename) and the engine lives in a module inside it;
19
- # detect_layout resolved both before any adapter ran. A literal path here would
20
- # print a sync command that does not exist, and would be wrong outright on a
21
- # case-sensitive filesystem — in the one document Cursor, Copilot and Codex all
22
- # read as canonical.
23
- # sync.sh exports both as layout tokens; recompute only when an adapter is
24
- # driven directly (tests), never assume a folder name.
16
+ # Repo-relative paths to the project's content dir and to the installed sync
17
+ # package, taken from the env contract — never hardcoded. The content dir is
18
+ # named by the project (`intelligence/`, a codename) and the package sits in
19
+ # the store; a literal path here would print a sync command that does not
20
+ # exist, and would be wrong outright on a case-sensitive filesystem — in the
21
+ # one document Cursor, Copilot and Codex all read as canonical.
22
+ # The defaults only cover an adapter driven directly (tests); sync.sh always
23
+ # exports both.
25
24
  agents_md_umbrella_rel() {
26
- local repo_root="$1" rel="${IS_UMBRELLA_REL:-}"
27
- [ -n "$rel" ] || rel="$(repo_rel_dir "$repo_root" "${LS_UMBRELLA_DIR:-$repo_root/intelligence}")"
28
- printf '%s' "${rel:-intelligence}"
25
+ printf '%s' "${IS_UMBRELLA_REL:-intelligence}"
29
26
  }
30
27
 
31
28
  agents_md_module_rel() {
32
- local repo_root="$1" rel="${IS_MODULE_REL:-}"
33
- [ -n "$rel" ] || rel="$(repo_rel_dir "$repo_root" "${LS_MODULE_DIR:-$repo_root/intelligence/sync}")"
34
- printf '%s' "${rel:-intelligence/sync}"
29
+ printf '%s' "${IS_MODULE_REL:-.intelligence/packages/@ainova-systems/sync}"
35
30
  }
36
31
 
37
32
  # Append the static header block from config.yaml (or a fallback).