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,2600 @@
1
+ import { v4 as uuidv4 } from 'uuid';
2
+ import path from 'path';
3
+ import { ProviderRegistry, ModelProviders, JobCategory, JobCategoryLabel } from './workforce/providers.js';
4
+ import { HRSystem, JobTemplates } from './workforce/hr.js';
5
+ import { Department } from './department.js';
6
+ import { Employee, createEmployee, deserializeEmployee, Secretary } from '../employee/index.js';
7
+ import { LLMAgent, CLIAgent, WebAgent } from '../agent/index.js';
8
+ import { PerformanceSystem } from '../employee/performance.js';
9
+ import { TalentMarket } from './workforce/talent-market.js';
10
+ import { MessageBus } from '../agent/message-bus.js';
11
+ import { existsSync, mkdirSync } from 'fs';
12
+ import { WorkspaceManager } from '../workspace.js';
13
+ import { debouncedSave } from './persistence.js';
14
+ import { llmClient } from '../agent/llm-agent/client.js';
15
+ import { webClientRegistry } from '../agent/web-agent/web-client.js';
16
+ import { loadAgentMemory, saveAgentMemory } from '../employee/memory/store.js';
17
+ import { Memory } from '../employee/memory/index.js';
18
+ import { RequirementManager, RequirementStatus } from '../requirement.js';
19
+ import { TeamManager, SprintStatus } from './team.js';
20
+ import { hookRegistry, HookEvent } from '../../lib/hooks.js';
21
+ import { sessionManager } from '../agent/session.js';
22
+ import { robustJSONParse } from '../utils/json-parse.js';
23
+ import { cronScheduler } from '../system/cron.js';
24
+ import { pluginRegistry } from '../system/plugin.js';
25
+ import { auditLogger, AuditCategory, AuditLevel } from '../system/audit.js';
26
+ import { chatStore } from '../agent/chat-store.js';
27
+ import { cliBackendRegistry } from '../agent/cli-agent/backends/index.js';
28
+ import { groupChatLoop } from './group-chat-loop.js';
29
+
30
+ // Expand short-form file references: [[file:path]] → [[file:deptId:path|name]]
31
+ // Also fix incomplete references: [[file:deptId:path]] → [[file:deptId:path|name]]
32
+ // Only creates clickable references for files that actually exist on disk.
33
+ // Returns { content, invalidRefs } so caller can provide feedback for bad references.
34
+ const SIMPLE_FILE_REF = /\[\[file:([^\]|:]+)\]\]/g;
35
+ const INCOMPLETE_FILE_REF = /\[\[file:([^:]+):([^\]|]+)\]\]/g;
36
+ function expandFileReferences(content, departmentId, workspacePath) {
37
+ if (!content || !departmentId) return { content, invalidRefs: [] };
38
+ const invalidRefs = [];
39
+ // First: fix incomplete refs [[file:deptId:path]] → [[file:deptId:path|name]]
40
+ let expanded = content.replace(INCOMPLETE_FILE_REF, (_match, deptId, filePath) => {
41
+ const trimmed = filePath.trim();
42
+ if (workspacePath) {
43
+ const fullPath = path.join(workspacePath, trimmed);
44
+ if (!existsSync(fullPath)) {
45
+ invalidRefs.push(trimmed);
46
+ return trimmed;
47
+ }
48
+ }
49
+ const displayName = path.basename(trimmed);
50
+ return `[[file:${deptId}:${trimmed}|${displayName}]]`;
51
+ });
52
+ // Then: expand simple refs [[file:path]] → [[file:deptId:path|name]]
53
+ expanded = expanded.replace(SIMPLE_FILE_REF, (_match, filePath) => {
54
+ const trimmed = filePath.trim();
55
+ if (workspacePath) {
56
+ const fullPath = path.join(workspacePath, trimmed);
57
+ if (!existsSync(fullPath)) {
58
+ invalidRefs.push(trimmed);
59
+ return trimmed;
60
+ }
61
+ }
62
+ const displayName = path.basename(trimmed);
63
+ return `[[file:${departmentId}:${trimmed}|${displayName}]]`;
64
+ });
65
+ return { content: expanded, invalidRefs };
66
+ }
67
+
68
+
69
+ /**
70
+ * Company - AI Enterprise
71
+ * Integrates message bus and workspace management, enabling Agents to actually perform work
72
+ */
73
+ export class Company {
74
+ constructor(companyName, bossName = 'Boss', secretaryConfig = null) {
75
+ this.id = uuidv4();
76
+ this.name = companyName;
77
+ this.bossName = bossName;
78
+ this.bossAvatar = null; // Boss avatar URL
79
+ this.departments = new Map();
80
+ this.providerRegistry = new ProviderRegistry();
81
+ // Sync CLI backends into provider registry so they appear in Brain Providers
82
+ this.providerRegistry.syncCLIBackends(cliBackendRegistry);
83
+ // Auto-detect CLI backends in background (async, fire-and-forget)
84
+ cliBackendRegistry.detectAll().then(() => {
85
+ this.providerRegistry.syncCLIBackends(cliBackendRegistry);
86
+ }).catch(() => {});
87
+ this.talentMarket = new TalentMarket();
88
+ this.performanceSystem = new PerformanceSystem();
89
+ this.hr = new HRSystem(this.providerRegistry, this.talentMarket);
90
+ this.logs = [];
91
+ // Chat history with secretary
92
+ // chatHistory kept as in-memory cache (for fast frontend UI access), also written to chatStore for persistence
93
+ this.chatHistory = [];
94
+ // Chat session ID (used for chatStore file storage)
95
+ this.chatSessionId = `boss-secretary-${this.id}`;
96
+ chatStore.createSession(this.chatSessionId, {
97
+ title: `${bossName} & Secretary`,
98
+ participants: [bossName, 'Secretary'],
99
+ type: 'boss-secretary',
100
+ });
101
+ // Department progress reports
102
+ this.progressReports = [];
103
+ // Mailbox: private messages from Agents to boss
104
+ this.mailbox = [];
105
+ // Pending recruitment plans for approval
106
+ this.pendingPlans = new Map();
107
+
108
+ // Message bus (inter-Agent communication)
109
+ this.messageBus = new MessageBus();
110
+
111
+ // Workspace manager
112
+ this.workspaceManager = new WorkspaceManager();
113
+
114
+ // Requirement manager
115
+ this.requirementManager = new RequirementManager();
116
+
117
+ // Team manager
118
+ this.teamManager = new TeamManager();
119
+
120
+ // Group chat loop engine
121
+ this.groupChatLoop = groupChatLoop;
122
+
123
+ // Configure provider for secretary
124
+ let secretaryProviderConfig;
125
+ if (secretaryConfig && secretaryConfig.providerId) {
126
+ const provider = this.providerRegistry.getById(secretaryConfig.providerId);
127
+ if (provider) {
128
+ this.providerRegistry.configure(secretaryConfig.providerId, secretaryConfig.apiKey || 'sk-configured');
129
+ secretaryProviderConfig = provider;
130
+ }
131
+ }
132
+ // Fallback: use a placeholder provider reference (NOT enabled — user must configure a real one via onboarding)
133
+ if (!secretaryProviderConfig) {
134
+ secretaryProviderConfig = { id: 'none', name: '⚠️ Not Configured', enabled: false, category: 'general' };
135
+ }
136
+
137
+ // Initialize personal secretary
138
+ this.secretary = new Secretary({
139
+ company: this,
140
+ providerConfig: secretaryProviderConfig,
141
+ secretaryName: secretaryConfig?.secretaryName,
142
+ secretaryAvatar: secretaryConfig?.secretaryAvatar,
143
+ secretaryGender: secretaryConfig?.secretaryGender || 'female',
144
+ secretaryAge: secretaryConfig?.secretaryAge || 18,
145
+ });
146
+
147
+ // If secretary uses a CLI provider, rebuild agent as CLIAgent
148
+ if (secretaryProviderConfig.isCLI && secretaryProviderConfig.cliBackendId) {
149
+ const fallback = this.providerRegistry.recommend('general');
150
+ this.secretary.agent = new CLIAgent({
151
+ cliBackend: secretaryProviderConfig.cliBackendId,
152
+ cliProvider: secretaryProviderConfig,
153
+ fallbackProvider: fallback, provider: fallback,
154
+ });
155
+ }
156
+ // If secretary uses a Web provider, rebuild agent as WebAgent
157
+ if (secretaryProviderConfig.isWeb) {
158
+ this.secretary.agent = new WebAgent({ provider: secretaryProviderConfig });
159
+ // Re-bind employeeId after agent replacement (for per-employee session isolation)
160
+ this.secretary.agent.setEmployeeId(this.secretary.id);
161
+ }
162
+
163
+ // Initialize secretary's toolKit so she can use tools (shell, file ops, etc.)
164
+ const secretaryWorkspace = this.workspaceManager.createDepartmentWorkspace('secretary', 'secretary');
165
+ this.secretary.initToolKit(secretaryWorkspace, this.messageBus);
166
+
167
+ this._log('Company founded', `"${this.name}" founded by ${this.bossName}`);
168
+ this._log('Secretary ready', `Secretary ${this.secretary.name} using model ${secretaryProviderConfig.name}`);
169
+
170
+ // Initialize distilled subsystems
171
+ this._initSubsystems();
172
+ }
173
+
174
+ /**
175
+ * Chat with secretary (task assignment or casual conversation)
176
+ * @param {string} message - Boss's message
177
+ * @returns {Promise<object>} Secretary's reply
178
+ */
179
+ async chatWithSecretary(message) {
180
+ const bossMsg = {
181
+ role: 'boss',
182
+ content: message,
183
+ time: new Date(),
184
+ };
185
+ this.chatHistory.push(bossMsg);
186
+ // Persist to file storage
187
+ chatStore.appendMessage(this.chatSessionId, bossMsg);
188
+
189
+ const sec = this.secretary;
190
+ let reply;
191
+
192
+ // If secretary has CLI backend configured, chat also goes through CLI
193
+ if (sec.cliBackend) {
194
+ try {
195
+ const recentMessages = chatStore.getRecentMessages(this.chatSessionId, 10);
196
+ const chatContext = recentMessages.slice(-6).map(m =>
197
+ `${m.role === 'boss' ? 'Boss' : sec.name}: ${m.content}`
198
+ ).join('\n');
199
+
200
+ const departments = [...this.departments.values()].map(d => ({
201
+ name: d.name, id: d.id, mission: d.mission, status: d.status,
202
+ memberCount: d.agents.size,
203
+ leader: d.getLeader()?.name || 'Unassigned',
204
+ }));
205
+ const deptContext = departments.length > 0
206
+ ? departments.map(d => ` 🏢 ${d.name} [id:${d.id}] - Mission: ${d.mission} | ${d.memberCount} people | Leader: ${d.leader}`).join('\n')
207
+ : 'No departments yet.';
208
+
209
+ const cliPrompt = `You are "${sec.name}", the personal secretary of "${this.bossName}".
210
+ ${sec.prompt ? `Your persona: ${sec.prompt}\n` : ''}
211
+ Current company "${this.name}" status:
212
+ - Departments: ${this.departments.size}
213
+ ${deptContext}
214
+
215
+ Recent conversation:
216
+ ${chatContext}
217
+
218
+ Boss's latest message: ${message}
219
+
220
+ You MUST reply with a JSON object (return JSON only, no other text):
221
+ {
222
+ "content": "Your natural language reply to the boss (warm, personal, with emoji)",
223
+ "action": null or one of:
224
+ - { "type": "secretary_handle", "taskDescription": "detailed task for yourself to execute" } — for simple tasks you can handle alone
225
+ - { "type": "task_assigned", "departmentId": "real dept id from above", "departmentName": "dept name", "taskTitle": "short title", "taskDescription": "detailed description" } — assign to existing department
226
+ - { "type": "create_department", "departmentName": "name", "mission": "mission", "members": [{ "templateId": "id", "name": "nickname", "isLeader": true/false, "reportsTo": null or 0 }] } — boss wants to create a new department (design team directly)
227
+ - { "type": "need_new_department", "suggestedMission": "task description" } — no existing dept can handle this
228
+ - { "type": "progress_report" } — boss wants progress
229
+ - null — casual chat, no action needed
230
+ }
231
+
232
+ Available job templates for team design (create_department): ${JSON.stringify(Object.values(JobTemplates).map(t => ({ id: t.id, title: t.title, category: t.category })))}
233
+ Enabled provider categories: ${[...new Set(this.providerRegistry.listEnabled().map(p => p.category))].join(', ')}
234
+ ONLY use templates whose category has an enabled provider above.
235
+
236
+ Rules:
237
+ - If boss wants to create a department → create_department (MUST include members array with 2-6 people, first must be project-leader with isLeader=true)
238
+ - If boss gives a task and an existing department matches → task_assigned (use the real departmentId!)
239
+ - If boss gives a simple task you can handle alone → secretary_handle
240
+ - If boss gives a task but no department matches → need_new_department
241
+ - Casual chat → null
242
+ - ALWAYS return valid JSON only, no markdown fences`;
243
+
244
+ const cliResult = await cliBackendRegistry.executeTask(
245
+ sec.cliBackend, sec,
246
+ { title: `Secretary chat reply`, description: cliPrompt },
247
+ sec.toolKit?.workspaceDir || process.cwd(), {}, { timeout: 60000 }
248
+ );
249
+ const rawOutput = cliResult.output || cliResult.errorOutput || '...';
250
+
251
+ reply = this._parseSecretaryJSON(rawOutput, message);
252
+ } catch (cliErr) {
253
+ const hasLLM = sec.agent.provider && sec.agent.provider.enabled && sec.agent.provider.apiKey && !sec.agent.provider.apiKey.startsWith('cli');
254
+ if (hasLLM) {
255
+ console.warn(` ⚠️ [Secretary] CLI chat failed, falling back to LLM: ${cliErr.message || cliErr.error}`);
256
+ reply = await this.secretary.handleBossMessage(message, this);
257
+ } else {
258
+ // No available LLM provider, return CLI error message
259
+ console.error(` ❌ [Secretary] CLI chat failed, no LLM fallback available: ${cliErr.message || cliErr.error}`);
260
+ reply = {
261
+ content: `⚠️ CLI execution error: ${cliErr.message || 'Unknown error'}. Please check if CodeBuddy CLI is running properly.`,
262
+ action: null,
263
+ };
264
+ }
265
+ }
266
+ } else {
267
+ // Let secretary analyze whether it's task assignment or casual conversation
268
+ reply = await this.secretary.handleBossMessage(message, this);
269
+ }
270
+
271
+ const secretaryMsg = {
272
+ role: 'secretary',
273
+ content: reply.content,
274
+ action: reply.action || null,
275
+ time: new Date(),
276
+ };
277
+ this.chatHistory.push(secretaryMsg);
278
+ // Persist to file storage
279
+ chatStore.appendMessage(this.chatSessionId, secretaryMsg);
280
+
281
+ // Keep only the latest 50 messages in memory (for frontend cache)
282
+ if (this.chatHistory.length > 50) {
283
+ this.chatHistory = this.chatHistory.slice(-50);
284
+ }
285
+
286
+ this._log('Secretary chat', `Boss: "${message.slice(0, 30)}..." → Secretary replied`);
287
+ return reply;
288
+ }
289
+
290
+ /**
291
+ * Parse secretary's returned JSON (shared by CLI and LLM)
292
+ * Extracts { content, action } structure from raw text
293
+ */
294
+ _parseSecretaryJSON(rawOutput, originalMessage) {
295
+ try {
296
+ const parsed = robustJSONParse(rawOutput);
297
+ const result = {
298
+ content: parsed.content || rawOutput,
299
+ action: parsed.action || null,
300
+ };
301
+
302
+ // Validate task_assigned departmentId
303
+ if (result.action?.type === 'task_assigned' && result.action.departmentId) {
304
+ const deptById = this.departments.get(result.action.departmentId);
305
+ if (!deptById) {
306
+ // departmentId invalid, try matching by name
307
+ const deptIdValue = result.action.departmentId;
308
+ const deptNameValue = result.action.departmentName || deptIdValue;
309
+ let foundDept = null;
310
+ for (const dept of this.departments.values()) {
311
+ if (dept.name === deptIdValue || dept.name === deptNameValue ||
312
+ dept.name.includes(deptIdValue) || deptIdValue.includes(dept.name)) {
313
+ foundDept = dept;
314
+ break;
315
+ }
316
+ }
317
+ if (foundDept) {
318
+ console.log(`🔧 [CLI] Fixed departmentId: "${deptIdValue}" → "${foundDept.id}" (${foundDept.name})`);
319
+ result.action.departmentId = foundDept.id;
320
+ result.action.departmentName = foundDept.name;
321
+ } else {
322
+ console.warn(`⚠️ [CLI] departmentId "${deptIdValue}" doesn't match any department, clearing action`);
323
+ result.action = null;
324
+ }
325
+ }
326
+ }
327
+
328
+ console.log(`🤖 [Secretary-CLI] action type: ${result.action?.type || 'null'}`);
329
+ return result;
330
+ } catch (parseError) {
331
+ console.warn('⚠️ [Secretary-CLI] JSON parse failed, using raw output:', parseError.message);
332
+ // JSON parse failed, try to extract content field
333
+ let displayContent = rawOutput;
334
+ const contentFieldMatch = rawOutput.match(/"content"\s*:\s*"((?:[^"\\]|\\.)*)"/);
335
+ if (contentFieldMatch) {
336
+ try {
337
+ displayContent = JSON.parse('"' + contentFieldMatch[1] + '"');
338
+ } catch {
339
+ displayContent = contentFieldMatch[1].replace(/\\n/g, '\n').replace(/\\"/g, '"');
340
+ }
341
+ }
342
+
343
+ // Try to extract action from raw output
344
+ let action = null;
345
+ const actionTypeMatch = rawOutput.match(/"type"\s*:\s*"(task_assigned|need_new_department|create_department|progress_report|secretary_handle)"/);
346
+ if (actionTypeMatch) {
347
+ const actionType = actionTypeMatch[1];
348
+ if (actionType === 'task_assigned') {
349
+ const deptNameMatch = rawOutput.match(/"departmentName"\s*:\s*"([^"]+)"/);
350
+ if (deptNameMatch) {
351
+ for (const dept of this.departments.values()) {
352
+ if (dept.name === deptNameMatch[1] || dept.name.includes(deptNameMatch[1]) || deptNameMatch[1].includes(dept.name)) {
353
+ const titleMatch = rawOutput.match(/"taskTitle"\s*:\s*"([^"]+)"/);
354
+ const descMatch = rawOutput.match(/"taskDescription"\s*:\s*"((?:[^"\\]|\\.)*)"/);
355
+ action = {
356
+ type: 'task_assigned',
357
+ departmentId: dept.id,
358
+ departmentName: dept.name,
359
+ taskTitle: titleMatch ? titleMatch[1] : originalMessage.slice(0, 50),
360
+ taskDescription: descMatch ? descMatch[1].replace(/\\n/g, '\n') : originalMessage,
361
+ };
362
+ break;
363
+ }
364
+ }
365
+ }
366
+ } else if (actionType === 'create_department') {
367
+ const deptNameMatch = rawOutput.match(/"departmentName"\s*:\s*"([^"]+)"/);
368
+ const missionMatch = rawOutput.match(/"mission"\s*:\s*"((?:[^"\\]|\\.)*)"/);
369
+ action = { type: 'create_department', departmentName: deptNameMatch?.[1] || '', mission: missionMatch?.[1]?.replace(/\\n/g, '\n') || originalMessage };
370
+ } else if (actionType === 'need_new_department') {
371
+ const missionMatch = rawOutput.match(/"suggestedMission"\s*:\s*"((?:[^"\\]|\\.)*)"/);
372
+ action = { type: 'need_new_department', suggestedMission: missionMatch?.[1]?.replace(/\\n/g, '\n') || originalMessage };
373
+ } else if (actionType === 'secretary_handle') {
374
+ const descMatch = rawOutput.match(/"taskDescription"\s*:\s*"((?:[^"\\]|\\.)*)"/);
375
+ action = { type: 'secretary_handle', taskDescription: descMatch?.[1]?.replace(/\\n/g, '\n') || originalMessage };
376
+ } else if (actionType === 'progress_report') {
377
+ action = { type: 'progress_report' };
378
+ }
379
+ }
380
+
381
+ return { content: displayContent, action };
382
+ }
383
+ }
384
+
385
+ /**
386
+ * Update boss profile (avatar)
387
+ */
388
+ updateBossProfile(settings) {
389
+ if (settings.avatar) {
390
+ this.bossAvatar = settings.avatar;
391
+ }
392
+ this._log('Boss profile updated', `Avatar updated`);
393
+ return { bossAvatar: this.bossAvatar };
394
+ }
395
+
396
+ /**
397
+ * Chat with a specific agent (boss <-> agent 1-on-1)
398
+ * @param {string} agentId - Agent ID
399
+ * @param {string} message - Boss's message
400
+ * @returns {Promise<object>} Agent's reply
401
+ */
402
+ async chatWithAgent(agentId, message) {
403
+ // Find the agent
404
+ let targetAgent = null;
405
+ let targetDept = null;
406
+ for (const dept of this.departments.values()) {
407
+ const agent = dept.agents.get(agentId);
408
+ if (agent) {
409
+ targetAgent = agent;
410
+ targetDept = dept;
411
+ break;
412
+ }
413
+ }
414
+ if (!targetAgent) throw new Error(`Employee not found: ${agentId}`);
415
+
416
+ // Create or get chat session
417
+ const sessionId = `boss-agent-${agentId}`;
418
+ chatStore.createSession(sessionId, {
419
+ title: `${this.bossName} & ${targetAgent.name}`,
420
+ participants: [this.bossName, targetAgent.name],
421
+ type: 'boss-agent',
422
+ });
423
+
424
+ // Append boss message
425
+ const bossMsg = { role: 'boss', content: message, time: new Date() };
426
+ chatStore.appendMessage(sessionId, bossMsg);
427
+
428
+ // Get recent chat history for context
429
+ const recentMessages = chatStore.getRecentMessages(sessionId, 20);
430
+
431
+ // Build memory context from the new Memory system
432
+ const bossChatGroupId = `boss-chat-${agentId}`;
433
+ const memoryContext = targetAgent.memory.buildMemoryContext(bossChatGroupId);
434
+
435
+ // Build messages for LLM — now with structured memory + JSON output
436
+ const systemMessage = targetAgent._buildSystemMessage()
437
+ + `\n\n## Current Conversation\nYou are having a private 1-on-1 conversation with your boss "${this.bossName}".`
438
+ + ` You work in the "${targetDept.name}" department.`
439
+ + ` Respond naturally based on your personality and role. Be helpful but stay in character.`
440
+ + memoryContext
441
+ + `\n\n## Output Format\nYou MUST return a JSON object (JSON only, nothing else):\n{\n "content": "Your natural language reply",\n "memorySummary": "A concise summary of older messages in this conversation — key facts, decisions, topics discussed. null if conversation just started.",\n "memoryOps": [\n { "op": "add", "type": "long_term", "content": "Important fact about the boss or decision made", "category": "fact", "importance": 8 },\n { "op": "add", "type": "short_term", "content": "Current topic context", "category": "context", "importance": 5, "ttl": 3600 },\n { "op": "delete", "id": "mem_id_to_forget" }\n ],\n "relationshipOps": [\n { "employeeId": "boss", "name": "Boss", "impression": "Demanding but fair, values results", "affinity": 65 }\n ]\n}\n\n## Memory Management\n- memorySummary: Summarize older conversation messages to compress context. Keep key info, skip chitchat. null if no old messages.\n- memoryOps: Manage your memory — add important facts/preferences about the boss as long_term, add current topic as short_term. [] if nothing to remember.\n- category: preference | fact | instruction | task | context | relationship | experience\n- importance: 1-10 (higher = more important)\n\n## Relationship Impressions\n- relationshipOps: Update your impression of the boss based on this conversation. Max 30 chars per impression, affinity 1-100 (50=neutral).\n- affinity should change gradually (+/- 5~15 per interaction). Start from 50 if first meeting.\n- Only update when something noteworthy happened. [] if nothing to update.`;
442
+
443
+ const messages = [
444
+ { role: 'system', content: systemMessage },
445
+ ];
446
+
447
+ // Add recent history as context
448
+ for (const msg of recentMessages.slice(0, -1)) { // exclude the one we just added
449
+ messages.push({
450
+ role: msg.role === 'boss' ? 'user' : 'assistant',
451
+ content: msg.content,
452
+ });
453
+ }
454
+
455
+ // Add current message
456
+ messages.push({ role: 'user', content: message });
457
+
458
+ // If this is a CLI agent, execute chat via CLI backend
459
+ let replyContent;
460
+ const displayInfo = targetAgent.getDisplayInfo();
461
+ const chatEngine = displayInfo.type === 'cli'
462
+ ? { engine: 'cli', cliName: displayInfo.name }
463
+ : displayInfo.type === 'web'
464
+ ? { engine: 'web', webName: displayInfo.name }
465
+ : { engine: 'llm', llmName: displayInfo.name };
466
+
467
+ if (targetAgent.agentType === 'cli' && targetAgent.isAvailable()) {
468
+ // CLI agent chat goes through CLI backend
469
+ try {
470
+ const chatContext = recentMessages.slice(-6).map(m =>
471
+ `${m.role === 'boss' ? 'Boss' : targetAgent.name}: ${m.content}`
472
+ ).join('\n');
473
+
474
+ const cliResult = await cliBackendRegistry.executeTask(
475
+ targetAgent.cliBackend,
476
+ targetAgent,
477
+ {
478
+ title: `Chat reply`,
479
+ description: `You are having a 1-on-1 conversation with your boss "${this.bossName}". Reply naturally and helpfully based on your personality and role. Keep your reply concise (2-6 sentences). Do NOT use any tools or execute any code — just reply conversationally.\n\nRecent conversation:\n${chatContext}\n\nBoss: ${message}\n\nReply as ${targetAgent.name}:`,
480
+ },
481
+ targetAgent.toolKit?.workspaceDir || process.cwd(),
482
+ {},
483
+ { timeout: 60000 }
484
+ );
485
+ replyContent = cliResult.output || cliResult.errorOutput || '...';
486
+ } catch (cliErr) {
487
+ // On CLI failure, attempt fallback to LLM
488
+ if (targetAgent.canChat()) {
489
+ console.warn(` ⚠️ [${targetAgent.name}] CLI chat failed, falling back to LLM: ${cliErr.message || cliErr.error}`);
490
+ try {
491
+ const response = await targetAgent.chat(messages, { temperature: 0.8, maxTokens: 2048 });
492
+ replyContent = response.content;
493
+ } catch (err) {
494
+ replyContent = `(Sorry boss, my brain froze: ${err.message})`;
495
+ }
496
+ } else {
497
+ console.error(` ❌ [${targetAgent.name}] CLI chat failed, no LLM fallback: ${cliErr.message || cliErr.error}`);
498
+ replyContent = `⚠️ CLI execution error: ${cliErr.message || 'Unknown error'}. Please check if CLI is running properly.`;
499
+ }
500
+ }
501
+ } else if (targetAgent.canChat()) {
502
+ // Regular LLM agent or CLI with fallback
503
+ try {
504
+ const response = await targetAgent.chat(messages, { temperature: 0.8, maxTokens: 2048 });
505
+ replyContent = response.content;
506
+ } catch (err) {
507
+ replyContent = `(Sorry boss, my brain froze: ${err.message})`;
508
+ }
509
+ }
510
+
511
+ // Process structured memory from AI response (new Memory system)
512
+ try {
513
+ const { robustJSONParse } = await import('../utils/json-parse.js');
514
+ const parsed = robustJSONParse(replyContent);
515
+ if (parsed && parsed.content) {
516
+ // Extract actual reply content from JSON
517
+ replyContent = parsed.content;
518
+
519
+ // Process rolling history summary
520
+ if (parsed.memorySummary) {
521
+ targetAgent.memory.updateHistorySummary(bossChatGroupId, parsed.memorySummary);
522
+ console.log(` 📜 [${targetAgent.name}] Boss-chat history summary updated`);
523
+ }
524
+
525
+ // Process memory operations
526
+ if (parsed.memoryOps && Array.isArray(parsed.memoryOps)) {
527
+ const result = targetAgent.memory.processMemoryOps(parsed.memoryOps);
528
+ if (result.added + result.updated + result.deleted > 0) {
529
+ console.log(` 🧠 [${targetAgent.name}] Boss-chat memory: +${result.added} ~${result.updated} -${result.deleted}`);
530
+ }
531
+ }
532
+
533
+ // Process relationship impressions
534
+ if (parsed.relationshipOps && Array.isArray(parsed.relationshipOps)) {
535
+ const relResult = targetAgent.memory.processRelationshipOps(parsed.relationshipOps);
536
+ if (relResult.updated > 0) {
537
+ console.log(` 👥 [${targetAgent.name}] Boss-chat relationship updates: ${relResult.updated}`);
538
+ }
539
+ }
540
+ }
541
+ } catch (e) {
542
+ // JSON parse failed — replyContent is plain text, that's fine
543
+ }
544
+
545
+ // Append agent reply
546
+ const agentMsg = { role: 'agent', content: replyContent, time: new Date() };
547
+ chatStore.appendMessage(sessionId, agentMsg);
548
+
549
+
550
+
551
+ this.save();
552
+
553
+ return {
554
+ agentId: targetAgent.id,
555
+ agentName: targetAgent.name,
556
+ reply: replyContent,
557
+ time: new Date(),
558
+ chatEngine,
559
+ };
560
+ }
561
+
562
+ /**
563
+ * Get chat history with a specific agent
564
+ * @param {string} agentId - Agent ID
565
+ * @param {number} limit - Max messages to return
566
+ * @returns {Array} Chat messages
567
+ */
568
+ getAgentChatHistory(agentId, limit = 30) {
569
+ const sessionId = `boss-agent-${agentId}`;
570
+ return chatStore.getRecentMessages(sessionId, limit);
571
+ }
572
+
573
+ /**
574
+ * Mark chat with agent as read
575
+ */
576
+ markAgentChatRead(agentId) {
577
+ const sessionId = `boss-agent-${agentId}`;
578
+ chatStore.markSessionRead(sessionId);
579
+ }
580
+
581
+ /**
582
+ * Get summary info for all boss-agent private chat sessions
583
+ * Used to display private chat session list in Mailbox
584
+ */
585
+ _getAgentChatSessions() {
586
+ const sessions = chatStore.listSessions();
587
+ const agentSessions = sessions.filter(s => s.type === 'boss-agent');
588
+
589
+ return agentSessions.map(session => {
590
+ // Extract agentId from sessionId: "boss-agent-{agentId}"
591
+ const agentId = session.sessionId.replace('boss-agent-', '');
592
+
593
+ // Find corresponding agent info
594
+ let agent = null;
595
+ let deptName = null;
596
+ for (const dept of this.departments.values()) {
597
+ const a = dept.agents.get(agentId);
598
+ if (a) {
599
+ agent = a;
600
+ deptName = dept.name;
601
+ break;
602
+ }
603
+ }
604
+
605
+ // Get latest message as preview
606
+ const recentMessages = chatStore.getRecentMessages(session.sessionId, 1);
607
+ const lastMsg = recentMessages.length > 0 ? recentMessages[recentMessages.length - 1] : null;
608
+
609
+ // Get read timestamp to determine if there are unread messages
610
+ const meta = chatStore.getSessionMeta(session.sessionId);
611
+ const bossLastReadAt = meta?.bossLastReadAt || null;
612
+ const lastTime = lastMsg?.time || session.lastActiveAt || session.createdAt;
613
+ // If never marked as read, or latest message time is after read time, it is unread
614
+ const unread = !bossLastReadAt || (lastTime && new Date(lastTime) > new Date(bossLastReadAt));
615
+
616
+ return {
617
+ sessionId: session.sessionId,
618
+ agentId,
619
+ agentName: agent?.name || session.participants?.[1] || 'Unknown',
620
+ agentAvatar: agent?.avatar || null,
621
+ agentRole: agent?.role || null,
622
+ agentSignature: agent?.signature || null,
623
+ departmentName: deptName,
624
+ lastMessage: lastMsg?.content?.slice(0, 50) || null,
625
+ lastMessageRole: lastMsg?.role || null,
626
+ lastTime,
627
+ totalMessages: session.totalMessages || 0,
628
+ unread,
629
+ };
630
+ }).filter(s => s.totalMessages > 0 && s.agentName !== 'Unknown' && s.agentAvatar !== null) // Only return sessions with messages, filter out dismissed agents (cannot find agent)
631
+ .sort((a, b) => new Date(b.lastTime) - new Date(a.lastTime)); // Sort by time descending
632
+ }
633
+
634
+ /**
635
+ * Get all agent-to-agent chat sessions for a given agent
636
+ * Used to "view chat records between this agent and others"
637
+ * @param {string} agentId - Target agent ID
638
+ * @returns {Array} Chat session list
639
+ */
640
+ getAgentConversations(agentId) {
641
+ const sessions = chatStore.listSessions();
642
+ // Find all agent-agent sessions that include this agent
643
+ const agentSessions = sessions.filter(s => {
644
+ if (s.type !== 'agent-agent') return false;
645
+ // sessionId format: agent-agent-{id1}-{id2}
646
+ return s.sessionId.includes(agentId);
647
+ });
648
+
649
+ // Also include boss-agent chats
650
+ const bossSessions = sessions.filter(s =>
651
+ s.type === 'boss-agent' && s.sessionId === `boss-agent-${agentId}`
652
+ );
653
+
654
+ const conversations = [];
655
+
656
+ for (const session of agentSessions) {
657
+ // Use participants to find the other party ID (more reliable than parsing sessionId)
658
+ const participants = session.participants || [];
659
+ const peerId = participants.find(p => p !== agentId) || null;
660
+ if (!peerId) continue;
661
+
662
+ // Find the other agent info
663
+ let peerAgent = null;
664
+ let peerDeptName = null;
665
+ for (const dept of this.departments.values()) {
666
+ const a = dept.agents.get(peerId);
667
+ if (a) {
668
+ peerAgent = a;
669
+ peerDeptName = dept.name;
670
+ break;
671
+ }
672
+ }
673
+
674
+ const recentMessages = chatStore.getRecentMessages(session.sessionId, 1);
675
+ const lastMsg = recentMessages.length > 0 ? recentMessages[recentMessages.length - 1] : null;
676
+
677
+ conversations.push({
678
+ sessionId: session.sessionId,
679
+ type: 'agent-agent',
680
+ peerId,
681
+ peerName: peerAgent?.name || session.participants?.find(p => p !== agentId) || 'Unknown',
682
+ peerAvatar: peerAgent?.avatar || null,
683
+ peerRole: peerAgent?.role || null,
684
+ peerDepartment: peerDeptName,
685
+ lastMessage: lastMsg?.content?.slice(0, 60) || null,
686
+ lastTime: lastMsg?.time || session.lastActiveAt || session.createdAt,
687
+ totalMessages: session.totalMessages || 0,
688
+ });
689
+ }
690
+
691
+ // Boss chat
692
+ for (const session of bossSessions) {
693
+ const recentMessages = chatStore.getRecentMessages(session.sessionId, 1);
694
+ const lastMsg = recentMessages.length > 0 ? recentMessages[recentMessages.length - 1] : null;
695
+
696
+ conversations.push({
697
+ sessionId: session.sessionId,
698
+ type: 'boss-agent',
699
+ peerId: 'boss',
700
+ peerName: this.bossName || 'Boss',
701
+ peerAvatar: this.bossAvatar || null,
702
+ peerRole: 'Boss',
703
+ peerDepartment: null,
704
+ lastMessage: lastMsg?.content?.slice(0, 60) || null,
705
+ lastTime: lastMsg?.time || session.lastActiveAt || session.createdAt,
706
+ totalMessages: session.totalMessages || 0,
707
+ });
708
+ }
709
+
710
+ // Sort by time descending
711
+ conversations.sort((a, b) => {
712
+ if (!a.lastTime) return 1;
713
+ if (!b.lastTime) return -1;
714
+ return new Date(b.lastTime) - new Date(a.lastTime);
715
+ });
716
+
717
+ return conversations;
718
+ }
719
+
720
+ /**
721
+ * Get chat messages for an agent-agent session
722
+ * @param {string} sessionId - Session ID
723
+ * @param {number} limit - Max message count
724
+ * @returns {Array} Message list
725
+ */
726
+ getAgentAgentChatHistory(sessionId, limit = 50) {
727
+ const messages = chatStore.getRecentMessages(sessionId, limit);
728
+ // Also return session participants info, for frontend to determine message direction
729
+ const meta = chatStore.getSessionMeta(sessionId);
730
+ return {
731
+ messages,
732
+ participants: meta?.participants || [],
733
+ };
734
+ }
735
+
736
+ /**
737
+ * Update secretary settings (name, avatar, prompt, etc.)
738
+ */
739
+ updateSecretarySettings(settings) {
740
+ const sec = this.secretary;
741
+ if (settings.name) sec.name = settings.name;
742
+ if (settings.avatar) sec.avatar = settings.avatar;
743
+ if (settings.avatarParams) sec.avatarParams = settings.avatarParams;
744
+ if (settings.gender) sec.gender = settings.gender;
745
+ if (settings.age != null) sec.age = settings.age;
746
+ if (settings.prompt) sec.prompt = settings.prompt;
747
+ if (settings.signature) sec.signature = settings.signature;
748
+ // Switch provider
749
+ if (settings.providerId) {
750
+ const newProvider = this.providerRegistry.getById(settings.providerId);
751
+ if (!newProvider) throw new Error(`Provider not found: ${settings.providerId}`);
752
+ if (!newProvider.enabled) throw new Error(`Provider ${newProvider.name} is not enabled, please configure API Key first`);
753
+
754
+ // Determine target agent type
755
+ const targetType = newProvider.isCLI ? 'cli' : newProvider.isWeb ? 'web' : 'llm';
756
+ const needsTypeSwitch = sec.agentType !== targetType;
757
+
758
+ if (needsTypeSwitch) {
759
+ // Agent type mismatch — rebuild the communication agent only
760
+ if (newProvider.isCLI && newProvider.cliBackendId) {
761
+ const fallback = this.providerRegistry.recommend('general');
762
+ sec.agent = new CLIAgent({
763
+ cliBackend: newProvider.cliBackendId,
764
+ cliProvider: newProvider, fallbackProvider: fallback, provider: fallback,
765
+ });
766
+ this._log('Secretary settings', `Secretary agent rebuilt as CLIAgent: ${newProvider.name} (${newProvider.cliBackendId})`);
767
+ } else if (newProvider.isWeb) {
768
+ sec.agent = new WebAgent({ provider: newProvider });
769
+ sec.agent.setEmployeeId(sec.id);
770
+ // Reset session so next chat reinitializes with the new provider
771
+ sec._sessionAwake = false;
772
+ this._log('Secretary settings', `Secretary agent rebuilt as WebAgent: ${newProvider.name}`);
773
+ } else {
774
+ sec.agent = new LLMAgent({ provider: newProvider });
775
+ this._log('Secretary settings', `Secretary agent rebuilt as LLMAgent: ${newProvider.name}`);
776
+ }
777
+ } else {
778
+ sec.switchProvider(newProvider);
779
+ }
780
+ // Sync HR assistant's provider
781
+ sec.hrAssistant.employee.switchProvider(newProvider);
782
+ this._log('Secretary settings', `Secretary provider switched to: ${newProvider.name}`);
783
+ }
784
+ this._log('Secretary settings', `Updated secretary settings: ${Object.keys(settings).join(', ')}`);
785
+ this.save();
786
+ const displayInfo = sec.getProviderDisplayInfo();
787
+ return {
788
+ name: sec.name,
789
+ avatar: sec.avatar,
790
+ gender: sec.gender,
791
+ age: sec.age,
792
+ prompt: sec.prompt,
793
+ signature: sec.signature,
794
+ provider: displayInfo.name,
795
+ providerId: displayInfo.id,
796
+ };
797
+ }
798
+
799
+ /**
800
+ * Initialize distilled subsystems (hooks, cron, plugins, sessions)
801
+ */
802
+ _initSubsystems() {
803
+ // 1. Configure cron executor to run tasks via company
804
+ cronScheduler.executor = async (agentId, taskPrompt, jobId) => {
805
+ // Find the agent and their department
806
+ for (const dept of this.departments.values()) {
807
+ const agent = dept.agents.get(agentId);
808
+ if (agent) {
809
+ const result = await agent.executeTask({
810
+ title: `Scheduled: ${taskPrompt.slice(0, 40)}`,
811
+ description: taskPrompt,
812
+ context: `This is an automated scheduled task (job: ${jobId})`,
813
+ });
814
+ return result.output;
815
+ }
816
+ }
817
+ throw new Error(`Agent ${agentId} not found for cron job`);
818
+ };
819
+
820
+ // 2. Register cron callbacks for hooks integration
821
+ cronScheduler.onJobRun = (job) => {
822
+ hookRegistry.trigger(HookEvent.TASK_ASSIGNED, {
823
+ source: 'cron', jobId: job.id, jobName: job.name, agentId: job.agentId,
824
+ });
825
+ };
826
+ cronScheduler.onJobComplete = (job, result) => {
827
+ hookRegistry.trigger(HookEvent.TASK_COMPLETED, {
828
+ source: 'cron', jobId: job.id, jobName: job.name, agentId: job.agentId,
829
+ });
830
+ };
831
+ cronScheduler.onJobError = (job, error) => {
832
+ hookRegistry.trigger(HookEvent.TASK_FAILED, {
833
+ source: 'cron', jobId: job.id, jobName: job.name, error: error.message,
834
+ });
835
+ auditLogger.log({
836
+ category: AuditCategory.AGENT_ACTION, level: AuditLevel.WARN,
837
+ agentId: job.agentId, action: `Cron job failed: ${job.name}`,
838
+ details: { error: error.message, jobId: job.id },
839
+ });
840
+ };
841
+
842
+ // 3. Enable built-in plugins by default
843
+ for (const plugin of pluginRegistry.list()) {
844
+ if (plugin.state === 'installed') {
845
+ try { pluginRegistry.enable(plugin.id); } catch {}
846
+ }
847
+ }
848
+
849
+ // 4. Start cron scheduler
850
+ cronScheduler.start();
851
+
852
+ // 5. Start session pruning
853
+ sessionManager.startPruning();
854
+
855
+ // 6. Start group chat loop engine
856
+ groupChatLoop.start(this);
857
+ // Start group chat loop for all existing agents
858
+ for (const dept of this.departments.values()) {
859
+ for (const agent of dept.getMembers()) {
860
+ groupChatLoop.startAgentLoop(agent);
861
+ }
862
+ }
863
+
864
+ // 7. Fire system startup hook
865
+ hookRegistry.trigger(HookEvent.SYSTEM_STARTUP, {
866
+ companyName: this.name, bossName: this.bossName,
867
+ });
868
+
869
+ console.log('⚡ Distilled subsystems initialized (hooks, cron, plugins, sessions)');
870
+ }
871
+
872
+ _log(action, detail) {
873
+ this.logs.push({ time: new Date(), action, detail });
874
+ // Auto-persist on every state change
875
+ debouncedSave(this);
876
+ }
877
+
878
+ /**
879
+ * Manually trigger persistence (call after important operations)
880
+ */
881
+ save() {
882
+ debouncedSave(this, 500);
883
+ }
884
+
885
+ /**
886
+ * Step 1: Generate recruitment plan (don't execute, wait for boss approval)
887
+ */
888
+ async planDepartment(name, mission) {
889
+ // Use a temporary Department instance for team design analysis
890
+ const tempDept = new Department({ name, mission, company: this.id });
891
+ const teamPlan = await tempDept.designTeam(mission, this.secretary, this.providerRegistry);
892
+ teamPlan.departmentName = name;
893
+
894
+ const planId = uuidv4();
895
+ this.pendingPlans.set(planId, { teamPlan, name, mission });
896
+
897
+ this._log('Recruitment plan', `Secretary planned a ${teamPlan.members.length}-person team for "${name}", pending boss approval`);
898
+
899
+ return {
900
+ planId,
901
+ departmentName: name,
902
+ mission,
903
+ reasoning: teamPlan.reasoning || null,
904
+ members: teamPlan.members.map(m => {
905
+ // Find job template for this position, get category and requiredCapabilities
906
+ const template = this.hr.getTemplate(m.templateId);
907
+ let providerName = null;
908
+ let providerModel = null;
909
+ if (template) {
910
+ const recommended = this.providerRegistry.recommend(
911
+ template.category,
912
+ template.requiredCapabilities
913
+ );
914
+ if (recommended) {
915
+ providerName = recommended.name;
916
+ providerModel = recommended.model;
917
+ }
918
+ }
919
+ return {
920
+ templateId: m.templateId,
921
+ title: m.templateTitle,
922
+ name: m.name,
923
+ isLeader: m.isLeader,
924
+ reportsTo: m.reportsTo !== null ? teamPlan.members[m.reportsTo]?.name : null,
925
+ reason: m.reason || null,
926
+ providerName, // Recommended provider name (model name)
927
+ providerModel, // Recommended provider model
928
+ };
929
+ }),
930
+ collaborationRules: teamPlan.collaborationRules,
931
+ };
932
+ }
933
+
934
+ /**
935
+ * Step 2: Confirm recruitment plan, execute hiring
936
+ */
937
+ async confirmPlan(planId) {
938
+ const plan = this.pendingPlans.get(planId);
939
+ if (!plan) throw new Error('Recruitment plan not found or expired');
940
+
941
+ this.pendingPlans.delete(planId);
942
+
943
+ const { teamPlan, name, mission } = plan;
944
+
945
+ // Recruit via HR assistant
946
+ const agents = this.secretary.hrAssistant.executeRecruitment(teamPlan, this.hr);
947
+
948
+ // Create department
949
+ const dept = new Department({ name, mission, company: this.id });
950
+ const wsPath = this.workspaceManager.createDepartmentWorkspace(dept.id, name);
951
+ dept.workspacePath = wsPath;
952
+
953
+ // Add to department + initialize toolkits
954
+ agents.forEach(agent => {
955
+ dept.addAgent(agent);
956
+ agent.initToolKit(wsPath, this.messageBus);
957
+ });
958
+
959
+ // Set department leader
960
+ const leader = agents.find(a => a.role === 'Project Leader');
961
+ if (leader) {
962
+ dept.setLeader(leader);
963
+ } else if (agents.length > 0) {
964
+ dept.setLeader(agents[0]);
965
+ }
966
+
967
+ this.departments.set(dept.id, dept);
968
+
969
+ this._log('Department created', `"${name}" department established, recruited ${agents.length} talents`);
970
+
971
+ // Fire hooks: department created + agents created
972
+ hookRegistry.trigger(HookEvent.DEPT_CREATED, {
973
+ departmentId: dept.id, departmentName: dept.name, memberCount: agents.length,
974
+ });
975
+ for (const agent of agents) {
976
+ hookRegistry.trigger(HookEvent.AGENT_CREATED, {
977
+ agentId: agent.id, agentName: agent.name, role: agent.role,
978
+ departmentId: dept.id, departmentName: dept.name,
979
+ });
980
+ }
981
+
982
+ // Background async: Agent self-intro + onboarding email + broadcast
983
+ this._onboardAgents(agents, dept).catch(e => console.error('Onboarding process error:', e));
984
+
985
+ // Start group chat loop for new employee
986
+ for (const agent of agents) {
987
+ groupChatLoop.startAgentLoop(agent);
988
+ }
989
+
990
+ // Persist
991
+ this.save();
992
+
993
+ return dept;
994
+ }
995
+
996
+ /**
997
+ * Create department directly from secretary's team plan (no second AI call).
998
+ * The secretary already designed the team in the create_department action.
999
+ * @param {object} params
1000
+ * @param {string} params.departmentName - Department name
1001
+ * @param {string} params.mission - Department mission
1002
+ * @param {Array} params.members - Team members from secretary's plan
1003
+ * @returns {Promise<Department>} Created department
1004
+ */
1005
+ async createDepartmentDirect({ departmentName, mission, members }) {
1006
+ const name = departmentName || 'New Project Dept';
1007
+
1008
+ // Validate and normalize members against JobTemplates
1009
+ const validTemplateIds = new Set(Object.values(JobTemplates).map(t => t.id));
1010
+ const validMembers = (members || []).filter(m => validTemplateIds.has(m.templateId));
1011
+
1012
+ if (validMembers.length === 0) {
1013
+ // Fallback: if secretary returned no valid members, fall back to planDepartment
1014
+ console.warn('⚠️ Secretary returned no valid members, falling back to planDepartment');
1015
+ const plan = await this.planDepartment(name, mission);
1016
+ return await this.confirmPlan(plan.planId);
1017
+ }
1018
+
1019
+ // Build teamPlan in the same format as designTeam returns
1020
+ const teamPlan = {
1021
+ departmentName: name,
1022
+ mission,
1023
+ members: validMembers.map((m, i) => {
1024
+ const template = Object.values(JobTemplates).find(t => t.id === m.templateId);
1025
+ return {
1026
+ templateId: m.templateId,
1027
+ templateTitle: template?.title || m.templateId,
1028
+ name: m.name || `Employee${i + 1}`,
1029
+ isLeader: m.isLeader || false,
1030
+ reportsTo: m.reportsTo ?? (i === 0 ? null : 0),
1031
+ reason: m.reason || '',
1032
+ };
1033
+ }),
1034
+ };
1035
+
1036
+ console.log(`📋 Secretary's direct team plan: ${name}, ${teamPlan.members.length} people`);
1037
+ teamPlan.members.forEach(m => {
1038
+ const prefix = m.isLeader ? '👔' : '👤';
1039
+ console.log(` ${prefix} ${m.name} - ${m.templateTitle}`);
1040
+ });
1041
+
1042
+ // Recruit via HR assistant
1043
+ const agents = this.secretary.hrAssistant.executeRecruitment(teamPlan, this.hr);
1044
+
1045
+ // Create department
1046
+ const dept = new Department({ name, mission, company: this.id });
1047
+ const wsPath = this.workspaceManager.createDepartmentWorkspace(dept.id, name);
1048
+ dept.workspacePath = wsPath;
1049
+
1050
+ // Add to department + initialize toolkits
1051
+ agents.forEach(agent => {
1052
+ dept.addAgent(agent);
1053
+ agent.initToolKit(wsPath, this.messageBus);
1054
+ });
1055
+
1056
+ // Set department leader
1057
+ const leader = agents.find(a => a.role === 'Project Leader');
1058
+ if (leader) {
1059
+ dept.setLeader(leader);
1060
+ } else if (agents.length > 0) {
1061
+ dept.setLeader(agents[0]);
1062
+ }
1063
+
1064
+ this.departments.set(dept.id, dept);
1065
+
1066
+ this._log('Department created', `"${name}" department established, recruited ${agents.length} talents`);
1067
+
1068
+ // Fire hooks
1069
+ hookRegistry.trigger(HookEvent.DEPT_CREATED, {
1070
+ departmentId: dept.id, departmentName: dept.name, memberCount: agents.length,
1071
+ });
1072
+ for (const agent of agents) {
1073
+ hookRegistry.trigger(HookEvent.AGENT_CREATED, {
1074
+ agentId: agent.id, agentName: agent.name, role: agent.role,
1075
+ departmentId: dept.id, departmentName: dept.name,
1076
+ });
1077
+ }
1078
+
1079
+ // Background: Agent self-intro + onboarding
1080
+ this._onboardAgents(agents, dept).catch(e => console.error('Onboarding process error:', e));
1081
+
1082
+ // Start group chat loop
1083
+ for (const agent of agents) {
1084
+ groupChatLoop.startAgentLoop(agent);
1085
+ }
1086
+
1087
+ this.save();
1088
+
1089
+ return dept;
1090
+ }
1091
+
1092
+ /**
1093
+ * Employee onboarding flow: generate self-intro + send onboarding email + broadcast
1094
+ */
1095
+ async _onboardAgents(agents, dept) {
1096
+ for (const agent of agents) {
1097
+ // Let the employee introduce themselves — this is THEIR moment, not secretary's
1098
+ const onboardResult = await agent.onboard({
1099
+ departmentName: dept.name,
1100
+ bossName: this.bossName,
1101
+ });
1102
+
1103
+ // Send greeting to boss (employee's own words, or fallback template)
1104
+ const greetingContent = onboardResult.greeting
1105
+ || `Hi ${this.bossName}, I'm ${agent.name}, just joined "${dept.name}" as ${agent.role}. My motto: "${agent.signature}". Looking forward to working with you!`;
1106
+ agent.sendMailToBoss(null, greetingContent, this);
1107
+
1108
+ // Broadcast to colleagues (employee's own words, or fallback)
1109
+ const allAgentIds = [];
1110
+ this.departments.forEach(d => {
1111
+ d.getMembers().forEach(a => {
1112
+ if (a.id !== agent.id) allAgentIds.push(a.id);
1113
+ });
1114
+ });
1115
+ if (allAgentIds.length > 0) {
1116
+ const broadcastContent = onboardResult.broadcast
1117
+ || `👋 Hi everyone, I'm ${agent.name}, the new ${agent.role} in "${dept.name}". Nice to meet you all!`;
1118
+ this.messageBus.broadcast(agent.id, allAgentIds, broadcastContent, 'broadcast');
1119
+ }
1120
+ }
1121
+ }
1122
+
1123
+ /**
1124
+ * General department lookup: try ID first, then fuzzy match by name
1125
+ * @param {string} idOrName - Department ID or name
1126
+ * @returns {Department|null}
1127
+ */
1128
+ findDepartment(idOrName) {
1129
+ if (!idOrName) return null;
1130
+ // Prioritize exact ID match
1131
+ const byId = this.departments.get(idOrName);
1132
+ if (byId) return byId;
1133
+ // Fallback: match by name
1134
+ for (const d of this.departments.values()) {
1135
+ if (d.name === idOrName || d.name.includes(idOrName) || idOrName.includes(d.name)) {
1136
+ console.log(`🔧 Matched department by name: "${idOrName}" → ${d.id} (${d.name})`);
1137
+ return d;
1138
+ }
1139
+ }
1140
+ return null;
1141
+ }
1142
+
1143
+ createDepartment(name, mission) {
1144
+ const dept = new Department({ name, mission, company: this.id });
1145
+ const wsPath = this.workspaceManager.createDepartmentWorkspace(dept.id, name);
1146
+ dept.workspacePath = wsPath;
1147
+ this.departments.set(dept.id, dept);
1148
+ return dept;
1149
+ }
1150
+
1151
+ hireAgent(departmentId, templateId, name, providerId = null) {
1152
+ const dept = this.findDepartment(departmentId);
1153
+ if (!dept) throw new Error(`Department not found: ${departmentId}`);
1154
+
1155
+ const recruitConfig = this.hr.recruit(templateId, name, providerId);
1156
+ const agent = createEmployee(recruitConfig);
1157
+ dept.addAgent(agent);
1158
+
1159
+ // Initialize toolkit
1160
+ if (dept.workspacePath) {
1161
+ agent.initToolKit(dept.workspacePath, this.messageBus);
1162
+ }
1163
+
1164
+ // Start group chat loop
1165
+ groupChatLoop.startAgentLoop(agent);
1166
+
1167
+ return agent;
1168
+ }
1169
+
1170
+ recallAgent(departmentId, profileId, newSkills = []) {
1171
+ const dept = this.findDepartment(departmentId);
1172
+ if (!dept) throw new Error(`Department not found: ${departmentId}`);
1173
+
1174
+ const recruitConfig = this.hr.recallFromMarket(profileId, newSkills);
1175
+ const agent = createEmployee(recruitConfig);
1176
+ agent.memory.addLongTerm(
1177
+ `Recalled to the "${dept.name}" department, carrying past experience and memories back to work`,
1178
+ 'experience'
1179
+ );
1180
+ dept.addAgent(agent);
1181
+
1182
+ if (dept.workspacePath) {
1183
+ agent.initToolKit(dept.workspacePath, this.messageBus);
1184
+ }
1185
+
1186
+ // Start group chat loop for recalled employee
1187
+ groupChatLoop.startAgentLoop(agent);
1188
+
1189
+ console.log(` 🔄 [${agent.name}] Recalled from talent market, joined "${dept.name}" department`);
1190
+ return agent;
1191
+ }
1192
+
1193
+ dismissAgent(departmentId, agentId, reason = 'Project ended') {
1194
+ const dept = this.findDepartment(departmentId);
1195
+ if (!dept) throw new Error(`Department not found: ${departmentId}`);
1196
+
1197
+ const agent = dept.removeAgent(agentId);
1198
+ if (!agent) throw new Error(`Employee not found: ${agentId}`);
1199
+
1200
+ agent.status = 'dismissed';
1201
+
1202
+ // Stop group chat loop
1203
+ groupChatLoop.stopAgentLoop(agentId);
1204
+
1205
+ // Fire hook: agent dismissed
1206
+ hookRegistry.trigger(HookEvent.AGENT_DISMISSED, {
1207
+ agentId: agent.id, agentName: agent.name, role: agent.role,
1208
+ departmentId: departmentId, reason,
1209
+ });
1210
+
1211
+ const performanceData = {
1212
+ reviews: this.performanceSystem.getReviews(agentId),
1213
+ averageScore: this.performanceSystem.getAverageScore(agentId),
1214
+ };
1215
+
1216
+ const profile = this.talentMarket.register(agent, reason, performanceData);
1217
+
1218
+
1219
+
1220
+ // Clean up message bus inbox
1221
+ this.messageBus.clearInbox(agentId);
1222
+
1223
+ console.log(` 📤 [${agent.name}] has been dismissed, entered talent market`);
1224
+ return profile;
1225
+ }
1226
+
1227
+ /**
1228
+ * Permanently delete a talent from the talent market, and clean up all their messages in mailbox and message bus
1229
+ * @param {string} profileId - Talent market profile ID
1230
+ */
1231
+ deleteTalent(profileId) {
1232
+ const profile = this.talentMarket.remove(profileId);
1233
+ const originalAgentId = profile.originalAgentId;
1234
+
1235
+ // Clean up mailbox messages from this person
1236
+ this.mailbox = this.mailbox.filter(m => m.from?.id !== originalAgentId);
1237
+
1238
+ // Clean up message bus messages (sent and received)
1239
+ this.messageBus.messages = this.messageBus.messages.filter(
1240
+ m => m.from !== originalAgentId && m.to !== originalAgentId
1241
+ );
1242
+ this.messageBus.inbox.delete(originalAgentId);
1243
+
1244
+ this._log('Delete talent', `Permanently deleted "${profile.name}" from talent market and cleaned up related messages`);
1245
+ this.save();
1246
+ return profile;
1247
+ }
1248
+
1249
+ /**
1250
+ * Adjust department staffing - Step 1: Get adjustment plan
1251
+ * @param {string} departmentId - Department ID
1252
+ * @param {string} adjustGoal - Adjustment goal
1253
+ * @returns {object} Adjustment plan (pending approval)
1254
+ */
1255
+ async planAdjustment(departmentId, adjustGoal) {
1256
+ const dept = this.findDepartment(departmentId);
1257
+ if (!dept) throw new Error(`Department not found: ${departmentId}`);
1258
+
1259
+ const adjustPlan = await dept.adjustTeam(adjustGoal, this.secretary, this.providerRegistry);
1260
+
1261
+ const planId = uuidv4();
1262
+ this.pendingPlans.set(planId, {
1263
+ type: 'adjustment',
1264
+ departmentId,
1265
+ departmentName: dept.name,
1266
+ adjustGoal,
1267
+ adjustPlan,
1268
+ });
1269
+
1270
+ this._log('Adjustment plan', `Secretary created adjustment plan for "${dept.name}": fire ${adjustPlan.fires.length}, hire ${adjustPlan.hires.length}, pending approval`);
1271
+
1272
+ return {
1273
+ planId,
1274
+ departmentId,
1275
+ departmentName: dept.name,
1276
+ adjustGoal,
1277
+ reasoning: adjustPlan.reasoning,
1278
+ fires: adjustPlan.fires,
1279
+ hires: adjustPlan.hires.map(h => {
1280
+ // Find recommended provider info
1281
+ const template = this.hr.getTemplate(h.templateId);
1282
+ let providerName = null;
1283
+ let providerModel = null;
1284
+ if (template) {
1285
+ const recommended = this.providerRegistry.recommend(
1286
+ template.category,
1287
+ template.requiredCapabilities
1288
+ );
1289
+ if (recommended) {
1290
+ providerName = recommended.name;
1291
+ providerModel = recommended.model;
1292
+ }
1293
+ }
1294
+ return { ...h, providerName, providerModel };
1295
+ }),
1296
+ };
1297
+ }
1298
+
1299
+ /**
1300
+ * Confirm adjustment plan - Step 2: Execute adjustment
1301
+ * @param {string} planId - Adjustment plan ID
1302
+ */
1303
+ async confirmAdjustment(planId) {
1304
+ const plan = this.pendingPlans.get(planId);
1305
+ if (!plan || plan.type !== 'adjustment') throw new Error('Adjustment plan not found or expired');
1306
+
1307
+ this.pendingPlans.delete(planId);
1308
+
1309
+ const dept = this.departments.get(plan.departmentId);
1310
+ if (!dept) throw new Error(`Department not found: ${plan.departmentId}`);
1311
+
1312
+ const { adjustPlan } = plan;
1313
+
1314
+ // Execute layoffs
1315
+ for (const fire of adjustPlan.fires) {
1316
+ try {
1317
+ this.dismissAgent(plan.departmentId, fire.agentId, fire.reason || 'Department restructuring');
1318
+ this._log('Adjustment layoff', `"${plan.departmentName}": ${fire.name} laid off - ${fire.reason || 'Department restructuring'}`);
1319
+ } catch (e) {
1320
+ console.error(`Layoff failed [${fire.name}]:`, e.message);
1321
+ }
1322
+ }
1323
+
1324
+ // Execute hiring
1325
+ const newAgents = [];
1326
+ if (adjustPlan.hires.length > 0) {
1327
+ // Construct in same format as designTeam plan
1328
+ const hirePlan = {
1329
+ members: adjustPlan.hires.map(h => ({
1330
+ templateId: h.templateId,
1331
+ templateTitle: h.templateTitle || h.templateId,
1332
+ name: h.name,
1333
+ isLeader: h.isLeader || false,
1334
+ reportsTo: h.reportsTo ?? 0,
1335
+ reason: h.reason,
1336
+ })),
1337
+ };
1338
+
1339
+ const agents = this.secretary.hrAssistant.executeRecruitment(hirePlan, this.hr);
1340
+ for (const agent of agents) {
1341
+ if (!agent) continue;
1342
+ dept.addAgent(agent);
1343
+ if (dept.workspacePath) {
1344
+ agent.initToolKit(dept.workspacePath, this.messageBus);
1345
+ }
1346
+ newAgents.push(agent);
1347
+ }
1348
+
1349
+ if (newAgents.length > 0) {
1350
+ this._log('Adjustment hire', `"${plan.departmentName}": hired ${newAgents.length} new employees`);
1351
+ // Background async onboarding
1352
+ this._onboardAgents(newAgents, dept).catch(e => console.error('Onboarding process error:', e));
1353
+ // Start group chat loop for new employee
1354
+ for (const agent of newAgents) {
1355
+ groupChatLoop.startAgentLoop(agent);
1356
+ }
1357
+ }
1358
+ }
1359
+
1360
+ this.save();
1361
+ return dept;
1362
+ }
1363
+
1364
+ /**
1365
+ * Disband department - all members enter talent market
1366
+ * @param {string} departmentId - Department ID
1367
+ * @param {string} reason - Disbanding reason
1368
+ */
1369
+ disbandDepartment(departmentId, reason = 'Organizational restructuring') {
1370
+ const dept = this.findDepartment(departmentId);
1371
+ if (!dept) throw new Error(`Department not found: ${departmentId}`);
1372
+
1373
+ const deptName = dept.name;
1374
+ const members = dept.getMembers();
1375
+
1376
+ // Dismiss all members one by one
1377
+ for (const agent of members) {
1378
+ try {
1379
+ this.dismissAgent(departmentId, agent.id, `Department "${deptName}" disbanded: ${reason}`);
1380
+ } catch (e) {
1381
+ console.error(`Dismissal failed [${agent.name}]:`, e.message);
1382
+ }
1383
+ }
1384
+
1385
+ // Delete department
1386
+ this.departments.delete(departmentId);
1387
+
1388
+ this._log('Department disbanded', `"${deptName}" department disbanded, ${members.length} employees entered talent market. Reason: ${reason}`);
1389
+ this.save();
1390
+
1391
+ return { departmentName: deptName, dismissedCount: members.length };
1392
+ }
1393
+
1394
+ evaluateAgent(agentId, reviewerId, taskTitle, scores = null, comment = null) {
1395
+ let agent = null;
1396
+ let reviewer = null;
1397
+
1398
+ for (const dept of this.departments.values()) {
1399
+ if (!agent) agent = dept.agents.get(agentId);
1400
+ if (!reviewer) reviewer = dept.agents.get(reviewerId);
1401
+ }
1402
+
1403
+ if (!agent) throw new Error(`Employee not found: ${agentId}`);
1404
+ if (!reviewer) throw new Error(`Reviewer not found: ${reviewerId}`);
1405
+
1406
+ let review;
1407
+ if (scores) {
1408
+ review = this.performanceSystem.evaluate({
1409
+ agent, reviewer, taskTitle, scores, comment,
1410
+ });
1411
+ } else {
1412
+ review = this.performanceSystem.autoEvaluate({
1413
+ agent, reviewer, taskTitle,
1414
+ });
1415
+ }
1416
+
1417
+ agent.receiveFeedback(review);
1418
+ return review;
1419
+ }
1420
+
1421
+ viewTalentMarket() {
1422
+ this.talentMarket.print();
1423
+ return this.talentMarket.listAvailable();
1424
+ }
1425
+
1426
+ searchTalentMarket(criteria) {
1427
+ return this.talentMarket.search(criteria);
1428
+ }
1429
+
1430
+ viewPerformanceReport(agentId, agentName = '') {
1431
+ this.performanceSystem.printReport(agentId, agentName);
1432
+ }
1433
+
1434
+ viewAgentMemory(agentId) {
1435
+ for (const dept of this.departments.values()) {
1436
+ const agent = dept.agents.get(agentId);
1437
+ if (agent) {
1438
+ agent.memory.print(agent.name);
1439
+ return agent.memory.getSummary();
1440
+ }
1441
+ }
1442
+ throw new Error(`Employee not found: ${agentId}`);
1443
+ }
1444
+
1445
+ getOverview() {
1446
+ const overview = {
1447
+ company: this.name,
1448
+ boss: this.bossName,
1449
+ departments: [],
1450
+ totalAgents: 0,
1451
+ talentMarket: this.talentMarket.getStats(),
1452
+ };
1453
+
1454
+ this.departments.forEach(dept => {
1455
+ const summary = dept.getSummary();
1456
+ overview.departments.push(summary);
1457
+ overview.totalAgents += summary.memberCount;
1458
+ });
1459
+
1460
+ return overview;
1461
+ }
1462
+
1463
+ listProviders(category = null) {
1464
+ if (category) {
1465
+ return this.providerRegistry.getByCategory(category);
1466
+ }
1467
+ return this.providerRegistry.listAll();
1468
+ }
1469
+
1470
+ listJobTemplates(category = null) {
1471
+ if (category) {
1472
+ return this.hr.listTemplatesByCategory(category);
1473
+ }
1474
+ return this.hr.listAllTemplates();
1475
+ }
1476
+
1477
+ configureProvider(providerId, apiKey) {
1478
+ const provider = this.providerRegistry.configure(providerId, apiKey);
1479
+ // Sync with the appropriate client
1480
+ if (provider.isWeb) {
1481
+ // For web providers, apiKey carries the session token (legacy cookie field)
1482
+ webClientRegistry.configureCookie(providerId, apiKey);
1483
+ } else {
1484
+ // Clear LLM client cache to ensure next call uses new apiKey
1485
+ llmClient.clearClient(providerId);
1486
+ }
1487
+ this._log('Configure provider', `${provider.name} has been ${apiKey ? 'enabled' : 'disabled'}`);
1488
+ return provider;
1489
+ }
1490
+
1491
+ getProviderDashboard() {
1492
+ return this.providerRegistry.getStats();
1493
+ }
1494
+
1495
+ /**
1496
+ * Assign task to department and let employees actually execute it
1497
+ * This is the core method that makes AI employees "actually work"
1498
+ * Uses requirement management: standardize requirement → leader decomposes workflow → execute by DAG → group chat communication
1499
+ * @param {string} departmentId - Target department ID
1500
+ * @param {string} taskDescription - Task description
1501
+ * @param {string} [taskTitle] - Task title
1502
+ * @returns {Promise<object>} Execution result
1503
+ */
1504
+ async assignTaskToDepartment(departmentId, taskDescription, taskTitle = null) {
1505
+ const dept = this.findDepartment(departmentId);
1506
+ if (!dept) throw new Error(`Department not found: ${departmentId}`);
1507
+
1508
+ const members = dept.getMembers();
1509
+ if (members.length === 0) throw new Error(`Department "${dept.name}" has no employees`);
1510
+
1511
+ const title = taskTitle || taskDescription.slice(0, 50);
1512
+ this._log('Task assigned', `"${dept.name}" received task: "${title}"`);
1513
+
1514
+ // Fire hook: task assigned
1515
+ hookRegistry.trigger(HookEvent.TASK_ASSIGNED, {
1516
+ departmentId: dept.id, departmentName: dept.name, taskTitle: title,
1517
+ });
1518
+
1519
+ // 1. Create standardized requirement
1520
+ const requirement = this.requirementManager.create({
1521
+ title,
1522
+ description: taskDescription,
1523
+ departmentId: dept.id,
1524
+ departmentName: dept.name,
1525
+ bossMessage: taskDescription,
1526
+ });
1527
+
1528
+ // Persist immediately to prevent data loss after requirement creation
1529
+ this.save();
1530
+ console.log(`📝 Requirement created: ${requirement.id} - ${title}`);
1531
+
1532
+ // 2. Leader decomposes workflow
1533
+ const leader = dept.getLeader() || members[0];
1534
+
1535
+ // Update liveStatus: record leader info during planning phase
1536
+ requirement.updateLiveStatus({
1537
+ currentAgent: leader.name,
1538
+ currentAgentId: leader.id,
1539
+ currentAgentAvatar: leader.avatar,
1540
+ currentAction: `${leader.name} is analyzing and decomposing the requirement...`,
1541
+ });
1542
+ this.save();
1543
+
1544
+ try {
1545
+ await this.requirementManager.planWorkflow(
1546
+ requirement, members
1547
+ );
1548
+ } catch (e) {
1549
+ console.error('Workflow decomposition failed:', e.message);
1550
+ // Save current state even if decomposition fails (fallback workflow is set inside planWorkflow)
1551
+ }
1552
+
1553
+ // Save again after workflow decomposition
1554
+ this.save();
1555
+
1556
+ // 3. Execute by workflow DAG
1557
+ let summary;
1558
+ try {
1559
+ summary = await this.requirementManager.executeWorkflow(
1560
+ requirement, dept, this.performanceSystem
1561
+ );
1562
+ } catch (e) {
1563
+ console.error('Workflow execution failed:', e.message);
1564
+ // Update requirement status to failed
1565
+ requirement.status = 'failed';
1566
+ requirement.completedAt = new Date();
1567
+ requirement.summary = { totalTasks: 0, successTasks: 0, failedTasks: 0, totalDuration: 0, outputs: [], error: e.message };
1568
+ requirement.addGroupMessage(
1569
+ { name: 'System', role: 'system' },
1570
+ `❌ Requirement execution failed: ${e.message}`,
1571
+ 'system', null, { auto: true }
1572
+ );
1573
+ this.save();
1574
+ summary = requirement.summary;
1575
+ }
1576
+
1577
+ // 4. Let leader send report email
1578
+ if (leader) {
1579
+ let reportContent = `Requirement "${title}" completed!\n\n`;
1580
+ reportContent += `📊 Execution Summary:\n`;
1581
+ reportContent += `- Tasks completed: ${summary.successTasks}/${summary.totalTasks}\n`;
1582
+ reportContent += `- Total duration: ${Math.round(summary.totalDuration / 1000)}s\n\n`;
1583
+ reportContent += `📝 Member outputs:\n`;
1584
+ for (const o of (summary.outputs || [])) {
1585
+ reportContent += `\n[${o.agentName} (${o.role})]\n`;
1586
+ reportContent += (o.content || '').slice(0, 300);
1587
+ if ((o.content || '').length > 300) reportContent += '...';
1588
+ reportContent += '\n';
1589
+ }
1590
+ leader.sendMailToBoss(`📋 Requirement Report: ${title}`, reportContent, this);
1591
+ }
1592
+
1593
+ // 5. Record to progress reports
1594
+ this.progressReports.push({
1595
+ time: new Date(),
1596
+ type: 'task_completed',
1597
+ reports: [{
1598
+ department: dept.name,
1599
+ task: title,
1600
+ requirementId: requirement.id,
1601
+ success: summary.successTasks === summary.totalTasks,
1602
+ detail: `${summary.successTasks}/${summary.totalTasks} subtasks completed, took ${Math.round(summary.totalDuration / 1000)}s`,
1603
+ }],
1604
+ });
1605
+
1606
+ // Fire hook: task completed
1607
+ hookRegistry.trigger(HookEvent.TASK_COMPLETED, {
1608
+ departmentId: dept.id, departmentName: dept.name, taskTitle: title,
1609
+ totalTasks: summary.totalTasks, successTasks: summary.successTasks,
1610
+ });
1611
+
1612
+ this._log('Task completed', `"${dept.name}" completed task: "${title}", ${summary.successTasks}/${summary.totalTasks} succeeded`);
1613
+ this.save();
1614
+
1615
+ // Return summary with requirement ID
1616
+ return {
1617
+ requirementId: requirement.id,
1618
+ projectId: requirement.id,
1619
+ title,
1620
+ department: dept.name,
1621
+ departmentId: dept.id,
1622
+ totalTasks: summary.totalTasks,
1623
+ successTasks: summary.successTasks,
1624
+ failedTasks: summary.failedTasks,
1625
+ totalDuration: summary.totalDuration,
1626
+ outputs: (summary.outputs || []).map(o => ({
1627
+ agentName: o.agentName,
1628
+ role: o.role,
1629
+ output: o.content,
1630
+ outputType: o.outputType,
1631
+ toolResults: o.metadata?.toolResults || [],
1632
+ success: true,
1633
+ duration: o.metadata?.duration || 0,
1634
+ })),
1635
+ completedAt: new Date(),
1636
+ };
1637
+ }
1638
+
1639
+ /**
1640
+ * Convert an approved Sprint into a standard Requirement and execute it.
1641
+ * Only team members participate (not the entire department).
1642
+ * @param {object} sprint - Sprint object
1643
+ * @param {object} team - Team object
1644
+ * @returns {Promise<object>} The created requirement
1645
+ */
1646
+ async assignSprintAsDepartmentTask(sprint, team) {
1647
+ const dept = this.findDepartment(team.departmentId);
1648
+ if (!dept) throw new Error(`Department not found: ${team.departmentId}`);
1649
+
1650
+ const members = team.memberIds.map(mid => dept.agents.get(mid)).filter(Boolean);
1651
+ if (members.length === 0) throw new Error(`Team "${team.name}" has no valid members`);
1652
+
1653
+ const leader = dept.agents.get(team.leaderId) || members[0];
1654
+
1655
+ // Build task description from sprint plan + goal
1656
+ const taskDescription = sprint.plan
1657
+ ? `# Sprint Goal\n${sprint.goal}\n\n# Implementation Plan\n${sprint.plan}`
1658
+ : sprint.goal;
1659
+ const title = sprint.title;
1660
+
1661
+ this._log('Sprint → Requirement', `Team "${team.name}" sprint "${title}" approved, creating requirement`);
1662
+
1663
+ // 1. Create standard requirement
1664
+ const requirement = this.requirementManager.create({
1665
+ title,
1666
+ description: taskDescription,
1667
+ departmentId: dept.id,
1668
+ departmentName: dept.name,
1669
+ bossMessage: sprint.goal,
1670
+ });
1671
+
1672
+ // Link sprint ↔ requirement
1673
+ sprint.requirementId = requirement.id;
1674
+ this.save();
1675
+ console.log(`📝 Sprint → Requirement created: ${requirement.id} - ${title}`);
1676
+
1677
+ // 2. Leader decomposes workflow (using only team members, not entire dept)
1678
+ requirement.updateLiveStatus({
1679
+ currentAgent: leader.name,
1680
+ currentAgentId: leader.id,
1681
+ currentAgentAvatar: leader.avatar,
1682
+ currentAction: `${leader.name} is analyzing and decomposing the sprint plan...`,
1683
+ });
1684
+ this.save();
1685
+
1686
+ try {
1687
+ await this.requirementManager.planWorkflow(
1688
+ requirement, members
1689
+ );
1690
+ } catch (e) {
1691
+ console.error('Sprint workflow decomposition failed:', e.message);
1692
+ }
1693
+ this.save();
1694
+
1695
+ // 3. Execute workflow DAG
1696
+ let summary;
1697
+ try {
1698
+ summary = await this.requirementManager.executeWorkflow(
1699
+ requirement, dept, this.performanceSystem
1700
+ );
1701
+ } catch (e) {
1702
+ console.error('Sprint workflow execution failed:', e.message);
1703
+ requirement.status = 'failed';
1704
+ requirement.completedAt = new Date();
1705
+ requirement.summary = { totalTasks: 0, successTasks: 0, failedTasks: 0, totalDuration: 0, outputs: [], error: e.message };
1706
+ requirement.addGroupMessage(
1707
+ { name: 'System', role: 'system' },
1708
+ `❌ Sprint requirement execution failed: ${e.message}`,
1709
+ 'system', null, { auto: true }
1710
+ );
1711
+ this.save();
1712
+ summary = requirement.summary;
1713
+ }
1714
+
1715
+ // 4. Leader sends report email
1716
+ if (leader && summary) {
1717
+ let reportContent = `Sprint "${title}" completed!\n\n`;
1718
+ reportContent += `📊 Execution Summary:\n`;
1719
+ reportContent += `- Tasks completed: ${summary.successTasks}/${summary.totalTasks}\n`;
1720
+ reportContent += `- Total duration: ${Math.round(summary.totalDuration / 1000)}s\n\n`;
1721
+ reportContent += `📝 Member outputs:\n`;
1722
+ for (const o of (summary.outputs || [])) {
1723
+ reportContent += `\n[${o.agentName} (${o.role})]\n`;
1724
+ reportContent += (o.content || '').slice(0, 300);
1725
+ if ((o.content || '').length > 300) reportContent += '...';
1726
+ reportContent += '\n';
1727
+ }
1728
+ leader.sendMailToBoss(`📋 Sprint Report: ${title}`, reportContent, this);
1729
+ }
1730
+
1731
+ // 5. Update sprint status based on requirement result
1732
+ const { SprintStatus } = await import('@/core/organization/team.js');
1733
+ if (requirement.status === 'completed' || requirement.status === 'pending_approval') {
1734
+ // For pending_approval, the sprint is done but the requirement awaits Boss review
1735
+ sprint.status = SprintStatus.COMPLETED;
1736
+ sprint.completedAt = new Date();
1737
+ sprint.summary = requirement.summary;
1738
+ } else if (requirement.status === 'failed') {
1739
+ sprint.status = SprintStatus.FAILED;
1740
+ sprint.completedAt = new Date();
1741
+ sprint.summary = requirement.summary;
1742
+ }
1743
+
1744
+ this.save();
1745
+ return requirement;
1746
+ }
1747
+
1748
+ /**
1749
+ * Boss sends a message in a requirement's group chat
1750
+ * The leader will see the message and decide whether to adjust the plan
1751
+ * @param {string} requirementId - Requirement ID
1752
+ * @param {string} message - Boss's message
1753
+ * @returns {Promise<object>} Leader's response
1754
+ */
1755
+ async sendBossGroupMessage(requirementId, message) {
1756
+ const requirement = this.requirementManager.get(requirementId);
1757
+ if (!requirement) throw new Error('Requirement not found');
1758
+
1759
+ const dept = this.findDepartment(requirement.departmentId);
1760
+ if (!dept) throw new Error('Department not found');
1761
+
1762
+ const leader = dept.getLeader() || dept.getMembers()[0];
1763
+ if (!leader) throw new Error('No leader found in department');
1764
+
1765
+ // 1. Add Boss message to group chat (expand [[file:path]] → full format)
1766
+ const { content: expandedMessage, invalidRefs } = expandFileReferences(message, requirement.departmentId, dept.workspacePath);
1767
+ requirement.addGroupMessage(
1768
+ {
1769
+ id: 'boss',
1770
+ name: this.bossName || 'Boss',
1771
+ avatar: this.bossAvatar || null,
1772
+ role: 'Boss',
1773
+ },
1774
+ expandedMessage,
1775
+ 'message'
1776
+ );
1777
+ // Auto-feedback: notify about invalid file references
1778
+ if (invalidRefs.length > 0) {
1779
+ const invalidList = invalidRefs.map(f => ` - ${f}`).join('\n');
1780
+ requirement.addGroupMessage(
1781
+ { id: 'system', name: 'System', role: 'system' },
1782
+ `⚠️ File reference error: the following files do not exist in workspace:\n${invalidList}`,
1783
+ 'message', null, { auto: true }
1784
+ );
1785
+ }
1786
+ this.save();
1787
+
1788
+ // 1.5 Trigger group chat loop: notify all members of new message (Boss message, everyone should pay attention)
1789
+ const allMembers = dept.getMembers();
1790
+ for (const member of allMembers) {
1791
+ // Delayed trigger to avoid everyone processing at the same time
1792
+ setTimeout(() => {
1793
+ groupChatLoop.triggerImmediate(member.id, requirementId, { from: { id: 'boss' }, content: message }).catch(() => {});
1794
+ }, 500 + Math.random() * 2000);
1795
+ }
1796
+
1797
+ // 2. Leader asynchronously handles Boss message (use LLM to decide if adjustment needed)
1798
+ const leaderResponse = await this._leaderHandleBossMessage(leader, requirement, dept, message);
1799
+
1800
+ // 3. Add Leader reply to group chat
1801
+ if (leaderResponse.reply) {
1802
+ requirement.addGroupMessage(leader, leaderResponse.reply, 'message', null, { auto: true });
1803
+ }
1804
+
1805
+ // 4. Execute operations based on Leader decision
1806
+ if (leaderResponse.action === 'stop') {
1807
+ // Stop project
1808
+ requirement.status = 'failed';
1809
+ requirement.completedAt = new Date();
1810
+ requirement.updateLiveStatus({
1811
+ currentAction: 'Boss requested stop, project halted',
1812
+ currentAgent: null,
1813
+ });
1814
+ requirement.addGroupMessage(
1815
+ { name: 'System', role: 'system' },
1816
+ `⏹️ Project stopped by Boss request`,
1817
+ 'system', null, { auto: true }
1818
+ );
1819
+ } else if (leaderResponse.action === 'restart') {
1820
+ // Restart project (mark as failed, frontend can use restart button)
1821
+ requirement.addGroupMessage(
1822
+ { name: 'System', role: 'system' },
1823
+ `🔄 Boss requested project restart, replanning...`,
1824
+ 'system', null, { auto: true }
1825
+ );
1826
+ // Async re-execute, non-blocking
1827
+ const title = requirement.title;
1828
+ const description = requirement.description;
1829
+ const deptId = requirement.departmentId;
1830
+ // Delete old requirement
1831
+ this.requirementManager.requirements.delete(requirementId);
1832
+ // Re-dispatch
1833
+ this.assignTaskToDepartment(deptId, description, title).catch(e => {
1834
+ console.error('Restart requirement failed:', e.message);
1835
+ });
1836
+ } else if (leaderResponse.action === 'adjust' && leaderResponse.adjustments) {
1837
+ // Adjust plan: leader has explained the adjustment in reply, trigger actual workflow modification
1838
+ requirement.addGroupMessage(
1839
+ { name: 'System', role: 'system' },
1840
+ `📝 ${leader.name} is adjusting the plan based on Boss instructions...`,
1841
+ 'system'
1842
+ );
1843
+
1844
+ // Record old workflow for reference
1845
+ const previousWorkflow = requirement.workflow?.nodes?.map(n =>
1846
+ `- [${n.status}] ${n.title} → ${n.assigneeName || 'unknown'}${n.dependencies?.length ? ` (deps: ${n.dependencies.join(', ')})` : ''}`
1847
+ ).join('\n') || 'No previous workflow';
1848
+
1849
+ // Record existing output files (preserved during adjustment, not deleted)
1850
+ const existingOutputs = requirement.outputs || [];
1851
+
1852
+ // Reset requirement status (preserve outputs, do not delete existing files)
1853
+ requirement.status = RequirementStatus.PLANNING;
1854
+ requirement.workflow = null;
1855
+ // Note: do not clear outputs, modify/supplement on top of existing results
1856
+ requirement.summary = null;
1857
+ requirement.completedAt = null;
1858
+ requirement.updateLiveStatus({
1859
+ currentAgent: leader.name,
1860
+ currentAgentId: leader.id,
1861
+ currentAgentAvatar: leader.avatar,
1862
+ currentAction: `${leader.name} is re-planning the workflow based on Boss's instructions...`,
1863
+ toolCallsInProgress: [],
1864
+ recentFileChanges: [],
1865
+ });
1866
+ this.save();
1867
+
1868
+ // Async replan + execute (non-blocking API return)
1869
+ const members = dept.getMembers();
1870
+ const adjustmentContext = {
1871
+ bossMessage: message,
1872
+ adjustments: leaderResponse.adjustments,
1873
+ previousWorkflow,
1874
+ existingOutputs: existingOutputs.map(o => o.fileName || o.title || 'unknown').join(', '),
1875
+ };
1876
+
1877
+ (async () => {
1878
+ try {
1879
+ await this.requirementManager.planWorkflow(
1880
+ requirement, members, adjustmentContext
1881
+ );
1882
+ this.save();
1883
+
1884
+ // Re-execute adjusted workflow
1885
+ await this.requirementManager.executeWorkflow(
1886
+ requirement, dept, this.performanceSystem
1887
+ );
1888
+ this.save();
1889
+
1890
+ // After completion, leader sends report email
1891
+ if (leader) {
1892
+ const summary = requirement.summary || {};
1893
+ let reportContent = `Requirement "${requirement.title}" has been re-completed after adjustment!\n\n`;
1894
+ reportContent += `📊 Execution Results:\n`;
1895
+ reportContent += `- Completed tasks: ${summary.successTasks || 0}/${summary.totalTasks || 0}\n`;
1896
+ reportContent += `- Total duration: ${Math.round((summary.totalDuration || 0) / 1000)}s\n\n`;
1897
+ reportContent += `📝 Adjustment reason: ${message}\n`;
1898
+ leader.sendMailToBoss(`📋 Adjusted Requirement Report: ${requirement.title}`, reportContent, this);
1899
+ }
1900
+ } catch (e) {
1901
+ console.error('Adjust workflow failed:', e.message);
1902
+ requirement.status = RequirementStatus.FAILED;
1903
+ requirement.completedAt = new Date();
1904
+ requirement.addGroupMessage(
1905
+ { name: 'System', role: 'system' },
1906
+ `❌ Adjustment plan execution failed: ${e.message}`,
1907
+ 'system', null, { auto: true }
1908
+ );
1909
+ this.save();
1910
+ }
1911
+ })();
1912
+ } else if (leaderResponse.action === 'approve') {
1913
+ // Boss approved the requirement — finalize it
1914
+ requirement.status = RequirementStatus.COMPLETED;
1915
+ requirement.completedAt = new Date();
1916
+ requirement.updateLiveStatus({
1917
+ currentAction: 'Boss approved — requirement completed',
1918
+ currentAgent: null,
1919
+ });
1920
+ requirement.addGroupMessage(
1921
+ { name: 'System', role: 'system' },
1922
+ `✅ Requirement "${requirement.title}" has been approved by Boss and is now completed!`,
1923
+ 'system', null, { auto: true }
1924
+ );
1925
+
1926
+ // Leader sends completion report email
1927
+ if (leader) {
1928
+ const summary = requirement.summary || {};
1929
+ let reportContent = `Requirement "${requirement.title}" has been approved and completed!\n\n`;
1930
+ reportContent += `📊 Execution Results:\n`;
1931
+ reportContent += `- Completed tasks: ${summary.successTasks || 0}/${summary.totalTasks || 0}\n`;
1932
+ reportContent += `- Total duration: ${Math.round((summary.totalDuration || 0) / 1000)}s\n`;
1933
+ leader.sendMailToBoss(`✅ Requirement Approved: ${requirement.title}`, reportContent, this);
1934
+ }
1935
+ }
1936
+ // action === 'continue' → No special handling needed, continue as normal
1937
+
1938
+ this.save();
1939
+
1940
+ return {
1941
+ requirementId,
1942
+ bossMessage: message,
1943
+ leaderReply: leaderResponse.reply,
1944
+ action: leaderResponse.action,
1945
+ };
1946
+ }
1947
+
1948
+ /**
1949
+ * Boss sends a message in department group chat
1950
+ * @param {string} departmentId - Department ID
1951
+ * @param {string} message - Message content
1952
+ */
1953
+ sendBossDeptGroupMessage(departmentId, message) {
1954
+ const dept = this.findDepartment(departmentId);
1955
+ if (!dept) throw new Error('Department not found');
1956
+
1957
+ // 1. Add Boss message to department group chat
1958
+ dept.addGroupMessage(
1959
+ {
1960
+ id: 'boss',
1961
+ name: this.bossName || 'Boss',
1962
+ avatar: this.bossAvatar || null,
1963
+ role: 'Boss',
1964
+ },
1965
+ message,
1966
+ 'message'
1967
+ );
1968
+ this.save();
1969
+
1970
+ // 2. Trigger group chat loop: notify all department members of new message
1971
+ const allMembers = dept.getMembers();
1972
+ for (const member of allMembers) {
1973
+ // Delayed trigger to avoid everyone processing at the same time
1974
+ setTimeout(() => {
1975
+ groupChatLoop.triggerImmediate(member.id, `dept-${dept.id}`, { from: { id: 'boss' }, content: message }).catch(() => {});
1976
+ }, 500 + Math.random() * 2000);
1977
+ }
1978
+
1979
+ return { groupChat: dept.groupChat };
1980
+ }
1981
+
1982
+ /**
1983
+ * Leader uses LLM to handle Boss message in group chat
1984
+ * @private
1985
+ */
1986
+ async _leaderHandleBossMessage(leader, requirement, department, bossMessage) {
1987
+ if (!leader.canChat()) {
1988
+ return {
1989
+ reply: `Received Boss instructions! I will execute them diligently.`,
1990
+ action: 'continue',
1991
+ };
1992
+ }
1993
+
1994
+ try {
1995
+ // Build group chat history context (latest 20 messages)
1996
+ const recentChat = (requirement.groupChat || []).slice(-20).map(m => {
1997
+ const sender = m.from?.name || 'Unknown';
1998
+ const role = m.from?.role ? `(${m.from.role})` : '';
1999
+ return `[${sender}${role}]: ${m.content}`;
2000
+ }).join('\n');
2001
+
2002
+ // Build workflow status
2003
+ const workflowStatus = requirement.workflow?.nodes?.map(n => {
2004
+ const agent = department.agents.get(n.assigneeId);
2005
+ return `- [${n.status}] ${n.title} (assigned to: ${agent?.name || n.assigneeName || 'unknown'})`;
2006
+ }).join('\n') || 'No workflow yet';
2007
+
2008
+ const p = leader.personality || {};
2009
+ const response = await leader.chat([
2010
+ {
2011
+ role: 'system',
2012
+ content: `You are "${leader.name}", the project leader of department "${department.name}".
2013
+ Your personality: ${p.trait || 'Professional'}. Speaking style: ${p.tone || 'Normal'}.
2014
+ You are leading the team to work on requirement "${requirement.title}": ${requirement.description}
2015
+
2016
+ Current project status: ${requirement.status}
2017
+ Current workflow:
2018
+ ${workflowStatus}
2019
+
2020
+ Recent group chat:
2021
+ ${recentChat}
2022
+
2023
+ The Boss (your employer) just sent a message in the group chat. You need to:
2024
+ 1. Carefully analyze the Boss's intent
2025
+ 2. Decide what action to take
2026
+ 3. Reply naturally in your personality style, addressing the Boss respectfully
2027
+
2028
+ You MUST reply in JSON format:
2029
+ {
2030
+ "reply": "Your natural reply to the Boss (addressing them as Boss, speaking in your personality style, explaining what you'll do)",
2031
+ "action": "continue|adjust|stop|restart|approve",
2032
+ "adjustments": "If action is 'adjust', briefly describe what changes you'll make to the plan"
2033
+ }
2034
+
2035
+ Action rules:
2036
+ - "continue": Boss is just commenting/encouraging, no changes needed. Or giving minor feedback that doesn't change the plan.
2037
+ - "approve": Boss is satisfied with the results and wants to close/accept/finalize the requirement. Use when Boss says things like "looks good", "approved", "OK", "done", "accept", "通过", "可以", "没问题", "完成", "好的", "确认", "LGTM", "ship it", etc. ${requirement.status === 'pending_approval' ? '**IMPORTANT: The project is currently PENDING APPROVAL. If the Boss seems satisfied or gives positive feedback, use "approve".**' : ''}
2038
+ - "adjust": Boss wants to modify, supplement, or revise the current plan. IMPORTANT: This means working on top of existing results — existing files and outputs will be PRESERVED, only new/modified content will be added. Use this when Boss says things like "add more", "revise", "change X to Y", "also include", "supplement", "modify", "adjust" etc.
2039
+ - "stop": Boss explicitly says to stop, halt, or cancel the project.
2040
+ - "restart": Boss EXPLICITLY wants to start over completely from scratch, redo everything, or completely restart. Use ONLY when Boss clearly says "start over", "redo from scratch", "start fresh" etc. All existing files will be DELETED.
2041
+
2042
+ Reply in the same language the Boss used. Be concise but warm.`
2043
+ },
2044
+ {
2045
+ role: 'user',
2046
+ content: `Boss says: "${bossMessage}"`
2047
+ },
2048
+ ], { temperature: 0.7, maxTokens: 512 });
2049
+
2050
+ // Parse JSON
2051
+ const tick = String.fromCharCode(96);
2052
+ const fence = tick + tick + tick;
2053
+ let jsonStr = response.content
2054
+ .replace(fence + 'json', '').replace(fence, '')
2055
+ .replace(fence + 'json', '').replace(fence, '')
2056
+ .trim();
2057
+
2058
+ try {
2059
+ const parsed = JSON.parse(jsonStr);
2060
+ return {
2061
+ reply: parsed.reply || 'Understood, Boss!',
2062
+ action: ['continue', 'adjust', 'stop', 'restart', 'approve'].includes(parsed.action) ? parsed.action : 'continue',
2063
+ adjustments: parsed.adjustments || null,
2064
+ };
2065
+ } catch {
2066
+ // JSON parse failed, use raw reply
2067
+ return {
2068
+ reply: response.content?.trim() || 'Understood, Boss!',
2069
+ action: 'continue',
2070
+ };
2071
+ }
2072
+ } catch (e) {
2073
+ console.error(`Leader ${leader.name} failed to handle boss message:`, e.message);
2074
+ return {
2075
+ reply: `Received Boss instructions, I will handle it promptly!`,
2076
+ action: 'continue',
2077
+ };
2078
+ }
2079
+ }
2080
+
2081
+ /**
2082
+ * Get full company state data (for Web rendering)
2083
+ */
2084
+ getFullState() {
2085
+ const departments = [];
2086
+
2087
+ // Calculate company-wide Token/cost statistics
2088
+ let companyTotalTokens = 0;
2089
+ let companyTotalCost = 0;
2090
+
2091
+ this.departments.forEach(dept => {
2092
+ let deptTokens = 0;
2093
+ let deptCost = 0;
2094
+ const members = dept.getMembers().map(a => {
2095
+ const usage = a.tokenUsage || { totalTokens: 0, totalCost: 0, promptTokens: 0, completionTokens: 0, callCount: 0 };
2096
+ deptTokens += usage.totalTokens;
2097
+ deptCost += usage.totalCost;
2098
+ return {
2099
+ id: a.id,
2100
+ name: a.name,
2101
+ role: a.role,
2102
+ avatar: a.avatar,
2103
+ gender: a.gender,
2104
+ age: a.age,
2105
+ signature: a.signature,
2106
+ personality: a.personality,
2107
+ status: a.status,
2108
+ provider: a.getProviderDisplayInfo(),
2109
+ cliBackend: a.cliBackend || null,
2110
+ fallbackProvider: a.getFallbackProviderName(),
2111
+ skills: a.skills,
2112
+ reportsTo: a.reportsTo,
2113
+ subordinates: a.subordinates,
2114
+ memory: a.memory.getSummary(),
2115
+ performanceHistory: a.performanceHistory,
2116
+ avgScore: a.performanceHistory.length > 0
2117
+ ? Math.round(a.performanceHistory.reduce((s, p) => s + p.score, 0) / a.performanceHistory.length)
2118
+ : null,
2119
+ taskCount: a.taskHistory.length,
2120
+ tokenUsage: { ...usage },
2121
+ };
2122
+ });
2123
+
2124
+ companyTotalTokens += deptTokens;
2125
+ companyTotalCost += deptCost;
2126
+
2127
+ departments.push({
2128
+ id: dept.id,
2129
+ name: dept.name,
2130
+ mission: dept.mission,
2131
+ status: dept.status,
2132
+ leader: dept.leader,
2133
+ workspacePath: dept.workspacePath || null,
2134
+ groupChat: dept.groupChat || [],
2135
+ members,
2136
+ tokenUsage: { totalTokens: deptTokens, totalCost: deptCost },
2137
+ });
2138
+ });
2139
+
2140
+ // Add secretary and HR consumption
2141
+ const secUsage = this.secretary.tokenUsage || { totalTokens: 0, totalCost: 0 };
2142
+ const hrUsage = this.secretary.hrAssistant.employee.tokenUsage || { totalTokens: 0, totalCost: 0 };
2143
+ companyTotalTokens += secUsage.totalTokens + hrUsage.totalTokens;
2144
+ companyTotalCost += secUsage.totalCost + hrUsage.totalCost;
2145
+
2146
+ return {
2147
+ id: this.id,
2148
+ name: this.name,
2149
+ boss: this.bossName,
2150
+ bossAvatar: this.bossAvatar,
2151
+ secretary: {
2152
+ name: this.secretary.name,
2153
+ avatar: this.secretary.avatar,
2154
+ gender: this.secretary.gender,
2155
+ age: this.secretary.age,
2156
+ signature: this.secretary.signature,
2157
+ prompt: this.secretary.prompt,
2158
+ provider: this.secretary.getProviderDisplayInfo().name,
2159
+ providerId: this.secretary.getProviderDisplayInfo().id,
2160
+ // Optional general-purpose + CLI + browser provider list (enabled only)
2161
+ availableProviders: [
2162
+ ...this.providerRegistry.getByCategory('general').filter(p => !p.isWeb).map(p => ({
2163
+ id: p.id,
2164
+ name: p.name,
2165
+ })),
2166
+ ...this.providerRegistry.getByCategory('cli').map(p => ({
2167
+ id: p.id,
2168
+ name: `${p.cliIcon || '🖥️'} ${p.name} (CLI)`,
2169
+ isCLI: true,
2170
+ cliBackendId: p.cliBackendId,
2171
+ })),
2172
+ ...this.providerRegistry.getByCategory('browser').map(p => ({
2173
+ id: p.id,
2174
+ name: `🌐 ${p.name}`,
2175
+ isWeb: true,
2176
+ })),
2177
+ ],
2178
+ hrAssistant: {
2179
+ name: this.secretary.hrAssistant.employee.name,
2180
+ avatar: this.secretary.hrAssistant.employee.avatar,
2181
+ signature: this.secretary.hrAssistant.employee.signature,
2182
+ },
2183
+ },
2184
+ departments,
2185
+ budget: {
2186
+ totalTokens: companyTotalTokens,
2187
+ totalCost: Math.round(companyTotalCost * 10000) / 10000,
2188
+ secretaryUsage: { ...secUsage },
2189
+ hrUsage: { ...hrUsage },
2190
+ },
2191
+ mailbox: this.mailbox.slice(-50).map(m => ({
2192
+ ...m,
2193
+ from: { ...m.from },
2194
+ replies: (m.replies || []).slice(-10),
2195
+ })),
2196
+ unreadMailCount: this.mailbox.filter(m => !m.read).length,
2197
+ pendingPlans: [...this.pendingPlans.entries()].map(([id, p]) => {
2198
+ if (p.type === 'adjustment') {
2199
+ return {
2200
+ planId: id,
2201
+ type: 'adjustment',
2202
+ departmentId: p.departmentId,
2203
+ departmentName: p.departmentName,
2204
+ adjustGoal: p.adjustGoal,
2205
+ reasoning: p.adjustPlan?.reasoning,
2206
+ fires: p.adjustPlan?.fires || [],
2207
+ hires: p.adjustPlan?.hires || [],
2208
+ };
2209
+ }
2210
+ return {
2211
+ planId: id,
2212
+ type: 'recruitment',
2213
+ name: p.name,
2214
+ mission: p.mission,
2215
+ members: (p.teamPlan?.members || []).map(m => ({
2216
+ templateId: m.templateId,
2217
+ title: m.templateTitle,
2218
+ name: m.name,
2219
+ isLeader: m.isLeader,
2220
+ reportsTo: m.reportsTo !== null ? p.teamPlan.members[m.reportsTo]?.name : null,
2221
+ })),
2222
+ };
2223
+ }),
2224
+ chatHistory: this.chatHistory.slice(-30),
2225
+ progressReports: this.progressReports.slice(-20),
2226
+ talentMarket: this.talentMarket.listAvailable().map(p => ({
2227
+ id: p.id,
2228
+ name: p.name,
2229
+ role: p.role,
2230
+ avatar: p.avatar,
2231
+ gender: p.gender,
2232
+ age: p.age,
2233
+ skills: [...p.skills, ...p.acquiredSkills],
2234
+ dismissalReason: p.dismissalReason,
2235
+ performanceScore: p.performanceData?.averageScore,
2236
+ registeredAt: p.registeredAt,
2237
+ memoryCount: p.memorySnapshot ? (p.memorySnapshot.shortTerm?.length || 0) + (p.memorySnapshot.longTerm?.length || 0) : 0,
2238
+ })),
2239
+ providerDashboard: this.providerRegistry.getStats(),
2240
+ messageBusStats: this.messageBus.getStats(),
2241
+ // Boss-Agent private chat session list
2242
+ agentChatSessions: this._getAgentChatSessions(),
2243
+ requirements: this.requirementManager.listAll().map(r => r.serialize()),
2244
+ teams: this.teamManager.listAll().map(t => t.serialize()),
2245
+ logs: this.logs.slice(-50),
2246
+ };
2247
+ }
2248
+
2249
+ /**
2250
+ * Get recent messages from message bus
2251
+ */
2252
+ getRecentMessages(limit = 20) {
2253
+ return this.messageBus.getRecent(limit);
2254
+ }
2255
+
2256
+ /**
2257
+ * Get conversation between Agents
2258
+ */
2259
+ getConversation(agentId1, agentId2, limit = 50) {
2260
+ return this.messageBus.getConversation(agentId1, agentId2, limit);
2261
+ }
2262
+
2263
+ /**
2264
+ * Get workspace file tree
2265
+ */
2266
+ async getWorkspaceFiles(departmentId) {
2267
+ const dept = this.findDepartment(departmentId);
2268
+ if (!dept || !dept.workspacePath) return [];
2269
+ return this.workspaceManager.getFileTree(dept.workspacePath);
2270
+ }
2271
+
2272
+ /**
2273
+ * Get shallow (one-level) workspace file listing
2274
+ */
2275
+ async getShallowWorkspaceFiles(departmentId, subPath = '') {
2276
+ const dept = this.findDepartment(departmentId);
2277
+ if (!dept || !dept.workspacePath) return [];
2278
+ return this.workspaceManager.getShallowFileTree(dept.workspacePath, subPath);
2279
+ }
2280
+
2281
+ /**
2282
+ * Read workspace file
2283
+ */
2284
+ async readWorkspaceFile(departmentId, filePath) {
2285
+ const dept = this.findDepartment(departmentId);
2286
+ if (!dept || !dept.workspacePath) throw new Error('Department workspace does not exist');
2287
+ return this.workspaceManager.readFile(dept.workspacePath, filePath);
2288
+ }
2289
+
2290
+ printCompanyOverview() {
2291
+ console.log(`\n${'='.repeat(60)}`);
2292
+ console.log(`🏢 "${this.name}" Company Overview`);
2293
+ console.log(`${'='.repeat(60)}`);
2294
+ console.log(`👤 Boss: ${this.bossName}`);
2295
+ console.log(`🏢 Departments: ${this.departments.size}`);
2296
+
2297
+ this.departments.forEach(dept => {
2298
+ console.log(`\n 📁 ${dept.name} (${dept.status})`);
2299
+ console.log(` Mission: ${dept.mission}`);
2300
+ console.log(` Members: ${dept.agents.size}`);
2301
+ dept.printOrgChart();
2302
+ });
2303
+
2304
+ this.talentMarket.print();
2305
+ console.log(`${'='.repeat(60)}\n`);
2306
+ }
2307
+
2308
+ // ========== Persistence Serialization ==========
2309
+
2310
+ /**
2311
+ * Serialize complete company state (for disk persistence)
2312
+ */
2313
+ serialize() {
2314
+ // Serialize departments and Agents
2315
+ const departments = [];
2316
+ this.departments.forEach(dept => {
2317
+ const members = dept.getMembers().map(a => a.serialize());
2318
+ departments.push({
2319
+ id: dept.id,
2320
+ name: dept.name,
2321
+ mission: dept.mission,
2322
+ status: dept.status,
2323
+ leader: dept.leader,
2324
+ workspacePath: dept.workspacePath,
2325
+ createdAt: dept.createdAt,
2326
+ // groupChat is persisted in chatStore files (data/chats/group-dept-{id}/)
2327
+ members,
2328
+ });
2329
+ });
2330
+
2331
+ // Serialize provider configs (only save API Key/session and enabled state)
2332
+ const providerConfigs = {};
2333
+ this.providerRegistry.listAll().forEach(p => {
2334
+ if (p.apiKey || p.cookie || p.enabled) {
2335
+ providerConfigs[p.id] = { apiKey: p.apiKey, enabled: p.enabled };
2336
+ if (p.isWeb && p.cookie) {
2337
+ providerConfigs[p.id].cookie = p.cookie;
2338
+ }
2339
+ }
2340
+ });
2341
+
2342
+ // Serialize talent market
2343
+ const talentPool = [];
2344
+ this.talentMarket.pool.forEach((profile, id) => {
2345
+ talentPool.push({
2346
+ ...profile,
2347
+ // Only save provider id
2348
+ provider: profile.provider ? { id: profile.provider.id } : null,
2349
+ });
2350
+ });
2351
+
2352
+ return {
2353
+ _version: 1,
2354
+ id: this.id,
2355
+ name: this.name,
2356
+ bossName: this.bossName,
2357
+ bossAvatar: this.bossAvatar,
2358
+ departments,
2359
+ providerConfigs,
2360
+ talentPool,
2361
+ mailbox: this.mailbox.slice(-200),
2362
+ chatSessionId: this.chatSessionId,
2363
+ chatHistory: this.chatHistory.slice(-50),
2364
+ progressReports: this.progressReports.slice(-30),
2365
+ logs: this.logs.slice(-100),
2366
+ secretary: {
2367
+ name: this.secretary.name,
2368
+ avatar: this.secretary.avatar,
2369
+ signature: this.secretary.signature,
2370
+ prompt: this.secretary.prompt,
2371
+ providerId: this.secretary.getProviderDisplayInfo().id,
2372
+ cliBackend: this.secretary.cliBackend || null,
2373
+ tokenUsage: { ...this.secretary.tokenUsage },
2374
+ hrTokenUsage: { ...this.secretary.hrAssistant.employee.tokenUsage },
2375
+ },
2376
+ messageBusMessages: this.messageBus.messages.slice(-500).map(m => m.toJSON()),
2377
+ requirements: this.requirementManager.serialize(),
2378
+ teams: this.teamManager.serialize(),
2379
+ cronJobs: cronScheduler.serialize(),
2380
+ cliBackends: cliBackendRegistry.serialize(),
2381
+ savedAt: new Date(),
2382
+ };
2383
+ }
2384
+
2385
+ /**
2386
+ * Restore company from serialized data (static factory method)
2387
+ */
2388
+ static deserialize(data) {
2389
+ if (!data || !data.name) throw new Error('Invalid company state data');
2390
+
2391
+ // Create shell company (don't trigger full initialization)
2392
+ // Get the real apiKey for the secretary's provider from providerConfigs
2393
+ const secretaryProviderId = data.secretary?.providerId || 'deepseek-v3';
2394
+ const secretaryApiKey = data.providerConfigs?.[secretaryProviderId]?.apiKey || 'sk-restored';
2395
+ const company = new Company(data.name, data.bossName, {
2396
+ providerId: secretaryProviderId,
2397
+ apiKey: secretaryApiKey,
2398
+ secretaryName: data.secretary?.name,
2399
+ secretaryAvatar: data.secretary?.avatar,
2400
+ });
2401
+
2402
+ // Restore ID
2403
+ company.id = data.id;
2404
+
2405
+ // Restore boss avatar
2406
+ if (data.bossAvatar) {
2407
+ company.bossAvatar = data.bossAvatar;
2408
+ }
2409
+
2410
+ // Restore provider configs
2411
+ if (data.providerConfigs) {
2412
+ for (const [pid, cfg] of Object.entries(data.providerConfigs)) {
2413
+ try {
2414
+ // For web providers, restore session and configure webClientRegistry
2415
+ if (cfg.cookie) {
2416
+ const provider = company.providerRegistry.getById(pid);
2417
+ if (provider && provider.isWeb) {
2418
+ provider.cookie = cfg.cookie;
2419
+ provider.enabled = cfg.enabled;
2420
+ webClientRegistry.configureCookie(pid, cfg.cookie);
2421
+ continue;
2422
+ }
2423
+ }
2424
+ company.providerRegistry.configure(pid, cfg.apiKey);
2425
+ // Clear old LLM client cache to ensure the restored real apiKey is used
2426
+ llmClient.clearClient(pid);
2427
+ } catch (e) { /* ignore non-existent providers */ }
2428
+ }
2429
+ }
2430
+
2431
+ // Restore secretary token usage
2432
+ if (data.secretary?.tokenUsage) {
2433
+ Object.assign(company.secretary.tokenUsage, data.secretary.tokenUsage);
2434
+ }
2435
+ if (data.secretary?.hrTokenUsage) {
2436
+ Object.assign(company.secretary.hrAssistant.employee.tokenUsage, data.secretary.hrTokenUsage);
2437
+ }
2438
+ // Restore secretary custom prompt
2439
+ if (data.secretary?.prompt) {
2440
+ company.secretary.prompt = data.secretary.prompt;
2441
+ }
2442
+ // Restore secretary signature
2443
+ if (data.secretary?.signature) {
2444
+ company.secretary.signature = data.secretary.signature;
2445
+ }
2446
+ // Restore secretary CLI backend configuration
2447
+ if (data.secretary?.cliBackend && company.secretary.agentType !== 'cli') {
2448
+ const cliProvider = company.providerRegistry.getById(data.secretary.providerId);
2449
+ if (cliProvider && cliProvider.isCLI && cliProvider.cliBackendId) {
2450
+ const fallback = company.providerRegistry.recommend('general');
2451
+ company.secretary.agent = new CLIAgent({
2452
+ cliBackend: cliProvider.cliBackendId,
2453
+ cliProvider: cliProvider,
2454
+ fallbackProvider: fallback, provider: fallback,
2455
+ });
2456
+ }
2457
+ }
2458
+ // Restore secretary Web agent configuration
2459
+ if (data.secretary?.providerId && company.secretary.agentType !== 'web') {
2460
+ const webProvider = company.providerRegistry.getById(data.secretary.providerId);
2461
+ if (webProvider && webProvider.isWeb) {
2462
+ company.secretary.agent = new WebAgent({ provider: webProvider });
2463
+ // Re-bind employeeId after agent replacement (for per-employee session isolation)
2464
+ company.secretary.agent.setEmployeeId(company.secretary.id);
2465
+ }
2466
+ }
2467
+
2468
+ // Restore secretary memory from separate memory file
2469
+ if (company.secretary?.id) {
2470
+ const secretaryMemory = loadAgentMemory(company.secretary.id);
2471
+ if (secretaryMemory) {
2472
+ company.secretary.memory = Memory.deserialize(secretaryMemory);
2473
+ console.log(` 🧠 Secretary memory restored: ${secretaryMemory.shortTerm?.length || 0} short-term, ${secretaryMemory.longTerm?.length || 0} long-term`);
2474
+ }
2475
+ }
2476
+
2477
+ // Restore departments and Agents
2478
+ company.departments.clear();
2479
+ for (const deptData of (data.departments || [])) {
2480
+ const dept = new Department({
2481
+ name: deptData.name,
2482
+ mission: deptData.mission,
2483
+ company: company.id,
2484
+ });
2485
+ dept.id = deptData.id;
2486
+ dept.status = deptData.status || 'active';
2487
+ dept.leader = deptData.leader;
2488
+ dept.workspacePath = deptData.workspacePath;
2489
+ // Ensure workspace directory exists (may be missing after migration or cleanup)
2490
+ if (dept.workspacePath && !existsSync(dept.workspacePath)) {
2491
+ try { mkdirSync(dept.workspacePath, { recursive: true }); } catch { /* ignore */ }
2492
+ }
2493
+ dept.createdAt = deptData.createdAt ? new Date(deptData.createdAt) : new Date();
2494
+ // Load groupChat from file storage (with legacy inline data migration)
2495
+ dept.loadGroupChatFromStore(deptData.groupChat);
2496
+
2497
+ // Restore Agents
2498
+ for (const agentData of (deptData.members || [])) {
2499
+ // Load memory from separate file (higher priority than serialized data)
2500
+ const externalMemory = loadAgentMemory(agentData.id);
2501
+ if (externalMemory) {
2502
+ agentData.memory = externalMemory;
2503
+ }
2504
+ const agent = deserializeEmployee(agentData, company.providerRegistry);
2505
+ dept.addAgent(agent);
2506
+ // Restore toolkit
2507
+ if (dept.workspacePath) {
2508
+ agent.initToolKit(dept.workspacePath, company.messageBus);
2509
+ }
2510
+ }
2511
+
2512
+ company.departments.set(dept.id, dept);
2513
+ }
2514
+
2515
+ // Restore talent market
2516
+ company.talentMarket.pool.clear();
2517
+ for (const profile of (data.talentPool || [])) {
2518
+ // Restore provider reference
2519
+ if (profile.provider?.id) {
2520
+ profile.provider = company.providerRegistry.getById(profile.provider.id) || profile.provider;
2521
+ }
2522
+ company.talentMarket.pool.set(profile.id, profile);
2523
+ }
2524
+
2525
+ // Restore mailbox, chat history, progress reports, logs
2526
+ company.mailbox = data.mailbox || [];
2527
+ company.chatHistory = data.chatHistory || [];
2528
+ // Restore chat session ID
2529
+ if (data.chatSessionId) {
2530
+ company.chatSessionId = data.chatSessionId;
2531
+ }
2532
+ // If there is old chatHistory data and file storage is empty, migrate it
2533
+ if (company.chatHistory.length > 0 && chatStore.getMessageCount(company.chatSessionId) === 0) {
2534
+ chatStore.migrateFromArray(company.chatSessionId, company.chatHistory);
2535
+ }
2536
+ company.progressReports = data.progressReports || [];
2537
+ company.logs = data.logs || [];
2538
+
2539
+ // Restore message bus
2540
+ if (data.messageBusMessages) {
2541
+ // Only restore history, don't rebuild inbox
2542
+ company.messageBus.messages = data.messageBusMessages.map(m => ({
2543
+ ...m,
2544
+ timestamp: new Date(m.timestamp),
2545
+ toJSON() { return m; },
2546
+ }));
2547
+ }
2548
+
2549
+ // Restore requirement manager
2550
+ if (data.requirements) {
2551
+ company.requirementManager = RequirementManager.deserialize(data.requirements);
2552
+ }
2553
+
2554
+ // Restore team manager
2555
+ if (data.teams) {
2556
+ company.teamManager = TeamManager.deserialize(data.teams);
2557
+ }
2558
+
2559
+ // Restore cron jobs
2560
+ if (data.cronJobs) {
2561
+ cronScheduler.restore(data.cronJobs);
2562
+ }
2563
+
2564
+ // Restore CLI backends (custom backends)
2565
+ if (data.cliBackends) {
2566
+ cliBackendRegistry.restore(data.cliBackends);
2567
+ }
2568
+ // Sync CLI backends into provider registry
2569
+ company.providerRegistry.syncCLIBackends(cliBackendRegistry);
2570
+ // Re-apply provider configs for CLI providers (to restore enabled state)
2571
+ const reapplyCLIConfigs = () => {
2572
+ if (data.providerConfigs) {
2573
+ for (const [pid, cfg] of Object.entries(data.providerConfigs)) {
2574
+ if (pid.startsWith('cli-')) {
2575
+ try { company.providerRegistry.configure(pid, cfg.apiKey); } catch {}
2576
+ }
2577
+ }
2578
+ }
2579
+ };
2580
+ reapplyCLIConfigs();
2581
+ // Auto-detect CLI backends in background, then re-sync and re-apply configs
2582
+ cliBackendRegistry.detectAll().then(() => {
2583
+ company.providerRegistry.syncCLIBackends(cliBackendRegistry);
2584
+ reapplyCLIConfigs();
2585
+ }).catch(() => {});
2586
+
2587
+ // Re-start group chat loop for all restored agents
2588
+ // (_initSubsystems ran during constructor when departments were still empty,
2589
+ // so we need to register all agents that were restored afterwards)
2590
+ groupChatLoop.start(company);
2591
+ for (const dept of company.departments.values()) {
2592
+ for (const agent of dept.getMembers()) {
2593
+ groupChatLoop.startAgentLoop(agent);
2594
+ }
2595
+ }
2596
+
2597
+ console.log(`✅ Company "${company.name}" state restored: ${company.departments.size} departments`);
2598
+ return company;
2599
+ }
2600
+ }