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