@vima_tech/telos 1.4.0 → 1.4.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/bin/telos.js CHANGED
@@ -25,6 +25,9 @@ function runScript(scriptPath, args, extraEnv = {}) {
25
25
  stdio: 'inherit',
26
26
  env,
27
27
  });
28
+ child.on('error', (err) => {
29
+ reject(new Error('无法启动 bash: ' + err.message));
30
+ });
28
31
  child.on('exit', (code) => {
29
32
  if (code === 0) resolve(0);
30
33
  else reject(new Error('exit code ' + code));
@@ -36,36 +39,56 @@ function downloadFile(url, destPath) {
36
39
  return new Promise((resolve, reject) => {
37
40
  const args = ['-fsSL', url, '-o', destPath];
38
41
  if (proxy) args.splice(1, 0, '--proxy', proxy);
39
- spawn('curl', args, { stdio: 'ignore' })
40
- .on('exit', (code) => {
41
- if (code === 0) resolve(0);
42
- else reject(new Error('curl failed'));
43
- });
42
+ const child = spawn('curl', args, { stdio: 'ignore' });
43
+ child.on('error', (err) => {
44
+ reject(new Error('无法启动 curl: ' + err.message));
45
+ });
46
+ child.on('exit', (code) => {
47
+ if (code === 0) resolve(0);
48
+ else reject(new Error('curl failed with code ' + code));
49
+ });
44
50
  });
45
51
  }
46
52
 
47
53
  async function initKernel(dir, skill) {
48
54
  const baseUrl = 'https://raw.githubusercontent.com/renmengkai/telos/main';
49
55
 
56
+ // 预先检查目标目录写权限
57
+ try {
58
+ const testFile = path.join(dir, '.telos-write-test');
59
+ fs.writeFileSync(testFile, '');
60
+ fs.unlinkSync(testFile);
61
+ } catch (e) {
62
+ console.error('❌ 目录无写权限: ' + dir);
63
+ console.error(' 请检查目录权限,或使用有写入权限的路径。');
64
+ throw e;
65
+ }
66
+
50
67
  const kernelFiles = [
51
68
  'CLAUDE.md', '.gitignore',
52
- '.claude/commands/telos.md', '.claude/commands/skill.md', '.claude/commands/add-skill.md',
69
+ '.claude/commands/telos.md', '.claude/commands/skill.md', '.claude/commands/add-skill.md', '.claude/commands/codeword.md',
70
+ '.opencode/commands/telos.md', '.opencode/commands/skill.md', '.opencode/commands/add-skill.md', '.opencode/commands/codeword.md',
53
71
  'scripts/auto-distill.sh', 'scripts/feedback-hook.sh',
54
72
  'scripts/bridge-to-coder.sh', 'scripts/telos-install.sh',
55
- 'templates/state.json',
56
- 'skills/_kernel/distillation.md', 'skills/_kernel/skill-extraction.md',
73
+ 'templates/state.json', 'templates/user-config.json',
74
+ 'skills/_kernel/distillation.md', 'skills/_kernel/skill-extraction.md', 'skills/_kernel/codewords.md',
57
75
  'skills/_template/skill.yaml', 'skills/_template/domain.md',
58
76
  'skills/_template/feedback-questions.sh',
59
77
  ];
60
78
 
61
79
  for (const f of kernelFiles) {
62
80
  const dest = path.join(dir, f);
63
- fs.mkdirSync(path.dirname(dest), { recursive: true });
64
- const srcPath = resolveKernelFile(f);
65
- if (fs.existsSync(srcPath)) {
66
- fs.copyFileSync(srcPath, dest);
67
- } else {
68
- await downloadFile(baseUrl + '/' + f, dest);
81
+ try {
82
+ fs.mkdirSync(path.dirname(dest), { recursive: true });
83
+ const srcPath = resolveKernelFile(f);
84
+ if (fs.existsSync(srcPath)) {
85
+ fs.copyFileSync(srcPath, dest);
86
+ } else {
87
+ await downloadFile(baseUrl + '/' + f, dest);
88
+ }
89
+ } catch (e) {
90
+ console.error(' ✗ 写入失败: ' + f + ' (' + e.message + ')');
91
+ throw e;
69
92
  }
70
93
  }
71
94
 
@@ -82,23 +105,32 @@ async function initKernel(dir, skill) {
82
105
 
83
106
  for (const f of skillRequired) {
84
107
  const dest = path.join(dir, f);
85
- fs.mkdirSync(path.dirname(dest), { recursive: true });
86
- const srcPath = resolveKernelFile(f);
87
- if (fs.existsSync(srcPath)) {
88
- fs.copyFileSync(srcPath, dest);
89
- } else {
90
- await downloadFile(baseUrl + '/' + f, dest);
108
+ try {
109
+ fs.mkdirSync(path.dirname(dest), { recursive: true });
110
+ const srcPath = resolveKernelFile(f);
111
+ if (fs.existsSync(srcPath)) {
112
+ fs.copyFileSync(srcPath, dest);
113
+ } else {
114
+ await downloadFile(baseUrl + '/' + f, dest);
115
+ }
116
+ } catch (e) {
117
+ console.error(' ✗ Skill 文件写入失败: ' + f + ' (' + e.message + ')');
118
+ throw e;
91
119
  }
92
120
  }
93
121
 
94
122
  for (const f of skillOptional) {
95
123
  const dest = path.join(dir, f);
96
- fs.mkdirSync(path.dirname(dest), { recursive: true });
97
- const srcPath = resolveKernelFile(f);
98
- if (fs.existsSync(srcPath)) {
99
- fs.copyFileSync(srcPath, dest);
100
- } else {
101
- try { await downloadFile(baseUrl + '/' + f, dest); } catch (e) {}
124
+ try {
125
+ fs.mkdirSync(path.dirname(dest), { recursive: true });
126
+ const srcPath = resolveKernelFile(f);
127
+ if (fs.existsSync(srcPath)) {
128
+ fs.copyFileSync(srcPath, dest);
129
+ } else {
130
+ try { await downloadFile(baseUrl + '/' + f, dest); } catch (e) {}
131
+ }
132
+ } catch (e) {
133
+ console.error(' ⚠ 可选文件写入失败(已跳过): ' + f);
102
134
  }
103
135
  }
104
136
 
@@ -109,36 +141,68 @@ async function initKernel(dir, skill) {
109
141
  for (const match of yamlContent.matchAll(/^\s+file:\s+(.+)$/gm)) {
110
142
  const f = match[1].trim();
111
143
  const dest = path.join(dir, f);
112
- fs.mkdirSync(path.dirname(dest), { recursive: true });
113
- const srcPath = resolveKernelFile(f);
114
- if (fs.existsSync(srcPath)) {
115
- fs.copyFileSync(srcPath, dest);
116
- } else {
117
- try { await downloadFile(baseUrl + '/' + f, dest); } catch (e) {}
144
+ try {
145
+ fs.mkdirSync(path.dirname(dest), { recursive: true });
146
+ const srcPath = resolveKernelFile(f);
147
+ if (fs.existsSync(srcPath)) {
148
+ fs.copyFileSync(srcPath, dest);
149
+ } else {
150
+ try { await downloadFile(baseUrl + '/' + f, dest); } catch (e) {}
151
+ }
152
+ } catch (e) {
153
+ console.error(' ⚠ 行业包写入失败(已跳过): ' + f);
118
154
  }
119
155
  }
120
156
  }
121
157
  }
122
158
 
123
- fs.mkdirSync(path.join(dir, '.distill-needed'), { recursive: true });
124
- fs.mkdirSync(path.join(dir, 'episodic-logs'), { recursive: true });
125
- fs.mkdirSync(path.join(dir, 'projects'), { recursive: true });
126
- fs.writeFileSync(path.join(dir, '.distill-needed/.gitkeep'), '');
127
- fs.writeFileSync(path.join(dir, 'episodic-logs/.gitkeep'), '');
128
- fs.writeFileSync(path.join(dir, 'projects/.gitkeep'), '');
159
+ try {
160
+ fs.mkdirSync(path.join(dir, '.distill-needed'), { recursive: true });
161
+ fs.mkdirSync(path.join(dir, 'episodic-logs'), { recursive: true });
162
+ fs.mkdirSync(path.join(dir, 'projects'), { recursive: true });
163
+ fs.writeFileSync(path.join(dir, '.distill-needed/.gitkeep'), '');
164
+ fs.writeFileSync(path.join(dir, 'episodic-logs/.gitkeep'), '');
165
+ fs.writeFileSync(path.join(dir, 'projects/.gitkeep'), '');
166
+ } catch (e) {
167
+ console.error(' ✗ 运行时目录创建失败: ' + e.message);
168
+ throw e;
169
+ }
170
+
171
+ // 复制用户配置模板(如果不存在)
172
+ try {
173
+ const configSrc = resolveKernelFile('templates/user-config.json');
174
+ const configDest = path.join(dir, '.telos-config.json');
175
+ if (fs.existsSync(configSrc) && !fs.existsSync(configDest)) {
176
+ fs.copyFileSync(configSrc, configDest);
177
+ }
178
+ } catch (e) {
179
+ console.error(' ⚠ 配置文件复制失败(不影响使用): ' + e.message);
180
+ }
129
181
 
130
- if (!fs.existsSync(path.join(dir, '.gitignore'))) {
131
- fs.writeFileSync(path.join(dir, '.gitignore'), `# telos-managed\nprojects/*/\nepisodic-logs/\n.distill-needed/*\n!.distill-needed/.gitkeep\n*.log\n`);
182
+ try {
183
+ if (!fs.existsSync(path.join(dir, '.gitignore'))) {
184
+ fs.writeFileSync(path.join(dir, '.gitignore'), `# telos-managed\nprojects/*/\nepisodic-logs/\n.distill-needed/*\n!.distill-needed/.gitkeep\n*.log\n`);
185
+ }
186
+ } catch (e) {
187
+ console.error(' ⚠ .gitignore 写入失败(不影响使用): ' + e.message);
132
188
  }
133
189
 
134
190
  fs.writeFileSync(path.join(dir, '.telos'), JSON.stringify({
135
- version: '1.4.0',
191
+ version: '1.4.1',
136
192
  created: new Date().toISOString(),
137
193
  skill: skill || 'none'
138
194
  }));
139
195
 
140
196
  try {
141
- execSync('chmod +x ' + path.join(dir, 'scripts/*.sh'), { stdio: 'ignore', shell: true });
197
+ const scriptsDir = path.join(dir, 'scripts');
198
+ if (fs.existsSync(scriptsDir)) {
199
+ const files = fs.readdirSync(scriptsDir);
200
+ for (const f of files) {
201
+ if (f.endsWith('.sh')) {
202
+ fs.chmodSync(path.join(scriptsDir, f), 0o755);
203
+ }
204
+ }
205
+ }
142
206
  } catch (e) {}
143
207
  }
144
208
 
@@ -156,6 +220,15 @@ async function cmdNew(args) {
156
220
  process.exit(1);
157
221
  }
158
222
 
223
+ // 检查当前目录写权限
224
+ try {
225
+ fs.accessSync(process.cwd(), fs.constants.W_OK);
226
+ } catch (e) {
227
+ console.error('❌ 当前目录无写权限: ' + process.cwd());
228
+ console.error(' 请切换到可写入的目录,或使用 sudo。');
229
+ process.exit(1);
230
+ }
231
+
159
232
  let skill = '';
160
233
 
161
234
  for (let i = 1; i < args.length; i++) {
@@ -190,21 +263,53 @@ async function cmdNew(args) {
190
263
  console.log('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━');
191
264
  console.log(' ✅ 项目创建完成!');
192
265
  console.log('');
193
- console.log(' cd ' + projectName + ' && claude');
266
+ const detected = detectCodingAgent();
267
+ const startCmd = detected === 'trae' || detected === 'cursor'
268
+ ? 'cd ' + projectName + ' && telos start'
269
+ : 'cd ' + projectName + ' && ' + (detected || 'claude');
270
+ console.log(' ' + startCmd);
194
271
  console.log(' 然后输入 tos 启动 Telos');
195
272
  console.log('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━');
196
273
  }
197
274
 
198
- function cmdStart() {
199
- const detected = detectCodingAgent();
200
- if (!detected) {
201
- console.error('❌ 未检测到 Claude Code 或 OpenCode');
202
- console.error('请先安装 Claude Code 或 OpenCode');
203
- process.exit(1);
275
+ function cmdStart(args) {
276
+ const requested = args && args[0];
277
+ let agent;
278
+
279
+ if (requested) {
280
+ const isWin = process.platform === 'win32';
281
+ const cmd = isWin ? `where ${requested}` : `command -v ${requested}`;
282
+ try {
283
+ execSync(cmd, { stdio: 'ignore', shell: true });
284
+ agent = requested;
285
+ } catch (e) {
286
+ console.error('❌ 未找到命令: ' + requested);
287
+ process.exit(1);
288
+ }
289
+ } else {
290
+ agent = detectCodingAgent();
291
+ if (!agent) {
292
+ console.error('❌ 未检测到支持的 AI 编程工具');
293
+ console.error('');
294
+ console.error('Telos 支持以下工具(安装任意一个即可):');
295
+ console.error(' • Claude Code https://claude.ai/code');
296
+ console.error(' • OpenCode https://github.com/opencode-ai/opencode');
297
+ console.error(' • Codex (OpenAI) https://github.com/openai/codex');
298
+ console.error(' • Trae https://www.trae.ai');
299
+ console.error(' • Cursor https://cursor.com');
300
+ console.error('');
301
+ console.error('安装后重新运行 telos start');
302
+ process.exit(1);
303
+ }
204
304
  }
205
305
 
206
- console.log('🚀 启动 ' + detected + '...');
207
- spawn(detected, [], {
306
+ console.log('🚀 启动 ' + agent + '...');
307
+
308
+ // trae/cursor 是 GUI 编辑器,需要传入目录参数
309
+ const guiEditors = ['trae', 'cursor'];
310
+ const spawnArgs = guiEditors.includes(agent) ? ['.'] : [];
311
+
312
+ spawn(agent, spawnArgs, {
208
313
  cwd: process.cwd(),
209
314
  stdio: 'inherit',
210
315
  shell: true,
@@ -213,10 +318,15 @@ function cmdStart() {
213
318
  }
214
319
 
215
320
  function detectCodingAgent() {
216
- const agents = ['claude', 'claude-code', 'opencode', 'codex'];
321
+ // 优先级:claude > claude-code > codex > opencode > trae > cursor
322
+ // 注意:trae/cursor 是 GUI 编辑器,通常需要 `.` 参数启动目录
323
+ const agents = ['claude', 'claude-code', 'codex', 'opencode', 'trae', 'cursor'];
217
324
  for (const a of agents) {
218
325
  try {
219
- execSync('which ' + a, { stdio: 'ignore' });
326
+ // 跨平台检测:优先使用 command -v(POSIX),回退到 where(Windows)
327
+ const isWin = process.platform === 'win32';
328
+ const cmd = isWin ? `where ${a}` : `command -v ${a}`;
329
+ execSync(cmd, { stdio: 'ignore', shell: true });
220
330
  return a;
221
331
  } catch (e) {}
222
332
  }
@@ -255,10 +365,11 @@ async function updateProjectFramework(projectPath) {
255
365
 
256
366
  const kernelFiles = [
257
367
  'CLAUDE.md',
258
- '.claude/commands/telos.md', '.claude/commands/skill.md', '.claude/commands/add-skill.md',
368
+ '.claude/commands/telos.md', '.claude/commands/skill.md', '.claude/commands/add-skill.md', '.claude/commands/codeword.md',
369
+ '.opencode/commands/telos.md', '.opencode/commands/skill.md', '.opencode/commands/add-skill.md', '.opencode/commands/codeword.md',
259
370
  'scripts/auto-distill.sh', 'scripts/feedback-hook.sh',
260
371
  'scripts/bridge-to-coder.sh', 'scripts/telos-install.sh',
261
- 'skills/_kernel/distillation.md', 'skills/_kernel/skill-extraction.md',
372
+ 'skills/_kernel/distillation.md', 'skills/_kernel/skill-extraction.md', 'skills/_kernel/codewords.md',
262
373
  'skills/_template/skill.yaml', 'skills/_template/domain.md',
263
374
  'skills/_template/feedback-questions.sh',
264
375
  ];
@@ -375,8 +486,13 @@ const rawArgs = process.argv.slice(2);
375
486
  const filteredArgs = [];
376
487
  for (let i = 0; i < rawArgs.length; i++) {
377
488
  if (rawArgs[i] === '-x' || rawArgs[i] === '--proxy') {
378
- proxy = rawArgs[i + 1];
379
- i++;
489
+ if (i + 1 < rawArgs.length && !rawArgs[i + 1].startsWith('-')) {
490
+ proxy = rawArgs[i + 1];
491
+ i++;
492
+ } else {
493
+ console.error('❌ --proxy 需要一个值,例如:--proxy http://127.0.0.1:7890');
494
+ process.exit(1);
495
+ }
380
496
  } else {
381
497
  filteredArgs.push(rawArgs[i]);
382
498
  }
@@ -424,7 +540,7 @@ switch (command) {
424
540
  case '--version':
425
541
  case '-V':
426
542
  case 'version':
427
- console.log('@vima_tech/telos v1.4.0');
543
+ console.log('@vima_tech/telos v1.4.1');
428
544
  break;
429
545
  case 'new':
430
546
  case 'create':
@@ -434,7 +550,7 @@ switch (command) {
434
550
  case 'start':
435
551
  case 'run':
436
552
  case 'launch':
437
- cmdStart();
553
+ cmdStart(restArgs);
438
554
  break;
439
555
  case '-h':
440
556
  case '--help':
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@vima_tech/telos",
3
- "version": "1.4.0",
3
+ "version": "1.4.1",
4
4
  "description": "对话驱动的个人能力操作系统 — 每次对话都让你变得更懂行,技能跨会话永久积累",
5
5
  "main": "bin/telos.js",
6
6
  "bin": {
@@ -13,7 +13,9 @@
13
13
  "templates/",
14
14
  "skills/_kernel/",
15
15
  "skills/_template/",
16
- "skills/req-mining/"
16
+ "skills/req-mining/",
17
+ ".claude/commands/",
18
+ "CLAUDE.md"
17
19
  ],
18
20
  "scripts": {
19
21
  "postinstall": "chmod +x bin/telos.js"
@@ -40,16 +40,17 @@ check_project() {
40
40
  [ -f "$log_file" ] || return 1
41
41
 
42
42
  local pending
43
- pending=$(python3 - << PYEOF
44
- import json
43
+ AD_LOG_FILE="$log_file" pending=$(python3 - << 'PYEOF'
44
+ import json, os
45
+ log_file = os.environ.get("AD_LOG_FILE", "")
45
46
  try:
46
- with open("$log_file") as f:
47
+ with open(log_file) as f:
47
48
  events = [json.loads(l) for l in f if l.strip()]
48
49
  pending = sum(1 for e in events
49
50
  if e.get("distillation_candidate") and not e.get("distilled")
50
51
  and e.get("quality_score", 0) >= 0.6)
51
52
  print(pending)
52
- except:
53
+ except Exception as e:
53
54
  print(0)
54
55
  PYEOF
55
56
  )
@@ -62,19 +63,22 @@ report_status() {
62
63
  local has_marker=""
63
64
  [ -f "$DISTILL_NEEDED_DIR/$project_id" ] && has_marker=" [标记文件]"
64
65
 
65
- python3 - << PYEOF
66
- import json
66
+ AD_LOG_FILE="$log_file" AD_PROJECT_ID="$project_id" AD_HAS_MARKER="$has_marker" python3 - << 'PYEOF'
67
+ import json, os
68
+ log_file = os.environ.get("AD_LOG_FILE", "")
69
+ project_id = os.environ.get("AD_PROJECT_ID", "")
70
+ has_marker = os.environ.get("AD_HAS_MARKER", "")
67
71
  try:
68
- with open("$log_file") as f:
72
+ with open(log_file) as f:
69
73
  events = [json.loads(l) for l in f if l.strip()]
70
74
  total = len(events)
71
75
  pending = sum(1 for e in events
72
76
  if e.get("distillation_candidate") and not e.get("distilled")
73
77
  and e.get("quality_score", 0) >= 0.6)
74
78
  distilled = sum(1 for e in events if e.get("distilled"))
75
- print(f" 项目 $project_id$has_marker: 总事件={total}, 待蒸馏={pending}, 已蒸馏={distilled}")
76
- except:
77
- print(" 项目 $project_id$has_marker: 无日志文件(由标记文件触发)")
79
+ print(f" 项目 {project_id}{has_marker}: 总事件={total}, 待蒸馏={pending}, 已蒸馏={distilled}")
80
+ except Exception:
81
+ print(f" 项目 {project_id}{has_marker}: 无日志文件(由标记文件触发)")
78
82
  PYEOF
79
83
  }
80
84
 
@@ -138,7 +142,16 @@ for p in "${needs_distill[@]}"; do
138
142
  done
139
143
  echo ""
140
144
  echo "操作方式:"
141
- echo " 1. 打开 Telos 会话(cd $REPO_DIR && claude)"
145
+ # 检测可用的 AI 编程工具并提示
146
+ if command -v claude &>/dev/null; then
147
+ echo " 1. 打开 Telos 会话(cd $REPO_DIR && claude)"
148
+ elif command -v codex &>/dev/null; then
149
+ echo " 1. 打开 Telos 会话(cd $REPO_DIR && codex)"
150
+ elif command -v opencode &>/dev/null; then
151
+ echo " 1. 打开 Telos 会话(cd $REPO_DIR && opencode)"
152
+ else
153
+ echo " 1. 打开 Telos 会话(cd $REPO_DIR && <你的 AI 编程工具>)"
154
+ fi
142
155
  echo " 2. 系统将自动检测待蒸馏标记并启动蒸馏"
143
156
  echo ""
144
157
 
@@ -37,39 +37,58 @@ mkdir -p "$IMPL_DIR"
37
37
  # ── 注入 AI 执行文档为编程工具上下文 ──────────────────────────
38
38
  PROJECT_NAME=$(python3 -c "import json,sys; d=json.load(open('$STATE_FILE')); print(d.get('project_name','未命名项目'))" 2>/dev/null || echo "未命名项目")
39
39
 
40
- cat > "$IMPL_DIR/CLAUDE.md" << HEREDOC
41
- # $PROJECT_NAME — AI 执行规格
40
+ export BC_PROJECT_NAME="$PROJECT_NAME"
41
+ export BC_AI_DOC="$AI_DOC"
42
+ export BC_IMPL_DIR="$IMPL_DIR"
43
+
44
+ # 使用 python3 安全拼接文件内容,避免 bash heredoc 展开特殊字符
45
+ python3 - << 'PYEOF'
46
+ import os
47
+
48
+ project_name = os.environ.get("BC_PROJECT_NAME", "未命名项目")
49
+ ai_doc_path = os.environ.get("BC_AI_DOC", "")
50
+ impl_dir = os.environ.get("BC_IMPL_DIR", "")
51
+
52
+ with open(ai_doc_path, "r", encoding="utf-8") as f:
53
+ ai_doc_content = f.read()
54
+
55
+ content = f"""# {project_name} — AI 执行规格
42
56
 
43
57
  > 本文档由 Telos Skill 分析自动生成,是唯一权威需求来源。
44
58
  > **遇到本文档未说明的情况,必须停止并记录,不得自行决定。**
45
59
 
46
60
  ---
47
61
 
48
- $(cat "$AI_DOC")
62
+ {ai_doc_content}
49
63
 
50
64
  ---
51
65
 
52
66
  ## 反馈协议(实现完成后必须执行)
53
67
 
54
- 实现过程中如遇到以下情况,请记录到 \`.telos-feedback.json\`:
68
+ 实现过程中如遇到以下情况,请记录到 `.telos-feedback.json`:
55
69
  - 需求描述不清晰或有歧义
56
70
  - 遇到文档未覆盖的技术决策
57
71
  - 发现需求假设与现实不符
58
72
  - 验收标准无法被测试
59
73
 
60
74
  记录格式:
61
- \`\`\`json
62
- {
75
+ ```json
76
+ {{
63
77
  "unclear_specs": ["具体描述哪条规格不清晰"],
64
78
  "uncovered_decisions": ["遇到的未覆盖决策"],
65
79
  "wrong_assumptions": ["发现的错误假设"],
66
80
  "untestable_criteria": ["无法测试的验收标准"],
67
81
  "general_feedback": "总体反馈和质量评分(1-10)"
68
- }
69
- \`\`\`
82
+ }}
83
+ ```
70
84
 
71
85
  **重要:** 实现完成后请写入上述文件。退出编程工具后,飞轮系统会自动读取该文件收集反馈,无需手动运行任何命令。
72
- HEREDOC
86
+ """
87
+
88
+ claude_md_path = os.path.join(impl_dir, "CLAUDE.md")
89
+ with open(claude_md_path, "w", encoding="utf-8") as f:
90
+ f.write(content)
91
+ PYEOF
73
92
 
74
93
  echo "✅ CLAUDE.md 已生成"
75
94
 
@@ -135,5 +154,21 @@ if [ -f "$DISTILL_MARKER" ]; then
135
154
  echo ""
136
155
  sleep 1
137
156
  cd "$REPO_DIR"
138
- exec claude
157
+ # 优先使用与桥接时相同的工具,否则检测可用的 AI 编程工具
158
+ # trae/cursor 是 GUI 编辑器,需要传 . 指定目录
159
+ if command -v "$TOOL" &>/dev/null; then
160
+ case "$TOOL" in
161
+ trae|cursor) exec "$TOOL" . ;;
162
+ *) exec "$TOOL" ;;
163
+ esac
164
+ elif command -v claude &>/dev/null; then
165
+ exec claude
166
+ elif command -v codex &>/dev/null; then
167
+ exec codex
168
+ elif command -v opencode &>/dev/null; then
169
+ exec opencode
170
+ else
171
+ echo "⚠️ 未检测到可用的 AI 编程工具,请手动进入目录运行蒸馏:"
172
+ echo " cd $REPO_DIR && claude # 或 codex / opencode"
173
+ fi
139
174
  fi
@@ -81,26 +81,43 @@ if ! $AUTO_MODE; then
81
81
  fi
82
82
 
83
83
  # ── 合并反馈并生成事件 ────────────────────────────────────────
84
- PENDING=$(python3 - << PYEOF
85
- import json, os
84
+ # 使用环境变量传递数据,避免 shell 字符串注入 Python 代码
85
+ export FE_H_EVENT_ID="$EVENT_ID"
86
+ export FE_H_PROJECT_ID="$PROJECT_ID"
87
+ export FE_H_TIMESTAMP="$TIMESTAMP"
88
+ export FE_H_IMPL_DIR="$IMPL_DIR"
89
+ export FE_H_LOG_FILE="$LOG_FILE"
90
+ export FE_H_THRESHOLD="$THRESHOLD"
91
+ export FE_H_FEEDBACK_JSON="$FEEDBACK_JSON"
92
+ export FE_H_UNCLEAR="$UNCLEAR"
93
+ export FE_H_UNCOVERED="$UNCOVERED"
94
+ export FE_H_WRONG_ASSUMPTIONS="$WRONG_ASSUMPTIONS"
95
+ export FE_H_UNTESTABLE="$UNTESTABLE"
96
+ export FE_H_GENERAL="$GENERAL"
97
+
98
+ PENDING=$(python3 - << 'PYEOF'
99
+ import json, os, sys
100
+
101
+ def get_env(k, default=""):
102
+ return os.environ.get(k, default)
86
103
 
87
104
  try:
88
- existing = json.loads('''$FEEDBACK_JSON''')
105
+ existing = json.loads(get_env("FE_H_FEEDBACK_JSON", "{}"))
89
106
  except:
90
107
  existing = {}
91
108
 
92
109
  feedback = {
93
- "event_id": "$EVENT_ID",
110
+ "event_id": get_env("FE_H_EVENT_ID"),
94
111
  "type": "implementation_feedback",
95
- "project_id": "$PROJECT_ID",
96
- "timestamp": "$TIMESTAMP",
112
+ "project_id": get_env("FE_H_PROJECT_ID"),
113
+ "timestamp": get_env("FE_H_TIMESTAMP"),
97
114
  "distillation_candidate": True,
98
- "unclear_specs": existing.get("unclear_specs", []) + (["$UNCLEAR"] if "$UNCLEAR" else []),
99
- "uncovered_decisions": existing.get("uncovered_decisions", []) + (["$UNCOVERED"] if "$UNCOVERED" else []),
100
- "wrong_assumptions": existing.get("wrong_assumptions", []) + (["$WRONG_ASSUMPTIONS"] if "$WRONG_ASSUMPTIONS" else []),
101
- "untestable_criteria": existing.get("untestable_criteria", []) + (["$UNTESTABLE"] if "$UNTESTABLE" else []),
102
- "general_feedback": existing.get("general_feedback", "") or "$GENERAL",
103
- "impl_dir": "$IMPL_DIR"
115
+ "unclear_specs": existing.get("unclear_specs", []) + ([get_env("FE_H_UNCLEAR")] if get_env("FE_H_UNCLEAR") else []),
116
+ "uncovered_decisions": existing.get("uncovered_decisions", []) + ([get_env("FE_H_UNCOVERED")] if get_env("FE_H_UNCOVERED") else []),
117
+ "wrong_assumptions": existing.get("wrong_assumptions", []) + ([get_env("FE_H_WRONG_ASSUMPTIONS")] if get_env("FE_H_WRONG_ASSUMPTIONS") else []),
118
+ "untestable_criteria": existing.get("untestable_criteria", []) + ([get_env("FE_H_UNTESTABLE")] if get_env("FE_H_UNTESTABLE") else []),
119
+ "general_feedback": existing.get("general_feedback", "") or get_env("FE_H_GENERAL"),
120
+ "impl_dir": get_env("FE_H_IMPL_DIR")
104
121
  }
105
122
 
106
123
  # 过滤空字符串
@@ -115,7 +132,7 @@ if feedback["wrong_assumptions"]: score += 0.3
115
132
  if feedback["general_feedback"]: score += 0.2
116
133
  feedback["quality_score"] = min(score, 0.9)
117
134
 
118
- log_file = "$LOG_FILE"
135
+ log_file = get_env("FE_H_LOG_FILE")
119
136
  os.makedirs(os.path.dirname(log_file), exist_ok=True)
120
137
  with open(log_file, "a") as f:
121
138
  f.write(json.dumps(feedback, ensure_ascii=False) + "\n")
@@ -130,9 +147,11 @@ try:
130
147
  pending = sum(1 for e in events
131
148
  if e.get("distillation_candidate") and not e.get("distilled")
132
149
  and e.get("quality_score", 0) >= 0.6)
133
- print(f" 待蒸馏事件:{pending} 条(阈值:$THRESHOLD)")
150
+ threshold = get_env("FE_H_THRESHOLD", "3")
151
+ print(f" 待蒸馏事件:{pending} 条(阈值:{threshold})")
134
152
  print(str(pending)) # 最后一行供 bash 读取
135
- except:
153
+ except Exception as e:
154
+ print(f" 统计待蒸馏事件时出错:{e}")
136
155
  print("0")
137
156
  PYEOF
138
157
  )
@@ -59,11 +59,15 @@ tier_label() {
59
59
  set_tier_in_yaml() {
60
60
  local yaml="$1"
61
61
  local tier="$2"
62
+ if [ ! -w "$yaml" ]; then
63
+ echo "⚠️ 文件无写权限,跳过 tier 标记: $yaml" >&2
64
+ return 1
65
+ fi
62
66
  if grep -q '^tier:' "$yaml" 2>/dev/null; then
63
- sed -i "s/^tier:.*/tier: $tier/" "$yaml"
67
+ sed -i.bak "s/^tier:.*/tier: $tier/" "$yaml" && rm -f "$yaml.bak"
64
68
  else
65
69
  # 插到 version 行之后
66
- sed -i "/^version:/a tier: $tier" "$yaml"
70
+ sed -i.bak "/^version:/a tier: $tier" "$yaml" && rm -f "$yaml.bak"
67
71
  fi
68
72
  }
69
73
 
@@ -146,8 +150,9 @@ cmd_add() {
146
150
 
147
151
  # 安装后标记为 managed
148
152
  if [ -f "$dest_yaml" ]; then
149
- set_tier_in_yaml "$dest_yaml" "managed"
150
- echo " ✓ 已标记为 managed"
153
+ if set_tier_in_yaml "$dest_yaml" "managed"; then
154
+ echo " ✓ 已标记为 managed"
155
+ fi
151
156
  fi
152
157
  }
153
158
 
@@ -169,7 +174,8 @@ cmd_publish() {
169
174
  local name version description
170
175
  name=$(grep '^name:' "$yaml" | head -1 | awk '{print $2}')
171
176
  version=$(grep '^version:' "$yaml" | head -1 | awk '{print $2}')
172
- description=$(grep '^description:' "$yaml" | head -1 | cut -d: -f2- | xargs)
177
+ description=$(grep '^description:' "$yaml" | head -1 | cut -d: -f2- | sed 's/^[[:space:]]*//')
178
+
173
179
 
174
180
  local available_file="$SCRIPT_DIR/available.json"
175
181