@fitlab-ai/agent-infra 0.11.7 → 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.
Files changed (95) hide show
  1. package/dist/bin/internal-cli.js +8 -2
  2. package/dist/lib/agent-clients/adapters/codex.js +1 -1
  3. package/dist/lib/agent-clients/config.js +11 -3
  4. package/dist/lib/defaults.json +6 -3
  5. package/dist/lib/internal/cli-route-inventory.js +3 -0
  6. package/dist/lib/internal/platform-pr-review.js +20 -3
  7. package/dist/lib/internal/task-operation-registry.js +3 -1
  8. package/dist/lib/internal/task-platform-recovery.js +56 -0
  9. package/dist/lib/merge.js +25 -5
  10. package/dist/lib/platform/completion-backfill.js +2 -1
  11. package/dist/lib/platform/in-label-sync.js +12 -1
  12. package/dist/lib/platform/issue-comments.js +58 -4
  13. package/dist/lib/platform/issues.js +105 -5
  14. package/dist/lib/platform/operation-coordinator.js +74 -0
  15. package/dist/lib/platform/pr-review.js +107 -1
  16. package/dist/lib/platform/pr-summary.js +79 -57
  17. package/dist/lib/platform/pull-requests.js +96 -4
  18. package/dist/lib/sandbox/commands/create.js +6 -3
  19. package/dist/lib/sandbox/commands/ls.js +4 -3
  20. package/dist/lib/sandbox/commands/rm.js +1 -1
  21. package/dist/lib/sandbox/config.js +22 -4
  22. package/dist/lib/sandbox/git-metadata.js +50 -0
  23. package/dist/lib/sandbox/index.js +1 -1
  24. package/dist/lib/sandbox/mounts.js +12 -0
  25. package/dist/lib/sandbox/recovery.js +11 -0
  26. package/dist/lib/sandbox/removal.js +58 -20
  27. package/dist/lib/sandbox/tools.js +29 -7
  28. package/dist/lib/task/activity-intent.js +4 -3
  29. package/dist/lib/task/create-service.js +5 -3
  30. package/dist/lib/task/create.js +22 -5
  31. package/dist/lib/task/finalization-artifacts.js +4 -1
  32. package/dist/lib/task/finalization.js +157 -24
  33. package/dist/lib/task/lifecycle.js +38 -2
  34. package/dist/lib/task/platform-operation-journal.js +152 -0
  35. package/dist/lib/task/platform-operation-recovery.js +459 -0
  36. package/dist/lib/task/workflow-warning-intents.js +7 -1
  37. package/dist/lifecycle-build-manifest.json +3 -3
  38. package/lib/agent-clients/adapters/codex.ts +1 -1
  39. package/lib/agent-clients/config.ts +9 -3
  40. package/lib/defaults.json +6 -3
  41. package/lib/internal/cli-route-inventory.ts +2 -0
  42. package/lib/internal/platform-pr-review.ts +14 -3
  43. package/lib/internal/task-operation-registry.ts +3 -1
  44. package/lib/internal/task-platform-recovery.ts +53 -0
  45. package/lib/merge.ts +26 -6
  46. package/lib/platform/completion-backfill.ts +2 -1
  47. package/lib/platform/in-label-sync.ts +12 -0
  48. package/lib/platform/issue-comments.ts +59 -3
  49. package/lib/platform/issues.ts +103 -5
  50. package/lib/platform/operation-coordinator.ts +96 -0
  51. package/lib/platform/pr-review.ts +113 -1
  52. package/lib/platform/pr-summary.ts +22 -1
  53. package/lib/platform/pull-requests.ts +105 -5
  54. package/lib/sandbox/commands/create.ts +7 -8
  55. package/lib/sandbox/commands/ls.ts +4 -3
  56. package/lib/sandbox/commands/rm.ts +1 -1
  57. package/lib/sandbox/config.ts +23 -5
  58. package/lib/sandbox/git-metadata.ts +66 -0
  59. package/lib/sandbox/index.ts +1 -1
  60. package/lib/sandbox/mounts.ts +14 -0
  61. package/lib/sandbox/recovery.ts +13 -0
  62. package/lib/sandbox/removal.ts +57 -19
  63. package/lib/sandbox/tool-types.ts +1 -1
  64. package/lib/sandbox/tools.ts +36 -7
  65. package/lib/task/activity-intent.ts +4 -3
  66. package/lib/task/create-service.ts +5 -3
  67. package/lib/task/create.ts +17 -5
  68. package/lib/task/finalization-artifacts.ts +4 -1
  69. package/lib/task/finalization.ts +154 -21
  70. package/lib/task/lifecycle.ts +35 -3
  71. package/lib/task/platform-operation-journal.ts +230 -0
  72. package/lib/task/platform-operation-recovery.ts +435 -0
  73. package/lib/task/workflow-warning-intents.ts +9 -1
  74. package/package.json +1 -1
  75. package/templates/.agents/README.en.md +19 -17
  76. package/templates/.agents/README.zh-CN.md +18 -16
  77. package/templates/.agents/rules/README.en.md +1 -1
  78. package/templates/.agents/rules/README.zh-CN.md +1 -1
  79. package/templates/.agents/rules/task-management.en.md +12 -0
  80. package/templates/.agents/rules/task-management.zh-CN.md +12 -0
  81. package/templates/.agents/skills/cancel-task/SKILL.en.md +4 -2
  82. package/templates/.agents/skills/cancel-task/SKILL.zh-CN.md +4 -2
  83. package/templates/.agents/skills/code-task/SKILL.en.md +2 -2
  84. package/templates/.agents/skills/code-task/SKILL.zh-CN.md +2 -2
  85. package/templates/.agents/skills/complete-task/SKILL.en.md +2 -0
  86. package/templates/.agents/skills/complete-task/SKILL.zh-CN.md +2 -0
  87. package/templates/.agents/skills/plan-task/config/verify.en.json +1 -1
  88. package/templates/.agents/skills/plan-task/config/verify.zh-CN.json +1 -1
  89. package/templates/.agents/skills/review-pr/SKILL.en.md +11 -11
  90. package/templates/.agents/skills/review-pr/SKILL.zh-CN.md +11 -11
  91. package/templates/.agents/skills/review-pr/reference/output-guidance.en.md +3 -3
  92. package/templates/.agents/skills/review-pr/reference/output-guidance.zh-CN.md +3 -3
  93. package/templates/.agents/skills/review-pr/reference/report-template.en.md +4 -0
  94. package/templates/.agents/skills/review-pr/reference/report-template.zh-CN.md +4 -0
  95. package/templates/.agents/skills/update-agent-infra/scripts/sync-templates.js +19 -6
