fix(context): derive the /context file listing from the builder's own discovery walk

Review follow-up (Enough1122) on the salvaged #91272: the original
list_context_file_sources() hand-mirrored the priority ladder inside
build_context_files_prompt, so the two would drift the moment the builder
gained a context type or changed precedence — misreporting what the prompt
holds is worse than not showing it.

Now prompt_builder exposes one candidate finder per context type
(_CONTEXT_FILE_CANDIDATES → discover_context_files) and BOTH the loaders and
the manifest walk it. The manifest lives in the new sibling
agent/context_file_sources.py (not appended to the facade) and:
- reports empty / unreadable files truthfully instead of "✓ 0 tokens",
- mirrors the install-tree guard ("suppressed") so a Desktop session that
  fell back into the Hermes tree sees why nothing loaded,
- lists every .cursor/rules/*.mdc as loaded, matching the builder which
  concatenates all of them,
- measures truncation on the rendered "## label" section like the builder.

The block now renders on every surface that shows the /context category
table: CLI/TUI (hermes_cli/cli_info_mixin.py) and the messaging gateway
(gateway/slash_commands_status.py). The Desktop popover consumes the raw
session.context_breakdown payload (no text table) and is left as-is.

Tests trimmed to the two invariants: manifest/prompt parity across every
context type at once, and truncated/suppressed follow the builder.
This commit is contained in:
teknium1
2026-09-14 19:15:40 -07:00
committed by Teknium
parent 5349aa609d
commit f271ba09b0
6 changed files with 278 additions and 131 deletions
+115
View File
@@ -0,0 +1,115 @@
"""Per-file manifest of the context/instruction files behind the ``/context`` "Rules" figure.
Read-only: enumerates the same candidates ``build_context_files_prompt`` loads (through
``agent.prompt_builder.discover_context_files`` — one discovery walk, so the listing cannot drift from the
prompt) and reports, per file, its size and whether it was loaded, truncated over the context-file cap,
shadowed by a higher-priority context type, empty/unreadable, or suppressed by the install-tree guard.
Nothing here builds a prompt or touches the truncation-warning ContextVar, so it is free of cache impact.
Inspired by Copilot CLI 1.0.81's per-file ``/instructions`` view.
"""
from __future__ import annotations
import os
from pathlib import Path
from typing import Any, Dict, List, Optional
from agent import prompt_builder as _pb
from agent.model_metadata import estimate_tokens_rough
# status -> (glyph, note shown after the token count; "" = none)
_STATUS_DISPLAY = {
"loaded": ("✓", ""),
"truncated": ("◐", "truncated — over context_file_max_chars"),
"shadowed": ("○", "not loaded — higher-priority context type wins"),
"empty": ("○", "not loaded — empty file"),
"unreadable": ("✗", "not loaded — could not be read"),
"suppressed": ("○", "not loaded — cwd fell back to the Hermes install tree"),
}
def _entry(label: str, path: Path, content: str, status: str) -> Dict[str, Any]:
return {
"label": label, "path": str(path), "chars": len(content), "est_tokens": estimate_tokens_rough(content),
"loaded": status in ("loaded", "truncated"), "status": status,
}
def _empty_status(path: Path) -> str:
"""A file the builder read as "" is either genuinely empty or unreadable (permissions, timeout)."""
try:
return "unreadable" if path.stat().st_size > 0 else "empty"
except OSError:
return "unreadable"
def _loaded_status(rendered_len: int, max_chars: int) -> str:
return "truncated" if rendered_len > max_chars else "loaded"
def list_context_file_sources(
cwd: Optional[str] = None, context_length: Optional[int] = None, allow_install_tree_fallback: bool = False,
home_override: "Path | None" = None, skip_soul: bool = False,
) -> List[Dict[str, Any]]:
"""One dict per context file Hermes considered, in the builder's priority order.
Same signature semantics as ``build_context_files_prompt`` (``cwd=None`` → launch dir, install-tree guard
unless *allow_install_tree_fallback*). Keys: ``label``, ``path``, ``chars``, ``est_tokens``, ``loaded``
and ``status`` ∈ loaded / truncated / shadowed / empty / unreadable / suppressed.
"""
cwd_path = Path(cwd if cwd is not None else os.getcwd()).resolve()
max_chars = _pb._get_context_file_max_chars(context_length)
suppressed = _pb._project_context_suppressed(cwd, cwd_path, allow_install_tree_fallback)
sources: List[Dict[str, Any]] = []
winner: Optional[str] = None
for kind, label, path, content in _pb.discover_context_files(cwd_path):
if not content:
status = _empty_status(path)
elif suppressed:
status = "suppressed"
elif winner in (None, kind):
winner = kind
# The builder caps the rendered ``## label`` section, not the raw file.
status = _loaded_status(len(f"## {label}\n\n{content}"), max_chars)
else:
status = "shadowed"
sources.append(_entry(label, path, content, status))
if not skip_soul:
home = Path(home_override) if home_override is not None else _pb.get_hermes_home()
soul_path = home / "SOUL.md"
if _pb._exists_or_denied(soul_path):
content = _pb._read_context_file(soul_path)
status = _loaded_status(len(content), max_chars) if content else _empty_status(soul_path)
sources.append(_entry("SOUL.md", soul_path, content, status))
return sources
def context_file_sources_for_agent(agent: Any) -> List[Dict[str, Any]]:
"""The manifest for a live agent, resolved exactly like ``agent.system_prompt._context_files_part``
(session cwd, install-tree policy per platform, the agent's own profile home)."""
if getattr(agent, "skip_context_files", False):
return []
from agent.runtime_cwd import resolve_context_cwd
from agent.system_prompt import _agent_home
launch_artifact = getattr(agent, "_context_cwd_is_launch_artifact", False)
cwd = None if launch_artifact else resolve_context_cwd()
ctx_len = getattr(getattr(agent, "context_compressor", None), "context_length", None)
return list_context_file_sources(
cwd=str(cwd) if cwd is not None else None, context_length=ctx_len if isinstance(ctx_len, int) else None,
allow_install_tree_fallback=getattr(agent, "platform", None) in ("cli", "tui"), home_override=_agent_home(agent),
)
def render_context_file_lines(sources: List[Dict[str, Any]]) -> List[str]:
"""Plain-text ``Context files`` block for ``/context``; [] when nothing was found."""
if not sources:
return []
width = max(len(str(src.get("label") or "")) for src in sources)
lines = ["Context files"]
for src in sources:
glyph, note = _STATUS_DISPLAY.get(str(src.get("status") or ""), ("•", ""))
suffix = f" ({note})" if note else ""
lines.append(f"{glyph} {str(src.get('label') or ''):<{width}} ~{int(src.get('est_tokens') or 0):>9,} tokens{suffix}")
return lines
+94 -41
View File
@@ -1501,14 +1501,13 @@ def _context_section(content: str, label: str, warn_name: str, path: Path, conte
return _truncate_content(body, warn_name, context_length=context_length, read_path=str(path))
def _load_hermes_md(cwd_path: Path, context_length: Optional[int] = None) -> str:
def _hermes_md_candidates(cwd_path: Path) -> list[tuple[str, Path, str]]:
""".hermes.md / HERMES.md — nearest match walking up to the git root."""
hermes_md_path = _find_hermes_md(cwd_path)
content = _read_context_file(hermes_md_path) if hermes_md_path else ""
if not content:
return ""
label = str(hermes_md_path.relative_to(cwd_path)) if hermes_md_path.is_relative_to(cwd_path) else hermes_md_path.name
return _context_section(_strip_yaml_frontmatter(content), label, ".hermes.md", hermes_md_path, context_length)
path = _find_hermes_md(cwd_path)
if path is None:
return []
label = str(path.relative_to(cwd_path)) if path.is_relative_to(cwd_path) else path.name
return [(label, path, _read_context_file(path))]
def _agents_md_directory_chain(cwd_path: Path) -> list[Path]:
@@ -1521,59 +1520,120 @@ def _agents_md_directory_chain(cwd_path: Path) -> list[Path]:
return [root] + [root.joinpath(*parts[: i + 1]) for i in range(len(parts))]
def _agents_md_candidates(cwd_path: Path) -> list[tuple[str, Path, str]]:
"""AGENTS.md chain from git root down to cwd; per directory the first NON-EMPTY of ``AGENTS.override.md`` /
``AGENTS.md`` / ``agents.md`` wins (empty or unreadable files are listed but fall through)."""
cwd_resolved = cwd_path.resolve()
found: list[tuple[str, Path, str]] = []
for directory in _agents_md_directory_chain(cwd_resolved):
for name in ("AGENTS.override.md", "AGENTS.md", "agents.md"):
candidate = directory / name
if not _exists_or_denied(candidate):
continue
content = _read_context_file(candidate)
label = name if directory == cwd_resolved else os.path.relpath(candidate, cwd_resolved)
found.append((label, candidate, content))
if content:
break # first name match wins per directory
return found
def _claude_md_candidates(cwd_path: Path) -> list[tuple[str, Path, str]]:
"""CLAUDE.md / claude.md — cwd only, first non-empty wins."""
found: list[tuple[str, Path, str]] = []
for name in ("CLAUDE.md", "claude.md"):
candidate = cwd_path / name
if not _exists_or_denied(candidate):
continue
content = _read_context_file(candidate)
found.append((name, candidate, content))
if content:
break
return found
def _cursorrules_candidates(cwd_path: Path) -> list[tuple[str, Path, str]]:
""".cursorrules + .cursor/rules/*.mdc — cwd only; every non-empty file is concatenated."""
candidates: list[tuple[str, Path]] = [(".cursorrules", cwd_path / ".cursorrules")]
cursor_rules_dir = cwd_path / ".cursor" / "rules"
if cursor_rules_dir.is_dir():
candidates += [(f".cursor/rules/{f.name}", f) for f in sorted(cursor_rules_dir.glob("*.mdc"))]
return [(label, path, _read_context_file(path)) for label, path in candidates if _exists_or_denied(path)]
# Project-context types in priority order: the first type with any non-empty file wins, later types are
# shadowed. Both the prompt build (loaders below) and the /context manifest
# (``agent/context_file_sources.py``) enumerate files through these finders, so the two cannot drift.
_CONTEXT_FILE_CANDIDATES = {
"hermes_md": _hermes_md_candidates,
"agents_md": _agents_md_candidates,
"claude_md": _claude_md_candidates,
"cursorrules": _cursorrules_candidates,
}
def discover_context_files(cwd_path: Path) -> list[tuple[str, str, Path, str]]:
"""Every project-context file on disk as ``(kind, label, path, content)`` in priority order.
``content == ""`` means empty or unreadable — such a file is never loaded."""
return [(kind, label, path, content)
for kind, finder in _CONTEXT_FILE_CANDIDATES.items() for label, path, content in finder(cwd_path)]
def _project_context_suppressed(cwd: Optional[str], cwd_path: Path, allow_install_tree_fallback: bool) -> bool:
"""A FALLBACK-picked cwd inside the Hermes install tree must not gain system-prompt authority (the desktop
default would load this repo's contributor AGENTS.md). An explicitly configured cwd is honored verbatim —
the Hermes tree is a legitimate workspace when the user deliberately points a session at it — and
CLI-style surfaces pass allow_install_tree_fallback=True because their launch dir IS the user's shell cwd
(developing Hermes in-tree). See #64590."""
from agent.runtime_cwd import _is_install_tree
return cwd is None and not allow_install_tree_fallback and _is_install_tree(cwd_path)
def _load_hermes_md(cwd_path: Path, context_length: Optional[int] = None) -> str:
""".hermes.md / HERMES.md — nearest match walking up to the git root."""
for label, path, content in _hermes_md_candidates(cwd_path):
if content:
return _context_section(_strip_yaml_frontmatter(content), label, ".hermes.md", path, context_length)
return ""
def _load_agents_md(cwd_path: Path, context_length: Optional[int] = None) -> str:
"""AGENTS.md — merged directory chain from git root down to cwd.
Per directory the first of ``AGENTS.override.md`` / ``AGENTS.md`` / ``agents.md`` wins (a gitignored
personal override shadows the committed file); identical content seen again down the chain is skipped.
Each directory on the chain (see ``_agents_md_directory_chain``) contributes its ``AGENTS.override.md``
/ ``AGENTS.md`` / ``agents.md`` (first name wins per directory) as its own provenance-labelled section.
Each directory on the chain (see ``_agents_md_candidates``) contributes its ``AGENTS.override.md`` /
``AGENTS.md`` / ``agents.md`` (first name wins per directory) as its own provenance-labelled section.
``AGENTS.override.md`` wins over ``AGENTS.md`` so a developer can keep a personal, typically-gitignored
override next to the committed project instructions without editing the tracked file (same convention as
earendil-works/pi#7681). Identical content encountered again further down the chain (copied or symlinked
files) is deduplicated. With a single match — the common case, and always the case outside a git repo —
output is identical to the historical single-file behavior.
"""
cwd_resolved = cwd_path.resolve()
sections: list[str] = []
seen_content: set = set()
for directory in _agents_md_directory_chain(cwd_resolved):
for name in ("AGENTS.override.md", "AGENTS.md", "agents.md"):
candidate = directory / name
content = _read_context_file(candidate)
if not content:
continue
if content not in seen_content: # else: identical copy along the chain
seen_content.add(content)
label = name if directory == cwd_resolved else os.path.relpath(candidate, cwd_resolved)
sections.append(_context_section(content, label, label, candidate, context_length))
break # first name match wins per directory
for label, candidate, content in _agents_md_candidates(cwd_path):
if content and content not in seen_content: # else: empty, or an identical copy along the chain
seen_content.add(content)
sections.append(_context_section(content, label, label, candidate, context_length))
if len(sections) <= 1:
return sections[0] if sections else ""
# Per-file budgets applied above; also cap the merged chain so a deep monorepo can't multiply the budget.
return _truncate_content("\n\n".join(sections), "AGENTS.md (directory chain)", context_length=context_length,
read_path=str(cwd_resolved / "AGENTS.md"))
read_path=str(cwd_path.resolve() / "AGENTS.md"))
def _load_claude_md(cwd_path: Path, context_length: Optional[int] = None) -> str:
"""CLAUDE.md / claude.md — cwd only."""
for name in ("CLAUDE.md", "claude.md"):
content = _read_context_file(cwd_path / name)
for name, path, content in _claude_md_candidates(cwd_path):
if content:
return _context_section(content, name, "CLAUDE.md", cwd_path / name, context_length)
return _context_section(content, name, "CLAUDE.md", path, context_length)
return ""
def _load_cursorrules(cwd_path: Path, context_length: Optional[int] = None) -> str:
""".cursorrules + .cursor/rules/*.mdc — cwd only, concatenated."""
candidates: list[tuple[Path, str]] = [(cwd_path / ".cursorrules", ".cursorrules")]
cursor_rules_dir = cwd_path / ".cursor" / "rules"
if cursor_rules_dir.is_dir():
candidates += [(f, f".cursor/rules/{f.name}") for f in sorted(cursor_rules_dir.glob("*.mdc"))]
cursorrules_content = "".join(
f"## {label}\n\n{_scan_context_content(content, label)}\n\n"
for path, label in candidates if (content := _read_context_file(path))
for label, _path, content in _cursorrules_candidates(cwd_path) if content
)
if not cursorrules_content:
return ""
@@ -1592,14 +1652,7 @@ def build_context_files_prompt(
from HERMES_HOME is independent and always included unless *skip_soul* (already the identity slot).
"""
cwd_path = Path(cwd if cwd is not None else os.getcwd()).resolve()
# A FALLBACK-picked cwd inside the Hermes install tree must not gain system-prompt authority (the desktop
# default would load this repo's contributor AGENTS.md). An explicit cwd is honored verbatim.
# An explicitly configured cwd is honored verbatim — the Hermes tree is a legitimate workspace when the
# user deliberately points a session at it — and CLI-style surfaces pass
# allow_install_tree_fallback=True because their launch dir IS the user's shell cwd (developing Hermes
# in-tree). See #64590.
from agent.runtime_cwd import _is_install_tree
if cwd is None and not allow_install_tree_fallback and _is_install_tree(cwd_path):
if _project_context_suppressed(cwd, cwd_path, allow_install_tree_fallback):
logger.warning(
"skipping project-context discovery: working-directory resolution fell back to the Hermes "
"install tree (%s) — set terminal.cwd to your project directory", cwd_path,
+3 -1
View File
@@ -488,7 +488,9 @@ class GatewayStatusCommandsMixin:
if not (payload.get("categories") or []):
return []
details = _quiet_sync(lambda: compute_context_details(agent), {"skills": [], "toolsets": []}) if expanded else None
return render_context_breakdown_lines(payload, details=details, grid=False)
from agent.context_file_sources import context_file_sources_for_agent, render_context_file_lines
file_lines = _quiet_sync(lambda: render_context_file_lines(context_file_sources_for_agent(agent)), [])
return render_context_breakdown_lines(payload, details=details, grid=False) + ([""] + file_lines if file_lines else [])
except Exception:
return []
+7 -1
View File
@@ -654,10 +654,16 @@ class CLIInfoMixin:
except Exception:
details = {"skills": [], "toolsets": []}
from agent.context_file_sources import context_file_sources_for_agent, render_context_file_lines
try:
file_lines = render_context_file_lines(context_file_sources_for_agent(self.agent))
except Exception:
file_lines = []
print()
print(f" 🧠 Context Usage — {payload.get('model') or self.model}")
print()
for line in render_context_breakdown_lines(payload, details=details, grid=True):
for line in render_context_breakdown_lines(payload, details=details, grid=True) + ([""] + file_lines if file_lines else []):
print(f" {line}")
print()
+58 -87
View File
@@ -1,17 +1,15 @@
#!/usr/bin/env python3
"""Per-file context source manifest (`list_context_file_sources`).
"""Per-file context manifest (``agent/context_file_sources.py``) behind the ``/context`` Rules figure.
Read-only mirror of ``build_context_files_prompt`` discovery: one entry per
candidate context/instruction file with size, token estimate, and whether it
was loaded, truncated, or shadowed by a higher-priority context type.
Inspired by: GitHub Copilot CLI 1.0.81 — "Show each user instruction file
separately in /instructions".
The manifest and ``build_context_files_prompt`` share one discovery walk, so the invariant under test is
parity: a file is reported ``loaded`` iff its content appears in the built prompt.
"""
from pathlib import Path
import pytest
from agent.prompt_builder import list_context_file_sources
from agent.context_file_sources import list_context_file_sources, render_context_file_lines
from agent.prompt_builder import build_context_files_prompt
@pytest.fixture()
@@ -24,89 +22,62 @@ def _by_label(sources):
return {s["label"]: s for s in sources}
class TestDiscovery:
def test_agents_md_loaded(self, project):
(project / "AGENTS.md").write_text("# rules\n" * 10)
sources = list_context_file_sources(cwd=str(project))
entry = _by_label(sources)["AGENTS.md"]
assert entry["loaded"] is True
assert entry["status"] == "loaded"
assert entry["chars"] > 0
assert entry["est_tokens"] == (entry["chars"] + 3) // 4
def test_manifest_matches_what_the_prompt_actually_loads(project, tmp_path_factory):
"""Every context type present at once; the ladder picks .hermes.md, the chain lists both AGENTS files,
CLAUDE.md/.cursorrules/.cursor/rules/*.mdc are shadowed, an empty file never wins, SOUL.md rides along."""
(project / ".hermes.md").write_text("hermes rules")
(project / "AGENTS.md").write_text("root agents rules")
sub = project / "pkg"
sub.mkdir()
(sub / "AGENTS.override.md").write_text("") # empty: falls through to AGENTS.md in the same directory
(sub / "AGENTS.md").write_text("pkg agents rules")
(sub / "CLAUDE.md").write_text("claude rules")
(sub / ".cursorrules").write_text("cursor rules")
(sub / ".cursor" / "rules").mkdir(parents=True)
(sub / ".cursor" / "rules" / "a.mdc").write_text("mdc rule a")
home = tmp_path_factory.mktemp("home")
(home / "SOUL.md").write_text("identity text")
def test_empty_project_returns_empty(self, tmp_path, tmp_path_factory):
(tmp_path / ".git").mkdir()
empty_home = tmp_path_factory.mktemp("empty_home")
assert (
list_context_file_sources(cwd=str(tmp_path), home_override=empty_home)
== []
)
sources = list_context_file_sources(cwd=str(sub), home_override=home)
prompt = build_context_files_prompt(cwd=str(sub), home_override=home)
def test_priority_shadowing_hermes_md_over_agents_md(self, project):
(project / ".hermes.md").write_text("hermes rules")
(project / "AGENTS.md").write_text("agents rules")
entries = _by_label(list_context_file_sources(cwd=str(project)))
assert entries[".hermes.md"]["status"] == "loaded"
assert entries["AGENTS.md"]["status"] == "shadowed"
assert entries["AGENTS.md"]["loaded"] is False
statuses = {s["label"]: s["status"] for s in sources}
assert statuses == {
".hermes.md": "loaded", "../AGENTS.md": "shadowed", "AGENTS.override.md": "empty", "AGENTS.md": "shadowed",
"CLAUDE.md": "shadowed", ".cursorrules": "shadowed", ".cursor/rules/a.mdc": "shadowed", "SOUL.md": "loaded",
}
for src in sources:
body = Path(src["path"]).read_text().strip() if src["chars"] else ""
assert src["loaded"] == (bool(body) and body in prompt), src
assert all(s["est_tokens"] > 0 for s in sources if s["chars"])
def test_claude_md_shadowed_by_agents_md(self, project):
(project / "AGENTS.md").write_text("agents rules")
(project / "CLAUDE.md").write_text("claude rules")
entries = _by_label(list_context_file_sources(cwd=str(project)))
assert entries["AGENTS.md"]["status"] == "loaded"
assert entries["CLAUDE.md"]["status"] == "shadowed"
# Same walk, other winner: drop .hermes.md and the whole AGENTS chain loads while the rest stays shadowed.
(project / ".hermes.md").unlink()
statuses = {s["label"]: s["status"] for s in list_context_file_sources(cwd=str(sub), home_override=home)}
prompt = build_context_files_prompt(cwd=str(sub), home_override=home)
assert statuses["../AGENTS.md"] == statuses["AGENTS.md"] == "loaded" and "root agents rules" in prompt
assert statuses["CLAUDE.md"] == "shadowed" and "claude rules" not in prompt
def test_cursorrules_listed(self, project):
(project / ".cursorrules").write_text("cursor rules")
rules_dir = project / ".cursor" / "rules"
rules_dir.mkdir(parents=True)
(rules_dir / "a.mdc").write_text("rule a")
entries = _by_label(list_context_file_sources(cwd=str(project)))
assert entries[".cursorrules"]["status"] == "loaded"
assert entries[".cursor/rules/a.mdc"]["status"] == "loaded"
def test_agents_override_wins_per_directory(self, project):
(project / "AGENTS.md").write_text("committed")
(project / "AGENTS.override.md").write_text("personal override")
labels = [s["label"] for s in list_context_file_sources(cwd=str(project))]
assert "AGENTS.override.md" in labels
assert "AGENTS.md" not in labels # first name wins per directory
def test_truncated_and_suppressed_statuses_follow_the_builder(project, monkeypatch, tmp_path_factory):
import agent.prompt_builder as pb
def test_directory_chain_lists_both_agents_files(self, project):
(project / "AGENTS.md").write_text("root rules")
sub = project / "pkg"
sub.mkdir()
(sub / "AGENTS.md").write_text("pkg rules")
sources = list_context_file_sources(cwd=str(sub))
agents_entries = [s for s in sources if "AGENTS.md" in s["label"]]
assert len(agents_entries) == 2
assert all(s["loaded"] for s in agents_entries)
monkeypatch.setattr(pb, "_get_context_file_max_chars", lambda *_a: 40)
(project / "AGENTS.md").write_text("x" * 100)
home = tmp_path_factory.mktemp("home")
entry = _by_label(list_context_file_sources(cwd=str(project), home_override=home))["AGENTS.md"]
assert entry["status"] == "truncated" and entry["loaded"] is True
assert "[...truncated AGENTS.md" in build_context_files_prompt(cwd=str(project), home_override=home)
def test_truncated_status_when_over_cap(self, project, monkeypatch):
import agent.prompt_builder as pb
# Install-tree guard: a fallback cwd (cwd=None) inside the Hermes tree lists the file but never loads it.
monkeypatch.setattr("agent.runtime_cwd._is_install_tree", lambda _p: True)
monkeypatch.chdir(project)
entry = _by_label(list_context_file_sources(cwd=None, home_override=home))["AGENTS.md"]
assert entry["status"] == "suppressed" and entry["loaded"] is False
assert build_context_files_prompt(cwd=None, skip_soul=True) == ""
assert _by_label(list_context_file_sources(cwd=None, allow_install_tree_fallback=True, home_override=home))[
"AGENTS.md"]["status"] == "truncated"
monkeypatch.setattr(pb, "_get_context_file_max_chars", lambda *_a: 10)
(project / "AGENTS.md").write_text("x" * 100)
entries = _by_label(list_context_file_sources(cwd=str(project)))
assert entries["AGENTS.md"]["status"] == "truncated"
assert entries["AGENTS.md"]["loaded"] is True
def test_soul_md_from_home_override(self, project, tmp_path_factory):
home = tmp_path_factory.mktemp("hermes_home")
(home / "SOUL.md").write_text("identity")
entries = _by_label(
list_context_file_sources(cwd=str(project), home_override=home)
)
assert entries["SOUL.md"]["status"] == "loaded"
def test_read_only_no_side_effects(self, project):
(project / "AGENTS.md").write_text("rules")
before = (project / "AGENTS.md").read_text()
list_context_file_sources(cwd=str(project))
assert (project / "AGENTS.md").read_text() == before
def test_accepts_path_object(self, project):
(project / "AGENTS.md").write_text("rules")
sources = list_context_file_sources(cwd=project)
assert _by_label(sources)["AGENTS.md"]["loaded"] is True
lines = render_context_file_lines(list_context_file_sources(cwd=None, home_override=home))
assert lines[0] == "Context files" and "AGENTS.md" in lines[1] and "install tree" in lines[1]
assert render_context_file_lines([]) == []
+1 -1
View File
@@ -63,7 +63,7 @@ Type `/` in the CLI to open the autocomplete menu. Built-in commands are case-in
| `/egress [status]` | Show Docker egress proxy status — enabled/configured/running state, credential source, token mappings, uncovered providers, and next remediation step. Works in CLI, TUI, Desktop chat, and messaging gateway. |
| `/redraw` | Force a full UI repaint (recovers from terminal drift after tmux resize, mouse selection artifacts, etc.) |
| `/status` | Show session info — model, provider, profile, session ID, working directory, title, created/updated timestamps, token totals, agent-running state — followed by a local **Session recap** block (recent user/assistant turn counts, tool result count, top tools used, last few files touched, the latest user prompt, and the latest assistant reply). The recap is computed locally from the in-memory conversation; no LLM call, no prompt-cache impact. |
| `/context [all]` (alias: `/ctx`) | Visual context-window breakdown. On the CLI/TUI: a 5×20 glyph block grid (each cell ≈ 1% of the model window) plus an estimated per-category table — system prompt, tool definitions, rules, skills index, MCP, subagents, memory, conversation — versus free space, followed by a per-file **Context files** listing (AGENTS.md chain, .hermes.md, CLAUDE.md, .cursorrules, SOUL.md) with each file's token estimate and whether it was loaded, truncated over `context_file_max_chars`, or shadowed by a higher-priority context type. On messaging platforms: a usage gauge with auto-compression threshold/headroom, compression stats, cumulative throughput, and the same category table in plain text. `/context all` appends per-skill and per-toolset cost listings (index cost vs SKILL.md load cost; schema tokens per toolset). Read-only and computed locally — no LLM call, no prompt-cache impact. |
| `/context [all]` (alias: `/ctx`) | Visual context-window breakdown. On the CLI/TUI: a 5×20 glyph block grid (each cell ≈ 1% of the model window) plus an estimated per-category table — system prompt, tool definitions, rules, skills index, MCP, subagents, memory, conversation — versus free space. On messaging platforms: a usage gauge with auto-compression threshold/headroom, compression stats, cumulative throughput, and the same category table in plain text. Both end with a per-file **Context files** listing (.hermes.md, AGENTS.md chain, CLAUDE.md, .cursorrules + .cursor/rules/*.mdc, SOUL.md) showing each file's token estimate and whether it was loaded, truncated over `context_file_max_chars`, shadowed by a higher-priority context type, empty/unreadable, or suppressed by the install-tree guard — the answer to "why is my CLAUDE.md ignored?". `/context all` appends per-skill and per-toolset cost listings (index cost vs SKILL.md load cost; schema tokens per toolset). Read-only and computed locally — no LLM call, no prompt-cache impact. |
| `/agents` (alias: `/tasks`) | Show active agents and running tasks across the current session. |
| `/bg <prompt>` | Run a prompt in a separate background session. The agent processes your prompt independently — your current session stays free for other work. Results appear as a panel when the task finishes. See [CLI Background Sessions](/user-guide/cli#background-sessions). |
| `/btw <question>` | Ask a quick side question **about the current conversation** without interrupting it. A one-shot auxiliary LLM call answers from a read-only snapshot of the transcript — the live session's history and prompt cache are untouched, and the current turn keeps running. For independent work with a fresh context, use `/bg`. |