Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -25,3 +25,7 @@ GEMINI.md
.agents/agents/
!.claude/
.antigravity.md
intelligence/rules/*.local.md
intelligence/agents/*.local.md
intelligence/skills/*.local/
AGENTS.override.md
31 changes: 31 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,37 @@ All notable changes to Intelligence are recorded here.

Legacy Intelligence Sync history remains in its [archive](https://github.com/ainova-systems/intelligence-sync/blob/main/CHANGELOG.md).

## [0.21.0]

### Added

- Local rules, agents and skills: a `*.local.md` rule or agent and a `*.local/` skill directory in the content directory render only into outputs Git ignores, never into `AGENTS.md` or another tracked output, so every clone and CI render the same committed files; `.environment.local.md` renders as `environment` (decision 0021).
- `intelligence init` writes `<content-dir>/rules/*.local.md`, `<content-dir>/agents/*.local.md` and `<content-dir>/skills/*.local/` into the managed `.gitignore` block, and `status --check` reports one missing.
- `status --check` names a rule, agent or skill with a local name that Git tracks in any project-owned source: `.gitignore` never applies to a tracked file, so Git would share what sync renders only into ignored output.
- `targets:` and `exclude_targets:` in a rule's, agent's or `SKILL.md`'s frontmatter select the adapters it renders for: adapter ids and the group `committed` (every output Git tracks), in the list syntax `paths:` takes. `targets: []` renders nowhere, an unknown id or a local artifact naming a committed output refuses the sync, and both fields are removed from every generated copy (decision 0021).
- An always-on rule `AGENTS.md` does not carry — a local one, or one whose targets leave `agents` out — reaches each tool through its own channel: Cursor `.mdc` with `alwaysApply: true`, Copilot `.instructions.md` with `applyTo: "**"`, Antigravity `trigger: always_on`, Pi's rules extension, and Codex `AGENTS.override.md`, which holds all of `AGENTS.md` plus those rules, is ignored, and exists only while such a rule does (decision 0021).
- Sync prints a `WARNING:` line when `AGENTS.md` or `.agents/skills/` delivers a local or targeted artifact to an enabled tool outside its targets, and when a tool in its targets has no channel for it — OpenCode for an always-on rule outside `AGENTS.md`.

### Changed

- `AGENTS.md`, every built-in adapter and the adapter template read sources through one catalog (`source_artifacts_var`): an artifact is named without a leading `.`, `.md` and a trailing `.local`, so a dotfile source renders in every adapter under that name, not only in `AGENTS.md` (decision 0021).
- A skill a later source overrides is replaced whole in every skill tree; its earlier version's resources no longer merge into the copy.
- Two artifacts with one name in one source directory (`x.md` beside `.x.md`), and a local and a non-local artifact with one name in any sources, refuse the sync before any output is touched, naming both files.
- Pi's scoped-rule extension lists rules in byte order, as `AGENTS.md` does, instead of the locale's glob order.
- The Codex contract manages and ignores `AGENTS.override.md`; with Codex enabled, run `intelligence init` once to add the pattern.

### Fixed

- `AGENTS.md` listed and inlined a same-named rule, agent or skill once per source while every other adapter rendered only the later one; it now applies the same override (decision 0021).
- A rule or agent that is a symlink to a file is listed in `AGENTS.md`, as every other adapter already rendered it.

### Breaking

- [ ] No source directory holds two artifacts whose names differ only by a leading `.` or a trailing `.local`, and no artifact name is both local and committed across the sources: `intelligence sync` succeeds.
- [ ] No file Git tracks in a project source is a rule or agent named `*.local.md` or a skill directory named `*.local/`: such a file now renders only into ignored output and leaves `AGENTS.md`. Rename it to keep it shared, or `git rm --cached` it to keep it on one machine.
- [ ] The CLI-managed block of `.gitignore` lists `<content-dir>/rules/*.local.md`, `<content-dir>/agents/*.local.md` and `<content-dir>/skills/*.local/`, and `AGENTS.override.md` when Codex is enabled — run `intelligence init` to apply it.
- [ ] `intelligence status --check` exits 0.

## [0.20.0]

### Added
Expand Down
5 changes: 3 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -118,8 +118,9 @@ conventions](packages/sync/references/conventions.md#generated-output-and-versio
`AGENTS.md` is the shared root instruction entry point. The learn skill proposes
migrating still-valid guidance from backed-up legacy `.cursorrules` or
instruction-bearing `CLAUDE.md` files into project-owned rules. It never
restores the original root monolith; a small new local file is created only for
separately approved machine-specific guidance.
restores the original root monolith; separately approved machine-specific
guidance becomes a local rule, `intelligence/rules/<name>.local.md`, which Git
ignores and sync renders only into ignored tool output.

Before the first sync can replace adapter-owned directories, `init` preserves
existing AI instructions under the gitignored `intelligence/_backup/` (or the
Expand Down
3 changes: 2 additions & 1 deletion cli/commands/init.sh
Original file line number Diff line number Diff line change
Expand Up @@ -356,7 +356,8 @@ manifest="$root/intelligence.yaml"
} > "$manifest"

# The package store is restorable state, never committed — the npm model.
ensure_base_gitignore "$root"
# Local rules, agents and skills stay on this machine (decision 0021).
ensure_base_gitignore "$root" "$content_dir"
for target in $targets; do
ensure_target_gitignore "$root" "$manifest" "$target"
done
Expand Down
39 changes: 39 additions & 0 deletions cli/internal/check.sh
Original file line number Diff line number Diff line change
Expand Up @@ -198,6 +198,14 @@ while IFS= read -r target; do
ok "adapter $target contract v1"
done < <(target_names "$manifest")

# A local rule, agent or skill is one these patterns name (decision 0021): the
# engine keeps it out of every committed output, and without the pattern Git
# would still offer to commit the file itself.
while IFS= read -r pattern; do
ignore_file_has_line "$IP_ROOT/.gitignore" "$pattern" \
|| warn "local artifact Git policy is missing '$pattern' — run 'intelligence init'"
done < <(local_artifact_ignore_patterns "$content_dir")
Comment thread
dzykovic marked this conversation as resolved.

# Lock orphans.
if [ "$lock_valid" -eq 1 ] && [ -f "$lock" ]; then
while IFS= read -r name; do
Expand All @@ -214,6 +222,7 @@ fi
# is exactly why doctor names it). Each entry is judged by the directory the
# engine renders it as, read through the same parser: a package reference by
# its store path, one that resolves to nothing by why.
local_specs=()
for section in rules agents skills; do
seen=$'\n'
while IFS=$'\037' read -r src state dir alias holders; do
Expand Down Expand Up @@ -269,10 +278,40 @@ for section in rules agents skills; do
# authoring later needs no config edit.
*) ok "sources.$section '$src' — not created yet (optional)" ;;
esac
continue
fi
# A project-owned source is where a tracked file can carry a local
# name; store content is restorable state Git never tracks.
case "$dir" in
.intelligence/*) ;;
*)
if [ "$section" = skills ]; then
local_specs+=(":(glob)${dir%/}/*.local/SKILL.md")
else
local_specs+=(":(glob)${dir%/}/*.local.md")
fi
;;
esac
done < <(read_source_entries "$manifest" "$section")
done

# A name makes an artifact local (decision 0021), and .gitignore never applies
# to a file Git already tracks: one committed before its name meant anything,
# or added by hand, renders only into ignored output while every clone holds
# it. Git and the engine would disagree about it, so it is named here.
if [ "${#local_specs[@]}" -gt 0 ] \
&& git -C "$IP_ROOT" rev-parse --is-inside-work-tree >/dev/null 2>&1; then
if tracked_local="$(git -C "$IP_ROOT" -c core.quotePath=false ls-files -- "${local_specs[@]}")"; then
while IFS= read -r path; do
[ -n "$path" ] || continue
case "$path" in */SKILL.md) path="${path%/SKILL.md}/" ;; esac
warn "$path is tracked by Git, but its name makes it local: sync renders it only into ignored output — rename it to share it, or 'git rm --cached' it to keep it on this machine"
done <<< "$tracked_local"
else
warn "tracked local artifacts could not be listed — 'git ls-files' failed"
fi
fi

