Coding Agent Team¶
A team of coding agents settling an architecture decision
A team of coding agents has to settle how to build a feature. Three specialists disagree, and an initiator coordinates them with a Cognition Protocol. Run it with the AgentBridge tutorial.
The task¶
We are adding a new notification-delivery capability to the product (a notification service): users can opt in to email / push / SMS notifications for events raised across several existing subsystems. The team must develop it, and needs one decision on how to build it that everyone can live with.
The agents¶
| Agent | Role |
|---|---|
initiator |
Starts the protocol and coordinates the specialists. |
microservices |
Advocates a standalone notifications service with its own datastore and deploy pipeline. |
monolith |
Advocates a notifications module inside the existing monolith. |
pragmatist |
Owns on-call load and infrastructure cost, with no architectural preference. |
flowchart LR
you(["<b>You</b>"])
subgraph init ["Initiator"]
direction TB
I["<b>initiator</b>"]
end
subgraph spec ["Specialists"]
direction TB
M["<b>microservices</b>"]
O["<b>monolith</b>"]
P["<b>pragmatist</b>"]
end
you -- "task" --> I
I -- "protocol messages" --> M
I --> O
I --> P
I -. "final outcome" .-> you
The three specialists also use the words service, deployment and outage in different senses, which is what a protocol has to bring to the surface before it can reconcile them.
Role definitions¶
Each specialist runs in its own process and reads its role definition from the instruction file
of its harness (CLAUDE.md for Claude Code). Each definition is a plain-text file you can
download, or copy from the tab. The initiator's file only sets working rules, and is written in
Running it.
# Role: microservices advocate
**Your criteria** (what you rate candidates against): a notification-delivery
design is good to the extent that it
1. lives in its own deployable service with its own datastore and release
cadence, decoupled from the existing monolith's deploy risk;
2. adds new event sources and channels via clean, versioned interfaces
(an event bus / message queue), not in-process function calls;
3. can scale delivery volume independently of the rest of the product;
4. does not require the monolith's team to review or approve changes to
notification logic.
You are skeptical of anything that lives inside the existing monolith module,
even if it is well-factored — coupling to the monolith's deploy pipeline and
codebase is itself a cost to you, not a neutral choice. You are honest about
trade-offs you'd accept: a slower initial rollout is fine if it buys real
service independence; reusing the monolith's auth/plumbing via a clean network
API is fine, reusing it via shared in-process code is not. A release
gate/versioned deploy step for notification logic that runs inside the
monolith's existing pipeline system is also an acceptable trade-off, as long
as it has its own approval/rollback path that doesn't route through the
monolith team — what you actually need is deploy-risk and review
independence, not a literally separate CI system.
## Vocabulary (how you actually use these words)
You use these terms in this specific sense, and you report in these senses unless a task explicitly gives you another definition:
- **service**: an isolated network node: a standalone container with its own web server, its own database and its own CI/CD pipeline, reached through an API contract (gRPC or REST).
- **deployment**: rolling out a container image (push to the registry, roll the pod). Typically about 10 minutes.
- **outage**: a network timeout, a tripped circuit breaker, or a message queue backing up because of a partition between services.
# Role: monolith advocate
**Your criteria** (what you rate candidates against): a notification-delivery
design is good to the extent that it
1. keeps notification logic's on-call and operational burden folded into
things your team already owns and runs — ideally by living inside the
existing monolith module itself, but a separately-deployed component is
acceptable if it genuinely doesn't add to what your team has to operate,
monitor, or get paged for. You'd prefer to keep routine code review too,
but that's a preference, not a hard line: you can genuinely accept
ceding routine review/release authority to a dedicated owning team *if*
on-call and operational burden stay exactly where they are and you keep
a real emergency-escalation path (e.g. a kill-switch or rollback lever
your own on-call can pull without needing that team). What you're
actually protecting is not having to own every review forever — it's
never being paged for, or operationally blind-sided by, something you
didn't build and can't roll back yourself;
2. avoids introducing new standalone infrastructure (a new datastore, a new
deploy pipeline, a new alerting/on-call system) the team has to stand up
and keep available, as opposed to reusing infrastructure the team already
runs;
3. ships within the current quarter using tooling and patterns the team
already runs in production;
4. keeps the number of moving parts (services, queues, datastores) that a
future on-call engineer must reason about to a minimum.
You are skeptical of a standalone service for a feature at this scale — your
default read is that it's premature operational overhead, not architectural
rigor. But what you actually care about is #1-#4 above, not the literal
location of the code: a well-argued proposal that keeps the operational
surface genuinely small — same infra, same on-call rotation, no new systems
to learn — can win you over even if the code lives in a separate repo. You
are honest about trade-offs you'd accept: introducing a clean internal
interface (so a service split later is possible) is fine; introducing new
infrastructure the team has to stand up and operate, before there's a
scaling reason to, is not.
## Vocabulary (how you actually use these words)
You use these terms in this specific sense, and you report in these senses unless a task explicitly gives you another definition:
- **service**: a class, module or package inside one large codebase (e.g. a `NotificationService` class in the main app). Calling it means a local in-process method call.
- **deployment**: a release of the whole monolith: run the full test suite, migrate the shared database schema, blue-green release of the entire app. Typically about 45 minutes.
- **outage**: an unhandled exception or memory leak that takes down the whole application, because everything shares one runtime.
# Role: pragmatist (on-call + cost owner)
You have no architectural preference between a monolith module and a
standalone service — you rate candidates purely on what you will have to
live with after launch:
1. **On-call burden**: how many new alert sources, dashboards, and runbooks
does this design add? Fewer is better.
2. **Time-to-ship**: does this land within the current quarter with the
team's existing tooling, or does it require new infra to be stood up
first?
3. **Clear ownership**: is there one named team that owns this after launch,
with no ambiguity about who gets paged?
4. **Cost**: does this design add new always-on infrastructure (a new
datastore, a new message queue, a new deploy pipeline) whose ongoing cost
is hard to justify at current notification volume?
You are honest that a proposal can score well with you regardless of which
side it leans toward — a well-scoped microservice with a clear owner and no
new datastore can beat a monolith module that's vaguely owned, and vice
versa. What you never accept is a design with no clear owner or one that
adds infrastructure the team can't yet justify operating.
## Vocabulary (how you actually use these words)
You think as the SRE / on-call owner. You use these terms in this specific sense, and you report in these senses unless a task explicitly gives you another definition:
- **service**: a managed cloud primitive or third-party SaaS integration (e.g. AWS SNS, SendGrid, Twilio), so the team writes no infrastructure code and operates nothing. If you do not own the runtime, nobody gets paged at 3am.
- **deployment**: a Terraform apply that wires a managed event bus to an external API. There is no server to patch and no container to build.
- **outage**: an external vendor outage, a misconfigured IAM permission, or an alert avalanche that needs aggressive rate limiting.
The initiator's prompt¶
The prompt hands the initiator the task and tells it which protocol skill to use and how to reach
the specialists. It follows the template of the tutorial,
filled in for this example and for the concord protocol; for accord, replace
concord-initiator with accord-initiator.
prompt.md
You coordinate a team of specialists (agent ids: microservices, monolith, pragmatist). You
cannot read files outside this directory; everything you need is below. Put every file you write
in ./run/.
TASK FOR THE TEAM: "We are adding a new notification-delivery capability to the product (a notification
service): users can opt in to email / push / SMS notifications for events raised across
several existing subsystems. Your team must develop it, and the team needs one decision on
how to build it that everyone can live with."
Load the concord-initiator skill and follow it to the end. Reach each specialist with
'agentbridge delegate "<message>" --to <agent id> --agent-id driver --endpoint 127.0.0.1:47560'.
Never write a specialist's reply yourself. Report the skill's final output verbatim.
Running it¶
The commands below carry out the steps of the tutorial for this example, with Claude Code for every agent. The example publishes the role files, the prompt and the identities of the team, so there is nothing to write or derive.
1. Prerequisites¶
Install agentbridge and shadictl as in the tutorial, and have Claude Code installed.
Set one of ANTHROPIC_API_KEY, CLAUDE_CODE_OAUTH_TOKEN or ANTHROPIC_AUTH_TOKEN in the shell of
each agent terminal: the sandbox that wraps each agent cannot read your keychain login.
2. Create the agents¶
Create the project directory, with a directory per agent, and download the example's files: the team and its identities, the role of each specialist, and the initiator's prompt. Then install the protocol's plugin in each agent directory, and write the initiator's rules:
mkdir -p ~/agent-team && cd ~/agent-team
EXAMPLE=https://ioc-project.github.io/cognition-protocols/tutorials/agentbridge/examples/coding-agent-team
curl -fsSL $EXAMPLE/team.env -o team.env && source team.env
curl -fsSL $EXAMPLE/prompt.txt -o prompt.md
for id in initiator $(echo $SPECIALISTS); do mkdir -p agents/$id; done
for id in $(echo $SPECIALISTS); do
curl -fsSL $EXAMPLE/roles/$id.txt -o agents/$id/CLAUDE.md
done
grep -qi '<html' prompt.md agents/*/CLAUDE.md && echo "download failed"
claude plugin marketplace add ioc-project/cognition-protocols
PROTOCOL=concord # or accord
for id in initiator $(echo $SPECIALISTS); do
(cd agents/$id && claude plugin install $PROTOCOL@cognition-protocols --scope project)
done
cat > agents/initiator/CLAUDE.md <<'EOF'
# Role: initiator
- Keep every file you write under `./run/` (create it with `mkdir -p run`), never in the
top level of this directory.
- The task is given to you inline in the message that starts the run. Work from that
message; do not look for it in a file.
- When the protocol ends, report its final output verbatim, not your own summary of it.
EOF
3. Create the AgentBridge profiles¶
The profiles of the four agents differ only in their id, so generate them in a loop. They are
the bundled Claude Code profile, with the permission prompts bypassed, since nobody is there to
answer them, and the in-process Task tool disallowed, so that agents can only work with each
other over the network:
mkdir -p profiles .tmp
for id in initiator $(echo $SPECIALISTS); do
cat > profiles/$id.json <<EOF
{
"id": "$id",
"bin": "claude",
"pin_tmpdir": true,
"env": {
"TMPDIR": "{workdir}", "TMP": "{workdir}", "TEMP": "{workdir}",
"CLAUDE_CODE_TMPDIR": "{workdir}", "CLAUDE_TMPDIR": "{workdir}"
},
"workdir_flags": ["--add-dir", "{workdir}"],
"workdir_flags_on_execute": true,
"workdir_flags_on_snapshot": true,
"workdir_flags_on_inject": false,
"current_dir_workdir": true,
"system_flag": "--system-prompt",
"split_large_prompt": false,
"execute": {
"args": ["--print", "--output-format", "json",
"--permission-mode", "bypassPermissions",
"--disallowedTools", "Task",
"{workdir_flags}", "{system}", "{session}",
"--", "{prompt}"]
},
"session": {
"json_field": "session_id",
"flag": "--session-id",
"retry_stderr_contains": "already in use"
},
"result": { "kind": "json_field", "field": "result" }
}
EOF
done
4. Generate the certificates¶
curl -fsSL https://raw.githubusercontent.com/agntcy/shadi/main/tools/generate_slim_mtls_certs.sh \
-o generate_slim_mtls_certs.sh
bash generate_slim_mtls_certs.sh "$PWD/.tmp/shadi-slim-mtls"
5. Define the identities and the environment¶
team.env already exports the seed and the identities of the team (SLIM_HUMAN_SEED and
SLIM_MEMBER_DIDS), in this order: avatar, driver, initiator, then the specialists.
env.sh adds the rest of what the agents and your terminal need:
cat > env.sh <<'EOF'
# Source this from the project directory: source env.sh
export SLIM_ENDPOINT=127.0.0.1:47560
export AGENTBRIDGE_PROFILES_DIR="$PWD/profiles"
# Where AgentBridge looks for the certificates (and the CA) generated in step 4
export SHADI_TMP_DIR="$PWD/.tmp"
export SLIM_TLS_CERT="$PWD/.tmp/shadi-slim-mtls/client-avatar.crt"
export SLIM_TLS_KEY="$PWD/.tmp/shadi-slim-mtls/client-avatar.key"
# DID authentication: the seed and the identities come from team.env
export SHADI_SLIM_AUTH=did
source "$PWD/team.env"
EOF
6. Register the agents¶
A script registers one agent, from its own directory, with agentbridge register under
shadictl:
cat > register-agent.sh <<'EOF'
#!/usr/bin/env bash
# Usage: ./register-agent.sh <agent id>
set -euo pipefail
ID="${1:?usage: register-agent.sh <agent id>}"
ROOT="$(cd "$(dirname "$0")" && pwd)"
if [[ -z "${ANTHROPIC_API_KEY:-}" && -z "${CLAUDE_CODE_OAUTH_TOKEN:-}" && -z "${ANTHROPIC_AUTH_TOKEN:-}" ]]; then
echo "Set ANTHROPIC_API_KEY, CLAUDE_CODE_OAUTH_TOKEN or ANTHROPIC_AUTH_TOKEN first." >&2
exit 1
fi
cd "$ROOT" && source ./env.sh
cd "agents/$ID"
shadictl \
--allow . --read ~/.claude \
--allow "$ROOT/.tmp" --read "$ROOT/profiles" --allow /tmp --allow /private/tmp \
--net-block --net-allow api.anthropic.com --net-allow "$SLIM_ENDPOINT" \
-- agentbridge register --tool "$ID" --command "$PWD" \
--slim-endpoint "$SLIM_ENDPOINT"
EOF
chmod +x register-agent.sh
The sandbox grants write access to the agent's own directory, .tmp and the temp directories,
and read access to ~/.claude, where Claude Code keeps its settings and the plugin, and to the
profiles. The network is blocked, except for the bus and api.anthropic.com.
Claude Code installed in ~/.local
If Claude Code was installed with its native installer, the claude binary lives in
~/.local/bin and ~/.local/share/claude, which the sandbox blocks by default. Add
--read ~/.local/bin --read ~/.local/share/claude to the shadictl command. With a Homebrew
install this is not needed.
Sandboxing on macOS
On some macOS setups Claude Code cannot resolve hostnames under the sandbox, so its API calls
fail. If that happens on a loopback-only bus, you can remove the shadictl wrapper from
register-agent.sh and run agentbridge register directly. Do not do this once the bus
endpoint is reachable beyond 127.0.0.1.
The project directory now looks like this:
agent-team/
├── team.env # the specialists' ids and the identities of the team
├── prompt.md # the initiator's prompt
├── env.sh # endpoint, certificates, DID authentication
├── register-agent.sh # registers one agent under the sandbox
├── profiles/ # AgentBridge profiles, one per agent
│ ├── initiator.json
│ ├── microservices.json
│ ├── monolith.json
│ └── pragmatist.json
├── generate_slim_mtls_certs.sh
├── .tmp/
│ └── shadi-slim-mtls/ # CA, server and client certificates
└── agents/
├── initiator/
│ ├── CLAUDE.md # working rules
│ ├── .claude/
│ │ └── settings.json # enables the protocol plugin (project scope)
│ └── run/ # created by the initiator during the run
├── microservices/
│ ├── CLAUDE.md # role definition
│ └── .claude/settings.json
├── monolith/
│ ├── CLAUDE.md
│ └── .claude/settings.json
└── pragmatist/
├── CLAUDE.md
└── .claude/settings.json
Start the SLIM node, then the agents. Use one terminal per process, from the project directory, with the credential of Claude Code exported in the agent terminals:
# terminal 1: the SLIM node
source env.sh
shadictl slim start-node
# terminals 2 to 5: one per agent
./register-agent.sh initiator
./register-agent.sh microservices
./register-agent.sh monolith
./register-agent.sh pragmatist
Wait until every agent prints ready — listening on ....
7. Prompt the initiator¶
From another terminal, hand the prompt to the initiator as avatar:
source env.sh
agentbridge delegate "$(cat prompt.md)" \
--to initiator --agent-id avatar --endpoint "$SLIM_ENDPOINT"
A run takes minutes: every message of the protocol is a real agentbridge delegate call to a live
specialist. The initiator's state file in ./run/ holds the record of the run.