@phren/cli 0.0.10 → 0.0.12

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 (100) hide show
  1. package/README.md +11 -17
  2. package/mcp/dist/capabilities/cli.js +1 -1
  3. package/mcp/dist/capabilities/mcp.js +1 -1
  4. package/mcp/dist/capabilities/vscode.js +1 -1
  5. package/mcp/dist/capabilities/web-ui.js +1 -1
  6. package/mcp/dist/cli-actions.js +58 -71
  7. package/mcp/dist/cli-config.js +337 -131
  8. package/mcp/dist/cli-extract.js +3 -2
  9. package/mcp/dist/cli-govern.js +35 -63
  10. package/mcp/dist/cli-graph.js +19 -4
  11. package/mcp/dist/cli-hooks-globs.js +2 -1
  12. package/mcp/dist/cli-hooks-output.js +4 -4
  13. package/mcp/dist/cli-hooks-session.js +1 -1
  14. package/mcp/dist/cli-hooks.js +44 -35
  15. package/mcp/dist/cli-namespaces.js +15 -5
  16. package/mcp/dist/cli-search.js +2 -2
  17. package/mcp/dist/cli.js +1 -1
  18. package/mcp/dist/content-archive.js +23 -14
  19. package/mcp/dist/content-citation.js +13 -2
  20. package/mcp/dist/content-dedup.js +9 -9
  21. package/mcp/dist/content-learning.js +6 -4
  22. package/mcp/dist/content-metadata.js +10 -0
  23. package/mcp/dist/core-finding.js +1 -1
  24. package/mcp/dist/data-access.js +10 -31
  25. package/mcp/dist/data-tasks.js +5 -26
  26. package/mcp/dist/embedding.js +7 -8
  27. package/mcp/dist/entrypoint.js +133 -102
  28. package/mcp/dist/finding-impact.js +1 -32
  29. package/mcp/dist/finding-journal.js +1 -1
  30. package/mcp/dist/finding-lifecycle.js +2 -7
  31. package/mcp/dist/governance-locks.js +12 -5
  32. package/mcp/dist/governance-policy.js +156 -9
  33. package/mcp/dist/governance-scores.js +4 -10
  34. package/mcp/dist/hooks.js +62 -18
  35. package/mcp/dist/index.js +4 -4
  36. package/mcp/dist/init-config.js +4 -25
  37. package/mcp/dist/init-preferences.js +1 -1
  38. package/mcp/dist/init-setup.js +6 -55
  39. package/mcp/dist/init-shared.js +53 -1
  40. package/mcp/dist/init.js +191 -29
  41. package/mcp/dist/link-checksums.js +3 -2
  42. package/mcp/dist/link-context.js +2 -2
  43. package/mcp/dist/link-doctor.js +14 -57
  44. package/mcp/dist/link-skills.js +98 -12
  45. package/mcp/dist/link.js +16 -75
  46. package/mcp/dist/machine-identity.js +1 -9
  47. package/mcp/dist/mcp-config.js +247 -42
  48. package/mcp/dist/mcp-data.js +9 -9
  49. package/mcp/dist/mcp-extract-facts.js +12 -7
  50. package/mcp/dist/mcp-extract.js +2 -2
  51. package/mcp/dist/mcp-finding.js +16 -20
  52. package/mcp/dist/mcp-graph.js +12 -12
  53. package/mcp/dist/mcp-hooks.js +1 -1
  54. package/mcp/dist/mcp-ops.js +18 -18
  55. package/mcp/dist/mcp-search.js +11 -16
  56. package/mcp/dist/mcp-session.js +12 -2
  57. package/mcp/dist/memory-ui-assets.js +1 -36
  58. package/mcp/dist/memory-ui-graph.js +152 -50
  59. package/mcp/dist/memory-ui-page.js +30 -5
  60. package/mcp/dist/memory-ui-scripts.js +252 -63
  61. package/mcp/dist/memory-ui-server.js +115 -3
  62. package/mcp/dist/phren-core.js +2 -0
  63. package/mcp/dist/phren-paths.js +8 -9
  64. package/mcp/dist/proactivity.js +5 -5
  65. package/mcp/dist/profile-store.js +2 -2
  66. package/mcp/dist/project-config.js +64 -17
  67. package/mcp/dist/provider-adapters.js +1 -1
  68. package/mcp/dist/query-correlation.js +22 -19
  69. package/mcp/dist/session-checkpoints.js +14 -14
  70. package/mcp/dist/session-utils.js +3 -2
  71. package/mcp/dist/shared-data-utils.js +28 -0
  72. package/mcp/dist/shared-fragment-graph.js +22 -21
  73. package/mcp/dist/shared-governance.js +1 -1
  74. package/mcp/dist/shared-index.js +144 -105
  75. package/mcp/dist/shared-retrieval.js +21 -23
  76. package/mcp/dist/shared-search-fallback.js +15 -25
  77. package/mcp/dist/shared-sqljs.js +3 -2
  78. package/mcp/dist/shared.js +5 -6
  79. package/mcp/dist/shell-entry.js +1 -1
  80. package/mcp/dist/shell-input.js +63 -53
  81. package/mcp/dist/shell-palette.js +6 -1
  82. package/mcp/dist/shell-render.js +9 -5
  83. package/mcp/dist/shell-state-store.js +2 -5
  84. package/mcp/dist/shell-view.js +7 -6
  85. package/mcp/dist/shell.js +5 -55
  86. package/mcp/dist/skill-files.js +4 -10
  87. package/mcp/dist/skill-registry.js +3 -0
  88. package/mcp/dist/status.js +43 -21
  89. package/mcp/dist/task-hygiene.js +1 -1
  90. package/mcp/dist/telemetry.js +5 -4
  91. package/mcp/dist/update.js +1 -1
  92. package/mcp/dist/utils.js +4 -4
  93. package/package.json +2 -3
  94. package/skills/docs.md +11 -11
  95. package/starter/README.md +1 -1
  96. package/starter/global/CLAUDE.md +2 -2
  97. package/starter/global/skills/audit.md +106 -0
  98. package/mcp/dist/cli-hooks-retrieval.js +0 -2
  99. package/mcp/dist/impact-scoring.js +0 -22
  100. package/mcp/dist/shared-paths.js +0 -1
@@ -27,11 +27,20 @@ export function renderGraphScript() {
27
27
  /* ── colour & size maps ─────────────────────────────────────────────── */
28
28
  /* Fixed colors for non-finding node types — phren purple/cyan palette */
29
29
  var COLORS = {
30
- project: '#7B68AE',
31
- 'task-active': '#10b981', 'task-queue': '#5B4B8A',
32
- entity: '#3a7bae', reference: '#6b8e7a',
30
+ project: '#9B7FD4',
31
+ 'task-active': '#10b981', 'task-queue': '#7B68AE',
32
+ entity: '#D4892E', reference: '#6b8e7a',
33
33
  other: '#B8A0D6'
34
34
  };
35
+
36
+ /* Finding topic slug -> semantic color for visual hierarchy */
37
+ var FINDING_TOPIC_COLORS = {
38
+ architecture: '#3B82F6', debugging: '#EF4444', security: '#EF4444',
39
+ performance: '#F59E0B', testing: '#10B981', devops: '#10B981',
40
+ tooling: '#3B82F6', api: '#3B82F6', database: '#3B82F6',
41
+ frontend: '#8B5CF6', auth: '#EF4444', data: '#6B7280',
42
+ mobile: '#06B6D4', ai_ml: '#8B5CF6', general: '#6B7280'
43
+ };
35
44
  var RADII = {
36
45
  project: 18,
37
46
  'task-active': 7, 'task-queue': 7,
@@ -53,7 +62,8 @@ export function renderGraphScript() {
53
62
  waypointIdx: 0, /* current waypoint index */
54
63
  tripDist: 0, /* total distance of current trip (for ease-in-out) */
55
64
  tripProgress: 0, /* distance traveled so far this trip */
56
- targetNodeId: null /* id of destination node (set in phrenMoveTo) */
65
+ targetNodeId: null, /* id of destination node (set in phrenMoveTo) */
66
+ targetNodeRef: null /* live reference to destination node object */
57
67
  };
58
68
 
59
69
  /* ── phren sprite image ─────────────────────────────────────────────── */
@@ -92,6 +102,14 @@ export function renderGraphScript() {
92
102
  }
93
103
 
94
104
  /* find shortest edge path from nearest node to target node via BFS */
105
+ /* look up a visible node by id — used to resolve live positions */
106
+ function phrenNodeById(id) {
107
+ for (var i = 0; i < visibleNodes.length; i++) {
108
+ if (visibleNodes[i].id === id) return visibleNodes[i];
109
+ }
110
+ return null;
111
+ }
112
+
95
113
  function phrenFindEdgePath(targetNode) {
96
114
  if (!targetNode || visibleNodes.length === 0) return [];
97
115
  /* find nearest visible node to phren's current position */
@@ -134,14 +152,11 @@ export function renderGraphScript() {
134
152
  }
135
153
  }
136
154
  if (!found) return [];
137
- /* reconstruct path */
155
+ /* reconstruct path — store node IDs so positions are resolved live */
138
156
  var path = [];
139
- var nodeById = {};
140
- for (var i = 0; i < visibleNodes.length; i++) nodeById[visibleNodes[i].id] = visibleNodes[i];
141
157
  var cur = targetId;
142
158
  while (cur !== undefined && cur !== startId) {
143
- var nd = nodeById[cur];
144
- if (nd) path.unshift({ x: nd.x, y: nd.y });
159
+ path.unshift(cur);
145
160
  cur = prev[cur];
146
161
  }
147
162
  /* cap path length to avoid very long walks */
@@ -160,9 +175,10 @@ export function renderGraphScript() {
160
175
  phren.tripDist = Math.sqrt(tdx * tdx + tdy * tdy);
161
176
  phren.tripProgress = 0;
162
177
  /* try edge-walking if a target node is given */
163
- phren.waypoints = targetNode ? phrenFindEdgePath(targetNode) : [];
178
+ phren.waypoints = targetNode ? phrenFindEdgePath(targetNode) : []; /* array of node IDs */
164
179
  phren.waypointIdx = 0;
165
180
  phren.targetNodeId = targetNode ? targetNode.id : null;
181
+ phren.targetNodeRef = targetNode || null; /* live reference for position tracking */
166
182
  /* ensure animation loop is running so phren movement renders */
167
183
  if (!animFrame) animFrame = requestAnimationFrame(tick);
168
184
  }
@@ -183,11 +199,18 @@ export function renderGraphScript() {
183
199
  function phrenUpdate(dt) {
184
200
  phren.idlePhase += dt;
185
201
  if (phren.moving) {
202
+ /* keep target coordinates synced with live node position (force layout moves nodes) */
203
+ if (phren.targetNodeRef && typeof phren.targetNodeRef.x === 'number') {
204
+ phren.targetX = phren.targetNodeRef.x;
205
+ phren.targetY = phren.targetNodeRef.y;
206
+ }
186
207
  /* determine current move target: next waypoint or final destination */
187
208
  var wx, wy;
188
209
  if (phren.waypoints.length > 0 && phren.waypointIdx < phren.waypoints.length) {
189
- wx = phren.waypoints[phren.waypointIdx].x;
190
- wy = phren.waypoints[phren.waypointIdx].y;
210
+ /* waypoints are node IDs — resolve to live positions */
211
+ var wpNode = phrenNodeById(phren.waypoints[phren.waypointIdx]);
212
+ if (wpNode) { wx = wpNode.x; wy = wpNode.y; }
213
+ else { wx = phren.targetX; wy = phren.targetY; phren.waypointIdx = phren.waypoints.length; }
191
214
  } else {
192
215
  wx = phren.targetX;
193
216
  wy = phren.targetY;
@@ -333,9 +356,11 @@ export function renderGraphScript() {
333
356
  }
334
357
 
335
358
  function topicGroupColor(group) {
336
- /* group is 'topic:<slug>' for findings */
359
+ /* group is 'topic:<slug>' for findings — use semantic color if available */
337
360
  if (typeof group === 'string' && group.indexOf('topic:') === 0) {
338
- return topicSlugToColor(group.slice(6));
361
+ var slug = group.slice(6);
362
+ if (FINDING_TOPIC_COLORS[slug]) return FINDING_TOPIC_COLORS[slug];
363
+ return topicSlugToColor(slug);
339
364
  }
340
365
  return COLORS[group] || COLORS.other;
341
366
  }
@@ -371,9 +396,13 @@ export function renderGraphScript() {
371
396
  function clamp(v, lo, hi) { return v < lo ? lo : v > hi ? hi : v; }
372
397
 
373
398
  function nodeRadius(n) {
374
- if (n.group === 'entity') return Math.min(6 + (n.refCount || 0), 16);
375
- if (typeof n.group === 'string' && n.group.indexOf('topic:') === 0) return RADII.other;
376
- return RADII[n.group] || RADII.other;
399
+ var base;
400
+ if (n.group === 'entity') base = Math.min(6 + (n.refCount || 0), 16);
401
+ else if (typeof n.group === 'string' && n.group.indexOf('topic:') === 0) base = RADII.other;
402
+ else base = RADII[n.group] || RADII.other;
403
+ /* size boost by connection count */
404
+ var conns = n._connectionCount || 0;
405
+ return base + Math.min(conns * 0.5, 6);
377
406
  }
378
407
 
379
408
  function nodeColor(n) { return topicGroupColor(n.group); }
@@ -865,6 +894,14 @@ export function renderGraphScript() {
865
894
  }
866
895
  }
867
896
 
897
+ /* compute connection count per node for size scaling */
898
+ for (var i = 0; i < visibleNodes.length; i++) visibleNodes[i]._connectionCount = 0;
899
+ for (var i = 0; i < visibleLinks.length; i++) {
900
+ var lk = visibleLinks[i];
901
+ if (lk._source) lk._source._connectionCount = (lk._source._connectionCount || 0) + 1;
902
+ if (lk._target) lk._target._connectionCount = (lk._target._connectionCount || 0) + 1;
903
+ }
904
+
868
905
  updateLegend();
869
906
  /* only restart simulation if node count changed significantly */
870
907
  var oldCount = _prevVisibleCount || 0;
@@ -940,14 +977,26 @@ export function renderGraphScript() {
940
977
  var sMatch = ((lk._source.label || '').toLowerCase().indexOf(_sq) !== -1 || (lk._source.fullLabel || '').toLowerCase().indexOf(_sq) !== -1);
941
978
  var tMatch = ((lk._target.label || '').toLowerCase().indexOf(_sq) !== -1 || (lk._target.fullLabel || '').toLowerCase().indexOf(_sq) !== -1);
942
979
  ctx.beginPath();
943
- ctx.strokeStyle = (sMatch || tMatch) ? 'rgba(150,150,150,0.11)' : 'rgba(150,150,150,0.035)';
980
+ ctx.strokeStyle = (sMatch || tMatch) ? 'rgba(255,255,255,0.20)' : 'rgba(255,255,255,0.04)';
981
+ ctx.moveTo(lk._source.x, lk._source.y);
982
+ ctx.lineTo(lk._target.x, lk._target.y);
983
+ ctx.stroke();
984
+ }
985
+ } else if (selectedNode) {
986
+ /* When a node is selected, highlight connected edges */
987
+ for (var i = 0; i < links.length; i++) {
988
+ var lk = links[i];
989
+ if (!lk._source || !lk._target) continue;
990
+ var isConnected = (lk._source === selectedNode || lk._target === selectedNode);
991
+ ctx.beginPath();
992
+ ctx.strokeStyle = isConnected ? 'rgba(255,255,255,0.25)' : 'rgba(255,255,255,0.08)';
944
993
  ctx.moveTo(lk._source.x, lk._source.y);
945
994
  ctx.lineTo(lk._target.x, lk._target.y);
946
995
  ctx.stroke();
947
996
  }
948
997
  } else {
949
998
  ctx.beginPath();
950
- ctx.strokeStyle = 'rgba(150,150,150,0.10)';
999
+ ctx.strokeStyle = 'rgba(255,255,255,0.08)';
951
1000
  for (var i = 0; i < links.length; i++) {
952
1001
  var lk = links[i];
953
1002
  if (!lk._source || !lk._target) continue;
@@ -1052,35 +1101,69 @@ export function renderGraphScript() {
1052
1101
  ctx.setLineDash([]);
1053
1102
  }
1054
1103
 
1055
- /* 3. nodes (batch by fill colour, apply opacity) */
1056
- var fillBuckets = {};
1104
+ /* 3. nodes with glow, selection dimming, and visual hierarchy */
1105
+ /* Build set of nodes connected to selected node for dimming */
1106
+ var _connectedToSelected = null;
1107
+ if (selectedNode) {
1108
+ _connectedToSelected = {};
1109
+ _connectedToSelected[selectedNode.id] = true;
1110
+ for (var i = 0; i < links.length; i++) {
1111
+ var lk = links[i];
1112
+ if (!lk._source || !lk._target) continue;
1113
+ if (lk._source === selectedNode) _connectedToSelected[lk._target.id] = true;
1114
+ if (lk._target === selectedNode) _connectedToSelected[lk._source.id] = true;
1115
+ }
1116
+ }
1117
+
1057
1118
  for (var i = 0; i < nodes.length; i++) {
1058
- var c = nodeColor(nodes[i]);
1059
- if (!fillBuckets[c]) fillBuckets[c] = [];
1060
- fillBuckets[c].push(nodes[i]);
1061
- }
1062
- var fillColors = Object.keys(fillBuckets);
1063
- for (var ci = 0; ci < fillColors.length; ci++) {
1064
- var col = fillColors[ci];
1065
- var bucket = fillBuckets[col];
1066
- for (var i = 0; i < bucket.length; i++) {
1067
- var nd = bucket[i];
1068
- var mult = qualityMultiplier(nd);
1069
- var opacity = 0.3 + (mult - 0.2) * (0.7 / 1.3);
1070
- opacity = clamp(opacity, 0.3, 1.0);
1071
- /* dim non-matching nodes when search is active */
1072
- if (searchQuery) {
1073
- var sq = searchQuery.toLowerCase();
1074
- var sl = (nd.label || '').toLowerCase();
1075
- var sf = (nd.fullLabel || '').toLowerCase();
1076
- if (sl.indexOf(sq) === -1 && sf.indexOf(sq) === -1) opacity = 0.1;
1077
- }
1078
- ctx.globalAlpha = opacity;
1079
- ctx.beginPath();
1080
- ctx.arc(nd.x, nd.y, nodeRadius(nd), 0, Math.PI * 2);
1081
- ctx.fillStyle = col;
1082
- ctx.fill();
1119
+ var nd = nodes[i];
1120
+ var col = nodeColor(nd);
1121
+ var r = nodeRadius(nd);
1122
+ var mult = qualityMultiplier(nd);
1123
+ var opacity = 0.3 + (mult - 0.2) * (0.7 / 1.3);
1124
+ opacity = clamp(opacity, 0.3, 1.0);
1125
+
1126
+ /* dim non-matching nodes when search is active */
1127
+ if (searchQuery) {
1128
+ var sq = searchQuery.toLowerCase();
1129
+ var sl = (nd.label || '').toLowerCase();
1130
+ var sf = (nd.fullLabel || '').toLowerCase();
1131
+ if (sl.indexOf(sq) === -1 && sf.indexOf(sq) === -1) opacity = 0.1;
1132
+ }
1133
+
1134
+ /* dim non-connected nodes when a node is selected */
1135
+ if (_connectedToSelected && !_connectedToSelected[nd.id]) {
1136
+ opacity *= 0.4;
1137
+ }
1138
+
1139
+ ctx.globalAlpha = opacity;
1140
+
1141
+ /* node glow based on type */
1142
+ var glowBlur = 0;
1143
+ if (nd === selectedNode) {
1144
+ glowBlur = 30;
1145
+ ctx.shadowColor = 'rgba(255,255,255,0.6)';
1146
+ } else if (nd.group === 'project') {
1147
+ glowBlur = 20;
1148
+ ctx.shadowColor = colorWithAlpha(col, 0.5);
1149
+ } else if (nd.group === 'entity') {
1150
+ glowBlur = 12;
1151
+ ctx.shadowColor = 'rgba(212,137,46,0.4)';
1152
+ } else if (typeof nd.group === 'string' && nd.group.indexOf('topic:') === 0) {
1153
+ glowBlur = 6;
1154
+ ctx.shadowColor = colorWithAlpha(col, 0.25);
1083
1155
  }
1156
+ if (glowBlur > 0) {
1157
+ ctx.shadowBlur = glowBlur / scale;
1158
+ }
1159
+
1160
+ ctx.beginPath();
1161
+ ctx.arc(nd.x, nd.y, r, 0, Math.PI * 2);
1162
+ ctx.fillStyle = col;
1163
+ ctx.fill();
1164
+
1165
+ ctx.shadowBlur = 0;
1166
+ ctx.shadowColor = 'transparent';
1084
1167
  }
1085
1168
  ctx.globalAlpha = 1.0;
1086
1169
 
@@ -1155,13 +1238,17 @@ export function renderGraphScript() {
1155
1238
  }
1156
1239
  }
1157
1240
 
1158
- /* 6. selection highlight */
1241
+ /* 6. selection highlight with bloom */
1159
1242
  if (selectedNode) {
1243
+ ctx.save();
1244
+ ctx.shadowColor = 'rgba(255,255,255,0.5)';
1245
+ ctx.shadowBlur = 30 / scale;
1160
1246
  ctx.beginPath();
1161
1247
  ctx.arc(selectedNode.x, selectedNode.y, nodeRadius(selectedNode) + 5, 0, Math.PI * 2);
1162
- ctx.strokeStyle = '#7B68AE';
1248
+ ctx.strokeStyle = 'rgba(255,255,255,0.7)';
1163
1249
  ctx.lineWidth = 2.5 / scale;
1164
1250
  ctx.stroke();
1251
+ ctx.restore();
1165
1252
  }
1166
1253
 
1167
1254
  /* 6c. phren character */
@@ -1407,17 +1494,22 @@ export function renderGraphScript() {
1407
1494
 
1408
1495
  /* ── interaction ────────────────────────────────────────────────────── */
1409
1496
  function setupInteraction() {
1497
+ var _mouseDownX = 0, _mouseDownY = 0, _mouseDownHit = null;
1498
+ var CLICK_THRESHOLD = 5; /* px — distinguish click from drag */
1499
+
1410
1500
  canvas.addEventListener('mousedown', function(e) {
1411
1501
  var rect = canvas.getBoundingClientRect();
1412
1502
  var mx = e.clientX - rect.left;
1413
1503
  var my = e.clientY - rect.top;
1504
+ _mouseDownX = mx;
1505
+ _mouseDownY = my;
1414
1506
  var hit = hitTest(mx, my);
1507
+ _mouseDownHit = hit;
1415
1508
  if (hit) {
1416
1509
  dragging = hit;
1417
1510
  dragPullDepths = collectConnectedDepths(hit, 2);
1418
1511
  dragOffX = (mx - panX) / scale - hit.x;
1419
1512
  dragOffY = (my - panY) / scale - hit.y;
1420
- renderGraphDetails(hit);
1421
1513
  } else {
1422
1514
  panning = true;
1423
1515
  panStartX = mx - panX;
@@ -1465,14 +1557,24 @@ export function renderGraphScript() {
1465
1557
  }
1466
1558
  });
1467
1559
 
1468
- canvas.addEventListener('mouseup', function() {
1560
+ canvas.addEventListener('mouseup', function(e) {
1561
+ var rect = canvas.getBoundingClientRect();
1562
+ var mx = e.clientX - rect.left;
1563
+ var my = e.clientY - rect.top;
1564
+ var dragDist = Math.sqrt((_mouseDownX - mx) * (_mouseDownX - mx) + (_mouseDownY - my) * (_mouseDownY - my));
1469
1565
  if (dragging) {
1566
+ var clickedNode = dragging;
1470
1567
  dragging = null;
1471
1568
  dragPullDepths = null;
1472
1569
  /* restart simulation so gravity pulls nodes back into place */
1473
1570
  alpha = Math.max(alpha, 0.3);
1571
+ /* only select + walk phren if this was a click, not a drag */
1572
+ if (dragDist < CLICK_THRESHOLD && _mouseDownHit && clickedNode === _mouseDownHit) {
1573
+ renderGraphDetails(_mouseDownHit);
1574
+ }
1474
1575
  }
1475
1576
  panning = false;
1577
+ _mouseDownHit = null;
1476
1578
  });
1477
1579
 
1478
1580
  canvas.addEventListener('mouseleave', function() {
@@ -2,7 +2,7 @@ import { WEB_UI_STYLES, renderWebUiScript } from "./memory-ui-assets.js";
2
2
  import { renderGraphScript } from "./memory-ui-graph.js";
3
3
  import { readSyncSnapshot } from "./memory-ui-data.js";
4
4
  import { PROJECT_REFERENCE_UI_STYLES, SETTINGS_TAB_UI_STYLES, TASK_UI_STYLES } from "./memory-ui-styles.js";
5
- import { renderSkillUiEnhancementScript, renderProjectReferenceEnhancementScript, renderReviewQueueEditSyncScript, renderTasksAndSettingsScript, renderSearchScript, renderEventWiringScript, } from "./memory-ui-scripts.js";
5
+ import { renderSharedWebUiHelpers, renderSkillUiEnhancementScript, renderProjectReferenceEnhancementScript, renderReviewQueueEditSyncScript, renderTasksAndSettingsScript, renderSearchScript, renderEventWiringScript, } from "./memory-ui-scripts.js";
6
6
  function h(s) {
7
7
  return s
8
8
  .replace(/&/g, "&amp;")
@@ -21,7 +21,6 @@ export function renderWebUiPage(phrenPath, authToken, nonce) {
21
21
  <link rel="preconnect" href="https://fonts.bunny.net" />
22
22
  <link href="https://fonts.bunny.net/css?family=inter:400,500,600,700&display=swap" rel="stylesheet" />
23
23
  <title>phren</title>
24
- <script${nonceAttr} src="https://cdn.jsdelivr.net/npm/marked@12/marked.min.js"></script>
25
24
  <style>
26
25
  ${WEB_UI_STYLES}
27
26
  ${PROJECT_REFERENCE_UI_STYLES}
@@ -104,9 +103,9 @@ ${TASK_UI_STYLES}
104
103
  <dt>What is the Review Queue?</dt>
105
104
  <dd>Fragments flagged by governance for human review. Items accumulate here when <code>phren maintain govern</code> is run.</dd>
106
105
  <dt>Can I approve, reject, or edit items here?</dt>
107
- <dd>No. The web UI review queue is read-only and exists for inspection only.</dd>
106
+ <dd>Yes. Each review card has <strong>Approve</strong>, <strong>Reject</strong>, and <strong>Edit</strong> buttons. Approve accepts the fragment, Reject removes it, and Edit lets you revise the text before accepting. You can also use batch actions to approve or reject multiple items at once.</dd>
108
107
  <dt>How do I clear items?</dt>
109
- <dd>Use maintenance flows such as <code>phren maintain prune</code>, or update the underlying findings/tasks directly.</dd>
108
+ <dd>Approve or reject items directly in the UI, or use maintenance flows such as <code>phren maintain prune</code>.</dd>
110
109
  <dt>Is this automatic?</dt>
111
110
  <dd>No. Agents do not auto-accept review-queue items.</dd>
112
111
  <dt>How do items get here?</dt>
@@ -116,7 +115,7 @@ ${TASK_UI_STYLES}
116
115
  </dl>
117
116
  </details>
118
117
 
119
- <p style="font-size:var(--text-sm);color:var(--muted);margin-bottom:12px;letter-spacing:-0.01em">Fragments flagged for review. Inspect them here; the web UI does not mutate queue items.</p>
118
+ <p style="font-size:var(--text-sm);color:var(--muted);margin-bottom:12px;letter-spacing:-0.01em">Fragments flagged for review. Approve, reject, or edit items directly from this tab.</p>
120
119
 
121
120
  <div id="review-summary-banner" style="display:flex;gap:8px;flex-wrap:wrap;margin-bottom:12px;align-items:center"></div>
122
121
 
@@ -267,6 +266,17 @@ ${TASK_UI_STYLES}
267
266
  <div id="tab-settings" class="tab-content">
268
267
  <div class="settings-shell">
269
268
  <div id="settings-status-inline" class="settings-status-inline" aria-live="polite"></div>
269
+ <section class="settings-section" style="border-top:3px solid color-mix(in srgb, var(--cyan) 45%, var(--border))">
270
+ <div class="settings-section-header" style="display:flex;align-items:center;justify-content:space-between;gap:16px">
271
+ <span>Scope</span>
272
+ <select id="settings-project-select" style="border:1px solid var(--border);border-radius:var(--radius-sm);padding:6px 10px;background:var(--surface);color:var(--ink);font-size:var(--text-sm);font-family:var(--font)">
273
+ <option value="">Global (all projects)</option>
274
+ </select>
275
+ </div>
276
+ <div class="settings-section-body" style="padding:12px 18px">
277
+ <div id="settings-scope-note" style="font-size:var(--text-sm);color:var(--muted)">Showing global settings. Select a project to view and edit per-project overrides.</div>
278
+ </div>
279
+ </section>
270
280
  <section class="settings-section settings-section-findings">
271
281
  <div class="settings-section-header">Findings</div>
272
282
  <div class="settings-section-body">
@@ -279,6 +289,18 @@ ${TASK_UI_STYLES}
279
289
  <div id="settings-behavior" style="color:var(--muted)">Loading...</div>
280
290
  </div>
281
291
  </section>
292
+ <section class="settings-section" style="border-top:3px solid color-mix(in srgb, var(--warning) 45%, var(--border))">
293
+ <div class="settings-section-header">Retention Policy</div>
294
+ <div class="settings-section-body">
295
+ <div id="settings-retention" style="color:var(--muted)">Loading...</div>
296
+ </div>
297
+ </section>
298
+ <section class="settings-section" style="border-top:3px solid color-mix(in srgb, var(--success) 45%, var(--border))">
299
+ <div class="settings-section-header">Workflow Policy</div>
300
+ <div class="settings-section-body">
301
+ <div id="settings-workflow" style="color:var(--muted)">Loading...</div>
302
+ </div>
303
+ </section>
282
304
  <section class="settings-section settings-section-integrations">
283
305
  <div class="settings-section-header">Integrations</div>
284
306
  <div class="settings-section-body">
@@ -308,6 +330,9 @@ ${renderGraphScript()}
308
330
  ${renderReviewQueueEditSyncScript()}
309
331
  </script>
310
332
  <script${nonceAttr}>
333
+ ${renderSharedWebUiHelpers(h(authToken || ""))}
334
+ </script>
335
+ <script${nonceAttr}>
311
336
  ${renderSkillUiEnhancementScript(h(authToken || ""))}
312
337
  </script>
313
338
  <script${nonceAttr}>