@fitlab-ai/agent-infra 0.6.0 → 0.6.1

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 (87) hide show
  1. package/README.md +12 -12
  2. package/README.zh-CN.md +12 -12
  3. package/bin/cli.ts +5 -1
  4. package/dist/bin/cli.js +6 -1
  5. package/dist/lib/defaults.json +4 -3
  6. package/dist/lib/sandbox/config.js +1 -1
  7. package/dist/package.json +1 -1
  8. package/lib/defaults.json +4 -3
  9. package/lib/sandbox/config.ts +1 -1
  10. package/package.json +3 -3
  11. package/templates/.agents/README.en.md +8 -8
  12. package/templates/.agents/README.zh-CN.md +8 -8
  13. package/templates/{.claude → .agents}/hooks/check-version-format.sh +3 -3
  14. package/templates/.agents/rules/create-issue.github.en.md +6 -0
  15. package/templates/.agents/rules/create-issue.github.zh-CN.md +6 -0
  16. package/templates/.agents/rules/issue-fields.github.en.md +155 -0
  17. package/templates/.agents/rules/issue-fields.github.zh-CN.md +155 -0
  18. package/templates/.agents/rules/issue-pr-commands.github.en.md +1 -0
  19. package/templates/.agents/rules/issue-pr-commands.github.zh-CN.md +1 -0
  20. package/templates/.agents/rules/issue-sync.github.en.md +2 -1
  21. package/templates/.agents/rules/issue-sync.github.zh-CN.md +2 -1
  22. package/templates/.agents/rules/task-management.en.md +17 -9
  23. package/templates/.agents/rules/task-management.zh-CN.md +17 -9
  24. package/templates/.agents/rules/testing-discipline.en.md +40 -0
  25. package/templates/.agents/rules/testing-discipline.zh-CN.md +40 -0
  26. package/templates/.agents/rules/version-stamp.en.md +29 -0
  27. package/templates/.agents/rules/version-stamp.zh-CN.md +29 -0
  28. package/templates/.agents/scripts/platform-adapters/platform-sync.github.js +143 -6
  29. package/templates/.agents/scripts/validate-artifact.js +32 -5
  30. package/templates/.agents/skills/analyze-task/SKILL.en.md +3 -0
  31. package/templates/.agents/skills/analyze-task/SKILL.zh-CN.md +3 -0
  32. package/templates/.agents/skills/analyze-task/config/verify.json +2 -0
  33. package/templates/.agents/skills/block-task/SKILL.en.md +3 -0
  34. package/templates/.agents/skills/block-task/SKILL.zh-CN.md +3 -0
  35. package/templates/.agents/skills/block-task/config/verify.json +1 -0
  36. package/templates/.agents/skills/cancel-task/SKILL.en.md +3 -0
  37. package/templates/.agents/skills/cancel-task/SKILL.zh-CN.md +3 -0
  38. package/templates/.agents/skills/cancel-task/config/verify.json +1 -0
  39. package/templates/.agents/skills/commit/SKILL.en.md +10 -0
  40. package/templates/.agents/skills/commit/SKILL.zh-CN.md +10 -0
  41. package/templates/.agents/skills/commit/config/verify.json +1 -0
  42. package/templates/.agents/skills/commit/reference/task-status-update.en.md +5 -0
  43. package/templates/.agents/skills/commit/reference/task-status-update.zh-CN.md +5 -0
  44. package/templates/.agents/skills/complete-task/SKILL.en.md +4 -0
  45. package/templates/.agents/skills/complete-task/SKILL.zh-CN.md +4 -0
  46. package/templates/.agents/skills/complete-task/config/verify.json +2 -0
  47. package/templates/.agents/skills/create-pr/SKILL.en.md +5 -1
  48. package/templates/.agents/skills/create-pr/SKILL.zh-CN.md +5 -1
  49. package/templates/.agents/skills/create-pr/config/verify.json +1 -0
  50. package/templates/.agents/skills/create-task/SKILL.en.md +9 -0
  51. package/templates/.agents/skills/create-task/SKILL.zh-CN.md +9 -0
  52. package/templates/.agents/skills/create-task/config/verify.json +1 -0
  53. package/templates/.agents/skills/implement-task/SKILL.en.md +16 -1
  54. package/templates/.agents/skills/implement-task/SKILL.zh-CN.md +16 -1
  55. package/templates/.agents/skills/implement-task/config/verify.json +2 -0
  56. package/templates/.agents/skills/import-codescan/config/verify.json +1 -0
  57. package/templates/.agents/skills/import-dependabot/config/verify.json +1 -0
  58. package/templates/.agents/skills/import-issue/SKILL.en.md +10 -0
  59. package/templates/.agents/skills/import-issue/SKILL.zh-CN.md +10 -0
  60. package/templates/.agents/skills/import-issue/config/verify.json +1 -0
  61. package/templates/.agents/skills/plan-task/SKILL.en.md +3 -0
  62. package/templates/.agents/skills/plan-task/SKILL.zh-CN.md +3 -0
  63. package/templates/.agents/skills/plan-task/config/verify.json +2 -0
  64. package/templates/.agents/skills/refine-task/SKILL.en.md +15 -1
  65. package/templates/.agents/skills/refine-task/SKILL.zh-CN.md +15 -1
  66. package/templates/.agents/skills/refine-task/config/verify.json +2 -0
  67. package/templates/.agents/skills/refine-task/reference/fix-workflow.en.md +9 -0
  68. package/templates/.agents/skills/refine-task/reference/fix-workflow.zh-CN.md +9 -0
  69. package/templates/.agents/skills/refine-task/reference/report-template.en.md +11 -0
  70. package/templates/.agents/skills/refine-task/reference/report-template.zh-CN.md +11 -0
  71. package/templates/.agents/skills/restore-task/SKILL.en.md +3 -0
  72. package/templates/.agents/skills/restore-task/SKILL.zh-CN.md +3 -0
  73. package/templates/.agents/skills/restore-task/config/verify.json +1 -0
  74. package/templates/.agents/skills/review-task/SKILL.en.md +16 -1
  75. package/templates/.agents/skills/review-task/SKILL.zh-CN.md +16 -1
  76. package/templates/.agents/skills/review-task/config/verify.json +3 -0
  77. package/templates/.agents/skills/review-task/reference/output-templates.en.md +20 -5
  78. package/templates/.agents/skills/review-task/reference/output-templates.zh-CN.md +20 -5
  79. package/templates/.agents/skills/review-task/reference/report-template.en.md +13 -0
  80. package/templates/.agents/skills/review-task/reference/report-template.zh-CN.md +13 -0
  81. package/templates/.agents/skills/review-task/reference/review-criteria.en.md +18 -0
  82. package/templates/.agents/skills/review-task/reference/review-criteria.zh-CN.md +18 -0
  83. package/templates/.agents/skills/update-agent-infra/scripts/sync-templates.js +4 -3
  84. package/templates/.agents/templates/task.en.md +5 -0
  85. package/templates/.agents/templates/task.zh-CN.md +5 -0
  86. package/templates/.claude/settings.json +1 -1
  87. package/templates/.codex/hooks.json +17 -0
