reflectt-node 0.1.7 → 0.1.11

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 (220) hide show
  1. package/README.md +13 -0
  2. package/defaults/TEAM-ROLES.yaml +317 -5
  3. package/defaults/gitignore.template +23 -0
  4. package/dist/agent-config.d.ts +51 -0
  5. package/dist/agent-config.d.ts.map +1 -0
  6. package/dist/agent-config.js +129 -0
  7. package/dist/agent-config.js.map +1 -0
  8. package/dist/agent-config.test.d.ts +2 -0
  9. package/dist/agent-config.test.d.ts.map +1 -0
  10. package/dist/agent-config.test.js +91 -0
  11. package/dist/agent-config.test.js.map +1 -0
  12. package/dist/agent-memories.d.ts +58 -0
  13. package/dist/agent-memories.d.ts.map +1 -0
  14. package/dist/agent-memories.js +168 -0
  15. package/dist/agent-memories.js.map +1 -0
  16. package/dist/agent-memories.test.d.ts +2 -0
  17. package/dist/agent-memories.test.d.ts.map +1 -0
  18. package/dist/agent-memories.test.js +327 -0
  19. package/dist/agent-memories.test.js.map +1 -0
  20. package/dist/agent-messaging.d.ts +50 -0
  21. package/dist/agent-messaging.d.ts.map +1 -0
  22. package/dist/agent-messaging.js +103 -0
  23. package/dist/agent-messaging.js.map +1 -0
  24. package/dist/agent-messaging.test.d.ts +2 -0
  25. package/dist/agent-messaging.test.d.ts.map +1 -0
  26. package/dist/agent-messaging.test.js +105 -0
  27. package/dist/agent-messaging.test.js.map +1 -0
  28. package/dist/agent-runs.d.ts +158 -0
  29. package/dist/agent-runs.d.ts.map +1 -0
  30. package/dist/agent-runs.js +514 -0
  31. package/dist/agent-runs.js.map +1 -0
  32. package/dist/agent-runs.test.d.ts +2 -0
  33. package/dist/agent-runs.test.d.ts.map +1 -0
  34. package/dist/agent-runs.test.js +386 -0
  35. package/dist/agent-runs.test.js.map +1 -0
  36. package/dist/approval-queue.test.d.ts +2 -0
  37. package/dist/approval-queue.test.d.ts.map +1 -0
  38. package/dist/approval-queue.test.js +118 -0
  39. package/dist/approval-queue.test.js.map +1 -0
  40. package/dist/artifact-store.d.ts +55 -0
  41. package/dist/artifact-store.d.ts.map +1 -0
  42. package/dist/artifact-store.js +128 -0
  43. package/dist/artifact-store.js.map +1 -0
  44. package/dist/artifact-store.test.d.ts +2 -0
  45. package/dist/artifact-store.test.d.ts.map +1 -0
  46. package/dist/artifact-store.test.js +119 -0
  47. package/dist/artifact-store.test.js.map +1 -0
  48. package/dist/boardHealthWorker.d.ts +32 -0
  49. package/dist/boardHealthWorker.d.ts.map +1 -1
  50. package/dist/boardHealthWorker.js +69 -2
  51. package/dist/boardHealthWorker.js.map +1 -1
  52. package/dist/buildInfo.d.ts.map +1 -1
  53. package/dist/buildInfo.js +47 -10
  54. package/dist/buildInfo.js.map +1 -1
  55. package/dist/canvas-input.test.d.ts +2 -0
  56. package/dist/canvas-input.test.d.ts.map +1 -0
  57. package/dist/canvas-input.test.js +96 -0
  58. package/dist/canvas-input.test.js.map +1 -0
  59. package/dist/canvas-render.test.d.ts +2 -0
  60. package/dist/canvas-render.test.d.ts.map +1 -0
  61. package/dist/canvas-render.test.js +95 -0
  62. package/dist/canvas-render.test.js.map +1 -0
  63. package/dist/capabilities/browser.d.ts +75 -0
  64. package/dist/capabilities/browser.d.ts.map +1 -0
  65. package/dist/capabilities/browser.js +172 -0
  66. package/dist/capabilities/browser.js.map +1 -0
  67. package/dist/channels.d.ts +1 -1
  68. package/dist/chat.d.ts +4 -0
  69. package/dist/chat.d.ts.map +1 -1
  70. package/dist/chat.js +6 -2
  71. package/dist/chat.js.map +1 -1
  72. package/dist/cli.js +41 -14
  73. package/dist/cli.js.map +1 -1
  74. package/dist/cloud.d.ts +2 -0
  75. package/dist/cloud.d.ts.map +1 -1
  76. package/dist/cloud.js +151 -64
  77. package/dist/cloud.js.map +1 -1
  78. package/dist/continuity-loop.d.ts.map +1 -1
  79. package/dist/continuity-loop.js +297 -29
  80. package/dist/continuity-loop.js.map +1 -1
  81. package/dist/cost-enforcement.d.ts +38 -0
  82. package/dist/cost-enforcement.d.ts.map +1 -0
  83. package/dist/cost-enforcement.js +84 -0
  84. package/dist/cost-enforcement.js.map +1 -0
  85. package/dist/db.d.ts.map +1 -1
  86. package/dist/db.js +131 -0
  87. package/dist/db.js.map +1 -1
  88. package/dist/deploy-monitor.d.ts +18 -0
  89. package/dist/deploy-monitor.d.ts.map +1 -0
  90. package/dist/deploy-monitor.js +165 -0
  91. package/dist/deploy-monitor.js.map +1 -0
  92. package/dist/e2e-loop-proof.test.d.ts +2 -0
  93. package/dist/e2e-loop-proof.test.d.ts.map +1 -0
  94. package/dist/e2e-loop-proof.test.js +104 -0
  95. package/dist/e2e-loop-proof.test.js.map +1 -0
  96. package/dist/email-sms-send.test.d.ts +2 -0
  97. package/dist/email-sms-send.test.d.ts.map +1 -0
  98. package/dist/email-sms-send.test.js +96 -0
  99. package/dist/email-sms-send.test.js.map +1 -0
  100. package/dist/events.d.ts +1 -1
  101. package/dist/events.d.ts.map +1 -1
  102. package/dist/events.js +2 -0
  103. package/dist/events.js.map +1 -1
  104. package/dist/executionSweeper.d.ts +1 -0
  105. package/dist/executionSweeper.d.ts.map +1 -1
  106. package/dist/executionSweeper.js +43 -7
  107. package/dist/executionSweeper.js.map +1 -1
  108. package/dist/files.d.ts.map +1 -1
  109. package/dist/files.js +17 -3
  110. package/dist/files.js.map +1 -1
  111. package/dist/fingerprint.d.ts +30 -0
  112. package/dist/fingerprint.d.ts.map +1 -0
  113. package/dist/fingerprint.js +117 -0
  114. package/dist/fingerprint.js.map +1 -0
  115. package/dist/github-webhook-attribution.d.ts +38 -0
  116. package/dist/github-webhook-attribution.d.ts.map +1 -0
  117. package/dist/github-webhook-attribution.js +123 -0
  118. package/dist/github-webhook-attribution.js.map +1 -0
  119. package/dist/github-webhook-chat.d.ts +75 -0
  120. package/dist/github-webhook-chat.d.ts.map +1 -0
  121. package/dist/github-webhook-chat.js +108 -0
  122. package/dist/github-webhook-chat.js.map +1 -0
  123. package/dist/handoff-state.test.d.ts +2 -0
  124. package/dist/handoff-state.test.d.ts.map +1 -0
  125. package/dist/handoff-state.test.js +102 -0
  126. package/dist/handoff-state.test.js.map +1 -0
  127. package/dist/health.d.ts +9 -0
  128. package/dist/health.d.ts.map +1 -1
  129. package/dist/health.js +18 -0
  130. package/dist/health.js.map +1 -1
  131. package/dist/host-error-correlation.d.ts +65 -0
  132. package/dist/host-error-correlation.d.ts.map +1 -0
  133. package/dist/host-error-correlation.js +123 -0
  134. package/dist/host-error-correlation.js.map +1 -0
  135. package/dist/inbox.d.ts.map +1 -1
  136. package/dist/inbox.js +4 -0
  137. package/dist/inbox.js.map +1 -1
  138. package/dist/index.js +76 -11
  139. package/dist/index.js.map +1 -1
  140. package/dist/notificationDedupeGuard.d.ts +4 -0
  141. package/dist/notificationDedupeGuard.d.ts.map +1 -1
  142. package/dist/notificationDedupeGuard.js +8 -4
  143. package/dist/notificationDedupeGuard.js.map +1 -1
  144. package/dist/presence.d.ts +37 -5
  145. package/dist/presence.d.ts.map +1 -1
  146. package/dist/presence.js +127 -16
  147. package/dist/presence.js.map +1 -1
  148. package/dist/pulse.d.ts +7 -0
  149. package/dist/pulse.d.ts.map +1 -1
  150. package/dist/pulse.js +15 -0
  151. package/dist/pulse.js.map +1 -1
  152. package/dist/review-sla.d.ts +9 -0
  153. package/dist/review-sla.d.ts.map +1 -0
  154. package/dist/review-sla.js +51 -0
  155. package/dist/review-sla.js.map +1 -0
  156. package/dist/review-state.d.ts +9 -0
  157. package/dist/review-state.d.ts.map +1 -0
  158. package/dist/review-state.js +17 -0
  159. package/dist/review-state.js.map +1 -0
  160. package/dist/routing-enforcement.test.d.ts +2 -0
  161. package/dist/routing-enforcement.test.d.ts.map +1 -0
  162. package/dist/routing-enforcement.test.js +86 -0
  163. package/dist/routing-enforcement.test.js.map +1 -0
  164. package/dist/run-retention.test.d.ts +2 -0
  165. package/dist/run-retention.test.d.ts.map +1 -0
  166. package/dist/run-retention.test.js +57 -0
  167. package/dist/run-retention.test.js.map +1 -0
  168. package/dist/run-stream.test.d.ts +2 -0
  169. package/dist/run-stream.test.d.ts.map +1 -0
  170. package/dist/run-stream.test.js +70 -0
  171. package/dist/run-stream.test.js.map +1 -0
  172. package/dist/schedule.d.ts +60 -0
  173. package/dist/schedule.d.ts.map +1 -0
  174. package/dist/schedule.js +176 -0
  175. package/dist/schedule.js.map +1 -0
  176. package/dist/server.d.ts.map +1 -1
  177. package/dist/server.js +1714 -88
  178. package/dist/server.js.map +1 -1
  179. package/dist/suppression-ledger.d.ts.map +1 -1
  180. package/dist/suppression-ledger.js +12 -3
  181. package/dist/suppression-ledger.js.map +1 -1
  182. package/dist/system-loop-state.d.ts +1 -1
  183. package/dist/system-loop-state.d.ts.map +1 -1
  184. package/dist/system-loop-state.js +1 -0
  185. package/dist/system-loop-state.js.map +1 -1
  186. package/dist/tasks.d.ts +9 -1
  187. package/dist/tasks.d.ts.map +1 -1
  188. package/dist/tasks.js +238 -41
  189. package/dist/tasks.js.map +1 -1
  190. package/dist/todoHoardingGuard.d.ts +17 -0
  191. package/dist/todoHoardingGuard.d.ts.map +1 -1
  192. package/dist/todoHoardingGuard.js +25 -2
  193. package/dist/todoHoardingGuard.js.map +1 -1
  194. package/dist/types.d.ts +1 -1
  195. package/dist/types.d.ts.map +1 -1
  196. package/dist/usage-tracking.d.ts +26 -0
  197. package/dist/usage-tracking.d.ts.map +1 -1
  198. package/dist/usage-tracking.js +91 -4
  199. package/dist/usage-tracking.js.map +1 -1
  200. package/dist/webhook-storage.d.ts +50 -0
  201. package/dist/webhook-storage.d.ts.map +1 -0
  202. package/dist/webhook-storage.js +102 -0
  203. package/dist/webhook-storage.js.map +1 -0
  204. package/dist/webhook-storage.test.d.ts +2 -0
  205. package/dist/webhook-storage.test.d.ts.map +1 -0
  206. package/dist/webhook-storage.test.js +86 -0
  207. package/dist/webhook-storage.test.js.map +1 -0
  208. package/dist/workflow-templates.d.ts +44 -0
  209. package/dist/workflow-templates.d.ts.map +1 -0
  210. package/dist/workflow-templates.js +154 -0
  211. package/dist/workflow-templates.js.map +1 -0
  212. package/dist/workflow-templates.test.d.ts +2 -0
  213. package/dist/workflow-templates.test.d.ts.map +1 -0
  214. package/dist/workflow-templates.test.js +76 -0
  215. package/dist/workflow-templates.test.js.map +1 -0
  216. package/package.json +3 -1
  217. package/public/dashboard.js +130 -37
  218. package/public/design-tokens-platform.md +118 -0
  219. package/public/design-tokens.css +195 -0
  220. package/public/docs.md +145 -2
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "reflectt-node",
3
- "version": "0.1.7",
3
+ "version": "0.1.11",
4
4
  "description": "Coordinate your AI agent team. Shared tasks, memory, reflections, and presence. Self-host for free.",
