From 8d24d1439dc992c2a78b2cd8d16ce7e2ca27533b Mon Sep 17 00:00:00 2001 From: Ed Heltzel <402910+edheltzel@users.noreply.github.com> Date: Wed, 15 Jul 2026 13:54:31 -0400 Subject: [PATCH] fix(ci): align the CLAUDE.md contract with the symlink, unbreaking main 5336d5f made CLAUDE.md a symlink to AGENTS.md (git mode 120000) but left the test and docs asserting the old `@AGENTS.md` shim, so main has been red since: (fail) CLAUDE.md is a shim that @-imports AGENTS.md (no duplicated content) Expected substring or pattern: /^@AGENTS\.md\s*$/m Reading CLAUDE.md now follows the link and yields all of AGENTS.md, so both assertions failed: the regex, and `not.toContain('.agents/atlas/artifacts/')`. The contract the 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 cannot drift. So assert the LINK, not the content. A content assertion here would just be asserting AGENTS.md twice. Adds a guard for the symlink's known failure mode: with core.symlinks=false (a common Windows default) git materializes CLAUDE.md as a plain file whose whole content is the literal string "AGENTS.md", and Claude Code would silently load that as the entire guide. The new test fails loudly instead. This affects only contributors -- the CLAUDE.md installed for users comes from templates/CLAUDE.md.template and is unaffected. Docs realigned (they described a mechanism that no longer exists): - AGENTS.md:3, :37, :105 -- "thin shim that @-imports" -> "symlink" - AGENTS.md Agent Context Files -- documents the symlink, the /init hazard, and the core.symlinks=false limitation - docs/releasing.md pre-flight -- `cat CLAUDE.md` would now print the whole guide, so the check is `ls -l CLAUDE.md`; recovery is `ln -sf AGENTS.md CLAUDE.md` --- AGENTS.md | 12 +++++---- docs/releasing.md | 14 +++++------ tests/commands/scout-workflow.test.ts | 35 +++++++++++++++++++++------ 3 files changed, 41 insertions(+), 20 deletions(-) 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/'); }); });