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.
- package/CHANGELOG.md +135 -0
- package/README.md +239 -213
- package/README.zh.md +0 -330
- package/bin/kiro-spec-engine.js +62 -0
- package/docs/README.md +223 -0
- package/docs/agent-hooks-analysis.md +815 -0
- package/docs/command-reference.md +252 -0
- package/docs/cross-tool-guide.md +554 -0
- package/docs/examples/add-export-command/design.md +194 -0
- package/docs/examples/add-export-command/requirements.md +110 -0
- package/docs/examples/add-export-command/tasks.md +88 -0
- package/docs/examples/add-rest-api/design.md +855 -0
- package/docs/examples/add-rest-api/requirements.md +323 -0
- package/docs/examples/add-rest-api/tasks.md +355 -0
- package/docs/examples/add-user-dashboard/design.md +192 -0
- package/docs/examples/add-user-dashboard/requirements.md +143 -0
- package/docs/examples/add-user-dashboard/tasks.md +91 -0
- package/docs/faq.md +696 -0
- package/docs/integration-modes.md +525 -0
- package/docs/integration-philosophy.md +313 -0
- package/docs/manual-workflows-guide.md +417 -0
- package/docs/quick-start-with-ai-tools.md +374 -0
- package/docs/quick-start.md +711 -0
- package/docs/spec-workflow.md +453 -0
- package/docs/steering-strategy-guide.md +196 -0
- package/docs/tools/claude-guide.md +653 -0
- package/docs/tools/cursor-guide.md +705 -0
- package/docs/tools/generic-guide.md +445 -0
- package/docs/tools/kiro-guide.md +308 -0
- package/docs/tools/vscode-guide.md +444 -0
- package/docs/tools/windsurf-guide.md +390 -0
- package/docs/troubleshooting.md +795 -0
- package/docs/zh/README.md +275 -0
- package/docs/zh/quick-start.md +711 -0
- package/docs/zh/tools/claude-guide.md +348 -0
- package/docs/zh/tools/cursor-guide.md +280 -0
- package/docs/zh/tools/generic-guide.md +498 -0
- package/docs/zh/tools/kiro-guide.md +342 -0
- package/docs/zh/tools/vscode-guide.md +448 -0
- package/docs/zh/tools/windsurf-guide.md +377 -0
- package/lib/adoption/detection-engine.js +14 -4
- package/lib/commands/adopt.js +117 -3
- package/lib/commands/context.js +99 -0
- package/lib/commands/prompt.js +105 -0
- package/lib/commands/status.js +225 -0
- package/lib/commands/task.js +199 -0
- package/lib/commands/watch.js +569 -0
- package/lib/commands/workflows.js +240 -0
- package/lib/commands/workspace.js +189 -0
- package/lib/context/context-exporter.js +378 -0
- package/lib/context/prompt-generator.js +482 -0
- package/lib/steering/adoption-config.js +164 -0
- package/lib/steering/steering-manager.js +289 -0
- package/lib/task/task-claimer.js +430 -0
- package/lib/utils/tool-detector.js +383 -0
- package/lib/watch/action-executor.js +458 -0
- package/lib/watch/event-debouncer.js +323 -0
- package/lib/watch/execution-logger.js +550 -0
- package/lib/watch/file-watcher.js +499 -0
- package/lib/watch/presets.js +266 -0
- package/lib/watch/watch-manager.js +533 -0
- package/lib/workspace/workspace-manager.js +370 -0
- package/lib/workspace/workspace-sync.js +356 -0
- package/package.json +3 -1
- package/template/.kiro/tools/backup_manager.py +295 -0
- package/template/.kiro/tools/configuration_manager.py +218 -0
- package/template/.kiro/tools/document_evaluator.py +550 -0
- package/template/.kiro/tools/enhancement_logger.py +168 -0
- package/template/.kiro/tools/error_handler.py +335 -0
- package/template/.kiro/tools/improvement_identifier.py +444 -0
- package/template/.kiro/tools/modification_applicator.py +737 -0
- package/template/.kiro/tools/quality_gate_enforcer.py +207 -0
- package/template/.kiro/tools/quality_scorer.py +305 -0
- package/template/.kiro/tools/report_generator.py +154 -0
- package/template/.kiro/tools/ultrawork_enhancer_refactored.py +0 -0
- package/template/.kiro/tools/ultrawork_enhancer_v2.py +463 -0
- package/template/.kiro/tools/ultrawork_enhancer_v3.py +606 -0
- package/template/.kiro/tools/workflow_quality_gate.py +100 -0
|
@@ -0,0 +1,289 @@
|
|
|
1
|
+
const fs = require('fs-extra');
|
|
2
|
+
const path = require('path');
|
|
3
|
+
const inquirer = require('inquirer');
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* SteeringManager - 管理 Steering 文件的独占使用
|
|
7
|
+
*
|
|
8
|
+
* 负责检测、备份、安装和恢复 steering 文件
|
|
9
|
+
*/
|
|
10
|
+
class SteeringManager {
|
|
11
|
+
constructor() {
|
|
12
|
+
this.steeringDir = '.kiro/steering';
|
|
13
|
+
this.backupBaseDir = '.kiro/backups';
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* 检测项目中的 steering 文件
|
|
18
|
+
*
|
|
19
|
+
* @param {string} projectPath - 项目根目录路径
|
|
20
|
+
* @returns {Promise<Object>} 检测结果
|
|
21
|
+
*/
|
|
22
|
+
async detectSteering(projectPath) {
|
|
23
|
+
const steeringPath = path.join(projectPath, this.steeringDir);
|
|
24
|
+
|
|
25
|
+
// 检查 steering 目录是否存在
|
|
26
|
+
const exists = await fs.pathExists(steeringPath);
|
|
27
|
+
|
|
28
|
+
if (!exists) {
|
|
29
|
+
return {
|
|
30
|
+
hasExistingSteering: false,
|
|
31
|
+
files: [],
|
|
32
|
+
path: steeringPath
|
|
33
|
+
};
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
// 读取 steering 目录中的文件
|
|
37
|
+
const files = await fs.readdir(steeringPath);
|
|
38
|
+
|
|
39
|
+
// 过滤出 .md 文件
|
|
40
|
+
const mdFiles = files.filter(f => f.endsWith('.md'));
|
|
41
|
+
|
|
42
|
+
// 获取文件详细信息
|
|
43
|
+
const fileDetails = await Promise.all(
|
|
44
|
+
mdFiles.map(async (file) => {
|
|
45
|
+
const filePath = path.join(steeringPath, file);
|
|
46
|
+
const stats = await fs.stat(filePath);
|
|
47
|
+
return {
|
|
48
|
+
name: file,
|
|
49
|
+
path: filePath,
|
|
50
|
+
size: stats.size,
|
|
51
|
+
modified: stats.mtime
|
|
52
|
+
};
|
|
53
|
+
})
|
|
54
|
+
);
|
|
55
|
+
|
|
56
|
+
return {
|
|
57
|
+
hasExistingSteering: mdFiles.length > 0,
|
|
58
|
+
files: fileDetails,
|
|
59
|
+
path: steeringPath,
|
|
60
|
+
count: mdFiles.length
|
|
61
|
+
};
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* 提示用户选择 steering 策略
|
|
66
|
+
*
|
|
67
|
+
* @param {Object} detection - detectSteering 的返回结果
|
|
68
|
+
* @returns {Promise<string>} 选择的策略 ('use-kse' | 'use-project')
|
|
69
|
+
*/
|
|
70
|
+
async promptStrategy(detection) {
|
|
71
|
+
if (!detection.hasExistingSteering) {
|
|
72
|
+
// 没有现有 steering 文件,默认使用 kse
|
|
73
|
+
return 'use-kse';
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
console.log('\n⚠️ Steering Conflict Detected');
|
|
77
|
+
console.log('━'.repeat(60));
|
|
78
|
+
console.log(`Found ${detection.count} existing steering file(s) in ${this.steeringDir}:`);
|
|
79
|
+
console.log('');
|
|
80
|
+
|
|
81
|
+
detection.files.forEach(file => {
|
|
82
|
+
console.log(` • ${file.name} (${(file.size / 1024).toFixed(1)} KB)`);
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
console.log('');
|
|
86
|
+
console.log('Kiro IDE loads all files in .kiro/steering/, which means you must');
|
|
87
|
+
console.log('choose between kse steering rules OR your project\'s existing rules.');
|
|
88
|
+
console.log('');
|
|
89
|
+
|
|
90
|
+
const response = await inquirer.prompt([{
|
|
91
|
+
type: 'list',
|
|
92
|
+
name: 'strategy',
|
|
93
|
+
message: 'How would you like to proceed?',
|
|
94
|
+
choices: [
|
|
95
|
+
{
|
|
96
|
+
name: 'Use kse steering (backup existing files) - Recommended for new kse users',
|
|
97
|
+
value: 'use-kse'
|
|
98
|
+
},
|
|
99
|
+
{
|
|
100
|
+
name: 'Keep existing steering (skip kse steering) - For projects with custom steering rules',
|
|
101
|
+
value: 'use-project'
|
|
102
|
+
}
|
|
103
|
+
]
|
|
104
|
+
}]);
|
|
105
|
+
|
|
106
|
+
return response.strategy;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* 备份现有的 steering 文件
|
|
111
|
+
*
|
|
112
|
+
* @param {string} projectPath - 项目根目录路径
|
|
113
|
+
* @returns {Promise<Object>} 备份结果
|
|
114
|
+
*/
|
|
115
|
+
async backupSteering(projectPath) {
|
|
116
|
+
const steeringPath = path.join(projectPath, this.steeringDir);
|
|
117
|
+
const timestamp = new Date().toISOString().replace(/[:.]/g, '-');
|
|
118
|
+
const backupId = `steering-${timestamp}`;
|
|
119
|
+
const backupPath = path.join(projectPath, this.backupBaseDir, backupId);
|
|
120
|
+
|
|
121
|
+
// 检查 steering 目录是否存在
|
|
122
|
+
const exists = await fs.pathExists(steeringPath);
|
|
123
|
+
if (!exists) {
|
|
124
|
+
return {
|
|
125
|
+
success: false,
|
|
126
|
+
error: 'Steering directory does not exist',
|
|
127
|
+
backupId: null,
|
|
128
|
+
backupPath: null
|
|
129
|
+
};
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
try {
|
|
133
|
+
// 创建备份目录
|
|
134
|
+
await fs.ensureDir(backupPath);
|
|
135
|
+
|
|
136
|
+
// 复制所有文件
|
|
137
|
+
await fs.copy(steeringPath, backupPath, {
|
|
138
|
+
overwrite: false,
|
|
139
|
+
errorOnExist: false
|
|
140
|
+
});
|
|
141
|
+
|
|
142
|
+
// 验证备份
|
|
143
|
+
const backupFiles = await fs.readdir(backupPath);
|
|
144
|
+
const originalFiles = await fs.readdir(steeringPath);
|
|
145
|
+
|
|
146
|
+
return {
|
|
147
|
+
success: true,
|
|
148
|
+
backupId,
|
|
149
|
+
backupPath,
|
|
150
|
+
filesBackedUp: backupFiles.length,
|
|
151
|
+
timestamp,
|
|
152
|
+
verified: backupFiles.length === originalFiles.length
|
|
153
|
+
};
|
|
154
|
+
} catch (error) {
|
|
155
|
+
return {
|
|
156
|
+
success: false,
|
|
157
|
+
error: error.message,
|
|
158
|
+
backupId,
|
|
159
|
+
backupPath
|
|
160
|
+
};
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
/**
|
|
165
|
+
* 安装 kse steering 文件
|
|
166
|
+
*
|
|
167
|
+
* @param {string} projectPath - 项目根目录路径
|
|
168
|
+
* @returns {Promise<Object>} 安装结果
|
|
169
|
+
*/
|
|
170
|
+
async installKseSteering(projectPath) {
|
|
171
|
+
const steeringPath = path.join(projectPath, this.steeringDir);
|
|
172
|
+
const templatePath = path.join(__dirname, '../../template/.kiro/steering');
|
|
173
|
+
|
|
174
|
+
try {
|
|
175
|
+
// 确保 steering 目录存在
|
|
176
|
+
await fs.ensureDir(steeringPath);
|
|
177
|
+
|
|
178
|
+
// 检查模板目录是否存在
|
|
179
|
+
const templateExists = await fs.pathExists(templatePath);
|
|
180
|
+
if (!templateExists) {
|
|
181
|
+
return {
|
|
182
|
+
success: false,
|
|
183
|
+
error: 'kse steering template directory not found',
|
|
184
|
+
filesInstalled: 0
|
|
185
|
+
};
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
// 复制模板文件
|
|
189
|
+
const templateFiles = await fs.readdir(templatePath);
|
|
190
|
+
const mdFiles = templateFiles.filter(f => f.endsWith('.md'));
|
|
191
|
+
|
|
192
|
+
let installedCount = 0;
|
|
193
|
+
for (const file of mdFiles) {
|
|
194
|
+
const srcPath = path.join(templatePath, file);
|
|
195
|
+
const destPath = path.join(steeringPath, file);
|
|
196
|
+
|
|
197
|
+
await fs.copy(srcPath, destPath, {
|
|
198
|
+
overwrite: true
|
|
199
|
+
});
|
|
200
|
+
installedCount++;
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
return {
|
|
204
|
+
success: true,
|
|
205
|
+
filesInstalled: installedCount,
|
|
206
|
+
files: mdFiles
|
|
207
|
+
};
|
|
208
|
+
} catch (error) {
|
|
209
|
+
return {
|
|
210
|
+
success: false,
|
|
211
|
+
error: error.message,
|
|
212
|
+
filesInstalled: 0
|
|
213
|
+
};
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
/**
|
|
218
|
+
* 从备份恢复 steering 文件
|
|
219
|
+
*
|
|
220
|
+
* @param {string} projectPath - 项目根目录路径
|
|
221
|
+
* @param {string} backupId - 备份 ID
|
|
222
|
+
* @returns {Promise<Object>} 恢复结果
|
|
223
|
+
*/
|
|
224
|
+
async restoreSteering(projectPath, backupId) {
|
|
225
|
+
const steeringPath = path.join(projectPath, this.steeringDir);
|
|
226
|
+
const backupPath = path.join(projectPath, this.backupBaseDir, backupId);
|
|
227
|
+
|
|
228
|
+
// 检查备份是否存在
|
|
229
|
+
const backupExists = await fs.pathExists(backupPath);
|
|
230
|
+
if (!backupExists) {
|
|
231
|
+
return {
|
|
232
|
+
success: false,
|
|
233
|
+
error: `Backup not found: ${backupId}`,
|
|
234
|
+
filesRestored: 0
|
|
235
|
+
};
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
try {
|
|
239
|
+
// 清空现有 steering 目录
|
|
240
|
+
await fs.emptyDir(steeringPath);
|
|
241
|
+
|
|
242
|
+
// 从备份恢复
|
|
243
|
+
await fs.copy(backupPath, steeringPath, {
|
|
244
|
+
overwrite: true
|
|
245
|
+
});
|
|
246
|
+
|
|
247
|
+
// 验证恢复
|
|
248
|
+
const restoredFiles = await fs.readdir(steeringPath);
|
|
249
|
+
|
|
250
|
+
return {
|
|
251
|
+
success: true,
|
|
252
|
+
filesRestored: restoredFiles.length,
|
|
253
|
+
backupId,
|
|
254
|
+
files: restoredFiles
|
|
255
|
+
};
|
|
256
|
+
} catch (error) {
|
|
257
|
+
return {
|
|
258
|
+
success: false,
|
|
259
|
+
error: error.message,
|
|
260
|
+
filesRestored: 0,
|
|
261
|
+
backupId
|
|
262
|
+
};
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
/**
|
|
267
|
+
* 列出所有可用的 steering 备份
|
|
268
|
+
*
|
|
269
|
+
* @param {string} projectPath - 项目根目录路径
|
|
270
|
+
* @returns {Promise<Array>} 备份列表
|
|
271
|
+
*/
|
|
272
|
+
async listBackups(projectPath) {
|
|
273
|
+
const backupBasePath = path.join(projectPath, this.backupBaseDir);
|
|
274
|
+
|
|
275
|
+
const exists = await fs.pathExists(backupBasePath);
|
|
276
|
+
if (!exists) {
|
|
277
|
+
return [];
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
const entries = await fs.readdir(backupBasePath, { withFileTypes: true });
|
|
281
|
+
const steeringBackups = entries
|
|
282
|
+
.filter(entry => entry.isDirectory() && entry.name.startsWith('steering-'))
|
|
283
|
+
.map(entry => entry.name);
|
|
284
|
+
|
|
285
|
+
return steeringBackups;
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
module.exports = SteeringManager;
|
|
@@ -0,0 +1,430 @@
|
|
|
1
|
+
const fs = require('fs-extra');
|
|
2
|
+
const path = require('path');
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* TaskClaimer - 任务认领管理
|
|
6
|
+
*
|
|
7
|
+
* 管理任务的认领、释放和状态跟踪
|
|
8
|
+
*/
|
|
9
|
+
class TaskClaimer {
|
|
10
|
+
constructor() {
|
|
11
|
+
this.taskFilePattern = /^-\s*\[([ x~-])\](\*)?\s+(.+)$/;
|
|
12
|
+
this.claimPattern = /\[@([^,]+),\s*claimed:\s*([^\]]+)\]/;
|
|
13
|
+
this.staleDays = 7;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* 解析 tasks.md 文件
|
|
18
|
+
*
|
|
19
|
+
* @param {string} tasksPath - tasks.md 文件路径
|
|
20
|
+
* @returns {Promise<Array>} 任务列表
|
|
21
|
+
*/
|
|
22
|
+
async parseTasks(tasksPath) {
|
|
23
|
+
try {
|
|
24
|
+
const content = await fs.readFile(tasksPath, 'utf8');
|
|
25
|
+
const lines = content.split('\n');
|
|
26
|
+
const tasks = [];
|
|
27
|
+
|
|
28
|
+
for (let i = 0; i < lines.length; i++) {
|
|
29
|
+
const line = lines[i];
|
|
30
|
+
const match = line.match(this.taskFilePattern);
|
|
31
|
+
|
|
32
|
+
if (match) {
|
|
33
|
+
const status = match[1];
|
|
34
|
+
const isOptional = match[2] === '*';
|
|
35
|
+
const taskContent = match[3];
|
|
36
|
+
|
|
37
|
+
// 提取任务 ID 和标题
|
|
38
|
+
const taskIdMatch = taskContent.match(/^(\d+(?:\.\d+)*)\s+(.+)$/);
|
|
39
|
+
|
|
40
|
+
if (taskIdMatch) {
|
|
41
|
+
const taskId = taskIdMatch[1];
|
|
42
|
+
const titleAndClaim = taskIdMatch[2];
|
|
43
|
+
|
|
44
|
+
// 检查是否有认领信息
|
|
45
|
+
const claimMatch = titleAndClaim.match(this.claimPattern);
|
|
46
|
+
|
|
47
|
+
let title = titleAndClaim;
|
|
48
|
+
let claimedBy = null;
|
|
49
|
+
let claimedAt = null;
|
|
50
|
+
|
|
51
|
+
if (claimMatch) {
|
|
52
|
+
title = titleAndClaim.substring(0, claimMatch.index).trim();
|
|
53
|
+
claimedBy = claimMatch[1];
|
|
54
|
+
claimedAt = claimMatch[2];
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
tasks.push({
|
|
58
|
+
lineNumber: i,
|
|
59
|
+
originalLine: line,
|
|
60
|
+
status: this.parseStatus(status),
|
|
61
|
+
isOptional,
|
|
62
|
+
taskId,
|
|
63
|
+
title,
|
|
64
|
+
claimedBy,
|
|
65
|
+
claimedAt,
|
|
66
|
+
isStale: claimedAt ? this.isStale(claimedAt) : false
|
|
67
|
+
});
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
return tasks;
|
|
73
|
+
} catch (error) {
|
|
74
|
+
throw new Error(`Failed to parse tasks: ${error.message}`);
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* 解析任务状态
|
|
80
|
+
*
|
|
81
|
+
* @param {string} statusChar - 状态字符
|
|
82
|
+
* @returns {string} 状态
|
|
83
|
+
*/
|
|
84
|
+
parseStatus(statusChar) {
|
|
85
|
+
switch (statusChar) {
|
|
86
|
+
case ' ':
|
|
87
|
+
return 'not-started';
|
|
88
|
+
case 'x':
|
|
89
|
+
return 'completed';
|
|
90
|
+
case '-':
|
|
91
|
+
return 'in-progress';
|
|
92
|
+
case '~':
|
|
93
|
+
return 'queued';
|
|
94
|
+
default:
|
|
95
|
+
return 'unknown';
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* 状态转换为字符
|
|
101
|
+
*
|
|
102
|
+
* @param {string} status - 状态
|
|
103
|
+
* @returns {string} 状态字符
|
|
104
|
+
*/
|
|
105
|
+
statusToChar(status) {
|
|
106
|
+
switch (status) {
|
|
107
|
+
case 'not-started':
|
|
108
|
+
return ' ';
|
|
109
|
+
case 'completed':
|
|
110
|
+
return 'x';
|
|
111
|
+
case 'in-progress':
|
|
112
|
+
return '-';
|
|
113
|
+
case 'queued':
|
|
114
|
+
return '~';
|
|
115
|
+
default:
|
|
116
|
+
return ' ';
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/**
|
|
121
|
+
* 检查认领是否过期
|
|
122
|
+
*
|
|
123
|
+
* @param {string} claimedAt - 认领时间(ISO 字符串)
|
|
124
|
+
* @returns {boolean} 是否过期
|
|
125
|
+
*/
|
|
126
|
+
isStale(claimedAt) {
|
|
127
|
+
try {
|
|
128
|
+
const claimDate = new Date(claimedAt);
|
|
129
|
+
const now = new Date();
|
|
130
|
+
const daysDiff = (now - claimDate) / (1000 * 60 * 60 * 24);
|
|
131
|
+
return daysDiff > this.staleDays;
|
|
132
|
+
} catch (error) {
|
|
133
|
+
return false;
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
/**
|
|
138
|
+
* 认领任务
|
|
139
|
+
*
|
|
140
|
+
* @param {string} projectPath - 项目根目录路径
|
|
141
|
+
* @param {string} specName - Spec 名称
|
|
142
|
+
* @param {string} taskId - 任务 ID
|
|
143
|
+
* @param {string} username - 用户名
|
|
144
|
+
* @param {boolean} force - 是否强制认领
|
|
145
|
+
* @returns {Promise<Object>} 认领结果
|
|
146
|
+
*/
|
|
147
|
+
async claimTask(projectPath, specName, taskId, username, force = false) {
|
|
148
|
+
const tasksPath = path.join(projectPath, '.kiro/specs', specName, 'tasks.md');
|
|
149
|
+
|
|
150
|
+
try {
|
|
151
|
+
// 检查文件是否存在
|
|
152
|
+
const exists = await fs.pathExists(tasksPath);
|
|
153
|
+
if (!exists) {
|
|
154
|
+
return {
|
|
155
|
+
success: false,
|
|
156
|
+
error: `tasks.md not found for spec: ${specName}`
|
|
157
|
+
};
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
// 解析任务
|
|
161
|
+
const tasks = await this.parseTasks(tasksPath);
|
|
162
|
+
const task = tasks.find(t => t.taskId === taskId);
|
|
163
|
+
|
|
164
|
+
if (!task) {
|
|
165
|
+
return {
|
|
166
|
+
success: false,
|
|
167
|
+
error: `Task not found: ${taskId}`
|
|
168
|
+
};
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
// 检查是否已被认领
|
|
172
|
+
if (task.claimedBy && task.claimedBy !== username && !force) {
|
|
173
|
+
return {
|
|
174
|
+
success: false,
|
|
175
|
+
error: `Task already claimed by ${task.claimedBy}`,
|
|
176
|
+
needsForce: true,
|
|
177
|
+
currentClaim: {
|
|
178
|
+
username: task.claimedBy,
|
|
179
|
+
claimedAt: task.claimedAt,
|
|
180
|
+
isStale: task.isStale
|
|
181
|
+
}
|
|
182
|
+
};
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
// 读取文件内容
|
|
186
|
+
const content = await fs.readFile(tasksPath, 'utf8');
|
|
187
|
+
const lines = content.split('\n');
|
|
188
|
+
|
|
189
|
+
// 构建新的任务行
|
|
190
|
+
const claimTimestamp = new Date().toISOString();
|
|
191
|
+
const statusChar = this.statusToChar('in-progress');
|
|
192
|
+
const optionalMarker = task.isOptional ? '*' : '';
|
|
193
|
+
const newLine = `- [${statusChar}]${optionalMarker} ${taskId} ${task.title} [@${username}, claimed: ${claimTimestamp}]`;
|
|
194
|
+
|
|
195
|
+
// 替换任务行
|
|
196
|
+
lines[task.lineNumber] = newLine;
|
|
197
|
+
|
|
198
|
+
// 写回文件
|
|
199
|
+
await fs.writeFile(tasksPath, lines.join('\n'), 'utf8');
|
|
200
|
+
|
|
201
|
+
return {
|
|
202
|
+
success: true,
|
|
203
|
+
taskId,
|
|
204
|
+
username,
|
|
205
|
+
claimedAt: claimTimestamp,
|
|
206
|
+
previousClaim: task.claimedBy ? {
|
|
207
|
+
username: task.claimedBy,
|
|
208
|
+
claimedAt: task.claimedAt
|
|
209
|
+
} : null
|
|
210
|
+
};
|
|
211
|
+
} catch (error) {
|
|
212
|
+
return {
|
|
213
|
+
success: false,
|
|
214
|
+
error: error.message
|
|
215
|
+
};
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
/**
|
|
220
|
+
* 释放任务认领
|
|
221
|
+
*
|
|
222
|
+
* @param {string} projectPath - 项目根目录路径
|
|
223
|
+
* @param {string} specName - Spec 名称
|
|
224
|
+
* @param {string} taskId - 任务 ID
|
|
225
|
+
* @param {string} username - 用户名
|
|
226
|
+
* @returns {Promise<Object>} 释放结果
|
|
227
|
+
*/
|
|
228
|
+
async unclaimTask(projectPath, specName, taskId, username) {
|
|
229
|
+
const tasksPath = path.join(projectPath, '.kiro/specs', specName, 'tasks.md');
|
|
230
|
+
|
|
231
|
+
try {
|
|
232
|
+
// 检查文件是否存在
|
|
233
|
+
const exists = await fs.pathExists(tasksPath);
|
|
234
|
+
if (!exists) {
|
|
235
|
+
return {
|
|
236
|
+
success: false,
|
|
237
|
+
error: `tasks.md not found for spec: ${specName}`
|
|
238
|
+
};
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
// 解析任务
|
|
242
|
+
const tasks = await this.parseTasks(tasksPath);
|
|
243
|
+
const task = tasks.find(t => t.taskId === taskId);
|
|
244
|
+
|
|
245
|
+
if (!task) {
|
|
246
|
+
return {
|
|
247
|
+
success: false,
|
|
248
|
+
error: `Task not found: ${taskId}`
|
|
249
|
+
};
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
// 检查是否被当前用户认领
|
|
253
|
+
if (!task.claimedBy) {
|
|
254
|
+
return {
|
|
255
|
+
success: false,
|
|
256
|
+
error: 'Task is not claimed'
|
|
257
|
+
};
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
if (task.claimedBy !== username) {
|
|
261
|
+
return {
|
|
262
|
+
success: false,
|
|
263
|
+
error: `Task is claimed by ${task.claimedBy}, not ${username}`
|
|
264
|
+
};
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
// 读取文件内容
|
|
268
|
+
const content = await fs.readFile(tasksPath, 'utf8');
|
|
269
|
+
const lines = content.split('\n');
|
|
270
|
+
|
|
271
|
+
// 构建新的任务行(移除认领信息,重置状态)
|
|
272
|
+
const statusChar = this.statusToChar('not-started');
|
|
273
|
+
const optionalMarker = task.isOptional ? '*' : '';
|
|
274
|
+
const newLine = `- [${statusChar}]${optionalMarker} ${taskId} ${task.title}`;
|
|
275
|
+
|
|
276
|
+
// 替换任务行
|
|
277
|
+
lines[task.lineNumber] = newLine;
|
|
278
|
+
|
|
279
|
+
// 写回文件
|
|
280
|
+
await fs.writeFile(tasksPath, lines.join('\n'), 'utf8');
|
|
281
|
+
|
|
282
|
+
return {
|
|
283
|
+
success: true,
|
|
284
|
+
taskId,
|
|
285
|
+
username,
|
|
286
|
+
unclaimedAt: new Date().toISOString()
|
|
287
|
+
};
|
|
288
|
+
} catch (error) {
|
|
289
|
+
return {
|
|
290
|
+
success: false,
|
|
291
|
+
error: error.message
|
|
292
|
+
};
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
/**
|
|
297
|
+
* 获取已认领的任务列表
|
|
298
|
+
*
|
|
299
|
+
* @param {string} projectPath - 项目根目录路径
|
|
300
|
+
* @param {string} specName - Spec 名称
|
|
301
|
+
* @returns {Promise<Array>} 已认领的任务列表
|
|
302
|
+
*/
|
|
303
|
+
async getClaimedTasks(projectPath, specName) {
|
|
304
|
+
const tasksPath = path.join(projectPath, '.kiro/specs', specName, 'tasks.md');
|
|
305
|
+
|
|
306
|
+
try {
|
|
307
|
+
const exists = await fs.pathExists(tasksPath);
|
|
308
|
+
if (!exists) {
|
|
309
|
+
return [];
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
const tasks = await this.parseTasks(tasksPath);
|
|
313
|
+
|
|
314
|
+
return tasks
|
|
315
|
+
.filter(task => task.claimedBy)
|
|
316
|
+
.map(task => ({
|
|
317
|
+
specName,
|
|
318
|
+
taskId: task.taskId,
|
|
319
|
+
taskTitle: task.title,
|
|
320
|
+
claimedBy: task.claimedBy,
|
|
321
|
+
claimedAt: task.claimedAt,
|
|
322
|
+
status: task.status,
|
|
323
|
+
isStale: task.isStale,
|
|
324
|
+
isOptional: task.isOptional
|
|
325
|
+
}));
|
|
326
|
+
} catch (error) {
|
|
327
|
+
return [];
|
|
328
|
+
}
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
/**
|
|
332
|
+
* 更新任务状态
|
|
333
|
+
*
|
|
334
|
+
* @param {string} projectPath - 项目根目录路径
|
|
335
|
+
* @param {string} specName - Spec 名称
|
|
336
|
+
* @param {string} taskId - 任务 ID
|
|
337
|
+
* @param {string} status - 新状态
|
|
338
|
+
* @returns {Promise<Object>} 更新结果
|
|
339
|
+
*/
|
|
340
|
+
async updateTaskStatus(projectPath, specName, taskId, status) {
|
|
341
|
+
const tasksPath = path.join(projectPath, '.kiro/specs', specName, 'tasks.md');
|
|
342
|
+
|
|
343
|
+
try {
|
|
344
|
+
const exists = await fs.pathExists(tasksPath);
|
|
345
|
+
if (!exists) {
|
|
346
|
+
return {
|
|
347
|
+
success: false,
|
|
348
|
+
error: `tasks.md not found for spec: ${specName}`
|
|
349
|
+
};
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
const tasks = await this.parseTasks(tasksPath);
|
|
353
|
+
const task = tasks.find(t => t.taskId === taskId);
|
|
354
|
+
|
|
355
|
+
if (!task) {
|
|
356
|
+
return {
|
|
357
|
+
success: false,
|
|
358
|
+
error: `Task not found: ${taskId}`
|
|
359
|
+
};
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
// 读取文件内容
|
|
363
|
+
const content = await fs.readFile(tasksPath, 'utf8');
|
|
364
|
+
const lines = content.split('\n');
|
|
365
|
+
|
|
366
|
+
// 构建新的任务行
|
|
367
|
+
const statusChar = this.statusToChar(status);
|
|
368
|
+
const optionalMarker = task.isOptional ? '*' : '';
|
|
369
|
+
|
|
370
|
+
let newLine = `- [${statusChar}]${optionalMarker} ${taskId} ${task.title}`;
|
|
371
|
+
|
|
372
|
+
// 保留认领信息(如果有)
|
|
373
|
+
if (task.claimedBy) {
|
|
374
|
+
newLine += ` [@${task.claimedBy}, claimed: ${task.claimedAt}]`;
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
// 替换任务行
|
|
378
|
+
lines[task.lineNumber] = newLine;
|
|
379
|
+
|
|
380
|
+
// 写回文件
|
|
381
|
+
await fs.writeFile(tasksPath, lines.join('\n'), 'utf8');
|
|
382
|
+
|
|
383
|
+
return {
|
|
384
|
+
success: true,
|
|
385
|
+
taskId,
|
|
386
|
+
oldStatus: task.status,
|
|
387
|
+
newStatus: status
|
|
388
|
+
};
|
|
389
|
+
} catch (error) {
|
|
390
|
+
return {
|
|
391
|
+
success: false,
|
|
392
|
+
error: error.message
|
|
393
|
+
};
|
|
394
|
+
}
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
/**
|
|
398
|
+
* 获取所有 Spec 的已认领任务
|
|
399
|
+
*
|
|
400
|
+
* @param {string} projectPath - 项目根目录路径
|
|
401
|
+
* @returns {Promise<Array>} 所有已认领的任务
|
|
402
|
+
*/
|
|
403
|
+
async getAllClaimedTasks(projectPath) {
|
|
404
|
+
const specsPath = path.join(projectPath, '.kiro/specs');
|
|
405
|
+
|
|
406
|
+
try {
|
|
407
|
+
const exists = await fs.pathExists(specsPath);
|
|
408
|
+
if (!exists) {
|
|
409
|
+
return [];
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
const entries = await fs.readdir(specsPath, { withFileTypes: true });
|
|
413
|
+
const specDirs = entries.filter(entry => entry.isDirectory());
|
|
414
|
+
|
|
415
|
+
const allClaimedTasks = [];
|
|
416
|
+
|
|
417
|
+
for (const specDir of specDirs) {
|
|
418
|
+
const specName = specDir.name;
|
|
419
|
+
const claimedTasks = await this.getClaimedTasks(projectPath, specName);
|
|
420
|
+
allClaimedTasks.push(...claimedTasks);
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
return allClaimedTasks;
|
|
424
|
+
} catch (error) {
|
|
425
|
+
return [];
|
|
426
|
+
}
|
|
427
|
+
}
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
module.exports = TaskClaimer;
|