@ainova-systems/intelligence 0.11.0-rc.9 → 0.11.1

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/cli/commands/adapter.sh +46 -6
  2. package/cli/commands/init.sh +182 -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 +51 -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 +43 -0
  20. package/cli/lib/cli-common.sh +12 -8
  21. package/cli/lib/gitignore.sh +215 -0
  22. package/cli/lib/manifest.sh +35 -1
  23. package/cli/lib/onboarding.sh +137 -0
  24. package/engine/ENGINE_SHA +1 -1
  25. package/engine/VERSION +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 +13 -2
  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 +60 -21
  42. package/packages/sync/references/onboarding-migration.md +87 -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 +94 -38
  46. package/packages/sync/skills/intelligence-review-skills/SKILL.md +1 -1
  47. package/packages/sync/skills/intelligence-sync/SKILL.md +9 -1
@@ -0,0 +1,215 @@
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
+ gitignore_path_is_ignored() {
13
+ local root="$1" path="$2"
14
+ git -C "$root" rev-parse --is-inside-work-tree >/dev/null 2>&1 || return 2
15
+ git -C "$root" check-ignore -q --no-index -- "$path"
16
+ }
17
+
18
+ # Git cannot re-include a child of an excluded directory. Add explicit parent
19
+ # reinclusions, then make sure both parent and child negations occur after any
20
+ # pre-existing rule that still wins. The check-ignore probe also catches broad
21
+ # patterns such as an old `.claude/` or `.cursor/**`, not just exact matches.
22
+ gitignore_add_effective_include() {
23
+ local root="$1" value="${2#./}" dir parent="" part rc
24
+ dir="${value%/*}"
25
+ if [ "$dir" != "$value" ]; then
26
+ while IFS= read -r part; do
27
+ [ -n "$part" ] || continue
28
+ if [ -n "$parent" ]; then parent="$parent/$part"; else parent="$part"; fi
29
+ gitignore_add_line "$root" "!$parent/"
30
+ if gitignore_path_is_ignored "$root" "$parent/"; then
31
+ printf '!%s/\n' "$parent" >> "$root/.gitignore"
32
+ else
33
+ rc=$?
34
+ [ "$rc" -eq 1 ] || [ "$rc" -eq 2 ] || return "$rc"
35
+ fi
36
+ done < <(printf '%s\n' "$dir" | tr '/' '\n')
37
+ fi
38
+
39
+ gitignore_add_line "$root" "!$value"
40
+ if gitignore_path_is_ignored "$root" "$value"; then
41
+ printf '!%s\n' "$value" >> "$root/.gitignore"
42
+ else
43
+ rc=$?
44
+ [ "$rc" -eq 1 ] || [ "$rc" -eq 2 ] || return "$rc"
45
+ fi
46
+ }
47
+
48
+ ensure_gitignore_header() {
49
+ local root="$1" file="$1/.gitignore"
50
+ if [ ! -f "$file" ] || ! grep -Fqx '# Intelligence generated state and tool output' "$file"; then
51
+ if [ -f "$file" ] && [ -s "$file" ] && [ -n "$(tail -c 1 "$file" 2>/dev/null)" ]; then
52
+ printf '\n' >> "$file"
53
+ fi
54
+ printf '%s\n' '# Intelligence generated state and tool output' >> "$file"
55
+ fi
56
+ }
57
+
58
+ ensure_base_gitignore() {
59
+ local root="$1"
60
+ ensure_gitignore_header "$root"
61
+ gitignore_add_line "$root" '.intelligence/'
62
+ }
63
+
64
+ ensure_target_gitignore() {
65
+ local root="$1" manifest="$2" target="$3" content_dir output kind value records
66
+ ensure_gitignore_header "$root"
67
+ content_dir="$(manifest_intelligence_dir "$manifest")"
68
+ output="$(get_target_output "$manifest" "$target")"
69
+ [ -n "$output" ] || output="$(default_target_output "$target")"
70
+ records="$(adapter_records_for "$root" "$content_dir" "$target" "$output")" \
71
+ || die "adapter '$target' has an invalid ownership contract"
72
+ while IFS=$'\t' read -r kind value; do
73
+ case "$kind" in
74
+ ignore) gitignore_add_line "$root" "$value" ;;
75
+ include) gitignore_add_effective_include "$root" "$value" ;;
76
+ esac
77
+ done <<< "$records"
78
+ }
79
+
80
+ ensure_manifest_gitignore() {
81
+ local root="$1" manifest="$2" target
82
+ ensure_base_gitignore "$root"
83
+ while IFS= read -r target; do
84
+ [ -n "$target" ] || continue
85
+ if [ "$(is_target_enabled "$manifest" "$target")" = "1" ]; then
86
+ ensure_target_gitignore "$root" "$manifest" "$target"
87
+ fi
88
+ done < <(target_names "$manifest")
89
+ }
90
+
91
+ # Publishing/build tools do not share Git's ignore policy. In particular,
92
+ # .vscodeignore and .npmignore become the packager's own filter, while Docker
93
+ # always builds from its independent context filter. Only amend files the
94
+ # project already has: their presence is the explicit signal that this root is
95
+ # packaged or sent as a build context.
96
+ publisher_ignore_file_names() {
97
+ printf '%s\n' .vscodeignore .npmignore .dockerignore
98
+ }
99
+
100
+ publisher_ignore_add_line() {
101
+ local file="$1" line="$2"
102
+ if grep -Fqx -- "$line" "$file" 2>/dev/null; then
103
+ return 0
104
+ fi
105
+ printf '%s\n' "$line" >> "$file"
106
+ PUBLISH_IGNORE_FILE_CHANGED=1
107
+ }
108
+
109
+ publisher_ignore_add_path() {
110
+ local file="$1" path="${2#./}"
111
+ path="${path%/}"
112
+ [ -n "$path" ] || return 0
113
+ publisher_ignore_add_line "$file" "$path"
114
+ publisher_ignore_add_line "$file" "$path/**"
115
+ }
116
+
117
+ ensure_manifest_publisher_ignores() {
118
+ local root="$1" manifest="$2" rel file content_dir target output records kind value
119
+ content_dir="$(manifest_intelligence_dir "$manifest")"
120
+ while IFS= read -r rel; do
121
+ [ -n "$rel" ] || continue
122
+ file="$root/$rel"
123
+ [ -f "$file" ] || continue
124
+ PUBLISH_IGNORE_FILE_CHANGED=0
125
+ if ! grep -Fqx '# Intelligence development context and generated output' "$file" 2>/dev/null; then
126
+ if [ -s "$file" ] && [ -n "$(tail -c 1 "$file" 2>/dev/null)" ]; then
127
+ printf '\n' >> "$file"
128
+ fi
129
+ printf '%s\n' '# Intelligence development context and generated output' >> "$file"
130
+ PUBLISH_IGNORE_FILE_CHANGED=1
131
+ fi
132
+ publisher_ignore_add_path "$file" '.intelligence'
133
+ publisher_ignore_add_line "$file" 'intelligence.yaml'
134
+ publisher_ignore_add_line "$file" 'intelligence.lock'
135
+ publisher_ignore_add_path "$file" "$content_dir"
136
+ while IFS= read -r target; do
137
+ [ -n "$target" ] || continue
138
+ [ "$(is_target_enabled "$manifest" "$target")" = "1" ] || continue
139
+ output="$(get_target_output "$manifest" "$target")"
140
+ [ -n "$output" ] || output="$(default_target_output "$target")"
141
+ publisher_ignore_add_path "$file" "$output"
142
+ records="$(adapter_records_for "$root" "$content_dir" "$target" "$output")" \
143
+ || die "adapter '$target' has an invalid ownership contract"
144
+ while IFS=$'\t' read -r kind value; do
145
+ case "$kind" in
146
+ owned|managed|legacy) publisher_ignore_add_path "$file" "$value" ;;
147
+ esac
148
+ done <<< "$records"
149
+ done < <(target_names "$manifest")
150
+ if [ "$PUBLISH_IGNORE_FILE_CHANGED" -eq 1 ]; then
151
+ echo "packaging exclusions updated: $rel"
152
+ fi
153
+ done < <(publisher_ignore_file_names)
154
+ }
155
+
156
+ managed_gitignore_patterns() {
157
+ local root="$1" manifest="$2" content_dir target output records kind value
158
+ content_dir="$(manifest_intelligence_dir "$manifest")"
159
+ printf '%s\n' '.intelligence/' "$content_dir/_backup/"
160
+ while IFS= read -r target; do
161
+ [ -n "$target" ] || continue
162
+ [ "$(is_target_enabled "$manifest" "$target")" = "1" ] || continue
163
+ output="$(get_target_output "$manifest" "$target")"
164
+ [ -n "$output" ] || output="$(default_target_output "$target")"
165
+ records="$(adapter_records_for "$root" "$content_dir" "$target" "$output")" \
166
+ || return 1
167
+ while IFS=$'\t' read -r kind value; do
168
+ case "$kind" in
169
+ ignore) printf '%s\n' "$value" ;;
170
+ include) printf '!%s\n' "$value" ;;
171
+ esac
172
+ done <<< "$records"
173
+ done < <(target_names "$manifest")
174
+ }
175
+
176
+ # Git does not apply ignore rules retroactively to files already in its index.
177
+ # Name each affected path that still exists and print a command which only
178
+ # untracks it. Quarantined legacy paths are already worktree deletions and must
179
+ # not receive a misleading "keep the local copy" instruction.
180
+ report_tracked_managed_ignores() {
181
+ local root="$1" manifest="$2" patterns tracked path quoted posix_quoted powershell_quoted announced=0
182
+ git -C "$root" rev-parse --is-inside-work-tree >/dev/null 2>&1 || return 0
183
+ patterns="$(mktemp -t intelligence-gitignore-XXXXXX)"
184
+ tracked="$(mktemp -t intelligence-tracked-XXXXXX)"
185
+ managed_gitignore_patterns "$root" "$manifest" > "$patterns" || {
186
+ rm -f "$patterns" "$tracked"
187
+ return 0
188
+ }
189
+ git -C "$root" ls-files -z -ci --exclude-from="$patterns" > "$tracked" 2>/dev/null || true
190
+ rm -f "$patterns"
191
+ [ -s "$tracked" ] || {
192
+ rm -f "$tracked"
193
+ return 0
194
+ }
195
+ while IFS= read -r -d '' path; do
196
+ [ -n "$path" ] || continue
197
+ [ -e "$root/$path" ] || [ -L "$root/$path" ] || continue
198
+ if [ "$announced" -eq 0 ]; then
199
+ echo " Tracked files still bypass these .gitignore rules. Untrack them without deleting local copies:"
200
+ announced=1
201
+ fi
202
+ if [[ "$path" == *"'"* ]]; then
203
+ # macOS still ships Bash 3.2; use portable sed rather than newer
204
+ # parameter-replacement behavior for the embedded quote.
205
+ posix_quoted="$(printf '%s' "$path" | sed "s/'/'\\\\''/g")"
206
+ powershell_quoted="$(printf '%s' "$path" | sed "s/'/''/g")"
207
+ printf " POSIX: git rm --cached -- '%s'\n" "$posix_quoted"
208
+ printf " PowerShell: git rm --cached -- '%s'\n" "$powershell_quoted"
209
+ else
210
+ quoted="'$path'"
211
+ printf ' git rm --cached -- %s\n' "$quoted"
212
+ fi
213
+ done < "$tracked"
214
+ rm -f "$tracked"
215
+ }
@@ -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
@@ -0,0 +1,137 @@
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_records() {
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_onboarding_records "$root" "$content_dir" "$target" "$output"
11
+ done | awk -F '\t' 'NF == 2 && !seen[$1 SUBSEP $2]++'
12
+ }
13
+
14
+ onboarding_source_paths() {
15
+ onboarding_source_records "$1" "$2" "$3" \
16
+ | awk -F '\t' 'NF == 2 && !seen[$2]++ { print $2 }'
17
+ }
18
+
19
+ onboarding_legacy_paths() {
20
+ onboarding_source_records "$1" "$2" "$3" \
21
+ | awk -F '\t' '$1 == "legacy" && !seen[$2]++ { print $2 }'
22
+ }
23
+
24
+ onboarding_source_count() {
25
+ local root="$1" content_dir="$2" targets="$3" rel count=0
26
+ while IFS= read -r rel; do
27
+ [ -e "$root/$rel" ] || [ -L "$root/$rel" ] || continue
28
+ count=$((count + 1))
29
+ done < <(onboarding_source_paths "$root" "$content_dir" "$targets")
30
+ printf '%s\n' "$count"
31
+ }
32
+
33
+ preserve_onboarding_sources() {
34
+ local root="$1" content_dir="$2" targets="$3"
35
+ local rel src dest backup_rel backup stage target legacy_count=0
36
+ backup_rel="$content_dir/_backup"
37
+ backup="$root/$backup_rel"
38
+ ONBOARDING_BACKUP_COUNT="$(onboarding_source_count "$root" "$content_dir" "$targets")"
39
+ ONBOARDING_BACKUP_REL=""
40
+ ONBOARDING_LEGACY_COUNT=0
41
+ export ONBOARDING_BACKUP_COUNT ONBOARDING_BACKUP_REL ONBOARDING_LEGACY_COUNT
42
+ [ "$ONBOARDING_BACKUP_COUNT" -gt 0 ] || return 0
43
+ [ ! -e "$backup" ] || die "onboarding backup already exists at $backup_rel - review or move it before rerunning init"
44
+
45
+ mkdir -p "$root/$content_dir"
46
+ stage="$root/$content_dir/.backup-stage.$$"
47
+ [ ! -e "$stage" ] || die "temporary onboarding backup path already exists"
48
+ mkdir -p "$stage"
49
+
50
+ {
51
+ printf '# Intelligence initial-state backup contract v1\n'
52
+ printf 'state\tinitial-onboarding\n'
53
+ printf 'source\tpre-intelligence\n'
54
+ for target in $targets; do printf 'target\t%s\n' "$target"; done
55
+ } > "$stage/manifest.tsv"
56
+
57
+ while IFS= read -r rel; do
58
+ src="$root/$rel"
59
+ [ -e "$src" ] || [ -L "$src" ] || continue
60
+ dest="$stage/$rel"
61
+ mkdir -p "$(dirname "$dest")"
62
+ if ! cp -R "$src" "$dest"; then
63
+ rm -rf "$stage"
64
+ die "failed to preserve initial AI instruction path '$rel'"
65
+ fi
66
+ printf 'path\t%s\n' "$rel" >> "$stage/manifest.tsv"
67
+ done < <(onboarding_source_paths "$root" "$content_dir" "$targets")
68
+
69
+ while IFS= read -r rel; do
70
+ [ -e "$stage/$rel" ] || [ -L "$stage/$rel" ] || continue
71
+ printf 'legacy\t%s\n' "$rel" >> "$stage/manifest.tsv"
72
+ legacy_count=$((legacy_count + 1))
73
+ done < <(onboarding_legacy_paths "$root" "$content_dir" "$targets")
74
+
75
+ # This marker is deliberately outside manifest.tsv: the manifest is the
76
+ # immutable inventory, while the marker is short-lived transaction state.
77
+ [ "$legacy_count" -eq 0 ] || : > "$stage/.quarantine-pending"
78
+
79
+ mv "$stage" "$backup"
80
+ ensure_gitignore_header "$root"
81
+ gitignore_add_line "$root" "$backup_rel/"
82
+ ONBOARDING_BACKUP_REL="$backup_rel"
83
+ ONBOARDING_LEGACY_COUNT="$legacy_count"
84
+ export ONBOARDING_BACKUP_REL ONBOARDING_LEGACY_COUNT
85
+ }
86
+
87
+ onboarding_quarantine_is_pending() {
88
+ [ -f "$1/$2/_backup/.quarantine-pending" ]
89
+ }
90
+
91
+ onboarding_backup_legacy_paths() {
92
+ local manifest="$1/$2/_backup/manifest.tsv"
93
+ [ -f "$manifest" ] || return 0
94
+ awk -F '\t' '$1 == "legacy" && NF == 2 && !seen[$2]++ { print $2 }' "$manifest"
95
+ }
96
+
97
+ quarantine_onboarding_legacy_sources() {
98
+ local root="$1" content_dir="$2" rel src backup
99
+ onboarding_quarantine_is_pending "$root" "$content_dir" || return 0
100
+ backup="$root/$content_dir/_backup"
101
+ while IFS= read -r rel; do
102
+ [ -n "$rel" ] || continue
103
+ src="$root/$rel"
104
+ [ -e "$src" ] || [ -L "$src" ] || continue
105
+ [ -e "$backup/$rel" ] || [ -L "$backup/$rel" ] \
106
+ || die "initial backup is missing legacy path '$rel'"
107
+ if [ -d "$src" ] && [ ! -L "$src" ]; then
108
+ rm -rf -- "$src"
109
+ else
110
+ rm -f -- "$src"
111
+ fi
112
+ done < <(onboarding_backup_legacy_paths "$root" "$content_dir")
113
+ }
114
+
115
+ restore_onboarding_legacy_sources() {
116
+ local root="$1" content_dir="$2" rel src dest backup
117
+ onboarding_quarantine_is_pending "$root" "$content_dir" || return 0
118
+ backup="$root/$content_dir/_backup"
119
+ while IFS= read -r rel; do
120
+ [ -n "$rel" ] || continue
121
+ src="$backup/$rel"
122
+ [ -e "$src" ] || [ -L "$src" ] || continue
123
+ dest="$root/$rel"
124
+ if [ -d "$dest" ] && [ ! -L "$dest" ]; then
125
+ rm -rf -- "$dest"
126
+ else
127
+ rm -f -- "$dest"
128
+ fi
129
+ mkdir -p "$(dirname "$dest")"
130
+ cp -R "$src" "$dest" || die "failed to restore legacy onboarding path '$rel'"
131
+ done < <(onboarding_backup_legacy_paths "$root" "$content_dir")
132
+ }
133
+
134
+ complete_onboarding_quarantine() {
135
+ local marker="$1/$2/_backup/.quarantine-pending"
136
+ [ ! -f "$marker" ] || rm -f -- "$marker"
137
+ }
package/engine/ENGINE_SHA CHANGED
@@ -1 +1 @@
1
- af238e67e8a9469edf6d2058e169de52cbeb93af
1
+ e62f3af481c9be3e9782b2f88115758c5cd21669
package/engine/VERSION CHANGED
@@ -1 +1 @@
1
- 0.11.0
1
+ 0.11.1
@@ -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,10 +26,21 @@
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
- # duplicating the awk block here would violate the "all parsing in common.sh"
32
- # convention (see CLAUDE.md / packages/sync/references/conventions.md).
42
+ # duplicating the awk block here would fork frontmatter parsing, which the
43
+ # convention keeps in one place.
33
44
 
