# Platform mirrors reference How `.lore/*` content gets mirrored to platform-specific config files. The main `SKILL.md` covers the high-level rules; this file holds the per-platform mapping, the two-section file structure, and the algorithm that resolves which files to generate (auto-detect by default, explicit override available). ## Platform → file mapping | Platform | File (default) | Also accepted | |---|---|---| | Claude Code | `CLAUDE.md` (root) | `.claude/CLAUDE.md` | | Cursor | `.cursorrules` (root) | `.cursor/rules/*.mdc` | | Cline | `.clinerules` (root) | — | | Aider | `AGENTS.md` (root) | `CONVENTIONS.md` | | OpenAI Codex | `AGENTS.md` (root) | — | | OpenCode | `AGENTS.md` (root) | — | | Windsurf | `.windsurfrules` (root) | — | | GitHub Copilot | `.github/copilot-instructions.md` | — | | Continue.dev | `.continue/rules/lore.md` | — | | LangGraph / DeepAgents | (no file — inject at runtime) | — | For LangGraph and DeepAgents, the skill does not produce a mirror file. Read `.lore/*.md` directly or ingest into the system prompt at runtime — that ingestion is the user's responsibility. ## Resolution: how `mirror_targets` is computed When the skill needs to know which platform files to generate (during `init`, `mirror`, and `compress` when `auto_mirror: true`), it runs the following procedure: ``` resolve_mirror_targets(config, repo_root): # 1. If config has mirror_targets set, auto-detect is skipped, but validation is not if "mirror_targets" in config: return validate_mirror_targets(config["mirror_targets"], repo_root) # 2. Scan repo root for existing platform files (see Scan candidates) detected = scan_existing_platform_files(repo_root) if detected: return validate_mirror_targets(detected, repo_root) # 3. Nothing detected → ask user via multi-select, persist to config, return selected = ask_user_multi_select(AGENT_CHOICES) write_mirror_targets_to_config(selected) return validate_mirror_targets(selected, repo_root) ``` This is the core resolution used by all three commands. `init` extends it with classification and per-file takeover steps — see "Init-time behavior (full procedure)" below. ### Mandatory target validation Validation is fail-closed and atomic. Validate the complete target list **before reading, creating, classifying, archiving, or writing any target**. If one entry fails, report it and abort the mirror operation without touching any target. For each target: 1. Require a non-empty string containing no NUL byte. Reject absolute paths on every platform (POSIX `/...`, Windows drive paths such as `C:\\...`, and UNC paths). 2. Parse path components without interpreting the value as a shell pattern. Reject any `..` component; normalize `.` and repeated separators only after that rejection. 3. Require the normalized relative path to match this allowlist exactly: `CLAUDE.md`, `.claude/CLAUDE.md`, `.cursorrules`, `.clinerules`, `AGENTS.md`, `CONVENTIONS.md`, `.windsurfrules`, `.github/copilot-instructions.md`, or `.continue/rules/lore.md`; or match `.cursor/rules/.mdc`, where `` is one non-empty filename segment (no nested directory). 4. Resolve `repo_root` to its canonical path. Walk every existing component of the target with symlinks resolved and require it to remain inside that canonical root. For a missing target, resolve its nearest existing parent the same way. Reject a target whose file or parent symlink escapes the root. Perform this check again immediately before every read or write to reduce check/use races. Examples: accept `CLAUDE.md`, `.github/copilot-instructions.md`, and `.cursor/rules/lore.mdc`. Reject `/tmp/CLAUDE.md`, `../CLAUDE.md`, `.cursor/rules/nested/lore.mdc`, `notes.md`, and an allowlisted-looking path whose existing parent or file is a symlink outside the project. ### Scan candidates The auto-detect step checks for the following paths at `repo_root`: ``` CLAUDE.md .claude/CLAUDE.md .cursorrules .clinerules AGENTS.md CONVENTIONS.md .windsurfrules .github/copilot-instructions.md .continue/rules/lore.md .cursor/rules/*.mdc # glob: any .mdc file under .cursor/rules/ ``` These match the platform table above (default + "Also accepted" filenames). The `.cursor/rules/*.mdc` entry is a glob — it's a hit if `.cursor/rules/` exists and contains at least one `.mdc` file. ### Multi-select agent choices When Step 3 fires, present this question to the user: | Choice | Primary file written | |---|---| | Claude Code | `CLAUDE.md` | | Cursor | `.cursorrules` | | Cline | `.clinerules` | | Aider | `AGENTS.md` | | Codex | `AGENTS.md` | | OpenCode | `AGENTS.md` | | Windsurf | `.windsurfrules` | | GitHub Copilot | `.github/copilot-instructions.md` | | Continue.dev | `.continue/rules/lore.md` | Aider, Codex, and OpenCode all map to `AGENTS.md`. Selecting any combination produces one entry. Selecting nothing is valid — writes `mirror_targets: []` (no mirrors generated). ### When this runs - `lore init` — always interactive. - `lore mirror` when `mirror_targets` is absent — also interactive (skill is invoked through chat). - `lore compress` (when `auto_mirror: true`) — also goes through this resolution if `mirror_targets` is absent. Both paths use the same function. Once `init` has run, `mirror_targets` is set, so subsequent `mirror` calls hit Step 1 and are silent. ## Two-section file structure Every mirror file is split into two sections by a `---` separator. The top section is Skill-managed and rewritten on mirror regeneration. The bottom section is user-editable and preserved verbatim. ```markdown ## Lore (auto-managed) # .lore SUMMARY (synced 2026-07-09) > Last compressed: 2026-07-09 > Total entries: 247 across 3 scopes ## Global - Monorepo with pnpm workspaces + Turborepo — [_global/ARCHITECTURE.md#ARCH-2026-01-15-d7a3] ... --- ## My notes (free edit) - Keep answers concise - Currently refactoring the user auth module - Prefer English ``` The `---` separator is a literal Markdown horizontal rule. Both sections are plain Markdown so any agent or editor can render them normally. ### Section detection rules When syncing a mirror file: 1. If the file contains `---` on its own line, that line is the boundary. Everything above is the Lore section, everything below is My notes. 2. If the file contains a `## My notes` header, the My notes section starts at that header and goes to EOF. 3. If neither marker is present, the entire file is treated as the Lore section (i.e. no My notes section). Subsequent sync appends a separator + empty My notes section. 4. If the file is missing the `## Lore` header but has `## My notes`, the entire file is treated as user notes. Skill does not write to it. User is asked to confirm before sync restructures the file. ## Sync-time behavior **`sync` does not regenerate platform mirrors.** This is intentional — see the "Mirror update triggers" section in `SKILL.md`. The skill only writes `.lore/*.md` during `sync`. To update mirrors after `sync`, the user runs `lore mirror` (or `compress`, which calls mirror generation as a side effect). If a project needs the old behavior (mirror updates on every `sync`), set `sync_updates_mirror: true` in `.lore/.config.json`. ## Mirror-time behavior (`lore mirror`) This is the actual write step for platform mirrors. 1. Validate the complete configured target list using "Mandatory target validation" above. Abort without reading or writing any target if validation fails. 2. Read the current state of `.lore/SUMMARY.md` and the scope-tagged index. 3. For each validated mirror target, recheck canonical containment immediately before access, then read the existing file and detect the section boundary. 4. Compute the new Lore section content. 5. **Content-based dedup**: if the new Lore section content is byte-identical to the existing one, skip writing. Report "No changes needed: ``". 6. If different, replace the Lore section (full rewrite, no merge with previous content). Preserve the My notes section verbatim. 7. Recheck canonical containment, then write the file back. Report "Mirror updated: ``". The content-based dedup step (4) is the key reason `mirror` can be run frequently without polluting `git log` — most invocations will be no-ops once the mirror is in sync. ## Init-time behavior (full procedure) The `init` command extends the resolution algorithm above with classification and per-file takeover steps. The full procedure: 1. **Check whether `.lore/` exists.** - Absent → create `.lore/` and write an initial empty config. - Present → load existing `.lore/.config.json` (use defaults if missing). 2. **Scan existing platform files** in repo root using the same candidate list as the resolution algorithm. Result: list of paths that exist. 3. **Classify each detected file** into one of three classes: - **Class (a)** — already a lore mirror: contains `## Lore` section. - **Class (b)** — user-written: contains `## My notes` but no `## Lore`. - **Class (c)** — unmarked: neither header present. For class (b) and (c) files, present a per-file choice: - **Take over**: file becomes a two-section mirror; existing content is preserved as My notes. - **Preserve as-is**: file is left alone; NOT added to `mirror_targets`. - **Abort**: exit init entirely. `.lore/` may exist (from Step 1) but no `mirror_targets` is written. Class (a) files are auto-included in `mirror_targets`. 4. **Multi-select question.** "Which agents do you use in this project?" Default pre-selection: every agent corresponding to a class (a) file. Empty selection is allowed — but class (a) files still get included via Step 5. 5. **Compute final `mirror_targets`** by combining three sources and deduplicating: - All class (a) files from Step 3 (always included, regardless of Step 4 selection). - Files chosen via "take over" in Step 3. - Primary files for additional agents the user selected in Step 4 that aren't already covered. Dedup: Aider and Codex both map to `AGENTS.md` and collapse to one entry. 6. **Write `.lore/.config.json`** with `mirror_targets` populated. 7. **Generate initial mirror files** for each target: - File absent → full template (`## Lore` + `---` + empty `## My notes`). - File present with `## Lore` → refresh Lore section, preserve My notes verbatim. - File present and "take over" chosen → old content becomes My notes, new `## Lore` above. - File present and "preserve" chosen → no write. For each generated mirror file, the section template is: ``` ## Lore (auto-managed) --- ## My notes (free edit) ``` ## What gets mirrored The mirror's Lore section is an **index** into `.lore/` — not a copy of its content. This keeps per-session token cost flat (~500 B regardless of project size) and aligns with how platform instruction files (`CLAUDE.md`, `.cursorrules`, etc.) are designed to be used: as small pointers that tell the agent where to find detail on demand. The agent generating the mirror walks `.lore/` and emits the structure below. Sections appear only when their content exists (adaptive rendering). ### Index template ``` ## Lore (auto-managed) Project memory. Read deeper on demand. **Structure**: - Digest: `.lore/SUMMARY.md` (top-level overview) - Global: `.lore/_global/` (architecture, decisions, conventions) - Scopes: `.lore/scopes/` - `.lore/scopes//` () - `.lore/scopes//` ... **Query**: `lore query ` or `lore query :` **Update**: see the `lore` skill (init / sync / query / audit / compress / mirror) --- ## My notes (free edit) ``` The `## Lore (auto-managed)` opener, `---` separator, and `## My notes (free edit)` closer are **always present** — only the `**Structure**:` body varies with adaptive rendering. Agent preserves the My notes section verbatim across regenerations. ### Field sources - `` — directory name under `.lore/scopes/`. Each scope's full path is `.lore/scopes//`. - `` — extracted from `.lore/scopes//ARCHITECTURE.md` via the HTML comment ``. See "Scope description extraction" below. If absent, the description is omitted (scope row still appears, just without parenthetical). The index does **not** track the project's source-directory mapping for each scope (e.g. `packages/frontend/` for the `frontend` scope). Source paths are detected by `references/monorepo-detection.md` at init time but not persisted in `.lore/`. If a user needs source paths surfaced in the mirror, that mapping belongs in the project's own docs. ### Section visibility rules | Section | Visible when | |---|---| | `Digest:` line | always | | `Global:` line | `.lore/_global/` exists and has any entry | | `Scopes:` block | at least one scope directory exists under `.lore/` | | `Query:` line | always | | `Update:` line | always | ### Adaptive renderings Only the `**Structure**:` body varies. The `## Lore (auto-managed)` opener, `---` separator, and `## My notes (free edit)` closer are always present and unchanged. **Empty project** (just initialized, no entries yet): ``` ## Lore (auto-managed) Project memory. Read deeper on demand. **Structure**: - Digest: `.lore/SUMMARY.md` (top-level overview) **Query**: `lore query ` **Update**: see the `lore` skill --- ## My notes (free edit) ``` `Global:` and `Scopes:` blocks omitted. **Single-scope project**: ``` **Structure**: - Digest: `.lore/SUMMARY.md` - Global: `.lore/_global/` - Scopes: `.lore/scopes/` - `.lore/scopes/frontend/` (React 18 + TypeScript) ``` `Scopes:` block has one entry. **Monorepo with multiple scopes**: ``` **Structure**: - Digest: `.lore/SUMMARY.md` - Global: `.lore/_global/` - Scopes: `.lore/scopes/` - `.lore/scopes/frontend/` (React 18 + TypeScript) - `.lore/scopes/backend/` (PostgreSQL + Prisma) - `.lore/scopes/shared/` ``` ### Scope description extraction The agent scans `.lore/scopes//ARCHITECTURE.md` for the **first line matching** `` (anchored to start of line; `description:` literal). Rules: - **First match wins.** If multiple `` lines exist, only the first is used. - **`` is single-line.** A comment must not contain a newline before `-->`. Multi-line comments are ignored. - **Whitespace trimmed.** Leading and trailing whitespace inside `` is stripped. - **No match → no description.** The scope row appears without parenthetical; the row is not removed. Example `ARCHITECTURE.md` with description: ``` # Frontend Architecture All UI code lives here. ... ``` ### Scope ordering Scope rows in the `Scopes:` block are emitted in **alphabetical order** by ``. Pinning order is important: the content-based dedup step compares byte-for-byte, so any order change between runs causes spurious "Mirror updated" reports. ### What does NOT trigger mirror regeneration Index content does not change when: - Individual entries are edited - `SUMMARY.md` content is updated (the index only points to its path) - Entry counts change - A scope's `ARCHITECTURE.md` content changes (only the `` comment affects the index) Index content changes require regeneration when: - A new scope directory is added under `.lore/` - A scope is removed - A scope's `ARCHITECTURE.md` `` line changes - `.lore/_global/` gains or loses its first entry (Global section visibility flips) ## Manual operations | Command | Effect | |---|---| | `lore mirror` | Force-regenerate all configured platform mirrors from current `.lore/*` state. Content-based dedup: skips targets whose new Lore section matches the existing one. | | `lore mirror reset ` | Archive current My notes content to `.lore/.archive/-.md`, then write a clean mirror with only the Lore section. User must confirm. | | `lore mirror show ` | Print the file with the two sections clearly delimited in the output. Pure read. | | `lore mirror check` | For each configured target, verify it has a `---` separator and a `## My notes` section. Report any structural problems. Read-only. | ## Trigger rules | Trigger | Behavior | |---|---| | `init` confirms draft | Auto-generate mirrors for all configured targets using the init-time rules above. | | `sync` proposal accepted | Writes to `.lore/*.md` only. Does **not** touch mirrors. User runs `lore mirror` separately to publish. (Override: set `sync_updates_mirror: true` in config to restore old behavior.) | | `compress` completes | If `auto_mirror: true`, regenerate mirrors (with content-based dedup). Otherwise ask per target. | | `lore mirror` | Force-regenerate all configured targets with content-based dedup. | | `query` / `audit` | Never touches mirrors. |