@fitlab-ai/agent-infra 0.5.6 → 0.5.8

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 (166) hide show
  1. package/README.md +92 -4
  2. package/README.zh-CN.md +92 -4
  3. package/bin/cli.js +28 -4
  4. package/lib/defaults.json +5 -2
  5. package/lib/init.js +86 -5
  6. package/lib/prompt.js +28 -1
  7. package/lib/render.js +1 -1
  8. package/lib/sandbox/commands/rm.js +6 -4
  9. package/lib/sandbox/commands/vm.js +43 -16
  10. package/lib/sandbox/config.js +5 -0
  11. package/lib/sandbox/engine.js +144 -16
  12. package/lib/sandbox/shell.js +36 -2
  13. package/lib/sandbox/task-resolver.js +13 -6
  14. package/lib/update.js +14 -3
  15. package/package.json +5 -5
  16. package/templates/.agents/QUICKSTART.en.md +19 -2
  17. package/templates/.agents/QUICKSTART.zh-CN.md +19 -2
  18. package/templates/.agents/README.en.md +71 -2
  19. package/templates/.agents/README.zh-CN.md +71 -2
  20. package/templates/.agents/rules/issue-pr-commands.en.md +5 -0
  21. package/templates/.agents/rules/issue-pr-commands.github.en.md +60 -0
  22. package/templates/.agents/rules/issue-pr-commands.github.zh-CN.md +60 -0
  23. package/templates/.agents/rules/issue-pr-commands.zh-CN.md +5 -0
  24. package/templates/.agents/rules/issue-sync.en.md +19 -0
  25. package/templates/.agents/rules/issue-sync.github.en.md +14 -0
  26. package/templates/.agents/rules/issue-sync.github.zh-CN.md +14 -0
  27. package/templates/.agents/rules/issue-sync.zh-CN.md +19 -0
  28. package/templates/.agents/rules/label-milestone-setup.en.md +5 -0
  29. package/templates/.agents/rules/label-milestone-setup.github.en.md +10 -0
  30. package/templates/.agents/rules/label-milestone-setup.github.zh-CN.md +10 -0
  31. package/templates/.agents/rules/label-milestone-setup.zh-CN.md +5 -0
  32. package/templates/.agents/rules/milestone-inference.en.md +5 -0
  33. package/templates/.agents/rules/milestone-inference.zh-CN.md +5 -0
  34. package/templates/.agents/rules/pr-sync.en.md +5 -0
  35. package/templates/.agents/rules/pr-sync.zh-CN.md +5 -0
  36. package/templates/.agents/rules/release-commands.en.md +5 -0
  37. package/templates/.agents/rules/release-commands.github.en.md +16 -0
  38. package/templates/.agents/rules/release-commands.github.zh-CN.md +16 -0
  39. package/templates/.agents/rules/release-commands.zh-CN.md +5 -0
  40. package/templates/.agents/rules/security-alerts.en.md +5 -0
  41. package/templates/.agents/rules/security-alerts.zh-CN.md +5 -0
  42. package/templates/.agents/scripts/platform-adapters/find-existing-task.github.js +272 -0
  43. package/templates/.agents/scripts/platform-adapters/find-existing-task.js +5 -0
  44. package/templates/.agents/scripts/platform-adapters/platform-sync.github.js +88 -8
  45. package/templates/.agents/scripts/platform-adapters/platform-sync.js +13 -0
  46. package/templates/.agents/skills/analyze-task/SKILL.en.md +5 -5
  47. package/templates/.agents/skills/analyze-task/SKILL.zh-CN.md +5 -5
  48. package/templates/.agents/skills/analyze-task/config/verify.json +3 -1
  49. package/templates/.agents/skills/block-task/SKILL.en.md +1 -1
  50. package/templates/.agents/skills/block-task/SKILL.zh-CN.md +1 -1
  51. package/templates/.agents/skills/block-task/config/verify.json +2 -1
  52. package/templates/.agents/skills/cancel-task/SKILL.en.md +3 -3
  53. package/templates/.agents/skills/cancel-task/SKILL.zh-CN.md +4 -4
  54. package/templates/.agents/skills/cancel-task/config/verify.json +2 -1
  55. package/templates/.agents/skills/check-task/SKILL.en.md +1 -1
  56. package/templates/.agents/skills/check-task/SKILL.zh-CN.md +1 -1
  57. package/templates/.agents/skills/close-codescan/SKILL.en.md +3 -3
  58. package/templates/.agents/skills/close-codescan/SKILL.zh-CN.md +3 -3
  59. package/templates/.agents/skills/close-dependabot/SKILL.en.md +1 -1
  60. package/templates/.agents/skills/close-dependabot/SKILL.zh-CN.md +1 -1
  61. package/templates/.agents/skills/commit/SKILL.en.md +2 -2
  62. package/templates/.agents/skills/commit/SKILL.zh-CN.md +2 -2
  63. package/templates/.agents/skills/commit/config/verify.json +2 -1
  64. package/templates/.agents/skills/complete-task/SKILL.en.md +1 -1
  65. package/templates/.agents/skills/complete-task/SKILL.zh-CN.md +1 -1
  66. package/templates/.agents/skills/complete-task/config/verify.json +2 -1
  67. package/templates/.agents/skills/create-issue/SKILL.en.md +10 -10
  68. package/templates/.agents/skills/create-issue/SKILL.zh-CN.md +10 -10
  69. package/templates/.agents/skills/create-issue/config/verify.json +2 -1
  70. package/templates/.agents/skills/create-issue/reference/label-and-type.en.md +3 -3
  71. package/templates/.agents/skills/create-issue/reference/label-and-type.zh-CN.md +3 -3
  72. package/templates/.agents/skills/create-issue/reference/template-matching.en.md +6 -34
  73. package/templates/.agents/skills/create-issue/reference/template-matching.zh-CN.md +8 -36
  74. package/templates/.agents/skills/create-pr/SKILL.en.md +3 -3
  75. package/templates/.agents/skills/create-pr/SKILL.zh-CN.md +3 -3
  76. package/templates/.agents/skills/create-pr/config/verify.json +2 -1
  77. package/templates/.agents/skills/create-pr/reference/pr-body-template.en.md +7 -17
  78. package/templates/.agents/skills/create-pr/reference/pr-body-template.zh-CN.md +27 -37
  79. package/templates/.agents/skills/create-release-note/SKILL.en.md +16 -9
  80. package/templates/.agents/skills/create-release-note/SKILL.zh-CN.md +16 -9
  81. package/templates/.agents/skills/create-task/SKILL.en.md +5 -5
  82. package/templates/.agents/skills/create-task/SKILL.zh-CN.md +5 -5
  83. package/templates/.agents/skills/implement-task/SKILL.en.md +3 -3
  84. package/templates/.agents/skills/implement-task/SKILL.zh-CN.md +3 -3
  85. package/templates/.agents/skills/implement-task/config/verify.json +3 -1
  86. package/templates/.agents/skills/import-codescan/SKILL.en.md +3 -3
  87. package/templates/.agents/skills/import-codescan/SKILL.zh-CN.md +3 -3
  88. package/templates/.agents/skills/import-dependabot/SKILL.en.md +2 -2
  89. package/templates/.agents/skills/import-dependabot/SKILL.zh-CN.md +2 -2
  90. package/templates/.agents/skills/import-issue/SKILL.en.md +41 -11
  91. package/templates/.agents/skills/import-issue/SKILL.zh-CN.md +41 -11
  92. package/templates/.agents/skills/init-labels/SKILL.en.md +10 -10
  93. package/templates/.agents/skills/init-labels/SKILL.zh-CN.md +10 -10
  94. package/templates/.agents/skills/init-labels/scripts/init-labels.sh +6 -0
  95. package/templates/.agents/skills/init-milestones/SKILL.en.md +8 -8
  96. package/templates/.agents/skills/init-milestones/SKILL.zh-CN.md +8 -8
  97. package/templates/.agents/skills/init-milestones/scripts/init-milestones.sh +6 -0
  98. package/templates/.agents/skills/plan-task/SKILL.en.md +3 -3
  99. package/templates/.agents/skills/plan-task/SKILL.zh-CN.md +3 -3
  100. package/templates/.agents/skills/plan-task/config/verify.json +3 -1
  101. package/templates/.agents/skills/post-release/SKILL.en.md +95 -0
  102. package/templates/.agents/skills/post-release/SKILL.zh-CN.md +95 -0
  103. package/templates/.agents/skills/refine-task/SKILL.en.md +2 -2
  104. package/templates/.agents/skills/refine-task/SKILL.zh-CN.md +2 -2
  105. package/templates/.agents/skills/refine-task/config/verify.json +3 -1
  106. package/templates/.agents/skills/refine-title/SKILL.en.md +1 -1
  107. package/templates/.agents/skills/refine-title/SKILL.zh-CN.md +1 -1
  108. package/templates/.agents/skills/release/SKILL.en.md +6 -1
  109. package/templates/.agents/skills/release/SKILL.zh-CN.md +6 -1
  110. package/templates/.agents/skills/release/scripts/manage-milestones.sh +6 -0
  111. package/templates/.agents/skills/restore-task/SKILL.en.md +13 -64
  112. package/templates/.agents/skills/restore-task/SKILL.zh-CN.md +13 -64
  113. package/templates/.agents/skills/review-task/SKILL.en.md +3 -3
  114. package/templates/.agents/skills/review-task/SKILL.zh-CN.md +3 -3
  115. package/templates/.agents/skills/review-task/config/verify.json +3 -1
  116. package/templates/.agents/skills/test/SKILL.en.md +1 -1
  117. package/templates/.agents/skills/test/SKILL.zh-CN.md +1 -1
  118. package/templates/.agents/skills/test-integration/SKILL.en.md +1 -1
  119. package/templates/.agents/skills/test-integration/SKILL.zh-CN.md +1 -1
  120. package/templates/.agents/skills/update-agent-infra/SKILL.en.md +12 -2
  121. package/templates/.agents/skills/update-agent-infra/SKILL.zh-CN.md +6 -2
  122. package/templates/.agents/skills/update-agent-infra/scripts/sync-templates.js +344 -16
  123. package/templates/.agents/skills/upgrade-dependency/SKILL.en.md +1 -1
  124. package/templates/.agents/skills/upgrade-dependency/SKILL.zh-CN.md +1 -1
  125. package/templates/.agents/templates/task.en.md +2 -2
  126. package/templates/.agents/templates/task.zh-CN.md +2 -2
  127. package/templates/.claude/commands/create-issue.en.md +1 -1
  128. package/templates/.claude/commands/create-issue.zh-CN.md +1 -1
  129. package/templates/.claude/commands/import-issue.en.md +1 -1
  130. package/templates/.claude/commands/import-issue.zh-CN.md +1 -1
  131. package/templates/.claude/commands/init-labels.en.md +1 -1
  132. package/templates/.claude/commands/init-labels.zh-CN.md +1 -1
  133. package/templates/.claude/commands/init-milestones.en.md +1 -1
  134. package/templates/.claude/commands/init-milestones.zh-CN.md +1 -1
  135. package/templates/.claude/commands/post-release.en.md +8 -0
  136. package/templates/.claude/commands/post-release.zh-CN.md +8 -0
  137. package/templates/.claude/commands/restore-task.en.md +1 -1
  138. package/templates/.claude/commands/restore-task.zh-CN.md +1 -1
  139. package/templates/.claude/hooks/check-version-format.sh +1 -1
  140. package/templates/.gemini/commands/_project_/create-issue.en.toml +1 -1
  141. package/templates/.gemini/commands/_project_/create-issue.zh-CN.toml +1 -1
  142. package/templates/.gemini/commands/_project_/import-issue.en.toml +1 -1
  143. package/templates/.gemini/commands/_project_/import-issue.zh-CN.toml +1 -1
  144. package/templates/.gemini/commands/_project_/init-labels.en.toml +2 -2
  145. package/templates/.gemini/commands/_project_/init-labels.zh-CN.toml +2 -2
  146. package/templates/.gemini/commands/_project_/init-milestones.en.toml +2 -2
  147. package/templates/.gemini/commands/_project_/init-milestones.zh-CN.toml +2 -2
  148. package/templates/.gemini/commands/_project_/post-release.en.toml +6 -0
  149. package/templates/.gemini/commands/_project_/post-release.zh-CN.toml +6 -0
  150. package/templates/.gemini/commands/_project_/restore-task.en.toml +1 -1
  151. package/templates/.gemini/commands/_project_/restore-task.zh-CN.toml +1 -1
  152. package/templates/{.github/hooks → .git-hooks}/check-version-format.sh +2 -2
  153. package/templates/.github/workflows/pr-label.yml +1 -1
  154. package/templates/.opencode/commands/create-issue.en.md +1 -1
  155. package/templates/.opencode/commands/create-issue.zh-CN.md +1 -1
  156. package/templates/.opencode/commands/import-issue.en.md +1 -1
  157. package/templates/.opencode/commands/import-issue.zh-CN.md +1 -1
  158. package/templates/.opencode/commands/init-labels.en.md +1 -1
  159. package/templates/.opencode/commands/init-labels.zh-CN.md +1 -1
  160. package/templates/.opencode/commands/init-milestones.en.md +1 -1
  161. package/templates/.opencode/commands/init-milestones.zh-CN.md +1 -1
  162. package/templates/.opencode/commands/post-release.en.md +9 -0
  163. package/templates/.opencode/commands/post-release.zh-CN.md +9 -0
  164. package/templates/.opencode/commands/restore-task.en.md +1 -1
  165. package/templates/.opencode/commands/restore-task.zh-CN.md +1 -1
  166. /package/templates/{.github/hooks → .git-hooks}/pre-commit +0 -0