@@ -0,0 +1,155 @@
1
+ # Issue 字段
2
+
3
+ 写入或校验 Issue Type pinned custom fields 前先读取本文件。
4
+
5
+ ## 边界
6
+
7
+ - 仅在已知 `upstream_repo`、`has_push` 和 Issue 编号后使用本规则。
8
+ - 如果 `has_push=false`,跳过直接字段写入并继续流程。
9
+ - 每次写入前都读取组织当前 Issue Type schema;不要硬编码字段集合。
10
+ - 缺失、空值或无法解析的值直接跳过。字段写入是 best-effort,不应阻断工作流。
11
+
12
+ ## 支持的 task.md frontmatter
13
+
14
+ 所有字段均可选:
15
+
16
+ | task.md 字段 | Issue 字段 | 值格式 |
17
+ |---|---|---|
18
+ | `priority` | `Priority` | `Urgent`、`High`、`Medium` 或 `Low` |
19
+ | `effort` | `Effort` | `High`、`Medium` 或 `Low` |
20
+ | `start_date` | `Start date` | `YYYY-MM-DD` |
21
+ | `target_date` | `Target date` | `YYYY-MM-DD` |
22
+
23
+ 写入前可规范化本地化选项:
24
+
25
+ | 输入 | 写入选项 |
26
+ |---|---|
27
+ | `紧急` | `Urgent` |
28
+ | `高` | `High` |
29
+ | `中` | `Medium` |
30
+ | `低` | `Low` |
31
+
32
+ AI agent 在创建或修订任务时可根据标题与描述推断 `priority` 和 `effort`,但除非用户或来源 Issue 明确提供日期,否则必须保持日期字段为空。`task.md` 中人工填写的值优先。
33
+
34
+ ## GraphQL 参考
35
+
36
+ 读取 Issue Type pinned fields:
37
+
38
+ ```graphql
39
+ query($owner:String!){
40
+ organization(login:$owner){ issueTypes(first:20){ nodes{
41
+ id name
42
+ pinnedFields{
43
+ __typename
44
+ ... on IssueFieldSingleSelect{ id name options{ id name } }
45
+ ... on IssueFieldDate{ id name }
46
+ ... on IssueFieldText{ id name }
47
+ ... on IssueFieldNumber{ id name }
48
+ }
49
+ } } }
50
+ }
51
+ ```
52
+
53
+ 读取单个 Issue 的当前 type 与字段值:
54
+
55
+ ```graphql
56
+ query($owner:String!,$name:String!,$number:Int!){
57
+ repository(owner:$owner,name:$name){ issue(number:$number){
58
+ id
59
+ issueType{ name pinnedFields{
60
+ __typename
61
+ ... on IssueFieldSingleSelect{ id name options{ id name } }
62
+ ... on IssueFieldDate{ id name }
63
+ ... on IssueFieldText{ id name }
64
+ ... on IssueFieldNumber{ id name }
65
+ } }
66
+ issueFieldValues(first:50){ nodes{
67
+ __typename
68
+ ... on IssueFieldSingleSelectValue{ name optionId field{ ... on IssueFieldSingleSelect{ name } } }
69
+ ... on IssueFieldDateValue{ value field{ ... on IssueFieldDate{ name } } }
70
+ ... on IssueFieldTextValue{ value field{ ... on IssueFieldText{ name } } }
71
+ ... on IssueFieldNumberValue{ value field{ ... on IssueFieldNumber{ name } } }
72
+ } }
73
+ } }
74
+ }
75
+ ```
76
+
77
+ 写入或清空字段,以及更新 Issue Type:
78
+
79
+ ```graphql
80
+ mutation($issueId:ID!,$issueFields:[IssueFieldCreateOrUpdateInput!]!){
81
+ setIssueFieldValue(input:{issueId:$issueId,issueFields:$issueFields}){ issue{ id } }
82
+ }
83
+
84
+ mutation($issueId:ID!,$issueTypeId:ID){
85
+ updateIssueIssueType(input:{issueId:$issueId,issueTypeId:$issueTypeId}){ issue{ id } }
86
+ }
87
+ ```
88
+
89
+ `IssueFieldCreateOrUpdateInput` 支持 `fieldId`、`singleSelectOptionId`、`dateValue`、`textValue`、`numberValue` 和 `delete`。
90
+
91
+ 最小命令壳:
92
+
93
+ ```bash
94
+ gh api graphql \
95
+ -f query='query($owner:String!){organization(login:$owner){issueTypes(first:20){nodes{id name pinnedFields{__typename ... on IssueFieldSingleSelect{id name options{id name}} ... on IssueFieldDate{id name} ... on IssueFieldText{id name} ... on IssueFieldNumber{id name}}}}}}' \
96
+ -F owner="{owner}"
97
+
98
+ gh api graphql \
99
+ -f query='query($owner:String!,$name:String!,$number:Int!){repository(owner:$owner,name:$name){issue(number:$number){id issueType{name pinnedFields{__typename ... on IssueFieldSingleSelect{id name options{id name}} ... on IssueFieldDate{id name} ... on IssueFieldText{id name} ... on IssueFieldNumber{id name}}} issueFieldValues(first:50){nodes{__typename ... on IssueFieldSingleSelectValue{name optionId field{... on IssueFieldSingleSelect{name}}} ... on IssueFieldDateValue{value field{... on IssueFieldDate{name}}} ... on IssueFieldTextValue{value field{... on IssueFieldText{name}}} ... on IssueFieldNumberValue{value field{... on IssueFieldNumber{name}}}}}}}}' \
100
+ -F owner="{owner}" -F name="{repo}" -F number="{issue-number}"
101
+
102
+ gh api graphql --input - <<'JSON'
103
+ {
104
+ "query": "mutation($issueId:ID!,$issueFields:[IssueFieldCreateOrUpdateInput!]!){setIssueFieldValue(input:{issueId:$issueId,issueFields:$issueFields}){issue{id}}}",
105
+ "variables": {
106
+ "issueId": "{issue-id}",
107
+ "issueFields": [
108
+ { "fieldId": "{field-id}", "singleSelectOptionId": "{option-id}" },
109
+ { "fieldId": "{date-field-id}", "dateValue": "YYYY-MM-DD" },
110
+ { "fieldId": "{old-field-id}", "delete": true }
111
+ ]
112
+ }
113
+ }
114
+ JSON
115
+
116
+ gh api graphql --input - <<'JSON'
117
+ {
118
+ "query": "mutation($issueId:ID!,$issueTypeId:ID){updateIssueIssueType(input:{issueId:$issueId,issueTypeId:$issueTypeId}){issue{id}}}",
119
+ "variables": {
120
+ "issueId": "{issue-id}",
121
+ "issueTypeId": "{issue-type-id}"
122
+ }
123
+ }
124
+ JSON
125
+ ```
126
+
127
+ 未列入本地化映射表的值会按字面量 option name 处理;这是为了支持规范英文输入。
128
+
129
+ ## 流程 A:创建 Issue 后写入字段
130
+
131
+ 1. 如果 `has_push` 不是 `true`,停止本流程。
132
+ 2. 从 `$upstream_repo` 解析 `{owner}`,查询 `organization.issueTypes`。
133
+ 3. 选择目标 Issue Type 的 `pinnedFields`。
134
+ 4. 从 `task.md` 读取非空的 `priority`、`effort`、`start_date` 和 `target_date`。
135
+ 5. 对每个值:
136
+ - 目标 type 未 pin 同名字段时跳过。
137
+ - single-select 字段先规范化本地化输入,再按 option name 匹配。
138
+ - date 字段只写入 `YYYY-MM-DD` 值。
139
+ 6. 用所有已解析 input 一次性提交 `setIssueFieldValue` mutation。没有 input 时跳过。
140
+
141
+ ## 流程 B:设置 Type 并迁移字段
142
+
143
+ 现有 Issue Type 发生变更时使用本流程。
144
+
145
+ 1. 如果 `has_push` 不是 `true`,停止本流程。
146
+ 2. 读取 Issue id、当前 Issue Type、pinned fields 和当前字段值。
147
+ 3. 查询组织 Issue Type 列表,解析目标 Issue Type id。
148
+ 4. 用目标 Issue Type id 执行 `updateIssueIssueType`。
149
+ 5. 解析目标 type 的 pinned fields。
150
+ 6. 对每个旧字段值:
151
+ - 目标 type 有同名字段时重新写入该值。single-select 值按 option name 在目标 type 中重新解析 option id。
152
+ - 目标 type 没有同名字段时,对旧字段发送 `{ fieldId, delete: true }`。
153
+ 7. 用所有迁移 input 一次性提交 `setIssueFieldValue` mutation。迁移 input 为空时跳过。
154
+
155
+ 两个流程均应保持幂等。重复写入未变化的值或删除已为空字段都是可接受的。
@@ -88,6 +88,7 @@ gh api "repos/$upstream_repo/issues/{issue-number}" -X PATCH -f type="{issue-typ
88
88
  ```
89
89
 
90
90
  - set the Issue Type only when `has_push=true`; otherwise skip and continue
91
+ - when changing an existing Issue Type, read `.agents/rules/issue-fields.md` and use Flow B so same-name pinned fields are migrated and fields absent from the new type are cleared
91
92
 
92
93
  ## Update Issues
93
94
 
@@ -88,6 +88,7 @@ gh api "repos/$upstream_repo/issues/{issue-number}" -X PATCH -f type="{issue-typ
88
88
  ```
