@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.
Files changed (39) hide show
  1. package/README.md +94 -1
  2. package/README.zh-CN.md +94 -1
  3. package/lib/defaults.json +1 -0
  4. package/lib/sandbox/commands/create.js +7 -3
  5. package/lib/sandbox/shell.js +47 -7
  6. package/lib/sandbox/tools.js +18 -14
  7. package/package.json +1 -1
  8. package/templates/.agents/README.en.md +52 -0
  9. package/templates/.agents/README.zh-CN.md +52 -0
  10. package/templates/.agents/rules/issue-pr-commands.github.en.md +10 -1
  11. package/templates/.agents/rules/issue-pr-commands.github.zh-CN.md +10 -1
  12. package/templates/.agents/rules/issue-sync.github.en.md +12 -10
  13. package/templates/.agents/rules/issue-sync.github.zh-CN.md +12 -10
  14. package/templates/.agents/rules/milestone-inference.github.en.md +6 -5
  15. package/templates/.agents/rules/milestone-inference.github.zh-CN.md +6 -5
  16. package/templates/.agents/scripts/platform-adapters/platform-sync.github.js +87 -14
  17. package/templates/.agents/skills/analyze-task/SKILL.en.md +1 -1
  18. package/templates/.agents/skills/analyze-task/SKILL.zh-CN.md +1 -1
  19. package/templates/.agents/skills/create-pr/config/verify.json +2 -0
  20. package/templates/.agents/skills/create-pr/reference/pr-body-template.en.md +6 -7
  21. package/templates/.agents/skills/create-pr/reference/pr-body-template.zh-CN.md +6 -7
  22. package/templates/.agents/skills/create-release-note/SKILL.en.md +27 -2
  23. package/templates/.agents/skills/create-release-note/SKILL.zh-CN.md +27 -2
  24. package/templates/.agents/skills/implement-task/SKILL.en.md +1 -1
  25. package/templates/.agents/skills/implement-task/SKILL.zh-CN.md +1 -1
  26. package/templates/.agents/skills/import-issue/SKILL.en.md +10 -2
  27. package/templates/.agents/skills/import-issue/SKILL.zh-CN.md +10 -2
  28. package/templates/.agents/skills/import-issue/config/verify.json +2 -1
  29. package/templates/.agents/skills/plan-task/SKILL.en.md +1 -1
  30. package/templates/.agents/skills/plan-task/SKILL.zh-CN.md +1 -1
  31. package/templates/.agents/skills/refine-task/SKILL.en.md +1 -1
  32. package/templates/.agents/skills/refine-task/SKILL.zh-CN.md +1 -1
  33. package/templates/.agents/skills/review-task/SKILL.en.md +1 -1
  34. package/templates/.agents/skills/review-task/SKILL.zh-CN.md +1 -1
  35. package/templates/.agents/skills/update-agent-infra/scripts/sync-templates.js +316 -1
  36. package/templates/.github/scripts/sync-labels-to-set.sh +110 -0
  37. package/templates/.github/workflows/metadata-sync.yml +11 -20
  38. package/templates/.github/workflows/pr-label.yml +10 -19
  39. package/templates/.github/workflows/status-label.yml +20 -34
package/README.md CHANGED
@@ -341,6 +341,92 @@ agent-infra ships with **a rich set of built-in AI skills**. They are organized
341
341
 
342
342
  > Every skill works across supported AI TUIs. The command prefix changes, but the workflow semantics stay the same.
343
343
 
