loadout-ai 0.8.0 → 0.9.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.
Files changed (82) hide show
  1. package/CHANGELOG.md +76 -1
  2. package/README.md +149 -304
  3. package/SECURITY.md +21 -1
  4. package/catalog/discovered.json +27897 -25327
  5. package/catalog/packages.json +4 -4
  6. package/dist/src/cli.js +12 -4
  7. package/dist/src/commands/agents.js +3 -1
  8. package/dist/src/commands/catalog-candidate.js +150 -0
  9. package/dist/src/commands/catalog-workflows.js +147 -0
  10. package/dist/src/commands/catalog.js +9 -354
  11. package/dist/src/commands/coordinate.js +586 -0
  12. package/dist/src/commands/coordination-discussions.js +194 -0
  13. package/dist/src/commands/coordination-sessions.js +197 -0
  14. package/dist/src/commands/inventory.js +4 -2
  15. package/dist/src/core/{routing → agents}/model-config.js +7 -2
  16. package/dist/src/core/catalog/catalog.js +1 -0
  17. package/dist/src/core/catalog/registry.js +5 -19
  18. package/dist/src/core/coordination/adapters/claude-code.js +154 -0
  19. package/dist/src/core/coordination/adapters/codex.js +137 -0
  20. package/dist/src/core/coordination/adapters/types.js +64 -0
  21. package/dist/src/core/coordination/auth.js +59 -0
  22. package/dist/src/core/coordination/bridge-lease.js +79 -0
  23. package/dist/src/core/coordination/conflict-preview.js +167 -0
  24. package/dist/src/core/coordination/contract-diff.js +106 -0
  25. package/dist/src/core/coordination/coordinator.js +552 -0
  26. package/dist/src/core/coordination/crash-recovery.js +169 -0
  27. package/dist/src/core/coordination/daemon.js +667 -0
  28. package/dist/src/core/coordination/discussion.js +340 -0
  29. package/dist/src/core/coordination/events.js +161 -0
  30. package/dist/src/core/coordination/http-api.js +118 -0
  31. package/dist/src/core/coordination/interrupt-policy.js +89 -0
  32. package/dist/src/core/coordination/lock.js +110 -0
  33. package/dist/src/core/coordination/mcp-server.js +424 -0
  34. package/dist/src/core/coordination/redaction.js +85 -0
  35. package/dist/src/core/coordination/replay.js +188 -0
  36. package/dist/src/core/coordination/retention.js +128 -0
  37. package/dist/src/core/coordination/runtime.js +21 -0
  38. package/dist/src/core/coordination/session-manager.js +366 -0
  39. package/dist/src/core/coordination/watcher.js +128 -0
  40. package/dist/src/core/{routing → delegation}/first-party-skills.js +25 -13
  41. package/dist/src/core/{routing → delegation}/handoff.js +40 -9
  42. package/dist/src/core/discovery/candidate-intelligence-evidence.js +108 -0
  43. package/dist/src/core/discovery/candidate-intelligence-types.js +1 -0
  44. package/dist/src/core/discovery/candidate-intelligence-validation.js +108 -0
  45. package/dist/src/core/discovery/candidate-intelligence.js +3 -214
  46. package/dist/src/core/discovery/community.js +12 -4
  47. package/dist/src/core/discovery/github-discovery.js +6 -2
  48. package/dist/src/core/discovery/private-discovery.js +6 -2
  49. package/dist/src/core/install/reconcile.js +1 -1
  50. package/dist/src/core/install/uninstall.js +39 -3
  51. package/dist/src/core/reporting/cli-guide.js +9 -5
  52. package/dist/src/core/reporting/completion.js +43 -38
  53. package/dist/src/core/reporting/doctor.js +1 -1
  54. package/dist/src/core/runtime/bounded-json.js +65 -0
  55. package/dist/src/core/runtime/github.js +30 -16
  56. package/dist/src/core/runtime/mcp-recipes.js +1 -1
  57. package/dist/src/core/workspace/active-policy.js +1 -1
  58. package/docs/DEMO_SCRIPT.md +19 -23
  59. package/docs/DISCOVERED.md +251 -251
  60. package/docs/FEATURE_TEST_MATRIX.md +3 -3
  61. package/docs/LIVE_COLLABORATION.md +234 -0
  62. package/docs/REFERENCE.md +163 -0
  63. package/docs/RELEASE_REVIEW.md +1 -1
  64. package/docs/TESTING.md +17 -1
  65. package/docs/USER_TEST_GUIDE.md +138 -3
  66. package/docs/assets/loadout-discover-activate.webp +0 -0
  67. package/docs/assets/loadout-handoff-coordinate.webp +0 -0
  68. package/docs/assets/loadout-social-preview.png +0 -0
  69. package/docs/decisions/001-coordination-jsonl-locking.md +35 -0
  70. package/docs/decisions/002-local-daemon-authentication.md +32 -0
  71. package/docs/decisions/003-bounded-agent-discussions.md +91 -0
  72. package/docs/specs/BOUNDED_AGENT_DISCUSSIONS.md +169 -0
  73. package/docs/superpowers/plans/2026-09-03-coordination-hardening.md +231 -0
  74. package/docs/superpowers/plans/2026-09-03-release-readiness.md +232 -0
  75. package/docs/superpowers/plans/2026-09-04-bounded-agent-discussions.md +121 -0
  76. package/package.json +13 -4
  77. package/skills/loadout-handoff/SKILL.md +238 -0
  78. package/dist/src/core/routing/policy.js +0 -147
  79. package/dist/src/core/routing/route.js +0 -411
  80. package/docs/assets/loadout-workflow.png +0 -0
  81. package/skills/loadout-router/SKILL.md +0 -85
  82. /package/dist/src/core/{routing → agents}/credentials.js +0 -0
