synomem 0.1.1 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (73) hide show
  1. package/ARCHITECTURE.md +2 -2
  2. package/CHANGELOG.md +37 -0
  3. package/README.md +26 -13
  4. package/dist/cli.d.ts.map +1 -1
  5. package/dist/cli.js +379 -51
  6. package/dist/cli.js.map +1 -1
  7. package/dist/client.d.ts +131 -17
  8. package/dist/client.d.ts.map +1 -1
  9. package/dist/client.js +489 -76
  10. package/dist/client.js.map +1 -1
  11. package/dist/config.d.ts +2 -2
  12. package/dist/config.js +8 -8
  13. package/dist/import.d.ts +356 -13
  14. package/dist/import.d.ts.map +1 -1
  15. package/dist/index.d.ts +2 -2
  16. package/dist/index.d.ts.map +1 -1
  17. package/dist/index.js +2 -2
  18. package/dist/index.js.map +1 -1
  19. package/dist/mcp/index.d.ts.map +1 -1
  20. package/dist/mcp/index.js +213 -31
  21. package/dist/mcp/index.js.map +1 -1
  22. package/dist/mcp-server.js +54 -8
  23. package/dist/mcp-server.js.map +1 -1
  24. package/dist/ports/repository.d.ts +21 -1
  25. package/dist/ports/repository.d.ts.map +1 -1
  26. package/dist/projections.d.ts +26 -1
  27. package/dist/projections.d.ts.map +1 -1
  28. package/dist/projections.js +208 -44
  29. package/dist/projections.js.map +1 -1
  30. package/dist/remote.d.ts +82 -9
  31. package/dist/remote.d.ts.map +1 -1
  32. package/dist/remote.js +53 -2
  33. package/dist/remote.js.map +1 -1
  34. package/dist/schemas.d.ts +476 -21
  35. package/dist/schemas.d.ts.map +1 -1
  36. package/dist/schemas.js +204 -26
  37. package/dist/schemas.js.map +1 -1
  38. package/dist/service.d.ts +82 -10
  39. package/dist/service.d.ts.map +1 -1
  40. package/dist/skill-install.d.ts +8 -2
  41. package/dist/skill-install.d.ts.map +1 -1
  42. package/dist/skill-install.js +6 -11
  43. package/dist/skill-install.js.map +1 -1
  44. package/dist/storage.d.ts +51 -1
  45. package/dist/storage.d.ts.map +1 -1
  46. package/dist/storage.js +366 -36
  47. package/dist/storage.js.map +1 -1
  48. package/dist/types.d.ts +310 -38
  49. package/dist/types.d.ts.map +1 -1
  50. package/docs/cli.md +53 -20
  51. package/docs/examples.md +4 -4
  52. package/docs/mcp.md +12 -4
  53. package/docs/skill.md +10 -7
  54. package/docs/storage-format.md +4 -4
  55. package/openapi/synomem-v1.yaml +24 -24
  56. package/package.json +1 -1
  57. package/skills/synomem/SKILL.md +24 -8
  58. package/skills/synomem/agents/openai.yaml +1 -1
  59. package/skills/synomem/references/examples.md +1 -1
  60. package/src/cli.ts +610 -95
  61. package/src/client.ts +587 -79
  62. package/src/config.ts +8 -8
  63. package/src/index.ts +11 -1
  64. package/src/mcp/index.ts +265 -30
  65. package/src/mcp-server.ts +58 -8
  66. package/src/ports/repository.ts +21 -0
  67. package/src/projections.ts +209 -44
  68. package/src/remote.ts +166 -8
  69. package/src/schemas.ts +211 -26
  70. package/src/service.ts +85 -7
  71. package/src/skill-install.ts +14 -17
  72. package/src/storage.ts +472 -34
  73. package/src/types.ts +332 -39
package/src/config.ts CHANGED
@@ -10,14 +10,14 @@ export const defaultConfig: SynomemConfig = {
10
10
  workspaceId: 'workspace',
11
11
  defaultVisibility: 'workspace',
12
12
  allowSelfAwards: false,
13
- allowCrossAgentTodos: true,
13
+ allowCrossAgentTasks: true,
14
14
  allowAgentCreationViaMcp: false,
15
15
  allowRebuildViaMcp: false,
16
16
  includePrivateInStats: false,
17
17
  projection: {
18
18
  writeWinsMarkdown: true,
19
19
  writeMemoryMarkdown: true,
20
- writeTodosMarkdown: true,
20
+ writeTasksMarkdown: true,
21
21
  writeInboxEntries: true,
22
22
  },
23
23
  };
