kiro-spec-engine 1.2.3 → 1.4.0

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 (78) hide show
  1. package/CHANGELOG.md +135 -0
  2. package/README.md +239 -213
  3. package/README.zh.md +0 -330
  4. package/bin/kiro-spec-engine.js +62 -0
  5. package/docs/README.md +223 -0
  6. package/docs/agent-hooks-analysis.md +815 -0
  7. package/docs/command-reference.md +252 -0
  8. package/docs/cross-tool-guide.md +554 -0
  9. package/docs/examples/add-export-command/design.md +194 -0
  10. package/docs/examples/add-export-command/requirements.md +110 -0
  11. package/docs/examples/add-export-command/tasks.md +88 -0
  12. package/docs/examples/add-rest-api/design.md +855 -0
  13. package/docs/examples/add-rest-api/requirements.md +323 -0
  14. package/docs/examples/add-rest-api/tasks.md +355 -0
  15. package/docs/examples/add-user-dashboard/design.md +192 -0
  16. package/docs/examples/add-user-dashboard/requirements.md +143 -0
  17. package/docs/examples/add-user-dashboard/tasks.md +91 -0
  18. package/docs/faq.md +696 -0
  19. package/docs/integration-modes.md +525 -0
  20. package/docs/integration-philosophy.md +313 -0
  21. package/docs/manual-workflows-guide.md +417 -0
  22. package/docs/quick-start-with-ai-tools.md +374 -0
  23. package/docs/quick-start.md +711 -0
  24. package/docs/spec-workflow.md +453 -0
  25. package/docs/steering-strategy-guide.md +196 -0
  26. package/docs/tools/claude-guide.md +653 -0
  27. package/docs/tools/cursor-guide.md +705 -0
  28. package/docs/tools/generic-guide.md +445 -0
  29. package/docs/tools/kiro-guide.md +308 -0
  30. package/docs/tools/vscode-guide.md +444 -0
  31. package/docs/tools/windsurf-guide.md +390 -0
  32. package/docs/troubleshooting.md +795 -0
  33. package/docs/zh/README.md +275 -0
  34. package/docs/zh/quick-start.md +711 -0
  35. package/docs/zh/tools/claude-guide.md +348 -0
  36. package/docs/zh/tools/cursor-guide.md +280 -0
  37. package/docs/zh/tools/generic-guide.md +498 -0
  38. package/docs/zh/tools/kiro-guide.md +342 -0
  39. package/docs/zh/tools/vscode-guide.md +448 -0
  40. package/docs/zh/tools/windsurf-guide.md +377 -0
  41. package/lib/adoption/detection-engine.js +14 -4
  42. package/lib/commands/adopt.js +117 -3
  43. package/lib/commands/context.js +99 -0
  44. package/lib/commands/prompt.js +105 -0
  45. package/lib/commands/status.js +225 -0
  46. package/lib/commands/task.js +199 -0
  47. package/lib/commands/watch.js +569 -0
  48. package/lib/commands/workflows.js +240 -0
  49. package/lib/commands/workspace.js +189 -0
  50. package/lib/context/context-exporter.js +378 -0
  51. package/lib/context/prompt-generator.js +482 -0
  52. package/lib/steering/adoption-config.js +164 -0
  53. package/lib/steering/steering-manager.js +289 -0
  54. package/lib/task/task-claimer.js +430 -0
  55. package/lib/utils/tool-detector.js +383 -0
  56. package/lib/watch/action-executor.js +458 -0
  57. package/lib/watch/event-debouncer.js +323 -0
  58. package/lib/watch/execution-logger.js +550 -0
  59. package/lib/watch/file-watcher.js +499 -0
  60. package/lib/watch/presets.js +266 -0
  61. package/lib/watch/watch-manager.js +533 -0
  62. package/lib/workspace/workspace-manager.js +370 -0
  63. package/lib/workspace/workspace-sync.js +356 -0
  64. package/package.json +3 -1
  65. package/template/.kiro/tools/backup_manager.py +295 -0
  66. package/template/.kiro/tools/configuration_manager.py +218 -0
  67. package/template/.kiro/tools/document_evaluator.py +550 -0
  68. package/template/.kiro/tools/enhancement_logger.py +168 -0
  69. package/template/.kiro/tools/error_handler.py +335 -0
  70. package/template/.kiro/tools/improvement_identifier.py +444 -0
  71. package/template/.kiro/tools/modification_applicator.py +737 -0
  72. package/template/.kiro/tools/quality_gate_enforcer.py +207 -0
  73. package/template/.kiro/tools/quality_scorer.py +305 -0
  74. package/template/.kiro/tools/report_generator.py +154 -0
  75. package/template/.kiro/tools/ultrawork_enhancer_refactored.py +0 -0
  76. package/template/.kiro/tools/ultrawork_enhancer_v2.py +463 -0
  77. package/template/.kiro/tools/ultrawork_enhancer_v3.py +606 -0
  78. package/template/.kiro/tools/workflow_quality_gate.py +100 -0
