No description
  • MDX 55.3%
  • TypeScript 44%
  • JavaScript 0.5%
  • CSS 0.2%
Find a file
Repository files (latest commit first)
Filename Latest commit message Latest commit date
Morten Olsen cac16bac68
updates
2026-09-02 15:01:56 +02:00
.agents/skills/opentui update 2026-09-01 13:30:07 +02:00
apps updated ui 2026-09-02 13:11:30 +02:00
docs updates 2026-09-02 15:01:56 +02:00
packages update 2026-09-01 13:30:07 +02:00
skills/workforce update 2026-09-01 13:30:07 +02:00
.gitignore update 2026-09-01 13:30:07 +02:00
.prettierignore update 2026-09-01 13:30:07 +02:00
.prettierrc init 2026-09-01 07:33:51 +02:00
CLAUDE.md updates 2026-09-02 15:01:56 +02:00
eslint.config.mjs update 2026-09-01 13:30:07 +02:00
mise.toml init 2026-09-01 07:33:51 +02:00
package.json update 2026-09-01 13:30:07 +02:00
pnpm-lock.yaml update 2026-09-01 13:30:07 +02:00
pnpm-workspace.yaml init 2026-09-01 07:33:51 +02:00
README.md readme 2026-09-01 14:03:45 +02:00
skills-lock.json init 2026-09-01 07:33:51 +02:00
Taskfile.yml updated ui 2026-09-02 13:11:30 +02:00
tsconfig.json update 2026-09-01 13:30:07 +02:00
vitest.config.ts init 2026-09-01 07:33:51 +02:00

Workforce

A self-hosted task board where the workers are coding agents.

You register a product (a git repository), hand it tasks, and a pool of ACP-speaking agents (Claude Code, omp, pi, …) works them: each task gets its own git worktree, moves through configurable lanes (triage, development, qa, …), and every transition is decided by an orchestrator agent that holds the task's full history. You step in when it escalates a question, when a review needs your approval — or not at all.

Workforce is the plumbing between "a coding agent in a terminal" and "a team of them working a backlog": isolation, routing, memory, review, and the moments where a human belongs in the loop.

The board — tasks grouped by who is waiting on whom

Screenshots show seeded demo data.

Why this exists

Coding agents are good at bounded work and bad at everything around it: picking up the next task, working without trampling your checkout, knowing what was already tried, handing off to a differently-prompted agent, stopping to ask instead of guessing, and landing finished work safely. Every team gluing agents together rebuilds that scaffolding.

Workforce is that scaffolding, built on a few opinions:

  • Structured agent output is a lie over ACP. There is no enforceable output schema, so agents never "return JSON" here — every structured exchange (hand-off, decision, question) is an MCP tool call, schema-validated at the protocol level. A run that ends without a hand-off is treated as an implicit question, not a parse error.
  • The conversation is the state. A task's durable record is one orchestrator-centric message log. Message kinds drive status (an escalation parks the task for you; your reply re-opens it). The orchestrator has no long-lived process — each decision replays the log, so a server restart loses nothing.
  • Push summaries, pull detail. Lane agents get a briefing, not the raw log; orchestrators get the log, not agent transcripts. Heavy artifacts live on run records and in the worktree, one pull away. This is also what keeps layered orchestration (task ↔ product) from drowning in context.
  • Agents never commit as you. Worktrees carry a per-task service identity; your repo's config is never touched. Landing work is gated: by default a merge requires a review approval at the exact branch head.

It is open source because it is useful plumbing, not a product. No pricing page follows.

What it is not (yet, or ever)

  • Single-user, no auth. It assumes one operator on a trusted machine.
  • SQLite. Fine for one server process; Postgres would be a mechanical swap (Kysely), but nobody has needed it.
  • No forge adapters. PR-based delivery drives gh/tea/… through the agent's own shell, using your ambient credentials and free-text "delivery notes" — deliberately, so any forge works.
  • Agents auto-approve their own tool permissions. Isolation comes from the worktree and the review gate, not from sandboxing the agent process. Treat lane agents as having shell access to the worktree and act accordingly.
  • Early. The web app is the flagship surface; the TUI is a proof of concept awaiting a re-port of the web UX.

How it works

The shape of the system

Every surface talks to one Fastify server over HTTP (+ SSE for live updates); the OpenAPI document at /api/docs is the contract the client package is generated from.

flowchart LR
    subgraph surfaces [Surfaces]
        web[Web app]
        cli[CLI]
        tui[TUI]
    end
    client["@workforce/client<br/>(generated from OpenAPI)"]
    subgraph server [Server]
        api[HTTP API + SSE]
        worker[Worker<br/>autonomy loop]
        dispatcher[Dispatcher<br/>per-task loop]
        prodorch[Product orchestrator<br/>per-product thread]
        runner[AgentRunner<br/>generic ACP seam]
        worktree[WorktreeService]
        db[(SQLite)]
    end
    agents["ACP agents<br/>claude-code / omp / pi …"]
    repo[(Your git repo<br/>+ per-task worktrees)]

    web & cli & tui --> client --> api
    api --> dispatcher
    worker --> dispatcher
    dispatcher --> prodorch
    dispatcher --> runner
    prodorch --> runner
    runner <-->|stdio + per-run MCP tools| agents
    dispatcher --> worktree --> repo
    api --> db

The domain is small: a product wraps a git repo, projects are an optional human grouping, tasks are the unit of work. Lanes and agents are editable configuration (database-backed, with workforce.json as a merged fallback) — a lane binds an agent definition to a role via its system prompt, optionally scoped to one product.

One task's life

The worker picks up any open task the moment it appears (with a periodic sweep as crash recovery). The dispatcher then loops: run the current lane's agent in the task's worktree, then ask the orchestrator what happens next. The lane agent's hand-off is a proposal, not a rail.

sequenceDiagram
    participant W as Worker
    participant D as Dispatcher
    participant L as Lane agent
    participant O as Task orchestrator

    W->>D: process(task)
    loop until the task leaves "open" (cap: 20 visits)
        D->>L: run in worktree (briefing + task card)
        L-->>D: handoff(summary) — an MCP tool call
        Note over L,O: mid-run, the lane agent may call<br/>ask_orchestrator(question) and continue
        D->>O: decide (replayed message log + hand-off)
        O-->>D: route / split / merge / done / escalate / …
        D->>D: apply (move lane, create subtasks, merge branch, …)
    end

The orchestrator's decision vocabulary:

  • route — move to a lane (or back to the same one) with a briefing for the receiving agent
  • split — break into subtasks; the parent waits, and when the last child finishes, their outcomes land in the parent's thread and it decides again
  • merge — land the task branch into the default branch and finish (merge-strategy products)
  • await_pr — park on the remote pull request; a periodic read-only check reports its state and the orchestrator decides on each report (pr-strategy products)
  • done — finish without anything to land
  • escalate — only the user can resolve this; the task parks in your inbox

Statuses fall out of those decisions:

stateDiagram-v2
    [*] --> open
    open --> open: route
    open --> waiting: split
    open --> waiting_pr: await_pr
    open --> awaiting_product: request to product orchestrator
    open --> awaiting_input: escalate
    open --> done: merge / done / accepted
    waiting --> open: all subtasks done, routed back
    waiting --> done: all subtasks done, finished
    waiting_pr --> open: PR feedback
    waiting_pr --> done: PR merged (via completion)
    awaiting_product --> open: reply / rejection
    awaiting_product --> done: completion accepted
    awaiting_input --> open: your reply
    done --> [*]

Runs, messages, and what survives a crash