@@ -24,6 +24,7 @@ const DEFAULTS = {
24
24
  "type": "github"
25
25
  },
26
26
  "sandbox": {
27
+ "engine": null,
27
28
  "runtimes": [
28
29
  "node20"
29
30
  ],
@@ -56,29 +57,30 @@ const DEFAULTS = {
56
57
  ".claude/commands/",
57
58
  ".claude/hooks/",
58
59
  ".gemini/commands/",
59
- ".github/hooks/check-version-format.sh",
60
+ ".git-hooks/check-version-format.sh",
60
61
  ".github/scripts/",
61
62
  ".opencode/commands/"
62
63
  ],
63
64
  "merged": [
65
+ "**/post-release.*",
64
66
  "**/release.*",
65
67
  "**/test-integration.*",
66
68
  "**/test.*",
67
69
  "**/upgrade-dependency.*",
70
+ ".agents/skills/post-release/SKILL.*",
68
71
  ".agents/skills/release/SKILL.*",
69
72
  ".agents/skills/test-integration/SKILL.*",
70
73
  ".agents/skills/test/SKILL.*",
71
74
  ".agents/skills/upgrade-dependency/SKILL.*",
72
75
  ".claude/settings.json",
73
76
  ".gemini/settings.json",
74
- ".github/hooks/pre-commit",
77
+ ".git-hooks/pre-commit",
75
78
  ".gitignore"
76
79
  ],
77
80
  "ejected": []
78
81
  }
79
82
  };
80
83
 
81
- const INSTALLER_VERSION = "v0.5.6";
82
84
  const PACKAGE_NAME = '@fitlab-ai/agent-infra';
83
85
  // Add a new identifier here only after shipping matching .{platform}. template variants.
84
86
  const KNOWN_PLATFORMS = new Set(['github']);
@@ -86,6 +88,31 @@ const KNOWN_LANGUAGES = new Set(['en', 'zh-CN']);
86
88
 
87
89
  function norm(p) { return p.replace(/\\/g, '/'); }
88
90
 
91
+ function normDir(p) {
92
+ return norm(p).replace(/^\.\//, '').replace(/\/+$/, '');
93
+ }
94
+
95
+ function isInsideProject(projectRoot, relativePath) {
96
+ if (typeof relativePath !== 'string' || relativePath.trim() === '' || path.isAbsolute(relativePath)) {
97
+ return false;
98
+ }
99
+
100
+ const root = path.resolve(projectRoot);
101
+ const resolved = path.resolve(projectRoot, relativePath);
102
+ const rel = path.relative(root, resolved);
103
+ return rel !== '' && !rel.startsWith('..') && !path.isAbsolute(rel);
104
+ }
105
+
106
+ function isPathOwnedByOtherPlatform(relativePath, platformType) {
107
+ const normalized = norm(relativePath).replace(/^\.\//, '');
108
+ const top = normalized.split('/')[0];
109
+ if (!top.startsWith('.')) return false;
110
+
111
+ const candidate = top.slice(1);
112
+ if (!KNOWN_PLATFORMS.has(candidate)) return false;
113
+ return candidate !== platformType;
114
+ }
115
+
89
116
  function globMatch(pattern, filePath) {
90
117
  const p = norm(pattern), f = norm(filePath);
91
118
  const globstarDir = '__GLOBSTAR_DIR__';
@@ -125,6 +152,12 @@ function removeEmptyDirs(dir) {
125
152
  }
126
153
  }
127
154
 
155
+ function resolveVersionFromTemplateRoot(tplRoot) {
156
+ const pkgPath = path.join(path.dirname(tplRoot), 'package.json');
157
+ const pkg = JSON.parse(fs.readFileSync(pkgPath, 'utf8'));
158
+ return 'v' + pkg.version;
159
+ }
160
+
128
161
  function parseSkillFrontmatter(filePath) {
129
162
  const content = fs.readFileSync(filePath, 'utf8');
130
163
  const match = content.match(/^---\r?\n([\s\S]*?)\r?\n---(?:\r?\n|$)/);
@@ -192,17 +225,26 @@ function detectCustomSkills(projectRoot, templateSkillNames) {
192
225
  .sort((left, right) => left.dirName.localeCompare(right.dirName));
193
226
  }
194
227
 
195
- function isCustomProtected(targetPath, customSkills, project) {
228
+ function isCustomProtected(targetPath, customSkills, project, customTUICommandTargets) {
196
229
  const normalized = norm(targetPath);
197
230
 
198
231
  return customSkills.some(({ dirName }) => (
199
232
  normalized.startsWith(`.agents/skills/${dirName}/`) ||
200
233
  normalized === `.claude/commands/${dirName}.md` ||
201
234
  normalized === `.opencode/commands/${dirName}.md` ||
202
- normalized === '.gemini/commands/' + project + '/' + dirName + '.toml'
235
+ normalized === '.gemini/commands/' + project + '/' + dirName + '.toml' ||
236
+ customTUICommandTargets.has(normalized)
203
237
  ));
204
238
  }
205
239
 
240
+ function recordCustomTUISkipped(report, entry) {
241
+ report?.custom?.customTUIs?.skipped?.push(entry);
242
+ }
243
+
244
+ function recordCustomTUISkippedRef(report, entry) {
245
+ report?.custom?.customTUIs?.skippedRefs?.push(entry);
246
+ }
247
+
206
248
  function expandHome(inputPath) {
207
249
  if (inputPath === '~') return os.homedir();
208
250
  if (inputPath.startsWith('~/')) {
@@ -212,6 +254,81 @@ function expandHome(inputPath) {
212
254
  return path.resolve(inputPath);
213
255
  }
214
256
 
257
+ function mergeTemplateSources(baseRoot, sources, report) {
258
+ const sourceMap = new Map();
259
+ const sourceMeta = new Map();
260
+ const conflictsByRel = new Map();
261
+ const baseRels = walkDir(baseRoot).map((filePath) => norm(path.relative(baseRoot, filePath)));
262
+
263
+ for (const rel of baseRels) {
264
+ sourceMap.set(rel, baseRoot);
265
+ sourceMeta.set(rel, { type: 'builtin' });
266
+ }
267
+
268
+ const recordConflict = (rel, winner, ignored) => {
269
+ const existing = conflictsByRel.get(rel);
270
+ if (existing) {
271
+ existing.winner = winner;
272
+ existing.ignored.push(...ignored);
273
+ return;
274
+ }
275
+
276
+ const conflict = { rel, winner, ignored: [...ignored] };
277
+ conflictsByRel.set(rel, conflict);
278
+ report.templateSources.conflicts.push(conflict);
279
+ };
280
+
281
+ const templateSources = Array.isArray(sources) ? sources : [];
282
+ for (const [index, source] of templateSources.entries()) {
283
+ if (source?.type !== 'local') continue;
284
+ if (typeof source.path !== 'string' || source.path.trim() === '') {
285
+ report.templateSources.errors.push({
286
+ index,
287
+ type: String(source?.type || ''),
288
+ path: String(source?.path || ''),
289
+ reason: 'invalid path'
290
+ });
291
+ continue;
292
+ }
293
+
294
+ const srcDir = expandHome(source.path);
295
+ if (!fs.existsSync(srcDir) || !fs.statSync(srcDir).isDirectory()) {
296
+ report.templateSources.errors.push({
297
+ index,
298
+ type: source.type,
299
+ path: source.path,
300
+ reason: 'directory not found'
301
+ });
302
+ continue;
303
+ }
304
+
305
+ const extRels = walkDir(srcDir).map((filePath) => norm(path.relative(srcDir, filePath)));
306
+ const sourceInfo = { type: source.type, path: source.path };
307
+ for (const rel of extRels) {
308
+ const existing = sourceMeta.get(rel);
309
+ if (existing?.type === 'builtin') {
310
+ recordConflict(rel, existing, [sourceInfo]);
311
+ continue;
312
+ }
313
+
314
+ if (existing) {
315
+ recordConflict(rel, sourceInfo, [existing]);
316
+ }
317
+
318
+ sourceMap.set(rel, srcDir);
319
+ sourceMeta.set(rel, sourceInfo);
320
+ }
321
+
322
+ report.templateSources.loaded += 1;
323
+ report.templateSources.files += extRels.length;
324
+ }
325
+
326
+ return {
327
+ mergedRels: [...sourceMap.keys()],
328
+ sourceMap
329
+ };
330
+ }
331
+
215
332
  function writeIfChanged(projectRoot, targetPath, content, reportBucket) {
216
333
  const fullPath = path.join(projectRoot, targetPath);
217
334
  const exists = fs.existsSync(fullPath);
@@ -388,7 +505,154 @@ function generateOpenCodeCommand(skill, lang) {
388
505
  return `${lines.join('\n')}\n`;
389
506
  }
390
507
 
391
- function generateCustomCommands(projectRoot, customSkills, project, lang, report) {
508
+ function validateCustomTUIs(projectRoot, customTUIs, report) {
509
+ const tools = Array.isArray(customTUIs) ? customTUIs : [];
510
+ return tools
511
+ .map((tool, index) => {
512
+ if (typeof tool?.dir !== 'string' || tool.dir.trim() === '') {
513
+ recordCustomTUISkipped(report, {
514
+ index,
515
+ name: String(tool?.name || ''),
516
+ dir: String(tool?.dir || ''),
517
+ reason: 'invalid dir'
518
+ });
519
+ return null;
520
+ }
521
+
522
+ if (!isInsideProject(projectRoot, tool.dir)) {
523
+ recordCustomTUISkipped(report, {
524
+ index,
525
+ name: String(tool?.name || ''),
526
+ dir: tool.dir,
527
+ reason: 'dir must be a relative path inside the project root'
528
+ });
529
+ return null;
530
+ }
531
+
532
+ return { ...tool, index, dir: normDir(tool.dir) };
533
+ })
534
+ .filter(Boolean);
535
+ }
536
+
537
+ function customTUITargetPath(tool, refFile, refSkillName, skillName) {
538
+ const targetFile = refFile.includes(refSkillName)
539
+ ? refFile.replaceAll(refSkillName, skillName)
540
+ : `${skillName}${path.extname(refFile)}`;
541
+ return norm(path.join(tool.dir, targetFile));
542
+ }
543
+
544
+ function findCustomTUIReference(projectRoot, tool, templateSkillNames, report, logSkipped = false) {
545
+ const cmdDir = path.join(projectRoot, tool.dir);
546
+ if (!fs.existsSync(cmdDir) || !fs.statSync(cmdDir).isDirectory()) {
547
+ if (logSkipped) {
548
+ recordCustomTUISkipped(report, {
549
+ index: tool.index,
550
+ name: String(tool.name || ''),
551
+ dir: tool.dir,
552
+ reason: 'directory not found'
553
+ });
554
+ }
555
+ return null;
556
+ }
557
+
558
+ const cmdFiles = fs.readdirSync(cmdDir)
559
+ .filter((file) => fs.statSync(path.join(cmdDir, file)).isFile())
560
+ .sort((left, right) => left.localeCompare(right));
561
+ if (cmdFiles.length === 0) {
562
+ if (logSkipped) {
563
+ recordCustomTUISkipped(report, {
564
+ index: tool.index,
565
+ name: String(tool.name || ''),
566
+ dir: tool.dir,
567
+ reason: 'no command files'
568
+ });
569
+ }
570
+ return null;
571
+ }
572
+
573
+ let sawKnownSkillReference = false;
574
+
575
+ for (const file of cmdFiles) {
576
+ const content = fs.readFileSync(path.join(cmdDir, file), 'utf8');
577
+ const match = content.match(/\.agents\/skills\/([^/]+)\/SKILL\.md/);
578
+ if (!match) continue;
579
+
580
+ const skillName = match[1];
581
+ if (!templateSkillNames.has(skillName)) continue;
582
+
583
+ const skillMd = path.join(projectRoot, '.agents/skills', skillName, 'SKILL.md');
584
+ if (!fs.existsSync(skillMd)) continue;
585
+
586
+ const meta = parseSkillFrontmatter(skillMd);
587
+ if (!meta.description) continue;
588
+
589
+ sawKnownSkillReference = true;
590
+ if (!content.includes(meta.description)) {
591
+ if (logSkipped) {
592
+ recordCustomTUISkippedRef(report, {
593
+ index: tool.index,
594
+ name: String(tool.name || ''),
595
+ dir: tool.dir,
596
+ file,
597
+ skill: skillName,
598
+ reason: 'description not found in reference command file'
599
+ });
600
+ }
601
+ continue;
602
+ }
603
+
604
+ return { content, file, skillName, skillDesc: meta.description };
605
+ }
606
+
607
+ if (logSkipped) {
608
+ recordCustomTUISkipped(report, {
609
+ index: tool.index,
610
+ name: String(tool.name || ''),
611
+ dir: tool.dir,
612
+ reason: sawKnownSkillReference
613
+ ? 'no reference command file with matching description'
614
+ : 'no usable reference command file'
615
+ });
616
+ }
617
+
618
+ return null;
619
+ }
620
+
621
+ function buildCustomTUICommandTargets(projectRoot, customSkills, customTUIs, templateSkillNames) {
622
+ const targets = new Set();
623
+ for (const tool of customTUIs) {
624
+ const ref = findCustomTUIReference(projectRoot, tool, templateSkillNames, null, false);
625
+ if (!ref) continue;
626
+
627
+ for (const skill of customSkills) {
628
+ targets.add(customTUITargetPath(tool, ref.file, ref.skillName, skill.dirName));
629
+ }
630
+ }
631
+
632
+ return targets;
633
+ }
634
+
635
+ function learnAndGenerateCommands(projectRoot, customSkills, tool, templateSkillNames, report) {
636
+ const ref = findCustomTUIReference(projectRoot, tool, templateSkillNames, report, true);
637
+ if (!ref) return;
638
+
639
+ for (const skill of customSkills) {
640
+ const descToken = '__AGENT_INFRA_CUSTOM_SKILL_DESCRIPTION__';
641
+ const generated = ref.content
642
+ .replaceAll(ref.skillDesc, descToken)
643
+ .replaceAll(ref.skillName, skill.dirName)
644
+ .replaceAll(descToken, skill.description);
645
+
646
+ writeIfChanged(
647
+ projectRoot,
648
+ customTUITargetPath(tool, ref.file, ref.skillName, skill.dirName),
649
+ generated,
650
+ report.custom.commands
651
+ );
652
+ }
653
+ }
654
+
655
+ function generateCustomCommands(projectRoot, customSkills, project, lang, report, customTUIs, templateSkillNames) {
392
656
  for (const skill of customSkills) {
393
657
  writeIfChanged(
394
658
  projectRoot,
@@ -409,6 +673,11 @@ function generateCustomCommands(projectRoot, customSkills, project, lang, report
409
673
  report.custom.commands
410
674
  );
411
675
  }
676
+
677
+ const tools = Array.isArray(customTUIs) ? customTUIs : [];
678
+ for (const tool of tools) {
679
+ learnAndGenerateCommands(projectRoot, customSkills, tool, templateSkillNames, report);
680
+ }
412
681
  }
413
682
 
414
683
  function matchesAny(rel, patterns) {
@@ -693,11 +962,12 @@ function syncTemplates(projectRoot, templateRootOverride) {
693
962
  };
694
963
  }
695
964
  }
696
- const version = INSTALLER_VERSION;
965
+ const version = resolveVersionFromTemplateRoot(templateRoot);
697
966
  const hadTemplateSource = Object.prototype.hasOwnProperty.call(cfg, 'templateSource');
698
967
 
699
968
  const { project, org, language: lang = 'en' } = cfg;
700
969
  const platformType = cfg.platform?.type || DEFAULTS.platform.type;
970
+ const customTUIsConfig = Array.isArray(cfg.customTUIs) ? cfg.customTUIs : [];
701
971
  const vars = { project, org };
702
972
  const templateSkillNames = listTemplateSkillNames(templateRoot);
703
973
  const protectedCustomSkills = detectCustomSkills(projectRoot, templateSkillNames);
@@ -710,7 +980,14 @@ function syncTemplates(projectRoot, templateRootOverride) {
710
980
  templateVersion: version,
711
981
  templateRoot: norm(templateRoot),
712
982
  registryAdded: [],
713
- managed: { written: [], created: [], unchanged: [], skippedMerged: [], removed: [] },
983
+ templateSources: {
984
+ configured: 0,
985
+ loaded: 0,
986
+ files: 0,
987
+ errors: [],
988
+ conflicts: []
989
+ },
990
+ managed: { written: [], created: [], unchanged: [], skippedMerged: [], skippedPlatform: [], removed: [] },
714
991
  custom: {
715
992
  detected: [],
716
993
  generated: [],
@@ -718,6 +995,7 @@ function syncTemplates(projectRoot, templateRootOverride) {
718
995
  unchanged: [],
719
996
  removed: [],
720
997
  sourceErrors: [],
998
+ customTUIs: { skipped: [], skippedRefs: [] },
721
999
  commands: { generated: [], updated: [], unchanged: [] }
722
1000
  },
723
1001
  ejected: { created: [], skipped: [] },
@@ -725,28 +1003,71 @@ function syncTemplates(projectRoot, templateRootOverride) {
725
1003
  configUpdated: false,
726
1004
  selfUpdate: false
727
1005
  };
1006
+ const customTUIs = validateCustomTUIs(projectRoot, customTUIsConfig, report);
1007
+ const customTUICommandTargets = buildCustomTUICommandTargets(
1008
+ projectRoot,
1009
+ protectedCustomSkills,
1010
+ customTUIs,
1011
+ templateSkillNames
1012
+ );
728
1013
 
729
1014
  const known = new Set([...managed, ...merged, ...ejected]);
730
1015
  for (const e of (DEFAULTS.files.managed || [])) {
1016
+ if (isPathOwnedByOtherPlatform(e, platformType)) continue;
731
1017
  if (!known.has(e)) { managed.push(e); known.add(e); report.registryAdded.push({ entry: e, list: 'managed' }); }
732
1018
  }
733
1019
  for (const e of (DEFAULTS.files.merged || [])) {
1020
+ if (isPathOwnedByOtherPlatform(e, platformType)) continue;
734
1021
  if (!known.has(e)) { merged.push(e); known.add(e); report.registryAdded.push({ entry: e, list: 'merged' }); }
735
1022
  }
736
1023
 
737
- const allRels = walkDir(templateRoot).map(f => norm(path.relative(templateRoot, f)));
1024
+ const templateSources = Array.isArray(cfg.templates?.sources) ? cfg.templates.sources : [];
1025
+ report.templateSources.configured = templateSources.length;
1026
+ const { mergedRels, sourceMap } = mergeTemplateSources(templateRoot, templateSources, report);
1027
+ const allRels = mergedRels;
738
1028
  const allSet = new Set(allRels);
1029
+
1030
+ for (const entry of [...managed, ...merged, ...ejected]) {
1031
+ if (!isPathOwnedByOtherPlatform(entry, platformType)) continue;
1032
+
1033
+ if (entry.endsWith('/')) {
1034
+ const dir = path.join(projectRoot, entry);
1035
+ if (!fs.existsSync(dir)) continue;
1036
+
1037
+ for (const filePath of walkDir(dir)) {
1038
+ fs.unlinkSync(filePath);
1039
+ report.managed.removed.push(norm(path.relative(projectRoot, filePath)));
1040
+ }
1041
+ removeEmptyDirs(dir);
1042
+ continue;
1043
+ }
1044
+
1045
+ const target = path.join(projectRoot, renderPathname(entry, project));
1046
+ if (!fs.existsSync(target)) continue;
1047
+ fs.unlinkSync(target);
1048
+ report.managed.removed.push(norm(path.relative(projectRoot, target)));
1049
+ }
1050
+
739
1051
  for (const entry of managed) {
1052
+ if (isPathOwnedByOtherPlatform(entry, platformType)) {
1053
+ report.managed.skippedPlatform.push(entry);
1054
+ continue;
1055
+ }
1056
+
740
1057
  const isDir = entry.endsWith('/');
741
1058
  let entryRels;
742
1059
  const expectedTargets = isDir ? new Set() : null;
743
1060
 
744
1061
  if (isDir) {
745
1062
  const dir = path.join(templateRoot, entry);
746
- if (!fs.existsSync(dir)) continue;
747
- entryRels = walkDir(dir).map(f => norm(path.relative(templateRoot, f)));
1063
+ const builtinRels = fs.existsSync(dir)
1064
+ ? walkDir(dir).map((filePath) => norm(path.relative(templateRoot, filePath)))
1065
+ : [];
1066
+ const prefix = norm(entry);
1067
+ const externalRels = allRels.filter((rel) => rel.startsWith(prefix) && !builtinRels.includes(rel));
1068
+ entryRels = [...builtinRels, ...externalRels];
1069
+ if (!entryRels.length) continue;
748
1070
  } else {
749
- entryRels = [];
750
1071
  entryRels = entryVariantRels(entry, allSet, platformType);
751
1072
  if (!entryRels.length) continue;
752
1073
  }
@@ -761,7 +1082,8 @@ function syncTemplates(projectRoot, templateRootOverride) {
761
1082
  continue;
762
1083
  }
763
1084
 
764
- const srcFull = path.join(templateRoot, src);
1085
+ const srcRoot = sourceMap.get(src) || templateRoot;
1086
+ const srcFull = path.join(srcRoot, src);
765
1087
  const dstFull = path.join(projectRoot, tgt);
766
1088
  const bin = isBinary(srcFull);
767
1089
  const content = bin
@@ -795,7 +1117,7 @@ function syncTemplates(projectRoot, templateRootOverride) {
795
1117
  for (const projFile of projFiles) {
796
1118
  if (expectedTargets.has(projFile)) continue;
797
1119
  if (projFile === configPathRel) continue;
798
- if (isCustomProtected(projFile, protectedCustomSkills, project)) continue;
1120
+ if (isCustomProtected(projFile, protectedCustomSkills, project, customTUICommandTargets)) continue;
799
1121
  if (matchesAny(projFile, merged) || matchesAny(projFile, ejected)) continue;
800
1122
 
801
1123
  fs.unlinkSync(path.join(projectRoot, projFile));
@@ -816,7 +1138,7 @@ function syncTemplates(projectRoot, templateRootOverride) {
816
1138
 
817
1139
  const customSkills = detectCustomSkills(projectRoot, templateSkillNames);
818
1140
  report.custom.detected = customSkills.map((skill) => skill.dirName);
819
- generateCustomCommands(projectRoot, customSkills, project, lang, report);
1141
+ generateCustomCommands(projectRoot, customSkills, project, lang, report, customTUIs, templateSkillNames);
820
1142
 
821
1143
  for (const entry of ejected) {
822
1144
  const dstFull = path.join(projectRoot, entry);
@@ -830,7 +1152,8 @@ function syncTemplates(projectRoot, templateRootOverride) {
830
1152
  const src = selected.get(target);
831
1153
  if (!src) continue;
832
1154
 
833
- const content = renderContent(fs.readFileSync(path.join(templateRoot, src), 'utf8'), vars);
1155
+ const srcRoot = sourceMap.get(src) || templateRoot;
1156
+ const content = renderContent(fs.readFileSync(path.join(srcRoot, src), 'utf8'), vars);
834
1157
  const dir = path.dirname(dstFull);
835
1158
  if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true });
836
1159
  fs.writeFileSync(dstFull, content);
@@ -839,6 +1162,11 @@ function syncTemplates(projectRoot, templateRootOverride) {
839
1162
 
840
1163
  const mergedMap = new Map();
841
1164
  for (const entry of merged) {
1165
+ if (isPathOwnedByOtherPlatform(entry, platformType)) {
1166
+ report.managed.skippedPlatform.push(entry);
1167
+ continue;
1168
+ }
1169
+
842
1170
  if (entry.includes('*')) {
843
1171
  const hits = allRels.filter(r => {
844
1172
  const t = norm(renderPathname(stripLangVariant(r), project));
@@ -61,7 +61,7 @@ Report:
61
61
 
62
62
  Suggest next step:
63
63
 
64
- > **IMPORTANT**: All TUI command formats listed below must be output in full. Do not show only the format for the current AI agent.
64
+ > **IMPORTANT**: All TUI command formats listed below must be output in full. Do not show only the format for the current AI agent. If `.agents/.airc.json` configures custom TUIs (via `customTUIs`), read each tool's `name` and `invoke`, then add the matching command line in the same format (`${skillName}` becomes the skill name and `${projectName}` becomes the project name).
65
65
 
66
66
  ```
67
67
  Next step - commit changes:
@@ -61,7 +61,7 @@ description: "升级项目依赖到新版本并验证"
61
61
 
62
62
  建议下一步:
63
63
 
64
- > **重要**:以下「下一步」中列出的所有 TUI 命令格式必须完整输出,不要只展示当前 AI 代理对应的格式。
64
+ > **重要**:以下「下一步」中列出的所有 TUI 命令格式必须完整输出,不要只展示当前 AI 代理对应的格式。如果 `.agents/.airc.json` 中配置了自定义 TUI(`customTUIs`),读取每个工具的 `name` 和 `invoke`,按同样格式补充对应命令行(`${skillName}` 替换为技能名,`${projectName}` 替换为项目名)。
65
65
 
66
66
  ```
67
67
  下一步 - 提交代码:
@@ -1,13 +1,13 @@
1
1
  ---
2
2
  id: task-XXX
3
3
  type: feature # feature | bugfix | refactor | docs | review
4
- branch: "" # <project>-<type>-<slug>
4
+ branch: # <project>-<type>-<slug>
5
5
  workflow: feature-development # feature-development | bug-fix | code-review | refactoring
6
6
  status: open # open | in-progress | review | blocked | completed
7
7
  created_at: YYYY-MM-DDTHH:mm:ss±HH:MM
8
8
  updated_at: YYYY-MM-DDTHH:mm:ss±HH:MM
9
9
  current_step: analysis # analysis | design | implementation | review | fix | commit
10
- assigned_to: "" # claude | codex | gemini | opencode | human
10
+ assigned_to: # claude | codex | gemini | opencode | human
11
11
  ---
12
12
 
13
13
  # Task: [Title]
@@ -1,13 +1,13 @@
1
1
  ---
2
2
  id: task-XXX
3
3
  type: feature # feature | bugfix | refactor | docs | review
4
- branch: "" # <project>-<type>-<slug>
4
+ branch: # <project>-<type>-<slug>
5
5
  workflow: feature-development # feature-development | bug-fix | code-review | refactoring
6
6
  status: open # open | in-progress | review | blocked | completed
7
7
  created_at: YYYY-MM-DDTHH:mm:ss±HH:MM
8
8
  updated_at: YYYY-MM-DDTHH:mm:ss±HH:MM
9
9
  current_step: analysis # analysis | design | implementation | review | fix | commit
10
- assigned_to: "" # claude | codex | gemini | opencode | human
10
+ assigned_to: # claude | codex | gemini | opencode | human
11
11
  ---
12
12
 
13
13
  # 任务:[标题]
@@ -1,5 +1,5 @@
1
1
  ---
2
- description: "Create a GitHub Issue from a task file"
2
+ description: "Create an Issue from a task file"
3
3
  usage: "/create-issue <task-id>"
4
4
  ---
5
5
 
@@ -1,5 +1,5 @@
1
1
  ---
2
- description: "从任务文件创建 GitHub Issue"
2
+ description: "从任务文件创建 Issue"
3
3
  usage: "/create-issue <task-id>"
4
4
  ---
5
5
 
@@ -1,5 +1,5 @@
1
1
  ---
2
- description: "Import a GitHub Issue and create a task"
2
+ description: "Import an Issue and create a task"
3
3
  usage: "/import-issue <issue-number>"
4
4
  ---
5
5
 
@@ -1,5 +1,5 @@
1
1
  ---
2
- description: "从 GitHub Issue 导入并创建任务"
2
+ description: "从 Issue 导入并创建任务"
3
3
  usage: "/import-issue <issue-number>"
4
4
  ---
5
5
 
@@ -1,5 +1,5 @@
1
1
  ---
2
- description: "Initialize the repository's standard GitHub Labels taxonomy"
2
+ description: "Initialize the repository's standard labels taxonomy"
3
3
  disable-model-invocation: true
4
4
  ---
5
5
 
@@ -1,5 +1,5 @@
1
1
  ---
2
- description: "初始化仓库的 GitHub Labels 体系"
2
+ description: "初始化仓库的 labels 体系"
3
3
  disable-model-invocation: true
4
4
  ---
5
5
 
@@ -1,5 +1,5 @@
1
1
  ---
2
- description: "Initialize the repository's standard GitHub Milestones taxonomy"
2
+ description: "Initialize the repository's standard milestones taxonomy"
3
3
  usage: "/init-milestones [--history]"
4
4
  disable-model-invocation: true
5
5
  ---
@@ -1,5 +1,5 @@
1
1
  ---
2
- description: "初始化仓库的 GitHub Milestones 体系"
2
+ description: "初始化仓库的 milestones 体系"
3
3
  usage: "/init-milestones [--history]"
4
4
  disable-model-invocation: true
5
5
  ---
@@ -0,0 +1,8 @@
1
+ ---
2
+ description: "Run post-release tasks"
3
+ disable-model-invocation: true
4
+ ---
5
+
6
+ Read and execute the post-release skill from `.agents/skills/post-release/SKILL.md`.
7
+
8
+ Follow all steps defined in the skill exactly.
@@ -0,0 +1,8 @@
1
+ ---
2
+ description: "执行版本发布后处理"
3
+ disable-model-invocation: true
4
+ ---
5
+
6
+ 读取并执行 `.agents/skills/post-release/SKILL.md` 中的 post-release 技能。
7
+
8
+ 严格按照技能中定义的所有步骤执行。
@@ -1,5 +1,5 @@
1
1
  ---
2
- description: "Restore local task files from GitHub Issue comments"
2
+ description: "Restore local task files from Issue comments"
3
3
  usage: "/restore-task <issue-number> [task-id]"
4
4
  disable-model-invocation: true
5
5
  ---
@@ -1,5 +1,5 @@
1
1
  ---
2
- description: "从 GitHub Issue 评论还原本地任务文件"
2
+ description: "从 Issue 评论还原本地任务文件"
3
3
  usage: "/restore-task <issue-number> [task-id]"
4
4
  disable-model-invocation: true
5
5
  ---