@fitlab-ai/agent-infra 0.4.1 → 0.4.3

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 (142) hide show
  1. package/README.md +44 -44
  2. package/README.zh-CN.md +44 -44
  3. package/lib/defaults.json +7 -9
  4. package/lib/init.js +1 -0
  5. package/lib/update.js +13 -1
  6. package/package.json +3 -3
  7. package/templates/.agents/QUICKSTART.md +7 -7
  8. package/templates/.agents/QUICKSTART.zh-CN.md +13 -13
  9. package/templates/.agents/README.md +31 -18
  10. package/templates/.agents/README.zh-CN.md +33 -20
  11. package/templates/.agents/rules/issue-sync.md +185 -0
  12. package/templates/.agents/rules/issue-sync.zh-CN.md +185 -0
  13. package/templates/.agents/scripts/validate-artifact.js +1280 -0
  14. package/templates/.agents/skills/analyze-task/SKILL.md +24 -1
  15. package/templates/.agents/skills/analyze-task/SKILL.zh-CN.md +24 -1
  16. package/templates/.agents/skills/analyze-task/config/verify.json +41 -0
  17. package/templates/.agents/skills/archive-tasks/SKILL.md +40 -0
  18. package/templates/.agents/skills/archive-tasks/SKILL.zh-CN.md +40 -0
  19. package/templates/.agents/skills/archive-tasks/scripts/archive-tasks.sh +403 -0
  20. package/templates/.agents/skills/block-task/SKILL.md +25 -37
  21. package/templates/.agents/skills/block-task/SKILL.zh-CN.md +25 -37
  22. package/templates/.agents/skills/block-task/config/verify.json +28 -0
  23. package/templates/.agents/skills/close-codescan/SKILL.md +7 -0
  24. package/templates/.agents/skills/close-codescan/SKILL.zh-CN.md +7 -0
  25. package/templates/.agents/skills/close-dependabot/SKILL.md +7 -0
  26. package/templates/.agents/skills/close-dependabot/SKILL.zh-CN.md +7 -0
  27. package/templates/.agents/skills/commit/SKILL.md +17 -0
  28. package/templates/.agents/skills/commit/SKILL.zh-CN.md +17 -0
  29. package/templates/.agents/skills/commit/config/verify.json +22 -0
  30. package/templates/.agents/skills/commit/reference/task-status-update.md +3 -3
  31. package/templates/.agents/skills/commit/reference/task-status-update.zh-CN.md +3 -3
  32. package/templates/.agents/skills/complete-task/SKILL.md +24 -10
  33. package/templates/.agents/skills/complete-task/SKILL.zh-CN.md +24 -10
  34. package/templates/.agents/skills/complete-task/config/verify.json +30 -0
  35. package/templates/.agents/skills/create-issue/SKILL.md +41 -5
  36. package/templates/.agents/skills/create-issue/SKILL.zh-CN.md +41 -5
  37. package/templates/.agents/skills/create-issue/config/verify.json +27 -0
  38. package/templates/.agents/skills/create-issue/reference/label-and-type.md +10 -11
  39. package/templates/.agents/skills/create-issue/reference/label-and-type.zh-CN.md +10 -11
  40. package/templates/.agents/skills/create-pr/SKILL.md +59 -16
  41. package/templates/.agents/skills/create-pr/SKILL.zh-CN.md +59 -16
  42. package/templates/.agents/skills/create-pr/config/verify.json +26 -0
  43. package/templates/.agents/skills/create-pr/reference/branch-strategy.md +3 -3
  44. package/templates/.agents/skills/create-pr/reference/branch-strategy.zh-CN.md +3 -3
  45. package/templates/.agents/skills/{sync-pr → create-pr}/reference/comment-publish.md +6 -6
  46. package/templates/.agents/skills/{sync-pr → create-pr}/reference/comment-publish.zh-CN.md +10 -10
  47. package/templates/.agents/skills/create-pr/reference/pr-body-template.md +15 -6
  48. package/templates/.agents/skills/create-pr/reference/pr-body-template.zh-CN.md +15 -6
  49. package/templates/.agents/skills/create-task/SKILL.md +25 -3
  50. package/templates/.agents/skills/create-task/SKILL.zh-CN.md +25 -3
  51. package/templates/.agents/skills/create-task/config/verify.json +24 -0
  52. package/templates/.agents/skills/implement-task/SKILL.md +44 -8
  53. package/templates/.agents/skills/implement-task/SKILL.zh-CN.md +44 -8
  54. package/templates/.agents/skills/implement-task/config/verify.json +41 -0
  55. package/templates/.agents/skills/implement-task/reference/branch-management.md +48 -0
  56. package/templates/.agents/skills/implement-task/reference/branch-management.zh-CN.md +49 -0
  57. package/templates/.agents/skills/implement-task/reference/output-template.md +20 -0
  58. package/templates/.agents/skills/implement-task/reference/output-template.zh-CN.md +20 -0
  59. package/templates/.agents/skills/import-codescan/SKILL.md +18 -7
  60. package/templates/.agents/skills/import-codescan/SKILL.zh-CN.md +18 -7
  61. package/templates/.agents/skills/import-codescan/config/verify.json +24 -0
  62. package/templates/.agents/skills/import-dependabot/SKILL.md +18 -7
  63. package/templates/.agents/skills/import-dependabot/SKILL.zh-CN.md +18 -7
  64. package/templates/.agents/skills/import-dependabot/config/verify.json +24 -0
  65. package/templates/.agents/skills/import-issue/SKILL.md +19 -1
  66. package/templates/.agents/skills/import-issue/SKILL.zh-CN.md +19 -1
  67. package/templates/.agents/skills/import-issue/config/verify.json +27 -0
  68. package/templates/.agents/skills/init-labels/SKILL.md +40 -10
  69. package/templates/.agents/skills/init-labels/SKILL.zh-CN.md +40 -10
  70. package/templates/.agents/skills/init-labels/scripts/init-labels.sh +1 -22
  71. package/templates/.agents/skills/init-milestones/SKILL.md +13 -0
  72. package/templates/.agents/skills/init-milestones/SKILL.zh-CN.md +13 -0
  73. package/templates/.agents/skills/plan-task/SKILL.md +29 -75
  74. package/templates/.agents/skills/plan-task/SKILL.zh-CN.md +29 -75
  75. package/templates/.agents/skills/plan-task/config/verify.json +42 -0
  76. package/templates/.agents/skills/refine-task/SKILL.md +51 -4
  77. package/templates/.agents/skills/refine-task/SKILL.zh-CN.md +51 -4
  78. package/templates/.agents/skills/refine-task/config/verify.json +37 -0
  79. package/templates/.agents/skills/refine-title/SKILL.md +10 -2
  80. package/templates/.agents/skills/refine-title/SKILL.zh-CN.md +10 -2
  81. package/templates/.agents/skills/restore-task/SKILL.md +159 -0
  82. package/templates/.agents/skills/restore-task/SKILL.zh-CN.md +159 -0
  83. package/templates/.agents/skills/restore-task/config/verify.json +24 -0
  84. package/templates/.agents/skills/review-task/SKILL.md +25 -1
  85. package/templates/.agents/skills/review-task/SKILL.zh-CN.md +25 -1
  86. package/templates/.agents/skills/review-task/config/verify.json +40 -0
  87. package/templates/.agents/skills/update-agent-infra/SKILL.md +11 -0
  88. package/templates/.agents/skills/update-agent-infra/SKILL.zh-CN.md +11 -0
  89. package/templates/.agents/skills/update-agent-infra/scripts/sync-templates.js +8 -10
  90. package/templates/.claude/commands/archive-tasks.md +9 -0
  91. package/templates/.claude/commands/archive-tasks.zh-CN.md +9 -0
  92. package/templates/.claude/commands/create-pr.md +1 -1
  93. package/templates/.claude/commands/create-pr.zh-CN.md +1 -1
  94. package/templates/.claude/commands/restore-task.md +9 -0
  95. package/templates/.claude/commands/restore-task.zh-CN.md +9 -0
  96. package/templates/.gemini/commands/_project_/archive-tasks.toml +10 -0
  97. package/templates/.gemini/commands/_project_/archive-tasks.zh-CN.toml +10 -0
  98. package/templates/.gemini/commands/_project_/restore-task.toml +8 -0
  99. package/templates/.gemini/commands/_project_/restore-task.zh-CN.toml +8 -0
  100. package/templates/.github/workflows/status-label.yml +82 -0
  101. package/templates/.opencode/commands/archive-tasks.md +11 -0
  102. package/templates/.opencode/commands/archive-tasks.zh-CN.md +11 -0
  103. package/templates/.opencode/commands/restore-task.md +11 -0
  104. package/templates/.opencode/commands/restore-task.zh-CN.md +11 -0
  105. package/templates/.agents/skills/sync-issue/SKILL.md +0 -91
  106. package/templates/.agents/skills/sync-issue/SKILL.zh-CN.md +0 -91
  107. package/templates/.agents/skills/sync-issue/reference/comment-publish.md +0 -88
  108. package/templates/.agents/skills/sync-issue/reference/comment-publish.zh-CN.md +0 -88
  109. package/templates/.agents/skills/sync-issue/reference/delivery-detection.md +0 -42
  110. package/templates/.agents/skills/sync-issue/reference/delivery-detection.zh-CN.md +0 -42
  111. package/templates/.agents/skills/sync-issue/reference/label-sync.md +0 -63
  112. package/templates/.agents/skills/sync-issue/reference/label-sync.zh-CN.md +0 -63
  113. package/templates/.agents/skills/sync-issue/reference/milestone-sync.md +0 -37
  114. package/templates/.agents/skills/sync-issue/reference/milestone-sync.zh-CN.md +0 -37
  115. package/templates/.agents/skills/sync-pr/SKILL.md +0 -72
  116. package/templates/.agents/skills/sync-pr/SKILL.zh-CN.md +0 -72
  117. package/templates/.agents/skills/sync-pr/reference/delivery-detection.md +0 -54
  118. package/templates/.agents/skills/sync-pr/reference/delivery-detection.zh-CN.md +0 -54
  119. package/templates/.claude/CLAUDE.md +0 -138
  120. package/templates/.claude/CLAUDE.zh-CN.md +0 -138
  121. package/templates/.claude/commands/sync-issue.md +0 -8
  122. package/templates/.claude/commands/sync-issue.zh-CN.md +0 -8
  123. package/templates/.claude/commands/sync-pr.md +0 -8
  124. package/templates/.claude/commands/sync-pr.zh-CN.md +0 -8
  125. package/templates/.claude/project-rules.md +0 -65
  126. package/templates/.claude/project-rules.zh-CN.md +0 -65
  127. package/templates/.codex/README.md +0 -38
  128. package/templates/.codex/README.zh-CN.md +0 -37
  129. package/templates/.gemini/commands/_project_/sync-issue.toml +0 -8
  130. package/templates/.gemini/commands/_project_/sync-issue.zh-CN.toml +0 -8
  131. package/templates/.gemini/commands/_project_/sync-pr.toml +0 -8
  132. package/templates/.gemini/commands/_project_/sync-pr.zh-CN.toml +0 -8
  133. package/templates/.opencode/COMMAND_STYLE_GUIDE.md +0 -232
  134. package/templates/.opencode/COMMAND_STYLE_GUIDE.zh-CN.md +0 -232
  135. package/templates/.opencode/README.md +0 -76
  136. package/templates/.opencode/README.zh-CN.md +0 -77
  137. package/templates/.opencode/commands/sync-issue.md +0 -11
  138. package/templates/.opencode/commands/sync-issue.zh-CN.md +0 -11
  139. package/templates/.opencode/commands/sync-pr.md +0 -11
  140. package/templates/.opencode/commands/sync-pr.zh-CN.md +0 -11
  141. package/templates/AGENTS.md +0 -112
  142. package/templates/AGENTS.zh-CN.md +0 -112
