@fitlab-ai/agent-infra 0.5.4 → 0.5.6
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 +94 -1
- package/README.zh-CN.md +94 -1
- package/lib/defaults.json +1 -0
- package/lib/sandbox/commands/create.js +7 -3
- package/lib/sandbox/shell.js +47 -7
- package/lib/sandbox/tools.js +18 -14
- package/package.json +1 -1
- package/templates/.agents/README.en.md +52 -0
- package/templates/.agents/README.zh-CN.md +52 -0
- package/templates/.agents/rules/issue-pr-commands.github.en.md +10 -1
- package/templates/.agents/rules/issue-pr-commands.github.zh-CN.md +10 -1
- package/templates/.agents/rules/issue-sync.github.en.md +12 -10
- package/templates/.agents/rules/issue-sync.github.zh-CN.md +12 -10
- package/templates/.agents/rules/milestone-inference.github.en.md +6 -5
- package/templates/.agents/rules/milestone-inference.github.zh-CN.md +6 -5
- package/templates/.agents/scripts/platform-adapters/platform-sync.github.js +87 -14
- package/templates/.agents/skills/analyze-task/SKILL.en.md +1 -1
- package/templates/.agents/skills/analyze-task/SKILL.zh-CN.md +1 -1
- package/templates/.agents/skills/create-pr/config/verify.json +2 -0
- package/templates/.agents/skills/create-pr/reference/pr-body-template.en.md +6 -7
- package/templates/.agents/skills/create-pr/reference/pr-body-template.zh-CN.md +6 -7
- package/templates/.agents/skills/create-release-note/SKILL.en.md +27 -2
- package/templates/.agents/skills/create-release-note/SKILL.zh-CN.md +27 -2
- package/templates/.agents/skills/implement-task/SKILL.en.md +1 -1
- package/templates/.agents/skills/implement-task/SKILL.zh-CN.md +1 -1
- package/templates/.agents/skills/import-issue/SKILL.en.md +10 -2
- package/templates/.agents/skills/import-issue/SKILL.zh-CN.md +10 -2
- package/templates/.agents/skills/import-issue/config/verify.json +2 -1
- package/templates/.agents/skills/plan-task/SKILL.en.md +1 -1
- package/templates/.agents/skills/plan-task/SKILL.zh-CN.md +1 -1
- package/templates/.agents/skills/refine-task/SKILL.en.md +1 -1
- package/templates/.agents/skills/refine-task/SKILL.zh-CN.md +1 -1
- package/templates/.agents/skills/review-task/SKILL.en.md +1 -1
- package/templates/.agents/skills/review-task/SKILL.zh-CN.md +1 -1
- package/templates/.agents/skills/update-agent-infra/scripts/sync-templates.js +316 -1
- package/templates/.github/scripts/sync-labels-to-set.sh +110 -0
- package/templates/.github/workflows/metadata-sync.yml +11 -20
- package/templates/.github/workflows/pr-label.yml +10 -19
- package/templates/.github/workflows/status-label.yml +20 -34
|
@@ -62,8 +62,8 @@ date "+%Y-%m-%d %H:%M:%S%:z"
|
|
|
62
62
|
如果 task.md 中存在有效的 `issue_number`,执行以下同步操作(任一失败则跳过并继续):
|
|
63
63
|
- 执行前先读取 `.agents/rules/issue-sync.md`,完成 upstream 仓库检测和权限检测
|
|
64
64
|
- 按 issue-sync.md 设置 `status: in-progress`
|
|
65
|
-
- 发布 `{refinement-artifact}` 评论
|
|
66
65
|
- 创建或更新 `<!-- sync-issue:{task-id}:task -->` 评论(按 issue-sync.md 的 task.md 评论同步规则)
|
|
66
|
+
- 发布 `{refinement-artifact}` 评论
|
|
67
67
|
|
|
68
68
|
### 7. 完成校验
|
|
69
69
|
|
|
@@ -56,8 +56,8 @@ Update task.md and append:
|
|
|
56
56
|
If task.md contains a valid `issue_number`, perform these sync actions (skip and continue on any failure):
|
|
57
57
|
- Read `.agents/rules/issue-sync.md` before syncing, and complete upstream repository detection plus permission detection
|
|
58
58
|
- Set `status: in-progress` by following issue-sync.md
|
|
59
|
-
- Publish the `{review-artifact}` comment
|
|
60
59
|
- Create or update the `<!-- sync-issue:{task-id}:task -->` comment (follow the task.md comment sync rule in issue-sync.md)
|
|
60
|
+
- Publish the `{review-artifact}` comment
|
|
61
61
|
|
|
62
62
|
### 7. Verification Gate
|
|
63
63
|
|
|
@@ -56,8 +56,8 @@ date "+%Y-%m-%d %H:%M:%S%:z"
|
|
|
56
56
|
如果 task.md 中存在有效的 `issue_number`,执行以下同步操作(任一失败则跳过并继续):
|
|
57
57
|
- 执行前先读取 `.agents/rules/issue-sync.md`,完成 upstream 仓库检测和权限检测
|
|
58
58
|
- 按 issue-sync.md 设置 `status: in-progress`
|
|
59
|
-
- 发布 `{review-artifact}` 评论
|
|
60
59
|
- 创建或更新 `<!-- sync-issue:{task-id}:task -->` 评论(按 issue-sync.md 的 task.md 评论同步规则)
|
|
60
|
+
- 发布 `{review-artifact}` 评论
|
|
61
61
|
|
|
62
62
|
### 7. 完成校验
|
|
63
63
|
|
|
@@ -15,6 +15,7 @@
|
|
|
15
15
|
|
|
16
16
|
import childProcess from 'node:child_process';
|
|
17
17
|
import fs from 'node:fs';
|
|
18
|
+
import os from 'node:os';
|
|
18
19
|
import path from 'node:path';
|
|
19
20
|
import { fileURLToPath } from 'node:url';
|
|
20
21
|
|
|
@@ -56,6 +57,7 @@ const DEFAULTS = {
|
|
|
56
57
|
".claude/hooks/",
|
|
57
58
|
".gemini/commands/",
|
|
58
59
|
".github/hooks/check-version-format.sh",
|
|
60
|
+
".github/scripts/",
|
|
59
61
|
".opencode/commands/"
|
|
60
62
|
],
|
|
61
63
|
"merged": [
|
|
@@ -76,7 +78,7 @@ const DEFAULTS = {
|
|
|
76
78
|
}
|
|
77
79
|
};
|
|
78
80
|
|
|
79
|
-
const INSTALLER_VERSION = "v0.5.
|
|
81
|
+
const INSTALLER_VERSION = "v0.5.6";
|
|
80
82
|
const PACKAGE_NAME = '@fitlab-ai/agent-infra';
|
|
81
83
|
// Add a new identifier here only after shipping matching .{platform}. template variants.
|
|
82
84
|
const KNOWN_PLATFORMS = new Set(['github']);
|
|
@@ -123,6 +125,292 @@ function removeEmptyDirs(dir) {
|
|
|
123
125
|
}
|
|
124
126
|
}
|
|
125
127
|
|
|
128
|
+
function parseSkillFrontmatter(filePath) {
|
|
129
|
+
const content = fs.readFileSync(filePath, 'utf8');
|
|
130
|
+
const match = content.match(/^---\r?\n([\s\S]*?)\r?\n---(?:\r?\n|$)/);
|
|
131
|
+
if (!match) return {};
|
|
132
|
+
|
|
133
|
+
const result = {};
|
|
134
|
+
const lines = match[1].split(/\r?\n/);
|
|
135
|
+
const normalizeValue = (value) => value.replace(/^["']|["']$/g, '').trim();
|
|
136
|
+
|
|
137
|
+
for (let index = 0; index < lines.length; index += 1) {
|
|
138
|
+
const line = lines[index];
|
|
139
|
+
const pair = line.match(/^([A-Za-z0-9_-]+):\s*(.*)$/);
|
|
140
|
+
if (!pair) continue;
|
|
141
|
+
|
|
142
|
+
const [, key, rawValue] = pair;
|
|
143
|
+
if (rawValue === '>') {
|
|
144
|
+
const block = [];
|
|
145
|
+
for (let offset = index + 1; offset < lines.length; offset += 1) {
|
|
146
|
+
const nextLine = lines[offset];
|
|
147
|
+
if (!/^\s+/.test(nextLine)) break;
|
|
148
|
+
|
|
149
|
+
block.push(nextLine.trim());
|
|
150
|
+
index = offset;
|
|
151
|
+
}
|
|
152
|
+
result[key] = block.join(' ').trim();
|
|
153
|
+
continue;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
result[key] = normalizeValue(rawValue);
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
return result;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
function listTemplateSkillNames(templateRoot) {
|
|
163
|
+
const templateSkillsDir = path.join(templateRoot, '.agents/skills');
|
|
164
|
+
if (!fs.existsSync(templateSkillsDir)) return new Set();
|
|
165
|
+
|
|
166
|
+
return new Set(
|
|
167
|
+
fs.readdirSync(templateSkillsDir, { withFileTypes: true })
|
|
168
|
+
.filter((entry) => entry.isDirectory())
|
|
169
|
+
.map((entry) => entry.name)
|
|
170
|
+
);
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
function detectCustomSkills(projectRoot, templateSkillNames) {
|
|
174
|
+
const skillsDir = path.join(projectRoot, '.agents/skills');
|
|
175
|
+
if (!fs.existsSync(skillsDir)) return [];
|
|
176
|
+
|
|
177
|
+
return fs.readdirSync(skillsDir, { withFileTypes: true })
|
|
178
|
+
.filter((entry) => entry.isDirectory() && !templateSkillNames.has(entry.name))
|
|
179
|
+
.map((entry) => {
|
|
180
|
+
const skillMd = path.join(skillsDir, entry.name, 'SKILL.md');
|
|
181
|
+
if (!fs.existsSync(skillMd)) return null;
|
|
182
|
+
|
|
183
|
+
const meta = parseSkillFrontmatter(skillMd);
|
|
184
|
+
return {
|
|
185
|
+
dirName: entry.name,
|
|
186
|
+
name: meta.name || entry.name,
|
|
187
|
+
description: meta.description || '',
|
|
188
|
+
args: meta.args || null
|
|
189
|
+
};
|
|
190
|
+
})
|
|
191
|
+
.filter(Boolean)
|
|
192
|
+
.sort((left, right) => left.dirName.localeCompare(right.dirName));
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
function isCustomProtected(targetPath, customSkills, project) {
|
|
196
|
+
const normalized = norm(targetPath);
|
|
197
|
+
|
|
198
|
+
return customSkills.some(({ dirName }) => (
|
|
199
|
+
normalized.startsWith(`.agents/skills/${dirName}/`) ||
|
|
200
|
+
normalized === `.claude/commands/${dirName}.md` ||
|
|
201
|
+
normalized === `.opencode/commands/${dirName}.md` ||
|
|
202
|
+
normalized === '.gemini/commands/' + project + '/' + dirName + '.toml'
|
|
203
|
+
));
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
function expandHome(inputPath) {
|
|
207
|
+
if (inputPath === '~') return os.homedir();
|
|
208
|
+
if (inputPath.startsWith('~/')) {
|
|
209
|
+
return path.join(os.homedir(), inputPath.slice(2));
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
return path.resolve(inputPath);
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
function writeIfChanged(projectRoot, targetPath, content, reportBucket) {
|
|
216
|
+
const fullPath = path.join(projectRoot, targetPath);
|
|
217
|
+
const exists = fs.existsSync(fullPath);
|
|
218
|
+
|
|
219
|
+
if (exists && fs.readFileSync(fullPath, 'utf8') === content) {
|
|
220
|
+
reportBucket.unchanged.push(targetPath);
|
|
221
|
+
return;
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
const dir = path.dirname(fullPath);
|
|
225
|
+
if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true });
|
|
226
|
+
fs.writeFileSync(fullPath, content, 'utf8');
|
|
227
|
+
|
|
228
|
+
(exists ? reportBucket.updated : reportBucket.generated).push(targetPath);
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
function syncCustomSkillSources(projectRoot, sources, report, templateSkillNames) {
|
|
232
|
+
const skillsDir = path.join(projectRoot, '.agents/skills');
|
|
233
|
+
const syncedSkills = new Map();
|
|
234
|
+
|
|
235
|
+
for (const source of sources) {
|
|
236
|
+
if (source?.type !== 'local') continue;
|
|
237
|
+
if (typeof source.path !== 'string' || source.path.trim() === '') {
|
|
238
|
+
report.custom.sourceErrors.push({ source: String(source?.path || ''), reason: 'invalid path' });
|
|
239
|
+
continue;
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
const srcDir = expandHome(source.path);
|
|
243
|
+
if (!fs.existsSync(srcDir) || !fs.statSync(srcDir).isDirectory()) {
|
|
244
|
+
report.custom.sourceErrors.push({ source: source.path, reason: 'directory not found' });
|
|
245
|
+
continue;
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
for (const entry of fs.readdirSync(srcDir, { withFileTypes: true })) {
|
|
249
|
+
if (!entry.isDirectory()) continue;
|
|
250
|
+
if (templateSkillNames.has(entry.name)) {
|
|
251
|
+
report.custom.sourceErrors.push({
|
|
252
|
+
source: source.path,
|
|
253
|
+
reason: `skill ${entry.name} conflicts with built-in skill`
|
|
254
|
+
});
|
|
255
|
+
continue;
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
const skillSrcDir = path.join(srcDir, entry.name);
|
|
259
|
+
const skillMd = path.join(skillSrcDir, 'SKILL.md');
|
|
260
|
+
if (!fs.existsSync(skillMd)) continue;
|
|
261
|
+
|
|
262
|
+
const skillDstDir = path.join(skillsDir, entry.name);
|
|
263
|
+
const trackedFiles = syncedSkills.get(entry.name) || new Set();
|
|
264
|
+
syncedSkills.set(entry.name, trackedFiles);
|
|
265
|
+
|
|
266
|
+
for (const srcFile of walkDir(skillSrcDir)) {
|
|
267
|
+
const relPath = norm(path.relative(skillSrcDir, srcFile));
|
|
268
|
+
const dstFile = path.join(skillDstDir, relPath);
|
|
269
|
+
const projectPath = norm(path.relative(projectRoot, dstFile));
|
|
270
|
+
const srcContent = fs.readFileSync(srcFile);
|
|
271
|
+
const existed = fs.existsSync(dstFile);
|
|
272
|
+
|
|
273
|
+
trackedFiles.add(relPath);
|
|
274
|
+
|
|
275
|
+
if (existed) {
|
|
276
|
+
const dstContent = fs.readFileSync(dstFile);
|
|
277
|
+
if (srcContent.equals(dstContent)) {
|
|
278
|
+
report.custom.unchanged.push(projectPath);
|
|
279
|
+
continue;
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
const dir = path.dirname(dstFile);
|
|
284
|
+
if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true });
|
|
285
|
+
fs.writeFileSync(dstFile, srcContent);
|
|
286
|
+
|
|
287
|
+
(existed ? report.custom.updated : report.custom.generated).push(projectPath);
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
return syncedSkills;
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
function cleanStaleSyncedFiles(projectRoot, syncedSkills, report) {
|
|
296
|
+
const skillsDir = path.join(projectRoot, '.agents/skills');
|
|
297
|
+
|
|
298
|
+
for (const [skillName, expectedFiles] of syncedSkills) {
|
|
299
|
+
const skillDir = path.join(skillsDir, skillName);
|
|
300
|
+
if (!fs.existsSync(skillDir)) continue;
|
|
301
|
+
|
|
302
|
+
const actualFiles = walkDir(skillDir).map((filePath) => norm(path.relative(skillDir, filePath)));
|
|
303
|
+
const removedBefore = report.custom.removed.length;
|
|
304
|
+
|
|
305
|
+
for (const actualFile of actualFiles) {
|
|
306
|
+
if (expectedFiles.has(actualFile)) continue;
|
|
307
|
+
|
|
308
|
+
const staleFile = path.join(skillDir, actualFile);
|
|
309
|
+
fs.unlinkSync(staleFile);
|
|
310
|
+
report.custom.removed.push(norm(path.relative(projectRoot, staleFile)));
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
if (report.custom.removed.length > removedBefore) {
|
|
314
|
+
removeEmptyDirs(skillDir);
|
|
315
|
+
}
|
|
316
|
+
}
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
function generateClaudeCommand(skill, lang) {
|
|
320
|
+
const isZhCN = lang === 'zh-CN';
|
|
321
|
+
const lines = ['---', `description: ${JSON.stringify(skill.description)}`];
|
|
322
|
+
|
|
323
|
+
if (skill.args) {
|
|
324
|
+
lines.push(`usage: ${JSON.stringify(`/${skill.dirName} ${skill.args}`)}`);
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
lines.push('---', '');
|
|
328
|
+
lines.push(
|
|
329
|
+
isZhCN
|
|
330
|
+
? `读取并执行 \`.agents/skills/${skill.dirName}/SKILL.md\` 中的 ${skill.dirName} 技能。`
|
|
331
|
+
: `Read and execute the ${skill.dirName} skill from \`.agents/skills/${skill.dirName}/SKILL.md\`.`
|
|
332
|
+
);
|
|
333
|
+
lines.push('');
|
|
334
|
+
lines.push(isZhCN ? '严格按照技能中定义的所有步骤执行。' : 'Follow all steps defined in the skill exactly.');
|
|
335
|
+
|
|
336
|
+
return `${lines.join('\n')}\n`;
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
function generateGeminiCommand(skill, lang) {
|
|
340
|
+
const isZhCN = lang === 'zh-CN';
|
|
341
|
+
const promptLines = [];
|
|
342
|
+
|
|
343
|
+
if (skill.args) {
|
|
344
|
+
promptLines.push(isZhCN ? '参数:{{args}}' : 'Arguments: {{args}}');
|
|
345
|
+
promptLines.push('');
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
promptLines.push(
|
|
349
|
+
isZhCN
|
|
350
|
+
? `读取并执行 \`.agents/skills/${skill.dirName}/SKILL.md\` 中的 ${skill.dirName} 技能。`
|
|
351
|
+
: `Read and execute the ${skill.dirName} skill from \`.agents/skills/${skill.dirName}/SKILL.md\`.`
|
|
352
|
+
);
|
|
353
|
+
promptLines.push('');
|
|
354
|
+
promptLines.push(isZhCN ? '严格按照技能中定义的所有步骤执行。' : 'Follow all steps defined in the skill exactly.');
|
|
355
|
+
|
|
356
|
+
return [
|
|
357
|
+
`description = ${JSON.stringify(skill.description)}`,
|
|
358
|
+
'prompt = """',
|
|
359
|
+
...promptLines,
|
|
360
|
+
'"""'
|
|
361
|
+
].join('\n') + '\n';
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
function generateOpenCodeCommand(skill, lang) {
|
|
365
|
+
const isZhCN = lang === 'zh-CN';
|
|
366
|
+
const lines = [
|
|
367
|
+
'---',
|
|
368
|
+
`description: ${JSON.stringify(skill.description)}`,
|
|
369
|
+
'agent: general',
|
|
370
|
+
'subtask: false',
|
|
371
|
+
'---',
|
|
372
|
+
''
|
|
373
|
+
];
|
|
374
|
+
|
|
375
|
+
if (skill.args) {
|
|
376
|
+
lines.push(isZhCN ? '参数:$ARGUMENTS' : 'Arguments: $ARGUMENTS');
|
|
377
|
+
lines.push('');
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
lines.push(
|
|
381
|
+
isZhCN
|
|
382
|
+
? `读取并执行 \`.agents/skills/${skill.dirName}/SKILL.md\` 中的 ${skill.dirName} 技能。`
|
|
383
|
+
: `Read and execute the ${skill.dirName} skill from \`.agents/skills/${skill.dirName}/SKILL.md\`.`
|
|
384
|
+
);
|
|
385
|
+
lines.push('');
|
|
386
|
+
lines.push(isZhCN ? '严格按照技能中定义的所有步骤执行。' : 'Follow all steps defined in the skill exactly.');
|
|
387
|
+
|
|
388
|
+
return `${lines.join('\n')}\n`;
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
function generateCustomCommands(projectRoot, customSkills, project, lang, report) {
|
|
392
|
+
for (const skill of customSkills) {
|
|
393
|
+
writeIfChanged(
|
|
394
|
+
projectRoot,
|
|
395
|
+
`.claude/commands/${skill.dirName}.md`,
|
|
396
|
+
generateClaudeCommand(skill, lang),
|
|
397
|
+
report.custom.commands
|
|
398
|
+
);
|
|
399
|
+
writeIfChanged(
|
|
400
|
+
projectRoot,
|
|
401
|
+
'.gemini/commands/' + project + '/' + skill.dirName + '.toml',
|
|
402
|
+
generateGeminiCommand(skill, lang),
|
|
403
|
+
report.custom.commands
|
|
404
|
+
);
|
|
405
|
+
writeIfChanged(
|
|
406
|
+
projectRoot,
|
|
407
|
+
`.opencode/commands/${skill.dirName}.md`,
|
|
408
|
+
generateOpenCodeCommand(skill, lang),
|
|
409
|
+
report.custom.commands
|
|
410
|
+
);
|
|
411
|
+
}
|
|
412
|
+
}
|
|
413
|
+
|
|
126
414
|
function matchesAny(rel, patterns) {
|
|
127
415
|
const n = norm(rel);
|
|
128
416
|
return patterns.some(p => norm(p) === n || globMatch(p, n));
|
|
@@ -411,6 +699,8 @@ function syncTemplates(projectRoot, templateRootOverride) {
|
|
|
411
699
|
const { project, org, language: lang = 'en' } = cfg;
|
|
412
700
|
const platformType = cfg.platform?.type || DEFAULTS.platform.type;
|
|
413
701
|
const vars = { project, org };
|
|
702
|
+
const templateSkillNames = listTemplateSkillNames(templateRoot);
|
|
703
|
+
const protectedCustomSkills = detectCustomSkills(projectRoot, templateSkillNames);
|
|
414
704
|
|
|
415
705
|
const managed = [...(cfg.files.managed || [])];
|
|
416
706
|
const merged = [...(cfg.files.merged || [])];
|
|
@@ -421,6 +711,15 @@ function syncTemplates(projectRoot, templateRootOverride) {
|
|
|
421
711
|
templateRoot: norm(templateRoot),
|
|
422
712
|
registryAdded: [],
|
|
423
713
|
managed: { written: [], created: [], unchanged: [], skippedMerged: [], removed: [] },
|
|
714
|
+
custom: {
|
|
715
|
+
detected: [],
|
|
716
|
+
generated: [],
|
|
717
|
+
updated: [],
|
|
718
|
+
unchanged: [],
|
|
719
|
+
removed: [],
|
|
720
|
+
sourceErrors: [],
|
|
721
|
+
commands: { generated: [], updated: [], unchanged: [] }
|
|
722
|
+
},
|
|
424
723
|
ejected: { created: [], skipped: [] },
|
|
425
724
|
merged: { pending: [] },
|
|
426
725
|
configUpdated: false,
|
|
@@ -496,6 +795,7 @@ function syncTemplates(projectRoot, templateRootOverride) {
|
|
|
496
795
|
for (const projFile of projFiles) {
|
|
497
796
|
if (expectedTargets.has(projFile)) continue;
|
|
498
797
|
if (projFile === configPathRel) continue;
|
|
798
|
+
if (isCustomProtected(projFile, protectedCustomSkills, project)) continue;
|
|
499
799
|
if (matchesAny(projFile, merged) || matchesAny(projFile, ejected)) continue;
|
|
500
800
|
|
|
501
801
|
fs.unlinkSync(path.join(projectRoot, projFile));
|
|
@@ -508,6 +808,16 @@ function syncTemplates(projectRoot, templateRootOverride) {
|
|
|
508
808
|
}
|
|
509
809
|
}
|
|
510
810
|
|
|
811
|
+
const sources = Array.isArray(cfg.skills?.sources) ? cfg.skills.sources : [];
|
|
812
|
+
if (sources.length > 0) {
|
|
813
|
+
const syncedSkills = syncCustomSkillSources(projectRoot, sources, report, templateSkillNames);
|
|
814
|
+
cleanStaleSyncedFiles(projectRoot, syncedSkills, report);
|
|
815
|
+
}
|
|
816
|
+
|
|
817
|
+
const customSkills = detectCustomSkills(projectRoot, templateSkillNames);
|
|
818
|
+
report.custom.detected = customSkills.map((skill) => skill.dirName);
|
|
819
|
+
generateCustomCommands(projectRoot, customSkills, project, lang, report);
|
|
820
|
+
|
|
511
821
|
for (const entry of ejected) {
|
|
512
822
|
const dstFull = path.join(projectRoot, entry);
|
|
513
823
|
if (fs.existsSync(dstFull)) {
|
|
@@ -556,6 +866,11 @@ function syncTemplates(projectRoot, templateRootOverride) {
|
|
|
556
866
|
report.managed.written.length +
|
|
557
867
|
report.managed.created.length +
|
|
558
868
|
report.managed.removed.length +
|
|
869
|
+
report.custom.generated.length +
|
|
870
|
+
report.custom.updated.length +
|
|
871
|
+
report.custom.removed.length +
|
|
872
|
+
report.custom.commands.generated.length +
|
|
873
|
+
report.custom.commands.updated.length +
|
|
559
874
|
report.ejected.created.length +
|
|
560
875
|
report.registryAdded.length
|
|
561
876
|
) > 0;
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
#!/bin/sh
|
|
2
|
+
# Ensure the labels matching --prefix on an issue or PR equal the set passed via
|
|
3
|
+
# repeated --target flags (0, 1, or N labels).
|
|
4
|
+
# Algorithm must stay in sync with .agents/rules/issue-sync.md.
|
|
5
|
+
|
|
6
|
+
set -e
|
|
7
|
+
|
|
8
|
+
usage() {
|
|
9
|
+
printf 'Usage: %s --repo <owner/repo> (--issue <number> | --pr <number>) --prefix <prefix> [--target <label> ...]\n' "$0" >&2
|
|
10
|
+
exit 1
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
append_target() {
|
|
14
|
+
if [ -n "$targets" ]; then
|
|
15
|
+
targets=$(printf '%s\n%s' "$targets" "$1")
|
|
16
|
+
else
|
|
17
|
+
targets=$1
|
|
18
|
+
fi
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
repo=""
|
|
22
|
+
number=""
|
|
23
|
+
kind=""
|
|
24
|
+
prefix=""
|
|
25
|
+
targets=""
|
|
26
|
+
|
|
27
|
+
while [ $# -gt 0 ]; do
|
|
28
|
+
case "$1" in
|
|
29
|
+
--repo)
|
|
30
|
+
[ $# -ge 2 ] || usage
|
|
31
|
+
repo=$2
|
|
32
|
+
shift 2
|
|
33
|
+
;;
|
|
34
|
+
--issue)
|
|
35
|
+
[ $# -ge 2 ] || usage
|
|
36
|
+
[ -z "$kind" ] || usage
|
|
37
|
+
kind="issue"
|
|
38
|
+
number=$2
|
|
39
|
+
shift 2
|
|
40
|
+
;;
|
|
41
|
+
--pr)
|
|
42
|
+
[ $# -ge 2 ] || usage
|
|
43
|
+
[ -z "$kind" ] || usage
|
|
44
|
+
kind="pr"
|
|
45
|
+
number=$2
|
|
46
|
+
shift 2
|
|
47
|
+
;;
|
|
48
|
+
--prefix)
|
|
49
|
+
[ $# -ge 2 ] || usage
|
|
50
|
+
prefix=$2
|
|
51
|
+
shift 2
|
|
52
|
+
;;
|
|
53
|
+
--target)
|
|
54
|
+
[ $# -ge 2 ] || usage
|
|
55
|
+
append_target "$2"
|
|
56
|
+
shift 2
|
|
57
|
+
;;
|
|
58
|
+
*)
|
|
59
|
+
printf 'Unknown argument: %s\n' "$1" >&2
|
|
60
|
+
usage
|
|
61
|
+
;;
|
|
62
|
+
esac
|
|
63
|
+
done
|
|
64
|
+
|
|
65
|
+
[ -n "$repo" ] || usage
|
|
66
|
+
[ -n "$number" ] || usage
|
|
67
|
+
[ -n "$kind" ] || usage
|
|
68
|
+
[ -n "$prefix" ] || usage
|
|
69
|
+
|
|
70
|
+
while IFS= read -r label; do
|
|
71
|
+
[ -z "$label" ] && continue
|
|
72
|
+
case "$label" in
|
|
73
|
+
"$prefix"*) ;;
|
|
74
|
+
*)
|
|
75
|
+
printf 'Target "%s" must start with prefix "%s"\n' "$label" "$prefix" >&2
|
|
76
|
+
exit 1
|
|
77
|
+
;;
|
|
78
|
+
esac
|
|
79
|
+
done <<EOF
|
|
80
|
+
$targets
|
|
81
|
+
EOF
|
|
82
|
+
|
|
83
|
+
current_labels=$(gh "$kind" view "$number" \
|
|
84
|
+
--repo "$repo" \
|
|
85
|
+
--json labels --jq ".labels[].name | select(startswith(\"$prefix\"))" \
|
|
86
|
+
2>/dev/null || true)
|
|
87
|
+
|
|
88
|
+
while IFS= read -r label; do
|
|
89
|
+
[ -z "$label" ] && continue
|
|
90
|
+
if ! printf '%s\n' "$targets" | grep -qxF "$label"; then
|
|
91
|
+
gh "$kind" edit "$number" \
|
|
92
|
+
--repo "$repo" \
|
|
93
|
+
--remove-label "$label" \
|
|
94
|
+
2>/dev/null || true
|
|
95
|
+
fi
|
|
96
|
+
done <<EOF
|
|
97
|
+
$current_labels
|
|
98
|
+
EOF
|
|
99
|
+
|
|
100
|
+
while IFS= read -r label; do
|
|
101
|
+
[ -z "$label" ] && continue
|
|
102
|
+
if ! printf '%s\n' "$current_labels" | grep -qxF "$label"; then
|
|
103
|
+
gh "$kind" edit "$number" \
|
|
104
|
+
--repo "$repo" \
|
|
105
|
+
--add-label "$label" \
|
|
106
|
+
2>/dev/null || true
|
|
107
|
+
fi
|
|
108
|
+
done <<EOF
|
|
109
|
+
$targets
|
|
110
|
+
EOF
|
|
@@ -54,6 +54,13 @@ jobs:
|
|
|
54
54
|
printf 'type=%s\n' "$type" >> "$GITHUB_OUTPUT"
|
|
55
55
|
printf 'milestone=%s\n' "$milestone" >> "$GITHUB_OUTPUT"
|
|
56
56
|
|
|
57
|
+
- name: Checkout shared scripts
|
|
58
|
+
if: steps.metadata.outputs.is_task_comment == 'true' && steps.metadata.outputs.type != ''
|
|
59
|
+
uses: actions/checkout@v6
|
|
60
|
+
with:
|
|
61
|
+
sparse-checkout: .github/scripts
|
|
62
|
+
sparse-checkout-cone-mode: false
|
|
63
|
+
|
|
57
64
|
- name: Sync type label
|
|
58
65
|
if: steps.metadata.outputs.is_task_comment == 'true' && steps.metadata.outputs.type != ''
|
|
59
66
|
env:
|
|
@@ -72,27 +79,11 @@ jobs:
|
|
|
72
79
|
esac
|
|
73
80
|
|
|
74
81
|
if [ -n "$TYPE_LABEL" ]; then
|
|
75
|
-
|
|
82
|
+
.github/scripts/sync-labels-to-set.sh \
|
|
76
83
|
--repo "$GITHUB_REPOSITORY" \
|
|
77
|
-
--
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
printf '%s\n' "$current_type_labels" | while IFS= read -r label; do
|
|
81
|
-
[ -z "$label" ] && continue
|
|
82
|
-
if [ "$label" != "$TYPE_LABEL" ]; then
|
|
83
|
-
gh issue edit "$ISSUE_NUMBER" \
|
|
84
|
-
--repo "$GITHUB_REPOSITORY" \
|
|
85
|
-
--remove-label "$label" \
|
|
86
|
-
2>/dev/null || true
|
|
87
|
-
fi
|
|
88
|
-
done || true
|
|
89
|
-
|
|
90
|
-
if ! printf '%s\n' "$current_type_labels" | grep -qxF "$TYPE_LABEL"; then
|
|
91
|
-
gh issue edit "$ISSUE_NUMBER" \
|
|
92
|
-
--repo "$GITHUB_REPOSITORY" \
|
|
93
|
-
--add-label "$TYPE_LABEL" \
|
|
94
|
-
2>/dev/null || true
|
|
95
|
-
fi
|
|
84
|
+
--issue "$ISSUE_NUMBER" \
|
|
85
|
+
--prefix "type:" \
|
|
86
|
+
--target "$TYPE_LABEL"
|
|
96
87
|
fi
|
|
97
88
|
|
|
98
89
|
- name: Sync milestone
|
|
@@ -18,7 +18,7 @@ jobs:
|
|
|
18
18
|
runs-on: ubuntu-latest
|
|
19
19
|
steps:
|
|
20
20
|
- name: Checkout base branch
|
|
21
|
-
uses: actions/checkout@
|
|
21
|
+
uses: actions/checkout@v6
|
|
22
22
|
|
|
23
23
|
- name: Sync in-labels
|
|
24
24
|
env:
|
|
@@ -37,28 +37,19 @@ jobs:
|
|
|
37
37
|
| map("in: " + .key)
|
|
38
38
|
| .[]?')
|
|
39
39
|
|
|
40
|
-
|
|
40
|
+
set -- \
|
|
41
41
|
--repo "$GITHUB_REPOSITORY" \
|
|
42
|
-
--
|
|
43
|
-
|
|
42
|
+
--pr "$PR_NUMBER" \
|
|
43
|
+
--prefix "in: "
|
|
44
44
|
|
|
45
|
-
|
|
45
|
+
while IFS= read -r label; do
|
|
46
46
|
[ -z "$label" ] && continue
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
fi
|
|
52
|
-
done
|
|
47
|
+
set -- "$@" --target "$label"
|
|
48
|
+
done <<EOF
|
|
49
|
+
$should_labels
|
|
50
|
+
EOF
|
|
53
51
|
|
|
54
|
-
|
|
55
|
-
[ -z "$label" ] && continue
|
|
56
|
-
if ! printf '%s\n' "$should_labels" | grep -qxF "$label"; then
|
|
57
|
-
gh pr edit "$PR_NUMBER" \
|
|
58
|
-
--repo "$GITHUB_REPOSITORY" \
|
|
59
|
-
--remove-label "$label" 2>/dev/null || true
|
|
60
|
-
fi
|
|
61
|
-
done
|
|
52
|
+
.github/scripts/sync-labels-to-set.sh "$@"
|
|
62
53
|
|
|
63
54
|
- name: Assign PR creator if unassigned
|
|
64
55
|
env:
|