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

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 (50) hide show
  1. package/README.md +15 -16
  2. package/cli/commands/adapter.sh +153 -0
  3. package/cli/commands/init.sh +113 -22
  4. package/cli/commands/package.sh +30 -0
  5. package/cli/commands/registry.sh +4 -2
  6. package/cli/commands/status.sh +12 -4
  7. package/cli/commands/sync.sh +7 -20
  8. package/cli/commands/update.sh +76 -70
  9. package/cli/engine-package.yaml +2 -2
  10. package/cli/intelligence +10 -12
  11. package/cli/{commands/doctor.sh → internal/check.sh} +32 -14
  12. package/cli/{commands/migrate.sh → internal/migrate-v1.sh} +114 -25
  13. package/cli/{commands/add.sh → internal/package-add.sh} +3 -3
  14. package/cli/{commands/list.sh → internal/package-list.sh} +9 -4
  15. package/cli/{commands/remove.sh → internal/package-remove.sh} +3 -3
  16. package/cli/{commands/search.sh → internal/package-search.sh} +4 -4
  17. package/cli/internal/package-update.sh +97 -0
  18. package/cli/{commands/install.sh → internal/restore.sh} +3 -3
  19. package/cli/internal/target-state.sh +57 -0
  20. package/cli/{commands/upgrade.sh → internal/upgrade-v2.sh} +48 -8
  21. package/cli/lib/cli-common.sh +133 -7
  22. package/cli/lib/lockfile.sh +1 -1
  23. package/cli/lib/manifest.sh +86 -0
  24. package/cli/lib/registry.sh +4 -4
  25. package/engine/ENGINE_SHA +1 -1
  26. package/engine/adapters/_template.sh +10 -9
  27. package/engine/adapters/agents.sh +11 -11
  28. package/engine/adapters/opencode.sh +1 -1
  29. package/engine/lib/common.sh +14 -22
  30. package/engine/lib/contract.sh +14 -14
  31. package/engine/sync.sh +25 -20
  32. package/package.json +1 -1
  33. package/packages/sync/agents/intelligence-architect.md +5 -3
  34. package/packages/sync/agents/intelligence-operator.md +10 -13
  35. package/packages/sync/references/adapters.md +252 -0
  36. package/packages/sync/references/conventions.md +385 -0
  37. package/packages/sync/rules/intelligence-authoring.md +6 -6
  38. package/packages/sync/skills/intelligence-add-agent/SKILL.md +5 -5
  39. package/packages/sync/skills/intelligence-add-rule/SKILL.md +3 -3
  40. package/packages/sync/skills/intelligence-add-skill/SKILL.md +3 -3
  41. package/packages/sync/skills/intelligence-extract-skill/SKILL.md +2 -2
  42. package/packages/sync/skills/intelligence-install-adapter/SKILL.md +29 -22
  43. package/packages/sync/skills/intelligence-learn-from-context/SKILL.md +3 -3
  44. package/packages/sync/skills/intelligence-learn-from-repository/SKILL.md +55 -0
  45. package/packages/sync/skills/intelligence-review-skills/SKILL.md +6 -6
  46. package/packages/sync/skills/intelligence-sync/SKILL.md +13 -9
  47. package/packages/sync/skills/intelligence-uninstall-adapter/SKILL.md +19 -37
  48. package/packages/sync/skills/intelligence-update/SKILL.md +34 -156
  49. package/packages/sync/docs/ADAPTERS.md +0 -214
  50. package/packages/sync/docs/CONVENTIONS.md +0 -456
@@ -1,10 +1,10 @@
1
1
  #!/bin/bash
2
- # intelligence remove <@scope/name> [--no-sync]
2
+ # Internal package remove operation.
3
3
  set -euo pipefail
4
4
  source "$CLI_DIR/lib/cli-common.sh"
5
5
 
6
6
  name="${1:-}"
7
- [ -n "$name" ] || die "usage: intelligence remove <@scope/name>"
7
+ [ -n "$name" ] || die "usage: intelligence package remove <@scope/name>"
8
8
  shift || true
9
9
  no_sync=0 force=0
10
10
  for a in "$@"; do
