@ainova-systems/intelligence 0.18.1 → 0.20.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 (48) hide show
  1. package/cli/commands/init.sh +6 -1
  2. package/cli/commands/package.sh +8 -3
  3. package/cli/commands/registry.sh +1 -1
  4. package/cli/commands/source.sh +56 -17
  5. package/cli/commands/sync.sh +78 -6
  6. package/cli/intelligence +16 -3
  7. package/cli/internal/check.sh +64 -13
  8. package/cli/internal/convert-legacy.sh +1 -1
  9. package/cli/internal/package-add.sh +25 -4
  10. package/cli/internal/package-alias.sh +51 -0
  11. package/cli/internal/package-list.sh +16 -2
  12. package/cli/internal/package-remove.sh +3 -1
  13. package/cli/internal/package-search.sh +1 -1
  14. package/cli/internal/package-update.sh +14 -6
  15. package/cli/internal/restore.sh +3 -2
  16. package/cli/lib/adapter-lifecycle.sh +5 -2
  17. package/cli/lib/cli-common.sh +149 -34
  18. package/cli/lib/gitignore.sh +128 -34
  19. package/cli/lib/lockfile.sh +14 -4
  20. package/cli/lib/manifest.sh +315 -21
  21. package/cli/lib/qmap.awk +97 -32
  22. package/cli/lib/registry.sh +19 -14
  23. package/cli/lib/sync-cache.sh +596 -142
  24. package/cli/lib/sync-lock.sh +19 -2
  25. package/engine/ENGINE_SHA +1 -1
  26. package/engine/VERSION +1 -1
  27. package/engine/adapters/_template.sh +6 -2
  28. package/engine/adapters/agents.sh +8 -2
  29. package/engine/adapters/antigravity.sh +3 -1
  30. package/engine/adapters/claude.sh +29 -12
  31. package/engine/adapters/codex.sh +11 -13
  32. package/engine/adapters/copilot.sh +54 -4
  33. package/engine/adapters/cursor.sh +13 -2
  34. package/engine/adapters/opencode.sh +7 -1
  35. package/engine/adapters/pi.sh +3 -1
  36. package/engine/lib/adapter-contract.sh +51 -8
  37. package/engine/lib/common.sh +1072 -160
  38. package/engine/lib/contract.sh +5 -0
  39. package/engine/sync.sh +128 -28
  40. package/package.json +1 -1
  41. package/packages/sync/references/adapters.md +44 -9
  42. package/packages/sync/references/conventions.md +48 -8
  43. package/packages/sync/references/onboarding-migration.md +3 -3
  44. package/packages/sync/skills/intelligence-learn-from-repository/SKILL.md +3 -2
  45. package/packages/sync/skills/intelligence-review-context/SKILL.md +2 -1
  46. package/packages/sync/skills/intelligence-update-context/SKILL.md +4 -2
  47. package/packages/sync/skills/intelligence-update-context/references/agents.md +7 -3
  48. package/packages/sync/skills/intelligence-update-context/references/skills.md +3 -1
@@ -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
- 7dfbb01c2182f3de05bff94f3e5e080cf267f8c6
1
+ c8058f91133b86479a03d7ae950191e2d21b68a6
package/engine/VERSION CHANGED
@@ -1 +1 @@
1
- 0.18.1
1
+ 0.20.0
@@ -27,9 +27,11 @@
27
27
  # has_paths(file) — check if file has paths: field
28
28
  # get_model(config, ide, tier) — resolve model from config or default
29
29
  # get_model_default(ide, tier) — hardcoded default for ide:tier
30
+ # map_effort(tool, effort) — the tool's level for a neutral effort; "" when none
30
31
  # map_access_to_claude_tools(access) — full->"" (no tools list; inherits all), readonly->restricted
31
32
  # map_access_to_claude_disallowed(access) — readonly->"Write, Edit", full->""
32
- # read_yaml_list(manifest, section) — read a source list from intelligence.yaml
33
+ # read_yaml_list(manifest, section) — read a source list from intelligence.yaml,
34
+ # package references as their store paths
33
35
  # get_target_field(config, target, field) — read a target's config field
34
36
 
35
37
  # Project adapters are sourced by engine/sync.sh after the shared library is
@@ -40,7 +42,9 @@
40
42
  # configured target output passed as $1. `owned` paths are exclusively managed;
41
43
  # `managed` paths share a directory with another adapter or hand-authored files.
42
44
  # Add `legacy` inputs to the initial backup, `preserve` for settings that must
43
- # never be replaced, and explicit ignore/include records for Git policy.
45
+ # never be replaced, and explicit ignore/include records for Git policy. $2, when
46
+ # given, is the manifest: read it only to choose Git policy (ignore versus
47
+ # unignore), never ownership — callers that read ownership alone omit it.
44
48
  adapter_contract_<name>() {
45
49
  local output="${1%/}"
46
50
  adapter_contract_version 1
@@ -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
@@ -212,10 +214,14 @@ agents_md_append_rules_list() {
212
214
  # Main entry point for AGENTS.md adapter
213
215
  adapter_contract_agents() {
214
216
  local output
215
- output="$(agents_output_path "$1")"
217
+ agents_output_path_var "$1"
218
+ output="$IS_AGENTS_OUTPUT_PATH"
216
219
  adapter_contract_version 1
217
220
  adapter_contract_owned "$output"
218
221
  adapter_contract_legacy "AGENTS.md"
222
+ # No ignore record, deliberately: AGENTS.md is the tool-neutral fallback
223
+ # every tool reads, and a fresh clone has it only if it is committed —
224
+ # before anyone has run a sync.
219
225
  }
220
226
 
221
227
  sync_to_agents() {
@@ -42,7 +42,9 @@
42
42
  # Every per-file loop batches its work into one awk process (see the batched
43
43
  # helpers in lib/common.sh): process spawns dominate sync time on Windows.
44
44
 
45
- source "$(dirname "${BASH_SOURCE[0]}")/../lib/common.sh"
45
+ # The engine and the CLI load the shared library before any adapter. Only a
46
+ # standalone source of this file loads it here: that costs a dirname process.
47
+ declare -F finalize_output_file >/dev/null 2>&1 || source "$(dirname "${BASH_SOURCE[0]}")/../lib/common.sh"
46
48
 
47
49
  adapter_contract_antigravity() {
48
50
  local output="${1%/}"
@@ -3,13 +3,16 @@
3
3
  # Transforms source prompts to .claude/ format
4
4
  #
5
5
  # Rules: copy as-is (paths: frontmatter preserved)
6
- # Skills: copy skill directories in full (SKILL.md + bundled resources)
7
- # Agents: tier -> model, access -> tools/disallowedTools
6
+ # Skills: copy skill directories in full (SKILL.md + bundled resources),
7
+ # effort -> Claude's effort level
8
+ # Agents: tier -> model, effort -> effort, access -> tools/disallowedTools
8
9
  #
9
10
  # Every per-file loop batches its work into one awk process (see the batched
10
11
  # helpers in lib/common.sh): process spawns dominate sync time on Windows.
11
12
 
12
- 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"
13
16
 
14
17
  adapter_contract_claude() {
15
18
  local output="${1%/}"
@@ -60,6 +63,9 @@ sync_claude_skills() {
60
63
 
61
64
  local src d skill_name
62
65
  local -a skill_dirs=()
66
+ # The inventory decides which skills render, symlinked ones included
67
+ # (decision 0017); the glob keeps this adapter's own listing order.
68
+ skill_source_inventory "$repo_root" "$config_file"
63
69
  load_yaml_list "$config_file" "skills"
64
70
  local list="$IS_YAML_LIST"
65
71
  while IFS= read -r src; do
@@ -69,11 +75,12 @@ sync_claude_skills() {
69
75
  for d in "$dir"/*/; do
70
76
  [ -d "$d" ] || continue
71
77
  [ -f "$d/SKILL.md" ] || continue
78
+ skill_source_rendered "$d" || continue
72
79
  skill_dirs+=("$d")
73
80
  done
74
81
  done <<< "$list"
75
82
  [ "${#skill_dirs[@]}" -gt 0 ] || return 0
76
- copy_skill_bundle_dirs "$output_dir/skills" "${skill_dirs[@]}"
83
+ copy_skill_bundle_dirs_for claude "$output_dir/skills" "${skill_dirs[@]}"
77
84
  for d in "${skill_dirs[@]}"; do
78
85
  skill_name="${d%/}"
79
86
  echo " skill: ${skill_name##*/}"
@@ -81,7 +88,7 @@ sync_claude_skills() {
81
88
  }
82
89
 
83
90
  # Sync all agents to Claude format: one frontmatter pass resolves
84
- # tier/access for every file, one transform pass writes every output.
91
+ # tier/effort/access for every file, one transform pass writes every output.
85
92
  sync_claude_agents() {
86
93
  local repo_root="$1"
87
94
  local config_file="$2"
@@ -104,19 +111,22 @@ sync_claude_agents() {
104
111
 
105
112
  load_model_tiers "$config_file" "claude"
106
113
 
107
- local path tier access spec="" report=""
108
- while IFS=$'\x1f' read -r path tier access; do
114
+ local path tier access effort spec="" report=""
115
+ while IFS=$'\x1f' read -r path tier access effort; do
109
116
  [ -n "$path" ] || continue
110
117
  resolve_model_var "$tier"
118
+ map_effort_var claude "$effort"
111
119
  map_access_to_claude_tools_var "$access"
112
120
  map_access_to_claude_disallowed_var "$access"
113
- spec+="$path"$'\x1f'"$IS_MODEL"$'\x1f'"$IS_CLAUDE_TOOLS"$'\x1f'"$IS_CLAUDE_DISALLOWED"$'\n'
121
+ spec+="$path"$'\x1f'"$IS_MODEL"$'\x1f'"$IS_CLAUDE_TOOLS"$'\x1f'"$IS_CLAUDE_DISALLOWED"$'\x1f'"$IS_EFFORT"$'\n'
114
122
  report+=" agent: ${path##*/} (tier=$tier -> model=$IS_MODEL, access=$access)"$'\n'
115
- done < <(frontmatter_index "tier,access" "${files[@]}")
123
+ done < <(frontmatter_index "tier,access,effort" "${files[@]}")
116
124
 
117
125
  # The transform used to run per file with the values injected via -v;
118
126
  # the batched form carries them per file through the environment (awk -v
119
- # would reprocess backslash escapes).
127
+ # would reprocess backslash escapes). The first frontmatter `effort:` line
128
+ # becomes Claude's level in place, or goes when there is none; a later
129
+ # one goes too, since frontmatter_index read only the first.
120
130
  is_fin_awk_vars
121
131
  IS_MAP_SPEC="$spec" awk "${IS_FIN_V[@]}" -v dst="$output_dir/agents" "$IS_AWK_LIB"'
122
132
  BEGIN {
@@ -125,18 +135,25 @@ sync_claude_agents() {
125
135
  for (i = 1; i <= n; i++) {
126
136
  if (recs[i] == "") continue
127
137
  split(recs[i], f, US)
128
- MODEL[f[1]] = f[2]; TOOLS[f[1]] = f[3]; EXTRA[f[1]] = f[4]
138
+ MODEL[f[1]] = f[2]; TOOLS[f[1]] = f[3]; EXTRA[f[1]] = f[4]; EFFORT[f[1]] = f[5]
129
139
  }
130
140
  }
131
141
  FNR == 1 {
132
142
  if (out != "") close(out)
133
143
  out = dst "/" base_name(FILENAME)
134
144
  count = 0
145
+ fm = 0
146
+ effort_seen = 0
135
147
  }
136
148
  /^tier:/ { next }
137
149
  /^access:/ { next }
138
150
  { sub(/\r$/, "") }
151
+ FNR == 1 && $0 == "---" { fm = 1 }
139
152
  /^---$/ { count++ }
153
+ fm && count == 1 && substr($0, 1, 7) == "effort:" {
154
+ if (!effort_seen++ && EFFORT[FILENAME] != "") print fin_line("effort: " EFFORT[FILENAME]) > out
155
+ next
156
+ }
140
157
  count == 2 && /^---$/ {
141
158
  if (TOOLS[FILENAME] != "") print fin_line("tools: " TOOLS[FILENAME]) > out
142
159
  if (EXTRA[FILENAME] != "") print fin_line("disallowedTools: " EXTRA[FILENAME]) > out
@@ -165,7 +182,7 @@ sync_to_claude() {
165
182
  # commands/, statusline.sh, etc.).
166
183
  rm -rf "$output_dir/rules" "$output_dir/agents"
167
184
  if [ -d "$output_dir/skills" ]; then
168
- find "$output_dir/skills" -mindepth 1 -maxdepth 1 -type d -exec rm -rf {} +
185
+ find "$output_dir/skills" -mindepth 1 -maxdepth 1 \( -type d -o -type l \) -exec rm -rf {} +
169
186
  fi
170
187
  mkdir -p "$output_dir/rules" "$output_dir/skills" "$output_dir/agents"
171
188
 
@@ -9,12 +9,16 @@
9
9
  # Skills: copy skill directories in full to .agents/skills/{name}/ (Codex
10
10
  # reads from $REPO_ROOT/.agents/skills exclusively per official docs)
11
11
  # Agents: -> .codex/agents/{name}.toml (name, description, model,
12
- # model_reasoning_effort, sandbox_mode, developer_instructions)
12
+ # model_reasoning_effort, sandbox_mode, developer_instructions).
13
+ # model_reasoning_effort comes from `effort:` alone — never from the tier —
14
+ # and is omitted without one, so Codex's own setting applies.
13
15
  #
14
16
  # Every per-file loop batches its work into one awk process (see the batched
15
17
  # helpers in lib/common.sh): process spawns dominate sync time on Windows.
16
18
 
17
- source "$(dirname "${BASH_SOURCE[0]}")/../lib/common.sh"
19
+ # The engine and the CLI load the shared library before any adapter. Only a
20
+ # standalone source of this file loads it here: that costs a dirname process.
21
+ declare -F finalize_output_file >/dev/null 2>&1 || source "$(dirname "${BASH_SOURCE[0]}")/../lib/common.sh"
18
22
 
19
23
  adapter_contract_codex() {
20
24
  local output="${1%/}"
@@ -97,18 +101,12 @@ sync_codex_agents() {
97
101
  if [ "${#files[@]}" -gt 0 ]; then
98
102
  load_model_tiers "$config_file" "codex"
99
103
 
100
- local path tier access description name effort sandbox spec="" report="" LS=$'\x1e'
101
- while IFS=$'\x1f' read -r path tier access description; do
104
+ local path tier access description effort name sandbox spec="" report="" LS=$'\x1e'
105
+ while IFS=$'\x1f' read -r path tier access description effort; do
102
106
  [ -n "$path" ] || continue
103
107
  name="${path##*/}"; name="${name%.md}"
104
108
  resolve_model_var "$tier"
105
- case "$tier" in
106
- frontier) effort="xhigh" ;;
107
- heavy|"") effort="high" ;;
108
- standard) effort="medium" ;;
109
- light) effort="low" ;;
110
- *) effort="medium" ;;
111
- esac
109
+ map_effort_var codex "$effort"
112
110
  case "$access" in
113
111
  readonly) sandbox="read-only" ;;
114
112
  *) sandbox="workspace-write" ;;
@@ -121,7 +119,7 @@ sync_codex_agents() {
121
119
  header="${LS}name = \"$name_escaped\""
122
120
  header+="${LS}description = \"$description_escaped\""
123
121
  header+="${LS}model = \"$IS_MODEL\""
124
- header+="${LS}model_reasoning_effort = \"$effort\""
122
+ [ -z "$IS_EFFORT" ] || header+="${LS}model_reasoning_effort = \"$IS_EFFORT\""
125
123
  header+="${LS}sandbox_mode = \"$sandbox\""
126
124
  header+="${LS}"
127
125
  header+="${LS}developer_instructions = \"\"\""
@@ -129,7 +127,7 @@ sync_codex_agents() {
129
127
  spec+="$path"$'\x1f'"$output_dir/$name.toml"$'\x1f'"fence"$'\x1f'"1"$'\x1f'"toml"$'\x1f'"$header"$'\x1f'"${LS}\"\"\""$'\n'
130
128
  report+=" agent: $name.toml"$'\n'
131
129
  count=$((count + 1))
132
- done < <(frontmatter_index "tier,access,description" "${files[@]}")
130
+ done < <(frontmatter_index "tier,access,description,effort" "${files[@]}")
133
131
 
134
132
  emit_wrapped_bodies "$spec"
135
133
  printf '%s' "$report"
@@ -12,13 +12,22 @@
12
12
  # Skills: copy skill directories in full to .github/skills/{name}/
13
13
  # Agents: -> .github/agents/{name}.agent.md (description, tools, model)
14
14
  #
15
+ # Git policy (decision 0020): generated output is ignored by default, as it is
16
+ # for Cursor and Claude Code — Copilot in the editor reads it from disk after
17
+ # sync. GitHub's cloud Copilot (code review, the coding agent) reads the
18
+ # repository instead, so `targets.copilot.commit_output: true` keeps the output
19
+ # tracked. Only the generated subdirectories are named, never `.github/`
20
+ # itself: workflows, templates and hand-written files there stay tracked.
21
+ #
15
22
  # Every per-file loop batches its work into one awk process (see the batched
16
23
  # helpers in lib/common.sh): process spawns dominate sync time on Windows.
17
24
 
18
- source "$(dirname "${BASH_SOURCE[0]}")/../lib/common.sh"
25
+ # The engine and the CLI load the shared library before any adapter. Only a
26
+ # standalone source of this file loads it here: that costs a dirname process.
27
+ declare -F finalize_output_file >/dev/null 2>&1 || source "$(dirname "${BASH_SOURCE[0]}")/../lib/common.sh"
19
28
 
20
29
  adapter_contract_copilot() {
21
- local output="${1%/}"
30
+ local output="${1%/}" record=adapter_contract_ignore sub
22
31
  adapter_contract_version 1
23
32
  adapter_contract_requires agents
24
33
  adapter_contract_owned "$output/instructions"
@@ -26,6 +35,37 @@ adapter_contract_copilot() {
26
35
  adapter_contract_owned "$output/agents"
27
36
  adapter_contract_owned "$output/skills"
28
37
  adapter_contract_legacy "$output/copilot-instructions.md"
38
+ # A malformed value declares the default here; sync refuses it with the
39
+ # reason, so the policy never rests on a guess.
40
+ if copilot_commit_output_var "${2:-}" && [ "$IS_COPILOT_COMMIT_OUTPUT" = 1 ]; then
41
+ record=adapter_contract_unignore
42
+ fi
43
+ # The same four paths the contract owns: every sync rewrites them, so the
44
+ # ignore covers generated output only.
45
+ for sub in instructions prompts agents skills; do
46
+ "$record" "$output/$sub/"
47
+ done
48
+ # The root instructions file onboarding quarantines, like CLAUDE.md and
49
+ # .cursorrules: its content migrates into project rules, and a copy left
50
+ # behind makes Copilot ignore AGENTS.md (copilot-cli#489).
51
+ "$record" "$output/copilot-instructions.md"
52
+ }
53
+
54
+ # copilot_commit_output_var <manifest> — the one reader of
55
+ # targets.copilot.commit_output. Sets IS_COPILOT_COMMIT_OUTPUT to 1 when the
56
+ # project commits generated output and 0 for the default; returns 1 for any
57
+ # value other than true or false. No manifest — a caller reading ownership
58
+ # only — is the default.
59
+ copilot_commit_output_var() {
60
+ local manifest="$1" setting
61
+ IS_COPILOT_COMMIT_OUTPUT=0
62
+ [ -n "$manifest" ] && [ -f "$manifest" ] || return 0
63
+ setting="$(get_target_field "$manifest" "copilot" "commit_output")"
64
+ case "$setting" in
65
+ ''|false) ;;
66
+ true) IS_COPILOT_COMMIT_OUTPUT=1 ;;
67
+ *) return 1 ;;
68
+ esac
29
69
  }
30
70
 
31
71
  # copilot_paths_rows <file>... — batch form of the old per-file `paths:`
@@ -128,6 +168,9 @@ sync_copilot_skills() {
128
168
 
129
169
  local src d skill_name
130
170
  local -a skill_dirs=()
171
+ # The inventory decides which skills render, symlinked ones included
172
+ # (decision 0017); the glob keeps this adapter's own listing order.
173
+ skill_source_inventory "$repo_root" "$config_file"
131
174
  load_yaml_list "$config_file" "skills"
132
175
  local list="$IS_YAML_LIST"
133
176
  while IFS= read -r src; do
@@ -137,13 +180,15 @@ sync_copilot_skills() {
137
180
  for d in "$dir"/*/; do
138
181
  [ -d "$d" ] || continue
139
182
  [ -f "$d/SKILL.md" ] || continue
183
+ skill_source_rendered "$d" || continue
140
184
  skill_dirs+=("$d")
141
185
  done
142
186
  done <<< "$list"
143
187
 
144
188
  local count=0
145
189
  if [ "${#skill_dirs[@]}" -gt 0 ]; then
146
- copy_skill_bundle_dirs "$output_dir/skills" "${skill_dirs[@]}"
190
+ # Copilot has no effort field: the copy carries no `effort:`.
191
+ copy_skill_bundle_dirs_for copilot "$output_dir/skills" "${skill_dirs[@]}"
147
192
  for d in "${skill_dirs[@]}"; do
148
193
  skill_name="${d%/}"
149
194
  count=$((count + 1))
@@ -221,10 +266,15 @@ sync_to_copilot() {
221
266
 
222
267
  echo "=== GitHub Copilot ==="
223
268
 
269
+ copilot_commit_output_var "$config_file" || {
270
+ echo "ERROR: targets.copilot.commit_output must be true or false: true keeps generated Copilot output tracked, false or omission ignores it." >&2
271
+ return 1
272
+ }
273
+
224
274
  # Clean generated content (preserve workflows, etc.)
225
275
  rm -rf "$output_dir/instructions" "$output_dir/prompts"
226
276
  if [ -d "$output_dir/skills" ]; then
227
- find "$output_dir/skills" -mindepth 1 -maxdepth 1 -type d -exec rm -rf {} +
277
+ find "$output_dir/skills" -mindepth 1 -maxdepth 1 \( -type d -o -type l \) -exec rm -rf {} +
228
278
  fi
229
279
  rm -rf "$output_dir/agents"
230
280
  mkdir -p "$output_dir" "$output_dir/skills" "$output_dir/agents"
@@ -10,11 +10,15 @@
10
10
  # same rules would cause double-loading and burn the context window.
11
11
  # Skills: copy skill directories in full (SKILL.md + bundled resources)
12
12
  # Agents: strip tier/access/tools/disallowedTools, add model + readonly:true
13
+ # Cursor has no per-agent or per-skill effort field (its effort is part of a
14
+ # model ID), so `effort:` is stripped from both.
13
15
  #
14
16
  # Every per-file loop batches its work into one awk process (see the batched
15
17
  # helpers in lib/common.sh): process spawns dominate sync time on Windows.
16
18
 
17
- source "$(dirname "${BASH_SOURCE[0]}")/../lib/common.sh"
19
+ # The engine and the CLI load the shared library before any adapter. Only a
20
+ # standalone source of this file loads it here: that costs a dirname process.
21
+ declare -F finalize_output_file >/dev/null 2>&1 || source "$(dirname "${BASH_SOURCE[0]}")/../lib/common.sh"
18
22
 
19
23
  adapter_contract_cursor() {
20
24
  local output="${1%/}"
@@ -100,6 +104,9 @@ sync_cursor_skills() {
100
104
 
101
105
  local src d skill_name
102
106
  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"
103
110
  load_yaml_list "$config_file" "skills"
104
111
  local list="$IS_YAML_LIST"
105
112
  while IFS= read -r src; do
@@ -109,11 +116,12 @@ sync_cursor_skills() {
109
116
  for d in "$dir"/*/; do
110
117
  [ -d "$d" ] || continue
111
118
  [ -f "$d/SKILL.md" ] || continue
119
+ skill_source_rendered "$d" || continue
112
120
  skill_dirs+=("$d")
113
121
  done
114
122
  done <<< "$list"
115
123
  [ "${#skill_dirs[@]}" -gt 0 ] || return 0
116
- copy_skill_bundle_dirs "$output_dir/skills" "${skill_dirs[@]}"
124
+ copy_skill_bundle_dirs_for cursor "$output_dir/skills" "${skill_dirs[@]}"
117
125
  for d in "${skill_dirs[@]}"; do
118
126
  skill_name="${d%/}"
119
127
  echo " skill: ${skill_name##*/}"
@@ -170,13 +178,16 @@ sync_cursor_agents() {
170
178
  if (out != "") close(out)
171
179
  out = dst "/" base_name(FILENAME)
172
180
  count = 0
181
+ fm = 0
173
182
  }
174
183
  /^tier:/ { next }
175
184
  /^access:/ { next }
176
185
  /^tools:/ { next }
177
186
  /^disallowedTools:/ { next }
178
187
  { sub(/\r$/, "") }
188
+ FNR == 1 && $0 == "---" { fm = 1 }
179
189
  /^---$/ { count++ }
190
+ fm && count == 1 && substr($0, 1, 7) == "effort:" { next }
180
191
  count == 2 && /^---$/ {
181
192
  print fin_line("model: " MODEL[FILENAME]) > out
182
193
  if (RO[FILENAME] != "") print fin_line(RO[FILENAME]) > out
@@ -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%/}"
@@ -88,6 +90,9 @@ sync_opencode_commands() {
88
90
 
89
91
  local src d skill_name
90
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"
91
96
  load_yaml_list "$config_file" "skills"
92
97
  local list="$IS_YAML_LIST"
93
98
  while IFS= read -r src; do
@@ -97,6 +102,7 @@ sync_opencode_commands() {
97
102
  for d in "$dir"/*/; do
98
103
  [ -d "$d" ] || continue
99
104
  [ -f "$d/SKILL.md" ] || continue
105
+ skill_source_rendered "$d" || continue
100
106
  skill_mds+=("${d%/}/SKILL.md")
101
107
  done
102
108
  done <<< "$list"
@@ -16,7 +16,9 @@
16
16
  # Every per-file loop batches its work into one awk process (see the batched
17
17
  # helpers in lib/common.sh): process spawns dominate sync time on Windows.
18
18
 
19
- source "$(dirname "${BASH_SOURCE[0]}")/../lib/common.sh"
19
+ # The engine and the CLI load the shared library before any adapter. Only a
20
+ # standalone source of this file loads it here: that costs a dirname process.
21
+ declare -F finalize_output_file >/dev/null 2>&1 || source "$(dirname "${BASH_SOURCE[0]}")/../lib/common.sh"
20
22
 
21
23
  adapter_contract_pi() {
22
24
  local output="${1%/}"
@@ -1,10 +1,17 @@
1
1
  #!/bin/bash
2
2
  # Declarative adapter ownership contract shared by the engine and CLI.
3
3
  #
4
- # Every adapter exposes adapter_contract_<name> <configured-output>. The
5
- # function emits tab-separated records through the helpers below. Keeping the
4
+ # Every adapter exposes adapter_contract_<name> <configured-output> [manifest].
5
+ # The function emits tab-separated records through the helpers below. Keeping the
6
6
  # declaration beside sync_to_<name>() makes backup, rollback, git policy and
7
7
  # lifecycle checks consume the same ownership model as the writer itself.
8
+ #
9
+ # The manifest argument exists for Git policy alone: a target field may choose
10
+ # whether generated output is ignored (targets.copilot.commit_output). Ownership
11
+ # never depends on it, so the engine and the sync cache, which read ownership
12
+ # only, pass none and spare every sync the extra read; a contract given none
13
+ # declares its default policy. Every caller that applies or checks Git policy
14
+ # passes the manifest.
8
15
 
9
16
  adapter_contract_version() { printf 'version\t%s\n' "$1"; }
10
17
  adapter_contract_requires() { printf 'requires\t%s\n' "$1"; }
@@ -14,6 +21,10 @@ adapter_contract_legacy() { printf 'legacy\t%s\n' "$1"; }
14
21
  adapter_contract_preserve() { printf 'preserve\t%s\n' "$1"; }
15
22
  adapter_contract_ignore() { printf 'ignore\t%s\n' "$1"; }
16
23
  adapter_contract_include() { printf 'include\t%s\n' "$1"; }
24
+ # An ignore line the policy must not hold: the writer takes it back out of the
25
+ # region it manages. It is how a configuration that keeps output tracked undoes
26
+ # the default ignore an earlier alignment already wrote.
27
+ adapter_contract_unignore() { printf 'unignore\t%s\n' "$1"; }
17
28
 
18
29
  # --- cross-adapter ownership -------------------------------------------------
19
30
  #
@@ -106,12 +117,12 @@ adapter_contract_safe_concrete_path() {
106
117
  esac
107
118
  }
108
119
 
109
- # adapter_contract_records <adapter-name> <adapter-file> <configured-output>
120
+ # adapter_contract_records <adapter-name> <adapter-file> <configured-output> [manifest]
110
121
  # Source and query in a subshell so a project adapter cannot leak shell state
111
122
  # into the caller. Project adapters are trusted executable code during sync;
112
123
  # the isolation here is for correctness, not a security boundary.
113
124
  adapter_contract_records() (
114
- local name="$1" file="$2" output="$3" fn line kind value saw_version=0
125
+ local name="$1" file="$2" output="$3" manifest="${4:-}" fn
115
126
  # shellcheck source=/dev/null
116
127
  source "$file"
117
128
  printf -v fn 'adapter_contract_%s' "$name"
@@ -119,6 +130,38 @@ adapter_contract_records() (
119
130
  echo "ERROR: adapter '$name' has no $fn contract" >&2
120
131
  return 1
121
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
122
165
  while IFS= read -r line; do
123
166
  [ -n "$line" ] || continue
124
167
  kind="${line%%$'\t'*}"
@@ -149,7 +192,7 @@ adapter_contract_records() (
149
192
  return 1
150
193
  }
151
194
  ;;
152
- ignore|include)
195
+ ignore|include|unignore)
153
196
  adapter_contract_safe_path "$value" || {
154
197
  echo "ERROR: adapter '$name' declares unsafe $kind path '$value'" >&2
155
198
  return 1
@@ -160,10 +203,10 @@ adapter_contract_records() (
160
203
  return 1
161
204
  ;;
162
205
  esac
163
- printf '%s\n' "$line"
164
- done < <("$fn" "$output")
206
+ printf '%s%s\n' "$prefix" "$line"
207
+ done
165
208
  [ "$saw_version" -eq 1 ] || {
166
209
  echo "ERROR: adapter '$name' contract did not declare version 1" >&2
167
210
  return 1
168
211
  }
169
- )
212
+ }