@@ -53,14 +53,14 @@ const policySchema = z.object({
53
53
  workspaceId: z.string().trim().min(1).max(100),
54
54
  defaultVisibility: z.enum(['private', 'workspace', 'public']),
55
55
  allowSelfAwards: z.boolean(),
56
- allowCrossAgentTodos: z.boolean(),
56
+ allowCrossAgentTasks: z.boolean(),
57
57
  allowAgentCreationViaMcp: z.boolean(),
58
58
  allowRebuildViaMcp: z.boolean(),
59
59
  includePrivateInStats: z.boolean(),
60
60
  projection: z.object({
61
61
  writeWinsMarkdown: z.boolean(),
62
62
  writeMemoryMarkdown: z.boolean(),
63
- writeTodosMarkdown: z.boolean(),
63
+ writeTasksMarkdown: z.boolean(),
64
64
  writeInboxEntries: z.boolean(),
65
65
  }),
66
66
  });
@@ -144,24 +144,24 @@ function environmentConfig(env: NodeJS.ProcessEnv): SynomemConfigOverrides {
144
144
 
145
145
  const writeWinsMarkdown = optionalBoolean(env, 'SYNOMEM_WRITE_WINS_MARKDOWN');
146
146
  const writeMemoryMarkdown = optionalBoolean(env, 'SYNOMEM_WRITE_MEMORY_MARKDOWN');
147
- const writeTodosMarkdown = optionalBoolean(env, 'SYNOMEM_WRITE_TODOS_MARKDOWN');
147
+ const writeTasksMarkdown = optionalBoolean(env, 'SYNOMEM_WRITE_TASKS_MARKDOWN');
148
148
  const writeInboxEntries = optionalBoolean(env, 'SYNOMEM_WRITE_INBOX_ENTRIES');
149
149
  const allowSelfAwards = optionalBoolean(env, 'SYNOMEM_ALLOW_SELF_AWARDS');
150
- const allowCrossAgentTodos = optionalBoolean(env, 'SYNOMEM_ALLOW_CROSS_AGENT_TODOS');
150
+ const allowCrossAgentTasks = optionalBoolean(env, 'SYNOMEM_ALLOW_CROSS_AGENT_TODOS');
151
151
  const allowAgentCreationViaMcp = optionalBoolean(env, 'SYNOMEM_ALLOW_AGENT_CREATION_VIA_MCP');
152
152
  const allowRebuildViaMcp = optionalBoolean(env, 'SYNOMEM_ALLOW_REBUILD_VIA_MCP');
153
153
  const includePrivateInStats = optionalBoolean(env, 'SYNOMEM_INCLUDE_PRIVATE_IN_STATS');
154
154
  const projection = {
155
155
  ...(writeWinsMarkdown !== undefined ? { writeWinsMarkdown } : {}),
156
156
  ...(writeMemoryMarkdown !== undefined ? { writeMemoryMarkdown } : {}),
157
- ...(writeTodosMarkdown !== undefined ? { writeTodosMarkdown } : {}),
157
+ ...(writeTasksMarkdown !== undefined ? { writeTasksMarkdown } : {}),
158
158
  ...(writeInboxEntries !== undefined ? { writeInboxEntries } : {}),
159
159
  };
160
160
 
161
161
  return {
162
162
  ...(visibility ? { defaultVisibility: visibility as SynomemConfig['defaultVisibility'] } : {}),
163
163
  ...(allowSelfAwards !== undefined ? { allowSelfAwards } : {}),
164
- ...(allowCrossAgentTodos !== undefined ? { allowCrossAgentTodos } : {}),
164
+ ...(allowCrossAgentTasks !== undefined ? { allowCrossAgentTasks } : {}),
165
165
  ...(allowAgentCreationViaMcp !== undefined ? { allowAgentCreationViaMcp } : {}),
166
166
  ...(allowRebuildViaMcp !== undefined ? { allowRebuildViaMcp } : {}),
167
167
  ...(includePrivateInStats !== undefined ? { includePrivateInStats } : {}),
package/src/index.ts CHANGED
@@ -36,13 +36,22 @@ export type {
36
36
  } from './service.js';
37
37
  export { defaultConfig, resolveHome } from './config.js';
38
38
  export { asSynomemError, errorCodes, SynomemError } from './errors.js';
39
- export { escapeMarkdown, recordsFromEvents, renderMarkdownExport } from './projections.js';
39
+ export {
40
+ dueInstant,
41
+ escapeMarkdown,
42
+ recordsFromEvents,
43
+ renderMarkdownExport,
44
+ } from './projections.js';
40
45
  export {
41
46
  actorSchema,
47
+ agentAliasSchema,
42
48
  agentIdSchema,
49
+ agentLookupSchema,
50
+ bindRuntimeSchema,
43
51
  changesInputSchema,
44
52
  createAgentSchema,
45
53
  createNoteSchema,
54
+ createTaskSchema,
46
55
  createTodoSchema,
47
56
  eventSchema,
48
57
  evidenceSchema,
@@ -55,6 +64,7 @@ export {
55
64
  profileSchema,
56
65
  reviseNoteSchema,
57
66
  sendMemoSchema,
67
+ updateTaskSchema,
58
68
  updateTodoSchema,
59
69
  updateAgentSchema,
60
70
  } from './schemas.js';
package/src/mcp/index.ts CHANGED
@@ -9,12 +9,14 @@ import {
9
9
  agentIdSchema,
10
10
  changesInputSchema,
11
11
  createNoteSchema,
12
+ createTaskSchema,
12
13
  createTodoSchema,
13
14
  giveKudosMcpSchema,
14
15
  itemListInputSchema,
15
16
  listInputSchema,
16
17
  reviseNoteSchema,
17
18
  sendMemoSchema,
19
+ updateTaskSchema,
18
20
  updateTodoSchema,
19
21
  } from '../schemas.js';
20
22
  import { packageVersion } from '../version.js';
@@ -93,7 +95,7 @@ export async function createSynomemMcpServer(
93
95
  { name: 'synomem', version: packageVersion() },
94
96
  {
95
97
  instructions:
96
- 'Use Synomem for durable kudos, memos, notes, and todos. Store only necessary, factual content; never secrets or raw sensitive tool output. The server binds every write to its configured actor.',
98
+ 'Use Synomem for durable kudos, memos, notes, and tasks. Store only necessary, factual content; never secrets or raw sensitive tool output. The server binds every write to its configured actor.',
97
99
  },
98
100
  );
99
101
 
@@ -324,6 +326,129 @@ export async function createSynomemMcpServer(
324
326
  },
325
327
  );
326
328
 
329
+ server.registerTool(
330
+ 'synomem_post_create',
331
+ {
332
+ title: 'Publish a post',
333
+ description:
334
+ 'Publish something the whole workspace can read. Use for an announcement, a decision, or context several agents need. A post has no recipient — if one named actor must act, send a memo or assign a task instead.',
335
+ inputSchema: z.object({
336
+ title: z.string().trim().min(1).max(200),
337
+ body: z.string().trim().min(1).max(32_000),
338
+ tags: z.array(z.string()).max(20).optional(),
339
+ replyTo: z.string().length(26).optional(),
340
+ idempotencyKey: z.string().max(200).optional(),
341
+ }),
342
+ outputSchema,
343
+ annotations: { readOnlyHint: false, destructiveHint: false, idempotentHint: false },
344
+ },
345
+ async (input) => {
346
+ try {
347
+ const result = await client.posts.create(input);
348
+ return success(actor, `Published post ${result.record.event.id}.`, {
349
+ post: result.record,
350
+ });
351
+ } catch (error) {
352
+ return failure(actor, error);
353
+ }
354
+ },
355
+ );
356
+
357
+ server.registerTool(
358
+ 'synomem_post_acknowledge',
359
+ {
360
+ title: 'Acknowledge a post',
361
+ description:
362
+ 'Record that YOU have seen a post. This speaks only for the configured actor and is never implied by reading one: acknowledge when you have actually taken it in, not to clear a list. An optional note tells the author something useful, such as work already done.',
363
+ inputSchema: z.object({
364
+ postId: z.string().length(26),
365
+ note: z.string().trim().min(1).max(2000).optional(),
366
+ idempotencyKey: z.string().max(200).optional(),
367
+ }),
368
+ outputSchema,
369
+ annotations: { readOnlyHint: false, destructiveHint: false, idempotentHint: true },
370
+ },
371
+ async (input) => {
372
+ try {
373
+ const record = await client.posts.acknowledge(input);
374
+ return success(actor, `Acknowledged post ${input.postId}.`, { post: record });
375
+ } catch (error) {
376
+ return failure(actor, error);
377
+ }
378
+ },
379
+ );
380
+
381
+ server.registerTool(
382
+ 'synomem_post_roster',
383
+ {
384
+ title: 'See who has acknowledged a post',
385
+ description:
386
+ 'Who has acknowledged a post and who has not. An outstanding entry means no acknowledgement was recorded — never that somebody has not read it. Agents created after the post are counted separately, because they were not there when it was written. This is read-only and does not acknowledge anything.',
387
+ inputSchema: z.object({ postId: z.string().length(26) }),
388
+ outputSchema,
389
+ annotations: { readOnlyHint: true, destructiveHint: false, idempotentHint: true },
390
+ },
391
+ async ({ postId }) => {
392
+ try {
393
+ const roster = await client.posts.roster(postId);
394
+ return success(
395
+ actor,
396
+ `${roster.acknowledged.length} acknowledged, ${roster.outstanding.length} with no acknowledgement recorded.`,
397
+ roster,
398
+ );
399
+ } catch (error) {
400
+ return failure(actor, error);
401
+ }
402
+ },
403
+ );
404
+
405
+ server.registerTool(
406
+ 'synomem_agent_resolve',
407
+ {
408
+ title: 'Resolve an agent name',
409
+ description:
410
+ 'Resolve a name or alias to exactly one agent. Matching ignores case. When several agents answer to the name, no match is returned and the candidates are listed instead — ask which one is meant rather than choosing.',
411
+ inputSchema: z.object({
412
+ query: z.string().min(1).describe('An agent ID or alias, in any casing.'),
413
+ }),
414
+ outputSchema,
415
+ annotations: { readOnlyHint: true, destructiveHint: false, idempotentHint: true },
416
+ },
417
+ async ({ query }) => {
418
+ try {
419
+ const resolution = await client.agents.resolve(query);
420
+ const message = resolution.match
421
+ ? `"${resolution.query}" resolves to ${resolution.match.id}.`
422
+ : resolution.candidates.length
423
+ ? `"${resolution.query}" is ambiguous across ${resolution.candidates.length} agents. Ask which one is meant.`
424
+ : `No agent answers to "${resolution.query}".`;
425
+ return success(actor, message, resolution);
426
+ } catch (error) {
427
+ return failure(actor, error);
428
+ }
429
+ },
430
+ );
431
+
432
+ server.registerTool(
433
+ 'synomem_agent_directory',
434
+ {
435
+ title: 'Browse the agent directory',
436
+ description:
437
+ 'List known agents with their aliases and runtime bindings. Runtime bindings describe where an agent was registered to run and when Synomem last observed it act; they never mean the agent is reachable now. This is read-only.',
438
+ inputSchema: z.object({}),
439
+ outputSchema,
440
+ annotations: { readOnlyHint: true, destructiveHint: false, idempotentHint: true },
441
+ },
442
+ async () => {
443
+ try {
444
+ const entries = await client.agents.directory();
445
+ return success(actor, `Found ${entries.length} agent identity or identities.`, { entries });
446
+ } catch (error) {
447
+ return failure(actor, error);
448
+ }
449
+ },
450
+ );
451
+
327
452
  server.registerTool(
328
453
  'synomem_rebuild',
329
454
  {
@@ -377,7 +502,7 @@ export async function createSynomemMcpServer(
377
502
  {
378
503
  title: 'List Synomem items',
379
504
  description:
380
- 'Discover a bounded page of compact kudos, memo, note, and todo summaries. Full bodies, reasons, evidence, descriptions, source, and metadata are omitted; use synomem_get for one selected item.',
505
+ 'Discover a bounded page of compact kudos, memo, note, and task summaries. Full bodies, reasons, evidence, descriptions, source, and metadata are omitted; use synomem_get for one selected item.',
381
506
  inputSchema: itemListInputSchema,
382
507
  outputSchema,
383
508
  annotations: { readOnlyHint: true, destructiveHint: false, idempotentHint: true },
@@ -401,7 +526,7 @@ export async function createSynomemMcpServer(
401
526
  {
402
527
  title: 'Get one Synomem item',
403
528
  description:
404
- 'Read the full authorized record for one explicitly selected kudos, memo, note, or todo ID.',
529
+ 'Read the full authorized record for one explicitly selected kudos, memo, note, or task ID.',
405
530
  inputSchema: z.object({ itemId: z.string().length(26) }),
406
531
  outputSchema,
407
532
  annotations: { readOnlyHint: true, destructiveHint: false, idempotentHint: true },
@@ -445,7 +570,7 @@ export async function createSynomemMcpServer(
445
570
  {
446
571
  title: 'Review an agent inbox',
447
572
  description:
448
- 'Return compact pending kudos, unread memos, and open todos for the configured agent. An agent may inspect only its own private items.',
573
+ 'Return compact pending kudos, unread memos, and open tasks for the configured agent. An agent may inspect only its own private items.',
449
574
  inputSchema: z.object({
450
575
  limit: z.number().int().min(1).max(50).default(10),
451
576
  cursor: z.string().max(500).optional(),
@@ -585,12 +710,58 @@ export async function createSynomemMcpServer(
585
710
  },
586
711
  );
587
712
 
713
+ server.registerTool(
714
+ 'synomem_task_create',
715
+ {
716
+ title: 'Create a task',
717
+ description:
718
+ 'Create a concrete actionable task assigned to an agent, optionally with a date-only or timezone-aware deadline. Do not use as a substitute for a memo when no action is required.',
719
+ inputSchema: createTaskSchema,
720
+ outputSchema,
721
+ annotations: { readOnlyHint: false, destructiveHint: false, idempotentHint: false },
722
+ },
723
+ async (input) => {
724
+ try {
725
+ const result = await client.tasks.create(input);
726
+ return success(
727
+ actor,
728
+ `${result.deduplicated ? 'Returned existing' : 'Created'} task “${result.record.current.title}” for ${result.record.event.assigneeDisplayName} (ID ${result.record.event.id}).`,
729
+ result,
730
+ );
731
+ } catch (error) {
732
+ return failure(actor, error);
733
+ }
734
+ },
735
+ );
736
+ server.registerTool(
737
+ 'synomem_task_update',
738
+ {
739
+ title: 'Update a task',
740
+ description:
741
+ 'Append an update to an open task using the version last read. Stale versions fail rather than overwriting concurrent work.',
742
+ inputSchema: updateTaskSchema,
743
+ outputSchema,
744
+ annotations: { readOnlyHint: false, destructiveHint: false, idempotentHint: false },
745
+ },
746
+ async (input) => {
747
+ try {
748
+ const record = await client.tasks.update(input);
749
+ return success(
750
+ actor,
751
+ `Updated task ${record.event.id} to version ${record.current.version}.`,
752
+ { record },
753
+ );
754
+ } catch (error) {
755
+ return failure(actor, error);
756
+ }
757
+ },
758
+ );
588
759
  server.registerTool(
589
760
  'synomem_todo_create',
590
761
  {
591
- title: 'Create a todo',
762
+ title: 'Create a private todo',
592
763
  description:
593
- 'Create a concrete actionable todo assigned to an agent, optionally with a date-only or timezone-aware deadline. Do not use as a substitute for a memo when no action is required.',
764
+ 'Create a private reminder for yourself. A todo has no assignee and nobody else can read it — use synomem_task_create when the work belongs to another agent.',
594
765
  inputSchema: createTodoSchema,
595
766
  outputSchema,
596
767
  annotations: { readOnlyHint: false, destructiveHint: false, idempotentHint: false },
@@ -600,7 +771,7 @@ export async function createSynomemMcpServer(
600
771
  const result = await client.todos.create(input);
601
772
  return success(
602
773
  actor,
603
- `${result.deduplicated ? 'Returned existing' : 'Created'} todo “${result.record.current.title}” for ${result.record.event.assigneeDisplayName} (ID ${result.record.event.id}).`,
774
+ `${result.deduplicated ? 'Returned existing' : 'Created'} private todo “${result.record.current.title}” (ID ${result.record.event.id}).`,
604
775
  result,
605
776
  );
606
777
  } catch (error) {
@@ -611,9 +782,9 @@ export async function createSynomemMcpServer(
611
782
  server.registerTool(
612
783
  'synomem_todo_update',
613
784
  {
614
- title: 'Update a todo',
785
+ title: 'Update a private todo',
615
786
  description:
616
- 'Append an update to an open todo using the version last read. Stale versions fail rather than overwriting concurrent work.',
787
+ 'Append an update to one of your own todos using the version last read. Stale versions fail rather than overwriting concurrent work.',
617
788
  inputSchema: updateTodoSchema,
618
789
  outputSchema,
619
790
  annotations: { readOnlyHint: false, destructiveHint: false, idempotentHint: false },
@@ -624,15 +795,17 @@ export async function createSynomemMcpServer(
624
795
  return success(
625
796
  actor,
626
797
  `Updated todo ${record.event.id} to version ${record.current.version}.`,
627
- { record },
798
+ {
799
+ record,
800
+ },
628
801
  );
629
802
  } catch (error) {
630
803
  return failure(actor, error);
631
804
  }
632
805
  },
633
806
  );
634
- for (const operation of ['accept', 'reject', 'complete', 'reopen', 'cancel'] as const) {
635
- const inputSchema = z.object({
807
+ for (const operation of ['complete', 'reopen', 'cancel', 'archive'] as const) {
808
+ const todoInputSchema = z.object({
636
809
  todoId: z.string().length(26),
637
810
  note: z.string().trim().min(1).max(2000).optional(),
638
811
  reason: z.string().trim().min(1).max(2000).optional(),
@@ -641,9 +814,9 @@ export async function createSynomemMcpServer(
641
814
  server.registerTool(
642
815
  `synomem_todo_${operation}`,
643
816
  {
644
- title: `${operation[0]!.toUpperCase()}${operation.slice(1)} a todo`,
645
- description: `${operation[0]!.toUpperCase()}${operation.slice(1)} an authorized todo by appending a lifecycle event; history is never deleted.`,
646
- inputSchema,
817
+ title: `${operation[0]!.toUpperCase()}${operation.slice(1)} a private todo`,
818
+ description: `${operation[0]!.toUpperCase()}${operation.slice(1)} one of your own todos by appending a lifecycle event; history is never deleted.`,
819
+ inputSchema: todoInputSchema,
647
820
  outputSchema,
648
821
  annotations: {
649
822
  readOnlyHint: false,
@@ -654,34 +827,96 @@ export async function createSynomemMcpServer(
654
827
  async (input) => {
655
828
  try {
656
829
  const record =
657
- operation === 'accept'
658
- ? await client.todos.accept({
830
+ operation === 'complete'
831
+ ? await client.todos.complete({
659
832
  todoId: input.todoId,
833
+ ...(input.note ? { note: input.note } : {}),
660
834
  ...(input.idempotencyKey ? { idempotencyKey: input.idempotencyKey } : {}),
661
835
  })
662
- : operation === 'reject'
663
- ? await client.todos.reject({
836
+ : operation === 'cancel'
837
+ ? await client.todos.cancel({
664
838
  todoId: input.todoId,
665
839
  ...(input.reason ? { reason: input.reason } : {}),
666
840
  ...(input.idempotencyKey ? { idempotencyKey: input.idempotencyKey } : {}),
667
841
  })
668
- : operation === 'complete'
669
- ? await client.todos.complete({
842
+ : operation === 'archive'
843
+ ? await client.todos.archive({
844
+ todoId: input.todoId,
845
+ ...(input.idempotencyKey ? { idempotencyKey: input.idempotencyKey } : {}),
846
+ })
847
+ : await client.todos.reopen({
670
848
  todoId: input.todoId,
849
+ ...(input.idempotencyKey ? { idempotencyKey: input.idempotencyKey } : {}),
850
+ });
851
+ return success(actor, `Todo ${record.event.id} is now ${record.status}.`, { record });
852
+ } catch (error) {
853
+ return failure(actor, error);
854
+ }
855
+ },
856
+ );
857
+ }
858
+
859
+ for (const operation of ['accept', 'reject', 'complete', 'reopen', 'cancel'] as const) {
860
+ const inputSchema = z.object({
861
+ taskId: z.string().length(26),
862
+ note: z.string().trim().min(1).max(2000).optional(),
863
+ reason: z.string().trim().min(1).max(2000).optional(),
864
+ // Required on reject, optional on accept. Declaring it required in the
865
+ // tool schema for reject means a model is told before it calls, rather
866
+ // than discovering it from an error.
867
+ ...(operation === 'reject'
868
+ ? { response: z.string().trim().min(1).max(2000) }
869
+ : operation === 'accept'
870
+ ? { response: z.string().trim().min(1).max(2000).optional() }
871
+ : {}),
872
+ idempotencyKey: z.string().max(200).optional(),
873
+ });
874
+ server.registerTool(
875
+ `synomem_task_${operation}`,
876
+ {
877
+ title: `${operation[0]!.toUpperCase()}${operation.slice(1)} a task`,
878
+ description: `${operation[0]!.toUpperCase()}${operation.slice(1)} an authorized task by appending a lifecycle event; history is never deleted.`,
879
+ inputSchema,
880
+ outputSchema,
881
+ annotations: {
882
+ readOnlyHint: false,
883
+ destructiveHint: operation === 'cancel',
884
+ idempotentHint: true,
885
+ },
886
+ },
887
+ async (input) => {
888
+ try {
889
+ const record =
890
+ operation === 'accept'
891
+ ? await client.tasks.accept({
892
+ taskId: input.taskId,
893
+ ...('response' in input && input.response ? { response: input.response } : {}),
894
+ ...(input.idempotencyKey ? { idempotencyKey: input.idempotencyKey } : {}),
895
+ })
896
+ : operation === 'reject'
897
+ ? await client.tasks.reject({
898
+ taskId: input.taskId,
899
+ response:
900
+ ('response' in input ? input.response : undefined) ?? input.reason ?? '',
901
+ ...(input.idempotencyKey ? { idempotencyKey: input.idempotencyKey } : {}),
902
+ })
903
+ : operation === 'complete'
904
+ ? await client.tasks.complete({
905
+ taskId: input.taskId,
671
906
  ...(input.note ? { note: input.note } : {}),
672
907
  ...(input.idempotencyKey ? { idempotencyKey: input.idempotencyKey } : {}),
673
908
  })
674
909
  : operation === 'cancel'
675
- ? await client.todos.cancel({
676
- todoId: input.todoId,
910
+ ? await client.tasks.cancel({
911
+ taskId: input.taskId,
677
912
  ...(input.reason ? { reason: input.reason } : {}),
678
913
  ...(input.idempotencyKey ? { idempotencyKey: input.idempotencyKey } : {}),
679
914
  })
680
- : await client.todos.reopen({
681
- todoId: input.todoId,
915
+ : await client.tasks.reopen({
916
+ taskId: input.taskId,
682
917
  ...(input.idempotencyKey ? { idempotencyKey: input.idempotencyKey } : {}),
683
918
  });
684
- return success(actor, `Todo ${record.event.id} is ${record.status}.`, { record });
919
+ return success(actor, `Task ${record.event.id} is ${record.status}.`, { record });
685
920
  } catch (error) {
686
921
  return failure(actor, error);
687
922
  }
@@ -758,7 +993,7 @@ export async function createSynomemMcpServer(
758
993
  new ResourceTemplate('synomem://agents/{agentId}/inbox', { list: undefined }),
759
994
  {
760
995
  title: 'Agent inbox',
761
- description: 'Ten recent visible pending kudos, memos, and todos for one agent',
996
+ description: 'Ten recent visible pending kudos, memos, and tasks for one agent',
762
997
  mimeType: 'application/json',
763
998
  },
764
999
  async (uri, { agentId }) => {
@@ -932,9 +1167,9 @@ export async function createSynomemMcpServer(
932
1167
  );
933
1168
 
934
1169
  server.registerPrompt(
935
- 'synomem_create_actionable_todo',
1170
+ 'synomem_create_actionable_task',
936
1171
  {
937
- title: 'Create an actionable todo',
1172
+ title: 'Create an actionable task',
938
1173
  description:
939
1174
  'Turn requested work into a concrete assigned action without inventing deadlines.',
940
1175
  argsSchema: { assignee: agentIdSchema, action: z.string() },
@@ -945,7 +1180,7 @@ export async function createSynomemMcpServer(
945
1180
  role: 'user',
946
1181
  content: {
947
1182
  type: 'text',
948
- text: `Prepare a concrete todo assigned to ${assignee}: ${action}. Preserve a date-only deadline as a date, require timezone data for a timed deadline, and do not invent missing timing.`,
1183
+ text: `Prepare a concrete task assigned to ${assignee}: ${action}. Preserve a date-only deadline as a date, require timezone data for a timed deadline, and do not invent missing timing.`,
949
1184
  },
950
1185
  },
951
1186
  ],
package/src/mcp-server.ts CHANGED
@@ -1,7 +1,10 @@
1
1
  #!/usr/bin/env node
2
2
  import { parseArgs } from 'node:util';
3
+ import { createConfiguredService } from './backend.js';
4
+ import { SynomemError } from './errors.js';
3
5
  import { actorSchema } from './schemas.js';
4
6
  import { startMcpServer } from './mcp/index.js';
7
+ import type { ActorIdentity } from './types.js';
5
8
  import { packageVersion } from './version.js';
6
9
 
7
10
  const version = packageVersion();
@@ -9,6 +12,7 @@ const version = packageVersion();
9
12
  const { values } = parseArgs({
10
13
  options: {
11
14
  home: { type: 'string' },
15
+ 'agent-id': { type: 'string' },
12
16
  'actor-id': { type: 'string' },
13
17
  'actor-kind': { type: 'string' },
14
18
  'actor-name': { type: 'string' },
@@ -17,6 +21,43 @@ const { values } = parseArgs({
17
21
  },
18
22
  });
19
23
 
24
+ /**
25
+ * Loads an agent's identity from canonical state rather than from arguments.
26
+ *
27
+ * A display name passed on the command line is a claim the runtime makes about
28
+ * itself, and it is written into every event the session appends. Reading the
29
+ * profile instead means a misconfigured runtime cannot sign another agent's
30
+ * name to work, and renaming an agent takes effect without re-registering it
31
+ * with every harness.
32
+ */
33
+ async function resolveAgentActor(agentId: string, home?: string): Promise<ActorIdentity> {
34
+ const lookup = createConfiguredService({
35
+ actor: { kind: 'system', id: 'synomem-mcp' },
36
+ ...(home ? { home } : {}),
37
+ });
38
+ await lookup.init();
39
+ try {
40
+ const resolution = await lookup.agents.resolve(agentId);
41
+ if (!resolution.match) {
42
+ throw new SynomemError(
43
+ 'AGENT_NOT_FOUND',
44
+ resolution.candidates.length
45
+ ? `"${agentId}" matches ${resolution.candidates.length} agents: ${resolution.candidates
46
+ .map((candidate) => candidate.id)
47
+ .join(', ')}. Register the MCP server with a canonical agent ID.`
48
+ : `Unknown agent: ${agentId}. Create it with \`synomem agent create\` first.`,
49
+ );
50
+ }
51
+ return {
52
+ kind: 'agent',
53
+ id: resolution.match.id,
54
+ ...(resolution.match.displayName ? { displayName: resolution.match.displayName } : {}),
55
+ };
56
+ } finally {
57
+ await lookup.close();
58
+ }
59
+ }
60
+
20
61
  if (values.help) {
21
62
  process.stdout.write(`synomem-mcp ${version}
22
63
 
@@ -24,20 +65,29 @@ Actor-bound Synomem MCP server (stdio transport)
24
65
 
25
66
  Options:
26
67
  --home <path> Storage root
27
- --actor-id <id> Bound actor ID (or SYNOMEM_ACTOR_ID)
28
- --actor-kind <kind> human, agent, or system (or SYNOMEM_ACTOR_KIND)
29
- --actor-name <name> Optional display name (or SYNOMEM_ACTOR_NAME)
68
+ --agent-id <id> Bound agent, whose identity is read from Synomem
69
+ (or SYNOMEM_AGENT_ID)
70
+ --actor-id <id> Bound non-agent actor ID (or SYNOMEM_ACTOR_ID)
71
+ --actor-kind <kind> human or system (or SYNOMEM_ACTOR_KIND)
72
+ --actor-name <name> Display name for a non-agent actor
73
+ (or SYNOMEM_ACTOR_NAME)
30
74
  -h, --help Show help
31
75
  -v, --version Show version
76
+
77
+ Prefer --agent-id for an agent runtime: the display name and kind then come
78
+ from the agent's profile instead of from whatever the harness was told to pass.
32
79
  `);
33
80
  } else if (values.version) {
34
81
  process.stdout.write(`${version}\n`);
35
82
  } else {
36
- const actor = actorSchema.parse({
37
- id: values['actor-id'] ?? process.env.SYNOMEM_ACTOR_ID,
38
- kind: values['actor-kind'] ?? process.env.SYNOMEM_ACTOR_KIND,
39
- displayName: values['actor-name'] ?? process.env.SYNOMEM_ACTOR_NAME,
40
- });
83
+ const agentId = values['agent-id'] ?? process.env.SYNOMEM_AGENT_ID;
84
+ const actor = agentId
85
+ ? await resolveAgentActor(agentId, values.home)
86
+ : actorSchema.parse({
87
+ id: values['actor-id'] ?? process.env.SYNOMEM_ACTOR_ID,
88
+ kind: values['actor-kind'] ?? process.env.SYNOMEM_ACTOR_KIND,
89
+ displayName: values['actor-name'] ?? process.env.SYNOMEM_ACTOR_NAME,
90
+ });
41
91
 
42
92
  await startMcpServer({
43
93
  actor,
@@ -1,8 +1,12 @@
1
1
  import type {
2
2
  ActorIdentity,
3
3
  AgentProfile,
4
+ AgentRuntimeBinding,
4
5
  ChangePage,
5
6
  ItemListInput,
7
+ JsonValue,
8
+ PostAcknowledgment,
9
+ PostRoster,
6
10
  ItemSummary,
7
11
  KudosListInput,
8
12
  KudosSummary,
@@ -41,6 +45,23 @@ export interface SynomemRepository {
41
45
  updateAgent(profile: AgentProfile, updatedAt: string): Awaitable<void>;
42
46
  getAgent(idOrAlias: string): Awaitable<AgentProfile | undefined>;
43
47
  listAgents(): Awaitable<AgentProfile[]>;
48
+ /** Resolves a name case-insensitively, reporting ambiguity instead of guessing. */
49
+ resolveAgent(query: string): Awaitable<{ match?: AgentProfile; candidates: AgentProfile[] }>;
50
+ listPostAcknowledgments(postId: string): Awaitable<PostAcknowledgment[]>;
51
+ /** Who has acknowledged a post and who has not; see PostRoster. */
52
+ postRoster(postId: string): Awaitable<PostRoster | undefined>;
53
+ listRuntimeBindings(agentId: string): Awaitable<AgentRuntimeBinding[]>;
54
+ bindRuntime(binding: {
55
+ id: string;
56
+ agentId: string;
57
+ installationId?: string;
58
+ runtime: string;
59
+ profile?: string;
60
+ capabilities?: Record<string, JsonValue>;
61
+ boundAt: string;
62
+ }): Awaitable<void>;
63
+ unbindRuntime(bindingId: string): Awaitable<boolean>;
64
+ touchRuntimeBinding(agentId: string, runtime: string, at: string): Awaitable<void>;
44
65
 
45
66
  listKudosSummaries(
46
67
  input: Required<Pick<KudosListInput, 'limit' | 'offset'>> & KudosListInput,