@ainova-systems/intelligence 0.11.0-rc.8 → 0.11.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 (47) hide show
  1. package/README.md +4 -0
  2. package/cli/commands/adapter.sh +46 -6
  3. package/cli/commands/init.sh +138 -33
  4. package/cli/commands/package.sh +2 -2
  5. package/cli/commands/registry.sh +3 -3
  6. package/cli/commands/status.sh +4 -4
  7. package/cli/commands/sync.sh +76 -23
  8. package/cli/commands/update.sh +1 -1
  9. package/cli/intelligence +18 -3
  10. package/cli/internal/{upgrade-v2.sh → align-project.sh} +33 -7
  11. package/cli/internal/check.sh +49 -20
  12. package/cli/internal/{migrate-v1.sh → convert-legacy.sh} +36 -34
  13. package/cli/internal/package-add.sh +9 -14
  14. package/cli/internal/package-list.sh +1 -1
  15. package/cli/internal/package-remove.sh +1 -1
  16. package/cli/internal/package-search.sh +1 -1
  17. package/cli/internal/package-update.sh +31 -25
  18. package/cli/internal/restore.sh +5 -16
  19. package/cli/internal/target-state.sh +26 -18
  20. package/cli/lib/adapter-lifecycle.sh +33 -0
  21. package/cli/lib/cli-common.sh +28 -20
  22. package/cli/lib/gitignore.sh +174 -0
  23. package/cli/lib/manifest.sh +58 -1
  24. package/cli/lib/onboarding.sh +63 -0
  25. package/engine/ENGINE_SHA +1 -1
  26. package/engine/adapters/_template.sh +19 -2
  27. package/engine/adapters/agents.sh +14 -3
  28. package/engine/adapters/claude.sh +15 -0
  29. package/engine/adapters/codex.sh +11 -1
  30. package/engine/adapters/copilot.sh +11 -0
  31. package/engine/adapters/cursor.sh +15 -0
  32. package/engine/adapters/opencode.sh +11 -0
  33. package/engine/adapters/pi.sh +18 -5
  34. package/engine/lib/adapter-contract.sh +100 -0
  35. package/engine/lib/common.sh +5 -0
  36. package/engine/lib/contract.sh +2 -2
  37. package/engine/sync.sh +87 -25
  38. package/package.json +1 -1
  39. package/packages/sync/agents/intelligence-architect.md +2 -2
  40. package/packages/sync/references/adapters.md +51 -7
  41. package/packages/sync/references/conventions.md +57 -22
  42. package/packages/sync/references/onboarding-migration.md +79 -0
  43. package/packages/sync/skills/intelligence-install-adapter/SKILL.md +12 -5
  44. package/packages/sync/skills/intelligence-learn-from-context/SKILL.md +23 -4
  45. package/packages/sync/skills/intelligence-learn-from-repository/SKILL.md +90 -38
  46. package/packages/sync/skills/intelligence-review-skills/SKILL.md +1 -1
  47. package/packages/sync/skills/intelligence-sync/SKILL.md +1 -1
@@ -20,9 +20,9 @@ case "$IP_MODE" in
20
20
  exit 0
21
21
  ;;
22
22
  legacy)
23
- echo "project: vendored (v1) at $IP_ROOT"
23
+ echo "project: legacy Intelligence Sync at $IP_ROOT"
24
24
  ok "vendored engine $(tr -d ' \t\r\n' < "$IP_MODULE_DIR/scripts/VERSION")"
25
- echo " → 'intelligence init' converts it to the CLI setup"
25
+ echo " → 'intelligence init' converts it into an Intelligence project"
26
26
  exit 0
27
27
  ;;
28
28
  esac
@@ -57,9 +57,9 @@ if [ -d "$IP_ROOT/.intelligence/engine" ]; then
57
57
  warn "stale .intelligence/engine directory (pre-package layout) — run 'intelligence init'"
58
58
  fi
59
59
 
60
- # Manifest packages: names valid, locked, installed — and the lock's source
61
- # still the one resolution would pick today (a registry silently re-pointing
62
- # an installed name is the dependency-confusion tail this check cuts off).
60
+ # Manifest packages: names valid, requested intent only, locked and installed.
61
+ # The lock is authoritative for resolved source URL/path until an explicit
62
+ # package re-add changes that trust decision.
63
63
  while IFS= read -r name; do
64
64
  [ -n "$name" ] || continue
65
65
  assert_valid_pkg_name "$name"
@@ -68,35 +68,64 @@ while IFS= read -r name; do
68
68
  else
69
69
  m_ver="$(qmap_field "$manifest" "packages" "$name" "version")"
70
70
  m_ref="$(qmap_field "$manifest" "packages" "$name" "ref")"
71
- m_url="$(qmap_field "$manifest" "packages" "$name" "url")"
72
- m_path="$(qmap_field "$manifest" "packages" "$name" "path")"
73
71
  l_req="$(qmap_field "$lock" "packages" "$name" "requested")"
74
72
  l_res="$(qmap_field "$lock" "packages" "$name" "resolved")"
75
- l_url="$(qmap_field "$lock" "packages" "$name" "url")"
76
- l_path="$(qmap_field "$lock" "packages" "$name" "path")"
77
73
  [ -z "$m_ver" ] || [ "$m_ver" = "$l_req" ] \
78
74
  || warn "$name requests '$m_ver' but the lock recorded '$l_req' — run 'intelligence update --preview'"
79
75
  [ -z "$m_ref" ] || [ "$m_ref" = "$l_res" ] \
80
76
  || warn "$name pins ref '$m_ref' but the lock resolved '$l_res'"
81
- [ -z "$m_url" ] || [ "$m_url" = "$l_url" ] \
82
- || warn "$name declares url '$m_url' but the lock recorded '$l_url'"
83
- [ -z "$m_path" ] || [ "$m_path" = "$l_path" ] \
84
- || warn "$name declares path '$m_path' but the lock recorded '$l_path'"
77
+ if [ -n "$(qmap_field "$manifest" "packages" "$name" "url")" ] \
78
+ || [ -n "$(qmap_field "$manifest" "packages" "$name" "path")" ]; then
79
+ warn "$name stores source details in the manifest — run 'intelligence init --apply' to move them to the lock-only model"
80
+ fi
85
81
  if [ ! -d "$IP_ROOT/.intelligence/packages/$name" ]; then
86
82
  warn "$name is locked but not installed — run 'intelligence sync'"
87
83
  continue
88
84
  fi
89
85
  ok "$name @ $(qmap_field "$lock" "packages" "$name" "resolved")"
90
- if [ -z "$(qmap_field "$manifest" "packages" "$name" "url")" ]; then
91
- locked_url="$(qmap_field "$lock" "packages" "$name" "url")"
92
- resolve_package_source "$manifest" "$name" 2>/dev/null || true
93
- if [ -n "${RES_URL:-}" ] && [ -n "$locked_url" ] && [ "$RES_URL" != "$locked_url" ]; then
94
- warn "$name now resolves to $RES_URL but the lock pins $locked_url — a registry re-pointed the name; verify with 'intelligence update --preview'"
95
- fi
96
- fi
97
86
  fi
98
87
  done < <(qmap_keys "$manifest" "packages")
99
88
 
89
+ # Adapter contracts are the shared source of truth for dependencies, rollback,
90
+ # onboarding backup and Git policy. Validate every declared target, including
91
+ # disabled ones, so a later enable cannot reveal a stale custom adapter.
92
+ content_dir="$(manifest_intelligence_dir "$manifest")"
93
+ while IFS= read -r target; do
94
+ [ -n "$target" ] || continue
95
+ output="$(get_target_output "$manifest" "$target")"
96
+ [ -n "$output" ] || output="$(default_target_output "$target")"
97
+ adapter_file="$(adapter_file_for "$IP_ROOT" "$content_dir" "$target" 2>/dev/null || true)"
98
+ if [ -z "$adapter_file" ]; then
99
+ warn "adapter '$target' is declared but its implementation is missing"
100
+ continue
101
+ fi
102
+ records="$(adapter_contract_records "$target" "$adapter_file" "$output" 2>/dev/null || true)"
103
+ if ! printf '%s\n' "$records" | grep -Fqx $'version\t1'; then
104
+ warn "adapter '$target' has an invalid ownership contract"
105
+ continue
106
+ fi
107
+ while IFS=$'\t' read -r kind value; do
108
+ if [ "$kind" = "requires" ] \
109
+ && [ "$(is_target_enabled "$manifest" "$target")" = "1" ] \
110
+ && [ "$(is_target_enabled "$manifest" "$value")" != "1" ]; then
111
+ warn "enabled adapter '$target' requires enabled adapter '$value'"
112
+ fi
113
+ if [ "$(is_target_enabled "$manifest" "$target")" = "1" ]; then
114
+ case "$kind" in
115
+ ignore)
116
+ grep -Fqx -- "$value" "$IP_ROOT/.gitignore" 2>/dev/null \
117
+ || warn "adapter '$target' Git policy is missing '$value'"
118
+ ;;
119
+ include)
120
+ grep -Fqx -- "!$value" "$IP_ROOT/.gitignore" 2>/dev/null \
121
+ || warn "adapter '$target' Git policy is missing '!$value'"
122
+ ;;
123
+ esac
124
+ fi
125
+ done <<< "$records"
126
+ ok "adapter $target contract v1"
127
+ done < <(target_names "$manifest")
128
+
100
129
  # Lock orphans.
