@colin4k1024/tsp 2.5.0 → 2.5.2
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 +67 -0
- package/commands/dashboard.md +105 -0
- package/commands/goal.md +142 -0
- package/commands/heartbeat.md +129 -0
- package/commands/triage.md +108 -0
- package/hooks/README.md +1 -1
- package/hooks/harness-context-monitor.js +4 -0
- package/hooks/hooks.json +27 -26
- package/hooks/strategic-compact/README.md +7 -5
- package/hooks/strategic-compact/suggest-compact.js +9 -174
- package/marketplace.json +7 -8
- package/package.json +1 -1
- package/schemas/goal.schema.json +172 -0
- package/scripts/harness-audit.js +7 -4
- package/scripts/hooks/session-start-goal-resume.js +95 -0
- package/scripts/hooks/suggest-compact.js +392 -62
- package/scripts/install-platform.js +68 -85
- package/scripts/lib/blame-attribution.js +210 -0
- package/scripts/lib/completion-oracle.js +351 -0
- package/scripts/lib/heartbeat-scheduler.js +265 -0
- package/scripts/lib/opencode/convert-agents.js +273 -0
- package/scripts/lib/opencode/convert-hooks.js +286 -0
- package/scripts/lib/opencode/generate-agents-md.js +361 -0
- package/scripts/lib/wave-cost-advisor.js +155 -0
- package/scripts/test-opencode-install.js +151 -0
- package/skills/goal-convergence/SKILL.md +150 -0
- package/skills/goframe-v2/examples/practices/quick-demo/manifest/config/config.yaml +14 -14
- package/skills/loop-heartbeat/SKILL.md +120 -0
- package/skills/mcp-connector-bridge/SKILL.md +132 -0
- package/skills/repo-scan/SKILL.md +63 -63
- package/skills/rework-loop/SKILL.md +131 -0
- package/scripts/__pycache__/__init__.cpython-311.pyc +0 -0
- package/scripts/__pycache__/build_platform_artifacts.cpython-311.pyc +0 -0
- package/scripts/__pycache__/install_platform.cpython-311.pyc +0 -0
- package/scripts/__pycache__/langfuse_trace.cpython-311.pyc +0 -0
- package/scripts/__pycache__/query_audit_logs.cpython-311.pyc +0 -0
- package/scripts/__pycache__/scan_leaked_keys.cpython-311.pyc +0 -0
- package/scripts/__pycache__/team_skills_platform.cpython-311.pyc +0 -0
- package/scripts/__pycache__/team_skills_platform.cpython-313.pyc +0 -0
- package/scripts/__pycache__/validate_library.cpython-311.pyc +0 -0
- package/scripts/__pycache__/validate_workflow_state.cpython-311.pyc +0 -0
- package/scripts/evolution/__pycache__/__init__.cpython-311.pyc +0 -0
- package/scripts/evolution/__pycache__/store.cpython-311.pyc +0 -0
- package/scripts/hooks/__pycache__/__init__.cpython-311.pyc +0 -0
- package/scripts/hooks/__pycache__/mcp_health_check.cpython-311.pyc +0 -0
- package/scripts/hooks/__pycache__/observe.cpython-311.pyc +0 -0
- package/scripts/hooks/__pycache__/session_end.cpython-311.pyc +0 -0
- package/scripts/hooks/__pycache__/session_start.cpython-311.pyc +0 -0
- package/scripts/lib/__pycache__/audit_logger.cpython-311.pyc +0 -0
- package/scripts/lib/__pycache__/audit_query.cpython-311.pyc +0 -0
- package/scripts/lib/__pycache__/hook_contract.cpython-311.pyc +0 -0
- package/scripts/lib/__pycache__/memory_store.cpython-311.pyc +0 -0
- package/scripts/lib/__pycache__/utils.cpython-311.pyc +0 -0
|
@@ -0,0 +1,265 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* heartbeat-scheduler.js
|
|
5
|
+
*
|
|
6
|
+
* Heartbeat engine for Loop Engineering.
|
|
7
|
+
* Runs configured discovery scans on a schedule, classifies results,
|
|
8
|
+
* and routes findings to goals or triage inbox.
|
|
9
|
+
*
|
|
10
|
+
* This module provides the logic; scheduling is handled by CronCreate/CronDelete
|
|
11
|
+
* or ScheduleWakeup in the Claude Code runtime.
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
const fs = require('fs');
|
|
15
|
+
const path = require('path');
|
|
16
|
+
const { execSync } = require('child_process');
|
|
17
|
+
const yaml = require ? undefined : undefined; // yaml parsing fallback below
|
|
18
|
+
|
|
19
|
+
const { createGoal, saveGoal } = require('./completion-oracle');
|
|
20
|
+
|
|
21
|
+
const DEFAULT_CONFIG = {
|
|
22
|
+
interval: '30m',
|
|
23
|
+
scans: [],
|
|
24
|
+
budget: {
|
|
25
|
+
maxDollarsPerHour: 2.0,
|
|
26
|
+
pauseOnExhaust: true,
|
|
27
|
+
},
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
const SCAN_ACTIONS = {
|
|
31
|
+
autoGoal: 'auto-goal',
|
|
32
|
+
triage: 'triage',
|
|
33
|
+
notify: 'notify',
|
|
34
|
+
ignore: 'ignore',
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
// Simple YAML parser for heartbeat config (avoids external dependency)
|
|
38
|
+
function parseSimpleYaml(content) {
|
|
39
|
+
try {
|
|
40
|
+
// Try JSON first (YAML superset)
|
|
41
|
+
return JSON.parse(content);
|
|
42
|
+
} catch {
|
|
43
|
+
// Minimal YAML parsing for heartbeat.yaml structure
|
|
44
|
+
const lines = content.split('\n');
|
|
45
|
+
const result = { heartbeat: { scans: [], budget: {} } };
|
|
46
|
+
let currentScan = null;
|
|
47
|
+
|
|
48
|
+
for (const line of lines) {
|
|
49
|
+
const trimmed = line.trim();
|
|
50
|
+
if (!trimmed || trimmed.startsWith('#')) continue;
|
|
51
|
+
|
|
52
|
+
const indentLevel = line.search(/\S/);
|
|
53
|
+
|
|
54
|
+
if (trimmed.startsWith('interval:')) {
|
|
55
|
+
result.heartbeat.interval = trimmed.split(':')[1].trim().replace(/['"]/g, '');
|
|
56
|
+
} else if (trimmed.startsWith('maxDollarsPerHour:')) {
|
|
57
|
+
result.heartbeat.budget.maxDollarsPerHour = parseFloat(trimmed.split(':')[1].trim());
|
|
58
|
+
} else if (trimmed.startsWith('pauseOnExhaust:')) {
|
|
59
|
+
result.heartbeat.budget.pauseOnExhaust = trimmed.split(':')[1].trim() === 'true';
|
|
60
|
+
} else if (trimmed.startsWith('- name:')) {
|
|
61
|
+
if (currentScan) result.heartbeat.scans.push(currentScan);
|
|
62
|
+
currentScan = { name: trimmed.replace('- name:', '').trim().replace(/['"]/g, '') };
|
|
63
|
+
} else if (currentScan && indentLevel >= 6) {
|
|
64
|
+
const [key, ...valueParts] = trimmed.split(':');
|
|
65
|
+
const value = valueParts.join(':').trim().replace(/['"]/g, '');
|
|
66
|
+
if (key === 'command') currentScan.command = value;
|
|
67
|
+
else if (key === 'onFailure') currentScan.onFailure = value;
|
|
68
|
+
else if (key === 'threshold') currentScan.threshold = parseFloat(value);
|
|
69
|
+
else if (key === 'description') currentScan.description = value;
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
if (currentScan) result.heartbeat.scans.push(currentScan);
|
|
73
|
+
return result;
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
function loadConfig(projectRoot) {
|
|
78
|
+
const configPath = path.join(projectRoot || process.cwd(), '.claude', 'heartbeat.yaml');
|
|
79
|
+
if (!fs.existsSync(configPath)) return DEFAULT_CONFIG;
|
|
80
|
+
|
|
81
|
+
try {
|
|
82
|
+
const content = fs.readFileSync(configPath, 'utf-8');
|
|
83
|
+
const parsed = parseSimpleYaml(content);
|
|
84
|
+
return { ...DEFAULT_CONFIG, ...parsed.heartbeat };
|
|
85
|
+
} catch {
|
|
86
|
+
return DEFAULT_CONFIG;
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
function runScan(scan) {
|
|
91
|
+
const startTime = Date.now();
|
|
92
|
+
try {
|
|
93
|
+
const output = execSync(scan.command, {
|
|
94
|
+
encoding: 'utf-8',
|
|
95
|
+
timeout: 60000,
|
|
96
|
+
stdio: ['pipe', 'pipe', 'pipe'],
|
|
97
|
+
}).trim();
|
|
98
|
+
|
|
99
|
+
let passed = true;
|
|
100
|
+
|
|
101
|
+
if (scan.threshold !== undefined) {
|
|
102
|
+
const numeric = parseInt(output.match(/\d+/)?.[0] || '0', 10);
|
|
103
|
+
passed = numeric <= scan.threshold;
|
|
104
|
+
} else {
|
|
105
|
+
const exitMatch = output.match(/EXIT:(\d+)$/);
|
|
106
|
+
passed = exitMatch ? exitMatch[1] === '0' : true;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
return {
|
|
110
|
+
name: scan.name,
|
|
111
|
+
passed,
|
|
112
|
+
output: output.slice(0, 500),
|
|
113
|
+
durationMs: Date.now() - startTime,
|
|
114
|
+
timestamp: new Date().toISOString(),
|
|
115
|
+
};
|
|
116
|
+
} catch (error) {
|
|
117
|
+
return {
|
|
118
|
+
name: scan.name,
|
|
119
|
+
passed: false,
|
|
120
|
+
output: (error.stderr || error.message || 'Unknown error').slice(0, 500),
|
|
121
|
+
durationMs: Date.now() - startTime,
|
|
122
|
+
timestamp: new Date().toISOString(),
|
|
123
|
+
};
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
function classifyResult(scan, result) {
|
|
128
|
+
if (result.passed) return { action: 'pass', scan, result };
|
|
129
|
+
return {
|
|
130
|
+
action: scan.onFailure || SCAN_ACTIONS.triage,
|
|
131
|
+
scan,
|
|
132
|
+
result,
|
|
133
|
+
};
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
function createTriageItem(scan, result) {
|
|
137
|
+
return {
|
|
138
|
+
id: `triage-${Date.now().toString(36)}`,
|
|
139
|
+
source: `heartbeat:${scan.name}`,
|
|
140
|
+
severity: scan.onFailure === SCAN_ACTIONS.autoGoal ? 'high' : 'medium',
|
|
141
|
+
summary: `${scan.description || scan.name}: ${result.output.slice(0, 100)}`,
|
|
142
|
+
detail: result.output,
|
|
143
|
+
suggestedActions: ['Create goal to fix', 'Ignore for now', 'Defer to next session'],
|
|
144
|
+
createdAt: new Date().toISOString(),
|
|
145
|
+
status: 'pending',
|
|
146
|
+
};
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
function appendToTriageInbox(item) {
|
|
150
|
+
const home = process.env.HOME || process.env.USERPROFILE || '';
|
|
151
|
+
const inboxDir = path.join(home, '.claude', 'triage');
|
|
152
|
+
if (!fs.existsSync(inboxDir)) {
|
|
153
|
+
fs.mkdirSync(inboxDir, { recursive: true });
|
|
154
|
+
}
|
|
155
|
+
const inboxPath = path.join(inboxDir, 'inbox.jsonl');
|
|
156
|
+
fs.appendFileSync(inboxPath, JSON.stringify(item) + '\n', 'utf-8');
|
|
157
|
+
return inboxPath;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
function createGoalFromScanFailure(scan, result) {
|
|
161
|
+
const objective = `Fix ${scan.description || scan.name}: ${result.output.slice(0, 80)}`;
|
|
162
|
+
|
|
163
|
+
const stoppingConditions = [{
|
|
164
|
+
type: 'custom_command',
|
|
165
|
+
command: scan.command,
|
|
166
|
+
description: scan.description || scan.name,
|
|
167
|
+
}];
|
|
168
|
+
|
|
169
|
+
if (scan.threshold !== undefined) {
|
|
170
|
+
stoppingConditions[0].threshold = scan.threshold;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
const goal = createGoal(objective, { stoppingConditions });
|
|
174
|
+
saveGoal(goal);
|
|
175
|
+
return goal;
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
function runHeartbeat(projectRoot) {
|
|
179
|
+
const config = loadConfig(projectRoot);
|
|
180
|
+
|
|
181
|
+
if (config.scans.length === 0) {
|
|
182
|
+
return {
|
|
183
|
+
status: 'no_scans',
|
|
184
|
+
message: 'No scans configured in .claude/heartbeat.yaml',
|
|
185
|
+
results: [],
|
|
186
|
+
};
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
const results = config.scans.map(scan => {
|
|
190
|
+
const result = runScan(scan);
|
|
191
|
+
const classified = classifyResult(scan, result);
|
|
192
|
+
|
|
193
|
+
if (classified.action === SCAN_ACTIONS.autoGoal) {
|
|
194
|
+
const goal = createGoalFromScanFailure(scan, result);
|
|
195
|
+
classified.goalId = goal.goalId;
|
|
196
|
+
} else if (classified.action === SCAN_ACTIONS.triage) {
|
|
197
|
+
const item = createTriageItem(scan, result);
|
|
198
|
+
appendToTriageInbox(item);
|
|
199
|
+
classified.triageId = item.id;
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
return classified;
|
|
203
|
+
});
|
|
204
|
+
|
|
205
|
+
const passed = results.filter(r => r.action === 'pass').length;
|
|
206
|
+
const failed = results.length - passed;
|
|
207
|
+
|
|
208
|
+
return {
|
|
209
|
+
status: failed > 0 ? 'issues_found' : 'all_clear',
|
|
210
|
+
timestamp: new Date().toISOString(),
|
|
211
|
+
summary: `${passed}/${results.length} scans passed`,
|
|
212
|
+
results,
|
|
213
|
+
};
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
function getHeartbeatStatus(projectRoot) {
|
|
217
|
+
const config = loadConfig(projectRoot);
|
|
218
|
+
|
|
219
|
+
const home = process.env.HOME || process.env.USERPROFILE || '';
|
|
220
|
+
const lastRunPath = path.join(home, '.claude', 'heartbeat-last-run.json');
|
|
221
|
+
let lastRun = null;
|
|
222
|
+
if (fs.existsSync(lastRunPath)) {
|
|
223
|
+
try {
|
|
224
|
+
lastRun = JSON.parse(fs.readFileSync(lastRunPath, 'utf-8'));
|
|
225
|
+
} catch { /* ignore */ }
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
return {
|
|
229
|
+
configured: config.scans.length > 0,
|
|
230
|
+
interval: config.interval,
|
|
231
|
+
scanCount: config.scans.length,
|
|
232
|
+
scans: config.scans.map(s => ({ name: s.name, onFailure: s.onFailure, description: s.description })),
|
|
233
|
+
budget: config.budget,
|
|
234
|
+
lastRun,
|
|
235
|
+
};
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
function saveLastRun(result) {
|
|
239
|
+
const home = process.env.HOME || process.env.USERPROFILE || '';
|
|
240
|
+
const lastRunPath = path.join(home, '.claude', 'heartbeat-last-run.json');
|
|
241
|
+
const dir = path.dirname(lastRunPath);
|
|
242
|
+
if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true });
|
|
243
|
+
fs.writeFileSync(lastRunPath, JSON.stringify(result, null, 2), 'utf-8');
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
function parseInterval(interval) {
|
|
247
|
+
const match = (interval || '30m').match(/^(\d+)(m|h)$/);
|
|
248
|
+
if (!match) return 30 * 60;
|
|
249
|
+
const value = parseInt(match[1], 10);
|
|
250
|
+
return match[2] === 'h' ? value * 60 * 60 : value * 60;
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
module.exports = {
|
|
254
|
+
SCAN_ACTIONS,
|
|
255
|
+
loadConfig,
|
|
256
|
+
runScan,
|
|
257
|
+
classifyResult,
|
|
258
|
+
createTriageItem,
|
|
259
|
+
appendToTriageInbox,
|
|
260
|
+
createGoalFromScanFailure,
|
|
261
|
+
runHeartbeat,
|
|
262
|
+
getHeartbeatStatus,
|
|
263
|
+
saveLastRun,
|
|
264
|
+
parseInterval,
|
|
265
|
+
};
|
|
@@ -0,0 +1,273 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
const fs = require('fs');
|
|
3
|
+
const path = require('path');
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* 角色配置映射
|
|
7
|
+
*/
|
|
8
|
+
const ROLE_CONFIGS = {
|
|
9
|
+
'tech-lead': {
|
|
10
|
+
description: 'Tech Lead(技术负责人)- 负责需求 intake、任务拆解、角色分派、冲突决策与最终交付收口',
|
|
11
|
+
tools: { '*': true },
|
|
12
|
+
color: '#3B82F6',
|
|
13
|
+
},
|
|
14
|
+
'product-manager': {
|
|
15
|
+
description: 'Product Manager(产品经理)- 负责需求澄清、PRD 编写、用户故事和验收标准定义',
|
|
16
|
+
tools: { read: true, write: true, grep: true, glob: true, webfetch: true },
|
|
17
|
+
color: '#10B981',
|
|
18
|
+
},
|
|
19
|
+
'project-manager': {
|
|
20
|
+
description: 'Project Manager(项目管理)- 负责排期、依赖管理、里程碑跟踪和风险推进',
|
|
21
|
+
tools: { read: true, write: true, grep: true, glob: true },
|
|
22
|
+
color: '#F59E0B',
|
|
23
|
+
},
|
|
24
|
+
'architect': {
|
|
25
|
+
description: 'Architect(架构师)- 负责 ADR、系统边界、接口与数据契约设计',
|
|
26
|
+
tools: { read: true, write: true, grep: true, glob: true, webfetch: true },
|
|
27
|
+
color: '#8B5CF6',
|
|
28
|
+
},
|
|
29
|
+
'frontend-engineer': {
|
|
30
|
+
description: 'Frontend Engineer(前端开发)- 负责前端实现与自测',
|
|
31
|
+
tools: { edit: true, write: true, bash: true, read: true, grep: true, glob: true },
|
|
32
|
+
color: '#EC4899',
|
|
33
|
+
},
|
|
34
|
+
'backend-engineer': {
|
|
35
|
+
description: 'Backend Engineer(后端开发)- 负责后端实现与自测',
|
|
36
|
+
tools: { edit: true, write: true, bash: true, read: true, grep: true, glob: true },
|
|
37
|
+
color: '#06B6D4',
|
|
38
|
+
},
|
|
39
|
+
'qa-engineer': {
|
|
40
|
+
description: 'QA Engineer(测试工程师)- 负责测试计划、回归验证和放行建议',
|
|
41
|
+
tools: { read: true, grep: true, glob: true, bash: true },
|
|
42
|
+
color: '#F97316',
|
|
43
|
+
},
|
|
44
|
+
'devops-engineer': {
|
|
45
|
+
description: 'DevOps Engineer(运维工程师)- 负责发布、监控、回滚与运行保障',
|
|
46
|
+
tools: { edit: true, write: true, bash: true, read: true, grep: true, glob: true },
|
|
47
|
+
color: '#6366F1',
|
|
48
|
+
},
|
|
49
|
+
};
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Specialist 配置映射
|
|
53
|
+
*/
|
|
54
|
+
const SPECIALIST_CONFIGS = {
|
|
55
|
+
'planner': {
|
|
56
|
+
description: 'Planner - 实现规划专家',
|
|
57
|
+
tools: { read: true, grep: true, glob: true },
|
|
58
|
+
hidden: true,
|
|
59
|
+
},
|
|
60
|
+
'architect': {
|
|
61
|
+
description: 'Architect - 系统设计专家',
|
|
62
|
+
tools: { read: true, grep: true, glob: true },
|
|
63
|
+
hidden: true,
|
|
64
|
+
},
|
|
65
|
+
'tdd-guide': {
|
|
66
|
+
description: 'TDD Guide - 测试驱动开发专家',
|
|
67
|
+
tools: { read: true, write: true, edit: true, bash: true, grep: true },
|
|
68
|
+
hidden: true,
|
|
69
|
+
},
|
|
70
|
+
'code-reviewer': {
|
|
71
|
+
description: 'Code Reviewer - 代码审查专家',
|
|
72
|
+
tools: { read: true, grep: true, glob: true, bash: true },
|
|
73
|
+
hidden: true,
|
|
74
|
+
},
|
|
75
|
+
'security-reviewer': {
|
|
76
|
+
description: 'Security Reviewer - 安全审查专家',
|
|
77
|
+
tools: { read: true, grep: true, glob: true },
|
|
78
|
+
hidden: true,
|
|
79
|
+
},
|
|
80
|
+
'build-error-resolver': {
|
|
81
|
+
description: 'Build Error Resolver - 构建错误修复专家',
|
|
82
|
+
tools: { read: true, write: true, edit: true, bash: true, grep: true, glob: true },
|
|
83
|
+
hidden: true,
|
|
84
|
+
},
|
|
85
|
+
};
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* 从 Markdown 内容中提取描述
|
|
89
|
+
*/
|
|
90
|
+
function extractDescription(content, fallback) {
|
|
91
|
+
for (const line of content.split(/\r?\n/)) {
|
|
92
|
+
const trimmed = line.trim();
|
|
93
|
+
if (trimmed.startsWith('# ')) {
|
|
94
|
+
return trimmed.slice(2).trim();
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
return fallback;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/**
|
|
101
|
+
* 为 Agent 文件添加 YAML front matter
|
|
102
|
+
*/
|
|
103
|
+
function addFrontMatter(filePath, config, isSpecialist = false) {
|
|
104
|
+
const content = fs.readFileSync(filePath, 'utf8');
|
|
105
|
+
const fileName = path.parse(filePath).name;
|
|
106
|
+
|
|
107
|
+
// 检查是否已经有 front matter
|
|
108
|
+
if (content.startsWith('---')) {
|
|
109
|
+
console.log(`Skipping ${fileName} - already has front matter`);
|
|
110
|
+
return content;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
const description = config.description || extractDescription(content, fileName);
|
|
114
|
+
const tools = config.tools || { '*': true };
|
|
115
|
+
|
|
116
|
+
const frontMatter = [
|
|
117
|
+
'---',
|
|
118
|
+
`description: "${description.replace(/"/g, '\\"')}"`,
|
|
119
|
+
`tools:`,
|
|
120
|
+
];
|
|
121
|
+
|
|
122
|
+
// Format tools as YAML object
|
|
123
|
+
for (const [tool, enabled] of Object.entries(tools)) {
|
|
124
|
+
frontMatter.push(` ${tool}: ${enabled}`);
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
if (config.color) {
|
|
128
|
+
frontMatter.push(`color: "${config.color}"`);
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
if (config.hidden) {
|
|
132
|
+
frontMatter.push(`hidden: true`);
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
frontMatter.push('---', '');
|
|
136
|
+
|
|
137
|
+
return `${frontMatter.join('\n')}${content}`;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
/**
|
|
141
|
+
* 转换角色 agents
|
|
142
|
+
*/
|
|
143
|
+
function convertRoleAgents(sourceDir, targetDir) {
|
|
144
|
+
if (!fs.existsSync(sourceDir)) {
|
|
145
|
+
console.log(`Source directory not found: ${sourceDir}`);
|
|
146
|
+
return;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
fs.mkdirSync(targetDir, { recursive: true });
|
|
150
|
+
|
|
151
|
+
const files = fs.readdirSync(sourceDir)
|
|
152
|
+
.filter(file => file.endsWith('.md'))
|
|
153
|
+
.sort();
|
|
154
|
+
|
|
155
|
+
for (const file of files) {
|
|
156
|
+
const sourcePath = path.join(sourceDir, file);
|
|
157
|
+
const targetPath = path.join(targetDir, file);
|
|
158
|
+
const roleName = path.parse(file).name;
|
|
159
|
+
|
|
160
|
+
const config = ROLE_CONFIGS[roleName] || {
|
|
161
|
+
description: extractDescription(fs.readFileSync(sourcePath, 'utf8'), roleName),
|
|
162
|
+
tools: { '*': true },
|
|
163
|
+
};
|
|
164
|
+
|
|
165
|
+
const content = addFrontMatter(sourcePath, config, false);
|
|
166
|
+
fs.writeFileSync(targetPath, content, 'utf8');
|
|
167
|
+
console.log(`Converted role agent: ${file}`);
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
/**
|
|
172
|
+
* 转换 specialist agents
|
|
173
|
+
*/
|
|
174
|
+
function convertSpecialistAgents(sourceDir, targetDir) {
|
|
175
|
+
if (!fs.existsSync(sourceDir)) {
|
|
176
|
+
console.log(`Source directory not found: ${sourceDir}`);
|
|
177
|
+
return;
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
fs.mkdirSync(targetDir, { recursive: true });
|
|
181
|
+
|
|
182
|
+
const files = fs.readdirSync(sourceDir)
|
|
183
|
+
.filter(file => file.endsWith('.md'))
|
|
184
|
+
.sort();
|
|
185
|
+
|
|
186
|
+
for (const file of files) {
|
|
187
|
+
const sourcePath = path.join(sourceDir, file);
|
|
188
|
+
const targetPath = path.join(targetDir, file);
|
|
189
|
+
const specialistName = path.parse(file).name;
|
|
190
|
+
|
|
191
|
+
const config = SPECIALIST_CONFIGS[specialistName] || {
|
|
192
|
+
description: extractDescription(fs.readFileSync(sourcePath, 'utf8'), specialistName),
|
|
193
|
+
tools: { '*': true },
|
|
194
|
+
hidden: true,
|
|
195
|
+
};
|
|
196
|
+
|
|
197
|
+
const content = addFrontMatter(sourcePath, config, true);
|
|
198
|
+
fs.writeFileSync(targetPath, content, 'utf8');
|
|
199
|
+
console.log(`Converted specialist agent: ${file}`);
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
/**
|
|
204
|
+
* 主函数
|
|
205
|
+
*/
|
|
206
|
+
function main() {
|
|
207
|
+
const root = path.join(__dirname, '../../..');
|
|
208
|
+
const opencodeHome = process.argv[2] || path.join(require('os').homedir(), '.config', 'opencode');
|
|
209
|
+
const pluginDir = path.join(opencodeHome, 'plugins', 'team-skills-platform');
|
|
210
|
+
|
|
211
|
+
console.log('Converting agents for OpenCode...');
|
|
212
|
+
console.log(`Root: ${root}`);
|
|
213
|
+
console.log(`Target: ${opencodeHome}`);
|
|
214
|
+
|
|
215
|
+
// 转换角色 agents
|
|
216
|
+
const rolesSourceDir = path.join(root, 'agents', 'roles');
|
|
217
|
+
const rolesTargetDir = path.join(pluginDir, 'agents', 'roles');
|
|
218
|
+
convertRoleAgents(rolesSourceDir, rolesTargetDir);
|
|
219
|
+
|
|
220
|
+
// 转换 specialist agents
|
|
221
|
+
const specialistsSourceDir = path.join(root, 'agents', 'specialists');
|
|
222
|
+
const specialistsTargetDir = path.join(pluginDir, 'agents', 'specialists');
|
|
223
|
+
convertSpecialistAgents(specialistsSourceDir, specialistsTargetDir);
|
|
224
|
+
|
|
225
|
+
// 同时复制到 opencodeHome/agents/ 目录
|
|
226
|
+
const agentsTargetDir = path.join(opencodeHome, 'agents');
|
|
227
|
+
fs.mkdirSync(agentsTargetDir, { recursive: true });
|
|
228
|
+
|
|
229
|
+
// 复制角色 agents
|
|
230
|
+
if (fs.existsSync(rolesTargetDir)) {
|
|
231
|
+
for (const file of fs.readdirSync(rolesTargetDir)) {
|
|
232
|
+
if (file.endsWith('.md')) {
|
|
233
|
+
fs.copyFileSync(
|
|
234
|
+
path.join(rolesTargetDir, file),
|
|
235
|
+
path.join(agentsTargetDir, file)
|
|
236
|
+
);
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
// 复制 specialist agents
|
|
242
|
+
if (fs.existsSync(specialistsTargetDir)) {
|
|
243
|
+
for (const file of fs.readdirSync(specialistsTargetDir)) {
|
|
244
|
+
if (file.endsWith('.md')) {
|
|
245
|
+
fs.copyFileSync(
|
|
246
|
+
path.join(specialistsTargetDir, file),
|
|
247
|
+
path.join(agentsTargetDir, `specialist-${file}`)
|
|
248
|
+
);
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
console.log('✅ Agent conversion completed');
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
// 导出函数供其他脚本使用
|
|
257
|
+
module.exports = {
|
|
258
|
+
addFrontMatter,
|
|
259
|
+
convertRoleAgents,
|
|
260
|
+
convertSpecialistAgents,
|
|
261
|
+
ROLE_CONFIGS,
|
|
262
|
+
SPECIALIST_CONFIGS,
|
|
263
|
+
};
|
|
264
|
+
|
|
265
|
+
// 如果直接运行此脚本
|
|
266
|
+
if (require.main === module) {
|
|
267
|
+
try {
|
|
268
|
+
main();
|
|
269
|
+
} catch (error) {
|
|
270
|
+
console.error('Error converting agents:', error.message);
|
|
271
|
+
process.exitCode = 1;
|
|
272
|
+
}
|
|
273
|
+
}
|