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

# Software factory, as a workflow

> Search calls it a software factory. What ships is a workflow: named roles, a review that sends work back, and a person at the merge, on the engines you already use.

Search calls it a software factory. What ships is a workflow: named roles, a
review that sends work back, a person at the merge. Obversa is that runtime,
on the engines you already use.

The factory picture is a belt that turns issues into merged pull requests
without a person. The teams that make it work do not run a belt. They run a
process: one agent writes, another reviews, the work goes back when the
review fails, tests run as commands rather than as agent turns, and a person
decides at the end. That process is what you write here, as one file.

## The file

The file is `examples/teams/feature-delivery.ts`.

```ts theme={null}
import { claude } from '@obversa/engine-claude-cli';
import { codex } from '@obversa/engine-codex';
import { run } from '@obversa/runtime';
import { fromFile, person, stage, workflow } from '@obversa/teams';

/**
 * A feature, delivered the way a team delivers one. The roles are named once;
 * every stage is a small block of nouns: who does it, what it writes, who
 * reads it, where a red result goes back to. Inference happens only where a
 * role is named; every other stage is a command or a person.
 */
const team = workflow('feature-delivery', {
  brief: fromFile('briefs/triple.md'),
  options: { timeout: '10m' },

  roles: {
    analyse: claude('claude-sonnet-4-5'),
    implement: codex('gpt-5.6-luna'),
    'research-review': [codex('gpt-5.6-luna')],
    'code-review': [claude('claude-sonnet-4-5')],
    approve: person('Ship this change?'),
  },

  stages: [
    stage('research-context', {
      agent: 'analyse',
      writes: 'team-output/research-context.md',
      desc: 'Read the workspace and write down what the change touches.',
      gate: 'The context note is in the workspace and a reviewer has accepted it.',
      reviewedBy: 'research-review',
      retry: 3,
    }),

    stage('research-requirements', {
      agent: 'analyse',
      writes: 'team-output/research-requirements.md',
      desc: 'Turn the brief and the context note into requirements, one REQ-n per line.',
      gate: 'The requirements note is in the workspace and a reviewer has accepted it.',
      reviewedBy: 'research-review',
      retry: 3,
    }),

    stage('plan', {
      agent: 'analyse',
      writes: 'team-output/plan.md',
      desc: 'Write an executable plan from the requirements, one check per REQ-n.',
      gate: 'Every requirement has a check in the plan.',
      reviewedBy: 'research-review',
      retry: 3,
    }),

    stage('tests-first', {
      agent: 'implement',
      writes: 'test/triple.test.mjs',
      desc: 'Write the declared test files from the accepted plan before any implementation exists.',
      gate: 'Every declared test file exists and covers the plan.',
      reviewedBy: 'code-review',
      retry: 3,
    }),

    stage('implement', {
      agent: 'implement',
      writes: 'src/triple.mjs',
      desc: 'Write the code to the plan and the tests.',
      gate: 'The source file exists.',
      retry: 3,
    }),

    stage('test', {
      run: ['node', '--test', 'test/triple.test.mjs'],
      desc: 'Run the tests; a red run goes back to implement with the output.',
      gate: 'The test command exits 0.',
      sendsBackTo: 'implement',
    }),

    stage('review', {
      panel: 'code-review',
      agree: 1,
      desc: 'Read the change and the test result against the plan.',
      gate: 'At least one reviewer has accepted the change.',
      sendsBackTo: 'implement',
    }),

    stage('approve', {
      input: 'approve',
      desc: 'Put the verified change in front of a person.',
      gate: 'A person has said yes.',
    }),

    stage('close', {
      agent: 'analyse',
      writes: ['team-output/evidence.md', 'team-output/learning.md'],
      desc: 'Write the evidence of the run and what was learned, from the record alone.',
      gate: 'Both notes are in the workspace.',
    }),
  ],

});

const result = await run(team);
console.log(JSON.stringify(result.outcome, null, 2));
```

Every stage names its worker and the files it may write. The review role is
a seat from another provider. A failed review sends the work back to the
stage that owns it, with the findings, up to the retry you set. The last
stage is a person.

## What a factory search usually wants

| the search                                  | what you write                                                                                                                                                                                                                |
| ------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| Issues in, pull requests out.               | A workflow whose first stage reads the issue and whose last stage is a person at the merge.                                                                                                                                   |
| Many agents on one change.                  | Named roles, one engine each, from more than one provider.                                                                                                                                                                    |
| Quality without a human reading every diff. | A review role that sends work back with findings, and commands for the tests.                                                                                                                                                 |
| Runs that do not lose work.                 | A record of every step as it happens. A plain run does not resume; the [supervised runner](/driving/runner) restarts a killed run from its own record, and today it drives a compiled graph rather than a file like this one. |
| A place to watch it.                        | Every run can [serve its own page](/driving/monitor).                                                                                                                                                                         |

## Things that catch people out

* **A product factory is the same search with a bigger scope.** The answer
  is the same: a workflow with the reviews and the person written in.
* **An agent pipeline without a return is a queue.** The step that sends
  work back is what makes the review count. See [the review loop](/reviewing/review-loop).
* **The tests are commands.** An agent that runs and supervises a test suite
  burns tokens to do what a command does for free. Write `run` stages for
  them.

## Where to go

[Domain-specific harnesses](/use-cases/domain-specific-harnesses) for the
same file read as one job's harness; [the review loop](/reviewing/review-loop)
for how a return works.
