@fitlab-ai/agent-infra 0.5.5 → 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.
Files changed (28) hide show
  1. package/README.md +94 -1
  2. package/README.zh-CN.md +94 -1
  3. package/lib/sandbox/commands/create.js +7 -3
  4. package/lib/sandbox/shell.js +47 -7
  5. package/lib/sandbox/tools.js +18 -14
  6. package/package.json +1 -1
  7. package/templates/.agents/README.en.md +52 -0
  8. package/templates/.agents/README.zh-CN.md +52 -0
  9. package/templates/.agents/rules/milestone-inference.github.en.md +6 -5
  10. package/templates/.agents/rules/milestone-inference.github.zh-CN.md +6 -5
  11. package/templates/.agents/scripts/platform-adapters/platform-sync.github.js +47 -12
  12. package/templates/.agents/skills/analyze-task/SKILL.en.md +1 -1
  13. package/templates/.agents/skills/analyze-task/SKILL.zh-CN.md +1 -1
  14. package/templates/.agents/skills/implement-task/SKILL.en.md +1 -1
  15. package/templates/.agents/skills/implement-task/SKILL.zh-CN.md +1 -1
  16. package/templates/.agents/skills/import-issue/SKILL.en.md +10 -2
  17. package/templates/.agents/skills/import-issue/SKILL.zh-CN.md +10 -2
  18. package/templates/.agents/skills/import-issue/config/verify.json +2 -1
  19. package/templates/.agents/skills/plan-task/SKILL.en.md +1 -1
  20. package/templates/.agents/skills/plan-task/SKILL.zh-CN.md +1 -1
  21. package/templates/.agents/skills/refine-task/SKILL.en.md +1 -1
  22. package/templates/.agents/skills/refine-task/SKILL.zh-CN.md +1 -1
  23. package/templates/.agents/skills/review-task/SKILL.en.md +1 -1
  24. package/templates/.agents/skills/review-task/SKILL.zh-CN.md +1 -1
  25. package/templates/.agents/skills/update-agent-infra/scripts/sync-templates.js +315 -1
  26. package/templates/.github/workflows/metadata-sync.yml +1 -1
  27. package/templates/.github/workflows/pr-label.yml +1 -1
  28. package/templates/.github/workflows/status-label.yml +1 -1
@@ -935,22 +935,22 @@ function resolveUpstreamRepo(taskDir) {
935
935
  return ownerRepo;
936
936
  }
937
937
 
938
- const upstreamResult = ghText([
938
+ const repoResult = ghJson([
939
939
  "api",
940
- `repos/${ownerRepo.value}`,
941
- "--jq",
942
- "if .fork then .parent.full_name else .full_name end"
940
+ `repos/${ownerRepo.value}`
943
941
  ], taskDir);
944
942
 
945
- if (!upstreamResult.ok) {
946
- return upstreamResult;
943
+ if (!repoResult.ok) {
944
+ return repoResult;
947
945
  }
948
946
 
949
- if (isBlank(upstreamResult.value)) {
947
+ const repo = repoResult.value && typeof repoResult.value === "object" ? repoResult.value : {};
948
+ const upstreamRepo = repo.fork ? repo.parent?.full_name : repo.full_name;
949
+ if (isBlank(upstreamRepo)) {
950
950
  return { ok: false, message: "Unable to resolve upstream repository" };
951
951
  }
952
952
 
953
- return { ok: true, value: upstreamResult.value };
953
+ return { ok: true, value: upstreamRepo };
954
954
  }
955
955
 
956
956
  function resolveOwnerRepo(taskDir) {
@@ -1017,11 +1017,12 @@ function ghText(args, cwd) {
1017
1017
  }
1018
1018
 
1019
1019
  function ghCommand(args, cwd) {
1020
- const result = spawnSync("gh", args, {
1020
+ const command = resolveCommand("gh");
1021
+ const result = spawnSync(command, args, commandOptions(command, {
1021
1022
  cwd,
1022
1023
  encoding: "utf8",
1023
1024
  env: process.env
1024
- });
1025
+ }));
1025
1026
 
1026
1027
  if (result.status !== 0) {
1027
1028
  const stderr = `${result.stderr || ""}${result.stdout || ""}`.trim();
@@ -1037,11 +1038,12 @@ function ghPaginatedJson(args, cwd) {
1037
1038
  }
1038
1039
 
1039
1040
  function gitText(args, cwd) {
1040
- const result = spawnSync("git", args, {
1041
+ const command = resolveCommand("git");
1042
+ const result = spawnSync(command, args, commandOptions(command, {
1041
1043
  cwd,
1042
1044
  encoding: "utf8",
1043
1045
  env: process.env
1044
- });
1046
+ }));
1045
1047
 
1046
1048
  if (result.status !== 0) {
1047
1049
  const stderr = `${result.stderr || ""}${result.stdout || ""}`.trim();
@@ -1110,6 +1112,39 @@ function sleep(delayMs) {
1110
1112
  Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, delayMs);
1111
1113
  }
1112
1114
 
1115
+ function resolveCommand(cmd) {
1116
+ if (process.platform !== "win32" || path.extname(cmd)) {
1117
+ return cmd;
1118
+ }
1119
+
1120
+ const pathValue = process.env.Path || process.env.PATH || "";
1121
+ const extensions = (process.env.PATHEXT || ".COM;.EXE;.BAT;.CMD")
1122
+ .split(";")
1123
+ .filter(Boolean);
1124
+
1125
+ for (const dir of pathValue.split(path.delimiter).filter(Boolean)) {
1126
+ for (const extension of extensions) {
1127
+ const lowerCandidate = path.join(dir, `${cmd}${extension.toLowerCase()}`);
1128
+ if (fs.existsSync(lowerCandidate)) {
1129
+ return lowerCandidate;
1130
+ }
1131
+ const upperCandidate = path.join(dir, `${cmd}${extension.toUpperCase()}`);
1132
+ if (fs.existsSync(upperCandidate)) {
1133
+ return upperCandidate;
1134
+ }
1135
+ }
1136
+ }
1137
+
1138
+ return cmd;
1139
+ }
1140
+
1141
+ function commandOptions(cmd, options) {
1142
+ if (process.platform === "win32" && /\.(?:bat|cmd)$/i.test(cmd)) {
1143
+ return { ...options, shell: true };
1144
+ }
1145
+ return options;
1146
+ }
1147
+
1113
1148
  function interpolate(template, taskDir, artifactFile) {
1114
1149
  const artifactStem = artifactFile ? path.basename(artifactFile, path.extname(artifactFile)) : "";
1115
1150
  return template
@@ -121,8 +121,8 @@ Update `.agents/workspace/active/{task-id}/task.md`:
121
121
  If task.md contains a valid `issue_number`, perform these sync actions (skip and continue on any failure):
122
122
  - Read `.agents/rules/issue-sync.md` before syncing, and complete upstream repository detection plus permission detection
123
123
  - Set `status: pending-design-work` by following issue-sync.md
124
- - Publish the `{analysis-artifact}` comment
125
124
  - Create or update the `<!-- sync-issue:{task-id}:task -->` comment (follow the task.md comment sync rule in issue-sync.md)
125
+ - Publish the `{analysis-artifact}` comment
126
126
 
127
127
  ### 7. Verification Gate
128
128
 
@@ -121,8 +121,8 @@ date "+%Y-%m-%d %H:%M:%S%:z"
121
121
  如果 task.md 中存在有效的 `issue_number`,执行以下同步操作(任一失败则跳过并继续):
122
122
  - 执行前先读取 `.agents/rules/issue-sync.md`,完成 upstream 仓库检测和权限检测
123
123
  - 按 issue-sync.md 设置 `status: pending-design-work`
124
- - 发布 `{analysis-artifact}` 评论
125
124
  - 创建或更新 `<!-- sync-issue:{task-id}:task -->` 评论(按 issue-sync.md 的 task.md 评论同步规则)
125
+ - 发布 `{analysis-artifact}` 评论
126
126
 
127
127
  ### 7. 完成校验
128
128
 
@@ -95,8 +95,8 @@ Update `.agents/workspace/active/{task-id}/task.md`:
95
95
 
96
96
  If task.md contains a valid `issue_number`, perform these sync actions (skip and continue on any failure; read `.agents/rules/issue-sync.md` first and complete upstream repository detection plus permission detection):
97
97
  - Set `status: in-progress` by following issue-sync.md
98
- - Publish the `{implementation-artifact}` comment
99
98
  - Create or update the `<!-- sync-issue:{task-id}:task -->` comment (follow the task.md comment sync rule in issue-sync.md)
99
+ - Publish the `{implementation-artifact}` comment
100
100
 
101
101
  ### 10. Verification Gate
102
102
 
@@ -95,8 +95,8 @@ date "+%Y-%m-%d %H:%M:%S%:z"
95
95
 
96
96
  如果 task.md 中存在有效的 `issue_number`,执行以下同步操作(任一失败则跳过并继续;执行前先读取 `.agents/rules/issue-sync.md`,完成 upstream 仓库检测和权限检测):
97
97
  - 按 issue-sync.md 设置 `status: in-progress`
98
- - 发布 `{implementation-artifact}` 评论
99
98
  - 创建或更新 `<!-- sync-issue:{task-id}:task -->` 评论(按 issue-sync.md 的 task.md 评论同步规则)
99
+ - 发布 `{implementation-artifact}` 评论
100
100
 
101
101
  ### 10. 完成校验
102
102
 
@@ -74,7 +74,14 @@ Update `.agents/workspace/active/{task-id}/task.md`:
74
74
 
75
75
  If task.md contains a valid `issue_number`, use the Issue update command from `.agents/rules/issue-pr-commands.md` to add the current executor as an assignee. The behavioral boundary still follows `.agents/rules/issue-sync.md`.
76
76
 
77
- ### 6. Verification Gate
77
+ ### 6. Sync to the Issue
78
+
79
+ If task.md contains a valid `issue_number`, perform these sync actions (skip and continue on any failure):
80
+ - Read `.agents/rules/issue-sync.md` before syncing, and complete upstream repository detection plus permission detection
81
+ - Check the Issue's current milestone; if it is unset, read `.agents/rules/milestone-inference.md` and infer plus set the milestone using "Stage 1: `create-issue`". If `has_triage=false` or the inference is uncertain, skip and continue
82
+ - Create or update the `<!-- sync-issue:{task-id}:task -->` comment (follow the task.md comment sync rule in issue-sync.md)
83
+
84
+ ### 7. Verification Gate
78
85
 
79
86
  Run the verification gate to confirm the task artifact and sync state are valid:
80
87
 
@@ -89,7 +96,7 @@ Handle the result as follows:
89
96
 
90
97
  Keep the gate output in your reply as fresh evidence. Do not claim completion without output from this run.
91
98
 
92
- ### 7. Inform User
99
+ ### 8. Inform User
93
100
 
94
101
  > Execute this step only after the verification gate passes.
95
102
 
@@ -119,6 +126,7 @@ Next step - run requirements analysis:
119
126
  - [ ] Updated `current_step` to requirement-analysis
120
127
  - [ ] Updated `updated_at` to the current time
121
128
  - [ ] Appended an Activity Log entry to task.md
129
+ - [ ] Synced the task comment to the Issue
122
130
  - [ ] Informed the user of the next step (must include all TUI command formats; do not filter)
123
131
  - [ ] **Did not modify any business code**
124
132
 
@@ -74,7 +74,14 @@ date "+%Y-%m-%d %H:%M:%S%:z"
74
74
 
75
75
  如果 task.md 中存在有效的 `issue_number`,按 `.agents/rules/issue-pr-commands.md` 的 Issue 更新命令为当前执行者添加 assignee;Assignee 同步的边界仍遵循 `.agents/rules/issue-sync.md`。
76
76
 
77
- ### 6. 完成校验
77
+ ### 6. 同步到 Issue
78
+
79
+ 如果 task.md 中存在有效的 `issue_number`,执行以下同步操作(任一失败则跳过并继续):
80
+ - 执行前先读取 `.agents/rules/issue-sync.md`,完成 upstream 仓库检测和权限检测
81
+ - 检查 Issue 当前 milestone;如果未设置,先读取 `.agents/rules/milestone-inference.md`,按其中的「阶段 1:`create-issue`」规则推断并设置 milestone;如果 `has_triage=false` 或推断不确定,跳过并继续
82
+ - 创建或更新 `<!-- sync-issue:{task-id}:task -->` 评论(按 issue-sync.md 的 task.md 评论同步规则)
83
+
84
+ ### 7. 完成校验
78
85
 
79
86
  运行完成校验,确认任务产物和同步状态符合规范:
80
87
 
@@ -89,7 +96,7 @@ node .agents/scripts/validate-artifact.js gate import-issue .agents/workspace/ac
89
96
 
90
97
  将校验输出保留在回复中作为当次验证输出。没有当次校验输出,不得声明完成。
91
98
 
92
- ### 7. 告知用户
99
+ ### 8. 告知用户
93
100
 
94
101
  > 仅在校验通过后执行本步骤。
95
102
 
@@ -119,6 +126,7 @@ Issue #{number} 已导入。
119
126
  - [ ] 更新了 `current_step` 为 requirement-analysis
120
127
  - [ ] 更新了 `updated_at` 为当前时间
121
128
  - [ ] 追加了 Activity Log 条目到 task.md
129
+ - [ ] 同步了 task 评论到 Issue
122
130
  - [ ] 告知了用户下一步(必须展示所有 TUI 的命令格式,不要筛选)
123
131
  - [ ] **没有修改任何业务代码**
124
132
 
@@ -22,7 +22,8 @@
22
22
  },
23
23
  "platform-sync": {
24
24
  "when": "issue_number_exists",
25
- "issue_must_exist": true
25
+ "issue_must_exist": true,
26
+ "verify_task_comment_content": true
26
27
  }
27
28
  }
28
29
  }
@@ -99,8 +99,8 @@ Update `.agents/workspace/active/{task-id}/task.md`:
99
99
  If task.md contains a valid `issue_number`, perform these sync actions (skip and continue on any failure):
100
100
  - Read `.agents/rules/issue-sync.md` before syncing, and complete upstream repository detection plus permission detection
101
101
  - Set `status: pending-design-work` by following issue-sync.md
102
- - Publish the `{plan-artifact}` comment
103
102
  - Create or update the `<!-- sync-issue:{task-id}:task -->` comment (follow the task.md comment sync rule in issue-sync.md)
103
+ - Publish the `{plan-artifact}` comment
104
104
 
105
105
  ### 8. Verification Gate
106
106
 
@@ -99,8 +99,8 @@ date "+%Y-%m-%d %H:%M:%S%:z"
99
99
  如果 task.md 中存在有效的 `issue_number`,执行以下同步操作(任一失败则跳过并继续):
100
100
  - 执行前先读取 `.agents/rules/issue-sync.md`,完成 upstream 仓库检测和权限检测
101
101
  - 按 issue-sync.md 设置 `status: pending-design-work`
102
- - 发布 `{plan-artifact}` 评论
103
102
  - 创建或更新 `<!-- sync-issue:{task-id}:task -->` 评论(按 issue-sync.md 的 task.md 评论同步规则)
103
+ - 发布 `{plan-artifact}` 评论
104
104
 
105
105
  ### 8. 完成校验
106
106
 
@@ -62,8 +62,8 @@ Update task.md:
62
62
  If task.md contains a valid `issue_number`, perform these sync actions (skip and continue on any failure):
63
63
  - Read `.agents/rules/issue-sync.md` before syncing, and complete upstream repository detection plus permission detection
64
64
  - Set `status: in-progress` by following issue-sync.md
65
- - Publish the `{refinement-artifact}` comment
66
65
  - Create or update the `<!-- sync-issue:{task-id}:task -->` comment (follow the task.md comment sync rule in issue-sync.md)
66
+ - Publish the `{refinement-artifact}` comment
67
67
 
68
68
  ### 7. Verification Gate
69
69
 
@@ -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
 
@@ -77,7 +78,7 @@ const DEFAULTS = {
77
78
  }
78
79
  };
79
80
 
80
- const INSTALLER_VERSION = "v0.5.5";
81
+ const INSTALLER_VERSION = "v0.5.6";
81
82
  const PACKAGE_NAME = '@fitlab-ai/agent-infra';
82
83
  // Add a new identifier here only after shipping matching .{platform}. template variants.
83
84
  const KNOWN_PLATFORMS = new Set(['github']);
@@ -124,6 +125,292 @@ function removeEmptyDirs(dir) {
124
125
  }
125
126
  }
126
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
+
127
414
  function matchesAny(rel, patterns) {
128
415
  const n = norm(rel);
129
416
  return patterns.some(p => norm(p) === n || globMatch(p, n));
@@ -412,6 +699,8 @@ function syncTemplates(projectRoot, templateRootOverride) {
412
699
  const { project, org, language: lang = 'en' } = cfg;
413
700
  const platformType = cfg.platform?.type || DEFAULTS.platform.type;
414
701
  const vars = { project, org };
702
+ const templateSkillNames = listTemplateSkillNames(templateRoot);
703
+ const protectedCustomSkills = detectCustomSkills(projectRoot, templateSkillNames);
415
704
 
416
705
  const managed = [...(cfg.files.managed || [])];
417
706
  const merged = [...(cfg.files.merged || [])];
@@ -422,6 +711,15 @@ function syncTemplates(projectRoot, templateRootOverride) {
422
711
  templateRoot: norm(templateRoot),
423
712
  registryAdded: [],
424
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
+ },
425
723
  ejected: { created: [], skipped: [] },
426
724
  merged: { pending: [] },
427
725
  configUpdated: false,
@@ -497,6 +795,7 @@ function syncTemplates(projectRoot, templateRootOverride) {
497
795
  for (const projFile of projFiles) {
498
796
  if (expectedTargets.has(projFile)) continue;
499
797
  if (projFile === configPathRel) continue;
798
+ if (isCustomProtected(projFile, protectedCustomSkills, project)) continue;
500
799
  if (matchesAny(projFile, merged) || matchesAny(projFile, ejected)) continue;
501
800
 
502
801
  fs.unlinkSync(path.join(projectRoot, projFile));
@@ -509,6 +808,16 @@ function syncTemplates(projectRoot, templateRootOverride) {
509
808
  }
510
809
  }
511
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
+
512
821
  for (const entry of ejected) {
513
822
  const dstFull = path.join(projectRoot, entry);
514
823
  if (fs.existsSync(dstFull)) {
@@ -557,6 +866,11 @@ function syncTemplates(projectRoot, templateRootOverride) {
557
866
  report.managed.written.length +
558
867
  report.managed.created.length +
559
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 +
560
874
  report.ejected.created.length +
561
875
  report.registryAdded.length
562
876
  ) > 0;
@@ -56,7 +56,7 @@ jobs:
56
56
 
57
57
  - name: Checkout shared scripts
58
58
  if: steps.metadata.outputs.is_task_comment == 'true' && steps.metadata.outputs.type != ''
59
- uses: actions/checkout@v4
59
+ uses: actions/checkout@v6
60
60
  with:
61
61
  sparse-checkout: .github/scripts
62
62
  sparse-checkout-cone-mode: false
@@ -18,7 +18,7 @@ jobs:
18
18
  runs-on: ubuntu-latest
19
19
  steps:
20
20
  - name: Checkout base branch
21
- uses: actions/checkout@v4
21
+ uses: actions/checkout@v6
22
22
 
23
23
  - name: Sync in-labels
24
24
  env: