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
@@ -1,4 +1,4 @@
1
- import type { ActorIdentity, AgentProfile, ChangePage, ItemListInput, ItemSummary, KudosListInput, KudosSummary, SynomemConfig, SynomemEvent, Page, RecordKind } from '../types.js';
1
+ import type { ActorIdentity, AgentProfile, AgentRuntimeBinding, ChangePage, ItemListInput, JsonValue, PostAcknowledgment, PostRoster, ItemSummary, KudosListInput, KudosSummary, SynomemConfig, SynomemEvent, Page, RecordKind } from '../types.js';
2
2
  /**
3
3
  * Operations required by authoritative domain behavior.
4
4
  *
@@ -21,6 +21,26 @@ export interface SynomemRepository {
21
21
  updateAgent(profile: AgentProfile, updatedAt: string): Awaitable<void>;
22
22
  getAgent(idOrAlias: string): Awaitable<AgentProfile | undefined>;
23
23
  listAgents(): Awaitable<AgentProfile[]>;
24
+ /** Resolves a name case-insensitively, reporting ambiguity instead of guessing. */
25
+ resolveAgent(query: string): Awaitable<{
26
+ match?: AgentProfile;
27
+ candidates: AgentProfile[];
28
+ }>;
29
+ listPostAcknowledgments(postId: string): Awaitable<PostAcknowledgment[]>;
30
+ /** Who has acknowledged a post and who has not; see PostRoster. */
31
+ postRoster(postId: string): Awaitable<PostRoster | undefined>;
32
+ listRuntimeBindings(agentId: string): Awaitable<AgentRuntimeBinding[]>;
33
+ bindRuntime(binding: {
34
+ id: string;
35
+ agentId: string;
36
+ installationId?: string;
37
+ runtime: string;
38
+ profile?: string;
39
+ capabilities?: Record<string, JsonValue>;
40
+ boundAt: string;
41
+ }): Awaitable<void>;
42
+ unbindRuntime(bindingId: string): Awaitable<boolean>;
43
+ touchRuntimeBinding(agentId: string, runtime: string, at: string): Awaitable<void>;
24
44
  listKudosSummaries(input: Required<Pick<KudosListInput, 'limit' | 'offset'>> & KudosListInput, viewer: ActorIdentity): Awaitable<Page<KudosSummary>>;
25
45
  listKudosChanges(after: string | undefined, limit: number, viewer: ActorIdentity): Awaitable<ChangePage>;
26
46
  listItemSummaries(input: Required<Pick<ItemListInput, 'limit' | 'offset'>> & ItemListInput, viewer: ActorIdentity): Awaitable<Page<ItemSummary>>;
@@ -1 +1 @@
1
- {"version":3,"file":"repository.d.ts","sourceRoot":"","sources":["../../src/ports/repository.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EACV,aAAa,EACb,YAAY,EACZ,UAAU,EACV,aAAa,EACb,WAAW,EACX,cAAc,EACd,YAAY,EACZ,aAAa,EACb,YAAY,EACZ,IAAI,EACJ,UAAU,EACX,MAAM,aAAa,CAAC;AAErB;;;;;;GAMG;AACH,MAAM,WAAW,iBAAiB;IAChC,QAAQ,CAAC,MAAM,EAAE,aAAa,CAAC;IAE/B,IAAI,IAAI,SAAS,CAAC,IAAI,CAAC,CAAC;IACxB,KAAK,IAAI,SAAS,CAAC,IAAI,CAAC,CAAC;IACzB,wBAAwB,IAAI,SAAS,CAAC,IAAI,CAAC,CAAC;IAC5C,WAAW,CAAC,CAAC,EAAE,SAAS,EAAE,MAAM,SAAS,CAAC,CAAC,CAAC,GAAG,OAAO,CAAC,CAAC,CAAC,CAAC;IAE1D,WAAW,CAAC,KAAK,EAAE,YAAY,GAAG,SAAS,CAAC,IAAI,CAAC,CAAC;IAClD,oBAAoB,CAAC,WAAW,EAAE,MAAM,GAAG,SAAS,CAAC,MAAM,CAAC,CAAC;IAC7D,qBAAqB,CACnB,SAAS,EAAE,aAAa,CAAC,MAAM,CAAC,EAChC,OAAO,EAAE,MAAM,EACf,GAAG,EAAE,MAAM,GACV,SAAS,CAAC,YAAY,GAAG,SAAS,CAAC,CAAC;IACvC,wBAAwB,CAAC,OAAO,EAAE,MAAM,GAAG,SAAS,CAAC,YAAY,EAAE,CAAC,CAAC;IACrE,qBAAqB,CAAC,EAAE,EAAE,MAAM,GAAG,SAAS,CAAC,YAAY,EAAE,CAAC,CAAC;IAE7D,WAAW,CAAC,OAAO,EAAE,YAAY,GAAG,SAAS,CAAC,IAAI,CAAC,CAAC;IACpD,WAAW,CAAC,OAAO,EAAE,YAAY,EAAE,SAAS,EAAE,MAAM,GAAG,SAAS,CAAC,IAAI,CAAC,CAAC;IACvE,QAAQ,CAAC,SAAS,EAAE,MAAM,GAAG,SAAS,CAAC,YAAY,GAAG,SAAS,CAAC,CAAC;IACjE,UAAU,IAAI,SAAS,CAAC,YAAY,EAAE,CAAC,CAAC;IAExC,kBAAkB,CAChB,KAAK,EAAE,QAAQ,CAAC,IAAI,CAAC,cAAc,EAAE,OAAO,GAAG,QAAQ,CAAC,CAAC,GAAG,cAAc,EAC1E,MAAM,EAAE,aAAa,GACpB,SAAS,CAAC,IAAI,CAAC,YAAY,CAAC,CAAC,CAAC;IACjC,gBAAgB,CACd,KAAK,EAAE,MAAM,GAAG,SAAS,EACzB,KAAK,EAAE,MAAM,EACb,MAAM,EAAE,aAAa,GACpB,SAAS,CAAC,UAAU,CAAC,CAAC;IACzB,iBAAiB,CACf,KAAK,EAAE,QAAQ,CAAC,IAAI,CAAC,aAAa,EAAE,OAAO,GAAG,QAAQ,CAAC,CAAC,GAAG,aAAa,EACxE,MAAM,EAAE,aAAa,GACpB,SAAS,CAAC,IAAI,CAAC,WAAW,CAAC,CAAC,CAAC;IAChC,eAAe,CACb,KAAK,EAAE,MAAM,GAAG,SAAS,EACzB,KAAK,EAAE,MAAM,EACb,MAAM,EAAE,aAAa,EACrB,KAAK,CAAC,EAAE,UAAU,EAAE,GACnB,SAAS,CAAC,UAAU,CAAC,CAAC;IACzB,cAAc,CAAC,EAAE,EAAE,MAAM,GAAG,SAAS,CAAC,WAAW,GAAG,SAAS,CAAC,CAAC;CAChE;AAED,MAAM,MAAM,SAAS,CAAC,CAAC,IAAI,CAAC,GAAG,OAAO,CAAC,CAAC,CAAC,CAAC"}
1
+ {"version":3,"file":"repository.d.ts","sourceRoot":"","sources":["../../src/ports/repository.ts"],"names":[],"mappings":"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"}
@@ -1,11 +1,36 @@
1
1
  import type { SynomemStorage } from './storage.js';
2
2
  import type { ProjectionWriter } from './ports/projections.js';
3
- import type { SynomemEvent, KudosRecord, MemoRecord, NoteRecord, TodoRecord } from './types.js';
3
+ import type { SynomemEvent, KudosRecord, MemoRecord, NoteRecord, PostRecord, TaskDue, TaskRecord, TodoRecord } from './types.js';
4
+ /**
5
+ * The instant a deadline has passed.
6
+ *
7
+ * A date-only deadline does not invent a time of day; it lapses at the end of
8
+ * that UTC day, which is the latest reading that cannot call work late while
9
+ * the stated day is still running. Every projection must use this one rule, or
10
+ * two stores would disagree about what is overdue.
11
+ */
12
+ export declare function dueInstant(due: TaskDue | undefined): string | undefined;
4
13
  export declare function escapeMarkdown(value: string): string;
5
14
  export declare function recordsFromEvents(events: SynomemEvent[]): KudosRecord[];
6
15
  export declare function memoRecordsFromEvents(events: SynomemEvent[]): MemoRecord[];
16
+ /**
17
+ * Rebuilds posts from their events, acknowledgements included.
18
+ *
19
+ * Edits are kept as a list rather than collapsed into the current text. A reader
20
+ * has to be able to see that a post changed after somebody acknowledged it —
21
+ * silently rewriting what was acknowledged is how a record becomes a lie.
22
+ */
23
+ export declare function postRecordsFromEvents(events: SynomemEvent[]): PostRecord[];
7
24
  export declare function noteRecordsFromEvents(events: SynomemEvent[]): NoteRecord[];
25
+ /**
26
+ * Builds private Todo records.
27
+ *
28
+ * Deliberately simpler than the Task reducer: there is no acceptance state and
29
+ * no response history, because there is nobody to negotiate with. A Todo goes
30
+ * open → completed / canceled / archived, and back to open on reopen.
31
+ */
8
32
  export declare function todoRecordsFromEvents(events: SynomemEvent[]): TodoRecord[];
33
+ export declare function taskRecordsFromEvents(events: SynomemEvent[]): TaskRecord[];
9
34
  /** Render a portable human-readable export without reading generated projections. */
10
35
  export declare function renderMarkdownExport(events: SynomemEvent[]): string;
11
36
  export declare class ProjectionManager implements ProjectionWriter {
@@ -1 +1 @@
1
- {"version":3,"file":"projections.d.ts","sourceRoot":"","sources":["../src/projections.ts"],"names":[],"mappings":"AASA,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,cAAc,CAAC;AACnD,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,wBAAwB,CAAC;AAC/D,OAAO,KAAK,EAGV,YAAY,EACZ,WAAW,EACX,UAAU,EACV,UAAU,EACV,UAAU,EACX,MAAM,YAAY,CAAC;AAQpB,wBAAgB,cAAc,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,CAOpD;AA2JD,wBAAgB,iBAAiB,CAAC,MAAM,EAAE,YAAY,EAAE,GAAG,WAAW,EAAE,CA4BvE;AAED,wBAAgB,qBAAqB,CAAC,MAAM,EAAE,YAAY,EAAE,GAAG,UAAU,EAAE,CAoB1E;AAED,wBAAgB,qBAAqB,CAAC,MAAM,EAAE,YAAY,EAAE,GAAG,UAAU,EAAE,CAqC1E;AAED,wBAAgB,qBAAqB,CAAC,MAAM,EAAE,YAAY,EAAE,GAAG,UAAU,EAAE,CA+D1E;AAED,qFAAqF;AACrF,wBAAgB,oBAAoB,CAAC,MAAM,EAAE,YAAY,EAAE,GAAG,MAAM,CAoBnE;AAED,qBAAa,iBAAkB,YAAW,gBAAgB;IAC5C,OAAO,CAAC,QAAQ,CAAC,OAAO;gBAAP,OAAO,EAAE,cAAc;IAEpD,OAAO,IAAI;QAAE,SAAS,EAAE,MAAM,EAAE,CAAC;QAAC,OAAO,EAAE,MAAM,EAAE,CAAA;KAAE;IAwGrD,SAAS,CAAC,OAAO,EAAE,MAAM,GAAG;QAAE,SAAS,EAAE,MAAM,EAAE,CAAC;QAAC,OAAO,EAAE,MAAM,EAAE,CAAA;KAAE;IA+GtE,aAAa,IAAI,MAAM,EAAE;CAqC1B"}
1
+ {"version":3,"file":"projections.d.ts","sourceRoot":"","sources":["../src/projections.ts"],"names":[],"mappings":"AASA,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,cAAc,CAAC;AACnD,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,wBAAwB,CAAC;AAC/D,OAAO,KAAK,EAGV,YAAY,EACZ,WAAW,EACX,UAAU,EACV,UAAU,EACV,UAAU,EACV,OAAO,EACP,UAAU,EACV,UAAU,EACX,MAAM,YAAY,CAAC;AAEpB;;;;;;;GAOG;AACH,wBAAgB,UAAU,CAAC,GAAG,EAAE,OAAO,GAAG,SAAS,GAAG,MAAM,GAAG,SAAS,CAGvE;AAQD,wBAAgB,cAAc,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,CAOpD;AA2JD,wBAAgB,iBAAiB,CAAC,MAAM,EAAE,YAAY,EAAE,GAAG,WAAW,EAAE,CA4BvE;AAED,wBAAgB,qBAAqB,CAAC,MAAM,EAAE,YAAY,EAAE,GAAG,UAAU,EAAE,CAoB1E;AAED;;;;;;GAMG;AACH,wBAAgB,qBAAqB,CAAC,MAAM,EAAE,YAAY,EAAE,GAAG,UAAU,EAAE,CA4D1E;AAED,wBAAgB,qBAAqB,CAAC,MAAM,EAAE,YAAY,EAAE,GAAG,UAAU,EAAE,CAqC1E;AAED;;;;;;GAMG;AACH,wBAAgB,qBAAqB,CAAC,MAAM,EAAE,YAAY,EAAE,GAAG,UAAU,EAAE,CAwD1E;AAED,wBAAgB,qBAAqB,CAAC,MAAM,EAAE,YAAY,EAAE,GAAG,UAAU,EAAE,CA8E1E;AAED,qFAAqF;AACrF,wBAAgB,oBAAoB,CAAC,MAAM,EAAE,YAAY,EAAE,GAAG,MAAM,CAoBnE;AAED,qBAAa,iBAAkB,YAAW,gBAAgB;IAC5C,OAAO,CAAC,QAAQ,CAAC,OAAO;gBAAP,OAAO,EAAE,cAAc;IAEpD,OAAO,IAAI;QAAE,SAAS,EAAE,MAAM,EAAE,CAAC;QAAC,OAAO,EAAE,MAAM,EAAE,CAAA;KAAE;IAwGrD,SAAS,CAAC,OAAO,EAAE,MAAM,GAAG;QAAE,SAAS,EAAE,MAAM,EAAE,CAAC;QAAC,OAAO,EAAE,MAAM,EAAE,CAAA;KAAE;IA+GtE,aAAa,IAAI,MAAM,EAAE;CAqC1B"}
@@ -2,6 +2,19 @@ import { existsSync, lstatSync, unlinkSync } from 'node:fs';
2
2
  import { join, relative, sep } from 'node:path';
3
3
  import { SynomemError } from './errors.js';
4
4
  import { assertNoSymlinkEscape, atomicWriteDerivedFile, atomicWriteFile, ensureDirectory, } from './fs-utils.js';
5
+ /**
6
+ * The instant a deadline has passed.
7
+ *
8
+ * A date-only deadline does not invent a time of day; it lapses at the end of
9
+ * that UTC day, which is the latest reading that cannot call work late while
10
+ * the stated day is still running. Every projection must use this one rule, or
11
+ * two stores would disagree about what is overdue.
12
+ */
13
+ export function dueInstant(due) {
14
+ if (!due)
15
+ return undefined;
16
+ return due.kind === 'date' ? `${due.date}T23:59:59.999Z` : due.datetime;
17
+ }
5
18
  const generatedMarker = '<!-- Generated by Synomem. Rebuild with `synomem rebuild`. -->';
6
19
  function manifestPath(home, path) {
7
20
  return relative(home, path).split(sep).join('/');
@@ -87,7 +100,7 @@ function memoInboxMarkdown(record) {
87
100
  '',
88
101
  ].join('\n')}\n`;
89
102
  }
90
- function todoInboxMarkdown(record) {
103
+ function taskInboxMarkdown(record) {
91
104
  const due = record.current.due?.kind === 'date'
92
105
  ? record.current.due.date
93
106
  : record.current.due
@@ -101,7 +114,7 @@ function todoInboxMarkdown(record) {
101
114
  `**Status:** ${record.status} `,
102
115
  `**Priority:** ${record.current.priority} `,
103
116
  `**Due:** ${escapeMarkdown(due)} `,
104
- `**Todo ID:** \`${record.event.id}\``,
117
+ `**Task ID:** \`${record.event.id}\``,
105
118
  '',
106
119
  ...(record.current.description ? [escapeMarkdown(record.current.description), ''] : []),
107
120
  ].join('\n')}\n`;
@@ -123,24 +136,24 @@ function memoryMarkdown(profile, records, rebuiltAt) {
123
136
  }
124
137
  return `${lines.join('\n')}\n`;
125
138
  }
126
- function todosMarkdown(profile, records, rebuiltAt) {
139
+ function tasksMarkdown(profile, records, rebuiltAt) {
127
140
  const lines = [
128
141
  generatedMarker,
129
142
  '',
130
- `# Todos — ${escapeMarkdown(profile.displayName)}`,
143
+ `# Tasks — ${escapeMarkdown(profile.displayName)}`,
131
144
  '',
