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.
Files changed (40) hide show
  1. package/README.md +166 -141
  2. package/bin/cli.js +19 -4
  3. package/package.json +2 -2
  4. package/src/common/assets.js +66 -52
  5. package/src/common/config.js +22 -0
  6. package/src/common/sdk.js +1 -3
  7. package/src/common/utils.js +3 -1
  8. package/src/core/coding.js +3 -1
  9. package/src/core/design.js +268 -0
  10. package/src/core/go.js +3 -3
  11. package/src/core/hooks.js +30 -16
  12. package/src/core/init.js +9 -0
  13. package/src/core/plan.js +21 -15
  14. package/src/core/prompts.js +47 -2
  15. package/src/core/repair.js +1 -1
  16. package/src/core/runner.js +84 -117
  17. package/src/core/scan.js +4 -3
  18. package/src/core/session.js +30 -16
  19. package/src/core/simplify.js +4 -2
  20. package/src/core/state.js +23 -8
  21. package/src/index.js +4 -0
  22. package/templates/{codingUser.md → coding/user.md} +1 -0
  23. package/templates/design/base.md +103 -0
  24. package/templates/design/fixSystem.md +71 -0
  25. package/templates/design/fixUser.md +3 -0
  26. package/templates/design/init.md +304 -0
  27. package/templates/design/system.md +108 -0
  28. package/templates/design/user.md +11 -0
  29. package/templates/{coreProtocol.md → other/coreProtocol.md} +1 -0
  30. package/templates/{test_rule.md → other/test_rule.md} +0 -2
  31. package/templates/{planUser.md → plan/user.md} +2 -1
  32. /package/templates/{codingSystem.md → coding/system.md} +0 -0
  33. /package/templates/{goSystem.md → go/system.md} +0 -0
  34. /package/templates/{bash-process.md → other/bash-process.md} +0 -0
  35. /package/templates/{guidance.json → other/guidance.json} +0 -0
  36. /package/templates/{requirements.example.md → other/requirements.example.md} +0 -0
  37. /package/templates/{web-testing.md → other/web-testing.md} +0 -0
  38. /package/templates/{planSystem.md → plan/system.md} +0 -0
  39. /package/templates/{scanSystem.md → scan/system.md} +0 -0
  40. /package/templates/{scanUser.md → scan/user.md} +0 -0
@@ -115,9 +115,31 @@ function updateEnvVar(key, value) {
115
115
  return true;
116
116
  }
117
117
 
118
+ const REPO_URL = 'https://lk19940215.github.io/claude-coder';
119
+
120
+ /**
121
+ * @param {string} command - 命令名(run / plan / design / go / simplify / scan)
122
+ * @param {string} detail - 右侧附加信息(模式、范围等)
123
+ * @param {string} [model] - 模型名
124
+ */
125
+ function printModeBanner(command, detail, model) {
126
+ const sep = ` ${COLOR.dim}│${COLOR.reset} `;
127
+ const parts = [`${COLOR.bold}Claude Coder${COLOR.reset}`, command];
128
+ if (model) parts.push(`model: ${model}`);
129
+ if (detail) parts.push(detail);
130
+ const inner = parts.join(sep);
131
+ console.error('');
132
+ console.error(`${COLOR.cyan}╔══════════════════════════════════════════════╗${COLOR.reset}`);
133
+ console.error(`${COLOR.cyan}║${COLOR.reset} ${inner}`);
134
+ console.error(`${COLOR.cyan}║${COLOR.reset} ${COLOR.dim}${REPO_URL}${COLOR.reset}`);
135
+ console.error(`${COLOR.cyan}╚══════════════════════════════════════════════╝${COLOR.reset}`);
136
+ console.error('');
137
+ }
138
+
118
139
  module.exports = {
119
140
  COLOR,
120
141
  log,
142
+ printModeBanner,
121
143
  parseEnvFile,
122
144
  loadConfig,
123
145
  buildEnvVars,
package/src/common/sdk.js CHANGED
@@ -43,9 +43,7 @@ async function loadSDK() {
43
43
  } catch { /* try next */ }
44
44
  }
45
45
 
46
- log('error', `未找到 ${pkgName}`);
47
- log('error', `请先安装:npm install -g ${pkgName}`);
48
- process.exit(1);
46
+ throw new Error(`未找到 ${pkgName},请先安装:npm install -g ${pkgName}`);
49
47
  }
50
48
 
51
49
  module.exports = { loadSDK };
@@ -133,6 +133,8 @@ function ensureGitignore(projectRoot) {
133
133
  '.claude-coder/*',
134
134
  '!.claude-coder/tasks.json',
135
135
  '!.claude-coder/project_profile.json',
136
+ '!.claude-coder/design/',
137
+ '!.claude-coder/harness_state.json',
136
138
  ];
137
139
  let added = false;
138
140
  for (const p of patterns) {
@@ -171,7 +173,7 @@ function killServices(projectRoot) {
171
173
  const { assets } = require('./assets');
172
174
  const profile = assets.readJson('profile', null);
173
175
  if (!profile) return;
174
- const ports = (profile.services || []).map(s => s.port).filter(Boolean);
176
+ const ports = (profile.services || []).map(s => s.port).filter(p => p && /^\d+$/.test(String(p)));
175
177
  if (ports.length === 0) return;
176
178
 
177
179
  for (const port of ports) {
@@ -19,7 +19,9 @@ async function executeCoding(config, sessionNum, opts = {}) {
19
19
  queryOpts.systemPrompt = buildSystemPrompt('coding');
20
20
  queryOpts.disallowedTools = ['askUserQuestion'];
21
21
 
22
- const { subtype, cost, usage } = await session.runQuery(prompt, queryOpts);
22
+ const { subtype, cost, usage } = await session.runQuery(prompt, queryOpts, {
23
+ continue: true,
24
+ });
23
25
 
24
26
  if (subtype && subtype !== 'success' && subtype !== 'unknown') {
25
27
  log('warn', `session 结束原因: ${subtype}`);
@@ -0,0 +1,268 @@
1
+ 'use strict';
2
+
3
+ const fs = require('fs');
4
+ const path = require('path');
5
+ const readline = require('readline');
6
+ const { buildSystemPrompt } = require('./prompts');
7
+ const { log, printModeBanner } = require('../common/config');
8
+ const { assets } = require('../common/assets');
9
+ const { saveDesignState } = require('./state');
10
+ const { Session } = require('./session');
11
+
12
+ // ─── Design Dir ───────────────────────────────────────────
13
+
14
+ function getDesignDir() {
15
+ return assets.dir('design');
16
+ }
17
+
18
+ function scanPenFiles(designDir) {
19
+ const files = [];
20
+ const scan = (dir, prefix = '') => {
21
+ if (!fs.existsSync(dir)) return;
22
+ for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
23
+ if (entry.isDirectory()) {
24
+ scan(path.join(dir, entry.name), prefix + entry.name + '/');
25
+ } else if (entry.name.endsWith('.pen')) {
26
+ files.push({ rel: prefix + entry.name, abs: path.join(dir, entry.name) });
27
+ }
28
+ }
29
+ };
30
+ scan(designDir);
31
+ return files;
32
+ }
33
+
34
+ // ─── Type Resolution ─────────────────────────────────────
35
+
36
+ function resolveType(opts, designDir) {
37
+ if (opts.type) return opts.type;
38
+
39
+ const systemPenPath = path.join(designDir, 'system.lib.pen');
40
+ if (!fs.existsSync(systemPenPath)) return 'init';
41
+
42
+ return 'new';
43
+ }
44
+
45
+ // ─── Prompt Builders ─────────────────────────────────────
46
+
47
+ function hasProjectCode(root) {
48
+ const markers = ['package.json', 'pyproject.toml', 'requirements.txt', 'Cargo.toml', 'go.mod', 'pom.xml'];
49
+ const dirs = ['src', 'lib', 'app', 'frontend', 'web', 'client', 'pages'];
50
+ for (const m of markers) { if (fs.existsSync(path.join(root, m))) return true; }
51
+ for (const d of dirs) {
52
+ const p = path.join(root, d);
53
+ if (fs.existsSync(p) && fs.statSync(p).isDirectory()) return true;
54
+ }
55
+ return false;
56
+ }
57
+
58
+ function buildProjectContext() {
59
+ const root = assets.projectRoot;
60
+ const hasCode = hasProjectCode(root);
61
+ let ctx = `### 项目类型\n${hasCode ? '已有代码项目(设计时应 Read 源码还原真实内容)' : '全新项目(无现有代码,根据需求从零设计)'}\n- 项目根路径: ${root}\n\n`;
62
+
63
+ if (hasCode) {
64
+ const pkgPath = path.join(root, 'package.json');
65
+ if (fs.existsSync(pkgPath)) {
66
+ try {
67
+ const pkg = JSON.parse(fs.readFileSync(pkgPath, 'utf8'));
68
+ ctx += `### 项目信息\n- name: ${pkg.name || '未定义'}\n- description: ${pkg.description || '未定义'}\n\n`;
69
+ } catch { /* ignore */ }
70
+ }
71
+ }
72
+
73
+ return ctx;
74
+ }
75
+
76
+ function buildDesignPrompt(instruction, designDir) {
77
+ let designContext = `### 设计文件目录\n绝对路径: ${designDir}\n`;
78
+
79
+ const systemPenPath = path.join(designDir, 'system.lib.pen');
80
+ const isInit = !fs.existsSync(systemPenPath);
81
+ designContext += isInit
82
+ ? '### 设计库\n尚未创建 system.lib.pen,请先根据下方「初始化模板」生成。\n\n'
83
+ : '### 设计库\n已有 system.lib.pen,请先 Read 查看并复用。\n\n';
84
+
85
+ if (isInit) {
86
+ const initTemplate = assets.read('designInit') || '';
87
+ if (initTemplate) {
88
+ designContext += `### 初始化模板\n\n${initTemplate}\n\n`;
89
+ }
90
+ }
91
+
92
+ const mapPath = path.join(designDir, 'design_map.json');
93
+ if (fs.existsSync(mapPath)) {
94
+ try {
95
+ const map = JSON.parse(fs.readFileSync(mapPath, 'utf8'));
96
+ const pages = Object.entries(map.pages || {});
97
+ if (pages.length > 0) {
98
+ designContext += '### 已有页面\n';
99
+ for (const [name, info] of pages) {
100
+ designContext += `- **${name}**: ${info.description} (${path.join(designDir, info.pen)})\n`;
101
+ }
102
+ designContext += '\n';
103
+ }
104
+ } catch { /* ignore */ }
105
+ }
106
+
107
+ designContext += buildProjectContext();
108
+
109
+ return assets.render('designUser', {
110
+ designContext,
111
+ instruction: instruction
112
+ ? `用户需求:\n${instruction}`
113
+ : '用户未提供需求,使用对话模式收集。',
114
+ modeHint: instruction
115
+ ? '【自动模式】用户已提供需求,直接设计,不要提问。'
116
+ : '【对话模式】使用 AskUserQuestion 工具引导用户描述需求。',
117
+ });
118
+ }
119
+
120
+ function buildFixPrompt(designDir, userInput) {
121
+ const penFiles = scanPenFiles(designDir);
122
+ let designContext = '### 需要检查修复的 .pen 文件\n\n';
123
+ if (penFiles.length === 0) {
124
+ designContext += '(未发现 .pen 文件)\n';
125
+ } else {
126
+ for (const f of penFiles) {
127
+ designContext += `- ${f.abs}\n`;
128
+ }
129
+ }
130
+ designContext += '\n';
131
+
132
+ const instruction = userInput
133
+ ? `用户反馈的问题:\n${userInput}\n\n请 Read 每个文件,检查并修复所有不合规内容。`
134
+ : '请 Read 每个文件,检查并修复所有不合规内容。';
135
+
136
+ return assets.render('designFixUser', { designContext, instruction });
137
+ }
138
+
139
+ // ─── Post-session Summary ─────────────────────────────────
140
+
141
+ function showDesignSummary(designDir) {
142
+ const penFiles = scanPenFiles(designDir);
143
+ if (penFiles.length === 0) {
144
+ log('warn', '设计目录中没有 .pen 文件');
145
+ return 0;
146
+ }
147
+
148
+ console.log('');
149
+ console.log('┌─ 设计文件 ─────────────────────────────────────┐');
150
+ for (const f of penFiles) {
151
+ console.log(`│ ${f.rel.padEnd(52)}│`);
152
+ }
153
+ console.log('└───────────────────────────────────────────────┘');
154
+
155
+ const hasMap = fs.existsSync(path.join(designDir, 'design_map.json'));
156
+ if (hasMap) log('info', 'design_map.json OK');
157
+
158
+ let hasJsonError = false;
159
+ for (const f of penFiles) {
160
+ try {
161
+ JSON.parse(fs.readFileSync(f.abs, 'utf8'));
162
+ } catch (e) {
163
+ hasJsonError = true;
164
+ log('error', `${f.rel}: JSON 语法错误 — ${e.message}`);
165
+ }
166
+ }
167
+ if (hasJsonError) {
168
+ log('warn', '存在 JSON 格式问题,建议运行: claude-coder design --type fix');
169
+ }
170
+
171
+ return penFiles.length;
172
+ }
173
+
174
+ // ─── User Confirm ────────────────────────────────────────
175
+
176
+ function askUser(question) {
177
+ if (!process.stdin.isTTY) return Promise.resolve('');
178
+ const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
179
+ return new Promise(resolve => {
180
+ rl.question(question, answer => { rl.close(); resolve(answer.trim()); });
181
+ });
182
+ }
183
+
184
+ // ─── Fix Session ─────────────────────────────────────────
185
+
186
+ async function runFixSession(config, designDir, userInput, opts) {
187
+ const ts = new Date().toISOString().replace(/[-:T]/g, '').slice(0, 12);
188
+ log('info', '正在修复 .pen 文件...');
189
+
190
+ await Session.run('design', config, {
191
+ logFileName: `design_fix_${ts}.log`,
192
+ label: 'design_fix',
193
+ async execute(session) {
194
+ const queryOpts = session.buildQueryOptions(opts);
195
+ queryOpts.systemPrompt = buildSystemPrompt('designFix');
196
+ return await session.runQuery(buildFixPrompt(designDir, userInput), queryOpts);
197
+ },
198
+ });
199
+
200
+ log('ok', '修复完成');
201
+ showDesignSummary(designDir);
202
+ }
203
+
204
+ // ─── Main Entry ──────────────────────────────────────────
205
+
206
+ async function executeDesign(config, input, opts = {}) {
207
+ if (opts.reset) {
208
+ saveDesignState({});
209
+ log('ok', 'Design 状态已重置');
210
+ return;
211
+ }
212
+
213
+ const designDir = getDesignDir();
214
+ if (!fs.existsSync(designDir)) fs.mkdirSync(designDir, { recursive: true });
215
+ const pagesDir = path.join(designDir, 'pages');
216
+ if (!fs.existsSync(pagesDir)) fs.mkdirSync(pagesDir, { recursive: true });
217
+
218
+ const type = resolveType(opts, designDir);
219
+ const instruction = input || '';
220
+ const isAutoMode = !!instruction;
221
+ const designLabel = type === 'fix' ? '修复' : isAutoMode ? '自动' : '对话';
222
+ printModeBanner('design', `${type} · ${designLabel}`, config?.model);
223
+
224
+ if (!opts.model || !opts.model.includes('glm-5')) {
225
+ log('info', '提示: design 推荐使用 --model glm-5 获得最佳效果');
226
+ }
227
+
228
+ if (type === 'fix') {
229
+ const penFiles = scanPenFiles(designDir);
230
+ if (penFiles.length === 0) {
231
+ log('warn', '设计目录中没有 .pen 文件需要修复');
232
+ return;
233
+ }
234
+ const answer = await askUser(`\n发现 ${penFiles.length} 个 .pen 文件,是否进行修复?(Y/n) `);
235
+ if (answer.toLowerCase() === 'n') { log('info', '已取消'); return; }
236
+ await runFixSession(config, designDir, input, opts);
237
+ return;
238
+ }
239
+
240
+ const ts = new Date().toISOString().replace(/[-:T]/g, '').slice(0, 12);
241
+
242
+ const sessionResult = await Session.run('design', config, {
243
+ logFileName: `design_${ts}_1.log`,
244
+ label: isAutoMode ? 'design_auto' : 'design_dialogue',
245
+ async execute(session) {
246
+ const queryOpts = session.buildQueryOptions(opts);
247
+ queryOpts.systemPrompt = buildSystemPrompt('design');
248
+ return await session.runQuery(buildDesignPrompt(instruction, designDir), queryOpts);
249
+ },
250
+ });
251
+
252
+ if (sessionResult && !sessionResult.success) {
253
+ log('warn', 'AI 会话未正常完成,检查生成结果...');
254
+ }
255
+
256
+ const penCount = showDesignSummary(designDir);
257
+ if (penCount === 0) {
258
+ log('error', 'AI 未生成任何 .pen 文件');
259
+ return;
260
+ }
261
+
262
+ saveDesignState({ lastTimestamp: new Date().toISOString(), designDir, penCount, type });
263
+ log('ok', `设计完成! 文件: ${penCount}`);
264
+ log('info', '迭代调整: claude-coder design "修改xxx"');
265
+ log('info', '修复文件: claude-coder design --type fix');
266
+ }
267
+
268
+ module.exports = { executeDesign };
package/src/core/go.js CHANGED
@@ -4,7 +4,7 @@ const fs = require('fs');
4
4
  const path = require('path');
5
5
  const readline = require('readline');
6
6
  const { buildSystemPrompt } = require('./prompts');
7
- const { log } = require('../common/config');
7
+ const { log, printModeBanner } = require('../common/config');
8
8
  const { assets } = require('../common/assets');
9
9
  const { extractResultText } = require('../common/logging');
10
10
  const { loadState, saveState } = require('./state');
@@ -184,8 +184,8 @@ async function executeGo(config, input, opts = {}) {
184
184
  }
185
185
 
186
186
  const isAutoMode = !!(instruction || opts.reqFile);
187
- const mode = isAutoMode ? '自动' : '对话';
188
- log('info', `Go 模式: ${mode}`);
187
+ const mode = isAutoMode ? '自动模式' : '对话模式';
188
+ printModeBanner('go', mode, config?.model);
189
189
 
190
190
  const ts = new Date().toISOString().replace(/[-:T]/g, '').slice(0, 12);
191
191
 
package/src/core/hooks.js CHANGED
@@ -88,8 +88,10 @@ class GuidanceInjector {
88
88
 
89
89
  if (condition.field && condition.pattern !== undefined) {
90
90
  const value = this.getFieldValue(input, condition.field);
91
- const re = (ruleName && this._compiledConditions?.get(ruleName)) ||
92
- new RegExp(condition.pattern, 'i');
91
+ let re = ruleName && this._compiledConditions?.get(ruleName);
92
+ if (!re) {
93
+ try { re = new RegExp(condition.pattern, 'i'); } catch { return false; }
94
+ }
93
95
  return re.test(String(value || ''));
94
96
  }
95
97
 
@@ -113,28 +115,41 @@ class GuidanceInjector {
113
115
 
114
116
  /**
115
117
  * Get rule file content
118
+ * Uses assets module for template resolution (supports user overrides + bundled fallback)
116
119
  * @param {object|string} file - File config or path string
117
- * @param {string} basePath - Base directory for relative paths
118
120
  */
119
- getFileContent(file, basePath) {
121
+ getFileContent(file) {
120
122
  if (!file) return null;
121
123
 
122
124
  const filePath = typeof file === 'string' ? file : file.path;
123
- const absolutePath = path.isAbsolute(filePath)
124
- ? filePath
125
- : path.join(basePath, filePath);
126
125
 
127
- try {
128
- return fs.readFileSync(absolutePath, 'utf8');
129
- } catch {
130
- return null;
126
+ // Check if path matches a known registry entry (e.g., 'webTesting', 'bashProcess')
127
+ // by converting 'assets/web-testing.md' → ['other', 'web-testing.md']
128
+ if (filePath.startsWith('assets/')) {
129
+ const segments = filePath.replace(/^assets\//, '').split('/');
130
+ // Map to template directory structure: assets/web-testing.md → other/web-testing.md
131
+ if (segments.length === 1) {
132
+ segments.unshift('other');
133
+ }
134
+ const resolved = assets._resolveTemplate?.(segments);
135
+ if (resolved) {
136
+ try { return fs.readFileSync(resolved, 'utf8'); } catch { /* fall through */ }
137
+ }
138
+ }
139
+
140
+ // Fallback: try direct path resolution
141
+ const absolutePath = path.isAbsolute(filePath) ? filePath : assets.path(filePath);
142
+ if (absolutePath) {
143
+ try { return fs.readFileSync(absolutePath, 'utf8'); } catch { /* ignore */ }
131
144
  }
145
+
146
+ return null;
132
147
  }
133
148
 
134
149
  /**
135
150
  * Process a single rule and return guidance content
136
151
  */
137
- processRule(rule, input, basePath) {
152
+ processRule(rule, input) {
138
153
  const matcherRe = this._compiledMatchers?.get(rule.name) ?? new RegExp(rule.matcher);
139
154
  if (!matcherRe.test(input.tool_name)) {
140
155
  return null;
@@ -161,7 +176,7 @@ class GuidanceInjector {
161
176
  // Get cached content or read file
162
177
  const cacheKey = `${rule.name}_content`;
163
178
  if (!this.cache[cacheKey]) {
164
- this.cache[cacheKey] = this.getFileContent(fileConfig.path, basePath);
179
+ this.cache[cacheKey] = this.getFileContent(fileConfig.path);
165
180
  }
166
181
  result.guidance = this.cache[cacheKey] || '';
167
182
  }
@@ -198,8 +213,6 @@ class GuidanceInjector {
198
213
  * Create hook function for PreToolUse
199
214
  */
200
215
  createHook() {
201
- const basePath = assets.dir('loop');
202
-
203
216
  return async (input, _toolUseID, _context) => {
204
217
  this.load();
205
218
 
@@ -209,7 +222,7 @@ class GuidanceInjector {
209
222
  const tipParts = [];
210
223
 
211
224
  for (const rule of this.rules) {
212
- const result = this.processRule(rule, input, basePath);
225
+ const result = this.processRule(rule, input);
213
226
  if (result) {
214
227
  if (result.guidance) guidanceParts.push(result.guidance);
215
228
  if (result.tip) tipParts.push(result.tip);
@@ -394,6 +407,7 @@ const FEATURE_MAP = {
394
407
  add: [FEATURES.STOP, FEATURES.STALL],
395
408
  simplify: [FEATURES.STOP, FEATURES.STALL, FEATURES.INTERACTION],
396
409
  go: [FEATURES.STOP, FEATURES.STALL, FEATURES.INTERACTION],
410
+ design: [FEATURES.STOP, FEATURES.STALL, FEATURES.INTERACTION],
397
411
  custom: null
398
412
  };
399
413
 
package/src/core/init.js CHANGED
@@ -127,6 +127,15 @@ async function executeInit(config, opts = {}) {
127
127
  throw new Error('project_profile.json 读取失败或已损坏');
128
128
  }
129
129
 
130
+ const reqDest = path.join(projectRoot, 'requirements.md');
131
+ if (!fs.existsSync(reqDest)) {
132
+ const reqTemplate = assets.read('requirements');
133
+ if (reqTemplate) {
134
+ fs.writeFileSync(reqDest, reqTemplate, 'utf8');
135
+ log('ok', '已生成 requirements.md(需求模板)');
136
+ }
137
+ }
138
+
130
139
  if (opts.deployTemplates) {
131
140
  for (const file of assets.deployAll()) log('ok', `已部署 → .claude-coder/assets/${file}`);
132
141
  const recipes = assets.deployRecipes();
package/src/core/plan.js CHANGED
@@ -5,7 +5,7 @@ const path = require('path');
5
5
  const os = require('os');
6
6
  const readline = require('readline');
7
7
  const { buildSystemPrompt, buildPlanPrompt } = require('./prompts');
8
- const { log } = require('../common/config');
8
+ const { log, printModeBanner } = require('../common/config');
9
9
  const { assets } = require('../common/assets');
10
10
  const { printStats } = require('../common/tasks');
11
11
  const { syncAfterPlan } = require('./state');
@@ -26,7 +26,11 @@ function buildPlanOnlySystem(opts = {}) {
26
26
  2. ${interactionRule}
27
27
  3. 使用 Write 工具将完整计划写入 ~/.claude/plans/ 目录(.md 格式)
28
28
  4. 写入后输出标记(独占一行):PLAN_FILE_PATH: <计划文件绝对路径>
29
- 5. 简要总结计划要点`;
29
+ 5. 简要总结计划要点
30
+
31
+ 【关键文件】
32
+ - \`.claude-coder/project_profile.json\` — 项目元数据
33
+ - \`.claude-coder/design/\` — UI 设计稿目录(design_map.json 索引 + .pen 设计文件),存在时应在方案中参考`;
30
34
  }
31
35
 
32
36
  function buildPlanOnlyPrompt(instruction, opts = {}) {
@@ -97,17 +101,19 @@ async function _executePlanGen(session, instruction, opts = {}) {
97
101
  return { success: false, reason: 'no_path', targetPath: null };
98
102
  }
99
103
 
100
- async function promptAutoRun() {
101
- if (!process.stdin.isTTY) return false;
104
+ function _askLine(question) {
105
+ if (!process.stdin.isTTY) return Promise.resolve('');
102
106
  const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
103
107
  return new Promise(resolve => {
104
- rl.question('任务分解完成后是否自动开始执行?(y/n) ', answer => {
105
- rl.close();
106
- resolve(/^[Yy]/.test(answer.trim()));
107
- });
108
+ rl.question(question, answer => { rl.close(); resolve(answer.trim()); });
108
109
  });
109
110
  }
110
111
 
112
+ async function promptAutoRun() {
113
+ const answer = await _askLine('任务分解完成后是否自动开始执行?(y/n) ');
114
+ return /^[Yy]/.test(answer);
115
+ }
116
+
111
117
  // ─── Main Entry ──────────────────────────────────────────
112
118
 
113
119
  async function executePlan(config, input, opts = {}) {
@@ -123,9 +129,8 @@ async function executePlan(config, input, opts = {}) {
123
129
  throw new Error('用法: claude-coder plan "需求内容" 或 claude-coder plan -r [requirements.md]');
124
130
  }
125
131
 
126
- if (opts.interactive) {
127
- log('info', '交互模式已启用,模型可能会向您提问');
128
- }
132
+ const modeLabel = opts.planOnly ? 'planOnly' : opts.interactive ? '交互模式' : '自动模式';
133
+ printModeBanner('plan', modeLabel, config?.model);
129
134
 
130
135
  let shouldAutoRun = false;
131
136
  if (!opts.planOnly) {
@@ -146,7 +151,7 @@ async function executePlan(config, input, opts = {}) {
146
151
  const planResult = await _executePlanGen(session, instruction, opts);
147
152
 
148
153
  if (!planResult.success) {
149
- log('error', `\n计划生成失败: ${planResult.reason || planResult.error}`);
154
+ log('error', `\n计划生成失败: ${planResult.reason}`);
150
155
  return { success: false, reason: planResult.reason };
151
156
  }
152
157
 
@@ -162,9 +167,10 @@ async function executePlan(config, input, opts = {}) {
162
167
  const queryOpts = session.buildQueryOptions(opts);
163
168
  queryOpts.systemPrompt = buildSystemPrompt('plan');
164
169
 
165
- const { success } = await session.runQuery(tasksPrompt, queryOpts);
170
+ const { success } = await session.runQuery(tasksPrompt, queryOpts, { continue: true });
166
171
  if (!success) {
167
172
  log('warn', '任务分解查询未正常结束');
173
+ return { success: false, reason: '任务分解未正常完成' };
168
174
  }
169
175
 
170
176
  syncAfterPlan();
@@ -178,9 +184,9 @@ async function executePlan(config, input, opts = {}) {
178
184
 
179
185
  if (shouldAutoRun) {
180
186
  console.log('');
181
- log('info', '开始自动执行任务...');
187
+ log('info', '开始自动执行任务(沿用会话上下文)...');
182
188
  const { executeRun } = require('./runner');
183
- await executeRun(config, opts);
189
+ await executeRun(config, { ...opts });
184
190
  }
185
191
  }
186
192
  }
@@ -17,6 +17,12 @@ function buildSystemPrompt(type) {
17
17
  case 'coding': specific = assets.read('codingSystem') || ''; break;
18
18
  case 'plan': specific = assets.read('planSystem') || ''; break;
19
19
  case 'go': specific = assets.read('goSystem') || ''; break;
20
+ case 'design': specific = assets.read('designSystem') || ''; break;
21
+ case 'designFix': specific = assets.read('designFixSystem') || ''; break;
22
+ }
23
+ if (type === 'design' || type === 'designFix') {
24
+ const base = assets.read('designBase') || '';
25
+ return base ? `${specific}\n\n---\n\n${base}` : specific;
20
26
  }
21
27
  return specific ? `${specific}\n\n${core}` : core;
22
28
  }
@@ -156,6 +162,27 @@ function buildServiceHint(maxSessions) {
156
162
  : '连续模式:收尾时不要停止后台服务,保持服务运行以便下个 session 继续使用。';
157
163
  }
158
164
 
165
+ // --------------- Design Hint ---------------
166
+
167
+ function buildDesignHint(task) {
168
+ if (!needsWebTools(task)) return '';
169
+ if (!assets.exists('designMap')) return '';
170
+
171
+ const map = assets.readJson('designMap', null);
172
+ if (!map || !map.pages) return '';
173
+
174
+ const pages = Object.entries(map.pages);
175
+ if (pages.length === 0) return '';
176
+
177
+ const designDir = assets.dir('design');
178
+ let hint = `【设计稿参考】项目已有 UI 设计稿(design_map.json: ${assets.path('designMap')}):\n`;
179
+ for (const [name, info] of pages) {
180
+ hint += ` - ${name}: ${info.description || ''} → ${path.join(designDir, info.pen)}\n`;
181
+ }
182
+ hint += '涉及 UI 的任务可先 Read 对应 .pen 文件了解设计意图。';
183
+ return hint;
184
+ }
185
+
159
186
  // --------------- Context Builders ---------------
160
187
 
161
188
  function _resolveTask(taskId) {
@@ -187,6 +214,7 @@ function buildCodingContext(sessionNum, opts = {}) {
187
214
  webTestHint: buildWebTestHint(config, task),
188
215
  memoryHint: buildMemoryHint(),
189
216
  serviceHint: buildServiceHint(opts.maxSessions || 50),
217
+ designHint: buildDesignHint(task),
190
218
  });
191
219
  }
192
220
 
@@ -223,17 +251,34 @@ function buildPlanPrompt(planPath) {
223
251
 
224
252
  let testRuleHint = '';
225
253
  if (assets.exists('testRule') && assets.exists('mcpConfig')) {
254
+ const testRulePath = assets.path('testRule');
226
255
  testRuleHint = '【浏览器测试规则】项目已配置浏览器测试工具(.mcp.json),' +
227
- '`.claude-coder/assets/test_rule.md` 包含测试规范(Smart Snapshot、等待策略、步骤模板等)。' +
228
- '前端页面 test 类任务 steps 首步加入 `【规则】阅读 .claude-coder/assets/test_rule.md`。';
256
+ `\`${testRulePath}\` 包含测试规范(Smart Snapshot、等待策略、步骤模板等)。` +
257
+ `前端页面 test 类任务 steps 首步加入 \`【规则】阅读 ${testRulePath}\`。`;
229
258
  }
230
259
 
260
+ let designHint = '';
261
+ try {
262
+ if (assets.exists('designMap')) {
263
+ const map = assets.readJson('designMap', null);
264
+ if (map?.pages && Object.keys(map.pages).length > 0) {
265
+ const designDir = assets.dir('design');
266
+ designHint = `【设计稿】项目已有 UI 设计稿(${assets.path('designMap')}):\n`;
267
+ for (const [name, info] of Object.entries(map.pages)) {
268
+ designHint += ` - ${name}: ${info.description || ''} → ${path.join(designDir, info.pen)}\n`;
269
+ }
270
+ designHint += '涉及 UI 的 task steps 应包含「Read 对应 .pen 设计稿」步骤。';
271
+ }
272
+ }
273
+ } catch { /* ignore */ }
274
+
231
275
  return assets.render('planUser', {
232
276
  taskContext,
233
277
  recentExamples,
234
278
  projectRoot,
235
279
  planPath,
236
280
  testRuleHint,
281
+ designHint,
237
282
  });
238
283
  }
239
284
 
@@ -23,7 +23,7 @@ async function executeRepair(config, filePath, opts = {}) {
23
23
 
24
24
  async execute(session) {
25
25
  const queryOpts = session.buildQueryOptions(opts);
26
- await session.runQuery(prompt, queryOpts);
26
+ await session.runQuery(prompt, queryOpts, { continue: true });
27
27
  log('ok', `AI 修复 ${fileName} 完成`);
28
28
  return {};
29
29
  },