Composing a small architecture from scratch

Walk through building a minimal code-review architecture: two skills, one workflow, two agents, ready to ship.

You’re going to build a complete cognitive architecture from scratch — two skills, one workflow, two agent configs, no external tools. It’s the smallest useful thing: a model proposes a patch, a critic adversarially reviews it, you ship or you don’t. By the end you’ll have a 30-line YAML you can run.

The goal

A code-review architecture. The flow:

  1. A reviewing agent reads a patch and produces a verdict against a rubric.
  2. A critiquing agent adversarially attacks the verdict — what did the reviewer miss?
  3. The workflow lands in done with both opinions recorded in the workflow’s shared state (its context).

No retrieval, no editing, no human gate. The minimum viable cognitive architecture. You’d extend it later; for now you’ll see the whole shape on one screen. And here’s the part that fixes the failure you actually care about: by Step 6 you’ll add a deterministic step that runs the real test suite before any approval lands — no model in the loop, so the model can’t talk its way past a red build.

Step 1 — Identify the verbs

Both jobs are evaluation against criteria. One verb: review. Subject root: review.*. That’s it.

If you found yourself reaching for two different verbs at this step, you’d want two different workflows or you’d want to think harder about whether you have one job or two. Keep it tight.

Step 2 — Write two skill fragments

skills:
  review.code.adversarial:
    verb: review
    lifecycle: stable
    body: |
      # Adversarial patch review
      - Find missed edge cases, hidden regressions, policy or security issues.
      - Cite each finding with file:line and a counterexample input.
      - Return verdict: accept | retry | escalate.

  review.code.final-approval:
    verb: review
    lifecycle: stable
    body: |
      # Final approval checklist
      - Re-read the diff against the original acceptance criteria, not just the summary.
      - Confirm the adversarial review's findings were addressed or explicitly waived.
      - Return verdict: approve | reject with a specific question.

Two skills, two distinct cognitive postures. Both review, but adversarial primes “attack this” and final-approval primes “decide and ship.”

Step 3 — Write the workflow

Three states: reviewing → critiquing → done. The first two delegate to named agents (the operator wires up which model plays each role). The terminal state has no delegate.

The blackboard: block is the workflow’s shared state — the typed declaration of what context holds (the same context you thread results into elsewhere; see Workflows). Each output: mapping writes into it, and later states read from it.

workflows:
  code_review:
    version: "2026-05-25"
    initialState: reviewing
    skills:
      - review.code.adversarial
      - review.code.final-approval
    blackboard:
      candidatePatch:    { type: object }
      reviewVerdict:     { type: string }
      critiqueVerdict:   { type: string }

    states:
      reviewing:
        delegate: review-agent
        goal: Adversarially review the candidate patch
        skills:
          - review.code.adversarial
        transitions:
          review_done:
            target: critiquing
            executor: { kind: noop }
            output:
              reviewVerdict: "$.arguments.verdict"

      critiquing:
        delegate: critique-agent
        goal: Final approval pass over the reviewed patch
        skills:
          - review.code.final-approval
        transitions:
          approve:
            target: done
            executor: { kind: noop }
            output:
              critiqueVerdict: "$.arguments.verdict"

      done:
        terminal: true

kind: noop means no executor runs on the transition — the agent’s verdict in arguments is the result, and the output: mapping records it. (The deterministic test-runner step in Step 6 is where an executor actually does work.)

A note on delegate:, because it changes meaning depending on how you run this. Run this workflow inside your existing agent — Claude Code or Cursor driving praxec’s two tools — and delegate: is purely informational: one model (your host’s model) drives every state, and review-agent/critique-agent are just labels on the record. You still get the real win here: the model must walk reviewing → critiquing → done in order, and the verification step in Step 6 can’t be skipped. The actual multi-model split — review-agent on one model, critique-agent on another, each in an isolated sub-agent session with no shared write handle — needs praxec’s own runtime, which is one of three ways to run this. See How to run it for which mode does what.

Step 4 — Pick agents

Two roles, two model choices. The bet is that the first pass is volume work and the critique is precision work.

You’d swap these around for other workloads (Opus for review on safety-critical code, Sonnet for critique on docs PRs). The workflow doesn’t change; the agent configs do.

Step 5 — Wire connections

None. This architecture doesn’t touch external tools. The reviewing and critiquing agents work entirely from the patch text and the rubric in the skill body.

This is the simplest case. The moment you want retrieval (search the codebase before reviewing), verification (run tests after editing), or human approval gates, you add a connections: block. For now: skip it.

Step 6 — Add a deterministic verification step (scripts)

Reviewers are good. They miss things anyway. Before the workflow declares approval, run the workspace’s actual test suite — deterministically, hash-pinned, no model in the loop.

