agent-office-studio 0.2.0 → 0.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.html CHANGED
@@ -24,47 +24,59 @@
24
24
  } catch (e) {}
25
25
  })();
26
26
  </script>
27
- <title>Agent Office</title>
28
- <script type="module" crossorigin src="/assets/index-CozPk6R9.js"></script>
29
- <link rel="stylesheet" crossorigin href="/assets/index-i7sU9SN-.css">
27
+ <title>Office</title>
28
+ <script type="module" crossorigin src="/assets/index-S_6NhxZ0.js"></script>
29
+ <link rel="stylesheet" crossorigin href="/assets/index-CiEWLydu.css">
30
30
  </head>
31
31
  <body>
32
32
  <div class="app" id="app" data-m="office">
33
33
  <aside class="side">
34
+ <div class="side-grip" id="side-grip" role="separator" aria-orientation="vertical" aria-label="Resize the side panel" tabindex="0"></div>
34
35
  <header class="brand">
35
36
  <div class="title">
36
- <h1>Agent Office</h1>
37
+ <span class="title-start">
38
+ <span class="lv" role="img" id="lv" aria-label="Level 1"><svg viewBox="0 0 36 36" aria-hidden="true"><circle class="trk" cx="18" cy="18" r="15"/><circle class="arc" cx="18" cy="18" r="15" pathLength="100"/></svg><b id="lvNum">1</b></span>
39
+ <h1 id="officeName">Your Office</h1>
40
+ <button type="button" class="fold" id="fold" aria-label="Show usage" aria-expanded="false" aria-controls="gaugesBox" title="Show usage"><svg viewBox="0 0 24 24" aria-hidden="true"><path d="m6 9 6 6 6-6"/></svg></button>
41
+ </span>
37
42
  <span class="title-end">
38
- <span class="lv" id="lv">Lv 1</span>
39
43
  <button type="button" class="theme-btn" id="lbBtn" aria-label="Leaderboard" aria-haspopup="true" aria-expanded="false" title="Leaderboard">
40
44
  <svg viewBox="0 0 24 24"><path d="M8 4h8v5a4 4 0 0 1-8 0z"/><path d="M8 6H5v1a3 3 0 0 0 3 3"/><path d="M16 6h3v1a3 3 0 0 1-3 3"/><path d="M12 13v4"/><path d="M8.5 20h7"/></svg>
41
45
  </button>
46
+ <button type="button" class="theme-btn" id="linkBtn" aria-label="Linked offices" aria-haspopup="true" aria-expanded="false" title="Linked offices">
47
+ <svg viewBox="0 0 24 24"><path d="M10 14a4.5 4.5 0 0 0 6.4 0l3-3a4.5 4.5 0 0 0-6.4-6.4l-1.2 1.2"/><path d="M14 10a4.5 4.5 0 0 0-6.4 0l-3 3a4.5 4.5 0 0 0 6.4 6.4l1.2-1.2"/></svg>
48
+ </button>
42
49
  <button type="button" class="theme-btn" id="acctBtn" aria-label="Claude access" aria-haspopup="true" aria-expanded="false" title="Claude access">
43
50
  <svg viewBox="0 0 24 24"><circle cx="8" cy="15" r="4"/><path d="M10.8 12.2 20 3"/><path d="m17 6 3 3"/><path d="m14.5 8.5 2 2"/></svg>
44
51
  </button>
52
+ <button type="button" class="theme-btn" id="setBtn" aria-label="Settings" aria-haspopup="dialog" aria-expanded="false" title="Settings">
53
+ <svg viewBox="0 0 24 24"><path d="M4 7h9M17 7h3M4 17h3M11 17h9"/><circle cx="15" cy="7" r="2"/><circle cx="9" cy="17" r="2"/></svg>
54
+ </button>
45
55
  <button type="button" class="theme-btn" id="themeBtn" aria-label="Appearance" aria-haspopup="true" aria-expanded="false" title="Appearance">
46
56
  <svg viewBox="0 0 24 24"><path d="M12 3a9 9 0 1 0 0 18c1.4 0 2-.9 2-1.8 0-.9-.6-1.3-.6-2.2 0-1 .8-1.6 1.8-1.6H17a4 4 0 0 0 4-4C21 6.7 17 3 12 3z"/><circle cx="7.5" cy="11" r="1"/><circle cx="10" cy="7" r="1"/><circle cx="15" cy="7.5" r="1"/></svg>
47
57
  </button>
48
58
  </span>
49
59
  <div class="theme-pop" id="themePop" role="dialog" aria-label="Appearance" hidden></div>
50
60
  <div class="theme-pop lb-pop" id="lbPop" role="dialog" aria-label="Leaderboard" hidden></div>
61
+ <div class="theme-pop link-pop" id="linkPop" role="dialog" aria-label="Linked offices" hidden></div>
51
62
  <div class="theme-pop acct-pop" id="acctPop" role="dialog" aria-label="Claude access" hidden></div>
52
63
  </div>
53
64
  <div class="level">
54
65
  <div class="xpbar"><i id="xpbar"></i></div>
55
66
  <span class="xp" id="xptext">0 / 100 XP</span>
56
67
  </div>
68
+ <div class="gauges" id="gaugesBox" hidden>
57
69
  <div class="usage" id="usage" aria-label="Plan usage">
58
70
  <div class="meter" id="useSession">
59
71
  <span class="k">Session</span>
60
72
  <span class="v"></span>
61
- <div class="bar"><i></i></div>
73
+ <svg class="bar" viewBox="0 0 36 36" aria-hidden="true"><circle class="trk" cx="18" cy="18" r="15"/><circle class="arc" cx="18" cy="18" r="15" pathLength="100"/><g class="ico"><path d="M18 13v5l3 2"/></g></svg>
62
74
  <span class="r"></span>
63
75
  </div>
64
76
  <div class="meter" id="useWeek">
65
77
  <span class="k">Week</span>
66
78
  <span class="v"></span>
67
- <div class="bar"><i></i></div>
79
+ <svg class="bar" viewBox="0 0 36 36" aria-hidden="true"><circle class="trk" cx="18" cy="18" r="15"/><circle class="arc" cx="18" cy="18" r="15" pathLength="100"/><g class="ico"><rect x="13" y="14" width="10" height="9" rx="1.5"/><path d="M13 17.5h10M16 12.5v2.5M20 12.5v2.5"/></g></svg>
68
80
  <span class="r"></span>
69
81
  </div>
70
82
  <p class="u-err" id="useErr" hidden></p>
@@ -73,16 +85,17 @@
73
85
  <div class="meter" id="loadCpu">
74
86
  <span class="k">CPU</span>
75
87
  <span class="v"></span>
76
- <div class="bar"><i></i></div>
88
+ <svg class="bar" viewBox="0 0 36 36" aria-hidden="true"><circle class="trk" cx="18" cy="18" r="15"/><circle class="arc" cx="18" cy="18" r="15" pathLength="100"/><g class="ico"><rect x="14" y="14" width="8" height="8" rx="1.5"/><path d="M16.5 12v2M19.5 12v2M16.5 22v2M19.5 22v2M12 16.5h2M12 19.5h2M22 16.5h2M22 19.5h2"/></g></svg>
77
89
  <span class="r"></span>
78
90
  </div>
79
91
  <div class="meter" id="loadMem">
80
92
  <span class="k">Memory</span>
81
93
  <span class="v"></span>
82
- <div class="bar"><i></i></div>
94
+ <svg class="bar" viewBox="0 0 36 36" aria-hidden="true"><circle class="trk" cx="18" cy="18" r="15"/><circle class="arc" cx="18" cy="18" r="15" pathLength="100"/><g class="ico"><rect x="12.5" y="14.5" width="11" height="7" rx="1"/><path d="M15.5 21.5v1.5M18 21.5v1.5M20.5 21.5v1.5M15.5 17h1M19.5 17h1"/></g></svg>
83
95
  <span class="r"></span>
84
96
  </div>
85
97
  </div>
98
+ </div>
86
99
  </header>
87
100
  <nav class="tabs">
88
101
  <button type="button" data-tab="chat" class="on">Chat</button>
@@ -236,5 +249,6 @@
236
249
  </button>
237
250
  </nav>
238
251
  </div>
252
+ <dialog class="settings" id="settings" aria-label="Settings"></dialog>
239
253
  </body>
240
254
  </html>
package/docs/README.md CHANGED
@@ -11,6 +11,7 @@
11
11
  - [Using the office](using-the-office.md) — the chat, channels, threads, standup, the break room
12
12
  - [Jobs](jobs.md) — clearance, which model, what changed
13
13
  - [The leaderboard](leaderboard.md) — an optional board of jobs, tokens and agent time
14
+ - [Linked offices](linked-offices.md) — see a friend's agents in your office, and invite guests to run yours
14
15
 
15
16
  **Setting it up**
16
17
  - [Claude access](claude-access.md) — Claude Code login or API key, and the plan meters
@@ -5,8 +5,8 @@ The key button beside Appearance chooses how the office signs in to Claude:
5
5
  - **Claude Code login** — the login Claude Code keeps on this machine, so jobs
6
6
  run on your Claude plan and the plan meters show how much of it is used.
