@netpilot/skills 0.3.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/.claude-plugin/marketplace.json +26 -0
- package/.claude-plugin/plugin.json +18 -0
- package/.codex-plugin/plugin.json +34 -0
- package/AGENTS.md +55 -0
- package/CHANGELOG.md +27 -0
- package/LICENSE +21 -0
- package/README.md +151 -0
- package/SECURITY.md +7 -0
- package/THIRD_PARTY_NOTICES.md +29 -0
- package/agents/codex/architecture-designer.toml +11 -0
- package/agents/codex/backend-reviewer.toml +11 -0
- package/agents/codex/code-reader.toml +11 -0
- package/agents/codex/frontend-reviewer.toml +11 -0
- package/agents/codex/test-verifier.toml +11 -0
- package/bin/netpilot-skills.mjs +68 -0
- package/docs/agent-authoring.md +64 -0
- package/package.json +55 -0
- package/scripts/doctor.mjs +81 -0
- package/scripts/public-hygiene.mjs +232 -0
- package/scripts/sync.mjs +699 -0
- package/scripts/validate.mjs +461 -0
- package/skills/ask/SKILL.md +67 -0
- package/skills/ask/agents/openai.yaml +6 -0
- package/skills/code-review/SKILL.md +79 -0
- package/skills/code-review/agents/openai.yaml +6 -0
- package/skills/codebase-design/SKILL.md +78 -0
- package/skills/codebase-design/agents/openai.yaml +6 -0
- package/skills/diagnosing-bugs/SKILL.md +82 -0
- package/skills/diagnosing-bugs/agents/openai.yaml +6 -0
- package/skills/domain-modeling/SKILL.md +85 -0
- package/skills/domain-modeling/agents/openai.yaml +6 -0
- package/skills/grill/SKILL.md +54 -0
- package/skills/grill/agents/openai.yaml +6 -0
- package/skills/grill-with-docs/SKILL.md +75 -0
- package/skills/grill-with-docs/agents/openai.yaml +6 -0
- package/skills/grilling/SKILL.md +66 -0
- package/skills/grilling/agents/openai.yaml +6 -0
- package/skills/handoff/SKILL.md +72 -0
- package/skills/handoff/agents/openai.yaml +6 -0
- package/skills/implement/SKILL.md +68 -0
- package/skills/implement/agents/openai.yaml +6 -0
- package/skills/prototype/SKILL.md +71 -0
- package/skills/prototype/agents/openai.yaml +6 -0
- package/skills/research/SKILL.md +77 -0
- package/skills/research/agents/openai.yaml +6 -0
- package/skills/tdd/SKILL.md +71 -0
- package/skills/tdd/agents/openai.yaml +6 -0
- package/skills/teach/SKILL.md +68 -0
- package/skills/teach/agents/openai.yaml +6 -0
- package/skills/teach/references/glossary-format.md +21 -0
- package/skills/teach/references/learning-record-format.md +18 -0
- package/skills/teach/references/mission-format.md +28 -0
- package/skills/teach/references/resources-format.md +28 -0
- package/skills/to-spec/SKILL.md +76 -0
- package/skills/to-spec/agents/openai.yaml +6 -0
- package/skills/to-tickets/SKILL.md +69 -0
- package/skills/to-tickets/agents/openai.yaml +6 -0
- package/skills/wayfinder/SKILL.md +81 -0
- package/skills/wayfinder/agents/openai.yaml +6 -0
- package/skills/writing-great-skills/SKILL.md +83 -0
- package/skills/writing-great-skills/agents/openai.yaml +6 -0
|
@@ -0,0 +1,461 @@
|
|
|
1
|
+
import { readFile, readdir } from "node:fs/promises";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { fileURLToPath } from "node:url";
|
|
4
|
+
|
|
5
|
+
const NAME_PATTERN = /^[a-z0-9]+(?:-[a-z0-9]+)*$/;
|
|
6
|
+
const CHINESE_PATTERN = /[\u3400-\u9fff]/u;
|
|
7
|
+
const NON_SKILL_INLINE_TOKENS = new Set(["name", "description"]);
|
|
8
|
+
const BUILT_IN_AGENT_NAMES = new Set(["default", "worker", "explorer"]);
|
|
9
|
+
const AGENT_REASONING_EFFORTS = new Set([
|
|
10
|
+
"none",
|
|
11
|
+
"minimal",
|
|
12
|
+
"low",
|
|
13
|
+
"medium",
|
|
14
|
+
"high",
|
|
15
|
+
"xhigh",
|
|
16
|
+
"max",
|
|
17
|
+
"ultra",
|
|
18
|
+
]);
|
|
19
|
+
const AGENT_SANDBOX_MODES = new Set(["read-only", "workspace-write", "danger-full-access"]);
|
|
20
|
+
const DISPLAY_NAME_ACRONYMS = new Map([
|
|
21
|
+
["tdd", "TDD"],
|
|
22
|
+
]);
|
|
23
|
+
|
|
24
|
+
function unquote(value) {
|
|
25
|
+
const trimmed = value.trim();
|
|
26
|
+
if (
|
|
27
|
+
(trimmed.startsWith('"') && trimmed.endsWith('"')) ||
|
|
28
|
+
(trimmed.startsWith("'") && trimmed.endsWith("'"))
|
|
29
|
+
) {
|
|
30
|
+
return trimmed.slice(1, -1);
|
|
31
|
+
}
|
|
32
|
+
return trimmed;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function parseFrontmatter(content) {
|
|
36
|
+
const match = content.match(/^---\r?\n([\s\S]*?)\r?\n---(?:\r?\n|$)/);
|
|
37
|
+
if (!match) return null;
|
|
38
|
+
const values = {};
|
|
39
|
+
for (const line of match[1].split(/\r?\n/u)) {
|
|
40
|
+
const field = line.match(/^([a-zA-Z0-9_-]+):\s*(.*)$/u);
|
|
41
|
+
if (field) values[field[1]] = unquote(field[2]);
|
|
42
|
+
}
|
|
43
|
+
return values;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function parseOpenAiMetadata(content) {
|
|
47
|
+
const values = { interface: {}, policy: {} };
|
|
48
|
+
const structuralErrors = [];
|
|
49
|
+
const allowedKeys = {
|
|
50
|
+
interface: new Set(["display_name", "short_description", "default_prompt"]),
|
|
51
|
+
policy: new Set(["allow_implicit_invocation"]),
|
|
52
|
+
};
|
|
53
|
+
const seenSections = new Set();
|
|
54
|
+
let section = null;
|
|
55
|
+
|
|
56
|
+
if (content.includes("\uFFFD")) structuralErrors.push("文件不是合法 UTF-8");
|
|
57
|
+
|
|
58
|
+
for (const [index, line] of content.split(/\r?\n/u).entries()) {
|
|
59
|
+
const lineNumber = index + 1;
|
|
60
|
+
if (!line.trim() || line.trimStart().startsWith("#")) continue;
|
|
61
|
+
if (line.includes("\t")) {
|
|
62
|
+
structuralErrors.push(`第 ${lineNumber} 行包含 tab 缩进`);
|
|
63
|
+
continue;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
if (!line.startsWith(" ")) {
|
|
67
|
+
const sectionMatch = line.match(/^(interface|policy):\s*$/u);
|
|
68
|
+
if (!sectionMatch) {
|
|
69
|
+
structuralErrors.push(`第 ${lineNumber} 行不是受支持的顶层 section`);
|
|
70
|
+
section = null;
|
|
71
|
+
continue;
|
|
72
|
+
}
|
|
73
|
+
section = sectionMatch[1];
|
|
74
|
+
if (seenSections.has(section)) structuralErrors.push(`重复 section:${section}`);
|
|
75
|
+
seenSections.add(section);
|
|
76
|
+
continue;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
const fieldMatch = line.match(/^ ([a-z_]+):\s*(.+)\s*$/u);
|
|
80
|
+
if (!section || !fieldMatch) {
|
|
81
|
+
structuralErrors.push(`第 ${lineNumber} 行缩进或字段格式不合法`);
|
|
82
|
+
continue;
|
|
83
|
+
}
|
|
84
|
+
const [, key, rawValue] = fieldMatch;
|
|
85
|
+
if (!allowedKeys[section].has(key)) {
|
|
86
|
+
structuralErrors.push(`字段 ${key} 不应位于 ${section} section`);
|
|
87
|
+
continue;
|
|
88
|
+
}
|
|
89
|
+
if (Object.hasOwn(values[section], key)) {
|
|
90
|
+
structuralErrors.push(`重复字段:${section}.${key}`);
|
|
91
|
+
continue;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
if (section === "interface") {
|
|
95
|
+
try {
|
|
96
|
+
const parsedValue = JSON.parse(rawValue);
|
|
97
|
+
if (typeof parsedValue !== "string") throw new Error("必须是字符串");
|
|
98
|
+
values[section][key] = parsedValue;
|
|
99
|
+
} catch {
|
|
100
|
+
structuralErrors.push(`字段 ${section}.${key} 必须是合法的双引号字符串`);
|
|
101
|
+
}
|
|
102
|
+
} else if (rawValue === "true" || rawValue === "false") {
|
|
103
|
+
values[section][key] = rawValue === "true";
|
|
104
|
+
} else {
|
|
105
|
+
structuralErrors.push(`字段 ${section}.${key} 必须是 true 或 false`);
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
return { values, structuralErrors };
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
function unicodeLength(value) {
|
|
113
|
+
return [...value].length;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
function expectedDisplayName(skillName) {
|
|
117
|
+
return skillName
|
|
118
|
+
.split("-")
|
|
119
|
+
.map((part) => DISPLAY_NAME_ACRONYMS.get(part) ?? `${part[0].toUpperCase()}${part.slice(1)}`)
|
|
120
|
+
.join(" ");
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
function parseCodexAgent(content) {
|
|
124
|
+
const values = {};
|
|
125
|
+
const errors = [];
|
|
126
|
+
const instructionMatches = [
|
|
127
|
+
...content.matchAll(/^developer_instructions\s*=\s*"""\r?\n([\s\S]*?)^"""\s*$/gmu),
|
|
128
|
+
];
|
|
129
|
+
if (instructionMatches.length === 1) {
|
|
130
|
+
values.developer_instructions = instructionMatches[0][1].trim();
|
|
131
|
+
if (values.developer_instructions.includes("\\")) {
|
|
132
|
+
errors.push("developer_instructions 不允许反斜杠转义;分发配置使用无转义多行字符串子集");
|
|
133
|
+
}
|
|
134
|
+
} else {
|
|
135
|
+
errors.push("developer_instructions 必须是唯一的三引号多行字符串");
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
const scalarContent = content.replace(
|
|
139
|
+
/^developer_instructions\s*=\s*"""\r?\n[\s\S]*?^"""\s*$/gmu,
|
|
140
|
+
"",
|
|
141
|
+
);
|
|
142
|
+
const allowedFields = new Set([
|
|
143
|
+
"name",
|
|
144
|
+
"description",
|
|
145
|
+
"model",
|
|
146
|
+
"model_reasoning_effort",
|
|
147
|
+
"sandbox_mode",
|
|
148
|
+
]);
|
|
149
|
+
|
|
150
|
+
for (const [index, line] of scalarContent.split(/\r?\n/u).entries()) {
|
|
151
|
+
const trimmed = line.trim();
|
|
152
|
+
if (!trimmed || trimmed.startsWith("#")) continue;
|
|
153
|
+
const field = trimmed.match(/^([a-z_]+)\s*=\s*"([^"\\\r\n]*)"\s*$/u);
|
|
154
|
+
if (!field) {
|
|
155
|
+
errors.push(`第 ${index + 1} 行不是受支持的字符串字段`);
|
|
156
|
+
continue;
|
|
157
|
+
}
|
|
158
|
+
const [, key, value] = field;
|
|
159
|
+
if (!allowedFields.has(key)) {
|
|
160
|
+
errors.push(`不支持的字段:${key}`);
|
|
161
|
+
continue;
|
|
162
|
+
}
|
|
163
|
+
if (Object.hasOwn(values, key)) {
|
|
164
|
+
errors.push(`重复字段:${key}`);
|
|
165
|
+
continue;
|
|
166
|
+
}
|
|
167
|
+
values[key] = value;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
return { values, errors };
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
export async function validateCodexAgentDirectory(agentDir) {
|
|
174
|
+
const errors = [];
|
|
175
|
+
const agents = [];
|
|
176
|
+
let entries = [];
|
|
177
|
+
try {
|
|
178
|
+
entries = (await readdir(agentDir, { withFileTypes: true }))
|
|
179
|
+
.sort((left, right) => left.name.localeCompare(right.name));
|
|
180
|
+
} catch (error) {
|
|
181
|
+
return { agentCount: 0, agents, errors: [`无法读取 Codex agents 目录:${error.message}`] };
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
for (const entry of entries) {
|
|
185
|
+
if (!entry.isFile() || !entry.name.endsWith(".toml")) {
|
|
186
|
+
errors.push(`只允许普通 .toml 文件:${entry.name}`);
|
|
187
|
+
continue;
|
|
188
|
+
}
|
|
189
|
+
const expectedName = entry.name.slice(0, -".toml".length);
|
|
190
|
+
const agentPath = path.join(agentDir, entry.name);
|
|
191
|
+
let content;
|
|
192
|
+
try {
|
|
193
|
+
content = await readFile(agentPath, "utf8");
|
|
194
|
+
} catch (error) {
|
|
195
|
+
errors.push(`${entry.name}: 无法读取:${error.message}`);
|
|
196
|
+
continue;
|
|
197
|
+
}
|
|
198
|
+
const parsed = parseCodexAgent(content);
|
|
199
|
+
for (const error of parsed.errors) errors.push(`${entry.name}: ${error}`);
|
|
200
|
+
const values = parsed.values;
|
|
201
|
+
|
|
202
|
+
if (!values.name) errors.push(`${entry.name}: 缺少 name`);
|
|
203
|
+
if (values.name !== expectedName) {
|
|
204
|
+
errors.push(`${entry.name}: name 必须与文件名一致:${values.name ?? "<missing>"} != ${expectedName}`);
|
|
205
|
+
}
|
|
206
|
+
if (!NAME_PATTERN.test(values.name ?? "")) errors.push(`${entry.name}: name 不是合法 kebab-case`);
|
|
207
|
+
if (BUILT_IN_AGENT_NAMES.has(values.name)) {
|
|
208
|
+
errors.push(`${entry.name}: 不得覆盖 Codex 内置 agent:${values.name}`);
|
|
209
|
+
}
|
|
210
|
+
if (!values.description || !CHINESE_PATTERN.test(values.description)) {
|
|
211
|
+
errors.push(`${entry.name}: description 应包含中文使用边界`);
|
|
212
|
+
}
|
|
213
|
+
if (!values.developer_instructions || !CHINESE_PATTERN.test(values.developer_instructions)) {
|
|
214
|
+
errors.push(`${entry.name}: developer_instructions 应包含中文执行规则`);
|
|
215
|
+
}
|
|
216
|
+
if (Object.hasOwn(values, "model") && !values.model.trim()) {
|
|
217
|
+
errors.push(`${entry.name}: model 出现时不得为空`);
|
|
218
|
+
}
|
|
219
|
+
if (
|
|
220
|
+
Object.hasOwn(values, "model_reasoning_effort") &&
|
|
221
|
+
!AGENT_REASONING_EFFORTS.has(values.model_reasoning_effort)
|
|
222
|
+
) {
|
|
223
|
+
errors.push(`${entry.name}: 不支持的 model_reasoning_effort:${values.model_reasoning_effort}`);
|
|
224
|
+
}
|
|
225
|
+
if (
|
|
226
|
+
Object.hasOwn(values, "sandbox_mode") &&
|
|
227
|
+
!AGENT_SANDBOX_MODES.has(values.sandbox_mode)
|
|
228
|
+
) {
|
|
229
|
+
errors.push(`${entry.name}: 不支持的 sandbox_mode:${values.sandbox_mode}`);
|
|
230
|
+
}
|
|
231
|
+
if (values.sandbox_mode === "danger-full-access") {
|
|
232
|
+
errors.push(`${entry.name}: 分发 agent 不允许 danger-full-access`);
|
|
233
|
+
}
|
|
234
|
+
agents.push({ name: values.name ?? expectedName, path: agentPath, values });
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
if (agents.length === 0) errors.push("至少需要一个 Codex custom agent");
|
|
238
|
+
return { agentCount: agents.length, agents, errors };
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
export async function validateSkillDirectory(skillDir, knownNames, knownAgentNames = new Set()) {
|
|
242
|
+
const errors = [];
|
|
243
|
+
const directoryName = path.basename(skillDir);
|
|
244
|
+
const skillPath = path.join(skillDir, "SKILL.md");
|
|
245
|
+
const metadataPath = path.join(skillDir, "agents", "openai.yaml");
|
|
246
|
+
let skillContent = "";
|
|
247
|
+
let metadataContent = "";
|
|
248
|
+
|
|
249
|
+
try {
|
|
250
|
+
skillContent = await readFile(skillPath, "utf8");
|
|
251
|
+
} catch (error) {
|
|
252
|
+
errors.push(`缺少或无法读取 SKILL.md:${error.message}`);
|
|
253
|
+
return { name: directoryName, errors };
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
const frontmatter = parseFrontmatter(skillContent);
|
|
257
|
+
if (!frontmatter) {
|
|
258
|
+
errors.push("SKILL.md 缺少合法 YAML frontmatter");
|
|
259
|
+
} else {
|
|
260
|
+
if (!frontmatter.name) errors.push("frontmatter 缺少 name");
|
|
261
|
+
if (frontmatter.name !== directoryName) {
|
|
262
|
+
errors.push(`frontmatter name 必须与目录名一致:${frontmatter.name ?? "<missing>"} != ${directoryName}`);
|
|
263
|
+
}
|
|
264
|
+
if (!NAME_PATTERN.test(frontmatter.name ?? "")) errors.push(`name 不是合法 kebab-case:${frontmatter.name}`);
|
|
265
|
+
if (!frontmatter.description) errors.push("frontmatter 缺少 description");
|
|
266
|
+
if ((frontmatter.description?.length ?? 0) > 1024) errors.push("description 超过 1024 字符");
|
|
267
|
+
if (frontmatter.description && !CHINESE_PATTERN.test(frontmatter.description)) {
|
|
268
|
+
errors.push("description 应包含中文说明");
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
if (/\bTODO\b|\[TODO|PLACEHOLDER/iu.test(skillContent)) errors.push("SKILL.md 包含占位文本");
|
|
273
|
+
for (const heading of ["## 完成标准", "## 反模式"]) {
|
|
274
|
+
if (!skillContent.includes(heading)) errors.push(`缺少章节:${heading}`);
|
|
275
|
+
}
|
|
276
|
+
if (!CHINESE_PATTERN.test(skillContent)) errors.push("SKILL.md 正文应包含中文内容");
|
|
277
|
+
|
|
278
|
+
const contentWithoutFences = skillContent.replace(/```[\s\S]*?```/gu, "");
|
|
279
|
+
const references = new Set(
|
|
280
|
+
[...contentWithoutFences.matchAll(/\$([a-z0-9]+(?:-[a-z0-9]+)*)/gu)].map((match) => match[1]),
|
|
281
|
+
);
|
|
282
|
+
for (const match of contentWithoutFences.matchAll(/`([a-z0-9]+(?:-[a-z0-9]+)*)`/gu)) {
|
|
283
|
+
if (!NON_SKILL_INLINE_TOKENS.has(match[1])) references.add(match[1]);
|
|
284
|
+
}
|
|
285
|
+
for (const reference of references) {
|
|
286
|
+
if (!knownNames.has(reference)) errors.push(`未知 skill 引用:$${reference}`);
|
|
287
|
+
}
|
|
288
|
+
const agentReferences = new Set(
|
|
289
|
+
[...contentWithoutFences.matchAll(/`agent:([a-z0-9]+(?:-[a-z0-9]+)*)`/gu)]
|
|
290
|
+
.map((match) => match[1]),
|
|
291
|
+
);
|
|
292
|
+
for (const reference of agentReferences) {
|
|
293
|
+
if (!knownAgentNames.has(reference)) errors.push(`未知 Codex agent 引用:agent:${reference}`);
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
try {
|
|
297
|
+
metadataContent = await readFile(metadataPath, "utf8");
|
|
298
|
+
} catch (error) {
|
|
299
|
+
errors.push(`缺少或无法读取 agents/openai.yaml:${error.message}`);
|
|
300
|
+
return { name: directoryName, errors };
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
const metadata = parseOpenAiMetadata(metadataContent);
|
|
304
|
+
for (const structuralError of metadata.structuralErrors) {
|
|
305
|
+
errors.push(`openai.yaml 结构不合法:${structuralError}`);
|
|
306
|
+
}
|
|
307
|
+
const displayName = metadata.values.interface.display_name ?? null;
|
|
308
|
+
const shortDescription = metadata.values.interface.short_description ?? null;
|
|
309
|
+
const defaultPrompt = metadata.values.interface.default_prompt ?? null;
|
|
310
|
+
const invocationPolicy = metadata.values.policy.allow_implicit_invocation;
|
|
311
|
+
|
|
312
|
+
if (!displayName) {
|
|
313
|
+
errors.push("agents/openai.yaml 缺少 display_name");
|
|
314
|
+
} else {
|
|
315
|
+
const canonicalDisplayName = expectedDisplayName(directoryName);
|
|
316
|
+
if (displayName !== canonicalDisplayName) {
|
|
317
|
+
errors.push(`display_name 必须使用与 canonical name 对应的英文显示名:${canonicalDisplayName}`);
|
|
318
|
+
}
|
|
319
|
+
}
|
|
320
|
+
if (!shortDescription || unicodeLength(shortDescription) < 25 || unicodeLength(shortDescription) > 64) {
|
|
321
|
+
errors.push("short_description 必须为 25 至 64 个字符");
|
|
322
|
+
}
|
|
323
|
+
if (!defaultPrompt || !defaultPrompt.includes(`$${directoryName}`)) {
|
|
324
|
+
errors.push(`default_prompt 必须显式包含 $${directoryName}`);
|
|
325
|
+
}
|
|
326
|
+
if (typeof invocationPolicy !== "boolean") errors.push("allow_implicit_invocation 必须是 true 或 false");
|
|
327
|
+
if (invocationPolicy === false) {
|
|
328
|
+
errors.push("跨宿主单源策略要求 allow_implicit_invocation 为 true;动作权限应由正文门禁控制");
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
return { name: directoryName, errors };
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
async function readJson(filePath, errors) {
|
|
335
|
+
try {
|
|
336
|
+
return JSON.parse(await readFile(filePath, "utf8"));
|
|
337
|
+
} catch (error) {
|
|
338
|
+
errors.push(`无法读取 JSON:${filePath}(${error.message})`);
|
|
339
|
+
return null;
|
|
340
|
+
}
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
export async function validateRepository(rootDir) {
|
|
344
|
+
const resolvedRoot = path.resolve(rootDir);
|
|
345
|
+
const errors = [];
|
|
346
|
+
const warnings = [];
|
|
347
|
+
const requiredFiles = [
|
|
348
|
+
"README.md",
|
|
349
|
+
"AGENTS.md",
|
|
350
|
+
"LICENSE",
|
|
351
|
+
"THIRD_PARTY_NOTICES.md",
|
|
352
|
+
"docs/agent-authoring.md",
|
|
353
|
+
"package.json",
|
|
354
|
+
".codex-plugin/plugin.json",
|
|
355
|
+
".claude-plugin/plugin.json",
|
|
356
|
+
".claude-plugin/marketplace.json",
|
|
357
|
+
];
|
|
358
|
+
|
|
359
|
+
for (const requiredFile of requiredFiles) {
|
|
360
|
+
try {
|
|
361
|
+
await readFile(path.join(resolvedRoot, requiredFile));
|
|
362
|
+
} catch {
|
|
363
|
+
errors.push(`缺少仓库文件:${requiredFile}`);
|
|
364
|
+
}
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
let skillEntries = [];
|
|
368
|
+
try {
|
|
369
|
+
skillEntries = (await readdir(path.join(resolvedRoot, "skills"), { withFileTypes: true }))
|
|
370
|
+
.filter((entry) => entry.isDirectory() && !entry.name.startsWith("."))
|
|
371
|
+
.sort((left, right) => left.name.localeCompare(right.name));
|
|
372
|
+
} catch (error) {
|
|
373
|
+
errors.push(`无法读取 skills 目录:${error.message}`);
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
const codexAgentResult = await validateCodexAgentDirectory(
|
|
377
|
+
path.join(resolvedRoot, "agents", "codex"),
|
|
378
|
+
);
|
|
379
|
+
for (const error of codexAgentResult.errors) errors.push(`Codex agent: ${error}`);
|
|
380
|
+
for (const agent of codexAgentResult.agents) {
|
|
381
|
+
if (Object.hasOwn(agent.values, "model")) {
|
|
382
|
+
errors.push(
|
|
383
|
+
`Codex agent: ${agent.name} 的分发配置不应固定 model;请继承当前会话或在用户层覆盖`,
|
|
384
|
+
);
|
|
385
|
+
}
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
const knownNames = new Set(skillEntries.map((entry) => entry.name));
|
|
389
|
+
const knownAgentNames = new Set(codexAgentResult.agents.map((agent) => agent.name));
|
|
390
|
+
const skillResults = [];
|
|
391
|
+
for (const entry of skillEntries) {
|
|
392
|
+
const result = await validateSkillDirectory(
|
|
393
|
+
path.join(resolvedRoot, "skills", entry.name),
|
|
394
|
+
knownNames,
|
|
395
|
+
knownAgentNames,
|
|
396
|
+
);
|
|
397
|
+
skillResults.push(result);
|
|
398
|
+
for (const error of result.errors) errors.push(`${entry.name}: ${error}`);
|
|
399
|
+
}
|
|
400
|
+
if (skillEntries.length === 0) errors.push("至少需要一个 skill");
|
|
401
|
+
|
|
402
|
+
const packageManifest = await readJson(path.join(resolvedRoot, "package.json"), errors);
|
|
403
|
+
const expectedVersion = packageManifest?.version ?? null;
|
|
404
|
+
if (packageManifest && Object.keys(packageManifest.dependencies ?? {}).length > 0) {
|
|
405
|
+
errors.push("本仓库不应包含生产 dependencies");
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
const codexManifest = await readJson(path.join(resolvedRoot, ".codex-plugin", "plugin.json"), errors);
|
|
409
|
+
if (codexManifest && codexManifest.name !== "netpilot-skills") {
|
|
410
|
+
errors.push("Codex plugin name 必须是 netpilot-skills");
|
|
411
|
+
}
|
|
412
|
+
if (codexManifest && codexManifest.skills !== "./skills/") {
|
|
413
|
+
errors.push('Codex plugin skills 必须指向 "./skills/"');
|
|
414
|
+
}
|
|
415
|
+
if (codexManifest && codexManifest.version !== expectedVersion) {
|
|
416
|
+
errors.push("Codex plugin version 必须与 package.json 一致");
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
const claudeManifest = await readJson(path.join(resolvedRoot, ".claude-plugin", "plugin.json"), errors);
|
|
420
|
+
if (claudeManifest && claudeManifest.name !== "netpilot-skills") {
|
|
421
|
+
errors.push("Claude plugin name 必须是 netpilot-skills");
|
|
422
|
+
}
|
|
423
|
+
if (claudeManifest && claudeManifest.version !== expectedVersion) {
|
|
424
|
+
errors.push("Claude plugin version 必须与 package.json 一致");
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
const marketplace = await readJson(path.join(resolvedRoot, ".claude-plugin", "marketplace.json"), errors);
|
|
428
|
+
const marketplacePlugin = marketplace?.plugins?.find((plugin) => plugin.name === "netpilot-skills");
|
|
429
|
+
if (marketplace && (!marketplacePlugin || marketplacePlugin.source !== "./")) {
|
|
430
|
+
errors.push("Claude marketplace 必须包含 source 为 ./ 的 netpilot-skills");
|
|
431
|
+
}
|
|
432
|
+
if (marketplacePlugin && marketplacePlugin.version !== expectedVersion) {
|
|
433
|
+
errors.push("Claude marketplace plugin version 必须与 package.json 一致");
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
return {
|
|
437
|
+
ok: errors.length === 0,
|
|
438
|
+
rootDir: resolvedRoot,
|
|
439
|
+
skillCount: skillEntries.length,
|
|
440
|
+
agentCount: codexAgentResult.agentCount,
|
|
441
|
+
errors,
|
|
442
|
+
warnings,
|
|
443
|
+
skills: skillResults,
|
|
444
|
+
};
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
async function main() {
|
|
448
|
+
const rootDir = fileURLToPath(new URL("..", import.meta.url));
|
|
449
|
+
const result = await validateRepository(rootDir);
|
|
450
|
+
if (result.ok) {
|
|
451
|
+
console.log(`校验通过:${result.skillCount} 个 skills,${result.agentCount} 个 Codex agents`);
|
|
452
|
+
return;
|
|
453
|
+
}
|
|
454
|
+
console.error(`校验失败:${result.errors.length} 个问题`);
|
|
455
|
+
for (const error of result.errors) console.error(`- ${error}`);
|
|
456
|
+
process.exitCode = 1;
|
|
457
|
+
}
|
|
458
|
+
|
|
459
|
+
if (process.argv[1] && path.resolve(process.argv[1]) === fileURLToPath(import.meta.url)) {
|
|
460
|
+
await main();
|
|
461
|
+
}
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: ask
|
|
3
|
+
description: 当用户明确要求选择工作流,或任务的目标、范围、约束、风险、成功标准与下一步不清楚时使用。它是唯一的工作流路由入口:先检查上下文,必要时提出最少量的问题,再选择并启动合适的 skill;不要把它当作普通问答或长期讨论角色。
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Ask
|
|
7
|
+
|
|
8
|
+
`ask` 是工作流路由器,不是某个个人角色,也不是第二套需求澄清流程。目标是用尽可能少的交互找到正确入口,然后推进工作。
|
|
9
|
+
|
|
10
|
+
## 工作流
|
|
11
|
+
|
|
12
|
+
1. 先读取当前对话、仓库规则和与任务直接相关的文件。能从上下文或只读检查得到的答案,不询问用户。
|
|
13
|
+
2. 判断任务是否已经具备可执行的目标、范围、约束和完成标准。
|
|
14
|
+
3. 若信息足够,选择一个主 skill,完整读取并按它执行;控制权转交后,`ask` 不再主持后续阶段。只有存在清晰阶段关系时才给出后续 skill 链。
|
|
15
|
+
4. 若缺失信息会实质改变方案、风险或写入范围,一次只问一个高价值问题。优先给出 2 至 3 个互斥选项、推荐项及影响,也允许用户自由回答。问题不阻塞安全、可逆的只读检查或设计时,先继续这些工作;只有答案会改变当前写入范围或造成不可逆影响时才暂停。
|
|
16
|
+
5. 每次回答后重新判断,不机械完成预设问卷。信息足够就停止提问并进入执行。
|
|
17
|
+
|
|
18
|
+
## 路由规则
|
|
19
|
+
|
|
20
|
+
| 情形 | 主 skill | 常见后续 |
|
|
21
|
+
| --- | --- | --- |
|
|
22
|
+
| 明确要建立或继续一个跨会话学习工作区 | `teach` | `research` |
|
|
23
|
+
| 需要深入访谈,并明确要求同步维护项目术语、CONTEXT 或 ADR | `grill-with-docs` | `to-spec` |
|
|
24
|
+
| 重大计划、架构或产品决策需要深入访谈 | `grill` | `to-spec` |
|
|
25
|
+
| 想法很大、方向模糊、需要寻找落地路径 | `wayfinder` | `research`、`prototype`、`to-spec` |
|
|
26
|
+
| 陌生技术、事实或方案需要证据 | `research` | `prototype` |
|
|
27
|
+
| 高风险假设需要快速实证 | `prototype` | `to-spec` |
|
|
28
|
+
| 术语、概念边界或业务不变量混乱 | `domain-modeling` | `codebase-design` |
|
|
29
|
+
| 需要决定模块、职责或依赖方向 | `codebase-design` | `to-spec` |
|
|
30
|
+
| 已有讨论,需要形成可验收规格 | `to-spec` | `to-tickets` |
|
|
31
|
+
| 已有规格,需要拆成垂直任务 | `to-tickets` | `implement` |
|
|
32
|
+
| 已有明确任务,需要按边界实施 | `implement` | `code-review` |
|
|
33
|
+
| 行为变更适合测试先行 | `tdd` | `code-review` |
|
|
34
|
+
| bug、测试失败或异常的根因未知 | `diagnosing-bugs` | `tdd` |
|
|
35
|
+
| 需要审查当前变更 | `code-review` | `implement` |
|
|
36
|
+
| 需要跨会话、工具或人员继续 | `handoff` | 无 |
|
|
37
|
+
| 创建或改进 skill | `writing-great-skills` | `code-review` |
|
|
38
|
+
|
|
39
|
+
同一阶段只指定一个主 skill。不要同时启动多个职责重叠的 skill。
|
|
40
|
+
|
|
41
|
+
相邻 skill 冲突时按未知项的性质裁决:已有较具体方案、需要挑战决策时选 `grill`;只有用户原始要求明确包含边访谈边更新项目领域文档时才选 `grill-with-docs`,不能因为仓库里存在 `CONTEXT.md` 就自动升级为写入模式;方向与落地路径尚未确定时选 `wayfinder`;主要能由外部证据回答时选 `research`;必须依靠运行结果回答时选 `prototype`。明确任务需要实现且适合测试先行时,以 `tdd` 驱动该实现切片,否则由 `implement` 统筹。
|
|
42
|
+
|
|
43
|
+
## 输出
|
|
44
|
+
|
|
45
|
+
信息足够时,用简短说明交代:
|
|
46
|
+
|
|
47
|
+
- 选择的主 skill 及原因;
|
|
48
|
+
- 已确认的关键边界;
|
|
49
|
+
- 仍需验证但不阻塞开始的假设;
|
|
50
|
+
- 紧接着执行的动作。
|
|
51
|
+
|
|
52
|
+
如果只是轻量、明确的任务,不要为了使用 skill 而扩大流程,直接执行最短路径。
|
|
53
|
+
|
|
54
|
+
## 完成标准
|
|
55
|
+
|
|
56
|
+
- 已选择并开始合适的主 skill,或已提出当前唯一真正阻塞的问题。
|
|
57
|
+
- 用户能看出选择依据、关键边界和下一步。
|
|
58
|
+
- 没有重复询问可从上下文发现的信息。
|
|
59
|
+
|
|
60
|
+
## 反模式
|
|
61
|
+
|
|
62
|
+
- 不要把 `ask` 和另一个“澄清”skill 串成重复入口。
|
|
63
|
+
- 不要一次抛出长问卷。
|
|
64
|
+
- 不要把推荐列表当作工作成果而停止推进。
|
|
65
|
+
- 不要在目标已经明确时强迫用户重新描述需求。
|
|
66
|
+
- 不要替用户擅自决定会显著改变范围、成本、风险或外部状态的事项。
|
|
67
|
+
- 不要把“请直接开始”解释为授权写入系统级目录、创建远程资源或一次性扩展全部范围。
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: code-review
|
|
3
|
+
description: 当用户要求审查当前 diff、提交、PR 或一组文件,或 AI 完成中大型代码改动需要独立检查正确性、风险、测试和架构边界时使用。它默认只读并优先报告可执行缺陷;用户要求直接修复评论时改用实施流程。
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Code Review
|
|
7
|
+
|
|
8
|
+
代码审查首先寻找会导致错误、回归、安全问题、数据风险或维护边界破坏的具体缺陷。默认保持只读,不因审查请求直接修改文件。
|
|
9
|
+
|
|
10
|
+
## 确认范围
|
|
11
|
+
|
|
12
|
+
1. 明确审查对象:工作树 diff、指定 commit、PR、文件或实现切片。
|
|
13
|
+
2. 读取适用的仓库规则、规格、验收标准和测试说明。
|
|
14
|
+
3. 检查完整变更及必要上下文,不只看单个片段。
|
|
15
|
+
4. 区分本次改动与仓库原有问题;只有变更引入、暴露或必须阻止交付的问题才作为主要 finding。
|
|
16
|
+
|
|
17
|
+
## 审查顺序
|
|
18
|
+
|
|
19
|
+
按风险优先检查:
|
|
20
|
+
|
|
21
|
+
1. **正确性**:行为是否满足规格,边界、失败路径、状态转换和并发是否正确。
|
|
22
|
+
2. **安全与数据**:认证授权、租户隔离、输入验证、密钥、注入、迁移和不可逆影响。
|
|
23
|
+
3. **兼容与集成**:公共 API、schema、配置、版本、调用方和回退路径。
|
|
24
|
+
4. **测试证据**:关键行为是否覆盖,测试是否会在实现错误时失败,验证命令是否真实执行。
|
|
25
|
+
5. **架构边界**:所有权、依赖方向、重复规则和不必要复杂度。
|
|
26
|
+
6. **可运维性**:错误信息、日志、指标、故障隔离和资源释放。
|
|
27
|
+
|
|
28
|
+
格式或个人偏好只有在违反项目规则、造成真实理解成本或隐藏缺陷时才报告。
|
|
29
|
+
|
|
30
|
+
## 子代理协作
|
|
31
|
+
|
|
32
|
+
只有宿主支持子代理、审查范围确实包含可独立检查的表面时才并行委派:
|
|
33
|
+
|
|
34
|
+
- 前端变更交给 `agent:frontend-reviewer`;
|
|
35
|
+
- 后端、接口或数据变更交给 `agent:backend-reviewer`;
|
|
36
|
+
- 跨模块边界和依赖方向交给 `agent:architecture-designer`;
|
|
37
|
+
- 大量只读定位可先交给 `agent:code-reader`。
|
|
38
|
+
|
|
39
|
+
主 agent 必须先固定 diff 基点、规格和每个子代理的排他范围,等待结果后回到完整 diff 核验证据、去重并统一优先级。custom agent 不可用时,由主 agent 串行执行同一检查维度;小改动不要为并行而并行。审查子代理默认只读,任何 finding 的修复都返回实施流程,不能在并行审查中直接写代码。
|
|
40
|
+
|
|
41
|
+
## Finding 标准
|
|
42
|
+
|
|
43
|
+
每个 finding 必须包含:
|
|
44
|
+
|
|
45
|
+
- 优先级:`P0` 阻断性事故,`P1` 高风险,`P2` 应修问题,`P3` 低风险改进;
|
|
46
|
+
- 精确文件和尽可能小的行范围;
|
|
47
|
+
- 哪种输入、状态或环境会触发;
|
|
48
|
+
- 实际影响以及为何由本次变更造成;
|
|
49
|
+
- 最小修复方向,不要求作者猜测意图。
|
|
50
|
+
|
|
51
|
+
如果证据不足,先检查或提问,不把可能性写成确定缺陷。合并同一根因的重复评论。
|
|
52
|
+
|
|
53
|
+
## 输出格式
|
|
54
|
+
|
|
55
|
+
先列 findings,按优先级排序;然后给出假设或未决问题;最后给出简短摘要和验证缺口。如果没有可执行 finding,明确写“未发现可执行问题”,但仍说明审查范围和剩余测试风险。
|
|
56
|
+
|
|
57
|
+
```markdown
|
|
58
|
+
### [P1] 标题
|
|
59
|
+
- 位置:`path/to/file:line`
|
|
60
|
+
- 触发:
|
|
61
|
+
- 影响:
|
|
62
|
+
- 建议:
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
## 完成标准
|
|
66
|
+
|
|
67
|
+
- 审查覆盖了完整目标变更与必要上下文。
|
|
68
|
+
- 每个 finding 都可复现、可定位且可执行。
|
|
69
|
+
- 测试充分性和未验证风险被如实说明。
|
|
70
|
+
- 没有用大量风格意见淹没真实风险。
|
|
71
|
+
|
|
72
|
+
## 反模式
|
|
73
|
+
|
|
74
|
+
- 不要只总结代码而不判断风险。
|
|
75
|
+
- 不要对未改动的历史问题进行无边界审计。
|
|
76
|
+
- 不要报告静态工具已经可靠处理且没有额外影响的噪音。
|
|
77
|
+
- 不要声称某测试通过,除非有实际运行证据。
|
|
78
|
+
- 不要因作者是 AI 或人类而改变证据标准。
|
|
79
|
+
- 不要无差别启动全部 specialist,造成重复评论和额外成本。
|