@ainova-systems/intelligence 0.20.0 → 0.22.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.
- package/cli/commands/init.sh +2 -1
- package/cli/commands/sync.sh +9 -0
- package/cli/internal/check.sh +39 -0
- package/cli/lib/gitignore.sh +19 -2
- package/cli/lib/sync-lock.sh +103 -21
- package/engine/ENGINE_SHA +1 -1
- package/engine/VERSION +1 -1
- package/engine/adapters/_template.sh +32 -8
- package/engine/adapters/agents.sh +42 -50
- package/engine/adapters/antigravity.sh +68 -52
- package/engine/adapters/claude-plugin/.claude-plugin/plugin.json +6 -0
- package/engine/adapters/claude-plugin/README.md +14 -0
- package/engine/adapters/claude-plugin/hooks/hooks.json +3 -0
- package/engine/adapters/claude-plugin/hooks/register.js +324 -0
- package/engine/adapters/claude.sh +129 -59
- package/engine/adapters/codex.sh +74 -15
- package/engine/adapters/copilot.sh +56 -79
- package/engine/adapters/cursor.sh +85 -81
- package/engine/adapters/opencode.sh +20 -35
- package/engine/adapters/pi.sh +137 -41
- package/engine/lib/common.sh +753 -56
- package/engine/lib/contract.sh +15 -6
- package/engine/sync.sh +49 -33
- package/package.json +1 -1
- package/packages/sync/references/adapters.md +56 -40
- package/packages/sync/references/conventions.md +57 -11
- package/packages/sync/references/onboarding-migration.md +4 -4
- package/packages/sync/rules/intelligence-authoring.md +2 -2
- package/packages/sync/skills/intelligence-learn-from-repository/SKILL.md +7 -5
- package/packages/sync/skills/intelligence-review-context/references/audit-checks.md +1 -1
- package/packages/sync/skills/intelligence-update-context/references/rules.md +5 -0
package/cli/commands/init.sh
CHANGED
|
@@ -356,7 +356,8 @@ manifest="$root/intelligence.yaml"
|
|
|
356
356
|
} > "$manifest"
|
|
357
357
|
|
|
358
358
|
# The package store is restorable state, never committed — the npm model.
|
|
359
|
-
|
|
359
|
+
# Local rules, agents and skills stay on this machine (decision 0021).
|
|
360
|
+
ensure_base_gitignore "$root" "$content_dir"
|
|
360
361
|
for target in $targets; do
|
|
361
362
|
ensure_target_gitignore "$root" "$manifest" "$target"
|
|
362
363
|
done
|
package/cli/commands/sync.sh
CHANGED
|
@@ -106,6 +106,10 @@ run_check() {
|
|
|
106
106
|
die "no intelligence project found here - run 'intelligence init'"
|
|
107
107
|
;;
|
|
108
108
|
esac
|
|
109
|
+
# A check marks the lock it holds, so a sync started meanwhile waits for
|
|
110
|
+
# it instead of refusing (sync-lock.sh).
|
|
111
|
+
# shellcheck disable=SC2034 # read by sync-lock.sh when the lock is taken
|
|
112
|
+
SYNC_LOCK_KIND=check
|
|
109
113
|
hold_project
|
|
110
114
|
project_preflight "$IP_ROOT" || return $?
|
|
111
115
|
if project_needs_upgrade "$IP_ROOT"; then
|
|
@@ -144,6 +148,11 @@ fi
|
|
|
144
148
|
compact_output="$(mktemp -t intelligence-sync-XXXXXX)"
|
|
145
149
|
trap 'rm -f "$compact_output"' EXIT
|
|
146
150
|
rc=0
|
|
151
|
+
# The one line that must not wait for the buffer: a wait for a check holding
|
|
152
|
+
# the lock can last minutes (sync-lock.sh). Descriptor 9 keeps the real stderr.
|
|
153
|
+
exec 9>&2
|
|
154
|
+
# shellcheck disable=SC2034 # read by sync-lock.sh while the lock is taken
|
|
155
|
+
IS_LOCK_NOTICE_FD=9
|
|
147
156
|
(run_sync) > "$compact_output" 2>&1 || rc=$?
|
|
148
157
|
if [ "$rc" -ne 0 ]; then
|
|
149
158
|
cat "$compact_output" >&2
|
package/cli/internal/check.sh
CHANGED
|
@@ -198,6 +198,14 @@ while IFS= read -r target; do
|
|
|
198
198
|
ok "adapter $target contract v1"
|
|
199
199
|
done < <(target_names "$manifest")
|
|
200
200
|
|
|
201
|
+
# A local rule, agent or skill is one these patterns name (decision 0021): the
|
|
202
|
+
# engine keeps it out of every committed output, and without the pattern Git
|
|
203
|
+
# would still offer to commit the file itself.
|
|
204
|
+
while IFS= read -r pattern; do
|
|
205
|
+
ignore_file_has_line "$IP_ROOT/.gitignore" "$pattern" \
|
|
206
|
+
|| warn "local artifact Git policy is missing '$pattern' — run 'intelligence init'"
|
|
207
|
+
done < <(local_artifact_ignore_patterns "$content_dir")
|
|
208
|
+
|
|
201
209
|
# Lock orphans.
|
|
202
210
|
if [ "$lock_valid" -eq 1 ] && [ -f "$lock" ]; then
|
|
203
211
|
while IFS= read -r name; do
|
|
@@ -214,6 +222,7 @@ fi
|
|
|
214
222
|
# is exactly why doctor names it). Each entry is judged by the directory the
|
|
215
223
|
# engine renders it as, read through the same parser: a package reference by
|
|
216
224
|
# its store path, one that resolves to nothing by why.
|
|
225
|
+
local_specs=()
|
|
217
226
|
for section in rules agents skills; do
|
|
218
227
|
seen=$'\n'
|
|
219
228
|
while IFS=$'\037' read -r src state dir alias holders; do
|
|
@@ -269,10 +278,40 @@ for section in rules agents skills; do
|
|
|
269
278
|
# authoring later needs no config edit.
|
|
270
279
|
*) ok "sources.$section '$src' — not created yet (optional)" ;;
|
|
271
280
|
esac
|
|
281
|
+
continue
|
|
272
282
|
fi
|
|
283
|
+
# A project-owned source is where a tracked file can carry a local
|
|
284
|
+
# name; store content is restorable state Git never tracks.
|
|
285
|
+
case "$dir" in
|
|
286
|
+
.intelligence/*) ;;
|
|
287
|
+
*)
|
|
288
|
+
if [ "$section" = skills ]; then
|
|
289
|
+
local_specs+=(":(glob)${dir%/}/*.local/SKILL.md")
|
|
290
|
+
else
|
|
291
|
+
local_specs+=(":(glob)${dir%/}/*.local.md")
|
|
292
|
+
fi
|
|
293
|
+
;;
|
|
294
|
+
esac
|
|
273
295
|
done < <(read_source_entries "$manifest" "$section")
|
|
274
296
|
done
|
|
275
297
|
|
|
298
|
+
# A name makes an artifact local (decision 0021), and .gitignore never applies
|
|
299
|
+
# to a file Git already tracks: one committed before its name meant anything,
|
|
300
|
+
# or added by hand, renders only into ignored output while every clone holds
|
|
301
|
+
# it. Git and the engine would disagree about it, so it is named here.
|
|
302
|
+
if [ "${#local_specs[@]}" -gt 0 ] \
|
|
303
|
+
&& git -C "$IP_ROOT" rev-parse --is-inside-work-tree >/dev/null 2>&1; then
|
|
304
|
+
if tracked_local="$(git -C "$IP_ROOT" -c core.quotePath=false ls-files -- "${local_specs[@]}")"; then
|
|
305
|
+
while IFS= read -r path; do
|
|
306
|
+
[ -n "$path" ] || continue
|
|
307
|
+
case "$path" in */SKILL.md) path="${path%/SKILL.md}/" ;; esac
|
|
308
|
+
warn "$path is tracked by Git, but its name makes it local: sync renders it only into ignored output — rename it to share it, or 'git rm --cached' it to keep it on this machine"
|
|
309
|
+
done <<< "$tracked_local"
|
|
310
|
+
else
|
|
311
|
+
warn "tracked local artifacts could not be listed — 'git ls-files' failed"
|
|
312
|
+
fi
|
|
313
|
+
fi
|
|
314
|
+
|
|
276
315
|
if [ "$problems" -eq 0 ]; then
|
|
277
316
|
echo "all good."
|
|
278
317
|
else
|
package/cli/lib/gitignore.sh
CHANGED
|
@@ -312,10 +312,26 @@ ensure_gitignore_header() {
|
|
|
312
312
|
fi
|
|
313
313
|
}
|
|
314
314
|
|
|
315
|
+
# local_artifact_ignore_patterns <content-dir> — the patterns that make a rule,
|
|
316
|
+
# agent or skill local (decision 0021): a file or skill directory with one of
|
|
317
|
+
# these names never reaches committed output, and Git never commits it. The
|
|
318
|
+
# engine recognizes the same names, so the two cannot disagree; `init` writes
|
|
319
|
+
# them and `status --check` reports one missing.
|
|
320
|
+
local_artifact_ignore_patterns() {
|
|
321
|
+
local dir="${1#./}"
|
|
322
|
+
while [ "${dir%/}" != "$dir" ]; do dir="${dir%/}"; done
|
|
323
|
+
printf '%s\n' "$dir/rules/*.local.md" "$dir/agents/*.local.md" "$dir/skills/*.local/"
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
# ensure_base_gitignore <root> <content-dir> — the package store and the local
|
|
327
|
+
# artifact patterns of the project's content directory.
|
|
315
328
|
ensure_base_gitignore() {
|
|
316
|
-
local root="$1"
|
|
329
|
+
local root="$1" content_dir="$2" line
|
|
317
330
|
ensure_gitignore_header "$root"
|
|
318
331
|
gitignore_add_line "$root" '.intelligence/'
|
|
332
|
+
while IFS= read -r line; do
|
|
333
|
+
gitignore_add_line "$root" "$line"
|
|
334
|
+
done < <(local_artifact_ignore_patterns "$content_dir")
|
|
319
335
|
}
|
|
320
336
|
|
|
321
337
|
ensure_target_gitignore() {
|
|
@@ -343,7 +359,7 @@ ensure_target_gitignore() {
|
|
|
343
359
|
|
|
344
360
|
ensure_manifest_gitignore() {
|
|
345
361
|
local root="$1" manifest="$2" target
|
|
346
|
-
ensure_base_gitignore "$root"
|
|
362
|
+
ensure_base_gitignore "$root" "$(manifest_intelligence_dir "$manifest")"
|
|
347
363
|
while IFS= read -r target; do
|
|
348
364
|
[ -n "$target" ] || continue
|
|
349
365
|
if [ "$(is_target_enabled "$manifest" "$target")" = "1" ]; then
|
|
@@ -420,6 +436,7 @@ managed_gitignore_patterns() {
|
|
|
420
436
|
local root="$1" manifest="$2" content_dir target output records kind value
|
|
421
437
|
content_dir="$(manifest_intelligence_dir "$manifest")"
|
|
422
438
|
printf '%s\n' '.intelligence/' "$content_dir/_backup/"
|
|
439
|
+
local_artifact_ignore_patterns "$content_dir"
|
|
423
440
|
while IFS= read -r target; do
|
|
424
441
|
[ -n "$target" ] || continue
|
|
425
442
|
[ "$(is_target_enabled "$manifest" "$target")" = "1" ] || continue
|
package/cli/lib/sync-lock.sh
CHANGED
|
@@ -12,9 +12,25 @@
|
|
|
12
12
|
# join instead of refusing, and a holder that died can be told from one whose
|
|
13
13
|
# writers still run. Taking over a dead holder's lock happens under a second
|
|
14
14
|
# lock, so two runs that judged the same lock stale cannot both take it.
|
|
15
|
+
#
|
|
16
|
+
# `sync --check` holds the lock too, because it may restore the package store
|
|
17
|
+
# and compares a copy of the live outputs. It writes nothing a person keeps, so
|
|
18
|
+
# a holder that is a check marks its lock with a `kind` file, and a writing
|
|
19
|
+
# command that finds such a holder waits for it instead of refusing (decision
|
|
20
|
+
# 0022): a status check that runs every few minutes must never make the
|
|
21
|
+
# person's own sync fail. A check never waits in line — it refuses any holder
|
|
22
|
+
# at once — so it is never queued behind a writer; one that arrives as a lock is
|
|
23
|
+
# released can still take it first, and the waiting writer's bound then starts
|
|
24
|
+
# again for that check. A writing holder is refused at once, as before.
|
|
15
25
|
|
|
16
26
|
SYNC_LOCK_HELD=""
|
|
17
27
|
SYNC_LOCK_TOKEN=""
|
|
28
|
+
# What this process takes the lock for: "check" for `sync --check`, empty for
|
|
29
|
+
# every writer. Set before the lock is taken.
|
|
30
|
+
SYNC_LOCK_KIND=""
|
|
31
|
+
# How long a writer waits for each check that holds the lock, in seconds. Kept
|
|
32
|
+
# in the environment only so the lock suite can exercise the bound quickly.
|
|
33
|
+
SYNC_LOCK_CHECK_WAIT="${INTELLIGENCE_SYNC_LOCK_CHECK_WAIT:-300}"
|
|
18
34
|
|
|
19
35
|
sync_lock_now() {
|
|
20
36
|
if [ -n "${EPOCHSECONDS:-}" ]; then
|
|
@@ -47,6 +63,9 @@ sync_lock_pgid() {
|
|
|
47
63
|
}
|
|
48
64
|
|
|
49
65
|
sync_lock_write_owner() {
|
|
66
|
+
# The kind goes first, before anything that costs time: whoever can read
|
|
67
|
+
# the owner can read the kind too.
|
|
68
|
+
[ -z "$SYNC_LOCK_KIND" ] || printf '%s\n' "$SYNC_LOCK_KIND" > "$1/kind"
|
|
50
69
|
sync_lock_now
|
|
51
70
|
sync_lock_pgid
|
|
52
71
|
SYNC_LOCK_TOKEN="$$.$IS_NOW.$RANDOM$RANDOM"
|
|
@@ -88,31 +107,89 @@ sync_lock_is_stale() {
|
|
|
88
107
|
[ -d "$1" ] && [ -n "$(find "$1" -maxdepth 0 -mmin +1 2>/dev/null)" ]
|
|
89
108
|
}
|
|
90
109
|
|
|
91
|
-
#
|
|
110
|
+
# sync_lock_held_by_check <lock> — true when the holder marked its lock as a
|
|
111
|
+
# `sync --check`. A lock without the mark is a writer's, or an older CLI's.
|
|
112
|
+
sync_lock_held_by_check() {
|
|
113
|
+
local kind=""
|
|
114
|
+
# The group silences the redirection too: a kind released between the
|
|
115
|
+
# test and the read must not print a stray error a caller would show.
|
|
116
|
+
[ -f "$1/kind" ] && { IFS= read -r kind < "$1/kind"; } 2>/dev/null
|
|
117
|
+
[ "$kind" = check ]
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
# sync_lock_acquire <root> — take <root>/.intelligence/sync.lock, or refuse.
|
|
121
|
+
# A writer waits for each check that holds it for up to SYNC_LOCK_CHECK_WAIT
|
|
122
|
+
# seconds; a check refuses at once.
|
|
92
123
|
sync_lock_acquire() {
|
|
93
|
-
local root="$1" lock="$1/.intelligence/sync.lock" took=0
|
|
124
|
+
local root="$1" lock="$1/.intelligence/sync.lock" took age="" started="$SECONDS" told=0 ownerless=""
|
|
125
|
+
local holder="" seen
|
|
94
126
|
# A process start costs tens of milliseconds on Git Bash: skip the usual case.
|
|
95
127
|
[ -d "$root/.intelligence" ] || mkdir -p "$root/.intelligence" || die "cannot create $root/.intelligence"
|
|
96
|
-
|
|
97
|
-
took=
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
128
|
+
while :; do
|
|
129
|
+
took=0
|
|
130
|
+
if mkdir "$lock" 2>/dev/null; then
|
|
131
|
+
took=1
|
|
132
|
+
elif sync_lock_is_stale "$lock" && mkdir "$lock.takeover" 2>/dev/null; then
|
|
133
|
+
# The takeover lock is taken only for a lock that already looks
|
|
134
|
+
# stale: a command waiting for a live check must never hold it,
|
|
135
|
+
# since one interrupted mid-wait would leave it behind for good.
|
|
136
|
+
# Judge again under it: another run may have taken the lock over
|
|
137
|
+
# since this one looked.
|
|
138
|
+
if sync_lock_is_stale "$lock"; then
|
|
139
|
+
rm -rf "$lock"
|
|
140
|
+
mkdir "$lock" 2>/dev/null && took=1
|
|
141
|
+
fi
|
|
142
|
+
rmdir "$lock.takeover"
|
|
143
|
+
elif [ -d "$lock.takeover" ] && [ -n "$(find "$lock.takeover" -maxdepth 0 -mmin +1 2>/dev/null)" ]; then
|
|
144
|
+
# A takeover runs for milliseconds. Removing an abandoned one is itself
|
|
145
|
+
# a check-then-delete race, so it is left to a person.
|
|
146
|
+
die "a takeover of $lock was abandoned; if no intelligence command is running in this project, remove $lock.takeover"
|
|
104
147
|
fi
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
148
|
+
if [ "$took" = 1 ]; then
|
|
149
|
+
sync_lock_write_owner "$lock" || { rm -rf "$lock"; die "cannot write $lock/owner"; }
|
|
150
|
+
SYNC_LOCK_HELD="$lock"
|
|
151
|
+
return 0
|
|
152
|
+
fi
|
|
153
|
+
if [ "$SYNC_LOCK_KIND" != check ] && sync_lock_held_by_check "$lock"; then
|
|
154
|
+
# A later ownerless moment belongs to another holder's start or
|
|
155
|
+
# release, and gets its own few seconds.
|
|
156
|
+
ownerless=""
|
|
157
|
+
# The bound is per check: one that took the lock while this
|
|
158
|
+
# command waited starts its own count.
|
|
159
|
+
if sync_lock_read_owner "$lock"; then
|
|
160
|
+
seen="$IS_LOCK_PID $IS_LOCK_SINCE $IS_LOCK_TOKEN"
|
|
161
|
+
if [ "$seen" != "$holder" ]; then
|
|
162
|
+
holder="$seen"
|
|
163
|
+
started="$SECONDS"
|
|
164
|
+
fi
|
|
165
|
+
fi
|
|
166
|
+
[ $((SECONDS - started)) -lt "$SYNC_LOCK_CHECK_WAIT" ] || break
|
|
167
|
+
if [ "$told" = 0 ]; then
|
|
168
|
+
# A caller that buffers this command's output (sync --compact)
|
|
169
|
+
# names a descriptor for the terminal, so a wait of minutes
|
|
170
|
+
# is never silent.
|
|
171
|
+
# Not one `>&"${fd:-2}"`: the group's 2>/dev/null, which keeps
|
|
172
|
+
# a closed descriptor quiet, would swallow the default too.
|
|
173
|
+
if [ -n "${IS_LOCK_NOTICE_FD:-}" ]; then
|
|
174
|
+
{ echo "waiting for intelligence sync --check to release $lock" >&"$IS_LOCK_NOTICE_FD"; } 2>/dev/null \
|
|
175
|
+
|| echo "waiting for intelligence sync --check to release $lock" >&2
|
|
176
|
+
else
|
|
177
|
+
echo "waiting for intelligence sync --check to release $lock" >&2
|
|
178
|
+
fi
|
|
179
|
+
told=1
|
|
180
|
+
fi
|
|
181
|
+
elif ! sync_lock_read_owner "$lock"; then
|
|
182
|
+
# No owner: a holder between its mkdir and its owner file, or one
|
|
183
|
+
# being released. Both last moments, so look again for a few
|
|
184
|
+
# seconds from the first sight before calling it a run that is
|
|
185
|
+
# still starting.
|
|
186
|
+
[ -n "$ownerless" ] || ownerless="$SECONDS"
|
|
187
|
+
[ $((SECONDS - ownerless)) -lt 3 ] || break
|
|
188
|
+
else
|
|
189
|
+
break
|
|
190
|
+
fi
|
|
191
|
+
sleep 1
|
|
192
|
+
done
|
|
116
193
|
if sync_lock_read_owner "$lock"; then
|
|
117
194
|
sync_lock_now
|
|
118
195
|
case "$IS_LOCK_SINCE" in ''|*[!0-9]*) ;; *) age=", for $((IS_NOW - IS_LOCK_SINCE))s" ;; esac
|
|
@@ -147,6 +224,11 @@ project_lock_hold() {
|
|
|
147
224
|
sync_lock_release() {
|
|
148
225
|
[ -n "$SYNC_LOCK_HELD" ] || return 0
|
|
149
226
|
if sync_lock_read_owner "$SYNC_LOCK_HELD" && [ "$IS_LOCK_TOKEN" = "$SYNC_LOCK_TOKEN" ]; then
|
|
227
|
+
# A check's owner goes first: `rm -rf` alone could expose a moment with
|
|
228
|
+
# the owner still there and the kind gone, and a command waiting for
|
|
229
|
+
# the check would be refused in it. Without the owner, a waiter sees a
|
|
230
|
+
# check or a lock still starting, and waits either way.
|
|
231
|
+
[ ! -f "$SYNC_LOCK_HELD/kind" ] || rm -f "$SYNC_LOCK_HELD/owner"
|
|
150
232
|
rm -rf "$SYNC_LOCK_HELD"
|
|
151
233
|
fi
|
|
152
234
|
SYNC_LOCK_HELD=""
|
package/engine/ENGINE_SHA
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
|
|
1
|
+
ee05b0f09bc74b1914e04c067cce1e87a75bd18a
|
package/engine/VERSION
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
0.
|
|
1
|
+
0.22.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
|
-
#
|
|
34
|
-
#
|
|
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
|
-
|
|
70
|
-
|
|
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
|
-
|
|
80
|
-
|
|
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
|
-
|
|
90
|
-
|
|
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
|
|
@@ -60,15 +60,19 @@ agents_md_append_agents_table() {
|
|
|
60
60
|
local rows=""
|
|
61
61
|
local count=0
|
|
62
62
|
|
|
63
|
-
local -a files=()
|
|
64
|
-
|
|
65
|
-
[ "${#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
|
|
66
69
|
|
|
67
70
|
if [ "${#files[@]}" -gt 0 ]; then
|
|
68
|
-
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.
|
|
69
73
|
while IFS=$'\x1f' read -r path tier access desc; do
|
|
70
|
-
|
|
71
|
-
|
|
74
|
+
name="${names[i]}"
|
|
75
|
+
i=$((i + 1))
|
|
72
76
|
repo_rel_link_var "$repo_root" "$path"
|
|
73
77
|
if [ -n "$IS_REPO_REL" ]; then
|
|
74
78
|
rows+="| [$name]($IS_REPO_REL) | ${tier:--} | ${access:--} | ${desc:--} |"$'\n'
|
|
@@ -101,21 +105,24 @@ agents_md_append_skills_table() {
|
|
|
101
105
|
local rows=""
|
|
102
106
|
local count=0
|
|
103
107
|
|
|
104
|
-
local
|
|
105
|
-
local -a skill_files=()
|
|
106
|
-
|
|
107
|
-
[ "${#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
|
|
108
115
|
|
|
109
116
|
if [ "${#skill_files[@]}" -gt 0 ]; then
|
|
110
|
-
local path desc
|
|
117
|
+
local path desc i=0
|
|
111
118
|
while IFS=$'\x1f' read -r path desc; do
|
|
112
|
-
|
|
113
|
-
|
|
119
|
+
name="${names[i]}"
|
|
120
|
+
i=$((i + 1))
|
|
114
121
|
repo_rel_link_var "$repo_root" "$path"
|
|
115
122
|
if [ -n "$IS_REPO_REL" ]; then
|
|
116
|
-
rows+="| [$
|
|
123
|
+
rows+="| [$name]($IS_REPO_REL) | ${desc:--} |"$'\n'
|
|
117
124
|
else
|
|
118
|
-
rows+="| $
|
|
125
|
+
rows+="| $name | ${desc:--} |"$'\n'
|
|
119
126
|
fi
|
|
120
127
|
count=$((count + 1))
|
|
121
128
|
done < <(frontmatter_index "description" "${skill_files[@]}")
|
|
@@ -144,30 +151,25 @@ agents_md_append_rules_list() {
|
|
|
144
151
|
local count=0
|
|
145
152
|
local global_rule_files=()
|
|
146
153
|
|
|
147
|
-
local
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
lines+="- $name ($scope)"$'\n'
|
|
167
|
-
fi
|
|
168
|
-
count=$((count + 1))
|
|
169
|
-
done < <(frontmatter_index "paths#" "${files[@]}")
|
|
170
|
-
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
|
|
171
173
|
|
|
172
174
|
[ "$count" -eq 0 ] && return 0
|
|
173
175
|
|
|
@@ -196,17 +198,7 @@ agents_md_append_rules_list() {
|
|
|
196
198
|
echo "<!-- Inlined from always-on rules in $content_rel/rules/ -->"
|
|
197
199
|
echo ""
|
|
198
200
|
} >> "$output"
|
|
199
|
-
|
|
200
|
-
# each — the same output the per-file passes produced.
|
|
201
|
-
awk '
|
|
202
|
-
FNR == 1 { if (NR != 1) print ""; in_fm = 0; past_fm = 0 }
|
|
203
|
-
{ sub(/\r$/, "") }
|
|
204
|
-
/^---$/ {
|
|
205
|
-
if (!past_fm) { in_fm = !in_fm; if (!in_fm) { past_fm = 1 }; next }
|
|
206
|
-
}
|
|
207
|
-
past_fm || !in_fm { print }
|
|
208
|
-
END { print "" }
|
|
209
|
-
' "${global_rule_files[@]}" >> "$output"
|
|
201
|
+
emit_rule_bodies "${global_rule_files[@]}" >> "$output"
|
|
210
202
|
echo " rules: ${#global_rule_files[@]} global rule(s) inlined"
|
|
211
203
|
fi
|
|
212
204
|
}
|