Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@ curl -fsSL https://raw.githubusercontent.com/AgentSystemLabs/agent-office/main/i
- **GitHub on the walls.** Issues and pull requests hang on cork boards. Hand an issue to a worker, queue tasks, give a worker its own git worktree and open its PR with one key (if one gets deleted behind the office's back, the worker waits at its desk until you rebuild it). One task can span several projects: the worker gets a worktree of each, and a PR in each that links the others.
- **Agents that manage agents.** Every worker can list, hire, message and send home the others, through an `agent-office` MCP server (Claude Code, Codex, OpenCode) or the `office-workers` command. Ask one to "send everyone whose PR merged home" and it does, deleting their worktrees and branches unless they hold unpushed work. A worker that opens its pull request itself (`gh pr create`) shows it at its desk, and one the office missed can be told which is its own (`office-workers pr`).
- **Together.** Voice, chat, screen sharing on the lounge TV and a shared whiteboard.
- **Subagents and visitors.** A Claude Code worker's subagents (the Agent tool, a workflow's agents) stand round its chair while they run, each with what it's on. Claude Code sessions the office didn't start (the Claude desktop app, a terminal, an IDE) wait in the lobby by the elevator of the floor whose repository they work in, with their own subagents round them, only to look at: the office reads their transcripts under `~/.claude/projects` and never writes to them or drives them (`--no-visitors` turns them off).

- **Other maps.** Turn the whole building into a castle: sit on a throne of iron blades while your workers line up before you when they're done, send new ones off through the Hand of the King, and watch their beards grow long and grey as they toil. Send one home and the Kingsguard runs up from the dungeon, marches it down the stairs and throws it in a cell, where it starves, dies and rots down to a skeleton. Or into a space station in orbit, the Earth turning outside its windows: you run it from the captain's chair on the bridge, and a worker sent home is marched to the airlock and blown out into space, to drift off past the observation windows with everyone who went before it. Or make a map of your own, with its own way of seeing workers off in JSON ([docs/maps.md](docs/maps.md)).

Expand Down
1 change: 1 addition & 0 deletions docs/configuration.md
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ agent-office [dir] [options]
--city <name> Put the office in a real city: its sun and live weather (open-meteo.com)
--weather <kind> Pin the weather: clear, cloudy, rain, storm, snow or fog
--real-time-sky Start the sky on the real clock, not a day an hour (env AGENT_OFFICE_SKY_CLOCK=real; ⚙️ Settings can switch it)
--no-visitors Don't show the Claude Code sessions the office didn't start as visitors (env AGENT_OFFICE_VISITORS=0)

agent-office setup [--projects <dir>] [--project <owner/repo>]... [--home <dir>]

Expand Down
2 changes: 2 additions & 0 deletions docs/features.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,8 @@ Everything in the office, room by room. Back to the [README](../README.md).
- **Minesweeper on the boss's monitor.** In the boss's chair, **E** plays Minesweeper on the monitor: the camera moves in close and the board sits right on the screen. Click to dig, right-click (or Shift-click) to flag, and click a number to dig around it once its mines are flagged. The first click is never a mine. **Stop playing** leaves your game up on the monitor, with its clock paused, until you come back. Walk off to get up.
- **Pick your character.** The first time you join, a character select screen lets you choose your name, skin tone, hair style, hair color and shirt, with a spinning preview. No name in mind? Leave it blank (or skip the screen) and you go by the made-up one shown in the box, like *Sunny Otter*, or press **🎲** for another. The office remembers your name and look in that browser, so you only pick once. Change it any time from **⚙️** or by clicking your name under *In the office* (turn on **👥 People** in the **☰** menu).
- **Hire workers.** Walk up to an empty desk and press **E** to choose Claude Code, OpenCode, Codex, Grok, Muse, DeepSeek Harness, Pi or Cursor, or press **P** to write a task first. A little worker sits down, a laptop opens, and the agent's live screen appears on it. Choose the agent for each queue task too.
- **Subagents at the desk.** When a Claude Code worker hands part of its work to subagents (the Agent tool, or the agents of a workflow it runs), each one stands round its chair while it runs: a smaller worker without a headset, turned to its boss, with a card saying what kind it is (*🔎 EXPLORE*, *🤖 AGENT*, a workflow's phase), what it was asked to do and its latest tool call. It pops in at `SubagentStart` and goes at `SubagentStop`; past six round one desk, a sign says how many more there are. One stopped mid-turn (Esc) without a word goes after 15 minutes of silence, and a worker that's asleep has none.
- **Visitors.** Claude Code sessions the office didn't start, in the Claude desktop app, a terminal or an IDE, show up as visitors: they wait in the lobby between the elevator and the lounge on the floor whose repository they work in (by their folder, or their folder's `origin`), or on the building's first floor with a 🛎️ when no floor's repository is theirs. Each has a card with its name in the app (or its first prompt), its folder and branch, and its latest tool call, its light shows whether it's working, done or waiting on you there (then it hops), and its subagents and workflow agents stand round it. Aim at one for where it runs. They're only to look at: the office reads the transcripts Claude Code keeps under `~/.claude/projects` (`$CLAUDE_CONFIG_DIR/projects`) every few seconds while someone's in the office, never writes to them, and can't type into those sessions. A session leaves 10 minutes after its last write, unless its subagents are still at work. The office's own workers never show up twice. `--no-visitors` (or `AGENT_OFFICE_VISITORS=0`) turns them off.
- **Signs over the desks.** Press **L** at any desk, empty or not, to hang a big sign from the ceiling over it: *Operations*, *Code cleanup*, *Frontend*, whatever that desk is for. Type up to 32 characters (or pick one of the ideas), choose one of seven colors and press **Enter**. It hangs over the far edge of the desk, facing the chair, so it reads over the shoulder of whoever sits there; while the desk back to back with it has no sign of its own, it says the same on the back, so you can read it from across the room either way. Everyone on the floor sees it, the hint at the desk names it, and it stays up across restarts. **L** again changes it or takes it down. Each floor has its own.
- **Room to grow.** When a floor needs more desks, the north wall past the gong comes down. Walk up to the yellow **🚧 Room to grow** sign on it and press **E**, then **🔨 Knock through**: the wall goes, and a back office opens behind it with a pair of desks, a rug, a lamp and a window, in a puff of confetti. Knock through once more and it goes back another row, for two more desks (two rows is as far as it goes before the street behind the building). New workers, and the task queue, take its desks once the room's are full, before any bean bag comes out. The sign moves to the back wall, where **E** can also wall the last row back up, once nobody's working there. Each floor is built out on its own, and from outside you see it: the back office sticks out of that storey, on posts down to the lawn.
- **Bean bags for the overflow.** There are 16 desks, and up to 4 more in the back office. Once they're all taken, a bean bag comes out on the floor with a low lap desk in front of it. Hire a worker there the same way as at a desk, and the next bean bag comes out, up to 12 around the room. The task queue seats workers on them too. A bean bag stays out while a worker sits on it, and the empty one goes away again as soon as a desk frees up.
Expand Down
4 changes: 3 additions & 1 deletion docs/how-it-works.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,9 @@ browser ──HTTPS/WSS──▶ agent-office (Node)
└─ WebRTC signaling relay (voice + screen share are peer-to-peer)
```

- **Status.** Claude workers start with `--settings .agent-office/claude-hooks.json`. That file adds hooks (`UserPromptSubmit`, `PreToolUse`, `PostToolUse`, `PostToolUseFailure`, `PermissionRequest`, `Notification`, `Stop`, `SessionStart`) which `curl` a server bound to `127.0.0.1`. The hooks merge with your own Claude settings; they don't replace them. OSC 9;4 progress sequences in Claude terminals also count, which catches an Esc-cancel. OpenCode workers use a generated plugin to forward session, permission and question events to the same loopback server, with a separate endpoint. Codex uses a generated command helper and its own authenticated endpoint. The server reads only token records from the explicitly linked root rollout; approval settings are preserved. DeepSeek Harness workers report through the ACP session itself: `session/prompt` in flight is *working*, an outstanding `session/request_permission` is *needs input*, and a settled turn is *done*, with no hooks and no polling.
- **Status.** Claude workers start with `--settings .agent-office/claude-hooks.json`. That file adds hooks (`UserPromptSubmit`, `PreToolUse`, `PostToolUse`, `PostToolUseFailure`, `PermissionRequest`, `Notification`, `Stop`, `SessionStart`, `SubagentStart`, `SubagentStop`) which `curl` a server bound to `127.0.0.1`. The hooks merge with your own Claude settings; they don't replace them. OSC 9;4 progress sequences in Claude terminals also count, which catches an Esc-cancel. OpenCode workers use a generated plugin to forward session, permission and question events to the same loopback server, with a separate endpoint. Codex uses a generated command helper and its own authenticated endpoint. The server reads only token records from the explicitly linked root rollout; approval settings are preserved. DeepSeek Harness workers report through the ACP session itself: `session/prompt` in flight is *working*, an outstanding `session/request_permission` is *needs input*, and a settled turn is *done*, with no hooks and no polling.
- **Subagents.** `SubagentStart` adds a subagent to the worker (`WorkerInfo.subagents`, `src/server/subagents.ts`) and `SubagentStop` takes it off; a subagent's own hook events carry its `agent_id`, so its `PreToolUse` says what it's doing, and one the office only hears of that way (it started before the office did) is taken in then. What it was asked to do is read from the `agent-<id>.meta.json` Claude Code writes beside its transcript (`<session>/subagents/`, or a workflow run's folder under `subagents/workflows/`). A new session (`SessionStart`) has none, and one not heard from in 15 minutes is dropped. They aren't saved in `workers.json`. The page stands them round the worker's chair (`src/client/features/subagents/`). Their spend was already in the worker's: the cost reader takes in `<session>/subagents/*.jsonl` too.
- **Visitors.** `src/server/visitors/` looks over Claude Code's projects folder (`~/.claude/projects`, or `$CLAUDE_CONFIG_DIR/projects`) every 4 seconds while anyone is in the office. It stats each session's transcript (`<cwd with / as ->/<session>.jsonl`; ones untouched for 6 hours only once a minute), and reads only the first 64 KB (where it runs, its first prompt) and the last 256 KB (its title, its latest messages) of the ones written in the last 10 minutes or with a subagent written to in the last 3, again only when the file has changed. Status comes from the last message: a tool call is *working* (`AskUserQuestion` and `ExitPlanMode` are *waiting on you*), a finished turn or an Esc is *done*. Subagents and workflow agents are the `agent-*.jsonl` in `<session>/subagents/` (and `subagents/workflows/<run>/`) written in the last 3 minutes whose meta file doesn't say `finished`. Sessions whose id is, or ever was, one of the office's workers' are skipped, and so are ones in a floor's `.agent-office`. A visitor goes to the floor whose checkout its folder is in, else whose repository its folder's `origin` is (asked of `git` once every 10 minutes), else to the first floor, marked as in the lobby. Each floor gets a `visitors` message when its list changes, and its list is part of `FloorView`. Nothing under that folder is ever opened for writing, and nothing is sent to those sessions: the files are opened read-only and only stat'ed, listed and read.
- **Shared shells.** Press **B** at an empty desk to open a plain login shell for dev servers, git or tests. It's shared the same way as a Claude terminal.
- **Isolated branches.** When you hire with a task, you can tick *own git worktree*. The worker then gets its own `office/<name>` branch under `.agent-office/worktrees/`, so parallel workers never share a checkout. The branch starts from what the office's branch is on GitHub: just before cutting it (a hire, a queue task, a meeting), the office runs `git fetch origin <branch>` (`src/server/worktrees.ts`; a burst of hires shares one fetch) and branches from `origin/<branch>`, so pull requests merged since anyone pulled are in it. It branches from the local `HEAD` instead when that already has all of it (it's ahead with commits not pushed yet), or when there's no origin or the fetch fails. When both have moved on, it starts from origin's and a toast says how many local commits it left out. The project's own checkout is never moved, and the Changes window diffs against whichever of the two is newer. When you send that worker home you choose whether to keep the worktree and branch, delete the worktree, or delete both, and the office warns you first about uncommitted changes or commits no remote has. `agent-office prune` clears out whatever was kept, once it is safe. A worker whose folder (its worktree, or its workspace) is gone is never started there: `checkLost` in `src/server/workers/worktree.ts` looks before every start and every 10 seconds, and sets `WorkerInfo.lost` with where its branch still is (`here`, only on `origin`, or `gone`), with no toast. `worker.rebuild` (`all` for every lost worker on the floor) runs `git worktree prune` and `git worktree add` at the same path, on the branch, else `-b <branch>` from `origin/<branch>`, else from its base commit (`Worktrees.restore`), rewrites a workspace's brief, and restarts everyone who worked in that folder (a meeting's table too) with `--resume`; one whose process was still running in the deleted folder is stopped and resumed there. A folder put back by hand clears the mark by itself. With *Workers whose pull request merged* set to go home by themselves (`src/server/leave-on-merge.ts`, saved in `.agent-office/leave-on-merge.json`), each floor looks again whenever the PR list comes back, a queue task's PR is linked or a worker changes, and uses the same call as the purple bubble (`workerPr` in `src/shared/status.ts`). A worker goes with the office's usual check for work its worktree would lose, except that commits up to the merged PR's head count as delivered, even once GitHub has deleted the branch.
- **Pull requests.** **O** at a worktree worker's desk runs `git push -u origin office/<name>` in its worktree, then `gh pr create` with the task as the body and the commits listed. The base is the branch the office was on when the worktree was cut, if that branch is on the remote. The PR number is saved with the worker, so the board can point back at the desk.
Expand Down
93 changes: 93 additions & 0 deletions src/client/features/subagents/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
/**
* A worker's subagents (WorkerInfo.subagents, from its Claude Code hooks, see server/subagents.ts):
* each one stands behind its worker's chair while it runs, with its kind, what it was asked to do and
* its latest tool call on a card over its head, and goes when it's done. Past SHOWN of them, a sign
* says how many more there are.
*/
import * as THREE from 'three';
import { liveSubagents, type WorkerStatus } from '../../../shared/protocol';
import type { Ctx } from '../../core/context';
import { store } from '../../state';
import type { DeskView } from '../../world/types';
import { SubagentCrowd } from '../../world/character';

/** A worker whose process is running: one asleep or gone has nobody working for it. */
const RUNNING = new Set<WorkerStatus>(['starting', 'idle', 'working', 'needs_input', 'done']);
/** How many stand round a desk; the rest are a count. */
const SHOWN = 6;
/** How big they are next to a worker. */
const SIZE = 0.6;

/**
* Where the i-th stands, in the seat's own frame (the worker faces +z, its desk): behind the chair,
* the first two at its shoulders and the rest in a row further back, each turned to its worker.
*/
const SPOTS: readonly (readonly [number, number])[] = [
[-0.62, -0.7],
[0.62, -0.7],
[-1.15, -1.2],
[0, -1.3],
[1.15, -1.2],
[-0.6, -1.85],
[0.6, -1.85],
];
const spot = (i: number) => {
const [x, z] = SPOTS[Math.min(i, SPOTS.length - 1)];
return [x, z, Math.atan2(-x, -z)] as const;
};

interface Standing {
desk: DeskView;
holder: THREE.Object3D;
crowd: SubagentCrowd;
}

/** Follows the workers, and keeps each one's subagents round its desk (its tick: 'others'). */
export function installSubagents(ctx: Ctx) {
const standing = new Map<string, Standing>();

function drop(id: string) {
const s = standing.get(id);
if (!s) return;
s.crowd.dispose();
s.holder.removeFromParent();
standing.delete(id);
}

function sync() {
const world = ctx.world();
const now = store.officeNow();
for (const w of store.workers.values()) {
const list = RUNNING.has(w.status) ? liveSubagents(w.subagents, now) : [];
const desk = world.desks.get(w.deskId);
let s = standing.get(w.id);
if (s && (s.desk !== desk || !list.length)) {
drop(w.id);
s = undefined;
}
if (!desk || !list.length) continue;
if (!s) {
// On the floor under the seat, turned the way its worker faces.
const holder = new THREE.Object3D();
holder.position.set(desk.seatAnchor.position.x, 0, desk.seatAnchor.position.z);
holder.rotation.y = desk.seatAnchor.rotation.y;
desk.group.add(holder);
s = { desk, holder, crowd: new SubagentCrowd(holder, spot, SIZE) };
standing.set(w.id, s);
}
s.crowd.sync(list.slice(0, SHOWN), list.length - SHOWN);
}
for (const id of standing.keys()) if (!store.workers.has(id)) drop(id);
}

store.on('workers', sync);
/** When (performance.now()) the subagents were last looked over for any gone quiet without a word. */
let checkedAt = 0;
ctx.ticks.add('others', ({ dt, t, now }) => {
if (now - checkedAt > 30_000) {
checkedAt = now;
if (standing.size) sync();
}
for (const s of standing.values()) s.crowd.update(dt, t);
});
}
Loading