89
89
 
90
90
  - 仅当 `has_push=true` 时执行 Issue Type 设置;否则跳过并继续
91
+ - 变更现有 Issue Type 时,先读取 `.agents/rules/issue-fields.md` 并使用流程 B,确保同名 pinned fields 迁移,且新 type 不包含的字段被清空
91
92
 
92
93
  ## Issue 更新
93
94
 
@@ -48,6 +48,7 @@ Operation-to-permission mapping:
48
48
  | add/remove milestones | `has_triage` | same as above |
49
49
  | edit Issue body | `has_triage` | used by requirement checkbox sync |
50
50
  | set Issue Type | `has_push` | requires write permission |
51
+ | set Issue fields | `has_push` | pinned custom fields; failures are non-blocking |
51
52
  | set assignee | no check | skip directly when it fails |
52
53
  | publish/update comments | no check | allowed for authenticated users in public repositories |
53
54
 
@@ -55,7 +56,7 @@ Operation-to-permission mapping:
55
56
 
56
57
  | Level | Operation type | With permission | Without permission |
57
58
  |------|---------|--------|--------|
58
- | silent degradation | label / milestone / Issue Type | run the `gh` command directly and also update the task comment | skip direct `gh` writes, update only the task comment, let the bot backfill |
59
+ | silent degradation | label / milestone / Issue Type / Issue fields | run the `gh` command directly and also update the task comment | skip direct `gh` writes, update only the task comment, let the bot backfill |
59
60
  | direct skip | assignee | run the `gh` command directly | do nothing else |
