@eamonpluto/agentboard 2.2.0 → 2.3.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/CHANGELOG.md +11 -0
- package/README.md +14 -4
- package/bin/agentboard-hook.js +38 -1
- package/bin/agentboard-mcp.js +46 -9
- package/bin/agentboard.js +104 -16
- package/opencode/plugins/dm-watch.js +14 -1
- package/opencode/tools/dm-send.js +33 -5
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,16 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 2.3.0 (unpublished)
|
|
4
|
+
|
|
5
|
+
- Board resolution walks up to the project board (CLI, hook helper, MCP
|
|
6
|
+
server, opencode tool + plugin), so agents running from subdirectories or
|
|
7
|
+
detached worktrees stop planting stray boards.
|
|
8
|
+
- Every send echoes its board (`[board <path>]`); optional `board` param on
|
|
9
|
+
`dm-send` and all MCP tools for an explicit anchor.
|
|
10
|
+
- Writers refuse to auto-create a board at a drive root and fail loudly
|
|
11
|
+
(set `AGENTBOARD_DIR` / pass `board` instead).
|
|
12
|
+
- Troubleshooting section: split-board recovery, `doctor` flow.
|
|
13
|
+
|
|
3
14
|
## 2.2.0
|
|
4
15
|
|
|
5
16
|
- Hook delivery is now batched (max 5 per poll): the cursor stops at the last
|
package/README.md
CHANGED
|
@@ -105,11 +105,21 @@ against the Claude, Codex, and grok-build docs (Antigravity uses
|
|
|
105
105
|
Or install globally (enables portable `init --portable` wiring):
|
|
106
106
|
|
|
107
107
|
```powershell
|
|
108
|
-
|
|
109
|
-
npm i -g .
|
|
108
|
+
npm i -g @eamonpluto/agentboard
|
|
110
109
|
agentboard --help
|
|
111
110
|
```
|
|
112
111
|
|
|
112
|
+
## Troubleshooting
|
|
113
|
+
|
|
114
|
+
- **Two agents see different boards** (every send echoes `[board <path>]`):
|
|
115
|
+
export `AGENTBOARD_DIR=<board>` so all sessions share one — for in-process
|
|
116
|
+
tools (opencode `dm-send`) set it where the harness process launches, or
|
|
117
|
+
pass `board` explicitly per call (`dm-send({..., board: "C:/proj/.agentboard"})`).
|
|
118
|
+
Writers refuse to auto-create a board at a drive root and fail loudly instead.
|
|
119
|
+
- **`doctor` reports FAIL**: re-run `agentboard init --harness <name>` (merges,
|
|
120
|
+
never overwrites your own hooks), then follow the printed follow-ups
|
|
121
|
+
(trust approvals, `AGENTBOARD_AGENT`, restarts).
|
|
122
|
+
|
|
113
123
|
## Safety notes / trust boundary
|
|
114
124
|
|
|
115
125
|
- **Never post secrets on the board.** Post references instead.
|
|
@@ -127,9 +137,9 @@ agentboard --help
|
|
|
127
137
|
## Publishing (maintainer)
|
|
128
138
|
|
|
129
139
|
```powershell
|
|
130
|
-
npm test #
|
|
140
|
+
npm test # 88 checks, all must pass
|
|
131
141
|
npm publish # ships bin/ + opencode/ + docs (see "files" in package.json)
|
|
132
142
|
```
|
|
133
143
|
|
|
134
144
|
After publishing, projects can skip the checkout entirely:
|
|
135
|
-
`npm i -g agentboard` then `agentboard init --harness <name> --portable`.
|
|
145
|
+
`npm i -g @eamonpluto/agentboard` then `agentboard init --harness <name> --portable`.
|
package/bin/agentboard-hook.js
CHANGED
|
@@ -46,7 +46,20 @@ function boardDir(args) {
|
|
|
46
46
|
const i = args.indexOf("--board");
|
|
47
47
|
if (i !== -1 && args[i + 1] && !String(args[i + 1]).startsWith("--")) return path.resolve(args[i + 1]);
|
|
48
48
|
if (process.env.AGENTBOARD_DIR) return path.resolve(process.env.AGENTBOARD_DIR);
|
|
49
|
-
return path.join(process.cwd(), ".agentboard");
|
|
49
|
+
return findBoardUpward(process.cwd()) || path.join(process.cwd(), ".agentboard");
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
// Nearest ancestor (incl. start) containing a .agentboard dir, or null.
|
|
53
|
+
function findBoardUpward(start) {
|
|
54
|
+
let dir = path.resolve(start);
|
|
55
|
+
for (;;) {
|
|
56
|
+
try {
|
|
57
|
+
if (fs.statSync(path.join(dir, ".agentboard")).isDirectory()) return path.join(dir, ".agentboard");
|
|
58
|
+
} catch {}
|
|
59
|
+
const parent = path.dirname(dir);
|
|
60
|
+
if (parent === dir) return null;
|
|
61
|
+
dir = parent;
|
|
62
|
+
}
|
|
50
63
|
}
|
|
51
64
|
|
|
52
65
|
function getFlag(args, flag) {
|
|
@@ -54,6 +67,24 @@ function getFlag(args, flag) {
|
|
|
54
67
|
return i !== -1 && args[i + 1] !== undefined && !String(args[i + 1]).startsWith("--") ? args[i + 1] : undefined;
|
|
55
68
|
}
|
|
56
69
|
|
|
70
|
+
// Never silently plant a board at a drive root: fail loudly instead so the
|
|
71
|
+
// misconfiguration surfaces instead of mail landing on a stray board.
|
|
72
|
+
function refuseDriveRootBoard(root, args) {
|
|
73
|
+
const explicit = args.includes("--board") || !!process.env.AGENTBOARD_DIR;
|
|
74
|
+
if (explicit) return;
|
|
75
|
+
let exists = false;
|
|
76
|
+
try {
|
|
77
|
+
exists = fs.statSync(root).isDirectory();
|
|
78
|
+
} catch {}
|
|
79
|
+
if (exists) return;
|
|
80
|
+
if (path.dirname(root) === path.parse(root).root) {
|
|
81
|
+
fail(
|
|
82
|
+
`refusing to create a board at drive root ${root} — no project board found above cwd. ` +
|
|
83
|
+
`Pass --board <path> or set AGENTBOARD_DIR.`
|
|
84
|
+
);
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
|
|
57
88
|
function cleanName(name, what) {
|
|
58
89
|
if (!name) fail(`missing --from <agent-name> (${what}); or set AGENTBOARD_AGENT=<name>`);
|
|
59
90
|
const c = String(name).trim().replace(/[^A-Za-z0-9_.-]/g, "-").slice(0, 40);
|
|
@@ -162,6 +193,7 @@ function writeCursor(root, agent, lastId) {
|
|
|
162
193
|
|
|
163
194
|
async function cmdSessionStart(args) {
|
|
164
195
|
const root = boardDir(args);
|
|
196
|
+
refuseDriveRootBoard(root, args);
|
|
165
197
|
const agent = resolveAgent(args, "agent");
|
|
166
198
|
const input = await readStdinSoon(300);
|
|
167
199
|
const sessionId =
|
|
@@ -195,6 +227,11 @@ async function cmdSessionStart(args) {
|
|
|
195
227
|
|
|
196
228
|
async function cmdPoll(args) {
|
|
197
229
|
const root = boardDir(args);
|
|
230
|
+
try {
|
|
231
|
+
if (!fs.statSync(root).isDirectory()) return; // no board, no mail — silent allow
|
|
232
|
+
} catch {
|
|
233
|
+
return;
|
|
234
|
+
}
|
|
198
235
|
const agent = resolveAgent(args, "agent");
|
|
199
236
|
const style = getFlag(args, "--style") || "claude";
|
|
200
237
|
const valid = new Set(["claude", "codex", "grok", "antigravity-stop", "antigravity-pre"]);
|
package/bin/agentboard-mcp.js
CHANGED
|
@@ -36,9 +36,23 @@ const KNOWN_PROTOCOL_VERSIONS = new Set(["2024-11-05", "2025-03-26", "2025-06-18
|
|
|
36
36
|
// board (mirrors bin/agentboard.js layout v2; no import to stay dep-free)
|
|
37
37
|
// ---------------------------------------------------------------------------
|
|
38
38
|
|
|
39
|
-
function boardRoot() {
|
|
39
|
+
function boardRoot(override) {
|
|
40
|
+
if (override) return path.resolve(String(override));
|
|
40
41
|
if (process.env.AGENTBOARD_DIR) return path.resolve(process.env.AGENTBOARD_DIR);
|
|
41
|
-
return path.join(process.cwd(), ".agentboard");
|
|
42
|
+
return findBoardUpward(process.cwd()) || path.join(process.cwd(), ".agentboard");
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
// Nearest ancestor (incl. start) containing a .agentboard dir, or null.
|
|
46
|
+
function findBoardUpward(start) {
|
|
47
|
+
let dir = path.resolve(start);
|
|
48
|
+
for (;;) {
|
|
49
|
+
try {
|
|
50
|
+
if (fs.statSync(path.join(dir, ".agentboard")).isDirectory()) return path.join(dir, ".agentboard");
|
|
51
|
+
} catch {}
|
|
52
|
+
const parent = path.dirname(dir);
|
|
53
|
+
if (parent === dir) return null;
|
|
54
|
+
dir = parent;
|
|
55
|
+
}
|
|
42
56
|
}
|
|
43
57
|
|
|
44
58
|
function dirs(root) {
|
|
@@ -136,13 +150,14 @@ const TOOLS = [
|
|
|
136
150
|
{
|
|
137
151
|
name: "dm_send",
|
|
138
152
|
description:
|
|
139
|
-
"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). Use whenever you want to coordinate, share a finding, or ask a peer.",
|
|
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). Use whenever you want to coordinate, share a finding, or ask a peer. Pass board (absolute path) when your session runs outside the project so all agents share one board.",
|
|
140
154
|
inputSchema: {
|
|
141
155
|
type: "object",
|
|
142
156
|
properties: {
|
|
143
157
|
from: { type: "string", description: "Your stable agent name, e.g. alice. Keep it constant for the session." },
|
|
144
158
|
to: { type: "string", description: "Recipient agent name, e.g. bob." },
|
|
145
159
|
body: { type: "string", description: "Message text, 1..8000 chars." },
|
|
160
|
+
board: { type: "string", description: "Optional absolute board path, e.g. C:/proj/.agentboard. Overrides AGENTBOARD_DIR and auto-detection." },
|
|
146
161
|
},
|
|
147
162
|
required: ["from", "to", "body"],
|
|
148
163
|
},
|
|
@@ -150,31 +165,38 @@ const TOOLS = [
|
|
|
150
165
|
{
|
|
151
166
|
name: "dm_inbox",
|
|
152
167
|
description:
|
|
153
|
-
"Read your direct messages on agent-board. No mark-read side effects; page with after. Poll this often when your harness has no push hook.",
|
|
168
|
+
"Read your direct messages on agent-board. No mark-read side effects; page with after. Poll this often when your harness has no push hook. Pass board (absolute path) when your session runs outside the project so all agents share one board.",
|
|
154
169
|
inputSchema: {
|
|
155
170
|
type: "object",
|
|
156
171
|
properties: {
|
|
157
172
|
agent: { type: "string", description: "Your agent name." },
|
|
158
173
|
limit: { type: "number", description: "Max messages, newest last. Default 20." },
|
|
159
174
|
after: { type: "string", description: "Only messages after this message id." },
|
|
175
|
+
board: { type: "string", description: "Optional absolute board path, e.g. C:/proj/.agentboard. Overrides AGENTBOARD_DIR and auto-detection." },
|
|
160
176
|
},
|
|
161
177
|
required: ["agent"],
|
|
162
178
|
},
|
|
163
179
|
},
|
|
164
180
|
{
|
|
165
181
|
name: "dm_agents",
|
|
166
|
-
description: "List agents known to this agent-board (registered names for addressing DMs).",
|
|
167
|
-
inputSchema: {
|
|
182
|
+
description: "List agents known to this agent-board (registered names for addressing DMs). Pass board when your session runs outside the project.",
|
|
183
|
+
inputSchema: {
|
|
184
|
+
type: "object",
|
|
185
|
+
properties: {
|
|
186
|
+
board: { type: "string", description: "Optional absolute board path. Overrides AGENTBOARD_DIR and auto-detection." },
|
|
187
|
+
},
|
|
188
|
+
},
|
|
168
189
|
},
|
|
169
190
|
{
|
|
170
191
|
name: "dm_register",
|
|
171
192
|
description:
|
|
172
|
-
"Register your agent name on this agent-board (optionally binding a harness session id for push routing). Do this once per session.",
|
|
193
|
+
"Register your agent name on this agent-board (optionally binding a harness session id for push routing). Do this once per session. Pass board when your session runs outside the project.",
|
|
173
194
|
inputSchema: {
|
|
174
195
|
type: "object",
|
|
175
196
|
properties: {
|
|
176
197
|
agent: { type: "string", description: "Your stable agent name." },
|
|
177
198
|
session: { type: "string", description: "Optional harness session/conversation id for push routing." },
|
|
199
|
+
board: { type: "string", description: "Optional absolute board path. Overrides AGENTBOARD_DIR and auto-detection." },
|
|
178
200
|
},
|
|
179
201
|
required: ["agent"],
|
|
180
202
|
},
|
|
@@ -185,9 +207,24 @@ function toolResult(text) {
|
|
|
185
207
|
return { content: [{ type: "text", text }] };
|
|
186
208
|
}
|
|
187
209
|
|
|
210
|
+
function isDriveRootMissing(root, explicit) {
|
|
211
|
+
if (explicit) return false;
|
|
212
|
+
try {
|
|
213
|
+
if (fs.statSync(root).isDirectory()) return false;
|
|
214
|
+
} catch {}
|
|
215
|
+
return path.dirname(root) === path.parse(root).root;
|
|
216
|
+
}
|
|
217
|
+
|
|
188
218
|
function callTool(name, args) {
|
|
189
|
-
const d = ensureBoard(boardRoot());
|
|
190
219
|
const a = args && typeof args === "object" ? args : {};
|
|
220
|
+
const boardArg = a.board === undefined || a.board === null || String(a.board).trim() === "" ? undefined : String(a.board);
|
|
221
|
+
const root = boardRoot(boardArg);
|
|
222
|
+
if (isDriveRootMissing(root, boardArg || process.env.AGENTBOARD_DIR)) {
|
|
223
|
+
throw new Error(
|
|
224
|
+
`refusing to create a board at drive root ${root} — pass board (absolute path) or set AGENTBOARD_DIR`
|
|
225
|
+
);
|
|
226
|
+
}
|
|
227
|
+
const d = ensureBoard(root);
|
|
191
228
|
switch (name) {
|
|
192
229
|
case "dm_send": {
|
|
193
230
|
const from = cleanName(a.from, "from");
|
|
@@ -200,7 +237,7 @@ function callTool(name, args) {
|
|
|
200
237
|
const msg = { id, from, to, body, at: new Date().toISOString() };
|
|
201
238
|
fs.mkdirSync(path.join(d.dm, to), { recursive: true });
|
|
202
239
|
writeJson(path.join(d.dm, to, `${id}.json`), msg);
|
|
203
|
-
return toolResult(`sent ${id} -> ${to}`);
|
|
240
|
+
return toolResult(`sent ${id} -> ${to} [board ${d.root}]`);
|
|
204
241
|
}
|
|
205
242
|
case "dm_inbox": {
|
|
206
243
|
const agent = cleanName(a.agent, "agent");
|
package/bin/agentboard.js
CHANGED
|
@@ -47,7 +47,46 @@ function boardDir(args) {
|
|
|
47
47
|
return path.join(os.homedir(), ".agentboard", "boards", "default");
|
|
48
48
|
}
|
|
49
49
|
if (process.env.AGENTBOARD_DIR) return path.resolve(process.env.AGENTBOARD_DIR);
|
|
50
|
-
|
|
50
|
+
// init always plants a board where you stand; every other command walks up
|
|
51
|
+
// so agents running from a subdirectory land on the project board instead
|
|
52
|
+
// of silently creating a stray one.
|
|
53
|
+
if (process.argv[2] === "init") return path.join(process.cwd(), ".agentboard");
|
|
54
|
+
return findBoardUpward(process.cwd()) || path.join(process.cwd(), ".agentboard");
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
// Nearest ancestor (incl. start) containing a .agentboard dir, or null.
|
|
58
|
+
function findBoardUpward(start) {
|
|
59
|
+
let dir = path.resolve(start);
|
|
60
|
+
for (;;) {
|
|
61
|
+
try {
|
|
62
|
+
if (fs.statSync(path.join(dir, ".agentboard")).isDirectory()) return path.join(dir, ".agentboard");
|
|
63
|
+
} catch {}
|
|
64
|
+
const parent = path.dirname(dir);
|
|
65
|
+
if (parent === dir) return null;
|
|
66
|
+
dir = parent;
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
// Never silently plant a board at a drive root (e.g. C:\.agentboard): that
|
|
71
|
+
// means cwd resolution failed (detached harness worktree). Fail loudly so
|
|
72
|
+
// the agent sets --board/AGENTBOARD_DIR instead of talking to a stray board.
|
|
73
|
+
function isExplicitBoard(args) {
|
|
74
|
+
return args.includes("--board") || args.includes("--global") || !!process.env.AGENTBOARD_DIR;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
function refuseDriveRootBoard(root, args) {
|
|
78
|
+
if (isExplicitBoard(args)) return;
|
|
79
|
+
let exists = false;
|
|
80
|
+
try {
|
|
81
|
+
exists = fs.statSync(root).isDirectory();
|
|
82
|
+
} catch {}
|
|
83
|
+
if (exists) return;
|
|
84
|
+
if (path.dirname(root) === path.parse(root).root) {
|
|
85
|
+
fail(
|
|
86
|
+
`refusing to create a board at drive root ${root} — no project board found above cwd. ` +
|
|
87
|
+
`Run from your project, pass --board <path>, or set AGENTBOARD_DIR.`
|
|
88
|
+
);
|
|
89
|
+
}
|
|
51
90
|
}
|
|
52
91
|
|
|
53
92
|
function dirs(root) {
|
|
@@ -63,8 +102,7 @@ function ensureBoard(root) {
|
|
|
63
102
|
const d = dirs(root);
|
|
64
103
|
for (const p of [d.root, d.agents, d.dm, d.delivered]) {
|
|
65
104
|
fs.mkdirSync(p, { recursive: true });
|
|
66
|
-
}
|
|
67
|
-
const metaPath = path.join(d.root, "board.json");
|
|
105
|
+
} const metaPath = path.join(d.root, "board.json");
|
|
68
106
|
if (!fs.existsSync(metaPath)) {
|
|
69
107
|
fs.writeFileSync(
|
|
70
108
|
metaPath,
|
|
@@ -202,9 +240,26 @@ import fs from "node:fs";
|
|
|
202
240
|
import path from "node:path";
|
|
203
241
|
import crypto from "node:crypto";
|
|
204
242
|
|
|
205
|
-
function boardRoot(worktree) {
|
|
243
|
+
function boardRoot(worktree, override) {
|
|
244
|
+
if (override) return path.resolve(String(override));
|
|
206
245
|
if (process.env.AGENTBOARD_DIR) return path.resolve(process.env.AGENTBOARD_DIR);
|
|
207
|
-
|
|
246
|
+
const base = worktree || process.cwd();
|
|
247
|
+
return findBoardUpward(base) || path.join(base, ".agentboard");
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
// Nearest ancestor (incl. start) containing a .agentboard dir, or null.
|
|
251
|
+
// Harnesses sometimes run agents with a cwd below (or beside) the project;
|
|
252
|
+
// walk-up keeps every session on the same board.
|
|
253
|
+
function findBoardUpward(start) {
|
|
254
|
+
let dir = path.resolve(start);
|
|
255
|
+
for (;;) {
|
|
256
|
+
try {
|
|
257
|
+
if (fs.statSync(path.join(dir, ".agentboard")).isDirectory()) return path.join(dir, ".agentboard");
|
|
258
|
+
} catch {}
|
|
259
|
+
const parent = path.dirname(dir);
|
|
260
|
+
if (parent === dir) return null;
|
|
261
|
+
dir = parent;
|
|
262
|
+
}
|
|
208
263
|
}
|
|
209
264
|
|
|
210
265
|
function clean(name, what) {
|
|
@@ -223,11 +278,12 @@ function writeJsonAtomic(p, obj) {
|
|
|
223
278
|
|
|
224
279
|
export default tool({
|
|
225
280
|
description:
|
|
226
|
-
"Send a direct message to another AI agent via agent-board. Use whenever you want to coordinate, share a finding, or ask a peer. Fire-and-forget like Slack — the peer's session gets it injected into context. Args: from (your stable agent name), to (peer's agent name), body (message text).",
|
|
281
|
+
"Send a direct message to another AI agent via agent-board. Use whenever you want to coordinate, share a finding, or ask a peer. Fire-and-forget like Slack — the peer's session gets it injected into context. Args: from (your stable agent name), to (peer's agent name), body (message text), board (optional absolute board path when your session runs outside the project).",
|
|
227
282
|
args: {
|
|
228
283
|
from: tool.schema.string().describe("Your stable agent name, e.g. alice. Keep it constant for the session."),
|
|
229
284
|
to: tool.schema.string().describe("Recipient agent name, e.g. bob. They receive it on inbox/listen even before registering."),
|
|
230
285
|
body: tool.schema.string().describe("Message text, 1..8000 chars."),
|
|
286
|
+
board: tool.schema.string().optional().describe("Optional absolute board path, e.g. C:/proj/.agentboard. Overrides AGENTBOARD_DIR and auto-detection."),
|
|
231
287
|
},
|
|
232
288
|
async execute(args, context) {
|
|
233
289
|
const from = clean(args.from, "from");
|
|
@@ -235,7 +291,17 @@ export default tool({
|
|
|
235
291
|
const body = String(args.body || "").trim();
|
|
236
292
|
if (!body) return "error: empty body";
|
|
237
293
|
if (body.length > 8000) return "error: body too large (max 8000 chars)";
|
|
238
|
-
const
|
|
294
|
+
const boardArg = args.board === undefined || args.board === null || String(args.board).trim() === "" ? undefined : String(args.board);
|
|
295
|
+
const root = boardRoot(context.worktree || context.directory || process.cwd(), boardArg);
|
|
296
|
+
if (!boardArg && !process.env.AGENTBOARD_DIR) {
|
|
297
|
+
let exists = false;
|
|
298
|
+
try {
|
|
299
|
+
exists = fs.statSync(root).isDirectory();
|
|
300
|
+
} catch {}
|
|
301
|
+
if (!exists && path.dirname(root) === path.parse(root).root) {
|
|
302
|
+
return \`error: refusing to create a board at drive root \${root} — pass board (absolute path) or set AGENTBOARD_DIR\`;
|
|
303
|
+
}
|
|
304
|
+
}
|
|
239
305
|
const now = new Date().toISOString();
|
|
240
306
|
const t = new Date();
|
|
241
307
|
const stamp =
|
|
@@ -261,7 +327,7 @@ export default tool({
|
|
|
261
327
|
sessionId: (context && context.sessionID) || (prev && prev.sessionId) || undefined,
|
|
262
328
|
lastDir: context.worktree || context.directory || undefined,
|
|
263
329
|
});
|
|
264
|
-
return "sent " + id + " -> " + to;
|
|
330
|
+
return "sent " + id + " -> " + to + " [board " + root + "]";
|
|
265
331
|
},
|
|
266
332
|
});
|
|
267
333
|
`;
|
|
@@ -291,7 +357,20 @@ const POLL_MS = 1000;
|
|
|
291
357
|
|
|
292
358
|
function boardRoot(directory) {
|
|
293
359
|
if (process.env.AGENTBOARD_DIR) return path.resolve(process.env.AGENTBOARD_DIR);
|
|
294
|
-
return path.join(directory, ".agentboard");
|
|
360
|
+
return findBoardUpward(directory) || path.join(directory, ".agentboard");
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
// Nearest ancestor (incl. start) containing a .agentboard dir, or null.
|
|
364
|
+
function findBoardUpward(start) {
|
|
365
|
+
let dir = path.resolve(start);
|
|
366
|
+
for (;;) {
|
|
367
|
+
try {
|
|
368
|
+
if (fs.statSync(path.join(dir, ".agentboard")).isDirectory()) return path.join(dir, ".agentboard");
|
|
369
|
+
} catch {}
|
|
370
|
+
const parent = path.dirname(dir);
|
|
371
|
+
if (parent === dir) return null;
|
|
372
|
+
dir = parent;
|
|
373
|
+
}
|
|
295
374
|
}
|
|
296
375
|
|
|
297
376
|
function readJsonSafe(p) {
|
|
@@ -734,7 +813,8 @@ function mergeMcpServers(file, entry) {
|
|
|
734
813
|
const HARNESS_SECTIONS = {
|
|
735
814
|
opencode: (cli) =>
|
|
736
815
|
`On opencode prefer the \`dm-send\` tool over \`${cli} send\` (same thing, plus it registers your session for push).\n` +
|
|
737
|
-
`Incoming DMs are inserted into your context automatically by the dm-watch plugin. Restart opencode after \`init\` so the tool + plugin load
|
|
816
|
+
`Incoming DMs are inserted into your context automatically by the dm-watch plugin. Restart opencode after \`init\` so the tool + plugin load.\n` +
|
|
817
|
+
`Every send echoes its board (\`[board <path>]\`): if two agents see different boards, export \`AGENTBOARD_DIR=<board>\` so all sessions share one.`,
|
|
738
818
|
claude: (cli) =>
|
|
739
819
|
`On Claude Code use the \`agentboard\` MCP tools (\`dm_send\` / \`dm_inbox\` / \`dm_agents\` / \`dm_register\`) — approve \`.mcp.json\` when prompted.\n` +
|
|
740
820
|
`A Stop hook (\`.claude/settings.json\`) injects waiting DMs at turn end. Set \`AGENTBOARD_AGENT=<you>\` once per terminal so hooks know who you are.`,
|
|
@@ -929,11 +1009,13 @@ function touchAgent(d, name, extra) {
|
|
|
929
1009
|
}
|
|
930
1010
|
|
|
931
1011
|
function cmdRegister(args) {
|
|
932
|
-
const
|
|
1012
|
+
const root = boardDir(args);
|
|
1013
|
+
refuseDriveRootBoard(root, args);
|
|
1014
|
+
const d = ensureBoard(root);
|
|
933
1015
|
const agent = resolveAgent(args, "agent");
|
|
934
1016
|
const session = getFlag(args, "--session");
|
|
935
1017
|
const doc = touchAgent(d, agent, { sessionId: session || undefined, lastDir: process.cwd() });
|
|
936
|
-
console.log(`registered ${agent}${doc.sessionId ? ` (session ${doc.sessionId})` : ""}`);
|
|
1018
|
+
console.log(`registered ${agent}${doc.sessionId ? ` (session ${doc.sessionId})` : ""} [board ${d.root}]`);
|
|
937
1019
|
}
|
|
938
1020
|
|
|
939
1021
|
function cmdAgents(args) {
|
|
@@ -956,7 +1038,9 @@ function cmdAgents(args) {
|
|
|
956
1038
|
}
|
|
957
1039
|
|
|
958
1040
|
function cmdSend(args) {
|
|
959
|
-
const
|
|
1041
|
+
const root = boardDir(args);
|
|
1042
|
+
refuseDriveRootBoard(root, args);
|
|
1043
|
+
const d = ensureBoard(root);
|
|
960
1044
|
const from = resolveAgent(args, "sender");
|
|
961
1045
|
const toRaw = getFlag(args, "--to");
|
|
962
1046
|
const to = sanitizeName(toRaw, "recipient");
|
|
@@ -970,7 +1054,7 @@ function cmdSend(args) {
|
|
|
970
1054
|
const dir = path.join(d.dm, to);
|
|
971
1055
|
fs.mkdirSync(dir, { recursive: true });
|
|
972
1056
|
writeJson(path.join(dir, `${id}.json`), msg);
|
|
973
|
-
console.log(`sent ${id} -> ${to}`);
|
|
1057
|
+
console.log(`sent ${id} -> ${to} [board ${d.root}]`);
|
|
974
1058
|
}
|
|
975
1059
|
|
|
976
1060
|
function readDMs(d, recipient) {
|
|
@@ -991,7 +1075,9 @@ function printMsg(m, showTo, json) {
|
|
|
991
1075
|
}
|
|
992
1076
|
|
|
993
1077
|
function cmdInbox(args) {
|
|
994
|
-
const
|
|
1078
|
+
const root = boardDir(args);
|
|
1079
|
+
refuseDriveRootBoard(root, args);
|
|
1080
|
+
const d = ensureBoard(root);
|
|
995
1081
|
const showAll = args.includes("--all");
|
|
996
1082
|
const limit = Number(getFlag(args, "--limit") || 20);
|
|
997
1083
|
const after = getFlag(args, "--after");
|
|
@@ -1039,7 +1125,9 @@ function cmdInbox(args) {
|
|
|
1039
1125
|
}
|
|
1040
1126
|
|
|
1041
1127
|
async function cmdListen(args) {
|
|
1042
|
-
const
|
|
1128
|
+
const root = boardDir(args);
|
|
1129
|
+
refuseDriveRootBoard(root, args);
|
|
1130
|
+
const d = ensureBoard(root);
|
|
1043
1131
|
const agent = resolveAgent(args, "listener");
|
|
1044
1132
|
const timeoutMs = Number(getFlag(args, "--timeout") || 0);
|
|
1045
1133
|
const json = args.includes("--json");
|
|
@@ -23,7 +23,20 @@ const POLL_MS = 1000;
|
|
|
23
23
|
|
|
24
24
|
function boardRoot(directory) {
|
|
25
25
|
if (process.env.AGENTBOARD_DIR) return path.resolve(process.env.AGENTBOARD_DIR);
|
|
26
|
-
return path.join(directory, ".agentboard");
|
|
26
|
+
return findBoardUpward(directory) || path.join(directory, ".agentboard");
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
// Nearest ancestor (incl. start) containing a .agentboard dir, or null.
|
|
30
|
+
function findBoardUpward(start) {
|
|
31
|
+
let dir = path.resolve(start);
|
|
32
|
+
for (;;) {
|
|
33
|
+
try {
|
|
34
|
+
if (fs.statSync(path.join(dir, ".agentboard")).isDirectory()) return path.join(dir, ".agentboard");
|
|
35
|
+
} catch {}
|
|
36
|
+
const parent = path.dirname(dir);
|
|
37
|
+
if (parent === dir) return null;
|
|
38
|
+
dir = parent;
|
|
39
|
+
}
|
|
27
40
|
}
|
|
28
41
|
|
|
29
42
|
function readJsonSafe(p) {
|
|
@@ -21,9 +21,26 @@ import fs from "node:fs";
|
|
|
21
21
|
import path from "node:path";
|
|
22
22
|
import crypto from "node:crypto";
|
|
23
23
|
|
|
24
|
-
function boardRoot(worktree) {
|
|
24
|
+
function boardRoot(worktree, override) {
|
|
25
|
+
if (override) return path.resolve(String(override));
|
|
25
26
|
if (process.env.AGENTBOARD_DIR) return path.resolve(process.env.AGENTBOARD_DIR);
|
|
26
|
-
|
|
27
|
+
const base = worktree || process.cwd();
|
|
28
|
+
return findBoardUpward(base) || path.join(base, ".agentboard");
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
// Nearest ancestor (incl. start) containing a .agentboard dir, or null.
|
|
32
|
+
// Harnesses sometimes run agents with a cwd below (or beside) the project;
|
|
33
|
+
// walk-up keeps every session on the same board.
|
|
34
|
+
function findBoardUpward(start) {
|
|
35
|
+
let dir = path.resolve(start);
|
|
36
|
+
for (;;) {
|
|
37
|
+
try {
|
|
38
|
+
if (fs.statSync(path.join(dir, ".agentboard")).isDirectory()) return path.join(dir, ".agentboard");
|
|
39
|
+
} catch {}
|
|
40
|
+
const parent = path.dirname(dir);
|
|
41
|
+
if (parent === dir) return null;
|
|
42
|
+
dir = parent;
|
|
43
|
+
}
|
|
27
44
|
}
|
|
28
45
|
|
|
29
46
|
function clean(name, what) {
|
|
@@ -42,11 +59,12 @@ function writeJsonAtomic(p, obj) {
|
|
|
42
59
|
|
|
43
60
|
export default tool({
|
|
44
61
|
description:
|
|
45
|
-
"Send a direct message to another AI agent via agent-board. Use whenever you want to coordinate, share a finding, or ask a peer. Fire-and-forget like Slack — the peer's session gets it injected into context. Args: from (your stable agent name), to (peer's agent name), body (message text).",
|
|
62
|
+
"Send a direct message to another AI agent via agent-board. Use whenever you want to coordinate, share a finding, or ask a peer. Fire-and-forget like Slack — the peer's session gets it injected into context. Args: from (your stable agent name), to (peer's agent name), body (message text), board (optional absolute board path when your session runs outside the project).",
|
|
46
63
|
args: {
|
|
47
64
|
from: tool.schema.string().describe("Your stable agent name, e.g. alice. Keep it constant for the session."),
|
|
48
65
|
to: tool.schema.string().describe("Recipient agent name, e.g. bob. They receive it on inbox/listen even before registering."),
|
|
49
66
|
body: tool.schema.string().describe("Message text, 1..8000 chars."),
|
|
67
|
+
board: tool.schema.string().optional().describe("Optional absolute board path, e.g. C:/proj/.agentboard. Overrides AGENTBOARD_DIR and auto-detection."),
|
|
50
68
|
},
|
|
51
69
|
async execute(args, context) {
|
|
52
70
|
const from = clean(args.from, "from");
|
|
@@ -54,7 +72,17 @@ export default tool({
|
|
|
54
72
|
const body = String(args.body || "").trim();
|
|
55
73
|
if (!body) return "error: empty body";
|
|
56
74
|
if (body.length > 8000) return "error: body too large (max 8000 chars)";
|
|
57
|
-
const
|
|
75
|
+
const boardArg = args.board === undefined || args.board === null || String(args.board).trim() === "" ? undefined : String(args.board);
|
|
76
|
+
const root = boardRoot(context.worktree || context.directory || process.cwd(), boardArg);
|
|
77
|
+
if (!boardArg && !process.env.AGENTBOARD_DIR) {
|
|
78
|
+
let exists = false;
|
|
79
|
+
try {
|
|
80
|
+
exists = fs.statSync(root).isDirectory();
|
|
81
|
+
} catch {}
|
|
82
|
+
if (!exists && path.dirname(root) === path.parse(root).root) {
|
|
83
|
+
return `error: refusing to create a board at drive root ${root} — pass board (absolute path) or set AGENTBOARD_DIR`;
|
|
84
|
+
}
|
|
85
|
+
}
|
|
58
86
|
const now = new Date().toISOString();
|
|
59
87
|
const t = new Date();
|
|
60
88
|
const stamp =
|
|
@@ -80,6 +108,6 @@ export default tool({
|
|
|
80
108
|
sessionId: (context && context.sessionID) || (prev && prev.sessionId) || undefined,
|
|
81
109
|
lastDir: context.worktree || context.directory || undefined,
|
|
82
110
|
});
|
|
83
|
-
return "sent " + id + " -> " + to;
|
|
111
|
+
return "sent " + id + " -> " + to + " [board " + root + "]";
|
|
84
112
|
},
|
|
85
113
|
});
|
package/package.json
CHANGED