344
+ <a id="custom-skills"></a>
345
+
346
+ ## Custom Skills
347
+
348
+ Built-in skills cover the standard delivery lifecycle, but teams often need project-specific instructions such as coding standards, deployment checks, or internal review rules. agent-infra supports that through **custom skills**.
349
+
350
+ ### Create a custom skill in the project
351
+
352
+ Create a directory under `.agents/skills/<name>/` and add a `SKILL.md` file:
353
+
354
+ ```text
355
+ .agents/skills/
356
+ enforce-style/
357
+ SKILL.md
358
+ reference/
359
+ style-guide.md
360
+ ```
361
+
362
+ Minimum frontmatter:
363
+
364
+ ```yaml
365
+ ---
366
+ name: enforce-style
367
+ description: "Apply team style checks before submitting code"
368
+ args: "<task-id>" # optional
369
+ ---
370
+ ```
371
+
372
+ - `name`: user-facing skill name
373
+ - `description`: used when generating editor command metadata
374
+ - `args`: optional argument hint; agent-infra uses it when generating slash commands for supported AI TUIs
375
+
376
+ After adding the skill, run `update-agent-infra` again:
377
+
378
+ | TUI | Command |
379
+ |-----|---------|
380
+ | Claude Code | `/update-agent-infra` |
381
+ | Codex | `$update-agent-infra` |
382
+ | Gemini CLI | `/{{project}}:update-agent-infra` |
383
+ | OpenCode | `/update-agent-infra` |
384
+
385
+ That refresh detects non-built-in skill directories in `.agents/skills/` and generates matching commands for Claude Code, Gemini CLI, and OpenCode automatically.
386
+
387
+ ### Sync custom skills from shared sources
388
+
389
+ If you maintain reusable team skills outside the repository, declare them in `.agents/.airc.json`:
390
+
391
+ ```json
392
+ {
393
+ "skills": {
394
+ "sources": [
395
+ { "type": "local", "path": "~/company-skills" },
396
+ { "type": "local", "path": "~/team-skills" }
397
+ ]
398
+ }
399
+ }
400
+ ```
401
+
402
+ Expected source layout:
403
+
404
+ ```text
405
+ ~/company-skills/
406
+ enforce-style/
407
+ SKILL.md
408
+ release-check/
409
+ SKILL.md
410
+ reference/
411
+ checklist.md
412
+ ```
413
+
414
+ Behavior:
415
+
416
+ - Sources are applied in list order; later sources overwrite earlier custom sources when they define the same file
417
+ - `type: "local"` is the only supported source type today; the structure leaves room for future source types
418
+ - `~` in source paths is expanded to the current user's home directory
419
+
420
+ ### Sync behavior and conflict rules
421
+
422
+ When `update-agent-infra` runs:
423
+
424
+ - Manually created custom skills in `.agents/skills/` are protected from managed-file cleanup
425
+ - Files synced from external custom sources are copied into `.agents/skills/`
426
+ - For synced skills that still exist in a configured source, files removed from the source are also removed locally during the next sync
427
+ - Built-in skills always win over custom sources; if a source defines a skill with the same name as a built-in skill, agent-infra skips that custom source skill instead of overriding the built-in one
428
+ - If you truly need to replace a built-in skill or command, use the existing `ejected` mechanism and own that file in the project
429
+
344
430
  <a id="prebuilt-workflows"></a>
345
431
 
346
432
  ## Prebuilt Workflows
@@ -410,7 +496,12 @@ The generated `.agents/.airc.json` file is the central contract between the boot
410
496
  "project": "my-project",
411
497
  "org": "my-org",
412
498
  "language": "en",
