claude-coder 1.9.2 → 1.10.1
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/README.md +166 -141
- package/bin/cli.js +19 -4
- package/package.json +2 -2
- package/src/common/assets.js +66 -52
- package/src/common/config.js +22 -0
- package/src/common/sdk.js +1 -3
- package/src/common/utils.js +3 -1
- package/src/core/coding.js +3 -1
- package/src/core/design.js +268 -0
- package/src/core/go.js +3 -3
- package/src/core/hooks.js +30 -16
- package/src/core/init.js +9 -0
- package/src/core/plan.js +21 -15
- package/src/core/prompts.js +47 -2
- package/src/core/repair.js +1 -1
- package/src/core/runner.js +84 -117
- package/src/core/scan.js +4 -3
- package/src/core/session.js +30 -16
- package/src/core/simplify.js +4 -2
- package/src/core/state.js +23 -8
- package/src/index.js +4 -0
- package/templates/{codingUser.md → coding/user.md} +1 -0
- package/templates/design/base.md +103 -0
- package/templates/design/fixSystem.md +71 -0
- package/templates/design/fixUser.md +3 -0
- package/templates/design/init.md +304 -0
- package/templates/design/system.md +108 -0
- package/templates/design/user.md +11 -0
- package/templates/{coreProtocol.md → other/coreProtocol.md} +1 -0
- package/templates/{test_rule.md → other/test_rule.md} +0 -2
- package/templates/{planUser.md → plan/user.md} +2 -1
- /package/templates/{codingSystem.md → coding/system.md} +0 -0
- /package/templates/{goSystem.md → go/system.md} +0 -0
- /package/templates/{bash-process.md → other/bash-process.md} +0 -0
- /package/templates/{guidance.json → other/guidance.json} +0 -0
- /package/templates/{requirements.example.md → other/requirements.example.md} +0 -0
- /package/templates/{web-testing.md → other/web-testing.md} +0 -0
- /package/templates/{planSystem.md → plan/system.md} +0 -0
- /package/templates/{scanSystem.md → scan/system.md} +0 -0
- /package/templates/{scanUser.md → scan/user.md} +0 -0
package/src/core/runner.js
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
const { execSync } = require('child_process');
|
|
4
4
|
const readline = require('readline');
|
|
5
|
-
const { log } = require('../common/config');
|
|
5
|
+
const { log, COLOR, printModeBanner } = require('../common/config');
|
|
6
6
|
const { assets } = require('../common/assets');
|
|
7
7
|
const { loadTasks, saveTasks, getFeatures, getStats, printStats } = require('../common/tasks');
|
|
8
8
|
const { getGitHead, sleep, tryPush, killServices } = require('../common/utils');
|
|
@@ -16,24 +16,29 @@ const MAX_RETRY = RETRY.MAX_ATTEMPTS;
|
|
|
16
16
|
|
|
17
17
|
// ─── Display Helpers ──────────────────────────────────────
|
|
18
18
|
|
|
19
|
-
function printBanner(dryRun) {
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
console.log(` Claude Coder${dryRun ? ' (预览模式)' : ''}`);
|
|
23
|
-
console.log('============================================');
|
|
24
|
-
console.log('');
|
|
19
|
+
function printBanner(dryRun, config, maxSessions) {
|
|
20
|
+
const mode = dryRun ? '预览模式' : `max: ${maxSessions}`;
|
|
21
|
+
printModeBanner('run', mode, config?.model);
|
|
25
22
|
}
|
|
26
23
|
|
|
27
|
-
function
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
console.log('--------------------------------------------');
|
|
24
|
+
function _progressBar(done, total, width = 24) {
|
|
25
|
+
if (total === 0) return `${COLOR.dim}[${'░'.repeat(width)}]${COLOR.reset}`;
|
|
26
|
+
const filled = Math.round(done / total * width);
|
|
27
|
+
return `${COLOR.green}[${'█'.repeat(filled)}${COLOR.dim}${'░'.repeat(width - filled)}${COLOR.reset}${COLOR.green}]${COLOR.reset}`;
|
|
32
28
|
}
|
|
33
29
|
|
|
34
|
-
function
|
|
30
|
+
function printSessionHeader(session, maxSessions, taskData, taskId) {
|
|
35
31
|
const stats = getStats(taskData);
|
|
36
|
-
|
|
32
|
+
const task = taskId ? getFeatures(taskData).find(f => f.id === taskId) : null;
|
|
33
|
+
const bar = _progressBar(stats.done, stats.total);
|
|
34
|
+
|
|
35
|
+
console.error('');
|
|
36
|
+
console.error(`${COLOR.cyan}┌─ Session ${session} / ${maxSessions} ${'─'.repeat(32)}┐${COLOR.reset}`);
|
|
37
|
+
if (task) {
|
|
38
|
+
console.error(`${COLOR.cyan}│${COLOR.reset} 任务: ${COLOR.bold}${task.id}${COLOR.reset} ${COLOR.dim}-${COLOR.reset} ${task.description || ''}`);
|
|
39
|
+
}
|
|
40
|
+
console.error(`${COLOR.cyan}│${COLOR.reset} 进度: ${bar} ${stats.done}/${stats.total} ${COLOR.green}✔${stats.done}${COLOR.reset} ${COLOR.yellow}○${stats.pending}${COLOR.reset} ${COLOR.red}✘${stats.failed}${COLOR.reset}`);
|
|
41
|
+
console.error(`${COLOR.cyan}└${'─'.repeat(46)}┘${COLOR.reset}`);
|
|
37
42
|
}
|
|
38
43
|
|
|
39
44
|
function printDryRun(taskData) {
|
|
@@ -51,16 +56,17 @@ function printDryRun(taskData) {
|
|
|
51
56
|
for (const f of features) {
|
|
52
57
|
const st = f.status || 'unknown';
|
|
53
58
|
const icon = { done: '✓', in_progress: '▸', pending: '○', failed: '✗', testing: '◇' }[st] || '?';
|
|
54
|
-
|
|
59
|
+
const color = { done: COLOR.green, failed: COLOR.red, in_progress: COLOR.blue, testing: COLOR.yellow, pending: COLOR.dim }[st] || '';
|
|
60
|
+
log('info', ` ${color}${icon}${COLOR.reset} [${st.padEnd(11)}] ${f.id} - ${f.description || ''}`);
|
|
55
61
|
}
|
|
56
62
|
}
|
|
57
63
|
|
|
58
64
|
function printEndBanner() {
|
|
59
|
-
console.
|
|
60
|
-
console.
|
|
61
|
-
console.
|
|
62
|
-
console.
|
|
63
|
-
console.
|
|
65
|
+
console.error('');
|
|
66
|
+
console.error(`${COLOR.cyan}╔══════════════════════════════════════════════╗${COLOR.reset}`);
|
|
67
|
+
console.error(`${COLOR.cyan}║${COLOR.reset} ${COLOR.bold}运行结束${COLOR.reset}`);
|
|
68
|
+
console.error(`${COLOR.cyan}╚══════════════════════════════════════════════╝${COLOR.reset}`);
|
|
69
|
+
console.error('');
|
|
64
70
|
}
|
|
65
71
|
|
|
66
72
|
async function promptContinue() {
|
|
@@ -94,74 +100,35 @@ function snapshot(projectRoot, taskData) {
|
|
|
94
100
|
|
|
95
101
|
function _validateSessionResult() {
|
|
96
102
|
if (!assets.exists('sessionResult')) {
|
|
97
|
-
log('error', 'Agent 未生成 session_result.json');
|
|
98
103
|
return { valid: false, reason: 'session_result.json 不存在' };
|
|
99
104
|
}
|
|
100
|
-
|
|
101
105
|
const raw = assets.readJson('sessionResult', null);
|
|
102
|
-
if (raw
|
|
103
|
-
log('warn', 'session_result.json 解析失败');
|
|
106
|
+
if (!raw) {
|
|
104
107
|
return { valid: false, reason: 'JSON 解析失败', rawContent: assets.read('sessionResult') };
|
|
105
108
|
}
|
|
106
|
-
|
|
107
109
|
const data = raw.current && typeof raw.current === 'object' ? raw.current : raw;
|
|
108
|
-
const { TASK_STATUSES } = require('../common/constants');
|
|
109
|
-
|
|
110
|
-
const required = ['session_result', 'status_after'];
|
|
111
|
-
const missing = required.filter(k => !(k in data));
|
|
112
|
-
if (missing.length > 0) {
|
|
113
|
-
log('warn', `session_result.json 缺少字段: ${missing.join(', ')}`);
|
|
114
|
-
return { valid: false, reason: `缺少字段: ${missing.join(', ')}`, data };
|
|
115
|
-
}
|
|
116
|
-
|
|
117
110
|
if (!['success', 'failed'].includes(data.session_result)) {
|
|
118
111
|
return { valid: false, reason: `无效 session_result: ${data.session_result}`, data };
|
|
119
112
|
}
|
|
120
|
-
|
|
121
|
-
if (!TASK_STATUSES.includes(data.status_after)) {
|
|
122
|
-
return { valid: false, reason: `无效 status_after: ${data.status_after}`, data };
|
|
123
|
-
}
|
|
124
|
-
|
|
125
|
-
const level = data.session_result === 'success' ? 'ok' : 'warn';
|
|
126
|
-
log(level, `session_result.json 合法 (${data.session_result})`);
|
|
113
|
+
log(data.session_result === 'success' ? 'ok' : 'warn', `session_result: ${data.session_result}`);
|
|
127
114
|
return { valid: true, data };
|
|
128
115
|
}
|
|
129
116
|
|
|
130
|
-
function _checkGitProgress(headBefore, projectRoot) {
|
|
131
|
-
if (!headBefore) {
|
|
132
|
-
log('info', '未提供 head_before,跳过 git 检查');
|
|
133
|
-
return { hasCommit: false, warning: false };
|
|
134
|
-
}
|
|
135
|
-
|
|
136
|
-
const headAfter = getGitHead(projectRoot);
|
|
137
|
-
|
|
138
|
-
if (headBefore === headAfter) {
|
|
139
|
-
log('warn', '本次会话没有新的 git 提交');
|
|
140
|
-
return { hasCommit: false, warning: true };
|
|
141
|
-
}
|
|
142
|
-
|
|
143
|
-
try {
|
|
144
|
-
const msg = execSync('git log --oneline -1', { cwd: projectRoot, encoding: 'utf8' }).trim();
|
|
145
|
-
log('ok', `检测到新提交: ${msg}`);
|
|
146
|
-
} catch { /* ignore */ }
|
|
147
|
-
|
|
148
|
-
return { hasCommit: true, warning: false };
|
|
149
|
-
}
|
|
150
|
-
|
|
151
|
-
function _inferFromTasks(taskId) {
|
|
152
|
-
if (!taskId) return null;
|
|
153
|
-
const data = loadTasks();
|
|
154
|
-
if (!data) return null;
|
|
155
|
-
const task = getFeatures(data).find(f => f.id === taskId);
|
|
156
|
-
return task ? task.status : null;
|
|
157
|
-
}
|
|
158
|
-
|
|
159
117
|
async function validate(config, headBefore, taskId) {
|
|
160
118
|
const projectRoot = assets.projectRoot;
|
|
161
|
-
log('info', '
|
|
119
|
+
log('info', '校验中...');
|
|
162
120
|
|
|
163
121
|
let srResult = _validateSessionResult();
|
|
164
|
-
const
|
|
122
|
+
const hasCommit = headBefore ? getGitHead(projectRoot) !== headBefore : false;
|
|
123
|
+
|
|
124
|
+
if (hasCommit) {
|
|
125
|
+
try {
|
|
126
|
+
const msg = execSync('git log --oneline -1', { cwd: projectRoot, encoding: 'utf8' }).trim();
|
|
127
|
+
log('ok', `检测到新提交: ${msg}`);
|
|
128
|
+
} catch { /* ignore */ }
|
|
129
|
+
} else if (headBefore) {
|
|
130
|
+
log('warn', '本次会话没有新的 git 提交');
|
|
131
|
+
}
|
|
165
132
|
|
|
166
133
|
if (!srResult.valid && srResult.rawContent) {
|
|
167
134
|
const srPath = assets.path('sessionResult');
|
|
@@ -173,35 +140,22 @@ async function validate(config, headBefore, taskId) {
|
|
|
173
140
|
}
|
|
174
141
|
|
|
175
142
|
let fatal = false;
|
|
176
|
-
let
|
|
177
|
-
|
|
178
|
-
if (srResult.valid) {
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
log('warn', 'session_result.json 异常,但有新提交,降级为警告(不回滚代码)');
|
|
187
|
-
}
|
|
188
|
-
hasWarnings = true;
|
|
189
|
-
} else {
|
|
190
|
-
log('error', '无新提交且 session_result.json 异常,视为致命');
|
|
191
|
-
fatal = true;
|
|
192
|
-
}
|
|
143
|
+
let reason = '';
|
|
144
|
+
|
|
145
|
+
if (srResult.valid && srResult.data.session_result === 'failed') {
|
|
146
|
+
fatal = true;
|
|
147
|
+
reason = 'AI 报告任务失败';
|
|
148
|
+
} else if (!srResult.valid && !hasCommit) {
|
|
149
|
+
fatal = true;
|
|
150
|
+
reason = srResult.reason || 'session_result.json 异常';
|
|
151
|
+
} else if (!srResult.valid && hasCommit) {
|
|
152
|
+
log('warn', 'session_result.json 异常,但有新提交,降级为警告');
|
|
193
153
|
}
|
|
194
154
|
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
} else if (hasWarnings) {
|
|
198
|
-
log('warn', '========== 校验通过 (有警告) ==========');
|
|
199
|
-
} else {
|
|
200
|
-
log('ok', '========== 校验全部通过 ==========');
|
|
201
|
-
}
|
|
155
|
+
log(fatal ? 'error' : (hasCommit ? 'ok' : 'warn'),
|
|
156
|
+
fatal ? `校验失败: ${reason}` : `校验通过${hasCommit ? ' ✓' : ' (无新提交)'}`);
|
|
202
157
|
|
|
203
|
-
|
|
204
|
-
return { fatal, hasWarnings, sessionData: srResult.data, reason };
|
|
158
|
+
return { fatal, hasCommit, sessionData: srResult.data, reason };
|
|
205
159
|
}
|
|
206
160
|
|
|
207
161
|
// ─── Lifecycle: Rollback ──────────────────────────────────
|
|
@@ -260,12 +214,16 @@ function _markTaskFailed(taskId) {
|
|
|
260
214
|
}
|
|
261
215
|
|
|
262
216
|
async function _handleRetryOrSkip(session, {
|
|
263
|
-
headBefore, taskId, sessionResult, consecutiveFailures, result, reason, lastFailMsg,
|
|
217
|
+
headBefore, taskId, sessionResult, consecutiveFailures, result, reason, lastFailMsg, skipRollback,
|
|
264
218
|
}) {
|
|
265
219
|
const newFailures = consecutiveFailures + 1;
|
|
266
220
|
const exceeded = newFailures >= MAX_RETRY;
|
|
267
221
|
|
|
268
|
-
|
|
222
|
+
if (skipRollback) {
|
|
223
|
+
log('info', '已有提交,跳过回滚');
|
|
224
|
+
} else {
|
|
225
|
+
await rollback(headBefore, reason);
|
|
226
|
+
}
|
|
269
227
|
|
|
270
228
|
if (exceeded) {
|
|
271
229
|
log('error', `连续失败 ${MAX_RETRY} 次,跳过当前任务`);
|
|
@@ -300,11 +258,12 @@ async function onSuccess(session, { taskId, sessionResult, validateResult }) {
|
|
|
300
258
|
|
|
301
259
|
async function onFailure(session, { headBefore, taskId, sessionResult, validateResult, consecutiveFailures }) {
|
|
302
260
|
const reason = validateResult.reason || '校验失败';
|
|
303
|
-
log('error', `Session ${session}
|
|
261
|
+
log('error', `Session ${session} 失败 (连续: ${consecutiveFailures + 1}/${MAX_RETRY})`);
|
|
304
262
|
return _handleRetryOrSkip(session, {
|
|
305
263
|
headBefore, taskId, sessionResult, consecutiveFailures,
|
|
306
264
|
result: 'fatal', reason,
|
|
307
|
-
lastFailMsg:
|
|
265
|
+
lastFailMsg: `上次失败: ${reason}${validateResult.hasCommit ? '' : ',代码已回滚'}`,
|
|
266
|
+
skipRollback: validateResult.hasCommit,
|
|
308
267
|
});
|
|
309
268
|
}
|
|
310
269
|
|
|
@@ -314,7 +273,7 @@ async function onStall(session, { headBefore, taskId, sessionResult, consecutive
|
|
|
314
273
|
const validateResult = await validate(config, headBefore, taskId);
|
|
315
274
|
|
|
316
275
|
if (!validateResult.fatal) {
|
|
317
|
-
log('ok',
|
|
276
|
+
log('ok', '停顿超时但任务已完成,按成功处理');
|
|
318
277
|
return onSuccess(session, { taskId, sessionResult, validateResult });
|
|
319
278
|
}
|
|
320
279
|
|
|
@@ -370,19 +329,15 @@ async function executeRun(config, opts = {}) {
|
|
|
370
329
|
const dryRun = opts.dryRun || false;
|
|
371
330
|
const maxSessions = opts.max || 50;
|
|
372
331
|
const pauseEvery = opts.pause ?? 0;
|
|
373
|
-
|
|
374
|
-
printBanner(dryRun);
|
|
332
|
+
printBanner(dryRun, config, maxSessions);
|
|
375
333
|
|
|
376
334
|
printStats();
|
|
377
335
|
|
|
378
336
|
log('info', `开始编码循环 (最多 ${maxSessions} 个会话) ...`);
|
|
379
|
-
console.log('');
|
|
380
337
|
|
|
381
338
|
let state = { consecutiveFailures: 0, lastFailReason: '' };
|
|
382
339
|
|
|
383
340
|
for (let session = 1; session <= maxSessions; session++) {
|
|
384
|
-
printSessionHeader(session, maxSessions);
|
|
385
|
-
|
|
386
341
|
let taskData = loadTasks();
|
|
387
342
|
if (!taskData) {
|
|
388
343
|
const tasksPath = assets.path('tasks');
|
|
@@ -404,21 +359,37 @@ async function executeRun(config, opts = {}) {
|
|
|
404
359
|
}
|
|
405
360
|
tryPush(projectRoot);
|
|
406
361
|
}
|
|
407
|
-
console.
|
|
362
|
+
console.error('');
|
|
408
363
|
log('ok', '所有任务已完成!');
|
|
409
364
|
printStats();
|
|
410
365
|
break;
|
|
411
366
|
}
|
|
412
367
|
|
|
413
|
-
|
|
368
|
+
const { headBefore, taskId } = dryRun ? { headBefore: null, taskId: null } : snapshot(projectRoot, taskData);
|
|
369
|
+
|
|
370
|
+
if (!dryRun && taskId === 'unknown') {
|
|
371
|
+
log('warn', '无可执行任务(剩余任务均已失败或依赖未满足),退出');
|
|
372
|
+
break;
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
const selectedTask = getFeatures(taskData).find(f => f.id === taskId);
|
|
376
|
+
if (!dryRun && selectedTask?.status === 'failed') {
|
|
377
|
+
const hasActionable = getFeatures(taskData).some(f =>
|
|
378
|
+
f.status === 'pending' || f.status === 'in_progress' || f.status === 'testing'
|
|
379
|
+
);
|
|
380
|
+
if (!hasActionable) {
|
|
381
|
+
log('warn', '所有可执行任务已完成或失败,退出(剩余 failed 任务需人工排查)');
|
|
382
|
+
break;
|
|
383
|
+
}
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
printSessionHeader(session, maxSessions, taskData, taskId);
|
|
414
387
|
|
|
415
388
|
if (dryRun) {
|
|
416
389
|
printDryRun(taskData);
|
|
417
390
|
break;
|
|
418
391
|
}
|
|
419
392
|
|
|
420
|
-
const { headBefore, taskId } = snapshot(projectRoot, taskData);
|
|
421
|
-
|
|
422
393
|
const { executeCoding } = require('./coding');
|
|
423
394
|
const sessionResult = await executeCoding(config, session, {
|
|
424
395
|
projectRoot,
|
|
@@ -426,29 +397,25 @@ async function executeRun(config, opts = {}) {
|
|
|
426
397
|
consecutiveFailures: state.consecutiveFailures,
|
|
427
398
|
maxSessions,
|
|
428
399
|
lastValidateLog: state.lastFailReason,
|
|
400
|
+
continue: true,
|
|
429
401
|
});
|
|
430
402
|
|
|
431
403
|
if (sessionResult.stalled) {
|
|
432
404
|
state = await onStall(session, { headBefore, taskId, sessionResult, config, ...state });
|
|
433
405
|
if (state.consecutiveFailures === 0) {
|
|
434
406
|
if (shouldSimplify(config)) await tryRunSimplify(config);
|
|
435
|
-
tryPush(projectRoot);
|
|
436
407
|
}
|
|
437
408
|
continue;
|
|
438
409
|
}
|
|
439
410
|
|
|
440
|
-
log('info', '开始 harness 校验 ...');
|
|
441
411
|
const validateResult = await validate(config, headBefore, taskId);
|
|
442
412
|
|
|
443
413
|
if (!validateResult.fatal) {
|
|
444
|
-
const level = validateResult.hasWarnings ? 'warn' : 'ok';
|
|
445
|
-
log(level, `Session ${session} 校验通过${validateResult.hasWarnings ? ' (有警告)' : ''}`);
|
|
446
414
|
state = await onSuccess(session, { taskId, sessionResult, validateResult });
|
|
447
415
|
|
|
448
416
|
if (shouldSimplify(config)) {
|
|
449
417
|
await tryRunSimplify(config);
|
|
450
418
|
}
|
|
451
|
-
tryPush(projectRoot);
|
|
452
419
|
} else {
|
|
453
420
|
state = await onFailure(session, { headBefore, taskId, sessionResult, validateResult, ...state });
|
|
454
421
|
}
|
package/src/core/scan.js
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
const fs = require('fs');
|
|
4
4
|
const path = require('path');
|
|
5
|
-
const { log } = require('../common/config');
|
|
5
|
+
const { log, printModeBanner } = require('../common/config');
|
|
6
6
|
const { assets } = require('../common/assets');
|
|
7
7
|
const { buildSystemPrompt, buildScanPrompt } = require('./prompts');
|
|
8
8
|
const { Session } = require('./session');
|
|
@@ -47,10 +47,11 @@ function validateProfile() {
|
|
|
47
47
|
async function executeScan(config, opts = {}) {
|
|
48
48
|
const maxAttempts = RETRY.SCAN_ATTEMPTS;
|
|
49
49
|
|
|
50
|
+
const projectType = hasCodeFiles(opts.projectRoot || assets.projectRoot) ? 'existing' : 'new';
|
|
51
|
+
printModeBanner('scan', projectType === 'existing' ? '已有项目' : '全新项目', config?.model);
|
|
52
|
+
|
|
50
53
|
for (let attempt = 1; attempt <= maxAttempts; attempt++) {
|
|
51
54
|
log('info', `初始化尝试 ${attempt} / ${maxAttempts} ...`);
|
|
52
|
-
|
|
53
|
-
const projectType = hasCodeFiles(opts.projectRoot || assets.projectRoot) ? 'existing' : 'new';
|
|
54
55
|
const dateStr = new Date().toISOString().slice(0, 10).replace(/-/g, '');
|
|
55
56
|
|
|
56
57
|
const result = await Session.run('scan', config, {
|
package/src/core/session.js
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
const fs = require('fs');
|
|
4
4
|
const path = require('path');
|
|
5
|
-
const { buildEnvVars, log } = require('../common/config');
|
|
5
|
+
const { buildEnvVars, log, COLOR } = require('../common/config');
|
|
6
6
|
const { Indicator } = require('../common/indicator');
|
|
7
7
|
const { logMessage: baseLogMessage, extractResult, writeSessionSeparator } = require('../common/logging');
|
|
8
8
|
const { createHooks } = require('./hooks');
|
|
@@ -12,7 +12,7 @@ const { assets } = require('../common/assets');
|
|
|
12
12
|
* @typedef {Object} SessionRunOptions
|
|
13
13
|
* @property {string} logFileName - 日志文件名
|
|
14
14
|
* @property {import('fs').WriteStream} [logStream] - 外部日志流(与 logFileName 二选一)
|
|
15
|
-
* @property {number} [sessionNum=
|
|
15
|
+
* @property {number} [sessionNum=1] - 会话编号
|
|
16
16
|
* @property {string} [label=''] - 会话标签
|
|
17
17
|
* @property {(session: Session) => Promise<Object>} execute - 执行回调,接收 session 实例
|
|
18
18
|
*/
|
|
@@ -72,7 +72,7 @@ class Session {
|
|
|
72
72
|
* @param {SessionRunOptions} options - 运行选项
|
|
73
73
|
* @returns {Promise<Object>} 包含 exitCode、logFile、stalled 以及 execute 返回值
|
|
74
74
|
*/
|
|
75
|
-
static async run(type, config, { logFileName, logStream, sessionNum =
|
|
75
|
+
static async run(type, config, { logFileName, logStream, sessionNum = 1, label = '', execute }) {
|
|
76
76
|
await Session.ensureSDK(config);
|
|
77
77
|
const session = new Session(type, config, { logFileName, logStream, sessionNum, label });
|
|
78
78
|
try {
|
|
@@ -99,7 +99,7 @@ class Session {
|
|
|
99
99
|
* @param {number} [options.sessionNum=0]
|
|
100
100
|
* @param {string} [options.label='']
|
|
101
101
|
*/
|
|
102
|
-
constructor(type, config, { logFileName, logStream, sessionNum =
|
|
102
|
+
constructor(type, config, { logFileName, logStream, sessionNum = 1, label = '' }) {
|
|
103
103
|
this.config = config;
|
|
104
104
|
this.type = type;
|
|
105
105
|
this.indicator = new Indicator();
|
|
@@ -148,7 +148,7 @@ class Session {
|
|
|
148
148
|
* 执行一次 SDK 查询,遍历消息流并收集结果
|
|
149
149
|
* @param {string} prompt - 用户提示
|
|
150
150
|
* @param {Object} queryOpts - SDK query 选项(通常来自 buildQueryOptions)
|
|
151
|
-
* @param {RunQueryOpts} [opts={}] - 额外选项(onMessage
|
|
151
|
+
* @param {RunQueryOpts} [opts={}] - 额外选项(onMessage 回调, continue, resume)
|
|
152
152
|
* @returns {Promise<QueryResult>}
|
|
153
153
|
*/
|
|
154
154
|
async runQuery(prompt, queryOpts, opts = {}) {
|
|
@@ -162,7 +162,10 @@ class Session {
|
|
|
162
162
|
|
|
163
163
|
const sdk = Session._sdk;
|
|
164
164
|
const messages = [];
|
|
165
|
-
const
|
|
165
|
+
const queryPayload = { prompt, options: queryOpts };
|
|
166
|
+
if (opts.continue) queryPayload.options.continue = true;
|
|
167
|
+
if (opts.resume) queryPayload.options.resume = opts.resume;
|
|
168
|
+
const querySession = sdk.query(queryPayload);
|
|
166
169
|
|
|
167
170
|
try {
|
|
168
171
|
for await (const message of querySession) {
|
|
@@ -191,21 +194,32 @@ class Session {
|
|
|
191
194
|
const cost = sdkResult?.total_cost_usd || null;
|
|
192
195
|
const usage = sdkResult?.usage || null;
|
|
193
196
|
const turns = sdkResult?.num_turns || null;
|
|
197
|
+
const sessionId = sdkResult?.session_id || null;
|
|
194
198
|
|
|
195
|
-
if (cost != null || turns != null) {
|
|
196
|
-
const
|
|
197
|
-
|
|
198
|
-
|
|
199
|
+
if (cost != null || turns != null || sessionId) {
|
|
200
|
+
const fmtTokens = (n) => n >= 10000 ? `${(n / 1000).toFixed(1)}k` : String(n);
|
|
201
|
+
const fmtCost = (c) => c >= 1 ? `$${c.toFixed(2)}` : `$${c.toFixed(4)}`;
|
|
202
|
+
|
|
203
|
+
const cliParts = [];
|
|
204
|
+
if (sessionId) cliParts.push(`${COLOR.dim}sid:${COLOR.reset} ${sessionId.slice(0, 8)}`);
|
|
205
|
+
if (turns != null) cliParts.push(`${COLOR.dim}turns:${COLOR.reset} ${turns}`);
|
|
206
|
+
if (cost != null) cliParts.push(`${COLOR.dim}cost:${COLOR.reset} ${COLOR.yellow}${fmtCost(cost)}${COLOR.reset}`);
|
|
199
207
|
if (usage) {
|
|
200
208
|
const inp = usage.input_tokens || 0;
|
|
201
209
|
const out = usage.output_tokens || 0;
|
|
202
|
-
|
|
210
|
+
cliParts.push(`${COLOR.dim}tokens:${COLOR.reset} ${fmtTokens(inp)}+${fmtTokens(out)}`);
|
|
203
211
|
}
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
212
|
+
console.error(`${COLOR.dim}─── session end ───${COLOR.reset}`);
|
|
213
|
+
log('info', `session 统计: ${cliParts.join(' ')}`);
|
|
214
|
+
|
|
215
|
+
// 日志文件保留完整精度
|
|
216
|
+
const logParts = [];
|
|
217
|
+
if (sessionId) logParts.push(`sid: ${sessionId}`);
|
|
218
|
+
if (turns != null) logParts.push(`turns: ${turns}`);
|
|
219
|
+
if (cost != null) logParts.push(`cost: $${cost}`);
|
|
220
|
+
if (usage) logParts.push(`tokens: ${usage.input_tokens || 0}+${usage.output_tokens || 0}`);
|
|
207
221
|
if (this.logStream?.writable) {
|
|
208
|
-
this.logStream.write(`[SESSION_INFO] ${
|
|
222
|
+
this.logStream.write(`[SESSION_INFO] ${logParts.join(', ')}\n`);
|
|
209
223
|
}
|
|
210
224
|
}
|
|
211
225
|
|
|
@@ -213,7 +227,7 @@ class Session {
|
|
|
213
227
|
messages,
|
|
214
228
|
success: sdkResult?.subtype === 'success',
|
|
215
229
|
subtype: sdkResult?.subtype || null,
|
|
216
|
-
cost, usage, turns,
|
|
230
|
+
cost, usage, turns, sessionId,
|
|
217
231
|
};
|
|
218
232
|
}
|
|
219
233
|
|
package/src/core/simplify.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
|
-
const { log } = require('../common/config');
|
|
3
|
+
const { log, printModeBanner } = require('../common/config');
|
|
4
4
|
const { assets } = require('../common/assets');
|
|
5
5
|
const { Session } = require('./session');
|
|
6
6
|
const { execSync } = require('child_process');
|
|
@@ -33,6 +33,8 @@ async function executeSimplify(config, focus = null, opts = {}) {
|
|
|
33
33
|
|
|
34
34
|
const { range, label } = getSmartDiffRange(projectRoot, n);
|
|
35
35
|
|
|
36
|
+
printModeBanner('simplify', label, config?.model);
|
|
37
|
+
|
|
36
38
|
let diff = '';
|
|
37
39
|
try {
|
|
38
40
|
diff = execSync(`git diff ${range}`, { cwd: projectRoot, encoding: 'utf8', maxBuffer: 50 * 1024 * 1024 });
|
|
@@ -59,7 +61,7 @@ async function executeSimplify(config, focus = null, opts = {}) {
|
|
|
59
61
|
const queryOpts = session.buildQueryOptions(opts);
|
|
60
62
|
queryOpts.disallowedTools = ['askUserQuestion'];
|
|
61
63
|
|
|
62
|
-
await session.runQuery(prompt, queryOpts);
|
|
64
|
+
await session.runQuery(prompt, queryOpts, { continue: true });
|
|
63
65
|
log('ok', '代码审查完成');
|
|
64
66
|
|
|
65
67
|
return {};
|
package/src/core/state.js
CHANGED
|
@@ -44,10 +44,7 @@ function syncAfterPlan() {
|
|
|
44
44
|
|
|
45
45
|
function selectNextTask(taskData) {
|
|
46
46
|
const features = getFeatures(taskData);
|
|
47
|
-
|
|
48
|
-
const failed = features.filter(f => f.status === 'failed')
|
|
49
|
-
.sort((a, b) => (a.priority || 999) - (b.priority || 999));
|
|
50
|
-
if (failed.length > 0) return failed[0];
|
|
47
|
+
const byPriority = (a, b) => (a.priority || 999) - (b.priority || 999);
|
|
51
48
|
|
|
52
49
|
const pending = features.filter(f => f.status === 'pending')
|
|
53
50
|
.filter(f => {
|
|
@@ -57,12 +54,15 @@ function selectNextTask(taskData) {
|
|
|
57
54
|
return dep && dep.status === 'done';
|
|
58
55
|
});
|
|
59
56
|
})
|
|
60
|
-
.sort(
|
|
57
|
+
.sort(byPriority);
|
|
61
58
|
if (pending.length > 0) return pending[0];
|
|
62
59
|
|
|
63
|
-
const
|
|
64
|
-
.sort(
|
|
65
|
-
return
|
|
60
|
+
const active = features.filter(f => f.status === 'in_progress' || f.status === 'testing')
|
|
61
|
+
.sort(byPriority);
|
|
62
|
+
if (active.length > 0) return active[0];
|
|
63
|
+
|
|
64
|
+
const failed = features.filter(f => f.status === 'failed').sort(byPriority);
|
|
65
|
+
return failed[0] || null;
|
|
66
66
|
}
|
|
67
67
|
|
|
68
68
|
function isAllDone(taskData) {
|
|
@@ -91,6 +91,19 @@ function markSimplifyDone() {
|
|
|
91
91
|
saveState(state);
|
|
92
92
|
}
|
|
93
93
|
|
|
94
|
+
// ─── Design State Helpers ─────────────────────────────────
|
|
95
|
+
|
|
96
|
+
function loadDesignState() {
|
|
97
|
+
const state = loadState();
|
|
98
|
+
return state.design || {};
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
function saveDesignState(designData) {
|
|
102
|
+
const state = loadState();
|
|
103
|
+
state.design = { ...state.design, ...designData };
|
|
104
|
+
saveState(state);
|
|
105
|
+
}
|
|
106
|
+
|
|
94
107
|
module.exports = {
|
|
95
108
|
DEFAULT_STATE,
|
|
96
109
|
loadState,
|
|
@@ -101,5 +114,7 @@ module.exports = {
|
|
|
101
114
|
appendProgress,
|
|
102
115
|
incrementSession,
|
|
103
116
|
markSimplifyDone,
|
|
117
|
+
loadDesignState,
|
|
118
|
+
saveDesignState,
|
|
104
119
|
TASK_STATUSES,
|
|
105
120
|
};
|
package/src/index.js
CHANGED
|
@@ -68,6 +68,10 @@ async function main(command, input, opts = {}) {
|
|
|
68
68
|
const { executeGo } = require('./core/go');
|
|
69
69
|
return executeGo(config, input, opts);
|
|
70
70
|
}
|
|
71
|
+
case 'design': {
|
|
72
|
+
const { executeDesign } = require('./core/design');
|
|
73
|
+
return executeDesign(config, input, opts);
|
|
74
|
+
}
|
|
71
75
|
default:
|
|
72
76
|
throw new Error(`未知命令: ${command}`);
|
|
73
77
|
}
|