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/ARCHITECTURE.md CHANGED
@@ -44,7 +44,7 @@ reconstruct one requested aggregate. Generated Markdown is never a machine query
44
44
  ## Authorization
45
45
 
46
46
  MCP servers bind one actor at startup, and tool inputs cannot override it. Humans have local
47
- administrative authority. Agents can manage their own notes, recipient memo state, and todos they
47
+ administrative authority. Agents can manage their own notes and todos, recipient memo state, and tasks they
48
48
  created or received. System actors have no implicit agent or administrator authority. Actor
49
49
  principals are keyed by both kind and ID; matching text IDs across kinds do not share author access.
50
50
 
@@ -55,7 +55,7 @@ The filesystem owner remains the ultimate local authority.
55
55
  ## Concurrency and durability
56
56
 
57
57
  SQLite uses WAL, foreign keys, `synchronous=FULL`, a bounded busy handler, and `BEGIN IMMEDIATE`
58
- transactions. Actor-scoped idempotency protects retries. Note and todo revisions use aggregate
58
+ transactions. Actor-scoped idempotency protects retries. Note, task, and todo revisions use aggregate
59
59
  versions so stale writes fail with `REVISION_CONFLICT` rather than overwriting concurrent state.
60
60
 
61
61
  V1 supports one machine and one filesystem owner. A future hosted service must enforce the same
package/CHANGELOG.md CHANGED
@@ -5,6 +5,34 @@ All notable changes will be documented here. The format follows
5
5
 
6
6
  ## [Unreleased]
7
7
 
8
+ ### Changed
9
+
10
+ - The MCP server binds to an agent with `--agent-id`, reading the display name and actor kind from
11
+ the agent's profile. `synomem skill install --agent <id-or-alias>` resolves the agent before
12
+ writing anything, records a runtime binding for each runtime it installs, and generates a
13
+ registration command carrying only the canonical ID.
14
+
15
+ ## [0.2.0] - 2026-09-05
16
+
17
+ ### Added
18
+
19
+ - Private todos as a distinct domain: a todo belongs to the agent that wrote it, is visible to no
20
+ one else, and cannot be assigned. Work meant for another agent is a task.
21
+ - Optional responses when accepting a task and required responses when rejecting one, so a refusal
22
+ always tells the assigner why.
23
+ - Discovery for work that has stalled: tasks awaiting a response and tasks past their deadline.
24
+ - Case-insensitive agent aliases with `agent resolve`, which returns candidates instead of guessing
25
+ when several agents answer to one name, and `agent directory`.
26
+ - Advisory runtime bindings recording where an agent was registered to run, with `agent runtime
27
+ bind`, `list`, and `unbind`, and the matching `synomem_agent_resolve` and
28
+ `synomem_agent_directory` MCP tools.
29
+
30
+ ### Changed
31
+
32
+ - Schema version 5 adds a normalized alias column with a unique index and an
33
+ `agent_runtime_bindings` table. Existing databases migrate in place.
34
+ - Aliases are stored folded to lowercase, so one name cannot be claimed twice in two casings.
35
+
8
36
  ## [0.1.1] - 2026-09-04
9
37
 
10
38
  ### Fixed
package/README.md CHANGED
@@ -69,11 +69,17 @@ synomem note create --as gracie \
69
69
  --title "Release invariant" \
70
70
  --body "Never publish without explicit maintainer authorization."
71
71
 
72
- synomem todo create codex \
72
+ synomem task create codex \
73
73
  --from gracie --title "Review the migration" --due-date 2026-09-15
74
74
 
75
75
  synomem inbox codex
76
- synomem todo accept <todo-id> --as codex
76
+ synomem task accept <task-id> --as codex --response "Starting after the tests."
77
+
78
+ # A todo is private to the agent that wrote it; nobody else can assign one.
79
+ synomem todo create --as codex --title "Re-read the migration notes"
80
+
81
+ synomem agent resolve Mike
82
+ synomem agent directory
77
83
  synomem list
78
84
  ```
79
85
 
@@ -87,15 +93,15 @@ terminal-capable agent. The Synomem package contains the portable
87
93
  installer for the six named local harnesses.
88
94
 
89
95
  ```text
90
- Set up Synomem for this agent and runtime. Synomem is a local-first coordination system for durable kudos, one-to-one memos, private agent notes, and consent-based assigned todos. It uses an append-only SQLite database under ~/.agents by default, an actor-bound stdio MCP server, and a portable Agent Skill. Multiple local agents may share the database, but every MCP server must be bound to its own stable identity.
96
+ Set up Synomem for this agent and runtime. Synomem is a local-first coordination system for durable kudos, one-to-one memos, private agent notes, consent-based assigned tasks, and private todos. It uses an append-only SQLite database under ~/.agents by default, an actor-bound stdio MCP server, and a portable Agent Skill. Multiple local agents may share the database, but every MCP server must be bound to its own stable identity.
91
97
 
92
98
  Work autonomously through the safe, reversible steps below. Do not expose secrets, overwrite unrelated configuration, invent an identity, use --force without my explicit approval, or modify another agent's integration.
93
99
 
94
100
  1. Verify Node.js 22.13+ and npm are available. Install or update the public package with `npm install --global synomem` if needed, then report `synomem --version`.
95
101
  2. Preserve an existing `SYNOMEM_HOME`; otherwise use the default ~/.agents. Run `synomem init`, then `synomem doctor`. Never point tests or experiments at another Synomem home.
96
102
  3. Run `synomem agent list`. Determine this agent's existing stable ID from the current harness or Synomem configuration and reuse it. If no identity is clearly established, ask me for the agent ID and display name before running `synomem agent create <id> --name <name>`. Never silently merge or rename identities.
97
- 4. Detect the current harness from actual local evidence and its installed CLI help. Use runtime `claude` for Claude Code, `codex` for Codex, `hermes` for Hermes, `openclaw` for OpenClaw, `cursor` for Cursor, or `grok` for local Grok Build (`grokbot` is accepted as an alias). Check `synomem skill install --help`, then preview with `synomem skill install --runtime <runtime> --actor-id <agent-id> --actor-name <display-name>`. Review the exact destination and apply the same command with `--yes`; it must report `current`. If the installed release does not yet list this runtime, locate the packaged source under the global npm root at `synomem/skills/synomem` and follow the verified destination and conflict rules in https://github.com/Coaden/synomem/blob/main/docs/skill.md instead. Do not guess a path, overwrite an existing skill, or create a fake harness home to make an unavailable runtime appear installed.
98
- 5. Inspect any actor-bound MCP registration command printed by the installer. Check the harness's existing MCP list/config first, then run the command only if `synomem` is absent or incorrect. Do not create duplicates. Cursor has no noninteractive MCP-add command: carefully merge a `synomem` stdio entry into its documented user `~/.cursor/mcp.json`, using command `synomem-mcp` and arguments `--actor-id <agent-id> --actor-kind agent --actor-name <display-name>`; preserve every existing entry.
103
+ 4. Detect the current harness from actual local evidence and its installed CLI help. Use runtime `claude` for Claude Code, `codex` for Codex, `hermes` for Hermes, `openclaw` for OpenClaw, `cursor` for Cursor, or `grok` for local Grok Build (`grokbot` is accepted as an alias). Check `synomem skill install --help`, then preview with `synomem skill install --runtime <runtime> --agent <agent-id>`. Review the exact destination and apply the same command with `--yes`; it must report `current`. If the installed release does not yet list this runtime, locate the packaged source under the global npm root at `synomem/skills/synomem` and follow the verified destination and conflict rules in https://github.com/Coaden/synomem/blob/main/docs/skill.md instead. Do not guess a path, overwrite an existing skill, or create a fake harness home to make an unavailable runtime appear installed.
104
+ 5. Inspect any actor-bound MCP registration command printed by the installer. Check the harness's existing MCP list/config first, then run the command only if `synomem` is absent or incorrect. Do not create duplicates. Cursor has no noninteractive MCP-add command: carefully merge a `synomem` stdio entry into its documented user `~/.cursor/mcp.json`, using command `synomem-mcp` and the single argument `--agent-id <agent-id>` (the display name and kind come from the agent's profile, so nothing in a shared config file asserts an identity); preserve every existing entry.
99
105
  6. Verify the harness can discover the installed skill and MCP server using its own list/status commands, then run `synomem doctor`. Start a new agent session if that harness does not live-reload a newly created skills directory.
100
106
  7. If this is hosted Grok Bot rather than local Grok Build, do not claim it shares the desktop's local SQLite database. Install the package and skill only inside a persistent terminal environment where `npm`, local stdio MCP, and ~/.grok are actually available. Otherwise provide the skill URL https://github.com/Coaden/synomem/blob/main/skills/synomem/SKILL.md and explain the unsupported boundary; do not expose the local database through a tunnel.
101
107
  8. Report the package version, stable actor ID, storage home, installed skill path, MCP registration and verification status, whether a new session is needed, and every file or configuration changed. Do not print record contents or environment values beyond the non-secret actor identity and home path.
@@ -130,13 +136,20 @@ await client.notes.revise({
130
136
  body: 'Never publish or create a release without explicit maintainer authorization.',
131
137
  });
132
138
 
133
- await client.todos.create({
139
+ // A task is assigned to someone else and needs their consent.
140
+ await client.tasks.create({
134
141
  assigneeAgentId: 'codex',
135
142
  title: 'Review the migration',
136
143
  due: { kind: 'date', date: '2026-09-15' },
137
144
  });
138
145
 
139
- const page = await client.items.list({ kinds: ['memo', 'todo'], limit: 10 });
146
+ // A todo is the agent's own reminder, visible to no one else.
147
+ await client.todos.create({
148
+ title: 'Re-read the migration notes',
149
+ due: { kind: 'date', date: '2026-09-14' },
150
+ });
151
+
152
+ const page = await client.items.list({ kinds: ['memo', 'task'], limit: 10 });
140
153
  const changes = await client.items.changes({ after: page.watermark });
141
154
 
142
155
  await client.close();
@@ -147,7 +160,7 @@ The library performs no filesystem work at import time and never terminates its
147
160
  ## Context-safe reads
148
161
 
149
162
  `client.items.list()` and MCP `synomem_list` return 10 compact summaries by default and at most 50.
150
- Summaries omit message bodies, kudos reasons and evidence, note bodies, todo descriptions, source,
163
+ Summaries omit message bodies, kudos reasons and evidence, note bodies, task and todo details, source,
151
164
  and metadata. Fetch one authorized detail record with `items.get(id)` or `synomem_get`.
152
165
 
153
166
  Incremental reads return at most 20 changes by default and 100 at most. List and change responses
@@ -176,9 +189,9 @@ bounded reads. See [the MCP guide](docs/mcp.md).
176
189
  The package includes [`skills/synomem`](skills/synomem). Installation is explicit and dry-run first:
177
190
 
178
191
  ```bash
179
- synomem skill install --runtime codex --actor-id codex --actor-name "Codex"
180
- synomem skill install --runtime codex --actor-id codex --actor-name "Codex" --yes
181
- synomem skill install --runtime hermes --actor-id mycroft --actor-name "Mycroft" --yes
192
+ synomem skill install --runtime codex --agent codex
193
+ synomem skill install --runtime codex --agent codex --yes
194
+ synomem skill install --runtime hermes --agent mycroft --yes
182
195
  synomem skill status
183
196
  ```
184
197
 
@@ -197,8 +210,8 @@ are `claude`, `codex`, `hermes`, `openclaw`, `cursor`, and `grok`; `grokbot` ali
197
210
  ├── profile.json
198
211
  ├── WINS.md
199
212
  ├── MEMORY.md
200
- ├── TODOS.md
201
- ├── inbox/{kudos,memos,todos}/
213
+ ├── TASKS.md
214
+ ├── inbox/{kudos,memos,tasks}/
202
215
  └── NOTES.md
203
216
  ```
204
217
 
package/dist/cli.d.ts.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"cli.d.ts","sourceRoot":"","sources":["../src/cli.ts"],"names":[],"mappings":";AAKA,OAAO,EAAE,OAAO,EAA0B,MAAM,WAAW,CAAC;AAE5D,OAAO,EAA0C,KAAK,eAAe,EAAE,MAAM,kBAAkB,CAAC;AAIhG,OAAO,EAA4C,KAAK,iBAAiB,EAAE,MAAM,YAAY,CAAC;AAE9F,OAAO,EAGL,KAAK,YAAY,EACjB,KAAK,aAAa,EAClB,KAAK,YAAY,EAClB,MAAM,aAAa,CAAC;AASrB,OAAO,KAAK,EACV,aAAa,EAQd,MAAM,YAAY,CAAC;AAEpB,OAAO,KAAK,EAAkB,qBAAqB,EAAE,MAAM,cAAc,CAAC;AAE1E,MAAM,WAAW,KAAK;IACpB,MAAM,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,IAAI,CAAC;IAC/B,MAAM,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,IAAI,CAAC;CAChC;AAED,MAAM,WAAW,eAAe;IAC9B,eAAe,CAAC,EAAE,eAAe,CAAC;IAClC,UAAU,CAAC,EAAE,CAAC,OAAO,EAAE,iBAAiB,KAAK,OAAO,CAAC,IAAI,CAAC,CAAC;IAC3D,GAAG,CAAC,EAAE,MAAM,CAAC,UAAU,CAAC;IACxB,sBAAsB,CAAC,EAAE,CAAC,OAAO,EAAE;QACjC,OAAO,EAAE,MAAM,CAAC;QAChB,WAAW,EAAE,MAAM,CAAC;QACpB,KAAK,EAAE,aAAa,CAAC;QACrB,SAAS,EAAE,MAAM,CAAC;QAClB,eAAe,EAAE,eAAe,CAAC;KAClC,KAAK,OAAO,CAAC,IAAI,CAAC,CAAC;IACpB,kBAAkB,CAAC,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,OAAO,CAAC,YAAY,CAAC,CAAC;IAC7D,YAAY,CAAC,EAAE,CAAC,OAAO,EAAE;QACvB,OAAO,EAAE,MAAM,CAAC;QAChB,WAAW,EAAE,MAAM,CAAC;QACpB,KAAK,EAAE,aAAa,CAAC;QACrB,MAAM,EAAE,YAAY,CAAC;QACrB,MAAM,CAAC,EAAE,MAAM,CAAC;KACjB,KAAK,OAAO,CAAC,aAAa,GAAG,YAAY,CAAC,CAAC;CAC7C;AAkND,wBAAgB,SAAS,CACvB,EAAE,GAAE,KAAiB,EACrB,cAAc,GAAE,qBAAgD,EAChE,YAAY,GAAE,eAAoB,GACjC,OAAO,CAi1CT;AAED,wBAAsB,MAAM,CAC1B,IAAI,WAAe,EACnB,EAAE,GAAE,KAAiB,EACrB,cAAc,GAAE,qBAAgD,EAChE,YAAY,GAAE,eAAoB,GACjC,OAAO,CAAC,MAAM,CAAC,CAqBjB"}
1
+ {"version":3,"file":"cli.d.ts","sourceRoot":"","sources":["../src/cli.ts"],"names":[],"mappings":";AAKA,OAAO,EAAE,OAAO,EAA0B,MAAM,WAAW,CAAC;AAE5D,OAAO,EAA0C,KAAK,eAAe,EAAE,MAAM,kBAAkB,CAAC;AAIhG,OAAO,EAA4C,KAAK,iBAAiB,EAAE,MAAM,YAAY,CAAC;AAE9F,OAAO,EAGL,KAAK,YAAY,EACjB,KAAK,aAAa,EAClB,KAAK,YAAY,EAClB,MAAM,aAAa,CAAC;AASrB,OAAO,KAAK,EACV,aAAa,EAQd,MAAM,YAAY,CAAC;AAEpB,OAAO,KAAK,EAAkB,qBAAqB,EAAE,MAAM,cAAc,CAAC;AAE1E,MAAM,WAAW,KAAK;IACpB,MAAM,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,IAAI,CAAC;IAC/B,MAAM,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,IAAI,CAAC;CAChC;AAED,MAAM,WAAW,eAAe;IAC9B,eAAe,CAAC,EAAE,eAAe,CAAC;IAClC,UAAU,CAAC,EAAE,CAAC,OAAO,EAAE,iBAAiB,KAAK,OAAO,CAAC,IAAI,CAAC,CAAC;IAC3D,GAAG,CAAC,EAAE,MAAM,CAAC,UAAU,CAAC;IACxB,sBAAsB,CAAC,EAAE,CAAC,OAAO,EAAE;QACjC,OAAO,EAAE,MAAM,CAAC;QAChB,WAAW,EAAE,MAAM,CAAC;QACpB,KAAK,EAAE,aAAa,CAAC;QACrB,SAAS,EAAE,MAAM,CAAC;QAClB,eAAe,EAAE,eAAe,CAAC;KAClC,KAAK,OAAO,CAAC,IAAI,CAAC,CAAC;IACpB,kBAAkB,CAAC,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,OAAO,CAAC,YAAY,CAAC,CAAC;IAC7D,YAAY,CAAC,EAAE,CAAC,OAAO,EAAE;QACvB,OAAO,EAAE,MAAM,CAAC;QAChB,WAAW,EAAE,MAAM,CAAC;QACpB,KAAK,EAAE,aAAa,CAAC;QACrB,MAAM,EAAE,YAAY,CAAC;QACrB,MAAM,CAAC,EAAE,MAAM,CAAC;KACjB,KAAK,OAAO,CAAC,aAAa,GAAG,YAAY,CAAC,CAAC;CAC7C;AAkND,wBAAgB,SAAS,CACvB,EAAE,GAAE,KAAiB,EACrB,cAAc,GAAE,qBAAgD,EAChE,YAAY,GAAE,eAAoB,GACjC,OAAO,CA8qDT;AAED,wBAAsB,MAAM,CAC1B,IAAI,WAAe,EACnB,EAAE,GAAE,KAAiB,EACrB,cAAc,GAAE,qBAAgD,EAChE,YAAY,GAAE,eAAoB,GACjC,OAAO,CAAC,MAAM,CAAC,CAqBjB"}
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,84 @@ 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
+ });
529
639
  const kudosCommand = program.command('kudos').description('Give and manage agent recognition');
530
640
  kudosCommand
531
641
  .command('give <recipient>')
@@ -555,7 +665,7 @@ export function createCli(io = defaultIo, serviceFactory = configuredServiceFact
555
665
  });
556
666
  program
557
667
  .command('inbox [agent]')
558
- .description('Show pending kudos, memos, and todos for an agent')
668
+ .description('Show pending kudos, memos, and tasks for an agent')
559
669
  .option('--as <agent-id>', 'defaults to the positional agent')
560
670
  .option('--limit <number>', 'maximum results (default 10, maximum 50)', '10')
561
671
  .option('--cursor <cursor>', 'opaque cursor returned by the previous page')
@@ -584,7 +694,7 @@ export function createCli(io = defaultIo, serviceFactory = configuredServiceFact
584
694
  program
585
695
  .command('list')
586
696
  .description('List compact summaries across all record types')
587
- .option('--kind <kind>', 'kudos, memo, note, or todo (repeatable)', collect, [])
697
+ .option('--kind <kind>', 'kudos, memo, note, or task (repeatable)', collect, [])
588
698
  .option('--participant <agent>')
589
699
  .option('--actor <id>')
590
700
  .option('--tag <tag>')
@@ -755,8 +865,100 @@ export function createCli(io = defaultIo, serviceFactory = configuredServiceFact
755
865
  }));
756
866
  output(io, global.json, record, `Archived note ${id}.`);
757
867
  });
758
- const todoCommand = program.command('todo').description('Create and manage agent todos');
868
+ const todoCommand = program
869
+ .command('todo')
870
+ .description('Create and manage your own private reminders');
759
871
  todoCommand
872
+ .command('create')
873
+ .requiredOption('--as <actor-id>')
874
+ .option('--actor-kind <kind>', 'human, agent, or system', 'agent')
875
+ .requiredOption('--title <title>')
876
+ .option('--details <text>', 'private working detail')
877
+ .option('--priority <number>', '1 highest, 4 lowest', '3')
878
+ .option('--due-date <date>')
879
+ .option('--due-at <datetime>')
880
+ .option('--time-zone <iana-zone>')
881
+ .option('--tag <tag>', 'tag (repeatable)', collect, [])
882
+ .option('--idempotency-key <key>')
883
+ .action(async (options, command) => {
884
+ const global = globals(command);
885
+ const result = await withClient(global.home, actor(options.actorKind, options.as), (client) => client.todos.create({
886
+ title: options.title,
887
+ ...(options.details ? { details: options.details } : {}),
888
+ priority: Number(options.priority),
889
+ ...((due) => (due ? { due } : {}))(taskDue(options)),
890
+ ...(options.tag.length ? { tags: options.tag } : {}),
891
+ ...(options.idempotencyKey ? { idempotencyKey: options.idempotencyKey } : {}),
892
+ }));
893
+ output(io, global.json, result, `${result.deduplicated ? 'Found existing' : 'Created'} private todo\nTitle: ${result.record.current.title}\nID: ${result.record.event.id}`);
894
+ });
895
+ todoCommand
896
+ .command('list')
897
+ .requiredOption('--as <actor-id>')
898
+ .option('--actor-kind <kind>', 'human, agent, or system', 'agent')
899
+ .option('--status <status>')
900
+ .option('--limit <number>', 'maximum results', '10')
901
+ .action(async (options, command) => {
902
+ const global = globals(command);
903
+ const page = await withClient(global.home, actor(options.actorKind, options.as), (client) => client.todos.list({
904
+ ...(options.status ? { status: options.status } : {}),
905
+ limit: Number(options.limit),
906
+ }));
907
+ output(io, global.json, page, page.items.length
908
+ ? page.items
909
+ .map((item) => `${item.id} ${item.status.padEnd(9)} ${item.title}`)
910
+ .join('\n')
911
+ : 'No todos.');
912
+ });
913
+ todoCommand
914
+ .command('show <todo-id>')
915
+ .requiredOption('--as <actor-id>')
916
+ .option('--actor-kind <kind>', 'human, agent, or system', 'agent')
917
+ .action(async (id, options, command) => {
918
+ const global = globals(command);
919
+ const record = await withClient(global.home, actor(options.actorKind, options.as), (client) => client.todos.get(id));
920
+ 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}` : ''}`);
921
+ });
922
+ for (const operation of ['complete', 'reopen', 'cancel', 'archive']) {
923
+ todoCommand
924
+ .command(`${operation} <todo-id>`)
925
+ .requiredOption('--as <actor-id>')
926
+ .option('--actor-kind <kind>', 'human, agent, or system', 'agent')
927
+ .option('--note <text>')
928
+ .option('--reason <text>')
929
+ .option('--idempotency-key <key>')
930
+ .action(async (id, options, command) => {
931
+ const global = globals(command);
932
+ const record = await withClient(global.home, actor(options.actorKind, options.as), (client) => operation === 'complete'
933
+ ? client.todos.complete({
934
+ todoId: id,
935
+ ...(options.note ? { note: options.note } : {}),
936
+ ...(options.idempotencyKey ? { idempotencyKey: options.idempotencyKey } : {}),
937
+ })
938
+ : operation === 'cancel'
939
+ ? client.todos.cancel({
940
+ todoId: id,
941
+ ...(options.reason ? { reason: options.reason } : {}),
942
+ ...(options.idempotencyKey ? { idempotencyKey: options.idempotencyKey } : {}),
943
+ })
944
+ : operation === 'archive'
945
+ ? client.todos.archive({
946
+ todoId: id,
947
+ ...(options.idempotencyKey
948
+ ? { idempotencyKey: options.idempotencyKey }
949
+ : {}),
950
+ })
951
+ : client.todos.reopen({
952
+ todoId: id,
953
+ ...(options.idempotencyKey
954
+ ? { idempotencyKey: options.idempotencyKey }
955
+ : {}),
956
+ }));
957
+ output(io, global.json, record, `Todo ${id} is now ${record.status}.`);
958
+ });
959
+ }
960
+ const taskCommand = program.command('task').description('Create and manage agent tasks');
961
+ taskCommand
760
962
  .command('create <assignee>')
761
963
  .requiredOption('--from <actor-id>')
762
964
  .option('--actor-kind <kind>', 'human, agent, or system', 'agent')
@@ -771,39 +973,39 @@ export function createCli(io = defaultIo, serviceFactory = configuredServiceFact
771
973
  .option('--idempotency-key <key>')
772
974
  .action(async (assignee, options, command) => {
773
975
  const global = globals(command);
774
- const result = await withClient(global.home, actor(options.actorKind, options.from), (client) => client.todos.create({
976
+ const result = await withClient(global.home, actor(options.actorKind, options.from), (client) => client.tasks.create({
775
977
  assigneeAgentId: assignee,
776
978
  title: options.title,
777
979
  ...(options.description ? { description: options.description } : {}),
778
980
  priority: Number(options.priority),
779
- ...((due) => (due ? { due } : {}))(todoDue(options)),
981
+ ...((due) => (due ? { due } : {}))(taskDue(options)),
780
982
  ...(options.tag.length ? { tags: options.tag } : {}),
781
983
  visibility: options.visibility,
782
984
  ...(options.idempotencyKey ? { idempotencyKey: options.idempotencyKey } : {}),
783
985
  }));
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}`);
986
+ 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
987
  });
786
- todoCommand
988
+ taskCommand
787
989
  .command('list')
788
990
  .option('--assignee <agent>')
789
991
  .option('--status <status>')
790
992
  .option('--limit <number>', 'maximum results', '10')
791
993
  .action(async (options, command) => {
792
994
  const global = globals(command);
793
- const page = await withClient(global.home, defaultActor(env, 'human', 'local-cli'), (client) => client.todos.list({
995
+ const page = await withClient(global.home, defaultActor(env, 'human', 'local-cli'), (client) => client.tasks.list({
794
996
  ...(options.assignee ? { participantAgentId: options.assignee } : {}),
795
997
  ...(options.status ? { status: options.status } : {}),
796
998
  limit: Number(options.limit),
797
999
  }));
798
- output(io, global.json, page, page.items.map(lineForItem).join('\n') || 'No todos found.');
1000
+ output(io, global.json, page, page.items.map(lineForItem).join('\n') || 'No tasks found.');
799
1001
  });
800
- todoCommand.command('show <todo-id>').action(async (id, _options, command) => {
1002
+ taskCommand.command('show <task-id>').action(async (id, _options, command) => {
801
1003
  const global = globals(command);
802
- const record = await withClient(global.home, defaultActor(env, 'human', 'local-cli'), (client) => client.todos.get(id));
1004
+ const record = await withClient(global.home, defaultActor(env, 'human', 'local-cli'), (client) => client.tasks.get(id));
803
1005
  output(io, global.json, record, `${record.current.title}\nID: ${record.event.id}\nStatus: ${record.status}\nVersion: ${record.current.version}`);
804
1006
  });
805
- todoCommand
806
- .command('update <todo-id>')
1007
+ taskCommand
1008
+ .command('update <task-id>')
807
1009
  .requiredOption('--as <actor-id>')
808
1010
  .option('--actor-kind <kind>', 'agent or human', 'agent')
809
1011
  .requiredOption('--expected-version <number>')
@@ -818,9 +1020,9 @@ export function createCli(io = defaultIo, serviceFactory = configuredServiceFact
818
1020
  .option('--idempotency-key <key>')
819
1021
  .action(async (id, options, command) => {
820
1022
  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,
1023
+ const parsedDue = taskDue(options);
1024
+ const record = await withClient(global.home, actor(options.actorKind, options.as), (client) => client.tasks.update({
1025
+ taskId: id,
824
1026
  expectedVersion: Number(options.expectedVersion),
825
1027
  ...(options.title ? { title: options.title } : {}),
826
1028
  ...(options.description !== undefined ? { description: options.description } : {}),
@@ -829,52 +1031,59 @@ export function createCli(io = defaultIo, serviceFactory = configuredServiceFact
829
1031
  ...(options.visibility ? { visibility: options.visibility } : {}),
830
1032
  ...(options.idempotencyKey ? { idempotencyKey: options.idempotencyKey } : {}),
831
1033
  }));
832
- output(io, global.json, record, `Updated todo ${id} to version ${record.current.version}.`);
1034
+ output(io, global.json, record, `Updated task ${id} to version ${record.current.version}.`);
833
1035
  });
834
1036
  for (const operation of ['accept', 'reject', 'complete', 'reopen', 'cancel']) {
835
- todoCommand
836
- .command(`${operation} <todo-id>`)
1037
+ const command_ = taskCommand
1038
+ .command(`${operation} <task-id>`)
837
1039
  .requiredOption('--as <actor-id>')
838
1040
  .option('--actor-kind <kind>', 'agent or human', 'agent')
839
1041
  .option('--note <text>')
840
1042
  .option('--reason <text>')
841
- .option('--idempotency-key <key>')
842
- .action(async (id, options, command) => {
1043
+ .option('--idempotency-key <key>');
1044
+ // Rejecting requires saying why; accepting may. Marked required at the
1045
+ // parser so the CLI refuses before touching the store.
1046
+ if (operation === 'reject') {
1047
+ command_.requiredOption('--response <text>', 'why the task is being refused');
1048
+ }
1049
+ else if (operation === 'accept') {
1050
+ command_.option('--response <text>', 'conditions, timing, or partial capability');
1051
+ }
1052
+ command_.action(async (id, options, command) => {
843
1053
  const global = globals(command);
844
1054
  const record = await withClient(global.home, actor(options.actorKind, options.as), (client) => operation === 'accept'
845
- ? client.todos.accept({
846
- todoId: id,
1055
+ ? client.tasks.accept({
1056
+ taskId: id,
1057
+ ...(options.response ? { response: options.response } : {}),
847
1058
  ...(options.idempotencyKey ? { idempotencyKey: options.idempotencyKey } : {}),
848
1059
  })
849
1060
  : operation === 'reject'
850
- ? client.todos.reject({
851
- todoId: id,
852
- ...(options.reason ? { reason: options.reason } : {}),
1061
+ ? client.tasks.reject({
1062
+ taskId: id,
1063
+ response: options.response ?? options.reason ?? '',
853
1064
  ...(options.idempotencyKey ? { idempotencyKey: options.idempotencyKey } : {}),
854
1065
  })
855
1066
  : operation === 'complete'
856
- ? client.todos.complete({
857
- todoId: id,
1067
+ ? client.tasks.complete({
1068
+ taskId: id,
858
1069
  ...(options.note ? { note: options.note } : {}),
859
- ...(options.idempotencyKey
860
- ? { idempotencyKey: options.idempotencyKey }
861
- : {}),
1070
+ ...(options.idempotencyKey ? { idempotencyKey: options.idempotencyKey } : {}),
862
1071
  })
863
1072
  : operation === 'cancel'
864
- ? client.todos.cancel({
865
- todoId: id,
1073
+ ? client.tasks.cancel({
1074
+ taskId: id,
866
1075
  ...(options.reason ? { reason: options.reason } : {}),
867
1076
  ...(options.idempotencyKey
868
1077
  ? { idempotencyKey: options.idempotencyKey }
869
1078
  : {}),
870
1079
  })
871
- : client.todos.reopen({
872
- todoId: id,
1080
+ : client.tasks.reopen({
1081
+ taskId: id,
873
1082
  ...(options.idempotencyKey
874
1083
  ? { idempotencyKey: options.idempotencyKey }
875
1084
  : {}),
876
1085
  }));
877
- output(io, global.json, record, `Todo ${id} is ${record.status}.`);
1086
+ output(io, global.json, record, `Task ${id} is ${record.status}.`);
878
1087
  });
879
1088
  }
880
1089
  kudosCommand
@@ -1011,14 +1220,34 @@ export function createCli(io = defaultIo, serviceFactory = configuredServiceFact
1011
1220
  program
1012
1221
  .command('mcp')
1013
1222
  .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>')
1223
+ .option('--agent-id <id>', 'bound agent, whose identity is read from Synomem')
1224
+ .option('--actor-id <id>', 'bound non-agent actor ID')
1225
+ .option('--actor-kind <kind>', 'human or system')
1226
+ .option('--actor-name <display-name>', 'display name for a non-agent actor')
1017
1227
  .action(async (options, command) => {
1018
1228
  const global = globals(command);
1229
+ // An agent's name comes from its profile, never from the command line:
1230
+ // the name is written into every event the session appends, and a
1231
+ // harness must not be able to sign another agent's name to work.
1232
+ const bound = options.agentId
1233
+ ? await withClient(global.home, defaultActor(env, 'system', 'cli'), async (client) => {
1234
+ const resolution = await client.agents.resolve(options.agentId);
1235
+ if (!resolution.match) {
1236
+ throw new SynomemError('AGENT_NOT_FOUND', resolution.candidates.length
1237
+ ? `"${options.agentId}" matches ${resolution.candidates.length} agents: ${resolution.candidates
1238
+ .map((candidate) => candidate.id)
1239
+ .join(', ')}. Name one of them.`
1240
+ : `Unknown agent: ${options.agentId}`);
1241
+ }
1242
+ return actor('agent', resolution.match.id, resolution.match.displayName);
1243
+ })
1244
+ : undefined;
1245
+ if (!bound && !(options.actorId && options.actorKind)) {
1246
+ throw new SynomemError('INVALID_INPUT', 'Specify --agent-id, or --actor-id with --actor-kind for a non-agent actor.');
1247
+ }
1019
1248
  await startMcpServer({
1020
1249
  ...(global.home ? { home: global.home } : {}),
1021
- actor: actor(options.actorKind, options.actorId, options.actorName),
1250
+ actor: bound ?? actor(options.actorKind, options.actorId, options.actorName),
1022
1251
  });
1023
1252
  });
1024
1253
  return program;