@fitlab-ai/agent-infra 0.11.2 → 0.11.4
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 +7 -2
- package/dist/lib/agent-clients/adapter.js +3 -0
- package/dist/lib/agent-clients/adapters/codex-lifecycle/recovery.js +234 -0
- package/dist/lib/agent-clients/adapters/codex-lifecycle/sandbox-controller.js +8 -35
- package/dist/lib/agent-clients/adapters/codex-lifecycle/store.js +1 -34
- package/dist/lib/agent-clients/adapters/codex.js +10 -0
- package/dist/lib/internal/cli-route-inventory.js +1 -1
- package/dist/lib/internal/manual-validation.js +22 -27
- package/dist/lib/internal/sandbox-control.js +1 -43
- package/dist/lib/internal/task-artifact.js +1 -1
- package/dist/lib/internal/task-finalization.js +41 -1
- package/dist/lib/internal/task-ledger.js +13 -5
- package/dist/lib/internal/task-lifecycle.js +1 -1
- package/dist/lib/internal/task-operation-registry.js +7 -2
- package/dist/lib/platform/completion-backfill.js +63 -17
- package/dist/lib/platform/issue-comments.js +2 -31
- package/dist/lib/platform/pr-summary.js +7 -2
- package/dist/lib/sandbox/control/client.js +3 -44
- package/dist/lib/sandbox/control/executor.js +14 -79
- package/dist/lib/sandbox/control/protocol.js +5 -64
- package/dist/lib/sandbox/control/server.js +49 -343
- package/dist/lib/sandbox/control/state.js +1 -7
- package/dist/lib/sandbox/control/task-view.js +1 -2
- package/dist/lib/sandbox/task-cleanup.js +13 -20
- 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 +1 -26
- package/dist/lib/task/artifact-receipts.js +4 -1
- package/dist/lib/task/artifact-schema.js +1 -0
- 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 +37 -38
- package/dist/lib/task/control-recovery.js +3 -42
- package/dist/lib/task/decision-intents.js +12 -0
- package/dist/lib/task/delegation-receipts.js +2 -2
- package/dist/lib/task/delivery.js +1 -43
- package/dist/lib/task/events.js +80 -64
- package/dist/lib/task/finalization-artifacts.js +1 -3
- package/dist/lib/task/finalization-state.js +1 -1
- package/dist/lib/task/finalization.js +319 -234
- package/dist/lib/task/ledger-intents.js +157 -15
- package/dist/lib/task/lifecycle-path.js +75 -0
- package/dist/lib/task/lifecycle.js +11 -1
- package/dist/lib/task/local-artifact-finalization.js +1 -20
- package/dist/lib/task/manual-validation-receipt.js +8 -3
- package/dist/lib/task/orchestration.js +29 -13
- package/dist/lib/task/review-finalization.js +3 -16
- package/dist/lib/task/rework-intent.js +70 -4
- package/dist/lib/task/verification-engine.js +20 -30
- package/dist/lib/task/verification.js +1 -0
- package/dist/lib/task/workflow-warning-intents.js +21 -6
- package/dist/lib/task/workspace-snapshot.js +7 -2
- package/dist/lifecycle-build-manifest.json +3 -3
- package/lib/agent-clients/adapter.ts +46 -0
- package/lib/agent-clients/adapters/codex-lifecycle/recovery.ts +297 -0
- package/lib/agent-clients/adapters/codex-lifecycle/sandbox-controller.ts +13 -33
- package/lib/agent-clients/adapters/codex-lifecycle/store.ts +1 -44
- package/lib/agent-clients/adapters/codex.ts +10 -0
- package/lib/internal/cli-route-inventory.ts +1 -1
- package/lib/internal/manual-validation.ts +22 -26
- package/lib/internal/sandbox-control.ts +0 -42
- package/lib/internal/task-artifact.ts +1 -1
- package/lib/internal/task-finalization.ts +40 -1
- package/lib/internal/task-ledger.ts +12 -5
- package/lib/internal/task-lifecycle.ts +2 -2
- package/lib/internal/task-operation-registry.ts +8 -4
- package/lib/platform/completion-backfill.ts +78 -16
- package/lib/platform/issue-comments.ts +1 -35
- package/lib/platform/pr-summary.ts +10 -2
- package/lib/sandbox/control/client.ts +5 -68
- package/lib/sandbox/control/executor.ts +14 -81
- package/lib/sandbox/control/protocol.ts +7 -83
- package/lib/sandbox/control/server.ts +47 -362
- package/lib/sandbox/control/state.ts +1 -9
- package/lib/sandbox/control/task-view.ts +1 -2
- package/lib/sandbox/task-cleanup.ts +12 -20
- package/lib/task/artifact-command.ts +14 -4
- package/lib/task/artifact-lifecycle.ts +236 -85
- package/lib/task/artifact-operations.ts +1 -26
- package/lib/task/artifact-receipts.ts +4 -1
- package/lib/task/artifact-schema.ts +1 -0
- 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 +41 -48
- package/lib/task/control-recovery.ts +3 -43
- package/lib/task/decision-intents.ts +9 -0
- package/lib/task/delegation-receipts.ts +2 -2
- package/lib/task/delivery.ts +1 -40
- package/lib/task/events.ts +82 -64
- package/lib/task/finalization-artifacts.ts +1 -3
- package/lib/task/finalization-state.ts +1 -1
- package/lib/task/finalization.ts +310 -216
- package/lib/task/ledger-intents.ts +155 -15
- package/lib/task/lifecycle-path.ts +98 -0
- package/lib/task/lifecycle.ts +15 -2
- package/lib/task/local-artifact-finalization.ts +2 -25
- package/lib/task/manual-validation-receipt.ts +8 -2
- package/lib/task/orchestration.ts +56 -27
- package/lib/task/review-finalization.ts +3 -16
- package/lib/task/rework-intent.ts +75 -5
- package/lib/task/verification-engine.ts +19 -30
- package/lib/task/verification.ts +2 -1
- package/lib/task/workflow-warning-intents.ts +18 -6
- package/lib/task/workspace-snapshot.ts +3 -2
- package/package.json +1 -1
- package/templates/.agents/rules/lifecycle-orchestration.en.md +19 -5
- package/templates/.agents/rules/lifecycle-orchestration.zh-CN.md +19 -5
- 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 +11 -4
- package/templates/.agents/skills/analyze-task/SKILL.zh-CN.md +11 -4
- 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 +1 -1
- package/templates/.agents/skills/code-task/reference/report-template.zh-CN.md +1 -1
- package/templates/.agents/skills/complete-manual-validation/SKILL.en.md +2 -2
- package/templates/.agents/skills/complete-manual-validation/SKILL.zh-CN.md +2 -2
- package/templates/.agents/skills/complete-task/SKILL.en.md +7 -8
- package/templates/.agents/skills/complete-task/SKILL.zh-CN.md +7 -8
- 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-code/SKILL.en.md +5 -1
- package/templates/.agents/skills/review-code/SKILL.zh-CN.md +5 -1
- 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/run-manual-validation/SKILL.en.md +2 -2
- package/templates/.agents/skills/run-manual-validation/SKILL.zh-CN.md +2 -2
- package/templates/.agents/skills/run-task/SKILL.en.md +9 -8
- package/templates/.agents/skills/run-task/SKILL.zh-CN.md +9 -8
- 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/sandbox/control/finalization-response.js +0 -35
- package/dist/lib/sandbox/control/task-workflow.js +0 -74
- package/dist/lib/sandbox/control/workflow-executor.js +0 -142
- package/dist/lib/task/lifecycle-recovery.js +0 -549
- package/dist/lib/task/recovery-warning.js +0 -19
- package/dist/lib/task/workflow-command.js +0 -21
- package/dist/lib/task/workflow-dispatch.js +0 -213
- package/lib/sandbox/control/finalization-response.ts +0 -37
- package/lib/sandbox/control/task-workflow.ts +0 -95
- package/lib/sandbox/control/workflow-executor.ts +0 -156
- package/lib/task/lifecycle-recovery.ts +0 -691
- package/lib/task/recovery-warning.ts +0 -21
- package/lib/task/workflow-command.ts +0 -22
- package/lib/task/workflow-dispatch.ts +0 -214
package/lib/task/finalization.ts
CHANGED
|
@@ -4,6 +4,7 @@ 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, inspectCompletionBackfillEligibility } from '../platform/completion-backfill.ts';
|
|
7
8
|
import type { PlatformResult } from '../platform/types.ts';
|
|
8
9
|
import { taskIssueIdentity } from '../platform/task-identities.ts';
|
|
9
10
|
import { parseTaskFrontmatter } from './frontmatter.ts';
|
|
@@ -25,8 +26,8 @@ import {
|
|
|
25
26
|
type OperationWarningSeverity
|
|
26
27
|
} from './operation-outcome.ts';
|
|
27
28
|
|
|
28
|
-
const RECEIPT_VERSION =
|
|
29
|
-
const FINALIZATION_STEPS = ['lifecycle', 'task-comment', 'verification', 'summary'
|
|
29
|
+
const RECEIPT_VERSION = 4 as const;
|
|
30
|
+
const FINALIZATION_STEPS = ['lifecycle', 'task-comment', 'verification', 'summary'] as const;
|
|
30
31
|
type FinalizationStep = typeof FINALIZATION_STEPS[number];
|
|
31
32
|
type FinalizationStepState = 'pending' | 'done' | 'skipped';
|
|
32
33
|
type FinalizationError = { code: string; message: string; retryable: boolean };
|
|
@@ -61,6 +62,7 @@ type TaskFinalizationOptions = Readonly<{
|
|
|
61
62
|
controlBinding?: Readonly<{ generation: string; requestId: string }>;
|
|
62
63
|
metadataProvider?: TaskLifecycleOptions['metadataProvider'];
|
|
63
64
|
lifecycle?: typeof applyTaskLifecycle;
|
|
65
|
+
backfill?: typeof backfillCompletionComments;
|
|
64
66
|
commentSync?: typeof syncPlatformComment;
|
|
65
67
|
verify?: typeof verifyTaskEvent;
|
|
66
68
|
preflight?: typeof verifyTaskEvent;
|
|
@@ -76,10 +78,8 @@ type TaskFinalizationReceipt = Readonly<{
|
|
|
76
78
|
taskComment: FinalizationStepState;
|
|
77
79
|
verification: FinalizationStepState;
|
|
78
80
|
summary: FinalizationStepState;
|
|
79
|
-
postSummaryVerification: FinalizationStepState;
|
|
80
81
|
warningProjection: WarningProjectionState;
|
|
81
82
|
warnings: readonly FinalizationWarning[];
|
|
82
|
-
summarySha256: string | null;
|
|
83
83
|
controlBinding?: Readonly<{ generation: string; requestId: string }>;
|
|
84
84
|
updatedAt: string;
|
|
85
85
|
lastError: FinalizationError | null;
|
|
@@ -101,17 +101,17 @@ type TaskCommentSyncOutcome = Readonly<{
|
|
|
101
101
|
}>;
|
|
102
102
|
|
|
103
103
|
type TaskFinalizationResult = Readonly<{
|
|
104
|
-
status: 'completed' | 'failed' | 'blocked';
|
|
104
|
+
status: 'prepared' | 'completed' | 'failed' | 'blocked';
|
|
105
105
|
changed: boolean;
|
|
106
106
|
taskId: string | null;
|
|
107
|
+
backfill: TaskFinalizationStep | null;
|
|
107
108
|
lifecycle: TaskFinalizationStep | null;
|
|
108
109
|
taskComment: TaskFinalizationStep | null;
|
|
109
110
|
verification: TaskFinalizationStep | null;
|
|
110
111
|
summary: TaskFinalizationStep | null;
|
|
111
|
-
postSummaryVerification: TaskFinalizationStep | null;
|
|
112
112
|
completedSteps: readonly FinalizationStep[];
|
|
113
113
|
pendingSteps: readonly FinalizationStep[];
|
|
114
|
-
result: 'completed' | 'completed_with_warnings' | 'failed' | 'blocked';
|
|
114
|
+
result: 'prepared' | 'completed' | 'completed_with_warnings' | 'failed' | 'blocked';
|
|
115
115
|
warnings: readonly OperationWarning[];
|
|
116
116
|
error: FinalizationError | null;
|
|
117
117
|
}>;
|
|
@@ -144,11 +144,11 @@ function failed(
|
|
|
144
144
|
status: error.retryable ? 'blocked' : 'failed',
|
|
145
145
|
changed: false,
|
|
146
146
|
taskId,
|
|
147
|
+
backfill: null,
|
|
147
148
|
lifecycle: null,
|
|
148
149
|
taskComment: null,
|
|
149
150
|
verification: null,
|
|
150
151
|
summary: null,
|
|
151
|
-
postSummaryVerification: null,
|
|
152
152
|
completedSteps: [],
|
|
153
153
|
pendingSteps: [...FINALIZATION_STEPS],
|
|
154
154
|
result: error.retryable ? 'blocked' : 'failed',
|
|
@@ -169,10 +169,8 @@ function emptyReceipt(taskId: string, controlBinding?: Readonly<{ generation: st
|
|
|
169
169
|
taskComment: 'pending',
|
|
170
170
|
verification: 'pending',
|
|
171
171
|
summary: 'pending',
|
|
172
|
-
postSummaryVerification: 'pending',
|
|
173
172
|
warningProjection: 'done',
|
|
174
173
|
warnings: [],
|
|
175
|
-
summarySha256: null,
|
|
176
174
|
...(controlBinding ? { controlBinding } : {}),
|
|
177
175
|
updatedAt: now(),
|
|
178
176
|
lastError: null
|
|
@@ -208,10 +206,8 @@ function validateReceipt(value: unknown, taskId: string): TaskFinalizationReceip
|
|
|
208
206
|
|| !states.includes(String(receipt.taskComment))
|
|
209
207
|
|| !states.includes(String(receipt.verification))
|
|
210
208
|
|| !states.includes(String(receipt.summary))
|
|
211
|
-
|| !states.includes(String(receipt.postSummaryVerification))
|
|
212
209
|
|| !['pending', 'done'].includes(String(receipt.warningProjection))
|
|
213
210
|
|| !Array.isArray(receipt.warnings) || receipt.warnings.some((warning) => !validWarning(warning))
|
|
214
|
-
|| (receipt.summarySha256 !== null && (typeof receipt.summarySha256 !== 'string' || !/^[a-f0-9]{64}$/u.test(receipt.summarySha256)))
|
|
215
211
|
|| typeof receipt.updatedAt !== 'string'
|
|
216
212
|
|| (receipt.lastError !== null && (typeof receipt.lastError !== 'object' || Array.isArray(receipt.lastError)))
|
|
217
213
|
|| (receipt.controlBinding !== undefined && (!controlBinding || typeof controlBinding !== 'object'
|
|
@@ -250,7 +246,7 @@ function writeReceipt(repoRoot: string, receipt: TaskFinalizationReceipt): void
|
|
|
250
246
|
function updateReceipt(
|
|
251
247
|
repoRoot: string,
|
|
252
248
|
receipt: TaskFinalizationReceipt,
|
|
253
|
-
patch: Partial<Pick<TaskFinalizationReceipt, 'lifecycle' | 'taskComment' | 'verification' | 'summary' | '
|
|
249
|
+
patch: Partial<Pick<TaskFinalizationReceipt, 'lifecycle' | 'taskComment' | 'verification' | 'summary' | 'warningProjection' | 'warnings' | 'controlBinding' | 'lastError'>>
|
|
254
250
|
): TaskFinalizationReceipt {
|
|
255
251
|
const current = readReceipt(repoRoot, receipt.taskId);
|
|
256
252
|
if (!current || current.receiptId !== receipt.receiptId) throw capabilityError(
|
|
@@ -272,7 +268,6 @@ function pendingSteps(receipt: TaskFinalizationReceipt): FinalizationStep[] {
|
|
|
272
268
|
|
|
273
269
|
function stepState(receipt: TaskFinalizationReceipt, step: FinalizationStep): FinalizationStepState {
|
|
274
270
|
if (step === 'task-comment') return receipt.taskComment;
|
|
275
|
-
if (step === 'post-summary-verification') return receipt.postSummaryVerification;
|
|
276
271
|
return receipt[step];
|
|
277
272
|
}
|
|
278
273
|
|
|
@@ -319,7 +314,7 @@ function verificationFailure(result: TaskVerificationResult): FinalizationError
|
|
|
319
314
|
}
|
|
320
315
|
|
|
321
316
|
function warningFromError(
|
|
322
|
-
step:
|
|
317
|
+
step: string,
|
|
323
318
|
error: FinalizationError,
|
|
324
319
|
severity: OperationWarningSeverity = 'ACTION_REQUIRED'
|
|
325
320
|
): OperationWarning {
|
|
@@ -362,6 +357,76 @@ function resolveStepWarnings(receipt: TaskFinalizationReceipt, step: Finalizatio
|
|
|
362
357
|
: warning);
|
|
363
358
|
}
|
|
364
359
|
|
|
360
|
+
function verificationWarnings(result: TaskVerificationResult): readonly OperationWarning[] {
|
|
361
|
+
const warnings: OperationWarning[] = [];
|
|
362
|
+
for (const invocation of result.invocations) {
|
|
363
|
+
const checks = Array.isArray(invocation.payload.checks) ? invocation.payload.checks : [invocation.payload];
|
|
364
|
+
for (const value of checks) {
|
|
365
|
+
if (!value || typeof value !== 'object' || Array.isArray(value)) continue;
|
|
366
|
+
const check = value as Record<string, unknown>;
|
|
367
|
+
const status = check.effectiveStatus ?? check.status;
|
|
368
|
+
const target = typeof check.checkId === 'string' && check.checkId ? check.checkId : typeof check.type === 'string' ? check.type : null;
|
|
369
|
+
if (!target || status === 'pass') continue;
|
|
370
|
+
const blocked = status === 'blocked';
|
|
371
|
+
const message = [check.reason, check.message, check.action]
|
|
372
|
+
.filter((item): item is string => typeof item === 'string' && item.length > 0)
|
|
373
|
+
.join(' - ');
|
|
374
|
+
warnings.push({
|
|
375
|
+
step: 'verification', target, code: blocked ? 'CHECK_BLOCKED' : 'CHECK_FAILED',
|
|
376
|
+
retryable: blocked, severity: 'ACTION_REQUIRED',
|
|
377
|
+
message: message || `verification ${String(status)}`
|
|
378
|
+
});
|
|
379
|
+
}
|
|
380
|
+
}
|
|
381
|
+
return mergeOperationWarnings(warnings);
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
function applyVerificationWarnings(
|
|
385
|
+
receipt: TaskFinalizationReceipt,
|
|
386
|
+
result: TaskVerificationResult
|
|
387
|
+
): readonly FinalizationWarning[] {
|
|
388
|
+
if (result.status === 'pass') return resolveStepWarnings(receipt, 'verification');
|
|
389
|
+
const observed = new Map<string, 'pass' | OperationWarning>();
|
|
390
|
+
for (const invocation of result.invocations) {
|
|
391
|
+
const checks = Array.isArray(invocation.payload.checks) ? invocation.payload.checks : [invocation.payload];
|
|
392
|
+
for (const value of checks) {
|
|
393
|
+
if (!value || typeof value !== 'object' || Array.isArray(value)) continue;
|
|
394
|
+
const check = value as Record<string, unknown>;
|
|
395
|
+
const target = typeof check.checkId === 'string' && check.checkId ? check.checkId : typeof check.type === 'string' ? check.type : null;
|
|
396
|
+
if (!target) continue;
|
|
397
|
+
const status = check.effectiveStatus ?? check.status;
|
|
398
|
+
if (status === 'pass') observed.set(target, 'pass');
|
|
399
|
+
}
|
|
400
|
+
}
|
|
401
|
+
for (const warning of verificationWarnings(result)) observed.set(warning.target, warning);
|
|
402
|
+
if (observed.size === 0) {
|
|
403
|
+
return replaceWarning(receipt, warningFromError('verification', verificationFailure(result)), 'open');
|
|
404
|
+
}
|
|
405
|
+
let warnings = [...receipt.warnings];
|
|
406
|
+
for (const [target, current] of observed) {
|
|
407
|
+
if (current === 'pass') {
|
|
408
|
+
warnings = warnings.map((warning) => warning.step === 'verification' && warning.target === target && warning.status === 'open'
|
|
409
|
+
? { ...warning, status: 'resolved', resolvedAt: now() }
|
|
410
|
+
: warning);
|
|
411
|
+
continue;
|
|
412
|
+
}
|
|
413
|
+
warnings = warnings.map((warning) => warning.step === 'verification' && warning.target === target
|
|
414
|
+
&& warning.status === 'open' && warning.code !== current.code
|
|
415
|
+
? { ...warning, status: 'resolved', resolvedAt: now() }
|
|
416
|
+
: warning);
|
|
417
|
+
const key = warningKey(current);
|
|
418
|
+
warnings = [...warnings.filter((warning) => warningKey(warning) !== key), warningRecord(current, 'open')];
|
|
419
|
+
}
|
|
420
|
+
return warnings;
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
function shouldRunBackfill(receipt: TaskFinalizationReceipt): boolean {
|
|
424
|
+
return receipt.lifecycle !== 'done'
|
|
425
|
+
|| receipt.taskComment === 'pending'
|
|
426
|
+
|| receipt.verification === 'pending'
|
|
427
|
+
|| receipt.warnings.some((warning) => warning.step === 'backfill' && warning.status === 'open');
|
|
428
|
+
}
|
|
429
|
+
|
|
365
430
|
function issueCapability(receipt: TaskFinalizationReceipt, scope: Exclude<FinalizationStep, 'lifecycle'> | 'warning-projection'): FinalizationCapability {
|
|
366
431
|
return {
|
|
367
432
|
receiptId: receipt.receiptId,
|
|
@@ -379,6 +444,15 @@ function capabilityError(code: 'FINALIZATION_CAPABILITY_STALE' | 'FINALIZATION_S
|
|
|
379
444
|
return error;
|
|
380
445
|
}
|
|
381
446
|
|
|
447
|
+
function hasPreparedTaskDocument(repoRoot: string, taskId: string): boolean {
|
|
448
|
+
const resolved = resolveTaskRef(taskId, { repoRoot });
|
|
449
|
+
if (!resolved.ok) return false;
|
|
450
|
+
if (resolved.state === 'completed') return true;
|
|
451
|
+
if (resolved.state !== 'active') return false;
|
|
452
|
+
try { return parseTaskFrontmatter(fs.readFileSync(resolved.taskMdPath, 'utf8')).status === 'completed'; }
|
|
453
|
+
catch { return false; }
|
|
454
|
+
}
|
|
455
|
+
|
|
382
456
|
function mutationKeys(value: FinalizationMutation): string[] {
|
|
383
457
|
return Object.keys(value).sort();
|
|
384
458
|
}
|
|
@@ -456,9 +530,8 @@ function applyFinalizationReceiptMutationUnderLock(
|
|
|
456
530
|
mutation: FinalizationMutation,
|
|
457
531
|
consumed: Set<string>
|
|
458
532
|
): TaskFinalizationReceipt {
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
throw capabilityError('FINALIZATION_SCOPE_INVALID', 'finalization capability requires a completed task');
|
|
533
|
+
if (!hasPreparedTaskDocument(repoRoot, receipt.taskId)) {
|
|
534
|
+
throw capabilityError('FINALIZATION_SCOPE_INVALID', 'finalization capability requires an active prepared task or a completed task');
|
|
462
535
|
}
|
|
463
536
|
const current = readReceipt(repoRoot, receipt.taskId);
|
|
464
537
|
if (!current || current.receiptId !== receipt.receiptId || current.revision !== receipt.revision) {
|
|
@@ -476,11 +549,10 @@ function applyFinalizationReceiptMutation(
|
|
|
476
549
|
capability: FinalizationCapability,
|
|
477
550
|
mutation: FinalizationMutation
|
|
478
551
|
): TaskFinalizationReceipt {
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
throw capabilityError('FINALIZATION_SCOPE_INVALID', 'finalization capability requires a completed task');
|
|
552
|
+
if (!hasPreparedTaskDocument(repoRoot, receipt.taskId)) {
|
|
553
|
+
throw capabilityError('FINALIZATION_SCOPE_INVALID', 'finalization capability requires an active prepared task or a completed task');
|
|
482
554
|
}
|
|
483
|
-
return withTaskExecutionLock(repoRoot,
|
|
555
|
+
return withTaskExecutionLock(repoRoot, receipt.taskId, 'task-finalization.receipt-mutation', () =>
|
|
484
556
|
applyFinalizationReceiptMutationUnderLock(repoRoot, receipt, capability, mutation, new Set<string>())
|
|
485
557
|
);
|
|
486
558
|
}
|
|
@@ -494,7 +566,10 @@ function reconcileWarningProjection(repoRoot: string, taskId: string, receipt: T
|
|
|
494
566
|
const warnings = [...new Map(receipt.warnings.map((warning) => [warningKey(warning), warning])).values()];
|
|
495
567
|
for (const warning of warnings) {
|
|
496
568
|
try {
|
|
497
|
-
const
|
|
569
|
+
const superseded = warning.status === 'resolved' && receipt.warnings.some((item) =>
|
|
570
|
+
item.status === 'open' && item.step === warning.step && item.target === warning.target && item.code !== warning.code
|
|
571
|
+
);
|
|
572
|
+
const projected = projectFinalizationWarning(taskId, warning, { repoRoot, resolution: superseded ? 'superseded' : 'passed' });
|
|
498
573
|
if (projected.status === 'failed') {
|
|
499
574
|
const detail: FinalizationError = {
|
|
500
575
|
code: projected.error?.code || 'FINALIZATION_WARNING_PROJECTION_FAILED',
|
|
@@ -598,15 +673,16 @@ async function syncPendingSummary(input: {
|
|
|
598
673
|
commentSync: typeof syncPlatformComment;
|
|
599
674
|
}): Promise<{ receipt: TaskFinalizationReceipt; step: TaskFinalizationStep; changed: boolean; error: FinalizationError | null }> {
|
|
600
675
|
let { receipt } = input;
|
|
601
|
-
if (receipt.summary !== 'pending') {
|
|
602
|
-
return { receipt, step: { status: receipt.summary === 'skipped' ? 'skipped' : 'no-op', changed: false, error: null }, changed: false, error: null };
|
|
603
|
-
}
|
|
604
676
|
try {
|
|
605
677
|
const resolved = resolveTaskRef(input.taskId, { repoRoot: input.repoRoot });
|
|
606
|
-
if (!resolved.ok ||
|
|
678
|
+
if (!resolved.ok || !hasPreparedTaskDocument(input.repoRoot, input.taskId)) throw Object.assign(new Error('prepared task is unavailable for summary sync'), { code: 'SUMMARY_STAGING_INVALID' });
|
|
679
|
+
if (!fs.existsSync(path.join(resolved.taskDir, '.delivery-summary.json'))) {
|
|
680
|
+
receipt = updateReceipt(input.repoRoot, receipt, { summary: 'skipped', lastError: null });
|
|
681
|
+
return { receipt, step: { status: 'skipped', changed: false, error: null }, changed: false, error: null };
|
|
682
|
+
}
|
|
607
683
|
if (!taskIssueIdentity(parseTaskFrontmatter(fs.readFileSync(resolved.taskMdPath, 'utf8')))) {
|
|
608
684
|
receipt = updateReceipt(input.repoRoot, receipt, {
|
|
609
|
-
summary: 'skipped',
|
|
685
|
+
summary: 'skipped', lastError: null
|
|
610
686
|
});
|
|
611
687
|
return { receipt, step: { status: 'skipped', changed: false, error: null }, changed: false, error: null };
|
|
612
688
|
}
|
|
@@ -621,8 +697,7 @@ async function syncPendingSummary(input: {
|
|
|
621
697
|
const resolvedWarning = receipt.warnings.some((warning) => warning.step === 'summary' && warning.status === 'open');
|
|
622
698
|
const warnings = resolveStepWarnings(receipt, 'summary');
|
|
623
699
|
receipt = updateReceipt(input.repoRoot, receipt, {
|
|
624
|
-
summary: skipped ? 'skipped' : 'done',
|
|
625
|
-
summarySha256: skipped ? null : summary.sha256,
|
|
700
|
+
summary: skipped ? 'skipped' : 'done',
|
|
626
701
|
taskComment: resolvedWarning ? 'pending' : receipt.taskComment,
|
|
627
702
|
warningProjection: warnings.some((warning) => warning.status === 'open') ? 'pending' : 'done',
|
|
628
703
|
warnings,
|
|
@@ -648,20 +723,15 @@ async function syncPendingSummary(input: {
|
|
|
648
723
|
}
|
|
649
724
|
}
|
|
650
725
|
|
|
651
|
-
function clearDeliverySummary(taskDir: string): void {
|
|
652
|
-
const file = path.join(taskDir, '.delivery-summary.json');
|
|
653
|
-
if (fs.existsSync(file)) fs.unlinkSync(file);
|
|
654
|
-
}
|
|
655
|
-
|
|
656
726
|
function terminalResult(
|
|
657
727
|
taskId: string,
|
|
658
728
|
receipt: TaskFinalizationReceipt,
|
|
659
|
-
steps: Partial<Pick<TaskFinalizationResult, '
|
|
729
|
+
steps: Partial<Pick<TaskFinalizationResult, 'backfill' | 'lifecycle' | 'taskComment' | 'verification' | 'summary'>>,
|
|
660
730
|
changed: boolean,
|
|
661
731
|
error: FinalizationError | null = null
|
|
662
732
|
): TaskFinalizationResult {
|
|
663
733
|
const pending = pendingSteps(receipt);
|
|
664
|
-
const blocked = [steps.lifecycle, steps.taskComment, steps.verification, steps.summary
|
|
734
|
+
const blocked = [steps.backfill, steps.lifecycle, steps.taskComment, steps.verification, steps.summary].some((step) => step?.status === 'blocked');
|
|
665
735
|
const warnings = openWarnings(receipt);
|
|
666
736
|
const postLifecyclePending = receipt.lifecycle === 'done' && (pending.some((step) => step !== 'lifecycle') || receipt.warningProjection === 'pending');
|
|
667
737
|
const hardError = error?.code.startsWith('FINALIZATION_') || error?.code === 'TASK_FINALIZATION_RECEIPT_INVALID';
|
|
@@ -669,18 +739,18 @@ function terminalResult(
|
|
|
669
739
|
status: hardError ? (error?.retryable ? 'blocked' : 'failed') : pending.length === 0 ? 'completed' : postLifecyclePending ? 'completed' : blocked ? 'blocked' : 'failed',
|
|
670
740
|
changed,
|
|
671
741
|
taskId,
|
|
742
|
+
backfill: steps.backfill ?? null,
|
|
672
743
|
lifecycle: steps.lifecycle ?? null,
|
|
673
744
|
taskComment: steps.taskComment ?? null,
|
|
674
745
|
verification: steps.verification ?? null,
|
|
675
746
|
summary: steps.summary ?? null,
|
|
676
|
-
postSummaryVerification: steps.postSummaryVerification ?? null,
|
|
677
747
|
completedSteps: completedSteps(receipt),
|
|
678
748
|
pendingSteps: pending,
|
|
679
749
|
result: hardError
|
|
680
750
|
? (error?.retryable ? 'blocked' : 'failed')
|
|
681
|
-
: postLifecyclePending
|
|
751
|
+
: postLifecyclePending
|
|
682
752
|
? 'completed_with_warnings'
|
|
683
|
-
: pending.length === 0
|
|
753
|
+
: pending.length === 0
|
|
684
754
|
? 'completed'
|
|
685
755
|
: blocked
|
|
686
756
|
? 'blocked'
|
|
@@ -690,13 +760,32 @@ function terminalResult(
|
|
|
690
760
|
};
|
|
691
761
|
}
|
|
692
762
|
|
|
693
|
-
|
|
763
|
+
function preparedResult(
|
|
764
|
+
taskId: string,
|
|
765
|
+
receipt: TaskFinalizationReceipt,
|
|
766
|
+
steps: Partial<Pick<TaskFinalizationResult, 'backfill' | 'lifecycle' | 'taskComment' | 'verification' | 'summary'>>,
|
|
767
|
+
changed: boolean
|
|
768
|
+
): TaskFinalizationResult {
|
|
769
|
+
return {
|
|
770
|
+
status: 'prepared', changed, taskId,
|
|
771
|
+
backfill: steps.backfill ?? null,
|
|
772
|
+
lifecycle: steps.lifecycle ?? null,
|
|
773
|
+
taskComment: steps.taskComment ?? null,
|
|
774
|
+
verification: steps.verification ?? null,
|
|
775
|
+
summary: steps.summary ?? null,
|
|
776
|
+
completedSteps: completedSteps(receipt), pendingSteps: pendingSteps(receipt),
|
|
777
|
+
result: 'prepared', warnings: openWarnings(receipt), error: null
|
|
778
|
+
};
|
|
779
|
+
}
|
|
780
|
+
|
|
781
|
+
async function prepareUnderLock(
|
|
694
782
|
request: TaskFinalizationRequest,
|
|
695
783
|
taskId: string,
|
|
696
784
|
options: TaskFinalizationOptions
|
|
697
785
|
): Promise<TaskFinalizationResult> {
|
|
698
786
|
const repoRoot = path.resolve(options.repoRoot);
|
|
699
787
|
const lifecycle = options.lifecycle ?? applyTaskLifecycle;
|
|
788
|
+
const backfill = options.backfill ?? backfillCompletionComments;
|
|
700
789
|
const commentSync = options.commentSync ?? syncPlatformComment;
|
|
701
790
|
const verify = options.verify ?? verifyTaskEvent;
|
|
702
791
|
const consumedCapabilities = new Set<string>();
|
|
@@ -736,136 +825,152 @@ async function applyUnderLock(
|
|
|
736
825
|
}
|
|
737
826
|
}
|
|
738
827
|
|
|
739
|
-
let lifecycleResult: TaskFinalizationStep | null = null;
|
|
740
828
|
let changed = false;
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
829
|
+
let backfillResult: TaskFinalizationStep | null = null;
|
|
830
|
+
let runBackfill = shouldRunBackfill(receipt);
|
|
831
|
+
if (!runBackfill && receipt.lifecycle === 'done') {
|
|
832
|
+
const eligibility = inspectCompletionBackfillEligibility(taskId, { cwd: repoRoot });
|
|
833
|
+
if (eligibility.status === 'failed') return failed(taskId, eligibility.error!, {
|
|
834
|
+
completedSteps: completedSteps(receipt), pendingSteps: pendingSteps(receipt), warnings: openWarnings(receipt)
|
|
835
|
+
});
|
|
836
|
+
runBackfill = eligibility.eligible;
|
|
837
|
+
}
|
|
838
|
+
if (receipt.lifecycle === 'done' && runBackfill && receipt.summary === 'done') {
|
|
839
|
+
receipt = updateReceipt(repoRoot, receipt, { summary: 'pending' });
|
|
840
|
+
}
|
|
841
|
+
if (runBackfill) try {
|
|
842
|
+
const result = await backfill(taskId, { agent: request.agent, cwd: repoRoot });
|
|
843
|
+
backfillResult = commentStep(result);
|
|
844
|
+
const issueNotLinked = result.status === 'no-op' && result.error?.code === 'ISSUE_NOT_LINKED';
|
|
845
|
+
if (!issueNotLinked && (result.status !== 'applied' && result.status !== 'no-op' || result.error)) {
|
|
846
|
+
const detail = backfillResult.error ?? {
|
|
847
|
+
code: 'COMPLETION_BACKFILL_FAILED', message: 'completion artifact backfill did not reach a successful terminal result', retryable: true
|
|
749
848
|
};
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
849
|
+
if (receipt.lifecycle !== 'done') return failed(taskId, detail, {
|
|
850
|
+
backfill: backfillResult,
|
|
851
|
+
completedSteps: completedSteps(receipt),
|
|
852
|
+
pendingSteps: pendingSteps(receipt)
|
|
853
|
+
});
|
|
854
|
+
const warnings = replaceWarning(receipt, warningFromError('backfill', detail), 'open');
|
|
855
|
+
receipt = updateReceipt(repoRoot, receipt, { warningProjection: 'pending', warnings, lastError: detail });
|
|
856
|
+
receipt = reconcileWarningProjection(repoRoot, taskId, receipt, consumedCapabilities);
|
|
857
|
+
if (!detail.retryable) return failed(taskId, detail, {
|
|
858
|
+
changed, backfill: backfillResult, completedSteps: completedSteps(receipt),
|
|
859
|
+
pendingSteps: pendingSteps(receipt), warnings: openWarnings(receipt)
|
|
860
|
+
});
|
|
861
|
+
return terminalResult(taskId, receipt, { backfill: backfillResult }, changed, detail);
|
|
756
862
|
}
|
|
757
|
-
|
|
758
|
-
if (
|
|
759
|
-
const
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
863
|
+
changed = result.changed;
|
|
864
|
+
if (receipt.warnings.some((warning) => warning.step === 'backfill' && warning.status === 'open')) {
|
|
865
|
+
const warnings = receipt.warnings.map((warning) => warning.step === 'backfill' && warning.status === 'open'
|
|
866
|
+
? { ...warning, status: 'resolved' as const, resolvedAt: now() }
|
|
867
|
+
: warning);
|
|
868
|
+
receipt = updateReceipt(repoRoot, receipt, { warningProjection: 'pending', warnings, lastError: null });
|
|
869
|
+
}
|
|
870
|
+
if (result.changed && receipt.taskComment !== 'pending') {
|
|
871
|
+
receipt = updateReceipt(repoRoot, receipt, { taskComment: 'pending' });
|
|
872
|
+
}
|
|
873
|
+
} catch (error) {
|
|
874
|
+
const detail = errorOf(error, 'COMPLETION_BACKFILL_FAILED', true);
|
|
875
|
+
if (receipt.lifecycle === 'done') {
|
|
876
|
+
const warnings = replaceWarning(receipt, warningFromError('backfill', detail), 'open');
|
|
877
|
+
receipt = updateReceipt(repoRoot, receipt, { warningProjection: 'pending', warnings, lastError: detail });
|
|
878
|
+
receipt = reconcileWarningProjection(repoRoot, taskId, receipt, consumedCapabilities);
|
|
765
879
|
return terminalResult(taskId, receipt, {
|
|
766
|
-
|
|
767
|
-
taskComment: null,
|
|
768
|
-
verification: null
|
|
880
|
+
backfill: { status: 'blocked', changed: false, error: detail }
|
|
769
881
|
}, changed, detail);
|
|
770
882
|
}
|
|
771
|
-
|
|
772
|
-
|
|
883
|
+
return failed(taskId, detail, {
|
|
884
|
+
backfill: { status: detail.retryable ? 'blocked' : 'failed', changed: false, error: detail },
|
|
885
|
+
completedSteps: completedSteps(receipt),
|
|
886
|
+
pendingSteps: pendingSteps(receipt)
|
|
887
|
+
});
|
|
888
|
+
} else backfillResult = { status: 'no-op', changed: false, error: null };
|
|
889
|
+
|
|
890
|
+
let lifecycleResult: TaskFinalizationStep | null = null;
|
|
891
|
+
try {
|
|
892
|
+
const resolved = resolveTaskRef(taskId, { repoRoot });
|
|
893
|
+
if (receipt.lifecycle === 'done' && resolved.ok && resolved.state === 'completed') {
|
|
773
894
|
lifecycleResult = { status: 'no-op', changed: false, error: null };
|
|
774
895
|
receipt = updateReceipt(repoRoot, receipt, { lifecycle: 'done', lastError: null });
|
|
775
896
|
} else {
|
|
776
897
|
const result = lifecycle(
|
|
777
898
|
{ taskRef: taskId, intent: 'complete', agent: request.agent },
|
|
778
|
-
{ repoRoot, ...(options.metadataProvider ? { metadataProvider: options.metadataProvider } : {}) }
|
|
899
|
+
{ repoRoot, prepareOnly: true, ...(options.metadataProvider ? { metadataProvider: options.metadataProvider } : {}) }
|
|
779
900
|
);
|
|
780
901
|
lifecycleResult = lifecycleStep(result);
|
|
781
902
|
if (result.status !== 'applied' && result.status !== 'no-op') {
|
|
782
903
|
receipt = updateReceipt(repoRoot, receipt, { lifecycle: 'pending', lastError: lifecycleResult.error });
|
|
783
|
-
return terminalResult(taskId, receipt, { lifecycle: lifecycleResult, taskComment: null, verification: null }, result.changed, lifecycleResult.error);
|
|
904
|
+
return terminalResult(taskId, receipt, { backfill: backfillResult, lifecycle: lifecycleResult, taskComment: null, verification: null }, result.changed, lifecycleResult.error);
|
|
784
905
|
}
|
|
785
|
-
changed = result.changed;
|
|
786
|
-
receipt = updateReceipt(repoRoot, receipt, { lifecycle: 'done', lastError: null });
|
|
906
|
+
changed = changed || result.changed;
|
|
787
907
|
}
|
|
788
908
|
} catch (error) {
|
|
789
909
|
const detail = errorOf(error, 'TASK_FINALIZATION_LIFECYCLE_FAILED');
|
|
790
910
|
try { receipt = updateReceipt(repoRoot, receipt, { lifecycle: 'pending', lastError: detail }); } catch { /* preserve the primary error */ }
|
|
791
|
-
return failed(taskId, detail, { lifecycle: lifecycleResult });
|
|
911
|
+
return failed(taskId, detail, { backfill: backfillResult, lifecycle: lifecycleResult });
|
|
792
912
|
}
|
|
793
913
|
|
|
794
914
|
let taskComment: TaskFinalizationStep | null = null;
|
|
795
915
|
receipt = reconcileWarningProjection(repoRoot, taskId, receipt, consumedCapabilities);
|
|
796
|
-
const initialComment = await syncPendingTaskComment({
|
|
797
|
-
repoRoot, taskId, agent: request.agent, receipt, commentSync, consumedCapabilities
|
|
798
|
-
});
|
|
799
|
-
receipt = initialComment.receipt;
|
|
800
|
-
taskComment = initialComment.step;
|
|
801
|
-
changed = changed || initialComment.changed;
|
|
802
|
-
if (initialComment.error) {
|
|
803
|
-
return terminalResult(taskId, receipt, { lifecycle: lifecycleResult, taskComment, verification: null }, changed, initialComment.error);
|
|
804
|
-
}
|
|
805
916
|
|
|
806
917
|
let verification: TaskFinalizationStep | null = null;
|
|
807
|
-
|
|
918
|
+
if (receipt.verification !== 'pending') {
|
|
919
|
+
verification = { status: receipt.verification === 'skipped' ? 'skipped' : 'no-op', changed: false, error: null };
|
|
920
|
+
} else {
|
|
921
|
+
try {
|
|
808
922
|
receipt = reconcileWarningProjection(repoRoot, taskId, receipt, consumedCapabilities);
|
|
809
|
-
const
|
|
923
|
+
const verificationState = resolveTaskRef(taskId, { repoRoot });
|
|
924
|
+
const verificationEvent = verificationState.ok && verificationState.state === 'completed'
|
|
925
|
+
? 'complete-task.completed'
|
|
926
|
+
: 'complete-task.prepared';
|
|
810
927
|
const result = await verify(
|
|
811
|
-
{ taskRef: taskId, event:
|
|
928
|
+
{ taskRef: taskId, event: verificationEvent },
|
|
812
929
|
{ repoRoot }
|
|
813
930
|
);
|
|
814
931
|
verification = verificationStep(result);
|
|
815
932
|
if (result.status === 'pass') {
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
}
|
|
831
|
-
} else {
|
|
832
|
-
receipt = updateReceipt(repoRoot, receipt, { lastError: null });
|
|
933
|
+
const resolvedTaskWarning = receipt.warnings.some((warning) => warning.step === 'verification' && warning.status === 'open');
|
|
934
|
+
const warnings = applyVerificationWarnings(receipt, result);
|
|
935
|
+
receipt = updateReceipt(repoRoot, receipt, {
|
|
936
|
+
verification: 'done', taskComment: resolvedTaskWarning ? 'pending' : receipt.taskComment,
|
|
937
|
+
warningProjection: warnings.length > 0 ? 'pending' : 'done', warnings, lastError: null
|
|
938
|
+
});
|
|
939
|
+
receipt = reconcileWarningProjection(repoRoot, taskId, receipt, consumedCapabilities);
|
|
940
|
+
const finalComment = await syncPendingTaskComment({
|
|
941
|
+
repoRoot, taskId, agent: request.agent, receipt, commentSync, consumedCapabilities
|
|
942
|
+
});
|
|
943
|
+
receipt = finalComment.receipt;
|
|
944
|
+
if (finalComment.step.status !== 'no-op') taskComment = finalComment.step;
|
|
945
|
+
changed = changed || finalComment.changed;
|
|
946
|
+
if (finalComment.error) {
|
|
947
|
+
return terminalResult(taskId, receipt, { backfill: backfillResult, lifecycle: lifecycleResult, taskComment, verification }, changed, finalComment.error);
|
|
833
948
|
}
|
|
834
949
|
} else {
|
|
835
950
|
const detail = verification.error ?? { code: 'VERIFY_FAILED', message: 'verification failed', retryable: true };
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
|
|
844
|
-
|
|
845
|
-
|
|
846
|
-
|
|
847
|
-
|
|
848
|
-
|
|
849
|
-
receipt = warningComment.receipt;
|
|
850
|
-
if (warningComment.step.status !== 'no-op') taskComment = warningComment.step;
|
|
851
|
-
changed = changed || warningComment.changed;
|
|
951
|
+
const warnings = applyVerificationWarnings(receipt, result);
|
|
952
|
+
receipt = updateReceipt(repoRoot, receipt, {
|
|
953
|
+
verification: 'pending', taskComment: 'pending', warningProjection: 'pending', warnings, lastError: detail
|
|
954
|
+
});
|
|
955
|
+
receipt = reconcileWarningProjection(repoRoot, taskId, receipt, consumedCapabilities);
|
|
956
|
+
const warningComment = await syncPendingTaskComment({
|
|
957
|
+
repoRoot, taskId, agent: request.agent, receipt, commentSync, consumedCapabilities
|
|
958
|
+
});
|
|
959
|
+
receipt = warningComment.receipt;
|
|
960
|
+
taskComment = warningComment.step;
|
|
961
|
+
changed = changed || warningComment.changed;
|
|
962
|
+
if (warningComment.error) {
|
|
963
|
+
return terminalResult(taskId, receipt, { backfill: backfillResult, lifecycle: lifecycleResult, taskComment, verification }, changed, warningComment.error);
|
|
852
964
|
}
|
|
853
|
-
return terminalResult(taskId, receipt, { lifecycle: lifecycleResult, taskComment, verification }, changed, detail);
|
|
965
|
+
return terminalResult(taskId, receipt, { backfill: backfillResult, lifecycle: lifecycleResult, taskComment, verification }, changed, detail);
|
|
854
966
|
}
|
|
855
967
|
} catch (error) {
|
|
856
968
|
const detail = errorOf(error, 'VERIFY_FAILED', true);
|
|
857
969
|
try {
|
|
858
|
-
|
|
859
|
-
|
|
860
|
-
|
|
861
|
-
|
|
862
|
-
}, consumedCapabilities);
|
|
863
|
-
} else {
|
|
864
|
-
const warnings = replaceWarning(receipt, warningFromError('verification', detail), 'open');
|
|
865
|
-
receipt = updateReceipt(repoRoot, receipt, {
|
|
866
|
-
verification: 'pending', taskComment: 'pending', warningProjection: 'pending', warnings, lastError: detail
|
|
867
|
-
});
|
|
868
|
-
}
|
|
970
|
+
const warnings = replaceWarning(receipt, warningFromError('verification', detail), 'open');
|
|
971
|
+
receipt = updateReceipt(repoRoot, receipt, {
|
|
972
|
+
verification: 'pending', taskComment: 'pending', warningProjection: 'pending', warnings, lastError: detail
|
|
973
|
+
});
|
|
869
974
|
receipt = reconcileWarningProjection(repoRoot, taskId, receipt, consumedCapabilities);
|
|
870
975
|
} catch (writeError) {
|
|
871
976
|
const persistence = errorOf(writeError, 'FINALIZATION_RECEIPT_WRITE_FAILED', true);
|
|
@@ -875,7 +980,7 @@ async function applyUnderLock(
|
|
|
875
980
|
retryable: true
|
|
876
981
|
};
|
|
877
982
|
return terminalResult(taskId, receipt, {
|
|
878
|
-
lifecycle: lifecycleResult, taskComment, verification: { status: 'blocked', changed: false, error: detail }
|
|
983
|
+
backfill: backfillResult, lifecycle: lifecycleResult, taskComment, verification: { status: 'blocked', changed: false, error: detail }
|
|
879
984
|
}, changed, hardError);
|
|
880
985
|
}
|
|
881
986
|
try {
|
|
@@ -884,102 +989,52 @@ async function applyUnderLock(
|
|
|
884
989
|
if (warningComment.step.status !== 'no-op') taskComment = warningComment.step;
|
|
885
990
|
changed = changed || warningComment.changed;
|
|
886
991
|
} catch { /* preserve the primary error */ }
|
|
887
|
-
return terminalResult(taskId, receipt, { lifecycle: lifecycleResult, taskComment, verification: { status: 'blocked', changed: false, error: detail } }, changed, detail);
|
|
992
|
+
return terminalResult(taskId, receipt, { backfill: backfillResult, lifecycle: lifecycleResult, taskComment, verification: { status: 'blocked', changed: false, error: detail } }, changed, detail);
|
|
993
|
+
}
|
|
888
994
|
}
|
|
889
995
|
|
|
890
|
-
|
|
996
|
+
let summary = receipt.summary === 'pending'
|
|
997
|
+
? await syncPendingSummary({ repoRoot, taskId, agent: request.agent, receipt, commentSync })
|
|
998
|
+
: { receipt, step: { status: 'no-op', changed: false, error: null }, changed: false, error: null };
|
|
891
999
|
receipt = summary.receipt;
|
|
892
1000
|
changed = changed || summary.changed;
|
|
893
1001
|
if (summary.error) {
|
|
894
1002
|
receipt = reconcileWarningProjection(repoRoot, taskId, receipt, consumedCapabilities);
|
|
895
|
-
|
|
1003
|
+
return terminalResult(taskId, receipt, { backfill: backfillResult, lifecycle: lifecycleResult, taskComment, verification, summary: summary.step }, changed, summary.error);
|
|
1004
|
+
}
|
|
1005
|
+
receipt = reconcileWarningProjection(repoRoot, taskId, receipt, consumedCapabilities);
|
|
1006
|
+
if (receipt.taskComment === 'pending') {
|
|
1007
|
+
const finalComment = await syncPendingTaskComment({
|
|
896
1008
|
repoRoot, taskId, agent: request.agent, receipt, commentSync, consumedCapabilities
|
|
897
1009
|
});
|
|
898
|
-
receipt =
|
|
899
|
-
|
|
900
|
-
changed = changed ||
|
|
901
|
-
|
|
902
|
-
|
|
903
|
-
|
|
904
|
-
let postSummaryVerification: TaskFinalizationStep | null = null;
|
|
905
|
-
if (receipt.postSummaryVerification === 'skipped') {
|
|
906
|
-
postSummaryVerification = { status: 'skipped', changed: false, error: null };
|
|
907
|
-
} else if (receipt.postSummaryVerification !== 'pending') {
|
|
908
|
-
const replay = await commentSync(taskId, {
|
|
909
|
-
kind: 'summary', agent: request.agent, body: '', cwd: repoRoot, verifyOnly: true,
|
|
910
|
-
summaryAuthorization: { sha256: receipt.summarySha256 ?? '' }
|
|
911
|
-
});
|
|
912
|
-
postSummaryVerification = commentStep(replay);
|
|
913
|
-
if (replay.status !== 'applied' && replay.status !== 'no-op') {
|
|
914
|
-
const error = postSummaryVerification.error ?? { code: 'SUMMARY_VERIFICATION_FAILED', message: 'summary replay verification failed', retryable: true };
|
|
915
|
-
const warnings = replaceWarning(receipt, warningFromError('post-summary-verification', error), 'open');
|
|
916
|
-
receipt = updateReceipt(repoRoot, receipt, { postSummaryVerification: 'pending', taskComment: 'pending', warningProjection: 'pending', warnings, lastError: error });
|
|
917
|
-
receipt = reconcileWarningProjection(repoRoot, taskId, receipt, consumedCapabilities);
|
|
918
|
-
return terminalResult(taskId, receipt, { lifecycle: lifecycleResult, taskComment, verification, summary: summary.step, postSummaryVerification }, changed, error);
|
|
919
|
-
}
|
|
920
|
-
} else {
|
|
921
|
-
try {
|
|
922
|
-
const summaryVerification = await commentSync(taskId, {
|
|
923
|
-
kind: 'summary', agent: request.agent, body: '', cwd: repoRoot, verifyOnly: true,
|
|
924
|
-
summaryAuthorization: { sha256: receipt.summarySha256 ?? '' }
|
|
925
|
-
});
|
|
926
|
-
if (summaryVerification.status !== 'applied' && summaryVerification.status !== 'no-op') {
|
|
927
|
-
const error = commentStep(summaryVerification).error ?? { code: 'SUMMARY_VERIFICATION_FAILED', message: 'summary verification failed', retryable: true };
|
|
928
|
-
const warnings = replaceWarning(receipt, warningFromError('post-summary-verification', error), 'open');
|
|
929
|
-
receipt = updateReceipt(repoRoot, receipt, { postSummaryVerification: 'pending', taskComment: 'pending', warningProjection: 'pending', warnings, lastError: error });
|
|
930
|
-
receipt = reconcileWarningProjection(repoRoot, taskId, receipt, consumedCapabilities);
|
|
931
|
-
const warningComment = await syncPendingTaskComment({ repoRoot, taskId, agent: request.agent, receipt, commentSync, consumedCapabilities });
|
|
932
|
-
receipt = warningComment.receipt;
|
|
933
|
-
if (warningComment.step.status !== 'no-op') taskComment = warningComment.step;
|
|
934
|
-
changed = changed || warningComment.changed;
|
|
935
|
-
return terminalResult(taskId, receipt, { lifecycle: lifecycleResult, taskComment, verification, summary: summary.step, postSummaryVerification: commentStep(summaryVerification) }, changed, error);
|
|
936
|
-
}
|
|
937
|
-
const result = await verify({ taskRef: taskId, event: 'complete-task.completed' }, { repoRoot });
|
|
938
|
-
postSummaryVerification = verificationStep(result);
|
|
939
|
-
if (result.status === 'pass') {
|
|
940
|
-
const resolvedWarning = receipt.warnings.some((warning) => warning.step === 'post-summary-verification' && warning.status === 'open');
|
|
941
|
-
const warnings = resolveStepWarnings(receipt, 'post-summary-verification');
|
|
942
|
-
receipt = updateReceipt(repoRoot, receipt, {
|
|
943
|
-
postSummaryVerification: 'done', taskComment: resolvedWarning ? 'pending' : receipt.taskComment,
|
|
944
|
-
warningProjection: warnings.some((warning) => warning.status === 'open') ? 'pending' : 'done', warnings, lastError: null
|
|
945
|
-
});
|
|
946
|
-
receipt = reconcileWarningProjection(repoRoot, taskId, receipt, consumedCapabilities);
|
|
947
|
-
const warningComment = await syncPendingTaskComment({ repoRoot, taskId, agent: request.agent, receipt, commentSync, consumedCapabilities });
|
|
948
|
-
receipt = warningComment.receipt;
|
|
949
|
-
if (warningComment.step.status !== 'no-op') taskComment = warningComment.step;
|
|
950
|
-
changed = changed || warningComment.changed;
|
|
951
|
-
} else {
|
|
952
|
-
const error = postSummaryVerification.error ?? { code: 'VERIFY_FAILED', message: 'post-summary verification failed', retryable: true };
|
|
953
|
-
const warnings = replaceWarning(receipt, warningFromError('post-summary-verification', error), 'open');
|
|
954
|
-
receipt = updateReceipt(repoRoot, receipt, { postSummaryVerification: 'pending', taskComment: 'pending', warningProjection: 'pending', warnings, lastError: error });
|
|
955
|
-
receipt = reconcileWarningProjection(repoRoot, taskId, receipt, consumedCapabilities);
|
|
956
|
-
const warningComment = await syncPendingTaskComment({ repoRoot, taskId, agent: request.agent, receipt, commentSync, consumedCapabilities });
|
|
957
|
-
receipt = warningComment.receipt;
|
|
958
|
-
if (warningComment.step.status !== 'no-op') taskComment = warningComment.step;
|
|
959
|
-
changed = changed || warningComment.changed;
|
|
960
|
-
return terminalResult(taskId, receipt, { lifecycle: lifecycleResult, taskComment, verification, summary: summary.step, postSummaryVerification }, changed, error);
|
|
961
|
-
}
|
|
962
|
-
} catch (error) {
|
|
963
|
-
const detail = errorOf(error, 'POST_SUMMARY_VERIFY_FAILED', true);
|
|
964
|
-
const warnings = replaceWarning(receipt, warningFromError('post-summary-verification', detail), 'open');
|
|
965
|
-
receipt = updateReceipt(repoRoot, receipt, { postSummaryVerification: 'pending', taskComment: 'pending', warningProjection: 'pending', warnings, lastError: detail });
|
|
966
|
-
receipt = reconcileWarningProjection(repoRoot, taskId, receipt, consumedCapabilities);
|
|
967
|
-
const warningComment = await syncPendingTaskComment({ repoRoot, taskId, agent: request.agent, receipt, commentSync, consumedCapabilities });
|
|
968
|
-
receipt = warningComment.receipt;
|
|
969
|
-
if (warningComment.step.status !== 'no-op') taskComment = warningComment.step;
|
|
970
|
-
changed = changed || warningComment.changed;
|
|
971
|
-
return terminalResult(taskId, receipt, { lifecycle: lifecycleResult, taskComment, verification, summary: summary.step, postSummaryVerification: { status: 'blocked', changed: false, error: detail } }, changed, detail);
|
|
1010
|
+
receipt = finalComment.receipt;
|
|
1011
|
+
taskComment = finalComment.step;
|
|
1012
|
+
changed = changed || finalComment.changed;
|
|
1013
|
+
if (finalComment.error) {
|
|
1014
|
+
return terminalResult(taskId, receipt, { backfill: backfillResult, lifecycle: lifecycleResult, taskComment, verification, summary: summary.step }, changed, finalComment.error);
|
|
972
1015
|
}
|
|
973
1016
|
}
|
|
974
|
-
receipt
|
|
975
|
-
|
|
976
|
-
|
|
977
|
-
|
|
1017
|
+
return preparedResult(taskId, receipt, { backfill: backfillResult, lifecycle: lifecycleResult, taskComment, verification, summary: summary.step }, changed);
|
|
1018
|
+
}
|
|
1019
|
+
|
|
1020
|
+
async function prepareTaskFinalization(request: TaskFinalizationRequest, options: TaskFinalizationOptions): Promise<TaskFinalizationResult> {
|
|
1021
|
+
if (request.intent !== 'complete' || !request.taskRef || !request.agent) {
|
|
1022
|
+
return failed(null, { code: 'TASK_FINALIZATION_PAYLOAD_INVALID', message: 'complete finalization requires taskRef and agent', retryable: false });
|
|
1023
|
+
}
|
|
1024
|
+
const repoRoot = path.resolve(options.repoRoot);
|
|
1025
|
+
const resolved = resolveTaskRef(request.taskRef, { repoRoot });
|
|
1026
|
+
if (!resolved.ok) return failed(resolved.taskId, { code: resolved.code, message: resolved.message, retryable: false });
|
|
1027
|
+
try {
|
|
1028
|
+
return await withTaskExecutionLock(repoRoot, resolved.taskId, 'task-finalization.prepare', () => prepareUnderLock(request, resolved.taskId, options));
|
|
1029
|
+
} catch (error) {
|
|
1030
|
+
const detail = error instanceof TaskExecutionLockError
|
|
1031
|
+
? { code: error.code, message: error.message, retryable: error.code === 'ORCHESTRATION_LOCK_BUSY' }
|
|
1032
|
+
: errorOf(error, 'TASK_FINALIZATION_FAILED');
|
|
1033
|
+
return failed(resolved.taskId, detail);
|
|
978
1034
|
}
|
|
979
|
-
return terminalResult(taskId, receipt, { lifecycle: lifecycleResult, taskComment, verification, summary: summary.step, postSummaryVerification }, changed);
|
|
980
1035
|
}
|
|
981
1036
|
|
|
982
|
-
async function
|
|
1037
|
+
async function commitPreparedTaskFinalization(request: TaskFinalizationRequest, options: TaskFinalizationOptions): Promise<TaskFinalizationResult> {
|
|
983
1038
|
if (request.intent !== 'complete' || !request.taskRef || !request.agent) {
|
|
984
1039
|
return failed(null, { code: 'TASK_FINALIZATION_PAYLOAD_INVALID', message: 'complete finalization requires taskRef and agent', retryable: false });
|
|
985
1040
|
}
|
|
@@ -987,7 +1042,38 @@ async function applyTaskFinalization(request: TaskFinalizationRequest, options:
|
|
|
987
1042
|
const resolved = resolveTaskRef(request.taskRef, { repoRoot });
|
|
988
1043
|
if (!resolved.ok) return failed(resolved.taskId, { code: resolved.code, message: resolved.message, retryable: false });
|
|
989
1044
|
try {
|
|
990
|
-
return await withTaskExecutionLock(repoRoot, resolved.taskId, 'task-finalization.
|
|
1045
|
+
return await withTaskExecutionLock(repoRoot, resolved.taskId, 'task-finalization.commit', () => {
|
|
1046
|
+
const receipt = readReceipt(repoRoot, resolved.taskId);
|
|
1047
|
+
if (!receipt) return failed(resolved.taskId, {
|
|
1048
|
+
code: 'TASK_FINALIZATION_PREPARATION_REQUIRED', message: 'sandbox preparation receipt is unavailable', retryable: true
|
|
1049
|
+
});
|
|
1050
|
+
if (receipt.lifecycle === 'done' && resolved.state === 'completed') {
|
|
1051
|
+
return terminalResult(resolved.taskId, receipt, { lifecycle: { status: 'no-op', changed: false, error: null } }, false);
|
|
1052
|
+
}
|
|
1053
|
+
if (receipt.taskComment === 'pending' || receipt.verification === 'pending' || receipt.summary === 'pending' || receipt.warningProjection === 'pending') {
|
|
1054
|
+
return failed(resolved.taskId, {
|
|
1055
|
+
code: 'TASK_FINALIZATION_PREPARATION_REQUIRED', message: 'sandbox preparation has pending external steps', retryable: true
|
|
1056
|
+
}, { completedSteps: completedSteps(receipt), pendingSteps: pendingSteps(receipt), warnings: openWarnings(receipt) });
|
|
1057
|
+
}
|
|
1058
|
+
const registry = inspectShortIdRegistry(repoRoot);
|
|
1059
|
+
if (registry.status !== 'valid') return failed(resolved.taskId, {
|
|
1060
|
+
code: 'TASK_FINALIZATION_SHORT_ID_REGISTRY_UNAVAILABLE',
|
|
1061
|
+
message: `cannot verify canonical short-id registry: ${registry.error.code}: ${registry.error.message}`,
|
|
1062
|
+
retryable: false
|
|
1063
|
+
});
|
|
1064
|
+
const lifecycle = options.lifecycle ?? applyTaskLifecycle;
|
|
1065
|
+
const result = lifecycle(
|
|
1066
|
+
{ taskRef: resolved.taskId, intent: 'complete', agent: request.agent },
|
|
1067
|
+
{ repoRoot, ...(options.metadataProvider ? { metadataProvider: options.metadataProvider } : {}) }
|
|
1068
|
+
);
|
|
1069
|
+
const step = lifecycleStep(result);
|
|
1070
|
+
if (result.status !== 'applied' && result.status !== 'no-op') {
|
|
1071
|
+
const updated = updateReceipt(repoRoot, receipt, { lifecycle: 'pending', lastError: step.error });
|
|
1072
|
+
return terminalResult(resolved.taskId, updated, { lifecycle: step }, result.changed, step.error);
|
|
1073
|
+
}
|
|
1074
|
+
const updated = updateReceipt(repoRoot, receipt, { lifecycle: 'done', lastError: null });
|
|
1075
|
+
return terminalResult(resolved.taskId, updated, { lifecycle: step }, result.changed);
|
|
1076
|
+
});
|
|
991
1077
|
} catch (error) {
|
|
992
1078
|
const detail = error instanceof TaskExecutionLockError
|
|
993
1079
|
? { code: error.code, message: error.message, retryable: error.code === 'ORCHESTRATION_LOCK_BUSY' }
|
|
@@ -996,9 +1082,17 @@ async function applyTaskFinalization(request: TaskFinalizationRequest, options:
|
|
|
996
1082
|
}
|
|
997
1083
|
}
|
|
998
1084
|
|
|
1085
|
+
async function applyTaskFinalization(request: TaskFinalizationRequest, options: TaskFinalizationOptions): Promise<TaskFinalizationResult> {
|
|
1086
|
+
const prepared = await prepareTaskFinalization(request, options);
|
|
1087
|
+
if (prepared.status !== 'prepared') return prepared;
|
|
1088
|
+
return commitPreparedTaskFinalization(request, options);
|
|
1089
|
+
}
|
|
1090
|
+
|
|
999
1091
|
export {
|
|
1000
1092
|
applyFinalizationReceiptMutation,
|
|
1093
|
+
commitPreparedTaskFinalization,
|
|
1001
1094
|
applyTaskFinalization,
|
|
1095
|
+
prepareTaskFinalization,
|
|
1002
1096
|
issueCapability as createFinalizationCapability,
|
|
1003
1097
|
readTaskFinalizationReceipt,
|
|
1004
1098
|
terminalResult
|