@rubytech/create-maxy-code 0.1.441 → 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 (95) 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 +107 -1
  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 +37 -2
  19. package/payload/platform/plugins/docs/references/admin-ui.md +1 -1
  20. package/payload/platform/plugins/docs/references/troubleshooting.md +35 -0
  21. package/payload/platform/plugins/memory/PLUGIN.md +2 -0
  22. package/payload/platform/plugins/memory/mcp/dist/lib/__tests__/resolve-active-vertical.test.js +12 -0
  23. package/payload/platform/plugins/memory/mcp/dist/lib/__tests__/resolve-active-vertical.test.js.map +1 -1
  24. package/payload/platform/plugins/memory/mcp/dist/lib/resolve-active-vertical.d.ts +9 -1
  25. package/payload/platform/plugins/memory/mcp/dist/lib/resolve-active-vertical.d.ts.map +1 -1
  26. package/payload/platform/plugins/memory/mcp/dist/lib/resolve-active-vertical.js +19 -2
  27. package/payload/platform/plugins/memory/mcp/dist/lib/resolve-active-vertical.js.map +1 -1
  28. package/payload/platform/plugins/memory/references/schema-estate-agent.md +8 -3
  29. package/payload/platform/plugins/outlook/mcp/dist/__tests__/device-flow.test.js +37 -4
  30. package/payload/platform/plugins/outlook/mcp/dist/__tests__/device-flow.test.js.map +1 -1
  31. package/payload/platform/plugins/outlook/mcp/dist/auth/device-flow.d.ts +27 -3
  32. package/payload/platform/plugins/outlook/mcp/dist/auth/device-flow.d.ts.map +1 -1
  33. package/payload/platform/plugins/outlook/mcp/dist/auth/device-flow.js +52 -11
  34. package/payload/platform/plugins/outlook/mcp/dist/auth/device-flow.js.map +1 -1
  35. package/payload/platform/plugins/outlook/mcp/dist/index.js +1 -1
  36. package/payload/platform/plugins/outlook/mcp/dist/index.js.map +1 -1
  37. package/payload/platform/plugins/outlook/mcp/dist/tools/account-register.d.ts +5 -0
  38. package/payload/platform/plugins/outlook/mcp/dist/tools/account-register.d.ts.map +1 -1
  39. package/payload/platform/plugins/outlook/mcp/dist/tools/account-register.js +32 -0
  40. package/payload/platform/plugins/outlook/mcp/dist/tools/account-register.js.map +1 -1
  41. package/payload/platform/plugins/whatsapp/mcp/dist/index.js +20 -22
  42. package/payload/platform/plugins/whatsapp/mcp/dist/index.js.map +1 -1
  43. package/payload/platform/plugins/whatsapp/references/channels-whatsapp.md +3 -1
  44. package/payload/platform/scripts/__tests__/account-schema-owned-dirs.test.sh +58 -0
  45. package/payload/platform/scripts/__tests__/provision-role-stamp.test.sh +6 -0
  46. package/payload/platform/scripts/lib/account-schema-owned-dirs.py +143 -10
  47. package/payload/platform/scripts/lib/provision-account-dir.sh +5 -0
  48. package/payload/platform/scripts/logs-read.sh +24 -6
  49. package/payload/platform/scripts/logs-read.test.sh +42 -0
  50. package/payload/platform/scripts/name-glsmith-owner.sh +239 -0
  51. package/payload/platform/scripts/rss-sampler.sh +107 -0
  52. package/payload/platform/services/claude-session-manager/dist/canonical-tool-names.generated.d.ts.map +1 -1
  53. package/payload/platform/services/claude-session-manager/dist/canonical-tool-names.generated.js +2 -0
  54. package/payload/platform/services/claude-session-manager/dist/canonical-tool-names.generated.js.map +1 -1
  55. package/payload/server/public/assets/{AdminLoginScreens-CukwQufW.js → AdminLoginScreens-BJVwtpLH.js} +1 -1
  56. package/payload/server/public/assets/{AdminShell-BwEBH6tT.js → AdminShell-CMu8_Xwn.js} +1 -1
  57. package/payload/server/public/assets/{Checkbox-B629mOPp.js → Checkbox-CzD0ORqA.js} +1 -1
  58. package/payload/server/public/assets/{admin-CdFRCUUZ.js → admin-D0yk2LMq.js} +1 -1
  59. package/payload/server/public/assets/{browser-BWR59IkB.js → browser-CvfbgznO.js} +1 -1
  60. package/payload/server/public/assets/{calendar-kuTcxX1t.js → calendar-fw4WethJ.js} +1 -1
  61. package/payload/server/public/assets/chat-DhD-NES_.js +1 -0
  62. package/payload/server/public/assets/chevron-left-B3xSeEDR.js +1 -0
  63. package/payload/server/public/assets/data-CUnDzDox.js +1 -0
  64. package/payload/server/public/assets/{file-download-4mKJxUi-.js → file-download-Px4s0Bhp.js} +1 -1
  65. package/payload/server/public/assets/{graph-CboWM2_-.js → graph-DbT8nA6R.js} +1 -1
  66. package/payload/server/public/assets/{graph-labels-B8YAdo3Z.js → graph-labels-C1YIyLiA.js} +1 -1
  67. package/payload/server/public/assets/{maximize-2-CxstTgoK.js → maximize-2-BwphzI9S.js} +1 -1
  68. package/payload/server/public/assets/{operator-oSaB1mya.js → operator-zcIfeB14.js} +1 -1
  69. package/payload/server/public/assets/{page-DMeJHVfA.js → page-6fCnIb45.js} +2 -2
  70. package/payload/server/public/assets/page-CfQFAJia.js +1 -0
  71. package/payload/server/public/assets/{public-C54tl6V4.js → public-C0ehZCdE.js} +1 -1
  72. package/payload/server/public/assets/{rotate-ccw-COGQTi7n.js → rotate-ccw-C7BqkMec.js} +1 -1
  73. package/payload/server/public/assets/{tasks-B97VpwEJ.js → tasks-DlgiCafL.js} +1 -1
  74. package/payload/server/public/assets/{time-entry-format-C09rDHo9.js → time-entry-format-CyNYVDkq.js} +1 -1
  75. package/payload/server/public/assets/{useCopyFeedback-DHoYHiK9.js → useCopyFeedback-ByGUMN94.js} +1 -1
  76. package/payload/server/public/assets/useSubAccountSwitcher-1izQvPiD.css +1 -0
  77. package/payload/server/public/assets/useVoiceRecorder-XgpWvXpk.js +2 -0
  78. package/payload/server/public/browser.html +5 -5
  79. package/payload/server/public/calendar.html +6 -6
  80. package/payload/server/public/chat.html +12 -12
  81. package/payload/server/public/data.html +10 -10
  82. package/payload/server/public/graph.html +11 -11
  83. package/payload/server/public/index.html +13 -13
  84. package/payload/server/public/operator.html +14 -14
  85. package/payload/server/public/public.html +12 -12
  86. package/payload/server/public/tasks.html +5 -5
  87. package/payload/server/server.js +742 -321
  88. package/payload/platform/plugins/memory/references/schema-trades.md +0 -34
  89. package/payload/server/public/assets/chat-ejnq2rck.js +0 -1
  90. package/payload/server/public/assets/chevron-left-u4tjq7fI.js +0 -1
  91. package/payload/server/public/assets/data-dZbOyLdT.js +0 -1
  92. package/payload/server/public/assets/page-DEkI-Gl1.js +0 -1
  93. package/payload/server/public/assets/useSubAccountSwitcher-Dzmd7Aip.css +0 -1
  94. package/payload/server/public/assets/useVoiceRecorder-b7HzTFs0.js +0 -2
  95. /package/payload/server/public/assets/{useSubAccountSwitcher-DLJFISqf.js → useSubAccountSwitcher-s6WlRn52.js} +0 -0
@@ -33,6 +33,20 @@ make_account() { # $1=tree
33
33
  echo "$A"
34
34
  }
35
35
 
36
+ # A vertical-aware tree: brand.json carries a `vertical`, and the real memory
37
+ # schema references are symlinked in so the generator exercises the shipped
38
+ # schema-<vertical>.md top-level table.
39
+ REAL_MEMORY_REFS="$REAL_PROJECT_DIR/plugins/memory/references"
40
+ make_vertical_tree() { # $1=ships $2=vertical (schema-basename)
41
+ local T; T="$(mktemp -d)"
42
+ mkdir -p "$T/platform/plugins/memory" "$T/platform/config" "$T/platform/templates/account-schema"
43
+ ln -s "$REAL_PREMIUM" "$T/premium-plugins"
44
+ ln -s "$REAL_MEMORY_REFS" "$T/platform/plugins/memory/references"
45
+ cp "$REAL_TEMPLATE" "$T/platform/templates/account-schema/SCHEMA.md"
46
+ printf '{"installDir":"test-brand","shipsPremiumBundles":%s,"vertical":"%s"}\n' "$1" "$2" > "$T/platform/config/brand.json"
47
+ echo "$T"
48
+ }
49
+
36
50
  # --- sitedesk-shipping brand: quoting added ---
