@ainova-systems/intelligence 0.11.0-rc.1 → 0.11.0-rc.11

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 (72) hide show
  1. package/README.md +19 -16
  2. package/cli/commands/adapter.sh +153 -0
  3. package/cli/commands/init.sh +171 -34
  4. package/cli/commands/package.sh +30 -0
  5. package/cli/commands/registry.sh +65 -30
  6. package/cli/commands/status.sh +20 -11
  7. package/cli/commands/sync.sh +9 -4
  8. package/cli/commands/update.sh +80 -46
  9. package/cli/engine-package.yaml +14 -0
  10. package/cli/intelligence +37 -26
  11. package/cli/internal/align-project.sh +134 -0
  12. package/cli/internal/check.sh +128 -0
  13. package/cli/internal/convert-legacy.sh +400 -0
  14. package/cli/{commands/add.sh → internal/package-add.sh} +37 -19
  15. package/cli/{commands/list.sh → internal/package-list.sh} +10 -5
  16. package/cli/{commands/remove.sh → internal/package-remove.sh} +16 -4
  17. package/cli/internal/package-search.sh +83 -0
  18. package/cli/internal/package-update.sh +103 -0
  19. package/cli/internal/restore.sh +130 -0
  20. package/cli/internal/target-state.sh +57 -0
  21. package/cli/lib/cli-common.sh +213 -63
  22. package/cli/lib/lockfile.sh +15 -9
  23. package/cli/lib/manifest.sh +257 -4
  24. package/cli/lib/registry.sh +96 -47
  25. package/cli/lib/semver.sh +12 -4
  26. package/engine/ENGINE_SHA +1 -0
  27. package/engine/{scripts/adapters → adapters}/_template.sh +10 -9
  28. package/engine/{scripts/adapters → adapters}/agents.sh +20 -25
  29. package/engine/{scripts/adapters → adapters}/opencode.sh +1 -1
  30. package/engine/{scripts/lib → lib}/common.sh +30 -538
  31. package/engine/lib/contract.sh +120 -0
  32. package/engine/sync.sh +238 -0
  33. package/package.json +5 -5
  34. package/{engine → packages/sync}/agents/intelligence-architect.md +5 -3
  35. package/{engine → packages/sync}/agents/intelligence-operator.md +10 -13
  36. package/packages/sync/references/adapters.md +252 -0
  37. package/packages/sync/references/conventions.md +385 -0
  38. package/{engine → packages/sync}/rules/intelligence-authoring.md +7 -7
  39. package/{engine → packages/sync}/skills/intelligence-add-agent/SKILL.md +7 -7
  40. package/{engine → packages/sync}/skills/intelligence-add-rule/SKILL.md +5 -5
  41. package/{engine → packages/sync}/skills/intelligence-add-skill/SKILL.md +5 -5
  42. package/{engine → packages/sync}/skills/intelligence-extract-skill/SKILL.md +2 -2
  43. package/packages/sync/skills/intelligence-install-adapter/SKILL.md +38 -0
  44. package/{engine → packages/sync}/skills/intelligence-learn-from-context/SKILL.md +3 -3
  45. package/packages/sync/skills/intelligence-learn-from-repository/SKILL.md +55 -0
  46. package/{engine → packages/sync}/skills/intelligence-review-skills/SKILL.md +7 -7
  47. package/packages/sync/skills/intelligence-sync/SKILL.md +20 -0
  48. package/packages/sync/skills/intelligence-uninstall-adapter/SKILL.md +24 -0
  49. package/packages/sync/skills/intelligence-update/SKILL.md +43 -0
  50. package/cli/commands/doctor.sh +0 -100
  51. package/cli/commands/install.sh +0 -84
  52. package/cli/commands/migrate.sh +0 -251
  53. package/cli/commands/upgrade.sh +0 -27
  54. package/engine/INIT.md +0 -498
  55. package/engine/docs/ADAPTERS.md +0 -212
  56. package/engine/docs/CLI.md +0 -91
  57. package/engine/docs/CONVENTIONS.md +0 -440
  58. package/engine/scripts/lib/layout.sh +0 -51
  59. package/engine/scripts/lib/migrations.sh +0 -708
  60. package/engine/scripts/sync.sh +0 -311
  61. package/engine/scripts/update.sh +0 -237
  62. package/engine/skills/intelligence-install-adapter/SKILL.md +0 -31
  63. package/engine/skills/intelligence-sync/SKILL.md +0 -18
  64. package/engine/skills/intelligence-uninstall-adapter/SKILL.md +0 -42
  65. package/engine/skills/intelligence-update/SKILL.md +0 -159
  66. package/registry/index.yaml +0 -15
  67. /package/engine/{scripts/VERSION → VERSION} +0 -0
  68. /package/engine/{scripts/adapters → adapters}/claude.sh +0 -0
  69. /package/engine/{scripts/adapters → adapters}/codex.sh +0 -0
  70. /package/engine/{scripts/adapters → adapters}/copilot.sh +0 -0
  71. /package/engine/{scripts/adapters → adapters}/cursor.sh +0 -0
  72. /package/engine/{scripts/adapters → adapters}/pi.sh +0 -0
@@ -0,0 +1,103 @@
1
+ #!/bin/bash
2
+ # Internal package range update operation.
3
+ #
4
+ # Resolve requested versions/refs against each package's locked source and
5
+ # rewrite the lock. `update <name>` limits the pass to one package; re-adding
6
+ # is the explicit operation for changing a source URL/path.
7
+ set -euo pipefail
8
+ source "$CLI_DIR/lib/cli-common.sh"
9
+
10
+ only="" no_sync=0 preview=0
11
+ while [ $# -gt 0 ]; do
12
+ case "$1" in
13
+ --no-sync) no_sync=1 ;;
14
+ --preview) preview=1 ;;
15
+ @*) only="$1" ;;
16
+ *) die "unknown argument '$1'" ;;
17
+ esac
18
+ shift || true
19
+ done
20
+
21
+ require_cli_project
22
+ manifest="$IP_ROOT/intelligence.yaml"
23
+ lock="$IP_ROOT/intelligence.lock"
24
+
25
+ moved=0 found=0
26
+ while IFS= read -r name; do
27
+ [ -n "$name" ] || continue
28
+ # Manifest keys are untrusted input on their way into store paths.
29
+ assert_valid_pkg_name "$name"
30
+ [ -n "$only" ] && [ "$name" != "$only" ] && continue
31
+ # A named package that exists is "found" whatever happens next — a skip
32
+ # message followed by "not in the manifest" would contradict itself.
33
+ found=1
34
+ if [ "$name" = "$SYNC_PKG_NAME" ]; then
35
+ echo " $name: engine content follows the installed CLI — skipped"
36
+ continue
37
+ fi
38
+ current="$(qmap_field "$lock" "packages" "$name" "resolved")"
39
+ locked_requested="$(qmap_field "$lock" "packages" "$name" "requested")"
40
+ url="$(qmap_field "$lock" "packages" "$name" "url")"
41
+ path="$(qmap_field "$lock" "packages" "$name" "path")"
42
+ [ -n "$url" ] || die "$name has no source in intelligence.lock — restore the committed lock or re-add the package"
43
+ ref="$(qmap_field "$manifest" "packages" "$name" "ref")"
44
+ range="$(qmap_field "$manifest" "packages" "$name" "version")"
45
+ [ -n "$ref" ] || [ -n "$range" ] || die "$name has neither version nor ref in the manifest"
46
+
47
+ if [ -n "$ref" ]; then
48
+ tag="$ref"
49
+ requested=""
50
+ else
51
+ picked="$(list_remote_versions "$url" | semver_pick_highest "$range")"
52
+ [ -n "$picked" ] || { echo " $name: nothing satisfies '$range' at $url" >&2; continue; }
53
+ read -r tag _ <<< "$(remote_tag_for_version "$url" "$picked")"
54
+ requested="$range"
55
+ fi
56
+ if [ "$tag" = "$current" ] && [ "$requested" != "$locked_requested" ]; then
57
+ if [ "$preview" -eq 1 ]; then
58
+ echo " $name: request ${locked_requested:-<none>} -> ${requested:-<ref>} (keeps $current)"
59
+ else
60
+ locked_sha="$(qmap_field "$lock" "packages" "$name" "sha")"
61
+ lock_upsert "$lock" "$name" "$requested" "$url" "$path" "$current" "$locked_sha"
62
+ echo " $name: request ${locked_requested:-<none>} -> ${requested:-<ref>} (kept $current)"
63
+ fi
64
+ moved=$((moved + 1))
65
+ continue
66
+ fi
67
+ if [ "$tag" = "$current" ]; then
68
+ echo " $name: ${current:-<none>} (up to date)"
69
+ continue
70
+ fi
71
+ if [ "$preview" -eq 1 ]; then
72
+ echo " $name: ${current:-<none>} -> $tag"
73
+ moved=$((moved + 1))
74
+ continue
75
+ fi
76
+ rel=".intelligence/packages/$name"
77
+ # Fetch into staging FIRST: a failed clone must leave the current install
78
+ # fully wired and intact. Only after success is the old shape unwired
79
+ # (the new version may have dropped a section dir), the store swapped,
80
+ # and the new shape wired.
81
+ staging="$IP_ROOT/.intelligence/.staging-$$"
82
+ rm -rf "$staging"
83
+ sha="$(fetch_package "$url" "$tag" "$path" "$staging")"
84
+ unwire_package_sources "$manifest" "$rel"
85
+ rm -rf "${IP_ROOT:?}/$rel"
86
+ mkdir -p "$(dirname "$IP_ROOT/$rel")"
87
+ mv "$staging" "$IP_ROOT/$rel"
88
+ wire_package_sources "$manifest" "$name" "$rel" "$IP_ROOT"
89
+ lock_upsert "$lock" "$name" "$requested" "$url" "$path" "$tag" "$sha"
90
+ echo " $name: ${current:-<none>} -> $tag"
91
+ moved=$((moved + 1))
92
+ done < <(qmap_keys "$manifest" "packages")
93
+
94
+ [ -n "$only" ] && [ "$found" -eq 0 ] && die "package '$only' is not in the manifest"
95
+ if [ "$preview" -eq 1 ]; then
96
+ echo "updates available: $moved package(s)"
97
+ else
98
+ echo "updated: $moved package(s)"
99
+ fi
100
+
101
+ if [ "$preview" -eq 0 ] && [ "$moved" -gt 0 ] && [ "$no_sync" -eq 0 ]; then
102
+ exec bash "$CLI_DIR/commands/sync.sh"
103
+ fi
@@ -0,0 +1,130 @@
1
+ #!/bin/bash
2
+ # Internal locked-store restore operation.
3
+ #
4
+ # Restore .intelligence/ from intelligence.lock — the fresh-clone command.
5
+ # --frozen additionally refuses any manifest/lock divergence (CI mode) and
6
+ # fails on a sha mismatch instead of re-resolving.
7
+ set -euo pipefail
8
+ source "$CLI_DIR/lib/cli-common.sh"
9
+
10
+ frozen=0 force=0 no_sync=0
11
+ while [ $# -gt 0 ]; do
12
+ case "$1" in
13
+ --frozen) frozen=1 ;;
14
+ --force) force=1 ;;
15
+ --no-sync) no_sync=1 ;;
16
+ *) die "unknown option '$1'" ;;
17
+ esac
18
+ shift || true
19
+ done
20
+
21
+ require_cli_project
22
+ manifest="$IP_ROOT/intelligence.yaml"
23
+ lock="$IP_ROOT/intelligence.lock"
24
+
25
+ # --frozen is the reproducibility contract: the lock must agree with the
26
+ # manifest on the package SET and on every requested-intent field before a
27
+ # single byte is restored. Anything less lets a manifest edit ride on a stale
28
+ # lock — the exact drift the flag exists to refuse.
29
+ if [ "$frozen" -eq 1 ]; then
30
+ while IFS= read -r name; do
31
+ [ -n "$name" ] || continue
32
+ [ -n "$(qmap_field "$lock" "packages" "$name" "url")" ] \
33
+ || die "locked restore: $name is in the manifest but not in intelligence.lock"
34
+ m_ver="$(qmap_field "$manifest" "packages" "$name" "version")"
35
+ m_ref="$(qmap_field "$manifest" "packages" "$name" "ref")"
36
+ l_req="$(qmap_field "$lock" "packages" "$name" "requested")"
37
+ l_res="$(qmap_field "$lock" "packages" "$name" "resolved")"
38
+ [ -z "$m_ver" ] || [ "$m_ver" = "$l_req" ] \
39
+ || die "locked restore: $name requests '$m_ver' but the lock recorded '$l_req' — reconcile the manifest and lock"
40
+ [ -z "$m_ref" ] || [ "$m_ref" = "$l_res" ] \
41
+ || die "--frozen: $name pins ref '$m_ref' but the lock resolved '$l_res'"
42
+ done < <(qmap_keys "$manifest" "packages")
43
+ while IFS= read -r name; do
44
+ [ -n "$name" ] || continue
45
+ found=0
46
+ while IFS= read -r m; do
47
+ [ "$m" = "$name" ] && found=1
48
+ done < <(qmap_keys "$manifest" "packages")
49
+ [ "$found" -eq 1 ] || die "--frozen: $name is locked but absent from the manifest"
50
+ done < <(qmap_keys "$lock" "packages")
51
+ fi
52
+
53
+ # Manifest packages missing from the lock: resolve them now (npm install
54
+ # semantics). Under --frozen the drift gate above has already refused.
55
+ missing=""
56
+ while IFS= read -r name; do
57
+ [ -n "$name" ] || continue
58
+ assert_valid_pkg_name "$name"
59
+ if [ ! -f "$lock" ] || [ -z "$(qmap_field "$lock" "packages" "$name" "url")" ]; then
60
+ missing="$missing $name"
61
+ fi
62
+ done < <(qmap_keys "$manifest" "packages")
63
+ if [ -n "$missing" ]; then
64
+ for name in $missing; do
65
+ # The engine-content package needs no remote resolution: its pin IS
66
+ # the bundled engine version and it seeds from the bundle, offline.
67
+ if [ "$name" = "$SYNC_PKG_NAME" ]; then
68
+ sync_pkg_install "$IP_ROOT"
69
+ continue
70
+ fi
71
+ ref="$(qmap_field "$manifest" "packages" "$name" "ref")"
72
+ range="$(qmap_field "$manifest" "packages" "$name" "version")"
73
+ resolve_package_source "$manifest" "$name"
74
+ url="$RES_URL"; path="$RES_PATH"
75
+ [ -n "$url" ] || die "no trusted registry declares '$name' and it is absent from intelligence.lock"
76
+ resolved_tag=""
77
+ if [ -z "$ref" ]; then
78
+ picked="$(list_remote_versions "$url" | semver_pick_highest "$range")"
79
+ [ -n "$picked" ] || die "no tag of $url satisfies '${range:-*}' for $name"
80
+ read -r resolved_tag _ <<< "$(remote_tag_for_version "$url" "$picked")"
81
+ fi
82
+ rel=".intelligence/packages/$name"
83
+ sha="$(fetch_package "$url" "${ref:-$resolved_tag}" "$path" "$IP_ROOT/$rel")"
84
+ wire_package_sources "$manifest" "$name" "$rel" "$IP_ROOT"
85
+ lock_upsert "$lock" "$name" "$range" "$url" "$path" "${ref:-$resolved_tag}" "$sha"
86
+ echo "+ $name@${resolved_tag:-${ref:-HEAD}} (resolved)"
87
+ done
88
+ fi
89
+
90
+ # Restore every locked package that is not already in the store.
91
+ if [ -f "$lock" ]; then
92
+ while IFS="$LOCK_SEP" read -r name requested url path resolved sha; do
93
+ [ -n "$name" ] || continue
94
+ # The lock arrives with a cloned repo — its keys are untrusted input
95
+ # on their way into rm -rf paths, and its urls into git argv.
96
+ assert_valid_pkg_name "$name"
97
+ rel=".intelligence/packages/$name"
98
+ # --frozen never trusts what is already on disk: a pre-existing
99
+ # (possibly substituted) store dir would otherwise be synced with no
100
+ # sha verification at all.
101
+ if [ -d "$IP_ROOT/$rel" ] && [ "$force" -eq 0 ] && [ "$frozen" -eq 0 ]; then
102
+ continue
103
+ fi
104
+ # Fetch into staging and commit to the store only after the integrity
105
+ # verdict: content that violates the lock must never be left where a
106
+ # re-run (whose dir-exists check skips verification) would sync it.
107
+ staging="$IP_ROOT/.intelligence/.staging-$$"
108
+ rm -rf "$staging"
109
+ got="$(fetch_package "$url" "$resolved" "$path" "$staging")"
110
+ # An empty sha (bundle-seeded content on a dev build) means unknown,
111
+ # not moved — only two known-and-different shas are a finding.
112
+ if [ -n "$sha" ] && [ -n "$got" ] && [ "$got" != "$sha" ]; then
113
+ if [ "$frozen" -eq 1 ]; then
114
+ rm -rf "$staging"
115
+ die "$name: fetched $got but the lock pins $sha — the ref moved; refusing under --frozen"
116
+ fi
117
+ echo " WARN: $name resolved to $got, lock had $sha — updating the lock" >&2
118
+ lock_upsert "$lock" "$name" "$requested" "$url" "$path" "$resolved" "$got"
119
+ fi
120
+ rm -rf "${IP_ROOT:?}/$rel"
121
+ mkdir -p "$(dirname "$IP_ROOT/$rel")"
122
+ mv "$staging" "$IP_ROOT/$rel"
123
+ wire_package_sources "$manifest" "$name" "$rel" "$IP_ROOT"
124
+ echo "= $name@${resolved:-HEAD}"
125
+ done < <(lock_to_tsv "$lock")
126
+ fi
127
+
128
+ if [ "$no_sync" -eq 0 ]; then
129
+ exec bash "$CLI_DIR/commands/sync.sh"
130
+ fi
@@ -0,0 +1,57 @@
1
+ #!/bin/bash
2
+ # Internal adapter target-state operation.
3
+ #
4
+ # Change only the manifest. Syncing and deleting generated output are separate
5
+ # explicit actions: a generic target command cannot infer a custom adapter's
6
+ # ownership safely.
7
+ set -euo pipefail
8
+ source "$CLI_DIR/lib/cli-common.sh"
9
+
10
+ action="${1:-}"
11
+ name="${2:-}"
12
+ [ $# -le 2 ] || die "internal target state: expected <enable|disable> <name>"
13
+ case "$action" in enable|disable) ;; *) die "internal target state: expected <enable|disable> <name>" ;; esac
14
+ [ -n "$name" ] || die "internal target state: missing adapter name"
15
+ assert_valid_target_name "$name"
16
+
17
+ require_cli_project
18
+ manifest="$IP_ROOT/intelligence.yaml"
19
+ content_dir="$(manifest_intelligence_dir "$manifest")"
20
+
21
+ if [ "$action" = "enable" ]; then
22
+ project_adapter="$IP_ROOT/$content_dir/adapters/$name.sh"
23
+ bundled_adapter="$IS_ENGINE_DIR/adapters/$name.sh"
24
+ if [ ! -f "$project_adapter" ] && [ ! -f "$bundled_adapter" ]; then
25
+ die "adapter '$name' not found — create it first: intelligence adapter create $name"
26
+ fi
27
+
28
+ # These adapters intentionally omit always-on rules because their tools
29
+ # read AGENTS.md. Do not create a manifest that full sync must refuse.
30
+ case "$name" in
31
+ cursor|copilot|codex|pi|opencode)
32
+ if [ "$(is_target_enabled "$manifest" "agents")" != "1" ]; then
33
+ die "target '$name' requires target 'agents' — enable it first"
34
+ fi
35
+ ;;
36
+ esac
37
+
38
+ output="$(get_target_output "$manifest" "$name")"
39
+ [ -n "$output" ] || output="$(default_target_output "$name")"
40
+ target_set_enabled "$manifest" "$name" true "$output"
41
+ echo "enabled: $name (output: $output)"
42
+ exit 0
43
+ fi
44
+
45
+ target_exists "$manifest" "$name" || die "target '$name' is not in the manifest"
46
+ if [ "$name" = "agents" ]; then
47
+ for dependent in cursor copilot codex pi opencode; do
48
+ if [ "$(is_target_enabled "$manifest" "$dependent")" = "1" ]; then
49
+ die "target 'agents' is required by enabled target '$dependent' — disable '$dependent' first"
50
+ fi
51
+ done
52
+ fi
53
+ output="$(get_target_output "$manifest" "$name")"
54
+ [ -n "$output" ] || output="$(default_target_output "$name")"
55
+ target_set_enabled "$manifest" "$name" false "$output"
56
+ echo "disabled: $name"
57
+ echo " generated output was kept; remove only paths owned by this adapter"
@@ -7,34 +7,74 @@
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
- # 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"
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 acquisition reaches git. The pin is held
58
+ # exactly at the bundled engine version and moved by lifecycle alignment.
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"
31
71
 
