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

gala_team in action

A user prompt enters the Team Lead's conversation. The Team Lead can emit orchestration directives in its assistant text:

DirectiveEffect
@dispatch(<member>) … @endHand a task to a teammate (Engineer / QA).
@consult(<key>) … @endAsk another team (in-project sibling or cross-repo).
@summary … @endFinal 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)
  • claude CLI on PATH (this is what gala_team spawns)
  • gh CLI on PATH (for gh pr create; merging is done by you on GitHub)
  • A clone of martianoff/gala and martianoff/gala_tui as siblings of this repo, or the modules pinned in MODULE.bazel.

Build:

bazel build //cmd:gala_team

The binary lands at bazel-bin/cmd/gala_team_/gala_team.


Quickstart

  1. Define your team in team.yaml at 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
    
  2. Run in your project directory:

    gala_team --project . --team team.yaml
    
  3. Type a prompt, press Enter. Watch the team work.

  4. When the lead emits @summary, the approval dialog opens. Approve it to fire gh 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

KeyEffect
EnterSend composer prompt to the lead. In history mode: open the cursored archive. When sidebar-focused on a member row: open the member-detail modal.
BackspaceDelete last rune from composer.
Tab / Ctrl+LToggle 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 / PgDnScroll the conversation pane.
EndRe-stick the conversation pane to the bottom (latest line).
SpaceWhen sidebar-focused, on a group header: fold / unfold the role group.
Ctrl+POpen the command palette (fuzzy-search actions: erase session, yank, history, member detail, quit).
Ctrl+VPaste from the system clipboard into the composer.
Ctrl+YCopy (yank) the entire conversation to the system clipboard.
Ctrl+TToggle the consult viewer modal (live tail of every active consult).
Ctrl+AApprove the lead's @summary → run pre-merge hooks → gh pr create.
Ctrl+RReject the summary → back to live conversation.
Ctrl+NErase current session (two-press confirmation).
Ctrl+HToggle history browser.
F2 / F3 / F4Switch the layout: Focus mode (no sidebar) / Grid (member cards) / Pipeline (full-screen org chart). Same key again returns to the default split.
EscCancels in priority order: quit confirmation, recovery banner, member-detail modal, palette, sidebar focus, history detail.
Ctrl+QQuit gracefully (twice to confirm). Closes every live claude subprocess first.
Ctrl+CForce-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:

FieldWhere in yamlAlways sent?
Member name + roleinferred from members[].name / members[].roleyes
Team nameteams[].nameyes
Personalitymembers[].personalitywhen non-empty
Extra instructionsmembers[].extra_instructionswhen non-empty
Team-wide onboarding docsteams[].onboarding (file paths)when non-empty
Per-member onboarding docsmembers[].onboarding (file paths)when non-empty
The promptcomposer text / @consult body / @dispatch bodyyes

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 named gala_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:

PathOwns
app/teamdomain types (Team, Member, Role, Status)
app/configteam.yaml parser
app/policypolicy: block + hook runner
app/projectrepo resolution + git checks
app/runtimeclaude subprocess lifecycle (spawn / read / send / close)
app/headlessone-shot non-interactive driver
app/onboardingonboarding-doc loader + prompt wrapper
app/consultcross-team consult registry, runner, streaming pump
app/sessionconversation log persistence + history archiving
app/directive@dispatch / @consult / @summary parser
app/fsmpure orchestration state machine
app/uiElm model / update / view
app/viewreusable widgets (team panel, pipeline view)
cmd/binaries: gala_team (TUI), gala_team_headless

Run the test suite:

bazel test //app/...