A software team: plan, build modules in parallel, integrate, fix
Halo runs a small team on a real project. An architect splits a spec into modules and shared types; coders write the modules in parallel, each looping until its file loads and has the exports the plan names; an integrator wires them together; and while the acceptance tests fail, a fixer works like an engineer: it runs the tests, reads files, edits them, and runs the tests again, choosing its own steps. Its tools are fenced in by policy: files only inside the project, never the tests, and the shell only for bun test. The tests decide when it's done. The project folder holds spec.md and acceptance.test.ts. Try the spreadsheet engine in examples/projects/spreadsheet (copy it first: this writes files there and runs model-written code, so use a sandbox).
cp -r examples/projects/spreadsheet /tmp/sheet
halo run examples/74-software-team.halo --arg workdir=/tmp/sheet// ============================================================
// A software team: plan, build modules in parallel, integrate, fix
// ============================================================
// Halo runs a small team on a real project. An architect splits a spec
// into modules and shared types; coders write the modules in parallel,
// each looping until its file loads and has the exports the plan names;
// an integrator wires them together; and while the acceptance tests fail,
// a fixer works like an engineer: it runs the tests, reads files, edits
// them, and runs the tests again, choosing its own steps. Its tools are
// fenced in by policy: files only inside the project, never the tests, and
// the shell only for `bun test`. The tests decide when it's done.
//
// The project folder holds spec.md and acceptance.test.ts. Try the
// spreadsheet engine in examples/projects/spreadsheet (copy it first:
// this writes files there and runs model-written code, so use a sandbox).
//
// RUN: cp -r examples/projects/spreadsheet /tmp/sheet
// halo run examples/74-software-team.halo --arg workdir=/tmp/sheet
// ============================================================
policy Team : Block(DestructiveOps) & Block(APIKey) & MaxSpend(3.00)
policy Json : Enforce(JSON)
// What the fixer may do with its tools: edit sources, run the tests. Nothing else.
policy FixerTools : AllowTools("Code.*", "Tests.run")
& DenyTools("Code.delete")
& DenyArgs("Code.write", "path", "\\.test\\.")
& DenyArgs("Code.edit", "path", "\\.test\\.")
& DenyArgs("Tests.run", "command", "^(?!bun test[\\w ./-]*$)")
tool Shell : Bash()
tool Code : Workspace("{{workdir}}", write: true)
tool Tests : Bash(cwd: "{{workdir}}", timeout_ms: 120000)
agent Architect [anthropic/claude-sonnet-4-5, temp: 0.2, max_tokens: 8000]
system """
You are the architect of a small TypeScript project for Bun. Split the
spec into 2 to 5 modules that coders will write in parallel, without
seeing each other's code. Modules must not import each other: they may
import only from "./types", a file you write now with every shared type
(tokens, AST nodes, values, errors). index.ts, written later, wires the
modules together and implements the public API from the spec.
Reply with only JSON:
{
"types": "<the full contents of types.ts>",
"modules": [
{ "file": "name.ts", "exports": ["fnName"],
"purpose": "what it does, which spec rules it owns",
"interface": "the exact TypeScript signature of each export, and how it behaves" }
]
}
"""
requires Team, Json
agent Coder [anthropic/claude-sonnet-4-5, temp: 0.0, max_tokens: 8000]
system """
You are a careful TypeScript engineer on a Bun project. Write the complete
contents of one file: no Markdown fences, no commentary, no placeholders.
Import shared types only from "./types" and never from other modules.
Follow the spec exactly. When an error is shown, fix the root cause.
"""
requires Team
agent Integrator [anthropic/claude-sonnet-4-5, temp: 0.0, max_tokens: 8000]
system """
You write index.ts for a Bun project: it implements the public API in the
spec using the project's modules, whose sources you are given. Reply with
only the file's contents: no Markdown fences, no commentary.
"""
requires Team
agent Fixer [anthropic/claude-sonnet-4-5, temp: 0.0, max_tokens: 8000, max_steps: 40]
system """
You fix a TypeScript project until its acceptance tests pass. Run them with
Tests.run (command: "bun test acceptance.test.ts"). Pick a failing test,
read the code it calls, and trace it to the line that goes wrong before
you edit anything. Prefer small edits (Code.edit) to rewriting files. Run
the tests after each fix. Never change the tests and don't special-case
test inputs. When they all pass, say in a few lines what was wrong.
"""
equips Code, Tests
requires Team, FixerTools
// One module: write it, then check it loads and has its exports.
pipeline BuildModule(m: JSON, workdir: String, spec: String, types: String) -> Result:
m
|> Coder.implement(
"Write {{m.file}}. Purpose: {{m.purpose}}. Interface: {{m.interface}}",
spec, types
) as code
|> Shell.run(command: """
set -o pipefail
cd {{workdir}}
case {{m.file}} in */*|.*|*.test.ts) echo "bad file name"; exit 9;; esac
printf '%s\n' {{code}} | sed '/^```/d' > {{m.file}}
bun -e 'const m = await import(process.argv[1]); const missing = JSON.parse(process.argv[2]).filter((n) => !(n in m)); if (missing.length) { console.error("missing exports: " + missing.join(", ")); process.exit(1) }' "$PWD"/{{m.file}} {{m.exports}} 2>&1 | tail -30
""") as check
|<> loop(Coder, "context.data.check.exitCode != 0", max: 3)
|> System.Value(m.file)
// The fixer runs the tests, reads and edits files, and reruns them, until
// they pass. Then the pipeline checks for itself, and sends it back if not.
pipeline FixUntilGreen(workdir: String, spec: String) -> Result:
workdir
|> Fixer.fix("Make the acceptance tests pass. The spec:", spec, tools: [Code, Tests]) as report
|> Tests.run(command: "bun test acceptance.test.ts 2>&1 | tail -40") as result
|<> loop(Fixer, "context.data.result.exitCode != 0", max: 3)
|> System.Value(context.data.report)
@correct(2)
pipeline BuildProject(workdir: String) -> Result:
workdir
|> Shell.run(command: "cat {{workdir}}/spec.md") as spec
|? (context.data.spec.exitCode == 0)
// 1. Plan: modules, their interfaces, and the shared types
|> Architect.plan("Plan this project.", context.data.spec.stdout) as plan
|> Shell.run(command: "cd {{workdir}} && printf '%s\n' {{plan.types}} > types.ts") as types
// 2. Build every module at once
|* each(m in context.data.plan.modules, concurrency: 5) => BuildModule(m, workdir, context.data.spec.stdout, context.data.plan.types) as modules
// 3. Wire them together, and run the acceptance tests
|> Shell.run(command: """
cd {{workdir}}
for f in *.ts; do case "$f" in *.test.ts) ;; *) printf '\n--- %s\n' "$f"; cat "$f";; esac; done
""") as sources
|> Integrator.wire("Write index.ts.", context.data.spec.stdout, context.data.sources.stdout) as index
|> Shell.run(command: """
set -o pipefail
cd {{workdir}}
printf '%s\n' {{index}} | sed '/^```/d' > index.ts
bun test acceptance.test.ts 2>&1 | tail -80
code=$?
for f in *.ts; do case "$f" in *.test.ts) ;; *) printf '\n--- %s\n' "$f"; cat "$f";; esac; done
exit $code
""") as result
// 4. Failing? The fixer patches until they pass
|? { (context.data.result.exitCode != 0) => FixUntilGreen(workdir, context.data.spec.stdout) } as fixes
|> Shell.run(command: "cd {{workdir}} && bun test acceptance.test.ts 2>&1 | tail -4 && wc -l *.ts | tail -1") as final
|> System.Value(context.data.final.stdout)