Skip to content

Drive the Lambos and Ferraris in the garage (#43) - #181

Merged
webdevcody merged 4 commits into
mainfrom
drive-cars-43
Sep 29, 2026
Merged

webdevcody merged 4 commits into
mainfrom
drive-cars-43

Conversation

@webdevcody

Copy link
Copy Markdown
Contributor

Closes #43.

Walk up to any of the nine cars in the garage (or the Lambo parked out front) and press E to get behind the wheel. W is the gas, S brakes and then reverses, A/D steer, Space brakes, H honks, E gets you out. Press E at a car someone else is driving to ride in the passenger seat.

How it works

  • Physics runs on the driver's page (shared/garage.ts): an arcade bicycle model (0 to 20 m/s, tighter turns at a crawl than flat out), moved in ≤25 cm steps so nothing thin gets skipped. Cars stay on the pavement: the garage, the lot in front, the east lot, across the sidewalk and ±90 m along the street. Columns, walls, lamp posts, tree trunks (they get colliders now) and other cars stop you. Hit something head on and you bounce off with a crunch. Hit it at an angle and you slide along it and turn parallel. A car that drives into someone standing on the street knocks them aside.
  • The office keeps seats and positions (server/garage.ts, one per floor like the basketball court): one driver and one passenger per car. Only the driver's car.drive moves a car. Poses off the pavement are ignored, and speed and steering are clamped. Nothing is saved, so a restart parks every car back in its spot. A car you get out of (or disconnect from, or leave by elevator) stays where you left it.
  • Everyone on the floor sees it: drives are relayed at ~15 Hz. Other pages smooth them and extrapolate up to 0.25 s. The roof comes off while anyone's in a car (windshield, bucket seats, steering wheel), so you can see who's driving. The front wheels steer, and the name tag says "🏎️ driving the Orange Lambo" or "riding in…" (in the people list too). There's a positional engine note with fake gear shifts, plus the horn, doors and crashes.
  • Races: the office answers each of your own car.enter/car.leave with answer: true. The page only reconciles once all of its own requests are answered, so someone else's car news arriving mid-request can't kick you out. If someone did beat you to the seat, you're put out with a toast.
  • Edge cases: riding the elevator, switching floors, N to a desk and walk-to all get you out first. A reload mid-drive puts you back on your feet beside the car, and the tab's pagehide parks the car exactly where you were. On a reconnect you get back into your seat if it's still free.
  • Cost: each car is now its own set of meshes (the old garage merged all nine into one batch). Up in the office, cars fully under the building aren't drawn, so the office view's draw calls are unchanged: 1733 with the fleet vs 1778 hidden, which is noise. In the garage and on the street they add ~185–300 calls. Headless Metal Chrome stayed at 60 fps everywhere.

Docs: docs/controls.md (W A S D, Space, E, and a new H row), docs/features.md, and the in-game help (H).

Testing

  • npm run typecheck, and npm test: 299 pass, including the new tests/garage.test.ts (roster/pavement/physics/SAT overlap, server seats/drive validation/honk rate) and tests/driving.test.ts (a real Fleet + Driver: drives down the road, stops at a column with a crunch and reverses away, slides along an angled wall and lines up with it, gets out by the driver's door or not at all when boxed in, passenger can't drive).
  • Headless Chrome smokes against a real office, two or three pages at once:
    • Ann and Bob each get into a Ferrari, drive out of the garage and along the street in opposite directions. Once stopped, both pages agree on both cars' positions, and each draws the other sitting in their car.
    • Bob's page shows "🏎️ driving the Rosso Ferrari". Ann's honk is heard on Bob's page. When Ann gets out, the car stays parked for Bob.
    • Bob rides with Ann in the Blue Lambo and moves with the car. Cat sees "Ann and Bob · full". Driving head on into a garage column stops the car with a crunch. Driving at Bob on foot knocks him ~1.3 m to the side. Ann riding the elevator to the roof mid-drive leaves the car parked and free.
    • An office restart parks the moved car back at its spot. On a two-floor building, driving from the upper floor's garage (street at y −10.7) works, and a reload mid-drive puts you beside your car.

🤖 Generated with Claude Code

webdevcody and others added 4 commits September 29, 2026 13:04
E at a car gets you behind the wheel, or beside whoever's driving it. W A S D
drive (arcade physics in shared/garage.ts, run on the driver's own page), Space
brakes, H honks, E gets you out. Cars stay on the garage, the lots round it and
the street, stop at columns, lamps, trees and each other, and knock anyone in
the way aside. The office keeps who's in which car and where its driver left
it, per floor and in memory only, so a restart parks them all back home.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A car that hits a wall at an angle turns to run along it instead of grinding
into it, and throttling and crunches run on the driving clock. Cars in under
the building aren't drawn while you're up in the office. A floor's cars snap to
where they are on arrival, so a reload mid-drive puts you beside yours, and
closing the tab mid-drive parks it where you were. Controls, features and the
in-game help say how to drive; tests/driving.test.ts drives a real Fleet.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
main now has the 2D view (#182) and the blender lounge, desk props, kitchen
and plants (#186). Keep both in main.ts (slowFrames beside shovedAt) and in
whereabouts (📱 on the 2D view before the car line), and give the props lab's
lambo and ferrari the .root that supercar() returns now.
@webdevcody
webdevcody merged commit 8030361 into main Sep 29, 2026
1 check passed
webdevcody added a commit that referenced this pull request Sep 29, 2026
…e maps branch

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
webdevcody added a commit that referenced this pull request Sep 29, 2026
…) into the desk signs branch

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
flrnoh referenced this pull request in flrnoh/agent-office Sep 30, 2026
* Office on your Tailscale network: no SSH tunnels (aws.sh up --tailscale) (#164)

* Tailscale: put the office on your tailnet instead of an SSH tunnel

provision.sh --tailscale (and aws.sh up --tailscale) joins the machine to
a tailnet and serves the office on https://<machine>.<tailnet>.ts.net with
Tailscale Serve. Workers' web servers get https://<office>.ts.net:<port>,
relayed through the office like a service tunnel. The Invite and Services
panels say how to get in on the tailnet.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* Docs for running the office on Tailscale

README's AWS and Add users sections, a Tailscale section in docs/aws.md,
provision.sh --tailscale in docs/self-hosting.md, and the tailnet's reach
in the security notes. aws.sh keeps a last output line without a newline.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>

* Deploy the office to Azure: deploy/azure.sh (Azure VMs, the EC2 script's twin) (#175)

* Azure: deploy/azure.sh puts the office on an Azure VM, like deploy/aws.sh on EC2

One command up (a Standard_B4s_v2 VM, Azure's t3.xlarge: 4 vCPU, 16 GiB, burstable),
the same lifecycle and team commands as the AWS script, and the same provision.sh.
Everything lives in one tagged resource group, so destroy is one az group delete.

The office learns which script deployed it (AGENT_OFFICE_DEPLOY_SCRIPT, written by
provision.sh), so the Invite and Services panels suggest deploy/azure.sh commands.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* Docs: deploying to Azure (README section + docs/azure.md)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* Azure: catch resizes Azure refuses before stopping the VM; clear errors without the SSH key

resize now checks architecture, Gen1/Gen2, Trusted Launch and local-temp-disk
compatibility first, so a size Azure would refuse costs no downtime. up turns down
Arm sizes without Trusted Launch (Bpsv2) and points at Cobalt ones. Commands that
SSH in say so when this computer has no key, instead of exiting silently.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* Azure: review fixes — D4as_v5 default, remembered subscription, safer lookups

- Default to Standard_D4as_v5: the t3.xlarge's 4 vCPU / 16 GiB at about its price,
  without B-series credit throttling (a t3 bursts without limit by default).
- Remember the subscription an office was made in; destroy keeps this computer's
  files for an office in another subscription.
- Resource group and firewall lookups fail loudly instead of reading as "none", so
  a hiccup can't retag someone's group or drop everyone else's SSH address.
- Firewall rule written with create (a PUT) in one call; 30-minute idle timeout on
  the public IP for teammates' keepalive-less tunnels.
- Options can come before the command, and the office's suggested commands carry
  --name for a second office. Names are lowercased (Azure group names ignore case).
- status tells a stopped (still billed) VM from a paused one; destroy lists what goes.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* Azure: second review — connect, safer resizes, keep Claude sign-in on re-runs

- deploy/azure.sh connect: a second computer adds its SSH key (az vm user update)
  and IP without re-provisioning, so the office keeps its GitHub and Claude sign-ins.
- provision.sh keeps the Claude token / API key it was given before when run again
  without one (up to resize or update no longer signs the office out; AWS too).
- resize also refuses Premium-less and NVMe-only sizes before stopping anything,
  goes back to the old size when Azure can't start the new one, and leaves a
  paused office paused.
- VM, NSG and public IP lookups are list calls that fail loudly, so an error can't
  re-create the NSG and drop teammates' addresses.
- When SSH won't connect because this computer's IP isn't allowed, say so and
  suggest `allow me` instead of retrying for minutes.
- Missing option values, a 10-minute silent wait, and Git Bash's /c mangling fixed;
  .gitattributes keeps *.sh LF on Windows checkouts. Docs: Free Trial quota limits,
  connect, re-running up.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>

* Terminals edit like your own: ⌘⌫, Ctrl+⌫ and Shift+Enter (#125) (#167)

xterm.js sends a plain backspace for ⌘⌫ and Ctrl+⌫, a plain Enter for
Shift+Enter, and nothing for ⌘← / ⌘→. The office terminal now sends the
readline control keys iTerm2's "Natural Text Editing" and VS Code send:
Shift+Enter → Ctrl+J (a new line in Claude Code, Codex and OpenCode),
Ctrl+⌫ → Ctrl+W, ⌘⌫ → Ctrl+U, ⌘⌦ → Ctrl+K, ⌘← / ⌘→ → Ctrl+A / Ctrl+E.

Closes #125

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>

* Drop or paste screenshots into a worker's terminal (#124) (#171)

Dragging a file onto the browser terminal opened it in a new tab, and a
pasted screenshot went in as nothing. Now a file dropped anywhere while a
terminal is open, or a picture pasted into it, is sent to the office
(POST /api/term/drop), kept in .agent-office/drops/<worker>/, and its
path is pasted into the terminal the way a native terminal does for a
dragged file. Claude Code and Codex turn that into [Image #1].

Paths are backslash-escaped for shells (quoted on Windows), file names
are made safe to type, and a worker's drops go when it goes home (and
are pruned at startup).

Closes #124

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>

* Worktrees start from what's on GitHub, not a stale local HEAD (#119) (#170)

Every worktree was cut from the project checkout's HEAD, which nothing ever
fetched or pulled. Once a task's PR merged on GitHub, the next task still
branched from the pre-merge commit, so its PR worked on old code and could
undo the fix before it.

- Worktrees.fetch() runs `git fetch origin <branch>` (no password prompts,
  15 s timeout, one fetch shared by a burst of hires; skipped with no origin,
  a failure is logged once and falls back to what's here).
- Worktrees.create() branches from origin/<branch> unless HEAD already has
  all of it (unpushed local commits keep HEAD, as before). When both moved
  on, it starts from origin's and a toast says what it left out.
- The queue fetches before seating a worktree task (only when it has a desk
  to seat it at), and hand hires with a worktree and meetings wait for the
  fetch in the server. The project's own checkout is never moved.
- The Changes window diffs against the newer of main and origin/main, so
  merged PRs the project never pulled aren't shown as the worker's changes.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>

* Filter the issue and PR board columns by title (#165)

Each column gets a "Filter by title…" box under its header. Typing
narrows it to the cards whose titles have every word typed (any case,
any order), together with the column's label filter. The count shows
shown / total, ✕ clears the box, and a refresh from GitHub keeps the
text, focus and caret. The boxes start empty each time the board opens.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>

* Random display names: leave your name blank, skip, or roll one (#120) (#166)

The character screen suggests a made-up name ("Sunny Otter") in the name
box. Leaving it blank, or skipping the screen with ✕/Esc, goes in under
that name instead of stopping you or making you "Guest", and a 🎲 beside
the box deals another. The name is saved with your look as before, so
you pick once per browser; a profile left as Guest by an earlier skip
gets a fresh suggestion the next time the screen opens.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>

* The elevator goes down to the garage, and back up from there (#169)

The elevator panel has a Garage stop under floor 1. From a floor it rides
down the same shaft to a new elevator stop at the garage's back wall (you
stay on your floor, down at its street); from the roof it goes to the
bottom floor's garage. In the garage, E at the elevator opens the panel,
where your own floor is rideable again ("your floor") along with every
other floor and the roof. Reloading in the garage car puts you back there.

- world/elevator.ts: buildElevator(height) with setFloor(y), so a short
  garage-tall copy follows the street down on higher floors
- office.ts: the garage stop, moved by setLevel; outside.ts paints a
  keep-clear box in front of it
- tower.ts: the bottom floor's slab over the garage, seen from below,
  on floors above the first (their garage had no ceiling before)
- player.ts: third-person camera stays on this side of the garage's back
  and west walls, like it stays in the room upstairs
- main.ts: pickTarget lets you use what's downstairs (the garage stop)
  while you're down there

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>

* Terminal: a ⎋ Esc button, so a menu like /skills can be closed (#131) (#168)

The Esc key leaves a terminal, so a Claude menu that only closes with Esc
(/skills) trapped you: Ctrl+[ was the only way to send one, and it was only
mentioned in the ✕ button's tooltip. Now:

- ⎋ Esc in the terminal's header sends Esc to the program (works on phones
  and any keyboard layout).
- Ctrl+[ and Ctrl+] also match by the key's position, for layouts where
  [ and ] are other letters; AltGr combos no longer close the terminal.
- Leaving with Esc while the screen mentions Esc (Claude's "Esc to close")
  shows a tip naming ⎋ Esc and Ctrl+[.
- On narrow windows the header wraps under the name instead of pushing ✕
  off the edge.

Closes #131

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>

* Follow a worker to the branch it made itself: PR badge, O and send-home (#172)

* Follow a worker to the branch it made itself: PR badge, O and send-home (#142)

A worktree worker's PR status and O at the desk went by the branch the office
cut for it (office/<name>-<id>), saved at hire and never looked at again. Agents
often run `git checkout -b <name>` themselves and open the PR from there with
gh, so the bubble stayed dark and O said "has no commits on office/… yet".

The office now reads the branch the worktree is on (as the Changes window does)
when a worker comes to rest or exits, when the office starts, whenever GitHub's
PR list comes in, and before O or sending it home, and keeps `worktree.branch`
on it. The office's own branch is remembered in `worktree.made`. workerPr,
workerForPull, openPr, the queue task's branch and the worktree check before
sending it home all follow along.

Sending it home with its branch deletes the branch it made, plus the office's
branch when that has nothing the other lacks. A branch that was there before
the worker was hired (by its reflog), like main, is never deleted.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* Send-home keeps the office's branch while it holds work; follow a renamed branch (#142)

A worker that committed on office/<name>-<id> and then checked out a branch
from before it was hired (release) had its commits deleted on send-home: the
worktree check only looked at release, found nothing, and 'all' then ran
`branch -D` on the office's branch. The check now counts commits on the
office's branch too, so the automatic send-home keeps everything and the
dialog warns. And deleting never takes the office's branch while it has
commits no remote, the project's checkout or the branch it's on has: the
worktree goes, that branch stays, and the note says so.

An agent that renamed the office's branch (`git branch -m fix-x`) made every
send-home fail with "Couldn't delete …'s worktree", fix-x left behind. The
office now spots the rename in the branch's reflog, treats fix-x as its own
and forgets office/…. If the office's branch was deleted instead, git can't
say whether the branch it's on is the worker's own, so only the worktree goes.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>

* One task across several projects: a worker gets a worktree of each, and a PR in each that links the others (#173)

* Workers across repositories: one task in several floors' projects, a PR in each (#161)

A worker hired with its own worktree can take other floors' projects along. It then
works in a workspace folder (.agent-office/worktrees/<name>) with a worktree of each
project, all on office/<name>, and a brief (CLAUDE.md + AGENTS.md, the new
"worker.repos" prompt) saying which folder is which. Git run in the workspace itself
finds no repository (GIT_CEILING_DIRECTORIES), so it can't touch the floor's checkout.

- Hire dialog and the boards' Ask window: "🗂️ Also work in" picks other floors.
- O opens a pull request in each repository with commits, each listing all of them
  (a marked block, kept up to date); the issue is closed by the home floor's PR and
  mentioned as owner/repo#n by the others. O again shows each repository's PR.
- Changes window: a tab per repository, diffed against that floor's branch.
- Send home checks and deletes every worktree, then the workspace.
- Going home once merged waits for every repository's PR, on its own floor's board.
- agent-office prune leaves workspaces and other floors' workers' branches alone.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* Docs for workers across repositories; prune help; smaller fixes

- docs: features (one task across several projects), how it works (workspace,
  GIT_CEILING_DIRECTORIES, PR list markers, landing, prune), controls (O), prune.
- "its worktree of api" for one repository in the uncommitted-changes warning.
- A failed check before a worker across repositories goes home never leaves it stuck.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>

* Deploy to Railway: deploy/railway.sh, SSH tunnel only, everything on a volume (#176)

* Deploy to Railway with deploy/railway.sh: SSH tunnel only, everything on a volume

deploy/railway.sh up creates a Railway project with one service built from
this checkout (deploy/container/Dockerfile), a volume on /data and a TCP proxy
in front of the container's sshd. The office listens on 127.0.0.1:4600 inside
and has no public URL: your key gets a shell as agentoffice, and teammates
invited from 👥 Invite teammates tunnel in as `office`, which can only forward
to the office. The team helper, tunnel login and sshd limits are copied out of
provision.sh at build time, so servers and containers share one copy.

The volume holds agentoffice's home (the office's data, the projects, Claude
Code and its sign-in, gh's sign-in, git config), the SSH host key and
teammates' keys, so restarts and redeploys keep all of it. Also: open, update,
restart, service, invite/uninvite/team, status, ssh, logs, reset-password and
destroy, like deploy/aws.sh.

The invite panel handles an SSH address with its own port
(ssh://office@host:port, from Railway's TCP proxy variables) and leaves out the
AWS-only "allow their IP" note there. AGENT_OFFICE_DEPLOY_SCRIPT is set to
deploy/railway.sh, so the panels suggest its commands.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* railway.sh: options anywhere, never forget a project Railway couldn't list

- The office suggests `deploy/railway.sh --name <name> service <port>`, which
  failed with `unknown command "--name"`. Options now go before or after the
  command, like deploy/azure.sh: the first word that isn't an option is the
  command, and an option missing its value says so.
- A failed `railway list --json` (network, expired login, 2FA) no longer counts
  as "the project is gone". destroy used to skip the delete, remove the only SSH
  key, claim token and IDs and print "All gone" with the project still running;
  up made a second project and forgot the first. Now both stop when the listing
  fails or can't be read, and destroy removes the state directory only after
  the project is deleted or a listing that worked shows it gone.
- ensure_project saves each ID as soon as it exists, so a failed `railway
  status` or `add --service` after `init` doesn't make the next up create
  another project, or destroy say there's nothing to delete.
- The `office` user can't forward to or create Unix sockets
  (AllowStreamLocalForwarding no, in provision.sh's sshd block, which the
  container copies too).
- The image leaves out .env*, .claude, keys, id_*, pw.txt and .agent-office at
  any depth: `COPY . .` took them from a local build, and railway up only drops
  what .gitignore lists.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>

* feat(models): blender model pipeline, the office dog in five breeds, and a loading screen (#177)

* feat(server): serve .glb models as model/gltf-binary

* feat(dog): blender script that models, rigs and animates the office dog

blender/scripts/build_dog.py builds the dog (one seamless skin, light coat patches, ears, eyes, jaw, collar), rigs it with IK legs, keys nine looping clips (stand, walk, run, wag, sniff, sit, bark, lie, nap) and exports src/client/models/dog.glb. tests/dog-model.test.ts holds the model to the bone, socket, material and clip names dog.ts relies on.

* feat(dog): play the blender dog in the office

The office dog is now the skinned dog.glb, cross-fading between its clips by what the server has it doing, trotting or galloping at the speed it moves. The woof, panting, blinking, costumes and coat colors stay in code, and the mouse picks it by capsules on its bones.

* chore(lab): dog preview page for checking the model by eye

* chore(blender): ignore python bytecode caches

* refactor(blender): share one modelling kit between the model scripts

blender/scripts/aokit.py holds what every build script needs (shapes, one smooth skin, painted patches, rigs and clips, export, review renders); build_dog.py keeps only the dog. blender/README.md sets out the conventions every model keeps.

* feat(models): preload blender models and paint them by material name

main.ts loads every model before it builds the world, so a builder can take its model synchronously with model(name); paintModel() and palette() give each material name its toon material.

* chore(lab): props lab, a shared stage and a screenshot helper

* test(models): shared reader for model tests

* feat(models): preload only the models the world is built with

Each model in world/models.ts says whether it's preloaded, so a page doesn't download every dog breed before it opens. Review sheets name their tiles after the sheet, so scripts rendering at once don't overwrite each other's.

* feat(dog): five breeds from the one dog script

blender/scripts/dog_breeds.py holds the presets (the pup as it was, a corgi, a dachshund, a pug and a shiba); build_dog.py builds each with the same bones, sockets, materials and clips from its own proportions and exports dog-<breed>.glb. The model test runs over every breed.

* feat(dog): every floor gets a breed of dog

A floor's breed comes from its id like its coat; the office sends it with the dog, and the page loads that breed's model when it first needs it, keeping the old one on until the new one is in. Picking capsules and tag heights are fitted to whichever breed it is.

* docs(blender): the dog script builds every breed

* feat(models): report how far the model files have loaded

* feat(dog): say when the first dog has its model on

* feat(loading): hold a loading screen until the office's models are in

* feat(models): the lounge furniture and the desk books and mugs from blender (#186)

* feat(kitchen): blender script for the kitchen corner

An espresso machine with a glowing light, a round-shouldered retro fridge with notes on it, and a counter with a wooden top and a sink, exported to src/client/models/kitchen.glb.

* feat(kitchen): build the kitchen corner from its blender model

world/kitchen.ts puts the model where the old boxes stood, turned to face into the room, with the same colliders and the coffee machine's interactable only on the machine.

* feat(plants): blender script for the office plants

A monstera with split leaves on arching stalks, a snake plant with banded sword leaves in a glazed planter, a little ficus tree with a crown of shingled leaves, and an echeveria for the desks, exported to src/client/models/plants.glb. Each species is its pot, the old code-built pot's size, with everything growing out of it hung under the pot as its own object, so Christmas can hide the leaves and leave the pot.

* feat(plants): pot the office plants from their blender model

office.ts clones a species out of plants.glb wherever a plant stood: the plants round the room take turns with the three floor species, the balcony and the loft skip the wide monstera, and the desks get the succulent. Their places and colliders stay as they were. holiday.ts now finds the leaves by name, so the Christmas tree is never among what it hides, and the props lab lines the four species up (show=plants).

* refactor(models): one helper for a painted piece of a model

models.ts gets piece(): a painted copy of one named object in a preload model, or an empty group if the model didn't load. plant() now uses it, and the lounge and the desk things will too, so a model holding several things placed each on their own has one way to hand them out.

* feat(desk): blender script for the desk books and mug

A chunky diner mug with a round handle and coffee in it, and hardback books in three arrangements (three standing side by side, two standing with one leaning on them, and a short pile lying flat), exported to src/client/models/desk_props.glb. Each is a root of its own standing on the desk at its origin, spines toward the chair. The mug is the old code-built mug's size with its body round its origin, and every arrangement fits the old three boxes' footprint, so they stay clear of the laptop, the holiday present and whoever dances on the desk.

* feat(desk): put the blender books and mug on the desks

office.ts takes the desks' knick-knacks out of desk_props.glb with piece(): deskMug() paints the mug's body in the desk's chair color as before, and deskBooks() hands out an arrangement of books, the desks with books taking turns with the three. Which desk gets the mug, the succulent or the books stays as it was, so the holiday presents and pumpkins still land in the free back corner, and the mug and books stand where the old ones did. The props lab lines the pieces up (show=desk_props), and with desks=1 shows them on desks built by buildDesk, which is exported for it.

* feat(lounge): blender script for the sofa, pillows, poufs and coffee table

A plump three-seat sofa with seat and back cushions, rolled arms and little wooden feet, a throw pillow, a round floor pouf with piping round its middle and a button in its top, and a round coffee table on a pedestal, exported to src/client/models/lounge.glb as four pieces of their own so the office can place each one freely. They keep the old code-built lounge's sizes: the sofa is 4.2 by 1.0 with its seat cushions at 0.47, under the couch's hips and collider, the pouf is 0.4 tall, and the table's top is 0.9 round at 0.46, where the holiday pumpkin stands. The pieces are built the way the desk furniture is, with its Wood, Frame, WoodDark and Cloth material names.

* feat(lounge): furnish the lounge from its blender model

office.ts places each piece of lounge.glb where the old boxes stood: the sofa turned to face the TV with a pillow either side of its middle, hung under the couch so a click on one still offers the couch, the coffee table, and a pouf for each of the two floor seats, each in its old color and turned to the TV. Every collider, seat id and seat radius stays as it was. models.ts preloads the model, and the props lab shows the pieces side by side (show=lounge).

* fix(lounge): pillows clear of the sitters, colliders on the cushions

The pillows lean on the back halfway between the couch's places, at 0.6 either side of its middle, where no sitter's body reaches; tucked against the arms they'd be in the side sitters' way, with only 0.17 m between a sitter's elbow and an arm. The couch's collider top comes down to its seat cushions (0.47) and each pouf's to its top (0.42), so standing on one no longer floats over it. The player test's copies of those colliders match.

* Bookshelf page turns: a soft swish instead of the loud rustle, and a way to turn it off (#174) (#178)

Reading at the bookshelf played the issue card's paper rustle, an 0.8 s crackle at half volume,
every time you opened a doc or scrolled a screenful. Page turns now have their own sound: a short
band-passed swish that rises and falls as the page goes over, and a light pat as it lands, about
10 dB quieter than the rustle, half as long, and without its crackle. Flicking through a doc is one
swish, not one a screenful. The 🔈 at the top of the bookshelf turns it off, and so does a new
"Page turns at the bookshelf" setting under ⚙️ → Sound & voice.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>

* Deploy to Fly.io: deploy/fly.sh, SSH tunnel only, everything on a volume (#179)

deploy/fly.sh up creates a Fly app with one machine built from this checkout
(deploy/container/Dockerfile, the image deploy/railway.sh runs), a volume on
/data and a dedicated IPv4 address with SSH on a random port, and nothing else.
The office listens on 127.0.0.1:4600 inside and has no public URL: your key gets
a shell as agentoffice, and teammates invited from 👥 Invite teammates tunnel in
as `office`, which can only forward to the office.

- The app gets a new name (office name + random suffix; --app to choose one)
  that is saved before it's made, and a new office never adopts an app you
  already have, so a later destroy can't delete it. Region: the one nearest you
  (--region), one shared-cpu-4x with 8 GB (--vm-size, --memory), 50 GB volume
  (--disk).
- fly.toml is written from the saved settings before each deploy: restart
  policy always, never auto-stopped or auto-started, [env] for the admin key,
  the SSH address and AGENT_OFFICE_DEPLOY_SCRIPT, TINI_SUBREAPER since Fly's
  init is PID 1. Tokens go in as staged secrets through stdin.
- start.sh takes AGENT_OFFICE_SSHD_PORT: on Fly, sshd listens on 2222 because
  port 22 in the machine is Fly's own SSH (`fly ssh console`).
- Commands like deploy/railway.sh (open, update, restart, service, invite,
  uninvite, team, status, ssh, logs, reset-password, destroy), plus resize,
  pause and resume. Listing failures never count as "the app is gone".
- docs/fly.md and a README section; the Add users steps cover Fly.io too.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>

* Deploy to Dokploy: deploy/dokploy.sh, SSH tunnel only, everything on a volume (#180)

deploy/dokploy.sh up --url <your Dokploy> puts the office on a Dokploy server
through its API (an API key from Settings → Profile → API/CLI Keys): a project
with one application, built by Dokploy from this checkout with the same
deploy/container/Dockerfile as Railway, a Docker volume on /data, and the
container's sshd published on one port of the server (2222, --ssh-port). The
office listens on 127.0.0.1:4600 inside and has no domain: your key gets a
shell as agentoffice, and teammates invited from 👥 Invite teammates tunnel in
as `office` with ssh://office@host:port.

The checkout goes up as a Dokploy "drop" zip, made with a scratch git index:
uncommitted and new files included, .gitignore'd files and local secrets
(.env*, .claude/, keys, pw.txt) left out. Dokploy unpacks it without file
modes, so the Dockerfile restores the scripts' exec bits. up also switches the
Swarm service to stop-first updates: Dokploy's default starts the new
container first, which would run two offices on one volume for a moment.

Also: open, update, restart, service, invite/uninvite/team, status, ssh, logs,
reset-password and destroy, like deploy/railway.sh. up keeps variables you add
on Dokploy's Environment tab. destroy deletes the application, then its volume
(Dokploy leaves volumes behind), then the project if nothing else is in it
(Dokploy's project delete alone leaves the service running). The Dokploy URL,
API key and IDs are kept in ~/.config/agent-office/dokploy/<name>/.

Tested against a real Dokploy v0.30.8 in a nested Docker VM: up, status, ssh,
invite + a teammate's tunnel, open + claim, update (volume data kept, old task
stopped before the new one), restart, logs, reset-password, a second up
(reuses everything, keeps user env vars), error paths, and destroy.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>

* A 2D view at /lite for phones and slow computers (#16) (#182)

* A 2D view at /lite for phones and slow computers (#16)

The workers on a floor, waiting ones first, each with its terminal (plus a
keypad for the keys a phone keyboard lacks and a prompt box), the boards,
the queue and new tasks, without three.js. The 3D office offers it on a
touch-only device or when frames stay slow, and falls back to it without
WebGL. People on it don't stand anywhere in the 3D office.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* 2D view: phone-width polish, voice skips it, docs

Keypad fits nine keys at 390 px and replaces the header's Esc there, the
waiting count no longer picks up the 3D panel's #waiting styling, the bars
line up with the list on a wide window, and 3D clients don't open WebRTC
connections to people on the 2D view. README, features and controls docs
describe /lite.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>

* Drive the Lambos and Ferraris in the garage (#43) (#181)

* Drive the Lambos and Ferraris in the garage

E at a car gets you behind the wheel, or beside whoever's driving it. W A S D
drive (arcade physics in shared/garage.ts, run on the driver's own page), Space
brakes, H honks, E gets you out. Cars stay on the garage, the lots round it and
the street, stop at columns, lamps, trees and each other, and knock anyone in
the way aside. The office keeps who's in which car and where its driver left
it, per floor and in memory only, so a restart parks them all back home.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* Cars: slide along walls, hide under the floor, docs and tests

A car that hits a wall at an angle turns to run along it instead of grinding
into it, and throttling and crunches run on the driving clock. Cars in under
the building aren't drawn while you're up in the office. A floor's cars snap to
where they are on arrival, so a reload mid-drive puts you beside yours, and
closing the tab mid-drive parks it where you were. Controls, features and the
in-game help say how to drive; tests/driving.test.ts drives a real Fleet.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* Props lab: a supercar's model comes in parts now, so show its root

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>

* Maps: turn the building into a castle (or a map of your own), with a throne, a line of waiting workers and peasants who age as they work (#184)

* refactor(nav): a NavGrid class, so a map other than the office can have its own walk grid

The office's grid is now OFFICE_NAV; walkable/route/nearestWalkable/wayIn/wayHome keep their
behavior and signatures. wayFrom/wayTo (off a seat to a point, and from a point to a seat) move
onto the class so any floor plan can use them.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* feat(maps): maps as config, the building's map pick, and each worker's working time

- shared/maps: MapConfig (plain data) checked and worked out into a MapPlan: seats at tables,
  board agents' lecterns, the meeting table, the boards, a throne, a line in front of it, the
  herald who hires, props and what's in the way for walking. Every map places the office's seat
  ids, so the server, the queue, meetings and saved workers work the same on any of them.
- The castle is the first one, all data (shared/maps/castle.ts).
- Custom maps: JSON in .agent-office/maps/, optionally extending a built-in one.
- server/maps.ts keeps the building's pick in .agent-office/map.json (map.set / map), and
  seats are checked against the map you're on.
- Workers keep count of how long they've worked (workedMs / workingSince).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* feat(castle): the castle map, and worlds the client can swap

- world/world.ts: what main.ts needs from a map's world (seats, boards, colliders, walks in and
  out, confetti, lighting), with the office adapted to it. main.ts shows one world at a time and
  switches when the building's map changes, like the office and the rooftop.
- world/castle.ts builds a castle-style map from its plan: a long hall, pillars and pointed arches,
  a timber roof, stained glass, a dais with a throne of iron blades, long tables and benches with
  tomes for laptops, lecterns for the board agents, a round table for meetings, the boards on the
  side walls, fire and candlelight, great doors, and the Hand of the King.
- world/court.ts: workers waiting on someone get up and line up before the throne, longest waiting
  first, and go back to their seats once answered; new ones sent out by the Hand run to their seat.
- On the throne, E is for whoever's first in line (or the Hand when nobody's waiting).
- Workers dress as peasants there and wear out as they work: a beard that grows long and grey,
  dirt and patches, a hunch, droopy eyes and a slower walk, fully after 30 minutes' work.
- Settings > Building > Map picks the map for everyone, and lists your own maps (or why they won't load).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* docs: maps, the castle, and making a map of your own

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* fix(maps): a map whose wall props reach over its walls says so instead of loading

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* fix(castle): you stay in the hall; only workers come and go through the doorway

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* feat(castle): the hall sounds like itself: no office phones or fridge, the gong and the windows where the hall has them, ale poured not ground

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* fix(maps): review round 1, maps and the server

- A custom map is checked for everything its builder reads: sizes in range, whole numbers of
  seats, steps and spots in line, known table sides, colors as text, at most 40 tables, 12 seats
  a side and 400 props, and an id that's already trimmed. A zero-width window or a dais with a
  billion steps no longer hangs every browser.
- null takes away a map's throne, herald or line; with no dais the throne's on the default one,
  at the height the plan says.
- mergeConfig skips __proto__/constructor/prototype keys.
- The map changing by a read of the folder (a picked map breaking or coming back) is told like a
  pick: everyone's seats are forgotten on the server and in every browser, and a toast says why.
  A failed pick still tells everyone what the read found; 'picked by' only shows for the map in effect.
- Nobody sits where someone on the floor already is: the office refuses it (sit.refused), so two
  people arriving at an empty throne at once don't both sit on it.
- Past the folder's first 24 maps, each is listed with why it isn't read.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* fix(castle): review round 1, the client

- A worker sent out by the Hand (or in off the queue) really runs to its seat: it was left
  'seated' where it came in, so it never set off, couldn't dance and left from the wrong place.
- Arriving (or reconnecting) on the castle no longer replays every worker walking in: the map
  switch put seatedAlready back to false in the middle of the welcome.
- A worker that left the line is its seat's again for a click (the spot's interactable stayed
  on it, and could open another worker's terminal); off interactables aren't aimed at.
- Up on the roof when the map changes to one without it: down to a floor, retried if the trip
  fails, and picking a floor meanwhile no longer loops ride/switchFloor forever.
- N (or Go to desk) to the worker at the front of the line sits you on the throne if it's free,
  else beside it, never inside the throne.
- The great banner gets the project's name; the office's basketball can't be picked up there;
  syncStack doesn't put the street back under the hall; walking to someone on another floor
  keeps walking.
- Getting up faces the way it was (no flip on a seat turned past a right angle), footsteps and
  strides go with the pace (running, shuffling), the tome's cover shuts over its pages, the gong
  and the herald stand on a raised floor, flames share their shapes, and an edited map of your
  own frees what it was built of.
- planOf answers from its cache without stringifying the config each time.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* refactor(maps): a style is a builder in a registry, and main.ts knows nothing of the castle

Review round 2, how it's put together:
- world/styles.ts maps each style (MAP_STYLES) to its builder; main.ts builds whatever a plan's
  style is. The herald, the room (wall thickness, roofed), the acoustics and dispose are World's,
  so there's no Castle type or cast left in main.ts.
- The plan carries the tables and the meeting table as checked, defaults filled in and the sides
  with seats worked out, so a builder reads them rather than working them out again.
- buildCastle is split into the shell, the dais, the tables and places, the council, the boards
  and the herald; each prop kind is an entry of an exhaustive PROPS table (a new kind won't
  compile without its builder).
- The herald's question and button, and the throne's label, are the map's (the castle keeps its
  'my liege'); the fire glow reuses the costumes' texture; planMap takes unknown.
- docs/maps/castle.json is the castle as a map of your own to copy (a test keeps it in step with
  castle.ts), and docs/maps.md has a map from nothing (tested to load), what's required, what
  happens when a map breaks, what it can't do yet, and adding a style.
- The toast says when a map of your own came back as well as when it broke.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* fix(maps): review round 2, the map changing under people

- The map changing while you're on your way to a floor (elevator, floor list, ladder) puts you
  where the new map has you come in once you land (or if the trip fails), instead of at your old
  spot, which could be outside the castle's walls. Arriving on the throne waits until you're there.
- A saved spot outside a hall of its own isn't come back to on a reload.
- Holding the basketball when the office goes: it leaves your hands, your character's and
  everyone else's; Minesweeper and the arcade cabinet stop.
- Refused a seat, you're on your feet for everyone, not left on the one you had.
- Workers sent out from the herald are remembered per floor and for half a minute, a second one
  goes to the next free seat rather than the same one, and the map changing forgets them.
- Only walking over to someone keeps walking through a floor switch; a walk to someone on a roof
  the map doesn't have stops.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* fix(maps): review round 2, the rest

- Ale at the cask no longer schedules the unused grinder in the past (a RangeError just after
  the sound starts); the pour just starts.
- N / Go to desk on a map of its own lands on open floor, not in a pillar; to a worker still on
  its way to the line, at its spot in the line.
- The round meeting table's collider fits inside its top.
- Court keeps the line's interactables (and the clickable models of who's in it) up to date when
  someone takes or leaves a spot, not every frame, and isn't raycast twice.
- Boards, their windows and the arcade name seats the way the map you're on does.
- A map change on a ride down to the garage is caught when the doors open.
- A map's benches and board agents have to fit in its hall too.
- Tests: the default dais, and every walk back to a seat as well as away from it.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* fix(castle): review round 3, the walkthrough

- K on the throne speaks to the Hand of the King whoever's in line, and the throne's hint says
  so (and how to get up). Hand him an issue card and he sends someone out for it. His hint says
  when the office is full or the budget's spent.
- A worker sent out by the Hand runs from beside him for everyone, not just whoever sent it
  (worker.spawn / WorkerInfo via: 'herald'); a second one goes to the next free seat.
- You stay on the throne through a floor switch and a reload (if nobody's taken it).
- No stained-glass window inside the hearth's chimney; the docs' example moves the issues board
  and its scribe to an empty bay; a lectern's rotY is documented the way it works.
- A map's line, herald, spawn and door have to be clear of its furniture.
- The toasts name a map as it's called (🏰 Castle), not 'the the barn'.
- 📚 Docs in the ☰ menu off the office, where there's no bookshelf.
- A reconnect's old connection doesn't refuse you your own seat; the fridge doesn't clunk in the
  hall; the elevator doors open when a map change catches you arriving; a failed trip off a roof
  the map doesn't have is tried again; the boards take the new map's seat names at once; Go to
  desk never lands outside a hall.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>

* Signs over the desks, and a back office to grow the floor into (#183)

L at any desk hangs a big sign from the ceiling over it ("Operations",
"Code cleanup"), in one of seven colors; it faces the chair and says the
same on its back while the desk across the pair has none of its own.

The north wall between the gong and the east corner can come down: E at
its "Room to grow" sign knocks through into a back office with a pair of
desks, and again for a second row (any deeper and it would stand in the
street behind the building). The same sign walls the last row back up
once nobody's working there. New workers and the task queue take its
desks before any bean bag comes out.

Each floor keeps its signs and how far it's built out in
.agent-office/floorplan.json. The nav grid, the dog, walks home, the
camera, the sky's indoor light and rain, sound and the building's outside
(each floor's bay, on posts down to the street) all know how far a floor
goes.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>

* Command palette (Ctrl+K) (#188)

* feat(palette): Ctrl+K command palette over workers, issues, PRs, services, boards, teammates and actions

Enter opens the selected item as clicking it in the office does; Shift+Enter walks
your avatar there first. Matching and ranking live in src/shared/palette.ts.

Closes #37

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

* Palette: walk over like walkTo does since the back office, maps and cars

Shift+Enter planned its route with the office nav and no back office
(wayTo without officeWing()), so it didn't route into a built-out back
office and used the office's grid on a map of its own. Route the same way
walkTick does. In a car there's no walking over, so it opens right away,
as on the roof. The free desk for "Hire a worker" now includes built back
office desks, and DESKS is imported again (main.ts stopped importing it).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

---------

Co-authored-by: hanson0291 <8506569+hanson0291@users.noreply.github.com>
Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Co-authored-by: webdevcody <webdevcody@gmail.com>

* Hire Grok and Muse Code workers at desks, with isolated homes and live status (#187)

Grok and Muse sit at desks like Claude Code, OpenCode and Codex: pick them in
the hire, queue and meeting windows, resume with R, and the desk shows working /
needs input / done from their command hooks.

Grok launches with --no-alt-screen --trust, a per-office GROK_HOME (login still
from ~/.grok/auth.json), and --resume <session-id>. Models come from `grok models`.

Muse launches with --trust-workspace, a per-office XDG tree under
.agent-office/muse-home (login linked from ~/.config/muse/auth.json), and
`muse resume <uuid>`. A follow-up prompt on resume is pasted into the TUI after
SessionStart, because muse resume does not take a prompt on argv. Model ids are
typed in (there is no muse models catalogue).

Neither writes into the user's Grok or Muse config. Token spend stays in the
worker terminal; --budget still tracks Claude Code only.

Co-authored-by: Steven Richards <steve@aimingupward.com>
Co-authored-by: webdevcody <webdevcody@gmail.com>

* Add DeepSeek Harness as a sixth provider, over ACP (#130)

DeepSeek Harness has no interactive terminal, so a DSH worker gets a DshSession that owns an ACP connection (dsh --profile acp, JSON-RPC over stdio) instead of a PTY, and renders what ACP sends into the worker's existing headless terminal: scrollback, sharing, search and reconnection keep working. Status comes straight off the protocol (prompt in flight, an open request_permission, a settled turn), approvals are answered from the terminal, and model/effort go through session/set_config_option.

Merged with main's Grok and Muse providers (#187): dsh joins agentProviders(), the wire union, validation, queue, meetings and restore, and the picker gets its own DSH model and effort fields. Docs live in docs/agents.md and friends since the README split.

Hardened after a security review: C1 controls and bidi overrides are stripped from everything written to the terminal; the markdown link pattern is linear; a new hire starts its own session instead of resuming the newest one in the checkout; Enter approves only an allow-once choice, a blank line in a paste is not an Enter, and the approval card shows the full command; a bad frame can't throw out of the stdout handler; DSH board agents authenticate office-queue; the terminal fits its window.

Co-authored-by: webdevcody <webdevcody@gmail.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* Add a minimal CLAUDE.md from how the project is actually worked on (#190)

Six rules taken from repeated prompts and corrections: PR by default,
the shared main checkout, the verify commands, docs with behaviour
changes, modal close/relock, and which PRs to merge.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>

* Hold the view still when a click gives the mouse back (#191)

Sending a worker home ends on a click on Send home. Closing the window
takes the mouse straight back for looking around, at the moment the
hand that clicked is often already moving on, so the pointer vanished
under it and the rest of the move swung the camera somewhere else.

A window closed with the mouse (Send home, ✕, the backdrop) still takes
it back at once, as closing by ✕ or Esc must, but the view now holds
still until the mouse has rested for 250 ms since it was taken (1 s at
most). Closing with a key (Esc, Enter) and clicking the office to look
around turn the view straight away as before.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>

* Let agents manage the office's workers: an agent-office MCP server and office-workers command (#192)

Until now only people (over the WebSocket) could hire workers or send them
home; agents could reach nothing but the board agents' task queue. Asking a
worker to "send all agents with merged PRs home" had no way to work.

- /office/workers on the loopback hook port, for any running worker on its
  own floor with its own token: GET lists every worker with its desk,
  status, task, branch, PR and whether its work landed (merged) or why it
  would stay; POST hires; POST /home sends named workers, or every one whose
  PR merged, home with a cleanup choice; POST /tell prompts an agent.
- bin/office-workers.js: the office-workers command (on every worker's
  PATH now) and, as `office-workers mcp`, a dependency-free stdio MCP server
  with list_workers, hire_worker, send_home and tell_worker. Claude Code gets
  it via --mcp-config (list_workers pre-allowed), Codex via -c mcp_servers
  with env_vars, OpenCode via OPENCODE_CONFIG_CONTENT.
- Floor.sendHome passes merged PRs' heads to kill, so a squash-merged branch
  whose remote was deleted counts as delivered and is cleaned up (the UI's
  send-home uses it too). landedWorkers is split into landedWork and
  notLeaving so explicit requests share the leave-on-merge rule.

Smoke-tested in a live office: a real Claude worker asked in plain words
sent the two merged workers home, deleting one worktree and branch and
keeping the other for its uncommitted change, and left the open-PR and
mid-turn workers alone. The MCP server was checked against real Claude
Code, Codex and OpenCode.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>

* Switching floors from outside takes you into that floor's elevator (#193)

Picking a floor from the project list keeps you on the same spot. Out on the
street, the balcony, the golf course or down in the garage, that spot looks the
same on every floor, so switching seemed to do nothing. From outside the office
it now rides the elevator and you step out of that floor's car. Indoors it
still keeps your spot.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>

* Loop the day and night outside every hour (#194)

The sky used to follow the office machine's real clock, so a day took a
day. Now the office's clock is sped up 24× for the sky (skyTime in
shared/sun.ts): midnight on the hour, sunrise around a quarter past, noon
at half past, sunset around a quarter to. It stays on today's date, so a
--city office still gets that place's season's day length. Every browser
keeps time by the office's clock, so everyone sees the same sky, and
⚙️ shows the sky's time of day.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>

* Scenic loop: drive out of town past a farm, pines, snowy mountains, a tunnel and the beach, and back (#196)

* Scenic loop: a country road off both ends of the street past a farm, pines, mountains and the beach

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* Scenic loop: docs, and tidy names

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>

* Workers whose worktree was deleted outside the office wait to be rebuilt instead of failing to start (#197)

* Workers whose worktree was deleted outside the office wait, marked lost, instead of failing to start

A worker whose worktree (or workspace) folder is gone no longer toasts
"Could not start …: working directory is gone" at every start. It stays
asleep with WorkerInfo.lost set, and whoever comes to its desk gets a
dialog to rebuild it on its branch (or origin's copy, or afresh from its
base), rebuild every lost worker at once, or send it home.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* Docs, and office-workers says when a worker's worktree was deleted

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>

* Give the mouse back at once when a click closes a window (#198)

Closing the castle's "Hand of the King: send out a worker" window with
✕ (or Cancel, or any window closed with a click) took the mouse back
straight away, but #191 then held the view still until the mouse had
rested for 250 ms, for up to a second. Looking around right after
closing never rests, so the view ignored the mouse for the whole second
and it felt like the mouse was gone.

The hold is now just the flick of the hand that clicked: 150 ms at most
once the mouse is taken, or until it has rested for 100 ms. In headless
Chrome on the throne, the view turns about 0.2 s after a ✕ click,
down from 1.1 s; Esc is unchanged.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>

* Castle dungeon: the Kingsguard drags workers sent home down to a cell, where they waste away to bones (#203)

In the castle, a worker sent home no longer walks out of the doors. The
Kingsguard, on watch in a new dungeon under the hall, runs up the stairs to
its seat, waits while it packs, marches it down with a hand on its shoulder
and throws it into a cell. Everyone sent home is kept for good (per floor, in
.agent-office/jail.json) and wastes away: thinner and paler until it starves
to death after a day, then rotting down to a skeleton over half a day more.
You can walk down the stairs and look in on them; the living ones mutter.

How a map sees its workers off is now data: MapConfig.sendHome is a script of
steps (pack, fetch, say, walk, jail, leave, wait, return) with an escort, and
MapConfig.dungeon lays out the vault, its stairs and its cells, so another map
can script its own send-home without touching the code (docs/maps.md). A map
without one still walks workers out of the door.

- shared/maps/dungeon.ts: checks and plans the dungeon and the script, the
  cells' seats, how far a prisoner has wasted away, routes between levels
- server/jail.ts: each floor's prisoners; worker.remove carries the jail
- client: dungeon builder, send-home script runner, the jail's prisoners,
  starving / dead / skeleton looks for Worker, a guard for Person, cell door
  sounds, camera kept inside the vault, gloomy torchlight down there

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>

---------

Co-authored-by: Web Dev Cody <webdevcody@gmail.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: Shirone <kacperlachowiczwp.pl@wp.pl>
Co-authored-by: Hanson Software LLC <enzo_owner63@yahoo.com>
Co-authored-by: hanson0291 <8506569+hanson0291@users.noreply.github.com>
Co-authored-by: Steven Richards <69158610+SkippySteve@users.noreply.github.com>
Co-authored-by: Steven Richards <steve@aimingupward.com>
Co-authored-by: AmirBN <36451043+amirbenasr@users.noreply.github.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Drive the Lambos and Ferraris in the garage

1 participant