@ainova-systems/intelligence 0.18.1 → 0.19.0

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 (35) hide show
  1. package/cli/commands/init.sh +6 -1
  2. package/cli/commands/package.sh +8 -3
  3. package/cli/commands/source.sh +55 -16
  4. package/cli/intelligence +1 -1
  5. package/cli/internal/check.sh +64 -13
  6. package/cli/internal/package-add.sh +25 -4
  7. package/cli/internal/package-alias.sh +51 -0
  8. package/cli/internal/package-list.sh +16 -2
  9. package/cli/internal/package-remove.sh +3 -1
  10. package/cli/internal/package-update.sh +12 -4
  11. package/cli/lib/adapter-lifecycle.sh +5 -2
  12. package/cli/lib/cli-common.sh +4 -1
  13. package/cli/lib/gitignore.sh +128 -34
  14. package/cli/lib/manifest.sh +168 -20
  15. package/cli/lib/registry.sh +18 -13
  16. package/engine/ENGINE_SHA +1 -1
  17. package/engine/VERSION +1 -1
  18. package/engine/adapters/_template.sh +6 -2
  19. package/engine/adapters/agents.sh +3 -0
  20. package/engine/adapters/claude.sh +21 -10
  21. package/engine/adapters/codex.sh +8 -12
  22. package/engine/adapters/copilot.sh +46 -2
  23. package/engine/adapters/cursor.sh +6 -1
  24. package/engine/lib/adapter-contract.sh +17 -6
  25. package/engine/lib/common.sh +418 -50
  26. package/engine/sync.sh +9 -0
  27. package/package.json +1 -1
  28. package/packages/sync/references/adapters.md +39 -9
  29. package/packages/sync/references/conventions.md +44 -7
  30. package/packages/sync/references/onboarding-migration.md +3 -3
  31. package/packages/sync/skills/intelligence-learn-from-repository/SKILL.md +3 -2
  32. package/packages/sync/skills/intelligence-review-context/SKILL.md +2 -1
  33. package/packages/sync/skills/intelligence-update-context/SKILL.md +4 -2
  34. package/packages/sync/skills/intelligence-update-context/references/agents.md +7 -3
  35. package/packages/sync/skills/intelligence-update-context/references/skills.md +3 -1
@@ -84,7 +84,11 @@ print_new_project_onboarding() {
84
84
  echo " intelligence adapter disable cursor"
85
85
  echo " Version control:"
86
86
  echo " Generated adapter output is gitignored by CLI-owned path."
87
- echo " Commit intelligence.yaml, intelligence.lock, intelligence/, AGENTS.md, and .github/."
87
+ echo " Commit intelligence.yaml, intelligence.lock, intelligence/, and AGENTS.md."
88
+ if [ "$(is_target_enabled "$manifest" copilot)" = "1" ]; then
89
+ echo " Copilot on github.com reads the repository, not a sync: to commit its output, set"
90
+ echo " targets.copilot.commit_output: true, then run intelligence init."
91
+ fi
88
92
  echo " Existing .vscodeignore, .npmignore, and .dockerignore files receive packaging exclusions."
89
93
  echo " Shared tool settings remain trackable; review the exact ownership policy:"
90
94
  echo " https://github.com/ainova-systems/intelligence/blob/main/packages/sync/references/conventions.md#generated-output-and-version-control"
@@ -296,6 +300,7 @@ manifest="$root/intelligence.yaml"
296
300
  echo "# its stack, and the constraints to respect"
297
301
  echo "# $content_dir/agents/<name>.md a persona (frontmatter: name, description,"
298
302
  echo "# tier: frontier|heavy|standard|light,"
303
+ echo "# effort: low|medium|high|xhigh|max|ultra,"
299
304
  echo "# access: full|readonly)"
300
305
  echo "# $content_dir/skills/<name>/SKILL.md a procedure invoked as /<name>"
301
306
  echo "# '.intelligence/' is CLI-managed (packages + engine content) — never edit it."
@@ -1,10 +1,10 @@
1
1
  #!/bin/bash
2
- # intelligence package <add|remove|list|search> ...
2
+ # intelligence package <add|remove|alias|list|search> ...
3
3
  set -euo pipefail
4
4
  source "$CLI_DIR/lib/cli-common.sh"
5
5
 
6
6
  action="${1:-}"
7
- [ -n "$action" ] || die "usage: intelligence package <add|remove|list|search> [args]"
7
+ [ -n "$action" ] || die "usage: intelligence package <add|remove|alias|list|search> [args]"
8
8
  shift
9
9
 
10
10
  case "$action" in
@@ -18,6 +18,11 @@ case "$action" in
18
18
  ensure_project_current "$IP_ROOT"
19
19
  exec bash "$CLI_DIR/internal/package-remove.sh" "$@"
20
20
  ;;
21
+ alias)
22
+ require_cli_project
23
+ ensure_project_current "$IP_ROOT"
24
+ exec bash "$CLI_DIR/internal/package-alias.sh" "$@"
25
+ ;;
21
26
  list)
22
27
  exec bash "$CLI_DIR/internal/package-list.sh" "$@"
23
28
  ;;
@@ -25,6 +30,6 @@ case "$action" in
25
30
  exec bash "$CLI_DIR/internal/package-search.sh" "$@"
26
31
  ;;
27
32
  *)
28
- die "usage: intelligence package <add|remove|list|search> [args]"
33
+ die "usage: intelligence package <add|remove|alias|list|search> [args]"
29
34
  ;;
30
35
  esac
@@ -10,8 +10,11 @@
10
10
  # that ships it) belongs after the store entries and before the project's.
11
11
  #
12
12
  # Installed package content is NOT managed here: `.intelligence/` entries are
13
- # written by `package add` and removed by `package remove`, and a hand-placed
14
- # entry under the store does not survive the next lifecycle alignment.
13
+ # written by `package add` and removed by `package remove`, and so are the
14
+ # entries that name a package's directory by its full name or alias
15
+ # (`@scope/name/<dir>`, `<alias>:<dir>`, decision 0019). Any of those spellings
16
+ # is still a valid `--before` / `--after` anchor: that is how a directory lands
17
+ # next to a package.
15
18
  set -euo pipefail
16
19
  source "$CLI_DIR/lib/cli-common.sh"
17
20
 
@@ -36,11 +39,32 @@ assert_source_section() {
36
39
  # repaired — a source entry decides which artifact wins, so a path the user did
37
40
  # not type is the wrong kind of help.
38
41
 
42
+ # assert_not_package_reference <manifest> <dir> <add|remove> — refuse an entry
43
+ # the engine reads as a package reference: a directory of a package `packages:`
44
+ # declares, by full name or alias, or an alias no package declares. It is
45
+ # package territory however it is spelled.
46
+ assert_not_package_reference() {
47
+ local state rendered rest pkg
48
+ IFS=$'\037' read -r state rendered <<< "$(sources_classify_entry "$1" "$2")"
49
+ rest="${rendered#.intelligence/packages/}"
50
+ pkg="${rest%%/*}/"
51
+ rest="${rest#*/}"
52
+ pkg="$pkg${rest%%/*}"
53
+ case "$state/$3" in
54
+ path/*) return 0 ;;
55
+ ok/add) die "'$2' names a directory of the installed package $pkg — 'intelligence package add' wires a package's directories into sources" ;;
56
+ ok/remove) die "'$2' is installed package content — remove the package instead: intelligence package remove $pkg" ;;
57
+ */add) die "'$2' is a package reference, not a project directory — 'intelligence package add' wires a package's directories into sources" ;;
58
+ *) die "'$2' is a package reference, not a project directory — 'intelligence package remove <@scope/name>' removes a package's entries" ;;
59
+ esac
60
+ }
61
+
39
62
  # assert_valid_source_dir <root> <dir> — refuse what the engine cannot report.
40
63
  # The shape checks are `source_entry_problem`, shared with `status --check` so
41
64
  # one definition covers both the entry about to be written and the one already