37
51
  T_SD="$(make_tree '["sitedesk"]')"; A_SD="$(make_account "$T_SD")"
38
52
  export PROJECT_DIR="$T_SD/platform"
@@ -96,5 +110,49 @@ merge_owned_dirs_into_schema "$A_TB" >/dev/null
96
110
  ALLOWED_TB="$(awk '/^```allowed-top-level$/{f=1;next} /^```$/{f=0} f' "$A_TB/SCHEMA.md")"
97
111
  assert_grep "ledger" "$ALLOWED_TB" "shapeb-allowlist-has-ledger"
98
112
 
113
+ # --- construction vertical: domain buckets derived, no sites-collision ---
114
+ T_C="$(make_vertical_tree '["sitedesk"]' 'schema-construction')"; A_C="$(make_account "$T_C")"
115
+ export PROJECT_DIR="$T_C/platform"
116
+ OUT_C="$(merge_owned_dirs_into_schema "$A_C")"
117
+ # Match a non-empty derived list, not the bare key (which prints even for zero).
118
+ assert_grep "domainBuckets=jobs" "$OUT_C" "con-domain-log"
119
+ ALLOWED_C="$(awk '/^```allowed-top-level$/{f=1;next} /^```$/{f=0} f' "$A_C/SCHEMA.md")"
120
+ for d in jobs customers customer-sites assets visits quotes purchase-orders ppm-contracts; do
121
+ assert_grep "$d" "$ALLOWED_C" "con-allow-$d"
122
+ done
123
+ assert_nogrep "whatsapp-groups" "$ALLOWED_C" "con-no-transport-bucket"
124
+ assert_grep "customer-sites" "$ALLOWED_C" "con-site-renamed"
125
+ assert_grep "Domain entity buckets" "$(cat "$A_C/SCHEMA.md")" "con-region"
126
+ B_C="$(shasum "$A_C/SCHEMA.md" | awk '{print $1}')"
127
+ merge_owned_dirs_into_schema "$A_C" >/dev/null
128
+ assert_eq "$(shasum "$A_C/SCHEMA.md" | awk '{print $1}')" "$B_C" "con-idempotent"
129
+
130
+ # --- estate-agent vertical: its own buckets, not SiteDesk's ---
131
+ T_E="$(make_vertical_tree '["real-agent"]' 'schema-estate-agent')"; A_E="$(make_account "$T_E")"
132
+ export PROJECT_DIR="$T_E/platform"
133
+ merge_owned_dirs_into_schema "$A_E" >/dev/null
134
+ ALLOWED_E="$(awk '/^```allowed-top-level$/{f=1;next} /^```$/{f=0} f' "$A_E/SCHEMA.md")"
135
+ for d in properties listings viewings offers; do
136
+ assert_grep "$d" "$ALLOWED_E" "ea-allow-$d"
137
+ done
138
+ assert_nogrep "jobs" "$ALLOWED_E" "ea-no-sitedesk-jobs"
139
+
140
+ # --- no vertical declared: generic base only ---
141
+ T_N="$(make_tree '[]')"; A_N="$(make_account "$T_N")"
142
+ export PROJECT_DIR="$T_N/platform"
143
+ merge_owned_dirs_into_schema "$A_N" >/dev/null
144
+ ALLOWED_N="$(awk '/^```allowed-top-level$/{f=1;next} /^```$/{f=0} f' "$A_N/SCHEMA.md")"
145
+ assert_nogrep "jobs" "$ALLOWED_N" "novert-no-domain"
146
+ assert_nogrep "Domain entity buckets" "$(cat "$A_N/SCHEMA.md")" "novert-no-region"
147
+
148
+ # --- reconcile reports each ontology entity present/absent ---
149
+ A_RCD="$(make_account "$T_C")"
150
+ export PROJECT_DIR="$T_C/platform"
151
+ RCD_BEFORE="$(PROJECT_DIR="$PROJECT_DIR" python3 "$PLAT_SCRIPTS/lib/account-schema-owned-dirs.py" reconcile "$A_RCD")"
152
+ assert_grep "ontologyEntity=Job bucket=jobs present=false" "$RCD_BEFORE" "rcd-job-absent"
153
+ merge_owned_dirs_into_schema "$A_RCD" >/dev/null
154
+ RCD_AFTER="$(PROJECT_DIR="$PROJECT_DIR" python3 "$PLAT_SCRIPTS/lib/account-schema-owned-dirs.py" reconcile "$A_RCD")"
155
+ assert_grep "ontologyEntity=Job bucket=jobs present=true" "$RCD_AFTER" "rcd-job-present"
156
+
99
157
  echo "PASS=$PASS FAIL=$FAIL"
