@ainova-systems/intelligence 0.20.0 → 0.22.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 (31) hide show
  1. package/cli/commands/init.sh +2 -1
  2. package/cli/commands/sync.sh +9 -0
  3. package/cli/internal/check.sh +39 -0
  4. package/cli/lib/gitignore.sh +19 -2
  5. package/cli/lib/sync-lock.sh +103 -21
  6. package/engine/ENGINE_SHA +1 -1
  7. package/engine/VERSION +1 -1
  8. package/engine/adapters/_template.sh +32 -8
  9. package/engine/adapters/agents.sh +42 -50
  10. package/engine/adapters/antigravity.sh +68 -52
  11. package/engine/adapters/claude-plugin/.claude-plugin/plugin.json +6 -0
  12. package/engine/adapters/claude-plugin/README.md +14 -0
  13. package/engine/adapters/claude-plugin/hooks/hooks.json +3 -0
  14. package/engine/adapters/claude-plugin/hooks/register.js +324 -0
  15. package/engine/adapters/claude.sh +129 -59
  16. package/engine/adapters/codex.sh +74 -15
  17. package/engine/adapters/copilot.sh +56 -79
  18. package/engine/adapters/cursor.sh +85 -81
  19. package/engine/adapters/opencode.sh +20 -35
  20. package/engine/adapters/pi.sh +137 -41
  21. package/engine/lib/common.sh +753 -56
  22. package/engine/lib/contract.sh +15 -6
  23. package/engine/sync.sh +49 -33
  24. package/package.json +1 -1
  25. package/packages/sync/references/adapters.md +56 -40
  26. package/packages/sync/references/conventions.md +57 -11
  27. package/packages/sync/references/onboarding-migration.md +4 -4
  28. package/packages/sync/rules/intelligence-authoring.md +2 -2
  29. package/packages/sync/skills/intelligence-learn-from-repository/SKILL.md +7 -5
  30. package/packages/sync/skills/intelligence-review-context/references/audit-checks.md +1 -1
  31. package/packages/sync/skills/intelligence-update-context/references/rules.md +5 -0
@@ -6,6 +6,11 @@
6
6
  # Skills: copy skill directories in full (SKILL.md + bundled resources),
7
7
  # effort -> Claude's effort level
8
8
  # Agents: tier -> model, effort -> effort, access -> tools/disallowedTools
9
+ # Plugin: with `targets.claude.plugin: true`, the Claude Code mod in
10
+ # adapters/claude-plugin/ is rendered into <output>/skills/intelligence/
11
+ # (decision 0022). Claude Code loads a plugin from a project's skills
12
+ # directory, so the plugin lives inside a path this contract already owns:
13
+ # ownership, Git policy, snapshots and `sync --check` need no new record.
9
14
  #
10
15
  # Every per-file loop batches its work into one awk process (see the batched
11
16
  # helpers in lib/common.sh): process spawns dominate sync time on Windows.
@@ -29,29 +34,67 @@ adapter_contract_claude() {
29
34
  adapter_contract_include "$output/settings.json"
30
35
  }
31
36
 
37
+ # claude_plugin_var <manifest> — the one reader of targets.claude.plugin. Sets
38
+ # IS_CLAUDE_PLUGIN to 1 when the project renders the Claude Code plugin and 0
39
+ # for the default; returns 1 for a value target_bool_var refuses.
40
+ claude_plugin_var() {
41
+ local rc=0
42
+ target_bool_var "$1" claude plugin || rc=1
43
+ IS_CLAUDE_PLUGIN="$IS_TARGET_BOOL"
44
+ return "$rc"
45
+ }
46
+
47
+ # The directory name the plugin takes under <output>/skills: a source skill
48
+ # rendered under this name would be overwritten, so sync refuses the pair.
49
+ IS_CLAUDE_PLUGIN_NAME=intelligence
50
+
51
+ # render_claude_plugin <output_dir> — copy the plugin template byte for byte,
52
+ # stamp the engine version into its manifest and normalize line endings. The
53
+ # template is in the engine's adapter directory, never in the content package:
54
+ # it is executable tooling, and the bundle copies all of engine/. It is found
55
+ # through the engine root, so a project's own claude.sh renders it too.
56
+ render_claude_plugin() {
57
+ local output_dir="$1" tpl
58
+ local dst="$output_dir/skills/$IS_CLAUDE_PLUGIN_NAME"
59
+ # Rendered beside the destination, under a name no skill can take, and
60
+ # swapped in only when it differs: a running session reloads the mod on
61
+ # every file change, and announces each reload in its transcript.
62
+ local tmp="$output_dir/skills/.$IS_CLAUDE_PLUGIN_NAME.render.$$"
63
+ engine_root_var
64
+ tpl="$IS_ENGINE_ROOT/adapters/claude-plugin"
65
+ engine_version_var
66
+ rm -rf "$tmp"
67
+ mkdir -p "$tmp/.claude-plugin" "$tmp/hooks"
68
+ cp "$tpl/hooks/hooks.json" "$tpl/hooks/register.js" "$tmp/hooks/"
69
+ cp "$tpl/README.md" "$tmp/README.md"
70
+ awk -v v="$IS_ENGINE_VERSION" '{ sub(/__INTELLIGENCE_VERSION__/, v); print }' \
71
+ "$tpl/.claude-plugin/plugin.json" > "$tmp/.claude-plugin/plugin.json"
72
+ finalize_output_files "$tmp/.claude-plugin/plugin.json" "$tmp/hooks/hooks.json" \
73
+ "$tmp/hooks/register.js" "$tmp/README.md"
74
+ if [ -d "$dst" ] && [ ! -L "$dst" ] && diff -r -q "$tmp" "$dst" >/dev/null 2>&1; then
75
+ rm -rf "$tmp"
76
+ else
77
+ rm -rf "$dst"
78
+ mv "$tmp" "$dst"
79
+ fi
80
+ echo " plugin: $IS_CLAUDE_PLUGIN_NAME (Claude Code mod $IS_ENGINE_VERSION)"
81
+ }
82
+
32
83
  # Sync rules to Claude format (copy as-is, normalize LF)
33
84
  sync_claude_rules() {
34
85
  local repo_root="$1"
35
86
  local config_file="$2"
36
87
  local output_dir="$3"
37
88
 
38
- local src f
39
- local -a files=()
40
- load_yaml_list "$config_file" "rules"
41
- local list="$IS_YAML_LIST"
42
- while IFS= read -r src; do
43
- [ -z "$src" ] && continue
44
- local dir="$repo_root/$src"
45
- [ -d "$dir" ] || continue
46
- for f in "$dir"/*.md; do
47
- [ -f "$f" ] || continue
48
- files+=("$f")
49
- done
50
- done <<< "$list"
51
- [ "${#files[@]}" -gt 0 ] || return 0
52
- finalize_copy_files "$output_dir/rules" "${files[@]}"
53
- for f in "${files[@]}"; do
54
- echo " rule: ${f##*/}"
89
+ local name
90
+ # Every source reaches this adapter through the source catalog (decision
91
+ # 0021): rendered names, overrides and local artifacts are settled there.
92
+ output_rel_var "$repo_root" "$output_dir"
93
+ source_artifacts_var "$repo_root" "$config_file" rules claude "$IS_OUTPUT_REL/rules/{}.md"
94
+ [ "${#IS_SOURCE_FILES[@]}" -gt 0 ] || return 0
95
+ finalize_copy_named "$output_dir/rules" "$IS_SOURCE_NAME_MAP" "${IS_SOURCE_FILES[@]}"
96
+ for name in "${IS_SOURCE_NAMES[@]}"; do
97
+ echo " rule: $name.md"
55
98
  done
56
99
  }
57
100
 