@@ -0,0 +1,89 @@
1
+ /**
2
+ * Interrupt policy for coordination events.
3
+ *
4
+ * Determines whether an incoming event should interrupt an active agent
5
+ * session immediately, queue for the next safe boundary, or be silently
6
+ * recorded for later consumption.
7
+ *
8
+ * Policies are configurable per project via .handoff/policy.json.
9
+ */
10
+ import { readFile } from "node:fs/promises";
11
+ import { join } from "node:path";
12
+ import { z } from "zod";
13
+ const DEFAULT_POLICY = {
14
+ defaultLevel: "boundary",
15
+ rules: [
16
+ // Ownership conflicts should interrupt immediately
17
+ { type: "ownership", from: "*", level: "immediate" },
18
+ // Contract changes interrupt at boundary
19
+ { type: "contract", from: "*", level: "boundary" },
20
+ // Decisions interrupt at boundary
21
+ { type: "decision", from: "*", level: "boundary" },
22
+ // Public design-room turns route sequentially at safe boundaries
23
+ { type: "discussion", from: "*", level: "boundary" },
24
+ // Progress updates are passive
25
+ { type: "update", from: "*", level: "passive" },
26
+ // Acks are always passive
27
+ { type: "ack", from: "*", level: "passive" },
28
+ // Tasks should interrupt at boundary
29
+ { type: "task", from: "*", level: "boundary" },
30
+ // Errors interrupt immediately
31
+ { type: "error", from: "*", level: "immediate" },
32
+ // Done events at boundary
33
+ { type: "done", from: "*", level: "boundary" },
34
+ ],
35
+ };
36
+ const policySchema = z
37
+ .object({
38
+ defaultLevel: z.enum(["immediate", "boundary", "passive"]).optional(),
39
+ rules: z
40
+ .array(z
41
+ .object({
42
+ type: z.string().trim().min(1).max(128),
43
+ from: z.string().trim().min(1).max(128),
44
+ level: z.enum(["immediate", "boundary", "passive"]),
45
+ })
46
+ .strict())
47
+ .max(100)
48
+ .optional(),
49
+ })
50
+ .strict();
51
+ export function evaluatePolicy(event, policy = DEFAULT_POLICY) {
52
+ for (const rule of policy.rules) {
53
+ const typeMatch = rule.type === "*" || rule.type === event.type;
54
+ const fromMatch = rule.from === "*" || rule.from === event.from;
55
+ if (typeMatch && fromMatch) {
56
+ return rule.level;
57
+ }
58
+ }
59
+ return policy.defaultLevel;
60
+ }
61
+ export function categorizeEvents(events, policy = DEFAULT_POLICY) {
62
+ const result = {
63
+ immediate: [],
64
+ boundary: [],
65
+ passive: [],
66
+ };
67
+ for (const event of events) {
68
+ const level = evaluatePolicy(event, policy);
69
+ result[level].push(event);
70
+ }
71
+ return result;
72
+ }
73
+ export async function loadPolicy(projectRoot) {
74
+ try {
75
+ const raw = await readFile(join(projectRoot, ".handoff", "policy.json"), "utf-8");
76
+ const parsed = policySchema.safeParse(JSON.parse(raw));
77
+ if (!parsed.success)
78
+ return DEFAULT_POLICY;
79
+ const custom = parsed.data;
80
+ return {
81
+ defaultLevel: custom.defaultLevel ?? DEFAULT_POLICY.defaultLevel,
82
+ rules: custom.rules ?? DEFAULT_POLICY.rules,
83
+ };
84
+ }
85
+ catch {
86
+ return DEFAULT_POLICY;
87
+ }
88
+ }
89
+ export { DEFAULT_POLICY };
@@ -0,0 +1,110 @@
1
+ import { randomUUID } from "node:crypto";
2
+ import { open, readFile, rm, stat } from "node:fs/promises";
3
+ import { join } from "node:path";
4
+ const LOCK_FILE = "coordination.lock";
5
+ const DEFAULT_TIMEOUT_MS = 10_000;
6
+ const MALFORMED_LOCK_STALE_MS = 30_000;
7
+ export class CoordinationLockTimeoutError extends Error {
8
+ constructor(timeoutMs) {
9
+ super(`Timed out waiting ${timeoutMs}ms for the coordination lock`);
10
+ this.name = "CoordinationLockTimeoutError";
11
+ }
12
+ }
13
+ function errorCode(error) {
14
+ return typeof error === "object" && error !== null && "code" in error
15
+ ? String(error.code)
16
+ : undefined;
17
+ }
18
+ function processIsAlive(pid) {
19
+ if (!Number.isSafeInteger(pid) || pid <= 0)
20
+ return false;
21
+ try {
22
+ process.kill(pid, 0);
23
+ return true;
24
+ }
25
+ catch (error) {
26
+ return errorCode(error) === "EPERM";
27
+ }
28
+ }
29
+ async function lockCanBeRecovered(path) {
30
+ try {
31
+ const raw = await readFile(path, "utf8");
32
+ const owner = JSON.parse(raw);
33
+ if (typeof owner.pid === "number" && typeof owner.token === "string") {
34
+ return !processIsAlive(owner.pid);
35
+ }
36
+ }
37
+ catch (error) {
38
+ if (errorCode(error) === "ENOENT")
39
+ return false;
40
+ }
41
+ try {
42
+ const info = await stat(path);
43
+ return Date.now() - info.mtimeMs > MALFORMED_LOCK_STALE_MS;
44
+ }
45
+ catch (error) {
46
+ if (errorCode(error) !== "ENOENT")
47
+ throw error;
48
+ return false;
49
+ }
50
+ }
51
+ async function releaseOwnedLock(path, token) {
52
+ try {
53
+ const raw = await readFile(path, "utf8");
54
+ const owner = JSON.parse(raw);
55
+ if (owner.token === token) {
56
+ await rm(path, { force: true });
57
+ }
58
+ }
59
+ catch (error) {
60
+ if (errorCode(error) !== "ENOENT")
61
+ throw error;
62
+ }
63
+ }
64
+ function wait(delayMs) {
65
+ return new Promise((resolve) => setTimeout(resolve, delayMs));
66
+ }
67
+ /** Serialize coordination mutations across Node processes sharing a project. */
68
+ export async function withCoordinationLock(coordinationDir, operation, timeoutMs = DEFAULT_TIMEOUT_MS) {
69
+ const path = join(coordinationDir, LOCK_FILE);
70
+ const deadline = Date.now() + timeoutMs;
71
+ const token = randomUUID();
72
+ const owner = {
73
+ token,
74
+ pid: process.pid,
75
+ createdAt: new Date().toISOString(),
76
+ };
77
+ let attempt = 0;
78
+ while (true) {
79
+ try {
80
+ const handle = await open(path, "wx", 0o600);
81
+ try {
82
+ await handle.writeFile(JSON.stringify(owner), "utf8");
83
+ }
84
+ finally {
85
+ await handle.close();
86
+ }
87
+ break;
88
+ }
89
+ catch (error) {
90
+ if (errorCode(error) !== "EEXIST")
91
+ throw error;
92
+ if (await lockCanBeRecovered(path)) {
93
+ await rm(path, { force: true });
94
+ continue;
95
+ }
96
+ if (Date.now() >= deadline) {
97
+ throw new CoordinationLockTimeoutError(timeoutMs);
98
+ }
99
+ const backoff = Math.min(5 * 2 ** attempt, 100);
100
+ attempt += 1;
101
+ await wait(backoff + Math.floor(Math.random() * 10));
102
+ }
103
+ }
104
+ try {
105
+ return await operation();
106
+ }
107
+ finally {
108
+ await releaseOwnedLock(path, token);
109
+ }
110
+ }
@@ -0,0 +1,424 @@
1
+ /**
2
+ * MCP server for live coordination between AI coding agents.
3
+ *
4
+ * Exposes the coordination tools over stdio MCP so both Claude Code and Codex
5
+ * can connect to the same project coordinator. Phase 1: backed by the
6
+ * append-only JSONL log, no daemon required.
7
+ *
8
+ * Start with: loadout serve
9
+ * Connect from Claude Code: add to .claude/settings.local.json mcp servers
10
+ * Connect from Codex: add to .agents/mcp.json
11
+ */
12
+ import { Server } from "@modelcontextprotocol/sdk/server/index.js";
13
+ import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
14
+ import { CallToolRequestSchema, ListToolsRequestSchema, } from "@modelcontextprotocol/sdk/types.js";
15
+ import { z } from "zod";
16
+ import { claimOwnership, releaseOwnership, emit, publishContract, readAfterCursor, snapshot, formatSnapshot, } from "./coordinator.js";
17
+ const agentSchema = z.string().trim().min(1).max(128);
18
+ const pathSchema = z.string().trim().min(1).max(1_024);
19
+ const claimTaskArgumentsSchema = z
20
+ .object({
21
+ agent: agentSchema,
22
+ taskId: z.string().trim().min(1).max(128).optional(),
23
+ description: z.string().trim().min(1).max(8_192).optional(),
24
+ ownPaths: z.array(pathSchema).min(1).max(256).optional(),
25
+ })
26
+ .strict()
27
+ .refine((args) => args.taskId || args.description, {
28
+ message: "description is required when taskId is omitted",
29
+ path: ["description"],
30
+ });
31
+ const publishContractArgumentsSchema = z
32
+ .object({
33
+ agent: agentSchema,
34
+ name: z.string().trim().min(1).max(200),
35
+ revision: z.number().int().positive().optional(),
36
+ body: z.string().max(100_000),
37
+ format: z.string().trim().min(1).max(50).optional(),
38
+ })
39
+ .strict();
40
+ const releaseOwnershipArgumentsSchema = z
41
+ .object({
42
+ agent: agentSchema,
43
+ paths: z.array(pathSchema).min(1).max(256),
44
+ })
45
+ .strict();
46
+ const publishUpdateArgumentsSchema = z
47
+ .object({
48
+ agent: agentSchema,
49
+ files: z.array(pathSchema).max(256).optional(),
50
+ commands: z
51
+ .array(z
52
+ .object({
53
+ command: z.string().max(500),
54
+ exitCode: z.number().int().optional(),
55
+ summary: z.string().max(1_000).optional(),
56
+ })
57
+ .strict())
58
+ .max(20)
59
+ .optional(),
60
+ note: z.string().max(5_000).optional(),
61
+ blockers: z.array(z.string().max(500)).max(10).optional(),
62
+ next: z.string().max(1_000).optional(),
63
+ })
64
+ .strict();
65
+ const subscribeArgumentsSchema = z
66
+ .object({
67
+ agent: agentSchema,
68
+ cursor: z.number().int().min(-1),
69
+ })
70
+ .strict();
71
+ const ackArgumentsSchema = z
72
+ .object({
73
+ agent: agentSchema,
74
+ eventSeq: z.number().int().nonnegative(),
75
+ note: z.string().max(1_000).optional(),
76
+ })
77
+ .strict();
78
+ const snapshotArgumentsSchema = z.object({ agent: agentSchema }).strict();
79
+ const TOOL_DEFINITIONS = [
80
+ {
81
+ name: "claim_task",
82
+ description: "Reserve a task and optionally declare file ownership. Returns the task event and any ownership conflicts.",
83
+ inputSchema: {
84
+ type: "object",
85
+ properties: {
86
+ agent: {
87
+ type: "string",
88
+ description: "Agent claiming the task (e.g. 'claude-code', 'codex')",
89
+ },
90
+ taskId: {
91
+ type: "string",
92
+ description: "ID of the pending task to claim, or omit to create a new task",
93
+ },
94
+ description: {
95
+ type: "string",
96
+ description: "Task description (required when creating a new task)",
97
+ },
98
+ ownPaths: {
99
+ type: "array",
100
+ items: { type: "string" },
101
+ description: "File/directory paths to claim exclusive ownership of during this task",
102
+ },
103
+ },
104
+ required: ["agent"],
105
+ },
106
+ },
107
+ {
108
+ name: "release_ownership",
109
+ description: "Release exact file or directory paths previously owned by this agent.",
110
+ inputSchema: {
111
+ type: "object",
112
+ properties: {
113
+ agent: { type: "string", description: "Agent releasing ownership" },
114
+ paths: {
115
+ type: "array",
116
+ items: { type: "string" },
117
+ description: "Exact project-relative paths to release",
118
+ },
119
+ },
120
+ required: ["agent", "paths"],
121
+ },
122
+ },
123
+ {
124
+ name: "publish_contract",
125
+ description: "Publish a versioned API contract (endpoint, schema, types). Other agents are notified of the new revision.",
126
+ inputSchema: {
127
+ type: "object",
128
+ properties: {
129
+ agent: {
130
+ type: "string",
131
+ description: "Publishing agent",
132
+ },
133
+ name: {
134
+ type: "string",
135
+ description: "Stable contract name (e.g. 'auth-api', 'user-schema')",
136
+ },
137
+ revision: {
138
+ type: "number",
139
+ description: "Monotonically increasing revision number",
140
+ },
141
+ body: {
142
+ type: "string",
143
+ description: "Contract body — TypeScript types, OpenAPI fragment, SQL, etc.",
144
+ },
145
+ format: {
146
+ type: "string",
147
+ description: "Format hint: 'typescript', 'openapi-yaml', 'sql', 'json-schema'",
148
+ },
149
+ },
150
+ required: ["agent", "name", "body"],
151
+ },
152
+ },
153
+ {
154
+ name: "publish_update",
155
+ description: "Report progress: files touched, commands run, blockers, next action.",
156
+ inputSchema: {
157
+ type: "object",
158
+ properties: {
159
+ agent: {
160
+ type: "string",
161
+ description: "Reporting agent",
162
+ },
163
+ files: {
164
+ type: "array",
165
+ items: { type: "string" },
166
+ description: "Files modified since last update",
167
+ },
168
+ commands: {
169
+ type: "array",
170
+ items: {
171
+ type: "object",
172
+ properties: {
173
+ command: { type: "string" },
174
+ exitCode: { type: "number" },
175
+ summary: { type: "string" },
176
+ },
177
+ required: ["command"],
178
+ },
179
+ description: "Commands run and their outcomes",
180
+ },
181
+ note: { type: "string", description: "Free-form progress note" },
182
+ blockers: {
183
+ type: "array",
184
+ items: { type: "string" },
185
+ description: "Blockers preventing further progress",
186
+ },
187
+ next: {
188
+ type: "string",
189
+ description: "What this agent plans to do next",
190
+ },
191
+ },
192
+ required: ["agent"],
193
+ },
194
+ },
195
+ {
196
+ name: "subscribe",
197
+ description: "Read events after a cursor (sequence number). Returns new events and the current high watermark for reconnection.",
198
+ inputSchema: {
199
+ type: "object",
200
+ properties: {
201
+ agent: {
202
+ type: "string",
203
+ description: "Subscribing agent (filters to relevant events)",
204
+ },
205
+ cursor: {
206
+ type: "number",
207
+ description: "Sequence number to read after (-1 for all). Events with seq > cursor are returned.",
208
+ },
209
+ },
210
+ required: ["agent", "cursor"],
211
+ },
212
+ },
213
+ {
214
+ name: "ack",
215
+ description: "Acknowledge that an agent has incorporated events up to a sequence number.",
216
+ inputSchema: {
217
+ type: "object",
218
+ properties: {
219
+ agent: {
220
+ type: "string",
221
+ description: "Acknowledging agent",
222
+ },
223
+ eventSeq: {
224
+ type: "number",
225
+ description: "Sequence number of the event being acknowledged",
226
+ },
227
+ note: {
228
+ type: "string",
229
+ description: "Optional note about what the agent did with this event",
230
+ },
231
+ },
232
+ required: ["agent", "eventSeq"],
233
+ },
234
+ },
235
+ {
236
+ name: "snapshot",
237
+ description: "Get a bounded summary of current coordination state for a reconnecting agent: pending tasks, active contracts, file ownership, unacked events.",
238
+ inputSchema: {
239
+ type: "object",
240
+ properties: {
241
+ agent: {
242
+ type: "string",
243
+ description: "Agent requesting the snapshot",
244
+ },
245
+ },
246
+ required: ["agent"],
247
+ },
248
+ },
249
+ ];
250
+ export async function startMcpServer(projectRoot) {
251
+ const server = new Server({ name: "loadout-coordinator", version: "0.9.0" }, { capabilities: { tools: {} } });
252
+ server.setRequestHandler(ListToolsRequestSchema, () => ({
253
+ tools: [...TOOL_DEFINITIONS],
254
+ }));
255
+ server.setRequestHandler(CallToolRequestSchema, async (request) => {
256
+ const { name } = request.params;
257
+ const rawArguments = request.params.arguments ?? {};
258
+ try {
259
+ switch (name) {
260
+ case "claim_task": {
261
+ const args = claimTaskArgumentsSchema.parse(rawArguments);
262
+ const results = [];
263
+ if (args.ownPaths) {
264
+ await claimOwnership(projectRoot, {
265
+ agent: args.agent,
266
+ paths: args.ownPaths,
267
+ mode: "exclusive",
268
+ });
269
+ results.push(`Claimed ownership: ${args.ownPaths.join(", ")}`);
270
+ }
271
+ const description = args.description ?? "Claimed existing task";
272
+ const event = await emit(projectRoot, {
273
+ from: args.agent,
274
+ to: "*",
275
+ type: "task",
276
+ description,
277
+ ...(args.taskId ? { resolves: args.taskId } : {}),
278
+ });
279
+ results.push(`Task ${event.id} (seq ${event.seq}): ${description}`);
280
+ return {
281
+ content: [{ type: "text", text: results.join("\n") }],
282
+ };
283
+ }
284
+ case "publish_contract": {
285
+ const args = publishContractArgumentsSchema.parse(rawArguments);
286
+ const event = await publishContract(projectRoot, {
287
+ from: args.agent,
288
+ name: args.name,
289
+ revision: args.revision,
290
+ body: args.body,
291
+ ...(args.format ? { format: args.format } : {}),
292
+ });
293
+ const revision = event.payload.revision;
294
+ return {
295
+ content: [
296
+ {
297
+ type: "text",
298
+ text: `Published contract '${args.name}' rev${revision} (seq ${event.seq})`,
299
+ },
300
+ ],
301
+ };
302
+ }
303
+ case "release_ownership": {
304
+ const args = releaseOwnershipArgumentsSchema.parse(rawArguments);
305
+ const event = await releaseOwnership(projectRoot, {
306
+ agent: args.agent,
307
+ paths: args.paths,
308
+ });
309
+ return {
310
+ content: [
311
+ {
312
+ type: "text",
313
+ text: `Released ownership: ${args.paths.join(", ")} (seq ${event.seq})`,
314
+ },
315
+ ],
316
+ };
317
+ }
318
+ case "publish_update": {
319
+ const args = publishUpdateArgumentsSchema.parse(rawArguments);
320
+ const payload = {};
321
+ if (args.files)
322
+ payload.files = args.files;
323
+ if (args.commands)
324
+ payload.commands = args.commands;
325
+ if (args.note)
326
+ payload.note = args.note;
327
+ if (args.blockers)
328
+ payload.blockers = args.blockers;
329
+ if (args.next)
330
+ payload.next = args.next;
331
+ const parts = [];
332
+ if (args.note)
333
+ parts.push(args.note);
334
+ if (args.files)
335
+ parts.push(`${args.files.length} files`);
336
+ if (args.blockers)
337
+ parts.push(`${args.blockers.length} blocker(s)`);
338
+ const event = await emit(projectRoot, {
339
+ from: args.agent,
340
+ to: "*",
341
+ type: "update",
342
+ description: parts.join("; ") || "Progress update",
343
+ payload,
344
+ });
345
+ return {
346
+ content: [
347
+ {
348
+ type: "text",
349
+ text: `Update published (seq ${event.seq})`,
350
+ },
351
+ ],
352
+ };
353
+ }
354
+ case "subscribe": {
355
+ const args = subscribeArgumentsSchema.parse(rawArguments);
356
+ const { events, highSeq } = await readAfterCursor(projectRoot, args.cursor);
357
+ // Filter to events relevant to this agent
358
+ const agent = args.agent;
359
+ const relevant = events.filter((e) => e.to === agent || e.to === "*" || e.from === agent);
360
+ return {
361
+ content: [
362
+ {
363
+ type: "text",
364
+ text: JSON.stringify({ events: relevant, highSeq, count: relevant.length }, null, 2),
365
+ },
366
+ ],
367
+ };
368
+ }
369
+ case "ack": {
370
+ const args = ackArgumentsSchema.parse(rawArguments);
371
+ const event = await emit(projectRoot, {
372
+ from: args.agent,
373
+ to: "*",
374
+ type: "ack",
375
+ description: `Acknowledged through seq ${args.eventSeq}`,
376
+ payload: {
377
+ eventSeq: args.eventSeq,
378
+ ...(args.note ? { note: args.note } : {}),
379
+ },
380
+ });
381
+ return {
382
+ content: [
383
+ {
384
+ type: "text",
385
+ text: `Acknowledged through seq ${args.eventSeq} (ack seq ${event.seq})`,
386
+ },
387
+ ],
388
+ };
389
+ }
390
+ case "snapshot": {
391
+ const args = snapshotArgumentsSchema.parse(rawArguments);
392
+ const snap = await snapshot(projectRoot, args.agent);
393
+ return {
394
+ content: [
395
+ { type: "text", text: formatSnapshot(snap) },
396
+ {
397
+ type: "text",
398
+ text: `\n---\nJSON:\n${JSON.stringify(snap, null, 2)}`,
399
+ },
400
+ ],
401
+ };
402
+ }
403
+ default:
404
+ return {
405
+ content: [{ type: "text", text: `Unknown tool: ${name}` }],
406
+ isError: true,
407
+ };
408
+ }
409
+ }
410
+ catch (error) {
411
+ return {
412
+ content: [
413
+ {
414
+ type: "text",
415
+ text: `Error: ${error instanceof Error ? error.message : String(error)}`,
416
+ },
417
+ ],
418
+ isError: true,
419
+ };
420
+ }
421
+ });
422
+ const transport = new StdioServerTransport();
423
+ await server.connect(transport);
424
+ }