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/dist/cli.js CHANGED
@@ -62,7 +62,7 @@ function defaultActor(env, fallbackKind, fallbackId) {
62
62
  return actor(fallbackKind, fallbackId);
63
63
  return actor(env.SYNOMEM_ACTOR_KIND?.trim() || fallbackKind, id, env.SYNOMEM_ACTOR_NAME?.trim());
64
64
  }
65
- function todoDue(options) {
65
+ function taskDue(options) {
66
66
  if (options.dueDate && options.dueAt)
67
67
  throw new SynomemError('INVALID_INPUT', 'Use due-date or due-at, not both.');
68
68
  if (options.dueDate)
@@ -444,32 +444,64 @@ export function createCli(io = defaultIo, serviceFactory = configuredServiceFact
444
444
  .option('--yes', 'apply the displayed plan', false)
445
445
  .option('--force', 'replace a conflicting synomem directory', false)
446
446
  .option('--link', 'symlink to the packaged skill instead of copying it', false)
447
- .option('--actor-id <id>', 'print actor-bound MCP registration commands')
448
- .option('--actor-name <name>', 'display name used in MCP registration commands')
449
- .action((options, command) => {
447
+ .option('--agent <id-or-alias>', 'bind this installation to an agent')
448
+ .action(async (options, command) => {
450
449
  const global = globals(command);
450
+ const runtimes = skillRuntimes(options.runtime);
451
+ /*
452
+ * The agent is resolved BEFORE anything is written. An ambiguous or
453
+ * unknown name then stops the command with a name to fix, rather than
454
+ * leaving a skill installed and pointed at an agent that does not
455
+ * exist.
456
+ */
457
+ let agentId;
458
+ if (options.agent) {
459
+ agentId = await withClient(global.home, defaultActor(env, 'system', 'cli'), async (client) => {
460
+ const resolution = await client.agents.resolve(options.agent);
461
+ if (!resolution.match) {
462
+ throw new SynomemError('AGENT_NOT_FOUND', resolution.candidates.length
463
+ ? `"${options.agent}" matches ${resolution.candidates.length} agents: ${resolution.candidates
464
+ .map((candidate) => candidate.id)
465
+ .join(', ')}. Name one of them.`
466
+ : `Unknown agent: ${options.agent}`);
467
+ }
468
+ return resolution.match.id;
469
+ });
470
+ }
451
471
  const result = installSkill({
452
- runtimes: skillRuntimes(options.runtime),
472
+ ...(runtimes ? { runtimes } : {}),
453
473
  apply: options.yes,
454
474
  force: options.force,
455
475
  link: options.link,
456
- actorId: options.actorId,
457
- actorName: options.actorName,
476
+ ...(agentId ? { agentId } : {}),
458
477
  });
478
+ // Bindings follow what was actually installed, and only on a real run:
479
+ // a dry run must not claim a binding it did not make, and a runtime
480
+ // whose harness is not present here is not somewhere this agent runs.
481
+ if (agentId && options.yes) {
482
+ const installed = result.locations
483
+ .filter((location) => location.state !== 'unavailable')
484
+ .map((location) => location.runtime);
485
+ if (installed.length) {
486
+ await withClient(global.home, defaultActor(env, 'system', 'cli'), async (client) => {
487
+ for (const runtime of installed) {
488
+ await client.agents.bindRuntime({ agentId, runtime });
489
+ }
490
+ });
491
+ }
492
+ }
459
493
  output(io, global.json, result, formatSkillResult(result, 'install'));
460
494
  });
461
495
  skillCommand
462
496
  .command('status')
463
497
  .description('Show installed, stale, missing, or conflicting skill copies')
464
498
  .option('--runtime <runtime>', `${skillRuntimeHelp} (repeatable)`, collect, [])
465
- .option('--actor-id <id>', 'print actor-bound MCP registration commands')
466
- .option('--actor-name <name>', 'display name used in MCP registration commands')
499
+ .option('--agent <id>', 'print the registration command for this agent')
467
500
  .action((options, command) => {
468
501
  const global = globals(command);
469
502
  const result = skillStatus({
470
503
  runtimes: skillRuntimes(options.runtime),
471
- actorId: options.actorId,
472
- actorName: options.actorName,
504
+ ...(options.agent ? { agentId: options.agent } : {}),
473
505
  });
474
506
  output(io, global.json, result, formatSkillResult(result, 'status'));
475
507
  });
@@ -526,6 +558,183 @@ export function createCli(io = defaultIo, serviceFactory = configuredServiceFact
526
558
  }));
527
559
  output(io, global.json, profile, `Updated ${profile.displayName} (${profile.id})`);
528
560
  });
561
+ agentCommand
562
+ .command('resolve <name>')
563
+ .description('Resolve a name or alias to one agent, or list the candidates')
564
+ .action(async (name, _options, command) => {
565
+ const global = globals(command);
566
+ const resolution = await withClient(global.home, defaultActor(env, 'system', 'cli'), (client) => client.agents.resolve(name));
567
+ // An ambiguous name is a question, not a failure: exit zero and show the
568
+ // candidates so the caller can pick one.
569
+ const human = resolution.match
570
+ ? `${resolution.match.displayName} (${resolution.match.id})`
571
+ : resolution.candidates.length
572
+ ? `"${resolution.query}" is ambiguous. Candidates:\n${resolution.candidates
573
+ .map((profile) => ` ${profile.id} ${profile.displayName}`)
574
+ .join('\n')}`
575
+ : `No agent answers to "${resolution.query}".`;
576
+ output(io, global.json, resolution, human);
577
+ });
578
+ agentCommand
579
+ .command('directory')
580
+ .description('List agents with their runtime bindings')
581
+ .action(async (_options, command) => {
582
+ const global = globals(command);
583
+ const entries = await withClient(global.home, defaultActor(env, 'system', 'cli'), (client) => client.agents.directory());
584
+ const human = entries.length
585
+ ? entries
586
+ .map((entry) => {
587
+ const runtimes = entry.runtimeBindings.length
588
+ ? entry.runtimeBindings
589
+ .map((binding) => ` ${binding.runtime}${binding.profile ? `/${binding.profile}` : ''}` +
590
+ // Last-seen is advisory, so it is labelled as an
591
+ // observation rather than a status.
592
+ `${binding.lastSeenAt ? ` last seen ${binding.lastSeenAt}` : ' not yet seen'}`)
593
+ .join('\n')
594
+ : ' no runtime bindings';
595
+ return `${entry.profile.id} ${entry.profile.displayName}\n${runtimes}`;
596
+ })
597
+ .join('\n')
598
+ : 'No agents configured.';
599
+ output(io, global.json, { entries }, human);
600
+ });
601
+ const runtimeCommand = agentCommand.command('runtime').description('Record where an agent runs');
602
+ runtimeCommand
603
+ .command('bind <agent>')
604
+ .description('Bind an agent to a runtime')
605
+ .requiredOption('--runtime <name>', 'runtime family, e.g. claude-code')
606
+ .option('--profile <name>', 'named configuration within the runtime')
607
+ .option('--installation <id>', 'hosted installation this binding belongs to')
608
+ .action(async (agent, options, command) => {
609
+ const global = globals(command);
610
+ const binding = await withClient(global.home, defaultActor(env, 'system', 'cli'), (client) => client.agents.bindRuntime({
611
+ agentId: agent,
612
+ runtime: options.runtime,
613
+ ...(options.profile ? { profile: options.profile } : {}),
614
+ ...(options.installation ? { installationId: options.installation } : {}),
615
+ }));
616
+ output(io, global.json, binding, `Bound ${binding.agentId} to ${binding.runtime}${binding.profile ? `/${binding.profile}` : ''} (${binding.id})`);
617
+ });
618
+ runtimeCommand
619
+ .command('list <agent>')
620
+ .description('List an agent runtime bindings')
621
+ .action(async (agent, _options, command) => {
622
+ const global = globals(command);
623
+ const bindings = await withClient(global.home, defaultActor(env, 'system', 'cli'), (client) => client.agents.bindings(agent));
624
+ const human = bindings.length
625
+ ? bindings
626
+ .map((binding) => `${binding.id} ${binding.runtime}${binding.profile ? `/${binding.profile}` : ''} bound ${binding.boundAt}`)
627
+ .join('\n')
628
+ : 'No runtime bindings.';
629
+ output(io, global.json, { bindings }, human);
630
+ });
631
+ runtimeCommand
632
+ .command('unbind <binding-id>')
633
+ .description('Remove a runtime binding')
634
+ .action(async (bindingId, _options, command) => {
635
+ const global = globals(command);
636
+ const removed = await withClient(global.home, defaultActor(env, 'system', 'cli'), (client) => client.agents.unbindRuntime(bindingId));
637
+ output(io, global.json, { removed }, removed ? `Removed binding ${bindingId}.` : `No binding ${bindingId}.`);
638
+ });
639
+ const postCommand = program.command('post').description('Publish to everyone in the workspace');
640
+ postCommand
641
+ .command('create')
642
+ .description('Publish a post the whole workspace can read')
643
+ .requiredOption('--as <actor-id>')
644
+ .option('--actor-kind <kind>', 'human, agent, or system', 'agent')
645
+ .requiredOption('--title <title>')
646
+ .requiredOption('--body <body>')
647
+ .option('--tag <tag>', 'repeatable', collect, [])
648
+ .option('--reply-to <post-id>')
649
+ .action(async (options, command) => {
650
+ const global = globals(command);
651
+ const result = await withClient(global.home, actor(options.actorKind, options.as), (client) => client.posts.create({
652
+ title: options.title,
653
+ body: options.body,
654
+ ...(options.tag.length ? { tags: options.tag } : {}),
655
+ ...(options.replyTo ? { replyTo: options.replyTo } : {}),
656
+ }));
657
+ output(io, global.json, result, `Published ${result.record.event.id}`);
658
+ });
659
+ postCommand
660
+ .command('list')
661
+ .description('List posts in this workspace')
662
+ .requiredOption('--as <actor-id>')
663
+ .option('--actor-kind <kind>', 'human, agent, or system', 'agent')
664
+ .option('--limit <n>', 'default 10, maximum 50')
665
+ .action(async (options, command) => {
666
+ const global = globals(command);
667
+ const page = await withClient(global.home, actor(options.actorKind, options.as), (client) => client.posts.list(options.limit ? { limit: Number(options.limit) } : {}));
668
+ const human = page.items.length
669
+ ? page.items.map((item) => `${item.id} ${item.title}`).join('\n')
670
+ : 'No posts yet.';
671
+ output(io, global.json, page, human);
672
+ });
673
+ postCommand
674
+ .command('show <post-id>')
675
+ .description('Show one post with its acknowledgements')
676
+ .requiredOption('--as <actor-id>')
677
+ .option('--actor-kind <kind>', 'human, agent, or system', 'agent')
678
+ .action(async (postId, options, command) => {
679
+ const global = globals(command);
680
+ const record = await withClient(global.home, actor(options.actorKind, options.as), (client) => client.posts.get(postId));
681
+ const acks = record.acknowledgments.length
682
+ ? record.acknowledgments
683
+ .map((entry) => ` ${entry.actor.id}${entry.note ? ` — ${entry.note}` : ''}`)
684
+ .join('\n')
685
+ : ' none yet';
686
+ output(io, global.json, record, `${record.title}\n\n${record.body}\n\nAcknowledged by:\n${acks}`);
687
+ });
688
+ postCommand
689
+ .command('acknowledge <post-id>')
690
+ .description('Say you have seen a post')
691
+ .requiredOption('--as <actor-id>')
692
+ .option('--actor-kind <kind>', 'human, agent, or system', 'agent')
693
+ .option('--note <text>', 'optional context for the author')
694
+ .action(async (postId, options, command) => {
695
+ const global = globals(command);
696
+ const record = await withClient(global.home, actor(options.actorKind, options.as), (client) => client.posts.acknowledge({
697
+ postId,
698
+ ...(options.note ? { note: options.note } : {}),
699
+ }));
700
+ output(io, global.json, record, `Acknowledged ${postId}`);
701
+ });
702
+ postCommand
703
+ .command('roster <post-id>')
704
+ .description('Who has acknowledged a post, and who has not')
705
+ .requiredOption('--as <actor-id>')
706
+ .option('--actor-kind <kind>', 'human, agent, or system', 'agent')
707
+ .action(async (postId, options, command) => {
708
+ const global = globals(command);
709
+ const roster = await withClient(global.home, actor(options.actorKind, options.as), (client) => client.posts.roster(postId));
710
+ // "Outstanding" means no acknowledgement recorded — never that somebody
711
+ // has not read it, which this cannot know.
712
+ const lines = [
713
+ `Acknowledged (${roster.acknowledged.length}):`,
714
+ ...(roster.acknowledged.length
715
+ ? roster.acknowledged.map((entry) => ` ${entry.actor.id}`)
716
+ : [' none yet']),
717
+ `No acknowledgement recorded (${roster.outstanding.length}):`,
718
+ ...(roster.outstanding.length
719
+ ? roster.outstanding.map((entry) => ` ${entry.id}`)
720
+ : [' none']),
721
+ ];
722
+ if (roster.joinedSince > 0) {
723
+ lines.push(`${roster.joinedSince} agent(s) joined after this was posted.`);
724
+ }
725
+ output(io, global.json, roster, lines.join('\n'));
726
+ });
727
+ postCommand
728
+ .command('archive <post-id>')
729
+ .description('Archive a post you wrote')
730
+ .requiredOption('--as <actor-id>')
731
+ .option('--actor-kind <kind>', 'human, agent, or system', 'agent')
732
+ .option('--reason <text>')
733
+ .action(async (postId, options, command) => {
734
+ const global = globals(command);
735
+ const record = await withClient(global.home, actor(options.actorKind, options.as), (client) => client.posts.archive({ postId, ...(options.reason ? { reason: options.reason } : {}) }));
736
+ output(io, global.json, record, `Archived ${postId}`);
737
+ });
529
738
  const kudosCommand = program.command('kudos').description('Give and manage agent recognition');
530
739
  kudosCommand
531
740
  .command('give <recipient>')
@@ -555,7 +764,7 @@ export function createCli(io = defaultIo, serviceFactory = configuredServiceFact
555
764
  });
556
765
  program
557
766
  .command('inbox [agent]')
558
- .description('Show pending kudos, memos, and todos for an agent')
767
+ .description('Show pending kudos, memos, and tasks for an agent')
559
768
  .option('--as <agent-id>', 'defaults to the positional agent')
560
769
  .option('--limit <number>', 'maximum results (default 10, maximum 50)', '10')
561
770
  .option('--cursor <cursor>', 'opaque cursor returned by the previous page')
@@ -584,7 +793,7 @@ export function createCli(io = defaultIo, serviceFactory = configuredServiceFact
584
793
  program
585
794
  .command('list')
586
795
  .description('List compact summaries across all record types')
587
- .option('--kind <kind>', 'kudos, memo, note, or todo (repeatable)', collect, [])
796
+ .option('--kind <kind>', 'kudos, memo, note, or task (repeatable)', collect, [])
588
797
  .option('--participant <agent>')
589
798
  .option('--actor <id>')
590
799
  .option('--tag <tag>')
@@ -755,8 +964,100 @@ export function createCli(io = defaultIo, serviceFactory = configuredServiceFact
755
964
  }));
756
965
  output(io, global.json, record, `Archived note ${id}.`);
757
966
  });
758
- const todoCommand = program.command('todo').description('Create and manage agent todos');
967
+ const todoCommand = program
968
+ .command('todo')
969
+ .description('Create and manage your own private reminders');
759
970
  todoCommand
971
+ .command('create')
972
+ .requiredOption('--as <actor-id>')
973
+ .option('--actor-kind <kind>', 'human, agent, or system', 'agent')
974
+ .requiredOption('--title <title>')
975
+ .option('--details <text>', 'private working detail')
976
+ .option('--priority <number>', '1 highest, 4 lowest', '3')
977
+ .option('--due-date <date>')
978
+ .option('--due-at <datetime>')
979
+ .option('--time-zone <iana-zone>')
980
+ .option('--tag <tag>', 'tag (repeatable)', collect, [])
981
+ .option('--idempotency-key <key>')
982
+ .action(async (options, command) => {
983
+ const global = globals(command);
984
+ const result = await withClient(global.home, actor(options.actorKind, options.as), (client) => client.todos.create({
985
+ title: options.title,
986
+ ...(options.details ? { details: options.details } : {}),
987
+ priority: Number(options.priority),
988
+ ...((due) => (due ? { due } : {}))(taskDue(options)),
989
+ ...(options.tag.length ? { tags: options.tag } : {}),
990
+ ...(options.idempotencyKey ? { idempotencyKey: options.idempotencyKey } : {}),
991
+ }));
992
+ output(io, global.json, result, `${result.deduplicated ? 'Found existing' : 'Created'} private todo\nTitle: ${result.record.current.title}\nID: ${result.record.event.id}`);
993
+ });
994
+ todoCommand
995
+ .command('list')
996
+ .requiredOption('--as <actor-id>')
997
+ .option('--actor-kind <kind>', 'human, agent, or system', 'agent')
998
+ .option('--status <status>')
999
+ .option('--limit <number>', 'maximum results', '10')
1000
+ .action(async (options, command) => {
1001
+ const global = globals(command);
1002
+ const page = await withClient(global.home, actor(options.actorKind, options.as), (client) => client.todos.list({
1003
+ ...(options.status ? { status: options.status } : {}),
1004
+ limit: Number(options.limit),
1005
+ }));
1006
+ output(io, global.json, page, page.items.length
1007
+ ? page.items
1008
+ .map((item) => `${item.id} ${item.status.padEnd(9)} ${item.title}`)
1009
+ .join('\n')
1010
+ : 'No todos.');
1011
+ });
1012
+ todoCommand
1013
+ .command('show <todo-id>')
1014
+ .requiredOption('--as <actor-id>')
1015
+ .option('--actor-kind <kind>', 'human, agent, or system', 'agent')
1016
+ .action(async (id, options, command) => {
1017
+ const global = globals(command);
1018
+ const record = await withClient(global.home, actor(options.actorKind, options.as), (client) => client.todos.get(id));
1019
+ output(io, global.json, record, `${record.current.title}\nStatus: ${record.status}\nPriority: ${record.current.priority}\nVersion: ${record.current.version}${record.current.details ? `\n\n${record.current.details}` : ''}`);
1020
+ });
1021
+ for (const operation of ['complete', 'reopen', 'cancel', 'archive']) {
1022
+ todoCommand
1023
+ .command(`${operation} <todo-id>`)
1024
+ .requiredOption('--as <actor-id>')
1025
+ .option('--actor-kind <kind>', 'human, agent, or system', 'agent')
1026
+ .option('--note <text>')
1027
+ .option('--reason <text>')
1028
+ .option('--idempotency-key <key>')
1029
+ .action(async (id, options, command) => {
1030
+ const global = globals(command);
1031
+ const record = await withClient(global.home, actor(options.actorKind, options.as), (client) => operation === 'complete'
1032
+ ? client.todos.complete({
1033
+ todoId: id,
1034
+ ...(options.note ? { note: options.note } : {}),
1035
+ ...(options.idempotencyKey ? { idempotencyKey: options.idempotencyKey } : {}),
1036
+ })
1037
+ : operation === 'cancel'
1038
+ ? client.todos.cancel({
1039
+ todoId: id,
1040
+ ...(options.reason ? { reason: options.reason } : {}),
1041
+ ...(options.idempotencyKey ? { idempotencyKey: options.idempotencyKey } : {}),
1042
+ })
1043
+ : operation === 'archive'
1044
+ ? client.todos.archive({
1045
+ todoId: id,
1046
+ ...(options.idempotencyKey
1047
+ ? { idempotencyKey: options.idempotencyKey }
1048
+ : {}),
1049
+ })
1050
+ : client.todos.reopen({
1051
+ todoId: id,
1052
+ ...(options.idempotencyKey
1053
+ ? { idempotencyKey: options.idempotencyKey }
1054
+ : {}),
1055
+ }));
1056
+ output(io, global.json, record, `Todo ${id} is now ${record.status}.`);
1057
+ });
1058
+ }
1059
+ const taskCommand = program.command('task').description('Create and manage agent tasks');
1060
+ taskCommand
760
1061
  .command('create <assignee>')
761
1062
  .requiredOption('--from <actor-id>')
762
1063
  .option('--actor-kind <kind>', 'human, agent, or system', 'agent')
@@ -771,39 +1072,39 @@ export function createCli(io = defaultIo, serviceFactory = configuredServiceFact
771
1072
  .option('--idempotency-key <key>')
772
1073
  .action(async (assignee, options, command) => {
773
1074
  const global = globals(command);
774
- const result = await withClient(global.home, actor(options.actorKind, options.from), (client) => client.todos.create({
1075
+ const result = await withClient(global.home, actor(options.actorKind, options.from), (client) => client.tasks.create({
775
1076
  assigneeAgentId: assignee,
776
1077
  title: options.title,
777
1078
  ...(options.description ? { description: options.description } : {}),
778
1079
  priority: Number(options.priority),
779
- ...((due) => (due ? { due } : {}))(todoDue(options)),
1080
+ ...((due) => (due ? { due } : {}))(taskDue(options)),
780
1081
  ...(options.tag.length ? { tags: options.tag } : {}),
781
1082
  visibility: options.visibility,
782
1083
  ...(options.idempotencyKey ? { idempotencyKey: options.idempotencyKey } : {}),
783
1084
  }));
784
- output(io, global.json, result, `${result.deduplicated ? 'Found existing' : 'Created'} todo for ${result.record.event.assigneeDisplayName}\nTitle: ${result.record.current.title}\nID: ${result.record.event.id}`);
1085
+ output(io, global.json, result, `${result.deduplicated ? 'Found existing' : 'Created'} task for ${result.record.event.assigneeDisplayName}\nTitle: ${result.record.current.title}\nID: ${result.record.event.id}`);
785
1086
  });
786
- todoCommand
1087
+ taskCommand
787
1088
  .command('list')
788
1089
  .option('--assignee <agent>')
789
1090
  .option('--status <status>')
790
1091
  .option('--limit <number>', 'maximum results', '10')
791
1092
  .action(async (options, command) => {
792
1093
  const global = globals(command);
793
- const page = await withClient(global.home, defaultActor(env, 'human', 'local-cli'), (client) => client.todos.list({
1094
+ const page = await withClient(global.home, defaultActor(env, 'human', 'local-cli'), (client) => client.tasks.list({
794
1095
  ...(options.assignee ? { participantAgentId: options.assignee } : {}),
795
1096
  ...(options.status ? { status: options.status } : {}),
796
1097
  limit: Number(options.limit),
797
1098
  }));
798
- output(io, global.json, page, page.items.map(lineForItem).join('\n') || 'No todos found.');
1099
+ output(io, global.json, page, page.items.map(lineForItem).join('\n') || 'No tasks found.');
799
1100
  });
800
- todoCommand.command('show <todo-id>').action(async (id, _options, command) => {
1101
+ taskCommand.command('show <task-id>').action(async (id, _options, command) => {
801
1102
  const global = globals(command);
802
- const record = await withClient(global.home, defaultActor(env, 'human', 'local-cli'), (client) => client.todos.get(id));
1103
+ const record = await withClient(global.home, defaultActor(env, 'human', 'local-cli'), (client) => client.tasks.get(id));
803
1104
  output(io, global.json, record, `${record.current.title}\nID: ${record.event.id}\nStatus: ${record.status}\nVersion: ${record.current.version}`);
804
1105
  });
805
- todoCommand
806
- .command('update <todo-id>')
1106
+ taskCommand
1107
+ .command('update <task-id>')
807
1108
  .requiredOption('--as <actor-id>')
808
1109
  .option('--actor-kind <kind>', 'agent or human', 'agent')
809
1110
  .requiredOption('--expected-version <number>')
@@ -818,9 +1119,9 @@ export function createCli(io = defaultIo, serviceFactory = configuredServiceFact
818
1119
  .option('--idempotency-key <key>')
819
1120
  .action(async (id, options, command) => {
820
1121
  const global = globals(command);
821
- const parsedDue = todoDue(options);
822
- const record = await withClient(global.home, actor(options.actorKind, options.as), (client) => client.todos.update({
823
- todoId: id,
1122
+ const parsedDue = taskDue(options);
1123
+ const record = await withClient(global.home, actor(options.actorKind, options.as), (client) => client.tasks.update({
1124
+ taskId: id,
824
1125
  expectedVersion: Number(options.expectedVersion),
825
1126
  ...(options.title ? { title: options.title } : {}),
826
1127
  ...(options.description !== undefined ? { description: options.description } : {}),
@@ -829,52 +1130,59 @@ export function createCli(io = defaultIo, serviceFactory = configuredServiceFact
829
1130
  ...(options.visibility ? { visibility: options.visibility } : {}),
830
1131
  ...(options.idempotencyKey ? { idempotencyKey: options.idempotencyKey } : {}),
831
1132
  }));
832
- output(io, global.json, record, `Updated todo ${id} to version ${record.current.version}.`);
1133
+ output(io, global.json, record, `Updated task ${id} to version ${record.current.version}.`);
833
1134
  });
834
1135
  for (const operation of ['accept', 'reject', 'complete', 'reopen', 'cancel']) {
835
- todoCommand
836
- .command(`${operation} <todo-id>`)
1136
+ const command_ = taskCommand
1137
+ .command(`${operation} <task-id>`)
837
1138
  .requiredOption('--as <actor-id>')
838
1139
  .option('--actor-kind <kind>', 'agent or human', 'agent')
839
1140
  .option('--note <text>')
840
1141
  .option('--reason <text>')
841
- .option('--idempotency-key <key>')
842
- .action(async (id, options, command) => {
1142
+ .option('--idempotency-key <key>');
1143
+ // Rejecting requires saying why; accepting may. Marked required at the
1144
+ // parser so the CLI refuses before touching the store.
1145
+ if (operation === 'reject') {
1146
+ command_.requiredOption('--response <text>', 'why the task is being refused');
1147
+ }
1148
+ else if (operation === 'accept') {
1149
+ command_.option('--response <text>', 'conditions, timing, or partial capability');
1150
+ }
1151
+ command_.action(async (id, options, command) => {
843
1152
  const global = globals(command);
844
1153
  const record = await withClient(global.home, actor(options.actorKind, options.as), (client) => operation === 'accept'
845
- ? client.todos.accept({
846
- todoId: id,
1154
+ ? client.tasks.accept({
1155
+ taskId: id,
1156
+ ...(options.response ? { response: options.response } : {}),
847
1157
  ...(options.idempotencyKey ? { idempotencyKey: options.idempotencyKey } : {}),
848
1158
  })
849
1159
  : operation === 'reject'
850
- ? client.todos.reject({
851
- todoId: id,
852
- ...(options.reason ? { reason: options.reason } : {}),
1160
+ ? client.tasks.reject({
1161
+ taskId: id,
1162
+ response: options.response ?? options.reason ?? '',
853
1163
  ...(options.idempotencyKey ? { idempotencyKey: options.idempotencyKey } : {}),
854
1164
  })
855
1165
  : operation === 'complete'
856
- ? client.todos.complete({
857
- todoId: id,
1166
+ ? client.tasks.complete({
1167
+ taskId: id,
858
1168
  ...(options.note ? { note: options.note } : {}),
859
- ...(options.idempotencyKey
860
- ? { idempotencyKey: options.idempotencyKey }
861
- : {}),
1169
+ ...(options.idempotencyKey ? { idempotencyKey: options.idempotencyKey } : {}),
862
1170
  })
863
1171
  : operation === 'cancel'
864
- ? client.todos.cancel({
865
- todoId: id,
1172
+ ? client.tasks.cancel({
1173
+ taskId: id,
866
1174
  ...(options.reason ? { reason: options.reason } : {}),
867
1175
  ...(options.idempotencyKey
868
1176
  ? { idempotencyKey: options.idempotencyKey }
869
1177
  : {}),
870
1178
  })
871
- : client.todos.reopen({
872
- todoId: id,
1179
+ : client.tasks.reopen({
1180
+ taskId: id,
873
1181
  ...(options.idempotencyKey
874
1182
  ? { idempotencyKey: options.idempotencyKey }
875
1183
  : {}),
876
1184
  }));
877
- output(io, global.json, record, `Todo ${id} is ${record.status}.`);
1185
+ output(io, global.json, record, `Task ${id} is ${record.status}.`);
878
1186
  });
879
1187
  }
880
1188
  kudosCommand
@@ -1011,14 +1319,34 @@ export function createCli(io = defaultIo, serviceFactory = configuredServiceFact
1011
1319
  program
1012
1320
  .command('mcp')
1013
1321
  .description('Run the actor-bound MCP server over stdio')
1014
- .requiredOption('--actor-id <id>')
1015
- .requiredOption('--actor-kind <kind>', 'human, agent, or system')
1016
- .option('--actor-name <display-name>')
1322
+ .option('--agent-id <id>', 'bound agent, whose identity is read from Synomem')
1323
+ .option('--actor-id <id>', 'bound non-agent actor ID')
1324
+ .option('--actor-kind <kind>', 'human or system')
1325
+ .option('--actor-name <display-name>', 'display name for a non-agent actor')
1017
1326
  .action(async (options, command) => {
1018
1327
  const global = globals(command);
1328
+ // An agent's name comes from its profile, never from the command line:
1329
+ // the name is written into every event the session appends, and a
1330
+ // harness must not be able to sign another agent's name to work.
1331
+ const bound = options.agentId
1332
+ ? await withClient(global.home, defaultActor(env, 'system', 'cli'), async (client) => {
1333
+ const resolution = await client.agents.resolve(options.agentId);
1334
+ if (!resolution.match) {
1335
+ throw new SynomemError('AGENT_NOT_FOUND', resolution.candidates.length
1336
+ ? `"${options.agentId}" matches ${resolution.candidates.length} agents: ${resolution.candidates
1337
+ .map((candidate) => candidate.id)
1338
+ .join(', ')}. Name one of them.`
1339
+ : `Unknown agent: ${options.agentId}`);
1340
+ }
1341
+ return actor('agent', resolution.match.id, resolution.match.displayName);
1342
+ })
1343
+ : undefined;
1344
+ if (!bound && !(options.actorId && options.actorKind)) {
1345
+ throw new SynomemError('INVALID_INPUT', 'Specify --agent-id, or --actor-id with --actor-kind for a non-agent actor.');
1346
+ }
1019
1347
  await startMcpServer({
1020
1348
  ...(global.home ? { home: global.home } : {}),
1021
- actor: actor(options.actorKind, options.actorId, options.actorName),
1349
+ actor: bound ?? actor(options.actorKind, options.actorId, options.actorName),
1022
1350
  });
1023
1351
  });
1024
1352
  return program;