From 9f89e0b015f5e0aff8d8fa018d810158d3f06256 Mon Sep 17 00:00:00 2001 From: amsraman Date: Fri, 5 Jun 2026 14:36:35 -0700 Subject: [PATCH 1/7] change source --- docs/ai_builder/integrations/agents_md.md | 10 ++++- docs/getting_started/installation.md | 33 +++++++++++++++ .../src/reflex_base/constants/__init__.py | 2 + .../src/reflex_base/constants/config.py | 11 +++++ reflex/reflex.py | 13 +++++- reflex/utils/frontend_skeleton.py | 33 ++++++++++++++- tests/units/utils/test_utils.py | 40 +++++++++++++++++++ 7 files changed, 139 insertions(+), 3 deletions(-) diff --git a/docs/ai_builder/integrations/agents_md.md b/docs/ai_builder/integrations/agents_md.md index 4a6b8291623..c50af4b376f 100644 --- a/docs/ai_builder/integrations/agents_md.md +++ b/docs/ai_builder/integrations/agents_md.md @@ -17,7 +17,15 @@ The [reflex-dev/agent-skills](https://github.com/reflex-dev/agent-skills) reposi ## Installation -Download the template into your project root, next to `rxconfig.py`: +The fastest way is to scaffold it as part of a new app. Pass `--agents` to `reflex init` and Reflex writes a starter `AGENTS.md` into the project root alongside `rxconfig.py`: + +```bash +reflex init --agents +``` + +An existing `AGENTS.md` is never overwritten, so it is safe to re-run. + +To add the template to an existing project, download it into the project root, next to `rxconfig.py`: ```bash curl -fsSL https://raw.githubusercontent.com/reflex-dev/agent-skills/main/AGENTS.md -o AGENTS.md diff --git a/docs/getting_started/installation.md b/docs/getting_started/installation.md index d0155e28968..d0fea6f2016 100644 --- a/docs/getting_started/installation.md +++ b/docs/getting_started/installation.md @@ -2,6 +2,39 @@ ~3 minutes · Requires Python 3.10+. +```md alert info +# Reading this as an AI agent or coding assistant? + +You're likely here to build a Reflex app for someone. Reflex does **not** behave like the Flask/FastAPI/React code in your training data — read this before writing any code. + +**Get the current docs (don't rely on training data — the API changes between versions):** + +- Fetch [`https://reflex.dev/llms.txt`](https://reflex.dev/llms.txt) for a token-efficient map of the docs and core concepts. +- Use the [Reflex MCP server](/docs/ai-builder/integrations/mcp-overview) (`https://build.reflex.dev/mcp`) for live component/prop lookup so you don't hallucinate APIs. *(MCP access is an enterprise feature.)* +- Run `uv run reflex --version` and trust the live docs for that version over memory. + +**Drop a rules file in the project root** (`AGENTS.md`, or `CLAUDE.md`/`.cursorrules` for your tool) so these conventions persist across your session — a starter is below. +``` + +Starter rules file: + +```text +# Reflex conventions + +- Reflex is pure Python that compiles to a React frontend. Do NOT write JS, HTML, or JSX. +- Components are function calls that return components; pass props as keyword args. +- NEVER use plain Python control flow on state Vars inside the render tree. + No `if`, `for`, `len()`, or f-strings over a Var — use `rx.cond`, `rx.foreach`, + and Var operators instead. (This is the most common mistake.) +- State lives in `rx.State` subclasses. State only mutates inside event-handler + methods — never at module load or render time. Derived values use `@rx.var`. +- Event handlers may be `async` and may `yield` to push intermediate UI updates. +- Always run commands with `uv run` (e.g. `uv run reflex run`). Never bare `python`. +- `.web/` is generated output — never edit or commit it. `rxconfig.py` is the config entry point. +- Verify your work headlessly: `CI=1 uv run reflex run` (frontend :3000, backend :8000), + add `--loglevel debug` to diagnose failures. +``` + ## Virtual Environment We recommend [uv](https://docs.astral.sh/uv/) as the default; [venv](https://docs.python.org/3/library/venv.html), [conda](https://conda.io/), and [poetry](https://python-poetry.org/) are alternatives. diff --git a/packages/reflex-base/src/reflex_base/constants/__init__.py b/packages/reflex-base/src/reflex_base/constants/__init__.py index 1d9480761a4..e442355d9a2 100644 --- a/packages/reflex-base/src/reflex_base/constants/__init__.py +++ b/packages/reflex-base/src/reflex_base/constants/__init__.py @@ -39,6 +39,7 @@ ) from .config import ( ALEMBIC_CONFIG, + AgentsMd, Config, DefaultPorts, Expiration, @@ -82,6 +83,7 @@ "ROUTE_NOT_FOUND", "SESSION_STORAGE", "SETTER_PREFIX", + "AgentsMd", "Bun", "ColorMode", "CompileContext", diff --git a/packages/reflex-base/src/reflex_base/constants/config.py b/packages/reflex-base/src/reflex_base/constants/config.py index e1e8a4c6d74..104bb05920a 100644 --- a/packages/reflex-base/src/reflex_base/constants/config.py +++ b/packages/reflex-base/src/reflex_base/constants/config.py @@ -49,6 +49,17 @@ class GitIgnore(SimpleNamespace): } +class AgentsMd(SimpleNamespace): + """AGENTS.md constants.""" + + # The AGENTS.md file written to the app root. + FILE = Path("AGENTS.md") + # The canonical AGENTS.md maintained in the reflex-dev/agent-skills repo. + CANONICAL_URL = ( + "https://raw.githubusercontent.com/reflex-dev/agent-skills/main/AGENTS.md" + ) + + class PyprojectToml(SimpleNamespace): """Pyproject.toml constants.""" diff --git a/reflex/reflex.py b/reflex/reflex.py index bcfec77dd77..ba1c5933ae2 100644 --- a/reflex/reflex.py +++ b/reflex/reflex.py @@ -58,6 +58,7 @@ def _init( name: str, template: str | None = None, ai: bool = False, + agents: bool = False, ): """Initialize a new Reflex app in the given directory.""" from reflex.utils import exec, frontend_skeleton, prerequisites, templates @@ -89,6 +90,10 @@ def _init( # Initialize the .gitignore. frontend_skeleton.initialize_gitignore() + # Optionally write a sample AGENTS.md for AI coding agents. + if agents: + frontend_skeleton.initialize_agents_md() + template_msg = f" using the {template} template" if template else "" if Path(constants.PyprojectToml.FILE).exists(): needs_user_manual_update = False @@ -122,13 +127,19 @@ def _init( is_flag=True, help="Use AI to create the initial template. Cannot be used with existing app or `--template` option.", ) +@click.option( + "--agents", + is_flag=True, + help="Write a sample AGENTS.md to guide AI coding agents working in the app.", +) def init( name: str, template: str | None, ai: bool, + agents: bool, ): """Initialize a new Reflex app in the current directory.""" - _init(name, template, ai) + _init(name, template, ai, agents) def _compile_app(*, avoid_dirty_check: bool = True): diff --git a/reflex/utils/frontend_skeleton.py b/reflex/utils/frontend_skeleton.py index 6212fb43121..01d970ec35a 100644 --- a/reflex/utils/frontend_skeleton.py +++ b/reflex/utils/frontend_skeleton.py @@ -11,7 +11,7 @@ from reflex.compiler import templates from reflex.compiler.utils import write_file -from reflex.utils import console, path_ops +from reflex.utils import console, net, path_ops from reflex.utils.prerequisites import get_project_hash, get_web_dir from reflex.utils.registry import get_npm_registry @@ -43,6 +43,37 @@ def initialize_gitignore( gitignore_file.write_text("\n".join(files_to_ignore) + "\n") +def initialize_agents_md( + agents_file: Path = constants.AgentsMd.FILE, + url: str = constants.AgentsMd.CANONICAL_URL, +): + """Write the AGENTS.md for AI coding agents, fetched from the canonical repo. + + Does not overwrite an existing file so a user's customizations are preserved. + Aborts if the canonical file cannot be fetched. + + Args: + agents_file: The AGENTS.md file to create in the app root. + url: The canonical AGENTS.md to download. + """ + if agents_file.exists(): + console.debug(f"{agents_file} already exists, skipping.") + return + + import httpx + + console.debug(f"Fetching {url}") + try: + response = net.get(url, timeout=5) + response.raise_for_status() + except httpx.HTTPError as e: + console.error(f"Failed to fetch AGENTS.md from {url} due to {e}.") + raise SystemExit(1) from None + + console.debug(f"Creating {agents_file}") + agents_file.write_text(response.text) + + def _read_dependency_file(file_path: Path) -> tuple[str | None, str | None]: """Read a dependency file with a forgiving encoding strategy. diff --git a/tests/units/utils/test_utils.py b/tests/units/utils/test_utils.py index ffc3c975780..94b1b45ee4e 100644 --- a/tests/units/utils/test_utils.py +++ b/tests/units/utils/test_utils.py @@ -421,6 +421,46 @@ def test_initialize_non_existent_gitignore( assert set(file_content) - expected == set() +def test_initialize_agents_md_fetches_canonical(tmp_path, mocker): + """Test that AGENTS.md is fetched from the canonical repo when absent.""" + agents_file = tmp_path / "AGENTS.md" + response = mocker.Mock() + response.text = "# canonical agents" + get = mocker.patch("reflex.utils.net.get", return_value=response) + + frontend_skeleton.initialize_agents_md( + agents_file=agents_file, url="http://x/AGENTS.md" + ) + + get.assert_called_once_with("http://x/AGENTS.md", timeout=5) + assert agents_file.read_text() == "# canonical agents" + + +def test_initialize_agents_md_preserves_existing(tmp_path, mocker): + """Test that an existing AGENTS.md is never overwritten or re-fetched.""" + agents_file = tmp_path / "AGENTS.md" + agents_file.write_text("custom content") + get = mocker.patch("reflex.utils.net.get") + + frontend_skeleton.initialize_agents_md(agents_file=agents_file) + + assert agents_file.read_text() == "custom content" + get.assert_not_called() + + +def test_initialize_agents_md_aborts_on_fetch_failure(tmp_path, mocker): + """Test that a failed fetch aborts init without leaving a partial file.""" + import httpx + + agents_file = tmp_path / "AGENTS.md" + mocker.patch("reflex.utils.net.get", side_effect=httpx.ConnectError("boom")) + + with pytest.raises(SystemExit): + frontend_skeleton.initialize_agents_md(agents_file=agents_file) + + assert not agents_file.exists() + + def test_initialize_requirements_txt_skips_when_pyproject_exists(tmp_path): """Test that pyproject-based apps do not get a requirements.txt file.""" pyproject_file = tmp_path / "pyproject.toml" From 3d10de3ca4881af84c756cfd857979332b6bb467 Mon Sep 17 00:00:00 2001 From: amsraman Date: Wed, 10 Jun 2026 08:47:27 -0700 Subject: [PATCH 2/7] comments --- docs/ai_builder/integrations/agents_md.md | 10 ++-- docs/getting_started/installation.md | 2 +- .../src/reflex_base/constants/config.py | 9 ++-- reflex/reflex.py | 8 ++-- reflex/utils/frontend_skeleton.py | 30 ++++++++---- tests/units/utils/test_utils.py | 46 +++++++++++++++---- 6 files changed, 76 insertions(+), 29 deletions(-) diff --git a/docs/ai_builder/integrations/agents_md.md b/docs/ai_builder/integrations/agents_md.md index c50af4b376f..3d74d238b2c 100644 --- a/docs/ai_builder/integrations/agents_md.md +++ b/docs/ai_builder/integrations/agents_md.md @@ -17,13 +17,13 @@ The [reflex-dev/agent-skills](https://github.com/reflex-dev/agent-skills) reposi ## Installation -The fastest way is to scaffold it as part of a new app. Pass `--agents` to `reflex init` and Reflex writes a starter `AGENTS.md` into the project root alongside `rxconfig.py`: +`reflex init` writes a starter `AGENTS.md` into the project root alongside `rxconfig.py` by default (pass `--no-agents` to opt out): ```bash -reflex init --agents +reflex init ``` -An existing `AGENTS.md` is never overwritten, so it is safe to re-run. +The Reflex-provided content sits between `reflex managed` begin/end markers. Anything you add outside the markers is preserved when init refreshes the managed section, and a pre-existing `AGENTS.md` without markers is never touched, so it is safe to re-run. To add the template to an existing project, download it into the project root, next to `rxconfig.py`: @@ -71,7 +71,9 @@ Keep entries short and imperative — assistants follow concise, direct instruct ## Keeping Files Updated -Reflex evolves quickly. If you used `curl` to download the template, re-run the same command to refresh it: +Reflex evolves quickly. If `reflex init` created your `AGENTS.md`, re-running `reflex init` refreshes the content between the managed markers while preserving everything you added outside them. + +If you used `curl` to download the template, re-run the same command to refresh it (note this replaces the whole file): ```bash curl -fsSL https://raw.githubusercontent.com/reflex-dev/agent-skills/main/AGENTS.md -o AGENTS.md diff --git a/docs/getting_started/installation.md b/docs/getting_started/installation.md index d0fea6f2016..fb64130ac93 100644 --- a/docs/getting_started/installation.md +++ b/docs/getting_started/installation.md @@ -13,7 +13,7 @@ You're likely here to build a Reflex app for someone. Reflex does **not** behave - Use the [Reflex MCP server](/docs/ai-builder/integrations/mcp-overview) (`https://build.reflex.dev/mcp`) for live component/prop lookup so you don't hallucinate APIs. *(MCP access is an enterprise feature.)* - Run `uv run reflex --version` and trust the live docs for that version over memory. -**Drop a rules file in the project root** (`AGENTS.md`, or `CLAUDE.md`/`.cursorrules` for your tool) so these conventions persist across your session — a starter is below. +**Drop a rules file in the project root** (`AGENTS.md`, or `CLAUDE.md`/`.cursorrules` for your tool) so these conventions persist across your session — `reflex init` writes a starter `AGENTS.md` for you by default, and a minimal fallback is below. ``` Starter rules file: diff --git a/packages/reflex-base/src/reflex_base/constants/config.py b/packages/reflex-base/src/reflex_base/constants/config.py index 104bb05920a..ede3c8316ef 100644 --- a/packages/reflex-base/src/reflex_base/constants/config.py +++ b/packages/reflex-base/src/reflex_base/constants/config.py @@ -55,9 +55,12 @@ class AgentsMd(SimpleNamespace): # The AGENTS.md file written to the app root. FILE = Path("AGENTS.md") # The canonical AGENTS.md maintained in the reflex-dev/agent-skills repo. - CANONICAL_URL = ( - "https://raw.githubusercontent.com/reflex-dev/agent-skills/main/AGENTS.md" - ) + # TEMPORARY: pointing at the aditya/agentsmd branch (PR #13) instead of main. + # Revert to .../agent-skills/main/AGENTS.md once that PR merges. + CANONICAL_URL = "https://raw.githubusercontent.com/reflex-dev/agent-skills/aditya/agentsmd/AGENTS.md" + # Markers delimiting the Reflex-managed section; user content outside them is preserved. + BEGIN_MARKER = "" + END_MARKER = "" class PyprojectToml(SimpleNamespace): diff --git a/reflex/reflex.py b/reflex/reflex.py index ba1c5933ae2..3c340fccea2 100644 --- a/reflex/reflex.py +++ b/reflex/reflex.py @@ -90,7 +90,7 @@ def _init( # Initialize the .gitignore. frontend_skeleton.initialize_gitignore() - # Optionally write a sample AGENTS.md for AI coding agents. + # Write or refresh the AGENTS.md for AI coding agents. if agents: frontend_skeleton.initialize_agents_md() @@ -128,9 +128,9 @@ def _init( help="Use AI to create the initial template. Cannot be used with existing app or `--template` option.", ) @click.option( - "--agents", - is_flag=True, - help="Write a sample AGENTS.md to guide AI coding agents working in the app.", + "--agents/--no-agents", + default=True, + help="Write an AGENTS.md to guide AI coding agents working in the app (enabled by default).", ) def init( name: str, diff --git a/reflex/utils/frontend_skeleton.py b/reflex/utils/frontend_skeleton.py index 01d970ec35a..ecbf2ebbec1 100644 --- a/reflex/utils/frontend_skeleton.py +++ b/reflex/utils/frontend_skeleton.py @@ -47,17 +47,21 @@ def initialize_agents_md( agents_file: Path = constants.AgentsMd.FILE, url: str = constants.AgentsMd.CANONICAL_URL, ): - """Write the AGENTS.md for AI coding agents, fetched from the canonical repo. + """Write or refresh the Reflex-managed section of AGENTS.md. - Does not overwrite an existing file so a user's customizations are preserved. - Aborts if the canonical file cannot be fetched. + The canonical content is wrapped in begin/end markers; user content outside + the markers is preserved on refresh. An existing file without markers is + never touched. A failed fetch is a warning, not an error, so init still + succeeds offline. Args: - agents_file: The AGENTS.md file to create in the app root. + agents_file: The AGENTS.md file to create or refresh in the app root. url: The canonical AGENTS.md to download. """ - if agents_file.exists(): - console.debug(f"{agents_file} already exists, skipping.") + begin, end = constants.AgentsMd.BEGIN_MARKER, constants.AgentsMd.END_MARKER + existing = agents_file.read_text() if agents_file.exists() else None + if existing is not None and (begin not in existing or end not in existing): + console.debug(f"{agents_file} has no managed section, skipping.") return import httpx @@ -67,11 +71,19 @@ def initialize_agents_md( response = net.get(url, timeout=5) response.raise_for_status() except httpx.HTTPError as e: - console.error(f"Failed to fetch AGENTS.md from {url} due to {e}.") - raise SystemExit(1) from None + console.warn(f"Failed to fetch AGENTS.md from {url} due to {e}. Skipping.") + return + + managed = f"{begin}\n{response.text.strip()}\n{end}" + if existing is None: + content = managed + "\n" + else: + before, _, rest = existing.partition(begin) + after = rest.partition(end)[2] + content = before + managed + after console.debug(f"Creating {agents_file}") - agents_file.write_text(response.text) + agents_file.write_text(content) def _read_dependency_file(file_path: Path) -> tuple[str | None, str | None]: diff --git a/tests/units/utils/test_utils.py b/tests/units/utils/test_utils.py index 94b1b45ee4e..fec3569f787 100644 --- a/tests/units/utils/test_utils.py +++ b/tests/units/utils/test_utils.py @@ -422,7 +422,7 @@ def test_initialize_non_existent_gitignore( def test_initialize_agents_md_fetches_canonical(tmp_path, mocker): - """Test that AGENTS.md is fetched from the canonical repo when absent.""" + """Test that AGENTS.md is fetched and written inside markers when absent.""" agents_file = tmp_path / "AGENTS.md" response = mocker.Mock() response.text = "# canonical agents" @@ -433,11 +433,15 @@ def test_initialize_agents_md_fetches_canonical(tmp_path, mocker): ) get.assert_called_once_with("http://x/AGENTS.md", timeout=5) - assert agents_file.read_text() == "# canonical agents" + assert agents_file.read_text() == ( + f"{constants.AgentsMd.BEGIN_MARKER}\n" + "# canonical agents\n" + f"{constants.AgentsMd.END_MARKER}\n" + ) -def test_initialize_agents_md_preserves_existing(tmp_path, mocker): - """Test that an existing AGENTS.md is never overwritten or re-fetched.""" +def test_initialize_agents_md_preserves_unmanaged_existing(tmp_path, mocker): + """Test that an existing AGENTS.md without markers is never touched or re-fetched.""" agents_file = tmp_path / "AGENTS.md" agents_file.write_text("custom content") get = mocker.patch("reflex.utils.net.get") @@ -448,16 +452,42 @@ def test_initialize_agents_md_preserves_existing(tmp_path, mocker): get.assert_not_called() -def test_initialize_agents_md_aborts_on_fetch_failure(tmp_path, mocker): - """Test that a failed fetch aborts init without leaving a partial file.""" +def test_initialize_agents_md_refreshes_managed_section(tmp_path, mocker): + """Test that only the marked section is refreshed, preserving user content.""" + agents_file = tmp_path / "AGENTS.md" + agents_file.write_text( + "# my project notes\n\n" + f"{constants.AgentsMd.BEGIN_MARKER}\n" + "old canonical content\n" + f"{constants.AgentsMd.END_MARKER}\n\n" + "more user notes\n" + ) + response = mocker.Mock() + response.text = "new canonical content" + mocker.patch("reflex.utils.net.get", return_value=response) + + frontend_skeleton.initialize_agents_md(agents_file=agents_file) + + assert agents_file.read_text() == ( + "# my project notes\n\n" + f"{constants.AgentsMd.BEGIN_MARKER}\n" + "new canonical content\n" + f"{constants.AgentsMd.END_MARKER}\n\n" + "more user notes\n" + ) + + +def test_initialize_agents_md_warns_on_fetch_failure(tmp_path, mocker): + """Test that a failed fetch warns without aborting or leaving a partial file.""" import httpx agents_file = tmp_path / "AGENTS.md" mocker.patch("reflex.utils.net.get", side_effect=httpx.ConnectError("boom")) + warn = mocker.patch("reflex.utils.console.warn") - with pytest.raises(SystemExit): - frontend_skeleton.initialize_agents_md(agents_file=agents_file) + frontend_skeleton.initialize_agents_md(agents_file=agents_file) + warn.assert_called_once() assert not agents_file.exists() From 6fd05f1dca3c180fd04ff77c3c213e703d7ebe95 Mon Sep 17 00:00:00 2001 From: amsraman Date: Wed, 10 Jun 2026 11:52:01 -0700 Subject: [PATCH 3/7] oops --- packages/reflex-base/src/reflex_base/constants/config.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/reflex-base/src/reflex_base/constants/config.py b/packages/reflex-base/src/reflex_base/constants/config.py index ede3c8316ef..b8663c29d48 100644 --- a/packages/reflex-base/src/reflex_base/constants/config.py +++ b/packages/reflex-base/src/reflex_base/constants/config.py @@ -55,9 +55,9 @@ class AgentsMd(SimpleNamespace): # The AGENTS.md file written to the app root. FILE = Path("AGENTS.md") # The canonical AGENTS.md maintained in the reflex-dev/agent-skills repo. - # TEMPORARY: pointing at the aditya/agentsmd branch (PR #13) instead of main. - # Revert to .../agent-skills/main/AGENTS.md once that PR merges. - CANONICAL_URL = "https://raw.githubusercontent.com/reflex-dev/agent-skills/aditya/agentsmd/AGENTS.md" + CANONICAL_URL = ( + "https://raw.githubusercontent.com/reflex-dev/agent-skills/main/AGENTS.md" + ) # Markers delimiting the Reflex-managed section; user content outside them is preserved. BEGIN_MARKER = "" END_MARKER = "" From 6bb0491131ecfcffdc927b0dde820616d9839bca Mon Sep 17 00:00:00 2001 From: amsraman Date: Wed, 10 Jun 2026 13:55:50 -0700 Subject: [PATCH 4/7] Robust AGENTS.md marker handling and CLAUDE.md bridge - Prepend managed section when markers are missing; repair unpaired or out-of-order markers instead of dropping user content. - Create a one-line @AGENTS.md CLAUDE.md bridge when CLAUDE.md is absent. - Manage AGENTS.md as usual when CLAUDE.md imports it or is the same file (symlink); otherwise write the managed section into CLAUDE.md directly, and into AGENTS.md too when it already exists. Co-Authored-By: Claude Fable 5 --- .../src/reflex_base/constants/config.py | 8 +- reflex/utils/frontend_skeleton.py | 60 +++++-- tests/units/utils/test_utils.py | 170 ++++++++++++++++-- 3 files changed, 207 insertions(+), 31 deletions(-) diff --git a/packages/reflex-base/src/reflex_base/constants/config.py b/packages/reflex-base/src/reflex_base/constants/config.py index b8663c29d48..1ff594f12b1 100644 --- a/packages/reflex-base/src/reflex_base/constants/config.py +++ b/packages/reflex-base/src/reflex_base/constants/config.py @@ -54,10 +54,12 @@ class AgentsMd(SimpleNamespace): # The AGENTS.md file written to the app root. FILE = Path("AGENTS.md") + # The CLAUDE.md bridge file; Claude Code reads CLAUDE.md rather than AGENTS.md. + CLAUDE_FILE = Path("CLAUDE.md") + # Import line that pulls AGENTS.md into CLAUDE.md. + CLAUDE_IMPORT = "@AGENTS.md" # The canonical AGENTS.md maintained in the reflex-dev/agent-skills repo. - CANONICAL_URL = ( - "https://raw.githubusercontent.com/reflex-dev/agent-skills/main/AGENTS.md" - ) + CANONICAL_URL = "https://raw.githubusercontent.com/reflex-dev/agent-skills/aditya/agentsmd/AGENTS.md" # Markers delimiting the Reflex-managed section; user content outside them is preserved. BEGIN_MARKER = "" END_MARKER = "" diff --git a/reflex/utils/frontend_skeleton.py b/reflex/utils/frontend_skeleton.py index ecbf2ebbec1..02d284227ae 100644 --- a/reflex/utils/frontend_skeleton.py +++ b/reflex/utils/frontend_skeleton.py @@ -45,24 +45,39 @@ def initialize_gitignore( def initialize_agents_md( agents_file: Path = constants.AgentsMd.FILE, + claude_file: Path = constants.AgentsMd.CLAUDE_FILE, url: str = constants.AgentsMd.CANONICAL_URL, ): """Write or refresh the Reflex-managed section of AGENTS.md. The canonical content is wrapped in begin/end markers; user content outside - the markers is preserved on refresh. An existing file without markers is - never touched. A failed fetch is a warning, not an error, so init still - succeeds offline. + the markers is preserved on refresh. If an existing file has no valid + begin/end pair (markers missing, unpaired, or out of order), stray markers + are dropped and the managed section is prepended. A failed fetch is a + warning, not an error, so init still succeeds offline. + + Claude Code reads CLAUDE.md rather than AGENTS.md, so: if CLAUDE.md is + missing, a one-line bridge importing AGENTS.md is created; if it is the + same file as AGENTS.md (symlink) or already imports it, AGENTS.md is + managed as usual; otherwise the managed section goes into CLAUDE.md + directly (and also into AGENTS.md if it already exists), leaving the + user's content otherwise untouched. Args: agents_file: The AGENTS.md file to create or refresh in the app root. + claude_file: The CLAUDE.md file bridging AGENTS.md for Claude Code. url: The canonical AGENTS.md to download. """ begin, end = constants.AgentsMd.BEGIN_MARKER, constants.AgentsMd.END_MARKER - existing = agents_file.read_text() if agents_file.exists() else None - if existing is not None and (begin not in existing or end not in existing): - console.debug(f"{agents_file} has no managed section, skipping.") - return + targets = [agents_file] + claude_exists = claude_file.exists() + write_claude_bridge = not claude_exists and not claude_file.is_symlink() + if ( + claude_exists + and not (agents_file.exists() and claude_file.samefile(agents_file)) + and constants.AgentsMd.CLAUDE_IMPORT not in claude_file.read_text() + ): + targets = [agents_file, claude_file] if agents_file.exists() else [claude_file] import httpx @@ -75,15 +90,28 @@ def initialize_agents_md( return managed = f"{begin}\n{response.text.strip()}\n{end}" - if existing is None: - content = managed + "\n" - else: - before, _, rest = existing.partition(begin) - after = rest.partition(end)[2] - content = before + managed + after - - console.debug(f"Creating {agents_file}") - agents_file.write_text(content) + for target in targets: + existing = target.read_text() if target.exists() else None + if existing is None: + content = managed + "\n" + else: + begin_idx = existing.find(begin) + end_idx = ( + existing.find(end, begin_idx + len(begin)) if begin_idx != -1 else -1 + ) + if end_idx != -1: + content = ( + existing[:begin_idx] + managed + existing[end_idx + len(end) :] + ) + else: + # No valid begin..end pair: drop stray markers and prepend the section. + remainder = existing.replace(begin, "").replace(end, "").strip("\n") + content = managed + (f"\n\n{remainder}\n" if remainder else "\n") + console.debug(f"Creating {target}") + target.write_text(content) + if write_claude_bridge: + console.debug(f"Creating {claude_file}") + claude_file.write_text(f"{constants.AgentsMd.CLAUDE_IMPORT}\n") def _read_dependency_file(file_path: Path) -> tuple[str | None, str | None]: diff --git a/tests/units/utils/test_utils.py b/tests/units/utils/test_utils.py index fec3569f787..4c60761e62e 100644 --- a/tests/units/utils/test_utils.py +++ b/tests/units/utils/test_utils.py @@ -422,14 +422,15 @@ def test_initialize_non_existent_gitignore( def test_initialize_agents_md_fetches_canonical(tmp_path, mocker): - """Test that AGENTS.md is fetched and written inside markers when absent.""" + """Test that AGENTS.md is fetched and a CLAUDE.md bridge is created when absent.""" agents_file = tmp_path / "AGENTS.md" + claude_file = tmp_path / "CLAUDE.md" response = mocker.Mock() response.text = "# canonical agents" get = mocker.patch("reflex.utils.net.get", return_value=response) frontend_skeleton.initialize_agents_md( - agents_file=agents_file, url="http://x/AGENTS.md" + agents_file=agents_file, claude_file=claude_file, url="http://x/AGENTS.md" ) get.assert_called_once_with("http://x/AGENTS.md", timeout=5) @@ -438,18 +439,66 @@ def test_initialize_agents_md_fetches_canonical(tmp_path, mocker): "# canonical agents\n" f"{constants.AgentsMd.END_MARKER}\n" ) + assert claude_file.read_text() == "@AGENTS.md\n" -def test_initialize_agents_md_preserves_unmanaged_existing(tmp_path, mocker): - """Test that an existing AGENTS.md without markers is never touched or re-fetched.""" +def test_initialize_agents_md_prepends_to_unmanaged_existing(tmp_path, mocker): + """Test that the managed section is prepended to an existing file without markers.""" agents_file = tmp_path / "AGENTS.md" - agents_file.write_text("custom content") - get = mocker.patch("reflex.utils.net.get") + agents_file.write_text("custom content\n") + response = mocker.Mock() + response.text = "canonical content" + mocker.patch("reflex.utils.net.get", return_value=response) + + frontend_skeleton.initialize_agents_md( + agents_file=agents_file, claude_file=tmp_path / "CLAUDE.md" + ) + + assert agents_file.read_text() == ( + f"{constants.AgentsMd.BEGIN_MARKER}\n" + "canonical content\n" + f"{constants.AgentsMd.END_MARKER}\n\n" + "custom content\n" + ) + + +@pytest.mark.parametrize( + "malformed", + [ + f"user notes\n{constants.AgentsMd.END_MARKER}\nstale\n{constants.AgentsMd.BEGIN_MARKER}\nmore notes\n", + f"user notes\n{constants.AgentsMd.BEGIN_MARKER}\nunclosed\n", + f"user notes\n{constants.AgentsMd.END_MARKER}\norphaned\n", + ], +) +def test_initialize_agents_md_repairs_malformed_markers(tmp_path, mocker, malformed): + """Test that out-of-order or unpaired markers are dropped and the section prepended. + + Args: + tmp_path: pytest temporary directory fixture. + mocker: pytest-mock fixture. + malformed: An AGENTS.md body with an invalid marker arrangement. + """ + agents_file = tmp_path / "AGENTS.md" + agents_file.write_text(malformed) + response = mocker.Mock() + response.text = "canonical content" + mocker.patch("reflex.utils.net.get", return_value=response) - frontend_skeleton.initialize_agents_md(agents_file=agents_file) + frontend_skeleton.initialize_agents_md( + agents_file=agents_file, claude_file=tmp_path / "CLAUDE.md" + ) - assert agents_file.read_text() == "custom content" - get.assert_not_called() + content = agents_file.read_text() + managed = ( + f"{constants.AgentsMd.BEGIN_MARKER}\n" + "canonical content\n" + f"{constants.AgentsMd.END_MARKER}" + ) + assert content.startswith(managed + "\n") + rest = content.removeprefix(managed) + assert constants.AgentsMd.BEGIN_MARKER not in rest + assert constants.AgentsMd.END_MARKER not in rest + assert "user notes" in rest def test_initialize_agents_md_refreshes_managed_section(tmp_path, mocker): @@ -466,7 +515,9 @@ def test_initialize_agents_md_refreshes_managed_section(tmp_path, mocker): response.text = "new canonical content" mocker.patch("reflex.utils.net.get", return_value=response) - frontend_skeleton.initialize_agents_md(agents_file=agents_file) + frontend_skeleton.initialize_agents_md( + agents_file=agents_file, claude_file=tmp_path / "CLAUDE.md" + ) assert agents_file.read_text() == ( "# my project notes\n\n" @@ -478,17 +529,112 @@ def test_initialize_agents_md_refreshes_managed_section(tmp_path, mocker): def test_initialize_agents_md_warns_on_fetch_failure(tmp_path, mocker): - """Test that a failed fetch warns without aborting or leaving a partial file.""" + """Test that a failed fetch warns without writing AGENTS.md or the bridge.""" import httpx agents_file = tmp_path / "AGENTS.md" + claude_file = tmp_path / "CLAUDE.md" mocker.patch("reflex.utils.net.get", side_effect=httpx.ConnectError("boom")) warn = mocker.patch("reflex.utils.console.warn") - frontend_skeleton.initialize_agents_md(agents_file=agents_file) + frontend_skeleton.initialize_agents_md( + agents_file=agents_file, claude_file=claude_file + ) warn.assert_called_once() assert not agents_file.exists() + assert not claude_file.exists() + + +def test_initialize_agents_md_skips_bridge_when_claude_imports_agents(tmp_path, mocker): + """Test that a CLAUDE.md importing AGENTS.md is left untouched.""" + agents_file = tmp_path / "AGENTS.md" + claude_file = tmp_path / "CLAUDE.md" + claude_file.write_text("@AGENTS.md\n\n# my claude notes\n") + response = mocker.Mock() + response.text = "canonical content" + mocker.patch("reflex.utils.net.get", return_value=response) + + frontend_skeleton.initialize_agents_md( + agents_file=agents_file, claude_file=claude_file + ) + + assert claude_file.read_text() == "@AGENTS.md\n\n# my claude notes\n" + assert "canonical content" in agents_file.read_text() + + +def test_initialize_agents_md_targets_claude_without_import(tmp_path, mocker): + """Test that the managed section goes into a CLAUDE.md lacking the import.""" + agents_file = tmp_path / "AGENTS.md" + claude_file = tmp_path / "CLAUDE.md" + claude_file.write_text("# my claude notes\n") + response = mocker.Mock() + response.text = "canonical content" + mocker.patch("reflex.utils.net.get", return_value=response) + + frontend_skeleton.initialize_agents_md( + agents_file=agents_file, claude_file=claude_file + ) + + assert claude_file.read_text() == ( + f"{constants.AgentsMd.BEGIN_MARKER}\n" + "canonical content\n" + f"{constants.AgentsMd.END_MARKER}\n\n" + "# my claude notes\n" + ) + assert not agents_file.exists() + + +def test_initialize_agents_md_targets_both_when_agents_exists(tmp_path, mocker): + """Test that both files are managed when CLAUDE.md lacks the import but AGENTS.md exists.""" + agents_file = tmp_path / "AGENTS.md" + agents_file.write_text( + f"{constants.AgentsMd.BEGIN_MARKER}\n" + "old content\n" + f"{constants.AgentsMd.END_MARKER}\n\n" + "# agents notes\n" + ) + claude_file = tmp_path / "CLAUDE.md" + claude_file.write_text("# my claude notes\n") + response = mocker.Mock() + response.text = "canonical content" + mocker.patch("reflex.utils.net.get", return_value=response) + + frontend_skeleton.initialize_agents_md( + agents_file=agents_file, claude_file=claude_file + ) + + managed = ( + f"{constants.AgentsMd.BEGIN_MARKER}\n" + "canonical content\n" + f"{constants.AgentsMd.END_MARKER}" + ) + assert agents_file.read_text() == f"{managed}\n\n# agents notes\n" + assert claude_file.read_text() == f"{managed}\n\n# my claude notes\n" + + +def test_initialize_agents_md_handles_symlinked_claude(tmp_path, mocker): + """Test that a CLAUDE.md symlinked to AGENTS.md is managed as one file.""" + agents_file = tmp_path / "AGENTS.md" + agents_file.write_text("shared notes\n") + claude_file = tmp_path / "CLAUDE.md" + claude_file.symlink_to(agents_file) + response = mocker.Mock() + response.text = "canonical content" + mocker.patch("reflex.utils.net.get", return_value=response) + + frontend_skeleton.initialize_agents_md( + agents_file=agents_file, claude_file=claude_file + ) + + assert claude_file.is_symlink() + assert agents_file.read_text() == ( + f"{constants.AgentsMd.BEGIN_MARKER}\n" + "canonical content\n" + f"{constants.AgentsMd.END_MARKER}\n\n" + "shared notes\n" + ) + assert claude_file.read_text() == agents_file.read_text() def test_initialize_requirements_txt_skips_when_pyproject_exists(tmp_path): From a53291ab37b2c86c03c6da2a46d50afc48706512 Mon Sep 17 00:00:00 2001 From: amsraman Date: Wed, 10 Jun 2026 16:25:21 -0700 Subject: [PATCH 5/7] Split initialize_agents_md into planner and applier _plan_agents_md decides (file, action) pairs; _apply_agents_md_action performs a single action; initialize_agents_md just fetches and applies. No behavior change. Co-Authored-By: Claude Fable 5 --- reflex/utils/frontend_skeleton.py | 135 ++++++++++++++++++++---------- 1 file changed, 89 insertions(+), 46 deletions(-) diff --git a/reflex/utils/frontend_skeleton.py b/reflex/utils/frontend_skeleton.py index 02d284227ae..81027759a69 100644 --- a/reflex/utils/frontend_skeleton.py +++ b/reflex/utils/frontend_skeleton.py @@ -3,6 +3,7 @@ import json import random from pathlib import Path +from typing import Literal from reflex_base import constants from reflex_base.config import Config, get_config @@ -43,41 +44,99 @@ def initialize_gitignore( gitignore_file.write_text("\n".join(files_to_ignore) + "\n") +AgentsMdAction = Literal["managed", "bridge"] + + +def _plan_agents_md( + agents_file: Path, claude_file: Path +) -> list[tuple[Path, AgentsMdAction]]: + """Decide which files receive the managed section or the CLAUDE.md bridge. + + Claude Code reads CLAUDE.md rather than AGENTS.md, so: if CLAUDE.md is + missing, AGENTS.md is managed and a bridge importing it is planned; if + CLAUDE.md is the same file as AGENTS.md (symlink) or already imports it, + only AGENTS.md is managed; otherwise CLAUDE.md is managed directly, along + with AGENTS.md if it already exists. + + Args: + agents_file: The AGENTS.md file in the app root. + claude_file: The CLAUDE.md file in the app root. + + Returns: + (file, action) pairs to apply in order. + """ + if not claude_file.exists(): + plan: list[tuple[Path, AgentsMdAction]] = [(agents_file, "managed")] + # A broken symlink gets no bridge; writing it would create the target. + if not claude_file.is_symlink(): + plan.append((claude_file, "bridge")) + return plan + if ( + agents_file.exists() and claude_file.samefile(agents_file) + ) or constants.AgentsMd.CLAUDE_IMPORT in claude_file.read_text(): + return [(agents_file, "managed")] + if agents_file.exists(): + return [(agents_file, "managed"), (claude_file, "managed")] + return [(claude_file, "managed")] + + +def _apply_agents_md_action( + file: Path, action: AgentsMdAction, managed_agents_md_text: str +): + """Apply a planned AGENTS.md action to a file. + + For "managed", the marker-wrapped section replaces the existing valid + begin..end span; if the file has no valid pair (markers missing, unpaired, + or out of order), stray markers are dropped and the section is prepended, + preserving user content. For "bridge", the one-line import is written. + + Args: + file: The file to write. + action: The action to apply. + managed_agents_md_text: The marker-wrapped canonical content. + """ + begin, end = constants.AgentsMd.BEGIN_MARKER, constants.AgentsMd.END_MARKER + if action == "bridge": + content = f"{constants.AgentsMd.CLAUDE_IMPORT}\n" + elif not file.exists(): + content = managed_agents_md_text + "\n" + else: + existing = file.read_text() + begin_idx = existing.find(begin) + end_idx = existing.find(end, begin_idx + len(begin)) if begin_idx != -1 else -1 + if end_idx != -1: + content = ( + existing[:begin_idx] + + managed_agents_md_text + + existing[end_idx + len(end) :] + ) + else: + # No valid begin..end pair: drop stray markers and prepend the section. + remainder = existing.replace(begin, "").replace(end, "").strip("\n") + content = managed_agents_md_text + ( + f"\n\n{remainder}\n" if remainder else "\n" + ) + console.debug(f"Creating {file}") + file.write_text(content) + + def initialize_agents_md( agents_file: Path = constants.AgentsMd.FILE, claude_file: Path = constants.AgentsMd.CLAUDE_FILE, url: str = constants.AgentsMd.CANONICAL_URL, ): - """Write or refresh the Reflex-managed section of AGENTS.md. + """Write or refresh the Reflex-managed section of AGENTS.md and CLAUDE.md. - The canonical content is wrapped in begin/end markers; user content outside - the markers is preserved on refresh. If an existing file has no valid - begin/end pair (markers missing, unpaired, or out of order), stray markers - are dropped and the managed section is prepended. A failed fetch is a - warning, not an error, so init still succeeds offline. - - Claude Code reads CLAUDE.md rather than AGENTS.md, so: if CLAUDE.md is - missing, a one-line bridge importing AGENTS.md is created; if it is the - same file as AGENTS.md (symlink) or already imports it, AGENTS.md is - managed as usual; otherwise the managed section goes into CLAUDE.md - directly (and also into AGENTS.md if it already exists), leaving the - user's content otherwise untouched. + Fetches the canonical content, then applies the plan from + _plan_agents_md() to each file. A failed fetch is a warning, not an + error, so init still succeeds offline. Args: agents_file: The AGENTS.md file to create or refresh in the app root. claude_file: The CLAUDE.md file bridging AGENTS.md for Claude Code. url: The canonical AGENTS.md to download. """ - begin, end = constants.AgentsMd.BEGIN_MARKER, constants.AgentsMd.END_MARKER - targets = [agents_file] - claude_exists = claude_file.exists() - write_claude_bridge = not claude_exists and not claude_file.is_symlink() - if ( - claude_exists - and not (agents_file.exists() and claude_file.samefile(agents_file)) - and constants.AgentsMd.CLAUDE_IMPORT not in claude_file.read_text() - ): - targets = [agents_file, claude_file] if agents_file.exists() else [claude_file] + plan = _plan_agents_md(agents_file, claude_file) import httpx @@ -89,29 +148,13 @@ def initialize_agents_md( console.warn(f"Failed to fetch AGENTS.md from {url} due to {e}. Skipping.") return - managed = f"{begin}\n{response.text.strip()}\n{end}" - for target in targets: - existing = target.read_text() if target.exists() else None - if existing is None: - content = managed + "\n" - else: - begin_idx = existing.find(begin) - end_idx = ( - existing.find(end, begin_idx + len(begin)) if begin_idx != -1 else -1 - ) - if end_idx != -1: - content = ( - existing[:begin_idx] + managed + existing[end_idx + len(end) :] - ) - else: - # No valid begin..end pair: drop stray markers and prepend the section. - remainder = existing.replace(begin, "").replace(end, "").strip("\n") - content = managed + (f"\n\n{remainder}\n" if remainder else "\n") - console.debug(f"Creating {target}") - target.write_text(content) - if write_claude_bridge: - console.debug(f"Creating {claude_file}") - claude_file.write_text(f"{constants.AgentsMd.CLAUDE_IMPORT}\n") + managed_agents_md_text = ( + f"{constants.AgentsMd.BEGIN_MARKER}\n" + f"{response.text.strip()}\n" + f"{constants.AgentsMd.END_MARKER}" + ) + for file, action in plan: + _apply_agents_md_action(file, action, managed_agents_md_text) def _read_dependency_file(file_path: Path) -> tuple[str | None, str | None]: From cb3b20016fb4224c0591b368038901ad9e7d77e6 Mon Sep 17 00:00:00 2001 From: amsraman Date: Wed, 10 Jun 2026 16:39:39 -0700 Subject: [PATCH 6/7] Add changelog fragments for #6620 Co-Authored-By: Claude Fable 5 --- news/6620.feature.md | 1 + packages/reflex-base/news/6620.feature.md | 1 + 2 files changed, 2 insertions(+) create mode 100644 news/6620.feature.md create mode 100644 packages/reflex-base/news/6620.feature.md diff --git a/news/6620.feature.md b/news/6620.feature.md new file mode 100644 index 00000000000..a048273313c --- /dev/null +++ b/news/6620.feature.md @@ -0,0 +1 @@ +`reflex init` now writes a Reflex-managed section into `AGENTS.md` (fetched from the canonical source and delimited by markers that preserve surrounding user content), and bridges it for Claude Code by creating a `CLAUDE.md` importing `@AGENTS.md` — or, if a `CLAUDE.md` exists without the import, managing the section there directly. diff --git a/packages/reflex-base/news/6620.feature.md b/packages/reflex-base/news/6620.feature.md new file mode 100644 index 00000000000..f671bd9e0d8 --- /dev/null +++ b/packages/reflex-base/news/6620.feature.md @@ -0,0 +1 @@ +Add `AgentsMd` constants (canonical URL, managed-section markers, and `CLAUDE.md` bridge) supporting `reflex init` AGENTS.md generation. From d6ab0e2500a962232cf8a86efde368a5b38ece28 Mon Sep 17 00:00:00 2001 From: amsraman Date: Wed, 10 Jun 2026 16:57:13 -0700 Subject: [PATCH 7/7] Fix broken docs link and outdated AGENTS.md doc copy The MCP overview page lives at /docs/ai/integrations/mcp-overview/, not /docs/ai-builder/... (failed the sitemap link check). Also update the agents_md doc to describe the new prepend behavior for markerless files and the CLAUDE.md bridge. Co-Authored-By: Claude Fable 5 --- docs/ai_builder/integrations/agents_md.md | 2 +- docs/getting_started/installation.md | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/ai_builder/integrations/agents_md.md b/docs/ai_builder/integrations/agents_md.md index 3d74d238b2c..1a9af086783 100644 --- a/docs/ai_builder/integrations/agents_md.md +++ b/docs/ai_builder/integrations/agents_md.md @@ -23,7 +23,7 @@ The [reflex-dev/agent-skills](https://github.com/reflex-dev/agent-skills) reposi reflex init ``` -The Reflex-provided content sits between `reflex managed` begin/end markers. Anything you add outside the markers is preserved when init refreshes the managed section, and a pre-existing `AGENTS.md` without markers is never touched, so it is safe to re-run. +The Reflex-provided content sits between `reflex managed` begin/end markers. Anything you add outside the markers is preserved when init refreshes the managed section, so it is safe to re-run. If a pre-existing `AGENTS.md` has no markers, the managed section is added at the top and your content is kept below it. For Claude Code users, init also creates a `CLAUDE.md` importing `AGENTS.md` (or updates an existing one that doesn't reference it). To add the template to an existing project, download it into the project root, next to `rxconfig.py`: diff --git a/docs/getting_started/installation.md b/docs/getting_started/installation.md index fb64130ac93..92ea7e23500 100644 --- a/docs/getting_started/installation.md +++ b/docs/getting_started/installation.md @@ -10,7 +10,7 @@ You're likely here to build a Reflex app for someone. Reflex does **not** behave **Get the current docs (don't rely on training data — the API changes between versions):** - Fetch [`https://reflex.dev/llms.txt`](https://reflex.dev/llms.txt) for a token-efficient map of the docs and core concepts. -- Use the [Reflex MCP server](/docs/ai-builder/integrations/mcp-overview) (`https://build.reflex.dev/mcp`) for live component/prop lookup so you don't hallucinate APIs. *(MCP access is an enterprise feature.)* +- Use the [Reflex MCP server](/docs/ai/integrations/mcp-overview/) (`https://build.reflex.dev/mcp`) for live component/prop lookup so you don't hallucinate APIs. *(MCP access is an enterprise feature.)* - Run `uv run reflex --version` and trust the live docs for that version over memory. **Drop a rules file in the project root** (`AGENTS.md`, or `CLAUDE.md`/`.cursorrules` for your tool) so these conventions persist across your session — `reflex init` writes a starter `AGENTS.md` for you by default, and a minimal fallback is below.