thinkpool-pair 0.7.327 → 0.7.330
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/README.md +236 -199
- package/bridge-service.sh +67 -0
- package/bridge.mjs +30 -14
- package/claude-session.mjs +2 -4
- package/cross-terminal.mjs +35 -4
- package/cumulative-event-relay.mjs +6 -1
- package/package.json +2 -1
- package/terminal-name.mjs +15 -4
- package/thinkpool-capabilities.json +7 -7
package/README.md
CHANGED
|
@@ -1,235 +1,272 @@
|
|
|
1
|
-
# thinkpool-pair
|
|
1
|
+
# `thinkpool-pair`
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
3
|
+
`thinkpool-pair` connects coding agents running on a machine you control to a
|
|
4
|
+
Thinkpool Code room. Both people in the room can watch and steer the visible
|
|
5
|
+
agent lanes from a browser or phone.
|
|
6
6
|
|
|
7
|
-
|
|
8
|
-
|
|
7
|
+
The bridge makes outbound connections to Thinkpool’s Supabase project. It does
|
|
8
|
+
not require an inbound port, tunnel, or public IP.
|
|
9
9
|
|
|
10
|
-
|
|
11
|
-
> you control (a cheap VM, a home server, a Raspberry Pi) so the agent stays live
|
|
12
|
-
> with your laptop closed. It's outbound-only — no ports, no public IP. See
|
|
13
|
-
> [**Run it in the cloud**](#run-it-in-the-cloud-remote-host--vm--container) below.
|
|
10
|
+
## Requirements
|
|
14
11
|
|
|
15
|
-
|
|
12
|
+
- Node.js 18 or newer
|
|
13
|
+
- A Thinkpool account and Code session
|
|
14
|
+
- At least one supported runtime available on the host:
|
|
15
|
+
- Claude
|
|
16
|
+
- Codex
|
|
17
|
+
- Hermes
|
|
18
|
+
- The provider login or API credentials required by that runtime
|
|
19
|
+
|
|
20
|
+
## Start the bridge
|
|
21
|
+
|
|
22
|
+
Run the launcher from the project directory the agents should use:
|
|
16
23
|
|
|
17
24
|
```bash
|
|
18
|
-
|
|
19
|
-
node bridge.mjs <ROOM> # picks an installed agent, shared into room <ROOM>
|
|
25
|
+
npx thinkpool-pair@latest
|
|
20
26
|
```
|
|
21
27
|
|
|
22
|
-
|
|
23
|
-
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
The
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
- **Boot-persistent service** — survive reboot/logout too. Installs the right
|
|
37
|
-
native service for your OS (launchd on macOS, systemd `--user` on Linux, a
|
|
38
|
-
Startup-folder script on Windows):
|
|
39
|
-
```bash
|
|
40
|
-
npx thinkpool-pair@latest install-service <ROOM> -- claude # set and forget
|
|
41
|
-
npx thinkpool-pair@latest uninstall-service <ROOM> # remove it
|
|
42
|
-
```
|
|
43
|
-
The service runs `npx thinkpool-pair@latest`, so it **auto-updates** — new
|
|
44
|
-
versions apply on the next restart, no re-install. (Linux: run
|
|
45
|
-
`loginctl enable-linger $USER` once to keep it running after logout.)
|
|
46
|
-
- **Watchdog** — if the realtime channel wedges for >60s, the bridge exits so the
|
|
47
|
-
supervisor/service restarts a clean process. Brief network blips reconnect on
|
|
48
|
-
their own.
|
|
49
|
-
|
|
50
|
-
Structured Claude sessions persist their scrollback + session id to
|
|
51
|
-
`~/.thinkpool-pair/<ROOM>/` and **resume on restart** (live context if recent),
|
|
52
|
-
so none of the above loses your place.
|
|
53
|
-
|
|
54
|
-
### Codex App Server driver
|
|
55
|
-
|
|
56
|
-
Structured Codex lanes use the long-lived App Server transport (streaming prose,
|
|
57
|
-
mid-turn steering, shared approval and question cards, and native image inputs)
|
|
58
|
-
when the installed Codex CLI is in the bridge's tested protocol range. To force
|
|
59
|
-
the stable `codex exec --json` fallback, set the kill switch before starting the
|
|
60
|
-
bridge:
|
|
61
|
-
|
|
62
|
-
```bash
|
|
63
|
-
export TP_CODEX_APP_SERVER=0
|
|
64
|
-
```
|
|
65
|
-
|
|
66
|
-
The bridge performs the initialize handshake before creating a thread and falls
|
|
67
|
-
back to `codex exec` when App Server is disabled, outside the tested version
|
|
68
|
-
range, or cannot initialize before a turn. It never retries a prompt via the
|
|
69
|
-
fallback after `turn/start` has been accepted, preventing duplicate work.
|
|
70
|
-
|
|
71
|
-
Your first agent runs **attached** — use it exactly like your normal terminal,
|
|
72
|
-
every byte mirrors to the web. The web's **"+ New terminal"** spawns additional
|
|
73
|
-
**headless** terminals here (same directory, same env), driven entirely from
|
|
74
|
-
the room. One bridge, many terminals.
|
|
75
|
-
|
|
76
|
-
### Built-in viewport capture
|
|
77
|
-
|
|
78
|
-
Structured Claude and Codex lanes have bridge-owned visual QA tools, even when
|
|
79
|
-
their own sandbox cannot bind localhost or launch Chrome:
|
|
80
|
-
|
|
81
|
-
- `preview_start` serves a built directory inside that lane's workspace
|
|
82
|
-
(`dist` by default; it must contain `index.html`).
|
|
83
|
-
- `preview_capture` returns exact desktop (1440×900) and mobile (390×844)
|
|
84
|
-
screenshots to the agent as verification evidence. It creates no transcript
|
|
85
|
-
artifact by default. Pass `card: true` only for an intentional user-facing
|
|
86
|
-
mockup/Design deliverable; a settled complete pair is then queued after the
|
|
87
|
-
agent's final response. Loading, empty, and public-auth fallback shells are
|
|
88
|
-
rejected before a card can be created.
|
|
89
|
-
- `preview_inspect` returns rendered DOM text, document size, and optional
|
|
90
|
-
selector geometry at either viewport.
|
|
91
|
-
- `preview_stop` releases the preview port.
|
|
92
|
-
|
|
93
|
-
The bridge launches the host's Chrome/Chromium lazily. Set `TP_BROWSER_PATH`
|
|
94
|
-
if it is installed somewhere non-standard. Preview files are read-only, roots
|
|
95
|
-
cannot escape the lane workspace (including through symlinks), and page network
|
|
96
|
-
requests are restricted to the preview server's exact loopback origin. The
|
|
97
|
-
tools never execute a caller-supplied command or open an arbitrary URL.
|
|
98
|
-
|
|
99
|
-
## Run it in the cloud (remote host / VM / container)
|
|
100
|
-
|
|
101
|
-
The bridge connects **outbound** to Supabase — no inbound ports, no public IP, no
|
|
102
|
-
tunnel. So a cloud VM, container, devcontainer, or remote dev box can stream its
|
|
103
|
-
Claude Code into the room exactly like a laptop. Run the same command there:
|
|
104
|
-
|
|
105
|
-
```bash
|
|
106
|
-
export ANTHROPIC_API_KEY=sk-ant-... # headless auth (no Keychain login)
|
|
107
|
-
cd /path/to/your/repo
|
|
108
|
-
npx thinkpool-pair@latest <ROOM> -- claude # structured Claude, no TTY needed
|
|
109
|
-
```
|
|
110
|
-
|
|
111
|
-
- **Auth** — two ways to sign in on a box with no browser. Both are your own
|
|
112
|
-
account on your own hardware, so both are fine:
|
|
113
|
-
- **API key** — `export ANTHROPIC_API_KEY=sk-ant-...` from your Anthropic
|
|
114
|
-
Console. Simplest, pay-per-token, cleanest cost tracking.
|
|
115
|
-
- **Your Claude subscription** — a server has no browser, so mint a long-lived
|
|
116
|
-
token *on a machine that does*: run `claude setup-token` there (prints a
|
|
117
|
-
~1-year `CLAUDE_CODE_OAUTH_TOKEN`), then `export CLAUDE_CODE_OAUTH_TOKEN=...`
|
|
118
|
-
on the box. Uses your existing Pro/Max plan — no per-token bill.
|
|
119
|
-
|
|
120
|
-
The Agent SDK reads either one automatically and runs its bundled `claude`, so
|
|
121
|
-
you don't need Claude Code installed separately. (Note: this is *you* running
|
|
122
|
-
*your* login on *your* machine. Handing a subscription token to a third-party
|
|
123
|
-
host to run for you is what Anthropic's terms forbid — that's why a future
|
|
124
|
-
"we host it for you" option would require an API key, not your subscription.)
|
|
125
|
-
- **Use `@latest`**: a bare `npx thinkpool-pair` reuses npx's local cache and can
|
|
126
|
-
run a stale version across restarts. `@latest` (and the service below) always
|
|
127
|
-
re-resolve the newest publish.
|
|
128
|
-
- **No TTY required**: the agent picker auto-selects and raw-mode is skipped when
|
|
129
|
-
there's no terminal, so it runs fine under CI / a service / `nohup`.
|
|
130
|
-
- **Always-on + auto-update**: `npx thinkpool-pair@latest install-service <ROOM> -- claude`
|
|
131
|
-
installs a systemd `--user` unit (auto-restart + boot-persistent). The bridge
|
|
132
|
-
polls npm for new publishes and **self-restarts at the next idle moment** into
|
|
133
|
-
the new version — the structured session resumes, so an away/unattended box
|
|
134
|
-
upgrades itself with nobody losing their place. On a server, also run
|
|
135
|
-
`loginctl enable-linger $USER` so it survives logout.
|
|
136
|
-
Tune with `THINKPOOL_PAIR_UPDATE_INTERVAL` (poll seconds, default 1800) and
|
|
137
|
-
`THINKPOOL_PAIR_UPDATE_IDLE` (idle seconds before restart, default 90). Live
|
|
138
|
-
auto-update applies to the launchd/systemd service tiers; bare `--supervise`
|
|
139
|
-
respawns the same version, so it updates only on the next manual/boot restart.
|
|
140
|
-
- **Security**: anyone with the room code can drive the agent (shell-trust by
|
|
141
|
-
design) — on a server that's a real shell, so treat the room code like a secret.
|
|
142
|
-
|
|
143
|
-
## How it works
|
|
144
|
-
|
|
145
|
-
`bridge.mjs` ⇄ **Supabase realtime** (`tpcode:<ROOM>`) ⇄ web `xterm`:
|
|
146
|
-
- `bridge` — announce: installed agents + live terminals.
|
|
147
|
-
- `pty-out` — terminal bytes (base64, per-terminal) → web clients render them.
|
|
148
|
-
- `pty-in` — keystrokes/prompts from the web → written to that terminal's PTY.
|
|
149
|
-
- `term-open` / `term-close` / `term-exit` — web-driven terminal lifecycle.
|
|
150
|
-
- `replay-request` / `pty-replay` — each terminal keeps a rolling ~120 KB
|
|
151
|
-
scrollback buffer for live recovery. Thinkpool also stores the cleaned reader
|
|
152
|
-
transcript and room events so members can reopen the room. It does not store
|
|
153
|
-
a copy of the repository or the raw PTY byte stream.
|
|
154
|
-
- `resize` — web viewport size → headless PTYs only (the attached terminal
|
|
155
|
-
follows your own TTY).
|
|
156
|
-
|
|
157
|
-
## Inspect the privacy boundary
|
|
158
|
-
|
|
159
|
-
Run a local report before pairing a repository:
|
|
28
|
+
The launcher shows the linked account, detected runtimes, current directory,
|
|
29
|
+
and background-service state. On first use it asks to link the machine in a
|
|
30
|
+
browser. Then choose one of:
|
|
31
|
+
|
|
32
|
+
- **Serve all my sessions** — runs in the current terminal until `Ctrl-C`.
|
|
33
|
+
- **Always-on background service** — starts at login/reboot and restarts after a
|
|
34
|
+
crash. The normal install is pinned to the confirmed package version; use the
|
|
35
|
+
launcher’s **Restart & update bridge** action when a new version should land.
|
|
36
|
+
|
|
37
|
+
Back in the room, open the runtime/model you want for each terminal.
|
|
38
|
+
|
|
39
|
+
## Account and service commands
|
|
40
|
+
|
|
41
|
+
Link the machine without opening the full launcher:
|
|
160
42
|
|
|
161
43
|
```bash
|
|
162
|
-
npx thinkpool-pair@latest
|
|
44
|
+
npx thinkpool-pair@latest login
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
Install or remove the account-wide background service:
|
|
48
|
+
|
|
49
|
+
```bash
|
|
50
|
+
npx thinkpool-pair@latest install-service
|
|
51
|
+
npx thinkpool-pair@latest restart-service
|
|
52
|
+
npx thinkpool-pair@latest uninstall-service
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
`install-service` is pinned by default so a later bad npm publish cannot
|
|
56
|
+
silently replace a working unattended bridge. Opt into tracking `@latest` only
|
|
57
|
+
when that trade-off is intentional:
|
|
58
|
+
|
|
59
|
+
```bash
|
|
60
|
+
npx thinkpool-pair@latest install-service --auto-update
|
|
163
61
|
```
|
|
164
62
|
|
|
165
|
-
|
|
166
|
-
present under `~/.thinkpool-pair`, known outbound service domains, what
|
|
167
|
-
Thinkpool stores remotely, and the limits of the report. It never prints a
|
|
168
|
-
provider key, refresh token, or bridge private key.
|
|
63
|
+
Service implementation by platform:
|
|
169
64
|
|
|
170
|
-
|
|
65
|
+
- macOS: LaunchAgent
|
|
66
|
+
- Linux: systemd user service; run `loginctl enable-linger "$USER"` once if it
|
|
67
|
+
must survive logout
|
|
68
|
+
- Windows: Startup-folder command file
|
|
171
69
|
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
that format:
|
|
70
|
+
On Windows the startup entry launches at login; update it from the launcher and
|
|
71
|
+
relaunch the bridge window to apply the new version.
|
|
175
72
|
|
|
176
|
-
|
|
177
|
-
- **Any other model via a translating gateway** — put **LiteLLM** (or a proxy of
|
|
178
|
-
your own) in front and run **GPT, Gemini, Llama, DeepSeek, or a local model**;
|
|
179
|
-
it presents the Anthropic Messages API while calling whatever you pick.
|
|
73
|
+
## Project directories
|
|
180
74
|
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
> Code expects Anthropic request semantics, so non-Anthropic models aren't
|
|
184
|
-
> supported through the native endpoint" — and this bridge spawns the `claude`
|
|
185
|
-
> CLI. To reach a non-Anthropic model, use a translating gateway (LiteLLM),
|
|
186
|
-
> not OpenRouter's Anthropic endpoint.
|
|
75
|
+
The account bridge serves sessions from their bound project directories. Set a
|
|
76
|
+
default for new/unbound sessions:
|
|
187
77
|
|
|
188
78
|
```bash
|
|
189
|
-
npx thinkpool-pair@latest
|
|
79
|
+
npx thinkpool-pair@latest set-default-dir /path/to/project
|
|
190
80
|
```
|
|
191
81
|
|
|
192
|
-
|
|
82
|
+
Bind one room explicitly when needed:
|
|
193
83
|
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
84
|
+
```bash
|
|
85
|
+
npx thinkpool-pair@latest bind <ROOM> /path/to/project
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
Room codes are treated as secrets because room members can drive agents that
|
|
89
|
+
have access to the bound project and the host permissions granted to that lane.
|
|
200
90
|
|
|
201
|
-
|
|
202
|
-
> OpenAI-style `/v1/chat/completions` won't work directly — that's exactly what a
|
|
203
|
-
> gateway is for. Avoid LiteLLM 1.82.7 / 1.82.8 (compromised releases); use a
|
|
204
|
-
> current version.
|
|
91
|
+
## Runtimes and providers
|
|
205
92
|
|
|
206
|
-
|
|
207
|
-
|
|
93
|
+
| Runtime | Authentication/provider behavior |
|
|
94
|
+
|---|---|
|
|
95
|
+
| Claude | Uses the host’s normal Anthropic login by default. A registered Anthropic-compatible provider can be selected for a lane. |
|
|
96
|
+
| Codex | Uses the host’s Codex/OpenAI login. Custom bridge providers are not wired to Codex lanes. |
|
|
97
|
+
| Hermes | Uses an isolated Thinkpool ACP profile. Set it up explicitly before the first Hermes lane. |
|
|
98
|
+
|
|
99
|
+
The launcher’s **Settings → Provider** flow configures the host default for
|
|
100
|
+
Claude-compatible lanes. The equivalent CLI commands are:
|
|
208
101
|
|
|
209
102
|
```bash
|
|
103
|
+
npx thinkpool-pair@latest provider
|
|
210
104
|
npx thinkpool-pair@latest provider anthropic
|
|
105
|
+
npx thinkpool-pair@latest provider custom --base <url> --token <key> --model <id>
|
|
211
106
|
```
|
|
212
107
|
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
108
|
+
The custom endpoint must implement the Anthropic Messages API. A raw
|
|
109
|
+
OpenAI-compatible chat-completions endpoint is not enough; place a translating
|
|
110
|
+
gateway in front of it or use the runtime that natively owns that provider.
|
|
216
111
|
|
|
217
|
-
|
|
112
|
+
Set up an isolated Hermes profile with one of:
|
|
218
113
|
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
114
|
+
```bash
|
|
115
|
+
npx thinkpool-pair@latest setup-hermes --clone
|
|
116
|
+
npx thinkpool-pair@latest setup-hermes --clean
|
|
117
|
+
```
|
|
118
|
+
|
|
119
|
+
`--clone` copies the active Hermes provider/configuration into the isolated
|
|
120
|
+
profile. `--clean` creates the isolation boundary without copying credentials.
|
|
121
|
+
|
|
122
|
+
## Direct single-room mode
|
|
123
|
+
|
|
124
|
+
The account-wide launcher is the normal path. A direct room process is still
|
|
125
|
+
available for debugging or a deliberately single-room host:
|
|
126
|
+
|
|
127
|
+
```bash
|
|
128
|
+
npx thinkpool-pair@latest <ROOM> -- claude
|
|
129
|
+
npx thinkpool-pair@latest <ROOM> -- codex
|
|
130
|
+
npx thinkpool-pair@latest <ROOM> -- hermes
|
|
131
|
+
```
|
|
224
132
|
|
|
225
|
-
|
|
133
|
+
Add `--headless` when the process should be driven only from the room. Direct
|
|
134
|
+
room services use the same command with the room code:
|
|
226
135
|
|
|
227
136
|
```bash
|
|
228
|
-
|
|
137
|
+
npx thinkpool-pair@latest install-service <ROOM> -- claude
|
|
138
|
+
npx thinkpool-pair@latest uninstall-service <ROOM>
|
|
229
139
|
```
|
|
230
140
|
|
|
231
|
-
|
|
232
|
-
|
|
141
|
+
## Cloud or always-on host
|
|
142
|
+
|
|
143
|
+
The bridge can run on a VM, dev box, home server, or container because its room
|
|
144
|
+
connection is outbound-only. Use the same launcher/service commands and provide
|
|
145
|
+
the runtime’s headless credentials through the host’s secret manager.
|
|
146
|
+
|
|
147
|
+
For Claude, either an Anthropic API key or a valid Claude Code OAuth token can
|
|
148
|
+
authenticate the runtime. Treat subscription/OAuth tokens as personal secrets;
|
|
149
|
+
do not hand them to a third-party host.
|
|
150
|
+
|
|
151
|
+
## What crosses the boundary
|
|
152
|
+
|
|
153
|
+
The bridge and web app deliberately split responsibility:
|
|
154
|
+
|
|
155
|
+
- Agent processes, repository access, provider credentials, and raw terminal
|
|
156
|
+
bytes remain on the bridge machine.
|
|
157
|
+
- Thinkpool relays room events and terminal output to authorized room members.
|
|
158
|
+
- Thinkpool stores the cleaned reader transcript and room events so members can
|
|
159
|
+
reopen the room.
|
|
160
|
+
- Thinkpool does not store a copy of the repository or a second copy of the raw
|
|
161
|
+
PTY byte stream.
|
|
162
|
+
- Voice dictation is an application API path and uses the configured
|
|
163
|
+
transcription service; it is not a local-only bridge operation.
|
|
164
|
+
|
|
165
|
+
Inspect the host-side boundary before pairing a sensitive repository:
|
|
166
|
+
|
|
167
|
+
```bash
|
|
168
|
+
npx thinkpool-pair@latest privacy-report
|
|
169
|
+
```
|
|
170
|
+
|
|
171
|
+
The report lists configured project directories, local bridge records, known
|
|
172
|
+
outbound domains, remote storage categories, and the report’s limits. It does
|
|
173
|
+
not print provider keys, refresh tokens, or bridge private keys.
|
|
174
|
+
|
|
175
|
+
Structured session state is stored below `~/.thinkpool-pair/` so an agent lane
|
|
176
|
+
can resume after a bridge restart. Protect that directory like other local
|
|
177
|
+
developer credentials and session state.
|
|
178
|
+
|
|
179
|
+
## Visual verification tools
|
|
180
|
+
|
|
181
|
+
Structured Claude and Codex lanes can use bridge-owned preview tools even when
|
|
182
|
+
their own sandbox cannot bind a port or launch a browser:
|
|
183
|
+
|
|
184
|
+
- `preview_start` serves a built directory inside the lane worktree.
|
|
185
|
+
- `preview_capture` records exact desktop and mobile renders.
|
|
186
|
+
- `preview_inspect` reads rendered DOM text and geometry.
|
|
187
|
+
- `preview_stop` releases the preview server.
|
|
188
|
+
|
|
189
|
+
Preview roots are read-only, cannot escape the lane worktree, and accept only
|
|
190
|
+
the preview server’s loopback origin for page requests.
|
|
191
|
+
|
|
192
|
+
## Optional bridge environment variables
|
|
193
|
+
|
|
194
|
+
| Variable | Purpose |
|
|
195
|
+
|---|---|
|
|
196
|
+
| `TP_NAME` | Label this bridge in the room |
|
|
197
|
+
| `TP_PAIR_ROOT` | Override the local state directory |
|
|
198
|
+
| `TP_SUPABASE_URL` / `TP_SUPABASE_ANON` | Override the built-in Thinkpool project endpoint/public key |
|
|
199
|
+
| `TP_ANTHROPIC_BASE_URL` / `TP_ANTHROPIC_AUTH_TOKEN` / `TP_ANTHROPIC_MODEL` | Override the Claude-compatible provider without writing provider config |
|
|
200
|
+
| `TP_CONTEXT_MAX` | Override the context-window size used by the room meter |
|
|
201
|
+
| `TP_BROWSER_PATH` | Point preview capture at a non-standard Chrome/Chromium binary |
|
|
202
|
+
| `TP_FLOW_OFF=1` | Disable Flow dispatch on this bridge |
|
|
203
|
+
| `TP_SPAWN_OFF=1` | Disable agent-spawned worker lanes |
|
|
204
|
+
| `TP_PAIRBUS_OFF=1` | Disable the paired-room bus |
|
|
205
|
+
| `TP_CROSSROOM_OFF=1` | Disable cross-session reach |
|
|
206
|
+
|
|
207
|
+
Provider and account configuration written by the launcher is preferred for a
|
|
208
|
+
managed service because shell startup files are not guaranteed to be sourced.
|
|
209
|
+
|
|
210
|
+
## Troubleshooting
|
|
211
|
+
|
|
212
|
+
### The room cannot see the bridge
|
|
213
|
+
|
|
214
|
+
Run the launcher and check its account/runtime/service header. If the account
|
|
215
|
+
link is missing or expired, rerun:
|
|
216
|
+
|
|
217
|
+
```bash
|
|
218
|
+
npx thinkpool-pair@latest login
|
|
219
|
+
```
|
|
220
|
+
|
|
221
|
+
### A service is installed but not serving
|
|
222
|
+
|
|
223
|
+
Use the launcher’s restart/update action or:
|
|
224
|
+
|
|
225
|
+
```bash
|
|
226
|
+
npx thinkpool-pair@latest restart-service
|
|
227
|
+
```
|
|
228
|
+
|
|
229
|
+
The install command prints the platform-specific log directory. Inspect that
|
|
230
|
+
log before reinstalling; repeated installation can hide the original failure.
|
|
231
|
+
|
|
232
|
+
### No runtimes are available
|
|
233
|
+
|
|
234
|
+
Install/sign in to Claude, Codex, or Hermes on the host, then restart the
|
|
235
|
+
launcher. Runtime availability is detected from the host; the web room cannot
|
|
236
|
+
install a missing CLI for you.
|
|
237
|
+
|
|
238
|
+
### A custom model fails immediately
|
|
239
|
+
|
|
240
|
+
Check that the endpoint implements Anthropic Messages semantics and that the
|
|
241
|
+
model ID exists on that provider. Codex lanes ignore the bridge’s custom Claude
|
|
242
|
+
provider configuration and use the Codex/OpenAI login.
|
|
243
|
+
|
|
244
|
+
### Preview capture cannot find Chrome
|
|
245
|
+
|
|
246
|
+
Install Chrome/Chromium on the bridge host or set `TP_BROWSER_PATH` to its
|
|
247
|
+
executable.
|
|
248
|
+
|
|
249
|
+
## Develop the bridge package
|
|
250
|
+
|
|
251
|
+
From the repository root:
|
|
252
|
+
|
|
253
|
+
```bash
|
|
254
|
+
npm --prefix bridge ci
|
|
255
|
+
npm run test:bridge
|
|
256
|
+
node bridge/test-packed-artifact.mjs
|
|
257
|
+
```
|
|
258
|
+
|
|
259
|
+
Run the checked-out launcher with:
|
|
260
|
+
|
|
261
|
+
```bash
|
|
262
|
+
node bridge/bridge.mjs
|
|
263
|
+
```
|
|
264
|
+
|
|
265
|
+
Publishing is separate from the web-app deploy:
|
|
266
|
+
|
|
267
|
+
```bash
|
|
268
|
+
npm --prefix bridge run release
|
|
269
|
+
```
|
|
233
270
|
|
|
234
|
-
|
|
235
|
-
|
|
271
|
+
The release command has external side effects and requires npm publish
|
|
272
|
+
authority. Do not use it as a local verification command.
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# bridge/bridge-service.sh — install thinkpool-pair as an AUTO-UPDATING launchd
|
|
3
|
+
# service for a room, with a fast update poll so a new `npm publish` lands in the
|
|
4
|
+
# room within ~1 min of an idle moment (no manual restart, no re-install).
|
|
5
|
+
#
|
|
6
|
+
# The stock `thinkpool-pair install-service <ROOM>` already runs under launchd
|
|
7
|
+
# KeepAlive with `npx -y thinkpool-pair@latest` (so each restart fetches the newest
|
|
8
|
+
# publish) and sets THINKPOOL_PAIR_AUTOUPDATE=1 — but it leaves the poll interval at
|
|
9
|
+
# the 30-min default. This wrapper installs it, then injects the faster poll/idle
|
|
10
|
+
# knobs into the generated LaunchAgent plist and reloads it.
|
|
11
|
+
#
|
|
12
|
+
# Usage: bash bridge/bridge-service.sh <ROOM> [-- <extra bridge args>]
|
|
13
|
+
# e.g. bash bridge/bridge-service.sh P6O6I # default: --headless --auto=claude
|
|
14
|
+
# bash bridge/bridge-service.sh P6O6I -- --headless --auto=claude
|
|
15
|
+
# Env: TP_UPDATE_INTERVAL (default 60) registry poll seconds
|
|
16
|
+
# TP_UPDATE_IDLE (default 20) idle seconds before an update restart
|
|
17
|
+
# TP_FORCE=1 skip the "bare bridge already running" guard
|
|
18
|
+
# Remove: npx thinkpool-pair@latest uninstall-service <ROOM>
|
|
19
|
+
set -euo pipefail
|
|
20
|
+
|
|
21
|
+
ROOM_RAW="${1:-}"
|
|
22
|
+
[ -n "$ROOM_RAW" ] && [ "${ROOM_RAW#-}" = "$ROOM_RAW" ] || { echo "usage: bash bridge/bridge-service.sh <ROOM> [-- <extra bridge args>]"; exit 1; }
|
|
23
|
+
ROOM="$(printf '%s' "$ROOM_RAW" | tr '[:lower:]' '[:upper:]')"
|
|
24
|
+
shift || true
|
|
25
|
+
# Everything after a literal `--` is forwarded to the bridge; default to the
|
|
26
|
+
# headless auto-claude invocation the rooms run interactively today.
|
|
27
|
+
TAIL=()
|
|
28
|
+
if [ "${1:-}" = "--" ]; then shift; TAIL=("$@"); else TAIL=(--headless --auto=claude); fi
|
|
29
|
+
|
|
30
|
+
INTERVAL="${TP_UPDATE_INTERVAL:-60}"
|
|
31
|
+
IDLE="${TP_UPDATE_IDLE:-20}"
|
|
32
|
+
PLIST="$HOME/Library/LaunchAgents/io.thinkpool.pair.$(printf '%s' "$ROOM" | tr '[:upper:]' '[:lower:]').plist"
|
|
33
|
+
|
|
34
|
+
[ "$(uname)" = "Darwin" ] || { echo "✗ this wrapper is macOS/launchd only (Linux: use systemd via 'npx thinkpool-pair@latest install-service')"; exit 1; }
|
|
35
|
+
|
|
36
|
+
# Guard: a bare (foreground) bridge for this room would fight the service —
|
|
37
|
+
# two relays on one realtime channel duplicate everything. Stop it first.
|
|
38
|
+
# NOTE: macOS pgrep is POSIX-ERE — no \b. Match "bridge.mjs <ROOM>" with the room
|
|
39
|
+
# delimited by whitespace or end-of-arg. (A \b guard here once failed silently and
|
|
40
|
+
# serviced the live room, restarting its bridge — don't reintroduce it.)
|
|
41
|
+
BRIDGE_RE="bridge\.mjs[[:space:]]+${ROOM}([[:space:]]|\$)"
|
|
42
|
+
if [ -z "${TP_FORCE:-}" ] && pgrep -f "$BRIDGE_RE" >/dev/null 2>&1; then
|
|
43
|
+
echo "✗ a bare bridge is already running for ${ROOM} (pid $(pgrep -f "$BRIDGE_RE" | tr '\n' ' '))."
|
|
44
|
+
echo " Stop it first (close its terminal / kill it), then re-run — or TP_FORCE=1 to override."
|
|
45
|
+
echo " NOTE: if ${ROOM} is hosting the Claude session you're talking to RIGHT NOW, servicing it restarts the bridge and drops that chat."
|
|
46
|
+
exit 2
|
|
47
|
+
fi
|
|
48
|
+
|
|
49
|
+
echo "◆ installing auto-updating service for ${ROOM} (npx thinkpool-pair@latest ${TAIL[*]})…"
|
|
50
|
+
npx -y thinkpool-pair@latest install-service "$ROOM" -- "${TAIL[@]}"
|
|
51
|
+
|
|
52
|
+
[ -f "$PLIST" ] || { echo "✗ expected plist not found at $PLIST — install may have failed"; exit 3; }
|
|
53
|
+
|
|
54
|
+
# Inject the fast-poll knobs into EnvironmentVariables (Add, or Set if present).
|
|
55
|
+
pb() { /usr/libexec/PlistBuddy -c "$1" "$PLIST" >/dev/null 2>&1; }
|
|
56
|
+
pb "Add :EnvironmentVariables:THINKPOOL_PAIR_UPDATE_INTERVAL string ${INTERVAL}" || pb "Set :EnvironmentVariables:THINKPOOL_PAIR_UPDATE_INTERVAL ${INTERVAL}"
|
|
57
|
+
pb "Add :EnvironmentVariables:THINKPOOL_PAIR_UPDATE_IDLE string ${IDLE}" || pb "Set :EnvironmentVariables:THINKPOOL_PAIR_UPDATE_IDLE ${IDLE}"
|
|
58
|
+
|
|
59
|
+
# Reload so the new env takes effect immediately.
|
|
60
|
+
launchctl unload "$PLIST" 2>/dev/null || true
|
|
61
|
+
launchctl load "$PLIST"
|
|
62
|
+
|
|
63
|
+
echo "✓ ${ROOM} is now a launchd service:"
|
|
64
|
+
echo " • tracks thinkpool-pair@latest, polls npm every ${INTERVAL}s, self-restarts at the next idle ≥${IDLE}s"
|
|
65
|
+
echo " • plist: $PLIST"
|
|
66
|
+
echo " • logs: ~/.thinkpool-pair/${ROOM}.log"
|
|
67
|
+
echo " • remove: npx thinkpool-pair@latest uninstall-service ${ROOM}"
|
package/bridge.mjs
CHANGED
|
@@ -117,7 +117,7 @@ const flowRedispatch = new Map()
|
|
|
117
117
|
// wave BEFORE overrun. Lives bridge-side because waves dispatch across separate
|
|
118
118
|
// broadcasts; without persistent state the cap can never bite.
|
|
119
119
|
const flowBudgets = new Map()
|
|
120
|
-
import { formatPeek, PEEK, siblingsOf, resolveSibling, crossPostDecision, CROSSPOST, spawnDecision, SPAWN, CROSSROOM, CROSSROOM_BUS, formatPairRoster, crossRoomPostDecision, formatRoomNow, formatClosableHint, closeLaneDecision, laneBusyOf, laneStatusOf, settleLaneBusy, nativeClaudeFallbackHint, buildDispatchPreview, authorizeDirectDispatch } from './cross-terminal.mjs'
|
|
120
|
+
import { formatPeek, PEEK, readTerminalBudgetDecision, siblingsOf, resolveSibling, crossPostDecision, CROSSPOST, spawnDecision, SPAWN, CROSSROOM, CROSSROOM_BUS, formatPairRoster, crossRoomPostDecision, formatRoomNow, formatClosableHint, closeLaneDecision, laneBusyOf, laneStatusOf, settleLaneBusy, nativeClaudeFallbackHint, buildDispatchPreview, authorizeDirectDispatch } from './cross-terminal.mjs'
|
|
121
121
|
import { createStandalonePairResponder, standalonePairIdentity } from './direct-pair-room.mjs'
|
|
122
122
|
import { dispatchPendingRecap, recoverInterruptedTurn, recoverMissingResumeOnce, sendInterruptedContinue, shouldAttemptNativeResume, supersedeInterruptedResume } from './interrupted-resume.mjs'
|
|
123
123
|
import { supersedeDispatchLease } from './dispatch-lease.mjs'
|
|
@@ -941,7 +941,7 @@ function recordCodeUsage (model, usage, provider) {
|
|
|
941
941
|
let _presence = null
|
|
942
942
|
const trackPresence = (payload) => { (_presence ||= makeThrottledTrack(channel, { minMs: 5000 }))(payload) }
|
|
943
943
|
const channel = supabase.channel(`tpcode:${room}`, {
|
|
944
|
-
config: { broadcast: { self: false }, presence: { key: `bridge:${name}` } },
|
|
944
|
+
config: { private: true, broadcast: { self: false }, presence: { key: `bridge:${name}` } },
|
|
945
945
|
})
|
|
946
946
|
|
|
947
947
|
// Thinkpool Flow rides its OWN topic, separate from the room's primary tpcode channel.
|
|
@@ -953,13 +953,13 @@ const flowChannel = supabase.channel(`tpflow:${room}`, {
|
|
|
953
953
|
config: { broadcast: { self: false } },
|
|
954
954
|
})
|
|
955
955
|
|
|
956
|
-
// Source-changing Design Mode control
|
|
956
|
+
// Source-changing Design Mode control stays isolated from the primary room bus.
|
|
957
957
|
// Membership RLS on realtime.messages gates join, receive, and send.
|
|
958
958
|
const designChannel = supabase.channel(`tpdesign:${room}`, {
|
|
959
959
|
config: { private: true, broadcast: { self: false } },
|
|
960
960
|
})
|
|
961
961
|
|
|
962
|
-
// Host-changing room commands are isolated from the
|
|
962
|
+
// Host-changing room commands are isolated from the primary collaboration topic.
|
|
963
963
|
// Realtime RLS admits current room members and authenticates every send/receive.
|
|
964
964
|
const controlChannel = supabase.channel(`tpcontrol:${room}`, {
|
|
965
965
|
config: { private: true, broadcast: { self: false } },
|
|
@@ -1463,7 +1463,7 @@ async function receiveCrossRoomPost({ fromRoom, fromHost, fromTerminalName, text
|
|
|
1463
1463
|
if (decision !== 'allow') return { error: `The people in room ${room} declined the incoming task from ${fromLabel}.` }
|
|
1464
1464
|
// Injected turn: one room-hop deep, fresh in-room budgets (the loop-breaker reset
|
|
1465
1465
|
// point is code-turn — a real human turn — which this is NOT, so the deeper hop sticks).
|
|
1466
|
-
te.roomHop = 1; te.hop = (te.hop || 0) + 1; te.peekCount = 0; te.postCount = 0; te.pairPeekCount = 0; te.crossRoomPostCount = 0
|
|
1466
|
+
te.roomHop = 1; te.hop = (te.hop || 0) + 1; te.peekCount = 0; te.peekRosterCount = 0; te.postCount = 0; te.pairPeekCount = 0; te.crossRoomPostCount = 0
|
|
1467
1467
|
const msg = `[From: room ${fromLabel} — relayed via the ThinkPool cross-room Ensemble, approved by a person in this room]\n${body}`
|
|
1468
1468
|
const evt = { kind: 'you', text: msg, by: `room ${fromRoom}`, crosspost: true, relaySourceName: String(fromTerminalName || '').trim().slice(0, 80) || undefined }
|
|
1469
1469
|
autoNameTerminal(targetId, body)
|
|
@@ -2581,18 +2581,29 @@ function openStructured({ id, runtime = 'claude', model, models, effort, resume,
|
|
|
2581
2581
|
...createViewportTools({ tool, z, manager: entry.viewport }),
|
|
2582
2582
|
tool(
|
|
2583
2583
|
'read_terminal',
|
|
2584
|
-
'Read-only view of ANOTHER terminal in this ThinkPool Code room (a sibling agent or a shell the people are using). Call with no arguments
|
|
2584
|
+
'Read-only view of ANOTHER terminal in this ThinkPool Code room (a sibling agent or a shell the people are using). The current ROOM NOW snapshot is the default roster. Call with no arguments only when that snapshot is missing or truncated; this optional full-roster lookup has its own one-call allowance. Call with `terminal` (a ref, id, name, or command) only when that lane’s detailed activity is needed. Targeted transcript reads have a separate bounded allowance: never poll, collect each finished owned worker once, then close it immediately.',
|
|
2585
2585
|
{
|
|
2586
2586
|
terminal: z.string().optional().describe('ref, id, or command of the terminal to read; omit to list the open terminals'),
|
|
2587
2587
|
lines: z.number().int().positive().max(PEEK.maxLines).optional().describe(`how many recent lines to return (default ${PEEK.defaultLines})`),
|
|
2588
2588
|
},
|
|
2589
2589
|
async (args) => {
|
|
2590
|
-
entry.peekCount = (entry.peekCount || 0) + 1
|
|
2591
|
-
if (entry.peekCount > PEEK.perTurnCap) {
|
|
2592
|
-
return { content: [{ type: 'text', text: `Cross-terminal read limit reached for this turn (${PEEK.perTurnCap}). Continue with what you have, or ask the people in the room.` }] }
|
|
2593
|
-
}
|
|
2594
2590
|
const sib = siblingsOf({ selfId: id, sessions, terms, names: termNames })
|
|
2595
2591
|
const target = args?.terminal ? resolveSibling(sib, args.terminal) : null
|
|
2592
|
+
// A malformed/stale ref is a no-op, so reject it before spending either
|
|
2593
|
+
// allowance. Discovery and transcript reads are metered independently:
|
|
2594
|
+
// ROOM NOW makes the roster optional, while targeted reads stay available
|
|
2595
|
+
// for actual worker collection and the final review.
|
|
2596
|
+
if (args?.terminal && !target) {
|
|
2597
|
+
return { content: [{ type: 'text', text: formatPeek({ selfId: id, sessions, terms, names: termNames, terminal: args.terminal, lines: args?.lines }) }] }
|
|
2598
|
+
}
|
|
2599
|
+
const budget = readTerminalBudgetDecision({
|
|
2600
|
+
targeted: !!args?.terminal,
|
|
2601
|
+
targetedCount: entry.peekCount,
|
|
2602
|
+
rosterCount: entry.peekRosterCount,
|
|
2603
|
+
})
|
|
2604
|
+
entry.peekCount = budget.targetedCount
|
|
2605
|
+
entry.peekRosterCount = budget.rosterCount
|
|
2606
|
+
if (!budget.ok) return { content: [{ type: 'text', text: budget.reason }] }
|
|
2596
2607
|
const targetEntry = target?.kind === 'agent' ? sessions.get(target.id) : null
|
|
2597
2608
|
const text = formatPeek({ selfId: id, sessions, terms, names: termNames, terminal: args?.terminal, lines: args?.lines })
|
|
2598
2609
|
+ nativeClaudeFallbackHint({ parentRuntime: entry.runtime, parentModels: entry.models, targetRuntime: targetEntry?.runtime, targetLog: targetEntry?.log })
|
|
@@ -2705,6 +2716,7 @@ function openStructured({ id, runtime = 'claude', model, models, effort, resume,
|
|
|
2705
2716
|
// The injected turn starts fresh budgets, one hop deeper (loop breaker).
|
|
2706
2717
|
te.hop = (entry.hop || 0) + 1
|
|
2707
2718
|
te.peekCount = 0
|
|
2719
|
+
te.peekRosterCount = 0
|
|
2708
2720
|
te.postCount = 0
|
|
2709
2721
|
// Echo the injected prompt into the TARGET lane so both people see what
|
|
2710
2722
|
// arrived (rides the existing code-event 'you' path — no new topic).
|
|
@@ -2762,7 +2774,7 @@ function openStructured({ id, runtime = 'claude', model, models, effort, resume,
|
|
|
2762
2774
|
if (args?.name) { delete termNames[newId]; saveNames(room, termNames) }
|
|
2763
2775
|
return okText('Could not open the main conductor terminal — the machine terminal cap may have just been reached.')
|
|
2764
2776
|
}
|
|
2765
|
-
conductor.peekCount = 0; conductor.postCount = 0; conductor.spawnTimes = []
|
|
2777
|
+
conductor.peekCount = 0; conductor.peekRosterCount = 0; conductor.postCount = 0; conductor.spawnTimes = []
|
|
2766
2778
|
announce()
|
|
2767
2779
|
const msg = `[Task from main terminal ${fromRef}'s agent — opened as an independent MAIN CASCADE CONDUCTOR terminal, not an Ensemble child]\n${String(args.task).trim()}`
|
|
2768
2780
|
const evt = { kind: 'you', text: msg, by: `main terminal ${fromRef} (agent)`, crosspost: true, relaySourceName: termNames[id] || undefined }
|
|
@@ -2837,7 +2849,10 @@ function openStructured({ id, runtime = 'claude', model, models, effort, resume,
|
|
|
2837
2849
|
resolvedModel: resolved.model,
|
|
2838
2850
|
},
|
|
2839
2851
|
})
|
|
2840
|
-
} catch
|
|
2852
|
+
} catch (error) {
|
|
2853
|
+
process.stderr.write(`\n ${A.red}✗ dispatch preview rejected — ${error?.message || error}${A.rst}\n`)
|
|
2854
|
+
return okText('Dispatch preview could not be built safely. No lane was created.')
|
|
2855
|
+
}
|
|
2841
2856
|
const currentNow = Date.now()
|
|
2842
2857
|
const current = dispatchContext(currentNow)
|
|
2843
2858
|
const authorization = authorizeDirectDispatch({
|
|
@@ -2880,7 +2895,7 @@ function openStructured({ id, runtime = 'claude', model, models, effort, resume,
|
|
|
2880
2895
|
openStructured({ id: newId, runtime: resolved.runtime, model: resolved.model, provider: resolved.provider, mode: effectiveArgs.mode, sliceType: args?.sliceType, flowReviewTargets: manualReviewSnapshots.map((item) => item.taskKey), flowReviewSnapshots: manualReviewSnapshots, spawnedBy: id, spawnDepth: childSpawnDepth, cascadeRole: 'worker', hop: childHop })
|
|
2881
2896
|
const ne = sessions.get(newId)
|
|
2882
2897
|
if (!ne) { if (args?.name) { delete termNames[newId]; saveNames(room, termNames) } return okText('Could not open a new lane — the terminal cap may have just been reached. Close one and retry.') }
|
|
2883
|
-
ne.peekCount = 0; ne.postCount = 0; ne.spawnTimes = []
|
|
2898
|
+
ne.peekCount = 0; ne.peekRosterCount = 0; ne.postCount = 0; ne.spawnTimes = []
|
|
2884
2899
|
announce() // (defensive) ensure the web renders the new tab live
|
|
2885
2900
|
if (args?.task) {
|
|
2886
2901
|
// The user-visible relay reinforces (but never defines) the structural
|
|
@@ -4244,6 +4259,7 @@ channel
|
|
|
4244
4259
|
// through here, so its deeper hop level + spent budget stick until a person
|
|
4245
4260
|
// speaks again.
|
|
4246
4261
|
s.peekCount = 0
|
|
4262
|
+
s.peekRosterCount = 0
|
|
4247
4263
|
s.postCount = 0
|
|
4248
4264
|
s.pairPeekCount = 0 // cross-room read budget resets with the in-room ones (Tier 1)
|
|
4249
4265
|
s.crossRoomPostCount = 0 // Tier 3 cross-room post budget resets on a real human turn
|
|
@@ -4508,7 +4524,7 @@ channel
|
|
|
4508
4524
|
const s = payload?.term && sessions.get(payload.term)
|
|
4509
4525
|
const p = s && payload.id && s.pending.get(payload.id)
|
|
4510
4526
|
if (!p) return
|
|
4511
|
-
// `tpcode` is
|
|
4527
|
+
// `tpcode` is membership-gated transport. Treat the frame only as a wake-up hint and
|
|
4512
4528
|
// reproduce its resolved item + unique delivery attempt through owner-authenticated
|
|
4513
4529
|
// RLS reads before touching the local pending resolver. A forged broadcast then
|
|
4514
4530
|
// has no more authority than packet noise.
|
package/claude-session.mjs
CHANGED
|
@@ -240,7 +240,6 @@ const TP_ROOM_REMINDER = [
|
|
|
240
240
|
'You are Claude in a ThinkPool Code room, driven live from a phone or browser — NOT a local terminal. Keep using the room\'s features.',
|
|
241
241
|
THINKPOOL_RUNTIME_TURN_REMINDER,
|
|
242
242
|
'TERMINAL HIERARCHY: obey your authoritative TERMINAL ROLE. Conductors are independent main terminals; Ensemble lanes are workers only. When a person explicitly asks to open, launch, start, or spawn a separate Cascade/conductor terminal, use open_main_terminal — NEVER spawn_terminal. If open_main_terminal is unavailable, say so; never substitute an Ensemble child. Only conductor-capable roles fan worker slices out with spawn_terminal. Leaf, worker, Side, and managed Flow lanes work directly. Never use built-in invisible Task/Agent subagents or hijack a busy sibling.',
|
|
243
|
-
'PEER: before substantive work, check what the other lanes are doing (the ROOM NOW snapshot below, read_terminal for detail; list_sessions/read_session across rooms) — coordinate on shared files/branches instead of colliding.',
|
|
244
243
|
'WORKTREES: parallel lanes share one repo — before code edits run `git worktree list`; if linked worktrees exist, take your OWN worktree + branch, never the shared checkout or a branch another lane is on.',
|
|
245
244
|
'BUILD WORKFLOW (default, no magic word): right-size within your TERMINAL ROLE — a trivial ask or delegated slice you just do; a conductor-capable role with a genuinely decomposable build FIRST writes a short plan in chat, THEN fans worker slices into visible spawn_terminal lanes and verifies them. Worker/leaf/Side/managed Flow roles do not fan out. A requested separate conductor uses open_main_terminal. Never plan-mode/ExitPlanMode; plans live in chat and lanes in the existing list.',
|
|
246
245
|
].join(' ')
|
|
@@ -748,14 +747,13 @@ export function startClaudeSession({ cwd, model, effort: initialEffort = 'high',
|
|
|
748
747
|
THINKPOOL_CASCADE_RULE,
|
|
749
748
|
'CRUCIAL RECONCILIATION for that workflow: it is NOT plan mode. Never call ExitPlanMode and never make the room wait behind a "plan ready — approve to start" card — your plan lives in the CHAT as a message, and your lanes live in the room\'s EXISTING terminal/lane list. Reuse only those two surfaces; there is no new Flow panel or mode to switch into, and you must not ask for one. Keep the plan and the lanes VISIBLE — that shared visibility is the whole point (it is the pair differentiator, and it catches bugs a single silent lane would hide); never collapse a decomposable build into one hidden lane just to look tidy.',
|
|
750
749
|
...THINKPOOL_REMOTE_DELIVERY_RULES,
|
|
751
|
-
'CROSS-TERMINAL AWARENESS:
|
|
750
|
+
'CROSS-TERMINAL AWARENESS: ROOM NOW is the default roster and already satisfies the room check when it has enough detail. Do not repeat it with a no-argument read_terminal call unless it is missing or truncated. Use a targeted read_terminal call only when the current task depends on a specific lane’s detailed activity; never poll. Identify a terminal by its NAME or stable ref/id, never by an on-screen number like "Terminal 2" — positional labels renumber when a terminal is closed. The tool is read-only and its optional roster lookup is budgeted separately from bounded targeted transcript reads.',
|
|
752
751
|
'CROSS-TERMINAL HAND-OFF: you also have post_to_terminal(terminal, text) to send a message or task to ANOTHER AGENT terminal in this room (not a plain shell). Use it sparingly and only when the people clearly want the lanes to coordinate — e.g. "tell the backend terminal the API is ready", or to hand a sibling agent a concrete task. Every post requires a person in the room to approve a card before it is delivered, and an agent that was itself reached via a cross-post cannot post onward — so do not rely on it for chit-chat or loops. Prefer read_terminal to understand a sibling before you ever post to it.',
|
|
753
|
-
'TERMINAL CREATION CONTRACT: conductors and workers use different tools. When a person explicitly asks to open, launch, start, or spawn a separate Cascade/conductor terminal, use open_main_terminal(name?, task?, model?) — it creates an independent MAIN terminal with no Ensemble owner. Never use spawn_terminal for that request, and if open_main_terminal is unavailable say so instead of substituting. Use spawn_terminal(name?, task?, model?, sliceType?) only for bounded WORKER SUB-TERMINALS when your authoritative role permits delegation. Workers never receive the creation tools and never conduct. Do not dump work into busy siblings. Spawned workers are always autonomous in bypassPermissions and never ask the room for an approval card; the bridge still enforces room caps, hop limits, the kill-switch, and isolated linked worktrees.
|
|
752
|
+
'TERMINAL CREATION CONTRACT: conductors and workers use different tools. When a person explicitly asks to open, launch, start, or spawn a separate Cascade/conductor terminal, use open_main_terminal(name?, task?, model?) — it creates an independent MAIN terminal with no Ensemble owner. Never use spawn_terminal for that request, and if open_main_terminal is unavailable say so instead of substituting. Use spawn_terminal(name?, task?, model?, sliceType?) only for bounded WORKER SUB-TERMINALS when your authoritative role permits delegation. Workers never receive the creation tools and never conduct. Do not dump work into busy siblings. Spawned workers are always autonomous in bypassPermissions and never ask the room for an approval card; the bridge still enforces room caps, hop limits, the kill-switch, and isolated linked worktrees. Wait for ROOM NOW or a completion signal instead of polling; after a worker finishes, collect it with one targeted read_terminal call and close_terminal immediately. Main conductors are independent terminals, keep their requested permission mode, and are not owned/closed through Ensemble.',
|
|
754
753
|
'CROSS-SESSION AWARENESS: the Ensemble reaches across your SESSIONS, not just the terminals in this room. list_sessions() lists your OTHER ThinkPool Code rooms — both your own rooms running on this machine AND your partner\'s rooms in the same pair, reachable over the per-pair bus (a room on the partner\'s machine shows its host). read_session(session, terminal?) reads recent activity inside one (omit `terminal` to list that room\'s terminals, or pass a ref/name to read that lane). Both are READ-ONLY — they never change another session, and they reach ONLY your own rooms and rooms you share with your partner, never a stranger\'s. Reach for them when work spans rooms — "what\'s the other project up to", "pick up where the other session left off", or to check a long-running task elsewhere before you act here.',
|
|
755
754
|
'CROSS-SESSION HAND-OFF: post_to_session(session, text, terminal?) sends a task or message to an agent in ANOTHER of your rooms — your own, or your partner\'s over the pair bus. Use it sparingly and only when the people clearly want the rooms to coordinate — e.g. hand the API room\'s agent a concrete follow-up once the frontend is ready. It is dual-consent: a person in YOUR room approves sending, and a person in the TARGET room approves receiving, before anything is delivered — so never rely on it for chit-chat or loops, and an agent that was itself reached via a cross-room post cannot post onward to a third room. It spends real model tokens in the other room (maybe on the other person\'s machine), so prefer read_session to understand a room before you ever post into it, and only post one concrete hand-off at a time. Outbound list/read/post tools need the ThinkPool account bridge; a standalone owner room can still receive a paired hand-off directly and will always raise its own approval card before delivery.',
|
|
756
755
|
'SUBAGENT POLICY: in this room, a main conductor delegates worker slices through visible spawn_terminal Ensemble lanes. A requested separate conductor is created with open_main_terminal, never Ensemble. Worker, leaf, Side, and managed Flow lanes do their assigned work directly. Do NOT reach for built-in Task/Agent subagents: an in-process subagent is invisible to the room, cannot be peered at or steered, and its work is lost to the Ensemble.',
|
|
757
756
|
'RESEARCH LANE: you have a `research` tool that runs a REAL multi-source web search + adversarial verification and returns each claim marked HELD or REJECTED with citations. Reach for it when the people would genuinely benefit from looking something external up or settling a question of current fact — pricing, "is X still maintained / deprecated", "is that benchmark real", a debate over facts you are not sure of. Do NOT run it unprompted or for things you already know: first OFFER in plain language ("want me to spawn a research lane on that and check it?"), and only call `research(question)` once they agree — it spends real budget (plan-gated Free 5 / Plus 100 runs a month) and takes ~a minute. When it returns, present the held/rejected findings clearly and invite both people to weigh the sources, flagging any held claim that rests on a source they might not trust — that shared scrutiny is the point.',
|
|
758
|
-
'PEER FIRST: other lanes may be working in the same repo as you, right now. Before starting substantive work — and before any code edit that could overlap another lane — check what the room is doing: the ROOM NOW snapshot appended to your latest turn, or read_terminal for detail; list_sessions/read_session when the question spans your other rooms. If a sibling is touching the same files or branch, coordinate (read its lane, or raise it in chat) instead of colliding.',
|
|
759
757
|
'WORKTREES: parallel lanes share one machine and usually one repo. Run `git worktree list` before your first code edit; if linked worktrees exist, the shared main checkout is contended (and may be guard-blocked) — do your work in your OWN worktree on your OWN branch (`git worktree add <dir> -b <branch>`), and never edit a checkout or ride a branch another lane is using.',
|
|
760
758
|
'WRITE PLANS INTO THE CHAT: whenever you form or revise a plan — because the room is in Plan mode, or because someone asked you to plan, design, or think it through first — write the actual plan out as a normal message in the room as you develop it: the approach, the concrete steps, the files you will touch, the open questions. The room does NOT surface plan files at all, and the plan-approval card does not reliably carry the plan text, so a plan that lives only in a plan file or only inside ExitPlanMode is INVISIBLE to the people you are working with — they just see "plan ready" with no content. The chat is the canonical place your plan lives; put it there so the room can read and react to it before you proceed.',
|
|
761
759
|
].join(' '),
|
package/cross-terminal.mjs
CHANGED
|
@@ -18,11 +18,38 @@ export const PEEK = {
|
|
|
18
18
|
// explicitly when a diagnosis genuinely needs deeper sibling history.
|
|
19
19
|
defaultLines: 20,
|
|
20
20
|
maxLines: 200,
|
|
21
|
-
perTurnCap: 10, //
|
|
21
|
+
perTurnCap: 10, // targeted transcript reads allowed per user turn (bridge resets it)
|
|
22
|
+
rosterPerTurnCap: 1, // no-argument roster reads use a separate allowance; ROOM NOW is default
|
|
22
23
|
lineCap: 200, // per-line truncation
|
|
23
24
|
rosterPreview: 100, // last-line preview length in the roster listing
|
|
24
25
|
}
|
|
25
26
|
|
|
27
|
+
// ROOM NOW already gives every agent a compact live roster. Keep the one optional
|
|
28
|
+
// full-roster lookup separate from transcript reads so a redundant discovery call
|
|
29
|
+
// cannot consume the budget needed to collect workers and run the final review.
|
|
30
|
+
// Unknown terminal refs are rejected before this helper is called and spend neither
|
|
31
|
+
// allowance. The bridge stores the returned counters on the current lane.
|
|
32
|
+
export const readTerminalBudgetDecision = ({ targeted = false, targetedCount = 0, rosterCount = 0 } = {}, limits = PEEK) => {
|
|
33
|
+
const nextTargeted = Math.max(0, Number(targetedCount) || 0)
|
|
34
|
+
const nextRoster = Math.max(0, Number(rosterCount) || 0)
|
|
35
|
+
if (targeted) {
|
|
36
|
+
if (nextTargeted >= limits.perTurnCap) return {
|
|
37
|
+
ok: false,
|
|
38
|
+
targetedCount: nextTargeted,
|
|
39
|
+
rosterCount: nextRoster,
|
|
40
|
+
reason: `Cross-terminal transcript read limit reached for this turn (${limits.perTurnCap}). Stop polling and continue with the worker results already collected; a new person-authored turn resets the allowance.`,
|
|
41
|
+
}
|
|
42
|
+
return { ok: true, targetedCount: nextTargeted + 1, rosterCount: nextRoster }
|
|
43
|
+
}
|
|
44
|
+
if (nextRoster >= limits.rosterPerTurnCap) return {
|
|
45
|
+
ok: false,
|
|
46
|
+
targetedCount: nextTargeted,
|
|
47
|
+
rosterCount: nextRoster,
|
|
48
|
+
reason: `The full terminal roster was already read this turn (${limits.rosterPerTurnCap}/${limits.rosterPerTurnCap}). Use the current ROOM NOW snapshot or read one relevant terminal by ref or name.`,
|
|
49
|
+
}
|
|
50
|
+
return { ok: true, targetedCount: nextTargeted, rosterCount: nextRoster + 1 }
|
|
51
|
+
}
|
|
52
|
+
|
|
26
53
|
// One shared lane-status classifier for every roster consumer: the bridge wire,
|
|
27
54
|
// read_terminal, and ROOM NOW. Timestamps stay absolute on the wire so clients can
|
|
28
55
|
// keep the displayed age current without a broadcast every second.
|
|
@@ -320,7 +347,7 @@ export const formatRoomNow = ({ selfId, sessions, terms, names = {}, worktrees =
|
|
|
320
347
|
return `- ${label(x)} · ${x.kind} · ${laneStatusText(x)} — ${clip(preview, limits.previewLen)}`
|
|
321
348
|
})
|
|
322
349
|
if (sib.length > shown.length) rows.push(`- … +${sib.length - shown.length} more (read_terminal lists all)`)
|
|
323
|
-
out.push(`Other lanes in this room right now (read_terminal for detail):\n${rows.join('\n')}`)
|
|
350
|
+
out.push(`Other lanes in this room right now (this is the roster; use targeted read_terminal only for needed detail):\n${rows.join('\n')}`)
|
|
324
351
|
}
|
|
325
352
|
const wtAll = Array.isArray(worktrees) ? worktrees : []
|
|
326
353
|
const wt = wtAll.slice(0, limits.wtCap)
|
|
@@ -447,7 +474,10 @@ export const spawnDecision = ({ hop = 0, spawnTimes = [], now = 0, spawnedLive =
|
|
|
447
474
|
// rechecks immediately before it spends a lane. No token, provider credential,
|
|
448
475
|
// host path, or mutable process object is copied into the preview.
|
|
449
476
|
const DISPATCH_ARG_KEYS = Object.freeze(['mode', 'model', 'name', 'provider', 'runtime', 'sliceType', 'task'])
|
|
450
|
-
|
|
477
|
+
// `review` is an internal Codex-only mode selected by structuredModeForSlice.
|
|
478
|
+
// It is not user-selectable in the spawn schema, but it must survive the immutable
|
|
479
|
+
// preview boundary or every Codex review lane fails before creation.
|
|
480
|
+
const DISPATCH_MODES = new Set(['default', 'acceptEdits', 'bypassPermissions', 'plan', 'review'])
|
|
451
481
|
const DISPATCH_RUNTIMES = new Set(['claude', 'codex', 'hermes'])
|
|
452
482
|
const DISPATCH_SLICES = new Set(['scaffold', 'feature', 'fix', 'review'])
|
|
453
483
|
const secretValue = /((?<![a-z0-9])sk-[a-z0-9_-]{8,}|(?<![a-z0-9])gsk_[a-z0-9_-]{8,}|(?<![a-z0-9])AIza[a-z0-9_-]{8,}|bearer\s+[a-z0-9._-]{8,})/ig
|
|
@@ -524,7 +554,8 @@ export function buildDispatchPreview ({ args, initiatorTerminalId, roomCode, bri
|
|
|
524
554
|
}),
|
|
525
555
|
permissions: Object.freeze({ mode: exactArgs.mode, consequence: exactArgs.mode === 'bypassPermissions'
|
|
526
556
|
? 'Unattended local access within this worker’s sandbox and tool policy.'
|
|
527
|
-
: exactArgs.mode === '
|
|
557
|
+
: exactArgs.mode === 'review' ? 'Read-only inspection of the pinned parent snapshot; writes and approval escapes are unavailable.'
|
|
558
|
+
: exactArgs.mode === 'plan' ? 'Planning only; execution remains gated.' : 'Protected local actions keep their normal permission gates.' }),
|
|
528
559
|
worktree: 'Automatically creates one visible lateral worker lane and an isolated git worktree on this machine.',
|
|
529
560
|
caps,
|
|
530
561
|
})
|
|
@@ -1,7 +1,12 @@
|
|
|
1
1
|
// Cumulative provider snapshots are replace-in-place UI state, not transcript
|
|
2
2
|
// facts. Deliver the first immediately and coalesce a burst to its latest frame;
|
|
3
3
|
// any durable event flushes the pending snapshot first to preserve wire order.
|
|
4
|
-
|
|
4
|
+
//
|
|
5
|
+
// 75ms is a readable 13.3fps ceiling: quick enough for an active reply without
|
|
6
|
+
// turning every provider delta into a realtime broadcast or a full UI repaint.
|
|
7
|
+
export const CUMULATIVE_EVENT_RELAY_WAIT_MS = 75
|
|
8
|
+
|
|
9
|
+
export function createCumulativeEventRelay(emit, waitMs = CUMULATIVE_EVENT_RELAY_WAIT_MS) {
|
|
5
10
|
let lastSentAt = 0
|
|
6
11
|
let pending = null
|
|
7
12
|
let timer = null
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "thinkpool-pair",
|
|
3
|
-
"version": "0.7.
|
|
3
|
+
"version": "0.7.330",
|
|
4
4
|
"description": "Share a local coding-agent CLI (Claude Code, Codex, Gemini, Aider, …) into a ThinkPool Code room, live.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -8,6 +8,7 @@
|
|
|
8
8
|
},
|
|
9
9
|
"files": [
|
|
10
10
|
"bridge.mjs",
|
|
11
|
+
"bridge-service.sh",
|
|
11
12
|
"command-guidance.mjs",
|
|
12
13
|
"abort-turn-barrier.mjs",
|
|
13
14
|
"host-memory.mjs",
|
package/terminal-name.mjs
CHANGED
|
@@ -7,7 +7,8 @@ const SKIP = new Set([
|
|
|
7
7
|
'do', 'much', 'better', 'really', 'need', 'want', 'your', 'their', 'its',
|
|
8
8
|
'also', 'all', 'away', 'before', 'cause', 'does', 'dont', "don't", 'fully', 'has',
|
|
9
9
|
'have', 'now', 'occasionally', 'return', 'second', 'so', 'sometimes', 'still',
|
|
10
|
-
'than', 'then', 'there', '
|
|
10
|
+
'than', 'then', 'there', 'what', 'when', 'where', 'which', 'who', 'whenever',
|
|
11
|
+
'why', 'working',
|
|
11
12
|
])
|
|
12
13
|
|
|
13
14
|
const GENERIC = /^(?:new )?(?:agent |coding )?(?:terminal|task|lane|session|work)$/i
|
|
@@ -16,6 +17,8 @@ const ANY_HEADING = /^(?:#{1,6}\s+|(?:context|background|constraints?|inputs?|ou
|
|
|
16
17
|
const NOISE = /^(?:context|background|for reference|here(?:'s| is)|note|current(?:ly)?|example|environment|room now|constraints?|acceptance|success criteria)\b/i
|
|
17
18
|
const EXPLANATION = /^(?:because|cause|since|so that|this is because)\b/i
|
|
18
19
|
const CONSTRAINT = /^(?:users? can still|keep|must|never|should|without)\b/i
|
|
20
|
+
const INFORMATION_REQUEST = /^(?:which|what|who|where|when|why|how)\b/i
|
|
21
|
+
const NEGATIVE_PREFERENCE = /^(?:(?:i|we)\s+)?(?:do not|don't|dont|would not|wouldn't|won't|wont)\s+(?:(?:want|wanna|need)(?:\s+to)?|use|include|choose)\b|^(?:(?:i|we)\s+)?(?:want|wanna|need)(?:\s+to)?\s+avoid\b/i
|
|
19
22
|
const ISSUE = /\b(?:broken|buggy|crash(?:es|ed|ing)?|duplicate|error|fail(?:s|ed|ing|ure)?|flash(?:es|ed|ing)?|missing|no animation|not working|out of (?:scrollable )?view|stuck|wrong)\b/i
|
|
20
23
|
const REQUEST = /^(?:please\s+)?(?:can|could|would|will)\s+(?:we|you)\b|^(?:please\s+)?(?:how about|let's|let us|we need to|i want you to)\b/i
|
|
21
24
|
const SECRET = /(?<![\p{L}\p{N}])(?:sk-(?:proj-)?[a-z0-9_-]{8,}|gsk_[a-z0-9_-]{8,}|xox[baprs]-[a-z0-9_-]{8,}|gh[pousr]_[a-z0-9_-]{8,}|glpat-[a-z0-9_-]{8,}|npm_[a-z0-9]{24,}|(?:sk|rk)_(?:live|test)_[a-z0-9]{8,}|whsec_[a-z0-9]{8,}|AIza[a-z0-9_-]{8,}|AKIA[A-Z0-9]{12,}|bearer\s+[a-z0-9._-]{8,}|eyJ[a-z0-9_-]{8,}\.[a-z0-9_-]{8,}\.[a-z0-9_-]{8,})(?![\p{L}\p{N}])/giu
|
|
@@ -125,12 +128,14 @@ const bestIntentClause = (text) => {
|
|
|
125
128
|
const action = bestAction(clause)
|
|
126
129
|
let score = action?.score || 0
|
|
127
130
|
if (REQUEST.test(clause)) score += 18
|
|
131
|
+
if (INFORMATION_REQUEST.test(clause)) score += 36
|
|
128
132
|
if (ISSUE.test(clause)) score += /\b(?:crash|fail|flash|stuck|wrong)\w*\b/i.test(clause) ? 36 : 26
|
|
129
133
|
if (words.length >= 3 && words.length <= 24) score += 10
|
|
130
134
|
else if (words.length > 40) score -= 14
|
|
131
135
|
if (NOISE.test(clause)) score -= 30
|
|
132
136
|
if (EXPLANATION.test(clause)) score -= 34
|
|
133
137
|
if (CONSTRAINT.test(clause)) score -= 45
|
|
138
|
+
if (NEGATIVE_PREFERENCE.test(clause)) score -= 70
|
|
134
139
|
score += Math.round((i / Math.max(1, clauses.length - 1)) * 4)
|
|
135
140
|
if (!best || score > best.score) best = { clause, score }
|
|
136
141
|
}
|
|
@@ -163,7 +168,7 @@ export function cleanTerminalName(value) {
|
|
|
163
168
|
|
|
164
169
|
const titleWord = (word) => {
|
|
165
170
|
const lower = word.toLowerCase()
|
|
166
|
-
if (lower === 'thinkpool') return 'ThinkPool'
|
|
171
|
+
if (lower === 'thinkpool' || lower === 'thinkpool.io') return 'ThinkPool'
|
|
167
172
|
if (lower === 'serverside') return 'Server-Side'
|
|
168
173
|
if (/^(?:api|ci|css|db|html|llm|mcp|npm|sdk|ui|ux)$/i.test(word)) return lower.toUpperCase()
|
|
169
174
|
if (/^[A-Z\d+#.-]{2,}$/.test(word) || /\d/.test(word)) return word
|
|
@@ -194,6 +199,9 @@ const taskTitle = (value) => {
|
|
|
194
199
|
.replace(/^\s*(?:how about|i (?:do not|don't) know|i guess|i want you to|we need to|your task is to|let(?:'s| us))\s+/i, '')
|
|
195
200
|
.trim()
|
|
196
201
|
if (!body) return null
|
|
202
|
+
if (NEGATIVE_PREFERENCE.test(body)) return null
|
|
203
|
+
const informationRequest = INFORMATION_REQUEST.test(body)
|
|
204
|
+
if (informationRequest) body = body.replace(/^\s*(?:which|what)\s+/i, '').trim()
|
|
197
205
|
const signalText = body
|
|
198
206
|
body = body
|
|
199
207
|
.replace(/\s*,?\s+(?:so (?:that|you|we|it)\b|because\b|cause\b|if you know what i mean\b)[\s\S]*$/i, '')
|
|
@@ -205,7 +213,6 @@ const taskTitle = (value) => {
|
|
|
205
213
|
let verb = issue ? 'Fix' : action?.title
|
|
206
214
|
if (!verb && /\bclos(?:e|es|ed|ing)\b/i.test(body)) verb = 'Close'
|
|
207
215
|
if (!verb && /\bopen(?:s|ed|ing)?\b/i.test(body)) verb = 'Open'
|
|
208
|
-
if (!verb) verb = 'Work On'
|
|
209
216
|
|
|
210
217
|
if (/\bautomatic(?:ally)?\b[\s\S]{0,24}\bnam(?:e|ing)\b/i.test(signalText) && /\bterminals?\b/i.test(signalText)) {
|
|
211
218
|
if (/\bcontext\b[\s\S]{0,24}\broom\b|\broom\b[\s\S]{0,24}\bcontext\b/i.test(signalText)) return 'Auto-Name Terminals From Room Context'
|
|
@@ -226,9 +233,13 @@ const taskTitle = (value) => {
|
|
|
226
233
|
}
|
|
227
234
|
let object = contentWords(objectText)
|
|
228
235
|
if (!object.length) object = contentWords(body)
|
|
236
|
+
if (informationRequest) {
|
|
237
|
+
const available = object.findIndex((word) => /^available$/i.test(word))
|
|
238
|
+
if (available > 0) object = [object[available], ...object.slice(0, available), ...object.slice(available + 1)]
|
|
239
|
+
}
|
|
229
240
|
const picked = [...domain, ...object].filter((word, index, all) => all.findIndex((other) => other.toLowerCase() === word.toLowerCase()) === index).slice(0, 4)
|
|
230
241
|
if (!picked.length) return null
|
|
231
|
-
return cleanTerminalName([verb, ...picked.map(titleWord)].join(' '))
|
|
242
|
+
return cleanTerminalName([verb, ...picked.map(titleWord)].filter(Boolean).join(' '))
|
|
232
243
|
}
|
|
233
244
|
|
|
234
245
|
export function fallbackTerminalName(text) {
|
|
@@ -1,16 +1,16 @@
|
|
|
1
1
|
{
|
|
2
2
|
"schemaVersion": 1,
|
|
3
|
-
"bundleVersion":
|
|
3
|
+
"bundleVersion": 15,
|
|
4
4
|
"contracts": [
|
|
5
5
|
{
|
|
6
6
|
"id": "room-coordination",
|
|
7
|
-
"version":
|
|
7
|
+
"version": 3,
|
|
8
8
|
"routes": [
|
|
9
9
|
{
|
|
10
10
|
"id": "room-awareness",
|
|
11
11
|
"tools": ["read_terminal"],
|
|
12
12
|
"trigger": "\\b(other|another|sibling|peer)\\s+(lane|terminal)|\\bread_terminal\\b",
|
|
13
|
-
"prompt": "When the request depends on another lane, a sibling may overlap the work, or a person refers to another terminal,
|
|
13
|
+
"prompt": "When the request depends on another lane, a sibling may overlap the work, or a person refers to another terminal, use the current ROOM NOW snapshot as the default roster. Do not repeat it with a no-argument read_terminal call unless the snapshot is missing or truncated; that roster lookup has its own one-call allowance. Read only the relevant terminal by ref or name when its detailed transcript is actually needed. Never poll with read_terminal: wait for the room snapshot or completion signal to show a state change, collect each finished owned worker once, then close it immediately. Preserve the bounded targeted-read allowance for worker collection and final review."
|
|
14
14
|
},
|
|
15
15
|
{
|
|
16
16
|
"id": "cross-room-awareness",
|
|
@@ -22,7 +22,7 @@
|
|
|
22
22
|
"id": "visible-handoff",
|
|
23
23
|
"tools": ["post_to_terminal", "post_to_session"],
|
|
24
24
|
"trigger": "\\b(hand[ -]?off|tell|send|post)\\b.{0,40}\\b(lane|terminal|room|session|agent)\\b|\\b(post_to_terminal|post_to_session)\\b",
|
|
25
|
-
"prompt": "When the people want a handoff, read the target
|
|
25
|
+
"prompt": "When the people want a handoff, read the target once if its current details are not already available, then use post_to_terminal or post_to_session; let the room approval contract handle consent."
|
|
26
26
|
}
|
|
27
27
|
],
|
|
28
28
|
"impact": [
|
|
@@ -41,7 +41,7 @@
|
|
|
41
41
|
},
|
|
42
42
|
{
|
|
43
43
|
"id": "work-routing",
|
|
44
|
-
"version":
|
|
44
|
+
"version": 6,
|
|
45
45
|
"providerRoutingContract": "A Claude lane may select a connected Anthropic-compatible provider by durable id, unique display name, or unique configured model. Unknown or ambiguous references fail closed and must never fall through to built-in Claude.",
|
|
46
46
|
"openerParityContract": "Every top-level terminal may use spawn_terminal or open_main_terminal to open every supported structured runtime: Claude, Codex, or Hermes. Runtime-specific provider/model validation remains authoritative and occurs before inference.",
|
|
47
47
|
"routes": [
|
|
@@ -49,10 +49,10 @@
|
|
|
49
49
|
"id": "work-routing",
|
|
50
50
|
"tools": ["spawn_terminal", "open_main_terminal", "close_terminal"],
|
|
51
51
|
"trigger": "\\b(parallel|delegate|worker|sub[- ]?terminal|conductor|cascade|spawn_terminal|open_main_terminal|close_terminal)\\b",
|
|
52
|
-
"prompt": "For genuinely decomposable work in a conductor-capable role, open visible worker slices with spawn_terminal, collect and verify
|
|
52
|
+
"prompt": "For genuinely decomposable work in a conductor-capable role, open visible worker slices with spawn_terminal, collect and verify each result with one targeted read_terminal call after completion, then close_terminal immediately. Do not poll workers or repeat the ROOM NOW roster. An explicitly requested separate Cascade/conductor uses open_main_terminal, never spawn_terminal. Review slices are structurally read-only and must use the review slice type; they never inherit autonomous bypass authority."
|
|
53
53
|
}
|
|
54
54
|
],
|
|
55
|
-
"expandedPrompt": "CASCADE WORKFLOW (default for non-trivial work that genuinely decomposes; mandatory when the people ask for Cascade, tiered lanes, one-shot flow, or unattended multi-slice execution): first investigate enough to name evidence-backed slices and their dependencies, then post a short plan in the room. The current top-level/conductor terminal keeps decomposition, integration, and final judgment; use spawn_terminal only for bounded worker slices, while an explicitly requested separate conductor uses open_main_terminal. Parallelize only disjoint slices and encode dependencies instead of holding them in your head. Choose sliceType=scaffold for mechanical/search work, feature or fix for implementation, and review with a balanced capable tier for adversarial verification. Review slices are structurally read-only: they resolve to a native review/Plan mode before launch and can never inherit autonomous bypass authority. Every worker brief names the evidence/diagnosis, exact scope, observable acceptance proof, relevant repo gates, whether merge/publish is required, and a safe-skip escape hatch. After dispatch, remain responsible:
|
|
55
|
+
"expandedPrompt": "CASCADE WORKFLOW (default for non-trivial work that genuinely decomposes; mandatory when the people ask for Cascade, tiered lanes, one-shot flow, or unattended multi-slice execution): first investigate enough to name evidence-backed slices and their dependencies, then post a short plan in the room. The current top-level/conductor terminal keeps decomposition, integration, and final judgment; use spawn_terminal only for bounded worker slices, while an explicitly requested separate conductor uses open_main_terminal. Parallelize only disjoint slices and encode dependencies instead of holding them in your head. Choose sliceType=scaffold for mechanical/search work, feature or fix for implementation, and review with a balanced capable tier for adversarial verification. Review slices are structurally read-only: they resolve to a native review/Plan mode before launch and can never inherit autonomous bypass authority. Every worker brief names the evidence/diagnosis, exact scope, observable acceptance proof, relevant repo gates, whether merge/publish is required, and a safe-skip escape hatch. ROOM NOW is the default roster and completion signals wake the conductor; do not spend the bounded read allowance polling or repeating a no-argument roster. After dispatch, remain responsible: once an owned worker is finished, make one targeted read_terminal call to collect its result, verify its claim, close_terminal immediately, and dispatch newly unblocked work. Preserve enough targeted reads for every open worker and the final adversarial review; never declare the Cascade done or yield a final result while owned workers remain uncollected. Check origin/main CI before merge-bearing waves, coordinate shared fixtures/publishers, and treat sibling pushes as context rather than proof. Run an adversarial review after builders (or pipeline review behind completed phases), finish with production-condition evidence, verify every reported SHA/version, and close every worker you opened.",
|
|
56
56
|
"impact": [
|
|
57
57
|
{"path": "bridge/bridge.mjs", "diffPattern": "spawn_terminal|open_main_terminal|close_terminal|cascadeRole|spawnDepth"},
|
|
58
58
|
{"path": "bridge/lane-lifecycle.mjs"},
|