@@ -0,0 +1,1280 @@
1
+ #!/usr/bin/env node
2
+
3
+ import fs from "node:fs";
4
+ import path from "node:path";
5
+ import process from "node:process";
6
+ import { spawnSync } from "node:child_process";
7
+ import { fileURLToPath } from "node:url";
8
+
9
+ const EXIT_CODE = {
10
+ pass: 0,
11
+ fail: 1,
12
+ blocked: 2
13
+ };
14
+
15
+ const TASK_ENUMS = {
16
+ type: ["feature", "bugfix", "refactor", "docs", "chore"],
17
+ workflow: ["feature-development", "bug-fix", "refactoring"],
18
+ status: ["active", "blocked", "completed"]
19
+ };
20
+
21
+ const DEFAULT_REQUIRED_FIELDS = [
22
+ "id",
23
+ "type",
24
+ "workflow",
25
+ "status",
26
+ "created_at",
27
+ "updated_at",
28
+ "current_step",
29
+ "assigned_to"
30
+ ];
31
+
32
+ const DEFAULT_RETRY_DELAYS_MS = [3000, 10000];
33
+ const DEFAULT_FRESHNESS_MINUTES = 30;
34
+ const DATE_TIME_PATTERN = /^\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}$/;
35
+ const ACTIVITY_LOG_PATTERN = /^- (\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}) — \*\*(.+?)\*\* by (.+?) — (.+)$/;
36
+
37
+ const scriptPath = fileURLToPath(import.meta.url);
38
+ const repoRoot = path.resolve(path.dirname(scriptPath), "..", "..");
39
+
40
+ // === CLI Entry ===
41
+
42
+ function main(argv) {
43
+ const [mode, ...rest] = argv;
44
+
45
+ if (mode === "gate") {
46
+ runGate(rest);
47
+ return;
48
+ }
49
+
50
+ if (mode === "check") {
51
+ runSingleCheck(rest);
52
+ return;
53
+ }
54
+
55
+ printUsageAndExit();
56
+ }
57
+
58
+ function runGate(args) {
59
+ const { value: formatValue, rest: positional } = extractOption(args, "--format");
60
+ const format = normalizeFormat(formatValue);
61
+ const [skillName, taskDirArg, artifactFile] = positional;
62
+
63
+ if (!skillName || !taskDirArg) {
64
+ printUsageAndExit();
65
+ }
66
+
67
+ const taskDir = path.resolve(taskDirArg);
68
+ const verifyConfig = loadVerifyConfig(skillName);
69
+ const checks = [];
70
+
71
+ for (const [type, checkConfig] of Object.entries(verifyConfig.checks || {})) {
72
+ if (checkConfig === null) {
73
+ continue;
74
+ }
75
+
76
+ const result = runCheck(type, {
77
+ skillName,
78
+ taskDir,
79
+ artifactFile,
80
+ config: checkConfig
81
+ });
82
+
83
+ checks.push(result);
84
+
85
+ if (result.status === "blocked") {
86
+ break;
87
+ }
88
+ }
89
+
90
+ const gate = summarizeGate(checks);
91
+ const output = {
92
+ gate,
93
+ skill: skillName,
94
+ checks,
95
+ summary: summarizeChecks(checks),
96
+ action: buildAction(gate, checks)
97
+ };
98
+
99
+ writeOutput(output, format);
100
+ process.exit(EXIT_CODE[gate]);
101
+ }
102
+
103
+ function runSingleCheck(args) {
104
+ const { value: formatValue, rest: formatArgs } = extractOption(args, "--format");
105
+ const format = normalizeFormat(formatValue);
106
+ const { value: skillName, rest: positional } = extractOption(formatArgs, "--skill");
107
+
108
+ if (!skillName) {
109
+ printUsageAndExit();
110
+ }
111
+
112
+ const [type, taskDirArg, artifactFile] = positional;
113
+
114
+ if (!type || !taskDirArg) {
115
+ printUsageAndExit();
116
+ }
117
+
118
+ const verifyConfig = loadVerifyConfig(skillName);
119
+ const config = (verifyConfig.checks || {})[type];
120
+
121
+ if (config === undefined) {
122
+ failUsage(`Unknown check type '${type}' for skill '${skillName}'.`);
123
+ }
124
+
125
+ if (config === null) {
126
+ writeOutput({
127
+ type,
128
+ skill: skillName,
129
+ status: "pass",
130
+ message: `Check '${type}' is disabled for skill '${skillName}'.`
131
+ }, format);
132
+ process.exit(0);
133
+ }
134
+
135
+ const result = runCheck(type, {
136
+ skillName,
137
+ taskDir: path.resolve(taskDirArg),
138
+ artifactFile,
139
+ config
140
+ });
141
+
142
+ writeOutput({
143
+ skill: skillName,
144
+ ...result
145
+ }, format);
146
+ process.exit(EXIT_CODE[result.status] ?? 1);
147
+ }
148
+
149
+ function runCheck(type, context) {
150
+ switch (type) {
151
+ case "task-meta":
152
+ return checkTaskMeta(context);
153
+ case "artifact":
154
+ return checkArtifact(context);
155
+ case "activity-log":
156
+ return checkActivityLog(context);
157
+ case "github-sync":
158
+ return checkGithubSync(context);
159
+ default:
160
+ return failResult(type, `Unsupported check type '${type}'.`);
161
+ }
162
+ }
163
+
164
+ // === Check Implementations ===
165
+
166
+ function checkTaskMeta({ taskDir, config }) {
167
+ const task = loadTask(taskDir);
168
+ if (!task.ok) {
169
+ return failResult("task-meta", task.message);
170
+ }
171
+
172
+ const metadata = task.metadata;
173
+ const requiredFields = config.required_fields || DEFAULT_REQUIRED_FIELDS;
174
+ const missingFields = requiredFields.filter((field) => isBlank(metadata[field]));
175
+ if (missingFields.length > 0) {
176
+ return failResult("task-meta", `Missing required fields: ${missingFields.join(", ")}`);
177
+ }
178
+
179
+ const invalidDates = ["created_at", "updated_at", "completed_at", "blocked_at"]
180
+ .filter((field) => !isBlank(metadata[field]) && !DATE_TIME_PATTERN.test(metadata[field]));
181
+ if (invalidDates.length > 0) {
182
+ return failResult("task-meta", `Invalid date format in: ${invalidDates.join(", ")}`);
183
+ }
184
+
185
+ for (const [field, allowedValues] of Object.entries(TASK_ENUMS)) {
186
+ if (!isBlank(metadata[field]) && !allowedValues.includes(metadata[field])) {
187
+ return failResult("task-meta", `Invalid ${field}: ${metadata[field]}`);
188
+ }
189
+ }
190
+
191
+ const expectedStep = config.expected_step;
192
+ if (expectedStep && metadata.current_step !== expectedStep) {
193
+ return failResult(
194
+ "task-meta",
195
+ `Expected current_step '${expectedStep}', got '${metadata.current_step || "(empty)"}'`
196
+ );
197
+ }
198
+
199
+ const expectedStatus = config.expected_status;
200
+ if (expectedStatus && metadata.status !== expectedStatus) {
201
+ return failResult(
202
+ "task-meta",
203
+ `Expected status '${expectedStatus}', got '${metadata.status || "(empty)"}'`
204
+ );
205
+ }
206
+
207
+ if (config.require_issue_number && !parseIssueNumber(metadata.issue_number)) {
208
+ return failResult("task-meta", "Expected a valid issue_number in task metadata");
209
+ }
210
+
211
+ if (config.require_completed_at && isBlank(metadata.completed_at)) {
212
+ return failResult("task-meta", "Expected completed_at to be present");
213
+ }
214
+
215
+ if (config.require_blocked_at && isBlank(metadata.blocked_at)) {
216
+ return failResult("task-meta", "Expected blocked_at to be present");
217
+ }
218
+
219
+ if (config.match_task_dir !== false) {
220
+ const expectedTaskId = path.basename(taskDir);
221
+ if (metadata.id !== expectedTaskId) {
222
+ return failResult("task-meta", `Task id '${metadata.id}' does not match directory '${expectedTaskId}'`);
223
+ }
224
+ }
225
+
226
+ return passResult("task-meta", `Task metadata valid (${requiredFields.length} required fields checked)`);
227
+ }
228
+
229
+ function checkArtifact({ taskDir, config, artifactFile }) {
230
+ const resolvedArtifact = resolveArtifactPath(taskDir, config.file_pattern, artifactFile);
231
+ if (!resolvedArtifact.ok) {
232
+ return failResult("artifact", resolvedArtifact.message);
233
+ }
234
+
235
+ const artifactPath = resolvedArtifact.path;
236
+ const stat = safeStat(artifactPath);
237
+ if (!stat) {
238
+ return failResult("artifact", `Artifact not found: ${path.basename(artifactPath)}`);
239
+ }
240
+
241
+ if (stat.size === 0) {
242
+ return failResult("artifact", `Artifact is empty: ${path.basename(artifactPath)}`);
243
+ }
244
+
245
+ const content = fs.readFileSync(artifactPath, "utf8");
246
+ const requiredSections = config.required_sections || [];
247
+ const missingSections = requiredSections.filter(
248
+ (section) => !new RegExp(`^##\\s+${escapeRegExp(section)}\\s*$`, "m").test(content)
249
+ );
250
+
251
+ if (missingSections.length > 0) {
252
+ return failResult(
253
+ "artifact",
254
+ `${path.basename(artifactPath)} is missing sections: ${missingSections.join(", ")}`
255
+ );
256
+ }
257
+
258
+ const requiredPatterns = config.required_patterns || [];
259
+ for (const pattern of requiredPatterns) {
260
+ if (!new RegExp(pattern, "m").test(content)) {
261
+ return failResult("artifact", `${path.basename(artifactPath)} is missing required pattern: ${pattern}`);
262
+ }
263
+ }
264
+
265
+ const freshnessMinutes = Number(config.freshness_minutes ?? DEFAULT_FRESHNESS_MINUTES);
266
+ const ageMinutes = (Date.now() - stat.mtimeMs) / 60000;
267
+ if (Number.isFinite(freshnessMinutes) && ageMinutes > freshnessMinutes) {
268
+ return failResult(
269
+ "artifact",
270
+ `${path.basename(artifactPath)} is stale (${ageMinutes.toFixed(1)}m old, limit ${freshnessMinutes}m)`
271
+ );
272
+ }
273
+
274
+ return passResult(
275
+ "artifact",
276
+ `${path.basename(artifactPath)} passed (${requiredSections.length} sections, ${Math.max(0, freshnessMinutes)}m freshness window)`
277
+ );
278
+ }
279
+
280
+ function checkActivityLog({ taskDir, config }) {
281
+ const task = loadTask(taskDir);
282
+ if (!task.ok) {
283
+ return failResult("activity-log", task.message);
284
+ }
285
+
286
+ const logSection = getSectionContent(task.content, ["活动日志", "Activity Log"]);
287
+ if (!logSection) {
288
+ return failResult("activity-log", "Activity Log section not found");
289
+ }
290
+
291
+ const entries = logSection
292
+ .split(/\r?\n/)
293
+ .map((line) => line.trim())
294
+ .filter((line) => line.startsWith("- "));
295
+
296
+ if (entries.length === 0) {
297
+ return failResult("activity-log", "Activity Log has no entries");
298
+ }
299
+
300
+ let previousTimestamp = "";
301
+ let latestAction = "";
302
+ let latestTimestamp = "";
303
+
304
+ for (const entry of entries) {
305
+ const match = entry.match(ACTIVITY_LOG_PATTERN);
306
+ if (!match) {
307
+ return failResult("activity-log", `Invalid Activity Log entry format: ${entry}`);
308
+ }
309
+
310
+ const [, timestamp, action] = match;
311
+ if (previousTimestamp && timestamp < previousTimestamp) {
312
+ return failResult("activity-log", "Activity Log timestamps are not in ascending order");
313
+ }
314
+
315
+ previousTimestamp = timestamp;
316
+ latestTimestamp = timestamp;
317
+ latestAction = action;
318
+ }
319
+
320
+ if (config.expected_action_pattern && !new RegExp(config.expected_action_pattern).test(latestAction)) {
321
+ return failResult(
322
+ "activity-log",
323
+ `Latest action '${latestAction}' does not match '${config.expected_action_pattern}'`
324
+ );
325
+ }
326
+
327
+ const freshnessMinutes = Number(config.freshness_minutes ?? DEFAULT_FRESHNESS_MINUTES);
328
+ if (Number.isFinite(freshnessMinutes)) {
329
+ const ageMinutes = minutesSinceTimestamp(latestTimestamp);
330
+ if (ageMinutes > freshnessMinutes) {
331
+ return failResult(
332
+ "activity-log",
333
+ `Latest Activity Log entry is stale (${ageMinutes.toFixed(1)}m old, limit ${freshnessMinutes}m)`
334
+ );
335
+ }
336
+ }
337
+
338
+ return passResult("activity-log", `Latest entry '${latestAction}' at ${latestTimestamp}`);
339
+ }
340
+
341
+ function checkGithubSync({ taskDir, config, artifactFile }) {
342
+ const context = buildSyncContext({ taskDir, config, artifactFile });
343
+ if (context.earlyReturn) {
344
+ return context.earlyReturn;
345
+ }
346
+
347
+ const remoteData = fetchRemoteData(context);
348
+ if (remoteData.earlyReturn) {
349
+ return remoteData.earlyReturn;
350
+ }
351
+
352
+ const subChecks = [
353
+ checkStatusLabel,
354
+ checkCommentMarker,
355
+ checkPrCommentMarker,
356
+ checkCommentContent,
357
+ checkTaskCommentContent,
358
+ checkInLabelsMatchPr,
359
+ checkSyncedRequirements
360
+ ];
361
+
362
+ for (const subCheck of subChecks) {
363
+ const result = subCheck(context, remoteData);
364
+ if (result) {
365
+ return result;
366
+ }
367
+ }
368
+
369
+ return passResult("github-sync", `GitHub sync checks passed for Issue #${context.issueNumber}`);
370
+ }
371
+
372
+ // === File & Config Loaders ===
373
+
374
+ function loadVerifyConfig(skillName) {
375
+ const verifyPath = path.join(repoRoot, ".agents", "skills", skillName, "config", "verify.json");
376
+ if (!fs.existsSync(verifyPath)) {
377
+ failUsage(`config/verify.json not found for skill '${skillName}'`);
378
+ }
379
+
380
+ return JSON.parse(fs.readFileSync(verifyPath, "utf8"));
381
+ }
382
+
383
+ function loadTask(taskDir) {
384
+ const taskPath = path.join(taskDir, "task.md");
385
+ if (!fs.existsSync(taskPath)) {
386
+ return { ok: false, message: `Task file not found: ${taskPath}` };
387
+ }
388
+
389
+ const content = fs.readFileSync(taskPath, "utf8");
390
+ const metadata = parseFrontmatter(content);
391
+ if (!metadata) {
392
+ return { ok: false, message: "task.md frontmatter not found or invalid" };
393
+ }
394
+
395
+ return { ok: true, content, metadata };
396
+ }
397
+
398
+ function resolveArtifactPath(taskDir, filePattern, artifactFile) {
399
+ if (artifactFile) {
400
+ return { ok: true, path: path.join(taskDir, artifactFile) };
401
+ }
402
+
403
+ if (!filePattern) {
404
+ return { ok: false, message: "Artifact file is required for this check" };
405
+ }
406
+
407
+ const entries = fs.existsSync(taskDir) ? fs.readdirSync(taskDir) : [];
408
+ const matches = [];
409
+
410
+ for (const pattern of filePattern.split("|").map((value) => value.trim()).filter(Boolean)) {
411
+ const regex = new RegExp(`^${escapePattern(pattern)}$`);
412
+ for (const entry of entries) {
413
+ const match = entry.match(regex);
414
+ if (!match) {
415
+ continue;
416
+ }
417
+
418
+ matches.push({
419
+ fileName: entry,
420
+ round: match[1] ? Number(match[1]) : 0
421
+ });
422
+ }
423
+ }
424
+
425
+ if (matches.length === 0) {
426
+ return { ok: false, message: `No artifact matched pattern '${filePattern}'` };
427
+ }
428
+
429
+ matches.sort((left, right) => right.round - left.round || left.fileName.localeCompare(right.fileName));
430
+ return { ok: true, path: path.join(taskDir, matches[0].fileName) };
431
+ }
432
+
433
+ function parseFrontmatter(content) {
434
+ const match = content.match(/^---\r?\n([\s\S]*?)\r?\n---(?:\r?\n|$)/);
435
+ if (!match) {
436
+ return null;
437
+ }
438
+
439
+ const metadata = {};
440
+ for (const line of match[1].split(/\r?\n/)) {
441
+ const parsed = line.match(/^([A-Za-z0-9_]+):\s*(.*)$/);
442
+ if (!parsed) {
443
+ continue;
444
+ }
445
+
446
+ const [, key, rawValue] = parsed;
447
+ metadata[key] = rawValue.trim().replace(/^['"]|['"]$/g, "");
448
+ }
449
+
450
+ return metadata;
451
+ }
452
+
453
+ function getSectionContent(content, names) {
454
+ const lines = content.split(/\r?\n/);
455
+
456
+ for (const name of names) {
457
+ const heading = `## ${name}`;
458
+ const startIndex = lines.findIndex((line) => line.trim() === heading);
459
+ if (startIndex === -1) {
460
+ continue;
461
+ }
462
+
463
+ const sectionLines = [];
464
+ for (let index = startIndex + 1; index < lines.length; index += 1) {
465
+ if (lines[index].startsWith("## ")) {
466
+ break;
467
+ }
468
+ sectionLines.push(lines[index]);
469
+ }
470
+
471
+ return sectionLines.join("\n").trim();
472
+ }
473
+
474
+ return "";
475
+ }
476
+
477
+ function getCheckedRequirements(content) {
478
+ const section = getSectionContent(content, ["需求", "Requirements"]);
479
+ if (!section) {
480
+ return [];
481
+ }
482
+
483
+ return section
484
+ .split(/\r?\n/)
485
+ .map((line) => line.trim())
486
+ .map((line) => line.match(/^- \[x\] (.+)$/i))
487
+ .filter(Boolean)
488
+ .map((match) => match[1].trim());
489
+ }
490
+
491
+ function buildSyncContext({ taskDir, config, artifactFile }) {
492
+ const task = loadTask(taskDir);
493
+ if (!task.ok) {
494
+ return { earlyReturn: failResult("github-sync", task.message) };
495
+ }
496
+
497
+ const issueNumber = parseIssueNumber(task.metadata.issue_number);
498
+ if (config.when === "issue_number_exists" && !issueNumber) {
499
+ return { earlyReturn: passResult("github-sync", "Skipped: task has no issue_number") };
500
+ }
501
+
502
+ if (!issueNumber) {
503
+ return { earlyReturn: passResult("github-sync", "Skipped: github-sync not required for this task") };
504
+ }
505
+
506
+ const ownerRepo = resolveOwnerRepo(taskDir);
507
+ if (!ownerRepo.ok) {
508
+ return { earlyReturn: blockedResult("github-sync", ownerRepo.message, "network_error") };
509
+ }
510
+
511
+ const marker = config.expected_comment_marker
512
+ ? interpolate(config.expected_comment_marker, taskDir, artifactFile)
513
+ : null;
514
+ const prMarker = config.expected_pr_comment_marker
515
+ ? interpolate(config.expected_pr_comment_marker, taskDir, artifactFile)
516
+ : null;
517
+ const artifactPath = artifactFile ? path.join(taskDir, artifactFile) : null;
518
+
519
+ return {
520
+ task,
521
+ taskDir,
522
+ config,
523
+ artifactFile,
524
+ artifactPath,
525
+ issueNumber,
526
+ prNumber: parsePrNumber(task.metadata.pr_number),
527
+ ownerRepo: ownerRepo.value,
528
+ marker,
529
+ prMarker
530
+ };
531
+ }
532
+
533
+ function fetchRemoteData(context) {
534
+ const issueResult = withRetry(() => ghJson([
535
+ "issue",
536
+ "view",
537
+ String(context.issueNumber),
538
+ "--json",
539
+ "state,labels,body"
540
+ ], context.taskDir));
541
+ if (!issueResult.ok) {
542
+ return {
543
+ earlyReturn: issueResult.type === "check_failed"
544
+ ? failResult("github-sync", issueResult.message, issueResult.type)
545
+ : blockedResult("github-sync", issueResult.message, issueResult.type)
546
+ };
547
+ }
548
+
549
+ const issue = issueResult.value;
550
+ if (context.config.issue_must_exist !== false && !issue) {
551
+ return {
552
+ earlyReturn: failResult("github-sync", `Issue #${context.issueNumber} not found`, "check_failed")
553
+ };
554
+ }
555
+
556
+ let comments = null;
557
+ if (shouldFetchComments(context.config)) {
558
+ const commentsResult = withRetry(() => ghPaginatedJson([
559
+ "api",
560
+ "--paginate",
561
+ "--slurp",
562
+ `repos/${context.ownerRepo}/issues/${context.issueNumber}/comments?per_page=100`
563
+ ], context.taskDir));
564
+
565
+ if (!commentsResult.ok) {
566
+ return {
567
+ earlyReturn: commentsResult.type === "check_failed"
568
+ ? failResult("github-sync", commentsResult.message, commentsResult.type)
569
+ : blockedResult("github-sync", commentsResult.message, commentsResult.type)
570
+ };
571
+ }
572
+
573
+ comments = flattenComments(commentsResult.value);
574
+ }
575
+
576
+ let prComments = null;
577
+ if (context.config.expected_pr_comment_marker) {
578
+ if (!context.prNumber) {
579
+ return {
580
+ earlyReturn: failResult("github-sync", "Expected a valid pr_number for PR comment verification", "check_failed")
581
+ };
582
+ }
583
+
584
+ const prCommentsResult = withRetry(() => ghPaginatedJson([
585
+ "api",
586
+ "--paginate",
587
+ "--slurp",
588
+ `repos/${context.ownerRepo}/issues/${context.prNumber}/comments?per_page=100`
589
+ ], context.taskDir));
590
+
591
+ if (!prCommentsResult.ok) {
592
+ return {
593
+ earlyReturn: prCommentsResult.type === "check_failed"
594
+ ? failResult("github-sync", prCommentsResult.message, prCommentsResult.type)
595
+ : blockedResult("github-sync", prCommentsResult.message, prCommentsResult.type)
596
+ };
597
+ }
598
+
599
+ prComments = flattenComments(prCommentsResult.value);
600
+ }
601
+
602
+ let prLabels = null;
603
+ if (context.config.verify_in_labels_match_pr && context.prNumber) {
604
+ const prResult = withRetry(() => ghJson([
605
+ "pr",
606
+ "view",
607
+ String(context.prNumber),
608
+ "--json",
609
+ "labels"
610
+ ], context.taskDir));
611
+
612
+ if (!prResult.ok) {
613
+ return {
614
+ earlyReturn: prResult.type === "check_failed"
615
+ ? failResult("github-sync", prResult.message, prResult.type)
616
+ : blockedResult("github-sync", prResult.message, prResult.type)
617
+ };
618
+ }
619
+
620
+ prLabels = extractLabelNames(prResult.value?.labels);
621
+ }
622
+
623
+ return {
624
+ issue,
625
+ comments,
626
+ prComments,
627
+ prLabels
628
+ };
629
+ }
630
+
631
+ function shouldFetchComments(config) {
632
+ return Boolean(
633
+ config.expected_comment_marker
634
+ || config.expected_pr_comment_marker
635
+ || config.verify_comment_content
636
+ || config.verify_task_comment_content
637
+ );
638
+ }
639
+
640
+ function flattenComments(value) {
641
+ if (!Array.isArray(value)) {
642
+ return [];
643
+ }
644
+
645
+ return value.flatMap((page) => Array.isArray(page) ? page : []);
646
+ }
647
+
648
+ function checkStatusLabel(context, remoteData) {
649
+ if (!context.config.expected_status_label || remoteData.issue.state !== "OPEN") {
650
+ return null;
651
+ }
652
+
653
+ const labels = extractLabelNames(remoteData.issue.labels);
654
+ if (labels.includes(context.config.expected_status_label)) {
655
+ return null;
656
+ }
657
+
658
+ return failResult(
659
+ "github-sync",
660
+ `Expected label '${context.config.expected_status_label}' not found on Issue #${context.issueNumber}`,
661
+ "check_failed"
662
+ );
663
+ }
664
+
665
+ function checkCommentMarker(context, remoteData) {
666
+ if (!context.marker) {
667
+ return null;
668
+ }
669
+
670
+ const comment = findCommentByMarker(remoteData.comments, context.marker);
671
+ if (comment) {
672
+ return null;
673
+ }
674
+
675
+ return failResult(
676
+ "github-sync",
677
+ `Expected comment marker '${context.marker}' not found on Issue #${context.issueNumber}`,
678
+ "check_failed"
679
+ );
680
+ }
681
+
682
+ function checkPrCommentMarker(context, remoteData) {
683
+ if (!context.prMarker) {
684
+ return null;
685
+ }
686
+
687
+ const comment = findCommentByMarker(remoteData.prComments, context.prMarker);
688
+ if (comment) {
689
+ return null;
690
+ }
691
+
692
+ return failResult(
693
+ "github-sync",
694
+ `Expected PR comment marker '${context.prMarker}' not found on PR #${context.prNumber}`,
695
+ "check_failed"
696
+ );
697
+ }
698
+
699
+ function checkCommentContent(context, remoteData) {
700
+ if (!context.config.verify_comment_content) {
701
+ return null;
702
+ }
703
+
704
+ if (!context.marker) {
705
+ return failResult("github-sync", "verify_comment_content requires expected_comment_marker", "check_failed");
706
+ }
707
+
708
+ if (!context.artifactPath || !safeStat(context.artifactPath)) {
709
+ return failResult(
710
+ "github-sync",
711
+ `Artifact not found for comment verification: ${context.artifactFile || "(missing artifactFile)"}`,
712
+ "check_failed"
713
+ );
714
+ }
715
+
716
+ const comment = findCommentByMarker(remoteData.comments, context.marker);
717
+ const localContent = normalizeContent(fs.readFileSync(context.artifactPath, "utf8"));
718
+ const commentContent = normalizeContent(extractCommentBody(comment?.body || ""));
719
+
720
+ if (localContent === commentContent) {
721
+ return null;
722
+ }
723
+
724
+ return failResult(
725
+ "github-sync",
726
+ buildCommentContentMismatchMessage(
727
+ path.basename(context.artifactPath, path.extname(context.artifactPath)),
728
+ context.issueNumber,
729
+ localContent,
730
+ commentContent
731
+ ),
732
+ "check_failed"
733
+ );
734
+ }
735
+
736
+ function checkTaskCommentContent(context, remoteData) {
737
+ if (!context.config.verify_task_comment_content) {
738
+ return null;
739
+ }
740
+
741
+ const taskMarker = `<!-- sync-issue:${context.task.metadata.id}:task -->`;
742
+ const comment = findCommentByMarker(remoteData.comments, taskMarker);
743
+ if (!comment) {
744
+ return failResult(
745
+ "github-sync",
746
+ `Expected comment marker '${taskMarker}' not found on Issue #${context.issueNumber}`,
747
+ "check_failed"
748
+ );
749
+ }
750
+
751
+ const expectedBody = normalizeContent(buildExpectedTaskBody(context.task.content));
752
+ const commentBody = normalizeContent(extractCommentBody(comment.body || ""));
753
+
754
+ if (expectedBody === commentBody) {
755
+ return null;
756
+ }
757
+
758
+ return failResult(
759
+ "github-sync",
760
+ buildCommentContentMismatchMessage("task", context.issueNumber, expectedBody, commentBody),
761
+ "check_failed"
762
+ );
763
+ }
764
+
765
+ function checkInLabelsMatchPr(context, remoteData) {
766
+ if (!context.config.verify_in_labels_match_pr || !context.prNumber || !remoteData.prLabels) {
767
+ return null;
768
+ }
769
+
770
+ const issueInLabels = extractLabelNames(remoteData.issue.labels)
771
+ .filter((label) => label.startsWith("in:"))
772
+ .sort();
773
+ const prInLabels = remoteData.prLabels
774
+ .filter((label) => label.startsWith("in:"))
775
+ .sort();
776
+
777
+ if (arraysEqual(issueInLabels, prInLabels)) {
778
+ return null;
779
+ }
780
+
781
+ return failResult(
782
+ "github-sync",
783
+ `in: labels mismatch — PR #${context.prNumber} has [${formatLabelList(prInLabels)}], Issue #${context.issueNumber} has [${formatLabelList(issueInLabels)}]`,
784
+ "check_failed"
785
+ );
786
+ }
787
+
788
+ function checkSyncedRequirements(context, remoteData) {
789
+ if (!context.config.sync_checked_requirements) {
790
+ return null;
791
+ }
792
+
793
+ const checkedRequirements = getCheckedRequirements(context.task.content);
794
+ if (checkedRequirements.length === 0) {
795
+ return null;
796
+ }
797
+
798
+ const issueBody = remoteData.issue.body || "";
799
+ const missingRequirements = checkedRequirements.filter(
800
+ (item) => !new RegExp(`^- \\[x\\] ${escapeRegExp(item)}$`, "m").test(issueBody)
801
+ );
802
+ if (missingRequirements.length === 0) {
803
+ return null;
804
+ }
805
+
806
+ return failResult(
807
+ "github-sync",
808
+ `Issue body is missing checked requirements: ${missingRequirements.join(", ")}`,
809
+ "check_failed"
810
+ );
811
+ }
812
+
813
+ function findCommentByMarker(comments, marker) {
814
+ return (comments || []).find((comment) => typeof comment.body === "string" && comment.body.includes(marker)) || null;
815
+ }
816
+
817
+ function extractCommentBody(commentBody) {
818
+ const lines = String(commentBody || "").split(/\r?\n/);
819
+
820
+ let start = 0;
821
+ while (start < lines.length && (lines[start].trim() === "" || /^<!--.*-->$/.test(lines[start].trim()))) {
822
+ start += 1;
823
+ }
824
+
825
+ if (start < lines.length && lines[start].startsWith("## ")) {
826
+ start += 1;
827
+ }
828
+
829
+ while (start < lines.length && lines[start].trim() === "") {
830
+ start += 1;
831
+ }
832
+
833
+ if (start < lines.length && /^> \*\*.+\*\* · .+$/.test(lines[start].trim())) {
834
+ start += 1;
835
+ }
836
+
837
+ while (start < lines.length && lines[start].trim() === "") {
838
+ start += 1;
839
+ }
840
+
841
+ let end = lines.length;
842
+ for (let index = lines.length - 1; index >= start; index -= 1) {
843
+ const trimmed = lines[index].trim();
844
+ if (trimmed === "") {
845
+ continue;
846
+ }
847
+
848
+ if (/^\*.*\*$/.test(trimmed)) {
849
+ end = index;
850
+ if (end > start && lines[end - 1].trim() === "---") {
851
+ end -= 1;
852
+ }
853
+ }
854
+ break;
855
+ }
856
+
857
+ return lines.slice(start, end).join("\n");
858
+ }
859
+
860
+ function buildExpectedTaskBody(taskContent) {
861
+ const frontmatterMatch = taskContent.match(/^---\r?\n([\s\S]*?)\r?\n---(?:\r?\n|$)/);
862
+ if (!frontmatterMatch) {
863
+ return taskContent.trim();
864
+ }
865
+
866
+ const body = taskContent.slice(frontmatterMatch[0].length).trim();
867
+ return [
868
+ buildTaskFrontmatterSummary(),
869
+ "",
870
+ "```yaml",
871
+ frontmatterMatch[0].trim(),
872
+ "```",
873
+ "",
874
+ "</details>",
875
+ "",
876
+ body
877
+ ].join("\n").trim();
878
+ }
879
+
880
+ function buildTaskFrontmatterSummary() {
881
+ const language = loadProjectLanguage();
882
+ if (language === "en" || language === "en-US") {
883
+ return "<details><summary>Metadata (frontmatter)</summary>";
884
+ }
885
+
886
+ return "<details><summary>元数据 (frontmatter)</summary>";
887
+ }
888
+
889
+ function loadProjectLanguage() {
890
+ const override = process.env.VALIDATE_ARTIFACT_LANGUAGE;
891
+ if (!isBlank(override)) {
892
+ return String(override).trim();
893
+ }
894
+
895
+ const configPath = path.join(repoRoot, ".agents", ".airc.json");
896
+ if (!fs.existsSync(configPath)) {
897
+ return "";
898
+ }
899
+
900
+ try {
901
+ const config = JSON.parse(fs.readFileSync(configPath, "utf8"));
902
+ return String(config.language || "").trim();
903
+ } catch {
904
+ return "";
905
+ }
906
+ }
907
+
908
+ function normalizeContent(text) {
909
+ return String(text || "")
910
+ .replace(/\r\n/g, "\n")
911
+ .replace(/\n{3,}/g, "\n\n")
912
+ .trim();
913
+ }
914
+
915
+ function buildCommentContentMismatchMessage(fileStem, issueNumber, localContent, commentContent) {
916
+ const diffIndex = firstDifferenceIndex(localContent, commentContent);
917
+ const position = indexToLineColumn(localContent, diffIndex);
918
+
919
+ return `Comment content mismatch for '${fileStem}' on Issue #${issueNumber}: local file has ${localContent.length} chars, comment body has ${commentContent.length} chars (first difference near char ${diffIndex + 1}, line ${position.line}, column ${position.column})`;
920
+ }
921
+
922
+ function firstDifferenceIndex(left, right) {
923
+ const limit = Math.max(left.length, right.length);
924
+ for (let index = 0; index < limit; index += 1) {
925
+ if (left[index] !== right[index]) {
926
+ return index;
927
+ }
928
+ }
929
+
930
+ return limit;
931
+ }
932
+
933
+ function indexToLineColumn(text, index) {
934
+ const prefix = text.slice(0, Math.min(index, text.length));
935
+ const lines = prefix.split("\n");
936
+ return {
937
+ line: lines.length,
938
+ column: (lines.at(-1) || "").length + 1
939
+ };
940
+ }
941
+
942
+ function extractLabelNames(labels) {
943
+ return (labels || [])
944
+ .map((label) => typeof label === "string" ? label : label?.name)
945
+ .filter((label) => typeof label === "string" && label.length > 0);
946
+ }
947
+
948
+ function arraysEqual(left, right) {
949
+ if (left.length !== right.length) {
950
+ return false;
951
+ }
952
+
953
+ return left.every((value, index) => value === right[index]);
954
+ }
955
+
956
+ function formatLabelList(labels) {
957
+ return labels.length > 0 ? labels.join(", ") : "none";
958
+ }
959
+
960
+ // === GitHub API ===
961
+
962
+ function parseIssueNumber(value) {
963
+ if (isBlank(value) || value === "N/A") {
964
+ return null;
965
+ }
966
+
967
+ const match = String(value).match(/\d+/);
968
+ return match ? Number(match[0]) : null;
969
+ }
970
+
971
+ function parsePrNumber(value) {
972
+ return parseIssueNumber(value);
973
+ }
974
+
975
+ function resolveOwnerRepo(taskDir) {
976
+ const gitResult = spawnSync("git", ["remote", "get-url", "origin"], {
977
+ cwd: taskDir,
978
+ encoding: "utf8"
979
+ });
980
+
981
+ if (gitResult.status !== 0) {
982
+ return { ok: false, message: `Unable to resolve git remote: ${gitResult.stderr.trim() || gitResult.stdout.trim()}` };
983
+ }
984
+
985
+ const remote = gitResult.stdout.trim();
986
+ const sshMatch = remote.match(/[:/]([^/]+\/[^/]+?)(?:\.git)?$/);
987
+ if (!sshMatch) {
988
+ return { ok: false, message: `Unable to parse owner/repo from remote '${remote}'` };
989
+ }
990
+
991
+ return { ok: true, value: sshMatch[1] };
992
+ }
993
+
994
+ function ghJson(args, cwd) {
995
+ const result = spawnSync("gh", args, {
996
+ cwd,
997
+ encoding: "utf8",
998
+ env: process.env
999
+ });
1000
+
1001
+ if (result.status !== 0) {
1002
+ const stderr = `${result.stderr || ""}${result.stdout || ""}`.trim();
1003
+ const classified = classifyGhFailure(stderr, args);
1004
+ return { ok: false, type: classified.type, message: classified.message };
1005
+ }
1006
+
1007
+ try {
1008
+ return { ok: true, value: JSON.parse(result.stdout || "null") };
1009
+ } catch (error) {
1010
+ return { ok: false, type: "network_error", message: `Invalid JSON from gh: ${error.message}` };
1011
+ }
1012
+ }
1013
+
1014
+ function ghPaginatedJson(args, cwd) {
1015
+ return ghJson(args, cwd);
1016
+ }
1017
+
1018
+ function withRetry(operation) {
1019
+ const delays = getRetryDelays();
1020
+ let lastFailure = null;
1021
+
1022
+ for (let attempt = 0; attempt <= delays.length; attempt += 1) {
1023
+ const result = operation();
1024
+ if (result.ok) {
1025
+ return result;
1026
+ }
1027
+
1028
+ lastFailure = result;
1029
+ if (result.type === "check_failed") {
1030
+ return result;
1031
+ }
1032
+
1033
+ if (attempt < delays.length) {
1034
+ sleep(delays[attempt]);
1035
+ }
1036
+ }
1037
+
1038
+ return lastFailure || { ok: false, type: "network_error", message: "Unknown GitHub sync failure" };
1039
+ }
1040
+
1041
+ function classifyGhFailure(stderr, args) {
1042
+ const message = stderr || `gh ${args.join(" ")} failed`;
1043
+
1044
+ if (/not found|could not resolve to an issue|http 404/i.test(message)) {
1045
+ return { type: "check_failed", message };
1046
+ }
1047
+
1048
+ return { type: "network_error", message };
1049
+ }
1050
+
1051
+ function getRetryDelays() {
1052
+ const override = process.env.VALIDATE_ARTIFACT_RETRY_DELAYS_MS;
1053
+ if (!override) {
1054
+ return DEFAULT_RETRY_DELAYS_MS;
1055
+ }
1056
+
1057
+ const parsed = override
1058
+ .split(",")
1059
+ .map((value) => Number(value.trim()))
1060
+ .filter((value) => Number.isFinite(value) && value >= 0);
1061
+
1062
+ return parsed.length > 0 ? parsed : DEFAULT_RETRY_DELAYS_MS;
1063
+ }
1064
+
1065
+ function sleep(delayMs) {
1066
+ if (delayMs <= 0) {
1067
+ return;
1068
+ }
1069
+
1070
+ Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, delayMs);
1071
+ }
1072
+
1073
+ // === Utilities ===
1074
+
1075
+ function minutesSinceTimestamp(timestamp) {
1076
+ const normalized = timestamp.replace(" ", "T");
1077
+ const parsed = Date.parse(normalized);
1078
+ if (Number.isNaN(parsed)) {
1079
+ return Number.POSITIVE_INFINITY;
1080
+ }
1081
+
1082
+ return (Date.now() - parsed) / 60000;
1083
+ }
1084
+
1085
+ function interpolate(template, taskDir, artifactFile) {
1086
+ const artifactStem = artifactFile ? path.basename(artifactFile, path.extname(artifactFile)) : "";
1087
+ return template
1088
+ .replace(/\{task-id\}/g, path.basename(taskDir))
1089
+ .replace(/\{artifact-stem\}/g, artifactStem);
1090
+ }
1091
+
1092
+ function summarizeGate(checks) {
1093
+ if (checks.some((check) => check.status === "blocked")) {
1094
+ return "blocked";
1095
+ }
1096
+
1097
+ if (checks.some((check) => check.status === "fail")) {
1098
+ return "fail";
1099
+ }
1100
+
1101
+ return "pass";
1102
+ }
1103
+
1104
+ function summarizeChecks(checks) {
1105
+ const counts = {
1106
+ pass: checks.filter((check) => check.status === "pass").length,
1107
+ fail: checks.filter((check) => check.status === "fail").length,
1108
+ blocked: checks.filter((check) => check.status === "blocked").length
1109
+ };
1110
+
1111
+ if (counts.blocked > 0) {
1112
+ return `${counts.pass} passed, ${counts.fail} failed, ${counts.blocked} blocked`;
1113
+ }
1114
+
1115
+ return `${counts.pass} passed, ${counts.fail} failed`;
1116
+ }
1117
+
1118
+ function buildAction(gate, checks) {
1119
+ if (gate === "pass") {
1120
+ return "All declared checks passed";
1121
+ }
1122
+
1123
+ const firstFailure = checks.find((check) => check.status !== "pass");
1124
+ if (!firstFailure) {
1125
+ return "Review validation output";
1126
+ }
1127
+
1128
+ if (gate === "blocked") {
1129
+ return `Resolve blocked ${firstFailure.type} check and re-run gate`;
1130
+ }
1131
+
1132
+ return `Fix ${firstFailure.type} issues and re-run gate`;
1133
+ }
1134
+
1135
+ function buildCheckAction(result) {
1136
+ if (result.status === "pass") {
1137
+ return "Requested check passed";
1138
+ }
1139
+
1140
+ if (result.status === "blocked") {
1141
+ return `Resolve blocked ${result.type} check and re-run check`;
1142
+ }
1143
+
1144
+ return `Fix ${result.type} issues and re-run check`;
1145
+ }
1146
+
1147
+ function buildSingleCheckSummary(status) {
1148
+ if (status === "pass") {
1149
+ return "1 passed, 0 failed";
1150
+ }
1151
+
1152
+ if (status === "blocked") {
1153
+ return "0 passed, 0 failed, 1 blocked";
1154
+ }
1155
+
1156
+ return "0 passed, 1 failed";
1157
+ }
1158
+
1159
+ function passResult(type, message) {
1160
+ return { type, status: "pass", message };
1161
+ }
1162
+
1163
+ function failResult(type, message, failType = "check_failed") {
1164
+ return { type, status: "fail", fail_type: failType, message };
1165
+ }
1166
+
1167
+ function blockedResult(type, message, failType = "network_error") {
1168
+ return { type, status: "blocked", fail_type: failType, message };
1169
+ }
1170
+
1171
+ function safeStat(filePath) {
1172
+ try {
1173
+ return fs.statSync(filePath);
1174
+ } catch {
1175
+ return null;
1176
+ }
1177
+ }
1178
+
1179
+ function escapePattern(pattern) {
1180
+ return escapeRegExp(pattern)
1181
+ .replace(/\\\{N\\\}/g, "(\\d+)");
1182
+ }
1183
+
1184
+ function escapeRegExp(value) {
1185
+ return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
1186
+ }
1187
+
1188
+ function isBlank(value) {
1189
+ return value === undefined || value === null || String(value).trim() === "";
1190
+ }
1191
+
1192
+ function extractOption(args, name) {
1193
+ const rest = [];
1194
+ let value;
1195
+
1196
+ for (let index = 0; index < args.length; index += 1) {
1197
+ const arg = args[index];
1198
+ if (arg === name) {
1199
+ value = args[index + 1];
1200
+ index += 1;
1201
+ continue;
1202
+ }
1203
+
1204
+ const inlinePrefix = `${name}=`;
1205
+ if (arg.startsWith(inlinePrefix)) {
1206
+ value = arg.slice(inlinePrefix.length);
1207
+ continue;
1208
+ }
1209
+
1210
+ rest.push(arg);
1211
+ }
1212
+
1213
+ return { value, rest };
1214
+ }
1215
+
1216
+ function normalizeFormat(value) {
1217
+ return value === "text" ? "text" : "json";
1218
+ }
1219
+
1220
+ function formatStatusLabel(status) {
1221
+ if (status === "fail") {
1222
+ return "FAIL";
1223
+ }
1224
+
1225
+ if (status === "blocked") {
1226
+ return "BLOCKED";
1227
+ }
1228
+
1229
+ return "pass";
1230
+ }
1231
+
1232
+ function writeOutput(value, format) {
1233
+ if (format === "text") {
1234
+ writeText(value);
1235
+ return;
1236
+ }
1237
+
1238
+ writeJson(value);
1239
+ }
1240
+
1241
+ function writeText(value) {
1242
+ const lines = [];
1243
+
1244
+ if (Array.isArray(value.checks)) {
1245
+ lines.push(`Verification: ${value.gate} | Skill: ${value.skill}`);
1246
+ lines.push("");
1247
+ for (const check of value.checks) {
1248
+ lines.push(` [${formatStatusLabel(check.status)}] ${check.type} - ${check.message}`);
1249
+ }
1250
+ lines.push("");
1251
+ lines.push(`Result: ${value.summary} - ${value.action}`);
1252
+ } else {
1253
+ lines.push(`Check: ${value.status} | Skill: ${value.skill} | Type: ${value.type}`);
1254
+ lines.push("");
1255
+ lines.push(` [${formatStatusLabel(value.status)}] ${value.type} - ${value.message}`);
1256
+ lines.push("");
1257
+ lines.push(`Result: ${buildSingleCheckSummary(value.status)} - ${buildCheckAction(value)}`);
1258
+ }
1259
+
1260
+ process.stdout.write(`${lines.join("\n")}\n`);
1261
+ }
1262
+
1263
+ function writeJson(value) {
1264
+ process.stdout.write(`${JSON.stringify(value, null, 2)}\n`);
1265
+ }
1266
+
1267
+ function printUsageAndExit() {
1268
+ failUsage(
1269
+ "Usage:\n" +
1270
+ " node .agents/scripts/validate-artifact.js gate <skill-name> <task-dir> [artifact-file] [--format json|text]\n" +
1271
+ " node .agents/scripts/validate-artifact.js check <type> <task-dir> [artifact-file] --skill <skill-name> [--format json|text]"
1272
+ );
1273
+ }
1274
+
1275
+ function failUsage(message) {
1276
+ process.stderr.write(`${message}\n`);
1277
+ process.exit(1);
1278
+ }
1279
+
1280
+ main(process.argv.slice(2));