101
130
  if [ -f "$lock" ]; then
102
131
  while IFS= read -r name; do
@@ -1,6 +1,6 @@
1
1
  #!/bin/bash
2
- # Internal v1-to-v2 migration operation.
3
- # to the CLI setup: root intelligence.yaml, .intelligence/ store,
2
+ # Internal conversion from legacy Intelligence Sync to Intelligence CLI.
3
+ # to an Intelligence project: root intelligence.yaml, .intelligence/ store,
4
4
  # intelligence.lock, no vendored engine.
5
5
  #
6
6
  # Transactional: STAGE everything next to the
@@ -24,8 +24,8 @@ done
24
24
  detect_project
25
25
  case "$IP_MODE" in
26
26
  legacy) ;;
27
- v2) die "already on the v2 setup ($IP_ROOT/intelligence.yaml)" ;;
28
- *) die "no vendored setup found here — 'intelligence init' starts a fresh project" ;;
27
+ cli) die "already an Intelligence project ($IP_ROOT/intelligence.yaml)" ;;
28
+ *) die "no legacy Intelligence Sync project found here — 'intelligence init' starts a fresh project" ;;
29
29
  esac
30
30
  root="$IP_ROOT"
31
31
  umbrella="$IP_UMBRELLA"
@@ -36,22 +36,22 @@ umbrella_rel="${umbrella#"$root"/}"
36
36
 
37
37
  for existing in intelligence.yaml intelligence.lock .intelligence; do
38
38
  if [ -e "$root/$existing" ] || [ -L "$root/$existing" ]; then
39
- die "$existing already exists at the root — conflicting v2 state; move it aside or restore the project before migrating"
39
+ die "$existing already exists at the root — conflicting Intelligence CLI state; move it aside or restore the project before converting"
40
40
  fi
41
41
  done
42
42
  if [ "$dry_run" -eq 0 ] && [ "$force" -eq 0 ]; then
43
43
  if [ -n "$(git -C "$root" status --porcelain 2>/dev/null)" ]; then
44
- die "working tree is not clean — commit or stash first (or --force); migrate wants a one-commit diff you can review and revert"
44
+ die "working tree is not clean — commit or stash first (or --force); conversion produces a one-commit diff you can review and revert"
45
45
  fi
46
46
  fi
47
47
 
48
- # `sync_version` belongs to the archived v1 format. The v2 contract uses
48
+ # `sync_version` belongs to the legacy Intelligence Sync format. Intelligence uses
49
49
  # `schema_version`, so conversion reads the legacy scalar explicitly.
50
50
  stamp="$(top_scalar "$config" "sync_version")"
51
51
  eng="$(bundled_engine_version)"
52
52
  [ -n "$stamp" ] && _ver_gt "$stamp" "$eng" && die "project schema $stamp is newer than this CLI's engine $eng — update the CLI first"
53
53
 
54
- # The v1 `packs:` block is the one v1 shape migrate still has to read, and the
54
+ # The legacy `packs:` block is the one Intelligence Sync shape conversion still reads, and the
55
55
  # engine no longer knows it — the reader lives here, with its only consumer.
