@cordfuse/crosstalk 7.0.0-alpha.9 → 7.0.0-beta.1

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.
Files changed (72) hide show
  1. package/GUIDE-CLI.md +322 -0
  2. package/GUIDE-PROMPTS.md +118 -0
  3. package/LICENSE +21 -0
  4. package/README.md +402 -61
  5. package/bin/crosstalk.js +88 -54
  6. package/commands/agent.js +69 -0
  7. package/commands/auth.js +273 -0
  8. package/commands/channel.js +54 -44
  9. package/commands/chat.js +107 -71
  10. package/commands/daemon.js +120 -0
  11. package/commands/logs.js +108 -19
  12. package/commands/message.js +125 -0
  13. package/commands/server.js +153 -0
  14. package/commands/settings.js +49 -0
  15. package/commands/status.js +37 -13
  16. package/commands/token.js +136 -0
  17. package/commands/transport.js +270 -0
  18. package/commands/version.js +3 -3
  19. package/commands/workflow.js +234 -0
  20. package/deploy/crosstalk@.service +62 -0
  21. package/deploy/install.sh +82 -0
  22. package/lib/api-client.js +77 -22
  23. package/lib/credentials.js +207 -0
  24. package/lib/nativeServer.js +173 -0
  25. package/lib/resolve.js +101 -34
  26. package/package.json +27 -4
  27. package/src/activation.ts +104 -0
  28. package/src/api.ts +1716 -0
  29. package/src/auth/enforce.ts +68 -0
  30. package/src/auth/handlers.ts +266 -0
  31. package/src/auth/middleware.ts +132 -0
  32. package/src/auth/setup.ts +263 -0
  33. package/src/auth/tokens.ts +285 -0
  34. package/src/auth/users.ts +267 -0
  35. package/src/dispatch.ts +492 -0
  36. package/src/dispatchers.ts +91 -0
  37. package/src/filenames.ts +28 -0
  38. package/src/frontmatter.ts +26 -0
  39. package/src/init.ts +116 -0
  40. package/src/invoke.ts +201 -0
  41. package/src/log-buffer.ts +67 -0
  42. package/src/models.ts +283 -0
  43. package/src/resolve.ts +100 -0
  44. package/src/state.ts +190 -0
  45. package/src/stop.ts +37 -0
  46. package/src/transport.ts +243 -0
  47. package/src/web/auth-pages.ts +160 -0
  48. package/src/web/channels.ts +395 -0
  49. package/src/web/chat-page.ts +636 -0
  50. package/src/web/chat-pty.ts +254 -0
  51. package/src/web/dashboard.ts +129 -0
  52. package/src/web/layout.ts +237 -0
  53. package/src/web/stubs.ts +510 -0
  54. package/src/web/workflows.ts +490 -0
  55. package/src/workflow.ts +470 -0
  56. package/template/CLAUDE.md +10 -0
  57. package/template/CROSSTALK-VERSION +1 -0
  58. package/template/CROSSTALK.md +262 -0
  59. package/template/PROTOCOL.md +70 -0
  60. package/template/README.md +64 -0
  61. package/template/auth/.gitkeep +0 -0
  62. package/template/auth/README.md +224 -0
  63. package/template/data/crosstalk.yaml +196 -0
  64. package/template/gitignore +4 -0
  65. package/commands/down.js +0 -40
  66. package/commands/init.js +0 -243
  67. package/commands/pull.js +0 -22
  68. package/commands/replies.js +0 -40
  69. package/commands/restart.js +0 -29
  70. package/commands/rm.js +0 -109
  71. package/commands/run.js +0 -115
  72. package/commands/up.js +0 -135
