@fitlab-ai/agent-infra 0.11.6 → 0.11.8
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/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/defaults.json +6 -3
- package/dist/lib/git/primary-worktree.js +58 -0
- 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 +125 -7
- 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/control/server.js +6 -2
- 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 +11 -0
- package/dist/lib/sandbox/removal.js +58 -20
- package/dist/lib/sandbox/tools.js +29 -7
- package/dist/lib/server/config.js +7 -27
- package/dist/lib/server/daemon.js +4 -3
- package/dist/lib/server/process-control.js +4 -2
- package/dist/lib/server/runner.js +4 -3
- package/dist/lib/task/activity-intent.js +4 -3
- package/dist/lib/task/artifact-lifecycle.js +42 -10
- package/dist/lib/task/artifact-receipts.js +62 -22
- package/dist/lib/task/capabilities.js +22 -4
- package/dist/lib/task/completion-facts.js +56 -0
- package/dist/lib/task/create-service.js +11 -5
- package/dist/lib/task/create.js +60 -79
- package/dist/lib/task/events.js +268 -195
- package/dist/lib/task/finalization-artifacts.js +4 -1
- package/dist/lib/task/finalization-handoff.js +2 -1
- package/dist/lib/task/finalization.js +157 -24
- package/dist/lib/task/invalidation-command.js +4 -2
- package/dist/lib/task/invalidation.js +38 -9
- package/dist/lib/task/lifecycle.js +38 -2
- package/dist/lib/task/local-artifact-finalization.js +17 -3
- package/dist/lib/task/platform-operation-journal.js +152 -0
- package/dist/lib/task/platform-operation-recovery.js +459 -0
- package/dist/lib/task/qualification-audit.js +23 -105
- package/dist/lib/task/review-finalization.js +7 -0
- package/dist/lib/task/review-identity.js +69 -0
- package/dist/lib/task/sections.js +6 -5
- package/dist/lib/task/task-execution-lock.js +6 -3
- package/dist/lib/task/verification-engine.js +14 -63
- package/dist/lib/task/verification.js +35 -38
- package/dist/lib/task/workflow-warning-intents.js +7 -1
- 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/defaults.json +6 -3
- package/lib/git/primary-worktree.ts +62 -0
- 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 +132 -8
- 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/control/server.ts +5 -2
- 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 -0
- package/lib/sandbox/removal.ts +57 -19
- package/lib/sandbox/tool-types.ts +1 -1
- package/lib/sandbox/tools.ts +36 -7
- package/lib/server/config.ts +7 -29
- package/lib/server/daemon.ts +5 -3
- package/lib/server/process-control.ts +4 -2
- package/lib/server/runner.ts +7 -4
- package/lib/server/server.schema.json +0 -4
- package/lib/task/activity-intent.ts +4 -3
- package/lib/task/artifact-lifecycle.ts +35 -11
- package/lib/task/artifact-receipts.ts +55 -15
- package/lib/task/capabilities.ts +17 -3
- package/lib/task/completion-facts.ts +66 -0
- package/lib/task/create-service.ts +12 -6
- package/lib/task/create.ts +54 -69
- package/lib/task/events.ts +248 -182
- package/lib/task/finalization-artifacts.ts +4 -1
- package/lib/task/finalization-handoff.ts +2 -1
- package/lib/task/finalization.ts +154 -21
- package/lib/task/invalidation-command.ts +4 -2
- package/lib/task/invalidation.ts +35 -10
- package/lib/task/lifecycle.ts +35 -3
- package/lib/task/local-artifact-finalization.ts +13 -3
- package/lib/task/platform-operation-journal.ts +230 -0
- package/lib/task/platform-operation-recovery.ts +435 -0
- package/lib/task/qualification-audit.ts +21 -114
- package/lib/task/review-finalization.ts +7 -0
- package/lib/task/review-identity.ts +85 -0
- package/lib/task/sections.ts +6 -6
- package/lib/task/task-execution-lock.ts +7 -3
- package/lib/task/verification-engine.ts +13 -86
- package/lib/task/verification.ts +34 -38
- package/lib/task/workflow-warning-intents.ts +9 -1
- package/package.json +1 -1
- package/templates/.agents/README.en.md +19 -17
- package/templates/.agents/README.zh-CN.md +18 -16
- package/templates/.agents/rules/README.en.md +2 -1
- package/templates/.agents/rules/README.zh-CN.md +2 -1
- package/templates/.agents/rules/decision-qualification.en.md +4 -3
- package/templates/.agents/rules/decision-qualification.zh-CN.md +4 -3
- package/templates/.agents/rules/task-management.en.md +12 -0
- package/templates/.agents/rules/task-management.zh-CN.md +12 -0
- package/templates/.agents/rules/validation-output.en.md +19 -0
- package/templates/.agents/rules/validation-output.zh-CN.md +19 -0
- package/templates/.agents/skills/analyze-task/SKILL.en.md +3 -1
- package/templates/.agents/skills/analyze-task/SKILL.zh-CN.md +3 -3
- package/templates/.agents/skills/block-task/SKILL.en.md +2 -0
- package/templates/.agents/skills/block-task/SKILL.zh-CN.md +1 -1
- package/templates/.agents/skills/cancel-task/SKILL.en.md +6 -2
- package/templates/.agents/skills/cancel-task/SKILL.zh-CN.md +5 -3
- package/templates/.agents/skills/code-task/SKILL.en.md +5 -3
- package/templates/.agents/skills/code-task/SKILL.zh-CN.md +4 -4
- package/templates/.agents/skills/code-task/reference/report-template.en.md +4 -8
- package/templates/.agents/skills/code-task/reference/report-template.zh-CN.md +4 -8
- 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-manual-validation/SKILL.en.md +1 -1
- package/templates/.agents/skills/complete-manual-validation/SKILL.zh-CN.md +1 -1
- package/templates/.agents/skills/complete-task/SKILL.en.md +4 -0
- package/templates/.agents/skills/complete-task/SKILL.zh-CN.md +4 -0
- package/templates/.agents/skills/create-pr/SKILL.en.md +2 -0
- package/templates/.agents/skills/create-pr/SKILL.zh-CN.md +1 -1
- package/templates/.agents/skills/create-task/SKILL.en.md +2 -0
- package/templates/.agents/skills/create-task/SKILL.zh-CN.md +1 -1
- package/templates/.agents/skills/import-codescan/SKILL.en.md +2 -0
- package/templates/.agents/skills/import-codescan/SKILL.zh-CN.md +1 -1
- package/templates/.agents/skills/import-dependabot/SKILL.en.md +2 -0
- package/templates/.agents/skills/import-dependabot/SKILL.zh-CN.md +1 -1
- package/templates/.agents/skills/import-issue/SKILL.en.md +2 -0
- package/templates/.agents/skills/import-issue/SKILL.zh-CN.md +1 -1
- package/templates/.agents/skills/plan-task/SKILL.en.md +3 -1
- package/templates/.agents/skills/plan-task/SKILL.zh-CN.md +3 -3
- 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-analysis/SKILL.en.md +3 -1
- package/templates/.agents/skills/review-analysis/SKILL.zh-CN.md +3 -1
- package/templates/.agents/skills/review-analysis/reference/report-template.en.md +4 -8
- package/templates/.agents/skills/review-analysis/reference/report-template.zh-CN.md +4 -8
- package/templates/.agents/skills/review-code/SKILL.en.md +13 -12
- package/templates/.agents/skills/review-code/SKILL.zh-CN.md +11 -12
- package/templates/.agents/skills/review-code/config/verify.en.json +1 -3
- package/templates/.agents/skills/review-code/config/verify.zh-CN.json +1 -3
- package/templates/.agents/skills/review-code/reference/output-templates.en.md +23 -46
- package/templates/.agents/skills/review-code/reference/output-templates.zh-CN.md +27 -48
- package/templates/.agents/skills/review-code/reference/report-template.en.md +8 -11
- package/templates/.agents/skills/review-code/reference/report-template.zh-CN.md +8 -11
- package/templates/.agents/skills/review-code/reference/review-criteria.en.md +2 -2
- package/templates/.agents/skills/review-code/reference/review-criteria.zh-CN.md +2 -2
- package/templates/.agents/skills/review-plan/SKILL.en.md +3 -1
- package/templates/.agents/skills/review-plan/SKILL.zh-CN.md +3 -1
- package/templates/.agents/skills/review-plan/reference/report-template.en.md +5 -9
- package/templates/.agents/skills/review-plan/reference/report-template.zh-CN.md +5 -9
- package/templates/.agents/skills/review-pr/SKILL.en.md +13 -11
- package/templates/.agents/skills/review-pr/SKILL.zh-CN.md +13 -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/run-manual-validation/SKILL.en.md +1 -1
- package/templates/.agents/skills/run-manual-validation/SKILL.zh-CN.md +1 -1
- package/templates/.agents/skills/run-task/SKILL.en.md +1 -1
- package/templates/.agents/skills/run-task/SKILL.zh-CN.md +1 -1
- package/templates/.agents/skills/update-agent-infra/scripts/sync-templates.js +19 -6
- package/templates/.agents/skills/watch-pr/SKILL.en.md +2 -0
- package/templates/.agents/skills/watch-pr/SKILL.zh-CN.md +1 -1
- package/templates/.agents/templates/task.en.md +2 -2
- package/templates/.agents/templates/task.zh-CN.md +2 -2
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
import fs from 'node:fs';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import { createHash, randomUUID } from 'node:crypto';
|
|
4
|
+
import { isResourceIdentity } from '../platform/resource-identity.js';
|
|
5
|
+
import { resolveTaskRef } from './resolve-ref.js';
|
|
6
|
+
const JOURNAL_FILE = '.platform-operations.json';
|
|
7
|
+
const MAX_ATTEMPTS = 3;
|
|
8
|
+
function operationId(input) {
|
|
9
|
+
return createHash('sha256')
|
|
10
|
+
.update(`${input.kind}\0${input.target}\0${input.expectedDigest}`)
|
|
11
|
+
.digest('hex');
|
|
12
|
+
}
|
|
13
|
+
function journalPath(taskDir) {
|
|
14
|
+
return path.join(taskDir, JOURNAL_FILE);
|
|
15
|
+
}
|
|
16
|
+
function parseJournal(file, taskId) {
|
|
17
|
+
let value;
|
|
18
|
+
try {
|
|
19
|
+
value = JSON.parse(fs.readFileSync(file, 'utf8'));
|
|
20
|
+
}
|
|
21
|
+
catch (error) {
|
|
22
|
+
if (error.code === 'ENOENT')
|
|
23
|
+
return { version: 1, taskId, operations: [] };
|
|
24
|
+
throw Object.assign(new Error(`Platform operation journal is unreadable: ${String(error)}`), { code: 'PLATFORM_OPERATION_JOURNAL_INVALID' });
|
|
25
|
+
}
|
|
26
|
+
if (!value || typeof value !== 'object')
|
|
27
|
+
throw Object.assign(new Error('Platform operation journal is invalid'), { code: 'PLATFORM_OPERATION_JOURNAL_INVALID' });
|
|
28
|
+
const journal = value;
|
|
29
|
+
if (journal.version !== 1 || journal.taskId !== taskId || !Array.isArray(journal.operations)) {
|
|
30
|
+
throw Object.assign(new Error('Platform operation journal identity or version is invalid'), { code: 'PLATFORM_OPERATION_JOURNAL_INVALID' });
|
|
31
|
+
}
|
|
32
|
+
for (const item of journal.operations) {
|
|
33
|
+
if (!item || typeof item !== 'object' || !/^[a-f0-9]{64}$/u.test(item.id)
|
|
34
|
+
|| !['task-comment', 'artifact-comment', 'summary-comment', 'cancel-comment', 'issue-create', 'issue-metadata', 'pull-request', 'pull-request-summary', 'pull-request-review'].includes(item.kind)
|
|
35
|
+
|| typeof item.target !== 'string' || !item.target
|
|
36
|
+
|| !/^[a-f0-9]{64}$/u.test(item.expectedDigest)
|
|
37
|
+
|| !['deferred', 'required'].includes(item.dependency)
|
|
38
|
+
|| !['queued', 'pending', 'unknown', 'succeeded', 'failed'].includes(item.state)
|
|
39
|
+
|| !Number.isSafeInteger(item.attempts) || item.attempts < 0 || item.attempts > MAX_ATTEMPTS
|
|
40
|
+
|| item.maxAttempts !== MAX_ATTEMPTS
|
|
41
|
+
|| !(item.lastCode === null || typeof item.lastCode === 'string')
|
|
42
|
+
|| (item.kind === 'issue-metadata'
|
|
43
|
+
? !item.issueMetadata || typeof item.issueMetadata.requirements !== 'boolean'
|
|
44
|
+
|| typeof item.issueMetadata.issueType !== 'boolean' || typeof item.issueMetadata.fields !== 'boolean'
|
|
45
|
+
|| (item.issueMetadata.status !== undefined && typeof item.issueMetadata.status !== 'string')
|
|
46
|
+
|| (item.issueMetadata.assignees !== undefined && !['current', 'none'].includes(item.issueMetadata.assignees))
|
|
47
|
+
|| (item.issueMetadata.milestone !== undefined && !['initial', 'specific', 'none'].includes(item.issueMetadata.milestone))
|
|
48
|
+
|| (item.issueMetadata.inLabels !== undefined && !['from-diff', 'none'].includes(item.issueMetadata.inLabels))
|
|
49
|
+
|| (item.issueMetadata.base !== undefined && (typeof item.issueMetadata.base !== 'string' || !item.issueMetadata.base.trim()))
|
|
50
|
+
|| (item.issueMetadata.fromDiffFiles !== undefined && (!Array.isArray(item.issueMetadata.fromDiffFiles)
|
|
51
|
+
|| item.issueMetadata.fromDiffFiles.some((file) => typeof file !== 'string')))
|
|
52
|
+
|| (item.issueMetadata.inLabelMappingDigest !== undefined && !/^[a-f0-9]{64}$/u.test(item.issueMetadata.inLabelMappingDigest))
|
|
53
|
+
|| (item.issueMetadata.state !== undefined && !['open', 'closed'].includes(item.issueMetadata.state))
|
|
54
|
+
|| (item.issueMetadata.closeReason !== undefined && !['completed', 'not_planned'].includes(item.issueMetadata.closeReason))
|
|
55
|
+
|| (!item.issueMetadata.requirements && !item.issueMetadata.issueType && !item.issueMetadata.fields
|
|
56
|
+
&& item.issueMetadata.status === undefined && item.issueMetadata.assignees === undefined
|
|
57
|
+
&& item.issueMetadata.milestone === undefined && item.issueMetadata.inLabels === undefined && item.issueMetadata.state === undefined)
|
|
58
|
+
: item.issueMetadata !== undefined)
|
|
59
|
+
|| (item.kind === 'pull-request'
|
|
60
|
+
? !item.pullRequest || !['create', 'bind', 'sync'].includes(item.pullRequest.action)
|
|
61
|
+
|| Object.entries(item.pullRequest).some(([key, field]) => key !== 'action'
|
|
62
|
+
&& key !== 'metadata' && key !== 'closingIssue' && (typeof field !== 'string' || !field.trim()))
|
|
63
|
+
|| (item.pullRequest.metadata !== undefined && typeof item.pullRequest.metadata !== 'boolean')
|
|
64
|
+
|| (item.pullRequest.closingIssue !== undefined && typeof item.pullRequest.closingIssue !== 'boolean')
|
|
65
|
+
: item.pullRequest !== undefined)
|
|
66
|
+
|| (item.kind === 'issue-create'
|
|
67
|
+
? !item.issueCreate || typeof item.issueCreate.title !== 'string' || !item.issueCreate.title.trim()
|
|
68
|
+
|| !/^[a-f0-9]{64}$/u.test(item.issueCreate.bodyDigest)
|
|
69
|
+
: item.issueCreate !== undefined)
|
|
70
|
+
|| (item.kind === 'pull-request-summary'
|
|
71
|
+
? !item.pullRequestSummary || typeof item.pullRequestSummary.body !== 'string'
|
|
72
|
+
|| !item.pullRequestSummary.body.trim() || typeof item.pullRequestSummary.changeReportFile !== 'string'
|
|
73
|
+
|| !item.pullRequestSummary.changeReportFile.trim()
|
|
74
|
+
: item.pullRequestSummary !== undefined)
|
|
75
|
+
|| (item.kind === 'pull-request-review'
|
|
76
|
+
? !item.pullRequestReview || typeof item.pullRequestReview.prNumber !== 'string'
|
|
77
|
+
|| !isResourceIdentity(item.pullRequestReview.resource)
|
|
78
|
+
|| typeof item.pullRequestReview.providerScopeId !== 'string' || !item.pullRequestReview.providerScopeId.trim()
|
|
79
|
+
|| !item.pullRequestReview.prNumber.trim() || !/^TASK-\d{8}-\d{6}$|^pr\d+$/u.test(item.pullRequestReview.scope)
|
|
80
|
+
|| !Number.isSafeInteger(item.pullRequestReview.round) || item.pullRequestReview.round <= 0
|
|
81
|
+
|| !/^[0-9a-f]{7,40}$/iu.test(item.pullRequestReview.commitSha)
|
|
82
|
+
|| !['COMMENT', 'APPROVE', 'REQUEST_CHANGES'].includes(item.pullRequestReview.event)
|
|
83
|
+
|| typeof item.pullRequestReview.artifactFile !== 'string'
|
|
84
|
+
|| !/^pr-review(?:-r\d+)?\.md$/u.test(item.pullRequestReview.artifactFile)
|
|
85
|
+
|| (item.pullRequestReview.artifactFile === 'pr-review.md'
|
|
86
|
+
? item.pullRequestReview.round !== 1
|
|
87
|
+
: item.pullRequestReview.artifactFile !== `pr-review-r${item.pullRequestReview.round}.md`)
|
|
88
|
+
|| !/^[a-f0-9]{64}$/u.test(item.pullRequestReview.bodyDigest)
|
|
89
|
+
: item.pullRequestReview !== undefined)
|
|
90
|
+
|| typeof item.updatedAt !== 'string') {
|
|
91
|
+
throw Object.assign(new Error('Platform operation journal contains an invalid operation'), { code: 'PLATFORM_OPERATION_JOURNAL_INVALID' });
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
return journal;
|
|
95
|
+
}
|
|
96
|
+
function writeJournal(file, journal) {
|
|
97
|
+
fs.mkdirSync(path.dirname(file), { recursive: true });
|
|
98
|
+
const temporary = `${file}.${process.pid}.${randomUUID()}.tmp`;
|
|
99
|
+
try {
|
|
100
|
+
fs.writeFileSync(temporary, `${JSON.stringify(journal, null, 2)}\n`, { flag: 'wx', mode: 0o600 });
|
|
101
|
+
fs.renameSync(temporary, file);
|
|
102
|
+
}
|
|
103
|
+
finally {
|
|
104
|
+
try {
|
|
105
|
+
fs.unlinkSync(temporary);
|
|
106
|
+
}
|
|
107
|
+
catch { /* renamed or already removed */ }
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
function resolveJournal(taskRef, cwd) {
|
|
111
|
+
const resolved = resolveTaskRef(taskRef, cwd ? { repoRoot: cwd } : {});
|
|
112
|
+
if (!resolved.ok)
|
|
113
|
+
throw Object.assign(new Error(resolved.message), { code: resolved.code });
|
|
114
|
+
return { taskId: resolved.taskId, file: journalPath(resolved.taskDir) };
|
|
115
|
+
}
|
|
116
|
+
function recordPlatformOperation(input) {
|
|
117
|
+
if (!/^[a-f0-9]{64}$/u.test(input.expectedDigest) || !input.target.trim()) {
|
|
118
|
+
throw Object.assign(new Error('Platform operation requires a stable target and SHA-256 digest'), { code: 'PLATFORM_OPERATION_PAYLOAD_INVALID' });
|
|
119
|
+
}
|
|
120
|
+
const { taskId, file } = resolveJournal(input.taskRef, input.cwd);
|
|
121
|
+
const journal = parseJournal(file, taskId);
|
|
122
|
+
const id = operationId(input);
|
|
123
|
+
const previous = journal.operations.find((item) => item.id === id);
|
|
124
|
+
const next = {
|
|
125
|
+
id,
|
|
126
|
+
kind: input.kind,
|
|
127
|
+
target: input.target,
|
|
128
|
+
expectedDigest: input.expectedDigest,
|
|
129
|
+
dependency: input.dependency,
|
|
130
|
+
state: input.state,
|
|
131
|
+
attempts: Math.min(MAX_ATTEMPTS, (previous?.attempts ?? 0) + (input.state === 'pending' ? 1 : 0)),
|
|
132
|
+
maxAttempts: MAX_ATTEMPTS,
|
|
133
|
+
lastCode: input.lastCode ?? null,
|
|
134
|
+
...(input.issueMetadata ? { issueMetadata: input.issueMetadata } : {}),
|
|
135
|
+
...(input.issueCreate ? { issueCreate: input.issueCreate } : {}),
|
|
136
|
+
...(input.pullRequest ? { pullRequest: input.pullRequest } : {}),
|
|
137
|
+
...(input.pullRequestSummary ? { pullRequestSummary: input.pullRequestSummary } : {}),
|
|
138
|
+
...(input.pullRequestReview ? { pullRequestReview: input.pullRequestReview } : {}),
|
|
139
|
+
updatedAt: new Date().toISOString()
|
|
140
|
+
};
|
|
141
|
+
const operations = previous
|
|
142
|
+
? journal.operations.map((item) => item.id === id ? next : item)
|
|
143
|
+
: [...journal.operations, next];
|
|
144
|
+
writeJournal(file, { version: 1, taskId, operations });
|
|
145
|
+
return next;
|
|
146
|
+
}
|
|
147
|
+
function readPlatformOperationJournal(taskRef, cwd) {
|
|
148
|
+
const { taskId, file } = resolveJournal(taskRef, cwd);
|
|
149
|
+
return parseJournal(file, taskId);
|
|
150
|
+
}
|
|
151
|
+
export { JOURNAL_FILE as PLATFORM_OPERATION_JOURNAL_FILE, MAX_ATTEMPTS as PLATFORM_OPERATION_MAX_ATTEMPTS, operationId, recordPlatformOperation, readPlatformOperationJournal };
|
|
152
|
+
//# sourceMappingURL=platform-operation-journal.js.map
|
|
@@ -0,0 +1,459 @@
|
|
|
1
|
+
import fs from 'node:fs';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import { createHash } from 'node:crypto';
|
|
4
|
+
import { execFileSync } from 'node:child_process';
|
|
5
|
+
import { inspectPlatformCommentOperation, normalizeCommentContent, syncPlatformComment } from '../platform/issue-comments.js';
|
|
6
|
+
import { inspectPlatformIssue, syncPlatformIssue } from '../platform/issues.js';
|
|
7
|
+
import { resolvePlatformProviderContext } from '../platform/context.js';
|
|
8
|
+
import { providerOperationContext, providerError, unsupportedProviderOperation } from '../platform/provider-bridge.js';
|
|
9
|
+
import { inLabelMappingDigest } from '../platform/in-label-sync.js';
|
|
10
|
+
import { bindPlatformPullRequest, inspectPlatformPullRequest, recoverCreatedPullRequest, syncPlatformPullRequest } from '../platform/pull-requests.js';
|
|
11
|
+
import { syncPullRequestSummary } from '../platform/pr-summary.js';
|
|
12
|
+
import { publishPrReview, readReviewBodyFile } from '../platform/pr-review.js';
|
|
13
|
+
import { parseTaskFrontmatter } from './frontmatter.js';
|
|
14
|
+
import { taskIssueIdentity } from '../platform/task-identities.js';
|
|
15
|
+
import { parseResourceIdentity, resourceIdentityEquals } from '../platform/resource-identity.js';
|
|
16
|
+
import { operationId } from './platform-operation-journal.js';
|
|
17
|
+
import { canonicalizeSummaryBody } from '../platform/comment-safety.js';
|
|
18
|
+
import { platformResult } from '../platform/types.js';
|
|
19
|
+
import { resolveTaskRef } from './resolve-ref.js';
|
|
20
|
+
import { readPlatformOperationJournal, recordPlatformOperation } from './platform-operation-journal.js';
|
|
21
|
+
function labelsMatchOwnedPrefix(actual, expected, prefix) {
|
|
22
|
+
return actual.filter((label) => label.startsWith(prefix)).sort().join('\0')
|
|
23
|
+
=== expected.filter((label) => label.startsWith(prefix)).sort().join('\0');
|
|
24
|
+
}
|
|
25
|
+
function fieldsMatchExpected(actual, expected) {
|
|
26
|
+
return Object.entries(expected).every(([name, value]) => actual[name] === value);
|
|
27
|
+
}
|
|
28
|
+
function result(status, recovered, pending, error = null) {
|
|
29
|
+
return { status, changed: recovered.length > 0, recovered, pending, error };
|
|
30
|
+
}
|
|
31
|
+
function summaryPayload(taskDir, taskId) {
|
|
32
|
+
const file = path.join(taskDir, '.delivery-summary.json');
|
|
33
|
+
if (!fs.existsSync(file))
|
|
34
|
+
return null;
|
|
35
|
+
const data = JSON.parse(fs.readFileSync(file, 'utf8'));
|
|
36
|
+
const canonical = typeof data.body === 'string' ? canonicalizeSummaryBody(data.body) : null;
|
|
37
|
+
if (data.taskId !== taskId || typeof data.body !== 'string' || typeof data.sha256 !== 'string'
|
|
38
|
+
|| !canonical?.ok || canonical.value !== data.body
|
|
39
|
+
|| createHash('sha256').update(data.body).digest('hex') !== data.sha256) {
|
|
40
|
+
throw Object.assign(new Error('delivery summary staging record is invalid'), { code: 'SUMMARY_STAGING_INVALID' });
|
|
41
|
+
}
|
|
42
|
+
return { body: data.body, sha256: data.sha256 };
|
|
43
|
+
}
|
|
44
|
+
async function replayComment(taskId, taskDir, operation, agent, cwd, client) {
|
|
45
|
+
if (operation.kind === 'task-comment') {
|
|
46
|
+
return syncPlatformComment(taskId, { kind: 'task', agent, cwd, client, dependency: operation.dependency, skipQueue: true });
|
|
47
|
+
}
|
|
48
|
+
if (operation.kind === 'artifact-comment') {
|
|
49
|
+
return syncPlatformComment(taskId, { kind: 'artifact', artifact: operation.target, agent, cwd, client, dependency: operation.dependency, skipQueue: true });
|
|
50
|
+
}
|
|
51
|
+
if (operation.kind === 'summary-comment') {
|
|
52
|
+
const summary = summaryPayload(taskDir, taskId);
|
|
53
|
+
if (!summary)
|
|
54
|
+
return platformResult('failed', { error: { code: 'SUMMARY_STAGING_MISSING', message: 'delivery summary staging record is unavailable', retryable: false } });
|
|
55
|
+
return syncPlatformComment(taskId, {
|
|
56
|
+
kind: 'summary', body: summary.body, agent, cwd,
|
|
57
|
+
summaryAuthorization: { sha256: summary.sha256 }, dependency: operation.dependency, client, skipQueue: true
|
|
58
|
+
});
|
|
59
|
+
}
|
|
60
|
+
if (operation.kind === 'cancel-comment') {
|
|
61
|
+
return syncPlatformComment(taskId, { kind: 'cancel', agent, cwd, client, dependency: operation.dependency, skipQueue: true });
|
|
62
|
+
}
|
|
63
|
+
return platformResult('failed', { error: { code: 'PLATFORM_OPERATION_UNSUPPORTED', message: `operation kind '${operation.kind}' has no registered recovery handler`, retryable: false } });
|
|
64
|
+
}
|
|
65
|
+
function currentIssueMetadataOperation(taskId, taskMdPath, repoRoot, operation) {
|
|
66
|
+
if (operation.kind !== 'issue-metadata' || !operation.issueMetadata)
|
|
67
|
+
return null;
|
|
68
|
+
const content = fs.readFileSync(taskMdPath, 'utf8');
|
|
69
|
+
const frontmatter = parseTaskFrontmatter(content);
|
|
70
|
+
const identity = taskIssueIdentity(frontmatter);
|
|
71
|
+
if (!identity)
|
|
72
|
+
return null;
|
|
73
|
+
let fromDiffFiles;
|
|
74
|
+
let mappingDigest;
|
|
75
|
+
if (operation.issueMetadata.inLabels === 'from-diff') {
|
|
76
|
+
const taskBase = typeof frontmatter.delivery_base_ref === 'string' ? frontmatter.delivery_base_ref.trim() : '';
|
|
77
|
+
if (!taskBase || (operation.issueMetadata.base && operation.issueMetadata.base !== taskBase))
|
|
78
|
+
return null;
|
|
79
|
+
try {
|
|
80
|
+
fromDiffFiles = execFileSync('git', ['diff', `${taskBase}...HEAD`, '--name-only'], {
|
|
81
|
+
cwd: repoRoot, encoding: 'utf8'
|
|
82
|
+
}).trim().split(/\r?\n/).filter(Boolean).sort();
|
|
83
|
+
const config = JSON.parse(fs.readFileSync(path.join(repoRoot, '.agents', '.airc.json'), 'utf8'));
|
|
84
|
+
const mapping = inLabelMappingDigest(config.labels?.in);
|
|
85
|
+
if (!mapping.ok)
|
|
86
|
+
return null;
|
|
87
|
+
mappingDigest = mapping.digest;
|
|
88
|
+
}
|
|
89
|
+
catch {
|
|
90
|
+
return null;
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
const currentMetadata = {
|
|
94
|
+
...operation.issueMetadata,
|
|
95
|
+
...(fromDiffFiles ? { fromDiffFiles } : {}),
|
|
96
|
+
...(mappingDigest ? { inLabelMappingDigest: mappingDigest } : {})
|
|
97
|
+
};
|
|
98
|
+
const target = JSON.stringify(identity);
|
|
99
|
+
const expectedDigest = createHash('sha256').update(JSON.stringify({
|
|
100
|
+
...currentMetadata,
|
|
101
|
+
task: taskId,
|
|
102
|
+
taskContent: createHash('sha256').update(content).digest('hex')
|
|
103
|
+
})).digest('hex');
|
|
104
|
+
return { kind: 'issue-metadata', target, expectedDigest, issueMetadata: currentMetadata, id: operationId({ kind: 'issue-metadata', target, expectedDigest }) };
|
|
105
|
+
}
|
|
106
|
+
async function replayIssueMetadata(taskId, operation, agent, cwd, client) {
|
|
107
|
+
if (!operation.issueMetadata)
|
|
108
|
+
return platformResult('failed', { error: { code: 'PLATFORM_OPERATION_PAYLOAD_INVALID', message: 'Issue metadata recovery parameters are missing', retryable: false } });
|
|
109
|
+
let expectedIdentity;
|
|
110
|
+
try {
|
|
111
|
+
expectedIdentity = parseResourceIdentity(JSON.parse(operation.target), 'journal Issue identity');
|
|
112
|
+
}
|
|
113
|
+
catch {
|
|
114
|
+
return platformResult('failed', { error: { code: 'PLATFORM_OPERATION_PAYLOAD_INVALID', message: 'Journal Issue identity is invalid', retryable: false } });
|
|
115
|
+
}
|
|
116
|
+
const before = await inspectPlatformIssue(taskId, { cwd, client });
|
|
117
|
+
if (before.status !== 'no-op' || before.error || !before.issue
|
|
118
|
+
|| !resourceIdentityEquals(before.issue.identity, expectedIdentity)) {
|
|
119
|
+
return platformResult('blocked', { error: before.error ?? { code: 'PLATFORM_OPERATION_IDENTITY_MISMATCH', message: 'Bound Issue identity differs from the journal target', retryable: false } });
|
|
120
|
+
}
|
|
121
|
+
const sync = await syncPlatformIssue(taskId, { ...operation.issueMetadata, agent, cwd, client, dependency: operation.dependency, skipQueue: true });
|
|
122
|
+
if ((sync.status !== 'applied' && sync.status !== 'no-op') || sync.error)
|
|
123
|
+
return sync;
|
|
124
|
+
const after = await inspectPlatformIssue(taskId, { cwd, client });
|
|
125
|
+
if (after.status !== 'no-op' || after.error || !after.issue
|
|
126
|
+
|| !resourceIdentityEquals(after.issue.identity, expectedIdentity)) {
|
|
127
|
+
return platformResult('blocked', { error: after.error ?? { code: 'PLATFORM_OPERATION_IDENTITY_MISMATCH', message: 'Issue identity changed during metadata recovery', retryable: false } });
|
|
128
|
+
}
|
|
129
|
+
const issue = after.issue;
|
|
130
|
+
const metadata = operation.issueMetadata;
|
|
131
|
+
const failedOperation = sync.operations.find((item) => item.status === 'failed' || item.status === 'skipped');
|
|
132
|
+
if (failedOperation)
|
|
133
|
+
return platformResult('blocked', { error: {
|
|
134
|
+
code: failedOperation.reasonCode || 'PLATFORM_OPERATION_METADATA_UNCONFIRMED',
|
|
135
|
+
message: `Issue metadata recovery did not confirm operation '${failedOperation.name}'`, retryable: true
|
|
136
|
+
} });
|
|
137
|
+
const unconfirmed = sync.operations.some((item) => {
|
|
138
|
+
if (!('value' in item) || item.value === undefined)
|
|
139
|
+
return false;
|
|
140
|
+
if (item.name === 'labels:status' || item.name === 'labels:in') {
|
|
141
|
+
const prefix = item.name === 'labels:status' ? 'status:' : 'in:';
|
|
142
|
+
return !labelsMatchOwnedPrefix(issue.labels, item.value, prefix);
|
|
143
|
+
}
|
|
144
|
+
if (item.name === 'assignees')
|
|
145
|
+
return [...issue.assignees].sort().join('\0') !== [...item.value].sort().join('\0');
|
|
146
|
+
if (item.name === 'milestone')
|
|
147
|
+
return issue.milestone !== item.value;
|
|
148
|
+
if (item.name === 'requirements')
|
|
149
|
+
return issue.body !== item.value;
|
|
150
|
+
if (item.name === 'state')
|
|
151
|
+
return issue.state !== item.value;
|
|
152
|
+
if (item.name === 'issue-type')
|
|
153
|
+
return issue.issueType !== item.value;
|
|
154
|
+
if (item.name === 'fields')
|
|
155
|
+
return !fieldsMatchExpected(issue.fields, item.value);
|
|
156
|
+
return false;
|
|
157
|
+
}) || (metadata.state !== undefined && issue.state !== metadata.state)
|
|
158
|
+
|| (metadata.assignees === 'none' && issue.assignees.length !== 0)
|
|
159
|
+
|| (metadata.milestone === 'none' && issue.milestone !== null);
|
|
160
|
+
if (unconfirmed)
|
|
161
|
+
return platformResult('blocked', { error: {
|
|
162
|
+
code: 'PLATFORM_OPERATION_METADATA_UNCONFIRMED',
|
|
163
|
+
message: 'Issue metadata differs from the requested recovery target after reread', retryable: true
|
|
164
|
+
} });
|
|
165
|
+
return platformResult('no-op', { changed: sync.changed || after.changed });
|
|
166
|
+
}
|
|
167
|
+
async function verifyIssueCreate(taskId, operation, cwd, client) {
|
|
168
|
+
if (!operation.issueCreate)
|
|
169
|
+
return platformResult('failed', { error: {
|
|
170
|
+
code: 'PLATFORM_OPERATION_PAYLOAD_INVALID', message: 'Issue creation recovery identity is missing', retryable: false
|
|
171
|
+
} });
|
|
172
|
+
const current = await inspectPlatformIssue(taskId, { cwd, client });
|
|
173
|
+
if (!current.issue)
|
|
174
|
+
return platformResult('blocked', { error: current.error ?? {
|
|
175
|
+
code: 'PLATFORM_OPERATION_TARGET_UNAVAILABLE',
|
|
176
|
+
message: 'Issue creation outcome is unknown and the task has no bound Issue identity; bind the verified remote Issue before continuing',
|
|
177
|
+
retryable: true
|
|
178
|
+
} });
|
|
179
|
+
if (current.issue.title !== operation.issueCreate.title
|
|
180
|
+
|| createHash('sha256').update(current.issue.body).digest('hex') !== operation.issueCreate.bodyDigest)
|
|
181
|
+
return platformResult('blocked', { error: {
|
|
182
|
+
code: 'PLATFORM_OPERATION_IDENTITY_MISMATCH', message: 'Bound Issue does not match the queued creation intent', retryable: false
|
|
183
|
+
} });
|
|
184
|
+
return platformResult('no-op');
|
|
185
|
+
}
|
|
186
|
+
async function replayPullRequestSummary(taskId, operation, agent, cwd, client) {
|
|
187
|
+
let identity;
|
|
188
|
+
try {
|
|
189
|
+
identity = parseResourceIdentity(JSON.parse(operation.target), 'queued pull-request summary identity');
|
|
190
|
+
}
|
|
191
|
+
catch {
|
|
192
|
+
return platformResult('failed', { error: { code: 'PLATFORM_OPERATION_PAYLOAD_INVALID', message: 'Queued pull-request summary identity is invalid', retryable: false } });
|
|
193
|
+
}
|
|
194
|
+
const resolved = await resolvePlatformProviderContext({ cwd, client });
|
|
195
|
+
if (!resolved.ok)
|
|
196
|
+
return resolved.context;
|
|
197
|
+
const listed = resolved.value.provider.comments?.list
|
|
198
|
+
? await resolved.value.provider.comments.list({ context: providerOperationContext(resolved.value), parent: identity })
|
|
199
|
+
: unsupportedProviderOperation(resolved.value.provider, 'comments.list');
|
|
200
|
+
if (!listed.ok)
|
|
201
|
+
return platformResult(listed.error.retryable ? 'blocked' : 'failed', { error: providerError(listed.error, 'PLATFORM_PROVIDER_OPERATION_FAILED') });
|
|
202
|
+
const marker = `<!-- sync-pr:${taskId}:summary -->`;
|
|
203
|
+
const summaries = listed.value.filter((comment) => normalizeCommentContent(comment.body).split('\n', 1)[0] === marker);
|
|
204
|
+
if (summaries.length > 1)
|
|
205
|
+
return platformResult('blocked', { error: {
|
|
206
|
+
code: 'PR_SUMMARY_MARKER_AMBIGUOUS', message: 'Multiple pull-request comments contain the queued summary marker', retryable: false
|
|
207
|
+
} });
|
|
208
|
+
if (summaries.length === 1 && createHash('sha256').update(normalizeCommentContent(summaries[0].body)).digest('hex') === operation.expectedDigest) {
|
|
209
|
+
return platformResult('no-op');
|
|
210
|
+
}
|
|
211
|
+
if (!operation.pullRequestSummary)
|
|
212
|
+
return platformResult('blocked', { error: {
|
|
213
|
+
code: 'PLATFORM_OPERATION_PAYLOAD_INVALID', message: 'Pull-request summary recovery content is missing', retryable: false
|
|
214
|
+
} });
|
|
215
|
+
const bound = await inspectPlatformPullRequest(taskId, { cwd, client });
|
|
216
|
+
if (!bound.pullRequest || !resourceIdentityEquals(bound.pullRequest.identity, identity))
|
|
217
|
+
return platformResult('blocked', { error: bound.error ?? {
|
|
218
|
+
code: 'PLATFORM_OPERATION_IDENTITY_MISMATCH', message: 'Bound pull request differs from the queued summary target', retryable: false
|
|
219
|
+
} });
|
|
220
|
+
const replayed = await syncPullRequestSummary(taskId, {
|
|
221
|
+
agent,
|
|
222
|
+
cwd,
|
|
223
|
+
client,
|
|
224
|
+
body: operation.pullRequestSummary.body,
|
|
225
|
+
changeReportFile: operation.pullRequestSummary.changeReportFile,
|
|
226
|
+
primaryResult: 'no_op',
|
|
227
|
+
strict: true,
|
|
228
|
+
skipQueue: true,
|
|
229
|
+
lockAlreadyHeld: true
|
|
230
|
+
});
|
|
231
|
+
if ((replayed.status === 'applied' || replayed.status === 'no-op') && !replayed.error && replayed.warnings.length === 0) {
|
|
232
|
+
return platformResult(replayed.status, { changed: replayed.changed });
|
|
233
|
+
}
|
|
234
|
+
return platformResult('blocked', { error: replayed.error ?? {
|
|
235
|
+
code: 'PR_SUMMARY_RECOVERY_UNCONFIRMED', message: 'Pull-request summary replay did not confirm the requested remote state', retryable: true
|
|
236
|
+
} });
|
|
237
|
+
}
|
|
238
|
+
async function replayPullRequestReview(operation, agent, cwd, client) {
|
|
239
|
+
const intent = operation.pullRequestReview;
|
|
240
|
+
if (!intent)
|
|
241
|
+
return platformResult('failed', { error: { code: 'PLATFORM_OPERATION_PAYLOAD_INVALID', message: 'Pull-request review recovery parameters are missing', retryable: false } });
|
|
242
|
+
let target;
|
|
243
|
+
try {
|
|
244
|
+
target = parseResourceIdentity(JSON.parse(operation.target), 'queued pull-request review identity');
|
|
245
|
+
}
|
|
246
|
+
catch {
|
|
247
|
+
return platformResult('failed', { error: { code: 'PLATFORM_OPERATION_PAYLOAD_INVALID', message: 'Queued pull-request review identity is invalid', retryable: false } });
|
|
248
|
+
}
|
|
249
|
+
if (!intent.resource || !resourceIdentityEquals(target, intent.resource))
|
|
250
|
+
return platformResult('failed', { error: {
|
|
251
|
+
code: 'PLATFORM_OPERATION_IDENTITY_MISMATCH', message: 'Pull-request review identity differs from the journal target', retryable: false
|
|
252
|
+
} });
|
|
253
|
+
let body;
|
|
254
|
+
try {
|
|
255
|
+
body = readReviewBodyFile(intent.scope, intent.round, intent.artifactFile, intent.bodyDigest, cwd);
|
|
256
|
+
}
|
|
257
|
+
catch {
|
|
258
|
+
return platformResult('failed', { error: {
|
|
259
|
+
code: 'PLATFORM_OPERATION_PAYLOAD_INVALID', message: 'Canonical pull-request review body is unavailable or does not match the queued digest', retryable: false
|
|
260
|
+
} });
|
|
261
|
+
}
|
|
262
|
+
return publishPrReview({
|
|
263
|
+
cwd,
|
|
264
|
+
client,
|
|
265
|
+
agent,
|
|
266
|
+
prNumber: intent.prNumber,
|
|
267
|
+
expectedResource: target,
|
|
268
|
+
expectedProviderScopeId: intent.providerScopeId,
|
|
269
|
+
identity: { scope: intent.scope, round: intent.round, commitSha: intent.commitSha, ...(intent.resource ? { resource: intent.resource } : {}) },
|
|
270
|
+
event: intent.event,
|
|
271
|
+
body,
|
|
272
|
+
recoveryArtifact: intent.artifactFile,
|
|
273
|
+
skipQueue: true
|
|
274
|
+
});
|
|
275
|
+
}
|
|
276
|
+
async function replayPullRequest(taskId, operation, agent, cwd) {
|
|
277
|
+
const intent = operation.pullRequest;
|
|
278
|
+
if (!intent)
|
|
279
|
+
return platformResult('failed', { error: { code: 'PLATFORM_OPERATION_PAYLOAD_INVALID', message: 'Pull-request recovery parameters are missing', retryable: false } });
|
|
280
|
+
if (intent.action === 'bind') {
|
|
281
|
+
if (!intent.prToken)
|
|
282
|
+
return platformResult('failed', { error: { code: 'PLATFORM_OPERATION_PAYLOAD_INVALID', message: 'Pull-request bind identity is missing', retryable: false } });
|
|
283
|
+
return bindPlatformPullRequest(taskId, { agent, cwd, pr: intent.prToken, skipQueue: true });
|
|
284
|
+
}
|
|
285
|
+
if (intent.action === 'sync') {
|
|
286
|
+
const synced = await syncPlatformPullRequest(taskId, {
|
|
287
|
+
agent, cwd, metadata: intent.metadata === true, closingIssue: intent.closingIssue === true, primaryResult: 'no_op', skipQueue: true
|
|
288
|
+
});
|
|
289
|
+
if (synced.warnings.length > 0 || synced.result?.endsWith('_with_warnings')) {
|
|
290
|
+
return platformResult('blocked', { error: { code: 'PLATFORM_OPERATION_PR_SYNC_UNCONFIRMED', message: 'Pull-request metadata sync remains degraded', retryable: true } });
|
|
291
|
+
}
|
|
292
|
+
return synced;
|
|
293
|
+
}
|
|
294
|
+
return recoverCreatedPullRequest(taskId, { agent, cwd, base: intent.baseRef || '', head: intent.headRef || '', skipQueue: true });
|
|
295
|
+
}
|
|
296
|
+
async function recoverPlatformOperations(taskRef, selection, options) {
|
|
297
|
+
const resolved = resolveTaskRef(taskRef, options.cwd ? { repoRoot: options.cwd } : {});
|
|
298
|
+
if (!resolved.ok)
|
|
299
|
+
return result('failed', [], [], { code: resolved.code, message: resolved.message, retryable: false });
|
|
300
|
+
let journal;
|
|
301
|
+
try {
|
|
302
|
+
journal = readPlatformOperationJournal(resolved.taskId, resolved.repoRoot);
|
|
303
|
+
}
|
|
304
|
+
catch (error) {
|
|
305
|
+
const value = error;
|
|
306
|
+
return result('failed', [], [], { code: value.code || 'PLATFORM_OPERATION_JOURNAL_INVALID', message: value.message || String(error), retryable: false });
|
|
307
|
+
}
|
|
308
|
+
const candidates = journal.operations.filter((operation) => operation.id !== options.excludeId
|
|
309
|
+
&& (operation.state === 'queued' || operation.state === 'pending' || operation.state === 'unknown'
|
|
310
|
+
|| (operation.state === 'failed' && operation.lastCode !== 'PLATFORM_OPERATION_SUPERSEDED')))
|
|
311
|
+
.filter((operation) => selection === 'all' || operation.dependency === selection)
|
|
312
|
+
.sort((left, right) => left.updatedAt.localeCompare(right.updatedAt));
|
|
313
|
+
if (!candidates.length)
|
|
314
|
+
return result('no-op', [], []);
|
|
315
|
+
const limit = options.limit === undefined ? candidates.length : Math.max(1, options.limit);
|
|
316
|
+
const recovered = [];
|
|
317
|
+
const pending = [];
|
|
318
|
+
const selected = candidates.slice(0, limit);
|
|
319
|
+
for (const [index, operation] of selected.entries()) {
|
|
320
|
+
if (operation.state === 'failed') {
|
|
321
|
+
pending.push(operation.id);
|
|
322
|
+
return result('blocked', recovered, pending, {
|
|
323
|
+
code: operation.lastCode || 'PLATFORM_OPERATION_FAILED',
|
|
324
|
+
message: 'A previous platform operation failed permanently; later writes remain queued',
|
|
325
|
+
retryable: false
|
|
326
|
+
});
|
|
327
|
+
}
|
|
328
|
+
if (operation.attempts >= operation.maxAttempts) {
|
|
329
|
+
pending.push(operation.id, ...selected.slice(index + 1).map((next) => next.id));
|
|
330
|
+
pending.push(...candidates.slice(limit).map((next) => next.id));
|
|
331
|
+
return result('blocked', recovered, pending, {
|
|
332
|
+
code: operation.lastCode || 'PLATFORM_OPERATION_RETRY_LIMIT_REACHED',
|
|
333
|
+
message: 'A previous platform operation exhausted its recovery attempts; later writes remain queued',
|
|
334
|
+
retryable: false
|
|
335
|
+
});
|
|
336
|
+
}
|
|
337
|
+
if (operation.kind !== 'issue-create') {
|
|
338
|
+
try {
|
|
339
|
+
recordPlatformOperation({
|
|
340
|
+
taskRef: resolved.taskId, cwd: resolved.repoRoot, kind: operation.kind,
|
|
341
|
+
target: operation.target, expectedDigest: operation.expectedDigest,
|
|
342
|
+
dependency: operation.dependency, state: 'pending',
|
|
343
|
+
...(operation.issueMetadata ? { issueMetadata: operation.issueMetadata } : {}),
|
|
344
|
+
...(operation.issueCreate ? { issueCreate: operation.issueCreate } : {}),
|
|
345
|
+
...(operation.pullRequest ? { pullRequest: operation.pullRequest } : {}),
|
|
346
|
+
...(operation.pullRequestSummary ? { pullRequestSummary: operation.pullRequestSummary } : {}),
|
|
347
|
+
...(operation.pullRequestReview ? { pullRequestReview: operation.pullRequestReview } : {})
|
|
348
|
+
});
|
|
349
|
+
}
|
|
350
|
+
catch (error) {
|
|
351
|
+
const value = error;
|
|
352
|
+
return result('blocked', recovered, [...pending, operation.id], {
|
|
353
|
+
code: value.code || 'PLATFORM_OPERATION_JOURNAL_WRITE_FAILED',
|
|
354
|
+
message: value.message || String(error), retryable: true
|
|
355
|
+
});
|
|
356
|
+
}
|
|
357
|
+
}
|
|
358
|
+
let targetOperation = operation;
|
|
359
|
+
if (operation.kind === 'issue-metadata') {
|
|
360
|
+
const current = currentIssueMetadataOperation(resolved.taskId, resolved.taskMdPath, resolved.repoRoot, operation);
|
|
361
|
+
if (!current) {
|
|
362
|
+
pending.push(operation.id);
|
|
363
|
+
return result('blocked', recovered, pending, { code: 'PLATFORM_OPERATION_TARGET_UNAVAILABLE', message: 'Current Issue metadata target cannot be reconstructed safely', retryable: true });
|
|
364
|
+
}
|
|
365
|
+
if (current.target !== operation.target) {
|
|
366
|
+
return result('blocked', recovered, [...pending, operation.id], { code: 'PLATFORM_OPERATION_IDENTITY_MISMATCH', message: 'Bound Issue identity differs from the journal target', retryable: false });
|
|
367
|
+
}
|
|
368
|
+
if (current.id !== operation.id) {
|
|
369
|
+
try {
|
|
370
|
+
recordPlatformOperation({ taskRef: resolved.taskId, cwd: resolved.repoRoot, kind: operation.kind, target: operation.target,
|
|
371
|
+
expectedDigest: operation.expectedDigest, issueMetadata: operation.issueMetadata, dependency: operation.dependency,
|
|
372
|
+
state: 'failed', lastCode: 'PLATFORM_OPERATION_SUPERSEDED' });
|
|
373
|
+
targetOperation = { ...operation, expectedDigest: current.expectedDigest, issueMetadata: current.issueMetadata, id: current.id };
|
|
374
|
+
}
|
|
375
|
+
catch (error) {
|
|
376
|
+
const value = error;
|
|
377
|
+
return result('blocked', recovered, [...pending, operation.id], { code: value.code || 'PLATFORM_OPERATION_JOURNAL_WRITE_FAILED', message: value.message || String(error), retryable: true });
|
|
378
|
+
}
|
|
379
|
+
}
|
|
380
|
+
}
|
|
381
|
+
if (operation.kind === 'task-comment' || operation.kind === 'artifact-comment'
|
|
382
|
+
|| operation.kind === 'summary-comment' || operation.kind === 'cancel-comment') {
|
|
383
|
+
const commentOptions = operation.kind === 'task-comment'
|
|
384
|
+
? { kind: 'task', agent: options.agent, cwd: resolved.repoRoot, dependency: operation.dependency }
|
|
385
|
+
: operation.kind === 'artifact-comment'
|
|
386
|
+
? { kind: 'artifact', artifact: operation.target, agent: options.agent, cwd: resolved.repoRoot, dependency: operation.dependency }
|
|
387
|
+
: operation.kind === 'summary-comment' ? (() => {
|
|
388
|
+
const summary = summaryPayload(resolved.taskDir, resolved.taskId);
|
|
389
|
+
return summary ? { kind: 'summary', body: summary.body, agent: options.agent, cwd: resolved.repoRoot, summaryAuthorization: { sha256: summary.sha256 }, dependency: operation.dependency } : null;
|
|
390
|
+
})() : { kind: 'cancel', agent: options.agent, cwd: resolved.repoRoot, dependency: operation.dependency };
|
|
391
|
+
const current = commentOptions ? inspectPlatformCommentOperation(resolved.taskId, commentOptions) : null;
|
|
392
|
+
if (!current) {
|
|
393
|
+
pending.push(operation.id);
|
|
394
|
+
return result('blocked', recovered, pending, { code: 'PLATFORM_OPERATION_TARGET_UNAVAILABLE', message: 'Current comment target cannot be reconstructed safely', retryable: true });
|
|
395
|
+
}
|
|
396
|
+
if (current.id !== operation.id) {
|
|
397
|
+
try {
|
|
398
|
+
recordPlatformOperation({ taskRef: resolved.taskId, cwd: resolved.repoRoot, kind: operation.kind, target: operation.target,
|
|
399
|
+
expectedDigest: operation.expectedDigest, dependency: operation.dependency, state: 'failed', lastCode: 'PLATFORM_OPERATION_SUPERSEDED' });
|
|
400
|
+
targetOperation = { ...operation, expectedDigest: current.expectedDigest, id: current.id };
|
|
401
|
+
}
|
|
402
|
+
catch (error) {
|
|
403
|
+
const value = error;
|
|
404
|
+
return result('blocked', recovered, [...pending, operation.id], { code: value.code || 'PLATFORM_OPERATION_JOURNAL_WRITE_FAILED', message: value.message || String(error), retryable: true });
|
|
405
|
+
}
|
|
406
|
+
}
|
|
407
|
+
}
|
|
408
|
+
let remote;
|
|
409
|
+
try {
|
|
410
|
+
remote = operation.kind === 'issue-create'
|
|
411
|
+
? await verifyIssueCreate(resolved.taskId, operation, resolved.repoRoot)
|
|
412
|
+
: operation.kind === 'pull-request-summary'
|
|
413
|
+
? await replayPullRequestSummary(resolved.taskId, operation, options.agent, resolved.repoRoot, options.client)
|
|
414
|
+
: operation.kind === 'pull-request-review'
|
|
415
|
+
? await replayPullRequestReview(operation, options.agent, resolved.repoRoot, options.client)
|
|
416
|
+
: operation.kind === 'issue-metadata'
|
|
417
|
+
? await replayIssueMetadata(resolved.taskId, targetOperation, options.agent, resolved.repoRoot)
|
|
418
|
+
: operation.kind === 'pull-request'
|
|
419
|
+
? await replayPullRequest(resolved.taskId, targetOperation, options.agent, resolved.repoRoot)
|
|
420
|
+
: await replayComment(resolved.taskId, resolved.taskDir, operation, options.agent, resolved.repoRoot);
|
|
421
|
+
}
|
|
422
|
+
catch (error) {
|
|
423
|
+
const value = error;
|
|
424
|
+
remote = platformResult('blocked', { error: { code: value.code || 'PLATFORM_OPERATION_RECOVERY_FAILED', message: value.message || String(error), retryable: true } });
|
|
425
|
+
}
|
|
426
|
+
const succeeded = (remote.status === 'applied' || remote.status === 'no-op') && !remote.error;
|
|
427
|
+
try {
|
|
428
|
+
recordPlatformOperation({
|
|
429
|
+
taskRef: resolved.taskId,
|
|
430
|
+
cwd: resolved.repoRoot,
|
|
431
|
+
kind: operation.kind,
|
|
432
|
+
target: operation.target,
|
|
433
|
+
expectedDigest: targetOperation.expectedDigest,
|
|
434
|
+
...(targetOperation.issueMetadata ? { issueMetadata: targetOperation.issueMetadata } : {}),
|
|
435
|
+
...(operation.issueCreate ? { issueCreate: operation.issueCreate } : {}),
|
|
436
|
+
...(operation.pullRequest ? { pullRequest: operation.pullRequest } : {}),
|
|
437
|
+
...(operation.pullRequestSummary ? { pullRequestSummary: operation.pullRequestSummary } : {}),
|
|
438
|
+
...(operation.pullRequestReview ? { pullRequestReview: operation.pullRequestReview } : {}),
|
|
439
|
+
dependency: operation.dependency,
|
|
440
|
+
state: succeeded ? 'succeeded' : remote.status === 'failed' && remote.error?.retryable === false ? 'failed' : 'unknown',
|
|
441
|
+
lastCode: remote.error?.code ?? null
|
|
442
|
+
});
|
|
443
|
+
}
|
|
444
|
+
catch (error) {
|
|
445
|
+
const value = error;
|
|
446
|
+
return result('blocked', recovered, [...pending, operation.id], { code: value.code || 'PLATFORM_OPERATION_JOURNAL_WRITE_FAILED', message: value.message || String(error), retryable: true });
|
|
447
|
+
}
|
|
448
|
+
if (!succeeded) {
|
|
449
|
+
pending.push(targetOperation.id);
|
|
450
|
+
return result(remote.status === 'failed' ? 'failed' : 'blocked', recovered, pending, remote.error ?? { code: 'PLATFORM_OPERATION_RECOVERY_UNKNOWN', message: 'Platform operation outcome remains unknown', retryable: true });
|
|
451
|
+
}
|
|
452
|
+
recovered.push(targetOperation.id);
|
|
453
|
+
}
|
|
454
|
+
const remaining = candidates.slice(limit).map((operation) => operation.id);
|
|
455
|
+
pending.push(...remaining);
|
|
456
|
+
return result(pending.length ? 'blocked' : recovered.length ? 'applied' : 'no-op', recovered, pending, pending.length ? { code: 'PLATFORM_OPERATION_RECOVERY_PENDING', message: 'Platform operations remain pending after the recovery budget', retryable: true } : null);
|
|
457
|
+
}
|
|
458
|
+
export { fieldsMatchExpected, labelsMatchOwnedPrefix, recoverPlatformOperations };
|
|
459
|
+
//# sourceMappingURL=platform-operation-recovery.js.map
|