100
158
  if [ "$FAIL" -ne 0 ]; then printf '%s\n' "${FAILED[@]}"; exit 1; fi
@@ -9,6 +9,7 @@ TEMPLATES_DIR="$PROJECT_DIR/templates"
9
9
  PASS=0; FAIL=0; FAILED=()
10
10
  role_of() { python3 -c "import json,sys; print(json.load(open(sys.argv[1])).get('role',''))" "$1"; }
11
11
  ms_of() { python3 -c "import json,sys; print(json.load(open(sys.argv[1])).get('managedService'))" "$1"; }
12
+ has_admins() { python3 -c "import json,sys; print('yes' if 'admins' in json.load(open(sys.argv[1])) else 'no')" "$1"; }
12
13
  assert_eq() { if [ "$1" = "$2" ]; then PASS=$((PASS+1)); else FAIL=$((FAIL+1)); FAILED+=("$3: expected [$2] got [$1]"); fi; }
13
14
  assert_nonempty() { if [ -s "$1" ]; then PASS=$((PASS+1)); else FAIL=$((FAIL+1)); FAILED+=("$2: expected non-empty file [$1]"); fi; }
14
15
 
@@ -24,6 +25,8 @@ assert_eq "$(role_of "$A1/account.json")" "house" "fresh-house-role"
24
25
  assert_eq "$(ms_of "$A1/account.json")" "False" "fresh-managedService-default"
25
26
  # Task 1483 — the WhatsApp passive requirement-intake identity is seeded, non-empty.
26
27
  assert_nonempty "$A1/agents/passive/IDENTITY.md" "passive-identity-seeded"
28
+ # Task 1623 — the house account keeps admins[] (the install-wide access surface).
29
+ assert_eq "$(has_admins "$A1/account.json")" "yes" "house-keeps-admins"
27
30
  rm -rf "$T1"
28
31
 
29
32
  # Legacy account.json with NO role -> migrated to house
@@ -37,6 +40,9 @@ rm -rf "$T2"
37
40
  T3=$(mktemp -d); A3="$T3/acct-client"
38
41
  provision_account_dir "$A3" "cccccccc-cccc-cccc-cccc-cccccccccccc" "client" >/dev/null 2>&1
39
42
  assert_eq "$(role_of "$A3/account.json")" "client" "client-role"
43
+ # Task 1623 — a client sub-account carries no admins[] (retired: seats 999,
44
+ # inbound read 1596, field 1623). Access is install-wide.
45
+ assert_eq "$(has_admins "$A3/account.json")" "no" "client-no-admins"
40
46
  rm -rf "$T3"
