Skip to content

A software team: plan, build modules in parallel, integrate, fix ​

examples/74-software-team.halo tools: policies |<> loops |* parallel

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

Open in the playground →

bash
cp -r examples/projects/spreadsheet /tmp/sheet
halo run examples/74-software-team.halo --arg workdir=/tmp/sheet
halo
// ============================================================
// 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)

curl -fsSL https://halolang.com/install.sh | sh