@agent-workspace/mcp-server 0.2.1 → 0.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +42 -5
- package/dist/index.d.ts +17 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1306 -38
- package/dist/index.js.map +1 -1
- package/package.json +7 -2
package/dist/index.js
CHANGED
|
@@ -2,10 +2,61 @@
|
|
|
2
2
|
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
3
3
|
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
4
4
|
import * as z from "zod";
|
|
5
|
-
import { readFile, writeFile, readdir, mkdir, access } from "node:fs/promises";
|
|
6
|
-
import { join } from "node:path";
|
|
5
|
+
import { readFile, writeFile, readdir, mkdir, access, stat } from "node:fs/promises";
|
|
6
|
+
import { join, resolve, relative, isAbsolute } from "node:path";
|
|
7
7
|
import matter from "gray-matter";
|
|
8
|
-
import { AWP_VERSION, SMP_VERSION, MEMORY_DIR, ARTIFACTS_DIR } from "@agent-workspace/core";
|
|
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
|
+
/**
|
|
50
|
+
* Read a file with size limit check.
|
|
51
|
+
* @internal Available for future use in tool handlers
|
|
52
|
+
*/
|
|
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
|
+
}
|
|
9
60
|
const server = new McpServer({
|
|
10
61
|
name: "awp-workspace",
|
|
11
62
|
version: AWP_VERSION,
|
|
@@ -146,9 +197,7 @@ server.registerTool("awp_read_memory", {
|
|
|
146
197
|
}
|
|
147
198
|
catch {
|
|
148
199
|
return {
|
|
149
|
-
content: [
|
|
150
|
-
{ type: "text", text: "No long-term memory file exists yet." },
|
|
151
|
-
],
|
|
200
|
+
content: [{ type: "text", text: "No long-term memory file exists yet." }],
|
|
152
201
|
};
|
|
153
202
|
}
|
|
154
203
|
}
|
|
@@ -171,9 +220,7 @@ server.registerTool("awp_read_memory", {
|
|
|
171
220
|
content: [
|
|
172
221
|
{
|
|
173
222
|
type: "text",
|
|
174
|
-
text: results.length
|
|
175
|
-
? results.join("\n\n")
|
|
176
|
-
: "No recent memory entries.",
|
|
223
|
+
text: results.length ? results.join("\n\n") : "No recent memory entries.",
|
|
177
224
|
},
|
|
178
225
|
],
|
|
179
226
|
};
|
|
@@ -200,9 +247,7 @@ server.registerTool("awp_read_memory", {
|
|
|
200
247
|
}
|
|
201
248
|
catch {
|
|
202
249
|
return {
|
|
203
|
-
content: [
|
|
204
|
-
{ type: "text", text: `No memory entry for ${target}.` },
|
|
205
|
-
],
|
|
250
|
+
content: [{ type: "text", text: `No memory entry for ${target}.` }],
|
|
206
251
|
};
|
|
207
252
|
}
|
|
208
253
|
});
|
|
@@ -212,10 +257,7 @@ server.registerTool("awp_write_memory", {
|
|
|
212
257
|
description: "Append an entry to today's memory log in the AWP workspace",
|
|
213
258
|
inputSchema: {
|
|
214
259
|
content: z.string().describe("The memory entry to log"),
|
|
215
|
-
tags: z
|
|
216
|
-
.array(z.string())
|
|
217
|
-
.optional()
|
|
218
|
-
.describe("Optional categorization tags"),
|
|
260
|
+
tags: z.array(z.string()).optional().describe("Optional categorization tags"),
|
|
219
261
|
},
|
|
220
262
|
}, async ({ content: entryContent, tags }) => {
|
|
221
263
|
const root = getWorkspaceRoot();
|
|
@@ -285,9 +327,7 @@ server.registerTool("awp_artifact_read", {
|
|
|
285
327
|
}
|
|
286
328
|
catch {
|
|
287
329
|
return {
|
|
288
|
-
content: [
|
|
289
|
-
{ type: "text", text: `Artifact "${slug}" not found.` },
|
|
290
|
-
],
|
|
330
|
+
content: [{ type: "text", text: `Artifact "${slug}" not found.` }],
|
|
291
331
|
isError: true,
|
|
292
332
|
};
|
|
293
333
|
}
|
|
@@ -301,12 +341,7 @@ server.registerTool("awp_artifact_write", {
|
|
|
301
341
|
title: z.string().optional().describe("Title (required for new artifacts)"),
|
|
302
342
|
content: z.string().describe("Markdown body content"),
|
|
303
343
|
tags: z.array(z.string()).optional().describe("Categorization tags"),
|
|
304
|
-
confidence: z
|
|
305
|
-
.number()
|
|
306
|
-
.min(0)
|
|
307
|
-
.max(1)
|
|
308
|
-
.optional()
|
|
309
|
-
.describe("Confidence score (0.0-1.0)"),
|
|
344
|
+
confidence: z.number().min(0).max(1).optional().describe("Confidence score (0.0-1.0)"),
|
|
310
345
|
message: z.string().optional().describe("Commit message for provenance"),
|
|
311
346
|
},
|
|
312
347
|
}, async ({ slug, title, content: bodyContent, tags, confidence, message }) => {
|
|
@@ -413,9 +448,7 @@ server.registerTool("awp_artifact_list", {
|
|
|
413
448
|
}
|
|
414
449
|
catch {
|
|
415
450
|
return {
|
|
416
|
-
content: [
|
|
417
|
-
{ type: "text", text: JSON.stringify({ artifacts: [] }, null, 2) },
|
|
418
|
-
],
|
|
451
|
+
content: [{ type: "text", text: JSON.stringify({ artifacts: [] }, null, 2) }],
|
|
419
452
|
};
|
|
420
453
|
}
|
|
421
454
|
const mdFiles = files.filter((f) => f.endsWith(".md")).sort();
|
|
@@ -469,9 +502,7 @@ server.registerTool("awp_artifact_search", {
|
|
|
469
502
|
}
|
|
470
503
|
catch {
|
|
471
504
|
return {
|
|
472
|
-
content: [
|
|
473
|
-
{ type: "text", text: JSON.stringify({ results: [] }, null, 2) },
|
|
474
|
-
],
|
|
505
|
+
content: [{ type: "text", text: JSON.stringify({ results: [] }, null, 2) }],
|
|
475
506
|
};
|
|
476
507
|
}
|
|
477
508
|
const mdFiles = files.filter((f) => f.endsWith(".md")).sort();
|
|
@@ -492,11 +523,7 @@ server.registerTool("awp_artifact_search", {
|
|
|
492
523
|
version: data.version,
|
|
493
524
|
confidence: data.confidence,
|
|
494
525
|
tags: data.tags,
|
|
495
|
-
matchedIn: [
|
|
496
|
-
titleMatch && "title",
|
|
497
|
-
tagMatch && "tags",
|
|
498
|
-
bodyMatch && "body",
|
|
499
|
-
].filter(Boolean),
|
|
526
|
+
matchedIn: [titleMatch && "title", tagMatch && "tags", bodyMatch && "body"].filter(Boolean),
|
|
500
527
|
});
|
|
501
528
|
}
|
|
502
529
|
}
|
|
@@ -516,7 +543,7 @@ server.registerTool("awp_artifact_search", {
|
|
|
516
543
|
// --- Tool: awp_workspace_status ---
|
|
517
544
|
server.registerTool("awp_workspace_status", {
|
|
518
545
|
title: "Workspace Status",
|
|
519
|
-
description: "Get AWP workspace health status — manifest
|
|
546
|
+
description: "Get AWP workspace health status — manifest, files, projects, tasks, reputation, contracts, artifacts, memory, health warnings",
|
|
520
547
|
inputSchema: {},
|
|
521
548
|
}, async () => {
|
|
522
549
|
const root = getWorkspaceRoot();
|
|
@@ -567,9 +594,1250 @@ server.registerTool("awp_workspace_status", {
|
|
|
567
594
|
catch {
|
|
568
595
|
status.artifacts = { count: 0 };
|
|
569
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
|
+
}
|
|
570
1835
|
return {
|
|
571
1836
|
content: [
|
|
572
|
-
{
|
|
1837
|
+
{
|
|
1838
|
+
type: "text",
|
|
1839
|
+
text: JSON.stringify(contracts, null, 2),
|
|
1840
|
+
},
|
|
573
1841
|
],
|
|
574
1842
|
};
|
|
575
1843
|
});
|