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
9 changes: 8 additions & 1 deletion scripts/flowctl/commands/review/adversarial.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ def _load_adversarial_prompt(focus_block: str, diff_summary: str,
"""Load adversarial review prompt from prompts/adversarial-review.md."""
prompt_path = _get_plugin_root() / "prompts" / "adversarial-review.md"
template = prompt_path.read_text()
return template.replace(
result = template.replace(
"{{focus_block}}", focus_block,
).replace(
"{{diff_summary}}", diff_summary,
Expand All @@ -35,6 +35,13 @@ def _load_adversarial_prompt(focus_block: str, diff_summary: str,
).replace(
"{{embedded_files}}", embedded_files,
)
# Warn on unconsumed placeholders
remaining = re.findall(r"\{\{(\w+)\}\}", result)
if remaining:
import sys
print(f"Warning: unconsumed placeholders in adversarial prompt: {remaining}",
file=sys.stderr)
return result


def parse_adversarial_output(output: str) -> Optional[dict]:
Expand Down
16 changes: 9 additions & 7 deletions scripts/flowctl/commands/review/codex_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -224,23 +224,24 @@ def run_codex_exec(
# Try resume first - use stdin for prompt (model already set in original session)
cmd = [codex, "exec", "resume", session_id, "-"]
try:
codex_timeout = int(os.environ.get("FLOW_CODEX_TIMEOUT", "600"))
result = subprocess.run(
cmd,
input=prompt,
capture_output=True,
text=True,
check=True,
timeout=600,
timeout=codex_timeout,
)
output = result.stdout
# For resumed sessions, thread_id stays the same
return output, session_id, 0, result.stderr
except subprocess.CalledProcessError:
except subprocess.CalledProcessError as e:
# Resume failed - fall through to new session
pass
except subprocess.TimeoutExpired:
print(f"WARNING: Codex resume failed ({type(e).__name__}), starting new session", file=sys.stderr)
except subprocess.TimeoutExpired as e:
# Resume failed - fall through to new session
pass
print(f"WARNING: Codex resume failed ({type(e).__name__}), starting new session", file=sys.stderr)

# New session with model + high reasoning effort
# --skip-git-repo-check: safe with read-only sandbox, allows reviews from /tmp etc (GH-33)
Expand All @@ -258,20 +259,21 @@ def run_codex_exec(
"--json",
"-",
]
codex_timeout = int(os.environ.get("FLOW_CODEX_TIMEOUT", "600"))
try:
result = subprocess.run(
cmd,
input=prompt,
capture_output=True,
text=True,
check=False, # Don't raise on non-zero exit
timeout=600,
timeout=codex_timeout,
)
output = result.stdout
thread_id = parse_codex_thread_id(output)
return output, thread_id, result.returncode, result.stderr
except subprocess.TimeoutExpired:
return "", None, 2, "codex exec timed out (600s)"
return "", None, 2, f"codex exec timed out ({codex_timeout}s)"


def parse_codex_thread_id(output: str) -> Optional[str]:
Expand Down
23 changes: 14 additions & 9 deletions scripts/flowctl/commands/review/commands.py
Original file line number Diff line number Diff line change
Expand Up @@ -276,8 +276,19 @@ def cmd_codex_plan_review(args: argparse.Namespace) -> None:
base_branch = args.base if hasattr(args, "base") and args.base else "main"
context_hints = gather_context_hints(base_branch)

# Only forbid disk reads when ALL files were fully embedded.
files_embedded = not embed_stats.get("budget_skipped") and not embed_stats.get("truncated")
# Resolve sandbox mode early — needed for files_embedded decision below
try:
sandbox = resolve_codex_sandbox(getattr(args, "sandbox", "auto"))
except ValueError as e:
error_exit(str(e), use_json=args.json, code=2)

# When sandbox prevents disk reads (anything other than "none"), Codex
# can't read files itself — tell it all context is embedded regardless of
# truncation. Only use the budget-based check for no-sandbox mode.
if sandbox != "none":
files_embedded = True
else:
files_embedded = not embed_stats.get("budget_skipped") and not embed_stats.get("truncated")
prompt = build_review_prompt(
"plan", epic_spec, context_hints, task_specs=task_specs, embedded_files=embedded_content,
files_embedded=files_embedded
Expand Down Expand Up @@ -305,13 +316,7 @@ def cmd_codex_plan_review(args: argparse.Namespace) -> None:
rereview_preamble = build_rereview_preamble(spec_files, "plan", files_embedded)
prompt = rereview_preamble + prompt

# Resolve sandbox mode (never pass 'auto' to Codex CLI)
try:
sandbox = resolve_codex_sandbox(getattr(args, "sandbox", "auto"))
except ValueError as e:
error_exit(str(e), use_json=args.json, code=2)

# Run codex
# Run codex (sandbox already resolved above)
effort = getattr(args, "effort", "high")
output, thread_id, exit_code, stderr = run_codex_exec(
prompt, session_id=session_id, sandbox=sandbox, effort=effort
Expand Down
Loading