@@ -24,7 +24,7 @@ if [ "$name" = "$SYNC_PKG_NAME" ] && [ "$force" -eq 0 ]; then
24
24
  echo "ERROR: $SYNC_PKG_NAME is the engine's own content — removing it drops every" >&2
25
25
  echo " intelligence-* meta-skill, the authoring rule and the engine agents from" >&2
26
26
  echo " the outputs. The sync engine itself keeps working (it ships with the CLI)." >&2
27
- echo " If that is what you want: intelligence remove $SYNC_PKG_NAME --force" >&2
27
+ echo " If that is what you want: intelligence package remove $SYNC_PKG_NAME --force" >&2
28
28
  exit 1
29
29
  fi
30
30
  assert_valid_pkg_name "$name"
@@ -1,5 +1,5 @@
1
1
  #!/bin/bash
2
- # intelligence search [term] — what the registries offer, and what this project
2
+ # Internal package search operation: what the registries offer and what this project
3
3
  # already has. The catalogue view `list` deliberately is not: `list` answers
4
4
  # "what does this project depend on", `search` answers "what could it".
5
5
  #
@@ -27,7 +27,7 @@ pkg_state() {
27
27
  elif [ -d "$IP_ROOT/.intelligence/packages/$name" ]; then
28
28
  printf 'installed %s' "$(qmap_field "$IP_ROOT/intelligence.lock" "packages" "$name" "resolved")"
29
29
  else
30
- printf 'declared (run install)'
30
+ printf 'declared (run sync)'
31
31
  fi
32
32
  }
33
33
 
@@ -75,9 +75,9 @@ if [ "$rows" -eq 0 ]; then
75
75
  else
76
76
  echo "No trusted registries (or they offer nothing) — intelligence registry add <repo-url>."
77
77
  fi
78
- echo "Explicit installs need no registry: intelligence add github:org/repo"
78
+ echo "Explicit installs need no registry: intelligence package add github:org/repo"
79
79
  exit 0
80
80
  fi
81
81
 
82
82
  echo ""
