@fitlab-ai/agent-infra 0.12.0 → 0.12.1
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/dist/lib/internal/manual-validation.js +11 -17
- package/dist/lib/internal/platform-comment.js +56 -7
- package/dist/lib/platform/issue-comments.js +373 -267
- package/dist/lib/platform/issue-summary-content.js +112 -0
- package/dist/lib/platform/pr-change-report.js +175 -26
- package/dist/lib/platform/pr-summary-content.js +54 -0
- package/dist/lib/platform/pr-summary.js +318 -284
- package/dist/lib/platform/summary-content-pipeline.js +18 -0
- package/dist/lib/sandbox/removal.js +59 -2
- package/dist/lib/task/finalization.js +16 -7
- package/dist/lib/task/platform-operation-recovery.js +87 -87
- package/dist/lifecycle-build-manifest.json +3 -3
- package/lib/internal/manual-validation.ts +12 -18
- package/lib/internal/platform-comment.ts +46 -4
- package/lib/platform/issue-comments.ts +478 -280
- package/lib/platform/issue-summary-content.ts +132 -0
- package/lib/platform/pr-change-report.ts +165 -20
- package/lib/platform/pr-summary-content.ts +60 -0
- package/lib/platform/pr-summary.ts +364 -257
- package/lib/platform/summary-content-pipeline.ts +23 -0
- package/lib/sandbox/removal.ts +57 -1
- package/lib/task/finalization.ts +11 -6
- package/lib/task/platform-operation-recovery.ts +70 -81
- package/package.json +1 -1
- package/templates/.agents/rules/pr-sync.en.md +4 -2
- package/templates/.agents/rules/pr-sync.zh-CN.md +6 -9
- package/templates/.agents/skills/cancel-task/SKILL.en.md +1 -1
- package/templates/.agents/skills/cancel-task/SKILL.zh-CN.md +1 -1
- package/templates/.agents/skills/commit/reference/pr-summary-sync.en.md +1 -1
- package/templates/.agents/skills/commit/reference/pr-summary-sync.zh-CN.md +1 -1
- package/templates/.agents/skills/complete-manual-validation/reference/summary-update.en.md +2 -2
- package/templates/.agents/skills/complete-manual-validation/reference/summary-update.zh-CN.md +2 -2
- package/templates/.agents/skills/complete-task/SKILL.en.md +10 -25
- package/templates/.agents/skills/complete-task/SKILL.zh-CN.md +11 -26
- package/templates/.agents/skills/create-pr/SKILL.en.md +1 -1
- package/templates/.agents/skills/create-pr/SKILL.zh-CN.md +1 -1
- package/templates/.agents/skills/create-pr/reference/change-report.en.md +9 -2
- package/templates/.agents/skills/create-pr/reference/change-report.zh-CN.md +9 -2
- package/templates/.agents/skills/create-pr/reference/comment-publish.en.md +2 -2
- package/templates/.agents/skills/create-pr/reference/comment-publish.zh-CN.md +2 -2
- package/templates/.agents/skills/create-task/SKILL.en.md +0 -18
- package/templates/.agents/skills/create-task/SKILL.zh-CN.md +0 -18
- package/templates/.agents/skills/watch-pr/SKILL.en.md +1 -1
- package/templates/.agents/skills/watch-pr/SKILL.zh-CN.md +1 -1
- package/templates/.agents/skills/watch-pr/reference/monitor-and-heal.en.md +3 -3
- package/templates/.agents/skills/watch-pr/reference/monitor-and-heal.zh-CN.md +3 -3
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
function parse(value, parser) {
|
|
2
|
+
try {
|
|
3
|
+
return parser(value);
|
|
4
|
+
}
|
|
5
|
+
catch (error) {
|
|
6
|
+
throw error instanceof Error ? error : new Error(String(error));
|
|
7
|
+
}
|
|
8
|
+
}
|
|
9
|
+
export function runSummaryContentPipeline(value, operations) {
|
|
10
|
+
try {
|
|
11
|
+
parse(value, operations.validate);
|
|
12
|
+
}
|
|
13
|
+
catch { /* Source correction gets a chance to repair invalid structure. */ }
|
|
14
|
+
const corrected = operations.correct(value);
|
|
15
|
+
const content = parse(corrected, operations.validate);
|
|
16
|
+
return { content, rendered: operations.render(content) };
|
|
17
|
+
}
|
|
18
|
+
//# sourceMappingURL=summary-content-pipeline.js.map
|
|
@@ -27,6 +27,40 @@ function isMissingPathError(error) {
|
|
|
27
27
|
return Boolean(error && typeof error === 'object' && 'code' in error
|
|
28
28
|
&& error.code === 'ENOENT');
|
|
29
29
|
}
|
|
30
|
+
function removalErrorCode(error) {
|
|
31
|
+
return error && typeof error === 'object' && 'code' in error
|
|
32
|
+
&& typeof error.code === 'string'
|
|
33
|
+
? error.code
|
|
34
|
+
: null;
|
|
35
|
+
}
|
|
36
|
+
function removalErrorMessage(error) {
|
|
37
|
+
const code = removalErrorCode(error);
|
|
38
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
39
|
+
return code ? `${code}: ${message}` : message;
|
|
40
|
+
}
|
|
41
|
+
function addOwnerWriteToDirectories(root) {
|
|
42
|
+
const pending = [root];
|
|
43
|
+
while (pending.length > 0) {
|
|
44
|
+
const current = pending.pop();
|
|
45
|
+
let stat;
|
|
46
|
+
try {
|
|
47
|
+
stat = fs.lstatSync(current);
|
|
48
|
+
}
|
|
49
|
+
catch (error) {
|
|
50
|
+
if (isMissingPathError(error))
|
|
51
|
+
continue;
|
|
52
|
+
throw error;
|
|
53
|
+
}
|
|
54
|
+
if (!stat.isDirectory())
|
|
55
|
+
continue;
|
|
56
|
+
fs.chmodSync(current, stat.mode | 0o200);
|
|
57
|
+
for (const entry of fs.readdirSync(current))
|
|
58
|
+
pending.push(path.join(current, entry));
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
function isPermissionRemovalError(error) {
|
|
62
|
+
return ['EACCES', 'EPERM', 'ENOTEMPTY'].includes(removalErrorCode(error) ?? '');
|
|
63
|
+
}
|
|
30
64
|
function canonicalPath(input) {
|
|
31
65
|
try {
|
|
32
66
|
return fs.realpathSync.native(input);
|
|
@@ -718,8 +752,28 @@ async function removeUncheckedSandbox(config, target, options) {
|
|
|
718
752
|
fs.rmSync(root, { recursive: true, force: true });
|
|
719
753
|
removeEmptyManagedParent(path.join(config.workspaceViewBase, config.project), root);
|
|
720
754
|
}
|
|
721
|
-
|
|
722
|
-
|
|
755
|
+
const toolRemovalFailures = [];
|
|
756
|
+
for (const candidate of toolCandidates.flatMap(({ candidates }) => candidates)) {
|
|
757
|
+
try {
|
|
758
|
+
fs.rmSync(candidate, { recursive: true, force: true });
|
|
759
|
+
continue;
|
|
760
|
+
}
|
|
761
|
+
catch (initialError) {
|
|
762
|
+
if (isPermissionRemovalError(initialError)) {
|
|
763
|
+
try {
|
|
764
|
+
addOwnerWriteToDirectories(candidate);
|
|
765
|
+
fs.rmSync(candidate, { recursive: true, force: true });
|
|
766
|
+
continue;
|
|
767
|
+
}
|
|
768
|
+
catch (retryError) {
|
|
769
|
+
toolRemovalFailures.push(`${candidate}: ${removalErrorMessage(initialError)}; retry failed: ${removalErrorMessage(retryError)}`);
|
|
770
|
+
}
|
|
771
|
+
}
|
|
772
|
+
else {
|
|
773
|
+
toolRemovalFailures.push(`${candidate}: ${removalErrorMessage(initialError)}`);
|
|
774
|
+
}
|
|
775
|
+
}
|
|
776
|
+
}
|
|
723
777
|
for (const shell of shellConfigDirCandidates(config, effectiveBranch))
|
|
724
778
|
fs.rmSync(shell, { recursive: true, force: true });
|
|
725
779
|
if (shouldRemoveShare)
|
|
@@ -736,6 +790,9 @@ async function removeUncheckedSandbox(config, target, options) {
|
|
|
736
790
|
assertBranchRemovalIdentity(config, effectiveBranch, worktreePermits);
|
|
737
791
|
runSafe('git', ['-C', config.repoRoot, 'branch', '-D', effectiveBranch]);
|
|
738
792
|
}
|
|
793
|
+
if (toolRemovalFailures.length > 0) {
|
|
794
|
+
throw new Error(`SANDBOX_TOOL_CLEANUP_INCOMPLETE:\n${toolRemovalFailures.map((failure) => ` - ${failure}`).join('\n')}`);
|
|
795
|
+
}
|
|
739
796
|
releaseTaskBoundSandboxIdentity(config.repoRoot, target.workspace);
|
|
740
797
|
if (!options.quiet)
|
|
741
798
|
p.outro('Sandbox removed');
|
|
@@ -17,6 +17,7 @@ import { projectFinalizationWarning } from './workflow-warning-intents.js';
|
|
|
17
17
|
import { mergeOperationWarnings } from './operation-outcome.js';
|
|
18
18
|
import { readSandboxControlManifest } from '../sandbox/control/lifecycle.js';
|
|
19
19
|
import { validateSandboxControlIdentity } from '../sandbox/control/identity-sentinel.js';
|
|
20
|
+
import { parseDeliverySummaryContent, renderDeliverySummaryContent } from '../platform/issue-summary-content.js';
|
|
20
21
|
const RECEIPT_VERSION = 4;
|
|
21
22
|
const FINALIZATION_STEPS = ['lifecycle', 'task-comment', 'verification', 'summary'];
|
|
22
23
|
export function resolveTaskFinalizationDirectory(repoRoot, taskId) {
|
|
@@ -569,17 +570,25 @@ function readDeliverySummary(taskDir, taskId) {
|
|
|
569
570
|
catch {
|
|
570
571
|
throw Object.assign(new Error('delivery summary staging record is unavailable'), { code: 'SUMMARY_STAGING_INVALID' });
|
|
571
572
|
}
|
|
572
|
-
if (value.taskId !== taskId ||
|
|
573
|
+
if (value.taskId !== taskId || !value.content || typeof value.contentSha256 !== 'string') {
|
|
573
574
|
throw Object.assign(new Error('delivery summary staging record is invalid'), { code: 'SUMMARY_STAGING_INVALID' });
|
|
574
575
|
}
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
576
|
+
let content;
|
|
577
|
+
try {
|
|
578
|
+
content = parseDeliverySummaryContent(value.content);
|
|
579
|
+
}
|
|
580
|
+
catch {
|
|
581
|
+
throw Object.assign(new Error('delivery summary staging payload is invalid'), { code: 'SUMMARY_STAGING_INVALID' });
|
|
582
|
+
}
|
|
583
|
+
if (content.taskId !== taskId || createHash('sha256').update(JSON.stringify(content)).digest('hex') !== value.contentSha256) {
|
|
584
|
+
throw Object.assign(new Error('delivery summary staging digest does not match payload'), { code: 'SUMMARY_STAGING_INVALID' });
|
|
578
585
|
}
|
|
586
|
+
const body = renderDeliverySummaryContent(content);
|
|
587
|
+
const canonical = canonicalizeSummaryBody(body);
|
|
588
|
+
if (!canonical.ok)
|
|
589
|
+
throw Object.assign(new Error('delivery summary staging body is not canonical'), { code: 'SUMMARY_STAGING_INVALID' });
|
|
579
590
|
const digest = createHash('sha256').update(canonical.value).digest('hex');
|
|
580
|
-
|
|
581
|
-
throw Object.assign(new Error('delivery summary staging digest does not match body'), { code: 'SUMMARY_STAGING_INVALID' });
|
|
582
|
-
return { body: canonical.value, sha256: value.sha256 };
|
|
591
|
+
return { body: canonical.value, sha256: digest };
|
|
583
592
|
}
|
|
584
593
|
async function syncPendingSummary(input) {
|
|
585
594
|
let { receipt } = input;
|
|
@@ -2,13 +2,15 @@ import fs from 'node:fs';
|
|
|
2
2
|
import path from 'node:path';
|
|
3
3
|
import { createHash } from 'node:crypto';
|
|
4
4
|
import { execFileSync } from 'node:child_process';
|
|
5
|
-
import { inspectPlatformCommentOperation,
|
|
5
|
+
import { inspectPlatformCommentOperation, syncPlatformComment } from '../platform/issue-comments.js';
|
|
6
|
+
import { parseDeliverySummaryContent, renderDeliverySummaryContent, parseCancelSummaryContent, renderCancelSummaryContent } from '../platform/issue-summary-content.js';
|
|
7
|
+
import { enumerateArtifacts } from './artifacts.js';
|
|
6
8
|
import { inspectPlatformIssue, syncPlatformIssue } from '../platform/issues.js';
|
|
7
9
|
import { resolvePlatformProviderContext } from '../platform/context.js';
|
|
8
10
|
import { providerOperationContext, providerError, unsupportedProviderOperation } from '../platform/provider-bridge.js';
|
|
9
11
|
import { inLabelMappingDigest } from '../platform/in-label-sync.js';
|
|
10
|
-
import { bindPlatformPullRequest,
|
|
11
|
-
import {
|
|
12
|
+
import { bindPlatformPullRequest, recoverCreatedPullRequest, syncPlatformPullRequest } from '../platform/pull-requests.js';
|
|
13
|
+
import { replayPullRequestSummary } from '../platform/pr-summary.js';
|
|
12
14
|
import { publishPrReview, readReviewBodyFile } from '../platform/pr-review.js';
|
|
13
15
|
import { parseTaskFrontmatter } from './frontmatter.js';
|
|
14
16
|
import { taskIssueIdentity } from '../platform/task-identities.js';
|
|
@@ -35,32 +37,70 @@ function summaryPayload(taskDir, taskId) {
|
|
|
35
37
|
if (!fs.existsSync(file))
|
|
36
38
|
return null;
|
|
37
39
|
const data = JSON.parse(fs.readFileSync(file, 'utf8'));
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
40
|
+
let content;
|
|
41
|
+
try {
|
|
42
|
+
content = parseDeliverySummaryContent(data.content);
|
|
43
|
+
}
|
|
44
|
+
catch {
|
|
45
|
+
content = null;
|
|
46
|
+
}
|
|
47
|
+
if (data.taskId !== taskId || !content || content.taskId !== taskId || typeof data.contentSha256 !== 'string'
|
|
48
|
+
|| createHash('sha256').update(JSON.stringify(content)).digest('hex') !== data.contentSha256) {
|
|
42
49
|
throw Object.assign(new Error('delivery summary staging record is invalid'), { code: 'SUMMARY_STAGING_INVALID' });
|
|
43
50
|
}
|
|
44
|
-
|
|
51
|
+
const canonical = canonicalizeSummaryBody(renderDeliverySummaryContent(content));
|
|
52
|
+
if (!canonical?.ok)
|
|
53
|
+
throw Object.assign(new Error('delivery summary staging body is not canonical'), { code: 'SUMMARY_STAGING_INVALID' });
|
|
54
|
+
return { body: canonical.value, sha256: createHash('sha256').update(canonical.value).digest('hex') };
|
|
55
|
+
}
|
|
56
|
+
function cancellationPayload(taskDir, taskId) {
|
|
57
|
+
const taskPath = path.join(taskDir, 'task.md');
|
|
58
|
+
if (!fs.existsSync(taskPath))
|
|
59
|
+
return null;
|
|
60
|
+
const taskContent = fs.readFileSync(taskPath, 'utf8');
|
|
61
|
+
const frontmatter = parseTaskFrontmatter(taskContent);
|
|
62
|
+
const title = /^#\s+(.+)$/mu.exec(taskContent)?.[1]?.trim() || taskId;
|
|
63
|
+
const artifacts = enumerateArtifacts(taskDir).filter((artifact) => artifact.name !== 'task.md').map((artifact) => artifact.name);
|
|
64
|
+
const content = parseCancelSummaryContent({
|
|
65
|
+
taskId,
|
|
66
|
+
title,
|
|
67
|
+
outcome: 'cancelled',
|
|
68
|
+
reason: frontmatter.cancel_reason,
|
|
69
|
+
cancelledAt: frontmatter.cancelled_at,
|
|
70
|
+
lastCompletedArtifact: artifacts.at(-1) ?? null
|
|
71
|
+
});
|
|
72
|
+
return renderCancelSummaryContent(content);
|
|
45
73
|
}
|
|
46
74
|
async function replayComment(taskId, taskDir, operation, agent, cwd, client) {
|
|
47
75
|
if (operation.kind === 'task-comment') {
|
|
48
|
-
return syncPlatformComment(taskId, { kind: 'task', agent, cwd, client, dependency: operation.dependency,
|
|
76
|
+
return syncPlatformComment(taskId, { kind: 'task', agent, cwd, client, dependency: operation.dependency, queuedOperation: operation });
|
|
49
77
|
}
|
|
50
78
|
if (operation.kind === 'artifact-comment') {
|
|
51
|
-
return syncPlatformComment(taskId, { kind: 'artifact', artifact: operation.target, agent, cwd, client, dependency: operation.dependency,
|
|
79
|
+
return syncPlatformComment(taskId, { kind: 'artifact', artifact: operation.target, agent, cwd, client, dependency: operation.dependency, queuedOperation: operation });
|
|
52
80
|
}
|
|
53
81
|
if (operation.kind === 'summary-comment') {
|
|
54
|
-
|
|
82
|
+
let summary;
|
|
83
|
+
try {
|
|
84
|
+
summary = summaryPayload(taskDir, taskId);
|
|
85
|
+
}
|
|
86
|
+
catch (error) {
|
|
87
|
+
return platformResult('failed', { error: { code: 'SUMMARY_STAGING_INVALID', message: error instanceof Error ? error.message : String(error), retryable: false } });
|
|
88
|
+
}
|
|
55
89
|
if (!summary)
|
|
56
|
-
return platformResult('
|
|
57
|
-
return syncPlatformComment(taskId, {
|
|
58
|
-
kind: 'summary', body: summary.body, agent, cwd,
|
|
59
|
-
summaryAuthorization: { sha256: summary.sha256 }, dependency: operation.dependency, client, skipQueue: true
|
|
60
|
-
});
|
|
90
|
+
return platformResult('blocked', { error: { code: 'SUMMARY_STAGING_MISSING', message: 'delivery summary staging record is unavailable', retryable: true } });
|
|
91
|
+
return syncPlatformComment(taskId, { kind: 'summary', body: summary.body, agent, cwd, client, summaryAuthorization: { sha256: summary.sha256 }, dependency: operation.dependency, queuedOperation: operation });
|
|
61
92
|
}
|
|
62
93
|
if (operation.kind === 'cancel-comment') {
|
|
63
|
-
|
|
94
|
+
let body;
|
|
95
|
+
try {
|
|
96
|
+
body = cancellationPayload(taskDir, taskId);
|
|
97
|
+
}
|
|
98
|
+
catch (error) {
|
|
99
|
+
return platformResult('failed', { error: { code: 'CANCEL_SUMMARY_INVALID', message: error instanceof Error ? error.message : String(error), retryable: false } });
|
|
100
|
+
}
|
|
101
|
+
if (!body)
|
|
102
|
+
return platformResult('blocked', { error: { code: 'CANCEL_SUMMARY_INVALID', message: 'Cancellation facts are unavailable for recovery', retryable: true } });
|
|
103
|
+
return syncPlatformComment(taskId, { kind: 'cancel', body, agent, cwd, client, dependency: operation.dependency, queuedOperation: operation });
|
|
64
104
|
}
|
|
65
105
|
return platformResult('failed', { error: { code: 'PLATFORM_OPERATION_UNSUPPORTED', message: `operation kind '${operation.kind}' has no registered recovery handler`, retryable: false } });
|
|
66
106
|
}
|
|
@@ -185,64 +225,6 @@ async function verifyIssueCreate(taskId, operation, cwd, client) {
|
|
|
185
225
|
} });
|
|
186
226
|
return platformResult('no-op');
|
|
187
227
|
}
|
|
188
|
-
async function replayPullRequestSummary(taskId, operation, agent, cwd, client) {
|
|
189
|
-
let identity;
|
|
190
|
-
try {
|
|
191
|
-
identity = parseResourceIdentity(JSON.parse(operation.target), 'queued pull-request summary identity');
|
|
192
|
-
}
|
|
193
|
-
catch {
|
|
194
|
-
return platformResult('failed', { error: { code: 'PLATFORM_OPERATION_PAYLOAD_INVALID', message: 'Queued pull-request summary identity is invalid', retryable: false } });
|
|
195
|
-
}
|
|
196
|
-
const summary = operation.pullRequestSummary;
|
|
197
|
-
if (!summary)
|
|
198
|
-
return platformResult('blocked', { error: {
|
|
199
|
-
code: 'PLATFORM_OPERATION_PAYLOAD_INVALID', message: 'Pull-request summary recovery content is missing', retryable: false
|
|
200
|
-
} });
|
|
201
|
-
const manualValidationHeading = /^###\s+(?:⏳\s+(?:Manual Validation Pending|人工验证待收尾)|✅\s+(?:Manual Validation Passed|人工验证已通过))\s*$/mu.test(summary.body);
|
|
202
|
-
if (manualValidationHeading && !summary.manualValidation)
|
|
203
|
-
return platformResult('blocked', { error: {
|
|
204
|
-
code: 'MANUAL_VALIDATION_TRANSACTION_REQUIRED', message: 'Queued manual-validation summary has no persisted transaction context', retryable: false
|
|
205
|
-
} });
|
|
206
|
-
const resolved = await resolvePlatformProviderContext({ cwd, client });
|
|
207
|
-
if (!resolved.ok)
|
|
208
|
-
return resolved.context;
|
|
209
|
-
const listed = resolved.value.provider.comments?.list
|
|
210
|
-
? await resolved.value.provider.comments.list({ context: providerOperationContext(resolved.value), parent: identity })
|
|
211
|
-
: unsupportedProviderOperation(resolved.value.provider, 'comments.list');
|
|
212
|
-
if (!listed.ok)
|
|
213
|
-
return platformResult(providerErrorRetryable(listed.error.code) ? 'blocked' : 'failed', { error: providerError(listed.error, 'PLATFORM_PROVIDER_OPERATION_FAILED') });
|
|
214
|
-
const marker = `<!-- sync-pr:${taskId}:summary -->`;
|
|
215
|
-
const summaries = listed.value.filter((comment) => normalizeCommentContent(comment.body).split('\n', 1)[0] === marker);
|
|
216
|
-
if (summaries.length > 1)
|
|
217
|
-
return platformResult('blocked', { error: {
|
|
218
|
-
code: 'PR_SUMMARY_MARKER_AMBIGUOUS', message: 'Multiple pull-request comments contain the queued summary marker', retryable: false
|
|
219
|
-
} });
|
|
220
|
-
if (!summary.manualValidation && summaries.length === 1 && createHash('sha256').update(normalizeCommentContent(summaries[0].body)).digest('hex') === operation.expectedDigest) {
|
|
221
|
-
return platformResult('no-op');
|
|
222
|
-
}
|
|
223
|
-
const bound = await inspectPlatformPullRequest(taskId, { cwd, client });
|
|
224
|
-
if (!bound.pullRequest || !resourceIdentityEquals(bound.pullRequest.identity, identity))
|
|
225
|
-
return platformResult('blocked', { error: bound.error ?? {
|
|
226
|
-
code: 'PLATFORM_OPERATION_IDENTITY_MISMATCH', message: 'Bound pull request differs from the queued summary target', retryable: false
|
|
227
|
-
} });
|
|
228
|
-
const replayed = await syncPullRequestSummary(taskId, {
|
|
229
|
-
agent,
|
|
230
|
-
cwd,
|
|
231
|
-
client,
|
|
232
|
-
body: summary.body,
|
|
233
|
-
changeReportFile: summary.changeReportFile,
|
|
234
|
-
...(summary.manualValidation ? { manualValidation: summary.manualValidation } : {}),
|
|
235
|
-
primaryResult: 'no_op',
|
|
236
|
-
strict: true,
|
|
237
|
-
skipQueue: true
|
|
238
|
-
});
|
|
239
|
-
if ((replayed.status === 'applied' || replayed.status === 'no-op') && !replayed.error && replayed.warnings.length === 0) {
|
|
240
|
-
return platformResult(replayed.status, { changed: replayed.changed });
|
|
241
|
-
}
|
|
242
|
-
return platformResult('blocked', { error: replayed.error ?? {
|
|
243
|
-
code: 'PR_SUMMARY_RECOVERY_UNCONFIRMED', message: 'Pull-request summary replay did not confirm the requested remote state', retryable: true
|
|
244
|
-
} });
|
|
245
|
-
}
|
|
246
228
|
async function replayPullRequestReview(operation, agent, cwd, client) {
|
|
247
229
|
const intent = operation.pullRequestReview;
|
|
248
230
|
if (!intent)
|
|
@@ -301,6 +283,14 @@ async function replayPullRequest(taskId, operation, agent, cwd) {
|
|
|
301
283
|
}
|
|
302
284
|
return recoverCreatedPullRequest(taskId, { agent, cwd, base: intent.baseRef || '', head: intent.headRef || '', skipQueue: true });
|
|
303
285
|
}
|
|
286
|
+
function isRecoveryEligible(operation, excludeId) {
|
|
287
|
+
if (operation.id === excludeId)
|
|
288
|
+
return false;
|
|
289
|
+
if (operation.state === 'queued' || operation.state === 'pending' || operation.state === 'unknown')
|
|
290
|
+
return true;
|
|
291
|
+
return operation.state === 'failed'
|
|
292
|
+
&& operation.lastCode !== 'PLATFORM_OPERATION_SUPERSEDED';
|
|
293
|
+
}
|
|
304
294
|
async function recoverPlatformOperationsOnce(taskRef, selection, options) {
|
|
305
295
|
const resolved = resolveTaskRef(taskRef, options.cwd ? { repoRoot: options.cwd } : {});
|
|
306
296
|
if (!resolved.ok)
|
|
@@ -313,9 +303,7 @@ async function recoverPlatformOperationsOnce(taskRef, selection, options) {
|
|
|
313
303
|
const value = error;
|
|
314
304
|
return result('failed', [], [], { code: value.code || 'PLATFORM_OPERATION_JOURNAL_INVALID', message: value.message || String(error), retryable: false });
|
|
315
305
|
}
|
|
316
|
-
const candidates = journal.operations.filter((operation) => operation
|
|
317
|
-
&& (operation.state === 'queued' || operation.state === 'pending' || operation.state === 'unknown'
|
|
318
|
-
|| (operation.state === 'failed' && operation.lastCode !== 'PLATFORM_OPERATION_SUPERSEDED')));
|
|
306
|
+
const candidates = journal.operations.filter((operation) => isRecoveryEligible(operation, options.excludeId));
|
|
319
307
|
if (!candidates.length)
|
|
320
308
|
return result('no-op', [], []);
|
|
321
309
|
const head = candidates[0];
|
|
@@ -382,9 +370,22 @@ async function recoverPlatformOperationsOnce(taskRef, selection, options) {
|
|
|
382
370
|
: operation.kind === 'artifact-comment'
|
|
383
371
|
? { kind: 'artifact', artifact: operation.target, agent: options.agent, cwd: resolved.repoRoot, dependency: operation.dependency }
|
|
384
372
|
: operation.kind === 'summary-comment' ? (() => {
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
373
|
+
try {
|
|
374
|
+
const summary = summaryPayload(resolved.taskDir, resolved.taskId);
|
|
375
|
+
return summary ? { kind: 'summary', body: summary.body, agent: options.agent, cwd: resolved.repoRoot, summaryAuthorization: { sha256: summary.sha256 }, dependency: operation.dependency } : null;
|
|
376
|
+
}
|
|
377
|
+
catch {
|
|
378
|
+
return null;
|
|
379
|
+
}
|
|
380
|
+
})() : (() => {
|
|
381
|
+
try {
|
|
382
|
+
const body = cancellationPayload(resolved.taskDir, resolved.taskId);
|
|
383
|
+
return body ? { kind: 'cancel', body, agent: options.agent, cwd: resolved.repoRoot, dependency: operation.dependency } : null;
|
|
384
|
+
}
|
|
385
|
+
catch {
|
|
386
|
+
return null;
|
|
387
|
+
}
|
|
388
|
+
})();
|
|
388
389
|
const current = commentOptions ? inspectPlatformCommentOperation(resolved.taskId, commentOptions) : null;
|
|
389
390
|
if (!current) {
|
|
390
391
|
pending.push(operation.id);
|
|
@@ -407,20 +408,21 @@ async function recoverPlatformOperationsOnce(taskRef, selection, options) {
|
|
|
407
408
|
remote = operation.kind === 'issue-create'
|
|
408
409
|
? await verifyIssueCreate(resolved.taskId, operation, resolved.repoRoot)
|
|
409
410
|
: operation.kind === 'pull-request-summary'
|
|
410
|
-
? await replayPullRequestSummary(resolved.taskId,
|
|
411
|
+
? await replayPullRequestSummary(resolved.taskId, targetOperation, { agent: options.agent, cwd: resolved.repoRoot, client: options.client })
|
|
411
412
|
: operation.kind === 'pull-request-review'
|
|
412
413
|
? await replayPullRequestReview(operation, options.agent, resolved.repoRoot, options.client)
|
|
413
414
|
: operation.kind === 'issue-metadata'
|
|
414
415
|
? await replayIssueMetadata(resolved.taskId, targetOperation, options.agent, resolved.repoRoot)
|
|
415
416
|
: operation.kind === 'pull-request'
|
|
416
417
|
? await replayPullRequest(resolved.taskId, targetOperation, options.agent, resolved.repoRoot)
|
|
417
|
-
: await replayComment(resolved.taskId, resolved.taskDir,
|
|
418
|
+
: await replayComment(resolved.taskId, resolved.taskDir, targetOperation, options.agent, resolved.repoRoot, options.client);
|
|
418
419
|
}
|
|
419
420
|
catch (error) {
|
|
420
421
|
const value = error;
|
|
421
422
|
remote = platformResult('blocked', { error: { code: value.code || 'PLATFORM_OPERATION_RECOVERY_FAILED', message: value.message || String(error), retryable: true } });
|
|
422
423
|
}
|
|
423
424
|
const succeeded = (remote.status === 'applied' || remote.status === 'no-op') && !remote.error;
|
|
425
|
+
const latestOperation = readPlatformOperationJournal(resolved.taskId, resolved.repoRoot).operations.find((item) => item.id === operation.id);
|
|
424
426
|
try {
|
|
425
427
|
recordPlatformOperation({
|
|
426
428
|
taskRef: resolved.taskId,
|
|
@@ -435,7 +437,7 @@ async function recoverPlatformOperationsOnce(taskRef, selection, options) {
|
|
|
435
437
|
...(operation.pullRequestReview ? { pullRequestReview: operation.pullRequestReview } : {}),
|
|
436
438
|
dependency: operation.dependency,
|
|
437
439
|
state: succeeded ? 'succeeded' : 'unknown',
|
|
438
|
-
lastCode: remote.error?.code ?? null
|
|
440
|
+
lastCode: remote.error?.code ?? latestOperation?.lastCode ?? null
|
|
439
441
|
});
|
|
440
442
|
}
|
|
441
443
|
catch (error) {
|
|
@@ -464,9 +466,7 @@ async function recoverPlatformOperations(taskRef, selection, options) {
|
|
|
464
466
|
const value = error;
|
|
465
467
|
return result('failed', [], [], { code: value.code || 'PLATFORM_OPERATION_JOURNAL_INVALID', message: value.message || String(error), retryable: false });
|
|
466
468
|
}
|
|
467
|
-
const eligible = (operation) => operation
|
|
468
|
-
&& (operation.state === 'queued' || operation.state === 'pending' || operation.state === 'unknown'
|
|
469
|
-
|| (operation.state === 'failed' && operation.lastCode !== 'PLATFORM_OPERATION_SUPERSEDED'));
|
|
469
|
+
const eligible = (operation) => isRecoveryEligible(operation, options.excludeId);
|
|
470
470
|
const first = initial.operations.filter(eligible);
|
|
471
471
|
if (!first.length)
|
|
472
472
|
return result('no-op', [], []);
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"protocolVersion": 3,
|
|
3
|
-
"packageVersion": "0.12.
|
|
4
|
-
"sourceInputHash": "
|
|
5
|
-
"internalExecutableBuildHash": "
|
|
3
|
+
"packageVersion": "0.12.1",
|
|
4
|
+
"sourceInputHash": "b998fe9d348d3f7f55e1258d2d09793f09240d1d70173d6bce49a9ce6c89429e",
|
|
5
|
+
"internalExecutableBuildHash": "d75b6e06c5ed88b0cd03663d98e2001c3f5c661d9637b8e6b8993580f69a3f16"
|
|
6
6
|
}
|
|
@@ -3,9 +3,10 @@ import path from 'node:path';
|
|
|
3
3
|
|
|
4
4
|
import { locateActivityLog, pairEntries } from '../task/activity-log.ts';
|
|
5
5
|
import { resolveTaskRef } from '../task/resolve-ref.ts';
|
|
6
|
+
import { prepareAndRenderPrSummaryContent } from '../platform/pr-summary.ts';
|
|
6
7
|
import { applyTaskEvent } from '../task/events.ts';
|
|
7
8
|
import { normalizeAgentToken } from '../agent-clients/tokens.ts';
|
|
8
|
-
import {
|
|
9
|
+
import { createManualValidationReceipt, manualValidationFinalSummaryDigest, manualValidationFinalSummaryProjectionMatches, readManualValidationReceipt, writeManualValidationReceiptAtomic } from '../task/manual-validation-receipt.ts';
|
|
9
10
|
import { archiveManualValidationGeneration, createManualValidationTransaction, readManualValidationTransaction, retryManualValidationTransaction, summaryPreimageDigest, transitionManualValidationTransaction, validateManualValidationGenerationArchive, writeManualValidationTransactionAtomic } from '../task/manual-validation-transaction.ts';
|
|
10
11
|
import type { ManualValidationTransaction } from '../task/manual-validation-transaction.ts';
|
|
11
12
|
import type { ManualValidationReceipt } from '../task/manual-validation-receipt.ts';
|
|
@@ -42,23 +43,15 @@ function printFailure(format: OutputFormat, error: { code: string; message: stri
|
|
|
42
43
|
process.exitCode = 1;
|
|
43
44
|
}
|
|
44
45
|
|
|
45
|
-
const MANUAL_VALIDATION_STATUS_HEADING = /^###\s+(?:⚠️\s+(?:需人工校验|Manual Validation Required)|✅\s+(?:人工验证已通过|无需人工校验|Manual Validation Passed|No Manual Validation Required)|⏳\s+(?:人工验证待收尾|Manual Validation Pending))\s*$/mu;
|
|
46
|
-
|
|
47
46
|
function manualSummaryBody(body: string, phase: 'pending' | 'final'): string {
|
|
48
|
-
const
|
|
49
|
-
const
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
return `${body.replace(MANUAL_VALIDATION_STATUS_HEADING, `${heading}${marker}`).replace(/\s+$/u, '')}\n`;
|
|
57
|
-
}
|
|
58
|
-
const fallback = phase === 'pending'
|
|
59
|
-
? chinese ? '人工验证正在等待最终完成。' : 'Manual validation is awaiting final promotion.'
|
|
60
|
-
: chinese ? '人工验证已通过。' : 'Manual validation passed.';
|
|
61
|
-
return `${body.replace(/\s+$/u, '')}\n\n${heading}${marker}\n\n${fallback}\n`;
|
|
47
|
+
const content = JSON.parse(body) as Record<string, unknown>;
|
|
48
|
+
const manual = content.manualValidation as Record<string, unknown>;
|
|
49
|
+
manual.state = phase === 'pending' ? 'pending' : 'passed';
|
|
50
|
+
return JSON.stringify(content);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function renderedManualSummaryBody(body: string, phase: 'pending' | 'final', taskContent: string): string {
|
|
54
|
+
return prepareAndRenderPrSummaryContent(JSON.parse(manualSummaryBody(body, phase)), taskContent).rendered;
|
|
62
55
|
}
|
|
63
56
|
|
|
64
57
|
function openManualValidationStarted(taskMdPath: string): { present: boolean; transactionId: string | null } {
|
|
@@ -112,6 +105,7 @@ async function executeManualValidationTransactionAtResolvedTask(
|
|
|
112
105
|
const preimage = { commentId: currentState.comment?.id ?? null, body: preimageBody, digest: summaryPreimageDigest(preimageBody) };
|
|
113
106
|
const summaryPath = path.resolve(cwd, values.summaryFile!);
|
|
114
107
|
const finalBodyWithoutReceipt = fs.readFileSync(summaryPath, 'utf8');
|
|
108
|
+
const taskContent = fs.readFileSync(resolved.taskMdPath, 'utf8');
|
|
115
109
|
const evidenceDigest = sha256File(summaryPath);
|
|
116
110
|
const archiveGeneration = options.archiveGeneration ?? archiveManualValidationGeneration;
|
|
117
111
|
let transactionResult = readManualValidationTransaction(resolved.taskDir, { taskId: resolved.taskId, prNumber: pullRequest.number, prHeadSha: pullRequest.head.sha, evidenceDigest, artifact: values.artifact! });
|
|
@@ -146,7 +140,7 @@ async function executeManualValidationTransactionAtResolvedTask(
|
|
|
146
140
|
evidenceDigest,
|
|
147
141
|
summaryPreimage: preimage,
|
|
148
142
|
pendingSummaryDigest: summaryPreimageDigest(pendingBody),
|
|
149
|
-
finalSummaryDigest: manualValidationFinalSummaryDigest(
|
|
143
|
+
finalSummaryDigest: manualValidationFinalSummaryDigest(renderedManualSummaryBody(finalBodyWithoutReceipt, 'final', taskContent)),
|
|
150
144
|
artifact: values.artifact!,
|
|
151
145
|
attempt: 1,
|
|
152
146
|
createdAt: new Date().toISOString(),
|
|
@@ -17,6 +17,9 @@ import { recoverTaskFromComments } from '../platform/task-recovery.ts';
|
|
|
17
17
|
import { backfillCompletionComments } from '../platform/completion-backfill.ts';
|
|
18
18
|
import { ensureInternalHandlerRoute, internalHandlerRoute } from './cli-route-inventory.ts';
|
|
19
19
|
import { resolveTaskRef } from '../task/resolve-ref.ts';
|
|
20
|
+
import { enumerateArtifacts } from '../task/artifacts.ts';
|
|
21
|
+
import { parseTaskFrontmatter } from '../task/frontmatter.ts';
|
|
22
|
+
import { prepareAndRenderCancelSummaryContent, prepareAndRenderDeliverySummaryContent } from '../platform/issue-summary-content.ts';
|
|
20
23
|
|
|
21
24
|
const USAGE = `Usage: agent-infra-internal platform-comment list --issue <token> [--cwd <path>]
|
|
22
25
|
agent-infra-internal platform-comment recover --issue <token> --task-id <TASK-id> --output <staging-task.md> [--cwd <path>]
|
|
@@ -158,13 +161,26 @@ async function platformComment(args: string[] = []): Promise<void> {
|
|
|
158
161
|
let body: string;
|
|
159
162
|
try { body = readBodyFile(parsed.values.bodyFile, cwd); }
|
|
160
163
|
catch (error) { fail(`unable to read body file: ${error instanceof Error ? error.message : String(error)}`); return; }
|
|
161
|
-
const canonical = canonicalizeSummaryBody(body);
|
|
162
|
-
if (!canonical.ok) { fail(`unable to stage summary: ${canonical.error.code}: ${canonical.error.message}`); return; }
|
|
163
|
-
body = canonical.value;
|
|
164
164
|
const resolved = resolveTaskRef(taskRef!, { repoRoot: cwd });
|
|
165
165
|
if (!resolved.ok || resolved.state !== 'active') { fail('stage-summary requires an active task'); return; }
|
|
166
166
|
const target = path.join(resolved.taskDir, '.delivery-summary.json');
|
|
167
|
-
|
|
167
|
+
let content;
|
|
168
|
+
try {
|
|
169
|
+
const supplied = JSON.parse(body) as Record<string, unknown>;
|
|
170
|
+
const taskContent = fs.readFileSync(resolved.taskMdPath, 'utf8');
|
|
171
|
+
const title = /^#\s+(.+)$/mu.exec(taskContent)?.[1]?.trim();
|
|
172
|
+
const prepared = prepareAndRenderDeliverySummaryContent(supplied, {
|
|
173
|
+
taskId: resolved.taskId,
|
|
174
|
+
title: title || resolved.taskId,
|
|
175
|
+
artifacts: enumerateArtifacts(resolved.taskDir).filter((artifact) => artifact.name !== 'task.md').map((artifact) => artifact.name),
|
|
176
|
+
verificationFallback: { state: 'not-run', detail: 'Final verification has not run at summary staging.' }
|
|
177
|
+
});
|
|
178
|
+
content = prepared.content;
|
|
179
|
+
const canonical = canonicalizeSummaryBody(prepared.rendered);
|
|
180
|
+
if (!canonical.ok) throw new Error(`${canonical.error.code}: ${canonical.error.message}`);
|
|
181
|
+
body = canonical.value;
|
|
182
|
+
} catch (error) { fail(`unable to stage summary: ${error instanceof Error ? error.message : String(error)}`); return; }
|
|
183
|
+
const value = { taskId: resolved.taskId, content, contentSha256: createHash('sha256').update(JSON.stringify(content)).digest('hex') };
|
|
168
184
|
const previous = fs.existsSync(target) ? fs.readFileSync(target, 'utf8') : null;
|
|
169
185
|
const next = `${JSON.stringify(value)}\n`;
|
|
170
186
|
if (previous === next) {
|
|
@@ -199,6 +215,32 @@ async function platformComment(args: string[] = []): Promise<void> {
|
|
|
199
215
|
fail(`unable to read body file: ${error instanceof Error ? error.message : String(error)}`);
|
|
200
216
|
return;
|
|
201
217
|
}
|
|
218
|
+
if (kind === 'summary' || kind === 'cancel') {
|
|
219
|
+
try {
|
|
220
|
+
const input = JSON.parse(body!) as Record<string, unknown>;
|
|
221
|
+
const resolved = resolveTaskRef(taskRef!, { repoRoot: cwd });
|
|
222
|
+
if (!resolved.ok) throw new Error(resolved.message);
|
|
223
|
+
const taskContent = fs.readFileSync(resolved.taskMdPath, 'utf8');
|
|
224
|
+
const taskFrontmatter = parseTaskFrontmatter(taskContent);
|
|
225
|
+
const title = /^#\s+(.+)$/mu.exec(taskContent)?.[1]?.trim();
|
|
226
|
+
const artifacts = enumerateArtifacts(resolved.taskDir).filter((artifact) => artifact.name !== 'task.md').map((artifact) => artifact.name);
|
|
227
|
+
const rendered = kind === 'summary'
|
|
228
|
+
? prepareAndRenderDeliverySummaryContent(input, {
|
|
229
|
+
taskId: resolved.taskId,
|
|
230
|
+
title: title || resolved.taskId,
|
|
231
|
+
artifacts,
|
|
232
|
+
...(resolved.state === 'active' ? { verificationFallback: { state: 'not-run', detail: 'Final verification has not run at summary staging.' } } : {})
|
|
233
|
+
}).rendered
|
|
234
|
+
: prepareAndRenderCancelSummaryContent(input, {
|
|
235
|
+
taskId: resolved.taskId,
|
|
236
|
+
title: title || resolved.taskId,
|
|
237
|
+
reason: typeof taskFrontmatter.cancel_reason === 'string' ? taskFrontmatter.cancel_reason : '',
|
|
238
|
+
cancelledAt: typeof taskFrontmatter.cancelled_at === 'string' ? taskFrontmatter.cancelled_at : '',
|
|
239
|
+
lastCompletedArtifact: artifacts.at(-1) ?? null
|
|
240
|
+
}).rendered;
|
|
241
|
+
body = rendered;
|
|
242
|
+
} catch (error) { fail(`invalid ${kind} summary structure: ${error instanceof Error ? error.message : String(error)}`); return; }
|
|
243
|
+
}
|
|
202
244
|
finish(await syncPlatformComment(taskRef!, {
|
|
203
245
|
kind: kind as CommentKind,
|
|
204
246
|
agent: normalizedAgent,
|