56
56
  get_pack_field() {
57
57
  get_nested_yaml_value "$1" "packs" "$2" "$3"
@@ -73,10 +73,10 @@ validate_mirror_path() {
73
73
  case "$canon" in "$root"/*) ;; *) die "pack '$pack' mirror escapes the repository: '$mirror'" ;; esac
74
74
  rel="${canon#"$root"/}"
75
75
 
76
- # A v1 mirror may live below the content directory (the normal location is
76
+ # A legacy mirror may live below the content directory (the normal location is
77
77
  # intelligence/external/<pack>), but it must never own the directory itself,
78
78
  # the vendored module, CLI state, authored sources, or generated outputs.
79
- case "$umbrella_rel" in "$rel"|"$rel"/*) die "pack '$pack' mirror '$mirror' contains the v1 content directory" ;; esac
79
+ case "$umbrella_rel" in "$rel"|"$rel"/*) die "pack '$pack' mirror '$mirror' contains the legacy content directory" ;; esac
80
80
  for protected in .git .intelligence "$module_rel"; do
81
81
  paths_overlap "$rel" "$protected" && die "pack '$pack' mirror '$mirror' overlaps protected path '$protected'"
82
82
  done
@@ -115,14 +115,14 @@ cleanup_stage() { rm -rf "$stage"; }
115
115
  trap cleanup_stage EXIT INT TERM
116
116
  mkdir -p "$stage/.intelligence/packages"
117
117
 
118
- # v2 carries no v1 migration chain: v1 is archived, and the archived engine is
119
- # what knows how to walk an old project forward. So migrate accepts only a
120
- # project already at the final v1 schema and names the exact remedy otherwise —
118
+ # Intelligence carries no legacy migration chain: the archived Intelligence Sync
119
+ # engine knows how to walk an old project forward. Conversion accepts only a
120
+ # project already at the final legacy schema and names the exact remedy otherwise —
121
121
  # a remedy that keeps working indefinitely, because the archive stays at the
122
122
  # URL the project's own update.sh already clones.
123
- V1_FINAL_SCHEMA="0.10.0"
124
- if [ -z "$stamp" ] || _ver_gt "$V1_FINAL_SCHEMA" "$stamp"; then
125
- echo "ERROR: project schema is ${stamp:-absent (pre-0.3.1)}, older than the final v1 schema $V1_FINAL_SCHEMA." >&2
123
+ LEGACY_FINAL_SCHEMA="0.10.0"
124
+ if [ -z "$stamp" ] || _ver_gt "$LEGACY_FINAL_SCHEMA" "$stamp"; then
125
+ echo "ERROR: project schema is ${stamp:-absent (pre-0.3.1)}, older than the final legacy Intelligence Sync schema $LEGACY_FINAL_SCHEMA." >&2
126
126
  echo " Bring it forward with its own engine first — that still works:" >&2
127
127
  echo " bash $module_rel/scripts/update.sh --yes" >&2
128
128
  echo " then re-run: intelligence init" >&2
@@ -134,7 +134,7 @@ echo "== staging =="
134
134
  # bundle (fetch_package's guard — no network).
135
135
  sync_pkg_sha="$(fetch_package "$SYNC_PKG_URL" "v$eng" "$SYNC_PKG_PATH" "$stage/$SYNC_PKG_STORE")"
136
136
 
137
- # Legacy pack name -> v2 package name (@org/repo derived from the url), plus
137
+ # Legacy pack name -> Intelligence package name (@org/repo derived from the url), plus
138
138
  # per-pack source facts. Recorded as rows for the config rewrite and the lock.
139
139
  pack_rows="$stage/packs.rows"
140
140
  : > "$pack_rows"
@@ -194,6 +194,7 @@ while IFS="$LOCK_SEP" read -r pack name url ref mirror; do
194
194
  if [ -z "$sha" ] || [ "$sha" = "unknown" ]; then
195
195
  sha="$(GIT_TERMINAL_PROMPT=0 git ls-remote -- "$url" "$ref" 2>/dev/null | awk '{print $1; exit}')"
196
196
  fi
197
+ [ -n "$ref" ] || ref="HEAD"
197
198
  printf '%s\037%s\037%s\037%s\037%s\037%s\n' \
198
199
  "$name" "" "$url" "" "$ref" "$sha" >> "$lock_rows"
199
200
  done < "$pack_rows"
@@ -261,17 +262,14 @@ while IFS= read -r line || [ -n "$line" ]; do
261
262
  done < "$config" > "$manifest_stage"
262
263
  stamp_schema_version "$manifest_stage" "$eng"
263
264
 
264
- # packages: entries (pin preserved — no invented ranges).
265
+ # packages: requested pins only; resolved source details live in the lock.
265
266
  while IFS="$LOCK_SEP" read -r pack name url ref mirror; do
266
267
  [ -n "$pack" ] || continue
267
- qmap_set "$manifest_stage" "packages" "$name" "url" "$url"
268
- [ -n "$ref" ] && qmap_set "$manifest_stage" "packages" "$name" "ref" "$ref"
268
+ qmap_set "$manifest_stage" "packages" "$name" "ref" "${ref:-HEAD}"
269
269
  done < "$pack_rows"
270
270
 
271
271
  # The engine's own content rides along as a package, pinned to the engine.
272
272
  qmap_set "$manifest_stage" "packages" "$SYNC_PKG_NAME" "version" "$eng"
273
- qmap_set "$manifest_stage" "packages" "$SYNC_PKG_NAME" "url" "$SYNC_PKG_URL"
274
- qmap_set "$manifest_stage" "packages" "$SYNC_PKG_NAME" "path" "$SYNC_PKG_PATH"
275
273
  printf '%s\037%s\037%s\037%s\037%s\037%s\n' \
276
274
  "$SYNC_PKG_NAME" "$eng" "$SYNC_PKG_URL" "$SYNC_PKG_PATH" "v$eng" "$sync_pkg_sha" >> "$lock_rows"
277
275
 
@@ -327,6 +325,10 @@ if [ -f "$root/.gitignore" ]; then
327
325
  gitignore_existed=1
328
326
  cp "$root/.gitignore" "$stage/gitignore.orig"
329
327
  fi
328
+ while IFS= read -r policy_file; do
329
+ [ -f "$root/$policy_file" ] || continue
330
+ cp "$root/$policy_file" "$stage/$policy_file.orig"
331
+ done < <(publisher_ignore_file_names)
330
332
  commit_active=1
331
333
  rollback() {
332
334
  [ "$commit_active" -eq 1 ] || return 0
@@ -338,6 +340,10 @@ rollback() {
338
340
  else
339
341
  rm -f "$root/.gitignore"
340
342
  fi
343
+ while IFS= read -r policy_file; do
344
+ [ -f "$stage/$policy_file.orig" ] || continue
345
+ cp "$stage/$policy_file.orig" "$root/$policy_file"
346
+ done < <(publisher_ignore_file_names)
341
347
  rm -rf "$stage"
342
348
  echo "rolled back — the vendored setup is untouched" >&2
343
349
  }
@@ -350,24 +356,19 @@ rollback_on_exit() {
350
356
  # plain set -e/EXIT failure rather than only an explicit sync refusal.
351
357
  trap rollback_on_exit EXIT
352
358
  trap 'rollback; exit 130' INT TERM
353
- if [ ! -f "$root/.gitignore" ] || ! grep -q '^\.intelligence/' "$root/.gitignore"; then
354
- {
355
- [ -f "$root/.gitignore" ] && [ -n "$(tail -c 1 "$root/.gitignore" 2>/dev/null)" ] && echo ""
356
- echo "# intelligence CLI package store (restored automatically by 'intelligence sync')"
357
- echo ".intelligence/"
358
- } >> "$root/.gitignore"
359
- fi
360
359
  mv "$stage/.intelligence" "$root/.intelligence"
361
360
  cp "$manifest_stage" "$root/intelligence.yaml"
362
361
  lock_write_from_tsv "$root/intelligence.lock" "$lock_rows"
362
+ ensure_manifest_gitignore "$root" "$root/intelligence.yaml"
363
+ ensure_manifest_publisher_ignores "$root" "$root/intelligence.yaml"
363
364
 
364
365
  sync_out="$(cd "$root" && IS_SUPPRESS_CLI_NOTE=1 bash "$CLI_DIR/commands/sync.sh" 2>&1)" || {
365
366
  echo "$sync_out"
366
- die "sync of the migrated state failed"
367
+ die "sync of the converted state failed"
367
368
  }
368
369
  echo "$sync_out" | grep -q '^IS_STATUS=ok' || {
369
370
  echo "$sync_out"
370
- die "sync of the migrated state did not report ok"
371
+ die "sync of the converted state did not report ok"
371
372
  }
372
373
  # Point of no return: the new state is verified. From here an interrupt must
373
374
  # NOT roll back (the old setup is being removed) — only clean the stage.
@@ -386,7 +387,7 @@ while IFS="$LOCK_SEP" read -r pack name url ref mirror; do
386
387
  esac
387
388
  if [ -d "$root/$mirror" ]; then
388
389
  rm -rf "${root:?}/$mirror"
389
- # An emptied v1 mirror parent (for example intelligence/external/) goes with it.
390
+ # An emptied legacy mirror parent (for example intelligence/external/) goes with it.
390
391
  rmdir "$(dirname "$root/$mirror")" 2>/dev/null || true
391
392
  fi
392
393
  done < "$pack_rows"
@@ -397,6 +398,7 @@ rm -f "$config"
397
398
  rmdir "$umbrella" 2>/dev/null || true
398
399
 
399
400
  echo ""
400
- echo "migrated. Review the diff, then commit. The old config.yaml is kept at .intelligence/backup/config.yaml."
401
+ echo "converted. Review the diff, then commit. The old config.yaml is kept at .intelligence/backup/config.yaml."
401
402
  echo "From now on: intelligence sync | package | update | status."
402
- echo "Next: ask your agent to run /intelligence-learn-from-repository to review the migrated project context."
403
+ report_tracked_managed_ignores "$root" "$root/intelligence.yaml"
404
+ echo "Next: ask your agent to run /intelligence-learn-from-repository to verify setup and review the converted project context."
@@ -23,7 +23,7 @@ while [ $# -gt 0 ]; do
23
23
  shift || true
24
24
  done
25
25
 
26
- require_v2
26
+ require_cli_project
27
27
  manifest="$IP_ROOT/intelligence.yaml"
28
28
  lock="$IP_ROOT/intelligence.lock"
29
29
 
@@ -117,6 +117,7 @@ if [ -z "$ref" ]; then
117
117
  die "'$name' has no stable version tags at $url — a registry package must be versioned; use github:/git+ with @<ref> to pin a branch or commit"
118
118
  fi
119
119
  echo " NOTE: no version tags at $url — pinning the default branch head" >&2
120
+ ref="HEAD"
120
121
  fi
121
122
  fi
122
123
 
@@ -125,22 +126,16 @@ sha="$(fetch_package "$url" "${ref:-$resolved_tag}" "$path" "$IP_ROOT/$rel")"
125
126
 
126
127
  wire_package_sources "$manifest" "$name" "$rel" "$IP_ROOT"
127
128
 
128
- # Manifest entry: a registry package records its range; a direct one records
129
- # its source so resolution never has to guess it again. The entry is replaced
130
- # WHOLE — re-adding under a different spec must not leave the old spec's
131
- # fields behind (a stale ref: would freeze update, a stale url: would pin the
132
- # old source).
129
+ # The manifest records requested intent only. The resolved source URL/path,
130
+ # tag/ref and SHA live in the required lock. Re-adding a package is the
131
+ # explicit operation for changing its trusted source.
133
132
  qmap_delete_key "$manifest" "packages" "$name"
134
- if [ "$mode" = "registry" ]; then
133
+ if [ -n "$ref" ]; then
134
+ qmap_set "$manifest" "packages" "$name" "ref" "$ref"
135
+ elif [ -n "$requested" ]; then
135
136
  qmap_set "$manifest" "packages" "$name" "version" "$requested"
136
137
  else
137
- qmap_set "$manifest" "packages" "$name" "url" "$url"
138
- [ -n "$path" ] && qmap_set "$manifest" "packages" "$name" "path" "$path"
139
- if [ -n "$ref" ]; then
140
- qmap_set "$manifest" "packages" "$name" "ref" "$ref"
141
- elif [ -n "$requested" ]; then
142
- qmap_set "$manifest" "packages" "$name" "version" "$requested"
143
- fi
138
+ die "internal: package '$name' has neither a requested version nor ref"
144
139
  fi
145
140
 
146
141
  lock_upsert "$lock" "$name" "$requested" "$url" "$path" "${ref:-$resolved_tag}" "$sha"
@@ -3,7 +3,7 @@
3
3
  set -euo pipefail
4
4
  source "$CLI_DIR/lib/cli-common.sh"
5
5
 
6
- require_v2
6
+ require_cli_project
7
7
  manifest="$IP_ROOT/intelligence.yaml"
8
8
  lock="$IP_ROOT/intelligence.lock"
9
9
 
@@ -15,7 +15,7 @@ for a in "$@"; do
15
15
  esac
16
16
  done
17
17
 
18
- require_v2
18
+ require_cli_project
19
19
 
20
20
  # Removing the engine-content package guts the outputs (meta-skills, the
21
21
  # authoring rule, both engine agents disappear) while everything still
@@ -12,7 +12,7 @@ term="${1:-}"
12
12
 
13
13
  detect_project
14
14
  manifest=""
15
- [ "$IP_MODE" = "v2" ] && manifest="$IP_ROOT/intelligence.yaml"
15
+ [ "$IP_MODE" = "cli" ] && manifest="$IP_ROOT/intelligence.yaml"
16
16
 
17
17
  # State of one package name in this project: installed / declared / available.
18
18
  pkg_state() {
@@ -1,9 +1,9 @@
1
1
  #!/bin/bash
2
2
  # Internal package range update operation.
3
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.
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
7
  set -euo pipefail
8
8
  source "$CLI_DIR/lib/cli-common.sh"
9
9
 
@@ -18,7 +18,7 @@ while [ $# -gt 0 ]; do
18
18
  shift || true
19
19
  done
20
20
 
21
- require_v2
21
+ require_cli_project
22
22
  manifest="$IP_ROOT/intelligence.yaml"
23
23
  lock="$IP_ROOT/intelligence.lock"
24
24
 
@@ -35,30 +35,36 @@ while IFS= read -r name; do
35
35
  echo " $name: engine content follows the installed CLI — skipped"
36
36
  continue
37
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"
38
43
  ref="$(qmap_field "$manifest" "packages" "$name" "ref")"
39
- [ -n "$ref" ] && { echo " $name: pinned to ref '$ref' — skipped"; continue; }
40
44
  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
+ [ -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"
45
55
  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"
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
51
66
  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
67
+ if [ "$tag" = "$current" ]; then
62
68
  echo " $name: ${current:-<none>} (up to date)"
63
69
  continue
64
70
  fi
@@ -80,7 +86,7 @@ while IFS= read -r name; do
80
86
  mkdir -p "$(dirname "$IP_ROOT/$rel")"
81
87
  mv "$staging" "$IP_ROOT/$rel"
82
88
  wire_package_sources "$manifest" "$name" "$rel" "$IP_ROOT"
83
- lock_upsert "$lock" "$name" "$range" "$url" "$path" "$tag" "$sha"
89
+ lock_upsert "$lock" "$name" "$requested" "$url" "$path" "$tag" "$sha"
84
90
  echo " $name: ${current:-<none>} -> $tag"
85
91
  moved=$((moved + 1))
86
92
  done < <(qmap_keys "$manifest" "packages")
@@ -18,12 +18,12 @@ while [ $# -gt 0 ]; do
18
18
  shift || true
19
19
  done
20
20
 
21
- require_v2
21
+ require_cli_project
22
22
  manifest="$IP_ROOT/intelligence.yaml"
23
23
  lock="$IP_ROOT/intelligence.lock"
24
24
 
25
25
  # --frozen is the reproducibility contract: the lock must agree with the
26
- # manifest on the package SET and on every requested-source field before a
26
+ # manifest on the package SET and on every requested-intent field before a
27
27
  # single byte is restored. Anything less lets a manifest edit ride on a stale
28
28
  # lock — the exact drift the flag exists to refuse.
29
29
  if [ "$frozen" -eq 1 ]; then
@@ -33,20 +33,12 @@ if [ "$frozen" -eq 1 ]; then
33
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
- m_url="$(qmap_field "$manifest" "packages" "$name" "url")"
37
- m_path="$(qmap_field "$manifest" "packages" "$name" "path")"
38
36
  l_req="$(qmap_field "$lock" "packages" "$name" "requested")"
39
37
  l_res="$(qmap_field "$lock" "packages" "$name" "resolved")"
40
- l_url="$(qmap_field "$lock" "packages" "$name" "url")"
41
- l_path="$(qmap_field "$lock" "packages" "$name" "path")"
42
38
  [ -z "$m_ver" ] || [ "$m_ver" = "$l_req" ] \
43
39
  || die "locked restore: $name requests '$m_ver' but the lock recorded '$l_req' — reconcile the manifest and lock"
44
40
  [ -z "$m_ref" ] || [ "$m_ref" = "$l_res" ] \
45
41
  || die "--frozen: $name pins ref '$m_ref' but the lock resolved '$l_res'"
46
- [ -z "$m_url" ] || [ "$m_url" = "$l_url" ] \
47
- || die "--frozen: $name declares url '$m_url' but the lock recorded '$l_url'"
48
- [ -z "$m_path" ] || [ "$m_path" = "$l_path" ] \
49
- || die "--frozen: $name declares path '$m_path' but the lock recorded '$l_path'"
50
42
  done < <(qmap_keys "$manifest" "packages")
51
43
  while IFS= read -r name; do
52
44
  [ -n "$name" ] || continue
@@ -76,14 +68,11 @@ if [ -n "$missing" ]; then
76
68
  sync_pkg_install "$IP_ROOT"
77
69
  continue
78
70
  fi
79
- url="$(qmap_field "$manifest" "packages" "$name" "url")"
80
- path="$(qmap_field "$manifest" "packages" "$name" "path")"
81
71
  ref="$(qmap_field "$manifest" "packages" "$name" "ref")"
82
72
  range="$(qmap_field "$manifest" "packages" "$name" "version")"
83
- if [ -z "$url" ]; then
84
- resolve_package_source "$manifest" "$name"
85
- url="$RES_URL"; path="$RES_PATH"
86
- fi
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"
87
76
  resolved_tag=""
88
77
  if [ -z "$ref" ]; then
89
78
  picked="$(list_remote_versions "$url" | semver_pick_highest "$range")"
@@ -14,7 +14,7 @@ case "$action" in enable|disable) ;; *) die "internal target state: expected <en
14
14
  [ -n "$name" ] || die "internal target state: missing adapter name"
15
15
  assert_valid_target_name "$name"
16
16
 
17
- require_v2
17
+ require_cli_project
18
18
  manifest="$IP_ROOT/intelligence.yaml"
19
19
  content_dir="$(manifest_intelligence_dir "$manifest")"
20
20
 
@@ -25,31 +25,39 @@ if [ "$action" = "enable" ]; then
25
25
  die "adapter '$name' not found — create it first: intelligence adapter create $name"
26
26
  fi
27
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
28
  output="$(get_target_output "$manifest" "$name")"
39
29
  [ -n "$output" ] || output="$(default_target_output "$name")"
30
+ validate_adapter_contract_for "$IP_ROOT" "$content_dir" "$name" "$output" \
31
+ || die "adapter '$name' has an invalid ownership contract"
32
+ while IFS= read -r required; do
33
+ [ -n "$required" ] || continue
34
+ if [ "$(is_target_enabled "$manifest" "$required")" != "1" ]; then
35
+ die "target '$name' requires target '$required' — enable it first"
36
+ fi
37
+ done < <(adapter_records_for "$IP_ROOT" "$content_dir" "$name" "$output" \
38
+ | awk -F '\t' '$1 == "requires" { print $2 }')
40
39
  target_set_enabled "$manifest" "$name" true "$output"
40
+ ensure_target_gitignore "$IP_ROOT" "$manifest" "$name"
41
+ ensure_manifest_publisher_ignores "$IP_ROOT" "$manifest"
41
42
  echo "enabled: $name (output: $output)"
42
43
  exit 0
43
44
  fi
44
45
 
45
46
  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
47
+ while IFS= read -r dependent; do
48
+ [ -n "$dependent" ] || continue
49
+ [ "$dependent" = "$name" ] && continue
50
+ [ "$(is_target_enabled "$manifest" "$dependent")" = "1" ] || continue
51
+ dependent_output="$(get_target_output "$manifest" "$dependent")"
52
+ [ -n "$dependent_output" ] || dependent_output="$(default_target_output "$dependent")"
53
+ dependent_records="$(adapter_records_for "$IP_ROOT" "$content_dir" "$dependent" "$dependent_output")" \
54
+ || die "adapter '$dependent' has an invalid ownership contract"
55
+ if awk -F '\t' -v required="$name" \
56
+ '$1 == "requires" && $2 == required { found=1 } END { exit(found ? 0 : 1) }' \
57
+ <<< "$dependent_records"; then
58
+ die "target '$name' is required by enabled target '$dependent' — disable '$dependent' first"
59
+ fi
60
+ done < <(target_names "$manifest")
53
61
  output="$(get_target_output "$manifest" "$name")"
54
62
  [ -n "$output" ] || output="$(default_target_output "$name")"
55
63
  target_set_enabled "$manifest" "$name" false "$output"
@@ -0,0 +1,33 @@
1
+ #!/bin/bash
2
+ # CLI consumers of the engine's declarative adapter contract.
3
+
4
+ adapter_file_for() {
5
+ local root="$1" content_dir="$2" name="$3"
6
+ if [ -f "$root/$content_dir/adapters/$name.sh" ]; then
7
+ printf '%s\n' "$root/$content_dir/adapters/$name.sh"
8
+ elif [ -f "$IS_ENGINE_DIR/adapters/$name.sh" ]; then
9
+ printf '%s\n' "$IS_ENGINE_DIR/adapters/$name.sh"
10
+ else
11
+ return 1
12
+ fi
13
+ }
14
+
15
+ adapter_records_for() {
16
+ local root="$1" content_dir="$2" name="$3" output="$4" file
17
+ file="$(adapter_file_for "$root" "$content_dir" "$name")" || {
18
+ echo "ERROR: adapter '$name' not found" >&2
19
+ return 1
20
+ }
21
+ adapter_contract_records "$name" "$file" "$output"
22
+ }
23
+
24
+ validate_adapter_contract_for() {
25
+ local root="$1" content_dir="$2" name="$3" output="$4"
26
+ adapter_records_for "$root" "$content_dir" "$name" "$output" >/dev/null
27
+ }
28
+
29
+ adapter_contract_paths() {
30
+ local root="$1" content_dir="$2" name="$3" output="$4"
31
+ adapter_records_for "$root" "$content_dir" "$name" "$output" \
32
+ | awk -F '\t' '$1 == "owned" || $1 == "managed" || $1 == "legacy" || $1 == "preserve" { print $2 }'
33
+ }