83
- echo "add one: intelligence add <name> | private registry: intelligence registry add <repo-url>"
83
+ echo "add one: intelligence package add <name> | private registry: intelligence registry add <repo-url>"
@@ -0,0 +1,97 @@
1
+ #!/bin/bash
2
+ # Internal package range update operation.
3
+ #
4
+ # Re-resolve version ranges against the remotes and rewrite the lock. Pinned
5
+ # packages (`ref:`) never move here — repin by editing the manifest or
6
+ # re-adding. `update <name>` limits the pass to one package.
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_v2
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
+ ref="$(qmap_field "$manifest" "packages" "$name" "ref")"
39
+ [ -n "$ref" ] && { echo " $name: pinned to ref '$ref' — skipped"; continue; }
40
+ range="$(qmap_field "$manifest" "packages" "$name" "version")"
41
+ # An exact version is a pin, same as ref: — update never moves pins.
42
+ if [ -n "$range" ] && semver_is_stable "$range"; then
43
+ echo " $name: pinned to $range — edit the manifest to move it"
44
+ continue
45
+ fi
46
+ url="$(qmap_field "$manifest" "packages" "$name" "url")"
47
+ path="$(qmap_field "$manifest" "packages" "$name" "path")"
48
+ if [ -z "$url" ]; then
49
+ resolve_package_source "$manifest" "$name"
50
+ url="$RES_URL"; path="$RES_PATH"
51
+ fi
52
+ picked="$(list_remote_versions "$url" | semver_pick_highest "$range")"
53
+ [ -n "$picked" ] || { echo " $name: nothing satisfies '${range:-*}' at $url" >&2; continue; }
54
+ read -r tag _ <<< "$(remote_tag_for_version "$url" "$picked")"
55
+ current="$(qmap_field "$lock" "packages" "$name" "resolved")"
56
+ cur_url="$(qmap_field "$lock" "packages" "$name" "url")"
57
+ cur_path="$(qmap_field "$lock" "packages" "$name" "path")"
58
+ # Same tag is not enough: a registry may re-point a name's source while
59
+ # keeping the version — the move doctor warns about must be actionable
60
+ # here, so url/path changes refetch too.
61
+ if [ "$tag" = "$current" ] && [ "$url" = "$cur_url" ] && [ "$path" = "$cur_path" ]; then
62
+ echo " $name: ${current:-<none>} (up to date)"
63
+ continue
64
+ fi
65
+ if [ "$preview" -eq 1 ]; then
66
+ echo " $name: ${current:-<none>} -> $tag"
67
+ moved=$((moved + 1))
68
+ continue
69
+ fi
70
+ rel=".intelligence/packages/$name"
71
+ # Fetch into staging FIRST: a failed clone must leave the current install
72
+ # fully wired and intact. Only after success is the old shape unwired
73
+ # (the new version may have dropped a section dir), the store swapped,
74
+ # and the new shape wired.
75
+ staging="$IP_ROOT/.intelligence/.staging-$$"
76
+ rm -rf "$staging"
77
+ sha="$(fetch_package "$url" "$tag" "$path" "$staging")"
78
+ unwire_package_sources "$manifest" "$rel"
79
+ rm -rf "${IP_ROOT:?}/$rel"
80
+ mkdir -p "$(dirname "$IP_ROOT/$rel")"
81
+ mv "$staging" "$IP_ROOT/$rel"
82
+ wire_package_sources "$manifest" "$name" "$rel" "$IP_ROOT"
83
+ lock_upsert "$lock" "$name" "$range" "$url" "$path" "$tag" "$sha"
84
+ echo " $name: ${current:-<none>} -> $tag"
85
+ moved=$((moved + 1))
86
+ done < <(qmap_keys "$manifest" "packages")
87
+
88
+ [ -n "$only" ] && [ "$found" -eq 0 ] && die "package '$only' is not in the manifest"
89
+ if [ "$preview" -eq 1 ]; then
90
+ echo "updates available: $moved package(s)"
91
+ else
92
+ echo "updated: $moved package(s)"
93
+ fi
94
+
95
+ if [ "$preview" -eq 0 ] && [ "$moved" -gt 0 ] && [ "$no_sync" -eq 0 ]; then
96
+ exec bash "$CLI_DIR/commands/sync.sh"
97
+ fi
@@ -1,5 +1,5 @@
1
1
  #!/bin/bash
2
- # intelligence install [--frozen] [--force] [--no-sync]
2
+ # Internal locked-store restore operation.
3
3
  #
4
4
  # Restore .intelligence/ from intelligence.lock — the fresh-clone command.
5
5
  # --frozen additionally refuses any manifest/lock divergence (CI mode) and
@@ -30,7 +30,7 @@ if [ "$frozen" -eq 1 ]; then
30
30
  while IFS= read -r name; do
31
31
  [ -n "$name" ] || continue
32
32
  [ -n "$(qmap_field "$lock" "packages" "$name" "url")" ] \
33
- || die "--frozen: $name is in the manifest but not in intelligence.lock"
33
+ || die "locked restore: $name is in the manifest but not in intelligence.lock"
34
34
  m_ver="$(qmap_field "$manifest" "packages" "$name" "version")"
35
35
  m_ref="$(qmap_field "$manifest" "packages" "$name" "ref")"
36
36
  m_url="$(qmap_field "$manifest" "packages" "$name" "url")"
@@ -40,7 +40,7 @@ if [ "$frozen" -eq 1 ]; then
40
40
  l_url="$(qmap_field "$lock" "packages" "$name" "url")"
41
41
  l_path="$(qmap_field "$lock" "packages" "$name" "path")"
42
42
  [ -z "$m_ver" ] || [ "$m_ver" = "$l_req" ] \
43
- || die "--frozen: $name requests '$m_ver' but the lock recorded '$l_req' — run 'intelligence install' without --frozen"
43
+ || die "locked restore: $name requests '$m_ver' but the lock recorded '$l_req' — reconcile the manifest and lock"
44
44
  [ -z "$m_ref" ] || [ "$m_ref" = "$l_res" ] \
45
45
  || die "--frozen: $name pins ref '$m_ref' but the lock resolved '$l_res'"
46
46
  [ -z "$m_url" ] || [ "$m_url" = "$l_url" ] \
