synomem 0.1.0 → 0.2.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 -1
  3. package/README.md +28 -15
  4. package/dist/cli.d.ts.map +1 -1
  5. package/dist/cli.js +311 -70
  6. package/dist/cli.js.map +1 -1
  7. package/dist/client.d.ts +96 -17
  8. package/dist/client.d.ts.map +1 -1
  9. package/dist/client.js +333 -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 +207 -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 +156 -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 +18 -1
  25. package/dist/ports/repository.d.ts.map +1 -1
  26. package/dist/projections.d.ts +18 -1
  27. package/dist/projections.d.ts.map +1 -1
  28. package/dist/projections.js +137 -44
  29. package/dist/projections.js.map +1 -1
  30. package/dist/remote.d.ts +57 -9
  31. package/dist/remote.d.ts.map +1 -1
  32. package/dist/remote.js +43 -2
  33. package/dist/remote.js.map +1 -1
  34. package/dist/schemas.d.ts +302 -21
  35. package/dist/schemas.d.ts.map +1 -1
  36. package/dist/schemas.js +142 -26
  37. package/dist/schemas.js.map +1 -1
  38. package/dist/service.d.ts +57 -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 +41 -1
  45. package/dist/storage.d.ts.map +1 -1
  46. package/dist/storage.js +254 -36
  47. package/dist/storage.js.map +1 -1
  48. package/dist/types.d.ts +213 -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 +572 -173
  61. package/src/client.ts +391 -79
  62. package/src/config.ts +8 -8
  63. package/src/index.ts +11 -1
  64. package/src/mcp/index.ts +189 -30
  65. package/src/mcp-server.ts +58 -8
  66. package/src/ports/repository.ts +16 -0
  67. package/src/projections.ts +139 -44
  68. package/src/remote.ts +118 -8
  69. package/src/schemas.ts +147 -26
  70. package/src/service.ts +59 -7
  71. package/src/skill-install.ts +14 -17
  72. package/src/storage.ts +326 -34
  73. package/src/types.ts +228 -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,53 @@ export async function createSynomemMcpServer(
324
326
  },
325
327
  );
326
328
 
