@cordfuse/crosstalk 7.0.0-beta.1 → 8.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -155,7 +155,7 @@ export function chatPage(d: ChatPageData): string {
155
155
 
156
156
  <section class="card picker-card">
157
157
  <h3>Interactive chat</h3>
158
- <p class="sub">Spawns a fresh agent process on the host attached to your browser via WebSocket. Close the tab, hit the back arrow, or type <code>/exit</code> to terminate gracefully. Hit the red <code>×</code> in the topbar for an immediate SIGKILL when an agent is wedged. Async-by-design: no persistent sessions for durable work, use the orch bus.</p>
158
+ <p class="sub">Opens a live terminal session with an agent, right in your browser. Type to interact; close the tab or type <code>/exit</code> to end it. Sessions aren't saved for work you want to keep, send a message on a channel and let the dispatcher reply. (To force-stop a stuck agent, the red <code>×</code> in the top bar kills it immediately.)</p>
159
159
 
160
160
  <div class="pick-row">
161
161
  <label for="chat-agent">agent</label>
@@ -17,7 +17,7 @@
17
17
  //
18
18
  // Async-by-design contract: NO persistent session. Each WS attach
19
19
  // spawns a fresh agent. There's no detach + reattach. If you want
20
- // durable state, send messages through the orch bus.
20
+ // durable state, send messages on a channel (the dispatcher replies).
21
21
 
22
22
  import type { Server as HttpServer } from 'http';
23
23
  import type { Duplex } from 'stream';
@@ -9,6 +9,7 @@ export interface DashboardData {
9
9
  alias: string;
10
10
  version: string;
11
11
  transportRoot: string;
12
+ transportKind: string;
12
13
  heartbeat: { ts: string; pid: number; version: string; alias: string | undefined } | null;
13
14
  cursor: string | null;
14
15
  claimedModels: string[];
@@ -28,13 +29,46 @@ export function dashboardPage(d: DashboardData): string {
28
29
  const namedChannels = d.channels.filter(c => c.name).map(c => c.name as string);
29
30
  const cursorShort = d.cursor ? d.cursor.slice(0, 8) + '…' : '—';
30
31
 
32
+ // Plain-language status banner so the first thing a newcomer reads is
33
+ // "what state am I in / what do I do next", not raw telemetry. Derived
34
+ // entirely from existing fields — no new backend.
35
+ const claimed = d.claimedModels.length;
36
+ const pendingTotal = d.pendingWork.reduce((sum, p) => sum + p.count, 0);
37
+ const hbStale = hbAge === null || hbAge >= 300_000;
38
+ let stColor: string, stLabel: string, stHint: string;
39
+ if (hbStale) {
40
+ stColor = '#f85149';
41
+ stLabel = 'Dispatcher not running';
42
+ stHint = 'No recent heartbeat — the engine isn\'t processing the bus. Start it with <code>crosstalk server start</code>.';
43
+ } else if (claimed === 0) {
44
+ stColor = '#d29922';
45
+ stLabel = 'No agents connected yet';
46
+ stHint = 'Connect your first agent in a guided setup — Crosstalk detects your installed CLIs and wires one up live. <a href="/welcome"><strong>Set up an agent →</strong></a>';
47
+ } else if (d.channels.length === 0) {
48
+ stColor = '#7ee787';
49
+ stLabel = `Ready · ${claimed} agent${claimed === 1 ? '' : 's'} connected`;
50
+ stHint = 'Create a channel to start a conversation, or open an interactive <a href="/chat">Chat</a>.';
51
+ } else {
52
+ stColor = '#7ee787';
53
+ stLabel = `Live · ${claimed} agent${claimed === 1 ? '' : 's'}, ${d.channels.length} channel${d.channels.length === 1 ? '' : 's'}`;
54
+ stHint = pendingTotal > 0
55
+ ? `${pendingTotal} message${pendingTotal === 1 ? '' : 's'} awaiting a reply — the dispatcher picks them up on its next tick.`
56
+ : 'Every message on the bus has a reply.';
57
+ }
58
+
31
59
  const body = `
60
+ <section class="card" style="border-left:3px solid ${stColor}">
61
+ <h3 style="color:${stColor}">${escapeHtml(stLabel)}</h3>
62
+ <p class="sub" style="margin-bottom:0">${stHint}</p>
63
+ </section>
64
+
32
65
  <div class="grid cols-2">
33
66
  <section class="card">
34
67
  <h3>Engine</h3>
35
68
  <div class="kv"><span class="key">alias</span><span class="val brand">${escapeHtml(d.alias)}</span></div>
36
69
  <div class="kv"><span class="key">version</span><span class="val">${escapeHtml(d.version)}</span></div>
37
- <div class="kv"><span class="key">transport</span><span class="val dim">${escapeHtml(d.transportRoot)}</span></div>
70
+ <div class="kv"><span class="key">transport</span><span class="val">${escapeHtml(d.transportKind)}</span></div>
71
+ <div style="font-size:11px;color:#7a7f87;word-break:break-all;margin:-2px 0 10px">${escapeHtml(d.transportRoot)}</div>
38
72
  <div class="kv"><span class="key">cursor</span><span class="val">${escapeHtml(cursorShort)}</span></div>
39
73
  </section>
40
74
 
package/src/web/layout.ts CHANGED
@@ -154,6 +154,15 @@ function commonStyles(): string {
154
154
  .toast.show{opacity:1;transform:translateX(-50%) translateY(0)}
155
155
  .toast.ok{color:#7ee787;border-color:#1f4528}
156
156
  .toast.err{color:#f85149;border-color:#4a2329}
157
+ /* Persistent sidebar on wide screens: the drawer is always open and the
158
+ content is inset to the right of it, so every nav item is visible
159
+ without opening the hamburger. Phones keep the slide-in drawer. */
160
+ @media (min-width:1024px){
161
+ body{margin-left:300px;max-width:1180px}
162
+ #nav-drawer{left:0;transition:none;box-shadow:none}
163
+ #nav-backdrop{display:none}
164
+ #nav-toggle{display:none}
165
+ }
157
166
  `;
158
167
  }
159
168
 
package/src/web/stubs.ts CHANGED
@@ -475,8 +475,8 @@ export function settingsPage(d: SettingsPageData): string {
475
475
  host: d.host, alias: d.alias, activeNav: 'settings', pageTitle: 'Settings',
476
476
  body: `
477
477
  <section class="card">
478
- <h3>Engine settings <span class="chip">read-only — backlog</span></h3>
479
- <p class="sub">Snapshot of the running engine. Editable settings are on the backlogfor now, restart the engine with different flags / env vars.</p>
478
+ <h3>Engine settings</h3>
479
+ <p class="sub">The running engine's configuration. Settings come from <code>data/crosstalk.yaml</code>, CLI flags, and environment variables edit those and restart the engine to change them. This page reflects what's currently live.</p>
480
480
  <div class="kv"><span class="key">alias</span><span class="val brand">${escapeHtml(d.alias)}</span></div>
481
481
  <div class="kv"><span class="key">version</span><span class="val">${escapeHtml(d.version)}</span></div>
482
482
  <div class="kv"><span class="key">transport root</span><span class="val dim" style="font-size:11px">${escapeHtml(d.transportRoot)}</span></div>
@@ -497,7 +497,7 @@ export function aboutPage(opts: CommonOpts & { version: string; transportRoot: s
497
497
  body: `
498
498
  <section class="card">
499
499
  <h3>Crosstalk</h3>
500
- <p class="sub">Async git-backed message bus + dispatcher for AI agent CLIs. Sibling product to <a href="https://github.com/cordfuse/llmux">@cordfuse/llmux</a> (interactive tmux-based session mux).</p>
500
+ <p class="sub">Async message bus + dispatcher for AI agent CLIs, carried by git or a plain (incl. shared) filesystem. Sibling product to <a href="https://github.com/cordfuse/llmux">@cordfuse/llmux</a> (interactive tmux-based session mux).</p>
501
501
  <div class="kv"><span class="key">engine version</span><span class="val brand">${escapeHtml(opts.version)}</span></div>
502
502
  <div class="kv"><span class="key">alias</span><span class="val">${escapeHtml(opts.alias)}</span></div>
503
503
  <div class="kv"><span class="key">host</span><span class="val">${escapeHtml(opts.host)}</span></div>
@@ -0,0 +1,172 @@
1
+ // wizard.ts — first-run onboarding, served at GET /welcome and linked from
2
+ // the dashboard when no agent is claimed yet. Three steps:
3
+ // 1. Connect an agent — POST /api/onboarding/connect (writes crosstalk.yaml
4
+ // + optional key, SIGHUPs the dispatcher so the model is claimed live).
5
+ // 2. Create a channel — POST /channels.
6
+ // 3. Send a message — POST /messages, then hand off to the channel view
7
+ // where the dispatcher's reply lands.
8
+ //
9
+ // The client JS builds DOM with string concatenation (no client-side template
10
+ // literals) so it nests cleanly inside this module's template string.
11
+
12
+ import { page, escapeHtml, TOAST_HELPER } from './layout.js';
13
+
14
+ export interface WizardPageData {
15
+ host: string;
16
+ alias: string;
17
+ }
18
+
19
+ export function wizardPage(d: WizardPageData): string {
20
+ const body = `
21
+ <section class="card">
22
+ <h3>Welcome to Crosstalk</h3>
23
+ <p class="sub">Crosstalk is a message bus where AI agent CLIs talk to each other and to you. Three quick steps to your first conversation.</p>
24
+ <div class="wiz-steps">
25
+ <span class="wiz-step active" data-step="1">1 · Connect an agent</span>
26
+ <span class="wiz-step" data-step="2">2 · Create a channel</span>
27
+ <span class="wiz-step" data-step="3">3 · Send a message</span>
28
+ </div>
29
+ </section>
30
+
31
+ <section class="card wiz-panel" id="panel-1">
32
+ <h3>Connect an agent</h3>
33
+ <p class="sub">These agent CLIs are installed on this engine. Pick one — Crosstalk writes the config and claims it live, no restart.</p>
34
+ <div id="agent-list"><p class="empty">Detecting installed agents…</p></div>
35
+ </section>
36
+
37
+ <section class="card wiz-panel" id="panel-2" style="display:none">
38
+ <h3>Create your first channel</h3>
39
+ <p class="sub">A channel is a conversation thread. Name it anything lowercase (e.g. <code>general</code>).</p>
40
+ <div class="field"><label for="ch-name">channel name</label><input id="ch-name" value="general" autocomplete="off"></div>
41
+ <div class="actions"><button id="ch-back" type="button">← back</button><button class="primary" id="ch-create" type="button">Create channel</button></div>
42
+ </section>
43
+
44
+ <section class="card wiz-panel" id="panel-3" style="display:none">
45
+ <h3>Send your first message</h3>
46
+ <p class="sub">Addressed to <span id="m-model" class="chip brand"></span> on <span id="m-channel" class="chip"></span>. The dispatcher delivers the reply to the channel.</p>
47
+ <div class="field"><label for="m-body">message</label><textarea id="m-body">Say hello and tell me one thing you can help with.</textarea></div>
48
+ <div class="actions"><button id="m-back" type="button">← back</button><button class="primary" id="m-send" type="button">Send message</button></div>
49
+ </section>
50
+
51
+ <section class="card wiz-panel" id="panel-done" style="display:none">
52
+ <h3>You're set up</h3>
53
+ <p class="sub">Your message is on the bus. Open the channel to watch your agent reply.</p>
54
+ <div class="actions"><a id="done-link" class="chip brand" href="/">View the conversation →</a></div>
55
+ </section>
56
+ `;
57
+
58
+ const js = `
59
+ ${TOAST_HELPER}
60
+ var state = { model: null, channel: null };
61
+ function show(step){
62
+ var panels = document.querySelectorAll('.wiz-panel');
63
+ for (var i=0;i<panels.length;i++) panels[i].style.display = 'none';
64
+ document.getElementById('panel-'+step).style.display = '';
65
+ var steps = document.querySelectorAll('.wiz-step');
66
+ for (var j=0;j<steps.length;j++){
67
+ var n = steps[j].getAttribute('data-step');
68
+ steps[j].classList.toggle('active', String(step) === 'done' || n <= String(step));
69
+ }
70
+ }
71
+ function esc(s){ var d = document.createElement('div'); d.textContent = s == null ? '' : String(s); return d.innerHTML; }
72
+ async function loadAgents(){
73
+ var list = document.getElementById('agent-list');
74
+ try{
75
+ var r = await fetch('/api/onboarding/agents');
76
+ var data = await r.json();
77
+ var installed = (data.agents || []).filter(function(a){ return a.installed; });
78
+ if (!installed.length){ list.innerHTML = '<p class="empty">No supported agent CLIs found on this engine PATH. Install one (claude, codex, gemini, …) and reload.</p>'; return; }
79
+ list.innerHTML = installed.map(function(a){
80
+ var badge = a.claimed ? '<span class="chip" style="color:#7ee787">connected</span>' : '';
81
+ var key = (a.needsKey && !a.claimed) ? '<input class="agent-key" placeholder="API key — optional, blank if already logged in" style="margin-top:8px" autocomplete="off">' : '';
82
+ // Already claimed (revisit, or claimed outside the wizard): nothing to
83
+ // POST — just let the row advance the wizard with this model, rather
84
+ // than stranding the user behind a disabled "done" button.
85
+ var btn = '<button class="primary use-btn" type="button">' + (a.claimed ? 'Continue →' : 'Use this') + '</button>';
86
+ return '<div class="agent-row" data-cli="' + esc(a.cli) + '" data-qualified="' + esc(a.provider + '/' + a.model) + '" data-claimed="' + (a.claimed ? '1' : '0') + '">' +
87
+ '<div style="flex:1 1 auto;min-width:0">' +
88
+ '<div><span class="chip brand">' + esc(a.label) + '</span> ' + badge + '</div>' + key +
89
+ '<div style="font-size:11px;color:#7a7f87;margin-top:4px">' + esc(a.keyHint) + '</div>' +
90
+ '</div>' + btn + '</div>';
91
+ }).join('');
92
+ var rows = list.querySelectorAll('.agent-row');
93
+ for (var i=0;i<rows.length;i++){
94
+ (function(row){
95
+ row.querySelector('.use-btn').addEventListener('click', function(){
96
+ if (row.getAttribute('data-claimed') === '1'){
97
+ state.model = row.getAttribute('data-qualified');
98
+ document.getElementById('m-model').textContent = state.model;
99
+ show(2);
100
+ } else {
101
+ connect(row);
102
+ }
103
+ });
104
+ })(rows[i]);
105
+ }
106
+ }catch(e){ list.innerHTML = '<p class="empty">Failed to load agents: ' + esc(e.message) + '</p>'; }
107
+ }
108
+ async function connect(row){
109
+ var cli = row.getAttribute('data-cli');
110
+ var keyEl = row.querySelector('.agent-key');
111
+ var btn = row.querySelector('.use-btn');
112
+ btn.disabled = true; btn.textContent = 'Connecting…';
113
+ try{
114
+ var r = await fetch('/api/onboarding/connect', { method:'POST', headers:{'content-type':'application/json'}, body: JSON.stringify({ cli: cli, apiKey: keyEl ? keyEl.value : undefined }) });
115
+ var data = await r.json();
116
+ if (!r.ok) throw new Error(data.error || 'connect failed');
117
+ state.model = data.qualified;
118
+ document.getElementById('m-model').textContent = data.qualified;
119
+ showToast(data.qualified + ' connected', 'ok');
120
+ show(2);
121
+ }catch(e){ showToast(e.message, 'err'); btn.disabled = false; btn.textContent = 'Use this'; }
122
+ }
123
+ document.getElementById('ch-back').addEventListener('click', function(){ show(1); });
124
+ document.getElementById('m-back').addEventListener('click', function(){ show(2); });
125
+ async function createChannel(){
126
+ var name = document.getElementById('ch-name').value.trim();
127
+ if (!name){ showToast('channel name required', 'err'); return; }
128
+ try{
129
+ var r = await fetch('/channels', { method:'POST', headers:{'content-type':'application/json'}, body: JSON.stringify({ name: name }) });
130
+ var data = await r.json();
131
+ if (!r.ok) throw new Error(data.error || 'create failed');
132
+ state.channel = name;
133
+ document.getElementById('m-channel').textContent = name;
134
+ show(3);
135
+ }catch(e){ showToast(e.message, 'err'); }
136
+ }
137
+ document.getElementById('ch-create').addEventListener('click', createChannel);
138
+ document.getElementById('ch-name').addEventListener('keydown', function(e){ if (e.key === 'Enter') { e.preventDefault(); createChannel(); } });
139
+ document.getElementById('m-send').addEventListener('click', async function(){
140
+ var text = document.getElementById('m-body').value.trim();
141
+ if (!text){ showToast('message required', 'err'); return; }
142
+ var btn = document.getElementById('m-send'); btn.disabled = true; btn.textContent = 'Sending…';
143
+ try{
144
+ var r = await fetch('/messages', { method:'POST', headers:{'content-type':'application/json'}, body: JSON.stringify({ type:'primitive', channel: state.channel, to: state.model, body: text }) });
145
+ var data = await r.json();
146
+ if (!r.ok) throw new Error(data.error || 'send failed');
147
+ document.getElementById('done-link').href = '/c/' + encodeURIComponent(state.channel);
148
+ show('done');
149
+ }catch(e){ showToast(e.message, 'err'); btn.disabled = false; btn.textContent = 'Send message'; }
150
+ });
151
+ loadAgents();
152
+ `;
153
+
154
+ const css = `
155
+ .wiz-steps{display:flex;gap:8px;flex-wrap:wrap;margin-top:10px}
156
+ .wiz-step{font-size:11px;color:#7a7f87;border:1px solid #1f2329;border-radius:12px;padding:4px 10px}
157
+ .wiz-step.active{color:#7cc4ff;border-color:#2d4a66}
158
+ .agent-row{display:flex;gap:12px;align-items:flex-start;padding:12px 0;border-bottom:1px solid #1f2329}
159
+ .agent-row:last-child{border-bottom:none}
160
+ .agent-key{width:100%}
161
+ `;
162
+
163
+ return page({
164
+ title: `crosstalk on ${escapeHtml(d.host)} · Welcome`,
165
+ host: d.host,
166
+ alias: d.alias,
167
+ pageTitle: 'Welcome',
168
+ extraCss: css,
169
+ body,
170
+ inlineScript: js,
171
+ });
172
+ }
@@ -100,7 +100,7 @@ export function workflowsListPage(d: WorkflowsListData): string {
100
100
  <div id="compose-step-prompt">
101
101
  <div class="field">
102
102
  <label for="cw-prompt">prompt</label>
103
- <textarea id="cw-prompt" rows="4" placeholder="e.g. compare how claude, codex, and gemini would refactor src/auth/users.ts to use bun's built-in crypto instead of node:crypto"></textarea>
103
+ <textarea id="cw-prompt" rows="4" placeholder="e.g. ask claude, codex, and gemini to each review README.md for unclear setup steps, then synthesize their notes into one list"></textarea>
104
104
  </div>
105
105
  <div class="field">
106
106
  <label for="cw-compiler">compiler agent</label>
@@ -1,6 +1,6 @@
1
1
  # Crosstalk transport
2
2
 
3
- You are inside a Crosstalk transport — a git repo that carries async messages between AI agents and humans across machines.
3
+ You are inside a Crosstalk transport — a directory (a git repo, or a plain/shared filesystem) that carries async messages between AI agents and humans across machines.
4
4
 
5
5
  Read these before doing anything here:
6
6
 
@@ -2,7 +2,7 @@
2
2
 
3
3
  Version: 7
4
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.
5
+ Crosstalk is a shared file format that lets humans and AI agents communicate asynchronously across machines. **A directory is the message bus** carried by git (the default: distributed, versioned, auditable) or a plain/shared filesystem (`transport: local`, including a mounted SMB/NFS share, no git required).
6
6
 
7
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
8
 
@@ -14,10 +14,10 @@ Every concept in Crosstalk maps to exactly one of these:
14
14
 
15
15
  | Noun | Definition |
16
16
  |---|---|
17
- | **Transport** | A git repo, scaffolded by `crosstalk init`. The bus. |
17
+ | **Transport** | The bus a directory scaffolded by `crosstalk transport init`. A git repo (default) or a plain/shared filesystem (`transport: local`). |
18
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`). |
19
+ | **Message** | A markdown file with YAML frontmatter, written to a channel. The unit of work. |
20
+ | **Model** | A named agent invocation declared in `data/crosstalk.yaml` (e.g. `sonnet`, `codex-o3`). |
21
21
  | **Actor** | An optional persona file (`local/actors/<name>.md`) that prepends to a model's prompt. |
22
22
  | **Channel** | A UUID directory under `data/channels/`. A conversation thread. Optionally parented. |
23
23
 
@@ -31,7 +31,7 @@ Every concept in Crosstalk maps to exactly one of these:
31
31
  PROTOCOL.md # agent orientation, prepended to every dispatch
32
32
  CROSSTALK.md # this file
33
33
  data/
34
- models.yaml # the shared model vocabulary
34
+ crosstalk.yaml # providers + models (+ optional transport: key)
35
35
  channels/<uuid>/
36
36
  CHANNEL.md # { name, optional parent }
37
37
  YYYY/MM/DD/<msg>.md # messages
@@ -42,7 +42,7 @@ Every concept in Crosstalk maps to exactly one of these:
42
42
 
43
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
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.
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/crosstalk.yaml` is shared by everyone; each dispatcher claims the entries whose CLI is on its PATH.
46
46
 
47
47
  ---
48
48
 
@@ -222,7 +222,7 @@ The model never sees state machinery: each invocation is a single-turn content t
222
222
 
223
223
  The `to:` field accepts:
224
224
 
225
- - `to: sonnet` — bare model name. Any dispatcher claiming `sonnet` may pick it up; first-grab wins via git push race.
225
+ - `to: sonnet` — bare model name. Any dispatcher claiming `sonnet` may pick it up; first-grab wins (via git push race on the git transport, via collision-free filenames on the filesystem transport).
226
226
  - `to: sonnet@cachy` — narrowed to the dispatcher whose `--alias` is `cachy`.
227
227
 
228
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.
@@ -49,7 +49,7 @@ You do not need to know whether the message you are processing was produced by a
49
49
 
50
50
  ## Delivery semantics
51
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.
52
+ **At-least-once.** Each dispatcher tracks one cursor recording where the transport was last scanned (a git commit on the git transport, an mtime watermark on the filesystem transport). 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
53
 
54
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
55
 
@@ -4,7 +4,7 @@
4
4
 
5
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
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)**.
7
+ > **What Crosstalk is.** Crosstalk is an agent-agnostic swarm communication protocol over a shared directory. The bus is a directory — a git repo (default) or a plain/shared filesystem (`transport: local`). Full background: **[the root README](../README.md)**.
8
8
 
9
9
  ---
10
10