ideaco 1.1.5

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 (159) hide show
  1. package/.dockerignore +33 -0
  2. package/.nvmrc +1 -0
  3. package/ARCHITECTURE.md +394 -0
  4. package/Dockerfile +50 -0
  5. package/LICENSE +29 -0
  6. package/README.md +206 -0
  7. package/bin/i18n.js +46 -0
  8. package/bin/ideaco.js +494 -0
  9. package/deploy.sh +15 -0
  10. package/docker-compose.yml +30 -0
  11. package/electron/main.cjs +986 -0
  12. package/electron/preload.cjs +14 -0
  13. package/electron/web-backends.cjs +854 -0
  14. package/jsconfig.json +8 -0
  15. package/next.config.mjs +34 -0
  16. package/package.json +134 -0
  17. package/postcss.config.mjs +6 -0
  18. package/public/demo/dashboard.png +0 -0
  19. package/public/demo/employee.png +0 -0
  20. package/public/demo/messages.png +0 -0
  21. package/public/demo/office.png +0 -0
  22. package/public/demo/requirement.png +0 -0
  23. package/public/logo.jpeg +0 -0
  24. package/public/logo.png +0 -0
  25. package/scripts/prepare-electron.js +67 -0
  26. package/scripts/release.js +76 -0
  27. package/src/app/api/agents/[agentId]/chat/route.js +70 -0
  28. package/src/app/api/agents/[agentId]/conversations/route.js +35 -0
  29. package/src/app/api/agents/[agentId]/route.js +106 -0
  30. package/src/app/api/avatar/route.js +104 -0
  31. package/src/app/api/browse-dir/route.js +44 -0
  32. package/src/app/api/chat/route.js +265 -0
  33. package/src/app/api/company/factory-reset/route.js +43 -0
  34. package/src/app/api/company/route.js +82 -0
  35. package/src/app/api/departments/[deptId]/agents/[agentId]/dismiss/route.js +19 -0
  36. package/src/app/api/departments/route.js +92 -0
  37. package/src/app/api/group-chat-loop/events/route.js +70 -0
  38. package/src/app/api/group-chat-loop/route.js +94 -0
  39. package/src/app/api/mailbox/route.js +100 -0
  40. package/src/app/api/messages/route.js +14 -0
  41. package/src/app/api/providers/[id]/configure/route.js +21 -0
  42. package/src/app/api/providers/[id]/refresh-cookie/route.js +38 -0
  43. package/src/app/api/providers/[id]/test-cookie/route.js +28 -0
  44. package/src/app/api/providers/route.js +11 -0
  45. package/src/app/api/requirements/route.js +242 -0
  46. package/src/app/api/secretary/route.js +65 -0
  47. package/src/app/api/system/cli-backends/route.js +91 -0
  48. package/src/app/api/system/cron/route.js +110 -0
  49. package/src/app/api/system/knowledge/route.js +104 -0
  50. package/src/app/api/system/plugins/route.js +40 -0
  51. package/src/app/api/system/skills/route.js +46 -0
  52. package/src/app/api/system/status/route.js +46 -0
  53. package/src/app/api/talent-market/[profileId]/recall/route.js +22 -0
  54. package/src/app/api/talent-market/[profileId]/route.js +17 -0
  55. package/src/app/api/talent-market/route.js +26 -0
  56. package/src/app/api/teams/route.js +773 -0
  57. package/src/app/api/ws-files/[departmentId]/file/route.js +27 -0
  58. package/src/app/api/ws-files/[departmentId]/files/route.js +22 -0
  59. package/src/app/globals.css +130 -0
  60. package/src/app/layout.jsx +40 -0
  61. package/src/app/page.jsx +97 -0
  62. package/src/components/AgentChatModal.jsx +164 -0
  63. package/src/components/AgentDetailModal.jsx +425 -0
  64. package/src/components/AgentSpyModal.jsx +481 -0
  65. package/src/components/AvatarGrid.jsx +29 -0
  66. package/src/components/BossProfileModal.jsx +162 -0
  67. package/src/components/CachedAvatar.jsx +77 -0
  68. package/src/components/ChatPanel.jsx +219 -0
  69. package/src/components/ChatShared.jsx +255 -0
  70. package/src/components/DepartmentDetail.jsx +842 -0
  71. package/src/components/DepartmentView.jsx +367 -0
  72. package/src/components/FileReference.jsx +260 -0
  73. package/src/components/FilesView.jsx +465 -0
  74. package/src/components/GroupChatView.jsx +799 -0
  75. package/src/components/Mailbox.jsx +926 -0
  76. package/src/components/MessagesView.jsx +112 -0
  77. package/src/components/OnboardingGuide.jsx +209 -0
  78. package/src/components/OrgTree.jsx +151 -0
  79. package/src/components/Overview.jsx +391 -0
  80. package/src/components/PixelOffice.jsx +2281 -0
  81. package/src/components/ProviderGrid.jsx +551 -0
  82. package/src/components/ProvidersBoard.jsx +16 -0
  83. package/src/components/RequirementDetail.jsx +1279 -0
  84. package/src/components/RequirementsBoard.jsx +187 -0
  85. package/src/components/SecretarySettings.jsx +295 -0
  86. package/src/components/SetupWizard.jsx +388 -0
  87. package/src/components/Sidebar.jsx +169 -0
  88. package/src/components/SystemMonitor.jsx +808 -0
  89. package/src/components/TalentMarket.jsx +183 -0
  90. package/src/components/TeamDetail.jsx +697 -0
  91. package/src/core/agent/base-agent.js +104 -0
  92. package/src/core/agent/chat-store.js +602 -0
  93. package/src/core/agent/cli-agent/backends/claude-code/README.md +52 -0
  94. package/src/core/agent/cli-agent/backends/claude-code/config.js +27 -0
  95. package/src/core/agent/cli-agent/backends/codebuddy/README.md +236 -0
  96. package/src/core/agent/cli-agent/backends/codebuddy/config.js +27 -0
  97. package/src/core/agent/cli-agent/backends/codex/README.md +51 -0
  98. package/src/core/agent/cli-agent/backends/codex/config.js +27 -0
  99. package/src/core/agent/cli-agent/backends/index.js +27 -0
  100. package/src/core/agent/cli-agent/backends/registry.js +580 -0
  101. package/src/core/agent/cli-agent/index.js +154 -0
  102. package/src/core/agent/index.js +60 -0
  103. package/src/core/agent/llm-agent/client.js +320 -0
  104. package/src/core/agent/llm-agent/index.js +97 -0
  105. package/src/core/agent/message-bus.js +211 -0
  106. package/src/core/agent/session.js +608 -0
  107. package/src/core/agent/tools.js +596 -0
  108. package/src/core/agent/web-agent/backends/base-backend.js +180 -0
  109. package/src/core/agent/web-agent/backends/chatgpt/client.js +146 -0
  110. package/src/core/agent/web-agent/backends/chatgpt/config.js +148 -0
  111. package/src/core/agent/web-agent/backends/chatgpt/dom-scripts.js +303 -0
  112. package/src/core/agent/web-agent/backends/index.js +91 -0
  113. package/src/core/agent/web-agent/index.js +278 -0
  114. package/src/core/agent/web-agent/web-client.js +407 -0
  115. package/src/core/employee/base-employee.js +1088 -0
  116. package/src/core/employee/index.js +35 -0
  117. package/src/core/employee/knowledge.js +327 -0
  118. package/src/core/employee/lifecycle.js +990 -0
  119. package/src/core/employee/memory/index.js +642 -0
  120. package/src/core/employee/memory/store.js +143 -0
  121. package/src/core/employee/performance.js +224 -0
  122. package/src/core/employee/secretary.js +625 -0
  123. package/src/core/employee/skills.js +398 -0
  124. package/src/core/index.js +38 -0
  125. package/src/core/organization/company.js +2600 -0
  126. package/src/core/organization/department.js +737 -0
  127. package/src/core/organization/group-chat-loop.js +264 -0
  128. package/src/core/organization/index.js +8 -0
  129. package/src/core/organization/persistence.js +111 -0
  130. package/src/core/organization/team.js +267 -0
  131. package/src/core/organization/workforce/hr.js +377 -0
  132. package/src/core/organization/workforce/providers.js +468 -0
  133. package/src/core/organization/workforce/role-archetypes.js +805 -0
  134. package/src/core/organization/workforce/talent-market.js +205 -0
  135. package/src/core/prompts.js +532 -0
  136. package/src/core/requirement.js +1789 -0
  137. package/src/core/system/audit.js +483 -0
  138. package/src/core/system/cron.js +449 -0
  139. package/src/core/system/index.js +7 -0
  140. package/src/core/system/plugin.js +2183 -0
  141. package/src/core/utils/json-parse.js +188 -0
  142. package/src/core/workspace.js +239 -0
  143. package/src/lib/api-i18n.js +211 -0
  144. package/src/lib/avatar.js +268 -0
  145. package/src/lib/client-store.js +1025 -0
  146. package/src/lib/config-validator.js +483 -0
  147. package/src/lib/format-time.js +22 -0
  148. package/src/lib/hooks.js +414 -0
  149. package/src/lib/i18n.js +134 -0
  150. package/src/lib/paths.js +23 -0
  151. package/src/lib/store.js +72 -0
  152. package/src/locales/de.js +393 -0
  153. package/src/locales/en.js +1054 -0
  154. package/src/locales/es.js +393 -0
  155. package/src/locales/fr.js +393 -0
  156. package/src/locales/ja.js +501 -0
  157. package/src/locales/ko.js +513 -0
  158. package/src/locales/zh.js +828 -0
  159. package/tailwind.config.mjs +11 -0
@@ -0,0 +1,264 @@
1
+ /**
2
+ * Group Chat Loop — Thin Global Coordinator
3
+ *
4
+ * This module is now a lightweight shell that:
5
+ * - Holds the company reference and running state
6
+ * - Registers / unregisters employees
7
+ * - Delegates all per-employee behaviour to EmployeeLifecycle
8
+ * - Emits events for the frontend (monologue:start, monologue:end, etc.)
9
+ * - Provides aggregate queries (all active thinkers, etc.)
10
+ * - Handles serialization/deserialization of all lifecycle states
11
+ *
12
+ * All the heavy logic (poll cycles, flow thinking, anti-spam, prompt building,
13
+ * fallback, self-check, idle-chat) now lives in employee/lifecycle.js.
14
+ */
15
+
16
+ import EventEmitter from 'eventemitter3';
17
+
18
+ /**
19
+ * GroupChatLoop — Global Chat Loop Coordinator
20
+ */
21
+ export class GroupChatLoop extends EventEmitter {
22
+ constructor() {
23
+ super();
24
+ this.company = null;
25
+ this.running = false;
26
+
27
+ // agentId → EmployeeLifecycle (populated when employees join)
28
+ this._lifecycles = new Map();
29
+ }
30
+
31
+ // ========================================================================
32
+ // Lifecycle management
33
+ // ========================================================================
34
+
35
+ /**
36
+ * Start the coordinator.
37
+ * @param {object} company
38
+ */
39
+ start(company) {
40
+ if (this.running) return;
41
+ this.company = company;
42
+ this.running = true;
43
+ console.log('🔄 GroupChatLoop: Chat loop engine started');
44
+ this.emit('started');
45
+ }
46
+
47
+ /**
48
+ * Stop the coordinator and all employee lifecycles.
49
+ */
50
+ stop() {
51
+ this.running = false;
52
+ for (const [, lifecycle] of this._lifecycles) {
53
+ lifecycle.stop();
54
+ }
55
+ console.log('⏹️ GroupChatLoop: Chat loop engine stopped');
56
+ this.emit('stopped');
57
+ }
58
+
59
+ // ========================================================================
60
+ // Agent registration
61
+ // ========================================================================
62
+
63
+ /**
64
+ * Start the poll loop for an employee.
65
+ * The employee is expected to have a `.lifecycle` (EmployeeLifecycle) property.
66
+ */
67
+ startAgentLoop(agent) {
68
+ if (!this.running) return;
69
+ if (this._lifecycles.has(agent.id)) return; // already registered
70
+
71
+ const lifecycle = agent.lifecycle;
72
+ if (!lifecycle) {
73
+ console.warn(` ⚠️ [GroupChatLoop] ${agent.name} has no lifecycle, skipping`);
74
+ return;
75
+ }
76
+
77
+ lifecycle.setCoordinator(this);
78
+
79
+ // Restore any pending state from persistence
80
+ if (this._pendingRestore?.has(agent.id)) {
81
+ lifecycle.restore(this._pendingRestore.get(agent.id));
82
+ this._pendingRestore.delete(agent.id);
83
+ }
84
+
85
+ lifecycle.start();
86
+ this._lifecycles.set(agent.id, lifecycle);
87
+
88
+ // NOTE: wakeUp() follows lazy-loading principle.
89
+ // Employees are NOT woken up here — they will be automatically
90
+ // woken up on their first chat() call via _ensureSession().
91
+ // This avoids unnecessary session initialization for idle employees.
92
+
93
+ console.log(` 🔄 [GroupChatLoop] ${agent.name} joined chat loop`);
94
+ }
95
+
96
+ /**
97
+ * Stop the poll loop for an employee.
98
+ */
99
+ stopAgentLoop(agentId) {
100
+ const lifecycle = this._lifecycles.get(agentId);
101
+ if (lifecycle) {
102
+ lifecycle.stop();
103
+ this._lifecycles.delete(agentId);
104
+ }
105
+ }
106
+
107
+ // ========================================================================
108
+ // Triggering
109
+ // ========================================================================
110
+
111
+ /**
112
+ * Trigger an employee to check a group (e.g. on @mention).
113
+ * The employee will be lazily woken up via _ensureSession() when they chat().
114
+ */
115
+ async triggerImmediate(agentId, groupId, _triggerMessage) {
116
+ if (!this.running || !this.company) return;
117
+ const lifecycle = this._lifecycles.get(agentId);
118
+ if (!lifecycle) return;
119
+ await lifecycle.triggerCheck(groupId);
120
+ }
121
+
122
+ /**
123
+ * Nudge an employee to check a group (lower urgency, used internally by lifecycles).
124
+ * The employee will be lazily woken up via _ensureSession() when they chat().
125
+ */
126
+ async nudgeAgent(agentId, groupId) {
127
+ if (!this.running || !this.company) return;
128
+ const lifecycle = this._lifecycles.get(agentId);
129
+ if (!lifecycle) {
130
+ // Agent might not have a lifecycle yet (e.g. hasn't joined loop)
131
+ // Try to find the employee and trigger directly
132
+ const agent = this._findAgent(agentId);
133
+ if (agent?.lifecycle) {
134
+ agent.lifecycle.setCoordinator(this);
135
+ await agent.lifecycle._processGroupMessages(groupId, false);
136
+ }
137
+ return;
138
+ }
139
+ // Use processGroupMessages directly (no mention flag)
140
+ await lifecycle._processGroupMessages(groupId, false).catch(() => {});
141
+ }
142
+
143
+ // ========================================================================
144
+ // Queries
145
+ // ========================================================================
146
+
147
+ /**
148
+ * Get an employee's active monologue for a group.
149
+ */
150
+ getActiveMonologue(agentId, groupId) {
151
+ const lifecycle = this._lifecycles.get(agentId);
152
+ return lifecycle?.getActiveMonologue(groupId) || null;
153
+ }
154
+
155
+ /**
156
+ * Get an employee's monologue history for a group.
157
+ */
158
+ getMonologueHistory(agentId, groupId, limit = 10) {
159
+ const lifecycle = this._lifecycles.get(agentId);
160
+ return lifecycle?.getMonologueHistory(groupId, limit) || [];
161
+ }
162
+
163
+ /**
164
+ * Get all employees currently in flow state across all groups.
165
+ */
166
+ getActiveThinkingAgents() {
167
+ const result = [];
168
+ for (const [, lifecycle] of this._lifecycles) {
169
+ result.push(...lifecycle.getActiveThinking());
170
+ }
171
+ return result;
172
+ }
173
+
174
+ // ========================================================================
175
+ // Serialization
176
+ // ========================================================================
177
+
178
+ /**
179
+ * Serialize all lifecycle states for persistence.
180
+ */
181
+ serialize() {
182
+ // Aggregate all lifecycle states keyed by agentId
183
+ // But we need backward-compatible format: flat maps keyed by `${agentId}:${groupId}`
184
+ const lastReadIndex = {};
185
+ const lastProcessedVisible = {};
186
+ const agentMemory = {};
187
+
188
+ for (const [agentId, lifecycle] of this._lifecycles) {
189
+ const state = lifecycle.serialize();
190
+ // Re-key from groupId to `${agentId}:${groupId}` for backward compat
191
+ for (const [groupId, val] of Object.entries(state.lastReadIndex)) {
192
+ lastReadIndex[`${agentId}:${groupId}`] = val;
193
+ }
194
+ for (const [groupId, val] of Object.entries(state.lastProcessedVisible)) {
195
+ lastProcessedVisible[`${agentId}:${groupId}`] = val;
196
+ }
197
+ for (const [groupId, val] of Object.entries(state.agentMemory)) {
198
+ agentMemory[`${agentId}:${groupId}`] = val;
199
+ }
200
+ }
201
+
202
+ return { lastReadIndex, lastProcessedVisible, agentMemory };
203
+ }
204
+
205
+ /**
206
+ * Restore lifecycle states from persistence data.
207
+ * Called after employees are loaded — distributes state to each lifecycle.
208
+ */
209
+ restore(data) {
210
+ if (!data) return;
211
+
212
+ // Parse the flat `${agentId}:${groupId}` keys back into per-agent maps
213
+ const perAgent = new Map(); // agentId → { lastReadIndex, lastProcessedVisible, agentMemory }
214
+
215
+ const distribute = (flatMap, field) => {
216
+ if (!flatMap) return;
217
+ for (const [compositeKey, val] of Object.entries(flatMap)) {
218
+ const sepIdx = compositeKey.indexOf(':');
219
+ if (sepIdx === -1) continue;
220
+ const agentId = compositeKey.slice(0, sepIdx);
221
+ const groupId = compositeKey.slice(sepIdx + 1);
222
+ if (!perAgent.has(agentId)) {
223
+ perAgent.set(agentId, { lastReadIndex: {}, lastProcessedVisible: {}, agentMemory: {} });
224
+ }
225
+ perAgent.get(agentId)[field][groupId] = val;
226
+ }
227
+ };
228
+
229
+ distribute(data.lastReadIndex, 'lastReadIndex');
230
+ distribute(data.lastProcessedVisible, 'lastProcessedVisible');
231
+ distribute(data.agentMemory, 'agentMemory');
232
+
233
+ // Now distribute to lifecycles that are already registered
234
+ for (const [agentId, agentData] of perAgent) {
235
+ const lifecycle = this._lifecycles.get(agentId);
236
+ if (lifecycle) {
237
+ lifecycle.restore(agentData);
238
+ } else {
239
+ // Store for later — when the agent joins, we'll restore then
240
+ if (!this._pendingRestore) this._pendingRestore = new Map();
241
+ this._pendingRestore.set(agentId, agentData);
242
+ }
243
+ }
244
+ }
245
+
246
+ // ========================================================================
247
+ // Internal helpers
248
+ // ========================================================================
249
+
250
+ /**
251
+ * Find an employee by ID across all departments.
252
+ */
253
+ _findAgent(agentId) {
254
+ if (!this.company) return null;
255
+ for (const dept of this.company.departments.values()) {
256
+ const agent = dept.agents.get(agentId);
257
+ if (agent) return agent;
258
+ }
259
+ return null;
260
+ }
261
+ }
262
+
263
+ // Global singleton
264
+ export const groupChatLoop = new GroupChatLoop();
@@ -0,0 +1,8 @@
1
+ /**
2
+ * Organization Module - Company structure & governance
3
+ * Contains Company, Department, Team/Sprint management, HR
4
+ */
5
+ export { Company } from './company.js';
6
+ export { Department } from './department.js';
7
+ export { TeamManager, Team, Sprint, SprintStatus } from './team.js';
8
+ export { HRSystem, JobTemplates } from './workforce/hr.js';
@@ -0,0 +1,111 @@
1
+ /**
2
+ * Persistence Storage Module
3
+ *
4
+ * Serializes Company's full state to disk (JSON), auto-restores after server restart.
5
+ * Storage location: under project root data/ directory.
6
+ */
7
+ import fs from 'fs';
8
+ import { saveAllAgentMemories, loadAgentMemory, clearAllMemories } from '../employee/memory/store.js';
9
+ import { DATA_DIR, STATE_FILE, BACKUP_FILE, CHATS_DIR } from '../../lib/paths.js';
10
+
11
+ // Ensure data directory exists
12
+ if (!fs.existsSync(DATA_DIR)) {
13
+ fs.mkdirSync(DATA_DIR, { recursive: true });
14
+ }
15
+
16
+ /**
17
+ * Save company state to disk
18
+ * @param {Company} company - Company instance
19
+ */
20
+ export function saveState(company) {
21
+ if (!company) return;
22
+ try {
23
+ // Collect all Agent memories, save to separate files
24
+ const agentMemories = [];
25
+ company.departments.forEach(dept => {
26
+ dept.getMembers().forEach(agent => {
27
+ agentMemories.push({
28
+ agentId: agent.id,
29
+ memoryData: agent.memory.serialize(),
30
+ meta: { name: agent.name, role: agent.role, department: dept.name },
31
+ });
32
+ });
33
+ });
34
+ // Secretary's memory also saved separately
35
+ if (company.secretary) {
36
+ agentMemories.push({
37
+ agentId: company.secretary.id,
38
+ memoryData: company.secretary.memory.serialize(),
39
+ meta: { name: company.secretary.name, role: 'Personal Secretary', department: 'HQ' },
40
+ });
41
+ }
42
+ saveAllAgentMemories(agentMemories);
43
+
44
+ const serialized = company.serialize();
45
+ const json = JSON.stringify(serialized, null, 2);
46
+
47
+ // Backup old file first
48
+ if (fs.existsSync(STATE_FILE)) {
49
+ fs.copyFileSync(STATE_FILE, BACKUP_FILE);
50
+ }
51
+
52
+ // Write new state
53
+ fs.writeFileSync(STATE_FILE, json, 'utf-8');
54
+ console.log(`💾 State persisted (${(json.length / 1024).toFixed(1)}KB)`);
55
+ } catch (e) {
56
+ console.error('❌ Persistence failed:', e.message);
57
+ }
58
+ }
59
+
60
+ /**
61
+ * Load company state from disk
62
+ * @returns {object|null} Serialized state data, or null
63
+ */
64
+ export function loadState() {
65
+ try {
66
+ let filePath = STATE_FILE;
67
+ if (!fs.existsSync(filePath)) {
68
+ filePath = BACKUP_FILE;
69
+ if (!fs.existsSync(filePath)) return null;
70
+ }
71
+
72
+ const json = fs.readFileSync(filePath, 'utf-8');
73
+ const data = JSON.parse(json);
74
+ console.log(`📂 State loaded from disk: ${data.name || 'unknown'}`);
75
+ return data;
76
+ } catch (e) {
77
+ console.error('❌ Failed to load state:', e.message);
78
+ return null;
79
+ }
80
+ }
81
+
82
+ /**
83
+ * Delete persisted data (reset)
84
+ */
85
+ export function clearState() {
86
+ try {
87
+ if (fs.existsSync(STATE_FILE)) fs.unlinkSync(STATE_FILE);
88
+ if (fs.existsSync(BACKUP_FILE)) fs.unlinkSync(BACKUP_FILE);
89
+ clearAllMemories();
90
+ // Clear all chat files (including group chats stored in data/chats/)
91
+ if (fs.existsSync(CHATS_DIR)) {
92
+ fs.rmSync(CHATS_DIR, { recursive: true, force: true });
93
+ fs.mkdirSync(CHATS_DIR, { recursive: true });
94
+ }
95
+ console.log('🗑️ Persisted data cleared (including memory, chat files)');
96
+ } catch (e) {
97
+ console.error('❌ Clear failed:', e.message);
98
+ }
99
+ }
100
+
101
+ /**
102
+ * Debounced save: prevent frequent disk writes
103
+ */
104
+ let saveTimer = null;
105
+ export function debouncedSave(company, delay = 2000) {
106
+ if (saveTimer) clearTimeout(saveTimer);
107
+ saveTimer = setTimeout(() => {
108
+ saveState(company);
109
+ saveTimer = null;
110
+ }, delay);
111
+ }
@@ -0,0 +1,267 @@
1
+ import { v4 as uuidv4 } from 'uuid';
2
+ import { chatStore } from '../agent/chat-store.js';
3
+
4
+ /**
5
+ * Sprint status enum
6
+ */
7
+ export const SprintStatus = {
8
+ DRAFT: 'draft', // Sprint draft, not started yet
9
+ DISCUSSING: 'discussing', // Leader initiates group discussion on sprint plan
10
+ PENDING_APPROVAL: 'pending_approval', // Discussion complete, waiting for Boss approval
11
+ IN_PROGRESS: 'in_progress', // Boss approved, sprint in progress
12
+ COMPLETED: 'completed', // Sprint completed
13
+ FAILED: 'failed', // Sprint failed
14
+ };
15
+
16
+ /**
17
+ * Sprint — similar to a Requirement, one iteration cycle
18
+ */
19
+ export class Sprint {
20
+ constructor({ title, goal, teamId, teamName }) {
21
+ this.id = uuidv4();
22
+ this.title = title;
23
+ this.goal = goal; // Sprint goal
24
+ this.teamId = teamId;
25
+ this.teamName = teamName;
26
+ this.status = SprintStatus.DRAFT;
27
+ this.plan = null; // Sprint plan formed after discussion
28
+ this.requirementId = null; // Standard requirement ID created after approval
29
+ this.workflow = null; // (deprecated) workflow is managed by the associated requirement
30
+ this.groupChat = []; // Sprint group chat
31
+ this.outputs = []; // Outputs
32
+ this.createdAt = new Date();
33
+ this.startedAt = null;
34
+ this.completedAt = null;
35
+ this.summary = null;
36
+
37
+ // Live progress
38
+ this.liveStatus = {
39
+ currentNodeId: null,
40
+ currentNodeTitle: null,
41
+ currentAgent: null,
42
+ currentAction: null,
43
+ lastActiveAt: null,
44
+ heartbeat: null,
45
+ toolCallsInProgress: [],
46
+ recentFileChanges: [],
47
+ };
48
+ }
49
+
50
+ updateLiveStatus(updates) {
51
+ Object.assign(this.liveStatus, updates, { lastActiveAt: new Date(), heartbeat: new Date() });
52
+ }
53
+
54
+ addFileChange(agentName, filePath, action = 'write') {
55
+ this.liveStatus.recentFileChanges.push({ agentName, filePath, action, time: new Date() });
56
+ if (this.liveStatus.recentFileChanges.length > 100) {
57
+ this.liveStatus.recentFileChanges = this.liveStatus.recentFileChanges.slice(-100);
58
+ }
59
+ this.liveStatus.lastActiveAt = new Date();
60
+ this.liveStatus.heartbeat = new Date();
61
+ }
62
+
63
+ addGroupMessage(from, content, type = 'message', visibility = null) {
64
+ const resolvedVisibility = visibility || (type === 'tool_call' || type === 'output' ? 'flow' : 'group');
65
+ const msg = {
66
+ id: uuidv4(),
67
+ from: {
68
+ id: from.id || 'system',
69
+ name: from.name || 'System',
70
+ avatar: from.avatar || null,
71
+ role: from.role || null,
72
+ },
73
+ content,
74
+ type,
75
+ visibility: resolvedVisibility,
76
+ time: new Date(),
77
+ };
78
+ this.groupChat.push(msg);
79
+ // Persist to file storage
80
+ try { chatStore.appendGroupMessage(`sprint-${this.id}`, msg); } catch {}
81
+ this.liveStatus.heartbeat = new Date();
82
+ this.liveStatus.lastActiveAt = new Date();
83
+ }
84
+
85
+ addOutput(agentId, agentName, role, outputType, content, metadata = {}) {
86
+ this.outputs.push({
87
+ id: uuidv4(), agentId, agentName, role, outputType, content, metadata, createdAt: new Date(),
88
+ });
89
+ }
90
+
91
+ serialize() {
92
+ return {
93
+ id: this.id,
94
+ title: this.title,
95
+ goal: this.goal,
96
+ teamId: this.teamId,
97
+ teamName: this.teamName,
98
+ status: this.status,
99
+ plan: this.plan,
100
+ requirementId: this.requirementId,
101
+ workflow: this.workflow,
102
+ // groupChat is persisted in chatStore files (data/chats/group-sprint-{id}/)
103
+ outputs: this.outputs,
104
+ createdAt: this.createdAt,
105
+ startedAt: this.startedAt,
106
+ completedAt: this.completedAt,
107
+ summary: this.summary,
108
+ liveStatus: this.liveStatus,
109
+ };
110
+ }
111
+
112
+ static deserialize(data) {
113
+ const s = new Sprint({
114
+ title: data.title,
115
+ goal: data.goal,
116
+ teamId: data.teamId,
117
+ teamName: data.teamName,
118
+ });
119
+ s.id = data.id;
120
+ s.status = data.status;
121
+ s.plan = data.plan;
122
+ s.requirementId = data.requirementId || null;
123
+ s.workflow = data.workflow;
124
+
125
+ // Load groupChat from file storage; migrate legacy inline data if present
126
+ const groupId = `sprint-${s.id}`;
127
+ if (data.groupChat && data.groupChat.length > 0) {
128
+ chatStore.migrateGroupChat(groupId, data.groupChat);
129
+ }
130
+ s.groupChat = chatStore.getGroupMessages(groupId, 500);
131
+
132
+ s.outputs = data.outputs || [];
133
+ s.createdAt = data.createdAt ? new Date(data.createdAt) : new Date();
134
+ s.startedAt = data.startedAt ? new Date(data.startedAt) : null;
135
+ s.completedAt = data.completedAt ? new Date(data.completedAt) : null;
136
+ s.summary = data.summary;
137
+ s.liveStatus = data.liveStatus || {
138
+ currentNodeId: null, currentNodeTitle: null, currentAgent: null,
139
+ currentAction: null, lastActiveAt: null, heartbeat: null,
140
+ toolCallsInProgress: [], recentFileChanges: [],
141
+ };
142
+ return s;
143
+ }
144
+ }
145
+
146
+ /**
147
+ * Team — a specialized subgroup within a department
148
+ */
149
+ export class Team {
150
+ constructor({ name, departmentId, departmentName, memberIds, leaderId, leaderName, description }) {
151
+ this.id = uuidv4();
152
+ this.name = name;
153
+ this.departmentId = departmentId;
154
+ this.departmentName = departmentName;
155
+ this.memberIds = memberIds || []; // Team member ID list
156
+ this.leaderId = leaderId; // Leader ID
157
+ this.leaderName = leaderName;
158
+ this.description = description || '';
159
+ this.skills = []; // Team skill list
160
+ this.workspacePath = null; // Dedicated workspace directory
161
+ this.sprints = new Map(); // Sprint list (sprintId => Sprint)
162
+ this.createdAt = new Date();
163
+ this.status = 'active'; // active | archived
164
+ }
165
+
166
+ addSprint(sprint) {
167
+ this.sprints.set(sprint.id, sprint);
168
+ return sprint;
169
+ }
170
+
171
+ getSprint(id) {
172
+ return this.sprints.get(id);
173
+ }
174
+
175
+ listSprints() {
176
+ return [...this.sprints.values()].sort(
177
+ (a, b) => new Date(b.createdAt) - new Date(a.createdAt)
178
+ );
179
+ }
180
+
181
+ serialize() {
182
+ return {
183
+ id: this.id,
184
+ name: this.name,
185
+ departmentId: this.departmentId,
186
+ departmentName: this.departmentName,
187
+ memberIds: this.memberIds,
188
+ leaderId: this.leaderId,
189
+ leaderName: this.leaderName,
190
+ description: this.description,
191
+ skills: this.skills,
192
+ workspacePath: this.workspacePath,
193
+ sprints: [...this.sprints.values()].map(s => s.serialize()),
194
+ createdAt: this.createdAt,
195
+ status: this.status,
196
+ };
197
+ }
198
+
199
+ static deserialize(data) {
200
+ const t = new Team({
201
+ name: data.name,
202
+ departmentId: data.departmentId,
203
+ departmentName: data.departmentName,
204
+ memberIds: data.memberIds,
205
+ leaderId: data.leaderId,
206
+ leaderName: data.leaderName,
207
+ description: data.description,
208
+ });
209
+ t.id = data.id;
210
+ t.skills = data.skills || [];
211
+ t.workspacePath = data.workspacePath || null;
212
+ t.createdAt = data.createdAt ? new Date(data.createdAt) : new Date();
213
+ t.status = data.status || 'active';
214
+ // Deserialize sprints
215
+ for (const sd of (data.sprints || [])) {
216
+ const sprint = Sprint.deserialize(sd);
217
+ t.sprints.set(sprint.id, sprint);
218
+ }
219
+ return t;
220
+ }
221
+ }
222
+
223
+ /**
224
+ * TeamManager — manages all teams
225
+ */
226
+ export class TeamManager {
227
+ constructor() {
228
+ this.teams = new Map();
229
+ }
230
+
231
+ create(data) {
232
+ const team = new Team(data);
233
+ this.teams.set(team.id, team);
234
+ return team;
235
+ }
236
+
237
+ get(id) {
238
+ return this.teams.get(id);
239
+ }
240
+
241
+ delete(id) {
242
+ return this.teams.delete(id);
243
+ }
244
+
245
+ listAll() {
246
+ return [...this.teams.values()].sort(
247
+ (a, b) => new Date(b.createdAt) - new Date(a.createdAt)
248
+ );
249
+ }
250
+
251
+ listByDepartment(departmentId) {
252
+ return this.listAll().filter(t => t.departmentId === departmentId);
253
+ }
254
+
255
+ serialize() {
256
+ return [...this.teams.values()].map(t => t.serialize());
257
+ }
258
+
259
+ static deserialize(dataList) {
260
+ const mgr = new TeamManager();
261
+ for (const d of (dataList || [])) {
262
+ const team = Team.deserialize(d);
263
+ mgr.teams.set(team.id, team);
264
+ }
265
+ return mgr;
266
+ }
267
+ }