@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/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 info, file presence, memory stats, artifact count",
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
- { type: "text", text: JSON.stringify(status, null, 2) },
1837
+ {
1838
+ type: "text",
1839
+ text: JSON.stringify(contracts, null, 2),
1840
+ },
573
1841
  ],
574
1842
  };
575
1843
  });