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