# Skill: Add a Review / Approval Gate

Use when you want a review agent to approve or reject the pipeline output before it moves
forward — with the option to retry the upstream worker on rejection.

## How Review Gates Differ from Retry Loops

| Feature | Retry Loop (`verify_node`) | Review Gate (`review_node`) |
|---------|--------------------------|---------------------------|
| State field | `verification_result` | `review_result` |
| Pass condition | `status == "pass"` | `approved == True` |
| Reject field | `error_message` | `feedback` |
| Typical use | Automated quality check | Subjective quality judgment |
| Can be human-in-the-loop | No | Yes (see below) |

Both patterns live in `arccrew/orchestrator/graph.py` and can be combined.

## The `review_result` State Convention

The reviewer agent **must** write to `state["review_result"]`. The gate reads
`review_result.get("approved")`:

```python
# Approved — pipeline continues forward
{"approved": True, "feedback": "Output is high quality and complete."}

# Rejected — retry from review_from node
{"approved": False, "feedback": "The tone is too formal. Rewrite in a casual voice."}
```

The `feedback` string is what the worker reads on retry (stored in `retry_history`).
Make the feedback specific and actionable — the worker will use it to self-correct.

## Writing a Reviewer Agent

Reviewers reason over existing state with no tools. Use `generate()`.

```python
class ReviewerAgent(BaseAgent):
    def __init__(self):
        super().__init__(name="reviewer", prompts_dir=PROMPTS_DIR)

    @property
    def system_prompt(self) -> str:
        return self.get_prompt_manager().assemble_prompt("reviewer")

    @track_timing
    async def execute(self, state: dict) -> Command:
        result = state.get("result", {})
        task = state.get("tasks", [{}])[state.get("current_task_index", 0)]
        retry_count = state.get("retry_count", 0)

        response = await self.generate(
            f"Original task: {task.get('description', '')}\n\n"
            f"Output to review (attempt {retry_count + 1}):\n{result}\n\n"
            f"Does this output meet the quality bar? Be specific."
        )
        data = self.extract_json(response) or {"approved": True, "feedback": response}
        return Command(goto="__end__", update={"review_result": data})
```

Reviewer prompt (`my_project/prompts/reviewer.md`):

```markdown
# Reviewer — Output Quality Gate

You evaluate the worker's output and decide whether it meets the quality bar.
You do NOT use tools. You reason over what you are given.

## Quality Criteria

1. Output is complete — all required fields are present
2. Content is accurate and well-grounded (no hallucinated facts)
3. Tone and style match the task requirements
4. Length and detail are appropriate

## What to Return

If the output meets all criteria:
{"approved": true, "feedback": "Output is complete and meets all requirements."}

If the output needs improvement:
{"approved": false, "feedback": "Specific, actionable description of what to fix."}

Output ONLY the JSON.
```

## Using `review_node` in `create_pipeline()`

```python
from arccrew.orchestrator.graph import create_pipeline
from state import MyState

def create_my_pipeline():
    worker = WorkerAgent()
    reviewer = ReviewerAgent()

    async def worker_node(state): return await worker.execute(state)
    async def reviewer_node(state): return await reviewer.execute(state)

    return create_pipeline(
        state_class=MyState,
        nodes={
            "worker": worker_node,
            "reviewer": reviewer_node,
        },
        flow=["worker", "reviewer"],
        review_node="reviewer",    # this node triggers review gate logic
        retry_from="worker",       # rejected output retries from here
        max_retries=2,             # max 2 rejections before accepting anyway
    )
```

## Combining Review Gate and Retry Loop

For a 3-agent pipeline (worker → verifier → reviewer):

```python
return create_pipeline(
    state_class=MyState,
    nodes={
        "worker": worker_node,
        "verifier": verifier_node,
        "reviewer": reviewer_node,
    },
    flow=["worker", "verifier", "reviewer"],
    verify_node="verifier",      # automated check with retry
    review_node="reviewer",      # final subjective review
    retry_from="worker",         # both failures go back to worker
    max_retries=3,
)
```

## How `_make_review_gate` Routes

From `arccrew/orchestrator/graph.py`:

```python
def _make_review_gate(next_node, retry_node, max_retries):
    def router(state):
        review = state.get("review_result", {})
        retry_count = state.get("retry_count", 0)

        if review.get("approved", True):      # approved (or no review yet)
            return next_node
        if retry_count >= max_retries:
            return next_node                  # max retries hit — move on anyway
        return retry_node                     # retry the worker
    return router
```

Note: `approved` defaults to `True` if missing — the pipeline does not stall if the reviewer
fails to return a valid result.

## Worker Reading `retry_history` for Self-Correction

After a rejection, the feedback is stored in `retry_history`. The worker reads this to
understand what to fix:

```python
@track_timing
async def execute(self, state: dict) -> Command:
    task = state.get("tasks", [{}])[state.get("current_task_index", 0)]
    retry_history = state.get("retry_history", [])

    correction = ""
    if retry_history:
        last_feedback = retry_history[-1].get("feedback", "")
        correction = f"\n\nREVIEWER FEEDBACK FROM LAST ATTEMPT:\n{last_feedback}\n\nAddress this feedback in your output."

    result = await self.run_react(
        task=f"Complete this task:\n{task.get('description', '')}" + correction,
        tools=get_research_tools(),
    )
    data = self.extract_json(result) or {"raw": result}
    return Command(goto="reviewer", update={"result": data})
```

## Passing Context to the Reviewer

The reviewer should have access to everything it needs to make a judgment:

```python
response = await self.generate(
    f"Task: {task.get('description', '')}\n\n"
    f"Research context:\n{state.get('context', {})}\n\n"
    f"Final output:\n{state.get('result', {})}\n\n"
    f"Previous retry feedback: {[h.get('feedback') for h in state.get('retry_history', [])]}\n\n"
    f"Review the final output. Is it complete, accurate, and appropriate?"
)
```

Include `context` when the reviewer needs to check factual accuracy (context has the sources).
Include `retry_history` so the reviewer can check if previous feedback was addressed.

## Human-in-the-Loop Reviewer

Replace the LLM reviewer with a node that always rejects and logs for human review:

```python
class HumanReviewGate(BaseAgent):
    def __init__(self):
        super().__init__(name="human_review", use_llm=False)  # no LLM needed

    @property
    def system_prompt(self) -> str:
        return ""

    @track_timing
    async def execute(self, state: dict) -> Command:
        result = state.get("result", {})
        batch_id = state.get("batch_id", "unknown")

        # Log to a file or external system for human review
        import json
        from pathlib import Path
        review_dir = Path("tmp/review_queue")
        review_dir.mkdir(parents=True, exist_ok=True)
        (review_dir / f"{batch_id}.json").write_text(json.dumps(result, indent=2))

        logger.info(f"[human_review] Output queued for review: {batch_id}")

        # Always "approve" automatically — human reviews asynchronously
        # Replace with webhook/Slack notification + wait if synchronous review needed
        return Command(
            goto="__end__",
            update={"review_result": {"approved": True, "feedback": "Queued for human review"}}
        )
```

For synchronous human-in-the-loop (pipeline waits for human input), use LangGraph's
interrupt mechanism — this is beyond the current framework's built-in support.
