Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

### Fixed

- Review instruction files now include a `## Project Root` directive stating the absolute project root so reviewer subagents read files from the correct working tree — fixes spurious findings in git-worktree setups where the subagent's cwd differed from the worktree the commits actually lived in (REVIEW-REQ-005.1.9)

### Removed
## [0.13.3] - 2026-04-10

Expand Down
18 changes: 13 additions & 5 deletions specs/deepwork/review/REVIEW-REQ-005-instruction-generation.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,11 +11,14 @@ For each `ReviewTask`, the system generates a self-contained markdown instructio
1. Each instruction file MUST be a valid markdown document.
2. The file MUST begin with a heading identifying the review rule and scope (e.g., `# Review: python_file_best_practices — src/app.py`).
3. The file MUST contain a "Review Instructions" section with the rule's resolved instruction text.
4. The file MUST contain a "Files to Review" section listing the file paths to examine when the task has at least one file to review. Inline-content tasks (see REVIEW-REQ-005.1.8) MUST NOT include a "Files to Review" section.
4. The file MUST contain a "Files to Review" section listing the file paths to examine when the task has at least one file to review.
5. File paths in the "Files to Review" section MUST be relative to the repository root.
6. When the task has `additional_files` (unchanged matching files), the file MUST contain an "Unchanged Matching Files" section listing those file paths.
7. When the task has `all_changed_filenames`, the file MUST contain an "All Changed Files" section listing every changed filename for context.
8. When the task has `inline_content` set (used for `type: string` step outputs — see JOBS-REQ-004.8), the file MUST contain a "Content to Review" section whose body is the inline content verbatim. The review heading scope MUST read `inline content` when the task has `inline_content` and no `files_to_review`.
8. When the task has `inline_content` set (used for `type: string` step outputs — see JOBS-REQ-004.8), the file MUST contain a "Content to Review" section whose body is the inline content verbatim.
9. The file MUST contain a "Project Root" section near the top (between the header and "Review Instructions") that states the absolute path of the project root against which all relative file paths in the document are resolved, and that instructs the reviewer to prepend this root when calling the Read tool. This is required so reviewer subagents read files from the correct working tree when their current working directory differs from the project root — e.g., when the review runs against a git worktree dispatched from the main checkout.
10. Inline-content tasks (see REVIEW-REQ-005.1.8) MUST NOT include a "Files to Review" section.
11. The review heading scope MUST read `inline content` when the task has `inline_content` and no `files_to_review`.

### REVIEW-REQ-005.2: File Path Formatting