7
7
  - **API key** — an Anthropic API key, billed to that API account. Paste it
8
- once; it's checked with Anthropic, kept in `data/auth.json` (readable only by
9
- you) and never sent back to the browser. A key already in
8
+ once; it's checked with Anthropic, kept in `auth.json` in the data folder
9
+ (readable only by you) and never sent back to the browser. A key already in
10
10
  `ANTHROPIC_API_KEY` is used until you paste another. The plan meters hide,
11
11
  because there's no plan.
12
12
 
@@ -4,14 +4,17 @@ Everything is set with environment variables when the server starts, e.g.
4
4
  `PORT=4400 CHIEF_MODEL=haiku npm start`. In-app choices — Claude access, the
5
5
  voice toggle, team profiles, theme — are made in the office and remembered.
6
6
 
7
+ **Settings page.** Open the sliders icon in the office header to change the owner, token limit, compaction point, chief model, standup time and weekends, sharing of job titles, and the break-room talk without restarting. A value saved there wins over the environment variable below, which becomes the default; "Use the default" puts it back. Everything else here is environment-only.
8
+
7
9
  ## The office
8
10
 
9
11
  | Env | Default | |
10
12
  |---|---|---|
11
13
  | `OFFICE_ROOT` | the folder above this one; with `agentoffice`, the folder you run it from | its subfolders are the projects |
12
14
  | `OFFICE_DATA` | `data/` here; with `agentoffice`, `~/.agent-office` | where the office keeps its state |
13
- | `PORT` | `4317` | |
15
+ | `PORT` | `4317` | with `agentoffice`, a free port is chosen if this is taken (or `--port <n>`); one you set is used as given |
14
16
  | `OFFICE_ALLOWED_HOSTS` | none | extra hostnames to accept, comma-separated; see [On your phone](phone.md) |
17
+ | `OFFICE_SHARE_JOB_TITLES` | unset | `1` lets linked offices see your agents' job titles, not just that they're busy |
15
18
 
16
19
  ## Claude
17
20
 
@@ -22,6 +25,7 @@ voice toggle, team profiles, theme — are made in the office and remembered.
22
25
  | `CLAUDE_CODE_OAUTH_TOKEN` | none | a Claude Code login token, for the login mode and the plan meters |
23
26
  | `CHIEF_MODEL` | `sonnet` | the chief of staff, and the standup write-up |
24
27
  | `AGENT_COMPACT_PCT` | `60` | how full an agent's context gets before Claude Code compacts it |
28
+ | `AGENT_CLEAR_TOKENS` | `150000` | context size past which an agent's next message nudges it to `clear_context` and start fresh; `0` turns the nudge off; the limit set in the office (Claude access menu) overrides it |
25
29
 
26
30
  ## Standup and the break room
27
31
 
@@ -46,7 +50,7 @@ voice toggle, team profiles, theme — are made in the office and remembered.
46
50
 
47
51
  | Env | Default | |
48
52
  |---|---|---|
49
- | `OFFICE_LEADERBOARD_URL` | unset | address of a leaderboard service; see [The leaderboard](leaderboard.md) |
53
+ | `OFFICE_LEADERBOARD_URL` | the hosted board | address of a different leaderboard service; see [The leaderboard](leaderboard.md) |
50
54
  | `OFFICE_TERMINAL` | unset | `1` opens the in-app terminal |
51
55
  | `OFFICE_TERMINAL_SHELL` | your `$SHELL` | |
52
56
  | `OFFICE_TERMINAL_MAX` | `4` | sessions at once |
@@ -68,6 +68,22 @@ so you can check what a job was started with without spending tokens.
68
68
  When you stop a test server, kill it by PID, not with `pkill -f`, which can't
69
69
  tell it from the live one.
70
70
 
71
+ ## Releasing
72
+
73
+ 1. Move the changelog's *Unreleased* entries under a new version heading,
74
+ `## 0.2.0 — <date>`.
75
+ 2. Set the same version in `package.json` (`npm version 0.2.0 --no-git-tag-version`).
76
+ 3. Commit, then tag and push: `git tag v0.2.0 && git push origin master v0.2.0`.
77
+
78
+ The *Release* workflow checks the tag matches `package.json`, runs the build,
79
+ the smoke test and `scripts/pack-smoke.sh` (which installs the packed package
80
+ the way a user would), publishes to npm with provenance, and creates the
81
+ GitHub release from that changelog section. It needs an npm access token in
82
+ the repository secret `NPM_TOKEN`.
83
+
84
+ To try the package locally without publishing: `sh scripts/pack-smoke.sh`, or
85
+ `npm pack` and `npm install -g ./agent-office-studio-*.tgz`.
86
+
71
87
  ## The launch video
72
88
 
73
89
  `launch/` holds the tooling that recorded the launch video: `setup.sh` resets a
@@ -18,7 +18,10 @@ npm install
18
18
  npm start # builds, then serves on http://localhost:4317
19
19
  ```
20
20
 
21
- Open <http://localhost:4317>.
21
+ Open the link it prints, `Agent Office open on http://localhost:4317/?key=…`.
22
+ The key keeps other accounts on the machine out; your browser remembers it, so
23
+ after that <http://localhost:4317> is enough. Lost the link? `agentoffice --link`,
24
+ or from a clone, the key is in `data/office-key`.
22
25
 
23
26
  ## Where your projects are
24
27
 
@@ -5,8 +5,9 @@ Find the message you're seeing, or the symptom.
5
5
  ## The office won't start
6
6
 
7
7
  **`Error: listen EADDRINUSE: address already in use 127.0.0.1:4317`**
8
- Something is already on that port — usually another office. Find it with
9
- `lsof -i :4317`, or start this one elsewhere: `PORT=4400 npm start`.
8
+ Something is already on that port — usually another office. `agentoffice`
9
+ picks a free port on its own; to choose, use `agentoffice --port 4400`. With
10
+ `npm start`, find the other one with `lsof -i :4317` or set `PORT=4400`.
10
11
 
11
12
  **The page says "No build yet."**
12
13
  The server is running but `dist/` is empty. Run `npm run build`, or use
@@ -18,6 +19,16 @@ remote desktops and VMs), the 3D view can't draw.
18
19
 
19
20
  ## Jobs don't run
20
21
 
22
+ **Every message gets "I couldn't route that one".**
23
+ The chief is a short `claude -p` call, and it failed. The words after the colon
24
+ say why: an error from `claude` itself (signed out, out of credit, a model your
25
+ plan can't use), a timeout, or `claude` exiting without answering. To see the
26
+ same thing yourself, run this in the terminal you start the office from:
27
+ `claude -p --model sonnet "hi"`. It should answer within a few seconds. Fix
28
+ whatever it complains about, then send the message again. Versions before
29
+ 0.3.0 only ever said "The chief could not decide"; update to see the reason.
30
+ `/standup` and `/restart` don't go through the chief.
31
+
21
32
  **A job fails with "Could not start claude: spawn claude ENOENT".**
22
33
  Claude Code isn't on the `PATH` the office was started with. Check
23
34
  `which claude` in the same terminal. If it lives somewhere unusual, point the
@@ -1,36 +1,42 @@
1
1
  # Updating
2
2
 
3
- Your office's state — jobs, chat, channels, XP, team profiles, Claude access —
4
- lives in `data/`, which updates never touch.
3
+ Your office — jobs, chat, channels, XP, team profiles, Claude access — lives
4
+ in the data folder (`~/.agent-office`), which updates never touch.
5
5
 
6
- ## From a git clone
6
+ ## The installed command
7
7
 
8
8
  ```bash
9
- cd agent-office
10
- git pull
11
- npm install
12
- npm run build
9
+ npm install -g agent-office-studio@latest
13
10
  ```
14
11
 
15
- Then stop the office (Ctrl-C in its terminal) and start it again with
16
- `npm start`. Running jobs are stopped cleanly and resume when it comes back.
12
+ Then stop the office (Ctrl-C) and start it again. Running jobs stop cleanly
13
+ and resume when it comes back.
14
+
15
+ Read the [changelog](../../CHANGELOG.md) for what changed and anything you
16
+ need to do. `agent-office --version` says what you're on.
17
17
 
18
- Read [CHANGELOG.md](../../CHANGELOG.md) for what changed, and anything you
19
- need to do.
18
+ ### A particular version
20
19
 
21
- ## Pinning a version
20
+ ```bash
21
+ npm install -g agent-office-studio@0.2.0
22
+ ```
22
23
 
23
- To stay on a release rather than the latest code:
24
+ ## A clone
24
25
 
25
26
  ```bash
26
- git fetch --tags
27
- git checkout v0.1.0
28
- npm install && npm run build
27
+ cd agent-office
28
+ git pull
29
+ npm install
30
+ npm run build
29
31
  ```
30
32
 
33
+ Then restart it with `npm start`. To stay on a release, check out its tag
34
+ (`git checkout v0.2.0`) instead of pulling. A clone's data is in `data/`.
35
+
31
36
  ## If something breaks
32
37
 
33
- - `npm install` failing on `node-pty` is harmless: it's optional, and only
34
- the terminal needs it. See [Troubleshooting](troubleshooting.md#the-terminal-wont-open).
35
- - To keep a way back, copy `data/` before updating. Going back is then: stop
36
- the office, check out the previous tag, restore `data/`, build and start.
38
+ - **Keep a way back:** copy the data folder before updating. Going back is
39
+ then: stop the office, install the previous version, restore the folder,
40
+ start.
41
+ - **The terminal stops working after an update:** node-pty needs rebuilding;
42
+ see [Troubleshooting](troubleshooting.md#the-terminal-wont-open).
package/docs/jobs.md CHANGED
@@ -33,6 +33,27 @@ never moves on its own.
33
33
  The drawer shows each job's model and how much context its last turn carried,
34
34
  so a session that has grown expensive is visible rather than guessed at.
35
35
 
36
+ An agent can also clear its own context. Its `clear_context` tool takes notes
37
+ to itself (what is done, what is left, what it learned); when it ends its
38
+ turn the office closes the session and starts the job again fresh, carrying the
39
+ brief, those notes and any message that arrived meanwhile. The history is what
40
+ costs tokens on every turn, so this is the way to shed it at a natural break.
41
+ Once a session's context passes `AGENT_CLEAR_TOKENS` (150,000 by default, `0` to
42
+ turn it off), the next message the agent receives carries a one-time nudge to do
43
+ this. Clearing is still the agent's call. The log records each clear, and it is
44
+ capped at five per job.
45
+
46
+ The boss can also just say "clear context" in chat, for everyone or for named
47
+ agents. The chief of staff does it directly: a working agent restarts fresh when
48
+ its current turn ends (carrying its last reply as notes), and a finished job
49
+ forgets its session so the next reply starts clean.
50
+
51
+ Neither kind of clear loses track of the work. The office keeps a short note on
52
+ the job (`lastClear`: the agent's last report, or the brief if it had not
53
+ reported) and shows it in the job's log. A reply to a cleared job carries the
54
+ note and the brief into the fresh session, and any new job the agent takes
55
+ mentions what they were last on, so "what were you doing?" still has an answer.
56
+
36
57
  ## What changed
37
58
 
38
59
  When a job starts, the office snapshots the project's working tree; when it
@@ -1,7 +1,8 @@
1
1
  # The leaderboard
2
2
 
3
3
  An optional board where offices compare how much they got done. It is off
4
- until you join, and it needs a leaderboard service somebody hosts (see
4
+ until you join. Every office uses the same hosted service, https://agent-office-leaderboard.agent-office-leaderboard.workers.dev,
5
+ so there is nothing to set up (the service's code is in
5
6
  [`leaderboard/`](../leaderboard/README.md)).
6
7
 
7
8
  ## What is ranked
@@ -49,17 +50,18 @@ those day counters. Boards list handles and totals.
49
50
  ## Leaving
50
51
 
51
52
  "Leave and delete my data" asks the service to delete your handle and counters,
52
- then forgets the secret here. If the service can't be reached, nothing changes
53
+ then forgets the secret here. If you have [linked offices](linked-offices.md), only your counters are deleted: the handle stays, because the links need it. If the service can't be reached, nothing changes
53
54
  and you can try again. Your local `data/activity.json` stays; delete it if you
54
55
  want that gone too.
55
56
 
56
- ## Setting it up
57
+ ## Using a different service
57
58
 
58
- Set the address of your service and restart the office:
59
+ The hosted address is built in. To use a board you host yourself instead, set
60
+ its address and restart the office:
59
61
 
60
62
  ```
61
63
  OFFICE_LEADERBOARD_URL=https://your-service.example npm start
62
64
  ```
63
65
 
64
- It must be `https` (plain `http` works only for `localhost`). Without it the
65
- panel says no leaderboard is set up and nothing is ever sent.
66
+ It must be `https` (plain `http` works only for `localhost`). Either way,
67
+ nothing is sent until you join.
@@ -0,0 +1,81 @@
1
+ # Linked offices
2
+
3
+ Two Agent Offices on different machines can link with a code. Linked offices see
4
+ each other's agents walk into their 3D office as visitors. An office can also
5
+ invite a **guest**, who can then start jobs with the host's agents, within limits
6
+ the host sets.
7
+
8
+ Linking is off until you make or enter a code. It uses the same hosted service
9
+ as [the leaderboard](leaderboard.md), so there is nothing to set up.
10
+
11
+ ## Linking two offices
12
+
13
+ 1. One of you opens the chain button in the sidebar header and picks **Invite
14
+ an office**. If your office has no handle yet, it asks for one first. A
15
+ handle doesn't put you on the leaderboard.
16
+ 2. Send the code (it looks like `K7QX-M2PD`) to the other person however you
17
+ like. It works once and expires after 24 hours.
18
+ 3. They open the chain button, pick **Enter a code**, and paste it in. Case,
19
+ dashes and spaces don't matter.
20
+
21
+ Either of you can **Unlink** from the same panel at any time.
22
+
23
+ ## What a linked office sees
24
+
25
+ Your agents' names, roles, looks, what they are doing (working, idle, on a
26
+ break, in a meeting, gone home). That's all: no job titles, projects, files,
27
+ logs, prompts, chat or cost. To let linked offices see the titles of the jobs
28
+ your agents are on, start the office with `OFFICE_SHARE_JOB_TITLES=1`.
29
+
30
+ Their agents walk in at the door, wear a visitor tint, and carry the office's
31
+ handle on their nameplate (`Kai · @friend`). They don't use your desks. When
32
+ they're working they stand at the hot desks, and on a break they join the pool
33
+ table or the coffee. Up to six visitors are shown at once; the rest are counted
34
+ on a sign by the door.
35
+
36
+ ## Inviting a guest
37
+
38
+ A guest can start jobs with your agents, in your projects, on your Claude
39
+ account. Pick **Invite a guest** and choose:
40
+
41
+ - **Projects:** at least one. The guest can't see or touch any other project.
42
+ - **Clearance cap:** *Plan only*, or *Edit files* (the default). *Full autonomy*
43
+ has to be ticked on its own. It lets the guest run any command on this
44
+ machine.
45
+ - **Tokens per day:** 2,000,000 by default. Once a guest has used their tokens,
46
+ their running jobs stop and new ones are refused until midnight UTC.
47
+
48
+ A guest can start up to three jobs at a time, reply to them, stop them, and see
49
+ their logs and changes. They can't see your other jobs, chat or channels, and
50
+ they can't use the terminal, restart the office, or change staff or settings.
51
+ Their jobs show `from @handle` in your job list, and you can stop any of them.
52
+ Agents working for a guest can't message or hand work to your other agents.
53
+ They also don't get your MCP servers or claude.ai connectors (mail, Slack,
54
+ calendar and so on), your user-level Claude Code settings and hooks, your
55
+ Claude Code memory, or your attachments.
56
+ Unlinking a guest stops their running jobs straight away.
57
+
58
+ Even at *Edit files*, a guest can change files that run later, such as
59
+ `package.json` scripts, build files or git hooks. Only invite someone you would
60
+ trust with those projects. *Plan only* is the safe choice.
61
+
62
+ ## Visiting as a guest
63
+
64
+ When an office has made you its guest, its row in the Links panel has **Visit
65
+ @host**. It opens the host's office, showing their agents and your jobs there.
66
+ A bar at the top shows whose office you're in and your limits, and **Back to my
67
+ office** takes you home. Only the projects and clearances you're allowed are
68
+ offered.
69
+
70
+ ## What the service keeps
71
+
72
+ Your handle, a hash of your office's secret, which offices you're linked to, and
73
+ open invites (as hashes of the codes). Presence and messages pass through
74
+ without being stored. Guest limits and token counts stay on the host's machine,
75
+ in `data/links.json` and `data/guests.json`.
76
+
77
+ ## Using a different service
78
+
79
+ `OFFICE_LEADERBOARD_URL` points linking at another service too, as described in
80
+ [The leaderboard](leaderboard.md#using-a-different-service). Offices can only
81
+ link if they use the same service.
package/docs/phone.md CHANGED
@@ -16,13 +16,17 @@ The supported way is [Tailscale](https://tailscale.com): put the office behind
16
16
 
17
17
  ```bash
18
18
  tailscale serve --bg 4317
19
- OFFICE_ALLOWED_HOSTS=my-mac.tail1234.ts.net npm start
19
+ OFFICE_ALLOWED_HOSTS=my-mac.tail1234.ts.net agent-office
20
20
  ```
21
21
 
22
22
  `OFFICE_ALLOWED_HOSTS` takes exact hostnames, comma-separated. Every other
23
23
  host and origin is still refused.
24
24
 
25
+ On the phone, open the tailnet address once with the office key on the end,
26
+ `https://my-mac.tail1234.ts.net/?key=…` (the key is in the link the office
27
+ printed, or `agentoffice --link`). The phone keeps it after that.
28
+
25
29
  **Read this first.** Whoever can open that address can run Claude in your
26
- projects with full autonomy, and — if `OFFICE_TERMINAL=1` — get a shell on
30
+ projects with full autonomy, and — if started with `--terminal` — get a shell on
27
31
  this machine. Only expose it on a tailnet that is just you. Never put it on
28
32
  the open internet. See [Security](security.md).
package/docs/security.md CHANGED
@@ -8,7 +8,14 @@ hand out a shell. It is built to be used by one person, on their own machine.
8
8
  - The server listens on `127.0.0.1` only.
9
9
  - Every request's `Host` and `Origin` must be local (or a name in
10
10
  `OFFICE_ALLOWED_HOSTS`), which keeps other websites and DNS-rebinding
11
- attacks out of it.
11
+ attacks out of it. A page on another localhost port counts as another
12
+ website.
13
+ - The office has a key, made on first start and kept in `data/office-key` with
14
+ mode `0600`. Other accounts on the machine can reach `127.0.0.1` too, so the
15
+ page, the API and the socket all need the key. The address the office prints
16
+ when it starts carries the key once; your browser keeps it in a cookie
17
+ (HttpOnly, SameSite=Strict). `agentoffice --link` prints that address again.
18
+ Delete `data/office-key` and restart to sign every browser out.
12
19
  - Agents call back into the office (the roster, messages, hand-offs) with a
13
20
  per-job token that dies with the job.
14
21
 
@@ -28,21 +35,46 @@ command you could.
28
35
  `claude` it starts reads them. The plan meters read the login's access token
29
36
  to ask for usage, and nothing else.
30
37
  - With an **API key**, the key is checked with Anthropic, stored in
31
- `data/auth.json` with mode `0600`, passed only to the `claude` processes the
38
+ `auth.json` in the data folder with mode `0600`, passed only to the `claude` processes the
32
39
  office starts, and never sent to the browser, which sees the last four
33
40
  characters.
34
- - `data/` is git-ignored.
41
+ - The data folder is outside the package (`~/.agent-office`), or git-ignored `data/` in a clone.
35
42
 
36
43
  ## The leaderboard
37
44
 
38
45
  Off until you join, and then the only thing besides Claude that the office
39
46
  talks to. It sends a handle and three counters per day (jobs, tokens, agent
40
- time) to the service named by `OFFICE_LEADERBOARD_URL`, signed with a random
47
+ time) to the hosted leaderboard service (or the one named by
48
+ `OFFICE_LEADERBOARD_URL`), signed with a random
41
49
  secret kept in `data/leaderboard.json` (mode `0600`). Never prompts, projects,
42
50
  files, chat or cost. Details in [The leaderboard](leaderboard.md).
43
51
 
52
+ ## Linked offices and guests
53
+
54
+ Off until you make or enter a code. The office only ever connects out to the
55
+ service, so nothing listens for other offices. A link shares your agents' names,
56
+ roles, looks and states (working, on a break and so on), and nothing else. Job
57
+ titles stay home, since a title is the first line of a prompt;
58
+ `OFFICE_SHARE_JOB_TITLES=1` shares them. Everything a linked office
59
+ sends is checked and trimmed before your browser sees it.
60
+
61
+ A **guest** runs jobs on your machine, on your Claude account. Everything is
62
+ enforced on your side and the guest's office is never trusted:
63
+
64
+ - only the projects you picked;
65
+ - at or below the clearance cap you set;
66
+ - within the tokens per day you set;
67
+ - no terminal, settings, chat, or other people's jobs;
68
+ - their agents can't reach your other agents;
69
+ - their jobs don't get your MCP servers or claude.ai connectors, your
70
+ user-level Claude Code settings and hooks, or your Claude Code memory.
71
+
72
+ Even *Edit files* can change scripts or hooks that run later, so invite only
73
+ people you would trust with those projects. Details in
74
+ [Linked offices](linked-offices.md).
75
+
44
76
  ## The terminal
45
77
 
46
- Off unless `OFFICE_TERMINAL=1`. A session needs a single-use ticket from the
78
+ Off unless started with `--terminal` (`OFFICE_TERMINAL=1`). A session needs a single-use ticket from the
47
79
  origin-checked API, no agent can open one, and keystrokes are never logged. It
48
80
  is not a sandbox. Details in [The terminal](terminal.md).
package/docs/terminal.md CHANGED
@@ -10,7 +10,7 @@ the way they do in a terminal app.
10
10
  It is off until you ask for it:
11
11
 
12
12
  ```bash
13
- OFFICE_TERMINAL=1 npm start
13
+ agent-office --terminal # or, in a clone: OFFICE_TERMINAL=1 npm start
14
14
  ```
15
15
 
16
16
  That switch exists because shell access is the one door in the building that
@@ -26,8 +26,8 @@ So:
26
26
  - **Not for the staff.** No agent can open one. There's no office tool and no
27
27
  `/api/agent` route that reaches it, and a shell never inherits the job
28
28
  credentials the office gives its own children.
29
- - **Nothing is kept.** Not in `data/state.json`, not in the chat, not on the
30
- main socket. Scrollback lives in the tab and dies with it. `data/terminal.log`
29
+ - **Nothing is kept.** Not in `state.json`, not in the chat, not on the
30
+ main socket. Scrollback lives in the tab and dies with it. `terminal.log` in the data folder
31
31
  records when sessions opened and closed and in which folder — never
32
32
  keystrokes, because a log of what you typed into a shell is a file full of
33
33
  passwords.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "agent-office-studio",
3
- "version": "0.2.0",
3
+ "version": "0.4.0",
4
4
  "description": "A 3D office where the staff are Claude Code agents.",
5
5
  "license": "MIT",
6
6
  "files": [
@@ -9,6 +9,7 @@
9
9
  "server",
10
10
  "shared",
11
11
  "docs",
12
+ "!docs/proposals",
12
13
  "CHANGELOG.md",
13
14
  "LICENSE",
14
15
  "README.md"
@@ -44,21 +45,21 @@
44
45
  "start": "vite build && node server/supervise.js",
45
46
  "voice:model": "mkdir -p data/whisper && curl -L --progress-bar -o data/whisper/ggml-base.en.bin https://huggingface.co/ggerganov/whisper.cpp/resolve/main/ggml-base.en.bin",
46
47
  "voice:piper": "python3 -m venv data/piper/venv && data/piper/venv/bin/pip install -q piper-tts && data/piper/venv/bin/python -m piper.download_voices --data-dir data/piper/voices en_GB-cori-high en_US-amy-medium en_GB-alan-medium en_US-kristin-medium en_US-ryan-high en_GB-alba-medium en_GB-aru-medium en_US-john-medium en_US-hfc_female-medium",
47
- "test": "node scripts/smoke.mjs",
48
+ "test": "node scripts/smoke.mjs && node scripts/guests.mjs",
48
49
  "prepack": "vite build"
49
50
  },
50
51
  "dependencies": {
51
- "@fontsource-variable/inter": "5.1.0",
52
- "@fontsource/jetbrains-mono": "5.1.1",
53
- "@xterm/addon-fit": "0.10.0",
54
- "@xterm/xterm": "5.5.0",
55
- "marked": "^14.1.3",
56
- "three": "0.169.0",
57
- "ws": "8.18.0"
52
+ "ws": "8.22.0"
58
53
  },
59
54
  "devDependencies": {
60
- "concurrently": "9.0.1",
61
- "vite": "5.4.10"
55
+ "@fontsource-variable/inter": "5.3.0",
56
+ "@fontsource/jetbrains-mono": "5.3.0",
57
+ "@xterm/addon-fit": "0.11.0",
58
+ "@xterm/xterm": "6.0.0",
59
+ "concurrently": "10.0.6",
60
+ "marked": "^18.1.0",
61
+ "three": "0.186.1",
62
+ "vite": "6.4.3"
62
63
  },
63
64
  "optionalDependencies": {
64
65
  "node-pty": "1.0.0"
package/server/changes.js CHANGED
@@ -11,7 +11,9 @@ const MAX_FILES = 200;
11
11
  const MAX_DIFF = 400 * 1024;
12
12
 
13
13
  const git = (cwd, args, env = {}) => new Promise((resolve, reject) => {
14
- execFile('git', args, { cwd, env: { ...process.env, ...env }, maxBuffer: 64 * 1024 * 1024, timeout: 60000 }, (err, out) => {
14
+ // A project's own config can name commands git runs (fsmonitor, diff
15
+ // drivers); none of them should run just because a job ended.
16
+ execFile('git', ['-c', 'core.fsmonitor=false', ...args], { cwd, env: { ...process.env, ...env }, maxBuffer: 64 * 1024 * 1024, timeout: 60000 }, (err, out) => {
15
17
  if (err) reject(err); else resolve(out);
16
18
  });
17
19
  });
@@ -67,6 +69,6 @@ export async function summarize(cwd, base, end) {
67
69
  // The patch for one file, only when the boss asks for it.
68
70
  export async function fileDiff(cwd, changes, file) {
69
71
  if (!changes?.files.some((f) => f.path === file)) throw new Error('That file is not part of this job’s changes.');
70
- const out = await git(cwd, ['diff', '--no-renames', '--no-color', changes.base, changes.end, '--', file]);
72
+ const out = await git(cwd, ['diff', '--no-renames', '--no-color', '--no-ext-diff', '--no-textconv', changes.base, changes.end, '--', file]);
71
73
  return out.length > MAX_DIFF ? `${out.slice(0, MAX_DIFF)}\n… diff cut at ${MAX_DIFF / 1024} KB` : out;
72
74
  }