From 7730fd75572bef0c2da93490843229dbdad22767 Mon Sep 17 00:00:00 2001 From: Dmitrij Zykovic Date: Sun, 12 Jul 2026 20:45:58 +0100 Subject: [PATCH 1/3] Shipped the intelligence-authoring rule and intelligence-architect agent from the engine, taught adapters to expand layout tokens, and made INIT derive skills from the repository instead of a catalogue as 0.7.0 --- .github/workflows/ci.yml | 24 ++++ CHANGELOG.md | 28 +++++ README.md | 2 + docs/ADAPTERS.md | 14 ++- docs/CONVENTIONS.md | 20 +++- .../config.yaml | 4 +- examples/go-api-with-opencode/config.yaml | 5 +- examples/go-api-with-pi-and-codex/config.yaml | 4 +- examples/go-api/config.yaml | 4 +- examples/platform-with-submodules/config.yaml | 4 +- examples/with-remote-skills/config.yaml | 4 +- intelligence/sync/INIT.md | 54 ++++----- .../sync/agents/intelligence-architect.md | 47 ++++++++ intelligence/sync/docs/ADAPTERS.md | 14 ++- intelligence/sync/docs/CONVENTIONS.md | 20 +++- .../sync/rules/intelligence-authoring.md | 88 +++++++++++++++ intelligence/sync/scripts/VERSION | 2 +- intelligence/sync/scripts/adapters/agents.sh | 12 +- intelligence/sync/scripts/adapters/claude.sh | 4 +- intelligence/sync/scripts/adapters/codex.sh | 2 +- intelligence/sync/scripts/adapters/copilot.sh | 4 +- intelligence/sync/scripts/adapters/cursor.sh | 4 +- .../sync/scripts/adapters/opencode.sh | 4 +- intelligence/sync/scripts/adapters/pi.sh | 6 +- intelligence/sync/scripts/lib/common.sh | 52 ++++++++- intelligence/sync/scripts/lib/migrations.sh | 105 ++++++++++++++---- intelligence/sync/scripts/sync.sh | 10 ++ intelligence/sync/scripts/update.sh | 28 ++++- 28 files changed, 480 insertions(+), 89 deletions(-) create mode 100644 intelligence/sync/agents/intelligence-architect.md create mode 100644 intelligence/sync/rules/intelligence-authoring.md diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 52cab09..2d4928f 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -159,6 +159,30 @@ jobs: grep -q "backend-developer" "$PROJ/AGENTS.md" grep -q "backend-add-endpoint" "$PROJ/AGENTS.md" + - name: Engine-owned rule + agent reach the outputs, with layout tokens expanded + shell: bash + run: | + set -euo pipefail + cd "$PROJ" + # The engine ships intelligence-authoring (rule) + intelligence-architect + # (agent). They must arrive via sources, and every `` / + # `` token must be gone: a literal token in an output means an + # adapter wrote a file without finalize_output_file(). + grep -q "intelligence-authoring" AGENTS.md + grep -q "intelligence-architect" AGENTS.md + if [ -d .claude ]; then + test -f .claude/rules/intelligence-authoring.md + test -f .claude/agents/intelligence-architect.md + grep -q '"intelligence/\*\*"' .claude/rules/intelligence-authoring.md + fi + leaked=$(grep -rl -e '' -e '' \ + .claude .cursor .github/instructions .github/agents .github/skills \ + .codex .agents .pi .opencode AGENTS.md 2>/dev/null || true) + if [ -n "$leaked" ]; then + echo "UNEXPANDED LAYOUT TOKEN shipped into generated output:"; echo "$leaked"; exit 1 + fi + echo "OK: engine rule/agent present, no unexpanded tokens" + - name: Verify Claude / Cursor / Copilot / Codex outputs (if enabled) shell: bash run: | diff --git a/CHANGELOG.md b/CHANGELOG.md index bb74c56..9345723 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -12,6 +12,34 @@ Update intelligence-sync: fetch the latest engine from https://github.com/ainova ## [Unreleased] +## [0.7.0] — 2026-07-12 + +The engine stops being only a pipeline and starts shipping its own **authoring discipline**: a rule the model respects while it works, and an agent that owns the layer's shape. Both are upstream-owned — they evolve with the engine instead of drifting inside each project. + +### Breaking + +- **`config.yaml` must list the module's `rules/` and `agents/` as sources.** The engine now ships artifacts of its own beside the meta-skills, and they only reach the IDEs if `sources` points at them. `migrate_to_0_7_0` adds both entries idempotently (nothing else in `config.yaml` is read or rewritten), exactly as `0.3.1` added the module's `skills/` entry. `sync.sh` fails closed (`IS_STATUS=needs-update`) until the update flow has run — it never migrates. + + Post-conditions to verify after updating: + 1. `sources.rules` contains `//rules` and `sources.agents` contains `//agents` — each exactly once. + 2. `/rules/intelligence-authoring.md` and `/agents/intelligence-architect.md` exist. + 3. `config.yaml` `sync_version` is `0.7.0`. + 4. After a sync, no generated file contains a literal `` or `` token. + + Project content (`rules/`, `agents/`, `skills/`, `adapters/`) is untouched. Re-running is a safe no-op. + +### Added + +- **`intelligence-authoring` — an engine-shipped rule carrying the authoring discipline** (`/rules/`). Path-scoped to the umbrella, so it loads exactly when someone edits the layer and costs nothing otherwise. It is the judgement that sits on top of `CONVENTIONS.md`'s mechanics: which artifact type a piece of knowledge belongs to (a checklist in an agent body is a procedure — it belongs in a skill), why always-on rules must earn their place, why an agent is thin and never restates the rules it already receives, why a skill without a verification step is not a skill, and the invariant that closes the loop on the defect that produced 0.6.0 — *never state behaviour of a tool you have not verified in its documentation or source*. +- **`intelligence-architect` — an engine-shipped agent that designs and prunes the layer** (`/agents/`). Decides rule vs skill vs agent, always-on vs scoped, split vs fold, and what to delete. It carries boundaries and verification only; the rule reaches it on its own, so it does not repeat it. +- **Layout tokens `` and `` in engine-shipped artifacts.** An artifact shipped by the engine cannot hardcode the umbrella's folder name — the project chooses it (`intelligence/`, `Intelligence/`, a codename) — but a scoped rule needs `paths:` to name it. Adapters now expand both tokens on the way out through a single helper (`finalize_output_file`, which replaces `normalize_file_to_lf` at every output site), in frontmatter and body alike: `paths: ["/**"]` arrives as Claude's `paths:`, Cursor's `globs:` and Copilot's `applyTo:` already carrying the real folder name. CI fails the build if any generated output still contains a literal token. +- **`update.sh` owns the module's `rules/` and `agents/`** the way it already owns `docs/` and the meta-skills: staged from the fresh upstream clone, shown in the diff before the confirmation prompt, then applied. Your own `/rules` and `/agents` are never touched. + +### Changed + +- **INIT no longer suggests skills from a catalogue.** §3.5 listed a menu of plausible skill names (`add-entity`, `add-endpoint`, `run-tests`, …), and an agent working from a menu produces a registry that describes software in general rather than the repository in front of it — every entry costing registry budget forever, whether or not anyone invokes it. A skill must now clear four bars — **repeated** (with the instance in the repo named as evidence), **multi-step and mechanical**, **verifiable** (it ends in a check that proves it worked; if there is nothing to verify, it is not a skill), and **stable** (a procedure that only routes around a current bug belongs in a rule that records known breakage, not in a skill that makes the workaround permanent) — and bootstrap now targets **0–3 skills**, with zero stated as a legitimate answer. +- `examples/go-api-with-opencode/config.yaml` never listed the module's `skills/` as a source, so the meta-skills were missing from that fixture. All six examples now register the module's `rules/`, `agents/` and `skills/`. + ## [0.6.0] — 2026-07-12 ### Added diff --git a/README.md b/README.md index f5f4184..5eebe44 100644 --- a/README.md +++ b/README.md @@ -135,6 +135,8 @@ intelligence-sync/ │ │ ├── VERSION # Module version (drives migrations) │ │ ├── lib/ # common.sh, layout.sh, migrations.sh │ │ └── adapters/ # 6 built-in + template +│ ├── rules/ # intelligence-authoring — how to author this layer +│ ├── agents/ # intelligence-architect — designs and prunes it │ └── skills/intelligence-* # Pre-installed meta-skills ├── examples/ # config.yaml for different project types ├── docs/ # Conventions and adapter guide (source) diff --git a/docs/ADAPTERS.md b/docs/ADAPTERS.md index 1479f81..3eac214 100644 --- a/docs/ADAPTERS.md +++ b/docs/ADAPTERS.md @@ -50,7 +50,8 @@ Source `lib/common.sh` for these utilities: | Function | Description | |----------|-------------| -| `normalize_file_to_lf(file)` | Convert CRLF to LF | +| `finalize_output_file(file)` | **Call this on every file you write.** Expands layout tokens (``, ``) and converts CRLF to LF | +| `normalize_file_to_lf(file)` | LF conversion only — for intermediate files that are not adapter output | | `lint_frontmatter(file)` | Warn about unquoted colons, leading tabs, and literal double quotes inside unquoted values (stderr) | | `get_frontmatter_value(key, file)` | Extract YAML frontmatter value | | `has_frontmatter(file)` | Check for `---` header | @@ -98,6 +99,17 @@ Skills follow the [Agent Skills open standard](https://agentskills.io). All supp Model names come from `get_model(config_file, ide, tier)` in `lib/common.sh`. Defaults are baked into `get_model_default()`; users override per-IDE/tier under `models:` in `config.yaml`. Sync prints a drift report when an override no longer matches the current default. +### Layout tokens (required) + +The engine ships artifacts of its own — the `intelligence-authoring` rule and the `intelligence-architect` agent — and they cannot hardcode the umbrella's folder name, because the project chooses it. They write `` and `` instead, and **every adapter must expand them by calling `finalize_output_file` on each file it writes** (it also does the LF normalization that `normalize_file_to_lf` used to do): + +| Token | Expands to | +|---|---| +| `` | repo-relative umbrella dir (e.g. `Intelligence`) | +| `` | repo-relative engine module (e.g. `Intelligence/sync`) | + +Values are exported by `sync.sh` (`IS_UMBRELLA_REL`, `IS_MODULE_REL`), derived from the detected layout. Expansion covers frontmatter and body, so `paths: ["/**"]` reaches Claude's `paths:`, Cursor's `globs:` and Copilot's `applyTo:` carrying the project's real folder name. A file written without `finalize_output_file` ships a literal `` into an IDE — CI fails the build if any generated output still contains a token. + ### Cleanup Contract Every adapter MUST follow these rules to stay safe alongside others: diff --git a/docs/CONVENTIONS.md b/docs/CONVENTIONS.md index e7bf256..d930097 100644 --- a/docs/CONVENTIONS.md +++ b/docs/CONVENTIONS.md @@ -38,10 +38,26 @@ intelligence/ # Umbrella — name NOT hardcoded (whateve ├── adapters/ # OPTIONAL — project-owned adapters (survive updates) │ └── myide.sh # sync_to_myide(); overrides a built-in of the same name └── sync/ # intelligence-sync MODULE (upstream-owned) - └── INIT.md docs/ scripts/(+VERSION) skills/intelligence-* + ├── INIT.md docs/ scripts/(+VERSION) + ├── rules/intelligence-authoring.md # authoring discipline for this layer + ├── agents/intelligence-architect.md # designs and prunes this layer + └── skills/intelligence-* # meta-skills ``` -Everything project-authored lives at the umbrella level (`rules/ agents/ skills/ adapters/`); everything upstream-owned lives in the self-contained module `sync/`, updated independently via `sync/scripts/update.sh`. Additional modules (e.g. `domain/`) sit beside `sync/`. The umbrella folder name is derived at runtime as "the directory holding `config.yaml`" — never hardcoded. The `intelligence-` skill prefix is **reserved** for upstream meta-skills; project skills must not use it (the updater moves/prunes anything matching that prefix). +Everything project-authored lives at the umbrella level (`rules/ agents/ skills/ adapters/`); everything upstream-owned lives in the self-contained module `sync/`, updated independently via `sync/scripts/update.sh`. Additional modules (e.g. `domain/`) sit beside `sync/`. The umbrella folder name is derived at runtime as "the directory holding `config.yaml`" — never hardcoded. The `intelligence-` prefix is **reserved** for upstream artifacts; project rules, agents and skills must not use it (the updater prunes what matches it). + +The module ships three kinds of artifact, and `config.yaml` must list all three under `sources` for them to reach the IDEs — `/sync/rules`, `/sync/agents`, `/sync/skills`. INIT emits those entries on bootstrap; the `0.7.0` migration adds them to existing projects. + +### Layout tokens in engine-shipped artifacts + +An artifact shipped *by the engine* cannot write the umbrella's name down — the project chooses it (`intelligence/`, `Intelligence/`, a codename). So engine artifacts spell it with tokens, and every adapter expands them on the way out (`finalize_output_file` in `lib/common.sh`): + +| Token | Expands to | Example | +|---|---|---| +| `` | repo-relative umbrella dir | `Intelligence` | +| `` | repo-relative engine module | `Intelligence/sync` | + +Expansion covers frontmatter and body alike, so `paths: ["/**"]` reaches Claude's `paths:`, Cursor's `globs:` and Copilot's `applyTo:` already carrying the project's real folder name. Project-authored artifacts may use the tokens too, but they have no reason to — they can simply name their own folders. A custom adapter belongs in the umbrella's `adapters/`, never in the module's `sync/scripts/adapters/` — the module is replaced wholesale on every update, so an adapter written there disappears at the next one. See `docs/ADAPTERS.md`. diff --git a/examples/dotnet-api-with-react-frontend/config.yaml b/examples/dotnet-api-with-react-frontend/config.yaml index ab9d6d7..f7c9f67 100644 --- a/examples/dotnet-api-with-react-frontend/config.yaml +++ b/examples/dotnet-api-with-react-frontend/config.yaml @@ -6,17 +6,19 @@ project: name: my-platform -sync_version: "0.6.0" +sync_version: "0.7.0" sources: rules: - "intelligence/rules" - "backend/intelligence/rules" - "frontend/intelligence/rules" + - "intelligence/sync/rules" agents: - "intelligence/agents" - "backend/intelligence/agents" - "frontend/intelligence/agents" + - "intelligence/sync/agents" skills: - "intelligence/skills" - "intelligence/sync/skills" diff --git a/examples/go-api-with-opencode/config.yaml b/examples/go-api-with-opencode/config.yaml index 3c75662..37eba98 100644 --- a/examples/go-api-with-opencode/config.yaml +++ b/examples/go-api-with-opencode/config.yaml @@ -5,15 +5,18 @@ project: name: my-api -sync_version: "0.6.0" +sync_version: "0.7.0" sources: rules: - "intelligence/rules" + - "intelligence/sync/rules" agents: - "intelligence/agents" + - "intelligence/sync/agents" skills: - "intelligence/skills" + - "intelligence/sync/skills" targets: agents: diff --git a/examples/go-api-with-pi-and-codex/config.yaml b/examples/go-api-with-pi-and-codex/config.yaml index 4a580b2..7d05fdb 100644 --- a/examples/go-api-with-pi-and-codex/config.yaml +++ b/examples/go-api-with-pi-and-codex/config.yaml @@ -5,13 +5,15 @@ project: name: my-api -sync_version: "0.6.0" +sync_version: "0.7.0" sources: rules: - "intelligence/rules" + - "intelligence/sync/rules" agents: - "intelligence/agents" + - "intelligence/sync/agents" skills: - "intelligence/skills" - "intelligence/sync/skills" diff --git a/examples/go-api/config.yaml b/examples/go-api/config.yaml index 6d2a4a5..7d2d7ee 100644 --- a/examples/go-api/config.yaml +++ b/examples/go-api/config.yaml @@ -4,13 +4,15 @@ project: name: my-api -sync_version: "0.6.0" +sync_version: "0.7.0" sources: rules: - "intelligence/rules" + - "intelligence/sync/rules" agents: - "intelligence/agents" + - "intelligence/sync/agents" skills: - "intelligence/skills" - "intelligence/sync/skills" diff --git a/examples/platform-with-submodules/config.yaml b/examples/platform-with-submodules/config.yaml index 526f9c5..a6209bd 100644 --- a/examples/platform-with-submodules/config.yaml +++ b/examples/platform-with-submodules/config.yaml @@ -6,7 +6,7 @@ project: name: my-platform -sync_version: "0.6.0" +sync_version: "0.7.0" sources: rules: @@ -14,10 +14,12 @@ sources: - "api/intelligence/rules" - "web/intelligence/rules" - "deploy/intelligence/rules" + - "intelligence/sync/rules" agents: - "intelligence/agents" - "api/intelligence/agents" - "web/intelligence/agents" + - "intelligence/sync/agents" skills: - "intelligence/skills" - "intelligence/sync/skills" diff --git a/examples/with-remote-skills/config.yaml b/examples/with-remote-skills/config.yaml index cd928b1..906724c 100644 --- a/examples/with-remote-skills/config.yaml +++ b/examples/with-remote-skills/config.yaml @@ -8,15 +8,17 @@ project: name: my-app -sync_version: "0.6.0" +sync_version: "0.7.0" sources: rules: - "intelligence/rules" + - "intelligence/sync/rules" # Shared rules from a central repo, pinned to a tag: - "git+https://github.com/org/shared-intel.git@v1.2.0#rules" agents: - "intelligence/agents" + - "intelligence/sync/agents" skills: - "intelligence/skills" - "intelligence/sync/skills" diff --git a/intelligence/sync/INIT.md b/intelligence/sync/INIT.md index 59eb7fc..bd4a579 100644 --- a/intelligence/sync/INIT.md +++ b/intelligence/sync/INIT.md @@ -232,16 +232,18 @@ project: # Managed by intelligence-sync — applied schema version. Do not hand-edit; # preserve on re-bootstrap. (Value = intelligence/sync/scripts/VERSION.) -sync_version: "0.6.0" +sync_version: "0.7.0" sources: rules: - "intelligence/rules" + - "intelligence/sync/rules" # engine-owned: intelligence-authoring agents: - "intelligence/agents" + - "intelligence/sync/agents" # engine-owned: intelligence-architect skills: - "intelligence/skills" - - "intelligence/sync/skills" + - "intelligence/sync/skills" # engine-owned: intelligence-* meta-skills targets: # agents: ALWAYS enabled — generates committed AGENTS.md as the @@ -330,47 +332,31 @@ One rule per component, scoped with `paths:` frontmatter: ### 3.5 Skills -Pre-installed skills: +Pre-installed by the engine — never recreate these, and never use the reserved `intelligence-` prefix for a project skill: - `/intelligence-sync` — sync to all enabled IDE targets -- `/intelligence-install-adapter` — enable an IDE target -- `/intelligence-uninstall-adapter` — disable an IDE target -- `/intelligence-add-skill` — create new skill with conventions -- `/intelligence-add-agent` — create new agent with conventions -- `/intelligence-add-rule` — create new rule with conventions +- `/intelligence-update` — update or migrate the engine +- `/intelligence-install-adapter` / `/intelligence-uninstall-adapter` — turn an IDE channel on or off +- `/intelligence-add-rule`, `/intelligence-add-agent`, `/intelligence-add-skill` — author an artifact with the conventions applied +- `/intelligence-extract-skill`, `/intelligence-learn-from-context`, `/intelligence-review-skills` — turn observed work into artifacts, and audit the layer -**Proactively suggest domain skills** based on detected stack. Analyze the codebase for repeatable multi-file patterns and propose skills. Look for operations that touch 3+ files in a predictable pattern. +The engine also ships the `intelligence-authoring` rule (authoring discipline, scoped to `/`) and the `intelligence-architect` agent (designs and prunes this layer). They are upstream-owned and arrive via `sources` — do not copy or rewrite them into project content. -Common skill categories (suggest what applies to this repo): +**Derive skills from this repository. Do not pick them from a catalogue.** A list of plausible skill names ("add-entity", "add-endpoint", "run-tests") is a trap: every one of them sounds right for every project, so working from such a list produces a registry that describes software in general rather than *this* codebase — and every entry costs context budget forever, whether or not anyone invokes it. -**Atomic (`add-`)** — creates/updates a single artifact: -- `-add-entity` — new domain model with all required files -- `-add-endpoint` — new API endpoint/route with handler -- `-add-page` — new page/view with routing -- `-add-component` — new UI component with tests -- `-add-service` — new service/client with types -- `-add-migration` — database schema change -- `-add-modal` — dialog/modal with form -- `-add-tests` — unit/integration tests for existing code -- `-add-e2e-tests` — end-to-end tests for feature +A candidate skill has to clear all four bars: -**Orchestrator (`create-`)** — invokes multiple add- skills. MUST use `create-` prefix, never `add-`: -- `-create-feature` — full feature from spec (entity + endpoint + page + tests) -- `-create-crud` — complete CRUD for single entity +1. **Repeated.** The operation has demonstrably been done more than once. Evidence: near-identical sibling files, a "how to add X" section in `README`/`CONTRIBUTING`, the same multi-file shape appearing across git history. Not "a project like this usually needs it". +2. **Multi-step and mechanical.** 3+ concrete steps in a predictable pattern, usually across more than one file. If it is one edit, the model does not need a procedure. +3. **Verifiable.** It ends in a command or check that proves it worked (build, test, lint, migration, sync). **If there is nothing to verify at the end, it is not a skill** — it is a note, or just work. +4. **Stable.** It describes the project *as it is meant to work*. A procedure that only exists to route around a current bug is not a skill — that belongs in a single rule that records known breakage, so it disappears when the bug is fixed instead of becoming permanent. -**Modifier (`update-`)** — changes across existing files: -- `-update-feature` — add/remove/modify fields across stack +**Show your evidence.** For every skill you propose, name the files it would touch and the concrete instance in the repo you derived it from. If you cannot point at one, do not propose it. -**Execution (`run-`)** — runs operations: -- `-run-tests` — run and analyze test results -- `-run-lint` — run linter and fix issues +**Start small: 0–3 skills at bootstrap.** Zero is a legitimate answer for a young or simple repository — say so plainly rather than padding. Skills can be added at any time with `/intelligence-add-skill`, and one added later, from a pattern the team actually hit twice, is worth more than five guessed today. -**Review (`review-`)** — read-only analysis: -- `-review-pending-changes` — review before commit -- `intelligence-review-rules` — check rules match actual codebase +Naming (details in `/sync/docs/CONVENTIONS.md`): `--`, domain prefix taken from the set already in use. Two verbs carry a contract worth keeping — **`add-` creates exactly one artifact**, and **`create-` orchestrates several `add-` skills** while duplicating none of their content. Other verbs (`run-`, `review-`, `update-`, `extract-`) read fine; prefer a verb already in use over a new synonym for the same thing. `intelligence-` is reserved for the engine's meta-skills. -Present the suggested list to the user. They choose which to create now. Remind them skills can be added any time with `/intelligence-add-skill` (or manually). - -Each skill must have 3+ concrete, repeatable steps derived from actual codebase patterns. Do not create skills for one-off operations. +Present the shortlist with its evidence and let the user choose. Then create only what they pick. ### 3.6 `.gitignore` diff --git a/intelligence/sync/agents/intelligence-architect.md b/intelligence/sync/agents/intelligence-architect.md new file mode 100644 index 0000000..d3b409c --- /dev/null +++ b/intelligence/sync/agents/intelligence-architect.md @@ -0,0 +1,47 @@ +--- +name: intelligence-architect +description: "Design and prune the intelligence layer — rule vs skill vs agent, split what grew, remove duplication and hardcoded paths" +tier: heavy +access: full +skills: + - intelligence-add-rule + - intelligence-add-agent + - intelligence-add-skill + - intelligence-extract-skill + - intelligence-review-skills + - intelligence-learn-from-context +--- + +# Intelligence architect + +Owns `/` itself: what the layer is made of, and what it is allowed to grow into. Not the project's code — the instructions that shape how everyone else writes it. + +## Expertise + +Where a piece of knowledge belongs. Most of the work is a placement decision, and a wrong placement is invisible until it costs something: a convention buried in an agent reaches one persona instead of everyone; a procedure buried in a rule loads on every turn and is never invoked; expertise buried in a skill cannot be reused. + +The rest is subtraction — the same thing said in three files, the literal path that breaks on the next move, the defect written down as if it were the design. + +## What this agent decides + +- **Rule, skill, or agent** — and whether the thing is worth an artifact at all. +- **Always-on or path-scoped.** An always-on rule is loaded into every session and inlined into `AGENTS.md`; it must earn that. Default to scoping. +- **Split or fold.** An artifact over budget is usually doing two jobs, not one long job. +- **What to delete.** An artifact nobody invokes and nothing enforces is cost with no return. + +## Boundaries + +The `intelligence-authoring` rule loads whenever this agent works — it carries the constraints, and this file does not repeat them. Two things it cannot carry, because they are judgement rather than form: + +- **Subtraction is the job.** The instinct is to add an artifact; usually the right move is to delete one, merge two, or decide the thing needed no artifact at all. A small registry is cheaper to trust than a crowded one. +- **A claim you cannot verify is one you do not get to write.** Not a softer version of it either. Say the gap out loud and leave it. + +## Build & verify + +``` +bash /scripts/sync.sh # expect IS_STATUS=ok +``` + +The per-artifact checks are procedure, so they live in the meta-skills. Reach for the one that fits the change instead of re-deriving the checks: `intelligence-add-rule`, `intelligence-add-agent`, `intelligence-add-skill`, `intelligence-extract-skill` (turn an observed workflow into a skill), `intelligence-review-skills` (audit the layer for duplication, drift, size), `intelligence-learn-from-context` (fold a session's lessons back in). + +Done means: the sync is green, and the skill you invoked reports clean. Size is a separate judgement — the budgets are ceilings, not quotas, and a short artifact is not a defect. diff --git a/intelligence/sync/docs/ADAPTERS.md b/intelligence/sync/docs/ADAPTERS.md index 1479f81..3eac214 100644 --- a/intelligence/sync/docs/ADAPTERS.md +++ b/intelligence/sync/docs/ADAPTERS.md @@ -50,7 +50,8 @@ Source `lib/common.sh` for these utilities: | Function | Description | |----------|-------------| -| `normalize_file_to_lf(file)` | Convert CRLF to LF | +| `finalize_output_file(file)` | **Call this on every file you write.** Expands layout tokens (``, ``) and converts CRLF to LF | +| `normalize_file_to_lf(file)` | LF conversion only — for intermediate files that are not adapter output | | `lint_frontmatter(file)` | Warn about unquoted colons, leading tabs, and literal double quotes inside unquoted values (stderr) | | `get_frontmatter_value(key, file)` | Extract YAML frontmatter value | | `has_frontmatter(file)` | Check for `---` header | @@ -98,6 +99,17 @@ Skills follow the [Agent Skills open standard](https://agentskills.io). All supp Model names come from `get_model(config_file, ide, tier)` in `lib/common.sh`. Defaults are baked into `get_model_default()`; users override per-IDE/tier under `models:` in `config.yaml`. Sync prints a drift report when an override no longer matches the current default. +### Layout tokens (required) + +The engine ships artifacts of its own — the `intelligence-authoring` rule and the `intelligence-architect` agent — and they cannot hardcode the umbrella's folder name, because the project chooses it. They write `` and `` instead, and **every adapter must expand them by calling `finalize_output_file` on each file it writes** (it also does the LF normalization that `normalize_file_to_lf` used to do): + +| Token | Expands to | +|---|---| +| `` | repo-relative umbrella dir (e.g. `Intelligence`) | +| `` | repo-relative engine module (e.g. `Intelligence/sync`) | + +Values are exported by `sync.sh` (`IS_UMBRELLA_REL`, `IS_MODULE_REL`), derived from the detected layout. Expansion covers frontmatter and body, so `paths: ["/**"]` reaches Claude's `paths:`, Cursor's `globs:` and Copilot's `applyTo:` carrying the project's real folder name. A file written without `finalize_output_file` ships a literal `` into an IDE — CI fails the build if any generated output still contains a token. + ### Cleanup Contract Every adapter MUST follow these rules to stay safe alongside others: diff --git a/intelligence/sync/docs/CONVENTIONS.md b/intelligence/sync/docs/CONVENTIONS.md index e7bf256..d930097 100644 --- a/intelligence/sync/docs/CONVENTIONS.md +++ b/intelligence/sync/docs/CONVENTIONS.md @@ -38,10 +38,26 @@ intelligence/ # Umbrella — name NOT hardcoded (whateve ├── adapters/ # OPTIONAL — project-owned adapters (survive updates) │ └── myide.sh # sync_to_myide(); overrides a built-in of the same name └── sync/ # intelligence-sync MODULE (upstream-owned) - └── INIT.md docs/ scripts/(+VERSION) skills/intelligence-* + ├── INIT.md docs/ scripts/(+VERSION) + ├── rules/intelligence-authoring.md # authoring discipline for this layer + ├── agents/intelligence-architect.md # designs and prunes this layer + └── skills/intelligence-* # meta-skills ``` -Everything project-authored lives at the umbrella level (`rules/ agents/ skills/ adapters/`); everything upstream-owned lives in the self-contained module `sync/`, updated independently via `sync/scripts/update.sh`. Additional modules (e.g. `domain/`) sit beside `sync/`. The umbrella folder name is derived at runtime as "the directory holding `config.yaml`" — never hardcoded. The `intelligence-` skill prefix is **reserved** for upstream meta-skills; project skills must not use it (the updater moves/prunes anything matching that prefix). +Everything project-authored lives at the umbrella level (`rules/ agents/ skills/ adapters/`); everything upstream-owned lives in the self-contained module `sync/`, updated independently via `sync/scripts/update.sh`. Additional modules (e.g. `domain/`) sit beside `sync/`. The umbrella folder name is derived at runtime as "the directory holding `config.yaml`" — never hardcoded. The `intelligence-` prefix is **reserved** for upstream artifacts; project rules, agents and skills must not use it (the updater prunes what matches it). + +The module ships three kinds of artifact, and `config.yaml` must list all three under `sources` for them to reach the IDEs — `/sync/rules`, `/sync/agents`, `/sync/skills`. INIT emits those entries on bootstrap; the `0.7.0` migration adds them to existing projects. + +### Layout tokens in engine-shipped artifacts + +An artifact shipped *by the engine* cannot write the umbrella's name down — the project chooses it (`intelligence/`, `Intelligence/`, a codename). So engine artifacts spell it with tokens, and every adapter expands them on the way out (`finalize_output_file` in `lib/common.sh`): + +| Token | Expands to | Example | +|---|---|---| +| `` | repo-relative umbrella dir | `Intelligence` | +| `` | repo-relative engine module | `Intelligence/sync` | + +Expansion covers frontmatter and body alike, so `paths: ["/**"]` reaches Claude's `paths:`, Cursor's `globs:` and Copilot's `applyTo:` already carrying the project's real folder name. Project-authored artifacts may use the tokens too, but they have no reason to — they can simply name their own folders. A custom adapter belongs in the umbrella's `adapters/`, never in the module's `sync/scripts/adapters/` — the module is replaced wholesale on every update, so an adapter written there disappears at the next one. See `docs/ADAPTERS.md`. diff --git a/intelligence/sync/rules/intelligence-authoring.md b/intelligence/sync/rules/intelligence-authoring.md new file mode 100644 index 0000000..bdd6cd8 --- /dev/null +++ b/intelligence/sync/rules/intelligence-authoring.md @@ -0,0 +1,88 @@ +--- +name: intelligence-authoring +description: "Authoring discipline for the intelligence layer — rule vs skill vs agent, scoping, size" +paths: + - "/**" +--- + +# Authoring the intelligence layer + +Applies when writing or changing anything under `/`. The mechanics — frontmatter fields, tier and access vocabulary, routing per tool — live in `/docs/CONVENTIONS.md`. This rule is the judgement that sits on top of them. + +Everything in this layer is context the model pays for on every turn. The discipline below is what keeps it worth its price. + +## Source of truth + +Edit `/{rules,agents,skills}` and `/config.yaml`. Everything else is derived: `.claude/`, `.cursor/`, `.github/{instructions,agents,skills}/`, `.codex/`, `.agents/skills/`, `.pi/`, `.opencode/` and `AGENTS.md` are **generated output** — a hand edit there survives exactly until the next sync. + +After any change: `bash /scripts/sync.sh`. A change that was not synced does not exist for any tool. + +## Pick the right artifact + +| Type | Intent | How it loads | +|---|---|---| +| **Rule** | The model **respects** a constraint while doing other work | Automatically — always-on, or path-scoped via `paths:` | +| **Skill** | The model **performs** a defined procedure | Explicitly — `/skill-name` | +| **Agent** | The model **adopts** a persona | Explicitly — via the agent picker | + +The mistakes that actually happen, in order of frequency: + +- A **checklist** written into an agent body → a checklist is a *procedure*; it belongs in a **skill**. This one keeps happening because a checklist *feels* like expertise. It is not: it changes with the task, a role does not. +- A **convention** written into an agent body → it belongs in a **rule**, so every agent gets it, not just the one you happened to be editing. +- A **workflow** written into a rule body → it belongs in a **skill**. A rule is loaded always; a procedure should be invoked. +- **Expertise** written into a skill body → it belongs in an **agent**; the persona is reusable across skills. + +## Rules + +**Scope with `paths:`.** A rule without `paths:` is loaded into *every* session and inlined into `AGENTS.md` — the most expensive context real estate there is. Earn it. A rule that only matters when someone touches the frontend belongs scoped to the frontend. + +**Lead with the positive default, then the invariant.** The model acts on the first instruction it reads, and it follows whatever is named — negation ("never do X") draws attention to X. Reserve absolute language (NEVER, MUST) for true must-nots: safety, security, output format. A judgement call written as a NEVER only teaches the model that NEVERs are negotiable. + +**Derive from the code, not from prose that already exists.** Every REQUIRED and every invariant must be backed by something observed in the repository. Documentation is a claim, not evidence — read the file. + +**Describe the repository, not a machine.** OS, shell, editor, local dev stack, personal tooling are environment facts; they belong in a personal, gitignored `CLAUDE.md`. A rule is committed and read by everyone. + +**Explain the why.** A constraint with its reason survives refactors; a bare imperative gets worked around. + +### Invariants + +- **Never state behaviour of a tool or engine you have not verified in its documentation or source.** An unverified claim about tooling is worse than a gap: it silently reshapes every downstream decision, and nothing in the repository will contradict it. +- **Never write a current defect into a rule as if it were the design.** Known breakage belongs in one place that says so. Every other rule describes the project *as it is meant to work* — a workaround documented as procedure becomes permanent. +- **Never link between always-on rules.** They are inlined verbatim into `AGENTS.md`, and the path-scoped channels carry only scoped rules, so a relative link is dead in at least one output. Name the rule instead; it loads on its own. + +## Agents + +An agent is **thin**: who it is, where it stops, how it verifies. Nothing else. + +- **Do not list rules for an agent to read.** They load on their own — Claude Code loads `.claude/rules/` into every custom subagent's startup context, and Cursor, Copilot, Codex, Pi and opencode receive always-on rules inlined in `AGENTS.md`. Pointing at a rule by name is fine; copying it is duplication that drifts. +- **Point at a rule, do not restate it.** Wanting to copy a rule into an agent means the rule is in the wrong place — move it. +- **Do carry** what is genuinely the agent's own: its boundaries ("if the app will not start, stop — do not hand-write the output"), its verification commands, its definition of done. + +## Skills + +A skill is a repeatable procedure someone invokes, and it ends in a verification. If there is nothing to verify, or it will only ever run once, it is not a skill — it is just work. + +- **`--`.** The domain prefix carries the weight: it clusters siblings and says which system the skill acts on. Take it from the set already in use. +- **`add-` creates exactly one artifact; `create-` orchestrates several `add-` skills** and repeats none of their content. +- **A skill is executed, so it must not hardcode what can move.** Paths, commands and project names belong in a rule the skill can read; a literal path baked into a procedure breaks the moment the layout moves. (Rules and agents are free to name paths — describing the repository is their job.) +- **`intelligence-` is reserved** for the engine's own meta-skills. Never use it for a project skill; the updater prunes what matches it. + +## Keep it short + +Ceilings, not quotas — an artifact that says everything it needs to is finished, not underweight. + +| Type | Comfortable | Hard cap | +|---|---|---| +| Rule | ~300 lines | 500 | +| Agent | ~150 lines | 200 | +| `SKILL.md` | under 500 lines | 1000 | + +Over budget means one of two things: the artifact is doing two jobs and should be split, or the detail belongs in `references/.md` with a pointer from the body. + +`description` fields compete for one shared budget across the whole registry — a long one pushes another artifact out of reach. Four to eight words when the name is unambiguous; up to ~20, with a distinguishing trigger, only when siblings do something similar. + +## Verifying a change to this layer + +The per-artifact checks are a procedure, not a constraint to hold in mind while doing other work — so they live in the meta-skills, not here. Invoke the one that matches what you are doing: `intelligence-add-rule`, `intelligence-add-agent`, `intelligence-add-skill`, `intelligence-extract-skill`, `intelligence-review-skills`, `intelligence-learn-from-context`, `intelligence-sync`, `intelligence-update`, `intelligence-install-adapter`, `intelligence-uninstall-adapter`. + +A change to this layer is done when `bash /scripts/sync.sh` reports `IS_STATUS=ok` and the skill you invoked reports clean. diff --git a/intelligence/sync/scripts/VERSION b/intelligence/sync/scripts/VERSION index a918a2a..faef31a 100644 --- a/intelligence/sync/scripts/VERSION +++ b/intelligence/sync/scripts/VERSION @@ -1 +1 @@ -0.6.0 +0.7.0 diff --git a/intelligence/sync/scripts/adapters/agents.sh b/intelligence/sync/scripts/adapters/agents.sh index 992c945..25f44a3 100644 --- a/intelligence/sync/scripts/adapters/agents.sh +++ b/intelligence/sync/scripts/adapters/agents.sh @@ -20,15 +20,17 @@ source "$(dirname "${BASH_SOURCE[0]}")/../lib/common.sh" # print a sync command that does not exist, and would be wrong outright on a # case-sensitive filesystem — in the one document Cursor, Copilot and Codex all # read as canonical. +# sync.sh exports both as layout tokens; recompute only when an adapter is +# driven directly (tests), never assume a folder name. agents_md_umbrella_rel() { - local repo_root="$1" rel - rel="$(repo_rel_dir "$repo_root" "${LS_UMBRELLA_DIR:-$repo_root/intelligence}")" + local repo_root="$1" rel="${IS_UMBRELLA_REL:-}" + [ -n "$rel" ] || rel="$(repo_rel_dir "$repo_root" "${LS_UMBRELLA_DIR:-$repo_root/intelligence}")" printf '%s' "${rel:-intelligence}" } agents_md_module_rel() { - local repo_root="$1" rel - rel="$(repo_rel_dir "$repo_root" "${LS_MODULE_DIR:-$repo_root/intelligence/sync}")" + local repo_root="$1" rel="${IS_MODULE_REL:-}" + [ -n "$rel" ] || rel="$(repo_rel_dir "$repo_root" "${LS_MODULE_DIR:-$repo_root/intelligence/sync}")" printf '%s' "${rel:-intelligence/sync}" } @@ -277,7 +279,7 @@ sync_to_agents() { agents_md_append_skills_table "$repo_root" "$config_file" "$output_file" agents_md_append_rules_list "$repo_root" "$config_file" "$output_file" - normalize_file_to_lf "$output_file" + finalize_output_file "$output_file" # Safety net: AGENTS.md is committed, so it must never carry an absolute or # transient link target (remote-pack content lives under the run cache). If diff --git a/intelligence/sync/scripts/adapters/claude.sh b/intelligence/sync/scripts/adapters/claude.sh index bfdfbe8..f80bb15 100644 --- a/intelligence/sync/scripts/adapters/claude.sh +++ b/intelligence/sync/scripts/adapters/claude.sh @@ -22,7 +22,7 @@ sync_claude_rules() { for f in "$dir"/*.md; do [ -f "$f" ] || continue cp "$f" "$output_dir/rules/" - normalize_file_to_lf "$output_dir/rules/$(basename "$f")" + finalize_output_file "$output_dir/rules/$(basename "$f")" echo " rule: $(basename "$f")" done done < <(read_yaml_list "$config_file" "rules") @@ -83,7 +83,7 @@ sync_claude_agent() { } { print } ' "$src" > "$output_dir/agents/$name" - normalize_file_to_lf "$output_dir/agents/$name" + finalize_output_file "$output_dir/agents/$name" echo " agent: $name (tier=$tier -> model=$model, access=$access)" } diff --git a/intelligence/sync/scripts/adapters/codex.sh b/intelligence/sync/scripts/adapters/codex.sh index a2c85c5..ad80504 100644 --- a/intelligence/sync/scripts/adapters/codex.sh +++ b/intelligence/sync/scripts/adapters/codex.sh @@ -88,7 +88,7 @@ sync_codex_agents() { echo "$body_safe" echo "\"\"\"" } > "$output_dir/$name.toml" - normalize_file_to_lf "$output_dir/$name.toml" + finalize_output_file "$output_dir/$name.toml" count=$((count + 1)) echo " agent: $name.toml" done diff --git a/intelligence/sync/scripts/adapters/copilot.sh b/intelligence/sync/scripts/adapters/copilot.sh index 237b39f..434bc09 100644 --- a/intelligence/sync/scripts/adapters/copilot.sh +++ b/intelligence/sync/scripts/adapters/copilot.sh @@ -71,7 +71,7 @@ sync_copilot_rules() { past_fm || !in_fm { print } ' "$f" } > "$instructions_dir/$base.instructions.md" - normalize_file_to_lf "$instructions_dir/$base.instructions.md" + finalize_output_file "$instructions_dir/$base.instructions.md" scoped_count=$((scoped_count + 1)) echo " rule: $base.instructions.md (scoped)" done @@ -160,7 +160,7 @@ sync_copilot_agents() { past_fm { print } ' "$f" } > "$output_dir/agents/$name.agent.md" - normalize_file_to_lf "$output_dir/agents/$name.agent.md" + finalize_output_file "$output_dir/agents/$name.agent.md" count=$((count + 1)) echo " agent: $name.agent.md (tier=$tier -> model=$model)" done diff --git a/intelligence/sync/scripts/adapters/cursor.sh b/intelligence/sync/scripts/adapters/cursor.sh index 2a7ef14..19af107 100644 --- a/intelligence/sync/scripts/adapters/cursor.sh +++ b/intelligence/sync/scripts/adapters/cursor.sh @@ -46,7 +46,7 @@ sync_cursor_rules() { NR==1 { print; print "alwaysApply: false"; next } { print } ' "$f" > "$output_dir/rules/$base.mdc" - normalize_file_to_lf "$output_dir/rules/$base.mdc" + finalize_output_file "$output_dir/rules/$base.mdc" echo " rule: $base.mdc (scoped)" done done < <(read_yaml_list "$config_file" "rules") @@ -117,7 +117,7 @@ sync_cursor_agents() { } { print } ' "$f" > "$output_dir/agents/$name" - normalize_file_to_lf "$output_dir/agents/$name" + finalize_output_file "$output_dir/agents/$name" echo " agent: $name (tier=$tier -> cursor)" done done < <(read_yaml_list "$config_file" "agents") diff --git a/intelligence/sync/scripts/adapters/opencode.sh b/intelligence/sync/scripts/adapters/opencode.sh index 1d50875..a37c009 100644 --- a/intelligence/sync/scripts/adapters/opencode.sh +++ b/intelligence/sync/scripts/adapters/opencode.sh @@ -109,7 +109,7 @@ sync_opencode_commands() { echo "Forward any provided arguments to the skill: \$ARGUMENTS" fi } > "$out_file" - normalize_file_to_lf "$out_file" + finalize_output_file "$out_file" count=$((count + 1)) echo " command: /$skill_name" done @@ -169,7 +169,7 @@ sync_opencode_agents() { echo "" printf '%s\n' "$body" } > "$out_file" - normalize_file_to_lf "$out_file" + finalize_output_file "$out_file" count=$((count + 1)) echo " agent: $name.md" done diff --git a/intelligence/sync/scripts/adapters/pi.sh b/intelligence/sync/scripts/adapters/pi.sh index 96408e2..2e3570a 100644 --- a/intelligence/sync/scripts/adapters/pi.sh +++ b/intelligence/sync/scripts/adapters/pi.sh @@ -84,7 +84,7 @@ sync_pi_rules() { display_sep="" cp "$f" "$rules_root/$base" - normalize_file_to_lf "$rules_root/$base" + finalize_output_file "$rules_root/$base" while IFS= read -r pat; do [ -z "$pat" ] && continue @@ -145,7 +145,7 @@ When the task touches files that match one of these path patterns, use the read } EOF } > "$extension_file" - normalize_file_to_lf "$extension_file" + finalize_output_file "$extension_file" fi echo " -> Rules: $count scoped" @@ -226,7 +226,7 @@ EOF echo "" echo "User task: \$@" } > "$prompt_file" - normalize_file_to_lf "$prompt_file" + finalize_output_file "$prompt_file" count=$((count + 1)) echo " agent: intelligence-agent-$name.md" done diff --git a/intelligence/sync/scripts/lib/common.sh b/intelligence/sync/scripts/lib/common.sh index e68dc60..6241355 100644 --- a/intelligence/sync/scripts/lib/common.sh +++ b/intelligence/sync/scripts/lib/common.sh @@ -14,6 +14,54 @@ normalize_file_to_lf() { mv "$tmp_file" "$target" } +# --- Layout tokens ----------------------------------------------------------- +# +# The umbrella folder is named by the project (`intelligence/`, `Intelligence/`, +# a codename) — so an artifact SHIPPED BY THE ENGINE cannot write that name +# down. A rule that scopes itself to the intelligence layer needs `paths:` to +# say "the umbrella", and an agent body needs to name the sync command. Both are +# spelled with tokens, expanded here at output time: +# +# -> the repo-relative umbrella dir (e.g. `Intelligence`) +# -> the repo-relative engine module (e.g. `Intelligence/sync`) +# +# Values come from IS_UMBRELLA_REL / IS_MODULE_REL, which sync.sh derives from +# the detected layout (never hardcoded) and exports before any adapter runs. +# Expansion happens in EVERY generated file, frontmatter and body alike, so a +# scoped rule reaches Claude's `paths:`, Cursor's `globs:` and Copilot's +# `applyTo:` already carrying the project's real folder name. + +# finalize_output_file +# The single exit gate for every file an adapter writes: expand layout tokens, +# then normalize CRLF -> LF. Adapters MUST call this (not normalize_file_to_lf) +# on each output — a missed call ships a literal `` into an IDE. +finalize_output_file() { + local target="$1" + local umb="${IS_UMBRELLA_REL:-intelligence}" + local mod="${IS_MODULE_REL:-intelligence/sync}" + local tmp_file="$target.tmp" + # Literal (index-based) substitution, not gsub: a regex replacement would + # give `&` in a path its special meaning, and POSIX awk has no way to pass a + # replacement string verbatim. + awk -v umb="$umb" -v mod="$mod" ' + function repl(s, from, to, out, i) { + out = "" + while ((i = index(s, from)) > 0) { + out = out substr(s, 1, i - 1) to + s = substr(s, i + length(from)) + } + return out s + } + { + sub(/\r$/, "") + $0 = repl($0, "", mod) + $0 = repl($0, "", umb) + print + } + ' "$target" > "$tmp_file" + mv "$tmp_file" "$target" +} + # Escape a string for safe interpolation into a TOML basic string (".."). # Backslash and double-quote are escaped; control chars stripped. toml_escape() { @@ -308,7 +356,7 @@ copy_skill_bundle() { cp -R "$src_dir/." "$dest_dir/" while IFS= read -r f; do [ -n "$f" ] || continue - normalize_file_to_lf "$f" + finalize_output_file "$f" done < <(find "$dest_dir" -type f -name '*.md') } @@ -355,7 +403,7 @@ sync_open_skill_dirs() { # SKILL.md additionally gets the strict-YAML frontmatter pass on # top of the plain bundle copy. copy_md_with_quoted_frontmatter "$d/SKILL.md" "$output_dir/$skill_name/SKILL.md" - normalize_file_to_lf "$output_dir/$skill_name/SKILL.md" + finalize_output_file "$output_dir/$skill_name/SKILL.md" count=$((count + 1)) echo " skill: $skill_name" done diff --git a/intelligence/sync/scripts/lib/migrations.sh b/intelligence/sync/scripts/lib/migrations.sh index 8e821e8..79641ab 100644 --- a/intelligence/sync/scripts/lib/migrations.sh +++ b/intelligence/sync/scripts/lib/migrations.sh @@ -27,7 +27,7 @@ # matching migrate_to_ — nothing here is rewritten or reordered. # Ordered (ascending) list of migration target versions. Append only. -MIGRATIONS=( "0_3_1" ) +MIGRATIONS=( "0_3_1" "0_7_0" ) # The applied-schema version is a managed key in config.yaml — NOT a dotfile, # NOT scripts/VERSION. config.yaml is what most future breaking changes will @@ -172,41 +172,62 @@ _mig_copy_file() { return 0 } -# Idempotently add a skills source entry to config.yaml. No backup — the edit -# is a single additive list item and the relocated content is recoverable. -# Name-agnostic: caller passes the already-resolved "//skills". -_mig_add_skill_source() { +# True (0) if is already listed anywhere in config.yaml (quoted or bare). +_mig_has_source() { local config="$1" entry="$2" + [ -f "$config" ] || return 1 + grep -Fq -- "\"$entry\"" "$config" || grep -Fq -- "- $entry" "$config" +} + +# Idempotently add one entry to `sources.
` in config.yaml. No backup — +# the edit is a single additive list item. Name-agnostic: the caller passes the +# already-resolved "//". If the section does not exist under +# `sources:` yet, it is created with the entry as its only item. +# Usage: _mig_add_source +_mig_add_source() { + local config="$1" section="$2" entry="$3" if [ ! -f "$config" ]; then - echo " [migrate] no config.yaml at $config — add this under sources.skills manually:" >&2 + echo " [migrate] no config.yaml at $config — add this under sources.$section manually:" >&2 echo " - \"$entry\"" >&2 return 0 fi - # Already present (quoted or bare) — nothing to do. - if grep -Fq -- "\"$entry\"" "$config" || grep -Fq -- "- $entry" "$config"; then - return 0 - fi + _mig_has_source "$config" "$entry" && return 0 local tmp="$config.mig.tmp" - awk -v entry="$entry" ' - function flush() { if (in_sk && !done) { print " - \"" entry "\""; done = 1 } } + awk -v section="$section" -v entry="$entry" ' + function emit() { print " - \"" entry "\""; inserted = 1 } + function close_here() { + if (in_sec && !inserted) { emit(); in_sec = 0 } + } { sub(/\r$/, "") } - # Top-level key: closes any open sources/skills tracking. + # Top-level key: ends the sources block (and any open section in it). /^[A-Za-z]/ { - flush(); in_sk = 0 + close_here() + # `sources:` existed but never declared this section — declare it. + if (in_src && !inserted) { print " " section ":"; emit() } + in_sec = 0 in_src = ($0 ~ /^sources:[[:space:]]*$/) ? 1 : 0 print; next } - in_src && /^ skills:[[:space:]]*$/ { print; in_sk = 1; next } - # Another 2-space sub-key (rules/agents) ends the skills block. - in_src && /^ [A-Za-z]/ { flush(); in_sk = 0; print; next } - in_sk && /^ -[[:space:]]/ { print; next } # existing skills item - in_sk && /^[[:space:]]*$/ { flush(); in_sk = 0; print; next } + in_src && $0 ~ "^ " section ":[[:space:]]*$" { print; in_sec = 1; next } + # Another 2-space sub-key ends this section. + in_src && /^ [A-Za-z]/ { close_here(); print; next } + in_sec && /^ -[[:space:]]/ { print; next } # existing item + in_sec && /^[[:space:]]*$/ { close_here(); print; next } { print } - END { flush() } + END { + close_here() + if (in_src && !inserted) { print " " section ":"; emit() } + } ' "$config" > "$tmp" && mv "$tmp" "$config" } +# Back-compat shim: 0.3.1 shipped with this name, and a shipped migration is +# never rewritten. +_mig_add_skill_source() { + _mig_add_source "$1" "skills" "$2" +} + # --- migrate_to_0_3_1 ------------------------------------------------------- # Pre-0.3.1: engine + meta-skills + INIT.md + docs lived flat under the # umbrella, mixed with project content. 0.3.1: they move into the self- @@ -314,6 +335,50 @@ migrate_to_0_3_1() { echo " [migrate 0.3.1] done — engine at '$module_name/', legacy removed, no duplicates" } +# --- migrate_to_0_7_0 ------------------------------------------------------- +# 0.7.0 is the first release where the engine ships a RULE and an AGENT of its +# own (`intelligence-authoring`, `intelligence-architect`), inside the module +# beside the meta-skills. Those reach the IDEs only if `config.yaml` lists the +# module's `rules/` and `agents/` directories as sources — so this migration +# registers them, exactly as 0.3.1 registered the module's `skills/`. +# +# Precondition is structural: both entries already present ⇒ applied ⇒ silent +# no-op. Nothing else in config.yaml is read or rewritten, and no file is +# deleted, so there is nothing to stage — the postcondition is simply that both +# entries are readable afterwards. +# +# migrate_to_0_7_0 [ — unused] +migrate_to_0_7_0() { + local umbrella="$1" module_name="$2" + local config="$umbrella/config.yaml" + [ -f "$config" ] || return 0 + + local base rules_entry agents_entry + base="$(basename "$umbrella")" + rules_entry="$base/$module_name/rules" + agents_entry="$base/$module_name/agents" + + if _mig_has_source "$config" "$rules_entry" && _mig_has_source "$config" "$agents_entry"; then + return 0 + fi + + echo " [migrate 0.7.0] registering the module's rules/ and agents/ as sources" + IS_MIGRATED=1 + _mig_add_source "$config" "rules" "$rules_entry" + _mig_add_source "$config" "agents" "$agents_entry" + + if ! _mig_has_source "$config" "$rules_entry" || ! _mig_has_source "$config" "$agents_entry"; then + is_status error "migrate_to_0_7_0 could not register module sources" + echo " ERROR: failed to add the module's rules/agents to sources in $config." >&2 + echo " Add them by hand under 'sources:':" >&2 + echo " rules: - \"$rules_entry\"" >&2 + echo " agents: - \"$agents_entry\"" >&2 + return "$IS_RC_ERROR" + fi + + echo " [migrate 0.7.0] done — sources.rules += $rules_entry, sources.agents += $agents_entry" +} + # run_migrations [] # The dispatcher of the breaking-change chain. Correctness rests on idempotent # structural preconditions, NOT on the stamp: every migrate_to_* self-detects diff --git a/intelligence/sync/scripts/sync.sh b/intelligence/sync/scripts/sync.sh index 0af70a1..45b7ea3 100644 --- a/intelligence/sync/scripts/sync.sh +++ b/intelligence/sync/scripts/sync.sh @@ -71,6 +71,16 @@ REPO_ROOT_RAW="${REPO_ROOT:-$(git rev-parse --show-toplevel 2>/dev/null || (cd " REPO_ROOT="$(cd "$REPO_ROOT_RAW" && pwd)" unset REPO_ROOT_RAW +# Layout tokens for generated output (see finalize_output_file in common.sh). +# Engine-shipped rules/agents cannot hardcode the umbrella's name — the project +# chooses it — so they write `` / `` and every adapter expands +# them to these repo-relative paths on the way out. +IS_UMBRELLA_REL="$(repo_rel_dir "$REPO_ROOT" "$LS_UMBRELLA_DIR")" +IS_MODULE_REL="$(repo_rel_dir "$REPO_ROOT" "$LS_MODULE_DIR")" +IS_UMBRELLA_REL="${IS_UMBRELLA_REL:-intelligence}" +IS_MODULE_REL="${IS_MODULE_REL:-intelligence/sync}" +export IS_UMBRELLA_REL IS_MODULE_REL + # Config: explicit env > config.yaml in the umbrella folder if [ -n "${CONFIG_FILE:-}" ]; then CONFIG_FILE="$CONFIG_FILE" diff --git a/intelligence/sync/scripts/update.sh b/intelligence/sync/scripts/update.sh index e9d8076..4636762 100644 --- a/intelligence/sync/scripts/update.sh +++ b/intelligence/sync/scripts/update.sh @@ -79,6 +79,9 @@ if [ -d "$WORK_DIR/intelligence/$UPSTREAM_MODULE/scripts" ]; then cp -r "$_SRC/scripts" "$UPMOD/scripts" [ -f "$_SRC/INIT.md" ] && cp "$_SRC/INIT.md" "$UPMOD/INIT.md" [ -d "$_SRC/docs" ] && cp -r "$_SRC/docs" "$UPMOD/docs" + # Engine-owned rule + agent (0.7.0+). Absent in older upstreams — optional. + [ -d "$_SRC/rules" ] && cp -r "$_SRC/rules" "$UPMOD/rules" + [ -d "$_SRC/agents" ] && cp -r "$_SRC/agents" "$UPMOD/agents" for s in "$_SRC"/skills/intelligence-*; do [ -d "$s" ] && cp -r "$s" "$UPMOD/skills/" done @@ -121,6 +124,14 @@ if [ -d "$UPMOD/docs" ]; then echo " Diff (docs/):" diff -ruN "$_LOCAL/docs" "$UPMOD/docs" 2>/dev/null || true fi +# Engine-owned rule + agent (0.7.0+): upstream-owned like the meta-skills, and +# distinct from the project's own /rules and /agents. +for _d in rules agents; do + [ -d "$UPMOD/$_d" ] || continue + echo "" + echo " Diff (engine $_d/):" + diff -ruN "$_LOCAL/$_d" "$UPMOD/$_d" 2>/dev/null || true +done if [ $AUTO_YES -ne 1 ]; then echo "" @@ -129,7 +140,7 @@ if [ $AUTO_YES -ne 1 ]; then echo " Legacy scripts/, INIT.md, docs/, and intelligence-* skills move there;" echo " a single additive line is added to config.yaml sources.skills." fi - read -r -p "Apply update? Engine/INIT/meta-skills/docs overwritten; rules/agents/project skills NOT touched. [y/N] " confirm + read -r -p "Apply update? Engine/INIT/meta-skills/docs + engine rule/agent overwritten; YOUR rules/agents/skills NOT touched. [y/N] " confirm case "$confirm" in y|Y|yes|YES) ;; *) echo " Cancelled."; exit 0 ;; @@ -164,6 +175,16 @@ else rm -rf "$MODULE_DIR/scripts"; cp -r "$UPMOD/scripts" "$MODULE_DIR/scripts" fi [ -f "$UPMOD/INIT.md" ] && cp "$UPMOD/INIT.md" "$MODULE_DIR/INIT.md" +for _d in rules agents; do + [ -d "$UPMOD/$_d" ] || continue + if command -v rsync >/dev/null 2>&1; then + mkdir -p "$MODULE_DIR/$_d" + rsync -a --delete "$UPMOD/$_d/" "$MODULE_DIR/$_d/" + else + # `:?` so an empty MODULE_DIR/_d can never expand this into `rm -rf /`. + rm -rf "${MODULE_DIR:?}/${_d:?}"; cp -r "$UPMOD/$_d" "$MODULE_DIR/$_d" + fi +done if [ -d "$UPMOD/docs" ]; then if command -v rsync >/dev/null 2>&1; then rsync -a --delete "$UPMOD/docs/" "$MODULE_DIR/docs/" @@ -202,6 +223,7 @@ if [ "${IS_MIGRATED:-0}" -eq 1 ]; then else is_status ok "version=$_ver" fi -echo " Updated: $MODULE_NAME/scripts/, $MODULE_NAME/INIT.md, $MODULE_NAME/skills/intelligence-*, $MODULE_NAME/docs/ (version $_ver)" -echo " Untouched: config.yaml (except idempotent sources.skills line on migration), rules/, agents/, project skills." +echo " Updated: $MODULE_NAME/{scripts,INIT.md,docs}, $MODULE_NAME/skills/intelligence-*, $MODULE_NAME/{rules,agents} (version $_ver)" +echo " Untouched: your /rules, /agents, project skills, /adapters." +echo " config.yaml only gains the engine's managed keys (sync_version, module sources)." echo " Next: bash $MODULE_NAME/scripts/sync.sh" From ba738590c49b18b9a820bacd3a61e64823671850 Mon Sep 17 00:00:00 2001 From: Dmitrij Zykovic Date: Sun, 12 Jul 2026 20:50:44 +0100 Subject: [PATCH 2/3] Derived the CI migration-chain fixture from the live registry so a new migration cannot silently stop it from testing the chain --- .github/workflows/ci.yml | 33 +++++++++++++++++++++------------ 1 file changed, 21 insertions(+), 12 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 2d4928f..20901f9 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -424,29 +424,34 @@ jobs: s=$(bash intelligence/sync/scripts/sync.sh); echo "$s" | grep -q 'IS_STATUS=ok' test -f AGENTS.md - - name: Multi-version chain walks forward (0.3.1 → 0.4.0), idempotent + - name: Multi-version chain walks the whole registry forward, idempotent shell: bash run: | set -euo pipefail source "$STAGE_LIB" - # An upstream with a synthetic SECOND breaking migration appended. - UP=$RUNNER_TEMP/up040 + # An upstream with a synthetic NEWEST breaking migration appended to + # whatever the real registry currently holds. The append is derived, + # never pinned to a literal MIGRATIONS=( ... ) line: pinning it means + # the chain silently stops being tested the moment a real migration + # is added. + UP=$RUNNER_TEMP/up990 mkdir -p "$UP" tar --exclude='.git' -cf - . | (cd "$UP" && tar -xf -) M="$UP/intelligence/sync/scripts/lib/migrations.sh" - printf '0.4.0\n' > "$UP/intelligence/sync/scripts/VERSION" - sed -i 's/^MIGRATIONS=( "0_3_1" )/MIGRATIONS=( "0_3_1" "0_4_0" )/' "$M" + printf '9.9.0\n' > "$UP/intelligence/sync/scripts/VERSION" + sed -i 's/^MIGRATIONS=(\(.*\))$/MIGRATIONS=(\1 "9_9_0" )/' "$M" + grep -q '"9_9_0"' "$M" || { echo "fixture failed to extend MIGRATIONS"; exit 1; } cat >> "$M" <<'EOF' # synthetic breaking migration for CI: structural precondition = # absence of the marker; idempotent; transactional. - migrate_to_0_4_0() { + migrate_to_9_9_0() { local umbrella="$1" module_name="$2" - local marker="$umbrella/$module_name/.mig040" + local marker="$umbrella/$module_name/.mig990" [ -f "$marker" ] && return 0 IS_MIGRATED=1 : > "$marker" - echo " [migrate 0.4.0] applied" + echo " [migrate 9.9.0] applied" } EOF git -C "$UP" init -q @@ -460,13 +465,17 @@ jobs: out=$(REPO_URL="file://$UP" bash intelligence/scripts/update.sh --yes) echo "$out" echo "$out" | grep -q 'IS_STATUS=migrated' || { echo "expected migrated"; exit 1; } - test -f intelligence/sync/.mig040 # 0.4.0 ran + test -f intelligence/sync/.mig990 # newest ran ! test -d intelligence/scripts # 0.3.1 ran - grep -q '^sync_version: *"0.4.0"' intelligence/config.yaml - # Idempotent: re-run is a clean no-op at 0.4.0. + # 0.7.0 ran too — the module's rules/ and agents/ are registered, once each. + test "$(grep -c '"intelligence/sync/rules"' intelligence/config.yaml)" -eq 1 + test "$(grep -c '"intelligence/sync/agents"' intelligence/config.yaml)" -eq 1 + grep -q '^sync_version: *"9.9.0"' intelligence/config.yaml + # Idempotent: re-run is a clean no-op, and adds no duplicate sources. out2=$(REPO_URL="file://$UP" bash intelligence/sync/scripts/update.sh --yes) echo "$out2" | grep -q 'IS_STATUS=ok' || { echo "expected ok on re-run"; exit 1; } - grep -q '^sync_version: *"0.4.0"' intelligence/config.yaml + grep -q '^sync_version: *"9.9.0"' intelligence/config.yaml + test "$(grep -c '"intelligence/sync/rules"' intelligence/config.yaml)" -eq 1 - name: Version-compat guard fails closed (ahead-of-engine) shell: bash From 003b09e70719a46b0a61ee7ef82d370d26ef6a95 Mon Sep 17 00:00:00 2001 From: Dmitrij Zykovic Date: Sun, 12 Jul 2026 20:54:09 +0100 Subject: [PATCH 3/3] Derived the migration source prefix from git so a nested umbrella registers the module under its real repo-relative path, and fixed the adapter doc example to use finalize_output_file --- docs/ADAPTERS.md | 4 +++- intelligence/sync/docs/ADAPTERS.md | 4 +++- intelligence/sync/scripts/lib/migrations.sh | 23 +++++++++++++++++++-- 3 files changed, 27 insertions(+), 4 deletions(-) diff --git a/docs/ADAPTERS.md b/docs/ADAPTERS.md index 3eac214..2b3c39d 100644 --- a/docs/ADAPTERS.md +++ b/docs/ADAPTERS.md @@ -152,7 +152,9 @@ sync_to_myide() { } past_fm || !in_fm { print } ' "$f" > "$output_dir/rules/$(basename "$f")" - normalize_file_to_lf "$output_dir/rules/$(basename "$f")" + # Every written file goes through finalize_output_file: it expands + # the / layout tokens and normalizes CRLF -> LF. + finalize_output_file "$output_dir/rules/$(basename "$f")" done done < <(read_yaml_list "$config_file" "rules") } diff --git a/intelligence/sync/docs/ADAPTERS.md b/intelligence/sync/docs/ADAPTERS.md index 3eac214..2b3c39d 100644 --- a/intelligence/sync/docs/ADAPTERS.md +++ b/intelligence/sync/docs/ADAPTERS.md @@ -152,7 +152,9 @@ sync_to_myide() { } past_fm || !in_fm { print } ' "$f" > "$output_dir/rules/$(basename "$f")" - normalize_file_to_lf "$output_dir/rules/$(basename "$f")" + # Every written file goes through finalize_output_file: it expands + # the / layout tokens and normalizes CRLF -> LF. + finalize_output_file "$output_dir/rules/$(basename "$f")" done done < <(read_yaml_list "$config_file" "rules") } diff --git a/intelligence/sync/scripts/lib/migrations.sh b/intelligence/sync/scripts/lib/migrations.sh index 79641ab..cb544c8 100644 --- a/intelligence/sync/scripts/lib/migrations.sh +++ b/intelligence/sync/scripts/lib/migrations.sh @@ -172,6 +172,25 @@ _mig_copy_file() { return 0 } +# Repo-root-relative path of the umbrella — the prefix every `sources:` entry is +# resolved against (resolve_source_dir does "$repo_root/$entry"). `basename` is +# only correct when the umbrella sits directly at the repo root; a nested one +# (`platform/intelligence/`) would yield `intelligence/...` and register a source +# that resolves nowhere. Ask git for the real prefix, and fall back to basename +# when there is no git (a tarball checkout), which is the flat case anyway. +_mig_umbrella_rel() { + local umbrella="$1" prefix="" + if command -v git >/dev/null 2>&1; then + prefix="$(git -C "$umbrella" rev-parse --show-prefix 2>/dev/null || true)" + fi + prefix="${prefix%/}" + if [ -n "$prefix" ]; then + printf '%s' "$prefix" + else + printf '%s' "$(basename "$umbrella")" + fi +} + # True (0) if is already listed anywhere in config.yaml (quoted or bare). _mig_has_source() { local config="$1" entry="$2" @@ -330,7 +349,7 @@ migrate_to_0_3_1() { fi # config.yaml: name-agnostic relative path under the actual umbrella base. - _mig_add_skill_source "$umbrella/config.yaml" "$(basename "$umbrella")/$module_name/skills" + _mig_add_skill_source "$umbrella/config.yaml" "$(_mig_umbrella_rel "$umbrella")/$module_name/skills" echo " [migrate 0.3.1] done — engine at '$module_name/', legacy removed, no duplicates" } @@ -354,7 +373,7 @@ migrate_to_0_7_0() { [ -f "$config" ] || return 0 local base rules_entry agents_entry - base="$(basename "$umbrella")" + base="$(_mig_umbrella_rel "$umbrella")" rules_entry="$base/$module_name/rules" agents_entry="$base/$module_name/agents"