@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.
- package/bin/cli.ts +26 -9
- package/dist/bin/cli.js +173 -157
- package/dist/bin/internal-cli.js +42 -21
- package/dist/lib/agent-client.js +5 -0
- package/dist/lib/internal/agent-client.js +5 -2
- package/dist/lib/internal/cli-route-inventory.js +152 -0
- package/dist/lib/internal/codex-lifecycle.js +9 -6
- package/dist/lib/internal/codex-sandbox-controller.js +8 -2
- package/dist/lib/internal/git-workflow.js +10 -6
- package/dist/lib/internal/platform-checks.js +16 -4
- package/dist/lib/internal/platform-comment.js +16 -4
- package/dist/lib/internal/platform-context.js +4 -1
- package/dist/lib/internal/platform-issue.js +12 -4
- package/dist/lib/internal/platform-metadata.js +88 -0
- package/dist/lib/internal/platform-pr-review.js +16 -3
- package/dist/lib/internal/platform-pr.js +24 -10
- package/dist/lib/internal/platform-release-notes.js +6 -3
- package/dist/lib/internal/platform-security.js +93 -0
- package/dist/lib/internal/pr-review-grade.js +14 -3
- package/dist/lib/internal/release-workflow.js +19 -5
- package/dist/lib/internal/sandbox-control.js +22 -5
- package/dist/lib/internal/task-activity.js +13 -4
- package/dist/lib/internal/task-artifact.js +186 -27
- package/dist/lib/internal/task-context.js +4 -1
- package/dist/lib/internal/task-create.js +48 -15
- package/dist/lib/internal/task-delivery.js +4 -1
- package/dist/lib/internal/task-event.js +4 -1
- package/dist/lib/internal/task-finalization.js +4 -1
- package/dist/lib/internal/task-invalidation.js +4 -1
- package/dist/lib/internal/task-ledger.js +12 -1
- package/dist/lib/internal/task-lifecycle.js +4 -1
- package/dist/lib/internal/task-operation-registry.js +421 -0
- package/dist/lib/internal/task-orchestration.js +12 -0
- package/dist/lib/internal/task-override.js +10 -3
- package/dist/lib/internal/task-qualification.js +90 -0
- package/dist/lib/internal/task-review.js +4 -1
- package/dist/lib/internal/task-short-id.js +11 -1
- package/dist/lib/internal/task-snapshot.js +4 -1
- package/dist/lib/internal/task-validate.js +7 -2
- package/dist/lib/internal/task-verify.js +5 -1
- package/dist/lib/internal/task-warning.js +8 -1
- package/dist/lib/platform/comment-safety.js +226 -0
- package/dist/lib/platform/github-provider.js +233 -6
- package/dist/lib/platform/issue-comments.js +146 -34
- package/dist/lib/platform/pr-change-report.js +6 -12
- package/dist/lib/platform/pr-summary.js +28 -7
- package/dist/lib/platform/provider-contract.d.ts +67 -1
- package/dist/lib/platform/provider-contract.js +6 -2
- package/dist/lib/platform/provider-validation.js +76 -3
- package/dist/lib/platform/pull-requests.js +115 -170
- package/dist/lib/platform/repository-metadata.js +377 -0
- package/dist/lib/platform/security-alerts.js +155 -0
- package/dist/lib/platform/verification-sync.js +33 -26
- package/dist/lib/process-data/index.js +7 -0
- package/dist/lib/run/index.js +5 -0
- package/dist/lib/sandbox/commands/control-status.js +22 -0
- package/dist/lib/sandbox/commands/create.js +32 -6
- package/dist/lib/sandbox/commands/ls.js +5 -0
- package/dist/lib/sandbox/commands/rm.js +827 -120
- package/dist/lib/sandbox/commands/show.js +16 -0
- package/dist/lib/sandbox/control/client.js +83 -9
- package/dist/lib/sandbox/control/completed-reentry.js +85 -0
- package/dist/lib/sandbox/control/container-identity.js +28 -31
- package/dist/lib/sandbox/control/executor.js +149 -14
- package/dist/lib/sandbox/control/lifecycle.js +621 -127
- package/dist/lib/sandbox/control/native-file-lock.js +177 -0
- package/dist/lib/sandbox/control/server.js +305 -35
- package/dist/lib/sandbox/control/state.js +5 -3
- package/dist/lib/sandbox/control/task-view.js +183 -0
- package/dist/lib/sandbox/engines/authority.js +289 -0
- package/dist/lib/sandbox/engines/docker-exact-discovery.js +97 -0
- package/dist/lib/sandbox/index.js +5 -0
- package/dist/lib/sandbox/recovery.js +21 -1
- package/dist/lib/sandbox/workspace-view.js +2 -1
- package/dist/lib/sandbox/worktree-safety.js +4 -1
- package/dist/lib/server/index.js +8 -0
- package/dist/lib/task/artifact-lifecycle.js +42 -13
- package/dist/lib/task/artifact-operations.js +520 -0
- package/dist/lib/task/artifact-repair-intent.js +59 -0
- package/dist/lib/task/artifact-schema.js +135 -0
- package/dist/lib/task/capabilities.js +72 -1
- package/dist/lib/task/control-authority.js +4 -0
- package/dist/lib/task/create-service.js +103 -4
- package/dist/lib/task/create.js +25 -0
- package/dist/lib/task/events.js +186 -67
- package/dist/lib/task/index.js +11 -3
- package/dist/lib/task/local-artifact-finalization.js +75 -150
- package/dist/lib/task/orchestration.js +192 -52
- package/dist/lib/task/qualification-audit.js +490 -0
- package/dist/lib/task/qualification-confirmation.js +109 -0
- package/dist/lib/task/qualification-intents.js +148 -0
- package/dist/lib/task/review-artifacts.js +22 -23
- package/dist/lib/task/review-finalization.js +121 -4
- package/dist/lib/task/sections.js +6 -4
- package/dist/lib/task/verification-config.js +11 -0
- package/dist/lib/task/verification-engine.js +54 -4
- package/dist/lifecycle-build-manifest.json +3 -3
- package/lib/agent-client.ts +9 -0
- package/lib/internal/agent-client.ts +4 -2
- package/lib/internal/cli-route-inventory.ts +140 -0
- package/lib/internal/codex-lifecycle.ts +8 -6
- package/lib/internal/codex-sandbox-controller.ts +7 -2
- package/lib/internal/git-workflow.ts +9 -6
- package/lib/internal/platform-checks.ts +12 -4
- package/lib/internal/platform-comment.ts +12 -4
- package/lib/internal/platform-context.ts +3 -1
- package/lib/internal/platform-issue.ts +11 -4
- package/lib/internal/platform-metadata.ts +70 -0
- package/lib/internal/platform-pr-review.ts +14 -3
- package/lib/internal/platform-pr.ts +23 -10
- package/lib/internal/platform-release-notes.ts +5 -3
- package/lib/internal/platform-security.ts +83 -0
- package/lib/internal/pr-review-grade.ts +10 -3
- package/lib/internal/release-workflow.ts +17 -5
- package/lib/internal/sandbox-control.ts +19 -5
- package/lib/internal/task-activity.ts +12 -4
- package/lib/internal/task-artifact.ts +164 -25
- package/lib/internal/task-context.ts +3 -1
- package/lib/internal/task-create.ts +76 -15
- package/lib/internal/task-delivery.ts +3 -1
- package/lib/internal/task-event.ts +3 -1
- package/lib/internal/task-finalization.ts +3 -1
- package/lib/internal/task-invalidation.ts +3 -1
- package/lib/internal/task-ledger.ts +11 -1
- package/lib/internal/task-lifecycle.ts +3 -1
- package/lib/internal/task-operation-registry.ts +482 -0
- package/lib/internal/task-orchestration.ts +12 -0
- package/lib/internal/task-override.ts +9 -3
- package/lib/internal/task-qualification.ts +61 -0
- package/lib/internal/task-review.ts +3 -1
- package/lib/internal/task-short-id.ts +10 -1
- package/lib/internal/task-snapshot.ts +3 -1
- package/lib/internal/task-validate.ts +6 -2
- package/lib/internal/task-verify.ts +4 -1
- package/lib/internal/task-warning.ts +7 -1
- package/lib/platform/comment-safety.ts +250 -0
- package/lib/platform/github-provider.ts +229 -7
- package/lib/platform/issue-comments.ts +160 -35
- package/lib/platform/pr-change-report.ts +6 -12
- package/lib/platform/pr-summary.ts +27 -6
- package/lib/platform/provider-contract.ts +78 -3
- package/lib/platform/provider-validation.ts +85 -3
- package/lib/platform/pull-requests.ts +126 -178
- package/lib/platform/repository-metadata.ts +453 -0
- package/lib/platform/security-alerts.ts +191 -0
- package/lib/platform/verification-sync.ts +32 -28
- package/lib/process-data/index.ts +7 -0
- package/lib/run/index.ts +5 -0
- package/lib/sandbox/commands/control-status.ts +26 -0
- package/lib/sandbox/commands/create.ts +46 -6
- package/lib/sandbox/commands/ls.ts +7 -0
- package/lib/sandbox/commands/rm.ts +1000 -87
- package/lib/sandbox/commands/show.ts +16 -0
- package/lib/sandbox/control/client.ts +103 -9
- package/lib/sandbox/control/completed-reentry.ts +98 -0
- package/lib/sandbox/control/container-identity.ts +27 -43
- package/lib/sandbox/control/executor.ts +177 -12
- package/lib/sandbox/control/lifecycle.ts +629 -17
- package/lib/sandbox/control/native-file-lock.ts +209 -0
- package/lib/sandbox/control/protocol.ts +5 -2
- package/lib/sandbox/control/server.ts +319 -37
- package/lib/sandbox/control/state.ts +6 -3
- package/lib/sandbox/control/task-view.ts +240 -0
- package/lib/sandbox/engines/authority.ts +365 -0
- package/lib/sandbox/engines/docker-exact-discovery.ts +132 -0
- package/lib/sandbox/index.ts +7 -0
- package/lib/sandbox/recovery.ts +21 -1
- package/lib/sandbox/workspace-view.ts +9 -3
- package/lib/sandbox/worktree-safety.ts +4 -1
- package/lib/server/index.ts +10 -0
- package/lib/task/artifact-lifecycle.ts +38 -16
- package/lib/task/artifact-operations.ts +626 -0
- package/lib/task/artifact-repair-intent.ts +71 -0
- package/lib/task/artifact-schema.ts +192 -0
- package/lib/task/capabilities.ts +70 -1
- package/lib/task/control-authority.ts +11 -1
- package/lib/task/create-service.ts +131 -6
- package/lib/task/create.ts +27 -0
- package/lib/task/events.ts +161 -65
- package/lib/task/index.ts +16 -3
- package/lib/task/local-artifact-finalization.ts +87 -218
- package/lib/task/orchestration.ts +172 -49
- package/lib/task/qualification-audit.ts +599 -0
- package/lib/task/qualification-confirmation.ts +117 -0
- package/lib/task/qualification-intents.ts +150 -0
- package/lib/task/review-artifacts.ts +21 -22
- package/lib/task/review-finalization.ts +133 -4
- package/lib/task/sections.ts +6 -4
- package/lib/task/verification-config.ts +12 -0
- package/lib/task/verification-engine.ts +55 -4
- package/package.json +6 -1
- package/templates/.agents/rules/README.en.md +2 -0
- package/templates/.agents/rules/README.zh-CN.md +2 -0
- package/templates/.agents/rules/decision-qualification.en.md +22 -0
- package/templates/.agents/rules/decision-qualification.zh-CN.md +22 -0
- package/templates/.agents/rules/evidence-reporting.en.md +28 -0
- package/templates/.agents/rules/evidence-reporting.zh-CN.md +28 -0
- package/templates/.agents/rules/label-milestone-setup.en.md +7 -7
- package/templates/.agents/rules/label-milestone-setup.zh-CN.md +7 -7
- package/templates/.agents/rules/local-artifact-repair.en.md +5 -1
- package/templates/.agents/rules/local-artifact-repair.zh-CN.md +5 -1
- package/templates/.agents/rules/security-alerts.en.md +8 -8
- package/templates/.agents/rules/security-alerts.zh-CN.md +8 -8
- package/templates/.agents/skills/analyze-task/SKILL.en.md +15 -2
- package/templates/.agents/skills/analyze-task/SKILL.zh-CN.md +19 -2
- package/templates/.agents/skills/analyze-task/config/verify.en.json +1 -12
- package/templates/.agents/skills/analyze-task/config/verify.zh-CN.json +1 -12
- package/templates/.agents/skills/close-codescan/SKILL.en.md +2 -2
- package/templates/.agents/skills/close-codescan/SKILL.zh-CN.md +2 -2
- package/templates/.agents/skills/close-dependabot/SKILL.en.md +2 -2
- package/templates/.agents/skills/close-dependabot/SKILL.zh-CN.md +2 -2
- package/templates/.agents/skills/code-task/SKILL.en.md +14 -1
- package/templates/.agents/skills/code-task/SKILL.zh-CN.md +14 -1
- package/templates/.agents/skills/code-task/config/verify.en.json +5 -14
- package/templates/.agents/skills/code-task/config/verify.zh-CN.json +5 -14
- package/templates/.agents/skills/code-task/reference/code-rules.en.md +5 -4
- package/templates/.agents/skills/code-task/reference/code-rules.zh-CN.md +5 -4
- package/templates/.agents/skills/code-task/reference/dual-mode.en.md +1 -1
- package/templates/.agents/skills/code-task/reference/dual-mode.zh-CN.md +1 -1
- package/templates/.agents/skills/code-task/reference/fix-mode.en.md +3 -1
- package/templates/.agents/skills/code-task/reference/fix-mode.zh-CN.md +3 -1
- package/templates/.agents/skills/code-task/reference/report-template.en.md +42 -15
- package/templates/.agents/skills/code-task/reference/report-template.zh-CN.md +39 -12
- package/templates/.agents/skills/complete-manual-validation/SKILL.en.md +5 -1
- package/templates/.agents/skills/complete-manual-validation/SKILL.zh-CN.md +5 -1
- package/templates/.agents/skills/complete-manual-validation/reference/report-template.en.md +2 -0
- package/templates/.agents/skills/complete-manual-validation/reference/report-template.zh-CN.md +2 -0
- package/templates/.agents/skills/complete-task/SKILL.en.md +9 -22
- package/templates/.agents/skills/complete-task/SKILL.zh-CN.md +9 -22
- package/templates/.agents/skills/create-task/SKILL.en.md +8 -0
- package/templates/.agents/skills/create-task/SKILL.zh-CN.md +7 -0
- package/templates/.agents/skills/import-codescan/SKILL.en.md +1 -1
- package/templates/.agents/skills/import-codescan/SKILL.zh-CN.md +1 -1
- package/templates/.agents/skills/import-dependabot/SKILL.en.md +1 -1
- package/templates/.agents/skills/import-dependabot/SKILL.zh-CN.md +1 -1
- package/templates/.agents/skills/init-labels/SKILL.en.md +8 -8
- package/templates/.agents/skills/init-labels/SKILL.zh-CN.md +8 -8
- package/templates/.agents/skills/init-milestones/SKILL.en.md +9 -11
- package/templates/.agents/skills/init-milestones/SKILL.zh-CN.md +9 -11
- package/templates/.agents/skills/plan-task/SKILL.en.md +14 -1
- package/templates/.agents/skills/plan-task/SKILL.zh-CN.md +16 -1
- package/templates/.agents/skills/plan-task/config/verify.en.json +4 -14
- package/templates/.agents/skills/plan-task/config/verify.zh-CN.json +4 -14
- package/templates/.agents/skills/review-analysis/SKILL.en.md +14 -1
- package/templates/.agents/skills/review-analysis/SKILL.zh-CN.md +14 -1
- package/templates/.agents/skills/review-analysis/config/verify.en.json +1 -18
- package/templates/.agents/skills/review-analysis/config/verify.zh-CN.json +1 -18
- package/templates/.agents/skills/review-analysis/reference/report-template.en.md +50 -23
- package/templates/.agents/skills/review-analysis/reference/report-template.zh-CN.md +45 -18
- package/templates/.agents/skills/review-code/SKILL.en.md +14 -1
- package/templates/.agents/skills/review-code/SKILL.zh-CN.md +14 -1
- package/templates/.agents/skills/review-code/config/verify.en.json +1 -23
- package/templates/.agents/skills/review-code/config/verify.zh-CN.json +1 -23
- package/templates/.agents/skills/review-code/reference/report-template.en.md +49 -22
- package/templates/.agents/skills/review-code/reference/report-template.zh-CN.md +45 -18
- package/templates/.agents/skills/review-plan/SKILL.en.md +14 -1
- package/templates/.agents/skills/review-plan/SKILL.zh-CN.md +14 -1
- package/templates/.agents/skills/review-plan/config/verify.en.json +1 -18
- package/templates/.agents/skills/review-plan/config/verify.zh-CN.json +1 -18
- package/templates/.agents/skills/review-plan/reference/report-template.en.md +49 -22
- package/templates/.agents/skills/review-plan/reference/report-template.zh-CN.md +45 -18
- package/templates/.agents/skills/review-pr/SKILL.en.md +5 -1
- package/templates/.agents/skills/review-pr/SKILL.zh-CN.md +5 -1
- package/templates/.agents/skills/review-pr/reference/report-template.en.md +4 -3
- package/templates/.agents/skills/review-pr/reference/report-template.zh-CN.md +4 -3
- package/templates/.agents/skills/run-manual-validation/SKILL.en.md +5 -1
- package/templates/.agents/skills/run-manual-validation/SKILL.zh-CN.md +5 -1
- package/templates/.agents/skills/run-manual-validation/reference/report-template.en.md +3 -1
- package/templates/.agents/skills/run-manual-validation/reference/report-template.zh-CN.md +3 -1
- package/templates/.agents/skills/test/SKILL.en.md +13 -2
- package/templates/.agents/skills/test/SKILL.zh-CN.md +13 -2
- package/templates/.agents/skills/update-agent-infra/scripts/sync-templates.js +14 -0
- package/templates/.agents/templates/task.en.md +6 -0
- package/templates/.agents/templates/task.zh-CN.md +6 -0
- package/templates/.agents/scripts/security-alerts.github.sh +0 -151
- package/templates/.agents/scripts/security-alerts.sh +0 -31
- package/templates/.agents/skills/init-labels/scripts/init-labels.github.sh +0 -146
- package/templates/.agents/skills/init-labels/scripts/init-labels.sh +0 -32
- package/templates/.agents/skills/init-milestones/scripts/init-milestones.github.sh +0 -294
- 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
|
-
...(
|
|
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
|
-
?
|
|
77
|
-
: `<details><summary>元数据 (frontmatter)</summary>\n\n
|
|
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
|
-
|
|
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:
|
|
102
|
-
let
|
|
103
|
-
while (
|
|
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
|
-
|
|
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
|
-
|
|
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,
|
|
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
|
|
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
|
-
|
|
177
|
-
if (
|
|
178
|
-
|
|
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
|
|
487
|
-
return value
|
|
488
|
-
.replaceAll('&', '&')
|
|
489
|
-
.replaceAll('<', '<')
|
|
490
|
-
.replaceAll('>', '>')
|
|
491
|
-
.replaceAll('"', '"')
|
|
492
|
-
.replaceAll("'", ''')
|
|
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>${
|
|
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>${
|
|
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' ? '通过' : '需复核'}。${
|
|
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
|
}
|