@fitlab-ai/agent-infra 0.4.5 → 0.5.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.
- package/README.md +18 -2
- package/README.zh-CN.md +18 -2
- package/bin/cli.js +19 -0
- package/lib/defaults.json +17 -0
- package/lib/init.js +1 -0
- package/lib/log.js +5 -10
- package/lib/merge.js +885 -0
- package/lib/sandbox/commands/create.js +1170 -0
- package/lib/sandbox/commands/enter.js +64 -0
- package/lib/sandbox/commands/ls.js +71 -0
- package/lib/sandbox/commands/rebuild.js +102 -0
- package/lib/sandbox/commands/rm.js +211 -0
- package/lib/sandbox/commands/vm.js +101 -0
- package/lib/sandbox/config.js +79 -0
- package/lib/sandbox/constants.js +113 -0
- package/lib/sandbox/dockerfile.js +95 -0
- package/lib/sandbox/engine.js +93 -0
- package/lib/sandbox/index.js +64 -0
- package/lib/sandbox/runtimes/ai-tools.dockerfile +26 -0
- package/lib/sandbox/runtimes/base.dockerfile +30 -0
- package/lib/sandbox/runtimes/java17.dockerfile +3 -0
- package/lib/sandbox/runtimes/java21.dockerfile +3 -0
- package/lib/sandbox/runtimes/node20.dockerfile +3 -0
- package/lib/sandbox/runtimes/node22.dockerfile +3 -0
- package/lib/sandbox/runtimes/python3.dockerfile +3 -0
- package/lib/sandbox/shell.js +48 -0
- package/lib/sandbox/task-resolver.js +35 -0
- package/lib/sandbox/tools.js +135 -0
- package/lib/update.js +16 -2
- package/package.json +5 -1
- package/templates/.agents/rules/pr-sync.md +110 -0
- package/templates/.agents/rules/pr-sync.zh-CN.md +110 -0
- package/templates/.agents/scripts/validate-artifact.js +117 -1
- package/templates/.agents/skills/archive-tasks/SKILL.md +6 -3
- package/templates/.agents/skills/archive-tasks/SKILL.zh-CN.md +6 -3
- package/templates/.agents/skills/archive-tasks/scripts/archive-tasks.sh +91 -8
- package/templates/.agents/skills/commit/SKILL.md +9 -1
- package/templates/.agents/skills/commit/SKILL.zh-CN.md +9 -1
- package/templates/.agents/skills/commit/config/verify.json +5 -1
- package/templates/.agents/skills/commit/reference/pr-summary-sync.md +21 -0
- package/templates/.agents/skills/commit/reference/pr-summary-sync.zh-CN.md +21 -0
- package/templates/.agents/skills/commit/reference/task-status-update.md +2 -0
- package/templates/.agents/skills/commit/reference/task-status-update.zh-CN.md +2 -0
- package/templates/.agents/skills/create-pr/SKILL.md +2 -1
- package/templates/.agents/skills/create-pr/SKILL.zh-CN.md +2 -1
- package/templates/.agents/skills/create-pr/reference/comment-publish.md +7 -74
- package/templates/.agents/skills/create-pr/reference/comment-publish.zh-CN.md +6 -73
- package/templates/.agents/skills/create-task/SKILL.md +6 -0
- package/templates/.agents/skills/create-task/SKILL.zh-CN.md +6 -0
- package/templates/.agents/skills/create-task/config/verify.json +1 -0
- package/templates/.agents/skills/import-issue/SKILL.md +2 -0
- package/templates/.agents/skills/import-issue/SKILL.zh-CN.md +2 -0
- package/templates/.agents/skills/import-issue/config/verify.json +1 -0
- package/templates/.agents/skills/update-agent-infra/scripts/sync-templates.js +18 -1
- package/templates/.agents/templates/task.md +5 -4
- package/templates/.agents/templates/task.zh-CN.md +5 -4
package/lib/update.js
CHANGED
|
@@ -139,9 +139,15 @@ async function cmdUpdate() {
|
|
|
139
139
|
// sync file registry
|
|
140
140
|
const { added, changed } = syncFileRegistry(config);
|
|
141
141
|
const hasNewEntries = added.managed.length > 0 || added.merged.length > 0;
|
|
142
|
+
const sandboxAdded = !config.sandbox;
|
|
142
143
|
const labelsAdded = !config.labels;
|
|
143
144
|
let configChanged = changed;
|
|
144
145
|
|
|
146
|
+
if (sandboxAdded) {
|
|
147
|
+
config.sandbox = structuredClone(defaults.sandbox);
|
|
148
|
+
configChanged = true;
|
|
149
|
+
}
|
|
150
|
+
|
|
145
151
|
if (labelsAdded) {
|
|
146
152
|
config.labels = structuredClone(defaults.labels);
|
|
147
153
|
configChanged = true;
|
|
@@ -157,11 +163,19 @@ async function cmdUpdate() {
|
|
|
157
163
|
for (const entry of added.merged) {
|
|
158
164
|
ok(` merged: ${entry}`);
|
|
159
165
|
}
|
|
160
|
-
} else if (labelsAdded) {
|
|
161
|
-
|
|
166
|
+
} else if (sandboxAdded || labelsAdded) {
|
|
167
|
+
if (sandboxAdded) {
|
|
168
|
+
info(`Default sandbox config added to ${CONFIG_PATH}.`);
|
|
169
|
+
}
|
|
170
|
+
if (labelsAdded) {
|
|
171
|
+
info(`Default labels.in config added to ${CONFIG_PATH}.`);
|
|
172
|
+
}
|
|
162
173
|
} else {
|
|
163
174
|
info(`File registry changed in ${CONFIG_PATH}.`);
|
|
164
175
|
}
|
|
176
|
+
if (hasNewEntries && sandboxAdded) {
|
|
177
|
+
info(`Default sandbox config added to ${CONFIG_PATH}.`);
|
|
178
|
+
}
|
|
165
179
|
if (hasNewEntries && labelsAdded) {
|
|
166
180
|
info(`Default labels.in config added to ${CONFIG_PATH}.`);
|
|
167
181
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@fitlab-ai/agent-infra",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.5.1",
|
|
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",
|
|
@@ -40,6 +40,10 @@
|
|
|
40
40
|
"bootstrap",
|
|
41
41
|
"installer"
|
|
42
42
|
],
|
|
43
|
+
"dependencies": {
|
|
44
|
+
"@clack/prompts": "1.2.0",
|
|
45
|
+
"picocolors": "1.1.1"
|
|
46
|
+
},
|
|
43
47
|
"scripts": {
|
|
44
48
|
"build": "node scripts/build-inline.js",
|
|
45
49
|
"prepare": "git config core.hooksPath .github/hooks || true",
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
# PR Summary Sync
|
|
2
|
+
|
|
3
|
+
Read this file before syncing the single reviewer-facing PR summary comment.
|
|
4
|
+
|
|
5
|
+
## Scope
|
|
6
|
+
|
|
7
|
+
Current callers:
|
|
8
|
+
- `create-pr`: create or update the summary comment when the PR is opened
|
|
9
|
+
- `commit`: refresh the summary comment on an existing PR when needed
|
|
10
|
+
|
|
11
|
+
If another skill needs to refresh the PR summary later, point that skill's reference file at this rule first, then document only the skill-specific trigger and failure behavior there.
|
|
12
|
+
|
|
13
|
+
## Hidden Marker
|
|
14
|
+
|
|
15
|
+
Use this hidden marker:
|
|
16
|
+
|
|
17
|
+
```html
|
|
18
|
+
<!-- sync-pr:{task-id}:summary -->
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
Within one PR, a given `{task-id}` must have only one summary comment with this marker.
|
|
22
|
+
|
|
23
|
+
## Aggregation Inputs
|
|
24
|
+
|
|
25
|
+
Aggregate the latest artifacts in the task directory:
|
|
26
|
+
- `plan.md` or the latest `plan-r{N}.md`
|
|
27
|
+
- `implementation.md` or the latest `implementation-r{N}.md`
|
|
28
|
+
- `review.md` or the latest `review-r{N}.md`
|
|
29
|
+
- `refinement.md` or the latest `refinement-r{N}.md`
|
|
30
|
+
|
|
31
|
+
Aggregation rules:
|
|
32
|
+
- extract 2-4 self-contained technical decisions from `plan*`
|
|
33
|
+
- build the review-history table from `review*` and `refinement*`
|
|
34
|
+
- extract the test summary from `implementation*` or `refinement*`
|
|
35
|
+
- if one artifact class is missing, treat it as "no data for this stage" and continue
|
|
36
|
+
|
|
37
|
+
## Comment Body Template
|
|
38
|
+
|
|
39
|
+
Use this canonical comment body template:
|
|
40
|
+
|
|
41
|
+
```markdown
|
|
42
|
+
<!-- sync-pr:{task-id}:summary -->
|
|
43
|
+
<!-- last-commit: {git-head-sha} -->
|
|
44
|
+
## Review Summary
|
|
45
|
+
|
|
46
|
+
> **{agent}** · {task-id}
|
|
47
|
+
|
|
48
|
+
**Updated At**: {current-time}
|
|
49
|
+
|
|
50
|
+
### Key Technical Decisions
|
|
51
|
+
|
|
52
|
+
- {decision-1}
|
|
53
|
+
- {decision-2}
|
|
54
|
+
|
|
55
|
+
### Review History
|
|
56
|
+
|
|
57
|
+
| Round | Verdict | Finding Counts | Fix Status |
|
|
58
|
+
|-------|---------|----------------|------------|
|
|
59
|
+
| Round 1 | Pending | N/A | N/A |
|
|
60
|
+
|
|
61
|
+
### Test Results
|
|
62
|
+
|
|
63
|
+
- {test-summary}
|
|
64
|
+
|
|
65
|
+
---
|
|
66
|
+
*Generated by {agent} · Internal tracking: {task-id}*
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
## Comment Lookup And Update
|
|
70
|
+
|
|
71
|
+
Fetch existing comments through the Issues comments API, not the dedicated PR comments API.
|
|
72
|
+
|
|
73
|
+
Process:
|
|
74
|
+
1. Fetch existing PR comments and locate the comment ID that starts with `<!-- sync-pr:{task-id}:summary -->`
|
|
75
|
+
2. When rendering the body, always write the current `git rev-parse HEAD` result into `<!-- last-commit: {git-head-sha} -->`
|
|
76
|
+
3. If it does not exist, create it once as a fallback
|
|
77
|
+
4. If it exists and the body is identical, skip the write
|
|
78
|
+
5. If it exists and the body changed, PATCH it in place
|
|
79
|
+
|
|
80
|
+
When updating an existing comment, use:
|
|
81
|
+
|
|
82
|
+
```bash
|
|
83
|
+
gh api "repos/{owner}/{repo}/issues/comments/{comment-id}" -X PATCH -f body="$(cat <<'EOF'
|
|
84
|
+
{comment-body}
|
|
85
|
+
EOF
|
|
86
|
+
)"
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
## Shell Safety Rules
|
|
90
|
+
|
|
91
|
+
1. Read local artifact content first, then inline the actual text into a `<<'EOF'` heredoc.
|
|
92
|
+
2. Do not use command substitution or variable expansion inside the heredoc.
|
|
93
|
+
3. Do not use `echo` to construct bodies that contain `<!-- -->`; use `cat <<'EOF'` or `printf '%s\n'`.
|
|
94
|
+
|
|
95
|
+
## Error Handling
|
|
96
|
+
|
|
97
|
+
| Failure Point | Handling |
|
|
98
|
+
|--------|------|
|
|
99
|
+
| `task.md` cannot be read | skip sync and let the later verification gate report it |
|
|
100
|
+
| aggregation input is missing | warn and continue with the available data |
|
|
101
|
+
| `gh api` GET/PATCH/POST fails | warn and continue; whether the current skill should block is decided by the caller |
|
|
102
|
+
| `pr_number` points to a missing PR | warn with `PR #{pr-number} not found` and continue |
|
|
103
|
+
|
|
104
|
+
## Result Reporting
|
|
105
|
+
|
|
106
|
+
Return one of these normalized results so callers can reuse it in Activity Log entries or user output:
|
|
107
|
+
- `summary created`
|
|
108
|
+
- `summary updated`
|
|
109
|
+
- `summary skipped (no diff)`
|
|
110
|
+
- `summary failed: <reason>`
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
# PR 摘要同步
|
|
2
|
+
|
|
3
|
+
在同步 reviewer 面向的唯一 PR 摘要评论之前先读取本文件。
|
|
4
|
+
|
|
5
|
+
## 适用范围
|
|
6
|
+
|
|
7
|
+
当前调用方:
|
|
8
|
+
- `create-pr`:首次创建或更新 PR 摘要评论
|
|
9
|
+
- `commit`:在已有 PR 上按需刷新摘要评论
|
|
10
|
+
|
|
11
|
+
如果后续 skill 也需要刷新 PR 摘要,先在对应 skill 的 reference 中引用本 rule,再补充该 skill 自身的触发条件和失败语义。
|
|
12
|
+
|
|
13
|
+
## 隐藏标记
|
|
14
|
+
|
|
15
|
+
统一使用以下隐藏标记:
|
|
16
|
+
|
|
17
|
+
```html
|
|
18
|
+
<!-- sync-pr:{task-id}:summary -->
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
同一个 PR 中,同一个 `{task-id}` 只能维护一条带该标记的摘要评论。
|
|
22
|
+
|
|
23
|
+
## 聚合输入
|
|
24
|
+
|
|
25
|
+
聚合当前任务目录中的最新产物:
|
|
26
|
+
- `plan.md` 或最新 `plan-r{N}.md`
|
|
27
|
+
- `implementation.md` 或最新 `implementation-r{N}.md`
|
|
28
|
+
- `review.md` 或最新 `review-r{N}.md`
|
|
29
|
+
- `refinement.md` 或最新 `refinement-r{N}.md`
|
|
30
|
+
|
|
31
|
+
聚合规则:
|
|
32
|
+
- 从 `plan*` 提取 2-4 条自包含的关键技术决策
|
|
33
|
+
- 用 `review*` 与 `refinement*` 构建审查历程表
|
|
34
|
+
- 从 `implementation*` 或 `refinement*` 提取测试结果摘要
|
|
35
|
+
- 某一类产物缺失时,按“无该阶段数据”处理并继续生成
|
|
36
|
+
|
|
37
|
+
## 评论体模板
|
|
38
|
+
|
|
39
|
+
评论正文使用以下唯一权威模板:
|
|
40
|
+
|
|
41
|
+
```markdown
|
|
42
|
+
<!-- sync-pr:{task-id}:summary -->
|
|
43
|
+
<!-- last-commit: {git-head-sha} -->
|
|
44
|
+
## 审查摘要
|
|
45
|
+
|
|
46
|
+
> **{agent}** · {task-id}
|
|
47
|
+
|
|
48
|
+
**更新时间**:{当前时间}
|
|
49
|
+
|
|
50
|
+
### 关键技术决策
|
|
51
|
+
|
|
52
|
+
- {decision-1}
|
|
53
|
+
- {decision-2}
|
|
54
|
+
|
|
55
|
+
### 审查历程
|
|
56
|
+
|
|
57
|
+
| 轮次 | 结论 | 问题统计 | 修复状态 |
|
|
58
|
+
|------|------|----------|----------|
|
|
59
|
+
| Round 1 | Pending | N/A | N/A |
|
|
60
|
+
|
|
61
|
+
### 测试结果
|
|
62
|
+
|
|
63
|
+
- {test-summary}
|
|
64
|
+
|
|
65
|
+
---
|
|
66
|
+
*由 {agent} 自动生成 · 内部追踪:{task-id}*
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
## 评论查找与更新
|
|
70
|
+
|
|
71
|
+
已有评论必须通过 Issues comments API 获取,而不是单独的 PR comments API。
|
|
72
|
+
|
|
73
|
+
处理顺序:
|
|
74
|
+
1. 获取 PR 上现有 comments,查找以 `<!-- sync-pr:{task-id}:summary -->` 开头的评论 ID
|
|
75
|
+
2. 渲染评论正文时,始终写入当前 `git rev-parse HEAD` 的结果到 `<!-- last-commit: {git-head-sha} -->`
|
|
76
|
+
3. 不存在时,POST 创建一条新评论作为兜底
|
|
77
|
+
4. 已存在且正文完全相同时,跳过写入
|
|
78
|
+
5. 已存在且正文有变化时,PATCH 原地更新
|
|
79
|
+
|
|
80
|
+
更新已有评论时,使用如下模式:
|
|
81
|
+
|
|
82
|
+
```bash
|
|
83
|
+
gh api "repos/{owner}/{repo}/issues/comments/{comment-id}" -X PATCH -f body="$(cat <<'EOF'
|
|
84
|
+
{comment-body}
|
|
85
|
+
EOF
|
|
86
|
+
)"
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
## Shell 安全规则
|
|
90
|
+
|
|
91
|
+
1. 先读取本地产物内容,再将实际文本内联到 `<<'EOF'` heredoc 中。
|
|
92
|
+
2. 禁止在 heredoc 中使用命令替换或变量展开。
|
|
93
|
+
3. 构造含 `<!-- -->` 的正文时禁止使用 `echo`,统一使用 `cat <<'EOF'` 或 `printf '%s\n'`。
|
|
94
|
+
|
|
95
|
+
## 错误处理
|
|
96
|
+
|
|
97
|
+
| 失败点 | 处理 |
|
|
98
|
+
|--------|------|
|
|
99
|
+
| `task.md` 无法读取 | 跳过同步,交由后续 verification gate 报错 |
|
|
100
|
+
| 聚合输入缺失 | 记警告并按现有数据继续生成 |
|
|
101
|
+
| `gh api` GET/PATCH/POST 失败 | 输出警告并继续;是否阻塞当前 skill 由调用方决定 |
|
|
102
|
+
| `pr_number` 指向的 PR 不存在 | 输出 `PR #{pr-number} not found` 警告并继续 |
|
|
103
|
+
|
|
104
|
+
## 结果回传
|
|
105
|
+
|
|
106
|
+
统一回传以下结果之一,供调用方在 Activity Log 或用户输出中复用:
|
|
107
|
+
- `summary created`
|
|
108
|
+
- `summary updated`
|
|
109
|
+
- `summary skipped (no diff)`
|
|
110
|
+
- `summary failed: <reason>`
|
|
@@ -33,6 +33,7 @@ const DEFAULT_RETRY_DELAYS_MS = [3000, 10000];
|
|
|
33
33
|
const DEFAULT_FRESHNESS_MINUTES = 30;
|
|
34
34
|
const DATE_TIME_PATTERN = /^\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}$/;
|
|
35
35
|
const ACTIVITY_LOG_PATTERN = /^- (\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}) — \*\*(.+?)\*\* by (.+?) — (.+)$/;
|
|
36
|
+
const BRANCH_SLUG_PATTERN = /^[a-z0-9]+(?:-[a-z0-9]+)*$/;
|
|
36
37
|
|
|
37
38
|
const scriptPath = fileURLToPath(import.meta.url);
|
|
38
39
|
const repoRoot = path.resolve(path.dirname(scriptPath), "..", "..");
|
|
@@ -190,6 +191,11 @@ function checkTaskMeta({ taskDir, config }) {
|
|
|
190
191
|
}
|
|
191
192
|
}
|
|
192
193
|
|
|
194
|
+
const branchValidationError = validateTaskBranch(metadata);
|
|
195
|
+
if (branchValidationError) {
|
|
196
|
+
return failResult("task-meta", branchValidationError);
|
|
197
|
+
}
|
|
198
|
+
|
|
193
199
|
const expectedStep = config.expected_step;
|
|
194
200
|
if (expectedStep && metadata.current_step !== expectedStep) {
|
|
195
201
|
return failResult(
|
|
@@ -232,6 +238,40 @@ function checkTaskMeta({ taskDir, config }) {
|
|
|
232
238
|
return passResult("task-meta", `Task metadata valid (${requiredFields.length} required fields checked)`);
|
|
233
239
|
}
|
|
234
240
|
|
|
241
|
+
function validateTaskBranch(metadata) {
|
|
242
|
+
if (isBlank(metadata.branch)) {
|
|
243
|
+
return null;
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
const projectName = loadProjectName();
|
|
247
|
+
const expectedPrefix = projectName ? `${projectName}-${metadata.type}-` : "";
|
|
248
|
+
|
|
249
|
+
if (expectedPrefix && !String(metadata.branch).startsWith(expectedPrefix)) {
|
|
250
|
+
return `Invalid branch: expected prefix '${expectedPrefix}', got '${metadata.branch}'`;
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
const slug = expectedPrefix ? String(metadata.branch).slice(expectedPrefix.length) : String(metadata.branch);
|
|
254
|
+
if (!BRANCH_SLUG_PATTERN.test(slug)) {
|
|
255
|
+
return `Invalid branch: '${metadata.branch}' must use kebab-case suffixes`;
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
return null;
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
function loadProjectName() {
|
|
262
|
+
const configPath = path.join(repoRoot, ".agents", ".airc.json");
|
|
263
|
+
if (!fs.existsSync(configPath)) {
|
|
264
|
+
return "";
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
try {
|
|
268
|
+
const config = JSON.parse(fs.readFileSync(configPath, "utf8"));
|
|
269
|
+
return String(config.project || "").trim();
|
|
270
|
+
} catch {
|
|
271
|
+
return "";
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
|
|
235
275
|
function checkArtifact({ taskDir, config, artifactFile }) {
|
|
236
276
|
const resolvedArtifact = resolveArtifactPath(taskDir, config.file_pattern, artifactFile);
|
|
237
277
|
if (!resolvedArtifact.ok) {
|
|
@@ -396,6 +436,7 @@ function checkGithubSync({ taskDir, config, artifactFile }) {
|
|
|
396
436
|
checkStatusLabel,
|
|
397
437
|
checkCommentMarker,
|
|
398
438
|
checkPrCommentMarker,
|
|
439
|
+
checkPrCommentLastCommit,
|
|
399
440
|
checkCommentContent,
|
|
400
441
|
checkTaskCommentContent,
|
|
401
442
|
checkInLabelsMatchPr,
|
|
@@ -540,9 +581,13 @@ function buildSyncContext({ taskDir, config, artifactFile }) {
|
|
|
540
581
|
}
|
|
541
582
|
|
|
542
583
|
const issueNumber = parseIssueNumber(task.metadata.issue_number);
|
|
584
|
+
const prNumber = parsePrNumber(task.metadata.pr_number);
|
|
543
585
|
if (config.when === "issue_number_exists" && !issueNumber) {
|
|
544
586
|
return { earlyReturn: passResult("github-sync", "Skipped: task has no issue_number") };
|
|
545
587
|
}
|
|
588
|
+
if (config.when === "pr_number_exists" && !prNumber) {
|
|
589
|
+
return { earlyReturn: passResult("github-sync", "Skipped: task has no pr_number") };
|
|
590
|
+
}
|
|
546
591
|
|
|
547
592
|
if (!issueNumber) {
|
|
548
593
|
return { earlyReturn: passResult("github-sync", "Skipped: github-sync not required for this task") };
|
|
@@ -568,7 +613,7 @@ function buildSyncContext({ taskDir, config, artifactFile }) {
|
|
|
568
613
|
artifactFile,
|
|
569
614
|
artifactPath,
|
|
570
615
|
issueNumber,
|
|
571
|
-
prNumber
|
|
616
|
+
prNumber,
|
|
572
617
|
ownerRepo: ownerRepo.value,
|
|
573
618
|
marker,
|
|
574
619
|
prMarker
|
|
@@ -707,6 +752,7 @@ function shouldFetchComments(config) {
|
|
|
707
752
|
return Boolean(
|
|
708
753
|
config.expected_comment_marker
|
|
709
754
|
|| config.expected_pr_comment_marker
|
|
755
|
+
|| config.verify_pr_comment_last_commit_matches_head
|
|
710
756
|
|| config.verify_comment_content
|
|
711
757
|
|| config.verify_task_comment_content
|
|
712
758
|
);
|
|
@@ -771,6 +817,57 @@ function checkPrCommentMarker(context, remoteData) {
|
|
|
771
817
|
);
|
|
772
818
|
}
|
|
773
819
|
|
|
820
|
+
function checkPrCommentLastCommit(context, remoteData) {
|
|
821
|
+
if (!context.config.verify_pr_comment_last_commit_matches_head) {
|
|
822
|
+
return null;
|
|
823
|
+
}
|
|
824
|
+
|
|
825
|
+
if (!context.prMarker) {
|
|
826
|
+
return failResult(
|
|
827
|
+
"github-sync",
|
|
828
|
+
"verify_pr_comment_last_commit_matches_head requires expected_pr_comment_marker",
|
|
829
|
+
"check_failed"
|
|
830
|
+
);
|
|
831
|
+
}
|
|
832
|
+
|
|
833
|
+
const comment = findCommentByMarker(remoteData.prComments, context.prMarker);
|
|
834
|
+
if (!comment) {
|
|
835
|
+
return failResult(
|
|
836
|
+
"github-sync",
|
|
837
|
+
`Expected PR comment marker '${context.prMarker}' not found on PR #${context.prNumber}`,
|
|
838
|
+
"check_failed"
|
|
839
|
+
);
|
|
840
|
+
}
|
|
841
|
+
|
|
842
|
+
const match = String(comment.body || "").match(/<!--\s*last-commit:\s*([0-9a-f]{7,40})\s*-->/i);
|
|
843
|
+
if (!match) {
|
|
844
|
+
return failResult(
|
|
845
|
+
"github-sync",
|
|
846
|
+
`PR #${context.prNumber} summary comment is missing '<!-- last-commit: <sha> -->' metadata`,
|
|
847
|
+
"check_failed"
|
|
848
|
+
);
|
|
849
|
+
}
|
|
850
|
+
|
|
851
|
+
const headResult = withRetry(() => gitText(["rev-parse", "HEAD"], context.taskDir));
|
|
852
|
+
if (!headResult.ok) {
|
|
853
|
+
return headResult.type === "check_failed"
|
|
854
|
+
? failResult("github-sync", headResult.message, headResult.type)
|
|
855
|
+
: blockedResult("github-sync", headResult.message, headResult.type);
|
|
856
|
+
}
|
|
857
|
+
|
|
858
|
+
const expectedHead = String(headResult.value || "").trim();
|
|
859
|
+
const actualHead = match[1].trim();
|
|
860
|
+
if (expectedHead === actualHead) {
|
|
861
|
+
return null;
|
|
862
|
+
}
|
|
863
|
+
|
|
864
|
+
return failResult(
|
|
865
|
+
"github-sync",
|
|
866
|
+
`PR #${context.prNumber} summary comment last-commit metadata mismatch: expected ${expectedHead}, got ${actualHead}`,
|
|
867
|
+
"check_failed"
|
|
868
|
+
);
|
|
869
|
+
}
|
|
870
|
+
|
|
774
871
|
function checkCommentContent(context, remoteData) {
|
|
775
872
|
if (!context.config.verify_comment_content) {
|
|
776
873
|
return null;
|
|
@@ -1179,6 +1276,25 @@ function ghPaginatedJson(args, cwd) {
|
|
|
1179
1276
|
return ghJson(args, cwd);
|
|
1180
1277
|
}
|
|
1181
1278
|
|
|
1279
|
+
function gitText(args, cwd) {
|
|
1280
|
+
const result = spawnSync("git", args, {
|
|
1281
|
+
cwd,
|
|
1282
|
+
encoding: "utf8",
|
|
1283
|
+
env: process.env
|
|
1284
|
+
});
|
|
1285
|
+
|
|
1286
|
+
if (result.status !== 0) {
|
|
1287
|
+
const stderr = `${result.stderr || ""}${result.stdout || ""}`.trim();
|
|
1288
|
+
return {
|
|
1289
|
+
ok: false,
|
|
1290
|
+
type: "check_failed",
|
|
1291
|
+
message: stderr || `git ${args.join(" ")} failed`
|
|
1292
|
+
};
|
|
1293
|
+
}
|
|
1294
|
+
|
|
1295
|
+
return { ok: true, value: String(result.stdout || "").trim() };
|
|
1296
|
+
}
|
|
1297
|
+
|
|
1182
1298
|
function withRetry(operation) {
|
|
1183
1299
|
const delays = getRetryDelays();
|
|
1184
1300
|
let lastFailure = null;
|
|
@@ -5,7 +5,10 @@ description: "Archive completed tasks into a date-organized workspace directory"
|
|
|
5
5
|
|
|
6
6
|
# Archive Completed Tasks
|
|
7
7
|
|
|
8
|
-
Move completed tasks from `.agents/workspace/completed/` into `.agents/workspace/archive/YYYY/MM/DD/TASK-xxx/` and rebuild
|
|
8
|
+
Move completed tasks from `.agents/workspace/completed/` into `.agents/workspace/archive/YYYY/MM/DD/TASK-xxx/` and rebuild a three-level archive index:
|
|
9
|
+
- root manifest: `.agents/workspace/archive/manifest.md`
|
|
10
|
+
- yearly manifest: `.agents/workspace/archive/YYYY/manifest.md`
|
|
11
|
+
- monthly manifest: `.agents/workspace/archive/YYYY/MM/manifest.md`
|
|
9
12
|
|
|
10
13
|
## Execution Flow
|
|
11
14
|
|
|
@@ -29,7 +32,7 @@ The script is responsible for:
|
|
|
29
32
|
- reading `completed_at` from `task.md` frontmatter and falling back to `updated_at`
|
|
30
33
|
- moving task directories directly into `YYYY/MM/DD/TASK-xxx/` without compression
|
|
31
34
|
- skipping already archived, missing, or malformed tasks
|
|
32
|
-
- rebuilding
|
|
35
|
+
- rebuilding root, yearly, and monthly manifests from all archived tasks
|
|
33
36
|
- printing an archive and skip summary
|
|
34
37
|
|
|
35
38
|
### 3. Inform the user
|
|
@@ -37,4 +40,4 @@ The script is responsible for:
|
|
|
37
40
|
Report:
|
|
38
41
|
- how many tasks were archived
|
|
39
42
|
- how many tasks were skipped and why
|
|
40
|
-
- the path to
|
|
43
|
+
- the path to the root manifest
|
|
@@ -5,7 +5,10 @@ description: "归档已完成任务到按日期组织的目录"
|
|
|
5
5
|
|
|
6
6
|
# 归档已完成任务
|
|
7
7
|
|
|
8
|
-
将 `.agents/workspace/completed/` 中的已完成任务移动到 `.agents/workspace/archive/YYYY/MM/DD/TASK-xxx
|
|
8
|
+
将 `.agents/workspace/completed/` 中的已完成任务移动到 `.agents/workspace/archive/YYYY/MM/DD/TASK-xxx/`,并重建三级归档索引:
|
|
9
|
+
- 根 manifest:`.agents/workspace/archive/manifest.md`
|
|
10
|
+
- 年 manifest:`.agents/workspace/archive/YYYY/manifest.md`
|
|
11
|
+
- 月 manifest:`.agents/workspace/archive/YYYY/MM/manifest.md`
|
|
9
12
|
|
|
10
13
|
## 执行流程
|
|
11
14
|
|
|
@@ -29,7 +32,7 @@ bash .agents/skills/archive-tasks/scripts/archive-tasks.sh [--days N | --before
|
|
|
29
32
|
- 解析 `task.md` frontmatter 中的 `completed_at`(缺失时回退到 `updated_at`)
|
|
30
33
|
- 按 `YYYY/MM/DD/TASK-xxx/` 目录直接移动任务,不压缩
|
|
31
34
|
- 跳过已归档、缺少元数据或不存在的任务
|
|
32
|
-
-
|
|
35
|
+
- 全量重建根 / 年 / 月三级 manifest
|
|
33
36
|
- 输出归档与跳过摘要
|
|
34
37
|
|
|
35
38
|
### 3. 告知用户
|
|
@@ -37,4 +40,4 @@ bash .agents/skills/archive-tasks/scripts/archive-tasks.sh [--days N | --before
|
|
|
37
40
|
向用户汇报:
|
|
38
41
|
- 本次归档的任务数量
|
|
39
42
|
- 跳过的任务数量和原因
|
|
40
|
-
-
|
|
43
|
+
- 根 manifest 的路径
|
|
@@ -208,6 +208,8 @@ should_archive_filtered_task() {
|
|
|
208
208
|
|
|
209
209
|
rebuild_manifest() {
|
|
210
210
|
entries_file="$tmpdir/manifest.tsv"
|
|
211
|
+
month_keys_file="$tmpdir/manifest-months.tsv"
|
|
212
|
+
year_keys_file="$tmpdir/manifest-years.tsv"
|
|
211
213
|
generated_at=$(date "+%Y-%m-%d %H:%M:%S")
|
|
212
214
|
|
|
213
215
|
mkdir -p "$ARCHIVE_DIR"
|
|
@@ -253,26 +255,107 @@ rebuild_manifest() {
|
|
|
253
255
|
fi
|
|
254
256
|
fi
|
|
255
257
|
|
|
256
|
-
printf '%s\t%s\t%s\t%s\t%s\n' "$completed_at" "$task_id" "$title" "$task_type" "$relative_path" >> "$entries_file"
|
|
258
|
+
printf '%s\t%s\t%s\t%s\t%s\t%s\t%s\n' "$year" "$month" "$completed_at" "$task_id" "$title" "$task_type" "$relative_path" >> "$entries_file"
|
|
257
259
|
done
|
|
258
260
|
done
|
|
259
261
|
done
|
|
260
262
|
done
|
|
261
263
|
|
|
264
|
+
find "$ARCHIVE_DIR" -type f -name 'manifest.md' -exec rm -f {} \;
|
|
265
|
+
|
|
266
|
+
awk -F'\t' '{print $1 "\t" $2}' "$entries_file" | LC_ALL=C sort -u > "$month_keys_file"
|
|
267
|
+
awk -F'\t' '{print $1}' "$entries_file" | LC_ALL=C sort -u > "$year_keys_file"
|
|
268
|
+
|
|
269
|
+
while IFS="$(printf '\t')" read -r year month; do
|
|
270
|
+
[ -n "$year" ] || continue
|
|
271
|
+
[ -n "$month" ] || continue
|
|
272
|
+
|
|
273
|
+
month_entries_file="$tmpdir/manifest-${year}-${month}.tsv"
|
|
274
|
+
month_manifest_path="$ARCHIVE_DIR/$year/$month/manifest.md"
|
|
275
|
+
|
|
276
|
+
awk -F'\t' -v target_year="$year" -v target_month="$month" '
|
|
277
|
+
$1 == target_year && $2 == target_month {
|
|
278
|
+
print $3 "\t" $4 "\t" $5 "\t" $6 "\t" $7
|
|
279
|
+
}
|
|
280
|
+
' "$entries_file" | LC_ALL=C sort -r > "$month_entries_file"
|
|
281
|
+
|
|
282
|
+
month_entry_count=$(wc -l < "$month_entries_file" | tr -d ' ')
|
|
283
|
+
|
|
284
|
+
{
|
|
285
|
+
echo "# Archive Manifest"
|
|
286
|
+
echo
|
|
287
|
+
echo "> Auto-generated by archive-tasks. Do not edit manually."
|
|
288
|
+
echo "> Last updated: $generated_at"
|
|
289
|
+
echo
|
|
290
|
+
echo "| Task ID | Title | Type | Completed | Path |"
|
|
291
|
+
echo "| --- | --- | --- | --- | --- |"
|
|
292
|
+
|
|
293
|
+
head -n 1000 "$month_entries_file" | while IFS="$(printf '\t')" read -r completed_at task_id title task_type relative_path; do
|
|
294
|
+
[ -n "$task_id" ] || continue
|
|
295
|
+
printf '| %s | %s | %s | %s | %s |\n' "$task_id" "$title" "$task_type" "$completed_at" "$relative_path"
|
|
296
|
+
done
|
|
297
|
+
|
|
298
|
+
if [ "$month_entry_count" -gt 1000 ]; then
|
|
299
|
+
echo
|
|
300
|
+
printf '> Showing 1000 of %s entries.\n' "$month_entry_count"
|
|
301
|
+
fi
|
|
302
|
+
} > "$month_manifest_path"
|
|
303
|
+
done < "$month_keys_file"
|
|
304
|
+
|
|
305
|
+
while IFS= read -r year; do
|
|
306
|
+
[ -n "$year" ] || continue
|
|
307
|
+
|
|
308
|
+
year_manifest_path="$ARCHIVE_DIR/$year/manifest.md"
|
|
309
|
+
|
|
310
|
+
{
|
|
311
|
+
echo "# Archive Manifest"
|
|
312
|
+
echo
|
|
313
|
+
echo "> Auto-generated by archive-tasks. Do not edit manually."
|
|
314
|
+
echo "> Last updated: $generated_at"
|
|
315
|
+
echo
|
|
316
|
+
echo "| Month | Tasks | Manifest |"
|
|
317
|
+
echo "| --- | --- | --- |"
|
|
318
|
+
|
|
319
|
+
awk -F'\t' -v target_year="$year" '
|
|
320
|
+
$1 == target_year {
|
|
321
|
+
counts[$2] += 1
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
END {
|
|
325
|
+
for (month in counts) {
|
|
326
|
+
print month "\t" counts[month]
|
|
327
|
+
}
|
|
328
|
+
}
|
|
329
|
+
' "$entries_file" | LC_ALL=C sort -r | while IFS="$(printf '\t')" read -r month task_count; do
|
|
330
|
+
[ -n "$month" ] || continue
|
|
331
|
+
printf '| %s | %s | [%s/manifest.md](%s/manifest.md) |\n' "$month" "$task_count" "$month" "$month"
|
|
332
|
+
done
|
|
333
|
+
} > "$year_manifest_path"
|
|
334
|
+
done < "$year_keys_file"
|
|
335
|
+
|
|
262
336
|
{
|
|
263
337
|
echo "# Archive Manifest"
|
|
264
338
|
echo
|
|
265
339
|
echo "> Auto-generated by archive-tasks. Do not edit manually."
|
|
266
340
|
echo "> Last updated: $generated_at"
|
|
267
341
|
echo
|
|
268
|
-
echo "|
|
|
269
|
-
echo "| --- | --- | --- |
|
|
342
|
+
echo "| Year | Tasks | Manifest |"
|
|
343
|
+
echo "| --- | --- | --- |"
|
|
270
344
|
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
345
|
+
awk -F'\t' '
|
|
346
|
+
{
|
|
347
|
+
counts[$1] += 1
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
END {
|
|
351
|
+
for (year in counts) {
|
|
352
|
+
print year "\t" counts[year]
|
|
353
|
+
}
|
|
354
|
+
}
|
|
355
|
+
' "$entries_file" | LC_ALL=C sort -r | while IFS="$(printf '\t')" read -r year task_count; do
|
|
356
|
+
[ -n "$year" ] || continue
|
|
357
|
+
printf '| %s | %s | [%s/manifest.md](%s/manifest.md) |\n' "$year" "$task_count" "$year" "$year"
|
|
358
|
+
done
|
|
276
359
|
} > "$MANIFEST_PATH"
|
|
277
360
|
}
|
|
278
361
|
|
|
@@ -52,7 +52,15 @@ Append the Commit Activity Log entry and choose exactly one next-step case:
|
|
|
52
52
|
- ready for review -> `review-task {task-id}`
|
|
53
53
|
- ready for PR -> `create-pr`
|
|
54
54
|
|
|
55
|
-
## 6.
|
|
55
|
+
## 6. Sync PR Summary When Applicable
|
|
56
|
+
|
|
57
|
+
When `{task-id}` exists and task.md contains a valid `pr_number`, refresh the PR summary comment `<!-- sync-pr:{task-id}:summary -->` on the PR. Otherwise, skip this step.
|
|
58
|
+
|
|
59
|
+
> The full trigger conditions, aggregation rules, PATCH/POST flow, shell-safety constraints, and error handling live in `reference/pr-summary-sync.md` (which in turn points to `.agents/rules/pr-sync.md`). Read `reference/pr-summary-sync.md` before executing this step.
|
|
60
|
+
|
|
61
|
+
Failure handling matches "Update Task Status When Applicable": warn, but do **not** block an already completed `git commit`.
|
|
62
|
+
|
|
63
|
+
## 7. Verification Gate
|
|
56
64
|
|
|
57
65
|
If this operation is associated with `{task-id}`, run the verification gate to confirm task metadata and sync state. If there is no task context, skip this step.
|
|
58
66
|
|
|
@@ -52,7 +52,15 @@ date "+%Y-%m-%d %H:%M:%S"
|
|
|
52
52
|
- 准备审查 -> `review-task {task-id}`
|
|
53
53
|
- 准备创建 PR -> `create-pr`
|
|
54
54
|
|
|
55
|
-
## 6.
|
|
55
|
+
## 6. 同步 PR 摘要(按需)
|
|
56
|
+
|
|
57
|
+
当 `{task-id}` 存在且 task.md 包含有效 `pr_number` 时,刷新 PR 上的 `<!-- sync-pr:{task-id}:summary -->` 摘要评论;否则跳过。
|
|
58
|
+
|
|
59
|
+
> 完整的触发条件、聚合规则、PATCH/POST 流程、Shell 安全约束和错误处理见 `reference/pr-summary-sync.md`(其内联引用 `.agents/rules/pr-sync.md`)。执行此步骤前先读取 `reference/pr-summary-sync.md`。
|
|
60
|
+
|
|
61
|
+
失败处理与「按需更新任务状态」一致:警告但**不**阻塞已完成的 `git commit`。
|
|
62
|
+
|
|
63
|
+
## 7. 完成校验
|
|
56
64
|
|
|
57
65
|
如果本次操作关联了 `{task-id}`,运行完成校验,确认任务元数据和同步状态符合规范;如果没有任务上下文,跳过本步骤。
|
|
58
66
|
|
|
@@ -17,6 +17,10 @@
|
|
|
17
17
|
"expected_action_pattern": "Commit",
|
|
18
18
|
"freshness_minutes": 30
|
|
19
19
|
},
|
|
20
|
-
"github-sync":
|
|
20
|
+
"github-sync": {
|
|
21
|
+
"when": "pr_number_exists",
|
|
22
|
+
"expected_pr_comment_marker": "<!-- sync-pr:{task-id}:summary -->",
|
|
23
|
+
"verify_pr_comment_last_commit_matches_head": true
|
|
24
|
+
}
|
|
21
25
|
}
|
|
22
26
|
}
|