Skip to content

Load AGENTS.md from the repository root down to the session directory #697

Description

@mvanhorn

Problem

load_agents_md_files_from_dir in crates/jcode-base/src/prompt.rs:816 loads exactly two files:

let project_dir = working_dir.unwrap_or(Path::new("."));
if let Some((content, size)) = load_file(
    &project_dir.join("AGENTS.md"),
    "Project Instructions (AGENTS.md)",
) { ... }

if let Ok(global_agents_md) = crate::storage::user_home_path("AGENTS.md") { ... }

The session working directory and the home directory. Nothing in between.

The consequence: start jcode anywhere other than the repository root and the repository's own AGENTS.md is silently not loaded. cd crates/jcode-tui && jcode gets no project instructions at all, because crates/jcode-tui/AGENTS.md does not exist and the root AGENTS.md is never looked at. In a monorepo the same thing happens for every package directory. There is no warning; /info just reports project AGENTS.md: not loaded, which is easy to read as "this repo has no AGENTS.md" rather than "you started from the wrong directory".

Every comparable harness resolves this by walking up:

  • Codex builds the instruction chain from the global ~/.codex/AGENTS.md plus every AGENTS.md on the path from the git root to the current directory, concatenated in that order, capped at project_doc_max_bytes (default 32 KiB). Files closer to cwd come later so they refine broader guidance.
  • opencode injects all parent AGENTS.md files on the path, with the nearest file taking precedence. From apps/web/components it loads the root AGENTS.md then apps/web/AGENTS.md, and leaves sibling branches alone.
  • Claude Code walks CLAUDE.md up the directory tree from cwd.

jcode is the outlier, and it is the one that already advertises AGENTS.md compatibility: it imports MCP config from ~/.claude.json and ~/.codex/config.toml on first run, loads skills from ~/.claude/skills and ~/.codex/skills, and resumes sessions from Claude Code, Codex, opencode, and pi. Someone arriving from Codex reasonably expects the same AGENTS.md resolution and does not get it.

The fix is contained: one function, a .git walk-up, a byte cap, and one format string in /info.

This feature was proposed from code analysis without any hands-on usage.

Proposed approach

crates/jcode-base/src/prompt.rs (implementation)

Add near load_agents_md_files_from_dir (currently line 816):

/// Byte ceiling for the merged project AGENTS.md chain. Matches Codex's
/// `project_doc_max_bytes` default so a repo tuned for one harness behaves
/// the same in the other. Files are added root-first and the walk stops once
/// the next file would exceed this.
const PROJECT_AGENTS_MD_MAX_BYTES: usize = 32 * 1024;

/// Nearest ancestor of `start` (inclusive) containing a `.git` entry.
/// `.git` may be a directory or, in a linked worktree, a file.
fn repository_root(start: &Path) -> Option<PathBuf> { ... }

/// Every AGENTS.md from `root` down to `leaf` inclusive, root first.
fn agents_md_chain(root: &Path, leaf: &Path) -> Vec<PathBuf> { ... }

Rewrite the project half of load_agents_md_files_from_dir to call these, keeping the existing load_file closure and the existing global-home branch untouched. Keep the function signature (Option<&Path>) -> (Option<String>, ContextInfo) so both call sites compile unchanged.

Canonicalize with std::fs::canonicalize where available and fall back to the raw path on error, so a working directory behind a symlink still terminates the walk. Bound the walk at 64 levels so a pathological path cannot spin.

Label format, so a reader of the prompt can attribute a rule:

# Project Instructions (AGENTS.md)              <- repository root, unchanged label
# Project Instructions (crates/jcode-tui/AGENTS.md)
# Global Instructions (~/AGENTS.md)             <- unchanged

Keeping the root label byte-identical to today's string matters: crates/jcode-base/src/prompt_tests.rs and the /info report both assert on these strings.

crates/jcode-base/src/prompt.rs (ContextInfo, currently line 205)

has_project_agents_md and project_agents_md_chars keep their meaning, now describing the merged chain. Add two fields:

/// Number of AGENTS.md files merged from the repository root down to the
/// session working directory.
pub project_agents_md_files: usize,
/// True when the 32 KiB project-chain cap stopped the walk early.
pub project_agents_md_truncated: bool,

ContextInfo derives Default, so no other constructor changes.

crates/jcode-base/src/prompt.rs call sites (lines 397 and 496)

Both already do:

info.has_project_agents_md = md_info.has_project_agents_md;
info.project_agents_md_chars = md_info.project_agents_md_chars;

Add the two new field copies to both. Line 397 is the dynamic path, line 496 is the cacheable-static path; missing either makes /info disagree with itself depending on which prompt builder ran.

crates/jcode-tui/src/tui/app/state_ui.rs (wiring, /info report at line 2130)

The context report line is currently:

- project AGENTS.md: {} ({})

Change to report the chain, for example project AGENTS.md: loaded (3 files, 4812 chars) and append [truncated at 32 KiB] when project_agents_md_truncated. This is the only user-visible surface that names AGENTS.md, and leaving it saying "loaded" for a 3-file chain is the kind of silent ambiguity the current bug already causes.

crates/jcode-base/src/prompt_tests.rs (tests)

The existing test_load_agents_md_files_uses_sandboxed_global_files must keep passing unchanged; it asserts the exact # Global Instructions (~/AGENTS.md) label. Add:

  1. test_agents_md_chain_loads_repo_root_from_subdirectory - tempdir with .git/, AGENTS.md at root and at crates/x/, call with Some(root/crates/x), assert both appear and the root content precedes the nested content.
  2. test_agents_md_chain_without_git_root_matches_legacy_behavior - no .git anywhere, assert only the working-directory file loads and project_agents_md_files == 1.
  3. test_agents_md_chain_dedupes_when_started_at_repo_root - .git/ and AGENTS.md both at root, call with Some(root), assert the content appears exactly once.
  4. test_agents_md_chain_respects_byte_cap - root file over 32 KiB plus a nested file, assert the nested file is dropped, project_agents_md_truncated is true, and the total stays at or under the cap.
  5. test_repository_root_detects_worktree_git_file - .git written as a file containing gitdir: ..., assert the root is still found.

Tests must use crate::storage::lock_test_env() and set JCODE_HOME the way the existing test does, because ~/AGENTS.md lookup is process-global.

Estimated scope: roughly 90 lines of implementation across prompt.rs, 6 lines in state_ui.rs, and about 110 lines of tests.

Willingness to implement

I'm happy to open a PR implementing this if the direction works for you.

Metadata

Metadata

Assignees

No one assigned

    Labels

    autonomous: likelyProbably hands-off: clearly worth fixing, agent can do it, minor judgment needed.enhancementNew feature or requestpriority: mediumP2 - normal prioritytriage: needs-decisionNeeds maintainer decision/design thought

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions