@fitlab-ai/agent-infra 0.9.14 → 0.9.15

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 (250) hide show
  1. package/README.md +10 -0
  2. package/README.zh-CN.md +10 -0
  3. package/bin/internal-cli.sh +45 -0
  4. package/dist/bin/internal-cli.js +93 -30
  5. package/dist/lib/agent-clients/adapters/codex-lifecycle/capability-store.js +165 -27
  6. package/dist/lib/agent-clients/adapters/codex-lifecycle/sandbox-controller.js +3 -2
  7. package/dist/lib/defaults.json +103 -103
  8. package/dist/lib/fs/durable-write.js +37 -0
  9. package/dist/lib/fs/file-lock.js +111 -0
  10. package/dist/lib/host-control/audit.js +19 -0
  11. package/dist/lib/host-control/client.js +142 -0
  12. package/dist/lib/host-control/command.js +49 -0
  13. package/dist/lib/host-control/path.js +160 -0
  14. package/dist/lib/host-control/secure-fs.js +140 -0
  15. package/dist/lib/host-control/server.js +159 -0
  16. package/dist/lib/host-control/service.js +112 -0
  17. package/dist/lib/init.js +11 -2
  18. package/dist/lib/internal/cli-route-inventory.js +3 -1
  19. package/dist/lib/internal/codex-lifecycle.js +6 -6
  20. package/dist/lib/internal/demo-promotion.js +322 -0
  21. package/dist/lib/internal/demo-transcript.js +454 -0
  22. package/dist/lib/internal/host-control.js +28 -0
  23. package/dist/lib/internal/manual-validation.js +322 -0
  24. package/dist/lib/internal/platform-pr.js +18 -4
  25. package/dist/lib/internal/release-workflow.js +63 -84
  26. package/dist/lib/internal/sandbox-control.js +149 -2
  27. package/dist/lib/internal/task-artifact.js +15 -262
  28. package/dist/lib/internal/task-create.js +6 -5
  29. package/dist/lib/internal/task-event.js +37 -37
  30. package/dist/lib/internal/task-operation-registry.js +101 -3
  31. package/dist/lib/internal/task-review.js +8 -53
  32. package/dist/lib/internal/task-validate.js +30 -10
  33. package/dist/lib/owned-path.js +33 -0
  34. package/dist/lib/platform/check-status.js +11 -0
  35. package/dist/lib/platform/comment-safety.js +4 -1
  36. package/dist/lib/platform/completion-backfill.js +1 -1
  37. package/dist/lib/platform/github-data.js +241 -0
  38. package/dist/lib/platform/github-provider.js +15 -20
  39. package/dist/lib/platform/issue-comments.js +2 -2
  40. package/dist/lib/platform/issues.js +1 -179
  41. package/dist/lib/platform/pr-checks.js +12 -65
  42. package/dist/lib/platform/pr-summary.js +55 -3
  43. package/dist/lib/platform/pull-requests.js +2 -105
  44. package/dist/lib/platform/snapshots.js +2 -0
  45. package/dist/lib/platform/verification-sync.js +127 -142
  46. package/dist/lib/release/workflow.js +1 -1
  47. package/dist/lib/sandbox/commands/create.js +7 -2
  48. package/dist/lib/sandbox/commands/rm.js +3 -1514
  49. package/dist/lib/sandbox/control/audit.js +203 -0
  50. package/dist/lib/sandbox/control/client.js +93 -54
  51. package/dist/lib/sandbox/control/executor.js +129 -65
  52. package/dist/lib/sandbox/control/finalization-response.js +27 -0
  53. package/dist/lib/sandbox/control/identity-sentinel.js +107 -0
  54. package/dist/lib/sandbox/control/lifecycle.js +36 -2
  55. package/dist/lib/sandbox/control/native-file-lock.js +6 -98
  56. package/dist/lib/sandbox/control/protocol.js +55 -4
  57. package/dist/lib/sandbox/control/server.js +491 -120
  58. package/dist/lib/sandbox/control/state.js +58 -0
  59. package/dist/lib/sandbox/control/task-workflow.js +74 -0
  60. package/dist/lib/sandbox/control/workflow-executor.js +99 -0
  61. package/dist/lib/sandbox/index.js +2 -2
  62. package/dist/lib/sandbox/recovery.js +28 -4
  63. package/dist/lib/sandbox/removal.js +1717 -0
  64. package/dist/lib/sandbox/task-cleanup.js +217 -0
  65. package/dist/lib/sandbox/workspace-view.js +13 -14
  66. package/dist/lib/task/activity-intent.js +2 -1
  67. package/dist/lib/task/activity-log.js +30 -24
  68. package/dist/lib/task/artifact-command.js +101 -0
  69. package/dist/lib/task/artifact-lifecycle.js +30 -28
  70. package/dist/lib/task/artifact-name.js +36 -0
  71. package/dist/lib/task/artifact-operations.js +23 -69
  72. package/dist/lib/task/artifact-receipts.js +3 -15
  73. package/dist/lib/task/artifact-repair-intent.js +87 -16
  74. package/dist/lib/task/artifact-schema.js +6 -14
  75. package/dist/lib/task/capabilities.js +12 -19
  76. package/dist/lib/task/codex-orchestration.js +71 -25
  77. package/dist/lib/task/commit-identity.js +28 -0
  78. package/dist/lib/task/commit-operation.js +2 -16
  79. package/dist/lib/task/control-authority.js +371 -5
  80. package/dist/lib/task/control-recovery.js +128 -0
  81. package/dist/lib/task/create.js +62 -9
  82. package/dist/lib/task/decision-details.js +2 -46
  83. package/dist/lib/task/events.js +120 -9
  84. package/dist/lib/task/finalization-state.js +9 -0
  85. package/dist/lib/task/human-override.js +3 -6
  86. package/dist/lib/task/implementation-inputs.js +3 -3
  87. package/dist/lib/task/intermediate-cleanup.js +383 -0
  88. package/dist/lib/task/invalidation.js +18 -65
  89. package/dist/lib/task/ledger-intents.js +7 -22
  90. package/dist/lib/task/ledger.js +1 -3
  91. package/dist/lib/task/lifecycle.js +19 -1
  92. package/dist/lib/task/local-artifact-finalization.js +132 -157
  93. package/dist/lib/task/manual-validation-completion.js +26 -0
  94. package/dist/lib/task/manual-validation-evidence.js +130 -0
  95. package/dist/lib/task/manual-validation-receipt.js +101 -0
  96. package/dist/lib/task/manual-validation-shared.js +40 -0
  97. package/dist/lib/task/manual-validation-transaction.js +243 -0
  98. package/dist/lib/task/markdown.js +47 -0
  99. package/dist/lib/task/orchestration.js +14 -17
  100. package/dist/lib/task/qualification-audit.js +20 -34
  101. package/dist/lib/task/review-artifacts.js +2 -18
  102. package/dist/lib/task/review-command.js +25 -0
  103. package/dist/lib/task/review-finalization.js +125 -193
  104. package/dist/lib/task/review-fingerprint.js +2 -2
  105. package/dist/lib/task/sections.js +34 -56
  106. package/dist/lib/task/verification-engine.js +116 -323
  107. package/dist/lib/task/verification.js +1 -1
  108. package/dist/lib/task/workflow-command.js +17 -0
  109. package/dist/lib/task/workflow-warning-intents.js +0 -16
  110. package/dist/lib/task/workflow-warnings.js +28 -2
  111. package/dist/lib/update.js +2 -0
  112. package/dist/lifecycle-build-manifest.json +3 -3
  113. package/lib/agent-clients/adapters/codex-lifecycle/capability-store.ts +223 -33
  114. package/lib/agent-clients/adapters/codex-lifecycle/sandbox-controller.ts +3 -2
  115. package/lib/fs/durable-write.ts +29 -0
  116. package/lib/fs/file-lock.ts +113 -0
  117. package/lib/host-control/audit.ts +22 -0
  118. package/lib/host-control/client.ts +172 -0
  119. package/lib/host-control/command.ts +60 -0
  120. package/lib/host-control/path.ts +167 -0
  121. package/lib/host-control/secure-fs.ts +135 -0
  122. package/lib/host-control/server.ts +180 -0
  123. package/lib/host-control/service.ts +133 -0
  124. package/lib/init.ts +15 -2
  125. package/lib/internal/cli-route-inventory.ts +3 -1
  126. package/lib/internal/codex-lifecycle.ts +6 -6
  127. package/lib/internal/demo-promotion.ts +354 -0
  128. package/lib/internal/demo-transcript.ts +419 -0
  129. package/lib/internal/host-control.ts +28 -0
  130. package/lib/internal/manual-validation.ts +306 -0
  131. package/lib/internal/platform-pr.ts +15 -4
  132. package/lib/internal/release-workflow.ts +74 -82
  133. package/lib/internal/sandbox-control.ts +162 -1
  134. package/lib/internal/task-artifact.ts +13 -216
  135. package/lib/internal/task-create.ts +5 -5
  136. package/lib/internal/task-event.ts +20 -11
  137. package/lib/internal/task-operation-registry.ts +111 -3
  138. package/lib/internal/task-review.ts +6 -51
  139. package/lib/internal/task-validate.ts +26 -10
  140. package/lib/owned-path.ts +26 -0
  141. package/lib/platform/check-status.ts +9 -0
  142. package/lib/platform/comment-safety.ts +5 -0
  143. package/lib/platform/completion-backfill.ts +1 -1
  144. package/lib/platform/github-data.ts +322 -0
  145. package/lib/platform/github-provider.ts +15 -20
  146. package/lib/platform/issue-comments.ts +3 -4
  147. package/lib/platform/issues.ts +4 -245
  148. package/lib/platform/pr-checks.ts +13 -77
  149. package/lib/platform/pr-summary.ts +61 -5
  150. package/lib/platform/pull-requests.ts +3 -170
  151. package/lib/platform/snapshots.ts +53 -0
  152. package/lib/platform/verification-sync.ts +145 -155
  153. package/lib/release/workflow.ts +3 -3
  154. package/lib/sandbox/commands/create.ts +7 -6
  155. package/lib/sandbox/commands/rm.ts +3 -1909
  156. package/lib/sandbox/control/audit.ts +253 -0
  157. package/lib/sandbox/control/client.ts +109 -66
  158. package/lib/sandbox/control/executor.ts +134 -74
  159. package/lib/sandbox/control/finalization-response.ts +29 -0
  160. package/lib/sandbox/control/identity-sentinel.ts +137 -0
  161. package/lib/sandbox/control/lifecycle.ts +45 -3
  162. package/lib/sandbox/control/native-file-lock.ts +6 -107
  163. package/lib/sandbox/control/protocol.ts +75 -6
  164. package/lib/sandbox/control/server.ts +538 -123
  165. package/lib/sandbox/control/state.ts +78 -0
  166. package/lib/sandbox/control/task-workflow.ts +95 -0
  167. package/lib/sandbox/control/workflow-executor.ts +102 -0
  168. package/lib/sandbox/index.ts +2 -2
  169. package/lib/sandbox/recovery.ts +28 -5
  170. package/lib/sandbox/removal.ts +2144 -0
  171. package/lib/sandbox/task-cleanup.ts +209 -0
  172. package/lib/sandbox/workspace-view.ts +17 -15
  173. package/lib/task/activity-intent.ts +2 -2
  174. package/lib/task/activity-log.ts +27 -19
  175. package/lib/task/artifact-command.ts +104 -0
  176. package/lib/task/artifact-lifecycle.ts +31 -26
  177. package/lib/task/artifact-name.ts +55 -0
  178. package/lib/task/artifact-operations.ts +22 -68
  179. package/lib/task/artifact-receipts.ts +3 -14
  180. package/lib/task/artifact-repair-intent.ts +99 -18
  181. package/lib/task/artifact-schema.ts +5 -34
  182. package/lib/task/capabilities.ts +13 -22
  183. package/lib/task/codex-orchestration.ts +85 -24
  184. package/lib/task/commit-identity.ts +31 -0
  185. package/lib/task/commit-operation.ts +2 -16
  186. package/lib/task/control-authority.ts +502 -5
  187. package/lib/task/control-recovery.ts +174 -0
  188. package/lib/task/create.ts +63 -8
  189. package/lib/task/decision-details.ts +3 -71
  190. package/lib/task/events.ts +131 -10
  191. package/lib/task/finalization-state.ts +11 -0
  192. package/lib/task/human-override.ts +3 -8
  193. package/lib/task/implementation-inputs.ts +3 -3
  194. package/lib/task/intermediate-cleanup.ts +455 -0
  195. package/lib/task/invalidation.ts +18 -62
  196. package/lib/task/ledger-intents.ts +7 -24
  197. package/lib/task/ledger.ts +1 -3
  198. package/lib/task/lifecycle.ts +17 -1
  199. package/lib/task/local-artifact-finalization.ts +148 -182
  200. package/lib/task/manual-validation-completion.ts +40 -0
  201. package/lib/task/manual-validation-evidence.ts +209 -0
  202. package/lib/task/manual-validation-receipt.ts +152 -0
  203. package/lib/task/manual-validation-shared.ts +50 -0
  204. package/lib/task/manual-validation-transaction.ts +304 -0
  205. package/lib/task/markdown.ts +70 -0
  206. package/lib/task/orchestration.ts +19 -19
  207. package/lib/task/qualification-audit.ts +22 -33
  208. package/lib/task/review-artifacts.ts +1 -20
  209. package/lib/task/review-command.ts +23 -0
  210. package/lib/task/review-finalization.ts +131 -259
  211. package/lib/task/review-fingerprint.ts +2 -2
  212. package/lib/task/sections.ts +33 -54
  213. package/lib/task/verification-engine.ts +110 -366
  214. package/lib/task/verification-types.ts +17 -0
  215. package/lib/task/verification.ts +1 -1
  216. package/lib/task/workflow-command.ts +18 -0
  217. package/lib/task/workflow-warning-intents.ts +0 -13
  218. package/lib/task/workflow-warnings.ts +21 -2
  219. package/lib/update.ts +6 -0
  220. package/package.json +6 -6
  221. package/templates/.agents/hooks/lifecycle-delegation.js +6 -6
  222. package/templates/.agents/rules/testing-discipline.en.md +2 -0
  223. package/templates/.agents/rules/testing-discipline.zh-CN.md +2 -0
  224. package/templates/.agents/scripts/package.json +3 -0
  225. package/templates/.agents/skills/complete-manual-validation/SKILL.en.md +32 -20
  226. package/templates/.agents/skills/complete-manual-validation/SKILL.zh-CN.md +32 -20
  227. package/templates/.agents/skills/complete-manual-validation/config/verify.en.json +1 -1
  228. package/templates/.agents/skills/complete-manual-validation/config/verify.zh-CN.json +1 -1
  229. package/templates/.agents/skills/complete-manual-validation/reference/report-template.en.md +2 -0
  230. package/templates/.agents/skills/complete-manual-validation/reference/report-template.zh-CN.md +2 -0
  231. package/templates/.agents/skills/complete-manual-validation/reference/summary-update.en.md +16 -7
  232. package/templates/.agents/skills/complete-manual-validation/reference/summary-update.zh-CN.md +16 -7
  233. package/templates/.agents/skills/complete-task/SKILL.en.md +1 -1
  234. package/templates/.agents/skills/complete-task/SKILL.zh-CN.md +1 -1
  235. package/templates/.agents/skills/release/SKILL.en.md +7 -3
  236. package/templates/.agents/skills/release/SKILL.zh-CN.md +7 -3
  237. package/templates/.agents/skills/run-manual-validation/SKILL.en.md +20 -3
  238. package/templates/.agents/skills/run-manual-validation/SKILL.zh-CN.md +20 -3
  239. package/templates/.agents/skills/run-manual-validation/reference/discovery-and-execution.en.md +4 -1
  240. package/templates/.agents/skills/run-manual-validation/reference/discovery-and-execution.zh-CN.md +4 -1
  241. package/templates/.agents/skills/run-manual-validation/reference/report-template.en.md +2 -0
  242. package/templates/.agents/skills/run-manual-validation/reference/report-template.zh-CN.md +2 -0
  243. package/templates/.agents/skills/run-task/SKILL.en.md +1 -1
  244. package/templates/.agents/skills/run-task/SKILL.zh-CN.md +1 -1
  245. package/templates/.agents/skills/test/SKILL.en.md +17 -6
  246. package/templates/.agents/skills/test/SKILL.zh-CN.md +17 -6
  247. package/templates/.claude/commands/run-manual-validation.en.md +1 -1
  248. package/templates/.claude/commands/run-manual-validation.zh-CN.md +1 -1
  249. package/dist/lib/platform/adapters.js +0 -56
  250. package/lib/platform/adapters.ts +0 -197
@@ -1,3 +1,4 @@
1
+ import type { VerificationShared } from "../task/verification-types.ts";
1
2
  import fs from "node:fs";
2
3
  import path from "node:path";
3
4
  import process from "node:process";
@@ -11,6 +12,12 @@ import { planInLabelUpdate, validateInLabelMapping } from "./in-label-sync.ts";
11
12
  import { readPrDeliveryFact } from "../task/pr-delivery-fact.ts";
12
13
  import { providerError, providerOperationContext, resourceIdentityNumber, unsupportedProviderOperation } from "./provider-bridge.ts";
13
14
  import { taskIssueIdentity } from "./task-identities.ts";
15
+ import {
16
+ CONTROL_MARKER_PATTERN,
17
+ canonicalizeCommentBody,
18
+ renderSafeCodeFence,
19
+ sanitizeMarkdownDocument
20
+ } from "./comment-safety.ts";
14
21
 
15
22
  const CHECK_TYPE = "platform-sync";
16
23
  const VERSION_LINE_REGEX = /^[0-9]+\.[0-9]+\.x$/;
@@ -27,9 +34,6 @@ const OPTION_LOCALIZATION: Record<string, string> = {
27
34
  "低": "Low"
28
35
  };
29
36
 
30
- let activeShared: any = null;
31
- let repoRoot = "";
32
-
33
37
  export function getDefaults(): any {
34
38
  return {
35
39
  statusLabels: {
@@ -50,59 +54,18 @@ export function getDefaults(): any {
50
54
  };
51
55
  }
52
56
 
53
- function getShared(): any {
54
- if (!activeShared) {
55
- throw new Error("platform-sync adapter shared utilities are unavailable");
56
- }
57
-
58
- return activeShared;
59
- }
60
-
61
- function loadTask(...args: any[]): any {
62
- return getShared().loadTask(...args);
63
- }
64
-
65
- function getCheckedRequirements(...args: any[]): any {
66
- return getShared().getCheckedRequirements(...args);
67
- }
68
-
69
- function normalizeContent(...args: any[]): any {
70
- return getShared().normalizeContent(...args);
71
- }
72
-
73
- function isBlank(...args: any[]): any {
74
- return getShared().isBlank(...args);
75
- }
76
-
77
- function escapeRegExp(...args: any[]): any {
78
- return getShared().escapeRegExp(...args);
79
- }
80
-
81
- function passResult(...args: any[]): any {
82
- return getShared().passResult(...args);
83
- }
84
-
85
- function failResult(...args: any[]): any {
86
- return getShared().failResult(...args);
87
- }
88
-
89
- function blockedResult(...args: any[]): any {
90
- return getShared().blockedResult(...args);
57
+ function sanitizeCommentContent(content: string): string | null {
58
+ const result = sanitizeMarkdownDocument(content, { reservedMarkers: [CONTROL_MARKER_PATTERN] });
59
+ return result.ok ? result.value : null;
91
60
  }
92
61
 
93
- function safeStat(...args: any[]): any {
94
- return getShared().safeStat(...args);
95
- }
96
-
97
- export async function check({ taskDir, config, artifactFile }: any, shared: any): Promise<any> {
98
- activeShared = shared;
99
- repoRoot = shared.repoRoot;
100
- const context = await buildSyncContext({ taskDir, config, artifactFile });
62
+ export async function check({ taskDir, config, artifactFile }: any, shared: VerificationShared): Promise<any> {
63
+ const context = await buildSyncContext({ taskDir, config, artifactFile }, shared);
101
64
  if (context.earlyReturn) {
102
65
  return context.earlyReturn;
103
66
  }
104
67
 
105
- const remoteData = await fetchRemoteData(context);
68
+ const remoteData = await fetchRemoteData(context, shared);
106
69
  if (remoteData.earlyReturn) {
107
70
  return remoteData.earlyReturn;
108
71
  }
@@ -127,57 +90,57 @@ export async function check({ taskDir, config, artifactFile }: any, shared: any)
127
90
  ];
128
91
 
129
92
  for (const subCheck of subChecks) {
130
- const result = subCheck(context, remoteData);
93
+ const result = subCheck(context, remoteData, shared);
131
94
  if (result) {
132
95
  return result;
133
96
  }
134
97
  }
135
98
 
136
- return passResult(CHECK_TYPE, `Platform sync checks passed for Issue ${context.issueNumber || "identity"}`);
99
+ return shared.passResult(CHECK_TYPE, `Platform sync checks passed for Issue ${context.issueNumber || "identity"}`);
137
100
  }
138
101
 
139
- async function buildSyncContext({ taskDir, config, artifactFile }: any): Promise<any> {
140
- const task = loadTask(taskDir);
102
+ async function buildSyncContext({ taskDir, config, artifactFile }: any, shared: VerificationShared): Promise<any> {
103
+ const task = shared.loadTask(taskDir);
141
104
  if (!task.ok) {
142
- return { earlyReturn: failResult(CHECK_TYPE, task.message) };
105
+ return { earlyReturn: shared.failResult(CHECK_TYPE, task.message) };
143
106
  }
144
107
 
145
108
  const issueIdentity = taskIssueIdentity(task.metadata);
146
109
  const issueNumber = resourceIdentityNumber(issueIdentity);
147
110
  const fact = readPrDeliveryFact(task.metadata);
148
111
  if (fact.status === "invalid") {
149
- return { earlyReturn: failResult(CHECK_TYPE, fact.error.message, "check_failed") };
112
+ return { earlyReturn: shared.failResult(CHECK_TYPE, fact.error.message, "check_failed") };
150
113
  }
151
114
  const prIdentity = fact.status === "valid" && fact.fact.state === "bound" ? fact.fact.identity.resource : null;
152
115
  const prNumber = resourceIdentityNumber(prIdentity);
153
116
  if (config.when === "issue_number_exists" && !issueIdentity) {
154
- return { earlyReturn: passResult(CHECK_TYPE, "Skipped: task has no issue_number") };
117
+ return { earlyReturn: shared.passResult(CHECK_TYPE, "Skipped: task has no issue_number") };
155
118
  }
156
119
  if (config.when === "pr_fact_bound" && !prIdentity) {
157
- return { earlyReturn: passResult(CHECK_TYPE, "Skipped: task has no verified bound pull request") };
120
+ return { earlyReturn: shared.passResult(CHECK_TYPE, "Skipped: task has no verified bound pull request") };
158
121
  }
159
122
 
160
123
  if (!issueIdentity) {
161
- return { earlyReturn: passResult(CHECK_TYPE, "Skipped: platform-sync not required for this task") };
124
+ return { earlyReturn: shared.passResult(CHECK_TYPE, "Skipped: platform-sync not required for this task") };
162
125
  }
163
126
 
164
- const loaded = await resolvePlatformProviderContext({ cwd: repoRoot });
127
+ const loaded = await resolvePlatformProviderContext({ cwd: shared.repoRoot });
165
128
  const platformContext = loaded.ok ? loaded.value.context : loaded.context;
166
129
  if (platformContext.status === "failed") {
167
- return { earlyReturn: failResult(CHECK_TYPE, platformContext.error?.message || "Platform context failed", "check_failed") };
130
+ return { earlyReturn: shared.failResult(CHECK_TYPE, platformContext.error?.message || "Platform context failed", "check_failed") };
168
131
  }
169
132
  if (platformContext.status === "blocked") {
170
- return { earlyReturn: blockedResult(CHECK_TYPE, platformContext.error?.message || "Platform context blocked", "network_error") };
133
+ return { earlyReturn: shared.blockedResult(CHECK_TYPE, platformContext.error?.message || "Platform context blocked", "network_error") };
171
134
  }
172
135
  if (!platformContext.platform.repository) {
173
136
  if (platformContext.error?.code === "REMOTE_MISSING" || platformContext.error?.code === "REMOTE_INVALID") {
174
- return { earlyReturn: blockedResult(CHECK_TYPE, platformContext.error.message, "network_error") };
137
+ return { earlyReturn: shared.blockedResult(CHECK_TYPE, platformContext.error.message, "network_error") };
175
138
  }
176
- return { earlyReturn: passResult(CHECK_TYPE, `Skipped: ${platformContext.error?.message || "platform unavailable"}`) };
139
+ return { earlyReturn: shared.passResult(CHECK_TYPE, `Skipped: ${platformContext.error?.message || "platform unavailable"}`) };
177
140
  }
178
141
  const expectedValues = resolveExpectedValues(config);
179
142
  if (!expectedValues.ok) {
180
- return { earlyReturn: failResult(CHECK_TYPE, expectedValues.message, "check_failed") };
143
+ return { earlyReturn: shared.failResult(CHECK_TYPE, expectedValues.message, "check_failed") };
181
144
  }
182
145
 
183
146
  const marker = expectedValues.commentMarker
@@ -263,7 +226,7 @@ function resolveDefaultValue({ collection, key, value, configKey }: any): any {
263
226
  return { ok: true, value: resolvedValue };
264
227
  }
265
228
 
266
- async function fetchRemoteData(context: any): Promise<any> {
229
+ async function fetchRemoteData(context: any, shared: VerificationShared): Promise<any> {
267
230
  const provider = context.provider;
268
231
  const operationContext = providerOperationContext(context.loadedContext, context.taskDir);
269
232
  const facts = provider?.verification?.fetchRemoteFacts
@@ -279,8 +242,8 @@ async function fetchRemoteData(context: any): Promise<any> {
279
242
  if (!facts.ok) {
280
243
  return {
281
244
  earlyReturn: facts.error.retryable
282
- ? blockedResult(CHECK_TYPE, providerError(facts.error, "PLATFORM_PROVIDER_OPERATION_FAILED").message, "network_error")
283
- : failResult(CHECK_TYPE, providerError(facts.error, "PLATFORM_PROVIDER_OPERATION_FAILED").message, "check_failed")
245
+ ? shared.blockedResult(CHECK_TYPE, providerError(facts.error, "PLATFORM_PROVIDER_OPERATION_FAILED").message, "network_error")
246
+ : shared.failResult(CHECK_TYPE, providerError(facts.error, "PLATFORM_PROVIDER_OPERATION_FAILED").message, "check_failed")
284
247
  };
285
248
  }
286
249
  const issueSnapshot = facts.value.issue;
@@ -305,8 +268,8 @@ async function fetchRemoteData(context: any): Promise<any> {
305
268
  const listed = await provider.comments.list({ context: operationContext, parent: context.prIdentity });
306
269
  if (!listed.ok) return {
307
270
  earlyReturn: listed.error.retryable
308
- ? blockedResult(CHECK_TYPE, listed.error.message, "network_error")
309
- : failResult(CHECK_TYPE, listed.error.message, "check_failed")
271
+ ? shared.blockedResult(CHECK_TYPE, listed.error.message, "network_error")
272
+ : shared.failResult(CHECK_TYPE, listed.error.message, "check_failed")
310
273
  };
311
274
  prComments = listed.value.map((comment: { id: string; body: string }) => ({ id: comment.id, body: comment.body }));
312
275
  }
@@ -314,8 +277,8 @@ async function fetchRemoteData(context: any): Promise<any> {
314
277
  let inLabelMapping: Record<string, string[]> = {};
315
278
  let repositoryLabels: string[] = [];
316
279
  if (context.config.verify_in_labels_computed && context.hasTriage) {
317
- const mapping = loadInLabelMapping();
318
- if (!mapping.ok) return { earlyReturn: failResult(CHECK_TYPE, mapping.message, "check_failed") };
280
+ const mapping = loadInLabelMapping(shared);
281
+ if (!mapping.ok) return { earlyReturn: shared.failResult(CHECK_TYPE, mapping.message, "check_failed") };
319
282
  inLabelMapping = mapping.value;
320
283
  if (Object.keys(inLabelMapping).length > 0) {
321
284
  const labels = provider?.issues?.listLabels
@@ -325,8 +288,8 @@ async function fetchRemoteData(context: any): Promise<any> {
325
288
  const error = providerError(labels.error, "PLATFORM_PROVIDER_OPERATION_FAILED");
326
289
  return {
327
290
  earlyReturn: error.retryable
328
- ? blockedResult(CHECK_TYPE, `${error.code}: ${error.message}`, "network_error")
329
- : failResult(CHECK_TYPE, `${error.code}: ${error.message}`, "check_failed")
291
+ ? shared.blockedResult(CHECK_TYPE, `${error.code}: ${error.message}`, "network_error")
292
+ : shared.failResult(CHECK_TYPE, `${error.code}: ${error.message}`, "check_failed")
330
293
  };
331
294
  }
332
295
  repositoryLabels = labels.value;
@@ -367,7 +330,7 @@ function shouldFetchComments(config: any): any {
367
330
  );
368
331
  }
369
332
 
370
- function checkStatusLabel(context: any, remoteData: any): any {
333
+ function checkStatusLabel(context: any, remoteData: any, shared: VerificationShared): any {
371
334
  if (!context.expectedStatusLabel || !context.hasTriage) {
372
335
  return null;
373
336
  }
@@ -381,13 +344,13 @@ function checkStatusLabel(context: any, remoteData: any): any {
381
344
  return null;
382
345
  }
383
346
 
384
- return failResult(CHECK_TYPE,
347
+ return shared.failResult(CHECK_TYPE,
385
348
  `Expected label '${context.expectedStatusLabel}' not found on Issue #${context.issueNumber}`,
386
349
  "check_failed"
387
350
  );
388
351
  }
389
352
 
390
- function checkClosedIssueStatusLabels(context: any, remoteData: any): any {
353
+ function checkClosedIssueStatusLabels(context: any, remoteData: any, shared: VerificationShared): any {
391
354
  if (!context.config.verify_closed_issue_has_no_status_labels) {
392
355
  return null;
393
356
  }
@@ -402,13 +365,13 @@ function checkClosedIssueStatusLabels(context: any, remoteData: any): any {
402
365
  return null;
403
366
  }
404
367
 
405
- return failResult(CHECK_TYPE,
368
+ return shared.failResult(CHECK_TYPE,
406
369
  `Closed Issue #${context.issueNumber} retains status labels: ${statusLabels.join(", ")}`,
407
370
  "check_failed"
408
371
  );
409
372
  }
410
373
 
411
- function checkCommentMarker(context: any, remoteData: any): any {
374
+ function checkCommentMarker(context: any, remoteData: any, shared: VerificationShared): any {
412
375
  if (!context.marker) {
413
376
  return null;
414
377
  }
@@ -418,13 +381,13 @@ function checkCommentMarker(context: any, remoteData: any): any {
418
381
  return null;
419
382
  }
420
383
 
421
- return failResult(CHECK_TYPE,
384
+ return shared.failResult(CHECK_TYPE,
422
385
  `Expected comment marker '${context.marker}' not found on Issue #${context.issueNumber}`,
423
386
  "check_failed"
424
387
  );
425
388
  }
426
389
 
427
- function checkPrCommentMarker(context: any, remoteData: any): any {
390
+ function checkPrCommentMarker(context: any, remoteData: any, shared: VerificationShared): any {
428
391
  if (!context.prMarker) {
429
392
  return null;
430
393
  }
@@ -434,19 +397,19 @@ function checkPrCommentMarker(context: any, remoteData: any): any {
434
397
  return null;
435
398
  }
436
399
 
437
- return failResult(CHECK_TYPE,
400
+ return shared.failResult(CHECK_TYPE,
438
401
  `Expected PR comment marker '${context.prMarker}' not found on PR #${context.prNumber}`,
439
402
  "check_failed"
440
403
  );
441
404
  }
442
405
 
443
- function checkPrCommentLastCommit(context: any, remoteData: any): any {
406
+ function checkPrCommentLastCommit(context: any, remoteData: any, shared: VerificationShared): any {
444
407
  if (!context.config.verify_pr_comment_last_commit_matches_head) {
445
408
  return null;
446
409
  }
447
410
 
448
411
  if (!context.prMarker) {
449
- return failResult(CHECK_TYPE,
412
+ return shared.failResult(CHECK_TYPE,
450
413
  "verify_pr_comment_last_commit_matches_head requires expected_pr_comment_marker",
451
414
  "check_failed"
452
415
  );
@@ -454,7 +417,7 @@ function checkPrCommentLastCommit(context: any, remoteData: any): any {
454
417
 
455
418
  const comment = findCommentByMarker(remoteData.prComments, context.prMarker);
456
419
  if (!comment) {
457
- return failResult(CHECK_TYPE,
420
+ return shared.failResult(CHECK_TYPE,
458
421
  `Expected PR comment marker '${context.prMarker}' not found on PR #${context.prNumber}`,
459
422
  "check_failed"
460
423
  );
@@ -462,33 +425,33 @@ function checkPrCommentLastCommit(context: any, remoteData: any): any {
462
425
 
463
426
  const match = String(comment.body || "").match(/<!--\s*last-commit:\s*([0-9a-f]{7,40})\s*-->/i);
464
427
  if (!match) {
465
- return failResult(CHECK_TYPE,
428
+ return shared.failResult(CHECK_TYPE,
466
429
  `PR #${context.prNumber} summary comment is missing '<!-- last-commit: <sha> -->' metadata`,
467
430
  "check_failed"
468
431
  );
469
432
  }
470
433
 
471
434
  const expectedHead = String(remoteData.prHeadSha || "").trim();
472
- if (!expectedHead) return blockedResult(CHECK_TYPE, "Unable to resolve the PR head SHA", "network_error");
435
+ if (!expectedHead) return shared.blockedResult(CHECK_TYPE, "Unable to resolve the PR head SHA", "network_error");
473
436
  const actualHead = match[1]!.trim();
474
437
  if (expectedHead === actualHead) {
475
438
  return null;
476
439
  }
477
440
 
478
- return failResult(CHECK_TYPE,
441
+ return shared.failResult(CHECK_TYPE,
479
442
  `PR #${context.prNumber} summary comment last-commit metadata mismatch: expected ${expectedHead}, got ${actualHead}`,
480
443
  "check_failed"
481
444
  );
482
445
  }
483
446
 
484
- function checkPrCommentRequiredPatterns(context: any, remoteData: any): any {
447
+ function checkPrCommentRequiredPatterns(context: any, remoteData: any, shared: VerificationShared): any {
485
448
  const patterns = context.config.expected_pr_comment_required_patterns || [];
486
449
  if (!Array.isArray(patterns) || patterns.length === 0) {
487
450
  return null;
488
451
  }
489
452
 
490
453
  if (!context.prMarker) {
491
- return failResult(CHECK_TYPE,
454
+ return shared.failResult(CHECK_TYPE,
492
455
  "expected_pr_comment_required_patterns requires expected_pr_comment_marker",
493
456
  "check_failed"
494
457
  );
@@ -496,7 +459,7 @@ function checkPrCommentRequiredPatterns(context: any, remoteData: any): any {
496
459
 
497
460
  const comment = findCommentByMarker(remoteData.prComments, context.prMarker);
498
461
  if (!comment) {
499
- return failResult(CHECK_TYPE,
462
+ return shared.failResult(CHECK_TYPE,
500
463
  `Expected PR comment marker '${context.prMarker}' not found on PR #${context.prNumber}`,
501
464
  "check_failed"
502
465
  );
@@ -506,7 +469,7 @@ function checkPrCommentRequiredPatterns(context: any, remoteData: any): any {
506
469
  for (const pattern of patterns) {
507
470
  const regex = new RegExp(pattern, "m");
508
471
  if (!regex.test(body)) {
509
- return failResult(CHECK_TYPE,
472
+ return shared.failResult(CHECK_TYPE,
510
473
  `PR #${context.prNumber} summary comment is missing required pattern: ${pattern}`,
511
474
  "check_failed"
512
475
  );
@@ -516,31 +479,54 @@ function checkPrCommentRequiredPatterns(context: any, remoteData: any): any {
516
479
  return null;
517
480
  }
518
481
 
519
- function checkCommentContent(context: any, remoteData: any): any {
482
+ function checkCommentContent(context: any, remoteData: any, shared: VerificationShared): any {
520
483
  if (!context.config.verify_comment_content) {
521
484
  return null;
522
485
  }
523
486
 
524
487
  if (!context.marker) {
525
- return failResult(CHECK_TYPE, "verify_comment_content requires expected_comment_marker", "check_failed");
488
+ return shared.failResult(CHECK_TYPE, "verify_comment_content requires expected_comment_marker", "check_failed");
526
489
  }
527
490
 
528
- if (!context.artifactPath || !safeStat(context.artifactPath)) {
529
- return failResult(CHECK_TYPE,
491
+ if (!context.artifactPath || !shared.safeStat(context.artifactPath)) {
492
+ return shared.failResult(CHECK_TYPE,
530
493
  `Artifact not found for comment verification: ${context.artifactFile || "(missing artifactFile)"}`,
531
494
  "check_failed"
532
495
  );
533
496
  }
534
497
 
535
498
  const comment = findCommentByMarker(remoteData.comments, context.marker);
536
- const localContent = normalizeContent(fs.readFileSync(context.artifactPath, "utf8"));
537
- const commentContent = normalizeContent(extractCommentBody(comment?.body || ""));
499
+ let localContent: string;
500
+ let commentContent: string;
501
+ try {
502
+ const localCanonical = canonicalizeCommentBody(fs.readFileSync(context.artifactPath, "utf8"));
503
+ const commentCanonical = canonicalizeCommentBody(extractCommentBody(comment?.body || ""));
504
+ if (!localCanonical.ok) {
505
+ return shared.failResult(CHECK_TYPE,
506
+ `Comment content cannot be canonicalized for '${path.basename(context.artifactPath, path.extname(context.artifactPath))}': ${localCanonical.error.message}`,
507
+ "check_failed"
508
+ );
509
+ }
510
+ if (!commentCanonical.ok) {
511
+ return shared.failResult(CHECK_TYPE,
512
+ `Comment content cannot be canonicalized for '${path.basename(context.artifactPath, path.extname(context.artifactPath))}': ${commentCanonical.error.message}`,
513
+ "check_failed"
514
+ );
515
+ }
516
+ localContent = shared.normalizeContent(localCanonical.value);
517
+ commentContent = shared.normalizeContent(commentCanonical.value);
518
+ } catch (error) {
519
+ return shared.failResult(CHECK_TYPE,
520
+ `Comment content cannot be read for '${path.basename(context.artifactPath, path.extname(context.artifactPath))}': ${error instanceof Error ? error.message : String(error)}`,
521
+ "check_failed"
522
+ );
523
+ }
538
524
 
539
525
  if (localContent === commentContent) {
540
526
  return null;
541
527
  }
542
528
 
543
- return failResult(CHECK_TYPE,
529
+ return shared.failResult(CHECK_TYPE,
544
530
  buildCommentContentMismatchMessage(
545
531
  path.basename(context.artifactPath, path.extname(context.artifactPath)),
546
532
  context.issueNumber,
@@ -551,7 +537,7 @@ function checkCommentContent(context: any, remoteData: any): any {
551
537
  );
552
538
  }
553
539
 
554
- function checkTaskCommentContent(context: any, remoteData: any): any {
540
+ function checkTaskCommentContent(context: any, remoteData: any, shared: VerificationShared): any {
555
541
  if (!context.config.verify_task_comment_content) {
556
542
  return null;
557
543
  }
@@ -559,26 +545,33 @@ function checkTaskCommentContent(context: any, remoteData: any): any {
559
545
  const taskMarker = `<!-- sync-issue:${context.task.metadata.id}:task -->`;
560
546
  const comment = findCommentByMarker(remoteData.comments, taskMarker);
561
547
  if (!comment) {
562
- return failResult(CHECK_TYPE,
548
+ return shared.failResult(CHECK_TYPE,
563
549
  `Expected comment marker '${taskMarker}' not found on Issue #${context.issueNumber}`,
564
550
  "check_failed"
565
551
  );
566
552
  }
567
553
 
568
- const expectedBody = normalizeContent(buildExpectedTaskBody(context.task.content));
569
- const commentBody = normalizeContent(extractCommentBody(comment.body || ""));
554
+ const expectedTaskBody = buildExpectedTaskBody(context.task.content, shared);
555
+ if (expectedTaskBody === null) {
556
+ return shared.failResult(CHECK_TYPE,
557
+ "Task content cannot be rendered safely for comment verification",
558
+ "check_failed"
559
+ );
560
+ }
561
+ const expectedBody = shared.normalizeContent(expectedTaskBody);
562
+ const commentBody = shared.normalizeContent(extractCommentBody(comment.body || ""));
570
563
 
571
564
  if (expectedBody === commentBody) {
572
565
  return null;
573
566
  }
574
567
 
575
- return failResult(CHECK_TYPE,
568
+ return shared.failResult(CHECK_TYPE,
576
569
  buildCommentContentMismatchMessage("task", context.issueNumber, expectedBody, commentBody),
577
570
  "check_failed"
578
571
  );
579
572
  }
580
573
 
581
- function checkPrTypeLabel(context: any, remoteData: any): any {
574
+ function checkPrTypeLabel(context: any, remoteData: any, shared: VerificationShared): any {
582
575
  if (!context.config.verify_pr_type_label || !context.hasTriage || !context.prNumber || !remoteData.prLabels) {
583
576
  return null;
584
577
  }
@@ -592,13 +585,13 @@ function checkPrTypeLabel(context: any, remoteData: any): any {
592
585
  return null;
593
586
  }
594
587
 
595
- return failResult(CHECK_TYPE,
588
+ return shared.failResult(CHECK_TYPE,
596
589
  `Expected type label '${expectedLabel}' not found on PR #${context.prNumber}`,
597
590
  "check_failed"
598
591
  );
599
592
  }
600
593
 
601
- function checkInLabelsMatchPr(context: any, remoteData: any): any {
594
+ function checkInLabelsMatchPr(context: any, remoteData: any, shared: VerificationShared): any {
602
595
  if (!context.config.verify_in_labels_match_pr || !context.hasTriage || !context.prNumber || !remoteData.prLabels) {
603
596
  return null;
604
597
  }
@@ -614,22 +607,22 @@ function checkInLabelsMatchPr(context: any, remoteData: any): any {
614
607
  return null;
615
608
  }
616
609
 
617
- return failResult(CHECK_TYPE,
610
+ return shared.failResult(CHECK_TYPE,
618
611
  `in: labels mismatch — PR #${context.prNumber} has [${formatLabelList(prInLabels)}], Issue #${context.issueNumber} has [${formatLabelList(issueInLabels)}]`,
619
612
  "check_failed"
620
613
  );
621
614
  }
622
615
 
623
- function checkInLabelsComputed(context: any, remoteData: any): any {
616
+ function checkInLabelsComputed(context: any, remoteData: any, shared: VerificationShared): any {
624
617
  if (!context.config.verify_in_labels_computed || !context.hasTriage) {
625
618
  return null;
626
619
  }
627
620
 
628
- const expectedInLabels = computeExpectedInLabels(context.taskDir, remoteData.repositoryLabels, remoteData.inLabelMapping);
621
+ const expectedInLabels = computeExpectedInLabels(context.taskDir, remoteData.repositoryLabels, remoteData.inLabelMapping, shared);
629
622
  if (!expectedInLabels.ok) {
630
623
  return expectedInLabels.type === "check_failed"
631
- ? failResult(CHECK_TYPE, expectedInLabels.message, expectedInLabels.type)
632
- : blockedResult(CHECK_TYPE, expectedInLabels.message, expectedInLabels.type);
624
+ ? shared.failResult(CHECK_TYPE, expectedInLabels.message, expectedInLabels.type)
625
+ : shared.blockedResult(CHECK_TYPE, expectedInLabels.message, expectedInLabels.type);
633
626
  }
634
627
 
635
628
  if (expectedInLabels.mode === "skipped") {
@@ -644,19 +637,19 @@ function checkInLabelsComputed(context: any, remoteData: any): any {
644
637
  return null;
645
638
  }
646
639
 
647
- return failResult(
640
+ return shared.failResult(
648
641
  CHECK_TYPE,
649
642
  `Issue #${context.issueNumber} in: labels do not match committed changes: expected [${formatLabelList(expectedInLabels.labels)}], got [${formatLabelList(actualInLabels)}]`,
650
643
  "check_failed"
651
644
  );
652
645
  }
653
646
 
654
- function checkSyncedRequirements(context: any, remoteData: any): any {
647
+ function checkSyncedRequirements(context: any, remoteData: any, shared: VerificationShared): any {
655
648
  if (!context.config.sync_checked_requirements || !context.hasTriage) {
656
649
  return null;
657
650
  }
658
651
 
659
- const checkedRequirements = getCheckedRequirements(context.task.content);
652
+ const checkedRequirements = shared.getCheckedRequirements(context.task.content);
660
653
  if (checkedRequirements.length === 0) {
661
654
  return null;
662
655
  }
@@ -664,13 +657,13 @@ function checkSyncedRequirements(context: any, remoteData: any): any {
664
657
  const issueBody = remoteData.issue.body || "";
665
658
  const resolution = resolveRequirementSection(
666
659
  issueBody,
667
- requirementSectionAnchors(repoRoot, context.task.metadata.type || "task")
660
+ requirementSectionAnchors(shared.repoRoot, context.task.metadata.type || "task")
668
661
  );
669
662
  if (resolution.status === "missing") {
670
663
  return null;
671
664
  }
672
665
  if (resolution.status === "ambiguous") {
673
- return failResult(CHECK_TYPE,
666
+ return shared.failResult(CHECK_TYPE,
674
667
  `Issue #${context.issueNumber} requirements section is ambiguous`,
675
668
  "check_failed"
676
669
  );
@@ -683,13 +676,13 @@ function checkSyncedRequirements(context: any, remoteData: any): any {
683
676
  return null;
684
677
  }
685
678
 
686
- return failResult(CHECK_TYPE,
679
+ return shared.failResult(CHECK_TYPE,
687
680
  `Issue body is missing checked requirements: ${missingRequirements.join(", ")}`,
688
681
  "check_failed"
689
682
  );
690
683
  }
691
684
 
692
- function checkIssueType(context: any, remoteData: any): any {
685
+ function checkIssueType(context: any, remoteData: any, shared: VerificationShared): any {
693
686
  if (!context.config.verify_issue_type || !context.hasPush) {
694
687
  return null;
695
688
  }
@@ -703,7 +696,7 @@ function checkIssueType(context: any, remoteData: any): any {
703
696
  return null;
704
697
  }
705
698
 
706
- return failResult(CHECK_TYPE,
699
+ return shared.failResult(CHECK_TYPE,
707
700
  `Issue #${context.issueNumber} has no Issue Type set`,
708
701
  "check_failed"
709
702
  );
@@ -711,7 +704,7 @@ function checkIssueType(context: any, remoteData: any): any {
711
704
 
712
705
  const expectedType = mapTaskTypeToIssueType(context.task.metadata.type);
713
706
  if (expectedType && remoteData.issueType !== expectedType) {
714
- return failResult(CHECK_TYPE,
707
+ return shared.failResult(CHECK_TYPE,
715
708
  `Issue #${context.issueNumber} has type '${remoteData.issueType}', expected '${expectedType}' (from task type '${context.task.metadata.type}')`,
716
709
  "check_failed"
717
710
  );
@@ -720,7 +713,7 @@ function checkIssueType(context: any, remoteData: any): any {
720
713
  return null;
721
714
  }
722
715
 
723
- function checkIssueFields(context: any, remoteData: any): any {
716
+ function checkIssueFields(context: any, remoteData: any, shared: VerificationShared): any {
724
717
  if (!context.config.verify_issue_fields || !context.hasPush) {
725
718
  return null;
726
719
  }
@@ -731,7 +724,7 @@ function checkIssueFields(context: any, remoteData: any): any {
731
724
 
732
725
  for (const [metadataKey, fieldName] of Object.entries(FRONTMATTER_FIELD_MAP)) {
733
726
  const expectedRaw = context.task.metadata[metadataKey];
734
- if (isBlank(expectedRaw) || !remoteData.issueFields.pinnedNames.has(fieldName)) {
727
+ if (shared.isBlank(expectedRaw) || !remoteData.issueFields.pinnedNames.has(fieldName)) {
735
728
  continue;
736
729
  }
737
730
 
@@ -742,14 +735,14 @@ function checkIssueFields(context: any, remoteData: any): any {
742
735
  }
743
736
 
744
737
  if (!actual) {
745
- return failResult(CHECK_TYPE,
738
+ return shared.failResult(CHECK_TYPE,
746
739
  `Issue #${context.issueNumber} field '${fieldName}' is missing, expected '${expected.value}'`,
747
740
  "check_failed"
748
741
  );
749
742
  }
750
743
 
751
744
  if (actual.kind !== expected.kind || actual.value !== expected.value) {
752
- return failResult(CHECK_TYPE,
745
+ return shared.failResult(CHECK_TYPE,
753
746
  `Issue #${context.issueNumber} field '${fieldName}' is '${actual.value}', expected '${expected.value}'`,
754
747
  "check_failed"
755
748
  );
@@ -759,13 +752,13 @@ function checkIssueFields(context: any, remoteData: any): any {
759
752
  return null;
760
753
  }
761
754
 
762
- function checkPrAssignee(context: any, remoteData: any): any {
755
+ function checkPrAssignee(context: any, remoteData: any, shared: VerificationShared): any {
763
756
  if (!context.config.verify_pr_assignee || !context.hasPush || !context.prNumber) {
764
757
  return null;
765
758
  }
766
759
 
767
760
  if (!remoteData.prAssignees || remoteData.prAssignees.length === 0) {
768
- return failResult(CHECK_TYPE,
761
+ return shared.failResult(CHECK_TYPE,
769
762
  `PR #${context.prNumber} has no assignee`,
770
763
  "check_failed"
771
764
  );
@@ -774,20 +767,20 @@ function checkPrAssignee(context: any, remoteData: any): any {
774
767
  return null;
775
768
  }
776
769
 
777
- function checkMilestone(context: any, remoteData: any): any {
770
+ function checkMilestone(context: any, remoteData: any, shared: VerificationShared): any {
778
771
  if (!context.config.verify_milestone || !context.hasTriage) {
779
772
  return null;
780
773
  }
781
774
 
782
775
  if (!remoteData.issue?.milestone?.title) {
783
- return failResult(CHECK_TYPE,
776
+ return shared.failResult(CHECK_TYPE,
784
777
  `Issue #${context.issueNumber} has no milestone set`,
785
778
  "check_failed"
786
779
  );
787
780
  }
788
781
 
789
782
  if (context.prNumber && remoteData.prMilestone !== undefined && !remoteData.prMilestone?.title) {
790
- return failResult(CHECK_TYPE,
783
+ return shared.failResult(CHECK_TYPE,
791
784
  `PR #${context.prNumber} has no milestone set`,
792
785
  "check_failed"
793
786
  );
@@ -796,13 +789,13 @@ function checkMilestone(context: any, remoteData: any): any {
796
789
  if (context.config.verify_milestone_specific) {
797
790
  const issueTitle = remoteData.issue.milestone.title;
798
791
  if (VERSION_LINE_REGEX.test(issueTitle)) {
799
- return failResult(CHECK_TYPE,
792
+ return shared.failResult(CHECK_TYPE,
800
793
  `Issue #${context.issueNumber} milestone '${issueTitle}' is a release line; narrow to a specific version (e.g. ${issueTitle.replace(/\.x$/, ".N")}) before continuing`,
801
794
  "check_failed"
802
795
  );
803
796
  }
804
797
  if (context.prNumber && remoteData.prMilestone?.title && VERSION_LINE_REGEX.test(remoteData.prMilestone.title)) {
805
- return failResult(CHECK_TYPE,
798
+ return shared.failResult(CHECK_TYPE,
806
799
  `PR #${context.prNumber} milestone '${remoteData.prMilestone.title}' is a release line; narrow to a specific version before continuing`,
807
800
  "check_failed"
808
801
  );
@@ -863,19 +856,20 @@ function extractCommentBody(commentBody: any): any {
863
856
  return lines.slice(start, end).join("\n");
864
857
  }
865
858
 
866
- function buildExpectedTaskBody(taskContent: any): any {
859
+ function buildExpectedTaskBody(taskContent: any, shared: VerificationShared): any {
867
860
  const frontmatterMatch = taskContent.match(/^---\r?\n([\s\S]*?)\r?\n---(?:\r?\n|$)/);
868
861
  if (!frontmatterMatch) {
869
- return taskContent.trim();
862
+ return sanitizeCommentContent(taskContent.trim());
870
863
  }
871
864
 
872
- const body = taskContent.slice(frontmatterMatch[0].length).trim();
865
+ const body = sanitizeCommentContent(taskContent.slice(frontmatterMatch[0].length).trim());
866
+ if (body === null) {
867
+ return null;
868
+ }
873
869
  return [
874
- buildTaskFrontmatterSummary(),
870
+ buildTaskFrontmatterSummary(shared),
875
871
  "",
876
- "```yaml",
877
- frontmatterMatch[0].trim(),
878
- "```",
872
+ renderSafeCodeFence(frontmatterMatch[0].trim(), "yaml"),
879
873
  "",
880
874
  "</details>",
881
875
  "",
@@ -883,8 +877,8 @@ function buildExpectedTaskBody(taskContent: any): any {
883
877
  ].join("\n").trim();
884
878
  }
885
879
 
886
- function buildTaskFrontmatterSummary(): any {
887
- const language = loadProjectLanguage();
880
+ function buildTaskFrontmatterSummary(shared: VerificationShared): any {
881
+ const language = loadProjectLanguage(shared);
888
882
  if (language === "en" || language === "en-US") {
889
883
  return "<details><summary>Metadata (frontmatter)</summary>";
890
884
  }
@@ -892,13 +886,13 @@ function buildTaskFrontmatterSummary(): any {
892
886
  return "<details><summary>元数据 (frontmatter)</summary>";
893
887
  }
894
888
 
895
- function loadProjectLanguage(): any {
889
+ function loadProjectLanguage(shared: VerificationShared): any {
896
890
  const override = process.env.VALIDATE_ARTIFACT_LANGUAGE;
897
- if (!isBlank(override)) {
891
+ if (!shared.isBlank(override)) {
898
892
  return String(override).trim();
899
893
  }
900
894
 
901
- const configPath = path.join(repoRoot, ".agents", ".airc.json");
895
+ const configPath = path.join(shared.repoRoot, ".agents", ".airc.json");
902
896
  if (!fs.existsSync(configPath)) {
903
897
  return "";
904
898
  }
@@ -1035,8 +1029,8 @@ function formatLabelList(labels: any): any {
1035
1029
  return labels.length > 0 ? labels.join(", ") : "none";
1036
1030
  }
1037
1031
 
1038
- function computeExpectedInLabels(taskDir: any, repositoryLabels: string[] = [], mappingOverride?: Record<string, string[]>): any {
1039
- const task = loadTask(taskDir);
1032
+ function computeExpectedInLabels(taskDir: any, repositoryLabels: string[], mappingOverride: Record<string, string[]> | undefined, shared: VerificationShared): any {
1033
+ const task = shared.loadTask(taskDir);
1040
1034
  if (!task.ok) {
1041
1035
  return task;
1042
1036
  }
@@ -1071,8 +1065,8 @@ function computeExpectedInLabels(taskDir: any, repositoryLabels: string[] = [],
1071
1065
  return { ok: true, labels: planned.target, mode: "mapped" };
1072
1066
  }
1073
1067
 
1074
- function loadInLabelMapping(): any {
1075
- const configPath = path.join(repoRoot, ".agents", ".airc.json");
1068
+ function loadInLabelMapping(shared: VerificationShared): any {
1069
+ const configPath = path.join(shared.repoRoot, ".agents", ".airc.json");
1076
1070
  if (!fs.existsSync(configPath)) {
1077
1071
  return { ok: true, value: {} };
1078
1072
  }
@@ -1105,13 +1099,13 @@ function gitText(args: any, cwd: any): any {
1105
1099
  }
1106
1100
 
1107
1101
  function resolvePrHeadSha(context: any): any {
1108
- const fallback = () => withRetry(() => gitText(["rev-parse", "HEAD"], context.taskDir));
1102
+ const fallback = () => gitText(["rev-parse", "HEAD"], context.taskDir);
1109
1103
  const branch = String(context.task?.metadata?.branch || "").trim();
1110
1104
  if (!branch) {
1111
1105
  return fallback();
1112
1106
  }
1113
1107
 
1114
- const worktreeList = withRetry(() => gitText(["worktree", "list", "--porcelain"], context.taskDir));
1108
+ const worktreeList = gitText(["worktree", "list", "--porcelain"], context.taskDir);
1115
1109
  if (!worktreeList.ok) {
1116
1110
  return fallback();
1117
1111
  }
@@ -1121,7 +1115,7 @@ function resolvePrHeadSha(context: any): any {
1121
1115
  return fallback();
1122
1116
  }
1123
1117
 
1124
- const headInWorktree = withRetry(() => gitText(["rev-parse", "HEAD"], matchedWorktree));
1118
+ const headInWorktree = gitText(["rev-parse", "HEAD"], matchedWorktree);
1125
1119
  if (!headInWorktree.ok) {
1126
1120
  return fallback();
1127
1121
  }
@@ -1149,10 +1143,6 @@ function findWorktreeForBranch(porcelainOutput: any, branch: any): any {
1149
1143
  return null;
1150
1144
  }
1151
1145
 
1152
- function withRetry(operation: any): any {
1153
- return operation();
1154
- }
1155
-
1156
1146
  function interpolate(template: any, taskDir: any, artifactFile: any): any {
1157
1147
  const artifactStem = artifactFile ? path.basename(artifactFile, path.extname(artifactFile)) : "";
1158
1148
  return template