diff --git a/AGENTS.md b/AGENTS.md index c601d42..b0277f3 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -1,6 +1,6 @@ # Recall — Persistent Memory for AI Coding Agents -> **Canonical agent guide.** This is the single source of truth for developing Recall, written to the cross-agent [`AGENTS.md`](https://agents.md) standard. `CLAUDE.md` is a thin shim that `@`-imports this file so Claude Code loads it automatically — do not duplicate content there. See **Agent Context Files** at the bottom. +> **Canonical agent guide.** This is the single source of truth for developing Recall, written to the cross-agent [`AGENTS.md`](https://agents.md) standard. `CLAUDE.md` is a symlink to this file so Claude Code loads it automatically — do not duplicate content there. See **Agent Context Files** at the bottom. ## Project Overview @@ -34,7 +34,7 @@ Top-level directories, by purpose (one line each — not a file enumeration): - `templates/` — install templates (`CLAUDE.md.template`, `mcp.json.template`) - `assets/` — README banner + VHS demo tapes / gifs -Key root files: `AGENTS.md` (canonical guide), `CLAUDE.md` (shim that `@`-imports it), `CHANGELOG.md`, `FOR_CLAUDE.md` / `FOR_OPENCODE.md` / `FOR_PI.md` (host usage guides), `CONTEXT.md`, `install.sh` / `update.sh` / `uninstall.sh`, `package.json`, `tsconfig.json`. +Key root files: `AGENTS.md` (canonical guide), `CLAUDE.md` (symlink to it), `CHANGELOG.md`, `FOR_CLAUDE.md` / `FOR_OPENCODE.md` / `FOR_PI.md` (host usage guides), `CONTEXT.md`, `install.sh` / `update.sh` / `uninstall.sh`, `package.json`, `tsconfig.json`. For the **live source tree** (every file, current symbols, callers), use the **codegraph** index (`codegraph_*` MCP tools / `codegraph_explore`) or `fd` — never an enumerated tree that drifts. `docs/architecture.md` is complementary: it documents the installed/runtime layout under `~/.agents/Recall/`, not the source tree. @@ -102,7 +102,7 @@ Workers run in an isolated worktree (`/ce-worktree`): verify `pwd` is the worktr - **Lifecycle scripts**: shared behavior lives in `lib/install-lib.sh` only — never re-implemented across `install.sh` / `update.sh` / `uninstall.sh`. - **Prompt / guide / workflow text**: when the same workflow must reach multiple guides (`FOR_CLAUDE.md`, `FOR_PI.md`, `FOR_OPENCODE.md`, `opencode/recall-memory.md`), author the workflow body **once** as a canonical block; each platform guide carries only its platform-specific tool-name mapping plus a reference to that canonical block. Do not hand-copy a full workflow into four files and hope they stay aligned. - **Host memory bootstraps**: generated `## MEMORY` sections point to the installed `Recall_GUIDE.md` and live MCP schemas; they never copy tool call syntax and carry ``. `recall_append_memory_section` is the shared separator-safe append path. `recall_memory_section_mutate` is the single Claude/Pi ownership classifier used by install, update, and uninstall: install/update refresh marked sections and migrate only normalized exact matches of complete legacy-generated bodies; uninstall removes those same owned sections. Unmarked customized/external sections survive. The marker explicitly retains Recall ownership even when the body is edited; remove it before taking external ownership. When `~/.claude/rules/memory.md` contains a Recall-specific marker (`Recall_GUIDE.md` or `recall-memory`), that external rule owns the Claude contract and install/update leave `~/.claude/CLAUDE.md` unchanged. An unrelated memory rule does not suppress Recall configuration. -- **Agent context files**: this `AGENTS.md` is canonical; `CLAUDE.md` is a shim that imports it. Never copy content into `CLAUDE.md`. +- **Agent context files**: this `AGENTS.md` is canonical; `CLAUDE.md` is a symlink to it. Never replace that symlink with a copy. - **Tests / docs**: assert or document a fact in one place; cross-reference rather than restate it. Before adding code or content, search for an existing definition and extend it. Code review **must** reject diffs that introduce copy-paste duplication. @@ -138,10 +138,12 @@ Before adding code or content, search for an existing definition and extend it. `AGENTS.md` (this file) is the **single source of truth** for agent guidance. To load it, each host points at it its own way: -- **Claude Code** does not read `AGENTS.md` natively — it auto-loads `CLAUDE.md`. So `CLAUDE.md` is a one-line shim containing `@AGENTS.md`, which Claude Code expands into context at launch (the same way an inlined CLAUDE.md would load). All real content lives here. +- **Claude Code** does not read `AGENTS.md` natively — it auto-loads `CLAUDE.md`. So `CLAUDE.md` is a **symlink** to `AGENTS.md` (git mode `120000`): Claude Code opens `CLAUDE.md`, git hands it this file, and only one copy ever exists on disk. - Other hosts that honor the `AGENTS.md` standard read this file directly. -**Do not run `/init` in this repo.** Claude Code's `/init` regenerates `CLAUDE.md` from scratch, which would overwrite the `@AGENTS.md` shim with a full duplicate copy and silently reintroduce the drift this layout exists to prevent. If `CLAUDE.md` ever ends up with content other than the import line, restore it to the shim and move any new guidance into this file. +**Do not run `/init` in this repo.** Claude Code's `/init` regenerates `CLAUDE.md` from scratch, which would replace the symlink with a full duplicate copy and silently reintroduce the drift this layout exists to prevent. If `CLAUDE.md` ever becomes a regular file, restore it with `ln -sf AGENTS.md CLAUDE.md` and move any new guidance into this file. + +**Known limitation — checkouts without symlink support.** With `core.symlinks=false` (a common Windows default) git materializes `CLAUDE.md` as a plain file whose entire content is the literal string `AGENTS.md` — and Claude Code would load *that* as the guide, silently and with no error. `tests/commands/scout-workflow.test.ts` asserts both that `CLAUDE.md` is a symlink and that it has not degraded into a literal-path file, so this fails loudly in CI instead of shipping a broken guide. It affects only contributors working on Recall; the `CLAUDE.md` that Recall *installs* for users comes from `templates/CLAUDE.md.template` and is unaffected. # DOX framework diff --git a/docs/releasing.md b/docs/releasing.md index 2d5fa9c..6ea9021 100644 --- a/docs/releasing.md +++ b/docs/releasing.md @@ -8,19 +8,19 @@ these steps. ## Pre-flight: agent context files -Before tagging, confirm `CLAUDE.md` is still the one-line `@AGENTS.md` shim and -has not been regenerated into a full duplicate: +Before tagging, confirm `CLAUDE.md` is still a symlink to `AGENTS.md` and has +not been regenerated into a full duplicate: ```bash -# Should print the @AGENTS.md import (and the HTML comment) — nothing else. -cat CLAUDE.md +# Should print: CLAUDE.md -> AGENTS.md +ls -l CLAUDE.md ``` -`AGENTS.md` is the canonical agent guide; `CLAUDE.md` only `@`-imports it so +`AGENTS.md` is the canonical agent guide; `CLAUDE.md` is a symlink to it so Claude Code auto-loads it. **Never run `/init` in this repo** — it rewrites `CLAUDE.md` from scratch and reintroduces the duplication. If `CLAUDE.md` has -drifted, restore the shim and fold any new content back into `AGENTS.md` before -releasing. +drifted into a regular file, restore it with `ln -sf AGENTS.md CLAUDE.md` and +fold any new content back into `AGENTS.md` before releasing. ## Source of truth: `CHANGELOG.md` diff --git a/tests/commands/scout-workflow.test.ts b/tests/commands/scout-workflow.test.ts index 2187175..204b0f5 100644 --- a/tests/commands/scout-workflow.test.ts +++ b/tests/commands/scout-workflow.test.ts @@ -11,11 +11,11 @@ // 3. DRY: the four platform guides reference the canonical block and do NOT // hand-copy its body (per the MANDATORY DRY rule in CLAUDE.md/AGENTS.md). // 4. The artifacts policy is recorded in canonical AGENTS.md; CLAUDE.md is a -// shim that @-imports it. +// symlink to it. // 5. recall-scout is listed in the user-facing docs (agent-skills, README). import { describe, test, expect } from 'bun:test'; -import { readFileSync } from 'fs'; +import { readFileSync, lstatSync, readlinkSync } from 'fs'; import { join } from 'path'; const repoRoot = join(import.meta.dir, '..', '..'); @@ -147,8 +147,8 @@ describe('DRY — guides reference the canonical block, never copy it', () => { }); describe('artifacts policy is recorded in the canonical dev guide', () => { - // AGENTS.md is the single source of truth; CLAUDE.md is a thin shim that - // @-imports it so Claude Code loads it. The policy lives in AGENTS.md only. + // AGENTS.md is the single source of truth; CLAUDE.md is a symlink to it so + // Claude Code loads it. The policy lives in AGENTS.md only. test('AGENTS.md documents the .agents/atlas/artifacts/ policy', () => { const body = read('AGENTS.md'); expect(body).toContain('.agents/atlas/artifacts/'); @@ -156,11 +156,30 @@ describe('artifacts policy is recorded in the canonical dev guide', () => { expect(body).toMatch(/\.agents\/atlas\/handoffs\/.*reserved|reserved for session handoff/i); }); - test('CLAUDE.md is a shim that @-imports AGENTS.md (no duplicated content)', () => { + test('CLAUDE.md is a symlink to AGENTS.md (no duplicated content)', () => { + // CLAUDE.md is a SYMLINK to AGENTS.md, not a file containing `@AGENTS.md`. + // The contract this test defends is unchanged — "CLAUDE.md must never carry + // a second copy of the guide" — but a symlink satisfies it by construction: + // there is literally one file, so content can't drift. Assert the LINK, not + // the content; reading CLAUDE.md now yields all of AGENTS.md, which is the + // point, so any content assertion here would be asserting AGENTS.md twice. + const link = lstatSync(join(repoRoot, 'CLAUDE.md')); + expect(link.isSymbolicLink()).toBe(true); + expect(readlinkSync(join(repoRoot, 'CLAUDE.md'))).toBe('AGENTS.md'); + + // Resolving it must land on the canonical guide, not a stale copy. + expect(read('CLAUDE.md')).toBe(read('AGENTS.md')); + }); + + // Guards the known cross-platform failure mode of the symlink approach: on a + // checkout with core.symlinks=false (common on Windows), git materializes + // CLAUDE.md as a PLAIN FILE whose whole content is the string "AGENTS.md" — + // and Claude Code would silently load that as the entire guide. The assertion + // above fails loudly in that case rather than shipping a broken guide. + test('CLAUDE.md did not degrade into a literal-path text file', () => { const body = read('CLAUDE.md'); - expect(body).toMatch(/^@AGENTS\.md\s*$/m); - // It must NOT hand-copy the policy — that would reintroduce drift. - expect(body).not.toContain('.agents/atlas/artifacts/'); + expect(body.trim()).not.toBe('AGENTS.md'); + expect(body).toContain('.agents/atlas/artifacts/'); }); });