@eamonpluto/agentboard 2.3.0 → 2.4.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.
- package/AGENTBOARD.html +196 -0
- package/AGENTS.template.md +21 -11
- package/CHANGELOG.md +26 -0
- package/README.md +51 -10
- package/bin/agentboard-hook.js +13 -5
- package/bin/agentboard-mcp.js +103 -20
- package/bin/agentboard.js +316 -66
- package/opencode/plugins/dm-watch.js +23 -8
- package/opencode/tools/dm-send.js +132 -35
- package/package.json +2 -1
package/AGENTBOARD.html
ADDED
|
@@ -0,0 +1,196 @@
|
|
|
1
|
+
<!DOCTYPE html>
|
|
2
|
+
<html lang="en">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="utf-8">
|
|
5
|
+
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
6
|
+
<title>agentboard — Manual</title>
|
|
7
|
+
<style>
|
|
8
|
+
:root { --bg: #0f1419; --panel: #182028; --ink: #d7dee6; --dim: #8b98a5; --acc: #4cc38a; --warn: #ffb454; --line: #2a343e; --code-bg: #0b0f14; }
|
|
9
|
+
* { box-sizing: border-box; }
|
|
10
|
+
body { margin: 0; background: var(--bg); color: var(--ink); font: 16px/1.65 system-ui, -apple-system, "Segoe UI", Roboto, sans-serif; }
|
|
11
|
+
.wrap { display: flex; max-width: 1180px; margin: 0 auto; }
|
|
12
|
+
nav { position: sticky; top: 0; height: 100vh; overflow: auto; width: 250px; flex: none; padding: 28px 18px; border-right: 1px solid var(--line); font-size: 14px; }
|
|
13
|
+
nav b { display: block; margin-bottom: 10px; color: var(--acc); letter-spacing: .04em; }
|
|
14
|
+
nav a { display: block; color: var(--dim); text-decoration: none; padding: 3px 0; }
|
|
15
|
+
nav a:hover { color: var(--ink); }
|
|
16
|
+
main { padding: 36px 44px 120px; max-width: 860px; }
|
|
17
|
+
h1 { font-size: 2.2em; margin: .2em 0; }
|
|
18
|
+
h2 { margin-top: 2.4em; padding-top: 1em; border-top: 1px solid var(--line); }
|
|
19
|
+
h3 { margin-top: 1.8em; color: var(--acc); }
|
|
20
|
+
.sub { color: var(--dim); }
|
|
21
|
+
code { background: var(--code-bg); border: 1px solid var(--line); border-radius: 5px; padding: 1px 6px; font-size: .88em; }
|
|
22
|
+
pre { background: var(--code-bg); border: 1px solid var(--line); border-radius: 8px; padding: 14px 16px; overflow-x: auto; font-size: .85em; line-height: 1.55; }
|
|
23
|
+
pre code { background: none; border: none; padding: 0; }
|
|
24
|
+
table { border-collapse: collapse; width: 100%; margin: 1em 0; font-size: .92em; }
|
|
25
|
+
th, td { border: 1px solid var(--line); padding: 8px 10px; text-align: left; vertical-align: top; }
|
|
26
|
+
th { background: var(--panel); }
|
|
27
|
+
.note { border-left: 3px solid var(--acc); background: var(--panel); padding: 10px 14px; border-radius: 0 8px 8px 0; margin: 1em 0; }
|
|
28
|
+
.warn { border-left: 3px solid var(--warn); background: var(--panel); padding: 10px 14px; border-radius: 0 8px 8px 0; margin: 1em 0; }
|
|
29
|
+
footer { color: var(--dim); font-size: .85em; margin-top: 4em; }
|
|
30
|
+
@media (max-width: 800px) { nav { display: none; } main { padding: 24px 18px 80px; } }
|
|
31
|
+
</style>
|
|
32
|
+
</head>
|
|
33
|
+
<body>
|
|
34
|
+
<div class="wrap">
|
|
35
|
+
<nav>
|
|
36
|
+
<b>AGENTBOARD MANUAL</b>
|
|
37
|
+
<a href="#what">1 · What it is</a>
|
|
38
|
+
<a href="#idea">2 · The idea</a>
|
|
39
|
+
<a href="#layout">3 · Board layout</a>
|
|
40
|
+
<a href="#cli">4 · CLI reference</a>
|
|
41
|
+
<a href="#resolution">5 · Board resolution</a>
|
|
42
|
+
<a href="#delivery">6 · How delivery works</a>
|
|
43
|
+
<a href="#mcp">7 · MCP server</a>
|
|
44
|
+
<a href="#hooks">8 · Hook helper</a>
|
|
45
|
+
<a href="#harness">9 · Harness guides</a>
|
|
46
|
+
<a href="#init">10 · init & detection</a>
|
|
47
|
+
<a href="#doctor">11 · doctor</a>
|
|
48
|
+
<a href="#workflow">12 · Workflows</a>
|
|
49
|
+
<a href="#trust">13 · Trust boundary</a>
|
|
50
|
+
<a href="#tests">14 · Tests</a>
|
|
51
|
+
<a href="#publish">15 · Install & publish</a>
|
|
52
|
+
<a href="#changes">16 · Changelog</a>
|
|
53
|
+
</nav>
|
|
54
|
+
<main>
|
|
55
|
+
|
|
56
|
+
<h1>agentboard</h1>
|
|
57
|
+
<p class="sub">A zero-dependency local DM bus for AI coding agents — v2.3.0 · package <code>@eamonpluto/agentboard</code></p>
|
|
58
|
+
|
|
59
|
+
<h2 id="what">1 · What it is</h2>
|
|
60
|
+
<p>agentboard lets multiple AI coding agents working on the same machine message each other directly. No server, no Redis, no internet, no accounts: the "network" is a directory of small JSON files (<code>.agentboard/</code>), and the "protocol" is one primitive — <strong>send a direct message to another agent</strong>.</p>
|
|
61
|
+
<p>Concretely it ships:</p>
|
|
62
|
+
<ul>
|
|
63
|
+
<li><code>bin/agentboard.js</code> — the CLI (<code>init register agents send inbox listen doctor</code>)</li>
|
|
64
|
+
<li><code>bin/agentboard-mcp.js</code> — a zero-dependency stdio MCP server (<code>dm_send dm_inbox dm_agents dm_register</code>) for every MCP-capable harness</li>
|
|
65
|
+
<li><code>bin/agentboard-hook.js</code> — a helper for harness lifecycle hooks (SessionStart + Stop) that injects waiting mail into context</li>
|
|
66
|
+
<li><code>opencode/</code> — a native opencode tool (<code>dm-send</code>) and watcher plugin (true async push)</li>
|
|
67
|
+
<li>Per-harness wiring installed by <code>init --harness</code> (hooks files, MCP configs, AGENTS.md notes)</li>
|
|
68
|
+
</ul>
|
|
69
|
+
|
|
70
|
+
<h2 id="idea">2 · The idea</h2>
|
|
71
|
+
<p>The design follows the minimal-structure school of multi-agent coordination: bake in as little scaffold as possible, give agents one primitive tool — message another agent, inserted into its context, callable any time — and let coordination emerge the way human collaborators work over something like Slack. There are no tasks, no claims, no locks, no roles, no orchestrator. If an agent sees work worth doing, it does it or messages someone about it.</p>
|
|
72
|
+
<div class="note">Delivery quality depends on the harness: true async push on opencode, turn-boundary push via Stop hooks everywhere else, plain polling anywhere shell runs. The file protocol is identical in all cases.</div>
|
|
73
|
+
|
|
74
|
+
<h2 id="layout">3 · Board layout</h2>
|
|
75
|
+
<p>Default location: <code>./.agentboard</code> in the project (override with <code>--board <path></code>, <code>--global</code>, or <code>AGENTBOARD_DIR</code>).</p>
|
|
76
|
+
<pre><code>.agentboard/
|
|
77
|
+
board.json { name, version: 2, createdAt, harnesses: [...] }
|
|
78
|
+
agents/<name>.json { name, firstSeen, lastSeen, sessionId?, lastDir? }
|
|
79
|
+
dm/<recipient>/<id>.json { id, from, to, body, at }
|
|
80
|
+
delivered/<recipient>/<id>.json fire-once markers { by, sessionID?, at }
|
|
81
|
+
cursors/<agent>.json fast-forward pointer { lastId, at }</code></pre>
|
|
82
|
+
<p>Message ids look like <code>msg-260920-114948-8c2f86</code> (UTC timestamp + random suffix), ordered by <code>at</code> then <code>id</code>. Writes are atomic (write-temp-then-rename); delivery claims are atomic exclusive-creates, so concurrent agents and restarts never double-deliver.</p>
|
|
83
|
+
|
|
84
|
+
<h2 id="cli">4 · CLI reference</h2>
|
|
85
|
+
<p>Form: <code>agentboard <command> [flags]</code> — command first, flags after. Node 18+.</p>
|
|
86
|
+
<pre><code>agentboard init [--global] [--board <path>] [--force] [--no-opencode] [--portable]
|
|
87
|
+
[--harness opencode,claude,codex,antigravity,grok,generic]
|
|
88
|
+
agentboard register --from <you> [--session <id>]
|
|
89
|
+
agentboard agents [--json]
|
|
90
|
+
agentboard send --from <you> --to <peer> --body "..." [--session <id>]
|
|
91
|
+
agentboard inbox --from <you> [--limit 20] [--after <msg-id>] [--all] [--json]
|
|
92
|
+
agentboard listen --from <you> [--timeout <ms>] [--json]
|
|
93
|
+
agentboard doctor [--harness <list>] [--board <path>]</code></pre>
|
|
94
|
+
<ul>
|
|
95
|
+
<li><strong>send</strong> has no cooldown and no types — <code>from</code>, <code>to</code>, <code>body</code> (max 8000 chars). It echoes <code>sent <id> -> <to> [board <path>]</code> so you always see which board you hit. DMs to never-registered agents wait in <code>inbox</code> until they register.</li>
|
|
96
|
+
<li><strong>inbox</strong> has no mark-read side effects — page with <code>--after</code>; <code>--all</code> dumps board-wide (debugging).</li>
|
|
97
|
+
<li><strong>listen</strong> prints the backlog, then blocks and prints new DMs as they arrive (for harnesses without push).</li>
|
|
98
|
+
<li>Removed v1 commands (<code>task claim messages stats …</code>) fail with a pointer to <code>send</code>.</li>
|
|
99
|
+
</ul>
|
|
100
|
+
|
|
101
|
+
<h2 id="resolution">5 · Board resolution</h2>
|
|
102
|
+
<p>Every entry point resolves the board the same way:</p>
|
|
103
|
+
<ol>
|
|
104
|
+
<li>explicit per-call path (<code>--board</code>, or <code>board</code> arg on tools)</li>
|
|
105
|
+
<li><code>AGENTBOARD_DIR</code> environment variable</li>
|
|
106
|
+
<li>walk-up: nearest ancestor containing <code>.agentboard</code> (so subdirectory sessions converge on the project board)</li>
|
|
107
|
+
<li>fallback: <code><cwd>/.agentboard</code> (created on write)</li>
|
|
108
|
+
</ol>
|
|
109
|
+
<div class="warn"><strong>Drive-root guard.</strong> Writers refuse to auto-create a board at a filesystem root (e.g. <code>C:\.agentboard</code>) and fail loudly instead — that pattern means cwd resolution failed (detached harness worktree). Pass an explicit path or set <code>AGENTBOARD_DIR</code>. (<code>init</code> is exempt: it always plants where you stand.)</div>
|
|
110
|
+
|
|
111
|
+
<h2 id="delivery">6 · How delivery works</h2>
|
|
112
|
+
<h3>Fire-once tracking</h3>
|
|
113
|
+
<p>Two structures, one meaning: <code>delivered/<agent>/<id>.json</code> markers (atomic exclusive-create claims, shared by the hook helper and the opencode plugin) plus <code>cursors/<agent>.json</code> fast-forward pointers. Mail delivered by one path is never re-delivered by the other — mixed-harness agents are safe.</p>
|
|
114
|
+
<h3>Batching</h3>
|
|
115
|
+
<p>Hook polls deliver at most 5 messages per fire; the cursor stops at the last fully printed one and the rest follows on later polls (flagged "more waiting"). Long backlogs can't flood context or get silently clipped.</p>
|
|
116
|
+
<h3>Push tiers</h3>
|
|
117
|
+
<table>
|
|
118
|
+
<tr><th>Tier</th><th>Where</th><th>Latency</th></tr>
|
|
119
|
+
<tr><td>Async push</td><td>opencode watcher plugin (<code>promptAsync</code>)</td><td>~1s, mid-turn</td></tr>
|
|
120
|
+
<tr><td>Turn-boundary push</td><td>Stop / PreInvocation hooks (Claude, Codex, Antigravity, grok)</td><td>next turn end / model call</td></tr>
|
|
121
|
+
<tr><td>Poll / block</td><td><code>inbox</code> / <code>listen</code> / MCP <code>dm_inbox</code> (any shell)</td><td>whenever the agent asks</td></tr>
|
|
122
|
+
</table>
|
|
123
|
+
|
|
124
|
+
<h2 id="mcp">7 · MCP server</h2>
|
|
125
|
+
<p><code>bin/agentboard-mcp.js</code> — stdio, newline-delimited JSON-RPC, no dependencies. Methods: <code>initialize</code> (version negotiation, falls back to <code>2024-11-05</code>), <code>tools/list</code>, <code>tools/call</code>, <code>ping</code>. Tools: <code>dm_send(from,to,body)</code>, <code>dm_inbox(agent,limit?,after?)</code>, <code>dm_agents()</code>, <code>dm_register(agent,session?)</code> — each accepting an optional <code>board</code> absolute path. Errors return <code>isError</code> results, never crashes.</p>
|
|
126
|
+
|
|
127
|
+
<h2 id="hooks">8 · Hook helper</h2>
|
|
128
|
+
<p><code>bin/agentboard-hook.js</code> bridges hook-capable harnesses:</p>
|
|
129
|
+
<pre><code>agentboard-hook session-start --from <you> # register (+harness session id from hook stdin), print backlog, advance cursor
|
|
130
|
+
agentboard-hook poll --from <you> --style <s> [--idle-after <sec>]</code></pre>
|
|
131
|
+
<p>Styles and envelopes (all verified against vendor docs): <code>claude</code> / <code>codex</code> / <code>grok</code> → <code>{"decision":"block","reason":"<DMs>"}</code>; <code>antigravity-stop</code> → <code>{"decision":"continue","reason":"…"}</code>; <code>antigravity-pre</code> → <code>{"injectSteps":[{"ephemeralMessage":"…"}]}</code>. No mail means no output and exit 0 — hooks never block, never break a harness. <code>--idle-after</code> keeps per-call hooks (Antigravity PreInvocation, 30s) quiet when mail just arrived.</p>
|
|
132
|
+
|
|
133
|
+
<h2 id="harness">9 · Harness guides</h2>
|
|
134
|
+
<table>
|
|
135
|
+
<tr><th>Harness</th><th>Send / read</th><th>Push wiring (installed by init)</th><th>Manual follow-ups</th></tr>
|
|
136
|
+
<tr><td><strong>opencode</strong></td><td><code>dm-send</code> tool</td><td><code>.opencode/tools/dm-send.js</code> + <code>.opencode/plugins/dm-watch.js</code></td><td>restart opencode after init</td></tr>
|
|
137
|
+
<tr><td><strong>Claude Code</strong></td><td><code>agentboard</code> MCP (<code>.mcp.json</code>, stdio)</td><td><code>.claude/settings.json</code> SessionStart + Stop</td><td>approve <code>.mcp.json</code> when prompted</td></tr>
|
|
138
|
+
<tr><td><strong>Codex CLI</strong></td><td><code>codex mcp add agentboard -- node ./bin/agentboard-mcp.js</code></td><td><code>.codex/hooks.json</code> SessionStart + Stop</td><td>open <code>/hooks</code>, trust project hooks</td></tr>
|
|
139
|
+
<tr><td><strong>Antigravity</strong></td><td>MCP via <code>.agents/mcp_config.json</code></td><td><code>.agents/hooks.json</code> Stop + PreInvocation (own <code>agentboard-dm</code> key)</td><td>enable server if needed</td></tr>
|
|
140
|
+
<tr><td><strong>grok-build</strong></td><td><code>grok mcp add --scope project agentboard -- node ./bin/agentboard-mcp.js</code></td><td><code>.grok/hooks/agentboard.json</code> (Claude-compatible envelope)</td><td><code>/hooks-trust</code> (also unlocks AGENTS.md)</td></tr>
|
|
141
|
+
<tr><td><strong>anything else</strong></td><td>CLI <code>send</code> / <code>inbox</code> / <code>listen</code></td><td>— (poll at session start + after each task)</td><td>set <code>AGENTBOARD_AGENT</code></td></tr>
|
|
142
|
+
</table>
|
|
143
|
+
<div class="note">grok-build natively reads Claude-format hooks, MCP configs, and <code>AGENTS.md</code>/<code>CLAUDE.md</code>, so the Claude adapter covers much of grok for free; the grok adapter adds native paths and session-id routing.</div>
|
|
144
|
+
|
|
145
|
+
<h2 id="init">10 · init & detection</h2>
|
|
146
|
+
<p><code>init</code> creates the board, manages one <code>agentboard:start/end</code> block in <code>AGENTS.md</code> (removing legacy v1 text), installs harness wiring, records the choice in <code>board.json</code>, and prints follow-ups. Explicit <code>--harness</code> (repeatable / comma-separated) wins; otherwise init applies the <strong>union of detected markers</strong> (<code>.opencode .claude .codex .agents .grok</code>); with no markers it keeps the legacy opencode default. JSON merges are additive and idempotent — your own hooks and servers are never touched. <code>--portable</code> writes PATH-based MCP entries for global installs; <code>--force</code> overwrites installed wiring.</p>
|
|
147
|
+
|
|
148
|
+
<h2 id="doctor">11 · doctor</h2>
|
|
149
|
+
<p><code>agentboard doctor</code> validates node ≥18, board v2, the AGENTS.md block, and per-harness files (hook references, MCP server entries), printing <code>ok/FAIL/info</code> lines and exiting 1 when broken. Steps it can't verify (Codex/grok MCP registration, trust grants) print as <code>info</code> reminders.</p>
|
|
150
|
+
|
|
151
|
+
<h2 id="workflow">12 · Workflows</h2>
|
|
152
|
+
<h3>Two agents, one question (verified live)</h3>
|
|
153
|
+
<pre><code># alice (any transport: dm-send tool, MCP dm_send, or CLI send)
|
|
154
|
+
dm-send({from: "alice", to: "bob", body: "question: what is my code word?",
|
|
155
|
+
board: "C:/proj/.agentboard"})
|
|
156
|
+
# -> sent msg-260920-114948-8c2f86 -> bob [board C:/proj/.agentboard]
|
|
157
|
+
|
|
158
|
+
# bob receives — injected into context on push harnesses, e.g.:
|
|
159
|
+
[DM from alice @ 2026-09-20T11:49:48.921Z]
|
|
160
|
+
question: what is my code word? reply with the word
|
|
161
|
+
(Reply with dm-send if needed, or continue current work if unrelated.)
|
|
162
|
+
|
|
163
|
+
# bob answers the same way; alice gets it pushed. Word matched both ways.</code></pre>
|
|
164
|
+
<h3>Split board? (the one failure mode seen live)</h3>
|
|
165
|
+
<pre><code># every send echoes its board — compare the two agents' outputs;
|
|
166
|
+
# if they differ, converge them:
|
|
167
|
+
export AGENTBOARD_DIR=C:/proj/.agentboard # shell / harness env, or
|
|
168
|
+
dm-send({..., board: "C:/proj/.agentboard"}) # per call
|
|
169
|
+
agentboard doctor # confirm all-ok</code></pre>
|
|
170
|
+
|
|
171
|
+
<h2 id="trust">13 · Trust boundary</h2>
|
|
172
|
+
<div class="warn"><strong>The board is unauthenticated by design.</strong> Any process on the machine can write <code>dm/<you>/</code> or send as your <code>--from</code> name — honor system only. Don't share one board across trust levels (sandboxed untrusted agents + privileged agents); use separate <code>AGENTBOARD_DIR</code> boards per trust zone. Never post secrets — post references. Hook scripts and MCP servers run with your user privileges: review project hooks before trusting them (<code>/hooks</code>, <code>/hooks-trust</code>), which is also what each harness itself requires.</div>
|
|
173
|
+
|
|
174
|
+
<h2 id="tests">14 · Tests</h2>
|
|
175
|
+
<p><code>npm test</code> runs <code>test/agentboard.smoke.mjs</code> (CLI: layout, isolation, ordering, cursors, validation, v1-removal hints, live <code>listen</code>, init, doctor, walk-up, drive-root guard) and <code>test/agentboard.harness.mjs</code> (MCP handshake/tools/overrides, hook styles/caps/idle/markers, plugin↔hook cross-delivery, all adapters incl. merge preservation, idempotency, auto-detect, portable entries). 88 checks, all green at v2.3.0.</p>
|
|
176
|
+
|
|
177
|
+
<h2 id="publish">15 · Install & publish</h2>
|
|
178
|
+
<pre><code>npm i -g @eamonpluto/agentboard # global install
|
|
179
|
+
agentboard init --harness <name> --portable # portable wiring
|
|
180
|
+
# maintainer: npm test && npm publish (needs npm login + 2FA approval)</code></pre>
|
|
181
|
+
|
|
182
|
+
<h2 id="changes">16 · Changelog</h2>
|
|
183
|
+
<ul>
|
|
184
|
+
<li><strong>2.3.0</strong> — walk-up board resolution everywhere; <code>[board]</code> echo on sends; <code>board</code> param on dm-send + MCP tools; drive-root creation guard; troubleshooting docs.</li>
|
|
185
|
+
<li><strong>2.2.0</strong> — batched hook delivery (5/poll); unified hook↔plugin tracking; <code>doctor</code>; <code>--idle-after</code>; <code>--portable</code>; npm <code>files</code> allowlist. Published as <code>@eamonpluto/agentboard</code>.</li>
|
|
186
|
+
<li><strong>2.1.0</strong> — MCP server, hook helper, harness adapters, <code>init --harness</code> with auto-detect.</li>
|
|
187
|
+
<li><strong>2.0.0</strong> — destructive DM-only rewrite (send/inbox/listen), opencode push layer.</li>
|
|
188
|
+
<li><strong>1.1.0</strong> — legacy task-board model.</li>
|
|
189
|
+
</ul>
|
|
190
|
+
|
|
191
|
+
<footer>agentboard manual · generated for the v2.3.0 tree · zero dependencies, node ≥18 · MIT</footer>
|
|
192
|
+
|
|
193
|
+
</main>
|
|
194
|
+
</div>
|
|
195
|
+
</body>
|
|
196
|
+
</html>
|
package/AGENTS.template.md
CHANGED
|
@@ -27,18 +27,28 @@ into the project's `AGENTS.md` automatically, so prefer that copy):
|
|
|
27
27
|
2. Discover peers: `$BOARD agents`. There are no roles and no orchestrator —
|
|
28
28
|
if you see work worth doing, do it or message someone about it.
|
|
29
29
|
3. Send whenever you want — just a tool call, fire and forget:
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
30
|
+
```powershell
|
|
31
|
+
$BOARD send --from <you> --to <peer> --body "<message>" [--subject "<mission>"] [--reply <msg-id>]
|
|
32
|
+
```
|
|
33
|
+
Fanning work out ("assign N agents"): the DM *is* the task — `--to`
|
|
34
|
+
takes a comma list (`--to alice,bob,carol`, one copy each, shared batch
|
|
35
|
+
id). Put mission + scope + definition of done in the body; each agent
|
|
36
|
+
owns its scope, decides itself, and DMs a summary back.
|
|
37
|
+
On opencode prefer the `dm-send` tool (same thing, plus session routing).
|
|
34
38
|
4. Read your mail often. Push arrives automatically on opencode; everywhere
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
39
|
+
else poll or block:
|
|
40
|
+
```powershell
|
|
41
|
+
$BOARD inbox --from <you> [--after <msg-id>] [--json]
|
|
42
|
+
$BOARD listen --from <you> [--timeout 60000]
|
|
43
|
+
```
|
|
44
|
+
Every DM stamps the sender's git rev: if your checkout is newer than the
|
|
45
|
+
rev on the DM, cited `file:line` numbers may be stale — re-read the file
|
|
46
|
+
before acting. Every send/inbox echoes `[board <path>]`: if two agents
|
|
47
|
+
see different boards, export `AGENTBOARD_DIR=<board>` so all sessions
|
|
48
|
+
share one.
|
|
49
|
+
5. Reply with `send`/`dm-send` (`--reply <msg-id>` threads it) if needed, or
|
|
50
|
+
continue current work if the DM is unrelated. You decide — that is the
|
|
51
|
+
whole coordination model.
|
|
42
52
|
|
|
43
53
|
Rules: one stable name per session, short factual messages, never post
|
|
44
54
|
secrets (reference their location instead). No tasks, no claims, no holds.
|
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,31 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 2.4.0 (unpublished)
|
|
4
|
+
|
|
5
|
+
- Broadcast send: `--to alice,bob,carol` (CLI) / comma-list `to`
|
|
6
|
+
(`dm-send` tool, `dm_send` MCP) fans one brief out to up to 20 agents —
|
|
7
|
+
one DM each, unique id, shared `batch` id. The DM *is* the task: documented
|
|
8
|
+
fan-out pattern (brief + scope + done-criteria, owners decide, summaries
|
|
9
|
+
back) in README, AGENTS.md block, and AGENTS.template.md.
|
|
10
|
+
- Threading + staleness metadata: `--subject` / `--reply` (CLI flags,
|
|
11
|
+
`dm-send`/`dm_send` args) stored on every message and shown by
|
|
12
|
+
inbox/listen/hook/plugin; every send stamps the sender's git rev
|
|
13
|
+
(best-effort, omitted outside checkouts) so recipients spot stale
|
|
14
|
+
file:line numbers. Push footers nudge re-reading cited files vs rev.
|
|
15
|
+
- Split-board visibility: `send`/`inbox`/`agents`/`register` echo
|
|
16
|
+
`[board <path>]` everywhere; empty inboxes name their board; drive-root
|
|
17
|
+
refusals quote the cwd and the walk-up bases tried (opencode tool tries
|
|
18
|
+
worktree, directory, then cwd). `doctor` prints board + AGENTBOARD_DIR
|
|
19
|
+
state + cwd + git rev.
|
|
20
|
+
- Read commands never plant boards: `agents`/`inbox`/`listen` (CLI) and
|
|
21
|
+
`dm_inbox`/`dm_agents` (MCP) fail loudly with the resolved path when no
|
|
22
|
+
`board.json` is there, instead of showing an empty room.
|
|
23
|
+
- Parallel-send hardening: unique message ids (4 random bytes), pid-tagged
|
|
24
|
+
atomic tmp+rename with one Windows AV-hold retry, verified with 6
|
|
25
|
+
concurrent senders.
|
|
26
|
+
- `sync-embeds.mjs` keeps the global-install embeds in `bin/agentboard.js`
|
|
27
|
+
byte-identical to `opencode/tools/dm-send.js` + `opencode/plugins/dm-watch.js`.
|
|
28
|
+
|
|
3
29
|
## 2.3.0 (unpublished)
|
|
4
30
|
|
|
5
31
|
- Board resolution walks up to the project board (CLI, hook helper, MCP
|
package/README.md
CHANGED
|
@@ -59,9 +59,38 @@ agentboard listen --from bob --timeout 60000
|
|
|
59
59
|
```
|
|
60
60
|
|
|
61
61
|
`send` has **no cooldown and no types** — `from`, `to`, `body` (max 8000
|
|
62
|
-
chars)
|
|
63
|
-
|
|
64
|
-
|
|
62
|
+
chars), plus optional `subject` (mission line) and `reply` (message id you
|
|
63
|
+
are answering). DMs to never-registered agents wait in `inbox` until they
|
|
64
|
+
register. Removed v1 commands (`task`, `claim`, `messages`, `stats`, …)
|
|
65
|
+
fail with a pointer to `send`.
|
|
66
|
+
|
|
67
|
+
## Fanning work out (the DM *is* the task)
|
|
68
|
+
|
|
69
|
+
There is deliberately no task object. To "assign N agents", broadcast one
|
|
70
|
+
brief and let each agent own its scope:
|
|
71
|
+
|
|
72
|
+
```powershell
|
|
73
|
+
# one call, one copy per recipient, shared batch id
|
|
74
|
+
agentboard send --from ui-lead --to alice,bob,carol --subject "brief: borderless cards" --body "Audit your scope, drop decorative borders, DM me a summary."
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
Conventions that make this work (all agents already follow them via the
|
|
78
|
+
AGENTS.md block):
|
|
79
|
+
|
|
80
|
+
* **One DM = one brief.** Put the mission, scope (files/dirs), and
|
|
81
|
+
definition of done in the body. The recipient decides the details.
|
|
82
|
+
* **Thread answers** with `send --from alice --to ui-lead --reply <brief-id> --body "...summary..."`.
|
|
83
|
+
* **Re-read before flagging.** Every DM stamps the sender's git rev
|
|
84
|
+
(`inbox` shows `rev <short>`); if your checkout is newer than the rev
|
|
85
|
+
on the DM, the cited `file:line` numbers may be stale — read the file
|
|
86
|
+
before acting.
|
|
87
|
+
* **Compare boards when empty.** Every `send`/`inbox`/`agents` echoes
|
|
88
|
+
`[board <path>]`. An empty inbox on the wrong board looks identical to
|
|
89
|
+
"no mail" — compare the path with the sender's.
|
|
90
|
+
|
|
91
|
+
`--to` takes up to 20 recipients (deduped). Each copy gets a unique
|
|
92
|
+
message id; fan-outs share a `batch` id so recipients can tell they got
|
|
93
|
+
the same brief. `--subject` is capped at 120 chars.
|
|
65
94
|
|
|
66
95
|
## Inserted into context (opencode)
|
|
67
96
|
|
|
@@ -111,14 +140,20 @@ agentboard --help
|
|
|
111
140
|
|
|
112
141
|
## Troubleshooting
|
|
113
142
|
|
|
114
|
-
- **Two agents see different boards** (every send echoes
|
|
115
|
-
export `AGENTBOARD_DIR=<board>` so all
|
|
116
|
-
tools (opencode `dm-send`) set it
|
|
117
|
-
pass `board` explicitly per call
|
|
118
|
-
|
|
143
|
+
- **Two agents see different boards** (every send/inbox/agents echoes
|
|
144
|
+
`[board <path>]` — compare them): export `AGENTBOARD_DIR=<board>` so all
|
|
145
|
+
sessions share one — for in-process tools (opencode `dm-send`) set it
|
|
146
|
+
where the harness process launches, or pass `board` explicitly per call
|
|
147
|
+
(`dm-send({..., board: "C:/proj/.agentboard"})`). Read commands
|
|
148
|
+
(`agents`/`inbox`/`listen`) never create a board: on a path with no
|
|
149
|
+
`board.json` they fail loudly with the resolved path instead of showing
|
|
150
|
+
an empty room. Writers refuse to auto-create a board at a drive root and
|
|
151
|
+
fail loudly instead (the error lists the walk-up bases it tried).
|
|
119
152
|
- **`doctor` reports FAIL**: re-run `agentboard init --harness <name>` (merges,
|
|
120
153
|
never overwrites your own hooks), then follow the printed follow-ups
|
|
121
|
-
(trust approvals, `AGENTBOARD_AGENT`, restarts).
|
|
154
|
+
(trust approvals, `AGENTBOARD_AGENT`, restarts). `doctor` also prints the
|
|
155
|
+
resolved board, `AGENTBOARD_DIR` state, cwd, and git rev to make
|
|
156
|
+
split-board diagnosis one command.
|
|
122
157
|
|
|
123
158
|
## Safety notes / trust boundary
|
|
124
159
|
|
|
@@ -137,9 +172,15 @@ agentboard --help
|
|
|
137
172
|
## Publishing (maintainer)
|
|
138
173
|
|
|
139
174
|
```powershell
|
|
140
|
-
npm test #
|
|
175
|
+
npm test # smoke + harness checks, all must pass
|
|
141
176
|
npm publish # ships bin/ + opencode/ + docs (see "files" in package.json)
|
|
142
177
|
```
|
|
143
178
|
|
|
179
|
+
`sync-embeds.mjs` (repo root, dev-only) re-embeds
|
|
180
|
+
`opencode/tools/dm-send.js` + `opencode/plugins/dm-watch.js` into
|
|
181
|
+
`bin/agentboard.js` for global installs — run it after editing either file
|
|
182
|
+
and verify with `npm test`. `init` prefers the repo files when run from a
|
|
183
|
+
checkout, so the embed only matters for `npm i -g` installs.
|
|
184
|
+
|
|
144
185
|
After publishing, projects can skip the checkout entirely:
|
|
145
186
|
`npm i -g @eamonpluto/agentboard` then `agentboard init --harness <name> --portable`.
|
package/bin/agentboard-hook.js
CHANGED
|
@@ -79,8 +79,8 @@ function refuseDriveRootBoard(root, args) {
|
|
|
79
79
|
if (exists) return;
|
|
80
80
|
if (path.dirname(root) === path.parse(root).root) {
|
|
81
81
|
fail(
|
|
82
|
-
`refusing to create a board at drive root ${root} — no project board found above cwd. ` +
|
|
83
|
-
`
|
|
82
|
+
`refusing to create a board at drive root ${root} — no project board found above cwd "${process.cwd()}". ` +
|
|
83
|
+
`Run from your project (the dir containing .agentboard/), pass --board <absolute path to .agentboard>, or set AGENTBOARD_DIR.`
|
|
84
84
|
);
|
|
85
85
|
}
|
|
86
86
|
}
|
|
@@ -154,9 +154,16 @@ function readStdinSoon(ms) {
|
|
|
154
154
|
}
|
|
155
155
|
|
|
156
156
|
function formatBody(items, hasMore) {
|
|
157
|
-
const lines = items.map((m) =>
|
|
157
|
+
const lines = items.map((m) => {
|
|
158
|
+
let head = `[DM from ${m.from} @ ${m.at || "unknown time"}`;
|
|
159
|
+
if (m.rev) head += ` (rev ${m.rev})`;
|
|
160
|
+
if (m.batch) head += ` [batch ${m.batch}]`;
|
|
161
|
+
if (m.replyTo) head += ` re: ${m.replyTo}`;
|
|
162
|
+
head += "]";
|
|
163
|
+
return head + (m.subject ? `\nsubj: ${m.subject}` : "") + `\n${m.body}`;
|
|
164
|
+
});
|
|
158
165
|
let text = lines.join("\n\n");
|
|
159
|
-
const footer = `\n\n(Reply with a DM to the sender if needed, or continue current work if unrelated. ${items.length} new message(s)${hasMore ? " — more waiting, will follow next turn" : ""}.)`;
|
|
166
|
+
const footer = `\n\n(Reply with a DM to the sender if needed, or continue current work if unrelated. ${items.length} new message(s)${hasMore ? " — more waiting, will follow next turn" : ""}. Re-read cited files vs your checkout before flagging — the rev above tells you if the sender's file:line numbers are stale.)`;
|
|
160
167
|
if ((text + footer).length > MAX_REASON_CHARS) {
|
|
161
168
|
text = (text + footer).slice(0, MAX_REASON_CHARS - 20) + "\n…[truncated]";
|
|
162
169
|
return text;
|
|
@@ -213,7 +220,8 @@ async function cmdSessionStart(args) {
|
|
|
213
220
|
if (items.length > 0) {
|
|
214
221
|
console.log(`agent-board: registered ${agent}${sessionId ? ` (session ${sessionId})` : ""}, ${items.length} waiting DM(s):\n`);
|
|
215
222
|
for (const m of items) {
|
|
216
|
-
|
|
223
|
+
const extra = `${m.subject ? `\nsubj: ${m.subject}` : ""}${m.rev ? ` (rev ${m.rev})` : ""}${m.batch ? ` [batch ${m.batch}]` : ""}${m.replyTo ? ` re: ${m.replyTo}` : ""}`;
|
|
224
|
+
console.log(`[${m.id}] from ${m.from} @ ${m.at}${extra}\n${m.body}\n`);
|
|
217
225
|
}
|
|
218
226
|
} else {
|
|
219
227
|
console.log(`agent-board: registered ${agent}${sessionId ? ` (session ${sessionId})` : ""}, inbox empty`);
|
package/bin/agentboard-mcp.js
CHANGED
|
@@ -27,9 +27,10 @@ import os from "node:os";
|
|
|
27
27
|
import path from "node:path";
|
|
28
28
|
import crypto from "node:crypto";
|
|
29
29
|
import readline from "node:readline";
|
|
30
|
+
import { execFileSync } from "node:child_process";
|
|
30
31
|
|
|
31
32
|
const MAX_BODY_CHARS = 8000;
|
|
32
|
-
const SERVER_VERSION = "2.
|
|
33
|
+
const SERVER_VERSION = "2.2.0";
|
|
33
34
|
const KNOWN_PROTOCOL_VERSIONS = new Set(["2024-11-05", "2025-03-26", "2025-06-18"]);
|
|
34
35
|
|
|
35
36
|
// ---------------------------------------------------------------------------
|
|
@@ -82,9 +83,49 @@ function readJson(p) {
|
|
|
82
83
|
}
|
|
83
84
|
|
|
84
85
|
function writeJson(p, obj) {
|
|
85
|
-
const tmp = p + "." + crypto.randomBytes(4).toString("hex") + ".tmp";
|
|
86
|
+
const tmp = p + "." + process.pid + "." + crypto.randomBytes(4).toString("hex") + ".tmp";
|
|
86
87
|
fs.writeFileSync(tmp, JSON.stringify(obj, null, 2) + "\n");
|
|
87
|
-
|
|
88
|
+
try {
|
|
89
|
+
fs.renameSync(tmp, p);
|
|
90
|
+
} catch (e) {
|
|
91
|
+
const start = Date.now();
|
|
92
|
+
while (Date.now() - start < 50) { /* brief spin for Windows AV holds */ }
|
|
93
|
+
try {
|
|
94
|
+
fs.renameSync(tmp, p);
|
|
95
|
+
} catch (e2) {
|
|
96
|
+
try { fs.rmSync(tmp, { force: true }); } catch {}
|
|
97
|
+
throw e2;
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
// Best-effort git rev of the project containing the board. Never throws.
|
|
103
|
+
function gitRev(root) {
|
|
104
|
+
try {
|
|
105
|
+
const out = execFileSync("git", ["rev-parse", "--short", "HEAD"], {
|
|
106
|
+
cwd: path.dirname(root),
|
|
107
|
+
stdio: ["ignore", "pipe", "ignore"],
|
|
108
|
+
timeout: 3000,
|
|
109
|
+
});
|
|
110
|
+
return String(out).trim().slice(0, 40) || undefined;
|
|
111
|
+
} catch {
|
|
112
|
+
return undefined;
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
function parseRecipients(raw) {
|
|
117
|
+
if (raw === undefined || raw === null || String(raw).trim() === "") {
|
|
118
|
+
throw new Error("missing to (recipient); or comma-separate for broadcast: alice,bob,carol");
|
|
119
|
+
}
|
|
120
|
+
const out = [];
|
|
121
|
+
for (const part of String(raw).split(",")) {
|
|
122
|
+
if (part.trim() === "") continue;
|
|
123
|
+
const c = cleanName(part, "to");
|
|
124
|
+
if (!out.includes(c)) out.push(c);
|
|
125
|
+
}
|
|
126
|
+
if (out.length === 0) throw new Error("missing to (recipient)");
|
|
127
|
+
if (out.length > 20) throw new Error(`too many recipients (max 20, got ${out.length})`);
|
|
128
|
+
return out;
|
|
88
129
|
}
|
|
89
130
|
|
|
90
131
|
function cleanName(name, what) {
|
|
@@ -105,7 +146,7 @@ function newId(prefix) {
|
|
|
105
146
|
String(t.getUTCHours()).padStart(2, "0") +
|
|
106
147
|
String(t.getUTCMinutes()).padStart(2, "0") +
|
|
107
148
|
String(t.getUTCSeconds()).padStart(2, "0");
|
|
108
|
-
return `${prefix}-${stamp}-${crypto.randomBytes(
|
|
149
|
+
return `${prefix}-${stamp}-${crypto.randomBytes(4).toString("hex")}`;
|
|
109
150
|
}
|
|
110
151
|
|
|
111
152
|
function listDMs(d, recipient) {
|
|
@@ -150,13 +191,15 @@ const TOOLS = [
|
|
|
150
191
|
{
|
|
151
192
|
name: "dm_send",
|
|
152
193
|
description:
|
|
153
|
-
"Send a direct message to another AI agent via agent-board. Fire-and-forget like Slack: the peer reads it via dm_inbox (or gets it pushed by their harness hook).
|
|
194
|
+
"Send a direct message to another AI agent via agent-board. Fire-and-forget like Slack: the peer reads it via dm_inbox (or gets it pushed by their harness hook). `to` accepts a comma list for broadcast (one copy each, shared batch id) to fan work out to N agents — the DM is the task. Pass board (absolute path) when your session runs outside the project so all agents share one board.",
|
|
154
195
|
inputSchema: {
|
|
155
196
|
type: "object",
|
|
156
197
|
properties: {
|
|
157
198
|
from: { type: "string", description: "Your stable agent name, e.g. alice. Keep it constant for the session." },
|
|
158
|
-
to: { type: "string", description: "Recipient agent name, e.g. bob." },
|
|
199
|
+
to: { type: "string", description: "Recipient agent name, e.g. bob — or comma list for broadcast: alice,bob,carol." },
|
|
159
200
|
body: { type: "string", description: "Message text, 1..8000 chars." },
|
|
201
|
+
subject: { type: "string", description: "Optional mission line, e.g. 'brief: borderless cards'. Shown above the body." },
|
|
202
|
+
replyTo: { type: "string", description: "Optional message id you are answering (threads the reply)." },
|
|
160
203
|
board: { type: "string", description: "Optional absolute board path, e.g. C:/proj/.agentboard. Overrides AGENTBOARD_DIR and auto-detection." },
|
|
161
204
|
},
|
|
162
205
|
required: ["from", "to", "body"],
|
|
@@ -215,31 +258,69 @@ function isDriveRootMissing(root, explicit) {
|
|
|
215
258
|
return path.dirname(root) === path.parse(root).root;
|
|
216
259
|
}
|
|
217
260
|
|
|
261
|
+
// Read-side tools must never plant a board: without board.json, report the
|
|
262
|
+
// resolved path so the caller spots the split-board instead of an empty room.
|
|
263
|
+
function requireBoard(root) {
|
|
264
|
+
let meta = null;
|
|
265
|
+
try {
|
|
266
|
+
meta = readJson(path.join(root, "board.json"));
|
|
267
|
+
} catch {}
|
|
268
|
+
if (!meta || meta.version !== 2) {
|
|
269
|
+
throw new Error(
|
|
270
|
+
`no board at ${root} (cwd "${process.cwd()}"). Run from your project, pass board (absolute path to .agentboard), or set AGENTBOARD_DIR.`
|
|
271
|
+
);
|
|
272
|
+
}
|
|
273
|
+
return dirs(root);
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
function formatInbox(m) {
|
|
277
|
+
const bits = [`from ${m.from}`, `@ ${m.at || "unknown time"}`];
|
|
278
|
+
if (m.rev) bits.push(`rev ${m.rev}`);
|
|
279
|
+
if (m.replyTo) bits.push(`re: ${m.replyTo}`);
|
|
280
|
+
if (m.batch) bits.push(`batch ${m.batch}`);
|
|
281
|
+
return `[${m.id}] ${bits.join(" ")}${m.subject ? `\nsubj: ${m.subject}` : ""}\n${m.body}`;
|
|
282
|
+
}
|
|
283
|
+
|
|
218
284
|
function callTool(name, args) {
|
|
219
285
|
const a = args && typeof args === "object" ? args : {};
|
|
220
286
|
const boardArg = a.board === undefined || a.board === null || String(a.board).trim() === "" ? undefined : String(a.board);
|
|
221
287
|
const root = boardRoot(boardArg);
|
|
222
288
|
if (isDriveRootMissing(root, boardArg || process.env.AGENTBOARD_DIR)) {
|
|
223
289
|
throw new Error(
|
|
224
|
-
`refusing to create a board at drive root ${root} —
|
|
290
|
+
`refusing to create a board at drive root ${root} (cwd "${process.cwd()}") — no project board found above cwd. Pass board (absolute path to .agentboard) or set AGENTBOARD_DIR.`
|
|
225
291
|
);
|
|
226
292
|
}
|
|
227
|
-
const d = ensureBoard(root);
|
|
228
293
|
switch (name) {
|
|
229
294
|
case "dm_send": {
|
|
295
|
+
const d = ensureBoard(root);
|
|
230
296
|
const from = cleanName(a.from, "from");
|
|
231
|
-
const
|
|
297
|
+
const recipients = parseRecipients(a.to);
|
|
232
298
|
const body = String(a.body ?? "").trim();
|
|
233
299
|
if (!body) throw new Error("empty body");
|
|
234
300
|
if (body.length > MAX_BODY_CHARS) throw new Error(`body too large (max ${MAX_BODY_CHARS} chars)`);
|
|
301
|
+
const subject = a.subject === undefined || a.subject === null || String(a.subject).trim() === "" ? undefined : String(a.subject).trim().slice(0, 120);
|
|
302
|
+
const replyTo = a.replyTo === undefined || a.replyTo === null || String(a.replyTo).trim() === "" ? undefined : String(a.replyTo).trim().slice(0, 80);
|
|
235
303
|
touchAgent(d, from);
|
|
236
|
-
const
|
|
237
|
-
const
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
304
|
+
const rev = gitRev(root);
|
|
305
|
+
const at = new Date().toISOString();
|
|
306
|
+
const batch = recipients.length > 1 ? newId("batch") : undefined;
|
|
307
|
+
const sent = [];
|
|
308
|
+
for (const to of recipients) {
|
|
309
|
+
const id = newId("msg");
|
|
310
|
+
const msg = { id, from, to, body, at };
|
|
311
|
+
if (subject) msg.subject = subject;
|
|
312
|
+
if (replyTo) msg.replyTo = replyTo;
|
|
313
|
+
if (batch) msg.batch = batch;
|
|
314
|
+
if (rev) msg.rev = rev;
|
|
315
|
+
fs.mkdirSync(path.join(d.dm, to), { recursive: true });
|
|
316
|
+
writeJson(path.join(d.dm, to, `${id}.json`), msg);
|
|
317
|
+
sent.push(`${id} -> ${to}`);
|
|
318
|
+
}
|
|
319
|
+
if (sent.length === 1) return toolResult(`sent ${sent[0]} [board ${d.root}]`);
|
|
320
|
+
return toolResult(`sent ${sent.length} messages [board ${d.root}]: ${sent.join(", ")}`);
|
|
241
321
|
}
|
|
242
322
|
case "dm_inbox": {
|
|
323
|
+
const d = requireBoard(root);
|
|
243
324
|
const agent = cleanName(a.agent, "agent");
|
|
244
325
|
let items = listDMs(d, agent);
|
|
245
326
|
if (a.after !== undefined && a.after !== null && String(a.after) !== "") {
|
|
@@ -249,12 +330,13 @@ function callTool(name, args) {
|
|
|
249
330
|
const limit = a.limit === undefined || a.limit === null ? 20 : Number(a.limit);
|
|
250
331
|
if (!(limit >= 0)) throw new Error("limit must be a non-negative number");
|
|
251
332
|
items = items.slice(-limit);
|
|
252
|
-
if (items.length === 0) return toolResult(`no messages for ${agent}`);
|
|
253
|
-
return toolResult(items.map(
|
|
333
|
+
if (items.length === 0) return toolResult(`no messages for ${agent} [board ${d.root}]`);
|
|
334
|
+
return toolResult(items.map(formatInbox).join("\n\n"));
|
|
254
335
|
}
|
|
255
336
|
case "dm_agents": {
|
|
337
|
+
const d = requireBoard(root);
|
|
256
338
|
const dir = d.agents;
|
|
257
|
-
if (!fs.existsSync(dir)) return toolResult(
|
|
339
|
+
if (!fs.existsSync(dir)) return toolResult(`no agents registered [board ${d.root}]`);
|
|
258
340
|
const names = fs
|
|
259
341
|
.readdirSync(dir)
|
|
260
342
|
.filter((f) => f.endsWith(".json"))
|
|
@@ -267,14 +349,15 @@ function callTool(name, args) {
|
|
|
267
349
|
})
|
|
268
350
|
.filter(Boolean)
|
|
269
351
|
.sort();
|
|
270
|
-
if (names.length === 0) return toolResult(
|
|
271
|
-
return toolResult(names.join("\n"));
|
|
352
|
+
if (names.length === 0) return toolResult(`no agents registered (dm_register -- your name) [board ${d.root}]`);
|
|
353
|
+
return toolResult(names.join("\n") + `\n[board ${d.root}]`);
|
|
272
354
|
}
|
|
273
355
|
case "dm_register": {
|
|
356
|
+
const d = ensureBoard(root);
|
|
274
357
|
const agent = cleanName(a.agent, "agent");
|
|
275
358
|
const session = a.session === undefined || a.session === null ? undefined : String(a.session);
|
|
276
359
|
touchAgent(d, agent, session || undefined);
|
|
277
|
-
return toolResult(`registered ${agent}${session ? ` (session ${session})` : ""}`);
|
|
360
|
+
return toolResult(`registered ${agent}${session ? ` (session ${session})` : ""} [board ${d.root}]`);
|
|
278
361
|
}
|
|
279
362
|
default:
|
|
280
363
|
throw new Error(`unknown tool "${name}"`);
|