@rubytech/create-maxy 1.0.743 → 1.0.745

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 (55) hide show
  1. package/package.json +1 -1
  2. package/payload/platform/lib/graph-mcp/dist/__tests__/cypher-validate-write.test.d.ts +2 -0
  3. package/payload/platform/lib/graph-mcp/dist/__tests__/cypher-validate-write.test.d.ts.map +1 -0
  4. package/payload/platform/lib/graph-mcp/dist/__tests__/cypher-validate-write.test.js +97 -0
  5. package/payload/platform/lib/graph-mcp/dist/__tests__/cypher-validate-write.test.js.map +1 -0
  6. package/payload/platform/lib/graph-mcp/dist/cypher-rewrite-stamp.d.ts +37 -0
  7. package/payload/platform/lib/graph-mcp/dist/cypher-rewrite-stamp.d.ts.map +1 -0
  8. package/payload/platform/lib/graph-mcp/dist/cypher-rewrite-stamp.js +333 -0
  9. package/payload/platform/lib/graph-mcp/dist/cypher-rewrite-stamp.js.map +1 -0
  10. package/payload/platform/lib/graph-mcp/dist/cypher-shim-write.d.ts +71 -0
  11. package/payload/platform/lib/graph-mcp/dist/cypher-shim-write.d.ts.map +1 -0
  12. package/payload/platform/lib/graph-mcp/dist/cypher-shim-write.js +168 -0
  13. package/payload/platform/lib/graph-mcp/dist/cypher-shim-write.js.map +1 -0
  14. package/payload/platform/lib/graph-mcp/dist/cypher-validate.d.ts +13 -1
  15. package/payload/platform/lib/graph-mcp/dist/cypher-validate.d.ts.map +1 -1
  16. package/payload/platform/lib/graph-mcp/dist/cypher-validate.js +70 -3
  17. package/payload/platform/lib/graph-mcp/dist/cypher-validate.js.map +1 -1
  18. package/payload/platform/lib/graph-mcp/dist/index.js +297 -11
  19. package/payload/platform/lib/graph-mcp/dist/index.js.map +1 -1
  20. package/payload/platform/lib/graph-mcp/dist/schema-cache.d.ts +3 -6
  21. package/payload/platform/lib/graph-mcp/dist/schema-cache.d.ts.map +1 -1
  22. package/payload/platform/lib/graph-mcp/dist/schema-cache.js +30 -7
  23. package/payload/platform/lib/graph-mcp/dist/schema-cache.js.map +1 -1
  24. package/payload/platform/lib/graph-mcp/src/__tests__/cypher-validate-write.test.ts +150 -0
  25. package/payload/platform/lib/graph-mcp/src/cypher-rewrite-stamp.ts +349 -0
  26. package/payload/platform/lib/graph-mcp/src/cypher-shim-write.ts +240 -0
  27. package/payload/platform/lib/graph-mcp/src/cypher-validate.ts +95 -3
  28. package/payload/platform/lib/graph-mcp/src/index.ts +415 -18
  29. package/payload/platform/lib/graph-mcp/src/schema-cache.ts +37 -7
  30. package/payload/platform/lib/graph-write/dist/__tests__/audit.test.d.ts +2 -0
  31. package/payload/platform/lib/graph-write/dist/__tests__/audit.test.d.ts.map +1 -0
  32. package/payload/platform/lib/graph-write/dist/__tests__/audit.test.js +147 -0
  33. package/payload/platform/lib/graph-write/dist/__tests__/audit.test.js.map +1 -0
  34. package/payload/platform/lib/graph-write/dist/audit.d.ts +84 -0
  35. package/payload/platform/lib/graph-write/dist/audit.d.ts.map +1 -0
  36. package/payload/platform/lib/graph-write/dist/audit.js +129 -0
  37. package/payload/platform/lib/graph-write/dist/audit.js.map +1 -0
  38. package/payload/platform/lib/graph-write/dist/index.d.ts +1 -0
  39. package/payload/platform/lib/graph-write/dist/index.d.ts.map +1 -1
  40. package/payload/platform/lib/graph-write/dist/index.js +18 -22
  41. package/payload/platform/lib/graph-write/dist/index.js.map +1 -1
  42. package/payload/platform/lib/graph-write/src/__tests__/audit.test.ts +162 -0
  43. package/payload/platform/lib/graph-write/src/audit.ts +182 -0
  44. package/payload/platform/lib/graph-write/src/index.ts +5 -0
  45. package/payload/platform/package.json +2 -2
  46. package/payload/platform/plugins/docs/references/deployment.md +2 -1
  47. package/payload/platform/plugins/docs/references/memory-guide.md +2 -0
  48. package/payload/platform/plugins/docs/references/troubleshooting.md +16 -0
  49. package/payload/platform/templates/specialists/agents/database-operator.md +20 -6
  50. package/payload/server/chunk-2T4RRIJK.js +9462 -0
  51. package/payload/server/chunk-SPTD7L7Z.js +9474 -0
  52. package/payload/server/maxy-edge.js +94 -16
  53. package/payload/server/public/assets/{graph-DhNy70eS.js → graph-BoSJpLG3.js} +1 -1
  54. package/payload/server/public/graph.html +1 -1
  55. package/payload/server/server.js +1 -1
