📦 deps(thirdparty): update snapshots

This commit is contained in:
ci[bot]
2026-07-13 07:44:54 +00:00
parent cb3f8713ee
commit b3cc57caff
92 changed files with 12712 additions and 97 deletions
@@ -0,0 +1,60 @@
# Audit report template
`audit` writes its output to `.lore/audit/audit-YYYY-MM-DD.md`. This file is read-only with respect to `.lore/*.md` (see main `SKILL.md` Conflict resolution — audit never mutates and never ALERTs).
## Template
```markdown
# Memory Audit Report
> Date: 2026-07-09
> Total entries audited: <N>
> Findings: <X> CONFLICT, <Y> STALE, <Z> UNVERIFIED
## Global (`_global/`)
### CONFLICT
- [CONV-2026-01-20-b1e8] claims "all packages TypeScript strict mode"
Evidence: `packages/legacy/tsconfig.json` has `"strict": false`
### STALE
- [ARCH-2026-01-15-d7a3] references `nx.json`
Evidence: file no longer exists at repo root
### UNVERIFIED
- [DEC-2026-02-03-7c19] last verified 2025-09-12 (>90 days)
## Scope: frontend
### CONFLICT
- ...
### STALE
- ...
### UNVERIFIED
- ...
## Summary
Recommended action: run `lore sync` to address these findings.
Audit itself does not modify any entry.
```
## Severity definitions
| Severity | Meaning |
|---|---|
| `CONFLICT` | Code/config directly contradicts the entry content (e.g. memory says `react@18`, `package.json` says `16`) |
| `STALE` | Entry references a resource (file, API, version) that no longer exists |
| `UNVERIFIED` | Entry's `#verified` date is >90 days; needs re-confirmation |
## Required rules
- The audit report **never** modifies any `.lore/*.md` file.
- The audit report **never** emits ALERT blocks (ALERT noise is contained to `sync` and `query`).
- Audit is a pure read-and-report operation. To act on findings, the user runs `sync`.
## Evidence format
Each finding includes a one-line `Evidence:` reference pointing to the file path and (when possible) line number that triggered the finding. The agent must verify the evidence exists before writing the report.
@@ -0,0 +1,228 @@
# Compatibility policy
This document defines how `lore` evolves without breaking existing user projects. It is the contract between current users and future maintainers. Any change to `.lore/` structure, file formats, Python scripts, mirror templates, or this skill's reference docs must conform to these rules.
## Three principles
1. **Add, never subtract.** New fields, scripts, sections, and reference docs always use new names. Removal happens only after the deprecation cycle (below).
2. **Readers are forward-compatible.** An older skill reading a newer `.lore/` ignores unknown fields, unknown files, and unknown tags. It never errors on unfamiliar content.
3. **Writers are backward-compatible (during transition).** A newer skill detecting an older `.lore/` runs a migration step before writing. It never overwrites old data with new defaults.
## Layer-specific rules
### Layer 1: `.lore/.config.json` schema
- `schema_version` is **required** (integer). See `references/config.md` for handling missing/newer/older values.
- Adding a new field = bump `schema_version` to N+1; old fields stay; the field is added with its default value at first read.
- Removing a field requires the deprecation cycle (one schema version's worth of warnings before hard removal).
- Renaming a field: write the new field, copy the value, mark the old one with `_deprecated: "reason"`. The migration tool handles this in `migrate.py`.
### Layer 2: Entry format
```
- [ARCH-2026-07-10-a3f2] Entry text; reason. #added:2026-07-10 #verified:2026-07-15
```
- IDs (`LAYER-DATE-HASH`) are stable as long as the entry text is unchanged. Editing an entry produces a new ID; old ID stays in history (via git) for `history` queries.
- Tag set is a closed set today: `#added`, `#verified`, `#stale`, `#archived`. Adding a new tag is allowed; old skills' tag parsers (which match `(added|verified|stale|archived)`) silently ignore unknown tags.
- **Never make a tag required.** Required tags break every old entry in every old `.lore/`.
### Layer 3: `.lore/` directory structure
Current canonical layout:
```
.lore/
├── SUMMARY.md
├── _global/
├── scopes/
├── draft/ (init only — temporary)
├── audit/ (audit only)
└── archive/ (referenced in spec; reserved for future)
```
Rules:
- Adding a new top-level directory (e.g., `rejected/` for rejected entries) is non-breaking.
- Renaming an existing directory is breaking — every reference in `references/*.md`, every script, and every user's project breaks.
- Removing a directory is breaking unless that directory was never actually written (e.g., removing `archive/` today is non-breaking because nothing writes there yet).
### Layer 4: Python scripts
Current scripts: `id_hash.py`, `list_entries.py`, `find_stale.py`, `find_duplicates.py`, `history.py`, plus planned `migrate.py`.
Rules:
- **Renaming is breaking.** All names are part of the public surface; they're referenced from `SKILL.md`, `references/*.md`, and downstream tooling. Don't rename; deprecate and add a new one if needed.
- **Removing is breaking.** A deprecated script stays on disk with a clear "Deprecated: use `migrate.py` instead" header for one schema version.
- **Adding a new script is non-breaking.** Reference it from `SKILL.md` reference index on introduction.
- **Changing output format is breaking for `--json` consumers.** Add `--v2-output` or a new flag; old flag keeps old behavior forever.
### Layer 5: Platform mirror files
Mirror files (`CLAUDE.md`, `.cursorrules`, `AGENTS.md`, etc.) follow this contract:
```markdown
## Lore (auto-managed)
... lore content ...
---
## My notes (free edit)
... user content (preserved verbatim) ...
```
Rules:
- `## Lore (auto-managed)` is a **contract string**. Never rename; never remove. Mirror detection regexes depend on it.
- `## My notes (free edit)` is a **contract string**. Never rename; never remove. User-written content depends on it.
- The content between `## Lore` and `---` is lore's domain; content after `---` is the user's. Respect the boundary on every regeneration.
- Adding a new auto-managed section (e.g., `## Sync history (auto-managed)`) is allowed; insert before `---`. Old skills ignore it.
- Changing the index template body (e.g., adding a "Last mirror:" line) is non-breaking: content-based dedup means unchanged mirrors are not rewritten, so old mirrors stay valid.
- **Backward-write safety**: if the existing mirror has no `## My notes (free edit)` section (e.g., a legacy single-section mirror from a pre-v1 project), the first `lore mirror` run must **append** an empty My notes section rather than overwriting the file.
### Layer 6: reference docs
Current docs: `entry-format.md`, `summary-template.md`, `audit-template.md`, `monorepo-detection.md`, `stale-new-markers.md`, `platform-mirrors.md`, `config.md`, `history-command.md`, `compatibility.md` (this file).
Rules:
- **Renaming a reference doc is breaking.** Every external link (issue trackers, blog posts, README badges) breaks. Add a redirect stub instead.
- **Splitting a doc** (e.g., `platform-mirrors.md``mirror-index.md` + `mirror-takeover.md`) requires a stub at the old path that points to the new location. Update `SKILL.md` reference index on the same commit.
- **Removing a doc** is breaking. Mark it `<!-- DEPRECATED: see new-location.md -->` for one schema version, then move to `archive/` (in `references/`, not in `.lore/`).
- **Adding a doc** is non-breaking. Add to `SKILL.md` reference index on introduction.
## Migration tool
`scripts/migrate.py` does not exist in v1. It will be added on the first `schema_version` bump.
**Triggers that will ship the first migration:**
- A new required field is added to `.lore/.config.json`.
- A field is renamed or its accepted values are tightened.
- The entry ID algorithm changes.
Until any of these happen, `schema_version: 1` is the only version and no migration is possible or needed.
Until the script ships, `list_entries.py` emits a one-time warning to stderr if `.lore/.config.json` is missing the `schema_version` field. This nudges users to add the field manually before the first breaking change ships, so future upgrades can detect the version mismatch correctly.
**Template — to be implemented when the first migration is needed:**
```python
#!/usr/bin/env python3
"""Migrate .lore/.config.json from version N to N+1.
Idempotent: running twice produces no further changes.
Refuses to run if schema_version > EXPECTED_FROM.
"""
import json, sys
from pathlib import Path
EXPECTED_FROM = 1 # versions this script can read
TARGET = 2 # current schema version after migration
def migrate(config: dict) -> tuple[dict, list[str]]:
msgs = []
# v1 → v2 transformations go here. Example:
# if config.get("mirror_mode") == "summary":
# config.pop("mirror_mode", None)
# msgs.append('removed deprecated "mirror_mode": "summary"')
config["schema_version"] = TARGET
return config, msgs
def main() -> int:
cfg_path = Path(".lore/.config.json")
if not cfg_path.exists():
print(f"error: {cfg_path} not found", file=sys.stderr)
return 1
cfg = json.loads(cfg_path.read_text(encoding="utf-8"))
current = cfg.get("schema_version", 1)
if current > EXPECTED_FROM:
print(
f"error: schema_version={current} is newer than this skill "
f"can read (max: {EXPECTED_FROM}). Pull latest lore.",
file=sys.stderr,
)
return 2
if current < TARGET:
print(f"migrating v{current} → v{TARGET}...")
cfg, msgs = migrate(cfg)
for m in msgs:
print(f" - {m}")
cfg_path.write_text(json.dumps(cfg, indent=2) + "\n", encoding="utf-8")
print("done.")
return 0
if __name__ == "__main__":
sys.exit(main())
```
## Deprecation workflow
Any capability slated for removal follows a three-stage cycle. The minimum cycle is **two schema versions** (typically 612 months).
| Stage | Schema version | Behavior |
|---|---|---|
| **Announce** | N | Add an entry to `references/deprecations.md` with the feature name, replacement, and removal target version. The skill prints a one-line notice when the feature is used. |
| **Warn** | N+1 | The skill prints a louder warning (with remediation steps) and writes a `_deprecation_warnings_shown` array to `.lore/.config.json` so the warning doesn't repeat. |
| **Remove** | N+2 | Hard delete. Old `.lore/` data is migrated by `migrate.py` to the new format. Users who skipped migrations will see explicit errors pointing at `migrate.py`. |
Skipping a stage is allowed only for security fixes or unreleased features that were never shipped.
## CI enforcement
A compatibility CI job (planned for `.github/workflows/compat.yml`) verifies:
1. **New skill reads old `.lore/`**: checkout a fixture project from `fixtures/v0-project/`, run `list_entries.py`, `history.py`, `find_stale.py` on it. Pass = no exceptions, correct counts.
2. **Old skill reads new `.lore/`**: build a fixture with the latest schema, check out the previous release's scripts, run them. Pass = no exceptions on known fields; unknown fields silently ignored.
3. **No rename or delete in `scripts/` or `references/`**: PR diff against `scripts/` and `references/` filenames; any removed file = failure.
At least one fixture project must be checked in at `fixtures/v0-project/` and re-pinned to a known old schema version after each major release.
## Examples
### Compatible change (additive)
Adding a new `compress_thresholds.max_entries_per_scope` field:
- Bump `schema_version` to 2.
- `migrate.py` v1 → v2: add the new field with default `100` if absent.
- Old skill reads v2 config: sees only the fields it knows; ignores `max_entries_per_scope`.
- New skill reads v1 config: detects `schema_version` mismatch, refuses to write until migration runs.
### Incompatible change (avoid)
Renaming `mirror_mode` to `render_mode`:
- Every existing `.lore/.config.json` would silently lose its `mirror_mode: "index"` setting on next migration (old field dropped, new field absent → defaults kick in).
- Bad. Instead: add `render_mode` as the new canonical field; mark `mirror_mode` as `_deprecated`; keep both for one schema version; eventually remove `mirror_mode` via the deprecation cycle.
### Breaking change (deprecation cycle required)
Removing support for `mirror_mode: "full"`:
- v1: ship `mirror_mode: "full"` as deprecated; full mode still works.
- v2: print warning when `"full"` is set; suggest migrating to `"index"`.
- v3: hard reject `"full"`; `migrate.py` auto-converts to `"index"`.
- Each version's release notes link to the deprecation entry in `references/deprecations.md`.
## Decision checklist
Before merging any change to lore, answer these questions:
1. Does this change add, modify, or remove anything in `.lore/`?
2. Does this change add, modify, or remove any script in `scripts/`?
3. Does this change add, modify, or remove any contract string (`## Lore (auto-managed)`, `## My notes (free edit)`, etc.)?
4. Does this change add, modify, or remove any reference doc filename?
5. Does this change add, modify, or remove any entry tag?
If any answer is "modify" or "remove", the change requires either:
- A migration step in `migrate.py` (for schema changes)
- A deprecation cycle (for removals)
- An entry in `references/deprecations.md`
If all answers are "add" or "no", the change is non-breaking and can ship as a minor or patch release.
@@ -0,0 +1,119 @@
# Configuration reference
`.lore/.config.json` holds user-tunable settings. The file is optional; without it, the skill uses sensible defaults.
## Schema
```json
{
"schema_version": 1,
"auto_mirror": true | false,
"sync_updates_mirror": true | false,
"sync_trust": "high" | "medium" | "low",
"mirror_targets": ["CLAUDE.md"], // optional — auto-detected if absent
"mirror_mode": "index",
"compress_thresholds": {
"max_entries": 500,
"max_days_since_compress": 30
},
"sync_thresholds": {
"min_lines_changed": 50,
"min_directories_changed": 2
}
}
```
## Schema version (`schema_version`)
**Required for new configs** (set automatically by `lore init`). Tracks the schema version of `.lore/.config.json` so future releases can detect old configs before writing.
- **Missing** → treated as `schema_version: 1`. A `[WARN]` notice is printed to stderr by `list_entries.py`; add the field manually to silence it.
- **Equal to skill's expected version** → use as-is.
- **Lower than expected** → refuse to write; ask the user to run the migration script shipped with that future release.
- **Higher than expected** → refuse to read with an error; the user's skill is older than their `.lore/`. They need to upgrade lore (pull latest from upstream) before continuing.
For the full compatibility policy (migration tools, deprecation cycle, reader/writer contracts), see `references/compatibility.md`.
## Field semantics
### `auto_mirror`
Default: `false`.
Controls whether `compress` and `lore mirror` regenerate platform mirrors automatically after the canonical change is accepted.
- `true` — regenerate mirrors automatically
- `false` — ask per target before writing
Note: this flag does **not** affect `sync`. By default `sync` does not touch mirrors at all (see `sync_updates_mirror`).
### `sync_updates_mirror`
Default: `false`.
Controls whether `sync` regenerates platform mirrors as a side effect.
- `false``sync` only writes `.lore/*.md`. Mirrors are updated by `compress` or explicit `lore mirror`. This is the recommended setting to avoid cluttering `git log` of mirror files.
- `true``sync` regenerates mirrors (with content-based dedup) after the canonical change is accepted. Restore this setting if the old "update everything on every sync" behavior is preferred.
### `sync_trust`
Default: `"medium"`.
Controls how much confirmation `sync` requires for individual change types.
- `"high"` — auto-apply everything, including `NEW` and `STALE`. Only `ALERT` blocks interrupt.
- `"medium"` — auto-apply low-risk changes (de-duplicate hits, equivalent REFINEDs). `NEW`, `STALE`, and `ALERT` require confirmation.
- `"low"` — every change requires confirmation, including de-duplicate hits and equivalent REFINEDs.
### `mirror_targets`
Default: auto-detected at runtime (see "If absent" below).
Array of file paths (relative to project root) that should be kept in sync with `.lore/*`. Path must match one of the platform entries in `references/platform-mirrors.md`. Unsupported paths trigger a warning at config-load time.
If absent: mirror targets are auto-detected at runtime by scanning the project root for existing platform files. If no platform files exist, the user is asked via a multi-select question during `init`, the first `mirror` call, or `compress` (when `auto_mirror: true`). See `references/platform-mirrors.md` for the resolution algorithm.
If present: used verbatim. Empty array `[]` is valid and disables mirror generation.
When auto-detection is in effect, `lore init` populates this field with the user's selections so subsequent runs are silent.
### `mirror_mode`
Default: `"index"`.
Only `"index"` is accepted. The mirror renders a small index structure pointing into `.lore/` (see `references/platform-mirrors.md` for the template and adaptive rendering rules). Per-session token cost stays flat (~500 B) regardless of project size.
Any other value (e.g., the historical `"summary"` or `"full"`) is rejected at config-load time with an error. Remove the field, or set it to `"index"`.
### `compress_thresholds`
Defaults: `{"max_entries": 500, "max_days_since_compress": 30}`.
`sync` checks these silently and emits a `[COMPRESS NOTICE]` when tripped. See `SKILL.md` sync procedure.
### `sync_thresholds`
Defaults: `{"min_lines_changed": 50, "min_directories_changed": 2}`.
`sync` only proposes an update when at least one trigger threshold is met (see `SKILL.md` sync trigger threshold). Lowering these values means `sync` proposes updates more often.
## Editing the config
Edit `.lore/.config.json` directly. After editing:
- `sync` and `compress` re-read the config on every run; no restart needed.
- Invalid JSON → fall back to defaults + warn the user.
- After editing, verify with `python scripts/list_entries.py --config-check` (added in a future migration).
- For schema version changes, see `references/compatibility.md`.
## Upgrade path
When a future lore release introduces the first `schema_version` bump:
1. That release ships `scripts/migrate.py` for the specific version bump.
2. The skill prompts: "Your `.lore/.config.json` is v1; lore now expects v2. Run `python scripts/migrate.py` to upgrade."
3. `migrate.py` is idempotent — running it twice is a no-op.
4. After migration, the file is updated in place; no manual editing needed.
In v1, no migration has shipped and `scripts/migrate.py` does not exist. Add `"schema_version": 1` manually to old configs to silence the warning.
@@ -0,0 +1,70 @@
# Entry format reference
Detailed specification for `.lore/` entries. The main `SKILL.md` covers entry structure briefly; this file is the full spec.
## Bullet structure
Each entry is a Markdown bullet (≤ 2 lines), containing:
- **Layer prefix**: `ARCH`, `DEC`, or `CONV`
- **ID**: `LAYER-YYYY-MM-DD-xxxx` where `xxxx` is a 4-char content hash
- **Inline status tags** (at the end of the entry)
```markdown
- [ARCH-2026-07-09-a3f2] Use Next.js App Router; reason: streaming + RSC. #added:2026-07-09
- [DEC-2026-02-03-7c19] Chose Zustand over Redux; reason: 60% less boilerplate. Alternatives: Redux Toolkit, Jotai. #added:2026-02-03
- [CONV-2026-01-20-b1e8] Never commit secrets; use `dotenv` + `.env.local` (gitignored). #added:2026-01-20
- [ARCH-2026-03-10-a1b2] Use TanStack Query for all server state. #added:2026-03-10 #verified:2026-06-15
```
## ID generation
The 4-char `xxxx` is the first 4 hex chars of `sha256(entry text)`. This makes IDs:
- **Deterministic**: rewriting the same fact produces the same ID
- **Conflict-free** under concurrent writes by multiple agents
- **Reverse-lookup-able** by audit tools
If two entries have identical content (hash collision, statistically rare), add a distinguishing word to one and recompute.
## Tag specification
| Tag | Meaning |
|---|---|
| `#added:YYYY-MM-DD` | When the entry was created |
| `#verified:YYYY-MM-DD` | Last time a human or audit confirmed the entry is still true |
| `#stale:YYYY-MM-DD` | Flagged by `sync` as superseded or contradicted; user decides keep/archive |
| `#archived:YYYY-MM-DD` | Moved to `archive/` |
Multiple tags can co-exist on one entry (e.g. `#added:2026-01-15 #verified:2026-06-01`).
## Cross-file references
When `SUMMARY.md` or another file references an entry, qualify it with the file path to avoid ID collisions across scopes:
```
[scopes/frontend/DECISIONS.md#DEC-2026-02-03-7c19]
[_global/CONVENTIONS.md#CONV-2026-01-20-b1e8]
```
The path is relative to `.lore/`.
## Splitting vs. single entries
If a fact can't fit in ≤ 2 lines, split into multiple entries and cross-reference them by ID:
```markdown
- [ARCH-2026-07-09-a3f2] Use Next.js App Router. #added:2026-07-09
- [DEC-2026-07-09-b1e8] Reason: streaming + RSC, see [ARCH-2026-07-09-a3f2]. #added:2026-07-09
```
Instead of stuffing them into a single overly long bullet.
## What counts as "atomic"
A fact is atomic if it answers exactly one question:
- "What is the frontend framework?" → `ARCH` entry about Next.js
- "Why Next.js not Remix?" → `DEC` entry referencing the `ARCH` entry
If your entry answers two questions, split it.
@@ -0,0 +1,106 @@
# `lore history` — full specification
Read-only command. Lists git commits related to a memory entry, a file,
or a scope, since the entry's `#added` date. Output to stdout only;
never writes to `.lore/`.
## Synopsis
```
lore history <entry-id>
lore history <file-path>
lore history --scope=<name>
lore history --since=<YYYY-MM-DD>
lore history --json
```
## Forms
| Form | Argument shape | Example | Behavior |
|---|---|---|---|
| Entry | `[A-Z]+-\d{4}-\d{2}-\d{2}-[a-f0-9]{4}` | `lore history DEC-2026-02-03-7c19` | Locate entry in `.lore/`, derive its `#added` date and code file, then `git log` since that date. |
| File | contains `/` or starts with `.` | `lore history frontend/src/store/index.ts` | Run `git log --since=1970-01-01` on the given path. |
| Scope | `--scope=<name>` only | `lore history --scope=frontend` | For each `*.md` in `.lore/scopes/<name>/`, run file form on the lore file path itself. |
## Code-file resolution (entry form)
Priority:
1. First backtick-quoted path in entry.text that looks like a file
(e.g. `src/store/index.ts`).
2. Scope directory at the project root (e.g. entry scope `frontend`
`frontend/`).
3. Project root `.` for entries in `_global/`.
If the regex finds no path, falls back to the scope directory.
## Data source
`git` CLI only. No network calls. Requires:
- A git repository at or above the current working directory.
- The `git` executable on `PATH`.
## Output
### Markdown (default)
Header block: `Entry`, `Since`, `File`, `Commits`. One section per
commit with `## <short-hash> (<date>, <author>)`, subject, optional
`Body:` line, optional `Refs:` line. A "Suggested next step" footer
appears only when at least one commit is found.
### JSON (`--json`)
```json
{
"entry_id": "DEC-2026-02-03-7c19",
"lore_file": "scopes/frontend/DECISIONS.md",
"code_file": "frontend/src/store/index.ts",
"since": "2026-02-03",
"since_source": "entry_added",
"commits": [
{
"hash": "...",
"short": "abc1234",
"author": "alice",
"date": "2026-04-12",
"subject": "Use Zustand v4",
"body": "Migrate notes here.",
"refs": ["#234"]
}
]
}
```
## Error handling
| Condition | Exit code | Message |
|---|---|---|
| No argument | 2 | `error: missing argument` |
| Unrecognized argument | 2 | `error: unrecognized argument: <arg>` |
| `.lore/` not found | 2 | `error: .lore/ not found. Run 'lore init' first.` |
| Entry not in index | 3 | `error: Entry <id> not found. Available: ...` |
| Not a git repo | 4 | `error: Not a git repository. ...` |
| `git` missing | 5 | `error: git executable not found on PATH.` |
| Bad scope name | 6 | `error: Scope '<name>' not found. Available: ...` |
| `git log` failure | 7 | `error: git log failed: <stderr>` |
| Entry missing `#added` | 0 (warning) | `warning: entry has no #added tag; using full history` |
## Exit codes summary
- `0` — success (including "0 commits found" case)
- `2` — usage / configuration error
- `3` — entry lookup failure
- `4` — not a git repo
- `5` — git CLI missing
- `6` — invalid scope
- `7` — git command failed
## Why this exists
`lore sync` reads `git diff` (working-tree deltas). It never reads
commit history. `lore history` fills that gap: given a memory entry,
it shows the commits that introduced or modified the underlying code,
letting the agent answer "why does this decision exist?" with a pointer
to the original commit instead of an LLM-generated guess.
@@ -0,0 +1,78 @@
# Monorepo detection rules
`init` needs to identify whether the project is a monorepo and how to split scopes. This file lists the detection rules per tool.
## Detection order
Check markers in this order; the first match determines scope layout:
1. pnpm workspaces
2. Yarn workspaces
3. npm workspaces
4. Lerna
5. Nx
6. Rush
7. Cargo workspaces
8. Go workspaces
9. Bazel
No monorepo marker → fall back to `_global/` only (single-scope project).
## Per-tool rules
### pnpm workspaces
- Marker: `pnpm-workspace.yaml` at repo root
- Read: `packages:` field, e.g. `packages: [frontend, backend, shared/*]`
- One scope per listed package directory
### Yarn workspaces (classic / berry)
- Marker: `package.json` top-level `workspaces` field
- Example: `"workspaces": ["packages/*"]`
- One scope per glob-resolved directory
### npm workspaces
- Same as Yarn (npm 7+ uses the same `package.json#workspaces` field)
### Lerna
- Marker: `lerna.json`
- Read: `packages` field (array of paths)
- One scope per path
### Nx
- Marker: `nx.json` or `workspace.json`
- Nx typically delegates package discovery to npm/yarn workspaces — read both
- One scope per resolved package
### Rush
- Marker: `rush.json`
- Read: `projects` array (each entry has a `packageName` and directory)
### Cargo workspaces
- Marker: `Cargo.toml` top-level `[workspace]` table
- Read: `members` array
- One scope per member crate
### Go workspaces
- Marker: `go.work`
- Read: `use` directives (one per module)
- One scope per module
### Bazel
- Marker: `MODULE.bazel` or `WORKSPACE`
- Bazel repos are deeply nested; precise extraction is fragile. Fallback: collapse to one scope per top-level directory and let the user override.
## Scope naming
- Default: directory name (`frontend/` → scope `frontend`)
- If multiple directories belong to one logical scope (e.g. `packages/web` and `packages/mobile` are both "frontend"), agent should ask the user whether to merge
- Nested monorepos (`packages/web/components/`) are **not** supported as nested scopes. Flatten to `web`.
## When detection fails
If detection succeeds but the resulting scopes don't match the user's mental model, agent should:
1. Show the proposed scope list
2. Let the user rename / merge / split scopes
3. Proceed with the corrected list
This is part of the init confirmation step (see main `SKILL.md` init step 2).
@@ -0,0 +1,346 @@
# 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, use it verbatim (auto-detect skipped)
if "mirror_targets" in config:
return list(config["mirror_targets"])
# 2. Scan repo root for existing platform files (see Scan candidates)
detected = scan_existing_platform_files(repo_root)
if detected:
return detected
# 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 selected
```
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.
### 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. Read the current state of `.lore/SUMMARY.md` and the scope-tagged index.
2. For each configured mirror target, read the existing file and detect the section boundary.
3. Compute the new Lore section content.
4. **Content-based dedup**: if the new Lore section content is byte-identical to the existing one, skip writing. Report "No changes needed: `<file>`".
5. If different, replace the Lore section (full rewrite, no merge with previous content). Preserve the My notes section verbatim.
6. 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. |
@@ -0,0 +1,62 @@
# Stale / New marking convention
When `sync` proposes a change, it never silently mutates files. Instead it emits one or more of these markers. The user reads the proposal and accepts/rejects per marker type.
## Marker types
| Marker | Purpose |
|---|---|
| `[NEW]` | Propose adding a new entry |
| `[STALE]` | Propose marking an existing entry as superseded/contradicted |
| `[REFINED]` | Propose updating an existing entry's text in place |
| `[ALERT]` | Conflicting signal detected during sync that needs human resolution |
| `[COMPRESS NOTICE]` | Threshold tripped; suggest running `compress` after this sync |
## Full example
```markdown
## [NEW] Proposed additions
- [scopes/frontend/ARCHITECTURE.md] [ARCH-2026-07-09-b4d2] Use `react-hook-form` for all forms. #added:2026-07-09
- [scopes/frontend/CONVENTIONS.md] [CONV-2026-07-09-c5e1] Never use `any` in TypeScript; prefer `unknown` + narrowing. #added:2026-07-09
## [STALE] Candidates for archive
- [scopes/frontend/ARCHITECTURE.md] [ARCH-2026-01-15-d7a3] Use Pages Router (Next.js). #stale:2026-07-09
Evidence: `frontend/package.json` shows `"next": "^14.0.0"` with `app/` directory present.
## [REFINED] Existing entries updated
- [scopes/frontend/DECISIONS.md] [DEC-2026-02-03-7c19] (was: "use Zustand") → "use Zustand v4+ with slices pattern" #verified:2026-07-09
## [ALERT] Conflicting signals detected during sync
- Sync proposes `[CONV-2026-07-09-c5e1]` (no `any`), but `[CONV-2026-06-01-f0a1]` already says "use `any` sparingly in test mocks". Resolution: refined entry above clarifies the exception.
## [COMPRESS NOTICE]
- Memory bank has 612 entries; last compression 47 days ago. Consider running `lore compress` after this sync.
```
## User reply semantics
The user can reply with:
- `"accept all"` — apply every `[NEW]`, `[STALE]`, and `[REFINED]` in the proposal
- `"accept only NEW"` — add new entries, leave existing untouched
- `"accept NEW + REFINE"` — add new and refine, do not mark anything stale
- `"drop STALE #d7a3"` — skip one specific stale entry
- `"reject all"` — discard the entire proposal
For partial acceptance, the user should explicitly list which items to apply.
## Marker → file operation mapping
| Marker | File action |
|---|---|
| `[NEW]` | Append a new bullet to the named file, with `#added:<today>` |
| `[STALE]` | Append `#stale:<today>` tag to the existing entry; entry stays in the file |
| `[REFINED]` | Replace the entry text in place, keep the ID, update `#verified:<today>` |
| `[ALERT]` | No direct file change; only marks the conflict for user resolution |
| `[COMPRESS NOTICE]` | No file change; advisory only |
Note: `[STALE]` does not delete or move anything. The entry remains in its file with a `#stale` tag until the user (or a later sync) explicitly moves it to `archive/`. This keeps the rollback path clean.
## When audit uses these markers
`audit` does **not** use these markers. It writes its own severity tags (`[CONFLICT]`, `[STALE]`, `[UNVERIFIED]`) into the audit report file under `.lore/audit/`. The naming overlap (`[STALE]` in sync vs `[STALE]` severity in audit) is intentional — both refer to the same concept (entry no longer accurate) but operate in different files with different downstream actions.
@@ -0,0 +1,98 @@
# SUMMARY.md template
`compress` generates/refreshes `SUMMARY.md` from existing entries. This file holds the schema and worked example.
## Skeleton
```markdown
# .lore SUMMARY
> Last compressed: <YYYY-MM-DD>
> Total entries: <N> across <M> scopes
## Global (`_global/`)
### Architecture
- <bullet> — [_global/ARCHITECTURE.md#<ID>]
- ...
### Decisions
- ...
### Conventions
- ...
## Scope: <name>
### Architecture
- ...
### Decisions
- ...
### Conventions
- ...
## Scope: <name2>
...
```
## Selection rule (35 entries per scope per layer)
For each (scope, layer) tuple, pick entries by this priority:
1. Most recent `#verified` date wins
2. Tiebreaker: most recent `#added` date
3. Tiebreaker: entries that contain "primary" / "main" / "core" / "use <X>" — these are typically the anchor facts
If a (scope, layer) has fewer than 3 entries, include all of them.
If a (scope, layer) is empty, omit the subsection entirely.
## Worked example
```markdown
# .lore SUMMARY
> Last compressed: 2026-07-09
> Total entries: 247 across 3 scopes
## Global (`_global/`)
### Architecture
- Monorepo with pnpm workspaces + Turborepo — [_global/ARCHITECTURE.md#ARCH-2026-01-15-d7a3]
- Node.js 20 baseline — [_global/ARCHITECTURE.md#ARCH-2026-02-01-9b1c]
### Decisions
- Rejected Nx → chose Turborepo (faster builds, simpler config) — [_global/DECISIONS.md#DEC-2026-02-03-7c19]
### Conventions
- All packages use TypeScript strict mode — [_global/CONVENTIONS.md#CONV-2026-01-20-b1e8]
## Scope: frontend
### Architecture
- Next.js 14 App Router — [scopes/frontend/ARCHITECTURE.md#ARCH-2026-03-10-a1b2]
- TanStack Query for server state — [scopes/frontend/ARCHITECTURE.md#ARCH-2026-03-15-e5f6]
### Decisions
- Zustand over Redux (60% less boilerplate) — [scopes/frontend/DECISIONS.md#DEC-2026-02-03-7c19]
### Conventions
- No default exports — [scopes/frontend/CONVENTIONS.md#CONV-2026-04-12-c3d4]
## Scope: backend
### Architecture
- Node.js + Fastify + PostgreSQL — [scopes/backend/ARCHITECTURE.md#ARCH-2026-01-15-e5f6]
### Decisions
- Fastify over Express (3x throughput in our benchmarks) — [scopes/backend/DECISIONS.md#DEC-2026-02-10-a8c9]
### Conventions
- All DB queries go through repository pattern — [scopes/backend/CONVENTIONS.md#CONV-2026-03-01-b1d2]
```
## Idempotency
Running `compress` twice without intervening `sync`s produces identical content (modulo the `Last compressed:` date). This is intentional — compress is a pure projection of the underlying entries.