@fitlab-ai/agent-infra 0.9.13 → 0.9.14

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 (280) hide show
  1. package/bin/cli.ts +26 -9
  2. package/dist/bin/cli.js +173 -157
  3. package/dist/bin/internal-cli.js +42 -21
  4. package/dist/lib/agent-client.js +5 -0
  5. package/dist/lib/internal/agent-client.js +5 -2
  6. package/dist/lib/internal/cli-route-inventory.js +152 -0
  7. package/dist/lib/internal/codex-lifecycle.js +9 -6
  8. package/dist/lib/internal/codex-sandbox-controller.js +8 -2
  9. package/dist/lib/internal/git-workflow.js +10 -6
  10. package/dist/lib/internal/platform-checks.js +16 -4
  11. package/dist/lib/internal/platform-comment.js +16 -4
  12. package/dist/lib/internal/platform-context.js +4 -1
  13. package/dist/lib/internal/platform-issue.js +12 -4
  14. package/dist/lib/internal/platform-metadata.js +88 -0
  15. package/dist/lib/internal/platform-pr-review.js +16 -3
  16. package/dist/lib/internal/platform-pr.js +24 -10
  17. package/dist/lib/internal/platform-release-notes.js +6 -3
  18. package/dist/lib/internal/platform-security.js +93 -0
  19. package/dist/lib/internal/pr-review-grade.js +14 -3
  20. package/dist/lib/internal/release-workflow.js +19 -5
  21. package/dist/lib/internal/sandbox-control.js +22 -5
  22. package/dist/lib/internal/task-activity.js +13 -4
  23. package/dist/lib/internal/task-artifact.js +186 -27
  24. package/dist/lib/internal/task-context.js +4 -1
  25. package/dist/lib/internal/task-create.js +48 -15
  26. package/dist/lib/internal/task-delivery.js +4 -1
  27. package/dist/lib/internal/task-event.js +4 -1
  28. package/dist/lib/internal/task-finalization.js +4 -1
  29. package/dist/lib/internal/task-invalidation.js +4 -1
  30. package/dist/lib/internal/task-ledger.js +12 -1
  31. package/dist/lib/internal/task-lifecycle.js +4 -1
  32. package/dist/lib/internal/task-operation-registry.js +421 -0
  33. package/dist/lib/internal/task-orchestration.js +12 -0
  34. package/dist/lib/internal/task-override.js +10 -3
  35. package/dist/lib/internal/task-qualification.js +90 -0
  36. package/dist/lib/internal/task-review.js +4 -1
  37. package/dist/lib/internal/task-short-id.js +11 -1
  38. package/dist/lib/internal/task-snapshot.js +4 -1
  39. package/dist/lib/internal/task-validate.js +7 -2
  40. package/dist/lib/internal/task-verify.js +5 -1
  41. package/dist/lib/internal/task-warning.js +8 -1
  42. package/dist/lib/platform/comment-safety.js +226 -0
  43. package/dist/lib/platform/github-provider.js +233 -6
  44. package/dist/lib/platform/issue-comments.js +146 -34
  45. package/dist/lib/platform/pr-change-report.js +6 -12
  46. package/dist/lib/platform/pr-summary.js +28 -7
  47. package/dist/lib/platform/provider-contract.d.ts +67 -1
  48. package/dist/lib/platform/provider-contract.js +6 -2
  49. package/dist/lib/platform/provider-validation.js +76 -3
  50. package/dist/lib/platform/pull-requests.js +115 -170
  51. package/dist/lib/platform/repository-metadata.js +377 -0
  52. package/dist/lib/platform/security-alerts.js +155 -0
  53. package/dist/lib/platform/verification-sync.js +33 -26
  54. package/dist/lib/process-data/index.js +7 -0
  55. package/dist/lib/run/index.js +5 -0
  56. package/dist/lib/sandbox/commands/control-status.js +22 -0
  57. package/dist/lib/sandbox/commands/create.js +32 -6
  58. package/dist/lib/sandbox/commands/ls.js +5 -0
  59. package/dist/lib/sandbox/commands/rm.js +827 -120
  60. package/dist/lib/sandbox/commands/show.js +16 -0
  61. package/dist/lib/sandbox/control/client.js +83 -9
  62. package/dist/lib/sandbox/control/completed-reentry.js +85 -0
  63. package/dist/lib/sandbox/control/container-identity.js +28 -31
  64. package/dist/lib/sandbox/control/executor.js +149 -14
  65. package/dist/lib/sandbox/control/lifecycle.js +621 -127
  66. package/dist/lib/sandbox/control/native-file-lock.js +177 -0
  67. package/dist/lib/sandbox/control/server.js +305 -35
  68. package/dist/lib/sandbox/control/state.js +5 -3
  69. package/dist/lib/sandbox/control/task-view.js +183 -0
  70. package/dist/lib/sandbox/engines/authority.js +289 -0
  71. package/dist/lib/sandbox/engines/docker-exact-discovery.js +97 -0
  72. package/dist/lib/sandbox/index.js +5 -0
  73. package/dist/lib/sandbox/recovery.js +21 -1
  74. package/dist/lib/sandbox/workspace-view.js +2 -1
  75. package/dist/lib/sandbox/worktree-safety.js +4 -1
  76. package/dist/lib/server/index.js +8 -0
  77. package/dist/lib/task/artifact-lifecycle.js +42 -13
  78. package/dist/lib/task/artifact-operations.js +520 -0
  79. package/dist/lib/task/artifact-repair-intent.js +59 -0
  80. package/dist/lib/task/artifact-schema.js +135 -0
  81. package/dist/lib/task/capabilities.js +72 -1
  82. package/dist/lib/task/control-authority.js +4 -0
  83. package/dist/lib/task/create-service.js +103 -4
  84. package/dist/lib/task/create.js +25 -0
  85. package/dist/lib/task/events.js +186 -67
  86. package/dist/lib/task/index.js +11 -3
  87. package/dist/lib/task/local-artifact-finalization.js +75 -150
  88. package/dist/lib/task/orchestration.js +192 -52
  89. package/dist/lib/task/qualification-audit.js +490 -0
  90. package/dist/lib/task/qualification-confirmation.js +109 -0
  91. package/dist/lib/task/qualification-intents.js +148 -0
  92. package/dist/lib/task/review-artifacts.js +22 -23
  93. package/dist/lib/task/review-finalization.js +121 -4
  94. package/dist/lib/task/sections.js +6 -4
  95. package/dist/lib/task/verification-config.js +11 -0
  96. package/dist/lib/task/verification-engine.js +54 -4
  97. package/dist/lifecycle-build-manifest.json +3 -3
  98. package/lib/agent-client.ts +9 -0
  99. package/lib/internal/agent-client.ts +4 -2
  100. package/lib/internal/cli-route-inventory.ts +140 -0
  101. package/lib/internal/codex-lifecycle.ts +8 -6
  102. package/lib/internal/codex-sandbox-controller.ts +7 -2
  103. package/lib/internal/git-workflow.ts +9 -6
  104. package/lib/internal/platform-checks.ts +12 -4
  105. package/lib/internal/platform-comment.ts +12 -4
  106. package/lib/internal/platform-context.ts +3 -1
  107. package/lib/internal/platform-issue.ts +11 -4
  108. package/lib/internal/platform-metadata.ts +70 -0
  109. package/lib/internal/platform-pr-review.ts +14 -3
  110. package/lib/internal/platform-pr.ts +23 -10
  111. package/lib/internal/platform-release-notes.ts +5 -3
  112. package/lib/internal/platform-security.ts +83 -0
  113. package/lib/internal/pr-review-grade.ts +10 -3
  114. package/lib/internal/release-workflow.ts +17 -5
  115. package/lib/internal/sandbox-control.ts +19 -5
  116. package/lib/internal/task-activity.ts +12 -4
  117. package/lib/internal/task-artifact.ts +164 -25
  118. package/lib/internal/task-context.ts +3 -1
  119. package/lib/internal/task-create.ts +76 -15
  120. package/lib/internal/task-delivery.ts +3 -1
  121. package/lib/internal/task-event.ts +3 -1
  122. package/lib/internal/task-finalization.ts +3 -1
  123. package/lib/internal/task-invalidation.ts +3 -1
  124. package/lib/internal/task-ledger.ts +11 -1
  125. package/lib/internal/task-lifecycle.ts +3 -1
  126. package/lib/internal/task-operation-registry.ts +482 -0
  127. package/lib/internal/task-orchestration.ts +12 -0
  128. package/lib/internal/task-override.ts +9 -3
  129. package/lib/internal/task-qualification.ts +61 -0
  130. package/lib/internal/task-review.ts +3 -1
  131. package/lib/internal/task-short-id.ts +10 -1
  132. package/lib/internal/task-snapshot.ts +3 -1
  133. package/lib/internal/task-validate.ts +6 -2
  134. package/lib/internal/task-verify.ts +4 -1
  135. package/lib/internal/task-warning.ts +7 -1
  136. package/lib/platform/comment-safety.ts +250 -0
  137. package/lib/platform/github-provider.ts +229 -7
  138. package/lib/platform/issue-comments.ts +160 -35
  139. package/lib/platform/pr-change-report.ts +6 -12
  140. package/lib/platform/pr-summary.ts +27 -6
  141. package/lib/platform/provider-contract.ts +78 -3
  142. package/lib/platform/provider-validation.ts +85 -3
  143. package/lib/platform/pull-requests.ts +126 -178
  144. package/lib/platform/repository-metadata.ts +453 -0
  145. package/lib/platform/security-alerts.ts +191 -0
  146. package/lib/platform/verification-sync.ts +32 -28
  147. package/lib/process-data/index.ts +7 -0
  148. package/lib/run/index.ts +5 -0
  149. package/lib/sandbox/commands/control-status.ts +26 -0
  150. package/lib/sandbox/commands/create.ts +46 -6
  151. package/lib/sandbox/commands/ls.ts +7 -0
  152. package/lib/sandbox/commands/rm.ts +1000 -87
  153. package/lib/sandbox/commands/show.ts +16 -0
  154. package/lib/sandbox/control/client.ts +103 -9
  155. package/lib/sandbox/control/completed-reentry.ts +98 -0
  156. package/lib/sandbox/control/container-identity.ts +27 -43
  157. package/lib/sandbox/control/executor.ts +177 -12
  158. package/lib/sandbox/control/lifecycle.ts +629 -17
  159. package/lib/sandbox/control/native-file-lock.ts +209 -0
  160. package/lib/sandbox/control/protocol.ts +5 -2
  161. package/lib/sandbox/control/server.ts +319 -37
  162. package/lib/sandbox/control/state.ts +6 -3
  163. package/lib/sandbox/control/task-view.ts +240 -0
  164. package/lib/sandbox/engines/authority.ts +365 -0
  165. package/lib/sandbox/engines/docker-exact-discovery.ts +132 -0
  166. package/lib/sandbox/index.ts +7 -0
  167. package/lib/sandbox/recovery.ts +21 -1
  168. package/lib/sandbox/workspace-view.ts +9 -3
  169. package/lib/sandbox/worktree-safety.ts +4 -1
  170. package/lib/server/index.ts +10 -0
  171. package/lib/task/artifact-lifecycle.ts +38 -16
  172. package/lib/task/artifact-operations.ts +626 -0
  173. package/lib/task/artifact-repair-intent.ts +71 -0
  174. package/lib/task/artifact-schema.ts +192 -0
  175. package/lib/task/capabilities.ts +70 -1
  176. package/lib/task/control-authority.ts +11 -1
  177. package/lib/task/create-service.ts +131 -6
  178. package/lib/task/create.ts +27 -0
  179. package/lib/task/events.ts +161 -65
  180. package/lib/task/index.ts +16 -3
  181. package/lib/task/local-artifact-finalization.ts +87 -218
  182. package/lib/task/orchestration.ts +172 -49
  183. package/lib/task/qualification-audit.ts +599 -0
  184. package/lib/task/qualification-confirmation.ts +117 -0
  185. package/lib/task/qualification-intents.ts +150 -0
  186. package/lib/task/review-artifacts.ts +21 -22
  187. package/lib/task/review-finalization.ts +133 -4
  188. package/lib/task/sections.ts +6 -4
  189. package/lib/task/verification-config.ts +12 -0
  190. package/lib/task/verification-engine.ts +55 -4
  191. package/package.json +6 -1
  192. package/templates/.agents/rules/README.en.md +2 -0
  193. package/templates/.agents/rules/README.zh-CN.md +2 -0
  194. package/templates/.agents/rules/decision-qualification.en.md +22 -0
  195. package/templates/.agents/rules/decision-qualification.zh-CN.md +22 -0
  196. package/templates/.agents/rules/evidence-reporting.en.md +28 -0
  197. package/templates/.agents/rules/evidence-reporting.zh-CN.md +28 -0
  198. package/templates/.agents/rules/label-milestone-setup.en.md +7 -7
  199. package/templates/.agents/rules/label-milestone-setup.zh-CN.md +7 -7
  200. package/templates/.agents/rules/local-artifact-repair.en.md +5 -1
  201. package/templates/.agents/rules/local-artifact-repair.zh-CN.md +5 -1
  202. package/templates/.agents/rules/security-alerts.en.md +8 -8
  203. package/templates/.agents/rules/security-alerts.zh-CN.md +8 -8
  204. package/templates/.agents/skills/analyze-task/SKILL.en.md +15 -2
  205. package/templates/.agents/skills/analyze-task/SKILL.zh-CN.md +19 -2
  206. package/templates/.agents/skills/analyze-task/config/verify.en.json +1 -12
  207. package/templates/.agents/skills/analyze-task/config/verify.zh-CN.json +1 -12
  208. package/templates/.agents/skills/close-codescan/SKILL.en.md +2 -2
  209. package/templates/.agents/skills/close-codescan/SKILL.zh-CN.md +2 -2
  210. package/templates/.agents/skills/close-dependabot/SKILL.en.md +2 -2
  211. package/templates/.agents/skills/close-dependabot/SKILL.zh-CN.md +2 -2
  212. package/templates/.agents/skills/code-task/SKILL.en.md +14 -1
  213. package/templates/.agents/skills/code-task/SKILL.zh-CN.md +14 -1
  214. package/templates/.agents/skills/code-task/config/verify.en.json +5 -14
  215. package/templates/.agents/skills/code-task/config/verify.zh-CN.json +5 -14
  216. package/templates/.agents/skills/code-task/reference/code-rules.en.md +5 -4
  217. package/templates/.agents/skills/code-task/reference/code-rules.zh-CN.md +5 -4
  218. package/templates/.agents/skills/code-task/reference/dual-mode.en.md +1 -1
  219. package/templates/.agents/skills/code-task/reference/dual-mode.zh-CN.md +1 -1
  220. package/templates/.agents/skills/code-task/reference/fix-mode.en.md +3 -1
  221. package/templates/.agents/skills/code-task/reference/fix-mode.zh-CN.md +3 -1
  222. package/templates/.agents/skills/code-task/reference/report-template.en.md +42 -15
  223. package/templates/.agents/skills/code-task/reference/report-template.zh-CN.md +39 -12
  224. package/templates/.agents/skills/complete-manual-validation/SKILL.en.md +5 -1
  225. package/templates/.agents/skills/complete-manual-validation/SKILL.zh-CN.md +5 -1
  226. package/templates/.agents/skills/complete-manual-validation/reference/report-template.en.md +2 -0
  227. package/templates/.agents/skills/complete-manual-validation/reference/report-template.zh-CN.md +2 -0
  228. package/templates/.agents/skills/complete-task/SKILL.en.md +9 -22
  229. package/templates/.agents/skills/complete-task/SKILL.zh-CN.md +9 -22
  230. package/templates/.agents/skills/create-task/SKILL.en.md +8 -0
  231. package/templates/.agents/skills/create-task/SKILL.zh-CN.md +7 -0
  232. package/templates/.agents/skills/import-codescan/SKILL.en.md +1 -1
  233. package/templates/.agents/skills/import-codescan/SKILL.zh-CN.md +1 -1
  234. package/templates/.agents/skills/import-dependabot/SKILL.en.md +1 -1
  235. package/templates/.agents/skills/import-dependabot/SKILL.zh-CN.md +1 -1
  236. package/templates/.agents/skills/init-labels/SKILL.en.md +8 -8
  237. package/templates/.agents/skills/init-labels/SKILL.zh-CN.md +8 -8
  238. package/templates/.agents/skills/init-milestones/SKILL.en.md +9 -11
  239. package/templates/.agents/skills/init-milestones/SKILL.zh-CN.md +9 -11
  240. package/templates/.agents/skills/plan-task/SKILL.en.md +14 -1
  241. package/templates/.agents/skills/plan-task/SKILL.zh-CN.md +16 -1
  242. package/templates/.agents/skills/plan-task/config/verify.en.json +4 -14
  243. package/templates/.agents/skills/plan-task/config/verify.zh-CN.json +4 -14
  244. package/templates/.agents/skills/review-analysis/SKILL.en.md +14 -1
  245. package/templates/.agents/skills/review-analysis/SKILL.zh-CN.md +14 -1
  246. package/templates/.agents/skills/review-analysis/config/verify.en.json +1 -18
  247. package/templates/.agents/skills/review-analysis/config/verify.zh-CN.json +1 -18
  248. package/templates/.agents/skills/review-analysis/reference/report-template.en.md +50 -23
  249. package/templates/.agents/skills/review-analysis/reference/report-template.zh-CN.md +45 -18
  250. package/templates/.agents/skills/review-code/SKILL.en.md +14 -1
  251. package/templates/.agents/skills/review-code/SKILL.zh-CN.md +14 -1
  252. package/templates/.agents/skills/review-code/config/verify.en.json +1 -23
  253. package/templates/.agents/skills/review-code/config/verify.zh-CN.json +1 -23
  254. package/templates/.agents/skills/review-code/reference/report-template.en.md +49 -22
  255. package/templates/.agents/skills/review-code/reference/report-template.zh-CN.md +45 -18
  256. package/templates/.agents/skills/review-plan/SKILL.en.md +14 -1
  257. package/templates/.agents/skills/review-plan/SKILL.zh-CN.md +14 -1
  258. package/templates/.agents/skills/review-plan/config/verify.en.json +1 -18
  259. package/templates/.agents/skills/review-plan/config/verify.zh-CN.json +1 -18
  260. package/templates/.agents/skills/review-plan/reference/report-template.en.md +49 -22
  261. package/templates/.agents/skills/review-plan/reference/report-template.zh-CN.md +45 -18
  262. package/templates/.agents/skills/review-pr/SKILL.en.md +5 -1
  263. package/templates/.agents/skills/review-pr/SKILL.zh-CN.md +5 -1
  264. package/templates/.agents/skills/review-pr/reference/report-template.en.md +4 -3
  265. package/templates/.agents/skills/review-pr/reference/report-template.zh-CN.md +4 -3
  266. package/templates/.agents/skills/run-manual-validation/SKILL.en.md +5 -1
  267. package/templates/.agents/skills/run-manual-validation/SKILL.zh-CN.md +5 -1
  268. package/templates/.agents/skills/run-manual-validation/reference/report-template.en.md +3 -1
  269. package/templates/.agents/skills/run-manual-validation/reference/report-template.zh-CN.md +3 -1
  270. package/templates/.agents/skills/test/SKILL.en.md +13 -2
  271. package/templates/.agents/skills/test/SKILL.zh-CN.md +13 -2
  272. package/templates/.agents/skills/update-agent-infra/scripts/sync-templates.js +14 -0
  273. package/templates/.agents/templates/task.en.md +6 -0
  274. package/templates/.agents/templates/task.zh-CN.md +6 -0
  275. package/templates/.agents/scripts/security-alerts.github.sh +0 -151
  276. package/templates/.agents/scripts/security-alerts.sh +0 -31
  277. package/templates/.agents/skills/init-labels/scripts/init-labels.github.sh +0 -146
  278. package/templates/.agents/skills/init-labels/scripts/init-labels.sh +0 -32
  279. package/templates/.agents/skills/init-milestones/scripts/init-milestones.github.sh +0 -294
  280. package/templates/.agents/skills/init-milestones/scripts/init-milestones.sh +0 -6
@@ -16,8 +16,11 @@ import type {
16
16
  CheckRunSnapshot,
17
17
  GitEvidenceSnapshot,
18
18
  IssueSnapshot,
19
+ LabelReconciliation,
19
20
  MutationReceipt,
21
+ MilestoneInitialization,
20
22
  PlatformContextSnapshot,
23
+ PlatformError,
21
24
  ProviderOperationContext,
22
25
  PlatformProvider,
23
26
  PlatformProviderFactoryInput,
@@ -29,14 +32,17 @@ import type {
29
32
  RemoteCommentSnapshot,
30
33
  RequiredCheckSnapshot,
31
34
  ReviewSnapshot,
35
+ SecurityAlertKind,
36
+ SecurityAlertSnapshot,
32
37
  VerificationRemoteFacts
33
38
  } from './provider-contract.ts';
34
39
  import { resourceIdentityNumber, resourceIdentityString } from './resource-identity.ts';
35
- import { syncLabelDelta } from './in-label-sync.ts';
40
+ import { extractPullRequestFileNames, syncLabelDelta } from './in-label-sync.ts';
36
41
 
37
42
  const CURRENT_USER_QUERY = 'query { viewer { login } }';
38
43
  const ISSUE_TYPES_QUERY = `query($owner:String!){organization(login:$owner){issueTypes(first:20){nodes{id name pinnedFields{__typename ... on IssueFieldSingleSelect{id name options{id name}} ... on IssueFieldDate{id name} ... on IssueFieldText{id name} ... on IssueFieldNumber{id name}}}}}}`;
39
44
  const ISSUE_FIELDS_QUERY = `query($owner:String!,$name:String!,$number:Int!){repository(owner:$owner,name:$name){issue(number:$number){id issueType{id name pinnedFields{__typename ... on IssueFieldSingleSelect{id name options{id name}} ... on IssueFieldDate{id name} ... on IssueFieldText{id name} ... on IssueFieldNumber{id name}}} issueFieldValues(first:50){nodes{__typename ... on IssueFieldSingleSelectValue{name optionId field{... on IssueFieldSingleSelect{id name}}} ... on IssueFieldDateValue{value field{... on IssueFieldDate{id name}}} ... on IssueFieldTextValue{value field{... on IssueFieldText{id name}}} ... on IssueFieldNumberValue{value field{... on IssueFieldNumber{id name}}}}}}}}`;
45
+ const CLOSING_ISSUES_QUERY = 'query($owner:String!,$name:String!,$number:Int!,$cursor:String){repository(owner:$owner,name:$name){pullRequest(number:$number){closingIssuesReferences(first:100,after:$cursor){nodes{number} pageInfo{hasNextPage endCursor}}}}}';
40
46
 
41
47
  function parseGitHubRemote(remote: string): string | null {
42
48
  const trimmed = remote.trim().replace(/\.git$/, '');
@@ -340,6 +346,29 @@ function createReceipt(remoteId: string): ProviderResult<MutationReceipt> {
340
346
  return { ok: true, value: { changed: true, remoteId } };
341
347
  }
342
348
 
349
+ function partialFailure<T>(response: { ok: false; error: PlatformError }, value: T): ProviderResult<T> {
350
+ return { ok: false, error: response.error, value };
351
+ }
352
+
353
+ function labelReconciliation(
354
+ created: string[],
355
+ updated: string[],
356
+ removed: string[],
357
+ skipped: string[]
358
+ ): LabelReconciliation {
359
+ return {
360
+ changed: created.length > 0 || updated.length > 0 || removed.length > 0,
361
+ created,
362
+ updated,
363
+ removed,
364
+ skipped
365
+ };
366
+ }
367
+
368
+ function milestoneInitialization(created: string[], skipped: string[]): MilestoneInitialization {
369
+ return { changed: created.length > 0, created, skipped };
370
+ }
371
+
343
372
  function githubResourceToken(identity: ResourceIdentity | null | undefined): string | null {
344
373
  return resourceIdentityString(identity) || (identity?.kind === 'number' ? String(identity.value) : null);
345
374
  }
@@ -371,7 +400,7 @@ function syncLabels(
371
400
  return { status: changed ? 'applied' as const : 'no-op' as const, changed, error: null };
372
401
  }
373
402
 
374
- function createGitHubOperations(client: GitHubClient): Pick<PlatformProvider, 'issues' | 'comments' | 'changeRequests' | 'checks' | 'reviews' | 'releases' | 'verification'> {
403
+ function createGitHubOperations(client: GitHubClient): Pick<PlatformProvider, 'issues' | 'comments' | 'changeRequests' | 'checks' | 'reviews' | 'releases' | 'securityAlerts' | 'repositoryMetadata' | 'verification'> {
375
404
  const issues: NonNullable<PlatformProvider['issues']> = {
376
405
  async listLabels({ context }) {
377
406
  const response = client.json<any>(['api', '--paginate', '--slurp', `repos/${repository(context)}/labels?per_page=100`], { cwd: context.workingDirectory });
@@ -443,6 +472,19 @@ function createGitHubOperations(client: GitHubClient): Pick<PlatformProvider, 'i
443
472
  return { ok: true, value: issueSnapshot(fetched.value, graph || undefined) };
444
473
  },
445
474
  async create({ context, desired }) {
475
+ let milestone: number | null = null;
476
+ if (desired.milestone) {
477
+ const milestones = client.json<any>(['api', '--paginate', '--slurp', `repos/${repository(context)}/milestones?state=open&per_page=100`], { cwd: context.workingDirectory });
478
+ if (!milestones.ok) return milestones;
479
+ const values = Array.isArray(milestones.value)
480
+ ? milestones.value.flatMap((entry: any) => Array.isArray(entry) ? entry : [entry])
481
+ : [];
482
+ const selected = values.find((item: any) => item?.title === desired.milestone);
483
+ if (!Number.isSafeInteger(selected?.number) || selected.number <= 0) {
484
+ return invalid('MILESTONE_IDENTITY_INVALID', 'Milestone title does not resolve to a positive number');
485
+ }
486
+ milestone = selected.number;
487
+ }
446
488
  const response = client.json<any>(['api', `repos/${repository(context)}/issues`, '-X', 'POST', '--input', '-'], {
447
489
  cwd: context.workingDirectory,
448
490
  method: 'POST',
@@ -451,7 +493,7 @@ function createGitHubOperations(client: GitHubClient): Pick<PlatformProvider, 'i
451
493
  body: desired.body,
452
494
  labels: desired.labels,
453
495
  assignees: desired.assignees,
454
- ...(desired.milestone ? { milestone: desired.milestone } : {})
496
+ ...(milestone ? { milestone } : {})
455
497
  })
456
498
  });
457
499
  if (!response.ok) return response;
@@ -611,6 +653,58 @@ function createGitHubOperations(client: GitHubClient): Pick<PlatformProvider, 'i
611
653
  if (!fetched.ok) return fetched;
612
654
  return { ok: true, value: changeRequestSnapshot(fetched.value) };
613
655
  },
656
+ async listFiles({ context, target }) {
657
+ const number = resourceIdentityNumber(target);
658
+ if (!number) return invalid('PR_NUMBER_INVALID', 'Pull request number must be positive');
659
+ const response = client.json<unknown>(['api', '--paginate', '--slurp', `repos/${repository(context)}/pulls/${number}/files?per_page=100`], { cwd: context.workingDirectory });
660
+ if (!response.ok) return response;
661
+ const files = extractPullRequestFileNames(response.value);
662
+ return files
663
+ ? { ok: true, value: files }
664
+ : invalid('IN_LABEL_SYNC_FILES_INVALID', 'Pull request files response is incomplete');
665
+ },
666
+ async listClosingIssues({ context, target }) {
667
+ const number = resourceIdentityNumber(target);
668
+ if (!number) return invalid('PR_NUMBER_INVALID', 'Pull request number must be positive');
669
+ const [owner, name] = repository(context).split('/');
670
+ if (!owner || !name) return invalid('IN_LABEL_SYNC_REPOSITORY_INVALID', 'Repository identity is invalid');
671
+ const identities: ResourceIdentity[] = [];
672
+ let cursor: string | null = null;
673
+ for (;;) {
674
+ const args = ['api', 'graphql', '-f', `query=${CLOSING_ISSUES_QUERY}`, '-F', `owner=${owner}`, '-F', `name=${name}`, '-F', `number=${number}`];
675
+ if (cursor) args.push('-F', `cursor=${cursor}`);
676
+ const response = client.json<unknown>(args, { cwd: context.workingDirectory });
677
+ if (!response.ok) return response;
678
+ const connection = (response.value as {
679
+ data?: { repository?: { pullRequest?: { closingIssuesReferences?: {
680
+ nodes?: unknown[]; pageInfo?: { hasNextPage?: boolean; endCursor?: string | null };
681
+ } } } };
682
+ })?.data?.repository?.pullRequest?.closingIssuesReferences;
683
+ if (!connection || !Array.isArray(connection.nodes) || !connection.pageInfo) {
684
+ return invalid('IN_LABEL_SYNC_ASSOCIATION_INVALID', 'Closing Issue response is incomplete');
685
+ }
686
+ for (const node of connection.nodes) {
687
+ const issue = Number((node as { number?: unknown })?.number);
688
+ if (!Number.isSafeInteger(issue) || issue <= 0) {
689
+ return invalid('IN_LABEL_SYNC_ASSOCIATION_INVALID', 'Closing Issue identity is invalid');
690
+ }
691
+ identities.push({ kind: 'number', value: issue });
692
+ }
693
+ if (!connection.pageInfo.hasNextPage) {
694
+ const seen = new Set<string>();
695
+ return { ok: true, value: identities.filter((identity) => {
696
+ const key = `${identity.kind}:${identity.value}`;
697
+ if (seen.has(key)) return false;
698
+ seen.add(key);
699
+ return true;
700
+ }) };
701
+ }
702
+ if (!connection.pageInfo.endCursor || connection.pageInfo.endCursor === cursor) {
703
+ return invalid('IN_LABEL_SYNC_ASSOCIATION_INVALID', 'Closing Issue pagination cursor is invalid');
704
+ }
705
+ cursor = connection.pageInfo.endCursor;
706
+ }
707
+ },
614
708
  async listClosing({ context, issue }) {
615
709
  const number = resourceIdentityNumber(issue);
616
710
  if (!number) return invalid('ISSUE_NUMBER_INVALID', 'Issue number must be positive');
@@ -854,20 +948,148 @@ function createGitHubOperations(client: GitHubClient): Pick<PlatformProvider, 'i
854
948
  }
855
949
  };
856
950
 
951
+ const securityAlerts: NonNullable<PlatformProvider['securityAlerts']> = {
952
+ async inspect({ context, kind, number }): Promise<ProviderResult<SecurityAlertSnapshot>> {
953
+ if (!Number.isSafeInteger(number) || number <= 0) return invalid('SECURITY_NUMBER_INVALID', 'Alert number must be a positive integer');
954
+ const suffix = kind === 'dependabot' ? 'dependabot' : kind === 'code-scanning' ? 'code-scanning' : null;
955
+ if (!suffix) return invalid('SECURITY_KIND_INVALID', 'Alert kind is invalid');
956
+ const response = client.json<any>(['api', `repos/${repository(context)}/${suffix}/alerts/${number}`], { cwd: context.workingDirectory });
957
+ if (!response.ok) return response;
958
+ const data = response.value;
959
+ if (!data || typeof data !== 'object' || Array.isArray(data)) return invalid('SECURITY_RESPONSE_INVALID', 'The platform returned an invalid security alert');
960
+ const value: SecurityAlertSnapshot = {
961
+ kind,
962
+ number,
963
+ state: typeof data.state === 'string' ? data.state : 'invalid',
964
+ data
965
+ };
966
+ return { ok: true, value };
967
+ },
968
+ async dismiss({ context, kind, number, reason, comment }): Promise<ProviderResult<MutationReceipt>> {
969
+ const suffix = kind === 'dependabot' ? 'dependabot' : kind === 'code-scanning' ? 'code-scanning' : null;
970
+ if (!suffix) return invalid('SECURITY_KIND_INVALID', 'Alert kind is invalid');
971
+ if (!Number.isSafeInteger(number) || number <= 0) return invalid('SECURITY_NUMBER_INVALID', 'Alert number must be a positive integer');
972
+ const response = client.json<any>(['api', `repos/${repository(context)}/${suffix}/alerts/${number}`, '-X', 'PATCH', '--input', '-'], {
973
+ cwd: context.workingDirectory,
974
+ method: 'PATCH',
975
+ input: JSON.stringify({ state: 'dismissed', dismissed_reason: reason, dismissed_comment: comment })
976
+ });
977
+ if (!response.ok) return response;
978
+ return createReceipt(String(response.value?.id || `${kind}:${number}`));
979
+ }
980
+ };
981
+
982
+ const repositoryMetadata: NonNullable<PlatformProvider['repositoryMetadata']> = {
983
+ async reconcileLabels({ context, desired, cleanupStaleIn }): Promise<ProviderResult<LabelReconciliation>> {
984
+ const endpoint = `repos/${repository(context)}/labels?per_page=100`;
985
+ const listed = client.json<any>(['api', '--paginate', '--slurp', endpoint], { cwd: context.workingDirectory });
986
+ if (!listed.ok) return listed;
987
+ if (!Array.isArray(listed.value)) return invalid('LABEL_RESPONSE_INVALID', 'The platform returned an invalid label list');
988
+ const values = Array.isArray(listed.value)
989
+ ? listed.value.flatMap((entry: any) => Array.isArray(entry) ? entry : [entry])
990
+ : [];
991
+ if (values.some((entry: any) => !entry || typeof entry.name !== 'string' || !entry.name)) {
992
+ return invalid('LABEL_RESPONSE_INVALID', 'The platform returned a label without a valid name');
993
+ }
994
+ const current = new Map<string, any>(values.map((entry: any) => [String(entry.name), entry]));
995
+ const created: string[] = [];
996
+ const updated: string[] = [];
997
+ const skipped: string[] = [];
998
+ const removed: string[] = [];
999
+ for (const label of desired) {
1000
+ const existing = current.get(label.name);
1001
+ if (!existing) {
1002
+ const response = client.json(['api', `repos/${repository(context)}/labels`, '-X', 'POST', '--input', '-'], {
1003
+ cwd: context.workingDirectory,
1004
+ method: 'POST',
1005
+ input: JSON.stringify({ name: label.name, color: label.color, description: label.description })
1006
+ });
1007
+ if (!response.ok) return partialFailure(response, labelReconciliation(created, updated, removed, skipped));
1008
+ created.push(label.name);
1009
+ continue;
1010
+ }
1011
+ const sameColor = typeof existing.color === 'string' && existing.color.toUpperCase() === label.color.toUpperCase();
1012
+ const sameDescription = String(existing.description || '') === label.description;
1013
+ if (sameColor && sameDescription) {
1014
+ skipped.push(label.name);
1015
+ continue;
1016
+ }
1017
+ const response = client.json(['api', `repos/${repository(context)}/labels/${encodeURIComponent(label.name)}`, '-X', 'PATCH', '--input', '-'], {
1018
+ cwd: context.workingDirectory,
1019
+ method: 'PATCH',
1020
+ input: JSON.stringify({ new_name: label.name, color: label.color, description: label.description })
1021
+ });
1022
+ if (!response.ok) return partialFailure(response, labelReconciliation(created, updated, removed, skipped));
1023
+ updated.push(label.name);
1024
+ }
1025
+ const desiredNames = new Set(desired.map((label) => label.name));
1026
+ if (cleanupStaleIn) {
1027
+ for (const name of current.keys()) {
1028
+ if (!name.startsWith('in:') || desiredNames.has(name)) continue;
1029
+ const response = client.text(['api', `repos/${repository(context)}/labels/${encodeURIComponent(name)}`, '-X', 'DELETE'], {
1030
+ cwd: context.workingDirectory,
1031
+ method: 'DELETE'
1032
+ });
1033
+ if (!response.ok) return partialFailure(response, labelReconciliation(created, updated, removed, skipped));
1034
+ removed.push(name);
1035
+ }
1036
+ }
1037
+ return {
1038
+ ok: true,
1039
+ value: {
1040
+ changed: created.length > 0 || updated.length > 0 || removed.length > 0,
1041
+ created,
1042
+ updated,
1043
+ removed,
1044
+ skipped
1045
+ }
1046
+ };
1047
+ },
1048
+ async reconcileMilestones({ context, desired }): Promise<ProviderResult<MilestoneInitialization>> {
1049
+ const listed = client.json<any>(['api', '--paginate', '--slurp', `repos/${repository(context)}/milestones?state=all&per_page=100`], { cwd: context.workingDirectory });
1050
+ if (!listed.ok) return listed;
1051
+ if (!Array.isArray(listed.value)) return invalid('MILESTONE_RESPONSE_INVALID', 'The platform returned an invalid milestone list');
1052
+ const values = Array.isArray(listed.value)
1053
+ ? listed.value.flatMap((entry: any) => Array.isArray(entry) ? entry : [entry])
1054
+ : [];
1055
+ if (values.some((entry: any) => !entry || typeof entry.title !== 'string' || !entry.title)) {
1056
+ return invalid('MILESTONE_RESPONSE_INVALID', 'The platform returned a milestone without a valid title');
1057
+ }
1058
+ const existing = new Set(values.map((entry: any) => String(entry.title)));
1059
+ const created: string[] = [];
1060
+ const skipped: string[] = [];
1061
+ for (const milestone of desired) {
1062
+ if (existing.has(milestone.title)) {
1063
+ skipped.push(milestone.title);
1064
+ continue;
1065
+ }
1066
+ const response = client.json(['api', `repos/${repository(context)}/milestones`, '-X', 'POST', '--input', '-'], {
1067
+ cwd: context.workingDirectory,
1068
+ method: 'POST',
1069
+ input: JSON.stringify({ title: milestone.title, description: milestone.description, state: milestone.state })
1070
+ });
1071
+ if (!response.ok) return partialFailure(response, milestoneInitialization(created, skipped));
1072
+ existing.add(milestone.title);
1073
+ created.push(milestone.title);
1074
+ }
1075
+ return { ok: true, value: { changed: created.length > 0, created, skipped } };
1076
+ }
1077
+ };
1078
+
857
1079
  const verification: NonNullable<PlatformProvider['verification']> = {
858
1080
  async fetchRemoteFacts(input) {
859
1081
  const issue = input.issue && issues.inspect
860
1082
  ? await issues.inspect({ context: input.context, target: input.issue })
861
1083
  : { ok: true as const, value: null };
862
- if (!issue.ok) return issue;
1084
+ if (!issue.ok) return { ok: false, error: issue.error };
863
1085
  const commentFacts = input.includeComments && input.issue && comments.list
864
1086
  ? await comments.list({ context: input.context, parent: input.issue })
865
1087
  : { ok: true as const, value: [] as RemoteCommentSnapshot[] };
866
- if (!commentFacts.ok) return commentFacts;
1088
+ if (!commentFacts.ok) return { ok: false, error: commentFacts.error };
867
1089
  const changeRequest = input.changeRequest && changeRequests.inspect
868
1090
  ? await changeRequests.inspect({ context: input.context, target: input.changeRequest })
869
1091
  : { ok: true as const, value: null };
870
- if (!changeRequest.ok) return changeRequest;
1092
+ if (!changeRequest.ok) return { ok: false, error: changeRequest.error };
871
1093
  return {
872
1094
  ok: true,
873
1095
  value: {
@@ -881,7 +1103,7 @@ function createGitHubOperations(client: GitHubClient): Pick<PlatformProvider, 'i
881
1103
  }
882
1104
  };
883
1105
 
884
- return { issues, comments, changeRequests, checks, reviews, releases, verification };
1106
+ return { issues, comments, changeRequests, checks, reviews, releases, securityAlerts, repositoryMetadata, verification };
885
1107
  }
886
1108
 
887
1109
  function createGitHubProvider(
@@ -16,6 +16,14 @@ import {
16
16
  } from './provider-bridge.ts';
17
17
  import { resourceIdentityNumber } from './resource-identity.ts';
18
18
  import { taskIssueIdentity, taskIssueIdentityError } from './task-identities.ts';
19
+ import {
20
+ CONTROL_MARKER_PATTERN,
21
+ escapeHtmlText,
22
+ fenceRanges,
23
+ renderSafeCodeFence,
24
+ sanitizeMarkdownDocument
25
+ } from './comment-safety.ts';
26
+ import type { FenceRange } from './comment-safety.ts';
19
27
 
20
28
  type RemoteComment = { id: number | string; body: string; user?: { login?: string } };
21
29
  type RenderedChunk = { marker: string; body: string; content: string; part: number; total: number };
@@ -66,15 +74,22 @@ function splitFrontmatter(content: string): { frontmatter: string | null; body:
66
74
  return { frontmatter: match[1]!, body: content.slice(match[0].length).replace(/^\r?\n/, '') };
67
75
  }
68
76
 
77
+ function sanitizeCommentBody(value: string, label: string): string {
78
+ const result = sanitizeMarkdownDocument(value, { reservedMarkers: [CONTROL_MARKER_PATTERN] });
79
+ if (!result.ok) throw new Error(`${result.error.code}: ${label}: ${result.error.message} at offset ${result.error.offset}`);
80
+ return result.value;
81
+ }
82
+
69
83
  function footer(agent: string, taskId: string): string {
70
84
  return `---\n*由 ${agent} 自动生成 · 内部追踪:${taskId}*`;
71
85
  }
72
86
 
73
87
  function renderTaskComment(content: string, taskId: string, agent: string): string {
74
88
  const split = splitFrontmatter(content);
89
+ const safeBody = sanitizeCommentBody(split.body, 'task body');
75
90
  const taskBody = split.frontmatter === null
76
- ? split.body
77
- : `<details><summary>元数据 (frontmatter)</summary>\n\n\`\`\`yaml\n---\n${split.frontmatter}\n---\n\`\`\`\n\n</details>\n\n${split.body}`;
91
+ ? safeBody
92
+ : `<details><summary>元数据 (frontmatter)</summary>\n\n${renderSafeCodeFence(`---\n${split.frontmatter}\n---`, 'yaml')}\n\n</details>\n\n${safeBody}`;
78
93
  return normalizeCommentContent([
79
94
  MARKERS.task(taskId),
80
95
  '## 任务文件',
@@ -96,13 +111,16 @@ function artifactIdentity(artifact: string): { stem: string; title: string } {
96
111
  return { stem, title: `${base}(Round ${round})` };
97
112
  }
98
113
 
99
- function chunkByUtf8(content: string, maxBytes: number): string[] {
114
+ type SourceChunk = { content: string; start: number; end: number };
115
+
116
+ function chunkByUtf8(content: string, maxBytes: number): SourceChunk[] {
100
117
  if (maxBytes <= 0) throw new Error('comment byte limit is too small');
101
- const chunks: string[] = [];
102
- let remaining = content;
103
- while (remaining) {
118
+ const chunks: SourceChunk[] = [];
119
+ let start = 0;
120
+ while (start < content.length) {
121
+ const remaining = content.slice(start);
104
122
  if (Buffer.byteLength(remaining, 'utf8') <= maxBytes) {
105
- chunks.push(remaining);
123
+ chunks.push({ content: remaining, start, end: content.length });
106
124
  break;
107
125
  }
108
126
  let bytes = 0;
@@ -117,10 +135,102 @@ function chunkByUtf8(content: string, maxBytes: number): string[] {
117
135
  }
118
136
  const cut = newlineIndex > 0 ? newlineIndex : index;
119
137
  if (cut === 0) throw new Error('comment byte limit cannot fit one Unicode code point');
120
- chunks.push(remaining.slice(0, cut));
121
- remaining = remaining.slice(cut);
138
+ chunks.push({ content: remaining.slice(0, cut), start, end: start + cut });
139
+ start += cut;
122
140
  }
123
- return chunks.length > 0 ? chunks : [''];
141
+ return chunks.length > 0 ? chunks : [{ content: '', start: 0, end: 0 }];
142
+ }
143
+
144
+ function longestFenceLineRun(content: string, character: '`' | '~'): number {
145
+ const pattern = new RegExp(`^ {0,3}(${character}+)[ \\t]*(?:\\n|$)`, 'gm');
146
+ let longest = 0;
147
+ for (const match of content.matchAll(pattern)) longest = Math.max(longest, match[1]!.length);
148
+ return longest;
149
+ }
150
+
151
+ function boundedFence(content: string, sourceCharacter: '`' | '~', maxBytes: number): { opening: string; closing: string } | null {
152
+ const candidates: Array<'`' | '~'> = sourceCharacter === '`' ? ['~', '`'] : ['`', '~'];
153
+ for (const character of candidates) {
154
+ const length = Math.max(3, longestFenceLineRun(content, character) + 1);
155
+ const delimiter = character.repeat(length);
156
+ if (Buffer.byteLength(delimiter, 'utf8') * 2 + 2 <= maxBytes) return { opening: `${delimiter}\n`, closing: `${delimiter}\n` };
157
+ }
158
+ return null;
159
+ }
160
+
161
+ function sourceSlice(piece: SourceChunk, start: number, end: number): string {
162
+ return piece.content.slice(Math.max(0, start - piece.start), Math.max(0, end - piece.start));
163
+ }
164
+
165
+ function renderOversizedFenceChunk(piece: SourceChunk, fences: readonly FenceRange[], maxBytes: number): string {
166
+ const insertions = new Map<number, Array<{ value: string; ensureLineStart: boolean }>>();
167
+ const omitted: Array<{ start: number; end: number }> = [];
168
+ const insert = (position: number, value: string, ensureLineStart = false) => {
169
+ const values = insertions.get(position) || [];
170
+ values.push({ value, ensureLineStart });
171
+ insertions.set(position, values);
172
+ };
173
+
174
+ for (const fence of fences) {
175
+ if (piece.end <= fence.start || piece.start >= fence.end) continue;
176
+ const oversized = Buffer.byteLength(fence.opening, 'utf8') > maxBytes
177
+ || Buffer.byteLength(fence.closing, 'utf8') > maxBytes;
178
+ if (!oversized) {
179
+ if (piece.start > fence.start && piece.start < fence.end) insert(piece.start, fence.opening);
180
+ if (piece.end > fence.start && piece.end < fence.end) insert(piece.end, fence.closing, true);
181
+ continue;
182
+ }
183
+
184
+ const code = sourceSlice(piece, Math.max(piece.start, fence.openingEnd), Math.min(piece.end, fence.closingStart));
185
+ const delimiter = boundedFence(code, fence.character, maxBytes);
186
+ if (!delimiter) return escapeHtmlText(piece.content);
187
+ insert(Math.max(piece.start, fence.start), delimiter.opening);
188
+ insert(Math.min(piece.end, fence.end), delimiter.closing, true);
189
+ const openingStart = Math.max(piece.start, fence.start);
190
+ const openingEnd = Math.min(piece.end, fence.openingEnd);
191
+ if (openingStart < openingEnd) omitted.push({ start: openingStart, end: openingEnd });
192
+ const closingStart = Math.max(piece.start, fence.closingStart);
193
+ const closingEnd = Math.min(piece.end, fence.end);
194
+ if (closingStart < closingEnd) omitted.push({ start: closingStart, end: closingEnd });
195
+ }
196
+
197
+ const boundaries = new Set<number>([piece.start, piece.end]);
198
+ for (const position of insertions.keys()) boundaries.add(position);
199
+ for (const range of omitted) {
200
+ boundaries.add(range.start);
201
+ boundaries.add(range.end);
202
+ }
203
+ const points = [...boundaries].sort((left, right) => left - right);
204
+ let content = '';
205
+ for (let index = 0; index < points.length; index += 1) {
206
+ const point = points[index]!;
207
+ for (const insertion of insertions.get(point) || []) {
208
+ if (insertion.ensureLineStart && content.length > 0 && !content.endsWith('\n')) content += '\n';
209
+ content += insertion.value;
210
+ }
211
+ const next = points[index + 1];
212
+ if (next === undefined) continue;
213
+ const skipped = omitted.some((range) => range.start <= point && next <= range.end);
214
+ if (!skipped) content += sourceSlice(piece, point, next);
215
+ }
216
+ return content;
217
+ }
218
+
219
+ function independentChunkContent(piece: SourceChunk, fences: readonly FenceRange[], maxBytes: number): string {
220
+ const oversizedFence = fences.some((fence) =>
221
+ piece.end > fence.start && piece.start < fence.end
222
+ && (Buffer.byteLength(fence.opening, 'utf8') > maxBytes || Buffer.byteLength(fence.closing, 'utf8') > maxBytes)
223
+ );
224
+ if (oversizedFence) return renderOversizedFenceChunk(piece, fences, maxBytes);
225
+ const openingFence = fences.find((fence) => piece.start > fence.start && piece.start < fence.end);
226
+ const closingFence = fences.find((fence) => piece.end > fence.start && piece.end < fence.end);
227
+ let content = piece.content;
228
+ if (openingFence) content = openingFence.opening + content;
229
+ if (closingFence) {
230
+ if (!content.endsWith('\n')) content += '\n';
231
+ content += closingFence.closing;
232
+ }
233
+ return content;
124
234
  }
125
235
 
126
236
  function buildArtifactChunk(
@@ -132,7 +242,8 @@ function buildArtifactChunk(
132
242
  part: number,
133
243
  total: number,
134
244
  chunked: boolean,
135
- backfill: boolean
245
+ backfill: boolean,
246
+ renderedContent = content
136
247
  ): RenderedChunk {
137
248
  const marker = chunked ? MARKERS.artifactChunk(taskId, stem, part, total) : MARKERS.artifact(taskId, stem);
138
249
  const heading = chunked ? `## ${title}(${part}/${total})` : `## ${title}`;
@@ -144,7 +255,7 @@ function buildArtifactChunk(
144
255
  '',
145
256
  `> **${agent}** · ${taskId}`,
146
257
  '',
147
- content,
258
+ renderedContent,
148
259
  '',
149
260
  footer(agent, taskId)
150
261
  ].join('\n'));
@@ -161,29 +272,44 @@ function chunkArtifactComment(input: {
161
272
  }): RenderedChunk[] {
162
273
  const byteLimit = input.byteLimit || 60_000;
163
274
  const identity = artifactIdentity(input.artifact);
275
+ const safeBody = sanitizeCommentBody(input.body, 'artifact body');
164
276
  const single = buildArtifactChunk(
165
- input.taskId, identity.stem, identity.title, input.agent, input.body, 1, 1, false, Boolean(input.backfill)
277
+ input.taskId, identity.stem, identity.title, input.agent, safeBody, 1, 1, false, Boolean(input.backfill)
166
278
  );
167
279
  if (Buffer.byteLength(single.body, 'utf8') <= byteLimit) return [single];
168
280
 
169
281
  let total = 2;
170
- let pieces: string[] = [];
282
+ let payloadLimit = Number.POSITIVE_INFINITY;
283
+ const ranges = fenceRanges(safeBody);
284
+ if (!ranges.ok) throw new Error(`${ranges.error.code}: artifact body: ${ranges.error.message} at offset ${ranges.error.offset}`);
171
285
  for (;;) {
172
286
  const probe = buildArtifactChunk(
173
287
  input.taskId, identity.stem, identity.title, input.agent, '', total, total, true, Boolean(input.backfill)
174
288
  );
175
289
  const available = byteLimit - Buffer.byteLength(probe.body, 'utf8');
176
- pieces = chunkByUtf8(input.body, available);
177
- if (pieces.length === total) break;
178
- total = pieces.length;
290
+ const sourceLimit = Math.min(available, payloadLimit);
291
+ if (sourceLimit <= 0) throw new Error('comment byte limit is too small for an artifact chunk');
292
+ const pieces = chunkByUtf8(safeBody, sourceLimit);
293
+ if (pieces.length !== total) {
294
+ total = pieces.length;
295
+ continue;
296
+ }
297
+ const chunks = pieces.map((piece, index) => buildArtifactChunk(
298
+ input.taskId,
299
+ identity.stem,
300
+ identity.title,
301
+ input.agent,
302
+ piece.content,
303
+ index + 1,
304
+ total,
305
+ true,
306
+ Boolean(input.backfill),
307
+ independentChunkContent(piece, ranges.value, sourceLimit)
308
+ ));
309
+ const overflow = Math.max(...chunks.map((chunk) => Buffer.byteLength(chunk.body, 'utf8') - byteLimit));
310
+ if (overflow <= 0) return chunks;
311
+ payloadLimit = sourceLimit - overflow;
179
312
  }
180
- return pieces.map((content, index) => {
181
- const chunk = buildArtifactChunk(
182
- input.taskId, identity.stem, identity.title, input.agent, content, index + 1, total, true, Boolean(input.backfill)
183
- );
184
- if (Buffer.byteLength(chunk.body, 'utf8') > byteLimit) throw new Error('rendered comment exceeds byte limit');
185
- return chunk;
186
- });
187
313
  }
188
314
 
189
315
  function findMarkerComments(comments: RemoteComment[], marker: string): RemoteComment[] {
@@ -224,7 +350,7 @@ function hasResolvedPlatformContext(context: PlatformResult): boolean {
224
350
 
225
351
  function bodyEnvelope(marker: string, title: string, taskId: string, agent: string, body: string): string {
226
352
  return normalizeCommentContent([
227
- marker, `## ${title}`, '', `> **${agent}** · ${taskId}`, '', body.replace(/\n+$/, ''), '', footer(agent, taskId)
353
+ marker, `## ${title}`, '', `> **${agent}** · ${taskId}`, '', sanitizeCommentBody(body, `${title} body`).replace(/\n+$/, ''), '', footer(agent, taskId)
228
354
  ].join('\n'));
229
355
  }
230
356
 
@@ -334,6 +460,15 @@ async function syncPlatformComment(taskRef: string, options: SyncOptions): Promi
334
460
  error: { code: 'ISSUE_NOT_LINKED', message: 'Task has no valid issue_number', retryable: false }
335
461
  });
336
462
  }
463
+ let desired: RenderedChunk[];
464
+ try {
465
+ desired = expectedComments(resolved.taskId, taskContent, resolved.taskDir, options);
466
+ } catch (error) {
467
+ return platformResult('failed', {
468
+ resource: { kind: 'issue', number: resourceIdentityNumber(issueIdentityFromTask) },
469
+ error: { code: 'COMMENT_PAYLOAD_INVALID', message: error instanceof Error ? error.message : String(error), retryable: false }
470
+ });
471
+ }
337
472
  const loaded = await resolvePlatformProviderContext({ cwd: resolved.repoRoot, client: options.client });
338
473
  const context = loaded.ok ? loaded.value.context : loaded.context;
339
474
  if (!hasResolvedPlatformContext(context) || !loaded.ok) return context;
@@ -356,16 +491,6 @@ async function syncPlatformComment(taskRef: string, options: SyncOptions): Promi
356
491
  });
357
492
  }
358
493
 
359
- let desired: RenderedChunk[];
360
- try {
361
- desired = expectedComments(resolved.taskId, taskContent, resolved.taskDir, options);
362
- } catch (error) {
363
- return platformResult('failed', {
364
- ...contextFields(context),
365
- resource: { kind: 'issue', number: issue },
366
- error: { code: 'COMMENT_PAYLOAD_INVALID', message: error instanceof Error ? error.message : String(error), retryable: false }
367
- });
368
- }
369
494
  const existing = relatedComments(listed.value, markerPrefix(resolved.taskId, options));
370
495
  if (!validateRelatedMarkerSet(existing, markerPrefix(resolved.taskId, options)).ok) {
371
496
  return platformResult('failed', {
@@ -2,6 +2,7 @@ import fs from 'node:fs';
2
2
  import path from 'node:path';
3
3
  import { createHash, randomUUID } from 'node:crypto';
4
4
  import { execFileSync } from 'node:child_process';
5
+ import { escapeHtmlText } from './comment-safety.ts';
5
6
 
6
7
  const REPORT_FILE_NAME = 'pr-change-report.json';
7
8
  const REPORT_VERSION = 1 as const;
@@ -483,22 +484,15 @@ function representativePath(file: ChangeFile): string {
483
484
  return file.newPath || file.oldPath || '';
484
485
  }
485
486
 
486
- function escapeHtml(value: string): string {
487
- return value
488
- .replaceAll('&', '&amp;')
489
- .replaceAll('<', '&lt;')
490
- .replaceAll('>', '&gt;')
491
- .replaceAll('"', '&quot;')
492
- .replaceAll("'", '&#39;')
493
- .replaceAll('\r', '\\r')
494
- .replaceAll('\n', '\\n');
487
+ function escapeReportText(value: string): string {
488
+ return escapeHtmlText(value).replaceAll('\r', '\\r').replaceAll('\n', '\\n');
495
489
  }
496
490
 
497
491
  function renderRepresentativePath(file: ChangeFile): string {
498
492
  const paths = file.oldPath && file.newPath && file.oldPath !== file.newPath
499
493
  ? [file.oldPath, file.newPath]
500
494
  : [representativePath(file)];
501
- return paths.map((filePath) => `<code>${escapeHtml(filePath)}</code>`).join(' → ');
495
+ return paths.map((filePath) => `<code>${escapeReportText(filePath)}</code>`).join(' → ');
502
496
  }
503
497
 
504
498
  function representativeScore(file: ChangeFile, metric: 'lines' | 'bytes'): number {
@@ -551,12 +545,12 @@ function renderEvidence(evidence: Evidence): string {
551
545
  ? `:${evidence.startLine}`
552
546
  : `:${evidence.startLine}-${evidence.endLine}`
553
547
  : `:${evidence.startLine ?? evidence.endLine}`;
554
- return ` - 证据:<code>${escapeHtml(evidence.path)}</code>${location}:${escapeHtml(evidence.detail)}`;
548
+ return ` - 证据:<code>${escapeReportText(evidence.path)}</code>${location}:${escapeReportText(evidence.detail)}`;
555
549
  }
556
550
 
557
551
  function renderPrechecks(checks: Precheck[]): string[] {
558
552
  return checks.flatMap((check) => [
559
- `- **${PRECHECK_LABELS[check.id]}(${check.id})**:${check.verdict === 'pass' ? '通过' : '需复核'}。${escapeHtml(check.rationale)}`,
553
+ `- **${PRECHECK_LABELS[check.id]}(${check.id})**:${check.verdict === 'pass' ? '通过' : '需复核'}。${escapeReportText(check.rationale)}`,
560
554
  ...check.evidence.slice(0, 1).map(renderEvidence)
561
555
  ]);
562
556
  }