claude-coder 1.8.3 → 1.9.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/README.md +59 -12
- package/bin/cli.js +20 -37
- package/package.json +4 -1
- package/recipes/_shared/roles/developer.md +11 -0
- package/recipes/_shared/roles/product.md +12 -0
- package/recipes/_shared/roles/tester.md +12 -0
- package/recipes/_shared/test/report-format.md +86 -0
- package/recipes/backend/base.md +27 -0
- package/recipes/backend/components/auth.md +18 -0
- package/recipes/backend/components/crud-api.md +18 -0
- package/recipes/backend/components/file-service.md +15 -0
- package/recipes/backend/manifest.json +20 -0
- package/recipes/backend/test/api-test.md +25 -0
- package/recipes/console/base.md +37 -0
- package/recipes/console/components/modal-form.md +20 -0
- package/recipes/console/components/pagination.md +17 -0
- package/recipes/console/components/search.md +17 -0
- package/recipes/console/components/table-list.md +18 -0
- package/recipes/console/components/tabs.md +14 -0
- package/recipes/console/components/tree.md +15 -0
- package/recipes/console/components/upload.md +15 -0
- package/recipes/console/manifest.json +24 -0
- package/recipes/console/test/crud-e2e.md +47 -0
- package/recipes/h5/base.md +26 -0
- package/recipes/h5/components/animation.md +11 -0
- package/recipes/h5/components/countdown.md +11 -0
- package/recipes/h5/components/share.md +11 -0
- package/recipes/h5/components/swiper.md +11 -0
- package/recipes/h5/manifest.json +21 -0
- package/recipes/h5/test/h5-e2e.md +20 -0
- package/src/commands/auth.js +87 -15
- package/src/commands/setup-modules/helpers.js +4 -3
- package/src/commands/setup-modules/mcp.js +44 -24
- package/src/commands/setup-modules/safety.js +1 -15
- package/src/commands/setup.js +8 -8
- package/src/common/assets.js +10 -1
- package/src/common/config.js +2 -2
- package/src/common/indicator.js +158 -120
- package/src/common/utils.js +60 -8
- package/src/core/coding.js +16 -38
- package/src/core/go.js +31 -77
- package/src/core/hooks.js +56 -89
- package/src/core/init.js +94 -100
- package/src/core/plan.js +85 -223
- package/src/core/prompts.js +36 -16
- package/src/core/repair.js +7 -17
- package/src/core/runner.js +306 -43
- package/src/core/scan.js +38 -34
- package/src/core/session.js +253 -39
- package/src/core/simplify.js +45 -24
- package/src/core/state.js +105 -0
- package/src/index.js +76 -0
- package/templates/codingSystem.md +2 -2
- package/templates/codingUser.md +1 -1
- package/templates/guidance.json +22 -3
- package/templates/planSystem.md +2 -2
- package/templates/scanSystem.md +3 -3
- package/templates/scanUser.md +1 -1
- package/templates/web-testing.md +17 -0
- package/types/index.d.ts +217 -0
- package/src/core/context.js +0 -117
- package/src/core/harness.js +0 -484
- package/src/core/query.js +0 -50
- package/templates/playwright.md +0 -17
package/src/core/scan.js
CHANGED
|
@@ -1,13 +1,29 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
|
+
const fs = require('fs');
|
|
4
|
+
const path = require('path');
|
|
3
5
|
const { log } = require('../common/config');
|
|
4
6
|
const { assets } = require('../common/assets');
|
|
5
|
-
const { runSession } = require('./session');
|
|
6
|
-
const { buildQueryOptions, hasCodeFiles } = require('./query');
|
|
7
7
|
const { buildSystemPrompt, buildScanPrompt } = require('./prompts');
|
|
8
|
-
const {
|
|
8
|
+
const { Session } = require('./session');
|
|
9
9
|
const { RETRY } = require('../common/constants');
|
|
10
10
|
|
|
11
|
+
function hasCodeFiles(projectRoot) {
|
|
12
|
+
const markers = [
|
|
13
|
+
'package.json', 'pyproject.toml', 'requirements.txt', 'setup.py',
|
|
14
|
+
'Cargo.toml', 'go.mod', 'pom.xml', 'build.gradle',
|
|
15
|
+
'Makefile', 'Dockerfile', 'docker-compose.yml',
|
|
16
|
+
'README.md', 'main.py', 'app.py', 'index.js', 'index.ts',
|
|
17
|
+
];
|
|
18
|
+
for (const m of markers) {
|
|
19
|
+
if (fs.existsSync(path.join(projectRoot, m))) return true;
|
|
20
|
+
}
|
|
21
|
+
for (const d of ['src', 'lib', 'app', 'backend', 'frontend', 'web', 'server', 'client']) {
|
|
22
|
+
if (fs.existsSync(path.join(projectRoot, d)) && fs.statSync(path.join(projectRoot, d)).isDirectory()) return true;
|
|
23
|
+
}
|
|
24
|
+
return false;
|
|
25
|
+
}
|
|
26
|
+
|
|
11
27
|
function validateProfile() {
|
|
12
28
|
if (!assets.exists('profile')) return { valid: false, issues: ['profile 不存在'] };
|
|
13
29
|
|
|
@@ -28,41 +44,30 @@ function validateProfile() {
|
|
|
28
44
|
return { valid: issues.length === 0, issues };
|
|
29
45
|
}
|
|
30
46
|
|
|
31
|
-
async function
|
|
32
|
-
const
|
|
33
|
-
const dateStr = new Date().toISOString().slice(0, 10).replace(/-/g, '');
|
|
34
|
-
|
|
35
|
-
return runSession('scan', {
|
|
36
|
-
opts,
|
|
37
|
-
sessionNum: 0,
|
|
38
|
-
logFileName: `scan_${dateStr}.log`,
|
|
39
|
-
label: `scan (${projectType})`,
|
|
40
|
-
|
|
41
|
-
async execute(sdk, ctx) {
|
|
42
|
-
log('info', `正在调用 Claude Code 执行项目扫描(${projectType}项目)...`);
|
|
47
|
+
async function executeScan(config, opts = {}) {
|
|
48
|
+
const maxAttempts = RETRY.SCAN_ATTEMPTS;
|
|
43
49
|
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
queryOpts.systemPrompt = buildSystemPrompt('scan');
|
|
47
|
-
queryOpts.hooks = ctx.hooks;
|
|
48
|
-
queryOpts.abortController = ctx.abortController;
|
|
50
|
+
for (let attempt = 1; attempt <= maxAttempts; attempt++) {
|
|
51
|
+
log('info', `初始化尝试 ${attempt} / ${maxAttempts} ...`);
|
|
49
52
|
|
|
50
|
-
|
|
51
|
-
|
|
53
|
+
const projectType = hasCodeFiles(opts.projectRoot || assets.projectRoot) ? 'existing' : 'new';
|
|
54
|
+
const dateStr = new Date().toISOString().slice(0, 10).replace(/-/g, '');
|
|
52
55
|
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
}
|
|
56
|
+
const result = await Session.run('scan', config, {
|
|
57
|
+
logFileName: `scan_${dateStr}.log`,
|
|
58
|
+
label: `scan (${projectType})`,
|
|
57
59
|
|
|
58
|
-
async
|
|
59
|
-
|
|
60
|
+
async execute(session) {
|
|
61
|
+
log('info', `正在调用 Claude Code 执行项目扫描(${projectType}项目)...`);
|
|
60
62
|
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
63
|
+
const prompt = buildScanPrompt(projectType);
|
|
64
|
+
const queryOpts = session.buildQueryOptions(opts);
|
|
65
|
+
queryOpts.systemPrompt = buildSystemPrompt('scan');
|
|
64
66
|
|
|
65
|
-
|
|
67
|
+
const { cost } = await session.runQuery(prompt, queryOpts);
|
|
68
|
+
return { cost };
|
|
69
|
+
},
|
|
70
|
+
});
|
|
66
71
|
|
|
67
72
|
if (assets.exists('profile')) {
|
|
68
73
|
const profileCheck = validateProfile();
|
|
@@ -83,7 +88,6 @@ async function scan(opts = {}) {
|
|
|
83
88
|
}
|
|
84
89
|
|
|
85
90
|
module.exports = {
|
|
86
|
-
|
|
91
|
+
executeScan,
|
|
87
92
|
validateProfile,
|
|
88
|
-
_runScanSession,
|
|
89
93
|
};
|
package/src/core/session.js
CHANGED
|
@@ -1,57 +1,271 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
|
-
const
|
|
4
|
-
const
|
|
5
|
-
const {
|
|
3
|
+
const fs = require('fs');
|
|
4
|
+
const path = require('path');
|
|
5
|
+
const { buildEnvVars, log } = require('../common/config');
|
|
6
|
+
const { Indicator } = require('../common/indicator');
|
|
7
|
+
const { logMessage: baseLogMessage, extractResult, writeSessionSeparator } = require('../common/logging');
|
|
8
|
+
const { createHooks } = require('./hooks');
|
|
9
|
+
const { assets } = require('../common/assets');
|
|
6
10
|
|
|
7
11
|
/**
|
|
8
|
-
*
|
|
9
|
-
* @
|
|
10
|
-
* @
|
|
11
|
-
* @
|
|
12
|
+
* @typedef {Object} SessionRunOptions
|
|
13
|
+
* @property {string} logFileName - 日志文件名
|
|
14
|
+
* @property {import('fs').WriteStream} [logStream] - 外部日志流(与 logFileName 二选一)
|
|
15
|
+
* @property {number} [sessionNum=0] - 会话编号
|
|
16
|
+
* @property {string} [label=''] - 会话标签
|
|
17
|
+
* @property {(session: Session) => Promise<Object>} execute - 执行回调,接收 session 实例
|
|
12
18
|
*/
|
|
13
|
-
async function runSession(type, config) {
|
|
14
|
-
const sdk = await loadSDK();
|
|
15
|
-
const ctx = config.externalCtx || new SessionContext(type, config.opts);
|
|
16
19
|
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
20
|
+
/**
|
|
21
|
+
* @typedef {Object} QueryResult
|
|
22
|
+
* @property {Array<Object>} messages - 所有 SDK 消息
|
|
23
|
+
* @property {boolean} success - 是否成功完成
|
|
24
|
+
* @property {string|null} subtype - 结果子类型
|
|
25
|
+
* @property {number|null} cost - 美元费用
|
|
26
|
+
* @property {Object|null} usage - token 用量 { input_tokens, output_tokens }
|
|
27
|
+
* @property {number|null} turns - 对话轮次
|
|
28
|
+
*/
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* @typedef {Object} RunQueryOpts
|
|
32
|
+
* @property {(message: Object, messages: Array<Object>) => void|'break'} [onMessage] - 每条消息的回调,返回 'break' 中断
|
|
33
|
+
*/
|
|
21
34
|
|
|
22
|
-
|
|
23
|
-
|
|
35
|
+
/**
|
|
36
|
+
* SDK 会话管理类。通过 Session.run() 创建和管理一次完整的 AI 会话生命周期。
|
|
37
|
+
*
|
|
38
|
+
* 使用方式:
|
|
39
|
+
* ```js
|
|
40
|
+
* const result = await Session.run('coding', config, {
|
|
41
|
+
* logFileName: 'coding.log',
|
|
42
|
+
* async execute(session) {
|
|
43
|
+
* const queryOpts = session.buildQueryOptions();
|
|
44
|
+
* const { messages, success } = await session.runQuery(prompt, queryOpts);
|
|
45
|
+
* return { success };
|
|
46
|
+
* },
|
|
47
|
+
* });
|
|
48
|
+
* ```
|
|
49
|
+
*/
|
|
50
|
+
class Session {
|
|
51
|
+
/** @type {Object|null} SDK 单例 */
|
|
52
|
+
static _sdk = null;
|
|
24
53
|
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
54
|
+
/**
|
|
55
|
+
* 确保 SDK 已加载(懒加载单例)
|
|
56
|
+
* @param {Object} config - 项目配置
|
|
57
|
+
* @returns {Promise<Object>} SDK 实例
|
|
58
|
+
*/
|
|
59
|
+
static async ensureSDK(config) {
|
|
60
|
+
if (!Session._sdk) {
|
|
61
|
+
Object.assign(process.env, buildEnvVars(config));
|
|
62
|
+
const { loadSDK } = require('../common/sdk');
|
|
63
|
+
Session._sdk = await loadSDK();
|
|
29
64
|
}
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
65
|
+
return Session._sdk;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* 创建 Session 实例并执行回调,自动管理生命周期(日志、hooks、indicator)
|
|
70
|
+
* @param {string} type - 会话类型(coding | plan | scan | go | simplify | repair 等)
|
|
71
|
+
* @param {Object} config - 项目配置
|
|
72
|
+
* @param {SessionRunOptions} options - 运行选项
|
|
73
|
+
* @returns {Promise<Object>} 包含 exitCode、logFile、stalled 以及 execute 返回值
|
|
74
|
+
*/
|
|
75
|
+
static async run(type, config, { logFileName, logStream, sessionNum = 0, label = '', execute }) {
|
|
76
|
+
await Session.ensureSDK(config);
|
|
77
|
+
const session = new Session(type, config, { logFileName, logStream, sessionNum, label });
|
|
78
|
+
try {
|
|
79
|
+
const result = await execute(session);
|
|
80
|
+
session.finish();
|
|
81
|
+
return {
|
|
82
|
+
exitCode: session.isStalled() ? 2 : 0,
|
|
83
|
+
logFile: session.logFile,
|
|
84
|
+
stalled: session.isStalled(),
|
|
85
|
+
...result,
|
|
86
|
+
};
|
|
87
|
+
} catch (err) {
|
|
88
|
+
session.finish();
|
|
89
|
+
throw err;
|
|
33
90
|
}
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* @param {string} type - 会话类型
|
|
95
|
+
* @param {Object} config - 项目配置
|
|
96
|
+
* @param {Object} options
|
|
97
|
+
* @param {string} options.logFileName - 日志文件名
|
|
98
|
+
* @param {import('fs').WriteStream} [options.logStream] - 外部日志流
|
|
99
|
+
* @param {number} [options.sessionNum=0]
|
|
100
|
+
* @param {string} [options.label='']
|
|
101
|
+
*/
|
|
102
|
+
constructor(type, config, { logFileName, logStream, sessionNum = 0, label = '' }) {
|
|
103
|
+
this.config = config;
|
|
104
|
+
this.type = type;
|
|
105
|
+
this.indicator = new Indicator();
|
|
106
|
+
/** @type {import('fs').WriteStream|null} */
|
|
107
|
+
this.logStream = null;
|
|
108
|
+
/** @type {string|null} */
|
|
109
|
+
this.logFile = null;
|
|
110
|
+
/** @type {Object|null} */
|
|
111
|
+
this.hooks = null;
|
|
112
|
+
/** @type {Function|null} */
|
|
113
|
+
this.cleanup = null;
|
|
114
|
+
this._isStalled = () => false;
|
|
115
|
+
this.abortController = new AbortController();
|
|
116
|
+
|
|
117
|
+
this._initLogging(logFileName, logStream);
|
|
118
|
+
writeSessionSeparator(this.logStream, sessionNum, label);
|
|
119
|
+
const stallTimeoutMin = this._initHooks(type);
|
|
120
|
+
this._startIndicator(sessionNum, stallTimeoutMin);
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/**
|
|
124
|
+
* 构建 SDK query 选项,自动附加 hooks、abortController、权限模式
|
|
125
|
+
* @param {Object} [overrides={}] - 覆盖选项(permissionMode, projectRoot, model 等)
|
|
126
|
+
* @returns {Object} SDK query options
|
|
127
|
+
*/
|
|
128
|
+
buildQueryOptions(overrides = {}) {
|
|
129
|
+
const mode = overrides.permissionMode || 'bypassPermissions';
|
|
130
|
+
const base = {
|
|
131
|
+
permissionMode: mode,
|
|
132
|
+
cwd: overrides.projectRoot || assets.projectRoot,
|
|
133
|
+
env: buildEnvVars(this.config),
|
|
134
|
+
settingSources: ['project'],
|
|
135
|
+
hooks: this.hooks,
|
|
136
|
+
abortController: this.abortController,
|
|
39
137
|
};
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
try { config.onError(err, ctx); } catch { /* ignore callback errors */ }
|
|
138
|
+
if (mode === 'bypassPermissions') {
|
|
139
|
+
base.allowDangerouslySkipPermissions = true;
|
|
43
140
|
}
|
|
44
|
-
if (
|
|
45
|
-
|
|
141
|
+
if (this.config.maxTurns > 0) base.maxTurns = this.config.maxTurns;
|
|
142
|
+
if (overrides.model) base.model = overrides.model;
|
|
143
|
+
else if (this.config.model) base.model = this.config.model;
|
|
144
|
+
return base;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
/**
|
|
148
|
+
* 执行一次 SDK 查询,遍历消息流并收集结果
|
|
149
|
+
* @param {string} prompt - 用户提示
|
|
150
|
+
* @param {Object} queryOpts - SDK query 选项(通常来自 buildQueryOptions)
|
|
151
|
+
* @param {RunQueryOpts} [opts={}] - 额外选项(onMessage 回调)
|
|
152
|
+
* @returns {Promise<QueryResult>}
|
|
153
|
+
*/
|
|
154
|
+
async runQuery(prompt, queryOpts, opts = {}) {
|
|
155
|
+
if (this.logStream?.writable) {
|
|
156
|
+
const sep = '-'.repeat(40);
|
|
157
|
+
if (queryOpts.systemPrompt) {
|
|
158
|
+
this.logStream.write(`\n${sep}\n[SYSTEM_PROMPT]\n${sep}\n${queryOpts.systemPrompt}\n`);
|
|
159
|
+
}
|
|
160
|
+
this.logStream.write(`\n${sep}\n[USER_PROMPT]\n${sep}\n${prompt}\n${sep}\n\n`);
|
|
46
161
|
}
|
|
162
|
+
|
|
163
|
+
const sdk = Session._sdk;
|
|
164
|
+
const messages = [];
|
|
165
|
+
const querySession = sdk.query({ prompt, options: queryOpts });
|
|
166
|
+
|
|
167
|
+
for await (const message of querySession) {
|
|
168
|
+
if (this._isStalled()) {
|
|
169
|
+
log('warn', '停顿超时,中断消息循环');
|
|
170
|
+
break;
|
|
171
|
+
}
|
|
172
|
+
messages.push(message);
|
|
173
|
+
this._logMessage(message);
|
|
174
|
+
|
|
175
|
+
if (opts.onMessage) {
|
|
176
|
+
const action = opts.onMessage(message, messages);
|
|
177
|
+
if (action === 'break') break;
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
const sdkResult = extractResult(messages);
|
|
182
|
+
const cost = sdkResult?.total_cost_usd || null;
|
|
183
|
+
const usage = sdkResult?.usage || null;
|
|
184
|
+
const turns = sdkResult?.num_turns || null;
|
|
185
|
+
|
|
186
|
+
if (cost != null || turns != null) {
|
|
187
|
+
const parts = [];
|
|
188
|
+
if (turns != null) parts.push(`turns: ${turns}`);
|
|
189
|
+
if (cost != null) parts.push(`cost: $${cost}`);
|
|
190
|
+
if (usage) {
|
|
191
|
+
const inp = usage.input_tokens || 0;
|
|
192
|
+
const out = usage.output_tokens || 0;
|
|
193
|
+
parts.push(`tokens: ${inp}+${out}`);
|
|
194
|
+
}
|
|
195
|
+
const summary = parts.join(', ');
|
|
196
|
+
console.log('----- SESSION END -----');
|
|
197
|
+
log('info', `session 统计: ${summary}`);
|
|
198
|
+
if (this.logStream?.writable) {
|
|
199
|
+
this.logStream.write(`[SESSION_STATS] ${summary}\n`);
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
|
|
47
203
|
return {
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
204
|
+
messages,
|
|
205
|
+
success: sdkResult?.subtype === 'success',
|
|
206
|
+
subtype: sdkResult?.subtype || null,
|
|
207
|
+
cost, usage, turns,
|
|
51
208
|
};
|
|
52
209
|
}
|
|
210
|
+
|
|
211
|
+
/** 检查会话是否因停顿超时 */
|
|
212
|
+
isStalled() {
|
|
213
|
+
return this._isStalled();
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
/** 结束会话:清理 hooks、关闭日志流、停止 indicator */
|
|
217
|
+
finish() {
|
|
218
|
+
if (this.cleanup) this.cleanup();
|
|
219
|
+
if (this.logStream && !this._externalLogStream) this.logStream.end();
|
|
220
|
+
this.indicator.stop();
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
// ─── Private ────────────────────────────────────────────
|
|
224
|
+
|
|
225
|
+
_initLogging(logFileName, externalLogStream) {
|
|
226
|
+
if (externalLogStream) {
|
|
227
|
+
this.logStream = externalLogStream;
|
|
228
|
+
this._externalLogStream = true;
|
|
229
|
+
} else {
|
|
230
|
+
const logsDir = assets.dir('logs');
|
|
231
|
+
this.logFile = path.join(logsDir, logFileName);
|
|
232
|
+
this.logStream = fs.createWriteStream(this.logFile, { flags: 'a' });
|
|
233
|
+
this._externalLogStream = false;
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
_initHooks(hookType) {
|
|
238
|
+
const stallTimeoutMs = this.config.stallTimeout * 1000;
|
|
239
|
+
const result = createHooks(hookType, this.indicator, this.logStream, {
|
|
240
|
+
stallTimeoutMs,
|
|
241
|
+
abortController: this.abortController,
|
|
242
|
+
editThreshold: this.config.editThreshold,
|
|
243
|
+
});
|
|
244
|
+
this.hooks = result.hooks;
|
|
245
|
+
this.cleanup = result.cleanup;
|
|
246
|
+
this._isStalled = result.isStalled;
|
|
247
|
+
return Math.floor(stallTimeoutMs / 60000);
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
_startIndicator(sessionNum, stallTimeoutMin) {
|
|
251
|
+
this.indicator.start(sessionNum, stallTimeoutMin, assets.projectRoot);
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
_logMessage(message) {
|
|
255
|
+
const hasText = message.type === 'assistant'
|
|
256
|
+
&& message.message?.content?.some(b => b.type === 'text' && b.text);
|
|
257
|
+
|
|
258
|
+
if (hasText && this.indicator) {
|
|
259
|
+
this.indicator.pauseRendering();
|
|
260
|
+
process.stderr.write('\r\x1b[K');
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
baseLogMessage(message, this.logStream, this.indicator);
|
|
264
|
+
|
|
265
|
+
if (hasText && this.indicator) {
|
|
266
|
+
this.indicator.resumeRendering();
|
|
267
|
+
}
|
|
268
|
+
}
|
|
53
269
|
}
|
|
54
270
|
|
|
55
|
-
module.exports = {
|
|
56
|
-
runSession,
|
|
57
|
-
};
|
|
271
|
+
module.exports = { Session };
|
package/src/core/simplify.js
CHANGED
|
@@ -1,53 +1,74 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
|
-
const { runSession } = require('./session');
|
|
4
|
-
const { buildQueryOptions } = require('./query');
|
|
5
3
|
const { log } = require('../common/config');
|
|
6
4
|
const { assets } = require('../common/assets');
|
|
5
|
+
const { Session } = require('./session');
|
|
7
6
|
const { execSync } = require('child_process');
|
|
8
7
|
|
|
9
|
-
|
|
8
|
+
const AUTO_COMMIT_MSG = 'style: auto simplify';
|
|
9
|
+
|
|
10
|
+
function getSmartDiffRange(projectRoot, fallbackN) {
|
|
11
|
+
try {
|
|
12
|
+
const hash = execSync(
|
|
13
|
+
`git log --grep='${AUTO_COMMIT_MSG}' -1 --format='%H'`,
|
|
14
|
+
{ cwd: projectRoot, encoding: 'utf8', stdio: ['pipe', 'pipe', 'pipe'] },
|
|
15
|
+
).trim();
|
|
16
|
+
if (hash) return { range: `${hash}..HEAD`, label: `自上次 auto simplify 以来` };
|
|
17
|
+
} catch { /* ignore */ }
|
|
18
|
+
return { range: `HEAD~${fallbackN}..HEAD`, label: `最近 ${fallbackN} 个 commit` };
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function commitIfDirty(projectRoot) {
|
|
22
|
+
try {
|
|
23
|
+
execSync('git diff --quiet HEAD', { cwd: projectRoot, stdio: 'pipe' });
|
|
24
|
+
} catch {
|
|
25
|
+
execSync(`git add -A && git commit -m "${AUTO_COMMIT_MSG}"`, { cwd: projectRoot, stdio: 'pipe' });
|
|
26
|
+
log('ok', `代码优化已提交: ${AUTO_COMMIT_MSG}`);
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
async function executeSimplify(config, focus = null, opts = {}) {
|
|
31
|
+
const n = opts.n || 3;
|
|
10
32
|
const projectRoot = assets.projectRoot;
|
|
33
|
+
|
|
34
|
+
const { range, label } = getSmartDiffRange(projectRoot, n);
|
|
35
|
+
|
|
11
36
|
let diff = '';
|
|
12
37
|
try {
|
|
13
|
-
diff = execSync(`git diff
|
|
38
|
+
diff = execSync(`git diff ${range}`, { cwd: projectRoot, encoding: 'utf8', maxBuffer: 50 * 1024 * 1024 });
|
|
14
39
|
} catch (err) {
|
|
15
|
-
log('warn',
|
|
40
|
+
log('warn', `无法获取 diff (${label}): ${err.message}`);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
if (!diff.trim()) {
|
|
44
|
+
log('info', `无变更需要审查 (${label})`);
|
|
45
|
+
return { success: true };
|
|
16
46
|
}
|
|
17
47
|
|
|
18
48
|
const focusLine = focus ? `\n审查聚焦方向:${focus}` : '';
|
|
19
|
-
const prompt = `/simplify\n\n
|
|
49
|
+
const prompt = `/simplify\n\n审查范围:${label}${focusLine}\n\n${diff.slice(0, 50000)}`;
|
|
20
50
|
const dateStr = new Date().toISOString().replace(/[-:T]/g, '').slice(0, 12);
|
|
21
51
|
|
|
22
|
-
|
|
23
|
-
opts,
|
|
24
|
-
sessionNum: 0,
|
|
52
|
+
const result = await Session.run('simplify', config, {
|
|
25
53
|
logFileName: `simplify_${dateStr}.log`,
|
|
26
54
|
label: 'simplify',
|
|
27
55
|
|
|
28
|
-
async execute(
|
|
29
|
-
log('info',
|
|
56
|
+
async execute(session) {
|
|
57
|
+
log('info', `正在审查代码变更 (${label})...`);
|
|
30
58
|
|
|
31
|
-
const queryOpts = buildQueryOptions(
|
|
32
|
-
queryOpts.hooks = ctx.hooks;
|
|
33
|
-
queryOpts.abortController = ctx.abortController;
|
|
59
|
+
const queryOpts = session.buildQueryOptions(opts);
|
|
34
60
|
queryOpts.disallowedTools = ['askUserQuestion'];
|
|
35
61
|
|
|
36
|
-
await
|
|
62
|
+
await session.runQuery(prompt, queryOpts);
|
|
37
63
|
log('ok', '代码审查完成');
|
|
38
64
|
|
|
39
65
|
return {};
|
|
40
66
|
},
|
|
41
67
|
});
|
|
42
|
-
}
|
|
43
68
|
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
return _runSimplifySession(n, focus, opts);
|
|
69
|
+
commitIfDirty(projectRoot);
|
|
70
|
+
|
|
71
|
+
return result;
|
|
48
72
|
}
|
|
49
73
|
|
|
50
|
-
module.exports = {
|
|
51
|
-
simplify,
|
|
52
|
-
_runSimplifySession,
|
|
53
|
-
};
|
|
74
|
+
module.exports = { executeSimplify };
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const { assets } = require('../common/assets');
|
|
4
|
+
const { getFeatures } = require('../common/tasks');
|
|
5
|
+
const { TASK_STATUSES } = require('../common/constants');
|
|
6
|
+
|
|
7
|
+
// ─── Harness State (harness_state.json) ───────────────────
|
|
8
|
+
|
|
9
|
+
const DEFAULT_STATE = Object.freeze({
|
|
10
|
+
version: 1,
|
|
11
|
+
next_task_id: 1,
|
|
12
|
+
next_priority: 1,
|
|
13
|
+
session_count: 0,
|
|
14
|
+
last_simplify_session: 0,
|
|
15
|
+
current_task_id: null,
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
function loadState() {
|
|
19
|
+
return assets.readJson('harnessState', { ...DEFAULT_STATE });
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function saveState(data) {
|
|
23
|
+
assets.writeJson('harnessState', data);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function extractIdNum(id) {
|
|
27
|
+
const m = String(id).match(/(\d+)$/);
|
|
28
|
+
return m ? parseInt(m[1], 10) : 0;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function syncAfterPlan() {
|
|
32
|
+
const state = loadState();
|
|
33
|
+
const tasks = assets.readJson('tasks', null);
|
|
34
|
+
if (!tasks || !tasks.features) return state;
|
|
35
|
+
|
|
36
|
+
const features = tasks.features;
|
|
37
|
+
state.next_task_id = features.reduce((max, f) => Math.max(max, extractIdNum(f.id)), 0) + 1;
|
|
38
|
+
state.next_priority = features.reduce((max, f) => Math.max(max, f.priority || 0), 0) + 1;
|
|
39
|
+
saveState(state);
|
|
40
|
+
return state;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
// ─── Task Scheduling ──────────────────────────────────────
|
|
44
|
+
|
|
45
|
+
function selectNextTask(taskData) {
|
|
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];
|
|
51
|
+
|
|
52
|
+
const pending = features.filter(f => f.status === 'pending')
|
|
53
|
+
.filter(f => {
|
|
54
|
+
const deps = f.depends_on || [];
|
|
55
|
+
return deps.every(depId => {
|
|
56
|
+
const dep = features.find(x => x.id === depId);
|
|
57
|
+
return dep && dep.status === 'done';
|
|
58
|
+
});
|
|
59
|
+
})
|
|
60
|
+
.sort((a, b) => (a.priority || 999) - (b.priority || 999));
|
|
61
|
+
if (pending.length > 0) return pending[0];
|
|
62
|
+
|
|
63
|
+
const inProgress = features.filter(f => f.status === 'in_progress')
|
|
64
|
+
.sort((a, b) => (a.priority || 999) - (b.priority || 999));
|
|
65
|
+
return inProgress[0] || null;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
function isAllDone(taskData) {
|
|
69
|
+
const features = getFeatures(taskData);
|
|
70
|
+
return features.length > 0 && features.every(f => f.status === 'done');
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
// ─── Progress & Counters ──────────────────────────────────
|
|
74
|
+
|
|
75
|
+
function appendProgress(entry) {
|
|
76
|
+
let progress = assets.readJson('progress', { sessions: [] });
|
|
77
|
+
if (!Array.isArray(progress.sessions)) progress.sessions = [];
|
|
78
|
+
progress.sessions.push(entry);
|
|
79
|
+
assets.writeJson('progress', progress);
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
function incrementSession() {
|
|
83
|
+
const state = loadState();
|
|
84
|
+
state.session_count++;
|
|
85
|
+
saveState(state);
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
function markSimplifyDone() {
|
|
89
|
+
const state = loadState();
|
|
90
|
+
state.last_simplify_session = state.session_count;
|
|
91
|
+
saveState(state);
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
module.exports = {
|
|
95
|
+
DEFAULT_STATE,
|
|
96
|
+
loadState,
|
|
97
|
+
saveState,
|
|
98
|
+
syncAfterPlan,
|
|
99
|
+
selectNextTask,
|
|
100
|
+
isAllDone,
|
|
101
|
+
appendProgress,
|
|
102
|
+
incrementSession,
|
|
103
|
+
markSimplifyDone,
|
|
104
|
+
TASK_STATUSES,
|
|
105
|
+
};
|