@ainova-systems/intelligence 0.19.0 → 0.21.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.
Files changed (41) hide show
  1. package/cli/commands/init.sh +2 -1
  2. package/cli/commands/registry.sh +1 -1
  3. package/cli/commands/source.sh +1 -1
  4. package/cli/commands/sync.sh +78 -6
  5. package/cli/intelligence +15 -2
  6. package/cli/internal/check.sh +39 -0
  7. package/cli/internal/convert-legacy.sh +1 -1
  8. package/cli/internal/package-search.sh +1 -1
  9. package/cli/internal/package-update.sh +2 -2
  10. package/cli/internal/restore.sh +3 -2
  11. package/cli/lib/cli-common.sh +145 -33
  12. package/cli/lib/gitignore.sh +19 -2
  13. package/cli/lib/lockfile.sh +14 -4
  14. package/cli/lib/manifest.sh +147 -1
  15. package/cli/lib/qmap.awk +97 -32
  16. package/cli/lib/registry.sh +1 -1
  17. package/cli/lib/sync-cache.sh +596 -142
  18. package/cli/lib/sync-lock.sh +19 -2
  19. package/engine/ENGINE_SHA +1 -1
  20. package/engine/VERSION +1 -1
  21. package/engine/adapters/_template.sh +32 -8
  22. package/engine/adapters/agents.sh +47 -52
  23. package/engine/adapters/antigravity.sh +71 -53
  24. package/engine/adapters/claude.sh +42 -57
  25. package/engine/adapters/codex.sh +77 -16
  26. package/engine/adapters/copilot.sh +54 -65
  27. package/engine/adapters/cursor.sh +88 -78
  28. package/engine/adapters/opencode.sh +23 -32
  29. package/engine/adapters/pi.sh +140 -42
  30. package/engine/lib/adapter-contract.sh +36 -4
  31. package/engine/lib/common.sh +1388 -165
  32. package/engine/lib/contract.sh +5 -0
  33. package/engine/sync.sh +145 -38
  34. package/package.json +1 -1
  35. package/packages/sync/references/adapters.md +48 -38
  36. package/packages/sync/references/conventions.md +61 -12
  37. package/packages/sync/references/onboarding-migration.md +4 -4
  38. package/packages/sync/rules/intelligence-authoring.md +2 -2
  39. package/packages/sync/skills/intelligence-learn-from-repository/SKILL.md +7 -5
  40. package/packages/sync/skills/intelligence-review-context/references/audit-checks.md +1 -1
  41. package/packages/sync/skills/intelligence-update-context/references/rules.md +5 -0
@@ -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,45 +113,19 @@ 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
 
