@ainova-systems/intelligence 0.11.0-rc.5 → 0.11.0-rc.7
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/README.md +2 -2
- package/cli/commands/add.sh +26 -20
- package/cli/commands/init.sh +24 -9
- package/cli/commands/install.sh +55 -4
- package/cli/commands/migrate.sh +62 -9
- package/cli/commands/registry.sh +2 -9
- package/cli/commands/search.sh +4 -6
- package/cli/commands/sync.sh +1 -1
- package/cli/commands/update.sh +29 -5
- package/cli/engine-package.yaml +14 -0
- package/cli/intelligence +27 -15
- package/cli/lib/cli-common.sh +52 -17
- package/cli/lib/lockfile.sh +14 -8
- package/cli/lib/manifest.sh +59 -1
- package/cli/lib/registry.sh +41 -55
- package/cli/lib/semver.sh +4 -2
- package/engine/ENGINE_SHA +1 -0
- package/engine/{scripts/adapters → adapters}/agents.sh +10 -15
- package/engine/{scripts/lib → lib}/common.sh +15 -519
- package/engine/lib/contract.sh +120 -0
- package/engine/sync.sh +233 -0
- package/package.json +5 -5
- package/engine/INIT.md +0 -500
- package/engine/docs/CLI.md +0 -94
- package/engine/scripts/ENGINE_SHA +0 -1
- package/engine/scripts/lib/layout.sh +0 -51
- package/engine/scripts/lib/migrations.sh +0 -708
- package/engine/scripts/sync.sh +0 -311
- package/engine/scripts/update.sh +0 -237
- package/registry/index.yaml +0 -26
- /package/engine/{scripts/VERSION → VERSION} +0 -0
- /package/engine/{scripts/adapters → adapters}/_template.sh +0 -0
- /package/engine/{scripts/adapters → adapters}/claude.sh +0 -0
- /package/engine/{scripts/adapters → adapters}/codex.sh +0 -0
- /package/engine/{scripts/adapters → adapters}/copilot.sh +0 -0
- /package/engine/{scripts/adapters → adapters}/cursor.sh +0 -0
- /package/engine/{scripts/adapters → adapters}/opencode.sh +0 -0
- /package/engine/{scripts/adapters → adapters}/pi.sh +0 -0
- /package/{engine → packages/sync}/agents/intelligence-architect.md +0 -0
- /package/{engine → packages/sync}/agents/intelligence-operator.md +0 -0
- /package/{engine → packages/sync}/docs/ADAPTERS.md +0 -0
- /package/{engine → packages/sync}/docs/CONVENTIONS.md +0 -0
- /package/{engine → packages/sync}/rules/intelligence-authoring.md +0 -0
- /package/{engine → packages/sync}/skills/intelligence-add-agent/SKILL.md +0 -0
- /package/{engine → packages/sync}/skills/intelligence-add-rule/SKILL.md +0 -0
- /package/{engine → packages/sync}/skills/intelligence-add-skill/SKILL.md +0 -0
- /package/{engine → packages/sync}/skills/intelligence-extract-skill/SKILL.md +0 -0
- /package/{engine → packages/sync}/skills/intelligence-install-adapter/SKILL.md +0 -0
- /package/{engine → packages/sync}/skills/intelligence-learn-from-context/SKILL.md +0 -0
- /package/{engine → packages/sync}/skills/intelligence-review-skills/SKILL.md +0 -0
- /package/{engine → packages/sync}/skills/intelligence-sync/SKILL.md +0 -0
- /package/{engine → packages/sync}/skills/intelligence-uninstall-adapter/SKILL.md +0 -0
- /package/{engine → packages/sync}/skills/intelligence-update/SKILL.md +0 -0
|
@@ -97,59 +97,17 @@ yaml_dq_escape() {
|
|
|
97
97
|
|
|
98
98
|
# --- Source Resolution -------------------------------------------------------
|
|
99
99
|
#
|
|
100
|
-
#
|
|
101
|
-
#
|
|
102
|
-
#
|
|
103
|
-
#
|
|
104
|
-
# routes its `$repo_root/$src` through resolve_source_dir, so no other file
|
|
105
|
-
# needs to know about remote sources.
|
|
106
|
-
#
|
|
107
|
-
# Spec format (inline string, so read_yaml_list parses it unchanged):
|
|
108
|
-
# git+<url>[@<ref>][#<subpath>]
|
|
109
|
-
# <url> explicit-scheme URL (https/http/ssh/git/file). Other transports
|
|
110
|
-
# (notably the command-executing ext::/fd::) are rejected.
|
|
111
|
-
# @<ref> optional tag / branch / SHA — the segment after the last `@` in
|
|
112
|
-
# the post-scheme part, accepted only if it has no `/` (so
|
|
113
|
-
# userinfo like `ssh://git@host/...` is not mistaken for a ref;
|
|
114
|
-
# branch names containing `/` are unsupported — use a tag, SHA,
|
|
115
|
-
# or slashless branch, which is the recommended pin anyway).
|
|
116
|
-
# #<subpath> optional dir inside the clone holding rules/agents/skills.
|
|
117
|
-
|
|
118
|
-
# True (0) if a source token is a remote git spec.
|
|
119
|
-
source_is_remote() {
|
|
120
|
-
case "$1" in
|
|
121
|
-
git+*) return 0 ;;
|
|
122
|
-
*) return 1 ;;
|
|
123
|
-
esac
|
|
124
|
-
}
|
|
125
|
-
|
|
126
|
-
# True (0) if a source token references a pack declared under `packs:`
|
|
127
|
-
# (`@<name>` or `@<name>/<subpath>`).
|
|
128
|
-
source_is_pack() {
|
|
129
|
-
case "$1" in
|
|
130
|
-
@?*) return 0 ;;
|
|
131
|
-
*) return 1 ;;
|
|
132
|
-
esac
|
|
133
|
-
}
|
|
134
|
-
|
|
135
|
-
# True (0) if a source token is a plain repo-relative path — i.e. NOT a pack
|
|
136
|
-
# reference and NOT an inline remote spec. The inverse of "resolves through a
|
|
137
|
-
# clone", which is what every caller that pattern-matches a token against a
|
|
138
|
-
# real directory needs.
|
|
139
|
-
source_is_local_path() {
|
|
140
|
-
source_is_pack "$1" && return 1
|
|
141
|
-
source_is_remote "$1" && return 1
|
|
142
|
-
return 0
|
|
143
|
-
}
|
|
100
|
+
# Every `sources.*` entry is a repo-relative path resolved as
|
|
101
|
+
# `$repo_root/<entry>`. Fetching, version resolution and pinning belong to the
|
|
102
|
+
# CLI: by the time the engine runs, an installed package is an ordinary
|
|
103
|
+
# directory under the store, indistinguishable from project content.
|
|
144
104
|
|
|
145
105
|
# Map an absolute file path to a repo-root-relative path for use as a link
|
|
146
106
|
# target inside a COMMITTED file (e.g. AGENTS.md). A file under $repo_root gets
|
|
147
|
-
# its repo-relative path
|
|
148
|
-
#
|
|
149
|
-
#
|
|
150
|
-
#
|
|
151
|
-
# no-op when $path is not under $repo_root, which is how the out-of-repo case is
|
|
152
|
-
# detected.
|
|
107
|
+
# its repo-relative path; one resolved outside it has no stable, committable
|
|
108
|
+
# path, so this returns the empty string — callers MUST then emit the bare
|
|
109
|
+
# name, never the absolute path. The `${path#"$repo_root"/}` strip is a no-op
|
|
110
|
+
# when $path is not under $repo_root, which is how that case is detected.
|
|
153
111
|
repo_rel_link() {
|
|
154
112
|
local repo_root="$1" path="$2" rel
|
|
155
113
|
rel="${path#"$repo_root"/}"
|
|
@@ -191,291 +149,15 @@ repo_rel_dir() {
|
|
|
191
149
|
done
|
|
192
150
|
}
|
|
193
151
|
|
|
194
|
-
# Resolve a single source token to an absolute local directory.
|
|
195
|
-
#
|
|
196
|
-
#
|
|
152
|
+
# Resolve a single source token to an absolute local directory. Every token is
|
|
153
|
+
# a repo-relative path: the CLI resolves, fetches and pins packages, so by the
|
|
154
|
+
# time the engine runs a package is just a directory under the store.
|
|
197
155
|
# ALWAYS returns 0 (echoes nothing on failure) so `set -e` callers using
|
|
198
156
|
# `dir="$(resolve_source_dir ...)"` never abort; the caller's existing
|
|
199
157
|
# `[ -d "$dir" ] || continue` guard then skips an unresolved source.
|
|
200
158
|
# Usage: dir="$(resolve_source_dir "$repo_root" "$src")"
|
|
201
159
|
resolve_source_dir() {
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
# A declared pack (`@<name>[/<subpath>]`) keeps its url / ref / mirror in
|
|
205
|
-
# config.yaml, so the token itself carries nothing but the reference.
|
|
206
|
-
if source_is_pack "$token"; then
|
|
207
|
-
resolve_pack_source "$repo_root" "$config_file" "$token"
|
|
208
|
-
return 0
|
|
209
|
-
fi
|
|
210
|
-
|
|
211
|
-
if ! source_is_remote "$token"; then
|
|
212
|
-
printf '%s' "$repo_root/$token"
|
|
213
|
-
return 0
|
|
214
|
-
fi
|
|
215
|
-
|
|
216
|
-
# --- parse: git+<url>[@<ref>][#<subpath>] ---
|
|
217
|
-
# An inline spec is an ANONYMOUS pack: it has no declared name and no
|
|
218
|
-
# mirror, so it is always transient. Declare it under `packs:` to commit it.
|
|
219
|
-
local rest="${token#git+}"
|
|
220
|
-
local subpath="" urlref="$rest"
|
|
221
|
-
case "$rest" in
|
|
222
|
-
*\#*) subpath="${rest#*#}"; urlref="${rest%%#*}" ;;
|
|
223
|
-
esac
|
|
224
|
-
|
|
225
|
-
# ref = segment after the last `@` in the post-scheme part, only if it has
|
|
226
|
-
# no `/` (else it is userinfo such as `git@host`, not a ref).
|
|
227
|
-
local url="$urlref" ref="" after_scheme="${urlref#*://}"
|
|
228
|
-
case "$after_scheme" in
|
|
229
|
-
*@*)
|
|
230
|
-
local cand="${after_scheme##*@}"
|
|
231
|
-
case "$cand" in
|
|
232
|
-
*/*|"") ;; # userinfo / empty -> no ref
|
|
233
|
-
*) ref="$cand"; url="${urlref%@$ref}" ;;
|
|
234
|
-
esac
|
|
235
|
-
;;
|
|
236
|
-
esac
|
|
237
|
-
|
|
238
|
-
fetch_remote_source "$token" "$url" "$ref" "$subpath" ""
|
|
239
|
-
}
|
|
240
|
-
|
|
241
|
-
# Resolve `@<name>[/<subpath>]` against the `packs:` block and fetch it.
|
|
242
|
-
# An undeclared name is a HARD error: unlike a mistyped local path (which the
|
|
243
|
-
# caller's `[ -d ]` guard silently skips), a pack reference names something the
|
|
244
|
-
# config claims to know, so a typo must not quietly drop a whole rule set.
|
|
245
|
-
# Usage: resolve_pack_source "$repo_root" "$config_file" "@shared/rules"
|
|
246
|
-
resolve_pack_source() {
|
|
247
|
-
local repo_root="$1" config_file="$2" token="$3"
|
|
248
|
-
|
|
249
|
-
local rest="${token#@}" name subpath=""
|
|
250
|
-
case "$rest" in
|
|
251
|
-
*/*) name="${rest%%/*}"; subpath="${rest#*/}" ;;
|
|
252
|
-
*) name="$rest" ;;
|
|
253
|
-
esac
|
|
254
|
-
|
|
255
|
-
local url ref mirror_rel mirror_abs=""
|
|
256
|
-
url="$(get_pack_field "$config_file" "$name" "url")"
|
|
257
|
-
# Defensive only: validate_pack_refs has already failed the run for an
|
|
258
|
-
# undeclared pack. It has to, because every caller invokes this inside `$( )`
|
|
259
|
-
# — an exit here would end the substitution subshell, not the sync.
|
|
260
|
-
if [ -z "$url" ]; then
|
|
261
|
-
echo " WARN: pack '$name' is not declared under 'packs:': $token" >&2
|
|
262
|
-
return 0
|
|
263
|
-
fi
|
|
264
|
-
ref="$(get_pack_field "$config_file" "$name" "ref")"
|
|
265
|
-
mirror_rel="$(get_pack_field "$config_file" "$name" "mirror")"
|
|
266
|
-
[ -n "$mirror_rel" ] && mirror_abs="$(resolve_mirror_dir "$repo_root" "$config_file" "$name" "$mirror_rel")"
|
|
267
|
-
|
|
268
|
-
fetch_remote_source "$token" "$url" "$ref" "$subpath" "$mirror_abs" "$mirror_rel"
|
|
269
|
-
}
|
|
270
|
-
|
|
271
|
-
# Shallow-clone <url>@<ref> into the run cache, echo "<clone>/<subpath>", and —
|
|
272
|
-
# when <mirror_abs> is set — additionally materialize it there so the content is
|
|
273
|
-
# committed. <token> is only used for messages.
|
|
274
|
-
# ALWAYS returns 0 (echoes nothing on failure), per resolve_source_dir's contract.
|
|
275
|
-
# Usage: fetch_remote_source <token> <url> <ref> <subpath> <mirror_abs> [<mirror_rel>]
|
|
276
|
-
fetch_remote_source() {
|
|
277
|
-
local token="$1" url="$2" ref="$3" subpath="$4" mirror="$5" mirror_rel="${6:-}"
|
|
278
|
-
|
|
279
|
-
# Reject path traversal in the subpath: a remote spec must not be able to
|
|
280
|
-
# escape the clone dir (e.g. `#../../etc`). Checked before any clone.
|
|
281
|
-
case "/$subpath/" in
|
|
282
|
-
*/../*)
|
|
283
|
-
echo " WARN: remote source rejected (subpath traversal '..'): $token" >&2
|
|
284
|
-
return 0
|
|
285
|
-
;;
|
|
286
|
-
esac
|
|
287
|
-
|
|
288
|
-
# Scheme whitelist — reject everything but plain fetch transports. The
|
|
289
|
-
# ext::/fd:: transports execute arbitrary commands on clone, so a malicious
|
|
290
|
-
# or mistyped config must never reach `git clone` with them.
|
|
291
|
-
case "$url" in
|
|
292
|
-
https://*|http://*|ssh://*|git://*|file://*) ;;
|
|
293
|
-
*)
|
|
294
|
-
echo " WARN: remote source rejected (unsupported scheme): $token" >&2
|
|
295
|
-
return 0
|
|
296
|
-
;;
|
|
297
|
-
esac
|
|
298
|
-
|
|
299
|
-
if ! command -v git >/dev/null 2>&1; then
|
|
300
|
-
echo " WARN: remote source needs git, which is not installed: $token" >&2
|
|
301
|
-
return 0
|
|
302
|
-
fi
|
|
303
|
-
|
|
304
|
-
# Cache root: run-scoped (set + cleaned by sync.sh) or a stable fallback so
|
|
305
|
-
# direct adapter calls still avoid re-cloning the same spec within a run.
|
|
306
|
-
local cache_root="${IS_REMOTE_CACHE:-${TMPDIR:-/tmp}/intelligence-sync-remotes}"
|
|
307
|
-
mkdir -p "$cache_root" 2>/dev/null || true
|
|
308
|
-
# Key on repo URL + ref ONLY (not the subpath): sources that point at the
|
|
309
|
-
# same repo@ref but different subpaths (e.g. `...repo.git@main#rules` and
|
|
310
|
-
# `...repo.git@main#skills`) share a SINGLE clone; the subpath only selects
|
|
311
|
-
# a directory inside it. Different ref → different clone (distinct versions).
|
|
312
|
-
local key
|
|
313
|
-
key="$(printf '%s' "$url@$ref" | cksum | awk '{print $1 "-" $2}')"
|
|
314
|
-
local dest="$cache_root/$key"
|
|
315
|
-
|
|
316
|
-
if [ ! -d "$dest/.git" ]; then
|
|
317
|
-
rm -rf "$dest"
|
|
318
|
-
# Untrusted remote content: never materialize symlinks from the cloned
|
|
319
|
-
# repo. With core.symlinks=false git writes each symlink as a plain text
|
|
320
|
-
# file holding its target path, so a hostile link like `skills -> /etc`
|
|
321
|
-
# cannot make the copy pipeline read host files outside the clone.
|
|
322
|
-
#
|
|
323
|
-
# Line endings are pinned for the same reason the engine's own
|
|
324
|
-
# `.gitattributes` pins them: the checkout must not depend on the host's
|
|
325
|
-
# `core.autocrlf`. On Windows that default rewrites a pack declaring no
|
|
326
|
-
# attributes to CRLF, `materialize_pack` copies bytes verbatim, and the
|
|
327
|
-
# mirror lands CRLF inside the project repo — a phantom diff that returns
|
|
328
|
-
# on every sync. `autocrlf=false` covers packs with no attributes,
|
|
329
|
-
# `eol=lf` covers those that mark files `text`.
|
|
330
|
-
local git_cfg=(-c core.symlinks=false -c core.autocrlf=false -c core.eol=lf)
|
|
331
|
-
local ok=0
|
|
332
|
-
if [ -n "$ref" ]; then
|
|
333
|
-
if GIT_TERMINAL_PROMPT=0 git "${git_cfg[@]}" clone --depth 1 --branch "$ref" --quiet \
|
|
334
|
-
"$url" "$dest" 2>/dev/null; then
|
|
335
|
-
ok=1
|
|
336
|
-
else
|
|
337
|
-
# ref is likely a SHA (not a branch/tag) — full clone + checkout.
|
|
338
|
-
rm -rf "$dest"
|
|
339
|
-
if GIT_TERMINAL_PROMPT=0 git "${git_cfg[@]}" clone --quiet "$url" "$dest" 2>/dev/null \
|
|
340
|
-
&& git -C "$dest" "${git_cfg[@]}" checkout --quiet "$ref" 2>/dev/null; then
|
|
341
|
-
ok=1
|
|
342
|
-
fi
|
|
343
|
-
fi
|
|
344
|
-
elif GIT_TERMINAL_PROMPT=0 git "${git_cfg[@]}" clone --depth 1 --quiet "$url" "$dest" 2>/dev/null; then
|
|
345
|
-
ok=1
|
|
346
|
-
fi
|
|
347
|
-
if [ "$ok" -ne 1 ]; then
|
|
348
|
-
rm -rf "$dest"
|
|
349
|
-
echo " WARN: remote source clone failed (url=$url ref=${ref:-<default>}): $token" >&2
|
|
350
|
-
return 0
|
|
351
|
-
fi
|
|
352
|
-
echo " remote: cloned $url${ref:+ @$ref}" >&2
|
|
353
|
-
fi
|
|
354
|
-
|
|
355
|
-
local out="$dest"
|
|
356
|
-
[ -n "$subpath" ] && out="$dest/$subpath"
|
|
357
|
-
if [ ! -d "$out" ]; then
|
|
358
|
-
echo " WARN: remote source subpath not found ('${subpath:-/}') in $url: $token" >&2
|
|
359
|
-
return 0
|
|
360
|
-
fi
|
|
361
|
-
# Containment (defense in depth on top of the `..` reject + symlink-free
|
|
362
|
-
# checkout): the resolved dir must stay inside the clone. Canonicalize both
|
|
363
|
-
# with `pwd -P` so a symlinked TMPDIR (e.g. macOS /tmp -> /private/tmp)
|
|
364
|
-
# resolves consistently on each side.
|
|
365
|
-
local real_dest real_out
|
|
366
|
-
real_dest="$(cd "$dest" 2>/dev/null && pwd -P)"
|
|
367
|
-
real_out="$(cd "$out" 2>/dev/null && pwd -P)"
|
|
368
|
-
case "${real_out:-/nonexistent}" in
|
|
369
|
-
"$real_dest"|"$real_dest"/*) ;;
|
|
370
|
-
*)
|
|
371
|
-
echo " WARN: remote source subpath escapes the clone ('${subpath:-/}'): $token" >&2
|
|
372
|
-
return 0
|
|
373
|
-
;;
|
|
374
|
-
esac
|
|
375
|
-
|
|
376
|
-
# No `mirror:` -> the clone stays in the transient run cache and nothing
|
|
377
|
-
# lands in the repo. This is every inline `git+` spec, and any pack that
|
|
378
|
-
# declares no mirror.
|
|
379
|
-
if [ -z "$mirror" ]; then
|
|
380
|
-
printf '%s' "$out"
|
|
381
|
-
return 0
|
|
382
|
-
fi
|
|
383
|
-
materialize_pack "$dest" "$out" "$url" "$ref" "$subpath" "$mirror" "$mirror_rel"
|
|
384
|
-
return 0
|
|
385
|
-
}
|
|
386
|
-
|
|
387
|
-
# Copy a resolved remote source out of the transient clone into the pack's
|
|
388
|
-
# declared `mirror:` directory, so pack content is committed and an upstream
|
|
389
|
-
# bump shows up in `git diff` instead of only in the generated output. Echoes
|
|
390
|
-
# the materialized directory; on any failure echoes the clone dir instead, so a
|
|
391
|
-
# broken mirror degrades to the transient behaviour rather than losing the
|
|
392
|
-
# source.
|
|
393
|
-
#
|
|
394
|
-
# The directory is DECLARED, never derived — `mirror:` says exactly where the
|
|
395
|
-
# pack lives, so there is no name to sanitize and no collision to resolve.
|
|
396
|
-
#
|
|
397
|
-
# The FIRST token to touch a pack in a run wipes it (clearing content left by a
|
|
398
|
-
# previous ref, or by a source entry that has since been removed); later tokens
|
|
399
|
-
# for the same pack only replace their own subpath. The claim is recorded in the
|
|
400
|
-
# clone cache, which is what makes "wipe once per run" work across the separate
|
|
401
|
-
# subshells each resolve_source_dir call runs in.
|
|
402
|
-
#
|
|
403
|
-
# The wipe is guarded by the stamp: a NON-EMPTY directory with no `.pack` in it
|
|
404
|
-
# is never deleted — it belongs to the project, not to us.
|
|
405
|
-
# <mirror_rel> is the path as authored in config.yaml, used only in messages.
|
|
406
|
-
# Usage: materialize_pack <clone> <src_dir> <url> <ref> <subpath> <mirror> <mirror_rel>
|
|
407
|
-
materialize_pack() {
|
|
408
|
-
local clone="$1" src_dir="$2" url="$3" ref="$4" subpath="$5" pack_dir="$6" mirror_rel="${7:-}"
|
|
409
|
-
|
|
410
|
-
# The claim is keyed on the DIRECTORY, not on url@ref: it records "this run
|
|
411
|
-
# already cleared this path". Keying it on the clone would let two packs
|
|
412
|
-
# that share a url@ref but declare different mirrors claim each other's,
|
|
413
|
-
# leaving the second mirror unstamped and never pruned.
|
|
414
|
-
#
|
|
415
|
-
# Same cache-root fallback as the clone: the claim must exist even when the
|
|
416
|
-
# caller is not sync.sh, or every token would re-wipe the pack and only the
|
|
417
|
-
# last subpath would survive. That fallback root is NOT run-scoped, though,
|
|
418
|
-
# so the claim carries `$$` — stable across the command-substitution
|
|
419
|
-
# subshells of one run, different for the next. A claim left behind by an
|
|
420
|
-
# earlier run must never suppress this run's wipe: that would rebuild the
|
|
421
|
-
# mirror with no `.pack` in it and freeze it against the guard below.
|
|
422
|
-
local cache_root claim
|
|
423
|
-
cache_root="${IS_REMOTE_CACHE:-${TMPDIR:-/tmp}/intelligence-sync-remotes}"
|
|
424
|
-
mkdir -p "$cache_root" 2>/dev/null || true
|
|
425
|
-
claim="$cache_root/$$-$(printf '%s' "$pack_dir" | cksum | awk '{print $1 "-" $2}').packdir"
|
|
426
|
-
|
|
427
|
-
if [ ! -f "$claim" ]; then
|
|
428
|
-
# Refuse to wipe a directory that is not ours. Ownership is the PRESENCE
|
|
429
|
-
# of the stamp, not the url inside it: a mirror is declared per pack, so
|
|
430
|
-
# a stamped directory is this pack's even after its `url:` is edited —
|
|
431
|
-
# a moved or renamed upstream must refresh the mirror, not freeze it at
|
|
432
|
-
# the old content while the generated output silently follows the new.
|
|
433
|
-
if [ -d "$pack_dir" ] && [ -n "$(find "$pack_dir" -mindepth 1 -maxdepth 1 2>/dev/null)" ] \
|
|
434
|
-
&& [ ! -f "$pack_dir/.pack" ]; then
|
|
435
|
-
echo " WARN: mirror '$pack_dir' holds content that is not a pack's (no .pack stamp) — skipping materialization" >&2
|
|
436
|
-
printf '%s' "$src_dir"
|
|
437
|
-
return 0
|
|
438
|
-
fi
|
|
439
|
-
|
|
440
|
-
rm -rf "$pack_dir"
|
|
441
|
-
if ! mkdir -p "$pack_dir"; then
|
|
442
|
-
echo " WARN: cannot create mirror dir '$pack_dir' — using the run cache" >&2
|
|
443
|
-
printf '%s' "$src_dir"
|
|
444
|
-
return 0
|
|
445
|
-
fi
|
|
446
|
-
{
|
|
447
|
-
printf 'url=%s\n' "$url"
|
|
448
|
-
printf 'ref=%s\n' "${ref:-<default>}"
|
|
449
|
-
printf 'sha=%s\n' "$(git -C "$clone" rev-parse HEAD 2>/dev/null || echo unknown)"
|
|
450
|
-
} > "$pack_dir/.pack"
|
|
451
|
-
printf '%s\n' "$pack_dir" > "$claim"
|
|
452
|
-
echo " pack: $url${ref:+ @$ref} -> ${mirror_rel:-$pack_dir}" >&2
|
|
453
|
-
fi
|
|
454
|
-
|
|
455
|
-
# Only a subpath is cleared here — clearing the pack root would delete the
|
|
456
|
-
# `.pack` stamp written above (and any sibling subpath already copied in
|
|
457
|
-
# this run). The root is already clean: the claim step wiped it.
|
|
458
|
-
local dest="$pack_dir"
|
|
459
|
-
if [ -n "$subpath" ]; then
|
|
460
|
-
dest="$pack_dir/$subpath"
|
|
461
|
-
rm -rf "$dest"
|
|
462
|
-
fi
|
|
463
|
-
mkdir -p "$dest"
|
|
464
|
-
|
|
465
|
-
# Copy the subpath's contents, skipping `.git` — it only exists when the
|
|
466
|
-
# source IS the clone root (no `#subpath`), and a nested `.git` inside the
|
|
467
|
-
# project repo would be recorded as a gitlink, which is exactly the
|
|
468
|
-
# untrackable state this whole feature exists to avoid.
|
|
469
|
-
local entry base
|
|
470
|
-
for entry in "$src_dir"/* "$src_dir"/.[!.]*; do
|
|
471
|
-
[ -e "$entry" ] || continue
|
|
472
|
-
base="${entry##*/}"
|
|
473
|
-
[ "$base" = ".git" ] && continue
|
|
474
|
-
cp -R "$entry" "$dest/"
|
|
475
|
-
done
|
|
476
|
-
|
|
477
|
-
printf '%s' "$dest"
|
|
478
|
-
return 0
|
|
160
|
+
printf '%s' "$1/$2"
|
|
479
161
|
}
|
|
480
162
|
|
|
481
163
|
# Copy a markdown file with frontmatter, ensuring free-text string fields are
|
|
@@ -1070,11 +752,6 @@ validate_output_path() {
|
|
|
1070
752
|
for section in rules agents skills; do
|
|
1071
753
|
while IFS= read -r src; do
|
|
1072
754
|
[ -z "$src" ] && continue
|
|
1073
|
-
# Remote sources never resolve to a local output path — skip them
|
|
1074
|
-
# so a `git+...` spec or an `@pack` reference is not pattern-matched
|
|
1075
|
-
# against the output dir. A mirrored pack is covered separately,
|
|
1076
|
-
# below, by its declared `mirror:`.
|
|
1077
|
-
source_is_local_path "$src" || continue
|
|
1078
755
|
src_rel="$(normalize_path "$repo_root/$src")"
|
|
1079
756
|
src_rel="${src_rel#"$repo_root"/}"
|
|
1080
757
|
case "$rel" in
|
|
@@ -1086,167 +763,6 @@ validate_output_path() {
|
|
|
1086
763
|
esac
|
|
1087
764
|
done < <(read_yaml_list "$config_file" "$section")
|
|
1088
765
|
done
|
|
1089
|
-
|
|
1090
|
-
# Reject any pack mirror — materialized pack content is source, and it is
|
|
1091
|
-
# committed, so an adapter cleanup aimed at it would delete work that is not
|
|
1092
|
-
# regenerated until the next successful clone.
|
|
1093
|
-
local mirror_rel
|
|
1094
|
-
while IFS= read -r mirror_rel; do
|
|
1095
|
-
[ -z "$mirror_rel" ] && continue
|
|
1096
|
-
case "$rel" in
|
|
1097
|
-
"$mirror_rel"|"$mirror_rel"/*)
|
|
1098
|
-
echo "ERROR: targets.$adapter.output ('$rel') points into a pack mirror ('$mirror_rel')." >&2
|
|
1099
|
-
echo " The adapter would delete materialized pack content." >&2
|
|
1100
|
-
exit 1
|
|
1101
|
-
;;
|
|
1102
|
-
esac
|
|
1103
|
-
done < <(list_pack_mirrors "$repo_root" "$config_file")
|
|
1104
|
-
}
|
|
1105
|
-
|
|
1106
|
-
# Resolve and validate one pack's `mirror:` into an absolute directory.
|
|
1107
|
-
#
|
|
1108
|
-
# materialize_pack `rm -rf`s this path, so the same class of check that guards
|
|
1109
|
-
# adapter outputs applies — with one deliberate difference: a mirror is ALLOWED
|
|
1110
|
-
# inside the intelligence umbrella, since `<umbrella>/external/<pack>` is the
|
|
1111
|
-
# recommended place for it.
|
|
1112
|
-
#
|
|
1113
|
-
# Echoes the absolute path; exits 1 with a clear message on rejection.
|
|
1114
|
-
# Usage: resolve_mirror_dir "$REPO_ROOT" "$CONFIG_FILE" <pack-name> <mirror-rel>
|
|
1115
|
-
resolve_mirror_dir() {
|
|
1116
|
-
local repo_root="$1" config_file="$2" name="$3" rel="$4"
|
|
1117
|
-
|
|
1118
|
-
local canon
|
|
1119
|
-
canon="$(normalize_path "$repo_root/$rel")"
|
|
1120
|
-
|
|
1121
|
-
case "$canon" in
|
|
1122
|
-
""|"/"|"$repo_root")
|
|
1123
|
-
echo "ERROR: packs.$name.mirror resolves to repo root or empty path: '$rel'" >&2
|
|
1124
|
-
exit 1
|
|
1125
|
-
;;
|
|
1126
|
-
esac
|
|
1127
|
-
case "$canon" in
|
|
1128
|
-
"$repo_root"/*) ;;
|
|
1129
|
-
*)
|
|
1130
|
-
echo "ERROR: packs.$name.mirror escapes the repository: '$rel' (resolves to '$canon')." >&2
|
|
1131
|
-
exit 1
|
|
1132
|
-
;;
|
|
1133
|
-
esac
|
|
1134
|
-
|
|
1135
|
-
# Never inside a configured source tree: a pack directory created there
|
|
1136
|
-
# would be `rm -rf`d alongside authored rules / agents / skills.
|
|
1137
|
-
local canon_rel section src src_rel
|
|
1138
|
-
canon_rel="${canon#"$repo_root"/}"
|
|
1139
|
-
for section in rules agents skills; do
|
|
1140
|
-
while IFS= read -r src; do
|
|
1141
|
-
[ -z "$src" ] && continue
|
|
1142
|
-
source_is_local_path "$src" || continue
|
|
1143
|
-
src_rel="$(normalize_path "$repo_root/$src")"
|
|
1144
|
-
src_rel="${src_rel#"$repo_root"/}"
|
|
1145
|
-
case "$canon_rel" in
|
|
1146
|
-
"$src_rel"|"$src_rel"/*)
|
|
1147
|
-
echo "ERROR: packs.$name.mirror ('$rel') is inside a configured source ('$src')." >&2
|
|
1148
|
-
echo " Materializing a pack there would overwrite authored content." >&2
|
|
1149
|
-
exit 1
|
|
1150
|
-
;;
|
|
1151
|
-
esac
|
|
1152
|
-
done < <(read_yaml_list "$config_file" "$section")
|
|
1153
|
-
done
|
|
1154
|
-
|
|
1155
|
-
printf '%s' "$canon"
|
|
1156
|
-
}
|
|
1157
|
-
|
|
1158
|
-
# Fail the run on a `@<pack>` source that names a pack the config does not
|
|
1159
|
-
# declare, and on a declared `mirror:` that is unsafe to `rm -rf`.
|
|
1160
|
-
#
|
|
1161
|
-
# This runs UP FRONT, before any adapter, because resolve_source_dir is always
|
|
1162
|
-
# called inside `$( )`: an error raised down there would exit the substitution
|
|
1163
|
-
# subshell only, and the caller's `[ -d "$dir" ] || continue` guard would turn a
|
|
1164
|
-
# typo into a silently dropped rule set — the exact failure this feature exists
|
|
1165
|
-
# to remove. A missing local path stays a warning; a bad pack reference does not,
|
|
1166
|
-
# because the config claims to know that name.
|
|
1167
|
-
#
|
|
1168
|
-
# Exits 1 with a clear message on rejection.
|
|
1169
|
-
# Usage: validate_pack_refs "$REPO_ROOT" "$CONFIG_FILE"
|
|
1170
|
-
validate_pack_refs() {
|
|
1171
|
-
local repo_root="$1" config_file="$2"
|
|
1172
|
-
local section src name known bad=0
|
|
1173
|
-
|
|
1174
|
-
for section in rules agents skills; do
|
|
1175
|
-
while IFS= read -r src; do
|
|
1176
|
-
[ -z "$src" ] && continue
|
|
1177
|
-
source_is_pack "$src" || continue
|
|
1178
|
-
name="${src#@}"
|
|
1179
|
-
name="${name%%/*}"
|
|
1180
|
-
if [ -z "$(get_pack_field "$config_file" "$name" "url")" ]; then
|
|
1181
|
-
echo "ERROR: sources.$section entry '$src' references pack '$name', which has no 'packs.$name.url' in $config_file." >&2
|
|
1182
|
-
bad=1
|
|
1183
|
-
fi
|
|
1184
|
-
done < <(read_yaml_list "$config_file" "$section")
|
|
1185
|
-
done
|
|
1186
|
-
|
|
1187
|
-
if [ "$bad" -ne 0 ]; then
|
|
1188
|
-
known="$(read_yaml_keys "$config_file" "packs" | tr '\n' ' ')"
|
|
1189
|
-
echo " Declared packs: ${known:-<none>}" >&2
|
|
1190
|
-
# `targets:` accepts the flow form, so a user reasonably writes
|
|
1191
|
-
# `packs:\n shared: { url: … }` — which reads as zero declared packs and
|
|
1192
|
-
# makes the message above point at a typo that is not there. Scoped to
|
|
1193
|
-
# the `packs:` block: every shipped example writes `targets:` in flow
|
|
1194
|
-
# form, so an unscoped match would print this note on every failure.
|
|
1195
|
-
if awk '
|
|
1196
|
-
{ sub(/\r$/, "") }
|
|
1197
|
-
/^packs:[[:space:]]*$/ { in_p = 1; next }
|
|
1198
|
-
/^[A-Za-z]/ { in_p = 0 }
|
|
1199
|
-
in_p && /^ [A-Za-z0-9_][A-Za-z0-9._-]*:[[:space:]]*\{/ { found = 1; exit }
|
|
1200
|
-
END { exit !found }
|
|
1201
|
-
' "$config_file"; then
|
|
1202
|
-
echo " Note: a pack must be declared in block form — 'name:' on its own line," >&2
|
|
1203
|
-
echo " then indented 'url:' / 'ref:' / 'mirror:'. The '{ … }' form is not read here." >&2
|
|
1204
|
-
fi
|
|
1205
|
-
exit 1
|
|
1206
|
-
fi
|
|
1207
|
-
|
|
1208
|
-
# Validate every declared mirror once, before a single clone runs, so an
|
|
1209
|
-
# unsafe path fails the run rather than being discovered mid-materialization.
|
|
1210
|
-
#
|
|
1211
|
-
# Two packs sharing one mirror is refused here too: the wipe is claimed per
|
|
1212
|
-
# DIRECTORY, so the second pack would skip the clear and copy its subpaths
|
|
1213
|
-
# in beside the first's, leaving one directory holding two packs' content
|
|
1214
|
-
# under a single `.pack`. Nothing downstream can untangle that.
|
|
1215
|
-
local rel canon i
|
|
1216
|
-
local mirror_dirs=() mirror_owners=()
|
|
1217
|
-
while IFS= read -r name; do
|
|
1218
|
-
[ -z "$name" ] && continue
|
|
1219
|
-
rel="$(get_pack_field "$config_file" "$name" "mirror")"
|
|
1220
|
-
[ -n "$rel" ] || continue
|
|
1221
|
-
canon="$(resolve_mirror_dir "$repo_root" "$config_file" "$name" "$rel")"
|
|
1222
|
-
i=0
|
|
1223
|
-
while [ "$i" -lt "${#mirror_dirs[@]}" ]; do
|
|
1224
|
-
if [ "${mirror_dirs[$i]}" = "$canon" ]; then
|
|
1225
|
-
echo "ERROR: packs.$name.mirror ('$rel') is already the mirror of pack '${mirror_owners[$i]}'." >&2
|
|
1226
|
-
echo " Each pack needs its own directory — sharing one leaves a single '.pack' stamp" >&2
|
|
1227
|
-
echo " naming one pack over a directory holding both packs' content." >&2
|
|
1228
|
-
exit 1
|
|
1229
|
-
fi
|
|
1230
|
-
i=$((i + 1))
|
|
1231
|
-
done
|
|
1232
|
-
mirror_dirs+=("$canon"); mirror_owners+=("$name")
|
|
1233
|
-
done < <(read_yaml_keys "$config_file" "packs")
|
|
1234
|
-
}
|
|
1235
|
-
|
|
1236
|
-
# Every declared pack's `mirror:`, one repo-relative path per line (packs with
|
|
1237
|
-
# no mirror contribute nothing). Used by the guards that must not mistake
|
|
1238
|
-
# materialized pack content for either an adapter output or an unsynced source.
|
|
1239
|
-
# Usage: readarray -t mirrors < <(list_pack_mirrors "$REPO_ROOT" "$CONFIG_FILE")
|
|
1240
|
-
list_pack_mirrors() {
|
|
1241
|
-
local repo_root="$1" config_file="$2"
|
|
1242
|
-
local name rel canon
|
|
1243
|
-
while IFS= read -r name; do
|
|
1244
|
-
[ -z "$name" ] && continue
|
|
1245
|
-
rel="$(get_pack_field "$config_file" "$name" "mirror")"
|
|
1246
|
-
[ -n "$rel" ] || continue
|
|
1247
|
-
canon="$(normalize_path "$repo_root/$rel")"
|
|
1248
|
-
printf '%s\n' "${canon#"$repo_root"/}"
|
|
1249
|
-
done < <(read_yaml_keys "$config_file" "packs")
|
|
1250
766
|
}
|
|
1251
767
|
|
|
1252
768
|
# Warn about prompt directories not listed in sources.
|
|
@@ -1259,14 +775,10 @@ warn_unsynced() {
|
|
|
1259
775
|
local repo_root="$1"
|
|
1260
776
|
local config_file="$2"
|
|
1261
777
|
|
|
1262
|
-
# Collect all configured source paths (local only — a remote spec and an
|
|
1263
|
-
# `@pack` reference are not filesystem dirs and cannot collide with an
|
|
1264
|
-
# unsynced local directory).
|
|
1265
778
|
local all_sources=()
|
|
1266
779
|
for section in rules agents skills; do
|
|
1267
780
|
while IFS= read -r src; do
|
|
1268
781
|
[ -z "$src" ] && continue
|
|
1269
|
-
source_is_local_path "$src" || continue
|
|
1270
782
|
all_sources+=("$src")
|
|
1271
783
|
done < <(read_yaml_list "$config_file" "$section")
|
|
1272
784
|
done
|
|
@@ -1281,19 +793,9 @@ warn_unsynced() {
|
|
|
1281
793
|
[ -z "$sub" ] && continue
|
|
1282
794
|
ignores+=("$sub")
|
|
1283
795
|
done < <(read_yaml_list "$config_file" "submodules")
|
|
1284
|
-
|
|
1285
|
-
#
|
|
1286
|
-
#
|
|
1287
|
-
local mirror_rel
|
|
1288
|
-
while IFS= read -r mirror_rel; do
|
|
1289
|
-
[ -n "$mirror_rel" ] && ignores+=("${mirror_rel%/}")
|
|
1290
|
-
done < <(list_pack_mirrors "$repo_root" "$config_file")
|
|
1291
|
-
|
|
1292
|
-
# Derive the intelligence folder basename from config.yaml's location —
|
|
1293
|
-
# whatever the user named it (`intelligence`, `Intelligence`, `prompts`).
|
|
1294
|
-
# In CLI mode the manifest sits at the repo root, so that derivation would
|
|
1295
|
-
# yield the repo directory's own name and never match — the content dir
|
|
1296
|
-
# comes from the env contract instead.
|
|
796
|
+
|
|
797
|
+
# The manifest sits at the repo root, so the content dir cannot be derived
|
|
798
|
+
# from its location — it comes from the env contract the CLI exports.
|
|
1297
799
|
local intel_basename
|
|
1298
800
|
if [ "${IS_CLI:-0}" = "1" ]; then
|
|
1299
801
|
intel_basename="$(basename "${IS_UMBRELLA_REL:-intelligence}")"
|
|
@@ -1579,12 +1081,6 @@ get_project_name() {
|
|
|
1579
1081
|
get_yaml_field "$1" "project" "name"
|
|
1580
1082
|
}
|
|
1581
1083
|
|
|
1582
|
-
# One field of a declared pack: `packs.<name>.<url|ref|mirror>`.
|
|
1583
|
-
# `mirror` empty means the pack is transient — cloned per run, never committed.
|
|
1584
|
-
get_pack_field() {
|
|
1585
|
-
get_nested_yaml_value "$1" "packs" "$2" "$3"
|
|
1586
|
-
}
|
|
1587
|
-
|
|
1588
1084
|
# Immediate sub-keys of a top-level block, one per line (`packs:` → pack names).
|
|
1589
1085
|
# Block form only: a pack always spans several lines, so the inline `{...}` form
|
|
1590
1086
|
# that `get_target_field` accommodates has no use here.
|