Include top-level AGENTS.md in project instructions
Add support for including a top-level AGENTS.md in a project's system instructions while keeping backwards-compatible defaults. Introduces include_agents_md project metadata (defaults to true), normalization and save logic, and helper functions to discover and format AGENTS.md and other instruction files into the system prompt. Updates build_system_prompt_vars to assemble instruction parts, adds UI checkbox in project edit form, normalizes defaults in the projects store, and adds tests for the new behavior. Also documents the compatibility rule in AGENTS.md and adds a small prompt rule reminder.
frdel committed
Jun 2, 2026 at 14:50 UTC
08306be6502ace22d654e3ddea75953a324fe325
6 files changed
+175
-9
helpers/AGENTS.md
+1
@@ -15,6 +15,7 @@
15
- Preserve public helper APIs used by core code and plugins unless all callers, docs, and tests are updated.
16
- Use structured parsers and serializers for YAML, JSON, paths, and URLs instead of ad hoc string handling.
17
- Keep path handling constrained to intended roots for user files, uploads, downloads, projects, and workdirs.
18
+- Project metadata defaults must remain backwards-compatible; missing `include_agents_md` is treated as enabled and project instruction file content is injected with an explicit source path.
19
- Do not hardcode secrets, provider keys, local absolute paths, or environment-specific values.
20
- Use `RepairableException` for errors an agent may be able to fix.
21
helpers/projects.py
+85
-8
@@ -1,5 +1,5 @@
1
import os
2
-from typing import Literal, TypedDict, TYPE_CHECKING, cast
2
+from typing import Literal, NotRequired, TypedDict, TYPE_CHECKING, cast
3
4
from helpers import files, dirty_json, persist_chat, file_tree
5
from helpers.print_style import PrintStyle
@@ -13,6 +13,7 @@ PROJECT_META_DIR = ".a0proj"
13
PROJECT_INSTRUCTIONS_DIR = "instructions"
14
PROJECT_KNOWLEDGE_DIR = "knowledge"
15
PROJECT_HEADER_FILE = "project.json"
16
+PROJECT_AGENTS_MD_FILES = ("AGENTS.md", "Agents.md", "agents.md")
17
18
CONTEXT_DATA_KEY_PROJECT = "project"
19
@@ -32,6 +33,7 @@ class BasicProjectData(TypedDict):
33
title: str
34
description: str
35
instructions: str
36
+ include_agents_md: NotRequired[bool]
37
color: str
38
git_url: str
39
file_structure: FileStructureInjectionSettings
@@ -159,6 +161,9 @@ def _normalizeBasicData(data: BasicProjectData) -> BasicProjectData:
161
"title": data.get("title", ""),
162
"description": data.get("description", ""),
163
"instructions": data.get("instructions", ""),
164
+ "include_agents_md": _normalize_include_agents_md(
165
+ data.get("include_agents_md", True)
166
+ ),
167
"color": data.get("color", ""),
168
"git_url": data.get("git_url", ""),
169
"file_structure": data.get(
@@ -174,6 +179,9 @@ def _normalizeEditData(data: EditProjectData) -> EditProjectData:
179
"title": data.get("title", ""),
180
"description": data.get("description", ""),
181
"instructions": data.get("instructions", ""),
182
+ "include_agents_md": _normalize_include_agents_md(
183
+ data.get("include_agents_md", True)
184
+ ),
185
"variables": data.get("variables", ""),
186
"color": data.get("color", ""),
187
"git_url": data.get("git_url", ""),
@@ -270,7 +278,7 @@ def load_edit_project_data(name: str) -> EditProjectData:
278
279
def save_project_header(name: str, data: BasicProjectData):
280
# save project header file
273
- header = dirty_json.stringify(data)
281
+ header = dirty_json.stringify(_project_header_for_save(data))
282
abs_path = files.get_abs_path(
283
PROJECTS_PARENT_DIR, name, PROJECT_META_DIR, PROJECT_HEADER_FILE
284
)
@@ -423,12 +431,16 @@ def deactivate_project_in_chats(name: str):
431
def build_system_prompt_vars(name: str):
432
project_data = load_basic_project_data(name)
433
main_instructions = project_data.get("instructions", "") or ""
426
- additional_instructions = get_additional_instructions_files(name)
427
- complete_instructions = (
428
- main_instructions
429
- + "\n\n".join(
430
- additional_instructions[k] for k in sorted(additional_instructions)
431
- )
434
+ instruction_files = get_project_instruction_files(
435
+ name,
436
+ include_agents_md=project_data.get("include_agents_md", True),
437
+ )
438
+ instruction_parts = [
439
+ main_instructions,
440
+ _format_project_instruction_files(instruction_files),
441
+ ]
442
+ complete_instructions = "\n\n".join(
443
+ part.strip() for part in instruction_parts if part.strip()
444
).strip()
445
return {
446
"project_name": project_data.get("title", ""),
@@ -446,6 +458,71 @@ def get_additional_instructions_files(name: str):
458
return files.read_text_files_in_dir(instructions_folder)
459
460
461
+def get_project_instruction_files(
462
+ name: str,
463
+ include_agents_md: bool = True,
464
+) -> list[tuple[str, str]]:
465
+ project_folder = get_project_folder(name)
466
+ result: list[tuple[str, str]] = []
467
+
468
+ if include_agents_md:
469
+ agents_md = get_project_agents_md_instruction_file(name)
470
+ if agents_md:
471
+ result.append(agents_md)
472
+
473
+ additional_instructions = get_additional_instructions_files(name)
474
+ for filename in sorted(additional_instructions):
475
+ path = files.get_abs_path(
476
+ project_folder,
477
+ PROJECT_META_DIR,
478
+ PROJECT_INSTRUCTIONS_DIR,
479
+ filename,
480
+ )
481
+ result.append(
482
+ (files.normalize_a0_path(path), additional_instructions[filename])
483
+ )
484
+
485
+ return result
486
+
487
+
488
+def get_project_agents_md_instruction_file(name: str) -> tuple[str, str] | None:
489
+ project_folder = get_project_folder(name)
490
+ for filename in PROJECT_AGENTS_MD_FILES:
491
+ matches = files.read_text_files_in_dir(project_folder, pattern=filename)
492
+ if filename in matches:
493
+ path = files.get_abs_path(project_folder, filename)
494
+ return (files.normalize_a0_path(path), matches[filename])
495
+ return None
496
+
497
+
498
+def _format_project_instruction_files(instruction_files: list[tuple[str, str]]) -> str:
499
+ if not instruction_files:
500
+ return ""
501
+
502
+ parts = ["## project instruction files"]
503
+ for path, content in instruction_files:
504
+ parts.append(f"### path: {path}\n\n{content}")
505
+ return "\n\n".join(parts)
506
+
507
+
508
+def _normalize_include_agents_md(value: object) -> bool:
509
+ if value is None:
510
+ return True
511
+ if isinstance(value, bool):
512
+ return value
513
+ if isinstance(value, str):
514
+ return value.strip().lower() not in {"0", "false", "no", "off"}
515
+ return bool(value)
516
+
517
+
518
+def _project_header_for_save(data: BasicProjectData) -> dict:
519
+ header = dict(data)
520
+ header["include_agents_md"] = _normalize_include_agents_md(
521
+ header.get("include_agents_md", True)
522
+ )
523
+ return header
524
+
525
+
526
def get_context_project_name(context: "AgentContext") -> str | None:
527
return context.get_data(CONTEXT_DATA_KEY_PROJECT)
528
prompts/agent.system.projects.active.md
+1
@@ -6,5 +6,6 @@ path: {{project_path}}
6
rules:
7
- work inside {{project_path}}
8
- do not rename project dir or change `.a0proj` unless asked
9
+- must always follow project instructions below
10
11
{{project_instructions}}
tests/test_projects.py
new
+69
@@ -0,0 +1,69 @@
1
+from pathlib import Path
2
+
3
+from helpers import dirty_json, files, projects
4
+
5
+
6
+def _prepare_project_tree(monkeypatch, tmp_path: Path) -> None:
7
+ monkeypatch.setattr(files, "_base_dir", str(tmp_path))
8
+ (tmp_path / "usr" / "projects").mkdir(parents=True, exist_ok=True)
9
+
10
+
11
+def test_project_include_agents_md_defaults_true_and_saves(monkeypatch, tmp_path):
12
+ _prepare_project_tree(monkeypatch, tmp_path)
13
+ meta = tmp_path / "usr" / "projects" / "demo" / ".a0proj"
14
+ meta.mkdir(parents=True)
15
+ (meta / "project.json").write_text('{"title": "Demo"}', encoding="utf-8")
16
+
17
+ data = projects.load_basic_project_data("demo")
18
+
19
+ assert data["include_agents_md"] is True
20
+
21
+ projects.save_project_header("demo", data)
22
+ saved = dirty_json.parse((meta / "project.json").read_text(encoding="utf-8"))
23
+
24
+ assert saved["include_agents_md"] is True
25
+
26
+
27
+def test_project_system_prompt_includes_root_agents_md_with_path(monkeypatch, tmp_path):
28
+ _prepare_project_tree(monkeypatch, tmp_path)
29
+ projects.create_project(
30
+ "demo",
31
+ {
32
+ "title": "Demo",
33
+ "instructions": "Main project rule.",
34
+ },
35
+ )
36
+ project_root = tmp_path / "usr" / "projects" / "demo"
37
+ (project_root / "AGENTS.md").write_text("Root AGENTS rule.", encoding="utf-8")
38
+ (
39
+ project_root / ".a0proj" / "instructions" / "extra.md"
40
+ ).write_text("Folder instruction rule.", encoding="utf-8")
41
+
42
+ prompt_vars = projects.build_system_prompt_vars("demo")
43
+ instructions = prompt_vars["project_instructions"]
44
+
45
+ assert "Main project rule." in instructions
46
+ assert instructions.count("## project instruction files") == 1
47
+ assert "## project instruction file\n" not in instructions
48
+ assert "### path: /a0/usr/projects/demo/AGENTS.md" in instructions
49
+ assert "Root AGENTS rule." in instructions
50
+ assert "### path: /a0/usr/projects/demo/.a0proj/instructions/extra.md" in instructions
51
+ assert "Folder instruction rule." in instructions
52
+
53
+
54
+def test_project_system_prompt_respects_disabled_agents_md(monkeypatch, tmp_path):
55
+ _prepare_project_tree(monkeypatch, tmp_path)
56
+ projects.create_project(
57
+ "demo",
58
+ {
59
+ "title": "Demo",
60
+ "include_agents_md": False,
61
+ },
62
+ )
63
+ project_root = tmp_path / "usr" / "projects" / "demo"
64
+ (project_root / "AGENTS.md").write_text("Root AGENTS rule.", encoding="utf-8")
65
+
66
+ prompt_vars = projects.build_system_prompt_vars("demo")
67
+
68
+ assert "Root AGENTS rule." not in prompt_vars["project_instructions"]
69
+ assert "AGENTS.md" not in prompt_vars["project_instructions"]
webui/components/projects/project-edit-instructions.html
+15
-1
@@ -33,6 +33,20 @@
33
placeholder="Enter project instructions"></textarea>
34
</div>
35
36
+ <div class="projects-setting-row" style="padding: 1rem 0;">
37
+ <div class="projects-setting-text">
38
+ <label class="projects-form-label">Include top-level AGENTS.md</label>
39
+ <span class="projects-form-description">When turned on, Agent Zero will include the project's
40
+ top-level AGENTS.md file in the active project instructions when that file exists.</span>
41
+ </div>
42
+ <div class="projects-setting-control">
43
+ <label class="toggle">
44
+ <input type="checkbox" x-model="$store.projects.selectedProject.include_agents_md">
45
+ <span class="toggler"></span>
46
+ </label>
47
+ </div>
48
+ </div>
49
+
50
<div class="projects-form-group">
51
<label class="projects-form-label">Instruction files</label>
52
<div class="projects-input-with-button-wrapper">
@@ -55,4 +69,4 @@
69
<style>
70
</style>
71
58
-</html>
\ No newline at end of file
72
+</html>
webui/components/projects/projects-store.js
+4
@@ -162,6 +162,7 @@ const model = {
162
title: project.title,
163
color: project.color,
164
git_url: project.git_url,
165
+ include_agents_md: project.include_agents_md !== false,
166
git_token: project.git_token || "",
167
llm: project.llm || null,
168
},
@@ -407,6 +408,8 @@ const model = {
408
name: ``,
409
title: `Project #${this.projectList.length + 1}`,
410
description: "",
411
+ instructions: "",
412
+ include_agents_md: true,
413
color: "",
414
git_url: "",
415
git_token: "",
@@ -425,6 +428,7 @@ const model = {
428
creating: false,
429
},
430
...projectData,
431
+ include_agents_md: projectData.include_agents_md !== false,
432
llm: this._normalizeProjectLlmData(projectData.llm, name),
433
};
434
},