@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 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
+ ```
@@ -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
- const r = spawnSync(process.execPath, [tsxCli, srcFile, ...argv.slice(1)], {
79
- cwd,
80
- stdio: 'inherit',
81
- });
82
- process.exit(r.status ?? 1);
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.1",
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.1",
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 hostName = osHostname();
62
+ const names = candidateHostNames();
40
63
  for (const f of files) {
41
64
  const parsed = parseHostFile(join(dir, f));
42
- if (parsed.hostname === hostName || parsed.alias === hostName) return parsed;
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 hostname '${hostName}' in ${dir}. ` +
70
+ `No host file matches any of [${names.join(', ')}] in ${dir}. ` +
46
71
  `Pass --host <alias> to override.`,
47
72
  );
48
73
  }