@@ -0,0 +1,370 @@
1
+ const fs = require('fs-extra');
2
+ const path = require('path');
3
+ const os = require('os');
4
+ const { execSync } = require('child_process');
5
+
6
+ /**
7
+ * WorkspaceManager - 管理个人工作区
8
+ *
9
+ * 为多用户协作提供隔离的个人工作区
10
+ */
11
+ class WorkspaceManager {
12
+ constructor() {
13
+ this.workspaceBaseDir = '.kiro/workspace';
14
+ this.gitignoreContent = '# Personal workspaces - not committed\n*\n!.gitignore\n';
15
+ }
16
+
17
+ /**
18
+ * 初始化个人工作区
19
+ *
20
+ * @param {string} projectPath - 项目根目录路径
21
+ * @param {string} username - 用户名(可选,自动检测)
22
+ * @returns {Promise<Object>} 初始化结果
23
+ */
24
+ async initWorkspace(projectPath, username = null) {
25
+ try {
26
+ // 检测用户名
27
+ const detectedUsername = username || await this.detectUsername();
28
+
29
+ if (!detectedUsername) {
30
+ return {
31
+ success: false,
32
+ error: 'Could not detect username. Please provide username explicitly.',
33
+ username: null
34
+ };
35
+ }
36
+
37
+ // 创建工作区目录
38
+ const workspacePath = path.join(projectPath, this.workspaceBaseDir, detectedUsername);
39
+ await fs.ensureDir(workspacePath);
40
+
41
+ // 创建 CURRENT_CONTEXT.md
42
+ const contextPath = path.join(workspacePath, 'CURRENT_CONTEXT.md');
43
+ if (!await fs.pathExists(contextPath)) {
44
+ const contextTemplate = this.generateContextTemplate(detectedUsername);
45
+ await fs.writeFile(contextPath, contextTemplate, 'utf8');
46
+ }
47
+
48
+ // 创建 task-state.json
49
+ const taskStatePath = path.join(workspacePath, 'task-state.json');
50
+ if (!await fs.pathExists(taskStatePath)) {
51
+ const initialState = {
52
+ username: detectedUsername,
53
+ createdAt: new Date().toISOString(),
54
+ lastSyncAt: null,
55
+ currentSpec: null,
56
+ taskState: {}
57
+ };
58
+ await fs.writeFile(taskStatePath, JSON.stringify(initialState, null, 2), 'utf8');
59
+ }
60
+
61
+ // 创建 sync.log
62
+ const syncLogPath = path.join(workspacePath, 'sync.log');
63
+ if (!await fs.pathExists(syncLogPath)) {
64
+ await fs.writeFile(syncLogPath, '', 'utf8');
65
+ }
66
+
67
+ // 确保 .gitignore 存在
68
+ await this.ensureGitignore(projectPath);
69
+
70
+ return {
71
+ success: true,
72
+ username: detectedUsername,
73
+ workspacePath,
74
+ filesCreated: ['CURRENT_CONTEXT.md', 'task-state.json', 'sync.log']
75
+ };
76
+ } catch (error) {
77
+ return {
78
+ success: false,
79
+ error: error.message,
80
+ username: username
81
+ };
82
+ }
83
+ }
84
+
85
+ /**
86
+ * 检测当前用户名
87
+ *
88
+ * 优先级:git config > 环境变量 > 系统用户名
89
+ *
90
+ * @returns {Promise<string|null>} 用户名或 null
91
+ */
92
+ async detectUsername() {
93
+ try {
94
+ // 1. 尝试从 git config 获取
95
+ try {
96
+ const gitUsername = execSync('git config user.name', {
97
+ encoding: 'utf8',
98
+ stdio: ['pipe', 'pipe', 'ignore']
99
+ }).trim();
100
+
101
+ if (gitUsername) {
102
+ return gitUsername;
103
+ }
104
+ } catch (error) {
105
+ // Git 不可用或未配置,继续尝试其他方法
106
+ }
107
+
108
+ // 2. 尝试从环境变量获取
109
+ const envUsername = process.env.USER || process.env.USERNAME;
110
+ if (envUsername) {
111
+ return envUsername;
112
+ }
113
+
114
+ // 3. 尝试从系统获取
115
+ const osUsername = os.userInfo().username;
116
+ if (osUsername) {
117
+ return osUsername;
118
+ }
119
+
120
+ return null;
121
+ } catch (error) {
122
+ return null;
123
+ }
124
+ }
125
+
126
+ /**
127
+ * 获取当前用户的工作区路径
128
+ *
129
+ * @param {string} projectPath - 项目根目录路径
130
+ * @param {string} username - 用户名(可选,自动检测)
131
+ * @returns {Promise<string|null>} 工作区路径或 null
132
+ */
133
+ async getWorkspacePath(projectPath, username = null) {
134
+ const detectedUsername = username || await this.detectUsername();
135
+
136
+ if (!detectedUsername) {
137
+ return null;
138
+ }
139
+
140
+ const workspacePath = path.join(projectPath, this.workspaceBaseDir, detectedUsername);
141
+ const exists = await fs.pathExists(workspacePath);
142
+
143
+ return exists ? workspacePath : null;
144
+ }
145
+
146
+ /**
147
+ * 检查是否启用多用户模式
148
+ *
149
+ * @param {string} projectPath - 项目根目录路径
150
+ * @returns {Promise<boolean>} 是否启用多用户模式
151
+ */
152
+ async isMultiUserMode(projectPath) {
153
+ const workspaceBasePath = path.join(projectPath, this.workspaceBaseDir);
154
+
155
+ // 检查 workspace 目录是否存在
156
+ const exists = await fs.pathExists(workspaceBasePath);
157
+ if (!exists) {
158
+ return false;
159
+ }
160
+
161
+ // 检查是否有用户子目录
162
+ try {
163
+ const entries = await fs.readdir(workspaceBasePath, { withFileTypes: true });
164
+ const userDirs = entries.filter(entry =>
165
+ entry.isDirectory() && !entry.name.startsWith('.')
166
+ );
167
+
168
+ return userDirs.length > 0;
169
+ } catch (error) {
170
+ return false;
171
+ }
172
+ }
173
+
174
+ /**
175
+ * 列出所有用户工作区
176
+ *
177
+ * @param {string} projectPath - 项目根目录路径
178
+ * @returns {Promise<Array<string>>} 用户名列表
179
+ */
180
+ async listWorkspaces(projectPath) {
181
+ const workspaceBasePath = path.join(projectPath, this.workspaceBaseDir);
182
+
183
+ const exists = await fs.pathExists(workspaceBasePath);
184
+ if (!exists) {
185
+ return [];
186
+ }
187
+
188
+ try {
189
+ const entries = await fs.readdir(workspaceBasePath, { withFileTypes: true });
190
+ const usernames = entries
191
+ .filter(entry => entry.isDirectory() && !entry.name.startsWith('.'))
192
+ .map(entry => entry.name);
193
+
194
+ return usernames;
195
+ } catch (error) {
196
+ return [];
197
+ }
198
+ }
199
+
200
+ /**
201
+ * 确保 workspace .gitignore 存在
202
+ *
203
+ * @param {string} projectPath - 项目根目录路径
204
+ * @returns {Promise<boolean>} 是否成功
205
+ */
206
+ async ensureGitignore(projectPath) {
207
+ const workspaceBasePath = path.join(projectPath, this.workspaceBaseDir);
208
+ const gitignorePath = path.join(workspaceBasePath, '.gitignore');
209
+
210
+ try {
211
+ await fs.ensureDir(workspaceBasePath);
212
+
213
+ const exists = await fs.pathExists(gitignorePath);
214
+ if (!exists) {
215
+ await fs.writeFile(gitignorePath, this.gitignoreContent, 'utf8');
216
+ }
217
+
218
+ return true;
219
+ } catch (error) {
220
+ return false;
221
+ }
222
+ }
223
+
224
+ /**
225
+ * 读取工作区状态
226
+ *
227
+ * @param {string} projectPath - 项目根目录路径
228
+ * @param {string} username - 用户名(可选,自动检测)
229
+ * @returns {Promise<Object|null>} 工作区状态或 null
230
+ */
231
+ async readWorkspaceState(projectPath, username = null) {
232
+ const workspacePath = await this.getWorkspacePath(projectPath, username);
233
+
234
+ if (!workspacePath) {
235
+ return null;
236
+ }
237
+
238
+ const taskStatePath = path.join(workspacePath, 'task-state.json');
239
+
240
+ try {
241
+ const exists = await fs.pathExists(taskStatePath);
242
+ if (!exists) {
243
+ return null;
244
+ }
245
+
246
+ const content = await fs.readFile(taskStatePath, 'utf8');
247
+ return JSON.parse(content);
248
+ } catch (error) {
249
+ return null;
250
+ }
251
+ }
252
+
253
+ /**
254
+ * 写入工作区状态
255
+ *
256
+ * @param {string} projectPath - 项目根目录路径
257
+ * @param {Object} state - 工作区状态
258
+ * @param {string} username - 用户名(可选,自动检测)
259
+ * @returns {Promise<boolean>} 是否成功
260
+ */
261
+ async writeWorkspaceState(projectPath, state, username = null) {
262
+ const workspacePath = await this.getWorkspacePath(projectPath, username);
263
+
264
+ if (!workspacePath) {
265
+ return false;
266
+ }
267
+
268
+ const taskStatePath = path.join(workspacePath, 'task-state.json');
269
+
270
+ try {
271
+ await fs.writeFile(taskStatePath, JSON.stringify(state, null, 2), 'utf8');
272
+ return true;
273
+ } catch (error) {
274
+ return false;
275
+ }
276
+ }
277
+
278
+ /**
279
+ * 生成 CURRENT_CONTEXT.md 模板
280
+ *
281
+ * @param {string} username - 用户名
282
+ * @returns {string} 模板内容
283
+ */
284
+ generateContextTemplate(username) {
285
+ return `# Personal Context - ${username}
286
+
287
+ > **Note**: This is your personal context file. It is not shared with other team members.
288
+
289
+ ---
290
+
291
+ ## 🎯 Current Status
292
+
293
+ **Status**: 🔥 Active
294
+ **Current Spec**: None
295
+ **Last Updated**: ${new Date().toISOString().split('T')[0]}
296
+
297
+ ---
298
+
299
+ ## 📝 Current Work
300
+
301
+ **What I'm working on**:
302
+ - (Add your current tasks here)
303
+
304
+ **Next Steps**:
305
+ - (Add your next steps here)
306
+
307
+ ---
308
+
309
+ ## 💡 Notes
310
+
311
+ **Important Information**:
312
+ - (Add important notes here)
313
+
314
+ **Blockers**:
315
+ - (Add any blockers here)
316
+
317
+ ---
318
+
319
+ ## 📋 Task Tracking
320
+
321
+ **Claimed Tasks**:
322
+ - (Tasks you've claimed will appear here)
323
+
324
+ **Completed Tasks**:
325
+ - (Completed tasks will appear here)
326
+
327
+ ---
328
+
329
+ **Version**: 1.0
330
+ **Created**: ${new Date().toISOString()}
331
+ **Owner**: ${username}
332
+ `;
333
+ }
334
+
335
+ /**
336
+ * 获取工作区信息
337
+ *
338
+ * @param {string} projectPath - 项目根目录路径
339
+ * @param {string} username - 用户名(可选,自动检测)
340
+ * @returns {Promise<Object|null>} 工作区信息或 null
341
+ */
342
+ async getWorkspaceInfo(projectPath, username = null) {
343
+ const detectedUsername = username || await this.detectUsername();
344
+
345
+ if (!detectedUsername) {
346
+ return null;
347
+ }
348
+
349
+ const workspacePath = await this.getWorkspacePath(projectPath, detectedUsername);
350
+
351
+ if (!workspacePath) {
352
+ return {
353
+ exists: false,
354
+ username: detectedUsername,
355
+ path: null
356
+ };
357
+ }
358
+
359
+ const state = await this.readWorkspaceState(projectPath, detectedUsername);
360
+
361
+ return {
362
+ exists: true,
363
+ username: detectedUsername,
364
+ path: workspacePath,
365
+ state
366
+ };
367
+ }
368
+ }
369
+
370
+ module.exports = WorkspaceManager;
@@ -0,0 +1,356 @@
1
+ const fs = require('fs-extra');
2
+ const path = require('path');
3
+ const TaskClaimer = require('../task/task-claimer');
4
+ const WorkspaceManager = require('./workspace-manager');
5
+
6
+ /**
7
+ * WorkspaceSync - 工作区同步
8
+ *
9
+ * 协调个人工作区状态和共享任务状态
10
+ */
11
+ class WorkspaceSync {
12
+ constructor() {
13
+ this.taskClaimer = new TaskClaimer();
14
+ this.workspaceManager = new WorkspaceManager();
15
+ }
16
+
17
+ /**
18
+ * 同步工作区
19
+ *
20
+ * @param {string} projectPath - 项目根目录路径
21
+ * @param {string} username - 用户名(可选,自动检测)
22
+ * @returns {Promise<Object>} 同步结果
23
+ */
24
+ async syncWorkspace(projectPath, username = null) {
25
+ try {
26
+ // 检测用户名
27
+ const detectedUsername = username || await this.workspaceManager.detectUsername();
28
+
29
+ if (!detectedUsername) {
30
+ return {
31
+ success: false,
32
+ error: 'Could not detect username'
33
+ };
34
+ }
35
+
36
+ // 检查工作区是否存在
37
+ const workspacePath = await this.workspaceManager.getWorkspacePath(projectPath, detectedUsername);
38
+
39
+ if (!workspacePath) {
40
+ return {
41
+ success: false,
42
+ error: 'Workspace not initialized. Run: kse workspace init'
43
+ };
44
+ }
45
+
46
+ // 读取个人工作区状态
47
+ const workspaceState = await this.workspaceManager.readWorkspaceState(projectPath, detectedUsername);
48
+
49
+ if (!workspaceState) {
50
+ return {
51
+ success: false,
52
+ error: 'Could not read workspace state'
53
+ };
54
+ }
55
+
56
+ // 获取所有 Spec
57
+ const specsPath = path.join(projectPath, '.kiro/specs');
58
+ const specsExist = await fs.pathExists(specsPath);
59
+
60
+ if (!specsExist) {
61
+ return {
62
+ success: true,
63
+ username: detectedUsername,
64
+ conflicts: [],
65
+ synced: 0,
66
+ message: 'No specs to sync'
67
+ };
68
+ }
69
+
70
+ const entries = await fs.readdir(specsPath, { withFileTypes: true });
71
+ const specDirs = entries.filter(entry => entry.isDirectory() && !entry.name.startsWith('.'));
72
+
73
+ // 同步每个 Spec
74
+ const conflicts = [];
75
+ let syncedCount = 0;
76
+
77
+ for (const specDir of specDirs) {
78
+ const specName = specDir.name;
79
+ const syncResult = await this.syncSpec(projectPath, specName, detectedUsername, workspaceState);
80
+
81
+ if (syncResult.conflicts.length > 0) {
82
+ conflicts.push(...syncResult.conflicts);
83
+ }
84
+
85
+ if (syncResult.synced) {
86
+ syncedCount++;
87
+ }
88
+ }
89
+
90
+ // 更新同步时间
91
+ workspaceState.lastSyncAt = new Date().toISOString();
92
+ await this.workspaceManager.writeWorkspaceState(projectPath, workspaceState, detectedUsername);
93
+
94
+ // 记录同步日志
95
+ await this.logSync(projectPath, detectedUsername, {
96
+ timestamp: new Date().toISOString(),
97
+ specs: syncedCount,
98
+ conflicts: conflicts.length,
99
+ success: conflicts.length === 0
100
+ });
101
+
102
+ return {
103
+ success: true,
104
+ username: detectedUsername,
105
+ conflicts,
106
+ synced: syncedCount,
107
+ message: conflicts.length > 0
108
+ ? `Synced with ${conflicts.length} conflict(s)`
109
+ : `Synced ${syncedCount} spec(s) successfully`
110
+ };
111
+ } catch (error) {
112
+ return {
113
+ success: false,
114
+ error: error.message
115
+ };
116
+ }
117
+ }
118
+
119
+ /**
120
+ * 同步单个 Spec
121
+ *
122
+ * @param {string} projectPath - 项目根目录路径
123
+ * @param {string} specName - Spec 名称
124
+ * @param {string} username - 用户名
125
+ * @param {Object} workspaceState - 工作区状态
126
+ * @returns {Promise<Object>} 同步结果
127
+ */
128
+ async syncSpec(projectPath, specName, username, workspaceState) {
129
+ const conflicts = [];
130
+ let synced = false;
131
+
132
+ try {
133
+ // 读取共享任务状态
134
+ const tasksPath = path.join(projectPath, '.kiro/specs', specName, 'tasks.md');
135
+ const tasksExist = await fs.pathExists(tasksPath);
136
+
137
+ if (!tasksExist) {
138
+ return { conflicts, synced: false };
139
+ }
140
+
141
+ const tasks = await this.taskClaimer.parseTasks(tasksPath);
142
+
143
+ // 获取个人任务状态
144
+ const personalTaskState = workspaceState.taskState[specName] || {};
145
+
146
+ // 检测冲突
147
+ for (const task of tasks) {
148
+ const personalState = personalTaskState[task.taskId];
149
+
150
+ if (!personalState) {
151
+ continue;
152
+ }
153
+
154
+ // 检查状态冲突
155
+ if (personalState.status !== task.status) {
156
+ // 如果任务被当前用户认领,个人状态优先
157
+ if (task.claimedBy === username) {
158
+ // 更新共享状态为个人状态
159
+ await this.taskClaimer.updateTaskStatus(
160
+ projectPath,
161
+ specName,
162
+ task.taskId,
163
+ personalState.status
164
+ );
165
+ synced = true;
166
+ } else {
167
+ // 冲突:任务被其他人认领或状态不一致
168
+ conflicts.push({
169
+ specName,
170
+ taskId: task.taskId,
171
+ taskTitle: task.title,
172
+ personalStatus: personalState.status,
173
+ sharedStatus: task.status,
174
+ claimedBy: task.claimedBy,
175
+ type: task.claimedBy ? 'claimed-by-other' : 'status-mismatch'
176
+ });
177
+ }
178
+ }
179
+ }
180
+
181
+ // 更新个人状态为共享状态(对于未认领的任务)
182
+ for (const task of tasks) {
183
+ if (!task.claimedBy || task.claimedBy !== username) {
184
+ if (!workspaceState.taskState[specName]) {
185
+ workspaceState.taskState[specName] = {};
186
+ }
187
+
188
+ workspaceState.taskState[specName][task.taskId] = {
189
+ status: task.status,
190
+ claimedAt: task.claimedAt,
191
+ completedAt: task.status === 'completed' ? new Date().toISOString() : null
192
+ };
193
+ }
194
+ }
195
+
196
+ return { conflicts, synced };
197
+ } catch (error) {
198
+ return { conflicts, synced: false };
199
+ }
200
+ }
201
+
202
+ /**
203
+ * 解决冲突
204
+ *
205
+ * @param {string} projectPath - 项目根目录路径
206
+ * @param {Object} conflict - 冲突对象
207
+ * @param {string} resolution - 解决策略 ('keep-local' | 'keep-remote' | 'merge')
208
+ * @param {string} username - 用户名
209
+ * @returns {Promise<Object>} 解决结果
210
+ */
211
+ async resolveConflict(projectPath, conflict, resolution, username) {
212
+ try {
213
+ const { specName, taskId, personalStatus, sharedStatus } = conflict;
214
+
215
+ if (resolution === 'keep-local') {
216
+ // 使用个人状态更新共享状态
217
+ const result = await this.taskClaimer.updateTaskStatus(
218
+ projectPath,
219
+ specName,
220
+ taskId,
221
+ personalStatus
222
+ );
223
+
224
+ return {
225
+ success: result.success,
226
+ resolution: 'keep-local',
227
+ finalStatus: personalStatus
228
+ };
229
+ } else if (resolution === 'keep-remote') {
230
+ // 使用共享状态更新个人状态
231
+ const workspaceState = await this.workspaceManager.readWorkspaceState(projectPath, username);
232
+
233
+ if (!workspaceState.taskState[specName]) {
234
+ workspaceState.taskState[specName] = {};
235
+ }
236
+
237
+ workspaceState.taskState[specName][taskId] = {
238
+ status: sharedStatus,
239
+ claimedAt: null,
240
+ completedAt: sharedStatus === 'completed' ? new Date().toISOString() : null
241
+ };
242
+
243
+ await this.workspaceManager.writeWorkspaceState(projectPath, workspaceState, username);
244
+
245
+ return {
246
+ success: true,
247
+ resolution: 'keep-remote',
248
+ finalStatus: sharedStatus
249
+ };
250
+ } else if (resolution === 'merge') {
251
+ // 合并策略:如果任一状态为 completed,使用 completed
252
+ const finalStatus = personalStatus === 'completed' || sharedStatus === 'completed'
253
+ ? 'completed'
254
+ : personalStatus === 'in-progress' || sharedStatus === 'in-progress'
255
+ ? 'in-progress'
256
+ : 'not-started';
257
+
258
+ // 更新共享状态
259
+ await this.taskClaimer.updateTaskStatus(projectPath, specName, taskId, finalStatus);
260
+
261
+ // 更新个人状态
262
+ const workspaceState = await this.workspaceManager.readWorkspaceState(projectPath, username);
263
+
264
+ if (!workspaceState.taskState[specName]) {
265
+ workspaceState.taskState[specName] = {};
266
+ }
267
+
268
+ workspaceState.taskState[specName][taskId] = {
269
+ status: finalStatus,
270
+ claimedAt: null,
271
+ completedAt: finalStatus === 'completed' ? new Date().toISOString() : null
272
+ };
273
+
274
+ await this.workspaceManager.writeWorkspaceState(projectPath, workspaceState, username);
275
+
276
+ return {
277
+ success: true,
278
+ resolution: 'merge',
279
+ finalStatus
280
+ };
281
+ }
282
+
283
+ return {
284
+ success: false,
285
+ error: 'Invalid resolution strategy'
286
+ };
287
+ } catch (error) {
288
+ return {
289
+ success: false,
290
+ error: error.message
291
+ };
292
+ }
293
+ }
294
+
295
+ /**
296
+ * 记录同步日志
297
+ *
298
+ * @param {string} projectPath - 项目根目录路径
299
+ * @param {string} username - 用户名
300
+ * @param {Object} syncInfo - 同步信息
301
+ * @returns {Promise<boolean>} 是否成功
302
+ */
303
+ async logSync(projectPath, username, syncInfo) {
304
+ try {
305
+ const workspacePath = await this.workspaceManager.getWorkspacePath(projectPath, username);
306
+
307
+ if (!workspacePath) {
308
+ return false;
309
+ }
310
+
311
+ const logPath = path.join(workspacePath, 'sync.log');
312
+ const logEntry = `[${syncInfo.timestamp}] Synced ${syncInfo.specs} spec(s), ${syncInfo.conflicts} conflict(s), success: ${syncInfo.success}\n`;
313
+
314
+ await fs.appendFile(logPath, logEntry, 'utf8');
315
+
316
+ return true;
317
+ } catch (error) {
318
+ return false;
319
+ }
320
+ }
321
+
322
+ /**
323
+ * 读取同步日志
324
+ *
325
+ * @param {string} projectPath - 项目根目录路径
326
+ * @param {string} username - 用户名
327
+ * @param {number} lines - 读取行数(默认 10)
328
+ * @returns {Promise<Array>} 日志条目
329
+ */
330
+ async readSyncLog(projectPath, username, lines = 10) {
331
+ try {
332
+ const workspacePath = await this.workspaceManager.getWorkspacePath(projectPath, username);
333
+
334
+ if (!workspacePath) {
335
+ return [];
336
+ }
337
+
338
+ const logPath = path.join(workspacePath, 'sync.log');
339
+ const exists = await fs.pathExists(logPath);
340
+
341
+ if (!exists) {
342
+ return [];
343
+ }
344
+
345
+ const content = await fs.readFile(logPath, 'utf8');
346
+ const allLines = content.trim().split('\n').filter(line => line);
347
+
348
+ // 返回最后 N 行
349
+ return allLines.slice(-lines);
350
+ } catch (error) {
351
+ return [];
352
+ }
353
+ }
354
+ }
355
+
356
+ module.exports = WorkspaceSync;