Skip to content

feat(agent): cascading context file discovery with walk-up - #10482

Open
iRonin wants to merge 1 commit into
NousResearch:mainfrom
iRonin:ironin/cascading-context
Open

feat(agent): cascading context file discovery with walk-up#10482
iRonin wants to merge 1 commit into
NousResearch:mainfrom
iRonin:ironin/cascading-context

Conversation

@iRonin

@iRonin iRonin commented Apr 15, 2026

Copy link
Copy Markdown
Contributor

Problem

Context file discovery (AGENTS.md, CLAUDE.md) is currently CWD-only at startup. Only .hermes.md walks up parent directories (to git root). Users with cascading context files at multiple levels (home, workspace, project) miss instructions from parent directories.

Two separate problems:

1. No walk-up for AGENTS.md and CLAUDE.md

_load_agents_md() and _load_claude_md() check only cwd_path / name. Users lose workspace-level and home-level instructions.

2. Hard-coded first-match-wins

The or chain in build_context_files_prompt() means only ONE context file type is loaded.

Solution

Config-driven cascading discovery

context:
  compose: true           # compose all found files (default: true)
  walk_limit: home        # git_root | home | unlimited | absolute path (default: home)
  show_loaded: true       # print loaded files at startup (default: true)

New functions in agent/prompt_builder.py

  • _resolve_walk_limit() — resolves limit config to a concrete directory
  • _walk_parents() — yields CWD + parents up to the configured limit
  • _load_all_of_type() — collects ALL matching files with inode dedup (macOS APFS)
  • discover_context_files() — returns file list for /context command and verbose display

/context command

/context list                     → show all discovered files
/context add /path/to/file.md     → add custom context file
/context remove 2                 → remove by index
/context walk home                → change walk limit (git_root/home/unlimited/path)
/context compose off              → revert to first-match-wins

Key changes

  • _load_agents_md and _load_claude_md now walk parents and compose all found files
  • build_context_files_prompt() accepts compose and walk_limit params
  • run_agent.py reads config and passes settings through
  • Startup verbose display shows discovered files with paths, types, and char counts
  • Inode-based dedup for case-insensitive filesystems (macOS APFS)

Testing

  • 131 tests pass, 1 skipped (case-insensitive filesystem)
  • Manual test on real Dropbox legal folder: discovers 4 CLAUDE.md files across 3 directory levels
  • All 6 changed files pass syntax check

Files changed

File Lines
agent/prompt_builder.py +338/-2
tests/agent/test_prompt_builder.py +204/-1
hermes_cli/config.py +28/-1
run_agent.py +16/-1
cli.py +38
hermes_cli/commands.py +2

Total: 6 files, +601/-25

Relevant Issues

Add three new config options for context file discovery:
- context.compose: compose all found files instead of first-match-wins (default: true)
- context.walk_limit: how far to walk up parents: git_root/home/unlimited/absolute path (default: home)
- context.show_loaded: print discovered context files at startup (default: true)
Add migration v17→v18 to seed defaults for existing users.
@mxnstrexgl

Copy link
Copy Markdown

🤖 Automated PR Review

Security Scan

✓ No hardcoded secrets
⚠️ Path traversal check: _walk_parents() uses .resolve() - GOOD
⚠️ walk_limit='unlimited' could walk to filesystem root - intentional but flag
✓ Git root detection safe
✓ No unsafe deserialization

Code Quality

✓ Well-structured new feature
✓ Comprehensive walk limit options (git_root, home, unlimited, custom path)
✓ Good test coverage
⚠️ _walk_parents() logic is complex - consider breaking into smaller functions
⚠️ Missing early termination if cwd is not under stop_path (logic attempts this but could be clearer)
ℹ️ 601 lines added - significant feature, needs thorough testing

Summary

Status: COMMENT

Good feature for cascading context discovery. Security looks OK (uses resolve()). Consider simplifying the walk parent logic for maintainability.


Reviewed by Hermes Agent

@iRonin

iRonin commented Apr 15, 2026

Copy link
Copy Markdown
Contributor Author

Response to automated review

Path traversal: _walk_parents() uses .resolve() on all paths, preventing symlink-based traversal attacks. Confirmed safe.

walk_limit="unlimited": Intentional — matches the original .hermes.md walk-to-git-root pattern. The default is home which bounds it to the user's home directory.

_walk_parents() complexity: The function has been updated in #10503 (which supersedes this) with a simpler implementation that:

  • Takes the approach: start at cwd, iterate through .parents, stop when stop_path is reached
  • Early termination when cwd is not under stop_path
  • Inode-based dedup for macOS APFS case-insensitive filesystems