@@ -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_v2
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"
@@ -1,17 +1,34 @@
1
1
  #!/bin/bash
2
- # intelligence upgrade — bring the project to this CLI's engine: apply v2
2
+ # Internal: bring the project to this CLI's engine: apply v2
3
3
  # schema migrations, align the engine-content package to the bundled version,
4
- # restamp sync_version, sync.
4
+ # restamp schema_version, sync.
5
5
  set -euo pipefail
6
6
  source "$CLI_DIR/lib/cli-common.sh"
7
7
 
8
+ no_sync=0
9
+ while [ $# -gt 0 ]; do
10
+ case "$1" in
11
+ --no-sync) no_sync=1 ;;
12
+ *) die "internal upgrade: unknown option '$1'" ;;
13
+ esac
14
+ shift
15
+ done
16
+
8
17
  require_v2
9
18
  manifest="$IP_ROOT/intelligence.yaml"
10
19
 
11
- stamp="$(read_engine_stamp "$manifest")"
20
+ stamp="$(read_schema_version "$manifest")"
21
+ legacy_stamp="$(top_scalar "$manifest" "sync_version")"
22
+ schema_key_count="$(awk '/^schema_version:[[:space:]]*/ { n++ } END { print n + 0 }' "$manifest")"
23
+ legacy_key_count="$(awk '/^sync_version:[[:space:]]*/ { n++ } END { print n + 0 }' "$manifest")"
12
24
  eng="$(bundled_engine_version)"
25
+ [ "$schema_key_count" -le 1 ] || die "manifest has duplicate schema_version keys"
26
+ [ "$legacy_key_count" -le 1 ] || die "manifest has duplicate sync_version keys"
13
27
  if [ -n "$stamp" ] && _ver_gt "$stamp" "$eng"; then
14
- die "manifest schema $stamp is newer than this CLI's engine $eng — update the CLI first: npm i -g @ainova-systems/intelligence@latest"
28
+ die "manifest schema $stamp is newer than this CLI's engine $eng — update the global CLI first"
29
+ fi
30
+ if [ -n "$legacy_stamp" ] && _ver_gt "$legacy_stamp" "$eng"; then
31
+ die "manifest schema $legacy_stamp is newer than this CLI's engine $eng — update the global CLI first"
15
32
  fi
16
33
 
17
34
  # --- v2 schema migrations -------------------------------------------------
@@ -19,6 +36,19 @@ fi
19
36
  # Same discipline: ascending, append-only, each migration self-detects and
20
37
  # no-ops when already applied.
21
38
 
39
+ # v2 migration 0: the original RC manifest called the permanent schema key
40
+ # `sync_version`. Rename it without changing its value; the final stamp below
41
+ # then aligns it with this engine. The archived v1 reader remains separate.
42
+ if [ -n "$legacy_stamp" ]; then
43
+ if [ -n "$stamp" ]; then
44
+ [ "$stamp" = "$legacy_stamp" ] || die "manifest has conflicting schema_version '$stamp' and sync_version '$legacy_stamp'"
45
+ _qmap_stage "$manifest" '$0 !~ /^sync_version:[[:space:]]*/'
46
+ else
47
+ _qmap_stage "$manifest" '{ sub(/^sync_version:/, "schema_version:"); print }'
48
+ fi
49
+ echo " migrating: sync_version -> schema_version"
50
+ fi
51
+
22
52
  # v2 migration 1: staged engine content -> the @ainova-systems/sync package.
23
53
  # Pre-package manifests listed `.intelligence/engine/{rules,agents,skills}`
24
54
  # as sources fed by a CLI-staged copy; that copy is now an ordinary package
@@ -59,10 +89,20 @@ if [ "$have_pkg" -eq 1 ] && [ "$migrated" -eq 0 ]; then
59
89
  sync_pkg_install "$IP_ROOT"
60
90
  fi
61
91
  if [ "$have_pkg" -eq 0 ] && [ "$migrated" -eq 0 ]; then
62
- echo " NOTE: $SYNC_PKG_NAME is not in this manifest (bare setup) — engine meta-skills stay uninstalled; 'intelligence add $SYNC_PKG_NAME' opts back in." >&2
92
+ echo " NOTE: $SYNC_PKG_NAME is not in this manifest (bare setup) — engine meta-skills stay uninstalled; 'intelligence package add $SYNC_PKG_NAME' opts back in." >&2
63
93
  fi
64
94
 
65
- stamp_version "$manifest" "$eng"
66
- echo " sync_version -> $eng"
95
+ # A previous RC could leave this managed directory even after its source
96
+ # entries were already removed. Its presence is itself an upgrade predicate,
97
+ # so always close that post-condition.
98
+ if [ -d "$IP_ROOT/.intelligence/engine" ]; then
99
+ rm -rf "$IP_ROOT/.intelligence/engine"
100
+ echo " removed stale .intelligence/engine"
101
+ fi
67
102
 
68
- exec bash "$CLI_DIR/commands/sync.sh"
103
+ stamp_schema_version "$manifest" "$eng"
104
+ echo " schema_version -> $eng"
105
+
106
+ if [ "$no_sync" -eq 0 ]; then
107
+ exec bash "$CLI_DIR/commands/sync.sh"
108
+ fi
@@ -54,8 +54,8 @@ source "$CLI_DIR/lib/lockfile.sh"
54
54
  # The engine-content package: OPTIONAL but auto-selected at init. Package by
55
55
  # UX (manifest entry, lockfile row, list/search/remove), bundle by mechanics —
56
56
  # at the version the CLI ships, it materializes from the npm bundle without
57
- # network; only a cross-version install reaches git. The pin is held exactly
58
- # at the bundled engine version and moved only by `upgrade`.
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
59
  #
60
60
  # Its identity is DATA shipped with the distribution (cli/engine-package.yaml),
61
61
  # never a name compiled into cli code — a fork edits the file.
@@ -113,7 +113,7 @@ require_v2() {
113
113
  detect_project
114
114
  case "$IP_MODE" in
115
115
  v2) ;;
116
- legacy) die "this is a vendored (v1) setup — run 'intelligence migrate' first, or keep using its own flow" ;;
116
+ legacy) die "this is a vendored (v1) setup — run 'intelligence init' to migrate it, or keep using its own flow" ;;
117
117
  *) die "no intelligence project found here — run 'intelligence init'" ;;
118
118
  esac
119
119
  }
@@ -126,6 +126,14 @@ manifest_intelligence_dir() {
126
126
  printf '%s' "${v:-intelligence}"
127
127
  }
128
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
+
129
137
  bundled_engine_version() {
130
138
  tr -d ' \t\r\n' < "$IS_ENGINE_DIR/VERSION"
131
139
  }
