@ainova-systems/intelligence 0.11.0-rc.4 → 0.11.0-rc.6
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/add.sh +23 -1
- package/cli/commands/doctor.sh +28 -7
- package/cli/commands/init.sh +67 -21
- package/cli/commands/install.sh +55 -5
- package/cli/commands/migrate.sh +41 -9
- package/cli/commands/registry.sh +54 -35
- package/cli/commands/remove.sh +13 -1
- package/cli/commands/search.sh +12 -13
- package/cli/commands/status.sh +5 -4
- package/cli/commands/sync.sh +19 -1
- package/cli/commands/update.sh +23 -4
- package/cli/commands/upgrade.sh +50 -9
- package/cli/engine-package.yaml +14 -0
- package/cli/lib/cli-common.sh +37 -48
- package/cli/lib/manifest.sh +118 -0
- package/cli/lib/registry.sh +74 -39
- package/cli/lib/semver.sh +5 -2
- package/engine/agents/intelligence-operator.md +4 -4
- package/engine/docs/ADAPTERS.md +2 -1
- package/engine/docs/CLI.md +12 -13
- package/engine/docs/CONVENTIONS.md +5 -3
- package/engine/rules/intelligence-authoring.md +4 -4
- package/engine/scripts/ENGINE_SHA +1 -0
- package/engine/scripts/lib/common.sh +6 -2
- package/engine/scripts/sync.sh +1 -1
- package/engine/skills/intelligence-add-agent/SKILL.md +7 -7
- package/engine/skills/intelligence-add-rule/SKILL.md +5 -5
- package/engine/skills/intelligence-add-skill/SKILL.md +5 -5
- package/engine/skills/intelligence-extract-skill/SKILL.md +2 -2
- package/engine/skills/intelligence-install-adapter/SKILL.md +6 -6
- package/engine/skills/intelligence-learn-from-context/SKILL.md +1 -1
- package/engine/skills/intelligence-review-skills/SKILL.md +5 -5
- package/engine/skills/intelligence-sync/SKILL.md +5 -7
- package/engine/skills/intelligence-uninstall-adapter/SKILL.md +2 -2
- package/engine/skills/intelligence-update/SKILL.md +6 -0
- package/package.json +2 -3
- package/registry/index.yaml +0 -22
package/cli/commands/status.sh
CHANGED
|
@@ -13,12 +13,13 @@ case "$IP_MODE" in
|
|
|
13
13
|
else
|
|
14
14
|
echo "Lockfile: none (no packages added yet)"
|
|
15
15
|
fi
|
|
16
|
-
|
|
17
|
-
|
|
16
|
+
sync_locked="$(qmap_field "$IP_ROOT/intelligence.lock" "packages" "$SYNC_PKG_NAME" "resolved")"
|
|
17
|
+
if [ -n "$sync_locked" ]; then
|
|
18
|
+
echo "Content: $SYNC_PKG_NAME at $sync_locked"
|
|
18
19
|
else
|
|
19
|
-
echo "
|
|
20
|
+
echo "Content: no $SYNC_PKG_NAME (bare setup)"
|
|
20
21
|
fi
|
|
21
|
-
echo "Engine: $(bundled_engine_version) (bundled)"
|
|
22
|
+
echo "Engine: $(bundled_engine_version) (bundled with the CLI)"
|
|
22
23
|
;;
|
|
23
24
|
legacy)
|
|
24
25
|
echo "Project: $IP_ROOT (vendored v1 setup)"
|
package/cli/commands/sync.sh
CHANGED
|
@@ -6,7 +6,25 @@ source "$CLI_DIR/lib/cli-common.sh"
|
|
|
6
6
|
detect_project
|
|
7
7
|
case "$IP_MODE" in
|
|
8
8
|
v2)
|
|
9
|
-
|
|
9
|
+
# Fail closed, never skip silently: a store-backed source that is not
|
|
10
|
+
# on disk means the project is not installed (or predates the package
|
|
11
|
+
# model) — syncing would quietly drop whole rule sets.
|
|
12
|
+
missing="" stale=0
|
|
13
|
+
for section in rules agents skills; do
|
|
14
|
+
while IFS= read -r src; do
|
|
15
|
+
[ -n "$src" ] || continue
|
|
16
|
+
case "$src" in
|
|
17
|
+
.intelligence/engine/*) stale=1 ;;
|
|
18
|
+
.intelligence/*) [ -d "$IP_ROOT/$src" ] || missing="$missing $src" ;;
|
|
19
|
+
esac
|
|
20
|
+
done < <(read_yaml_list "$IP_ROOT/intelligence.yaml" "$section")
|
|
21
|
+
done
|
|
22
|
+
if [ "$stale" -eq 1 ]; then
|
|
23
|
+
die "this manifest predates the engine-content package (.intelligence/engine sources) — run 'intelligence upgrade'"
|
|
24
|
+
fi
|
|
25
|
+
if [ -n "$missing" ]; then
|
|
26
|
+
die "installed content missing on disk:$missing — run 'intelligence install'"
|
|
27
|
+
fi
|
|
10
28
|
export_engine_env "$IP_ROOT"
|
|
11
29
|
exec bash "$IS_ENGINE_DIR/scripts/sync.sh" "$@"
|
|
12
30
|
;;
|
package/cli/commands/update.sh
CHANGED
|
@@ -21,29 +21,48 @@ require_v2
|
|
|
21
21
|
manifest="$IP_ROOT/intelligence.yaml"
|
|
22
22
|
lock="$IP_ROOT/intelligence.lock"
|
|
23
23
|
|
|
24
|
-
moved=0
|
|
24
|
+
moved=0 found=0
|
|
25
25
|
while IFS= read -r name; do
|
|
26
26
|
[ -n "$name" ] || continue
|
|
27
27
|
[ -n "$only" ] && [ "$name" != "$only" ] && continue
|
|
28
|
+
# A named package that exists is "found" whatever happens next — a skip
|
|
29
|
+
# message followed by "not in the manifest" would contradict itself.
|
|
30
|
+
found=1
|
|
31
|
+
if [ "$name" = "$SYNC_PKG_NAME" ]; then
|
|
32
|
+
echo " $name: engine content moves with the engine — 'intelligence upgrade' owns it"
|
|
33
|
+
continue
|
|
34
|
+
fi
|
|
28
35
|
ref="$(qmap_field "$manifest" "packages" "$name" "ref")"
|
|
29
36
|
[ -n "$ref" ] && { echo " $name: pinned to ref '$ref' — skipped"; continue; }
|
|
30
37
|
range="$(qmap_field "$manifest" "packages" "$name" "version")"
|
|
38
|
+
# An exact version is a pin, same as ref: — update never moves pins.
|
|
39
|
+
if [ -n "$range" ] && semver_is_stable "$range"; then
|
|
40
|
+
echo " $name: pinned to $range — edit the manifest to move it"
|
|
41
|
+
continue
|
|
42
|
+
fi
|
|
31
43
|
url="$(qmap_field "$manifest" "packages" "$name" "url")"
|
|
32
44
|
path="$(qmap_field "$manifest" "packages" "$name" "path")"
|
|
33
45
|
if [ -z "$url" ]; then
|
|
34
46
|
resolve_package_source "$manifest" "$name"
|
|
35
47
|
url="$RES_URL"; path="$RES_PATH"
|
|
36
48
|
fi
|
|
37
|
-
checked=$((checked + 1))
|
|
38
49
|
picked="$(list_remote_versions "$url" | semver_pick_highest "$range")"
|
|
39
50
|
[ -n "$picked" ] || { echo " $name: nothing satisfies '${range:-*}' at $url" >&2; continue; }
|
|
40
51
|
read -r tag _ <<< "$(remote_tag_for_version "$url" "$picked")"
|
|
41
52
|
current="$(qmap_field "$lock" "packages" "$name" "resolved")"
|
|
42
|
-
|
|
53
|
+
cur_url="$(qmap_field "$lock" "packages" "$name" "url")"
|
|
54
|
+
cur_path="$(qmap_field "$lock" "packages" "$name" "path")"
|
|
55
|
+
# Same tag is not enough: a registry may re-point a name's source while
|
|
56
|
+
# keeping the version — the move doctor warns about must be actionable
|
|
57
|
+
# here, so url/path changes refetch too.
|
|
58
|
+
if [ "$tag" = "$current" ] && [ "$url" = "$cur_url" ] && [ "$path" = "$cur_path" ]; then
|
|
43
59
|
echo " $name: ${current:-<none>} (up to date)"
|
|
44
60
|
continue
|
|
45
61
|
fi
|
|
46
62
|
rel=".intelligence/packages/$name"
|
|
63
|
+
# The new version may have dropped a top-level section dir: unwire the old
|
|
64
|
+
# shape first, or a dangling store-backed source bricks the next sync.
|
|
65
|
+
unwire_package_sources "$manifest" "$rel"
|
|
47
66
|
sha="$(fetch_package "$url" "$tag" "$path" "$IP_ROOT/$rel")"
|
|
48
67
|
wire_package_sources "$manifest" "$name" "$rel" "$IP_ROOT"
|
|
49
68
|
lock_upsert "$lock" "$name" "$range" "$url" "$path" "$tag" "$sha"
|
|
@@ -51,7 +70,7 @@ while IFS= read -r name; do
|
|
|
51
70
|
moved=$((moved + 1))
|
|
52
71
|
done < <(qmap_keys "$manifest" "packages")
|
|
53
72
|
|
|
54
|
-
[ -n "$only" ] && [ "$
|
|
73
|
+
[ -n "$only" ] && [ "$found" -eq 0 ] && die "package '$only' is not in the manifest"
|
|
55
74
|
echo "updated: $moved package(s)"
|
|
56
75
|
|
|
57
76
|
if [ "$moved" -gt 0 ] && [ "$no_sync" -eq 0 ]; then
|
package/cli/commands/upgrade.sh
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
#!/bin/bash
|
|
2
|
-
# intelligence upgrade — bring the project to this CLI's engine:
|
|
3
|
-
#
|
|
4
|
-
#
|
|
2
|
+
# intelligence upgrade — bring the project to this CLI's engine: apply v2
|
|
3
|
+
# schema migrations, align the engine-content package to the bundled version,
|
|
4
|
+
# restamp sync_version, sync.
|
|
5
5
|
set -euo pipefail
|
|
6
6
|
source "$CLI_DIR/lib/cli-common.sh"
|
|
7
7
|
|
|
@@ -14,13 +14,54 @@ if [ -n "$stamp" ] && _ver_gt "$stamp" "$eng"; then
|
|
|
14
14
|
die "manifest schema $stamp is newer than this CLI's engine $eng — update the CLI first: npm i -g @ainova-systems/intelligence@latest"
|
|
15
15
|
fi
|
|
16
16
|
|
|
17
|
-
# v2 schema migrations
|
|
18
|
-
# vendored layouts
|
|
19
|
-
#
|
|
20
|
-
#
|
|
17
|
+
# --- v2 schema migrations -------------------------------------------------
|
|
18
|
+
# The engine's own chain owns vendored layouts; this one owns the manifest.
|
|
19
|
+
# Same discipline: ascending, append-only, each migration self-detects and
|
|
20
|
+
# no-ops when already applied.
|
|
21
|
+
|
|
22
|
+
# v2 migration 1: staged engine content -> the @ainova-systems/sync package.
|
|
23
|
+
# Pre-package manifests listed `.intelligence/engine/{rules,agents,skills}`
|
|
24
|
+
# as sources fed by a CLI-staged copy; that copy is now an ordinary package
|
|
25
|
+
# entry seeded from the bundle. Post-condition: no `.intelligence/engine`
|
|
26
|
+
# source entries, the package present in manifest+lock+store, stale dir gone.
|
|
27
|
+
migrated=0
|
|
28
|
+
for section in rules agents skills; do
|
|
29
|
+
while IFS= read -r src; do
|
|
30
|
+
case "$src" in
|
|
31
|
+
.intelligence/engine/*)
|
|
32
|
+
sources_remove_entry "$manifest" "$section" "$src"
|
|
33
|
+
migrated=1
|
|
34
|
+
;;
|
|
35
|
+
esac
|
|
36
|
+
done < <(read_yaml_list "$manifest" "$section")
|
|
37
|
+
done
|
|
38
|
+
if [ "$migrated" -eq 1 ]; then
|
|
39
|
+
echo " migrating: staged engine content -> $SYNC_PKG_NAME package"
|
|
40
|
+
rm -rf "$IP_ROOT/.intelligence/engine"
|
|
41
|
+
sync_pkg_entry "$manifest"
|
|
42
|
+
sync_pkg_install "$IP_ROOT"
|
|
43
|
+
fi
|
|
44
|
+
|
|
45
|
+
# --- steady state ---------------------------------------------------------
|
|
46
|
+
# Align the engine-content package with the bundled engine. Only a manifest
|
|
47
|
+
# that HAS the entry is touched — a --bare project opted out, and upgrade
|
|
48
|
+
# respects that.
|
|
49
|
+
have_pkg=0
|
|
50
|
+
while IFS= read -r name; do
|
|
51
|
+
[ "$name" = "$SYNC_PKG_NAME" ] && have_pkg=1
|
|
52
|
+
done < <(qmap_keys "$manifest" "packages")
|
|
53
|
+
if [ "$have_pkg" -eq 1 ] && [ "$migrated" -eq 0 ]; then
|
|
54
|
+
pinned="$(qmap_field "$manifest" "packages" "$SYNC_PKG_NAME" "version")"
|
|
55
|
+
if [ "$pinned" != "$eng" ]; then
|
|
56
|
+
echo " $SYNC_PKG_NAME: $pinned -> $eng"
|
|
57
|
+
fi
|
|
58
|
+
sync_pkg_entry "$manifest"
|
|
59
|
+
sync_pkg_install "$IP_ROOT"
|
|
60
|
+
fi
|
|
61
|
+
if [ "$have_pkg" -eq 0 ] && [ "$migrated" -eq 0 ]; then
|
|
62
|
+
echo " NOTE: $SYNC_PKG_NAME is not in this manifest (bare setup) — engine meta-skills stay uninstalled; 'intelligence add $SYNC_PKG_NAME' opts back in." >&2
|
|
63
|
+
fi
|
|
21
64
|
|
|
22
|
-
stage_engine_content "$IP_ROOT/.intelligence/engine"
|
|
23
|
-
echo " engine content restaged ($eng)"
|
|
24
65
|
stamp_version "$manifest" "$eng"
|
|
25
66
|
echo " sync_version -> $eng"
|
|
26
67
|
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
# What THIS CLI distribution ships as its engine-content package, and which
|
|
2
|
+
# registry its init seeds into new manifests. Data, not code: a fork or an
|
|
3
|
+
# enterprise mirror edits this file (or its copy in the npm build) — nothing
|
|
4
|
+
# in cli/ names a vendor.
|
|
5
|
+
#
|
|
6
|
+
# The engine-content package is the one package whose content documents the
|
|
7
|
+
# engine scripts bundled with the CLI: init writes this entry into the
|
|
8
|
+
# manifest (visible, removable), its pin always equals the engine VERSION,
|
|
9
|
+
# `upgrade` moves it, and at the matching version it materializes from the
|
|
10
|
+
# bundle without network.
|
|
11
|
+
name: "@ainova-systems/sync"
|
|
12
|
+
url: "https://github.com/ainova-systems/intelligence-sync.git"
|
|
13
|
+
path: "intelligence/sync"
|
|
14
|
+
default_registry: "https://github.com/ainova-systems/intelligence-registry.git"
|
package/cli/lib/cli-common.sh
CHANGED
|
@@ -24,10 +24,23 @@ source "$CLI_DIR/lib/semver.sh"
|
|
|
24
24
|
source "$CLI_DIR/lib/registry.sh"
|
|
25
25
|
source "$CLI_DIR/lib/lockfile.sh"
|
|
26
26
|
|
|
27
|
-
#
|
|
28
|
-
#
|
|
29
|
-
# the CLI
|
|
30
|
-
|
|
27
|
+
# The engine-content package: OPTIONAL but auto-selected at init. Package by
|
|
28
|
+
# UX (manifest entry, lockfile row, list/search/remove), bundle by mechanics —
|
|
29
|
+
# at the version the CLI ships, it materializes from the npm bundle without
|
|
30
|
+
# network; only a cross-version install reaches git. The pin is held exactly
|
|
31
|
+
# at the bundled engine version and moved only by `upgrade`.
|
|
32
|
+
#
|
|
33
|
+
# Its identity is DATA shipped with the distribution (cli/engine-package.yaml),
|
|
34
|
+
# never a name compiled into cli code — a fork edits the file.
|
|
35
|
+
_EPKG="$CLI_DIR/engine-package.yaml"
|
|
36
|
+
SYNC_PKG_NAME="$(top_scalar "$_EPKG" "name")"
|
|
37
|
+
SYNC_PKG_URL="$(top_scalar "$_EPKG" "url")"
|
|
38
|
+
SYNC_PKG_PATH="$(top_scalar "$_EPKG" "path")"
|
|
39
|
+
# Read by commands (init seeds it) — per-file shellcheck cannot see that.
|
|
40
|
+
# shellcheck disable=SC2034
|
|
41
|
+
DEFAULT_REGISTRY_URL="$(top_scalar "$_EPKG" "default_registry")"
|
|
42
|
+
[ -n "$SYNC_PKG_NAME" ] || die "corrupt CLI installation: $_EPKG is missing or has no 'name'"
|
|
43
|
+
SYNC_PKG_STORE=".intelligence/packages/$SYNC_PKG_NAME"
|
|
31
44
|
|
|
32
45
|
# --- Project detection ---------------------------------------------------
|
|
33
46
|
# Sets: IP_MODE (v2|legacy|none), IP_ROOT, IP_UMBRELLA, IP_MODULE_DIR.
|
|
@@ -90,51 +103,26 @@ bundled_engine_version() {
|
|
|
90
103
|
tr -d ' \t\r\n' < "$IS_ENGINE_DIR/scripts/VERSION"
|
|
91
104
|
}
|
|
92
105
|
|
|
93
|
-
# ---
|
|
94
|
-
#
|
|
95
|
-
#
|
|
96
|
-
#
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
rm -rf "$store"
|
|
103
|
-
mkdir -p "$store"
|
|
104
|
-
local d s name skip
|
|
105
|
-
# `scripts` travels too: engine-shipped skills reference `<module>/scripts/…`
|
|
106
|
-
# (adapters, docs) and `<module>` resolves here, so the paths they hand an
|
|
107
|
-
# agent must exist. Running that sync.sh directly is harmless — outside CLI
|
|
108
|
-
# mode it fails closed rather than generating against the wrong layout.
|
|
109
|
-
for d in rules agents docs scripts; do
|
|
110
|
-
[ -d "$IS_ENGINE_DIR/$d" ] && cp -R "$IS_ENGINE_DIR/$d" "$store/$d"
|
|
111
|
-
done
|
|
112
|
-
if [ -d "$IS_ENGINE_DIR/skills" ]; then
|
|
113
|
-
mkdir -p "$store/skills"
|
|
114
|
-
for s in "$IS_ENGINE_DIR"/skills/*/; do
|
|
115
|
-
[ -d "$s" ] || continue
|
|
116
|
-
name="$(basename "$s")"
|
|
117
|
-
skip=0
|
|
118
|
-
for d in $CLI_OBSOLETE_SKILLS; do
|
|
119
|
-
[ "$name" = "$d" ] && skip=1
|
|
120
|
-
done
|
|
121
|
-
[ "$skip" = "1" ] && continue
|
|
122
|
-
cp -R "$s" "$store/skills/$name"
|
|
123
|
-
done
|
|
124
|
-
fi
|
|
125
|
-
bundled_engine_version > "$store/.version"
|
|
106
|
+
# --- The sync package's manifest/lock plumbing ---------------------------
|
|
107
|
+
# sync_pkg_entry <manifest> — write/refresh the package's manifest entry:
|
|
108
|
+
# name + exact pin + explicit url/path, so resolution never depends on any
|
|
109
|
+
# registry (a project registry shadowing the name cannot brick the engine).
|
|
110
|
+
sync_pkg_entry() {
|
|
111
|
+
local manifest="$1"
|
|
112
|
+
qmap_set "$manifest" "packages" "$SYNC_PKG_NAME" "version" "$(bundled_engine_version)"
|
|
113
|
+
qmap_set "$manifest" "packages" "$SYNC_PKG_NAME" "url" "$SYNC_PKG_URL"
|
|
114
|
+
qmap_set "$manifest" "packages" "$SYNC_PKG_NAME" "path" "$SYNC_PKG_PATH"
|
|
126
115
|
}
|
|
127
116
|
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
local
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
echo " engine content staged: .intelligence/engine ($bundled_ver)"
|
|
117
|
+
# sync_pkg_install <root> — materialize the package into the store and lock
|
|
118
|
+
# it (offline at the bundled version — fetch_package's bundle-seed guard).
|
|
119
|
+
sync_pkg_install() {
|
|
120
|
+
local root="$1" ver sha
|
|
121
|
+
ver="$(bundled_engine_version)"
|
|
122
|
+
sha="$(fetch_package "$SYNC_PKG_URL" "v$ver" "$SYNC_PKG_PATH" "$root/$SYNC_PKG_STORE")"
|
|
123
|
+
wire_package_sources "$root/intelligence.yaml" "$SYNC_PKG_NAME" "$SYNC_PKG_STORE" "$root"
|
|
124
|
+
lock_upsert "$root/intelligence.lock" "$SYNC_PKG_NAME" "$ver" "$SYNC_PKG_URL" "$SYNC_PKG_PATH" "v$ver" "$sha"
|
|
125
|
+
echo " engine content installed: $SYNC_PKG_STORE (v$ver)"
|
|
138
126
|
}
|
|
139
127
|
|
|
140
128
|
# --- The engine env contract ---------------------------------------------
|
|
@@ -147,7 +135,8 @@ export_engine_env() {
|
|
|
147
135
|
export CONFIG_FILE="$root/intelligence.yaml"
|
|
148
136
|
export REPO_ROOT="$root"
|
|
149
137
|
export IS_UMBRELLA_REL="$umbrella"
|
|
150
|
-
export IS_MODULE_REL="
|
|
138
|
+
export IS_MODULE_REL="$SYNC_PKG_STORE"
|
|
151
139
|
export IS_SYNC_CMD="intelligence sync"
|
|
140
|
+
export IS_MANIFEST_NAME="intelligence.yaml"
|
|
152
141
|
export IS_PROTECTED_DIRS="$umbrella:.intelligence"
|
|
153
142
|
}
|
package/cli/lib/manifest.sh
CHANGED
|
@@ -24,6 +24,16 @@ assert_valid_pkg_name() {
|
|
|
24
24
|
case "$1" in
|
|
25
25
|
*[!@/A-Za-z0-9._-]*|*/*/*) die "invalid package name '$1' — allowed: letters, digits, . _ -, one '/'" ;;
|
|
26
26
|
esac
|
|
27
|
+
# Both segments become store path components fed to rm -rf: an empty one
|
|
28
|
+
# ('@scope/') resolves to the whole scope dir, and a dot segment walks the
|
|
29
|
+
# tree — either would turn one bad --name into deleting other packages.
|
|
30
|
+
local scope="${1%%/*}" short="${1#*/}"
|
|
31
|
+
case "${scope#@}" in
|
|
32
|
+
""|.|..) die "invalid package name '$1' — empty or dot scope" ;;
|
|
33
|
+
esac
|
|
34
|
+
case "$short" in
|
|
35
|
+
""|.|..) die "invalid package name '$1' — empty or dot name" ;;
|
|
36
|
+
esac
|
|
27
37
|
}
|
|
28
38
|
|
|
29
39
|
# qmap_keys <file> <block> — quoted keys, one per line.
|
|
@@ -194,6 +204,114 @@ qmap_delete_key() {
|
|
|
194
204
|
'
|
|
195
205
|
}
|
|
196
206
|
|
|
207
|
+
# --- registries: a trust LIST, not a scope map -----------------------------
|
|
208
|
+
# The block holds registry repo URLs in trust order. Two shapes are read so
|
|
209
|
+
# early manifests keep working: the list form (`- "url"`) and the retired
|
|
210
|
+
# flat-map form (`"@scope": "url"` — the scope label is ignored, the URL is
|
|
211
|
+
# simply another registry).
|
|
212
|
+
|
|
213
|
+
# registries_list <file> — registry URLs, one per line, manifest order.
|
|
214
|
+
registries_list() {
|
|
215
|
+
[ -f "$1" ] || return 0
|
|
216
|
+
awk '
|
|
217
|
+
{ sub(/\r$/, "") }
|
|
218
|
+
/^registries:[ \t]*$/ { inb = 1; next }
|
|
219
|
+
inb && /^[^ #]/ { inb = 0 }
|
|
220
|
+
inb {
|
|
221
|
+
line = $0
|
|
222
|
+
if (line ~ /^[ \t]*-[ \t]*/) {
|
|
223
|
+
sub(/^[ \t]*-[ \t]*/, "", line)
|
|
224
|
+
} else if (line ~ /^ "/) {
|
|
225
|
+
c = index(line, ":")
|
|
226
|
+
if (c == 0) next
|
|
227
|
+
line = substr(line, c + 1)
|
|
228
|
+
} else next
|
|
229
|
+
sub(/^[ \t]+/, "", line)
|
|
230
|
+
gsub(/["\x27]/, "", line)
|
|
231
|
+
sub(/[ \t]+#.*$/, "", line)
|
|
232
|
+
sub(/[ \t]+$/, "", line)
|
|
233
|
+
if (line != "") print line
|
|
234
|
+
}
|
|
235
|
+
' "$1"
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
# registries_add <file> <url> — idempotent append in list form.
|
|
239
|
+
registries_add() {
|
|
240
|
+
local file="$1" url="$2" existing
|
|
241
|
+
while IFS= read -r existing; do
|
|
242
|
+
[ "$existing" = "$url" ] && return 0
|
|
243
|
+
done < <(registries_list "$file")
|
|
244
|
+
_qmap_stage "$file" -v url="$url" '
|
|
245
|
+
function entry() { return " - \"" url "\"" }
|
|
246
|
+
{ sub(/\r$/, "") }
|
|
247
|
+
/^registries:[ \t]*$/ { blockseen = 1; inb = 1; print; next }
|
|
248
|
+
inb && /^[^ #]/ { if (!done) { print entry(); done = 1 }; inb = 0 }
|
|
249
|
+
{ last = $0; print }
|
|
250
|
+
END {
|
|
251
|
+
if (inb && !done) { print entry(); done = 1 }
|
|
252
|
+
if (!blockseen) {
|
|
253
|
+
if (last != "") print ""
|
|
254
|
+
print "registries:"
|
|
255
|
+
print entry()
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
'
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
# registries_remove <file> <url> — drop the entry, either shape.
|
|
262
|
+
registries_remove() {
|
|
263
|
+
local file="$1" url="$2"
|
|
264
|
+
[ -f "$file" ] || return 0
|
|
265
|
+
_qmap_stage "$file" -v url="$url" '
|
|
266
|
+
{ sub(/\r$/, "") }
|
|
267
|
+
/^registries:[ \t]*$/ { inb = 1; print; next }
|
|
268
|
+
inb && /^[^ #]/ { inb = 0 }
|
|
269
|
+
inb {
|
|
270
|
+
line = $0
|
|
271
|
+
v = ""
|
|
272
|
+
if (line ~ /^[ \t]*-[ \t]*/) { v = line; sub(/^[ \t]*-[ \t]*/, "", v) }
|
|
273
|
+
else if (line ~ /^ "/) { c = index(line, ":"); if (c > 0) v = substr(line, c + 1) }
|
|
274
|
+
if (v != "") {
|
|
275
|
+
sub(/^[ \t]+/, "", v)
|
|
276
|
+
gsub(/["\x27]/, "", v)
|
|
277
|
+
sub(/[ \t]+#.*$/, "", v)
|
|
278
|
+
sub(/[ \t]+$/, "", v)
|
|
279
|
+
if (v == url) next
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
{ print }
|
|
283
|
+
'
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
# sources_add_entry_first <file> <section> <entry> — idempotent insert at the
|
|
287
|
+
# TOP of sources.<section> (creating sources:/section as needed). Package
|
|
288
|
+
# entries go through this so project-owned entries stay later in the list —
|
|
289
|
+
# adapters copy sources in order and the last write wins, which is exactly
|
|
290
|
+
# the documented "your file overrides the package's" behavior.
|
|
291
|
+
sources_add_entry_first() {
|
|
292
|
+
local file="$1" section="$2" entry="$3"
|
|
293
|
+
_mig_has_source "$file" "$entry" && return 0
|
|
294
|
+
_qmap_stage "$file" -v section="$section" -v entry="$entry" '
|
|
295
|
+
function line() { return " - \"" entry "\"" }
|
|
296
|
+
{ sub(/\r$/, "") }
|
|
297
|
+
/^sources:[ \t]*$/ { ins = 1; sourceseen = 1; print; next }
|
|
298
|
+
ins && /^[^ #]/ {
|
|
299
|
+
if (!secseen && !done) { print " " section ":"; print line(); done = 1 }
|
|
300
|
+
ins = 0
|
|
301
|
+
}
|
|
302
|
+
ins && $0 ~ "^ " section ":[ \t]*$" { secseen = 1; print; print line(); done = 1; next }
|
|
303
|
+
{ print }
|
|
304
|
+
END {
|
|
305
|
+
if (ins && !secseen && !done) { print " " section ":"; print line(); done = 1 }
|
|
306
|
+
if (!sourceseen) {
|
|
307
|
+
print "sources:"
|
|
308
|
+
print " " section ":"
|
|
309
|
+
print line()
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
'
|
|
313
|
+
}
|
|
314
|
+
|
|
197
315
|
# sources_remove_entry <file> <section> <entry> — remove `- "entry"` from
|
|
198
316
|
# sources.<section>. Inverse of the engine-side _mig_add_source.
|
|
199
317
|
sources_remove_entry() {
|
package/cli/lib/registry.sh
CHANGED
|
@@ -1,23 +1,23 @@
|
|
|
1
1
|
#!/bin/bash
|
|
2
2
|
# Name -> source resolution and package fetching.
|
|
3
3
|
#
|
|
4
|
-
#
|
|
5
|
-
#
|
|
6
|
-
#
|
|
7
|
-
#
|
|
8
|
-
#
|
|
9
|
-
#
|
|
4
|
+
# A name resolves ONLY through the manifest's `registries:` trust list (git
|
|
5
|
+
# repos holding index.yaml, consulted in order; the first to declare the name
|
|
6
|
+
# wins). There is deliberately no built-in catalog and no name->github
|
|
7
|
+
# convention: the CLI core knows no vendor, and a name nobody explicitly
|
|
8
|
+
# trusted must never silently turn into an install from a guessed URL.
|
|
9
|
+
# Sources without a registry are always EXPLICIT: github:org/repo, git+<url>.
|
|
10
|
+
# Vendor defaults exist only as lines `init` writes into the manifest —
|
|
11
|
+
# visible, reviewable, deletable.
|
|
12
|
+
#
|
|
13
|
+
# The NAME is the trust anchor a developer reasons with; source integrity is
|
|
14
|
+
# a separate mechanism: the lock pins url+sha, and doctor flags
|
|
15
|
+
# resolution/lock url drift.
|
|
10
16
|
#
|
|
11
17
|
# An index is itself fetched with git (never curl): auth, proxies and private
|
|
12
18
|
# hosting all come for free, and the CLI keeps the engine's bash+awk+git-only
|
|
13
19
|
# dependency footprint.
|
|
14
20
|
|
|
15
|
-
default_index_file() {
|
|
16
|
-
local f="$CLI_DIR/../registry/index.yaml"
|
|
17
|
-
[ -f "$f" ] && printf '%s' "$f"
|
|
18
|
-
return 0
|
|
19
|
-
}
|
|
20
|
-
|
|
21
21
|
# _fetch_index <registry-repo-url> — clones the registry repo (shallow) and
|
|
22
22
|
# prints the path of its index.yaml; empty on failure. Cached per run+url.
|
|
23
23
|
_fetch_index() {
|
|
@@ -43,38 +43,57 @@ _fetch_index() {
|
|
|
43
43
|
# shellcheck disable=SC2034
|
|
44
44
|
resolve_package_source() {
|
|
45
45
|
local manifest="$1" name="$2"
|
|
46
|
-
local scope="${name%%/*}" short="${name#*/}"
|
|
47
46
|
RES_URL=""; RES_PATH=""; RES_VIA=""
|
|
48
47
|
|
|
48
|
+
local reg_url index
|
|
49
49
|
if [ -n "$manifest" ] && [ -f "$manifest" ]; then
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
if [ -n "$reg_url" ]; then
|
|
50
|
+
while IFS= read -r reg_url; do
|
|
51
|
+
[ -n "$reg_url" ] || continue
|
|
53
52
|
index="$(_fetch_index "${reg_url#git+}")"
|
|
54
|
-
[ -
|
|
53
|
+
if [ -z "$index" ]; then
|
|
54
|
+
echo " WARN: registry unreachable or missing index.yaml, skipped: $reg_url" >&2
|
|
55
|
+
continue
|
|
56
|
+
fi
|
|
55
57
|
RES_URL="$(qmap_field "$index" "packages" "$name" "url")"
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
58
|
+
if [ -n "$RES_URL" ]; then
|
|
59
|
+
RES_PATH="$(qmap_field "$index" "packages" "$name" "path")"
|
|
60
|
+
RES_VIA="registry:$reg_url"
|
|
61
|
+
return 0
|
|
62
|
+
fi
|
|
63
|
+
done < <(registries_list "$manifest")
|
|
61
64
|
fi
|
|
65
|
+
# No trusted registry declares the name. Deliberately NOT an invented
|
|
66
|
+
# URL: the caller reports it, with suggestions.
|
|
67
|
+
return 0
|
|
68
|
+
}
|
|
62
69
|
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
70
|
+
# suggest_similar <manifest-or-empty> <@scope/name> — "Did you mean" lines to
|
|
71
|
+
# stderr, matched on the short name (singular/plural tolerant) across every
|
|
72
|
+
# trusted registry.
|
|
73
|
+
suggest_similar() {
|
|
74
|
+
local manifest="$1" name="$2"
|
|
75
|
+
local want cand cshort reg_url index seen=" "
|
|
76
|
+
want="${name#*/}"; want="${want%s}"
|
|
77
|
+
_suggest_from() {
|
|
78
|
+
local idx="$1"
|
|
79
|
+
[ -n "$idx" ] && [ -f "$idx" ] || return 0
|
|
80
|
+
while IFS= read -r cand; do
|
|
81
|
+
[ -n "$cand" ] || continue
|
|
82
|
+
case "$seen" in *" $cand "*) continue ;; esac
|
|
83
|
+
cshort="${cand#*/}"; cshort="${cshort%s}"
|
|
84
|
+
if [ "$cshort" = "$want" ] && [ "$cand" != "$name" ]; then
|
|
85
|
+
seen="$seen$cand "
|
|
86
|
+
echo " Did you mean: intelligence add $cand" >&2
|
|
87
|
+
fi
|
|
88
|
+
done < <(qmap_keys "$idx" "packages")
|
|
89
|
+
}
|
|
90
|
+
if [ -n "$manifest" ] && [ -f "$manifest" ]; then
|
|
91
|
+
while IFS= read -r reg_url; do
|
|
92
|
+
[ -n "$reg_url" ] || continue
|
|
93
|
+
index="$(_fetch_index "${reg_url#git+}")"
|
|
94
|
+
_suggest_from "$index"
|
|
95
|
+
done < <(registries_list "$manifest")
|
|
72
96
|
fi
|
|
73
|
-
|
|
74
|
-
RES_URL="https://github.com/${scope#@}/$short.git"
|
|
75
|
-
RES_PATH=""
|
|
76
|
-
RES_VIA="convention"
|
|
77
|
-
return 0
|
|
78
97
|
}
|
|
79
98
|
|
|
80
99
|
# fetch_package <url> <ref> <subpath> <dest-dir>
|
|
@@ -84,6 +103,22 @@ resolve_package_source() {
|
|
|
84
103
|
# store holds the bytes the package published.
|
|
85
104
|
fetch_package() {
|
|
86
105
|
local url="$1" ref="$2" subpath="$3" dest="$4"
|
|
106
|
+
# Bundle seed: the engine-content package at the CLI's own version copies
|
|
107
|
+
# from the npm bundle — no network, which keeps init / fresh-clone install
|
|
108
|
+
# / migrate offline exactly like the staging they replace. Keyed on the
|
|
109
|
+
# full (url, path, ref) triple; any other version or source falls through
|
|
110
|
+
# to the normal clone.
|
|
111
|
+
if [ "$url" = "$SYNC_PKG_URL" ] && [ "$subpath" = "$SYNC_PKG_PATH" ] \
|
|
112
|
+
&& [ "${ref#v}" = "$(bundled_engine_version)" ]; then
|
|
113
|
+
rm -rf "$dest"
|
|
114
|
+
mkdir -p "$dest"
|
|
115
|
+
cp -R "$IS_ENGINE_DIR/." "$dest/"
|
|
116
|
+
rm -rf "$dest/.git"
|
|
117
|
+
if [ -f "$IS_ENGINE_DIR/scripts/ENGINE_SHA" ]; then
|
|
118
|
+
tr -d ' \t\r\n' < "$IS_ENGINE_DIR/scripts/ENGINE_SHA"
|
|
119
|
+
fi
|
|
120
|
+
return 0
|
|
121
|
+
fi
|
|
87
122
|
local tmp sha src
|
|
88
123
|
local -a branch_arg=()
|
|
89
124
|
[ -n "$ref" ] && branch_arg=(--branch "$ref")
|
|
@@ -119,14 +154,14 @@ fetch_package() {
|
|
|
119
154
|
|
|
120
155
|
# wire_package_sources <manifest> <@scope/name> <store-rel-dir>
|
|
121
156
|
# The provides convention: whichever of rules/agents/skills the installed
|
|
122
|
-
# package has becomes a sources entry
|
|
123
|
-
#
|
|
157
|
+
# package has becomes a sources entry — inserted FIRST in its section, so
|
|
158
|
+
# project-owned entries come later and win on same-named artifacts.
|
|
124
159
|
wire_package_sources() {
|
|
125
160
|
local manifest="$1" name="$2" rel="$3" root="$4"
|
|
126
161
|
local section
|
|
127
162
|
for section in rules agents skills; do
|
|
128
163
|
if [ -d "$root/$rel/$section" ]; then
|
|
129
|
-
|
|
164
|
+
sources_add_entry_first "$manifest" "$section" "$rel/$section"
|
|
130
165
|
fi
|
|
131
166
|
done
|
|
132
167
|
}
|
package/cli/lib/semver.sh
CHANGED
|
@@ -75,7 +75,10 @@ semver_match() {
|
|
|
75
75
|
# onto their tag.
|
|
76
76
|
list_remote_versions() {
|
|
77
77
|
local url="$1"
|
|
78
|
-
|
|
78
|
+
# `|| true` inside the pipeline: an unreachable repo must read as "no
|
|
79
|
+
# versions", not kill a pipefail caller mid-command-substitution —
|
|
80
|
+
# reachability is the CALLER's question (add probes it explicitly).
|
|
81
|
+
{ GIT_TERMINAL_PROMPT=0 git ls-remote --tags "$url" 2>/dev/null || true; } \
|
|
79
82
|
| awk '{
|
|
80
83
|
sub(/\r$/, "")
|
|
81
84
|
ref = $2
|
|
@@ -92,7 +95,7 @@ list_remote_versions() {
|
|
|
92
95
|
# prints "<tag> <sha>". Peeled sha (the commit a tag object points at) wins.
|
|
93
96
|
remote_tag_for_version() {
|
|
94
97
|
local url="$1" ver="${2#v}"
|
|
95
|
-
GIT_TERMINAL_PROMPT=0 git ls-remote --tags "$url" 2>/dev/null \
|
|
98
|
+
{ GIT_TERMINAL_PROMPT=0 git ls-remote --tags "$url" 2>/dev/null || true; } \
|
|
96
99
|
| awk -v want="$ver" '
|
|
97
100
|
{
|
|
98
101
|
sub(/\r$/, "")
|
|
@@ -19,10 +19,10 @@ that ships it.
|
|
|
19
19
|
|
|
20
20
|
## Expertise
|
|
21
21
|
|
|
22
|
-
The
|
|
23
|
-
here is deterministic and fail-closed:
|
|
24
|
-
un-applied schema,
|
|
25
|
-
|
|
22
|
+
The status contract (`<module>/docs/CONVENTIONS.md`, Migration & Module Contract). Every flow
|
|
23
|
+
here is deterministic and fail-closed: the sync is a pure synchronizer that refuses across an
|
|
24
|
+
un-applied schema, the update flow stages, verifies and only then commits, and every flow
|
|
25
|
+
reports `IS_STATUS`. The work is running the right flow, reading the code it returns, and doing
|
|
26
26
|
what that code says - including stopping. The guards carry the decisions, which is why this agent
|
|
27
27
|
runs on the standard tier: the failure mode is a loud refusal and a retry, not a plausible wrong
|
|
28
28
|
answer.
|