329
+ server.registerTool(
330
+ 'synomem_agent_resolve',
331
+ {
332
+ title: 'Resolve an agent name',
333
+ description:
334
+ '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.',
335
+ inputSchema: z.object({
336
+ query: z.string().min(1).describe('An agent ID or alias, in any casing.'),
337
+ }),
338
+ outputSchema,
339
+ annotations: { readOnlyHint: true, destructiveHint: false, idempotentHint: true },
340
+ },
341
+ async ({ query }) => {
342
+ try {
343
+ const resolution = await client.agents.resolve(query);
344
+ const message = resolution.match
345
+ ? `"${resolution.query}" resolves to ${resolution.match.id}.`
346
+ : resolution.candidates.length
347
+ ? `"${resolution.query}" is ambiguous across ${resolution.candidates.length} agents. Ask which one is meant.`
348
+ : `No agent answers to "${resolution.query}".`;
349
+ return success(actor, message, resolution);
350
+ } catch (error) {
351
+ return failure(actor, error);
352
+ }
353
+ },
354
+ );
355
+
356
+ server.registerTool(
357
+ 'synomem_agent_directory',
358
+ {
359
+ title: 'Browse the agent directory',
360
+ description:
361
+ '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.',
362
+ inputSchema: z.object({}),
363
+ outputSchema,
364
+ annotations: { readOnlyHint: true, destructiveHint: false, idempotentHint: true },
365
+ },
366
+ async () => {
367
+ try {
368
+ const entries = await client.agents.directory();
369
+ return success(actor, `Found ${entries.length} agent identity or identities.`, { entries });
370
+ } catch (error) {
371
+ return failure(actor, error);
372
+ }
373
+ },
374
+ );
375
+
327
376
  server.registerTool(
328
377
  'synomem_rebuild',
329
378
  {
@@ -377,7 +426,7 @@ export async function createSynomemMcpServer(
377
426
  {
378
427
  title: 'List Synomem items',
379
428
  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.',
429
+ '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
430
  inputSchema: itemListInputSchema,
382
431
  outputSchema,
383
432
  annotations: { readOnlyHint: true, destructiveHint: false, idempotentHint: true },
@@ -401,7 +450,7 @@ export async function createSynomemMcpServer(
401
450
  {
402
451
  title: 'Get one Synomem item',
403
452
  description:
404
- 'Read the full authorized record for one explicitly selected kudos, memo, note, or todo ID.',
453
+ 'Read the full authorized record for one explicitly selected kudos, memo, note, or task ID.',
405
454
  inputSchema: z.object({ itemId: z.string().length(26) }),
406
455
  outputSchema,
407
456
  annotations: { readOnlyHint: true, destructiveHint: false, idempotentHint: true },
@@ -445,7 +494,7 @@ export async function createSynomemMcpServer(
445
494
  {
446
495
  title: 'Review an agent inbox',
447
496
  description:
448
- 'Return compact pending kudos, unread memos, and open todos for the configured agent. An agent may inspect only its own private items.',
497
+ 'Return compact pending kudos, unread memos, and open tasks for the configured agent. An agent may inspect only its own private items.',
449
498
  inputSchema: z.object({
450
499
  limit: z.number().int().min(1).max(50).default(10),
451
500
  cursor: z.string().max(500).optional(),
@@ -585,12 +634,58 @@ export async function createSynomemMcpServer(
585
634
  },
586
635
  );
587
636
 
637
+ server.registerTool(
638
+ 'synomem_task_create',
639
+ {
640
+ title: 'Create a task',
641
+ description:
642
+ '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.',
643
+ inputSchema: createTaskSchema,
644
+ outputSchema,
645
+ annotations: { readOnlyHint: false, destructiveHint: false, idempotentHint: false },
646
+ },
647
+ async (input) => {
648
+ try {
649
+ const result = await client.tasks.create(input);
650
+ return success(
651
+ actor,
652
+ `${result.deduplicated ? 'Returned existing' : 'Created'} task “${result.record.current.title}” for ${result.record.event.assigneeDisplayName} (ID ${result.record.event.id}).`,
653
+ result,
654
+ );
655
+ } catch (error) {
656
+ return failure(actor, error);
657
+ }
658
+ },
659
+ );
660
+ server.registerTool(
661
+ 'synomem_task_update',
662
+ {
663
+ title: 'Update a task',
664
+ description:
665
+ 'Append an update to an open task using the version last read. Stale versions fail rather than overwriting concurrent work.',
666
+ inputSchema: updateTaskSchema,
667
+ outputSchema,
668
+ annotations: { readOnlyHint: false, destructiveHint: false, idempotentHint: false },
669
+ },
670
+ async (input) => {
671
+ try {
672
+ const record = await client.tasks.update(input);
673
+ return success(
674
+ actor,
675
+ `Updated task ${record.event.id} to version ${record.current.version}.`,
676
+ { record },
677
+ );
678
+ } catch (error) {
679
+ return failure(actor, error);
680
+ }
681
+ },
682
+ );
588
683
  server.registerTool(
589
684
  'synomem_todo_create',
590
685
  {
591
- title: 'Create a todo',
686
+ title: 'Create a private todo',
592
687
  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.',
688
+ '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
689
  inputSchema: createTodoSchema,
595
690
  outputSchema,
596
691
  annotations: { readOnlyHint: false, destructiveHint: false, idempotentHint: false },
@@ -600,7 +695,7 @@ export async function createSynomemMcpServer(
600
695
  const result = await client.todos.create(input);
601
696
  return success(
602
697
  actor,
603
- `${result.deduplicated ? 'Returned existing' : 'Created'} todo “${result.record.current.title}” for ${result.record.event.assigneeDisplayName} (ID ${result.record.event.id}).`,
698
+ `${result.deduplicated ? 'Returned existing' : 'Created'} private todo “${result.record.current.title}” (ID ${result.record.event.id}).`,
604
699
  result,
605
700
  );
606
701
  } catch (error) {
@@ -611,9 +706,9 @@ export async function createSynomemMcpServer(
611
706
  server.registerTool(
612
707
  'synomem_todo_update',
613
708
  {
614
- title: 'Update a todo',
709
+ title: 'Update a private todo',
615
710
  description:
616
- 'Append an update to an open todo using the version last read. Stale versions fail rather than overwriting concurrent work.',
711
+ 'Append an update to one of your own todos using the version last read. Stale versions fail rather than overwriting concurrent work.',
617
712
  inputSchema: updateTodoSchema,
618
713
  outputSchema,
619
714
  annotations: { readOnlyHint: false, destructiveHint: false, idempotentHint: false },
@@ -624,15 +719,17 @@ export async function createSynomemMcpServer(
624
719
  return success(
625
720
  actor,
626
721
  `Updated todo ${record.event.id} to version ${record.current.version}.`,
627
- { record },
722
+ {
723
+ record,
724
+ },
628
725
  );
629
726
  } catch (error) {
630
727
  return failure(actor, error);
631
728
  }
632
729
  },
633
730
  );
634
- for (const operation of ['accept', 'reject', 'complete', 'reopen', 'cancel'] as const) {
635
- const inputSchema = z.object({
731
+ for (const operation of ['complete', 'reopen', 'cancel', 'archive'] as const) {
732
+ const todoInputSchema = z.object({
636
733
  todoId: z.string().length(26),
637
734
  note: z.string().trim().min(1).max(2000).optional(),
638
735
  reason: z.string().trim().min(1).max(2000).optional(),
@@ -641,9 +738,9 @@ export async function createSynomemMcpServer(
641
738
  server.registerTool(
642
739
  `synomem_todo_${operation}`,
643
740
  {
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,
741
+ title: `${operation[0]!.toUpperCase()}${operation.slice(1)} a private todo`,
742
+ description: `${operation[0]!.toUpperCase()}${operation.slice(1)} one of your own todos by appending a lifecycle event; history is never deleted.`,
743
+ inputSchema: todoInputSchema,
647
744
  outputSchema,
648
745
  annotations: {
649
746
  readOnlyHint: false,
@@ -654,34 +751,96 @@ export async function createSynomemMcpServer(
654
751
  async (input) => {
655
752
  try {
656
753
  const record =
657
- operation === 'accept'
658
- ? await client.todos.accept({
754
+ operation === 'complete'
755
+ ? await client.todos.complete({
659
756
  todoId: input.todoId,
757
+ ...(input.note ? { note: input.note } : {}),
660
758
  ...(input.idempotencyKey ? { idempotencyKey: input.idempotencyKey } : {}),
661
759
  })
662
- : operation === 'reject'
663
- ? await client.todos.reject({
760
+ : operation === 'cancel'
761
+ ? await client.todos.cancel({
664
762
  todoId: input.todoId,
665
763
  ...(input.reason ? { reason: input.reason } : {}),
666
764
  ...(input.idempotencyKey ? { idempotencyKey: input.idempotencyKey } : {}),
667
765
  })
668
- : operation === 'complete'
669
- ? await client.todos.complete({
766
+ : operation === 'archive'
767
+ ? await client.todos.archive({
768
+ todoId: input.todoId,
769
+ ...(input.idempotencyKey ? { idempotencyKey: input.idempotencyKey } : {}),
770
+ })
771
+ : await client.todos.reopen({
670
772
  todoId: input.todoId,
773
+ ...(input.idempotencyKey ? { idempotencyKey: input.idempotencyKey } : {}),
774
+ });
775
+ return success(actor, `Todo ${record.event.id} is now ${record.status}.`, { record });
776
+ } catch (error) {
777
+ return failure(actor, error);
778
+ }
779
+ },
780
+ );
781
+ }
782
+
783
+ for (const operation of ['accept', 'reject', 'complete', 'reopen', 'cancel'] as const) {
784
+ const inputSchema = z.object({
785
+ taskId: z.string().length(26),
786
+ note: z.string().trim().min(1).max(2000).optional(),
787
+ reason: z.string().trim().min(1).max(2000).optional(),
788
+ // Required on reject, optional on accept. Declaring it required in the
789
+ // tool schema for reject means a model is told before it calls, rather
790
+ // than discovering it from an error.
791
+ ...(operation === 'reject'
792
+ ? { response: z.string().trim().min(1).max(2000) }
793
+ : operation === 'accept'
794
+ ? { response: z.string().trim().min(1).max(2000).optional() }
795
+ : {}),
796
+ idempotencyKey: z.string().max(200).optional(),
797
+ });
798
+ server.registerTool(
799
+ `synomem_task_${operation}`,
800
+ {
801
+ title: `${operation[0]!.toUpperCase()}${operation.slice(1)} a task`,
802
+ description: `${operation[0]!.toUpperCase()}${operation.slice(1)} an authorized task by appending a lifecycle event; history is never deleted.`,
803
+ inputSchema,
804
+ outputSchema,
805
+ annotations: {
806
+ readOnlyHint: false,
807
+ destructiveHint: operation === 'cancel',
808
+ idempotentHint: true,
809
+ },
810
+ },
811
+ async (input) => {
812
+ try {
813
+ const record =
814
+ operation === 'accept'
815
+ ? await client.tasks.accept({
816
+ taskId: input.taskId,
817
+ ...('response' in input && input.response ? { response: input.response } : {}),
818
+ ...(input.idempotencyKey ? { idempotencyKey: input.idempotencyKey } : {}),
819
+ })
820
+ : operation === 'reject'
821
+ ? await client.tasks.reject({
822
+ taskId: input.taskId,
823
+ response:
824
+ ('response' in input ? input.response : undefined) ?? input.reason ?? '',
825
+ ...(input.idempotencyKey ? { idempotencyKey: input.idempotencyKey } : {}),
826
+ })
827
+ : operation === 'complete'
828
+ ? await client.tasks.complete({
829
+ taskId: input.taskId,
671
830
  ...(input.note ? { note: input.note } : {}),
672
831
  ...(input.idempotencyKey ? { idempotencyKey: input.idempotencyKey } : {}),
673
832
  })
674
833
  : operation === 'cancel'
675
- ? await client.todos.cancel({
676
- todoId: input.todoId,
834
+ ? await client.tasks.cancel({
835
+ taskId: input.taskId,
677
836
  ...(input.reason ? { reason: input.reason } : {}),
678
837
  ...(input.idempotencyKey ? { idempotencyKey: input.idempotencyKey } : {}),
679
838
  })
680
- : await client.todos.reopen({
681
- todoId: input.todoId,
839
+ : await client.tasks.reopen({
840
+ taskId: input.taskId,
682
841
  ...(input.idempotencyKey ? { idempotencyKey: input.idempotencyKey } : {}),
683
842
  });
684
- return success(actor, `Todo ${record.event.id} is ${record.status}.`, { record });
843
+ return success(actor, `Task ${record.event.id} is ${record.status}.`, { record });
685
844
  } catch (error) {
686
845
  return failure(actor, error);
687
846
  }
@@ -758,7 +917,7 @@ export async function createSynomemMcpServer(
758
917
  new ResourceTemplate('synomem://agents/{agentId}/inbox', { list: undefined }),
759
918
  {
760
919
  title: 'Agent inbox',
761
- description: 'Ten recent visible pending kudos, memos, and todos for one agent',
920
+ description: 'Ten recent visible pending kudos, memos, and tasks for one agent',
762
921
  mimeType: 'application/json',
763
922
  },
764
923
  async (uri, { agentId }) => {
@@ -932,9 +1091,9 @@ export async function createSynomemMcpServer(
932
1091
  );
933
1092
 
934
1093
  server.registerPrompt(
935
- 'synomem_create_actionable_todo',
1094
+ 'synomem_create_actionable_task',
936
1095
  {
937
- title: 'Create an actionable todo',
1096
+ title: 'Create an actionable task',
938
1097
  description:
939
1098
  'Turn requested work into a concrete assigned action without inventing deadlines.',
940
1099
  argsSchema: { assignee: agentIdSchema, action: z.string() },
@@ -945,7 +1104,7 @@ export async function createSynomemMcpServer(
945
1104
  role: 'user',
946
1105
  content: {
947
1106
  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.`,
1107
+ 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
1108
  },
950
1109
  },
951
1110
  ],
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,10 @@
1
1
  import type {
2
2
  ActorIdentity,
3
3
  AgentProfile,
4
+ AgentRuntimeBinding,
4
5
  ChangePage,
5
6
  ItemListInput,
7
+ JsonValue,
6
8
  ItemSummary,
7
9
  KudosListInput,
8
10
  KudosSummary,
@@ -41,6 +43,20 @@ export interface SynomemRepository {
41
43
  updateAgent(profile: AgentProfile, updatedAt: string): Awaitable<void>;
42
44
  getAgent(idOrAlias: string): Awaitable<AgentProfile | undefined>;
43
45
  listAgents(): Awaitable<AgentProfile[]>;
46
+ /** Resolves a name case-insensitively, reporting ambiguity instead of guessing. */
47
+ resolveAgent(query: string): Awaitable<{ match?: AgentProfile; candidates: AgentProfile[] }>;
48
+ listRuntimeBindings(agentId: string): Awaitable<AgentRuntimeBinding[]>;
49
+ bindRuntime(binding: {
50
+ id: string;
51
+ agentId: string;
52
+ installationId?: string;
53
+ runtime: string;
54
+ profile?: string;
55
+ capabilities?: Record<string, JsonValue>;
56
+ boundAt: string;
57
+ }): Awaitable<void>;
58
+ unbindRuntime(bindingId: string): Awaitable<boolean>;
59
+ touchRuntimeBinding(agentId: string, runtime: string, at: string): Awaitable<void>;
44
60
 
45
61
  listKudosSummaries(
46
62
  input: Required<Pick<KudosListInput, 'limit' | 'offset'>> & KudosListInput,