@@ -156,14 +164,132 @@ sync_pkg_install() {
156
164
  # Everything the IS_CLI mode of sync.sh needs, derived from the manifest.
157
165
  export_engine_env() {
158
166
  local root="$1"
159
- local umbrella
160
- umbrella="$(manifest_intelligence_dir "$root/intelligence.yaml")"
167
+ local content_rel
168
+ content_rel="$(manifest_intelligence_dir "$root/intelligence.yaml")"
161
169
  export IS_CLI=1
162
170
  export CONFIG_FILE="$root/intelligence.yaml"
163
171
  export REPO_ROOT="$root"
164
- export IS_UMBRELLA_REL="$umbrella"
172
+ export IS_CONTENT_REL="$content_rel"
165
173
  export IS_MODULE_REL="$SYNC_PKG_STORE"
166
174
  export IS_SYNC_CMD="intelligence sync"
167
175
  export IS_MANIFEST_NAME="intelligence.yaml"
168
- export IS_PROTECTED_DIRS="$umbrella:.intelligence"
176
+ export IS_PROTECTED_DIRS="$content_rel:.intelligence"
177
+ }
178
+
179
+ # --- Project lifecycle preflight -----------------------------------------
180
+ # Public commands are intentionally few. They share this state gate so a CLI
181
+ # installed at a newer engine version brings the current v2 project forward
182
+ # before a mutating operation. The npm install itself cannot do that: it runs
183
+ # outside any project and does not know which repositories the user owns.
184
+
185
+ is_ci_environment() {
186
+ case "${CI:-}" in
187
+ 1|true|TRUE|True|yes|YES|Yes|on|ON|On) return 0 ;;
188
+ *) return 1 ;;
189
+ esac
190
+ }
191
+
192
+ # Validate a repository-relative project content directory, including every
193
+ # existing symlinked path component. Call before writing a new manifest.
194
+ assert_safe_content_dir() {
195
+ local root="$1" content_dir="$2" repo_phys probe old_ifs part probe_phys
196
+ local -a parts
197
+ case "$content_dir" in
198
+ ""|/*|.|..|../*|*/../*|*/..|.git|.git/*|.intelligence|.intelligence/*|*\\*|[A-Za-z]:*)
199
+ die "unsafe content directory '$content_dir'"
200
+ ;;
201
+ esac
202
+ repo_phys="$(cd "$root" && pwd -P)"
203
+ probe="$root"
204
+ old_ifs="$IFS"; IFS='/'; read -r -a parts <<< "$content_dir"; IFS="$old_ifs"
205
+ for part in "${parts[@]}"; do
206
+ [ -n "$part" ] || continue
207
+ probe="$probe/$part"
208
+ if [ -e "$probe" ] || [ -L "$probe" ]; then
209
+ probe_phys="$(cd "$probe" 2>/dev/null && pwd -P)" \
210
+ || die "cannot resolve project content path '$content_dir'"
211
+ case "$probe_phys" in
212
+ "$repo_phys"|"$repo_phys"/*) ;;
213
+ *) die "project content directory resolves outside the repository: '$content_dir'" ;;
214
+ esac
215
+ fi
216
+ done
217
+ }
218
+
219
+ project_needs_upgrade() {
220
+ local root="$1" manifest="$1/intelligence.yaml" stamp eng pinned locked name
221
+ [ -f "$manifest" ] || return 1
222
+ stamp="$(read_schema_version "$manifest")"
223
+ eng="$(bundled_engine_version)"
224
+ [ -z "$stamp" ] && return 0
225
+ [ -n "$(top_scalar "$manifest" "sync_version")" ] && return 0
226
+ _ver_gt "$eng" "$stamp" && return 0
227
+ [ -d "$root/.intelligence/engine" ] && return 0
228
+
229
+ while IFS= read -r name; do
230
+ [ "$name" = "$SYNC_PKG_NAME" ] || continue
231
+ pinned="$(qmap_field "$manifest" "packages" "$name" "version")"
232
+ locked="$(qmap_field "$root/intelligence.lock" "packages" "$name" "resolved")"
233
+ [ "$pinned" = "$eng" ] || return 0
234
+ if [ -f "$root/intelligence.lock" ]; then
235
+ [ -n "$locked" ] || return 0
236
+ [ "${locked#v}" = "$eng" ] || return 0
237
+ fi
238
+ done < <(qmap_keys "$manifest" "packages")
239
+ return 1
240
+ }
241
+
242
+ # A manifest with packages but no lock has no trustworthy resolved state from
243
+ # which lifecycle alignment can proceed. Never manufacture a partial lock.
244
+ project_has_packages() {
245
+ local root="$1" name found=1
246
+ while IFS= read -r name; do
247
+ [ -n "$name" ] || continue
248
+ assert_valid_pkg_name "$name"
249
+ found=0
250
+ done < <(qmap_keys "$root/intelligence.yaml" "packages")
251
+ return "$found"
252
+ }
253
+
254
+ ensure_project_current() {
255
+ local root="$1" explicit="${2:-}" manifest="$1/intelligence.yaml" stamp eng
256
+ check_version_compat "$manifest" || return $?
257
+ if project_has_packages "$root" && [ ! -f "$root/intelligence.lock" ]; then
258
+ die "manifest declares packages but intelligence.lock is absent — restore the committed lock before running project lifecycle commands"
259
+ fi
260
+ project_needs_upgrade "$root" || return 0
261
+ stamp="$(read_schema_version "$manifest")"
262
+ eng="$(bundled_engine_version)"
263
+ if is_ci_environment && [ "$explicit" != "--explicit" ]; then
264
+ die "project lifecycle requires alignment (stamp ${stamp:-unstamped}, engine $eng) — run 'intelligence init --apply' locally, review and commit the diff"
265
+ fi
266
+ echo " project alignment: stamp ${stamp:-unstamped}, engine $eng"
267
+ bash "$CLI_DIR/internal/upgrade-v2.sh" --no-sync
268
+ }
269
+
270
+ project_store_missing() {
271
+ local root="$1" manifest="$1/intelligence.yaml" name src
272
+ while IFS= read -r name; do
273
+ [ -n "$name" ] || continue
274
+ assert_valid_pkg_name "$name"
275
+ [ -d "$root/.intelligence/packages/$name" ] || return 0
276
+ done < <(qmap_keys "$manifest" "packages")
277
+ for section in rules agents skills; do
278
+ while IFS= read -r src; do
279
+ case "$src" in
280
+ .intelligence/packages/*)
281
+ [ -d "$root/$src" ] || return 0
282
+ ;;
283
+ esac
284
+ done < <(read_yaml_list "$manifest" "$section")
285
+ done
286
+ return 1
287
+ }
288
+
289
+ restore_project_store_if_missing() {
290
+ local root="$1"
291
+ project_store_missing "$root" || return 0
292
+ [ -f "$root/intelligence.lock" ] || die "package store is missing and intelligence.lock is absent — run 'intelligence init'"
293
+ echo " restoring package store from intelligence.lock"
294
+ bash "$CLI_DIR/internal/restore.sh" --frozen --no-sync
169
295
  }
@@ -12,7 +12,7 @@ LOCK_SEP=$'\x1f'
12
12
 
13
13
  # top_scalar <file> <key> — one top-level scalar (`key: "value"`), quotes and
14
14
  # trailing comment stripped. The declared exception to "no new parsing": the
15
- # engine's read_engine_stamp is hardwired to its own key.
15
+ # engine's read_schema_version is hardwired to its own key.
16
16
  top_scalar() {
17
17
  [ -f "$1" ] || return 0
18
18
  awk -v key="$2" '
@@ -36,6 +36,92 @@ assert_valid_pkg_name() {
36
36
  esac
37
37
  }
38
38
 
39
+ # Target names become adapter filenames and shell function suffixes, so keep
40
+ # them inside the portable shell-identifier subset used by built-in adapters.
41
+ assert_valid_target_name() {
42
+ case "$1" in
43
+ ""|[!abcdefghijklmnopqrstuvwxyz]*|*[!abcdefghijklmnopqrstuvwxyz0123456789_]*)
44
+ die "invalid target name '$1' — expected [a-z][a-z0-9_]*"
45
+ ;;
46
+ esac
47
+ }
48
+
49
+ # target_exists <manifest> <name> — distinguish a disabled target from one
50
+ # that is not declared at all. Both inline and block target shapes count.
51
+ target_exists() {
52
+ local file="$1" target="$2"
53
+ [ -f "$file" ] || return 1
54
+ awk -v target="$target" '
55
+ { sub(/\r$/, "") }
56
+ /^targets:[[:space:]]*$/ { in_targets = 1; next }
57
+ /^[A-Za-z]/ { in_targets = 0 }
58
+ in_targets && $0 ~ "^ " target ":[[:space:]]*" { found = 1; exit }
59
+ END { exit(found ? 0 : 1) }
60
+ ' "$file"
61
+ }
62
+
63
+ # target_set_enabled <manifest> <name> <true|false> <default-output>
64
+ # Transactionally update only `enabled`, preserving every other target field
65
+ # and comment. A missing target is added in the compact form used by init.
66
+ target_set_enabled() {
67
+ local file="$1" target="$2" enabled="$3" output="$4"
68
+ [ -f "$file" ] || die "no such file: $file"
69
+ case "$enabled" in true|false) ;; *) die "internal: invalid target state '$enabled'" ;; esac
70
+ output="$(_yq_esc "$output")"
71
+ _qmap_stage "$file" -v target="$target" -v enabled="$enabled" -v output="$output" '
72
+ function entry() { return " " target ": { enabled: " enabled ", output: \"" output "\" }" }
73
+ function add_enabled_to_inline(line, p) {
74
+ p = index(line, "{")
75
+ if (p > 0) return substr(line, 1, p) " enabled: " enabled "," substr(line, p + 1)
76
+ return line
77
+ }
78
+ function flush_target() {
79
+ if (in_target && !enabled_seen) print " enabled: " enabled
80
+ in_target = 0
81
+ }
82
+ function flush_targets() {
83
+ flush_target()
84
+ if (in_targets && !target_seen) { print entry(); target_seen = 1 }
85
+ in_targets = 0
86
+ }
87
+ { sub(/\r$/, "") }
88
+ /^targets:[[:space:]]*$/ { targets_seen = 1; in_targets = 1; print; next }
89
+ in_targets && /^[A-Za-z]/ { flush_targets() }
90
+ in_targets && /^ [A-Za-z]/ {
91
+ flush_target()
92
+ if ($0 ~ "^ " target ":[[:space:]]*") {
93
+ target_seen = 1
94
+ if (match($0, /enabled:[[:space:]]*(true|false)/)) {
95
+ print substr($0, 1, RSTART - 1) "enabled: " enabled substr($0, RSTART + RLENGTH)
96
+ enabled_seen = 1
97
+ } else if ($0 ~ /\{/) {
98
+ print add_enabled_to_inline($0)
99
+ enabled_seen = 1
100
+ } else {
101
+ print
102
+ in_target = 1
103
+ enabled_seen = 0
104
+ }
105
+ next
106
+ }
107
+ }
108
+ in_target && /^ enabled:[[:space:]]*/ {
109
+ print " enabled: " enabled
110
+ enabled_seen = 1
111
+ next
112
+ }
113
+ { last = $0; print }
114
+ END {
115
+ flush_targets()
116
+ if (!targets_seen) {
117
+ if (last != "") print ""
118
+ print "targets:"
119
+ print entry()
120
+ }
121
+ }
122
+ '
123
+ }
124
+
39
125
  # qmap_keys <file> <block> — quoted keys, one per line.
