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:
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.
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.
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.
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.
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.
Problem
load_agents_md_files_from_dirincrates/jcode-base/src/prompt.rs:816loads exactly two files: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 && jcodegets no project instructions at all, becausecrates/jcode-tui/AGENTS.mddoes not exist and the rootAGENTS.mdis never looked at. In a monorepo the same thing happens for every package directory. There is no warning;/infojust reportsproject 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/AGENTS.mdplus everyAGENTS.mdon the path from the git root to the current directory, concatenated in that order, capped atproject_doc_max_bytes(default 32 KiB). Files closer to cwd come later so they refine broader guidance.AGENTS.mdfiles on the path, with the nearest file taking precedence. Fromapps/web/componentsit loads the rootAGENTS.mdthenapps/web/AGENTS.md, and leaves sibling branches alone.CLAUDE.mdup 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.jsonand~/.codex/config.tomlon first run, loads skills from~/.claude/skillsand~/.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
.gitwalk-up, a byte cap, and one format string in/info.Proposed approach
crates/jcode-base/src/prompt.rs(implementation)Add near
load_agents_md_files_from_dir(currently line 816):Rewrite the project half of
load_agents_md_files_from_dirto call these, keeping the existingload_fileclosure 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::canonicalizewhere 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:
Keeping the root label byte-identical to today's string matters:
crates/jcode-base/src/prompt_tests.rsand the/inforeport both assert on these strings.crates/jcode-base/src/prompt.rs(ContextInfo, currently line 205)has_project_agents_mdandproject_agents_md_charskeep their meaning, now describing the merged chain. Add two fields:ContextInfoderivesDefault, so no other constructor changes.crates/jcode-base/src/prompt.rscall sites (lines 397 and 496)Both already do:
Add the two new field copies to both. Line 397 is the dynamic path, line 496 is the cacheable-static path; missing either makes
/infodisagree with itself depending on which prompt builder ran.crates/jcode-tui/src/tui/app/state_ui.rs(wiring,/inforeport at line 2130)The context report line is currently:
Change to report the chain, for example
project AGENTS.md: loaded (3 files, 4812 chars)and append[truncated at 32 KiB]whenproject_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_filesmust keep passing unchanged; it asserts the exact# Global Instructions (~/AGENTS.md)label. Add:test_agents_md_chain_loads_repo_root_from_subdirectory- tempdir with.git/,AGENTS.mdat root and atcrates/x/, call withSome(root/crates/x), assert both appear and the root content precedes the nested content.test_agents_md_chain_without_git_root_matches_legacy_behavior- no.gitanywhere, assert only the working-directory file loads andproject_agents_md_files == 1.test_agents_md_chain_dedupes_when_started_at_repo_root-.git/andAGENTS.mdboth at root, call withSome(root), assert the content appears exactly once.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_truncatedis true, and the total stays at or under the cap.test_repository_root_detects_worktree_git_file-.gitwritten as a file containinggitdir: ..., assert the root is still found.Tests must use
crate::storage::lock_test_env()and setJCODE_HOMEthe way the existing test does, because~/AGENTS.mdlookup is process-global.Estimated scope: roughly 90 lines of implementation across
prompt.rs, 6 lines instate_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.