@monoes/monomindcli 1.10.46 → 1.10.54

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 (144) hide show
  1. package/.claude/commands/monomind/do.md +52 -0
  2. package/.claude/commands/monomind/improve.md +2 -0
  3. package/.claude/helpers/handlers/route-handler.cjs +61 -11
  4. package/.claude/helpers/skill-registry.json +99 -51
  5. package/.claude/skills/agent-browser-testing/SKILL.md +522 -152
  6. package/.claude/skills/github-issue-triage/SKILL.md +354 -0
  7. package/.claude/skills/github-repo-recap/SKILL.md +207 -0
  8. package/.claude/skills/mastermind/_delegation.md +83 -0
  9. package/.claude/skills/mastermind/_protocol.md +14 -0
  10. package/dist/src/init/executor.d.ts.map +1 -1
  11. package/dist/src/init/executor.js +18 -19
  12. package/dist/src/init/executor.js.map +1 -1
  13. package/dist/src/init/helpers-generator.js +3 -3
  14. package/dist/src/init/helpers-generator.js.map +1 -1
  15. package/dist/src/ui/dashboard-v2.html +782 -42
  16. package/dist/src/ui/dashboard.html +1 -0
  17. package/dist/src/ui/data/agent-avatars.html +763 -0
  18. package/dist/src/ui/data/agent-avatars.json +966 -0
  19. package/dist/src/ui/data/avatars/account-strategist.svg +58 -0
  20. package/dist/src/ui/data/avatars/accounts-payable.svg +54 -0
  21. package/dist/src/ui/data/avatars/adaptive-coordinator.svg +55 -0
  22. package/dist/src/ui/data/avatars/adaptive-coordinator2.svg +54 -0
  23. package/dist/src/ui/data/avatars/ai-citation.svg +57 -0
  24. package/dist/src/ui/data/avatars/ai-engineer.svg +61 -0
  25. package/dist/src/ui/data/avatars/analytics-reporter.svg +53 -0
  26. package/dist/src/ui/data/avatars/api-tester.svg +53 -0
  27. package/dist/src/ui/data/avatars/architecture.svg +54 -0
  28. package/dist/src/ui/data/avatars/automation-governance.svg +55 -0
  29. package/dist/src/ui/data/avatars/backend-dev.svg +53 -0
  30. package/dist/src/ui/data/avatars/benchmarker.svg +54 -0
  31. package/dist/src/ui/data/avatars/blockchain-auditor.svg +53 -0
  32. package/dist/src/ui/data/avatars/byzantine-coord.svg +57 -0
  33. package/dist/src/ui/data/avatars/case-analyst.svg +57 -0
  34. package/dist/src/ui/data/avatars/cicd-engineer.svg +55 -0
  35. package/dist/src/ui/data/avatars/cloud-architect.svg +54 -0
  36. package/dist/src/ui/data/avatars/code-review-swarm.svg +57 -0
  37. package/dist/src/ui/data/avatars/coder-v119.svg +57 -0
  38. package/dist/src/ui/data/avatars/coder.svg +58 -0
  39. package/dist/src/ui/data/avatars/collective-coord.svg +54 -0
  40. package/dist/src/ui/data/avatars/compliance-auditor.svg +58 -0
  41. package/dist/src/ui/data/avatars/consensus-coordinator.svg +54 -0
  42. package/dist/src/ui/data/avatars/content-creator.svg +54 -0
  43. package/dist/src/ui/data/avatars/crdt-synchronizer.svg +53 -0
  44. package/dist/src/ui/data/avatars/cro-specialist.svg +58 -0
  45. package/dist/src/ui/data/avatars/data-consolidator.svg +54 -0
  46. package/dist/src/ui/data/avatars/data-engineer.svg +53 -0
  47. package/dist/src/ui/data/avatars/database-optimizer.svg +61 -0
  48. package/dist/src/ui/data/avatars/deal-strategist.svg +54 -0
  49. package/dist/src/ui/data/avatars/defender.svg +53 -0
  50. package/dist/src/ui/data/avatars/devops-automator.svg +56 -0
  51. package/dist/src/ui/data/avatars/discovery-coach.svg +54 -0
  52. package/dist/src/ui/data/avatars/email-marketing.svg +57 -0
  53. package/dist/src/ui/data/avatars/embedded-firmware.svg +61 -0
  54. package/dist/src/ui/data/avatars/evidence-collector.svg +57 -0
  55. package/dist/src/ui/data/avatars/experiment-tracker.svg +53 -0
  56. package/dist/src/ui/data/avatars/feedback-synthesizer.svg +54 -0
  57. package/dist/src/ui/data/avatars/finance-tracker.svg +54 -0
  58. package/dist/src/ui/data/avatars/frontend-developer.svg +54 -0
  59. package/dist/src/ui/data/avatars/game-audio-engineer.svg +59 -0
  60. package/dist/src/ui/data/avatars/game-designer.svg +54 -0
  61. package/dist/src/ui/data/avatars/gossip-coordinator.svg +54 -0
  62. package/dist/src/ui/data/avatars/hierarchical-coord.svg +54 -0
  63. package/dist/src/ui/data/avatars/incident-commander.svg +57 -0
  64. package/dist/src/ui/data/avatars/infrastructure.svg +54 -0
  65. package/dist/src/ui/data/avatars/input-validator.svg +53 -0
  66. package/dist/src/ui/data/avatars/ios-developer.svg +54 -0
  67. package/dist/src/ui/data/avatars/issue-tracker.svg +53 -0
  68. package/dist/src/ui/data/avatars/judge.svg +55 -0
  69. package/dist/src/ui/data/avatars/launch-strategist.svg +54 -0
  70. package/dist/src/ui/data/avatars/legal-compliance.svg +53 -0
  71. package/dist/src/ui/data/avatars/level-designer.svg +53 -0
  72. package/dist/src/ui/data/avatars/load-balancer.svg +57 -0
  73. package/dist/src/ui/data/avatars/mcp-builder.svg +53 -0
  74. package/dist/src/ui/data/avatars/memory-coordinator.svg +55 -0
  75. package/dist/src/ui/data/avatars/mesh-coordinator.svg +55 -0
  76. package/dist/src/ui/data/avatars/ml-developer.svg +58 -0
  77. package/dist/src/ui/data/avatars/mobile-app-builder.svg +53 -0
  78. package/dist/src/ui/data/avatars/mobile-dev.svg +54 -0
  79. package/dist/src/ui/data/avatars/model-qa.svg +58 -0
  80. package/dist/src/ui/data/avatars/narrative-designer.svg +58 -0
  81. package/dist/src/ui/data/avatars/outbound-strategist.svg +55 -0
  82. package/dist/src/ui/data/avatars/path-validator.svg +54 -0
  83. package/dist/src/ui/data/avatars/payment-agent.svg +53 -0
  84. package/dist/src/ui/data/avatars/perf-analyzer.svg +58 -0
  85. package/dist/src/ui/data/avatars/pipeline-analyst.svg +54 -0
  86. package/dist/src/ui/data/avatars/planner.svg +55 -0
  87. package/dist/src/ui/data/avatars/pr-manager.svg +54 -0
  88. package/dist/src/ui/data/avatars/pricing-strategist.svg +54 -0
  89. package/dist/src/ui/data/avatars/product-manager.svg +54 -0
  90. package/dist/src/ui/data/avatars/production-validator.svg +54 -0
  91. package/dist/src/ui/data/avatars/project-shepherd.svg +54 -0
  92. package/dist/src/ui/data/avatars/proposal-strategist.svg +54 -0
  93. package/dist/src/ui/data/avatars/prosecutor.svg +57 -0
  94. package/dist/src/ui/data/avatars/pseudocode.svg +53 -0
  95. package/dist/src/ui/data/avatars/queen-coordinator.svg +55 -0
  96. package/dist/src/ui/data/avatars/quorum-manager.svg +53 -0
  97. package/dist/src/ui/data/avatars/raft-manager.svg +53 -0
  98. package/dist/src/ui/data/avatars/reality-checker.svg +58 -0
  99. package/dist/src/ui/data/avatars/recruitment.svg +58 -0
  100. package/dist/src/ui/data/avatars/refinement.svg +53 -0
  101. package/dist/src/ui/data/avatars/release-manager.svg +54 -0
  102. package/dist/src/ui/data/avatars/repo-architect.svg +54 -0
  103. package/dist/src/ui/data/avatars/researcher.svg +58 -0
  104. package/dist/src/ui/data/avatars/resource-allocator.svg +53 -0
  105. package/dist/src/ui/data/avatars/reviewer.svg +53 -0
  106. package/dist/src/ui/data/avatars/safe-executor.svg +53 -0
  107. package/dist/src/ui/data/avatars/sales-coach.svg +53 -0
  108. package/dist/src/ui/data/avatars/sales-engineer.svg +58 -0
  109. package/dist/src/ui/data/avatars/scout-explorer.svg +58 -0
  110. package/dist/src/ui/data/avatars/security-architect.svg +54 -0
  111. package/dist/src/ui/data/avatars/security-auditor.svg +55 -0
  112. package/dist/src/ui/data/avatars/senior-developer.svg +58 -0
  113. package/dist/src/ui/data/avatars/senior-pm.svg +58 -0
  114. package/dist/src/ui/data/avatars/seo-specialist.svg +57 -0
  115. package/dist/src/ui/data/avatars/social-media.svg +54 -0
  116. package/dist/src/ui/data/avatars/solidity-engineer.svg +58 -0
  117. package/dist/src/ui/data/avatars/sparc-coder.svg +58 -0
  118. package/dist/src/ui/data/avatars/sparc-coord.svg +56 -0
  119. package/dist/src/ui/data/avatars/specification.svg +57 -0
  120. package/dist/src/ui/data/avatars/sprint-prioritizer.svg +53 -0
  121. package/dist/src/ui/data/avatars/sre.svg +54 -0
  122. package/dist/src/ui/data/avatars/studio-operations.svg +53 -0
  123. package/dist/src/ui/data/avatars/studio-producer.svg +55 -0
  124. package/dist/src/ui/data/avatars/support-responder.svg +56 -0
  125. package/dist/src/ui/data/avatars/system-architect.svg +54 -0
  126. package/dist/src/ui/data/avatars/task-orchestrator.svg +56 -0
  127. package/dist/src/ui/data/avatars/technical-artist.svg +53 -0
  128. package/dist/src/ui/data/avatars/technical-writer.svg +59 -0
  129. package/dist/src/ui/data/avatars/tester.svg +53 -0
  130. package/dist/src/ui/data/avatars/threat-detection.svg +61 -0
  131. package/dist/src/ui/data/avatars/trend-researcher.svg +54 -0
  132. package/dist/src/ui/data/avatars/trial-director.svg +55 -0
  133. package/dist/src/ui/data/avatars/unity-architect.svg +54 -0
  134. package/dist/src/ui/data/avatars/visionos-engineer.svg +57 -0
  135. package/dist/src/ui/data/avatars/worker-specialist.svg +55 -0
  136. package/dist/src/ui/data/avatars/workflow-architect.svg +57 -0
  137. package/dist/src/ui/data/avatars/workflow-automation.svg +54 -0
  138. package/dist/src/ui/data/avatars/zk-steward.svg +54 -0
  139. package/dist/src/ui/data/known-projects.json +1 -1
  140. package/dist/src/ui/data/mastermind-events.jsonl +28 -0
  141. package/dist/src/ui/orgs.html +1171 -0
  142. package/dist/src/ui/server.mjs +25 -8
  143. package/dist/tsconfig.tsbuildinfo +1 -1
  144. package/package.json +1 -1
