@fitlab-ai/agent-infra 0.5.4 → 0.5.5

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 CHANGED
@@ -410,7 +410,7 @@ The generated `.agents/.airc.json` file is the central contract between the boot
410
410
  "project": "my-project",
411
411
  "org": "my-org",
412
412
  "language": "en",
413
- "templateVersion": "v0.5.4",
413
+ "templateVersion": "v0.5.5",
414
414
  "files": {
415
415
  "managed": [
416
416
  ".agents/workspace/README.md",
package/README.zh-CN.md CHANGED
@@ -410,7 +410,7 @@ import-issue #42 从 GitHub Issue 导入任务
410
410
  "project": "my-project",
411
411
  "org": "my-org",
412
412
  "language": "en",
413
- "templateVersion": "v0.5.4",
413
+ "templateVersion": "v0.5.5",
414
414
  "files": {
415
415
  "managed": [
416
416
  ".agents/workspace/README.md",
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": [
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.5",
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",
@@ -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:
@@ -70,20 +70,22 @@ Decision rules:
70
70
 
71
71
  ## Direct `status:` Label Updates
72
72
 
73
- If task.md contains a valid `issue_number` (not empty and not `N/A`) and the Issue state is `OPEN`, replace every existing `status:` label and add the target one:
73
+ Algorithm note: keep the flow below aligned with `.github/scripts/sync-labels-to-set.sh` (set-diff sync). This is the AI agent-side equivalent implementation for the `target_set = {"{target-status-label}"}` case. If either side changes, update the other one in the same patch to avoid drift between the agent and the bot.
74
+
75
+ If task.md contains a valid `issue_number` (not empty and not `N/A`) and the Issue state is `OPEN`, sync the `status:` labels to the target value with an idempotent set diff:
74
76
 
75
77
  ```bash
76
78
  state=$(gh issue view {issue-number} -R "$upstream_repo" --json state --jq '.state' 2>/dev/null)
77
79
  if [ "$state" = "OPEN" ]; then
78
- gh issue view {issue-number} -R "$upstream_repo" --json labels \
79
- --jq '.labels[].name | select(startswith("status:"))' 2>/dev/null \
80
- | while IFS= read -r label; do
81
- [ -z "$label" ] && continue
82
- if [ "$has_triage" = "true" ]; then
83
- gh issue edit {issue-number} -R "$upstream_repo" --remove-label "$label" 2>/dev/null || true
84
- fi
85
- done
86
- if [ "$has_triage" = "true" ]; then
80
+ current_status_labels=$(gh issue view {issue-number} -R "$upstream_repo" \
81
+ --json labels --jq '.labels[].name | select(startswith("status:"))' 2>/dev/null || true)
82
+ printf '%s\n' "$current_status_labels" | while IFS= read -r label; do
83
+ [ -z "$label" ] && continue
84
+ if [ "$label" != "{target-status-label}" ] && [ "$has_triage" = "true" ]; then
85
+ gh issue edit {issue-number} -R "$upstream_repo" --remove-label "$label" 2>/dev/null || true
86
+ fi
87
+ done
88
+ if [ "$has_triage" = "true" ] && ! printf '%s\n' "$current_status_labels" | grep -qxF "{target-status-label}"; then
87
89
  gh issue edit {issue-number} -R "$upstream_repo" --add-label "{target-status-label}" 2>/dev/null || true
88
90
  fi
89
91
  fi
@@ -70,20 +70,22 @@ current_user=$(gh api user --jq '.login' 2>/dev/null || echo "")
70
70
 
71
71
  ## status label 设置
72
72
 
73
- 如果 task.md 中存在有效的 `issue_number`(非空、非 `N/A`),且 Issue 状态为 `OPEN`,则替换所有 `status:` label 并设置目标值:
73
+ 算法说明:下面的流程与 `.github/scripts/sync-labels-to-set.sh` 保持一致(集合差集)。本章节是 AI Agent 侧的等价实现(`target_set = {"{target-status-label}"}` 的特例)。修改任一侧时,必须同步另一侧,避免 Agent 与 Bot 的行为漂移。
74
+
75
+ 如果 task.md 中存在有效的 `issue_number`(非空、非 `N/A`),且 Issue 状态为 `OPEN`,则按幂等差集方式将 `status:` label 同步到目标值:
74
76
 
75
77
  ```bash
76
78
  state=$(gh issue view {issue-number} -R "$upstream_repo" --json state --jq '.state' 2>/dev/null)
77
79
  if [ "$state" = "OPEN" ]; then
78
- gh issue view {issue-number} -R "$upstream_repo" --json labels \
79
- --jq '.labels[].name | select(startswith("status:"))' 2>/dev/null \
80
- | while IFS= read -r label; do
81
- [ -z "$label" ] && continue
82
- if [ "$has_triage" = "true" ]; then
83
- gh issue edit {issue-number} -R "$upstream_repo" --remove-label "$label" 2>/dev/null || true
84
- fi
85
- done
86
- if [ "$has_triage" = "true" ]; then
80
+ current_status_labels=$(gh issue view {issue-number} -R "$upstream_repo" \
81
+ --json labels --jq '.labels[].name | select(startswith("status:"))' 2>/dev/null || true)
82
+ printf '%s\n' "$current_status_labels" | while IFS= read -r label; do
83
+ [ -z "$label" ] && continue
84
+ if [ "$label" != "{target-status-label}" ] && [ "$has_triage" = "true" ]; then
85
+ gh issue edit {issue-number} -R "$upstream_repo" --remove-label "$label" 2>/dev/null || true
86
+ fi
87
+ done
88
+ if [ "$has_triage" = "true" ] && ! printf '%s\n' "$current_status_labels" | grep -qxF "{target-status-label}"; then
87
89
  gh issue edit {issue-number} -R "$upstream_repo" --add-label "{target-status-label}" 2>/dev/null || true
88
90
  fi
89
91
  fi
@@ -82,6 +82,7 @@ export function check({ taskDir, config, artifactFile }, shared) {
82
82
  checkCommentContent,
83
83
  checkTaskCommentContent,
84
84
  checkInLabelsComputed,
85
+ checkPrTypeLabel,
85
86
  checkInLabelsMatchPr,
86
87
  checkPrAssignee,
87
88
  checkSyncedRequirements,
@@ -251,10 +252,11 @@ function fetchRemoteData(context) {
251
252
  let prMilestone;
252
253
  let prAssignees;
253
254
  if (((context.config.verify_in_labels_match_pr && context.hasTriage)
255
+ || (context.config.verify_pr_type_label && context.hasTriage)
254
256
  || (context.config.verify_milestone && context.hasTriage)
255
257
  || (context.config.verify_pr_assignee && context.hasPush)) && context.prNumber) {
256
258
  const prFields = [];
257
- if (context.config.verify_in_labels_match_pr) {
259
+ if (context.config.verify_in_labels_match_pr || context.config.verify_pr_type_label) {
258
260
  prFields.push("labels");
259
261
  }
260
262
  if (context.config.verify_milestone) {
@@ -280,7 +282,7 @@ function fetchRemoteData(context) {
280
282
  };
281
283
  }
282
284
 
283
- prLabels = context.config.verify_in_labels_match_pr
285
+ prLabels = (context.config.verify_in_labels_match_pr || context.config.verify_pr_type_label)
284
286
  ? extractLabelNames(prResult.value?.labels)
285
287
  : null;
286
288
  prMilestone = context.config.verify_milestone
@@ -302,6 +304,22 @@ function fetchRemoteData(context) {
302
304
  };
303
305
  }
304
306
 
307
+ function mapTaskTypeToLabel(taskType) {
308
+ const mapping = {
309
+ bug: "type: bug",
310
+ bugfix: "type: bug",
311
+ feature: "type: feature",
312
+ enhancement: "type: enhancement",
313
+ refactor: "type: enhancement",
314
+ refactoring: "type: enhancement",
315
+ documentation: "type: documentation",
316
+ "dependency-upgrade": "type: dependency-upgrade",
317
+ task: "type: task"
318
+ };
319
+
320
+ return mapping[taskType] || null;
321
+ }
322
+
305
323
  function shouldFetchComments(config) {
306
324
  return Boolean(
307
325
  config.expected_comment_marker
@@ -481,6 +499,26 @@ function checkTaskCommentContent(context, remoteData) {
481
499
  );
482
500
  }
483
501
 
502
+ function checkPrTypeLabel(context, remoteData) {
503
+ if (!context.config.verify_pr_type_label || !context.hasTriage || !context.prNumber || !remoteData.prLabels) {
504
+ return null;
505
+ }
506
+
507
+ const expectedLabel = mapTaskTypeToLabel(context.task.metadata.type);
508
+ if (!expectedLabel) {
509
+ return null;
510
+ }
511
+
512
+ if (remoteData.prLabels.includes(expectedLabel)) {
513
+ return null;
514
+ }
515
+
516
+ return failResult(CHECK_TYPE,
517
+ `Expected type label '${expectedLabel}' not found on PR #${context.prNumber}`,
518
+ "check_failed"
519
+ );
520
+ }
521
+
484
522
  function checkInLabelsMatchPr(context, remoteData) {
485
523
  if (!context.config.verify_in_labels_match_pr || !context.hasTriage || !context.prNumber || !remoteData.prLabels) {
486
524
  return null;
@@ -21,6 +21,8 @@
21
21
  "when": "issue_number_exists",
22
22
  "expected_pr_comment_marker": "<!-- sync-pr:{task-id}:summary -->",
23
23
  "verify_in_labels_match_pr": true,
24
+ "verify_pr_type_label": true,
25
+ "verify_pr_assignee": true,
24
26
  "verify_milestone": true
25
27
  }
26
28
  }
@@ -27,7 +27,7 @@ git diff <target-branch>...HEAD
27
27
 
28
28
  Read `.agents/rules/issue-pr-commands.md` before this step.
29
29
 
30
- Before syncing linked Issue metadata, complete authentication and code-hosting platform detection through that rule. Keep `gh pr list` / `gh pr edit` on the current repository.
30
+ Before syncing linked Issue metadata, complete authentication and code-hosting platform detection through that rule. Keep `gh pr list` / `gh pr create` on the current repository.
31
31
 
32
32
  Before syncing labels, verify the standard label system:
33
33
 
@@ -52,13 +52,12 @@ Type label mapping:
52
52
 
53
53
  Metadata sync order:
54
54
  1. query Issue labels and milestone via the Issue read command in `.agents/rules/issue-pr-commands.md`
55
- 2. handle the mapped type label via the PR update command and permission-degradation rules in `.agents/rules/issue-pr-commands.md`
56
- 3. handle inheritance of non-`type:` and non-`status:` Issue labels via repeated PR update commands and the same permission-degradation rules
57
- 4. copy the current Issue `in:` labels to the PR (commit already computed them, so do not recompute them here and do not write back to the Issue)
58
- 5. handle the milestone by following "Phase 3: `create-pr`" in `.agents/rules/milestone-inference.md`, including its permission rules, and reuse the Issue milestone directly
59
- 6. ensure the PR body contains `Closes #{issue-number}` or an equivalent closing keyword
55
+ 2. build `{label-args}` from the mapped type label, non-`type:` / non-`status:` Issue labels, and the current Issue `in:` labels (commit already computed them, so do not recompute them here and do not write back to the Issue)
56
+ 3. build `{milestone-arg}` by following "Phase 3: `create-pr`" in `.agents/rules/milestone-inference.md` and reusing the Issue milestone directly
57
+ 4. pass `{label-args}` and `{milestone-arg}` atomically to `gh pr create` by using the create-PR command template and permission-degradation rules in `.agents/rules/issue-pr-commands.md`
58
+ 5. ensure the PR body contains `Closes #{issue-number}` or an equivalent closing keyword
60
59
 
61
- If those rules say to skip the direct metadata writes above, keep only the PR body linkage plus later comment sync.
60
+ If those rules say to skip the direct metadata arguments above, keep only the PR body linkage plus later comment sync.
62
61
 
63
62
  Milestone rule:
64
63
  - Follow "Phase 3: `create-pr`" in `.agents/rules/milestone-inference.md`
@@ -27,7 +27,7 @@ git diff <target-branch>...HEAD
27
27
 
28
28
  执行前先读取 `.agents/rules/issue-pr-commands.md`。
29
29
 
30
- 同步关联 Issue 元数据前,先按该规则完成认证和代码托管平台检测;`gh pr list` / `gh pr edit` 仍保持作用于当前仓库。
30
+ 同步关联 Issue 元数据前,先按该规则完成认证和代码托管平台检测;`gh pr list` / `gh pr create` 仍保持作用于当前仓库。
31
31
 
32
32
  在同步 label 之前,先确认标准 label 体系已经存在:
33
33
 
@@ -52,13 +52,12 @@ Type label 映射:
52
52
 
53
53
  元数据同步顺序:
54
54
  1. 按 `.agents/rules/issue-pr-commands.md` 的 Issue 读取命令查询关联 Issue 的 labels 和 milestone
55
- 2. `.agents/rules/issue-pr-commands.md` 的 PR 更新命令和权限降级规则处理映射后的 type label
56
- 3. 按同一规则的 PR 更新命令和权限降级规则处理非 `type:`、非 `status:` Issue labels 继承
57
- 4. Issue 当前的 `in:` labels 复制到 PR(commit 阶段已完成计算,此处不重新计算也不反向更新 Issue)
58
- 5. `.agents/rules/milestone-inference.md` 的「阶段 3:`create-pr`」及其权限规则处理 milestone,直接复用 Issue milestone
59
- 6. 确保 PR 正文包含 `Closes #{issue-number}` 或等价的 closing keyword
55
+ 2. 构建 `{label-args}`:包含映射后的 type label、非 `type:`/`status:`Issue labels,以及 Issue 当前的 `in:` labels(commit 阶段已完成计算,此处不重新计算也不反向更新 Issue)
56
+ 3. 构建 `{milestone-arg}`:按 `.agents/rules/milestone-inference.md` 的「阶段 3:`create-pr`」直接复用 Issue milestone
57
+ 4. `.agents/rules/issue-pr-commands.md` 的创建 PR 命令模板与权限降级规则,将 `{label-args}` `{milestone-arg}` 原子化传入 `gh pr create`
58
+ 5. 确保 PR 正文包含 `Closes #{issue-number}` 或等价的 closing keyword
60
59
 
61
- 如果上述规则判定应跳过直接元数据写入,则只保留 PR 正文中的关联信息与后续评论同步。
60
+ 如果上述规则判定应跳过直接元数据参数写入,则只保留 PR 正文中的关联信息与后续评论同步。
62
61
 
63
62
  Milestone 规则:
64
63
  - 按 `.agents/rules/milestone-inference.md` 的「阶段 3:`create-pr`」处理
@@ -54,7 +54,7 @@ Read `.agents/rules/release-commands.md` before this step.
54
54
  - When generating release notes in Step 7, **must** follow both the historical format style and the full category list gathered in Step 3
55
55
  - If no historical release notes exist, use the default format defined in Step 7
56
56
 
57
- ### 4. Collect Merged PRs
57
+ ### 4. Collect Merged PRs and Contributors
58
58
 
59
59
  Get the date range between tags, then query merged PRs:
60
60
 
@@ -71,6 +71,17 @@ Also collect direct commits without PRs:
71
71
  git log v<prev-version>..v<version> --format="%H %s" --no-merges
72
72
  ```
73
73
 
74
+ Collect collaborative contributors from commit `Co-authored-by` trailers:
75
+
76
+ ```bash
77
+ git log v<prev-version>..v<version> \
78
+ --no-merges \
79
+ --format='%(trailers:key=Co-authored-by,valueonly,unfold)' \
80
+ | grep -v '^$' | sort | uniq -c | sort -rn
81
+ ```
82
+
83
+ Each output line is `Name <email>` and `uniq -c` provides the number of commits where that identity appeared as a co-author within the range.
84
+
74
85
  ### 5. Collect Related Issues
75
86
 
76
87
  From each PR body, extract linked Issues:
@@ -115,7 +126,21 @@ If no historical release notes exist, use the following default Markdown format:
115
126
  1. Item format: `- [scope] Description by @author in [#N](url)`
116
127
  2. Issue + PR: `in [#Issue](url) and [#PR](url)`
117
128
  3. Description: Use PR title, remove `type(scope):` prefix, capitalize first letter
118
- 4. Contributors: Deduplicated, sorted by contribution count (descending)
129
+ 4. **Contributor collection**:
130
+ - **Data sources**:
131
+ - PR authors from Step 4 `gh pr list --json author`
132
+ - Commit co-authors from Step 4 `git log ... --format='%(trailers:key=Co-authored-by,valueonly,unfold)'`
133
+ - **Contribution count**: `PR count + co-authored commit count` for the same identity, merged across both sources
134
+ - **Name -> `@login` mapping**:
135
+ - Raw `Co-authored-by` values are `Name <email>` and must be mapped to a GitHub `@login`
136
+ - Prefer email extraction: if it matches `(\d+\+)?(\S+?)@users\.noreply\.github\.com`, use the second capture group lowercased; this regex covers both `{id}+{login}@users.noreply.github.com` and `{login}@users.noreply.github.com`
137
+ - Otherwise use a Name heuristic: take the first token before a space and lowercase it, for example `Claude Opus 4.6 (1M context)` -> `@claude`, `Codex` -> `@codex`, `Gemini` -> `@gemini`
138
+ - If the login already appears in the PR author list, merge counts into that login so `Claude` and `@claude` do not become separate entries
139
+ - Merge all Name variants that map to the same login before counting and sorting; for example, `Claude` and `Claude Opus 4.6 (1M context)` should both collapse into `@claude`
140
+ - Preserve bot identities as-is, for example `dependabot[bot]`
141
+ - If the login still cannot be determined reliably, output `@{lowercased first Name token}` and append `<!-- TODO(reviewer): confirm GitHub login for {original Name <email>} -->` below the `Contributors` section
142
+ - **Sorting**: descending by contribution count, then lexicographically by login for ties
143
+ - **Deduplication**: use the final mapped `@login` as the key
119
144
  5. Empty sections: Omit sections with no entries
120
145
 
121
146
  ### 8. Present and Confirm
@@ -54,7 +54,7 @@ git rev-parse v<prev-version>
54
54
  - 后续步骤 7 生成发布说明时,**必须**同时参考步骤 3 的历史格式风格和完整分类清单,保持版本间的一致性
55
55
  - 如果没有历史发布说明,则使用步骤 7 中定义的默认格式
56
56
 
57
- ### 4. 收集已合并的 PR
57
+ ### 4. 收集已合并的 PR 与贡献者
58
58
 
59
59
  获取标签之间的日期范围,然后查询已合并的 PR:
60
60
 
@@ -71,6 +71,17 @@ git log v<version> --format=%aI -1
71
71
  git log v<prev-version>..v<version> --format="%H %s" --no-merges
72
72
  ```
73
73
 
74
+ 从 commit `Co-authored-by` trailer 中收集协作贡献者:
75
+
76
+ ```bash
77
+ git log v<prev-version>..v<version> \
78
+ --no-merges \
79
+ --format='%(trailers:key=Co-authored-by,valueonly,unfold)' \
80
+ | grep -v '^$' | sort | uniq -c | sort -rn
81
+ ```
82
+
83
+ 输出每行一个 `Name <email>`(`uniq -c` 给出该身份在范围内作为 co-author 的 commit 数)。
84
+
74
85
  ### 5. 收集关联 Issue
75
86
 
76
87
  从每个 PR body 中提取关联的 Issue:
@@ -115,7 +126,21 @@ git log v<prev-version>..v<version> --format="%H %s" --no-merges
115
126
  1. 条目格式:`- [scope] Description by @author in [#N](url)`
116
127
  2. Issue + PR:`in [#Issue](url) and [#PR](url)`
117
128
  3. 描述:使用 PR 标题,移除 `type(scope):` 前缀,首字母大写
118
- 4. 贡献者:去重,按贡献数量降序排列
129
+ 4. **贡献者搜集**:
130
+ - **数据源**:
131
+ - PR author:来自步骤 4 的 `gh pr list --json author`
132
+ - Commit co-authors:来自步骤 4 的 `git log ... --format='%(trailers:key=Co-authored-by,valueonly,unfold)'`
133
+ - **贡献数定义**:`该人的 PR 数 + 该人作为 co-author 的 commit 数`(同一身份跨来源合并计数)
134
+ - **Name → `@login` 映射**:
135
+ - `Co-authored-by` 原始格式为 `Name <email>`,需要推断对应的 GitHub `@login`
136
+ - 优先从 email 提取:匹配 `(\d+\+)?(\S+?)@users\.noreply\.github\.com` 时,取第二个捕获组并转为小写;该正则同时覆盖 `{id}+{login}@users.noreply.github.com` 与 `{login}@users.noreply.github.com`
137
+ - 否则按 Name 启发式:取首个空格前的 token 并转为小写(例如 `Claude Opus 4.6 (1M context)` → `@claude`、`Codex` → `@codex`、`Gemini` → `@gemini`)
138
+ - 已出现在 PR author 列表中的 login,必须按该 login 合并计数,避免把 `Claude` 和 `@claude` 拆成两个条目
139
+ - 同一 login 的所有 Name 变体都必须归并后再计数与排序;例如 `Claude` 与 `Claude Opus 4.6 (1M context)` 都映射到 `@claude` 时,应先合并为同一个贡献者
140
+ - Bot 身份保留原样(如 `dependabot[bot]`)
141
+ - 若仍无法可靠确定 login,则输出 `@{Name 首 token 小写}`,并在 `Contributors` 段落下追加 `<!-- TODO(reviewer): 确认 {原始 Name <email>} 的 GitHub login -->`
142
+ - **排序**:按贡献数降序;贡献数相同时按 login 字典序
143
+ - **去重**:以最终映射后的 `@login` 为键
119
144
  5. 空部分:省略没有条目的部分
120
145
 
121
146
  ### 8. 展示并确认
@@ -56,6 +56,7 @@ const DEFAULTS = {
56
56
  ".claude/hooks/",
57
57
  ".gemini/commands/",
58
58
  ".github/hooks/check-version-format.sh",
59
+ ".github/scripts/",
59
60
  ".opencode/commands/"
60
61
  ],
61
62
  "merged": [
@@ -76,7 +77,7 @@ const DEFAULTS = {
76
77
  }
77
78
  };
78
79
 
79
- const INSTALLER_VERSION = "v0.5.4";
80
+ const INSTALLER_VERSION = "v0.5.5";
80
81
  const PACKAGE_NAME = '@fitlab-ai/agent-infra';
81
82
  // Add a new identifier here only after shipping matching .{platform}. template variants.
82
83
  const KNOWN_PLATFORMS = new Set(['github']);
@@ -0,0 +1,110 @@
1
+ #!/bin/sh
2
+ # Ensure the labels matching --prefix on an issue or PR equal the set passed via
3
+ # repeated --target flags (0, 1, or N labels).
4
+ # Algorithm must stay in sync with .agents/rules/issue-sync.md.
5
+
6
+ set -e
7
+
8
+ usage() {
9
+ printf 'Usage: %s --repo <owner/repo> (--issue <number> | --pr <number>) --prefix <prefix> [--target <label> ...]\n' "$0" >&2
10
+ exit 1
11
+ }
12
+
13
+ append_target() {
14
+ if [ -n "$targets" ]; then
15
+ targets=$(printf '%s\n%s' "$targets" "$1")
16
+ else
17
+ targets=$1
18
+ fi
19
+ }
20
+
21
+ repo=""
22
+ number=""
23
+ kind=""
24
+ prefix=""
25
+ targets=""
26
+
27
+ while [ $# -gt 0 ]; do
28
+ case "$1" in
29
+ --repo)
30
+ [ $# -ge 2 ] || usage
31
+ repo=$2
32
+ shift 2
33
+ ;;
34
+ --issue)
35
+ [ $# -ge 2 ] || usage
36
+ [ -z "$kind" ] || usage
37
+ kind="issue"
38
+ number=$2
39
+ shift 2
40
+ ;;
41
+ --pr)
42
+ [ $# -ge 2 ] || usage
43
+ [ -z "$kind" ] || usage
44
+ kind="pr"
45
+ number=$2
46
+ shift 2
47
+ ;;
48
+ --prefix)
49
+ [ $# -ge 2 ] || usage
50
+ prefix=$2
51
+ shift 2
52
+ ;;
53
+ --target)
54
+ [ $# -ge 2 ] || usage
55
+ append_target "$2"
56
+ shift 2
57
+ ;;
58
+ *)
59
+ printf 'Unknown argument: %s\n' "$1" >&2
60
+ usage
61
+ ;;
62
+ esac
63
+ done
64
+
65
+ [ -n "$repo" ] || usage
66
+ [ -n "$number" ] || usage
67
+ [ -n "$kind" ] || usage
68
+ [ -n "$prefix" ] || usage
69
+
70
+ while IFS= read -r label; do
71
+ [ -z "$label" ] && continue
72
+ case "$label" in
73
+ "$prefix"*) ;;
74
+ *)
75
+ printf 'Target "%s" must start with prefix "%s"\n' "$label" "$prefix" >&2
76
+ exit 1
77
+ ;;
78
+ esac
79
+ done <<EOF
80
+ $targets
81
+ EOF
82
+
83
+ current_labels=$(gh "$kind" view "$number" \
84
+ --repo "$repo" \
85
+ --json labels --jq ".labels[].name | select(startswith(\"$prefix\"))" \
86
+ 2>/dev/null || true)
87
+
88
+ while IFS= read -r label; do
89
+ [ -z "$label" ] && continue
90
+ if ! printf '%s\n' "$targets" | grep -qxF "$label"; then
91
+ gh "$kind" edit "$number" \
92
+ --repo "$repo" \
93
+ --remove-label "$label" \
94
+ 2>/dev/null || true
95
+ fi
96
+ done <<EOF
97
+ $current_labels
98
+ EOF
99
+
100
+ while IFS= read -r label; do
101
+ [ -z "$label" ] && continue
102
+ if ! printf '%s\n' "$current_labels" | grep -qxF "$label"; then
103
+ gh "$kind" edit "$number" \
104
+ --repo "$repo" \
105
+ --add-label "$label" \
106
+ 2>/dev/null || true
107
+ fi
108
+ done <<EOF
109
+ $targets
110
+ EOF
@@ -54,6 +54,13 @@ jobs:
54
54
  printf 'type=%s\n' "$type" >> "$GITHUB_OUTPUT"
55
55
  printf 'milestone=%s\n' "$milestone" >> "$GITHUB_OUTPUT"
56
56
 
57
+ - name: Checkout shared scripts
58
+ if: steps.metadata.outputs.is_task_comment == 'true' && steps.metadata.outputs.type != ''
59
+ uses: actions/checkout@v4
60
+ with:
61
+ sparse-checkout: .github/scripts
62
+ sparse-checkout-cone-mode: false
63
+
57
64
  - name: Sync type label
58
65
  if: steps.metadata.outputs.is_task_comment == 'true' && steps.metadata.outputs.type != ''
59
66
  env:
@@ -72,27 +79,11 @@ jobs:
72
79
  esac
73
80
 
74
81
  if [ -n "$TYPE_LABEL" ]; then
75
- current_type_labels=$(gh issue view "$ISSUE_NUMBER" \
82
+ .github/scripts/sync-labels-to-set.sh \
76
83
  --repo "$GITHUB_REPOSITORY" \
77
- --json labels --jq '.labels[].name | select(startswith("type:"))' \
78
- 2>/dev/null || true)
79
-
80
- printf '%s\n' "$current_type_labels" | while IFS= read -r label; do
81
- [ -z "$label" ] && continue
82
- if [ "$label" != "$TYPE_LABEL" ]; then
83
- gh issue edit "$ISSUE_NUMBER" \
84
- --repo "$GITHUB_REPOSITORY" \
85
- --remove-label "$label" \
86
- 2>/dev/null || true
87
- fi
88
- done || true
89
-
90
- if ! printf '%s\n' "$current_type_labels" | grep -qxF "$TYPE_LABEL"; then
91
- gh issue edit "$ISSUE_NUMBER" \
92
- --repo "$GITHUB_REPOSITORY" \
93
- --add-label "$TYPE_LABEL" \
94
- 2>/dev/null || true
95
- fi
84
+ --issue "$ISSUE_NUMBER" \
85
+ --prefix "type:" \
86
+ --target "$TYPE_LABEL"
96
87
  fi
97
88
 
98
89
  - name: Sync milestone
@@ -37,28 +37,19 @@ jobs:
37
37
  | map("in: " + .key)
38
38
  | .[]?')
39
39
 
40
- current_labels=$(gh pr view "$PR_NUMBER" \
40
+ set -- \
41
41
  --repo "$GITHUB_REPOSITORY" \
42
- --json labels --jq '.labels[].name | select(startswith("in: "))' \
43
- 2>/dev/null || true)
42
+ --pr "$PR_NUMBER" \
43
+ --prefix "in: "
44
44
 
45
- printf '%s\n' "$should_labels" | while IFS= read -r label; do
45
+ while IFS= read -r label; do
46
46
  [ -z "$label" ] && continue
47
- if ! printf '%s\n' "$current_labels" | grep -qxF "$label"; then
48
- gh pr edit "$PR_NUMBER" \
49
- --repo "$GITHUB_REPOSITORY" \
50
- --add-label "$label" 2>/dev/null || true
51
- fi
52
- done
47
+ set -- "$@" --target "$label"
48
+ done <<EOF
49
+ $should_labels
50
+ EOF
53
51
 
54
- printf '%s\n' "$current_labels" | while IFS= read -r label; do
55
- [ -z "$label" ] && continue
56
- if ! printf '%s\n' "$should_labels" | grep -qxF "$label"; then
57
- gh pr edit "$PR_NUMBER" \
58
- --repo "$GITHUB_REPOSITORY" \
59
- --remove-label "$label" 2>/dev/null || true
60
- fi
61
- done
52
+ .github/scripts/sync-labels-to-set.sh "$@"
62
53
 
63
54
  - name: Assign PR creator if unassigned
64
55
  env:
@@ -14,6 +14,16 @@ jobs:
14
14
  manage-status-labels:
15
15
  runs-on: ubuntu-latest
16
16
  steps:
17
+ - name: Checkout shared scripts
18
+ if: >-
19
+ (github.event_name == 'issues' && github.event.action == 'closed' && github.event.issue.state_reason == 'completed') ||
20
+ (github.event_name == 'issues' && github.event.action == 'reopened') ||
21
+ (github.event_name == 'pull_request_target' && github.event.action == 'closed' && github.event.pull_request.merged == true)
22
+ uses: actions/checkout@v4
23
+ with:
24
+ sparse-checkout: .github/scripts
25
+ sparse-checkout-cone-mode: false
26
+
17
27
  - name: Remove status labels on issue close
18
28
  if: >-
19
29
  github.event_name == 'issues' &&
@@ -23,16 +33,10 @@ jobs:
23
33
  GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
24
34
  ISSUE_NUMBER: ${{ github.event.issue.number }}
25
35
  run: |
26
- gh issue view "$ISSUE_NUMBER" \
36
+ .github/scripts/sync-labels-to-set.sh \
27
37
  --repo "$GITHUB_REPOSITORY" \
28
- --json labels --jq '.labels[].name | select(startswith("status:"))' \
29
- 2>/dev/null \
30
- | while IFS= read -r label; do
31
- [ -z "$label" ] && continue
32
- gh issue edit "$ISSUE_NUMBER" \
33
- --repo "$GITHUB_REPOSITORY" \
34
- --remove-label "$label"
35
- done || true
38
+ --issue "$ISSUE_NUMBER" \
39
+ --prefix "status:"
36
40
 
37
41
  - name: Add triage label on issue reopen
38
42
  if: github.event_name == 'issues' && github.event.action == 'reopened'
@@ -40,23 +44,11 @@ jobs:
40
44
  GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
41
45
  ISSUE_NUMBER: ${{ github.event.issue.number }}
42
46
  run: |
43
- current_status_labels=$(gh issue view "$ISSUE_NUMBER" \
47
+ .github/scripts/sync-labels-to-set.sh \
44
48
  --repo "$GITHUB_REPOSITORY" \
45
- --json labels --jq '.labels[].name | select(startswith("status:"))' \
46
- 2>/dev/null || true)
47
- printf '%s\n' "$current_status_labels" | while IFS= read -r label; do
48
- [ -z "$label" ] && continue
49
- if [ "$label" != "status: waiting-for-triage" ]; then
50
- gh issue edit "$ISSUE_NUMBER" \
51
- --repo "$GITHUB_REPOSITORY" \
52
- --remove-label "$label"
53
- fi
54
- done || true
55
- if ! printf '%s\n' "$current_status_labels" | grep -qxF "status: waiting-for-triage"; then
56
- gh issue edit "$ISSUE_NUMBER" \
57
- --repo "$GITHUB_REPOSITORY" \
58
- --add-label "status: waiting-for-triage"
59
- fi
49
+ --issue "$ISSUE_NUMBER" \
50
+ --prefix "status:" \
51
+ --target "status: waiting-for-triage"
60
52
 
61
53
  - name: Clean status labels on PR merge
62
54
  if: >-
@@ -75,15 +67,9 @@ jobs:
75
67
  --repo "$GITHUB_REPOSITORY" \
76
68
  --json state --jq '.state')
77
69
  if [ "$state" = "CLOSED" ]; then
78
- gh issue view "$issue_number" \
70
+ .github/scripts/sync-labels-to-set.sh \
79
71
  --repo "$GITHUB_REPOSITORY" \
80
- --json labels --jq '.labels[].name | select(startswith("status:"))' \
81
- 2>/dev/null \
82
- | while IFS= read -r label; do
83
- [ -z "$label" ] && continue
84
- gh issue edit "$issue_number" \
85
- --repo "$GITHUB_REPOSITORY" \
86
- --remove-label "$label"
87
- done || true
72
+ --issue "$issue_number" \
73
+ --prefix "status:"
88
74
  fi
89
75
  done