@rubytech/create-realagent 1.0.832 → 1.0.834

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 (97) hide show
  1. package/dist/index.js +131 -9
  2. package/package.json +1 -1
  3. package/payload/platform/lib/admins-write/dist/index.d.ts +87 -0
  4. package/payload/platform/lib/admins-write/dist/index.d.ts.map +1 -0
  5. package/payload/platform/lib/admins-write/dist/index.js +248 -0
  6. package/payload/platform/lib/admins-write/dist/index.js.map +1 -0
  7. package/payload/platform/lib/admins-write/src/index.ts +311 -0
  8. package/payload/platform/lib/admins-write/tsconfig.json +8 -0
  9. package/payload/platform/neo4j/migrations/009-conversation-archive-title.ts +197 -0
  10. package/payload/platform/neo4j/schema.cypher +1 -1
  11. package/payload/platform/package.json +2 -2
  12. package/payload/platform/plugins/admin/PLUGIN.md +1 -1
  13. package/payload/platform/plugins/admin/mcp/dist/index.js +37 -44
  14. package/payload/platform/plugins/admin/mcp/dist/index.js.map +1 -1
  15. package/payload/platform/plugins/docs/references/internals.md +4 -3
  16. package/payload/platform/plugins/memory/bin/conversation-archive-ingest.mjs +215 -43
  17. package/payload/platform/plugins/memory/bin/conversation-archive-ingest.sh +7 -2
  18. package/payload/platform/plugins/memory/mcp/dist/lib/__tests__/llm-classifier.test.js +75 -0
  19. package/payload/platform/plugins/memory/mcp/dist/lib/__tests__/llm-classifier.test.js.map +1 -1
  20. package/payload/platform/plugins/memory/mcp/dist/lib/llm-classifier.d.ts +16 -10
  21. package/payload/platform/plugins/memory/mcp/dist/lib/llm-classifier.d.ts.map +1 -1
  22. package/payload/platform/plugins/memory/mcp/dist/lib/llm-classifier.js +155 -100
  23. package/payload/platform/plugins/memory/mcp/dist/lib/llm-classifier.js.map +1 -1
  24. package/payload/platform/plugins/memory/mcp/dist/lib/llm-ranker.d.ts +13 -5
  25. package/payload/platform/plugins/memory/mcp/dist/lib/llm-ranker.d.ts.map +1 -1
  26. package/payload/platform/plugins/memory/mcp/dist/lib/llm-ranker.js +53 -59
  27. package/payload/platform/plugins/memory/mcp/dist/lib/llm-ranker.js.map +1 -1
  28. package/payload/platform/plugins/memory/mcp/dist/tools/__tests__/memory-ingest.test.js +9 -0
  29. package/payload/platform/plugins/memory/mcp/dist/tools/__tests__/memory-ingest.test.js.map +1 -1
  30. package/payload/platform/plugins/memory/mcp/dist/tools/memory-ingest.d.ts +24 -7
  31. package/payload/platform/plugins/memory/mcp/dist/tools/memory-ingest.d.ts.map +1 -1
  32. package/payload/platform/plugins/memory/mcp/dist/tools/memory-ingest.js +47 -11
  33. package/payload/platform/plugins/memory/mcp/dist/tools/memory-ingest.js.map +1 -1
  34. package/payload/platform/plugins/memory/skills/conversation-archive/SKILL.md +45 -8
  35. package/payload/platform/scripts/lib/resolve-account-dir.sh +3 -1
  36. package/payload/platform/scripts/migrate-import.sh +3 -1
  37. package/payload/platform/scripts/seed-neo4j.sh +13 -3
  38. package/payload/server/chunk-CRAIGEXY.js +654 -0
  39. package/payload/server/chunk-GK4WHM3H.js +9961 -0
  40. package/payload/server/chunk-I2NOLBQA.js +2123 -0
  41. package/payload/server/chunk-IVTESKFR.js +9961 -0
  42. package/payload/server/chunk-KD3XP4IK.js +1116 -0
  43. package/payload/server/chunk-KKGGT5RH.js +654 -0
  44. package/payload/server/chunk-MRJGG6CS.js +2124 -0
  45. package/payload/server/chunk-OJZPS4BL.js +367 -0
  46. package/payload/server/chunk-ZVW5XKPU.js +1116 -0
  47. package/payload/server/client-pool-FM3YJWV5.js +32 -0
  48. package/payload/server/client-pool-J5BCVVI2.js +32 -0
  49. package/payload/server/cloudflare-task-tracker-FSPEJOTH.js +19 -0
  50. package/payload/server/cloudflare-task-tracker-XCUO4N74.js +19 -0
  51. package/payload/server/maxy-edge.js +6 -5
  52. package/payload/server/neo4j-migrations-5AN2U3YO.js +664 -0
  53. package/payload/server/neo4j-migrations-XP7XDVPX.js +664 -0
  54. package/payload/server/public/assets/{Checkbox-CTGhpDKq.js → Checkbox-Bq6ORjz2.js} +1 -1
  55. package/payload/server/public/assets/admin-CstEkw-G.js +352 -0
  56. package/payload/server/public/assets/data-DwZZ7qbH.js +1 -0
  57. package/payload/server/public/assets/graph-DceEv42K.js +1 -0
  58. package/payload/server/public/assets/{jsx-runtime-D4WovFYk.css → jsx-runtime-DidQeNoZ.css} +1 -1
  59. package/payload/server/public/assets/page-Bpi_jPw6.js +50 -0
  60. package/payload/server/public/assets/{page-DkBfWy4C.js → page-CFWoVkgV.js} +1 -1
  61. package/payload/server/public/assets/{public-BdVIVpv8.js → public-BWMwq5Jj.js} +1 -1
  62. package/payload/server/public/assets/{useAdminFetch-DmHu0oCx.js → useAdminFetch-B93ig7ef.js} +1 -1
  63. package/payload/server/public/assets/{useVoiceRecorder-CSc_hxjV.js → useVoiceRecorder-Cb0nAtOo.js} +1 -1
  64. package/payload/server/public/data.html +5 -5
  65. package/payload/server/public/graph.html +6 -6
  66. package/payload/server/public/index.html +8 -8
  67. package/payload/server/public/public.html +5 -5
  68. package/payload/server/server.js +376 -167
  69. package/payload/platform/plugins/admin/mcp/dist/lib/review-tools.d.ts +0 -31
  70. package/payload/platform/plugins/admin/mcp/dist/lib/review-tools.d.ts.map +0 -1
  71. package/payload/platform/plugins/admin/mcp/dist/lib/review-tools.js +0 -666
  72. package/payload/platform/plugins/admin/mcp/dist/lib/review-tools.js.map +0 -1
  73. package/payload/platform/plugins/memory/mcp/dist/lib/semantic-chunker.d.ts +0 -61
  74. package/payload/platform/plugins/memory/mcp/dist/lib/semantic-chunker.d.ts.map +0 -1
  75. package/payload/platform/plugins/memory/mcp/dist/lib/semantic-chunker.js +0 -266
  76. package/payload/platform/plugins/memory/mcp/dist/lib/semantic-chunker.js.map +0 -1
  77. package/payload/platform/plugins/memory/mcp/dist/tools/whatsapp-export-insight-pass.d.ts +0 -27
  78. package/payload/platform/plugins/memory/mcp/dist/tools/whatsapp-export-insight-pass.d.ts.map +0 -1
  79. package/payload/platform/plugins/memory/mcp/dist/tools/whatsapp-export-insight-pass.js +0 -477
  80. package/payload/platform/plugins/memory/mcp/dist/tools/whatsapp-export-insight-pass.js.map +0 -1
  81. package/payload/platform/plugins/memory/mcp/dist/tools/whatsapp-export-insight-write.d.ts +0 -27
  82. package/payload/platform/plugins/memory/mcp/dist/tools/whatsapp-export-insight-write.d.ts.map +0 -1
  83. package/payload/platform/plugins/memory/mcp/dist/tools/whatsapp-export-insight-write.js +0 -160
  84. package/payload/platform/plugins/memory/mcp/dist/tools/whatsapp-export-insight-write.js.map +0 -1
  85. package/payload/platform/plugins/memory/mcp/dist/tools/whatsapp-export-parse.d.ts +0 -10
  86. package/payload/platform/plugins/memory/mcp/dist/tools/whatsapp-export-parse.d.ts.map +0 -1
  87. package/payload/platform/plugins/memory/mcp/dist/tools/whatsapp-export-parse.js +0 -29
  88. package/payload/platform/plugins/memory/mcp/dist/tools/whatsapp-export-parse.js.map +0 -1
  89. package/payload/platform/plugins/memory/mcp/dist/tools/whatsapp-export-preview.d.ts +0 -28
  90. package/payload/platform/plugins/memory/mcp/dist/tools/whatsapp-export-preview.d.ts.map +0 -1
  91. package/payload/platform/plugins/memory/mcp/dist/tools/whatsapp-export-preview.js +0 -34
  92. package/payload/platform/plugins/memory/mcp/dist/tools/whatsapp-export-preview.js.map +0 -1
  93. package/payload/server/public/assets/admin-BNwPsMhJ.js +0 -352
  94. package/payload/server/public/assets/data-Y77FLKjs.js +0 -1
  95. package/payload/server/public/assets/graph-N_Bw-8oT.js +0 -1
  96. package/payload/server/public/assets/page-BKLGP-th.js +0 -50
  97. /package/payload/server/public/assets/{jsx-runtime-DkaAusaX.js → jsx-runtime-DH5S-MwB.js} +0 -0
@@ -69,7 +69,10 @@ Capture that path. The progress file lives at `data/accounts/$ACCOUNT_ID/logs/co
69
69
  Optional flags:
70
70
  - `--timezone <iana>` — IANA zone for timestamps (default `Europe/London`).
71
71
  - `--date-format <DD/MM/YY|MM/DD/YY|DD/MM/YYYY|MM/DD/YYYY>` — WhatsApp only; override auto-detect for ambiguous locales.
72
- - `--session-gap-hours <N>` — gap threshold (in hours) used to split parsed messages into sessions for chunking (default `12`).
72
+
73
+ Sessions split deterministically at 8h gap (fixed code constant — Task 902). The pre-902 `--session-gap-hours` flag is removed: chunked chat-mode classify (Task 902 sub-scope C) absorbs oversize sessions internally so the gap value is no longer an operational lever. Passing the legacy flag FAILs at `phase=argv`.
74
+
75
+ `--rebuild` (operator-issued only; never agent-autonomous — see Doctrine below): treats the run as first-ingest, deletes prior `:Section:Conversation` chunks for THIS export's `archiveSha256`, and re-classifies the entire archive. The skipped-cursor-lookup means a `--rebuild` and a fresh-export-with-different-bytes are operationally equivalent — one is destructive on identifiable bytes, the other is additive.
73
76
 
74
77
  ### Heartbeat protocol
75
78
 
@@ -79,9 +82,35 @@ Every 30 s while the background bash is live:
79
82
  tail -n 100 <progress-file>
80
83
  ```
81
84
 
82
- Surface fresh `[conversation-archive] classify-session sessionIndex=K/N ...` lines as chat tokens — one chat line per fresh progress line. The SDK silence timer resets on every output token; a 30 s poll cadence keeps the silence wall (180 s) untripped on a steady classify loop.
85
+ The bin emits one **derived** progress line per session boundary; the agent surfaces those (and only those) as chat tokens.
86
+
87
+ Grep contract — surface ONLY these line shapes to operator chat:
88
+
89
+ | Line shape (bin emits) | Surface as |
90
+ |---|---|
91
+ | `[conversation-archive] progress sessionIndex=K/N pct=P chunks-so-far=X elapsed-ms=M` | `Classifying session K/N (P%) — chunks so far X — elapsed <Ms>` |
92
+ | `[conversation-archive] WARN cleanup-dropped chunks=N archiveSha256=<sha>` | **WARN** `cleanup-dropped chunks=N — prior data deleted (only legitimate under --rebuild; STOP and verify if not)` |
93
+ | `[conversation-archive] FAIL phase=…` | verbatim + yield (do not retry) |
94
+
95
+ Suppress all other `[conversation-archive] …` lines (raw `classify-session`, `session-committed`, `cleanup-skipped`, etc.) — they remain on disk for diagnosis but are not operator-stream noise. The bin's `progress` line is computed deterministically (Task 902 sub-scope F): per-tick percentage arithmetic and running totals come from one place, not from agent prose-rule arithmetic which drifts.
96
+
97
+ If the tail returns no NEW `progress` line for 60 s while the bg pid is still live: surface one `still on session K/N (P%) — last update <Δ>s ago` and stop emitting until the next advance. Forbidden: emitting "polling progress file" or any meta-status the operator did not request. Forbidden: blind-reissue of the bash.
98
+
99
+ ### Bash poll `description` field — pin progress forward (Task 905)
100
+
101
+ Each heartbeat `tail` call's `description` carries the most recent progress signal forward, parsed from the prior poll's last surfaced line. Chat timeline rows collapse to the `description`; the body holding the actual progress is hidden until the operator expands every row, so the progress belongs in the title.
83
102
 
84
- If the tail returns no NEW lines for 60 s while the bg pid is still live: surface the last progress line verbatim to the operator, and stop polling until they direct otherwise. **Do not blind-reissue the bash.**
103
+ Format: `Heartbeat poll sessionIndex=K/N pct=P chunks-so-far=X`
104
+
105
+ The first poll (no prior progress yet) is `Heartbeat poll — starting`. Every subsequent poll updates the suffix from the last surfaced progress line; if the most recent poll surfaced the `still on session …` stall line, the suffix becomes `Heartbeat poll — sessionIndex=K/N pct=P stalled <Δ>s`.
106
+
107
+ The literal label `Heartbeat poll` never shortens — not to `Poll N`, not to `polling`, not to anything else. Forbidden: any heartbeat `description` that drops the progress suffix once a `progress` line has been surfaced; any label other than `Heartbeat poll`.
108
+
109
+ ### Subagent dispatch contract (Task 905)
110
+
111
+ When the heartbeat loop is delegated to a subagent (ad-hoc `Agent` invocation watching a `run_in_background` bash for this skill), the dispatch prompt MUST include the **Heartbeat protocol** subsection above verbatim — the grep contract, the description-format clause, and the no-NEW-progress branch. Subagents do not inherit memory or per-turn feedback; the dispatch prompt is the only carrier of this convention across the subagent boundary.
112
+
113
+ A subagent that emits `Poll 58`, `Poll 69`, or any non-canonical label is a contract violation, not stylistic drift. The main agent's prompt-construction step is the gate: the subagent prompt is wrong if the literal string `Heartbeat poll —` is missing from it.
85
114
 
86
115
  ### Resume after silent stall
87
116
 
@@ -93,11 +122,12 @@ Forbidden: blind-reissue without reading the progress file first.
93
122
  - Picks the normaliser for `--source`. WhatsApp: locates `_chat.txt` (zip / dir / direct file), parses deterministically, computes `archiveSha256`. Other sources interpret the path according to their own format.
94
123
  - Validates every distinct parsed senderName against the closed set of `{owner, participants...}` candidate names. Any miss LOUD-FAILs `parser-miss reason="senderName=<verbatim> not in confirmed participant set ..."`.
95
124
  - Computes `conversationIdentity` from accountId + sorted participant elementIds.
96
- - Looks up any prior `:ConversationArchive` carrying that identity → reads `lastIngestedMessageHash`. If found, slices parsed lines after the cursor (delta-append). Cursor not found → `FAIL delta-cursor-missing`. Cursor at last line → empty-delta noop (exit 0, no writes).
97
- - Sessionizes the delta lines at the operator-supplied gap-hours boundary.
98
- - For each session: renders as turn-attributed text (`[ts] Sender: body\n…`) and calls `memory-classify` with `mode='chat'`. Returns one or more `:Section:Conversation` chunk specs.
125
+ - **Without `--rebuild`** (default): looks up any prior `:ConversationArchive` carrying that identity → reads `lastIngestedMessageHash`. If found, slices parsed lines after the cursor (delta-append). Cursor not found → `FAIL delta-cursor-missing`. Cursor at last line → empty-delta noop (exit 0, no writes; existing chunk count surfaced in JSON summary). **With `--rebuild`** (Task 902 sub-scope B): skips the prior-cursor lookup entirely; every line is treated as first-ingest delta; the first session's `memory-ingest` deletes prior chunks for THIS `archiveSha256` and the new cursor overwrites the stale one in the same MERGE.
126
+ - Sessionizes the delta lines at the fixed 8h gap (Task 902 sub-scope D — code constant, no flag).
127
+ - Computes the stable archive `title` once (Task 902 sub-scope A): `<source> · <owner> <others> · <YYYY-MM-DD>→<YYYY-MM-DD>`.
128
+ - For each session: renders as turn-attributed text (`[ts] Sender: body\n…`) and calls `memory-classify` with `mode='chat'`. Oversize sessions dispatch to the chunked-chat path (Task 902 sub-scope C) — sessionize size is no longer an operator concern. Returns one or more `:Section:Conversation` chunk specs.
99
129
  - **Per-session checkpoint** (Task 900 sub-scope E): immediately after each successful classify, calls `memory-ingest` for THAT session's chunks AND advances `lastIngestedMessageHash` / `lastIngestedMessageAt` on the parent `:ConversationArchive` to the last message of that session — atomic (chunk writes + cursor advance happen inside one Cypher transaction). A kill mid-loop leaves the cursor at session N-1's last message; re-issuing with the same argv and `--session-id` resumes from session N onward without re-classifying prior sessions.
