@agent-workspace/mcp-server 0.4.0 → 0.6.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 (53) hide show
  1. package/dist/index.d.ts +5 -15
  2. package/dist/index.d.ts.map +1 -1
  3. package/dist/index.js +30 -1836
  4. package/dist/index.js.map +1 -1
  5. package/dist/index.test.d.ts +2 -0
  6. package/dist/index.test.d.ts.map +1 -0
  7. package/dist/index.test.js +726 -0
  8. package/dist/index.test.js.map +1 -0
  9. package/dist/tools/artifact.d.ts +6 -0
  10. package/dist/tools/artifact.d.ts.map +1 -0
  11. package/dist/tools/artifact.js +428 -0
  12. package/dist/tools/artifact.js.map +1 -0
  13. package/dist/tools/config.d.ts +6 -0
  14. package/dist/tools/config.d.ts.map +1 -0
  15. package/dist/tools/config.js +91 -0
  16. package/dist/tools/config.js.map +1 -0
  17. package/dist/tools/contract.d.ts +6 -0
  18. package/dist/tools/contract.d.ts.map +1 -0
  19. package/dist/tools/contract.js +233 -0
  20. package/dist/tools/contract.js.map +1 -0
  21. package/dist/tools/identity.d.ts +6 -0
  22. package/dist/tools/identity.d.ts.map +1 -0
  23. package/dist/tools/identity.js +91 -0
  24. package/dist/tools/identity.js.map +1 -0
  25. package/dist/tools/memory.d.ts +6 -0
  26. package/dist/tools/memory.d.ts.map +1 -0
  27. package/dist/tools/memory.js +145 -0
  28. package/dist/tools/memory.js.map +1 -0
  29. package/dist/tools/project.d.ts +6 -0
  30. package/dist/tools/project.d.ts.map +1 -0
  31. package/dist/tools/project.js +184 -0
  32. package/dist/tools/project.js.map +1 -0
  33. package/dist/tools/reputation.d.ts +6 -0
  34. package/dist/tools/reputation.d.ts.map +1 -0
  35. package/dist/tools/reputation.js +206 -0
  36. package/dist/tools/reputation.js.map +1 -0
  37. package/dist/tools/status.d.ts +6 -0
  38. package/dist/tools/status.d.ts.map +1 -0
  39. package/dist/tools/status.js +223 -0
  40. package/dist/tools/status.js.map +1 -0
  41. package/dist/tools/swarm.d.ts +6 -0
  42. package/dist/tools/swarm.d.ts.map +1 -0
  43. package/dist/tools/swarm.js +483 -0
  44. package/dist/tools/swarm.js.map +1 -0
  45. package/dist/tools/task.d.ts +6 -0
  46. package/dist/tools/task.d.ts.map +1 -0
  47. package/dist/tools/task.js +347 -0
  48. package/dist/tools/task.js.map +1 -0
  49. package/dist/utils.d.ts +23 -0
  50. package/dist/utils.d.ts.map +1 -0
  51. package/dist/utils.js +56 -0
  52. package/dist/utils.js.map +1 -0
  53. package/package.json +3 -2
package/dist/index.js CHANGED
@@ -1,1846 +1,40 @@
1
1
  #!/usr/bin/env node
2
- import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
3
- import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
4
- import * as z from "zod";
5
- import { readFile, writeFile, readdir, mkdir, access, stat } from "node:fs/promises";
6
- import { join, resolve, relative, isAbsolute } from "node:path";
7
- import matter from "gray-matter";
8
- import { AWP_VERSION, SMP_VERSION, RDP_VERSION, CDP_VERSION, MEMORY_DIR, ARTIFACTS_DIR, REPUTATION_DIR, CONTRACTS_DIR, PROJECTS_DIR, } from "@agent-workspace/core";
9
- // =============================================================================
10
- // Security Constants
11
- // =============================================================================
12
- /** Maximum file size allowed (1MB) */
13
- const MAX_FILE_SIZE = 1024 * 1024;
14
- /** Pattern for valid slugs */
15
- const SLUG_PATTERN = /^[a-z0-9][a-z0-9-]*$/;
16
- // =============================================================================
17
- // Security Utilities
18
- // =============================================================================
19
- /**
20
- * Validate that a path is within the workspace root (prevents directory traversal).
21
- * Returns the normalized absolute path if valid, or throws an error.
22
- * @internal Available for future use in tool handlers
23
- */
24
- export function _validatePath(root, targetPath) {
25
- const normalized = resolve(root, targetPath);
26
- const rel = relative(root, normalized);
27
- // Prevent directory traversal
28
- if (rel.startsWith("..") || isAbsolute(rel)) {
29
- throw new Error(`Path traversal detected: ${targetPath}`);
30
- }
31
- return normalized;
32
- }
33
- /**
34
- * Validate and sanitize a slug.
35
- * Slugs must be lowercase alphanumeric with hyphens, not starting with hyphen.
36
- * @internal Available for future use in tool handlers
37
- */
38
- export function _validateSlug(slug) {
39
- const trimmed = slug.trim().toLowerCase();
40
- if (!SLUG_PATTERN.test(trimmed)) {
41
- throw new Error(`Invalid slug: "${slug}". Must be lowercase alphanumeric with hyphens, not starting with hyphen.`);
42
- }
43
- // Additional safety: limit length
44
- if (trimmed.length > 100) {
45
- throw new Error(`Slug too long: max 100 characters`);
46
- }
47
- return trimmed;
48
- }
49
2
  /**
50
- * Read a file with size limit check.
51
- * @internal Available for future use in tool handlers
3
+ * AWP MCP Server
4
+ *
5
+ * MCP server exposing Agent Workspace Protocol operations.
6
+ * Plug any AWP workspace into any MCP client.
52
7
  */
53
- export async function _safeReadFile(path) {
54
- const stats = await stat(path);
55
- if (stats.size > MAX_FILE_SIZE) {
56
- throw new Error(`File too large: ${stats.size} bytes (max: ${MAX_FILE_SIZE})`);
57
- }
58
- return readFile(path, "utf-8");
59
- }
8
+ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
9
+ import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
10
+ import { AWP_VERSION } from "@agent-workspace/core";
11
+ // Tool registration modules
12
+ import { registerIdentityTools } from "./tools/identity.js";
13
+ import { registerMemoryTools } from "./tools/memory.js";
14
+ import { registerArtifactTools } from "./tools/artifact.js";
15
+ import { registerStatusTools } from "./tools/status.js";
16
+ import { registerReputationTools } from "./tools/reputation.js";
17
+ import { registerContractTools } from "./tools/contract.js";
18
+ import { registerProjectTools } from "./tools/project.js";
19
+ import { registerTaskTools } from "./tools/task.js";
20
+ import { registerConfigTools } from "./tools/config.js";
21
+ import { registerSwarmTools } from "./tools/swarm.js";
22
+ // Create MCP server
60
23
  const server = new McpServer({
61
24
  name: "awp-workspace",
62
25
  version: AWP_VERSION,
63
26
  });
64
- /**
65
- * Resolve workspace root from the AWP_WORKSPACE env var or cwd
66
- */
67
- function getWorkspaceRoot() {
68
- return process.env.AWP_WORKSPACE || process.cwd();
69
- }
70
- async function fileExists(path) {
71
- try {
72
- await access(path);
73
- return true;
74
- }
75
- catch {
76
- return false;
77
- }
78
- }
79
- /**
80
- * Get agent DID from workspace manifest, or "anonymous"
81
- */
82
- async function getAgentDid(root) {
83
- try {
84
- const raw = await readFile(join(root, ".awp", "workspace.json"), "utf-8");
85
- const manifest = JSON.parse(raw);
86
- return manifest.agent?.did || "anonymous";
87
- }
88
- catch {
89
- return "anonymous";
90
- }
91
- }
92
- // --- Tool: awp_read_identity ---
93
- server.registerTool("awp_read_identity", {
94
- title: "Read Agent Identity",
95
- description: "Read this agent's identity (name, creature, capabilities, DID) from the AWP workspace",
96
- inputSchema: {},
97
- }, async () => {
98
- const root = getWorkspaceRoot();
99
- const path = join(root, "IDENTITY.md");
100
- try {
101
- const raw = await readFile(path, "utf-8");
102
- const { data, content } = matter(raw);
103
- return {
104
- content: [
105
- {
106
- type: "text",
107
- text: JSON.stringify({ frontmatter: data, body: content.trim() }, null, 2),
108
- },
109
- ],
110
- };
111
- }
112
- catch {
113
- return {
114
- content: [{ type: "text", text: "Error: IDENTITY.md not found" }],
115
- isError: true,
116
- };
117
- }
118
- });
119
- // --- Tool: awp_read_soul ---
120
- server.registerTool("awp_read_soul", {
121
- title: "Read Agent Soul",
122
- description: "Read this agent's values, boundaries, and governance rules from the AWP workspace",
123
- inputSchema: {},
124
- }, async () => {
125
- const root = getWorkspaceRoot();
126
- const path = join(root, "SOUL.md");
127
- try {
128
- const raw = await readFile(path, "utf-8");
129
- const { data, content } = matter(raw);
130
- return {
131
- content: [
132
- {
133
- type: "text",
134
- text: JSON.stringify({ frontmatter: data, body: content.trim() }, null, 2),
135
- },
136
- ],
137
- };
138
- }
139
- catch {
140
- return {
141
- content: [{ type: "text", text: "Error: SOUL.md not found" }],
142
- isError: true,
143
- };
144
- }
145
- });
146
- // --- Tool: awp_read_user ---
147
- server.registerTool("awp_read_user", {
148
- title: "Read Human Profile",
149
- description: "Read the human user's profile from the AWP workspace",
150
- inputSchema: {},
151
- }, async () => {
152
- const root = getWorkspaceRoot();
153
- const path = join(root, "USER.md");
154
- try {
155
- const raw = await readFile(path, "utf-8");
156
- const { data, content } = matter(raw);
157
- return {
158
- content: [
159
- {
160
- type: "text",
161
- text: JSON.stringify({ frontmatter: data, body: content.trim() }, null, 2),
162
- },
163
- ],
164
- };
165
- }
166
- catch {
167
- return {
168
- content: [{ type: "text", text: "Error: USER.md not found" }],
169
- isError: true,
170
- };
171
- }
172
- });
173
- // --- Tool: awp_read_memory ---
174
- server.registerTool("awp_read_memory", {
175
- title: "Read Memory",
176
- description: "Read memory entries. Specify a date (YYYY-MM-DD) for daily logs, or 'longterm' for MEMORY.md, or 'recent' for the last 3 days",
177
- inputSchema: {
178
- target: z
179
- .string()
180
- .describe("Which memory to read: a date like '2026-01-30', 'longterm', or 'recent'"),
181
- },
182
- }, async ({ target }) => {
183
- const root = getWorkspaceRoot();
184
- if (target === "longterm") {
185
- const path = join(root, "MEMORY.md");
186
- try {
187
- const raw = await readFile(path, "utf-8");
188
- const { data, content } = matter(raw);
189
- return {
190
- content: [
191
- {
192
- type: "text",
193
- text: JSON.stringify({ frontmatter: data, body: content.trim() }, null, 2),
194
- },
195
- ],
196
- };
197
- }
198
- catch {
199
- return {
200
- content: [{ type: "text", text: "No long-term memory file exists yet." }],
201
- };
202
- }
203
- }
204
- if (target === "recent") {
205
- const memDir = join(root, MEMORY_DIR);
206
- try {
207
- const files = await readdir(memDir);
208
- const mdFiles = files
209
- .filter((f) => f.endsWith(".md"))
210
- .sort()
211
- .reverse()
212
- .slice(0, 3);
213
- const results = [];
214
- for (const f of mdFiles) {
215
- const raw = await readFile(join(memDir, f), "utf-8");
216
- const { data, content } = matter(raw);
217
- results.push(`--- ${f} ---\n${JSON.stringify(data, null, 2)}\n${content.trim()}`);
218
- }
219
- return {
220
- content: [
221
- {
222
- type: "text",
223
- text: results.length ? results.join("\n\n") : "No recent memory entries.",
224
- },
225
- ],
226
- };
227
- }
228
- catch {
229
- return {
230
- content: [{ type: "text", text: "No memory directory found." }],
231
- };
232
- }
233
- }
234
- // Specific date
235
- const path = join(root, MEMORY_DIR, `${target}.md`);
236
- try {
237
- const raw = await readFile(path, "utf-8");
238
- const { data, content } = matter(raw);
239
- return {
240
- content: [
241
- {
242
- type: "text",
243
- text: JSON.stringify({ frontmatter: data, body: content.trim() }, null, 2),
244
- },
245
- ],
246
- };
247
- }
248
- catch {
249
- return {
250
- content: [{ type: "text", text: `No memory entry for ${target}.` }],
251
- };
252
- }
253
- });
254
- // --- Tool: awp_write_memory ---
255
- server.registerTool("awp_write_memory", {
256
- title: "Write Memory",
257
- description: "Append an entry to today's memory log in the AWP workspace",
258
- inputSchema: {
259
- content: z.string().describe("The memory entry to log"),
260
- tags: z.array(z.string()).optional().describe("Optional categorization tags"),
261
- },
262
- }, async ({ content: entryContent, tags }) => {
263
- const root = getWorkspaceRoot();
264
- const memDir = join(root, MEMORY_DIR);
265
- await mkdir(memDir, { recursive: true });
266
- const date = new Date().toISOString().split("T")[0];
267
- const time = new Date().toTimeString().slice(0, 5);
268
- const filePath = join(memDir, `${date}.md`);
269
- const entry = {
270
- time,
271
- content: entryContent,
272
- tags: tags?.length ? tags : undefined,
273
- };
274
- let fileData;
275
- try {
276
- const raw = await readFile(filePath, "utf-8");
277
- fileData = matter(raw);
278
- if (!fileData.data.entries)
279
- fileData.data.entries = [];
280
- fileData.data.entries.push(entry);
281
- }
282
- catch {
283
- fileData = {
284
- data: {
285
- awp: AWP_VERSION,
286
- type: "memory-daily",
287
- date,
288
- entries: [entry],
289
- },
290
- content: `\n# ${date}\n\n`,
291
- };
292
- }
293
- const tagStr = tags?.length ? ` [${tags.join(", ")}]` : "";
294
- fileData.content += `- **${time}** — ${entryContent}${tagStr}\n`;
295
- const output = matter.stringify(fileData.content, fileData.data);
296
- await writeFile(filePath, output, "utf-8");
297
- return {
298
- content: [
299
- {
300
- type: "text",
301
- text: `Logged to memory/${date}.md at ${time}`,
302
- },
303
- ],
304
- };
305
- });
306
- // --- Tool: awp_artifact_read ---
307
- server.registerTool("awp_artifact_read", {
308
- title: "Read Knowledge Artifact",
309
- description: "Read a knowledge artifact by slug. Returns metadata (title, version, confidence, provenance) and body content.",
310
- inputSchema: {
311
- slug: z.string().describe("Artifact slug (e.g., 'llm-context-research')"),
312
- },
313
- }, async ({ slug }) => {
314
- const root = getWorkspaceRoot();
315
- const path = join(root, ARTIFACTS_DIR, `${slug}.md`);
316
- try {
317
- const raw = await readFile(path, "utf-8");
318
- const { data, content } = matter(raw);
319
- return {
320
- content: [
321
- {
322
- type: "text",
323
- text: JSON.stringify({ frontmatter: data, body: content.trim() }, null, 2),
324
- },
325
- ],
326
- };
327
- }
328
- catch {
329
- return {
330
- content: [{ type: "text", text: `Artifact "${slug}" not found.` }],
331
- isError: true,
332
- };
333
- }
334
- });
335
- // --- Tool: awp_artifact_write ---
336
- server.registerTool("awp_artifact_write", {
337
- title: "Write Knowledge Artifact",
338
- description: "Create or update a knowledge artifact. If the artifact exists, increments version and appends provenance. If new, creates it with version 1.",
339
- inputSchema: {
340
- slug: z.string().describe("Artifact slug (lowercase, hyphens only)"),
341
- title: z.string().optional().describe("Title (required for new artifacts)"),
342
- content: z.string().describe("Markdown body content"),
343
- tags: z.array(z.string()).optional().describe("Categorization tags"),
344
- confidence: z.number().min(0).max(1).optional().describe("Confidence score (0.0-1.0)"),
345
- message: z.string().optional().describe("Commit message for provenance"),
346
- },
347
- }, async ({ slug, title, content: bodyContent, tags, confidence, message }) => {
348
- const root = getWorkspaceRoot();
349
- const artifactsDir = join(root, ARTIFACTS_DIR);
350
- await mkdir(artifactsDir, { recursive: true });
351
- const filePath = join(artifactsDir, `${slug}.md`);
352
- const did = await getAgentDid(root);
353
- const now = new Date().toISOString();
354
- let fileData;
355
- let version;
356
- let isNew = false;
357
- try {
358
- const raw = await readFile(filePath, "utf-8");
359
- fileData = matter(raw);
360
- // Update existing
361
- fileData.data.version = (fileData.data.version || 1) + 1;
362
- version = fileData.data.version;
363
- fileData.data.lastModified = now;
364
- fileData.data.modifiedBy = did;
365
- fileData.content = `\n${bodyContent}\n`;
366
- if (confidence !== undefined)
367
- fileData.data.confidence = confidence;
368
- if (tags)
369
- fileData.data.tags = tags;
370
- if (title)
371
- fileData.data.title = title;
372
- // Add author if new
373
- if (!fileData.data.authors?.includes(did)) {
374
- if (!fileData.data.authors)
375
- fileData.data.authors = [];
376
- fileData.data.authors.push(did);
377
- }
378
- // Append provenance
379
- if (!fileData.data.provenance)
380
- fileData.data.provenance = [];
381
- fileData.data.provenance.push({
382
- agent: did,
383
- action: "updated",
384
- timestamp: now,
385
- message,
386
- confidence,
387
- });
388
- }
389
- catch {
390
- // Create new
391
- isNew = true;
392
- version = 1;
393
- const artifactTitle = title ||
394
- slug
395
- .split("-")
396
- .map((w) => w.charAt(0).toUpperCase() + w.slice(1))
397
- .join(" ");
398
- fileData = {
399
- data: {
400
- awp: AWP_VERSION,
401
- smp: SMP_VERSION,
402
- type: "knowledge-artifact",
403
- id: `artifact:${slug}`,
404
- title: artifactTitle,
405
- authors: [did],
406
- version: 1,
407
- confidence,
408
- tags: tags?.length ? tags : undefined,
409
- created: now,
410
- lastModified: now,
411
- modifiedBy: did,
412
- provenance: [
413
- {
414
- agent: did,
415
- action: "created",
416
- timestamp: now,
417
- message,
418
- },
419
- ],
420
- },
421
- content: `\n${bodyContent}\n`,
422
- };
423
- }
424
- const output = matter.stringify(fileData.content, fileData.data);
425
- await writeFile(filePath, output, "utf-8");
426
- return {
427
- content: [
428
- {
429
- type: "text",
430
- text: `${isNew ? "Created" : "Updated"} artifacts/${slug}.md (version ${version})`,
431
- },
432
- ],
433
- };
434
- });
435
- // --- Tool: awp_artifact_list ---
436
- server.registerTool("awp_artifact_list", {
437
- title: "List Knowledge Artifacts",
438
- description: "List all knowledge artifacts in the workspace with metadata",
439
- inputSchema: {
440
- tag: z.string().optional().describe("Filter by tag"),
441
- },
442
- }, async ({ tag }) => {
443
- const root = getWorkspaceRoot();
444
- const artifactsDir = join(root, ARTIFACTS_DIR);
445
- let files;
446
- try {
447
- files = await readdir(artifactsDir);
448
- }
449
- catch {
450
- return {
451
- content: [{ type: "text", text: JSON.stringify({ artifacts: [] }, null, 2) }],
452
- };
453
- }
454
- const mdFiles = files.filter((f) => f.endsWith(".md")).sort();
455
- const artifacts = [];
456
- for (const f of mdFiles) {
457
- try {
458
- const raw = await readFile(join(artifactsDir, f), "utf-8");
459
- const { data } = matter(raw);
460
- if (data.type !== "knowledge-artifact")
461
- continue;
462
- if (tag && !data.tags?.some((t) => t.toLowerCase() === tag.toLowerCase())) {
463
- continue;
464
- }
465
- artifacts.push({
466
- slug: f.replace(/\.md$/, ""),
467
- title: data.title,
468
- version: data.version,
469
- confidence: data.confidence,
470
- tags: data.tags,
471
- authors: data.authors,
472
- lastModified: data.lastModified,
473
- });
474
- }
475
- catch {
476
- // Skip unparseable
477
- }
478
- }
479
- return {
480
- content: [
481
- {
482
- type: "text",
483
- text: JSON.stringify({ artifacts }, null, 2),
484
- },
485
- ],
486
- };
487
- });
488
- // --- Tool: awp_artifact_search ---
489
- server.registerTool("awp_artifact_search", {
490
- title: "Search Knowledge Artifacts",
491
- description: "Search artifacts by title, tags, or body content",
492
- inputSchema: {
493
- query: z.string().describe("Search query"),
494
- },
495
- }, async ({ query }) => {
496
- const root = getWorkspaceRoot();
497
- const artifactsDir = join(root, ARTIFACTS_DIR);
498
- const queryLower = query.toLowerCase();
499
- let files;
500
- try {
501
- files = await readdir(artifactsDir);
502
- }
503
- catch {
504
- return {
505
- content: [{ type: "text", text: JSON.stringify({ results: [] }, null, 2) }],
506
- };
507
- }
508
- const mdFiles = files.filter((f) => f.endsWith(".md")).sort();
509
- const results = [];
510
- for (const f of mdFiles) {
511
- try {
512
- const raw = await readFile(join(artifactsDir, f), "utf-8");
513
- const { data, content } = matter(raw);
514
- if (data.type !== "knowledge-artifact")
515
- continue;
516
- const titleMatch = data.title?.toLowerCase().includes(queryLower);
517
- const tagMatch = data.tags?.some((t) => t.toLowerCase().includes(queryLower));
518
- const bodyMatch = content.toLowerCase().includes(queryLower);
519
- if (titleMatch || tagMatch || bodyMatch) {
520
- results.push({
521
- slug: f.replace(/\.md$/, ""),
522
- title: data.title,
523
- version: data.version,
524
- confidence: data.confidence,
525
- tags: data.tags,
526
- matchedIn: [titleMatch && "title", tagMatch && "tags", bodyMatch && "body"].filter(Boolean),
527
- });
528
- }
529
- }
530
- catch {
531
- // Skip
532
- }
533
- }
534
- return {
535
- content: [
536
- {
537
- type: "text",
538
- text: JSON.stringify({ results }, null, 2),
539
- },
540
- ],
541
- };
542
- });
543
- // --- Tool: awp_workspace_status ---
544
- server.registerTool("awp_workspace_status", {
545
- title: "Workspace Status",
546
- description: "Get AWP workspace health status — manifest, files, projects, tasks, reputation, contracts, artifacts, memory, health warnings",
547
- inputSchema: {},
548
- }, async () => {
549
- const root = getWorkspaceRoot();
550
- const status = { root };
551
- // Manifest
552
- try {
553
- const manifestRaw = await readFile(join(root, ".awp", "workspace.json"), "utf-8");
554
- status.manifest = JSON.parse(manifestRaw);
555
- }
556
- catch {
557
- status.manifest = null;
558
- status.error = "No .awp/workspace.json found";
559
- }
560
- // File presence
561
- const fileChecks = [
562
- "IDENTITY.md",
563
- "SOUL.md",
564
- "AGENTS.md",
565
- "USER.md",
566
- "TOOLS.md",
567
- "HEARTBEAT.md",
568
- "MEMORY.md",
569
- ];
570
- status.files = {};
571
- for (const f of fileChecks) {
572
- status.files[f] = await fileExists(join(root, f));
573
- }
574
- // Memory stats
575
- try {
576
- const memDir = join(root, MEMORY_DIR);
577
- const files = await readdir(memDir);
578
- const mdFiles = files.filter((f) => f.endsWith(".md"));
579
- status.memory = {
580
- logCount: mdFiles.length,
581
- latest: mdFiles.sort().reverse()[0] || null,
582
- };
583
- }
584
- catch {
585
- status.memory = { logCount: 0, latest: null };
586
- }
587
- // Artifact stats
588
- try {
589
- const artDir = join(root, ARTIFACTS_DIR);
590
- const files = await readdir(artDir);
591
- const mdFiles = files.filter((f) => f.endsWith(".md"));
592
- status.artifacts = { count: mdFiles.length };
593
- }
594
- catch {
595
- status.artifacts = { count: 0 };
596
- }
597
- // Reputation stats
598
- try {
599
- const repDir = join(root, REPUTATION_DIR);
600
- const files = await readdir(repDir);
601
- const mdFiles = files.filter((f) => f.endsWith(".md"));
602
- status.reputation = { count: mdFiles.length };
603
- }
604
- catch {
605
- status.reputation = { count: 0 };
606
- }
607
- // Contract stats
608
- try {
609
- const conDir = join(root, CONTRACTS_DIR);
610
- const files = await readdir(conDir);
611
- const mdFiles = files.filter((f) => f.endsWith(".md"));
612
- status.contracts = { count: mdFiles.length };
613
- }
614
- catch {
615
- status.contracts = { count: 0 };
616
- }
617
- // Project + task stats
618
- const projectsSummary = [];
619
- let totalTasks = 0;
620
- let activeTasks = 0;
621
- try {
622
- const projDir = join(root, PROJECTS_DIR);
623
- const projFiles = await readdir(projDir);
624
- const mdFiles = projFiles.filter((f) => f.endsWith(".md")).sort();
625
- for (const f of mdFiles) {
626
- try {
627
- const raw = await readFile(join(projDir, f), "utf-8");
628
- const { data } = matter(raw);
629
- if (data.type !== "project")
630
- continue;
631
- const slug = f.replace(/\.md$/, "");
632
- const projInfo = {
633
- slug,
634
- title: data.title,
635
- status: data.status,
636
- taskCount: data.taskCount || 0,
637
- completedCount: data.completedCount || 0,
638
- };
639
- if (data.deadline)
640
- projInfo.deadline = data.deadline;
641
- projectsSummary.push(projInfo);
642
- totalTasks += data.taskCount || 0;
643
- // Count active tasks
644
- try {
645
- const taskDir = join(projDir, slug, "tasks");
646
- const taskFiles = await readdir(taskDir);
647
- for (const tf of taskFiles.filter((t) => t.endsWith(".md"))) {
648
- try {
649
- const tRaw = await readFile(join(taskDir, tf), "utf-8");
650
- const { data: tData } = matter(tRaw);
651
- if (tData.status === "in-progress" ||
652
- tData.status === "blocked" ||
653
- tData.status === "review") {
654
- activeTasks++;
655
- }
656
- }
657
- catch {
658
- /* skip */
659
- }
660
- }
661
- }
662
- catch {
663
- /* no tasks dir */
664
- }
665
- }
666
- catch {
667
- /* skip */
668
- }
669
- }
670
- }
671
- catch {
672
- /* no projects dir */
673
- }
674
- status.projects = {
675
- count: projectsSummary.length,
676
- totalTasks,
677
- activeTasks,
678
- list: projectsSummary,
679
- };
680
- // Health warnings
681
- const warnings = [];
682
- const now = new Date();
683
- const MS_PER_DAY = 24 * 60 * 60 * 1000;
684
- // Check required files
685
- if (!status.files["IDENTITY.md"])
686
- warnings.push("IDENTITY.md missing");
687
- if (!status.files["SOUL.md"])
688
- warnings.push("SOUL.md missing");
689
- // Check contract deadlines
690
- try {
691
- const conDir = join(root, CONTRACTS_DIR);
692
- const conFiles = await readdir(conDir);
693
- for (const f of conFiles.filter((f) => f.endsWith(".md"))) {
694
- try {
695
- const raw = await readFile(join(conDir, f), "utf-8");
696
- const { data } = matter(raw);
697
- if (data.deadline && (data.status === "active" || data.status === "draft")) {
698
- if (new Date(data.deadline) < now) {
699
- warnings.push(`Contract "${f.replace(/\.md$/, "")}" is past deadline`);
700
- }
701
- }
702
- }
703
- catch {
704
- /* skip */
705
- }
706
- }
707
- }
708
- catch {
709
- /* no contracts */
710
- }
711
- // Check reputation decay
712
- try {
713
- const repDir = join(root, REPUTATION_DIR);
714
- const repFiles = await readdir(repDir);
715
- for (const f of repFiles.filter((f) => f.endsWith(".md"))) {
716
- try {
717
- const raw = await readFile(join(repDir, f), "utf-8");
718
- const { data } = matter(raw);
719
- if (data.lastUpdated) {
720
- const daysSince = Math.floor((now.getTime() - new Date(data.lastUpdated).getTime()) / MS_PER_DAY);
721
- if (daysSince > 30) {
722
- warnings.push(`${f.replace(/\.md$/, "")} reputation decaying (no signal in ${daysSince} days)`);
723
- }
724
- }
725
- }
726
- catch {
727
- /* skip */
728
- }
729
- }
730
- }
731
- catch {
732
- /* no reputation */
733
- }
734
- status.health = {
735
- warnings,
736
- ok: warnings.length === 0,
737
- };
738
- return {
739
- content: [{ type: "text", text: JSON.stringify(status, null, 2) }],
740
- };
741
- });
742
- // --- Tool: awp_reputation_query ---
743
- server.registerTool("awp_reputation_query", {
744
- title: "Query Reputation",
745
- description: "Query an agent's reputation profile. Returns multi-dimensional scores with decay applied. Omit slug to list all tracked agents.",
746
- inputSchema: {
747
- slug: z.string().optional().describe("Agent reputation slug (omit to list all)"),
748
- dimension: z.string().optional().describe("Filter by dimension"),
749
- domain: z.string().optional().describe("Filter by domain competence"),
750
- },
751
- }, async ({ slug, dimension, domain }) => {
752
- const root = getWorkspaceRoot();
753
- if (!slug) {
754
- // List all profiles
755
- const repDir = join(root, REPUTATION_DIR);
756
- let files;
757
- try {
758
- files = await readdir(repDir);
759
- }
760
- catch {
761
- return {
762
- content: [{ type: "text", text: JSON.stringify({ profiles: [] }, null, 2) }],
763
- };
764
- }
765
- const profiles = [];
766
- for (const f of files.filter((f) => f.endsWith(".md")).sort()) {
767
- try {
768
- const raw = await readFile(join(repDir, f), "utf-8");
769
- const { data } = matter(raw);
770
- if (data.type !== "reputation-profile")
771
- continue;
772
- profiles.push({
773
- slug: f.replace(/\.md$/, ""),
774
- agentName: data.agentName,
775
- agentDid: data.agentDid,
776
- signalCount: data.signals?.length || 0,
777
- dimensions: Object.keys(data.dimensions || {}),
778
- domains: Object.keys(data.domainCompetence || {}),
779
- });
780
- }
781
- catch {
782
- /* skip */
783
- }
784
- }
785
- return {
786
- content: [{ type: "text", text: JSON.stringify({ profiles }, null, 2) }],
787
- };
788
- }
789
- // Read specific profile
790
- const path = join(root, REPUTATION_DIR, `${slug}.md`);
791
- try {
792
- const raw = await readFile(path, "utf-8");
793
- const { data, content } = matter(raw);
794
- // Apply decay to scores
795
- const now = new Date();
796
- const DECAY_RATE = 0.02;
797
- const MS_PER_MONTH = 30.44 * 24 * 60 * 60 * 1000;
798
- const applyDecay = (dim) => {
799
- if (!dim?.lastSignal)
800
- return dim;
801
- const months = (now.getTime() - new Date(dim.lastSignal).getTime()) / MS_PER_MONTH;
802
- if (months <= 0)
803
- return { ...dim };
804
- const factor = Math.exp(-DECAY_RATE * months);
805
- const decayed = 0.5 + (dim.score - 0.5) * factor;
806
- return { ...dim, decayedScore: Math.round(decayed * 1000) / 1000 };
807
- };
808
- const result = { ...data, body: content.trim() };
809
- // Apply decay to dimensions
810
- if (data.dimensions) {
811
- result.dimensions = {};
812
- for (const [name, dim] of Object.entries(data.dimensions)) {
813
- if (dimension && name !== dimension)
814
- continue;
815
- result.dimensions[name] = applyDecay(dim);
816
- }
817
- }
818
- if (data.domainCompetence) {
819
- result.domainCompetence = {};
820
- for (const [name, dim] of Object.entries(data.domainCompetence)) {
821
- if (domain && name !== domain)
822
- continue;
823
- result.domainCompetence[name] = applyDecay(dim);
824
- }
825
- }
826
- return {
827
- content: [{ type: "text", text: JSON.stringify(result, null, 2) }],
828
- };
829
- }
830
- catch {
831
- return {
832
- content: [{ type: "text", text: `Reputation profile "${slug}" not found.` }],
833
- isError: true,
834
- };
835
- }
836
- });
837
- // --- Tool: awp_reputation_signal ---
838
- server.registerTool("awp_reputation_signal", {
839
- title: "Log Reputation Signal",
840
- description: "Log a reputation signal for an agent. Creates the profile if it doesn't exist (requires agentDid and agentName for new profiles).",
841
- inputSchema: {
842
- slug: z.string().describe("Agent reputation slug"),
843
- dimension: z
844
- .string()
845
- .describe("Dimension (reliability, epistemic-hygiene, coordination, domain-competence)"),
846
- score: z.number().min(0).max(1).describe("Score (0.0-1.0)"),
847
- domain: z.string().optional().describe("Domain (required for domain-competence)"),
848
- evidence: z.string().optional().describe("Evidence reference"),
849
- message: z.string().optional().describe("Human-readable note"),
850
- agentDid: z.string().optional().describe("Agent DID (required for new profiles)"),
851
- agentName: z.string().optional().describe("Agent name (required for new profiles)"),
852
- },
853
- }, async ({ slug, dimension: dim, score, domain, evidence, message, agentDid: newDid, agentName: newName, }) => {
854
- const root = getWorkspaceRoot();
855
- const repDir = join(root, REPUTATION_DIR);
856
- await mkdir(repDir, { recursive: true });
857
- const filePath = join(repDir, `${slug}.md`);
858
- const sourceDid = await getAgentDid(root);
859
- const now = new Date();
860
- const timestamp = now.toISOString();
861
- const ALPHA = 0.15;
862
- const signal = { source: sourceDid, dimension: dim, score, timestamp };
863
- if (domain)
864
- signal.domain = domain;
865
- if (evidence)
866
- signal.evidence = evidence;
867
- if (message)
868
- signal.message = message;
869
- const updateDim = (existing, signalScore) => {
870
- if (!existing) {
871
- return {
872
- score: signalScore,
873
- confidence: Math.round((1 - 1 / (1 + 1 * 0.1)) * 100) / 100,
874
- sampleSize: 1,
875
- lastSignal: timestamp,
876
- };
877
- }
878
- // Apply decay then EWMA
879
- const MS_PER_MONTH = 30.44 * 24 * 60 * 60 * 1000;
880
- const months = (now.getTime() - new Date(existing.lastSignal).getTime()) / MS_PER_MONTH;
881
- const decayFactor = months > 0 ? Math.exp(-0.02 * months) : 1;
882
- const decayed = 0.5 + (existing.score - 0.5) * decayFactor;
883
- const newScore = ALPHA * signalScore + (1 - ALPHA) * decayed;
884
- const newSampleSize = existing.sampleSize + 1;
885
- return {
886
- score: Math.round(newScore * 1000) / 1000,
887
- confidence: Math.round((1 - 1 / (1 + newSampleSize * 0.1)) * 100) / 100,
888
- sampleSize: newSampleSize,
889
- lastSignal: timestamp,
890
- };
891
- };
892
- let isNew = false;
893
- let fileData;
894
- try {
895
- const raw = await readFile(filePath, "utf-8");
896
- fileData = matter(raw);
897
- }
898
- catch {
899
- // New profile
900
- if (!newDid || !newName) {
901
- return {
902
- content: [
903
- {
904
- type: "text",
905
- text: "Error: agentDid and agentName required for new profiles.",
906
- },
907
- ],
908
- isError: true,
909
- };
910
- }
911
- isNew = true;
912
- fileData = {
913
- data: {
914
- awp: AWP_VERSION,
915
- rdp: RDP_VERSION,
916
- type: "reputation-profile",
917
- id: `reputation:${slug}`,
918
- agentDid: newDid,
919
- agentName: newName,
920
- lastUpdated: timestamp,
921
- dimensions: {},
922
- domainCompetence: {},
923
- signals: [],
924
- },
925
- content: `\n# ${newName} — Reputation Profile\n\nTracked since ${timestamp.split("T")[0]}.\n`,
926
- };
927
- }
928
- fileData.data.lastUpdated = timestamp;
929
- fileData.data.signals.push(signal);
930
- if (!fileData.data.dimensions)
931
- fileData.data.dimensions = {};
932
- if (!fileData.data.domainCompetence)
933
- fileData.data.domainCompetence = {};
934
- if (dim === "domain-competence" && domain) {
935
- fileData.data.domainCompetence[domain] = updateDim(fileData.data.domainCompetence[domain], score);
936
- }
937
- else {
938
- fileData.data.dimensions[dim] = updateDim(fileData.data.dimensions[dim], score);
939
- }
940
- const output = matter.stringify(fileData.content, fileData.data);
941
- await writeFile(filePath, output, "utf-8");
942
- return {
943
- content: [
944
- {
945
- type: "text",
946
- text: `${isNew ? "Created" : "Updated"} reputation/${slug}.md — ${dim}${domain ? `:${domain}` : ""}: ${score}`,
947
- },
948
- ],
949
- };
950
- });
951
- // --- Tool: awp_contract_create ---
952
- server.registerTool("awp_contract_create", {
953
- title: "Create Delegation Contract",
954
- description: "Create a new delegation contract between agents with task definition and evaluation criteria.",
955
- inputSchema: {
956
- slug: z.string().describe("Contract slug"),
957
- delegate: z.string().describe("Delegate agent DID"),
958
- delegateSlug: z.string().describe("Delegate reputation profile slug"),
959
- description: z.string().describe("Task description"),
960
- deadline: z.string().optional().describe("Deadline (ISO 8601)"),
961
- outputFormat: z.string().optional().describe("Expected output type"),
962
- outputSlug: z.string().optional().describe("Expected output artifact slug"),
963
- criteria: z
964
- .record(z.string(), z.number())
965
- .optional()
966
- .describe("Evaluation criteria weights (default: completeness:0.3, accuracy:0.4, clarity:0.2, timeliness:0.1)"),
967
- },
968
- }, async ({ slug, delegate, delegateSlug, description, deadline, outputFormat, outputSlug, criteria, }) => {
969
- const root = getWorkspaceRoot();
970
- const conDir = join(root, CONTRACTS_DIR);
971
- await mkdir(conDir, { recursive: true });
972
- const filePath = join(conDir, `${slug}.md`);
973
- const delegatorDid = await getAgentDid(root);
974
- const now = new Date().toISOString();
975
- const evalCriteria = criteria || {
976
- completeness: 0.3,
977
- accuracy: 0.4,
978
- clarity: 0.2,
979
- timeliness: 0.1,
980
- };
981
- const data = {
982
- awp: AWP_VERSION,
983
- rdp: RDP_VERSION,
984
- type: "delegation-contract",
985
- id: `contract:${slug}`,
986
- status: "active",
987
- delegator: delegatorDid,
988
- delegate,
989
- delegateSlug,
990
- created: now,
991
- task: { description },
992
- evaluation: { criteria: evalCriteria, result: null },
993
- };
994
- if (deadline)
995
- data.deadline = deadline;
996
- if (outputFormat)
997
- data.task.outputFormat = outputFormat;
998
- if (outputSlug)
999
- data.task.outputSlug = outputSlug;
1000
- const body = `\n# ${slug} — Delegation Contract\n\nDelegated to ${delegateSlug}: ${description}\n\n## Status\nActive — awaiting completion.\n`;
1001
- const output = matter.stringify(body, data);
1002
- await writeFile(filePath, output, "utf-8");
1003
- return {
1004
- content: [
1005
- {
1006
- type: "text",
1007
- text: `Created contracts/${slug}.md (status: active)`,
1008
- },
1009
- ],
1010
- };
1011
- });
1012
- // --- Tool: awp_contract_evaluate ---
1013
- server.registerTool("awp_contract_evaluate", {
1014
- title: "Evaluate Delegation Contract",
1015
- description: "Evaluate a completed contract with scores for each criterion. Generates reputation signals for the delegate automatically.",
1016
- inputSchema: {
1017
- slug: z.string().describe("Contract slug"),
1018
- scores: z
1019
- .record(z.string(), z.number().min(0).max(1))
1020
- .describe("Map of criterion name to score (0.0-1.0)"),
1021
- },
1022
- }, async ({ slug, scores }) => {
1023
- const root = getWorkspaceRoot();
1024
- const filePath = join(root, CONTRACTS_DIR, `${slug}.md`);
1025
- let fileData;
1026
- try {
1027
- const raw = await readFile(filePath, "utf-8");
1028
- fileData = matter(raw);
1029
- }
1030
- catch {
1031
- return {
1032
- content: [{ type: "text", text: `Contract "${slug}" not found.` }],
1033
- isError: true,
1034
- };
1035
- }
1036
- if (fileData.data.status === "evaluated") {
1037
- return {
1038
- content: [{ type: "text", text: "Contract has already been evaluated." }],
1039
- isError: true,
1040
- };
1041
- }
1042
- const criteria = fileData.data.evaluation.criteria;
1043
- const scoreMap = scores;
1044
- let weightedScore = 0;
1045
- for (const [name, weight] of Object.entries(criteria)) {
1046
- if (scoreMap[name] === undefined) {
1047
- return {
1048
- content: [{ type: "text", text: `Missing score for criterion: ${name}` }],
1049
- isError: true,
1050
- };
1051
- }
1052
- weightedScore += weight * scoreMap[name];
1053
- }
1054
- weightedScore = Math.round(weightedScore * 1000) / 1000;
1055
- // Update contract
1056
- fileData.data.status = "evaluated";
1057
- fileData.data.evaluation.result = scores;
1058
- const contractOutput = matter.stringify(fileData.content, fileData.data);
1059
- await writeFile(filePath, contractOutput, "utf-8");
1060
- // Generate reputation signal for delegate
1061
- const evaluatorDid = await getAgentDid(root);
1062
- const delegateSlug = fileData.data.delegateSlug;
1063
- const now = new Date();
1064
- const timestamp = now.toISOString();
1065
- const signal = {
1066
- source: evaluatorDid,
1067
- dimension: "reliability",
1068
- score: weightedScore,
1069
- timestamp,
1070
- evidence: fileData.data.id,
1071
- message: `Contract evaluation: ${fileData.data.task.description}`,
1072
- };
1073
- // Try to update delegate's reputation profile
1074
- const repPath = join(root, REPUTATION_DIR, `${delegateSlug}.md`);
1075
- let repUpdated = false;
1076
- try {
1077
- const repRaw = await readFile(repPath, "utf-8");
1078
- const repData = matter(repRaw);
1079
- repData.data.lastUpdated = timestamp;
1080
- repData.data.signals.push(signal);
1081
- if (!repData.data.dimensions)
1082
- repData.data.dimensions = {};
1083
- const existing = repData.data.dimensions.reliability;
1084
- const ALPHA = 0.15;
1085
- const MS_PER_MONTH = 30.44 * 24 * 60 * 60 * 1000;
1086
- if (existing) {
1087
- const months = (now.getTime() - new Date(existing.lastSignal).getTime()) / MS_PER_MONTH;
1088
- const decayFactor = months > 0 ? Math.exp(-0.02 * months) : 1;
1089
- const decayed = 0.5 + (existing.score - 0.5) * decayFactor;
1090
- const newScore = ALPHA * weightedScore + (1 - ALPHA) * decayed;
1091
- const newSampleSize = existing.sampleSize + 1;
1092
- repData.data.dimensions.reliability = {
1093
- score: Math.round(newScore * 1000) / 1000,
1094
- confidence: Math.round((1 - 1 / (1 + newSampleSize * 0.1)) * 100) / 100,
1095
- sampleSize: newSampleSize,
1096
- lastSignal: timestamp,
1097
- };
1098
- }
1099
- else {
1100
- repData.data.dimensions.reliability = {
1101
- score: weightedScore,
1102
- confidence: Math.round((1 - 1 / (1 + 1 * 0.1)) * 100) / 100,
1103
- sampleSize: 1,
1104
- lastSignal: timestamp,
1105
- };
1106
- }
1107
- const repOutput = matter.stringify(repData.content, repData.data);
1108
- await writeFile(repPath, repOutput, "utf-8");
1109
- repUpdated = true;
1110
- }
1111
- catch {
1112
- // No profile — that's OK
1113
- }
1114
- const resultText = [
1115
- `Evaluated contracts/${slug}.md — weighted score: ${weightedScore}`,
1116
- repUpdated
1117
- ? `Updated reputation/${delegateSlug}.md with reliability signal`
1118
- : `Note: No reputation profile for ${delegateSlug} — signal not recorded`,
1119
- ].join("\n");
1120
- return {
1121
- content: [{ type: "text", text: resultText }],
1122
- };
1123
- });
1124
- // --- Tool: awp_project_create ---
1125
- server.registerTool("awp_project_create", {
1126
- title: "Create Project",
1127
- description: "Create a new coordination project with member roles and optional reputation gates.",
1128
- inputSchema: {
1129
- slug: z.string().describe("Project slug (e.g., 'q3-product-launch')"),
1130
- title: z.string().optional().describe("Project title"),
1131
- deadline: z.string().optional().describe("Deadline (ISO 8601 or YYYY-MM-DD)"),
1132
- tags: z.array(z.string()).optional().describe("Classification tags"),
1133
- },
1134
- }, async ({ slug, title, deadline, tags }) => {
1135
- const root = getWorkspaceRoot();
1136
- const projDir = join(root, PROJECTS_DIR);
1137
- await mkdir(projDir, { recursive: true });
1138
- const filePath = join(projDir, `${slug}.md`);
1139
- if (await fileExists(filePath)) {
1140
- return {
1141
- content: [{ type: "text", text: `Project "${slug}" already exists.` }],
1142
- isError: true,
1143
- };
1144
- }
1145
- const did = await getAgentDid(root);
1146
- const now = new Date().toISOString();
1147
- const projectTitle = title ||
1148
- slug
1149
- .split("-")
1150
- .map((w) => w.charAt(0).toUpperCase() + w.slice(1))
1151
- .join(" ");
1152
- const data = {
1153
- awp: AWP_VERSION,
1154
- cdp: CDP_VERSION,
1155
- type: "project",
1156
- id: `project:${slug}`,
1157
- title: projectTitle,
1158
- status: "active",
1159
- owner: did,
1160
- created: now,
1161
- members: [{ did, role: "lead", slug: "self" }],
1162
- taskCount: 0,
1163
- completedCount: 0,
1164
- };
1165
- if (deadline)
1166
- data.deadline = deadline;
1167
- if (tags?.length)
1168
- data.tags = tags;
1169
- const body = `\n# ${projectTitle}\n\n`;
1170
- const output = matter.stringify(body, data);
1171
- await writeFile(filePath, output, "utf-8");
1172
- return {
1173
- content: [{ type: "text", text: `Created projects/${slug}.md (status: active)` }],
1174
- };
1175
- });
1176
- // --- Tool: awp_project_list ---
1177
- server.registerTool("awp_project_list", {
1178
- title: "List Projects",
1179
- description: "List all projects in the workspace with status and task progress.",
1180
- inputSchema: {
1181
- status: z
1182
- .string()
1183
- .optional()
1184
- .describe("Filter by status (draft, active, paused, completed, archived)"),
1185
- },
1186
- }, async ({ status: statusFilter }) => {
1187
- const root = getWorkspaceRoot();
1188
- const projDir = join(root, PROJECTS_DIR);
1189
- let files;
1190
- try {
1191
- files = await readdir(projDir);
1192
- }
1193
- catch {
1194
- return {
1195
- content: [{ type: "text", text: JSON.stringify({ projects: [] }, null, 2) }],
1196
- };
1197
- }
1198
- const projects = [];
1199
- for (const f of files.filter((f) => f.endsWith(".md")).sort()) {
1200
- try {
1201
- const raw = await readFile(join(projDir, f), "utf-8");
1202
- const { data } = matter(raw);
1203
- if (data.type !== "project")
1204
- continue;
1205
- if (statusFilter && data.status !== statusFilter)
1206
- continue;
1207
- projects.push({
1208
- slug: f.replace(/\.md$/, ""),
1209
- title: data.title,
1210
- status: data.status,
1211
- taskCount: data.taskCount || 0,
1212
- completedCount: data.completedCount || 0,
1213
- deadline: data.deadline,
1214
- owner: data.owner,
1215
- memberCount: data.members?.length || 0,
1216
- });
1217
- }
1218
- catch {
1219
- /* skip */
1220
- }
1221
- }
1222
- return {
1223
- content: [{ type: "text", text: JSON.stringify({ projects }, null, 2) }],
1224
- };
1225
- });
1226
- // --- Tool: awp_project_status ---
1227
- server.registerTool("awp_project_status", {
1228
- title: "Project Status",
1229
- description: "Get detailed project status including members, tasks, and progress.",
1230
- inputSchema: {
1231
- slug: z.string().describe("Project slug"),
1232
- },
1233
- }, async ({ slug }) => {
1234
- const root = getWorkspaceRoot();
1235
- const filePath = join(root, PROJECTS_DIR, `${slug}.md`);
1236
- try {
1237
- const raw = await readFile(filePath, "utf-8");
1238
- const { data, content } = matter(raw);
1239
- // Load tasks
1240
- const tasks = [];
1241
- try {
1242
- const taskDir = join(root, PROJECTS_DIR, slug, "tasks");
1243
- const taskFiles = await readdir(taskDir);
1244
- for (const tf of taskFiles.filter((t) => t.endsWith(".md")).sort()) {
1245
- try {
1246
- const tRaw = await readFile(join(taskDir, tf), "utf-8");
1247
- const { data: tData } = matter(tRaw);
1248
- tasks.push({
1249
- slug: tf.replace(/\.md$/, ""),
1250
- title: tData.title,
1251
- status: tData.status,
1252
- assigneeSlug: tData.assigneeSlug,
1253
- priority: tData.priority,
1254
- deadline: tData.deadline,
1255
- blockedBy: tData.blockedBy || [],
1256
- });
1257
- }
1258
- catch {
1259
- /* skip */
1260
- }
1261
- }
1262
- }
1263
- catch {
1264
- /* no tasks */
1265
- }
1266
- return {
1267
- content: [
1268
- {
1269
- type: "text",
1270
- text: JSON.stringify({ frontmatter: data, body: content.trim(), tasks }, null, 2),
1271
- },
1272
- ],
1273
- };
1274
- }
1275
- catch {
1276
- return {
1277
- content: [{ type: "text", text: `Project "${slug}" not found.` }],
1278
- isError: true,
1279
- };
1280
- }
1281
- });
1282
- // --- Tool: awp_task_create ---
1283
- server.registerTool("awp_task_create", {
1284
- title: "Create Task",
1285
- description: "Create a new task within a project.",
1286
- inputSchema: {
1287
- projectSlug: z.string().describe("Project slug"),
1288
- taskSlug: z.string().describe("Task slug"),
1289
- title: z.string().optional().describe("Task title"),
1290
- assignee: z.string().optional().describe("Assignee agent DID"),
1291
- assigneeSlug: z.string().optional().describe("Assignee reputation profile slug"),
1292
- priority: z.string().optional().describe("Priority (low, medium, high, critical)"),
1293
- deadline: z.string().optional().describe("Deadline (ISO 8601 or YYYY-MM-DD)"),
1294
- blockedBy: z.array(z.string()).optional().describe("Task IDs that block this task"),
1295
- outputArtifact: z.string().optional().describe("Output artifact slug"),
1296
- contractSlug: z.string().optional().describe("Associated contract slug"),
1297
- tags: z.array(z.string()).optional().describe("Tags"),
1298
- },
1299
- }, async ({ projectSlug, taskSlug, title, assignee, assigneeSlug, priority, deadline, blockedBy, outputArtifact, contractSlug, tags, }) => {
1300
- const root = getWorkspaceRoot();
1301
- // Check project exists
1302
- const projPath = join(root, PROJECTS_DIR, `${projectSlug}.md`);
1303
- let projData;
1304
- try {
1305
- const raw = await readFile(projPath, "utf-8");
1306
- projData = matter(raw);
1307
- }
1308
- catch {
1309
- return {
1310
- content: [{ type: "text", text: `Project "${projectSlug}" not found.` }],
1311
- isError: true,
1312
- };
1313
- }
1314
- const taskDir = join(root, PROJECTS_DIR, projectSlug, "tasks");
1315
- await mkdir(taskDir, { recursive: true });
1316
- const taskPath = join(taskDir, `${taskSlug}.md`);
1317
- if (await fileExists(taskPath)) {
1318
- return {
1319
- content: [
1320
- {
1321
- type: "text",
1322
- text: `Task "${taskSlug}" already exists in project "${projectSlug}".`,
1323
- },
1324
- ],
1325
- isError: true,
1326
- };
1327
- }
1328
- const did = await getAgentDid(root);
1329
- const now = new Date().toISOString();
1330
- const taskTitle = title ||
1331
- taskSlug
1332
- .split("-")
1333
- .map((w) => w.charAt(0).toUpperCase() + w.slice(1))
1334
- .join(" ");
1335
- const data = {
1336
- awp: AWP_VERSION,
1337
- cdp: CDP_VERSION,
1338
- type: "task",
1339
- id: `task:${projectSlug}/${taskSlug}`,
1340
- projectId: `project:${projectSlug}`,
1341
- title: taskTitle,
1342
- status: "pending",
1343
- priority: priority || "medium",
1344
- created: now,
1345
- blockedBy: blockedBy || [],
1346
- blocks: [],
1347
- lastModified: now,
1348
- modifiedBy: did,
1349
- };
1350
- if (assignee)
1351
- data.assignee = assignee;
1352
- if (assigneeSlug)
1353
- data.assigneeSlug = assigneeSlug;
1354
- if (deadline)
1355
- data.deadline = deadline;
1356
- if (outputArtifact)
1357
- data.outputArtifact = outputArtifact;
1358
- if (contractSlug)
1359
- data.contractSlug = contractSlug;
1360
- if (tags?.length)
1361
- data.tags = tags;
1362
- const body = `\n# ${taskTitle}\n\n`;
1363
- const output = matter.stringify(body, data);
1364
- await writeFile(taskPath, output, "utf-8");
1365
- // Update project counts
1366
- projData.data.taskCount = (projData.data.taskCount || 0) + 1;
1367
- const projOutput = matter.stringify(projData.content, projData.data);
1368
- await writeFile(projPath, projOutput, "utf-8");
1369
- return {
1370
- content: [
1371
- {
1372
- type: "text",
1373
- text: `Created task "${taskSlug}" in project "${projectSlug}" (status: pending)`,
1374
- },
1375
- ],
1376
- };
1377
- });
1378
- // --- Tool: awp_task_update ---
1379
- server.registerTool("awp_task_update", {
1380
- title: "Update Task",
1381
- description: "Update a task's status, assignee, or other fields.",
1382
- inputSchema: {
1383
- projectSlug: z.string().describe("Project slug"),
1384
- taskSlug: z.string().describe("Task slug"),
1385
- status: z
1386
- .string()
1387
- .optional()
1388
- .describe("New status (pending, in-progress, blocked, review, completed, cancelled)"),
1389
- assignee: z.string().optional().describe("New assignee DID"),
1390
- assigneeSlug: z.string().optional().describe("New assignee reputation slug"),
1391
- },
1392
- }, async ({ projectSlug, taskSlug, status: newStatus, assignee, assigneeSlug }) => {
1393
- const root = getWorkspaceRoot();
1394
- const taskPath = join(root, PROJECTS_DIR, projectSlug, "tasks", `${taskSlug}.md`);
1395
- let taskData;
1396
- try {
1397
- const raw = await readFile(taskPath, "utf-8");
1398
- taskData = matter(raw);
1399
- }
1400
- catch {
1401
- return {
1402
- content: [
1403
- {
1404
- type: "text",
1405
- text: `Task "${taskSlug}" not found in project "${projectSlug}".`,
1406
- },
1407
- ],
1408
- isError: true,
1409
- };
1410
- }
1411
- const did = await getAgentDid(root);
1412
- const now = new Date().toISOString();
1413
- const changes = [];
1414
- if (newStatus) {
1415
- taskData.data.status = newStatus;
1416
- changes.push(`status → ${newStatus}`);
1417
- }
1418
- if (assignee) {
1419
- taskData.data.assignee = assignee;
1420
- changes.push(`assignee → ${assignee}`);
1421
- }
1422
- if (assigneeSlug) {
1423
- taskData.data.assigneeSlug = assigneeSlug;
1424
- changes.push(`assigneeSlug → ${assigneeSlug}`);
1425
- }
1426
- taskData.data.lastModified = now;
1427
- taskData.data.modifiedBy = did;
1428
- const output = matter.stringify(taskData.content, taskData.data);
1429
- await writeFile(taskPath, output, "utf-8");
1430
- // Update project counts if status changed
1431
- if (newStatus) {
1432
- const projPath = join(root, PROJECTS_DIR, `${projectSlug}.md`);
1433
- try {
1434
- const projRaw = await readFile(projPath, "utf-8");
1435
- const projData = matter(projRaw);
1436
- // Recount completed tasks
1437
- const taskDir = join(root, PROJECTS_DIR, projectSlug, "tasks");
1438
- let taskCount = 0;
1439
- let completedCount = 0;
1440
- try {
1441
- const taskFiles = await readdir(taskDir);
1442
- for (const tf of taskFiles.filter((t) => t.endsWith(".md"))) {
1443
- try {
1444
- const tRaw = await readFile(join(taskDir, tf), "utf-8");
1445
- const { data: tData } = matter(tRaw);
1446
- if (tData.type === "task") {
1447
- taskCount++;
1448
- if (tData.status === "completed")
1449
- completedCount++;
1450
- }
1451
- }
1452
- catch {
1453
- /* skip */
1454
- }
1455
- }
1456
- }
1457
- catch {
1458
- /* no tasks */
1459
- }
1460
- projData.data.taskCount = taskCount;
1461
- projData.data.completedCount = completedCount;
1462
- const projOutput = matter.stringify(projData.content, projData.data);
1463
- await writeFile(projPath, projOutput, "utf-8");
1464
- }
1465
- catch {
1466
- /* project not found */
1467
- }
1468
- }
1469
- return {
1470
- content: [
1471
- { type: "text", text: `Updated task "${taskSlug}": ${changes.join(", ")}` },
1472
- ],
1473
- };
1474
- });
1475
- // --- Tool: awp_task_list ---
1476
- server.registerTool("awp_task_list", {
1477
- title: "List Tasks",
1478
- description: "List all tasks for a project with optional status and assignee filters.",
1479
- inputSchema: {
1480
- projectSlug: z.string().describe("Project slug"),
1481
- status: z.string().optional().describe("Filter by status"),
1482
- assigneeSlug: z.string().optional().describe("Filter by assignee slug"),
1483
- },
1484
- }, async ({ projectSlug, status: statusFilter, assigneeSlug }) => {
1485
- const root = getWorkspaceRoot();
1486
- const taskDir = join(root, PROJECTS_DIR, projectSlug, "tasks");
1487
- let files;
1488
- try {
1489
- files = await readdir(taskDir);
1490
- }
1491
- catch {
1492
- return {
1493
- content: [{ type: "text", text: JSON.stringify({ tasks: [] }, null, 2) }],
1494
- };
1495
- }
1496
- const tasks = [];
1497
- for (const f of files.filter((f) => f.endsWith(".md")).sort()) {
1498
- try {
1499
- const raw = await readFile(join(taskDir, f), "utf-8");
1500
- const { data } = matter(raw);
1501
- if (data.type !== "task")
1502
- continue;
1503
- if (statusFilter && data.status !== statusFilter)
1504
- continue;
1505
- if (assigneeSlug && data.assigneeSlug !== assigneeSlug)
1506
- continue;
1507
- tasks.push({
1508
- slug: f.replace(/\.md$/, ""),
1509
- title: data.title,
1510
- status: data.status,
1511
- assigneeSlug: data.assigneeSlug,
1512
- priority: data.priority,
1513
- deadline: data.deadline,
1514
- blockedBy: data.blockedBy || [],
1515
- blocks: data.blocks || [],
1516
- });
1517
- }
1518
- catch {
1519
- /* skip */
1520
- }
1521
- }
1522
- return {
1523
- content: [{ type: "text", text: JSON.stringify({ tasks }, null, 2) }],
1524
- };
1525
- });
1526
- // --- Tool: awp_artifact_merge ---
1527
- server.registerTool("awp_artifact_merge", {
1528
- title: "Merge Artifacts",
1529
- description: "Merge a source artifact into a target artifact. Supports 'additive' (append) and 'authority' (reputation-based ordering) strategies.",
1530
- inputSchema: {
1531
- targetSlug: z.string().describe("Target artifact slug"),
1532
- sourceSlug: z.string().describe("Source artifact slug"),
1533
- strategy: z
1534
- .string()
1535
- .optional()
1536
- .describe("Merge strategy: 'additive' (default) or 'authority'"),
1537
- message: z.string().optional().describe("Merge message"),
1538
- },
1539
- }, async ({ targetSlug, sourceSlug, strategy: strat, message }) => {
1540
- const root = getWorkspaceRoot();
1541
- const strategy = strat || "additive";
1542
- if (strategy !== "additive" && strategy !== "authority") {
1543
- return {
1544
- content: [
1545
- {
1546
- type: "text",
1547
- text: `Unknown strategy "${strategy}". Use "additive" or "authority".`,
1548
- },
1549
- ],
1550
- isError: true,
1551
- };
1552
- }
1553
- let targetRaw, sourceRaw;
1554
- try {
1555
- targetRaw = await readFile(join(root, ARTIFACTS_DIR, `${targetSlug}.md`), "utf-8");
1556
- }
1557
- catch {
1558
- return {
1559
- content: [{ type: "text", text: `Target artifact "${targetSlug}" not found.` }],
1560
- isError: true,
1561
- };
1562
- }
1563
- try {
1564
- sourceRaw = await readFile(join(root, ARTIFACTS_DIR, `${sourceSlug}.md`), "utf-8");
1565
- }
1566
- catch {
1567
- return {
1568
- content: [{ type: "text", text: `Source artifact "${sourceSlug}" not found.` }],
1569
- isError: true,
1570
- };
1571
- }
1572
- const target = matter(targetRaw);
1573
- const source = matter(sourceRaw);
1574
- const did = await getAgentDid(root);
1575
- const now = new Date();
1576
- const nowIso = now.toISOString();
1577
- const tfm = target.data;
1578
- const sfm = source.data;
1579
- if (strategy === "authority") {
1580
- // Authority merge using reputation
1581
- const sharedTags = (tfm.tags || []).filter((t) => (sfm.tags || []).includes(t));
1582
- const targetAuthor = tfm.authors?.[0] || "anonymous";
1583
- const sourceAuthor = sfm.authors?.[0] || "anonymous";
1584
- // Look up reputation scores
1585
- const getScore = async (authorDid) => {
1586
- const repDir = join(root, REPUTATION_DIR);
1587
- try {
1588
- const repFiles = await readdir(repDir);
1589
- for (const f of repFiles.filter((f) => f.endsWith(".md"))) {
1590
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
1591
- let data;
1592
- try {
1593
- const raw = await readFile(join(repDir, f), "utf-8");
1594
- ({ data } = matter(raw));
1595
- }
1596
- catch {
1597
- continue; // skip corrupted reputation files
1598
- }
1599
- if (data.agentDid !== authorDid)
1600
- continue;
1601
- const MS_PER_MONTH = 30.44 * 24 * 60 * 60 * 1000;
1602
- let best = 0;
1603
- // Check domain scores for shared tags
1604
- for (const tag of sharedTags) {
1605
- const dim = data.domainCompetence?.[tag];
1606
- if (dim) {
1607
- const months = (now.getTime() - new Date(dim.lastSignal).getTime()) / MS_PER_MONTH;
1608
- const factor = months > 0 ? Math.exp(-0.02 * months) : 1;
1609
- const decayed = 0.5 + (dim.score - 0.5) * factor;
1610
- if (decayed > best)
1611
- best = decayed;
1612
- }
1613
- }
1614
- // Fallback to reliability
1615
- if (best === 0 && data.dimensions?.reliability) {
1616
- const dim = data.dimensions.reliability;
1617
- const months = (now.getTime() - new Date(dim.lastSignal).getTime()) / MS_PER_MONTH;
1618
- const factor = months > 0 ? Math.exp(-0.02 * months) : 1;
1619
- best = 0.5 + (dim.score - 0.5) * factor;
1620
- }
1621
- return best;
1622
- }
1623
- }
1624
- catch {
1625
- /* no reputation */
1626
- }
1627
- return 0;
1628
- };
1629
- const targetScore = await getScore(targetAuthor);
1630
- const sourceScore = await getScore(sourceAuthor);
1631
- const targetIsHigher = targetScore >= sourceScore;
1632
- const higherBody = targetIsHigher ? target.content.trim() : source.content.trim();
1633
- const lowerBody = targetIsHigher ? source.content.trim() : target.content.trim();
1634
- const lowerAuthor = targetIsHigher ? sourceAuthor : targetAuthor;
1635
- const lowerScore = targetIsHigher ? sourceScore : targetScore;
1636
- const higherScore = targetIsHigher ? targetScore : sourceScore;
1637
- target.content = `\n${higherBody}\n\n---\n*Authority merge: content below from ${lowerAuthor} (authority score: ${lowerScore.toFixed(2)} vs ${higherScore.toFixed(2)})*\n\n${lowerBody}\n`;
1638
- }
1639
- else {
1640
- // Additive merge
1641
- const separator = `\n---\n*Merged from ${sfm.id} (version ${sfm.version}) on ${nowIso}*\n\n`;
1642
- target.content += separator + source.content.trim() + "\n";
1643
- }
1644
- // Union authors
1645
- for (const author of sfm.authors || []) {
1646
- if (!tfm.authors?.includes(author)) {
1647
- if (!tfm.authors)
1648
- tfm.authors = [];
1649
- tfm.authors.push(author);
1650
- }
1651
- }
1652
- if (!tfm.authors?.includes(did)) {
1653
- if (!tfm.authors)
1654
- tfm.authors = [];
1655
- tfm.authors.push(did);
1656
- }
1657
- // Union tags
1658
- if (sfm.tags) {
1659
- if (!tfm.tags)
1660
- tfm.tags = [];
1661
- for (const tag of sfm.tags) {
1662
- if (!tfm.tags.includes(tag))
1663
- tfm.tags.push(tag);
1664
- }
1665
- }
1666
- // Confidence: minimum
1667
- if (tfm.confidence !== undefined && sfm.confidence !== undefined) {
1668
- tfm.confidence = Math.min(tfm.confidence, sfm.confidence);
1669
- }
1670
- else if (sfm.confidence !== undefined) {
1671
- tfm.confidence = sfm.confidence;
1672
- }
1673
- // Bump version + provenance
1674
- tfm.version = (tfm.version || 1) + 1;
1675
- tfm.lastModified = nowIso;
1676
- tfm.modifiedBy = did;
1677
- if (!tfm.provenance)
1678
- tfm.provenance = [];
1679
- tfm.provenance.push({
1680
- agent: did,
1681
- action: "merged",
1682
- timestamp: nowIso,
1683
- message: message || `Merged from ${sfm.id} (version ${sfm.version}, strategy: ${strategy})`,
1684
- confidence: tfm.confidence,
1685
- });
1686
- const output = matter.stringify(target.content, tfm);
1687
- await writeFile(join(root, ARTIFACTS_DIR, `${targetSlug}.md`), output, "utf-8");
1688
- return {
1689
- content: [
1690
- {
1691
- type: "text",
1692
- text: `Merged ${sfm.id} into ${tfm.id} (now version ${tfm.version}, strategy: ${strategy})`,
1693
- },
1694
- ],
1695
- };
1696
- });
1697
- // --- Tool: awp_read_heartbeat ---
1698
- server.registerTool("awp_read_heartbeat", {
1699
- title: "Read Heartbeat Config",
1700
- description: "Read the agent's heartbeat configuration (HEARTBEAT.md)",
1701
- inputSchema: {},
1702
- }, async () => {
1703
- const root = getWorkspaceRoot();
1704
- const path = join(root, "HEARTBEAT.md");
1705
- try {
1706
- const raw = await readFile(path, "utf-8");
1707
- const { data, content } = matter(raw);
1708
- return {
1709
- content: [
1710
- {
1711
- type: "text",
1712
- text: JSON.stringify({ frontmatter: data, body: content.trim() }, null, 2),
1713
- },
1714
- ],
1715
- };
1716
- }
1717
- catch {
1718
- return {
1719
- content: [{ type: "text", text: "HEARTBEAT.md not found" }],
1720
- isError: true,
1721
- };
1722
- }
1723
- });
1724
- // --- Tool: awp_read_tools ---
1725
- server.registerTool("awp_read_tools", {
1726
- title: "Read Tools Config",
1727
- description: "Read the agent's tools configuration (TOOLS.md)",
1728
- inputSchema: {},
1729
- }, async () => {
1730
- const root = getWorkspaceRoot();
1731
- const path = join(root, "TOOLS.md");
1732
- try {
1733
- const raw = await readFile(path, "utf-8");
1734
- const { data, content } = matter(raw);
1735
- return {
1736
- content: [
1737
- {
1738
- type: "text",
1739
- text: JSON.stringify({ frontmatter: data, body: content.trim() }, null, 2),
1740
- },
1741
- ],
1742
- };
1743
- }
1744
- catch {
1745
- return {
1746
- content: [{ type: "text", text: "TOOLS.md not found" }],
1747
- isError: true,
1748
- };
1749
- }
1750
- });
1751
- // --- Tool: awp_read_agents ---
1752
- server.registerTool("awp_read_agents", {
1753
- title: "Read Operations/Agents Config",
1754
- description: "Read the agent's operations configuration (AGENTS.md)",
1755
- inputSchema: {},
1756
- }, async () => {
1757
- const root = getWorkspaceRoot();
1758
- const path = join(root, "AGENTS.md");
1759
- try {
1760
- const raw = await readFile(path, "utf-8");
1761
- const { data, content } = matter(raw);
1762
- return {
1763
- content: [
1764
- {
1765
- type: "text",
1766
- text: JSON.stringify({ frontmatter: data, body: content.trim() }, null, 2),
1767
- },
1768
- ],
1769
- };
1770
- }
1771
- catch {
1772
- return {
1773
- content: [{ type: "text", text: "AGENTS.md not found" }],
1774
- isError: true,
1775
- };
1776
- }
1777
- });
1778
- // --- Tool: awp_contract_list ---
1779
- server.registerTool("awp_contract_list", {
1780
- title: "List Delegation Contracts",
1781
- description: "List all delegation contracts with optional status filter",
1782
- inputSchema: {
1783
- status: z
1784
- .string()
1785
- .optional()
1786
- .describe("Filter by status (active, completed, evaluated, cancelled)"),
1787
- },
1788
- }, async ({ status: statusFilter }) => {
1789
- const root = getWorkspaceRoot();
1790
- const contractsDir = join(root, CONTRACTS_DIR);
1791
- let files;
1792
- try {
1793
- files = await readdir(contractsDir);
1794
- }
1795
- catch {
1796
- return {
1797
- content: [{ type: "text", text: "No contracts directory found." }],
1798
- };
1799
- }
1800
- const mdFiles = files.filter((f) => f.endsWith(".md")).sort();
1801
- const contracts = [];
1802
- for (const f of mdFiles) {
1803
- try {
1804
- const raw = await readFile(join(contractsDir, f), "utf-8");
1805
- const { data } = matter(raw);
1806
- if (data.type === "delegation-contract") {
1807
- if (!statusFilter || data.status === statusFilter) {
1808
- contracts.push({
1809
- slug: f.replace(".md", ""),
1810
- status: data.status || "unknown",
1811
- delegate: data.delegate || "unknown",
1812
- delegateSlug: data.delegateSlug || "unknown",
1813
- created: data.created || "unknown",
1814
- deadline: data.deadline,
1815
- });
1816
- }
1817
- }
1818
- }
1819
- catch {
1820
- // Skip unparseable files
1821
- }
1822
- }
1823
- if (contracts.length === 0) {
1824
- return {
1825
- content: [
1826
- {
1827
- type: "text",
1828
- text: statusFilter
1829
- ? `No contracts with status: ${statusFilter}`
1830
- : "No contracts found.",
1831
- },
1832
- ],
1833
- };
1834
- }
1835
- return {
1836
- content: [
1837
- {
1838
- type: "text",
1839
- text: JSON.stringify(contracts, null, 2),
1840
- },
1841
- ],
1842
- };
1843
- });
27
+ // Register all tool groups
28
+ registerIdentityTools(server);
29
+ registerMemoryTools(server);
30
+ registerArtifactTools(server);
31
+ registerStatusTools(server);
32
+ registerReputationTools(server);
33
+ registerContractTools(server);
34
+ registerProjectTools(server);
35
+ registerTaskTools(server);
36
+ registerConfigTools(server);
37
+ registerSwarmTools(server);
1844
38
  // Start the server
1845
39
  const transport = new StdioServerTransport();
1846
40
  await server.connect(transport);