@fitlab-ai/agent-infra 0.11.7 → 0.11.9
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/README.md +1 -1
- package/README.zh-CN.md +1 -1
- package/dist/bin/internal-cli.js +8 -2
- package/dist/lib/agent-clients/adapters/codex.js +1 -1
- package/dist/lib/agent-clients/config.js +11 -3
- package/dist/lib/agent-clients/custom-tool-invocations.js +92 -0
- package/dist/lib/agent-clients/next-steps.js +1 -1
- package/dist/lib/agent-clients/reconcile.js +3 -3
- package/dist/lib/agent-clients/schema.js +18 -10
- package/dist/lib/defaults.json +6 -3
- package/dist/lib/init.js +2 -2
- package/dist/lib/internal/agent-client.js +3 -3
- package/dist/lib/internal/cli-route-inventory.js +3 -0
- package/dist/lib/internal/platform-pr-review.js +20 -3
- package/dist/lib/internal/task-operation-registry.js +3 -1
- package/dist/lib/internal/task-platform-recovery.js +56 -0
- package/dist/lib/merge.js +25 -5
- package/dist/lib/platform/completion-backfill.js +2 -1
- package/dist/lib/platform/in-label-sync.js +12 -1
- package/dist/lib/platform/issue-comments.js +58 -4
- package/dist/lib/platform/issues.js +105 -5
- package/dist/lib/platform/operation-coordinator.js +74 -0
- package/dist/lib/platform/pr-review.js +107 -1
- package/dist/lib/platform/pr-summary.js +79 -57
- package/dist/lib/platform/pull-requests.js +96 -4
- package/dist/lib/sandbox/commands/create.js +6 -3
- package/dist/lib/sandbox/commands/ls.js +4 -3
- package/dist/lib/sandbox/commands/rm.js +1 -1
- package/dist/lib/sandbox/config.js +22 -4
- package/dist/lib/sandbox/git-metadata.js +50 -0
- package/dist/lib/sandbox/index.js +1 -1
- package/dist/lib/sandbox/mounts.js +12 -0
- package/dist/lib/sandbox/recovery.js +12 -22
- package/dist/lib/sandbox/removal.js +58 -20
- package/dist/lib/sandbox/tools.js +31 -7
- package/dist/lib/task/activity-intent.js +4 -3
- package/dist/lib/task/create-service.js +5 -3
- package/dist/lib/task/create.js +22 -5
- package/dist/lib/task/finalization-artifacts.js +4 -1
- package/dist/lib/task/finalization.js +243 -113
- package/dist/lib/task/lifecycle.js +38 -2
- package/dist/lib/task/platform-operation-journal.js +152 -0
- package/dist/lib/task/platform-operation-recovery.js +459 -0
- package/dist/lib/task/verification.js +0 -1
- package/dist/lib/task/workflow-warning-intents.js +7 -1
- package/dist/lib/update.js +2 -2
- package/dist/lifecycle-build-manifest.json +3 -3
- package/lib/agent-clients/adapters/codex.ts +1 -1
- package/lib/agent-clients/config.ts +9 -3
- package/lib/agent-clients/custom-tool-invocations.ts +129 -0
- package/lib/agent-clients/next-steps.ts +3 -3
- package/lib/agent-clients/reconcile.ts +3 -3
- package/lib/agent-clients/schema.ts +18 -10
- package/lib/defaults.json +6 -3
- package/lib/init.ts +2 -2
- package/lib/internal/agent-client.ts +3 -3
- package/lib/internal/cli-route-inventory.ts +2 -0
- package/lib/internal/platform-pr-review.ts +14 -3
- package/lib/internal/task-operation-registry.ts +3 -1
- package/lib/internal/task-platform-recovery.ts +53 -0
- package/lib/merge.ts +26 -6
- package/lib/platform/completion-backfill.ts +2 -1
- package/lib/platform/in-label-sync.ts +12 -0
- package/lib/platform/issue-comments.ts +59 -3
- package/lib/platform/issues.ts +103 -5
- package/lib/platform/operation-coordinator.ts +96 -0
- package/lib/platform/pr-review.ts +113 -1
- package/lib/platform/pr-summary.ts +22 -1
- package/lib/platform/pull-requests.ts +105 -5
- package/lib/sandbox/commands/create.ts +7 -8
- package/lib/sandbox/commands/ls.ts +4 -3
- package/lib/sandbox/commands/rm.ts +1 -1
- package/lib/sandbox/config.ts +23 -5
- package/lib/sandbox/git-metadata.ts +66 -0
- package/lib/sandbox/index.ts +1 -1
- package/lib/sandbox/mounts.ts +14 -0
- package/lib/sandbox/recovery.ts +13 -25
- package/lib/sandbox/removal.ts +57 -19
- package/lib/sandbox/tool-types.ts +2 -1
- package/lib/sandbox/tools.ts +38 -7
- package/lib/task/activity-intent.ts +4 -3
- package/lib/task/create-service.ts +5 -3
- package/lib/task/create.ts +17 -5
- package/lib/task/finalization-artifacts.ts +4 -1
- package/lib/task/finalization.ts +242 -106
- package/lib/task/lifecycle.ts +35 -3
- package/lib/task/platform-operation-journal.ts +230 -0
- package/lib/task/platform-operation-recovery.ts +435 -0
- package/lib/task/verification.ts +1 -2
- package/lib/task/workflow-warning-intents.ts +9 -1
- package/lib/update.ts +2 -3
- package/package.json +1 -1
- package/templates/.agents/README.en.md +20 -66
- package/templates/.agents/README.zh-CN.md +19 -65
- package/templates/.agents/rules/README.en.md +1 -1
- package/templates/.agents/rules/README.zh-CN.md +1 -1
- package/templates/.agents/rules/next-step-output.en.md +1 -1
- package/templates/.agents/rules/next-step-output.zh-CN.md +1 -1
- package/templates/.agents/rules/task-management.en.md +12 -0
- package/templates/.agents/rules/task-management.zh-CN.md +12 -0
- package/templates/.agents/scripts/lib/agent-infra-package.js +0 -7
- package/templates/.agents/skills/cancel-task/SKILL.en.md +4 -2
- package/templates/.agents/skills/cancel-task/SKILL.zh-CN.md +4 -2
- package/templates/.agents/skills/code-task/SKILL.en.md +2 -2
- package/templates/.agents/skills/code-task/SKILL.zh-CN.md +2 -2
- package/templates/.agents/skills/commit/SKILL.en.md +1 -1
- package/templates/.agents/skills/commit/SKILL.zh-CN.md +1 -1
- package/templates/.agents/skills/complete-task/SKILL.en.md +2 -0
- package/templates/.agents/skills/complete-task/SKILL.zh-CN.md +2 -0
- package/templates/.agents/skills/create-pr/reference/pr-body-template.en.md +1 -1
- package/templates/.agents/skills/create-pr/reference/pr-body-template.zh-CN.md +1 -1
- package/templates/.agents/skills/plan-task/config/verify.en.json +1 -1
- package/templates/.agents/skills/plan-task/config/verify.zh-CN.json +1 -1
- package/templates/.agents/skills/review-code/SKILL.en.md +1 -1
- package/templates/.agents/skills/review-code/SKILL.zh-CN.md +1 -1
- package/templates/.agents/skills/review-code/reference/output-templates.en.md +15 -6
- package/templates/.agents/skills/review-code/reference/output-templates.zh-CN.md +15 -6
- package/templates/.agents/skills/review-pr/SKILL.en.md +11 -11
- package/templates/.agents/skills/review-pr/SKILL.zh-CN.md +11 -11
- package/templates/.agents/skills/review-pr/reference/output-guidance.en.md +3 -3
- package/templates/.agents/skills/review-pr/reference/output-guidance.zh-CN.md +3 -3
- package/templates/.agents/skills/review-pr/reference/report-template.en.md +4 -0
- package/templates/.agents/skills/review-pr/reference/report-template.zh-CN.md +4 -0
- package/templates/.agents/skills/update-agent-infra/SKILL.en.md +1 -1
- package/templates/.agents/skills/update-agent-infra/SKILL.zh-CN.md +1 -1
- package/templates/.agents/skills/update-agent-infra/scripts/sync-templates.js +21 -274
- package/dist/lib/agent-clients/custom-tuis.js +0 -99
- package/lib/agent-clients/custom-tuis.ts +0 -149
|
@@ -4,6 +4,7 @@ import { createHash } from 'node:crypto';
|
|
|
4
4
|
|
|
5
5
|
import { parseTaskFrontmatter } from '../task/frontmatter.ts';
|
|
6
6
|
import { resolveTaskRef } from '../task/resolve-ref.ts';
|
|
7
|
+
import { coordinatePlatformWrite } from './operation-coordinator.ts';
|
|
7
8
|
import { resolvePlatformProviderContext } from './context.ts';
|
|
8
9
|
import type { PlatformClient } from './context.ts';
|
|
9
10
|
import { platformResult } from './types.ts';
|
|
@@ -51,6 +52,9 @@ type SyncOptions = {
|
|
|
51
52
|
client?: PlatformClient;
|
|
52
53
|
runtimeVersion?: string;
|
|
53
54
|
summaryAuthorization?: { sha256: string };
|
|
55
|
+
dependency?: 'deferred' | 'required';
|
|
56
|
+
skipQueue?: boolean;
|
|
57
|
+
taskProjection?: { content: string; sha256: string };
|
|
54
58
|
};
|
|
55
59
|
|
|
56
60
|
function providerCommentId(id: string, provider: { identity?: { comment?: string } }): number | string {
|
|
@@ -74,6 +78,8 @@ const ARTIFACT_TITLES: Record<string, string> = {
|
|
|
74
78
|
'review-plan': '技术方案审查',
|
|
75
79
|
code: '实现报告',
|
|
76
80
|
'review-code': '代码审查',
|
|
81
|
+
'manual-validation': '人工验证',
|
|
82
|
+
'validation-run': '验证运行证据',
|
|
77
83
|
'pr-review': 'PR 审查报告'
|
|
78
84
|
};
|
|
79
85
|
|
|
@@ -150,7 +156,7 @@ function taskCommentLanguage(repoRoot: string): string {
|
|
|
150
156
|
|
|
151
157
|
function artifactIdentity(artifact: string): { stem: string; title: string } {
|
|
152
158
|
const stem = path.basename(artifact, '.md');
|
|
153
|
-
const match = stem.match(/^(analysis|review-analysis|plan|review-plan|code|review-code|pr-review)(?:-r(\d+))?$/);
|
|
159
|
+
const match = stem.match(/^(analysis|review-analysis|plan|review-plan|code|review-code|manual-validation|validation-run|pr-review)(?:-r(\d+))?$/);
|
|
154
160
|
if (!match) throw new Error(`unsupported artifact '${artifact}'`);
|
|
155
161
|
const base = ARTIFACT_TITLES[match[1]!]!;
|
|
156
162
|
const round = match[2] ? Number(match[2]) : 1;
|
|
@@ -529,7 +535,7 @@ function writeComment(
|
|
|
529
535
|
);
|
|
530
536
|
}
|
|
531
537
|
|
|
532
|
-
async function
|
|
538
|
+
async function syncPlatformCommentImpl(taskRef: string, options: SyncOptions): Promise<PlatformResult> {
|
|
533
539
|
const resolved = resolveTaskRef(taskRef, options.cwd ? { repoRoot: options.cwd } : {});
|
|
534
540
|
if (!resolved.ok) {
|
|
535
541
|
return platformResult('failed', {
|
|
@@ -547,7 +553,12 @@ async function syncPlatformComment(taskRef: string, options: SyncOptions): Promi
|
|
|
547
553
|
}
|
|
548
554
|
let desired: RenderedChunk[];
|
|
549
555
|
try {
|
|
550
|
-
|
|
556
|
+
const projectedContent = options.kind === 'task' && options.taskProjection ? options.taskProjection.content : taskContent;
|
|
557
|
+
if (options.kind === 'task' && options.taskProjection
|
|
558
|
+
&& createHash('sha256').update(projectedContent).digest('hex') !== options.taskProjection.sha256) {
|
|
559
|
+
throw Object.assign(new Error('finalization task projection digest does not match content'), { code: 'FINALIZATION_PROJECTION_INVALID' });
|
|
560
|
+
}
|
|
561
|
+
desired = expectedComments(resolved.taskId, projectedContent, resolved.taskDir, resolved.repoRoot, options);
|
|
551
562
|
} catch (error) {
|
|
552
563
|
return platformResult('failed', {
|
|
553
564
|
resource: { kind: 'issue', number: resourceIdentityNumber(issueIdentityFromTask) },
|
|
@@ -743,6 +754,50 @@ async function syncPlatformComment(taskRef: string, options: SyncOptions): Promi
|
|
|
743
754
|
return result;
|
|
744
755
|
}
|
|
745
756
|
|
|
757
|
+
async function syncPlatformComment(taskRef: string, options: SyncOptions): Promise<PlatformResult> {
|
|
758
|
+
const resolved = resolveTaskRef(taskRef, options.cwd ? { repoRoot: options.cwd } : {});
|
|
759
|
+
if (!resolved.ok) return syncPlatformCommentImpl(taskRef, options);
|
|
760
|
+
const taskContent = fs.readFileSync(resolved.taskMdPath, 'utf8');
|
|
761
|
+
if (!taskIssueIdentity(parseTaskFrontmatter(taskContent))) return syncPlatformCommentImpl(taskRef, options);
|
|
762
|
+
|
|
763
|
+
let operation: { kind: 'task-comment' | 'artifact-comment' | 'summary-comment' | 'cancel-comment'; target: string; expectedDigest: string };
|
|
764
|
+
try {
|
|
765
|
+
const sourceContent = options.kind === 'task' && options.taskProjection ? options.taskProjection.content : taskContent;
|
|
766
|
+
const desired = expectedComments(resolved.taskId, sourceContent, resolved.taskDir, resolved.repoRoot, options);
|
|
767
|
+
const content = desired.map((chunk) => chunk.content).join('\0');
|
|
768
|
+
const kind = options.kind === 'task' ? 'task-comment' : options.kind === 'summary' ? 'summary-comment'
|
|
769
|
+
: options.kind === 'cancel' ? 'cancel-comment' : 'artifact-comment';
|
|
770
|
+
const target = options.kind === 'artifact' ? options.artifact! : options.kind;
|
|
771
|
+
operation = { kind, target, expectedDigest: createHash('sha256').update(content).digest('hex') };
|
|
772
|
+
} catch {
|
|
773
|
+
return syncPlatformCommentImpl(taskRef, options);
|
|
774
|
+
}
|
|
775
|
+
|
|
776
|
+
const execute = () => syncPlatformCommentImpl(taskRef, options);
|
|
777
|
+
if (options.skipQueue) return execute();
|
|
778
|
+
return coordinatePlatformWrite({
|
|
779
|
+
operation: { taskRef: resolved.taskId, cwd: resolved.repoRoot, ...operation, dependency: options.dependency ?? 'deferred' },
|
|
780
|
+
agent: options.agent,
|
|
781
|
+
execute,
|
|
782
|
+
block: (error) => platformResult('blocked', { error }),
|
|
783
|
+
persistenceFailure: (error) => platformResult('failed', { error })
|
|
784
|
+
});
|
|
785
|
+
}
|
|
786
|
+
|
|
787
|
+
function inspectPlatformCommentOperation(taskRef: string, options: SyncOptions): { kind: 'task-comment' | 'artifact-comment' | 'summary-comment' | 'cancel-comment'; target: string; expectedDigest: string; id: string } | null {
|
|
788
|
+
const resolved = resolveTaskRef(taskRef, options.cwd ? { repoRoot: options.cwd } : {});
|
|
789
|
+
if (!resolved.ok) return null;
|
|
790
|
+
const taskContent = fs.readFileSync(resolved.taskMdPath, 'utf8');
|
|
791
|
+
if (!taskIssueIdentity(parseTaskFrontmatter(taskContent))) return null;
|
|
792
|
+
const sourceContent = options.kind === 'task' && options.taskProjection ? options.taskProjection.content : taskContent;
|
|
793
|
+
const desired = expectedComments(resolved.taskId, sourceContent, resolved.taskDir, resolved.repoRoot, options);
|
|
794
|
+
const kind = options.kind === 'task' ? 'task-comment' : options.kind === 'summary' ? 'summary-comment'
|
|
795
|
+
: options.kind === 'cancel' ? 'cancel-comment' : 'artifact-comment';
|
|
796
|
+
const target = options.kind === 'artifact' ? options.artifact! : options.kind;
|
|
797
|
+
const expectedDigest = createHash('sha256').update(desired.map((chunk) => chunk.content).join('\0')).digest('hex');
|
|
798
|
+
return { kind, target, expectedDigest, id: createHash('sha256').update(`${kind}\0${target}\0${expectedDigest}`).digest('hex') };
|
|
799
|
+
}
|
|
800
|
+
|
|
746
801
|
async function listPlatformComments(issue: string | number, cwd = process.cwd(), client?: PlatformClient): Promise<PlatformResult & { comments?: RemoteComment[] }> {
|
|
747
802
|
const loaded = await resolvePlatformProviderContext({ cwd, client });
|
|
748
803
|
const context = loaded.ok ? loaded.value.context : loaded.context;
|
|
@@ -800,6 +855,7 @@ export {
|
|
|
800
855
|
COMMENT_BYTE_LIMIT,
|
|
801
856
|
MARKERS,
|
|
802
857
|
chunkArtifactComment,
|
|
858
|
+
inspectPlatformCommentOperation,
|
|
803
859
|
findMarkerComments,
|
|
804
860
|
listRemoteComments,
|
|
805
861
|
checkPlatformCommentOwner,
|
package/lib/platform/issues.ts
CHANGED
|
@@ -2,6 +2,7 @@ import type { PlatformIssueSnapshot as IssueSnapshot } from './snapshots.ts';
|
|
|
2
2
|
import type { IssueFieldSchema } from './github-data.ts';
|
|
3
3
|
import fs from 'node:fs';
|
|
4
4
|
import path from 'node:path';
|
|
5
|
+
import { createHash } from 'node:crypto';
|
|
5
6
|
import { execFileSync } from 'node:child_process';
|
|
6
7
|
|
|
7
8
|
import { buildDefaultBody } from '../task/commands/issue-body.ts';
|
|
@@ -24,6 +25,7 @@ import { platformResult } from './types.ts';
|
|
|
24
25
|
import type { PlatformResult } from './types.ts';
|
|
25
26
|
import {
|
|
26
27
|
labelDelta,
|
|
28
|
+
inLabelMappingDigest,
|
|
27
29
|
planInLabelUpdate,
|
|
28
30
|
syncLabelDelta,
|
|
29
31
|
validateInLabelMapping,
|
|
@@ -39,9 +41,11 @@ import {
|
|
|
39
41
|
} from './provider-bridge.ts';
|
|
40
42
|
import { resourceIdentityEquals, resourceIdentityNumber, resourceIdentityString, serializeResourceIdentity } from './resource-identity.ts';
|
|
41
43
|
import { taskIssueIdentity, taskIssueIdentityError } from './task-identities.ts';
|
|
44
|
+
import { coordinatePlatformWrite } from './operation-coordinator.ts';
|
|
42
45
|
import type { ResourceIdentity } from './resource-identity.ts';
|
|
43
46
|
import type { IssueSnapshot as ProviderIssueSnapshot, RepositoryMetadataSnapshot } from './provider-contract.ts';
|
|
44
|
-
type IssueResult = PlatformResult & {
|
|
47
|
+
type IssueResult = Omit<PlatformResult, 'operations'> & {
|
|
48
|
+
operations: PlannedOperation[];
|
|
45
49
|
task: { id: string | null; issueNumber: number | null };
|
|
46
50
|
issue: IssueSnapshot | null;
|
|
47
51
|
};
|
|
@@ -61,6 +65,8 @@ type SyncOptions = SharedOptions & {
|
|
|
61
65
|
state?: 'open' | 'closed';
|
|
62
66
|
closeReason?: 'completed' | 'not_planned';
|
|
63
67
|
dryRun?: boolean;
|
|
68
|
+
dependency?: 'deferred' | 'required';
|
|
69
|
+
skipQueue?: boolean;
|
|
64
70
|
};
|
|
65
71
|
|
|
66
72
|
function result(
|
|
@@ -305,7 +311,7 @@ async function createPlatformIssue(taskRef: string, options: CreateOptions): Pro
|
|
|
305
311
|
platform: base.context.platform, capabilities: base.context.capabilities,
|
|
306
312
|
operations: [{ name: 'issue:create', status: 'planned', reasonCode: null }], error: null
|
|
307
313
|
});
|
|
308
|
-
{
|
|
314
|
+
const execute = async (): Promise<IssueResult> => {
|
|
309
315
|
const created = base.provider.issues?.create
|
|
310
316
|
? await base.provider.issues.create({
|
|
311
317
|
context: providerOperationContext(base.loadedContext),
|
|
@@ -344,7 +350,7 @@ async function createPlatformIssue(taskRef: string, options: CreateOptions): Pro
|
|
|
344
350
|
if (written.status === 'failed') return result('failed', base.resolved.taskId, null, {
|
|
345
351
|
platform: base.context.platform, capabilities: base.context.capabilities,
|
|
346
352
|
resource: { kind: 'issue', number: issueNumber, identity: inspected.value.identity || createdIdentity },
|
|
347
|
-
error: { code: 'ISSUE_CREATED_BIND_FAILED', message: written.error.message, retryable:
|
|
353
|
+
error: { code: 'ISSUE_CREATED_BIND_FAILED', message: written.error.message, retryable: true }
|
|
348
354
|
});
|
|
349
355
|
return result('applied', base.resolved.taskId, issueNumber, {
|
|
350
356
|
changed: true,
|
|
@@ -355,7 +361,26 @@ async function createPlatformIssue(taskRef: string, options: CreateOptions): Pro
|
|
|
355
361
|
issue: normalizeProviderIssue(inspected.value, repository, issueNumber),
|
|
356
362
|
error: null
|
|
357
363
|
});
|
|
358
|
-
}
|
|
364
|
+
};
|
|
365
|
+
const issueCreate = { title: payload.title, bodyDigest: createHash('sha256').update(payload.body).digest('hex') };
|
|
366
|
+
const operation = {
|
|
367
|
+
kind: 'issue-create' as const,
|
|
368
|
+
target: `task:${base.resolved.taskId}`,
|
|
369
|
+
expectedDigest: createHash('sha256').update(JSON.stringify({
|
|
370
|
+
taskId: base.resolved.taskId,
|
|
371
|
+
title: payload.title,
|
|
372
|
+
body: createHash('sha256').update(payload.body).digest('hex'),
|
|
373
|
+
labels: payload.labels || [], assignees: payload.assignees,
|
|
374
|
+
milestone: typeof payload.milestone === 'string' ? payload.milestone : null
|
|
375
|
+
})).digest('hex')
|
|
376
|
+
};
|
|
377
|
+
return coordinatePlatformWrite({
|
|
378
|
+
operation: { taskRef: base.resolved.taskId, cwd: base.resolved.repoRoot, ...operation, issueCreate, dependency: 'required' },
|
|
379
|
+
agent: options.agent,
|
|
380
|
+
execute,
|
|
381
|
+
block: (error) => result('blocked', base.resolved.taskId, null, { platform: base.context.platform, capabilities: base.context.capabilities, error }),
|
|
382
|
+
persistenceFailure: (error) => result('failed', base.resolved.taskId, null, { error })
|
|
383
|
+
});
|
|
359
384
|
}
|
|
360
385
|
|
|
361
386
|
async function bindPlatformIssue(taskRef: string, options: BindOptions): Promise<IssueResult> {
|
|
@@ -478,7 +503,7 @@ function desiredFieldValues(frontmatter: Record<string, string>, fields: IssueFi
|
|
|
478
503
|
return desired;
|
|
479
504
|
}
|
|
480
505
|
|
|
481
|
-
async function
|
|
506
|
+
async function syncPlatformIssueImpl(taskRef: string, options: SyncOptions): Promise<IssueResult> {
|
|
482
507
|
const base = await resolvedContext(taskRef, options);
|
|
483
508
|
if (!base.ok) return base.output;
|
|
484
509
|
if (!base.issueIdentity) return result('failed', base.resolved.taskId, null, { error: { code: 'ISSUE_NOT_LINKED', message: 'Task has no valid platform issue identity', retryable: false } });
|
|
@@ -617,5 +642,78 @@ async function syncPlatformIssue(taskRef: string, options: SyncOptions): Promise
|
|
|
617
642
|
});
|
|
618
643
|
}
|
|
619
644
|
|
|
645
|
+
async function syncPlatformIssue(taskRef: string, options: SyncOptions): Promise<IssueResult> {
|
|
646
|
+
const resolved = resolveTaskRef(taskRef, options.cwd ? { repoRoot: options.cwd } : {});
|
|
647
|
+
if (!resolved.ok || options.dryRun) return syncPlatformIssueImpl(taskRef, options);
|
|
648
|
+
let frontmatter: Record<string, string>;
|
|
649
|
+
let taskContent: string;
|
|
650
|
+
try {
|
|
651
|
+
taskContent = fs.readFileSync(resolved.taskMdPath, 'utf8');
|
|
652
|
+
frontmatter = parseTaskFrontmatter(taskContent);
|
|
653
|
+
}
|
|
654
|
+
catch { return syncPlatformIssueImpl(taskRef, options); }
|
|
655
|
+
const identity = taskIssueIdentity(frontmatter);
|
|
656
|
+
if (!identity || ![
|
|
657
|
+
options.requirements, options.issueType, options.fields, options.status !== undefined,
|
|
658
|
+
options.assignees !== undefined, options.milestone !== undefined,
|
|
659
|
+
options.inLabels !== undefined, options.state !== undefined
|
|
660
|
+
].some(Boolean)) return syncPlatformIssueImpl(taskRef, options);
|
|
661
|
+
let fromDiffFiles: string[] | undefined;
|
|
662
|
+
let mappingDigest: string | undefined;
|
|
663
|
+
if (options.inLabels === 'from-diff') {
|
|
664
|
+
const taskBase = typeof frontmatter.delivery_base_ref === 'string' ? frontmatter.delivery_base_ref.trim() : '';
|
|
665
|
+
if (taskBase && (!options.base || options.base === taskBase)) {
|
|
666
|
+
try {
|
|
667
|
+
fromDiffFiles = execFileSync('git', ['diff', `${taskBase}...HEAD`, '--name-only'], {
|
|
668
|
+
cwd: resolved.repoRoot, encoding: 'utf8'
|
|
669
|
+
}).trim().split(/\r?\n/).filter(Boolean).sort();
|
|
670
|
+
const config = JSON.parse(fs.readFileSync(path.join(resolved.repoRoot, '.agents', '.airc.json'), 'utf8')) as { labels?: { in?: unknown } };
|
|
671
|
+
const mapping = inLabelMappingDigest(config.labels?.in);
|
|
672
|
+
if (!mapping.ok) return result('failed', resolved.taskId, resourceIdentityNumber(identity), {
|
|
673
|
+
error: { code: mapping.error.code, message: mapping.error.message, retryable: false }
|
|
674
|
+
});
|
|
675
|
+
mappingDigest = mapping.digest;
|
|
676
|
+
} catch {
|
|
677
|
+
return result('failed', resolved.taskId, resourceIdentityNumber(identity), {
|
|
678
|
+
error: { code: 'IN_LABEL_SYNC_EVIDENCE_UNAVAILABLE', message: 'Unable to derive the current changed-file set for Issue metadata recovery', retryable: false }
|
|
679
|
+
});
|
|
680
|
+
}
|
|
681
|
+
}
|
|
682
|
+
}
|
|
683
|
+
const issueMetadata = {
|
|
684
|
+
requirements: options.requirements === true,
|
|
685
|
+
issueType: options.issueType === true,
|
|
686
|
+
fields: options.fields === true,
|
|
687
|
+
...(options.status !== undefined ? { status: options.status } : {}),
|
|
688
|
+
...(options.assignees !== undefined ? { assignees: options.assignees } : {}),
|
|
689
|
+
...(options.milestone !== undefined ? { milestone: options.milestone } : {}),
|
|
690
|
+
...(options.inLabels ? { inLabels: options.inLabels } : {}),
|
|
691
|
+
...(options.base ? { base: options.base } : {}),
|
|
692
|
+
...(fromDiffFiles ? { fromDiffFiles } : {}),
|
|
693
|
+
...(mappingDigest ? { inLabelMappingDigest: mappingDigest } : {}),
|
|
694
|
+
...(options.state ? { state: options.state } : {}),
|
|
695
|
+
...(options.closeReason ? { closeReason: options.closeReason } : {})
|
|
696
|
+
};
|
|
697
|
+
const operation = {
|
|
698
|
+
kind: 'issue-metadata' as const,
|
|
699
|
+
target: JSON.stringify(identity),
|
|
700
|
+
expectedDigest: createHash('sha256').update(JSON.stringify({
|
|
701
|
+
...issueMetadata,
|
|
702
|
+
task: resolved.taskId,
|
|
703
|
+
taskContent: createHash('sha256').update(taskContent).digest('hex')
|
|
704
|
+
})).digest('hex')
|
|
705
|
+
};
|
|
706
|
+
const dependency = options.dependency ?? (options.state === 'closed' && options.closeReason === 'not_planned' ? 'required' : 'deferred');
|
|
707
|
+
const execute = () => syncPlatformIssueImpl(taskRef, options);
|
|
708
|
+
if (options.skipQueue) return execute();
|
|
709
|
+
return coordinatePlatformWrite({
|
|
710
|
+
operation: { taskRef: resolved.taskId, cwd: resolved.repoRoot, ...operation, issueMetadata, dependency },
|
|
711
|
+
agent: options.agent,
|
|
712
|
+
execute,
|
|
713
|
+
block: (error) => result('blocked', resolved.taskId, resourceIdentityNumber(identity), { error }),
|
|
714
|
+
persistenceFailure: (error) => result('failed', resolved.taskId, resourceIdentityNumber(identity), { error })
|
|
715
|
+
});
|
|
716
|
+
}
|
|
717
|
+
|
|
620
718
|
export { bindPlatformIssue, createPlatformIssue, inspectPlatformIssue, requirementSectionAnchors, syncPlatformIssue };
|
|
621
719
|
export type { BindOptions, CreateOptions, IssueResult, SyncOptions };
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
import { recoverPlatformOperations } from '../task/platform-operation-recovery.ts';
|
|
2
|
+
import { recordPlatformOperation } from '../task/platform-operation-journal.ts';
|
|
3
|
+
import type { RecordOperationInput } from '../task/platform-operation-journal.ts';
|
|
4
|
+
import type { PlatformError, PlatformStatus } from './types.ts';
|
|
5
|
+
import type { PlatformClient } from './context.ts';
|
|
6
|
+
|
|
7
|
+
type OperationResult = Readonly<{
|
|
8
|
+
status: PlatformStatus;
|
|
9
|
+
error?: PlatformError | null;
|
|
10
|
+
warnings?: readonly { code: string }[];
|
|
11
|
+
}>;
|
|
12
|
+
|
|
13
|
+
type CoordinateInput<T extends OperationResult> = Readonly<{
|
|
14
|
+
operation: Omit<RecordOperationInput, 'state' | 'lastCode'>;
|
|
15
|
+
agent: string;
|
|
16
|
+
client?: PlatformClient;
|
|
17
|
+
drain?: (taskRef: string, excludeId: string, agent: string, cwd?: string) => Promise<{
|
|
18
|
+
status: 'applied' | 'no-op' | 'blocked' | 'failed';
|
|
19
|
+
error: PlatformError | null;
|
|
20
|
+
}>;
|
|
21
|
+
execute: () => Promise<T>;
|
|
22
|
+
block: (error: PlatformError) => T;
|
|
23
|
+
persistenceFailure: (error: PlatformError) => T;
|
|
24
|
+
}>;
|
|
25
|
+
|
|
26
|
+
async function coordinatePlatformWrite<T extends OperationResult>(input: CoordinateInput<T>): Promise<T> {
|
|
27
|
+
let operationId: string;
|
|
28
|
+
try {
|
|
29
|
+
const queued = recordPlatformOperation({ ...input.operation, state: 'queued' });
|
|
30
|
+
operationId = queued.id;
|
|
31
|
+
} catch (error) {
|
|
32
|
+
const value = error as { code?: string; message?: string };
|
|
33
|
+
return input.persistenceFailure({
|
|
34
|
+
code: value.code || 'PLATFORM_OPERATION_JOURNAL_WRITE_FAILED',
|
|
35
|
+
message: value.message || 'Unable to queue platform operation',
|
|
36
|
+
retryable: true
|
|
37
|
+
});
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
const selection = input.operation.kind === 'issue-metadata' && input.operation.issueMetadata?.state === 'closed'
|
|
41
|
+
? 'all' : input.operation.dependency;
|
|
42
|
+
const drained = input.drain
|
|
43
|
+
? await input.drain(input.operation.taskRef, operationId, input.agent, input.operation.cwd)
|
|
44
|
+
: await recoverPlatformOperations(input.operation.taskRef, selection, {
|
|
45
|
+
agent: input.agent,
|
|
46
|
+
client: input.client,
|
|
47
|
+
cwd: input.operation.cwd,
|
|
48
|
+
excludeId: operationId
|
|
49
|
+
});
|
|
50
|
+
if (drained.status !== 'applied' && drained.status !== 'no-op') {
|
|
51
|
+
return input.block(drained.error ?? {
|
|
52
|
+
code: 'PLATFORM_OPERATION_QUEUE_BLOCKED',
|
|
53
|
+
message: 'A previous platform operation is still unresolved; the current write remains queued',
|
|
54
|
+
retryable: true
|
|
55
|
+
});
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
try {
|
|
59
|
+
recordPlatformOperation({ ...input.operation, state: 'pending' });
|
|
60
|
+
} catch (error) {
|
|
61
|
+
const value = error as { code?: string; message?: string };
|
|
62
|
+
return input.persistenceFailure({
|
|
63
|
+
code: value.code || 'PLATFORM_OPERATION_JOURNAL_WRITE_FAILED',
|
|
64
|
+
message: value.message || 'Unable to mark platform operation as started',
|
|
65
|
+
retryable: true
|
|
66
|
+
});
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
let output: T;
|
|
70
|
+
try {
|
|
71
|
+
output = await input.execute();
|
|
72
|
+
} catch (error) {
|
|
73
|
+
const value = error as { code?: string; message?: string };
|
|
74
|
+
output = input.block({ code: value.code || 'PLATFORM_WRITE_FAILED', message: value.message || String(error), retryable: true });
|
|
75
|
+
}
|
|
76
|
+
const succeeded = (output.status === 'applied' || output.status === 'no-op')
|
|
77
|
+
&& !output.error && !(output.warnings?.length);
|
|
78
|
+
const state = succeeded ? 'succeeded' : output.status === 'failed' && output.error?.retryable === false ? 'failed' : 'unknown';
|
|
79
|
+
try {
|
|
80
|
+
recordPlatformOperation({
|
|
81
|
+
...input.operation,
|
|
82
|
+
state,
|
|
83
|
+
lastCode: output.error?.code ?? output.warnings?.[0]?.code ?? null
|
|
84
|
+
});
|
|
85
|
+
} catch (error) {
|
|
86
|
+
const value = error as { code?: string; message?: string };
|
|
87
|
+
return input.persistenceFailure({
|
|
88
|
+
code: value.code || 'PLATFORM_OPERATION_JOURNAL_WRITE_FAILED',
|
|
89
|
+
message: value.message || 'Unable to persist platform operation outcome',
|
|
90
|
+
retryable: true
|
|
91
|
+
});
|
|
92
|
+
}
|
|
93
|
+
return output;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
export { coordinatePlatformWrite };
|
|
@@ -1,3 +1,8 @@
|
|
|
1
|
+
import { createHash } from 'node:crypto';
|
|
2
|
+
import fs from 'node:fs';
|
|
3
|
+
import path from 'node:path';
|
|
4
|
+
|
|
5
|
+
import { coordinatePlatformWrite } from './operation-coordinator.ts';
|
|
1
6
|
import { resolvePlatformProviderContext } from './context.ts';
|
|
2
7
|
import type { PlatformClient } from './context.ts';
|
|
3
8
|
import { platformResult } from './types.ts';
|
|
@@ -9,8 +14,10 @@ import {
|
|
|
9
14
|
providerResourceToken,
|
|
10
15
|
unsupportedProviderOperation
|
|
11
16
|
} from './provider-bridge.ts';
|
|
12
|
-
import { isResourceIdentity, resourceIdentityNumber, reviewMarker as resourceReviewMarker } from './resource-identity.ts';
|
|
17
|
+
import { isResourceIdentity, resourceIdentityEquals, resourceIdentityNumber, reviewMarker as resourceReviewMarker } from './resource-identity.ts';
|
|
13
18
|
import type { ResourceIdentity } from './resource-identity.ts';
|
|
19
|
+
import type { PlatformPullRequestReviewIntent } from '../task/platform-operation-journal.ts';
|
|
20
|
+
import { resolveTaskRef } from '../task/resolve-ref.ts';
|
|
14
21
|
|
|
15
22
|
export type PrReviewEvent = 'COMMENT' | 'APPROVE' | 'REQUEST_CHANGES';
|
|
16
23
|
export type PrReviewIdentity = { scope: string; round: number; commitSha: string; resource?: ResourceIdentity };
|
|
@@ -38,6 +45,36 @@ function normalizeBody(body: string): string {
|
|
|
38
45
|
return String(body || '').replace(/\r\n/g, '\n').replace(/\n+$/, '\n');
|
|
39
46
|
}
|
|
40
47
|
|
|
48
|
+
function canonicalReviewBody(body: string): string {
|
|
49
|
+
return normalizeBody(body).replace(/\n$/u, '');
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function reviewArtifactName(round: number, artifactFile: string): boolean {
|
|
53
|
+
return path.basename(artifactFile) === artifactFile
|
|
54
|
+
&& (artifactFile === 'pr-review.md' && round === 1 || artifactFile === `pr-review-r${round}.md`);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export function readReviewBodyFile(taskRef: string, round: number, artifactFile: string, bodyDigest: string, cwd?: string): string {
|
|
58
|
+
if (!/^TASK-\d{8}-\d{6}$/u.test(taskRef) || path.basename(artifactFile) !== artifactFile
|
|
59
|
+
|| !reviewArtifactName(round, artifactFile)) {
|
|
60
|
+
throw Object.assign(new Error('Pull-request review artifact identity is invalid'), { code: 'PLATFORM_OPERATION_PAYLOAD_INVALID' });
|
|
61
|
+
}
|
|
62
|
+
const task = resolveTaskRef(taskRef, cwd ? { repoRoot: cwd } : {});
|
|
63
|
+
if (!task.ok) throw Object.assign(new Error(task.message), { code: 'PLATFORM_OPERATION_PAYLOAD_INVALID' });
|
|
64
|
+
const bodyFile = artifactFile.replace(/^pr-review/u, 'pr-review-body');
|
|
65
|
+
const report = fs.readFileSync(path.join(task.taskDir, artifactFile), 'utf8');
|
|
66
|
+
const bodyReferences = [...report.matchAll(/^\*\*(?:正文文件|Body file)\*\*(?::|:)\s*`([^`]+)`[^\r\n]*$/gmu)]
|
|
67
|
+
.map((match) => match[1]);
|
|
68
|
+
if (bodyReferences.length !== 1 || bodyReferences[0] !== bodyFile) {
|
|
69
|
+
throw Object.assign(new Error('Pull-request review artifact does not reference its canonical body file'), { code: 'PLATFORM_OPERATION_PAYLOAD_INVALID' });
|
|
70
|
+
}
|
|
71
|
+
const body = canonicalReviewBody(fs.readFileSync(path.join(task.taskDir, bodyFile), 'utf8'));
|
|
72
|
+
if (!body || createHash('sha256').update(body).digest('hex') !== bodyDigest) {
|
|
73
|
+
throw Object.assign(new Error('Canonical pull-request review body does not match its persisted digest'), { code: 'PLATFORM_OPERATION_PAYLOAD_INVALID' });
|
|
74
|
+
}
|
|
75
|
+
return body;
|
|
76
|
+
}
|
|
77
|
+
|
|
41
78
|
function firstLine(body: string): string {
|
|
42
79
|
return normalizeBody(String(body || '')).split('\n', 1)[0] || '';
|
|
43
80
|
}
|
|
@@ -90,6 +127,11 @@ export async function publishPrReview(options: {
|
|
|
90
127
|
cwd?: string;
|
|
91
128
|
client?: PlatformClient;
|
|
92
129
|
dryRun?: boolean;
|
|
130
|
+
agent?: string;
|
|
131
|
+
skipQueue?: boolean;
|
|
132
|
+
expectedResource?: ResourceIdentity;
|
|
133
|
+
expectedProviderScopeId?: string;
|
|
134
|
+
recoveryArtifact?: string;
|
|
93
135
|
prNumber: PrToken;
|
|
94
136
|
identity: PrReviewIdentity;
|
|
95
137
|
event: PrReviewEvent;
|
|
@@ -100,6 +142,13 @@ export async function publishPrReview(options: {
|
|
|
100
142
|
if (!hasUsableContext(context)) {
|
|
101
143
|
return platformResult(context.status, { platform: context.platform, capabilities: context.capabilities, error: context.error });
|
|
102
144
|
}
|
|
145
|
+
const providerScopeId = loaded.ok ? loaded.value.snapshot.scope.id : null;
|
|
146
|
+
if (options.expectedProviderScopeId && providerScopeId !== options.expectedProviderScopeId) {
|
|
147
|
+
return platformResult('failed', {
|
|
148
|
+
platform: context.platform, capabilities: context.capabilities,
|
|
149
|
+
error: { code: 'PLATFORM_OPERATION_IDENTITY_MISMATCH', message: 'Current provider scope differs from the persisted operation target', retryable: false }
|
|
150
|
+
});
|
|
151
|
+
}
|
|
103
152
|
const identity = loaded.ok ? (() => { try { return providerResourceToken(loaded.value.provider, 'pull-request', String(options.prNumber)); } catch { return null; } })() : null;
|
|
104
153
|
if (!identity) {
|
|
105
154
|
return platformResult('failed', {
|
|
@@ -108,6 +157,13 @@ export async function publishPrReview(options: {
|
|
|
108
157
|
});
|
|
109
158
|
}
|
|
110
159
|
const identityNumber = resourceIdentityNumber(identity);
|
|
160
|
+
if (options.expectedResource && !resourceIdentityEquals(identity, options.expectedResource)) {
|
|
161
|
+
return platformResult('failed', {
|
|
162
|
+
platform: context.platform, capabilities: context.capabilities,
|
|
163
|
+
resource: { kind: 'pull-request', number: identityNumber, identity },
|
|
164
|
+
error: { code: 'PLATFORM_OPERATION_IDENTITY_MISMATCH', message: 'Current pull request identity differs from the persisted operation target', retryable: false }
|
|
165
|
+
});
|
|
166
|
+
}
|
|
111
167
|
if (!REVIEW_EVENTS.includes(options.event)) {
|
|
112
168
|
return platformResult('failed', {
|
|
113
169
|
platform: context.platform, capabilities: context.capabilities,
|
|
@@ -123,6 +179,62 @@ export async function publishPrReview(options: {
|
|
|
123
179
|
});
|
|
124
180
|
}
|
|
125
181
|
|
|
182
|
+
if (/^TASK-\d{8}-\d{6}$/u.test(options.identity.scope) && !options.skipQueue && !options.dryRun) {
|
|
183
|
+
if (!options.agent) return platformResult('failed', {
|
|
184
|
+
platform: context.platform, capabilities: context.capabilities,
|
|
185
|
+
resource: { kind: 'pull-request', number: identityNumber, identity },
|
|
186
|
+
error: { code: 'PR_REVIEW_AGENT_REQUIRED', message: 'task-scoped review publication requires an agent token', retryable: false }
|
|
187
|
+
});
|
|
188
|
+
if (!options.recoveryArtifact || !reviewArtifactName(options.identity.round, options.recoveryArtifact)) return platformResult('failed', {
|
|
189
|
+
platform: context.platform, capabilities: context.capabilities,
|
|
190
|
+
resource: { kind: 'pull-request', number: identityNumber, identity },
|
|
191
|
+
error: { code: 'PR_REVIEW_ARTIFACT_REQUIRED', message: 'task-scoped review publication requires its canonical review artifact', retryable: false }
|
|
192
|
+
});
|
|
193
|
+
const body = canonicalReviewBody(options.body);
|
|
194
|
+
const bodyDigest = createHash('sha256').update(body).digest('hex');
|
|
195
|
+
try {
|
|
196
|
+
if (readReviewBodyFile(options.identity.scope, options.identity.round, options.recoveryArtifact, bodyDigest, options.cwd) !== body) throw new Error('body mismatch');
|
|
197
|
+
} catch {
|
|
198
|
+
return platformResult('failed', {
|
|
199
|
+
platform: context.platform, capabilities: context.capabilities,
|
|
200
|
+
resource: { kind: 'pull-request', number: identityNumber, identity },
|
|
201
|
+
error: { code: 'PR_REVIEW_ARTIFACT_MISMATCH', message: 'Review body must match the canonical review body file', retryable: false }
|
|
202
|
+
});
|
|
203
|
+
}
|
|
204
|
+
const pullRequestReview: PlatformPullRequestReviewIntent = {
|
|
205
|
+
prNumber: String(options.prNumber),
|
|
206
|
+
resource: identity,
|
|
207
|
+
providerScopeId: providerScopeId!,
|
|
208
|
+
scope: options.identity.scope,
|
|
209
|
+
round: options.identity.round,
|
|
210
|
+
commitSha: options.identity.commitSha,
|
|
211
|
+
event: options.event,
|
|
212
|
+
artifactFile: options.recoveryArtifact,
|
|
213
|
+
bodyDigest
|
|
214
|
+
};
|
|
215
|
+
return coordinatePlatformWrite({
|
|
216
|
+
operation: {
|
|
217
|
+
taskRef: options.identity.scope,
|
|
218
|
+
cwd: options.cwd || process.cwd(),
|
|
219
|
+
kind: 'pull-request-review',
|
|
220
|
+
target: JSON.stringify(identity),
|
|
221
|
+
expectedDigest: createHash('sha256').update(JSON.stringify(pullRequestReview)).digest('hex'),
|
|
222
|
+
dependency: 'deferred',
|
|
223
|
+
pullRequestReview
|
|
224
|
+
},
|
|
225
|
+
agent: options.agent,
|
|
226
|
+
execute: () => publishPrReview({ ...options, expectedProviderScopeId: providerScopeId!, skipQueue: true }),
|
|
227
|
+
block: (error) => platformResult('blocked', {
|
|
228
|
+
platform: context.platform, capabilities: context.capabilities,
|
|
229
|
+
resource: { kind: 'pull-request', number: identityNumber, identity }, error
|
|
230
|
+
}),
|
|
231
|
+
persistenceFailure: (error) => platformResult('failed', {
|
|
232
|
+
platform: context.platform, capabilities: context.capabilities,
|
|
233
|
+
resource: { kind: 'pull-request', number: identityNumber, identity }, error
|
|
234
|
+
})
|
|
235
|
+
});
|
|
236
|
+
}
|
|
237
|
+
|
|
126
238
|
const marker = reviewMarker({ ...options.identity, resource: identity });
|
|
127
239
|
const listed = await listPrReviews(options.prNumber, { cwd: options.cwd, client: options.client });
|
|
128
240
|
if (listed.status === 'failed' || listed.status === 'blocked') return listed;
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import fs from 'node:fs';
|
|
2
2
|
import path from 'node:path';
|
|
3
|
+
import { createHash } from 'node:crypto';
|
|
3
4
|
|
|
4
5
|
import { enumerateArtifacts } from '../task/artifacts.ts';
|
|
5
6
|
import { parseTypedTaskFrontmatter } from '../task/frontmatter.ts';
|
|
@@ -13,6 +14,7 @@ import { normalizeCommentContent } from './issue-comments.ts';
|
|
|
13
14
|
import { CONTROL_MARKER_PATTERN, sanitizeMarkdownDocument, splitDocumentPlaceholder } from './comment-safety.ts';
|
|
14
15
|
import { platformResult } from './types.ts';
|
|
15
16
|
import type { PlatformResult } from './types.ts';
|
|
17
|
+
import { coordinatePlatformWrite } from './operation-coordinator.ts';
|
|
16
18
|
import type { OperationWarning } from '../task/operation-outcome.ts';
|
|
17
19
|
import { providerError, providerOperationContext, providerStatus, providerResourceToken, resourceIdentityNumber, unsupportedProviderOperation } from './provider-bridge.ts';
|
|
18
20
|
import { resourceIdentityEquals } from './resource-identity.ts';
|
|
@@ -491,7 +493,7 @@ async function reportWrite(taskRef: string, options: ReportWriteOptions): Promis
|
|
|
491
493
|
|
|
492
494
|
async function syncPullRequestSummary(
|
|
493
495
|
taskRef: string,
|
|
494
|
-
options: { agent: string; body: string; changeReportFile?: string; cwd?: string; client?: PlatformClient; dryRun?: boolean; strict?: boolean; primaryResult: PullRequestPrimaryResult; runtimeVersion?: string; manualValidation?: ManualValidationSummaryOptions; lockAlreadyHeld?: boolean }
|
|
496
|
+
options: { agent: string; body: string; changeReportFile?: string; cwd?: string; client?: PlatformClient; dryRun?: boolean; strict?: boolean; primaryResult: PullRequestPrimaryResult; runtimeVersion?: string; manualValidation?: ManualValidationSummaryOptions; lockAlreadyHeld?: boolean; skipQueue?: boolean }
|
|
495
497
|
): Promise<PullRequestSummaryResult> {
|
|
496
498
|
const warningResult = warningResultForPrimary(options.primaryResult);
|
|
497
499
|
let knownPrNumber: number | null = null;
|
|
@@ -657,6 +659,7 @@ async function syncPullRequestSummary(
|
|
|
657
659
|
operations: [{ name: `summary:${reconciliation.action}`, status: 'planned', reasonCode: null }],
|
|
658
660
|
result: null, warnings: [], ...info
|
|
659
661
|
};
|
|
662
|
+
const publish = async (): Promise<PullRequestSummaryResult> => {
|
|
660
663
|
const existingComment = reconciliation.commentId === null
|
|
661
664
|
? undefined
|
|
662
665
|
: providerResourceToken(loaded.value.provider, 'comment', String(reconciliation.commentId));
|
|
@@ -722,6 +725,24 @@ async function syncPullRequestSummary(
|
|
|
722
725
|
operations: [{ name: `summary:${reconciliation.action}`, status: 'applied', reasonCode: null }],
|
|
723
726
|
result: null, warnings: [], ...info
|
|
724
727
|
};
|
|
728
|
+
};
|
|
729
|
+
const operation = {
|
|
730
|
+
taskRef: resolved.taskId,
|
|
731
|
+
cwd: resolved.repoRoot,
|
|
732
|
+
kind: 'pull-request-summary' as const,
|
|
733
|
+
target: JSON.stringify(prIdentity),
|
|
734
|
+
expectedDigest: createHash('sha256').update(desired).digest('hex'),
|
|
735
|
+
dependency: 'deferred' as const,
|
|
736
|
+
pullRequestSummary: { body: options.body, changeReportFile: options.changeReportFile! }
|
|
737
|
+
};
|
|
738
|
+
if (options.skipQueue) return publish();
|
|
739
|
+
return coordinatePlatformWrite({
|
|
740
|
+
operation,
|
|
741
|
+
agent: options.agent,
|
|
742
|
+
execute: publish,
|
|
743
|
+
block: (error) => fail('blocked', context, error, prNumber),
|
|
744
|
+
persistenceFailure: (error) => fail('failed', context, error, prNumber)
|
|
745
|
+
});
|
|
725
746
|
};
|
|
726
747
|
return await (options.lockAlreadyHeld
|
|
727
748
|
? execute()
|