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,1789 @@
1
+ import { v4 as uuidv4 } from 'uuid';
2
+ import path from 'path';
3
+ import { existsSync, readFileSync } from 'fs';
4
+ import { chatStore } from './agent/chat-store.js';
5
+ import { WorkspaceManager } from './workspace.js';
6
+ import { robustJSONParse } from './utils/json-parse.js';
7
+
8
+ /** Group chat prefix for requirement group chats in chatStore */
9
+ const REQ_GROUP_PREFIX = 'req-';
10
+
11
+ /**
12
+ * Requirement status enum
13
+ */
14
+ export const RequirementStatus = {
15
+ PENDING: 'pending', // Just created, awaiting assignment
16
+ PLANNING: 'planning', // Leader is decomposing workflow
17
+ IN_PROGRESS: 'in_progress', // In progress
18
+ PENDING_APPROVAL: 'pending_approval', // All tasks done, awaiting Boss approval
19
+ COMPLETED: 'completed', // Completed (Boss approved)
20
+ FAILED: 'failed', // Failed
21
+ };
22
+
23
+ /**
24
+ * Workflow node status
25
+ */
26
+ export const TaskNodeStatus = {
27
+ WAITING: 'waiting', // Waiting for dependencies
28
+ READY: 'ready', // Ready to start
29
+ RUNNING: 'running', // Running
30
+ REVIEWING: 'reviewing', // Completed execution, under review
31
+ REVISION: 'revision', // Review rejected, needs revision
32
+ COMPLETED: 'completed', // Completed (and passed review if reviewer assigned)
33
+ FAILED: 'failed', // Failed
34
+ };
35
+
36
+ /**
37
+ * Requirement data model
38
+ */
39
+ export class Requirement {
40
+ constructor({ title, description, departmentId, departmentName, bossMessage }) {
41
+ this.id = uuidv4();
42
+ this.title = title;
43
+ this.description = description;
44
+ this.departmentId = departmentId;
45
+ this.departmentName = departmentName;
46
+ this.bossMessage = bossMessage; // Boss's original message
47
+ this.status = RequirementStatus.PENDING;
48
+ this.workflow = null; // Workflow (decomposed by leader)
49
+ this.groupChat = []; // Group chat messages
50
+ this.outputs = []; // Output results
51
+ this.createdAt = new Date();
52
+ this.startedAt = null;
53
+ this.completedAt = null;
54
+ this.summary = null; // Post-completion execution summary
55
+
56
+ // Live progress status (dynamically updated during execution)
57
+ this.liveStatus = {
58
+ currentNodeId: null, // Currently executing node ID
59
+ currentNodeTitle: null, // Currently executing node title
60
+ currentAgent: null, // Currently executing Agent name
61
+ currentAction: null, // Current action description (e.g. "calling LLM", "executing tool file_write")
62
+ lastActiveAt: null, // Last active time
63
+ heartbeat: null, // Heartbeat time (updated on each LLM/tool call)
64
+ toolCallsInProgress: [], // Tool calls in progress
65
+ recentFileChanges: [], // Recent file change records
66
+ };
67
+ }
68
+
69
+ /** Update live status */
70
+ updateLiveStatus(updates) {
71
+ Object.assign(this.liveStatus, updates, { lastActiveAt: new Date(), heartbeat: new Date() });
72
+ }
73
+
74
+ /** Record file change */
75
+ addFileChange(agentName, filePath, action = 'write') {
76
+ this.liveStatus.recentFileChanges.push({
77
+ agentName,
78
+ filePath,
79
+ action,
80
+ time: new Date(),
81
+ });
82
+ // Keep only last 100 entries (more generous to ensure files tab shows all files)
83
+ if (this.liveStatus.recentFileChanges.length > 100) {
84
+ this.liveStatus.recentFileChanges = this.liveStatus.recentFileChanges.slice(-100);
85
+ }
86
+ this.liveStatus.lastActiveAt = new Date();
87
+ this.liveStatus.heartbeat = new Date();
88
+ }
89
+
90
+ /**
91
+ * Add group chat message
92
+ * @param {object} from - Sender info
93
+ * @param {string} content - Message content
94
+ * @param {string} type - Message type: message | system | tool_call | output
95
+ * @param {string|null} visibility - Visibility: 'group' (broadcast to group chat) | 'flow' (flow log, only visible to self and boss)
96
+ * - tool_call type defaults to 'flow' (flow log, work process doesn't flood the group chat)
97
+ * - Other types default to 'group' (broadcast to group chat)
98
+ * @param {object} options - Extra options { auto: boolean } - mark as auto-generated message
99
+ */
100
+ addGroupMessage(from, content, type = 'message', visibility = null, options = {}) {
101
+ // Auto-infer visibility: tool_call and output types default to 'flow', others default to 'group'
102
+ const resolvedVisibility = visibility || (type === 'tool_call' || type === 'output' ? 'flow' : 'group');
103
+ const msg = {
104
+ id: uuidv4(),
105
+ from: {
106
+ id: from.id || 'system',
107
+ name: from.name || 'System',
108
+ avatar: from.avatar || null,
109
+ role: from.role || null,
110
+ },
111
+ content,
112
+ type, // message | system | tool_call | output
113
+ visibility: resolvedVisibility, // group | flow
114
+ time: new Date(),
115
+ ...(options.auto ? { auto: true } : {}),
116
+ };
117
+ this.groupChat.push(msg);
118
+ // Persist to file storage (non-blocking, fire-and-forget)
119
+ try { chatStore.appendGroupMessage(`${REQ_GROUP_PREFIX}${this.id}`, msg); } catch {}
120
+ // Sync update heartbeat
121
+ this.liveStatus.heartbeat = new Date();
122
+ this.liveStatus.lastActiveAt = new Date();
123
+ }
124
+
125
+ /** Add output */
126
+ addOutput(agentId, agentName, role, outputType, content, metadata = {}) {
127
+ this.outputs.push({
128
+ id: uuidv4(),
129
+ agentId,
130
+ agentName,
131
+ role,
132
+ outputType, // text | code | image | file
133
+ content,
134
+ metadata,
135
+ createdAt: new Date(),
136
+ });
137
+ }
138
+
139
+ /** Serialize (groupChat is stored in separate files, not included here) */
140
+ serialize() {
141
+ return {
142
+ id: this.id,
143
+ title: this.title,
144
+ description: this.description,
145
+ departmentId: this.departmentId,
146
+ departmentName: this.departmentName,
147
+ bossMessage: this.bossMessage,
148
+ status: this.status,
149
+ workflow: this.workflow,
150
+ // groupChat is persisted in chatStore files (data/chats/group-req-{id}/)
151
+ outputs: this.outputs,
152
+ createdAt: this.createdAt,
153
+ startedAt: this.startedAt,
154
+ completedAt: this.completedAt,
155
+ summary: this.summary,
156
+ liveStatus: this.liveStatus,
157
+ };
158
+ }
159
+
160
+ /** Deserialize */
161
+ static deserialize(data) {
162
+ const req = new Requirement({
163
+ title: data.title,
164
+ description: data.description,
165
+ departmentId: data.departmentId,
166
+ departmentName: data.departmentName,
167
+ bossMessage: data.bossMessage,
168
+ });
169
+ req.id = data.id;
170
+ req.status = data.status;
171
+ req.workflow = data.workflow;
172
+
173
+ // Load groupChat from file storage; migrate legacy inline data if present
174
+ const groupId = `${REQ_GROUP_PREFIX}${req.id}`;
175
+ if (data.groupChat && data.groupChat.length > 0) {
176
+ // Legacy data found inline — migrate to file storage, then discard from state
177
+ chatStore.migrateGroupChat(groupId, data.groupChat);
178
+ }
179
+ req.groupChat = chatStore.getGroupMessages(groupId, 500);
180
+
181
+ req.outputs = data.outputs || [];
182
+ req.createdAt = data.createdAt ? new Date(data.createdAt) : new Date();
183
+ req.startedAt = data.startedAt ? new Date(data.startedAt) : null;
184
+ req.completedAt = data.completedAt ? new Date(data.completedAt) : null;
185
+ req.summary = data.summary;
186
+ req.liveStatus = data.liveStatus || {
187
+ currentNodeId: null, currentNodeTitle: null, currentAgent: null,
188
+ currentAction: null, lastActiveAt: null, heartbeat: null,
189
+ toolCallsInProgress: [], recentFileChanges: [],
190
+ };
191
+ return req;
192
+ }
193
+ }
194
+
195
+ /**
196
+ * Requirement Manager
197
+ * Manages the lifecycle of all company requirements
198
+ */
199
+ export class RequirementManager {
200
+ constructor() {
201
+ this.requirements = new Map();
202
+ }
203
+
204
+ /** Create requirement */
205
+ create(data) {
206
+ const req = new Requirement(data);
207
+ this.requirements.set(req.id, req);
208
+ return req;
209
+ }
210
+
211
+ /** Get requirement */
212
+ get(id) {
213
+ return this.requirements.get(id);
214
+ }
215
+
216
+ /** List all requirements */
217
+ listAll() {
218
+ return [...this.requirements.values()].sort(
219
+ (a, b) => new Date(b.createdAt) - new Date(a.createdAt)
220
+ );
221
+ }
222
+
223
+ /** List requirements by department */
224
+ listByDepartment(departmentId) {
225
+ return this.listAll().filter(r => r.departmentId === departmentId);
226
+ }
227
+
228
+ /** List requirements by status */
229
+ listByStatus(status) {
230
+ return this.listAll().filter(r => r.status === status);
231
+ }
232
+
233
+ /**
234
+ * Leader decomposes requirement into workflow.
235
+ * Automatically finds a capable agent from members to perform the decomposition.
236
+ * @param {Requirement} requirement - Requirement
237
+ * @param {Array} members - Department members list (Employee instances)
238
+ * @param {object} [adjustmentContext] - If present, this is a workflow adjustment
239
+ * @returns {object} Workflow
240
+ */
241
+ async planWorkflow(requirement, members, adjustmentContext = null) {
242
+ requirement.status = RequirementStatus.PLANNING;
243
+ if (adjustmentContext) {
244
+ requirement.addGroupMessage(
245
+ { name: 'System', role: 'system' },
246
+ `🔄 Adjusting workflow based on Boss's instructions...`,
247
+ 'system', null, { auto: true }
248
+ );
249
+ } else {
250
+ requirement.addGroupMessage(
251
+ { name: 'System', role: 'system' },
252
+ `📋 Requirement "${requirement.title}" created, leader is decomposing the workflow...`,
253
+ 'system', null, { auto: true }
254
+ );
255
+ }
256
+
257
+ // Find a member who can chat (LLM or CLI with chat ability) to do the decomposition.
258
+ // Prefer leader, then fall back to any member who canChat().
259
+ const leader = members.find(m => m.role === 'Project Leader') || members[0];
260
+ let planner = leader?.canChat() ? leader : null;
261
+ if (!planner) {
262
+ planner = members.find(m => m.canChat()) || null;
263
+ }
264
+ if (planner && planner !== leader) {
265
+ console.log(` 🔄 Leader cannot chat, borrowing [${planner.name}] for workflow decomposition`);
266
+ }
267
+
268
+ // Build member info
269
+ const memberInfo = members.map(m => ({
270
+ id: m.id,
271
+ name: m.name,
272
+ role: m.role,
273
+ skills: m.skills,
274
+ }));
275
+
276
+ const systemPrompt = `You are a project leader who needs to decompose a requirement into an executable workflow.
277
+
278
+ Team members:
279
+ ${JSON.stringify(memberInfo, null, 2)}
280
+
281
+ Please decompose the requirement into a workflow (DAG - Directed Acyclic Graph) with multiple task nodes. Tasks can have dependency relationships.
282
+ Output in JSON format:
283
+ {
284
+ "nodes": [
285
+ {
286
+ "id": "node_1",
287
+ "title": "Task title",
288
+ "description": "Detailed description",
289
+ "assigneeId": "Assignee ID",
290
+ "assigneeName": "Assignee name",
291
+ "dependencies": [],
292
+ "estimatedMinutes": 5,
293
+ "outputType": "text|code|file",
294
+ "reviewerId": "Reviewer agent ID (optional, null if no review needed)",
295
+ "reviewerName": "Reviewer name",
296
+ "reviewCriteria": "Specific review criteria the reviewer should check (optional)"
297
+ }
298
+ ],
299
+ "summary": "Workflow overview"
300
+ }
301
+
302
+ Requirements:
303
+ 1. Task granularity should be moderate, each task assigned to one person
304
+ 2. **MAXIMIZE PARALLELISM**: Tasks that can run in parallel MUST not be serialized. Prefer wide parallel DAGs over deep serial chains
305
+ 3. dependencies should contain the dependent node id array, empty array if no dependencies
306
+ 4. The leader can handle "integration and review" type tasks
307
+ 5. assigneeId must be selected from team members
308
+ 6. Return JSON only, no other content
309
+ 7. **Extremely important: Not every member needs to participate!** Only assign people who are truly needed. Members unrelated to the requirement should not be given tasks. Better to leave people idle than to create busywork
310
+ 8. Task nodes should be lean and efficient. Simple requirements only need 1-3 nodes, avoid over-decomposition
311
+ 9. Each task description should be clear and specific, allowing the assignee to complete it in one go, avoiding multiple iterations
312
+ 10. **Encourage collaboration**: When multiple agents work in parallel, their tasks should be designed to have natural interaction points. Include in descriptions that they should coordinate with parallel teammates via send_message
313
+ 11. **REVIEW MECHANISM**: For complex or important tasks, you MAY assign a reviewer (reviewerId). The reviewer will audit the work for significant issues. Review rules:
314
+ - The reviewer MUST be a different person from the assignee (never review your own work)
315
+ - For tasks where two agents work in parallel, they can review EACH OTHER's work (Agent A reviews Agent B, Agent B reviews Agent A)
316
+ - The leader can serve as reviewer for critical integration tasks
317
+ - reviewCriteria should be specific and measurable (e.g. "Check that all API endpoints have error handling and input validation" rather than vague "review the code")
318
+ - MOST tasks do NOT need a reviewer. Only assign reviewers for genuinely complex, high-risk tasks. Simple or medium tasks should skip review (set reviewerId to null)
319
+ - When in doubt, do NOT assign a reviewer — over-reviewing slows down the workflow significantly`;
320
+
321
+ const userPrompt = adjustmentContext
322
+ ? `Requirement title: ${requirement.title}\nRequirement description: ${requirement.description}\n\n**ADJUSTMENT REQUEST FROM BOSS:**\n${adjustmentContext.bossMessage}\n\n**YOUR PLANNED ADJUSTMENTS:**\n${adjustmentContext.adjustments}\n\n**PREVIOUS WORKFLOW (for reference):**\n${adjustmentContext.previousWorkflow}\n\n**EXISTING OUTPUT FILES (must be preserved and built upon):**\n${adjustmentContext.existingOutputs || 'None'}\n\n**IMPORTANT:** This is an ADJUSTMENT, NOT a restart. You must:\n1. PRESERVE all existing output files - do NOT recreate them from scratch\n2. Only create tasks that MODIFY existing files or ADD new content\n3. When a task needs to change an existing file, the agent should READ the current file first, then modify it\n4. Only add NEW tasks for genuinely new work that wasn't done before\n5. Reuse the previous workflow structure where possible, adjusting only what the Boss requested\n\nPlease create an ADJUSTED workflow based on the Boss's instructions.`
323
+ : `Requirement title: ${requirement.title}\nRequirement description: ${requirement.description}\n\nPlease decompose the workflow.`;
324
+
325
+ if (!planner) {
326
+ console.error('No member can chat, falling back to rule-based workflow');
327
+ const fallbackWorkflow = this._fallbackWorkflow(requirement, members);
328
+ requirement.addGroupMessage(
329
+ { name: 'System', role: 'system' },
330
+ `⚠️ No available agent for decomposition, generated a simple workflow using rules (${fallbackWorkflow.nodes.length} tasks)`,
331
+ 'system', null, { auto: true }
332
+ );
333
+ return fallbackWorkflow;
334
+ }
335
+
336
+ try {
337
+ const response = await planner.chat([
338
+ { role: 'system', content: systemPrompt },
339
+ { role: 'user', content: userPrompt },
340
+ ], { temperature: 0.7, maxTokens: 2048 });
341
+
342
+ // Parse JSON (robust extraction for both LLM and CLI output)
343
+ const workflow = robustJSONParse(response.content);
344
+
345
+ // Validate and complete
346
+ const memberIds = new Set(members.map(m => m.id));
347
+ workflow.nodes = (workflow.nodes || []).map(node => ({
348
+ ...node,
349
+ id: node.id || `node_${uuidv4().slice(0, 8)}`,
350
+ status: TaskNodeStatus.WAITING,
351
+ dependencies: (node.dependencies || []).filter(d =>
352
+ workflow.nodes.some(n => n.id === d)
353
+ ),
354
+ assigneeId: memberIds.has(node.assigneeId) ? node.assigneeId : members[0]?.id,
355
+ reviewerId: node.reviewerId && memberIds.has(node.reviewerId) ? node.reviewerId : null,
356
+ reviewerName: node.reviewerName || null,
357
+ reviewCriteria: node.reviewCriteria || null,
358
+ reviewRounds: 0, // Review iteration round counter
359
+ maxReviewRounds: 10, // Max review iterations (prevents infinite loops)
360
+ result: null,
361
+ startedAt: null,
362
+ completedAt: null,
363
+ }));
364
+
365
+ // Nodes with no dependencies are marked as ready
366
+ workflow.nodes.forEach(node => {
367
+ if (node.dependencies.length === 0) {
368
+ node.status = TaskNodeStatus.READY;
369
+ }
370
+ });
371
+
372
+ requirement.workflow = workflow;
373
+
374
+ // Group chat notification
375
+ const leader = members.find(m => m.role === 'Project Leader') || members[0];
376
+ requirement.addGroupMessage(
377
+ leader,
378
+ `📊 Workflow decomposition complete! ${workflow.nodes.length} task nodes in total.\n\n${workflow.summary || ''}\n\n${workflow.nodes.map((n, i) =>
379
+ `${i + 1}. [${n.assigneeName || 'TBD'}] ${n.title}${n.dependencies.length > 0 ? ` (depends on: ${n.dependencies.join(', ')})` : ' (can start immediately)'}${n.reviewerId ? ` 🔍 Reviewer: ${n.reviewerName || n.reviewerId}` : ''}`
380
+ ).join('\n')}`,
381
+ 'message', null, { auto: true }
382
+ );
383
+
384
+ return workflow;
385
+ } catch (e) {
386
+ // LLM failed, generate simple workflow using rules
387
+ console.error('Workflow decomposition failed:', e.message);
388
+ const fallbackWorkflow = this._fallbackWorkflow(requirement, members);
389
+ requirement.addGroupMessage(
390
+ { name: 'System', role: 'system' },
391
+ `⚠️ AI decomposition failed, generated a simple workflow using rules (${fallbackWorkflow.nodes.length} tasks)`,
392
+ 'system', null, { auto: true }
393
+ );
394
+ return fallbackWorkflow;
395
+ }
396
+ }
397
+
398
+ /**
399
+ * Fallback: generate simple serial workflow
400
+ */
401
+ _fallbackWorkflow(requirement, members) {
402
+ const leader = members.find(m => m.role === 'Project Leader') || members[0];
403
+ const workers = members.filter(m => m.id !== leader?.id);
404
+
405
+ const nodes = [];
406
+
407
+ // Simple requirements only need one core worker, not everyone
408
+ // Pick the most suitable worker (first non-leader member)
409
+ const primaryWorker = workers[0];
410
+
411
+ if (primaryWorker) {
412
+ // Simple mode: one person directly executes the core task
413
+ nodes.push({
414
+ id: 'node_work_0',
415
+ title: `${primaryWorker.role}: Execute requirement`,
416
+ description: `Directly execute requirement "${requirement.title}": ${requirement.description}. Please complete all work in one go and output the final result.`,
417
+ assigneeId: primaryWorker.id,
418
+ assigneeName: primaryWorker.name,
419
+ dependencies: [],
420
+ status: TaskNodeStatus.READY,
421
+ outputType: 'text',
422
+ result: null,
423
+ startedAt: null,
424
+ completedAt: null,
425
+ });
426
+
427
+ // Only add a leader integration step when there are many members (>2)
428
+ if (leader && workers.length > 2) {
429
+ // Assign a second worker to assist
430
+ const secondWorker = workers[1];
431
+ if (secondWorker) {
432
+ nodes.push({
433
+ id: 'node_work_1',
434
+ title: `${secondWorker.role}: Assist with work`,
435
+ description: `Assist in completing the parts of requirement "${requirement.title}" related to your expertise`,
436
+ assigneeId: secondWorker.id,
437
+ assigneeName: secondWorker.name,
438
+ dependencies: [],
439
+ status: TaskNodeStatus.READY,
440
+ outputType: 'text',
441
+ result: null,
442
+ startedAt: null,
443
+ completedAt: null,
444
+ });
445
+ }
446
+
447
+ nodes.push({
448
+ id: 'node_integrate',
449
+ title: 'Results integration and delivery',
450
+ description: 'Consolidate all member outputs and produce the final deliverable',
451
+ assigneeId: leader.id,
452
+ assigneeName: leader.name,
453
+ dependencies: nodes.map(n => n.id),
454
+ status: TaskNodeStatus.WAITING,
455
+ outputType: 'text',
456
+ result: null,
457
+ startedAt: null,
458
+ completedAt: null,
459
+ });
460
+ }
461
+ } else if (leader) {
462
+ // Only leader available, leader executes directly
463
+ nodes.push({
464
+ id: 'node_work_0',
465
+ title: 'Execute requirement',
466
+ description: `Directly execute requirement "${requirement.title}": ${requirement.description}`,
467
+ assigneeId: leader.id,
468
+ assigneeName: leader.name,
469
+ dependencies: [],
470
+ status: TaskNodeStatus.READY,
471
+ outputType: 'text',
472
+ result: null,
473
+ startedAt: null,
474
+ completedAt: null,
475
+ });
476
+ }
477
+
478
+ const workflow = {
479
+ nodes,
480
+ summary: `Rule-based workflow: ${nodes.length} tasks`,
481
+ };
482
+
483
+ requirement.workflow = workflow;
484
+ return workflow;
485
+ }
486
+
487
+ /**
488
+ * Execute workflow by DAG dependency order
489
+ * Supports parallel + dependency serialization
490
+ */
491
+ async executeWorkflow(requirement, department, performanceSystem) {
492
+ if (!requirement.workflow?.nodes?.length) {
493
+ // If workflow is empty, auto-create a simple fallback workflow
494
+ console.log('Workflow is empty, auto-creating fallback workflow...');
495
+ const members = department.getMembers();
496
+ if (members.length === 0) {
497
+ throw new Error('Department has no employees, cannot execute');
498
+ } this._fallbackWorkflow(requirement, members);
499
+ }
500
+
501
+ requirement.status = RequirementStatus.IN_PROGRESS;
502
+ requirement.startedAt = new Date();
503
+ requirement.updateLiveStatus({
504
+ currentAction: 'Workflow execution started',
505
+ toolCallsInProgress: [],
506
+ recentFileChanges: [],
507
+ });
508
+
509
+ requirement.addGroupMessage(
510
+ { name: 'System', role: 'system' },
511
+ `🚀 Requirement "${requirement.title}" execution started!`,
512
+ 'system', null, { auto: true }
513
+ );
514
+
515
+ // === Set up message bus listener for real-time agent-to-agent communication ===
516
+ const messageBus = department.company?.messageBus || null;
517
+ const messageHandler = async (message) => {
518
+ // When an agent sends a message via send_message tool,
519
+ // post it to the group chat with @mention, then let GroupChatLoop
520
+ // handle the response through the normal flow/monologue mechanism.
521
+ // This ensures all replies go through the heartflow thinking process.
522
+ const receiverAgent = department.agents.get(message.to);
523
+ const senderAgent = department.agents.get(message.from);
524
+ if (!receiverAgent || !senderAgent) return;
525
+
526
+ // Show the sent message in group chat with @mention
527
+ const groupMsg = `@[${receiverAgent.id}] ${message.content}`;
528
+ requirement.addGroupMessage(senderAgent, groupMsg, 'message');
529
+ this._recordAgentChat(senderAgent, receiverAgent, message.content);
530
+
531
+ // Trigger the receiver's GroupChatLoop to process via heartflow
532
+ // instead of auto-replying directly (bypassing flow thinking)
533
+ try {
534
+ const { groupChatLoop } = await import('./organization/group-chat-loop.js');
535
+ groupChatLoop.triggerImmediate(receiverAgent.id, requirement.id, {
536
+ content: groupMsg,
537
+ from: senderAgent,
538
+ }).catch(() => {});
539
+ } catch (e) {
540
+ // Non-blocking
541
+ }
542
+ };
543
+
544
+ if (messageBus) {
545
+ messageBus.on('message', messageHandler);
546
+ }
547
+
548
+ const nodes = requirement.workflow.nodes;
549
+ const completed = new Set();
550
+ const failed = new Set();
551
+ const allResults = [];
552
+
553
+ // Loop until all nodes are completed or no further progress possible
554
+ while (completed.size + failed.size < nodes.length) {
555
+ // Find executable nodes (all dependencies completed + status is READY/WAITING)
556
+ const readyNodes = nodes.filter(n =>
557
+ n.status !== TaskNodeStatus.COMPLETED &&
558
+ n.status !== TaskNodeStatus.FAILED &&
559
+ n.status !== TaskNodeStatus.RUNNING &&
560
+ n.status !== TaskNodeStatus.REVIEWING &&
561
+ n.status !== TaskNodeStatus.REVISION &&
562
+ n.dependencies.every(d => completed.has(d))
563
+ );
564
+
565
+ if (readyNodes.length === 0) {
566
+ // No executable nodes left (could be circular dependency or all failed)
567
+ break;
568
+ }
569
+
570
+ // Execute all ready nodes in parallel
571
+ // Build parallel context: let agents know who else is working in parallel
572
+ const parallelInfo = readyNodes.length > 1
573
+ ? readyNodes.map(n => {
574
+ const a = department.agents.get(n.assigneeId);
575
+ return a ? `- ${a.name} (${a.role}): "${n.title}"` : null;
576
+ }).filter(Boolean).join('\n')
577
+ : null;
578
+
579
+ const promises = readyNodes.map(async (node) => {
580
+ node.status = TaskNodeStatus.RUNNING;
581
+ node.startedAt = new Date();
582
+
583
+ const agent = department.agents.get(node.assigneeId);
584
+ if (!agent) {
585
+ node.status = TaskNodeStatus.FAILED;
586
+ node.result = { error: 'Assignee not found' };
587
+ failed.add(node.id);
588
+ requirement.addGroupMessage(
589
+ { name: 'System' },
590
+ `❌ Task "${node.title}" failed: Assignee not found`,
591
+ 'system', null, { auto: true }
592
+ );
593
+ return null;
594
+ }
595
+
596
+ // Update live status
597
+ requirement.updateLiveStatus({
598
+ currentNodeId: node.id,
599
+ currentNodeTitle: node.title,
600
+ currentAgent: agent.name,
601
+ currentAction: `${agent.name} is preparing to execute "${node.title}"`,
602
+ toolCallsInProgress: [],
603
+ });
604
+
605
+ // Group chat notification: starting work
606
+ if (parallelInfo && readyNodes.length > 1) {
607
+ // Parallel mode: announce teamwork
608
+ const myParallel = readyNodes
609
+ .filter(n => n.id !== node.id)
610
+ .map(n => {
611
+ const a = department.agents.get(n.assigneeId);
612
+ return a ? `@[${a.id}]` : null;
613
+ }).filter(Boolean);
614
+ if (myParallel.length > 0) {
615
+ requirement.addGroupMessage(
616
+ agent,
617
+ `🔨 Starting "${node.title}"! Working in parallel with ${myParallel.join(', ')} — let's sync up if needed! 💪`,
618
+ 'message', null, { auto: true }
619
+ );
620
+ } else {
621
+ requirement.addGroupMessage(
622
+ agent,
623
+ `🔨 Starting to work on "${node.title}"!`,
624
+ 'message', null, { auto: true }
625
+ );
626
+ }
627
+ } else {
628
+ requirement.addGroupMessage(
629
+ agent,
630
+ `🔨 Starting to work on "${node.title}"!`,
631
+ 'message', null, { auto: true }
632
+ );
633
+ }
634
+
635
+ try {
636
+ // Collect dependency node outputs as context (including file deliverables)
637
+ const depContext = node.dependencies
638
+ .map(d => nodes.find(n => n.id === d))
639
+ .filter(Boolean)
640
+ .map(d => {
641
+ const output = d.result?.output || '(no output)';
642
+ // Extract files written by upstream task (only include files that still exist)
643
+ const depWsPath = department.workspacePath;
644
+ const fileWriteTools = new Set(['file_write', 'file_append', 'file_patch']);
645
+ const upstreamFiles = (d.result?.toolResults || [])
646
+ .filter(t => fileWriteTools.has(t.tool))
647
+ .map(t => t.args?.path || t.args?.filePath || t.args?.file_path || '')
648
+ .filter(f => f && (!depWsPath || existsSync(path.join(depWsPath, f))));
649
+ const fileList = upstreamFiles.length > 0
650
+ ? `\n📁 Files delivered:\n${upstreamFiles.map(f => ` - ${f}`).join('\n')}\nYou can use file_read to review these files.`
651
+ : '';
652
+ return `[${d.assigneeName}'s output - ${d.title}]\n${output}${fileList}`;
653
+ })
654
+ .join('\n\n');
655
+
656
+ // Build colleague info for collaboration context
657
+ const colleagues = Array.from(department.agents.values())
658
+ .filter(a => a.id !== agent.id)
659
+ .map(a => `- ${a.name} (${a.role}), ID: ${a.id}`)
660
+ .join('\n');
661
+
662
+ // Build parallel context: who is working at the same time?
663
+ let parallelContext = '';
664
+ if (parallelInfo && readyNodes.length > 1) {
665
+ const otherParallel = readyNodes
666
+ .filter(n => n.id !== node.id)
667
+ .map(n => {
668
+ const a = department.agents.get(n.assigneeId);
669
+ return a ? `- ${a.name} (${a.role}) is working on "${n.title}" right now` : null;
670
+ }).filter(Boolean).join('\n');
671
+ if (otherParallel) {
672
+ parallelContext = `\n\n**Working in parallel with you right now:**\n${otherParallel}\nFeel free to use send_message to coordinate with them, share progress, or ask questions!`;
673
+ }
674
+ }
675
+
676
+ const task = {
677
+ title: node.title,
678
+ description: node.description,
679
+ context: (depContext ? `Here are the outputs from preceding tasks for your reference:\n\n${depContext}\n\n` : '')
680
+ + (colleagues ? `Your colleagues in this department (you can send_message to them):\n${colleagues}` : '')
681
+ + parallelContext,
682
+ requirements: `This is part of requirement "${requirement.title}". Requirement description: ${requirement.description}`,
683
+ };
684
+
685
+ // Update live status: starting LLM call
686
+ requirement.updateLiveStatus({
687
+ currentAction: `${agent.name} is typing..."${node.title}"`,
688
+ });
689
+ requirement.addGroupMessage(
690
+ agent,
691
+ `⌨️ Typing... planning how to complete this task`,
692
+ 'tool_call'
693
+ );
694
+
695
+ // Take workspace snapshot before CLI execution for file change detection
696
+ const wsPath = department.workspacePath;
697
+ const isCLIAgent = !!agent.cliBackend;
698
+ let snapshotBefore = null;
699
+ if (isCLIAgent && wsPath) {
700
+ try {
701
+ const wsm = new WorkspaceManager();
702
+ snapshotBefore = await wsm.takeSnapshot(wsPath);
703
+ } catch { /* ignore snapshot errors */ }
704
+ }
705
+
706
+ const result = await agent.executeTask(task, {
707
+ onToolCall: ({ tool, args, status, success, error: toolErr }) => {
708
+ // Update requirement's liveStatus in real-time
709
+ if (status === 'start') {
710
+ // CLI progress heartbeat — special handling
711
+ if (tool === 'cli_progress') {
712
+ const elapsed = args?.elapsed || 0;
713
+ const backend = args?.backend || 'CLI';
714
+ requirement.updateLiveStatus({
715
+ currentAction: `${agent.name} is working via ${backend}... (${elapsed}s elapsed)`,
716
+ });
717
+ requirement.addGroupMessage(agent, `🖥️ Still working via ${backend}... (${elapsed}s elapsed)`, 'tool_call');
718
+ return;
719
+ }
720
+
721
+ requirement.updateLiveStatus({
722
+ currentAction: `${agent.name} is calling tool ${tool}`,
723
+ toolCallsInProgress: [...(requirement.liveStatus.toolCallsInProgress || []), tool],
724
+ });
725
+ // Real-time group chat: calling tool
726
+ if (tool === 'file_write') {
727
+ const filePath = args?.path || args?.filePath || args?.file_path || '';
728
+ requirement.addGroupMessage(agent, `📝 Writing file: ${filePath}`, 'tool_call');
729
+ requirement.addFileChange(agent.name, filePath, 'write');
730
+ } else if (tool === 'file_append') {
731
+ const filePath = args?.path || args?.filePath || args?.file_path || '';
732
+ requirement.addGroupMessage(agent, `📝 Appending to file: ${filePath}`, 'tool_call');
733
+ requirement.addFileChange(agent.name, filePath, 'write');
734
+ } else if (tool === 'file_patch') {
735
+ const filePath = args?.path || args?.filePath || args?.file_path || '';
736
+ requirement.addGroupMessage(agent, `📝 Patching file: ${filePath}`, 'tool_call');
737
+ requirement.addFileChange(agent.name, filePath, 'write');
738
+ } else if (tool === 'file_read') {
739
+ requirement.addGroupMessage(agent, `📄 Reading file: ${args?.path || args?.filePath || args?.file_path || ''}`, 'tool_call');
740
+ } else if (tool === 'shell_exec') {
741
+ requirement.addGroupMessage(agent, `⌨️ Executing command: ${(args?.command || '').slice(0, 80)}`, 'tool_call');
742
+ } else if (tool === 'send_message') {
743
+ requirement.addGroupMessage(agent, `💬 Sending message to colleague`, 'tool_call');
744
+ } else {
745
+ requirement.addGroupMessage(agent, `🔧 Using tool: ${tool}`, 'tool_call');
746
+ }
747
+ } else if (status === 'done') {
748
+ // CLI complete — special handling
749
+ if (tool === 'cli_complete') {
750
+ const backend = args?.backend || 'CLI';
751
+ const exitCode = args?.exitCode;
752
+ requirement.updateLiveStatus({
753
+ currentAction: `${agent.name} completed work via ${backend} (exit: ${exitCode})`,
754
+ toolCallsInProgress: [],
755
+ });
756
+ requirement.addGroupMessage(agent, `✅ ${backend} execution completed (exit code: ${exitCode})`, 'tool_call');
757
+ return;
758
+ }
759
+
760
+ requirement.updateLiveStatus({
761
+ currentAction: `${agent.name} completed tool call ${tool}`,
762
+ toolCallsInProgress: (requirement.liveStatus.toolCallsInProgress || []).filter(t => t !== tool),
763
+ });
764
+ } else if (status === 'error') {
765
+ requirement.addGroupMessage(agent, `⚠️ Tool ${tool} failed: ${toolErr}`, 'tool_call');
766
+ requirement.updateLiveStatus({
767
+ toolCallsInProgress: (requirement.liveStatus.toolCallsInProgress || []).filter(t => t !== tool),
768
+ });
769
+ }
770
+ },
771
+ onLLMCall: ({ iteration, maxIterations }) => {
772
+ requirement.updateLiveStatus({
773
+ currentAction: `${agent.name} is typing... (round ${iteration})`,
774
+ });
775
+ if (iteration > 1) {
776
+ requirement.addGroupMessage(agent, `🧠 Continuing to think and execute... (round ${iteration})`, 'tool_call');
777
+ }
778
+ },
779
+ });
780
+
781
+ // After CLI execution: detect file changes by comparing snapshots
782
+ if (snapshotBefore && wsPath) {
783
+ try {
784
+ const wsm = new WorkspaceManager();
785
+ const snapshotAfter = await wsm.takeSnapshot(wsPath);
786
+ const { created, modified } = wsm.diffSnapshots(snapshotBefore, snapshotAfter);
787
+ for (const fp of created) {
788
+ requirement.addFileChange(agent.name, fp, 'create');
789
+ }
790
+ for (const fp of modified) {
791
+ requirement.addFileChange(agent.name, fp, 'write');
792
+ }
793
+ if (created.length + modified.length > 0) {
794
+ console.log(` 📁 [CLI file detection] ${agent.name}: ${created.length} created, ${modified.length} modified`);
795
+ }
796
+ } catch (err) {
797
+ console.warn(` ⚠️ CLI file change detection failed:`, err.message);
798
+ }
799
+ }
800
+
801
+ node.status = TaskNodeStatus.COMPLETED;
802
+ node.completedAt = new Date();
803
+ node.result = result;
804
+
805
+ // === QUALITY CHECK: Warn if review/integration tasks didn't actually read files ===
806
+ const titleLower = (node.title || '').toLowerCase();
807
+ const isReviewLikeTask = titleLower.includes('review') || titleLower.includes('审') ||
808
+ titleLower.includes('integrat') || titleLower.includes('整合') ||
809
+ titleLower.includes('check') || titleLower.includes('检查') ||
810
+ titleLower.includes('final') || titleLower.includes('最终');
811
+ const usedFileRead = (result.toolResults || []).some(t => t.tool === 'file_read');
812
+ const hasWrittenFiles = (result.toolResults || []).some(t =>
813
+ t.tool === 'file_write' || t.tool === 'file_append' || t.tool === 'file_patch'
814
+ );
815
+ if (isReviewLikeTask && !usedFileRead && !hasWrittenFiles && result.duration < 15000) {
816
+ requirement.addGroupMessage(
817
+ { name: 'System', role: 'system' },
818
+ `⚠️ Task "${node.title}" appears to be a review/integration task but completed in ${Math.round(result.duration / 1000)}s without reading any files. The output may be superficial.`,
819
+ 'system', null, { auto: true }
820
+ );
821
+ }
822
+
823
+ // === REVIEW GATE: If reviewer assigned, trigger strict review loop ===
824
+ if (node.reviewerId && node.reviewerId !== node.assigneeId) {
825
+ const reviewer = department.agents.get(node.reviewerId);
826
+ if (reviewer) {
827
+ node.status = TaskNodeStatus.REVIEWING;
828
+ requirement.addGroupMessage(
829
+ { name: 'System', role: 'system' },
830
+ `🔍 Task "${node.title}" entering review phase`,
831
+ 'system', null, { auto: true }
832
+ );
833
+ requirement.addGroupMessage(
834
+ reviewer,
835
+ `🔍 @[${agent.id}] I'm now reviewing your work on "${node.title}". Let me take a careful look...`,
836
+ 'message', null, { auto: true }
837
+ );
838
+
839
+ let currentOutput = result.output;
840
+ let approved = false;
841
+ const maxRounds = node.maxReviewRounds || 10;
842
+
843
+ while (!approved && node.reviewRounds < maxRounds) {
844
+ node.reviewRounds++;
845
+
846
+ // Reviewer performs strict review
847
+ requirement.updateLiveStatus({
848
+ currentNodeId: node.id,
849
+ currentNodeTitle: `Review: ${node.title}`,
850
+ currentAgent: reviewer.name,
851
+ currentAction: `${reviewer.name} is reviewing "${node.title}" (round ${node.reviewRounds})`,
852
+ });
853
+
854
+ const reviewResult = await this._strictReview(
855
+ reviewer, agent, node, currentOutput, requirement, node.reviewRounds, department
856
+ );
857
+
858
+ if (reviewResult.approved) {
859
+ approved = true;
860
+ requirement.addGroupMessage(
861
+ reviewer,
862
+ `✅ @[${agent.id}] Review APPROVED for "${node.title}"${node.reviewRounds > 1 ? ` (after ${node.reviewRounds} rounds)` : ''}! ${reviewResult.comment || 'Good work!'}`,
863
+ 'message', null, { auto: true }
864
+ );
865
+ this._recordAgentChat(reviewer, agent, `✅ Review APPROVED: ${reviewResult.comment || 'Good work!'}`);
866
+ } else {
867
+ // Review rejected — enter negotiation phase
868
+ node.status = TaskNodeStatus.REVISION;
869
+ requirement.addGroupMessage(
870
+ reviewer,
871
+ `❌ @[${agent.id}] Review REJECTED for "${node.title}" (round ${node.reviewRounds}/${maxRounds}):\n${reviewResult.feedback}`,
872
+ 'message', null, { auto: true }
873
+ );
874
+ this._recordAgentChat(reviewer, agent, `❌ Review REJECTED (round ${node.reviewRounds}): ${reviewResult.feedback}`);
875
+
876
+ if (node.reviewRounds >= maxRounds) {
877
+ // Max rounds reached, force approve with warning
878
+ approved = true;
879
+ requirement.addGroupMessage(
880
+ { name: 'System', role: 'system' },
881
+ `⚠️ Review for "${node.title}" reached max rounds (${maxRounds}). Force-proceeding with latest revision.`,
882
+ 'system', null, { auto: true }
883
+ );
884
+ } else {
885
+ // === Negotiation phase: reviewee can accept or contest the feedback ===
886
+ const rebuttalResult = await this._assigneeRebuttal(
887
+ agent, reviewer, node, currentOutput, reviewResult.feedback, requirement, node.reviewRounds
888
+ );
889
+
890
+ if (rebuttalResult.accept) {
891
+ // Reviewee accepts feedback, making revisions
892
+ node.status = TaskNodeStatus.RUNNING;
893
+ requirement.updateLiveStatus({
894
+ currentNodeId: node.id,
895
+ currentNodeTitle: `Revision: ${node.title}`,
896
+ currentAgent: agent.name,
897
+ currentAction: `${agent.name} is revising "${node.title}" based on review feedback (round ${node.reviewRounds})`,
898
+ });
899
+ requirement.addGroupMessage(
900
+ agent,
901
+ `🔄 @[${reviewer.id}] ${rebuttalResult.message || `Got it, revising "${node.title}" based on your feedback...`}`,
902
+ 'message', null, { auto: true }
903
+ );
904
+
905
+ try {
906
+ const revisionResult = await this._executeRevision(
907
+ agent, node, currentOutput, reviewResult.feedback, requirement, {
908
+ onToolCall: ({ tool, args, status, success, error: toolErr }) => {
909
+ if (status === 'start') {
910
+ requirement.updateLiveStatus({
911
+ currentAction: `${agent.name} (revision) calling ${tool}`,
912
+ toolCallsInProgress: [...(requirement.liveStatus.toolCallsInProgress || []), tool],
913
+ });
914
+ if (tool === 'file_write' || tool === 'file_append' || tool === 'file_patch') {
915
+ const filePath = args?.path || args?.filePath || args?.file_path || '';
916
+ requirement.addGroupMessage(agent, `📝 [Revision] Writing file: ${filePath}`, 'tool_call');
917
+ requirement.addFileChange(agent.name, filePath, 'write');
918
+ }
919
+ } else if (status === 'done') {
920
+ requirement.updateLiveStatus({
921
+ toolCallsInProgress: (requirement.liveStatus.toolCallsInProgress || []).filter(t => t !== tool),
922
+ });
923
+ }
924
+ },
925
+ onLLMCall: ({ iteration }) => {
926
+ requirement.updateLiveStatus({
927
+ currentAction: `${agent.name} is revising... (iteration ${iteration})`,
928
+ });
929
+ },
930
+ }
931
+ );
932
+ currentOutput = revisionResult.output || currentOutput;
933
+ } catch (revisionErr) {
934
+ console.error(` ❌ Revision failed for "${node.title}":`, revisionErr.message);
935
+ requirement.addGroupMessage(
936
+ agent,
937
+ `⚠️ Revision attempt failed: ${revisionErr.message}. Proceeding with previous output.`,
938
+ 'message', null, { auto: true }
939
+ );
940
+ }
941
+ } else {
942
+ // Reviewee contests! Entering confrontation phase
943
+ requirement.addGroupMessage(
944
+ agent,
945
+ `💬 @[${reviewer.id}] ${rebuttalResult.message}`,
946
+ 'message', null, { auto: true }
947
+ );
948
+ this._recordAgentChat(agent, reviewer, `💬 Rebuttal: ${rebuttalResult.message}`);
949
+
950
+ // Reviewer re-evaluates
951
+ const reEvalResult = await this._reviewerReEvaluate(
952
+ reviewer, agent, node, currentOutput, reviewResult.feedback, rebuttalResult.message, requirement, node.reviewRounds
953
+ );
954
+
955
+ if (reEvalResult.convinced) {
956
+ // Reviewer was persuaded!
957
+ approved = true;
958
+ requirement.addGroupMessage(
959
+ reviewer,
960
+ `✅ @[${agent.id}] ${reEvalResult.message || `Fair point! I'll approve "${node.title}".`}`,
961
+ 'message', null, { auto: true }
962
+ );
963
+ this._recordAgentChat(reviewer, agent, `✅ Convinced by rebuttal, approved: ${reEvalResult.message}`);
964
+ } else {
965
+ // Reviewer stands firm
966
+ requirement.addGroupMessage(
967
+ reviewer,
968
+ `🤔 @[${agent.id}] ${reEvalResult.message || `I understand your point, but I still think the issues need to be addressed.`}`,
969
+ 'message', null, { auto: true }
970
+ );
971
+ this._recordAgentChat(reviewer, agent, `🤔 Not convinced: ${reEvalResult.message}`);
972
+
973
+ // Reviewee ultimately must revise
974
+ node.status = TaskNodeStatus.RUNNING;
975
+ requirement.updateLiveStatus({
976
+ currentNodeId: node.id,
977
+ currentNodeTitle: `Revision: ${node.title}`,
978
+ currentAgent: agent.name,
979
+ currentAction: `${agent.name} is revising "${node.title}" after discussion (round ${node.reviewRounds})`,
980
+ });
981
+ requirement.addGroupMessage(
982
+ agent,
983
+ `🔄 @[${reviewer.id}] Alright, I'll revise "${node.title}" incorporating your feedback.`,
984
+ 'message', null, { auto: true }
985
+ );
986
+
987
+ try {
988
+ const revisionResult = await this._executeRevision(
989
+ agent, node, currentOutput, `${reviewResult.feedback}\n\n[Discussion context] Assignee argued: "${rebuttalResult.message}" but reviewer insisted: "${reEvalResult.message}"`, requirement, {
990
+ onToolCall: ({ tool, args, status }) => {
991
+ if (status === 'start') {
992
+ requirement.updateLiveStatus({
993
+ currentAction: `${agent.name} (revision) calling ${tool}`,
994
+ toolCallsInProgress: [...(requirement.liveStatus.toolCallsInProgress || []), tool],
995
+ });
996
+ if (tool === 'file_write' || tool === 'file_append' || tool === 'file_patch') {
997
+ const filePath = args?.path || args?.filePath || args?.file_path || '';
998
+ requirement.addGroupMessage(agent, `📝 [Revision] Writing file: ${filePath}`, 'tool_call');
999
+ requirement.addFileChange(agent.name, filePath, 'write');
1000
+ }
1001
+ } else if (status === 'done') {
1002
+ requirement.updateLiveStatus({
1003
+ toolCallsInProgress: (requirement.liveStatus.toolCallsInProgress || []).filter(t => t !== tool),
1004
+ });
1005
+ }
1006
+ },
1007
+ onLLMCall: ({ iteration }) => {
1008
+ requirement.updateLiveStatus({
1009
+ currentAction: `${agent.name} is revising after discussion... (iteration ${iteration})`,
1010
+ });
1011
+ },
1012
+ }
1013
+ );
1014
+ currentOutput = revisionResult.output || currentOutput;
1015
+ } catch (revisionErr) {
1016
+ console.error(` ❌ Revision after discussion failed for "${node.title}":`, revisionErr.message);
1017
+ requirement.addGroupMessage(
1018
+ agent,
1019
+ `⚠️ Revision attempt failed: ${revisionErr.message}. Proceeding with previous output.`,
1020
+ 'message', null, { auto: true }
1021
+ );
1022
+ }
1023
+ }
1024
+ }
1025
+
1026
+ // After negotiation/revision, notify reviewer to re-review (if not approved during negotiation)
1027
+ if (!approved) {
1028
+ requirement.addGroupMessage(
1029
+ agent,
1030
+ `📝 @[${reviewer.id}] Revision complete for "${node.title}", please review again.`,
1031
+ 'message', null, { auto: true }
1032
+ );
1033
+ this._recordAgentChat(agent, reviewer, `Revision complete, please review again.`);
1034
+
1035
+ // Back to REVIEWING for next loop iteration
1036
+ node.status = TaskNodeStatus.REVIEWING;
1037
+ }
1038
+ }
1039
+ }
1040
+ }
1041
+
1042
+ // Final status
1043
+ node.status = TaskNodeStatus.COMPLETED;
1044
+ node.completedAt = new Date();
1045
+ }
1046
+ }
1047
+ // === END REVIEW GATE ===
1048
+
1049
+ completed.add(node.id);
1050
+
1051
+ // Record output (use node.result which may have been updated by revision)
1052
+ const finalResult = node.result || result;
1053
+ if (finalResult.output) {
1054
+ // Determine output type
1055
+ const outputType = this._detectOutputType(finalResult);
1056
+ // Clean LLM tool-call markup from output before storing
1057
+ const cleanedOutputForStore = this._cleanLLMOutput(finalResult.output) || finalResult.output;
1058
+ requirement.addOutput(
1059
+ agent.id, agent.name, agent.role,
1060
+ outputType, cleanedOutputForStore,
1061
+ { toolResults: finalResult.toolResults, duration: finalResult.duration }
1062
+ );
1063
+ } else if (finalResult.toolResults?.length > 0) {
1064
+ // Even if output text is empty, if tools were used (e.g. file_write),
1065
+ // still record an output entry so it shows in the Outputs tab
1066
+ const fileWriteToolsForOutput = new Set(['file_write', 'file_append', 'file_patch']);
1067
+ const fileWrites = (finalResult.toolResults || []).filter(t => fileWriteToolsForOutput.has(t.tool));
1068
+ if (fileWrites.length > 0) {
1069
+ const filePaths = fileWrites.map(t => t.args?.path || t.args?.filePath || t.args?.file_path || 'unknown').join(', ');
1070
+ requirement.addOutput(
1071
+ agent.id, agent.name, agent.role,
1072
+ 'code', `[Files written] ${filePaths}`,
1073
+ { toolResults: finalResult.toolResults, duration: finalResult.duration }
1074
+ );
1075
+ } else {
1076
+ const toolNames = finalResult.toolResults.map(t => t.tool).join(', ');
1077
+ requirement.addOutput(
1078
+ agent.id, agent.name, agent.role,
1079
+ 'text', `[Tools used] ${toolNames}`,
1080
+ { toolResults: finalResult.toolResults, duration: finalResult.duration }
1081
+ );
1082
+ }
1083
+ }
1084
+
1085
+ // Group chat notification: completed — with file references for written files
1086
+ const duration = Math.round((finalResult.duration || 0) / 1000);
1087
+ const fileWriteTools = new Set(['file_write', 'file_append', 'file_patch']);
1088
+ const fileWrites = (finalResult.toolResults || []).filter(t => fileWriteTools.has(t.tool));
1089
+ const validFileRefs = fileWrites
1090
+ .map(t => {
1091
+ const fp = t.args?.path || t.args?.filePath || t.args?.file_path || '';
1092
+ // Skip empty paths (can happen when LLM sends malformed tool args)
1093
+ if (!fp) return null;
1094
+ // Only create clickable reference if file actually exists
1095
+ if (wsPath && !existsSync(path.join(wsPath, fp))) return null;
1096
+ const name = fp.split('/').pop() || fp;
1097
+ return `[[file:${requirement.departmentId}:${fp}|${name}]]`;
1098
+ })
1099
+ .filter(Boolean);
1100
+ const fileRefTags = validFileRefs.length > 0
1101
+ ? '\n' + validFileRefs.join(' ')
1102
+ : '';
1103
+ requirement.addGroupMessage(
1104
+ agent,
1105
+ `✅ "${node.title}" completed! Took ${duration}s.${finalResult.toolResults?.length ? `\n🔧 Used ${finalResult.toolResults.length} tools` : ''}${node.reviewRounds > 0 ? `\n🔍 Passed review after ${node.reviewRounds} round(s)` : ''}${fileRefTags}`,
1106
+ 'message', null, { auto: true }
1107
+ );
1108
+
1109
+ // Share output: prioritize file references over LLM text summary
1110
+ const hasFiles = validFileRefs.length > 0;
1111
+ if (!hasFiles && finalResult.output?.trim()) {
1112
+ // No files written → this is a text-only task, show the LLM text output
1113
+ const cleanedOutput = this._cleanLLMOutput(finalResult.output);
1114
+ if (cleanedOutput && cleanedOutput.length > 20) {
1115
+ const preview = cleanedOutput.length > 300
1116
+ ? cleanedOutput.slice(0, 300) + '...\n(output truncated)'
1117
+ : cleanedOutput;
1118
+ requirement.addGroupMessage(
1119
+ agent,
1120
+ `📄 My output:\n${preview}`,
1121
+ 'message', null, { auto: true }
1122
+ );
1123
+ }
1124
+ }
1125
+ // If files were written, the ✅ completed message above already includes
1126
+ // clickable [[file:...]] references — no need to also dump LLM's text summary,
1127
+ // which is typically just a verbose description of what was already done.
1128
+
1129
+ // Agent-to-agent collaboration: notify downstream agents with @mention
1130
+ const downstreamNodes = nodes.filter(n =>
1131
+ n.dependencies.includes(node.id) &&
1132
+ n.status !== TaskNodeStatus.COMPLETED &&
1133
+ n.status !== TaskNodeStatus.FAILED
1134
+ );
1135
+ if (downstreamNodes.length > 0 && finalResult.output) {
1136
+ for (const downNode of downstreamNodes) {
1137
+ const downAgent = department.agents.get(downNode.assigneeId);
1138
+ if (downAgent && downAgent.id !== agent.id) {
1139
+ // Record in group chat with @mention + file references
1140
+ const nodeFileRefs = fileRefTags ? `\nFiles: ${fileRefTags.trim()}` : '';
1141
+ const mentionMsg = `@[${downAgent.id}] I've completed "${node.title}", the output is ready for your "${downNode.title}" task. Please review and let me know if anything needs adjustment!${nodeFileRefs}`;
1142
+ requirement.addGroupMessage(agent, mentionMsg, 'message', null, { auto: true });
1143
+
1144
+ // Persist to agent-to-agent chatStore
1145
+ this._recordAgentChat(agent, downAgent, mentionMsg);
1146
+
1147
+ // Trigger downstream agent's GroupChatLoop to process via heartflow
1148
+ // instead of auto-replying directly (bypassing flow thinking)
1149
+ try {
1150
+ const { groupChatLoop } = await import('./organization/group-chat-loop.js');
1151
+ groupChatLoop.triggerImmediate(downAgent.id, requirement.id, {
1152
+ content: mentionMsg,
1153
+ from: agent,
1154
+ }).catch(() => {});
1155
+ } catch (e) {
1156
+ // Non-blocking
1157
+ }
1158
+ }
1159
+ }
1160
+ }
1161
+
1162
+ // Also notify parallel peers that just completed (non-blocking peer sync)
1163
+ const justCompletedPeers = readyNodes
1164
+ .filter(n => n.id !== node.id && n.status === TaskNodeStatus.COMPLETED)
1165
+ .slice(0, 2); // Limit to avoid spam
1166
+ for (const peerNode of justCompletedPeers) {
1167
+ const peerAgent = department.agents.get(peerNode.assigneeId);
1168
+ if (peerAgent && peerAgent.id !== agent.id) {
1169
+ const syncMsg = `@[${peerAgent.id}] Just finished my part "${node.title}" ✅ — how's yours going?`;
1170
+ requirement.addGroupMessage(agent, syncMsg, 'message', null, { auto: true });
1171
+ this._recordAgentChat(agent, peerAgent, syncMsg);
1172
+
1173
+ // Trigger peer's GroupChatLoop to process via heartflow
1174
+ try {
1175
+ const { groupChatLoop } = await import('./organization/group-chat-loop.js');
1176
+ groupChatLoop.triggerImmediate(peerAgent.id, requirement.id, {
1177
+ content: syncMsg,
1178
+ from: agent,
1179
+ }).catch(() => {});
1180
+ } catch (e) {
1181
+ // Non-blocking
1182
+ }
1183
+ }
1184
+ }
1185
+
1186
+ allResults.push(finalResult);
1187
+ return finalResult;
1188
+ } catch (err) {
1189
+ node.status = TaskNodeStatus.FAILED;
1190
+ node.completedAt = new Date();
1191
+ node.result = { error: err.message, success: false };
1192
+ failed.add(node.id);
1193
+
1194
+ requirement.addGroupMessage(
1195
+ agent,
1196
+ `❌ "${node.title}" failed: ${err.message}`,
1197
+ 'message', null, { auto: true }
1198
+ );
1199
+ return null;
1200
+ }
1201
+ });
1202
+
1203
+ await Promise.all(promises);
1204
+
1205
+ // === Parallel Sync Point: agents that just finished in this batch exchange feedback ===
1206
+ const justFinished = readyNodes.filter(n => n.status === TaskNodeStatus.COMPLETED);
1207
+ if (justFinished.length > 1) {
1208
+ requirement.addGroupMessage(
1209
+ { name: 'System', role: 'system' },
1210
+ `🤝 ${justFinished.length} tasks completed in parallel! Agents are exchanging feedback...`,
1211
+ 'system', null, { auto: true }
1212
+ );
1213
+
1214
+ // Each agent gives brief feedback to one other agent's work (round-robin, non-blocking)
1215
+ const syncPromises = justFinished.map(async (node, idx) => {
1216
+ const nextNode = justFinished[(idx + 1) % justFinished.length];
1217
+ if (nextNode.id === node.id) return; // Only 1 node, skip
1218
+
1219
+ const reviewer = department.agents.get(node.assigneeId);
1220
+ const reviewee = department.agents.get(nextNode.assigneeId);
1221
+ if (!reviewer || !reviewee || reviewer.id === reviewee.id) return;
1222
+
1223
+ try {
1224
+ const feedback = await this._agentPeerReview(
1225
+ reviewer, reviewee, nextNode.title, nextNode.result?.output, requirement
1226
+ );
1227
+ if (feedback) {
1228
+ const feedbackMsg = `@[${reviewee.id}] ${feedback}`;
1229
+ requirement.addGroupMessage(reviewer, feedbackMsg, 'message', null, { auto: true });
1230
+ this._recordAgentChat(reviewer, reviewee, feedbackMsg);
1231
+ }
1232
+ } catch (e) {
1233
+ // Non-blocking
1234
+ }
1235
+ });
1236
+ // Don't await all — fire and forget for non-critical feedback
1237
+ Promise.all(syncPromises).catch(() => {});
1238
+ }
1239
+ }
1240
+
1241
+ // === Clean up message bus listener ===
1242
+ if (messageBus) {
1243
+ messageBus.off('message', messageHandler);
1244
+ }
1245
+
1246
+ // Summary
1247
+ const successCount = completed.size;
1248
+ const totalCount = nodes.length;
1249
+ const totalDuration = allResults.reduce((s, r) => s + (r?.duration || 0), 0);
1250
+
1251
+ // Detect suspiciously fast completions (tasks that finished too quickly with no real output)
1252
+ const suspiciousNodes = nodes.filter(n => {
1253
+ if (n.status !== TaskNodeStatus.COMPLETED) return false;
1254
+ const dur = n.result?.duration || 0;
1255
+ const hasOutput = n.result?.output?.trim();
1256
+ const hasToolResults = n.result?.toolResults?.length > 0;
1257
+ // Flag nodes that completed in < 5 seconds with no tool usage and minimal output
1258
+ return dur < 5000 && !hasToolResults && (!hasOutput || hasOutput.length < 50);
1259
+ });
1260
+
1261
+ if (suspiciousNodes.length > 0) {
1262
+ const names = suspiciousNodes.map(n => `"${n.title}"`).join(', ');
1263
+ requirement.addGroupMessage(
1264
+ { name: 'System', role: 'system' },
1265
+ `⚠️ Warning: ${suspiciousNodes.length} task(s) completed suspiciously fast with minimal output: ${names}. These may not have been executed thoroughly.`,
1266
+ 'system', null, { auto: true }
1267
+ );
1268
+ }
1269
+
1270
+ if (failed.size === totalCount) {
1271
+ // All tasks failed → mark as FAILED immediately
1272
+ requirement.status = RequirementStatus.FAILED;
1273
+ requirement.completedAt = new Date();
1274
+ requirement.updateLiveStatus({
1275
+ currentNodeId: null, currentNodeTitle: null, currentAgent: null,
1276
+ currentAction: 'Execution finished (all tasks failed)',
1277
+ toolCallsInProgress: [],
1278
+ });
1279
+ } else {
1280
+ // Tasks done → enter PENDING_APPROVAL, wait for Boss to review and confirm
1281
+ requirement.status = RequirementStatus.PENDING_APPROVAL;
1282
+ requirement.updateLiveStatus({
1283
+ currentNodeId: null, currentNodeTitle: null, currentAgent: null,
1284
+ currentAction: 'All tasks done — awaiting Boss approval',
1285
+ toolCallsInProgress: [],
1286
+ });
1287
+ }
1288
+
1289
+ requirement.summary = {
1290
+ totalTasks: totalCount,
1291
+ successTasks: successCount,
1292
+ failedTasks: failed.size,
1293
+ totalDuration,
1294
+ outputs: requirement.outputs,
1295
+ suspiciousTasks: suspiciousNodes.length,
1296
+ };
1297
+
1298
+ if (requirement.status === RequirementStatus.FAILED) {
1299
+ requirement.addGroupMessage(
1300
+ { name: 'System', role: 'system' },
1301
+ `❌ Requirement "${requirement.title}" failed!\n📊 ${successCount}/${totalCount} tasks succeeded, total duration ${Math.round(totalDuration / 1000)}s`,
1302
+ 'system', null, { auto: true }
1303
+ );
1304
+ } else {
1305
+ requirement.addGroupMessage(
1306
+ { name: 'System', role: 'system' },
1307
+ `📋 All tasks for "${requirement.title}" are done!\n📊 ${successCount}/${totalCount} tasks succeeded, total duration ${Math.round(totalDuration / 1000)}s${suspiciousNodes.length > 0 ? `\n⚠️ ${suspiciousNodes.length} task(s) may need manual review` : ''}\n\n⏳ **Pending your approval, Boss.** The requirement is NOT yet completed.\n💬 Please review the outputs, then reply in this chat:\n • "OK" / "通过" / "approved" → approve and finalize\n • Or send feedback to request changes`,
1308
+ 'system', null, { auto: true }
1309
+ );
1310
+ }
1311
+
1312
+ // Performance evaluation
1313
+ if (performanceSystem) {
1314
+ const leader = department.getLeader();
1315
+ for (const node of nodes) {
1316
+ if (node.status !== TaskNodeStatus.COMPLETED) continue;
1317
+ const agent = department.agents.get(node.assigneeId);
1318
+ if (!agent || !leader || leader.id === agent.id) continue;
1319
+ try {
1320
+ performanceSystem.autoEvaluate({
1321
+ agent,
1322
+ reviewer: leader,
1323
+ taskTitle: node.title,
1324
+ });
1325
+ } catch (e) { /* ignore */ }
1326
+ }
1327
+ }
1328
+
1329
+ return requirement.summary;
1330
+ }
1331
+
1332
+ /**
1333
+ * Agent collaboration reply: downstream agent reviews upstream output and responds
1334
+ * @param {Agent} responder - The agent responding
1335
+ * @param {Agent} sender - The agent who completed the task
1336
+ * @param {string} taskTitle - Completed task title
1337
+ * @param {string} output - Task output
1338
+ * @param {Requirement} requirement - Requirement context
1339
+ * @returns {Promise<string|null>} Reply content
1340
+ */
1341
+ async _agentCollabReply(responder, sender, taskTitle, output, requirement) {
1342
+ if (!responder.canChat()) return null;
1343
+
1344
+ try {
1345
+ const p = responder.personality || {};
1346
+ const outputPreview = output.length > 500
1347
+ ? output.slice(0, 500) + '\n...(truncated — if you need the full output, use file_read to read the workspace files)'
1348
+ : output;
1349
+ const response = await responder.chat([
1350
+ {
1351
+ role: 'system',
1352
+ content: `You are "${responder.name}", working as "${responder.role}".
1353
+ Your personality: ${p.trait || 'Professional'}. Speaking style: ${p.tone || 'Normal'}.
1354
+ You are collaborating with colleagues on requirement "${requirement.title}".
1355
+ A colleague just completed their task and shared the output with you.
1356
+ Please respond briefly (1-2 sentences) acknowledging their work, in your personality style.
1357
+ You can comment on the quality, ask a question, or just acknowledge. Keep it natural and brief.`
1358
+ },
1359
+ {
1360
+ role: 'user',
1361
+ content: `Your colleague ${sender.name} (${sender.role}) completed "${taskTitle}" and shared the output:\n\n${outputPreview}\n\nPlease respond briefly.`
1362
+ },
1363
+ ], { temperature: 0.9, maxTokens: 128 });
1364
+
1365
+ return response.content?.trim() || null;
1366
+ } catch (e) {
1367
+ return null;
1368
+ }
1369
+ }
1370
+
1371
+ /**
1372
+ * Agent peer review: parallel peers review each other's work
1373
+ * More casual and constructive than upstream→downstream handoff
1374
+ */
1375
+ async _agentPeerReview(reviewer, reviewee, taskTitle, output, requirement) {
1376
+ if (!reviewer.canChat()) return null;
1377
+ if (!output) return null;
1378
+
1379
+ try {
1380
+ const p = reviewer.personality || {};
1381
+ const outputPreview = output.length > 400
1382
+ ? output.slice(0, 400) + '\n...(truncated — use file_read to view full content in workspace files)'
1383
+ : output;
1384
+ const response = await reviewer.chat([
1385
+ {
1386
+ role: 'system',
1387
+ content: `You are "${reviewer.name}", working as "${reviewer.role}".
1388
+ Your personality: ${p.trait || 'Professional'}. Speaking style: ${p.tone || 'Normal'}.
1389
+ You just completed your parallel task for requirement "${requirement.title}".
1390
+ A colleague who worked in parallel with you also just finished their task. Please give brief, constructive feedback (1-2 sentences) on their work.
1391
+ Be natural, in character, and collegial. You can praise, suggest improvements, or note synergies with your own work.`
1392
+ },
1393
+ {
1394
+ role: 'user',
1395
+ content: `Your colleague ${reviewee.name} (${reviewee.role}) completed "${taskTitle}" in parallel with you. Their output:\n\n${outputPreview}\n\nGive brief peer feedback.`
1396
+ },
1397
+ ], { temperature: 0.9, maxTokens: 128 });
1398
+
1399
+ return response.content?.trim() || null;
1400
+ } catch (e) {
1401
+ return null;
1402
+ }
1403
+ }
1404
+
1405
+ /**
1406
+ * Strict review: reviewer carefully audits the assignee's work
1407
+ * Returns { approved: boolean, feedback: string, comment: string }
1408
+ */
1409
+ async _strictReview(reviewer, assignee, node, output, requirement, round, department = null) {
1410
+ if (!reviewer.canChat()) {
1411
+ return { approved: true, feedback: '', comment: 'Reviewer unavailable, auto-approved.' };
1412
+ }
1413
+
1414
+ try {
1415
+ const p = reviewer.personality || {};
1416
+ const outputContent = output?.length > 2000
1417
+ ? output.slice(0, 2000) + '\n...(truncated)'
1418
+ : output;
1419
+ const reviewCriteria = node.reviewCriteria || 'Check for correctness, completeness, and quality.';
1420
+
1421
+ // Collect actual file contents written by the assignee so the reviewer can see real deliverables
1422
+ const fileWriteTools = new Set(['file_write', 'file_append', 'file_patch']);
1423
+ const writtenFiles = (node.result?.toolResults || [])
1424
+ .filter(t => fileWriteTools.has(t.tool))
1425
+ .map(t => t.args?.path || t.args?.filePath || t.args?.file_path || '')
1426
+ .filter(Boolean);
1427
+
1428
+ let fileContentsSection = '';
1429
+ if (writtenFiles.length > 0) {
1430
+ const wsPath = department?.workspacePath || reviewer.toolKit?.workspaceDir;
1431
+ if (wsPath) {
1432
+ const uniqueFiles = [...new Set(writtenFiles)];
1433
+ const fileSnippets = [];
1434
+ for (const fp of uniqueFiles.slice(0, 5)) {
1435
+ try {
1436
+ const fullPath = path.join(wsPath, fp);
1437
+ if (existsSync(fullPath)) {
1438
+ const content = readFileSync(fullPath, 'utf-8');
1439
+ const preview = content.length > 2000
1440
+ ? content.slice(0, 2000) + '\n...(file truncated)'
1441
+ : content;
1442
+ fileSnippets.push(`--- File: ${fp} ---\n${preview}`);
1443
+ }
1444
+ } catch { /* skip unreadable files */ }
1445
+ }
1446
+ if (fileSnippets.length > 0) {
1447
+ fileContentsSection = `\n\n**Actual file contents delivered:**\n${fileSnippets.join('\n\n')}`;
1448
+ }
1449
+ }
1450
+ }
1451
+
1452
+ const response = await reviewer.chat([
1453
+ {
1454
+ role: 'system',
1455
+ content: `You are "${reviewer.name}", working as "${reviewer.role}".
1456
+ Your personality: ${p.trait || 'Professional'}. Speaking style: ${p.tone || 'Normal'}.
1457
+
1458
+ You are reviewing work for the requirement "${requirement.title}".
1459
+
1460
+ **Review Criteria for this task:**
1461
+ ${reviewCriteria}
1462
+
1463
+ **Your review guidelines:**
1464
+ - Be fair and professional. Approve if the work reasonably meets the criteria, even if minor improvements are possible.
1465
+ - ONLY reject for SIGNIFICANT issues: critical bugs, missing core requirements, major quality gaps, or incomplete deliverables.
1466
+ - Do NOT reject for stylistic preferences, minor improvements, or nice-to-haves.
1467
+ - When rejecting, provide SPECIFIC, ACTIONABLE feedback explaining exactly what MUST be fixed.
1468
+ - When approving, briefly comment on what was done well. You may suggest minor improvements as non-blocking notes.
1469
+ - ${round === 1 ? 'This is the first review. Focus on whether core requirements are met.' : `This is review round ${round}. The assignee has revised based on your previous feedback. Check if the CRITICAL issues from your previous feedback were addressed. Minor issues that were not addressed can be accepted.`}
1470
+ - IMPORTANT: If the assignee has produced actual file deliverables, review the FILE CONTENTS (not just the text output). The text output may be a summary; the real work is in the files.
1471
+
1472
+ **Output format (JSON only, no other text):**
1473
+ {
1474
+ "approved": true/false,
1475
+ "feedback": "Detailed rejection feedback with specific issues (only if rejected)",
1476
+ "comment": "Brief approval comment (only if approved)"
1477
+ }`
1478
+ },
1479
+ {
1480
+ role: 'user',
1481
+ content: `Please review ${assignee.name}'s (${assignee.role}) work on task "${node.title}":
1482
+
1483
+ **Task description:** ${node.description}
1484
+
1485
+ **${round > 1 ? `Revised output (round ${round}):` : 'Agent output:'}**
1486
+ ${outputContent || '(empty output)'}${fileContentsSection}
1487
+
1488
+ Please provide your review verdict as JSON.`
1489
+ },
1490
+ ], { temperature: 0.3, maxTokens: 1024 });
1491
+
1492
+ // Parse review result
1493
+ const tick = String.fromCharCode(96);
1494
+ const fence = tick + tick + tick;
1495
+ let content = response.content?.trim() || '';
1496
+ content = content.replace(fence + 'json', '').replace(fence, '').trim();
1497
+
1498
+ try {
1499
+ const result = JSON.parse(content);
1500
+ return {
1501
+ approved: !!result.approved,
1502
+ feedback: result.feedback || '',
1503
+ comment: result.comment || '',
1504
+ };
1505
+ } catch (parseErr) {
1506
+ // If JSON parse fails, try to detect approval from text
1507
+ const lower = content.toLowerCase();
1508
+ if (lower.includes('"approved": true') || (lower.includes('approved') && !lower.includes('reject'))) {
1509
+ return { approved: true, feedback: '', comment: content.slice(0, 200) };
1510
+ }
1511
+ return { approved: false, feedback: content.slice(0, 500), comment: '' };
1512
+ }
1513
+ } catch (e) {
1514
+ console.error(`[StrictReview] ${reviewer.name} review failed:`, e.message);
1515
+ // Don't silently auto-approve on error — flag it so it's visible
1516
+ requirement?.addGroupMessage?.(
1517
+ { name: 'System', role: 'system' },
1518
+ `⚠️ Review by ${reviewer.name} encountered an error: ${e.message}. Proceeding with caution (auto-approved due to error).`,
1519
+ 'system', null, { auto: true }
1520
+ );
1521
+ return { approved: true, feedback: '', comment: `⚠️ Review error (auto-approved): ${e.message}` };
1522
+ }
1523
+ }
1524
+
1525
+ /**
1526
+ * Execute revision: agent revises their work based on review feedback
1527
+ * Similar to executeTask but with revision context
1528
+ */
1529
+ async _executeRevision(agent, node, previousOutput, reviewFeedback, requirement, callbacks = {}) {
1530
+ const revisionTask = {
1531
+ title: `[Revision] ${node.title}`,
1532
+ description: `Your previous work on "${node.title}" was reviewed and REJECTED. You need to revise it.
1533
+
1534
+ **Original task description:**
1535
+ ${node.description}
1536
+
1537
+ **Your previous output:**
1538
+ ${previousOutput?.length > 1500 ? previousOutput.slice(0, 1500) + '\n...(truncated — use file_read to review your previous files before revising)' : previousOutput || '(empty)'}
1539
+
1540
+ **Reviewer's feedback (MUST address ALL points):**
1541
+ ${reviewFeedback}
1542
+
1543
+ **Instructions:**
1544
+ 1. Carefully read the reviewer's feedback
1545
+ 2. Address EVERY issue mentioned by the reviewer
1546
+ 3. If you wrote files before, READ them first, then MODIFY them (don't recreate from scratch)
1547
+ 4. Make sure the revised output fully addresses all review comments
1548
+ 5. Output your complete revised result`,
1549
+ context: '',
1550
+ requirements: `This is a REVISION for requirement "${requirement.title}". The reviewer was not satisfied and you must address their feedback.`,
1551
+ };
1552
+
1553
+ return await agent.executeTask(revisionTask, callbacks);
1554
+ }
1555
+
1556
+ /**
1557
+ * Assignee rebuttal: after receiving review rejection, the assignee can choose to
1558
+ * accept and revise, or push back (rebut) the reviewer's feedback.
1559
+ *
1560
+ * Returns { accept: boolean, message: string }
1561
+ * - accept=true: assignee agrees with feedback, will revise
1562
+ * - accept=false: assignee disagrees and provides counter-arguments
1563
+ */
1564
+ async _assigneeRebuttal(agent, reviewer, node, currentOutput, reviewFeedback, requirement, round) {
1565
+ if (!agent.canChat()) {
1566
+ return { accept: true, message: 'Got it, I\'ll revise.' };
1567
+ }
1568
+
1569
+ try {
1570
+ const p = agent.personality || {};
1571
+ const outputPreview = currentOutput?.length > 1500
1572
+ ? currentOutput.slice(0, 1500) + '\n...(truncated — use file_read to review the full content before deciding)'
1573
+ : currentOutput;
1574
+
1575
+ const response = await agent.chat([
1576
+ {
1577
+ role: 'system',
1578
+ content: `You are "${agent.name}", working as "${agent.role}".
1579
+ Your personality: ${p.trait || 'Professional'}. Speaking style: ${p.tone || 'Normal'}.
1580
+
1581
+ Your work on task "${node.title}" for requirement "${requirement.title}" was just reviewed and REJECTED by ${reviewer.name} (${reviewer.role}).
1582
+
1583
+ You now have a choice — this is a professional discussion, not a hierarchy:
1584
+ 1. **Accept** the feedback and revise your work (if you genuinely agree the feedback is valid)
1585
+ 2. **Push back** and argue your case (if you believe your work is correct or the feedback is unfair/wrong)
1586
+
1587
+ **Important guidelines for your decision:**
1588
+ - Be honest and professional. Don't just accept to be polite — if you genuinely believe your work is good, defend it!
1589
+ - Consider whether the reviewer's points are technically valid
1590
+ - If the feedback is about style preferences vs. actual errors, you can push back
1591
+ - If you made a genuine mistake, accept gracefully
1592
+ - When pushing back, provide SPECIFIC technical arguments, not just "I disagree"
1593
+ - Be respectful but firm — this is a professional debate between colleagues
1594
+
1595
+ **Output format (JSON only):**
1596
+ {
1597
+ "accept": true/false,
1598
+ "reasoning": "Your internal reasoning about the feedback (not shown to reviewer)",
1599
+ "message": "What you want to say to the reviewer (will be posted in group chat)"
1600
+ }`
1601
+ },
1602
+ {
1603
+ role: 'user',
1604
+ content: `Your work output:
1605
+ ${outputPreview}
1606
+
1607
+ Reviewer ${reviewer.name}'s rejection feedback:
1608
+ ${reviewFeedback}
1609
+
1610
+ This is review round ${round}. Do you accept the feedback and revise, or do you want to push back?`
1611
+ },
1612
+ ], { temperature: 0.7, maxTokens: 512 });
1613
+
1614
+ const tick = String.fromCharCode(96);
1615
+ const fence = tick + tick + tick;
1616
+ let content = response.content?.trim() || '';
1617
+ content = content.replace(fence + 'json', '').replace(fence, '').trim();
1618
+
1619
+ try {
1620
+ const start = content.indexOf('{');
1621
+ const end = content.lastIndexOf('}');
1622
+ if (start !== -1 && end > start) {
1623
+ const result = JSON.parse(content.slice(start, end + 1));
1624
+ return {
1625
+ accept: !!result.accept,
1626
+ message: result.message || (result.accept ? 'Got it, I\'ll revise.' : 'I respectfully disagree with some of the feedback.'),
1627
+ };
1628
+ }
1629
+ } catch {}
1630
+
1631
+ // Parse failure fallback: accept
1632
+ return { accept: true, message: 'Got it, I\'ll revise based on your feedback.' };
1633
+ } catch (e) {
1634
+ console.error(`[AssigneeRebuttal] ${agent.name} rebuttal failed:`, e.message);
1635
+ return { accept: true, message: 'Got it, I\'ll revise.' };
1636
+ }
1637
+ }
1638
+
1639
+ /**
1640
+ * Reviewer re-evaluate: after the assignee pushes back with counter-arguments,
1641
+ * the reviewer decides whether they're convinced or if they stand firm.
1642
+ *
1643
+ * Returns { convinced: boolean, message: string }
1644
+ * - convinced=true: reviewer accepts the rebuttal, work is approved
1645
+ * - convinced=false: reviewer insists, assignee must revise
1646
+ */
1647
+ async _reviewerReEvaluate(reviewer, assignee, node, output, originalFeedback, rebuttalMessage, requirement, round) {
1648
+ if (!reviewer.canChat()) {
1649
+ return { convinced: false, message: 'Please address my feedback.' };
1650
+ }
1651
+
1652
+ try {
1653
+ const p = reviewer.personality || {};
1654
+ const outputPreview = output?.length > 1000
1655
+ ? output.slice(0, 1000) + '\n...(truncated — use file_read to review the full content before deciding)'
1656
+ : output;
1657
+
1658
+ const response = await reviewer.chat([
1659
+ {
1660
+ role: 'system',
1661
+ content: `You are "${reviewer.name}", working as "${reviewer.role}".
1662
+ Your personality: ${p.trait || 'Professional'}. Speaking style: ${p.tone || 'Normal'}.
1663
+
1664
+ You rejected ${assignee.name}'s work on "${node.title}", but they pushed back with counter-arguments.
1665
+ Now you need to decide: were you wrong, or do you stand firm?
1666
+
1667
+ **Guidelines:**
1668
+ - Be open-minded — good reviewers can admit when they're wrong
1669
+ - If the assignee makes valid technical points that address your concerns, be convinced
1670
+ - If the assignee is just making excuses without substance, stand firm
1671
+ - If it's a matter of style/preference rather than correctness, consider being flexible
1672
+ - Don't be stubborn for the sake of it — the goal is quality, not winning arguments
1673
+ - Be approximately 30-40% likely to be convinced if the argument has some merit
1674
+
1675
+ **Output format (JSON only):**
1676
+ {
1677
+ "convinced": true/false,
1678
+ "reasoning": "Your internal reasoning (not shown to assignee)",
1679
+ "message": "What you want to say in response (will be posted in group chat)"
1680
+ }`
1681
+ },
1682
+ {
1683
+ role: 'user',
1684
+ content: `Your original rejection feedback:
1685
+ ${originalFeedback}
1686
+
1687
+ ${assignee.name}'s counter-argument:
1688
+ ${rebuttalMessage}
1689
+
1690
+ The work in question:
1691
+ ${outputPreview}
1692
+
1693
+ Are you convinced by their argument, or do you insist they need to revise?`
1694
+ },
1695
+ ], { temperature: 0.6, maxTokens: 512 });
1696
+
1697
+ const tick = String.fromCharCode(96);
1698
+ const fence = tick + tick + tick;
1699
+ let content = response.content?.trim() || '';
1700
+ content = content.replace(fence + 'json', '').replace(fence, '').trim();
1701
+
1702
+ try {
1703
+ const start = content.indexOf('{');
1704
+ const end = content.lastIndexOf('}');
1705
+ if (start !== -1 && end > start) {
1706
+ const result = JSON.parse(content.slice(start, end + 1));
1707
+ return {
1708
+ convinced: !!result.convinced,
1709
+ message: result.message || (result.convinced ? 'You make a fair point, approved!' : 'I still think the issues need to be addressed.'),
1710
+ };
1711
+ }
1712
+ } catch {}
1713
+
1714
+ return { convinced: false, message: 'I appreciate your perspective, but please address my feedback.' };
1715
+ } catch (e) {
1716
+ console.error(`[ReviewerReEvaluate] ${reviewer.name} re-evaluation failed:`, e.message);
1717
+ return { convinced: false, message: 'Please address my feedback.' };
1718
+ }
1719
+ }
1720
+
1721
+ /**
1722
+ * Record agent-to-agent chat message in chatStore
1723
+ * Session format: agent-agent-{smallerId}-{largerId} (consistent ordering)
1724
+ */
1725
+ _recordAgentChat(fromAgent, toAgent, content) {
1726
+ const ids = [fromAgent.id, toAgent.id].sort();
1727
+ const sessionId = `agent-agent-${ids[0]}-${ids[1]}`;
1728
+ chatStore.createSession(sessionId, {
1729
+ title: `${fromAgent.name} & ${toAgent.name}`,
1730
+ participants: [fromAgent.id, toAgent.id],
1731
+ participantNames: [fromAgent.name, toAgent.name],
1732
+ type: 'agent-agent',
1733
+ });
1734
+ chatStore.appendMessage(sessionId, {
1735
+ role: 'agent',
1736
+ content,
1737
+ time: new Date(),
1738
+ fromAgentId: fromAgent.id,
1739
+ fromAgentName: fromAgent.name,
1740
+ toAgentId: toAgent.id,
1741
+ toAgentName: toAgent.name,
1742
+ });
1743
+ }
1744
+
1745
+ /**
1746
+ * Clean LLM output by stripping tool-call markup that some models embed in text
1747
+ * (e.g. DeepSeek's DSML format, or XML-style function_call tags)
1748
+ */
1749
+ _cleanLLMOutput(text) {
1750
+ if (!text) return '';
1751
+ let cleaned = text;
1752
+ // Strip DSML-style tool call blocks: <|DSML|function_calls>...</|DSML|function_calls> or trailing
1753
+ cleaned = cleaned.replace(/<|DSML|function_calls>[\s\S]*?<\/|DSML|function_calls>/g, '');
1754
+ // Strip trailing incomplete DSML block (when output was truncated mid-tool-call)
1755
+ cleaned = cleaned.replace(/<|DSML|[\s\S]*$/g, '');
1756
+ // Strip generic XML-style tool call blocks: <function_call>...</function_call>
1757
+ cleaned = cleaned.replace(/<function_call>[\s\S]*?<\/function_call>/g, '');
1758
+ cleaned = cleaned.replace(/<function_call>[\s\S]*$/g, '');
1759
+ // Strip <tool_call>...</tool_call> blocks
1760
+ cleaned = cleaned.replace(/<tool_call>[\s\S]*?<\/tool_call>/g, '');
1761
+ cleaned = cleaned.replace(/<tool_call>[\s\S]*$/g, '');
1762
+ return cleaned.trim();
1763
+ }
1764
+
1765
+ /**
1766
+ * Detect output type
1767
+ */
1768
+ _detectOutputType(result) {
1769
+ const toolNames = (result.toolResults || []).map(t => t.tool);
1770
+ if (toolNames.includes('file_write')) return 'code';
1771
+ if (result.output?.includes('```')) return 'code';
1772
+ return 'text';
1773
+ }
1774
+
1775
+ /** Serialize all requirements */
1776
+ serialize() {
1777
+ return [...this.requirements.values()].map(r => r.serialize());
1778
+ }
1779
+
1780
+ /** Deserialize */
1781
+ static deserialize(dataList) {
1782
+ const mgr = new RequirementManager();
1783
+ for (const d of (dataList || [])) {
1784
+ const req = Requirement.deserialize(d);
1785
+ mgr.requirements.set(req.id, req);
1786
+ }
1787
+ return mgr;
1788
+ }
1789
+ }