42
- # in the manifest; the store rule is this command's alone, because a
43
- # `.intelligence/` entry is legitimate when `package add` wrote it.
65
+ # in the manifest; the package rules are this command's alone, because a
66
+ # package entry is legitimate when `package add` wrote it or a person spelled
67
+ # it by the package's name.
44
68
  assert_valid_source_dir() {
45
69
  local root="$1" dir="$2" problem
46
70
  [ -n "$dir" ] || die "$USAGE"
@@ -50,26 +74,34 @@ assert_valid_source_dir() {
50
74
  die "'$dir' is inside the CLI-managed package store — install content with 'intelligence package add', and it is wired into sources automatically"
51
75
  ;;
52
76
  esac
77
+ assert_not_package_reference "$root/intelligence.yaml" "$dir" add
53
78
  problem="$(source_entry_problem "$root" "$dir")"
54
79
  [ -z "$problem" ] || die "invalid source '$dir': it $problem"
55
80
  }
56
81
 
57
82
  # Print one section with its override direction and the state of each entry.
58
83
  # Position is the whole point of the block, so every mutation ends by showing
59
- # the order it produced rather than only naming what it wrote.
84
+ # the order it produced rather than only naming what it wrote. Entries are
85
+ # listed as written and judged by the directory the engine renders them as.
60
86
  print_section() {
61
- local manifest="$1" root="$2" section="$3" entry n=0 note
87
+ local manifest="$1" root="$2" section="$3" entry state dir n=0 note
62
88
  echo "sources.$section (a later entry overrides an earlier one):"
63
- while IFS= read -r entry; do
89
+ [ -f "$manifest" ] || { echo " (none)"; return 0; }
90
+ while IFS=$'\037' read -r entry state dir _; do
64
91
  [ -n "$entry" ] || continue
65
92
  n=$((n + 1))
66
93
  note=""
67
- case "$entry" in
68
- .intelligence/*) note=" package" ;;
94
+ case "$state" in
95
+ path|ok)
96
+ case "$dir" in
97
+ .intelligence/*) note=" package" ;;
98
+ esac
99
+ [ -d "$root/$dir" ] || note="$note MISSING"
100
+ ;;
101
+ *) note=" UNRESOLVED — intelligence status --check" ;;
69
102
  esac
70
- [ -d "$root/$entry" ] || note="$note MISSING"
71
103
  printf ' %d. %s%s\n' "$n" "$entry" "$note"
72
- done < <(sources_list_entries "$manifest" "$section")
104
+ done < <(read_source_entries "$manifest" "$section")
73
105
  [ "$n" -eq 0 ] && echo " (none)"
74
106
  return 0
75
107
  }
@@ -110,10 +142,16 @@ case "$action" in
110
142
  ensure_project_current "$IP_ROOT"
111
143
  manifest="$IP_ROOT/intelligence.yaml"
112
144
 
113
- if [ -n "$anchor" ] && ! sources_has_entry "$manifest" "$section" "$anchor"; then
114
- echo "ERROR: sources.$section does not list '$anchor'." >&2
115
- print_section "$manifest" "$IP_ROOT" "$section" >&2
116
- exit 1
145
+ # The anchor may be spelled differently from the entry it means — a
146
+ # package's store path, full name or alias — so it is matched by the
147
+ # directory it names and placed against the entry as written.
148
+ if [ -n "$anchor" ]; then
149
+ if ! listed="$(sources_find_entry "$manifest" "$section" "$anchor")"; then
150
+ echo "ERROR: sources.$section does not list '$anchor'." >&2
151
+ print_section "$manifest" "$IP_ROOT" "$section" >&2
152
+ exit 1
153
+ fi
154
+ anchor="$listed"
117
155
  fi
118
156
 
119
157
  if sources_has_entry "$manifest" "$section" "$dir"; then
@@ -149,8 +187,9 @@ case "$action" in
149
187
  esac
150
188
 
151
189
  require_cli_project
152
- ensure_project_current "$IP_ROOT"
153
190
  manifest="$IP_ROOT/intelligence.yaml"
191
+ assert_not_package_reference "$manifest" "$dir" remove
192
+ ensure_project_current "$IP_ROOT"
154
193
 
155
194
  if ! sources_has_entry "$manifest" "$section" "$dir"; then
156
195
  echo "not listed: sources.$section does not hold $dir"
package/cli/intelligence CHANGED
@@ -61,7 +61,7 @@ Usage: intelligence <command> [args]
61
61
  --latest Move one named package past its range to the newest version
62
62
  upgrade [--next] [--preview|--apply] Replace the installed CLI with the newest npm version
63
63
  --next Follow the prerelease line instead of the stable one
64
- package <command> add | remove | list | search
64
+ package <command> add | remove | alias | list | search
65
65
  source <command> add | remove | list — the sources: block (own directories)
66
66
  adapter <command> list | create | enable | disable | remove
67
67
  status [--check] Project state; --check runs deep consistency checks
@@ -119,6 +119,18 @@ while IFS= read -r name; do
119
119
  fi
120
120
  done < <(qmap_keys "$manifest" "packages")
121
121
 
122
+ # Aliases name packages in sources: (decision 0019). One the engine would not
123
+ # resolve, or one two packages state, leaves every <alias>:<dir> entry naming
124
+ # nothing. An invalid alias is never echoed: it is manifest input that may hold
125
+ # anything.
126
+ while IFS=$'\037' read -r name alias verdict; do
127
+ [ -n "$name" ] || continue
128
+ case "$verdict" in
129
+ invalid) warn "$name declares an alias that is not one — expected $IS_PKG_ALIAS_RULE: intelligence package alias $name <alias>" ;;
130
+ ambiguous) warn "$name declares alias '$alias', which another package declares too — an alias names one package" ;;
131
+ esac
132
+ done < <(read_package_aliases "$manifest")
133
+
122
134
  # Adapter contracts are the shared source of truth for dependencies, rollback,
123
135
  # onboarding backup and Git policy. Validate every declared target, including
124
136
  # disabled ones, so a later enable cannot reveal a stale custom adapter.
@@ -135,7 +147,7 @@ while IFS= read -r target; do
135
147
  warn "adapter '$target' is declared but its implementation is missing"
136
148
  continue
137
149
  fi
138
- records="$(adapter_contract_records "$target" "$adapter_file" "$output" 2>/dev/null || true)"
150
+ records="$(adapter_contract_records "$target" "$adapter_file" "$output" "$manifest" 2>/dev/null || true)"
139
151
  if ! printf '%s\n' "$records" | grep -Fqx $'version\t1'; then
140
152
  warn "adapter '$target' has an invalid ownership contract"
141
153
  continue
@@ -150,18 +162,29 @@ while IFS= read -r target; do
150
162
  && [ "$(is_target_enabled "$manifest" "$value")" != "1" ]; then
151
163
  warn "enabled adapter '$target' requires enabled adapter '$value'"
152
164
  fi
165
+ # Presence is the writer's own test: a line `init` treats as present
166
+ # and leaves alone must not be reported missing here, or the warning
167
+ # can never clear — a CRLF .gitignore on Linux and macOS did exactly that.
153
168
  if [ "$(is_target_enabled "$manifest" "$target")" = "1" ]; then
154
169
  case "$kind" in
155
170
  ignore)
156
- grep -Fqx -- "$value" "$IP_ROOT/.gitignore" 2>/dev/null \
157
- || warn "adapter '$target' Git policy is missing '$value'"
171
+ ignore_file_has_line "$IP_ROOT/.gitignore" "$value" \
172
+ || warn "adapter '$target' Git policy is missing '$value' — run 'intelligence init'"
173
+ ;;
174
+ unignore)
175
+ if gitignore_managed_has_line "$IP_ROOT" "$value"; then
176
+ warn "adapter '$target' Git policy keeps '$value' tracked, but .gitignore still ignores it — run 'intelligence init'"
177
+ fi
158
178
  ;;
159
179
  include)
160
- if ! grep -Fqx -- "!$value" "$IP_ROOT/.gitignore" 2>/dev/null; then
161
- warn "adapter '$target' Git policy is missing '!$value'"
180
+ if ! ignore_file_has_line "$IP_ROOT/.gitignore" "!$value"; then
181
+ warn "adapter '$target' Git policy is missing '!$value' — run 'intelligence init'"
162
182
  elif git -C "$IP_ROOT" rev-parse --is-inside-work-tree >/dev/null 2>&1; then
183
+ # init moves the negation chain last, which beats any
184
+ # later rule in .gitignore; a deeper .gitignore still
185
+ # wins, so the finding also says how to find it.
163
186
  if git -C "$IP_ROOT" check-ignore -q --no-index -- "$value"; then
164
- warn "adapter '$target' Git policy cannot re-include '$value' because another ignore rule still wins"
187
+ warn "adapter '$target' Git policy cannot re-include '$value' because another ignore rule still wins — run 'intelligence init'; if it still wins, 'git check-ignore -v --no-index -- $value' names the rule"
165
188
  else
166
189
  ignore_rc=$?
167
190
  [ "$ignore_rc" -eq 1 ] \
@@ -188,22 +211,50 @@ if [ "$lock_valid" -eq 1 ] && [ -f "$lock" ]; then
188
211
  fi
189
212
 
190
213
  # Sources must exist (a missing dir is silently skipped by the engine, which
191
- # is exactly why doctor names it).
214
+ # is exactly why doctor names it). Each entry is judged by the directory the
215
+ # engine renders it as, read through the same parser: a package reference by
216
+ # its store path, one that resolves to nothing by why.
192
217
  for section in rules agents skills; do
193
- while IFS= read -r src; do
218
+ seen=$'\n'
219
+ while IFS=$'\037' read -r src state dir alias holders; do
194
220
  [ -n "$src" ] || continue
221
+ case "$state" in
222
+ unknown|ambiguous|invalid)
223
+ source_reference_problem_var "$state" "$alias" "$holders"
224
+ case "$state" in
225
+ unknown) warn "sources.$section '$src' $IS_SOURCE_PROBLEM — declare it with 'intelligence package alias <@scope/name> $alias', or correct the entry" ;;
226
+ *) warn "sources.$section '$src' $IS_SOURCE_PROBLEM" ;;
227
+ esac
228
+ continue
229
+ ;;
230
+ esac
231
+ # One package directory under two spellings is listed twice, and the
232
+ # later one moves the package past every entry between them.
233
+ case "$dir" in
234
+ .intelligence/packages/*)
235
+ case "$seen" in
236
+ *$'\n'"$dir"$'\037'*)
237
+ first="${seen#*$'\n'"$dir"$'\037'}"
238
+ first="${first%%$'\n'*}"
239
+ [ "$first" = "$src" ] \
240
+ || warn "sources.$section names $dir twice, as '$first' and as '$src' — keep one: where it stands decides which artifacts win"
241
+ ;;
242
+ *) seen="$seen$dir"$'\037'"$src"$'\n' ;;
243
+ esac
244
+ ;;
245
+ esac
195
246
  case "$src" in
196
- git+*|@*) continue ;;
247
+ git+*) continue ;;
197
248
  esac
198
249
  # A hand-edited entry the engine cannot render is worse than a missing
199
250
  # one: it is skipped without a word and the sync still reports ok.
200
- problem="$(source_entry_problem "$IP_ROOT" "$src")"
251
+ problem="$(source_entry_problem "$IP_ROOT" "$dir")"
201
252
  if [ -n "$problem" ]; then
202
253
  warn "sources.$section '$src' $problem"
203
254
  continue
204
255
  fi
205
- if [ ! -d "$IP_ROOT/$src" ]; then
206
- case "$src" in
256
+ if [ ! -d "$IP_ROOT/$dir" ]; then
257
+ case "$dir" in
207
258
  # Store content is restorable state: absent means un-installed.
208
259
  .intelligence/*)
209
260
  if [ "$lock_valid" -eq 1 ]; then
@@ -219,7 +270,7 @@ for section in rules agents skills; do
219
270
  *) ok "sources.$section '$src' — not created yet (optional)" ;;
220
271
  esac
221
272
  fi
222
- done < <(read_yaml_list "$manifest" "$section")
273
+ done < <(read_source_entries "$manifest" "$section")
223
274
  done
224
275
 
225
276
  if [ "$problems" -eq 0 ]; then
@@ -9,15 +9,18 @@ set -euo pipefail
9
9
  source "$CLI_DIR/lib/cli-common.sh"
10
10
 
11
11
  spec="${1:-}"
12
- [ -n "$spec" ] || die "usage: intelligence package 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]> [--name @scope/name] [--alias <alias>] [--no-sync]"
13
13
  shift || true
14
14
 
15
15
  no_sync=0
16
16
  name_override=""
17
+ alias_given=0
18
+ alias_req=""
17
19
  while [ $# -gt 0 ]; do
18
20
  case "$1" in
19
21
  --no-sync) no_sync=1 ;;
20
22
  --name) shift; name_override="${1:-}" ;;
23
+ --alias) shift; alias_given=1; alias_req="${1:-}" ;;
21
24
  *) die "unknown option '$1'" ;;
22
25
  esac
23
26
  shift || true
@@ -77,6 +80,20 @@ esac
77
80
  [ -n "$name_override" ] && name="$name_override"
78
81
  assert_valid_pkg_name "$name"
79
82
 
83
+ # The alias is settled before anything is fetched or written: a refusal leaves
84
+ # the manifest, the lock and the store as they were. Re-adding a package keeps
85
+ # the alias it declares unless --alias names another one, and never takes away
86
+ # an alias `sources:` still spells entries with.
87
+ alias="$(package_alias_of "$manifest" "$name")"
88
+ if [ "$alias_given" -eq 1 ]; then
89
+ assert_valid_alias "$alias_req"
90
+ assert_alias_free "$manifest" "$name" "$alias_req"
91
+ if [ -n "$alias" ] && [ "$alias" != "$alias_req" ]; then
92
+ assert_alias_unused "$manifest" "$name" "$alias" replace
93
+ fi
94
+ alias="$alias_req"
95
+ fi
96
+
80
97
  if [ "$mode" = "registry" ]; then
81
98
  resolve_package_source "$manifest" "$name"
82
99
  url="$RES_URL"; path="$RES_PATH"
@@ -129,8 +146,6 @@ rel=".intelligence/packages/$name"
129
146
  store_record_remove "$IP_ROOT" "$name"
130
147
  sha="$(fetch_package "$url" "${ref:-$resolved_tag}" "$path" "$IP_ROOT/$rel")"
131
148
 
132
- wire_package_sources "$manifest" "$name" "$rel" "$IP_ROOT"
133
-
134
149
  # The manifest records requested intent only. The resolved source URL/path,
135
150
  # tag/ref and SHA live in the required lock. Re-adding a package is the
136
151
  # explicit operation for changing its trusted source.
@@ -142,6 +157,12 @@ elif [ -n "$requested" ]; then
142
157
  else
143
158
  die "internal: package '$name' has neither a requested version nor ref"
144
159
  fi
160
+ [ -z "$alias" ] || qmap_set "$manifest" "packages" "$name" "alias" "$alias"
161
+
162
+ # Wired once the package and its alias are declared, so a section that already
163
+ # names one of its directories as @scope/name/<dir> or <alias>:<dir> is left as
164
+ # it stands instead of gaining the store path beside it.
165
+ wire_package_sources "$manifest" "$name" "$rel" "$IP_ROOT"
145
166
 
146
167
  lock_upsert "$lock" "$name" "$requested" "$url" "$path" "${ref:-$resolved_tag}" "$sha"
147
168
  store_record_set "$IP_ROOT" "$name" "$url" "$path" "${ref:-$resolved_tag}" "$sha"
@@ -150,7 +171,7 @@ sections=""
150
171
  for s in rules agents skills; do
151
172
  [ -d "$IP_ROOT/$rel/$s" ] && sections="$sections $s"
152
173
  done
153
- echo "+ $name@${resolved_tag:-${ref:-HEAD}} (${sections# })"
174
+ echo "+ $name@${resolved_tag:-${ref:-HEAD}} (${sections# })${alias:+ alias $alias}"
154
175
 
155
176
  if [ "$no_sync" -eq 0 ]; then
156
177
  exec bash "$CLI_DIR/commands/sync.sh"
@@ -0,0 +1,51 @@
1
+ #!/bin/bash
2
+ # Internal package alias operation: set, replace or remove the alias a package
3
+ # declares in `packages:`. `sources:` spells that package's directories as
4
+ # <alias>:<dir> (decision 0019); the alias is the CLI's to write, like the rest
5
+ # of the block, and one alias names exactly one package.
6
+ set -euo pipefail
7
+ source "$CLI_DIR/lib/cli-common.sh"
8
+
9
+ USAGE="usage: intelligence package alias <@scope/name> <alias>
10
+ intelligence package alias <@scope/name> --remove"
11
+
12
+ [ $# -eq 2 ] || die "$USAGE"
13
+ name="$1"
14
+ want="$2"
15
+
16
+ require_cli_project
17
+ project_lock_hold "$IP_ROOT"
18
+ manifest="$IP_ROOT/intelligence.yaml"
19
+ assert_valid_pkg_name "$name"
20
+
21
+ known=0
22
+ while IFS= read -r k; do
23
+ [ "$k" = "$name" ] && known=1
24
+ done < <(qmap_keys "$manifest" "packages")
25
+ [ "$known" -eq 1 ] || die "package '$name' is not in the manifest — 'intelligence package list' shows the declared ones"
26
+
27
+ current="$(package_alias_of "$manifest" "$name")"
28
+
29
+ if [ "$want" = "--remove" ]; then
30
+ # An entry can only spell a well-formed alias, so only a well-formed one can
31
+ # still be in use; a malformed one is dropped like any other.
32
+ [ -z "$current" ] || assert_alias_unused "$manifest" "$name" "$current" remove
33
+ if [ -z "$(qmap_field "$manifest" "packages" "$name" "alias")" ]; then
34
+ echo "$name declares no alias"
35
+ exit 0
36
+ fi
37
+ qmap_delete_field "$manifest" "packages" "$name" "alias"
38
+ echo "$name: alias removed"
39
+ exit 0
40
+ fi
41
+
42
+ case "$want" in -*) die "$USAGE" ;; esac
43
+ assert_valid_alias "$want"
44
+ if [ "$current" = "$want" ]; then
45
+ echo "$name: alias $want (unchanged)"
46
+ exit 0
47
+ fi
48
+ assert_alias_free "$manifest" "$name" "$want"
49
+ [ -z "$current" ] || assert_alias_unused "$manifest" "$name" "$current" replace
50
+ qmap_set "$manifest" "packages" "$name" "alias" "$want"
51
+ echo "$name: alias $want — sources: may name its directories $want:<dir>"
@@ -9,6 +9,10 @@ lock="$IP_ROOT/intelligence.lock"
9
9
  lock_valid=1
10
10
  if ! check_project_lock "$IP_ROOT"; then lock_valid=0; fi
11
11
 
12
+ # Aliases come from the parser the engine resolves them with, in one pass; an
13
+ # alias it would not resolve is shown as such, never echoed.
14
+ aliases="$(read_package_aliases "$manifest")"
15
+
12
16
  count=0
13
17
  while IFS= read -r name; do
14
18
  [ -n "$name" ] || continue
@@ -29,11 +33,21 @@ while IFS= read -r name; do
29
33
  else
30
34
  state="store missing — run 'intelligence sync'"
31
35
  fi
32
- printf '%s %s locked:%s %s\n' \
36
+ alias_note=""
37
+ while IFS=$'\037' read -r a_name a_alias a_verdict; do
38
+ [ "$a_name" = "$name" ] || continue
39
+ case "$a_verdict" in
40
+ ok) alias_note=" alias:$a_alias" ;;
41
+ ambiguous) alias_note=" alias:$a_alias (another package declares it too)" ;;
42
+ *) alias_note=" alias:invalid — intelligence status --check" ;;
43
+ esac
44
+ done <<< "$aliases"
45
+ printf '%s %s locked:%s %s%s\n' \
33
46
  "$name" \
34
47
  "${ref:+ref:$ref}${ref:-${range:-*}}" \
35
48
  "$(pin_label "$ref" "$resolved" "$sha")" \
36
- "$state"
49
+ "$state" \
50
+ "$alias_note"
37
51
  done < <(qmap_keys "$manifest" "packages")
38
52
 
39
53
  if [ "$count" -eq 0 ]; then
@@ -39,7 +39,9 @@ while IFS= read -r k; do
39
39
  done < <(qmap_keys "$manifest" "packages")
40
40
  [ "$known" -eq 1 ] || die "package '$name' is not in the manifest"
41
41
 
42
- unwire_package_sources "$manifest" "$rel"
42
+ # Every entry naming the package, in every spelling, goes while `packages:`
43
+ # still declares it and its alias: that is what resolves the references.
44
+ unwire_package_sources "$manifest" "$name"
43
45
  qmap_delete_key "$manifest" "packages" "$name"
44
46
  lock_remove "$lock" "$name"
45
47
  rm -rf "${IP_ROOT:?}/$rel"
@@ -167,13 +167,21 @@ while IFS= read -r name; do
167
167
  fi
168
168
  rel=".intelligence/packages/$name"
169
169
  # Fetch into staging FIRST: a failed clone must leave the current install
170
- # fully wired and intact. Only after success is the old shape unwired
171
- # (the new version may have dropped a section dir), the store swapped,
172
- # and the new shape wired.
170
+ # fully wired and intact. Only after success are the sections the new
171
+ # version dropped unwired, in every spelling, the store swapped, and the
172
+ # sections it gained wired. A section it keeps stays where it stands in
173
+ # the list: position is the override order between packages, and an
174
+ # update changes content, not which package wins.
173
175
  staging="$IP_ROOT/.intelligence/.staging-$$"
174
176
  rm -rf "$staging"
175
177
  sha="$(fetch_package "$url" "$tag" "$path" "$staging")"
176
- unwire_package_sources "$manifest" "$rel"
178
+ dropped=()
179
+ for section in rules agents skills; do
180
+ [ -d "$staging/$section" ] || dropped+=("$section")
181
+ done
182
+ if [ "${#dropped[@]}" -gt 0 ]; then
183
+ unwire_package_sources "$manifest" "$name" "${dropped[@]}"
184
+ fi
177
185
  store_record_remove "$IP_ROOT" "$name"
178
186
  rm -rf "${IP_ROOT:?}/$rel"
179
187
  mkdir -p "$(dirname "$IP_ROOT/$rel")"
@@ -12,13 +12,16 @@ adapter_file_for() {
12
12
  fi
13
13
  }
14
14
 
15
+ # adapter_records_for <root> <content-dir> <name> <output> [manifest] — pass the
16
+ # manifest whenever the caller applies or checks Git policy; ownership callers
17
+ # may omit it (engine/lib/adapter-contract.sh).
15
18
  adapter_records_for() {
16
- local root="$1" content_dir="$2" name="$3" output="$4" file
19
+ local root="$1" content_dir="$2" name="$3" output="$4" manifest="${5:-}" file
17
20
  file="$(adapter_file_for "$root" "$content_dir" "$name")" || {
18
21
  echo "ERROR: adapter '$name' not found" >&2
19
22
  return 1
20
23
  }
21
- adapter_contract_records "$name" "$file" "$output"
24
+ adapter_contract_records "$name" "$file" "$output" "$manifest"
22
25
  }
23
26
 
24
27
  validate_adapter_contract_for() {
@@ -5,7 +5,8 @@
5
5
  # All YAML reading goes through the engine's lib/common.sh — the CLI never
6
6
  # grows a parallel parser for shapes the engine can already read. The only
7
7
  # CLI-owned parsing lives in lib/manifest.sh (quoted-key `packages:` /
8
- # `registries:` blocks, which the engine deliberately never reads).
8
+ # `registries:` blocks, which the engine never reads beyond the package names
9
+ # and aliases its list parser resolves `sources:` references against).
9
10
 
10
11
  # Engine libraries (readers, is_status, IS_RC_*, engine_version). CLI_DIR /
11
12
  # IS_ENGINE_DIR come exported from the dispatcher.
@@ -545,6 +546,8 @@ project_store_missing() {
545
546
  assert_valid_pkg_name "$name"
546
547
  [ -d "$root/.intelligence/packages/$name" ] || return 0
547
548
  done < <(qmap_keys "$manifest" "packages")
549
+ # The parser hands a package reference over as the store directory it
550
+ # names, so every spelling is checked here as that directory.
548
551
  for section in rules agents skills; do
549
552
  while IFS= read -r src; do
550
553
  case "$src" in