60
61
  | normal execution | comments | run normally | run normally |
61
62
 
@@ -48,6 +48,7 @@ has_push=$(printf '%s' "$repo_perms" | grep -q '"push":true' 2>/dev/null && echo
48
48
  | 设置/移除 milestone | `has_triage` | 同上 |
49
49
  | 编辑 Issue body | `has_triage` | 需求复选框同步使用 |
50
50
  | 设置 Issue Type | `has_push` | 需要 write 权限 |
51
+ | 设置 Issue 字段 | `has_push` | pinned custom fields;失败不阻断 |
51
52
  | 设置 assignee | 不检测 | 无权限时直接跳过 |
52
53
  | 发布/更新评论 | 无需检测 | 公开仓库中认证用户可执行 |
53
54
 
@@ -55,7 +56,7 @@ has_push=$(printf '%s' "$repo_perms" | grep -q '"push":true' 2>/dev/null && echo
55
56
 
56
57
  | 层级 | 操作类型 | 有权限 | 无权限 |
57
58
  |------|---------|--------|--------|
58
- | 静默降级 | label / milestone / Issue Type | 直接执行 `gh` 命令,同时更新 task 留言 | 跳过 `gh` 直接操作,仅更新 task 留言,由 bot 补位 |
59
+ | 静默降级 | label / milestone / Issue Type / Issue 字段 | 直接执行 `gh` 命令,同时更新 task 留言 | 跳过 `gh` 直接操作,仅更新 task 留言,由 bot 补位 |
59
60
  | 直接跳过 | assignee | 直接执行 `gh` 命令 | 不做任何替代 |
60
61
  | 正常执行 | 评论 | 正常执行 | 正常执行 |
61
62
 
@@ -13,16 +13,24 @@ Map user intent to the corresponding workflow command:
13
13
  ## Task State Management
14
14
 
15
15
  - Update the corresponding `task.md` immediately after every workflow command
16
- - At minimum, synchronize `current_step`, `updated_at`, `assigned_to`, and the current-round artifact reference
16
+ - At minimum, synchronize `current_step`, `updated_at`, `assigned_to`, `agent_infra_version`, and the current-round artifact reference
17
+ - Before updating `agent_infra_version`, read `.agents/rules/version-stamp.md`
17
18
  - Activity Log entries are append-only and must never overwrite history
18
19
 
19
20
  ## Required State Updates by Command
20
21
 
21
- - `import-issue`: update `current_step`, `updated_at`, `assigned_to`
22
- - `analyze-task`: update `current_step`, `updated_at`, `assigned_to`
23
- - `plan-task`: update `current_step`, `updated_at`
24
- - `implement-task`: update `current_step`, `updated_at`
25
- - `review-task`: update `current_step`, `updated_at`
26
- - `refine-task`: update `current_step`, `updated_at`
27
- - `complete-task`: update `status`, `completed_at`, `updated_at`
28
- - `block-task`: update `status`, `blocked_at`, `blocked_reason`
22
+ - `create-task`: create `branch`, `workflow`, `status`, `created_at`, `updated_at`, `assigned_to`, `agent_infra_version`
23
+ - `import-issue`: update `current_step`, `updated_at`, `assigned_to`, `agent_infra_version`
24
+ - `import-codescan`: update `current_step`, `updated_at`, `assigned_to`, `agent_infra_version`
25
+ - `import-dependabot`: update `current_step`, `updated_at`, `assigned_to`, `agent_infra_version`
26
+ - `restore-task`: update `status`, `updated_at`, `assigned_to`, `agent_infra_version`
27
+ - `analyze-task`: update `current_step`, `updated_at`, `assigned_to`, `agent_infra_version`
28
+ - `plan-task`: update `current_step`, `updated_at`, `agent_infra_version`
29
+ - `implement-task`: update `current_step`, `updated_at`, `agent_infra_version`
30
+ - `review-task`: update `current_step`, `updated_at`, `agent_infra_version`
31
+ - `refine-task`: update `current_step`, `updated_at`, `agent_infra_version`
32
+ - `create-pr`: update `pr_number`, `updated_at`, `agent_infra_version`
33
+ - `commit`: update `updated_at`, `agent_infra_version`; update `current_step` when needed (see `commit/reference/task-status-update.md`)
34
+ - `complete-task`: update `status`, `current_step`, `completed_at`, `updated_at`, `agent_infra_version`
35
+ - `block-task`: update `status`, `blocked_at`, `blocked_reason`, `updated_at`, `agent_infra_version`
36
+ - `cancel-task`: update `status`, `cancelled_at`, `cancel_reason`, `updated_at`, `agent_infra_version`
@@ -13,16 +13,24 @@
13
13
  ## 任务状态管理
14
14
 
15
15
  - 每次执行工作流命令后,必须立即更新对应任务的 `task.md`
16
- - 至少同步 `current_step`、`updated_at`、`assigned_to`,以及本轮产物引用
16
+ - 至少同步 `current_step`、`updated_at`、`assigned_to`、`agent_infra_version`,以及本轮产物引用
17
+ - 更新 `agent_infra_version` 前,先读取 `.agents/rules/version-stamp.md`
17
18
  - Activity Log 只能追加,不能覆盖历史记录
18
19
 
19
20
  ## 常见命令的状态更新要求
20
21
 
21
- - `import-issue`:更新 `current_step`、`updated_at`、`assigned_to`
22
- - `analyze-task`:更新 `current_step`、`updated_at`、`assigned_to`
23
- - `plan-task`:更新 `current_step`、`updated_at`
24
- - `implement-task`:更新 `current_step`、`updated_at`
25
- - `review-task`:更新 `current_step`、`updated_at`
26
- - `refine-task`:更新 `current_step`、`updated_at`
27
- - `complete-task`:更新 `status`、`completed_at`、`updated_at`
28
- - `block-task`:更新 `status`、`blocked_at`、`blocked_reason`
22
+ - `create-task`:创建 `branch`、`workflow`、`status`、`created_at`、`updated_at`、`assigned_to`、`agent_infra_version`
23
+ - `import-issue`:更新 `current_step`、`updated_at`、`assigned_to`、`agent_infra_version`
24
+ - `import-codescan`:更新 `current_step`、`updated_at`、`assigned_to`、`agent_infra_version`
25
+ - `import-dependabot`:更新 `current_step`、`updated_at`、`assigned_to`、`agent_infra_version`
26
+ - `restore-task`:更新 `status`、`updated_at`、`assigned_to`、`agent_infra_version`
27
+ - `analyze-task`:更新 `current_step`、`updated_at`、`assigned_to`、`agent_infra_version`
28
+ - `plan-task`:更新 `current_step`、`updated_at`、`agent_infra_version`
29
+ - `implement-task`:更新 `current_step`、`updated_at`、`agent_infra_version`
30
+ - `review-task`:更新 `current_step`、`updated_at`、`agent_infra_version`
31
+ - `refine-task`:更新 `current_step`、`updated_at`、`agent_infra_version`
32
+ - `create-pr`:更新 `pr_number`、`updated_at`、`agent_infra_version`
33
+ - `commit`:更新 `updated_at`、`agent_infra_version`;必要时更新 `current_step`(详见 `commit/reference/task-status-update.md`)
34
+ - `complete-task`:更新 `status`、`current_step`、`completed_at`、`updated_at`、`agent_infra_version`
35
+ - `block-task`:更新 `status`、`blocked_at`、`blocked_reason`、`updated_at`、`agent_infra_version`
36
+ - `cancel-task`:更新 `status`、`cancelled_at`、`cancel_reason`、`updated_at`、`agent_infra_version`
@@ -0,0 +1,40 @@
1
+ # Common Rule - Testing Discipline
2
+
3
+ > This file carries detailed examples for test-writing discipline. AGENTS.md (and CLAUDE.md) keep only concise testing rules and point here to avoid inflating high-frequency context.
4
+
5
+ ## Background
6
+
7
+ A batch of fragile keyword-matching assertions once had to be replaced with structural checks (valid frontmatter, step numbering, reference integrity, zh-CN variants, and size thresholds). Lesson: binding tests to natural-language wording, or using assertions to "remember a deleted concept", creates endless test debt.
8
+
9
+ ## Example: do not add negative assertions when a positive assertion already covers the behavior
10
+
11
+ When a positive assertion already covers the expected behavior, do not add another negative assertion for "the opposite should not appear".
12
+
13
+ Bad:
14
+ ```ts
15
+ assert.match(content, /^name: implement-task$/m); // The positive assertion already covers the expected value.
16
+ assert.doesNotMatch(content, /^name: wrong-name$/m); // Redundant: permanently remembers a value that should not appear.
17
+ ```
18
+
19
+ Good:
20
+ ```ts
21
+ assert.match(content, /^name: implement-task$/m); // The positive assertion is enough.
22
+ ```
23
+
24
+ If the positive assertion passes, the value is correct. The extra negative assertion adds no protection, only maintenance cost, and can become a test that permanently remembers a concept after the feature is gone.
25
+
26
+ ## RED-GREEN-REFACTOR rhythm
27
+
28
+ During implementation, turn the requirement into a test for observable behavior before writing the code:
29
+
30
+ 1. **RED**: First write a failing test that reproduces the requirement or defect, and confirm that it really fails. The test should cover business behavior, inputs and outputs, or user-visible results, not internal implementation details.
31
+ 2. **GREEN**: Write the smallest amount of code needed to make the failing test pass. Do not expand behavior that is not covered by the test or the requirement.
32
+ 3. **REFACTOR**: After the tests are green, clean up names, structure, or duplication; keep the same test set passing before and after the refactor.
33
+
34
+ This mirrors "Goal-Driven Execution" in AGENTS.md: define a verifiable success criterion first, then make the implementation satisfy it.
35
+
36
+ ## Test anti-patterns
37
+
38
+ - **Over-mocking**: Stub only real boundaries such as network, filesystem, time, or randomness; do not mock the logic of the unit under test, or the test only proves that the mock followed the script.
39
+ - **Testing implementation details**: Prefer assertions on public APIs, artifacts, state changes, or error results; avoid assertions on private functions, internal call order, or temporary data structures.
40
+ - **Insufficient assertions**: Assertions must pin down concrete expected values; do not replace checks on key fields, counts, and boundaries with "does not throw" or "result exists".
@@ -0,0 +1,40 @@
1
+ # 通用规则 - 测试编写纪律
2
+
3
+ > 本文件承载测试编写的正反例细节;AGENTS.md(及 CLAUDE.md)的「测试编写规约」只保留精简条目并指向此处,避免高频上下文膨胀。
4
+
5
+ ## 背景
6
+
7
+ 曾有一批脆弱的关键词匹配断言需要整体替换为结构性检查(frontmatter 合法性、步骤编号、引用完整性、zh-CN 变体、体积阈值)。教训:绑定自然语言措辞、或用断言"记住一个已删除概念",都会形成无止境的测试债务。
8
+
9
+ ## 正反例:正向断言已覆盖时,不应再加反向断言
10
+
11
+ 当正向断言已覆盖期望行为,就不要再为"反面不应出现"补一条反向断言。
12
+
13
+ ❌ 反例:
14
+ ```ts
15
+ assert.match(content, /^name: implement-task$/m); // 正向已覆盖期望值
16
+ assert.doesNotMatch(content, /^name: wrong-name$/m); // 多余:永久记住一个不该出现的值
17
+ ```
18
+
19
+ ✅ 正例:
20
+ ```ts
21
+ assert.match(content, /^name: implement-task$/m); // 正向断言已足够
22
+ ```
23
+
24
+ 正向断言通过即证明值正确;额外的反向断言不增加保护,只增加维护成本,并会在功能删除后退化为"测试永久记住一个不再存在的概念"。
25
+
26
+ ## RED-GREEN-REFACTOR 节奏
27
+
28
+ 实现阶段先把需求转成可观察行为的测试,再写代码:
29
+
30
+ 1. **RED**:先写一个能复现需求或缺陷的失败用例,并确认它确实失败。测试应覆盖业务行为、输入输出或用户可见结果,不绑定内部实现细节。
31
+ 2. **GREEN**:写最少代码让失败用例通过。不要顺手扩展未被测试和未被需求覆盖的行为。
32
+ 3. **REFACTOR**:在测试全绿后整理命名、结构或重复代码;重构前后保持同一组测试通过。
33
+
34
+ 这与 AGENTS.md 的「目标驱动执行」一致:先定义可验证成功标准,再让实现满足它。
35
+
36
+ ## 测试反模式
37
+
38
+ - **mock 过度**:只在网络、文件系统、时间、随机数等真实边界打桩;不要 mock 被测对象自身逻辑,否则测试只验证 mock 是否按预设运行。
39
+ - **测试实现细节**:优先断言公开接口、产物、状态变化或错误结果;避免断言私有函数、内部调用顺序、临时数据结构。
40
+ - **断言不充分**:断言必须锁定具体期望值;不要用"只要不抛异常""结果存在即可"替代对关键字段、数量和边界的验证。
@@ -0,0 +1,29 @@
1
+ # General Rule - agent-infra Version Stamp
2
+
3
+ ## When to Write
4
+
5
+ Every time a workflow creates or updates `task.md` frontmatter, also write `agent_infra_version`.
6
+
7
+ This field records the `agent-infra` CLI version that last wrote the task metadata, and is refreshed together with `updated_at`.
8
+
9
+ ## Value Command
10
+
11
+ ```bash
12
+ agent_infra_version=$(ai version --raw 2>/dev/null || echo "unknown")
13
+ ```
14
+
15
+ - On success, write the command output directly, for example `vX.Y.Z` or `vX.Y.Z-alpha.0`
16
+ - Do not add the `v` prefix in the writer
17
+ - If the command fails, write `unknown`
18
+
19
+ ## Frontmatter Field
20
+
21
+ ```yaml
22
+ agent_infra_version: {agent_infra_version}
23
+ ```
24
+
25
+ ## Compatibility
26
+
27
+ - Historical tasks may not have this field; reading or restoring tasks must not block on that alone
28
+ - When present, the value must be `vX.Y.Z`, `vX.Y.Z-prerelease`, a version with SemVer build metadata, or `unknown`
29
+ - Issue / PR comment sync does not need special handling; frontmatter mirroring naturally includes this field
@@ -0,0 +1,29 @@
1
+ # 通用规则 - agent-infra 版本戳
2
+
3
+ ## 写入时机
4
+
5
+ 每次创建或更新 `task.md` frontmatter 时,同步写入 `agent_infra_version`。
6
+
7
+ 该字段表示最后一次写入该任务元数据的 `agent-infra` CLI 版本,与 `updated_at` 同步刷新。
8
+
9
+ ## 取值命令
10
+
11
+ ```bash
12
+ agent_infra_version=$(ai version --raw 2>/dev/null || echo "unknown")
13
+ ```
14
+
15
+ - 命令成功时,值必须直接使用输出结果,例如 `vX.Y.Z` 或 `vX.Y.Z-alpha.0`
16
+ - 不要在写入端自行拼接 `v` 前缀
17
+ - 命令失败时写入 `unknown`
18
+
19
+ ## frontmatter 字段
20
+
21
+ ```yaml
22
+ agent_infra_version: {agent_infra_version}
23
+ ```
24
+
25
+ ## 兼容性
26
+
27
+ - 历史任务可能缺少该字段;读取或恢复任务时不得因此阻塞
28
+ - 字段存在时,值必须是 `vX.Y.Z`、`vX.Y.Z-prerelease`、带 SemVer build 元数据的版本,或 `unknown`
29
+ - 同步 Issue / PR 评论时无需额外处理;frontmatter 镜像会自然包含该字段
@@ -5,6 +5,18 @@ import spawn from "cross-spawn";
5
5
 
6
6
  const CHECK_TYPE = "platform-sync";
7
7
  const DEFAULT_RETRY_DELAYS_MS = [3000, 10000];
8
+ const FRONTMATTER_FIELD_MAP = {
9
+ priority: "Priority",
10
+ effort: "Effort",
11
+ start_date: "Start date",
12
+ target_date: "Target date"
13
+ };
14
+ const OPTION_LOCALIZATION = {
15
+ "紧急": "Urgent",
16
+ "高": "High",
17
+ "中": "Medium",
18
+ "低": "Low"
19
+ };
8
20
 
9
21
  let activeShared = null;
10
22
  let repoRoot = "";
@@ -107,6 +119,7 @@ export function check({ taskDir, config, artifactFile }, shared) {
107
119
  checkPrAssignee,
108
120
  checkSyncedRequirements,
109
121
  checkIssueType,
122
+ checkIssueFields,
110
123
  checkMilestone
111
124
  ];
112
125
 
@@ -326,6 +339,27 @@ function fetchRemoteData(context) {
326
339
  }
327
340
  }
328
341
 
342
+ let issueFields;
343
+ if (context.config.verify_issue_fields && context.hasPush) {
344
+ const [owner, name] = context.upstreamRepo.split("/");
345
+ const issueFieldsResult = withRetry(() => ghJson([
346
+ "api",
347
+ "graphql",
348
+ "-f",
349
+ `query=${ISSUE_FIELDS_QUERY}`,
350
+ "-F",
351
+ `owner=${owner}`,
352
+ "-F",
353
+ `name=${name}`,
354
+ "-F",
355
+ `number=${context.issueNumber}`
356
+ ], context.taskDir));
357
+
358
+ if (issueFieldsResult.ok) {
359
+ issueFields = normalizeIssueFields(issueFieldsResult.value);
360
+ }
361
+ }
362
+
329
363
  let prLabels = null;
330
364
  let prMilestone;
331
365
  let prAssignees;
@@ -377,6 +411,7 @@ function fetchRemoteData(context) {
377
411
  prComments,
378
412
  prLabels,
379
413
  issueType,
414
+ issueFields,
380
415
  prMilestone,
381
416
  prAssignees
382
417
  };
@@ -703,6 +738,45 @@ function checkIssueType(context, remoteData) {
703
738
  return null;
704
739
  }
705
740
 
741
+ function checkIssueFields(context, remoteData) {
742
+ if (!context.config.verify_issue_fields || !context.hasPush) {
743
+ return null;
744
+ }
745
+
746
+ if (remoteData.issueFields === undefined) {
747
+ return null;
748
+ }
749
+
750
+ for (const [metadataKey, fieldName] of Object.entries(FRONTMATTER_FIELD_MAP)) {
751
+ const expectedRaw = context.task.metadata[metadataKey];
752
+ if (isBlank(expectedRaw) || !remoteData.issueFields.pinnedNames.has(fieldName)) {
753
+ continue;
754
+ }
755
+
756
+ const actual = remoteData.issueFields.values.get(fieldName);
757
+ const expected = normalizeExpectedIssueField(metadataKey, expectedRaw);
758
+ if (!expected) {
759
+ continue;
760
+ }
761
+
762
+ if (!actual) {
763
+ return failResult(CHECK_TYPE,
764
+ `Issue #${context.issueNumber} field '${fieldName}' is missing, expected '${expected.value}'`,
765
+ "check_failed"
766
+ );
767
+ }
768
+
769
+ if (actual.kind !== expected.kind || actual.value !== expected.value) {
770
+ return failResult(CHECK_TYPE,
771
+ `Issue #${context.issueNumber} field '${fieldName}' is '${actual.value}', expected '${expected.value}'`,
772
+ "check_failed"
773
+ );
774
+ }
775
+ }
776
+
777
+ return null;
778
+ }
779
+
706
780
  function checkPrAssignee(context, remoteData) {
707
781
  if (!context.config.verify_pr_assignee || !context.hasPush || !context.prNumber) {
708
782
  return null;
@@ -886,6 +960,69 @@ function mapTaskTypeToIssueType(taskType) {
886
960
  return mapping[taskType] || "Task";
887
961
  }
888
962
 
963
+ const ISSUE_FIELDS_QUERY = `query($owner:String!,$name:String!,$number:Int!){repository(owner:$owner,name:$name){issue(number:$number){issueType{name pinnedFields{__typename ... on IssueFieldSingleSelect{id name} ... on IssueFieldDate{id name} ... on IssueFieldText{id name} ... on IssueFieldNumber{id name}}} issueFieldValues(first:50){nodes{__typename ... on IssueFieldSingleSelectValue{name optionId field{... on IssueFieldSingleSelect{name}}} ... on IssueFieldDateValue{value field{... on IssueFieldDate{name}}} ... on IssueFieldTextValue{value field{... on IssueFieldText{name}}} ... on IssueFieldNumberValue{value field{... on IssueFieldNumber{name}}}}}}}}`;
964
+
965
+ function normalizeIssueFields(payload) {
966
+ const issue = payload?.data?.repository?.issue;
967
+ const pinnedFields = Array.isArray(issue?.issueType?.pinnedFields)
968
+ ? issue.issueType.pinnedFields
969
+ : [];
970
+ const values = Array.isArray(issue?.issueFieldValues?.nodes)
971
+ ? issue.issueFieldValues.nodes
972
+ : [];
973
+ const pinnedNames = new Set(
974
+ pinnedFields
975
+ .map((field) => typeof field?.name === "string" ? field.name : "")
976
+ .filter(Boolean)
977
+ );
978
+ const normalizedValues = new Map();
979
+
980
+ for (const value of values) {
981
+ const fieldName = value?.field?.name;
982
+ if (!fieldName) {
983
+ continue;
984
+ }
985
+
986
+ if (value.__typename === "IssueFieldSingleSelectValue") {
987
+ normalizedValues.set(fieldName, {
988
+ kind: "single-select",
989
+ value: normalizeOptionName(value.name)
990
+ });
991
+ } else if (value.__typename === "IssueFieldDateValue") {
992
+ normalizedValues.set(fieldName, {
993
+ kind: "date",
994
+ value: normalizeDateValue(value.value)
995
+ });
996
+ }
997
+ }
998
+
999
+ return { pinnedNames, values: normalizedValues };
1000
+ }
1001
+
1002
+ function normalizeExpectedIssueField(metadataKey, rawValue) {
1003
+ const value = String(rawValue || "").trim();
1004
+ if (!value) {
1005
+ return null;
1006
+ }
1007
+
1008
+ if (metadataKey === "start_date" || metadataKey === "target_date") {
1009
+ return { kind: "date", value: normalizeDateValue(value) };
1010
+ }
1011
+
1012
+ return { kind: "single-select", value: normalizeOptionName(value) };
1013
+ }
1014
+
1015
+ function normalizeOptionName(value) {
1016
+ const normalized = String(value || "").trim();
1017
+ return OPTION_LOCALIZATION[normalized] || normalized;
1018
+ }
1019
+
1020
+ function normalizeDateValue(value) {
1021
+ const normalized = String(value || "").trim();
1022
+ const match = normalized.match(/^\d{4}-\d{2}-\d{2}/);
1023
+ return match ? match[0] : normalized;
1024
+ }
1025
+
889
1026
  function arraysEqual(left, right) {
890
1027
  if (left.length !== right.length) {
891
1028
  return false;
@@ -928,14 +1065,14 @@ function computeExpectedInLabels(taskDir) {
928
1065
  return { ok: true, labels: Array.from(labels).sort(), mode: "mapped" };
929
1066
  }
930
1067
 
931
- const repoLabelsResult = ghJson([
1068
+ const repoLabelsResult = withRetry(() => ghJson([
932
1069
  "label",
933
1070
  "list",
934
1071
  "--limit",
935
1072
  "200",
936
1073
  "--json",
937
1074
  "name"
938
- ], taskDir);
1075
+ ], taskDir));
939
1076
  if (!repoLabelsResult.ok) {
940
1077
  return repoLabelsResult;
941
1078
  }
@@ -1015,10 +1152,10 @@ function resolveUpstreamRepo(taskDir) {
1015
1152
  return ownerRepo;
1016
1153
  }
1017
1154
 
1018
- const repoResult = ghJson([
1155
+ const repoResult = withRetry(() => ghJson([
1019
1156
  "api",
1020
1157
  `repos/${ownerRepo.value}`
1021
- ], taskDir);
1158
+ ], taskDir));
1022
1159
 
1023
1160
  if (!repoResult.ok) {
1024
1161
  return repoResult;
@@ -1053,12 +1190,12 @@ function resolveOwnerRepo(taskDir) {
1053
1190
  }
1054
1191
 
1055
1192
  function detectPermissions(upstreamRepo, taskDir) {
1056
- const permissionsResult = ghJson([
1193
+ const permissionsResult = withRetry(() => ghJson([
1057
1194
  "api",
1058
1195
  `repos/${upstreamRepo}`,
1059
1196
  "--jq",
1060
1197
  ".permissions"
1061
- ], taskDir);
1198
+ ], taskDir));
1062
1199
 
1063
1200
  if (!permissionsResult.ok) {
1064
1201
  return { hasTriage: false, hasPush: false };