@ainova-systems/intelligence 0.11.0-rc.6 → 0.11.0-rc.7

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (50) hide show
  1. package/README.md +2 -2
  2. package/cli/commands/add.sh +8 -7
  3. package/cli/commands/install.sh +8 -1
  4. package/cli/commands/migrate.sh +49 -19
  5. package/cli/commands/registry.sh +1 -0
  6. package/cli/commands/sync.sh +1 -1
  7. package/cli/commands/update.sh +12 -3
  8. package/cli/engine-package.yaml +2 -2
  9. package/cli/intelligence +27 -15
  10. package/cli/lib/cli-common.sh +34 -7
  11. package/cli/lib/lockfile.sh +14 -8
  12. package/cli/lib/manifest.sh +20 -1
  13. package/cli/lib/registry.sh +25 -11
  14. package/cli/lib/semver.sh +4 -2
  15. package/engine/ENGINE_SHA +1 -0
  16. package/engine/{scripts/adapters → adapters}/agents.sh +10 -15
  17. package/engine/{scripts/lib → lib}/common.sh +15 -519
  18. package/engine/lib/contract.sh +120 -0
  19. package/engine/sync.sh +233 -0
  20. package/package.json +6 -5
  21. package/engine/INIT.md +0 -500
  22. package/engine/docs/CLI.md +0 -90
  23. package/engine/scripts/ENGINE_SHA +0 -1
  24. package/engine/scripts/lib/layout.sh +0 -51
  25. package/engine/scripts/lib/migrations.sh +0 -708
  26. package/engine/scripts/sync.sh +0 -311
  27. package/engine/scripts/update.sh +0 -237
  28. /package/engine/{scripts/VERSION → VERSION} +0 -0
  29. /package/engine/{scripts/adapters → adapters}/_template.sh +0 -0
  30. /package/engine/{scripts/adapters → adapters}/claude.sh +0 -0
  31. /package/engine/{scripts/adapters → adapters}/codex.sh +0 -0
  32. /package/engine/{scripts/adapters → adapters}/copilot.sh +0 -0
  33. /package/engine/{scripts/adapters → adapters}/cursor.sh +0 -0
  34. /package/engine/{scripts/adapters → adapters}/opencode.sh +0 -0
  35. /package/engine/{scripts/adapters → adapters}/pi.sh +0 -0
  36. /package/{engine → packages/sync}/agents/intelligence-architect.md +0 -0
  37. /package/{engine → packages/sync}/agents/intelligence-operator.md +0 -0
  38. /package/{engine → packages/sync}/docs/ADAPTERS.md +0 -0
  39. /package/{engine → packages/sync}/docs/CONVENTIONS.md +0 -0
  40. /package/{engine → packages/sync}/rules/intelligence-authoring.md +0 -0
  41. /package/{engine → packages/sync}/skills/intelligence-add-agent/SKILL.md +0 -0
  42. /package/{engine → packages/sync}/skills/intelligence-add-rule/SKILL.md +0 -0
  43. /package/{engine → packages/sync}/skills/intelligence-add-skill/SKILL.md +0 -0
  44. /package/{engine → packages/sync}/skills/intelligence-extract-skill/SKILL.md +0 -0
  45. /package/{engine → packages/sync}/skills/intelligence-install-adapter/SKILL.md +0 -0
  46. /package/{engine → packages/sync}/skills/intelligence-learn-from-context/SKILL.md +0 -0
  47. /package/{engine → packages/sync}/skills/intelligence-review-skills/SKILL.md +0 -0
  48. /package/{engine → packages/sync}/skills/intelligence-sync/SKILL.md +0 -0
  49. /package/{engine → packages/sync}/skills/intelligence-uninstall-adapter/SKILL.md +0 -0
  50. /package/{engine → packages/sync}/skills/intelligence-update/SKILL.md +0 -0
@@ -1,708 +0,0 @@
1
- #!/bin/bash
2
- # shellcheck disable=SC2034 # IS_RC_*/IS_MIGRATED/IS_VERSION_KEY are the public bash↔skill contract, consumed by the scripts that source this lib
3
- # intelligence-sync: versioned migrations — breaking-change update architecture
4
- # Source this file — never execute directly. Requires layout.sh already
5
- # sourced (uses no globals from it directly; callers pass paths explicitly).
6
- #
7
- # THE MODEL (how breaking changes are shipped & absorbed):
8
- # * The project carries a version stamp (.intelligence-sync-version); the
9
- # engine carries scripts/VERSION. The gap stamped → engine IS the set of
10
- # breaking changes to apply.
11
- # * Each breaking structural change ships as ONE registered migrate_to_<ver>
12
- # in MIGRATIONS (ordered, ascending). The dispatcher applies only the
13
- # pending ones (target version > stamped), in order, so a project several
14
- # versions behind is walked forward step by step, stamping after each.
15
- # * Every migrate_to_<ver> obeys the contract: precondition → stage → verify
16
- # postcondition (sentinel) → commit → cleanup; idempotent; fail-closed
17
- # (never destroy prior state before the replacement is verified).
18
- # * A stale engine refuses a project stamped newer than it knows
19
- # (ahead-of-engine). sync.sh refuses to sync across an un-applied gap
20
- # (needs-update). The intelligence-update SKILL is the brain: it reads the
21
- # CHANGELOG across the gap, surfaces breaking items, runs this chain, and
22
- # verifies after.
23
- #
24
- # Naming carries the target version (bash forbids dots → underscores):
25
- # migrate_to_0_3_1 flat <umbrella>/scripts → modular <umbrella>/sync/
26
- # Future breaking changes: append a new suffix to MIGRATIONS and add the
27
- # matching migrate_to_<ver> — nothing here is rewritten or reordered.
28
-
29
- # Ordered (ascending) list of migration target versions. Append only.
30
- MIGRATIONS=( "0_3_1" "0_7_0" "0_10_0" )
31
-
32
- # The applied-schema version is a managed key in config.yaml — NOT a dotfile,
33
- # NOT scripts/VERSION. config.yaml is what most future breaking changes will
34
- # reshape, so the schema version lives with what it versions.
35
- #
36
- # INVARIANT: this key is a PERMANENT, format-stable, top-level scalar contract.
37
- # Migrations may restructure anything else in config.yaml, but never the name,
38
- # location, or shape of this key — so any engine (however old/new) can always
39
- # read "what schema is this?" before parsing the rest. The bootstrap/INIT flow
40
- # must emit it for fresh projects and preserve it on re-bootstrap.
41
- IS_VERSION_KEY="sync_version"
42
-
43
- # "0_3_1" → "0.3.1"
44
- _mig_ver_dotted() { printf '%s' "$1" | tr '_' '.'; }
45
-
46
- # read_engine_stamp <config_file> → applied version, or "" if absent.
47
- read_engine_stamp() {
48
- local cf="$1"
49
- [ -f "$cf" ] || return 0
50
- awk -v k="$IS_VERSION_KEY" '
51
- { sub(/\r$/, "") }
52
- $0 ~ "^" k ":" {
53
- v = $0; sub(/^[^:]*:[[:space:]]*/, "", v)
54
- gsub(/^["\047]|["\047][[:space:]]*$/, "", v)
55
- sub(/[[:space:]]+$/, "", v)
56
- print v; exit
57
- }
58
- ' "$cf"
59
- }
60
-
61
- # stamp_version <config_file> <version> — idempotent, transactional upsert of
62
- # the contract key (replace in place if present, else append at top level).
63
- # No-op if config.yaml does not exist yet (pre-bootstrap).
64
- stamp_version() {
65
- local cf="$1" ver="$2"
66
- [ -f "$cf" ] || return 0
67
- local tmp="$cf.ver.tmp"
68
- awk -v k="$IS_VERSION_KEY" -v val="$ver" '
69
- { sub(/\r$/, "") }
70
- $0 ~ "^" k ":" { print k ": \"" val "\""; found=1; next }
71
- { print }
72
- END { if (!found) print k ": \"" val "\"" }
73
- ' "$cf" > "$tmp" && mv "$tmp" "$cf"
74
- }
75
-
76
- # --- bash ↔ skill status contract -------------------------------------------
77
- # Bash is the deterministic, fail-closed core: it never guesses. Any state it
78
- # cannot resolve safely is reported as a machine-readable status line on
79
- # stdout plus a stable exit code, and the intelligence-update SKILL (the
80
- # intelligent layer) decides what to do. Codes are part of the public
81
- # contract — do not renumber.
82
- IS_RC_OK=0 # success (synced / migrated / nothing to do)
83
- IS_RC_ERROR=1 # generic error
84
- IS_RC_CONFIG_MISSING=2 # no config.yaml found
85
- IS_RC_AMBIGUOUS=3 # conflicting state; skill/human-only — bash never emits this itself, it is reserved for the intelligence-update skill to report
86
- IS_RC_AHEAD=4 # project stamped newer than this engine understands
87
- IS_RC_ABORTED_INCOMPLETE=5 # staged module incomplete; legacy left intact
88
- IS_RC_NEEDS_UPDATE=6 # pending breaking changes (stamp < engine) — run the update flow first
89
-
90
- # is_status <code-name> [detail] — emit one parseable line for the skill.
91
- is_status() {
92
- local code="$1" detail="${2:-}"
93
- if [ -n "$detail" ]; then
94
- echo "IS_STATUS=$code IS_DETAIL=$detail"
95
- else
96
- echo "IS_STATUS=$code"
97
- fi
98
- }
99
-
100
- # Engine version = scripts/VERSION next to this lib (BASH_SOURCE works when
101
- # sourced). Empty if unreadable — callers treat empty as "no guard".
102
- engine_version() {
103
- local vf
104
- vf="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." 2>/dev/null && pwd)/VERSION"
105
- [ -f "$vf" ] && tr -d ' \t\r\n' < "$vf"
106
- }
107
-
108
- # _ver_gt A B → true if semver A is strictly greater than B (numeric x.y.z;
109
- # any non-numeric suffix on a field is ignored). Missing fields = 0.
110
- # Pre-release/build metadata ordering is intentionally NOT handled — the
111
- # stamp only ever stores plain x.y.z, so this is sufficient.
112
- _ver_gt() {
113
- local a="$1" b="$2" i ai bi
114
- local -a A B
115
- IFS=. read -r -a A <<< "$a"
116
- IFS=. read -r -a B <<< "$b"
117
- for i in 0 1 2; do
118
- ai=$(printf '%s' "${A[$i]:-0}" | tr -cd '0-9'); ai=${ai:-0}
119
- bi=$(printf '%s' "${B[$i]:-0}" | tr -cd '0-9'); bi=${bi:-0}
120
- if [ "$((10#$ai))" -gt "$((10#$bi))" ]; then return 0; fi
121
- if [ "$((10#$ai))" -lt "$((10#$bi))" ]; then return 1; fi
122
- done
123
- return 1
124
- }
125
-
126
- # check_version_compat <config_file> — refuse to operate on a project whose
127
- # config schema is stamped newer than this engine knows (a stale engine must
128
- # never rewrite/sync a newer schema). Emits status + returns IS_RC_AHEAD on
129
- # conflict, else 0.
130
- check_version_compat() {
131
- local cf="$1" stamp eng
132
- stamp="$(read_engine_stamp "$cf")"
133
- [ -n "$stamp" ] || return 0
134
- eng="$(engine_version)"
135
- [ -n "$eng" ] || return 0
136
- if _ver_gt "$stamp" "$eng"; then
137
- is_status ahead-of-engine "stamp=$stamp engine=$eng"
138
- echo " ERROR: project stamped $stamp but this engine is $eng — refusing." >&2
139
- echo " Update the engine first (the intelligence-update skill handles this)." >&2
140
- return "$IS_RC_AHEAD"
141
- fi
142
- return 0
143
- }
144
-
145
- # Set by migrate_to_* when it actually performed work this run, so the caller
146
- # can report `migrated` vs `ok`.
147
- IS_MIGRATED=0
148
-
149
- # Idempotent directory replace (rsync if present, else rm+cp). Returns non-zero
150
- # if any step fails: a migration runs in an `||` context, so `set -e` is off and
151
- # an unchecked copy failure (permissions, full disk) would otherwise sail on to
152
- # the cleanup and delete the legacy files it never actually copied.
153
- _mig_copy_dir() {
154
- local src="$1" dst="$2"
155
- [ -d "$src" ] || return 0
156
- if command -v rsync >/dev/null 2>&1; then
157
- mkdir -p "$dst" || return 1
158
- rsync -a --delete "$src/" "$dst/" || return 1
159
- else
160
- rm -rf "$dst" || return 1
161
- mkdir -p "$(dirname "$dst")" || return 1
162
- cp -r "$src" "$dst" || return 1
163
- fi
164
- return 0
165
- }
166
-
167
- _mig_copy_file() {
168
- local src="$1" dst="$2"
169
- [ -f "$src" ] || return 0
170
- mkdir -p "$(dirname "$dst")" || return 1
171
- cp "$src" "$dst" || return 1
172
- return 0
173
- }
174
-
175
- # Repo-root-relative path of the umbrella — the prefix every `sources:` entry is
176
- # resolved against (resolve_source_dir does "$repo_root/$entry"). `basename` is
177
- # only correct when the umbrella sits directly at the repo root; a nested one
178
- # (`platform/intelligence/`) would yield `intelligence/...` and register a source
179
- # that resolves nowhere. Ask git for the real prefix, and fall back to basename
180
- # when there is no git (a tarball checkout), which is the flat case anyway.
181
- _mig_umbrella_rel() {
182
- local umbrella="$1" prefix=""
183
- if command -v git >/dev/null 2>&1; then
184
- prefix="$(git -C "$umbrella" rev-parse --show-prefix 2>/dev/null || true)"
185
- fi
186
- prefix="${prefix%/}"
187
- if [ -n "$prefix" ]; then
188
- printf '%s' "$prefix"
189
- else
190
- printf '%s' "$(basename "$umbrella")"
191
- fi
192
- }
193
-
194
- # True (0) if <entry> is already listed anywhere in config.yaml (quoted or bare).
195
- _mig_has_source() {
196
- local config="$1" entry="$2"
197
- [ -f "$config" ] || return 1
198
- grep -Fq -- "\"$entry\"" "$config" || grep -Fq -- "- $entry" "$config"
199
- }
200
-
201
- # Idempotently add one entry to `sources.<section>` in config.yaml. No backup —
202
- # the edit is a single additive list item. Name-agnostic: the caller passes the
203
- # already-resolved "<base>/<module>/<dir>". If the section does not exist under
204
- # `sources:` yet, it is created with the entry as its only item.
205
- # Usage: _mig_add_source <config.yaml> <rules|agents|skills> <entry>
206
- _mig_add_source() {
207
- local config="$1" section="$2" entry="$3"
208
- if [ ! -f "$config" ]; then
209
- echo " [migrate] no config.yaml at $config — add this under sources.$section manually:" >&2
210
- echo " - \"$entry\"" >&2
211
- return 0
212
- fi
213
- _mig_has_source "$config" "$entry" && return 0
214
-
215
- local tmp="$config.mig.tmp"
216
- awk -v section="$section" -v entry="$entry" '
217
- function emit() { print " - \"" entry "\""; inserted = 1 }
218
- function close_here() {
219
- if (in_sec && !inserted) { emit(); in_sec = 0 }
220
- }
221
- { sub(/\r$/, "") }
222
- # Top-level key: ends the sources block (and any open section in it).
223
- /^[A-Za-z]/ {
224
- close_here()
225
- # `sources:` existed but never declared this section — declare it.
226
- if (in_src && !inserted) { print " " section ":"; emit() }
227
- in_sec = 0
228
- in_src = ($0 ~ /^sources:[[:space:]]*$/) ? 1 : 0
229
- print; next
230
- }
231
- in_src && $0 ~ "^ " section ":[[:space:]]*$" { print; in_sec = 1; next }
232
- # Another 2-space sub-key ends this section.
233
- in_src && /^ [A-Za-z]/ { close_here(); print; next }
234
- in_sec && /^ -[[:space:]]/ { print; next } # existing item
235
- in_sec && /^[[:space:]]*$/ { close_here(); print; next }
236
- { print }
237
- END {
238
- close_here()
239
- if (in_src && !inserted) { print " " section ":"; emit() }
240
- }
241
- ' "$config" > "$tmp" && mv "$tmp" "$config"
242
- }
243
-
244
- # Back-compat shim: 0.3.1 shipped with this name, and a shipped migration is
245
- # never rewritten.
246
- _mig_add_skill_source() {
247
- _mig_add_source "$1" "skills" "$2"
248
- }
249
-
250
- # --- migrate_to_0_3_1 -------------------------------------------------------
251
- # Pre-0.3.1: engine + meta-skills + INIT.md + docs lived flat under the
252
- # umbrella, mixed with project content. 0.3.1: they move into the self-
253
- # contained module subfolder <umbrella>/<module>/. Project content
254
- # (rules/, agents/, non-meta skills/, config.yaml) is never moved or deleted.
255
- #
256
- # Ownership of meta-skills is by the reserved `intelligence-` prefix. A
257
- # project skill must not use that prefix (documented in CONVENTIONS.md).
258
- #
259
- # migrate_to_0_3_1 <umbrella_dir> <module_name> [<upstream_module_dir>]
260
- # upstream_module_dir set → authoritative content source (update.sh)
261
- # upstream_module_dir empty → relocate local legacy files (sync.sh, offline)
262
- migrate_to_0_3_1() {
263
- local umbrella="$1" module_name="$2" upstream="${3:-}"
264
- local module_dir="$umbrella/$module_name"
265
- local s
266
-
267
- # Precondition: any legacy upstream-owned artifact directly under the
268
- # umbrella. None ⇒ already modular or fresh ⇒ idempotent no-op.
269
- #
270
- # The flat engine is identified by its entry script `scripts/sync.sh`, never
271
- # by the bare `scripts/` directory: a project may legitimately own
272
- # <umbrella>/scripts/ for its own tooling, and mistaking it for the engine
273
- # would relocate then delete the project's code (the postcondition below
274
- # already treats scripts/sync.sh as the engine sentinel).
275
- local has_legacy=0
276
- local legacy_engine=0
277
- [ -f "$umbrella/scripts/sync.sh" ] && legacy_engine=1
278
- [ "$legacy_engine" -eq 1 ] && has_legacy=1
279
- [ -e "$umbrella/INIT.md" ] && has_legacy=1
280
- [ -d "$umbrella/docs" ] && has_legacy=1
281
- for s in "$umbrella"/skills/intelligence-*; do
282
- [ -e "$s" ] && { has_legacy=1; break; }
283
- done
284
- # Already modular / fresh ⇒ nothing to relocate. Stamping is owned by the
285
- # dispatcher (run_migrations), not here.
286
- if [ "$has_legacy" -eq 0 ]; then
287
- return 0
288
- fi
289
-
290
- echo " [migrate 0.3.1] legacy flat layout detected — relocating engine into '$module_name/'"
291
- IS_MIGRATED=1
292
- mkdir -p "$module_dir"
293
-
294
- # Authoritative content source: the fresh upstream clone when update.sh
295
- # supplies one, else the project's own legacy files (sync.sh, offline).
296
- local src_root="$umbrella"
297
- if [ -n "$upstream" ] && [ -d "$upstream" ]; then
298
- src_root="$upstream"
299
- fi
300
-
301
- # Stage. Every copy's exit status is captured — see _mig_copy_dir.
302
- local copy_rc=0
303
- _mig_copy_dir "$src_root/scripts" "$module_dir/scripts" || copy_rc=1
304
- _mig_copy_file "$src_root/INIT.md" "$module_dir/INIT.md" || copy_rc=1
305
- _mig_copy_dir "$src_root/docs" "$module_dir/docs" || copy_rc=1
306
- mkdir -p "$module_dir/skills" || copy_rc=1
307
- for s in "$src_root"/skills/intelligence-*; do
308
- [ -d "$s" ] || continue
309
- _mig_copy_dir "$s" "$module_dir/skills/$(basename "$s")" || copy_rc=1
310
- done
311
-
312
- # Verify the FULL postcondition BEFORE any destructive cleanup: every
313
- # artifact present at the source must now exist in the module, non-empty.
314
- # This is the crash-safety gate — a half-populated module must never
315
- # trigger legacy deletion, so two script sentinels are not enough.
316
- local missing=""
317
- [ "$copy_rc" -eq 0 ] || missing="$missing copy-failed"
318
- [ -s "$module_dir/scripts/sync.sh" ] || missing="$missing scripts/sync.sh"
319
- [ -s "$module_dir/scripts/lib/common.sh" ] || missing="$missing scripts/lib/common.sh"
320
- if [ -f "$src_root/INIT.md" ] && [ ! -s "$module_dir/INIT.md" ]; then
321
- missing="$missing INIT.md"
322
- fi
323
- if [ -d "$src_root/docs" ] && [ -z "$(find "$module_dir/docs" -type f 2>/dev/null | head -1)" ]; then
324
- missing="$missing docs/"
325
- fi
326
- for s in "$src_root"/skills/intelligence-*; do
327
- [ -d "$s" ] || continue
328
- if [ ! -s "$module_dir/skills/$(basename "$s")/SKILL.md" ]; then
329
- missing="$missing skills/$(basename "$s")"
330
- fi
331
- done
332
-
333
- if [ -n "$missing" ]; then
334
- is_status aborted-incomplete "module=$module_name missing=${missing# }"
335
- echo " ERROR: migration aborted — '$module_name/' incomplete (${missing# }); legacy left intact." >&2
336
- return "$IS_RC_ABORTED_INCOMPLETE"
337
- fi
338
-
339
- # Remove ONLY the legacy upstream-owned locations. Meta-skills / INIT /
340
- # docs are never the running process, so these always succeed → no
341
- # duplicate intelligence-* ever survives under <umbrella>/skills/.
342
- rm -f "$umbrella/INIT.md"
343
- rm -rf "$umbrella/docs"
344
- for s in "$umbrella"/skills/intelligence-*; do
345
- [ -e "$s" ] && rm -rf "$s"
346
- done
347
- # The legacy scripts/ dir may host the *currently running* update.sh.
348
- # On Linux deleting an open script is fine; some Windows shells refuse
349
- # it. Try, and if it lingers print a one-line manual cleanup instead of
350
- # failing — the new location, config, and stamp are already correct, and
351
- # the next sync/update run removes the dead dir. Guarded by the engine
352
- # sentinel so a project-owned <umbrella>/scripts/ (no sync.sh) is never
353
- # removed, even if another legacy signal set has_legacy.
354
- if [ "$legacy_engine" -eq 1 ]; then
355
- rm -rf "$umbrella/scripts" 2>/dev/null || true
356
- if [ -d "$umbrella/scripts" ]; then
357
- echo " NOTE: legacy '$umbrella/scripts' still present (likely in use)." >&2
358
- echo " Remove it manually once this process exits:" >&2
359
- echo " rm -rf \"$umbrella/scripts\"" >&2
360
- fi
361
- fi
362
-
363
- # config.yaml: name-agnostic relative path under the actual umbrella base.
364
- _mig_add_skill_source "$umbrella/config.yaml" "$(_mig_umbrella_rel "$umbrella")/$module_name/skills"
365
-
366
- echo " [migrate 0.3.1] done — engine at '$module_name/', legacy removed, no duplicates"
367
- }
368
-
369
- # --- migrate_to_0_7_0 -------------------------------------------------------
370
- # 0.7.0 is the first release where the engine ships a RULE and an AGENT of its
371
- # own (`intelligence-authoring`, `intelligence-architect`), inside the module
372
- # beside the meta-skills. Those reach the IDEs only if `config.yaml` lists the
373
- # module's `rules/` and `agents/` directories as sources — so this migration
374
- # registers them, exactly as 0.3.1 registered the module's `skills/`.
375
- #
376
- # Precondition is structural: both entries already present ⇒ applied ⇒ silent
377
- # no-op. Nothing else in config.yaml is read or rewritten, and no file is
378
- # deleted, so there is nothing to stage — the postcondition is simply that both
379
- # entries are readable afterwards.
380
- #
381
- # migrate_to_0_7_0 <umbrella_dir> <module_name> [<upstream_module_dir> — unused]
382
- migrate_to_0_7_0() {
383
- local umbrella="$1" module_name="$2"
384
- local config="$umbrella/config.yaml"
385
- [ -f "$config" ] || return 0
386
-
387
- local base rules_entry agents_entry
388
- base="$(_mig_umbrella_rel "$umbrella")"
389
- rules_entry="$base/$module_name/rules"
390
- agents_entry="$base/$module_name/agents"
391
-
392
- if _mig_has_source "$config" "$rules_entry" && _mig_has_source "$config" "$agents_entry"; then
393
- return 0
394
- fi
395
-
396
- echo " [migrate 0.7.0] registering the module's rules/ and agents/ as sources"
397
- IS_MIGRATED=1
398
- _mig_add_source "$config" "rules" "$rules_entry"
399
- _mig_add_source "$config" "agents" "$agents_entry"
400
-
401
- if ! _mig_has_source "$config" "$rules_entry" || ! _mig_has_source "$config" "$agents_entry"; then
402
- is_status error "migrate_to_0_7_0 could not register module sources"
403
- echo " ERROR: failed to add the module's rules/agents to sources in $config." >&2
404
- echo " Add them by hand under 'sources:':" >&2
405
- echo " rules: - \"$rules_entry\"" >&2
406
- echo " agents: - \"$agents_entry\"" >&2
407
- return "$IS_RC_ERROR"
408
- fi
409
-
410
- echo " [migrate 0.7.0] done — sources.rules += $rules_entry, sources.agents += $agents_entry"
411
- }
412
-
413
- # --- migrate_to_0_10_0 ------------------------------------------------------
414
-
415
- # Split `git+<url>[@<ref>][#<subpath>]` into _MIG_URL / _MIG_REF / _MIG_SUBPATH.
416
- # Same grammar resolve_source_dir parses; kept here so a migration never has to
417
- # source the engine library it is migrating towards.
418
- #
419
- # Globals, never a delimited string on stdout: two of the three fields are
420
- # optional, and `read` with a whitespace IFS collapses a run of delimiters into
421
- # one — so an unpinned `git+<url>#rules` would come back as ref=rules with no
422
- # subpath, silently rewriting a whole source into a branch that does not exist.
423
- _mig_split_git_token() {
424
- local rest="${1#git+}" urlref after cand
425
- _MIG_SUBPATH=""; _MIG_REF=""
426
- case "$rest" in
427
- *\#*) _MIG_SUBPATH="${rest#*#}"; urlref="${rest%%#*}" ;;
428
- *) urlref="$rest" ;;
429
- esac
430
- _MIG_URL="$urlref"
431
- after="${urlref#*://}"
432
- case "$after" in
433
- *@*)
434
- cand="${after##*@}"
435
- case "$cand" in
436
- */*|"") ;;
437
- *) _MIG_REF="$cand"; _MIG_URL="${urlref%@$cand}" ;;
438
- esac
439
- ;;
440
- esac
441
- }
442
-
443
- # Pack name from a repo URL: basename minus `.git`, restricted to a safe
444
- # filename charset. This is the ONLY place a name is still derived, it runs
445
- # once, and the result is written into config.yaml where a human can rename it.
446
- _mig_pack_name_from_url() {
447
- local name="${1%/}"
448
- name="${name##*/}"
449
- name="${name%.git}"
450
- name="$(printf '%s' "$name" | tr -c 'A-Za-z0-9._-' '-')"
451
- case "$name" in ""|.*|-*) name="pack" ;; esac
452
- printf '%s' "$name"
453
- }
454
-
455
- # 0.10.0 replaces the single `external: { dir: … }` block with a `packs:` block:
456
- # a remote source is DECLARED once (url + ref + optional mirror) and referenced
457
- # from `sources.*` by name (`@<pack>/<subpath>`). That removes the duplicated
458
- # `url@ref` an inline spec forced into every section, and makes the mirror
459
- # directory declared rather than derived from the URL.
460
- #
461
- # This migration rewrites the config for the project: every inline `git+` spec
462
- # becomes a declared pack plus an `@name` reference, and `external.dir` becomes
463
- # each pack's `mirror:` so vendored content keeps landing where it already is.
464
- # Inline `git+` specs remain legal afterwards — they are simply no longer the
465
- # only way to reach a remote source, and they are always transient.
466
- #
467
- # Precondition is structural, and each key is a one-way marker: `external:` only
468
- # ever existed BEFORE 0.10.0, `packs:` only ever exists after it. So an
469
- # `external:` key ⇒ convert; otherwise a `packs:` key ⇒ applied ⇒ silent no-op;
470
- # otherwise convert only if there is an inline spec to convert. That last clause
471
- # is what keeps the migration idempotent once inline specs are legal again: the
472
- # run that converts them writes `packs:`, and every later run stops at the marker
473
- # instead of appending a SECOND top-level `packs:` key for each spec added since.
474
- # Fail-closed: the rewrite is staged in a temp file and verified before it
475
- # replaces config.yaml.
476
- #
477
- # migrate_to_0_10_0 <umbrella_dir> <module_name> [<upstream_module_dir> — unused]
478
- migrate_to_0_10_0() {
479
- local umbrella="$1"
480
- local config="$umbrella/config.yaml"
481
- [ -f "$config" ] || return 0
482
-
483
- local has_external=0
484
- grep -q '^external:[[:space:]]*$' "$config" && has_external=1
485
- local has_packs=0
486
- grep -q '^packs:[[:space:]]*$' "$config" && has_packs=1
487
- local has_inline=0
488
- # ERE, and anchored to a list entry: `\|` is a GNU BRE extension that BSD
489
- # grep (the macOS default) reads as a literal, and matching `git+` anywhere
490
- # would fire on the comment that documents the old spec format.
491
- grep -qE '^[[:space:]]*-[[:space:]]*["'\'']?git\+' "$config" && has_inline=1
492
- if [ "$has_external" -eq 0 ] && { [ "$has_packs" -eq 1 ] || [ "$has_inline" -eq 0 ]; }; then
493
- return 0
494
- fi
495
-
496
- echo " [migrate 0.10.0] converting remote sources to declared packs"
497
- IS_MIGRATED=1
498
-
499
- # The old external dir, if any — it becomes each pack's mirror parent.
500
- local ext_dir=""
501
- if [ "$has_external" -eq 1 ]; then
502
- ext_dir="$(awk '
503
- { sub(/\r$/, "") }
504
- /^external:[[:space:]]*$/ { in_ext = 1; next }
505
- /^[A-Za-z]/ { in_ext = 0 }
506
- in_ext && /^ dir:/ {
507
- v = $0
508
- sub(/^[[:space:]]*[^:]*:[[:space:]]*/, "", v)
509
- # Two subs, never one alternation: POSIX awk takes the LONGEST
510
- # match at the leftmost position, so `^["]|["].*$` would match
511
- # the whole quoted value at position 1 and erase it.
512
- sub(/^["\047]/, "", v)
513
- sub(/["\047][[:space:]]*$/, "", v)
514
- sub(/[[:space:]]+$/, "", v)
515
- print v; exit
516
- }
517
- ' "$config")"
518
- ext_dir="${ext_dir%/}"
519
- fi
520
-
521
- # Collect the distinct url@ref pairs across every section, in first-seen
522
- # order, assigning each a unique name.
523
- local names=() urls=() refs=() seen=()
524
- local section token url ref subpath sig i found name base n
525
- for section in rules agents skills; do
526
- while IFS= read -r token; do
527
- case "$token" in git+*) ;; *) continue ;; esac
528
- _mig_split_git_token "$token"
529
- url="$_MIG_URL"; ref="$_MIG_REF"
530
- sig="$url@$ref"
531
- # `${#arr[@]}` guards, never `"${!arr[@]}"` on a possibly-empty
532
- # array: bash 3.2 (macOS default) treats that as unbound under
533
- # `set -u`, which every script here runs with.
534
- found=0
535
- i=0
536
- while [ "$i" -lt "${#seen[@]}" ]; do
537
- [ "${seen[$i]}" = "$sig" ] && { found=1; break; }
538
- i=$((i + 1))
539
- done
540
- [ "$found" -eq 1 ] && continue
541
- base="$(_mig_pack_name_from_url "$url")"
542
- name="$base"; n=2
543
- while :; do
544
- found=0
545
- i=0
546
- while [ "$i" -lt "${#names[@]}" ]; do
547
- [ "${names[$i]}" = "$name" ] && { found=1; break; }
548
- i=$((i + 1))
549
- done
550
- [ "$found" -eq 0 ] && break
551
- name="$base-$n"; n=$((n + 1))
552
- done
553
- seen+=("$sig"); names+=("$name"); urls+=("$url"); refs+=("$ref")
554
- done < <(_mig_read_sources "$config" "$section")
555
- done
556
-
557
- # Build the packs: block.
558
- local packs_block="" mirror
559
- i=0
560
- while [ "$i" -lt "${#names[@]}" ]; do
561
- packs_block="$packs_block ${names[$i]}:"$'\n'
562
- packs_block="$packs_block url: ${urls[$i]}"$'\n'
563
- [ -n "${refs[$i]}" ] && packs_block="$packs_block ref: ${refs[$i]}"$'\n'
564
- if [ -n "$ext_dir" ]; then
565
- mirror="$ext_dir/${names[$i]}"
566
- packs_block="$packs_block mirror: \"$mirror\""$'\n'
567
- fi
568
- i=$((i + 1))
569
- done
570
-
571
- # Map every inline spec to its `@name[/subpath]` replacement.
572
- local map=""
573
- for section in rules agents skills; do
574
- while IFS= read -r token; do
575
- case "$token" in git+*) ;; *) continue ;; esac
576
- _mig_split_git_token "$token"
577
- url="$_MIG_URL"; ref="$_MIG_REF"; subpath="$_MIG_SUBPATH"
578
- sig="$url@$ref"
579
- i=0
580
- while [ "$i" -lt "${#seen[@]}" ]; do
581
- if [ "${seen[$i]}" = "$sig" ]; then
582
- map="$map$token"$'\t'"@${names[$i]}${subpath:+/$subpath}"$'\n'
583
- break
584
- fi
585
- i=$((i + 1))
586
- done
587
- done < <(_mig_read_sources "$config" "$section")
588
- done
589
-
590
- # An `external:` block with no remote source at all declares nothing — emit
591
- # no `packs:` key rather than a dangling empty one, and drop `external:`.
592
- local emit_packs=1
593
- [ "${#names[@]}" -eq 0 ] && emit_packs=0
594
-
595
- local tmp="$config.mig.tmp" mapfile_="$config.mig.map"
596
- printf '%s' "$map" > "$mapfile_"
597
- awk -v packs="$packs_block" -v mapfile="$mapfile_" -v emit="$emit_packs" '
598
- BEGIN {
599
- while ((getline line < mapfile) > 0) {
600
- sub(/\r$/, "", line)
601
- t = index(line, "\t")
602
- if (t > 0) repl[substr(line, 1, t - 1)] = substr(line, t + 1)
603
- }
604
- close(mapfile)
605
- }
606
- { sub(/\r$/, "") }
607
- # Drop the whole external: block.
608
- /^external:[[:space:]]*$/ { in_ext = 1; next }
609
- in_ext && /^[[:space:]]/ { next }
610
- in_ext { in_ext = 0 }
611
- # Emit packs: immediately before sources:.
612
- /^sources:[[:space:]]*$/ && emit == 1 && !done_packs {
613
- printf "packs:\n%s\n", packs
614
- done_packs = 1
615
- }
616
- # Rewrite an inline spec in place, preserving indentation and quoting.
617
- /^[[:space:]]*-[[:space:]]*["\047]?git\+/ {
618
- val = $0
619
- sub(/^[[:space:]]*-[[:space:]]*/, "", val)
620
- gsub(/^["\047]|["\047][[:space:]]*$/, "", val)
621
- if (val in repl) {
622
- indent = $0
623
- sub(/-.*$/, "", indent)
624
- print indent "- \"" repl[val] "\""
625
- next
626
- }
627
- }
628
- { print }
629
- END { if (emit == 1 && !done_packs) printf "packs:\n%s\n", packs }
630
- ' "$config" > "$tmp"
631
-
632
- # Verify the staged file before it replaces anything: `external:` gone, and
633
- # EXACTLY the expected number of top-level `packs:` keys — a second one
634
- # would be duplicate-key YAML that strict parsers reject.
635
- local packs_keys
636
- packs_keys="$(grep -c '^packs:[[:space:]]*$' "$tmp" || true)"
637
- if [ "$packs_keys" -ne "$emit_packs" ] || grep -q '^external:[[:space:]]*$' "$tmp"; then
638
- rm -f "$tmp" "$mapfile_"
639
- is_status error "migrate_to_0_10_0 could not rewrite config.yaml"
640
- echo " ERROR: failed to convert remote sources to packs in $config." >&2
641
- echo " Declare each remote under 'packs:' and reference it as '@<name>/<subpath>'." >&2
642
- return "$IS_RC_ERROR"
643
- fi
644
-
645
- mv "$tmp" "$config"
646
- rm -f "$mapfile_"
647
- if [ "$emit_packs" -eq 0 ]; then
648
- echo " [migrate 0.10.0] done — no remote source to declare, dropped the empty 'external:' block"
649
- else
650
- echo " [migrate 0.10.0] done — ${#names[@]} pack(s) declared${ext_dir:+, mirrored under $ext_dir}"
651
- fi
652
- }
653
-
654
- # Read one `sources.<section>` list, one raw value per line (quotes stripped).
655
- # Local to migrations so the chain never depends on the engine library.
656
- _mig_read_sources() {
657
- local config="$1" section="$2"
658
- awk -v section="$section" '
659
- { sub(/\r$/, "") }
660
- /^sources:[[:space:]]*$/ { in_src = 1; next }
661
- /^[A-Za-z]/ { in_src = 0; in_sec = 0 }
662
- in_src && $0 ~ "^ " section ":[[:space:]]*$" { in_sec = 1; next }
663
- in_src && /^ [A-Za-z]/ { in_sec = 0 }
664
- in_sec && /^[[:space:]]*-[[:space:]]*/ {
665
- v = $0
666
- sub(/^[[:space:]]*-[[:space:]]*/, "", v)
667
- gsub(/^["\047]|["\047][[:space:]]*$/, "", v)
668
- sub(/[[:space:]]+$/, "", v)
669
- if (v != "") print v
670
- }
671
- ' "$config"
672
- }
673
-
674
- # run_migrations <umbrella_dir> <module_name> [<upstream_module_dir>]
675
- # The dispatcher of the breaking-change chain. Correctness rests on idempotent
676
- # structural preconditions, NOT on the stamp: every migrate_to_* self-detects
677
- # whether its change is already applied and is a silent no-op if so, so the
678
- # whole chain is simply run in order — a wrong/missing stamp can never cause a
679
- # needed migration to be skipped. The stamp is only a safety guard
680
- # (ahead-of-engine / needs-update) + reporting. Returns 0 on success
681
- # (IS_MIGRATED tells whether work happened) or the failing migration's IS_RC_*
682
- # code; never partially destroys (each migrate_to_* is transactional and
683
- # fail-closed). Caller maps the code to an exit status + IS_STATUS line.
684
- run_migrations() {
685
- local umbrella="$1" module_name="$2" upstream="${3:-}"
686
- local cf="$umbrella/config.yaml"
687
- local v ver rc
688
- # A stale engine must never touch a project schema stamped newer than it
689
- # knows. (Reads the frozen contract key from config.yaml.)
690
- check_version_compat "$cf" || return $?
691
- for v in "${MIGRATIONS[@]}"; do
692
- ver="$(_mig_ver_dotted "$v")"
693
- "migrate_to_$v" "$umbrella" "$module_name" "$upstream"
694
- rc=$?
695
- if [ "$rc" -ne 0 ]; then return "$rc"; fi
696
- # Commit progress: stamp this version so an interrupted chain resumes
697
- # from the last good point. Idempotent migrations that no-op'd just
698
- # re-assert the same value.
699
- stamp_version "$cf" "$ver"
700
- done
701
- # Fresh project with no stamp at all (e.g. just-bootstrapped, nothing in
702
- # the chain applied) → stamp to the engine version so future runs have a
703
- # baseline. Safe: migrations already self-skipped.
704
- if [ -z "$(read_engine_stamp "$cf")" ]; then
705
- stamp_version "$cf" "$(engine_version)"
706
- fi
707
- return 0
708
- }