@ainova-systems/intelligence 0.19.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.
Files changed (41) hide show
  1. package/cli/commands/init.sh +2 -1
  2. package/cli/commands/registry.sh +1 -1
  3. package/cli/commands/source.sh +1 -1
  4. package/cli/commands/sync.sh +78 -6
  5. package/cli/intelligence +15 -2
  6. package/cli/internal/check.sh +39 -0
  7. package/cli/internal/convert-legacy.sh +1 -1
  8. package/cli/internal/package-search.sh +1 -1
  9. package/cli/internal/package-update.sh +2 -2
  10. package/cli/internal/restore.sh +3 -2
  11. package/cli/lib/cli-common.sh +145 -33
  12. package/cli/lib/gitignore.sh +19 -2
  13. package/cli/lib/lockfile.sh +14 -4
  14. package/cli/lib/manifest.sh +147 -1
  15. package/cli/lib/qmap.awk +97 -32
  16. package/cli/lib/registry.sh +1 -1
  17. package/cli/lib/sync-cache.sh +596 -142
  18. package/cli/lib/sync-lock.sh +19 -2
  19. package/engine/ENGINE_SHA +1 -1
  20. package/engine/VERSION +1 -1
  21. package/engine/adapters/_template.sh +32 -8
  22. package/engine/adapters/agents.sh +47 -52
  23. package/engine/adapters/antigravity.sh +71 -53
  24. package/engine/adapters/claude.sh +42 -57
  25. package/engine/adapters/codex.sh +77 -16
  26. package/engine/adapters/copilot.sh +54 -65
  27. package/engine/adapters/cursor.sh +88 -78
  28. package/engine/adapters/opencode.sh +23 -32
  29. package/engine/adapters/pi.sh +140 -42
  30. package/engine/lib/adapter-contract.sh +36 -4
  31. package/engine/lib/common.sh +1388 -165
  32. package/engine/lib/contract.sh +5 -0
  33. package/engine/sync.sh +145 -38
  34. package/package.json +1 -1
  35. package/packages/sync/references/adapters.md +48 -38
  36. package/packages/sync/references/conventions.md +61 -12
  37. package/packages/sync/references/onboarding-migration.md +4 -4
  38. package/packages/sync/rules/intelligence-authoring.md +2 -2
  39. package/packages/sync/skills/intelligence-learn-from-repository/SKILL.md +7 -5
  40. package/packages/sync/skills/intelligence-review-context/references/audit-checks.md +1 -1
  41. package/packages/sync/skills/intelligence-update-context/references/rules.md +5 -0
@@ -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,
@@ -16,7 +23,9 @@
16
23
  # Every per-file loop batches its work into one awk process (see the batched
17
24
  # helpers in lib/common.sh): process spawns dominate sync time on Windows.
18
25
 
19
- source "$(dirname "${BASH_SOURCE[0]}")/../lib/common.sh"
26
+ # The engine and the CLI load the shared library before any adapter. Only a
27
+ # standalone source of this file loads it here: that costs a dirname process.
28
+ declare -F finalize_output_file >/dev/null 2>&1 || source "$(dirname "${BASH_SOURCE[0]}")/../lib/common.sh"
20
29
 
21
30
  adapter_contract_codex() {
22
31
  local output="${1%/}"
@@ -24,8 +33,61 @@ adapter_contract_codex() {
24
33
  adapter_contract_requires agents
25
34
  adapter_contract_managed ".agents/skills"
26
35
  adapter_contract_owned "$output/agents"
36
+ # Marker-managed: a hand-written override is left alone.
37
+ adapter_contract_managed "AGENTS.override.md"
27
38
  adapter_contract_ignore ".agents/skills/"
28
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"
29
91
  }
30
92
 
31
93
  # Codex-specific guard for its documented project-instruction byte budget.
@@ -50,6 +112,8 @@ codex_warn_project_doc_size() {
50
112
  target_output_var "$config_file" "agents"
51
113
  local output
52
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"
53
117
 
54
118
  local output_file="$repo_root/$output"
55
119
  [ -f "$output_file" ] || return 0
@@ -81,19 +145,13 @@ sync_codex_agents() {
81
145
  local config_file="$2"
82
146
  local output_dir="$3"
83
147
 
84
- local src f
85
- local -a files=()
86
- load_yaml_list "$config_file" "agents"
87
- local list="$IS_YAML_LIST"
88
- while IFS= read -r src; do
89
- [ -z "$src" ] && continue
90
- local dir="$repo_root/$src"
91
- [ -d "$dir" ] || continue
92
- for f in "$dir"/*.md; do
93
- [ -f "$f" ] || continue
94
- files+=("$f")
95
- done
96
- 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
97
155
 
98
156
  local count=0
99
157
  if [ "${#files[@]}" -gt 0 ]; then
@@ -101,8 +159,7 @@ sync_codex_agents() {
101
159
 
102
160
  local path tier access description effort name sandbox spec="" report="" LS=$'\x1e'
103
161
  while IFS=$'\x1f' read -r path tier access description effort; do
104
- [ -n "$path" ] || continue
105
- name="${path##*/}"; name="${name%.md}"
162
+ name="${names[count]}"
106
163
  resolve_model_var "$tier"
107
164
  map_effort_var codex "$effort"
108
165
  case "$access" in
@@ -153,5 +210,9 @@ sync_to_codex() {
153
210
  mkdir -p "$agents_dir"
154
211
  sync_codex_agents "$repo_root" "$config_file" "$agents_dir"
155
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
+
156
217
  codex_warn_project_doc_size "$repo_root" "$config_file"
157
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
  #
@@ -22,7 +24,9 @@
22
24
  # Every per-file loop batches its work into one awk process (see the batched
23
25
  # helpers in lib/common.sh): process spawns dominate sync time on Windows.
24
26
 
25
- source "$(dirname "${BASH_SOURCE[0]}")/../lib/common.sh"
27
+ # The engine and the CLI load the shared library before any adapter. Only a
28
+ # standalone source of this file loads it here: that costs a dirname process.
29
+ declare -F finalize_output_file >/dev/null 2>&1 || source "$(dirname "${BASH_SOURCE[0]}")/../lib/common.sh"
26
30
 
27
31
  adapter_contract_copilot() {
28
32
  local output="${1%/}" record=adapter_contract_ignore sub
@@ -98,8 +102,8 @@ copilot_paths_rows() {
98
102
  ' "$@"
99
103
  }
100
104
 
101
- # Sync rules to Copilot format. Only path-scoped rules are emitted —
102
- # always-on rules live in AGENTS.md (read by Copilot natively).
105
+ # Sync rules to Copilot format. Path-scoped rules are emitted, and an always-on
106
+ # rule only when AGENTS.md (read by Copilot natively) does not carry it.
103
107
  sync_copilot_rules() {
104
108
  local repo_root="$1"
105
109
  local config_file="$2"
@@ -110,35 +114,28 @@ sync_copilot_rules() {
110
114
 
111
115
  local scoped_count=0
112
116
 
113
- local src f
114
- local -a files=()
115
- load_yaml_list "$config_file" "rules"
116
- local list="$IS_YAML_LIST"
117
- while IFS= read -r src; do
118
- [ -z "$src" ] && continue
119
- local dir="$repo_root/$src"
120
- [ -d "$dir" ] || continue
121
- for f in "$dir"/*.md; do
122
- [ -f "$f" ] || continue
123
- files+=("$f")
124
- done
125
- done <<< "$list"
126
-
127
- local -a scoped=()
128
- if [ "${#files[@]}" -gt 0 ]; then
129
- local path hp
130
- while IFS=$'\x1f' read -r path hp; do
131
- [ -n "$path" ] || continue
132
- [ "$hp" -eq 0 ] && continue
133
- scoped+=("$path")
134
- done < <(frontmatter_index "paths#" "${files[@]}")
135
- fi
117
+ local i always_count=0 spec="" report="" LS=$'\x1e'
118
+ local -a scoped=() scoped_names=()
119
+ output_rel_var "$repo_root" "$instructions_dir"
120
+ source_artifacts_var "$repo_root" "$config_file" rules copilot "$IS_OUTPUT_REL/{}.instructions.md"
121
+ for ((i = 0; i < ${#IS_SOURCE_FILES[@]}; i++)); do
122
+ if [ "${IS_SOURCE_SCOPED[i]}" = 1 ]; then
123
+ scoped+=("${IS_SOURCE_FILES[i]}")
124
+ scoped_names+=("${IS_SOURCE_NAMES[i]}")
125
+ elif [ "${IS_SOURCE_IN_AGENTS[i]}" = 0 ]; then
126
+ # An always-on rule AGENTS.md does not carry applies everywhere.
127
+ 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'
128
+ report+=" rule: ${IS_SOURCE_NAMES[i]}.instructions.md (always-on)"$'\n'
129
+ always_count=$((always_count + 1))
130
+ fi
131
+ done
136
132
 
137
133
  if [ "${#scoped[@]}" -gt 0 ]; then
138
- local base paths_line spec="" report="" LS=$'\x1e'
134
+ local path base paths_line
135
+ i=0
139
136
  while IFS=$'\x1f' read -r path paths_line; do
140
- [ -n "$path" ] || continue
141
- base="${path##*/}"; base="${base%.md}"
137
+ base="${scoped_names[i]}"
138
+ i=$((i + 1))
142
139
 
143
140
  local header
144
141
  header="${LS}---"
@@ -150,12 +147,17 @@ sync_copilot_rules() {
150
147
  report+=" rule: $base.instructions.md (scoped)"$'\n'
151
148
  scoped_count=$((scoped_count + 1))
152
149
  done < <(copilot_paths_rows "${scoped[@]}")
153
-
150
+ fi
151
+ if [ -n "$spec" ]; then
154
152
  emit_wrapped_bodies "$spec"
155
153
  printf '%s' "$report"
156
154
  fi
157
155
 
158
- echo " rules: $scoped_count scoped"
156
+ if [ "$always_count" -gt 0 ]; then
157
+ echo " rules: $scoped_count scoped, $always_count always-on"
158
+ else
159
+ echo " rules: $scoped_count scoped"
160
+ fi
159
161
  }
160
162
 
161
163
  # Sync skills to Copilot format (SKILL.md in .github/skills/)
@@ -164,29 +166,23 @@ sync_copilot_skills() {
164
166
  local config_file="$2"
165
167
  local output_dir="$3"
166
168
 
167
- local src d skill_name
169
+ local i name
168
170
  local -a skill_dirs=()
169
- load_yaml_list "$config_file" "skills"
170
- local list="$IS_YAML_LIST"
171
- while IFS= read -r src; do
172
- [ -z "$src" ] && continue
173
- local dir="$repo_root/$src"
174
- [ -d "$dir" ] || continue
175
- for d in "$dir"/*/; do
176
- [ -d "$d" ] || continue
177
- [ -f "$d/SKILL.md" ] || continue
178
- skill_dirs+=("$d")
179
- done
180
- done <<< "$list"
171
+ # The catalog's skills are the ones the inventory renders, symlinked ones
172
+ # included (decision 0017).
173
+ output_rel_var "$repo_root" "$output_dir"
174
+ source_artifacts_var "$repo_root" "$config_file" skills copilot "$IS_OUTPUT_REL/skills/{}"
175
+ for ((i = 0; i < ${#IS_SOURCE_FILES[@]}; i++)); do
176
+ skill_dirs+=("${IS_SOURCE_FILES[i]%/SKILL.md}/")
177
+ done
181
178
 
182
179
  local count=0
183
180
  if [ "${#skill_dirs[@]}" -gt 0 ]; then
184
181
  # Copilot has no effort field: the copy carries no `effort:`.
185
- copy_skill_bundle_dirs_for copilot "$output_dir/skills" "${skill_dirs[@]}"
186
- for d in "${skill_dirs[@]}"; do
187
- skill_name="${d%/}"
182
+ copy_skill_bundle_dirs_named copilot "$output_dir/skills" "$IS_SOURCE_NAME_MAP" "${skill_dirs[@]}"
183
+ for name in "${IS_SOURCE_NAMES[@]}"; do
188
184
  count=$((count + 1))
189
- echo " skill: ${skill_name##*/}"
185
+ echo " skill: $name"
190
186
  done
191
187
  fi
192
188
 
@@ -199,19 +195,13 @@ sync_copilot_agents() {
199
195
  local config_file="$2"
200
196
  local output_dir="$3"
201
197
 
202
- local src f
203
- local -a files=()
204
- load_yaml_list "$config_file" "agents"
205
- local list="$IS_YAML_LIST"
206
- while IFS= read -r src; do
207
- [ -z "$src" ] && continue
208
- local dir="$repo_root/$src"
209
- [ -d "$dir" ] || continue
210
- for f in "$dir"/*.md; do
211
- [ -f "$f" ] || continue
212
- files+=("$f")
213
- done
214
- done <<< "$list"
198
+ local -a files=() names=()
199
+ output_rel_var "$repo_root" "$output_dir"
200
+ source_artifacts_var "$repo_root" "$config_file" agents copilot "$IS_OUTPUT_REL/agents/{}.agent.md"
201
+ if [ "${#IS_SOURCE_FILES[@]}" -gt 0 ]; then
202
+ files=("${IS_SOURCE_FILES[@]}")
203
+ names=("${IS_SOURCE_NAMES[@]}")
204
+ fi
215
205
 
216
206
  local count=0
217
207
  if [ "${#files[@]}" -gt 0 ]; then
@@ -219,8 +209,7 @@ sync_copilot_agents() {
219
209
 
220
210
  local path tier access description name tools_line spec="" report="" LS=$'\x1e'
221
211
  while IFS=$'\x1f' read -r path tier access description; do
222
- [ -n "$path" ] || continue
223
- name="${path##*/}"; name="${name%.md}"
212
+ name="${names[count]}"
224
213
  resolve_model_var "$tier"
225
214
 
226
215
  # Build tools list based on access. Tool aliases per Copilot custom-agent
@@ -268,7 +257,7 @@ sync_to_copilot() {
268
257
  # Clean generated content (preserve workflows, etc.)
269
258
  rm -rf "$output_dir/instructions" "$output_dir/prompts"
270
259
  if [ -d "$output_dir/skills" ]; then
271
- find "$output_dir/skills" -mindepth 1 -maxdepth 1 -type d -exec rm -rf {} +
260
+ find "$output_dir/skills" -mindepth 1 -maxdepth 1 \( -type d -o -type l \) -exec rm -rf {} +
272
261
  fi
273
262
  rm -rf "$output_dir/agents"
274
263
  mkdir -p "$output_dir" "$output_dir/skills" "$output_dir/agents"
@@ -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
@@ -16,7 +19,9 @@
16
19
  # Every per-file loop batches its work into one awk process (see the batched
17
20
  # helpers in lib/common.sh): process spawns dominate sync time on Windows.
18
21
 
19
- source "$(dirname "${BASH_SOURCE[0]}")/../lib/common.sh"
22
+ # The engine and the CLI load the shared library before any adapter. Only a
23
+ # standalone source of this file loads it here: that costs a dirname process.
24
+ declare -F finalize_output_file >/dev/null 2>&1 || source "$(dirname "${BASH_SOURCE[0]}")/../lib/common.sh"
20
25
 
21
26
  adapter_contract_cursor() {
22
27
  local output="${1%/}"
@@ -39,45 +44,55 @@ sync_cursor_rules() {
39
44
  local config_file="$2"
40
45
  local output_dir="$3"
41
46
 
42
- local src f
43
- local -a files=()
44
- load_yaml_list "$config_file" "rules"
45
- local list="$IS_YAML_LIST"
46
- while IFS= read -r src; do
47
- [ -z "$src" ] && continue
48
- local dir="$repo_root/$src"
49
- [ -d "$dir" ] || continue
50
- for f in "$dir"/*.md; do
51
- [ -f "$f" ] || continue
52
- files+=("$f")
53
- done
54
- 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
55
66
  [ "${#files[@]}" -gt 0 ] || return 0
56
67
 
57
- # Skip always-on rules: AGENTS.md (canonical) carries them.
58
- local path hp base
59
- local -a scoped=()
60
- while IFS=$'\x1f' read -r path hp; do
61
- [ -n "$path" ] || continue
62
- [ "$hp" -eq 0 ] && continue
63
- scoped+=("$path")
64
- done < <(frontmatter_index "paths#" "${files[@]}")
65
- [ "${#scoped[@]}" -gt 0 ] || return 0
66
-
67
- # Path-scoped rule -> Auto Attached (paths -> globs). The rewrite is
68
- # anchored inside the frontmatter block: a body line may legitimately start
69
- # with `paths:` (a rule documenting rule syntax does exactly that), and
70
- # 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.
71
74
  is_fin_awk_vars
72
- 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
+ }
73
80
  FNR == 1 {
74
81
  if (out != "") close(out)
75
- nm = base_name(FILENAME)
76
- sub(/\.md$/, ".mdc", nm)
77
- out = dst "/" nm
82
+ out = dst "/" art_name(FILENAME) ".mdc"
78
83
  fm = 0
84
+ tgt_reset()
79
85
  }
80
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
+ }
81
96
  FNR == 1 {
82
97
  if ($0 == "---") fm = 1
83
98
  print fin_line($0) > out
@@ -85,12 +100,16 @@ sync_cursor_rules() {
85
100
  next
86
101
  }
87
102
  fm == 1 && $0 == "---" { fm = 2; print fin_line($0) > out; next }
103
+ fm == 1 && tgt_drop($0) { next }
88
104
  fm == 1 { sub(/^paths:/, "globs:") }
89
105
  { print fin_line($0) > out }
90
- ' "${scoped[@]}"
91
- for f in "${scoped[@]}"; do
92
- base="${f##*/}"; base="${base%.md}"
93
- 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]})"
94
113
  done
95
114
  }
96
115
 
@@ -100,25 +119,19 @@ sync_cursor_skills() {
100
119
  local config_file="$2"
101
120
  local output_dir="$3"
102
121
 
103
- local src d skill_name
122
+ local i name
104
123
  local -a skill_dirs=()
105
- load_yaml_list "$config_file" "skills"
106
- local list="$IS_YAML_LIST"
107
- while IFS= read -r src; do
108
- [ -z "$src" ] && continue
109
- local dir="$repo_root/$src"
110
- [ -d "$dir" ] || continue
111
- for d in "$dir"/*/; do
112
- [ -d "$d" ] || continue
113
- [ -f "$d/SKILL.md" ] || continue
114
- skill_dirs+=("$d")
115
- done
116
- done <<< "$list"
117
- [ "${#skill_dirs[@]}" -gt 0 ] || return 0
118
- copy_skill_bundle_dirs_for cursor "$output_dir/skills" "${skill_dirs[@]}"
119
- for d in "${skill_dirs[@]}"; do
120
- skill_name="${d%/}"
121
- 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"
122
135
  done
123
136
  }
124
137
 
@@ -128,37 +141,32 @@ sync_cursor_agents() {
128
141
  local config_file="$2"
129
142
  local output_dir="$3"
130
143
 
131
- local src f
132
- local -a files=()
133
- load_yaml_list "$config_file" "agents"
134
- local list="$IS_YAML_LIST"
135
- while IFS= read -r src; do
136
- [ -z "$src" ] && continue
137
- local dir="$repo_root/$src"
138
- [ -d "$dir" ] || continue
139
- for f in "$dir"/*.md; do
140
- [ -f "$f" ] || continue
141
- files+=("$f")
142
- done
143
- done <<< "$list"
144
- [ "${#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"
145
152
 
146
153
  load_model_tiers "$config_file" "cursor"
147
154
 
148
155
  local path tier access spec="" report="" cursor_readonly
156
+ i=0
149
157
  while IFS=$'\x1f' read -r path tier access; do
150
- [ -n "$path" ] || continue
151
158
  resolve_model_var "$tier"
152
159
  cursor_readonly=""
153
160
  [ "$access" = "readonly" ] && cursor_readonly="readonly: true"
154
161
  spec+="$path"$'\x1f'"$IS_MODEL"$'\x1f'"$cursor_readonly"$'\n'
155
- report+=" agent: ${path##*/} (tier=$tier -> cursor)"$'\n'
162
+ report+=" agent: ${names[i]}.md (tier=$tier -> cursor)"$'\n'
163
+ i=$((i + 1))
156
164
  done < <(frontmatter_index "tier,access" "${files[@]}")
157
165
 
158
166
  # Always emit model: line (even when value matches Cursor's default)
159
167
  # so config is explicit and grep-able.
160
168
  is_fin_awk_vars
161
- 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"'
162
170
  BEGIN {
163
171
  US = sprintf("%c", 31)
164
172
  n = split(ENVIRON["IS_MAP_SPEC"], recs, "\n")
@@ -170,9 +178,10 @@ sync_cursor_agents() {
170
178
  }
171
179
  FNR == 1 {
172
180
  if (out != "") close(out)
173
- out = dst "/" base_name(FILENAME)
181
+ out = dst "/" art_file(FILENAME, ".md")
174
182
  count = 0
175
183
  fm = 0
184
+ tgt_reset()
176
185
  }
177
186
  /^tier:/ { next }
178
187
  /^access:/ { next }
@@ -181,6 +190,7 @@ sync_cursor_agents() {
181
190
  { sub(/\r$/, "") }
182
191
  FNR == 1 && $0 == "---" { fm = 1 }
183
192
  /^---$/ { count++ }
193
+ fm && count == 1 && !/^---$/ && tgt_drop($0) { next }
184
194
  fm && count == 1 && substr($0, 1, 7) == "effort:" { next }
185
195
  count == 2 && /^---$/ {
186
196
  print fin_line("model: " MODEL[FILENAME]) > out
@@ -188,8 +198,8 @@ sync_cursor_agents() {
188
198
  }
189
199
  { print fin_line($0) > out }
190
200
  ' "${files[@]}"
191
- for f in "${files[@]}"; do
192
- [ -s "$f" ] || : > "$output_dir/agents/${f##*/}"
201
+ for ((i = 0; i < ${#files[@]}; i++)); do
202
+ [ -s "${files[i]}" ] || : > "$output_dir/agents/${names[i]}.md"
193
203
  done
194
204
  printf '%s' "$report"
195
205
  }