100
- - Server MERGEs the parent on `conversationIdentity`, MERGEs `:PARTICIPANT_IN` edges, CREATEs new chunks, extends the `:NEXT` chain from its tail. The cleanup-by-`archiveSha256` step (drops THIS export's prior chunks) runs only on the FIRST per-session call of a run; subsequent sessions skip cleanup or they would delete chunks just written. Every node and edge stamps `source=<enum>` and `createdByAgent='conversation-archive'`.
130
+ - Server MERGEs the parent on `conversationIdentity` (writing `title` ON CREATE and COALESCE-on-MATCH so a stable title is never overwritten), MERGEs `:PARTICIPANT_IN` edges, CREATEs new chunks, extends the `:NEXT` chain from its tail. The cleanup-by-`archiveSha256` step runs ONLY when `--rebuild` is set AND only on the FIRST per-session call of a run. Every node and edge stamps `source=<enum>` and `createdByAgent='conversation-archive'`.
101
131
 
102
132
  NO insight pass runs. Phase 2 (operator-driven `:Observation` / `:Task` / `:Preference` derivation against chunks) is its own follow-up task — and applies uniformly to every source once chunks exist.
103
133
 
@@ -143,8 +173,15 @@ For an empty-delta re-import (`delta.kind === "empty-delta"`): emit only message
143
173
 
144
174
  ## Idempotency
145
175
 
146
- - **Re-running the same export bytes** is a no-op: the cleanup-by-`archiveSha256` step drops THIS export's prior chunks and re-creates them with identical content. `:NEXT` chain length unchanged.
176
+ - **Re-running the same export bytes is a no-op by default** (Task 902 sub-scope B). The bin reports the existing chunk count and exits 0; cursor and chunks are unchanged. The pre-902 contract that ran cleanup-by-`archiveSha256` on every same-bytes re-run was the destructive primitive that combined with `--session-gap-hours` to silently destroy 138 chunks (Adam Mackay incident, 2026-05-04).
147
177
  - **Re-running with appended messages** (a fresh export from the same chat with new messages at the tail): cursor lookup finds the prior `lastIngestedMessageHash`, slices new messages, sessionizes only those, and appends new chunks at the tail of the existing `:NEXT` chain. Pre-existing chunks are never touched.
178
+ - **Destructive rebuild** requires the explicit `--rebuild` flag — and only the operator may pass it (see Doctrine below). With `--rebuild`, the bin treats the run as first-ingest, the FIRST session's writer cleans prior chunks for the matching `archiveSha256`, the cursor is overwritten by that session's MERGE, and the heartbeat surfaces a WARN line so the destructive action is operator-visible.
179
+ - **Re-ingesting into a previously-trashed archive** revives the parent before MERGE. If the operator drag-trashed a `:ConversationArchive` and then re-runs ingest (delta or `--rebuild`), `memory-ingest`'s `ingestConversationArchive` strips `:Trashed` from the matching `conversationIdentity` so the MERGE binds to a non-trashed parent — otherwise the chunks would land under a `:Trashed` parent the graph UI hides, making the entire archive (and every freshly-written chunk under it) invisible until something else clears the label. Mirrors the KnowledgeDocument trash-revival path (Task 576).
180
+
181
+ ## Doctrine
182
+
183
+ - **`--rebuild` is operator-issued only.** The agent must NEVER propose `--rebuild` as a fix for any FAIL phase, classifier error, oversize-session report, or partial-state recovery. The operator must type `--rebuild` themselves. Agent-autonomous `--rebuild` would re-introduce the silent-partial-wipe surface that Task 902 closed; the flag exists exclusively for operators reasoning about identifiable export bytes they want to re-classify.
184
+ - **Use a fresh export** (different `archiveSha256`) for every legitimate re-classify case the agent surfaces. Same-bytes re-classify is the operator's call, not the agent's.
148
185
 
149
186
  ## Verification (post-write)
150
187
 
@@ -8,7 +8,9 @@
8
8
  #
9
9
  # Contract (inputs via env vars):
10
10
  # ACCOUNTS_DIR — {installDir}/data/accounts
11
- # USERS_FILE — {installDir}/platform/config/users.json
11
+ # USERS_FILE — $HOME/<brand.configDir>/users.json (Task 904 — was
12
+ # {installDir}/platform/config/users.json pre-Task-904, but
13
+ # that path lived in the installer's wipe zone)
12
14
  #
13
15
  # Contract (outputs via env vars):
14
16
  # ACCOUNT_ID — resolved uuid (existing kept, or freshly minted)
@@ -376,7 +376,9 @@ if [ -f "$PINS_FILE" ]; then
376
376
  # users.json from .admin-pin, but only at install time — and install runs
377
377
  # before this script writes .admin-pin. Self-contained migration writes
378
378
  # users.json directly, mirroring seed-neo4j.sh's migration branch.
379
- USERS_FILE="$INSTALL_DIR/platform/config/users.json"
379
+ # Task 904 — write to the persistent location (outside the install wipe
380
+ # zone) so subsequent installs do not overwrite this row with a stale backup.
381
+ USERS_FILE="$PIN_DIR/users.json"
380
382
  if [ ! -f "$USERS_FILE" ]; then
381
383
  USER_ID="$(cat /proc/sys/kernel/random/uuid 2>/dev/null || python3 -c 'import uuid; print(uuid.uuid4())')"
382
384
  PIN_HASH="$(cat "$PIN_DIR/.admin-pin")"
@@ -50,7 +50,15 @@ CYPHER_SHELL="cypher-shell"
50
50
  # no identity match aborts loud; .trash/ is operator-emptied, never auto-cleaned.
51
51
  # shellcheck source=lib/resolve-account-dir.sh
52
52
  . "$SCRIPT_DIR/lib/resolve-account-dir.sh"
53
- USERS_FILE="$PROJECT_DIR/config/users.json" resolve_and_sweep_account_dir
53
+ # Resolve brand-aware persistent users.json path before the resolver runs.
54
+ # Mirrors the resolution lower in this file (around the post-Task-904 USERS_FILE
55
+ # block); duplicated here because resolve_and_sweep_account_dir runs first.
56
+ _RESOLVER_CONFIG_DIR_NAME=".maxy"
57
+ if [ -f "$PROJECT_DIR/config/brand.json" ]; then
58
+ _RESOLVER_BRAND_CFG_DIR=$(python3 -c "import json; print(json.load(open('$PROJECT_DIR/config/brand.json')).get('configDir',''))" 2>/dev/null || true)
59
+ [ -n "$_RESOLVER_BRAND_CFG_DIR" ] && _RESOLVER_CONFIG_DIR_NAME="$_RESOLVER_BRAND_CFG_DIR"
60
+ fi
61
+ USERS_FILE="$HOME/$_RESOLVER_CONFIG_DIR_NAME/users.json" resolve_and_sweep_account_dir
54
62
 
55
63
  mkdir -p "$ACCOUNT_DIR/agents/admin" "$ACCOUNT_DIR/.claude" "$ACCOUNT_DIR/specialists/.claude-plugin" "$ACCOUNT_DIR/specialists/agents"
56
64
 
@@ -376,9 +384,8 @@ echo " Account $ACCOUNT_ID at $ACCOUNT_DIR"
376
384
  # ------------------------------------------------------------------
377
385
 
378
386
  CONFIG_DIR="$PROJECT_DIR/config"
379
- USERS_FILE="$CONFIG_DIR/users.json"
380
387
 
381
- # Resolve the brand-specific config directory for .admin-pin location.
388
+ # Resolve the brand-specific config directory.
382
389
  # Mirrors the logic in platform/ui/app/lib/paths.ts.
383
390
  _CONFIG_DIR_NAME=".maxy"
384
391
  if [ -f "$CONFIG_DIR/brand.json" ]; then
@@ -386,6 +393,9 @@ if [ -f "$CONFIG_DIR/brand.json" ]; then
386
393
  [ -n "$_BRAND_CFG_DIR" ] && _CONFIG_DIR_NAME="$_BRAND_CFG_DIR"
387
394
  fi
388
395
  ADMIN_PIN_FILE="$HOME/$_CONFIG_DIR_NAME/.admin-pin"
396
+ # Task 904 — users.json lives under $HOME/$_CONFIG_DIR_NAME, not the wipe zone.
397
+ USERS_FILE="$HOME/$_CONFIG_DIR_NAME/users.json"
398
+ mkdir -p "$HOME/$_CONFIG_DIR_NAME"
389
399
 
390
400
  # Only create users.json if it doesn't exist AND .admin-pin exists (migration case).
391
401
  # Fresh installs: users.json is created by set-pin POST during onboarding.