@@ -0,0 +1,1171 @@
1
+ <!DOCTYPE html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="UTF-8">
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
+ <title>MASTERMIND ORGS</title>
7
+ <style>
8
+ *, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
9
+
10
+ :root {
11
+ --bg: oklch(7% 0.010 186);
12
+ --bg-panel: oklch(9% 0.012 186);
13
+ --bg-hover: oklch(62% 0.20 186 / 0.06);
14
+ --bg-active: oklch(62% 0.20 186 / 0.11);
15
+ --teal: oklch(62% 0.20 186);
16
+ --teal-dim: oklch(62% 0.20 186 / 0.18);
17
+ --teal-glow: oklch(62% 0.20 186 / 0.08);
18
+ --indigo: oklch(68% 0.18 252);
19
+ --green: oklch(68% 0.20 150);
20
+ --green-glow:oklch(68% 0.20 150 / 0.25);
21
+ --amber: oklch(78% 0.18 80);
22
+ --red: oklch(62% 0.22 25);
23
+ --text: oklch(80% 0.012 186);
24
+ --muted: oklch(52% 0.010 186);
25
+ --dim: oklch(36% 0.008 186);
26
+ --border: oklch(62% 0.20 186 / 0.12);
27
+ --border-hi: oklch(62% 0.20 186 / 0.35);
28
+ --mono: 'Azeret Mono', 'Space Mono', 'Courier New', monospace;
29
+ }
30
+
31
+ html, body {
32
+ width: 100%; height: 100%; overflow: hidden;
33
+ background: var(--bg);
34
+ font-family: var(--mono);
35
+ color: var(--text);
36
+ font-size: 13px;
37
+ line-height: 1.5;
38
+ user-select: none;
39
+ }
40
+
41
+ /* ── Layout ─────────────────────────────────────────────────── */
42
+ #app { display: flex; height: 100vh; }
43
+
44
+ #sidebar {
45
+ width: 260px; flex-shrink: 0;
46
+ background: var(--bg-panel);
47
+ border-right: 1px solid var(--border);
48
+ display: flex; flex-direction: column;
49
+ overflow: hidden;
50
+ }
51
+
52
+ #main {
53
+ flex: 1; display: flex; flex-direction: column;
54
+ overflow: hidden; min-width: 0;
55
+ }
56
+
57
+ /* ── Sidebar ─────────────────────────────────────────────────── */
58
+ #sb-header {
59
+ padding: 14px 14px 12px;
60
+ border-bottom: 1px solid var(--border);
61
+ flex-shrink: 0;
62
+ }
63
+
64
+ #sb-wordmark {
65
+ display: flex; align-items: center; gap: 8px; margin-bottom: 10px;
66
+ }
67
+
68
+ #sb-back {
69
+ font-size: 9px; letter-spacing: 1px; color: var(--dim);
70
+ text-decoration: none; padding: 3px 7px;
71
+ border: 1px solid var(--border); border-radius: 3px;
72
+ transition: color 0.12s, border-color 0.12s;
73
+ }
74
+ #sb-back:hover { color: var(--teal); border-color: var(--teal-dim); }
75
+
76
+ #sb-title {
77
+ font-size: 9px; letter-spacing: 4px; color: var(--teal);
78
+ opacity: 0.8;
79
+ }
80
+
81
+ #sb-status-row {
82
+ display: flex; align-items: center; gap: 6px;
83
+ }
84
+
85
+ .live-dot {
86
+ width: 6px; height: 6px; border-radius: 50%;
87
+ background: var(--dim); flex-shrink: 0;
88
+ transition: background 0.4s, box-shadow 0.4s;
89
+ }
90
+ .live-dot.on {
91
+ background: var(--green);
92
+ box-shadow: 0 0 6px var(--green-glow);
93
+ }
94
+ @media (prefers-reduced-motion: no-preference) {
95
+ .live-dot.on { animation: livepulse 2.2s ease-in-out infinite; }
96
+ }
97
+ @keyframes livepulse { 0%,100%{opacity:1} 50%{opacity:0.45} }
98
+
99
+ #sb-status-text { font-size: 9px; letter-spacing: 2px; color: var(--dim); }
100
+ #sb-running-count { font-size: 8px; color: var(--green); margin-left: auto; letter-spacing: 1px; }
101
+
102
+ #sb-list {
103
+ flex: 1; overflow-y: auto;
104
+ padding: 6px 0;
105
+ scrollbar-width: thin;
106
+ scrollbar-color: var(--teal-dim) transparent;
107
+ }
108
+ #sb-list::-webkit-scrollbar { width: 3px; }
109
+ #sb-list::-webkit-scrollbar-thumb { background: var(--teal-dim); border-radius: 2px; }
110
+
111
+ .org-item {
112
+ padding: 10px 14px;
113
+ cursor: pointer;
114
+ border-left: 2px solid transparent;
115
+ transition: background 0.12s, border-color 0.12s;
116
+ position: relative;
117
+ }
118
+ .org-item:hover { background: var(--bg-hover); }
119
+ .org-item.active {
120
+ background: var(--bg-active);
121
+ border-left-color: var(--teal);
122
+ }
123
+ .org-item.running { border-left-color: var(--green); }
124
+ .org-item.active.running { border-left-color: var(--teal); }
125
+
126
+ .oi-header { display: flex; align-items: center; gap: 7px; margin-bottom: 3px; }
127
+
128
+ .oi-dot {
129
+ width: 5px; height: 5px; border-radius: 50%;
130
+ background: var(--dim); flex-shrink: 0;
131
+ }
132
+ .oi-dot.running {
133
+ background: var(--green);
134
+ box-shadow: 0 0 4px var(--green);
135
+ }
136
+ @media (prefers-reduced-motion: no-preference) {
137
+ .oi-dot.running { animation: livepulse 2s ease-in-out infinite; }
138
+ }
139
+
140
+ .oi-name {
141
+ font-size: 11px; color: var(--text); font-weight: 500;
142
+ white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
143
+ flex: 1;
144
+ }
145
+
146
+ .oi-badge {
147
+ font-size: 7px; padding: 1px 6px; border-radius: 2px; flex-shrink: 0;
148
+ text-transform: uppercase; letter-spacing: 0.06em;
149
+ }
150
+ .oi-badge.running {
151
+ background: oklch(68% 0.20 150 / 0.12);
152
+ color: var(--green);
153
+ border: 1px solid oklch(68% 0.20 150 / 0.28);
154
+ }
155
+ .oi-badge.idle {
156
+ background: transparent; color: var(--dim);
157
+ border: 1px solid var(--border);
158
+ }
159
+
160
+ .oi-goal {
161
+ font-size: 9px; color: var(--muted); line-height: 1.5;
162
+ white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
163
+ padding-left: 12px;
164
+ }
165
+
166
+ .oi-meta {
167
+ display: flex; gap: 8px; padding-left: 12px; margin-top: 3px;
168
+ }
169
+ .oi-chip {
170
+ font-size: 8px; color: var(--dim); letter-spacing: 0.5px;
171
+ }
172
+ .oi-chip span { color: var(--muted); }
173
+
174
+ #sb-empty {
175
+ padding: 28px 16px; text-align: center;
176
+ color: var(--dim); font-size: 9px; line-height: 1.8; letter-spacing: 0.5px;
177
+ }
178
+ #sb-empty .empty-icon {
179
+ font-size: 24px; display: block; margin-bottom: 10px; opacity: 0.35;
180
+ }
181
+ #sb-empty .empty-cmd {
182
+ display: inline-block; margin-top: 10px;
183
+ color: var(--teal); border: 1px solid var(--teal-dim);
184
+ padding: 3px 10px; border-radius: 3px; font-size: 8px; letter-spacing: 1px;
185
+ }
186
+
187
+ /* ── Main area ─────────────────────────────────────────────────── */
188
+ #main-header {
189
+ padding: 12px 20px 11px;
190
+ border-bottom: 1px solid var(--border);
191
+ display: flex; align-items: center; gap: 12px;
192
+ flex-shrink: 0;
193
+ }
194
+
195
+ #org-label {
196
+ font-size: 9px; letter-spacing: 4px; color: var(--teal);
197
+ opacity: 0.7; text-transform: uppercase;
198
+ }
199
+
200
+ #org-name {
201
+ font-size: 15px; letter-spacing: 2px; color: var(--text);
202
+ font-weight: 600; text-transform: uppercase;
203
+ }
204
+
205
+ #org-status-badge {
206
+ font-size: 8px; padding: 2px 8px; border-radius: 2px;
207
+ text-transform: uppercase; letter-spacing: 0.1em; margin-left: 4px;
208
+ }
209
+ #org-status-badge.running {
210
+ background: oklch(68% 0.20 150 / 0.12);
211
+ color: var(--green); border: 1px solid oklch(68% 0.20 150 / 0.25);
212
+ }
213
+ #org-status-badge.idle {
214
+ background: transparent; color: var(--dim);
215
+ border: 1px solid var(--border);
216
+ }
217
+
218
+ .hdr-chip {
219
+ font-size: 8px; color: var(--dim); letter-spacing: 0.5px;
220
+ padding: 2px 8px; border: 1px solid var(--border); border-radius: 2px;
221
+ }
222
+ .hdr-chip span { color: var(--muted); }
223
+
224
+ #main-tabs {
225
+ display: flex; gap: 0; border-bottom: 1px solid var(--border);
226
+ flex-shrink: 0; padding: 0 20px;
227
+ }
228
+
229
+ .tab-btn {
230
+ font-size: 9px; letter-spacing: 2px; color: var(--dim);
231
+ padding: 8px 14px; background: none; border: none; cursor: pointer;
232
+ font-family: var(--mono); text-transform: uppercase;
233
+ border-bottom: 2px solid transparent; margin-bottom: -1px;
234
+ transition: color 0.12s, border-color 0.12s;
235
+ }
236
+ .tab-btn:hover { color: var(--muted); }
237
+ .tab-btn.active { color: var(--teal); border-bottom-color: var(--teal); }
238
+
239
+ #main-body { flex: 1; overflow-y: auto; overflow-x: hidden; }
240
+ #main-body::-webkit-scrollbar { width: 4px; }
241
+ #main-body::-webkit-scrollbar-thumb { background: var(--teal-dim); border-radius: 2px; }
242
+
243
+ .tab-pane { display: none; }
244
+ .tab-pane.active { display: block; }
245
+
246
+ /* ── Empty / no-selection state ─────────────────────────────────── */
247
+ #no-org-state {
248
+ display: flex; flex-direction: column; align-items: center;
249
+ justify-content: center; height: 100%; gap: 10px; opacity: 0.5;
250
+ padding: 40px;
251
+ }
252
+ #no-org-state .ns-icon { font-size: 48px; opacity: 0.25; }
253
+ #no-org-state .ns-text {
254
+ font-size: 9px; letter-spacing: 3px; color: var(--dim);
255
+ text-align: center;
256
+ }
257
+
258
+ /* ── Org Chart ──────────────────────────────────────────────────── */
259
+ #chart-pane {
260
+ padding: 20px;
261
+ display: flex; flex-direction: column; gap: 16px;
262
+ }
263
+
264
+ #chart-svg-wrap {
265
+ background: oklch(8% 0.01 186);
266
+ border: 1px solid var(--border);
267
+ border-radius: 4px;
268
+ overflow: hidden; position: relative;
269
+ }
270
+
271
+ #org-chart-svg {
272
+ width: 100%; display: block;
273
+ }
274
+
275
+ /* ── Info grid (goal / topology / created) ─────────────────────── */
276
+ #org-meta-grid {
277
+ display: grid; grid-template-columns: 1fr 1fr 1fr; gap: 1px;
278
+ background: var(--border); border: 1px solid var(--border);
279
+ border-radius: 3px; overflow: hidden;
280
+ }
281
+ .meta-cell {
282
+ background: var(--bg-panel);
283
+ padding: 10px 14px;
284
+ }
285
+ .meta-cell-label { font-size: 8px; letter-spacing: 2px; color: var(--dim); margin-bottom: 4px; }
286
+ .meta-cell-value { font-size: 11px; color: var(--text); }
287
+
288
+ /* ── Roles list ─────────────────────────────────────────────────── */
289
+ #roles-pane { padding: 20px; }
290
+
291
+ .roles-grid {
292
+ display: grid; grid-template-columns: repeat(auto-fill, minmax(240px, 1fr));
293
+ gap: 10px;
294
+ }
295
+
296
+ .role-block {
297
+ background: var(--bg-panel);
298
+ border: 1px solid var(--border);
299
+ border-radius: 3px; padding: 12px 14px;
300
+ transition: border-color 0.12s;
301
+ }
302
+ .role-block:hover { border-color: var(--teal-dim); }
303
+
304
+ .rb-header { display: flex; align-items: flex-start; gap: 8px; margin-bottom: 6px; }
305
+ .rb-dot {
306
+ width: 7px; height: 7px; border-radius: 50%; flex-shrink: 0; margin-top: 4px;
307
+ }
308
+ .rb-title { font-size: 12px; color: var(--text); font-weight: 500; line-height: 1.3; }
309
+ .rb-agent {
310
+ margin-left: auto; flex-shrink: 0;
311
+ font-size: 8px; padding: 1px 7px; border-radius: 2px;
312
+ border: 1px solid var(--teal-dim); color: var(--teal); opacity: 0.85;
313
+ letter-spacing: 0.5px;
314
+ }
315
+ .rb-id { font-size: 9px; color: var(--dim); margin-bottom: 5px; padding-left: 15px; }
316
+ .rb-reports { font-size: 9px; color: var(--muted); padding-left: 15px; margin-bottom: 6px; }
317
+ .rb-reports span { color: var(--indigo); }
318
+
319
+ .rb-resps { padding-left: 15px; }
320
+ .rb-resp-item {
321
+ font-size: 9px; color: var(--dim); line-height: 1.6;
322
+ display: flex; gap: 6px; align-items: flex-start;
323
+ }
324
+ .rb-resp-item::before {
325
+ content: '—'; color: oklch(36% 0.008 186 / 0.5); flex-shrink: 0; margin-top: 1px;
326
+ }
327
+
328
+ /* ── Activity / event log ───────────────────────────────────────── */
329
+ #activity-pane { padding: 20px; }
330
+ #activity-log { display: flex; flex-direction: column; gap: 0; }
331
+
332
+ .act-row {
333
+ display: grid;
334
+ grid-template-columns: 60px 90px 1fr;
335
+ gap: 8px; padding: 5px 0;
336
+ border-bottom: 1px solid oklch(62% 0.20 186 / 0.05);
337
+ align-items: baseline; font-size: 10px;
338
+ }
339
+ .act-row:last-child { border-bottom: none; }
340
+
341
+ .act-time { color: var(--dim); font-variant-numeric: tabular-nums; font-size: 9px; }
342
+ .act-type { color: var(--teal); font-weight: 500; letter-spacing: 0.5px; }
343
+ .act-msg { color: var(--muted); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
344
+
345
+ .act-empty { color: var(--dim); font-size: 9px; padding: 16px 0; letter-spacing: 1px; }
346
+
347
+ /* ── Health pane ─────────────────────────────────────────────────── */
348
+ #health-pane { padding: 20px; }
349
+
350
+ .health-grid {
351
+ display: grid; grid-template-columns: repeat(auto-fill, minmax(160px, 1fr));
352
+ gap: 10px; margin-bottom: 16px;
353
+ }
354
+
355
+ .health-cell {
356
+ background: var(--bg-panel);
357
+ border: 1px solid var(--border); border-radius: 3px;
358
+ padding: 12px 14px;
359
+ }
360
+ .hc-label { font-size: 8px; letter-spacing: 2px; color: var(--dim); margin-bottom: 4px; }
361
+ .hc-value { font-size: 18px; color: var(--text); font-weight: 600; font-variant-numeric: tabular-nums; }
362
+ .hc-sub { font-size: 9px; color: var(--muted); margin-top: 2px; }
363
+ .hc-value.green { color: var(--green); }
364
+ .hc-value.amber { color: var(--amber); }
365
+ .hc-value.red { color: var(--red); }
366
+
367
+ /* ── Communication edges (SVG in chart) ─────────────────────────── */
368
+ .oc-edge { stroke: var(--teal); stroke-opacity: 0.25; stroke-width: 1; fill: none; }
369
+ .oc-edge.command { stroke: var(--teal); stroke-opacity: 0.4; }
370
+ .oc-edge.report { stroke: var(--indigo); stroke-opacity: 0.35; stroke-dasharray: 4 3; }
371
+ .oc-edge.feedback { stroke: var(--amber); stroke-opacity: 0.25; stroke-dasharray: 2 4; }
372
+ .oc-edge.handoff { stroke: var(--green); stroke-opacity: 0.3; }
373
+
374
+ .oc-node-bg { fill: oklch(8% 0.01 186); stroke-width: 1.5; }
375
+ .oc-node-bg.boss { stroke: var(--teal); }
376
+ .oc-node-bg.peer { stroke: oklch(68% 0.18 252); }
377
+ .oc-node-text {
378
+ fill: var(--text); font-size: 10px;
379
+ font-family: 'Azeret Mono', 'Space Mono', monospace;
380
+ text-anchor: middle; dominant-baseline: middle;
381
+ pointer-events: none;
382
+ }
383
+ .oc-node-sub {
384
+ fill: var(--muted); font-size: 8px;
385
+ font-family: 'Azeret Mono', 'Space Mono', monospace;
386
+ text-anchor: middle; dominant-baseline: middle;
387
+ pointer-events: none;
388
+ }
389
+ .oc-node-pulse { fill: none; stroke-width: 1; opacity: 0; }
390
+ @media (prefers-reduced-motion: no-preference) {
391
+ .oc-node-pulse.running {
392
+ animation: nodepulse 2.5s ease-in-out infinite;
393
+ stroke: var(--green); opacity: 0.35;
394
+ }
395
+ }
396
+ @keyframes nodepulse { 0%,100%{r:26;opacity:0.2} 50%{r:30;opacity:0.55} }
397
+
398
+ /* Arrow markers are inline in the SVG */
399
+ </style>
400
+ </head>
401
+ <body>
402
+ <div id="app">
403
+
404
+ <!-- ── Sidebar: org list ── -->
405
+ <div id="sidebar">
406
+ <div id="sb-header">
407
+ <div id="sb-wordmark">
408
+ <a id="sb-back" href="/">← CONTROL</a>
409
+ <span id="sb-title">ORGS</span>
410
+ </div>
411
+ <div id="sb-status-row">
412
+ <div class="live-dot" id="sb-live-dot"></div>
413
+ <span id="sb-status-text">LOADING</span>
414
+ <span id="sb-running-count"></span>
415
+ </div>
416
+ </div>
417
+ <div id="sb-list">
418
+ <div id="sb-empty" style="display:none">
419
+ <span class="empty-icon">⬡</span>
420
+ NO ORGS FOUND<br><br>
421
+ Create a named agent team<br>that coordinates across sessions.
422
+ <span class="empty-cmd">/mastermind:createorg</span>
423
+ </div>
424
+ </div>
425
+ </div>
426
+
427
+ <!-- ── Main content ── -->
428
+ <div id="main">
429
+
430
+ <!-- No-selection splash -->
431
+ <div id="no-org-state">
432
+ <div class="ns-icon">⬡</div>
433
+ <div class="ns-text">SELECT AN ORG</div>
434
+ </div>
435
+
436
+ <!-- Org detail (hidden until org selected) -->
437
+ <div id="org-detail" style="display:none; height:100%; flex-direction:column; overflow:hidden;">
438
+
439
+ <div id="main-header">
440
+ <div>
441
+ <div id="org-label">MASTERMIND ORG</div>
442
+ <div style="display:flex; align-items:center; gap:8px; margin-top:2px;">
443
+ <div id="org-name">—</div>
444
+ <span id="org-status-badge" class="idle">IDLE</span>
445
+ </div>
446
+ </div>
447
+ <div style="margin-left:auto; display:flex; gap:6px; flex-wrap:wrap; justify-content:flex-end;">
448
+ <div class="hdr-chip">ROLES <span id="hdr-roles">0</span></div>
449
+ <div class="hdr-chip">TOPOLOGY <span id="hdr-topology">—</span></div>
450
+ <div class="hdr-chip" id="hdr-created-wrap" style="display:none">SINCE <span id="hdr-created">—</span></div>
451
+ </div>
452
+ <button id="stop-btn" onclick="stopCurrentOrg()" style="display:none;
453
+ font-size:9px; font-family:var(--mono); letter-spacing:1px; color:var(--red);
454
+ border:1px solid oklch(62% 0.22 25 / 0.35); background:none; padding:4px 12px;
455
+ border-radius:3px; cursor:pointer; text-transform:uppercase; transition:background 0.12s;"
456
+ onmouseover="this.style.background='oklch(62% 0.22 25 / 0.08)'"
457
+ onmouseout="this.style.background='none'">STOP</button>
458
+ </div>
459
+
460
+ <div id="main-tabs">
461
+ <button class="tab-btn active" onclick="switchTab('chart')">ORG CHART</button>
462
+ <button class="tab-btn" onclick="switchTab('roles')">ROLES</button>
463
+ <button class="tab-btn" onclick="switchTab('activity')">ACTIVITY</button>
464
+ <button class="tab-btn" onclick="switchTab('health')">HEALTH</button>
465
+ </div>
466
+
467
+ <div id="main-body">
468
+
469
+ <!-- Chart tab -->
470
+ <div class="tab-pane active" id="tab-chart">
471
+ <div id="chart-pane">
472
+ <div id="org-meta-grid">
473
+ <div class="meta-cell">
474
+ <div class="meta-cell-label">GOAL</div>
475
+ <div class="meta-cell-value" id="meta-goal" style="font-size:10px;line-height:1.5;white-space:normal;color:var(--muted)">—</div>
476
+ </div>
477
+ <div class="meta-cell">
478
+ <div class="meta-cell-label">TOPOLOGY</div>
479
+ <div class="meta-cell-value" id="meta-topology">—</div>
480
+ </div>
481
+ <div class="meta-cell">
482
+ <div class="meta-cell-label">CREATED</div>
483
+ <div class="meta-cell-value" id="meta-created" style="font-size:10px;color:var(--muted)">—</div>
484
+ </div>
485
+ </div>
486
+ <div id="chart-svg-wrap">
487
+ <svg id="org-chart-svg" viewBox="0 0 720 320">
488
+ <defs>
489
+ <marker id="arr-teal" markerWidth="6" markerHeight="6" refX="5" refY="3" orient="auto">
490
+ <path d="M0,0 L0,6 L6,3 z" fill="oklch(62% 0.20 186 / 0.5)"/>
491
+ </marker>
492
+ <marker id="arr-indigo" markerWidth="6" markerHeight="6" refX="5" refY="3" orient="auto">
493
+ <path d="M0,0 L0,6 L6,3 z" fill="oklch(68% 0.18 252 / 0.45)"/>
494
+ </marker>
495
+ <marker id="arr-amber" markerWidth="6" markerHeight="6" refX="5" refY="3" orient="auto">
496
+ <path d="M0,0 L0,6 L6,3 z" fill="oklch(78% 0.18 80 / 0.35)"/>
497
+ </marker>
498
+ <marker id="arr-green" markerWidth="6" markerHeight="6" refX="5" refY="3" orient="auto">
499
+ <path d="M0,0 L0,6 L6,3 z" fill="oklch(68% 0.20 150 / 0.4)"/>
500
+ </marker>
501
+ <filter id="node-glow" x="-50%" y="-50%" width="200%" height="200%">
502
+ <feGaussianBlur in="SourceGraphic" stdDeviation="3" result="b"/>
503
+ <feMerge><feMergeNode in="b"/><feMergeNode in="SourceGraphic"/></feMerge>
504
+ </filter>
505
+ </defs>
506
+ <rect width="720" height="320" fill="oklch(7% 0.008 186)"/>
507
+ <g id="oc-edges"></g>
508
+ <g id="oc-nodes"></g>
509
+ </svg>
510
+ </div>
511
+
512
+ <!-- Legend -->
513
+ <div style="display:flex; gap:16px; flex-wrap:wrap; padding:0 2px;">
514
+ <div style="display:flex; align-items:center; gap:5px; font-size:8px; color:var(--dim);">
515
+ <svg width="24" height="8"><line x1="0" y1="4" x2="24" y2="4" stroke="oklch(62% 0.20 186)" stroke-opacity="0.5" stroke-width="1.5" marker-end="url(#arr-teal)"/></svg>
516
+ COMMAND
517
+ </div>
518
+ <div style="display:flex; align-items:center; gap:5px; font-size:8px; color:var(--dim);">
519
+ <svg width="24" height="8"><line x1="0" y1="4" x2="24" y2="4" stroke="oklch(68% 0.18 252)" stroke-opacity="0.45" stroke-width="1" stroke-dasharray="4 3" marker-end="url(#arr-indigo)"/></svg>
520
+ REPORT
521
+ </div>
522
+ <div style="display:flex; align-items:center; gap:5px; font-size:8px; color:var(--dim);">
523
+ <svg width="24" height="8"><line x1="0" y1="4" x2="24" y2="4" stroke="oklch(78% 0.18 80)" stroke-opacity="0.35" stroke-width="1" stroke-dasharray="2 4" marker-end="url(#arr-amber)"/></svg>
524
+ FEEDBACK
525
+ </div>
526
+ <div style="display:flex; align-items:center; gap:5px; font-size:8px; color:var(--dim);">
527
+ <svg width="24" height="8"><line x1="0" y1="4" x2="24" y2="4" stroke="oklch(68% 0.20 150)" stroke-opacity="0.4" stroke-width="1.5" marker-end="url(#arr-green)"/></svg>
528
+ HANDOFF
529
+ </div>
530
+ </div>
531
+ </div>
532
+ </div>
533
+
534
+ <!-- Roles tab -->
535
+ <div class="tab-pane" id="tab-roles">
536
+ <div id="roles-pane">
537
+ <div id="roles-grid" class="roles-grid"></div>
538
+ </div>
539
+ </div>
540
+
541
+ <!-- Activity tab -->
542
+ <div class="tab-pane" id="tab-activity">
543
+ <div id="activity-pane">
544
+ <div id="activity-log"></div>
545
+ </div>
546
+ </div>
547
+
548
+ <!-- Health tab -->
549
+ <div class="tab-pane" id="tab-health">
550
+ <div id="health-pane">
551
+ <div id="health-grid" class="health-grid"></div>
552
+ <div id="health-detail"></div>
553
+ </div>
554
+ </div>
555
+
556
+ </div><!-- end main-body -->
557
+ </div><!-- end org-detail -->
558
+ </div><!-- end main -->
559
+ </div><!-- end app -->
560
+
561
+ <script>
562
+ 'use strict';
563
+
564
+ // ── State ─────────────────────────────────────────────────────────
565
+ let allOrgs = [];
566
+ let selectedOrg = null;
567
+ let orgDetailData = null;
568
+ let currentTab = 'chart';
569
+
570
+ // ── Utilities ──────────────────────────────────────────────────────
571
+ function esc(s) {
572
+ return String(s ?? '')
573
+ .replace(/&/g,'&amp;').replace(/</g,'&lt;').replace(/>/g,'&gt;')
574
+ .replace(/"/g,'&quot;').replace(/'/g,'&#39;');
575
+ }
576
+ function fmtDate(ts) {
577
+ if (!ts) return '—';
578
+ const d = new Date(typeof ts === 'number' ? ts : Date.parse(ts));
579
+ if (isNaN(d)) return ts;
580
+ return d.toLocaleDateString('en', {month:'short', day:'numeric', year:'numeric'});
581
+ }
582
+ function fmtTime(ts) {
583
+ if (!ts) return '';
584
+ const d = new Date(typeof ts === 'number' ? ts : Date.parse(ts));
585
+ if (isNaN(d)) return '';
586
+ return d.toLocaleTimeString('en', {hour12:false, hour:'2-digit', minute:'2-digit', second:'2-digit'});
587
+ }
588
+ function fmtEventType(t) {
589
+ const m = {
590
+ 'org:start':'START','org:stop':'STOP','org:complete':'COMPLETE',
591
+ 'org:create':'CREATE','org:heartbeat':'HB','org:agent:online':'AGENT ON',
592
+ 'org:comms':'COMMS','org:checkpoint':'CHECKPOINT',
593
+ };
594
+ return m[t] || (t || '').replace(/^org:/,'').toUpperCase();
595
+ }
596
+
597
+ // Role colors cycle
598
+ const ROLE_COLORS = [
599
+ 'oklch(62% 0.20 186)',
600
+ 'oklch(68% 0.18 252)',
601
+ 'oklch(68% 0.20 150)',
602
+ 'oklch(78% 0.18 80)',
603
+ 'oklch(62% 0.22 25)',
604
+ 'oklch(74% 0.16 310)',
605
+ ];
606
+ function roleColor(i) { return ROLE_COLORS[i % ROLE_COLORS.length]; }
607
+
608
+ // ── Tab switching ─────────────────────────────────────────────────
609
+ window.switchTab = function(tab) {
610
+ currentTab = tab;
611
+ document.querySelectorAll('.tab-btn').forEach(b => {
612
+ b.classList.toggle('active', b.textContent.toLowerCase().includes(tab) ||
613
+ (tab === 'chart' && b.textContent === 'ORG CHART') ||
614
+ (tab === 'roles' && b.textContent === 'ROLES') ||
615
+ (tab === 'activity' && b.textContent === 'ACTIVITY') ||
616
+ (tab === 'health' && b.textContent === 'HEALTH'));
617
+ });
618
+ document.querySelectorAll('.tab-pane').forEach(p => p.classList.remove('active'));
619
+ const pane = document.getElementById('tab-' + tab);
620
+ if (pane) pane.classList.add('active');
621
+ if (orgDetailData) renderTab(tab);
622
+ };
623
+
624
+ // ── Render all orgs in sidebar ────────────────────────────────────
625
+ function renderSidebar() {
626
+ const list = document.getElementById('sb-list');
627
+ const empty = document.getElementById('sb-empty');
628
+ const dot = document.getElementById('sb-live-dot');
629
+ const statusText = document.getElementById('sb-status-text');
630
+ const runCount = document.getElementById('sb-running-count');
631
+
632
+ // Clear existing items
633
+ list.querySelectorAll('.org-item').forEach(el => el.remove());
634
+
635
+ const running = allOrgs.filter(o => o.running).length;
636
+ if (running > 0) {
637
+ dot.classList.add('on');
638
+ statusText.textContent = 'LIVE';
639
+ runCount.textContent = running + ' RUNNING';
640
+ } else {
641
+ dot.classList.remove('on');
642
+ statusText.textContent = allOrgs.length ? 'IDLE' : 'EMPTY';
643
+ runCount.textContent = '';
644
+ }
645
+
646
+ if (!allOrgs.length) {
647
+ empty.style.display = 'block';
648
+ return;
649
+ }
650
+ empty.style.display = 'none';
651
+
652
+ allOrgs.forEach(org => {
653
+ const item = document.createElement('div');
654
+ item.className = 'org-item' +
655
+ (org.running ? ' running' : '') +
656
+ (selectedOrg === org.name ? ' active' : '');
657
+ item.dataset.orgName = org.name;
658
+
659
+ const rolesCount = Array.isArray(org.roles) ? org.roles.length : (org.roles || 0);
660
+ const topo = org.topology || 'hierarchical';
661
+ const goalText = (org.goal || '').slice(0, 55) + ((org.goal || '').length > 55 ? '…' : '');
662
+
663
+ item.innerHTML = `
664
+ <div class="oi-header">
665
+ <div class="oi-dot ${org.running ? 'running' : ''}"></div>
666
+ <div class="oi-name">${esc(org.name)}</div>
667
+ <span class="oi-badge ${org.running ? 'running' : 'idle'}">${org.running ? 'LIVE' : 'IDLE'}</span>
668
+ </div>
669
+ ${goalText ? `<div class="oi-goal">${esc(goalText)}</div>` : ''}
670
+ <div class="oi-meta">
671
+ <span class="oi-chip">${rolesCount} <span>roles</span></span>
672
+ <span class="oi-chip">${esc(topo)}</span>
673
+ </div>`;
674
+
675
+ item.addEventListener('click', () => selectOrg(org.name));
676
+ list.appendChild(item);
677
+ });
678
+ }
679
+
680
+ // ── Select and load an org ────────────────────────────────────────
681
+ async function selectOrg(name) {
682
+ selectedOrg = name;
683
+
684
+ // Update sidebar active state
685
+ document.querySelectorAll('.org-item').forEach(el => {
686
+ el.classList.toggle('active', el.dataset.orgName === name);
687
+ });
688
+
689
+ // Show detail area
690
+ document.getElementById('no-org-state').style.display = 'none';
691
+ const detail = document.getElementById('org-detail');
692
+ detail.style.display = 'flex';
693
+
694
+ // Basic data from list
695
+ const listOrg = allOrgs.find(o => o.name === name) || {};
696
+ updateHeader(listOrg, null);
697
+
698
+ // Fetch full data
699
+ orgDetailData = null;
700
+ try {
701
+ const [mainR, actR, healthR] = await Promise.all([
702
+ fetch(`/api/org/${encodeURIComponent(name)}`),
703
+ fetch(`/api/org/${encodeURIComponent(name)}/activity`).catch(() => null),
704
+ fetch(`/api/org/${encodeURIComponent(name)}/health`).catch(() => null),
705
+ ]);
706
+ if (!mainR.ok) throw new Error('not found');
707
+ const raw = await mainR.json();
708
+ // API returns {config, state, goals, routines, approvals, running, tasks} — flatten config to top level
709
+ orgDetailData = raw.config ? { ...raw.config, ...raw, running: raw.running } : raw;
710
+ orgDetailData._activity = actR && actR.ok ? await actR.json() : [];
711
+ orgDetailData._health = healthR && healthR.ok ? await healthR.json() : null;
712
+ } catch (e) {
713
+ orgDetailData = { ...listOrg, roles: listOrg.roles || [], communication: [] };
714
+ orgDetailData._activity = [];
715
+ orgDetailData._health = null;
716
+ }
717
+
718
+ updateHeader(listOrg, orgDetailData);
719
+ renderTab(currentTab);
720
+ }
721
+
722
+ function updateHeader(listOrg, data) {
723
+ const name = listOrg.name || selectedOrg || '—';
724
+ document.getElementById('org-name').textContent = name.toUpperCase();
725
+
726
+ const running = listOrg.running;
727
+ const badge = document.getElementById('org-status-badge');
728
+ badge.textContent = running ? 'LIVE' : 'IDLE';
729
+ badge.className = 'oi-badge ' + (running ? 'running' : 'idle');
730
+
731
+ const rolesArr = data ? (data.roles || []) : [];
732
+ const rolesCount = Array.isArray(rolesArr) ? rolesArr.length : (listOrg.roles || 0);
733
+ document.getElementById('hdr-roles').textContent = rolesCount;
734
+ document.getElementById('hdr-topology').textContent = (data?.topology || listOrg.topology || '—').toUpperCase();
735
+
736
+ const created = data?.created_at || listOrg.created_at;
737
+ if (created) {
738
+ document.getElementById('hdr-created').textContent = fmtDate(created);
739
+ document.getElementById('hdr-created-wrap').style.display = '';
740
+ }
741
+
742
+ document.getElementById('stop-btn').style.display = running ? '' : 'none';
743
+ }
744
+
745
+ // ── Tab renderers ─────────────────────────────────────────────────
746
+ function renderTab(tab) {
747
+ if (!orgDetailData) return;
748
+ if (tab === 'chart') renderChart();
749
+ else if (tab === 'roles') renderRoles();
750
+ else if (tab === 'activity') renderActivity();
751
+ else if (tab === 'health') renderHealth();
752
+ }
753
+
754
+ // ── CHART TAB ─────────────────────────────────────────────────────
755
+ function renderChart() {
756
+ const d = orgDetailData;
757
+ const roles = Array.isArray(d.roles) ? d.roles : [];
758
+ const comms = Array.isArray(d.communication) ? d.communication : [];
759
+ const running = !!(allOrgs.find(o => o.name === selectedOrg) || {}).running;
760
+
761
+ // Meta bar
762
+ document.getElementById('meta-goal').textContent = d.goal || '—';
763
+ document.getElementById('meta-topology').textContent = (d.topology || '—').toUpperCase();
764
+ document.getElementById('meta-created').textContent = fmtDate(d.created_at);
765
+
766
+ // Build SVG chart
767
+ const svgEl = document.getElementById('org-chart-svg');
768
+ const edgesG = document.getElementById('oc-edges');
769
+ const nodesG = document.getElementById('oc-nodes');
770
+ edgesG.innerHTML = '';
771
+ nodesG.innerHTML = '';
772
+
773
+ if (!roles.length) {
774
+ // Empty chart placeholder
775
+ const text = document.createElementNS('http://www.w3.org/2000/svg', 'text');
776
+ text.setAttribute('x', '360');
777
+ text.setAttribute('y', '160');
778
+ text.setAttribute('text-anchor', 'middle');
779
+ text.setAttribute('fill', 'oklch(36% 0.008 186)');
780
+ text.setAttribute('font-size', '11');
781
+ text.setAttribute('font-family', "'Azeret Mono','Space Mono',monospace");
782
+ text.setAttribute('letter-spacing', '2');
783
+ text.textContent = 'NO ROLES DEFINED';
784
+ nodesG.appendChild(text);
785
+ return;
786
+ }
787
+
788
+ // Layout: compute node positions
789
+ const W = 720, H = 320;
790
+ const topo = (d.topology || 'hierarchical').toLowerCase();
791
+ const positions = computeLayout(roles, comms, topo, W, H);
792
+
793
+ const NS = 'http://www.w3.org/2000/svg';
794
+
795
+ // Draw edges first
796
+ const markerMap = {
797
+ command: 'arr-teal', report: 'arr-indigo', feedback: 'arr-amber', handoff: 'arr-green'
798
+ };
799
+ const strokeMap = {
800
+ command: { color: 'oklch(62% 0.20 186)', opacity: 0.5, width: 1.5, dash: '' },
801
+ report: { color: 'oklch(68% 0.18 252)', opacity: 0.4, width: 1, dash: '4 3' },
802
+ feedback: { color: 'oklch(78% 0.18 80)', opacity: 0.3, width: 1, dash: '2 4' },
803
+ handoff: { color: 'oklch(68% 0.20 150)', opacity: 0.45, width: 1.5, dash: '' },
804
+ };
805
+
806
+ // Build set of edge pairs to detect anti-parallel edges (A→B + B→A)
807
+ const edgePairSet = new Set(comms.map(c => `${c.from}|${c.to}`));
808
+
809
+ comms.forEach(edge => {
810
+ const fromPos = positions[edge.from];
811
+ const toPos = positions[edge.to];
812
+ if (!fromPos || !toPos || edge.from === edge.to) return;
813
+
814
+ const style = strokeMap[edge.type] || strokeMap.command;
815
+ const marker = markerMap[edge.type] || 'arr-teal';
816
+
817
+ // Nudge endpoints toward each edge center to avoid overlap with nodes
818
+ const dx = toPos.x - fromPos.x, dy = toPos.y - fromPos.y;
819
+ const len = Math.sqrt(dx*dx + dy*dy) || 1;
820
+ const R = 24; // node radius to clear
821
+
822
+ // Perpendicular offset for anti-parallel pairs so both edges remain visible
823
+ let px = 0, py = 0;
824
+ if (edgePairSet.has(`${edge.to}|${edge.from}`)) {
825
+ const [canonFrom, canonTo] = edge.from < edge.to ? [edge.from, edge.to] : [edge.to, edge.from];
826
+ const cp = positions[canonFrom], ct = positions[canonTo];
827
+ const cdx = ct.x - cp.x, cdy = ct.y - cp.y;
828
+ const clen = Math.sqrt(cdx*cdx + cdy*cdy) || 1;
829
+ const sign = edge.from === canonFrom ? 1 : -1;
830
+ px = sign * (-cdy / clen) * 6;
831
+ py = sign * ( cdx / clen) * 6;
832
+ }
833
+
834
+ const x1 = fromPos.x + (dx/len)*R + px;
835
+ const y1 = fromPos.y + (dy/len)*R + py;
836
+ const x2 = toPos.x - (dx/len)*(R + 4) + px;
837
+ const y2 = toPos.y - (dy/len)*(R + 4) + py;
838
+
839
+ const line = document.createElementNS(NS, 'line');
840
+ line.setAttribute('x1', x1.toFixed(1));
841
+ line.setAttribute('y1', y1.toFixed(1));
842
+ line.setAttribute('x2', x2.toFixed(1));
843
+ line.setAttribute('y2', y2.toFixed(1));
844
+ line.setAttribute('stroke', style.color);
845
+ line.setAttribute('stroke-opacity', style.opacity);
846
+ line.setAttribute('stroke-width', style.width);
847
+ if (style.dash) line.setAttribute('stroke-dasharray', style.dash);
848
+ line.setAttribute('marker-end', `url(#${marker})`);
849
+ edgesG.appendChild(line);
850
+ });
851
+
852
+ // Draw nodes
853
+ roles.forEach((role, i) => {
854
+ const pos = positions[role.id];
855
+ if (!pos) return;
856
+ const isBoss = !role.reports_to || role.reports_to === role.id;
857
+ const color = roleColor(i);
858
+
859
+ const g = document.createElementNS(NS, 'g');
860
+ g.setAttribute('transform', `translate(${pos.x.toFixed(1)},${pos.y.toFixed(1)})`);
861
+
862
+ // Pulse ring (running state)
863
+ if (running) {
864
+ const pulse = document.createElementNS(NS, 'circle');
865
+ pulse.setAttribute('r', '26');
866
+ pulse.setAttribute('class', 'oc-node-pulse running');
867
+ pulse.setAttribute('stroke', isBoss ? 'oklch(62% 0.20 186)' : 'oklch(68% 0.20 150)');
868
+ g.appendChild(pulse);
869
+ }
870
+
871
+ // Node circle
872
+ const circle = document.createElementNS(NS, 'circle');
873
+ circle.setAttribute('r', '22');
874
+ circle.setAttribute('class', `oc-node-bg ${isBoss ? 'boss' : 'peer'}`);
875
+ circle.setAttribute('stroke', color);
876
+ circle.setAttribute('filter', 'url(#node-glow)');
877
+ g.appendChild(circle);
878
+
879
+ // Role title — abbreviated to fit circle
880
+ const titleText = document.createElementNS(NS, 'text');
881
+ titleText.setAttribute('class', 'oc-node-text');
882
+ titleText.setAttribute('y', '-4');
883
+ const title = role.title || role.id;
884
+ const shortTitle = title.length > 10 ? title.slice(0, 9) + '…' : title;
885
+ titleText.textContent = shortTitle;
886
+ titleText.style.fill = color;
887
+ g.appendChild(titleText);
888
+
889
+ // Agent type
890
+ if (role.agent_type) {
891
+ const subText = document.createElementNS(NS, 'text');
892
+ subText.setAttribute('class', 'oc-node-sub');
893
+ subText.setAttribute('y', '10');
894
+ const agent = role.agent_type.length > 12 ? role.agent_type.slice(0, 11) + '…' : role.agent_type;
895
+ subText.textContent = agent;
896
+ g.appendChild(subText);
897
+ }
898
+
899
+ nodesG.appendChild(g);
900
+ });
901
+ }
902
+
903
+ function computeLayout(roles, comms, topology, W, H) {
904
+ const positions = {};
905
+ const n = roles.length;
906
+ if (n === 0) return positions;
907
+
908
+ const cx = W / 2, cy = H / 2;
909
+ const PAD = 60;
910
+
911
+ if (topology === 'mesh') {
912
+ // All nodes in a circle
913
+ roles.forEach((role, i) => {
914
+ const angle = (2 * Math.PI * i / n) - Math.PI / 2;
915
+ const r = Math.min((W - PAD*2) / 2, (H - PAD*2) / 2) * 0.8;
916
+ positions[role.id] = {
917
+ x: cx + r * Math.cos(angle),
918
+ y: cy + r * Math.sin(angle),
919
+ };
920
+ });
921
+
922
+ } else if (topology === 'star') {
923
+ // Boss in center, others around edge
924
+ const boss = roles.find(r => !r.reports_to) || roles[0];
925
+ const rest = roles.filter(r => r.id !== boss.id);
926
+ positions[boss.id] = { x: cx, y: cy };
927
+ const r2 = Math.min((W - PAD*2) / 2, (H - PAD*2) / 2) * 0.75;
928
+ rest.forEach((role, i) => {
929
+ const angle = (2 * Math.PI * i / rest.length) - Math.PI / 2;
930
+ positions[role.id] = {
931
+ x: cx + r2 * Math.cos(angle),
932
+ y: cy + r2 * Math.sin(angle),
933
+ };
934
+ });
935
+
936
+ } else {
937
+ // Hierarchical: layer by reports_to depth
938
+ const depthMap = {};
939
+ const childMap = {};
940
+ roles.forEach(r => {
941
+ if (!r.reports_to || r.reports_to === r.id) { depthMap[r.id] = 0; }
942
+ const parent = r.reports_to;
943
+ if (parent && parent !== r.id) {
944
+ if (!childMap[parent]) childMap[parent] = [];
945
+ childMap[parent].push(r.id);
946
+ }
947
+ });
948
+ // BFS to assign depths
949
+ let changed = true, iter = 0;
950
+ while (changed && iter < 20) {
951
+ changed = false; iter++;
952
+ roles.forEach(r => {
953
+ if (r.reports_to && r.reports_to !== r.id && depthMap[r.reports_to] !== undefined) {
954
+ const newDepth = (depthMap[r.reports_to] || 0) + 1;
955
+ if (depthMap[r.id] !== newDepth) { depthMap[r.id] = newDepth; changed = true; }
956
+ }
957
+ });
958
+ }
959
+ // Assign depth 0 to any unresolved
960
+ roles.forEach(r => { if (depthMap[r.id] === undefined) depthMap[r.id] = 1; });
961
+
962
+ const maxDepth = Math.max(...Object.values(depthMap));
963
+ const layerMap = {};
964
+ Object.entries(depthMap).forEach(([id, depth]) => {
965
+ if (!layerMap[depth]) layerMap[depth] = [];
966
+ layerMap[depth].push(id);
967
+ });
968
+
969
+ const rowH = maxDepth > 0 ? (H - PAD * 2) / (maxDepth + 1) : H - PAD * 2;
970
+ Object.entries(layerMap).forEach(([depth, ids]) => {
971
+ const y = PAD + Number(depth) * rowH + (maxDepth > 0 ? rowH / 2 : H / 2);
972
+ ids.forEach((id, i) => {
973
+ const colW = (W - PAD * 2) / ids.length;
974
+ const x = PAD + colW * i + colW / 2;
975
+ positions[id] = { x, y };
976
+ });
977
+ });
978
+ }
979
+
980
+ return positions;
981
+ }
982
+
983
+ // ── ROLES TAB ─────────────────────────────────────────────────────
984
+ function renderRoles() {
985
+ const d = orgDetailData;
986
+ const roles = Array.isArray(d.roles) ? d.roles : [];
987
+ const grid = document.getElementById('roles-grid');
988
+ grid.innerHTML = '';
989
+
990
+ if (!roles.length) {
991
+ grid.innerHTML = '<div style="color:var(--dim);font-size:9px;letter-spacing:1px;">NO ROLES DEFINED</div>';
992
+ return;
993
+ }
994
+
995
+ roles.forEach((role, i) => {
996
+ const color = roleColor(i);
997
+ const isBoss = !role.reports_to || role.reports_to === role.id;
998
+ const resps = Array.isArray(role.responsibilities) ? role.responsibilities : [];
999
+
1000
+ const block = document.createElement('div');
1001
+ block.className = 'role-block';
1002
+
1003
+ block.innerHTML = `
1004
+ <div class="rb-header">
1005
+ <div class="rb-dot" style="background:${color}; box-shadow:0 0 4px ${color}50;"></div>
1006
+ <div class="rb-title">${esc(role.title || role.id)}</div>
1007
+ ${role.agent_type ? `<span class="rb-agent">${esc(role.agent_type)}</span>` : ''}
1008
+ </div>
1009
+ <div class="rb-id">id: <span style="color:var(--muted)">${esc(role.id)}</span>${isBoss ? ' <span style="color:var(--teal);font-size:8px;margin-left:4px">BOSS</span>' : ''}</div>
1010
+ ${role.reports_to && !isBoss ? `<div class="rb-reports">reports to: <span>${esc(role.reports_to)}</span></div>` : ''}
1011
+ ${resps.length ? `<div class="rb-resps">${resps.map(r => `<div class="rb-resp-item">${esc(r)}</div>`).join('')}</div>` : ''}
1012
+ `;
1013
+
1014
+ grid.appendChild(block);
1015
+ });
1016
+ }
1017
+
1018
+ // ── ACTIVITY TAB ─────────────────────────────────────────────────
1019
+ function renderActivity() {
1020
+ const log = document.getElementById('activity-log');
1021
+ const activity = orgDetailData._activity;
1022
+
1023
+ // Also inject org event log from SSE-captured events
1024
+ const orgEvents = orgEventLog[selectedOrg] || [];
1025
+
1026
+ // Merge and sort
1027
+ let events = [];
1028
+ if (Array.isArray(activity)) events = [...activity];
1029
+ events.push(...orgEvents);
1030
+ events.sort((a, b) => (b.ts || 0) - (a.ts || 0));
1031
+
1032
+ if (!events.length) {
1033
+ log.innerHTML = '<div class="act-empty">NO ACTIVITY RECORDED</div>';
1034
+ return;
1035
+ }
1036
+
1037
+ log.innerHTML = events.slice(0, 80).map(ev => {
1038
+ const t = fmtTime(ev.ts);
1039
+ const type = fmtEventType(ev.type);
1040
+ const detail = ev.role ? ev.role : ev.msg ? ev.msg : ev.agent ? ev.agent : '';
1041
+ return `<div class="act-row">
1042
+ <span class="act-time">${esc(t)}</span>
1043
+ <span class="act-type">${esc(type)}</span>
1044
+ <span class="act-msg">${esc(detail)}</span>
1045
+ </div>`;
1046
+ }).join('');
1047
+ }
1048
+
1049
+ // ── HEALTH TAB ─────────────────────────────────────────────────────
1050
+ function renderHealth() {
1051
+ const health = orgDetailData._health;
1052
+ const listOrg = allOrgs.find(o => o.name === selectedOrg) || {};
1053
+ const roles = Array.isArray(orgDetailData.roles) ? orgDetailData.roles : [];
1054
+ const running = listOrg.running;
1055
+
1056
+ const grid = document.getElementById('health-grid');
1057
+ const detail = document.getElementById('health-detail');
1058
+
1059
+ grid.innerHTML = `
1060
+ <div class="health-cell">
1061
+ <div class="hc-label">STATUS</div>
1062
+ <div class="hc-value ${running ? 'green' : ''}">${running ? 'LIVE' : 'IDLE'}</div>
1063
+ </div>
1064
+ <div class="health-cell">
1065
+ <div class="hc-label">ROLES</div>
1066
+ <div class="hc-value">${roles.length}</div>
1067
+ </div>
1068
+ <div class="health-cell">
1069
+ <div class="hc-label">TOPOLOGY</div>
1070
+ <div class="hc-value" style="font-size:13px">${esc((orgDetailData.topology || '—').toUpperCase())}</div>
1071
+ </div>
1072
+ ${health?.agents_active !== undefined ? `
1073
+ <div class="health-cell">
1074
+ <div class="hc-label">AGENTS</div>
1075
+ <div class="hc-value ${health.agents_active > 0 ? 'green' : ''}">${health.agents_active}</div>
1076
+ <div class="hc-sub">active</div>
1077
+ </div>` : ''}
1078
+ ${health?.tasks_pending !== undefined ? `
1079
+ <div class="health-cell">
1080
+ <div class="hc-label">TASKS</div>
1081
+ <div class="hc-value ${health.tasks_pending > 5 ? 'amber' : ''}">${health.tasks_pending}</div>
1082
+ <div class="hc-sub">pending</div>
1083
+ </div>` : ''}
1084
+ `;
1085
+
1086
+ if (health?.errors && health.errors.length) {
1087
+ detail.innerHTML = `
1088
+ <div style="margin-top:16px">
1089
+ <div style="font-size:8px;letter-spacing:2px;color:var(--dim);margin-bottom:8px">RECENT ERRORS</div>
1090
+ ${health.errors.slice(0,5).map(e => `
1091
+ <div style="font-size:9px;color:var(--red);padding:5px 0;border-bottom:1px solid oklch(62% 0.22 25 / 0.1)">
1092
+ ${esc(e)}
1093
+ </div>`).join('')}
1094
+ </div>`;
1095
+ } else {
1096
+ detail.innerHTML = health ? `
1097
+ <div style="margin-top:16px;font-size:9px;color:var(--dim);letter-spacing:1px;">
1098
+ ${running ? 'ORG IS RUNNING — NO ERRORS DETECTED' : 'ORG IS IDLE'}
1099
+ </div>` : `
1100
+ <div style="margin-top:16px;font-size:9px;color:var(--dim);letter-spacing:1px;">
1101
+ HEALTH DATA UNAVAILABLE
1102
+ </div>`;
1103
+ }
1104
+ }
1105
+
1106
+ // ── Stop org action ────────────────────────────────────────────────
1107
+ window.stopCurrentOrg = async function() {
1108
+ if (!selectedOrg) return;
1109
+ try {
1110
+ await fetch(`/api/orgs/${encodeURIComponent(selectedOrg)}/stop`, { method: 'POST' });
1111
+ setTimeout(loadOrgs, 600);
1112
+ } catch (_) {}
1113
+ };
1114
+
1115
+ // ── SSE org events ────────────────────────────────────────────────
1116
+ const orgEventLog = {};
1117
+
1118
+ let evtSource = null;
1119
+ function connectSSE() {
1120
+ if (evtSource) evtSource.close();
1121
+ evtSource = new EventSource('/api/events');
1122
+ evtSource.onmessage = e => {
1123
+ try {
1124
+ const ev = JSON.parse(e.data);
1125
+ if (ev && typeof ev.org === 'string' && ev.type && ev.type.startsWith('org:')) {
1126
+ const name = ev.org;
1127
+ if (!orgEventLog[name]) orgEventLog[name] = [];
1128
+ orgEventLog[name].push(ev);
1129
+ if (orgEventLog[name].length > 50) orgEventLog[name].shift();
1130
+
1131
+ // Refresh if this org is selected
1132
+ if (name === selectedOrg && currentTab === 'activity') {
1133
+ renderActivity();
1134
+ }
1135
+ // Reload org list if start/stop event
1136
+ if (ev.type === 'org:start' || ev.type === 'org:stop') {
1137
+ setTimeout(loadOrgs, 400);
1138
+ }
1139
+ }
1140
+ } catch (_) {}
1141
+ };
1142
+ evtSource.onerror = () => {
1143
+ setTimeout(connectSSE, 5000);
1144
+ };
1145
+ }
1146
+
1147
+ // ── Data loading ──────────────────────────────────────────────────
1148
+ async function loadOrgs() {
1149
+ try {
1150
+ const r = await fetch('/api/orgs');
1151
+ allOrgs = await r.json();
1152
+ if (!Array.isArray(allOrgs)) allOrgs = [];
1153
+ } catch (_) {
1154
+ allOrgs = [];
1155
+ }
1156
+ renderSidebar();
1157
+
1158
+ // Re-render detail if selected org changed running state
1159
+ if (selectedOrg && orgDetailData) {
1160
+ const listOrg = allOrgs.find(o => o.name === selectedOrg);
1161
+ if (listOrg) updateHeader(listOrg, orgDetailData);
1162
+ }
1163
+ }
1164
+
1165
+ // ── Bootstrap ─────────────────────────────────────────────────────
1166
+ connectSSE();
1167
+ loadOrgs();
1168
+ setInterval(loadOrgs, 8000);
1169
+ </script>
1170
+ </body>
1171
+ </html>