@rubytech/create-maxy-code 0.1.442 → 0.1.443

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 (37) hide show
  1. package/dist/__tests__/converge-client-admins.test.js +50 -0
  2. package/dist/converge-client-admins.js +67 -0
  3. package/dist/index.js +8 -0
  4. package/package.json +1 -1
  5. package/payload/platform/docs/superpowers/plans/2026-07-13-account-schema-ontology-projection.md +547 -0
  6. package/payload/platform/docs/superpowers/plans/2026-07-14-graph-top-level-labels-ontology-single-source.md +458 -0
  7. package/payload/platform/docs/superpowers/specs/2026-07-13-account-schema-ontology-projection-design.md +178 -0
  8. package/payload/platform/docs/superpowers/specs/2026-07-14-graph-top-level-labels-ontology-single-source-design.md +119 -0
  9. package/payload/platform/plugins/admin/.claude-plugin/plugin.json +1 -1
  10. package/payload/platform/plugins/admin/PLUGIN.md +7 -1
  11. package/payload/platform/plugins/admin/hooks/__tests__/fs-schema-guard.test.sh +19 -2
  12. package/payload/platform/plugins/admin/mcp/dist/index.js +126 -1
  13. package/payload/platform/plugins/admin/mcp/dist/index.js.map +1 -1
  14. package/payload/platform/plugins/admin/mcp/dist/tools/account-lifecycle.d.ts +30 -0
  15. package/payload/platform/plugins/admin/mcp/dist/tools/account-lifecycle.d.ts.map +1 -1
  16. package/payload/platform/plugins/admin/mcp/dist/tools/account-lifecycle.js +78 -0
  17. package/payload/platform/plugins/admin/mcp/dist/tools/account-lifecycle.js.map +1 -1
  18. package/payload/platform/plugins/admin/skills/platform-architecture/SKILL.md +2 -2
  19. package/payload/platform/plugins/docs/references/admin-ui.md +1 -1
  20. package/payload/platform/plugins/memory/PLUGIN.md +2 -0
  21. package/payload/platform/plugins/memory/mcp/dist/lib/__tests__/resolve-active-vertical.test.js +12 -0
  22. package/payload/platform/plugins/memory/mcp/dist/lib/__tests__/resolve-active-vertical.test.js.map +1 -1
  23. package/payload/platform/plugins/memory/mcp/dist/lib/resolve-active-vertical.d.ts +9 -1
  24. package/payload/platform/plugins/memory/mcp/dist/lib/resolve-active-vertical.d.ts.map +1 -1
  25. package/payload/platform/plugins/memory/mcp/dist/lib/resolve-active-vertical.js +19 -2
  26. package/payload/platform/plugins/memory/mcp/dist/lib/resolve-active-vertical.js.map +1 -1
  27. package/payload/platform/plugins/memory/references/schema-estate-agent.md +8 -3
  28. package/payload/platform/scripts/__tests__/account-schema-owned-dirs.test.sh +58 -0
  29. package/payload/platform/scripts/__tests__/provision-role-stamp.test.sh +6 -0
  30. package/payload/platform/scripts/lib/account-schema-owned-dirs.py +143 -10
  31. package/payload/platform/scripts/lib/provision-account-dir.sh +5 -0
  32. package/payload/platform/scripts/name-glsmith-owner.sh +239 -0
  33. package/payload/platform/services/claude-session-manager/dist/canonical-tool-names.generated.d.ts.map +1 -1
  34. package/payload/platform/services/claude-session-manager/dist/canonical-tool-names.generated.js +2 -0
  35. package/payload/platform/services/claude-session-manager/dist/canonical-tool-names.generated.js.map +1 -1
  36. package/payload/server/server.js +341 -169
  37. package/payload/platform/plugins/memory/references/schema-trades.md +0 -34
@@ -10,11 +10,34 @@ The owned-dir set for a brand is the union declared by every platform plugin
10
10
  ($PROJECT_DIR/plugins/*/PLUGIN.md) and every premium sub-plugin of a bundle the
11
11
  brand ships ($PREMIUM_ROOT/<bundle>/plugins/*/PLUGIN.md, gated by brand.json
12
12
  shipsPremiumBundles). Reads PROJECT_DIR from the environment.
