@ainova-systems/intelligence 0.11.0-rc.9 → 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 (46) hide show
  1. package/cli/commands/adapter.sh +46 -6
  2. package/cli/commands/init.sh +138 -33
  3. package/cli/commands/package.sh +2 -2
  4. package/cli/commands/registry.sh +3 -3
  5. package/cli/commands/status.sh +4 -4
  6. package/cli/commands/sync.sh +76 -23
  7. package/cli/commands/update.sh +1 -1
  8. package/cli/intelligence +18 -3
  9. package/cli/internal/{upgrade-v2.sh → align-project.sh} +9 -8
  10. package/cli/internal/check.sh +42 -2
  11. package/cli/internal/{migrate-v1.sh → convert-legacy.sh} +33 -29
  12. package/cli/internal/package-add.sh +1 -1
  13. package/cli/internal/package-list.sh +1 -1
  14. package/cli/internal/package-remove.sh +1 -1
  15. package/cli/internal/package-search.sh +1 -1
  16. package/cli/internal/package-update.sh +1 -1
  17. package/cli/internal/restore.sh +1 -1
  18. package/cli/internal/target-state.sh +26 -18
  19. package/cli/lib/adapter-lifecycle.sh +33 -0
  20. package/cli/lib/cli-common.sh +12 -8
  21. package/cli/lib/gitignore.sh +174 -0
  22. package/cli/lib/manifest.sh +35 -1
  23. package/cli/lib/onboarding.sh +63 -0
  24. package/engine/ENGINE_SHA +1 -1
  25. package/engine/adapters/_template.sh +19 -2
  26. package/engine/adapters/agents.sh +14 -3
  27. package/engine/adapters/claude.sh +15 -0
  28. package/engine/adapters/codex.sh +11 -1
  29. package/engine/adapters/copilot.sh +11 -0
  30. package/engine/adapters/cursor.sh +15 -0
  31. package/engine/adapters/opencode.sh +11 -0
  32. package/engine/adapters/pi.sh +18 -5
  33. package/engine/lib/adapter-contract.sh +100 -0
  34. package/engine/lib/common.sh +5 -0
  35. package/engine/lib/contract.sh +2 -2
  36. package/engine/sync.sh +87 -25
  37. package/package.json +1 -1
  38. package/packages/sync/agents/intelligence-architect.md +2 -2
  39. package/packages/sync/references/adapters.md +51 -7
  40. package/packages/sync/references/conventions.md +55 -20
  41. package/packages/sync/references/onboarding-migration.md +79 -0
  42. package/packages/sync/skills/intelligence-install-adapter/SKILL.md +12 -5
  43. package/packages/sync/skills/intelligence-learn-from-context/SKILL.md +23 -4
  44. package/packages/sync/skills/intelligence-learn-from-repository/SKILL.md +90 -38
  45. package/packages/sync/skills/intelligence-review-skills/SKILL.md +1 -1
  46. 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
@@ -86,6 +86,46 @@ while IFS= read -r name; do
86
86
  fi
87
87
  done < <(qmap_keys "$manifest" "packages")
88
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
+
89
129
  # Lock orphans.
90
130
  if [ -f "$lock" ]; then
91
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"
@@ -325,6 +325,10 @@ if [ -f "$root/.gitignore" ]; then
325
325
  gitignore_existed=1
326
326
  cp "$root/.gitignore" "$stage/gitignore.orig"
327
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)
328
332
  commit_active=1
329
333
  rollback() {
330
334
  [ "$commit_active" -eq 1 ] || return 0
@@ -336,6 +340,10 @@ rollback() {
336
340
  else
337
341
  rm -f "$root/.gitignore"
338
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)
339
347
  rm -rf "$stage"
340
348
  echo "rolled back — the vendored setup is untouched" >&2
341
349
  }
