@cordfuse/crosstalk 6.0.0-alpha.1 → 6.0.0-alpha.11
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/GUIDE-CLI.md +298 -0
- package/GUIDE-PROMPTS.md +132 -0
- package/README.md +136 -0
- package/bin/crosstalk.js +22 -7
- package/package.json +8 -4
- package/src/actor.ts +29 -4
- package/src/dispatch.ts +55 -18
- package/src/dlq.ts +26 -5
- package/src/init.ts +12 -2
- package/src/send.ts +39 -5
- package/src/state.ts +30 -0
- package/src/stop.ts +37 -0
- package/src/transport.ts +13 -0
- package/template/CLAUDE.md +12 -0
- package/template/README.md +94 -0
- package/template/gitignore +4 -0
- package/template/upstream/CROSSTALK-VERSION +1 -0
- package/template/upstream/CROSSTALK.md +298 -0
- package/template/upstream/OPERATOR.md +60 -0
- package/template/upstream/PROTOCOL.md +82 -0
- package/template/upstream/actors/concierge.md +36 -0
package/GUIDE-CLI.md
ADDED
|
@@ -0,0 +1,298 @@
|
|
|
1
|
+
# Crosstalk CLI Guide
|
|
2
|
+
|
|
3
|
+
Quick reference for the `crosstalk` CLI. Run `crosstalk <subcommand> --help` for flags.
|
|
4
|
+
|
|
5
|
+
Requires: Node.js ≥ 20. All subcommands (except `init`) must be run from inside a transport (a directory containing `upstream/CROSSTALK-VERSION`).
|
|
6
|
+
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
## Setup
|
|
10
|
+
|
|
11
|
+
### `crosstalk init`
|
|
12
|
+
|
|
13
|
+
Scaffold a new transport in the current directory:
|
|
14
|
+
|
|
15
|
+
```sh
|
|
16
|
+
mkdir my-transport && cd my-transport
|
|
17
|
+
git init
|
|
18
|
+
crosstalk init
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
Creates `upstream/` (spec + protocol docs), `hosts/`, `data/`, and `local/actors/`. Commit and push to your git remote; that repo is now the message bus.
|
|
22
|
+
|
|
23
|
+
> ### Multi-host: seed every host file before sending
|
|
24
|
+
>
|
|
25
|
+
> Each host's delivery waterline is the commit that first added **its** host
|
|
26
|
+
> file to the transport. A message addressed to a host *before* that host's
|
|
27
|
+
> file exists in history sits below the waterline and is never delivered.
|
|
28
|
+
>
|
|
29
|
+
> **Therefore, when adding a host to a multi-host bus, commit + push its
|
|
30
|
+
> `hosts/<alias>.md` BEFORE anyone sends messages to it.** A host that joins
|
|
31
|
+
> an existing transport must seed its host file first; messages sent to it
|
|
32
|
+
> earlier won't be received.
|
|
33
|
+
>
|
|
34
|
+
> `crosstalk send` warns when `--to <actor>@<host>` names a host with no host
|
|
35
|
+
> file in the transport, so the silent drop becomes a visible error at send
|
|
36
|
+
> time.
|
|
37
|
+
>
|
|
38
|
+
> **Single-host operators don't need to think about this** — `crosstalk init`
|
|
39
|
+
> seeds your host file at setup, before any messaging, so the ordering can
|
|
40
|
+
> never bite you.
|
|
41
|
+
|
|
42
|
+
---
|
|
43
|
+
|
|
44
|
+
## Choosing and wiring an agent
|
|
45
|
+
|
|
46
|
+
Crosstalk is **agent-agnostic**. An actor is any CLI that reads a prompt and
|
|
47
|
+
prints a reply — there is no built-in model and no vendor requirement. You wire
|
|
48
|
+
your agent in the host file, under the actor's tier, as a `cli:` string:
|
|
49
|
+
|
|
50
|
+
```yaml
|
|
51
|
+
# hosts/<your-host>.md
|
|
52
|
+
---
|
|
53
|
+
alias: my-laptop
|
|
54
|
+
hostname: my-laptop
|
|
55
|
+
actors:
|
|
56
|
+
concierge:
|
|
57
|
+
default:
|
|
58
|
+
cli: <your-agent-cli> # the command crosstalk runs to invoke the agent
|
|
59
|
+
---
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
### The contract
|
|
63
|
+
|
|
64
|
+
When the dispatcher invokes an actor it:
|
|
65
|
+
|
|
66
|
+
1. composes `system prompt + the message(s)` and **appends it as the last
|
|
67
|
+
argument to your CLI command**, then
|
|
68
|
+
2. reads the CLI's **stdout** as the reply body, and expects exit code **0**.
|
|
69
|
+
|
|
70
|
+
So a `cli:` works if the command runs **one prompt non-interactively** (taking
|
|
71
|
+
the prompt as its trailing positional or flag-value) and **prints the answer
|
|
72
|
+
to stdout**. Two things to get right per agent:
|
|
73
|
+
|
|
74
|
+
- **Non-interactive / skip-approval flag.** Each agent has a flag that stops it
|
|
75
|
+
from pausing for confirmation or opening a TUI. Use it, or every dispatch will
|
|
76
|
+
hang until the 5-minute timeout and land in the DLQ.
|
|
77
|
+
- **Trailing argument shape.** The dispatcher tacks the prompt on the end of
|
|
78
|
+
whatever you put in `cli:`. So `cli: codex exec` becomes
|
|
79
|
+
`codex exec "<prompt>"`, `cli: gemini -p` becomes `gemini -p "<prompt>"`,
|
|
80
|
+
and `cli: claude --print --dangerously-skip-permissions` becomes
|
|
81
|
+
`claude --print --dangerously-skip-permissions "<prompt>"`. Every modern
|
|
82
|
+
agent CLI is happy with one of those shapes.
|
|
83
|
+
|
|
84
|
+
> For prompts larger than 64 KB (huge batches) the dispatcher falls back to
|
|
85
|
+
> writing the prompt to **stdin** automatically — the default argv path covers
|
|
86
|
+
> every supported agent, and the stdin fallback keeps things working past the
|
|
87
|
+
> OS `ARG_MAX` limit.
|
|
88
|
+
|
|
89
|
+
### Recipes
|
|
90
|
+
|
|
91
|
+
Starting points — **verify flags against your installed CLI version**;
|
|
92
|
+
vendors change them.
|
|
93
|
+
|
|
94
|
+
| Agent | Example `cli:` | Notes |
|
|
95
|
+
|---|---|---|
|
|
96
|
+
| Claude Code | `claude --print --dangerously-skip-permissions` | accepts the prompt as a positional arg |
|
|
97
|
+
| OpenAI Codex | `codex exec` | `exec` = non-interactive; add its bypass/sandbox flag |
|
|
98
|
+
| Gemini CLI | `gemini -p` | `-p` one-shot prompt; add `--yolo` to skip approvals |
|
|
99
|
+
| Qwen Code | `qwen -p` | Gemini-CLI-family flags |
|
|
100
|
+
| opencode | `opencode run` | `run` = one-shot |
|
|
101
|
+
| Antigravity | `agy -p` | `-p`/`--print` non-interactive |
|
|
102
|
+
|
|
103
|
+
You can mix agents in one transport — different actors (or different hosts) can
|
|
104
|
+
run entirely different vendors. Senders never know or care which agent answered.
|
|
105
|
+
|
|
106
|
+
> **Privacy note for shared infrastructure.** The prompt is passed as an
|
|
107
|
+
> argv arg, which means it's visible to anything that can read the
|
|
108
|
+
> dispatcher process's `/proc/<pid>/cmdline` (e.g. `ps auxww`). On a single-
|
|
109
|
+
> user laptop or dedicated dispatcher VM that's a non-issue; on multi-user
|
|
110
|
+
> machines, plan accordingly.
|
|
111
|
+
|
|
112
|
+
### Tiers (optional)
|
|
113
|
+
|
|
114
|
+
A tier is a named CLI slot under an actor. The bare-string shorthand is `count: 1`;
|
|
115
|
+
the object form adds `count:` for parallel instances. Senders may request a tier
|
|
116
|
+
with `--tier <name>`; the dispatcher falls back to the first declared tier when
|
|
117
|
+
the requested one is absent.
|
|
118
|
+
|
|
119
|
+
```yaml
|
|
120
|
+
actors:
|
|
121
|
+
junior-developer:
|
|
122
|
+
fast:
|
|
123
|
+
cli: gemini -p --yolo
|
|
124
|
+
count: 5 # five parallel slots picking up work independently
|
|
125
|
+
```
|
|
126
|
+
|
|
127
|
+
---
|
|
128
|
+
|
|
129
|
+
## Sending messages
|
|
130
|
+
|
|
131
|
+
### `crosstalk send --to <actor> --channel <uuid> "<body>"`
|
|
132
|
+
|
|
133
|
+
Post a message to an actor. The channel must already exist (see `crosstalk channel`).
|
|
134
|
+
|
|
135
|
+
```sh
|
|
136
|
+
crosstalk send \
|
|
137
|
+
--to concierge \
|
|
138
|
+
--channel 56f57ff9-7031-4ab2-9b98-4f299d3240fb \
|
|
139
|
+
"What is the capital of France?"
|
|
140
|
+
```
|
|
141
|
+
|
|
142
|
+
Prints `Sent: <relPath>` — save that path if you need to check for replies.
|
|
143
|
+
|
|
144
|
+
| Flag | Required | Notes |
|
|
145
|
+
|---|---|---|
|
|
146
|
+
| `--to <actor[,actor]>` | yes | bare name or `actor@host`; comma-separated list OK |
|
|
147
|
+
| `--channel <uuid>` | only if multiple channels exist | Auto-detected when the transport has exactly one channel; required to disambiguate when there are several. |
|
|
148
|
+
| `--from <actor>` | no | defaults to `$USER` / `operator` |
|
|
149
|
+
| `--tier <name>` | no | request a specific model tier |
|
|
150
|
+
| `--new` | no | suppress automatic `re:` linking (start new work, not a reply) |
|
|
151
|
+
|
|
152
|
+
---
|
|
153
|
+
|
|
154
|
+
## Running a dispatcher
|
|
155
|
+
|
|
156
|
+
### `crosstalk dispatch`
|
|
157
|
+
|
|
158
|
+
Start the dispatch loop in the current transport. Runs forever, polling for new messages and invoking actor CLIs.
|
|
159
|
+
|
|
160
|
+
```sh
|
|
161
|
+
crosstalk dispatch --poll 30 --json
|
|
162
|
+
```
|
|
163
|
+
|
|
164
|
+
| Flag | Default | Notes |
|
|
165
|
+
|---|---|---|
|
|
166
|
+
| `--poll <seconds>` | `30` | quiet-tick interval; active ticks drop to 1s automatically |
|
|
167
|
+
| `--host <alias>` | auto-detect | override which `hosts/<alias>.md` to load |
|
|
168
|
+
| `--json` | off | structured JSON log lines |
|
|
169
|
+
| `--log-file <path>` | none | mirror logs to a file |
|
|
170
|
+
| `--once` | off | run one tick then exit (useful for testing) |
|
|
171
|
+
|
|
172
|
+
> **macOS note — hostname auto-detect.** macOS's kernel hostname (`os.hostname()`) can drift across network state when the static **HostName** is unset (`configd` re-derives it from DHCP, reverse-DNS, or your VPN/Tailscale machine name). The dispatcher works around this on Darwin by also trying `scutil --get LocalHostName` and its `.local` form when matching against `hosts/<alias>.md`. If auto-detect still fails, either pin `--host <alias>` or pin your kernel hostname once:
|
|
173
|
+
> ```sh
|
|
174
|
+
> sudo scutil --set HostName <your-host-alias>
|
|
175
|
+
> ```
|
|
176
|
+
|
|
177
|
+
Dispatcher state (cursors, DLQ, heartbeat, error log) is stored under `$CROSSTALK_STATE_DIR` (default `~/.local/state/crosstalk/<transport-id>/`) — never in the transport repo.
|
|
178
|
+
|
|
179
|
+
---
|
|
180
|
+
|
|
181
|
+
## Observability
|
|
182
|
+
|
|
183
|
+
### `crosstalk status`
|
|
184
|
+
|
|
185
|
+
Print host file, active channels, cursor positions, DLQ count, and dispatcher heartbeat.
|
|
186
|
+
|
|
187
|
+
```sh
|
|
188
|
+
crosstalk status
|
|
189
|
+
```
|
|
190
|
+
|
|
191
|
+
### `crosstalk replies --re <relPath>[,<relPath>...]`
|
|
192
|
+
|
|
193
|
+
Show which of your dispatched messages have replies. Matches via the runtime-written `re:` field — ground truth, not body-parsing.
|
|
194
|
+
|
|
195
|
+
```sh
|
|
196
|
+
crosstalk replies --re 2026/06/10/130847758Z-780a8b90.md
|
|
197
|
+
```
|
|
198
|
+
|
|
199
|
+
---
|
|
200
|
+
|
|
201
|
+
## DLQ
|
|
202
|
+
|
|
203
|
+
### `crosstalk dlq`
|
|
204
|
+
|
|
205
|
+
Manage the dead-letter queue — messages that failed to dispatch repeatedly.
|
|
206
|
+
|
|
207
|
+
```sh
|
|
208
|
+
crosstalk dlq --list # list quarantined entries
|
|
209
|
+
crosstalk dlq --show <id> # full detail for one entry
|
|
210
|
+
crosstalk dlq --retry <id> # re-queue for dispatch
|
|
211
|
+
```
|
|
212
|
+
|
|
213
|
+
A common first-run cause of DLQ entries: the actor's `cli:` isn't truly
|
|
214
|
+
non-interactive (missing its skip-approval flag), so dispatches hang until the
|
|
215
|
+
5-minute timeout. See **Choosing and wiring an agent**.
|
|
216
|
+
|
|
217
|
+
---
|
|
218
|
+
|
|
219
|
+
## Channels
|
|
220
|
+
|
|
221
|
+
### `crosstalk channel --name <name>`
|
|
222
|
+
|
|
223
|
+
Create a new channel and print its UUID.
|
|
224
|
+
|
|
225
|
+
```sh
|
|
226
|
+
crosstalk channel --name "sprint-42"
|
|
227
|
+
```
|
|
228
|
+
|
|
229
|
+
| Flag | Notes |
|
|
230
|
+
|---|---|
|
|
231
|
+
| `--name <name>` | required; unique within the transport |
|
|
232
|
+
| `--parent <uuid>` | make this a subchannel; it reports back to the parent |
|
|
233
|
+
|
|
234
|
+
---
|
|
235
|
+
|
|
236
|
+
## Interactive
|
|
237
|
+
|
|
238
|
+
### `crosstalk chat`
|
|
239
|
+
|
|
240
|
+
Send a message and wait for the reply in one command. Useful for interactive sessions from the terminal.
|
|
241
|
+
|
|
242
|
+
```sh
|
|
243
|
+
crosstalk chat --to concierge --channel <uuid>
|
|
244
|
+
```
|
|
245
|
+
|
|
246
|
+
### `crosstalk open --actor <name>`
|
|
247
|
+
|
|
248
|
+
Open an interactive REPL-style session with an actor — each exchange is a full dispatch cycle. Requires a TTY.
|
|
249
|
+
|
|
250
|
+
```sh
|
|
251
|
+
crosstalk open --actor concierge
|
|
252
|
+
```
|
|
253
|
+
|
|
254
|
+
### `crosstalk attach`
|
|
255
|
+
|
|
256
|
+
Attach to a running dispatch loop's live log output (like `tail -f`, but structured).
|
|
257
|
+
|
|
258
|
+
> Prefer talking to Crosstalk in plain language instead of memorising flags?
|
|
259
|
+
> See **[GUIDE-PROMPTS.md](GUIDE-PROMPTS.md)** for the natural-language operator interface.
|
|
260
|
+
|
|
261
|
+
---
|
|
262
|
+
|
|
263
|
+
## Misc
|
|
264
|
+
|
|
265
|
+
### `crosstalk wake`
|
|
266
|
+
|
|
267
|
+
Poke the dispatcher to tick immediately. Rarely needed — `send` already signals the dispatcher.
|
|
268
|
+
|
|
269
|
+
### `crosstalk version`
|
|
270
|
+
|
|
271
|
+
Print the installed runtime version.
|
|
272
|
+
|
|
273
|
+
### `crosstalk upgrade`
|
|
274
|
+
|
|
275
|
+
Check whether the transport's protocol version matches the installed runtime and apply any necessary upgrades to `upstream/`.
|
|
276
|
+
|
|
277
|
+
---
|
|
278
|
+
|
|
279
|
+
## Common patterns
|
|
280
|
+
|
|
281
|
+
**One-shot task from a script:**
|
|
282
|
+
```sh
|
|
283
|
+
UUID=$(crosstalk channel --name "batch-$(date +%s)" | grep -o '[0-9a-f-]\{36\}')
|
|
284
|
+
crosstalk send --to concierge --channel "$UUID" "Summarise this file: ..." > /dev/null
|
|
285
|
+
# dispatch loop will pick it up on next poll
|
|
286
|
+
```
|
|
287
|
+
|
|
288
|
+
**Check if fan-out replies are all in:**
|
|
289
|
+
```sh
|
|
290
|
+
# After dispatching to 5 peers, record their relPaths:
|
|
291
|
+
crosstalk replies --re path1.md,path2.md,path3.md,path4.md,path5.md
|
|
292
|
+
```
|
|
293
|
+
|
|
294
|
+
**Troubleshooting a stuck dispatcher:**
|
|
295
|
+
```sh
|
|
296
|
+
crosstalk status # check heartbeat freshness and DLQ count
|
|
297
|
+
crosstalk dlq --list # see what's quarantined
|
|
298
|
+
```
|
package/GUIDE-PROMPTS.md
ADDED
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
# Crosstalk Natural-Language Guide
|
|
2
|
+
|
|
3
|
+
You don't have to memorise the `crosstalk` CLI to use Crosstalk. You can just
|
|
4
|
+
**talk to it.**
|
|
5
|
+
|
|
6
|
+
Crosstalk has an *operator mode*: you open a conversation with an agent inside
|
|
7
|
+
your transport and ask for things in plain language — "ask the test-runner on
|
|
8
|
+
the server whether the build is green." The agent translates your words into the
|
|
9
|
+
right `crosstalk` commands, waits for the answer, and tells you what came back —
|
|
10
|
+
no UUIDs, no file paths, no git.
|
|
11
|
+
|
|
12
|
+
This is the companion to the **[CLI guide](GUIDE-CLI.md)**. Same system, two
|
|
13
|
+
front doors: type commands, or just speak.
|
|
14
|
+
|
|
15
|
+
> Operator mode is **agent-agnostic**, like the rest of Crosstalk. Whatever
|
|
16
|
+
> coding agent you already run — Claude Code, Codex, Gemini, Qwen, opencode,
|
|
17
|
+
> Antigravity — can be your operator interface, as long as it can read the
|
|
18
|
+
> transport's orientation file and run shell commands.
|
|
19
|
+
|
|
20
|
+
---
|
|
21
|
+
|
|
22
|
+
## Starting an operator conversation
|
|
23
|
+
|
|
24
|
+
Pick whichever fits your setup:
|
|
25
|
+
|
|
26
|
+
```sh
|
|
27
|
+
# Purpose-built: open an actor in an interactive operator session
|
|
28
|
+
crosstalk open --actor concierge
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
or simply **launch your agent CLI from inside the transport directory.** The
|
|
32
|
+
transport ships an orientation file at its root (`CLAUDE.md`, and equivalents
|
|
33
|
+
for other agents) that tells your agent it is in operator mode and how to drive
|
|
34
|
+
Crosstalk. Start talking.
|
|
35
|
+
|
|
36
|
+
> **Operator mode ≠ being an actor.** When you open a session this way, *you*
|
|
37
|
+
> (through the agent) are the human's interface — you are not `concierge` or any
|
|
38
|
+
> other actor, and you do **not** process incoming messages. Leave message
|
|
39
|
+
> *processing* to the running dispatcher (`crosstalk dispatch`). Don't run
|
|
40
|
+
> `crosstalk dispatch` or `crosstalk open` from an operator session — both
|
|
41
|
+
> compete with the dispatcher.
|
|
42
|
+
|
|
43
|
+
---
|
|
44
|
+
|
|
45
|
+
## The phrasebook
|
|
46
|
+
|
|
47
|
+
Say it however feels natural; these are the intents the operator agent
|
|
48
|
+
recognises and what it does for you.
|
|
49
|
+
|
|
50
|
+
| You say… | What happens |
|
|
51
|
+
|---|---|
|
|
52
|
+
| "ask concierge to summarise `report.md`" | sends the task to `concierge`, waits, surfaces the reply |
|
|
53
|
+
| "ask the junior-dev on the server to run the tests" | sends to `junior-developer@server`, waits, reports back |
|
|
54
|
+
| "ask everyone for a status update" | sends to `all`, collects the replies as they arrive |
|
|
55
|
+
| "in the *sprint-42* channel, ask concierge to plan the work" | scopes the message to that channel |
|
|
56
|
+
| "is the dispatcher alive?" / "check status" | runs `crosstalk status`, explains the heartbeat plainly |
|
|
57
|
+
| "what's stuck?" / "what's in the dead-letter queue?" | runs `crosstalk dlq`, summarises the failures |
|
|
58
|
+
| "retry that failed one" | runs `crosstalk dlq --retry <id>` |
|
|
59
|
+
| "make a channel called *release-7*" | creates the channel, tells you it's ready |
|
|
60
|
+
| "what channels exist?" | lists channels by their human names |
|
|
61
|
+
| "tweak the concierge actor to be more terse" | edits `local/actors/concierge.md`, commits, pushes |
|
|
62
|
+
| "pull the latest" | runs `git pull --rebase` |
|
|
63
|
+
|
|
64
|
+
---
|
|
65
|
+
|
|
66
|
+
## Coordinating a team in plain language
|
|
67
|
+
|
|
68
|
+
The real power is fan-out. Tell the coordinator the goal and let it dispatch:
|
|
69
|
+
|
|
70
|
+
> **You:** "Ask concierge to get three things done: update the changelog, run
|
|
71
|
+
> the test suite on the server, and draft release notes — then summarise when
|
|
72
|
+
> they're all back."
|
|
73
|
+
|
|
74
|
+
Behind the scenes the coordinator sends one message per task to the right
|
|
75
|
+
actors, ends its turn, and is automatically re-woken as each reply lands; when
|
|
76
|
+
all are in, it aggregates and answers you. You just see:
|
|
77
|
+
|
|
78
|
+
> *(waiting for concierge…)*
|
|
79
|
+
> **concierge:** All three done. Changelog updated, 142 tests green on the
|
|
80
|
+
> server, draft release notes attached below. …
|
|
81
|
+
|
|
82
|
+
You never see the orchestration — only the result.
|
|
83
|
+
|
|
84
|
+
---
|
|
85
|
+
|
|
86
|
+
## What to expect
|
|
87
|
+
|
|
88
|
+
- **It's asynchronous.** Each round-trip is roughly 5–30 seconds — your message
|
|
89
|
+
is committed and pushed, the recipient's dispatcher polls, the agent runs, its
|
|
90
|
+
reply is committed and pulled back. The operator agent will say something like
|
|
91
|
+
*"(waiting for concierge…)"* and surface the answer when it arrives.
|
|
92
|
+
- **Replies are matched by fact, not by claim.** Crosstalk records which message
|
|
93
|
+
each reply answers, so "has it replied yet?" is always answered from ground
|
|
94
|
+
truth — a busy or confused actor can't fake completion.
|
|
95
|
+
- **At-least-once delivery.** For anything with side effects (sending an email,
|
|
96
|
+
deploying), the operator agent — and the actors — check the channel for prior
|
|
97
|
+
completion before repeating. For lookups and advice, the occasional duplicate
|
|
98
|
+
is harmless.
|
|
99
|
+
- **If nothing comes back** (~10 minutes), the operator agent will offer to
|
|
100
|
+
check `status` and the DLQ for clues — usually a dispatcher that's down or an
|
|
101
|
+
actor whose CLI isn't wired correctly (see
|
|
102
|
+
[GUIDE-CLI.md → Choosing and wiring an agent](GUIDE-CLI.md#choosing-and-wiring-an-agent)).
|
|
103
|
+
|
|
104
|
+
---
|
|
105
|
+
|
|
106
|
+
## Tips for good results
|
|
107
|
+
|
|
108
|
+
- **Name the actor when it matters.** "ask concierge" reaches it on every host
|
|
109
|
+
that runs it; "ask concierge **on cachy**" pins it to one machine. Use the host
|
|
110
|
+
when *where* the work runs is part of the ask.
|
|
111
|
+
- **Name the channel for threaded work.** If you don't, and there's more than one
|
|
112
|
+
channel, the operator agent will ask which one rather than guess.
|
|
113
|
+
- **Be explicit for non-idempotent actions.** "deploy to staging" is a side
|
|
114
|
+
effect; say so clearly and the actor will verify it hasn't already happened.
|
|
115
|
+
- **You don't need to know UUIDs or paths — ever.** If the operator agent starts
|
|
116
|
+
showing you raw file paths or git output, tell it to "talk to me like a person";
|
|
117
|
+
hiding that machinery is its job.
|
|
118
|
+
|
|
119
|
+
---
|
|
120
|
+
|
|
121
|
+
## When to drop to the CLI
|
|
122
|
+
|
|
123
|
+
Natural language covers day-to-day operation. Reach for the
|
|
124
|
+
**[CLI guide](GUIDE-CLI.md)** when you're:
|
|
125
|
+
|
|
126
|
+
- scripting or automating (cron jobs, CI),
|
|
127
|
+
- setting up hosts and wiring agent CLIs,
|
|
128
|
+
- running the dispatcher itself, or
|
|
129
|
+
- debugging cursors / the DLQ in detail.
|
|
130
|
+
|
|
131
|
+
Both drive the exact same transport — use whichever is faster for the task in
|
|
132
|
+
front of you.
|
package/README.md
ADDED
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
# @cordfuse/crosstalk — runtime
|
|
2
|
+
|
|
3
|
+
> Part of the [Crosstalk repo](https://github.com/cordfuse/crosstalk) — the
|
|
4
|
+
> root README has the full problem statement, solution overview, and
|
|
5
|
+
> repository layout. The other tiers in the same repo are the
|
|
6
|
+
> [transport template](../transport/) and the
|
|
7
|
+
> [reference server image](../server/).
|
|
8
|
+
|
|
9
|
+
The `crosstalk` command-line tool. Installs as a single Node.js binary; runs
|
|
10
|
+
on a developer laptop or inside a server container. Provides every operator
|
|
11
|
+
verb in the Crosstalk protocol: scaffold a transport, send messages, run
|
|
12
|
+
the dispatcher loop, manage channels, retry the DLQ.
|
|
13
|
+
|
|
14
|
+
> **What Crosstalk is.** Crosstalk is an agent-agnostic swarm communication
|
|
15
|
+
> protocol built on git: a git repo is the message bus. Any CLI that accepts a
|
|
16
|
+
> prompt and prints a reply can be an actor; messages are committed files;
|
|
17
|
+
> coordination is peer-to-peer with no broker. Full background and problem
|
|
18
|
+
> space:
|
|
19
|
+
> **[github.com/cordfuse/crosstalk](https://github.com/cordfuse/crosstalk#the-problem)**.
|
|
20
|
+
|
|
21
|
+
---
|
|
22
|
+
|
|
23
|
+
## Install
|
|
24
|
+
|
|
25
|
+
```sh
|
|
26
|
+
npm install -g @cordfuse/crosstalk
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
Requires Node.js ≥ 20. Works on Linux and macOS. Windows is untested.
|
|
30
|
+
|
|
31
|
+
To verify:
|
|
32
|
+
|
|
33
|
+
```sh
|
|
34
|
+
crosstalk --version
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
## 60-second quickstart
|
|
38
|
+
|
|
39
|
+
```sh
|
|
40
|
+
# 1. Scaffold a transport (a git repo that is the message bus)
|
|
41
|
+
mkdir my-bus && cd my-bus && git init
|
|
42
|
+
crosstalk init .
|
|
43
|
+
git add -A && git commit -m "initial transport"
|
|
44
|
+
git remote add origin <your-repo-url> && git push -u origin main
|
|
45
|
+
|
|
46
|
+
# 2. Edit hosts/<this-host>.md to wire your agent's CLI under the actor.
|
|
47
|
+
# For example, to use Claude Code as `concierge`:
|
|
48
|
+
# actors:
|
|
49
|
+
# concierge:
|
|
50
|
+
# default:
|
|
51
|
+
# cli: claude --print --dangerously-skip-permissions
|
|
52
|
+
git add hosts && git commit -m "configure host" && git push
|
|
53
|
+
|
|
54
|
+
# 3. Start the dispatch loop on this machine
|
|
55
|
+
crosstalk dispatch &
|
|
56
|
+
|
|
57
|
+
# 4. Send a message — the dispatcher invokes your agent and commits its reply
|
|
58
|
+
crosstalk send --to concierge "What is the capital of France?"
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
The dispatcher invokes the configured CLI by appending the composed prompt as
|
|
62
|
+
the CLI's last argument (with automatic stdin fallback for prompts > 64 KB),
|
|
63
|
+
so almost any modern agent CLI works without a per-vendor wrapper.
|
|
64
|
+
|
|
65
|
+
## Subcommand reference
|
|
66
|
+
|
|
67
|
+
| Subcommand | Purpose |
|
|
68
|
+
|---|---|
|
|
69
|
+
| `crosstalk init <dir>` | Scaffold a new transport into the given directory (copies the [template](../transport/)). |
|
|
70
|
+
| `crosstalk send --to <actor> [...]` | Write a message into a channel and push it. Auto-detects the channel if there's only one. |
|
|
71
|
+
| `crosstalk dispatch` | Run the per-machine dispatch loop. Polls for new messages, invokes actor CLIs, commits + pushes replies. |
|
|
72
|
+
| `crosstalk stop` | Stop the running dispatcher on this machine cleanly. Reads `dispatcher.pid` and sends SIGTERM. |
|
|
73
|
+
| `crosstalk status` | Print host file, channels, cursors, DLQ count, dispatcher heartbeat. |
|
|
74
|
+
| `crosstalk replies --re <relPath>` | Check which dispatched messages have replies. |
|
|
75
|
+
| `crosstalk dlq [--retry <id>]` | Inspect or retry dead-letter entries. |
|
|
76
|
+
| `crosstalk channel --name <name>` | Create a new channel; prints the UUID. |
|
|
77
|
+
| `crosstalk chat / open / attach` | Interactive operator entrypoints. |
|
|
78
|
+
| `crosstalk wake` | Poke a running dispatcher to tick immediately. |
|
|
79
|
+
| `crosstalk upgrade` | Reconcile the transport's protocol version against this runtime. |
|
|
80
|
+
| `crosstalk version` | Print runtime version. |
|
|
81
|
+
|
|
82
|
+
Detailed flag reference for every subcommand: **[GUIDE-CLI.md](./GUIDE-CLI.md)**
|
|
83
|
+
(also published alongside this package).
|
|
84
|
+
|
|
85
|
+
For natural-language operator use ("ask concierge to summarise this file") see
|
|
86
|
+
**[GUIDE-PROMPTS.md](./GUIDE-PROMPTS.md)**.
|
|
87
|
+
|
|
88
|
+
---
|
|
89
|
+
|
|
90
|
+
## What ships in this package
|
|
91
|
+
|
|
92
|
+
- `bin/crosstalk.js` — the subcommand dispatcher; `npm install -g` links it as `crosstalk`.
|
|
93
|
+
- `src/` — TypeScript source; the runtime is executed via [tsx](https://www.npmjs.com/package/tsx) at run time (no compile step at install).
|
|
94
|
+
- `template/` — a snapshot of the [transport template](../transport/) at publish time. `crosstalk init` copies from here.
|
|
95
|
+
- `GUIDE-CLI.md`, `GUIDE-PROMPTS.md` — operator guides published alongside this package.
|
|
96
|
+
|
|
97
|
+
---
|
|
98
|
+
|
|
99
|
+
## Local development
|
|
100
|
+
|
|
101
|
+
```sh
|
|
102
|
+
# In the repo root
|
|
103
|
+
cd runtime
|
|
104
|
+
npm install
|
|
105
|
+
npm run build # tsc --noEmit type-check
|
|
106
|
+
node bin/crosstalk.js --version # invoke the dev binary directly
|
|
107
|
+
```
|
|
108
|
+
|
|
109
|
+
The runtime is type-checked but not transpiled — `tsx` runs the TypeScript
|
|
110
|
+
sources directly at invocation time. No build step is required for an
|
|
111
|
+
end-user install.
|
|
112
|
+
|
|
113
|
+
To work on the source while testing against a local transport, point the
|
|
114
|
+
binary at the dev path:
|
|
115
|
+
|
|
116
|
+
```sh
|
|
117
|
+
cd /path/to/your/transport
|
|
118
|
+
node /path/to/crosstalk/runtime/bin/crosstalk.js status
|
|
119
|
+
```
|
|
120
|
+
|
|
121
|
+
---
|
|
122
|
+
|
|
123
|
+
## Dependencies
|
|
124
|
+
|
|
125
|
+
- [`@cordfuse/turnq`](https://www.npmjs.com/package/@cordfuse/turnq) — advisory
|
|
126
|
+
turn-coordination for serializing the commit+push window across hosts.
|
|
127
|
+
Optional in single-host setups; falls back to a local lockfile when no
|
|
128
|
+
turnq URL is configured.
|
|
129
|
+
- [`tsx`](https://www.npmjs.com/package/tsx) — run TypeScript directly without a build step.
|
|
130
|
+
- [`yaml`](https://www.npmjs.com/package/yaml) — frontmatter parsing.
|
|
131
|
+
|
|
132
|
+
---
|
|
133
|
+
|
|
134
|
+
## License
|
|
135
|
+
|
|
136
|
+
MIT. See [LICENSE](https://github.com/cordfuse/crosstalk/blob/main/LICENSE) in the repo.
|
package/bin/crosstalk.js
CHANGED
|
@@ -9,12 +9,12 @@
|
|
|
9
9
|
|
|
10
10
|
import { existsSync, statSync } from 'fs';
|
|
11
11
|
import { resolve, join, dirname } from 'path';
|
|
12
|
-
import { spawnSync } from 'child_process';
|
|
12
|
+
import { spawnSync, spawn } from 'child_process';
|
|
13
13
|
import { fileURLToPath } from 'url';
|
|
14
14
|
import { createRequire } from 'module';
|
|
15
15
|
|
|
16
16
|
const SUBCOMMANDS = [
|
|
17
|
-
'dispatch', 'send', 'replies', 'wake', 'status', 'init', 'dlq', 'channel',
|
|
17
|
+
'dispatch', 'stop', 'send', 'replies', 'wake', 'status', 'init', 'dlq', 'channel',
|
|
18
18
|
'chat', 'open', 'attach', 'upgrade',
|
|
19
19
|
];
|
|
20
20
|
const STANDALONE_SUBCOMMANDS = new Set(['init']);
|
|
@@ -75,8 +75,23 @@ if (!STANDALONE_SUBCOMMANDS.has(cmd)) {
|
|
|
75
75
|
|
|
76
76
|
const require = createRequire(import.meta.url);
|
|
77
77
|
const tsxCli = require.resolve('tsx/cli');
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
78
|
+
|
|
79
|
+
// dispatch is long-running: use async spawn so SIGTERM/SIGINT forwarded to
|
|
80
|
+
// the tsx child kills the whole chain cleanly. All other subcommands are
|
|
81
|
+
// short-lived and spawnSync is fine.
|
|
82
|
+
if (cmd === 'dispatch') {
|
|
83
|
+
const child = spawn(process.execPath, [tsxCli, srcFile, ...argv.slice(1)], {
|
|
84
|
+
cwd,
|
|
85
|
+
stdio: 'inherit',
|
|
86
|
+
});
|
|
87
|
+
const forward = (sig) => { try { child.kill(sig); } catch {} };
|
|
88
|
+
process.on('SIGTERM', () => forward('SIGTERM'));
|
|
89
|
+
process.on('SIGINT', () => forward('SIGTERM'));
|
|
90
|
+
child.on('exit', (code, signal) => process.exit(signal ? 1 : (code ?? 0)));
|
|
91
|
+
} else {
|
|
92
|
+
const r = spawnSync(process.execPath, [tsxCli, srcFile, ...argv.slice(1)], {
|
|
93
|
+
cwd,
|
|
94
|
+
stdio: 'inherit',
|
|
95
|
+
});
|
|
96
|
+
process.exit(r.status ?? 1);
|
|
97
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@cordfuse/crosstalk",
|
|
3
|
-
"version": "6.0.0-alpha.
|
|
3
|
+
"version": "6.0.0-alpha.11",
|
|
4
4
|
"description": "Crosstalk runtime — async messaging between agents over git, across machines.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
@@ -15,15 +15,19 @@
|
|
|
15
15
|
"files": [
|
|
16
16
|
"bin/",
|
|
17
17
|
"src/",
|
|
18
|
-
"template/"
|
|
18
|
+
"template/",
|
|
19
|
+
"GUIDE-CLI.md",
|
|
20
|
+
"GUIDE-PROMPTS.md"
|
|
19
21
|
],
|
|
20
22
|
"scripts": {
|
|
21
23
|
"build": "tsc --noEmit",
|
|
22
24
|
"lint": "tsc --noEmit",
|
|
23
|
-
"test": "bun test"
|
|
25
|
+
"test": "bun test",
|
|
26
|
+
"prepack": "cp -r ../transport template && cp ../GUIDE-CLI.md ../GUIDE-PROMPTS.md .",
|
|
27
|
+
"postpack": "rm -rf template GUIDE-CLI.md GUIDE-PROMPTS.md"
|
|
24
28
|
},
|
|
25
29
|
"dependencies": {
|
|
26
|
-
"@cordfuse/turnq": "^0.4.
|
|
30
|
+
"@cordfuse/turnq": "^0.4.2",
|
|
27
31
|
"@lydell/node-pty": "^1.2.0-beta.12",
|
|
28
32
|
"tsx": "^4.20.0",
|
|
29
33
|
"yaml": "^2.8.0"
|
package/src/actor.ts
CHANGED
|
@@ -1,8 +1,31 @@
|
|
|
1
1
|
import { existsSync, readFileSync, readdirSync } from 'fs';
|
|
2
2
|
import { join } from 'path';
|
|
3
|
-
import { hostname as osHostname } from 'os';
|
|
3
|
+
import { hostname as osHostname, platform } from 'os';
|
|
4
|
+
import { spawnSync } from 'child_process';
|
|
4
5
|
import { parseFrontmatter } from './frontmatter.js';
|
|
5
6
|
|
|
7
|
+
// Collect the names this machine might be known by. On macOS, the kernel
|
|
8
|
+
// hostname (`os.hostname()`) drifts with DHCP/VPN/Tailscale when the static
|
|
9
|
+
// HostName is unset — `scutil --get LocalHostName` is the stable Bonjour
|
|
10
|
+
// name (e.g. `Steves-MacBook-Air`), and host files commonly use the `.local`
|
|
11
|
+
// form. Trying all variants makes auto-detect deterministic across network
|
|
12
|
+
// state without forcing every Mac operator to pin `--host`.
|
|
13
|
+
function candidateHostNames(): string[] {
|
|
14
|
+
const names = new Set<string>();
|
|
15
|
+
names.add(osHostname());
|
|
16
|
+
if (platform() === 'darwin') {
|
|
17
|
+
const r = spawnSync('scutil', ['--get', 'LocalHostName'], { encoding: 'utf-8' });
|
|
18
|
+
if (r.status === 0) {
|
|
19
|
+
const local = r.stdout.trim();
|
|
20
|
+
if (local) {
|
|
21
|
+
names.add(local);
|
|
22
|
+
names.add(`${local}.local`);
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
return [...names];
|
|
27
|
+
}
|
|
28
|
+
|
|
6
29
|
export interface HostActorTier {
|
|
7
30
|
cli: string;
|
|
8
31
|
count?: number;
|
|
@@ -36,13 +59,15 @@ export function findHostFile(transportRoot: string, override?: string): HostFile
|
|
|
36
59
|
if (!target) throw new Error(`Host file '${override}' not found in ${dir}`);
|
|
37
60
|
return parseHostFile(join(dir, target));
|
|
38
61
|
}
|
|
39
|
-
const
|
|
62
|
+
const names = candidateHostNames();
|
|
40
63
|
for (const f of files) {
|
|
41
64
|
const parsed = parseHostFile(join(dir, f));
|
|
42
|
-
|
|
65
|
+
for (const n of names) {
|
|
66
|
+
if (parsed.hostname === n || parsed.alias === n) return parsed;
|
|
67
|
+
}
|
|
43
68
|
}
|
|
44
69
|
throw new Error(
|
|
45
|
-
`No host file matches
|
|
70
|
+
`No host file matches any of [${names.join(', ')}] in ${dir}. ` +
|
|
46
71
|
`Pass --host <alias> to override.`,
|
|
47
72
|
);
|
|
48
73
|
}
|