> ## Documentation Index
> Fetch the complete documentation index at: https://obversa.ai/docs/llms.txt
> Use this file to discover all available pages before exploring further.

# Get a Second Opinion

> A different model reviews the work and gives the writer notes to act on.

Let another model review a draft before you call it done. Use it when a
second opinion can catch what the writer missed. The writer gets the notes
and tries again, within the revision limit you set.

In `workflow()`, the reviewer must come from a different model family than
the writer. If the decision needs several views, [ask a panel](/docs/patterns/review-panel).

## Shape

```mermaid theme={null}
flowchart LR
  write["write: Claude seat"] -->|writes| test[["test: node --test"]]
  test -->|exit 0| review["review: Codex seat"]
  test -->|sendsBackTo| write
  review -->|sendsBackTo| write
  review -->|agree: 1| done((pass))
```

## The team

Two roles and three stages. The writer is a Claude seat, the reviewer a
Codex seat, and the test between them is a command:

```ts examples/teams/writer-reviewer-pair.ts (excerpt) {5-8,22,29} theme={null}
  return workflow('writer-reviewer-pair', {
    brief: briefFromFile('briefs/add.md'),
    options: { timeout: '10m' },

    roles: {
      write: engines.claude('claude-sonnet-4-5'),
      review: [engines.codex('gpt-5.6-luna')],
    },

    stages: [
      stage('write', {
        agent: 'write',
        writes: ['src/add.mjs', 'test/add.test.mjs'],
        desc: 'Write the function and its test from the brief.',
        gate: 'The files named in the brief exist in the workspace.',
        refine: 1,
      }),
      stage('test', {
        run: ['node', '--test', 'test/add.test.mjs'],
        desc: 'Run the test command against the written files.',
        gate: 'The test command exits 0.',
        sendsBackTo: 'write',
      }),
      stage('review', {
        panel: 'review',
        agree: 1,
        desc: 'Read the code, the test and its result.',
        gate: 'The change meets the brief.',
        sendsBackTo: 'write',
      }),
    ],
  });
```

The writer produces the files the brief names. `writes` is checked after
the stage: a missing or empty file fails it by name, and so does a change to
another declared file. Node runs the test, and a red run carries its output
to `write`, which runs again with it. The reviewer reads the code, the test
and its result only after the test passes, and a rejection goes the same
way. `refine: 1` on the writer allows one
[refinement](/docs/concepts/feedback-loops#counting-rounds): the first build
and one more.
When the rounds run out before the reviewer passes the work, the run fails
with the last findings.
The two seats must come from different model families; `workflow()`
refuses the team before any model runs when they share one, so a model
never approves its own work.

<Warning>
  Run the file in a directory you're happy for a model to change. The Claude
  seat runs with permission prompts off, so it can run any command the process
  can.
</Warning>

## What the run did

The brief is `briefs/add.md` beside the file: its body is the task, and its
front matter names the files the writer must produce. Run the file from the
directory the work belongs in, with the Claude Code and Codex command line
tools signed in. One real run printed:

```json Output theme={null}
{
  "status": "pass",
  "summary": "dag \"writer-reviewer-pair\": all 3 node(s) green",
  "data": {
    "write": {
      "status": "pass",
      "summary": "Created src/add.mjs with a pure add function and test/add.test.mjs with Node.js test coverage"
    },
    "test": {
      "status": "pass",
      "summary": "`node` exited 0"
    },
    "review": {
      "status": "pass",
      "summary": "Review panel: 1/1 reviewer(s) cleared.",
      "data": {
        "findings": [],
        "escalatedFindings": [],
        "errors": [],
        "results": [
          {
            "kind": "verdict",
            "name": "review-1",
            "met": true,
            "reason": "The named add(a, b) function returns a + b without side effects, and the required Node test passes."
          }
        ],
        "passed": 1,
        "required": 1,
        "severityCounts": {}
      }
    }
  }
}
```

All three stages passed first time. The writer left `src/add.mjs`:

```js src/add.mjs theme={null}
export function add(a, b) {
  return a + b;
}
```

and `test/add.test.mjs`:

```js test/add.test.mjs theme={null}
import assert from 'node:assert/strict';
import { test } from 'node:test';
import { add } from '../src/add.mjs';

test('add(2, 3) returns 5', () => {
  assert.strictEqual(add(2, 3), 5);
});

test('add(0, 0) returns 0', () => {
  assert.strictEqual(add(0, 0), 0);
});

test('add(-1, 1) returns 0', () => {
  assert.strictEqual(add(-1, 1), 0);
});

test('add handles negative numbers', () => {
  assert.strictEqual(add(-5, -3), -8);
});
```

Each seat comes from its plugin. `claude('claude-sonnet-4-5')` and
`codex('gpt-5.6-luna')` carry the identity the plugin records, and the
record and the family check read it. The Codex seat runs sandboxed to the
workspace with approvals off. Each helper takes an options object to choose
otherwise.

<Accordion title="Full file">
  ```ts examples/teams/writer-reviewer-pair.ts theme={null}
  import { claude } from '@obversa/engine-claude-cli';
  import { codex } from '@obversa/engine-codex-cli';
  import { run } from '@obversa/runtime';
  import { briefFromFile, stage, workflow, type TeamSeat } from '@obversa/runtime';

  interface WriterReviewerEngines {
    readonly claude: (model: string) => TeamSeat;
    readonly codex: (model: string) => TeamSeat;
  }

  const realEngines: WriterReviewerEngines = { claude, codex };

  function createWriterReviewerPair(engines: WriterReviewerEngines = realEngines) {
    return workflow('writer-reviewer-pair', {
      brief: briefFromFile('briefs/add.md'),
      options: { timeout: '10m' },

      roles: {
        write: engines.claude('claude-sonnet-4-5'),
        review: [engines.codex('gpt-5.6-luna')],
      },

      stages: [
        stage('write', {
          agent: 'write',
          writes: ['src/add.mjs', 'test/add.test.mjs'],
          desc: 'Write the function and its test from the brief.',
          gate: 'The files named in the brief exist in the workspace.',
          refine: 1,
        }),
        stage('test', {
          run: ['node', '--test', 'test/add.test.mjs'],
          desc: 'Run the test command against the written files.',
          gate: 'The test command exits 0.',
          sendsBackTo: 'write',
        }),
        stage('review', {
          panel: 'review',
          agree: 1,
          desc: 'Read the code, the test and its result.',
          gate: 'The change meets the brief.',
          sendsBackTo: 'write',
        }),
      ],
    });
  }

  const result = await run(createWriterReviewerPair());
  console.log(JSON.stringify(result.outcome, null, 2));
  ```
</Accordion>

## Next steps

* [Feature delivery](/docs/workflows/feature-team): the same builder, test and
  reviewers inside a seven-step team, from a ticket to a person's approval.
* [Runtime](/docs/packages/runtime): `workflow`, `stage`, `briefFromFile` and
  the keys of a stage.
* [Feedback loops](/docs/concepts/feedback-loops): the shapes a review can take
  and the limit that stops the loop.


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.