@rubytech/create-maxy-code 0.1.322 → 0.1.323

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 (83) hide show
  1. package/dist/index.js +55 -3
  2. package/package.json +1 -1
  3. package/payload/platform/plugins/admin/skills/insight/SKILL.md +1 -1
  4. package/payload/platform/plugins/admin/skills/platform-architecture/SKILL.md +36 -5
  5. package/payload/platform/plugins/docs/references/admin-ui.md +12 -0
  6. package/payload/platform/plugins/docs/references/deployment.md +19 -0
  7. package/payload/platform/plugins/docs/references/plugins-guide.md +4 -4
  8. package/payload/platform/plugins/memory/PLUGIN.md +0 -1
  9. package/payload/platform/plugins/memory/mcp/dist/index.js +0 -45
  10. package/payload/platform/plugins/memory/mcp/dist/index.js.map +1 -1
  11. package/payload/platform/plugins/memory/mcp/dist/lib/typed-edge-schema.js +6 -6
  12. package/payload/platform/plugins/memory/mcp/dist/lib/typed-edge-schema.js.map +1 -1
  13. package/payload/platform/plugins/notion-import/.claude-plugin/plugin.json +1 -1
  14. package/payload/platform/plugins/notion-import/PLUGIN.md +6 -6
  15. package/payload/platform/plugins/notion-import/skills/notion-import/SKILL.md +1 -1
  16. package/payload/platform/plugins/notion-import/skills/notion-import/references/databases.md +1 -1
  17. package/payload/platform/plugins/obsidian-import/.claude-plugin/plugin.json +1 -1
  18. package/payload/platform/plugins/obsidian-import/PLUGIN.md +5 -5
  19. package/payload/platform/plugins/obsidian-import/skills/obsidian-import/SKILL.md +1 -1
  20. package/payload/platform/plugins/substack-import/.claude-plugin/plugin.json +1 -1
  21. package/payload/platform/plugins/substack-import/PLUGIN.md +8 -8
  22. package/payload/platform/plugins/substack-import/skills/substack-import/SKILL.md +2 -2
  23. package/payload/platform/plugins/substack-import/skills/substack-import/references/posts.md +1 -1
  24. package/payload/platform/plugins/x-import/.claude-plugin/plugin.json +1 -1
  25. package/payload/platform/plugins/x-import/PLUGIN.md +5 -5
  26. package/payload/platform/plugins/x-import/skills/x-import/SKILL.md +1 -1
  27. package/payload/platform/scripts/check-no-esm-require.mjs +3 -0
  28. package/payload/platform/scripts/identity-forbidden-token-check.mjs +0 -1
  29. package/payload/platform/services/claude-session-manager/dist/canonical-tool-names.generated.d.ts.map +1 -1
  30. package/payload/platform/services/claude-session-manager/dist/canonical-tool-names.generated.js +0 -1
  31. package/payload/platform/services/claude-session-manager/dist/canonical-tool-names.generated.js.map +1 -1
  32. package/payload/platform/services/claude-session-manager/dist/index.js +3 -3
  33. package/payload/platform/services/claude-session-manager/dist/index.js.map +1 -1
  34. package/payload/platform/services/claude-session-manager/dist/jsonl-tail.d.ts.map +1 -1
  35. package/payload/platform/services/claude-session-manager/dist/jsonl-tail.js +8 -4
  36. package/payload/platform/services/claude-session-manager/dist/jsonl-tail.js.map +1 -1
  37. package/payload/platform/services/claude-session-manager/dist/pty-spawner.d.ts +3 -3
  38. package/payload/platform/services/claude-session-manager/dist/pty-spawner.d.ts.map +1 -1
  39. package/payload/platform/services/claude-session-manager/dist/pty-spawner.js +13 -12
  40. package/payload/platform/services/claude-session-manager/dist/pty-spawner.js.map +1 -1
  41. package/payload/platform/services/claude-session-manager/dist/rc-daemon.d.ts +5 -4
  42. package/payload/platform/services/claude-session-manager/dist/rc-daemon.d.ts.map +1 -1
  43. package/payload/platform/services/claude-session-manager/dist/rc-daemon.js.map +1 -1
  44. package/payload/platform/services/claude-session-manager/dist/reaper.js +6 -6
  45. package/payload/platform/services/claude-session-manager/dist/reaper.js.map +1 -1
  46. package/payload/platform/services/claude-session-manager/dist/system-prompt.d.ts.map +1 -1
  47. package/payload/platform/services/claude-session-manager/dist/system-prompt.js +0 -1
  48. package/payload/platform/services/claude-session-manager/dist/system-prompt.js.map +1 -1
  49. package/payload/platform/templates/specialists/agents/coding-assistant.md +1 -1
  50. package/payload/platform/templates/specialists/agents/database-operator.md +2 -6
  51. package/payload/platform/templates/specialists/agents/librarian.md +20 -7
  52. package/payload/server/power-health.cjs +129 -0
  53. package/payload/server/public/assets/{AdminLoginScreens-D1X4M9wX.js → AdminLoginScreens-BBtA0mi8.js} +1 -1
  54. package/payload/server/public/assets/AdminShell-CZW9x0-A.js +1 -0
  55. package/payload/server/public/assets/{Checkbox-DjANdKoK.js → Checkbox-CezYRnKL.js} +1 -1
  56. package/payload/server/public/assets/{OperatorConversations-C-eFXT7w.css → OperatorConversations-Bhyf22Ct.css} +1 -1
  57. package/payload/server/public/assets/OperatorConversations-D6BawEz3.js +9 -0
  58. package/payload/server/public/assets/{admin--FlkQOMu.js → admin-BVpCUS4j.js} +1 -1
  59. package/payload/server/public/assets/{browser-vowQc0TJ.js → browser-ByLzuh0X.js} +1 -1
  60. package/payload/server/public/assets/chat-DTvzNb92.js +1 -0
  61. package/payload/server/public/assets/data-Da1oTowD.js +1 -0
  62. package/payload/server/public/assets/{graph-B-64Cq-y.js → graph-iYrodF_0.js} +3 -3
  63. package/payload/server/public/assets/{graph-labels-Dtd2ckXH.js → graph-labels-DM9u5VTf.js} +1 -1
  64. package/payload/server/public/assets/operator-BPplyxq-.js +1 -0
  65. package/payload/server/public/assets/page-oogapuCz.js +30 -0
  66. package/payload/server/public/assets/{public-CrFepd06.js → public-wBFC1mQO.js} +1 -1
  67. package/payload/server/public/browser.html +4 -4
  68. package/payload/server/public/chat.html +5 -5
  69. package/payload/server/public/data.html +4 -4
  70. package/payload/server/public/graph.html +6 -6
  71. package/payload/server/public/index.html +6 -6
  72. package/payload/server/public/operator.html +7 -7
  73. package/payload/server/public/public.html +5 -5
  74. package/payload/server/server-init.cjs +40 -0
  75. package/payload/server/server.js +27 -0
  76. package/payload/platform/plugins/memory/mcp/scripts/backfill-typed-edges.ts +0 -72
  77. package/payload/platform/templates/specialists/agents/archive-ingest-operator.md +0 -45
  78. package/payload/server/public/assets/AdminShell-BGZ3d3gY.js +0 -1
  79. package/payload/server/public/assets/OperatorConversations-BkitSIiT.js +0 -9
  80. package/payload/server/public/assets/chat-Cw6rUWiC.js +0 -1
  81. package/payload/server/public/assets/data-9ArhwX1q.js +0 -1
  82. package/payload/server/public/assets/operator-Cqn1Ne60.js +0 -1
  83. package/payload/server/public/assets/page-DQ5D6Dg9.js +0 -30