This is what curated scripts are for. Where a skill is guidance for an LLM, a script is executable bytes the workflow runs through the script executor. Both live at the same intent layer (see The intent layer). Both are content-identified by hash. Their division of labor is determinism — skills are indeterminate (the model interprets), scripts are determinate (the bytes execute).

Add a scripts: block. One script for now: run the workspace’s checks and either pass or fail loud.

scripts:
  verify.workspace.green:
    verb: verify
    lifecycle: stable
    body: |
      #!/usr/bin/env bash
      set -euo pipefail
      cargo fmt --all -- --check >&2
      cargo clippy --workspace --all-targets -- -D warnings >&2
      cargo test --workspace >&2
      echo '{"passed":true,"stages":["format","lint","test"]}'

Insert a verifying state into the workflow between critiquing and done. The state’s transition uses kind: script with the curated subject — the runtime materializes the body from this workflow’s pinned snapshot, execs it (chmod 0700 temp file), captures the verdict, and audit-logs the body hash.

verifying:
  goal: Confirm the workspace is green before approval
  transitions:
    run_verifier:
      target: done
      actor: deterministic
      executor:
        kind: script
        subject: verify.workspace.green
      output:
        verifierPassed: "$.output.success"

The transition is actor: deterministic because no model decides whether to take it — the workflow auto-chains once the critique succeeds. The verifying script’s exit code drives the verdict: a non-zero exit fails the transition (if you declared an onTimeout or escalate path on the state, it takes that — this minimal example just fails the transition and stops). Pair the executor with a script_acknowledged guard if you want operators to review the script body before it runs.

Step 7 — Compose the gateway config

Put it all together. Save as code-review.yaml:

version: "1.0.0"

skills:
  review.code.adversarial:
    verb: review
    lifecycle: stable
    body: |
      # Adversarial patch review
      - Find missed edge cases, hidden regressions, policy or security issues.
      - Cite each finding with file:line and a counterexample input.
      - Return verdict: accept | retry | escalate.

  review.code.final-approval:
    verb: review
    lifecycle: stable
    body: |
      # Final approval checklist
      - Re-read the diff against the original acceptance criteria.
      - Confirm the adversarial review's findings were addressed or waived.
      - Return verdict: approve | reject with a specific question.

scripts:
  verify.workspace.green:
    verb: verify
    lifecycle: stable
    body: |
      #!/usr/bin/env bash
      set -euo pipefail
      cargo fmt --all -- --check >&2
      cargo clippy --workspace --all-targets -- -D warnings >&2
      cargo test --workspace >&2
      echo '{"passed":true,"stages":["format","lint","test"]}'

workflows:
  code_review:
    version: "2026-05-25"
    initialState: reviewing
    skills: [review.code.adversarial, review.code.final-approval]
    blackboard:
      candidatePatch:  { type: object }
      reviewVerdict:   { type: string }
      critiqueVerdict: { type: string }
      verifierPassed:  { type: boolean }
    states:
      reviewing:
        delegate: review-agent
        goal: Adversarially review the candidate patch
        skills: [review.code.adversarial]
        transitions:
          review_done:
            target: critiquing
            executor: { kind: noop }
            output: { reviewVerdict: "$.arguments.verdict" }
      critiquing:
        delegate: critique-agent
        goal: Final approval pass over the reviewed patch
        skills: [review.code.final-approval]
        transitions:
          approve:
            target: verifying
            executor: { kind: noop }
            output: { critiqueVerdict: "$.arguments.verdict" }
      verifying:
        goal: Confirm the workspace is green before approval
        transitions:
          run_verifier:
            target: done
            actor: deterministic
            executor:
              kind: script
              subject: verify.workspace.green
            output:
              verifierPassed: "$.output.success"
      done:
        terminal: true

Step 8 — Validate

praxec check --config code-review.yaml

check confirms the skill verbs are in the closed ten, the script verbs are in the closed twelve, every subject starts with a blessed root, every skills: ref resolves, the script executor’s subject resolves into the workflow’s snapshot, the blackboard slots line up with the output: mappings, and every state is reachable. Fix anything it flags before you run.

Step 9 — Run

Start the workflow through any MCP host by calling praxec.command:

{
  "definitionId": "code_review",
  "input": { "candidatePatch": { "diff": "..." } }
}

The host’s model walks reviewing → critiquing → verifying → done, following the HATEOAS links the gateway returns at each step. Run it this way and one model drives all four states. To actually bind review-agent and critique-agent to different models — each in its own isolated sub-agent session, with per-state timeouts and escalation as safety rails — wire them up via agents.yaml and run under praxec’s runtime (How to run it covers the trade-offs).

What you skipped

This architecture has no retrieval, no verification, no human review gate, no retry budget on failed critique. All of those are one or two states each, and you compose them in by extending the workflow.

For a fuller example with retrieval, constrained editing, deterministic verification, and a human approval gate, see cognitive-architectures/examples/full-swe-pipeline.yaml. Same three layers, more states, more skills, real connections.