@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
@@ -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
  }
@@ -139,14 +143,12 @@ bundled_engine_version() {
139
143
  }
140
144
 
141
145
  # --- The sync package's manifest/lock plumbing ---------------------------
142
- # sync_pkg_entry <manifest> — write/refresh the package's manifest entry:
143
- # name + exact pin + explicit url/path, so resolution never depends on any
144
- # registry (a project registry shadowing the name cannot brick the engine).
146
+ # sync_pkg_entry <manifest> — write/refresh only the requested exact pin.
147
+ # The built-in source lives in engine-package.yaml and resolved source state
148
+ # belongs exclusively to intelligence.lock.
145
149
  sync_pkg_entry() {
146
150
  local manifest="$1"
147
151
  qmap_set "$manifest" "packages" "$SYNC_PKG_NAME" "version" "$(bundled_engine_version)"
148
- qmap_set "$manifest" "packages" "$SYNC_PKG_NAME" "url" "$SYNC_PKG_URL"
149
- qmap_set "$manifest" "packages" "$SYNC_PKG_NAME" "path" "$SYNC_PKG_PATH"
150
152
  }
151
153
 
152
154
  # sync_pkg_install <root> — materialize the package into the store and lock
@@ -178,7 +180,7 @@ export_engine_env() {
178
180
 
179
181
  # --- Project lifecycle preflight -----------------------------------------
180
182
  # 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
183
+ # installed at a newer engine version brings the current Intelligence project forward
182
184
  # before a mutating operation. The npm install itself cannot do that: it runs
183
185
  # outside any project and does not know which repositories the user owns.
184
186
 
@@ -227,13 +229,19 @@ project_needs_upgrade() {
227
229
  [ -d "$root/.intelligence/engine" ] && return 0
228
230
 
229
231
  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
232
+ [ -n "$name" ] || continue
233
+ # Early RC manifests mixed requested intent with resolved source
234
+ # details. Source URL/path now live only in the required lockfile.
235
+ [ -n "$(qmap_field "$manifest" "packages" "$name" "url")" ] && return 0
236
+ [ -n "$(qmap_field "$manifest" "packages" "$name" "path")" ] && return 0
237
+ if [ "$name" = "$SYNC_PKG_NAME" ]; then
238
+ pinned="$(qmap_field "$manifest" "packages" "$name" "version")"
239
+ locked="$(qmap_field "$root/intelligence.lock" "packages" "$name" "resolved")"
240
+ [ "$pinned" = "$eng" ] || return 0
241
+ if [ -f "$root/intelligence.lock" ]; then
242
+ [ -n "$locked" ] || return 0
243
+ [ "${locked#v}" = "$eng" ] || return 0
244
+ fi
237
245
  fi
238
246
  done < <(qmap_keys "$manifest" "packages")
239
247
  return 1
@@ -264,7 +272,7 @@ ensure_project_current() {
264
272
  die "project lifecycle requires alignment (stamp ${stamp:-unstamped}, engine $eng) — run 'intelligence init --apply' locally, review and commit the diff"
265
273
  fi
266
274
  echo " project alignment: stamp ${stamp:-unstamped}, engine $eng"
267
- bash "$CLI_DIR/internal/upgrade-v2.sh" --no-sync
275
+ bash "$CLI_DIR/internal/align-project.sh" --no-sync
268
276
  }
269
277
 
270
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
+ }
@@ -60,16 +60,38 @@ target_exists() {
60
60
  ' "$file"
61
61
  }
62
62
 
63
+ # target_names <manifest> — declared target keys in manifest order.
64
+ target_names() {
65
+ local file="$1"
66
+ [ -f "$file" ] || return 0
67
+ awk '
68
+ { sub(/\r$/, "") }
69
+ /^targets:[[:space:]]*$/ { in_targets = 1; next }
70
+ in_targets && /^[A-Za-z]/ { in_targets = 0 }
71
+ in_targets && /^ [a-z][a-z0-9_]*:/ {
72
+ line = $0
73
+ sub(/^ /, "", line)
74
+ sub(/:.*/, "", line)
75
+ print line
76
+ }
77
+ ' "$file"
78
+ }
79
+
63
80
  # target_set_enabled <manifest> <name> <true|false> <default-output>
64
81
  # Transactionally update only `enabled`, preserving every other target field
65
82
  # and comment. A missing target is added in the compact form used by init.
66
83
  target_set_enabled() {
67
84
  local file="$1" target="$2" enabled="$3" output="$4"
85
+ local target_missing=1
68
86
  [ -f "$file" ] || die "no such file: $file"
69
87
  case "$enabled" in true|false) ;; *) die "internal: invalid target state '$enabled'" ;; esac
88
+ target_exists "$file" "$target" && target_missing=0
70
89
  output="$(_yq_esc "$output")"
71
- _qmap_stage "$file" -v target="$target" -v enabled="$enabled" -v output="$output" '
90
+ _qmap_stage "$file" -v target="$target" -v enabled="$enabled" -v output="$output" -v target_missing="$target_missing" '
72
91
  function entry() { return " " target ": { enabled: " enabled ", output: \"" output "\" }" }
92
+ function insert_missing_target() {
93
+ if (in_targets && target_missing && !target_seen) { print entry(); target_seen = 1 }
94
+ }
73
95
  function add_enabled_to_inline(line, p) {
74
96
  p = index(line, "{")
75
97
  if (p > 0) return substr(line, 1, p) " enabled: " enabled "," substr(line, p + 1)
@@ -86,8 +108,20 @@ target_set_enabled() {
86
108
  }
87
109
  { sub(/\r$/, "") }
88
110
  /^targets:[[:space:]]*$/ { targets_seen = 1; in_targets = 1; print; next }
111
+ in_targets && target_missing && target_entry_seen && /^#/ {
112
+ flush_target()
113
+ insert_missing_target()
114
+ print
115
+ next
116
+ }
117
+ in_targets && target_missing && target_entry_seen && !in_target && /^[[:space:]]*$/ {
118
+ insert_missing_target()
119
+ print
120
+ next
121
+ }
89
122
  in_targets && /^[A-Za-z]/ { flush_targets() }
90
123
  in_targets && /^ [A-Za-z]/ {
124
+ target_entry_seen = 1
91
125
  flush_target()
92
126
  if ($0 ~ "^ " target ":[[:space:]]*") {
93
127
  target_seen = 1
@@ -301,6 +335,29 @@ qmap_delete_key() {
301
335
  '
302
336
  }
303
337
 
338
+ # qmap_delete_field <file> <block> <key> <field> — drop one field from a
339
+ # quoted-key entry without disturbing its other fields or surrounding YAML.
340
+ qmap_delete_field() {
341
+ local file="$1" block="$2" key="$3" field="$4"
342
+ [ -f "$file" ] || return 0
343
+ _qmap_stage "$file" -v block="$block" -v key="$key" -v field="$field" '
344
+ { sub(/\r$/, "") }
345
+ $0 ~ "^" block ":[ \t]*$" { inb = 1; print; next }
346
+ inb && /^[^ #]/ { inb = 0; ink = 0 }
347
+ inb && /^ "/ {
348
+ s = substr($0, 4); q = index(s, "\"")
349
+ ink = (q > 0 && substr(s, 1, q - 1) == key)
350
+ print; next
351
+ }
352
+ inb && ink && /^ [A-Za-z_]/ {
353
+ line = $0; sub(/^ /, "", line)
354
+ c = index(line, ":")
355
+ if (c > 0 && substr(line, 1, c - 1) == field) next
356
+ }
357
+ { print }
358
+ '
359
+ }
360
+
304
361
  # --- registries: a trust LIST, not a scope map -----------------------------
305
362
  # The block holds registry repo URLs in trust order. Two shapes are read so
306
363
  # early manifests keep working: the list form (`- "url"`) and the retired
@@ -0,0 +1,63 @@
1
+ #!/bin/bash
2
+ # Preserve the pre-Intelligence state described by enabled adapter contracts.
3
+ # The snapshot is data-only and explicitly identifies itself as initial
4
+ # onboarding state so recovery skills never mistake it for generated output.
5
+
6
+ onboarding_source_paths() {
7
+ local root="$1" content_dir="$2" targets="$3" target output
8
+ for target in $targets; do
9
+ output="$(default_target_output "$target")"
10
+ adapter_contract_paths "$root" "$content_dir" "$target" "$output"
11
+ done | awk 'NF && !seen[$0]++'
12
+ }
13
+
14
+ onboarding_source_count() {
15
+ local root="$1" content_dir="$2" targets="$3" rel count=0
16
+ while IFS= read -r rel; do
17
+ [ -e "$root/$rel" ] || [ -L "$root/$rel" ] || continue
18
+ count=$((count + 1))
19
+ done < <(onboarding_source_paths "$root" "$content_dir" "$targets")
20
+ printf '%s\n' "$count"
21
+ }
22
+
23
+ preserve_onboarding_sources() {
24
+ local root="$1" content_dir="$2" targets="$3"
25
+ local rel src dest backup_rel backup stage target
26
+ backup_rel="$content_dir/_backup"
27
+ backup="$root/$backup_rel"
28
+ ONBOARDING_BACKUP_COUNT="$(onboarding_source_count "$root" "$content_dir" "$targets")"
29
+ ONBOARDING_BACKUP_REL=""
30
+ export ONBOARDING_BACKUP_COUNT ONBOARDING_BACKUP_REL
31
+ [ "$ONBOARDING_BACKUP_COUNT" -gt 0 ] || return 0
32
+ [ ! -e "$backup" ] || die "onboarding backup already exists at $backup_rel - review or move it before rerunning init"
33
+
34
+ mkdir -p "$root/$content_dir"
35
+ stage="$root/$content_dir/.backup-stage.$$"
36
+ [ ! -e "$stage" ] || die "temporary onboarding backup path already exists"
37
+ mkdir -p "$stage"
38
+
39
+ {
40
+ printf '# Intelligence initial-state backup contract v1\n'
41
+ printf 'state\tinitial-onboarding\n'
42
+ printf 'source\tpre-intelligence\n'
43
+ for target in $targets; do printf 'target\t%s\n' "$target"; done
44
+ } > "$stage/manifest.tsv"
45
+
46
+ while IFS= read -r rel; do
47
+ src="$root/$rel"
48
+ [ -e "$src" ] || [ -L "$src" ] || continue
49
+ dest="$stage/$rel"
50
+ mkdir -p "$(dirname "$dest")"
51
+ if ! cp -R "$src" "$dest"; then
52
+ rm -rf "$stage"
53
+ die "failed to preserve initial AI instruction path '$rel'"
54
+ fi
55
+ printf 'path\t%s\n' "$rel" >> "$stage/manifest.tsv"
56
+ done < <(onboarding_source_paths "$root" "$content_dir" "$targets")
57
+
58
+ mv "$stage" "$backup"
59
+ ensure_gitignore_header "$root"
60
+ gitignore_add_line "$root" "$backup_rel/"
61
+ ONBOARDING_BACKUP_REL="$backup_rel"
62
+ export ONBOARDING_BACKUP_REL
63
+ }
package/engine/ENGINE_SHA CHANGED
@@ -1 +1 @@
1
- f2bfc5e3722bf6fb2f7309db2d248368d5b0cc5e
1
+ 88a9c74ddb8bee338671c7badbebd07669b668c0
@@ -7,8 +7,9 @@
7
7
  # occurrence with your adapter name before sourcing.
8
8
  #
9
9
  # Required:
10
- # 1. Implement sync_to_<name>() and its three content transforms
11
- # 2. Enable it after implementation: intelligence adapter enable <name>
10
+ # 1. Keep adapter_contract_<name>() accurate for every path this file writes
11
+ # 2. Implement sync_to_<name>() and its three content transforms
12
+ # 3. Enable it after implementation: intelligence adapter enable <name>
12
13
  # This adds the target to intelligence.yaml when it is absent:
13
14
  # targets:
14
15
  # <name>: { enabled: true, output: ".<name>" }
@@ -35,6 +36,22 @@
35
36
  # loaded. Keep this template position-independent: after scaffolding it lives
36
37
  # under the project's content directory, not beside engine/lib/.
37
38
 
39
+ # Ownership contract. Paths are repository-relative and derive from the
40
+ # configured target output passed as $1. `owned` paths are exclusively managed;
41
+ # `managed` paths share a directory with another adapter or hand-authored files.
42
+ # Add `legacy` inputs to the initial backup, `preserve` for settings that must
43
+ # never be replaced, and explicit ignore/include records for Git policy.
44
+ adapter_contract_<name>() {
45
+ local output="${1%/}"
46
+ adapter_contract_version 1
47
+ adapter_contract_owned "$output/rules"
48
+ adapter_contract_owned "$output/agents"
49
+ adapter_contract_owned "$output/skills"
50
+ adapter_contract_ignore "$output/rules/"
51
+ adapter_contract_ignore "$output/agents/"
52
+ adapter_contract_ignore "$output/skills/"
53
+ }
54
+
38
55
  # Sync rules for <agent-name>
39
56
  # Typical transformations:
40
57
  # - Copy as-is (like Claude)
@@ -223,6 +223,16 @@ agents_md_append_rules_list() {
223
223
  }
224
224
 
225
225
  # Main entry point for AGENTS.md adapter
226
+ adapter_contract_agents() {
227
+ local output="$1"
228
+ if [[ "$output" == */ ]] || [[ "$output" != *.md ]]; then
229
+ output="${output%/}/AGENTS.md"
230
+ fi
231
+ adapter_contract_version 1
232
+ adapter_contract_owned "$output"
233
+ adapter_contract_legacy "AGENTS.md"
234
+ }
235
+
226
236
  sync_to_agents() {
227
237
  local repo_root="$1"
228
238
  local config_file="$2"
@@ -231,10 +241,11 @@ sync_to_agents() {
231
241
  echo "=== AGENTS.md ==="
232
242
 
233
243
  # output_dir points at the target file path (e.g., /repo/AGENTS.md).
234
- # If it looks like a directory (trailing slash, existing dir, or no .md
235
- # extension), append default filename.
244
+ # If it looks like a directory lexically (trailing slash or no .md
245
+ # extension), append the default filename. The ownership contract uses the
246
+ # same rule, so filesystem state cannot make its write-set ambiguous.
236
247
  local output_file="$output_dir"
237
- if [ -d "$output_file" ] || [[ "$output_file" == */ ]] || [[ "$output_file" != *.md ]]; then
248
+ if [[ "$output_file" == */ ]] || [[ "$output_file" != *.md ]]; then
238
249
  output_file="${output_file%/}/AGENTS.md"
239
250
  fi
240
251
 
@@ -8,6 +8,21 @@
8
8
 
9
9
  source "$(dirname "${BASH_SOURCE[0]}")/../lib/common.sh"
10
10
 
11
+ adapter_contract_claude() {
12
+ local output="${1%/}"
13
+ adapter_contract_version 1
14
+ adapter_contract_owned "$output/rules"
15
+ adapter_contract_owned "$output/agents"
16
+ adapter_contract_owned "$output/skills"
17
+ adapter_contract_legacy "CLAUDE.md"
18
+ adapter_contract_legacy "$output/commands"
19
+ adapter_contract_preserve "$output/settings.json"
20
+ adapter_contract_preserve "$output/settings.local.json"
21
+ adapter_contract_ignore "CLAUDE.md"
22
+ adapter_contract_ignore "$output/*"
23
+ adapter_contract_include "$output/settings.json"
24
+ }
25
+
11
26
  # Sync rules to Claude format (copy as-is, normalize LF)
12
27
  sync_claude_rules() {
13
28
  local repo_root="$1"
@@ -13,6 +13,16 @@
13
13
 
14
14
  source "$(dirname "${BASH_SOURCE[0]}")/../lib/common.sh"
15
15
 
16
+ adapter_contract_codex() {
17
+ local output="${1%/}"
18
+ adapter_contract_version 1
19
+ adapter_contract_requires agents
20
+ adapter_contract_managed ".agents/skills"
21
+ adapter_contract_owned "$output/agents"
22
+ adapter_contract_ignore ".agents/skills/"
23
+ adapter_contract_ignore "$output/agents/"
24
+ }
25
+
16
26
  # Sync skills to Codex format (.agents/skills/{name}/SKILL.md)
17
27
  sync_codex_skills() {
18
28
  local repo_root="$1"
@@ -111,7 +121,7 @@ sync_to_codex() {
111
121
  sync_codex_skills "$repo_root" "$config_file" "$skills_dir"
112
122
 
113
123
  # Agents -> .codex/agents/
114
- local agents_dir="$repo_root/.codex/agents"
124
+ local agents_dir="$output_dir/agents"
115
125
  rm -rf "$agents_dir"
116
126
  mkdir -p "$agents_dir"
117
127
  sync_codex_agents "$repo_root" "$config_file" "$agents_dir"
@@ -14,6 +14,17 @@
14
14
 
15
15
  source "$(dirname "${BASH_SOURCE[0]}")/../lib/common.sh"
16
16
 
17
+ adapter_contract_copilot() {
18
+ local output="${1%/}"
19
+ adapter_contract_version 1
20
+ adapter_contract_requires agents
21
+ adapter_contract_owned "$output/instructions"
22
+ adapter_contract_owned "$output/prompts"
23
+ adapter_contract_owned "$output/agents"
24
+ adapter_contract_owned "$output/skills"
25
+ adapter_contract_legacy "$output/copilot-instructions.md"
26
+ }
27
+
17
28
  # Sync rules to Copilot format. Only path-scoped rules are emitted —
18
29
  # always-on rules live in AGENTS.md (read by Copilot natively).
19
30
  sync_copilot_rules() {
@@ -13,6 +13,21 @@
13
13
 
14
14
  source "$(dirname "${BASH_SOURCE[0]}")/../lib/common.sh"
15
15
 
16
+ adapter_contract_cursor() {
17
+ local output="${1%/}"
18
+ adapter_contract_version 1
19
+ adapter_contract_requires agents
20
+ adapter_contract_owned "$output/rules"
21
+ adapter_contract_owned "$output/agents"
22
+ adapter_contract_owned "$output/skills"
23
+ adapter_contract_legacy ".cursorrules"
24
+ adapter_contract_legacy "$output/commands"
25
+ adapter_contract_preserve "$output/settings.json"
26
+ adapter_contract_ignore ".cursorrules"
27
+ adapter_contract_ignore "$output/*"
28
+ adapter_contract_include "$output/settings.json"
29
+ }
30
+
16
31
  # Sync rules to Cursor format (.md -> .mdc, paths -> globs)
17
32
  sync_cursor_rules() {
18
33
  local repo_root="$1"
@@ -26,6 +26,17 @@
26
26
 
27
27
  source "$(dirname "${BASH_SOURCE[0]}")/../lib/common.sh"
28
28
 
29
+ adapter_contract_opencode() {
30
+ local output="${1%/}"
31
+ adapter_contract_version 1
32
+ adapter_contract_requires agents
33
+ adapter_contract_managed ".agents/skills"
34
+ adapter_contract_owned "$output/agents"
35
+ adapter_contract_managed "$output/commands"
36
+ adapter_contract_ignore ".agents/skills/"
37
+ adapter_contract_ignore "$output/agents/"
38
+ }
39
+
29
40
  # Strip frontmatter, return body only.
30
41
  # Direct call to the shared `strip_frontmatter` helper in lib/common.sh —
31
42
  # duplicating the awk block here would violate the "all parsing in common.sh"