package/dist/index.js CHANGED
@@ -608,6 +608,55 @@ function writeChromiumBinaryPathFile() {
608
608
  console.log(` Wrote ${target} → ${RESOLVED_CHROMIUM_BIN}`);
609
609
  logFile(` [snap-chromium] wrote ${target} contents=${RESOLVED_CHROMIUM_BIN}`);
610
610
  }
611
+ // Arm the systemd system-manager hardware watchdog so a *kernel hang* (the
612
+ // freeze mode: board powered but stuck, /dev/watchdog never petted) reboots the
613
+ // board without a manual power cycle. Distinct from the brand user-service's
614
+ // software watchdog (Type=notify WATCHDOG_USEC), which only restarts the UI
615
+ // process — this resets the whole SoC. Idempotent host-wide drop-in; gated on
616
+ // /dev/watchdog existing (driver loaded) and Linux. Best-effort: a failure
617
+ // warns, never aborts the install. Cannot recover a latched-off brownout (no
618
+ // power = no watchdog) — that needs the external power-cycler documented in
619
+ // deployment.md.
620
+ function configureHardwareWatchdog() {
621
+ try {
622
+ if (!isLinux()) {
623
+ logFile(" hardware watchdog: skipped (not Linux)");
624
+ return;
625
+ }
626
+ if (!existsSync("/dev/watchdog")) {
627
+ logFile(" hardware watchdog: skipped (/dev/watchdog absent)");
628
+ return;
629
+ }
630
+ const desired = "[Manager]\nRuntimeWatchdogSec=20s\nRebootWatchdogSec=2min\n";
631
+ const dropinDir = "/etc/systemd/system.conf.d";
632
+ const dropinPath = `${dropinDir}/10-maxy-watchdog.conf`;
633
+ let current = "";
634
+ if (existsSync(dropinPath)) {
635
+ try {
636
+ current = readFileSync(dropinPath, "utf-8");
637
+ }
638
+ catch {
639
+ // Unreadable existing drop-in — treat as changed so we rewrite.
640
+ current = "";
641
+ }
642
+ }
643
+ if (current === desired) {
644
+ logFile(` hardware watchdog: already active (${dropinPath})`);
645
+ return;
646
+ }
647
+ const tmpPath = "/tmp/10-maxy-watchdog.conf";
648
+ writeFileSync(tmpPath, desired);
649
+ console.log(" [privileged] install systemd hardware watchdog drop-in (RuntimeWatchdogSec=20s RebootWatchdogSec=2min)");
650
+ shell("mkdir", ["-p", dropinDir], { sudo: true });
651
+ shell("cp", [tmpPath, dropinPath], { sudo: true });
652
+ spawnSync("rm", ["-f", tmpPath]);
653
+ spawnSync("sudo", ["systemctl", "daemon-reload"], { stdio: "inherit" });
654
+ logFile(` hardware watchdog: enabled (wrote ${dropinPath}; arms on next boot or systemd re-exec)`);
655
+ }
656
+ catch (err) {
657
+ console.error(` WARNING: failed to configure hardware watchdog: ${err instanceof Error ? err.message : String(err)}`);
658
+ }
659
+ }
611
660
  function installSystemDeps() {
612
661
  log("1", TOTAL, "System dependencies and network...");
613
662
  const platform = requireSupportedPlatform(process.platform);
@@ -1566,8 +1615,10 @@ function ensureOllamaServing() {
1566
1615
  // `ollama.service` bounds llama-server to a fraction of the cores so peak
1567
1616
  // temperature plateaus below the limit regardless of batch shape or which
1568
1617
  // platform path issues the embeds — the caller-agnostic fix, inherited by every
1569
- // install. Quota is half the cores (rounded down, min 1) so at least half stay
1570
- // idle for the thermal budget: 200% on a 4-core Pi. Applied only when the ollama
1618
+ // install. Quota is one core (100%): an embedding burst then draws roughly a
1619
+ // quarter of a 4-core Pi's compute current instead of half, which both keeps
1620
+ // peak SoC temperature down and shaves the current spike that browns out a
1621
+ // marginal PSU. Ingest is slower in exchange. Applied only when the ollama
1571
1622
  // system service exists (the normal Linux path, where ollama's install.sh
1572
1623
  // registers `ollama.service`); a detached `ollama serve` has no unit to cap.
1573
1624
  // ollama is one shared host service across co-resident brands, so the drop-in is
@@ -1582,7 +1633,7 @@ function capOllamaServiceCpu() {
1582
1633
  return;
1583
1634
  }
1584
1635
  const coreCount = Math.max(1, cpus().length);
1585
- const quotaCores = Math.max(1, Math.floor(coreCount / 2));
1636
+ const quotaCores = 1;
1586
1637
  const quotaPct = quotaCores * 100;
1587
1638
  const desired = `[Service]\nCPUQuota=${quotaPct}%\n`;
1588
1639
  const dropinDir = "/etc/systemd/system/ollama.service.d";
@@ -4188,6 +4239,7 @@ logDiagnostics("pre-flight");
4188
4239
  logFile(` Neo4j instance: ${NEO4J_DEDICATED ? "dedicated" : "shared"} on bolt://localhost:${NEO4J_PORT}`);
4189
4240
  try {
4190
4241
  installSystemDeps();
4242
+ configureHardwareWatchdog();
4191
4243
  installNodejs();
4192
4244
  installClaudeCode();
4193
4245
  installNeo4j();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@rubytech/create-maxy-code",
3
- "version": "0.1.322",
3
+ "version": "0.1.323",
4
4
  "description": "Install Maxy — AI for Productive People",
5
5
  "bin": {
6
6
  "create-maxy-code": "./dist/index.js"
@@ -19,6 +19,6 @@ Pass 2 — Operator-relationship updates. Walk this session for tonal signals, r
19
19
 
20
20
  Pass 3 — Graph completeness sweep. Walk this session for any node, edge, or commitment that was discussed but not written to the graph in-flight. For each missing write, delegate to `database-operator` via the Task tool. Count what you write.
21
21
 
22
- Pass 4 — Typed-edge auto-extraction. Delegate to `database-operator` via the Task tool with a prompt naming the typed-edge pass and the session-start timestamp. The specialist calls `memory-typed-edge-pass` with that timestamp and returns the counters. Capture `nodesProcessed` and the accepted count from its reply.
22
+ Pass 4 — Typed-edge auto-extraction. Delegate to `typed-edge-classifier` via the Task tool with a prompt supplying the `accountId` and the session-start timestamp as `sinceIso`. The specialist reads prose nodes written since that timestamp, writes typed edges from the closed allowlist, and returns one line `typed-edge-classifier: nodesProcessed=… proposed=… accepted=… rejected=… ms=…`. Capture `nodesProcessed` and `accepted` from that line.
23
23
 
24
24
  Then reply with one short paragraph in your house voice summarising what you wrote — naming the five counts (learnings, preferences, graph-writes, typed-edges, nodesProcessed) — and carry on with the operator's work.
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  name: platform-architecture
3
3
  description: Use when grounding any documented-surface claim about what Maxy ships — plugins, skills, specialists, install/deploy flows, internals. This is the install catalogue, not evidence of what is enabled on the current account. For install state on this account, call `capabilities-here`; for documented surface, cite the `Source:` URL inline.
4
- content-hash: sha256:009036cb2953a53e5a5d85f5cdf3399d4a7dd12c40fa17e03ebcb46ebeec87a7
4
+ content-hash: sha256:479e04a1e25ace04e44415db3c536ed28faac7ee30b9171524d38b7a667716d1
5
5
  brand: maxy-code
6
6
  product-name: Maxy
7
7
  ---
@@ -365,10 +365,10 @@ These are enabled during onboarding and can be added or removed at any time. Som
365
365
  | `whatsapp` | WhatsApp messaging, pairing, and conversation browsing | Personal assistant |
366
366
  | `replicate` | Image generation — three models for photorealistic, design, and fast draft images | Content producer, Research assistant |
367
367
  | `linkedin-import` | Import a LinkedIn Basic Data Export — Profile and Connections today, more CSVs as references land | Database operator |
368
- | `notion-import` | Import a Notion workspace export (markdown + CSV) — pages, databases, hierarchy, attachments, schema-bounded relations, `@person` mentions account-filtered | Database operator |
369
- | `obsidian-import` | Import an extracted Obsidian vault — pages map to `:KnowledgeDocument`, wikilinks resolve to intra-vault pages or existing entities, tags become `:DefinedTerm`, embedded images become `:DigitalDocument`. Two-phase tool (dry-run → operator disambiguation → commit). | Database operator |
370
- | `x-import` | Import an X (Twitter) Basic Data Export — tweet stream renders as one chronological transcript and ingests as a single `:KnowledgeDocument` (`source='x'`); each DM `sessionId` ingests as one `:ConversationArchive` (`source='x-dm'`, keyed on `conversationIdentity`) via `conversation-archive-ingest.sh`. Mentions, replies, and quote-tweet authors resolve to `:Person` on lowercased `xHandle`; every handle and DM senderId confirms against existing nodes (no auto-create). Per-thread KD granularity and `:Post` / `:DirectMessage` labels are explicitly rejected. | Database operator |
371
- | `substack-import` | Import a Substack "Export your data" archive — per-essay `:KnowledgeDocument {kind:'substack-post'}` via librarian/document-ingest with synthetic stable `attachmentId = "substack-post-${substackPostId}"` (survives Substack edits); one `:KnowledgeDocument {kind:'substack-subscriber-roster'}` per import run with `:MENTIONS {mentionContext:'substack-subscription', tier, totalOpens, totalClicks, lastOpenedAt, lastClickedAt, engagementWindowDays}` to each subscriber `:Person` MERGEd on `(accountId, email)`. Engagement aggregates parsed from `email_activity.csv` (or `subscriber_activity.csv` / `emails.csv`); overwrite-on-reimport. No new label, no new edge type, no new graph writer. Images attach via canonical `:HAS_ENCLOSURE` (or `:MENTIONS` fallback). Bulk-gate at >200 posts or >2000 subscribers. | Database operator |
368
+ | `notion-import` | Import a Notion workspace export (markdown + CSV) — pages, databases, hierarchy, attachments, schema-bounded relations, `@person` mentions account-filtered | Librarian |
369
+ | `obsidian-import` | Import an extracted Obsidian vault — pages map to `:KnowledgeDocument`, wikilinks resolve to intra-vault pages or existing entities, tags become `:DefinedTerm`, embedded images become `:DigitalDocument`. Two-phase tool (dry-run → operator disambiguation → commit). | Librarian |
370
+ | `x-import` | Import an X (Twitter) Basic Data Export — tweet stream renders as one chronological transcript and ingests as a single `:KnowledgeDocument` (`source='x'`); each DM `sessionId` ingests as one `:ConversationArchive` (`source='x-dm'`, keyed on `conversationIdentity`) via `conversation-archive-ingest.sh`. Mentions, replies, and quote-tweet authors resolve to `:Person` on lowercased `xHandle`; every handle and DM senderId confirms against existing nodes (no auto-create). Per-thread KD granularity and `:Post` / `:DirectMessage` labels are explicitly rejected. | Librarian |
371
+ | `substack-import` | Import a Substack "Export your data" archive — per-essay `:KnowledgeDocument {kind:'substack-post'}` via librarian/document-ingest with synthetic stable `attachmentId = "substack-post-${substackPostId}"` (survives Substack edits); one `:KnowledgeDocument {kind:'substack-subscriber-roster'}` per import run with `:MENTIONS {mentionContext:'substack-subscription', tier, totalOpens, totalClicks, lastOpenedAt, lastClickedAt, engagementWindowDays}` to each subscriber `:Person` MERGEd on `(accountId, email)`. Engagement aggregates parsed from `email_activity.csv` (or `subscriber_activity.csv` / `emails.csv`); overwrite-on-reimport. No new label, no new edge type, no new graph writer. Images attach via canonical `:HAS_ENCLOSURE` (or `:MENTIONS` fallback). Bulk-gate at >200 posts or >2000 subscribers. | Librarian |
372
372
  | `memory/skills/conversation-archive` | Source-agnostic conversation transcript ingest. One skill for WhatsApp `_chat.txt`, Telegram, Signal, LinkedIn DMs, Zoom transcript, meeting minutes, iMessage, Slack, X DMs — `--source <enum>` selects the per-source normaliser. Single Bash entry — `bash platform/plugins/memory/bin/conversation-archive-ingest.sh <archive> --source <enum> --participant-person-ids <csv> --scope <admin\|public>` — runs normalise → operator-confirms owner + every distinct sender (owner derived from env via Cypher, no flag) → sessionize at the fixed 8h gap → emit one JSON line carrying prepared sessions (turn-attributed text + per-session cursor). The dispatched specialist iterates the sessions in-turn, produces a typed-section JSON chunking for each, and calls the `memory-ingest` MCP tool with `conversationIdentity` set (writes `:ConversationArchive`, source=<enum>) once per session — chunks + cursor advance commit atomically inside one Cypher transaction, so a kill mid-archive resumes from the next session on re-issue without re-classifying anything already written. Re-imports are delta-append. Auto-creating participants is forbidden — any sender outside the operator-confirmed closed set LOUD-FAILs with `parser-miss`. Distinct from the live `whatsapp` plugin (Baileys). | Database operator |
373
373
  | `memory/skills/conversation-archive-enrich` | Phase 2 for any named `:ConversationArchive` — source-agnostic per-row insight derivation. Operator-triggered (never auto-fires on Phase 1 completion). Walks the parent's `:Section` chunks in pages via the read-only MCP tool `mcp__plugin_memory_memory__conversation-archive-list-chunks`; the dispatched specialist reads each chunk in-turn and emits claims under the four-kind contract (`mention`, `task`, `preference`, `observed-relationship`); the skill hands those claims to `mcp__plugin_memory_memory__conversation-archive-derive-insights` for per-kind cypher emission, then runs the per-row operator gate (`wire / skip / reject`). Idempotent on `(elementId(chunk), kind, contentHash)` — re-runs collapse identical claims. Confidence floor is a hedging-avoidance instruction the skill embeds in the specialist's per-chunk prompt, not a numeric post-filter; the LLM step runs in-turn from the dispatched specialist rather than as a server-side OAuth round-trip. | Database operator |
374
374
 
@@ -2290,6 +2290,18 @@ reserved for vertical / integration notes (LinkedIn extension,
2290
2290
  PropertyData, Real Agent standalone, MCP server inventory) — not for
2291
2291
  core-platform docs.
2292
2292
 
2293
+ ## UI conventions
2294
+
2295
+ **Icons are Lucide components only — no emoji.** Every glyph rendered in
2296
+ `platform/ui` JSX (the `/chat` web chat, the WhatsApp/channel readers, the
2297
+ admin shell, the `/data` grid) is a `lucide-react` component (e.g.
2298
+ `<Paperclip>`, `<FileText>`), never an emoji/emoticon character in the rendered
2299
+ string. The rule is enforced at build time by
2300
+ [`scripts/check-no-emoji-in-jsx.mjs`](../../../ui/scripts/check-no-emoji-in-jsx.mjs),
2301
+ which fails `prebuild` if any emoji codepoint appears in a `.ts`/`.tsx` file
2302
+ under `app/` (tests excluded). Inline an icon at the surrounding text size and
2303
+ colour (`size={14}` matches body text; Lucide inherits `currentColor`).
2304
+
2293
2305
  ## Admin Hono routes
2294
2306
 
2295
2307
  Every admin sub-app is mounted by
@@ -3458,6 +3470,25 @@ Source: https://docs.getmaxy.com/deployment.md
3458
3470
  - Mac with macOS 14 (Sonoma) or newer — both Apple Silicon and Intel
3459
3471
  - 256GB storage minimum
3460
3472
  - Always-on power and network connection
3473
+ - **On a Pi 5: the official 27W USB-C power supply (5.1V / 5A) and a good cable.** This is not optional — see Power and recovery below.
3474
+
3475
+ ## Power and recovery (Raspberry Pi)
3476
+
3477
+ A Pi 5 running Maxy draws hard during document ingest: the embedding model runs the CPU flat-out in bursts. An underpowered supply or a thin/long USB-C cable cannot hold 5V under that current spike, the board *browns out*, and you get one of two failures:
3478
+
3479
+ - **A reset or freeze** — the board drops out mid-task and either reboots or hangs.
3480
+ - **A latched-off board** — the board goes dark and stays dark until you physically unplug and replug the power. No software can recover this, because there is no power to run any recovery.
3481
+
3482
+ **The cure is hardware.** Use the official Raspberry Pi 5 27W PSU (5.1V / 5A) and a short, good-quality USB-C cable. A marginal phone charger or a cheap cable is the usual cause. This is the only thing that *prevents* a brownout.
3483
+
3484
+ **Recovery seatbelts the installer sets up for you:**
3485
+
3486
+ - **A hardware watchdog.** If the board *hangs* (powered but stuck), it reboots itself automatically within about two minutes — no manual power cycle. This only helps a hung board; it cannot help a board that has gone dark.
3487
+ - **Power-health logging.** The admin server writes a `[power-health]` line to `server.log` at boot and every 15 minutes, so a brownout shows up without waiting for a crash. A healthy line reads `throttled=0x0 ... undervolt_occurred=false`. Any `undervolt`-flag set, or `reboots_unclean_24h` above zero, means the board is browning out — fix the power before trusting it with heavy work.
3488
+
3489
+ **For a board that latches off (the dark-board case):** put the Pi on a smart plug that you can power-cycle remotely, ideally one driven by a heartbeat (cut power and restore it when the box stops responding). This is the robust recovery for the dark-board failure and is an operator hardware choice, not something the installer can do.
3490
+
3491
+ **Keep one address.** After a reboot the Pi can pick up a different DHCP address and look like it vanished. Add a **DHCP reservation** on your router that pins the Pi's MAC to a fixed LAN IP, so the box is always reachable at the same address. The boot `[power-health] op=boot lan_ip=<ip>` line records the address each boot — if it changes across reboots, the reservation is not in place.
3461
3492
 
3462
3493
  ## macOS install
3463
3494
 
@@ -25,6 +25,18 @@ reserved for vertical / integration notes (LinkedIn extension,
25
25
  PropertyData, Real Agent standalone, MCP server inventory) — not for
26
26
  core-platform docs.
27
27
 
28
+ ## UI conventions
29
+
30
+ **Icons are Lucide components only — no emoji.** Every glyph rendered in
31
+ `platform/ui` JSX (the `/chat` web chat, the WhatsApp/channel readers, the
32
+ admin shell, the `/data` grid) is a `lucide-react` component (e.g.
33
+ `<Paperclip>`, `<FileText>`), never an emoji/emoticon character in the rendered
34
+ string. The rule is enforced at build time by
35
+ [`scripts/check-no-emoji-in-jsx.mjs`](../../../ui/scripts/check-no-emoji-in-jsx.mjs),
36
+ which fails `prebuild` if any emoji codepoint appears in a `.ts`/`.tsx` file
37
+ under `app/` (tests excluded). Inline an icon at the surrounding text size and
38
+ colour (`size={14}` matches body text; Lucide inherits `currentColor`).
39
+
28
40
  ## Admin Hono routes
29
41
 
30
42
  Every admin sub-app is mounted by
@@ -6,6 +6,25 @@
6
6
  - Mac with macOS 14 (Sonoma) or newer — both Apple Silicon and Intel
7
7
  - 256GB storage minimum
8
8
  - Always-on power and network connection
9
+ - **On a Pi 5: the official 27W USB-C power supply (5.1V / 5A) and a good cable.** This is not optional — see Power and recovery below.
10
+
11
+ ## Power and recovery (Raspberry Pi)
12
+
13
+ A Pi 5 running {{productName}} draws hard during document ingest: the embedding model runs the CPU flat-out in bursts. An underpowered supply or a thin/long USB-C cable cannot hold 5V under that current spike, the board *browns out*, and you get one of two failures:
14
+
15
+ - **A reset or freeze** — the board drops out mid-task and either reboots or hangs.
16
+ - **A latched-off board** — the board goes dark and stays dark until you physically unplug and replug the power. No software can recover this, because there is no power to run any recovery.
17
+
18
+ **The cure is hardware.** Use the official Raspberry Pi 5 27W PSU (5.1V / 5A) and a short, good-quality USB-C cable. A marginal phone charger or a cheap cable is the usual cause. This is the only thing that *prevents* a brownout.
19
+
20
+ **Recovery seatbelts the installer sets up for you:**
21
+
22
+ - **A hardware watchdog.** If the board *hangs* (powered but stuck), it reboots itself automatically within about two minutes — no manual power cycle. This only helps a hung board; it cannot help a board that has gone dark.
23
+ - **Power-health logging.** The admin server writes a `[power-health]` line to `server.log` at boot and every 15 minutes, so a brownout shows up without waiting for a crash. A healthy line reads `throttled=0x0 ... undervolt_occurred=false`. Any `undervolt`-flag set, or `reboots_unclean_24h` above zero, means the board is browning out — fix the power before trusting it with heavy work.
24
+
25
+ **For a board that latches off (the dark-board case):** put the Pi on a smart plug that you can power-cycle remotely, ideally one driven by a heartbeat (cut power and restore it when the box stops responding). This is the robust recovery for the dark-board failure and is an operator hardware choice, not something the installer can do.
26
+
27
+ **Keep one address.** After a reboot the Pi can pick up a different DHCP address and look like it vanished. Add a **DHCP reservation** on your router that pins the Pi's MAC to a fixed LAN IP, so the box is always reachable at the same address. The boot `[power-health] op=boot lan_ip=<ip>` line records the address each boot — if it changes across reboots, the reservation is not in place.
9
28
 
10
29
  ## macOS install
11
30
 
@@ -43,10 +43,10 @@ These are enabled during onboarding and can be added or removed at any time. Som
43
43
  | `whatsapp` | WhatsApp messaging, pairing, and conversation browsing | Personal assistant |
44
44
  | `replicate` | Image generation — three models for photorealistic, design, and fast draft images | Content producer, Research assistant |
45
45
  | `linkedin-import` | Import a LinkedIn Basic Data Export — Profile and Connections today, more CSVs as references land | Database operator |
46
- | `notion-import` | Import a Notion workspace export (markdown + CSV) — pages, databases, hierarchy, attachments, schema-bounded relations, `@person` mentions account-filtered | Database operator |
47
- | `obsidian-import` | Import an extracted Obsidian vault — pages map to `:KnowledgeDocument`, wikilinks resolve to intra-vault pages or existing entities, tags become `:DefinedTerm`, embedded images become `:DigitalDocument`. Two-phase tool (dry-run → operator disambiguation → commit). | Database operator |
48
- | `x-import` | Import an X (Twitter) Basic Data Export — tweet stream renders as one chronological transcript and ingests as a single `:KnowledgeDocument` (`source='x'`); each DM `sessionId` ingests as one `:ConversationArchive` (`source='x-dm'`, keyed on `conversationIdentity`) via `conversation-archive-ingest.sh`. Mentions, replies, and quote-tweet authors resolve to `:Person` on lowercased `xHandle`; every handle and DM senderId confirms against existing nodes (no auto-create). Per-thread KD granularity and `:Post` / `:DirectMessage` labels are explicitly rejected. | Database operator |
49
- | `substack-import` | Import a Substack "Export your data" archive — per-essay `:KnowledgeDocument {kind:'substack-post'}` via librarian/document-ingest with synthetic stable `attachmentId = "substack-post-${substackPostId}"` (survives Substack edits); one `:KnowledgeDocument {kind:'substack-subscriber-roster'}` per import run with `:MENTIONS {mentionContext:'substack-subscription', tier, totalOpens, totalClicks, lastOpenedAt, lastClickedAt, engagementWindowDays}` to each subscriber `:Person` MERGEd on `(accountId, email)`. Engagement aggregates parsed from `email_activity.csv` (or `subscriber_activity.csv` / `emails.csv`); overwrite-on-reimport. No new label, no new edge type, no new graph writer. Images attach via canonical `:HAS_ENCLOSURE` (or `:MENTIONS` fallback). Bulk-gate at >200 posts or >2000 subscribers. | Database operator |
46
+ | `notion-import` | Import a Notion workspace export (markdown + CSV) — pages, databases, hierarchy, attachments, schema-bounded relations, `@person` mentions account-filtered | Librarian |
47
+ | `obsidian-import` | Import an extracted Obsidian vault — pages map to `:KnowledgeDocument`, wikilinks resolve to intra-vault pages or existing entities, tags become `:DefinedTerm`, embedded images become `:DigitalDocument`. Two-phase tool (dry-run → operator disambiguation → commit). | Librarian |
48
+ | `x-import` | Import an X (Twitter) Basic Data Export — tweet stream renders as one chronological transcript and ingests as a single `:KnowledgeDocument` (`source='x'`); each DM `sessionId` ingests as one `:ConversationArchive` (`source='x-dm'`, keyed on `conversationIdentity`) via `conversation-archive-ingest.sh`. Mentions, replies, and quote-tweet authors resolve to `:Person` on lowercased `xHandle`; every handle and DM senderId confirms against existing nodes (no auto-create). Per-thread KD granularity and `:Post` / `:DirectMessage` labels are explicitly rejected. | Librarian |
49
+ | `substack-import` | Import a Substack "Export your data" archive — per-essay `:KnowledgeDocument {kind:'substack-post'}` via librarian/document-ingest with synthetic stable `attachmentId = "substack-post-${substackPostId}"` (survives Substack edits); one `:KnowledgeDocument {kind:'substack-subscriber-roster'}` per import run with `:MENTIONS {mentionContext:'substack-subscription', tier, totalOpens, totalClicks, lastOpenedAt, lastClickedAt, engagementWindowDays}` to each subscriber `:Person` MERGEd on `(accountId, email)`. Engagement aggregates parsed from `email_activity.csv` (or `subscriber_activity.csv` / `emails.csv`); overwrite-on-reimport. No new label, no new edge type, no new graph writer. Images attach via canonical `:HAS_ENCLOSURE` (or `:MENTIONS` fallback). Bulk-gate at >200 posts or >2000 subscribers. | Librarian |
50
50
  | `memory/skills/conversation-archive` | Source-agnostic conversation transcript ingest. One skill for WhatsApp `_chat.txt`, Telegram, Signal, LinkedIn DMs, Zoom transcript, meeting minutes, iMessage, Slack, X DMs — `--source <enum>` selects the per-source normaliser. Single Bash entry — `bash platform/plugins/memory/bin/conversation-archive-ingest.sh <archive> --source <enum> --participant-person-ids <csv> --scope <admin\|public>` — runs normalise → operator-confirms owner + every distinct sender (owner derived from env via Cypher, no flag) → sessionize at the fixed 8h gap → emit one JSON line carrying prepared sessions (turn-attributed text + per-session cursor). The dispatched specialist iterates the sessions in-turn, produces a typed-section JSON chunking for each, and calls the `memory-ingest` MCP tool with `conversationIdentity` set (writes `:ConversationArchive`, source=<enum>) once per session — chunks + cursor advance commit atomically inside one Cypher transaction, so a kill mid-archive resumes from the next session on re-issue without re-classifying anything already written. Re-imports are delta-append. Auto-creating participants is forbidden — any sender outside the operator-confirmed closed set LOUD-FAILs with `parser-miss`. Distinct from the live `whatsapp` plugin (Baileys). | Database operator |
51
51
  | `memory/skills/conversation-archive-enrich` | Phase 2 for any named `:ConversationArchive` — source-agnostic per-row insight derivation. Operator-triggered (never auto-fires on Phase 1 completion). Walks the parent's `:Section` chunks in pages via the read-only MCP tool `mcp__plugin_memory_memory__conversation-archive-list-chunks`; the dispatched specialist reads each chunk in-turn and emits claims under the four-kind contract (`mention`, `task`, `preference`, `observed-relationship`); the skill hands those claims to `mcp__plugin_memory_memory__conversation-archive-derive-insights` for per-kind cypher emission, then runs the per-row operator gate (`wire / skip / reject`). Idempotent on `(elementId(chunk), kind, contentHash)` — re-runs collapse identical claims. Confidence floor is a hedging-avoidance instruction the skill embeds in the specialist's per-chunk prompt, not a numeric post-filter; the LLM step runs in-turn from the dispatched specialist rather than as a server-side OAuth round-trip. | Database operator |
52
52
 
@@ -129,7 +129,6 @@ hidden:
129
129
  - session-compact
130
130
  - session-compact-status
131
131
  - image-fetch
132
- - memory-typed-edge-pass
133
132
  skills:
134
133
  - skills/conversational-memory/SKILL.md
135
134
  - skills/document-ingest/SKILL.md
@@ -49,7 +49,6 @@ import { conversationArchiveDeriveInsights, PROPOSAL_KINDS, } from "./tools/conv
49
49
  import { conversationArchiveListChunks } from "./tools/conversation-archive-list-chunks.js";
50
50
  import { conversationArchiveEnrichRejection } from "./tools/conversation-archive-enrich-rejection.js";
51
51
  import { fetchImageAsContent } from "./tools/image-fetch.js";
52
- import { memoryTypedEdgePass } from "./tools/memory-typed-edge-pass.js";
53
52
  import { memoryReportWrite } from "./tools/memory-report-write.js";
54
53
  import { memoryReportReadLatest } from "./tools/memory-report-read-latest.js";
55
54
  import { memoryReportList } from "./tools/memory-report-list.js";
@@ -2423,50 +2422,6 @@ lifelineTool(server, "image-fetch", [
2423
2422
  };
2424
2423
  });
2425
2424
  // ===================================================================
2426
- // Typed-edge auto-extraction pass (Task 305)
2427
- // ===================================================================
2428
- //
2429
- // Schema-bounded pass that reads prose-bearing nodes the operator's account
2430
- // wrote since `sinceIso` and asks Haiku for typed-edge proposals against
2431
- // `TYPED_EDGE_ALLOWLIST`. Owned by `database-operator`
2432
- // (admin Task-spawns it during an on-demand `/insight` pass); NOT in
2433
- // `ADMIN_CORE_TOOLS`. Backfill is via the CLI under `scripts/backfill-typed-edges.ts`,
2434
- // bypassing this tool.
2435
- lifelineTool(server, "memory-typed-edge-pass", "Run the typed-edge auto-extraction pass over prose-bearing nodes this account wrote since sinceIso. Reads Message/Page/Meeting/Email/Idea/Note/Post/Report nodes, batches them, asks Haiku for typed-edge proposals against the closed allowlist (ATTENDED, WORKS_AT, INVESTED_IN, FOUNDED, ADVISES, MENTIONS, AUTHORED, ATTACHED_TO, REFERENCES), validates each proposal inline, and MERGEs accepted edges with createdBy provenance. Returns the counters (nodes, proposed, accepted, rejected, failed, ms) for the caller to hand back to admin for its insight-pass summary.", {
2436
- sinceIso: z.string().describe("ISO timestamp lower bound. Prose nodes with updatedAt >= sinceIso are processed."),
2437
- sessionId: z.string().optional().describe("Conversation id stamped onto the createdBy provenance of MERGEd edges. Defaults to SESSION_ID env if absent."),
2438
- }, async (args) => {
2439
- if (!accountId)
2440
- return refuseNoAccount("memory-typed-edge-pass");
2441
- const dbSession = getSession();
2442
- try {
2443
- // Cast: the McpServer instance is `any`-typed at the module level (see
2444
- // file-top comment on TS2589), so `args` infers as the loose
2445
- // ShapeOutput<ZodRawShapeCompat> instead of the Zod schema above.
2446
- // Runtime shape is enforced by the schema; this re-asserts the typed
2447
- // contract at the consumer boundary. Task 404.
2448
- const result = await memoryTypedEdgePass(args, dbSession, accountId);
2449
- return {
2450
- content: [{
2451
- type: "text",
2452
- text: JSON.stringify(result),
2453
- }],
2454
- };
2455
- }
2456
- catch (err) {
2457
- return {
2458
- content: [{
2459
- type: "text",
2460
- text: `Typed-edge pass failed: ${err instanceof Error ? err.message : String(err)}`,
2461
- }],
2462
- isError: true,
2463
- };
2464
- }
2465
- finally {
2466
- await dbSession.close();
2467
- }
2468
- });
2469
- // ===================================================================
2470
2425
  // Dream-cycle (Task 327, absorbs 330)
2471
2426
  // ===================================================================
2472
2427
  // Report MCP tools (Task 332) are registered earlier in this file.