Every agent invocation is a run row (prompt, output, cost-relevant audit, plus a persisted transcript of the agent's inner dialog). The durable conversation is separate: messages with typed kinds (handoff, question, decision, escalation, …). Runs interrupted by a server restart are marked failed at boot, and the task is re-driven from the message log — the log, not the process, is the memory.

Everything is observable live: domain events (each carrying the fresh object) and per-chunk agent dialog stream over SSE at GET /api/events; the web board's status dots only breathe while an agent is actually running.

A task page — the orchestrator's conversation with the runs ledger alongside

The product orchestrator (opt-in)

Per product you can enable a second orchestration layer: a product-level orchestrator with one continuous conversation per product that every task start, completion, question, and user exchange lands in — so it accumulates product knowledge over time (thread compression is deliberately not built yet). Its runs are serialized per product; the thread is one conversation by construction.

With it enabled, tasks no longer finish themselves:

sequenceDiagram
    participant U as You
    participant P as Product orchestrator
    participant D as Dispatcher
    participant T as Task orchestrator

    U->>P: "Add usage-based billing" (product thread)
    P->>P: create_task(title, briefing)
    Note over P,T: the briefing (conventions, related work,<br/>pitfalls) rides into every lane run
    D->>T: …lanes run as usual…
    T->>P: request_completion(summary)
    alt accept
        P->>D: accept — landing IS the acceptance
        D->>D: merge branch (review gate applies), task done
    else reject
        P->>T: feedback re-opens the task
        T->>D: route back to a work lane
    end
    P-->>U: reply in the product thread

Concretely, for opted-in products:

  • every new top-level task is briefed by the product orchestrator before its first lane run (tasks it creates itself arrive pre-briefed)
  • the task orchestrator's merge/done actions are replaced by request_completion — the product-level accept is what lands the work, and a reject sends feedback back
  • a task orchestrator can ask the product a question or request a lane it is missing; the product orchestrator can create or adjust product-scoped lanes on the spot, or decline
  • you can write into the product thread ("create a task for…", "how do we deploy?") from the web product page or workforce product ask — the orchestrator acts (creates tasks, adjusts lanes) and replies

A product thread — briefings, completion verdicts, and the user's conversation in one place

Products without the flag behave exactly as before.

Worktrees and the review gate

Each task works on branch task/<id> in its own worktree (under ~/.workforce/worktrees by default) with a per-worktree service identity — commits are authored by the task, never by you, and your repo's own config is untouched. Products can define a setupScript (dependency install, env bootstrap) that runs in every fresh worktree.

Review is PR-style with the task as the PR: the diff of the task branch, viewed-marks that go stale when a file changes, and append-only notes anchored to {path, line, commit} that are re-mapped through git at read time (never fuzzily re-anchored). By default (requireReview), merging requires an approval at the exact branch head — new commits age approvals out automatically. Lane agents can leave and resolve anchored notes themselves.

Getting started

Prerequisites: mise (pins Node 26 and go-task); pnpm comes via corepack.

git clone <this repo> && cd workforce
mise install
task install

Configure at least one ACP agent and some lanes. Config is layered (/etc/workforce/~/.config/workforce/./workforce.json → env vars), or manage everything over the API/CLI (the database wins over file config):

// workforce.json
{
  "agents": {
    "claude-code": {
      "command": "env",
      "args": ["-u", "CLAUDECODE", "-u", "CLAUDE_CODE_ENTRYPOINT", "npx", "-y", "@zed-industries/claude-code-acp"],
    },
  },
  "lanes": {
    "triage": { "agent": "claude-code", "systemPrompt": "You triage tasks…" },
    "development": { "agent": "claude-code", "systemPrompt": "You implement…" },
  },
  "orchestrator": { "agent": "claude-code", "systemPrompt": "You route tasks…" },
  // optional — only used by products that enable it:
  "productOrchestrator": { "agent": "claude-code", "systemPrompt": "You oversee the product…" },
}

Then hand it work. The CLI auto-starts and reuses a managed background server on 127.0.0.1:9675 — you never boot anything by hand for local use:

task cli -- product add zap ~/projects/zap
task cli -- task add zap "Fix the login flow"   # the worker picks it up
task cli -- inbox                               # questions waiting on you
task cli -- task reply <ref> "Focus on SSO"     # answering re-opens the task
task cli -- watch                               # live event stream

With the product orchestrator:

task cli -- product update zap --orchestrator on
task cli -- product ask zap "Create a task to add rate limiting to the API"
task cli -- product thread zap

The web app is the primary surface: task dev:web (proxies /api to the managed server), then open the printed URL. workforce server status|start|stop|restart|logs manages the background server; a bare workforce in a TTY opens the TUI.

Development

task lint               # type-check + ESLint
task test               # vitest across all workspaces
task dev:server         # server in watch mode
task dev:web            # web app (Vite)
task storybook          # the design system, documented as a book
task generate:client    # regenerate client types after changing routes/schemas

Tests are API-level by design: each test boots the full app on in-memory SQLite and exercises real routes; agent behavior is scripted through a fake runner, so the whole orchestration loop is tested end to end without spawning real agents.

docs/architecture.md is the deep dive — the domain model, every module and its seam, the agent exchange protocol, and the open design questions. CLAUDE.md holds conventions and the gotchas that cost someone an afternoon.

License

Not chosen yet — if you want to build on this and the missing license blocks you, open an issue.