13
+
14
+ The account SCHEMA.md also gains one domain-entity bucket per top-level
15
+ operator-entry node type of the brand's business ontology. That set is derived
16
+ from the brand vertical's schema reference (brand.json#vertical ->
17
+ plugins/memory/references/<vertical>.md, the '## Top-level node types' table),
18
+ so the file schema mirrors the graph ontology rather than a hardcoded list.
13
19
  """
14
20
  import json, os, re, sys
15
21
 
16
22
  MARK_START = "<!-- plugin-owned-dirs:start -->"
17
23
  MARK_END = "<!-- plugin-owned-dirs:end -->"
24
+ ONT_START = "<!-- ontology-buckets:start -->"
25
+ ONT_END = "<!-- ontology-buckets:end -->"
26
+
27
+ LABEL_DIR_OVERRIDES = {"Site": "customer-sites"}
28
+
29
+
30
+ def _label_to_dir(label):
31
+ """Map a Neo4j label to its file-schema bucket dir: kebab-case-plural,
32
+ lowercased. camelCase word boundaries split on case. One override:
33
+ `Site` -> `customer-sites`, so the tool-owned `sites/` published tree is
34
+ untouched."""
35
+ if label in LABEL_DIR_OVERRIDES:
36
+ return LABEL_DIR_OVERRIDES[label]
37
+ kebab = re.sub(r"(?<=[a-z0-9])(?=[A-Z])", "-", label).lower()
38
+ if kebab.endswith("y") and len(kebab) > 1 and kebab[-2] not in "aeiou":
39
+ return kebab[:-1] + "ies"
40
+ return kebab + "s"
18
41
 
19
42
 
20
43
  def _project_dir():
@@ -37,6 +60,21 @@ def _read_brand(project_dir):
37
60
  return "unknown", []
38
61
 
39
62
 
63
+ def _read_vertical(project_dir):
64
+ """Return the brand's default vertical (the schema file basename, e.g.
65
+ 'schema-construction'), or None. Read from config/brand.json#vertical."""
66
+ path = os.path.join(project_dir, "config", "brand.json")
67
+ if not os.path.isfile(path):
68
+ return None
69
+ try:
70
+ with open(path) as f:
71
+ v = json.load(f).get("vertical")
72
+ v = (v or "").strip()
73
+ return v or None
74
+ except Exception:
75
+ return None
76
+
77
+
40
78
  def _parse_plugin_md(path):
41
79
  """Return (name, [ {dir, description} ]) from a PLUGIN.md, or (None, [])."""
42
80
  try:
@@ -108,6 +146,73 @@ def resolve(project_dir):
108
146
  return [seen[d] for d in order]
109
147
 
110
148
 
149
+ def _top_level_labels(schema_path):
150
+ """Extract operator-entry labels from the entity table under the
151
+ '## Top-level node types' heading of a vertical schema file. A row is
152
+ included iff its 'Schema.org Type' cell is a schema:-namespaced type
153
+ (excludes transport labels such as WhatsAppGroup -> cdm:Channel). Returns
154
+ labels in table order. No such heading -> empty list."""
155
+ try:
156
+ with open(schema_path) as f:
157
+ lines = f.read().split("\n")
158
+ except Exception:
159
+ return []
160
+ start = None
161
+ for i, ln in enumerate(lines):
162
+ if ln.startswith("## ") and "top-level node types" in ln.lower():
163
+ start = i
164
+ break
165
+ if start is None:
166
+ return []
167
+ hdr = None
168
+ for j in range(start + 1, len(lines)):
169
+ if lines[j].startswith("## "):
170
+ return []
171
+ if "|" in lines[j] and "Neo4j Label" in lines[j] and "Schema.org Type" in lines[j]:
172
+ hdr = j
173
+ break
174
+ if hdr is None:
175
+ return []
176
+ cols = [c.strip() for c in lines[hdr].strip().strip("|").split("|")]
177
+ label_i = cols.index("Neo4j Label")
178
+ type_i = cols.index("Schema.org Type")
179
+ out = []
180
+ for k in range(hdr + 2, len(lines)): # +2 skips the header separator row
181
+ row = lines[k]
182
+ if row.startswith("## "):
183
+ break
184
+ if "|" not in row or not row.strip():
185
+ break
186
+ cells = [c.strip() for c in row.strip().strip("|").split("|")]
187
+ if len(cells) <= max(label_i, type_i):
188
+ continue
189
+ label = cells[label_i].strip("`").strip()
190
+ stype = cells[type_i].strip("`").strip()
191
+ if label and stype.startswith("schema:"):
192
+ out.append(label)
193
+ return out
194
+
195
+
196
+ def resolve_domain_buckets(project_dir):
197
+ """Domain operator-entity buckets derived from the brand's vertical
198
+ ontology. Returns [{dir, label}] in table order, deduped by dir. Empty when
199
+ the brand declares no vertical or the vertical file has no top-level
200
+ section."""
201
+ vertical = _read_vertical(project_dir)
202
+ if not vertical:
203
+ return []
204
+ schema_path = os.path.join(project_dir, "plugins", "memory", "references", vertical + ".md")
205
+ seen = set()
206
+ out = []
207
+ for label in _top_level_labels(schema_path):
208
+ d = _label_to_dir(label)
209
+ if d in seen:
210
+ continue
211
+ seen.add(d)
212
+ out.append({"dir": d, "label": label})
213
+ return out
214
+
215
+
111
216
  def _allowed_block(text):
112
217
  """Return (list_of_entries, start_idx, end_idx) for the fenced
113
218
  allowed-top-level block. end_idx is the closing-fence line index."""
@@ -125,40 +230,47 @@ def _allowed_block(text):
125
230
  return None, None, None
126
231
 
127
232
 
128
- def _strip_region(text):
129
- if MARK_START not in text:
233
+ def _strip_region(text, start=MARK_START, end=MARK_END):
234
+ if start not in text:
130
235
  return text
131
- pre = text.split(MARK_START, 1)[0]
236
+ pre = text.split(start, 1)[0]
132
237
  post = ""
133
- if MARK_END in text:
134
- post = text.split(MARK_END, 1)[1]
238
+ if end in text:
239
+ post = text.split(end, 1)[1]
135
240
  return (pre.rstrip("\n") + "\n" + post.lstrip("\n")).rstrip("\n") + "\n"
136
241
 
137
242
 
138
243
  def merge(project_dir, account_dir):
139
244
  brand, _ = _read_brand(project_dir)
140
245
  owned = resolve(project_dir)
246
+ domain = resolve_domain_buckets(project_dir)
141
247
  schema_path = os.path.join(account_dir, "SCHEMA.md")
142
248
  if not os.path.isfile(schema_path):
143
- print(f"brand={brand} added=none")
249
+ print(f"brand={brand} added=none domainBuckets=none")
144
250
  return
145
251
  with open(schema_path) as f:
146
252
  text = f.read()
147
253
  entries, start, end = _allowed_block(text)
148
254
  added = []
255
+ added_domain = []
149
256
  if entries is not None:
150
257
  present = set(entries)
151
258
  lines = text.split("\n")
152
259
  insert = []
153
260
  for e in owned:
154
- if e["dir"] not in present:
261
+ if e["dir"] not in present and e["dir"] not in insert:
155
262
  insert.append(e["dir"])
156
263
  added.append(e["dir"])
264
+ for b in domain:
265
+ if b["dir"] not in present and b["dir"] not in insert:
266
+ insert.append(b["dir"])
267
+ added_domain.append(b["dir"])
157
268
  if insert:
158
269
  lines[end:end] = insert
159
270
  text = "\n".join(lines)
160
- # Regenerate the descriptive region wholesale (idempotent).
161
- text = _strip_region(text)
271
+ # Regenerate both descriptive regions wholesale (idempotent).
272
+ text = _strip_region(text, MARK_START, MARK_END)
273
+ text = _strip_region(text, ONT_START, ONT_END)
162
274
  if owned:
