@rubytech/create-maxy-code 0.1.230 → 0.1.232

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 (49) hide show
  1. package/package.json +1 -1
  2. package/payload/platform/neo4j/schema.cypher +20 -0
  3. package/payload/platform/plugins/admin/skills/platform-architecture/SKILL.md +39 -1
  4. package/payload/platform/plugins/docs/references/graph.md +12 -0
  5. package/payload/platform/plugins/docs/references/memory-guide.md +26 -0
  6. package/payload/platform/plugins/docs/superpowers/plans/2026-06-01-memory-edge.md +589 -0
  7. package/payload/platform/plugins/memory/.claude-plugin/plugin.json +1 -1
  8. package/payload/platform/plugins/memory/PLUGIN.md +7 -1
  9. package/payload/platform/plugins/memory/mcp/dist/index.js +41 -0
  10. package/payload/platform/plugins/memory/mcp/dist/index.js.map +1 -1
  11. package/payload/platform/plugins/memory/mcp/dist/tools/__tests__/memory-compiled-truth-history.test.js +72 -26
  12. package/payload/platform/plugins/memory/mcp/dist/tools/__tests__/memory-compiled-truth-history.test.js.map +1 -1
  13. package/payload/platform/plugins/memory/mcp/dist/tools/__tests__/memory-edge.test.d.ts +2 -0
  14. package/payload/platform/plugins/memory/mcp/dist/tools/__tests__/memory-edge.test.d.ts.map +1 -0
  15. package/payload/platform/plugins/memory/mcp/dist/tools/__tests__/memory-edge.test.js +164 -0
  16. package/payload/platform/plugins/memory/mcp/dist/tools/__tests__/memory-edge.test.js.map +1 -0
  17. package/payload/platform/plugins/memory/mcp/dist/tools/__tests__/memory-write-timeline-learned-at.test.d.ts +2 -0
  18. package/payload/platform/plugins/memory/mcp/dist/tools/__tests__/memory-write-timeline-learned-at.test.d.ts.map +1 -0
  19. package/payload/platform/plugins/memory/mcp/dist/tools/__tests__/memory-write-timeline-learned-at.test.js +30 -0
  20. package/payload/platform/plugins/memory/mcp/dist/tools/__tests__/memory-write-timeline-learned-at.test.js.map +1 -0
  21. package/payload/platform/plugins/memory/mcp/dist/tools/memory-compiled-truth-history.d.ts +17 -1
  22. package/payload/platform/plugins/memory/mcp/dist/tools/memory-compiled-truth-history.d.ts.map +1 -1
  23. package/payload/platform/plugins/memory/mcp/dist/tools/memory-compiled-truth-history.js +33 -13
  24. package/payload/platform/plugins/memory/mcp/dist/tools/memory-compiled-truth-history.js.map +1 -1
  25. package/payload/platform/plugins/memory/mcp/dist/tools/memory-edge.d.ts +21 -0
  26. package/payload/platform/plugins/memory/mcp/dist/tools/memory-edge.d.ts.map +1 -0
  27. package/payload/platform/plugins/memory/mcp/dist/tools/memory-edge.js +51 -0
  28. package/payload/platform/plugins/memory/mcp/dist/tools/memory-edge.js.map +1 -0
  29. package/payload/platform/plugins/memory/mcp/dist/tools/memory-write.d.ts.map +1 -1
  30. package/payload/platform/plugins/memory/mcp/dist/tools/memory-write.js +1 -0
  31. package/payload/platform/plugins/memory/mcp/dist/tools/memory-write.js.map +1 -1
  32. package/payload/platform/plugins/memory/skills/challenge/SKILL.md +52 -0
  33. package/payload/platform/plugins/memory/skills/connect/SKILL.md +56 -0
  34. package/payload/platform/plugins/memory/skills/emerge/SKILL.md +87 -0
  35. package/payload/platform/services/claude-session-manager/dist/canonical-tool-names.generated.d.ts.map +1 -1
  36. package/payload/platform/services/claude-session-manager/dist/canonical-tool-names.generated.js +1 -0
  37. package/payload/platform/services/claude-session-manager/dist/canonical-tool-names.generated.js.map +1 -1
  38. package/payload/platform/services/claude-session-manager/dist/http-server.d.ts.map +1 -1
  39. package/payload/platform/services/claude-session-manager/dist/http-server.js +26 -4
  40. package/payload/platform/services/claude-session-manager/dist/http-server.js.map +1 -1
  41. package/payload/platform/templates/specialists/agents/archive-ingest-operator.md +1 -1
  42. package/payload/platform/templates/specialists/agents/database-operator.md +1 -1
  43. package/payload/platform/templates/specialists/agents/librarian.md +1 -1
  44. package/payload/server/{chunk-EXIPYITB.js → chunk-63M3ILJ6.js} +1 -0
  45. package/payload/server/maxy-edge.js +1 -1
  46. package/payload/server/public/assets/admin-DfeXRW1m.js +1 -0
  47. package/payload/server/public/index.html +1 -1
  48. package/payload/server/server.js +1 -1
  49. package/payload/server/public/assets/admin-BKawc0eQ.js +0 -1