114
- # Lint frontmatter across all source files (rules, agents, skills).
115
- # Catches issues like unquoted colons that strict YAML consumers reject.
116
- LINT_FILES=()
117
- LINT_SKILL_DIRS=()
118
- for section in rules agents skills; do
119
- load_yaml_list "$CONFIG_FILE" "$section"
120
- while IFS= read -r src; do
121
- [ -z "$src" ] && continue
122
- src_dir="$REPO_ROOT/$src"
123
- [ -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
131
- done <<< "$IS_YAML_LIST"
132
- 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
139
- if [ "${#LINT_FILES[@]}" -gt 0 ]; then
140
- lint_frontmatter_files "${LINT_FILES[@]}"
141
- fi
142
116
 
143
117
  # 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.
118
+ # output inside one), so the source catalog is built once, in this shell, and
119
+ # every renderer replays it: AGENTS.md, every adapter, the shared skill
120
+ # directory and the context report read sources through source_artifacts_var
121
+ # (decision 0021). Building it names every artifact, settles which of a name
122
+ # renders, refuses ambiguous names before any output is touched, settles every
123
+ # symlink in a skills source (decision 0017), and lints the frontmatter of every
124
+ # source — skills as the inventory renders them: SKILL.md behind a link inside
125
+ # its source is linted, one that leaves its source is never read.
148
126
  # shellcheck disable=SC2034 # read by read_source_artifact_files in lib/common.sh
149
127
  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
128
+ source_catalog_build "$REPO_ROOT" "$CONFIG_FILE" lint
153
129
 
154
130
  # Adapters come from two places, discovered by filename (minus `.sh`,
155
131
  # `_template` excluded):
@@ -190,6 +166,11 @@ for adapters_dir in "$SCRIPT_DIR/adapters" "$INTELLIGENCE_DIR/adapters"; do
190
166
  done
191
167
  done
192
168
 
169
+ # A `targets:` or `exclude_targets:` id must name an adapter this project has,
170
+ # enabled or not, or the group `committed` (decision 0021). Refused here,
171
+ # before anything is snapshotted or written.
172
+ source_catalog_check_ids "$REPO_ROOT" "${ADAPTERS[*]}" || exit 1
173
+
193
174
  # Validate every selected adapter contract before any output is touched, then
194
175
  # snapshot the declared write-set. If a later adapter fails, the EXIT handler
195
176
  # restores all earlier adapter outputs so sync is atomic from the repository's
@@ -211,6 +192,17 @@ SYNC_PARALLEL=1
211
192
  SYNC_BG_PIDS=()
212
193
  SYNC_COPY_INDEXES=()
213
194
 
195
+ # An accepted output-root directory link is a write path, unlike a nested
196
+ # generated asset link. Checks must retain the bytes behind that root too.
197
+ snapshot_sync_entry() {
198
+ local src="$1" index="$2"
199
+ cp -a "$src" "$SYNC_TX_DIR/data/$index" || return $?
200
+ if [ -n "${IS_SYNC_CHECK:-}" ] && [ -L "$src" ] && [ -d "$src" ]; then
201
+ mkdir -p "$SYNC_TX_DIR/targets/$index" || return $?
202
+ cp -a "$src/." "$SYNC_TX_DIR/targets/$index" || return $?
203
+ fi
204
+ }
205
+
214
206
  snapshot_sync_path() {
215
207
  local adapter_name="$1" rel="$2" src index present=0
216
208
  case "$SYNC_TX_SEEN_LIST" in
@@ -224,11 +216,11 @@ snapshot_sync_path() {
224
216
  # Copies of distinct paths into distinct slots: they can overlap, and
225
217
  # The snapshot wait before the render collects every status.
226
218
  if [ "$SYNC_PARALLEL" = 1 ]; then
227
- cp -a "$src" "$SYNC_TX_DIR/data/$index" 2> "$SYNC_TX_DIR/copy.$index.err" &
219
+ snapshot_sync_entry "$src" "$index" 2> "$SYNC_TX_DIR/copy.$index.err" &
228
220
  SYNC_BG_PIDS+=("$!")
229
221
  SYNC_COPY_INDEXES+=("$index")
230
222
  else
231
- cp -a "$src" "$SYNC_TX_DIR/data/$index"
223
+ snapshot_sync_entry "$src" "$index"
232
224
  fi
233
225
  present=1
234
226
  fi
@@ -274,6 +266,68 @@ wait_sync_jobs() {
274
266
  return "$first"
275
267
  }
276
268
 
269
+ # Compare each entry without following links: copied relative and dangling
270
+ # links describe their targets, not bytes at the snapshot's new location.
271
+ compare_sync_entries() {
272
+ local snap="$1" live="$2" rc=0 target before
273
+ if [ -L "$snap" ]; then
274
+ [ -L "$live" ] || return 1
275
+ before="$(readlink "$snap")" || return 2
276
+ target="$(readlink "$live")" || return 2
277
+ # Source links inside the private copy refer to its root. Compare the
278
+ # generated target in the original project's coordinate system.
279
+ if [ -n "${IS_SYNC_CHECK_ROOT:-}" ] && [ -n "${IS_SYNC_CHECK:-}" ]; then
280
+ case "$before" in "$REPO_ROOT") before="$IS_SYNC_CHECK_ROOT" ;; "$REPO_ROOT"/*) before="$IS_SYNC_CHECK_ROOT/${before#"$REPO_ROOT/"}" ;; esac
281
+ case "$target" in "$REPO_ROOT") target="$IS_SYNC_CHECK_ROOT" ;; "$REPO_ROOT"/*) target="$IS_SYNC_CHECK_ROOT/${target#"$REPO_ROOT/"}" ;; esac
282
+ fi
283
+ [ "$before" = "$target" ] || return 1
284
+ elif [ -L "$live" ]; then
285
+ return 1
286
+ elif [ -f "$snap" ] && [ -f "$live" ]; then
287
+ cmp -s "$snap" "$live" || rc=$?
288
+ case "$rc" in 0) ;; 1) return 1 ;; *) return 2 ;; esac
289
+ if [ -x "$snap" ]; then [ -x "$live" ] || return 1; else [ ! -x "$live" ] || return 1; fi
290
+ elif [ -d "$snap" ] && [ -d "$live" ]; then
291
+ return 0
292
+ else
293
+ return 1
294
+ fi
295
+ }
296
+
297
+ compare_sync_snapshot() {
298
+ local index rel present live snap entry sub
299
+ while IFS=$'\t' read -r index rel present; do
300
+ [ -n "$rel" ] || continue
301
+ live="$REPO_ROOT/$rel"
302
+ snap="$SYNC_TX_DIR/data/$index"
303
+ if [ "$present" != 1 ]; then
304
+ if [ -e "$live" ] || [ -L "$live" ]; then return 1; fi
305
+ continue
306
+ fi
307
+ compare_sync_entries "$snap" "$live" || return $?
308
+ if [ -d "$SYNC_TX_DIR/targets/$index" ]; then
309
+ # validate_output_path permitted this physical directory inside
310
+ # the private project. Only its root is followed; descendants
311
+ # still use the entry-kind and literal-target comparison.
312
+ snap="$SYNC_TX_DIR/targets/$index"
313
+ live="$(cd "$live" && pwd -P)" || return 2
314
+ fi
315
+ [ ! -L "$snap" ] && [ -d "$snap" ] || continue
316
+ # NUL-delimited paths also preserve whitespace and newlines. find's
317
+ # default walk does not dereference links on GNU or BSD userland.
318
+ find "$snap" -print0 > "$SYNC_TX_DIR/snapshot.list" || return 2
319
+ find "$live" -print0 > "$SYNC_TX_DIR/live.list" || return 2
320
+ while IFS= read -r -d '' entry; do
321
+ sub="${entry#"$snap"}"
322
+ compare_sync_entries "$entry" "$live$sub" || return $?
323
+ done < "$SYNC_TX_DIR/snapshot.list"
324
+ while IFS= read -r -d '' entry; do
325
+ sub="${entry#"$live"}"
326
+ [ -e "$snap$sub" ] || [ -L "$snap$sub" ] || return 1
327
+ done < "$SYNC_TX_DIR/live.list"
328
+ done < "$SYNC_TX_INDEX"
329
+ }
330
+
277
331
  finish_sync_transaction() {
278
332
  local rc=$?
279
333
  trap - EXIT INT TERM
@@ -305,6 +359,20 @@ note_agents_dependent() {
305
359
  }
306
360
  SELECTED_RECORDS=()
307
361
  RENDER_PARALLEL="$SYNC_PARALLEL"
362
+ # What a source may render into depends on Git policy only when the catalog
363
+ # holds a local artifact: then every contract is read with the manifest, which
364
+ # may choose that policy (targets.copilot.commit_output), and its ignore and
365
+ # include records become IS_COMMIT_POLICY. Every other sync reads ownership
366
+ # alone, as decision 0020 keeps it (decision 0021).
367
+ policy_manifest=""
368
+ [ "$IS_CAT_NEED_COMMITTED" != 1 ] || policy_manifest="$CONFIG_FILE"
369
+ IS_COMMIT_POLICY=""
370
+ # The tools that read .agents/skills/ and AGENTS.md, by their contracts: a
371
+ # skill lands in .agents/skills/ when it is for any of them, and a shared path
372
+ # that delivers an artifact to a tool outside its targets is named.
373
+ IS_OPEN_SKILLS_READERS=""
374
+ IS_AGENTS_READERS=""
375
+ IS_ENABLED_ADAPTERS=""
308
376
  preflight_idx=0
309
377
  while [ "$preflight_idx" -lt "${#ADAPTERS[@]}" ]; do
310
378
  adapter="${ADAPTERS[$preflight_idx]}"
@@ -315,13 +383,26 @@ while [ "$preflight_idx" -lt "${#ADAPTERS[@]}" ]; do
315
383
  target_output_var "$CONFIG_FILE" "$adapter"
316
384
  output="$IS_TGT_OUTPUT"
317
385
  [ -n "$output" ] || output=".$adapter"
318
- records="$(adapter_contract_records "$adapter" "$adapter_file" "$output")" || exit 1
386
+ records="$(adapter_contract_records "$adapter" "$adapter_file" "$output" "$policy_manifest")" || exit 1
319
387
 
320
388
  if ! adapter_claims_add_records "$adapter" "$records"; then
321
389
  echo "ERROR: $IS_ADAPTER_CLAIM_CONFLICT." >&2
322
390
  echo " Give each adapter its own output: intelligence adapter list" >&2
323
391
  exit 1
324
392
  fi
393
+ IS_ENABLED_ADAPTERS="$IS_ENABLED_ADAPTERS${IS_ENABLED_ADAPTERS:+ }$adapter"
394
+ while IFS=$'\t' read -r kind value; do
395
+ case "$kind" in
396
+ requires) [ "$value" != agents ] || IS_AGENTS_READERS="$IS_AGENTS_READERS${IS_AGENTS_READERS:+ }$adapter" ;;
397
+ ignore|include) IS_COMMIT_POLICY="$IS_COMMIT_POLICY$adapter"$'\t'"$kind"$'\t'"$value"$'\n' ;;
398
+ owned|managed)
399
+ adapter_contract_rel_path "$value"
400
+ if [ "$IS_ADAPTER_REL_PATH" = ".agents/skills" ]; then
401
+ IS_OPEN_SKILLS_READERS="$IS_OPEN_SKILLS_READERS${IS_OPEN_SKILLS_READERS:+ }$adapter"
402
+ fi
403
+ ;;
404
+ esac
405
+ done <<< "$records"
325
406
 
326
407
  if [ -n "$TARGET_FILTER" ] && [ "$adapter" != "$TARGET_FILTER" ]; then
327
408
  # Not selected: its claims are registered, its output is not touched.
@@ -356,6 +437,13 @@ while [ "$preflight_idx" -lt "${#ADAPTERS[@]}" ]; do
356
437
  esac
357
438
  done <<< "$records"
358
439
  done
440
+ # shellcheck disable=SC2034 # read by output_committed in lib/common.sh
441
+ [ -z "$policy_manifest" ] || IS_COMMIT_POLICY_SET=1
442
+ # A local artifact that names a committed output in its targets is refused
443
+ # before any adapter writes; then every shared path that delivers an artifact
444
+ # beyond its targets, and every target no channel reaches, is named once.
445
+ source_catalog_check_local "$REPO_ROOT" || exit 1
446
+ source_catalog_warn "$REPO_ROOT"
359
447
 
360
448
  # Every adapter that requires `agents` skips always-on rules because AGENTS.md
361
449
  # carries them — and each of those tools reads AGENTS.md at the workspace root
@@ -625,6 +713,25 @@ if [ $synced -eq 0 ]; then
625
713
  exit 1
626
714
  fi
627
715
 
716
+ # A check renders exactly like a sync, then compares every snapshotted path
717
+ # with what the render left and restores the snapshot whatever the answer: it
718
+ # never changes a file. The transaction stays active until the restore is
719
+ # complete, so an interrupted restore is finished by the EXIT handler.
720
+ if [ -n "${IS_SYNC_CHECK:-}" ]; then
721
+ check_rc=0
722
+ compare_sync_snapshot || check_rc=$?
723
+ case "$check_rc" in
724
+ 0) check_verdict=same ;;
725
+ 1) check_verdict=differs ;;
726
+ *)
727
+ echo "ERROR: sync --check could not compare the rendered output with its snapshot." >&2
728
+ exit 1
729
+ ;;
730
+ esac
731
+ restore_sync_snapshot
732
+ printf '%s\n' "$check_verdict" > "$IS_SYNC_CHECK"
733
+ fi
734
+
628
735
  SYNC_TX_ACTIVE=0
629
736
 
630
737
  # 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.21.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"
@@ -141,22 +141,26 @@ The engine has already loaded `engine/lib/common.sh` before it sources the adapt
141
141
 
142
142
  `sources.rules`, `sources.agents` and `sources.skills` contain repo-relative directory paths. Packages have already been resolved, fetched and pinned by the CLI, so package content appears as an ordinary path under `.intelligence/packages/`. The manifest may name a package's directory by the package's full name or alias instead; `read_yaml_list` and `load_yaml_list` hand an adapter the store path either way, and leave out a reference that names nothing, so an adapter reads every source the same way. Adapters never perform network access or parse `packages:`.
143
143
 
144
- Iterate a source section in manifest order:
144
+ Read a section through the source catalog, never with a glob of your own. `AGENTS.md`, every built-in adapter and the template read it the same way (decision 0021):
145
145
 
146
146
  ```bash
147
- while IFS= read -r src; do
148
- [ -n "$src" ] || continue
149
- dir="$(resolve_source_dir "$repo_root" "$src")"
150
- [ -d "$dir" ] || continue
151
-
152
- for file in "$dir"/*.md; do
153
- [ -f "$file" ] || continue
154
- # transform file
155
- done
156
- done < <(read_yaml_list "$config_file" "rules")
147
+ output_rel_var "$repo_root" "$output_dir"
148
+ source_artifacts_var "$repo_root" "$config_file" rules mytool "$IS_OUTPUT_REL/rules/{}.md"
149
+ for ((i = 0; i < ${#IS_SOURCE_FILES[@]}; i++)); do
150
+ # IS_SOURCE_FILES[i] is the source, IS_SOURCE_NAMES[i] its rendered name,
151
+ # IS_SOURCE_SCOPED[i] is 1 for a rule with paths:
152
+ :
153
+ done
157
154
  ```
158
155
 
159
- Later sources intentionally overwrite same-named artifacts from earlier sources. Package sources are wired before project sources, so project content can override a package artifact by name.
156
+ The last argument is the repository-relative path an artifact lands at, `{}` standing for its name. The catalog lists sources in manifest order and each directory's entries in byte order, and settles four things before any adapter runs:
157
+
158
+ - **Names.** An artifact is named by its file name without a leading `.`, `.md` and a trailing `.local`, a skill by its directory name without the leading `.` and `.local`: `.draft.md` renders as `draft`, `deploy.local/` as `deploy/`. Write each output under `IS_SOURCE_NAMES`, never under the source file name; `finalize_copy_named` and `copy_skill_bundle_dirs_named` take the `IS_SOURCE_NAME_MAP` the call leaves.
159
+ - **Overrides.** Of two artifacts with one name, only the later source's is listed. Package sources are wired before project sources, so project content overrides a package artifact by name. Two of one name in one directory, or a local and a non-local one anywhere, refuse the sync before any adapter runs.
160
+ - **Local artifacts.** A `*.local.md` rule or agent and a `*.local/` skill are listed only for an output path no `ignore` record of your contract covers — what Git tracks never holds one. That is why the path must be the real one: an adapter that globs sources itself, or names a path it does not write, can put a teammate's local file into a committed output.
161
+ - **Targets.** An artifact whose `targets:` leaves your adapter out, or whose `exclude_targets:` names it, is not listed for it; `committed` in either field is resolved against the path you pass ([Selecting adapters](conventions.md#selecting-adapters)). Your adapter's id is a valid target as soon as its file exists, and sync has removed both fields from every copy the shared helpers write; strip them from any frontmatter you copy yourself.
162
+
163
+ `IS_SOURCE_IN_AGENTS` is 1 when `AGENTS.md` carries the artifact too. An adapter whose tool reads `AGENTS.md` renders an always-on rule only where it is 0: that is a local rule, or one whose targets leave `agents` out, and the tool's own always-on channel is the only way it arrives.
160
164
 
161
165
  ## Shared functions
162
166
 
@@ -164,6 +168,10 @@ Use the engine library instead of copying parsers or file-handling logic.
164
168
 
165
169
  | Function | Purpose |
166
170
  |---|---|
171
+ | `source_artifacts_var(root, config, section, adapters, output)` | The artifacts of a section that render for one of `adapters` at `output` (`{}` is the name): `IS_SOURCE_FILES`, `IS_SOURCE_NAMES`, `IS_SOURCE_SCOPED`, `IS_SOURCE_IN_AGENTS`, `IS_SOURCE_NAME_MAP`. |
172
+ | `output_rel_var(root, output_dir)` | The output's repository-relative path in `IS_OUTPUT_REL`, as `source_artifacts_var` takes it. |
173
+ | `finalize_copy_named(dest, map, src...)` | Copy-and-finalize every source into `dest` under its rendered name, in one process. |
174
+ | `copy_skill_bundle_dirs_named(tool, dest_root, map, src...)` | `copy_skill_bundle_dirs_for` with each skill copied under its rendered name. |
167
175
  | `resolve_source_dir(repo_root, source)` | Resolve a manifest source to its local directory. |
168
176
  | `read_yaml_list(config, section)` | Stream entries from `sources.<section>`, package references as their store paths. |
169
177
  | `load_yaml_list(config, section)` | Same list into `IS_YAML_LIST`, cached — no subprocess on repeat reads. |
@@ -195,23 +203,27 @@ Prefer the batched forms inside per-file loops: a process spawn costs tens of mi
195
203
 
196
204
  Rules without `paths:` are always-on. Rules with `paths:` are scoped.
197
205
 
198
- Intelligence routes always-on rules once through `AGENTS.md` for tools that consume it. An adapter for such a tool should not copy those rules again. If the tool has a native scoped-rule channel, transform `paths:` to the tool's equivalent.
206
+ Intelligence routes always-on rules once through `AGENTS.md` for tools that consume it. An adapter for such a tool should not copy those rules again — except one `AGENTS.md` does not carry (`IS_SOURCE_IN_AGENTS` is 0), which reaches the tool only through its own always-on channel. If the tool has a native scoped-rule channel, transform `paths:` to the tool's equivalent.
199
207
 
200
- | Built-in | Always-on rules | Scoped rules |
201
- |---|---|---|
202
- | `agents` | Inline in `AGENTS.md` | List with links |
203
- | `antigravity` | Omit; reads `AGENTS.md` | `.agents/rules/*.md` with `trigger: glob` and `globs:` |
204
- | `claude` | Copy | Copy with `paths:` preserved |
205
- | `cursor` | Omit; reads `AGENTS.md` | `.mdc` with `globs:` |
206
- | `copilot` | Omit; reads `AGENTS.md` | `.instructions.md` with `applyTo:` |
207
- | `codex` | Omit; reads `AGENTS.md` | No native channel |
208
- | `pi` | Omit; reads `AGENTS.md` | Generated on-demand files and extension |
209
- | `opencode` | Omit; reads `AGENTS.md` | No native channel |
208
+ | Built-in | Always-on rules | Always-on, not in `AGENTS.md` | Scoped rules |
209
+ |---|---|---|---|
210
+ | `agents` | Inline in `AGENTS.md` | — | List with links |
211
+ | `antigravity` | Omit; reads `AGENTS.md` | `.agents/rules/*.md` with `trigger: always_on` | `.agents/rules/*.md` with `trigger: glob` and `globs:` |
212
+ | `claude` | Copy | Copy | Copy with `paths:` preserved |
213
+ | `cursor` | Omit; reads `AGENTS.md` | `.mdc` with `alwaysApply: true` | `.mdc` with `globs:` |
214
+ | `copilot` | Omit; reads `AGENTS.md` | `.instructions.md` with `applyTo: "**"` | `.instructions.md` with `applyTo:` |
215
+ | `codex` | Omit; reads `AGENTS.md` | `AGENTS.override.md`: all of `AGENTS.md` plus these rules | No native channel |
216
+ | `pi` | Omit; reads `AGENTS.md` | Added to the system prompt by the generated extension | Generated on-demand files and extension |
217
+ | `opencode` | Omit; reads `AGENTS.md` | No channel; sync warns | No native channel |
218
+
219
+ Codex reads `AGENTS.override.md` instead of `AGENTS.md` in the same directory, so the Codex adapter writes it — the whole rendered `AGENTS.md`, then the rules only Codex receives — while such a rule exists, and removes it when none does. Its contract manages and ignores the path; a file there that Intelligence did not generate is never touched, and a render that needs the path refuses until it is moved.
210
220
 
211
221
  If a new adapter relies on `AGENTS.md` for always-on rules, its target must require `agents`. Add that invariant to `engine/sync.sh` when contributing the adapter upstream.
212
222
 
213
223
  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
224
 
225
+ 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.
226
+
215
227
  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
228
 
217
229
  The Antigravity adapter checks the 12,000-character limit its documentation
@@ -237,7 +249,9 @@ Skills follow the [Agent Skills standard](https://agentskills.io). Copy each ski
237
249
  copy_skill_bundle "$source_skill_dir" "$output_dir/skills/$skill_name"
238
250
  ```
239
251
 
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.
252
+ 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).
253
+
254
+ `source_artifacts_var ... skills` lists only the skills sync renders: one 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. Copy them with `copy_skill_bundle_dirs_named` and the call's `IS_SOURCE_NAME_MAP`, so `deploy.local/` lands as `deploy/`.
241
255
 
242
256
  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
257
 
@@ -303,21 +317,16 @@ This example copies rules without frontmatter. A real adapter must decide how th
303
317
  #!/bin/bash
304
318
 
305
319
  sync_mytool_rules() {
306
- local repo_root="$1" config_file="$2" output_dir="$3"
320
+ local repo_root="$1" config_file="$2" output_dir="$3" i out
307
321
  mkdir -p "$output_dir/rules"
308
322
 
309
- while IFS= read -r src; do
310
- [ -n "$src" ] || continue
311
- local dir
312
- dir="$(resolve_source_dir "$repo_root" "$src")"
313
- [ -d "$dir" ] || continue
314
-
315
- for file in "$dir"/*.md; do
316
- [ -f "$file" ] || continue
317
- strip_frontmatter "$file" > "$output_dir/rules/$(basename "$file")"
318
- finalize_output_file "$output_dir/rules/$(basename "$file")"
319
- done
320
- done < <(read_yaml_list "$config_file" "rules")
323
+ output_rel_var "$repo_root" "$output_dir"
324
+ source_artifacts_var "$repo_root" "$config_file" rules mytool "$IS_OUTPUT_REL/rules/{}.md"
325
+ for ((i = 0; i < ${#IS_SOURCE_FILES[@]}; i++)); do
326
+ out="$output_dir/rules/${IS_SOURCE_NAMES[i]}.md"
327
+ strip_frontmatter "${IS_SOURCE_FILES[i]}" > "$out"
328
+ finalize_output_file "$out"
329
+ done
321
330
  }
322
331
 
323
332
  adapter_contract_mytool() {
@@ -347,6 +356,7 @@ Verify:
347
356
 
348
357
  - native file names, frontmatter and model/permission mappings;
349
358
  - scoped and always-on routing without duplicated context;
359
+ - a dotfile or `*.local.md` source renders under its name, and a local one only into outputs the contract ignores;
350
360
  - skill resources are present;
351
361
  - no literal layout tokens remain;
352
362
  - hand-authored siblings under the tool root survive;
@@ -366,6 +376,6 @@ For a built-in adapter, add the same assertions to the repository's smoke or lif
366
376
  | `claude` | `.claude/rules`, `.claude/agents`, `.claude/skills` |
367
377
  | `cursor` | `.cursor/rules`, `.cursor/agents`, `.cursor/skills` |
368
378
  | `copilot` | `.github/instructions`, `.github/agents`, `.github/skills` |
369
- | `codex` | `.codex/agents`, `.agents/skills` |
379
+ | `codex` | `.codex/agents`, `.agents/skills`, marker-owned `AGENTS.override.md` |
370
380
  | `pi` | `.pi/intelligence-sync`, `.pi/extensions`, `.pi/prompts`, `.agents/skills` |
371
381
  | `opencode` | `.opencode/agents`, marker-owned `.opencode/commands`, `.agents/skills` |
@@ -73,7 +73,7 @@ sources:
73
73
 
74
74
  A package's directory may also be named by the package instead of its store path: `@ainova-systems/sync/rules` names folder `rules` of the package `packages:` declares as `@ainova-systems/sync`, and `sync:rules` names it through the alias that package declares (`intelligence package alias @ainova-systems/sync sync`). Both render exactly as the store path, and both are installed package content like it: edit that content in its source repository. The CLI writes store paths and never rewrites a reference a person wrote; an `@scope/name/...` entry for a package `packages:` does not declare is an ordinary project path.
75
75
 
76
- Missing project-owned source directories are skipped, so a package-only project need not create empty `rules/`, `agents/` or `skills/` directories. Source order matters: later files with the same artifact name overwrite earlier ones. Package sources are wired before project sources so the project can override a package artifact deliberately.
76
+ Missing project-owned source directories are skipped, so a package-only project need not create empty `rules/`, `agents/` or `skills/` directories. Source order matters: of two artifacts with one name, the later source's renders everywhere, `AGENTS.md` included, and the earlier one renders nowhere (see [Artifact names](#artifact-names-and-local-artifacts)). Package sources are wired before project sources so the project can override a package artifact deliberately.
77
77
 
78
78
  Project-owned entries are managed with `intelligence source add|remove|list` rather than by hand — it validates the path against the way the engine resolves it and prints the resulting order. `add` appends by default; `--before <entry>` / `--after <entry>` place content that should behave like a package ahead of the project's own directories.
79
79
 
@@ -164,6 +164,41 @@ Common skill verbs:
164
164
 
165
165
  The verb describes the outcome, not whether the skill implements the work or delegates to another command or skill.
166
166
 
167
+ ## Artifact names and local artifacts
168
+
169
+ An artifact's name is its file name without a leading `.`, `.md` and a trailing `.local`; a skill's is its directory name without the leading `.` and the trailing `.local`. `backend.md`, `.backend.md` and `backend.local.md` are all the rule `backend`, `deploy.local/` is the skill `deploy`, and every generated copy carries the name: `.claude/rules/backend.md`, `.cursor/rules/backend.mdc`, `.claude/skills/deploy/`. A leading dot means nothing — a dotfile source renders like any other.
170
+
171
+ Names stay unique. Across sources the later source's artifact renders and the earlier one does not, in `AGENTS.md` as in every tool's output; a skill a later source overrides is replaced whole, resources included. Sync refuses two artifacts with one name in one source directory (`backend.md` beside `.backend.md`), and a local and a non-local artifact with one name anywhere in the sources: a local file that overrode or yielded to a committed one would change what committed output holds.
172
+
173
+ A **local** artifact is personal or machine-specific guidance — the OS, the shell, a local service, a personal habit — that only this clone loads: a rule or agent named `*.local.md`, or a skill directory named `*.local/`. `intelligence init` writes the matching patterns, for the project's content directory, into its managed `.gitignore` block, so Git and sync agree on which files are local:
174
+
175
+ ```gitignore
176
+ intelligence/rules/*.local.md
177
+ intelligence/agents/*.local.md
178
+ intelligence/skills/*.local/
179
+ ```
180
+
181
+ Sync renders a local artifact only into outputs Git ignores, and never into one it tracks: not `AGENTS.md`, not OpenCode's slash commands, not Copilot's output under `commit_output: true`. A clone without the file therefore renders the same committed files, and `intelligence sync --check` passes in CI. A local always-on rule reaches each tool through that tool's own always-on channel (see [Routing](#routing)), since `AGENTS.md` cannot carry it; OpenCode has none, and sync says so. `intelligence status --check` reports a missing pattern, and `intelligence init` restores it.
182
+
183
+ A pattern never applies to a file Git already tracks, and a source outside the content directory has no pattern at all. `intelligence status --check` therefore names every file with a local name that Git tracks in a project-owned source: rename it to share it, or `git rm --cached` it to keep it on this machine.
184
+
185
+ ## Selecting adapters
186
+
187
+ Every rule, agent and skill renders to every enabled adapter. Two frontmatter fields narrow that, for guidance that matters to one tool or should cost no other tool context:
188
+
189
+ ```yaml
190
+ ---
191
+ targets: [claude, cursor] # only these adapters; without it, every enabled one
192
+ exclude_targets: [committed] # remove these from that set
193
+ ---
194
+ ```
195
+
196
+ Both take adapter ids — built-in or the project's own — and the group `committed`: every output path Git tracks, resolved per path from each adapter's contract. `AGENTS.md` always belongs to it, and so do OpenCode's slash commands and, under `commit_output: true`, Copilot's output; OpenCode's agents and the shared `.agents/skills/` do not. Write them in the list syntax `paths:` takes — a flow list `[a, b]` or a block list — and in `SKILL.md` for a skill. `targets: []` renders the artifact nowhere. Sync removes both fields from every generated copy.
197
+
198
+ An id that names no adapter refuses the sync. An adapter that is not enabled is simply no target: enablement stays the manifest's decision. A local artifact excludes `committed` already, and one whose `targets` names a committed output — `agents`, `committed`, or an adapter all of whose output Git tracks — refuses the sync, because every other clone and CI render that output without it.
199
+
200
+ Two paths are shared, not per tool, and a filter can withhold an artifact from a shared path but never from one of its readers. An artifact whose set contains `agents` goes into `AGENTS.md` and reaches every tool that reads it. A skill whose set contains any reader of `.agents/skills/` — Antigravity, Codex, Pi, OpenCode — lands there for all of them. Sync prints a `WARNING:` line when a shared path delivers an artifact to an enabled adapter outside its set, and when an adapter in its set can receive it through no channel. An artifact with neither field nor a `.local` name keeps the routing, and the warnings, it always had.
201
+
167
202
  ## Agent conventions
168
203
 
169
204
  ```yaml
@@ -246,9 +281,12 @@ Rule content in Markdown.
246
281
  |---|---|---|---|---|---|
247
282
  | Scoped | copied with `paths:` | `.mdc` with `globs:` | `.md` with `trigger: glob` and `globs:` | `.instructions.md` with `applyTo:` | listed in `AGENTS.md`; Pi also gets on-demand files and an extension |
248
283
  | Always-on | copied | omitted | omitted | omitted | inlined once into `AGENTS.md` |
284
+ | Always-on, not in `AGENTS.md` | copied | `.mdc` with `alwaysApply: true` | `.md` with `trigger: always_on` | `.instructions.md` with `applyTo: "**"` | Codex: `AGENTS.override.md`; Pi: added to the system prompt by its extension; OpenCode: none |
249
285
 
250
286
  Cursor, Copilot, Codex, Pi, OpenCode and Antigravity consume `AGENTS.md`, so always-on rules are not duplicated in their tool-specific channels. Claude does not consume `AGENTS.md`, so it receives the full rule set. OpenCode and Codex have no generated path-scoped rule channel; OpenCode users may configure `instructions:` globs themselves.
251
287
 
288
+ An always-on rule `AGENTS.md` does not carry — a local one, or one whose set leaves `agents` out (see [Selecting adapters](#selecting-adapters)) — reaches each tool in its set through that tool's own channel instead, the last row. Codex reads `AGENTS.override.md` instead of `AGENTS.md` in the same directory, so the file holds the whole `AGENTS.md` plus the rules only Codex receives; it is ignored, exists only while such a rule does, and one Intelligence did not generate is never touched. Antigravity's `trigger: always_on` belongs to the same undocumented key family as its `trigger: glob`. OpenCode reads instructions only from `AGENTS.md` or its committed `opencode.json`, so it can receive neither a local nor a tool-targeted always-on rule.
289
+
252
290
  Keep always-on rules small. Put narrow framework or component guidance behind `paths:` so unrelated tasks do not pay its context cost.
253
291
 
254
292
  ## Skill conventions
@@ -270,7 +308,7 @@ argument-hint: "<route-name>"
270
308
  4. Report the changed route and verification.
271
309
  ```
272
310
 
273
- Standard optional fields (`license`, `compatibility`, `metadata`, `allowed-tools`) and tool extensions pass through unchanged. A tool ignores fields it does not understand. `effort:` is the exception: sync renders it per tool, as [Effort mappings](#effort-mappings) describes.
311
+ Standard optional fields (`license`, `compatibility`, `metadata`, `allowed-tools`) and tool extensions pass through unchanged. A tool ignores fields it does not understand. `effort:` is the exception: sync renders it per tool, as [Effort mappings](#effort-mappings) describes. `targets:` and `exclude_targets:` select the adapters that receive the skill and are removed from every copy ([Selecting adapters](#selecting-adapters)).
274
312
 
275
313
  `disable-model-invocation: true` makes a skill one only the owner starts, as a slash command: use it for a procedure that changes versions, the lock or generated outputs, where an agent selecting it unasked is the failure. Claude Code, Cursor and Copilot read the field itself; Codex does not, so sync writes `agents/openai.yaml` with `allow_implicit_invocation: false` beside the skill in `.agents/skills/`. Keep the source to the field. A skill that ships its own `agents/openai.yaml` keeps everything else in it: sync adds the policy when the file sets none, and refuses one that sets it otherwise or not as a plain `allow_implicit_invocation: false` directly under `policy:`.
276
314
 
@@ -311,6 +349,8 @@ skill-name/
311
349
 
312
350
  The engine copies the complete skill directory. Promote a helper outside the skill only when multiple skills share it.
313
351
 
352
+ 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.
353
+
314
354
  Size limits are backstops, not quotas:
315
355
 
316
356
  | Artifact | Hard cap | Response |
@@ -336,12 +376,12 @@ Every line enters a finite context budget. Prefer subtraction, consolidation and
336
376
  | Target | Rules | Skills | Agents |
337
377
  |---|---|---|---|
338
378
  | `agents` | Always-on inlined; scoped listed in `AGENTS.md` | Listed | Listed |
339
- | Antigravity | `AGENTS.md` plus scoped `.agents/rules/*.md` | `.agents/skills/` | `.agents/agents/*.md` |
379
+ | Antigravity | `AGENTS.md` plus `.agents/rules/*.md` | `.agents/skills/` | `.agents/agents/*.md` |
340
380
  | Claude | `.claude/rules/` | `.claude/skills/` | `.claude/agents/` |
341
- | Cursor | scoped `.cursor/rules/*.mdc` | `.cursor/skills/` | `.cursor/agents/` |
342
- | Copilot | scoped `.github/instructions/*.instructions.md` | `.github/skills/` | `.github/agents/` |
343
- | Codex | `AGENTS.md` only | `.agents/skills/` | `.codex/agents/*.toml` |
344
- | Pi | `AGENTS.md` plus scoped `.pi/intelligence-sync/rules/` | `.agents/skills/` | `.pi/prompts/intelligence-agent-*.md` |
381
+ | Cursor | `AGENTS.md` plus `.cursor/rules/*.mdc` | `.cursor/skills/` | `.cursor/agents/` |
382
+ | Copilot | `AGENTS.md` plus `.github/instructions/*.instructions.md` | `.github/skills/` | `.github/agents/` |
383
+ | Codex | `AGENTS.md`, or `AGENTS.override.md` while a rule needs it | `.agents/skills/` | `.codex/agents/*.toml` |
384
+ | Pi | `AGENTS.md` plus scoped `.pi/intelligence-sync/rules/` and its extension | `.agents/skills/` | `.pi/prompts/intelligence-agent-*.md` |
345
385
  | OpenCode | `AGENTS.md` only | `.agents/skills/` plus slash commands | `.opencode/agents/*.md` |
346
386
 
347
387
  `AGENTS.md` is regenerated by the `agents` adapter. Its optional static header is `targets.agents.header` in `intelligence.yaml`; generated rule, agent and skill sections follow it. Commit `AGENTS.md` when it is the project's shared canonical context.
@@ -352,7 +392,12 @@ By default, commit the manifest, lock, project-owned content and `AGENTS.md`. Ig
352
392
  # CLI-managed package store
353
393
  .intelligence/
354
394
 
355
- # Local root instructions migrate into project rules; local preferences stay ignored
395
+ # Local rules, agents and skills of the content directory stay on this machine
396
+ intelligence/rules/*.local.md
397
+ intelligence/agents/*.local.md
398
+ intelligence/skills/*.local/
399
+
400
+ # Root tool instructions migrate into project rules and stay ignored
356
401
  CLAUDE.md
357
402
  .cursorrules
358
403
  GEMINI.md
@@ -372,6 +417,7 @@ GEMINI.md
372
417
  .agents/rules/
373
418
  .agents/agents/
374
419
  .codex/agents/
420
+ AGENTS.override.md
375
421
 
376
422
  # Generated Pi content
377
423
  .pi/intelligence-sync/
@@ -428,9 +474,10 @@ than inferring contents from Git status.
428
474
  move useful repository guidance from legacy root files such as `.cursorrules`
429
475
  and instruction-bearing `CLAUDE.md` out of their quarantined backup copies and
430
476
  into project-owned rules, then verify the generated tool output. Do not restore
431
- the original root monolith. Create a new root tool-specific file only when it
432
- contains genuinely local configuration that an adapter cannot represent; keep
433
- that exception gitignored rather than maintaining a second committed source.
477
+ the original root monolith. Genuinely machine-local guidance becomes a local
478
+ rule, agent or skill (see
479
+ [Artifact names and local artifacts](#artifact-names-and-local-artifacts)), not
480
+ a new root tool-specific file.
434
481
 
435
482
  Before the first render, `intelligence init` preserves existing AI prompt paths
436
483
  under `<content-dir>/_backup/`. Its `manifest.tsv` labels the snapshot
@@ -458,7 +505,7 @@ The permanent applied-schema key is the top-level scalar `schema_version` in `in
458
505
  The public lifecycle is deliberately compact:
459
506
 
460
507
  - `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.
508
+ - `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
509
  - `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
510
  - `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
511
  - `intelligence package add|remove|list|search` owns package inventory.
@@ -479,6 +526,7 @@ The engine emits one machine-readable line, `IS_STATUS=<code> [IS_DETAIL=...]`,
479
526
  | `migrated` | 0 | Initialization converted an older layout |
480
527
  | `error` | 1 | Generic failure |
481
528
  | `config-missing` | 2 | Required manifest is absent |
529
+ | `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
530
  | `ambiguous` | 3 | Conflicting state requiring agent/human judgment; reserved |
483
531
  | `ahead-of-engine` | 4 | Manifest schema is a newer major than the engine (a newer minor or patch warns and continues) |
484
532
  | `aborted-incomplete` | 5 | Staged replacement was incomplete; prior state remains |
@@ -493,6 +541,7 @@ Callers capture the real code with `command || rc=$?`. Do not use `if ! command;
493
541
  | `intelligence.yaml` | Manifest and schema contract | Tracked |
494
542
  | `intelligence.lock` | Resolved package state | Tracked |
495
543
  | `<content-dir>/{rules,agents,skills,adapters}/` | Project source of truth | Tracked |
544
+ | `<content-dir>/{rules,agents}/*.local.md`, `<content-dir>/skills/*.local/` | Local artifacts, this clone only | Ignored |
496
545
  | `.intelligence/` | Restorable package store | Ignored |
497
546
  | `AGENTS.md` | Generated canonical project context | Normally tracked |
498
547
  | Tool output directories | Generated native content | Built-in adapter-owned paths ignored; shared settings tracked |
@@ -47,7 +47,7 @@ Migrate meaning, not tool syntax:
47
47
 
48
48
  | Existing format | Project-owned destination |
49
49
  |---|---|
50
- | Root `AGENTS.md`, `CLAUDE.md`, `.cursorrules`, Copilot root instructions | Split verified guidance by topic into rules; keep local machine preferences in a gitignored root file only when no adapter representation exists |
50
+ | Root `AGENTS.md`, `CLAUDE.md`, `.cursorrules`, Copilot root instructions | Split verified guidance by topic into rules; move machine-local preferences into local rules (`<content-dir>/rules/<name>.local.md`), which Git ignores and sync keeps out of committed output |
51
51
  | `.claude/rules/*.md` | Rule; preserve valid `paths:` |
52
52
  | `.cursor/rules/*.mdc` | Rule; rename `globs:` to `paths:` and remove `alwaysApply:` |
53
53
  | Claude/Cursor/Copilot agents, Codex `.codex/agents/*.toml` | Agent; map native model/readonly/tool fields back to `tier:` and `access:`, and a native effort level (`effort:`, `model_reasoning_effort`) back to `effort:` |
@@ -64,9 +64,9 @@ Obtain approval per `CREATE`, `UPDATE`, `REMOVE`, or `KEEP` proposal. Apply
64
64
  project-owned source changes first, then run `intelligence sync` and
65
65
  `intelligence status --check`. Inspect the enabled targets to prove the
66
66
  migrated guidance arrived and verify quarantined old root instructions remain
67
- absent. Create a new root tool file only from a separately approved, genuinely
68
- machine-local subset that has no adapter representation; never restore the
69
- original instruction monolith.
67
+ absent. A separately approved, genuinely machine-local subset becomes a local
68
+ rule, `<content-dir>/rules/<name>.local.md`, not a new root tool file; never
69
+ restore the original instruction monolith.
70
70
 
71
71
  For every removed or renamed path, search all tracked files with `git ls-files`
72
72
  and report remaining references with file and line number. Apply an unambiguous