40
126
  qmap_keys() {
41
127
  [ -f "$1" ] || return 0
@@ -11,7 +11,7 @@
11
11
  # visible, reviewable, deletable.
12
12
  #
13
13
  # The NAME is the trust anchor a developer reasons with; source integrity is
14
- # a separate mechanism: the lock pins url+sha, and doctor flags
14
+ # a separate mechanism: the lock pins url+sha, and `status --check` flags
15
15
  # resolution/lock url drift.
16
16
  #
17
17
  # An index is itself fetched with git (never curl): auth, proxies and private
@@ -93,7 +93,7 @@ suggest_similar() {
93
93
  cshort="${cand#*/}"; cshort="${cshort%s}"
94
94
  if [ "$cshort" = "$want" ] && [ "$cand" != "$name" ]; then
95
95
  seen="$seen$cand "
96
- echo " Did you mean: intelligence add $cand" >&2
96
+ echo " Did you mean: intelligence package add $cand" >&2
97
97
  fi
98
98
  done < <(qmap_keys "$idx" "packages")
99
99
  }
@@ -116,8 +116,8 @@ fetch_package() {
116
116
  assert_safe_source_url "$url"
117
117
  assert_safe_ref "$ref"
118
118
  # Bundle seed: the engine-content package at the CLI's own version copies
119
- # from the npm bundle — no network, which keeps init / fresh-clone install
120
- # / migrate offline exactly like the staging they replace. Keyed on the
119
+ # from the npm bundle — no network, which keeps init, sync restoration and
120
+ # archived-project conversion offline. Keyed on the
121
121
  # full (url, path, ref) triple; any other version or source falls through
122
122
  # to the normal clone.
123
123
  if [ "$url" = "$SYNC_PKG_URL" ] && [ "$subpath" = "$SYNC_PKG_PATH" ] \
package/engine/ENGINE_SHA CHANGED
@@ -1 +1 @@
1
- 557cde6997dce612e0e8a5a11adc2472173dcec0
1
+ f2bfc5e3722bf6fb2f7309db2d248368d5b0cc5e