@@ -348,24 +356,19 @@ rollback_on_exit() {
348
356
  # plain set -e/EXIT failure rather than only an explicit sync refusal.
349
357
  trap rollback_on_exit EXIT
350
358
  trap 'rollback; exit 130' INT TERM
351
- if [ ! -f "$root/.gitignore" ] || ! grep -q '^\.intelligence/' "$root/.gitignore"; then
352
- {
353
- [ -f "$root/.gitignore" ] && [ -n "$(tail -c 1 "$root/.gitignore" 2>/dev/null)" ] && echo ""
354
- echo "# intelligence CLI package store (restored automatically by 'intelligence sync')"
355
- echo ".intelligence/"
356
- } >> "$root/.gitignore"
357
- fi
358
359
  mv "$stage/.intelligence" "$root/.intelligence"
359
360
  cp "$manifest_stage" "$root/intelligence.yaml"
360
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"
361
364
 
362
365
  sync_out="$(cd "$root" && IS_SUPPRESS_CLI_NOTE=1 bash "$CLI_DIR/commands/sync.sh" 2>&1)" || {
363
366
  echo "$sync_out"
364
- die "sync of the migrated state failed"
367
+ die "sync of the converted state failed"
365
368
  }
366
369
  echo "$sync_out" | grep -q '^IS_STATUS=ok' || {
367
370
  echo "$sync_out"
368
- die "sync of the migrated state did not report ok"
371
+ die "sync of the converted state did not report ok"
369
372
  }
370
373
  # Point of no return: the new state is verified. From here an interrupt must
371
374
  # NOT roll back (the old setup is being removed) — only clean the stage.
@@ -384,7 +387,7 @@ while IFS="$LOCK_SEP" read -r pack name url ref mirror; do
384
387
  esac
385
388
  if [ -d "$root/$mirror" ]; then
386
389
  rm -rf "${root:?}/$mirror"
387
- # 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.
388
391
  rmdir "$(dirname "$root/$mirror")" 2>/dev/null || true
389
392
  fi
390
393
  done < "$pack_rows"
@@ -395,6 +398,7 @@ rm -f "$config"
395
398
  rmdir "$umbrella" 2>/dev/null || true
396
399
 
397
400
  echo ""
398
- 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."
399
402
  echo "From now on: intelligence sync | package | update | status."
400
- 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
 
@@ -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() {
@@ -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
 
@@ -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
 
@@ -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
+ }
@@ -11,6 +11,7 @@
11
11
  # IS_ENGINE_DIR come exported from the dispatcher.
12
12
  source "$IS_ENGINE_DIR/lib/common.sh"
13
13
  source "$IS_ENGINE_DIR/lib/contract.sh"
14
+ source "$IS_ENGINE_DIR/lib/adapter-contract.sh"
14
15
 
15
16
  die() {
16
17
  echo "ERROR: $*" >&2
@@ -50,6 +51,9 @@ source "$CLI_DIR/lib/manifest.sh"
50
51
  source "$CLI_DIR/lib/semver.sh"
51
52
  source "$CLI_DIR/lib/registry.sh"
52
53
  source "$CLI_DIR/lib/lockfile.sh"
54
+ source "$CLI_DIR/lib/adapter-lifecycle.sh"
55
+ source "$CLI_DIR/lib/gitignore.sh"
56
+ source "$CLI_DIR/lib/onboarding.sh"
53
57
 
54
58
  # The engine-content package: OPTIONAL but auto-selected at init. Package by
55
59
  # UX (manifest entry, lockfile row, list/search/remove), bundle by mechanics —
@@ -70,11 +74,11 @@ DEFAULT_REGISTRY_URL="$(top_scalar "$_EPKG" "default_registry")"
70
74
  SYNC_PKG_STORE=".intelligence/packages/$SYNC_PKG_NAME"
71
75
 
72
76
  # --- Project detection ---------------------------------------------------
73
- # Sets: IP_MODE (v2|legacy|none), IP_ROOT, IP_UMBRELLA, IP_MODULE_DIR.
77
+ # Sets: IP_MODE (cli|legacy|none), IP_ROOT, IP_UMBRELLA, IP_MODULE_DIR.
74
78
  # These ARE this lib's public API — every command reads them after calling
75
79
  # detect_project, which per-file shellcheck cannot see.
76
80
  # shellcheck disable=SC2034
77
- # v2 wins: the nearest ancestor holding intelligence.yaml. Legacy is a git
81
+ # The CLI product wins: the nearest ancestor holding intelligence.yaml. Legacy is a git
78
82
  # repo holding an umbrella (a dir with config.yaml) whose module is identified
79
83
  # by role — scripts/sync.sh + scripts/VERSION — never by name.
80
84
  detect_project() {
@@ -82,7 +86,7 @@ detect_project() {
82
86
  local dir="$PWD"
83
87
  while :; do
84
88
  if [ -f "$dir/intelligence.yaml" ]; then
85
- IP_MODE="v2"
89
+ IP_MODE="cli"
86
90
  IP_ROOT="$(cd "$dir" && pwd)"
87
91
  return 0
88
92
  fi
@@ -109,11 +113,11 @@ detect_project() {
109
113
  return 0
110
114
  }
111
115
 
112
- require_v2() {
116
+ require_cli_project() {
113
117
  detect_project
114
118
  case "$IP_MODE" in
115
- v2) ;;
116
- legacy) die "this is a vendored (v1) setup — run 'intelligence init' to migrate it, or keep using its own flow" ;;
119
+ cli) ;;
120
+ legacy) die "this is a legacy Intelligence Sync project — run 'intelligence init' to convert it, or keep using its own flow" ;;
117
121
  *) die "no intelligence project found here — run 'intelligence init'" ;;
118
122
  esac
119
123
  }
@@ -176,7 +180,7 @@ export_engine_env() {
176
180
 
177
181
  # --- Project lifecycle preflight -----------------------------------------
178
182
  # Public commands are intentionally few. They share this state gate so a CLI
179
- # installed at a newer engine version brings the current v2 project forward
183
+ # installed at a newer engine version brings the current Intelligence project forward
180
184
  # before a mutating operation. The npm install itself cannot do that: it runs
181
185
  # outside any project and does not know which repositories the user owns.
182
186
 
@@ -268,7 +272,7 @@ ensure_project_current() {
268
272
  die "project lifecycle requires alignment (stamp ${stamp:-unstamped}, engine $eng) — run 'intelligence init --apply' locally, review and commit the diff"
269
273
  fi
270
274
  echo " project alignment: stamp ${stamp:-unstamped}, engine $eng"
271
- bash "$CLI_DIR/internal/upgrade-v2.sh" --no-sync
275
+ bash "$CLI_DIR/internal/align-project.sh" --no-sync
272
276
  }
273
277
 
274
278
  project_store_missing() {
@@ -0,0 +1,174 @@
1
+ #!/bin/bash
2
+ # Generated-output policy derived from each adapter's ownership contract.
3
+
4
+ gitignore_add_line() {
5
+ local root="$1" line="$2" file="$1/.gitignore"
6
+ if [ -f "$file" ] && grep -Fqx -- "$line" "$file"; then
7
+ return 0
8
+ fi
9
+ printf '%s\n' "$line" >> "$file"
10
+ }
11
+
12
+ ensure_gitignore_header() {
13
+ local root="$1" file="$1/.gitignore"
14
+ if [ ! -f "$file" ] || ! grep -Fqx '# Intelligence generated state and tool output' "$file"; then
15
+ if [ -f "$file" ] && [ -s "$file" ] && [ -n "$(tail -c 1 "$file" 2>/dev/null)" ]; then
16
+ printf '\n' >> "$file"
17
+ fi
18
+ printf '%s\n' '# Intelligence generated state and tool output' >> "$file"
19
+ fi
20
+ }
21
+
22
+ ensure_base_gitignore() {
23
+ local root="$1"
24
+ ensure_gitignore_header "$root"
25
+ gitignore_add_line "$root" '.intelligence/'
26
+ }
27
+
28
+ ensure_target_gitignore() {
29
+ local root="$1" manifest="$2" target="$3" content_dir output kind value records
30
+ ensure_gitignore_header "$root"
31
+ content_dir="$(manifest_intelligence_dir "$manifest")"
32
+ output="$(get_target_output "$manifest" "$target")"
33
+ [ -n "$output" ] || output="$(default_target_output "$target")"
34
+ records="$(adapter_records_for "$root" "$content_dir" "$target" "$output")" \
35
+ || die "adapter '$target' has an invalid ownership contract"
36
+ while IFS=$'\t' read -r kind value; do
37
+ case "$kind" in
38
+ ignore) gitignore_add_line "$root" "$value" ;;
39
+ include) gitignore_add_line "$root" "!$value" ;;
40
+ esac
41
+ done <<< "$records"
42
+ }
43
+
44
+ ensure_manifest_gitignore() {
45
+ local root="$1" manifest="$2" target
46
+ ensure_base_gitignore "$root"
47
+ while IFS= read -r target; do
48
+ [ -n "$target" ] || continue
49
+ if [ "$(is_target_enabled "$manifest" "$target")" = "1" ]; then
50
+ ensure_target_gitignore "$root" "$manifest" "$target"
51
+ fi
52
+ done < <(target_names "$manifest")
53
+ }
54
+
55
+ # Publishing/build tools do not share Git's ignore policy. In particular,
56
+ # .vscodeignore and .npmignore become the packager's own filter, while Docker
57
+ # always builds from its independent context filter. Only amend files the
58
+ # project already has: their presence is the explicit signal that this root is
59
+ # packaged or sent as a build context.
60
+ publisher_ignore_file_names() {
61
+ printf '%s\n' .vscodeignore .npmignore .dockerignore
62
+ }
63
+
64
+ publisher_ignore_add_line() {
65
+ local file="$1" line="$2"
66
+ if grep -Fqx -- "$line" "$file" 2>/dev/null; then
67
+ return 0
68
+ fi
69
+ printf '%s\n' "$line" >> "$file"
70
+ PUBLISH_IGNORE_FILE_CHANGED=1
71
+ }
72
+
73
+ publisher_ignore_add_path() {
74
+ local file="$1" path="${2#./}"
75
+ path="${path%/}"
76
+ [ -n "$path" ] || return 0
77
+ publisher_ignore_add_line "$file" "$path"
78
+ publisher_ignore_add_line "$file" "$path/**"
79
+ }
80
+
81
+ ensure_manifest_publisher_ignores() {
82
+ local root="$1" manifest="$2" rel file content_dir target output records kind value
83
+ content_dir="$(manifest_intelligence_dir "$manifest")"
84
+ while IFS= read -r rel; do
85
+ [ -n "$rel" ] || continue
86
+ file="$root/$rel"
87
+ [ -f "$file" ] || continue
88
+ PUBLISH_IGNORE_FILE_CHANGED=0
89
+ if ! grep -Fqx '# Intelligence development context and generated output' "$file" 2>/dev/null; then
90
+ if [ -s "$file" ] && [ -n "$(tail -c 1 "$file" 2>/dev/null)" ]; then
91
+ printf '\n' >> "$file"
92
+ fi
93
+ printf '%s\n' '# Intelligence development context and generated output' >> "$file"
94
+ PUBLISH_IGNORE_FILE_CHANGED=1
95
+ fi
96
+ publisher_ignore_add_path "$file" '.intelligence'
97
+ publisher_ignore_add_line "$file" 'intelligence.yaml'
98
+ publisher_ignore_add_line "$file" 'intelligence.lock'
99
+ publisher_ignore_add_path "$file" "$content_dir"
100
+ while IFS= read -r target; do
101
+ [ -n "$target" ] || continue
102
+ [ "$(is_target_enabled "$manifest" "$target")" = "1" ] || continue
103
+ output="$(get_target_output "$manifest" "$target")"
104
+ [ -n "$output" ] || output="$(default_target_output "$target")"
105
+ publisher_ignore_add_path "$file" "$output"
106
+ records="$(adapter_records_for "$root" "$content_dir" "$target" "$output")" \
107
+ || die "adapter '$target' has an invalid ownership contract"
108
+ while IFS=$'\t' read -r kind value; do
109
+ case "$kind" in
110
+ owned|managed|legacy) publisher_ignore_add_path "$file" "$value" ;;
111
+ esac
112
+ done <<< "$records"
113
+ done < <(target_names "$manifest")
114
+ if [ "$PUBLISH_IGNORE_FILE_CHANGED" -eq 1 ]; then
115
+ echo "packaging exclusions updated: $rel"
116
+ fi
117
+ done < <(publisher_ignore_file_names)
118
+ }
119
+
120
+ managed_gitignore_patterns() {
121
+ local root="$1" manifest="$2" content_dir target output records kind value
122
+ content_dir="$(manifest_intelligence_dir "$manifest")"
123
+ printf '%s\n' '.intelligence/' "$content_dir/_backup/"
124
+ while IFS= read -r target; do
125
+ [ -n "$target" ] || continue
126
+ [ "$(is_target_enabled "$manifest" "$target")" = "1" ] || continue
127
+ output="$(get_target_output "$manifest" "$target")"
128
+ [ -n "$output" ] || output="$(default_target_output "$target")"
129
+ records="$(adapter_records_for "$root" "$content_dir" "$target" "$output")" \
130
+ || return 1
131
+ while IFS=$'\t' read -r kind value; do
132
+ case "$kind" in
133
+ ignore) printf '%s\n' "$value" ;;
134
+ include) printf '!%s\n' "$value" ;;
135
+ esac
136
+ done <<< "$records"
137
+ done < <(target_names "$manifest")
138
+ }
139
+
140
+ # Git does not apply ignore rules retroactively to files already in its index.
141
+ # Name each affected path and print a command which only untracks it; the local
142
+ # file remains available as migration input.
143
+ report_tracked_managed_ignores() {
144
+ local root="$1" manifest="$2" patterns tracked path quoted posix_quoted powershell_quoted
145
+ git -C "$root" rev-parse --is-inside-work-tree >/dev/null 2>&1 || return 0
146
+ patterns="$(mktemp -t intelligence-gitignore-XXXXXX)"
147
+ tracked="$(mktemp -t intelligence-tracked-XXXXXX)"
148
+ managed_gitignore_patterns "$root" "$manifest" > "$patterns" || {
149
+ rm -f "$patterns" "$tracked"
150
+ return 0
151
+ }
152
+ git -C "$root" ls-files -z -ci --exclude-from="$patterns" > "$tracked" 2>/dev/null || true
153
+ rm -f "$patterns"
154
+ [ -s "$tracked" ] || {
155
+ rm -f "$tracked"
156
+ return 0
157
+ }
158
+ echo " Tracked files still bypass these .gitignore rules. Untrack them without deleting local copies:"
159
+ while IFS= read -r -d '' path; do
160
+ [ -n "$path" ] || continue
161
+ if [[ "$path" == *"'"* ]]; then
162
+ # macOS still ships Bash 3.2; use portable sed rather than newer
163
+ # parameter-replacement behavior for the embedded quote.
164
+ posix_quoted="$(printf '%s' "$path" | sed "s/'/'\\\\''/g")"
165
+ powershell_quoted="$(printf '%s' "$path" | sed "s/'/''/g")"
166
+ printf " POSIX: git rm --cached -- '%s'\n" "$posix_quoted"
167
+ printf " PowerShell: git rm --cached -- '%s'\n" "$powershell_quoted"
168
+ else
169
+ quoted="'$path'"
170
+ printf ' git rm --cached -- %s\n' "$quoted"
171
+ fi
172
+ done < "$tracked"
173
+ rm -f "$tracked"
174
+ }