@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
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
#!/bin/bash
|
|
2
|
+
# Internal package range update operation.
|
|
3
|
+
#
|
|
4
|
+
# Resolve requested versions/refs against each package's locked source and
|
|
5
|
+
# rewrite the lock. `update <name>` limits the pass to one package; re-adding
|
|
6
|
+
# is the explicit operation for changing a source URL/path.
|
|
7
|
+
set -euo pipefail
|
|
8
|
+
source "$CLI_DIR/lib/cli-common.sh"
|
|
9
|
+
|
|
10
|
+
only="" no_sync=0 preview=0
|
|
11
|
+
while [ $# -gt 0 ]; do
|
|
12
|
+
case "$1" in
|
|
13
|
+
--no-sync) no_sync=1 ;;
|
|
14
|
+
--preview) preview=1 ;;
|
|
15
|
+
@*) only="$1" ;;
|
|
16
|
+
*) die "unknown argument '$1'" ;;
|
|
17
|
+
esac
|
|
18
|
+
shift || true
|
|
19
|
+
done
|
|
20
|
+
|
|
21
|
+
require_cli_project
|
|
22
|
+
manifest="$IP_ROOT/intelligence.yaml"
|
|
23
|
+
lock="$IP_ROOT/intelligence.lock"
|
|
24
|
+
|
|
25
|
+
moved=0 found=0
|
|
26
|
+
while IFS= read -r name; do
|
|
27
|
+
[ -n "$name" ] || continue
|
|
28
|
+
# Manifest keys are untrusted input on their way into store paths.
|
|
29
|
+
assert_valid_pkg_name "$name"
|
|
30
|
+
[ -n "$only" ] && [ "$name" != "$only" ] && continue
|
|
31
|
+
# A named package that exists is "found" whatever happens next — a skip
|
|
32
|
+
# message followed by "not in the manifest" would contradict itself.
|
|
33
|
+
found=1
|
|
34
|
+
if [ "$name" = "$SYNC_PKG_NAME" ]; then
|
|
35
|
+
echo " $name: engine content follows the installed CLI — skipped"
|
|
36
|
+
continue
|
|
37
|
+
fi
|
|
38
|
+
current="$(qmap_field "$lock" "packages" "$name" "resolved")"
|
|
39
|
+
locked_requested="$(qmap_field "$lock" "packages" "$name" "requested")"
|
|
40
|
+
url="$(qmap_field "$lock" "packages" "$name" "url")"
|
|
41
|
+
path="$(qmap_field "$lock" "packages" "$name" "path")"
|
|
42
|
+
[ -n "$url" ] || die "$name has no source in intelligence.lock — restore the committed lock or re-add the package"
|
|
43
|
+
ref="$(qmap_field "$manifest" "packages" "$name" "ref")"
|
|
44
|
+
range="$(qmap_field "$manifest" "packages" "$name" "version")"
|
|
45
|
+
[ -n "$ref" ] || [ -n "$range" ] || die "$name has neither version nor ref in the manifest"
|
|
46
|
+
|
|
47
|
+
if [ -n "$ref" ]; then
|
|
48
|
+
tag="$ref"
|
|
49
|
+
requested=""
|
|
50
|
+
else
|
|
51
|
+
picked="$(list_remote_versions "$url" | semver_pick_highest "$range")"
|
|
52
|
+
[ -n "$picked" ] || { echo " $name: nothing satisfies '$range' at $url" >&2; continue; }
|
|
53
|
+
read -r tag _ <<< "$(remote_tag_for_version "$url" "$picked")"
|
|
54
|
+
requested="$range"
|
|
55
|
+
fi
|
|
56
|
+
if [ "$tag" = "$current" ] && [ "$requested" != "$locked_requested" ]; then
|
|
57
|
+
if [ "$preview" -eq 1 ]; then
|
|
58
|
+
echo " $name: request ${locked_requested:-<none>} -> ${requested:-<ref>} (keeps $current)"
|
|
59
|
+
else
|
|
60
|
+
locked_sha="$(qmap_field "$lock" "packages" "$name" "sha")"
|
|
61
|
+
lock_upsert "$lock" "$name" "$requested" "$url" "$path" "$current" "$locked_sha"
|
|
62
|
+
echo " $name: request ${locked_requested:-<none>} -> ${requested:-<ref>} (kept $current)"
|
|
63
|
+
fi
|
|
64
|
+
moved=$((moved + 1))
|
|
65
|
+
continue
|
|
66
|
+
fi
|
|
67
|
+
if [ "$tag" = "$current" ]; then
|
|
68
|
+
echo " $name: ${current:-<none>} (up to date)"
|
|
69
|
+
continue
|
|
70
|
+
fi
|
|
71
|
+
if [ "$preview" -eq 1 ]; then
|
|
72
|
+
echo " $name: ${current:-<none>} -> $tag"
|
|
73
|
+
moved=$((moved + 1))
|
|
74
|
+
continue
|
|
75
|
+
fi
|
|
76
|
+
rel=".intelligence/packages/$name"
|
|
77
|
+
# Fetch into staging FIRST: a failed clone must leave the current install
|
|
78
|
+
# fully wired and intact. Only after success is the old shape unwired
|
|
79
|
+
# (the new version may have dropped a section dir), the store swapped,
|
|
80
|
+
# and the new shape wired.
|
|
81
|
+
staging="$IP_ROOT/.intelligence/.staging-$$"
|
|
82
|
+
rm -rf "$staging"
|
|
83
|
+
sha="$(fetch_package "$url" "$tag" "$path" "$staging")"
|
|
84
|
+
unwire_package_sources "$manifest" "$rel"
|
|
85
|
+
rm -rf "${IP_ROOT:?}/$rel"
|
|
86
|
+
mkdir -p "$(dirname "$IP_ROOT/$rel")"
|
|
87
|
+
mv "$staging" "$IP_ROOT/$rel"
|
|
88
|
+
wire_package_sources "$manifest" "$name" "$rel" "$IP_ROOT"
|
|
89
|
+
lock_upsert "$lock" "$name" "$requested" "$url" "$path" "$tag" "$sha"
|
|
90
|
+
echo " $name: ${current:-<none>} -> $tag"
|
|
91
|
+
moved=$((moved + 1))
|
|
92
|
+
done < <(qmap_keys "$manifest" "packages")
|
|
93
|
+
|
|
94
|
+
[ -n "$only" ] && [ "$found" -eq 0 ] && die "package '$only' is not in the manifest"
|
|
95
|
+
if [ "$preview" -eq 1 ]; then
|
|
96
|
+
echo "updates available: $moved package(s)"
|
|
97
|
+
else
|
|
98
|
+
echo "updated: $moved package(s)"
|
|
99
|
+
fi
|
|
100
|
+
|
|
101
|
+
if [ "$preview" -eq 0 ] && [ "$moved" -gt 0 ] && [ "$no_sync" -eq 0 ]; then
|
|
102
|
+
exec bash "$CLI_DIR/commands/sync.sh"
|
|
103
|
+
fi
|
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
#!/bin/bash
|
|
2
|
+
# Internal locked-store restore operation.
|
|
3
|
+
#
|
|
4
|
+
# Restore .intelligence/ from intelligence.lock — the fresh-clone command.
|
|
5
|
+
# --frozen additionally refuses any manifest/lock divergence (CI mode) and
|
|
6
|
+
# fails on a sha mismatch instead of re-resolving.
|
|
7
|
+
set -euo pipefail
|
|
8
|
+
source "$CLI_DIR/lib/cli-common.sh"
|
|
9
|
+
|
|
10
|
+
frozen=0 force=0 no_sync=0
|
|
11
|
+
while [ $# -gt 0 ]; do
|
|
12
|
+
case "$1" in
|
|
13
|
+
--frozen) frozen=1 ;;
|
|
14
|
+
--force) force=1 ;;
|
|
15
|
+
--no-sync) no_sync=1 ;;
|
|
16
|
+
*) die "unknown option '$1'" ;;
|
|
17
|
+
esac
|
|
18
|
+
shift || true
|
|
19
|
+
done
|
|
20
|
+
|
|
21
|
+
require_cli_project
|
|
22
|
+
manifest="$IP_ROOT/intelligence.yaml"
|
|
23
|
+
lock="$IP_ROOT/intelligence.lock"
|
|
24
|
+
|
|
25
|
+
# --frozen is the reproducibility contract: the lock must agree with the
|
|
26
|
+
# manifest on the package SET and on every requested-intent field before a
|
|
27
|
+
# single byte is restored. Anything less lets a manifest edit ride on a stale
|
|
28
|
+
# lock — the exact drift the flag exists to refuse.
|
|
29
|
+
if [ "$frozen" -eq 1 ]; then
|
|
30
|
+
while IFS= read -r name; do
|
|
31
|
+
[ -n "$name" ] || continue
|
|
32
|
+
[ -n "$(qmap_field "$lock" "packages" "$name" "url")" ] \
|
|
33
|
+
|| die "locked restore: $name is in the manifest but not in intelligence.lock"
|
|
34
|
+
m_ver="$(qmap_field "$manifest" "packages" "$name" "version")"
|
|
35
|
+
m_ref="$(qmap_field "$manifest" "packages" "$name" "ref")"
|
|
36
|
+
l_req="$(qmap_field "$lock" "packages" "$name" "requested")"
|
|
37
|
+
l_res="$(qmap_field "$lock" "packages" "$name" "resolved")"
|
|
38
|
+
[ -z "$m_ver" ] || [ "$m_ver" = "$l_req" ] \
|
|
39
|
+
|| die "locked restore: $name requests '$m_ver' but the lock recorded '$l_req' — reconcile the manifest and lock"
|
|
40
|
+
[ -z "$m_ref" ] || [ "$m_ref" = "$l_res" ] \
|
|
41
|
+
|| die "--frozen: $name pins ref '$m_ref' but the lock resolved '$l_res'"
|
|
42
|
+
done < <(qmap_keys "$manifest" "packages")
|
|
43
|
+
while IFS= read -r name; do
|
|
44
|
+
[ -n "$name" ] || continue
|
|
45
|
+
found=0
|
|
46
|
+
while IFS= read -r m; do
|
|
47
|
+
[ "$m" = "$name" ] && found=1
|
|
48
|
+
done < <(qmap_keys "$manifest" "packages")
|
|
49
|
+
[ "$found" -eq 1 ] || die "--frozen: $name is locked but absent from the manifest"
|
|
50
|
+
done < <(qmap_keys "$lock" "packages")
|
|
51
|
+
fi
|
|
52
|
+
|
|
53
|
+
# Manifest packages missing from the lock: resolve them now (npm install
|
|
54
|
+
# semantics). Under --frozen the drift gate above has already refused.
|
|
55
|
+
missing=""
|
|
56
|
+
while IFS= read -r name; do
|
|
57
|
+
[ -n "$name" ] || continue
|
|
58
|
+
assert_valid_pkg_name "$name"
|
|
59
|
+
if [ ! -f "$lock" ] || [ -z "$(qmap_field "$lock" "packages" "$name" "url")" ]; then
|
|
60
|
+
missing="$missing $name"
|
|
61
|
+
fi
|
|
62
|
+
done < <(qmap_keys "$manifest" "packages")
|
|
63
|
+
if [ -n "$missing" ]; then
|
|
64
|
+
for name in $missing; do
|
|
65
|
+
# The engine-content package needs no remote resolution: its pin IS
|
|
66
|
+
# the bundled engine version and it seeds from the bundle, offline.
|
|
67
|
+
if [ "$name" = "$SYNC_PKG_NAME" ]; then
|
|
68
|
+
sync_pkg_install "$IP_ROOT"
|
|
69
|
+
continue
|
|
70
|
+
fi
|
|
71
|
+
ref="$(qmap_field "$manifest" "packages" "$name" "ref")"
|
|
72
|
+
range="$(qmap_field "$manifest" "packages" "$name" "version")"
|
|
73
|
+
resolve_package_source "$manifest" "$name"
|
|
74
|
+
url="$RES_URL"; path="$RES_PATH"
|
|
75
|
+
[ -n "$url" ] || die "no trusted registry declares '$name' and it is absent from intelligence.lock"
|
|
76
|
+
resolved_tag=""
|
|
77
|
+
if [ -z "$ref" ]; then
|
|
78
|
+
picked="$(list_remote_versions "$url" | semver_pick_highest "$range")"
|
|
79
|
+
[ -n "$picked" ] || die "no tag of $url satisfies '${range:-*}' for $name"
|
|
80
|
+
read -r resolved_tag _ <<< "$(remote_tag_for_version "$url" "$picked")"
|
|
81
|
+
fi
|
|
82
|
+
rel=".intelligence/packages/$name"
|
|
83
|
+
sha="$(fetch_package "$url" "${ref:-$resolved_tag}" "$path" "$IP_ROOT/$rel")"
|
|
84
|
+
wire_package_sources "$manifest" "$name" "$rel" "$IP_ROOT"
|
|
85
|
+
lock_upsert "$lock" "$name" "$range" "$url" "$path" "${ref:-$resolved_tag}" "$sha"
|
|
86
|
+
echo "+ $name@${resolved_tag:-${ref:-HEAD}} (resolved)"
|
|
87
|
+
done
|
|
88
|
+
fi
|
|
89
|
+
|
|
90
|
+
# Restore every locked package that is not already in the store.
|
|
91
|
+
if [ -f "$lock" ]; then
|
|
92
|
+
while IFS="$LOCK_SEP" read -r name requested url path resolved sha; do
|
|
93
|
+
[ -n "$name" ] || continue
|
|
94
|
+
# The lock arrives with a cloned repo — its keys are untrusted input
|
|
95
|
+
# on their way into rm -rf paths, and its urls into git argv.
|
|
96
|
+
assert_valid_pkg_name "$name"
|
|
97
|
+
rel=".intelligence/packages/$name"
|
|
98
|
+
# --frozen never trusts what is already on disk: a pre-existing
|
|
99
|
+
# (possibly substituted) store dir would otherwise be synced with no
|
|
100
|
+
# sha verification at all.
|
|
101
|
+
if [ -d "$IP_ROOT/$rel" ] && [ "$force" -eq 0 ] && [ "$frozen" -eq 0 ]; then
|
|
102
|
+
continue
|
|
103
|
+
fi
|
|
104
|
+
# Fetch into staging and commit to the store only after the integrity
|
|
105
|
+
# verdict: content that violates the lock must never be left where a
|
|
106
|
+
# re-run (whose dir-exists check skips verification) would sync it.
|
|
107
|
+
staging="$IP_ROOT/.intelligence/.staging-$$"
|
|
108
|
+
rm -rf "$staging"
|
|
109
|
+
got="$(fetch_package "$url" "$resolved" "$path" "$staging")"
|
|
110
|
+
# An empty sha (bundle-seeded content on a dev build) means unknown,
|
|
111
|
+
# not moved — only two known-and-different shas are a finding.
|
|
112
|
+
if [ -n "$sha" ] && [ -n "$got" ] && [ "$got" != "$sha" ]; then
|
|
113
|
+
if [ "$frozen" -eq 1 ]; then
|
|
114
|
+
rm -rf "$staging"
|
|
115
|
+
die "$name: fetched $got but the lock pins $sha — the ref moved; refusing under --frozen"
|
|
116
|
+
fi
|
|
117
|
+
echo " WARN: $name resolved to $got, lock had $sha — updating the lock" >&2
|
|
118
|
+
lock_upsert "$lock" "$name" "$requested" "$url" "$path" "$resolved" "$got"
|
|
119
|
+
fi
|
|
120
|
+
rm -rf "${IP_ROOT:?}/$rel"
|
|
121
|
+
mkdir -p "$(dirname "$IP_ROOT/$rel")"
|
|
122
|
+
mv "$staging" "$IP_ROOT/$rel"
|
|
123
|
+
wire_package_sources "$manifest" "$name" "$rel" "$IP_ROOT"
|
|
124
|
+
echo "= $name@${resolved:-HEAD}"
|
|
125
|
+
done < <(lock_to_tsv "$lock")
|
|
126
|
+
fi
|
|
127
|
+
|
|
128
|
+
if [ "$no_sync" -eq 0 ]; then
|
|
129
|
+
exec bash "$CLI_DIR/commands/sync.sh"
|
|
130
|
+
fi
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
#!/bin/bash
|
|
2
|
+
# Internal adapter target-state operation.
|
|
3
|
+
#
|
|
4
|
+
# Change only the manifest. Syncing and deleting generated output are separate
|
|
5
|
+
# explicit actions: a generic target command cannot infer a custom adapter's
|
|
6
|
+
# ownership safely.
|
|
7
|
+
set -euo pipefail
|
|
8
|
+
source "$CLI_DIR/lib/cli-common.sh"
|
|
9
|
+
|
|
10
|
+
action="${1:-}"
|
|
11
|
+
name="${2:-}"
|
|
12
|
+
[ $# -le 2 ] || die "internal target state: expected <enable|disable> <name>"
|
|
13
|
+
case "$action" in enable|disable) ;; *) die "internal target state: expected <enable|disable> <name>" ;; esac
|
|
14
|
+
[ -n "$name" ] || die "internal target state: missing adapter name"
|
|
15
|
+
assert_valid_target_name "$name"
|
|
16
|
+
|
|
17
|
+
require_cli_project
|
|
18
|
+
manifest="$IP_ROOT/intelligence.yaml"
|
|
19
|
+
content_dir="$(manifest_intelligence_dir "$manifest")"
|
|
20
|
+
|
|
21
|
+
if [ "$action" = "enable" ]; then
|
|
22
|
+
project_adapter="$IP_ROOT/$content_dir/adapters/$name.sh"
|
|
23
|
+
bundled_adapter="$IS_ENGINE_DIR/adapters/$name.sh"
|
|
24
|
+
if [ ! -f "$project_adapter" ] && [ ! -f "$bundled_adapter" ]; then
|
|
25
|
+
die "adapter '$name' not found — create it first: intelligence adapter create $name"
|
|
26
|
+
fi
|
|
27
|
+
|
|
28
|
+
# These adapters intentionally omit always-on rules because their tools
|
|
29
|
+
# read AGENTS.md. Do not create a manifest that full sync must refuse.
|
|
30
|
+
case "$name" in
|
|
31
|
+
cursor|copilot|codex|pi|opencode)
|
|
32
|
+
if [ "$(is_target_enabled "$manifest" "agents")" != "1" ]; then
|
|
33
|
+
die "target '$name' requires target 'agents' — enable it first"
|
|
34
|
+
fi
|
|
35
|
+
;;
|
|
36
|
+
esac
|
|
37
|
+
|
|
38
|
+
output="$(get_target_output "$manifest" "$name")"
|
|
39
|
+
[ -n "$output" ] || output="$(default_target_output "$name")"
|
|
40
|
+
target_set_enabled "$manifest" "$name" true "$output"
|
|
41
|
+
echo "enabled: $name (output: $output)"
|
|
42
|
+
exit 0
|
|
43
|
+
fi
|
|
44
|
+
|
|
45
|
+
target_exists "$manifest" "$name" || die "target '$name' is not in the manifest"
|
|
46
|
+
if [ "$name" = "agents" ]; then
|
|
47
|
+
for dependent in cursor copilot codex pi opencode; do
|
|
48
|
+
if [ "$(is_target_enabled "$manifest" "$dependent")" = "1" ]; then
|
|
49
|
+
die "target 'agents' is required by enabled target '$dependent' — disable '$dependent' first"
|
|
50
|
+
fi
|
|
51
|
+
done
|
|
52
|
+
fi
|
|
53
|
+
output="$(get_target_output "$manifest" "$name")"
|
|
54
|
+
[ -n "$output" ] || output="$(default_target_output "$name")"
|
|
55
|
+
target_set_enabled "$manifest" "$name" false "$output"
|
|
56
|
+
echo "disabled: $name"
|
|
57
|
+
echo " generated output was kept; remove only paths owned by this adapter"
|
package/cli/lib/cli-common.sh
CHANGED
|
@@ -7,34 +7,74 @@
|
|
|
7
7
|
# CLI-owned parsing lives in lib/manifest.sh (quoted-key `packages:` /
|
|
8
8
|
# `registries:` blocks, which the engine deliberately never reads).
|
|
9
9
|
|
|
10
|
-
# Engine libraries (readers, is_status, IS_RC_*, engine_version).
|
|
11
|
-
#
|
|
12
|
-
|
|
13
|
-
source "$IS_ENGINE_DIR/
|
|
14
|
-
source "$IS_ENGINE_DIR/scripts/lib/layout.sh"
|
|
15
|
-
source "$IS_ENGINE_DIR/scripts/lib/migrations.sh"
|
|
10
|
+
# Engine libraries (readers, is_status, IS_RC_*, engine_version). CLI_DIR /
|
|
11
|
+
# IS_ENGINE_DIR come exported from the dispatcher.
|
|
12
|
+
source "$IS_ENGINE_DIR/lib/common.sh"
|
|
13
|
+
source "$IS_ENGINE_DIR/lib/contract.sh"
|
|
16
14
|
|
|
17
15
|
die() {
|
|
18
16
|
echo "ERROR: $*" >&2
|
|
19
17
|
exit 1
|
|
20
18
|
}
|
|
21
19
|
|
|
20
|
+
# --- Untrusted-input guards ----------------------------------------------
|
|
21
|
+
# Manifest, lock and registry indexes arrive with a cloned repo: every value
|
|
22
|
+
# they carry is attacker-adjacent. URLs and refs reach git argv (a leading
|
|
23
|
+
# `-` would be an option — `--upload-pack=<cmd>` is code execution), names
|
|
24
|
+
# become store paths fed to rm -rf. Guard at the choke points, loudly.
|
|
25
|
+
|
|
26
|
+
# assert_safe_source_url <url> — scheme allowlist (the engine's own list) or
|
|
27
|
+
# scp-like user@host:path; never option-shaped, never quote-bearing.
|
|
28
|
+
assert_safe_source_url() {
|
|
29
|
+
local url="$1"
|
|
30
|
+
case "$url" in
|
|
31
|
+
""|-*) die "unsafe source url '$url' — option-shaped or empty" ;;
|
|
32
|
+
*[\"\'\ ]*) die "unsafe source url '$url' — quotes or spaces" ;;
|
|
33
|
+
https://*|http://*|ssh://*|git://*|file://*) ;;
|
|
34
|
+
*@*:*) ;;
|
|
35
|
+
*) die "unsafe source url '$url' — allowed: https, http, ssh, git, file, or user@host:path" ;;
|
|
36
|
+
esac
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
# assert_safe_ref <ref-or-empty> — tags/branches/SHAs; never option-shaped.
|
|
40
|
+
assert_safe_ref() {
|
|
41
|
+
local ref="$1"
|
|
42
|
+
[ -z "$ref" ] && return 0
|
|
43
|
+
case "$ref" in
|
|
44
|
+
-*) die "unsafe git ref '$ref' — option-shaped" ;;
|
|
45
|
+
*[\"\'\ \\]*) die "unsafe git ref '$ref'" ;;
|
|
46
|
+
esac
|
|
47
|
+
}
|
|
48
|
+
|
|
22
49
|
source "$CLI_DIR/lib/manifest.sh"
|
|
23
50
|
source "$CLI_DIR/lib/semver.sh"
|
|
24
51
|
source "$CLI_DIR/lib/registry.sh"
|
|
25
52
|
source "$CLI_DIR/lib/lockfile.sh"
|
|
26
53
|
|
|
27
|
-
#
|
|
28
|
-
#
|
|
29
|
-
# the CLI
|
|
30
|
-
|
|
54
|
+
# The engine-content package: OPTIONAL but auto-selected at init. Package by
|
|
55
|
+
# UX (manifest entry, lockfile row, list/search/remove), bundle by mechanics —
|
|
56
|
+
# at the version the CLI ships, it materializes from the npm bundle without
|
|
57
|
+
# network; only a cross-version acquisition reaches git. The pin is held
|
|
58
|
+
# exactly at the bundled engine version and moved by lifecycle alignment.
|
|
59
|
+
#
|
|
60
|
+
# Its identity is DATA shipped with the distribution (cli/engine-package.yaml),
|
|
61
|
+
# never a name compiled into cli code — a fork edits the file.
|
|
62
|
+
_EPKG="$CLI_DIR/engine-package.yaml"
|
|
63
|
+
SYNC_PKG_NAME="$(top_scalar "$_EPKG" "name")"
|
|
64
|
+
SYNC_PKG_URL="$(top_scalar "$_EPKG" "url")"
|
|
65
|
+
SYNC_PKG_PATH="$(top_scalar "$_EPKG" "path")"
|
|
66
|
+
# Read by commands (init seeds it) — per-file shellcheck cannot see that.
|
|
67
|
+
# shellcheck disable=SC2034
|
|
68
|
+
DEFAULT_REGISTRY_URL="$(top_scalar "$_EPKG" "default_registry")"
|
|
69
|
+
[ -n "$SYNC_PKG_NAME" ] || die "corrupt CLI installation: $_EPKG is missing or has no 'name'"
|
|
70
|
+
SYNC_PKG_STORE=".intelligence/packages/$SYNC_PKG_NAME"
|
|
31
71
|
|
|
32
72
|
# --- Project detection ---------------------------------------------------
|
|
33
|
-
# Sets: IP_MODE (
|
|
73
|
+
# Sets: IP_MODE (cli|legacy|none), IP_ROOT, IP_UMBRELLA, IP_MODULE_DIR.
|
|
34
74
|
# These ARE this lib's public API — every command reads them after calling
|
|
35
75
|
# detect_project, which per-file shellcheck cannot see.
|
|
36
76
|
# shellcheck disable=SC2034
|
|
37
|
-
#
|
|
77
|
+
# The CLI product wins: the nearest ancestor holding intelligence.yaml. Legacy is a git
|
|
38
78
|
# repo holding an umbrella (a dir with config.yaml) whose module is identified
|
|
39
79
|
# by role — scripts/sync.sh + scripts/VERSION — never by name.
|
|
40
80
|
detect_project() {
|
|
@@ -42,7 +82,7 @@ detect_project() {
|
|
|
42
82
|
local dir="$PWD"
|
|
43
83
|
while :; do
|
|
44
84
|
if [ -f "$dir/intelligence.yaml" ]; then
|
|
45
|
-
IP_MODE="
|
|
85
|
+
IP_MODE="cli"
|
|
46
86
|
IP_ROOT="$(cd "$dir" && pwd)"
|
|
47
87
|
return 0
|
|
48
88
|
fi
|
|
@@ -69,11 +109,11 @@ detect_project() {
|
|
|
69
109
|
return 0
|
|
70
110
|
}
|
|
71
111
|
|
|
72
|
-
|
|
112
|
+
require_cli_project() {
|
|
73
113
|
detect_project
|
|
74
114
|
case "$IP_MODE" in
|
|
75
|
-
|
|
76
|
-
legacy) die "this is a
|
|
115
|
+
cli) ;;
|
|
116
|
+
legacy) die "this is a legacy Intelligence Sync project — run 'intelligence init' to convert it, or keep using its own flow" ;;
|
|
77
117
|
*) die "no intelligence project found here — run 'intelligence init'" ;;
|
|
78
118
|
esac
|
|
79
119
|
}
|
|
@@ -86,64 +126,174 @@ manifest_intelligence_dir() {
|
|
|
86
126
|
printf '%s' "${v:-intelligence}"
|
|
87
127
|
}
|
|
88
128
|
|
|
129
|
+
default_target_output() {
|
|
130
|
+
case "$1" in
|
|
131
|
+
agents) printf '%s' "AGENTS.md" ;;
|
|
132
|
+
copilot) printf '%s' ".github" ;;
|
|
133
|
+
*) printf '.%s' "$1" ;;
|
|
134
|
+
esac
|
|
135
|
+
}
|
|
136
|
+
|
|
89
137
|
bundled_engine_version() {
|
|
90
|
-
tr -d ' \t\r\n' < "$IS_ENGINE_DIR/
|
|
91
|
-
}
|
|
92
|
-
|
|
93
|
-
# --- Engine content staging ----------------------------------------------
|
|
94
|
-
# The v2 project vendors no engine code; the engine's own rules / agents /
|
|
95
|
-
# meta-skills / docs are staged into the package store so sources can reach
|
|
96
|
-
# them as ordinary repo-relative dirs. Re-staged whenever the bundled engine
|
|
97
|
-
# version differs from the `.version` stamp.
|
|
98
|
-
# stage_engine_content <dest-dir> — unconditional copy of the engine's own
|
|
99
|
-
# content into <dest-dir> plus the `.version` stamp.
|
|
100
|
-
stage_engine_content() {
|
|
101
|
-
local store="$1"
|
|
102
|
-
rm -rf "$store"
|
|
103
|
-
mkdir -p "$store"
|
|
104
|
-
local d s name skip
|
|
105
|
-
for d in rules agents docs; do
|
|
106
|
-
[ -d "$IS_ENGINE_DIR/$d" ] && cp -R "$IS_ENGINE_DIR/$d" "$store/$d"
|
|
107
|
-
done
|
|
108
|
-
if [ -d "$IS_ENGINE_DIR/skills" ]; then
|
|
109
|
-
mkdir -p "$store/skills"
|
|
110
|
-
for s in "$IS_ENGINE_DIR"/skills/*/; do
|
|
111
|
-
[ -d "$s" ] || continue
|
|
112
|
-
name="$(basename "$s")"
|
|
113
|
-
skip=0
|
|
114
|
-
for d in $CLI_OBSOLETE_SKILLS; do
|
|
115
|
-
[ "$name" = "$d" ] && skip=1
|
|
116
|
-
done
|
|
117
|
-
[ "$skip" = "1" ] && continue
|
|
118
|
-
cp -R "$s" "$store/skills/$name"
|
|
119
|
-
done
|
|
120
|
-
fi
|
|
121
|
-
bundled_engine_version > "$store/.version"
|
|
138
|
+
tr -d ' \t\r\n' < "$IS_ENGINE_DIR/VERSION"
|
|
122
139
|
}
|
|
123
140
|
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
141
|
+
# --- The sync package's manifest/lock plumbing ---------------------------
|
|
142
|
+
# sync_pkg_entry <manifest> — write/refresh only the requested exact pin.
|
|
143
|
+
# The built-in source lives in engine-package.yaml and resolved source state
|
|
144
|
+
# belongs exclusively to intelligence.lock.
|
|
145
|
+
sync_pkg_entry() {
|
|
146
|
+
local manifest="$1"
|
|
147
|
+
qmap_set "$manifest" "packages" "$SYNC_PKG_NAME" "version" "$(bundled_engine_version)"
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
# sync_pkg_install <root> — materialize the package into the store and lock
|
|
151
|
+
# it (offline at the bundled version — fetch_package's bundle-seed guard).
|
|
152
|
+
sync_pkg_install() {
|
|
153
|
+
local root="$1" ver sha
|
|
154
|
+
ver="$(bundled_engine_version)"
|
|
155
|
+
sha="$(fetch_package "$SYNC_PKG_URL" "v$ver" "$SYNC_PKG_PATH" "$root/$SYNC_PKG_STORE")"
|
|
156
|
+
wire_package_sources "$root/intelligence.yaml" "$SYNC_PKG_NAME" "$SYNC_PKG_STORE" "$root"
|
|
157
|
+
lock_upsert "$root/intelligence.lock" "$SYNC_PKG_NAME" "$ver" "$SYNC_PKG_URL" "$SYNC_PKG_PATH" "v$ver" "$sha"
|
|
158
|
+
echo " engine content installed: $SYNC_PKG_STORE (v$ver)"
|
|
134
159
|
}
|
|
135
160
|
|
|
136
161
|
# --- The engine env contract ---------------------------------------------
|
|
137
162
|
# Everything the IS_CLI mode of sync.sh needs, derived from the manifest.
|
|
138
163
|
export_engine_env() {
|
|
139
164
|
local root="$1"
|
|
140
|
-
local
|
|
141
|
-
|
|
165
|
+
local content_rel
|
|
166
|
+
content_rel="$(manifest_intelligence_dir "$root/intelligence.yaml")"
|
|
142
167
|
export IS_CLI=1
|
|
143
168
|
export CONFIG_FILE="$root/intelligence.yaml"
|
|
144
169
|
export REPO_ROOT="$root"
|
|
145
|
-
export
|
|
146
|
-
export IS_MODULE_REL="
|
|
170
|
+
export IS_CONTENT_REL="$content_rel"
|
|
171
|
+
export IS_MODULE_REL="$SYNC_PKG_STORE"
|
|
147
172
|
export IS_SYNC_CMD="intelligence sync"
|
|
148
|
-
export
|
|
173
|
+
export IS_MANIFEST_NAME="intelligence.yaml"
|
|
174
|
+
export IS_PROTECTED_DIRS="$content_rel:.intelligence"
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
# --- Project lifecycle preflight -----------------------------------------
|
|
178
|
+
# Public commands are intentionally few. They share this state gate so a CLI
|
|
179
|
+
# installed at a newer engine version brings the current Intelligence project forward
|
|
180
|
+
# before a mutating operation. The npm install itself cannot do that: it runs
|
|
181
|
+
# outside any project and does not know which repositories the user owns.
|
|
182
|
+
|
|
183
|
+
is_ci_environment() {
|
|
184
|
+
case "${CI:-}" in
|
|
185
|
+
1|true|TRUE|True|yes|YES|Yes|on|ON|On) return 0 ;;
|
|
186
|
+
*) return 1 ;;
|
|
187
|
+
esac
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
# Validate a repository-relative project content directory, including every
|
|
191
|
+
# existing symlinked path component. Call before writing a new manifest.
|
|
192
|
+
assert_safe_content_dir() {
|
|
193
|
+
local root="$1" content_dir="$2" repo_phys probe old_ifs part probe_phys
|
|
194
|
+
local -a parts
|
|
195
|
+
case "$content_dir" in
|
|
196
|
+
""|/*|.|..|../*|*/../*|*/..|.git|.git/*|.intelligence|.intelligence/*|*\\*|[A-Za-z]:*)
|
|
197
|
+
die "unsafe content directory '$content_dir'"
|
|
198
|
+
;;
|
|
199
|
+
esac
|
|
200
|
+
repo_phys="$(cd "$root" && pwd -P)"
|
|
201
|
+
probe="$root"
|
|
202
|
+
old_ifs="$IFS"; IFS='/'; read -r -a parts <<< "$content_dir"; IFS="$old_ifs"
|
|
203
|
+
for part in "${parts[@]}"; do
|
|
204
|
+
[ -n "$part" ] || continue
|
|
205
|
+
probe="$probe/$part"
|
|
206
|
+
if [ -e "$probe" ] || [ -L "$probe" ]; then
|
|
207
|
+
probe_phys="$(cd "$probe" 2>/dev/null && pwd -P)" \
|
|
208
|
+
|| die "cannot resolve project content path '$content_dir'"
|
|
209
|
+
case "$probe_phys" in
|
|
210
|
+
"$repo_phys"|"$repo_phys"/*) ;;
|
|
211
|
+
*) die "project content directory resolves outside the repository: '$content_dir'" ;;
|
|
212
|
+
esac
|
|
213
|
+
fi
|
|
214
|
+
done
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
project_needs_upgrade() {
|
|
218
|
+
local root="$1" manifest="$1/intelligence.yaml" stamp eng pinned locked name
|
|
219
|
+
[ -f "$manifest" ] || return 1
|
|
220
|
+
stamp="$(read_schema_version "$manifest")"
|
|
221
|
+
eng="$(bundled_engine_version)"
|
|
222
|
+
[ -z "$stamp" ] && return 0
|
|
223
|
+
[ -n "$(top_scalar "$manifest" "sync_version")" ] && return 0
|
|
224
|
+
_ver_gt "$eng" "$stamp" && return 0
|
|
225
|
+
[ -d "$root/.intelligence/engine" ] && return 0
|
|
226
|
+
|
|
227
|
+
while IFS= read -r name; do
|
|
228
|
+
[ -n "$name" ] || continue
|
|
229
|
+
# Early RC manifests mixed requested intent with resolved source
|
|
230
|
+
# details. Source URL/path now live only in the required lockfile.
|
|
231
|
+
[ -n "$(qmap_field "$manifest" "packages" "$name" "url")" ] && return 0
|
|
232
|
+
[ -n "$(qmap_field "$manifest" "packages" "$name" "path")" ] && return 0
|
|
233
|
+
if [ "$name" = "$SYNC_PKG_NAME" ]; then
|
|
234
|
+
pinned="$(qmap_field "$manifest" "packages" "$name" "version")"
|
|
235
|
+
locked="$(qmap_field "$root/intelligence.lock" "packages" "$name" "resolved")"
|
|
236
|
+
[ "$pinned" = "$eng" ] || return 0
|
|
237
|
+
if [ -f "$root/intelligence.lock" ]; then
|
|
238
|
+
[ -n "$locked" ] || return 0
|
|
239
|
+
[ "${locked#v}" = "$eng" ] || return 0
|
|
240
|
+
fi
|
|
241
|
+
fi
|
|
242
|
+
done < <(qmap_keys "$manifest" "packages")
|
|
243
|
+
return 1
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
# A manifest with packages but no lock has no trustworthy resolved state from
|
|
247
|
+
# which lifecycle alignment can proceed. Never manufacture a partial lock.
|
|
248
|
+
project_has_packages() {
|
|
249
|
+
local root="$1" name found=1
|
|
250
|
+
while IFS= read -r name; do
|
|
251
|
+
[ -n "$name" ] || continue
|
|
252
|
+
assert_valid_pkg_name "$name"
|
|
253
|
+
found=0
|
|
254
|
+
done < <(qmap_keys "$root/intelligence.yaml" "packages")
|
|
255
|
+
return "$found"
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
ensure_project_current() {
|
|
259
|
+
local root="$1" explicit="${2:-}" manifest="$1/intelligence.yaml" stamp eng
|
|
260
|
+
check_version_compat "$manifest" || return $?
|
|
261
|
+
if project_has_packages "$root" && [ ! -f "$root/intelligence.lock" ]; then
|
|
262
|
+
die "manifest declares packages but intelligence.lock is absent — restore the committed lock before running project lifecycle commands"
|
|
263
|
+
fi
|
|
264
|
+
project_needs_upgrade "$root" || return 0
|
|
265
|
+
stamp="$(read_schema_version "$manifest")"
|
|
266
|
+
eng="$(bundled_engine_version)"
|
|
267
|
+
if is_ci_environment && [ "$explicit" != "--explicit" ]; then
|
|
268
|
+
die "project lifecycle requires alignment (stamp ${stamp:-unstamped}, engine $eng) — run 'intelligence init --apply' locally, review and commit the diff"
|
|
269
|
+
fi
|
|
270
|
+
echo " project alignment: stamp ${stamp:-unstamped}, engine $eng"
|
|
271
|
+
bash "$CLI_DIR/internal/align-project.sh" --no-sync
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
project_store_missing() {
|
|
275
|
+
local root="$1" manifest="$1/intelligence.yaml" name src
|
|
276
|
+
while IFS= read -r name; do
|
|
277
|
+
[ -n "$name" ] || continue
|
|
278
|
+
assert_valid_pkg_name "$name"
|
|
279
|
+
[ -d "$root/.intelligence/packages/$name" ] || return 0
|
|
280
|
+
done < <(qmap_keys "$manifest" "packages")
|
|
281
|
+
for section in rules agents skills; do
|
|
282
|
+
while IFS= read -r src; do
|
|
283
|
+
case "$src" in
|
|
284
|
+
.intelligence/packages/*)
|
|
285
|
+
[ -d "$root/$src" ] || return 0
|
|
286
|
+
;;
|
|
287
|
+
esac
|
|
288
|
+
done < <(read_yaml_list "$manifest" "$section")
|
|
289
|
+
done
|
|
290
|
+
return 1
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
restore_project_store_if_missing() {
|
|
294
|
+
local root="$1"
|
|
295
|
+
project_store_missing "$root" || return 0
|
|
296
|
+
[ -f "$root/intelligence.lock" ] || die "package store is missing and intelligence.lock is absent — run 'intelligence init'"
|
|
297
|
+
echo " restoring package store from intelligence.lock"
|
|
298
|
+
bash "$CLI_DIR/internal/restore.sh" --frozen --no-sync
|
|
149
299
|
}
|