@ainova-systems/intelligence 0.11.3 → 0.11.5

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.
@@ -11,6 +11,9 @@
11
11
  # copilot-instructions.md is present.
12
12
  # Skills: copy skill directories in full to .github/skills/{name}/
13
13
  # Agents: -> .github/agents/{name}.agent.md (description, tools, model)
14
+ #
15
+ # Every per-file loop batches its work into one awk process (see the batched
16
+ # helpers in lib/common.sh): process spawns dominate sync time on Windows.
14
17
 
15
18
  source "$(dirname "${BASH_SOURCE[0]}")/../lib/common.sh"
16
19
 
@@ -25,6 +28,38 @@ adapter_contract_copilot() {
25
28
  adapter_contract_legacy "$output/copilot-instructions.md"
26
29
  }
27
30
 
31
+ # copilot_paths_rows <file>... — batch form of the old per-file `paths:`
32
+ # joiner: one row per file in argument order, `path \x1f p1,p2,...`. Quotes
33
+ # are stripped anywhere in a pattern and the list is scanned from the first
34
+ # `paths:` line, exactly like the per-file reader did.
35
+ copilot_paths_rows() {
36
+ [ "$#" -gt 0 ] || return 0
37
+ awk '
38
+ BEGIN { US = sprintf("%c", 31) }
39
+ function store() { if (cur != "") R[cur] = row }
40
+ FNR == 1 { store(); cur = FILENAME; row = cur US; sep = ""; done_f = 0; in_paths = 0 }
41
+ { sub(/\r$/, "") }
42
+ done_f { next }
43
+ /^paths:/ { in_paths = 1; next }
44
+ in_paths && /^ - / {
45
+ val = $0
46
+ sub(/^ - /, "", val)
47
+ gsub(/["\047]/, "", val)
48
+ row = row sep val
49
+ sep = ","
50
+ next
51
+ }
52
+ in_paths && !/^ - / && !/^$/ { done_f = 1 }
53
+ END {
54
+ store()
55
+ for (i = 1; i < ARGC; i++) {
56
+ if (ARGV[i] in R) print R[ARGV[i]]
57
+ else print ARGV[i] US
58
+ }
59
+ }
60
+ ' "$@"
61
+ }
62
+
28
63
  # Sync rules to Copilot format. Only path-scoped rules are emitted —
29
64
  # always-on rules live in AGENTS.md (read by Copilot natively).
30
65
  sync_copilot_rules() {
@@ -37,56 +72,50 @@ sync_copilot_rules() {
37
72
 
38
73
  local scoped_count=0
39
74
 
75
+ local src f
76
+ local -a files=()
77
+ load_yaml_list "$config_file" "rules"
78
+ local list="$IS_YAML_LIST"
40
79
  while IFS= read -r src; do
41
80
  [ -z "$src" ] && continue
42
- local dir
43
- dir="$(resolve_source_dir "$repo_root" "$src")"
81
+ local dir="$repo_root/$src"
44
82
  [ -d "$dir" ] || continue
45
83
  for f in "$dir"/*.md; do
46
84
  [ -f "$f" ] || continue
47
- local hp
48
- hp=$(has_paths "$f")
85
+ files+=("$f")
86
+ done
87
+ done <<< "$list"
49
88
 
50
- # Skip always-on rules: AGENTS.md carries them.
89
+ local -a scoped=()
90
+ if [ "${#files[@]}" -gt 0 ]; then
91
+ local path hp
92
+ while IFS=$'\x1f' read -r path hp; do
93
+ [ -n "$path" ] || continue
51
94
  [ "$hp" -eq 0 ] && continue
95
+ scoped+=("$path")
96
+ done < <(frontmatter_index "paths#" "${files[@]}")
97
+ fi
98
+
99
+ if [ "${#scoped[@]}" -gt 0 ]; then
100
+ local base paths_line spec="" report="" LS=$'\x1e'
101
+ while IFS=$'\x1f' read -r path paths_line; do
102
+ [ -n "$path" ] || continue
103
+ base="${path##*/}"; base="${base%.md}"
52
104
 
53
- local base
54
- base="$(basename "$f" .md)"
55
-
56
- local paths_line
57
- paths_line=$(awk '
58
- { sub(/\r$/, "") }
59
- /^paths:/ { in_paths=1; next }
60
- in_paths && /^ - / {
61
- val = $0
62
- sub(/^ - /, "", val)
63
- gsub(/["\047]/, "", val)
64
- printf "%s%s", sep, val
65
- sep = ","
66
- }
67
- in_paths && !/^ - / && !/^$/ { exit }
68
- END { print "" }
69
- ' "$f")
70
-
71
- {
72
- echo "---"
73
- echo "applyTo: \"$paths_line\""
74
- echo "---"
75
- echo ""
76
- awk '
77
- BEGIN { in_fm=0; past_fm=0 }
78
- { sub(/\r$/, "") }
79
- /^---$/ {
80
- if (!past_fm) { in_fm = !in_fm; if (!in_fm) { past_fm=1 }; next }
81
- }
82
- past_fm || !in_fm { print }
83
- ' "$f"
84
- } > "$instructions_dir/$base.instructions.md"
85
- finalize_output_file "$instructions_dir/$base.instructions.md"
105
+ local header
106
+ header="${LS}---"
107
+ header+="${LS}applyTo: \"$paths_line\""
108
+ header+="${LS}---"
109
+ header+="${LS}"
110
+
111
+ spec+="$path"$'\x1f'"$instructions_dir/$base.instructions.md"$'\x1f'"fence_nofm"$'\x1f'"0"$'\x1f'"none"$'\x1f'"$header"$'\x1f\n'
112
+ report+=" rule: $base.instructions.md (scoped)"$'\n'
86
113
  scoped_count=$((scoped_count + 1))
87
- echo " rule: $base.instructions.md (scoped)"
88
- done
89
- done < <(read_yaml_list "$config_file" "rules")
114
+ done < <(copilot_paths_rows "${scoped[@]}")
115
+
116
+ emit_wrapped_bodies "$spec"
117
+ printf '%s' "$report"
118
+ fi
90
119
 
91
120
  echo " rules: $scoped_count scoped"
92
121
  }
@@ -97,22 +126,30 @@ sync_copilot_skills() {
97
126
  local config_file="$2"
98
127
  local output_dir="$3"
99
128
 
100
- local count=0
129
+ local src d skill_name
130
+ local -a skill_dirs=()
131
+ load_yaml_list "$config_file" "skills"
132
+ local list="$IS_YAML_LIST"
101
133
  while IFS= read -r src; do
102
134
  [ -z "$src" ] && continue
103
- local dir
104
- dir="$(resolve_source_dir "$repo_root" "$src")"
135
+ local dir="$repo_root/$src"
105
136
  [ -d "$dir" ] || continue
106
137
  for d in "$dir"/*/; do
107
138
  [ -d "$d" ] || continue
108
- local skill_name
109
- skill_name="$(basename "$d")"
110
139
  [ -f "$d/SKILL.md" ] || continue
111
- copy_skill_bundle "$d" "$output_dir/skills/$skill_name"
140
+ skill_dirs+=("$d")
141
+ done
142
+ done <<< "$list"
143
+
144
+ local count=0
145
+ if [ "${#skill_dirs[@]}" -gt 0 ]; then
146
+ copy_skill_bundle_dirs "$output_dir/skills" "${skill_dirs[@]}"
147
+ for d in "${skill_dirs[@]}"; do
148
+ skill_name="${d%/}"
112
149
  count=$((count + 1))
113
- echo " skill: $skill_name"
150
+ echo " skill: ${skill_name##*/}"
114
151
  done
115
- done < <(read_yaml_list "$config_file" "skills")
152
+ fi
116
153
 
117
154
  echo " -> Skills: $count"
118
155
  }
@@ -123,59 +160,55 @@ sync_copilot_agents() {
123
160
  local config_file="$2"
124
161
  local output_dir="$3"
125
162
 
126
- local count=0
163
+ local src f
164
+ local -a files=()
165
+ load_yaml_list "$config_file" "agents"
166
+ local list="$IS_YAML_LIST"
127
167
  while IFS= read -r src; do
128
168
  [ -z "$src" ] && continue
129
- local dir
130
- dir="$(resolve_source_dir "$repo_root" "$src")"
169
+ local dir="$repo_root/$src"
131
170
  [ -d "$dir" ] || continue
132
171
  for f in "$dir"/*.md; do
133
172
  [ -f "$f" ] || continue
134
- local name
135
- name="$(basename "$f" .md)"
173
+ files+=("$f")
174
+ done
175
+ done <<< "$list"
136
176
 
137
- local tier access description
138
- tier=$(get_frontmatter_value "tier" "$f")
139
- access=$(get_frontmatter_value "access" "$f")
140
- description=$(get_frontmatter_value "description" "$f")
177
+ local count=0
178
+ if [ "${#files[@]}" -gt 0 ]; then
179
+ load_model_tiers "$config_file" "copilot"
141
180
 
142
- local model
143
- model=$(get_model "$config_file" "copilot" "$tier")
181
+ local path tier access description name tools_line spec="" report="" LS=$'\x1e'
182
+ while IFS=$'\x1f' read -r path tier access description; do
183
+ [ -n "$path" ] || continue
184
+ name="${path##*/}"; name="${name%.md}"
185
+ resolve_model_var "$tier"
144
186
 
145
187
  # Build tools list based on access. Tool aliases per Copilot custom-agent
146
188
  # spec: `read`, `search`, `edit`, `execute`, `agent`, `web`, `todo`, `*`.
147
189
  # Omitting `tools:` grants all; restrict to read/search for readonly access.
148
- local tools_line=""
190
+ tools_line=""
149
191
  if [ "$access" = "readonly" ]; then
150
192
  tools_line="tools: [\"read\", \"search\"]"
151
193
  fi
152
194
 
153
- local description_escaped
154
- description_escaped=$(yaml_dq_escape "$description")
155
-
156
- # Generate .agent.md: replace tier/access with description/model/tools
157
- {
158
- echo "---"
159
- echo "description: \"$description_escaped\""
160
- echo "model: $model"
161
- [ -n "$tools_line" ] && echo "$tools_line"
162
- echo "---"
163
- echo ""
164
- # Strip original frontmatter, keep body
165
- awk '
166
- BEGIN { in_fm=0; past_fm=0 }
167
- { sub(/\r$/, "") }
168
- /^---$/ {
169
- if (!past_fm) { in_fm = !in_fm; if (!in_fm) { past_fm=1 }; next }
170
- }
171
- past_fm { print }
172
- ' "$f"
173
- } > "$output_dir/agents/$name.agent.md"
174
- finalize_output_file "$output_dir/agents/$name.agent.md"
195
+ local header
196
+ yaml_dq_escape_var "$description"
197
+ header="${LS}---"
198
+ header+="${LS}description: \"$IS_YAML_ESCAPED\""
199
+ header+="${LS}model: $IS_MODEL"
200
+ [ -n "$tools_line" ] && header+="${LS}$tools_line"
201
+ header+="${LS}---"
202
+ header+="${LS}"
203
+
204
+ spec+="$path"$'\x1f'"$output_dir/agents/$name.agent.md"$'\x1f'"fence"$'\x1f'"0"$'\x1f'"none"$'\x1f'"$header"$'\x1f\n'
205
+ report+=" agent: $name.agent.md (tier=$tier -> model=$IS_MODEL)"$'\n'
175
206
  count=$((count + 1))
176
- echo " agent: $name.agent.md (tier=$tier -> model=$model)"
177
- done
178
- done < <(read_yaml_list "$config_file" "agents")
207
+ done < <(frontmatter_index "tier,access,description" "${files[@]}")
208
+
209
+ emit_wrapped_bodies "$spec"
210
+ printf '%s' "$report"
211
+ fi
179
212
 
180
213
  echo " -> Agents: $count"
181
214
  }
@@ -191,9 +224,7 @@ sync_to_copilot() {
191
224
  # Clean generated content (preserve workflows, etc.)
192
225
  rm -rf "$output_dir/instructions" "$output_dir/prompts"
193
226
  if [ -d "$output_dir/skills" ]; then
194
- find "$output_dir/skills" -mindepth 1 -maxdepth 1 -type d | while read -r d; do
195
- rm -rf "$d"
196
- done
227
+ find "$output_dir/skills" -mindepth 1 -maxdepth 1 -type d -exec rm -rf {} +
197
228
  fi
198
229
  rm -rf "$output_dir/agents"
199
230
  mkdir -p "$output_dir" "$output_dir/skills" "$output_dir/agents"
@@ -10,6 +10,9 @@
10
10
  # same rules would cause double-loading and burn the context window.
11
11
  # Skills: copy skill directories in full (SKILL.md + bundled resources)
12
12
  # Agents: strip tier/access/tools/disallowedTools, add model + readonly:true
13
+ #
14
+ # Every per-file loop batches its work into one awk process (see the batched
15
+ # helpers in lib/common.sh): process spawns dominate sync time on Windows.
13
16
 
14
17
  source "$(dirname "${BASH_SOURCE[0]}")/../lib/common.sh"
15
18
 
@@ -34,37 +37,51 @@ sync_cursor_rules() {
34
37
  local config_file="$2"
35
38
  local output_dir="$3"
36
39
 
40
+ local src f
41
+ local -a files=()
42
+ load_yaml_list "$config_file" "rules"
43
+ local list="$IS_YAML_LIST"
37
44
  while IFS= read -r src; do
38
45
  [ -z "$src" ] && continue
39
- local dir
40
- dir="$(resolve_source_dir "$repo_root" "$src")"
46
+ local dir="$repo_root/$src"
41
47
  [ -d "$dir" ] || continue
42
48
  for f in "$dir"/*.md; do
43
49
  [ -f "$f" ] || continue
44
- local hp
45
- hp=$(has_paths "$f")
46
-
47
- # Skip always-on rules: AGENTS.md (canonical) carries them.
48
- if [ "$hp" -eq 0 ]; then
49
- continue
50
- fi
51
-
52
- local base
53
- base="$(basename "$f" .md)"
54
-
55
- # Path-scoped rule -> Auto Attached (paths -> globs)
56
- awk '
57
- {
58
- sub(/\r$/, "")
59
- sub(/^paths:/, "globs:")
60
- }
61
- NR==1 { print; print "alwaysApply: false"; next }
62
- { print }
63
- ' "$f" > "$output_dir/rules/$base.mdc"
64
- finalize_output_file "$output_dir/rules/$base.mdc"
65
- echo " rule: $base.mdc (scoped)"
50
+ files+=("$f")
66
51
  done
67
- done < <(read_yaml_list "$config_file" "rules")
52
+ done <<< "$list"
53
+ [ "${#files[@]}" -gt 0 ] || return 0
54
+
55
+ # Skip always-on rules: AGENTS.md (canonical) carries them.
56
+ local path hp base
57
+ local -a scoped=()
58
+ while IFS=$'\x1f' read -r path hp; do
59
+ [ -n "$path" ] || continue
60
+ [ "$hp" -eq 0 ] && continue
61
+ scoped+=("$path")
62
+ done < <(frontmatter_index "paths#" "${files[@]}")
63
+ [ "${#scoped[@]}" -gt 0 ] || return 0
64
+
65
+ # Path-scoped rule -> Auto Attached (paths -> globs)
66
+ is_fin_awk_vars
67
+ awk "${IS_FIN_V[@]}" -v dst="$output_dir/rules" "$IS_AWK_LIB"'
68
+ FNR == 1 {
69
+ if (out != "") close(out)
70
+ nm = base_name(FILENAME)
71
+ sub(/\.md$/, ".mdc", nm)
72
+ out = dst "/" nm
73
+ }
74
+ {
75
+ sub(/\r$/, "")
76
+ sub(/^paths:/, "globs:")
77
+ }
78
+ FNR == 1 { print fin_line($0) > out; print fin_line("alwaysApply: false") > out; next }
79
+ { print fin_line($0) > out }
80
+ ' "${scoped[@]}"
81
+ for f in "${scoped[@]}"; do
82
+ base="${f##*/}"; base="${base%.md}"
83
+ echo " rule: $base.mdc (scoped)"
84
+ done
68
85
  }
69
86
 
70
87
  # Sync skills to Cursor format (copy as-is)
@@ -73,20 +90,26 @@ sync_cursor_skills() {
73
90
  local config_file="$2"
74
91
  local output_dir="$3"
75
92
 
93
+ local src d skill_name
94
+ local -a skill_dirs=()
95
+ load_yaml_list "$config_file" "skills"
96
+ local list="$IS_YAML_LIST"
76
97
  while IFS= read -r src; do
77
98
  [ -z "$src" ] && continue
78
- local dir
79
- dir="$(resolve_source_dir "$repo_root" "$src")"
99
+ local dir="$repo_root/$src"
80
100
  [ -d "$dir" ] || continue
81
101
  for d in "$dir"/*/; do
82
102
  [ -d "$d" ] || continue
83
- local skill_name
84
- skill_name="$(basename "$d")"
85
103
  [ -f "$d/SKILL.md" ] || continue
86
- copy_skill_bundle "$d" "$output_dir/skills/$skill_name"
87
- echo " skill: $skill_name"
104
+ skill_dirs+=("$d")
88
105
  done
89
- done < <(read_yaml_list "$config_file" "skills")
106
+ done <<< "$list"
107
+ [ "${#skill_dirs[@]}" -gt 0 ] || return 0
108
+ copy_skill_bundle_dirs "$output_dir/skills" "${skill_dirs[@]}"
109
+ for d in "${skill_dirs[@]}"; do
110
+ skill_name="${d%/}"
111
+ echo " skill: ${skill_name##*/}"
112
+ done
90
113
  }
91
114
 
92
115
  # Sync agents to Cursor format
@@ -95,47 +118,67 @@ sync_cursor_agents() {
95
118
  local config_file="$2"
96
119
  local output_dir="$3"
97
120
 
121
+ local src f
122
+ local -a files=()
123
+ load_yaml_list "$config_file" "agents"
124
+ local list="$IS_YAML_LIST"
98
125
  while IFS= read -r src; do
99
126
  [ -z "$src" ] && continue
100
- local dir
101
- dir="$(resolve_source_dir "$repo_root" "$src")"
127
+ local dir="$repo_root/$src"
102
128
  [ -d "$dir" ] || continue
103
129
  for f in "$dir"/*.md; do
104
130
  [ -f "$f" ] || continue
105
- local name
106
- name="$(basename "$f")"
107
-
108
- local tier access
109
- tier=$(get_frontmatter_value "tier" "$f")
110
- access=$(get_frontmatter_value "access" "$f")
111
-
112
- local cursor_model cursor_readonly
113
- cursor_model=$(get_model "$config_file" "cursor" "$tier")
114
- cursor_readonly=""
115
- [ "$access" = "readonly" ] && cursor_readonly="readonly: true"
116
-
117
- # Always emit model: line (even when value matches Cursor's default)
118
- # so config is explicit and grep-able.
119
- awk -v cm="$cursor_model" -v cr="$cursor_readonly" '
120
- BEGIN { count=0 }
121
- /^tier:/ { next }
122
- /^access:/ { next }
123
- /^tools:/ { next }
124
- /^disallowedTools:/ { next }
125
- {
126
- sub(/\r$/, "")
127
- }
128
- /^---$/ { count++ }
129
- count==2 && /^---$/ {
130
- print "model: " cm
131
- if (cr != "") print cr
132
- }
133
- { print }
134
- ' "$f" > "$output_dir/agents/$name"
135
- finalize_output_file "$output_dir/agents/$name"
136
- echo " agent: $name (tier=$tier -> cursor)"
131
+ files+=("$f")
137
132
  done
138
- done < <(read_yaml_list "$config_file" "agents")
133
+ done <<< "$list"
134
+ [ "${#files[@]}" -gt 0 ] || return 0
135
+
136
+ load_model_tiers "$config_file" "cursor"
137
+
138
+ local path tier access spec="" report="" cursor_readonly
139
+ while IFS=$'\x1f' read -r path tier access; do
140
+ [ -n "$path" ] || continue
141
+ resolve_model_var "$tier"
142
+ cursor_readonly=""
143
+ [ "$access" = "readonly" ] && cursor_readonly="readonly: true"
144
+ spec+="$path"$'\x1f'"$IS_MODEL"$'\x1f'"$cursor_readonly"$'\n'
145
+ report+=" agent: ${path##*/} (tier=$tier -> cursor)"$'\n'
146
+ done < <(frontmatter_index "tier,access" "${files[@]}")
147
+
148
+ # Always emit model: line (even when value matches Cursor's default)
149
+ # so config is explicit and grep-able.
150
+ is_fin_awk_vars
151
+ IS_MAP_SPEC="$spec" awk "${IS_FIN_V[@]}" -v dst="$output_dir/agents" "$IS_AWK_LIB"'
152
+ BEGIN {
153
+ US = sprintf("%c", 31)
154
+ n = split(ENVIRON["IS_MAP_SPEC"], recs, "\n")
155
+ for (i = 1; i <= n; i++) {
156
+ if (recs[i] == "") continue
157
+ split(recs[i], f, US)
158
+ MODEL[f[1]] = f[2]; RO[f[1]] = f[3]
159
+ }
160
+ }
161
+ FNR == 1 {
162
+ if (out != "") close(out)
163
+ out = dst "/" base_name(FILENAME)
164
+ count = 0
165
+ }
166
+ /^tier:/ { next }
167
+ /^access:/ { next }
168
+ /^tools:/ { next }
169
+ /^disallowedTools:/ { next }
170
+ { sub(/\r$/, "") }
171
+ /^---$/ { count++ }
172
+ count == 2 && /^---$/ {
173
+ print fin_line("model: " MODEL[FILENAME]) > out
174
+ if (RO[FILENAME] != "") print fin_line(RO[FILENAME]) > out
175
+ }
176
+ { print fin_line($0) > out }
177
+ ' "${files[@]}"
178
+ for f in "${files[@]}"; do
179
+ [ -s "$f" ] || : > "$output_dir/agents/${f##*/}"
180
+ done
181
+ printf '%s' "$report"
139
182
  }
140
183
 
141
184
  # Main entry point for Cursor adapter