163
275
  region = [MARK_START, "## Plugin-owned top-level workspaces", "",
164
276
  "A plugin that owns a top-level workspace declares it. The internal",
@@ -169,9 +281,23 @@ def merge(project_dir, account_dir):
169
281
  region.append(f"- `{e['dir']}/` — {desc}")
170
282
  region.append(MARK_END)
171
283
  text = text.rstrip("\n") + "\n\n" + "\n".join(region) + "\n"
284
+ owned_dirs = {e["dir"] for e in owned}
285
+ # A dir already claimed by a plugin (described in the plugin-owned region)
286
+ # is not also described here, so a name collision cannot double-describe.
287
+ domain_region = [b for b in domain if b["dir"] not in owned_dirs]
288
+ if domain_region:
289
+ region = [ONT_START, "## Domain entity buckets (from the graph ontology)", "",
290
+ "One bucket per top-level operator-entry node type of this brand's",
291
+ "business ontology. File a job's artifacts under its entity folder;",
292
+ "the folder may hold a subtree.", ""]
293
+ for b in domain_region:
294
+ region.append(f"- `{b['dir']}/` - one folder per {b['label']} record.")
295
+ region.append(ONT_END)
296
+ text = text.rstrip("\n") + "\n\n" + "\n".join(region) + "\n"
172
297
  with open(schema_path, "w") as f:
173
298
  f.write(text)
174
- print(f"brand={brand} added={','.join(added) if added else 'none'}")
299
+ print(f"brand={brand} added={','.join(added) if added else 'none'} "
300
+ f"domainBuckets={','.join(added_domain) if added_domain else 'none'}")
175
301
 
176
302
 
177
303
  def reconcile(project_dir, account_dir):
@@ -186,6 +312,13 @@ def reconcile(project_dir, account_dir):
186
312
  for e in owned:
187
313
  is_present = "true" if e["dir"] in present else "false"
188
314
  print(f"brand={brand} plugin={e['plugin']} ownedDir={e['dir']} present={is_present}")
315
+ vertical = _read_vertical(project_dir)
316
+ domain = resolve_domain_buckets(project_dir)
317
+ if vertical and not domain:
318
+ print(f"brand={brand} ontologyVertical={vertical} topLevelSection=absent")
319
+ for b in domain:
320
+ is_present = "true" if b["dir"] in present else "false"
321
+ print(f"brand={brand} ontologyEntity={b['label']} bucket={b['dir']} present={is_present}")
189
322
 
190
323
 
191
324
  def main():
@@ -254,6 +254,11 @@ if role and cfg.get("role") != role:
254
254
  cfg["role"] = role; changed = True
255
255
  if "managedService" not in cfg:
256
256
  cfg["managedService"] = False; changed = True
257
+ # Task 1623 — the client sub-account admins[] is vestigial (Task 999 retired the
258
+ # seats; Task 1596 retired the inbound read). Access is install-wide, so a client
259
+ # never carries admins[]. The house keeps it (its real access surface).
260
+ if role == "client" and "admins" in cfg:
261
+ del cfg["admins"]; changed = True
257
262
  if changed:
258
263
  with open(path, "w") as f:
259
264
  json.dump(cfg, f, indent=2); f.write("\n")
@@ -0,0 +1,239 @@
1
+ #!/usr/bin/env bash
2
+ # ============================================================
3
+ # name-glsmith-owner.sh
4
+ #
5
+ # Purpose
6
+ # -------
7
+ # Name the G.L.Smith & Sons (accountId 2078cb54) business owner on the
8
+ # SiteDesk Code install (Neo4j bolt://localhost:7689). Task 1623.
9
+ #
10
+ # The owner name lives on the owner AdminUser's owned :Person
11
+ # (givenName/familyName) — the Task 1570 canonical name source. This is
12
+ # the ONLY correct place to write it. The older remediate-glsmith-identity.sh
13
+ # (Task 1569) wrote AdminUser.name, which Task 1570 retired; that field is
14
+ # no longer read for display, which is why the owner still renders nameless.
15
+ # This script supersedes that step with the owned-Person write.
16
+ #
17
+ # What it does (idempotent; logs its post-condition):
18
+ # name-owner set the owner's owned Person givenName='Dale',
19
+ # familyName='Smith', embedding=null (so the standing reindex
20
+ # re-embeds). If the owner owns no Person, create+OWNS one.
21
+ # report-orphan DETECT (read-only, never mutates): the orphaned
22
+ # 'Dale Smith' Person holding telephone 447958391734 with zero
23
+ # edges to the LocalBusiness. Merging it into the owner identity
24
+ # is OUT OF SCOPE (identity-dedup task); Dale is GL Smith's
25
+ # account manager, so this contact is left in place. Reported so
26
+ # the operator sees both Dale nodes and the deferral is explicit.
27
+ #
28
+ # Does NOT touch: managingAdminUserId (blocked on the managing-admin design
29
+ # question), account.json admins[] (removed fleet-wide by the installer, not
30
+ # here), the test account.
31
+ #
32
+ # Write-gate invariant: the account never drops below one AdminUser{accountId}
33
+ # and always retains its role:'owner' node. Asserted before and after the write.
34
+ #
35
+ # Usage
36
+ # -----
37
+ # bash name-glsmith-owner.sh # dry-run (default)
38
+ # bash name-glsmith-owner.sh --dry-run
39
+ # bash name-glsmith-owner.sh --verify # post-apply asserts
40
+ # bash name-glsmith-owner.sh --apply --dump=/path/to/neo4j.dump
41
+ # bash name-glsmith-owner.sh --apply --force-no-dump # no dump (loud)
42
+ #
43
+ # --dry-run Print every intended change; mutate nothing (default).
44
+ # --apply Perform the write. Refuses without a valid --dump (a fresh full
45
+ # neo4j-admin dump) unless --force-no-dump is given.
46
+ # --verify Run the post-apply asserts; exit non-zero if any fail.
47
+ #
48
+ # Rollback: restore the operator's --dump via `neo4j-admin database load`.
49
+ #
50
+ # Reads:
51
+ # ~/sitedesk-code/platform/config/.neo4j-password Neo4j password
52
+ # ============================================================
53
+
54
+ set -euo pipefail
55
+
56
+ readonly ACCT='2078cb54-08e9-49e9-bf8e-e9f3ad76ca41'
57
+ readonly OWNER='3f81e7c2-8e38-42f0-bce2-7355cd7c1ecf' # role:owner AdminUser
58
+ readonly GIVEN='Dale'
59
+ readonly FAMILY='Smith'
60
+ readonly DALE_PHONE='447958391734'
61
+
62
+ readonly PWFILE="$HOME/sitedesk-code/platform/config/.neo4j-password"
63
+ readonly NEO4J_USER="${NEO4J_USER:-neo4j}"
64
+ readonly URI="${NEO4J_URI:-bolt://localhost:7689}"
65
+
66
+ MODE="dry-run"
67
+ DUMP_PATH=""
68
+ FORCE_NO_DUMP=0
69
+
70
+ while [ $# -gt 0 ]; do
71
+ case "$1" in
72
+ --dry-run) MODE="dry-run"; shift ;;
73
+ --apply) MODE="apply"; shift ;;
74
+ --verify) MODE="verify"; shift ;;
75
+ --dump=*) DUMP_PATH="${1#*=}"; shift ;;
76
+ --dump) DUMP_PATH="${2:-}"; shift 2 ;;
77
+ --force-no-dump) FORCE_NO_DUMP=1; shift ;;
78
+ -h|--help) sed -n '2,/^# =\{20,\}/p' "$0" | sed 's/^# \{0,1\}//'; exit 0 ;;
79
+ *) echo "Unknown arg: $1" >&2; exit 2 ;;
80
+ esac
81
+ done
82
+
83
+ log() { echo "[name-glsmith-owner] $*" >&2; }
84
+
85
+ [ -f "$PWFILE" ] || { echo "Error: $PWFILE missing (run on the SiteDesk Code box)" >&2; exit 1; }
86
+ command -v cypher-shell >/dev/null 2>&1 || { echo "Error: cypher-shell not in PATH" >&2; exit 1; }
87
+ PW="$(cat "$PWFILE")"
88
+
89
+ run_cypher() { cypher-shell -u "$NEO4J_USER" -p "$PW" -a "$URI" --format plain; }
90
+ scalar() { { tail -n +2 | tr -d '"' | grep -v '^[[:space:]]*$' | head -1; } || true; }
91
+ as_int() { awk -F',' '{for(i=1;i<=NF;i++){gsub(/[^0-9-]/,"",$i); if($i ~ /^-?[0-9]+$/) v=$i}} END{print v+0}'; }
92
+
93
+ # --- Write-gate: >=1 AdminUser{accountId} and >=1 role:'owner' + LocalBusiness -
94
+ write_gate() {
95
+ local out n owners biz
96
+ out="$(run_cypher <<CYPHER
97
+ MATCH (a:AdminUser {accountId:'$ACCT'})
98
+ WITH count(a) AS n, sum(CASE WHEN a.role='owner' THEN 1 ELSE 0 END) AS owners
99
+ OPTIONAL MATCH (b:LocalBusiness {accountId:'$ACCT'})
100
+ RETURN n, owners, CASE WHEN count(b) > 0 THEN 1 ELSE 0 END AS business;
101
+ CYPHER
102
+ )"
103
+ n="$(echo "$out" | tail -n +2 | awk -F',' '{print $1+0}')"
104
+ owners="$(echo "$out" | tail -n +2 | awk -F',' '{print $2+0}')"
105
+ biz="$(echo "$out" | tail -n +2 | awk -F',' '{print $3+0}')"
106
+ echo "admins=$n owners=$owners business=$biz"
107
+ [ "$n" -ge 1 ] && [ "$owners" -ge 1 ]
108
+ }
109
+
110
+ assert_write_gate() {
111
+ local g; g="$(write_gate)" || { log "ABORT write-gate breached: $g"; exit 3; }
112
+ case "$g" in *business=1*) : ;; *) log "ABORT write-gate: no LocalBusiness ($g)"; exit 3 ;; esac
113
+ log "write-gate ok $g"
114
+ }
115
+
116
+ preconditions() {
117
+ run_cypher <<< "RETURN 1;" >/dev/null 2>&1 || { echo "Error: cannot connect to $URI" >&2; exit 1; }
118
+ local ownerCount
119
+ ownerCount="$(run_cypher <<CYPHER | as_int
120
+ MATCH (a:AdminUser {userId:'$OWNER', accountId:'$ACCT', role:'owner'}) RETURN count(a);
121
+ CYPHER
122
+ )"
123
+ [ "$ownerCount" = "1" ] || { log "ABORT precondition: expected exactly one role:owner AdminUser $OWNER on $ACCT, found $ownerCount"; exit 3; }
124
+ assert_write_gate
125
+ log "preconditions ok owner=$OWNER"
126
+ }
127
+
128
+ # --- report-orphan: read-only detection, never mutates -----------------------
129
+ report_orphan() {
130
+ local orphan
131
+ orphan="$(run_cypher <<CYPHER | as_int
132
+ MATCH (p:Person {accountId:'$ACCT'})
133
+ WHERE p.givenName='$GIVEN' AND p.familyName='$FAMILY' AND p.telephone='$DALE_PHONE'
134
+ AND NOT (p)<-[:OWNS]-(:AdminUser {accountId:'$ACCT', role:'owner'})
135
+ RETURN count(p);
136
+ CYPHER
137
+ )"
138
+ if [ "$orphan" -ge 1 ]; then
139
+ log "report-orphan found=$orphan note=orphaned '$GIVEN $FAMILY' Person holds WA $DALE_PHONE, not owned by the owner. Left as the account-manager contact; merge is out of scope (identity-dedup task)."
140
+ else
141
+ log "report-orphan found=0 (no separate account-manager Dale contact detected)"
142
+ fi
143
+ }
144
+
145
+ # --- name-owner: SET the owner's owned Person, or create it if absent ---------
146
+ name_owner() {
147
+ local ownedNamed ownsAny
148
+ ownedNamed="$(run_cypher <<CYPHER | as_int
149
+ MATCH (:AdminUser {userId:'$OWNER', accountId:'$ACCT'})-[:OWNS]->(p:Person)
150
+ WHERE p.givenName='$GIVEN' AND p.familyName='$FAMILY' RETURN count(p);
151
+ CYPHER
152
+ )"
153
+ if [ "$ownedNamed" -ge 1 ]; then log "step=name-owner no-op (owner already owns a '$GIVEN $FAMILY' Person)"; return; fi
154
+ ownsAny="$(run_cypher <<CYPHER | as_int
155
+ MATCH (:AdminUser {userId:'$OWNER', accountId:'$ACCT'})-[:OWNS]->(p:Person) RETURN count(p);
156
+ CYPHER
157
+ )"
158
+ if [ "$MODE" = "dry-run" ]; then
159
+ if [ "$ownsAny" -ge 1 ]; then
160
+ log "step=name-owner WOULD SET the owner's owned Person givenName='$GIVEN', familyName='$FAMILY', embedding=null (owner currently owns $ownsAny Person(s))"
161
+ else
162
+ log "step=name-owner WOULD CREATE a Person givenName='$GIVEN', familyName='$FAMILY' and OWNS it (owner currently owns 0)"
163
+ fi
164
+ return
165
+ fi
166
+ # apply: rename the existing owned Person, else create+OWNS one. Same shape as
167
+ # setOwnerName (account-lifecycle.ts). embedding=null so the reindex re-embeds.
168
+ run_cypher <<CYPHER >/dev/null
169
+ MATCH (au:AdminUser {userId:'$OWNER', accountId:'$ACCT'})
170
+ OPTIONAL MATCH (au)-[:OWNS]->(existing:Person)
171
+ WITH au, existing LIMIT 1
172
+ CALL {
173
+ WITH au, existing
174
+ WITH au, existing WHERE existing IS NOT NULL
175
+ SET existing.givenName='$GIVEN', existing.familyName='$FAMILY',
176
+ existing.embedding=null, existing.updatedAt=timestamp()
177
+ RETURN 1 AS done
178
+ UNION
179
+ WITH au, existing
180
+ WITH au WHERE existing IS NULL
181
+ CREATE (p:Person {accountId:'$ACCT', givenName:'$GIVEN', familyName:'$FAMILY',
182
+ role:'admin-personal', scope:'admin', embedding:null, createdAt:timestamp()})
183
+ MERGE (au)-[:OWNS]->(p)
184
+ RETURN 1 AS done
185
+ }
186
+ RETURN done;
187
+ CYPHER
188
+ assert_write_gate
189
+ log "step=name-owner owner owned Person named '$GIVEN $FAMILY'"
190
+ }
191
+
192
+ rollback_guard() {
193
+ if [ -n "$DUMP_PATH" ]; then
194
+ [ -s "$DUMP_PATH" ] || { echo "Error: --dump path '$DUMP_PATH' is missing or empty" >&2; exit 1; }
195
+ log "rollback dump provided path=$DUMP_PATH bytes=$(wc -c < "$DUMP_PATH")"
196
+ elif [ "$FORCE_NO_DUMP" -eq 1 ]; then
197
+ log "WARNING proceeding WITHOUT a neo4j dump (--force-no-dump)"
198
+ else
199
+ echo "Error: --apply needs --dump=/path/to/neo4j.dump (fresh full dump) or --force-no-dump" >&2
200
+ echo " Take a dump on the box, e.g.:" >&2
201
+ echo " systemctl --user stop neo4j-sitedesk-code.service && \\" >&2
202
+ echo " neo4j-admin database dump neo4j --to-path=\$HOME/glsmith-1623-dump && \\" >&2
203
+ echo " systemctl --user start neo4j-sitedesk-code.service" >&2
204
+ exit 1
205
+ fi
206
+ }
207
+
208
+ verify() {
209
+ local fail=0
210
+ check() { if [ "$2" = "$3" ]; then log "verify OK $1 ($2)"; else log "verify FAIL $1 (got '$2' want '$3')"; fail=1; fi; }
211
+ local ownerName ownerOwns gate
212
+ ownerName="$(run_cypher <<CYPHER | scalar
213
+ MATCH (:AdminUser {userId:'$OWNER', accountId:'$ACCT'})-[:OWNS]->(p:Person)
214
+ RETURN trim(coalesce(p.givenName,'') + ' ' + coalesce(p.familyName,''));
215
+ CYPHER
216
+ )"
217
+ ownerOwns="$(run_cypher <<CYPHER | as_int
218
+ MATCH (:AdminUser {userId:'$OWNER', accountId:'$ACCT'})-[:OWNS]->(p:Person) RETURN count(p);
219
+ CYPHER
220
+ )"
221
+ gate="$(write_gate || true)"
222
+ check "owner owned Person named '$GIVEN $FAMILY'" "$ownerName" "$GIVEN $FAMILY"
223
+ check "owner owns at least one Person" "$([ "$ownerOwns" -ge 1 ] && echo yes || echo no)" "yes"
224
+ case "$gate" in *"owners=1 business=1"*) log "verify OK write-gate ($gate)";; *) log "verify FAIL write-gate ($gate)"; fail=1;; esac
225
+ [ "$fail" -eq 0 ] && { log "verify PASS"; return 0; } || { log "verify FAILED"; return 1; }
226
+ }
227
+
228
+ log "start mode=$MODE account=$ACCT uri=$URI"
229
+ if [ "$MODE" = "verify" ]; then verify; exit $?; fi
230
+ preconditions
231
+ report_orphan
232
+ if [ "$MODE" = "apply" ]; then rollback_guard; fi
233
+ name_owner
234
+ if [ "$MODE" = "apply" ]; then
235
+ log "apply complete — running verify"
236
+ verify || { log "POST-APPLY VERIFY FAILED — inspect before trusting"; exit 4; }
237
+ else
238
+ log "dry-run complete — no writes performed. Re-run with --apply --dump=<path>."
239
+ fi
@@ -1 +1 @@
1
- {"version":3,"file":"canonical-tool-names.generated.d.ts","sourceRoot":"","sources":["../src/canonical-tool-names.generated.ts"],"names":[],"mappings":"AAQA,+EAA+E;AAC/E,eAAO,MAAM,gBAAgB,EAAE,SAAS,MAAM,EAqB7C,CAAA;AAED,gEAAgE;AAChE,eAAO,MAAM,yBAAyB,EAAE,SAAS,MAAM,EA6QtD,CAAA"}
1
+ {"version":3,"file":"canonical-tool-names.generated.d.ts","sourceRoot":"","sources":["../src/canonical-tool-names.generated.ts"],"names":[],"mappings":"AAQA,+EAA+E;AAC/E,eAAO,MAAM,gBAAgB,EAAE,SAAS,MAAM,EAqB7C,CAAA;AAED,gEAAgE;AAChE,eAAO,MAAM,yBAAyB,EAAE,SAAS,MAAM,EA+QtD,CAAA"}
@@ -39,6 +39,8 @@ export const CANONICAL_MAXY_TOOL_NAMES = [
39
39
  "mcp__plugin_admin_admin__account_list",
40
40
  "mcp__plugin_admin_admin__account_purge",
41
41
  "mcp__plugin_admin_admin__account_reassign_admin",
42
+ "mcp__plugin_admin_admin__account_roster",
43
+ "mcp__plugin_admin_admin__account_set_owner_name",
42
44
  "mcp__plugin_admin_admin__action-approve",
43
45
  "mcp__plugin_admin_admin__action-edit",
44
46
  "mcp__plugin_admin_admin__action-pending",
@@ -1 +1 @@
1
- {"version":3,"file":"canonical-tool-names.generated.js","sourceRoot":"","sources":["../src/canonical-tool-names.generated.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"canonical-tool-names.generated.js","sourceRoot":"","sources":["../src/canonical-tool-names.generated.ts"],"names":[],"mappings":"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"}