if [ "$problems" -eq 0 ]; then
echo "all good."
else
Expand Down
21 changes: 19 additions & 2 deletions cli/lib/gitignore.sh
Original file line number Diff line number Diff line change
Expand Up @@ -312,10 +312,26 @@ ensure_gitignore_header() {
fi
}

# local_artifact_ignore_patterns <content-dir> — the patterns that make a rule,
# agent or skill local (decision 0021): a file or skill directory with one of
# these names never reaches committed output, and Git never commits it. The
# engine recognizes the same names, so the two cannot disagree; `init` writes
# them and `status --check` reports one missing.
local_artifact_ignore_patterns() {
local dir="${1#./}"
while [ "${dir%/}" != "$dir" ]; do dir="${dir%/}"; done
printf '%s\n' "$dir/rules/*.local.md" "$dir/agents/*.local.md" "$dir/skills/*.local/"
Comment thread
dzykovic marked this conversation as resolved.
}

# ensure_base_gitignore <root> <content-dir> — the package store and the local
# artifact patterns of the project's content directory.
ensure_base_gitignore() {
local root="$1"
local root="$1" content_dir="$2" line
ensure_gitignore_header "$root"
gitignore_add_line "$root" '.intelligence/'
while IFS= read -r line; do
gitignore_add_line "$root" "$line"
done < <(local_artifact_ignore_patterns "$content_dir")
}

ensure_target_gitignore() {
Expand Down Expand Up @@ -343,7 +359,7 @@ ensure_target_gitignore() {

ensure_manifest_gitignore() {
local root="$1" manifest="$2" target
ensure_base_gitignore "$root"
ensure_base_gitignore "$root" "$(manifest_intelligence_dir "$manifest")"
while IFS= read -r target; do
[ -n "$target" ] || continue
if [ "$(is_target_enabled "$manifest" "$target")" = "1" ]; then
Expand Down Expand Up @@ -420,6 +436,7 @@ managed_gitignore_patterns() {
local root="$1" manifest="$2" content_dir target output records kind value
content_dir="$(manifest_intelligence_dir "$manifest")"
printf '%s\n' '.intelligence/' "$content_dir/_backup/"
local_artifact_ignore_patterns "$content_dir"
while IFS= read -r target; do
[ -n "$target" ] || continue
[ "$(is_target_enabled "$manifest" "$target")" = "1" ] || continue
Expand Down
3 changes: 3 additions & 0 deletions cli/tests/e2e-compat.sh
Original file line number Diff line number Diff line change
Expand Up @@ -111,6 +111,9 @@ MANIFEST
printf '# Ctx\n\nCOMPAT_MARKER\n' > "$d/intelligence/rules/context.md"
cat > "$d/.gitignore" <<'IGNORE'
.intelligence/
intelligence/rules/*.local.md
intelligence/agents/*.local.md
intelligence/skills/*.local/
CLAUDE.md
.claude/*
!.claude/settings.json
Expand Down
Loading
Loading