@fitlab-ai/agent-infra 0.5.2 → 0.5.4

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 (85) hide show
  1. package/README.md +3 -3
  2. package/README.zh-CN.md +3 -3
  3. package/lib/merge.js +22 -7
  4. package/lib/sandbox/commands/rm.js +1 -1
  5. package/lib/sandbox/runtimes/base.dockerfile +17 -1
  6. package/package.json +1 -1
  7. package/templates/.agents/rules/issue-pr-commands.github.en.md +25 -9
  8. package/templates/.agents/rules/issue-pr-commands.github.zh-CN.md +25 -9
  9. package/templates/.agents/rules/issue-sync.github.en.md +111 -23
  10. package/templates/.agents/rules/issue-sync.github.zh-CN.md +105 -17
  11. package/templates/.agents/rules/milestone-inference.github.en.md +13 -6
  12. package/templates/.agents/rules/milestone-inference.github.zh-CN.md +13 -6
  13. package/templates/.agents/rules/pr-sync.github.en.md +3 -1
  14. package/templates/.agents/rules/pr-sync.github.zh-CN.md +3 -1
  15. package/templates/.agents/scripts/platform-adapters/platform-sync.github.js +1080 -0
  16. package/templates/.agents/scripts/validate-artifact.js +54 -805
  17. package/templates/.agents/skills/analyze-task/SKILL.en.md +4 -4
  18. package/templates/.agents/skills/analyze-task/SKILL.zh-CN.md +4 -4
  19. package/templates/.agents/skills/analyze-task/config/verify.json +1 -1
  20. package/templates/.agents/skills/archive-tasks/scripts/archive-tasks.sh +1 -1
  21. package/templates/.agents/skills/block-task/SKILL.en.md +4 -4
  22. package/templates/.agents/skills/block-task/SKILL.zh-CN.md +4 -4
  23. package/templates/.agents/skills/block-task/config/verify.json +1 -1
  24. package/templates/.agents/skills/cancel-task/SKILL.en.md +18 -18
  25. package/templates/.agents/skills/cancel-task/SKILL.zh-CN.md +18 -18
  26. package/templates/.agents/skills/cancel-task/config/verify.json +1 -1
  27. package/templates/.agents/skills/close-codescan/SKILL.en.md +2 -2
  28. package/templates/.agents/skills/close-codescan/SKILL.zh-CN.md +2 -2
  29. package/templates/.agents/skills/close-dependabot/SKILL.en.md +2 -2
  30. package/templates/.agents/skills/close-dependabot/SKILL.zh-CN.md +2 -2
  31. package/templates/.agents/skills/commit/SKILL.en.md +15 -3
  32. package/templates/.agents/skills/commit/SKILL.zh-CN.md +15 -3
  33. package/templates/.agents/skills/commit/config/verify.json +2 -1
  34. package/templates/.agents/skills/commit/reference/issue-metadata-sync.en.md +23 -0
  35. package/templates/.agents/skills/commit/reference/issue-metadata-sync.zh-CN.md +23 -0
  36. package/templates/.agents/skills/commit/reference/task-status-update.en.md +2 -2
  37. package/templates/.agents/skills/commit/reference/task-status-update.zh-CN.md +2 -2
  38. package/templates/.agents/skills/complete-task/SKILL.en.md +13 -13
  39. package/templates/.agents/skills/complete-task/SKILL.zh-CN.md +13 -13
  40. package/templates/.agents/skills/complete-task/config/verify.json +1 -1
  41. package/templates/.agents/skills/create-issue/SKILL.en.md +4 -2
  42. package/templates/.agents/skills/create-issue/SKILL.zh-CN.md +4 -2
  43. package/templates/.agents/skills/create-issue/config/verify.json +1 -1
  44. package/templates/.agents/skills/create-issue/reference/label-and-type.en.md +6 -1
  45. package/templates/.agents/skills/create-issue/reference/label-and-type.zh-CN.md +6 -1
  46. package/templates/.agents/skills/create-pr/SKILL.en.md +5 -5
  47. package/templates/.agents/skills/create-pr/SKILL.zh-CN.md +5 -5
  48. package/templates/.agents/skills/create-pr/config/verify.json +1 -1
  49. package/templates/.agents/skills/create-pr/reference/pr-body-template.en.md +9 -5
  50. package/templates/.agents/skills/create-pr/reference/pr-body-template.zh-CN.md +9 -5
  51. package/templates/.agents/skills/create-task/SKILL.en.md +4 -4
  52. package/templates/.agents/skills/create-task/SKILL.zh-CN.md +4 -4
  53. package/templates/.agents/skills/create-task/config/verify.json +1 -1
  54. package/templates/.agents/skills/implement-task/SKILL.en.md +6 -6
  55. package/templates/.agents/skills/implement-task/SKILL.zh-CN.md +6 -6
  56. package/templates/.agents/skills/implement-task/config/verify.json +1 -2
  57. package/templates/.agents/skills/import-codescan/SKILL.en.md +2 -2
  58. package/templates/.agents/skills/import-codescan/SKILL.zh-CN.md +2 -2
  59. package/templates/.agents/skills/import-codescan/config/verify.json +1 -1
  60. package/templates/.agents/skills/import-dependabot/SKILL.en.md +2 -2
  61. package/templates/.agents/skills/import-dependabot/SKILL.zh-CN.md +2 -2
  62. package/templates/.agents/skills/import-dependabot/config/verify.json +1 -1
  63. package/templates/.agents/skills/import-issue/SKILL.en.md +5 -5
  64. package/templates/.agents/skills/import-issue/SKILL.zh-CN.md +5 -5
  65. package/templates/.agents/skills/import-issue/config/verify.json +1 -1
  66. package/templates/.agents/skills/plan-task/SKILL.en.md +4 -4
  67. package/templates/.agents/skills/plan-task/SKILL.zh-CN.md +4 -4
  68. package/templates/.agents/skills/plan-task/config/verify.json +1 -1
  69. package/templates/.agents/skills/refine-task/SKILL.en.md +4 -6
  70. package/templates/.agents/skills/refine-task/SKILL.zh-CN.md +4 -6
  71. package/templates/.agents/skills/refine-task/config/verify.json +1 -2
  72. package/templates/.agents/skills/refine-title/SKILL.en.md +5 -1
  73. package/templates/.agents/skills/refine-title/SKILL.zh-CN.md +5 -1
  74. package/templates/.agents/skills/restore-task/SKILL.en.md +2 -2
  75. package/templates/.agents/skills/restore-task/SKILL.zh-CN.md +2 -2
  76. package/templates/.agents/skills/restore-task/config/verify.json +1 -1
  77. package/templates/.agents/skills/review-task/SKILL.en.md +4 -4
  78. package/templates/.agents/skills/review-task/SKILL.zh-CN.md +4 -4
  79. package/templates/.agents/skills/review-task/config/verify.json +1 -1
  80. package/templates/.agents/skills/update-agent-infra/scripts/sync-templates.js +1 -1
  81. package/templates/.agents/templates/task.en.md +3 -3
  82. package/templates/.agents/templates/task.zh-CN.md +3 -3
  83. package/templates/.github/workflows/metadata-sync.yml +127 -0
  84. package/templates/.github/workflows/pr-label.yml +75 -0
  85. package/templates/.github/workflows/status-label.yml +12 -8
