@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.
Files changed (56) hide show
  1. package/README.md +18 -2
  2. package/README.zh-CN.md +18 -2
  3. package/bin/cli.js +19 -0
  4. package/lib/defaults.json +17 -0
  5. package/lib/init.js +1 -0
  6. package/lib/log.js +5 -10
  7. package/lib/merge.js +885 -0
  8. package/lib/sandbox/commands/create.js +1170 -0
  9. package/lib/sandbox/commands/enter.js +64 -0
  10. package/lib/sandbox/commands/ls.js +71 -0
  11. package/lib/sandbox/commands/rebuild.js +102 -0
  12. package/lib/sandbox/commands/rm.js +211 -0
  13. package/lib/sandbox/commands/vm.js +101 -0
  14. package/lib/sandbox/config.js +79 -0
  15. package/lib/sandbox/constants.js +113 -0
  16. package/lib/sandbox/dockerfile.js +95 -0
  17. package/lib/sandbox/engine.js +93 -0
  18. package/lib/sandbox/index.js +64 -0
  19. package/lib/sandbox/runtimes/ai-tools.dockerfile +26 -0
  20. package/lib/sandbox/runtimes/base.dockerfile +30 -0
  21. package/lib/sandbox/runtimes/java17.dockerfile +3 -0
  22. package/lib/sandbox/runtimes/java21.dockerfile +3 -0
  23. package/lib/sandbox/runtimes/node20.dockerfile +3 -0
  24. package/lib/sandbox/runtimes/node22.dockerfile +3 -0
  25. package/lib/sandbox/runtimes/python3.dockerfile +3 -0
  26. package/lib/sandbox/shell.js +48 -0
  27. package/lib/sandbox/task-resolver.js +35 -0
  28. package/lib/sandbox/tools.js +135 -0
  29. package/lib/update.js +16 -2
  30. package/package.json +5 -1
  31. package/templates/.agents/rules/pr-sync.md +110 -0
  32. package/templates/.agents/rules/pr-sync.zh-CN.md +110 -0
  33. package/templates/.agents/scripts/validate-artifact.js +117 -1
  34. package/templates/.agents/skills/archive-tasks/SKILL.md +6 -3
  35. package/templates/.agents/skills/archive-tasks/SKILL.zh-CN.md +6 -3
  36. package/templates/.agents/skills/archive-tasks/scripts/archive-tasks.sh +91 -8
  37. package/templates/.agents/skills/commit/SKILL.md +9 -1
  38. package/templates/.agents/skills/commit/SKILL.zh-CN.md +9 -1
  39. package/templates/.agents/skills/commit/config/verify.json +5 -1
  40. package/templates/.agents/skills/commit/reference/pr-summary-sync.md +21 -0
  41. package/templates/.agents/skills/commit/reference/pr-summary-sync.zh-CN.md +21 -0
  42. package/templates/.agents/skills/commit/reference/task-status-update.md +2 -0
  43. package/templates/.agents/skills/commit/reference/task-status-update.zh-CN.md +2 -0
  44. package/templates/.agents/skills/create-pr/SKILL.md +2 -1
  45. package/templates/.agents/skills/create-pr/SKILL.zh-CN.md +2 -1
  46. package/templates/.agents/skills/create-pr/reference/comment-publish.md +7 -74
  47. package/templates/.agents/skills/create-pr/reference/comment-publish.zh-CN.md +6 -73
  48. package/templates/.agents/skills/create-task/SKILL.md +6 -0
  49. package/templates/.agents/skills/create-task/SKILL.zh-CN.md +6 -0
  50. package/templates/.agents/skills/create-task/config/verify.json +1 -0
  51. package/templates/.agents/skills/import-issue/SKILL.md +2 -0
  52. package/templates/.agents/skills/import-issue/SKILL.zh-CN.md +2 -0
  53. package/templates/.agents/skills/import-issue/config/verify.json +1 -0
  54. package/templates/.agents/skills/update-agent-infra/scripts/sync-templates.js +18 -1
  55. package/templates/.agents/templates/task.md +5 -4
  56. 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
- info(`Default labels.in config added to ${CONFIG_PATH}.`);
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.4.5",
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: parsePrNumber(task.metadata.pr_number),
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 the archive index `manifest.md`.
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 `.agents/workspace/archive/manifest.md` from all archived tasks
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 `manifest.md`
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/`,并重建归档索引 `manifest.md`。
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
- - 全量重建 `.agents/workspace/archive/manifest.md`
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
- - `manifest.md` 的路径
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 "| Task ID | Title | Type | Completed | Path |"
269
- echo "| --- | --- | --- | --- | --- |"
342
+ echo "| Year | Tasks | Manifest |"
343
+ echo "| --- | --- | --- |"
270
344
 
271
- if [ -s "$entries_file" ]; then
272
- LC_ALL=C sort -r "$entries_file" | while IFS="$(printf '\t')" read -r completed_at task_id title task_type relative_path; do
273
- printf '| %s | %s | %s | %s | %s |\n' "$task_id" "$title" "$task_type" "$completed_at" "$relative_path"
274
- done
275
- fi
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. Verification Gate
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": null
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
  }