@@ -61,29 +104,38 @@ sync_claude_skills() {
61
104
  local config_file="$2"
62
105
  local output_dir="$3"
63
106
 
64
- local src d skill_name
107
+ local i name clash="" nocase=0
65
108
  local -a skill_dirs=()
66
- # The inventory decides which skills render, symlinked ones included
67
- # (decision 0017); the glob keeps this adapter's own listing order.
68
- skill_source_inventory "$repo_root" "$config_file"
69
- load_yaml_list "$config_file" "skills"
70
- local list="$IS_YAML_LIST"
71
- while IFS= read -r src; do
72
- [ -z "$src" ] && continue
73
- local dir="$repo_root/$src"
74
- [ -d "$dir" ] || continue
75
- for d in "$dir"/*/; do
76
- [ -d "$d" ] || continue
77
- [ -f "$d/SKILL.md" ] || continue
78
- skill_source_rendered "$d" || continue
79
- skill_dirs+=("$d")
109
+ # The catalog's skills are the ones the inventory renders, symlinked ones
110
+ # included (decision 0017).
111
+ output_rel_var "$repo_root" "$output_dir"
112
+ source_artifacts_var "$repo_root" "$config_file" skills claude "$IS_OUTPUT_REL/skills/{}"
113
+ [ "${#IS_SOURCE_FILES[@]}" -gt 0 ] || return 0
114
+ # The plugin takes one skill directory name; a skill rendered under it and
115
+ # the plugin would each erase the other, so neither is written. Names
116
+ # compare without case: on Windows and macOS `Intelligence/` is the same
117
+ # directory. (nocasematch, not ${name,,}: macOS ships bash 3.2.)
118
+ if [ "${IS_CLAUDE_PLUGIN:-0}" = 1 ]; then
119
+ shopt -q nocasematch && nocase=1
120
+ shopt -s nocasematch
121
+ for ((i = 0; i < ${#IS_SOURCE_FILES[@]}; i++)); do
122
+ if [[ "${IS_SOURCE_NAMES[i]}" == "$IS_CLAUDE_PLUGIN_NAME" ]]; then
123
+ clash="${IS_SOURCE_FILES[i]#"$repo_root/"}"
124
+ break
125
+ fi
80
126
  done
81
- done <<< "$list"
82
- [ "${#skill_dirs[@]}" -gt 0 ] || return 0
83
- copy_skill_bundle_dirs_for claude "$output_dir/skills" "${skill_dirs[@]}"
84
- for d in "${skill_dirs[@]}"; do
85
- skill_name="${d%/}"
86
- echo " skill: ${skill_name##*/}"
127
+ [ "$nocase" = 1 ] || shopt -u nocasematch
128
+ if [ -n "$clash" ]; then
129
+ echo "ERROR: skill $clash renders as $IS_OUTPUT_REL/skills/$IS_CLAUDE_PLUGIN_NAME, the directory targets.claude.plugin: true reserves for the Claude Code plugin. Rename the skill or set targets.claude.plugin to false." >&2
130
+ return 1
131
+ fi
132
+ fi
133
+ for ((i = 0; i < ${#IS_SOURCE_FILES[@]}; i++)); do
134
+ skill_dirs+=("${IS_SOURCE_FILES[i]%/SKILL.md}/")
135
+ done
136
+ copy_skill_bundle_dirs_named claude "$output_dir/skills" "$IS_SOURCE_NAME_MAP" "${skill_dirs[@]}"
137
+ for name in "${IS_SOURCE_NAMES[@]}"; do
138
+ echo " skill: $name"
87
139
  done
88
140
  }
89
141
 
@@ -94,32 +146,27 @@ sync_claude_agents() {
94
146
  local config_file="$2"
95
147
  local output_dir="$3"
96
148
 
97
- local src f
98
- local -a files=()
99
- load_yaml_list "$config_file" "agents"
100
- local list="$IS_YAML_LIST"
101
- while IFS= read -r src; do
102
- [ -z "$src" ] && continue
103
- local dir="$repo_root/$src"
104
- [ -d "$dir" ] || continue
105
- for f in "$dir"/*.md; do
106
- [ -f "$f" ] || continue
107
- files+=("$f")
108
- done
109
- done <<< "$list"
110
- [ "${#files[@]}" -gt 0 ] || return 0
149
+ local i
150
+ local -a files=() names=()
151
+ output_rel_var "$repo_root" "$output_dir"
152
+ source_artifacts_var "$repo_root" "$config_file" agents claude "$IS_OUTPUT_REL/agents/{}.md"
153
+ [ "${#IS_SOURCE_FILES[@]}" -gt 0 ] || return 0
154
+ files=("${IS_SOURCE_FILES[@]}")
155
+ names=("${IS_SOURCE_NAMES[@]}")
156
+ local name_map="$IS_SOURCE_NAME_MAP"
111
157
 
112
158
  load_model_tiers "$config_file" "claude"
113
159
 
114
160
  local path tier access effort spec="" report=""
161
+ i=0
115
162
  while IFS=$'\x1f' read -r path tier access effort; do
116
- [ -n "$path" ] || continue
117
163
  resolve_model_var "$tier"
118
164
  map_effort_var claude "$effort"
119
165
  map_access_to_claude_tools_var "$access"
120
166
  map_access_to_claude_disallowed_var "$access"
121
167
  spec+="$path"$'\x1f'"$IS_MODEL"$'\x1f'"$IS_CLAUDE_TOOLS"$'\x1f'"$IS_CLAUDE_DISALLOWED"$'\x1f'"$IS_EFFORT"$'\n'
122
- report+=" agent: ${path##*/} (tier=$tier -> model=$IS_MODEL, access=$access)"$'\n'
168
+ report+=" agent: ${names[i]}.md (tier=$tier -> model=$IS_MODEL, access=$access)"$'\n'
169
+ i=$((i + 1))
123
170
  done < <(frontmatter_index "tier,access,effort" "${files[@]}")
124
171
 
125
172
  # The transform used to run per file with the values injected via -v;
@@ -128,7 +175,7 @@ sync_claude_agents() {
128
175
  # becomes Claude's level in place, or goes when there is none; a later
129
176
  # one goes too, since frontmatter_index read only the first.
130
177
  is_fin_awk_vars
131
- IS_MAP_SPEC="$spec" awk "${IS_FIN_V[@]}" -v dst="$output_dir/agents" "$IS_AWK_LIB"'
178
+ IS_NAME_MAP="$name_map" IS_MAP_SPEC="$spec" awk "${IS_FIN_V[@]}" -v dst="$output_dir/agents" "$IS_AWK_LIB"'
132
179
  BEGIN {
133
180
  US = sprintf("%c", 31)
134
181
  n = split(ENVIRON["IS_MAP_SPEC"], recs, "\n")
@@ -140,16 +187,18 @@ sync_claude_agents() {
140
187
  }
141
188
  FNR == 1 {
142
189
  if (out != "") close(out)
143
- out = dst "/" base_name(FILENAME)
190
+ out = dst "/" art_file(FILENAME, ".md")
144
191
  count = 0
145
192
  fm = 0
146
193
  effort_seen = 0
194
+ tgt_reset()
147
195
  }
148
196
  /^tier:/ { next }
149
197
  /^access:/ { next }
150
198
  { sub(/\r$/, "") }
151
199
  FNR == 1 && $0 == "---" { fm = 1 }
152
200
  /^---$/ { count++ }
201
+ fm && count == 1 && !/^---$/ && tgt_drop($0) { next }
153
202
  fm && count == 1 && substr($0, 1, 7) == "effort:" {
154
203
  if (!effort_seen++ && EFFORT[FILENAME] != "") print fin_line("effort: " EFFORT[FILENAME]) > out
155
204
  next
@@ -163,8 +212,8 @@ sync_claude_agents() {
163
212
  ' "${files[@]}"
164
213
  # awk never reads a record from an empty source; the old per-file shell
165
214
  # redirect still created an empty output for it.
166
- for f in "${files[@]}"; do
167
- [ -s "$f" ] || : > "$output_dir/agents/${f##*/}"
215
+ for ((i = 0; i < ${#files[@]}; i++)); do
216
+ [ -s "${files[i]}" ] || : > "$output_dir/agents/${names[i]}.md"
168
217
  done
169
218
  printf '%s' "$report"
170
219
  }
@@ -177,18 +226,39 @@ sync_to_claude() {
177
226
 
178
227
  echo "=== Claude Code ==="
179
228
 
229
+ # The flag is read before anything is removed: a malformed value refuses
230
+ # the sync with the reason, and the output stays exactly as it was.
231
+ claude_plugin_var "$config_file" || {
232
+ echo "ERROR: targets.claude.plugin must be true or false: true renders the Claude Code plugin into ${output_dir#"$repo_root/"}/skills/$IS_CLAUDE_PLUGIN_NAME, false or omission does not." >&2
233
+ return 1
234
+ }
235
+
180
236
  # Clean generated content only — preserve any project-level files
181
237
  # the user manages directly (settings.json, settings.local.json,
182
238
  # commands/, statusline.sh, etc.).
183
239
  rm -rf "$output_dir/rules" "$output_dir/agents"
184
240
  if [ -d "$output_dir/skills" ]; then
185
- find "$output_dir/skills" -mindepth 1 -maxdepth 1 \( -type d -o -type l \) -exec rm -rf {} +
241
+ # The plugin directory stays while the flag holds: Claude Code reloads
242
+ # a mod whose files change, so render_claude_plugin rewrites it only
243
+ # when its content differs. A skill of that name was refused above.
244
+ if [ "$IS_CLAUDE_PLUGIN" = 1 ]; then
245
+ find "$output_dir/skills" -mindepth 1 -maxdepth 1 \( -type d -o -type l \) \
246
+ ! -name "$IS_CLAUDE_PLUGIN_NAME" -exec rm -rf {} +
247
+ else
248
+ find "$output_dir/skills" -mindepth 1 -maxdepth 1 \( -type d -o -type l \) -exec rm -rf {} +
249
+ fi
186
250
  fi
187
251
  mkdir -p "$output_dir/rules" "$output_dir/skills" "$output_dir/agents"
188
252
 
189
253
  sync_claude_rules "$repo_root" "$config_file" "$output_dir"
254
+ # Called plainly, never on the left of `||`: that would turn errexit off
255
+ # for every copy inside, and a failed one would no longer fail the sync.
190
256
  sync_claude_skills "$repo_root" "$config_file" "$output_dir"
191
257
  sync_claude_agents "$repo_root" "$config_file" "$output_dir"
258
+ # The skills renderer has refused a skill of the plugin's name by now, so
259
+ # the plugin never lands on top of one. Cleared above with every other
260
+ # skill directory, it exists only while the flag says so.
261
+ [ "$IS_CLAUDE_PLUGIN" = 0 ] || render_claude_plugin "$output_dir"
192
262
 
193
263
  count_matching_files "$output_dir/rules" "*.md" \
194
264
  "$output_dir/skills" "SKILL.md" "$output_dir/agents" "*.md"
@@ -6,6 +6,13 @@
6
6
  # inlines always-on rules into AGENTS.md, so this adapter only emits skills
7
7
  # and subagent definitions.
8
8
  #
9
+ # Rules: an always-on rule AGENTS.md does not carry — a local one, or one whose
10
+ # targets leave `agents` out — goes into AGENTS.override.md beside it
11
+ # (decision 0021). Codex reads that file instead of AGENTS.md in the same
12
+ # directory, so it holds the whole AGENTS.md plus those rules. The contract
13
+ # ignores it, and it exists only while such a rule does; one Intelligence did
14
+ # not generate is never touched, and refuses a render that needs the path.
15
+ #
9
16
  # Skills: copy skill directories in full to .agents/skills/{name}/ (Codex
10
17
  # reads from $REPO_ROOT/.agents/skills exclusively per official docs)
11
18
  # Agents: -> .codex/agents/{name}.toml (name, description, model,
@@ -26,8 +33,61 @@ adapter_contract_codex() {
26
33
  adapter_contract_requires agents
27
34
  adapter_contract_managed ".agents/skills"
28
35
  adapter_contract_owned "$output/agents"
36
+ # Marker-managed: a hand-written override is left alone.
37
+ adapter_contract_managed "AGENTS.override.md"
29
38
  adapter_contract_ignore ".agents/skills/"
30
39
  adapter_contract_ignore "$output/agents/"
40
+ adapter_contract_ignore "AGENTS.override.md"
41
+ }
42
+
43
+ CODEX_OVERRIDE_MARKER="<!-- Generated by intelligence-sync. Do not edit manually. -->"
44
+
45
+ # sync_codex_override <repo_root> <config_file> — write AGENTS.override.md
46
+ # while an always-on rule AGENTS.md does not carry is for Codex, and remove a
47
+ # generated one otherwise.
48
+ sync_codex_override() {
49
+ local repo_root="$1" config_file="$2" override="$1/AGENTS.override.md" first="" agents_file i
50
+ local -a files=() names=()
51
+ source_artifacts_var "$repo_root" "$config_file" rules codex "AGENTS.override.md"
52
+ for ((i = 0; i < ${#IS_SOURCE_FILES[@]}; i++)); do
53
+ [ "${IS_SOURCE_SCOPED[i]}" = 0 ] && [ "${IS_SOURCE_IN_AGENTS[i]}" = 0 ] || continue
54
+ files+=("${IS_SOURCE_FILES[i]}")
55
+ names+=("${IS_SOURCE_NAMES[i]}")
56
+ done
57
+ if [ -f "$override" ]; then
58
+ IFS= read -r first < "$override" || true
59
+ if [ "${first%$'\r'}" != "$CODEX_OVERRIDE_MARKER" ]; then
60
+ [ "${#files[@]}" -gt 0 ] || return 0
61
+ echo "ERROR: AGENTS.override.md was not generated by intelligence-sync, but ${names[*]} need it: always-on rules for Codex that AGENTS.md does not carry go there. Move its guidance into a rule, remove it, then sync." >&2
62
+ return 1
63
+ fi
64
+ fi
65
+ if [ "${#files[@]}" -eq 0 ]; then
66
+ rm -f "$override"
67
+ return 0
68
+ fi
69
+ target_output_var "$config_file" "agents"
70
+ agents_output_path_var "${IS_TGT_OUTPUT:-.agents}"
71
+ agents_file="$repo_root/$IS_AGENTS_OUTPUT_PATH"
72
+ {
73
+ if [ -f "$agents_file" ]; then
74
+ cat "$agents_file"
75
+ else
76
+ printf '%s\n\n' "$CODEX_OVERRIDE_MARKER"
77
+ fi
78
+ echo "---"
79
+ echo ""
80
+ echo "## Codex Project Context"
81
+ echo ""
82
+ echo "<!-- Always-on rules AGENTS.md does not carry. Codex reads this file instead of AGENTS.md. -->"
83
+ echo ""
84
+ emit_rule_bodies "${files[@]}"
85
+ } > "$override"
86
+ finalize_output_file "$override"
87
+ for ((i = 0; i < ${#names[@]}; i++)); do
88
+ echo " rule: ${names[i]} (always-on, AGENTS.override.md)"
89
+ done
90
+ echo " -> AGENTS.override.md"
31
91
  }
32
92
 
33
93
  # Codex-specific guard for its documented project-instruction byte budget.
@@ -52,6 +112,8 @@ codex_warn_project_doc_size() {
52
112
  target_output_var "$config_file" "agents"
53
113
  local output
54
114
  output="$(agents_output_path "${IS_TGT_OUTPUT:-.agents}")"
115
+ # Codex reads AGENTS.override.md instead of AGENTS.md when it exists.
116
+ [ ! -f "$repo_root/AGENTS.override.md" ] || output="AGENTS.override.md"
55
117
 
56
118
  local output_file="$repo_root/$output"
57
119
  [ -f "$output_file" ] || return 0
@@ -83,19 +145,13 @@ sync_codex_agents() {
83
145
  local config_file="$2"
84
146
  local output_dir="$3"
85
147
 
86
- local src f
87
- local -a files=()
88
- load_yaml_list "$config_file" "agents"
89
- local list="$IS_YAML_LIST"
90
- while IFS= read -r src; do
91
- [ -z "$src" ] && continue
92
- local dir="$repo_root/$src"
93
- [ -d "$dir" ] || continue
94
- for f in "$dir"/*.md; do
95
- [ -f "$f" ] || continue
96
- files+=("$f")
97
- done
98
- done <<< "$list"
148
+ local -a files=() names=()
149
+ output_rel_var "$repo_root" "$output_dir"
150
+ source_artifacts_var "$repo_root" "$config_file" agents codex "$IS_OUTPUT_REL/{}.toml"
151
+ if [ "${#IS_SOURCE_FILES[@]}" -gt 0 ]; then
152
+ files=("${IS_SOURCE_FILES[@]}")
153
+ names=("${IS_SOURCE_NAMES[@]}")
154
+ fi
99
155
 
100
156
  local count=0
101
157
  if [ "${#files[@]}" -gt 0 ]; then
@@ -103,8 +159,7 @@ sync_codex_agents() {
103
159
 
104
160
  local path tier access description effort name sandbox spec="" report="" LS=$'\x1e'
105
161
  while IFS=$'\x1f' read -r path tier access description effort; do
106
- [ -n "$path" ] || continue
107
- name="${path##*/}"; name="${name%.md}"
162
+ name="${names[count]}"
108
163
  resolve_model_var "$tier"
109
164
  map_effort_var codex "$effort"
110
165
  case "$access" in
@@ -155,5 +210,9 @@ sync_to_codex() {
155
210
  mkdir -p "$agents_dir"
156
211
  sync_codex_agents "$repo_root" "$config_file" "$agents_dir"
157
212
 
213
+ # Rules AGENTS.md does not carry -> AGENTS.override.md. The contract
214
+ # requires `agents`, so AGENTS.md has rendered by now.
215
+ sync_codex_override "$repo_root" "$config_file" || return 1
216
+
158
217
  codex_warn_project_doc_size "$repo_root" "$config_file"
159
218
  }
@@ -8,7 +8,9 @@
8
8
  # natively, and the `agents` adapter inlines always-on rule content
9
9
  # into AGENTS.md. We also do not generate .github/copilot-instructions.md
10
10
  # because Copilot bug copilot-cli#489 makes AGENTS.md ignored when
11
- # copilot-instructions.md is present.
11
+ # copilot-instructions.md is present. An always-on rule AGENTS.md does not
12
+ # carry — a local one, or one whose targets leave `agents` out — becomes
13
+ # an .instructions.md file with `applyTo: "**"` (decision 0021).
12
14
  # Skills: copy skill directories in full to .github/skills/{name}/
13
15
  # Agents: -> .github/agents/{name}.agent.md (description, tools, model)
14
16
  #
@@ -53,19 +55,13 @@ adapter_contract_copilot() {
53
55
 
54
56
  # copilot_commit_output_var <manifest> — the one reader of
55
57
  # targets.copilot.commit_output. Sets IS_COPILOT_COMMIT_OUTPUT to 1 when the
56
- # project commits generated output and 0 for the default; returns 1 for any
57
- # value other than true or false. No manifest — a caller reading ownership
58
- # only — is the default.
58
+ # project commits generated output and 0 for the default; returns 1 for a value
59
+ # target_bool_var refuses.
59
60
  copilot_commit_output_var() {
60
- local manifest="$1" setting
61
- IS_COPILOT_COMMIT_OUTPUT=0
62
- [ -n "$manifest" ] && [ -f "$manifest" ] || return 0
63
- setting="$(get_target_field "$manifest" "copilot" "commit_output")"
64
- case "$setting" in
65
- ''|false) ;;
66
- true) IS_COPILOT_COMMIT_OUTPUT=1 ;;
67
- *) return 1 ;;
68
- esac
61
+ local rc=0
62
+ target_bool_var "$1" copilot commit_output || rc=1
63
+ IS_COPILOT_COMMIT_OUTPUT="$IS_TARGET_BOOL"
64
+ return "$rc"
69
65
  }
70
66
 
71
67
  # copilot_paths_rows <file>... — batch form of the old per-file `paths:`
@@ -100,8 +96,8 @@ copilot_paths_rows() {
100
96
  ' "$@"
101
97
  }
102
98
 
103
- # Sync rules to Copilot format. Only path-scoped rules are emitted —
104
- # always-on rules live in AGENTS.md (read by Copilot natively).
99
+ # Sync rules to Copilot format. Path-scoped rules are emitted, and an always-on
100
+ # rule only when AGENTS.md (read by Copilot natively) does not carry it.
105
101
  sync_copilot_rules() {
106
102
  local repo_root="$1"
107
103
  local config_file="$2"
@@ -112,35 +108,28 @@ sync_copilot_rules() {
112
108
 
113
109
  local scoped_count=0
114
110
 
115
- local src f
116
- local -a files=()
117
- load_yaml_list "$config_file" "rules"
118
- local list="$IS_YAML_LIST"
119
- while IFS= read -r src; do
120
- [ -z "$src" ] && continue
121
- local dir="$repo_root/$src"
122
- [ -d "$dir" ] || continue
123
- for f in "$dir"/*.md; do
124
- [ -f "$f" ] || continue
125
- files+=("$f")
126
- done
127
- done <<< "$list"
128
-
129
- local -a scoped=()
130
- if [ "${#files[@]}" -gt 0 ]; then
131
- local path hp
132
- while IFS=$'\x1f' read -r path hp; do
133
- [ -n "$path" ] || continue
134
- [ "$hp" -eq 0 ] && continue
135
- scoped+=("$path")
136
- done < <(frontmatter_index "paths#" "${files[@]}")
137
- fi
111
+ local i always_count=0 spec="" report="" LS=$'\x1e'
112
+ local -a scoped=() scoped_names=()
113
+ output_rel_var "$repo_root" "$instructions_dir"
114
+ source_artifacts_var "$repo_root" "$config_file" rules copilot "$IS_OUTPUT_REL/{}.instructions.md"
115
+ for ((i = 0; i < ${#IS_SOURCE_FILES[@]}; i++)); do
116
+ if [ "${IS_SOURCE_SCOPED[i]}" = 1 ]; then
117
+ scoped+=("${IS_SOURCE_FILES[i]}")
118
+ scoped_names+=("${IS_SOURCE_NAMES[i]}")
119
+ elif [ "${IS_SOURCE_IN_AGENTS[i]}" = 0 ]; then
120
+ # An always-on rule AGENTS.md does not carry applies everywhere.
121
+ spec+="${IS_SOURCE_FILES[i]}"$'\x1f'"$instructions_dir/${IS_SOURCE_NAMES[i]}.instructions.md"$'\x1f'"fence_nofm"$'\x1f'"0"$'\x1f'"none"$'\x1f'"${LS}---${LS}applyTo: \"**\"${LS}---${LS}"$'\x1f\n'
122
+ report+=" rule: ${IS_SOURCE_NAMES[i]}.instructions.md (always-on)"$'\n'
123
+ always_count=$((always_count + 1))
124
+ fi
125
+ done
138
126
 
139
127
  if [ "${#scoped[@]}" -gt 0 ]; then
140
- local base paths_line spec="" report="" LS=$'\x1e'
128
+ local path base paths_line
129
+ i=0
141
130
  while IFS=$'\x1f' read -r path paths_line; do
142
- [ -n "$path" ] || continue
143
- base="${path##*/}"; base="${base%.md}"
131
+ base="${scoped_names[i]}"
132
+ i=$((i + 1))
144
133
 
145
134
  local header
146
135
  header="${LS}---"
@@ -152,12 +141,17 @@ sync_copilot_rules() {
152
141
  report+=" rule: $base.instructions.md (scoped)"$'\n'
153
142
  scoped_count=$((scoped_count + 1))
154
143
  done < <(copilot_paths_rows "${scoped[@]}")
155
-
144
+ fi
145
+ if [ -n "$spec" ]; then
156
146
  emit_wrapped_bodies "$spec"
157
147
  printf '%s' "$report"
158
148
  fi
159
149
 
160
- echo " rules: $scoped_count scoped"
150
+ if [ "$always_count" -gt 0 ]; then
151
+ echo " rules: $scoped_count scoped, $always_count always-on"
152
+ else
153
+ echo " rules: $scoped_count scoped"
154
+ fi
161
155
  }
162
156
 
163
157
  # Sync skills to Copilot format (SKILL.md in .github/skills/)
@@ -166,33 +160,23 @@ sync_copilot_skills() {
166
160
  local config_file="$2"
167
161
  local output_dir="$3"
168
162
 
169
- local src d skill_name
163
+ local i name
170
164
  local -a skill_dirs=()
171
- # The inventory decides which skills render, symlinked ones included
172
- # (decision 0017); the glob keeps this adapter's own listing order.
173
- skill_source_inventory "$repo_root" "$config_file"
174
- load_yaml_list "$config_file" "skills"
175
- local list="$IS_YAML_LIST"
176
- while IFS= read -r src; do
177
- [ -z "$src" ] && continue
178
- local dir="$repo_root/$src"
179
- [ -d "$dir" ] || continue
180
- for d in "$dir"/*/; do
181
- [ -d "$d" ] || continue
182
- [ -f "$d/SKILL.md" ] || continue
183
- skill_source_rendered "$d" || continue
184
- skill_dirs+=("$d")
185
- done
186
- done <<< "$list"
165
+ # The catalog's skills are the ones the inventory renders, symlinked ones
166
+ # included (decision 0017).
167
+ output_rel_var "$repo_root" "$output_dir"
168
+ source_artifacts_var "$repo_root" "$config_file" skills copilot "$IS_OUTPUT_REL/skills/{}"
169
+ for ((i = 0; i < ${#IS_SOURCE_FILES[@]}; i++)); do
170
+ skill_dirs+=("${IS_SOURCE_FILES[i]%/SKILL.md}/")
171
+ done
187
172
 
188
173
  local count=0
189
174
  if [ "${#skill_dirs[@]}" -gt 0 ]; then
190
175
  # Copilot has no effort field: the copy carries no `effort:`.
191
- copy_skill_bundle_dirs_for copilot "$output_dir/skills" "${skill_dirs[@]}"
192
- for d in "${skill_dirs[@]}"; do
193
- skill_name="${d%/}"
176
+ copy_skill_bundle_dirs_named copilot "$output_dir/skills" "$IS_SOURCE_NAME_MAP" "${skill_dirs[@]}"
177
+ for name in "${IS_SOURCE_NAMES[@]}"; do
194
178
  count=$((count + 1))
195
- echo " skill: ${skill_name##*/}"
179
+ echo " skill: $name"
196
180
  done
197
181
  fi
198
182
 
@@ -205,19 +189,13 @@ sync_copilot_agents() {
205
189
  local config_file="$2"
206
190
  local output_dir="$3"
207
191
 
208
- local src f
209
- local -a files=()
210
- load_yaml_list "$config_file" "agents"
211
- local list="$IS_YAML_LIST"
212
- while IFS= read -r src; do
213
- [ -z "$src" ] && continue
214
- local dir="$repo_root/$src"
215
- [ -d "$dir" ] || continue
216
- for f in "$dir"/*.md; do
217
- [ -f "$f" ] || continue
218
- files+=("$f")
219
- done
220
- done <<< "$list"
192
+ local -a files=() names=()
193
+ output_rel_var "$repo_root" "$output_dir"
194
+ source_artifacts_var "$repo_root" "$config_file" agents copilot "$IS_OUTPUT_REL/agents/{}.agent.md"
195
+ if [ "${#IS_SOURCE_FILES[@]}" -gt 0 ]; then
196
+ files=("${IS_SOURCE_FILES[@]}")
197
+ names=("${IS_SOURCE_NAMES[@]}")
198
+ fi
221
199
 
222
200
  local count=0
223
201
  if [ "${#files[@]}" -gt 0 ]; then
@@ -225,8 +203,7 @@ sync_copilot_agents() {
225
203
 
226
204
  local path tier access description name tools_line spec="" report="" LS=$'\x1e'
227
205
  while IFS=$'\x1f' read -r path tier access description; do
228
- [ -n "$path" ] || continue
229
- name="${path##*/}"; name="${name%.md}"
206
+ name="${names[count]}"
230
207
  resolve_model_var "$tier"
231
208
 
232
209
  # Build tools list based on access. Tool aliases per Copilot custom-agent