minimal-agent 0.7.1 → 0.8.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/package.json +1 -1
- package/plugins/ralph-wiggum/plugin.js +1 -1
- package/plugins/workflow-runner/plugin.js +3 -0
- package/plugins/workflow-runner/src/agentCommon.js +22 -5
- package/plugins/workflow-runner/src/index.js +2 -0
- package/plugins/workflow-runner/src/loader.js +12 -0
- package/plugins/workflow-runner/src/recovery.js +4 -2
- package/plugins/workflow-runner/src/runner.js +36 -6
- package/plugins/workflow-runner/src/stepExecutors/agent.js +39 -56
- package/plugins/workflow-runner/src/stepExecutors/llm.js +12 -2
- package/plugins/workflow-runner/src/stepExecutors/verify.js +41 -0
- package/plugins/workflow-runner/src/workflowState.js +30 -2
- package/src/infra/verification.js +228 -0
- package/src/plugins/pluginRunner.js +19 -2
- package/src/prompts/pluginList.js +54 -0
- package/src/prompts/promptAssembly.js +243 -0
- package/src/prompts/skillList.js +15 -20
- package/src/prompts/system.js +12 -167
- package/src/ui/App.js +1 -1
- package/workflows/plan.yaml +59 -47
|
@@ -0,0 +1,228 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ============================================================
|
|
3
|
+
* src/infra/verification.ts —— 共享验证基础设施
|
|
4
|
+
* ------------------------------------------------------------
|
|
5
|
+
* 从 plugins/ralph-wiggum/src/verificationGate.ts 提升为共享模块。
|
|
6
|
+
*
|
|
7
|
+
* 支持四种检查器(跨平台):
|
|
8
|
+
* - shell:<cmd> 跨平台 spawn(Windows 走 cmd /c, 其余 bash -c)
|
|
9
|
+
* - file_exists:<path> fs.existsSync
|
|
10
|
+
* - file_contains:<file>:<re> 正则匹配文件内容
|
|
11
|
+
* - test_count:<min> 跑 `bun test`,从 stdout 解析 "N pass"
|
|
12
|
+
*
|
|
13
|
+
* 消费者:
|
|
14
|
+
* - ralph-wiggum 插件(循环验证门)
|
|
15
|
+
* - workflow-runner 插件(verify 步骤类型)
|
|
16
|
+
* - 未来任何需要声明式验证的插件或模块
|
|
17
|
+
*
|
|
18
|
+
* 设计原则:
|
|
19
|
+
* - 纯工具函数,无全局状态,无副作用(除了 shell 子进程)
|
|
20
|
+
* - 不依赖任何插件代码,可被 src/ 和 plugins/ 同时导入
|
|
21
|
+
* - 工作目录隔离:所有相对路径以 getWorkingDir() 为锚
|
|
22
|
+
* ============================================================
|
|
23
|
+
*/
|
|
24
|
+
import { existsSync, readFileSync } from 'node:fs';
|
|
25
|
+
import { spawn } from 'node:child_process';
|
|
26
|
+
import { getWorkingDir } from '../bootstrap/workingDir.js';
|
|
27
|
+
/** 从 "type:value" 字符串解析为 Check 对象。 */
|
|
28
|
+
function parseVerifyArg(arg) {
|
|
29
|
+
const colonIdx = arg.indexOf(':');
|
|
30
|
+
if (colonIdx < 0)
|
|
31
|
+
return null;
|
|
32
|
+
const type = arg.slice(0, colonIdx).trim().toLowerCase();
|
|
33
|
+
const value = arg.slice(colonIdx + 1).trim();
|
|
34
|
+
switch (type) {
|
|
35
|
+
case 'shell':
|
|
36
|
+
return { type: 'shell', command: value, timeout: 30_000 };
|
|
37
|
+
case 'file_exists':
|
|
38
|
+
return { type: 'file_exists', file: value };
|
|
39
|
+
case 'file_contains': {
|
|
40
|
+
const sep = value.indexOf(':');
|
|
41
|
+
if (sep < 0)
|
|
42
|
+
return null;
|
|
43
|
+
return {
|
|
44
|
+
type: 'file_contains',
|
|
45
|
+
file: value.slice(0, sep),
|
|
46
|
+
pattern: value.slice(sep + 1),
|
|
47
|
+
};
|
|
48
|
+
}
|
|
49
|
+
case 'test_count': {
|
|
50
|
+
const count = parseInt(value, 10);
|
|
51
|
+
if (isNaN(count) || count < 0)
|
|
52
|
+
return null;
|
|
53
|
+
return { type: 'test_count', minCount: count };
|
|
54
|
+
}
|
|
55
|
+
default:
|
|
56
|
+
return null;
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
/**
|
|
60
|
+
* 从命令行参数字符串中提取所有 --verify 项。
|
|
61
|
+
* 兼容两种调用方式:
|
|
62
|
+
* - ralph-loop 的 args 字符串:`"goal --verify 'shell:bun test'"`
|
|
63
|
+
* - workflow verify 步骤的 checks 数组元素:`"shell:bun test"`
|
|
64
|
+
*/
|
|
65
|
+
export function parseVerifyArgs(args) {
|
|
66
|
+
const checks = [];
|
|
67
|
+
const regex = /--verify\s+("[^"]*"|\S+)/gi;
|
|
68
|
+
let match;
|
|
69
|
+
while ((match = regex.exec(args)) !== null) {
|
|
70
|
+
const raw = match[1].replace(/^"|"$/g, '');
|
|
71
|
+
const check = parseVerifyArg(raw);
|
|
72
|
+
if (check)
|
|
73
|
+
checks.push(check);
|
|
74
|
+
}
|
|
75
|
+
return checks;
|
|
76
|
+
}
|
|
77
|
+
/** 从单个 check 字符串解析(workflow verify 步骤用)。 */
|
|
78
|
+
export function parseCheckString(raw) {
|
|
79
|
+
return parseVerifyArg(raw);
|
|
80
|
+
}
|
|
81
|
+
function runShell(command, timeout) {
|
|
82
|
+
return new Promise((resolve) => {
|
|
83
|
+
const isWin = process.platform === 'win32';
|
|
84
|
+
const child = isWin
|
|
85
|
+
? spawn('cmd', ['/c', command], { timeout, env: process.env })
|
|
86
|
+
: spawn('bash', ['-c', command], { timeout, env: process.env });
|
|
87
|
+
let stdout = '';
|
|
88
|
+
let stderr = '';
|
|
89
|
+
child.stdout.on('data', (d) => {
|
|
90
|
+
stdout += d.toString();
|
|
91
|
+
});
|
|
92
|
+
child.stderr.on('data', (d) => {
|
|
93
|
+
stderr += d.toString();
|
|
94
|
+
});
|
|
95
|
+
child.on('error', () => {
|
|
96
|
+
resolve({ exitCode: null, stdout, stderr, errored: true });
|
|
97
|
+
});
|
|
98
|
+
child.on('close', (code) => {
|
|
99
|
+
resolve({ exitCode: code, stdout, stderr, errored: false });
|
|
100
|
+
});
|
|
101
|
+
});
|
|
102
|
+
}
|
|
103
|
+
export async function verifyShell(command, timeout) {
|
|
104
|
+
const r = await runShell(command, timeout);
|
|
105
|
+
if (r.errored) {
|
|
106
|
+
return {
|
|
107
|
+
check: { type: 'shell', command },
|
|
108
|
+
passed: false,
|
|
109
|
+
output: `执行失败`,
|
|
110
|
+
};
|
|
111
|
+
}
|
|
112
|
+
return {
|
|
113
|
+
check: { type: 'shell', command },
|
|
114
|
+
passed: r.exitCode === 0,
|
|
115
|
+
output: r.stdout.trim() || r.stderr.trim() || `exit code ${r.exitCode}`,
|
|
116
|
+
};
|
|
117
|
+
}
|
|
118
|
+
export function verifyFileExists(file) {
|
|
119
|
+
// 相对路径以 workingDir 为锚
|
|
120
|
+
const resolved = file.startsWith('/') || /^[A-Za-z]:/.test(file)
|
|
121
|
+
? file
|
|
122
|
+
: `${getWorkingDir()}/${file}`;
|
|
123
|
+
const exists = existsSync(resolved);
|
|
124
|
+
return {
|
|
125
|
+
check: { type: 'file_exists', file },
|
|
126
|
+
passed: exists,
|
|
127
|
+
output: exists ? '文件存在' : `文件不存在: ${resolved}`,
|
|
128
|
+
};
|
|
129
|
+
}
|
|
130
|
+
export function verifyFileContains(file, pattern) {
|
|
131
|
+
const resolved = file.startsWith('/') || /^[A-Za-z]:/.test(file)
|
|
132
|
+
? file
|
|
133
|
+
: `${getWorkingDir()}/${file}`;
|
|
134
|
+
try {
|
|
135
|
+
const content = readFileSync(resolved, 'utf8');
|
|
136
|
+
const regex = new RegExp(pattern);
|
|
137
|
+
const matched = regex.test(content);
|
|
138
|
+
return {
|
|
139
|
+
check: { type: 'file_contains', file, pattern },
|
|
140
|
+
passed: matched,
|
|
141
|
+
output: matched ? `文件包含 "${pattern}"` : `文件不包含 "${pattern}"`,
|
|
142
|
+
};
|
|
143
|
+
}
|
|
144
|
+
catch {
|
|
145
|
+
return {
|
|
146
|
+
check: { type: 'file_contains', file, pattern },
|
|
147
|
+
passed: false,
|
|
148
|
+
output: `无法读取文件: ${resolved}`,
|
|
149
|
+
};
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
async function verifyTestCount(minCount) {
|
|
153
|
+
const r = await runShell(`bun test`, 60_000);
|
|
154
|
+
const combined = `${r.stdout}\n${r.stderr}`;
|
|
155
|
+
if (r.errored) {
|
|
156
|
+
return {
|
|
157
|
+
check: { type: 'test_count', minCount },
|
|
158
|
+
passed: false,
|
|
159
|
+
output: '无法执行 bun test',
|
|
160
|
+
};
|
|
161
|
+
}
|
|
162
|
+
const matches = [...combined.matchAll(/(\d+)\s+pass/gi)];
|
|
163
|
+
const count = matches.length > 0 ? parseInt(matches[matches.length - 1][1], 10) : 0;
|
|
164
|
+
const passed = count >= minCount;
|
|
165
|
+
return {
|
|
166
|
+
check: { type: 'test_count', minCount },
|
|
167
|
+
passed,
|
|
168
|
+
output: `${count} pass (需要 >=${minCount})`,
|
|
169
|
+
};
|
|
170
|
+
}
|
|
171
|
+
/**
|
|
172
|
+
* 执行一组检查,返回聚合结果。
|
|
173
|
+
* 全部通过 → passed=true;任一失败 → passed=false + summary 列出失败项。
|
|
174
|
+
*/
|
|
175
|
+
export async function runVerification(checks) {
|
|
176
|
+
if (checks.length === 0) {
|
|
177
|
+
return { passed: true, details: [], summary: '无验证项' };
|
|
178
|
+
}
|
|
179
|
+
const results = [];
|
|
180
|
+
for (const check of checks) {
|
|
181
|
+
let result;
|
|
182
|
+
switch (check.type) {
|
|
183
|
+
case 'shell':
|
|
184
|
+
result = await verifyShell(check.command ?? '', check.timeout ?? 30_000);
|
|
185
|
+
break;
|
|
186
|
+
case 'file_exists':
|
|
187
|
+
result = verifyFileExists(check.file ?? '');
|
|
188
|
+
break;
|
|
189
|
+
case 'file_contains':
|
|
190
|
+
result = verifyFileContains(check.file ?? '', check.pattern ?? '');
|
|
191
|
+
break;
|
|
192
|
+
case 'test_count':
|
|
193
|
+
result = await verifyTestCount(check.minCount ?? 0);
|
|
194
|
+
break;
|
|
195
|
+
default:
|
|
196
|
+
result = {
|
|
197
|
+
check,
|
|
198
|
+
passed: false,
|
|
199
|
+
output: `未知验证类型: ${check.type}`,
|
|
200
|
+
};
|
|
201
|
+
}
|
|
202
|
+
results.push(result);
|
|
203
|
+
}
|
|
204
|
+
const allPassed = results.every((r) => r.passed);
|
|
205
|
+
const failedNames = results.filter((r) => !r.passed).map((r) => formatCheckName(r.check));
|
|
206
|
+
let summary;
|
|
207
|
+
if (allPassed) {
|
|
208
|
+
summary = `✅ 全部通过 (${results.length}/${results.length})`;
|
|
209
|
+
}
|
|
210
|
+
else {
|
|
211
|
+
summary = `❌ 验证未通过: ${failedNames.join(', ')}`;
|
|
212
|
+
}
|
|
213
|
+
return { passed: allPassed, details: results, summary };
|
|
214
|
+
}
|
|
215
|
+
function formatCheckName(check) {
|
|
216
|
+
switch (check.type) {
|
|
217
|
+
case 'shell':
|
|
218
|
+
return `shell(${(check.command ?? '').slice(0, 40)})`;
|
|
219
|
+
case 'file_exists':
|
|
220
|
+
return `exists(${check.file})`;
|
|
221
|
+
case 'file_contains':
|
|
222
|
+
return `contains(${check.file}:${check.pattern})`;
|
|
223
|
+
case 'test_count':
|
|
224
|
+
return `tests(>=${check.minCount})`;
|
|
225
|
+
default:
|
|
226
|
+
return check.type;
|
|
227
|
+
}
|
|
228
|
+
}
|
|
@@ -28,10 +28,27 @@ export async function* runWithPlugins(userInput, options) {
|
|
|
28
28
|
});
|
|
29
29
|
return;
|
|
30
30
|
}
|
|
31
|
-
await discoverPlugins();
|
|
31
|
+
const manifests = await discoverPlugins();
|
|
32
32
|
const matched = resolveCommand(userInput);
|
|
33
33
|
if (!matched) {
|
|
34
|
-
//
|
|
34
|
+
// 提取命令名用于错误提示
|
|
35
|
+
const cmdName = userInput.trimStart().split(/\s+/)[0]; // "/workflow" 或 "/plan"
|
|
36
|
+
// 有插件但未命中该命令 → 明确提示可用命令
|
|
37
|
+
if (manifests.length > 0) {
|
|
38
|
+
const cmdList = manifests.flatMap((m) => m.commands.map((c) => `/${c.name}`)).join('、');
|
|
39
|
+
yield {
|
|
40
|
+
type: 'error',
|
|
41
|
+
error: `未找到命令 "${cmdName}"。已安装的插件命令:${cmdList || '(无)'}。`,
|
|
42
|
+
};
|
|
43
|
+
}
|
|
44
|
+
else {
|
|
45
|
+
// 无任何插件 → 告知用户
|
|
46
|
+
yield {
|
|
47
|
+
type: 'error',
|
|
48
|
+
error: `当前未安装任何插件。命令 "${cmdName}" 无法执行。`,
|
|
49
|
+
};
|
|
50
|
+
}
|
|
51
|
+
// fallback 到普通 TAOR 对话,用户可继续操作
|
|
35
52
|
yield* runQuery(userInput, {
|
|
36
53
|
provider,
|
|
37
54
|
history,
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* src/prompts/pluginList.ts —— 插件列表生成
|
|
3
|
+
*
|
|
4
|
+
* 与 skillList.ts 对称设计:
|
|
5
|
+
* - getPluginMetaList() ← 对标 getSkillList()
|
|
6
|
+
* - formatPluginHint() ← 对标 formatSkillHint()
|
|
7
|
+
*
|
|
8
|
+
* 数据源:commandRouter.discoverPlugins()(纯元数据提取,不加载代码)
|
|
9
|
+
*/
|
|
10
|
+
import { discoverPlugins } from '../plugins/commandRouter.js';
|
|
11
|
+
/**
|
|
12
|
+
* 扫描 plugins/ 目录,返回已安装插件的元数据摘要。
|
|
13
|
+
* 复用 discoverPlugins() 的缓存结果,只提取轻量信息。
|
|
14
|
+
*/
|
|
15
|
+
export async function getPluginMetaList() {
|
|
16
|
+
const manifests = await discoverPlugins();
|
|
17
|
+
return manifests.map((m) => ({
|
|
18
|
+
name: m.name,
|
|
19
|
+
description: m.description ?? '(no description)',
|
|
20
|
+
commands: m.commands.map((c) => ({
|
|
21
|
+
name: c.name,
|
|
22
|
+
description: c.description,
|
|
23
|
+
})),
|
|
24
|
+
}));
|
|
25
|
+
}
|
|
26
|
+
/**
|
|
27
|
+
* 格式化插件列表为系统提示词片段。
|
|
28
|
+
*
|
|
29
|
+
* 风格对齐 skillList.formatSkillHint():
|
|
30
|
+
* - 每个插件固定格式
|
|
31
|
+
* - 明确标注触发方式("必须由用户输入 /xxx 触发")
|
|
32
|
+
* - 无插件时返回空字符串(调用方自行决定是否追加)
|
|
33
|
+
*/
|
|
34
|
+
export function formatPluginHint(plugins) {
|
|
35
|
+
if (plugins.length === 0)
|
|
36
|
+
return '';
|
|
37
|
+
const lines = [
|
|
38
|
+
'# 可用插件(/命令)',
|
|
39
|
+
'',
|
|
40
|
+
'以下是当前已安装的插件。**插件只能由用户通过 /xxx 命令触发——你绝对不能代替用户执行、模拟或建议使用插件功能**。',
|
|
41
|
+
'只有当用户明确询问"有哪些命令/插件可用"时,才列出以下内容。其他时候不要提及插件。',
|
|
42
|
+
'',
|
|
43
|
+
];
|
|
44
|
+
for (const p of plugins) {
|
|
45
|
+
lines.push(`## /${p.name}`);
|
|
46
|
+
lines.push(`- **描述**: ${p.description}`);
|
|
47
|
+
if (p.commands.length > 0) {
|
|
48
|
+
const cmdList = p.commands.map((c) => `\`/${c.name}\``).join('、');
|
|
49
|
+
lines.push(`- **命令**: ${cmdList}`);
|
|
50
|
+
}
|
|
51
|
+
lines.push('');
|
|
52
|
+
}
|
|
53
|
+
return lines.join('\n');
|
|
54
|
+
}
|
|
@@ -0,0 +1,243 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* src/prompts/promptAssembly.ts —— 统一提示词组装器
|
|
3
|
+
*
|
|
4
|
+
* 设计目标:
|
|
5
|
+
* 1. 所有消费者(TUI / workflow / gateway)共享同一个"基础内核"
|
|
6
|
+
* 2. Skill 和 Plugin 都是基础内核的一等公民(Layer 3 + Layer 4)
|
|
7
|
+
* 3. 消费者只在基础之上做增量追加,不做替换或删减
|
|
8
|
+
*
|
|
9
|
+
* 五层架构:
|
|
10
|
+
* Layer 0: 内核身份(身份/立场/风格/行为约束)
|
|
11
|
+
* Layer 1: 环境感知(cwd / OS / 日期)
|
|
12
|
+
* Layer 2: 工具目录(工具列表 + 使用规范)
|
|
13
|
+
* Layer 3: 技能目录(skill 列表 + 触发词)
|
|
14
|
+
* Layer 4: 插件目录(plugin 列表 + 命令 + 触发方式)
|
|
15
|
+
* Layer 5: 项目指令(minimal-agent.md,可选)
|
|
16
|
+
*/
|
|
17
|
+
import { homedir } from 'node:os';
|
|
18
|
+
import { loadProjectInstructions } from './projectInstructions.js';
|
|
19
|
+
import { getSkillList, formatSkillHint } from './skillList.js';
|
|
20
|
+
import { getPluginMetaList, formatPluginHint } from './pluginList.js';
|
|
21
|
+
// ==================== Layer 0: 内核身份 ====================
|
|
22
|
+
const CORE_IDENTITY = `# 思维与立场(核心:像专家一样独立思考)
|
|
23
|
+
- **你是工程协作者,不是奉承者**。不无条件夸赞、不为附和而附和、不同意时直说。不用"很好的问题"、"你说得对"、"完全同意"这类前缀——直接答内容。
|
|
24
|
+
- **基于证据,不基于揣测用户想听什么**。没读过的代码先 Read,没验证过的行为先验证,不凭印象答。回答前先问自己:这个判断有具体证据吗,还是我在猜?
|
|
25
|
+
- **不知道就说不知道**。明确边界:"这个版本号我不确定"、"这个 API 我没看过文档"、"这部分行为我没验证过"。胡编一个看起来对的答案,代价远高于承认不知道。
|
|
26
|
+
- **明示 confidence 分层**。结论按确定度分层表达——"已 verify / 高度确信"、"基于代码合理推测"、"未验证猜测"——让用户基于你的不确定程度决策,不要把推测说成事实。
|
|
27
|
+
- **主动质疑用户的前提 / 方案 / 诊断**。发现方向不对时直说"这里我觉得不对,理由是 ...",给出你的看法 + 证据 + 让用户拍板。不要为了维持氛围而顺着错的方向走。
|
|
28
|
+
- **给根因不给表面答案**。bug 报告先定位 root cause 再给 fix,不要先给个让症状消失的 workaround 蒙混过去。修复要能解释"为什么这样改就对了"。
|
|
29
|
+
- **任何方案都有代价**。主动说 trade-off(性能 vs 可读、激进 vs 保守、本期 vs 长期),让用户基于全貌决策,不要只夸方案的优点。
|
|
30
|
+
- **被指出错误时直接承认 + 修正**。不解释、不防御、不找借口。承认 → 给出修正 → 避免下次。错误是 update prior 的信号,不是要消化的尴尬。
|
|
31
|
+
- **读过就别反复读,验证过就别反复验**。本轮已 Read 且未变的内容、已经 verify 的结论,直接拿来用——重复 Read 同一未变区域、反复 grep 同一答案,是不自信的空转而非严谨。真严谨是"一次读对、读透",不是"读很多次";读完就基于已读内容推进。
|
|
32
|
+
|
|
33
|
+
# 输出风格
|
|
34
|
+
- 用中文回答用户。
|
|
35
|
+
- 简明扼要,避免重复解释代码做了什么 —— 让代码自身说话。
|
|
36
|
+
- 报错时给出根因,不要只复述错误消息。
|
|
37
|
+
- 不主动跑测试或 typecheck,除非用户要求。
|
|
38
|
+
|
|
39
|
+
# 行为约束
|
|
40
|
+
- 没有用户许可不要执行破坏性操作(rm -rf / git reset --hard / git push --force 等)。
|
|
41
|
+
- **提问克制原则(重要!)**:只在以下**三种情况**才使用 AskUserQuestion 提问用户,其他情况一律自己判断、选推荐项继续:
|
|
42
|
+
1. **信息不全且无法推断**:用户请求缺少关键信息(如"部署到哪个环境"),且无法从上下文/代码/文件中找到答案;
|
|
43
|
+
2. **多方案各有真实优劣**:存在多个可行方案,每个都有明确 trade-off(不是你懒得选),且选择会影响后续方向;
|
|
44
|
+
3. **不可逆破坏性操作前确认**:涉及数据丢失或不可回滚的操作(如删除数据库、覆盖生产配置)。
|
|
45
|
+
**不符合以上三条的,不要提问。** "有取舍就问"是错误的理解——大多数取舍你应该自己判断并选推荐项继续。只缺一点小信息时自己推断或用一句话默认假设即可。
|
|
46
|
+
- 任务完成后用一两句话总结改了什么,不要长篇大论。
|
|
47
|
+
|
|
48
|
+
# 修改 vs 讨论(动手边界,重要)
|
|
49
|
+
先判断用户要的是"动手改"还是"聊一聊",再决定要不要写盘——默认克制、被要求才行动:
|
|
50
|
+
- **默认是讨论/分析/给方案**:除非判定用户要你改,否则只回答、只给建议,**不调用 Edit/Write/MultiEdit、不跑会改状态或破坏性的 Bash**。未经允许的修改和"不读就改"一样是越界。
|
|
51
|
+
- **只读工具不受此限**:Read/Grep/Glob/WebSearch/WebFetch 任何时候都能直接用——读代码、查资料是"理解"不是"动手",讨论也要先把事实读准。
|
|
52
|
+
- **命中修改意图就果断改,别反问试探**:祈使式动词(改/加/删/修/修复/重构/实现/优化/重命名/迁移/接上/补全…)、明确的"直接改/帮我写/动手/落地"、或一个可执行的 bug 修复请求——都是放行信号,直接干。别再用"要不要我帮你改"来回打转,那是讨好型的犹豫。
|
|
53
|
+
- **纯讨论意图就别擅自改**:以"为什么/是不是/能不能/看看/评估下/解释下/你怎么看/可行吗/有没有问题"为主、没有修改祈使的,给分析和建议即可,不要顺手把代码改了。
|
|
54
|
+
- **只有"讨论还是修改"真拿不准时**,才用一句话澄清("你是想我直接改,还是先讨论方案?")再停;能从措辞判断的就别问。
|
|
55
|
+
|
|
56
|
+
# 中断支持
|
|
57
|
+
- 用户可以随时按 ESC 或 Ctrl+C 中断你的工作。
|
|
58
|
+
- 中断后等待用户输入新任务,不要自行继续执行被中断的操作。
|
|
59
|
+
- 如果用户输入了新任务,直接响应新任务,不必提及之前被中断的操作。
|
|
60
|
+
|
|
61
|
+
# 任务边界与上下文关联(每轮开工前自检)
|
|
62
|
+
用户不会一问一会话,历史里常混着已结束的旧任务。每轮开工前先答三个问题,再决定怎么用上下文:
|
|
63
|
+
1. **这是新任务还是上一任务的延续?**——"继续/接着做/再加一个/还有"=延续,保持当前任务上下文;"帮我做 X/这次做 Y/另外"=多半是新任务;"重新开始/不算了,做 X"=立即放下旧任务。
|
|
64
|
+
2. **历史里哪些和当前问题相关?**——只把与当前问题直接相关的消息、文件、工具结果当活动上下文;无关的旧任务细节当背景,**不要拿旧任务的结论/文件/方案硬套当前问题**(典型翻车:上个任务在改 A,这次问的是 B,却惯性去翻 A)。
|
|
65
|
+
3. **判不准边界就先确认**:一句话澄清"你是继续刚才的 X,还是开始新的?",别默默假设。
|
|
66
|
+
- 当前用户消息 + 紧邻的最近几条消息,权重永远高于更早历史;越早、越不相关的内容参考价值越低。
|
|
67
|
+
|
|
68
|
+
# 上下文隔离(重要!工作流/命令执行完毕后的行为规则)
|
|
69
|
+
- /plan、/workflow 等命令执行完后**自动回到普通对话模式**——下一条用户输入默认是全新任务。
|
|
70
|
+
- **只有当用户明确提到延续意图时**("继续"、"接着做"、"按照计划执行"、"读一下 PLAN.md"、"修改刚才的结果"),才去关联之前的工作流产物(PLAN.md、生成的文件等)。
|
|
71
|
+
- 用户提出全新任务时(如"帮我写个函数"、"这个bug怎么修"、"review 下代码"),**不要主动 Read 之前的 PLAN.md 或引用上次工作流的结果**——那是上一次的产出,与当前任务无关。
|
|
72
|
+
- 当前目录可能存在之前 /plan 留下的 PLAN.md 文件:判断标准是**用户消息是否包含对之前产出的引用**?没有就当新任务处理。
|
|
73
|
+
- 如果你不确定是新任务还是延续,优先按**新任务**处理——宁可多问一句也不要用旧上下文污染新工作。
|
|
74
|
+
|
|
75
|
+
# 中断后调整(重要!)
|
|
76
|
+
- 用户可能在上一步 /plan 或 /workflow 执行中途按 ESC/Ctrl+C 中断,然后立即输入调整指令(如"把第3步改成xxx"、"换个方向重新计划")。
|
|
77
|
+
- **检测方法**:如果用户消息包含调整/修改/重做/换方向 等词 + 历史中有 /plan 或 /workflow 的痕迹 → 这是**中断后调整**,不是全新任务。
|
|
78
|
+
- **小调整处理**:先 Read ./PLAN.md 了解当前计划状态,然后直接在当前 TAOR 对话中完成修改(Edit 写入 PLAN.md 或用工具产出新文件)。不需要重新跑 /plan 工作流。
|
|
79
|
+
- **大改/重做处理**:如果用户要求完全重做或换方向,建议用户用 '/plan 新目标' 重新跑完整流程——这会覆盖旧的 PLAN.md 并从头开始。
|
|
80
|
+
- **不要混淆**:"中断后调整"和"全新任务"的关键区别是**用户是否提到了对之前产出的修改意图**。提到修改→读 PLAN.md 继续接着做;没提→当新任务。`;
|
|
81
|
+
// ==================== Layer 1: 环境感知 ====================
|
|
82
|
+
function buildEnvLayer(cwd) {
|
|
83
|
+
const today = new Date().toISOString().slice(0, 10);
|
|
84
|
+
return `你是 minimal-agent,一个最小化的 AI 编程助手。
|
|
85
|
+
你运行在用户的本地终端里,可以读取、修改、创建文件,可以搜索文件名和文件内容,可以联网搜索最新信息。
|
|
86
|
+
|
|
87
|
+
# 工作环境
|
|
88
|
+
- 当前工作目录: ${cwd}
|
|
89
|
+
- 用户主目录: ${homedir()}
|
|
90
|
+
- 平台: ${process.platform}
|
|
91
|
+
- 当前日期: ${today}
|
|
92
|
+
|
|
93
|
+
# 时间感知规则(重要!搜索前必读)
|
|
94
|
+
- **每次执行 WebSearch 前,必须先确认当前日期**
|
|
95
|
+
- 当前日期:${today}
|
|
96
|
+
- 搜索时必须在 query 末尾附带日期限制,如 "... as of [日期]" 或 "... ${today}"
|
|
97
|
+
- 例如搜索日本政策时写:"日本入境政策 ${today}"
|
|
98
|
+
- 禁止使用 "现在"、"最近"、"最新" 等模糊时间词——必须用具体日期`;
|
|
99
|
+
}
|
|
100
|
+
// ==================== Layer 2: 工具目录 ====================
|
|
101
|
+
function buildToolLayer(tools) {
|
|
102
|
+
const toolList = tools.map((t) => ` - ${t.name}`).join('\n');
|
|
103
|
+
return `# 可用工具
|
|
104
|
+
${toolList}
|
|
105
|
+
|
|
106
|
+
# 工具使用规范
|
|
107
|
+
- 修改既有文件之前,必须用 Read 工具读取相关代码的最新内容。
|
|
108
|
+
"我记得"、"我之前看过"、"应该差不多"都不算读过——必须在本轮任务中实际调用 Read。
|
|
109
|
+
尤其当你要参考某个文件的写法来实现类似功能时,必须先重读该文件的关键部分(函数实现、判断逻辑、数据流),理解清楚后再动手。
|
|
110
|
+
反过来:**本轮已 Read 过、且你没用 Edit/Write 改动过的区域,不要重复 Read**——直接基于已读内容操作。如果你重复读取同一未变区域,结果头部会出现"已读过该区域"的提示,看到就停止重读、立刻用已有内容推进(重复读既费轮次又是死循环前兆)。
|
|
111
|
+
- Edit / MultiEdit 拦截 fallback:如果你忘了先 Read 就直接调 Edit/MultiEdit,错误响应里会**自动附带当前文件前 200 行 / 32KB 以内的 snippet**(带行号)。这种情况下**直接基于 snippet 重新构造 old_string 再发一次 Edit 即可,不要再额外调 Read**。只有当 snippet 显示"未注入内容"(文件过大或二进制)时才需要显式 Read。Write 不附带 snippet——整文件覆盖语义下必须先显式 Read,避免基于片段重写导致截断范围外内容丢失。
|
|
112
|
+
- Edit 工具的 old_string 必须在文件中唯一;不唯一时请扩大上下文或显式 replace_all=true。
|
|
113
|
+
- 同一文件需要修改多处(3 处及以上)时优先用 MultiEdit:所有 edit 按顺序在内存中应用,**全部成功才落盘**;任一失败磁盘不动,避免中间状态污染。单点修改继续用 Edit。
|
|
114
|
+
- Write 覆盖既有文件前同样必须先 Read(与 Edit 对称,未先 Read 会被拒绝并提示"请先 Read");写新文件无此要求。
|
|
115
|
+
- 创建新文件用 Write,或 Edit 时 old_string 传空字符串。
|
|
116
|
+
- 找文件用 Glob("**/*.ts"),找文件内容用 Grep(基于 ripgrep)。
|
|
117
|
+
- 当用户问到训练截止后才出现的信息(最新版本号、近期新闻、第三方 API 文档)时用 WebSearch;优先精确的自然语言查询。
|
|
118
|
+
- 当需要执行终端命令(安装依赖、运行测试/构建/lint、git 操作、文件系统操作等)时用 Bash。
|
|
119
|
+
⚠️ **Bash 是万能工具,但必须作为最后手段**:当 Read/Edit/Write/Glob/Grep 等专用工具无法完成任务时,才考虑用 Bash。
|
|
120
|
+
不要用 Bash 执行 find/grep/cat/head/tail/sed/awk/echo 等命令来替代专用工具——专用工具提供更好的用户体验和可审计性。
|
|
121
|
+
Bash 有安全黑名单(rm -rf /、mkfs、shutdown 等危险命令会被拦截),但仍需谨慎:
|
|
122
|
+
先确认命令无害再执行,避免不可逆操作(如 git push --force、git reset --hard)。
|
|
123
|
+
长时间运行的命令(npm install、bun test)注意超时设置;需要交互输入的命令不要用 Bash(用 Write 写脚本代替)。
|
|
124
|
+
Bash 已识别"信息性退出码":grep/rg/find/diff/test 的 exit=1(无匹配 / 部分不可访问 / 有差异 / 条件假)会自动判为成功,不要因 1 而重试。
|
|
125
|
+
破坏性命令(git reset --hard / git push --f / rm -rf 等)会被 Bash 主动在输出头部加 ⚠️ 警告(不拦截),看到时应向用户确认意图。
|
|
126
|
+
- 当需要获取网页静态文本内容(抓取文档、读取文章)时用 WebFetch。
|
|
127
|
+
WebBrowser 依赖可选包(playwright-core + chromium)—— **默认假定未安装**,
|
|
128
|
+
仅在 WebFetch 明确无法满足(如需要 JS 渲染后的内容、点击按钮、填表单、截图)时再尝试。
|
|
129
|
+
如果 WebBrowser 报"无法启动浏览器/缺少依赖",不要重试,改回 WebFetch 或告知用户安装依赖。
|
|
130
|
+
- 一次回答里可以并行调用多个只读工具(Read/Glob/Grep/WebSearch/WebFetch),写工具(Edit/Write/Bash)请逐个执行。
|
|
131
|
+
- 需要**用户拍板**时用 AskUserQuestion 提结构化选择题(最多 4 题、每题 2-4 选项;第一个选项放你的推荐并在 label 末尾标「(推荐)」):
|
|
132
|
+
① 需求/方案有取舍、没有绝对对错(多个合理方案各有利弊)→ 让用户选;② 关键信息缺失或模糊、继续做风险大 → 让用户补充。
|
|
133
|
+
能自己从代码/上下文/常识判断出答案的别问(免打断);界面会自动附「其他(自由输入)」逃生项,无需你手动加。
|
|
134
|
+
|
|
135
|
+
# 文件读取规范(重要!防止上下文污染)
|
|
136
|
+
## 读取策略(按场景选择)
|
|
137
|
+
### 场景 1:不确定文件内容
|
|
138
|
+
→ 先 Grep("关键字", path="文件路径") 定位
|
|
139
|
+
→ 再 Read(file_path, offset=X, limit=Y) 读指定区域
|
|
140
|
+
|
|
141
|
+
### 场景 2:知道要改哪个函数
|
|
142
|
+
→ 直接 Read(file_path, offset=1, limit=400) 先读前 400 行
|
|
143
|
+
或者用 Grep 找到行号后 Read(file_path, offset=行号, limit=200)
|
|
144
|
+
|
|
145
|
+
### 场景 3:大文件(>256KB)
|
|
146
|
+
→ 不要一次性读,用 Grep 定位关键部分
|
|
147
|
+
→ 分多次小范围 Read,每次不超过 200 行
|
|
148
|
+
例如:Read(file, offset=1, limit=100) → Read(file, offset=101, limit=100)
|
|
149
|
+
|
|
150
|
+
### 场景 4:需要看 imports/exports
|
|
151
|
+
→ Read(file, offset=1, limit=50) 只读文件头部
|
|
152
|
+
|
|
153
|
+
## 警惕信号
|
|
154
|
+
- 遇到 "文件过大" 错误 → 说明你读了不该读的大文件,改用分段读
|
|
155
|
+
- 遇到 "输出超过 30000 字符" → 说明单次读太多,改小 limit`;
|
|
156
|
+
}
|
|
157
|
+
/**
|
|
158
|
+
* 组装完整的"基础内核"系统提示词(Layer 0-5)。
|
|
159
|
+
*
|
|
160
|
+
* 这是所有消费者的统一来源。TUI 直接使用;workflow 在此基础上增量追加。
|
|
161
|
+
*/
|
|
162
|
+
export async function assembleBasePrompt(options) {
|
|
163
|
+
const { cwd, tools, includeProjectInstructions = true, includePlugins = true, } = options;
|
|
164
|
+
const parts = [];
|
|
165
|
+
// Layer 0+1: 内核身份 + 环境感知
|
|
166
|
+
parts.push(buildEnvLayer(cwd));
|
|
167
|
+
parts.push(CORE_IDENTITY);
|
|
168
|
+
// Layer 2: 工具目录
|
|
169
|
+
parts.push(buildToolLayer(tools));
|
|
170
|
+
// Layer 3: 技能目录(实时扫描 skills/)
|
|
171
|
+
const skills = await getSkillList();
|
|
172
|
+
const skillHint = formatSkillHint(skills);
|
|
173
|
+
if (skillHint)
|
|
174
|
+
parts.push(skillHint);
|
|
175
|
+
// Layer 4: 插件目录(可选增量,异常时不阻塞基础内核)
|
|
176
|
+
if (includePlugins) {
|
|
177
|
+
try {
|
|
178
|
+
const plugins = await getPluginMetaList();
|
|
179
|
+
const pluginHint = formatPluginHint(plugins);
|
|
180
|
+
if (pluginHint)
|
|
181
|
+
parts.push(pluginHint);
|
|
182
|
+
}
|
|
183
|
+
catch {
|
|
184
|
+
// 插件扫描失败(目录损坏/权限错误/yaml语法错误等)不影响基础内核
|
|
185
|
+
// dlog('prompt', 'plugin scan failed, skipping Layer 4');
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
// Layer 5: 项目指令(可选)
|
|
189
|
+
if (includeProjectInstructions) {
|
|
190
|
+
const projectInstructions = await loadProjectInstructions(cwd);
|
|
191
|
+
if (projectInstructions) {
|
|
192
|
+
parts.push([
|
|
193
|
+
'',
|
|
194
|
+
'--- 项目指令(minimal-agent.md) ---',
|
|
195
|
+
'',
|
|
196
|
+
'⚠️ 时效性声明:',
|
|
197
|
+
'- 以下内容来源于项目根目录的 `minimal-agent.md` 文件,具有时效性,可能已过时。',
|
|
198
|
+
'- 它是参考线索,不是绝对真理。当它与实际代码或文件内容冲突时,以代码为准。',
|
|
199
|
+
'- 涉及代码和项目文件的任何操作前,必须先用 Read / Grep 工具确认当前代码的实际状态,再基于最新信息做决策,不要仅凭此文档的描述操作。',
|
|
200
|
+
'- 同步规则:仅当你在**本轮任务中实际用 Write/Edit/MultiEdit 改动了代码或项目结构**、且这些改动使 `minimal-agent.md` 某处描述过时时,才在本轮结束的总结里用一句话**指出过时的具体位置**(哪一节 / 哪条),并询问用户是否要同步、或建议运行 `/init` 重建。',
|
|
201
|
+
'- 除非用户已明确授权,**不要擅自 Write 改写 `minimal-agent.md`**(它通常已纳入 git,盲目重写会污染 diff;任务进行中代码尚未定型,过早同步会把暂态写进文档、反而加重误导)。纯讨论 / 只读 / 搜索类的轮次不做任何同步动作。',
|
|
202
|
+
'',
|
|
203
|
+
projectInstructions,
|
|
204
|
+
].join('\n'));
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
return parts.join('\n\n');
|
|
208
|
+
}
|
|
209
|
+
/**
|
|
210
|
+
* 只生成 Layer 3(skill 列表)+ Layer 4(plugin 列表)的轻量提示词。
|
|
211
|
+
*
|
|
212
|
+
* 用于 workflow 的 llm 步骤(tools=[],不能执行工具但需要知道能力边界,
|
|
213
|
+
* 以便在输出文本中引用可用的 skill/plugin)。
|
|
214
|
+
*/
|
|
215
|
+
export async function assembleCapabilityHint(cwd) {
|
|
216
|
+
const parts = [];
|
|
217
|
+
let hasContent = false;
|
|
218
|
+
// Layer 3: Skill 目录
|
|
219
|
+
const skills = await getSkillList();
|
|
220
|
+
const skillHint = formatSkillHint(skills);
|
|
221
|
+
if (skillHint) {
|
|
222
|
+
parts.push(skillHint);
|
|
223
|
+
hasContent = true;
|
|
224
|
+
}
|
|
225
|
+
// Layer 4: Plugin 目录(可选增量,异常时跳过)
|
|
226
|
+
try {
|
|
227
|
+
const plugins = await getPluginMetaList();
|
|
228
|
+
const pluginHint = formatPluginHint(plugins);
|
|
229
|
+
if (pluginHint) {
|
|
230
|
+
parts.push(pluginHint);
|
|
231
|
+
hasContent = true;
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
catch {
|
|
235
|
+
// 插件扫描失败不影响 skill hint
|
|
236
|
+
}
|
|
237
|
+
if (!hasContent)
|
|
238
|
+
return '';
|
|
239
|
+
return ('\n\n--- 能力参考 ---\n' +
|
|
240
|
+
'以下是你可用的技能和插件(本步骤不能直接调用工具,但编写计划/回答时应考虑它们的存在,必要时建议用户后续使用):\n' +
|
|
241
|
+
parts.join('\n\n') +
|
|
242
|
+
'\n');
|
|
243
|
+
}
|
package/src/prompts/skillList.js
CHANGED
|
@@ -1,17 +1,16 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* ============================================================
|
|
3
|
-
* src/prompts/skillList.ts —— Skill
|
|
3
|
+
* src/prompts/skillList.ts —— Skill 列表生成
|
|
4
4
|
* ------------------------------------------------------------
|
|
5
|
-
*
|
|
5
|
+
* 设计原则:
|
|
6
6
|
* 1. System Prompt 里只注入 frontmatter 元数据轻量列表
|
|
7
|
-
* → 让
|
|
7
|
+
* → 让 LLM 知道"有哪些技能可用 + 功能是什么"
|
|
8
8
|
* → 不加载完整 SKILL.md(省 token,按需加载)
|
|
9
9
|
* 2. LLM 决定使用某个 skill 时,用 Read 工具读取对应 SKILL.md
|
|
10
10
|
* → 类似 lazy loading:知道存在 → 需要时才加载详情
|
|
11
|
-
* 3.
|
|
12
|
-
* 4.
|
|
13
|
-
*
|
|
14
|
-
* type 和 triggers 字段(缺省时给出占位提示)。
|
|
11
|
+
* 3. 格式简洁:每个 skill 固定 2-3 行信息
|
|
12
|
+
* 4. 仅依赖 frontmatter 的 name / description 字段,
|
|
13
|
+
* 不依赖 triggers(并非所有 skill 都声明触发词)。
|
|
15
14
|
* ============================================================
|
|
16
15
|
*/
|
|
17
16
|
import { readFile, readdir } from 'node:fs/promises';
|
|
@@ -33,14 +32,12 @@ function parseFrontmatter(content, sourceDir) {
|
|
|
33
32
|
const nameMatch = frontmatter.match(/^name:\s*(.+)$/m);
|
|
34
33
|
const descMatch = frontmatter.match(/^description:\s*(.+)$/m);
|
|
35
34
|
const typeMatch = frontmatter.match(/^type:\s*(.+)$/m);
|
|
36
|
-
const triggersMatch = frontmatter.match(/^triggers:\s*(.+)$/m);
|
|
37
35
|
if (!nameMatch || !descMatch)
|
|
38
36
|
return null;
|
|
39
37
|
return {
|
|
40
38
|
name: stripQuotes(nameMatch[1]),
|
|
41
39
|
description: stripQuotes(descMatch[1]),
|
|
42
40
|
type: typeMatch ? stripQuotes(typeMatch[1]) : undefined,
|
|
43
|
-
triggers: triggersMatch ? stripQuotes(triggersMatch[1]) : undefined,
|
|
44
41
|
sourceDir,
|
|
45
42
|
};
|
|
46
43
|
}
|
|
@@ -75,18 +72,18 @@ export async function getSkillList() {
|
|
|
75
72
|
return skills;
|
|
76
73
|
}
|
|
77
74
|
/**
|
|
78
|
-
*
|
|
75
|
+
* 生成 skill 列表提示词。
|
|
79
76
|
*
|
|
80
|
-
* 输出格式(每个 skill 固定 3 行):
|
|
81
|
-
* ## /skill-name
|
|
82
|
-
* - **触发时机**: 用户说 "xxx"、"yyy" 时使用
|
|
77
|
+
* 输出格式(每个 skill 固定 2-3 行):
|
|
78
|
+
* ## /skill-name
|
|
83
79
|
* - **功能**: 一句话描述这个 skill 做什么
|
|
84
80
|
* - **类型**: prompt / local / local-jsx
|
|
85
81
|
*
|
|
86
82
|
* 关键设计决策:
|
|
87
83
|
* - 不加载 SKILL.md 全文(节省 ~1500+ tokens/skill)
|
|
88
|
-
* - 只输出 name + description(每个 skill ~
|
|
89
|
-
* -
|
|
84
|
+
* - 只输出 name + description(每个 skill ~30 tokens)
|
|
85
|
+
* - 不依赖 triggers 字段(并非所有 skill 都声明触发词)
|
|
86
|
+
* - LLM 根据 description 语义匹配决定是否使用
|
|
90
87
|
*/
|
|
91
88
|
export function formatSkillHint(skills) {
|
|
92
89
|
if (skills.length === 0) {
|
|
@@ -96,15 +93,13 @@ export function formatSkillHint(skills) {
|
|
|
96
93
|
// ---- 标题段 ----
|
|
97
94
|
lines.push('# 可用技能(/命令)');
|
|
98
95
|
lines.push('');
|
|
99
|
-
lines.push('以下是当前可用的技能(**双源**:用户项目目录 + minimal-agent
|
|
96
|
+
lines.push('以下是当前可用的技能(**双源**:用户项目目录 + minimal-agent 包目录,同名时项目优先)。**当用户需求与某个技能的功能描述匹配时,你应该主动使用它**。');
|
|
100
97
|
lines.push('在使用前,先用 Read 工具读取下方每个技能卡片里的 `**技能目录**` 路径 + `/SKILL.md` 了解完整流程。');
|
|
101
98
|
lines.push('');
|
|
102
99
|
// ---- 每个技能的卡片 ----
|
|
103
100
|
for (const skill of skills) {
|
|
104
101
|
const type = skill.type ?? 'prompt(展开为系统提示词,指导模型执行复杂任务)';
|
|
105
|
-
const triggers = skill.triggers ?? '(详见 SKILL.md)';
|
|
106
102
|
lines.push(`## ${skill.name}`);
|
|
107
|
-
lines.push(`- **触发时机**: ${triggers}`);
|
|
108
103
|
lines.push(`- **功能**: ${skill.description}`);
|
|
109
104
|
lines.push(`- **类型**: ${type}`);
|
|
110
105
|
lines.push(`- **技能目录**: \`${skill.sourceDir}\``);
|
|
@@ -114,8 +109,8 @@ export function formatSkillHint(skills) {
|
|
|
114
109
|
lines.push('## 技能调用流程');
|
|
115
110
|
lines.push('');
|
|
116
111
|
lines.push('```');
|
|
117
|
-
lines.push('1.
|
|
118
|
-
lines.push('2. 匹配 →
|
|
112
|
+
lines.push('1. 判断用户意图是否与某个技能的功能描述匹配');
|
|
113
|
+
lines.push('2. 匹配 → 用 Read 工具读取该技能卡片的"技能目录"/SKILL.md');
|
|
119
114
|
lines.push('3. 按照 SKILL.md 的 Quick Reference 和流程执行');
|
|
120
115
|
lines.push('4. 技能脚本(scripts/)与 SKILL.md 位于同一目录下,用绝对路径执行');
|
|
121
116
|
lines.push('5. 如果技能类型是 prompt,将其内容作为额外上下文指导后续操作');
|