@ainova-systems/intelligence 0.19.0 → 0.20.0

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.
@@ -84,6 +84,11 @@ IS_RC_AMBIGUOUS=3 # conflicting state; agent/human-only — bash never
84
84
  IS_RC_AHEAD=4 # project stamped newer than this engine understands
85
85
  IS_RC_ABORTED_INCOMPLETE=5 # staged state incomplete; the project was left untouched
86
86
  IS_RC_NEEDS_UPDATE=6 # pending schema changes (stamp < engine) — run `intelligence init --apply`
87
+ # `intelligence sync --check` only: generated files differ from what sync would
88
+ # leave, or alignment is pending (IS_STATUS=out-of-date). It shares 2 with
89
+ # config-missing, which a check never reports: the CLI found the manifest, and
90
+ # the check turns any other failure that ends in 2 into 1.
91
+ IS_RC_OUT_OF_DATE=2
87
92
 
88
93
  # is_status <code-name> [detail] — emit one parseable line for the skill.
89
94
  is_status() {
package/engine/sync.sh CHANGED
@@ -10,6 +10,8 @@
10
10
  # IS_CONTENT_REL the project's content dir, repo-relative
11
11
  # IS_MODULE_REL the installed sync package, repo-relative
12
12
  # IS_PROTECTED_DIRS dirs an adapter output may never overlap
13
+ # IS_SYNC_CHECK optional: render, compare and restore, then write
14
+ # `same` or `differs` to this file (`sync --check`)
13
15
  #
14
16
  # Usage: intelligence sync [target]
15
17
 
@@ -111,46 +113,43 @@ while IFS=$'\037' read -r section src state _dir alias holders; do
111
113
  echo "WARNING: sources.$section '$src' $IS_SOURCE_PROBLEM — skipped; 'intelligence status --check' reports it" >&2
112
114
  done <<< "${IS_YL_UNRESOLVED:-}"
113
115
 
116
+
117
+ # Sources stay read-only for the whole run (validate_output_path refuses an
118
+ # output inside one), so read_source_artifact_files enumerates each section
119
+ # once, in this shell, and its later callers — the agents adapter, the shared
120
+ # skill directory, the context report — replay it. Adapters that list sources
121
+ # with in-shell globs spawn nothing and keep their own, locale-ordered listing.
122
+ # The skills pass also settles every symlink in a skills source and warns once
123
+ # about each it leaves out (decision 0017).
124
+ # shellcheck disable=SC2034 # read by read_source_artifact_files in lib/common.sh
125
+ IS_SOURCE_FILES_MEMO=1
126
+ for section in rules agents skills; do
127
+ read_source_artifact_files "$REPO_ROOT" "$CONFIG_FILE" "$section"
128
+ done
129
+
114
130
  # Lint frontmatter across all source files (rules, agents, skills).
115
131
  # Catches issues like unquoted colons that strict YAML consumers reject.
132
+ # Skills are the rendered ones the pass above settled: SKILL.md behind a link
133
+ # inside its source is linted, one that leaves its source is never read.
116
134
  LINT_FILES=()
117
- LINT_SKILL_DIRS=()
118
- for section in rules agents skills; do
135
+ LINT_SKILLS=()
136
+ [ "${#IS_SOURCE_FILES[@]}" -eq 0 ] || LINT_SKILLS=("${IS_SOURCE_FILES[@]}")
137
+ for section in rules agents; do
119
138
  load_yaml_list "$CONFIG_FILE" "$section"
120
139
  while IFS= read -r src; do
121
140
  [ -z "$src" ] && continue
122
141
  src_dir="$REPO_ROOT/$src"
123
142
  [ -d "$src_dir" ] || continue
124
- if [ "$section" = "skills" ]; then
125
- LINT_SKILL_DIRS+=("$src_dir")
126
- else
127
- for f in "$src_dir"/*.md; do
128
- [ -f "$f" ] && LINT_FILES+=("$f")
129
- done
130
- fi
143
+ for f in "$src_dir"/*.md; do
144
+ [ -f "$f" ] && LINT_FILES+=("$f")
145
+ done
131
146
  done <<< "$IS_YAML_LIST"
132
147
  done
133
- # One find for every skills source: it walks its start points in order.
134
- if [ "${#LINT_SKILL_DIRS[@]}" -gt 0 ]; then
135
- while IFS= read -r f; do
136
- [ -n "$f" ] && LINT_FILES+=("$f")
137
- done < <(find "${LINT_SKILL_DIRS[@]}" -mindepth 2 -maxdepth 2 -name 'SKILL.md' 2>/dev/null)
138
- fi
148
+ [ "${#LINT_SKILLS[@]}" -eq 0 ] || LINT_FILES+=("${LINT_SKILLS[@]}")
139
149
  if [ "${#LINT_FILES[@]}" -gt 0 ]; then
140
150
  lint_frontmatter_files "${LINT_FILES[@]}"
141
151
  fi
142
152
 
143
- # Sources stay read-only for the whole run (validate_output_path refuses an
144
- # output inside one), so read_source_artifact_files enumerates each section
145
- # once, in this shell, and its later callers — the agents adapter, the shared
146
- # skill directory, the context report — replay it. Adapters that list sources
147
- # with in-shell globs spawn nothing and keep their own, locale-ordered listing.
148
- # shellcheck disable=SC2034 # read by read_source_artifact_files in lib/common.sh
149
- IS_SOURCE_FILES_MEMO=1
150
- for section in rules agents skills; do
151
- read_source_artifact_files "$REPO_ROOT" "$CONFIG_FILE" "$section"
152
- done
153
-
154
153
  # Adapters come from two places, discovered by filename (minus `.sh`,
155
154
  # `_template` excluded):
156
155
  #
@@ -211,6 +210,17 @@ SYNC_PARALLEL=1
211
210
  SYNC_BG_PIDS=()
212
211
  SYNC_COPY_INDEXES=()
213
212
 
213
+ # An accepted output-root directory link is a write path, unlike a nested
214
+ # generated asset link. Checks must retain the bytes behind that root too.
215
+ snapshot_sync_entry() {
216
+ local src="$1" index="$2"
217
+ cp -a "$src" "$SYNC_TX_DIR/data/$index" || return $?
218
+ if [ -n "${IS_SYNC_CHECK:-}" ] && [ -L "$src" ] && [ -d "$src" ]; then
219
+ mkdir -p "$SYNC_TX_DIR/targets/$index" || return $?
220
+ cp -a "$src/." "$SYNC_TX_DIR/targets/$index" || return $?
221
+ fi
222
+ }
223
+
214
224
  snapshot_sync_path() {
215
225
  local adapter_name="$1" rel="$2" src index present=0
216
226
  case "$SYNC_TX_SEEN_LIST" in
@@ -224,11 +234,11 @@ snapshot_sync_path() {
224
234
  # Copies of distinct paths into distinct slots: they can overlap, and
225
235
  # The snapshot wait before the render collects every status.
226
236
  if [ "$SYNC_PARALLEL" = 1 ]; then
227
- cp -a "$src" "$SYNC_TX_DIR/data/$index" 2> "$SYNC_TX_DIR/copy.$index.err" &
237
+ snapshot_sync_entry "$src" "$index" 2> "$SYNC_TX_DIR/copy.$index.err" &
228
238
  SYNC_BG_PIDS+=("$!")
229
239
  SYNC_COPY_INDEXES+=("$index")
230
240
  else
231
- cp -a "$src" "$SYNC_TX_DIR/data/$index"
241
+ snapshot_sync_entry "$src" "$index"
232
242
  fi
233
243
  present=1
234
244
  fi
@@ -274,6 +284,68 @@ wait_sync_jobs() {
274
284
  return "$first"
275
285
  }
276
286
 
287
+ # Compare each entry without following links: copied relative and dangling
288
+ # links describe their targets, not bytes at the snapshot's new location.
289
+ compare_sync_entries() {
290
+ local snap="$1" live="$2" rc=0 target before
291
+ if [ -L "$snap" ]; then
292
+ [ -L "$live" ] || return 1
293
+ before="$(readlink "$snap")" || return 2
294
+ target="$(readlink "$live")" || return 2
295
+ # Source links inside the private copy refer to its root. Compare the
296
+ # generated target in the original project's coordinate system.
297
+ if [ -n "${IS_SYNC_CHECK_ROOT:-}" ] && [ -n "${IS_SYNC_CHECK:-}" ]; then
298
+ case "$before" in "$REPO_ROOT") before="$IS_SYNC_CHECK_ROOT" ;; "$REPO_ROOT"/*) before="$IS_SYNC_CHECK_ROOT/${before#"$REPO_ROOT/"}" ;; esac
299
+ case "$target" in "$REPO_ROOT") target="$IS_SYNC_CHECK_ROOT" ;; "$REPO_ROOT"/*) target="$IS_SYNC_CHECK_ROOT/${target#"$REPO_ROOT/"}" ;; esac
300
+ fi
301
+ [ "$before" = "$target" ] || return 1
302
+ elif [ -L "$live" ]; then
303
+ return 1
304
+ elif [ -f "$snap" ] && [ -f "$live" ]; then
305
+ cmp -s "$snap" "$live" || rc=$?
306
+ case "$rc" in 0) ;; 1) return 1 ;; *) return 2 ;; esac
307
+ if [ -x "$snap" ]; then [ -x "$live" ] || return 1; else [ ! -x "$live" ] || return 1; fi
308
+ elif [ -d "$snap" ] && [ -d "$live" ]; then
309
+ return 0
310
+ else
311
+ return 1
312
+ fi
313
+ }
314
+
315
+ compare_sync_snapshot() {
316
+ local index rel present live snap entry sub
317
+ while IFS=$'\t' read -r index rel present; do
318
+ [ -n "$rel" ] || continue
319
+ live="$REPO_ROOT/$rel"
320
+ snap="$SYNC_TX_DIR/data/$index"
321
+ if [ "$present" != 1 ]; then
322
+ if [ -e "$live" ] || [ -L "$live" ]; then return 1; fi
323
+ continue
324
+ fi
325
+ compare_sync_entries "$snap" "$live" || return $?
326
+ if [ -d "$SYNC_TX_DIR/targets/$index" ]; then
327
+ # validate_output_path permitted this physical directory inside
328
+ # the private project. Only its root is followed; descendants
329
+ # still use the entry-kind and literal-target comparison.
330
+ snap="$SYNC_TX_DIR/targets/$index"
331
+ live="$(cd "$live" && pwd -P)" || return 2
332
+ fi
333
+ [ ! -L "$snap" ] && [ -d "$snap" ] || continue
334
+ # NUL-delimited paths also preserve whitespace and newlines. find's
335
+ # default walk does not dereference links on GNU or BSD userland.
336
+ find "$snap" -print0 > "$SYNC_TX_DIR/snapshot.list" || return 2
337
+ find "$live" -print0 > "$SYNC_TX_DIR/live.list" || return 2
338
+ while IFS= read -r -d '' entry; do
339
+ sub="${entry#"$snap"}"
340
+ compare_sync_entries "$entry" "$live$sub" || return $?
341
+ done < "$SYNC_TX_DIR/snapshot.list"
342
+ while IFS= read -r -d '' entry; do
343
+ sub="${entry#"$live"}"
344
+ [ -e "$snap$sub" ] || [ -L "$snap$sub" ] || return 1
345
+ done < "$SYNC_TX_DIR/live.list"
346
+ done < "$SYNC_TX_INDEX"
347
+ }
348
+
277
349
  finish_sync_transaction() {
278
350
  local rc=$?
279
351
  trap - EXIT INT TERM
@@ -625,6 +697,25 @@ if [ $synced -eq 0 ]; then
625
697
  exit 1
626
698
  fi
627
699
 
700
+ # A check renders exactly like a sync, then compares every snapshotted path
701
+ # with what the render left and restores the snapshot whatever the answer: it
702
+ # never changes a file. The transaction stays active until the restore is
703
+ # complete, so an interrupted restore is finished by the EXIT handler.
704
+ if [ -n "${IS_SYNC_CHECK:-}" ]; then
705
+ check_rc=0
706
+ compare_sync_snapshot || check_rc=$?
707
+ case "$check_rc" in
708
+ 0) check_verdict=same ;;
709
+ 1) check_verdict=differs ;;
710
+ *)
711
+ echo "ERROR: sync --check could not compare the rendered output with its snapshot." >&2
712
+ exit 1
713
+ ;;
714
+ esac
715
+ restore_sync_snapshot
716
+ printf '%s\n' "$check_verdict" > "$IS_SYNC_CHECK"
717
+ fi
718
+
628
719
  SYNC_TX_ACTIVE=0
629
720
 
630
721
  # After rendering, three read-only reports, in this order:
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ainova-systems/intelligence",
3
- "version": "0.19.0",
3
+ "version": "0.20.0",
4
4
  "description": "Build, version and distribute AI agent intelligence across your organization — one CLI, versioned Intelligence Packages, and a sync engine for Claude Code, Cursor, Copilot, Codex, Pi and OpenCode.",
5
5
  "bin": {
6
6
  "intelligence": "bin/intelligence.js"
@@ -178,6 +178,7 @@ Use the engine library instead of copying parsers or file-handling logic.
178
178
  | `copy_skill_bundle(src, dest)` | Copy `SKILL.md` and all resources safely, normalize Markdown, quote free-text frontmatter, and keep a valid `effort:` as written, as the shared skills tree does. |
179
179
  | `copy_skill_bundle_dirs(dest_root, src...)` | Batch form: copy every skill directory into `dest_root/<name>` with one copy and one finalize pass. |
180
180
  | `copy_skill_bundle_dirs_for(tool, dest_root, src...)` | The batch form for a tree one tool reads: `SKILL.md`'s `effort:` becomes that tool's level, or is removed when it has none. |
181
+ | `skill_source_rendered(skill_dir)` | Whether sync renders a skill directory an adapter listed with its own glob; a skill left out for its symlinks fails it. |
181
182
  | `sync_open_skill_dirs(root, config, dest)` | Own and populate a shared Agent Skills directory such as `.agents/skills/`, deriving Codex's `agents/openai.yaml` for a skill with `disable-model-invocation: true`. |
182
183
  | `finalize_output_file(file)` | Expand layout tokens and normalize line endings; required for every emitted text file. |
183
184
  | `finalize_output_files(file...)` / `finalize_copy_files(dest, src...)` | Batch forms: finalize in place, or copy-and-finalize into a directory, in one process. |
@@ -212,6 +213,8 @@ If a new adapter relies on `AGENTS.md` for always-on rules, its target must requ
212
213
 
213
214
  Every successful sync prints an adapter-agnostic `CONTEXT:` summary with source byte totals and file counts. It separates always-on rules from custom context (scoped rules, agent prompts and skill entry points), then reports a numeric `agents-md` byte count and `generated`, `not-generated`, or `disabled` status; supporting skill assets are excluded until explicitly read. Adapter-specific hard limits and suppression controls stay inside the adapter that owns them.
214
215
 
216
+ Print an adapter warning on stderr as one unindented line starting `WARNING:`. `sync --compact`, which `intelligence init` runs, keeps exactly those lines and drops indented progress, so a warning in any other form never reaches the reader.
217
+
215
218
  The Codex adapter checks its `project_doc_max_bytes` default (32 KiB). When generated `AGENTS.md` exceeds it, sync prints the byte count and a sufficient Codex setting. The warning says "may truncate" because a developer can already have a larger override. `targets.codex.warn_project_doc_limit` accepts `true` or omission for the default threshold, a positive byte count to match another effective limit, or `false` to disable the Intelligence warning. Both inline and block target forms work, and the field does not change Codex configuration. The warning remains visible in `sync --compact`.
216
219
 
217
220
  The Antigravity adapter checks the 12,000-character limit its documentation
@@ -237,7 +240,9 @@ Skills follow the [Agent Skills standard](https://agentskills.io). Copy each ski
237
240
  copy_skill_bundle "$source_skill_dir" "$output_dir/skills/$skill_name"
238
241
  ```
239
242
 
240
- Do not use plain `cp` for skill bundles. `copy_skill_bundle` preserves non-Markdown assets, avoids materializing symlink targets, normalizes Markdown, expands layout tokens and quotes `description` and `argument-hint` where strict YAML readers require strings. It renders `effort:` as the shared tree does; a tree only one tool reads uses `copy_skill_bundle_dirs_for <tool>`, so the copy carries that tool's level or no effort at all.
243
+ Do not use plain `cp` for skill bundles. `copy_skill_bundle` preserves non-Markdown assets, normalizes Markdown, expands layout tokens and quotes `description` and `argument-hint` where strict YAML readers require strings. It renders `effort:` as the shared tree does; a tree only one tool reads uses `copy_skill_bundle_dirs_for <tool>`, so the copy carries that tool's level or no effort at all. It never writes a symlink; how a link in a skills source renders is defined in [conventions](conventions.md#skill-body-and-resources).
244
+
245
+ An adapter that lists a skills source with its own glob checks each directory with `skill_source_rendered`. A skill whose directory or `SKILL.md` is a link leaving its source is left out of every output, and sync has already warned about it once.
241
246
 
242
247
  Antigravity, Codex, Pi and OpenCode share `.agents/skills/`. Any adapter writing that open-standard directory must call `sync_open_skill_dirs`; it is the single lifecycle owner for immediate skill subdirectories.
243
248
 
@@ -311,6 +311,8 @@ skill-name/
311
311
 
312
312
  The engine copies the complete skill directory. Promote a helper outside the skill only when multiple skills share it.
313
313
 
314
+ A symlink inside a skills source renders as the file or directory it points at when that stays inside the same source, so one source can alias a skill or share a resource between skills. A link that leaves its source, dangles or points at a directory enclosing it is left out with a `WARNING:` line; a skill whose directory or `SKILL.md` is such a link is left out entirely. Add a skill kept elsewhere as its own source or package instead of linking it in. Generated skill directories never contain symlinks.
315
+
314
316
  Size limits are backstops, not quotas:
315
317
 
316
318
  | Artifact | Hard cap | Response |
@@ -458,7 +460,7 @@ The permanent applied-schema key is the top-level scalar `schema_version` in `in
458
460
  The public lifecycle is deliberately compact:
459
461
 
460
462
  - `intelligence init [--preview|--apply]` is universal: it creates a new setup, aligns an existing Intelligence project, or plans/applies conversion of an eligible legacy Intelligence Sync project.
461
- - `intelligence sync [adapter] [--compact] [--force]` first aligns an existing Intelligence project with the installed CLI and restores every package the store lacks or holds at another commit, strictly from `intelligence.lock`. It skips rendering when local inputs and outputs match a previous successful run. Changes or missing outputs trigger rendering; `--force` always renders and refreshes the search for unconfigured source directories. An unchanged run replays the last full run's useful diagnostics. Compact mode shows context sizes, actionable warnings and final status on success, and all diagnostics on failure. In CI it refuses an alignment that would change tracked files and points to a local `intelligence init --apply` plus review/commit.
463
+ - `intelligence sync [adapter] [--compact] [--force] [--check]` first aligns an existing Intelligence project with the installed CLI and restores every package the store lacks or holds at another commit, strictly from `intelligence.lock`. It skips rendering when local inputs and outputs match a previous successful run. Changes or missing outputs trigger rendering; `--force` always renders and refreshes the search for unconfigured source directories. An unchanged run replays the last full run's useful diagnostics. Compact mode shows context sizes, actionable warnings and final status on success, and all diagnostics on failure. In CI it refuses an alignment that would change tracked files and points to a local `intelligence init --apply` plus review/commit. `--check` changes no generated file: it prints one `IS_STATUS` line and exits 0 when generated files are up to date and 2 (`out-of-date`) when sync would change them or alignment is pending; it never applies alignment.
462
464
  - `intelligence update [@scope/name] [--preview|--apply]` is the only update surface. It prints the CLI/project/package plan; default mode prompts, `--preview` never writes, and `--apply` does not prompt. It never moves `ref:` pins.
463
465
  - `intelligence upgrade [--next] [--preview|--apply]` replaces the installed CLI with the newest version on its npm channel (`next` for a prerelease or with `--next`, otherwise `latest`) with the same modes. It touches no project, never downgrades, and refuses an installation that npm did not make.
464
466
  - `intelligence package add|remove|list|search` owns package inventory.
@@ -479,6 +481,7 @@ The engine emits one machine-readable line, `IS_STATUS=<code> [IS_DETAIL=...]`,
479
481
  | `migrated` | 0 | Initialization converted an older layout |
480
482
  | `error` | 1 | Generic failure |
481
483
  | `config-missing` | 2 | Required manifest is absent |
484
+ | `out-of-date` | 2 | `sync --check` only: sync would change generated files, or alignment is pending. A check never reports `config-missing`, and turns any other failure that would end in 2 into 1 |
482
485
  | `ambiguous` | 3 | Conflicting state requiring agent/human judgment; reserved |
483
486
  | `ahead-of-engine` | 4 | Manifest schema is a newer major than the engine (a newer minor or patch warns and continues) |
484
487
  | `aborted-incomplete` | 5 | Staged replacement was incomplete; prior state remains |