@@ -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
@@ -1,4 +1,5 @@
1
1
  import fs from 'node:fs';
2
+ import path from 'node:path';
2
3
  import { resolveTaskRef } from './resolve-ref.js';
3
4
  import { parseTaskFrontmatter } from './frontmatter.js';
4
5
  import { WORKFLOW_WARNING_COLUMNS, WORKFLOW_WARNING_HEADINGS, WORKFLOW_WARNING_SEVERITIES, WORKFLOW_WARNING_STATUSES, parseWorkflowWarnings } from './workflow-warnings.js';
@@ -120,7 +121,12 @@ function projectFinalizationWarning(taskRef, warning, options = {}) {
120
121
  let prepared = resolved.state === 'completed';
121
122
  if (!prepared && resolved.state === 'active') {
122
123
  try {
123
- prepared = parseTaskFrontmatter(fs.readFileSync(resolved.taskMdPath, 'utf8')).status === 'completed';
124
+ const taskContent = fs.readFileSync(resolved.taskMdPath, 'utf8');
125
+ prepared = parseTaskFrontmatter(taskContent).status === 'completed';
126
+ if (!prepared) {
127
+ const lifecycle = JSON.parse(fs.readFileSync(path.join(resolved.taskDir, '.task-lifecycle.json'), 'utf8'));
128
+ prepared = lifecycle.taskId === resolved.taskId && lifecycle.intent === 'complete';
129
+ }
124
130
  }
125
131
  catch {
126
132
  prepared = false;
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "protocolVersion": 3,
3
- "packageVersion": "0.11.7",
4
- "sourceInputHash": "095f0cf30aa29fcd2dd45123dc755e5097d7a9f81de33420461fd943cff122b2",
5
- "internalExecutableBuildHash": "a956f2ed366e025a111a87bc1f542b87d83d4025e068304f95b1aae704709d3f"
3
+ "packageVersion": "0.11.8",
4
+ "sourceInputHash": "6ddb72f8b50137d5148a47172bd7eb2628b17a937ae647ac38350e582ba08124",
5
+ "internalExecutableBuildHash": "c886e015506879620bb490f1ea5977d9952f2bbb4515d3a0b49e0ec184b48028"
6
6
  }
@@ -54,7 +54,7 @@ const codexAdapter = defineAgentClientAdapter({
54
54
  containerMount: '/home/devuser/.codex',
55
55
  versionCmd: 'codex --version',
56
56
  setupHint: 'Run codex once inside the container and choose Device Code login if needed.',
57
- tmpfs: { size: '512m', seed: ['config.toml', 'model-catalogs'] },
57
+ tmpfs: { size: '512m', seed: ['config.toml', 'model-catalogs'], exec: true },
58
58
  hostLiveMounts: [
59
59
  {
60
60
  hostPath: hostJoin(home, '.codex', 'auth.json'),
@@ -167,10 +167,16 @@ function normalizeAgentClients(
167
167
  }
168
168
  if (hasOwn(input, 'tuis')) fail('INVALID_AGENT_CLIENTS', 'tuis');
169
169
  const sandbox = isRecord(input.sandbox) ? input.sandbox : undefined;
170
- if (Array.isArray(sandbox?.tools)) {
171
- for (const [index, tool] of sandbox.tools.entries()) {
170
+ if (sandbox && hasOwn(sandbox, 'customTools')) {
171
+ fail('INVALID_AGENT_CLIENTS', 'sandbox.customTools');
172
+ }
173
+ if (sandbox && hasOwn(sandbox, 'tools')) {
174
+ if (!isRecord(sandbox.tools)) fail('INVALID_AGENT_CLIENTS', 'sandbox.tools');
175
+ const tools = sandbox.tools;
176
+ if (!Array.isArray(tools.ids)) fail('INVALID_AGENT_CLIENTS', 'sandbox.tools.ids');
177
+ for (const [index, tool] of tools.ids.entries()) {
172
178
  if (isAgentClientId(tool)) {
173
- fail('INVALID_AGENT_CLIENTS', `sandbox.tools[${index}]`);
179
+ fail('INVALID_AGENT_CLIENTS', `sandbox.tools.ids[${index}]`);
174
180
  }
175
181
  }
176
182
  }
package/lib/defaults.json CHANGED
@@ -34,9 +34,12 @@
34
34
  "runtimes": [
35
35
  "node22"
36
36
  ],
37
- "tools": [
38
- "agent-infra"
39
- ],
37
+ "tools": {
38
+ "ids": [
39
+ "agent-infra"
40
+ ],
41
+ "definitions": {}
42
+ },
40
43
  "refreshIntervalDays": 7,
41
44
  "dockerfile": null,
42
45
  "vm": {
@@ -63,6 +63,7 @@ export const INTERNAL_HANDLER_ROUTE_SELECTORS = Object.freeze({
63
63
  'task-invalidation': ['reconcile'],
64
64
  'task-lifecycle': ['intent'],
65
65
  'task-finalization': ['complete'],
66
+ 'task-platform-recovery': ['inspect', 'recover'],
66
67
  'task-short-id': ['list', 'list-verify', 'alloc', 'release', 'resolve'],
67
68
  'task-snapshot': ['snapshot'],
68
69
  'task-verify': ['event'],
@@ -89,6 +90,7 @@ export function internalRouteSelector(command: string, args: readonly string[]):
89
90
  if (command === 'task-qualification') return args[1] ?? '';
90
91
  if (command === 'task-lifecycle') return args[1] ? 'intent' : '';
91
92
  if (command === 'task-finalization') return args[1] === 'complete' ? 'complete' : '';
93
+ if (command === 'task-platform-recovery') return args[1] ?? '';
92
94
  if (command === 'task-event') return args[1] ? 'event' : '';
93
95
  if (command === 'task-verify') return args[0] && args[1] ? 'event' : '';
94
96
  if (command === 'task-snapshot') return 'snapshot';
@@ -6,14 +6,15 @@ import { listPrReviews, publishPrReview } from '../platform/pr-review.ts';
6
6
  import type { PrReviewEvent } from '../platform/pr-review.ts';
7
7
  import type { PlatformResult } from '../platform/types.ts';
8
8
  import { ensureInternalHandlerRoute, internalHandlerRoute } from './cli-route-inventory.ts';
9
+ import { normalizeAgentToken, AGENT_USAGE_HINT } from '../agent-clients/tokens.ts';
9
10
 
10
11
  const USAGE = `Usage: agent-infra-internal platform-pr-review inspect --pr <token> [--cwd <path>]
11
12
  agent-infra-internal platform-pr-review list --pr <token> [--cwd <path>]
12
- agent-infra-internal platform-pr-review publish --pr <token> --scope <taskId|pr{N}> --round <N> --commit <sha> --event <COMMENT|APPROVE|REQUEST_CHANGES> --body-file <path|-> [--dry-run] [--cwd <path>]
13
+ agent-infra-internal platform-pr-review publish --pr <token> --scope <taskId|pr{N}> --round <N> --commit <sha> --event <COMMENT|APPROVE|REQUEST_CHANGES> --body-file <path|-> [--artifact <pr-review-artifact>] [--agent <agent>] [--dry-run] [--cwd <path>]
13
14
  `;
14
15
 
15
16
  const BOOLEAN_FLAGS = new Set(['--dry-run']);
16
- const VALUE_FLAGS = new Set(['--cwd', '--pr', '--scope', '--round', '--commit', '--event', '--body-file']);
17
+ const VALUE_FLAGS = new Set(['--cwd', '--pr', '--scope', '--round', '--commit', '--event', '--body-file', '--artifact', '--agent']);
17
18
 
18
19
  function key(flag: string): string {
19
20
  return flag.slice(2).replace(/-([a-z])/g, (_, letter: string) => letter.toUpperCase());
@@ -67,7 +68,7 @@ async function platformPrReview(args: string[] = []): Promise<void> {
67
68
  const allowed: Record<string, string[]> = {
68
69
  inspect: ['cwd', 'pr'],
69
70
  list: ['cwd', 'pr'],
70
- publish: ['cwd', 'pr', 'scope', 'round', 'commit', 'event', 'bodyFile', 'dryRun']
71
+ publish: ['cwd', 'pr', 'scope', 'round', 'commit', 'event', 'bodyFile', 'artifact', 'agent', 'dryRun']
71
72
  };
72
73
  const unexpected = Object.keys(values).find((name) => !allowed[operation]!.includes(name));
73
74
  if (unexpected) { fail(`${operation} does not accept --${unexpected}`); return; }
@@ -86,6 +87,8 @@ async function platformPrReview(args: string[] = []): Promise<void> {
86
87
  const round = Number(values.round);
87
88
  const commit = typeof values.commit === 'string' ? values.commit : '';
88
89
  const event = values.event as string;
90
+ const rawAgent = typeof values.agent === 'string' ? values.agent : '';
91
+ const agent = rawAgent ? normalizeAgentToken(rawAgent) : null;
89
92
  if (!scope || !/^(?:pr\d+|TASK-\d{8}-\d{6})$/.test(scope)) { fail('publish requires --scope <taskId|pr{N}>'); return; }
90
93
  if (!internalHandlerRoute('platform-pr-review', 'publish-task', publishSelector)
91
94
  && !internalHandlerRoute('platform-pr-review', 'publish-pr', publishSelector)) {
@@ -95,6 +98,12 @@ async function platformPrReview(args: string[] = []): Promise<void> {
95
98
  if (!/^[0-9a-f]{7,40}$/i.test(commit)) { fail('publish requires --commit <sha>'); return; }
96
99
  if (!['COMMENT', 'APPROVE', 'REQUEST_CHANGES'].includes(event)) { fail('publish requires --event <COMMENT|APPROVE|REQUEST_CHANGES>'); return; }
97
100
  if (typeof values.bodyFile !== 'string') { fail('publish requires --body-file'); return; }
101
+ if (/^TASK-\d{8}-\d{6}$/u.test(scope) && typeof values.artifact !== 'string') { fail('task-scoped publish requires --artifact <pr-review-artifact>'); return; }
102
+ if (/^TASK-\d{8}-\d{6}$/u.test(scope) && !agent) {
103
+ fail(`task-scoped publish requires --agent <agent>${rawAgent ? `: invalid '${rawAgent}'` : ''}: ${AGENT_USAGE_HINT}`);
104
+ return;
105
+ }
106
+ if (rawAgent && !agent) { fail(`invalid --agent '${rawAgent}': ${AGENT_USAGE_HINT}`); return; }
98
107
  let body: string;
99
108
  try {
100
109
  body = readBodyFile(values.bodyFile, cwd);
@@ -108,6 +117,8 @@ async function platformPrReview(args: string[] = []): Promise<void> {
108
117
  identity: { scope, round, commitSha: commit },
109
118
  event: event as PrReviewEvent,
110
119
  body,
120
+ ...(typeof values.artifact === 'string' ? { recoveryArtifact: values.artifact } : {}),
121
+ ...(agent ? { agent } : {}),
111
122
  dryRun: values.dryRun === true
112
123
  }));
113
124
  }
@@ -145,6 +145,8 @@ function internalTaskRoutes(): TaskOperationDescriptor[] {
145
145
  descriptor('internal', 'task-invalidation', 'reconcile', 'task-bound', 'progress'),
146
146
  descriptor('internal', 'task-lifecycle', 'intent', 'task-bound', 'progress'),
147
147
  descriptor('internal', 'task-finalization', 'complete', 'task-bound', 'progress'),
148
+ descriptor('internal', 'task-platform-recovery', 'inspect', 'task-bound', 'diagnostic'),
149
+ descriptor('internal', 'task-platform-recovery', 'recover', 'task-bound', 'recovery'),
148
150
  descriptor('internal', 'task-short-id', 'list', 'conditional', 'diagnostic'),
149
151
  descriptor('internal', 'task-short-id', 'list-verify', 'conditional', 'diagnostic'),
150
152
  descriptor('internal', 'task-short-id', 'alloc', 'task-bound', 'progress'),
@@ -221,7 +223,7 @@ export const INTERNAL_DISPATCHER_ROUTES = Object.freeze([
221
223
  'platform-comment', 'platform-issue', 'platform-pr', 'platform-pr-review', 'pr-review-grade',
222
224
  'platform-checks', 'task-context', 'task-ledger', 'task-warning', 'task-activity', 'task-artifact',
223
225
  'task-orchestration', 'task-review', 'task-event', 'task-invalidation', 'task-lifecycle',
224
- 'task-finalization', 'task-short-id', 'task-snapshot', 'task-verify', 'task-validate', 'manual-validation'
226
+ 'task-finalization', 'task-platform-recovery', 'task-short-id', 'task-snapshot', 'task-verify', 'task-validate', 'manual-validation'
225
227
  ]);
226
228
  export const PUBLIC_DISPATCHER_ROUTES = Object.freeze([
227
229
  'agent-client', 'cp', 'data', 'decide', 'help', 'init', 'merge', 'run', 'sandbox', 'server', 'task', 'sync', 'update', 'version'