@ainova-systems/intelligence 0.20.0 → 0.21.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.
@@ -4,10 +4,13 @@
4
4
  #
5
5
  # Rules:
6
6
  # - Path-scoped (with `paths:`) -> .cursor/rules/<name>.mdc with `globs:`
7
- # - Always-on (no `paths:`) -> SKIPPED here. Cursor reads AGENTS.md
8
- # natively for project-level context, and the `agents` adapter inlines
9
- # always-on rule content into AGENTS.md. Generating .mdc copies of the
10
- # same rules would cause double-loading and burn the context window.
7
+ # - Always-on (no `paths:`) -> SKIPPED here while AGENTS.md carries it.
8
+ # Cursor reads AGENTS.md natively for project-level context, and the
9
+ # `agents` adapter inlines always-on rule content into AGENTS.md.
10
+ # Generating .mdc copies of the same rules would cause double-loading and
11
+ # burn the context window. An always-on rule AGENTS.md does not carry — a
12
+ # local one, or one whose targets leave `agents` out — becomes
13
+ # .cursor/rules/<name>.mdc with `alwaysApply: true` (decision 0021).
11
14
  # Skills: copy skill directories in full (SKILL.md + bundled resources)
12
15
  # Agents: strip tier/access/tools/disallowedTools, add model + readonly:true
13
16
  # Cursor has no per-agent or per-skill effort field (its effort is part of a
@@ -41,45 +44,55 @@ sync_cursor_rules() {
41
44
  local config_file="$2"
42
45
  local output_dir="$3"
43
46
 
44
- local src f
45
- local -a files=()
46
- load_yaml_list "$config_file" "rules"
47
- local list="$IS_YAML_LIST"
48
- while IFS= read -r src; do
49
- [ -z "$src" ] && continue
50
- local dir="$repo_root/$src"
51
- [ -d "$dir" ] || continue
52
- for f in "$dir"/*.md; do
53
- [ -f "$f" ] || continue
54
- files+=("$f")
55
- done
56
- done <<< "$list"
47
+ local i always=""
48
+ local -a files=() names=() kinds=()
49
+ output_rel_var "$repo_root" "$output_dir"
50
+ source_artifacts_var "$repo_root" "$config_file" rules cursor "$IS_OUTPUT_REL/rules/{}.mdc"
51
+ # A scoped rule takes Cursor's own channel. An always-on rule does only
52
+ # when AGENTS.md, which Cursor reads, does not carry it: a local rule, or
53
+ # one whose targets leave `agents` out (decision 0021).
54
+ for ((i = 0; i < ${#IS_SOURCE_FILES[@]}; i++)); do
55
+ if [ "${IS_SOURCE_SCOPED[i]}" = 1 ]; then
56
+ kinds+=(scoped)
57
+ elif [ "${IS_SOURCE_IN_AGENTS[i]}" = 0 ]; then
58
+ kinds+=(always-on)
59
+ always="$always${IS_SOURCE_FILES[i]}"$'\n'
60
+ else
61
+ continue
62
+ fi
63
+ files+=("${IS_SOURCE_FILES[i]}")
64
+ names+=("${IS_SOURCE_NAMES[i]}")
65
+ done
57
66
  [ "${#files[@]}" -gt 0 ] || return 0
58
67
 
59
- # Skip always-on rules: AGENTS.md (canonical) carries them.
60
- local path hp base
61
- local -a scoped=()
62
- while IFS=$'\x1f' read -r path hp; do
63
- [ -n "$path" ] || continue
64
- [ "$hp" -eq 0 ] && continue
65
- scoped+=("$path")
66
- done < <(frontmatter_index "paths#" "${files[@]}")
67
- [ "${#scoped[@]}" -gt 0 ] || return 0
68
-
69
- # Path-scoped rule -> Auto Attached (paths -> globs). The rewrite is
70
- # anchored inside the frontmatter block: a body line may legitimately start
71
- # with `paths:` (a rule documenting rule syntax does exactly that), and
72
- # rewriting it would corrupt the text it teaches.
68
+ # Path-scoped rule -> Auto Attached (paths -> globs); always-on rule ->
69
+ # Always (`alwaysApply: true`), with a frontmatter block of its own when the
70
+ # source has none. The rewrite is anchored inside the frontmatter block: a
71
+ # body line may legitimately start with `paths:` (a rule documenting rule
72
+ # syntax does exactly that), and rewriting it would corrupt the text it
73
+ # teaches.
73
74
  is_fin_awk_vars
74
- awk "${IS_FIN_V[@]}" -v dst="$output_dir/rules" "$IS_AWK_LIB"'
75
+ IS_MDC_ALWAYS="$always" IS_NAME_MAP="$IS_SOURCE_NAME_MAP" awk "${IS_FIN_V[@]}" -v dst="$output_dir/rules" "$IS_AWK_LIB"'
76
+ BEGIN {
77
+ n = split(ENVIRON["IS_MDC_ALWAYS"], al, "\n")
78
+ for (k = 1; k <= n; k++) if (al[k] != "") ALWAYS[al[k]] = 1
79
+ }
75
80
  FNR == 1 {
76
81
  if (out != "") close(out)
77
- nm = base_name(FILENAME)
78
- sub(/\.md$/, ".mdc", nm)
79
- out = dst "/" nm
82
+ out = dst "/" art_name(FILENAME) ".mdc"
80
83
  fm = 0
84
+ tgt_reset()
81
85
  }
82
86
  { sub(/\r$/, "") }
87
+ FNR == 1 && (FILENAME in ALWAYS) {
88
+ print fin_line("---") > out
89
+ print fin_line("alwaysApply: true") > out
90
+ if ($0 == "---") { fm = 1; next }
91
+ print fin_line("---") > out
92
+ fm = 2
93
+ print fin_line($0) > out
94
+ next
95
+ }
83
96
  FNR == 1 {
84
97
  if ($0 == "---") fm = 1
85
98
  print fin_line($0) > out
@@ -87,12 +100,16 @@ sync_cursor_rules() {
87
100
  next
88
101
  }
89
102
  fm == 1 && $0 == "---" { fm = 2; print fin_line($0) > out; next }
103
+ fm == 1 && tgt_drop($0) { next }
90
104
  fm == 1 { sub(/^paths:/, "globs:") }
91
105
  { print fin_line($0) > out }
92
- ' "${scoped[@]}"
93
- for f in "${scoped[@]}"; do
94
- base="${f##*/}"; base="${base%.md}"
95
- echo " rule: $base.mdc (scoped)"
106
+ ' "${files[@]}"
107
+ for ((i = 0; i < ${#files[@]}; i++)); do
108
+ # awk never reads a record from an empty source.
109
+ if [ "${kinds[i]}" = always-on ] && [ ! -s "${files[i]}" ]; then
110
+ printf -- '---\nalwaysApply: true\n---\n' > "$output_dir/rules/${names[i]}.mdc"
111
+ fi
112
+ echo " rule: ${names[i]}.mdc (${kinds[i]})"
96
113
  done
97
114
  }
98
115
 
@@ -102,29 +119,19 @@ sync_cursor_skills() {
102
119
  local config_file="$2"
103
120
  local output_dir="$3"
104
121
 
105
- local src d skill_name
122
+ local i name
106
123
  local -a skill_dirs=()
107
- # The inventory decides which skills render, symlinked ones included
108
- # (decision 0017); the glob keeps this adapter's own listing order.
109
- skill_source_inventory "$repo_root" "$config_file"
110
- load_yaml_list "$config_file" "skills"
111
- local list="$IS_YAML_LIST"
112
- while IFS= read -r src; do
113
- [ -z "$src" ] && continue
114
- local dir="$repo_root/$src"
115
- [ -d "$dir" ] || continue
116
- for d in "$dir"/*/; do
117
- [ -d "$d" ] || continue
118
- [ -f "$d/SKILL.md" ] || continue
119
- skill_source_rendered "$d" || continue
120
- skill_dirs+=("$d")
121
- done
122
- done <<< "$list"
123
- [ "${#skill_dirs[@]}" -gt 0 ] || return 0
124
- copy_skill_bundle_dirs_for cursor "$output_dir/skills" "${skill_dirs[@]}"
125
- for d in "${skill_dirs[@]}"; do
126
- skill_name="${d%/}"
127
- echo " skill: ${skill_name##*/}"
124
+ # The catalog's skills are the ones the inventory renders, symlinked ones
125
+ # included (decision 0017).
126
+ output_rel_var "$repo_root" "$output_dir"
127
+ source_artifacts_var "$repo_root" "$config_file" skills cursor "$IS_OUTPUT_REL/skills/{}"
128
+ [ "${#IS_SOURCE_FILES[@]}" -gt 0 ] || return 0
129
+ for ((i = 0; i < ${#IS_SOURCE_FILES[@]}; i++)); do
130
+ skill_dirs+=("${IS_SOURCE_FILES[i]%/SKILL.md}/")
131
+ done
132
+ copy_skill_bundle_dirs_named cursor "$output_dir/skills" "$IS_SOURCE_NAME_MAP" "${skill_dirs[@]}"
133
+ for name in "${IS_SOURCE_NAMES[@]}"; do
134
+ echo " skill: $name"
128
135
  done
129
136
  }
130
137
 
@@ -134,37 +141,32 @@ sync_cursor_agents() {
134
141
  local config_file="$2"
135
142
  local output_dir="$3"
136
143
 
137
- local src f
138
- local -a files=()
139
- load_yaml_list "$config_file" "agents"
140
- local list="$IS_YAML_LIST"
141
- while IFS= read -r src; do
142
- [ -z "$src" ] && continue
143
- local dir="$repo_root/$src"
144
- [ -d "$dir" ] || continue
145
- for f in "$dir"/*.md; do
146
- [ -f "$f" ] || continue
147
- files+=("$f")
148
- done
149
- done <<< "$list"
150
- [ "${#files[@]}" -gt 0 ] || return 0
144
+ local i
145
+ local -a files=() names=()
146
+ output_rel_var "$repo_root" "$output_dir"
147
+ source_artifacts_var "$repo_root" "$config_file" agents cursor "$IS_OUTPUT_REL/agents/{}.md"
148
+ [ "${#IS_SOURCE_FILES[@]}" -gt 0 ] || return 0
149
+ files=("${IS_SOURCE_FILES[@]}")
150
+ names=("${IS_SOURCE_NAMES[@]}")
151
+ local name_map="$IS_SOURCE_NAME_MAP"
151
152
 
152
153
  load_model_tiers "$config_file" "cursor"
153
154
 
154
155
  local path tier access spec="" report="" cursor_readonly
156
+ i=0
155
157
  while IFS=$'\x1f' read -r path tier access; do
156
- [ -n "$path" ] || continue
157
158
  resolve_model_var "$tier"
158
159
  cursor_readonly=""
159
160
  [ "$access" = "readonly" ] && cursor_readonly="readonly: true"
160
161
  spec+="$path"$'\x1f'"$IS_MODEL"$'\x1f'"$cursor_readonly"$'\n'
161
- report+=" agent: ${path##*/} (tier=$tier -> cursor)"$'\n'
162
+ report+=" agent: ${names[i]}.md (tier=$tier -> cursor)"$'\n'
163
+ i=$((i + 1))
162
164
  done < <(frontmatter_index "tier,access" "${files[@]}")
163
165
 
164
166
  # Always emit model: line (even when value matches Cursor's default)
165
167
  # so config is explicit and grep-able.
166
168
  is_fin_awk_vars
167
- IS_MAP_SPEC="$spec" awk "${IS_FIN_V[@]}" -v dst="$output_dir/agents" "$IS_AWK_LIB"'
169
+ IS_NAME_MAP="$name_map" IS_MAP_SPEC="$spec" awk "${IS_FIN_V[@]}" -v dst="$output_dir/agents" "$IS_AWK_LIB"'
168
170
  BEGIN {
169
171
  US = sprintf("%c", 31)
170
172
  n = split(ENVIRON["IS_MAP_SPEC"], recs, "\n")
@@ -176,9 +178,10 @@ sync_cursor_agents() {
176
178
  }
177
179
  FNR == 1 {
178
180
  if (out != "") close(out)
179
- out = dst "/" base_name(FILENAME)
181
+ out = dst "/" art_file(FILENAME, ".md")
180
182
  count = 0
181
183
  fm = 0
184
+ tgt_reset()
182
185
  }
183
186
  /^tier:/ { next }
184
187
  /^access:/ { next }
@@ -187,6 +190,7 @@ sync_cursor_agents() {
187
190
  { sub(/\r$/, "") }
188
191
  FNR == 1 && $0 == "---" { fm = 1 }
189
192
  /^---$/ { count++ }
193
+ fm && count == 1 && !/^---$/ && tgt_drop($0) { next }
190
194
  fm && count == 1 && substr($0, 1, 7) == "effort:" { next }
191
195
  count == 2 && /^---$/ {
192
196
  print fin_line("model: " MODEL[FILENAME]) > out
@@ -194,8 +198,8 @@ sync_cursor_agents() {
194
198
  }
195
199
  { print fin_line($0) > out }
196
200
  ' "${files[@]}"
197
- for f in "${files[@]}"; do
198
- [ -s "$f" ] || : > "$output_dir/agents/${f##*/}"
201
+ for ((i = 0; i < ${#files[@]}; i++)); do
202
+ [ -s "${files[i]}" ] || : > "$output_dir/agents/${names[i]}.md"
199
203
  done
200
204
  printf '%s' "$report"
201
205
  }
@@ -88,32 +88,24 @@ sync_opencode_commands() {
88
88
  [ "${#generated[@]}" -gt 0 ] && rm -f "${generated[@]}"
89
89
  fi
90
90
 
91
- local src d skill_name
92
- local -a skill_mds=()
93
- # The inventory decides which skills render, symlinked ones included
94
- # (decision 0017); the glob keeps this adapter's own listing order.
95
- skill_source_inventory "$repo_root" "$config_file"
96
- load_yaml_list "$config_file" "skills"
97
- local list="$IS_YAML_LIST"
98
- while IFS= read -r src; do
99
- [ -z "$src" ] && continue
100
- local dir="$repo_root/$src"
101
- [ -d "$dir" ] || continue
102
- for d in "$dir"/*/; do
103
- [ -d "$d" ] || continue
104
- [ -f "$d/SKILL.md" ] || continue
105
- skill_source_rendered "$d" || continue
106
- skill_mds+=("${d%/}/SKILL.md")
107
- done
108
- done <<< "$list"
91
+ local skill_name
92
+ local -a skill_mds=() names=()
93
+ # The catalog's skills are the ones the inventory renders, symlinked ones
94
+ # included (decision 0017). The commands directory is tracked, so a local
95
+ # skill gets no command.
96
+ output_rel_var "$repo_root" "$commands_dir"
97
+ source_artifacts_var "$repo_root" "$config_file" skills opencode "$IS_OUTPUT_REL/{}.md"
98
+ if [ "${#IS_SOURCE_FILES[@]}" -gt 0 ]; then
99
+ skill_mds=("${IS_SOURCE_FILES[@]}")
100
+ names=("${IS_SOURCE_NAMES[@]}")
101
+ fi
109
102
 
110
103
  local count=0
111
104
  if [ "${#skill_mds[@]}" -gt 0 ]; then
112
105
  local path description argument_hint out_file
113
106
  local -a out_files=()
114
107
  while IFS=$'\x1f' read -r path description argument_hint; do
115
- [ -n "$path" ] || continue
116
- skill_name="${path%/SKILL.md}"; skill_name="${skill_name##*/}"
108
+ skill_name="${names[count]}"
117
109
  out_file="$commands_dir/$skill_name.md"
118
110
  yaml_dq_escape_var "$description"
119
111
  {
@@ -151,19 +143,13 @@ sync_opencode_agents() {
151
143
  local agents_dir="$output_dir/agents"
152
144
  mkdir -p "$agents_dir"
153
145
 
154
- local src f
155
- local -a files=()
156
- load_yaml_list "$config_file" "agents"
157
- local list="$IS_YAML_LIST"
158
- while IFS= read -r src; do
159
- [ -z "$src" ] && continue
160
- local dir="$repo_root/$src"
161
- [ -d "$dir" ] || continue
162
- for f in "$dir"/*.md; do
163
- [ -f "$f" ] || continue
164
- files+=("$f")
165
- done
166
- done <<< "$list"
146
+ local -a files=() names=()
147
+ output_rel_var "$repo_root" "$agents_dir"
148
+ source_artifacts_var "$repo_root" "$config_file" agents opencode "$IS_OUTPUT_REL/{}.md"
149
+ if [ "${#IS_SOURCE_FILES[@]}" -gt 0 ]; then
150
+ files=("${IS_SOURCE_FILES[@]}")
151
+ names=("${IS_SOURCE_NAMES[@]}")
152
+ fi
167
153
 
168
154
  local count=0
169
155
  if [ "${#files[@]}" -gt 0 ]; then
@@ -171,8 +157,7 @@ sync_opencode_agents() {
171
157
 
172
158
  local path tier access description name edit_perm bash_perm spec="" report="" LS=$'\x1e'
173
159
  while IFS=$'\x1f' read -r path tier access description; do
174
- [ -n "$path" ] || continue
175
- name="${path##*/}"; name="${name%.md}"
160
+ name="${names[count]}"
176
161
  resolve_model_var "$tier"
177
162
 
178
163
  # Map source `access:` to opencode permission keys.
@@ -4,6 +4,9 @@
4
4
  #
5
5
  # Rules:
6
6
  # - Always-on (no `paths:`) -> SKIPPED here. Pi reads AGENTS.md natively.
7
+ # An always-on rule AGENTS.md does not carry — a local one, or one whose
8
+ # targets leave `agents` out — goes into the generated extension, which
9
+ # adds its body to the system prompt (decision 0021).
7
10
  # - Path-scoped (with `paths:`) -> copied to .pi/intelligence-sync/rules/
8
11
  # and surfaced via a generated Pi extension that lists them in the system
9
12
  # prompt so the model can `read` the relevant rule on demand.
@@ -85,6 +88,39 @@ pi_rule_paths_rows() {
85
88
  ' "$@"
86
89
  }
87
90
 
91
+ # pi_rule_body_rows <file>... — one line per file in argument order: its body,
92
+ # without the frontmatter and leading or trailing blank lines, escaped as the
93
+ # contents of a TypeScript double-quoted string (`\`, `"`, tabs and line breaks
94
+ # escaped).
95
+ pi_rule_body_rows() {
96
+ [ "$#" -gt 0 ] || return 0
97
+ awk "$IS_AWK_LIB"'
98
+ function store( j, first, last, s) {
99
+ if (cur == "") return
100
+ first = 0; last = 0
101
+ for (j = 1; j <= n; j++) if (B[j] != "") { if (!first) first = j; last = j }
102
+ s = ""
103
+ for (j = first; first && j <= last; j++) s = s (j > first ? "\\n" : "") B[j]
104
+ R[cur] = s
105
+ }
106
+ FNR == 1 { store(); cur = FILENAME; n = 0; in_fm = 0; past_fm = 0 }
107
+ { sub(/\r$/, "") }
108
+ FNR == 1 && $0 != "---" { past_fm = 1 }
109
+ /^---$/ && !past_fm { in_fm = !in_fm; if (!in_fm) past_fm = 1; next }
110
+ !past_fm { next }
111
+ {
112
+ line = is_repl($0, "\\", "\\\\")
113
+ line = is_repl(line, "\"", "\\\"")
114
+ line = is_repl(line, "\t", "\\t")
115
+ B[++n] = line
116
+ }
117
+ END {
118
+ store()
119
+ for (i = 1; i < ARGC; i++) print (ARGV[i] in R) ? R[ARGV[i]] : ""
120
+ }
121
+ ' "$@"
122
+ }
123
+
88
124
  sync_pi_rules() {
89
125
  local repo_root="$1"
90
126
  local config_file="$2"
@@ -98,38 +134,44 @@ sync_pi_rules() {
98
134
 
99
135
  mkdir -p "$rules_root" "$(dirname "$extension_file")"
100
136
 
101
- local src f
102
- local -a files=()
103
- load_yaml_list "$config_file" "rules"
104
- local list="$IS_YAML_LIST"
105
- while IFS= read -r src; do
106
- [ -z "$src" ] && continue
107
- local dir="$repo_root/$src"
108
- [ -d "$dir" ] || continue
109
- for f in "$dir"/*.md; do
110
- [ -f "$f" ] || continue
111
- files+=("$f")
112
- done
113
- done <<< "$list"
114
-
115
- local -a scoped=()
116
- if [ "${#files[@]}" -gt 0 ]; then
117
- local path hp
118
- while IFS=$'\x1f' read -r path hp; do
119
- [ -n "$path" ] || continue
120
- [ "$hp" -eq 0 ] && continue
121
- scoped+=("$path")
122
- done < <(frontmatter_index "paths#" "${files[@]}")
137
+ local -a scoped=() scoped_names=() always=() always_names=()
138
+ local k=0 always_rules=""
139
+ output_rel_var "$repo_root" "$rules_root"
140
+ source_artifacts_var "$repo_root" "$config_file" rules pi "$IS_OUTPUT_REL/{}.md"
141
+ while [ "$k" -lt "${#IS_SOURCE_FILES[@]}" ]; do
142
+ if [ "${IS_SOURCE_SCOPED[k]}" = 1 ]; then
143
+ scoped+=("${IS_SOURCE_FILES[k]}")
144
+ scoped_names+=("${IS_SOURCE_NAMES[k]}")
145
+ elif [ "${IS_SOURCE_IN_AGENTS[k]}" = 0 ]; then
146
+ # An always-on rule AGENTS.md does not carry: the extension adds
147
+ # its body to the system prompt (decision 0021).
148
+ always+=("${IS_SOURCE_FILES[k]}")
149
+ always_names+=("${IS_SOURCE_NAMES[k]}")
150
+ fi
151
+ k=$((k + 1))
152
+ done
153
+
154
+ if [ "${#always[@]}" -gt 0 ]; then
155
+ local body
156
+ k=0
157
+ while IFS= read -r body; do
158
+ pi_ts_escape_var "${always_names[k]}"
159
+ always_rules+=" { name: \"$IS_PI_TS_ESCAPED\", body: \"$body\" },"$'\n'
160
+ echo " rule: ${always_names[k]} (always-on, in the extension)"
161
+ k=$((k + 1))
162
+ done < <(pi_rule_body_rows "${always[@]}")
123
163
  fi
124
164
 
125
165
  if [ "${#scoped[@]}" -gt 0 ]; then
126
- finalize_copy_files "$rules_root" "${scoped[@]}"
166
+ finalize_copy_named "$rules_root" "$IS_SOURCE_NAME_MAP" "${scoped[@]}"
127
167
 
128
168
  local -a row_arr
129
169
  local base rel_rule_path patterns_js display sep display_sep pat i
170
+ k=0
130
171
  while IFS=$'\x1f' read -r -a row_arr; do
131
172
  [ "${#row_arr[@]}" -gt 0 ] || continue
132
- base="${row_arr[0]##*/}"
173
+ base="${scoped_names[k]}.md"
174
+ k=$((k + 1))
133
175
  rel_rule_path="$output_rel/intelligence-sync/rules/$base"
134
176
  patterns_js=""
135
177
  display=""
@@ -153,7 +195,64 @@ sync_pi_rules() {
153
195
  done < <(pi_rule_paths_rows "${scoped[@]}")
154
196
  fi
155
197
 
156
- if [ "$count" -gt 0 ]; then
198
+ if [ "${#always[@]}" -gt 0 ]; then
199
+ {
200
+ cat <<'EOF'
201
+ /**
202
+ * Generated by intelligence-sync. Do not edit manually.
203
+ * Adds the always-on project rules AGENTS.md does not carry to the system
204
+ * prompt, and lists path-scoped project rules generated from intelligence/rules/.
205
+ */
206
+
207
+ import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
208
+
209
+ const projectRules: Array<{ name: string; body: string }> = [
210
+ EOF
211
+ printf '%s' "$always_rules"
212
+ cat <<'EOF'
213
+ ];
214
+
215
+ const scopedRules: Array<{ path: string; patterns: string[] }> = [
216
+ EOF
217
+ printf '%s' "$manifest"
218
+ cat <<'EOF'
219
+ ];
220
+
221
+ export default function intelligenceSyncRulesExtension(pi: ExtensionAPI) {
222
+ pi.on("before_agent_start", async (event) => {
223
+ let systemPrompt = event.systemPrompt;
224
+
225
+ if (projectRules.length > 0) {
226
+ systemPrompt += `
227
+
228
+ ## Project Rules
229
+
230
+ ${projectRules.map((rule) => rule.body).join("\n\n")}`;
231
+ }
232
+
233
+ if (scopedRules.length > 0) {
234
+ const rulesList = scopedRules
235
+ .map((rule) => `- ${rule.path} — paths: ${rule.patterns.join(", ")}`)
236
+ .join("\n");
237
+
238
+ systemPrompt += `
239
+
240
+ ## Scoped Project Rules
241
+
242
+ AGENTS.md already carries the always-on project rules. Additional path-scoped rules are available on demand:
243
+
244
+ ${rulesList}
245
+
246
+ When the task touches files that match one of these path patterns, use the read tool to load the relevant rule file before making changes.`;
247
+ }
248
+
249
+ return { systemPrompt };
250
+ });
251
+ }
252
+ EOF
253
+ } > "$extension_file"
254
+ finalize_output_file "$extension_file"
255
+ elif [ "$count" -gt 0 ]; then
157
256
  {
158
257
  cat <<'EOF'
159
258
  /**
@@ -199,7 +298,11 @@ EOF
199
298
  finalize_output_file "$extension_file"
200
299
  fi
201
300
 
202
- echo " -> Rules: $count scoped"
301
+ if [ "${#always[@]}" -gt 0 ]; then
302
+ echo " -> Rules: $count scoped, ${#always[@]} always-on"
303
+ else
304
+ echo " -> Rules: $count scoped"
305
+ fi
203
306
  }
204
307
 
205
308
  sync_pi_skills() {
@@ -223,25 +326,18 @@ sync_pi_agents() {
223
326
 
224
327
  mkdir -p "$prompts_dir"
225
328
 
226
- local src f
227
- local -a files=()
228
- load_yaml_list "$config_file" "agents"
229
- local list="$IS_YAML_LIST"
230
- while IFS= read -r src; do
231
- [ -z "$src" ] && continue
232
- local dir="$repo_root/$src"
233
- [ -d "$dir" ] || continue
234
- for f in "$dir"/*.md; do
235
- [ -f "$f" ] || continue
236
- files+=("$f")
237
- done
238
- done <<< "$list"
329
+ local -a files=() names=()
330
+ output_rel_var "$repo_root" "$prompts_dir"
331
+ source_artifacts_var "$repo_root" "$config_file" agents pi "$IS_OUTPUT_REL/intelligence-agent-{}.md"
332
+ if [ "${#IS_SOURCE_FILES[@]}" -gt 0 ]; then
333
+ files=("${IS_SOURCE_FILES[@]}")
334
+ names=("${IS_SOURCE_NAMES[@]}")
335
+ fi
239
336
 
240
337
  if [ "${#files[@]}" -gt 0 ]; then
241
338
  local path desc access name template_desc spec="" report="" LS=$'\x1e'
242
339
  while IFS=$'\x1f' read -r path desc access; do
243
- [ -n "$path" ] || continue
244
- name="${path##*/}"; name="${name%.md}"
340
+ name="${names[count]}"
245
341
  repo_rel_link_var "$repo_root" "$path"
246
342
 
247
343
  template_desc="Use $name persona"