Skip to content

Composing a small architecture from scratch

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.

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.

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.

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.”

Three states: reviewingcritiquingdone. 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 flowgate’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 reviewingcritiquingdone 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 flowgate’s own runtime, which is one of three ways to run this. See How to run it for which mode does what.

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

  • review-agent — Sonnet. Cheap enough to run on every patch, smart enough to catch the obvious misses. The agent’s job is throughput.
  • critique-agent — Opus. You’re paying for the second opinion. If it agrees with the reviewer, you ship. If it doesn’t, you’ve spent the money to find the bug before the user did.

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.

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)

Section titled “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.

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
Terminal window
mcp-flowgate 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.

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

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

The host’s model walks reviewingcritiquingverifyingdone, 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 flowgate’s runtime (How to run it covers the trade-offs).

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.