5
5
  "main": "dist/index.js",
6
6
  "type": "module",
@@ -44,6 +44,7 @@
44
44
  "author": "Team Reflectt",
45
45
  "license": "Apache-2.0",
46
46
  "dependencies": {
47
+ "@browserbasehq/stagehand": "^3.1.0",
47
48
  "@fastify/cors": "^10.0.1",
48
49
  "@fastify/multipart": "^9.4.0",
49
50
  "@fastify/websocket": "^11.0.1",
@@ -87,6 +88,7 @@
87
88
  "README.md"
88
89
  ],
89
90
  "optionalDependencies": {
91
+ "@browserbasehq/stagehand": "^3.1.0",
90
92
  "@xenova/transformers": "^2.17.2"
91
93
  },
92
94
  "overrides": {
@@ -726,9 +726,10 @@ async function loadPresence() {
726
726
  // ---- Tasks ----
727
727
  async function loadTasks(forceFull = false) {
728
728
  try {
729
- const useDelta = !forceFull && lastTaskSync > 0;
729
+ // Force full load if we have no tasks yet (e.g. first load failed)
730
+ const useDelta = !forceFull && lastTaskSync > 0 && allTasks.length > 0;
730
731
  const qs = new URLSearchParams();
731
- qs.set('limit', '80');
732
+ qs.set('limit', '200');
732
733
  if (useDelta) qs.set('updatedSince', String(lastTaskSync));
733
734
 
734
735
  const r = await fetch(BASE + '/tasks?' + qs.toString());
@@ -1384,16 +1385,8 @@ async function loadSharedArtifacts() {
1384
1385
  const pinnedRows = pinned.map(p => {
1385
1386
  const found = files.find(f => (String(f.name || '')).toLowerCase() === p.name.toLowerCase());
1386
1387
  if (!found) {
1387
- return `<div style="padding:10px 12px;border-bottom:1px solid var(--border-subtle)">
1388
- <div style="display:flex;align-items:center;justify-content:space-between;gap:10px">
1389
- <div style="font-size:13px;color:var(--text-bright);font-weight:600">${esc(p.label)}</div>
1390
- <span class="assignee-tag" style="color:var(--yellow)">missing</span>
1391
- </div>
1392
- <div style="font-size:11px;color:var(--text-muted);margin-top:4px">
1393
- To make this available here, create a symlink in the shared workspace:<br/>
1394
- <code>ln -s ../RYANS-THOUGHTS.md ~/.openclaw/workspace-shared/process/RYANS-THOUGHTS.md</code>
1395
- </div>
1396
- </div>`;
1388
+ // Operator notes are optional. If not configured, do not show an operator-only instruction to end users.
1389
+ return '';
1397
1390
  }
1398
1391
  const href = '/shared/view?path=' + encodeURIComponent(found.path);
1399
1392
  return `<div style="padding:10px 12px;border-bottom:1px solid var(--border-subtle)">
@@ -1422,9 +1415,13 @@ async function loadSharedArtifacts() {
1422
1415
  </div>`;
1423
1416
  }).join('');
1424
1417
 
1418
+ const pinnedBox = pinnedRows
1419
+ ? `<div style="border:1px solid var(--border-subtle);border-radius:8px;overflow:hidden">${pinnedRows}</div>
1420
+ <div style="height:10px"></div>`
1421
+ : '';
1422
+
1425
1423
  body.innerHTML = `
1426
- <div style="border:1px solid var(--border-subtle);border-radius:8px;overflow:hidden">${pinnedRows}</div>
1427
- <div style="height:10px"></div>
1424
+ ${pinnedBox}
1428
1425
  <div style="font-size:11px;color:var(--text-muted);margin:0 0 8px">Shared workspace directory: <code>process/</code></div>
1429
1426
  <div style="border:1px solid var(--border-subtle);border-radius:8px;overflow:hidden">${listRows}</div>
1430
1427
  `;
@@ -1460,6 +1457,7 @@ async function loadHealth() {
1460
1457
  const team = health.team || { blockers: [], overlaps: [], compliance: null, agents: [] };
1461
1458
  const agentsSummary = health.agentsSummary || { agents: [] };
1462
1459
  const idleNudgeDebug = health.idleNudgeDebug || null;
1460
+ const scope = team.scope || null;
1463
1461
 
1464
1462
  healthAgentMap = new Map((team.agents || []).map(a => [String(a.agent || '').toLowerCase(), a]));
1465
1463
  const workflow = health.workflow || { agents: [] };
@@ -1528,6 +1526,16 @@ async function loadHealth() {
1528
1526
  const body = document.getElementById('health-body');
1529
1527
  let html = '';
1530
1528
 
1529
+ if (scope) {
1530
+ const orgHealthLink = scope.orgHealthUrl
1531
+ ? ` <a href="${esc(scope.orgHealthUrl)}" target="_blank" rel="noopener">Open org-health</a>`
1532
+ : '';
1533
+ html += `<div class="blocker-item" style="border-left:4px solid var(--yellow);margin-bottom:12px">
1534
+ <div class="blocker-agent">${esc(scope.label || 'Host-local health')}</div>
1535
+ <div class="blocker-text">${esc(scope.message || '')}${orgHealthLink}</div>
1536
+ </div>`;
1537
+ }
1538
+
1531
1539
  // Agent Health Grid
1532
1540
  if (displayAgents.length > 0) {
1533
1541
  html += '<div class="health-section"><div class="health-section-title">Agent Status</div><div class="health-grid">';
@@ -1806,6 +1814,18 @@ function normalizeEpochMs(v) {
1806
1814
  return v;
1807
1815
  }
1808
1816
 
1817
+ function hasReviewerDecision(task) {
1818
+ const meta = task && task.metadata && typeof task.metadata === 'object' ? task.metadata : null;
1819
+ const decision = meta && meta.reviewer_decision && typeof meta.reviewer_decision === 'object'
1820
+ ? meta.reviewer_decision
1821
+ : null;
1822
+ return !!decision;
1823
+ }
1824
+
1825
+ function shouldEscalateReviewerSla(task) {
1826
+ return task && task.slaState === 'breach' && !hasReviewerDecision(task);
1827
+ }
1828
+
1809
1829
  function renderReviewQueue() {
1810
1830
  const panel = document.getElementById('review-queue-panel');
1811
1831
  const body = document.getElementById('review-queue-body');
@@ -1817,11 +1837,23 @@ function renderReviewQueue() {
1817
1837
  const validating = allTasks
1818
1838
  .filter(t => t.status === 'validating')
1819
1839
  .map(t => {
1820
- const rawEntered = t.metadata?.entered_validating_at || t.updatedAt || t.createdAt;
1840
+ const meta = t.metadata || {};
1841
+ const reviewState = typeof meta.review_state === 'string' ? meta.review_state : '';
1842
+ const reviewerDecision = meta.reviewer_decision;
1843
+
1844
+ // If reviewer has acted (needs_author or reviewer_decision recorded), the ball is with the assignee.
1845
+ // We still track an SLA timer, but it should page the assignee (author), not the reviewer.
1846
+ const waitOn = (reviewState === 'needs_author' || reviewerDecision != null) ? 'author' : 'reviewer';
1847
+
1848
+ const rawEntered = waitOn === 'author'
1849
+ ? (reviewerDecision && reviewerDecision.decidedAt) || meta.review_last_activity_at || meta.entered_validating_at || t.updatedAt || t.createdAt
1850
+ : meta.entered_validating_at || t.updatedAt || t.createdAt;
1851
+
1821
1852
  const enteredAt = normalizeEpochMs(rawEntered) || now;
1822
1853
  const timeInReview = Math.min(Math.max(0, now - enteredAt), MAX_REVIEW_MS);
1823
1854
  const slaState = getReviewSlaState(timeInReview);
1824
- return { ...t, timeInReview, slaState, enteredAt };
1855
+
1856
+ return { ...t, timeInReview, slaState, enteredAt, waitOn, reviewState, hasReviewerDecision: reviewerDecision != null };
1825
1857
  })
1826
1858
  .sort((a, b) => {
1827
1859
  // Breaches first, then by time descending
@@ -1831,6 +1863,9 @@ function renderReviewQueue() {
1831
1863
  return b.timeInReview - a.timeInReview;
1832
1864
  });
1833
1865
 
1866
+ const reviewerQueue = validating.filter(t => t.waitOn === 'reviewer');
1867
+ const authorQueue = validating.filter(t => t.waitOn === 'author');
1868
+
1834
1869
  if (validating.length === 0) {
1835
1870
  panel.style.display = '';
1836
1871
  count.textContent = '';
@@ -1846,16 +1881,29 @@ function renderReviewQueue() {
1846
1881
  }
1847
1882
 
1848
1883
  panel.style.display = '';
1849
- count.textContent = validating.length + ' awaiting review';
1850
- // Update sidebar badge
1884
+ const reviewerBreachCount = reviewerQueue.filter(t => t.slaState === 'breach').length;
1885
+ const authorBreachCount = authorQueue.filter(t => t.slaState === 'breach').length;
1886
+
1887
+ // Primary count is "awaiting reviewer" — author-wait tasks are tracked separately.
1888
+ count.textContent = reviewerQueue.length + ' awaiting review';
1889
+
1890
+ // Update sidebar badge (reviewer queue only)
1851
1891
  const navReviewBadge = document.getElementById('nav-review-count');
1852
- if (navReviewBadge) navReviewBadge.textContent = validating.length;
1892
+ if (navReviewBadge) navReviewBadge.textContent = String(reviewerQueue.length);
1893
+
1894
+ const headerExtra = (reviewerBreachCount > 0 || authorBreachCount > 0)
1895
+ ? ' <span style="color:var(--red);font-size:11px;font-weight:600">'
1896
+ + (reviewerBreachCount > 0 ? (reviewerBreachCount + ' reviewer breach' + (reviewerBreachCount > 1 ? 'es' : '')) : '')
1897
+ + (reviewerBreachCount > 0 && authorBreachCount > 0 ? ' · ' : '')
1898
+ + (authorBreachCount > 0 ? (authorBreachCount + ' author breach' + (authorBreachCount > 1 ? 'es' : '')) : '')
1899
+ + '</span>'
1900
+ : '';
1853
1901
 
1854
- const breachCount = validating.filter(t => t.slaState === 'breach').length;
1855
- const headerExtra = breachCount > 0
1856
- ? ' <span style="color:var(--red);font-size:11px;font-weight:600">' + breachCount + ' breach' + (breachCount > 1 ? 'es' : '') + '</span>'
1902
+ const authorExtra = authorQueue.length > 0
1903
+ ? ' <span style="color:var(--text-muted);font-size:11px;font-weight:500">· ' + authorQueue.length + ' awaiting author</span>'
1857
1904
  : '';
1858
- count.innerHTML = validating.length + ' awaiting review' + headerExtra;
1905
+
1906
+ count.innerHTML = reviewerQueue.length + ' awaiting review' + authorExtra + headerExtra;
1859
1907
 
1860
1908
  body.innerHTML = validating.map(t => {
1861
1909
  const reviewer = t.reviewer || '<span style="color:var(--yellow)">unassigned</span>';
@@ -1884,33 +1932,40 @@ function renderReviewQueue() {
1884
1932
 
1885
1933
  bindTaskLinkHandlers(body);
1886
1934
 
1887
- // SLA breach escalation: post to watchdog if any breach found
1888
- if (breachCount > 0) {
1889
- escalateReviewBreaches(validating.filter(t => t.slaState === 'breach'));
1935
+ // SLA breach escalation: split reviewer-wait vs author-wait so we page the right person.
1936
+ // shouldEscalateReviewerSla() suppresses escalation when reviewer_decision already exists.
1937
+ if (reviewerBreachCount > 0) {
1938
+ escalateReviewerBreaches(reviewerQueue.filter(shouldEscalateReviewerSla));
1939
+ }
1940
+ if (authorBreachCount > 0) {
1941
+ escalateAuthorBreaches(authorQueue.filter(t => t.slaState === 'breach'));
1890
1942
  }
1891
1943
  }
1892
1944
 
1893
- let lastReviewEscalationAt = 0;
1945
+ let lastReviewerEscalationAt = 0;
1946
+ let lastAuthorEscalationAt = 0;
1894
1947
  const REVIEW_ESCALATION_COOLDOWN = 20 * 60 * 1000; // 20m
1895
1948
 
1896
- async function escalateReviewBreaches(breachedTasks) {
1949
+ async function escalateReviewerBreaches(breachedTasks) {
1897
1950
  const now = Date.now();
1898
- if (now - lastReviewEscalationAt < REVIEW_ESCALATION_COOLDOWN) return;
1899
- lastReviewEscalationAt = now;
1951
+ if (now - lastReviewerEscalationAt < REVIEW_ESCALATION_COOLDOWN) return;
1952
+ lastReviewerEscalationAt = now;
1900
1953
 
1901
1954
  const lines = breachedTasks.slice(0, 5).map(t => {
1902
1955
  const reviewer = t.reviewer || 'unassigned';
1903
- return '- ' + t.id + ' (' + (t.title || '').slice(0, 50) + ') — reviewer: @' + reviewer + ', waiting ' + formatDuration(t.timeInReview);
1956
+ const prUrl = t.metadata?.review_handoff?.pr_url || t.metadata?.review_handoff?.prUrl || '';
1957
+ const prPart = prUrl ? ' — ' + prUrl : '';
1958
+ return '- ' + t.id + ' (' + (t.title || '').slice(0, 50) + ') — reviewer: @' + reviewer + ', waiting ' + formatDuration(t.timeInReview) + prPart;
1904
1959
  });
1905
1960
 
1906
- const content = '@owner Review SLA breach detected:\n' + lines.join('\n');
1961
+ const content = 'Review overdue (reviewer SLA):\n' + lines.join('\n');
1907
1962
 
1908
1963
  try {
1909
1964
  await fetch(BASE + '/chat/messages', {
1910
1965
  method: 'POST',
1911
1966
  headers: { 'Content-Type': 'application/json' },
1912
1967
  body: JSON.stringify({
1913
- from: 'system',
1968
+ from: 'dashboard',
1914
1969
  content,
1915
1970
  channel: 'general',
1916
1971
  timestamp: now
@@ -1921,6 +1976,36 @@ async function escalateReviewBreaches(breachedTasks) {
1921
1976
  }
1922
1977
  }
1923
1978
 
1979
+ async function escalateAuthorBreaches(breachedTasks) {
1980
+ const now = Date.now();
1981
+ if (now - lastAuthorEscalationAt < REVIEW_ESCALATION_COOLDOWN) return;
1982
+ lastAuthorEscalationAt = now;
1983
+
1984
+ const lines = breachedTasks.slice(0, 5).map(t => {
1985
+ const assignee = t.assignee || 'unassigned';
1986
+ const prUrl = t.metadata?.review_handoff?.pr_url || t.metadata?.review_handoff?.prUrl || '';
1987
+ const prPart = prUrl ? ' — ' + prUrl : '';
1988
+ return '- ' + t.id + ' (' + (t.title || '').slice(0, 50) + ') — assignee: @' + assignee + ', waiting ' + formatDuration(t.timeInReview) + prPart;
1989
+ });
1990
+
1991
+ const content = 'Author action needed (post-review):\n' + lines.join('\n');
1992
+
1993
+ try {
1994
+ await fetch(BASE + '/chat/messages', {
1995
+ method: 'POST',
1996
+ headers: { 'Content-Type': 'application/json' },
1997
+ body: JSON.stringify({
1998
+ from: 'dashboard',
1999
+ content,
2000
+ channel: 'general',
2001
+ timestamp: now
2002
+ })
2003
+ });
2004
+ } catch (err) {
2005
+ console.error('Failed to escalate author breach:', err);
2006
+ }
2007
+ }
2008
+
1924
2009
  // ---- Feedback ----
1925
2010
  let feedbackData = null;
1926
2011
 
@@ -3020,13 +3105,21 @@ async function checkGettingStarted() {
3020
3105
 
3021
3106
  function dismissGettingStarted() {
3022
3107
  const panel = document.getElementById('getting-started');
3023
- if (panel) panel.classList.add('hidden');
3108
+ if (panel) {
3109
+ panel.classList.add('hidden');
3110
+ panel.style.display = 'none'; // belt + suspenders
3111
+ }
3024
3112
  try { localStorage.setItem('reflectt-gs-dismissed', '1'); } catch {}
3025
3113
  }
3114
+ // Expose globally for onclick handlers (safety net)
3115
+ window.dismissGettingStarted = dismissGettingStarted;
3116
+ window.dismissFirstBootBanner = dismissFirstBootBanner;
3026
3117
 
3027
3118
  updateClock();
3028
3119
  setInterval(updateClock, 30000);
3029
3120
  checkGettingStarted();
3121
+ // Retry getting-started check after 5s in case health wasn't ready at boot
3122
+ setTimeout(checkGettingStarted, 5000);
3030
3123
  refresh();
3031
3124
  connectEventStream();
3032
3125
  startAdaptiveRefresh();
@@ -3406,7 +3499,7 @@ function fileIcon(mimeType, name) {
3406
3499
  }
3407
3500
 
3408
3501
  function formatFileSize(bytes) {
3409
- if (!bytes) return '0 B';
3502
+ if (typeof bytes !== 'number' || bytes <= 0) return '0 B';
3410
3503
  if (bytes < 1024) return bytes + ' B';
3411
3504
  if (bytes < 1048576) return (bytes / 1024).toFixed(1) + ' KB';
3412
3505
  return (bytes / 1048576).toFixed(1) + ' MB';
@@ -3455,7 +3548,7 @@ function renderFiles(files) {
3455
3548
  return '<div class="file-card" tabindex="0" role="button" aria-label="' + (f.originalName || f.filename) + '">' +
3456
3549
  '<div class="thumb" style="display:flex;align-items:center;justify-content:center;height:80px;background:var(--surface-raised);border-radius:var(--radius-sm);overflow:hidden">' + thumb + '</div>' +
3457
3550
  '<div class="card-name">' + (f.originalName || f.filename) + '</div>' +
3458
- '<div class="card-meta">' + formatFileSize(f.size) + ' · ' + timeAgo(f.uploadedAt || f.createdAt) + '</div>' +
3551
+ '<div class="card-meta">' + formatFileSize(f.sizeBytes) + ' · ' + timeAgo(f.uploadedAt || f.createdAt) + '</div>' +
3459
3552
  '<div class="card-actions">' +
3460
3553
  '<a href="/files/' + f.id + '" download="' + (f.originalName || f.filename) + '" class="action-btn" aria-label="Download" onclick="event.stopPropagation()">⬇</a>' +
3461
3554
  '<button class="action-btn delete" aria-label="Delete" onclick="event.stopPropagation();deleteFile(\'' + f.id + '\')">🗑</button>' +
@@ -3468,7 +3561,7 @@ function renderFiles(files) {
3468
3561
  return '<div class="file-list-item" tabindex="0" role="button">' +
3469
3562
  '<span class="list-icon">' + icon + '</span>' +
3470
3563
  '<span class="list-name">' + (f.originalName || f.filename) + '</span>' +
3471
- '<span class="list-meta">' + formatFileSize(f.size) + '</span>' +
3564
+ '<span class="list-meta">' + formatFileSize(f.sizeBytes) + '</span>' +
3472
3565
  '<span class="list-meta">' + timeAgo(f.uploadedAt || f.createdAt) + '</span>' +
3473
3566
  '<div class="list-actions">' +
3474
3567
  '<a href="/files/' + f.id + '" download="' + (f.originalName || f.filename) + '" class="action-btn" aria-label="Download" onclick="event.stopPropagation()">⬇</a>' +
@@ -0,0 +1,118 @@
1
+ # Design Tokens — Platform Mapping
2
+
3
+ Source of truth: `/public/design-tokens.css`
4
+
5
+ ## iOS (Swift)
6
+
7
+ ```swift
8
+ // Colors.swift — auto-generate from design-tokens.css
9
+ import SwiftUI
10
+
11
+ extension Color {
12
+ static let brandPrimary = Color(hex: "#7C3AED")
13
+ static let brandPrimaryLight = Color(hex: "#A78BFA")
14
+ static let brandPrimaryDark = Color(hex: "#5B21B6")
15
+
16
+ // Canvas states
17
+ static let stateFloor = Color(hex: "#1F2937")
18
+ static let stateListening = Color(hex: "#7C3AED")
19
+ static let stateThinking = Color(hex: "#6366F1")
20
+ static let stateRendering = Color(hex: "#8B5CF6")
21
+ static let stateAmbient = Color(hex: "#374151")
22
+ static let stateDecision = Color(hex: "#F59E0B")
23
+ static let stateUrgent = Color(hex: "#EF4444")
24
+ static let stateHandoff = Color(hex: "#10B981")
25
+
26
+ // Trust
27
+ static let trustActive = Color(hex: "#F87171") // Red 400 — visible without alarming
28
+ }
29
+
30
+ // Dimensions.swift
31
+ enum Dimension {
32
+ static let tapTargetMin: CGFloat = 44
33
+ static let tapTargetUrgent: CGFloat = 52
34
+ static let orbSizeIdle: CGFloat = 64
35
+ static let orbSizeTranscript: CGFloat = 44
36
+ static let trustIndicatorSize: CGFloat = 10
37
+ static let overrideBarHeight: CGFloat = 52
38
+ static let presenceDotSize: CGFloat = 8
39
+ }
40
+
41
+ // Animation.swift
42
+ enum Timing {
43
+ static let fast: Double = 0.15
44
+ static let base: Double = 0.25
45
+ static let slow: Double = 0.35
46
+ static let canvas: Double = 0.5
47
+ static let orbGlow: Double = 1.8
48
+ }
49
+ ```
50
+
51
+ ## Android (Kotlin)
52
+
53
+ ```xml
54
+ <!-- colors.xml -->
55
+ <color name="brand_primary">#FF7C3AED</color>
56
+ <color name="brand_primary_light">#FFA78BFA</color>
57
+ <color name="brand_primary_dark">#FF5B21B6</color>
58
+
59
+ <color name="state_floor">#FF1F2937</color>
60
+ <color name="state_listening">#FF7C3AED</color>
61
+ <color name="state_thinking">#FF6366F1</color>
62
+ <color name="state_rendering">#FF8B5CF6</color>
63
+ <color name="state_ambient">#FF374151</color>
64
+ <color name="state_decision">#FFF59E0B</color>
65
+ <color name="state_urgent">#FFEF4444</color>
66
+ <color name="state_handoff">#FF10B981</color>
67
+
68
+ <color name="trust_active">#FFF87171</color> <!-- Red 400 -->
69
+
70
+ <!-- dimens.xml -->
71
+ <dimen name="tap_target_min">44dp</dimen>
72
+ <dimen name="tap_target_urgent">52dp</dimen>
73
+ <dimen name="orb_size_idle">64dp</dimen>
74
+ <dimen name="orb_size_transcript">44dp</dimen>
75
+ <dimen name="trust_indicator_size">10dp</dimen>
76
+ <dimen name="override_bar_height">52dp</dimen>
77
+ <dimen name="presence_dot_size">8dp</dimen>
78
+ ```
79
+
80
+ ```kotlin
81
+ // Timing.kt
82
+ object Timing {
83
+ const val FAST = 150L
84
+ const val BASE = 250L
85
+ const val SLOW = 350L
86
+ const val CANVAS = 500L
87
+ const val ORB_GLOW = 1800L
88
+ }
89
+ ```
90
+
91
+ ## Important Notes
92
+
93
+ **Canvas state colors are tint references, not solid fills.** The spec uses these as radial gradient tints. Platform implementations should apply them as gradient bases, not flat backgrounds. For v0 surfaces using solid fills, these values are acceptable approximations.
94
+
95
+ **Trust indicator uses red-400 (#F87171)** — deliberately lighter than error red. Present and visible without being alarming. Both-sensors escalates to red-500 (#EF4444).
96
+
97
+ ## Token Categories
98
+
99
+ | Category | Count | Notes |
100
+ |----------|-------|-------|
101
+ | Brand colors | 4 | Purple family |
102
+ | Semantic colors | 8 | Success/warning/error/info + backgrounds |
103
+ | Canvas states | 8 | Maps to state machine spec |
104
+ | Trust | 4 | Mic/camera indicators |
105
+ | Surface/bg | 6 | Light + dark mode |
106
+ | Text | 6 | Primary through inverse |
107
+ | Borders | 4 | Default/hover/focus/error |
108
+ | Typography | 14 | Font family, size, weight, line-height |
109
+ | Spacing | 12 | 0–64px scale |
110
+ | Radius | 6 | 4px–full |
111
+ | Shadows | 6 | sm through glow |
112
+ | Transitions | 9 | Duration + easing |
113
+ | Z-index | 8 | Layering scale |
114
+ | Interactive | 3 | Tap targets + focus ring |
115
+ | Canvas/orb | 5 | Orb dimensions + glow |
116
+ | Override bar | 3 | Height/bg/blur |
117
+ | Presence | 3 | Dot states |
118
+ | **Total** | **~105** | |
@@ -0,0 +1,195 @@
1
+ /* ═══════════════════════════════════════════════════════════════════════
2
+ Reflectt Design Tokens v0
3
+ ═══════════════════════════════════════════════════════════════════════
4
+ Single source of truth for all visual values across web, iOS, Android.
5
+ Platform implementations should map these to native equivalents:
6
+ - iOS: UIColor extensions + CGFloat constants
7
+ - Android: colors.xml + dimens.xml + attrs.xml
8
+ - Web: CSS custom properties (this file)
9
+
10
+ Usage: @import url('/design-tokens.css');
11
+ Or: <link rel="stylesheet" href="/design-tokens.css">
12
+ ═══════════════════════════════════════════════════════════════════════ */
13
+
14
+ :root {
15
+ /* ── Brand Colors ─────────────────────────────────────────────────── */
16
+ --color-brand-primary: #7C3AED; /* Purple 600 — main brand */
17
+ --color-brand-primary-light: #A78BFA; /* Purple 400 — hover/active */
18
+ --color-brand-primary-dark: #5B21B6; /* Purple 800 — pressed */
19
+ --color-brand-accent: #8B5CF6; /* Purple 500 — secondary accent */
20
+
21
+ /* ── Semantic Colors ──────────────────────────────────────────────── */
22
+ --color-success: #10B981; /* Emerald 500 */
23
+ --color-success-bg: #D1FAE5; /* Emerald 100 */
24
+ --color-warning: #F59E0B; /* Amber 500 */
25
+ --color-warning-bg: #FEF3C7; /* Amber 100 */
26
+ --color-error: #EF4444; /* Red 500 */
27
+ --color-error-bg: #FEE2E2; /* Red 100 */
28
+ --color-info: #3B82F6; /* Blue 500 */
29
+ --color-info-bg: #DBEAFE; /* Blue 100 */
30
+
31
+ /* ── Canvas State Colors ──────────────────────────────────────────── */
32
+ /* Maps to the 8-state machine from interface-os-v0-state-machine.html */
33
+ --color-state-floor: #1F2937; /* Gray 800 — resting */
34
+ --color-state-listening: #7C3AED; /* Purple 600 — active listening */
35
+ --color-state-thinking: #6366F1; /* Indigo 500 — processing */
36
+ --color-state-rendering: #8B5CF6; /* Purple 500 — outputting */
37
+ --color-state-ambient: #374151; /* Gray 700 — passive monitoring */
38
+ --color-state-decision: #F59E0B; /* Amber 500 — needs human input */
39
+ --color-state-urgent: #EF4444; /* Red 500 — critical alert */
40
+ --color-state-handoff: #10B981; /* Emerald 500 — transferring control */
41
+
42
+ /* ── Trust Indicator ──────────────────────────────────────────────── */
43
+ --color-trust-mic: #F87171; /* Red 400 — visible without alarming */ /* Red — microphone active */
44
+ --color-trust-camera: #F87171; /* Red — camera active */
45
+ --color-trust-mic-camera: #EF4444; /* Red 500 — both sensors escalated */ /* Red 600 — both active */
46
+ --trust-indicator-size: 10px;
47
+ --trust-indicator-position-top: env(safe-area-inset-top, 8px);
48
+ --trust-indicator-position-left: 12px;
49
+ --trust-indicator-z-index: 9999;
50
+
51
+ /* ── Surface / Background ─────────────────────────────────────────── */
52
+ --color-bg-primary: #FFFFFF;
53
+ --color-bg-secondary: #F9FAFB; /* Gray 50 */
54
+ --color-bg-tertiary: #F3F4F6; /* Gray 100 */
55
+ --color-bg-elevated: #FFFFFF;
56
+ --color-bg-overlay: rgba(0, 0, 0, 0.5);
57
+ --color-bg-canvas: #0F0F0F; /* Near-black for canvas surface */
58
+
59
+ /* ── Text ─────────────────────────────────────────────────────────── */
60
+ --color-text-primary: #111827; /* Gray 900 */
61
+ --color-text-secondary: #6B7280; /* Gray 500 */
62
+ --color-text-tertiary: #9CA3AF; /* Gray 400 */
63
+ --color-text-inverse: #FFFFFF;
64
+ --color-text-link: #7C3AED;
65
+ --color-text-on-brand: #FFFFFF;
66
+
67
+ /* ── Borders ──────────────────────────────────────────────────────── */
68
+ --color-border-default: #E5E7EB; /* Gray 200 */
69
+ --color-border-hover: #D1D5DB; /* Gray 300 */
70
+ --color-border-focus: #7C3AED;
71
+ --color-border-error: #EF4444;
72
+
73
+ /* ── Typography ───────────────────────────────────────────────────── */
74
+ --font-family-sans: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif;
75
+ --font-family-mono: 'SF Mono', SFMono-Regular, ui-monospace, 'Cascadia Code', Menlo, monospace;
76
+
77
+ --font-size-xs: 0.75rem; /* 12px */
78
+ --font-size-sm: 0.875rem; /* 14px */
79
+ --font-size-base: 1rem; /* 16px */
80
+ --font-size-lg: 1.125rem; /* 18px */
81
+ --font-size-xl: 1.25rem; /* 20px */
82
+ --font-size-2xl: 1.5rem; /* 24px */
83
+ --font-size-3xl: 1.875rem; /* 30px */
84
+
85
+ --font-weight-normal: 400;
86
+ --font-weight-medium: 500;
87
+ --font-weight-semibold: 600;
88
+ --font-weight-bold: 700;
89
+
90
+ --line-height-tight: 1.25;
91
+ --line-height-normal: 1.5;
92
+ --line-height-relaxed: 1.75;
93
+
94
+ /* ── Spacing ──────────────────────────────────────────────────────── */
95
+ --space-0: 0;
96
+ --space-1: 0.25rem; /* 4px */
97
+ --space-2: 0.5rem; /* 8px */
98
+ --space-3: 0.75rem; /* 12px */
99
+ --space-4: 1rem; /* 16px */
100
+ --space-5: 1.25rem; /* 20px */
101
+ --space-6: 1.5rem; /* 24px */
102
+ --space-8: 2rem; /* 32px */
103
+ --space-10: 2.5rem; /* 40px */
104
+ --space-12: 3rem; /* 48px */
105
+ --space-16: 4rem; /* 64px */
106
+
107
+ /* ── Border Radius ────────────────────────────────────────────────── */
108
+ --radius-sm: 4px;
109
+ --radius-md: 8px;
110
+ --radius-lg: 12px;
111
+ --radius-xl: 16px;
112
+ --radius-2xl: 24px;
113
+ --radius-full: 9999px;
114
+
115
+ /* ── Shadows ──────────────────────────────────────────────────────── */
116
+ --shadow-sm: 0 1px 2px rgba(0, 0, 0, 0.05);
117
+ --shadow-md: 0 4px 6px -1px rgba(0, 0, 0, 0.1), 0 2px 4px -2px rgba(0, 0, 0, 0.1);
118
+ --shadow-lg: 0 10px 15px -3px rgba(0, 0, 0, 0.1), 0 4px 6px -4px rgba(0, 0, 0, 0.1);
119
+ --shadow-xl: 0 20px 25px -5px rgba(0, 0, 0, 0.1), 0 8px 10px -6px rgba(0, 0, 0, 0.1);
120
+ --shadow-glow-brand: 0 0 20px rgba(124, 58, 237, 0.3);
121
+ --shadow-glow-urgent: 0 0 20px rgba(239, 68, 68, 0.4);
122
+
123
+ /* ── Transitions ──────────────────────────────────────────────────── */
124
+ --transition-fast: 150ms;
125
+ --transition-base: 250ms;
126
+ --transition-slow: 350ms;
127
+ --transition-canvas: 500ms; /* Canvas state transitions */
128
+
129
+ --easing-smooth: cubic-bezier(0.4, 0, 0.2, 1);
130
+ --easing-ease-out: cubic-bezier(0, 0, 0.2, 1);
131
+ --easing-ease-in: cubic-bezier(0.4, 0, 1, 1);
132
+ --easing-bounce: cubic-bezier(0.34, 1.56, 0.64, 1);
133
+ --easing-spring: cubic-bezier(0.175, 0.885, 0.32, 1.275);
134
+
135
+ /* ── Z-Index Scale ────────────────────────────────────────────────── */
136
+ --z-base: 0;
137
+ --z-dropdown: 100;
138
+ --z-sticky: 200;
139
+ --z-overlay: 300;
140
+ --z-modal: 400;
141
+ --z-popover: 500;
142
+ --z-toast: 600;
143
+ --z-override-bar: 800; /* Persistent controls */
144
+ --z-trust-indicator: 9999; /* Non-dismissable, always on top */
145
+
146
+ /* ── Interactive Elements ─────────────────────────────────────────── */
147
+ --tap-target-min: 44px; /* WCAG minimum */
148
+ --tap-target-urgent: 52px; /* Enlarged in urgent state */
149
+ --focus-ring: 0 0 0 3px rgba(124, 58, 237, 0.4);
150
+
151
+ /* ── Canvas / Orb ─────────────────────────────────────────────────── */
152
+ --orb-size-idle: 64px;
153
+ --orb-size-active: 64px;
154
+ --orb-size-transcript: 44px; /* Shrinks when transcript appears */
155
+ --orb-glow-duration: 1.8s;
156
+ --orb-color: var(--color-brand-primary);
157
+
158
+ /* ── Override Bar ─────────────────────────────────────────────────── */
159
+ --override-bar-height: 52px;
160
+ --override-bar-bg: rgba(0, 0, 0, 0.85);
161
+ --override-bar-blur: 20px;
162
+
163
+ /* ── Presence Dot ─────────────────────────────────────────────────── */
164
+ --presence-dot-size: 8px;
165
+ --presence-dot-active: #10B981; /* Online */
166
+ --presence-dot-idle: #F59E0B; /* Away */
167
+ --presence-dot-offline: #6B7280; /* Disconnected */
168
+ }
169
+
170
+ /* ── Dark Mode ──────────────────────────────────────────────────────── */
171
+ @media (prefers-color-scheme: dark) {
172
+ :root {
173
+ --color-bg-primary: #111827;
174
+ --color-bg-secondary: #1F2937;
175
+ --color-bg-tertiary: #374151;
176
+ --color-bg-elevated: #1F2937;
177
+ --color-bg-overlay: rgba(0, 0, 0, 0.7);
178
+
179
+ --color-text-primary: #F9FAFB;
180
+ --color-text-secondary: #9CA3AF;
181
+ --color-text-tertiary: #6B7280;
182
+
183
+ --color-border-default: #374151;
184
+ --color-border-hover: #4B5563;
185
+
186
+ --color-success-bg: rgba(16, 185, 129, 0.15);
187
+ --color-warning-bg: rgba(245, 158, 11, 0.15);
188
+ --color-error-bg: rgba(239, 68, 68, 0.15);
189
+ --color-info-bg: rgba(59, 130, 246, 0.15);
190
+
191
+ --shadow-sm: 0 1px 2px rgba(0, 0, 0, 0.3);
192
+ --shadow-md: 0 4px 6px -1px rgba(0, 0, 0, 0.4);
193
+ --shadow-lg: 0 10px 15px -3px rgba(0, 0, 0, 0.4);
194
+ }
195
+ }