@@ -0,0 +1,182 @@
1
+ /**
2
+ * Post-write audit for the database-operator's raw `write_neo4j_cypher` tool
3
+ * (Task 796). Sibling to `[graph-write] accepted` for the wrapped writers
4
+ * (`memory-write`, `contact-create`, etc. — Task 673) — same module so the
5
+ * `[graph-*]` log family stays uniform across both write surfaces.
6
+ *
7
+ * Design posture:
8
+ * - Static parse first. Pre-flight regex on the cypher text catches missing
9
+ * provenance stamps and unknown edge types cheaply, no driver round-trip.
10
+ * - Dynamic orphan detection is the caller's responsibility — the graph-mcp
11
+ * shim runs the detection query against Neo4j after the upstream commits
12
+ * and passes the resulting elementId list as `orphanIds`. Keeping the
13
+ * module pure (no neo4j-driver import) makes it unit-testable without a
14
+ * live database.
15
+ * - Audit is observational. Warnings never block the write. The
16
+ * prompt-level Graph Stewardship Doctrine in
17
+ * [database-operator.md](../../../templates/specialists/agents/database-operator.md)
18
+ * names the discipline; the audit is the verification stream.
19
+ *
20
+ * String literals are stripped before pattern checks so that property values
21
+ * containing words like `'createdByAgent in my bio'` cannot trip a false
22
+ * provenance count, and edge-like patterns inside quoted strings cannot
23
+ * trip a false unknown-type warning. The regex shape is duplicated from
24
+ * [cypher-validate.ts](../../graph-mcp/src/cypher-validate.ts) intentionally:
25
+ * graph-write does not depend on graph-mcp, and a one-line regex helper
26
+ * doesn't justify a third package. Drift in either copy is a regression
27
+ * surfaced by both the validator tests and the audit tests.
28
+ */
29
+
30
+ export interface SchemaSnapshot {
31
+ readonly labels: ReadonlySet<string>;
32
+ readonly relationshipTypes: ReadonlySet<string>;
33
+ }
34
+
35
+ export interface CypherWriteAuditInput {
36
+ cypher: string;
37
+ schema: SchemaSnapshot;
38
+ agentName: string;
39
+ sessionId: string;
40
+ /** Counter from upstream response — drives missing-provenance arithmetic. */
41
+ nodesCreated: number;
42
+ /** Counter from upstream response — informational only. */
43
+ relsCreated: number;
44
+ /**
45
+ * elementIds of nodes the post-write orphan query identified. Caller scopes
46
+ * the query to (createdBySession, createdAt >= writeStartTimestamp) so this
47
+ * list cannot include nodes from prior writes in the same session. Empty
48
+ * array = no orphans (or no scope to check).
49
+ */
50
+ orphanIds: string[];
51
+ }
52
+
53
+ export type AuditWarning =
54
+ | { kind: "orphan-warning"; orphanIds: string[] }
55
+ | { kind: "unknown-type-warning"; type: string }
56
+ | { kind: "missing-provenance-warning"; created: number; stamped: number };
57
+
58
+ export type AuditLine =
59
+ | {
60
+ kind: "accepted";
61
+ cypherPrefix: string;
62
+ nodesCreated: number;
63
+ relsCreated: number;
64
+ agentName: string;
65
+ sessionId: string;
66
+ }
67
+ | { kind: "orphan-warning"; cypherPrefix: string; orphanIds: string[] }
68
+ | { kind: "unknown-type-warning"; cypherPrefix: string; type: string }
69
+ | {
70
+ kind: "missing-provenance-warning";
71
+ cypherPrefix: string;
72
+ created: number;
73
+ stamped: number;
74
+ };
75
+
76
+ // Mirrors cypher-validate's pattern. Captures TYPE(|TYPE)* alternation; the
77
+ // audit splits on `|` to enumerate every referenced type.
78
+ const EDGE_PATTERN = /\[[^\]]*?:([A-Z_][A-Za-z0-9_]*(?:\|[A-Z_][A-Za-z0-9_]*)*)[^\]]*?\]/g;
79
+
80
+ // Counts CREATE (n:Label) and MERGE (n:Label) statements that introduce a
81
+ // node. The trailing `[A-Z]` requires at least one label (so `CREATE INDEX`
82
+ // and bare `CREATE (a)-[:R]->(b)` shapes don't trip this — bare patterns
83
+ // reference a previously-MATCHed alias and don't introduce new nodes).
84
+ const CREATE_OR_MERGE_NODE = /\b(?:CREATE|MERGE)\s*\(\s*[A-Za-z_][A-Za-z0-9_]*\s*:\s*[A-Z]/g;
85
+
86
+ // Stamp tokens — at least one of these MUST appear per created node for the
87
+ // write to satisfy provenance discipline. The audit counts occurrences;
88
+ // stamp-count >= node-count is the pass criterion.
89
+ const PROVENANCE_TOKEN = /\bcreatedBy(?:Agent|Tool|Session|Source)\b/g;
90
+
91
+ function stripStringLiterals(cypher: string): string {
92
+ // Identical regex to cypher-validate.ts. Replaces single- and double-
93
+ // quoted literals with empty quotes so e.g. 'CREATE (n:Person)' inside a
94
+ // property value cannot inflate the create-count.
95
+ return cypher.replace(/'[^']*'|"[^"]*"/g, '""');
96
+ }
97
+
98
+ function extractEdgeTypes(cleaned: string): Set<string> {
99
+ const out = new Set<string>();
100
+ // Use String.matchAll for stateless iteration — no shared regex lastIndex.
101
+ for (const m of cleaned.matchAll(EDGE_PATTERN)) {
102
+ for (const t of m[1].split("|")) {
103
+ const clean = t.trim();
104
+ if (clean) out.add(clean);
105
+ }
106
+ }
107
+ return out;
108
+ }
109
+
110
+ function countCreateOrMergeNodes(cleaned: string): number {
111
+ const matches = cleaned.match(CREATE_OR_MERGE_NODE);
112
+ return matches ? matches.length : 0;
113
+ }
114
+
115
+ function countProvenanceStamps(cleaned: string): number {
116
+ const matches = cleaned.match(PROVENANCE_TOKEN);
117
+ return matches ? matches.length : 0;
118
+ }
119
+
120
+ export function auditCypherWrite(input: CypherWriteAuditInput): AuditWarning[] {
121
+ const warnings: AuditWarning[] = [];
122
+ const cleaned = stripStringLiterals(input.cypher);
123
+
124
+ // 1. Unknown edge type — cypher names a type not in the live ontology.
125
+ // Enumerate every referenced type; emit one warning per unknown.
126
+ const referencedTypes = extractEdgeTypes(cleaned);
127
+ for (const t of referencedTypes) {
128
+ if (!input.schema.relationshipTypes.has(t)) {
129
+ warnings.push({ kind: "unknown-type-warning", type: t });
130
+ }
131
+ }
132
+
133
+ // 2. Missing provenance — count CREATE/MERGE-with-label statements vs
134
+ // `createdBy*` token occurrences. Stamps may live in inline maps
135
+ // (`{createdByAgent: $agent}`) or SET clauses; both shapes contribute
136
+ // to the token count. The arithmetic is precision-imprecise — a
137
+ // multi-stamp single CREATE inflates `stamped`, a multi-CREATE single
138
+ // SET undercounts — but the directional signal (zero stamps for many
139
+ // creates) catches the failure mode this audit exists to surface.
140
+ //
141
+ // nodesCreated=0 short-circuits the check: the upstream counter is
142
+ // ground truth for whether any node was actually persisted. An
143
+ // idempotent MERGE that matched an existing node has a static
144
+ // create-count of 1 but contributed no new node — no provenance was
145
+ // needed, and warning here would be a false positive.
146
+ if (input.nodesCreated > 0) {
147
+ const createOrMergeNodes = countCreateOrMergeNodes(cleaned);
148
+ if (createOrMergeNodes > 0) {
149
+ const stamps = countProvenanceStamps(cleaned);
150
+ if (stamps < createOrMergeNodes) {
151
+ warnings.push({
152
+ kind: "missing-provenance-warning",
153
+ created: createOrMergeNodes,
154
+ stamped: stamps,
155
+ });
156
+ }
157
+ }
158
+ }
159
+
160
+ // 3. Orphans — caller-supplied. The dynamic detection happens at the
161
+ // graph-mcp shim layer (which has the Neo4j driver); the audit module
162
+ // only renders the warning shape.
163
+ if (input.orphanIds.length > 0) {
164
+ warnings.push({ kind: "orphan-warning", orphanIds: input.orphanIds });
165
+ }
166
+
167
+ return warnings;
168
+ }
169
+
170
+ export function formatAuditLine(line: AuditLine): string {
171
+ const prefixField = `query="${line.cypherPrefix.replace(/"/g, "'")}"`;
172
+ switch (line.kind) {
173
+ case "accepted":
174
+ return `[graph-cypher-write] accepted ${prefixField} nodesCreated=${line.nodesCreated} relsCreated=${line.relsCreated} agentName=${line.agentName} sessionId=${line.sessionId}`;
175
+ case "orphan-warning":
176
+ return `[graph-cypher-write] orphan-warning ${prefixField} orphanIds=${line.orphanIds.join(",")}`;
177
+ case "unknown-type-warning":
178
+ return `[graph-cypher-write] unknown-type-warning ${prefixField} type=${line.type}`;
179
+ case "missing-provenance-warning":
180
+ return `[graph-cypher-write] missing-provenance-warning ${prefixField} created=${line.created} stamped=${line.stamped}`;
181
+ }
182
+ }
@@ -1,3 +1,8 @@
1
+ // Task 796: re-export the post-write audit so consumers (graph-mcp shim)
2
+ // import from the same package as `writeNodeWithEdges`. Keeps the
3
+ // `[graph-*]` log family colocated.
4
+ export * from "./audit.js";
5
+
1
6
  /**
2
7
  * Write doctrine (Task 673): a node without at least one adjacency is noise,
3
8
  * not knowledge. `writeNodeWithEdges` is the single primitive every graph
@@ -6,8 +6,8 @@
6
6
  "plugins/*/mcp"
7
7
  ],
8
8
  "scripts": {
9
- "build": "tsc -p lib/models/tsconfig.json && tsc -p lib/anthropic-key/tsconfig.json && tsc -p lib/oauth-llm/tsconfig.json && tsc -p lib/mcp-stderr-tee/tsconfig.json && tsc -p lib/mcp-spawn-tee/tsconfig.json && tsc -p lib/graph-mcp/tsconfig.json && tsc -p lib/graph-trash/tsconfig.json && tsc -p lib/graph-search/tsconfig.json && tsc -p lib/graph-write/tsconfig.json && tsc -p lib/screening-patterns/tsconfig.json && tsc -p lib/device-url/tsconfig.json && tsc -p lib/brand-templating/tsconfig.json && NODE_OPTIONS='--max-old-space-size=8192' tsc -b plugins/*/mcp/tsconfig.json",
10
- "build:lib": "tsc -p lib/models/tsconfig.json && tsc -p lib/anthropic-key/tsconfig.json && tsc -p lib/oauth-llm/tsconfig.json && tsc -p lib/mcp-stderr-tee/tsconfig.json && tsc -p lib/mcp-spawn-tee/tsconfig.json && tsc -p lib/graph-mcp/tsconfig.json && tsc -p lib/graph-trash/tsconfig.json && tsc -p lib/graph-search/tsconfig.json && tsc -p lib/graph-write/tsconfig.json && tsc -p lib/screening-patterns/tsconfig.json && tsc -p lib/device-url/tsconfig.json && tsc -p lib/brand-templating/tsconfig.json",
9
+ "build": "tsc -p lib/models/tsconfig.json && tsc -p lib/anthropic-key/tsconfig.json && tsc -p lib/oauth-llm/tsconfig.json && tsc -p lib/mcp-stderr-tee/tsconfig.json && tsc -p lib/mcp-spawn-tee/tsconfig.json && tsc -p lib/graph-write/tsconfig.json && tsc -p lib/graph-mcp/tsconfig.json && tsc -p lib/graph-trash/tsconfig.json && tsc -p lib/graph-search/tsconfig.json && tsc -p lib/screening-patterns/tsconfig.json && tsc -p lib/device-url/tsconfig.json && tsc -p lib/brand-templating/tsconfig.json && NODE_OPTIONS='--max-old-space-size=8192' tsc -b plugins/*/mcp/tsconfig.json",
10
+ "build:lib": "tsc -p lib/models/tsconfig.json && tsc -p lib/anthropic-key/tsconfig.json && tsc -p lib/oauth-llm/tsconfig.json && tsc -p lib/mcp-stderr-tee/tsconfig.json && tsc -p lib/mcp-spawn-tee/tsconfig.json && tsc -p lib/graph-write/tsconfig.json && tsc -p lib/graph-mcp/tsconfig.json && tsc -p lib/graph-trash/tsconfig.json && tsc -p lib/graph-search/tsconfig.json && tsc -p lib/screening-patterns/tsconfig.json && tsc -p lib/device-url/tsconfig.json && tsc -p lib/brand-templating/tsconfig.json",
11
11
  "build:memory": "tsc -p plugins/memory/mcp/tsconfig.json",
12
12
  "build:contacts": "tsc -p plugins/contacts/mcp/tsconfig.json",
13
13
  "build:telegram": "tsc -p plugins/telegram/mcp/tsconfig.json",
@@ -100,7 +100,8 @@ systemctl --user daemon-reload
100
100
  A single Pi or laptop can host more than one brand (for example Maxy and Real Agent) side by side. Each brand runs as its own service on its own port, with its own install directory and its own data. Installing one brand does not touch the other.
101
101
 
102
102
  - **Separate:** each brand has its own install folder (`~/maxy/`, `~/realagent/`), its own config folder (`~/.maxy/`, `~/.realagent/`), its own web port, its own Cloudflare tunnel state, its own edge systemd unit (`maxy-edge.service` vs `realagent-edge.service`), and by default its own Neo4j database (Maxy on bolt port 7687, Real Agent on 7688). Action runner units are transient and per-invocation, not per-brand, so no naming conflict is possible.
103
- - **Brand-isolated Neo4j (Task 787):** when a brand provisions a dedicated Neo4j instance (any port other than 7687), the installer stops and disables the apt-package's system `neo4j.service` after enabling the brand-dedicated unit, so only one Neo4j process holds the shared `/var/lib/neo4j/run/` PID file. The seed step receives the brand-correct `NEO4J_URI` and `NEO4J_PASSWORD` as explicit environment variables — the seed script no longer carries a `bolt://localhost:7687` default. A failed dedicated start aborts the install loudly with a journalctl tail; there is no silent fallback to the system instance. Stop/disable targets the literal `neo4j.service` only, so re-running another brand's installer never touches a peer brand's `neo4j-{brand}.service`.
103
+ - **Brand-isolated Neo4j (Task 787):** when a brand provisions a dedicated Neo4j instance (any port other than 7687), the installer stops and disables the apt-package's system `neo4j.service` after enabling the brand-dedicated unit, so only one Neo4j process holds the shared `/var/lib/neo4j/run/` PID file. The seed step receives the brand-correct `NEO4J_URI` and `NEO4J_PASSWORD` as explicit environment variables — the seed script no longer carries a `bolt://localhost:7687` default. A failed dedicated start aborts the install loudly with a journalctl tail; there is no silent fallback to the system instance. Stop/disable targets the literal `neo4j.service` only, so peer brands running their own `neo4j-{brand}.service` are unaffected.
104
+ - **Multi-brand-host caveat (Task 800):** on a host where the default brand keeps using the shared system `neo4j.service` per Task 659 V3 (e.g. a laptop running Maxy on default port 7687 alongside a branded build on a dedicated port), the Task 787 disable above kills the default brand's database when a branded installer runs. Symptom: default-brand admin server keeps running but bolt port 7687 has no listener; the branded install log contains `[neo4j] disabling system unit (brand-dedicated active on port <branded-port>)` immediately followed by `Stopping neo4j.service` in the systemd journal. Data on disk is intact (`disable` only removes the WantedBy symlink); recovery is `sudo systemctl enable neo4j && sudo systemctl start neo4j`. Task 800 adds a peer-brand guard so the disable is skipped when any `~/.<brand>/.env` pins `NEO4J_URI=bolt://localhost:7687`. Until Task 800 lands, repeat the recovery after each branded `create-{brand}` upgrade on a multi-brand host.
104
105
  - **Shared:** both brands share the system Chromium/VNC stack, the Ollama model server, and the `cloudflared` command itself. Browser automation is serialised — one admin session at a time across both brands.
105
106
 
106
107
  To install a second brand on a device that already runs the first, just run the other installer. No flags needed for isolation:
@@ -106,6 +106,8 @@ Every new node in {{productName}}'s graph is created with at least one connectio
106
106
 
107
107
  Every node also carries a provenance stamp — which agent wrote it, in which session, via which tool. You never see these fields, but they are how operators trace unusual growth back to the code path that produced it, and why your graph stays clean over time.
108
108
 
109
+ **Two write surfaces, one substrate.** General agents write through schema-aware helpers — {{productName}} can record a new contact, a new commitment, a new preference without ever typing a database query, and the helper enforces the connection-and-provenance rule above structurally. The graph-steward role (the specialist {{productName}} dispatches when you ask for graph hygiene — "merge those two duplicate contacts," "wire those four tasks to the meeting," "rename the legacy label across the graph") additionally has a raw Cypher write tool for the multi-step operations the helpers cannot express. The steward role internalises the same connection-and-provenance discipline in its prompt; a post-write audit emits a warning on every breach so the same rules apply to both surfaces. Both paths feed the same hourly orphan trend and the same forensic provenance fields — read-side, you cannot tell the two apart, and that is the point.
110
+
109
111
  ## Privacy
110
112
 
111
113
  All memory is stored on your local Raspberry Pi. The Neo4j database never leaves your network. {{productName}} does not sync memory to any cloud service or third party.
@@ -121,6 +121,22 @@ If the initial Cloudflare login fails during setup, {{productName}} will fall ba
121
121
 
122
122
  ---
123
123
 
124
+ ## "Bad Gateway" or holding page during an upgrade
125
+
126
+ Task 795 — `maxy-edge.service` (always-on front door) classifies upstream errors and serves a brand-aware response. There are two distinct user-visible shapes; the right one depends on what failed.
127
+
128
+ **Branded holding page ("{{productName}} is starting") for ~10 s during an upgrade — this is expected and self-healing.** The edge process binds the public port immediately, but `maxy.service` (the upstream UI) takes ~10 s after restart to apply the neo4j schema and mount its 11 routes. Any browser navigation that lands during that window gets a self-contained HTML holding page that polls `/api/health` and reloads automatically once the upstream binds. No operator action required. The diagnostic line in `~/.maxy/logs/edge.log` is `[edge] upstream http error path=… err=connect ECONNREFUSED 127.0.0.1:<UPSTREAM_PORT> err-class=econnrefused-coldstart upstream=…` and disappears as soon as upstream binds.
129
+
130
+ **Branded plain-text 502 ("Bad Gateway ({{productName}} unavailable)") — real upstream failure, not cold-start.** Any error class other than `ECONNREFUSED` (timeouts, resets, host-unreachable) returns the existing 502 path. The diagnostic line carries `err-class=other`. Read the log with `tail -200 ~/.maxy/logs/edge.log | rg 'err-class=other'` and check `~/.maxy/logs/server.log` for upstream stack traces — the upstream itself is the source.
131
+
132
+ **Continuous `err-class=econnrefused-coldstart` for >30 s past the last `[edge] listening` line** indicates the upstream never binds — the upgrade or boot has stalled. Recover via `sudo systemctl --user status maxy.service` and check the action runner log per the next section. Permanent-failure UI escalation (turning the holding page into an error after N seconds) is intentionally deferred.
133
+
134
+ **The literal string `maxy-ui` should never appear in `edge.log` or in any user-visible 502 body**, regardless of brand. If it does, the edge is running pre-Task-795 code — re-bundle and re-publish.
135
+
136
+ **Verifying the holding page locally:** `curl -sS -H 'Accept: text/html' http://127.0.0.1:<EDGE_PORT>/` while `maxy.service` is stopped should return HTML containing the brand `productName`. The `Accept: text/html` header is required — non-html clients (default `curl`, `fetch()`, XHR) get the branded plain-text 502 instead, so the holding page's own `/api/health` polls don't break themselves during cold-start.
137
+
138
+ ---
139
+
124
140
  ## Action runner — upgrade or Cloudflare setup appears stuck
125
141
 
126
142
  Task 664 replaced the ttyd/xterm admin terminal with a detached action runner. Upgrades and Cloudflare setup now run under transient `systemd-run --user` units whose stdout+stderr land in a persisted per-action log, streamed to the browser via SSE. Task 666 moved the four routes that serve the modal (`/api/admin/actions/*`, `/api/admin/version`) onto `maxy-edge.service`, so the log panel's stream survives a mid-run restart of `maxy.service` without reconnecting.
@@ -3,7 +3,7 @@ name: database-operator
3
3
  description: "Document and archive ingestion and ad-hoc graph operations — running the universal `document-ingest` skill for any unstructured document (PDF, text, transcript, web page, audio, video) and per-source archive-import skills (LinkedIn Basic Data Export today; CRM-type seed archives as each plugin ships), plus operator-driven graph hygiene (prune orphans, deduplicate entities, add edges, normalise labels). Delegate when the operator uploads any document, drops an archive directory into chat, or asks for any graph operation that is not a routine per-turn write."
4
4
  summary: "Ingests every unstructured document and external archive into your graph (LinkedIn today; other CRM sources in future) and handles ad-hoc graph tidy-ups on request. For example, when you upload a CV, a pricing guide, or a contract; when you drop a LinkedIn export folder into chat; or when you ask to prune orphan nodes, merge duplicate people, or add edges between entities."
5
5
  model: claude-sonnet-4-6
6
- tools: Read, Bash, Glob, Grep, mcp__graph__maxy-graph-read_neo4j_cypher, mcp__graph__maxy-graph-get_neo4j_schema, mcp__memory__memory-write, mcp__memory__memory-update, mcp__memory__memory-delete, mcp__memory__memory-search, mcp__memory__memory-rank, mcp__memory__memory-reindex, mcp__memory__memory-find-candidates, mcp__memory__memory-ingest, mcp__memory__memory-ingest-extract, mcp__memory__memory-ingest-web, mcp__memory__memory-classify, mcp__memory__memory-archive-write, mcp__memory__graph-prune-denylist-list, mcp__memory__graph-prune-denylist-add, mcp__memory__graph-prune-denylist-remove, mcp__contacts__contact-create, mcp__contacts__contact-update, mcp__contacts__contact-lookup, mcp__contacts__contact-list, mcp__admin__file-attach, mcp__admin__plugin-read
6
+ tools: Read, Bash, Glob, Grep, mcp__graph__maxy-graph-read_neo4j_cypher, mcp__graph__maxy-graph-write_neo4j_cypher, mcp__graph__maxy-graph-get_neo4j_schema, mcp__memory__memory-write, mcp__memory__memory-update, mcp__memory__memory-delete, mcp__memory__memory-search, mcp__memory__memory-rank, mcp__memory__memory-reindex, mcp__memory__memory-find-candidates, mcp__memory__memory-ingest, mcp__memory__memory-ingest-extract, mcp__memory__memory-ingest-web, mcp__memory__memory-classify, mcp__memory__memory-archive-write, mcp__memory__graph-prune-denylist-list, mcp__memory__graph-prune-denylist-add, mcp__memory__graph-prune-denylist-remove, mcp__contacts__contact-create, mcp__contacts__contact-update, mcp__contacts__contact-lookup, mcp__contacts__contact-list, mcp__admin__file-attach, mcp__admin__plugin-read
7
7
  ---
8
8
 
9
9
  # Database Operator
@@ -124,6 +124,20 @@ Future CRM-type seed plugins (HubSpot, Salesforce, Pipedrive, iCloud contacts, G
124
124
 
125
125
  When the admin delegates a graph operation — pruning, dedup, edge addition, label normalisation, schema-drift tidy — follow this discipline.
126
126
 
127
+ You wield two write surfaces. Wrapped writers (`memory-write`, `memory-update`, `memory-delete`, `memory-find-candidates`, `contact-create`, etc.) are schema-aware helpers that enforce orphan-prevention and provenance structurally — the right surface for single-node creates, contact ingestion, soft-delete sweeps. Raw Cypher via `mcp__graph__maxy-graph-write_neo4j_cypher` is the right surface for the writes wrapped helpers cannot express: edges between two pre-existing nodes, multi-statement hygiene (`DETACH DELETE` orphans, `apoc.refactor.mergeNodes` duplicates, `REMOVE :OldLabel SET :NewLabel`), and any operation that scopes a transaction across multiple matched node sets. The graph is the account's knowledge substrate; raw Cypher is power tooling for the role responsible for that substrate.
128
+
129
+ ### Graph stewardship doctrine — raw Cypher writes
130
+
131
+ Two rules govern every raw Cypher write you author. They require LLM judgement — the structural gate cannot make these calls for you. Two further rules (provenance, orphan-prevention) are now structurally enforced by the shim and need no prose discipline.
132
+
133
+ **1. Every node has ≥1 typed edge in the same transaction.** A bare `CREATE (n:Person {name: 'Ian'})` is data, not knowledge — retrieval finds the node but it carries no context. Anchor every node creation to the relationship that explains it: `MATCH (anchor) WHERE … CREATE (anchor)-[:TYPE]->(n:Label {…})`, or `MERGE` the node and its edge in the same statement. *Why:* a graph that accumulates islands defeats relationship-traversal queries — the value of the graph is in the edges, not the rows.
134
+
135
+ **2. Every edge type is in the live ontology.** Inventing types fragments retrieval — `KNOWS` ≠ `knows` ≠ `HAS_KNOWN`. Call `mcp__graph__maxy-graph-get_neo4j_schema` before authoring any write whose edge type you are not certain about; if no fitting type exists, stop and ask the admin agent for ontology guidance — never coin a synonym. *Why:* edge typology compounds over time. A synonym today blocks every future query that expected the canonical type, and the only fix is a label-rewrite Cypher pass that touches the same edge from both sides.
136
+
137
+ **Structural enforcement (Task 797).** The shim auto-stamps `createdAt`, `createdByAgent`, `createdByTool`, `createdBySession` on every `CREATE`/`MERGE` alias before forwarding to Neo4j — you do not write these properties yourself. The shim runs the cypher inside a managed `executeWrite` and self-audits for unattached nodes before committing; if any node you created has zero edges in the same transaction, the entire transaction rolls back and you receive a structured error naming the orphan label(s). Treat the rollback as a hard failure (do not retry the same cypher); your job is to author atomic CREATE/MERGE-with-edge statements per Rule 1, not to write defensive WITH/MATCH/RETURN audits or hand-written SET clauses for `createdBy*` fields. The `[graph-cypher-write]` audit lines (`auto-stamp applied`, `accepted`, `orphan-rollback`, `orphan-warning`, `missing-provenance-warning`, `unknown-type-warning`) name what the structural enforcement saw — they are observation surfaces, not duties.
138
+
139
+ The two rules together replace the LOUD-FAIL improvisation pattern that prior versions of this prompt prescribed when a wrapped writer lacked an edge-between-existing-nodes path. You no longer loud-fail on missing graph-write tools — you have them. You loud-fail on credentials, on out-of-surface tools (a skill prescribing a non-graph MCP token you do not hold), and on dispatched skills whose prerequisites are unmet — exactly as the LOUD-FAIL prerogative names.
140
+
127
141
  ### Before writing any Cypher
128
142
 
129
143
  1. **Consult the SCHEMA block.** Your MCP tool set includes `maxy-graph-get_neo4j_schema` for live schema snapshots. Call it before authoring any write Cypher you are not certain about. The upstream cypher-mcp validator rejects writes with unknown labels or edges; catch the mismatch upfront, not at the rejection.
@@ -148,14 +162,14 @@ When two nodes represent the same real-world entity (two `:Person` rows for the
148
162
 
149
163
  1. Read both via `memory-search` or direct `maxy-graph-read_neo4j_cypher`.
150
164
  2. Decide which is the survivor (usually the older `createdAt`, or the one with richer properties). State the choice.
151
- 3. Copy missing properties from the duplicate onto the survivor via `memory-update`.
152
- 4. Re-point every edge from the duplicate onto the survivor via `memory-update` (or a targeted `memory-write` with the new relationship and a follow-up `memory-delete` of the old one).
153
- 5. `memory-delete` the duplicate with a single-node call — the operator named this merge explicitly, no filter-token needed.
165
+ 3. Property reconciliation: `memory-update` for property copies, or `write_neo4j_cypher` with `apoc.refactor.mergeNodes([survivor, duplicate])` when the merge is multi-edge (apoc reparents every relationship onto the survivor in one transaction). Stamp `mergedAt`, `mergedFromAgent`, `mergedFromSession` per the stewardship doctrine.
166
+ 4. For property-only merges that leave edges unchanged, follow up with `memory-update` on the survivor and `memory-delete` on the duplicate. The duplicate delete is single-node, no filter-token needed (the operator named this merge explicitly).
154
167
 
155
168
  ### Edge addition, label normalisation, prune-denylist
156
169
 
157
- - **Edge addition** — `memory-write` with a `relationships` payload naming the exact edge type from the schema. Validator rejects unknown edge types; re-read the schema before authoring.
158
- - **Label normalisation** — call `memory-update` with the new label set. Never rename a label across the whole graph without a named owner asking for it label renames are schema changes.
170
+ - **Edge addition between two existing nodes** — raw Cypher via `mcp__graph__maxy-graph-write_neo4j_cypher`: `MATCH (a:LabelA {…}), (b:LabelB {…}) MERGE (a)-[r:TYPE]->(b) SET r.createdAt = datetime(), r.createdByAgent = $agent, r.createdByTool = 'graph-cypher-write', r.createdBySession = $sessionId`. The wrapped `memory-write` requires a new node payload — for edge-only writes, raw Cypher is the surface.
171
+ - **Edge addition that creates a new node** — `memory-write` with a `relationships` payload naming the exact edge type. Validator rejects unknown edge types structurally; re-read the schema before authoring.
172
+ - **Label normalisation** — `write_neo4j_cypher` with `MATCH (n:OldLabel) WHERE … REMOVE n:OldLabel SET n:NewLabel SET n.relabelledAt = datetime(), n.relabelledByAgent = $agent`. Bulk renames are schema changes — always confirm the owner / scope with the admin before executing across the whole graph.
159
173
  - **Prune denylist** — when the operator wants to preserve specific nodes from future prune passes (current or scheduled-autonomous), use `graph-prune-denylist-add/remove/list` to manage the registry.
160
174
 
161
175
  ---