132
145
  `_Last rebuilt: ${rebuiltAt}_`,
133
146
  '',
134
147
  ];
135
148
  if (!records.length)
136
- lines.push('_No todos yet._', '');
149
+ lines.push('_No tasks yet._', '');
137
150
  for (const record of records) {
138
151
  const due = record.current.due?.kind === 'date'
139
152
  ? record.current.due.date
140
153
  : record.current.due
141
154
  ? `${record.current.due.datetime} (${record.current.due.timeZone})`
142
155
  : 'None';
143
- lines.push(`## ${escapeMarkdown(record.current.title)}`, '', `**Status:** ${record.status} `, `**Priority:** ${record.current.priority} `, `**Due:** ${escapeMarkdown(due)} `, `**Todo ID:** \`${record.event.id}\``, '');
156
+ lines.push(`## ${escapeMarkdown(record.current.title)}`, '', `**Status:** ${record.status} `, `**Priority:** ${record.current.priority} `, `**Due:** ${escapeMarkdown(due)} `, `**Task ID:** \`${record.event.id}\``, '');
144
157
  }
145
158
  return `${lines.join('\n')}\n`;
146
159
  }
@@ -194,6 +207,77 @@ export function memoRecordsFromEvents(events) {
194
207
  }
195
208
  return [...records.values()].sort((a, b) => b.event.createdAt.localeCompare(a.event.createdAt));
196
209
  }
210
+ /**
211
+ * Rebuilds posts from their events, acknowledgements included.
212
+ *
213
+ * Edits are kept as a list rather than collapsed into the current text. A reader
214
+ * has to be able to see that a post changed after somebody acknowledged it —
215
+ * silently rewriting what was acknowledged is how a record becomes a lie.
216
+ */
217
+ export function postRecordsFromEvents(events) {
218
+ const records = new Map();
219
+ for (const event of events) {
220
+ if (event.type === 'post.created') {
221
+ records.set(event.id, {
222
+ event,
223
+ edits: [],
224
+ acknowledgments: [],
225
+ status: 'active',
226
+ title: event.title,
227
+ body: event.body,
228
+ ...(event.tags ? { tags: event.tags } : {}),
229
+ // The text version, counting only changes to the text.
230
+ //
231
+ // Deliberately not the aggregate version, which also counts every
232
+ // acknowledgement. If they were the same number, somebody
233
+ // acknowledging a post would invalidate an edit the author was in the
234
+ // middle of making — a conflict with nothing to reconcile.
235
+ version: 1,
236
+ });
237
+ }
238
+ else if (event.type === 'post.edited') {
239
+ const record = records.get(event.postId);
240
+ if (record) {
241
+ record.edits.push(event);
242
+ record.title = event.title;
243
+ record.body = event.body;
244
+ if (event.tags)
245
+ record.tags = event.tags;
246
+ record.version += 1;
247
+ }
248
+ }
249
+ else if (event.type === 'post.archived') {
250
+ const record = records.get(event.postId);
251
+ if (record) {
252
+ record.archived = event;
253
+ record.status = 'archived';
254
+ }
255
+ }
256
+ else if (event.type === 'post.acknowledged') {
257
+ const record = records.get(event.postId);
258
+ if (record) {
259
+ // One statement per actor: a repeat replaces rather than accumulates.
260
+ const existing = record.acknowledgments.findIndex((entry) => entry.actor.id === event.actor.id && entry.actor.kind === event.actor.kind);
261
+ const entry = {
262
+ actor: event.actor,
263
+ acknowledgedAt: event.createdAt,
264
+ ...(event.note ? { note: event.note } : {}),
265
+ };
266
+ if (existing >= 0)
267
+ record.acknowledgments[existing] = entry;
268
+ else
269
+ record.acknowledgments.push(entry);
270
+ }
271
+ }
272
+ else if (event.type === 'post.acknowledgment.withdrawn') {
273
+ const record = records.get(event.postId);
274
+ if (record) {
275
+ record.acknowledgments = record.acknowledgments.filter((entry) => !(entry.actor.id === event.actor.id && entry.actor.kind === event.actor.kind));
276
+ }
277
+ }
278
+ }
279
+ return [...records.values()];
280
+ }
197
281
  export function noteRecordsFromEvents(events) {
198
282
  const records = new Map();
199
283
  for (const event of events) {
@@ -234,6 +318,13 @@ export function noteRecordsFromEvents(events) {
234
318
  }
235
319
  return [...records.values()].sort((a, b) => b.event.createdAt.localeCompare(a.event.createdAt));
236
320
  }
321
+ /**
322
+ * Builds private Todo records.
323
+ *
324
+ * Deliberately simpler than the Task reducer: there is no acceptance state and
325
+ * no response history, because there is nobody to negotiate with. A Todo goes
326
+ * open → completed / canceled / archived, and back to open on reopen.
327
+ */
237
328
  export function todoRecordsFromEvents(events) {
238
329
  const records = new Map();
239
330
  for (const event of events) {
@@ -242,14 +333,13 @@ export function todoRecordsFromEvents(events) {
242
333
  event,
243
334
  current: {
244
335
  title: event.title,
245
- ...(event.description !== undefined ? { description: event.description } : {}),
336
+ ...(event.details !== undefined ? { details: event.details } : {}),
246
337
  priority: event.priority,
247
338
  ...(event.due ? { due: event.due } : {}),
248
339
  tags: event.tags ?? [],
249
- visibility: event.visibility,
250
340
  version: event.aggregateVersion,
251
341
  },
252
- status: event.requiresAcceptance ? 'assigned' : 'open',
342
+ status: 'open',
253
343
  });
254
344
  }
255
345
  else if (event.type === 'todo.updated') {
@@ -258,25 +348,26 @@ export function todoRecordsFromEvents(events) {
258
348
  record.update = event;
259
349
  record.current = {
260
350
  title: event.title,
261
- ...(event.description !== undefined ? { description: event.description } : {}),
351
+ ...(event.details !== undefined ? { details: event.details } : {}),
262
352
  priority: event.priority,
263
353
  ...(event.due ? { due: event.due } : {}),
264
354
  tags: event.tags ?? [],
265
- visibility: event.visibility,
266
355
  version: event.aggregateVersion,
267
356
  };
268
357
  }
269
358
  }
270
- else if (event.type === 'todo.accepted') {
359
+ else if (event.type === 'todo.reopened') {
271
360
  const record = records.get(event.todoId);
272
361
  if (record) {
362
+ record.reopened = event;
273
363
  record.current.version = event.aggregateVersion;
274
364
  record.status = 'open';
365
+ delete record.terminal;
275
366
  }
276
367
  }
277
368
  else if (event.type === 'todo.completed' ||
278
- event.type === 'todo.rejected' ||
279
- event.type === 'todo.canceled') {
369
+ event.type === 'todo.canceled' ||
370
+ event.type === 'todo.archived') {
280
371
  const record = records.get(event.todoId);
281
372
  if (record) {
282
373
  record.terminal = event;
@@ -284,13 +375,86 @@ export function todoRecordsFromEvents(events) {
284
375
  record.status =
285
376
  event.type === 'todo.completed'
286
377
  ? 'completed'
287
- : event.type === 'todo.rejected'
378
+ : event.type === 'todo.canceled'
379
+ ? 'canceled'
380
+ : 'archived';
381
+ }
382
+ }
383
+ }
384
+ return [...records.values()];
385
+ }
386
+ export function taskRecordsFromEvents(events) {
387
+ const records = new Map();
388
+ for (const event of events) {
389
+ if (event.type === 'task.created') {
390
+ records.set(event.id, {
391
+ event,
392
+ current: {
393
+ title: event.title,
394
+ ...(event.description !== undefined ? { description: event.description } : {}),
395
+ priority: event.priority,
396
+ ...(event.due ? { due: event.due } : {}),
397
+ tags: event.tags ?? [],
398
+ visibility: event.visibility,
399
+ version: event.aggregateVersion,
400
+ },
401
+ status: event.requiresAcceptance ? 'assigned' : 'open',
402
+ responses: [],
403
+ });
404
+ }
405
+ else if (event.type === 'task.updated') {
406
+ const record = records.get(event.taskId);
407
+ if (record) {
408
+ record.update = event;
409
+ record.current = {
410
+ title: event.title,
411
+ ...(event.description !== undefined ? { description: event.description } : {}),
412
+ priority: event.priority,
413
+ ...(event.due ? { due: event.due } : {}),
414
+ tags: event.tags ?? [],
415
+ visibility: event.visibility,
416
+ version: event.aggregateVersion,
417
+ };
418
+ }
419
+ }
420
+ else if (event.type === 'task.accepted') {
421
+ const record = records.get(event.taskId);
422
+ if (record) {
423
+ record.current.version = event.aggregateVersion;
424
+ record.status = 'open';
425
+ record.responses.push({
426
+ kind: 'accepted',
427
+ ...(event.response !== undefined ? { response: event.response } : {}),
428
+ actor: event.actor,
429
+ at: event.createdAt,
430
+ });
431
+ }
432
+ }
433
+ else if (event.type === 'task.completed' ||
434
+ event.type === 'task.rejected' ||
435
+ event.type === 'task.canceled') {
436
+ const record = records.get(event.taskId);
437
+ if (record) {
438
+ record.terminal = event;
439
+ record.current.version = event.aggregateVersion;
440
+ if (event.type === 'task.rejected') {
441
+ record.responses.push({
442
+ kind: 'rejected',
443
+ response: event.response,
444
+ actor: event.actor,
445
+ at: event.createdAt,
446
+ });
447
+ }
448
+ record.status =
449
+ event.type === 'task.completed'
450
+ ? 'completed'
451
+ : event.type === 'task.rejected'
288
452
  ? 'rejected'
289
453
  : 'canceled';
290
454
  }
291
455
  }
292
- else if (event.type === 'todo.reopened') {
293
- const record = records.get(event.todoId);
456
+ else if (event.type === 'task.reopened') {
457
+ const record = records.get(event.taskId);
294
458
  if (record) {
295
459
  record.reopened = event;
296
460
  record.current.version = event.aggregateVersion;
@@ -306,7 +470,7 @@ export function renderMarkdownExport(events) {
306
470
  ...recordsFromEvents(events).map((record) => `## Kudos: ${escapeMarkdown(record.event.title)}\n\n${escapeMarkdown(record.event.reason)}\n\nStatus: ${record.revocationStatus === 'revoked' ? 'Revoked' : record.status}\n\nID: \`${record.event.id}\``),
307
471
  ...memoRecordsFromEvents(events).map((record) => `## Memo: ${escapeMarkdown(record.event.subject)}\n\n${escapeMarkdown(record.event.body)}\n\nStatus: ${record.status}\n\nID: \`${record.event.id}\``),
308
472
  ...noteRecordsFromEvents(events).map((record) => `## Note: ${escapeMarkdown(record.current.title)}\n\n${escapeMarkdown(record.current.body)}\n\nStatus: ${record.status}; version ${record.current.version}\n\nID: \`${record.event.id}\``),
309
- ...todoRecordsFromEvents(events).map((record) => `## Todo: ${escapeMarkdown(record.current.title)}\n\n${record.current.description ? `${escapeMarkdown(record.current.description)}\n\n` : ''}Status: ${record.status}; priority ${record.current.priority}\n\nID: \`${record.event.id}\``),
473
+ ...taskRecordsFromEvents(events).map((record) => `## Task: ${escapeMarkdown(record.current.title)}\n\n${record.current.description ? `${escapeMarkdown(record.current.description)}\n\n` : ''}Status: ${record.status}; priority ${record.current.priority}\n\nID: \`${record.event.id}\``),
310
474
  ];
311
475
  return `${sections.join('\n\n')}\n`;
312
476
  }
@@ -326,7 +490,7 @@ export class ProjectionManager {
326
490
  const records = recordsFromEvents(events);
327
491
  const memos = memoRecordsFromEvents(events);
328
492
  const notes = noteRecordsFromEvents(events);
329
- const todos = todoRecordsFromEvents(events);
493
+ const tasks = taskRecordsFromEvents(events);
330
494
  const rebuiltAt = events.at(-1)?.createdAt ?? new Date(0).toISOString();
331
495
  const generated = [];
332
496
  for (const profile of profiles) {
@@ -339,12 +503,12 @@ export class ProjectionManager {
339
503
  const inboxDirectory = join(agentDirectory, 'inbox');
340
504
  const kudosInboxDirectory = join(inboxDirectory, 'kudos');
341
505
  const memoInboxDirectory = join(inboxDirectory, 'memos');
342
- const todoInboxDirectory = join(inboxDirectory, 'todos');
506
+ const taskInboxDirectory = join(inboxDirectory, 'tasks');
343
507
  assertNoSymlinkEscape(this.storage.home, inboxDirectory);
344
508
  ensureDirectory(inboxDirectory);
345
509
  ensureDirectory(kudosInboxDirectory);
346
510
  ensureDirectory(memoInboxDirectory);
347
- ensureDirectory(todoInboxDirectory);
511
+ ensureDirectory(taskInboxDirectory);
348
512
  atomicWriteDerivedFile(profilePath, `${JSON.stringify({ _generatedBy: 'synomem', ...profile }, null, 2)}\n`);
349
513
  generated.push(manifestPath(this.storage.home, profilePath));
350
514
  if (!existsSync(notesPath)) {
@@ -361,11 +525,11 @@ export class ProjectionManager {
361
525
  atomicWriteDerivedFile(memoryPath, memoryMarkdown(profile, agentNotes, rebuiltAt));
362
526
  generated.push(manifestPath(this.storage.home, memoryPath));
363
527
  }
364
- const agentTodos = todos.filter((record) => record.event.assigneeAgentId === profile.id);
365
- if (this.storage.config.projection.writeTodosMarkdown) {
366
- const todosPath = join(agentDirectory, 'TODOS.md');
367
- atomicWriteDerivedFile(todosPath, todosMarkdown(profile, agentTodos, rebuiltAt));
368
- generated.push(manifestPath(this.storage.home, todosPath));
528
+ const agentTasks = tasks.filter((record) => record.event.assigneeAgentId === profile.id);
529
+ if (this.storage.config.projection.writeTasksMarkdown) {
530
+ const tasksPath = join(agentDirectory, 'TASKS.md');
531
+ atomicWriteDerivedFile(tasksPath, tasksMarkdown(profile, agentTasks, rebuiltAt));
532
+ generated.push(manifestPath(this.storage.home, tasksPath));
369
533
  }
370
534
  if (this.storage.config.projection.writeInboxEntries) {
371
535
  for (const record of agentRecords.filter((item) => item.status === 'unacknowledged' && item.revocationStatus === 'active')) {
@@ -378,9 +542,9 @@ export class ProjectionManager {
378
542
  atomicWriteDerivedFile(inboxPath, memoInboxMarkdown(record));
379
543
  generated.push(manifestPath(this.storage.home, inboxPath));
380
544
  }
381
- for (const record of agentTodos.filter((item) => item.status === 'open' || item.status === 'assigned')) {
382
- const inboxPath = join(todoInboxDirectory, `${record.event.id}.md`);
383
- atomicWriteDerivedFile(inboxPath, todoInboxMarkdown(record));
545
+ for (const record of agentTasks.filter((item) => item.status === 'open' || item.status === 'assigned')) {
546
+ const inboxPath = join(taskInboxDirectory, `${record.event.id}.md`);
547
+ atomicWriteDerivedFile(inboxPath, taskInboxMarkdown(record));
384
548
  generated.push(manifestPath(this.storage.home, inboxPath));
385
549
  }
386
550
  }
@@ -413,20 +577,20 @@ export class ProjectionManager {
413
577
  const records = recordsFromEvents(events).filter((record) => record.event.recipientAgentId === profile.id);
414
578
  const memos = memoRecordsFromEvents(events).filter((record) => record.event.recipientAgentId === profile.id);
415
579
  const notes = noteRecordsFromEvents(events).filter((record) => record.event.ownerAgentId === profile.id);
416
- const todos = todoRecordsFromEvents(events).filter((record) => record.event.assigneeAgentId === profile.id);
580
+ const tasks = taskRecordsFromEvents(events).filter((record) => record.event.assigneeAgentId === profile.id);
417
581
  const rebuiltAt = events.at(-1)?.createdAt ?? new Date(0).toISOString();
418
582
  const agentDirectory = join(this.storage.home, profile.id);
419
583
  const inboxDirectory = join(agentDirectory, 'inbox');
420
584
  const kudosInboxDirectory = join(inboxDirectory, 'kudos');
421
585
  const memoInboxDirectory = join(inboxDirectory, 'memos');
422
- const todoInboxDirectory = join(inboxDirectory, 'todos');
586
+ const taskInboxDirectory = join(inboxDirectory, 'tasks');
423
587
  assertNoSymlinkEscape(this.storage.home, agentDirectory);
424
588
  assertNoSymlinkEscape(this.storage.home, inboxDirectory);
425
589
  ensureDirectory(agentDirectory);
426
590
  ensureDirectory(inboxDirectory);
427
591
  ensureDirectory(kudosInboxDirectory);
428
592
  ensureDirectory(memoInboxDirectory);
429
- ensureDirectory(todoInboxDirectory);
593
+ ensureDirectory(taskInboxDirectory);
430
594
  const generated = [];
431
595
  const profilePath = join(agentDirectory, 'profile.json');
432
596
  atomicWriteDerivedFile(profilePath, `${JSON.stringify({ _generatedBy: 'synomem', ...profile }, null, 2)}\n`);
@@ -445,10 +609,10 @@ export class ProjectionManager {
445
609
  atomicWriteDerivedFile(memoryPath, memoryMarkdown(profile, notes, rebuiltAt));
446
610
  generated.push(manifestPath(this.storage.home, memoryPath));
447
611
  }
448
- if (this.storage.config.projection.writeTodosMarkdown) {
449
- const todosPath = join(agentDirectory, 'TODOS.md');
450
- atomicWriteDerivedFile(todosPath, todosMarkdown(profile, todos, rebuiltAt));
451
- generated.push(manifestPath(this.storage.home, todosPath));
612
+ if (this.storage.config.projection.writeTasksMarkdown) {
613
+ const tasksPath = join(agentDirectory, 'TASKS.md');
614
+ atomicWriteDerivedFile(tasksPath, tasksMarkdown(profile, tasks, rebuiltAt));
615
+ generated.push(manifestPath(this.storage.home, tasksPath));
452
616
  }
453
617
  if (this.storage.config.projection.writeInboxEntries) {
454
618
  for (const record of records.filter((item) => item.status === 'unacknowledged' && item.revocationStatus === 'active')) {
@@ -465,11 +629,11 @@ export class ProjectionManager {
465
629
  atomicWriteDerivedFile(inboxPath, memoInboxMarkdown(record));
466
630
  generated.push(relativePath);
467
631
  }
468
- for (const record of todos.filter((item) => item.status === 'open' || item.status === 'assigned')) {
469
- const inboxPath = join(todoInboxDirectory, `${record.event.id}.md`);
632
+ for (const record of tasks.filter((item) => item.status === 'open' || item.status === 'assigned')) {
633
+ const inboxPath = join(taskInboxDirectory, `${record.event.id}.md`);
470
634
  const relativePath = manifestPath(this.storage.home, inboxPath);
471
635
  if (!existsSync(inboxPath))
472
- atomicWriteDerivedFile(inboxPath, todoInboxMarkdown(record));
636
+ atomicWriteDerivedFile(inboxPath, taskInboxMarkdown(record));
473
637
  generated.push(relativePath);
474
638
  }
475
639
  }
@@ -500,7 +664,7 @@ export class ProjectionManager {
500
664
  const events = this.storage.getReadableEvents();
501
665
  const records = recordsFromEvents(events);
502
666
  const memos = memoRecordsFromEvents(events);
503
- const todos = todoRecordsFromEvents(events);
667
+ const tasks = taskRecordsFromEvents(events);
504
668
  const paths = [];
505
669
  for (const profile of profiles) {
506
670
  paths.push(`${profile.id}/profile.json`);
@@ -508,8 +672,8 @@ export class ProjectionManager {
508
672
  paths.push(`${profile.id}/WINS.md`);
509
673
  if (this.storage.config.projection.writeMemoryMarkdown)
510
674
  paths.push(`${profile.id}/MEMORY.md`);
511
- if (this.storage.config.projection.writeTodosMarkdown)
512
- paths.push(`${profile.id}/TODOS.md`);
675
+ if (this.storage.config.projection.writeTasksMarkdown)
676
+ paths.push(`${profile.id}/TASKS.md`);
513
677
  if (this.storage.config.projection.writeInboxEntries) {
514
678
  for (const record of records.filter((item) => item.event.recipientAgentId === profile.id &&
515
679
  item.status === 'unacknowledged' &&
@@ -519,9 +683,9 @@ export class ProjectionManager {
519
683
  for (const record of memos.filter((item) => item.event.recipientAgentId === profile.id && item.status === 'unread')) {
520
684
  paths.push(`${profile.id}/inbox/memos/${record.event.id}.md`);
521
685
  }
522
- for (const record of todos.filter((item) => item.event.assigneeAgentId === profile.id &&
686
+ for (const record of tasks.filter((item) => item.event.assigneeAgentId === profile.id &&
523
687
  (item.status === 'open' || item.status === 'assigned'))) {
524
- paths.push(`${profile.id}/inbox/todos/${record.event.id}.md`);
688
+ paths.push(`${profile.id}/inbox/tasks/${record.event.id}.md`);
525
689
  }
526
690
  }
527
691
  }