@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
@@ -27,7 +27,9 @@
27
27
  # Every per-file loop batches its work into one awk process (see the batched
28
28
  # helpers in lib/common.sh): process spawns dominate sync time on Windows.
29
29
 
30
- source "$(dirname "${BASH_SOURCE[0]}")/../lib/common.sh"
30
+ # The engine and the CLI load the shared library before any adapter. Only a
31
+ # standalone source of this file loads it here: that costs a dirname process.
32
+ declare -F finalize_output_file >/dev/null 2>&1 || source "$(dirname "${BASH_SOURCE[0]}")/../lib/common.sh"
31
33
 
32
34
  adapter_contract_opencode() {
33
35
  local output="${1%/}"
@@ -86,28 +88,24 @@ sync_opencode_commands() {
86
88
  [ "${#generated[@]}" -gt 0 ] && rm -f "${generated[@]}"
87
89
  fi
88
90
 
89
- local src d skill_name
90
- local -a skill_mds=()
91
- load_yaml_list "$config_file" "skills"
92
- local list="$IS_YAML_LIST"
93
- while IFS= read -r src; do
94
- [ -z "$src" ] && continue
95
- local dir="$repo_root/$src"
96
- [ -d "$dir" ] || continue
97
- for d in "$dir"/*/; do
98
- [ -d "$d" ] || continue
99
- [ -f "$d/SKILL.md" ] || continue
100
- skill_mds+=("${d%/}/SKILL.md")
101
- done
102
- 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
103
102
 
104
103
  local count=0
105
104
  if [ "${#skill_mds[@]}" -gt 0 ]; then
106
105
  local path description argument_hint out_file
107
106
  local -a out_files=()
108
107
  while IFS=$'\x1f' read -r path description argument_hint; do
109
- [ -n "$path" ] || continue
110
- skill_name="${path%/SKILL.md}"; skill_name="${skill_name##*/}"
108
+ skill_name="${names[count]}"
111
109
  out_file="$commands_dir/$skill_name.md"
112
110
  yaml_dq_escape_var "$description"
113
111
  {
@@ -145,19 +143,13 @@ sync_opencode_agents() {
145
143
  local agents_dir="$output_dir/agents"
146
144
  mkdir -p "$agents_dir"
147
145
 
148
- local src f
149
- local -a files=()
150
- load_yaml_list "$config_file" "agents"
151
- local list="$IS_YAML_LIST"
152
- while IFS= read -r src; do
153
- [ -z "$src" ] && continue
154
- local dir="$repo_root/$src"
155
- [ -d "$dir" ] || continue
156
- for f in "$dir"/*.md; do
157
- [ -f "$f" ] || continue
158
- files+=("$f")
159
- done
160
- 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
161
153
 
162
154
  local count=0
163
155
  if [ "${#files[@]}" -gt 0 ]; then
@@ -165,8 +157,7 @@ sync_opencode_agents() {
165
157
 
166
158
  local path tier access description name edit_perm bash_perm spec="" report="" LS=$'\x1e'
167
159
  while IFS=$'\x1f' read -r path tier access description; do
168
- [ -n "$path" ] || continue
169
- name="${path##*/}"; name="${name%.md}"
160
+ name="${names[count]}"
170
161
  resolve_model_var "$tier"
171
162
 
172
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.
@@ -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_pi() {
22
27
  local output="${1%/}"
@@ -83,6 +88,39 @@ pi_rule_paths_rows() {
83
88
  ' "$@"
84
89
  }
85
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
+
86
124
  sync_pi_rules() {
87
125
  local repo_root="$1"
88
126
  local config_file="$2"
@@ -96,38 +134,44 @@ sync_pi_rules() {
96
134
 
97
135
  mkdir -p "$rules_root" "$(dirname "$extension_file")"
98
136
 
99
- local src f
100
- local -a files=()
101
- load_yaml_list "$config_file" "rules"
102
- local list="$IS_YAML_LIST"
103
- while IFS= read -r src; do
104
- [ -z "$src" ] && continue
105
- local dir="$repo_root/$src"
106
- [ -d "$dir" ] || continue
107
- for f in "$dir"/*.md; do
108
- [ -f "$f" ] || continue
109
- files+=("$f")
110
- done
111
- done <<< "$list"
112
-
113
- local -a scoped=()
114
- if [ "${#files[@]}" -gt 0 ]; then
115
- local path hp
116
- while IFS=$'\x1f' read -r path hp; do
117
- [ -n "$path" ] || continue
118
- [ "$hp" -eq 0 ] && continue
119
- scoped+=("$path")
120
- 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[@]}")
121
163
  fi
122
164
 
123
165
  if [ "${#scoped[@]}" -gt 0 ]; then
124
- finalize_copy_files "$rules_root" "${scoped[@]}"
166
+ finalize_copy_named "$rules_root" "$IS_SOURCE_NAME_MAP" "${scoped[@]}"
125
167
 
126
168
  local -a row_arr
127
169
  local base rel_rule_path patterns_js display sep display_sep pat i
170
+ k=0
128
171
  while IFS=$'\x1f' read -r -a row_arr; do
129
172
  [ "${#row_arr[@]}" -gt 0 ] || continue
130
- base="${row_arr[0]##*/}"
173
+ base="${scoped_names[k]}.md"
174
+ k=$((k + 1))
131
175
  rel_rule_path="$output_rel/intelligence-sync/rules/$base"
132
176
  patterns_js=""
133
177
  display=""
@@ -151,7 +195,64 @@ sync_pi_rules() {
151
195
  done < <(pi_rule_paths_rows "${scoped[@]}")
152
196
  fi
153
197
 
154
- 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
155
256
  {
156
257
  cat <<'EOF'
157
258
  /**
@@ -197,7 +298,11 @@ EOF
197
298
  finalize_output_file "$extension_file"
198
299
  fi
199
300
 
200
- 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
201
306
  }
202
307
 
203
308
  sync_pi_skills() {
@@ -221,25 +326,18 @@ sync_pi_agents() {
221
326
 
222
327
  mkdir -p "$prompts_dir"
223
328
 
224
- local src f
225
- local -a files=()
226
- load_yaml_list "$config_file" "agents"
227
- local list="$IS_YAML_LIST"
228
- while IFS= read -r src; do
229
- [ -z "$src" ] && continue
230
- local dir="$repo_root/$src"
231
- [ -d "$dir" ] || continue
232
- for f in "$dir"/*.md; do
233
- [ -f "$f" ] || continue
234
- files+=("$f")
235
- done
236
- 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
237
336
 
238
337
  if [ "${#files[@]}" -gt 0 ]; then
239
338
  local path desc access name template_desc spec="" report="" LS=$'\x1e'
240
339
  while IFS=$'\x1f' read -r path desc access; do
241
- [ -n "$path" ] || continue
242
- name="${path##*/}"; name="${name%.md}"
340
+ name="${names[count]}"
243
341
  repo_rel_link_var "$repo_root" "$path"
244
342
 
245
343
  template_desc="Use $name persona"
@@ -122,7 +122,7 @@ adapter_contract_safe_concrete_path() {
122
122
  # into the caller. Project adapters are trusted executable code during sync;
123
123
  # the isolation here is for correctness, not a security boundary.
124
124
  adapter_contract_records() (
125
- local name="$1" file="$2" output="$3" manifest="${4:-}" fn line kind value saw_version=0
125
+ local name="$1" file="$2" output="$3" manifest="${4:-}" fn
126
126
  # shellcheck source=/dev/null
127
127
  source "$file"
128
128
  printf -v fn 'adapter_contract_%s' "$name"
@@ -130,6 +130,38 @@ adapter_contract_records() (
130
130
  echo "ERROR: adapter '$name' has no $fn contract" >&2
131
131
  return 1
132
132
  }
133
+ _adapter_contract_emit "$name" '' < <("$fn" "$output" "$manifest")
134
+ )
135
+
136
+ # adapter_contract_records_batch <scratch-file> <name> <file> <output>... —
137
+ # adapter_contract_records for several built-in adapters in one subshell, each
138
+ # record prefixed with "<name><TAB>". One process instead of two per adapter:
139
+ # on Git Bash every fork costs tens of milliseconds. <scratch-file> receives
140
+ # each contract's raw lines before they are checked.
141
+ adapter_contract_records_batch() (
142
+ local scratch="$1" name file output fn
143
+ shift
144
+ while [ "$#" -ge 3 ]; do
145
+ name="$1" file="$2" output="$3"
146
+ shift 3
147
+ # shellcheck source=/dev/null
148
+ source "$file"
149
+ printf -v fn 'adapter_contract_%s' "$name"
150
+ declare -F "$fn" >/dev/null 2>&1 || {
151
+ echo "ERROR: adapter '$name' has no $fn contract" >&2
152
+ return 1
153
+ }
154
+ # Like the process substitution above, a contract's own status is not
155
+ # its verdict: the records it printed are.
156
+ "$fn" "$output" > "$scratch" || true
157
+ _adapter_contract_emit "$name" "$name"$'\t' < "$scratch" || return 1
158
+ done
159
+ )
160
+
161
+ # _adapter_contract_emit <adapter-name> <prefix> — check the contract records
162
+ # on stdin and print each one after <prefix>; return 1 on the first bad one.
163
+ _adapter_contract_emit() {
164
+ local name="$1" prefix="$2" line kind value saw_version=0
133
165
  while IFS= read -r line; do
134
166
  [ -n "$line" ] || continue
135
167
  kind="${line%%$'\t'*}"
@@ -171,10 +203,10 @@ adapter_contract_records() (
171
203
  return 1
172
204
  ;;
173
205
  esac
174
- printf '%s\n' "$line"
175
- done < <("$fn" "$output" "$manifest")
206
+ printf '%s%s\n' "$prefix" "$line"
207
+ done
176
208
  [ "$saw_version" -eq 1 ] || {
177
209
  echo "ERROR: adapter '$name' contract did not declare version 1" >&2
178
210
  return 1
179
211
  }
180
- )
212
+ }