413
- "templateVersion": "v0.5.4",
499
+ "templateVersion": "v0.5.6",
500
+ "skills": {
501
+ "sources": [
502
+ { "type": "local", "path": "~/company-skills" }
503
+ ]
504
+ },
414
505
  "files": {
415
506
  "managed": [
416
507
  ".agents/workspace/README.md",
@@ -439,6 +530,8 @@ The generated `.agents/.airc.json` file is the central contract between the boot
439
530
  | `org` | GitHub organization or owner used by generated metadata and links. |
440
531
  | `language` | Primary project language or locale used by rendered templates. |
441
532
  | `templateVersion` | Installed template version for future upgrades and drift tracking. |
533
+ | `skills` | Optional custom skill sync configuration. |
534
+ | `skills.sources` | Optional ordered list of external custom skill sources. Only `type: "local"` is supported today. |
442
535
  | `files` | Per-path update strategy configuration for managed, merged, and ejected files. |
443
536
 
444
537
  <a id="file-management-strategies"></a>
package/README.zh-CN.md CHANGED
@@ -341,6 +341,92 @@ agent-infra 提供 **丰富的内置 AI skills**。它们按使用场景分组
341
341
 
342
342
  > 所有 skills 都可跨支持的 AI TUI 复用。变化的只是命令前缀,工作流语义保持一致。
343
343
 
344
+ <a id="custom-skills"></a>
345
+
346
+ ## 自定义 Skills
347
+
348
+ 内置 skills 覆盖了标准交付生命周期,但很多团队还需要项目特有的指令,例如编码规范、发布检查或内部审查规则。agent-infra 通过**自定义 skill**支持这些场景。
349
+
350
+ ### 在项目中创建自定义 skill
351
+
352
+ 在 `.agents/skills/<name>/` 下创建目录,并添加 `SKILL.md`:
353
+
354
+ ```text
355
+ .agents/skills/
356
+ enforce-style/
357
+ SKILL.md
358
+ reference/
359
+ style-guide.md
360
+ ```
361
+
362
+ 最小 frontmatter 示例:
363
+
364
+ ```yaml
365
+ ---
366
+ name: enforce-style
367
+ description: "在提交代码前执行团队风格检查"
368
+ args: "<task-id>" # 可选
369
+ ---
370
+ ```
371
+
372
+ - `name`:对用户可见的 skill 名称
373
+ - `description`:用于生成编辑器命令元数据
374
+ - `args`:可选参数提示;agent-infra 会在生成支持的 AI TUI 命令时使用它
375
+
376
+ 添加 skill 后,再执行一次 `update-agent-infra`:
377
+
378
+ | TUI | 命令 |
379
+ |-----|------|
380
+ | Claude Code | `/update-agent-infra` |
381
+ | Codex | `$update-agent-infra` |
382
+ | Gemini CLI | `/{{project}}:update-agent-infra` |
383
+ | OpenCode | `/update-agent-infra` |
384
+
385
+ 同步时会自动检测 `.agents/skills/` 下的非内置 skill 目录,并为 Claude Code、Gemini CLI、OpenCode 生成对应命令。
386
+
387
+ ### 从共享源同步自定义 skills
388
+
389
+ 如果团队在仓库外统一维护可复用 skill,可以在 `.agents/.airc.json` 中声明:
390
+
391
+ ```json
392
+ {
393
+ "skills": {
394
+ "sources": [
395
+ { "type": "local", "path": "~/company-skills" },
396
+ { "type": "local", "path": "~/team-skills" }
397
+ ]
398
+ }
399
+ }
400
+ ```
401
+
402
+ 源目录结构示例:
403
+
404
+ ```text
405
+ ~/company-skills/
406
+ enforce-style/
407
+ SKILL.md
408
+ release-check/
409
+ SKILL.md
410
+ reference/
411
+ checklist.md
412
+ ```
413
+
414
+ 行为说明:
415
+
416
+ - 多个 source 按数组顺序应用;后面的 source 如果定义了同名文件,会覆盖前面的自定义 source 文件
417
+ - 当前只支持 `type: "local"`;配置结构已为未来扩展其他来源类型预留
418
+ - source 路径中的 `~` 会自动展开为当前用户的 home 目录
419
+
420
+ ### 同步行为与冲突规则
421
+
422
+ 执行 `update-agent-infra` 时:
423
+
424
+ - 手动放在 `.agents/skills/` 下的自定义 skill 不会被 managed 文件清理删除
425
+ - 外部 source 中的 skill 会同步复制到 `.agents/skills/`
426
+ - 对于仍存在于配置 source 中的 skill,如果源里删掉某个文件,下次同步时本地对应残留文件也会被删除
427
+ - 内置 skill 始终优先于自定义 source;如果 source 里出现与内置 skill 同名的目录,agent-infra 会跳过该 source skill,而不是覆盖内置实现
428
+ - 如果你确实需要替换内置 skill 或命令,请使用现有的 `ejected` 机制,让项目自己接管该文件
429
+
344
430
  <a id="prebuilt-workflows"></a>
345
431
 
346
432
  ## 预置工作流
@@ -410,7 +496,12 @@ import-issue #42 从 GitHub Issue 导入任务
410
496
  "project": "my-project",
411
497
  "org": "my-org",
412
498
  "language": "en",
413
- "templateVersion": "v0.5.4",
499
+ "templateVersion": "v0.5.6",
500
+ "skills": {
501
+ "sources": [
502
+ { "type": "local", "path": "~/company-skills" }
503
+ ]
504
+ },
414
505
  "files": {
415
506
  "managed": [
416
507
  ".agents/workspace/README.md",
@@ -439,6 +530,8 @@ import-issue #42 从 GitHub Issue 导入任务
439
530
  | `org` | 生成元数据和链接时使用的 GitHub 组织或拥有者。 |
440
531
  | `language` | 渲染模板时采用的项目主语言或区域设置。 |
441
532
  | `templateVersion` | 当前安装的模板版本,用于升级和差异追踪。 |
533
+ | `skills` | 可选的自定义 skill 同步配置。 |
534
+ | `skills.sources` | 可选的外部自定义 skill 源列表,按顺序应用。当前仅支持 `type: "local"`。 |
442
535
  | `files` | 针对具体路径配置 `managed`、`merged`、`ejected` 三类更新策略。 |
443
536
 
444
537
  <a id="file-management-strategies"></a>
package/lib/defaults.json CHANGED
@@ -36,6 +36,7 @@
36
36
  ".claude/hooks/",
37
37
  ".gemini/commands/",
38
38
  ".github/hooks/check-version-format.sh",
39
+ ".github/scripts/",
39
40
  ".opencode/commands/"
40
41
  ],
41
42
  "merged": [
@@ -63,6 +63,10 @@ function buildSignature(preparedDockerfile, tools) {
63
63
  .slice(0, 12);
64
64
  }
65
65
 
66
+ function hostJoin(basePath, ...segments) {
67
+ return basePath.startsWith('/') ? path.posix.join(basePath, ...segments) : path.join(basePath, ...segments);
68
+ }
69
+
66
70
  function resolveToolDirs(config, tools, branch) {
67
71
  return tools.map((tool) => {
68
72
  const candidates = toolConfigDirCandidates(tool, config.project, branch);
@@ -266,7 +270,7 @@ export function prepareHostShellConfig({ home, project, branch, repoRoot }) {
266
270
  }
267
271
 
268
272
  function gpgCacheDir(home, project) {
269
- return path.join(home, '.agent-infra', 'gpg-cache', project);
273
+ return hostJoin(home, '.agent-infra', 'gpg-cache', project);
270
274
  }
271
275
 
272
276
  function normalizeSigningKey(signingKey) {
@@ -649,11 +653,11 @@ export function extractClaudeCredentialsBlob(home, execFn = execFileSync) {
649
653
  }
650
654
 
651
655
  export function claudeCredentialsDir(home, project) {
652
- return path.join(home, '.agent-infra', 'credentials', project, 'claude-code');
656
+ return hostJoin(home, '.agent-infra', 'credentials', project, 'claude-code');
653
657
  }
654
658
 
655
659
  export function claudeCredentialsPath(home, project) {
656
- return path.join(claudeCredentialsDir(home, project), '.credentials.json');
660
+ return hostJoin(claudeCredentialsDir(home, project), '.credentials.json');
657
661
  }
658
662
 
659
663
  export function writeClaudeCredentialsFile(home, project, blob) {
@@ -1,4 +1,6 @@
1
1
  import { execFileSync, spawnSync } from 'node:child_process';
2
+ import fs from 'node:fs';
3
+ import path from 'node:path';
2
4
 
3
5
  const DEFAULT_TIMEOUT_MS = 60 * 60 * 1000;
4
6
 
@@ -11,25 +13,62 @@ function normalizeOptions(opts = {}, stdio) {
11
13
  };
12
14
  }
13
15
 
16
+ function resolveCommand(cmd) {
17
+ if (process.platform !== 'win32' || path.extname(cmd)) {
18
+ return cmd;
19
+ }
20
+
21
+ const pathValue = process.env.Path || process.env.PATH || '';
22
+ const extensions = (process.env.PATHEXT || '.COM;.EXE;.BAT;.CMD')
23
+ .split(';')
24
+ .filter(Boolean);
25
+
26
+ for (const dir of pathValue.split(path.delimiter).filter(Boolean)) {
27
+ for (const extension of extensions) {
28
+ const candidate = path.join(dir, `${cmd}${extension.toLowerCase()}`);
29
+ if (fs.existsSync(candidate)) {
30
+ return candidate;
31
+ }
32
+ const upperCandidate = path.join(dir, `${cmd}${extension.toUpperCase()}`);
33
+ if (fs.existsSync(upperCandidate)) {
34
+ return upperCandidate;
35
+ }
36
+ }
37
+ }
38
+
39
+ return cmd;
40
+ }
41
+
42
+ function commandOptions(cmd, opts) {
43
+ if (process.platform === 'win32' && /\.(?:bat|cmd)$/i.test(cmd)) {
44
+ return { ...opts, shell: true };
45
+ }
46
+ return opts;
47
+ }
48
+
14
49
  export function run(cmd, args, opts = {}) {
15
- return execFileSync(cmd, args, {
50
+ const resolved = resolveCommand(cmd);
51
+ return execFileSync(resolved, args, commandOptions(resolved, {
16
52
  ...normalizeOptions(opts, ['pipe', 'pipe', 'pipe']),
17
53
  encoding: 'utf8'
18
- }).trim();
54
+ })).trim();
19
55
  }
20
56
 
21
57
  export function runOk(cmd, args, opts = {}) {
22
- const result = spawnSync(cmd, args, normalizeOptions(opts, 'pipe'));
58
+ const resolved = resolveCommand(cmd);
59
+ const result = spawnSync(resolved, args, commandOptions(resolved, normalizeOptions(opts, 'pipe')));
23
60
  return result.status === 0;
24
61
  }
25
62
 
26
63
  export function runInteractive(cmd, args, opts = {}) {
27
- const result = spawnSync(cmd, args, normalizeOptions(opts, 'inherit'));
64
+ const resolved = resolveCommand(cmd);
65
+ const result = spawnSync(resolved, args, commandOptions(resolved, normalizeOptions(opts, 'inherit')));
28
66
  return result.status ?? 1;
29
67
  }
30
68
 
31
69
  export function runVerbose(cmd, args, opts = {}) {
32
- const result = spawnSync(cmd, args, normalizeOptions(opts, 'inherit'));
70
+ const resolved = resolveCommand(cmd);
71
+ const result = spawnSync(resolved, args, commandOptions(resolved, normalizeOptions(opts, 'inherit')));
33
72
 
34
73
  if (result.status !== 0) {
35
74
  if (result.signal === 'SIGTERM') {
@@ -40,9 +79,10 @@ export function runVerbose(cmd, args, opts = {}) {
40
79
  }
41
80
 
42
81
  export function runSafe(cmd, args, opts = {}) {
43
- const result = spawnSync(cmd, args, {
82
+ const resolved = resolveCommand(cmd);
83
+ const result = spawnSync(resolved, args, commandOptions(resolved, {
44
84
  ...normalizeOptions(opts, ['pipe', 'pipe', 'pipe']),
45
85
  encoding: 'utf8',
46
- });
86
+ }));
47
87
  return (result.stdout ?? '').trim();
48
88
  }
@@ -18,6 +18,10 @@ import { safeNameCandidates, sanitizeBranchName } from './constants.js';
18
18
  * @property {string[]=} postSetupCmds
19
19
  */
20
20
 
21
+ function hostJoin(basePath, ...segments) {
22
+ return basePath.startsWith('/') ? path.posix.join(basePath, ...segments) : path.join(basePath, ...segments);
23
+ }
24
+
21
25
  function createBuiltinTools(home, project) {
22
26
  /** @type {Record<string, SandboxTool>} */
23
27
  return {
@@ -25,7 +29,7 @@ function createBuiltinTools(home, project) {
25
29
  id: 'claude-code',
26
30
  name: 'Claude Code',
27
31
  npmPackage: '@anthropic-ai/claude-code',
28
- sandboxBase: path.join(home, '.agent-infra', 'sandboxes', 'claude-code'),
32
+ sandboxBase: hostJoin(home, '.agent-infra', 'sandboxes', 'claude-code'),
29
33
  containerMount: '/home/devuser/.claude',
30
34
  versionCmd: 'claude --version',
31
35
  setupHint: 'Authenticates via host credentials live-mounted at ~/.claude/.credentials.json',
@@ -38,7 +42,7 @@ function createBuiltinTools(home, project) {
38
42
  // letting ensureClaudeOnboarding actually take effect.
39
43
  envVars: { CLAUDE_CONFIG_DIR: '/home/devuser/.claude' },
40
44
  hostPreSeedDirs: [
41
- { hostDir: path.join(home, '.claude', 'plugins'), sandboxSubdir: 'plugins' }
45
+ { hostDir: hostJoin(home, '.claude', 'plugins'), sandboxSubdir: 'plugins' }
42
46
  ],
43
47
  pathRewriteFiles: [
44
48
  'plugins/installed_plugins.json',
@@ -46,7 +50,7 @@ function createBuiltinTools(home, project) {
46
50
  ],
47
51
  hostLiveMounts: [
48
52
  {
49
- hostPath: path.join(home, '.agent-infra', 'credentials', project, 'claude-code', '.credentials.json'),
53
+ hostPath: hostJoin(home, '.agent-infra', 'credentials', project, 'claude-code', '.credentials.json'),
50
54
  containerSubpath: '.credentials.json'
51
55
  }
52
56
  ]
@@ -55,12 +59,12 @@ function createBuiltinTools(home, project) {
55
59
  id: 'codex',
56
60
  name: 'Codex',
57
61
  npmPackage: '@openai/codex',
58
- sandboxBase: path.join(home, '.agent-infra', 'sandboxes', 'codex'),
62
+ sandboxBase: hostJoin(home, '.agent-infra', 'sandboxes', 'codex'),
59
63
  containerMount: '/home/devuser/.codex',
60
64
  versionCmd: 'codex --version',
61
65
  setupHint: 'Run codex once inside the container and choose Device Code login if needed.',
62
66
  hostLiveMounts: [
63
- { hostPath: path.join(home, '.codex', 'auth.json'), containerSubpath: 'auth.json' }
67
+ { hostPath: hostJoin(home, '.codex', 'auth.json'), containerSubpath: 'auth.json' }
64
68
  ],
65
69
  postSetupCmds: [
66
70
  'test -d /workspace/.codex/commands && ln -sfn /workspace/.codex/commands /home/devuser/.codex/prompts || true'
@@ -70,13 +74,13 @@ function createBuiltinTools(home, project) {
70
74
  id: 'opencode',
71
75
  name: 'OpenCode',
72
76
  npmPackage: 'opencode-ai',
73
- sandboxBase: path.join(home, '.agent-infra', 'sandboxes', 'opencode'),
77
+ sandboxBase: hostJoin(home, '.agent-infra', 'sandboxes', 'opencode'),
74
78
  containerMount: '/home/devuser/.local/share/opencode',
75
79
  versionCmd: 'opencode version',
76
80
  setupHint: 'Configure OpenCode credentials inside the container before first use.',
77
81
  hostLiveMounts: [
78
82
  {
79
- hostPath: path.join(home, '.local', 'share', 'opencode', 'auth.json'),
83
+ hostPath: hostJoin(home, '.local', 'share', 'opencode', 'auth.json'),
80
84
  containerSubpath: 'auth.json'
81
85
  }
82
86
  ]
@@ -85,16 +89,16 @@ function createBuiltinTools(home, project) {
85
89
  id: 'gemini-cli',
86
90
  name: 'Gemini CLI',
87
91
  npmPackage: '@google/gemini-cli',
88
- sandboxBase: path.join(home, '.agent-infra', 'sandboxes', 'gemini-cli'),
92
+ sandboxBase: hostJoin(home, '.agent-infra', 'sandboxes', 'gemini-cli'),
89
93
  containerMount: '/home/devuser/.gemini',
90
94
  versionCmd: 'gemini --version',
91
95
  setupHint: 'Run gemini inside the container to finish authentication.',
92
96
  hostLiveMounts: [
93
- { hostPath: path.join(home, '.gemini', 'oauth_creds.json'), containerSubpath: 'oauth_creds.json' }
97
+ { hostPath: hostJoin(home, '.gemini', 'oauth_creds.json'), containerSubpath: 'oauth_creds.json' }
94
98
  ],
95
99
  hostPreSeedFiles: [
96
- { hostPath: path.join(home, '.gemini', 'settings.json'), sandboxName: 'settings.json' },
97
- { hostPath: path.join(home, '.gemini', 'google_accounts.json'), sandboxName: 'google_accounts.json' }
100
+ { hostPath: hostJoin(home, '.gemini', 'settings.json'), sandboxName: 'settings.json' },
101
+ { hostPath: hostJoin(home, '.gemini', 'google_accounts.json'), sandboxName: 'google_accounts.json' }
98
102
  ]
99
103
  }
100
104
  };
@@ -119,15 +123,15 @@ export function resolveTools(config) {
119
123
  }
120
124
 
121
125
  export function toolConfigDir(tool, project, branch) {
122
- return path.join(tool.sandboxBase, project, sanitizeBranchName(branch));
126
+ return hostJoin(tool.sandboxBase, project, sanitizeBranchName(branch));
123
127
  }
124
128
 
125
129
  export function toolConfigDirCandidates(tool, project, branch) {
126
- return safeNameCandidates(branch).map((name) => path.join(tool.sandboxBase, project, name));
130
+ return safeNameCandidates(branch).map((name) => hostJoin(tool.sandboxBase, project, name));
127
131
  }
128
132
 
129
133
  export function toolProjectDirCandidates(tool, project) {
130
- return [path.join(tool.sandboxBase, project)];
134
+ return [hostJoin(tool.sandboxBase, project)];
131
135
  }
132
136
 
133
137
  export function toolNpmPackagesArg(tools) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@fitlab-ai/agent-infra",
3
- "version": "0.5.4",
3
+ "version": "0.5.6",
4
4
  "description": "Bootstrap tool for AI multi-tool collaboration infrastructure — works with Claude Code, Codex, Gemini CLI, and OpenCode",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -120,6 +120,58 @@ To adapt agent-infra to a private code-hosting platform:
120
120
  4. If you maintain a fork of the template source, add matching `.{platform}.` template variants before adding that platform identifier to the sync logic.
121
121
  5. Validate the customized workflow on a test task before rolling it out broadly.
122
122
 
123
+ ## Custom Skills
124
+
125
+ Projects can add their own skills alongside the built-in task workflow.
126
+
127
+ ### Local project skills
128
+
129
+ Create a directory under `.agents/skills/<name>/` and add a `SKILL.md` file:
130
+
131
+ ```text
132
+ .agents/skills/
133
+ enforce-style/
134
+ SKILL.md
135
+ reference/
136
+ style-guide.md
137
+ ```
138
+
139
+ Recommended frontmatter:
140
+
141
+ ```yaml
142
+ ---
143
+ name: enforce-style
144
+ description: "Apply the team style guide before code review"
145
+ args: "<task-id>" # optional
146
+ ---
147
+ ```
148
+
149
+ After adding or updating a custom skill, run `update-agent-infra` again. The sync step detects non-built-in skills and generates matching commands for Claude Code, Gemini CLI, and OpenCode automatically.
150
+
151
+ ### Shared skill sources
152
+
153
+ To reuse centralized team skills, configure `.agents/.airc.json`:
154
+
155
+ ```json
156
+ {
157
+ "skills": {
158
+ "sources": [
159
+ { "type": "local", "path": "~/company-skills" }
160
+ ]
161
+ }
162
+ }
163
+ ```
164
+
165
+ Each source should mirror the `.agents/skills/` layout and include `SKILL.md` at the root of every skill directory.
166
+
167
+ ### Sync behavior
168
+
169
+ - Custom project skills in `.agents/skills/` are protected from managed-file cleanup
170
+ - Source entries are applied in order; later custom sources overwrite earlier custom sources
171
+ - Files deleted from an existing configured source are removed locally on the next sync for that sourced skill
172
+ - Built-in skills are not overridable by custom sources; if a source skill name conflicts with a built-in skill, the source copy is skipped
173
+ - Use `files.ejected` if the project must take ownership of a built-in skill or command
174
+
123
175
  ## Skill Authoring Conventions
124
176
 
125
177
  When writing or updating `.agents/skills/*/SKILL.md` files and their templates, keep step numbering consistent:
@@ -120,6 +120,58 @@
120
120
  4. 如果你维护的是模板源码分支或私有 fork,需要先补齐对应的 `.{platform}.` 模板变体,再把该平台标识加入模板同步逻辑。
121
121
  5. 在正式推广前,先用一个测试任务完整验证工作流和 gate 校验。
122
122
 
123
+ ## 自定义 Skills
124
+
125
+ 项目可以在内置任务工作流之外增加自己的 skill。
126
+
127
+ ### 项目内本地 skill
128
+
129
+ 在 `.agents/skills/<name>/` 下创建目录,并添加 `SKILL.md`:
130
+
131
+ ```text
132
+ .agents/skills/
133
+ enforce-style/
134
+ SKILL.md
135
+ reference/
136
+ style-guide.md
137
+ ```
138
+
139
+ 推荐 frontmatter:
140
+
141
+ ```yaml
142
+ ---
143
+ name: enforce-style
144
+ description: "在代码审查前应用团队风格规范"
145
+ args: "<task-id>" # 可选
146
+ ---
147
+ ```
148
+
149
+ 新增或修改自定义 skill 后,再执行一次 `update-agent-infra`。同步过程会自动检测非内置 skill,并为 Claude Code、Gemini CLI、OpenCode 生成对应命令。
150
+
151
+ ### 共享 skill 源
152
+
153
+ 如需复用团队集中维护的 skill,可在 `.agents/.airc.json` 中配置:
154
+
155
+ ```json
156
+ {
157
+ "skills": {
158
+ "sources": [
159
+ { "type": "local", "path": "~/company-skills" }
160
+ ]
161
+ }
162
+ }
163
+ ```
164
+
165
+ 每个 source 都应镜像 `.agents/skills/` 的目录结构,并在每个 skill 目录根部提供 `SKILL.md`。
166
+
167
+ ### 同步行为
168
+
169
+ - `.agents/skills/` 中手动创建的项目自定义 skill 不会被 managed 文件清理删除
170
+ - 多个 source 按声明顺序应用;后面的自定义 source 会覆盖前面的自定义 source 文件
171
+ - 对于仍存在于配置 source 中的 skill,如果源里删掉文件,下次同步时会删除本地对应残留文件
172
+ - 自定义 source 不能覆盖内置 skill;如果与内置 skill 同名,会跳过该 source skill
173
+ - 如果项目必须接管某个内置 skill 或命令,请使用 `files.ejected`
174
+
123
175
  ## Skill 编写规范
124
176
 
125
177
  编写或维护 `.agents/skills/*/SKILL.md` 及其模板时,步骤编号遵循以下规则:
@@ -99,12 +99,21 @@ gh pr list --state {state} --base {base-branch} --json number,title,url,headRefN
99
99
  Create a PR:
100
100
 
101
101
  ```bash
102
- gh pr create --base "{target-branch}" --title "{title}" --assignee @me --body "$(cat <<'EOF'
102
+ gh pr create --base "{target-branch}" --title "{title}" --assignee @me \
103
+ {label-args} {milestone-arg} \
104
+ --body "$(cat <<'EOF'
103
105
  {pr-body}
104
106
  EOF
105
107
  )"
106
108
  ```
107
109
 
110
+ - expand `{label-args}` into repeated `--label "{label}"` flags from the validated label list
111
+ - pass `{label-args}` only when `has_triage=true`; otherwise omit it and continue
112
+ - omit all `--label` flags when nothing valid remains
113
+ - expand `{milestone-arg}` into `--milestone "{milestone}"`
114
+ - pass `{milestone-arg}` only when `has_triage=true`; otherwise omit it and continue
115
+ - omit `{milestone-arg}` entirely when no milestone should be set
116
+
108
117
  ## Update PRs
109
118
 
110
119
  Update PR titles, labels, or milestones with:
@@ -99,12 +99,21 @@ gh pr list --state {state} --base {base-branch} --json number,title,url,headRefN
99
99
  创建 PR:
100
100
 
101
101
  ```bash
102
- gh pr create --base "{target-branch}" --title "{title}" --assignee @me --body "$(cat <<'EOF'
102
+ gh pr create --base "{target-branch}" --title "{title}" --assignee @me \
103
+ {label-args} {milestone-arg} \
104
+ --body "$(cat <<'EOF'
103
105
  {pr-body}
104
106
  EOF
105
107
  )"
106
108
  ```
107
109
 
110
+ - `{label-args}` 由调用方按有效 label 列表展开为多个 `--label "{label}"`
111
+ - 仅当 `has_triage=true` 时传入 `{label-args}`;否则整体省略并继续
112
+ - 没有有效 label 时省略全部 `--label`
113
+ - `{milestone-arg}` 展开为 `--milestone "{milestone}"`
114
+ - 仅当 `has_triage=true` 时传入 `{milestone-arg}`;否则整体省略并继续
115
+ - `{milestone-arg}` 为空时整体省略
116
+
108
117
  ## PR 更新
109
118
 
110
119
  更新 PR 标题、label 或 milestone: