@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
@@ -31,7 +31,11 @@ sync_lock_pgid() {
31
31
  local stat
32
32
  local -a fields
33
33
  IS_PGID=""
34
- if [ -r "/proc/$$/stat" ] && IFS= read -r stat < "/proc/$$/stat"; then
34
+ # Cygwin and MSYS publish the group on its own; composing their stat file
35
+ # takes tens of milliseconds.
36
+ if [ -r "/proc/$$/pgid" ] && IFS= read -r IS_PGID < "/proc/$$/pgid"; then
37
+ :
38
+ elif [ -r "/proc/$$/stat" ] && IFS= read -r stat < "/proc/$$/stat"; then
35
39
  # pid (comm) state ppid pgrp …: comm may hold spaces, so cut after it.
36
40
  read -r -a fields <<< "${stat##*) }"
37
41
  IS_PGID="${fields[2]:-}"
@@ -87,7 +91,8 @@ sync_lock_is_stale() {
87
91
  # sync_lock_acquire <root> — take <root>/.intelligence/sync.lock or refuse.
88
92
  sync_lock_acquire() {
89
93
  local root="$1" lock="$1/.intelligence/sync.lock" took=0 age=""
90
- mkdir -p "$root/.intelligence" || die "cannot create $root/.intelligence"
94
+ # A process start costs tens of milliseconds on Git Bash: skip the usual case.
95
+ [ -d "$root/.intelligence" ] || mkdir -p "$root/.intelligence" || die "cannot create $root/.intelligence"
91
96
  if mkdir "$lock" 2>/dev/null; then
92
97
  took=1
93
98
  elif mkdir "$lock.takeover" 2>/dev/null; then
@@ -146,3 +151,15 @@ sync_lock_release() {
146
151
  fi
147
152
  SYNC_LOCK_HELD=""
148
153
  }
154
+
155
+ # project_lock_scratch <root> — set IS_LOCK_SCRATCH to a path prefix inside the
156
+ # lock this process holds or joined, for files that live only as long as the
157
+ # run: releasing the lock removes them with it, so a hot path needs no process
158
+ # to create or delete a temporary directory. Names carry the pid, because a
159
+ # joined child shares the directory with its holder.
160
+ # shellcheck disable=SC2034 # IS_LOCK_SCRATCH is the return channel
161
+ project_lock_scratch() {
162
+ local lock="$1/.intelligence/sync.lock"
163
+ [ -d "$lock" ] && [ ! -L "$lock" ] || return 1
164
+ IS_LOCK_SCRATCH="$lock/scratch.$$"
165
+ }
package/engine/ENGINE_SHA CHANGED
@@ -1 +1 @@
1
- b1113b0658f6fddb23017e845a4ba79401caedbf
1
+ af0a0fdbc3f505e4f063ae516c9243c92db6e376
package/engine/VERSION CHANGED
@@ -1 +1 @@
1
- 0.19.0
1
+ 0.21.0
@@ -30,9 +30,17 @@
30
30
  # map_effort(tool, effort) — the tool's level for a neutral effort; "" when none
31
31
  # map_access_to_claude_tools(access) — full->"" (no tools list; inherits all), readonly->restricted
32
32
  # map_access_to_claude_disallowed(access) — readonly->"Write, Edit", full->""
33
- # read_yaml_list(manifest, section) — read a source list from intelligence.yaml,
34
- # package references as their store paths
33
+ # source_artifacts_var(root, config, section, adapter, output)
34
+ # — the sources this adapter renders (below)
35
+ # finalize_copy_named(dir, map, file...) — copy and finalize files under their rendered names
36
+ # copy_skill_bundle_dirs_named(tool, dir, map, skill_dir...) — copy skills under their rendered names
35
37
  # get_target_field(config, target, field) — read a target's config field
38
+ #
39
+ # Read sources through source_artifacts_var, never with a glob of your own: it
40
+ # names each artifact (`.draft.md` is `draft`, `me.local.md` is `me`), renders
41
+ # only the later of two same-named artifacts, and leaves a local artifact out of
42
+ # every output path Git tracks (decision 0021). An adapter that globs sources
43
+ # itself can write a teammate's local file into a committed output.
36
44
 
37
45
  # Project adapters are sourced by engine/sync.sh after the shared library is
38
46
  # loaded. Keep this template position-independent: after scaffolding it lives
@@ -61,13 +69,24 @@ adapter_contract_<name>() {
61
69
  # - Copy as-is (like Claude)
62
70
  # - Convert paths: to globs: (like Cursor)
63
71
  # - Merge into single file (like Copilot)
72
+ #
73
+ # source_artifacts_var takes the repo-relative path an artifact lands at, `{}`
74
+ # standing for its name, and fills IS_SOURCE_FILES (the source file, or a
75
+ # skill's SKILL.md), IS_SOURCE_NAMES, IS_SOURCE_SCOPED (1 for a rule with
76
+ # `paths:`), IS_SOURCE_IN_AGENTS (1 when AGENTS.md carries the artifact too)
77
+ # and IS_SOURCE_NAME_MAP, the names finalize_copy_named and
78
+ # copy_skill_bundle_dirs_named write under. It lists only the artifacts whose
79
+ # `targets:` / `exclude_targets:` select this adapter; the shared helpers drop
80
+ # both fields from every copy, and a frontmatter copied by hand must too.
64
81
  sync_<name>_rules() {
65
82
  local repo_root="$1"
66
83
  local config_file="$2"
67
84
  local output_dir="$3"
68
85
 
69
- # TODO: Implement rule sync logic
70
- echo " rules: not implemented"
86
+ output_rel_var "$repo_root" "$output_dir"
87
+ source_artifacts_var "$repo_root" "$config_file" rules <name> "$IS_OUTPUT_REL/rules/{}.md"
88
+ # TODO: Render each of IS_SOURCE_FILES into $output_dir/rules/ under its IS_SOURCE_NAMES entry
89
+ echo " rules: ${#IS_SOURCE_FILES[@]} found, not rendered"
71
90
  }
72
91
 
73
92
  # Sync skills for <agent-name>
@@ -76,8 +95,11 @@ sync_<name>_skills() {
76
95
  local config_file="$2"
77
96
  local output_dir="$3"
78
97
 
79
- # TODO: Implement skill sync logic
80
- echo " skills: not implemented"
98
+ output_rel_var "$repo_root" "$output_dir"
99
+ source_artifacts_var "$repo_root" "$config_file" skills <name> "$IS_OUTPUT_REL/skills/{}"
100
+ # TODO: Copy each skill directory ("${IS_SOURCE_FILES[i]%/SKILL.md}") with
101
+ # copy_skill_bundle_dirs_named <name> "$output_dir/skills" "$IS_SOURCE_NAME_MAP" <dir>...
102
+ echo " skills: ${#IS_SOURCE_FILES[@]} found, not rendered"
81
103
  }
82
104
 
83
105
  # Sync agents for <agent-name>
@@ -86,8 +108,10 @@ sync_<name>_agents() {
86
108
  local config_file="$2"
87
109
  local output_dir="$3"
88
110
 
89
- # TODO: Implement agent sync logic
90
- echo " agents: not implemented"
111
+ output_rel_var "$repo_root" "$output_dir"
112
+ source_artifacts_var "$repo_root" "$config_file" agents <name> "$IS_OUTPUT_REL/agents/{}.md"
113
+ # TODO: Render each of IS_SOURCE_FILES into $output_dir/agents/ under its IS_SOURCE_NAMES entry
114
+ echo " agents: ${#IS_SOURCE_FILES[@]} found, not rendered"
91
115
  }
92
116
 
93
117
  # Main entry point
@@ -14,7 +14,9 @@
14
14
  # Every per-file loop batches its work into one awk process (see the batched
15
15
  # helpers in lib/common.sh): process spawns dominate sync time on Windows.
16
16
 
17
- source "$(dirname "${BASH_SOURCE[0]}")/../lib/common.sh"
17
+ # The engine and the CLI load the shared library before any adapter. Only a
18
+ # standalone source of this file loads it here: that costs a dirname process.
19
+ declare -F finalize_output_file >/dev/null 2>&1 || source "$(dirname "${BASH_SOURCE[0]}")/../lib/common.sh"
18
20
 
19
21
  # Repo-relative paths to the project's content dir and to the installed sync
20
22
  # package, taken from the env contract — never hardcoded. The content dir is
@@ -58,15 +60,19 @@ agents_md_append_agents_table() {
58
60
  local rows=""
59
61
  local count=0
60
62
 
61
- local -a files=()
62
- read_source_artifact_files "$repo_root" "$config_file" "agents"
63
- [ "${#IS_SOURCE_FILES[@]}" -eq 0 ] || files=("${IS_SOURCE_FILES[@]}")
63
+ local -a files=() names=()
64
+ source_artifacts_var "$repo_root" "$config_file" agents agents "${output#"$repo_root"/}"
65
+ if [ "${#IS_SOURCE_FILES[@]}" -gt 0 ]; then
66
+ files=("${IS_SOURCE_FILES[@]}")
67
+ names=("${IS_SOURCE_NAMES[@]}")
68
+ fi
64
69
 
65
70
  if [ "${#files[@]}" -gt 0 ]; then
66
- local path tier access desc name
71
+ local path tier access desc name i=0
72
+ # frontmatter_index prints one row per file, in argument order.
67
73
  while IFS=$'\x1f' read -r path tier access desc; do
68
- [ -n "$path" ] || continue
69
- name="${path##*/}"; name="${name%.md}"
74
+ name="${names[i]}"
75
+ i=$((i + 1))
70
76
  repo_rel_link_var "$repo_root" "$path"
71
77
  if [ -n "$IS_REPO_REL" ]; then
72
78
  rows+="| [$name]($IS_REPO_REL) | ${tier:--} | ${access:--} | ${desc:--} |"$'\n'
@@ -99,21 +105,24 @@ agents_md_append_skills_table() {
99
105
  local rows=""
100
106
  local count=0
101
107
 
102
- local dirname
103
- local -a skill_files=()
104
- read_source_artifact_files "$repo_root" "$config_file" "skills"
105
- [ "${#IS_SOURCE_FILES[@]}" -eq 0 ] || skill_files=("${IS_SOURCE_FILES[@]}")
108
+ local name
109
+ local -a skill_files=() names=()
110
+ source_artifacts_var "$repo_root" "$config_file" skills agents "${output#"$repo_root"/}"
111
+ if [ "${#IS_SOURCE_FILES[@]}" -gt 0 ]; then
112
+ skill_files=("${IS_SOURCE_FILES[@]}")
113
+ names=("${IS_SOURCE_NAMES[@]}")
114
+ fi
106
115
 
107
116
  if [ "${#skill_files[@]}" -gt 0 ]; then
108
- local path desc
117
+ local path desc i=0
109
118
  while IFS=$'\x1f' read -r path desc; do
110
- [ -n "$path" ] || continue
111
- dirname="${path%/SKILL.md}"; dirname="${dirname##*/}"
119
+ name="${names[i]}"
120
+ i=$((i + 1))
112
121
  repo_rel_link_var "$repo_root" "$path"
113
122
  if [ -n "$IS_REPO_REL" ]; then
114
- rows+="| [$dirname]($IS_REPO_REL) | ${desc:--} |"$'\n'
123
+ rows+="| [$name]($IS_REPO_REL) | ${desc:--} |"$'\n'
115
124
  else
116
- rows+="| $dirname | ${desc:--} |"$'\n'
125
+ rows+="| $name | ${desc:--} |"$'\n'
117
126
  fi
118
127
  count=$((count + 1))
119
128
  done < <(frontmatter_index "description" "${skill_files[@]}")
@@ -142,30 +151,25 @@ agents_md_append_rules_list() {
142
151
  local count=0
143
152
  local global_rule_files=()
144
153
 
145
- local -a files=()
146
- read_source_artifact_files "$repo_root" "$config_file" "rules"
147
- [ "${#IS_SOURCE_FILES[@]}" -eq 0 ] || files=("${IS_SOURCE_FILES[@]}")
148
-
149
- if [ "${#files[@]}" -gt 0 ]; then
150
- local path hp name scope
151
- while IFS=$'\x1f' read -r path hp; do
152
- [ -n "$path" ] || continue
153
- name="${path##*/}"; name="${name%.md}"
154
- scope="global"
155
- if [ "$hp" != "0" ]; then
156
- scope="scoped"
157
- else
158
- global_rule_files+=("$path")
159
- fi
160
- repo_rel_link_var "$repo_root" "$path"
161
- if [ -n "$IS_REPO_REL" ]; then
162
- lines+="- [$name]($IS_REPO_REL) ($scope)"$'\n'
163
- else
164
- lines+="- $name ($scope)"$'\n'
165
- fi
166
- count=$((count + 1))
167
- done < <(frontmatter_index "paths#" "${files[@]}")
168
- fi
154
+ local i path name scope
155
+ source_artifacts_var "$repo_root" "$config_file" rules agents "${output#"$repo_root"/}"
156
+ for ((i = 0; i < ${#IS_SOURCE_FILES[@]}; i++)); do
157
+ path="${IS_SOURCE_FILES[i]}"
158
+ name="${IS_SOURCE_NAMES[i]}"
159
+ scope="global"
160
+ if [ "${IS_SOURCE_SCOPED[i]}" = 1 ]; then
161
+ scope="scoped"
162
+ else
163
+ global_rule_files+=("$path")
164
+ fi
165
+ repo_rel_link_var "$repo_root" "$path"
166
+ if [ -n "$IS_REPO_REL" ]; then
167
+ lines+="- [$name]($IS_REPO_REL) ($scope)"$'\n'
168
+ else
169
+ lines+="- $name ($scope)"$'\n'
170
+ fi
171
+ count=$((count + 1))
172
+ done
169
173
 
170
174
  [ "$count" -eq 0 ] && return 0
171
175
 
@@ -194,17 +198,7 @@ agents_md_append_rules_list() {
194
198
  echo "<!-- Inlined from always-on rules in $content_rel/rules/ -->"
195
199
  echo ""
196
200
  } >> "$output"
197
- # One awk inlines every global rule body, with a blank line after
198
- # each — the same output the per-file passes produced.
199
- awk '
200
- FNR == 1 { if (NR != 1) print ""; in_fm = 0; past_fm = 0 }
201
- { sub(/\r$/, "") }
202
- /^---$/ {
203
- if (!past_fm) { in_fm = !in_fm; if (!in_fm) { past_fm = 1 }; next }
204
- }
205
- past_fm || !in_fm { print }
206
- END { print "" }
207
- ' "${global_rule_files[@]}" >> "$output"
201
+ emit_rule_bodies "${global_rule_files[@]}" >> "$output"
208
202
  echo " rules: ${#global_rule_files[@]} global rule(s) inlined"
209
203
  fi
210
204
  }
@@ -212,7 +206,8 @@ agents_md_append_rules_list() {
212
206
  # Main entry point for AGENTS.md adapter
213
207
  adapter_contract_agents() {
214
208
  local output
215
- output="$(agents_output_path "$1")"
209
+ agents_output_path_var "$1"
210
+ output="$IS_AGENTS_OUTPUT_PATH"
216
211
  adapter_contract_version 1
217
212
  adapter_contract_owned "$output"
218
213
  adapter_contract_legacy "AGENTS.md"
@@ -11,7 +11,11 @@
11
11
  # - Always-on (no `paths:`) -> SKIPPED here. The CLI docs tell a project to
12
12
  # "Create a GEMINI.md or AGENTS.md file at your workspace root", and the
13
13
  # `agents` adapter inlines always-on rule content into AGENTS.md. Emitting
14
- # copies under .agents/rules/ would double-load the same text.
14
+ # copies under .agents/rules/ would double-load the same text. An always-on
15
+ # rule AGENTS.md does not carry — a local one, or one whose targets leave
16
+ # `agents` out — becomes .agents/rules/<name>.md with `trigger: always_on`,
17
+ # the same key family as `trigger: glob` and just as undocumented
18
+ # (decision 0021).
15
19
  # - Path-scoped (with `paths:`) -> .agents/rules/<name>.md carrying
16
20
  # `trigger: glob` and `globs:`. NOT vendor-documented: the rules page names
17
21
  # four activation modes in prose (manual, always on, model decision, glob
@@ -42,7 +46,9 @@
42
46
  # Every per-file loop batches its work into one awk process (see the batched
43
47
  # helpers in lib/common.sh): process spawns dominate sync time on Windows.
44
48
 
45
- source "$(dirname "${BASH_SOURCE[0]}")/../lib/common.sh"
49
+ # The engine and the CLI load the shared library before any adapter. Only a
50
+ # standalone source of this file loads it here: that costs a dirname process.
51
+ declare -F finalize_output_file >/dev/null 2>&1 || source "$(dirname "${BASH_SOURCE[0]}")/../lib/common.sh"
46
52
 
47
53
  adapter_contract_antigravity() {
48
54
  local output="${1%/}"
@@ -93,45 +99,46 @@ sync_antigravity_rules() {
93
99
  antigravity_rule_limit "$config_file" || return 1
94
100
  rule_limit="$IS_AG_RULE_LIMIT"
95
101
 
96
- local src f
97
- local -a files=()
98
- load_yaml_list "$config_file" "rules"
99
- local list="$IS_YAML_LIST"
100
- while IFS= read -r src; do
101
- [ -z "$src" ] && continue
102
- local dir="$repo_root/$src"
103
- [ -d "$dir" ] || continue
104
- for f in "$dir"/*.md; do
105
- [ -f "$f" ] || continue
106
- files+=("$f")
107
- done
108
- done <<< "$list"
109
-
110
- # Skip always-on rules: AGENTS.md (canonical) carries them.
111
- local path hp base
112
- local -a scoped=()
113
- if [ "${#files[@]}" -gt 0 ]; then
114
- while IFS=$'\x1f' read -r path hp; do
115
- [ -n "$path" ] || continue
116
- [ "$hp" -eq 0 ] && continue
117
- scoped+=("$path")
118
- done < <(frontmatter_index "paths#" "${files[@]}")
119
- fi
120
- if [ "${#scoped[@]}" -eq 0 ]; then
102
+ # Skip always-on rules AGENTS.md (canonical) carries; one it does not
103
+ # carry takes the always_on trigger instead (decision 0021).
104
+ local i always="" always_count=0
105
+ local -a files=() names=() kinds=()
106
+ output_rel_var "$repo_root" "$output_dir"
107
+ source_artifacts_var "$repo_root" "$config_file" rules antigravity "$IS_OUTPUT_REL/rules/{}.md"
108
+ for ((i = 0; i < ${#IS_SOURCE_FILES[@]}; i++)); do
109
+ if [ "${IS_SOURCE_SCOPED[i]}" = 1 ]; then
110
+ kinds+=(scoped)
111
+ elif [ "${IS_SOURCE_IN_AGENTS[i]}" = 0 ]; then
112
+ kinds+=(always-on)
113
+ always="$always${IS_SOURCE_FILES[i]}"$'\n'
114
+ always_count=$((always_count + 1))
115
+ else
116
+ continue
117
+ fi
118
+ files+=("${IS_SOURCE_FILES[i]}")
119
+ names+=("${IS_SOURCE_NAMES[i]}")
120
+ done
121
+ if [ "${#files[@]}" -eq 0 ]; then
121
122
  echo " -> Rules: 0 scoped (AGENTS.md carries always-on)"
122
123
  return 0
123
124
  fi
124
125
 
125
- # Path-scoped rule -> Glob activation (paths -> globs). The rewrite is
126
- # anchored inside the frontmatter block: a body line may legitimately start
127
- # with `paths:` (a rule documenting rule syntax does exactly that), and
128
- # rewriting it would corrupt the text it teaches.
126
+ # Path-scoped rule -> Glob activation (paths -> globs); always-on rule ->
127
+ # `trigger: always_on`, with a frontmatter block of its own when the source
128
+ # has none. The rewrite is anchored inside the frontmatter block: a body
129
+ # line may legitimately start with `paths:` (a rule documenting rule
130
+ # syntax does exactly that), and rewriting it would corrupt the text it
131
+ # teaches.
129
132
  #
130
133
  # stdout carries nothing but the character-count overflow report, so the
131
134
  # limit check costs no extra process.
132
135
  local overflow
133
136
  is_fin_awk_vars
134
- overflow="$(awk "${IS_FIN_V[@]}" -v dst="$output_dir/rules" -v limit="$rule_limit" "$IS_AWK_LIB"'
137
+ overflow="$(IS_AG_ALWAYS="$always" IS_NAME_MAP="$IS_SOURCE_NAME_MAP" awk "${IS_FIN_V[@]}" -v dst="$output_dir/rules" -v limit="$rule_limit" "$IS_AWK_LIB"'
138
+ BEGIN {
139
+ n = split(ENVIRON["IS_AG_ALWAYS"], al, "\n")
140
+ for (k = 1; k <= n; k++) if (al[k] != "") ALWAYS[al[k]] = 1
141
+ }
135
142
  function emit(line, rendered) {
136
143
  rendered = fin_line(line)
137
144
  print rendered > out
@@ -139,11 +146,21 @@ sync_antigravity_rules() {
139
146
  }
140
147
  FNR == 1 {
141
148
  if (out != "") close(out)
142
- out = dst "/" base_name(FILENAME)
149
+ out = dst "/" art_file(FILENAME, ".md")
143
150
  chars[out] = 0
144
151
  fm = 0
152
+ tgt_reset()
145
153
  }
146
154
  { sub(/\r$/, "") }
155
+ FNR == 1 && (FILENAME in ALWAYS) {
156
+ emit("---")
157
+ emit("trigger: always_on")
158
+ if ($0 == "---") { fm = 1; next }
159
+ emit("---")
160
+ fm = 2
161
+ emit($0)
162
+ next
163
+ }
147
164
  FNR == 1 {
148
165
  if ($0 == "---") fm = 1
149
166
  emit($0)
@@ -151,6 +168,7 @@ sync_antigravity_rules() {
151
168
  next
152
169
  }
153
170
  fm == 1 && $0 == "---" { fm = 2; emit($0); next }
171
+ fm == 1 && tgt_drop($0) { next }
154
172
  fm == 1 { sub(/^paths:/, "globs:") }
155
173
  { emit($0) }
156
174
  END {
@@ -159,7 +177,7 @@ sync_antigravity_rules() {
159
177
  for (f in chars)
160
178
  if (chars[f] > limit) printf "%s\t%d\n", f, chars[f]
161
179
  }
162
- ' "${scoped[@]}" | sort)"
180
+ ' "${files[@]}" | sort)"
163
181
 
164
182
  local over_file over_chars
165
183
  while IFS=$'\t' read -r over_file over_chars; do
@@ -167,11 +185,18 @@ sync_antigravity_rules() {
167
185
  echo "WARNING: Antigravity limits a rule file to $rule_limit characters: ${over_file#"$repo_root/"} renders $over_chars; split the rule, shorten it, set targets.antigravity.warn_rule_limit to the effective limit, or set it to false to disable this warning." >&2
168
186
  done <<< "$overflow"
169
187
 
170
- for f in "${scoped[@]}"; do
171
- base="${f##*/}"
172
- echo " rule: $base (scoped)"
188
+ for ((i = 0; i < ${#files[@]}; i++)); do
189
+ # awk never reads a record from an empty source.
190
+ if [ "${kinds[i]}" = always-on ] && [ ! -s "${files[i]}" ]; then
191
+ printf -- '---\ntrigger: always_on\n---\n' > "$output_dir/rules/${names[i]}.md"
192
+ fi
193
+ echo " rule: ${names[i]}.md (${kinds[i]})"
173
194
  done
174
- echo " -> Rules: ${#scoped[@]} scoped"
195
+ if [ "$always_count" -gt 0 ]; then
196
+ echo " -> Rules: $(( ${#files[@]} - always_count )) scoped, $always_count always-on"
197
+ else
198
+ echo " -> Rules: ${#files[@]} scoped"
199
+ fi
175
200
  }
176
201
 
177
202
  sync_antigravity_agents() {
@@ -179,19 +204,13 @@ sync_antigravity_agents() {
179
204
  local config_file="$2"
180
205
  local agents_dir="$3"
181
206
 
182
- local src f
183
- local -a files=()
184
- load_yaml_list "$config_file" "agents"
185
- local list="$IS_YAML_LIST"
186
- while IFS= read -r src; do
187
- [ -z "$src" ] && continue
188
- local dir="$repo_root/$src"
189
- [ -d "$dir" ] || continue
190
- for f in "$dir"/*.md; do
191
- [ -f "$f" ] || continue
192
- files+=("$f")
193
- done
194
- done <<< "$list"
207
+ local -a files=() names=()
208
+ output_rel_var "$repo_root" "$agents_dir"
209
+ source_artifacts_var "$repo_root" "$config_file" agents antigravity "$IS_OUTPUT_REL/{}.md"
210
+ if [ "${#IS_SOURCE_FILES[@]}" -gt 0 ]; then
211
+ files=("${IS_SOURCE_FILES[@]}")
212
+ names=("${IS_SOURCE_NAMES[@]}")
213
+ fi
195
214
 
196
215
  local count=0
197
216
  if [ "${#files[@]}" -gt 0 ]; then
@@ -199,8 +218,7 @@ sync_antigravity_agents() {
199
218
 
200
219
  local path tier access description name spec="" report="" LS=$'\x1e'
201
220
  while IFS=$'\x1f' read -r path tier access description; do
202
- [ -n "$path" ] || continue
203
- name="${path##*/}"; name="${name%.md}"
221
+ name="${names[count]}"
204
222
  resolve_model_var "$tier"
205
223
 
206
224
  local header
@@ -10,7 +10,9 @@
10
10
  # Every per-file loop batches its work into one awk process (see the batched
11
11
  # helpers in lib/common.sh): process spawns dominate sync time on Windows.
12
12
 
13
- source "$(dirname "${BASH_SOURCE[0]}")/../lib/common.sh"
13
+ # The engine and the CLI load the shared library before any adapter. Only a
14
+ # standalone source of this file loads it here: that costs a dirname process.
15
+ declare -F finalize_output_file >/dev/null 2>&1 || source "$(dirname "${BASH_SOURCE[0]}")/../lib/common.sh"
14
16
 
15
17
  adapter_contract_claude() {
16
18
  local output="${1%/}"
@@ -33,23 +35,15 @@ sync_claude_rules() {
33
35
  local config_file="$2"
34
36
  local output_dir="$3"
35
37
 
36
- local src f
37
- local -a files=()
38
- load_yaml_list "$config_file" "rules"
39
- local list="$IS_YAML_LIST"
40
- while IFS= read -r src; do
41
- [ -z "$src" ] && continue
42
- local dir="$repo_root/$src"
43
- [ -d "$dir" ] || continue
44
- for f in "$dir"/*.md; do
45
- [ -f "$f" ] || continue
46
- files+=("$f")
47
- done
48
- done <<< "$list"
49
- [ "${#files[@]}" -gt 0 ] || return 0
50
- finalize_copy_files "$output_dir/rules" "${files[@]}"
51
- for f in "${files[@]}"; do
52
- echo " rule: ${f##*/}"
38
+ local name
39
+ # Every source reaches this adapter through the source catalog (decision
40
+ # 0021): rendered names, overrides and local artifacts are settled there.
41
+ output_rel_var "$repo_root" "$output_dir"
42
+ source_artifacts_var "$repo_root" "$config_file" rules claude "$IS_OUTPUT_REL/rules/{}.md"
43
+ [ "${#IS_SOURCE_FILES[@]}" -gt 0 ] || return 0
44
+ finalize_copy_named "$output_dir/rules" "$IS_SOURCE_NAME_MAP" "${IS_SOURCE_FILES[@]}"
45
+ for name in "${IS_SOURCE_NAMES[@]}"; do
46
+ echo " rule: $name.md"
53
47
  done
54
48
  }
55
49
 
@@ -59,25 +53,19 @@ sync_claude_skills() {
59
53
  local config_file="$2"
60
54
  local output_dir="$3"
61
55
 
62
- local src d skill_name
56
+ local i name
63
57
  local -a skill_dirs=()
64
- load_yaml_list "$config_file" "skills"
65
- local list="$IS_YAML_LIST"
66
- while IFS= read -r src; do
67
- [ -z "$src" ] && continue
68
- local dir="$repo_root/$src"
69
- [ -d "$dir" ] || continue
70
- for d in "$dir"/*/; do
71
- [ -d "$d" ] || continue
72
- [ -f "$d/SKILL.md" ] || continue
73
- skill_dirs+=("$d")
74
- done
75
- done <<< "$list"
76
- [ "${#skill_dirs[@]}" -gt 0 ] || return 0
77
- copy_skill_bundle_dirs_for claude "$output_dir/skills" "${skill_dirs[@]}"
78
- for d in "${skill_dirs[@]}"; do
79
- skill_name="${d%/}"
80
- echo " skill: ${skill_name##*/}"
58
+ # The catalog's skills are the ones the inventory renders, symlinked ones
59
+ # included (decision 0017).
60
+ output_rel_var "$repo_root" "$output_dir"
61
+ source_artifacts_var "$repo_root" "$config_file" skills claude "$IS_OUTPUT_REL/skills/{}"
62
+ [ "${#IS_SOURCE_FILES[@]}" -gt 0 ] || return 0
63
+ for ((i = 0; i < ${#IS_SOURCE_FILES[@]}; i++)); do
64
+ skill_dirs+=("${IS_SOURCE_FILES[i]%/SKILL.md}/")
65
+ done
66
+ copy_skill_bundle_dirs_named claude "$output_dir/skills" "$IS_SOURCE_NAME_MAP" "${skill_dirs[@]}"
67
+ for name in "${IS_SOURCE_NAMES[@]}"; do
68
+ echo " skill: $name"
81
69
  done
82
70
  }
83
71
 
@@ -88,32 +76,27 @@ sync_claude_agents() {
88
76
  local config_file="$2"
89
77
  local output_dir="$3"
90
78
 
91
- local src f
92
- local -a files=()
93
- load_yaml_list "$config_file" "agents"
94
- local list="$IS_YAML_LIST"
95
- while IFS= read -r src; do
96
- [ -z "$src" ] && continue
97
- local dir="$repo_root/$src"
98
- [ -d "$dir" ] || continue
99
- for f in "$dir"/*.md; do
100
- [ -f "$f" ] || continue
101
- files+=("$f")
102
- done
103
- done <<< "$list"
104
- [ "${#files[@]}" -gt 0 ] || return 0
79
+ local i
80
+ local -a files=() names=()
81
+ output_rel_var "$repo_root" "$output_dir"
82
+ source_artifacts_var "$repo_root" "$config_file" agents claude "$IS_OUTPUT_REL/agents/{}.md"
83
+ [ "${#IS_SOURCE_FILES[@]}" -gt 0 ] || return 0
84
+ files=("${IS_SOURCE_FILES[@]}")
85
+ names=("${IS_SOURCE_NAMES[@]}")
86
+ local name_map="$IS_SOURCE_NAME_MAP"
105
87
 
106
88
  load_model_tiers "$config_file" "claude"
107
89
 
108
90
  local path tier access effort spec="" report=""
91
+ i=0
109
92
  while IFS=$'\x1f' read -r path tier access effort; do
110
- [ -n "$path" ] || continue
111
93
  resolve_model_var "$tier"
112
94
  map_effort_var claude "$effort"
113
95
  map_access_to_claude_tools_var "$access"
114
96
  map_access_to_claude_disallowed_var "$access"
115
97
  spec+="$path"$'\x1f'"$IS_MODEL"$'\x1f'"$IS_CLAUDE_TOOLS"$'\x1f'"$IS_CLAUDE_DISALLOWED"$'\x1f'"$IS_EFFORT"$'\n'
116
- report+=" agent: ${path##*/} (tier=$tier -> model=$IS_MODEL, access=$access)"$'\n'
98
+ report+=" agent: ${names[i]}.md (tier=$tier -> model=$IS_MODEL, access=$access)"$'\n'
99
+ i=$((i + 1))
117
100
  done < <(frontmatter_index "tier,access,effort" "${files[@]}")
118
101
 
119
102
  # The transform used to run per file with the values injected via -v;
@@ -122,7 +105,7 @@ sync_claude_agents() {
122
105
  # becomes Claude's level in place, or goes when there is none; a later
123
106
  # one goes too, since frontmatter_index read only the first.
124
107
  is_fin_awk_vars
125
- IS_MAP_SPEC="$spec" awk "${IS_FIN_V[@]}" -v dst="$output_dir/agents" "$IS_AWK_LIB"'
108
+ IS_NAME_MAP="$name_map" IS_MAP_SPEC="$spec" awk "${IS_FIN_V[@]}" -v dst="$output_dir/agents" "$IS_AWK_LIB"'
126
109
  BEGIN {
127
110
  US = sprintf("%c", 31)
128
111
  n = split(ENVIRON["IS_MAP_SPEC"], recs, "\n")
@@ -134,16 +117,18 @@ sync_claude_agents() {
134
117
  }
135
118
  FNR == 1 {
136
119
  if (out != "") close(out)
137
- out = dst "/" base_name(FILENAME)
120
+ out = dst "/" art_file(FILENAME, ".md")
138
121
  count = 0
139
122
  fm = 0
140
123
  effort_seen = 0
124
+ tgt_reset()
141
125
  }
142
126
  /^tier:/ { next }
143
127
  /^access:/ { next }
144
128
  { sub(/\r$/, "") }
145
129
  FNR == 1 && $0 == "---" { fm = 1 }
146
130
  /^---$/ { count++ }
131
+ fm && count == 1 && !/^---$/ && tgt_drop($0) { next }
147
132
  fm && count == 1 && substr($0, 1, 7) == "effort:" {
148
133
  if (!effort_seen++ && EFFORT[FILENAME] != "") print fin_line("effort: " EFFORT[FILENAME]) > out
149
134
  next
@@ -157,8 +142,8 @@ sync_claude_agents() {
157
142
  ' "${files[@]}"
158
143
  # awk never reads a record from an empty source; the old per-file shell
159
144
  # redirect still created an empty output for it.
160
- for f in "${files[@]}"; do
161
- [ -s "$f" ] || : > "$output_dir/agents/${f##*/}"
145
+ for ((i = 0; i < ${#files[@]}; i++)); do
146
+ [ -s "${files[i]}" ] || : > "$output_dir/agents/${names[i]}.md"
162
147
  done
163
148
  printf '%s' "$report"
164
149
  }
@@ -176,7 +161,7 @@ sync_to_claude() {
176
161
  # commands/, statusline.sh, etc.).
177
162
  rm -rf "$output_dir/rules" "$output_dir/agents"
178
163
  if [ -d "$output_dir/skills" ]; then
179
- find "$output_dir/skills" -mindepth 1 -maxdepth 1 -type d -exec rm -rf {} +
164
+ find "$output_dir/skills" -mindepth 1 -maxdepth 1 \( -type d -o -type l \) -exec rm -rf {} +
180
165
  fi
181
166
  mkdir -p "$output_dir/rules" "$output_dir/skills" "$output_dir/agents"
182
167