41
47
 
42
48
  echo "PASS=$PASS FAIL=$FAIL"
@@ -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")
@@ -22,10 +22,10 @@
22
22
  #
23
23
  # Types: agent-stream (canonical, per-session tool-use/tool-result archive;
24
24
  # `system` is kept as a backwards-compatible alias), session, error,
25
- # heartbeat, public, server, mcp, vnc, chat-attempts
25
+ # heartbeat, public, server, mcp, vnc, mem, chat-attempts
26
26
  # For per-session types (agent-stream/system/session/error/public): sessionKey
27
27
  # is required in the first mode. For platform-scoped types (server/vnc/
28
- # heartbeat): the id is ignored. chat-attempts is --tail-only:
28
+ # heartbeat/mem): the id is ignored. chat-attempts is --tail-only:
29
29
  # cross-grep of edge.log + server.log for the chat-ingress quartet within
30
30
  # the most recent N minutes.
31
31
  #
@@ -57,6 +57,10 @@ INSTALL_DIR_NAME=$(basename "$INSTALL_DIR")
57
57
  CONFIG_DIR="$HOME/.$INSTALL_DIR_NAME"
58
58
  SERVER_LOG="$CONFIG_DIR/logs/server.log"
59
59
  VNC_LOG="$CONFIG_DIR/logs/vnc-boot.log"
60
+ # Task 1593 — the standing memory sampler (rss-sampler.sh) writes its persistent
61
+ # log directly under the configDir (persistDir == CONFIG_DIR), NOT under logs/.
62
+ # `--tail mem` surfaces it so the approach-to-OOM diagnostic is one SSH command.
63
+ MEM_LOG="$CONFIG_DIR/rss-sampler.log"
60
64
 
61
65
  # --- Resolve the Claude Code state tree (Task 585) ---
62
66
  # rc-spawn per-session evidence is NOT under data/accounts/<id>/logs/ — it is
@@ -148,7 +152,7 @@ is_per_conversation_type() {
148
152
  }
149
153
 
150
154
  SEARCH_TYPES="agent-stream error session public server mcp vnc"
151
- VALID_TYPES="agent-stream, system, session, error, heartbeat, public, server, mcp, vnc, chat-attempts"
155
+ VALID_TYPES="agent-stream, system, session, error, heartbeat, public, server, mcp, vnc, mem, chat-attempts"
152
156
 
153
157
  # chat-attempts cross-grep prefixes. One row per chat-ingress
154
158
  # checkpoint emitted across edge.log + server.log: edge-side inbound/outcome,
@@ -188,7 +192,7 @@ prefix), and prints the parent transcript merged with every subagent
188
192
  transcript, flagging subagent is_error results inline. An explicit <type>
189
193
  keeps the legacy per-account stream-log read.
190
194
  Per-session stream-log types (agent-stream, system, session, error, public)
191
- require sessionKey; platform-scoped types (server, vnc, heartbeat) ignore the id.
195
+ require sessionKey; platform-scoped types (server, vnc, heartbeat, mem) ignore the id.
192
196
  chat-attempts is --tail-only — it aggregates the four ingress checkpoints
193
197
  ([edge-admin] inbound, [admin-auth] outcome, [chat-route] entered,
194
198
  [edge-admin] outcome) into one timeline.
@@ -199,7 +203,7 @@ EOF
199
203
  validate_type() {
200
204
  local t="$1"
201
205
  case "$t" in
202
- agent-stream|system|session|error|heartbeat|public|server|mcp|vnc|chat-attempts) return 0 ;;
206
+ agent-stream|system|session|error|heartbeat|public|server|mcp|vnc|mem|chat-attempts) return 0 ;;
203
207
  *) echo "Error: invalid type '$t'. Valid types: $VALID_TYPES" >&2; exit 2 ;;
204
208
  esac
205
209
  }
@@ -336,7 +340,7 @@ per_conversation_mode() {
336
340
 
337
341
  # Platform-scoped types shortcut to their fixed files regardless of id.
338
342
  case "$filter_type" in
339
- server|vnc|heartbeat|mcp)
343
+ server|vnc|heartbeat|mcp|mem)
340
344
  echo "# note: type=$filter_type is platform-scoped; sessionKey ignored — showing fixed file" >&2