@@ -1,7 +1,6 @@
1
1
  import fs from "node:fs";
2
2
  import path from "node:path";
3
3
  import process from "node:process";
4
- import { spawnSync } from "node:child_process";
5
4
  import { fileURLToPath } from "node:url";
6
5
 
7
6
  const EXIT_CODE = {
@@ -27,15 +26,47 @@ const DEFAULT_REQUIRED_FIELDS = [
27
26
  "assigned_to"
28
27
  ];
29
28
 
30
- const DEFAULT_RETRY_DELAYS_MS = [3000, 10000];
31
29
  const DEFAULT_FRESHNESS_MINUTES = 30;
32
- const DATE_TIME_PATTERN = /^\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}$/;
33
- const ACTIVITY_LOG_PATTERN = /^- (\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}) — \*\*(.+?)\*\* by (.+?) — (.+)$/;
30
+ const DATE_TIME_PATTERN = /^\d{4}-\d{2}-\d{2}[T ]\d{2}:\d{2}:\d{2}(?:[+-]\d{2}:\d{2})?$/;
31
+ const ACTIVITY_LOG_PATTERN = /^- (\d{4}-\d{2}-\d{2}[T ]\d{2}:\d{2}:\d{2}(?:[+-]\d{2}:\d{2})?) — \*\*(.+?)\*\* by (.+?) — (.+)$/;
34
32
  const BRANCH_SLUG_PATTERN = /^[a-z0-9]+(?:-[a-z0-9]+)*$/;
35
33
 
36
34
  const scriptPath = fileURLToPath(import.meta.url);
37
35
  const repoRoot = path.resolve(path.dirname(scriptPath), "..", "..");
38
36
 
37
+ const PLATFORM_ADAPTERS = {};
38
+ const OPTIONAL_PLATFORM_CHECKS = new Set(["platform-sync"]);
39
+ const adaptersDir = path.join(path.dirname(scriptPath), "platform-adapters");
40
+
41
+ if (fs.existsSync(adaptersDir)) {
42
+ for (const file of fs.readdirSync(adaptersDir)) {
43
+ if (!file.endsWith(".js")) {
44
+ continue;
45
+ }
46
+
47
+ const adapterName = path.basename(file, ".js");
48
+ const mod = await import(new URL(`./platform-adapters/${file}`, import.meta.url));
49
+ if (typeof mod.check === "function") {
50
+ PLATFORM_ADAPTERS[adapterName] = mod.check;
51
+ }
52
+ }
53
+ }
54
+
55
+ const sharedUtils = {
56
+ loadTask,
57
+ getCheckedRequirements,
58
+ normalizeContent,
59
+ isBlank,
60
+ escapeRegExp,
61
+ passResult,
62
+ failResult,
63
+ blockedResult,
64
+ safeStat,
65
+ parseIssueNumber,
66
+ parsePrNumber,
67
+ repoRoot
68
+ };
69
+
39
70
  // === CLI Entry ===
