@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.
- package/README.md +94 -1
- package/README.zh-CN.md +94 -1
- package/lib/sandbox/commands/create.js +7 -3
- package/lib/sandbox/shell.js +47 -7
- package/lib/sandbox/tools.js +18 -14
- package/package.json +1 -1
- package/templates/.agents/README.en.md +52 -0
- package/templates/.agents/README.zh-CN.md +52 -0
- package/templates/.agents/rules/milestone-inference.github.en.md +6 -5
- package/templates/.agents/rules/milestone-inference.github.zh-CN.md +6 -5
- package/templates/.agents/scripts/platform-adapters/platform-sync.github.js +47 -12
- package/templates/.agents/skills/analyze-task/SKILL.en.md +1 -1
- package/templates/.agents/skills/analyze-task/SKILL.zh-CN.md +1 -1
- package/templates/.agents/skills/implement-task/SKILL.en.md +1 -1
- package/templates/.agents/skills/implement-task/SKILL.zh-CN.md +1 -1
- package/templates/.agents/skills/import-issue/SKILL.en.md +10 -2
- package/templates/.agents/skills/import-issue/SKILL.zh-CN.md +10 -2
- package/templates/.agents/skills/import-issue/config/verify.json +2 -1
- package/templates/.agents/skills/plan-task/SKILL.en.md +1 -1
- package/templates/.agents/skills/plan-task/SKILL.zh-CN.md +1 -1
- package/templates/.agents/skills/refine-task/SKILL.en.md +1 -1
- package/templates/.agents/skills/refine-task/SKILL.zh-CN.md +1 -1
- package/templates/.agents/skills/review-task/SKILL.en.md +1 -1
- package/templates/.agents/skills/review-task/SKILL.zh-CN.md +1 -1
- package/templates/.agents/skills/update-agent-infra/scripts/sync-templates.js +315 -1
- package/templates/.github/workflows/metadata-sync.yml +1 -1
- package/templates/.github/workflows/pr-label.yml +1 -1
- 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
|
|
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 (!
|
|
946
|
-
return
|
|
943
|
+
if (!repoResult.ok) {
|
|
944
|
+
return repoResult;
|
|
947
945
|
}
|
|
948
946
|
|
|
949
|
-
|
|
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:
|
|
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
|
|
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
|
|
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.
|
|
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
|
-
###
|
|
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
|
-
###
|
|
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
|
|
|
@@ -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.
|
|
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@
|
|
59
|
+
uses: actions/checkout@v6
|
|
60
60
|
with:
|
|
61
61
|
sparse-checkout: .github/scripts
|
|
62
62
|
sparse-checkout-cone-mode: false
|