synomem 0.1.1 → 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 +28 -0
  3. package/README.md +26 -13
  4. package/dist/cli.d.ts.map +1 -1
  5. package/dist/cli.js +280 -51
  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 +444 -95
  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/cli.ts CHANGED
@@ -34,7 +34,7 @@ import type {
34
34
  KudosSummary,
35
35
  ItemListInput,
36
36
  ItemSummary,
37
- TodoDue,
37
+ TaskDue,
38
38
  } from './types.js';
39
39
  import { packageVersion } from './version.js';
40
40
  import type { SynomemService, SynomemServiceFactory } from './service.js';
@@ -125,11 +125,11 @@ function defaultActor(
125
125
  return actor(env.SYNOMEM_ACTOR_KIND?.trim() || fallbackKind, id, env.SYNOMEM_ACTOR_NAME?.trim());
126
126
  }
127
127
 
128
- function todoDue(options: {
128
+ function taskDue(options: {
129
129
  dueDate?: string;
130
130
  dueAt?: string;
131
131
  timeZone?: string;
132
- }): TodoDue | undefined {
132
+ }): TaskDue | undefined {
133
133
  if (options.dueDate && options.dueAt)
134
134
  throw new SynomemError('INVALID_INPUT', 'Use due-date or due-at, not both.');
135
135
  if (options.dueDate) return { kind: 'date', date: options.dueDate };
@@ -637,29 +637,73 @@ export function createCli(
637
637
  .option('--yes', 'apply the displayed plan', false)
638
638
  .option('--force', 'replace a conflicting synomem directory', false)
639
639
  .option('--link', 'symlink to the packaged skill instead of copying it', false)
640
- .option('--actor-id <id>', 'print actor-bound MCP registration commands')
641
- .option('--actor-name <name>', 'display name used in MCP registration commands')
640
+ .option('--agent <id-or-alias>', 'bind this installation to an agent')
642
641
  .action(
643
- (
642
+ async (
644
643
  options: {
645
644
  runtime: string[];
646
645
  yes: boolean;
647
646
  force: boolean;
648
647
  link: boolean;
649
- actorId?: string;
650
- actorName?: string;
648
+ agent?: string;
651
649
  },
652
650
  command: Command,
653
651
  ) => {
654
652
  const global = globals(command);
653
+ const runtimes = skillRuntimes(options.runtime);
654
+
655
+ /*
656
+ * The agent is resolved BEFORE anything is written. An ambiguous or
657
+ * unknown name then stops the command with a name to fix, rather than
658
+ * leaving a skill installed and pointed at an agent that does not
659
+ * exist.
660
+ */
661
+ let agentId: string | undefined;
662
+ if (options.agent) {
663
+ agentId = await withClient(
664
+ global.home,
665
+ defaultActor(env, 'system', 'cli'),
666
+ async (client) => {
667
+ const resolution = await client.agents.resolve(options.agent!);
668
+ if (!resolution.match) {
669
+ throw new SynomemError(
670
+ 'AGENT_NOT_FOUND',
671
+ resolution.candidates.length
672
+ ? `"${options.agent}" matches ${resolution.candidates.length} agents: ${resolution.candidates
673
+ .map((candidate) => candidate.id)
674
+ .join(', ')}. Name one of them.`
675
+ : `Unknown agent: ${options.agent}`,
676
+ );
677
+ }
678
+ return resolution.match.id;
679
+ },
680
+ );
681
+ }
682
+
655
683
  const result = installSkill({
656
- runtimes: skillRuntimes(options.runtime),
684
+ ...(runtimes ? { runtimes } : {}),
657
685
  apply: options.yes,
658
686
  force: options.force,
659
687
  link: options.link,
660
- actorId: options.actorId,
661
- actorName: options.actorName,
688
+ ...(agentId ? { agentId } : {}),
662
689
  });
690
+
691
+ // Bindings follow what was actually installed, and only on a real run:
692
+ // a dry run must not claim a binding it did not make, and a runtime
693
+ // whose harness is not present here is not somewhere this agent runs.
694
+ if (agentId && options.yes) {
695
+ const installed = result.locations
696
+ .filter((location) => location.state !== 'unavailable')
697
+ .map((location) => location.runtime);
698
+ if (installed.length) {
699
+ await withClient(global.home, defaultActor(env, 'system', 'cli'), async (client) => {
700
+ for (const runtime of installed) {
701
+ await client.agents.bindRuntime({ agentId, runtime });
702
+ }
703
+ });
704
+ }
705
+ }
706
+
663
707
  output(io, global.json, result, formatSkillResult(result, 'install'));
664
708
  },
665
709
  );
@@ -668,19 +712,15 @@ export function createCli(
668
712
  .command('status')
669
713
  .description('Show installed, stale, missing, or conflicting skill copies')
670
714
  .option('--runtime <runtime>', `${skillRuntimeHelp} (repeatable)`, collect, [])
671
- .option('--actor-id <id>', 'print actor-bound MCP registration commands')
672
- .option('--actor-name <name>', 'display name used in MCP registration commands')
673
- .action(
674
- (options: { runtime: string[]; actorId?: string; actorName?: string }, command: Command) => {
675
- const global = globals(command);
676
- const result = skillStatus({
677
- runtimes: skillRuntimes(options.runtime),
678
- actorId: options.actorId,
679
- actorName: options.actorName,
680
- });
681
- output(io, global.json, result, formatSkillResult(result, 'status'));
682
- },
683
- );
715
+ .option('--agent <id>', 'print the registration command for this agent')
716
+ .action((options: { runtime: string[]; agent?: string }, command: Command) => {
717
+ const global = globals(command);
718
+ const result = skillStatus({
719
+ runtimes: skillRuntimes(options.runtime),
720
+ ...(options.agent ? { agentId: options.agent } : {}),
721
+ });
722
+ output(io, global.json, result, formatSkillResult(result, 'status'));
723
+ });
684
724
 
685
725
  skillCommand
686
726
  .command('uninstall')
@@ -762,6 +802,127 @@ export function createCli(
762
802
  },
763
803
  );
764
804
 
805
+ agentCommand
806
+ .command('resolve <name>')
807
+ .description('Resolve a name or alias to one agent, or list the candidates')
808
+ .action(async (name: string, _options, command: Command) => {
809
+ const global = globals(command);
810
+ const resolution = await withClient(
811
+ global.home,
812
+ defaultActor(env, 'system', 'cli'),
813
+ (client) => client.agents.resolve(name),
814
+ );
815
+ // An ambiguous name is a question, not a failure: exit zero and show the
816
+ // candidates so the caller can pick one.
817
+ const human = resolution.match
818
+ ? `${resolution.match.displayName} (${resolution.match.id})`
819
+ : resolution.candidates.length
820
+ ? `"${resolution.query}" is ambiguous. Candidates:\n${resolution.candidates
821
+ .map((profile) => ` ${profile.id} ${profile.displayName}`)
822
+ .join('\n')}`
823
+ : `No agent answers to "${resolution.query}".`;
824
+ output(io, global.json, resolution, human);
825
+ });
826
+
827
+ agentCommand
828
+ .command('directory')
829
+ .description('List agents with their runtime bindings')
830
+ .action(async (_options, command: Command) => {
831
+ const global = globals(command);
832
+ const entries = await withClient(global.home, defaultActor(env, 'system', 'cli'), (client) =>
833
+ client.agents.directory(),
834
+ );
835
+ const human = entries.length
836
+ ? entries
837
+ .map((entry) => {
838
+ const runtimes = entry.runtimeBindings.length
839
+ ? entry.runtimeBindings
840
+ .map(
841
+ (binding) =>
842
+ ` ${binding.runtime}${binding.profile ? `/${binding.profile}` : ''}` +
843
+ // Last-seen is advisory, so it is labelled as an
844
+ // observation rather than a status.
845
+ `${binding.lastSeenAt ? ` last seen ${binding.lastSeenAt}` : ' not yet seen'}`,
846
+ )
847
+ .join('\n')
848
+ : ' no runtime bindings';
849
+ return `${entry.profile.id} ${entry.profile.displayName}\n${runtimes}`;
850
+ })
851
+ .join('\n')
852
+ : 'No agents configured.';
853
+ output(io, global.json, { entries }, human);
854
+ });
855
+
856
+ const runtimeCommand = agentCommand.command('runtime').description('Record where an agent runs');
857
+
858
+ runtimeCommand
859
+ .command('bind <agent>')
860
+ .description('Bind an agent to a runtime')
861
+ .requiredOption('--runtime <name>', 'runtime family, e.g. claude-code')
862
+ .option('--profile <name>', 'named configuration within the runtime')
863
+ .option('--installation <id>', 'hosted installation this binding belongs to')
864
+ .action(
865
+ async (
866
+ agent: string,
867
+ options: { runtime: string; profile?: string; installation?: string },
868
+ command: Command,
869
+ ) => {
870
+ const global = globals(command);
871
+ const binding = await withClient(
872
+ global.home,
873
+ defaultActor(env, 'system', 'cli'),
874
+ (client) =>
875
+ client.agents.bindRuntime({
876
+ agentId: agent,
877
+ runtime: options.runtime,
878
+ ...(options.profile ? { profile: options.profile } : {}),
879
+ ...(options.installation ? { installationId: options.installation } : {}),
880
+ }),
881
+ );
882
+ output(
883
+ io,
884
+ global.json,
885
+ binding,
886
+ `Bound ${binding.agentId} to ${binding.runtime}${binding.profile ? `/${binding.profile}` : ''} (${binding.id})`,
887
+ );
888
+ },
889
+ );
890
+
891
+ runtimeCommand
892
+ .command('list <agent>')
893
+ .description('List an agent runtime bindings')
894
+ .action(async (agent: string, _options, command: Command) => {
895
+ const global = globals(command);
896
+ const bindings = await withClient(global.home, defaultActor(env, 'system', 'cli'), (client) =>
897
+ client.agents.bindings(agent),
898
+ );
899
+ const human = bindings.length
900
+ ? bindings
901
+ .map(
902
+ (binding) =>
903
+ `${binding.id} ${binding.runtime}${binding.profile ? `/${binding.profile}` : ''} bound ${binding.boundAt}`,
904
+ )
905
+ .join('\n')
906
+ : 'No runtime bindings.';
907
+ output(io, global.json, { bindings }, human);
908
+ });
909
+
910
+ runtimeCommand
911
+ .command('unbind <binding-id>')
912
+ .description('Remove a runtime binding')
913
+ .action(async (bindingId: string, _options, command: Command) => {
914
+ const global = globals(command);
915
+ const removed = await withClient(global.home, defaultActor(env, 'system', 'cli'), (client) =>
916
+ client.agents.unbindRuntime(bindingId),
917
+ );
918
+ output(
919
+ io,
920
+ global.json,
921
+ { removed },
922
+ removed ? `Removed binding ${bindingId}.` : `No binding ${bindingId}.`,
923
+ );
924
+ });
925
+
765
926
  const kudosCommand = program.command('kudos').description('Give and manage agent recognition');
766
927
 
767
928
  kudosCommand
@@ -819,7 +980,7 @@ export function createCli(
819
980
 
820
981
  program
821
982
  .command('inbox [agent]')
822
- .description('Show pending kudos, memos, and todos for an agent')
983
+ .description('Show pending kudos, memos, and tasks for an agent')
823
984
  .option('--as <agent-id>', 'defaults to the positional agent')
824
985
  .option('--limit <number>', 'maximum results (default 10, maximum 50)', '10')
825
986
  .option('--cursor <cursor>', 'opaque cursor returned by the previous page')
@@ -876,7 +1037,7 @@ export function createCli(
876
1037
  program
877
1038
  .command('list')
878
1039
  .description('List compact summaries across all record types')
879
- .option('--kind <kind>', 'kudos, memo, note, or todo (repeatable)', collect, [])
1040
+ .option('--kind <kind>', 'kudos, memo, note, or task (repeatable)', collect, [])
880
1041
  .option('--participant <agent>')
881
1042
  .option('--actor <id>')
882
1043
  .option('--tag <tag>')
@@ -1194,8 +1355,163 @@ export function createCli(
1194
1355
  },
1195
1356
  );
1196
1357
 
1197
- const todoCommand = program.command('todo').description('Create and manage agent todos');
1358
+ const todoCommand = program
1359
+ .command('todo')
1360
+ .description('Create and manage your own private reminders');
1198
1361
  todoCommand
1362
+ .command('create')
1363
+ .requiredOption('--as <actor-id>')
1364
+ .option('--actor-kind <kind>', 'human, agent, or system', 'agent')
1365
+ .requiredOption('--title <title>')
1366
+ .option('--details <text>', 'private working detail')
1367
+ .option('--priority <number>', '1 highest, 4 lowest', '3')
1368
+ .option('--due-date <date>')
1369
+ .option('--due-at <datetime>')
1370
+ .option('--time-zone <iana-zone>')
1371
+ .option('--tag <tag>', 'tag (repeatable)', collect, [])
1372
+ .option('--idempotency-key <key>')
1373
+ .action(
1374
+ async (
1375
+ options: {
1376
+ as: string;
1377
+ actorKind: string;
1378
+ title: string;
1379
+ details?: string;
1380
+ priority: string;
1381
+ dueDate?: string;
1382
+ dueAt?: string;
1383
+ timeZone?: string;
1384
+ tag: string[];
1385
+ idempotencyKey?: string;
1386
+ },
1387
+ command: Command,
1388
+ ) => {
1389
+ const global = globals(command);
1390
+ const result = await withClient(
1391
+ global.home,
1392
+ actor(options.actorKind, options.as),
1393
+ (client) =>
1394
+ client.todos.create({
1395
+ title: options.title,
1396
+ ...(options.details ? { details: options.details } : {}),
1397
+ priority: Number(options.priority) as 1 | 2 | 3 | 4,
1398
+ ...((due) => (due ? { due } : {}))(taskDue(options)),
1399
+ ...(options.tag.length ? { tags: options.tag } : {}),
1400
+ ...(options.idempotencyKey ? { idempotencyKey: options.idempotencyKey } : {}),
1401
+ }),
1402
+ );
1403
+ output(
1404
+ io,
1405
+ global.json,
1406
+ result,
1407
+ `${result.deduplicated ? 'Found existing' : 'Created'} private todo\nTitle: ${result.record.current.title}\nID: ${result.record.event.id}`,
1408
+ );
1409
+ },
1410
+ );
1411
+ todoCommand
1412
+ .command('list')
1413
+ .requiredOption('--as <actor-id>')
1414
+ .option('--actor-kind <kind>', 'human, agent, or system', 'agent')
1415
+ .option('--status <status>')
1416
+ .option('--limit <number>', 'maximum results', '10')
1417
+ .action(
1418
+ async (
1419
+ options: { as: string; actorKind: string; status?: string; limit: string },
1420
+ command: Command,
1421
+ ) => {
1422
+ const global = globals(command);
1423
+ const page = await withClient(global.home, actor(options.actorKind, options.as), (client) =>
1424
+ client.todos.list({
1425
+ ...(options.status ? { status: options.status } : {}),
1426
+ limit: Number(options.limit),
1427
+ }),
1428
+ );
1429
+ output(
1430
+ io,
1431
+ global.json,
1432
+ page,
1433
+ page.items.length
1434
+ ? page.items
1435
+ .map((item) => `${item.id} ${item.status.padEnd(9)} ${item.title}`)
1436
+ .join('\n')
1437
+ : 'No todos.',
1438
+ );
1439
+ },
1440
+ );
1441
+ todoCommand
1442
+ .command('show <todo-id>')
1443
+ .requiredOption('--as <actor-id>')
1444
+ .option('--actor-kind <kind>', 'human, agent, or system', 'agent')
1445
+ .action(async (id: string, options: { as: string; actorKind: string }, command: Command) => {
1446
+ const global = globals(command);
1447
+ const record = await withClient(global.home, actor(options.actorKind, options.as), (client) =>
1448
+ client.todos.get(id),
1449
+ );
1450
+ output(
1451
+ io,
1452
+ global.json,
1453
+ record,
1454
+ `${record.current.title}\nStatus: ${record.status}\nPriority: ${record.current.priority}\nVersion: ${record.current.version}${record.current.details ? `\n\n${record.current.details}` : ''}`,
1455
+ );
1456
+ });
1457
+ for (const operation of ['complete', 'reopen', 'cancel', 'archive'] as const) {
1458
+ todoCommand
1459
+ .command(`${operation} <todo-id>`)
1460
+ .requiredOption('--as <actor-id>')
1461
+ .option('--actor-kind <kind>', 'human, agent, or system', 'agent')
1462
+ .option('--note <text>')
1463
+ .option('--reason <text>')
1464
+ .option('--idempotency-key <key>')
1465
+ .action(
1466
+ async (
1467
+ id: string,
1468
+ options: {
1469
+ as: string;
1470
+ actorKind: string;
1471
+ note?: string;
1472
+ reason?: string;
1473
+ idempotencyKey?: string;
1474
+ },
1475
+ command: Command,
1476
+ ) => {
1477
+ const global = globals(command);
1478
+ const record = await withClient(
1479
+ global.home,
1480
+ actor(options.actorKind, options.as),
1481
+ (client) =>
1482
+ operation === 'complete'
1483
+ ? client.todos.complete({
1484
+ todoId: id,
1485
+ ...(options.note ? { note: options.note } : {}),
1486
+ ...(options.idempotencyKey ? { idempotencyKey: options.idempotencyKey } : {}),
1487
+ })
1488
+ : operation === 'cancel'
1489
+ ? client.todos.cancel({
1490
+ todoId: id,
1491
+ ...(options.reason ? { reason: options.reason } : {}),
1492
+ ...(options.idempotencyKey ? { idempotencyKey: options.idempotencyKey } : {}),
1493
+ })
1494
+ : operation === 'archive'
1495
+ ? client.todos.archive({
1496
+ todoId: id,
1497
+ ...(options.idempotencyKey
1498
+ ? { idempotencyKey: options.idempotencyKey }
1499
+ : {}),
1500
+ })
1501
+ : client.todos.reopen({
1502
+ todoId: id,
1503
+ ...(options.idempotencyKey
1504
+ ? { idempotencyKey: options.idempotencyKey }
1505
+ : {}),
1506
+ }),
1507
+ );
1508
+ output(io, global.json, record, `Todo ${id} is now ${record.status}.`);
1509
+ },
1510
+ );
1511
+ }
1512
+
1513
+ const taskCommand = program.command('task').description('Create and manage agent tasks');
1514
+ taskCommand
1199
1515
  .command('create <assignee>')
1200
1516
  .requiredOption('--from <actor-id>')
1201
1517
  .option('--actor-kind <kind>', 'human, agent, or system', 'agent')
@@ -1231,12 +1547,12 @@ export function createCli(
1231
1547
  global.home,
1232
1548
  actor(options.actorKind, options.from),
1233
1549
  (client) =>
1234
- client.todos.create({
1550
+ client.tasks.create({
1235
1551
  assigneeAgentId: assignee,
1236
1552
  title: options.title,
1237
1553
  ...(options.description ? { description: options.description } : {}),
1238
1554
  priority: Number(options.priority) as 1 | 2 | 3 | 4,
1239
- ...((due) => (due ? { due } : {}))(todoDue(options)),
1555
+ ...((due) => (due ? { due } : {}))(taskDue(options)),
1240
1556
  ...(options.tag.length ? { tags: options.tag } : {}),
1241
1557
  visibility: options.visibility,
1242
1558
  ...(options.idempotencyKey ? { idempotencyKey: options.idempotencyKey } : {}),
@@ -1246,11 +1562,11 @@ export function createCli(
1246
1562
  io,
1247
1563
  global.json,
1248
1564
  result,
1249
- `${result.deduplicated ? 'Found existing' : 'Created'} todo for ${result.record.event.assigneeDisplayName}\nTitle: ${result.record.current.title}\nID: ${result.record.event.id}`,
1565
+ `${result.deduplicated ? 'Found existing' : 'Created'} task for ${result.record.event.assigneeDisplayName}\nTitle: ${result.record.current.title}\nID: ${result.record.event.id}`,
1250
1566
  );
1251
1567
  },
1252
1568
  );
1253
- todoCommand
1569
+ taskCommand
1254
1570
  .command('list')
1255
1571
  .option('--assignee <agent>')
1256
1572
  .option('--status <status>')
@@ -1262,21 +1578,21 @@ export function createCli(
1262
1578
  global.home,
1263
1579
  defaultActor(env, 'human', 'local-cli'),
1264
1580
  (client) =>
1265
- client.todos.list({
1581
+ client.tasks.list({
1266
1582
  ...(options.assignee ? { participantAgentId: options.assignee } : {}),
1267
1583
  ...(options.status ? { status: options.status } : {}),
1268
1584
  limit: Number(options.limit),
1269
1585
  }),
1270
1586
  );
1271
- output(io, global.json, page, page.items.map(lineForItem).join('\n') || 'No todos found.');
1587
+ output(io, global.json, page, page.items.map(lineForItem).join('\n') || 'No tasks found.');
1272
1588
  },
1273
1589
  );
1274
- todoCommand.command('show <todo-id>').action(async (id: string, _options, command: Command) => {
1590
+ taskCommand.command('show <task-id>').action(async (id: string, _options, command: Command) => {
1275
1591
  const global = globals(command);
1276
1592
  const record = await withClient(
1277
1593
  global.home,
1278
1594
  defaultActor(env, 'human', 'local-cli'),
1279
- (client) => client.todos.get(id),
1595
+ (client) => client.tasks.get(id),
1280
1596
  );
1281
1597
  output(
1282
1598
  io,
@@ -1285,8 +1601,8 @@ export function createCli(
1285
1601
  `${record.current.title}\nID: ${record.event.id}\nStatus: ${record.status}\nVersion: ${record.current.version}`,
1286
1602
  );
1287
1603
  });
1288
- todoCommand
1289
- .command('update <todo-id>')
1604
+ taskCommand
1605
+ .command('update <task-id>')
1290
1606
  .requiredOption('--as <actor-id>')
1291
1607
  .option('--actor-kind <kind>', 'agent or human', 'agent')
1292
1608
  .requiredOption('--expected-version <number>')
@@ -1319,13 +1635,13 @@ export function createCli(
1319
1635
  command: Command,
1320
1636
  ) => {
1321
1637
  const global = globals(command);
1322
- const parsedDue = todoDue(options);
1638
+ const parsedDue = taskDue(options);
1323
1639
  const record = await withClient(
1324
1640
  global.home,
1325
1641
  actor(options.actorKind, options.as),
1326
1642
  (client) =>
1327
- client.todos.update({
1328
- todoId: id,
1643
+ client.tasks.update({
1644
+ taskId: id,
1329
1645
  expectedVersion: Number(options.expectedVersion),
1330
1646
  ...(options.title ? { title: options.title } : {}),
1331
1647
  ...(options.description !== undefined ? { description: options.description } : {}),
@@ -1335,71 +1651,78 @@ export function createCli(
1335
1651
  ...(options.idempotencyKey ? { idempotencyKey: options.idempotencyKey } : {}),
1336
1652
  }),
1337
1653
  );
1338
- output(io, global.json, record, `Updated todo ${id} to version ${record.current.version}.`);
1654
+ output(io, global.json, record, `Updated task ${id} to version ${record.current.version}.`);
1339
1655
  },
1340
1656
  );
1341
1657
  for (const operation of ['accept', 'reject', 'complete', 'reopen', 'cancel'] as const) {
1342
- todoCommand
1343
- .command(`${operation} <todo-id>`)
1658
+ const command_ = taskCommand
1659
+ .command(`${operation} <task-id>`)
1344
1660
  .requiredOption('--as <actor-id>')
1345
1661
  .option('--actor-kind <kind>', 'agent or human', 'agent')
1346
1662
  .option('--note <text>')
1347
1663
  .option('--reason <text>')
1348
- .option('--idempotency-key <key>')
1349
- .action(
1350
- async (
1351
- id: string,
1352
- options: {
1353
- as: string;
1354
- actorKind: string;
1355
- note?: string;
1356
- reason?: string;
1357
- idempotencyKey?: string;
1358
- },
1359
- command: Command,
1360
- ) => {
1361
- const global = globals(command);
1362
- const record = await withClient(
1363
- global.home,
1364
- actor(options.actorKind, options.as),
1365
- (client) =>
1366
- operation === 'accept'
1367
- ? client.todos.accept({
1368
- todoId: id,
1664
+ .option('--idempotency-key <key>');
1665
+ // Rejecting requires saying why; accepting may. Marked required at the
1666
+ // parser so the CLI refuses before touching the store.
1667
+ if (operation === 'reject') {
1668
+ command_.requiredOption('--response <text>', 'why the task is being refused');
1669
+ } else if (operation === 'accept') {
1670
+ command_.option('--response <text>', 'conditions, timing, or partial capability');
1671
+ }
1672
+ command_.action(
1673
+ async (
1674
+ id: string,
1675
+ options: {
1676
+ as: string;
1677
+ actorKind: string;
1678
+ note?: string;
1679
+ reason?: string;
1680
+ response?: string;
1681
+ idempotencyKey?: string;
1682
+ },
1683
+ command: Command,
1684
+ ) => {
1685
+ const global = globals(command);
1686
+ const record = await withClient(
1687
+ global.home,
1688
+ actor(options.actorKind, options.as),
1689
+ (client) =>
1690
+ operation === 'accept'
1691
+ ? client.tasks.accept({
1692
+ taskId: id,
1693
+ ...(options.response ? { response: options.response } : {}),
1694
+ ...(options.idempotencyKey ? { idempotencyKey: options.idempotencyKey } : {}),
1695
+ })
1696
+ : operation === 'reject'
1697
+ ? client.tasks.reject({
1698
+ taskId: id,
1699
+ response: options.response ?? options.reason ?? '',
1369
1700
  ...(options.idempotencyKey ? { idempotencyKey: options.idempotencyKey } : {}),
1370
1701
  })
1371
- : operation === 'reject'
1372
- ? client.todos.reject({
1373
- todoId: id,
1374
- ...(options.reason ? { reason: options.reason } : {}),
1702
+ : operation === 'complete'
1703
+ ? client.tasks.complete({
1704
+ taskId: id,
1705
+ ...(options.note ? { note: options.note } : {}),
1375
1706
  ...(options.idempotencyKey ? { idempotencyKey: options.idempotencyKey } : {}),
1376
1707
  })
1377
- : operation === 'complete'
1378
- ? client.todos.complete({
1379
- todoId: id,
1380
- ...(options.note ? { note: options.note } : {}),
1708
+ : operation === 'cancel'
1709
+ ? client.tasks.cancel({
1710
+ taskId: id,
1711
+ ...(options.reason ? { reason: options.reason } : {}),
1381
1712
  ...(options.idempotencyKey
1382
1713
  ? { idempotencyKey: options.idempotencyKey }
1383
1714
  : {}),
1384
1715
  })
1385
- : operation === 'cancel'
1386
- ? client.todos.cancel({
1387
- todoId: id,
1388
- ...(options.reason ? { reason: options.reason } : {}),
1389
- ...(options.idempotencyKey
1390
- ? { idempotencyKey: options.idempotencyKey }
1391
- : {}),
1392
- })
1393
- : client.todos.reopen({
1394
- todoId: id,
1395
- ...(options.idempotencyKey
1396
- ? { idempotencyKey: options.idempotencyKey }
1397
- : {}),
1398
- }),
1399
- );
1400
- output(io, global.json, record, `Todo ${id} is ${record.status}.`);
1401
- },
1402
- );
1716
+ : client.tasks.reopen({
1717
+ taskId: id,
1718
+ ...(options.idempotencyKey
1719
+ ? { idempotencyKey: options.idempotencyKey }
1720
+ : {}),
1721
+ }),
1722
+ );
1723
+ output(io, global.json, record, `Task ${id} is ${record.status}.`);
1724
+ },
1725
+ );
1403
1726
  }
1404
1727
 
1405
1728
  kudosCommand
@@ -1621,18 +1944,44 @@ export function createCli(
1621
1944
  program
1622
1945
  .command('mcp')
1623
1946
  .description('Run the actor-bound MCP server over stdio')
1624
- .requiredOption('--actor-id <id>')
1625
- .requiredOption('--actor-kind <kind>', 'human, agent, or system')
1626
- .option('--actor-name <display-name>')
1947
+ .option('--agent-id <id>', 'bound agent, whose identity is read from Synomem')
1948
+ .option('--actor-id <id>', 'bound non-agent actor ID')
1949
+ .option('--actor-kind <kind>', 'human or system')
1950
+ .option('--actor-name <display-name>', 'display name for a non-agent actor')
1627
1951
  .action(
1628
1952
  async (
1629
- options: { actorId: string; actorKind: string; actorName?: string },
1953
+ options: { agentId?: string; actorId?: string; actorKind?: string; actorName?: string },
1630
1954
  command: Command,
1631
1955
  ) => {
1632
1956
  const global = globals(command);
1957
+ // An agent's name comes from its profile, never from the command line:
1958
+ // the name is written into every event the session appends, and a
1959
+ // harness must not be able to sign another agent's name to work.
1960
+ const bound = options.agentId
1961
+ ? await withClient(global.home, defaultActor(env, 'system', 'cli'), async (client) => {
1962
+ const resolution = await client.agents.resolve(options.agentId!);
1963
+ if (!resolution.match) {
1964
+ throw new SynomemError(
1965
+ 'AGENT_NOT_FOUND',
1966
+ resolution.candidates.length
1967
+ ? `"${options.agentId}" matches ${resolution.candidates.length} agents: ${resolution.candidates
1968
+ .map((candidate) => candidate.id)
1969
+ .join(', ')}. Name one of them.`
1970
+ : `Unknown agent: ${options.agentId}`,
1971
+ );
1972
+ }
1973
+ return actor('agent', resolution.match.id, resolution.match.displayName);
1974
+ })
1975
+ : undefined;
1976
+ if (!bound && !(options.actorId && options.actorKind)) {
1977
+ throw new SynomemError(
1978
+ 'INVALID_INPUT',
1979
+ 'Specify --agent-id, or --actor-id with --actor-kind for a non-agent actor.',
1980
+ );
1981
+ }
1633
1982
  await startMcpServer({
1634
1983
  ...(global.home ? { home: global.home } : {}),
1635
- actor: actor(options.actorKind, options.actorId, options.actorName),
1984
+ actor: bound ?? actor(options.actorKind!, options.actorId!, options.actorName),
1636
1985
  });
1637
1986
  },
1638
1987
  );