Lines changed: The 601 lines include 338 lines for prompt_builder.py (4 new functions + 2 rewritten loaders + 1 rewritten builder + 1 new discovery function). Each function is focused and testable.

Note: #10503 builds on this PR and includes further simplifications based on testing feedback.

@alt-glitch

Copy link
Copy Markdown
Collaborator

Implements #10299 — cascading context file discovery with walk-up and /context command.

@alt-glitch alt-glitch added type/feature New feature or request P3 Low — cosmetic, nice to have comp/agent Core agent runtime: loop, agent_init, prompt builder, context-compression, responses endpoint area/config Config system, migrations, profiles labels Apr 26, 2026
@alt-glitch

Copy link
Copy Markdown
Collaborator

Implements #10299 — cascading context file discovery with walk-up and /context command.

@teknium1 teknium1 left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for the detailed proposal. The startup limitation is real, but current main has deliberately moved toward a different context-loading model.

Problems

  • agent/prompt_builder.py:1004 uses string-prefix containment. With stop path /home/alice, a CWD under /home/alice2 is treated as in-bounds and the loop can append /home and /. Use path ancestry (relative_to/is_relative_to) and stop at the exact resolved boundary. This is especially important because main commit 306b6615cf00a77e39fb27b69653a2a5d24ce5f8 hardened parent discovery against context files outside a safe project boundary.
  • cli.py:6205 never parses cmd; all advertised /context add, remove, walk, and compose invocations produce the same listing. context.show_loaded is read at cli.py:6221 but unused, and run_agent.py:3274-3289 does not consume it, so the promised startup display is absent.
  • Current main intentionally keeps startup context CWD-only (agent/prompt_builder.py:1876-1984) and progressively loads workspace-local hints with an explicit outside-workspace rejection (agent/subdirectory_hints.py:169-196). The current docs explain this avoids prompt bloat and preserves a stable system prompt (website/docs/user-guide/features/context-files.md:32-49).

Suggested changes

  • Rework the boundary check and add regression tests for sibling-prefix paths and CWDs outside the configured root.
  • Resolve the eager-home-composition design against the existing progressive, workspace-bounded mechanism before salvaging; then implement or narrow the /context and show_loaded contract consistently.

Automated hermes-sweeper review.

Comment thread agent/prompt_builder.py
dirs.append(parent)
break
# Check if we've gone past the stop path
if str(parent_resolved).startswith(str(stop_path)):

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

String prefixes do not establish path ancestry: with stop /home/alice and CWD /home/alice2/project, this branch treats the CWD as in-bounds and later appends /home and /. Use Path.is_relative_to()/relative_to() on resolved paths, reject a CWD outside the bound, and stop exactly at the bound.

Comment thread hermes_cli/config.py
# "git_root" | "home" | "unlimited" | absolute path
"walk_limit": "home",
# Print discovered context files at startup.
"show_loaded": True,

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

show_loaded is persisted but not used: run_agent.py forwards only compose and walk_limit, and the /context handler reads this value without branching on it. The advertised startup display will never occur until this is wired into startup presentation.

Comment thread cli.py
}
_cprint(labels.get(self.tool_progress_mode, ""))

def _handle_context_command(self, cmd: str):

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This handler never parses cmd, so the body-promised /context add, remove, walk, and compose subcommands all execute the same listing. Either implement the durable config mutations/subcommand dispatch or narrow the command contract.

@teknium1 teknium1 added sweeper:risk-security-boundary Sweeper risk: may affect sandboxing, auth, credentials, or sensitive data sweeper:risk-compatibility Sweeper risk: may break existing users, config, migrations, defaults, or upgrades sweeper:blast-broad Sweeper blast radius: broad — a core path most sessions hit labels Jul 12, 2026
@alt-glitch alt-glitch added comp/cli CLI entry point, hermes_cli/, setup wizard and removed sweeper:risk-security-boundary Sweeper risk: may affect sandboxing, auth, credentials, or sensitive data labels Jul 12, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

area/config Config system, migrations, profiles comp/agent Core agent runtime: loop, agent_init, prompt builder, context-compression, responses endpoint comp/cli CLI entry point, hermes_cli/, setup wizard P3 Low — cosmetic, nice to have sweeper:blast-broad Sweeper blast radius: broad — a core path most sessions hit sweeper:risk-compatibility Sweeper risk: may break existing users, config, migrations, defaults, or upgrades type/feature New feature or request

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants