feat: every subagent's prompt embeds the workspace's project context files

Widened from /review to the class: _build_child_system_prompt now runs
the parent's resolved workspace_path through
agent.prompt_builder.build_context_files_prompt (same discovery/
priority/caps as the main system prompt: .hermes.md > AGENTS.md chain >
CLAUDE.md > .cursorrules; SOUL.md skipped) and embeds the result as
binding conventions. All delegate_task children get it — reviewer
included — since children are built with skip_context_files=True and
previously worked in repos without the repo's own conventions.

The review-engine-local load_workspace_context duplicate is removed;
the reviewer inherits the block via the shared child prompt path.
workspace_path comes only from explicit sources (_resolve_workspace_hint
— TERMINAL_CWD / agent cwd hints, never bare getcwd), so the #64590
install-tree-fallback guard concern doesn't apply.

Tests moved to pin the generalized path (real-filesystem AGENTS.md via
_build_child_system_prompt, empty/no-workspace negatives, reviewer E2E
through start_review). Docs: subagent-context section + /review flow
(en + zh-Hans).
This commit is contained in:
Teknium
2026-08-23 18:48:25 -07:00
parent 23fb949f2c
commit 7526bd39a8
5 changed files with 68 additions and 73 deletions

View File

@@ -144,41 +144,10 @@ def collect_parent_loaded_skills(
return names[:limit]
def load_workspace_context(parent_agent) -> str:
"""Project context files (AGENTS.md / CLAUDE.md / .cursorrules ...) from
the parent's workspace.
Reuses the SAME discovery/priority/cap logic the main agent's system
prompt uses (``agent.prompt_builder.build_context_files_prompt``), pointed
at the parent's resolved workspace directory. Subagents are built with
``skip_context_files=True``, so without this the reviewer would judge repo
work without the repo's own conventions unless it thought to go read them.
SOUL.md is skipped — identity belongs to the parent, not the reviewer.
Returns "" when no workspace can be resolved or no context files exist.
"""
try:
from tools.delegate_tool import _resolve_workspace_hint
cwd = _resolve_workspace_hint(parent_agent)
except Exception:
cwd = None
if not cwd:
return ""
try:
from agent.prompt_builder import build_context_files_prompt
return build_context_files_prompt(cwd=str(cwd), skip_soul=True) or ""
except Exception:
logger.debug("review: workspace context load failed", exc_info=True)
return ""
def build_review_task(
snapshot: List[Dict[str, str]],
user_prompt: str = "",
loaded_skills: Optional[List[str]] = None,
workspace_context: str = "",
) -> tuple:
"""Compose the reviewer subagent's (goal, context) pair."""
goal = (
@@ -218,15 +187,6 @@ def build_review_task(
"and review standards as binding for your assessment — the work "
"was produced under them and must be judged against them."
)
if workspace_context.strip():
lines.append("")
lines.append(
"The workspace's project context files are reproduced below. "
"They are binding for your review — judge the work against "
"these conventions and invariants."
)
lines.append("")
lines.append(workspace_context.strip())
if user_prompt.strip():
lines.append("")
lines.append("Additional review instructions from the user:")
@@ -296,10 +256,7 @@ def start_review(
raise ValueError("Nothing to review yet — the conversation is empty.")
loaded_skills = collect_parent_loaded_skills(parent_agent, messages)
workspace_context = load_workspace_context(parent_agent)
goal, context = build_review_task(
snapshot, user_prompt, loaded_skills, workspace_context
)
goal, context = build_review_task(snapshot, user_prompt, loaded_skills)
credentials_cfg = _load_review_credentials_cfg()
from tools.delegate_tool import delegate_task

View File

@@ -383,48 +383,47 @@ def test_start_review_threads_loaded_skills_into_context(monkeypatch):
# ---------------------------------------------------------------------------
# load_workspace_context — reviewer sees the workspace's AGENTS.md et al.
# Workspace context files — ALL subagents (reviewer included) get AGENTS.md
# et al. in their child system prompt (tools/delegate_tool.py)
# ---------------------------------------------------------------------------
def test_load_workspace_context_reads_agents_md(tmp_path, monkeypatch):
def test_child_system_prompt_embeds_workspace_context(tmp_path):
"""Real file I/O through the same loader the main system prompt uses."""
import agent.review_engine as engine
import tools.delegate_tool as dt
from tools.delegate_tool import _build_child_system_prompt
workspace = tmp_path / "proj"
workspace.mkdir()
(workspace / "AGENTS.md").write_text(
"# Project Rules\nAll fixes must cover sibling call sites.\n"
)
monkeypatch.setattr(dt, "_resolve_workspace_hint", lambda parent: str(workspace))
out = engine.load_workspace_context(MagicMock())
assert "sibling call sites" in out
assert "Project Context" in out
prompt = _build_child_system_prompt(
"do the thing", None, workspace_path=str(workspace)
)
assert "sibling call sites" in prompt
assert "binding for your work" in prompt
def test_load_workspace_context_empty_without_workspace(monkeypatch):
import agent.review_engine as engine
import tools.delegate_tool as dt
def test_child_system_prompt_no_context_block_without_files(tmp_path):
from tools.delegate_tool import _build_child_system_prompt
monkeypatch.setattr(dt, "_resolve_workspace_hint", lambda parent: None)
assert engine.load_workspace_context(MagicMock()) == ""
workspace = tmp_path / "empty"
workspace.mkdir()
prompt = _build_child_system_prompt(
"do the thing", None, workspace_path=str(workspace)
)
assert "project context files" not in prompt
def test_briefing_embeds_workspace_context():
snap = [{"role": "user", "text": "review my PR"}]
ctx_files = "# Project Context\n\nNo hooks without a concrete consumer."
_, context = build_review_task(snap, "", None, ctx_files)
assert "No hooks without a concrete consumer." in context
assert "binding for your review" in context
def test_child_system_prompt_no_workspace_no_block():
from tools.delegate_tool import _build_child_system_prompt
prompt = _build_child_system_prompt("do the thing", None, workspace_path=None)
assert "project context files" not in prompt
def test_briefing_omits_workspace_block_when_empty():
_, context = build_review_task([{"role": "user", "text": "hi"}], "")
assert "project context files are reproduced" not in context
def test_start_review_threads_workspace_context(monkeypatch, tmp_path):
def test_review_child_gets_workspace_context_via_dispatch(monkeypatch, tmp_path):
"""E2E through start_review: the reviewer child's *system prompt* carries
the workspace AGENTS.md (inherited from the generalized subagent path)."""
import tools.delegate_tool as dt
workspace = tmp_path / "repo"
@@ -440,8 +439,15 @@ def test_start_review_threads_workspace_context(monkeypatch, tmp_path):
}
built = {}
real_build_prompt = dt._build_child_system_prompt
def fake_build(**kw):
built.update(kw)
# Reproduce what _build_child_agent does with the real prompt builder
built["child_prompt"] = real_build_prompt(
kw.get("goal") or "", kw.get("context"),
workspace_path=dt._resolve_workspace_hint(kw.get("parent_agent")),
)
return fake_child
monkeypatch.setattr(dt, "_build_child_agent", fake_build)
@@ -461,7 +467,7 @@ def test_start_review_threads_workspace_context(monkeypatch, tmp_path):
{"role": "assistant", "content": "PR #4 opened"},
], "")
assert result["status"] == "dispatched"
assert "Never break prompt caching." in built["context"]
assert "Never break prompt caching." in built["child_prompt"]
# ---------------------------------------------------------------------------

View File

@@ -1200,6 +1200,34 @@ def _build_child_system_prompt(
f"{workspace_path}\n"
"Use this exact path for local repository/workdir operations unless the task explicitly says otherwise."
)
# Project context files (AGENTS.md / CLAUDE.md / .cursorrules ...)
# from the workspace, via the SAME discovery/priority/cap logic the
# main agent's system prompt uses. Children are constructed with
# skip_context_files=True (their prompt is this focused one), so
# without this a subagent works in a repo without the repo's own
# conventions unless it thinks to go read them. SOUL.md is skipped —
# identity belongs to the parent. workspace_path comes only from
# explicit sources (_resolve_workspace_hint: TERMINAL_CWD / agent cwd
# hints, never bare getcwd), so the #64590 install-tree-fallback leak
# doesn't apply here. Best-effort: on any failure the child prompt is
# simply built without the block.
try:
from agent.prompt_builder import build_context_files_prompt
_ctx_files = build_context_files_prompt(
cwd=str(workspace_path), skip_soul=True
)
except Exception:
logger.debug(
"subagent: workspace context-files load failed", exc_info=True
)
_ctx_files = ""
if _ctx_files.strip():
parts.append(
"\nThe workspace's project context files are reproduced "
"below. Their conventions and invariants are binding for "
"your work in this workspace.\n\n" + _ctx_files.strip()
)
parts.append(
"\nComplete this task using the tools available to you. "
"When finished, provide a clear, concise summary of:\n"

View File

@@ -37,6 +37,8 @@ delegate_task(tasks=[
Subagents start with a **completely fresh conversation**. They have zero knowledge of the parent's conversation history, prior tool calls, or anything discussed before delegation. The subagent's only context comes from the `goal` and `context` fields the parent agent populates when it calls `delegate_task`.
:::
One exception: when the parent has a resolved workspace directory, every subagent's system prompt embeds that workspace's **project context files** (`.hermes.md` > AGENTS.md chain > CLAUDE.md > `.cursorrules` — the same discovery, priority, and size caps as the main agent's system prompt; SOUL.md is excluded). Subagents working in a repo operate under the repo's own conventions without having to rediscover them.
This means the parent agent must pass **everything** the subagent needs in the call:
```python
@@ -183,7 +185,7 @@ What happens:
1. The last 10 user/assistant messages are snapshotted as the reviewer's starting evidence (tool output and system messages are excluded).
2. A reviewer subagent is dispatched on the same background delegation rail as `delegate_task` — it gets the full normal subagent toolset (terminal, web, files, browser...), so it actually opens the PR, reads the diff, and runs code rather than judging from the excerpt.
3. The reviewer inherits the primary agent's working context: any skills the primary agent had loaded (launch-preloaded or via `skill_view` during the session) are named in its briefing with an instruction to load them and judge the work against their conventions, and the workspace's project context files (AGENTS.md / CLAUDE.md / .cursorrules — same discovery as the main agent's system prompt) are embedded in the briefing as binding review standards.
3. The reviewer inherits the primary agent's working context: any skills the primary agent had loaded (launch-preloaded or via `skill_view` during the session) are named in its briefing with an instruction to load them and judge the work against their conventions. Like every subagent, its system prompt also embeds the workspace's project context files (AGENTS.md / CLAUDE.md / .cursorrules) as binding conventions.
4. When it finishes, its full review re-enters the same session as a normal background-subagent completion — your primary agent sees it and can act on it (fix the findings, push follow-ups, reply to you).
The canonical flow: your main agent opens a PR, you type `/review`, and a second pair of eyes investigates it while you keep working; the review lands back in the chat addressed to the agent that created the PR.

View File

@@ -37,6 +37,8 @@ delegate_task(tasks=[
子智能体以**全新对话**启动。它们对父智能体的对话历史、之前的工具调用或委派前讨论的任何内容一无所知。子智能体的唯一上下文来自父智能体调用 `delegate_task` 时填写的 `goal` 和 `context` 字段。
:::
唯一例外:当父智能体有已解析的工作区目录时,每个子智能体的系统提示都会嵌入该工作区的**项目上下文文件**(`.hermes.md` > AGENTS.md 链 > CLAUDE.md > `.cursorrules`——与主智能体系统提示相同的发现逻辑、优先级和大小上限;SOUL.md 除外)。在仓库中工作的子智能体无需重新发现即可遵循仓库自身的约定。
这意味着父智能体必须在调用中传递子智能体所需的**一切**信息:
```python
@@ -174,7 +176,7 @@ delegation:
1. 最近 10 条用户/助手消息被快照为评审者的起始证据(工具输出和系统消息被排除)。
2. 评审子智能体在与 `delegate_task` 相同的后台委派通道上派发——它拥有完整的常规子智能体工具集(终端、网络、文件、浏览器等),因此会实际打开 PR、阅读 diff、运行代码,而不是仅凭摘录下判断。
3. 评审者继承主智能体的工作上下文:主智能体已加载的技能(启动预加载或会话中通过 `skill_view` 加载)会列在其简报中,并指示其加载这些技能、以其约定为标准评判工作;工作区的项目上下文文件(AGENTS.md / CLAUDE.md / .cursorrules——与主智能体系统提示相同的发现逻辑)也会嵌入简报,作为具有约束力的评审标准。
3. 评审者继承主智能体的工作上下文:主智能体已加载的技能(启动预加载或会话中通过 `skill_view` 加载)会列在其简报中,并指示其加载这些技能、以其约定为标准评判工作。与所有子智能体一样,其系统提示也会嵌入工作区的项目上下文文件(AGENTS.md / CLAUDE.md / .cursorrules)作为具有约束力的约定。
4. 完成后,完整评审作为常规后台子智能体完成事件重新进入同一会话——你的主智能体可以看到并据此行动(修复问题、推送后续提交、回复你)。
典型流程:主智能体开了一个 PR,你输入 `/review`,第二双眼睛在你继续工作的同时对其进行调查;评审结果回到聊天中,交给创建该 PR 的智能体。