@@ -0,0 +1,262 @@
1
+ # Crosstalk
2
+
3
+ Version: 7
4
+
5
+ Crosstalk is a shared file format over git that lets humans and AI agents communicate asynchronously across machines. **The git repository is the message bus.** No special software is required to participate beyond git itself.
6
+
7
+ Design rule for this spec: every feature must be explainable in one sentence. The runtime records facts at write time; it never reconstructs them by inference at read time.
8
+
9
+ ---
10
+
11
+ ## The six nouns
12
+
13
+ Every concept in Crosstalk maps to exactly one of these:
14
+
15
+ | Noun | Definition |
16
+ |---|---|
17
+ | **Transport** | A git repo, scaffolded by `crosstalk init`. The bus. |
18
+ | **Machine** | A host running the crosstalk engine (`crosstalk daemon dispatch`, spawned by `crosstalk server start` or systemd). Identifies itself via `CROSSTALK_ALIAS` (defaults to the transport name). |
19
+ | **Message** | A markdown file with YAML frontmatter, committed to a channel. The unit of work. |
20
+ | **Model** | A named agent invocation declared in `data/models.yaml` (e.g. `sonnet`, `codex-o3`). |
21
+ | **Actor** | An optional persona file (`local/actors/<name>.md`) that prepends to a model's prompt. |
22
+ | **Channel** | A UUID directory under `data/channels/`. A conversation thread. Optionally parented. |
23
+
24
+ ---
25
+
26
+ ## Transport layout
27
+
28
+ ```
29
+ <transport-root>/
30
+ CROSSTALK-VERSION # protocol version, single integer
31
+ PROTOCOL.md # agent orientation, prepended to every dispatch
32
+ CROSSTALK.md # this file
33
+ data/
34
+ models.yaml # the shared model vocabulary
35
+ channels/<uuid>/
36
+ CHANNEL.md # { name, optional parent }
37
+ YYYY/MM/DD/<msg>.md # messages
38
+ local/
39
+ actors/<name>.md # optional persona files
40
+ workflows/ # convention, not enforced — reusable workflow markdown
41
+ ```
42
+
43
+ That is the complete committed surface. **Dispatcher bookkeeping (cursor, heartbeat, pidfile, wake signal) is machine-local state and never lives in the repo** — it is kept under `~/.config/crosstalk/state/<transport-name>/`. The repo carries conversation; each machine carries its own progress through it.
44
+
45
+ There is no `hosts/` directory and no `upstream/` directory at all — v6's `upstream/`-vs-`local/` override hierarchy is gone. `local/` still exists as the home for actor persona files (`local/actors/<name>.md`). The model vocabulary in `data/models.yaml` is shared by everyone; each dispatcher claims the entries whose CLI is on its PATH.
46
+
47
+ ---
48
+
49
+ ## Channels
50
+
51
+ A channel is a UUID v4 directory under `data/channels/`. Create one with `crosstalk channel <name>`; the runtime generates the UUID and writes a `CHANNEL.md`:
52
+
53
+ ```
54
+ ---
55
+ name: review-and-synthesize
56
+ parent: 550e8400-e29b-41d4-a716-446655440000 # optional
57
+ ---
58
+ ```
59
+
60
+ Two fields. `name` is required and must be unique within the transport (the runtime rejects collisions at create / rename time). `parent` makes this a subchannel — a workflow's child channel uses this to record its provenance.
61
+
62
+ There is no `created_by`, no `created_at`, no prose body. Git's commit history records who and when.
63
+
64
+ There is no close signal; a finished channel simply goes quiet. To remove one: `crosstalk channel <name> --delete` (hard delete with typed-name confirmation).
65
+
66
+ ---
67
+
68
+ ## Messages
69
+
70
+ Every message is a markdown file with YAML frontmatter:
71
+
72
+ ```
73
+ ---
74
+ from: alice
75
+ to: sonnet@cachy
76
+ timestamp: 2026-06-11T19:00:00.000Z
77
+ ---
78
+
79
+ Message body here.
80
+ ```
81
+
82
+ ### Frontmatter fields
83
+
84
+ | Field | Required | Notes |
85
+ |---|---|---|
86
+ | `from` | yes | logical actor or operator name — unverified; trust boundary is repo access |
87
+ | `to` | yes | `<model>[@<machine>]`; bare model name reaches any dispatcher claiming it |
88
+ | `timestamp` | yes | ISO 8601 UTC |
89
+ | `re` | no | relPath (or list) of the message(s) this one answers — **written by the runtime, never by hand** |
90
+ | `as` | no | actor persona to prepend on dispatch |
91
+ | `wake_as` | no | persona to invoke the asker with when replies to this message wake them — written automatically by `crosstalk run` when invoked from a dispatched turn that had `--as`. Powers workflow persona continuity. |
92
+ | `child_channel` | no | UUID of a child channel created for this workflow message (set automatically by `crosstalk run --type workflow`) |
93
+ | `type` | no | always omitted on regular messages; `workflow` on workflow documents only |
94
+ | `failed` | no | `true` on failure replies written by the dispatcher |
95
+ | `error` | no | failure detail, paired with `failed: true` |
96
+
97
+ Readers must ignore unknown fields.
98
+
99
+ ### The `re:` field — causality is recorded, not inferred
100
+
101
+ A message **without** `re:` is a new task. A message **with** `re:` is a reply to the message(s) at the listed relPath(s) (paths relative to the channel directory). The field is a string for one target and a list for several.
102
+
103
+ The runtime sets `re:` from facts it directly observes:
104
+
105
+ - When an actor answers via stdout, the runtime writes the reply with `re:` listing every message in the dispatched batch from that asker.
106
+ - When a dispatched actor uses `crosstalk run`, the runtime injects the triggering relPath(s) into the environment and `run` records them automatically. An actor can suppress this (`--new`) to start genuinely new work.
107
+ - Messages written by operators carry no `re:` — they are new tasks by definition.
108
+
109
+ Actors never compute or hand-write `re:`.
110
+
111
+ ### Filenames
112
+
113
+ `data/channels/<uuid>/YYYY/MM/DD/HHMMSSmmmZ-<hex>.md` — current UTC time plus a hex suffix of at least 8 characters from a CSPRNG. Filenames sort chronologically and are collision-free by construction, so concurrent writers on different machines never produce git conflicts in message files.
114
+
115
+ ---
116
+
117
+ ## Activation — when does a message wake its addressee?
118
+
119
+ One rule:
120
+
121
+ > **A message wakes its addressee if it has no `re:` (a new task), or any `re:` entry points at a message the addressee sent.**
122
+
123
+ Consequences:
124
+
125
+ - Tasks always wake the model they address.
126
+ - A reply wakes whoever asked the question, and no one else.
127
+ - A reply addressed to someone who never asked (an FYI, a broadcast copy) is visible in the channel but does not wake them — fan-in cannot oscillate.
128
+ - Self-sent messages never wake their sender.
129
+
130
+ There are no other wake conditions and no inference. The dispatcher evaluates this rule with two field reads.
131
+
132
+ ---
133
+
134
+ ## Delivery semantics
135
+
136
+ **At-least-once.** Each dispatcher tracks one cursor (machine-global, not per-channel-per-model) recording the git commit the transport was last scanned at. If a machine crashes mid-tick, the next tick re-dispatches anything not past the cursor; a duplicate reply may land in the channel.
137
+
138
+ For idempotent work (lookups, computation, advice) duplicates are harmless. For non-idempotent side effects, the actor must check the channel for evidence of prior completion before acting. Crosstalk does not provide exactly-once semantics.
139
+
140
+ **Batched delivery.** When a dispatcher wakes a model, it hands over ALL pending messages addressed to that model in that channel in a single invocation. One activation drains the mailbox — a coordinator that fanned out to 10 peers wakes once and sees all 10 replies together.
141
+
142
+ The transport is an **append-only log**. No retraction at the protocol level. Retention is the operator's concern at the git/storage layer.
143
+
144
+ ---
145
+
146
+ ## Models
147
+
148
+ Models are declared in `data/crosstalk.yaml` — one file, shared by everyone. Schema is nested under providers (alpha.8+):
149
+
150
+ ```yaml
151
+ providers:
152
+ anthropic-personal:
153
+ env_file: auth/anthropic-personal.env
154
+ models:
155
+ sonnet: claude --print --dangerously-skip-permissions --model sonnet
156
+ haiku: claude --print --dangerously-skip-permissions --model haiku
157
+ google-personal:
158
+ env:
159
+ GEMINI_API_KEY: ${GOOGLE_PERSONAL_KEY}
160
+ models:
161
+ gemini-pro: gemini --skip-trust --yolo -p --model gemini-1.5-pro
162
+ ```
163
+
164
+ Models are addressed as `provider/model` (e.g. `anthropic-personal/sonnet`). Bare `<model>` is also accepted when exactly one claimed model has that name; ambiguous bare names fail with a pick-list.
165
+
166
+ **Command contract.** Each model's command is a whitespace-tokenized argv list — **not** a shell pipeline. The runtime splits on whitespace, treats the first token as the binary, and appends the message body as the final positional argument (stdin fallback for prompts > 64 KB). Quoting (`'…'`, `"…"`), command substitution (`$(…)`), pipes (`|`), and redirections (`>`, `<`) are NOT interpreted — they pass through as literal argv characters. If you need shell features, write a wrapper script and reference its path in the command. Wrappers can read the prompt from `$1` (the final argv) — never assume it's on stdin.
167
+
168
+ **Per-provider auth.** Two layouts coexist per provider:
169
+ - `env_file: <relpath>` — dotenv file, raw secrets, gitignore the file.
170
+ - `env: { KEY: ${HOST_VAR} }` — inline ${HOST_VAR} references resolved from process.env at spawn. Raw values rejected at parse time.
171
+
172
+ Spawn precedence (last-wins): `process.env → env_file → env → dispatch metadata`. See `auth/README.md` for per-agent recipes.
173
+
174
+ **PATH-based self-selection.** Each dispatcher reads `data/crosstalk.yaml` at boot, takes the first token of each model's command (`claude`, `codex`, `gemini`, …), and checks PATH. Only models whose CLI is installed locally are claimed. A laptop with `claude` and `gemini` installed claims any model that maps to those CLIs; a server with only `claude` claims just the Claude entries.
175
+
176
+ **Skip-bad-provider (alpha.11+).** A single typo'd provider is logged to stderr and skipped; the rest of the registry loads. The engine never goes dark on one bad entry.
177
+
178
+ Adding a new model = one line under a provider in `data/crosstalk.yaml`. No PR to crosstalk. Custom or private agents: drop a wrapper script `crosstalk-myagent` on PATH and reference it in the command.
179
+
180
+ ---
181
+
182
+ ## Actors
183
+
184
+ An actor is an optional persona file at `local/actors/<name>.md`. Plain markdown, no required frontmatter. The full body is prepended to a model's prompt as system context when `--as <name>` is used.
185
+
186
+ ```sh
187
+ crosstalk run --type primitive --to sonnet --as junior-developer "implement this"
188
+ crosstalk run --type primitive --to opus --as senior-architect "review this"
189
+ ```
190
+
191
+ Actors are not bound to machines or models. Any model can be invoked with any actor. There are no declarations, no tier wiring, no host file lookup. The runtime concatenates persona body + message body and ships.
192
+
193
+ No `orchestrator` actor exists. Workflows are runtime-managed (see below); the model never plays an orchestration role, only content roles (drafting, synthesizing).
194
+
195
+ ---
196
+
197
+ ## Workflows
198
+
199
+ A workflow is a markdown document with `type: workflow` in the frontmatter. Body is plain prose — no YAML steps, no `to:`, no `as:`:
200
+
201
+ ```markdown
202
+ ---
203
+ type: workflow
204
+ ---
205
+
206
+ Fan out 3 junior-developer drafts of the proposal using sonnet. Have opus synthesize the strongest unified version.
207
+ ```
208
+
209
+ When the operator runs `crosstalk run --type workflow <file>`:
210
+
211
+ 1. **Marker**: the document is written into the operator's current channel as a marker message addressed to the reserved recipient `workflow`. The frontmatter includes `child_channel: <uuid>` (a new sibling channel for the workflow's sub-traffic) and `dispatch_host: <alias>` (the dispatcher that owns this workflow's progression — exactly one).
212
+ 2. **Compile pass**: the owning dispatcher's `workflowTick` invokes the first claimed model with a fixed `COMPILE_PROMPT` against the workflow body. The model returns a JSON plan: `{fanout: {to, count, body}, synthesize: {to, body}}`. The plan is persisted at `data/channels/<child>/PLAN.json` (idempotency: re-running compile would be wasteful and PLAN.json's existence guards against it).
213
+ 3. **Fanout dispatch**: the owning dispatcher writes `plan.fanout.count` sub-primitives into the child channel. Recipients are scoped (`to: <model>@<dispatcher-alias>`, round-robined across the dispatcher registry) to avoid duplicate work under at-least-once activation.
214
+ 4. **Synthesize dispatch**: once all fanout replies are in (re:-linked), the owning dispatcher writes one synthesis sub-primitive: `body = plan.synthesize.body + "\n\n" + concatenated candidates`. Pinned to a single dispatcher (no replication).
215
+ 5. **Route**: when the synthesis reply lands, the owning dispatcher writes a final reply in the **parent** channel addressed to the workflow marker's `from:`, `re:`-linked back to the marker. A `COMPLETE` side file in the child channel marks the workflow done so future ticks skip it.
216
+
217
+ The model never sees state machinery: each invocation is a single-turn content task (draft, synthesize). The runtime owns orchestration deterministically. Failures (invalid compile output, unclaimed model in the compiled plan, fanout sub-failures) propagate as `failed: true` reply messages.
218
+
219
+ ---
220
+
221
+ ## Routing
222
+
223
+ The `to:` field accepts:
224
+
225
+ - `to: sonnet` — bare model name. Any dispatcher claiming `sonnet` may pick it up; first-grab wins via git push race.
226
+ - `to: sonnet@cachy` — narrowed to the dispatcher whose `--alias` is `cachy`.
227
+
228
+ The model name is everything before the `@`; the machine alias is everything after. The `re:` activation rule ignores `@machine` suffixes — only addressing honors them.
229
+
230
+ Use bare names for work-pool patterns where any machine will do; use `@machine` when which machine runs the work matters. This addressing is the entirety of Crosstalk's multi-machine model.
231
+
232
+ ---
233
+
234
+ ## Identity and trust
235
+
236
+ `from:` is an unverified string. The trust boundary is repository access: anyone who can push can claim any name. `to:` is a routing hint, not access control — every message is visible to anyone with repo access. Operators who need confidentiality or verified identity must secure the repository itself (private remote, SSH keys, branch protection).
237
+
238
+ ---
239
+
240
+ ## Coordination
241
+
242
+ Git is self-coordinating: filenames are collision-free and non-fast-forward pushes are rejected and retried with `git pull --rebase`. A transport works with no broker, no coordinator, no advisory lock service.
243
+
244
+ There is no turnq, no advisory locking, no token server. v6 carried these as churn-reducers; v7 confirmed git rebase-retry is the actual correctness mechanism and the rest was overhead.
245
+
246
+ ---
247
+
248
+ ## What v7 dropped vs v6
249
+
250
+ For operators familiar with v6: the entire `hosts/` directory, all tier declarations, the DLQ subsystem, operator-mode, the concierge actor name (renamed `orchestrator`, then deleted entirely — see Workflows), the orchestrator actor itself, `crosstalk attach/open/wake/dlq/upgrade` subcommands, the `@cordfuse/turnq` dependency, the `local/`-vs-`upstream/` actor override hierarchy, and the `data/memories/` shared-notes subsystem are all gone.
251
+
252
+ Alpha.18 (the v8-native series) dropped the Docker container deployment and the dual-package split. Operators now install a single npm package — `@cordfuse/crosstalk` — that contains both the operator CLI and the engine. The engine runs as a native host process (or systemd unit), spawned by `crosstalk server start`; the dispatch loop itself lives as the internal subcommand `crosstalk daemon dispatch`. There is no `crosstalk-server` container and no separate `@cordfuse/crosstalkd` package (the latter is deprecated on npm).
253
+
254
+ Subcommand surface (alpha.18+, strict noun-verb, mirrors `@cordfuse/llmux`):
255
+ - `auth login|logout|whoami|list|use` · `token create|list|revoke|rename`
256
+ - `server start|stop|restart|status|logs` · `transport init|rm`
257
+ - `channel list|create|rename|delete` · `message send|replies`
258
+ - `workflow compose|run|status` · `chat <agent>` · `agent list`
259
+ - `logs list|tail` · `settings show` · `status` · `version`
260
+ - `daemon dispatch|init|stop` (internal — spawned by `server start` + systemd)
261
+
262
+ The v6 → v7 transition is a clean break. There is no auto-migration tool; v6 transports stay on v6 (the runtime checks `CROSSTALK-VERSION` and refuses to operate on incompatible versions).
@@ -0,0 +1,70 @@
1
+ # Crosstalk Protocol — Agent Orientation
2
+
3
+ You are an actor in a Crosstalk transport. This file is prepended to your prompt every time a dispatcher invokes you. Read it once per session and behave accordingly.
4
+
5
+ ## The single most important rule
6
+
7
+ **Never write files under `data/channels/` by hand.** All message creation goes through `crosstalk message send` (primitives) or `crosstalk workflow run` (workflows). The runtime owns frontmatter; you own message bodies. Violating this rule will desync the bus and break every receiver. There is no exception, ever, even if "it would be faster."
8
+
9
+ If you find yourself wanting to use `cat <<EOF`, `printf`, or `echo` to write a markdown file into `data/channels/`, **stop**. Use `crosstalk message send` instead.
10
+
11
+ ## How to reply
12
+
13
+ **Just answer.** Your stdout becomes the body of a reply message. The runtime writes the YAML frontmatter (`from`, `to`, `timestamp`, `re`) for you. Never write frontmatter yourself.
14
+
15
+ Your reply is automatically addressed to whoever messaged you and `re:`-linked to their message. Replies wake only the participant who asked — answering is always safe and cannot start a message loop.
16
+
17
+ If you have nothing useful to say (e.g. the message is a reply that doesn't need acknowledging), exit with empty stdout. The dispatcher logs that as `dispatch_silent` and the asker's `crosstalk message replies` stays PENDING — visible, not lost.
18
+
19
+ ## Batched delivery
20
+
21
+ If several messages were waiting for you in a channel, you receive them all in one prompt, delimited by `--- Message K of N (from: ..., ref: ...) ---`. Process them collectively and reply once.
22
+
23
+ ## Tools
24
+
25
+ You have shell access from the transport root.
26
+
27
+ - `crosstalk message send --to <model>[@<machine>] [--as <persona>] [--fanout <n>] [--channel <name|uuid>] <body|file|->` — proactively message a model. Replying to your prompt needs no tool; just answer. Sends are automatically linked (`re:`) to the message you are currently processing. `<body>` can be inline, a file path, or `-` for stdin. Prints the sent relPath — keep it if you are orchestrating.
28
+ - `crosstalk workflow run <file>` — dispatch a workflow document (markdown with `type: workflow` frontmatter) from a file path. The runtime auto-creates a child channel and runs compile → fanout → synthesize → route.
29
+ - `crosstalk workflow compose "<prose>"` — interactive variant: the engine compiles your prose into a workflow plan, surfaces the YAML for `[y]es / [e]dit / [n]o` review, then submits.
30
+ - `crosstalk message replies <relPath> [<relPath>...]` — shows which of your dispatched messages have replies. Exits 0 when every target is REPLIED or FAILED, 2 while anything is PENDING. This is ground truth: replies are matched by the runtime-written `re:` field, not by anything a peer claims in its body.
31
+ - `crosstalk status` — transport snapshot: channels (name → uuid), dispatcher heartbeat, claimed models, error count.
32
+ - `crosstalk channel list` — list all channels with their UUIDs.
33
+ - `crosstalk channel create <name>` — create a channel; prints its UUID.
34
+ - `crosstalk channel rename <handle> <new-name>` — rename a channel (handle = name or UUID).
35
+ - `crosstalk channel delete <handle>` — delete a channel (the verb is your explicit consent).
36
+
37
+ There is no `wake` subcommand — `crosstalk message send` automatically pokes the dispatcher.
38
+
39
+ ## Workflows
40
+
41
+ A workflow is a markdown document with `type: workflow` in the frontmatter, dispatched via `crosstalk workflow run <file>` (or `crosstalk workflow compose "<prose>"` for the interactive prose variant). The runtime handles execution: it compiles the prose body into a structured plan and runs the fan-out → synthesize → reply loop deterministically. No model is ever asked to orchestrate.
42
+
43
+ You do not need to know whether the message you are processing was produced by a workflow or by a hand-rolled `crosstalk message send`. Treat every wake as a single task and just answer.
44
+
45
+ ## Addressing
46
+
47
+ `to: model` (bare) reaches any machine claiming that model.
48
+ `to: model@machine` narrows to one machine's dispatcher.
49
+
50
+ ## Delivery semantics
51
+
52
+ **At-least-once.** Each dispatcher tracks one cursor recording the git commit the transport was last scanned at. If a machine crashes mid-tick, the next tick re-dispatches anything not past the cursor; a duplicate reply may land in the channel.
53
+
54
+ For idempotent work (lookups, computation, advice) duplicates are harmless. For non-idempotent side effects (sending email, file deletion, payments), check the channel for evidence of prior completion before acting.
55
+
56
+ ## Failure handling
57
+
58
+ If your invocation fails (non-zero exit, no stdout when stdout was required, timeout), the dispatcher writes a failure reply with `failed: true` and `error: <text>` in the frontmatter. This is a normal message that wakes the original sender the same as a success reply. There is no DLQ subsystem and no retry subcommand.
59
+
60
+ ## Frontmatter contract (v7)
61
+
62
+ Required fields the runtime writes on every message: `from`, `to`, `timestamp`. Optional fields: `re`, `as`, `child_channel`, `failed`, `error`. The `type:` field is only valid as `type: workflow` on workflow documents — any other value (including v6's `type: text`) is a protocol violation and gets rejected by the dispatcher at parse time.
63
+
64
+ ## Do not
65
+
66
+ - **Write YAML frontmatter or files under `data/channels/` by hand** — the cardinal sin. Use stdout or `crosstalk message send`.
67
+ - Reply to messages addressed to other actors or models.
68
+ - Fabricate channel UUIDs — run `crosstalk channel list` or `crosstalk status`.
69
+ - Use your model-native memory or scratchpad systems for cross-session state — the channel IS your state.
70
+ - Hand-write `type: text` in any frontmatter — v6 muscle memory. v7 messages omit `type:` on regular messages.
@@ -0,0 +1,64 @@
1
+ # transport/ — the seed template
2
+
3
+ > Part of the [Crosstalk repo](../README.md) — the root README has the full problem statement, solution overview, and repository layout. As of alpha.18 the engine and operator CLI ship as one npm package (`@cordfuse/crosstalk`); the daemon is an internal subcommand (`crosstalk daemon dispatch`), not a separate binary.
4
+
5
+ Source-of-truth content that `crosstalk daemon init` copies into every new transport. Not a runtime artifact, not a published package on its own — it lives here in the monorepo and gets bundled into the published npm tarball at `prepack` time (`package.json` copies `transport/` → `template/`).
6
+
7
+ > **What Crosstalk is.** Crosstalk is an agent-agnostic swarm communication protocol built on git. A git repo is the message bus. Full background: **[the root README](../README.md)**.
8
+
9
+ ---
10
+
11
+ ## What lives here
12
+
13
+ | File | Purpose in the scaffolded transport |
14
+ |---|---|
15
+ | `CROSSTALK-VERSION` | Protocol version this transport is on (`7`). The runtime checks compatibility on every boot and refuses to dispatch against an incompatible version. |
16
+ | `CROSSTALK.md` | **The protocol specification.** Every behavioral rule the runtime obeys — message shape, activation, batching, cursors, durability semantics — is defined here. Authoritative. |
17
+ | `PROTOCOL.md` | Agent-facing orientation: prepended to every dispatched model's prompt so a fresh invocation knows it's running inside a Crosstalk transport and what's expected of it. |
18
+ | `data/crosstalk.yaml` | Seed provider + model registry. Operators edit this file to add or remove providers and models. Each dispatcher claims the models whose CLI is on its PATH. |
19
+ | `local/actors/` | Optional persona files (`<name>.md` each). Operator-authored. Empty by default in alpha.1+ — the v6/early-v7 `orchestrator.md` persona was deleted when workflows became runtime-managed (see `CROSSTALK.md` § Workflows). |
20
+ | `CLAUDE.md` | Repo-style orientation for an AI coding agent that lands inside the transport directory. Mirrors the librarian / monorepo convention used across Cordfuse projects. |
21
+ | `gitignore` | Becomes the transport's `.gitignore` after init renames it. (npm strips files literally named `.gitignore` from published tarballs; we ship it without the leading dot and the runtime renames it on copy.) |
22
+
23
+ What does **not** live here:
24
+
25
+ - `data/channels/<uuid>/` — created on demand by `crosstalk channel create <name>`.
26
+ - `local/actors/<name>.md` — operator-authored, not in the template.
27
+ - `workflows/` — convention not enforced; operators put workflow markdown wherever they like.
28
+ - `hosts/` — v7 has no host files. Routing is by `CROSSTALK_ALIAS` (dispatcher boot env) or the transport name.
29
+ - `README.md` for the operator's own transport — `crosstalk daemon init` generates a minimal one but operators edit freely.
30
+
31
+ ---
32
+
33
+ ## How `crosstalk transport init` consumes this
34
+
35
+ Alpha.18 ships the template inside the npm tarball at `template/` (copied from `transport/` by `package.json` `prepack`). At install time, `crosstalk transport init` (the operator verb) calls into `crosstalk daemon init <target-dir>` under the hood, which:
36
+
37
+ 1. Finds the bundled template at `<package-root>/template/` (or `<repo-root>/transport/` in monorepo dev).
38
+ 2. Recursively copies the template into the target directory under the operator's storage base.
39
+ 3. Renames the copied `gitignore` to `.gitignore`.
40
+
41
+ `crosstalk transport init` then runs `git init --initial-branch=main && git add -A && git commit -m "initial transport"` so the operator's local git config is the author identity, allocates a free API port, and records it at `<base>/<name>/api-port`.
42
+
43
+ No host file is generated. No first channel is generated. Operators create their first channel with `crosstalk channel create <name>` and start sending.
44
+
45
+ The runtime never reads from this directory at dispatch time — only at `init` time, and only from the bundled copy.
46
+
47
+ ---
48
+
49
+ ## Editing the template
50
+
51
+ Anything in this directory is the source of truth for every future transport scaffolded by `crosstalk transport init`. Edit conservatively:
52
+
53
+ - **`CROSSTALK.md`** — only when changing the protocol itself. Bump `CROSSTALK-VERSION` in the same change.
54
+ - **`PROTOCOL.md`** — when the agent-facing contract changes (new envelope fields, new activation rules, new CLI verbs the agent should use). Agents inherit this at dispatch time, so changes affect every actor immediately on next dispatch.
55
+ - **`data/crosstalk.yaml`** — when the canonical seed set of providers/models changes. Operators edit their own copy after init, so changes here only affect *newly initialized* transports.
56
+ - **`CLAUDE.md`** — orientation for repo-savvy agents that land inside a transport directory.
57
+
58
+ Existing transports do **not** automatically pick up template changes. Operators wanting newer template content can manually copy the file they want (or re-init fresh and migrate `data/`).
59
+
60
+ ---
61
+
62
+ ## License
63
+
64
+ MIT. See [LICENSE](../LICENSE).
File without changes
@@ -0,0 +1,224 @@
1
+ # Auth recipes — per-agent env vars
2
+
3
+ Two layouts. Pick whichever fits — they coexist per provider in `data/crosstalk.yaml`.
4
+
5
+ **`env_file:`** — points at a dotenv file under this directory. Engine reads it at agent spawn and merges KEY=VALUE pairs into the subprocess env. The dotenv file holds the raw secrets; it's operator-edited and should be gitignored (the `auth/` directory is gitignored by default in the transport scaffold).
6
+
7
+ ```yaml
8
+ providers:
9
+ anthropic-personal:
10
+ env_file: auth/anthropic-personal.env
11
+ models:
12
+ sonnet: claude --print --dangerously-skip-permissions --model sonnet
13
+ ```
14
+
15
+ **`env:`** (alpha.10) — inline map per provider. Values MUST be `${HOST_VAR}` references; raw secrets are refused at parse time so the yaml stays committable. Engine resolves each `${HOST_VAR}` from the host process environment at spawn time. Export the vars before `crosstalk up` (your shell rc, a sourced .env file, your secret store, etc.). Useful when running multiple accounts of the same provider so the provider→credential mapping is visible in one file.
16
+
17
+ ```yaml
18
+ providers:
19
+ google-personal:
20
+ env:
21
+ GEMINI_API_KEY: ${GOOGLE_PERSONAL_KEY}
22
+ models:
23
+ gemini-pro: gemini --skip-trust --yolo -p
24
+
25
+ google-work:
26
+ env:
27
+ GEMINI_API_KEY: ${GOOGLE_WORK_KEY}
28
+ models:
29
+ gemini-work: gemini --skip-trust --yolo -p
30
+ ```
31
+
32
+ **Both at once.** A provider can declare both `env_file:` AND `env:`. Precedence at spawn (last-wins): `process.env → env_file → env → dispatchEnv`. So `env:` overrides `env_file:` per-key — bulk-load from a file and override a single key inline.
33
+
34
+ **If a value is wrong** or a referenced `${VAR}` is missing on the host, the agent CLI surfaces its own auth error and the dispatched message gets a `failed: true` reply with the error text (claude writes to stdout, codex to stderr — both are captured in alpha.9+).
35
+
36
+ **Mode 0600 recommended** for `env_file:` paths. Crosstalk doesn't enforce it.
37
+
38
+ ---
39
+
40
+ ## Command contract (how the agent CLI is invoked)
41
+
42
+ The `<cli command>` value under each provider's `models:` is a **whitespace-tokenized argv list, not a shell pipeline.** The first token is the binary, the rest are static argv passed to it, and the runtime appends the message body as the **final positional argument** (stdin fallback when the prompt > 64 KB).
43
+
44
+ This matters when wiring a new agent:
45
+ - Quoting (`'…'`, `"…"`), command substitution (`$(…)`), pipes (`|`), and redirection (`>`, `<`) **do not work** — they pass through as literal argv characters. A `models: m: bash -c 'echo X'` line will break at runtime because `'echo`, `X'` arrive as separate argv entries.
46
+ - Most agent CLIs work fine because their invocation IS pure argv: `claude --print --model sonnet`, `gemini --skip-trust --yolo -p`, `codex exec --model o3`. The runtime appends the body, the CLI reads it as the trailing positional, done.
47
+ - For agents that genuinely need shell features (a script that pipes the prompt, transforms it, sets a dynamic env), write a wrapper script that reads the prompt from `$1` and drop it at `/crosstalk-root/.local/bin/<name>` inside the container (via `crosstalk chat --shell`). That directory is already on `$PATH` AND lives under the persistent `/crosstalk-root` volume, so the wrapper survives `crosstalk restart`. **Do not use `/usr/local/bin/`** — it's in the container's writable layer and gets wiped on every restart, so a wrapper there is gone before the dispatcher can claim it.
48
+
49
+ Example:
50
+ ```sh
51
+ crosstalk chat --shell
52
+ cat > /crosstalk-root/.local/bin/qwen-via-pipe <<'WRAP'
53
+ #!/usr/bin/env bash
54
+ exec qwen --yolo --model qwen3-coder <<<"$1"
55
+ WRAP
56
+ chmod +x /crosstalk-root/.local/bin/qwen-via-pipe
57
+ exit
58
+ crosstalk restart # rescan PATH so the wrapper gets claimed
59
+ ```
60
+
61
+ Then in `crosstalk.yaml`:
62
+ ```yaml
63
+ providers:
64
+ custom-qwen:
65
+ env_file: auth/openrouter.env
66
+ models:
67
+ piped: qwen-via-pipe
68
+ ```
69
+
70
+ **Absolute paths also work (alpha.12+).** If you'd rather keep wrappers organised under their own directory (`/crosstalk-root/wrappers/`, anywhere persistent), reference the absolute path directly: `piped: /crosstalk-root/wrappers/qwen-via-pipe`. The dispatcher resolves absolute first tokens by stat'ing the file rather than walking PATH.
71
+
72
+ **Skip-bad-provider / skip-bad-model.** A typo'd provider (alpha.11+) or an unreachable model binary (alpha.12+) doesn't dark every agent on the dispatcher — it's logged to stderr (visible via `crosstalk logs` or `docker logs`) and skipped. The rest of the registry loads. If a wrapper you've just dropped doesn't appear in `claimed_models`, check the logs for the exact reason (wrong path, not executable, etc.).
73
+
74
+ ---
75
+
76
+ ## claude (Claude Code)
77
+
78
+ Two paths:
79
+
80
+ ```
81
+ # auth/anthropic-personal.env
82
+
83
+ # Option 1 — subscription OAuth token (preferred for personal use):
84
+ # On a host with a browser, run once: claude setup-token
85
+ # Copy the printed token here.
86
+ CLAUDE_CODE_OAUTH_TOKEN=
87
+
88
+ # Option 2 — Console API key (preferred for headless / CI):
89
+ # Get from console.anthropic.com.
90
+ # ANTHROPIC_API_KEY=
91
+ ```
92
+
93
+ **Precedence:** if both are set, `CLAUDE_CODE_OAUTH_TOKEN` wins.
94
+
95
+ ---
96
+
97
+ ## gemini (Google Gemini CLI)
98
+
99
+ ```
100
+ # auth/google-personal.env
101
+
102
+ # Get key: https://aistudio.google.com/apikey
103
+ GEMINI_API_KEY=
104
+ ```
105
+
106
+ **Required CLI flag:** `gemini --skip-trust` (already in the template). Without it gemini refuses to start headless.
107
+
108
+ Vertex AI / Code Assist (GCA) alternatives: set `GOOGLE_GENAI_USE_VERTEXAI=1` or `GOOGLE_GENAI_USE_GCA=1` instead. See the gemini-cli docs for the full env list.
109
+
110
+ ---
111
+
112
+ ## agy (Antigravity CLI)
113
+
114
+ Reads `GEMINI_API_KEY` — same env var as gemini-cli. Two reasonable patterns:
115
+
116
+ - **Share one env file** with the google-* provider (the template's `google-agy` example points to `auth/google-personal.env`).
117
+ - **Isolated keys** — declare a separate `google-agy` provider with its own `auth/agy.env`.
118
+
119
+ agy also supports interactive Google OAuth (browser flow), but the env-key path is what crosstalk drives.
120
+
121
+ ---
122
+
123
+ ## codex (OpenAI Codex CLI)
124
+
125
+ **Codex doesn't read env vars for auth.** It uses its own credential store at `~/.codex/auth.json`. Run once via `crosstalk chat --shell`:
126
+
127
+ ```
128
+ crosstalk chat --shell
129
+ echo "$OPENAI_API_KEY" | codex login --with-api-key
130
+ exit
131
+ ```
132
+
133
+ The credential file persists in `/crosstalk-root/.codex/` across container restarts. No `env_file:` needed on the codex provider — leave it off entirely.
134
+
135
+ **Multi-account?** Express HOME isolation in the model's `cli:` string:
136
+
137
+ ```yaml
138
+ openai-codex-work:
139
+ models:
140
+ codex-work: env HOME=/var/lib/crosstalk-transport/auth/homes/codex-work codex --skip-git-repo-check
141
+ ```
142
+
143
+ Then `codex login --with-api-key` in that HOME gives you a second account, isolated.
144
+
145
+ ---
146
+
147
+ ## qwen (Qwen Code CLI)
148
+
149
+ Multi-provider via `--auth-type`. The OpenAI-compatible path is the cleanest headless route:
150
+
151
+ ```
152
+ # auth/openrouter.env (or any OpenAI-compat backend)
153
+ OPENAI_API_KEY=sk-or-v1-...
154
+ OPENAI_BASE_URL=https://openrouter.ai/api/v1
155
+ ```
156
+
157
+ **Required CLI flags:** `qwen --auth-type openai --yolo --model <provider/model>` (e.g. `qwen/qwen3-coder` on OpenRouter). The model id must match the backend's catalog.
158
+
159
+ Qwen's native OAuth tier was discontinued 2026-04-15 — use the env-key path.
160
+
161
+ ---
162
+
163
+ ## opencode
164
+
165
+ Reads multiple env vars depending on what providers you've configured:
166
+
167
+ - `OPENROUTER_API_KEY` — native OpenRouter integration
168
+ - `ANTHROPIC_API_KEY`, `OPENAI_API_KEY`, etc. — per-provider keys
169
+ - Or its own config at `~/.config/opencode/`
170
+
171
+ Mix and match per-provider in `data/crosstalk.yaml`:
172
+
173
+ ```yaml
174
+ openrouter:
175
+ env_file: auth/openrouter.env # OPENAI_API_KEY + OPENAI_BASE_URL (compat shim)
176
+ models:
177
+ opencode-or: opencode --model qwen/qwen3-coder
178
+ ```
179
+
180
+ ---
181
+
182
+ ## Local Ollama
183
+
184
+ ```
185
+ # auth/ollama-local.env
186
+ OPENAI_API_KEY=ollama # any non-empty string works — ollama has no auth
187
+ OPENAI_BASE_URL=http://host.docker.internal:11434/v1
188
+ ```
189
+
190
+ For a remote ollama on the tailnet (the "one GPU box serves the swarm" pattern):
191
+
192
+ ```
193
+ # auth/ollama-tailnet.env
194
+ OPENAI_API_KEY=ollama
195
+ OPENAI_BASE_URL=http://100.x.x.x:11434/v1 # the GPU host's Tailscale IP
196
+ ```
197
+
198
+ Then point qwen, codex, opencode (any OpenAI-compat client) at it.
199
+
200
+ ---
201
+
202
+ ## OpenRouter
203
+
204
+ A gateway, not an agent. Use as the OpenAI-compat backend for codex / qwen / opencode:
205
+
206
+ ```
207
+ # auth/openrouter.env
208
+ OPENAI_API_KEY=sk-or-v1-...
209
+ OPENAI_BASE_URL=https://openrouter.ai/api/v1
210
+ ```
211
+
212
+ **Model ids** carry the provider prefix: `qwen/qwen3-coder`, `google/gemini-2.5-pro`, `anthropic/claude-sonnet-4-5`, etc. Pass via `--model` in the `cli:` string.
213
+
214
+ ---
215
+
216
+ ## How env files are read
217
+
218
+ Standard dotenv:
219
+ - `KEY=value` per line
220
+ - Lines starting with `#` are comments
221
+ - Blank lines ignored
222
+ - No shell expansion, no escaping — value is taken literally after the first `=`
223
+
224
+ No special quoting. If your value has a `#`, it's still part of the value (not a comment).