gala_team
June 6, 2026 · View on GitHub
Multi-agent Claude CLI orchestrator -- a Team Lead delegates to Engineers and QAs, reviews their work, and hands you back a summary and a pull request for sign-off.
Written in GALA with the
gala_tui Elm-style TUI framework.
Status: alpha. Schema and key bindings may shift between commits.
What it does

A user prompt enters the Team Lead's conversation. The Team Lead can emit orchestration directives in its assistant text:
| Directive | Effect |
|---|---|
@dispatch(<member>) … @end | Hand a task to a teammate (Engineer / QA). |
@consult(<key>) … @end | Ask another team (in-project sibling or cross-repo). |
@summary … @end | Final answer + PR body. Triggers approval mode. |
Install
You need:
- Bazelisk (a wrapper around
bazel) - Go 1.25+ (Bazel will use the SDK declared in
MODULE.bazel) claudeCLI onPATH(this is what gala_team spawns)ghCLI onPATH(forgh pr create; merging is done by you on GitHub)- A clone of
martianoff/galaandmartianoff/gala_tuias siblings of this repo, or the modules pinned inMODULE.bazel.
Build:
bazel build //cmd:gala_team
The binary lands at bazel-bin/cmd/gala_team_/gala_team.
Quickstart
-
Define your team in
team.yamlat the project root. Smallest viable file:teams: - key: main name: Skunkworks description: Default product team members: - role: team_lead name: Iris personality: Calm, decisive. Asks for evidence. - role: engineer name: Felix personality: Functional-leaning, terse. - role: qa name: Theo personality: Evidence-driven. Asks for tests. workflow: qa_required: true -
Run in your project directory:
gala_team --project . --team team.yaml -
Type a prompt, press
Enter. Watch the team work. -
When the lead emits
@summary, the approval dialog opens. Approve it to firegh pr create. Review and merge the PR on GitHub — the app never merges; it stops at "PR created".
team.yaml schema (full reference)
Every field, with defaults and acceptable values. Anything not listed here is ignored by the parser.
# ─── teams ────────────────────────────────────────────────────────────────
# Required. List of one or more teams. The first entry is the "main" team
# unless --team-key picks another. Sibling entries are addressable from the
# main team's lead via @consult(<key>).
teams:
- key: main # required if multi-team. Used by @consult / --team-key.
# Falls back to a slug of `name` when omitted.
# Must be unique across the teams list.
name: "Skunkworks" # required. Display name shown in the TUI header.
description: "Product team" # optional. One-line subtitle shown next to `name`.
dangerously_skip_permissions: false # optional. Default false. When true, every
# spawned `claude` for this team is invoked with
# `--dangerously-skip-permissions`. Use ONLY when
# you've already granted the parent permissions
# and want sub-agents to inherit them without
# re-prompting. Each team is a separate decision
# — a sibling consult team's flag is independent.
onboarding: # optional. List of file paths (relative to --project,
- docs/CONTRIBUTING.md # absolute paths also accepted) read once and prepended
- docs/ARCHITECTURE.md # to every member's first prompt. Subsequent prompts
# don't re-send.
members: # required. ≥1 entry; exactly one role: team_lead;
# ≥1 role: engineer; role: qa needed when
# workflow.qa_required is true.
- role: team_lead # required. one of: team_lead | engineer | qa
name: "Iris" # required. Unique within the team.
personality: | # optional. Free-form text fed into the system prompt.
Calm, decisive. Asks clarifying questions before delegating.
model: claude-opus-4-7 # optional. Pins the Claude model. Omit to use claude-cli's
# default. Passed as `--model <name>` to claude.
extra_instructions: | # optional. Appended to the member's system prompt
Always cite line numbers. # below the personality block. Free-form text.
onboarding: # optional. Per-member paths, in addition to the
- docs/lead-handbook.md # team-wide onboarding above.
- role: engineer
name: "Felix"
personality: "Terse, functional-leaning."
- role: qa
name: "Theo"
personality: "Evidence-driven."
# Sibling team — addressable as @consult(transpiler) from `main`'s lead.
- key: transpiler
name: "Transpiler Wizards"
description: "Compiler / language work"
members:
- role: team_lead
name: "Theo"
personality: "Precise. Cites specs."
- role: engineer
name: "Cade"
personality: "Pragmatic."
# ─── workflow ─────────────────────────────────────────────────────────────
# How a team operates internally. All keys optional; defaults shown.
workflow:
qa_required: true # default true. When true, every team must declare ≥1
# qa member; the orchestration FSM gates approval on
# QADone. Set false for engineer-only teams.
parallel_engineers: true # default true. When true, multiple @dispatch directives
# in one TL message run engineers concurrently. When
# false, the FSM serialises them one at a time.
post_engineer_prompt: "/simplify" # optional. Default "". Re-sent to each engineer in-session
# after they finish + commit, before QA.
approval:
require_user_confirm: true # default true. Final @summary opens the Approval banner
# and waits for Ctrl+A. Set false to auto-approve
# (CI-style headless flows).
# ─── policy ───────────────────────────────────────────────────────────────
# Per-project orchestration rules. ALL keys optional; defaults shown.
# IMPORTANT: `policy:` is a top-level sibling of `workflow:` — NOT nested inside it.
policy:
workspace_mode: shared # default `shared`. Possible values:
# shared — engineers/QAs run in --project
# worktree-per-engineer — engineers/QAs get their own
# git worktree under
# .gala_team/worktrees/<name>
# on branch gala_team/<name>
# NOTE: the team lead ALWAYS runs in
# .gala_team/worktrees/_lead, regardless of mode — every
# orchestrator git op (lead-branch reset, snapshot push,
# gh pr create) targets that worktree, never the user's
# main checkout.
merge_rule: squash # default `squash`. squash | rebase | merge.
# Informational only: the app never runs
# `gh pr merge`. Surfaced in the PR-created
# log line so you know which strategy to
# pick when you merge on GitHub.
pre_merge: # default `[]`. List of hooks that must succeed before
# `gh pr create` runs. Each hook spawns a subprocess in
# the project repo's cwd. First non-zero exit blocks the PR.
- name: lint # required. Used in the conversation log.
cmd: go # required. Executable to run.
args: [vet, ./...] # optional. Default `[]` (no args).
- name: test
cmd: go
args: [test, ./...]
post_merge: # default `[]`. Parsed but INERT: the app no
# longer merges (merging is done by you on
# GitHub), so there is no merge event to run
# these after. Kept for forward-compat /
# documentation; runs nothing today.
- name: notify
cmd: scripts/notify-slack.sh
args: []
Picking the main team
If teams: has more than one entry, pick which one drives:
gala_team --team team.yaml --team-key transpiler
The default is the first team in source order. If --team-key doesn't match any
team in the file, the binary exits with the list of valid keys.
Cross-team @consult
There are two ways to make another team available to the lead:
1. In-project (same yaml)
Define multiple teams in team.yaml. The lead can @consult(<key>) … @end
to hand off to a sibling — same repo, no extra setup.
2. Cross-repo registry
Drop a .gala_team/consults.yaml next to your project:
consults:
- name: transpiler
repo: /work/gala_simple
team: /work/gala_simple/team.yaml
- name: qa-lib
repo: ../qa-lib
team: ../qa-lib/team.yaml
@consult(transpiler) now spawns a child gala_team against the target
repo's team.yaml. The child team's stdout streams back live; you can watch
it with Ctrl+T (consult viewer modal).
When the in-project schema and the cross-repo registry both define <name>,
the in-project entry wins.
Keyboard
| Key | Effect |
|---|---|
Enter | Send composer prompt to the lead. In history mode: open the cursored archive. When sidebar-focused on a member row: open the member-detail modal. |
Backspace | Delete last rune from composer. |
Tab / Ctrl+L | Toggle focus between composer and the team sidebar. |
↑ / ↓ | When sidebar-focused: move row cursor. In history mode: history cursor. Inside the command palette: navigate matches. |
PgUp / PgDn | Scroll the conversation pane. |
End | Re-stick the conversation pane to the bottom (latest line). |
Space | When sidebar-focused, on a group header: fold / unfold the role group. |
Ctrl+P | Open the command palette (fuzzy-search actions: erase session, yank, history, member detail, quit). |
Ctrl+V | Paste from the system clipboard into the composer. |
Ctrl+Y | Copy (yank) the entire conversation to the system clipboard. |
Ctrl+T | Toggle the consult viewer modal (live tail of every active consult). |
Ctrl+A | Approve the lead's @summary → run pre-merge hooks → gh pr create. |
Ctrl+R | Reject the summary → back to live conversation. |
Ctrl+N | Erase current session (two-press confirmation). |
Ctrl+H | Toggle history browser. |
F2 / F3 / F4 | Switch the layout: Focus mode (no sidebar) / Grid (member cards) / Pipeline (full-screen org chart). Same key again returns to the default split. |
Esc | Cancels in priority order: quit confirmation, recovery banner, member-detail modal, palette, sidebar focus, history detail. |
Ctrl+Q | Quit gracefully (twice to confirm). Closes every live claude subprocess first. |
Ctrl+C | Force-kill backstop owned by gala-tui's runtime; bypasses the quit confirmation. |
Onboarding & first-prompt payload
The very first message sent to a freshly-spawned claude subprocess is
assembled from four sources:
# Your role
You are <Member.Name>, the <role> on team <Team.Name>.
## Personality
<Member.personality>
## Additional instructions
<Member.extra_instructions>
# Project onboarding ← omitted if no onboarding paths
--- docs/CONTRIBUTING.md ---
<file contents>
--- docs/lead-handbook.md ---
<file contents>
# Your task
<the prompt the user typed, or the @consult body, or the @dispatch body>
Sources, in order they're stitched:
| Field | Where in yaml | Always sent? |
|---|---|---|
| Member name + role | inferred from members[].name / members[].role | yes |
| Team name | teams[].name | yes |
| Personality | members[].personality | when non-empty |
| Extra instructions | members[].extra_instructions | when non-empty |
| Team-wide onboarding docs | teams[].onboarding (file paths) | when non-empty |
| Per-member onboarding docs | members[].onboarding (file paths) | when non-empty |
| The prompt | composer text / @consult body / @dispatch body | yes |
Subsequent prompts in the same session do not re-send any of this — the model already has the role + onboarding in its context window. Only the bare prompt goes through.
teams:
- key: main
name: Skunkworks
onboarding: # team-wide — every member reads these
- docs/CONTRIBUTING.md
- docs/ARCHITECTURE.md
members:
- role: team_lead
name: Iris
personality: |
Calm, decisive. Asks clarifying questions before delegating.
extra_instructions: |
Always cite line numbers when referencing code.
onboarding: # additionally for the lead
- docs/lead-handbook.md
Session history (Ctrl+H)
Every PR opened (gh pr create) archives the conversation log to
.gala_team/sessions/archive/pr-created-<savedAt>.json — PR creation is
the app's completion point (merging happens later, by you, on GitHub).
Press Ctrl+H to browse:
History · ↑↓ navigate · Enter open · Ctrl+H close
▶ 2026-04-28 14:32 · pr-created · myproject (47 lines)
2026-04-26 09:11 · pr-created · myproject (62 lines)
2026-04-25 16:48 · pr-created · myproject (38 lines)
Enter loads the selected archive in read-only view; Esc returns to the
index.
Workspace modes
policy.workspace_mode decides where engineers and QAs run:
shared(default) — engineers/QAs run in the project repo. Simpler, fastest, fine for chat-based dispatch.worktree-per-engineer— engineers/QAs each get a private git worktree at<repo>/.gala_team/worktrees/<member>on a branch namedgala_team/<project>/<member>. They can commit independently without fighting over the working tree.
The team lead is special: it ALWAYS runs in
<repo>/.gala_team/worktrees/_lead on branch gala_team/<project>/lead,
regardless of workspace_mode. Every orchestrator git op
(lead-branch reset on Start fresh, per-PR snapshot push, gh pr create)
targets that dedicated worktree — the user's main
checkout is never mutated by orchestrator code. workspace_mode
controls only the engineer/QA isolation story, not the TL's safety.
Headless mode
For non-interactive use:
bazel run //cmd/gala_team_headless -- \
--project /work/myproject \
--team /work/myproject/team.yaml \
--prompt "ship the metrics endpoint"
Returns a JSON object with the captured conversation, summary, and any
non-fatal errors. Used internally by the @consult registry path.
Layout on disk
<project>/
├── team.yaml # team definition (this file)
└── .gala_team/
├── consults.yaml # cross-repo consult registry (optional)
├── sessions/
│ ├── latest.json # current session — restored on launch
│ └── archive/
│ └── pr-created-<savedAt>.json # one per PR opened
└── worktrees/
├── _lead/ # team lead's worktree (always; per-project branch)
├── Felix/ # only when workspace_mode = worktree-per-engineer
└── Theo/ # ditto
Contributing
The codebase is laid out under app/ by responsibility:
| Path | Owns |
|---|---|
app/team | domain types (Team, Member, Role, Status) |
app/config | team.yaml parser |
app/policy | policy: block + hook runner |
app/project | repo resolution + git checks |
app/runtime | claude subprocess lifecycle (spawn / read / send / close) |
app/headless | one-shot non-interactive driver |
app/onboarding | onboarding-doc loader + prompt wrapper |
app/consult | cross-team consult registry, runner, streaming pump |
app/session | conversation log persistence + history archiving |
app/directive | @dispatch / @consult / @summary parser |
app/fsm | pure orchestration state machine |
app/ui | Elm model / update / view |
app/view | reusable widgets (team panel, pipeline view) |
cmd/ | binaries: gala_team (TUI), gala_team_headless |
Run the test suite:
bazel test //app/...