@ainova-systems/intelligence 0.18.1 → 0.20.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 (48) hide show
  1. package/cli/commands/init.sh +6 -1
  2. package/cli/commands/package.sh +8 -3
  3. package/cli/commands/registry.sh +1 -1
  4. package/cli/commands/source.sh +56 -17
  5. package/cli/commands/sync.sh +78 -6
  6. package/cli/intelligence +16 -3
  7. package/cli/internal/check.sh +64 -13
  8. package/cli/internal/convert-legacy.sh +1 -1
  9. package/cli/internal/package-add.sh +25 -4
  10. package/cli/internal/package-alias.sh +51 -0
  11. package/cli/internal/package-list.sh +16 -2
  12. package/cli/internal/package-remove.sh +3 -1
  13. package/cli/internal/package-search.sh +1 -1
  14. package/cli/internal/package-update.sh +14 -6
  15. package/cli/internal/restore.sh +3 -2
  16. package/cli/lib/adapter-lifecycle.sh +5 -2
  17. package/cli/lib/cli-common.sh +149 -34
  18. package/cli/lib/gitignore.sh +128 -34
  19. package/cli/lib/lockfile.sh +14 -4
  20. package/cli/lib/manifest.sh +315 -21
  21. package/cli/lib/qmap.awk +97 -32
  22. package/cli/lib/registry.sh +19 -14
  23. package/cli/lib/sync-cache.sh +596 -142
  24. package/cli/lib/sync-lock.sh +19 -2
  25. package/engine/ENGINE_SHA +1 -1
  26. package/engine/VERSION +1 -1
  27. package/engine/adapters/_template.sh +6 -2
  28. package/engine/adapters/agents.sh +8 -2
  29. package/engine/adapters/antigravity.sh +3 -1
  30. package/engine/adapters/claude.sh +29 -12
  31. package/engine/adapters/codex.sh +11 -13
  32. package/engine/adapters/copilot.sh +54 -4
  33. package/engine/adapters/cursor.sh +13 -2
  34. package/engine/adapters/opencode.sh +7 -1
  35. package/engine/adapters/pi.sh +3 -1
  36. package/engine/lib/adapter-contract.sh +51 -8
  37. package/engine/lib/common.sh +1072 -160
  38. package/engine/lib/contract.sh +5 -0
  39. package/engine/sync.sh +128 -28
  40. package/package.json +1 -1
  41. package/packages/sync/references/adapters.md +44 -9
  42. package/packages/sync/references/conventions.md +48 -8
  43. package/packages/sync/references/onboarding-migration.md +3 -3
  44. package/packages/sync/skills/intelligence-learn-from-repository/SKILL.md +3 -2
  45. package/packages/sync/skills/intelligence-review-context/SKILL.md +2 -1
  46. package/packages/sync/skills/intelligence-update-context/SKILL.md +4 -2
  47. package/packages/sync/skills/intelligence-update-context/references/agents.md +7 -3
  48. 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
@@ -51,7 +51,7 @@ case "$sub" in
51
51
  echo " index.yaml reachable — $n package(s):"
52
52
  qmap_keys "$index" "packages" | sed 's/^/ /'
53
53
  elif [ "$force" -eq 1 ]; then
54
- echo " WARN: no index.yaml at $url — recording anyway (--force)." >&2
54
+ echo "WARNING: no index.yaml at $url — recording anyway (--force)." >&2
55
55
  else
56
56
  echo "ERROR: no index.yaml found at $url — not adding it." >&2
57
57
  echo " A registry is a git repo with index.yaml at its root, mapping names to sources:" >&2
@@ -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
@@ -130,7 +168,7 @@ case "$action" in
130
168
  echo "added: $dir"
131
169
  fi
132
170
  if [ ! -d "$IP_ROOT/$dir" ]; then
133
- echo " WARN: $dir does not exist yet — sync skips a missing source and renders it once the directory appears." >&2
171
+ echo "WARNING: $dir does not exist yet — sync skips a missing source and renders it once the directory appears." >&2
134
172
  fi
135
173
  print_section "$manifest" "$IP_ROOT" "$section"
136
174
  echo "Run 'intelligence sync' to render it."
@@ -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"
@@ -1,14 +1,27 @@
1
1
  #!/bin/bash
2
- # intelligence sync [target] [--compact] [--force] - render intelligence to enabled tools.
2
+ # intelligence sync [target] [--compact] [--force] [--check] - render intelligence
3
+ # to enabled tools, or report whether rendering would change anything.
3
4
  set -euo pipefail
4
5
 
6
+ # Only sync_check may request a comparison from the engine.
7
+ unset IS_SYNC_CHECK IS_SYNC_CHECK_ROOT
8
+
5
9
  # Print before library loading and lifecycle checks so a content verification
6
- # never looks idle. Compact output remains a buffered machine-readable report.
10
+ # never looks idle. Compact output remains a buffered machine-readable report,
11
+ # and a check prints only its verdict.
7
12
  compact=0
13
+ check=0
8
14
  for argument in "$@"; do
9
- [ "$argument" != --compact ] || compact=1
15
+ case "$argument" in
16
+ --compact) compact=1 ;;
17
+ --check) check=1 ;;
18
+ esac
10
19
  done
11
- [ "$compact" -ne 0 ] || echo 'Checking project...'
20
+ [ "$compact" -ne 0 ] || [ "$check" -ne 0 ] || echo 'Checking project...'
21
+ # The CLI's package descriptor is read in the same pass as the project's
22
+ # manifest and lock, once this command holds the project lock.
23
+ # shellcheck disable=SC2034 # read by cli-common.sh while it loads
24
+ CLI_DEFER_SYNC_PKG=1
12
25
  source "$CLI_DIR/lib/cli-common.sh"
13
26
  force=0
14
27
  target=""
@@ -16,22 +29,35 @@ while [ $# -gt 0 ]; do
16
29
  case "$1" in
17
30
  --compact) compact=1 ;;
18
31
  --force) force=1 ;;
32
+ --check) check=1 ;;
19
33
  -*) die "unknown option '$1'" ;;
20
34
  *)
21
- [ -z "$target" ] || die "usage: intelligence sync [adapter] [--compact] [--force]"
35
+ [ -z "$target" ] || die "usage: intelligence sync [adapter] [--compact] [--force] [--check]"
22
36
  target="$1"
23
37
  ;;
24
38
  esac
25
39
  shift
26
40
  done
27
41
 
42
+ # Set where a check concludes that sync is needed, so a failure that happens to
43
+ # end in the same status is never mistaken for that verdict.
44
+ IS_CHECK_OUT_OF_DATE=0
45
+
46
+ # hold_project — take the project lock, then read the package descriptor,
47
+ # manifest and lock once for the whole preflight.
48
+ hold_project() {
49
+ project_lock_hold "$IP_ROOT"
50
+ project_reads_preload "$IP_ROOT"
51
+ load_sync_package_identity
52
+ }
53
+
28
54
  run_sync() {
29
55
  detect_project || return $?
30
56
  case "$IP_MODE" in
31
57
  cli)
32
58
  # One writer per project at a time: the store restore and the
33
59
  # render below both rewrite shared state (sync-lock.sh).
34
- project_lock_hold "$IP_ROOT"
60
+ hold_project
35
61
  # `sync` is the normal fresh-clone command. A newer globally installed
36
62
  # CLI aligns the current Intelligence project first (except in CI, where a
37
63
  # tracked migration must be reviewed and committed locally), and a
@@ -42,6 +68,7 @@ run_sync() {
42
68
  sync_with_cache "$target" "$force" || return $?
43
69
  ;;
44
70
  legacy)
71
+ load_sync_package_identity
45
72
  [ "$force" -eq 0 ] || die "--force is unavailable for a legacy Intelligence Sync project - run 'intelligence init' to convert it first"
46
73
  [ "$compact" -eq 0 ] || die "--compact is unavailable for a legacy Intelligence Sync project - run 'intelligence init' to convert it first"
47
74
  # A vendored project syncs with its own engine: its pin is the
@@ -55,11 +82,56 @@ run_sync() {
55
82
  fi
56
83
  ;;
57
84
  *)
85
+ load_sync_package_identity
58
86
  die "no intelligence project found here - run 'intelligence init'"
59
87
  ;;
60
88
  esac
61
89
  }
62
90
 
91
+ # run_check — the same preflight as sync, except that tracked alignment is never
92
+ # applied (it is reported as needed, in CI or not) and everything but the
93
+ # verdict goes to stderr. Restoring the ignored package store from the lock is
94
+ # allowed: it is reproducible state, and the comparison needs it.
95
+ run_check() {
96
+ local stamp
97
+ detect_project || return $?
98
+ case "$IP_MODE" in
99
+ cli) ;;
100
+ legacy)
101
+ load_sync_package_identity
102
+ die "--check is unavailable for a legacy Intelligence Sync project - run 'intelligence init' to convert it first"
103
+ ;;
104
+ *)
105
+ load_sync_package_identity
106
+ die "no intelligence project found here - run 'intelligence init'"
107
+ ;;
108
+ esac
109
+ hold_project
110
+ project_preflight "$IP_ROOT" || return $?
111
+ if project_needs_upgrade "$IP_ROOT"; then
112
+ read_schema_version_var "$IP_ROOT/intelligence.yaml"
113
+ stamp="$IS_SCHEMA_VERSION"
114
+ bundled_engine_version_var
115
+ IS_CHECK_OUT_OF_DATE=1
116
+ is_status out-of-date "the project needs alignment (stamp ${stamp:-unstamped}, engine $IS_BUNDLED_ENGINE_VERSION); run 'intelligence init --apply' locally, review and commit the diff"
117
+ return "$IS_RC_OUT_OF_DATE"
118
+ fi
119
+ restore_project_store_if_missing "$IP_ROOT" keep-sources >&2 || return $?
120
+ export_engine_env "$IP_ROOT" || return $?
121
+ sync_check "$target" "$force" || return $?
122
+ }
123
+
124
+ if [ "$check" -eq 1 ]; then
125
+ # --compact changes nothing here: the verdict is already one status line,
126
+ # and a failure's diagnostics already reach stderr.
127
+ rc=0
128
+ run_check || rc=$?
129
+ if [ "$rc" = "$IS_RC_OUT_OF_DATE" ] && [ "$IS_CHECK_OUT_OF_DATE" != 1 ]; then
130
+ rc=1
131
+ fi
132
+ exit "$rc"
133
+ fi
134
+
63
135
  if [ "$compact" -eq 0 ]; then
64
136
  run_sync
65
137
  exit $?
package/cli/intelligence CHANGED
@@ -11,7 +11,11 @@ case "$0" in
11
11
  */*) CLI_DIR="${0%/*}"; [ -n "$CLI_DIR" ] || CLI_DIR="/" ;;
12
12
  *) CLI_DIR="." ;;
13
13
  esac
14
- CLI_DIR="$(cd "$CLI_DIR" && pwd)"
14
+ # An absolute spelling without `.` or `..` segments or doubled slashes is
15
+ # already what `cd && pwd` prints; on Git Bash that subshell costs a fork.
16
+ case "$CLI_DIR" in
17
+ */|*/./*|*/.|*/../*|*/..|*//*|[!/]*) CLI_DIR="$(cd "$CLI_DIR" && pwd)" ;;
18
+ esac
15
19
  export CLI_DIR
16
20
 
17
21
  # Engine discovery, by role (sync.sh + VERSION). The repo and the npm package
@@ -52,16 +56,17 @@ Usage: intelligence <command> [args]
52
56
  init [--preview|--apply] Initialize, convert, restore or align this project
53
57
  --targets a,b --dir name --bare --no-sync New-project options
54
58
  --force Accept a dirty worktree during legacy project conversion
55
- sync [adapter] [--compact] [--force] Restore locked content, then render enabled adapters
59
+ sync [adapter] [--compact] [--force] [--check] Restore locked content, then render enabled adapters
56
60
  --compact On success show context, warnings and final status; on failure show diagnostics
57
61
  --force Render again and refresh discovery of unconfigured source directories
62
+ --check Change no generated file; exit 0 when up to date, 2 when sync is needed
58
63
  update [@scope/name] Show an update plan; ask before applying it
59
64
  --preview Show the plan without asking or writing
60
65
  --apply Apply the plan without asking
61
66
  --latest Move one named package past its range to the newest version
62
67
  upgrade [--next] [--preview|--apply] Replace the installed CLI with the newest npm version
63
68
  --next Follow the prerelease line instead of the stable one
64
- package <command> add | remove | list | search
69
+ package <command> add | remove | alias | list | search
65
70
  source <command> add | remove | list — the sources: block (own directories)
66
71
  adapter <command> list | create | enable | disable | remove
67
72
  status [--check] Project state; --check runs deep consistency checks
@@ -109,4 +114,12 @@ if [ ! -f "$cmd_file" ]; then
109
114
  echo "ERROR: unknown command '$cmd'. Run 'intelligence help'." >&2
110
115
  exit 1
111
116
  fi
117
+ # `sync` runs on every save hook and CI check, so it runs in this process: a
118
+ # second bash start is a large share of an unchanged sync on Git Bash. It
119
+ # exits on every path and reads nothing of this script but the exports.
120
+ if [ "$cmd" = sync ]; then
121
+ # shellcheck source=commands/sync.sh
122
+ source "$cmd_file"
123
+ exit $?
124
+ fi
112
125
  exec bash "$cmd_file" "$@"
@@ -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
@@ -435,7 +435,7 @@ while IFS="$LOCK_SEP" read -r pack name url ref mirror; do
435
435
  # Containment before deletion: a mirror value is config input, and rm -rf
436
436
  # must never follow it out of the repository.
437
437
  case "$mirror" in
438
- ""|/*|*..*) [ -n "$mirror" ] && echo " WARN: mirror '$mirror' not repo-contained — left in place" >&2; continue ;;
438
+ ""|/*|*..*) [ -n "$mirror" ] && echo "WARNING: mirror '$mirror' not repo-contained — left in place" >&2; continue ;;
439
439
  esac
440
440
  if [ -d "$root/$mirror" ]; then
441
441
  rm -rf "${root:?}/$mirror"
@@ -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>"