341
345
  tail_mode "$filter_type" "50"
342
346
  return
@@ -566,6 +570,20 @@ tail_mode() {
566
570
  exit 0
567
571
  fi
568
572
 
573
+ # Task 1593 — the standing memory sampler log. Platform-scoped fixed file
574
+ # (like server/vnc): the sessionKey is ignored. Filter with `grep op=mem-...`
575
+ # per the diagnostic path in troubleshooting.md.
576
+ if [[ "$log_type" == "mem" ]]; then
577
+ if [[ ! -f "$MEM_LOG" ]]; then
578
+ echo "No mem sampler log found at $MEM_LOG"
579
+ exit 1
580
+ fi
581
+ echo "# rss-sampler.log (mem)"
582
+ echo ""
583
+ tail -n "$lines" "$MEM_LOG"
584
+ exit 0
585
+ fi
586
+
569
587
  # Reached only for per-session stream-log types (agent-stream/error/session/
570
588
  # public/mcp); these live under the per-account log dirs.
571
589
  require_account_logs
@@ -148,9 +148,51 @@ case_ambiguous() {
148
148
  return 0
149
149
  }
150
150
 
151
+ # --- Case 4 (Task 1593): --tail mem present → exit 0, returns sampler lines ---
152
+ case_mem_present() {
153
+ local root="/tmp/maxy-logs-read-test-4-$$"
154
+ local script
155
+ script=$(setup_install_tree "$root")
156
+ # The mem log lives directly under the configDir (persistDir == CONFIG_DIR),
157
+ # NOT under logs/ — mirror the installer's join(persistDir, "rss-sampler.log").
158
+ local mem_log="$HOME/.$(basename "$root")/rss-sampler.log"
159
+ {
160
+ echo "[mem-sampler] op=mem-sample ts=2026-07-13T10:00:00Z MemAvailable=900 MemFree=400 SwapUsed=100 ptys=2048 neo4j=900 brand=900 edge=800 vnc=0"
161
+ echo "[mem-sampler] op=mem-pressure MemAvailable=600 threshold=800 topSlice=ptys topRss=2048"
162
+ } > "$mem_log"
163
+
164
+ local stdout rc=0
165
+ stdout=$("$script" --tail mem 10 2>/dev/null) || rc=$?
166
+
167
+ cleanup_install_tree "$root"
168
+
169
+ if [[ $rc -ne 0 ]]; then echo " expected exit 0, got $rc"; return 1; fi
170
+ if [[ "$stdout" != *"op=mem-sample"* ]]; then echo " stdout missing op=mem-sample"; return 1; fi
171
+ if [[ "$stdout" != *"op=mem-pressure"* ]]; then echo " stdout missing op=mem-pressure"; return 1; fi
172
+ return 0
173
+ }
174
+
175
+ # --- Case 5 (Task 1593): --tail mem absent → exit 1 with a clear message ---
176
+ case_mem_absent() {
177
+ local root="/tmp/maxy-logs-read-test-5-$$"
178
+ local script
179
+ script=$(setup_install_tree "$root")
180
+
181
+ local stdout rc=0
182
+ stdout=$("$script" --tail mem 10 2>/dev/null) || rc=$?
183
+
184
+ cleanup_install_tree "$root"
185
+
186
+ if [[ $rc -ne 1 ]]; then echo " expected exit 1, got $rc"; return 1; fi
187
+ if [[ "$stdout" != *"No mem sampler log found"* ]]; then echo " missing not-found message: $stdout"; return 1; fi
188
+ return 0
189
+ }
190
+
151
191
  run_case "sessionKey file present → exit 0" case_file_present
152
192
  run_case "no file → exit 1 + missing-on-resolve" case_missing
153
193
  run_case "ambiguous prefix → exit 1, refuses to pick" case_ambiguous
194
+ run_case "--tail mem present → exit 0 + sampler lines" case_mem_present
195
+ run_case "--tail mem absent → exit 1 + not-found message" case_mem_absent
154
196
 
155
197
  echo ""
156
198
  echo "================================================"