agent-office-studio 0.2.0 → 0.3.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/CHANGELOG.md +19 -1
- package/README.md +1 -1
- package/bin/agentoffice.js +65 -5
- package/dist/assets/index-D5IzEWKU.css +32 -0
- package/dist/assets/index-DgXF8Kqx.js +4457 -0
- package/dist/index.html +7 -2
- package/docs/README.md +1 -0
- package/docs/configuration.md +3 -2
- package/docs/getting-started.md +4 -1
- package/docs/guides/troubleshooting.md +13 -2
- package/docs/leaderboard.md +8 -6
- package/docs/linked-offices.md +81 -0
- package/docs/phone.md +4 -0
- package/docs/security.md +34 -2
- package/package.json +3 -2
- package/server/changes.js +4 -2
- package/server/chief.js +18 -2
- package/server/guests.js +391 -0
- package/server/index.js +285 -18
- package/server/leaderboard.js +63 -14
- package/server/links.js +350 -0
- package/server/supervise.js +2 -0
- package/server/voice.js +3 -1
- package/dist/assets/index-CozPk6R9.js +0 -4457
- package/dist/assets/index-i7sU9SN-.css +0 -32
package/dist/index.html
CHANGED
|
@@ -25,12 +25,13 @@
|
|
|
25
25
|
})();
|
|
26
26
|
</script>
|
|
27
27
|
<title>Agent Office</title>
|
|
28
|
-
<script type="module" crossorigin src="/assets/index-
|
|
29
|
-
<link rel="stylesheet" crossorigin href="/assets/index-
|
|
28
|
+
<script type="module" crossorigin src="/assets/index-DgXF8Kqx.js"></script>
|
|
29
|
+
<link rel="stylesheet" crossorigin href="/assets/index-D5IzEWKU.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
37
|
<h1>Agent Office</h1>
|
|
@@ -39,6 +40,9 @@
|
|
|
39
40
|
<button type="button" class="theme-btn" id="lbBtn" aria-label="Leaderboard" aria-haspopup="true" aria-expanded="false" title="Leaderboard">
|
|
40
41
|
<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
42
|
</button>
|
|
43
|
+
<button type="button" class="theme-btn" id="linkBtn" aria-label="Linked offices" aria-haspopup="true" aria-expanded="false" title="Linked offices">
|
|
44
|
+
<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>
|
|
45
|
+
</button>
|
|
42
46
|
<button type="button" class="theme-btn" id="acctBtn" aria-label="Claude access" aria-haspopup="true" aria-expanded="false" title="Claude access">
|
|
43
47
|
<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
48
|
</button>
|
|
@@ -48,6 +52,7 @@
|
|
|
48
52
|
</span>
|
|
49
53
|
<div class="theme-pop" id="themePop" role="dialog" aria-label="Appearance" hidden></div>
|
|
50
54
|
<div class="theme-pop lb-pop" id="lbPop" role="dialog" aria-label="Leaderboard" hidden></div>
|
|
55
|
+
<div class="theme-pop link-pop" id="linkPop" role="dialog" aria-label="Linked offices" hidden></div>
|
|
51
56
|
<div class="theme-pop acct-pop" id="acctPop" role="dialog" aria-label="Claude access" hidden></div>
|
|
52
57
|
</div>
|
|
53
58
|
<div class="level">
|
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
|
package/docs/configuration.md
CHANGED
|
@@ -10,8 +10,9 @@ voice toggle, team profiles, theme — are made in the office and remembered.
|
|
|
10
10
|
|---|---|---|
|
|
11
11
|
| `OFFICE_ROOT` | the folder above this one; with `agentoffice`, the folder you run it from | its subfolders are the projects |
|
|
12
12
|
| `OFFICE_DATA` | `data/` here; with `agentoffice`, `~/.agent-office` | where the office keeps its state |
|
|
13
|
-
| `PORT` | `4317` | |
|
|
13
|
+
| `PORT` | `4317` | with `agentoffice`, a free port is chosen if this is taken (or `--port <n>`); one you set is used as given |
|
|
14
14
|
| `OFFICE_ALLOWED_HOSTS` | none | extra hostnames to accept, comma-separated; see [On your phone](phone.md) |
|
|
15
|
+
| `OFFICE_SHARE_JOB_TITLES` | unset | `1` lets linked offices see your agents' job titles, not just that they're busy |
|
|
15
16
|
|
|
16
17
|
## Claude
|
|
17
18
|
|
|
@@ -46,7 +47,7 @@ voice toggle, team profiles, theme — are made in the office and remembered.
|
|
|
46
47
|
|
|
47
48
|
| Env | Default | |
|
|
48
49
|
|---|---|---|
|
|
49
|
-
| `OFFICE_LEADERBOARD_URL` |
|
|
50
|
+
| `OFFICE_LEADERBOARD_URL` | the hosted board | address of a different leaderboard service; see [The leaderboard](leaderboard.md) |
|
|
50
51
|
| `OFFICE_TERMINAL` | unset | `1` opens the in-app terminal |
|
|
51
52
|
| `OFFICE_TERMINAL_SHELL` | your `$SHELL` | |
|
|
52
53
|
| `OFFICE_TERMINAL_MAX` | `4` | sessions at once |
|
package/docs/getting-started.md
CHANGED
|
@@ -18,7 +18,10 @@ npm install
|
|
|
18
18
|
npm start # builds, then serves on http://localhost:4317
|
|
19
19
|
```
|
|
20
20
|
|
|
21
|
-
Open
|
|
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.
|
|
9
|
-
|
|
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
|
package/docs/leaderboard.md
CHANGED
|
@@ -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
|
|
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
|
-
##
|
|
57
|
+
## Using a different service
|
|
57
58
|
|
|
58
|
-
|
|
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`).
|
|
65
|
-
|
|
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
|
@@ -22,6 +22,10 @@ OFFICE_ALLOWED_HOSTS=my-mac.tail1234.ts.net npm start
|
|
|
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
30
|
projects with full autonomy, and — if `OFFICE_TERMINAL=1` — get a shell on
|
|
27
31
|
this machine. Only expose it on a tailnet that is just you. Never put it on
|
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
|
|
|
@@ -37,10 +44,35 @@ command you could.
|
|
|
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
|
|
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
78
|
Off unless `OFFICE_TERMINAL=1`. A session needs a single-use ticket from the
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "agent-office-studio",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.3.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,7 +45,7 @@
|
|
|
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": {
|
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
|
-
|
|
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
|
}
|
package/server/chief.js
CHANGED
|
@@ -90,7 +90,8 @@ function ask(context, { bin, model, timeoutMs, env, images }) {
|
|
|
90
90
|
let out = '', err = '';
|
|
91
91
|
child.stdout.on('data', (d) => (out += d));
|
|
92
92
|
child.stderr.on('data', (d) => (err += d));
|
|
93
|
-
|
|
93
|
+
let timedOut = false;
|
|
94
|
+
const timer = setTimeout(() => { timedOut = true; child.kill('SIGKILL'); }, timeoutMs);
|
|
94
95
|
child.on('error', (e) => { clearTimeout(timer); reject(e); });
|
|
95
96
|
child.on('close', () => {
|
|
96
97
|
clearTimeout(timer);
|
|
@@ -99,7 +100,7 @@ function ask(context, { bin, model, timeoutMs, env, images }) {
|
|
|
99
100
|
const decision = r.structured_output || JSON.parse(r.result);
|
|
100
101
|
resolve({ ...decision, cost: r.total_cost_usd || 0 });
|
|
101
102
|
} catch {
|
|
102
|
-
reject(new Error(
|
|
103
|
+
reject(new Error(whyNot(out, err, streamed, timedOut, timeoutMs)));
|
|
103
104
|
}
|
|
104
105
|
});
|
|
105
106
|
child.stdin.on('error', () => {});
|
|
@@ -119,6 +120,21 @@ function message(context, images) {
|
|
|
119
120
|
}
|
|
120
121
|
|
|
121
122
|
// The decision is in the last line of the stream, the one the run ends on.
|
|
123
|
+
// Why a routing call gave no decision, in words the boss can act on. claude
|
|
124
|
+
// reports most failures (not signed in, out of credit, a model the account
|
|
125
|
+
// can't use) inside its JSON on stdout with nothing on stderr, so read that
|
|
126
|
+
// before falling back.
|
|
127
|
+
function whyNot(out, err, streamed, timedOut, timeoutMs) {
|
|
128
|
+
if (timedOut) return `The chief took longer than ${Math.round(timeoutMs / 1000)} seconds to answer. Check that \`claude -p "hi"\` answers in a terminal.`;
|
|
129
|
+
let r = null;
|
|
130
|
+
try { r = streamed ? result(out) : JSON.parse(out); } catch {}
|
|
131
|
+
const said = String(r?.is_error ? r.result || r.subtype || '' : '').trim();
|
|
132
|
+
if (said) return `claude said: ${said.split('\n')[0].slice(0, 300)}`;
|
|
133
|
+
const last = err.trim().split('\n').pop();
|
|
134
|
+
if (last) return last.slice(0, 300);
|
|
135
|
+
return out.trim() ? 'The chief could not decide.' : 'claude exited without answering. Check that `claude -p "hi"` answers in a terminal.';
|
|
136
|
+
}
|
|
137
|
+
|
|
122
138
|
function result(out) {
|
|
123
139
|
for (const line of out.trim().split('\n').reverse()) {
|
|
124
140
|
try {
|