@@ -0,0 +1,589 @@
1
+ # memory-edge Implementation Plan
2
+
3
+ > **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
4
+
5
+ **Goal:** Add a `memory-edge` MCP tool to the memory plugin that creates or deletes a typed directed edge between two pre-existing Neo4j nodes that belong to the same account.
6
+
7
+ **Architecture:** New `tools/memory-edge.ts` exports a single `memoryEdge()` function. It opens one Neo4j session, runs two ownership-check queries (one per endpoint), then runs a MERGE or DELETE query with the relationship type interpolated (backtick-escaped) into Cypher. The tool is registered with `server.tool` in `index.ts` and listed in `PLUGIN.md` and three specialist agent templates.
8
+
9
+ **Tech Stack:** TypeScript, Neo4j driver (`getSession` from `lib/neo4j.js`), Vitest for unit tests.
10
+
11
+ ---
12
+
13
+ ### Task 1: Create `tools/memory-edge.ts` with core logic
14
+
15
+ **Files:**
16
+ - Create: `maxy-code/platform/plugins/memory/mcp/src/tools/memory-edge.ts`
17
+
18
+ - [ ] **Step 1: Write the failing test** (see Task 2 — implement this file first so tests have something to import, then add tests)
19
+
20
+ - [ ] **Step 2: Create the file**
21
+
22
+ ```typescript
23
+ import { getSession } from "../lib/neo4j.js";
24
+
25
+ export interface MemoryEdgeParams {
26
+ action: "create" | "delete";
27
+ fromId: string;
28
+ toId: string;
29
+ relationshipType: string;
30
+ properties?: Record<string, unknown>;
31
+ accountId: string;
32
+ }
33
+
34
+ export type MemoryEdgeResult =
35
+ | { created: boolean }
36
+ | { deleted: boolean }
37
+ | { error: "foreign-node"; elementId: string }
38
+ | { error: "reserved-type"; type: string };
39
+
40
+ export async function memoryEdge(params: MemoryEdgeParams): Promise<MemoryEdgeResult> {
41
+ const { action, fromId, toId, accountId } = params;
42
+
43
+ const rawType = params.relationshipType.toUpperCase().replace(/`/g, "");
44
+
45
+ if (rawType.startsWith("_")) {
46
+ process.stderr.write(
47
+ `[memory-edge] action=${action} from=${fromId.slice(0, 8)} to=${toId.slice(0, 8)} type=${rawType} accountId=${accountId?.slice(0, 8) ?? "none"} result=rejected reason=reserved-type type=${rawType}\n`,
48
+ );
49
+ return { error: "reserved-type", type: rawType };
50
+ }
51
+
52
+ const session = getSession();
53
+ try {
54
+ const fromCheck = await session.run(
55
+ `MATCH (n) WHERE elementId(n) = $nodeId AND n.accountId = $accountId RETURN elementId(n) AS id`,
56
+ { nodeId: fromId, accountId },
57
+ );
58
+ if (fromCheck.records.length === 0) {
59
+ process.stderr.write(
60
+ `[memory-edge] action=${action} from=${fromId.slice(0, 8)} to=${toId.slice(0, 8)} type=${rawType} accountId=${accountId?.slice(0, 8) ?? "none"} result=rejected reason=foreign-node elementId=${fromId}\n`,
61
+ );
62
+ return { error: "foreign-node", elementId: fromId };
63
+ }
64
+
65
+ const toCheck = await session.run(
66
+ `MATCH (n) WHERE elementId(n) = $nodeId AND n.accountId = $accountId RETURN elementId(n) AS id`,
67
+ { nodeId: toId, accountId },
68
+ );
69
+ if (toCheck.records.length === 0) {
70
+ process.stderr.write(
71
+ `[memory-edge] action=${action} from=${fromId.slice(0, 8)} to=${toId.slice(0, 8)} type=${rawType} accountId=${accountId?.slice(0, 8) ?? "none"} result=rejected reason=foreign-node elementId=${toId}\n`,
72
+ );
73
+ return { error: "foreign-node", elementId: toId };
74
+ }
75
+
76
+ if (action === "create") {
77
+ const props = params.properties ?? {};
78
+ const q = `
79
+ MATCH (a) WHERE elementId(a) = $fromId
80
+ MATCH (b) WHERE elementId(b) = $toId
81
+ MERGE (a)-[r:\`${rawType}\`]->(b)
82
+ ON CREATE SET r += $props, r.createdAt = datetime()
83
+ RETURN r
84
+ `;
85
+ const result = await session.run(q, { fromId, toId, props });
86
+ const created = result.summary.counters.updates().relationshipsCreated > 0;
87
+ process.stderr.write(
88
+ `[memory-edge] action=create from=${fromId.slice(0, 8)} to=${toId.slice(0, 8)} type=${rawType} accountId=${accountId?.slice(0, 8) ?? "none"} result=${created ? "created" : "idempotent"}\n`,
89
+ );
90
+ return { created };
91
+ } else {
92
+ const q = `
93
+ MATCH (a)-[r:\`${rawType}\`]->(b)
94
+ WHERE elementId(a) = $fromId AND elementId(b) = $toId
95
+ DELETE r
96
+ `;
97
+ const result = await session.run(q, { fromId, toId });
98
+ const deleted = result.summary.counters.updates().relationshipsDeleted > 0;
99
+ process.stderr.write(
100
+ `[memory-edge] action=delete from=${fromId.slice(0, 8)} to=${toId.slice(0, 8)} type=${rawType} accountId=${accountId?.slice(0, 8) ?? "none"} result=${deleted ? "deleted" : "absent"}\n`,
101
+ );
102
+ return { deleted };
103
+ }
104
+ } finally {
105
+ await session.close();
106
+ }
107
+ }
108
+ ```
109
+
110
+ - [ ] **Step 3: TypeScript check** (run after Task 2 tests pass)
111
+
112
+ ```bash
113
+ cd maxy-code/platform/plugins/memory/mcp && npx tsc --noEmit 2>&1 | head -20
114
+ ```
115
+ Expected: no errors for this file (other pre-existing errors are acceptable if present before this change).
116
+
117
+ ---
118
+
119
+ ### Task 2: Unit tests for `memory-edge`
120
+
121
+ **Files:**
122
+ - Create: `maxy-code/platform/plugins/memory/mcp/src/tools/__tests__/memory-edge.test.ts`
123
+
124
+ - [ ] **Step 1: Write the test file**
125
+
126
+ ```typescript
127
+ // @vitest-environment node
128
+ import { describe, it, expect, beforeEach, vi } from "vitest";
129
+
130
+ // Stage session run results in call order. Each entry in the array is the
131
+ // return value for one session.run() call.
132
+ function makeSession(runResults: Array<{ records: unknown[]; summary?: unknown }>) {
133
+ let i = 0;
134
+ return {
135
+ run: vi.fn().mockImplementation(async () => {
136
+ const res = runResults[i++] ?? { records: [] };
137
+ return res;
138
+ }),
139
+ close: vi.fn().mockResolvedValue(undefined),
140
+ };
141
+ }
142
+
143
+ function ownerOk(id: string) {
144
+ return { records: [{ get: (_: string) => id }] };
145
+ }
146
+
147
+ function ownerFail() {
148
+ return { records: [] };
149
+ }
150
+
151
+ function mergeResult(created: number) {
152
+ return {
153
+ records: [{ get: (_: string) => ({}) }],
154
+ summary: { counters: { updates: () => ({ relationshipsCreated: created, relationshipsDeleted: 0 }) } },
155
+ };
156
+ }
157
+
158
+ function deleteResult(deleted: number) {
159
+ return {
160
+ records: [],
161
+ summary: { counters: { updates: () => ({ relationshipsCreated: 0, relationshipsDeleted: deleted }) } },
162
+ };
163
+ }
164
+
165
+ const ACCOUNT_ID = "acct-1";
166
+ const FROM_ID = "4:eid:node-a";
167
+ const TO_ID = "4:eid:node-b";
168
+
169
+ let mockSession: ReturnType<typeof makeSession>;
170
+
171
+ vi.mock("../../lib/neo4j.js", () => ({
172
+ getSession: () => mockSession,
173
+ }));
174
+
175
+ import { memoryEdge } from "../memory-edge.js";
176
+
177
+ beforeEach(() => {
178
+ vi.clearAllMocks();
179
+ });
180
+
181
+ // Suppress stderr noise during tests
182
+ beforeEach(() => {
183
+ vi.spyOn(process.stderr, "write").mockImplementation(() => true);
184
+ });
185
+
186
+ describe("memory-edge — create", () => {
187
+ it("creates edge on first call — returned created: true", async () => {
188
+ mockSession = makeSession([ownerOk(FROM_ID), ownerOk(TO_ID), mergeResult(1)]);
189
+ const result = await memoryEdge({
190
+ action: "create",
191
+ fromId: FROM_ID,
192
+ toId: TO_ID,
193
+ relationshipType: "RELATES_TO",
194
+ accountId: ACCOUNT_ID,
195
+ });
196
+ expect(result).toEqual({ created: true });
197
+ expect(mockSession.run).toHaveBeenCalledTimes(3);
198
+ });
199
+
200
+ it("idempotent on second call — returned created: false", async () => {
201
+ mockSession = makeSession([ownerOk(FROM_ID), ownerOk(TO_ID), mergeResult(0)]);
202
+ const result = await memoryEdge({
203
+ action: "create",
204
+ fromId: FROM_ID,
205
+ toId: TO_ID,
206
+ relationshipType: "RELATES_TO",
207
+ accountId: ACCOUNT_ID,
208
+ });
209
+ expect(result).toEqual({ created: false });
210
+ });
211
+
212
+ it("uppercases the relationship type before the Cypher call", async () => {
213
+ mockSession = makeSession([ownerOk(FROM_ID), ownerOk(TO_ID), mergeResult(1)]);
214
+ await memoryEdge({
215
+ action: "create",
216
+ fromId: FROM_ID,
217
+ toId: TO_ID,
218
+ relationshipType: "relates_to",
219
+ accountId: ACCOUNT_ID,
220
+ });
221
+ const mergeCall = mockSession.run.mock.calls[2][0] as string;
222
+ expect(mergeCall).toContain("`RELATES_TO`");
223
+ });
224
+ });
225
+
226
+ describe("memory-edge — delete", () => {
227
+ it("deletes a present edge — returned deleted: true", async () => {
228
+ mockSession = makeSession([ownerOk(FROM_ID), ownerOk(TO_ID), deleteResult(1)]);
229
+ const result = await memoryEdge({
230
+ action: "delete",
231
+ fromId: FROM_ID,
232
+ toId: TO_ID,
233
+ relationshipType: "RELATES_TO",
234
+ accountId: ACCOUNT_ID,
235
+ });
236
+ expect(result).toEqual({ deleted: true });
237
+ });
238
+
239
+ it("absent edge is a no-op — returned deleted: false", async () => {
240
+ mockSession = makeSession([ownerOk(FROM_ID), ownerOk(TO_ID), deleteResult(0)]);
241
+ const result = await memoryEdge({
242
+ action: "delete",
243
+ fromId: FROM_ID,
244
+ toId: TO_ID,
245
+ relationshipType: "RELATES_TO",
246
+ accountId: ACCOUNT_ID,
247
+ });
248
+ expect(result).toEqual({ deleted: false });
249
+ });
250
+ });
251
+
252
+ describe("memory-edge — account ownership", () => {
253
+ it("rejects when fromId does not belong to the account — foreign-node error, no mutation", async () => {
254
+ mockSession = makeSession([ownerFail()]);
255
+ const result = await memoryEdge({
256
+ action: "create",
257
+ fromId: FROM_ID,
258
+ toId: TO_ID,
259
+ relationshipType: "RELATES_TO",
260
+ accountId: ACCOUNT_ID,
261
+ });
262
+ expect(result).toEqual({ error: "foreign-node", elementId: FROM_ID });
263
+ expect(mockSession.run).toHaveBeenCalledTimes(1);
264
+ });
265
+
266
+ it("rejects when toId does not belong to the account — foreign-node error, no mutation", async () => {
267
+ mockSession = makeSession([ownerOk(FROM_ID), ownerFail()]);
268
+ const result = await memoryEdge({
269
+ action: "create",
270
+ fromId: FROM_ID,
271
+ toId: TO_ID,
272
+ relationshipType: "RELATES_TO",
273
+ accountId: ACCOUNT_ID,
274
+ });
275
+ expect(result).toEqual({ error: "foreign-node", elementId: TO_ID });
276
+ expect(mockSession.run).toHaveBeenCalledTimes(2);
277
+ });
278
+ });
279
+
280
+ describe("memory-edge — reserved type guard", () => {
281
+ it("rejects underscore-prefixed type before opening a session", async () => {
282
+ mockSession = makeSession([]);
283
+ const result = await memoryEdge({
284
+ action: "create",
285
+ fromId: FROM_ID,
286
+ toId: TO_ID,
287
+ relationshipType: "_INTERNAL",
288
+ accountId: ACCOUNT_ID,
289
+ });
290
+ expect(result).toEqual({ error: "reserved-type", type: "_INTERNAL" });
291
+ expect(mockSession.run).not.toHaveBeenCalled();
292
+ });
293
+
294
+ it("uppercase-coerces before the reserved check — _soft_delete is also rejected", async () => {
295
+ mockSession = makeSession([]);
296
+ const result = await memoryEdge({
297
+ action: "create",
298
+ fromId: FROM_ID,
299
+ toId: TO_ID,
300
+ relationshipType: "_soft_delete",
301
+ accountId: ACCOUNT_ID,
302
+ });
303
+ expect(result).toEqual({ error: "reserved-type", type: "_SOFT_DELETE" });
304
+ });
305
+ });
306
+ ```
307
+
308
+ - [ ] **Step 2: Run the tests — expect PASS**
309
+
310
+ ```bash
311
+ cd maxy-code/platform/plugins/memory/mcp && npx vitest run src/tools/__tests__/memory-edge.test.ts 2>&1
312
+ ```
313
+
314
+ Expected: all tests pass (the implementation in Task 1 is already complete).
315
+
316
+ - [ ] **Step 3: Run the full test suite — no regressions**
317
+
318
+ ```bash
319
+ cd maxy-code/platform/plugins/memory/mcp && npx vitest run 2>&1 | tail -20
320
+ ```
321
+
322
+ Expected: same pass count as before this sprint (pre-existing failures are acceptable if they existed before).
323
+
324
+ - [ ] **Step 4: Commit**
325
+
326
+ ```bash
327
+ cd maxy-code/platform/plugins/memory/mcp && git add src/tools/memory-edge.ts src/tools/__tests__/memory-edge.test.ts
328
+ git commit -m "$(cat <<'EOF'
329
+ feat: add memory-edge tool — create/delete typed edges between existing nodes
330
+
331
+ Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
332
+ Session: 3104d4b4-d03e-4339-b65e-6014223c069e
333
+ EOF
334
+ )"
335
+ ```
336
+
337
+ ---
338
+
339
+ ### Task 3: Register in `index.ts`
340
+
341
+ **Files:**
342
+ - Modify: `maxy-code/platform/plugins/memory/mcp/src/index.ts`
343
+
344
+ - [ ] **Step 1: Add the import** after the `memoryUpdate` import line (around line 36)
345
+
346
+ Find:
347
+ ```typescript
348
+ import { memoryUpdate } from "./tools/memory-update.js";
349
+ ```
350
+
351
+ Add immediately after:
352
+ ```typescript
353
+ import { memoryEdge } from "./tools/memory-edge.js";
354
+ ```
355
+
356
+ - [ ] **Step 2: Add the `server.tool` registration** after the closing brace of the `memory-update` block (after line 1111 — search for the block ending with `"Update failed:"`)
357
+
358
+ Find this exact closing sequence of the memory-update registration:
359
+ ```typescript
360
+ isError: true,
361
+ };
362
+ }
363
+ }
364
+ );
365
+
366
+ eagerTool(server,
367
+ "session-compact-status",
368
+ ```
369
+
370
+ Replace with:
371
+ ```typescript
372
+ isError: true,
373
+ };
374
+ }
375
+ }
376
+ );
377
+
378
+ server.tool(
379
+ "memory-edge",
380
+ "Create or delete a typed directed edge between two nodes that already exist in the knowledge graph. Both nodes must belong to the same account. Use action='create' to MERGE the edge (idempotent — returns {created:false} if it already exists) or action='delete' to remove it (returns {deleted:false} if already absent). relationshipType is uppercase-coerced; types starting with underscore are reserved and rejected. Properties are stamped on the edge on CREATE only.",
381
+ {
382
+ action: z.enum(["create", "delete"]).describe("'create' MERGEs the edge (idempotent); 'delete' removes it (no-op if absent)."),
383
+ fromId: z.string().describe("elementId of the source node (from memory-search results)."),
384
+ toId: z.string().describe("elementId of the target node (from memory-search results)."),
385
+ relationshipType: z.string().describe("Relationship type label — e.g. RELATES_TO, MENTIONS, WORKED_WITH. Uppercase-coerced. Underscore-prefixed types (_SOFT_DELETE, etc.) are reserved and rejected."),
386
+ properties: z.record(z.string(), z.unknown()).optional().describe("Properties to set on the relationship on CREATE. Ignored on DELETE and on idempotent CREATE (MERGE-matched)."),
387
+ },
388
+ async ({ action, fromId, toId, relationshipType, properties }: {
389
+ action: "create" | "delete";
390
+ fromId: string;
391
+ toId: string;
392
+ relationshipType: string;
393
+ properties?: Record<string, unknown>;
394
+ }) => {
395
+ if (!accountId) return refuseNoAccount("memory-edge");
396
+ try {
397
+ const result = await memoryEdge({
398
+ action,
399
+ fromId,
400
+ toId,
401
+ relationshipType,
402
+ properties,
403
+ accountId,
404
+ });
405
+
406
+ if ("error" in result) {
407
+ return {
408
+ content: [{ type: "text" as const, text: `memory-edge rejected: ${JSON.stringify(result)}` }],
409
+ isError: true,
410
+ };
411
+ }
412
+
413
+ const detail =
414
+ "created" in result
415
+ ? result.created
416
+ ? "Edge created."
417
+ : "Edge already exists (idempotent)."
418
+ : result.deleted
419
+ ? "Edge deleted."
420
+ : "Edge was not present (no-op).";
421
+
422
+ return { content: [{ type: "text" as const, text: detail }] };
423
+ } catch (err) {
424
+ return {
425
+ content: [{ type: "text" as const, text: `memory-edge failed: ${err instanceof Error ? err.message : String(err)}` }],
426
+ isError: true,
427
+ };
428
+ }
429
+ }
430
+ );
431
+
432
+ eagerTool(server,
433
+ "session-compact-status",
434
+ ```
435
+
436
+ - [ ] **Step 3: TypeScript check**
437
+
438
+ ```bash
439
+ cd maxy-code/platform/plugins/memory/mcp && npx tsc --noEmit 2>&1 | head -20
440
+ ```
441
+
442
+ Expected: no new errors introduced by this change.
443
+
444
+ - [ ] **Step 4: Commit**
445
+
446
+ ```bash
447
+ git add maxy-code/platform/plugins/memory/mcp/src/index.ts
448
+ git commit -m "$(cat <<'EOF'
449
+ feat: register memory-edge in memory MCP server index.ts
450
+
451
+ Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
452
+ Session: 3104d4b4-d03e-4339-b65e-6014223c069e
453
+ EOF
454
+ )"
455
+ ```
456
+
457
+ ---
458
+
459
+ ### Task 4: Update `PLUGIN.md` and `graph.md`
460
+
461
+ **Files:**
462
+ - Modify: `maxy-code/platform/plugins/memory/PLUGIN.md`
463
+ - Modify: `maxy-code/platform/plugins/docs/references/graph.md`
464
+
465
+ - [ ] **Step 1: Add tool entry to `PLUGIN.md`** (tool list, after the `memory-update` entry)
466
+
467
+ Find:
468
+ ```yaml
469
+ - name: memory-update
470
+ publicAllowlist: false
471
+ adminAllowlist: true
472
+ - name: memory-update-by-name
473
+ ```
474
+
475
+ Replace with:
476
+ ```yaml
477
+ - name: memory-update
478
+ publicAllowlist: false
479
+ adminAllowlist: true
480
+ - name: memory-edge
481
+ publicAllowlist: false
482
+ adminAllowlist: true
483
+ - name: memory-update-by-name
484
+ ```
485
+
486
+ - [ ] **Step 2: Update `PLUGIN.md` description prose**
487
+
488
+ Find the `description:` field's first sentence fragment (around "memory-write, memory-update,"):
489
+ ```
490
+ memory-write, memory-update, memory-update-by-name
491
+ ```
492
+
493
+ Replace with:
494
+ ```
495
+ memory-write, memory-update, memory-edge (create/delete typed directed edges between pre-existing nodes), memory-update-by-name
496
+ ```
497
+
498
+ - [ ] **Step 3: Add edge-write section to `graph.md`** — append at end of file
499
+
500
+ ```markdown
501
+
502
+ ## Direct edge management
503
+
504
+ `memory-edge` creates or deletes a typed directed edge between two nodes that already exist in the graph. Both nodes must belong to the same account — mismatched or foreign nodes are rejected with a structured error before any mutation runs.
505
+
506
+ **Create:** MERGE is idempotent. First call returns `{created: true}`; a repeated call with the same endpoints and type returns `{created: false}`. Properties supplied on the call are stamped onto the relationship on CREATE only; a subsequent idempotent hit does not overwrite them.
507
+
508
+ **Delete:** If the edge is present it is deleted and `{deleted: true}` is returned. If absent, the call is a no-op and returns `{deleted: false}`.
509
+
510
+ `relationshipType` is uppercase-coerced. Types that start with an underscore (e.g. `_SOFT_DELETE`) are reserved for platform internals and are rejected.
511
+
512
+ Typical flow: call `memory-search` for each endpoint to retrieve their `elementId` values, then call `memory-edge action=create relationshipType=RELATES_TO fromId=<id> toId=<id>`. The new edge appears when you hop-expand either endpoint on the `/graph` canvas.
513
+ ```
514
+
515
+ - [ ] **Step 4: Commit**
516
+
517
+ ```bash
518
+ git add maxy-code/platform/plugins/memory/PLUGIN.md maxy-code/platform/plugins/docs/references/graph.md
519
+ git commit -m "$(cat <<'EOF'
520
+ docs: add memory-edge to PLUGIN.md tool list and graph.md edge-write section
521
+
522
+ Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
523
+ Session: 3104d4b4-d03e-4339-b65e-6014223c069e
524
+ EOF
525
+ )"
526
+ ```
527
+
528
+ ---
529
+
530
+ ### Task 5: Update specialist agent templates
531
+
532
+ **Files:**
533
+ - Modify: `maxy-code/platform/templates/specialists/agents/database-operator.md`
534
+ - Modify: `maxy-code/platform/templates/specialists/agents/archive-ingest-operator.md`
535
+ - Modify: `maxy-code/platform/templates/specialists/agents/librarian.md`
536
+
537
+ - [ ] **Step 1: Update `database-operator.md`**
538
+
539
+ Find:
540
+ ```
541
+ tools: Read, mcp__plugin_memory_memory__memory-write, mcp__plugin_memory_memory__memory-update, mcp__plugin_memory_memory__memory-update-by-name,
542
+ ```
543
+
544
+ Replace with:
545
+ ```
546
+ tools: Read, mcp__plugin_memory_memory__memory-write, mcp__plugin_memory_memory__memory-update, mcp__plugin_memory_memory__memory-edge, mcp__plugin_memory_memory__memory-update-by-name,
547
+ ```
548
+
549
+ (Add `mcp__plugin_memory_memory__memory-edge` after `memory-update` and before `memory-update-by-name`.)
550
+
551
+ - [ ] **Step 2: Update `archive-ingest-operator.md`**
552
+
553
+ Find:
554
+ ```
555
+ tools: Read, mcp__plugin_memory_memory__memory-write, mcp__plugin_memory_memory__memory-update, mcp__plugin_memory_memory__memory-search,
556
+ ```
557
+
558
+ Replace with:
559
+ ```
560
+ tools: Read, mcp__plugin_memory_memory__memory-write, mcp__plugin_memory_memory__memory-update, mcp__plugin_memory_memory__memory-edge, mcp__plugin_memory_memory__memory-search,
561
+ ```
562
+
563
+ - [ ] **Step 3: Update `librarian.md`**
564
+
565
+ Find:
566
+ ```
567
+ tools: mcp__plugin_memory_memory__memory-ingest-extract, mcp__plugin_memory_memory__memory-ingest, mcp__plugin_memory_memory__memory-ingest-web, mcp__plugin_memory_memory__memory-archive-write, mcp__plugin_memory_memory__memory-write, mcp__plugin_memory_memory__memory-update, mcp__plugin_memory_memory__memory-search,
568
+ ```
569
+
570
+ Replace with:
571
+ ```
572
+ tools: mcp__plugin_memory_memory__memory-ingest-extract, mcp__plugin_memory_memory__memory-ingest, mcp__plugin_memory_memory__memory-ingest-web, mcp__plugin_memory_memory__memory-archive-write, mcp__plugin_memory_memory__memory-write, mcp__plugin_memory_memory__memory-update, mcp__plugin_memory_memory__memory-edge, mcp__plugin_memory_memory__memory-search,
573
+ ```
574
+
575
+ - [ ] **Step 4: Commit**
576
+
577
+ ```bash
578
+ git add \
579
+ maxy-code/platform/templates/specialists/agents/database-operator.md \
580
+ maxy-code/platform/templates/specialists/agents/archive-ingest-operator.md \
581
+ maxy-code/platform/templates/specialists/agents/librarian.md
582
+ git commit -m "$(cat <<'EOF'
583
+ feat: add memory-edge to write-specialist agent templates
584
+
585
+ Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
586
+ Session: 3104d4b4-d03e-4339-b65e-6014223c069e
587
+ EOF
588
+ )"
589
+ ```
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "memory",
3
- "description": "Graph memory plugin. Provides memory-search (with optional `fields` projection for known-shape lookups), memory-write, memory-update, memory-update-by-name (repair surface that resolves an elementId from a (label, name, accountId) tuple when search cannot return one), memory-lookup-by-name (deterministic read surface that returns nodes by case-insensitive exact `name` match, optionally constrained by labels, bypassing the memory-search ranking stack so a present entity is never missed because a diluted query ranked it below the cut), and the :Report surface (memory-report-write / memory-report-read-latest / memory-report-list) for reading from, writing to, and updating the Neo4j knowledge graph. Includes conversational memory — organic preference learning, evidence-backed recall, and transparent 'what do you know about me?' responses. Document ingestion goes through memory-ingest; the dispatched specialist produces typed-section JSON in-turn from the loaded ontology. Operator-solicited reclassify of an already-stored thread-shaped KD (today: email-thread KDs) is exposed as `kd-classify <attachmentId>` — runs a body-growth gate (proceed iff body grew ≥25% since the last classify, or first-ever classify), stages the body to a temp file under `$ACCOUNT_DIR/tmp/`, and returns a dispatch envelope for the `librarian`; on success `memory-ingest` replaces the prior :Section children and stamps `lastClassifiedAt` + `lastClassifiedBodyLength` on the parent KD. Two modes: `document` (default) for unstructured PDF/web content → :KnowledgeDocument (keyed on `attachmentId`) + :Section, and `chat` for conversation transcripts → :ConversationArchive (keyed on `conversationIdentity`) + :Section chunks; two parent labels, two writer paths chosen by which identity property is set. Conversation-archive Phase 2 splits across two read-only tools (Task 433): `conversation-archive-list-chunks` pages chunk bodies for the dispatched specialist to read in-turn; `conversation-archive-derive-insights` converts the specialist's per-chunk claims into operator-facing proposals with per-kind cypher. `conversation-archive-enrich-rejection` records (or undoes) durable per-row rejections so already-triaged claims do not re-surface on re-runs. Ships five skills: `conversational-memory`, `document-ingest`, `conversation-archive` (source-agnostic transcript ingest for WhatsApp, Telegram, Signal, LinkedIn DMs, Zoom, meeting minutes, iMessage, Slack), `conversation-archive-mcp` (operator-initiated MCP-driven ingest for Granola, Otter, Circleback meetings — sibling of `conversation-archive` sharing the same writer surface and identity formula), and `conversation-archive-enrich` (per-row operator-gated insight derivation over a named archive's chunks).",
3
+ "description": "Graph memory plugin. Provides memory-search (with optional `fields` projection for known-shape lookups), memory-write, memory-update, memory-edge (create/delete typed directed edges between pre-existing nodes), memory-update-by-name (repair surface that resolves an elementId from a (label, name, accountId) tuple when search cannot return one), memory-lookup-by-name (deterministic read surface that returns nodes by case-insensitive exact `name` match, optionally constrained by labels, bypassing the memory-search ranking stack so a present entity is never missed because a diluted query ranked it below the cut), and the :Report surface (memory-report-write / memory-report-read-latest / memory-report-list) for reading from, writing to, and updating the Neo4j knowledge graph. Includes conversational memory — organic preference learning, evidence-backed recall, and transparent 'what do you know about me?' responses. Document ingestion goes through memory-ingest; the dispatched specialist produces typed-section JSON in-turn from the loaded ontology. Operator-solicited reclassify of an already-stored thread-shaped KD (today: email-thread KDs) is exposed as `kd-classify <attachmentId>` — runs a body-growth gate (proceed iff body grew ≥25% since the last classify, or first-ever classify), stages the body to a temp file under `$ACCOUNT_DIR/tmp/`, and returns a dispatch envelope for the `librarian`; on success `memory-ingest` replaces the prior :Section children and stamps `lastClassifiedAt` + `lastClassifiedBodyLength` on the parent KD. Two modes: `document` (default) for unstructured PDF/web content → :KnowledgeDocument (keyed on `attachmentId`) + :Section, and `chat` for conversation transcripts → :ConversationArchive (keyed on `conversationIdentity`) + :Section chunks; two parent labels, two writer paths chosen by which identity property is set. Conversation-archive Phase 2 splits across two read-only tools (Task 433): `conversation-archive-list-chunks` pages chunk bodies for the dispatched specialist to read in-turn; `conversation-archive-derive-insights` converts the specialist's per-chunk claims into operator-facing proposals with per-kind cypher. `conversation-archive-enrich-rejection` records (or undoes) durable per-row rejections so already-triaged claims do not re-surface on re-runs. Ships five skills: `conversational-memory`, `document-ingest`, `conversation-archive` (source-agnostic transcript ingest for WhatsApp, Telegram, Signal, LinkedIn DMs, Zoom, meeting minutes, iMessage, Slack), `conversation-archive-mcp` (operator-initiated MCP-driven ingest for Granola, Otter, Circleback meetings — sibling of `conversation-archive` sharing the same writer surface and identity formula), and `conversation-archive-enrich` (per-row operator-gated insight derivation over a named archive's chunks). Ships three thinking-tool skills: `challenge` (adversarial retrieval — strongest counter-evidence from the graph for a given claim), `connect` (bridge-finding between two topics via shared graph neighbors), and `emerge` (clusters uncategorised KnowledgeDocument and Section nodes into operator-approved Concept proposals).",
4
4
  "version": "0.1.0",
5
5
  "author": {
6
6
  "name": "Rubytech LLC"
@@ -1,6 +1,6 @@
1
1
  ---
2
2
  name: memory
3
- description: "Graph memory plugin. Provides memory-search (with optional `fields` projection for known-shape lookups), memory-write, memory-update, memory-update-by-name (repair surface that resolves an elementId from a (label, name, accountId) tuple when search cannot return one), memory-lookup-by-name (deterministic read surface that returns nodes by case-insensitive exact `name` match, optionally constrained by labels, bypassing the memory-search ranking stack so a present entity is never missed because a diluted query ranked it below the cut), and the :Report surface (memory-report-write / memory-report-read-latest / memory-report-list) for reading from, writing to, and updating the Neo4j knowledge graph. Includes conversational memory — organic preference learning, evidence-backed recall, and transparent 'what do you know about me?' responses. Document ingestion goes through memory-ingest; the dispatched specialist produces typed-section JSON in-turn from the loaded ontology. Operator-solicited reclassify of an already-stored thread-shaped KD (today: email-thread KDs) is exposed as `kd-classify <attachmentId>` — runs a body-growth gate (proceed iff body grew ≥25% since the last classify, or first-ever classify), stages the body to a temp file under `$ACCOUNT_DIR/tmp/`, and returns a dispatch envelope for the `librarian`; on success `memory-ingest` replaces the prior :Section children and stamps `lastClassifiedAt` + `lastClassifiedBodyLength` on the parent KD. Two modes: `document` (default) for unstructured PDF/web content → :KnowledgeDocument (keyed on `attachmentId`) + :Section, and `chat` for conversation transcripts → :ConversationArchive (keyed on `conversationIdentity`) + :Section chunks; two parent labels, two writer paths chosen by which identity property is set. Conversation-archive Phase 2 splits across two read-only tools (Task 433): `conversation-archive-list-chunks` pages chunk bodies for the dispatched specialist to read in-turn; `conversation-archive-derive-insights` converts the specialist's per-chunk claims into operator-facing proposals with per-kind cypher. `conversation-archive-enrich-rejection` records (or undoes) durable per-row rejections so already-triaged claims do not re-surface on re-runs. Ships five skills: `conversational-memory`, `document-ingest`, `conversation-archive` (source-agnostic transcript ingest for WhatsApp, Telegram, Signal, LinkedIn DMs, Zoom, meeting minutes, iMessage, Slack), `conversation-archive-mcp` (operator-initiated MCP-driven ingest for Granola, Otter, Circleback meetings — sibling of `conversation-archive` sharing the same writer surface and identity formula), and `conversation-archive-enrich` (per-row operator-gated insight derivation over a named archive's chunks)."
3
+ description: "Graph memory plugin. Provides memory-search (with optional `fields` projection for known-shape lookups), memory-write, memory-update, memory-edge (create/delete typed directed edges between pre-existing nodes), memory-update-by-name (repair surface that resolves an elementId from a (label, name, accountId) tuple when search cannot return one), memory-lookup-by-name (deterministic read surface that returns nodes by case-insensitive exact `name` match, optionally constrained by labels, bypassing the memory-search ranking stack so a present entity is never missed because a diluted query ranked it below the cut), and the :Report surface (memory-report-write / memory-report-read-latest / memory-report-list) for reading from, writing to, and updating the Neo4j knowledge graph. Includes conversational memory — organic preference learning, evidence-backed recall, and transparent 'what do you know about me?' responses. Document ingestion goes through memory-ingest; the dispatched specialist produces typed-section JSON in-turn from the loaded ontology. Operator-solicited reclassify of an already-stored thread-shaped KD (today: email-thread KDs) is exposed as `kd-classify <attachmentId>` — runs a body-growth gate (proceed iff body grew ≥25% since the last classify, or first-ever classify), stages the body to a temp file under `$ACCOUNT_DIR/tmp/`, and returns a dispatch envelope for the `librarian`; on success `memory-ingest` replaces the prior :Section children and stamps `lastClassifiedAt` + `lastClassifiedBodyLength` on the parent KD. Two modes: `document` (default) for unstructured PDF/web content → :KnowledgeDocument (keyed on `attachmentId`) + :Section, and `chat` for conversation transcripts → :ConversationArchive (keyed on `conversationIdentity`) + :Section chunks; two parent labels, two writer paths chosen by which identity property is set. Conversation-archive Phase 2 splits across two read-only tools (Task 433): `conversation-archive-list-chunks` pages chunk bodies for the dispatched specialist to read in-turn; `conversation-archive-derive-insights` converts the specialist's per-chunk claims into operator-facing proposals with per-kind cypher. `conversation-archive-enrich-rejection` records (or undoes) durable per-row rejections so already-triaged claims do not re-surface on re-runs. Ships five skills: `conversational-memory`, `document-ingest`, `conversation-archive` (source-agnostic transcript ingest for WhatsApp, Telegram, Signal, LinkedIn DMs, Zoom, meeting minutes, iMessage, Slack), `conversation-archive-mcp` (operator-initiated MCP-driven ingest for Granola, Otter, Circleback meetings — sibling of `conversation-archive` sharing the same writer surface and identity formula), and `conversation-archive-enrich` (per-row operator-gated insight derivation over a named archive's chunks). Ships three thinking-tool skills: `challenge` (adversarial retrieval — strongest counter-evidence from the graph for a given claim), `connect` (bridge-finding between two topics via shared graph neighbors), and `emerge` (clusters uncategorised KnowledgeDocument and Section nodes into operator-approved Concept proposals)."
4
4
  tools:
5
5
  - name: memory-search
6
6
  publicAllowlist: true
@@ -11,6 +11,9 @@ tools:
11
11
  - name: memory-update
12
12
  publicAllowlist: false
13
13
  adminAllowlist: true
14
+ - name: memory-edge
15
+ publicAllowlist: false
16
+ adminAllowlist: true
14
17
  - name: memory-update-by-name
15
18
  publicAllowlist: false
16
19
  adminAllowlist: true
@@ -135,6 +138,9 @@ skills:
135
138
  - skills/conversation-archive-enrich/SKILL.md
136
139
  - skills/concept-synthesis/SKILL.md
137
140
  - skills/archive-crawler/SKILL.md
141
+ - skills/challenge/SKILL.md
142
+ - skills/connect/SKILL.md
143
+ - skills/emerge/SKILL.md
138
144
  always: true
139
145
  embed: false
140
146
  mcp: