Deploy to Dokploy: deploy/dokploy.sh, SSH tunnel only, everything on a volume - #180
Merged
Merged
Conversation
…a volume 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>
Both add a deploy target to the same README lists, container comments and team/protocol docs: name Fly.io and Dokploy side by side.
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>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What
deploy/dokploy.shputs Agent Office on a Dokploy server, the same waydeploy/railway.sh(#176) puts it on Railway:deploy/container/Dockerfile./datafor everything the office keeps, so deploys and restarts lose nothing.--ssh-port). There's no domain; the office stays on127.0.0.1:4600inside. Teammates getssh -L 4600:localhost:4600 ssh://office@<server>:2222from 👥 Invite teammates ordeploy/dokploy.sh invite.open,update,restart,service,invite/uninvite/team,status,ssh,logs,reset-password,destroy. Options:--server <name>(a Dokploy remote server; required on Dokploy Cloud) and--ssh-host.docs/dokploy.md, a "Deploy to Dokploy" section in the README, and Dokploy added next to Railway in the invite and removal instructions.Dokploy details the script works around (from Dokploy's source, v0.30.x)
.gitignored files and local secrets (.env*,.claude/, keys,pw.txt). The real index isn't touched.chmodsbin/*.jsanddeploy/container/*.shbefore building. Railway builds are unaffected.upsets stop-first for both updates and rollbacks.upkeeps variables you add on Dokploy's Environment tab, and turns offcreateEnvFile. Otherwise Dokploy would write the Claude token into the build folder as.env.destroydeletes the application, then the volume (dockerVolume.removeVolume), then the project, but only if nothing else is in it.upcan't quietly forget a running office.Testing
Run against a real Dokploy v0.30.8, installed with its own install script in a nested Docker VM on this Mac, using macOS's
/bin/bash3.2:up: project, app, volume and port created; first build about 5 min; claim page and password through the tunnel.status,ssh,invitewith a key file, and the teammate's tunnel:ssh://office@…:portreaches the office, and a command sent over the teammate's key only runs the tunnel-only login. A screenshot of the office's 👥 panel showed the Dokploy address and no AWS "allow their IP" note.update: the old task stopped before the new one started, and data on the volume was kept.restart,logs(follows new lines, no duplicates) andreset-password(new password, old one refused with a 401).up: reused everything, kept a user env var, a comment and build args, and added the token. The office got the values unquoted.--server(checked before anything is created), a bad--ssh-port, a missing office.destroy: a full office (application, volume and project all gone, local state removed), a half-made one, and one with nothing behind it.npm run typecheckandnpm test(248 pass).Not tested: Dokploy remote servers / Dokploy Cloud (
--server), a real public IP with a cloud firewall in front, andgh auth logininside the container (the same code asrailway.sh).🤖 Generated with Claude Code