The farm's messaging inside Claude Code: wakes an idle conversation for its mail, gates and logs SendMessage, and gives the model a msg tool

<a href="assets/demo-10s.mp4"><img src="assets/demo-10s.webp" alt="clodfarm in 10 seconds, on the farm UI's HD pixel art, with its tokens counter and your Claude's card top left and the labelled dock (your Claude, the farm, manage, + new Claude) along the bottom: 1, ask your Claude from the Claude app on your phone, and matan's Claude walks in through the field's gate to its plot; 2, it splits the work into mini-Claude sub-agents, each on its own git branch, and its crops start to grow; 3, Claudes team up: matan's Claude asks gil's Claude to take the tests and a sub-agent runs on gil's account; 4, it stays in budget: noa's Claude pauses at 80% of its weekly limit, puts its laptop away and goes to nap in the yard by the barn; 5, it ships tested code: merged to main only when the tests pass, the sub-agents finish and the crops bloom into Claude's spark" width="100%"></a>
<a href="#make-money"><b>Make money</b></a> · <a href="#quick-start"><b>Quick start</b></a> · <a href="#how-it-works"><b>How it works</b></a> · <a href="#multiple-deployments-one-farm"><b>Multi-seat</b></a> · <a href="docs/"><b>Docs</b></a> · <a href="#faq"><b>FAQ</b></a>
<a href="https://github.com/matank001/clodfarm/actions/workflows/ci.yml"><img src="https://img.shields.io/github/actions/workflow/status/matank001/clodfarm/ci.yml?branch=main&label=CI&labelColor=4a3b2c" alt="CI"></a> <a href="LICENSE"><img src="https://img.shields.io/badge/license-MIT-E8875B?labelColor=4a3b2c" alt="MIT license"></a> <img src="https://img.shields.io/badge/python-3.10%2B-3776AB?logo=python&logoColor=white&labelColor=4a3b2c" alt="Python 3.10+"> <img src="https://img.shields.io/badge/docker-ready-2496ED?logo=docker&logoColor=white&labelColor=4a3b2c" alt="Docker"> <img src="https://img.shields.io/badge/runs-Claude%20Code-D97757?labelColor=4a3b2c" alt="Runs Claude Code"> <img src="https://img.shields.io/badge/deploy-AWS%20in%2010%20min-FF9900?logo=amazonaws&logoColor=white&labelColor=4a3b2c" alt="AWS deploy">
<a href="docs/bots.md"><img src="https://img.shields.io/badge/Claude-Anthropic-D97757?logo=anthropic&logoColor=white&labelColor=4a3b2c" alt="Claude (Anthropic)"></a> <a href="docs/bots.md"><img src="https://img.shields.io/badge/GPT-OpenAI-10A37F?labelColor=4a3b2c" alt="GPT (OpenAI)"></a> <a href="docs/bots.md"><img src="https://img.shields.io/badge/Grok-xAI-000000?logo=x&logoColor=white&labelColor=4a3b2c" alt="Grok (xAI)"></a> <a href="docs/bots.md"><img src="https://img.shields.io/badge/Gemini-Google-4285F4?logo=googlegemini&logoColor=white&labelColor=4a3b2c" alt="Gemini (Google)"></a> <a href="docs/bots.md"><img src="https://img.shields.io/badge/Groq-open%20models-F55036?labelColor=4a3b2c" alt="Groq"></a> <a href="docs/bots.md"><img src="https://img.shields.io/badge/OpenRouter-free%20models-6566F1?logo=openrouter&logoColor=white&labelColor=4a3b2c" alt="OpenRouter"></a> <a href="docs/bots.md"><img src="https://img.shields.io/badge/Ollama-local-FFFFFF?logo=ollama&logoColor=black&labelColor=4a3b2c" alt="Ollama"></a>
<h3 align="center">A farm of Claude Code agents that builds your apps, puts them in the cloud, takes the payments and buys the traffic. Around the clock, on your Claude subscription.</h3>
<a name="make-money"></a> <h2><img src="assets/readme/make-money.png" height="44" alt="Make money"></h2>
Give the farm a product to run and it works on it day and night: it builds the app, ships it, charges for it and brings people to it. Every piece is connected once, for every Claude on the farm:
| <img src="assets/icons/aws.svg" width="18" height="18" align="top" alt=""> Builds apps in the cloud | The Claudes write the app, test it and deploy it to your AWS account: landing pages, APIs, a small SaaS. Serverless, inside a fenced role with a hard monthly budget cap, in an account of its own if you like (details). |
| <img src="assets/icons/stripe.svg" width="18" height="18" align="top" alt=""> Takes payments with Stripe | Connect the farm's Stripe once (CONNECTORS): every Claude can make products, prices and payment links, send invoices, run subscriptions and see what came in (docs). |
| <img src="assets/icons/google-ads.svg" width="18" height="18" align="top" alt=""> Brings traffic with Google Ads | Connect Google Ads once (CONNECTORS): the Claudes report on spend, clicks and conversions, and change budgets, bids and campaigns (docs). |
| <img src="assets/icons/chart-column.svg" width="18" height="18" align="top" alt=""> Shows you it's working | Live dashboards the farm keeps fresh: sign-ups, revenue, ad spend, cost per customer. REFRESH gets the numbers now: it runs the dashboard's code, or has its Claude collect them (docs). |
Ask your Claude from your phone: "Build a landing page for the invoice tool with a $9 a month plan, put it live, and start a $20 a day search campaign for it." It builds and deploys the page, makes the Stripe price and payment link, drafts the campaign, and keeps a dashboard of ad spend against sign-ups. Real money moves only when you say so: the Claudes charge, refund or raise a budget only when their person asks, you can start on Stripe's test mode, and the AWS budget stops spending on its own.
clodfarm (say it out loud) is a farm of Claude Code agents running around the clock in a container. A clod is a lump of soil, and this is where your agents grow. They keep working while you sleep, and stop before they eat your week.
Your Claudes run on your Claude subscription. Next to them, bots run Claude Code on any other lab's model, with the same tools, sub-agents, worktrees and messages. A bot can take the jobs your Claudes send it, or run the planner while the Claudes write the code. Each one is counted at its model's list price.
| Provider | Models (for example) | How Claude Code reaches it | Its reasoning on the farm |
|---|---|---|---|
| Anthropic | Opus, Sonnet, Haiku | your subscription (or an API key) | thinking |
| OpenAI | gpt-6.1-sol, gpt-5.5, gpt-5.3-codex | the bot's relay, on OpenAI's Responses API | reasoning summaries |
| xAI | grok-4.7, grok-build-0.1 | the bot's relay, on Chat Completions | reasoning |
gemini-3.1-pro-preview, gemini-3.8-flash | the bot's relay, on Gemini's OpenAI-compatible API | thoughts | |
| Groq | openai/gpt-oss-120b, Kimi, Llama | the bot's relay, on Chat Completions | when the model gives it |
| OpenRouter | free models (:free) | Anthropic's API, directly | when the model gives it |
| Ollama | any local model that calls tools | Anthropic's API, directly | when the model gives it |
| Anything else | vLLM, LM Studio, LiteLLM, a gateway | OpenAI- or Anthropic-compatible | when the model gives it |
clodfarm bot add gpt --provider openai --model gpt-6.1-sol <<< "$OPENAI_API_KEY"
clodfarm bot add grok --provider xai --model grok-4.7 <<< "$XAI_API_KEY"
clodfarm bot add gemini --provider gemini --model gemini-3.1-pro-preview <<< "$GEMINI_API_KEY"
clodfarm planner host gemini # the planner thinks on Gemini; the Claudes write the code
See docs/bots.md.
<img src="assets/architecture.png" alt="How clodfarm works, drawn as the farm in its HD pixel art: on your phone you ask your Claude (matan, with the gold arrow) for work over Remote Control, down the dirt path and through the gate of the fenced field; it works at its plot with three mini-Claude sub-agents, one running on gil's account; gil works at the next plot with a sub-agent of its own; noa naps in the yard by the barn because its budget is paced; the barn is the shared store and git repo, the board beside it runs schedules, and the scarecrow in the field's corner is the planner" width="100%">
<table> <tr> <td width="44%"><img src="assets/readme/shot-farm.png" alt="The real farm UI with thirteen Claudes: the tokens counter and your Claude's card with its 5-hour and 7-day usage top left, chips for approvals, Claudes and sub-agents under them; the barn with its fenced yard and pond on the left; the fenced field on the right, where busy Claudes type at their plots with mini-Claude sub-agents around them and the scarecrow planner rests in the corner; a hint box bottom left and the labelled dock bottom right: help, your Claude (talk, browser), the farm (everyone, tasks, stats), manage (Slack, manage) and + new Claude"></td> <td width="32%"><img src="assets/readme/shot-picker.png" alt="The look picker for your Claude: a big portrait in a straw hat, then twelve hats (straw, beanie, cap, flower, headphones, bow, crown, sprout, leaf, wizard, chef, none), colour swatches for the hat, band and body, extras (scarf, glasses, bowtie, backpack, cape), a surprise me button and save"></td> <td width="24%"><img src="assets/readme/shot-phone.png" alt="The farm on a phone: the tokens counter and your Claude's card on top, the approvals chip, the barn, yard and pond, the fenced field with its busy Claudes and their sub-agents below, and the dock with talk, web, all, tasks, stats, manage and + new along the bottom"></td> </tr> </table>
<img src="assets/readme/steps.png" alt="How you get started. 01 Install: one line on any box with Docker, or one command on AWS with no open ports. 02 Add your Claudes: tap the egg in the farm UI and log in; teammates add theirs the same way. 03 Talk to yours: Claude app, Code, your farm; ask for anything, from anywhere. 04 Watch it grow: sub-agents, messages, schedules and dashboards, all on the farm, paced on real usage." width="100%">
<a name="why-clodfarm"></a> <h2><img src="assets/readme/why.png" height="44" alt="Why clodfarm"></h2>
A while true; claude -p loop forgets what it did, can't split work, can't be reached from your phone, and burns through your limits. clodfarm is the operations layer it's missing:
| <img src="assets/icons/moon.svg" width="18" height="18" align="top" alt=""> Always on | Crashes, restarts and timeouts don't lose work. Claudes come back and pick up where they left off. |
| <img src="assets/icons/claude.svg" width="18" height="18" align="top" alt=""> Talk to it from your phone | Every Claude is a session in your Claude app. Ask for work from anywhere. |
| <img src="assets/icons/git-branch.svg" width="18" height="18" align="top" alt=""> Sub-agents you can see | Big jobs split into sub-agents, each on its own branch, and you watch them work. |
| <img src="assets/icons/gauge.svg" width="18" height="18" align="top" alt=""> Never eats your week | Paced on your real 5-hour and weekly usage. It leaves you 20% and never pays overage. |
| <img src="assets/icons/users.svg" width="18" height="18" align="top" alt=""> Claudes that work together | Teammates' Claudes message each other and run work on whoever has budget left. |
| <img src="assets/icons/shield-check.svg" width="18" height="18" align="top" alt=""> Nothing lands untested | Work reaches main only when your tests pass. |
| <img src="assets/icons/clock.svg" width="18" height="18" align="top" alt=""> Schedules | "Every weekday at 9, triage new issues." Cron, intervals or one-offs. |
| <img src="assets/icons/chart-column.svg" width="18" height="18" align="top" alt=""> Dashboards | The Claudes keep live pages that show what is improving, each with a REFRESH button (docs). |
| <img src="assets/icons/presentation.svg" width="18" height="18" align="top" alt=""> Whiteboards | Sketch for your Claude and it draws back: architectures from Mermaid, flows, plans, sticky notes, live for everyone on the farm (docs). |
| <img src="assets/icons/globe.svg" width="18" height="18" align="top" alt=""> A browser, logged in | Log in to a site once in the farm's browser (LinkedIn, an admin panel) and every Claude works there as you, in the window you watch (docs). |
| <img src="assets/icons/slack.svg" width="18" height="18" align="top" alt=""> Slack | DM or @mention the farm and a sub-agent answers in the thread. Two-minute setup (docs). |
| <img src="assets/icons/box.svg" width="18" height="18" align="top" alt=""> Blender | Connect a Blender MCP server once (CONNECTORS): every Claude builds scenes and game assets, animates, renders and exports on the machine that runs Blender (docs). |
| <img src="assets/icons/modelcontextprotocol.svg" width="18" height="18" align="top" alt=""> Claude Code on your laptop | Connect over MCP and hand the farm work without leaving your editor. |
| <img src="assets/icons/server.svg" width="18" height="18" align="top" alt=""> Many boxes, many seats | One farm and one repo across machines and accounts, each on its own budget. |
| <img src="assets/icons/bell.svg" width="18" height="18" align="top" alt=""> Tells you when it matters | Failures and usage limits go to ntfy, Slack or Discord. |
| <img src="assets/icons/monitor-play.svg" width="18" height="18" align="top" alt=""> A farm you can watch | Every Claude, its sub-agents, usage and tools at a glance, in your browser. A hundred Claudes and a crowd watching stay quick; tokens burned, farm-wide and yours, top left. |
| <img src="assets/icons/users.svg" width="18" height="18" align="top" alt=""> Your own Claude | Hatch one from the farm, pick its look and its tools, sign in to it from your phone with "farm login". Missions others send it wait for your OK (docs). |
| <img src="assets/icons/gauge.svg" width="18" height="18" align="top" alt=""> A planner with a goal | Switch it on with a goal: it works in cycles all the time, delegates to the Claude with the right tools and budget, and builds the tools it needs (docs). |
| <img src="assets/icons/server.svg" width="18" height="18" align="top" alt=""> Upgrades that stop nothing | clodfarm upgrade replaces the farm's code and UI while every agent, and every phone conversation, keeps running (docs). |
You talk to your Claude in the Claude app; the farm UI shows who is working on what; the Claudes and you use the same CLI.
<a name="quick-start"></a> <h2><img src="assets/readme/quick-start.png" height="44" alt="Quick start"></h2>
One container, no config, no database to run:
curl -fsSL https://raw.githubusercontent.com/matank001/clodfarm/main/scripts/install.sh | sh
It pulls the image, starts clodfarm (restarting on reboot), and opens the login: a URL you approve on any device, then paste the code back. Then open Claude app → Code → [clodfarm] clodfarm on your phone and just talk to it: "add CSV export to the report page", "have gil's Claude review it", "every morning at 9, triage new issues". Or from a shell:
docker exec clodfarm clodfarm spawn "Add CSV export" --prompt "Add CSV export to the report page, with tests."
docker exec clodfarm clodfarm status
The farm UI is at http://localhost:8080 (see docs/ui.md): anyone who reaches it watches. There's no admin password: the person of the farm's first Claude is its manager (they sign in to it with "farm login" in the Claude app), can hand that to others and make the farm private (docs/people.md). From the UI you can also log the farm in: tap the egg, open the Claude login link and paste the code back.
docker run -d --name clodfarm --restart unless-stopped \
-v clodfarm_claude-home:/home/farm/.claude -v clodfarm_workspace:/workspace \
ghcr.io/matank001/clodfarm
docker exec -it clodfarm clodfarm login
Or clone the repo and run docker compose up -d. A .env is optional: copy .env.example to change any setting. Either way the farm's state lives in a SQLite file inside the workspace volume, so there's nothing else to run.
[!TIP] Point it at a real repo with
FARM_REPO_URL(plus a deploy key) and setFARM_VERIFY_CMD="pytest -q". The farm clones the repo, and every sub-agent's work lands onmainonly when your tests pass.
<a name="connect-claude-code-on-your-computer"></a> <h2><img src="assets/readme/connect.png" height="44" alt="Connect Claude Code on your computer"></h2>
The farm is a remote MCP server, so the Claude Code on your laptop can see the farm, start sub-agents on it and message its Claudes:
claude mcp add --transport http --scope user farm http://localhost:8080/mcp # or https://<your farm>/mcp
Run /mcp in Claude Code and sign in. The farm's own page asks for its password and a name for your computer, and Claude Code gets a token for this farm only (OAuth 2.1 + PKCE; clodfarm disconnect ends it). Then just ask: "what's the farm doing?", "have the farm add CSV export, on gil", "tell noa the release is out". See docs/mcp.md.
To put your computer's own Claude Code sessions on the farm too (each one shows under your Claude, and the farm's Claudes can message it), install the farm plugin and connect once:
claude plugin marketplace add matank001/clodfarm && claude plugin install farm@clodfarm
Then, in a new Claude Code session, /farm:connect https://<your farm>. After that /farm:on puts a session on the farm, /farm:folder every session started in that folder, and /farm:everywhere all of them. See docs/mcp.md.
<a name="deploy"></a> <h2><img src="assets/readme/deploy.png" height="44" alt="Deploy"></h2>
| What you get | |
|---|---|
| Single deployment | One box, one Claude account. Everything in one container. |
| Multiple deployments, one farm | Several boxes on your account or teammates' own accounts: one farm and one repo, each account paced on its own budget. |
| + Apps role (optional) | Add-on for either one: the Claudes create and run their own serverless apps on AWS, fenced by a permissions boundary and a monthly budget. |
You can start single and add boxes later. A new box simply joins the first one's table.
Any Docker host: run the one-line installer on the server (ssh myserver, then the curl … | sh above). Or start it there and log in from your laptop with ssh -t myserver docker exec -it clodfarm clodfarm login.
AWS (about 10 minutes, one box, no inbound ports):
deploy/aws/deploy.sh up # CloudFormation: VPC, EC2 t4g.medium, DynamoDB table, IAM role limited to that table
deploy/aws/deploy.sh login # over SSM Session Manager: URL + code, same as above
deploy/aws/deploy.sh status # also: logs · shell · down
You need the AWS CLI v2 and the Session Manager plugin. Remote Control, the Claude API and SSM all use outbound HTTPS only. See docs/deploy-aws.md.
The plain deployment above gives the agents no AWS access beyond the farm's own table, and that stays the default. If you want your Claudes to ship their own apps (a landing page, an API, a small SaaS), add the apps role:
deploy/aws/deploy.sh apps-role --email you@example.com --budget 50 # same AWS account as the farm
APPS_PROFILE=my-apps-account deploy/aws/deploy.sh apps-role --email you@example.com \
--budget 50 --domain apps.example.com --regions us-east-1,eu-central-1 # recommended: an account of its own
deploy/aws/deploy.sh apps-down # switch it off again
It deploys deploy/aws/apps-role.yaml, lets the farm box assume the new role, and restarts the farm with FARM_AWS_APPS_* set (the farm stack itself isn't touched). From then on every Claude knows (it's in their guide) that aws --profile apps ... runs as that role, and how to deploy.
| Fence | What it does |
|---|---|
| Serverless only | Lambda, API Gateway, DynamoDB, S3, CloudFront, ACM, Route 53, CloudWatch, EventBridge, SQS, SNS, Step Functions, Cognito, Bedrock and friends. No EC2, RDS or containers. |
| Permissions boundary | Every role the farm creates must be named farm-app-* and carry the boundary, so app roles can never use IAM, billing or the account. The farm can't give itself more rights. |
| Budget lock | A monthly AWS Budget (--budget, USD). Alerts at 50% and 80%; at 100% of actual spend AWS attaches a deny-all to the role by itself. Running apps keep serving. |
| Hard denies | No IAM users or access keys, no email (SES), no domain purchases, no Marketplace or Savings Plans, and no changes to the stack that made the role. |
| Optional | --domain creates a Route 53 zone for <app>.<domain>; --regions limits where it deploys. |
Use a separate AWS account for the apps (APPS_PROFILE, e.g. a new account in your AWS Organization): then nothing the farm deploys can reach anything else you run, and closing the account removes it all. An SCP on that account makes the fences hold even against a mistake in IAM. More in docs/deploy-aws.md.
A single box keeps its farm in a local SQLite file. To spread one farm over several boxes, the boxes share a DynamoDB table instead (FARM_STORE=dynamodb; the AWS deploy sets it for you).
You need:
--workspace-repo / FARM_REPO_URL); sub-agent branches travel through it.# box 1 creates the farm (table "clodfarm")
deploy/aws/deploy.sh up --workspace-repo git@github.com:you/repo.git && deploy/aws/deploy.sh login
# more boxes on your account: more room to run, never extra usage
STACK=farm-2 deploy/aws/deploy.sh up --table clodfarm --workspace-repo git@github.com:you/repo.git
STACK=farm-2 deploy/aws/deploy.sh login
# a teammate's box, logged in to THEIR account: a second seat, with its own budget
STACK=farm-gil deploy/aws/deploy.sh up --table clodfarm --workspacehooks/register.ts 122 lines1import type { EngineInterface, Register } from 'claude-code'
2
3// The farm's messaging from inside Claude Code. The farm keeps every message in its store and touches this session's
4// flag file (FARM_MAIL_FLAG) when one waits for its Claude or sub-agent; the settings hooks it installs
5// (auth.farm_hooks) hand the mail over at a prompt, after a batch of tool calls and before a turn ends. This module
6// does the parts a shell hook can't do well:
7// * wakes an idle conversation when its mail arrives, with a prompt of its own, instead of a `clodfarm hook --listen`
8// process left polling for ten minutes after every turn (and Claude Code waiting up to 30 s for it at exit)
9// * gates and logs every message Claude Code's SendMessage sends (the main loop, its agents, other plugins) at
10// session.send, where a refusal reads as the tool's own and only what was delivered is logged
11// * gives the model `msg`, the farm's own messages as a tool rather than a shell command
12// It sets FARM_MOD_LIVE for everything the session starts, so the settings hooks' shell versions of those steps stand
13// down; a session that didn't load it keeps their behaviour.
14
15const POLL_MS = 1000
16const LISTEN_MS = 600_000 // auth.LISTEN_SECONDS: a conversation is woken for mail this long after its last turn
17const MSG = 'msg'
18
19type Reply = { text?: string; ok?: boolean; why?: string }
20type MsgInput = { to?: unknown; text?: unknown; urgent?: unknown; wake?: unknown }
21
22/** `clodfarm hook --mod <action>`: the input as JSON on stdin, a JSON answer on stdout (see cli._mod). */
23async function farm($: EngineInterface, action: string, input: Record<string, unknown> = {}): Promise<Reply> {
24 const run = await $.process.run(['clodfarm', 'hook', '--mod', action],
25 { stdin: JSON.stringify(input), timeoutMs: 20_000 })
26 try {
27 return JSON.parse(run.stdout || '{}') as Reply
28 } catch {
29 return {}
30 }
31}
32
33/** This session's farm state: module values, so a reload starts them over (the mail waits in the store meanwhile). */
34const my = {
35 flag: undefined as string | undefined, // set only in a farm session: every hook below stands aside otherwise
36 busy: false, // the main loop's turn is running: the settings hooks deliver mail inside it
37 lastTurnEnd: undefined as number | undefined,
38 waking: false,
39}
40
41/** Every POLL_MS in a conversation: when its mail flag is up and its last turn ended a little while ago, take the
42 * mail and start a turn with it. */
43async function wake($: EngineInterface) {
44 if (!my.flag || my.busy || my.waking || my.lastTurnEnd === undefined) return
45 if ((await $.clock.now()) - my.lastTurnEnd > LISTEN_MS) return // its mail waits for its next prompt
46 if (!(await $.fs.exists(my.flag))) return
47 my.waking = true
48 try {
49 const { text } = await farm($, 'take', { how: 'wake' })
50 if (text) {
51 my.busy = true // until that turn ends, so the next tick doesn't take more for it
52 await $.prompt.submit({ text })
53 }
54 } finally {
55 my.waking = false
56 }
57}
58
59export const register: Register = on => {
60 on('session.start', async ($, e, next) => {
61 const task = await $.env.get('FARM_TASK_ID')
62 my.flag = task === 'usage' ? undefined : await $.env.get('FARM_MAIL_FLAG')
63 if (!my.flag) return next(e)
64 await $.env.set('FARM_MOD_LIVE', '1')
65 await $.tool.register({
66 name: MSG,
67 description:
68 'Send a message to another Claude on this farm (by its name) or to a sub-agent (by its id), the same as ' +
69 '`clodfarm msg`. A Claude\'s conversations get it at their next tool call or prompt, and one active in the ' +
70 'last 10 minutes is woken for it; a running sub-agent gets it at its next tool call. `urgent` interrupts a ' +
71 'running sub-agent now; `wake` starts someone to handle it if nobody has read it in a while.',
72 inputSchema: {
73 type: 'object',
74 properties: {
75 to: { type: 'string', description: 'A Claude\'s name, or a sub-agent\'s id' },
76 text: { type: 'string', description: 'The message' },
77 urgent: { type: 'boolean', description: 'Interrupt that running sub-agent with it now' },
78 wake: { type: 'boolean', description: 'If nobody reads it in time, start someone to handle it' },
79 },
80 required: ['to', 'text'],
81 },
82 })
83 if (!task) $.clock.every(POLL_MS, () => void wake($)) // a conversation: a sub-agent's run ends with its turn
84 return next(e)
85 })
86
87 on('turn.start', ($, e, next) => {
88 my.busy = true
89 return next(e)
90 })
91
92 on('turn.complete', async ($, e, next) => {
93 if (e.agentId === undefined) {
94 my.busy = false
95 my.lastTurnEnd = await $.clock.now()
96 }
97 return next(e)
98 })
99
100 // A message to a Claude whose person approves everything sent to it goes through `clodfarm msg`, which asks them;
101 // the farm's own errors never stop a message
102 on('session.send', async ($, e, next) => {
103 if (!my.flag) return next(e)
104 const gate = await farm($, 'send', { to: e.to }).catch((): Reply => ({}))
105 if (gate.ok === false) return { isDelivered: false, reason: gate.why || 'the farm turned it away' }
106 const sent = await next(e)
107 if (sent.isDelivered) await farm($, 'sent', { to: e.to, text: e.text }).catch(() => undefined)
108 return sent
109 })
110 .catch(($, e, next) => next(e))
111
112 on('tool.call', { tool: 'mcp__clodfarm__msg' }, async ($, e) => {
113 const { to, text, urgent, wake } = e as MsgInput
114 if (typeof to !== 'string' || typeof text !== 'string') return { deny: 'msg takes `to` and `text`' }
115 const run = await $.process.run(
116 ['clodfarm', 'msg', ...(urgent === true ? ['--urgent'] : []), ...(wake === true ? ['--wake'] : []), '--', to, '-'],
117 { stdin: text, timeoutMs: 30_000 })
118 const out = (run.stdout.trim() || run.stderr.trim())
119 return run.exitCode === 0 ? { result: out } : { deny: out || `clodfarm msg exited ${run.exitCode}` }
120 })
121}
122