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
2 changes: 1 addition & 1 deletion docs/agents.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

Back to the [README](../README.md).

Every new worker starts on the office's **Default worker**: a provider, model and effort an admin picks in ⚙️ Settings → **🤖 Workers** (the `--agent` with its own default model until someone does). The hire, ask, queue, issue and meeting windows show it on one line; click **✏️ Edit** there to pick **Claude Code**, **OpenCode**, **Codex**, **Grok**, **Muse**, **DeepSeek Harness**, **Pi** or **Cursor**, a model and an effort for just that worker or task, and **↺ Use the default** to go back. Board agents and tasks the Queue agent adds, which nobody picks for, start on the default too. Existing workers keep their provider when prompted or resumed, and queued tasks keep their choice when retried or restored after a restart.
Every new worker starts on the office's **Default worker**: a provider, model and effort an admin picks in ⚙️ Settings → **🤖 Workers** (the `--agent` with its own default model until someone does). The hire, ask, queue, issue and meeting windows show it on one line; click **✏️ Edit** there to pick **Claude Code**, **OpenCode**, **Codex**, **Grok**, **Muse**, **DeepSeek Harness**, **Pi** or **Cursor**, a model and an effort for just that worker or task, and **↺ Use the default** to go back. The Issues and PR agents start on the default too, unless an admin gives one its own: under ⚙️ Settings → **🤖 Workers**, each has a line with its own **✏️ Edit** and **↺ Use the default**. The Queue agent, and the tasks it adds, always run on the default. Existing workers keep their provider when prompted or resumed, and queued tasks keep their choice when retried or restored after a restart.

Every harness takes both a **Model** and an **Effort**, each optional: leave one on **Default** and the harness uses its own settings.

Expand Down
2 changes: 1 addition & 1 deletion docs/features.md
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,7 @@ Everything in the office, room by room. Back to the [README](../README.md).
- **PR board.** A second tack board shows pull requests in *Draft*, *In review*, *Approved*, *Merged* and *Closed*, with CI status and diff size. Its column headers filter by label too, and the box under each header by title. Click a PR to read its description, comments, reviews and checks, and comment on it. **Files changed** shows the diff, with the files listed on the left as a folder tree or a flat list, and review comments under the lines they're on. Tick a file **Reviewed** (or press **V**; **J**/**K** step through files) and it folds away. Your ticks are kept in your browser, and a file you ticked that changes again is flagged. **Merge…** squashes, merges or rebases it, and can have GitHub merge it once its checks pass. **Close PR…** closes it without merging, with an optional comment, and can delete its branch. **Review** and **Fix comments & merge** hand the PR to a worker; the second one addresses the review comments, gets the checks green and merges. When the PR conflicts with its base, that button becomes **Fix conflicts & merge**: a new worker is picked by default, merges the base in, resolves the conflicts and merges. Its labels change the same way as an issue's: **🏷️ Edit** in the PR window, or the 🏷️ on its card.
- **The merge gong.** A brass gong stands by the elevator, on the far side from the PR board. When a pull request merges, it rings for everyone on the floor, confetti rains down from the ceiling all over the floor (the loft too) and bursts over the desk the PR came from (with no worker there, over the gong), and every worker that's awake climbs up on its desk for a quick dance, its light flashing like a disco ball, before it hops back into its seat. Workers on bean bags dance on the bag, and the board agents on their kiosks. It rings however the PR merged: from the PR window, by a worker's `gh pr merge`, by auto-merge or on GitHub itself. When the last task on the task queue gets done, the gong rings three times and confetti flies over every desk. Walk up and press **E** to bang it yourself.
- **Ask a worker about anything on a board.** Every issue and PR has **✍️ Ask a worker…**: type your own prompt, and the worker gets it along with which issue or PR it's about. Send it to a new worker at a free desk, or to one already sitting at a desk.
- **Board agents.** An agent stands at a little kiosk beside each of the issues board, the PR board and the task queue. Walk up and press **E**, type what you want, and it runs as a Claude Code (or your configured) agent that already knows its job. The issues agent files, finds, triages, labels and closes issues with `gh`. The PR agent sums up, reviews, comments on and merges pull requests. The queue agent turns what you ask for into tasks on the queue, one per piece of work, and can tell you what's queued or take tasks off. It never does the work itself, not even a one-line fix: it queues it and tells you what it queued. The first prompt hires it; after that your prompts go into the same session, so you can follow up, and a sleeping one wakes up with your prompt. Press **O** there to watch its terminal and **X** to send it home, and the next prompt starts a fresh one. The agents work in the floor's main checkout, so they're told not to change code there: work that needs code changed goes on the queue, where a fresh worker does it in its own worktree.
- **Board agents.** An agent stands at a little kiosk beside each of the issues board, the PR board and the task queue. Walk up and press **E**, type what you want, and it runs as a Claude Code (or your configured) agent that already knows its job. The issues agent files, finds, triages, labels and closes issues with `gh`. The PR agent sums up, reviews, comments on and merges pull requests. The queue agent turns what you ask for into tasks on the queue, one per piece of work, and can tell you what's queued or take tasks off. It never does the work itself, not even a one-line fix: it queues it and tells you what it queued. It starts on the office's default worker, or on a provider, model and effort an admin gives it under ⚙️ Settings → **🤖 Workers** (the queue agent always runs on the default). The first prompt hires it; after that your prompts go into the same session, so you can follow up, and a sleeping one wakes up with your prompt. Press **O** there to watch its terminal and **X** to send it home, and the next prompt starts a fresh one. The agents work in the floor's main checkout, so they're told not to change code there: work that needs code changed goes on the queue, where a fresh worker does it in its own worktree.
- **Agents that manage agents.** Any worker can see and run the office's other workers. Ask one to "send everyone whose PR merged home", "hire someone to fix the flaky test" or "tell Mochi to rebase", and it can. Claude Code, Codex and OpenCode workers get an `agent-office` MCP server with five tools: `list_workers` (who's at which desk, their status and task, their branch, and where their pull request stands), `hire_worker`, `send_home`, `tell_worker` and `link_pr`. Every worker, whichever agent it runs, also has the same thing as the `office-workers` command (`office-workers list`, `office-workers home --merged`, and so on). Sending home works as it does from a desk: each worker's worktree and branch are deleted unless they hold work that isn't on GitHub (what a merged PR delivered counts as on GitHub), and the answer says what was kept and why. Everyone on the floor sees a toast saying which worker did what, and nobody can send themselves home.
- **Whose pull request is whose.** A worker's desk shows where its pull request stands (open, or merged and free to go home) rather than just *ready* or *done*. The office knows the pull request when it was opened from the desk (**O**), from the branch the office gave the worker, or by a queue task. A Claude Code worker that runs `gh pr create` itself is linked to what that opened too, whatever branch it pushed, which is how a worker hired without its own worktree gets one. For the ones it can't see (another agent that opened its own from a branch of its own, or one opened by hand), ask any worker to "link the PRs to the workers that opened them": it matches them up and says so with `link_pr` (`office-workers pr 12 --worker Mochi`, or `--none` to take a wrong one off).
- **The meeting room.** Under the boss office there's a glass meeting room with a long table. Press **E** at it (or **🤝 Meeting room** in the **☰** menu, which sits on the top bar while a meeting is on), or **🤝 Meeting…** on an issue or **🤝 Review panel…** on a PR, to call a meeting: 2 to 5 workers step out of the elevator one after another, walk over and sit down round the table, each with a role you can rename, and work through one question or task together in a pattern. **Debate**: everyone proposes, then critiques the others, and the head of the table writes the decision. **Lead & team**: the lead splits the task into a part for each of the others, they each do theirs, and the lead merges the work. **Map-reduce**: the same task over a list of parts (files, modules, issues) shared out between the mappers, then one worker combines what they found. **Red / blue**: red attacks the change (bugs, security holes, edge cases) and blue fixes what holds up, round after round, until red finds nothing more. **Review panel**: reviewers each read a PR through their own lens (correctness, security, performance…), and the head of the table merges them into one review, every finding tagged with its lens, which the office posts on the PR. Every meeting declares its output file up front (`docs/decisions/….md`, a review…) and ends when that file is written: the office commits it on the meeting's own branch (**🔀 Open PR** in the meeting window opens a pull request for it). Meetings are bounded by a round limit per pattern, and have no token limit. You pick the round limit for a debate (2 to 4) or red / blue (1 to 5). The other patterns always run the same rounds, so the form says so in place of a limit to set (**🔒 3 rounds · fixed by the Lead & team workflow**) and names them: planning, execution and merge for lead & team, map and reduce for map-reduce, the reviews and the combined review for a review panel. When a worker won't write its part, or when someone is sent home, the meeting stops and says why; **⛔ Stop meeting** stops one by hand. The sign on the door shows the pattern, the round, and the tokens and cost so far, and a one-line summary once it's over; the board on the back wall shows the output as it's written, and the card over each worker says whose turn it is and which round. The workers stay at the table so you can read their terminals until **🧹 Clear the room** or the next meeting sends them home.
Expand Down
110 changes: 110 additions & 0 deletions src/client/ui/settings-boards.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,110 @@
// ⚙️ Settings' Board agents, under Workers: what the Issues and PR agents start on when nobody
// picks for them there. An admin can give each one a provider, model and effort of its own; left
// alone, they run on the office's Default worker (see server/stations.ts). The Queue agent is never
// picked for here: it always runs on the default.
import type { Net } from '../net';
import { store } from '../state';
import type { AgentChoice } from '../../shared/protocol';
import { STATION_AGENT, type ConfigurableStation } from '../../shared/layout.js';
import { h } from './dom';
import { agentFields, choiceLabel, officeChoice } from './provider.js';

/** The boards an admin picks a worker for, in the order they stand here. */
export const BOARD_AGENTS: readonly ConfigurableStation[] = ['issues', 'pulls'] as const;

/** One board agent's row: what it starts on, and the fields to pick another for it. */
export interface BoardAgentRow {
/** What it starts on: the station's own pick, or the office's default worker. */
now: HTMLElement;
fields: HTMLElement;
/** Save, and ↺ Use the default. */
actions: HTMLElement;
/** Repaints it from the store; called again whenever ⚙️ Settings changes. */
paint: () => void;
}

/**
* A board agent's provider, model and effort, in ⚙️ Settings → 🤖 Workers. It reads what the station
* starts on — its own pick, else the office's default — and offers ✏️ Edit to pick another, Save to
* send it, and ↺ Use the default to drop it. Only admins get the buttons; for everyone else it's a
* line saying what that agent runs on.
*/
export function boardAgentRow(net: Net, kind: ConfigurableStation): BoardAgentRow {
const name = STATION_AGENT[kind].name;
const id = `station-${kind}`;
const fields = agentFields(store.project, id, stationChoice(kind));
let editing = false;
/** What the fields were opened on: ✏️ Edit with nothing changed sends nothing. */
let opened: AgentChoice | undefined;
const now = h('p.outside-now');
const edit = h('button.btn', { type: 'button', 'aria-expanded': 'false' });
const save = h('button.btn.primary', { type: 'button' }, 'Save');
const back = h('button.btn', { type: 'button' }, '↺ Use the default');
const actions = h('div.seg', { style: 'margin-top:8px' }, edit, save, back);

/** The station's own pick, when an admin made one this project can still run. */
const own = () => store.prompts.stations?.[kind] as (AgentChoice & { by: string; at: number }) | undefined;

edit.addEventListener('click', () => {
editing = !editing;
opened = editing ? stationChoice(kind) : undefined;
// It opens on what the agent runs on now, and closing without saving goes back to it.
if (editing) fields.set(stationChoice(kind));
paint();
if (editing) (fields.element.querySelector('select') as HTMLSelectElement | null)?.focus();
});
save.addEventListener('click', () => {
if (!fields.valid()) return;
const choice = fields.choice();
// Nothing changed: saving would only restamp who set it.
if (opened && sameChoice(opened, choice)) {
editing = false;
paint();
return;
}
net.send({ t: 'prompts.station', kind, choice });
editing = false;
paint();
});
back.addEventListener('click', () => {
net.send({ t: 'prompts.station', kind, choice: null });
editing = false;
paint();
});

function paint(): void {
const admin = store.me.admin;
const mine = own();
const choice = stationChoice(kind);
fields.element.classList.toggle('hidden', !admin || !editing);
actions.classList.toggle('hidden', !admin);
edit.textContent = editing ? '↺ Cancel' : '✏️ Edit';
edit.title = editing ? `Leave the ${name.toLowerCase()} on ${choiceLabel(stationChoice(kind))}` : `Pick a provider, model or effort for the ${name.toLowerCase()}`;
edit.setAttribute('aria-expanded', String(editing));
save.classList.toggle('hidden', !editing);
// ↺ Use the default only means something while there's a pick to drop, and not while picking.
back.classList.toggle('hidden', editing || !mine);
now.classList.toggle('hidden', editing);
now.textContent = choiceLabel(choice);
now.title = mine
? `The ${name}'s own worker, set by ${mine.by}: it starts on this instead of the office's default`
: `The office's default worker; an admin can give the ${name.toLowerCase()} one of its own in ⚙️ Settings`;
fields.element.setAttribute('aria-label', `${name} worker`);
}
paint();
// The default or a pick can change while this is open; it goes once ⚙️ Settings has closed.
const off = store.on('prompts', () => (now.isConnected ? paint() : off()));
return { now, fields: fields.element, actions, paint };
}

/** What a board agent starts on: its own pick, or the office's default worker. */
export function stationChoice(kind: ConfigurableStation): AgentChoice {
const mine = store.prompts.stations?.[kind];
if (mine) return { provider: mine.provider, ...(mine.model ? { model: mine.model } : {}), ...(mine.effort ? { effort: mine.effort } : {}) };
return officeChoice(store.project);
}

/** Whether two picks would start a worker the same way. */
function sameChoice(a: AgentChoice, b: AgentChoice): boolean {
return a.provider === b.provider && (a.model ?? '') === (b.model ?? '') && (a.effort ?? '') === (b.effort ?? '');
}
8 changes: 8 additions & 0 deletions src/client/ui/settings.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,9 @@ import { askNotifyPermission, notifyPermission, type DesktopNotifier } from '../
import type { ThemePick, WebhookKind } from '../../shared/protocol';
import { THEME_PICKS } from '../../shared/theme';
import { mapChoices } from '../../shared/maps';
import { STATION_AGENT } from '../../shared/layout';
import { dogSetting } from './settings-dog';
import { BOARD_AGENTS, boardAgentRow } from './settings-boards';
import { h, openModal, timeAgo } from './dom';
import { agentFields, choiceLabel, officeChoice } from './provider';
import { openPromptEditor, rewrittenPrompts } from './prompts';
Expand Down Expand Up @@ -297,6 +299,10 @@ export function openSettings(net: Net, settings: Settings, onChange: (s: Setting
const agentActions = h('div.seg', { style: 'margin-top:8px' }, agentSave, agentBack);
const agentNow = h('p.outside-now');
const agentNote = h('p.setting-note');
// What the Issues and PR agents start on: each one's own pick, or the default above (see settings-boards.ts).
const boardRows = BOARD_AGENTS.map((kind) => ({ kind, row: boardAgentRow(net, kind) }));
const boardNote = h('p.setting-note');
boardNote.textContent = 'What the Issues and PR agents start on when nobody is there yet: leave them on the default worker above, or press ✏️ Edit to give one a provider, model and effort of its own. The Queue agent always runs on the default.';
const paintAgent = () => {
const admin = store.me.admin;
const picked = store.prompts.agent;
Expand Down Expand Up @@ -468,6 +474,8 @@ export function openSettings(net: Net, settings: Settings, onChange: (s: Setting
],
workers: [
setting('Default worker', 'office', agentNow, agent.element, agentActions, agentNote),
...boardRows.map(({ kind, row }) => setting(STATION_AGENT[kind].name, 'office', row.now, row.fields, row.actions)),
setting('Board agents', 'office', boardNote),
setting('Worker limit', 'office', limitRow, limitNote),
setting('Workers whose pull request merged', 'office', leaveRow, leaveNote),
setting('Prompts', 'office', promptsOpen, promptsNote),
Expand Down
Loading