Expand Down Expand Up @@ -61,6 +64,11 @@ For each `ReviewTask`, the system generates a self-contained markdown instructio
1. When a task's `reference_files` is empty, the instruction file MUST NOT contain a "Relevant File Contents" section.
2. When a task has `reference_files`, the instruction file MUST contain a "## Relevant File Contents" section placed between "Review Instructions" and "Files to Review".
3. Each inlined file MUST be rendered with a `### {relative_label}` subheading, the optional description, and the file contents inside a fenced code block whose language is inferred from the file extension.
4. The number of inlined reference files MUST NOT exceed `MAX_INLINE_FILES` (20). Entries beyond that cap MUST be listed in an "omitted due to size/count caps" summary line rather than inlined.
5. The total inlined byte size of reference file contents MUST NOT exceed `MAX_INLINE_TOTAL_BYTES` (256 * 1024). Files whose contents would exceed the remaining byte budget MUST be truncated with a visible truncation marker, and any subsequent entries MUST be reported in the omitted summary line.
6. When a referenced file cannot be read (missing, permission denied, or invalid UTF-8), the system MUST emit a graceful marker line referencing the file and the error, MUST NOT abort the section, and MUST NOT count the file's would-be bytes against the budget.
4. The number of inlined reference files MUST NOT exceed `MAX_INLINE_FILES` (20).
5. The total inlined byte size of reference file contents MUST NOT exceed `MAX_INLINE_TOTAL_BYTES` (256 * 1024).
6. When a referenced file cannot be read (missing, permission denied, or invalid UTF-8), the system MUST emit a graceful marker line referencing the file and the error.
7. Reference file entries beyond the `MAX_INLINE_FILES` cap MUST be listed in an "omitted due to size/count caps" summary line rather than inlined.
8. When a reference file's content would exceed the remaining byte budget, the file MUST be truncated with a visible truncation marker.
9. Reference file entries that cannot be inlined because the byte budget was exhausted by a preceding truncation MUST be reported in the omitted summary line.
10. When a referenced file cannot be read, the system MUST NOT abort the "Relevant File Contents" section.
11. When a referenced file cannot be read, the system MUST NOT count the file's would-be bytes against the total byte budget.
4 changes: 3 additions & 1 deletion src/deepwork/review/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,9 @@ class ReviewRule:
agent: dict[str, str] | None
all_changed_filenames: bool
unchanged_matching_files: bool
precomputed_info_bash_command: str | None # Resolved absolute command path
precomputed_info_bash_command: (
str | None
) # Shell command string (first path component resolved to absolute)
source_dir: Path # Directory containing the .deepreview file
source_file: Path # Path to the .deepreview file
source_line: int # Line number of the rule name in the .deepreview file
Expand Down
60 changes: 51 additions & 9 deletions src/deepwork/review/instructions.py
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,11 @@ def _content_hash(files: list[str], project_root: Path, inline_content: str | No
``inline_content`` is provided, it is mixed into the hash (via a
sentinel marker) so that each distinct string value produces a
distinct review ID.

``files`` entries are expected to be repo-root-relative paths sourced
from trusted ``.deepreview`` config files. Absolute paths or ``..``
segments are not validated here; callers MUST ensure paths stay
inside ``project_root``.
"""
h = hashlib.sha256()
for filepath in sorted(files):
Expand All @@ -105,8 +110,15 @@ def _content_hash(files: list[str], project_root: Path, inline_content: str | No
def _run_precompute_command(command: str, project_root: Path) -> str:
"""Run a single precompute bash command and return its stdout.

Precompute commands are fully trusted input sourced from the repo's own
``.deepreview`` files; they run via ``shell=True`` and MUST NOT be fed
any untrusted external data (e.g., user-supplied strings interpolated
into the command).

Args:
command: Resolved absolute path to the command to execute.
command: Shell command string to execute. The first path component
has been resolved to an absolute path by the caller; the rest
of the command is passed through to the shell verbatim.
project_root: Working directory for command execution.

Returns:
Expand Down Expand Up @@ -146,8 +158,10 @@ def _run_precompute_commands(commands: set[str], project_root: Path) -> dict[str
if not commands:
return {}

# Cap concurrent precompute shells so a repo with many rules does not
# briefly fork dozens of subprocesses at once on CI runners.
results: dict[str, str] = {}
with ThreadPoolExecutor() as executor:
with ThreadPoolExecutor(max_workers=8) as executor:
futures = {
executor.submit(_run_precompute_command, cmd, project_root): cmd for cmd in commands
}
Expand Down Expand Up @@ -205,12 +219,15 @@ def write_instruction_files(
if passed_marker.exists():
continue

# Direct key access (not .get()): the invariant below enforces that
# every non-None command produced unique_commands above MUST have a
# result in precompute_results — a missing key indicates drift.
precomputed_info = (
precompute_results.get(task.precomputed_info_bash_command)
if task.precomputed_info_bash_command
precompute_results[task.precomputed_info_bash_command]
if task.precomputed_info_bash_command is not None
else None
)
content = build_instruction_file(task, review_id, precomputed_info)
content = build_instruction_file(task, review_id, precomputed_info, project_root)
file_path = instructions_dir / f"{review_id}.md"

safe_write(file_path, content)
Expand All @@ -223,6 +240,7 @@ def build_instruction_file(
task: ReviewTask,
review_id: str = "",
precomputed_info: str | None = None,
project_root: Path | None = None,
) -> str:
"""Build the markdown content for a single review instruction file.

Expand All @@ -231,6 +249,12 @@ def build_instruction_file(
review_id: The deterministic review ID for this task (used in the
"After Review" section).
precomputed_info: Pre-executed command output to include as context.
project_root: Absolute path to the project root that all relative
file paths in this instruction file resolve against. When
provided, the file includes an explicit "Project Root"
directive so the reviewer agent reads files from the correct
working tree even when its cwd differs (e.g., in a git
worktree dispatched from the main checkout).

Returns:
Markdown string containing the complete review instructions.
Expand All @@ -241,6 +265,21 @@ def build_instruction_file(
scope = _describe_scope(task)
parts.append(f"# Review: {task.rule_name} — {scope}\n")

# Project root directive — tells the reviewer where to read files from.
# Critical in git-worktree setups where the agent's cwd may differ from
# the worktree the commits actually live in.
if project_root is not None:
abs_root = project_root.resolve()
parts.append("## Project Root\n")
parts.append(
f"**All file paths in this document are relative to `{abs_root}`.** "
"When reading any file below with the Read tool, you MUST construct "
"the absolute path by prepending this project root. Do NOT read files "
"relative to your current working directory — it may differ from the "
"project root (e.g., when this review runs against a git worktree)."
)
parts.append("")

# Review instructions
parts.append("## Review Instructions\n")
parts.append(task.instructions.strip())
Expand Down Expand Up @@ -353,17 +392,20 @@ def _build_reference_files_section(reference_files: list[ReferenceFile]) -> str:
remaining = MAX_INLINE_TOTAL_BYTES - total_bytes
truncated_marker = ""
content_bytes = content.encode("utf-8")
if len(content_bytes) > remaining:
original_byte_len = len(content_bytes)
if original_byte_len > remaining:
# Truncate at a character boundary near the byte budget.
content = content_bytes[:remaining].decode("utf-8", errors="ignore")
truncated_marker = (
f"\n... (truncated: file is {len(content_bytes)} bytes, "
f"budget left was {remaining})"
f"\n... (truncated: file is {original_byte_len} bytes, budget left was {remaining})"
)
consumed = remaining
else:
consumed = original_byte_len

parts.append(header)
parts.append(f"\n\n```{lang}\n{content}{truncated_marker}\n```\n")
total_bytes += len(content.encode("utf-8"))
total_bytes += consumed
inlined_count += 1

if omitted:
Expand Down
Loading
Loading