34
45
  sync_opencode_skills() {
35
46
  local repo_root="$1"
@@ -15,6 +15,20 @@
15
15
 
16
16
  source "$(dirname "${BASH_SOURCE[0]}")/../lib/common.sh"
17
17
 
18
+ adapter_contract_pi() {
19
+ local output="${1%/}"
20
+ adapter_contract_version 1
21
+ adapter_contract_requires agents
22
+ adapter_contract_managed ".agents/skills"
23
+ adapter_contract_owned "$output/intelligence-sync"
24
+ adapter_contract_managed "$output/extensions"
25
+ adapter_contract_managed "$output/prompts"
26
+ adapter_contract_ignore ".agents/skills/"
27
+ adapter_contract_ignore "$output/intelligence-sync/"
28
+ adapter_contract_ignore "$output/extensions/intelligence-sync-rules.ts"
29
+ adapter_contract_ignore "$output/prompts/intelligence-agent-*.md"
30
+ }
31
+
18
32
  pi_ts_escape() {
19
33
  local s="$1"
20
34
  s="${s//\\/\\\\}"
@@ -193,13 +207,12 @@ sync_pi_agents() {
193
207
 
194
208
  access_note=""
195
209
  if [ "$access" = "readonly" ]; then
196
- access_note=$(cat <<'EOF'
197
- ## Access Mode
210
+ # Bash 3.2 (the macOS system shell) misparses an apostrophe in
211
+ # a quoted heredoc nested inside command substitution.
212
+ access_note="## Access Mode
198
213
 
199
214
  Default to read-only analysis. Do not change files or run mutating commands unless the user explicitly asks you to override this agent's normal restriction.
200
-
201
- EOF
202
- )
215
+ "
203
216
  fi
204
217
 
205
218
  {