@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/.claude/commands/add-skill.md +180 -0
- package/.claude/commands/codeword.md +166 -0
- package/.claude/commands/skill.md +172 -0
- package/.claude/commands/telos.md +299 -0
- package/CLAUDE.md +449 -0
- package/README.md +12 -2
- package/README.zh.md +100 -7
- package/bin/telos.js +175 -59
- package/package.json +4 -2
- package/scripts/auto-distill.sh +24 -11
- package/scripts/bridge-to-coder.sh +45 -10
- package/scripts/feedback-hook.sh +34 -15
- package/scripts/telos-install.sh +11 -5
- package/skills/_kernel/codewords.md +264 -0
- package/skills/_kernel/skill-extraction.md +194 -27
- package/skills/_template/skill.yaml +2 -0
- package/skills/req-mining/skill.yaml +2 -0
- package/templates/user-config.json +67 -0
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
|
-
|
|
41
|
-
|
|
42
|
-
|
|
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
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
fs.
|
|
67
|
-
|
|
68
|
-
|
|
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
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
fs.
|
|
89
|
-
|
|
90
|
-
|
|
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
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
fs.
|
|
100
|
-
|
|
101
|
-
|
|
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
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
fs.
|
|
116
|
-
|
|
117
|
-
|
|
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
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
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
|
-
|
|
131
|
-
fs.
|
|
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.
|
|
191
|
+
version: '1.4.1',
|
|
136
192
|
created: new Date().toISOString(),
|
|
137
193
|
skill: skill || 'none'
|
|
138
194
|
}));
|
|
139
195
|
|
|
140
196
|
try {
|
|
141
|
-
|
|
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
|
-
|
|
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
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
process.
|
|
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('🚀 启动 ' +
|
|
207
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
379
|
-
|
|
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.
|
|
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.
|
|
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"
|
package/scripts/auto-distill.sh
CHANGED
|
@@ -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(
|
|
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(
|
|
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" 项目
|
|
76
|
-
except:
|
|
77
|
-
print(" 项目
|
|
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
|
-
|
|
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
|
-
|
|
41
|
-
|
|
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
|
-
|
|
62
|
+
{ai_doc_content}
|
|
49
63
|
|
|
50
64
|
---
|
|
51
65
|
|
|
52
66
|
## 反馈协议(实现完成后必须执行)
|
|
53
67
|
|
|
54
|
-
实现过程中如遇到以下情况,请记录到
|
|
68
|
+
实现过程中如遇到以下情况,请记录到 `.telos-feedback.json`:
|
|
55
69
|
- 需求描述不清晰或有歧义
|
|
56
70
|
- 遇到文档未覆盖的技术决策
|
|
57
71
|
- 发现需求假设与现实不符
|
|
58
72
|
- 验收标准无法被测试
|
|
59
73
|
|
|
60
74
|
记录格式:
|
|
61
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
package/scripts/feedback-hook.sh
CHANGED
|
@@ -81,26 +81,43 @@ if ! $AUTO_MODE; then
|
|
|
81
81
|
fi
|
|
82
82
|
|
|
83
83
|
# ── 合并反馈并生成事件 ────────────────────────────────────────
|
|
84
|
-
|
|
85
|
-
|
|
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(
|
|
105
|
+
existing = json.loads(get_env("FE_H_FEEDBACK_JSON", "{}"))
|
|
89
106
|
except:
|
|
90
107
|
existing = {}
|
|
91
108
|
|
|
92
109
|
feedback = {
|
|
93
|
-
"event_id": "
|
|
110
|
+
"event_id": get_env("FE_H_EVENT_ID"),
|
|
94
111
|
"type": "implementation_feedback",
|
|
95
|
-
"project_id": "
|
|
96
|
-
"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", []) + (["
|
|
99
|
-
"uncovered_decisions": existing.get("uncovered_decisions", []) + (["
|
|
100
|
-
"wrong_assumptions": existing.get("wrong_assumptions", []) + (["
|
|
101
|
-
"untestable_criteria": existing.get("untestable_criteria", []) + (["
|
|
102
|
-
"general_feedback": existing.get("general_feedback", "") or "
|
|
103
|
-
"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 = "
|
|
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
|
-
|
|
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
|
)
|
package/scripts/telos-install.sh
CHANGED
|
@@ -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
|
-
|
|
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- |
|
|
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
|
|