32
72
  # --- Project detection ---------------------------------------------------
33
- # Sets: IP_MODE (v2|legacy|none), IP_ROOT, IP_UMBRELLA, IP_MODULE_DIR.
73
+ # Sets: IP_MODE (cli|legacy|none), IP_ROOT, IP_UMBRELLA, IP_MODULE_DIR.
34
74
  # These ARE this lib's public API — every command reads them after calling
35
75
  # detect_project, which per-file shellcheck cannot see.
36
76
  # shellcheck disable=SC2034
37
- # v2 wins: the nearest ancestor holding intelligence.yaml. Legacy is a git
77
+ # The CLI product wins: the nearest ancestor holding intelligence.yaml. Legacy is a git
38
78
  # repo holding an umbrella (a dir with config.yaml) whose module is identified
39
79
  # by role — scripts/sync.sh + scripts/VERSION — never by name.
40
80
  detect_project() {
@@ -42,7 +82,7 @@ detect_project() {
42
82
  local dir="$PWD"
43
83
  while :; do
44
84
  if [ -f "$dir/intelligence.yaml" ]; then
45
- IP_MODE="v2"
85
+ IP_MODE="cli"
46
86
  IP_ROOT="$(cd "$dir" && pwd)"
47
87
  return 0
48
88
  fi
@@ -69,11 +109,11 @@ detect_project() {
69
109
  return 0
70
110
  }
71
111
 
72
- require_v2() {
112
+ require_cli_project() {
73
113
  detect_project
74
114
  case "$IP_MODE" in
75
- v2) ;;
76
- legacy) die "this is a vendored (v1) setup — run 'intelligence migrate' first, or keep using its own flow" ;;
115
+ cli) ;;
116
+ legacy) die "this is a legacy Intelligence Sync project — run 'intelligence init' to convert it, or keep using its own flow" ;;
77
117
  *) die "no intelligence project found here — run 'intelligence init'" ;;
