# Plan, implement, review

**The problem:** take a ticket to a reviewed change, with a plan checked before any code is
written, and pick up where it stopped when a step fails, without redoing the steps that worked.

**The workflow:**
[`examples/feature-delivery`](https://github.com/romanlv/agentswf/tree/main/examples/feature-delivery).
Five [stages](/guide/runs), one after the other:

| Stage | Who | What |
| --- | --- | --- |
| `ticket-doc` | planner, claude | writes an implementation-ready ticket doc, with a skill that says what one holds |
| `doc-review` | reviewer, codex | gates the doc; asks the planner for each revision by message |
| `implementation` | implementer, codex | builds the change from the doc, and records decisions in it |
| `implementation-review` | reviewer | gates the change the same way |
| `additional-review` | one per `--reviewer` | more reviewers at once; the implementer fixes what they ask, and the reviewer checks again |

It needs claude and codex logged in, and Herdr, since the planner and the
implementer run in their TUIs; [Install](/guide/install) has both. From a clone of the repository,
after `bun install`, with `--cwd` at the repository the ticket is for:

```sh
bun awf run --cwd ../my-app --run-root ~/awf-runs examples/feature-delivery/workflow.ts -- ABC-1
bun awf run --cwd ../my-app --run-root ~/awf-runs examples/feature-delivery/workflow.ts --id ABC-1 --continue
```

`--revisions {n}` bounds each review's revisions, 3 by default, and `--reviewer {name}={runtime}`,
given once per reviewer, adds an additional reviewer: `--reviewer security=claude`. The
workflow's `id: (args) => args.ticket` names the run after the ticket, so the continue finds it
with `--id ABC-1`.

<!-- TODO(operator): record a run -->
**Cost and time:** not recorded yet. They come from a recorded run of the example, priced at list
prices; [Cost](/guide/cost) has where an estimate comes from.

## The workflow

```ts
async function deliverFeature(wf: WorkflowContext, args: FeatureArgs): Promise<Handoff> {
  const { ticket } = args;
  const team = await openTeam(wf, args);
  const { planner, reviewer } = team;
  const reviewUntilApproved = reviewLoop(wf, reviewer, args.maxRevisions);

  const plan = await wf.stage("ticket-doc", { result: WORK, summary: (work) => work.docPath }, () =>
    planner.ask(WORK, {
      label: "Create ticket doc",
      prompt: md`Create an implementation-ready ticket doc for ${ticket}.`,
    }),
  );

  const approvedPlan = await wf.stage(
    "doc-review",
    { result: REVIEWED, summary: (review) => review.summary },
    () => reviewUntilApproved(planner, plan, ticketDoc(ticket)),
  );
  const { docPath } = approvedPlan.work;

  const implementer = await team.implementer();
  const built = await wf.stage(
    "implementation",
    { result: WORK, summary: (work) => work.summary },
    () =>
      workOn(implementer, docPath, {
        label: "Implement feature",
        prompt: md`
          Implement ${ticket} from ${docPath}.
          Update that document with the decisions made and any deviations from the plan.
        `,
      }),
  );

  const approvedBuild = await wf.stage(
    "implementation-review",
    { result: REVIEWED, summary: (review) => review.summary },
    () => reviewUntilApproved(implementer, built, implementation(ticket)),
  );

  const { final, additional } = await wf.stage(
    "additional-review",
    { result: ADDITIONAL, summary: ({ additional }) => tally(additional) },
    async () => {
      const additional = await additionalReviews(
        wf,
        args,
        team.additionalReviewer,
        approvedBuild.work,
      );
      const asked = requestedChanges(additional);
      if (asked.length === 0) return { final: approvedBuild, additional };

      const fixed = await workOn(implementer, docPath, {
        label: "Apply additional review",
        prompt: applyFeedback(ticket, approvedBuild.work, asked),
      });
      const final = await reviewUntilApproved(implementer, fixed, implementation(ticket, asked));
      return { final, additional };
    },
  );

  return {
    docPath,
    summary: final.work.summary,
    decisions: final.work.decisions,
    review: { primary: final.summary, additional },
  };
}

export const featureDelivery = defineWorkflow<FeatureArgs, Handoff>({
  meta: {
    name: "feature-delivery",
    description: "Plan, implement, review, and prepare a feature for human review.",
    version: "1.0.0",
  },
  run: deliverFeature,
  prepare: ({ argv }) => parseArgs(argv),
  id: (args) => args.ticket,
  present: ({ docPath, summary, review }) =>
    [summary, docPath, tally(review.additional)].join("\n"),
});

export default featureDelivery;
```

`planner`, `reviewer` and `team.implementer()` come from `team.ts`; each is a small wrapper from
`session.ts` whose `ask` returns the typed answer or stops the run, and whose `stop` names the
agent in the reason. `md` and `list`, from `prompt.ts`, write a prompt inline.

The review loop it runs at each gate:

```ts
/**
 * Returns the loop where the reviewer owns the revisions: it messages the author the changes it
 * needs and reviews each revision the author replies with, until it approves. Code cannot read the
 * revisions, so the work as it stands after review comes from one more turn of the author's.
 */
export function reviewLoop(wf: WorkflowContext, reviewer: Session, maxRevisions = 3) {
  return async (author: Session, work: WorkUpdate, subject: Subject): Promise<Reviewed> => {
    const revising = maxRevisions > 0;
    if (revising) {
      await wf.messages.allow({
        from: reviewer.agent,
        to: author.agent,
        purpose: `Revise the ${subject.name} until the reviewer approves it.`,
        maxMessages: 2 * maxRevisions,
      });
    }
    const verdict = await reviewer.ask(VERDICT, {
      label: `Review ${subject.name}`,
      prompt: revising
        ? md`
            ${subject.review(work)}
            You own the revision loop. When changes are needed, send them with
            \`wfa send --await-reply ${author.agent.key}\` and end your turn; review the revision
            it reports. Return your verdict when it is ready, or the changes still needed once
            the route's limit is reached.
          `
        : subject.review(work),
    });
    if (verdict.kind === "inconclusive") return reviewer.stop(verdict.reason);
    if (verdict.kind === "changes-requested") {
      return wf.stop(`${subject.name}: not approved; ${verdict.feedback.length} changes open`);
    }
    const final = await workOn(author, work.docPath, {
      label: `Record ${subject.name}`,
      prompt: md`Report ${work.docPath} as it stands after review, with every decision made.`,
    });
    return { summary: verdict.summary, work: final };
  };
}
```

The rest is in the folder: `cli.ts` reads the arguments and `schema.ts` holds the answers'
schemas. Its
[test](https://github.com/romanlv/agentswf/blob/main/examples/feature-delivery/workflow.test.ts)
covers a full run, each kind of stop, and continues.

## Why it's shaped this way

- **Each step is a stage, and its value is what the next steps need.** A stage records its value
  when it succeeds, and `--continue` reuses it, so a stop in `implementation-review` keeps the
  plan and the build. The continue opens fresh agents, so every prompt says everything its agent
  needs: the ticket, the doc's path, what the author said it did. Nothing depends on an agent
  remembering an earlier attempt. [Durability](/guide/durability) has how a continue works.
- **The plan is reviewed before code exists.** A wrong plan caught in `doc-review` costs one
  planner revision; caught after implementation, it costs the build. The implementer isn't even
  opened until the plan is approved.
- **The ticket doc is the record.** The planner writes it, the implementer keeps it current with
  each decision, and both reviews read it. An author whose answer names a different doc stops the
  run, so the record can't drift to another file.
- **The reviewer is on another model.** `team.ts` refuses a planner and reviewer on the same
  model, so the gate doesn't share the author's blind spots.
- **The reviewer owns the revisions.** It messages the author each change it needs and reviews the
  revision it gets back, up to `--revisions` of them: the [review loop](/patterns/review-loop),
  inside a stage, so its route ends with the stage. The reviewer runs headless because a codex
  TUI agent can't wait for a reply.
- **After approval, the author reports the work once more.** Code can't read the revisions that
  happened in messages, so the stage's value comes from one more author turn, after review.
- **Every way of not finishing stops the run with a reason.** No answer, an inconclusive review,
  or changes still asked for when the revisions run out each end the attempt `stopped`, naming
  who stopped it and why, ready for a person and a continue. A stop exits 3:
  [Runs](/guide/runs) has the exit codes.

## Catches

- **The prompt names only the ticket.** The planner needs a way to read it: a file in the
  repository, or a tracker its tools reach.
- **Keep the run's records outside the repository.** The planner's skill is copied into the
  run's records, and claude takes no skill from inside its working directory, so `--run-root`
  points elsewhere. Give the same `--run-root` to the continue, so it finds the run.
- **A continue redoes the whole stage that stopped,** a review loop included, from its first
  round.
- **The bound is on revisions, not time.** Give the run a deadline with `--timeout` when it
  shouldn't run longer than you'd wait: [Timeouts](/guide/timeouts).

## See also

- [Stages](/reference/workflow-api#stages) and [Messaging](/reference/workflow-api#messaging) in
  the workflow API.
- [Runs](/guide/runs) and [Durability](/guide/durability) in the guide.
