Files
playbook/antigravity-awesome-skills/skills/lore/references/platform-mirrors.md
T
2026-07-18 00:02:59 +00:00

17 KiB

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/<name>.mdc, where <name> 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.

## 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: <file>".
  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: <file>".

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)

<initial or refreshed Lore content>

---

## My notes (free edit)

<preserved or empty>

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/<scope_name>/` (<description>)
  - `.lore/scopes/<scope_name>/`
  ...

**Query**: `lore query <term>` or `lore query <scope>:<term>`
**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

  • <scope_name> — directory name under .lore/scopes/. Each scope's full path is .lore/scopes/<scope_name>/.
  • <description> — extracted from .lore/scopes/<scope_name>/ARCHITECTURE.md via the HTML comment <!-- description: ... -->. 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 <term>`
**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/<scope_name>/ARCHITECTURE.md for the first line matching <!-- description: <text> --> (anchored to start of line; description: literal). Rules:

  • First match wins. If multiple <!-- description: ... --> lines exist, only the first is used.
  • <text> is single-line. A comment must not contain a newline before -->. Multi-line comments are ignored.
  • Whitespace trimmed. Leading and trailing whitespace inside <text> is stripped.
  • No match → no description. The scope row appears without parenthetical; the row is not removed.

Example ARCHITECTURE.md with description:

<!-- description: React 18 + TypeScript frontend -->
# Frontend Architecture

All UI code lives here. ...

Scope ordering

Scope rows in the Scopes: block are emitted in alphabetical order by <scope_name>. 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 <!-- description: --> 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 <!-- description: --> 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 <file> Archive current My notes content to .lore/.archive/<file>-<date>.md, then write a clean mirror with only the Lore section. User must confirm.
lore mirror show <file> 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.