78
118
  esac
79
119
  }
@@ -86,64 +126,174 @@ manifest_intelligence_dir() {
86
126
  printf '%s' "${v:-intelligence}"
87
127
  }
88
128
 
129
+ default_target_output() {
130
+ case "$1" in
131
+ agents) printf '%s' "AGENTS.md" ;;
132
+ copilot) printf '%s' ".github" ;;
133
+ *) printf '.%s' "$1" ;;
134
+ esac
135
+ }
136
+
89
137
  bundled_engine_version() {
90
- tr -d ' \t\r\n' < "$IS_ENGINE_DIR/scripts/VERSION"
91
- }
92
-
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
- for d in rules agents docs; do
106
- [ -d "$IS_ENGINE_DIR/$d" ] && cp -R "$IS_ENGINE_DIR/$d" "$store/$d"
107
- done
108
- if [ -d "$IS_ENGINE_DIR/skills" ]; then
109
- mkdir -p "$store/skills"
110
- for s in "$IS_ENGINE_DIR"/skills/*/; do
111
- [ -d "$s" ] || continue
112
- name="$(basename "$s")"
113
- skip=0
114
- for d in $CLI_OBSOLETE_SKILLS; do
115
- [ "$name" = "$d" ] && skip=1
116
- done
117
- [ "$skip" = "1" ] && continue
118
- cp -R "$s" "$store/skills/$name"
119
- done
120
- fi
121
- bundled_engine_version > "$store/.version"
138
+ tr -d ' \t\r\n' < "$IS_ENGINE_DIR/VERSION"
122
139
  }
123
140
 
124
- ensure_engine_staged() {
125
- local root="$1"
126
- local store="$root/.intelligence/engine"
127
- local bundled_ver staged_ver
128
- bundled_ver="$(bundled_engine_version)"
129
- staged_ver=""
130
- [ -f "$store/.version" ] && staged_ver="$(tr -d ' \t\r\n' < "$store/.version")"
131
- [ "$staged_ver" = "$bundled_ver" ] && return 0
132
- stage_engine_content "$store"
133
- echo " engine content staged: .intelligence/engine ($bundled_ver)"
141
+ # --- The sync package's manifest/lock plumbing ---------------------------
142
+ # sync_pkg_entry <manifest> — write/refresh only the requested exact pin.
143
+ # The built-in source lives in engine-package.yaml and resolved source state
144
+ # belongs exclusively to intelligence.lock.
145
+ sync_pkg_entry() {
146
+ local manifest="$1"
147
+ qmap_set "$manifest" "packages" "$SYNC_PKG_NAME" "version" "$(bundled_engine_version)"
148
+ }
149
+
150
+ # sync_pkg_install <root> — materialize the package into the store and lock
151
+ # it (offline at the bundled version — fetch_package's bundle-seed guard).
152
+ sync_pkg_install() {
153
+ local root="$1" ver sha
154
+ ver="$(bundled_engine_version)"
155
+ sha="$(fetch_package "$SYNC_PKG_URL" "v$ver" "$SYNC_PKG_PATH" "$root/$SYNC_PKG_STORE")"
156
+ wire_package_sources "$root/intelligence.yaml" "$SYNC_PKG_NAME" "$SYNC_PKG_STORE" "$root"
157
+ lock_upsert "$root/intelligence.lock" "$SYNC_PKG_NAME" "$ver" "$SYNC_PKG_URL" "$SYNC_PKG_PATH" "v$ver" "$sha"
158
+ echo " engine content installed: $SYNC_PKG_STORE (v$ver)"
134
159
  }
135
160
 
136
161
  # --- The engine env contract ---------------------------------------------
137
162
  # Everything the IS_CLI mode of sync.sh needs, derived from the manifest.
138
163
  export_engine_env() {
139
164
  local root="$1"
140
- local umbrella
141
- umbrella="$(manifest_intelligence_dir "$root/intelligence.yaml")"
165
+ local content_rel
166
+ content_rel="$(manifest_intelligence_dir "$root/intelligence.yaml")"
142
167
  export IS_CLI=1
143
168
  export CONFIG_FILE="$root/intelligence.yaml"
144
169
  export REPO_ROOT="$root"
145
- export IS_UMBRELLA_REL="$umbrella"
146
- export IS_MODULE_REL=".intelligence/engine"
170
+ export IS_CONTENT_REL="$content_rel"
171
+ export IS_MODULE_REL="$SYNC_PKG_STORE"
147
172
  export IS_SYNC_CMD="intelligence sync"
148
- export IS_PROTECTED_DIRS="$umbrella:.intelligence"
173
+ export IS_MANIFEST_NAME="intelligence.yaml"
174
+ export IS_PROTECTED_DIRS="$content_rel:.intelligence"
175
+ }
176
+
177
+ # --- Project lifecycle preflight -----------------------------------------
178
+ # Public commands are intentionally few. They share this state gate so a CLI
179
+ # installed at a newer engine version brings the current Intelligence project forward
180
+ # before a mutating operation. The npm install itself cannot do that: it runs
181
+ # outside any project and does not know which repositories the user owns.
182
+
183
+ is_ci_environment() {
184
+ case "${CI:-}" in
185
+ 1|true|TRUE|True|yes|YES|Yes|on|ON|On) return 0 ;;
186
+ *) return 1 ;;
187
+ esac
188
+ }
189
+
190
+ # Validate a repository-relative project content directory, including every
191
+ # existing symlinked path component. Call before writing a new manifest.
192
+ assert_safe_content_dir() {
193
+ local root="$1" content_dir="$2" repo_phys probe old_ifs part probe_phys
194
+ local -a parts
195
+ case "$content_dir" in
196
+ ""|/*|.|..|../*|*/../*|*/..|.git|.git/*|.intelligence|.intelligence/*|*\\*|[A-Za-z]:*)
197
+ die "unsafe content directory '$content_dir'"
198
+ ;;
199
+ esac
200
+ repo_phys="$(cd "$root" && pwd -P)"
201
+ probe="$root"
202
+ old_ifs="$IFS"; IFS='/'; read -r -a parts <<< "$content_dir"; IFS="$old_ifs"
203
+ for part in "${parts[@]}"; do
204
+ [ -n "$part" ] || continue
205
+ probe="$probe/$part"
206
+ if [ -e "$probe" ] || [ -L "$probe" ]; then
207
+ probe_phys="$(cd "$probe" 2>/dev/null && pwd -P)" \
208
+ || die "cannot resolve project content path '$content_dir'"
209
+ case "$probe_phys" in
210
+ "$repo_phys"|"$repo_phys"/*) ;;
211
+ *) die "project content directory resolves outside the repository: '$content_dir'" ;;
212
+ esac
213
+ fi
214
+ done
215
+ }
216
+
217
+ project_needs_upgrade() {
218
+ local root="$1" manifest="$1/intelligence.yaml" stamp eng pinned locked name
219
+ [ -f "$manifest" ] || return 1
220
+ stamp="$(read_schema_version "$manifest")"
221
+ eng="$(bundled_engine_version)"
222
+ [ -z "$stamp" ] && return 0
223
+ [ -n "$(top_scalar "$manifest" "sync_version")" ] && return 0
224
+ _ver_gt "$eng" "$stamp" && return 0
225
+ [ -d "$root/.intelligence/engine" ] && return 0
226
+
227
+ while IFS= read -r name; do
228
+ [ -n "$name" ] || continue
229
+ # Early RC manifests mixed requested intent with resolved source
230
+ # details. Source URL/path now live only in the required lockfile.
231
+ [ -n "$(qmap_field "$manifest" "packages" "$name" "url")" ] && return 0
232
+ [ -n "$(qmap_field "$manifest" "packages" "$name" "path")" ] && return 0
233
+ if [ "$name" = "$SYNC_PKG_NAME" ]; then
234
+ pinned="$(qmap_field "$manifest" "packages" "$name" "version")"
235
+ locked="$(qmap_field "$root/intelligence.lock" "packages" "$name" "resolved")"
236
+ [ "$pinned" = "$eng" ] || return 0
237
+ if [ -f "$root/intelligence.lock" ]; then
238
+ [ -n "$locked" ] || return 0
239
+ [ "${locked#v}" = "$eng" ] || return 0
240
+ fi
241
+ fi
242
+ done < <(qmap_keys "$manifest" "packages")
243
+ return 1
244
+ }
245
+
246
+ # A manifest with packages but no lock has no trustworthy resolved state from
247
+ # which lifecycle alignment can proceed. Never manufacture a partial lock.
248
+ project_has_packages() {
249
+ local root="$1" name found=1
250
+ while IFS= read -r name; do
251
+ [ -n "$name" ] || continue
252
+ assert_valid_pkg_name "$name"
253
+ found=0
254
+ done < <(qmap_keys "$root/intelligence.yaml" "packages")
255
+ return "$found"
256
+ }
257
+
258
+ ensure_project_current() {
259
+ local root="$1" explicit="${2:-}" manifest="$1/intelligence.yaml" stamp eng
260
+ check_version_compat "$manifest" || return $?
261
+ if project_has_packages "$root" && [ ! -f "$root/intelligence.lock" ]; then
262
+ die "manifest declares packages but intelligence.lock is absent — restore the committed lock before running project lifecycle commands"
263
+ fi
264
+ project_needs_upgrade "$root" || return 0
265
+ stamp="$(read_schema_version "$manifest")"
266
+ eng="$(bundled_engine_version)"
267
+ if is_ci_environment && [ "$explicit" != "--explicit" ]; then
268
+ die "project lifecycle requires alignment (stamp ${stamp:-unstamped}, engine $eng) — run 'intelligence init --apply' locally, review and commit the diff"
269
+ fi
270
+ echo " project alignment: stamp ${stamp:-unstamped}, engine $eng"
271
+ bash "$CLI_DIR/internal/align-project.sh" --no-sync
272
+ }
273
+
274
+ project_store_missing() {
275
+ local root="$1" manifest="$1/intelligence.yaml" name src
276
+ while IFS= read -r name; do
277
+ [ -n "$name" ] || continue
278
+ assert_valid_pkg_name "$name"
279
+ [ -d "$root/.intelligence/packages/$name" ] || return 0
280
+ done < <(qmap_keys "$manifest" "packages")
281
+ for section in rules agents skills; do
282
+ while IFS= read -r src; do
283
+ case "$src" in
284
+ .intelligence/packages/*)
285
+ [ -d "$root/$src" ] || return 0
286
+ ;;
287
+ esac
288
+ done < <(read_yaml_list "$manifest" "$section")
289
+ done
290
+ return 1
291
+ }
292
+
293
+ restore_project_store_if_missing() {
294
+ local root="$1"
295
+ project_store_missing "$root" || return 0
296
+ [ -f "$root/intelligence.lock" ] || die "package store is missing and intelligence.lock is absent — run 'intelligence init'"
297
+ echo " restoring package store from intelligence.lock"
298
+ bash "$CLI_DIR/internal/restore.sh" --frozen --no-sync
149
299
  }