40
71
 
41
72
  function main(argv) {
@@ -155,10 +186,18 @@ function runCheck(type, context) {
155
186
  return checkActivityLog(context);
156
187
  case "completion-checklist":
157
188
  return checkCompletionChecklist(context);
158
- case "github-sync":
159
- return checkGithubSync(context);
160
- default:
161
- return failResult(type, `Unsupported check type '${type}'.`);
189
+ default: {
190
+ const adapter = PLATFORM_ADAPTERS[type];
191
+ if (!adapter) {
192
+ if (OPTIONAL_PLATFORM_CHECKS.has(type)) {
193
+ return passResult(type, `Skipped: no platform adapter registered for '${type}'`);
194
+ }
195
+
196
+ return failResult(type, `Unsupported check type '${type}'.`);
197
+ }
198
+
199
+ return adapter(context, sharedUtils);
200
+ }
162
201
  }
163
202
  }
164
203
 
@@ -419,40 +458,6 @@ function checkCompletionChecklist({ taskDir, config }) {
419
458
  return passResult("completion-checklist", `Completion Checklist valid (${items.length} items checked)`);
420
459
  }
421
460
 
422
- function checkGithubSync({ taskDir, config, artifactFile }) {
423
- const context = buildSyncContext({ taskDir, config, artifactFile });
424
- if (context.earlyReturn) {
425
- return context.earlyReturn;
426
- }
427
-
428
- const remoteData = fetchRemoteData(context);
429
- if (remoteData.earlyReturn) {
430
- return remoteData.earlyReturn;
431
- }
432
-
433
- const subChecks = [
434
- checkStatusLabel,
435
- checkCommentMarker,
436
- checkPrCommentMarker,
437
- checkPrCommentLastCommit,
438
- checkCommentContent,
439
- checkTaskCommentContent,
440
- checkInLabelsMatchPr,
441
- checkSyncedRequirements,
442
- checkIssueType,
443
- checkMilestone
444
- ];
445
-
446
- for (const subCheck of subChecks) {
447
- const result = subCheck(context, remoteData);
448
- if (result) {
449
- return result;
450
- }
451
- }
452
-
453
- return passResult("github-sync", `GitHub sync checks passed for Issue #${context.issueNumber}`);
454
- }
455
-
456
461
  // === File & Config Loaders ===
457
462
 
458
463
  function loadVerifyConfig(skillName) {
@@ -572,634 +577,6 @@ function getCheckedRequirements(content) {
572
577
  .map((match) => match[1].trim());
573
578
  }
574
579
 
575
- function buildSyncContext({ taskDir, config, artifactFile }) {
576
- const task = loadTask(taskDir);
577
- if (!task.ok) {
578
- return { earlyReturn: failResult("github-sync", task.message) };
579
- }
580
-
581
- const issueNumber = parseIssueNumber(task.metadata.issue_number);
582
- const prNumber = parsePrNumber(task.metadata.pr_number);
583
- if (config.when === "issue_number_exists" && !issueNumber) {
584
- return { earlyReturn: passResult("github-sync", "Skipped: task has no issue_number") };
585
- }
586
- if (config.when === "pr_number_exists" && !prNumber) {
587
- return { earlyReturn: passResult("github-sync", "Skipped: task has no pr_number") };
588
- }
589
-
590
- if (!issueNumber) {
591
- return { earlyReturn: passResult("github-sync", "Skipped: github-sync not required for this task") };
592
- }
593
-
594
- const ownerRepo = resolveOwnerRepo(taskDir);
595
- if (!ownerRepo.ok) {
596
- return { earlyReturn: blockedResult("github-sync", ownerRepo.message, "network_error") };
597
- }
598
-
599
- const marker = config.expected_comment_marker
600
- ? interpolate(config.expected_comment_marker, taskDir, artifactFile)
601
- : null;
602
- const prMarker = config.expected_pr_comment_marker
603
- ? interpolate(config.expected_pr_comment_marker, taskDir, artifactFile)
604
- : null;
605
- const artifactPath = artifactFile ? path.join(taskDir, artifactFile) : null;
606
-
607
- return {
608
- task,
609
- taskDir,
610
- config,
611
- artifactFile,
612
- artifactPath,
613
- issueNumber,
614
- prNumber,
615
- ownerRepo: ownerRepo.value,
616
- marker,
617
- prMarker
618
- };
619
- }
620
-
621
- function fetchRemoteData(context) {
622
- const issueResult = withRetry(() => ghJson([
623
- "issue",
624
- "view",
625
- String(context.issueNumber),
626
- "--json",
627
- "state,labels,body,milestone"
628
- ], context.taskDir));
629
- if (!issueResult.ok) {
630
- return {
631
- earlyReturn: issueResult.type === "check_failed"
632
- ? failResult("github-sync", issueResult.message, issueResult.type)
633
- : blockedResult("github-sync", issueResult.message, issueResult.type)
634
- };
635
- }
636
-
637
- const issue = issueResult.value;
638
- if (context.config.issue_must_exist !== false && !issue) {
639
- return {
640
- earlyReturn: failResult("github-sync", `Issue #${context.issueNumber} not found`, "check_failed")
641
- };
642
- }
643
-
644
- let comments = null;
645
- if (shouldFetchComments(context.config)) {
646
- const commentsResult = withRetry(() => ghPaginatedJson([
647
- "api",
648
- "--paginate",
649
- "--slurp",
650
- `repos/${context.ownerRepo}/issues/${context.issueNumber}/comments?per_page=100`
651
- ], context.taskDir));
652
-
653
- if (!commentsResult.ok) {
654
- return {
655
- earlyReturn: commentsResult.type === "check_failed"
656
- ? failResult("github-sync", commentsResult.message, commentsResult.type)
657
- : blockedResult("github-sync", commentsResult.message, commentsResult.type)
658
- };
659
- }
660
-
661
- comments = flattenComments(commentsResult.value);
662
- }
663
-
664
- let prComments = null;
665
- if (context.config.expected_pr_comment_marker) {
666
- if (!context.prNumber) {
667
- return {
668
- earlyReturn: failResult("github-sync", "Expected a valid pr_number for PR comment verification", "check_failed")
669
- };
670
- }
671
-
672
- const prCommentsResult = withRetry(() => ghPaginatedJson([
673
- "api",
674
- "--paginate",
675
- "--slurp",
676
- `repos/${context.ownerRepo}/issues/${context.prNumber}/comments?per_page=100`
677
- ], context.taskDir));
678
-
679
- if (!prCommentsResult.ok) {
680
- return {
681
- earlyReturn: prCommentsResult.type === "check_failed"
682
- ? failResult("github-sync", prCommentsResult.message, prCommentsResult.type)
683
- : blockedResult("github-sync", prCommentsResult.message, prCommentsResult.type)
684
- };
685
- }
686
-
687
- prComments = flattenComments(prCommentsResult.value);
688
- }
689
-
690
- let issueType;
691
- if (context.config.verify_issue_type) {
692
- const issueTypeResult = withRetry(() => ghText([
693
- "api",
694
- `repos/${context.ownerRepo}/issues/${context.issueNumber}`,
695
- "--jq",
696
- ".type.name // empty"
697
- ], context.taskDir));
698
-
699
- if (issueTypeResult.ok) {
700
- issueType = issueTypeResult.value || null;
701
- }
702
- }
703
-
704
- let prLabels = null;
705
- let prMilestone;
706
- if ((context.config.verify_in_labels_match_pr || context.config.verify_milestone) && context.prNumber) {
707
- const prFields = [];
708
- if (context.config.verify_in_labels_match_pr) {
709
- prFields.push("labels");
710
- }
711
- if (context.config.verify_milestone) {
712
- prFields.push("milestone");
713
- }
714
-
715
- const prResult = withRetry(() => ghJson([
716
- "pr",
717
- "view",
718
- String(context.prNumber),
719
- "--json",
720
- prFields.join(",")
721
- ], context.taskDir));
722
-
723
- if (!prResult.ok) {
724
- return {
725
- earlyReturn: prResult.type === "check_failed"
726
- ? failResult("github-sync", prResult.message, prResult.type)
727
- : blockedResult("github-sync", prResult.message, prResult.type)
728
- };
729
- }
730
-
731
- prLabels = context.config.verify_in_labels_match_pr
732
- ? extractLabelNames(prResult.value?.labels)
733
- : null;
734
- prMilestone = context.config.verify_milestone
735
- ? prResult.value?.milestone ?? null
736
- : undefined;
737
- }
738
-
739
- return {
740
- issue,
741
- comments,
742
- prComments,
743
- prLabels,
744
- issueType,
745
- prMilestone
746
- };
747
- }
748
-
749
- function shouldFetchComments(config) {
750
- return Boolean(
751
- config.expected_comment_marker
752
- || config.expected_pr_comment_marker
753
- || config.verify_pr_comment_last_commit_matches_head
754
- || config.verify_comment_content
755
- || config.verify_task_comment_content
756
- );
757
- }
758
-
759
- function flattenComments(value) {
760
- if (!Array.isArray(value)) {
761
- return [];
762
- }
763
-
764
- return value.flatMap((page) => Array.isArray(page) ? page : []);
765
- }
766
-
767
- function checkStatusLabel(context, remoteData) {
768
- if (!context.config.expected_status_label || remoteData.issue.state !== "OPEN") {
769
- return null;
770
- }
771
-
772
- const labels = extractLabelNames(remoteData.issue.labels);
773
- if (labels.includes(context.config.expected_status_label)) {
774
- return null;
775
- }
776
-
777
- return failResult(
778
- "github-sync",
779
- `Expected label '${context.config.expected_status_label}' not found on Issue #${context.issueNumber}`,
780
- "check_failed"
781
- );
782
- }
783
-
784
- function checkCommentMarker(context, remoteData) {
785
- if (!context.marker) {
786
- return null;
787
- }
788
-
789
- const comment = findCommentByMarker(remoteData.comments, context.marker);
790
- if (comment) {
791
- return null;
792
- }
793
-
794
- return failResult(
795
- "github-sync",
796
- `Expected comment marker '${context.marker}' not found on Issue #${context.issueNumber}`,
797
- "check_failed"
798
- );
799
- }
800
-
801
- function checkPrCommentMarker(context, remoteData) {
802
- if (!context.prMarker) {
803
- return null;
804
- }
805
-
806
- const comment = findCommentByMarker(remoteData.prComments, context.prMarker);
807
- if (comment) {
808
- return null;
809
- }
810
-
811
- return failResult(
812
- "github-sync",
813
- `Expected PR comment marker '${context.prMarker}' not found on PR #${context.prNumber}`,
814
- "check_failed"
815
- );
816
- }
817
-
818
- function checkPrCommentLastCommit(context, remoteData) {
819
- if (!context.config.verify_pr_comment_last_commit_matches_head) {
820
- return null;
821
- }
822
-
823
- if (!context.prMarker) {
824
- return failResult(
825
- "github-sync",
826
- "verify_pr_comment_last_commit_matches_head requires expected_pr_comment_marker",
827
- "check_failed"
828
- );
829
- }
830
-
831
- const comment = findCommentByMarker(remoteData.prComments, context.prMarker);
832
- if (!comment) {
833
- return failResult(
834
- "github-sync",
835
- `Expected PR comment marker '${context.prMarker}' not found on PR #${context.prNumber}`,
836
- "check_failed"
837
- );
838
- }
839
-
840
- const match = String(comment.body || "").match(/<!--\s*last-commit:\s*([0-9a-f]{7,40})\s*-->/i);
841
- if (!match) {
842
- return failResult(
843
- "github-sync",
844
- `PR #${context.prNumber} summary comment is missing '<!-- last-commit: <sha> -->' metadata`,
845
- "check_failed"
846
- );
847
- }
848
-
849
- const headResult = withRetry(() => gitText(["rev-parse", "HEAD"], context.taskDir));
850
- if (!headResult.ok) {
851
- return headResult.type === "check_failed"
852
- ? failResult("github-sync", headResult.message, headResult.type)
853
- : blockedResult("github-sync", headResult.message, headResult.type);
854
- }
855
-
856
- const expectedHead = String(headResult.value || "").trim();
857
- const actualHead = match[1].trim();
858
- if (expectedHead === actualHead) {
859
- return null;
860
- }
861
-
862
- return failResult(
863
- "github-sync",
864
- `PR #${context.prNumber} summary comment last-commit metadata mismatch: expected ${expectedHead}, got ${actualHead}`,
865
- "check_failed"
866
- );
867
- }
868
-
869
- function checkCommentContent(context, remoteData) {
870
- if (!context.config.verify_comment_content) {
871
- return null;
872
- }
873
-
874
- if (!context.marker) {
875
- return failResult("github-sync", "verify_comment_content requires expected_comment_marker", "check_failed");
876
- }
877
-
878
- if (!context.artifactPath || !safeStat(context.artifactPath)) {
879
- return failResult(
880
- "github-sync",
881
- `Artifact not found for comment verification: ${context.artifactFile || "(missing artifactFile)"}`,
882
- "check_failed"
883
- );
884
- }
885
-
886
- const comment = findCommentByMarker(remoteData.comments, context.marker);
887
- const localContent = normalizeContent(fs.readFileSync(context.artifactPath, "utf8"));
888
- const commentContent = normalizeContent(extractCommentBody(comment?.body || ""));
889
-
890
- if (localContent === commentContent) {
891
- return null;
892
- }
893
-
894
- return failResult(
895
- "github-sync",
896
- buildCommentContentMismatchMessage(
897
- path.basename(context.artifactPath, path.extname(context.artifactPath)),
898
- context.issueNumber,
899
- localContent,
900
- commentContent
901
- ),
902
- "check_failed"
903
- );
904
- }
905
-
906
- function checkTaskCommentContent(context, remoteData) {
907
- if (!context.config.verify_task_comment_content) {
908
- return null;
909
- }
910
-
911
- const taskMarker = `<!-- sync-issue:${context.task.metadata.id}:task -->`;
912
- const comment = findCommentByMarker(remoteData.comments, taskMarker);
913
- if (!comment) {
914
- return failResult(
915
- "github-sync",
916
- `Expected comment marker '${taskMarker}' not found on Issue #${context.issueNumber}`,
917
- "check_failed"
918
- );
919
- }
920
-
921
- const expectedBody = normalizeContent(buildExpectedTaskBody(context.task.content));
922
- const commentBody = normalizeContent(extractCommentBody(comment.body || ""));
923
-
924
- if (expectedBody === commentBody) {
925
- return null;
926
- }
927
-
928
- return failResult(
929
- "github-sync",
930
- buildCommentContentMismatchMessage("task", context.issueNumber, expectedBody, commentBody),
931
- "check_failed"
932
- );
933
- }
934
-
935
- function checkInLabelsMatchPr(context, remoteData) {
936
- if (!context.config.verify_in_labels_match_pr || !context.prNumber || !remoteData.prLabels) {
937
- return null;
938
- }
939
-
940
- const issueInLabels = extractLabelNames(remoteData.issue.labels)
941
- .filter((label) => label.startsWith("in:"))
942
- .sort();
943
- const prInLabels = remoteData.prLabels
944
- .filter((label) => label.startsWith("in:"))
945
- .sort();
946
-
947
- if (arraysEqual(issueInLabels, prInLabels)) {
948
- return null;
949
- }
950
-
951
- return failResult(
952
- "github-sync",
953
- `in: labels mismatch — PR #${context.prNumber} has [${formatLabelList(prInLabels)}], Issue #${context.issueNumber} has [${formatLabelList(issueInLabels)}]`,
954
- "check_failed"
955
- );
956
- }
957
-
958
- function checkSyncedRequirements(context, remoteData) {
959
- if (!context.config.sync_checked_requirements) {
960
- return null;
961
- }
962
-
963
- const checkedRequirements = getCheckedRequirements(context.task.content);
964
- if (checkedRequirements.length === 0) {
965
- return null;
966
- }
967
-
968
- const issueBody = remoteData.issue.body || "";
969
- const missingRequirements = checkedRequirements.filter(
970
- (item) => !new RegExp(`^- \\[x\\] ${escapeRegExp(item)}$`, "m").test(issueBody)
971
- );
972
- if (missingRequirements.length === 0) {
973
- return null;
974
- }
975
-
976
- return failResult(
977
- "github-sync",
978
- `Issue body is missing checked requirements: ${missingRequirements.join(", ")}`,
979
- "check_failed"
980
- );
981
- }
982
-
983
- function checkIssueType(context, remoteData) {
984
- if (!context.config.verify_issue_type) {
985
- return null;
986
- }
987
-
988
- if (remoteData.issueType === undefined) {
989
- return null;
990
- }
991
-
992
- if (!remoteData.issueType) {
993
- return failResult(
994
- "github-sync",
995
- `Issue #${context.issueNumber} has no Issue Type set`,
996
- "check_failed"
997
- );
998
- }
999
-
1000
- const expectedType = mapTaskTypeToIssueType(context.task.metadata.type);
1001
- if (expectedType && remoteData.issueType !== expectedType) {
1002
- return failResult(
1003
- "github-sync",
1004
- `Issue #${context.issueNumber} has type '${remoteData.issueType}', expected '${expectedType}' (from task type '${context.task.metadata.type}')`,
1005
- "check_failed"
1006
- );
1007
- }
1008
-
1009
- return null;
1010
- }
1011
-
1012
- function checkMilestone(context, remoteData) {
1013
- if (!context.config.verify_milestone) {
1014
- return null;
1015
- }
1016
-
1017
- if (!remoteData.issue?.milestone?.title) {
1018
- return failResult(
1019
- "github-sync",
1020
- `Issue #${context.issueNumber} has no milestone set`,
1021
- "check_failed"
1022
- );
1023
- }
1024
-
1025
- if (context.prNumber && remoteData.prMilestone !== undefined && !remoteData.prMilestone?.title) {
1026
- return failResult(
1027
- "github-sync",
1028
- `PR #${context.prNumber} has no milestone set`,
1029
- "check_failed"
1030
- );
1031
- }
1032
-
1033
- return null;
1034
- }
1035
-
1036
- function findCommentByMarker(comments, marker) {
1037
- return (comments || []).find((comment) => typeof comment.body === "string" && comment.body.includes(marker)) || null;
1038
- }
1039
-
1040
- function extractCommentBody(commentBody) {
1041
- const lines = String(commentBody || "").split(/\r?\n/);
1042
-
1043
- let start = 0;
1044
- while (start < lines.length && (lines[start].trim() === "" || /^<!--.*-->$/.test(lines[start].trim()))) {
1045
- start += 1;
1046
- }
1047
-
1048
- if (start < lines.length && lines[start].startsWith("## ")) {
1049
- start += 1;
1050
- }
1051
-
1052
- while (start < lines.length && lines[start].trim() === "") {
1053
- start += 1;
1054
- }
1055
-
1056
- if (start < lines.length && /^> \*\*.+\*\* · .+$/.test(lines[start].trim())) {
1057
- start += 1;
1058
- }
1059
-
1060
- while (start < lines.length && lines[start].trim() === "") {
1061
- start += 1;
1062
- }
1063
-
1064
- let end = lines.length;
1065
- for (let index = lines.length - 1; index >= start; index -= 1) {
1066
- const trimmed = lines[index].trim();
1067
- if (trimmed === "") {
1068
- continue;
1069
- }
1070
-
1071
- if (/^\*.*\*$/.test(trimmed)) {
1072
- end = index;
1073
- if (end > start && lines[end - 1].trim() === "---") {
1074
- end -= 1;
1075
- }
1076
- }
1077
- break;
1078
- }
1079
-
1080
- return lines.slice(start, end).join("\n");
1081
- }
1082
-
1083
- function buildExpectedTaskBody(taskContent) {
1084
- const frontmatterMatch = taskContent.match(/^---\r?\n([\s\S]*?)\r?\n---(?:\r?\n|$)/);
1085
- if (!frontmatterMatch) {
1086
- return taskContent.trim();
1087
- }
1088
-
1089
- const body = taskContent.slice(frontmatterMatch[0].length).trim();
1090
- return [
1091
- buildTaskFrontmatterSummary(),
1092
- "",
1093
- "```yaml",
1094
- frontmatterMatch[0].trim(),
1095
- "```",
1096
- "",
1097
- "</details>",
1098
- "",
1099
- body
1100
- ].join("\n").trim();
1101
- }
1102
-
1103
- function buildTaskFrontmatterSummary() {
1104
- const language = loadProjectLanguage();
1105
- if (language === "en" || language === "en-US") {
1106
- return "<details><summary>Metadata (frontmatter)</summary>";
1107
- }
1108
-
1109
- return "<details><summary>元数据 (frontmatter)</summary>";
1110
- }
1111
-
1112
- function loadProjectLanguage() {
1113
- const override = process.env.VALIDATE_ARTIFACT_LANGUAGE;
1114
- if (!isBlank(override)) {
1115
- return String(override).trim();
1116
- }
1117
-
1118
- const configPath = path.join(repoRoot, ".agents", ".airc.json");
1119
- if (!fs.existsSync(configPath)) {
1120
- return "";
1121
- }
1122
-
1123
- try {
1124
- const config = JSON.parse(fs.readFileSync(configPath, "utf8"));
1125
- return String(config.language || "").trim();
1126
- } catch {
1127
- return "";
1128
- }
1129
- }
1130
-
1131
- function normalizeContent(text) {
1132
- return String(text || "")
1133
- .replace(/\r\n/g, "\n")
1134
- .replace(/\n{3,}/g, "\n\n")
1135
- .trim();
1136
- }
1137
-
1138
- function buildCommentContentMismatchMessage(fileStem, issueNumber, localContent, commentContent) {
1139
- const diffIndex = firstDifferenceIndex(localContent, commentContent);
1140
- const position = indexToLineColumn(localContent, diffIndex);
1141
-
1142
- 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})`;
1143
- }
1144
-
1145
- function firstDifferenceIndex(left, right) {
1146
- const limit = Math.max(left.length, right.length);
1147
- for (let index = 0; index < limit; index += 1) {
1148
- if (left[index] !== right[index]) {
1149
- return index;
1150
- }
1151
- }
1152
-
1153
- return limit;
1154
- }
1155
-
1156
- function indexToLineColumn(text, index) {
1157
- const prefix = text.slice(0, Math.min(index, text.length));
1158
- const lines = prefix.split("\n");
1159
- return {
1160
- line: lines.length,
1161
- column: (lines.at(-1) || "").length + 1
1162
- };
1163
- }
1164
-
1165
- function extractLabelNames(labels) {
1166
- return (labels || [])
1167
- .map((label) => typeof label === "string" ? label : label?.name)
1168
- .filter((label) => typeof label === "string" && label.length > 0);
1169
- }
1170
-
1171
- function mapTaskTypeToIssueType(taskType) {
1172
- const mapping = {
1173
- bug: "Bug",
1174
- bugfix: "Bug",
1175
- enhancement: "Feature",
1176
- feature: "Feature",
1177
- task: "Task",
1178
- documentation: "Task",
1179
- "dependency-upgrade": "Task",
1180
- chore: "Task",
1181
- docs: "Task",
1182
- refactor: "Task",
1183
- refactoring: "Task"
1184
- };
1185
-
1186
- return mapping[taskType] || "Task";
1187
- }
1188
-
1189
- function arraysEqual(left, right) {
1190
- if (left.length !== right.length) {
1191
- return false;
1192
- }
1193
-
1194
- return left.every((value, index) => value === right[index]);
1195
- }
1196
-
1197
- function formatLabelList(labels) {
1198
- return labels.length > 0 ? labels.join(", ") : "none";
1199
- }
1200
-
1201
- // === GitHub API ===
1202
-
1203
580
  function parseIssueNumber(value) {
1204
581
  if (isBlank(value) || value === "N/A") {
1205
582
  return null;
@@ -1213,145 +590,17 @@ function parsePrNumber(value) {
1213
590
  return parseIssueNumber(value);
1214
591
  }
1215
592
 
1216
- function resolveOwnerRepo(taskDir) {
1217
- const gitResult = spawnSync("git", ["remote", "get-url", "origin"], {
1218
- cwd: taskDir,
1219
- encoding: "utf8"
1220
- });
1221
-
1222
- if (gitResult.status !== 0) {
1223
- return { ok: false, message: `Unable to resolve git remote: ${gitResult.stderr.trim() || gitResult.stdout.trim()}` };
1224
- }
1225
-
1226
- const remote = gitResult.stdout.trim();
1227
- const sshMatch = remote.match(/[:/]([^/]+\/[^/]+?)(?:\.git)?$/);
1228
- if (!sshMatch) {
1229
- return { ok: false, message: `Unable to parse owner/repo from remote '${remote}'` };
1230
- }
1231
-
1232
- return { ok: true, value: sshMatch[1] };
1233
- }
1234
-
1235
- function ghJson(args, cwd) {
1236
- const result = ghCommand(args, cwd);
1237
- if (!result.ok) {
1238
- return result;
1239
- }
1240
-
1241
- try {
1242
- return { ok: true, value: JSON.parse(result.value || "null") };
1243
- } catch (error) {
1244
- return { ok: false, type: "network_error", message: `Invalid JSON from gh: ${error.message}` };
1245
- }
1246
- }
1247
-
1248
- function ghText(args, cwd) {
1249
- const result = ghCommand(args, cwd);
1250
- if (!result.ok) {
1251
- return result;
1252
- }
1253
-
1254
- return { ok: true, value: String(result.value || "").trim() };
1255
- }
1256
-
1257
- function ghCommand(args, cwd) {
1258
- const result = spawnSync("gh", args, {
1259
- cwd,
1260
- encoding: "utf8",
1261
- env: process.env
1262
- });
1263
-
1264
- if (result.status !== 0) {
1265
- const stderr = `${result.stderr || ""}${result.stdout || ""}`.trim();
1266
- const classified = classifyGhFailure(stderr, args);
1267
- return { ok: false, type: classified.type, message: classified.message };
1268
- }
1269
-
1270
- return { ok: true, value: result.stdout };
1271
- }
1272
-
1273
- function ghPaginatedJson(args, cwd) {
1274
- return ghJson(args, cwd);
1275
- }
1276
-
1277
- function gitText(args, cwd) {
1278
- const result = spawnSync("git", args, {
1279
- cwd,
1280
- encoding: "utf8",
1281
- env: process.env
1282
- });
1283
-
1284
- if (result.status !== 0) {
1285
- const stderr = `${result.stderr || ""}${result.stdout || ""}`.trim();
1286
- return {
1287
- ok: false,
1288
- type: "check_failed",
1289
- message: stderr || `git ${args.join(" ")} failed`
1290
- };
1291
- }
1292
-
1293
- return { ok: true, value: String(result.stdout || "").trim() };
1294
- }
1295
-
1296
- function withRetry(operation) {
1297
- const delays = getRetryDelays();
1298
- let lastFailure = null;
1299
-
1300
- for (let attempt = 0; attempt <= delays.length; attempt += 1) {
1301
- const result = operation();
1302
- if (result.ok) {
1303
- return result;
1304
- }
1305
-
1306
- lastFailure = result;
1307
- if (result.type === "check_failed") {
1308
- return result;
1309
- }
1310
-
1311
- if (attempt < delays.length) {
1312
- sleep(delays[attempt]);
1313
- }
1314
- }
1315
-
1316
- return lastFailure || { ok: false, type: "network_error", message: "Unknown GitHub sync failure" };
1317
- }
1318
-
1319
- function classifyGhFailure(stderr, args) {
1320
- const message = stderr || `gh ${args.join(" ")} failed`;
1321
-
1322
- if (/not found|could not resolve to an issue|http 404/i.test(message)) {
1323
- return { type: "check_failed", message };
1324
- }
1325
-
1326
- return { type: "network_error", message };
1327
- }
1328
-
1329
- function getRetryDelays() {
1330
- const override = process.env.VALIDATE_ARTIFACT_RETRY_DELAYS_MS;
1331
- if (!override) {
1332
- return DEFAULT_RETRY_DELAYS_MS;
1333
- }
1334
-
1335
- const parsed = override
1336
- .split(",")
1337
- .map((value) => Number(value.trim()))
1338
- .filter((value) => Number.isFinite(value) && value >= 0);
1339
-
1340
- return parsed.length > 0 ? parsed : DEFAULT_RETRY_DELAYS_MS;
1341
- }
1342
-
1343
- function sleep(delayMs) {
1344
- if (delayMs <= 0) {
1345
- return;
1346
- }
593
+ // === Utilities ===
1347
594
 
1348
- Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, delayMs);
595
+ function normalizeContent(text) {
596
+ return String(text || "")
597
+ .replace(/\r\n/g, "\n")
598
+ .replace(/\n{3,}/g, "\n\n")
599
+ .trim();
1349
600
  }
1350
601
 
1351
- // === Utilities ===
1352
-
1353
602
  function minutesSinceTimestamp(timestamp) {
1354
- const normalized = timestamp.replace(" ", "T");
603
+ const normalized = timestamp.includes("T") ? timestamp : timestamp.replace(" ", "T");
1355
604
  const parsed = Date.parse(normalized);
1356
605
  if (Number.isNaN(parsed)) {
1357
606
  return Number.POSITIVE_INFINITY;