@fitlab-ai/agent-infra 0.11.3 → 0.11.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +17 -8
- package/README.zh-CN.md +17 -8
- package/dist/bin/internal-cli.js +1 -6
- package/dist/lib/decide.js +4 -41
- package/dist/lib/git/branch-worktree.js +45 -0
- package/dist/lib/internal/cli-route-inventory.js +1 -4
- package/dist/lib/internal/task-activity.js +7 -40
- package/dist/lib/internal/task-artifact.js +1 -1
- package/dist/lib/internal/task-event.js +4 -37
- package/dist/lib/internal/task-finalization.js +48 -1
- package/dist/lib/internal/task-ledger.js +14 -44
- package/dist/lib/internal/task-lifecycle.js +1 -1
- package/dist/lib/internal/task-operation-registry.js +2 -4
- package/dist/lib/internal/task-review.js +5 -37
- package/dist/lib/internal/task-verify.js +3 -47
- package/dist/lib/internal/task-warning.js +4 -40
- package/dist/lib/platform/completion-backfill.js +63 -17
- package/dist/lib/platform/pr-summary.js +4 -23
- package/dist/lib/platform/verification-sync.js +12 -49
- package/dist/lib/sandbox/control/client.js +4 -1
- package/dist/lib/sandbox/control/executor.js +12 -4
- package/dist/lib/sandbox/control/protocol.js +2 -1
- package/dist/lib/sandbox/control/server.js +26 -143
- package/dist/lib/sandbox/removal.js +15 -8
- package/dist/lib/sandbox/task-cleanup.js +13 -20
- package/dist/lib/task/activity-intent.js +1 -2
- package/dist/lib/task/artifact-command.js +11 -4
- package/dist/lib/task/artifact-lifecycle.js +217 -85
- package/dist/lib/task/artifact-operations.js +37 -28
- package/dist/lib/task/artifact-receipts.js +4 -1
- package/dist/lib/task/artifact-schema.js +8 -7
- package/dist/lib/task/artifact-selection.js +25 -0
- package/dist/lib/task/capabilities.js +158 -39
- package/dist/lib/task/commands/status.js +9 -2
- package/dist/lib/task/control-authority.js +12 -168
- package/dist/lib/task/decision-intents.js +14 -4
- package/dist/lib/task/delivery.js +1 -43
- package/dist/lib/task/events.js +68 -73
- package/dist/lib/task/finalization-handoff.js +92 -0
- package/dist/lib/task/finalization.js +423 -154
- package/dist/lib/task/intermediate-cleanup.js +9 -148
- package/dist/lib/task/ledger-intents.js +159 -20
- package/dist/lib/task/lifecycle-path.js +75 -0
- package/dist/lib/task/lifecycle.js +60 -11
- package/dist/lib/task/local-artifact-finalization.js +13 -47
- package/dist/lib/task/orchestration.js +17 -7
- package/dist/lib/task/review-command.js +1 -2
- package/dist/lib/task/review-finalization.js +4 -18
- package/dist/lib/task/rework-intent.js +70 -4
- package/dist/lib/task/snapshot.js +6 -6
- package/dist/lib/task/verification-engine.js +25 -30
- package/dist/lib/task/verification.js +2 -13
- package/dist/lib/task/workflow-warning-intents.js +23 -10
- package/dist/lib/task/workspace-snapshot.js +7 -2
- package/dist/lib/task/write.js +1 -2
- package/dist/lifecycle-build-manifest.json +3 -3
- package/lib/decide.ts +5 -35
- package/lib/git/branch-worktree.ts +41 -0
- package/lib/internal/cli-route-inventory.ts +1 -3
- package/lib/internal/task-activity.ts +6 -34
- package/lib/internal/task-artifact.ts +1 -1
- package/lib/internal/task-event.ts +4 -37
- package/lib/internal/task-finalization.ts +47 -1
- package/lib/internal/task-ledger.ts +15 -41
- package/lib/internal/task-lifecycle.ts +2 -2
- package/lib/internal/task-operation-registry.ts +2 -4
- package/lib/internal/task-review.ts +5 -31
- package/lib/internal/task-verify.ts +3 -39
- package/lib/internal/task-warning.ts +3 -32
- package/lib/platform/completion-backfill.ts +78 -16
- package/lib/platform/pr-summary.ts +4 -25
- package/lib/platform/verification-sync.ts +13 -53
- package/lib/sandbox/control/client.ts +5 -1
- package/lib/sandbox/control/executor.ts +13 -4
- package/lib/sandbox/control/protocol.ts +3 -2
- package/lib/sandbox/control/server.ts +21 -146
- package/lib/sandbox/removal.ts +23 -7
- package/lib/sandbox/task-cleanup.ts +12 -20
- package/lib/task/activity-intent.ts +1 -2
- package/lib/task/artifact-command.ts +14 -4
- package/lib/task/artifact-lifecycle.ts +236 -85
- package/lib/task/artifact-operations.ts +59 -28
- package/lib/task/artifact-receipts.ts +4 -1
- package/lib/task/artifact-schema.ts +10 -7
- package/lib/task/artifact-selection.ts +53 -0
- package/lib/task/capabilities.ts +154 -40
- package/lib/task/commands/status.ts +11 -2
- package/lib/task/control-authority.ts +17 -189
- package/lib/task/decision-intents.ts +11 -4
- package/lib/task/delivery.ts +1 -40
- package/lib/task/events.ts +73 -74
- package/lib/task/finalization-handoff.ts +104 -0
- package/lib/task/finalization.ts +388 -122
- package/lib/task/intermediate-cleanup.ts +9 -125
- package/lib/task/ledger-intents.ts +157 -20
- package/lib/task/lifecycle-path.ts +98 -0
- package/lib/task/lifecycle.ts +58 -13
- package/lib/task/local-artifact-finalization.ts +14 -56
- package/lib/task/orchestration.ts +26 -8
- package/lib/task/review-command.ts +2 -5
- package/lib/task/review-finalization.ts +4 -20
- package/lib/task/rework-intent.ts +75 -5
- package/lib/task/snapshot.ts +6 -6
- package/lib/task/verification-engine.ts +29 -30
- package/lib/task/verification.ts +3 -16
- package/lib/task/workflow-warning-intents.ts +20 -10
- package/lib/task/workspace-snapshot.ts +3 -2
- package/lib/task/write.ts +1 -4
- package/package.json +1 -1
- package/templates/.agents/rules/review-handshake.en.md +4 -4
- package/templates/.agents/rules/review-handshake.zh-CN.md +4 -4
- package/templates/.agents/skills/analyze-task/SKILL.en.md +21 -5
- package/templates/.agents/skills/analyze-task/SKILL.zh-CN.md +21 -5
- package/templates/.agents/skills/code-task/SKILL.en.md +14 -8
- package/templates/.agents/skills/code-task/SKILL.zh-CN.md +18 -13
- package/templates/.agents/skills/code-task/reference/code-rules.en.md +2 -2
- package/templates/.agents/skills/code-task/reference/code-rules.zh-CN.md +2 -2
- package/templates/.agents/skills/code-task/reference/dual-mode.en.md +4 -4
- package/templates/.agents/skills/code-task/reference/dual-mode.zh-CN.md +4 -4
- package/templates/.agents/skills/code-task/reference/report-template.en.md +7 -7
- package/templates/.agents/skills/code-task/reference/report-template.zh-CN.md +7 -7
- package/templates/.agents/skills/complete-manual-validation/config/verify.en.json +2 -2
- package/templates/.agents/skills/complete-manual-validation/config/verify.zh-CN.json +2 -2
- package/templates/.agents/skills/complete-manual-validation/reference/report-template.en.md +1 -3
- package/templates/.agents/skills/complete-manual-validation/reference/report-template.zh-CN.md +1 -3
- package/templates/.agents/skills/complete-task/SKILL.en.md +2 -2
- package/templates/.agents/skills/complete-task/SKILL.zh-CN.md +2 -2
- package/templates/.agents/skills/complete-task/config/verify.en.json +2 -2
- package/templates/.agents/skills/complete-task/config/verify.zh-CN.json +2 -2
- package/templates/.agents/skills/plan-task/SKILL.en.md +13 -10
- package/templates/.agents/skills/plan-task/SKILL.zh-CN.md +13 -10
- package/templates/.agents/skills/review-analysis/SKILL.en.md +5 -1
- package/templates/.agents/skills/review-analysis/SKILL.zh-CN.md +5 -1
- package/templates/.agents/skills/review-analysis/reference/report-template.en.md +3 -3
- package/templates/.agents/skills/review-analysis/reference/report-template.zh-CN.md +3 -3
- package/templates/.agents/skills/review-code/SKILL.en.md +5 -1
- package/templates/.agents/skills/review-code/SKILL.zh-CN.md +5 -1
- package/templates/.agents/skills/review-code/reference/report-template.en.md +3 -3
- package/templates/.agents/skills/review-code/reference/report-template.zh-CN.md +3 -3
- package/templates/.agents/skills/review-plan/SKILL.en.md +5 -1
- package/templates/.agents/skills/review-plan/SKILL.zh-CN.md +5 -1
- package/templates/.agents/skills/review-plan/reference/report-template.en.md +3 -3
- package/templates/.agents/skills/review-plan/reference/report-template.zh-CN.md +3 -3
- package/templates/.agents/skills/review-pr/config/verify.en.json +3 -1
- package/templates/.agents/skills/review-pr/config/verify.zh-CN.json +3 -1
- package/templates/.agents/skills/review-pr/reference/report-template.en.md +3 -3
- package/templates/.agents/skills/review-pr/reference/report-template.zh-CN.md +3 -3
- package/templates/.agents/skills/run-manual-validation/config/verify.en.json +2 -2
- package/templates/.agents/skills/run-manual-validation/config/verify.zh-CN.json +2 -2
- package/templates/.agents/skills/run-manual-validation/reference/report-template.en.md +2 -2
- package/templates/.agents/skills/run-manual-validation/reference/report-template.zh-CN.md +2 -2
- package/templates/.agents/skills/update-agent-infra/scripts/sync-templates.js +18 -2
- package/templates/.agents/templates/task.en.md +2 -2
- package/templates/.agents/templates/task.zh-CN.md +2 -2
- package/templates/.agents/workflows/bug-fix.en.yaml +11 -6
- package/templates/.agents/workflows/bug-fix.zh-CN.yaml +11 -6
- package/templates/.agents/workflows/feature-development.en.yaml +11 -6
- package/templates/.agents/workflows/feature-development.zh-CN.yaml +11 -6
- package/templates/.agents/workflows/refactoring.en.yaml +11 -6
- package/templates/.agents/workflows/refactoring.zh-CN.yaml +11 -6
- package/dist/lib/internal/task-override.js +0 -138
- package/dist/lib/sandbox/control/finalization-response.js +0 -33
- package/dist/lib/task/artifact-recovery.js +0 -476
- package/dist/lib/task/artifact-repair-intent.js +0 -181
- package/dist/lib/task/guard-override.js +0 -31
- package/dist/lib/task/human-override.js +0 -1259
- package/lib/internal/task-override.ts +0 -144
- package/lib/sandbox/control/finalization-response.ts +0 -35
- package/lib/task/artifact-recovery.ts +0 -572
- package/lib/task/artifact-repair-intent.ts +0 -227
- package/lib/task/guard-override.ts +0 -50
- package/lib/task/human-override.ts +0 -1522
package/lib/task/finalization.ts
CHANGED
|
@@ -4,12 +4,13 @@ import { createHash, randomUUID } from 'node:crypto';
|
|
|
4
4
|
|
|
5
5
|
import { syncPlatformComment } from '../platform/issue-comments.ts';
|
|
6
6
|
import { canonicalizeSummaryBody } from '../platform/comment-safety.ts';
|
|
7
|
-
import { backfillCompletionComments } from '../platform/completion-backfill.ts';
|
|
7
|
+
import { backfillCompletionComments, inspectCompletionBackfillEligibility } from '../platform/completion-backfill.ts';
|
|
8
8
|
import type { PlatformResult } from '../platform/types.ts';
|
|
9
9
|
import { taskIssueIdentity } from '../platform/task-identities.ts';
|
|
10
10
|
import { parseTaskFrontmatter } from './frontmatter.ts';
|
|
11
11
|
import {
|
|
12
12
|
applyTaskLifecycle,
|
|
13
|
+
inspectTaskLifecycleProgress,
|
|
13
14
|
type TaskLifecycleOptions,
|
|
14
15
|
type TaskLifecycleRequest,
|
|
15
16
|
type TaskLifecycleResult
|
|
@@ -25,6 +26,9 @@ import {
|
|
|
25
26
|
type OperationWarning,
|
|
26
27
|
type OperationWarningSeverity
|
|
27
28
|
} from './operation-outcome.ts';
|
|
29
|
+
import { readTaskFinalizationHandoff } from './finalization-handoff.ts';
|
|
30
|
+
import { readSandboxControlManifest } from '../sandbox/control/lifecycle.ts';
|
|
31
|
+
import { validateSandboxControlIdentity } from '../sandbox/control/identity-sentinel.ts';
|
|
28
32
|
|
|
29
33
|
const RECEIPT_VERSION = 4 as const;
|
|
30
34
|
const FINALIZATION_STEPS = ['lifecycle', 'task-comment', 'verification', 'summary'] as const;
|
|
@@ -55,11 +59,14 @@ type TaskFinalizationRequest = Readonly<{
|
|
|
55
59
|
taskRef: string;
|
|
56
60
|
intent: 'complete';
|
|
57
61
|
agent: string;
|
|
62
|
+
handoffSha256?: string;
|
|
58
63
|
}>;
|
|
59
64
|
|
|
60
65
|
type TaskFinalizationOptions = Readonly<{
|
|
61
66
|
repoRoot: string;
|
|
62
67
|
controlBinding?: Readonly<{ generation: string; requestId: string }>;
|
|
68
|
+
handoffDirectory?: string;
|
|
69
|
+
manifestPath?: string;
|
|
63
70
|
metadataProvider?: TaskLifecycleOptions['metadataProvider'];
|
|
64
71
|
lifecycle?: typeof applyTaskLifecycle;
|
|
65
72
|
backfill?: typeof backfillCompletionComments;
|
|
@@ -101,7 +108,7 @@ type TaskCommentSyncOutcome = Readonly<{
|
|
|
101
108
|
}>;
|
|
102
109
|
|
|
103
110
|
type TaskFinalizationResult = Readonly<{
|
|
104
|
-
status: 'completed' | 'failed' | 'blocked';
|
|
111
|
+
status: 'prepared' | 'completed' | 'failed' | 'blocked';
|
|
105
112
|
changed: boolean;
|
|
106
113
|
taskId: string | null;
|
|
107
114
|
backfill: TaskFinalizationStep | null;
|
|
@@ -111,7 +118,7 @@ type TaskFinalizationResult = Readonly<{
|
|
|
111
118
|
summary: TaskFinalizationStep | null;
|
|
112
119
|
completedSteps: readonly FinalizationStep[];
|
|
113
120
|
pendingSteps: readonly FinalizationStep[];
|
|
114
|
-
result: 'completed' | 'completed_with_warnings' | 'failed' | 'blocked';
|
|
121
|
+
result: 'prepared' | 'completed' | 'completed_with_warnings' | 'failed' | 'blocked';
|
|
115
122
|
warnings: readonly OperationWarning[];
|
|
116
123
|
error: FinalizationError | null;
|
|
117
124
|
}>;
|
|
@@ -198,20 +205,32 @@ function validateReceipt(value: unknown, taskId: string): TaskFinalizationReceip
|
|
|
198
205
|
const receipt = value as Record<string, unknown>;
|
|
199
206
|
const controlBinding = receipt.controlBinding as Record<string, unknown> | null | undefined;
|
|
200
207
|
const states = ['pending', 'done', 'skipped'];
|
|
208
|
+
const requiredKeys = [
|
|
209
|
+
'version', 'taskId', 'intent', 'receiptId', 'revision', 'lifecycle', 'taskComment',
|
|
210
|
+
'verification', 'summary', 'warningProjection', 'warnings', 'updatedAt', 'lastError'
|
|
211
|
+
];
|
|
212
|
+
if (receipt.controlBinding !== undefined) requiredKeys.push('controlBinding');
|
|
201
213
|
if (
|
|
214
|
+
Object.keys(receipt).sort().join('\0') !== requiredKeys.sort().join('\0')
|
|
215
|
+
||
|
|
202
216
|
receipt.version !== RECEIPT_VERSION || receipt.taskId !== taskId || receipt.intent !== 'complete'
|
|
203
217
|
|| typeof receipt.receiptId !== 'string' || receipt.receiptId.length === 0
|
|
204
218
|
|| !Number.isSafeInteger(receipt.revision) || Number(receipt.revision) < 0
|
|
205
|
-
|| !states.includes(
|
|
206
|
-
|| !states.includes(
|
|
207
|
-
|| !states.includes(
|
|
208
|
-
|| !states.includes(
|
|
209
|
-
|| !['pending', 'done'].includes(
|
|
219
|
+
|| !states.includes(receipt.lifecycle as string)
|
|
220
|
+
|| !states.includes(receipt.taskComment as string)
|
|
221
|
+
|| !states.includes(receipt.verification as string)
|
|
222
|
+
|| !states.includes(receipt.summary as string)
|
|
223
|
+
|| !['pending', 'done'].includes(receipt.warningProjection as string)
|
|
210
224
|
|| !Array.isArray(receipt.warnings) || receipt.warnings.some((warning) => !validWarning(warning))
|
|
211
|
-
|| typeof receipt.updatedAt !== 'string'
|
|
212
|
-
|| (receipt.lastError !== null && (typeof receipt.lastError !== 'object' || Array.isArray(receipt.lastError)
|
|
225
|
+
|| typeof receipt.updatedAt !== 'string' || receipt.updatedAt.length === 0
|
|
226
|
+
|| (receipt.lastError !== null && (typeof receipt.lastError !== 'object' || Array.isArray(receipt.lastError)
|
|
227
|
+
|| Object.keys(receipt.lastError).sort().join('\0') !== ['code', 'message', 'retryable'].join('\0')
|
|
228
|
+
|| typeof (receipt.lastError as Record<string, unknown>).code !== 'string'
|
|
229
|
+
|| typeof (receipt.lastError as Record<string, unknown>).message !== 'string'
|
|
230
|
+
|| typeof (receipt.lastError as Record<string, unknown>).retryable !== 'boolean'))
|
|
213
231
|
|| (receipt.controlBinding !== undefined && (!controlBinding || typeof controlBinding !== 'object'
|
|
214
232
|
|| Array.isArray(controlBinding)
|
|
233
|
+
|| Object.keys(controlBinding).sort().join('\0') !== ['generation', 'requestId'].join('\0')
|
|
215
234
|
|| typeof controlBinding.generation !== 'string' || controlBinding.generation.length === 0
|
|
216
235
|
|| typeof controlBinding.requestId !== 'string' || !/^[a-f0-9-]{16,64}$/u.test(controlBinding.requestId)))
|
|
217
236
|
) throw new Error('receipt schema is invalid');
|
|
@@ -243,6 +262,14 @@ function writeReceipt(repoRoot: string, receipt: TaskFinalizationReceipt): void
|
|
|
243
262
|
}
|
|
244
263
|
}
|
|
245
264
|
|
|
265
|
+
export function bindTaskFinalizationReceipt(
|
|
266
|
+
repoRoot: string, taskId: string, controlBinding: Readonly<{ generation: string; requestId: string }>
|
|
267
|
+
): TaskFinalizationReceipt {
|
|
268
|
+
const receipt = readReceipt(path.resolve(repoRoot), taskId);
|
|
269
|
+
if (!receipt) throw new Error('TASK_FINALIZATION_PREPARATION_REQUIRED');
|
|
270
|
+
return updateReceipt(path.resolve(repoRoot), receipt, { controlBinding, lastError: null });
|
|
271
|
+
}
|
|
272
|
+
|
|
246
273
|
function updateReceipt(
|
|
247
274
|
repoRoot: string,
|
|
248
275
|
receipt: TaskFinalizationReceipt,
|
|
@@ -314,7 +341,7 @@ function verificationFailure(result: TaskVerificationResult): FinalizationError
|
|
|
314
341
|
}
|
|
315
342
|
|
|
316
343
|
function warningFromError(
|
|
317
|
-
step:
|
|
344
|
+
step: string,
|
|
318
345
|
error: FinalizationError,
|
|
319
346
|
severity: OperationWarningSeverity = 'ACTION_REQUIRED'
|
|
320
347
|
): OperationWarning {
|
|
@@ -357,6 +384,76 @@ function resolveStepWarnings(receipt: TaskFinalizationReceipt, step: Finalizatio
|
|
|
357
384
|
: warning);
|
|
358
385
|
}
|
|
359
386
|
|
|
387
|
+
function verificationWarnings(result: TaskVerificationResult): readonly OperationWarning[] {
|
|
388
|
+
const warnings: OperationWarning[] = [];
|
|
389
|
+
for (const invocation of result.invocations) {
|
|
390
|
+
const checks = Array.isArray(invocation.payload.checks) ? invocation.payload.checks : [invocation.payload];
|
|
391
|
+
for (const value of checks) {
|
|
392
|
+
if (!value || typeof value !== 'object' || Array.isArray(value)) continue;
|
|
393
|
+
const check = value as Record<string, unknown>;
|
|
394
|
+
const status = check.effectiveStatus ?? check.status;
|
|
395
|
+
const target = typeof check.checkId === 'string' && check.checkId ? check.checkId : typeof check.type === 'string' ? check.type : null;
|
|
396
|
+
if (!target || status === 'pass') continue;
|
|
397
|
+
const blocked = status === 'blocked';
|
|
398
|
+
const message = [check.reason, check.message, check.action]
|
|
399
|
+
.filter((item): item is string => typeof item === 'string' && item.length > 0)
|
|
400
|
+
.join(' - ');
|
|
401
|
+
warnings.push({
|
|
402
|
+
step: 'verification', target, code: blocked ? 'CHECK_BLOCKED' : 'CHECK_FAILED',
|
|
403
|
+
retryable: blocked, severity: 'ACTION_REQUIRED',
|
|
404
|
+
message: message || `verification ${String(status)}`
|
|
405
|
+
});
|
|
406
|
+
}
|
|
407
|
+
}
|
|
408
|
+
return mergeOperationWarnings(warnings);
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
function applyVerificationWarnings(
|
|
412
|
+
receipt: TaskFinalizationReceipt,
|
|
413
|
+
result: TaskVerificationResult
|
|
414
|
+
): readonly FinalizationWarning[] {
|
|
415
|
+
if (result.status === 'pass') return resolveStepWarnings(receipt, 'verification');
|
|
416
|
+
const observed = new Map<string, 'pass' | OperationWarning>();
|
|
417
|
+
for (const invocation of result.invocations) {
|
|
418
|
+
const checks = Array.isArray(invocation.payload.checks) ? invocation.payload.checks : [invocation.payload];
|
|
419
|
+
for (const value of checks) {
|
|
420
|
+
if (!value || typeof value !== 'object' || Array.isArray(value)) continue;
|
|
421
|
+
const check = value as Record<string, unknown>;
|
|
422
|
+
const target = typeof check.checkId === 'string' && check.checkId ? check.checkId : typeof check.type === 'string' ? check.type : null;
|
|
423
|
+
if (!target) continue;
|
|
424
|
+
const status = check.effectiveStatus ?? check.status;
|
|
425
|
+
if (status === 'pass') observed.set(target, 'pass');
|
|
426
|
+
}
|
|
427
|
+
}
|
|
428
|
+
for (const warning of verificationWarnings(result)) observed.set(warning.target, warning);
|
|
429
|
+
if (observed.size === 0) {
|
|
430
|
+
return replaceWarning(receipt, warningFromError('verification', verificationFailure(result)), 'open');
|
|
431
|
+
}
|
|
432
|
+
let warnings = [...receipt.warnings];
|
|
433
|
+
for (const [target, current] of observed) {
|
|
434
|
+
if (current === 'pass') {
|
|
435
|
+
warnings = warnings.map((warning) => warning.step === 'verification' && warning.target === target && warning.status === 'open'
|
|
436
|
+
? { ...warning, status: 'resolved', resolvedAt: now() }
|
|
437
|
+
: warning);
|
|
438
|
+
continue;
|
|
439
|
+
}
|
|
440
|
+
warnings = warnings.map((warning) => warning.step === 'verification' && warning.target === target
|
|
441
|
+
&& warning.status === 'open' && warning.code !== current.code
|
|
442
|
+
? { ...warning, status: 'resolved', resolvedAt: now() }
|
|
443
|
+
: warning);
|
|
444
|
+
const key = warningKey(current);
|
|
445
|
+
warnings = [...warnings.filter((warning) => warningKey(warning) !== key), warningRecord(current, 'open')];
|
|
446
|
+
}
|
|
447
|
+
return warnings;
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
function shouldRunBackfill(receipt: TaskFinalizationReceipt): boolean {
|
|
451
|
+
return receipt.lifecycle !== 'done'
|
|
452
|
+
|| receipt.taskComment === 'pending'
|
|
453
|
+
|| receipt.verification === 'pending'
|
|
454
|
+
|| receipt.warnings.some((warning) => warning.step === 'backfill' && warning.status === 'open');
|
|
455
|
+
}
|
|
456
|
+
|
|
360
457
|
function issueCapability(receipt: TaskFinalizationReceipt, scope: Exclude<FinalizationStep, 'lifecycle'> | 'warning-projection'): FinalizationCapability {
|
|
361
458
|
return {
|
|
362
459
|
receiptId: receipt.receiptId,
|
|
@@ -374,6 +471,15 @@ function capabilityError(code: 'FINALIZATION_CAPABILITY_STALE' | 'FINALIZATION_S
|
|
|
374
471
|
return error;
|
|
375
472
|
}
|
|
376
473
|
|
|
474
|
+
function hasPreparedTaskDocument(repoRoot: string, taskId: string): boolean {
|
|
475
|
+
const resolved = resolveTaskRef(taskId, { repoRoot });
|
|
476
|
+
if (!resolved.ok) return false;
|
|
477
|
+
if (resolved.state === 'completed') return true;
|
|
478
|
+
if (resolved.state !== 'active') return false;
|
|
479
|
+
try { return parseTaskFrontmatter(fs.readFileSync(resolved.taskMdPath, 'utf8')).status === 'completed'; }
|
|
480
|
+
catch { return false; }
|
|
481
|
+
}
|
|
482
|
+
|
|
377
483
|
function mutationKeys(value: FinalizationMutation): string[] {
|
|
378
484
|
return Object.keys(value).sort();
|
|
379
485
|
}
|
|
@@ -451,9 +557,8 @@ function applyFinalizationReceiptMutationUnderLock(
|
|
|
451
557
|
mutation: FinalizationMutation,
|
|
452
558
|
consumed: Set<string>
|
|
453
559
|
): TaskFinalizationReceipt {
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
throw capabilityError('FINALIZATION_SCOPE_INVALID', 'finalization capability requires a completed task');
|
|
560
|
+
if (!hasPreparedTaskDocument(repoRoot, receipt.taskId)) {
|
|
561
|
+
throw capabilityError('FINALIZATION_SCOPE_INVALID', 'finalization capability requires an active prepared task or a completed task');
|
|
457
562
|
}
|
|
458
563
|
const current = readReceipt(repoRoot, receipt.taskId);
|
|
459
564
|
if (!current || current.receiptId !== receipt.receiptId || current.revision !== receipt.revision) {
|
|
@@ -471,11 +576,10 @@ function applyFinalizationReceiptMutation(
|
|
|
471
576
|
capability: FinalizationCapability,
|
|
472
577
|
mutation: FinalizationMutation
|
|
473
578
|
): TaskFinalizationReceipt {
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
throw capabilityError('FINALIZATION_SCOPE_INVALID', 'finalization capability requires a completed task');
|
|
579
|
+
if (!hasPreparedTaskDocument(repoRoot, receipt.taskId)) {
|
|
580
|
+
throw capabilityError('FINALIZATION_SCOPE_INVALID', 'finalization capability requires an active prepared task or a completed task');
|
|
477
581
|
}
|
|
478
|
-
return withTaskExecutionLock(repoRoot,
|
|
582
|
+
return withTaskExecutionLock(repoRoot, receipt.taskId, 'task-finalization.receipt-mutation', () =>
|
|
479
583
|
applyFinalizationReceiptMutationUnderLock(repoRoot, receipt, capability, mutation, new Set<string>())
|
|
480
584
|
);
|
|
481
585
|
}
|
|
@@ -489,7 +593,10 @@ function reconcileWarningProjection(repoRoot: string, taskId: string, receipt: T
|
|
|
489
593
|
const warnings = [...new Map(receipt.warnings.map((warning) => [warningKey(warning), warning])).values()];
|
|
490
594
|
for (const warning of warnings) {
|
|
491
595
|
try {
|
|
492
|
-
const
|
|
596
|
+
const superseded = warning.status === 'resolved' && receipt.warnings.some((item) =>
|
|
597
|
+
item.status === 'open' && item.step === warning.step && item.target === warning.target && item.code !== warning.code
|
|
598
|
+
);
|
|
599
|
+
const projected = projectFinalizationWarning(taskId, warning, { repoRoot, resolution: superseded ? 'superseded' : 'passed' });
|
|
493
600
|
if (projected.status === 'failed') {
|
|
494
601
|
const detail: FinalizationError = {
|
|
495
602
|
code: projected.error?.code || 'FINALIZATION_WARNING_PROJECTION_FAILED',
|
|
@@ -595,7 +702,11 @@ async function syncPendingSummary(input: {
|
|
|
595
702
|
let { receipt } = input;
|
|
596
703
|
try {
|
|
597
704
|
const resolved = resolveTaskRef(input.taskId, { repoRoot: input.repoRoot });
|
|
598
|
-
if (!resolved.ok ||
|
|
705
|
+
if (!resolved.ok || !hasPreparedTaskDocument(input.repoRoot, input.taskId)) throw Object.assign(new Error('prepared task is unavailable for summary sync'), { code: 'SUMMARY_STAGING_INVALID' });
|
|
706
|
+
if (!fs.existsSync(path.join(resolved.taskDir, '.delivery-summary.json'))) {
|
|
707
|
+
receipt = updateReceipt(input.repoRoot, receipt, { summary: 'skipped', lastError: null });
|
|
708
|
+
return { receipt, step: { status: 'skipped', changed: false, error: null }, changed: false, error: null };
|
|
709
|
+
}
|
|
599
710
|
if (!taskIssueIdentity(parseTaskFrontmatter(fs.readFileSync(resolved.taskMdPath, 'utf8')))) {
|
|
600
711
|
receipt = updateReceipt(input.repoRoot, receipt, {
|
|
601
712
|
summary: 'skipped', lastError: null
|
|
@@ -676,7 +787,25 @@ function terminalResult(
|
|
|
676
787
|
};
|
|
677
788
|
}
|
|
678
789
|
|
|
679
|
-
|
|
790
|
+
function preparedResult(
|
|
791
|
+
taskId: string,
|
|
792
|
+
receipt: TaskFinalizationReceipt,
|
|
793
|
+
steps: Partial<Pick<TaskFinalizationResult, 'backfill' | 'lifecycle' | 'taskComment' | 'verification' | 'summary'>>,
|
|
794
|
+
changed: boolean
|
|
795
|
+
): TaskFinalizationResult {
|
|
796
|
+
return {
|
|
797
|
+
status: 'prepared', changed, taskId,
|
|
798
|
+
backfill: steps.backfill ?? null,
|
|
799
|
+
lifecycle: steps.lifecycle ?? null,
|
|
800
|
+
taskComment: steps.taskComment ?? null,
|
|
801
|
+
verification: steps.verification ?? null,
|
|
802
|
+
summary: steps.summary ?? null,
|
|
803
|
+
completedSteps: completedSteps(receipt), pendingSteps: pendingSteps(receipt),
|
|
804
|
+
result: 'prepared', warnings: openWarnings(receipt), error: null
|
|
805
|
+
};
|
|
806
|
+
}
|
|
807
|
+
|
|
808
|
+
async function prepareUnderLock(
|
|
680
809
|
request: TaskFinalizationRequest,
|
|
681
810
|
taskId: string,
|
|
682
811
|
options: TaskFinalizationOptions
|
|
@@ -696,10 +825,16 @@ async function applyUnderLock(
|
|
|
696
825
|
} catch (error) {
|
|
697
826
|
return failed(taskId, errorOf(error, 'TASK_FINALIZATION_RECEIPT_INVALID'));
|
|
698
827
|
}
|
|
699
|
-
if (options.controlBinding && (
|
|
700
|
-
receipt.controlBinding
|
|
701
|
-
|
|
702
|
-
|
|
828
|
+
if (options.controlBinding && (receipt.controlBinding?.generation !== options.controlBinding.generation
|
|
829
|
+
|| receipt.controlBinding.requestId !== options.controlBinding.requestId)) {
|
|
830
|
+
const state = resolveTaskRef(taskId, { repoRoot });
|
|
831
|
+
if (!(receipt.lifecycle === 'done' && state.ok && state.state === 'completed')) {
|
|
832
|
+
return failed(taskId, {
|
|
833
|
+
code: 'TASK_FINALIZATION_RECOVERY_PROOF_UNAVAILABLE',
|
|
834
|
+
message: 'canonical receipt is not bound to the current sandbox request', retryable: false
|
|
835
|
+
});
|
|
836
|
+
}
|
|
837
|
+
}
|
|
703
838
|
|
|
704
839
|
const preflightState = resolveTaskRef(taskId, { repoRoot });
|
|
705
840
|
if (options.preflight && preflightState.ok && preflightState.state === 'active') {
|
|
@@ -725,7 +860,18 @@ async function applyUnderLock(
|
|
|
725
860
|
|
|
726
861
|
let changed = false;
|
|
727
862
|
let backfillResult: TaskFinalizationStep | null = null;
|
|
728
|
-
|
|
863
|
+
let runBackfill = shouldRunBackfill(receipt);
|
|
864
|
+
if (!runBackfill && receipt.lifecycle === 'done') {
|
|
865
|
+
const eligibility = inspectCompletionBackfillEligibility(taskId, { cwd: repoRoot });
|
|
866
|
+
if (eligibility.status === 'failed') return failed(taskId, eligibility.error!, {
|
|
867
|
+
completedSteps: completedSteps(receipt), pendingSteps: pendingSteps(receipt), warnings: openWarnings(receipt)
|
|
868
|
+
});
|
|
869
|
+
runBackfill = eligibility.eligible;
|
|
870
|
+
}
|
|
871
|
+
if (receipt.lifecycle === 'done' && runBackfill && receipt.summary === 'done') {
|
|
872
|
+
receipt = updateReceipt(repoRoot, receipt, { summary: 'pending' });
|
|
873
|
+
}
|
|
874
|
+
if (runBackfill) try {
|
|
729
875
|
const result = await backfill(taskId, { agent: request.agent, cwd: repoRoot });
|
|
730
876
|
backfillResult = commentStep(result);
|
|
731
877
|
const issueNotLinked = result.status === 'no-op' && result.error?.code === 'ISSUE_NOT_LINKED';
|
|
@@ -733,66 +879,57 @@ async function applyUnderLock(
|
|
|
733
879
|
const detail = backfillResult.error ?? {
|
|
734
880
|
code: 'COMPLETION_BACKFILL_FAILED', message: 'completion artifact backfill did not reach a successful terminal result', retryable: true
|
|
735
881
|
};
|
|
736
|
-
return failed(taskId, detail, {
|
|
882
|
+
if (receipt.lifecycle !== 'done') return failed(taskId, detail, {
|
|
737
883
|
backfill: backfillResult,
|
|
738
884
|
completedSteps: completedSteps(receipt),
|
|
739
885
|
pendingSteps: pendingSteps(receipt)
|
|
740
886
|
});
|
|
887
|
+
const warnings = replaceWarning(receipt, warningFromError('backfill', detail), 'open');
|
|
888
|
+
receipt = updateReceipt(repoRoot, receipt, { warningProjection: 'pending', warnings, lastError: detail });
|
|
889
|
+
receipt = reconcileWarningProjection(repoRoot, taskId, receipt, consumedCapabilities);
|
|
890
|
+
if (!detail.retryable) return failed(taskId, detail, {
|
|
891
|
+
changed, backfill: backfillResult, completedSteps: completedSteps(receipt),
|
|
892
|
+
pendingSteps: pendingSteps(receipt), warnings: openWarnings(receipt)
|
|
893
|
+
});
|
|
894
|
+
return terminalResult(taskId, receipt, { backfill: backfillResult }, changed, detail);
|
|
741
895
|
}
|
|
742
896
|
changed = result.changed;
|
|
897
|
+
if (receipt.warnings.some((warning) => warning.step === 'backfill' && warning.status === 'open')) {
|
|
898
|
+
const warnings = receipt.warnings.map((warning) => warning.step === 'backfill' && warning.status === 'open'
|
|
899
|
+
? { ...warning, status: 'resolved' as const, resolvedAt: now() }
|
|
900
|
+
: warning);
|
|
901
|
+
receipt = updateReceipt(repoRoot, receipt, { warningProjection: 'pending', warnings, lastError: null });
|
|
902
|
+
}
|
|
743
903
|
if (result.changed && receipt.taskComment !== 'pending') {
|
|
744
904
|
receipt = updateReceipt(repoRoot, receipt, { taskComment: 'pending' });
|
|
745
905
|
}
|
|
746
906
|
} catch (error) {
|
|
747
907
|
const detail = errorOf(error, 'COMPLETION_BACKFILL_FAILED', true);
|
|
908
|
+
if (receipt.lifecycle === 'done') {
|
|
909
|
+
const warnings = replaceWarning(receipt, warningFromError('backfill', detail), 'open');
|
|
910
|
+
receipt = updateReceipt(repoRoot, receipt, { warningProjection: 'pending', warnings, lastError: detail });
|
|
911
|
+
receipt = reconcileWarningProjection(repoRoot, taskId, receipt, consumedCapabilities);
|
|
912
|
+
return terminalResult(taskId, receipt, {
|
|
913
|
+
backfill: { status: 'blocked', changed: false, error: detail }
|
|
914
|
+
}, changed, detail);
|
|
915
|
+
}
|
|
748
916
|
return failed(taskId, detail, {
|
|
749
917
|
backfill: { status: detail.retryable ? 'blocked' : 'failed', changed: false, error: detail },
|
|
750
918
|
completedSteps: completedSteps(receipt),
|
|
751
919
|
pendingSteps: pendingSteps(receipt)
|
|
752
920
|
});
|
|
753
|
-
}
|
|
921
|
+
} else backfillResult = { status: 'no-op', changed: false, error: null };
|
|
754
922
|
|
|
755
923
|
let lifecycleResult: TaskFinalizationStep | null = null;
|
|
756
924
|
try {
|
|
757
925
|
const resolved = resolveTaskRef(taskId, { repoRoot });
|
|
758
|
-
|
|
759
|
-
if (registry && registry.status !== 'valid') {
|
|
760
|
-
const detail: FinalizationError = {
|
|
761
|
-
code: 'TASK_FINALIZATION_SHORT_ID_REGISTRY_UNAVAILABLE',
|
|
762
|
-
message: `cannot verify canonical short-id registry: ${registry.error.code}: ${registry.error.message}`,
|
|
763
|
-
retryable: false
|
|
764
|
-
};
|
|
765
|
-
receipt = updateReceipt(repoRoot, receipt, { lifecycle: 'pending', lastError: detail });
|
|
766
|
-
return terminalResult(taskId, receipt, {
|
|
767
|
-
backfill: backfillResult,
|
|
768
|
-
lifecycle: { status: 'failed', changed: false, error: detail },
|
|
769
|
-
taskComment: null,
|
|
770
|
-
verification: null
|
|
771
|
-
}, changed, detail);
|
|
772
|
-
}
|
|
773
|
-
const shortIds = registry?.status === 'valid' ? registry.shortIds : null;
|
|
774
|
-
if (shortIds?.has(taskId)) {
|
|
775
|
-
const detail: FinalizationError = {
|
|
776
|
-
code: 'TASK_FINALIZATION_CANONICAL_STATE_INVALID',
|
|
777
|
-
message: 'completed task still has an active short-id registry entry',
|
|
778
|
-
retryable: false
|
|
779
|
-
};
|
|
780
|
-
receipt = updateReceipt(repoRoot, receipt, { lifecycle: 'pending', lastError: detail });
|
|
781
|
-
return terminalResult(taskId, receipt, {
|
|
782
|
-
backfill: backfillResult,
|
|
783
|
-
lifecycle: { status: 'failed', changed: false, error: detail },
|
|
784
|
-
taskComment: null,
|
|
785
|
-
verification: null
|
|
786
|
-
}, changed, detail);
|
|
787
|
-
}
|
|
788
|
-
const lifecycleDone = receipt.lifecycle === 'done' && resolved.ok && resolved.state === 'completed' && shortIds !== null;
|
|
789
|
-
if (lifecycleDone) {
|
|
926
|
+
if (receipt.lifecycle === 'done' && resolved.ok && resolved.state === 'completed') {
|
|
790
927
|
lifecycleResult = { status: 'no-op', changed: false, error: null };
|
|
791
928
|
receipt = updateReceipt(repoRoot, receipt, { lifecycle: 'done', lastError: null });
|
|
792
929
|
} else {
|
|
793
930
|
const result = lifecycle(
|
|
794
931
|
{ taskRef: taskId, intent: 'complete', agent: request.agent },
|
|
795
|
-
{ repoRoot, ...(options.metadataProvider ? { metadataProvider: options.metadataProvider } : {}) }
|
|
932
|
+
{ repoRoot, prepareOnly: true, ...(options.metadataProvider ? { metadataProvider: options.metadataProvider } : {}) }
|
|
796
933
|
);
|
|
797
934
|
lifecycleResult = lifecycleStep(result);
|
|
798
935
|
if (result.status !== 'applied' && result.status !== 'no-op') {
|
|
@@ -800,7 +937,6 @@ async function applyUnderLock(
|
|
|
800
937
|
return terminalResult(taskId, receipt, { backfill: backfillResult, lifecycle: lifecycleResult, taskComment: null, verification: null }, result.changed, lifecycleResult.error);
|
|
801
938
|
}
|
|
802
939
|
changed = changed || result.changed;
|
|
803
|
-
receipt = updateReceipt(repoRoot, receipt, { lifecycle: 'done', lastError: null });
|
|
804
940
|
}
|
|
805
941
|
} catch (error) {
|
|
806
942
|
const detail = errorOf(error, 'TASK_FINALIZATION_LIFECYCLE_FAILED');
|
|
@@ -810,79 +946,64 @@ async function applyUnderLock(
|
|
|
810
946
|
|
|
811
947
|
let taskComment: TaskFinalizationStep | null = null;
|
|
812
948
|
receipt = reconcileWarningProjection(repoRoot, taskId, receipt, consumedCapabilities);
|
|
813
|
-
const initialComment = await syncPendingTaskComment({
|
|
814
|
-
repoRoot, taskId, agent: request.agent, receipt, commentSync, consumedCapabilities
|
|
815
|
-
});
|
|
816
|
-
receipt = initialComment.receipt;
|
|
817
|
-
taskComment = initialComment.step;
|
|
818
|
-
changed = changed || initialComment.changed;
|
|
819
|
-
if (initialComment.error) {
|
|
820
|
-
return terminalResult(taskId, receipt, { backfill: backfillResult, lifecycle: lifecycleResult, taskComment, verification: null }, changed, initialComment.error);
|
|
821
|
-
}
|
|
822
949
|
|
|
823
950
|
let verification: TaskFinalizationStep | null = null;
|
|
824
|
-
|
|
951
|
+
if (receipt.verification !== 'pending') {
|
|
952
|
+
verification = { status: receipt.verification === 'skipped' ? 'skipped' : 'no-op', changed: false, error: null };
|
|
953
|
+
} else {
|
|
954
|
+
try {
|
|
825
955
|
receipt = reconcileWarningProjection(repoRoot, taskId, receipt, consumedCapabilities);
|
|
826
|
-
const
|
|
956
|
+
const verificationState = resolveTaskRef(taskId, { repoRoot });
|
|
957
|
+
const verificationEvent = verificationState.ok && verificationState.state === 'completed'
|
|
958
|
+
? 'complete-task.completed'
|
|
959
|
+
: 'complete-task.prepared';
|
|
827
960
|
const result = await verify(
|
|
828
|
-
{ taskRef: taskId, event:
|
|
961
|
+
{ taskRef: taskId, event: verificationEvent },
|
|
829
962
|
{ repoRoot }
|
|
830
963
|
);
|
|
831
964
|
verification = verificationStep(result);
|
|
832
965
|
if (result.status === 'pass') {
|
|
833
|
-
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
|
|
844
|
-
|
|
845
|
-
|
|
846
|
-
|
|
847
|
-
}
|
|
848
|
-
} else {
|
|
849
|
-
receipt = updateReceipt(repoRoot, receipt, { lastError: null });
|
|
966
|
+
const resolvedTaskWarning = receipt.warnings.some((warning) => warning.step === 'verification' && warning.status === 'open');
|
|
967
|
+
const warnings = applyVerificationWarnings(receipt, result);
|
|
968
|
+
receipt = updateReceipt(repoRoot, receipt, {
|
|
969
|
+
verification: 'done', taskComment: resolvedTaskWarning ? 'pending' : receipt.taskComment,
|
|
970
|
+
warningProjection: warnings.length > 0 ? 'pending' : 'done', warnings, lastError: null
|
|
971
|
+
});
|
|
972
|
+
receipt = reconcileWarningProjection(repoRoot, taskId, receipt, consumedCapabilities);
|
|
973
|
+
const finalComment = await syncPendingTaskComment({
|
|
974
|
+
repoRoot, taskId, agent: request.agent, receipt, commentSync, consumedCapabilities
|
|
975
|
+
});
|
|
976
|
+
receipt = finalComment.receipt;
|
|
977
|
+
if (finalComment.step.status !== 'no-op') taskComment = finalComment.step;
|
|
978
|
+
changed = changed || finalComment.changed;
|
|
979
|
+
if (finalComment.error) {
|
|
980
|
+
return terminalResult(taskId, receipt, { backfill: backfillResult, lifecycle: lifecycleResult, taskComment, verification }, changed, finalComment.error);
|
|
850
981
|
}
|
|
851
982
|
} else {
|
|
852
983
|
const detail = verification.error ?? { code: 'VERIFY_FAILED', message: 'verification failed', retryable: true };
|
|
853
|
-
|
|
854
|
-
|
|
855
|
-
|
|
856
|
-
|
|
857
|
-
|
|
858
|
-
|
|
859
|
-
|
|
860
|
-
|
|
861
|
-
|
|
862
|
-
|
|
863
|
-
|
|
864
|
-
|
|
865
|
-
|
|
866
|
-
receipt = warningComment.receipt;
|
|
867
|
-
if (warningComment.step.status !== 'no-op') taskComment = warningComment.step;
|
|
868
|
-
changed = changed || warningComment.changed;
|
|
984
|
+
const warnings = applyVerificationWarnings(receipt, result);
|
|
985
|
+
receipt = updateReceipt(repoRoot, receipt, {
|
|
986
|
+
verification: 'pending', taskComment: 'pending', warningProjection: 'pending', warnings, lastError: detail
|
|
987
|
+
});
|
|
988
|
+
receipt = reconcileWarningProjection(repoRoot, taskId, receipt, consumedCapabilities);
|
|
989
|
+
const warningComment = await syncPendingTaskComment({
|
|
990
|
+
repoRoot, taskId, agent: request.agent, receipt, commentSync, consumedCapabilities
|
|
991
|
+
});
|
|
992
|
+
receipt = warningComment.receipt;
|
|
993
|
+
taskComment = warningComment.step;
|
|
994
|
+
changed = changed || warningComment.changed;
|
|
995
|
+
if (warningComment.error) {
|
|
996
|
+
return terminalResult(taskId, receipt, { backfill: backfillResult, lifecycle: lifecycleResult, taskComment, verification }, changed, warningComment.error);
|
|
869
997
|
}
|
|
870
998
|
return terminalResult(taskId, receipt, { backfill: backfillResult, lifecycle: lifecycleResult, taskComment, verification }, changed, detail);
|
|
871
999
|
}
|
|
872
1000
|
} catch (error) {
|
|
873
1001
|
const detail = errorOf(error, 'VERIFY_FAILED', true);
|
|
874
1002
|
try {
|
|
875
|
-
|
|
876
|
-
|
|
877
|
-
|
|
878
|
-
|
|
879
|
-
}, consumedCapabilities);
|
|
880
|
-
} else {
|
|
881
|
-
const warnings = replaceWarning(receipt, warningFromError('verification', detail), 'open');
|
|
882
|
-
receipt = updateReceipt(repoRoot, receipt, {
|
|
883
|
-
verification: 'pending', taskComment: 'pending', warningProjection: 'pending', warnings, lastError: detail
|
|
884
|
-
});
|
|
885
|
-
}
|
|
1003
|
+
const warnings = replaceWarning(receipt, warningFromError('verification', detail), 'open');
|
|
1004
|
+
receipt = updateReceipt(repoRoot, receipt, {
|
|
1005
|
+
verification: 'pending', taskComment: 'pending', warningProjection: 'pending', warnings, lastError: detail
|
|
1006
|
+
});
|
|
886
1007
|
receipt = reconcileWarningProjection(repoRoot, taskId, receipt, consumedCapabilities);
|
|
887
1008
|
} catch (writeError) {
|
|
888
1009
|
const persistence = errorOf(writeError, 'FINALIZATION_RECEIPT_WRITE_FAILED', true);
|
|
@@ -902,9 +1023,12 @@ async function applyUnderLock(
|
|
|
902
1023
|
changed = changed || warningComment.changed;
|
|
903
1024
|
} catch { /* preserve the primary error */ }
|
|
904
1025
|
return terminalResult(taskId, receipt, { backfill: backfillResult, lifecycle: lifecycleResult, taskComment, verification: { status: 'blocked', changed: false, error: detail } }, changed, detail);
|
|
1026
|
+
}
|
|
905
1027
|
}
|
|
906
1028
|
|
|
907
|
-
let summary =
|
|
1029
|
+
let summary = receipt.summary === 'pending'
|
|
1030
|
+
? await syncPendingSummary({ repoRoot, taskId, agent: request.agent, receipt, commentSync })
|
|
1031
|
+
: { receipt, step: { status: 'no-op', changed: false, error: null }, changed: false, error: null };
|
|
908
1032
|
receipt = summary.receipt;
|
|
909
1033
|
changed = changed || summary.changed;
|
|
910
1034
|
if (summary.error) {
|
|
@@ -912,10 +1036,21 @@ async function applyUnderLock(
|
|
|
912
1036
|
return terminalResult(taskId, receipt, { backfill: backfillResult, lifecycle: lifecycleResult, taskComment, verification, summary: summary.step }, changed, summary.error);
|
|
913
1037
|
}
|
|
914
1038
|
receipt = reconcileWarningProjection(repoRoot, taskId, receipt, consumedCapabilities);
|
|
915
|
-
|
|
1039
|
+
if (receipt.taskComment === 'pending') {
|
|
1040
|
+
const finalComment = await syncPendingTaskComment({
|
|
1041
|
+
repoRoot, taskId, agent: request.agent, receipt, commentSync, consumedCapabilities
|
|
1042
|
+
});
|
|
1043
|
+
receipt = finalComment.receipt;
|
|
1044
|
+
taskComment = finalComment.step;
|
|
1045
|
+
changed = changed || finalComment.changed;
|
|
1046
|
+
if (finalComment.error) {
|
|
1047
|
+
return terminalResult(taskId, receipt, { backfill: backfillResult, lifecycle: lifecycleResult, taskComment, verification, summary: summary.step }, changed, finalComment.error);
|
|
1048
|
+
}
|
|
1049
|
+
}
|
|
1050
|
+
return preparedResult(taskId, receipt, { backfill: backfillResult, lifecycle: lifecycleResult, taskComment, verification, summary: summary.step }, changed);
|
|
916
1051
|
}
|
|
917
1052
|
|
|
918
|
-
async function
|
|
1053
|
+
async function prepareTaskFinalization(request: TaskFinalizationRequest, options: TaskFinalizationOptions): Promise<TaskFinalizationResult> {
|
|
919
1054
|
if (request.intent !== 'complete' || !request.taskRef || !request.agent) {
|
|
920
1055
|
return failed(null, { code: 'TASK_FINALIZATION_PAYLOAD_INVALID', message: 'complete finalization requires taskRef and agent', retryable: false });
|
|
921
1056
|
}
|
|
@@ -923,7 +1058,7 @@ async function applyTaskFinalization(request: TaskFinalizationRequest, options:
|
|
|
923
1058
|
const resolved = resolveTaskRef(request.taskRef, { repoRoot });
|
|
924
1059
|
if (!resolved.ok) return failed(resolved.taskId, { code: resolved.code, message: resolved.message, retryable: false });
|
|
925
1060
|
try {
|
|
926
|
-
return await withTaskExecutionLock(repoRoot, resolved.taskId, 'task-finalization.
|
|
1061
|
+
return await withTaskExecutionLock(repoRoot, resolved.taskId, 'task-finalization.prepare', () => prepareUnderLock(request, resolved.taskId, options));
|
|
927
1062
|
} catch (error) {
|
|
928
1063
|
const detail = error instanceof TaskExecutionLockError
|
|
929
1064
|
? { code: error.code, message: error.message, retryable: error.code === 'ORCHESTRATION_LOCK_BUSY' }
|
|
@@ -932,9 +1067,140 @@ async function applyTaskFinalization(request: TaskFinalizationRequest, options:
|
|
|
932
1067
|
}
|
|
933
1068
|
}
|
|
934
1069
|
|
|
1070
|
+
async function commitPreparedTaskFinalization(request: TaskFinalizationRequest, options: TaskFinalizationOptions): Promise<TaskFinalizationResult> {
|
|
1071
|
+
if (request.intent !== 'complete' || !request.taskRef || !request.agent) {
|
|
1072
|
+
return failed(null, { code: 'TASK_FINALIZATION_PAYLOAD_INVALID', message: 'complete finalization requires taskRef and agent', retryable: false });
|
|
1073
|
+
}
|
|
1074
|
+
const repoRoot = path.resolve(options.repoRoot);
|
|
1075
|
+
const resolved = resolveTaskRef(request.taskRef, { repoRoot });
|
|
1076
|
+
if (!resolved.ok) return failed(resolved.taskId, { code: resolved.code, message: resolved.message, retryable: false });
|
|
1077
|
+
try {
|
|
1078
|
+
return await withTaskExecutionLock(repoRoot, resolved.taskId, 'task-finalization.commit', () => {
|
|
1079
|
+
if (options.controlBinding && options.manifestPath) {
|
|
1080
|
+
try {
|
|
1081
|
+
const manifest = readSandboxControlManifest(options.manifestPath);
|
|
1082
|
+
const identity = validateSandboxControlIdentity({
|
|
1083
|
+
publicStatusDir: manifest.publicStatusDir,
|
|
1084
|
+
root: path.dirname(path.resolve(options.manifestPath)),
|
|
1085
|
+
mode: manifest.mode,
|
|
1086
|
+
taskId: manifest.taskId,
|
|
1087
|
+
generation: manifest.generation,
|
|
1088
|
+
controlRootId: manifest.controlRootId
|
|
1089
|
+
});
|
|
1090
|
+
if (identity.state !== 'valid' || manifest.taskId !== resolved.taskId
|
|
1091
|
+
|| manifest.generation !== options.controlBinding.generation
|
|
1092
|
+
|| fs.realpathSync.native(manifest.repoRoot) !== fs.realpathSync.native(repoRoot)) {
|
|
1093
|
+
throw new Error('current sandbox manifest does not match finalization request');
|
|
1094
|
+
}
|
|
1095
|
+
} catch (error) {
|
|
1096
|
+
return failed(resolved.taskId, {
|
|
1097
|
+
code: 'TASK_FINALIZATION_RECOVERY_PROOF_UNAVAILABLE',
|
|
1098
|
+
message: error instanceof Error ? error.message : String(error), retryable: false
|
|
1099
|
+
});
|
|
1100
|
+
}
|
|
1101
|
+
}
|
|
1102
|
+
let receipt = readReceipt(repoRoot, resolved.taskId);
|
|
1103
|
+
if (!receipt && options.controlBinding && options.handoffDirectory && request.handoffSha256) {
|
|
1104
|
+
try {
|
|
1105
|
+
const handoff = readTaskFinalizationHandoff(
|
|
1106
|
+
options.handoffDirectory, resolved.taskId, options.controlBinding, request.handoffSha256
|
|
1107
|
+
);
|
|
1108
|
+
receipt = validateReceipt(handoff.receipt, resolved.taskId);
|
|
1109
|
+
if (receipt.lifecycle !== 'pending') throw new Error('handoff lifecycle must be pending');
|
|
1110
|
+
if (inspectTaskLifecycleProgress(repoRoot, resolved.taskId, request.agent) === 'unknown') {
|
|
1111
|
+
throw new Error('TASK_FINALIZATION_RECOVERY_PROOF_UNAVAILABLE');
|
|
1112
|
+
}
|
|
1113
|
+
writeReceipt(repoRoot, receipt);
|
|
1114
|
+
try { handoff.cleanup(); } catch { /* keep the persisted canonical receipt usable */ }
|
|
1115
|
+
} catch (error) {
|
|
1116
|
+
return failed(resolved.taskId, {
|
|
1117
|
+
code: 'TASK_FINALIZATION_HANDOFF_INVALID',
|
|
1118
|
+
message: error instanceof Error ? error.message : String(error), retryable: false
|
|
1119
|
+
});
|
|
1120
|
+
}
|
|
1121
|
+
}
|
|
1122
|
+
if (!receipt) return failed(resolved.taskId, {
|
|
1123
|
+
code: 'TASK_FINALIZATION_PREPARATION_REQUIRED', message: 'sandbox preparation receipt is unavailable', retryable: true
|
|
1124
|
+
});
|
|
1125
|
+
if (receipt.lifecycle === 'done' && resolved.state === 'completed') {
|
|
1126
|
+
return terminalResult(resolved.taskId, receipt, { lifecycle: { status: 'no-op', changed: false, error: null } }, false);
|
|
1127
|
+
}
|
|
1128
|
+
if (options.controlBinding) {
|
|
1129
|
+
const proof = inspectTaskLifecycleProgress(repoRoot, resolved.taskId, request.agent);
|
|
1130
|
+
const oldBinding = receipt.controlBinding;
|
|
1131
|
+
if (!oldBinding || oldBinding.generation !== options.controlBinding.generation || proof === 'unknown') {
|
|
1132
|
+
return failed(resolved.taskId, {
|
|
1133
|
+
code: 'TASK_FINALIZATION_RECOVERY_PROOF_UNAVAILABLE',
|
|
1134
|
+
message: 'canonical receipt has no valid host recovery proof', retryable: false
|
|
1135
|
+
});
|
|
1136
|
+
}
|
|
1137
|
+
if (oldBinding.requestId !== options.controlBinding.requestId && proof === 'not-started') {
|
|
1138
|
+
if (receipt.lifecycle !== 'pending' || !options.handoffDirectory || !request.handoffSha256) {
|
|
1139
|
+
return failed(resolved.taskId, {
|
|
1140
|
+
code: 'TASK_FINALIZATION_RECOVERY_PROOF_UNAVAILABLE',
|
|
1141
|
+
message: 'pending receipt cannot be rebound without a current handoff', retryable: false
|
|
1142
|
+
});
|
|
1143
|
+
}
|
|
1144
|
+
try {
|
|
1145
|
+
const handoff = readTaskFinalizationHandoff(
|
|
1146
|
+
options.handoffDirectory, resolved.taskId, options.controlBinding, request.handoffSha256
|
|
1147
|
+
);
|
|
1148
|
+
const nextReceipt = validateReceipt(handoff.receipt, resolved.taskId);
|
|
1149
|
+
if (nextReceipt.receiptId !== receipt.receiptId || nextReceipt.intent !== receipt.intent
|
|
1150
|
+
|| nextReceipt.lifecycle !== 'pending') throw new Error('handoff is not continuous with canonical receipt');
|
|
1151
|
+
receipt = updateReceipt(repoRoot, receipt, { controlBinding: options.controlBinding });
|
|
1152
|
+
try { handoff.cleanup(); } catch { /* keep the persisted canonical receipt usable */ }
|
|
1153
|
+
} catch (error) {
|
|
1154
|
+
return failed(resolved.taskId, {
|
|
1155
|
+
code: 'TASK_FINALIZATION_HANDOFF_INVALID',
|
|
1156
|
+
message: error instanceof Error ? error.message : String(error), retryable: false
|
|
1157
|
+
});
|
|
1158
|
+
}
|
|
1159
|
+
}
|
|
1160
|
+
}
|
|
1161
|
+
if (receipt.taskComment === 'pending' || receipt.verification === 'pending' || receipt.summary === 'pending' || receipt.warningProjection === 'pending') {
|
|
1162
|
+
return failed(resolved.taskId, {
|
|
1163
|
+
code: 'TASK_FINALIZATION_PREPARATION_REQUIRED', message: 'sandbox preparation has pending external steps', retryable: true
|
|
1164
|
+
}, { completedSteps: completedSteps(receipt), pendingSteps: pendingSteps(receipt), warnings: openWarnings(receipt) });
|
|
1165
|
+
}
|
|
1166
|
+
const registry = inspectShortIdRegistry(repoRoot);
|
|
1167
|
+
if (registry.status !== 'valid') return failed(resolved.taskId, {
|
|
1168
|
+
code: 'TASK_FINALIZATION_SHORT_ID_REGISTRY_UNAVAILABLE',
|
|
1169
|
+
message: `cannot verify canonical short-id registry: ${registry.error.code}: ${registry.error.message}`,
|
|
1170
|
+
retryable: false
|
|
1171
|
+
});
|
|
1172
|
+
const lifecycle = options.lifecycle ?? applyTaskLifecycle;
|
|
1173
|
+
const result = lifecycle(
|
|
1174
|
+
{ taskRef: resolved.taskId, intent: 'complete', agent: request.agent },
|
|
1175
|
+
{ repoRoot, ...(options.metadataProvider ? { metadataProvider: options.metadataProvider } : {}) }
|
|
1176
|
+
);
|
|
1177
|
+
const step = lifecycleStep(result);
|
|
1178
|
+
if (result.status !== 'applied' && result.status !== 'no-op') {
|
|
1179
|
+
const updated = updateReceipt(repoRoot, receipt, { lifecycle: 'pending', lastError: step.error });
|
|
1180
|
+
return terminalResult(resolved.taskId, updated, { lifecycle: step }, result.changed, step.error);
|
|
1181
|
+
}
|
|
1182
|
+
const updated = updateReceipt(repoRoot, receipt, { lifecycle: 'done', lastError: null });
|
|
1183
|
+
return terminalResult(resolved.taskId, updated, { lifecycle: step }, result.changed);
|
|
1184
|
+
});
|
|
1185
|
+
} catch (error) {
|
|
1186
|
+
const detail = error instanceof TaskExecutionLockError
|
|
1187
|
+
? { code: error.code, message: error.message, retryable: error.code === 'ORCHESTRATION_LOCK_BUSY' }
|
|
1188
|
+
: errorOf(error, 'TASK_FINALIZATION_FAILED');
|
|
1189
|
+
return failed(resolved.taskId, detail);
|
|
1190
|
+
}
|
|
1191
|
+
}
|
|
1192
|
+
|
|
1193
|
+
async function applyTaskFinalization(request: TaskFinalizationRequest, options: TaskFinalizationOptions): Promise<TaskFinalizationResult> {
|
|
1194
|
+
const prepared = await prepareTaskFinalization(request, options);
|
|
1195
|
+
if (prepared.status !== 'prepared') return prepared;
|
|
1196
|
+
return commitPreparedTaskFinalization(request, options);
|
|
1197
|
+
}
|
|
1198
|
+
|
|
935
1199
|
export {
|
|
936
1200
|
applyFinalizationReceiptMutation,
|
|
1201
|
+
commitPreparedTaskFinalization,
|
|
937
1202
|
applyTaskFinalization,
|
|
1203
|
+
prepareTaskFinalization,
|
|
938
1204
|
issueCapability as createFinalizationCapability,
|
|
939
1205
|
readTaskFinalizationReceipt,
|
|
940
1206
|
terminalResult
|