prism-mcp-server 3.0.0 → 3.0.1

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.
package/README.md CHANGED
@@ -14,7 +14,7 @@
14
14
 
15
15
  ## Table of Contents
16
16
 
17
- - [What's New (v3.0.0)](#whats-new-in-v300---agent-hivemind-)
17
+ - [What's New (v3.0.1)](#whats-new-in-v301---agent-identity--brain-clean-up-)
18
18
  - [How Prism Compares](#how-prism-compares)
19
19
  - [Quick Start](#quick-start-zero-config--local-mode)
20
20
  - [Mind Palace Dashboard](#-the-mind-palace-dashboard)
@@ -22,6 +22,7 @@
22
22
  - [Use Cases](#use-cases)
23
23
  - [Architecture](#architecture)
24
24
  - [Tool Reference](#tool-reference)
25
+ - [Agent Hivemind — Role Usage](#agent-hivemind--role-usage)
25
26
  - [LangChain / LangGraph Integration](#langchain--langgraph-integration)
26
27
  - [Environment Variables](#environment-variables)
27
28
  - [Boot Settings (Restart Required)](#-boot-settings-restart-required)
@@ -38,7 +39,17 @@
38
39
 
39
40
  ---
40
41
 
41
- ## What's New in v3.0.0 — Agent Hivemind 🐝
42
+ ## What's New in v3.0.1 — Agent Identity & Brain Clean-up 🧹
43
+
44
+ | Feature | Description |
45
+ |---|---|
46
+ | 🧹 **Brain Health Clean-up** | New **Fix Issues** button in the Mind Palace Dashboard's Brain Health card — detects orphaned handoffs, missing embeddings, and stale rollups, then cleans them up in one click without needing the MCP tool. |
47
+ | 👤 **Agent Identity Settings** | Dashboard Settings → Agent Identity panel lets you set a **Default Role** (`dev`, `qa`, `pm`…) and **Agent Name** (e.g. `Dmitri`). Both values auto-apply as fallbacks in all memory and Hivemind tools — no need to pass them per call. |
48
+ | 📜 **Role-Scoped Skills** | Each agent role can have its own persistent skill/rules document stored in the dashboard (⚙️ Settings → Skills). It is automatically injected into every `session_load_context` response so the agent boots with its rules pre-loaded. |
49
+ | 🔤 **Resource Formatting Fix** | `memory://{project}/handoff` resources now render as formatted plain text (Last Summary, TODOs, Keywords) instead of a raw JSON blob — readable in Claude Desktop's paperclip attach panel. |
50
+
51
+ <details>
52
+ <summary><strong>What's in v3.0.0 — Agent Hivemind 🐝</strong></summary>
42
53
 
43
54
  | Feature | Description |
44
55
  |---|---|
@@ -50,6 +61,9 @@
50
61
  | 🔒 **Conditional Tool Registration** | `PRISM_ENABLE_HIVEMIND` env var gates Hivemind tools — users who don't need multi-agent features keep the same lean tool count as v2.x. |
51
62
  | ✅ **Test Suite** | 58 tests across 4 suites (storage, tools, dashboard, load) with Vitest — includes concurrent write stress tests, role isolation verification, and 0.2ms/write performance benchmarks. |
52
63
 
64
+ </details>
65
+
66
+
53
67
  <details>
54
68
  <summary><strong>What's in v2.5.0 — Enterprise Memory 🏗️</strong></summary>
55
69
 
@@ -102,7 +116,7 @@
102
116
  | Feature | Description |
103
117
  |---|---|
104
118
  | 🩺 **Brain Health Check** | `session_health_check` — like Unix `fsck` for your agent's memory. Detects missing embeddings, duplicate entries, orphaned handoffs, and stale rollups. Use `auto_fix: true` to repair automatically. |
105
- | 📊 **Mind Palace Health** | Brain health indicator on the Mind Palace Dashboard — see your memory integrity at a glance. |
119
+ | 📊 **Mind Palace Health** | Brain health indicator on the Mind Palace Dashboard — see your memory integrity at a glance. **🧹 Fix Issues** button auto-deletes orphaned handoffs in one click. |
106
120
 
107
121
  </details>
108
122
 
@@ -237,11 +251,15 @@ Open **`http://localhost:3000`** in your browser to see exactly what your AI age
237
251
  ![Mind Palace Dashboard](docs/mind-palace-dashboard.png)
238
252
 
239
253
  - **Current State & TODOs** — See the exact context injected into the LLM's prompt
254
+ - **Agent Identity Chip** — Header shows your active role + name (e.g. `🛠️ dev · Antigravity`); click to open Settings
255
+ - **Brain Health 🩺** — Memory integrity status at a glance; **🧹 Fix Issues** button auto-cleans orphaned handoffs in one click
240
256
  - **Git Drift Detection** — Alerts you if you've modified code outside the agent's view
241
257
  - **Morning Briefing** — AI-synthesized action plan from your last sessions
242
258
  - **Time Travel Timeline** — Browse historical handoff states and revert any version
243
259
  - **Visual Memory Vault** — Browse UI screenshots and auto-captured HTML states
244
260
  - **Session Ledger** — Full audit trail of every decision your agent has made
261
+ - **Neural Graph** — Force-directed visualization of project ↔ keyword associations
262
+ - **Hivemind Radar** — Real-time active agent roster with role, task, and heartbeat
245
263
 
246
264
  The dashboard auto-discovers all your projects and updates in real time.
247
265
 
@@ -463,7 +481,15 @@ graph TB
463
481
 
464
482
  | Tool | Purpose | Key Args | Returns |
465
483
  |------|---------|----------|---------|
466
- | `session_health_check` | Scan brain for integrity issues (`fsck`) | `auto_fix` (boolean) | Health report & auto-repairs |
484
+ | `session_health_check` | Scan brain for integrity issues (`fsck`) | `project`, `auto_fix` (boolean) | Health report & auto-repairs |
485
+
486
+ The **Mind Palace Dashboard** also shows a live **Brain Health 🩺** card for every project:
487
+
488
+ - **Status indicator** — `✅ Healthy` or `⚠️ Issues detected` with entry/handoff/rollup counts
489
+ - **🧹 Fix Issues button** — appears automatically when issues are detected; click to clean up orphaned handoffs and stale rollups in one click, no MCP tool call required
490
+ - **No issues found** — shown in green when memory integrity is confirmed
491
+
492
+ The tool and dashboard button both call the same repair logic — the dashboard button is simply a zero-friction shortcut for common maintenance.
467
493
 
468
494
  ### v2.5 Enterprise Memory Tools
469
495
 
@@ -492,6 +518,110 @@ Instead of writing custom JavaScript, pass a `template` name for instant extract
492
518
 
493
519
  ---
494
520
 
521
+ ## Agent Hivemind — Role Usage
522
+
523
+ Role-scoped memory lets multiple agents work on the same project without stepping on each other's memory. Each role gets its own isolated memory lane. Defaults to `global` for full backward compatibility.
524
+
525
+ ### Available Roles
526
+
527
+ | Role | Use for |
528
+ |------|---------|
529
+ | `dev` | Development agent |
530
+ | `qa` | Testing / QA agent |
531
+ | `pm` | Product management |
532
+ | `lead` | Tech lead / orchestrator |
533
+ | `security` | Security review |
534
+ | `ux` | Design / UX |
535
+ | `global` | Default — shared, no isolation |
536
+
537
+ Custom role strings are also supported (e.g. `"docs"`, `"ml"`).
538
+
539
+ ### Using Roles with Memory Tools
540
+
541
+ Just add `"role"` to any of the core memory tools:
542
+
543
+ ```json
544
+ // Save a ledger entry as the "dev" agent
545
+ { "name": "session_save_ledger", "arguments": {
546
+ "project": "my-app",
547
+ "role": "dev",
548
+ "conversation_id": "abc123",
549
+ "summary": "Fixed the auth race condition"
550
+ }}
551
+
552
+ // Load context scoped to your role
553
+ // Also injects a Team Roster showing active teammates
554
+ { "name": "session_load_context", "arguments": {
555
+ "project": "my-app",
556
+ "role": "dev",
557
+ "level": "standard"
558
+ }}
559
+
560
+ // Save handoff as the "qa" agent
561
+ { "name": "session_save_handoff", "arguments": {
562
+ "project": "my-app",
563
+ "role": "qa",
564
+ "last_summary": "Ran regression suite — 2 failures in auth module"
565
+ }}
566
+ ```
567
+
568
+ ### Hivemind Coordination Tools
569
+
570
+ > **Requires:** `PRISM_ENABLE_HIVEMIND=true` (Boot Setting — restart required)
571
+
572
+ ```json
573
+ // Announce yourself to the team at session start
574
+ { "name": "agent_register", "arguments": {
575
+ "project": "my-app",
576
+ "role": "dev",
577
+ "agent_name": "Dev Agent #1",
578
+ "current_task": "Refactoring auth module"
579
+ }}
580
+
581
+ // Pulse every ~5 min to stay visible (agents pruned after 30 min)
582
+ { "name": "agent_heartbeat", "arguments": {
583
+ "project": "my-app",
584
+ "role": "dev",
585
+ "current_task": "Now writing tests"
586
+ }}
587
+
588
+ // See everyone on the team
589
+ { "name": "agent_list_team", "arguments": {
590
+ "project": "my-app"
591
+ }}
592
+ ```
593
+
594
+ ### How Role Isolation Works
595
+
596
+ - `session_load_context` with `role: "dev"` only sees entries saved with `role: "dev"`
597
+ - The `global` role is a shared pool — anything saved without a role goes here
598
+ - When loading *with* a role, Prism auto-injects a **Team Roster** block listing active teammates, roles, and tasks — no extra tool call needed
599
+ - The Hivemind Radar widget in the Mind Palace dashboard shows agent activity in real time
600
+
601
+ ### Setting Your Agent Identity
602
+
603
+ The easiest way to configure your role and name is via the **Mind Palace Dashboard ⚙️ Settings → Agent Identity**:
604
+
605
+ - **Default Role** — dropdown to select `dev`, `qa`, `pm`, `lead`, `security`, `ux`, or `global`
606
+ - **Agent Name** — free text for your display name (e.g. `Dmitri`, `Dev Alex`, `QA Bot`)
607
+
608
+ Once set, **all memory and Hivemind tools automatically use these values** as fallbacks — no need to pass `role` or `agent_name` in every tool call.
609
+
610
+ > **Priority order:** explicit tool arg → dashboard setting → `"global"` (default)
611
+
612
+ **Alternative — hardcode in your startup rules** (if you prefer prompt-level config):
613
+
614
+ ```markdown
615
+ ## Prism MCP Memory Auto-Load (CRITICAL)
616
+ At the start of every new session, call session_load_context with:
617
+ - project: "my-app", role: "dev"
618
+ - project: "my-other-project", role: "dev"
619
+ ```
620
+
621
+ > **Tip:** For true multi-agent setups, each AI instance has its own Mind Palace dashboard — set a different identity per agent there rather than managing it in prompts.
622
+
623
+ ---
624
+
495
625
  ## LangChain / LangGraph Integration
496
626
 
497
627
  Prism MCP includes first-class Python adapters for the LangChain ecosystem, located in `examples/langgraph-agent/`:
@@ -979,14 +1109,19 @@ See [`vertex-ai/`](vertex-ai/) for setup and benchmarks.
979
1109
 
980
1110
  > **[View the full project board →](https://github.com/users/dcostenco/projects/1/views/1)**
981
1111
 
1112
+ ### ✅ v3.0.1 — Agent Identity & Brain Clean-up (Shipped!)
1113
+
1114
+ See [What's New in v3.0.1](#whats-new-in-v301---agent-identity--brain-clean-up-) above.
1115
+
982
1116
  ### ✅ v3.0 — Agent Hivemind (Shipped!)
983
1117
 
984
- See [What's New in v3.0.0](#whats-new-in-v300---agent-hivemind-) above.
1118
+ See [What's New in v3.0.0 — Agent Hivemind](#whats-new-in-v300---agent-hivemind-) above.
985
1119
 
986
1120
  ### 🚀 Future Ideas
987
1121
 
988
1122
  | Feature | Issue | Description |
989
1123
  |---------|-------|-------------|
1124
+ | **Role-Scoped Skills & Rules** | — | Each agent role (`dev`, `qa`, `pm`, etc.) gets its own persistent skill/rules document. Preloaded automatically at session start via `session_load_context`. Skills editable and uploadable from the Mind Palace Dashboard (⚙️ → Skills tab per role). Stored in `configStorage` per-role key — backend already exists. |
990
1125
  | OpenTelemetry SDK Integration | [#6](https://github.com/dcostenco/prism-mcp/issues/6) | W3C-compliant tracing with Jaeger/Zipkin export |
991
1126
  | GDPR Right to Portability | [#7](https://github.com/dcostenco/prism-mcp/issues/7) | `session_export_memory` tool for Art. 20 compliance |
992
1127
  | Multi-agent CRDT Conflict Resolution | [#9](https://github.com/dcostenco/prism-mcp/issues/9) | Conflict-free replicated data types for concurrent agent edits |
@@ -21,6 +21,7 @@ import { exec } from "child_process";
21
21
  import { getStorage } from "../storage/index.js";
22
22
  import { PRISM_USER_ID, SERVER_CONFIG } from "../config.js";
23
23
  import { renderDashboardHTML } from "./ui.js";
24
+ import { getAllSettings, setSetting } from "../storage/configStorage.js";
24
25
  const PORT = parseInt(process.env.PRISM_DASHBOARD_PORT || "3000", 10);
25
26
  /** Read HTTP request body as string */
26
27
  function readBody(req) {
@@ -125,7 +126,7 @@ export async function startDashboardServer() {
125
126
  return res.end(JSON.stringify({ context, ledger, history }));
126
127
  }
127
128
  // ─── API: Brain Health Check (v2.2.0) ───
128
- if (url.pathname === "/api/health") {
129
+ if (url.pathname === "/api/health" && req.method === "GET") {
129
130
  try {
130
131
  const { runHealthCheck } = await import("../utils/healthCheck.js");
131
132
  const stats = await storage.getHealthStats(PRISM_USER_ID);
@@ -146,6 +147,78 @@ export async function startDashboardServer() {
146
147
  }));
147
148
  }
148
149
  }
150
+ // ─── API: Brain Health Cleanup (v3.1) ───
151
+ // Deletes orphaned handoffs (handoffs with no backing ledger entries).
152
+ if (url.pathname === "/api/health/cleanup" && req.method === "POST") {
153
+ try {
154
+ const { runHealthCheck } = await import("../utils/healthCheck.js");
155
+ const stats = await storage.getHealthStats(PRISM_USER_ID);
156
+ const report = runHealthCheck(stats);
157
+ // Collect orphaned handoff projects from the health issues
158
+ const orphaned = stats.orphanedHandoffs || [];
159
+ const cleaned = [];
160
+ for (const { project } of orphaned) {
161
+ try {
162
+ await storage.deleteHandoff(project, PRISM_USER_ID);
163
+ cleaned.push(project);
164
+ console.error(`[Dashboard] Cleaned up orphaned handoff: ${project}`);
165
+ }
166
+ catch (delErr) {
167
+ console.error(`[Dashboard] Failed to delete handoff for ${project}:`, delErr);
168
+ }
169
+ }
170
+ res.writeHead(200, { "Content-Type": "application/json" });
171
+ return res.end(JSON.stringify({
172
+ ok: true,
173
+ cleaned,
174
+ count: cleaned.length,
175
+ message: cleaned.length > 0
176
+ ? `Cleaned up ${cleaned.length} orphaned handoff(s): ${cleaned.join(", ")}`
177
+ : "No orphaned handoffs to clean up.",
178
+ }));
179
+ }
180
+ catch (err) {
181
+ console.error("[Dashboard] Health cleanup error:", err);
182
+ res.writeHead(500, { "Content-Type": "application/json" });
183
+ return res.end(JSON.stringify({ ok: false, error: "Cleanup failed" }));
184
+ }
185
+ }
186
+ // ─── API: Role-Scoped Skills (v3.1) ───
187
+ // GET /api/skills → { skills: { dev: "...", qa: "..." } }
188
+ if (url.pathname === "/api/skills" && req.method === "GET") {
189
+ const all = await getAllSettings();
190
+ const skills = {};
191
+ for (const [k, v] of Object.entries(all)) {
192
+ if (k.startsWith("skill:") && v) {
193
+ skills[k.replace("skill:", "")] = v;
194
+ }
195
+ }
196
+ res.writeHead(200, { "Content-Type": "application/json" });
197
+ return res.end(JSON.stringify({ skills }));
198
+ }
199
+ // POST /api/skills → { role, content } saves skill:<role>
200
+ if (url.pathname === "/api/skills" && req.method === "POST") {
201
+ const body = await new Promise(resolve => {
202
+ let data = "";
203
+ req.on("data", c => data += c);
204
+ req.on("end", () => resolve(data));
205
+ });
206
+ const { role, content } = JSON.parse(body || "{}");
207
+ if (!role) {
208
+ res.writeHead(400);
209
+ return res.end(JSON.stringify({ error: "role required" }));
210
+ }
211
+ await setSetting(`skill:${role}`, content || "");
212
+ res.writeHead(200, { "Content-Type": "application/json" });
213
+ return res.end(JSON.stringify({ ok: true, role }));
214
+ }
215
+ // DELETE /api/skills/:role → clears skill:<role>
216
+ if (url.pathname.startsWith("/api/skills/") && req.method === "DELETE") {
217
+ const role = url.pathname.replace("/api/skills/", "");
218
+ await setSetting(`skill:${role}`, "");
219
+ res.writeHead(200, { "Content-Type": "application/json" });
220
+ return res.end(JSON.stringify({ ok: true, role }));
221
+ }
149
222
  // ─── API: Knowledge Graph Data (v2.3.0) ───
150
223
  if (url.pathname === "/api/graph") {
151
224
  // Fetch recent ledger entries to build the graph
@@ -295,6 +295,21 @@ export function renderDashboardHTML(version) {
295
295
  .health-issues .issue-row {
296
296
  padding: 0.3rem 0; display: flex; gap: 0.5rem; align-items: flex-start;
297
297
  }
298
+ .cleanup-btn {
299
+ margin-left: auto; background: rgba(244,63,94,0.12); border: 1px solid rgba(244,63,94,0.3);
300
+ color: var(--accent-rose); cursor: pointer; font-size: 0.75rem; font-weight: 600;
301
+ padding: 0.2rem 0.65rem; border-radius: 6px; transition: all 0.2s;
302
+ }
303
+ .cleanup-btn:hover { background: rgba(244,63,94,0.25); border-color: var(--accent-rose); }
304
+ .cleanup-btn:disabled { opacity: 0.5; cursor: not-allowed; }
305
+ .toast-fixed {
306
+ position: fixed; bottom: 1.5rem; right: 1.5rem; z-index: 200;
307
+ padding: 0.65rem 1.2rem; border-radius: 10px; font-size: 0.85rem; font-weight: 500;
308
+ backdrop-filter: blur(10px); border: 1px solid var(--border-glow);
309
+ background: var(--bg-secondary); color: var(--text-primary);
310
+ opacity: 0; transition: opacity 0.3s; pointer-events: none;
311
+ }
312
+ .toast-fixed.show { opacity: 1; }
298
313
 
299
314
  /* ─── Neural Graph (v2.3.0) ─── */
300
315
  #network-container {
@@ -318,6 +333,45 @@ export function renderDashboardHTML(version) {
318
333
  transition: all 0.2s;
319
334
  }
320
335
  .settings-btn:hover { border-color: var(--border-glow); color: var(--accent-purple); }
336
+ .identity-chip {
337
+ display: none; align-items: center; gap: 0.4rem;
338
+ padding: 0.35rem 0.75rem; border-radius: 999px;
339
+ background: rgba(139,92,246,0.12); border: 1px solid rgba(139,92,246,0.25);
340
+ color: var(--text-secondary); font-size: 0.8rem; font-weight: 500;
341
+ cursor: pointer; transition: all 0.2s;
342
+ }
343
+ .identity-chip:hover { border-color: var(--accent-purple); color: var(--accent-purple); background: rgba(139,92,246,0.2); }
344
+ .identity-chip .role-icon { font-size: 0.9rem; }
345
+ .identity-chip .identity-label { max-width: 120px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
346
+ /* Settings modal tab bar */
347
+ .settings-tabs { display: flex; gap: 0; border-bottom: 1px solid var(--border-glass); margin: 0 -1.5rem 1.2rem; padding: 0 1.5rem; }
348
+ .s-tab { padding: 0.55rem 1.1rem; font-size: 0.85rem; font-weight: 500; color: var(--text-secondary); cursor: pointer;
349
+ border-bottom: 2px solid transparent; transition: all 0.2s; background: none; border-top: none; border-left: none; border-right: none; }
350
+ .s-tab.active { color: var(--accent-purple); border-bottom-color: var(--accent-purple); }
351
+ .s-tab:hover:not(.active) { color: var(--text-primary); }
352
+ .s-tab-panel { display: none; } .s-tab-panel.active { display: block; }
353
+ /* Skills editor */
354
+ .skill-role-row { display: flex; align-items: center; gap: 0.75rem; margin-bottom: 1rem; }
355
+ .skill-role-row label { font-size: 0.82rem; color: var(--text-secondary); }
356
+ .skill-role-select { padding: 0.3rem 0.6rem; background: var(--bg-hover); color: var(--text-primary);
357
+ border: 1px solid var(--border-color); border-radius: 4px; font-size: 0.85rem; font-family: var(--font-mono); }
358
+ .skill-textarea { width: 100%; min-height: 220px; background: var(--bg-hover); color: var(--text-primary);
359
+ border: 1px solid var(--border-color); border-radius: var(--radius-sm); padding: 0.75rem;
360
+ font-size: 0.82rem; font-family: var(--font-mono); line-height: 1.5; resize: vertical;
361
+ box-sizing: border-box; transition: border-color 0.2s; }
362
+ .skill-textarea:focus { outline: none; border-color: var(--accent-purple); }
363
+ .skill-char-count { font-size: 0.74rem; color: var(--text-muted); text-align: right; margin-top: 0.3rem; }
364
+ .skill-actions { display: flex; gap: 0.6rem; margin-top: 0.85rem; align-items: center; }
365
+ .skill-save-btn { background: var(--accent-purple); color: #fff; border: none; border-radius: var(--radius-sm);
366
+ padding: 0.45rem 1rem; font-size: 0.82rem; font-weight: 600; cursor: pointer; transition: opacity 0.2s; }
367
+ .skill-save-btn:hover { opacity: 0.85; }
368
+ .skill-upload-btn { background: none; border: 1px solid var(--border-glass); color: var(--text-secondary);
369
+ border-radius: var(--radius-sm); padding: 0.45rem 0.85rem; font-size: 0.82rem; cursor: pointer; transition: all 0.2s; }
370
+ .skill-upload-btn:hover { border-color: var(--accent-purple); color: var(--accent-purple); }
371
+ .skill-clear-btn { background: none; border: none; color: var(--text-muted); font-size: 0.8rem; cursor: pointer;
372
+ margin-left: auto; transition: color 0.2s; }
373
+ .skill-clear-btn:hover { color: #ef4444; }
374
+ .skill-hint { font-size: 0.78rem; color: var(--text-muted); margin-top: 0.6rem; line-height: 1.5; }
321
375
  .modal-overlay {
322
376
  display: none; position: fixed; inset: 0; z-index: 100;
323
377
  background: rgba(0,0,0,0.6); backdrop-filter: blur(4px);
@@ -402,6 +456,7 @@ export function renderDashboardHTML(version) {
402
456
  <span class="version-badge">v${version}</span>
403
457
  </div>
404
458
  <div class="selector">
459
+ <span class="identity-chip" id="identityChip" onclick="openSettings()" title="Agent Identity — click to change"></span>
405
460
  <select id="projectSelect">
406
461
  <option value="">Loading projects...</option>
407
462
  </select>
@@ -440,7 +495,10 @@ export function renderDashboardHTML(version) {
440
495
 
441
496
  <!-- Brain Health (v2.2.0) -->
442
497
  <div class="card" id="healthCard" style="display:none">
443
- <div class="card-title"><span class="dot" style="background:var(--accent-green)"></span> Brain Health 🩺</div>
498
+ <div class="card-title">
499
+ <span class="dot" style="background:var(--accent-green)"></span> Brain Health 🩺
500
+ <button class="cleanup-btn" id="cleanupBtn" onclick="cleanupIssues()" style="display:none">🧹 Fix Issues</button>
501
+ </div>
444
502
  <div class="health-status">
445
503
  <div class="health-dot unknown" id="healthDot"></div>
446
504
  <div>
@@ -512,6 +570,15 @@ export function renderDashboardHTML(version) {
512
570
  <button class="modal-close" onclick="closeSettings()">✕</button>
513
571
  <h2>⚙️ Settings</h2>
514
572
 
573
+ <!-- Tab bar -->
574
+ <div class="settings-tabs">
575
+ <button class="s-tab active" id="stab-settings" onclick="switchSettingsTab('settings')">⚙️ Settings</button>
576
+ <button class="s-tab" id="stab-skills" onclick="switchSettingsTab('skills')">📜 Skills</button>
577
+ </div>
578
+
579
+ <!-- Settings panel (existing content) -->
580
+ <div class="s-tab-panel active" id="spanel-settings">
581
+
515
582
  <div class="setting-section">Runtime Settings</div>
516
583
 
517
584
  <div class="setting-row">
@@ -565,12 +632,107 @@ export function renderDashboardHTML(version) {
565
632
  <option value="supabase">Supabase</option>
566
633
  </select>
567
634
  </div>
635
+
636
+ <div class="setting-section">Agent Identity</div>
637
+
638
+ <div class="setting-row">
639
+ <div>
640
+ <div class="setting-label">Default Role</div>
641
+ <div class="setting-desc">Used when no role is passed to memory/Hivemind tools</div>
642
+ </div>
643
+ <select class="setting-select" id="select-default-role" onchange="saveSetting('default_role', this.value)">
644
+ <option value="global">global (shared)</option>
645
+ <option value="dev">dev</option>
646
+ <option value="qa">qa</option>
647
+ <option value="pm">pm</option>
648
+ <option value="lead">lead</option>
649
+ <option value="security">security</option>
650
+ <option value="ux">ux</option>
651
+ </select>
652
+ </div>
653
+
654
+ <div class="setting-row">
655
+ <div>
656
+ <div class="setting-label">Agent Name</div>
657
+ <div class="setting-desc">Display name shown in Hivemind Radar (e.g. Dmitri, Dev Alex)</div>
658
+ </div>
659
+ <input type="text" id="input-agent-name"
660
+ placeholder="e.g. Dmitri"
661
+ style="padding: 0.2rem 0.5rem; background: var(--bg-hover); color: var(--text-primary); border: 1px solid var(--border-color); border-radius: 4px; font-size: 0.85rem; font-family: var(--font-mono); width: 130px;"
662
+ onchange="saveSetting('agent_name', this.value)"
663
+ oninput="clearTimeout(this._t); this._t=setTimeout(()=>saveSetting('agent_name',this.value),800)" />
664
+ </div>
665
+
568
666
  <span class="setting-saved" id="savedToast">Saved ✓</span>
667
+ </div><!-- /spanel-settings -->
668
+
669
+ <!-- Skills panel -->
670
+ <div class="s-tab-panel" id="spanel-skills">
671
+ <div class="skill-role-row">
672
+ <label>Role</label>
673
+ <select class="skill-role-select" id="skillRoleSelect" onchange="loadSkillForRole(this.value)">
674
+ <option value="global">🌐 global</option>
675
+ <option value="dev">🛠️ dev</option>
676
+ <option value="qa">🔍 qa</option>
677
+ <option value="pm">📋 pm</option>
678
+ <option value="lead">🏗️ lead</option>
679
+ <option value="security">🔒 security</option>
680
+ <option value="ux">🎨 ux</option>
681
+ </select>
682
+ </div>
683
+ <textarea class="skill-textarea" id="skillTextarea"
684
+ placeholder="Paste rules, conventions, or prompts for this role...
685
+ Example:\n## Dev Rules\n- Always write tests first\n- Use TypeScript strict mode\n- Log errors to console.error"
686
+ oninput="document.getElementById('skillCharCount').textContent = this.value.length + ' chars'">
687
+ </textarea>
688
+ <div class="skill-char-count" id="skillCharCount">0 chars</div>
689
+ <div class="skill-actions">
690
+ <button class="skill-save-btn" onclick="saveCurrentSkill()">💾 Save</button>
691
+ <label class="skill-upload-btn" title="Upload a .md or .txt file">
692
+ 📎 Upload file
693
+ <input type="file" accept=".md,.txt,.markdown" style="display:none"
694
+ onchange="handleSkillUpload(this)">
695
+ </label>
696
+ <button class="skill-clear-btn" onclick="clearCurrentSkill()">🗑️ Clear</button>
697
+ </div>
698
+ <div class="skill-hint">
699
+ Skills are auto-injected into <code>session_load_context</code> responses for this role.<br>
700
+ Use Markdown. Changes take effect immediately — no restart needed.
701
+ </div>
702
+ </div><!-- /spanel-skills -->
703
+
569
704
  </div>
570
705
  </div>
571
706
  </div>
572
707
 
708
+ <!-- Fixed toast for cleanup feedback -->
709
+ <div class="toast-fixed" id="fixedToast"></div>
710
+
573
711
  <script>
712
+ // Role icon map
713
+ var ROLE_ICONS = {dev:'🛠️',qa:'🔍',pm:'📋',lead:'🏗️',security:'🔒',ux:'🎨',global:'🌐',cmo:'📢'};
714
+
715
+ // Load and render the identity chip from settings
716
+ async function loadIdentityChip() {
717
+ try {
718
+ var res = await fetch('/api/settings');
719
+ var data = await res.json();
720
+ var s = data.settings || {};
721
+ var role = s.default_role || '';
722
+ var name = s.agent_name || '';
723
+ var chip = document.getElementById('identityChip');
724
+ if (!chip) return;
725
+ if (role && role !== 'global' || name) {
726
+ var icon = ROLE_ICONS[role] || '🤖';
727
+ var label = name ? (role && role !== 'global' ? role + ' · ' + name : name) : role;
728
+ chip.innerHTML = '<span class="role-icon">' + icon + '</span><span class="identity-label">' + escapeHtml(label) + '</span>';
729
+ chip.style.display = 'flex';
730
+ } else {
731
+ chip.style.display = 'none';
732
+ }
733
+ } catch(e) { /* silently skip */ }
734
+ }
735
+
574
736
  // Auto-load project list on page load
575
737
  (async function() {
576
738
  try {
@@ -586,6 +748,8 @@ export function renderDashboardHTML(version) {
586
748
  } catch(e) {
587
749
  document.getElementById('projectSelect').innerHTML = '<option value="">Error loading projects</option>';
588
750
  }
751
+ // Load identity chip once settings are available
752
+ loadIdentityChip();
589
753
  })();
590
754
 
591
755
  async function loadProject() {
@@ -707,6 +871,7 @@ export function renderDashboardHTML(version) {
707
871
 
708
872
  // Issue rows
709
873
  var issues = healthData.issues || [];
874
+ var cleanupBtn = document.getElementById('cleanupBtn');
710
875
  if (issues.length > 0) {
711
876
  var sevIcons = { error: '🔴', warning: '🟡', info: '🔵' };
712
877
  healthIssues.innerHTML = issues.map(function(i) {
@@ -715,8 +880,10 @@ export function renderDashboardHTML(version) {
715
880
  '<span>' + escapeHtml(i.message) + '</span>' +
716
881
  '</div>';
717
882
  }).join('');
883
+ if (cleanupBtn) cleanupBtn.style.display = 'inline-block';
718
884
  } else {
719
885
  healthIssues.innerHTML = '<div style="color:var(--accent-green);font-size:0.8rem">🎉 No issues found</div>';
886
+ if (cleanupBtn) cleanupBtn.style.display = 'none';
720
887
  }
721
888
 
722
889
  healthCard.style.display = 'block';
@@ -832,6 +999,75 @@ export function renderDashboardHTML(version) {
832
999
  if (e.target === this) closeSettings();
833
1000
  });
834
1001
 
1002
+ // ─── Skills Tab JS ───────────────────────────────────────────
1003
+ var _skillsCache = {}; // role → content cache
1004
+
1005
+ function switchSettingsTab(tab) {
1006
+ ['settings','skills'].forEach(function(t) {
1007
+ document.getElementById('stab-' + t).classList.toggle('active', t === tab);
1008
+ document.getElementById('spanel-' + t).classList.toggle('active', t === tab);
1009
+ });
1010
+ if (tab === 'skills') {
1011
+ // Load skill for whichever role is currently selected
1012
+ var role = document.getElementById('skillRoleSelect').value;
1013
+ loadSkillForRole(role);
1014
+ }
1015
+ }
1016
+
1017
+ async function loadSkillForRole(role) {
1018
+ try {
1019
+ var res = await fetch('/api/skills');
1020
+ var data = await res.json();
1021
+ _skillsCache = data.skills || {};
1022
+ var content = _skillsCache[role] || '';
1023
+ var ta = document.getElementById('skillTextarea');
1024
+ ta.value = content;
1025
+ document.getElementById('skillCharCount').textContent = content.length + ' chars';
1026
+ } catch(e) { console.warn('Skills load failed:', e); }
1027
+ }
1028
+
1029
+ async function saveCurrentSkill() {
1030
+ var role = document.getElementById('skillRoleSelect').value;
1031
+ var content = document.getElementById('skillTextarea').value;
1032
+ try {
1033
+ await fetch('/api/skills', {
1034
+ method: 'POST',
1035
+ headers: { 'Content-Type': 'application/json' },
1036
+ body: JSON.stringify({ role: role, content: content })
1037
+ });
1038
+ _skillsCache[role] = content;
1039
+ showFixedToast('✅ Skill saved for ' + role, true);
1040
+ } catch(e) { showFixedToast('❌ Save failed', false); }
1041
+ }
1042
+
1043
+ async function clearCurrentSkill() {
1044
+ var role = document.getElementById('skillRoleSelect').value;
1045
+ try {
1046
+ await fetch('/api/skills/' + role, { method: 'DELETE' });
1047
+ document.getElementById('skillTextarea').value = '';
1048
+ document.getElementById('skillCharCount').textContent = '0 chars';
1049
+ _skillsCache[role] = '';
1050
+ showFixedToast('🗑️ Skill cleared for ' + role, true);
1051
+ } catch(e) { showFixedToast('❌ Clear failed', false); }
1052
+ }
1053
+
1054
+ function handleSkillUpload(input) {
1055
+ var file = input.files[0];
1056
+ if (!file) return;
1057
+ var reader = new FileReader();
1058
+ reader.onload = async function(e) {
1059
+ var content = e.target.result;
1060
+ var ta = document.getElementById('skillTextarea');
1061
+ ta.value = content;
1062
+ document.getElementById('skillCharCount').textContent = content.length + ' chars';
1063
+ // Auto-save after upload
1064
+ await saveCurrentSkill();
1065
+ };
1066
+ reader.readAsText(file);
1067
+ input.value = ''; // reset so same file can be re-uploaded
1068
+ }
1069
+
1070
+
835
1071
  async function loadSettings() {
836
1072
  try {
837
1073
  var res = await fetch('/api/settings');
@@ -855,6 +1091,9 @@ export function renderDashboardHTML(version) {
855
1091
  if (s.PRISM_STORAGE) {
856
1092
  document.getElementById('storageBackendSelect').value = s.PRISM_STORAGE;
857
1093
  }
1094
+ // Agent Identity
1095
+ if (s.default_role) document.getElementById('select-default-role').value = s.default_role;
1096
+ if (s.agent_name) document.getElementById('input-agent-name').value = s.agent_name;
858
1097
  } catch(e) { console.warn('Settings load failed:', e); }
859
1098
  }
860
1099
 
@@ -879,8 +1118,9 @@ export function renderDashboardHTML(version) {
879
1118
  headers: { 'Content-Type': 'application/json' },
880
1119
  body: JSON.stringify({ key: key, value: value })
881
1120
  });
882
- // Apply theme instantly on change
883
1121
  if (key === 'dashboard_theme') applyTheme(value);
1122
+ // Refresh identity chip if role or name changed
1123
+ if (key === 'default_role' || key === 'agent_name') loadIdentityChip();
884
1124
  showToast('Saved ✓');
885
1125
  } catch(e) { console.error('Setting save failed:', e); }
886
1126
  }
@@ -939,6 +1179,55 @@ export function renderDashboardHTML(version) {
939
1179
  if (mins < 60) return mins + 'm ago';
940
1180
  return Math.floor(mins/60) + 'h ago';
941
1181
  }
1182
+
1183
+ // ─── Brain Health Cleanup (v3.1) ───
1184
+ async function cleanupIssues() {
1185
+ var btn = document.getElementById('cleanupBtn');
1186
+ if (btn) { btn.disabled = true; btn.textContent = 'Cleaning...'; }
1187
+ try {
1188
+ var res = await fetch('/api/health/cleanup', { method: 'POST' });
1189
+ var data = await res.json();
1190
+ showFixedToast(data.message || (data.ok ? 'Cleanup complete.' : 'Cleanup failed.'), data.ok);
1191
+ // Re-run health check to refresh the card
1192
+ setTimeout(async function() {
1193
+ try {
1194
+ var healthRes = await fetch('/api/health');
1195
+ var healthData = await healthRes.json();
1196
+ var healthDot = document.getElementById('healthDot');
1197
+ var healthLabel = document.getElementById('healthLabel');
1198
+ var healthSummary = document.getElementById('healthSummary');
1199
+ var healthIssues = document.getElementById('healthIssues');
1200
+ var cleanupBtn = document.getElementById('cleanupBtn');
1201
+ var statusMap = { healthy: '✅ Healthy', degraded: '⚠️ Degraded', unhealthy: '🔴 Unhealthy' };
1202
+ healthDot.className = 'health-dot ' + (healthData.status || 'unknown');
1203
+ healthLabel.textContent = statusMap[healthData.status] || '❓ Unknown';
1204
+ var t = healthData.totals || {};
1205
+ healthSummary.textContent = (t.activeEntries || 0) + ' entries · ' + (t.handoffs || 0) + ' handoffs · ' + (t.rollups || 0) + ' rollups';
1206
+ var issues = healthData.issues || [];
1207
+ if (issues.length > 0) {
1208
+ var sevIcons = { error: '🔴', warning: '🟡', info: '🔵' };
1209
+ healthIssues.innerHTML = issues.map(function(i) {
1210
+ return '<div class="issue-row"><span>' + (sevIcons[i.severity] || '❓') + '</span><span>' + escapeHtml(i.message) + '</span></div>';
1211
+ }).join('');
1212
+ if (cleanupBtn) { cleanupBtn.disabled = false; cleanupBtn.textContent = '🧹 Fix Issues'; cleanupBtn.style.display = 'inline-block'; }
1213
+ } else {
1214
+ healthIssues.innerHTML = '<div style="color:var(--accent-green);font-size:0.8rem">🎉 No issues found</div>';
1215
+ if (cleanupBtn) cleanupBtn.style.display = 'none';
1216
+ }
1217
+ } catch(e) {}
1218
+ }, 400);
1219
+ } catch(e) {
1220
+ showFixedToast('Cleanup request failed.', false);
1221
+ if (btn) { btn.disabled = false; btn.textContent = '🧹 Fix Issues'; }
1222
+ }
1223
+ }
1224
+
1225
+ function showFixedToast(msg, ok) {
1226
+ var t = document.getElementById('fixedToast');
1227
+ t.textContent = (ok === false ? '❌ ' : '✅ ') + msg;
1228
+ t.classList.add('show');
1229
+ setTimeout(function() { t.classList.remove('show'); }, 3500);
1230
+ }
942
1231
  </script>
943
1232
  </body>
944
1233
  </html>`;
package/dist/server.js CHANGED
@@ -348,6 +348,8 @@ export function createServer() {
348
348
  }
349
349
  });
350
350
  // Read a specific project's handoff as a resource
351
+ // v3.1 FIX: Returns formatted text/plain (same layout as session_load_context)
352
+ // so MCP clients render it as readable text instead of a raw JSON blob.
351
353
  server.setRequestHandler(ReadResourceRequestSchema, async (request) => {
352
354
  const uri = request.params.uri;
353
355
  const match = uri.match(/^memory:\/\/(.+)\/handoff$/);
@@ -356,20 +358,49 @@ export function createServer() {
356
358
  }
357
359
  const project = decodeURIComponent(match[1]);
358
360
  try {
359
- // v2.3.6 FIX: Use storage abstraction instead of direct supabaseRpc
360
361
  const storage = await getStorage();
361
362
  const data = await storage.loadContext(project, "standard", PRISM_USER_ID);
362
- const resourceData = data || { status: "no_session_found", project };
363
- if (data?.version) {
364
- resourceData._occ_instruction =
365
- `When saving handoff state, you MUST pass expected_version: ${data.version} ` +
366
- `to prevent state collisions with other sessions.`;
363
+ if (!data) {
364
+ return {
365
+ contents: [{
366
+ uri,
367
+ mimeType: "text/plain",
368
+ text: `No session context found for project "${project}".\nThis project has no previous session history. Starting fresh.`,
369
+ }],
370
+ };
371
+ }
372
+ // Format identically to sessionLoadContextHandler so the resource
373
+ // renders as readable text rather than a raw JSON dump.
374
+ const d = data;
375
+ let formattedContext = "";
376
+ if (d.last_summary)
377
+ formattedContext += `📝 Last Summary: ${d.last_summary}\n`;
378
+ if (d.active_branch)
379
+ formattedContext += `🌿 Active Branch: ${d.active_branch}\n`;
380
+ if (d.key_context)
381
+ formattedContext += `💡 Key Context: ${d.key_context}\n`;
382
+ if (d.pending_todo?.length) {
383
+ formattedContext += `\n✅ Open TODOs:\n` + d.pending_todo.map((t) => ` - ${t}`).join("\n") + `\n`;
384
+ }
385
+ if (d.active_decisions?.length) {
386
+ formattedContext += `\n⚖️ Active Decisions:\n` + d.active_decisions.map((dec) => ` - ${dec}`).join("\n") + `\n`;
367
387
  }
388
+ if (d.keywords?.length) {
389
+ formattedContext += `\n🔑 Keywords: ${d.keywords.join(", ")}\n`;
390
+ }
391
+ if (d.recent_sessions?.length) {
392
+ formattedContext += `\n⏳ Recent Sessions:\n` + d.recent_sessions.map((s) => ` [${s.session_date?.split("T")[0]}] ${s.summary}`).join("\n") + `\n`;
393
+ }
394
+ const version = d.version;
395
+ const versionNote = version
396
+ ? `\n\n🔑 Session version: ${version}. Pass expected_version: ${version} when saving handoff.\n` +
397
+ `_occ_instruction: When saving handoff state, you MUST pass expected_version: ${version} to prevent state collisions with other sessions.`
398
+ : "";
368
399
  return {
369
400
  contents: [{
370
- uri: uri,
371
- mimeType: "application/json",
372
- text: JSON.stringify(resourceData, null, 2),
401
+ uri,
402
+ mimeType: "text/plain",
403
+ text: `📋 Session context for "${project}" (standard):\n\n${formattedContext.trim()}${versionNote}`,
373
404
  }],
374
405
  };
375
406
  }
@@ -378,7 +409,7 @@ export function createServer() {
378
409
  return {
379
410
  isError: true,
380
411
  contents: [{
381
- uri: uri,
412
+ uri,
382
413
  mimeType: "text/plain",
383
414
  text: `Error reading resource: ${error instanceof Error ? error.message : String(error)}`,
384
415
  }],
@@ -20,6 +20,7 @@ import * as fs from "fs";
20
20
  import * as path from "path";
21
21
  import * as os from "os";
22
22
  import { randomUUID } from "crypto";
23
+ import { getSetting as cfgGet, setSetting as cfgSet, getAllSettings as cfgGetAll } from "./configStorage.js";
23
24
  import { debugLog } from "../utils/logger.js";
24
25
  export class SqliteStorage {
25
26
  db;
@@ -1130,4 +1131,15 @@ export class SqliteStorage {
1130
1131
  });
1131
1132
  debugLog(`[SqliteStorage] Agent deregistered: ${project}/${role}`);
1132
1133
  }
1134
+ // ─── System Settings (v3.0 Dashboard) — proxy to configStorage ───
1135
+ async getSetting(key) {
1136
+ const val = await cfgGet(key, "");
1137
+ return val === "" ? null : val;
1138
+ }
1139
+ async setSetting(key, value) {
1140
+ await cfgSet(key, value);
1141
+ }
1142
+ async getAllSettings() {
1143
+ return cfgGetAll();
1144
+ }
1133
1145
  }
@@ -14,6 +14,7 @@
14
14
  */
15
15
  import { supabasePost, supabaseGet, supabaseRpc, supabasePatch, supabaseDelete, } from "../utils/supabaseApi.js";
16
16
  import { debugLog } from "../utils/logger.js";
17
+ import { getSetting as cfgGet, setSetting as cfgSet, getAllSettings as cfgGetAll } from "./configStorage.js";
17
18
  export class SupabaseStorage {
18
19
  // ─── Lifecycle ─────────────────────────────────────────────
19
20
  async initialize() {
@@ -284,4 +285,15 @@ export class SupabaseStorage {
284
285
  role: `eq.${role}`,
285
286
  });
286
287
  }
288
+ // ─── System Settings (v3.0 Dashboard) — proxy to configStorage ───
289
+ async getSetting(key) {
290
+ const val = await cfgGet(key, "");
291
+ return val === "" ? null : val;
292
+ }
293
+ async setSetting(key, value) {
294
+ await cfgSet(key, value);
295
+ }
296
+ async getAllSettings() {
297
+ return cfgGetAll();
298
+ }
287
299
  }
@@ -7,12 +7,13 @@
7
7
  import { getStorage } from "../storage/index.js";
8
8
  import { PRISM_USER_ID } from "../config.js";
9
9
  import { getRoleIcon } from "./agentRegistryDefinitions.js";
10
+ import { getSetting } from "../storage/configStorage.js";
10
11
  // ─── Type Guards ─────────────────────────────────────────────
11
12
  function isAgentRegisterArgs(args) {
12
- return typeof args.project === "string" && typeof args.role === "string";
13
+ return typeof args.project === "string";
13
14
  }
14
15
  function isAgentHeartbeatArgs(args) {
15
- return typeof args.project === "string" && typeof args.role === "string";
16
+ return typeof args.project === "string";
16
17
  }
17
18
  function isAgentListTeamArgs(args) {
18
19
  return typeof args.project === "string";
@@ -21,27 +22,30 @@ function isAgentListTeamArgs(args) {
21
22
  export async function agentRegisterHandler(args) {
22
23
  if (!isAgentRegisterArgs(args)) {
23
24
  return {
24
- content: [{ type: "text", text: "Missing required: project, role" }],
25
+ content: [{ type: "text", text: "Missing required: project" }],
25
26
  isError: true,
26
27
  };
27
28
  }
29
+ // Fall back to dashboard-configured identity if not passed explicitly
30
+ const effectiveRole = args.role || await getSetting("default_role", "global");
31
+ const effectiveName = args.agent_name || await getSetting("agent_name", "") || null;
28
32
  const storage = await getStorage();
29
33
  const result = await storage.registerAgent({
30
34
  project: args.project,
31
35
  user_id: PRISM_USER_ID,
32
- role: args.role,
33
- agent_name: args.agent_name || null,
36
+ role: effectiveRole,
37
+ agent_name: effectiveName,
34
38
  status: "active",
35
39
  current_task: args.current_task || null,
36
40
  });
37
- const icon = getRoleIcon(args.role);
41
+ const icon = getRoleIcon(effectiveRole);
38
42
  return {
39
43
  content: [{
40
44
  type: "text",
41
45
  text: `${icon} **Agent Registered**\n\n` +
42
46
  `- **Project:** ${args.project}\n` +
43
- `- **Role:** ${args.role}\n` +
44
- (args.agent_name ? `- **Name:** ${args.agent_name}\n` : "") +
47
+ `- **Role:** ${effectiveRole}\n` +
48
+ (effectiveName ? `- **Name:** ${effectiveName}\n` : "") +
45
49
  (args.current_task ? `- **Task:** ${args.current_task}\n` : "") +
46
50
  `\nOther agents will see you when they call \`agent_list_team\` or \`session_load_context\`.`,
47
51
  }],
@@ -50,16 +54,17 @@ export async function agentRegisterHandler(args) {
50
54
  export async function agentHeartbeatHandler(args) {
51
55
  if (!isAgentHeartbeatArgs(args)) {
52
56
  return {
53
- content: [{ type: "text", text: "Missing required: project, role" }],
57
+ content: [{ type: "text", text: "Missing required: project" }],
54
58
  isError: true,
55
59
  };
56
60
  }
61
+ const effectiveRole = args.role || await getSetting("default_role", "global");
57
62
  const storage = await getStorage();
58
- await storage.heartbeatAgent(args.project, PRISM_USER_ID, args.role, args.current_task);
63
+ await storage.heartbeatAgent(args.project, PRISM_USER_ID, effectiveRole, args.current_task);
59
64
  return {
60
65
  content: [{
61
66
  type: "text",
62
- text: `💓 Heartbeat updated for **${args.role}** on \`${args.project}\`.` +
67
+ text: `💓 Heartbeat updated for **${effectiveRole}** on \`${args.project}\`.` +
63
68
  (args.current_task ? ` Task: ${args.current_task}` : ""),
64
69
  }],
65
70
  };
@@ -20,6 +20,7 @@ import { getStorage } from "../storage/index.js";
20
20
  import { toKeywordArray } from "../utils/keywordExtractor.js";
21
21
  import { generateEmbedding } from "../utils/embeddingApi.js";
22
22
  import { getCurrentGitState, getGitDrift } from "../utils/git.js";
23
+ import { getSetting } from "../storage/configStorage.js";
23
24
  // ─── Phase 1: Explainability & Memory Lineage ────────────────
24
25
  // These utilities provide structured tracing metadata for search operations.
25
26
  // When `enable_trace: true` is passed to session_search_memory or knowledge_search,
@@ -54,6 +55,7 @@ export async function sessionSaveLedgerHandler(args) {
54
55
  const keywords = toKeywordArray(combinedText);
55
56
  debugLog(`[session_save_ledger] Extracted ${keywords.length} keywords: ${keywords.slice(0, 5).join(", ")}...`);
56
57
  // Save via storage backend
58
+ const effectiveRole = role || await getSetting("default_role", "global");
57
59
  const result = await storage.saveLedger({
58
60
  project,
59
61
  conversation_id,
@@ -63,7 +65,7 @@ export async function sessionSaveLedgerHandler(args) {
63
65
  files_changed: files_changed || [],
64
66
  decisions: decisions || [],
65
67
  keywords,
66
- role: role || "global", // v3.0: Hivemind role scoping
68
+ role: effectiveRole, // v3.0: Hivemind role scoping (dashboard fallback)
67
69
  });
68
70
  // ─── Fire-and-forget embedding generation ───
69
71
  if (GOOGLE_API_KEY && result) {
@@ -125,6 +127,7 @@ export async function sessionSaveHandoffHandler(args, server) {
125
127
  debugLog(`[session_save_handoff] Git state captured: branch=${gitState.branch}, sha=${gitState.commitSha?.substring(0, 8)}`);
126
128
  }
127
129
  // Save via storage backend (OCC-aware)
130
+ const effectiveRole = role || await getSetting("default_role", "global");
128
131
  const data = await storage.saveHandoff({
129
132
  project,
130
133
  user_id: PRISM_USER_ID,
@@ -135,7 +138,7 @@ export async function sessionSaveHandoffHandler(args, server) {
135
138
  key_context: key_context ?? null,
136
139
  active_branch: active_branch ?? null,
137
140
  metadata,
138
- role: role || "global", // v3.0: Hivemind role scoping
141
+ role: effectiveRole, // v3.0: Hivemind role scoping (dashboard fallback)
139
142
  }, expected_version ?? null);
140
143
  // ─── Handle version conflict ───
141
144
  if (data.status === "conflict") {
@@ -321,6 +324,7 @@ export async function sessionLoadContextHandler(args) {
321
324
  throw new Error("Invalid arguments for session_load_context");
322
325
  }
323
326
  const { project, level = "standard", role } = args;
327
+ const agentName = await getSetting("agent_name", "");
324
328
  const validLevels = ["quick", "standard", "deep"];
325
329
  if (!validLevels.includes(level)) {
326
330
  return {
@@ -333,7 +337,8 @@ export async function sessionLoadContextHandler(args) {
333
337
  }
334
338
  debugLog(`[session_load_context] Loading ${level} context for project="${project}"`);
335
339
  const storage = await getStorage();
336
- const data = await storage.loadContext(project, level, PRISM_USER_ID, role); // v3.0: pass role
340
+ const effectiveRole = role || await getSetting("default_role", "") || undefined;
341
+ const data = await storage.loadContext(project, level, PRISM_USER_ID, effectiveRole); // v3.0: role with dashboard fallback
337
342
  if (!data) {
338
343
  return {
339
344
  content: [{
@@ -471,10 +476,32 @@ export async function sessionLoadContextHandler(args) {
471
476
  if (d.session_history?.length) {
472
477
  formattedContext += `\n📂 Session History (${d.session_history.length} entries):\n` + d.session_history.map((s) => ` [${s.session_date?.split("T")[0]}] ${s.summary}`).join("\n") + `\n`;
473
478
  }
479
+ // ─── Role-Scoped Skill Injection ─────────────────────────────
480
+ // If the active role has a skill document stored, append it so the
481
+ // agent loads its rules/conventions automatically at session start.
482
+ let skillBlock = "";
483
+ let skillLoaded = false;
484
+ if (effectiveRole) {
485
+ const skillContent = await getSetting(`skill:${effectiveRole}`, "");
486
+ if (skillContent && skillContent.trim()) {
487
+ skillBlock = `\n\n[📜 ROLE SKILL: ${effectiveRole}]\n${skillContent.trim()}`;
488
+ skillLoaded = true;
489
+ debugLog(`[session_load_context] Injecting skill for role="${effectiveRole}" (${skillContent.length} chars)`);
490
+ }
491
+ }
492
+ // ─── Agent Greeting Block ────────────────────────────────────
493
+ // Shows agent identity (name + role) and skill status after briefing.
494
+ let greetingBlock = "";
495
+ if (agentName || effectiveRole) {
496
+ const namePart = agentName ? `👋 **${agentName}**` : `👋 **Agent**`;
497
+ const rolePart = effectiveRole ? ` · Role: \`${effectiveRole}\`` : "";
498
+ const skillPart = skillLoaded ? ` · 📜 \`${effectiveRole}\` skill loaded` : (effectiveRole ? " · 📜 No skill configured" : "");
499
+ greetingBlock = `\n\n[👤 AGENT IDENTITY]\n${namePart}${rolePart}${skillPart}`;
500
+ }
474
501
  return {
475
502
  content: [{
476
503
  type: "text",
477
- text: `📋 Session context for "${project}" (${level}):\n\n${formattedContext.trim()}${driftReport}${briefingBlock}${visualMemoryBlock}${versionNote}`,
504
+ text: `📋 Session context for "${project}" (${level}):\n\n${formattedContext.trim()}${driftReport}${briefingBlock}${greetingBlock}${visualMemoryBlock}${skillBlock}${versionNote}`,
478
505
  }],
479
506
  isError: false,
480
507
  };
package/package.json CHANGED
@@ -1,13 +1,13 @@
1
1
  {
2
2
  "name": "prism-mcp-server",
3
- "version": "3.0.0",
3
+ "version": "3.0.1",
4
4
  "mcpName": "io.github.dcostenco/prism-mcp",
5
5
  "description": "The Mind Palace for AI Agents — local-first MCP server with persistent memory (SQLite/Supabase), visual dashboard, time travel, multi-agent sync, Morning Briefings, reality drift detection, code mode templates, semantic vector search, and Brave Search + Gemini analysis. Zero-config local mode.",
6
6
  "module": "index.ts",
7
7
  "type": "module",
8
8
  "main": "dist/server.js",
9
9
  "bin": {
10
- "prism-mcp-server": "./dist/server.js"
10
+ "prism-mcp-server": "dist/server.js"
11
11
  },
12
12
  "files": [
13
13
  "dist"
@@ -68,7 +68,7 @@
68
68
  "homepage": "https://github.com/dcostenco/prism-mcp",
69
69
  "repository": {
70
70
  "type": "git",
71
- "url": "https://github.com/dcostenco/prism-mcp.git"
71
+ "url": "git+https://github.com/dcostenco/prism-mcp.git"
72
72
  },
73
73
  "author": "Dmitri Costenco",
74
74
  "license": "MIT",