- MDX 55.3%
- TypeScript 44%
- JavaScript 0.5%
- CSS 0.2%
| Filename | Latest commit message | Latest commit date |
|---|---|---|
| .agents/skills/opentui | ||
| apps | ||
| docs | ||
| packages | ||
| skills/workforce | ||
| .gitignore | ||
| .prettierignore | ||
| .prettierrc | ||
| CLAUDE.md | ||
| eslint.config.mjs | ||
| mise.toml | ||
| package.json | ||
| pnpm-lock.yaml | ||
| pnpm-workspace.yaml | ||
| README.md | ||
| skills-lock.json | ||
| Taskfile.yml | ||
| tsconfig.json | ||
| vitest.config.ts | ||
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.
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
escalationparks 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.
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/doneactions are replaced byrequest_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
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.


