@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
|
@@ -3,16 +3,19 @@ import path from 'node:path';
|
|
|
3
3
|
import { createHash, randomUUID } from 'node:crypto';
|
|
4
4
|
import { syncPlatformComment } from '../platform/issue-comments.js';
|
|
5
5
|
import { canonicalizeSummaryBody } from '../platform/comment-safety.js';
|
|
6
|
-
import { backfillCompletionComments } from '../platform/completion-backfill.js';
|
|
6
|
+
import { backfillCompletionComments, inspectCompletionBackfillEligibility } from '../platform/completion-backfill.js';
|
|
7
7
|
import { taskIssueIdentity } from '../platform/task-identities.js';
|
|
8
8
|
import { parseTaskFrontmatter } from './frontmatter.js';
|
|
9
|
-
import { applyTaskLifecycle } from './lifecycle.js';
|
|
9
|
+
import { applyTaskLifecycle, inspectTaskLifecycleProgress } from './lifecycle.js';
|
|
10
10
|
import { resolveTaskRef } from './resolve-ref.js';
|
|
11
11
|
import { inspectShortIdRegistry } from './short-id.js';
|
|
12
12
|
import { TaskExecutionLockError, withTaskExecutionLock } from './task-execution-lock.js';
|
|
13
13
|
import { verifyTaskEvent } from './verification.js';
|
|
14
14
|
import { projectFinalizationWarning } from './workflow-warning-intents.js';
|
|
15
15
|
import { mergeOperationWarnings } from './operation-outcome.js';
|
|
16
|
+
import { readTaskFinalizationHandoff } from './finalization-handoff.js';
|
|
17
|
+
import { readSandboxControlManifest } from '../sandbox/control/lifecycle.js';
|
|
18
|
+
import { validateSandboxControlIdentity } from '../sandbox/control/identity-sentinel.js';
|
|
16
19
|
const RECEIPT_VERSION = 4;
|
|
17
20
|
const FINALIZATION_STEPS = ['lifecycle', 'task-comment', 'verification', 'summary'];
|
|
18
21
|
function finalizationRoot(repoRoot) {
|
|
@@ -89,19 +92,32 @@ function validateReceipt(value, taskId) {
|
|
|
89
92
|
const receipt = value;
|
|
90
93
|
const controlBinding = receipt.controlBinding;
|
|
91
94
|
const states = ['pending', 'done', 'skipped'];
|
|
92
|
-
|
|
95
|
+
const requiredKeys = [
|
|
96
|
+
'version', 'taskId', 'intent', 'receiptId', 'revision', 'lifecycle', 'taskComment',
|
|
97
|
+
'verification', 'summary', 'warningProjection', 'warnings', 'updatedAt', 'lastError'
|
|
98
|
+
];
|
|
99
|
+
if (receipt.controlBinding !== undefined)
|
|
100
|
+
requiredKeys.push('controlBinding');
|
|
101
|
+
if (Object.keys(receipt).sort().join('\0') !== requiredKeys.sort().join('\0')
|
|
102
|
+
||
|
|
103
|
+
receipt.version !== RECEIPT_VERSION || receipt.taskId !== taskId || receipt.intent !== 'complete'
|
|
93
104
|
|| typeof receipt.receiptId !== 'string' || receipt.receiptId.length === 0
|
|
94
105
|
|| !Number.isSafeInteger(receipt.revision) || Number(receipt.revision) < 0
|
|
95
|
-
|| !states.includes(
|
|
96
|
-
|| !states.includes(
|
|
97
|
-
|| !states.includes(
|
|
98
|
-
|| !states.includes(
|
|
99
|
-
|| !['pending', 'done'].includes(
|
|
106
|
+
|| !states.includes(receipt.lifecycle)
|
|
107
|
+
|| !states.includes(receipt.taskComment)
|
|
108
|
+
|| !states.includes(receipt.verification)
|
|
109
|
+
|| !states.includes(receipt.summary)
|
|
110
|
+
|| !['pending', 'done'].includes(receipt.warningProjection)
|
|
100
111
|
|| !Array.isArray(receipt.warnings) || receipt.warnings.some((warning) => !validWarning(warning))
|
|
101
|
-
|| typeof receipt.updatedAt !== 'string'
|
|
102
|
-
|| (receipt.lastError !== null && (typeof receipt.lastError !== 'object' || Array.isArray(receipt.lastError)
|
|
112
|
+
|| typeof receipt.updatedAt !== 'string' || receipt.updatedAt.length === 0
|
|
113
|
+
|| (receipt.lastError !== null && (typeof receipt.lastError !== 'object' || Array.isArray(receipt.lastError)
|
|
114
|
+
|| Object.keys(receipt.lastError).sort().join('\0') !== ['code', 'message', 'retryable'].join('\0')
|
|
115
|
+
|| typeof receipt.lastError.code !== 'string'
|
|
116
|
+
|| typeof receipt.lastError.message !== 'string'
|
|
117
|
+
|| typeof receipt.lastError.retryable !== 'boolean'))
|
|
103
118
|
|| (receipt.controlBinding !== undefined && (!controlBinding || typeof controlBinding !== 'object'
|
|
104
119
|
|| Array.isArray(controlBinding)
|
|
120
|
+
|| Object.keys(controlBinding).sort().join('\0') !== ['generation', 'requestId'].join('\0')
|
|
105
121
|
|| typeof controlBinding.generation !== 'string' || controlBinding.generation.length === 0
|
|
106
122
|
|| typeof controlBinding.requestId !== 'string' || !/^[a-f0-9-]{16,64}$/u.test(controlBinding.requestId))))
|
|
107
123
|
throw new Error('receipt schema is invalid');
|
|
@@ -134,6 +150,12 @@ function writeReceipt(repoRoot, receipt) {
|
|
|
134
150
|
throw error;
|
|
135
151
|
}
|
|
136
152
|
}
|
|
153
|
+
export function bindTaskFinalizationReceipt(repoRoot, taskId, controlBinding) {
|
|
154
|
+
const receipt = readReceipt(path.resolve(repoRoot), taskId);
|
|
155
|
+
if (!receipt)
|
|
156
|
+
throw new Error('TASK_FINALIZATION_PREPARATION_REQUIRED');
|
|
157
|
+
return updateReceipt(path.resolve(repoRoot), receipt, { controlBinding, lastError: null });
|
|
158
|
+
}
|
|
137
159
|
function updateReceipt(repoRoot, receipt, patch) {
|
|
138
160
|
const current = readReceipt(repoRoot, receipt.taskId);
|
|
139
161
|
if (!current || current.receiptId !== receipt.receiptId)
|
|
@@ -221,6 +243,77 @@ function resolveStepWarnings(receipt, step) {
|
|
|
221
243
|
? { ...warning, status: 'resolved', resolvedAt: now() }
|
|
222
244
|
: warning);
|
|
223
245
|
}
|
|
246
|
+
function verificationWarnings(result) {
|
|
247
|
+
const warnings = [];
|
|
248
|
+
for (const invocation of result.invocations) {
|
|
249
|
+
const checks = Array.isArray(invocation.payload.checks) ? invocation.payload.checks : [invocation.payload];
|
|
250
|
+
for (const value of checks) {
|
|
251
|
+
if (!value || typeof value !== 'object' || Array.isArray(value))
|
|
252
|
+
continue;
|
|
253
|
+
const check = value;
|
|
254
|
+
const status = check.effectiveStatus ?? check.status;
|
|
255
|
+
const target = typeof check.checkId === 'string' && check.checkId ? check.checkId : typeof check.type === 'string' ? check.type : null;
|
|
256
|
+
if (!target || status === 'pass')
|
|
257
|
+
continue;
|
|
258
|
+
const blocked = status === 'blocked';
|
|
259
|
+
const message = [check.reason, check.message, check.action]
|
|
260
|
+
.filter((item) => typeof item === 'string' && item.length > 0)
|
|
261
|
+
.join(' - ');
|
|
262
|
+
warnings.push({
|
|
263
|
+
step: 'verification', target, code: blocked ? 'CHECK_BLOCKED' : 'CHECK_FAILED',
|
|
264
|
+
retryable: blocked, severity: 'ACTION_REQUIRED',
|
|
265
|
+
message: message || `verification ${String(status)}`
|
|
266
|
+
});
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
return mergeOperationWarnings(warnings);
|
|
270
|
+
}
|
|
271
|
+
function applyVerificationWarnings(receipt, result) {
|
|
272
|
+
if (result.status === 'pass')
|
|
273
|
+
return resolveStepWarnings(receipt, 'verification');
|
|
274
|
+
const observed = new Map();
|
|
275
|
+
for (const invocation of result.invocations) {
|
|
276
|
+
const checks = Array.isArray(invocation.payload.checks) ? invocation.payload.checks : [invocation.payload];
|
|
277
|
+
for (const value of checks) {
|
|
278
|
+
if (!value || typeof value !== 'object' || Array.isArray(value))
|
|
279
|
+
continue;
|
|
280
|
+
const check = value;
|
|
281
|
+
const target = typeof check.checkId === 'string' && check.checkId ? check.checkId : typeof check.type === 'string' ? check.type : null;
|
|
282
|
+
if (!target)
|
|
283
|
+
continue;
|
|
284
|
+
const status = check.effectiveStatus ?? check.status;
|
|
285
|
+
if (status === 'pass')
|
|
286
|
+
observed.set(target, 'pass');
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
for (const warning of verificationWarnings(result))
|
|
290
|
+
observed.set(warning.target, warning);
|
|
291
|
+
if (observed.size === 0) {
|
|
292
|
+
return replaceWarning(receipt, warningFromError('verification', verificationFailure(result)), 'open');
|
|
293
|
+
}
|
|
294
|
+
let warnings = [...receipt.warnings];
|
|
295
|
+
for (const [target, current] of observed) {
|
|
296
|
+
if (current === 'pass') {
|
|
297
|
+
warnings = warnings.map((warning) => warning.step === 'verification' && warning.target === target && warning.status === 'open'
|
|
298
|
+
? { ...warning, status: 'resolved', resolvedAt: now() }
|
|
299
|
+
: warning);
|
|
300
|
+
continue;
|
|
301
|
+
}
|
|
302
|
+
warnings = warnings.map((warning) => warning.step === 'verification' && warning.target === target
|
|
303
|
+
&& warning.status === 'open' && warning.code !== current.code
|
|
304
|
+
? { ...warning, status: 'resolved', resolvedAt: now() }
|
|
305
|
+
: warning);
|
|
306
|
+
const key = warningKey(current);
|
|
307
|
+
warnings = [...warnings.filter((warning) => warningKey(warning) !== key), warningRecord(current, 'open')];
|
|
308
|
+
}
|
|
309
|
+
return warnings;
|
|
310
|
+
}
|
|
311
|
+
function shouldRunBackfill(receipt) {
|
|
312
|
+
return receipt.lifecycle !== 'done'
|
|
313
|
+
|| receipt.taskComment === 'pending'
|
|
314
|
+
|| receipt.verification === 'pending'
|
|
315
|
+
|| receipt.warnings.some((warning) => warning.step === 'backfill' && warning.status === 'open');
|
|
316
|
+
}
|
|
224
317
|
function issueCapability(receipt, scope) {
|
|
225
318
|
return {
|
|
226
319
|
receiptId: receipt.receiptId,
|
|
@@ -236,6 +329,21 @@ function capabilityError(code, message) {
|
|
|
236
329
|
Object.assign(error, { code });
|
|
237
330
|
return error;
|
|
238
331
|
}
|
|
332
|
+
function hasPreparedTaskDocument(repoRoot, taskId) {
|
|
333
|
+
const resolved = resolveTaskRef(taskId, { repoRoot });
|
|
334
|
+
if (!resolved.ok)
|
|
335
|
+
return false;
|
|
336
|
+
if (resolved.state === 'completed')
|
|
337
|
+
return true;
|
|
338
|
+
if (resolved.state !== 'active')
|
|
339
|
+
return false;
|
|
340
|
+
try {
|
|
341
|
+
return parseTaskFrontmatter(fs.readFileSync(resolved.taskMdPath, 'utf8')).status === 'completed';
|
|
342
|
+
}
|
|
343
|
+
catch {
|
|
344
|
+
return false;
|
|
345
|
+
}
|
|
346
|
+
}
|
|
239
347
|
function mutationKeys(value) {
|
|
240
348
|
return Object.keys(value).sort();
|
|
241
349
|
}
|
|
@@ -305,9 +413,8 @@ function mutationPatch(current, mutation) {
|
|
|
305
413
|
: { warningProjection: 'pending', lastError: mutation.error };
|
|
306
414
|
}
|
|
307
415
|
function applyFinalizationReceiptMutationUnderLock(repoRoot, receipt, capability, mutation, consumed) {
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
throw capabilityError('FINALIZATION_SCOPE_INVALID', 'finalization capability requires a completed task');
|
|
416
|
+
if (!hasPreparedTaskDocument(repoRoot, receipt.taskId)) {
|
|
417
|
+
throw capabilityError('FINALIZATION_SCOPE_INVALID', 'finalization capability requires an active prepared task or a completed task');
|
|
311
418
|
}
|
|
312
419
|
const current = readReceipt(repoRoot, receipt.taskId);
|
|
313
420
|
if (!current || current.receiptId !== receipt.receiptId || current.revision !== receipt.revision) {
|
|
@@ -319,11 +426,10 @@ function applyFinalizationReceiptMutationUnderLock(repoRoot, receipt, capability
|
|
|
319
426
|
return next;
|
|
320
427
|
}
|
|
321
428
|
function applyFinalizationReceiptMutation(repoRoot, receipt, capability, mutation) {
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
throw capabilityError('FINALIZATION_SCOPE_INVALID', 'finalization capability requires a completed task');
|
|
429
|
+
if (!hasPreparedTaskDocument(repoRoot, receipt.taskId)) {
|
|
430
|
+
throw capabilityError('FINALIZATION_SCOPE_INVALID', 'finalization capability requires an active prepared task or a completed task');
|
|
325
431
|
}
|
|
326
|
-
return withTaskExecutionLock(repoRoot,
|
|
432
|
+
return withTaskExecutionLock(repoRoot, receipt.taskId, 'task-finalization.receipt-mutation', () => applyFinalizationReceiptMutationUnderLock(repoRoot, receipt, capability, mutation, new Set()));
|
|
327
433
|
}
|
|
328
434
|
function warningKey(warning) {
|
|
329
435
|
return `${warning.step}\0${warning.code}\0${warning.target}`;
|
|
@@ -334,7 +440,8 @@ function reconcileWarningProjection(repoRoot, taskId, receipt, consumed) {
|
|
|
334
440
|
const warnings = [...new Map(receipt.warnings.map((warning) => [warningKey(warning), warning])).values()];
|
|
335
441
|
for (const warning of warnings) {
|
|
336
442
|
try {
|
|
337
|
-
const
|
|
443
|
+
const superseded = warning.status === 'resolved' && receipt.warnings.some((item) => item.status === 'open' && item.step === warning.step && item.target === warning.target && item.code !== warning.code);
|
|
444
|
+
const projected = projectFinalizationWarning(taskId, warning, { repoRoot, resolution: superseded ? 'superseded' : 'passed' });
|
|
338
445
|
if (projected.status === 'failed') {
|
|
339
446
|
const detail = {
|
|
340
447
|
code: projected.error?.code || 'FINALIZATION_WARNING_PROJECTION_FAILED',
|
|
@@ -433,8 +540,12 @@ async function syncPendingSummary(input) {
|
|
|
433
540
|
let { receipt } = input;
|
|
434
541
|
try {
|
|
435
542
|
const resolved = resolveTaskRef(input.taskId, { repoRoot: input.repoRoot });
|
|
436
|
-
if (!resolved.ok ||
|
|
437
|
-
throw Object.assign(new Error('
|
|
543
|
+
if (!resolved.ok || !hasPreparedTaskDocument(input.repoRoot, input.taskId))
|
|
544
|
+
throw Object.assign(new Error('prepared task is unavailable for summary sync'), { code: 'SUMMARY_STAGING_INVALID' });
|
|
545
|
+
if (!fs.existsSync(path.join(resolved.taskDir, '.delivery-summary.json'))) {
|
|
546
|
+
receipt = updateReceipt(input.repoRoot, receipt, { summary: 'skipped', lastError: null });
|
|
547
|
+
return { receipt, step: { status: 'skipped', changed: false, error: null }, changed: false, error: null };
|
|
548
|
+
}
|
|
438
549
|
if (!taskIssueIdentity(parseTaskFrontmatter(fs.readFileSync(resolved.taskMdPath, 'utf8')))) {
|
|
439
550
|
receipt = updateReceipt(input.repoRoot, receipt, {
|
|
440
551
|
summary: 'skipped', lastError: null
|
|
@@ -509,7 +620,19 @@ function terminalResult(taskId, receipt, steps, changed, error = null) {
|
|
|
509
620
|
error: hardError ? error : postLifecyclePending ? null : error
|
|
510
621
|
};
|
|
511
622
|
}
|
|
512
|
-
|
|
623
|
+
function preparedResult(taskId, receipt, steps, changed) {
|
|
624
|
+
return {
|
|
625
|
+
status: 'prepared', changed, taskId,
|
|
626
|
+
backfill: steps.backfill ?? null,
|
|
627
|
+
lifecycle: steps.lifecycle ?? null,
|
|
628
|
+
taskComment: steps.taskComment ?? null,
|
|
629
|
+
verification: steps.verification ?? null,
|
|
630
|
+
summary: steps.summary ?? null,
|
|
631
|
+
completedSteps: completedSteps(receipt), pendingSteps: pendingSteps(receipt),
|
|
632
|
+
result: 'prepared', warnings: openWarnings(receipt), error: null
|
|
633
|
+
};
|
|
634
|
+
}
|
|
635
|
+
async function prepareUnderLock(request, taskId, options) {
|
|
513
636
|
const repoRoot = path.resolve(options.repoRoot);
|
|
514
637
|
const lifecycle = options.lifecycle ?? applyTaskLifecycle;
|
|
515
638
|
const backfill = options.backfill ?? backfillCompletionComments;
|
|
@@ -528,8 +651,15 @@ async function applyUnderLock(request, taskId, options) {
|
|
|
528
651
|
return failed(taskId, errorOf(error, 'TASK_FINALIZATION_RECEIPT_INVALID'));
|
|
529
652
|
}
|
|
530
653
|
if (options.controlBinding && (receipt.controlBinding?.generation !== options.controlBinding.generation
|
|
531
|
-
|| receipt.controlBinding.requestId !== options.controlBinding.requestId))
|
|
532
|
-
|
|
654
|
+
|| receipt.controlBinding.requestId !== options.controlBinding.requestId)) {
|
|
655
|
+
const state = resolveTaskRef(taskId, { repoRoot });
|
|
656
|
+
if (!(receipt.lifecycle === 'done' && state.ok && state.state === 'completed')) {
|
|
657
|
+
return failed(taskId, {
|
|
658
|
+
code: 'TASK_FINALIZATION_RECOVERY_PROOF_UNAVAILABLE',
|
|
659
|
+
message: 'canonical receipt is not bound to the current sandbox request', retryable: false
|
|
660
|
+
});
|
|
661
|
+
}
|
|
662
|
+
}
|
|
533
663
|
const preflightState = resolveTaskRef(taskId, { repoRoot });
|
|
534
664
|
if (options.preflight && preflightState.ok && preflightState.state === 'active') {
|
|
535
665
|
try {
|
|
@@ -551,80 +681,87 @@ async function applyUnderLock(request, taskId, options) {
|
|
|
551
681
|
}
|
|
552
682
|
let changed = false;
|
|
553
683
|
let backfillResult = null;
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
684
|
+
let runBackfill = shouldRunBackfill(receipt);
|
|
685
|
+
if (!runBackfill && receipt.lifecycle === 'done') {
|
|
686
|
+
const eligibility = inspectCompletionBackfillEligibility(taskId, { cwd: repoRoot });
|
|
687
|
+
if (eligibility.status === 'failed')
|
|
688
|
+
return failed(taskId, eligibility.error, {
|
|
689
|
+
completedSteps: completedSteps(receipt), pendingSteps: pendingSteps(receipt), warnings: openWarnings(receipt)
|
|
690
|
+
});
|
|
691
|
+
runBackfill = eligibility.eligible;
|
|
692
|
+
}
|
|
693
|
+
if (receipt.lifecycle === 'done' && runBackfill && receipt.summary === 'done') {
|
|
694
|
+
receipt = updateReceipt(repoRoot, receipt, { summary: 'pending' });
|
|
695
|
+
}
|
|
696
|
+
if (runBackfill)
|
|
697
|
+
try {
|
|
698
|
+
const result = await backfill(taskId, { agent: request.agent, cwd: repoRoot });
|
|
699
|
+
backfillResult = commentStep(result);
|
|
700
|
+
const issueNotLinked = result.status === 'no-op' && result.error?.code === 'ISSUE_NOT_LINKED';
|
|
701
|
+
if (!issueNotLinked && (result.status !== 'applied' && result.status !== 'no-op' || result.error)) {
|
|
702
|
+
const detail = backfillResult.error ?? {
|
|
703
|
+
code: 'COMPLETION_BACKFILL_FAILED', message: 'completion artifact backfill did not reach a successful terminal result', retryable: true
|
|
704
|
+
};
|
|
705
|
+
if (receipt.lifecycle !== 'done')
|
|
706
|
+
return failed(taskId, detail, {
|
|
707
|
+
backfill: backfillResult,
|
|
708
|
+
completedSteps: completedSteps(receipt),
|
|
709
|
+
pendingSteps: pendingSteps(receipt)
|
|
710
|
+
});
|
|
711
|
+
const warnings = replaceWarning(receipt, warningFromError('backfill', detail), 'open');
|
|
712
|
+
receipt = updateReceipt(repoRoot, receipt, { warningProjection: 'pending', warnings, lastError: detail });
|
|
713
|
+
receipt = reconcileWarningProjection(repoRoot, taskId, receipt, consumedCapabilities);
|
|
714
|
+
if (!detail.retryable)
|
|
715
|
+
return failed(taskId, detail, {
|
|
716
|
+
changed, backfill: backfillResult, completedSteps: completedSteps(receipt),
|
|
717
|
+
pendingSteps: pendingSteps(receipt), warnings: openWarnings(receipt)
|
|
718
|
+
});
|
|
719
|
+
return terminalResult(taskId, receipt, { backfill: backfillResult }, changed, detail);
|
|
720
|
+
}
|
|
721
|
+
changed = result.changed;
|
|
722
|
+
if (receipt.warnings.some((warning) => warning.step === 'backfill' && warning.status === 'open')) {
|
|
723
|
+
const warnings = receipt.warnings.map((warning) => warning.step === 'backfill' && warning.status === 'open'
|
|
724
|
+
? { ...warning, status: 'resolved', resolvedAt: now() }
|
|
725
|
+
: warning);
|
|
726
|
+
receipt = updateReceipt(repoRoot, receipt, { warningProjection: 'pending', warnings, lastError: null });
|
|
727
|
+
}
|
|
728
|
+
if (result.changed && receipt.taskComment !== 'pending') {
|
|
729
|
+
receipt = updateReceipt(repoRoot, receipt, { taskComment: 'pending' });
|
|
730
|
+
}
|
|
731
|
+
}
|
|
732
|
+
catch (error) {
|
|
733
|
+
const detail = errorOf(error, 'COMPLETION_BACKFILL_FAILED', true);
|
|
734
|
+
if (receipt.lifecycle === 'done') {
|
|
735
|
+
const warnings = replaceWarning(receipt, warningFromError('backfill', detail), 'open');
|
|
736
|
+
receipt = updateReceipt(repoRoot, receipt, { warningProjection: 'pending', warnings, lastError: detail });
|
|
737
|
+
receipt = reconcileWarningProjection(repoRoot, taskId, receipt, consumedCapabilities);
|
|
738
|
+
return terminalResult(taskId, receipt, {
|
|
739
|
+
backfill: { status: 'blocked', changed: false, error: detail }
|
|
740
|
+
}, changed, detail);
|
|
741
|
+
}
|
|
562
742
|
return failed(taskId, detail, {
|
|
563
|
-
backfill:
|
|
743
|
+
backfill: { status: detail.retryable ? 'blocked' : 'failed', changed: false, error: detail },
|
|
564
744
|
completedSteps: completedSteps(receipt),
|
|
565
745
|
pendingSteps: pendingSteps(receipt)
|
|
566
746
|
});
|
|
567
747
|
}
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
receipt = updateReceipt(repoRoot, receipt, { taskComment: 'pending' });
|
|
571
|
-
}
|
|
572
|
-
}
|
|
573
|
-
catch (error) {
|
|
574
|
-
const detail = errorOf(error, 'COMPLETION_BACKFILL_FAILED', true);
|
|
575
|
-
return failed(taskId, detail, {
|
|
576
|
-
backfill: { status: detail.retryable ? 'blocked' : 'failed', changed: false, error: detail },
|
|
577
|
-
completedSteps: completedSteps(receipt),
|
|
578
|
-
pendingSteps: pendingSteps(receipt)
|
|
579
|
-
});
|
|
580
|
-
}
|
|
748
|
+
else
|
|
749
|
+
backfillResult = { status: 'no-op', changed: false, error: null };
|
|
581
750
|
let lifecycleResult = null;
|
|
582
751
|
try {
|
|
583
752
|
const resolved = resolveTaskRef(taskId, { repoRoot });
|
|
584
|
-
|
|
585
|
-
if (registry && registry.status !== 'valid') {
|
|
586
|
-
const detail = {
|
|
587
|
-
code: 'TASK_FINALIZATION_SHORT_ID_REGISTRY_UNAVAILABLE',
|
|
588
|
-
message: `cannot verify canonical short-id registry: ${registry.error.code}: ${registry.error.message}`,
|
|
589
|
-
retryable: false
|
|
590
|
-
};
|
|
591
|
-
receipt = updateReceipt(repoRoot, receipt, { lifecycle: 'pending', lastError: detail });
|
|
592
|
-
return terminalResult(taskId, receipt, {
|
|
593
|
-
backfill: backfillResult,
|
|
594
|
-
lifecycle: { status: 'failed', changed: false, error: detail },
|
|
595
|
-
taskComment: null,
|
|
596
|
-
verification: null
|
|
597
|
-
}, changed, detail);
|
|
598
|
-
}
|
|
599
|
-
const shortIds = registry?.status === 'valid' ? registry.shortIds : null;
|
|
600
|
-
if (shortIds?.has(taskId)) {
|
|
601
|
-
const detail = {
|
|
602
|
-
code: 'TASK_FINALIZATION_CANONICAL_STATE_INVALID',
|
|
603
|
-
message: 'completed task still has an active short-id registry entry',
|
|
604
|
-
retryable: false
|
|
605
|
-
};
|
|
606
|
-
receipt = updateReceipt(repoRoot, receipt, { lifecycle: 'pending', lastError: detail });
|
|
607
|
-
return terminalResult(taskId, receipt, {
|
|
608
|
-
backfill: backfillResult,
|
|
609
|
-
lifecycle: { status: 'failed', changed: false, error: detail },
|
|
610
|
-
taskComment: null,
|
|
611
|
-
verification: null
|
|
612
|
-
}, changed, detail);
|
|
613
|
-
}
|
|
614
|
-
const lifecycleDone = receipt.lifecycle === 'done' && resolved.ok && resolved.state === 'completed' && shortIds !== null;
|
|
615
|
-
if (lifecycleDone) {
|
|
753
|
+
if (receipt.lifecycle === 'done' && resolved.ok && resolved.state === 'completed') {
|
|
616
754
|
lifecycleResult = { status: 'no-op', changed: false, error: null };
|
|
617
755
|
receipt = updateReceipt(repoRoot, receipt, { lifecycle: 'done', lastError: null });
|
|
618
756
|
}
|
|
619
757
|
else {
|
|
620
|
-
const result = lifecycle({ taskRef: taskId, intent: 'complete', agent: request.agent }, { repoRoot, ...(options.metadataProvider ? { metadataProvider: options.metadataProvider } : {}) });
|
|
758
|
+
const result = lifecycle({ taskRef: taskId, intent: 'complete', agent: request.agent }, { repoRoot, prepareOnly: true, ...(options.metadataProvider ? { metadataProvider: options.metadataProvider } : {}) });
|
|
621
759
|
lifecycleResult = lifecycleStep(result);
|
|
622
760
|
if (result.status !== 'applied' && result.status !== 'no-op') {
|
|
623
761
|
receipt = updateReceipt(repoRoot, receipt, { lifecycle: 'pending', lastError: lifecycleResult.error });
|
|
624
762
|
return terminalResult(taskId, receipt, { backfill: backfillResult, lifecycle: lifecycleResult, taskComment: null, verification: null }, result.changed, lifecycleResult.error);
|
|
625
763
|
}
|
|
626
764
|
changed = changed || result.changed;
|
|
627
|
-
receipt = updateReceipt(repoRoot, receipt, { lifecycle: 'done', lastError: null });
|
|
628
765
|
}
|
|
629
766
|
}
|
|
630
767
|
catch (error) {
|
|
@@ -637,27 +774,26 @@ async function applyUnderLock(request, taskId, options) {
|
|
|
637
774
|
}
|
|
638
775
|
let taskComment = null;
|
|
639
776
|
receipt = reconcileWarningProjection(repoRoot, taskId, receipt, consumedCapabilities);
|
|
640
|
-
const initialComment = await syncPendingTaskComment({
|
|
641
|
-
repoRoot, taskId, agent: request.agent, receipt, commentSync, consumedCapabilities
|
|
642
|
-
});
|
|
643
|
-
receipt = initialComment.receipt;
|
|
644
|
-
taskComment = initialComment.step;
|
|
645
|
-
changed = changed || initialComment.changed;
|
|
646
|
-
if (initialComment.error) {
|
|
647
|
-
return terminalResult(taskId, receipt, { backfill: backfillResult, lifecycle: lifecycleResult, taskComment, verification: null }, changed, initialComment.error);
|
|
648
|
-
}
|
|
649
777
|
let verification = null;
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
778
|
+
if (receipt.verification !== 'pending') {
|
|
779
|
+
verification = { status: receipt.verification === 'skipped' ? 'skipped' : 'no-op', changed: false, error: null };
|
|
780
|
+
}
|
|
781
|
+
else {
|
|
782
|
+
try {
|
|
783
|
+
receipt = reconcileWarningProjection(repoRoot, taskId, receipt, consumedCapabilities);
|
|
784
|
+
const verificationState = resolveTaskRef(taskId, { repoRoot });
|
|
785
|
+
const verificationEvent = verificationState.ok && verificationState.state === 'completed'
|
|
786
|
+
? 'complete-task.completed'
|
|
787
|
+
: 'complete-task.prepared';
|
|
788
|
+
const result = await verify({ taskRef: taskId, event: verificationEvent }, { repoRoot });
|
|
789
|
+
verification = verificationStep(result);
|
|
790
|
+
if (result.status === 'pass') {
|
|
791
|
+
const resolvedTaskWarning = receipt.warnings.some((warning) => warning.step === 'verification' && warning.status === 'open');
|
|
792
|
+
const warnings = applyVerificationWarnings(receipt, result);
|
|
793
|
+
receipt = updateReceipt(repoRoot, receipt, {
|
|
794
|
+
verification: 'done', taskComment: resolvedTaskWarning ? 'pending' : receipt.taskComment,
|
|
795
|
+
warningProjection: warnings.length > 0 ? 'pending' : 'done', warnings, lastError: null
|
|
796
|
+
});
|
|
661
797
|
receipt = reconcileWarningProjection(repoRoot, taskId, receipt, consumedCapabilities);
|
|
662
798
|
const finalComment = await syncPendingTaskComment({
|
|
663
799
|
repoRoot, taskId, agent: request.agent, receipt, commentSync, consumedCapabilities
|
|
@@ -671,72 +807,58 @@ async function applyUnderLock(request, taskId, options) {
|
|
|
671
807
|
}
|
|
672
808
|
}
|
|
673
809
|
else {
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
}
|
|
677
|
-
else {
|
|
678
|
-
const detail = verification.error ?? { code: 'VERIFY_FAILED', message: 'verification failed', retryable: true };
|
|
679
|
-
if (verificationPending) {
|
|
680
|
-
const capability = issueCapability(receipt, 'verification');
|
|
681
|
-
receipt = applyFinalizationReceiptMutationUnderLock(repoRoot, receipt, capability, {
|
|
682
|
-
scope: 'verification', operation: 'failed', error: detail
|
|
683
|
-
}, consumedCapabilities);
|
|
684
|
-
receipt = reconcileWarningProjection(repoRoot, taskId, receipt, consumedCapabilities);
|
|
685
|
-
}
|
|
686
|
-
else {
|
|
687
|
-
const warnings = replaceWarning(receipt, warningFromError('verification', detail), 'open');
|
|
810
|
+
const detail = verification.error ?? { code: 'VERIFY_FAILED', message: 'verification failed', retryable: true };
|
|
811
|
+
const warnings = applyVerificationWarnings(receipt, result);
|
|
688
812
|
receipt = updateReceipt(repoRoot, receipt, {
|
|
689
813
|
verification: 'pending', taskComment: 'pending', warningProjection: 'pending', warnings, lastError: detail
|
|
690
814
|
});
|
|
691
815
|
receipt = reconcileWarningProjection(repoRoot, taskId, receipt, consumedCapabilities);
|
|
692
|
-
const warningComment = await syncPendingTaskComment({
|
|
816
|
+
const warningComment = await syncPendingTaskComment({
|
|
817
|
+
repoRoot, taskId, agent: request.agent, receipt, commentSync, consumedCapabilities
|
|
818
|
+
});
|
|
693
819
|
receipt = warningComment.receipt;
|
|
694
|
-
|
|
695
|
-
taskComment = warningComment.step;
|
|
820
|
+
taskComment = warningComment.step;
|
|
696
821
|
changed = changed || warningComment.changed;
|
|
822
|
+
if (warningComment.error) {
|
|
823
|
+
return terminalResult(taskId, receipt, { backfill: backfillResult, lifecycle: lifecycleResult, taskComment, verification }, changed, warningComment.error);
|
|
824
|
+
}
|
|
825
|
+
return terminalResult(taskId, receipt, { backfill: backfillResult, lifecycle: lifecycleResult, taskComment, verification }, changed, detail);
|
|
697
826
|
}
|
|
698
|
-
return terminalResult(taskId, receipt, { backfill: backfillResult, lifecycle: lifecycleResult, taskComment, verification }, changed, detail);
|
|
699
827
|
}
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
try {
|
|
704
|
-
if (receipt.verification === 'pending') {
|
|
705
|
-
const capability = issueCapability(receipt, 'verification');
|
|
706
|
-
receipt = applyFinalizationReceiptMutationUnderLock(repoRoot, receipt, capability, {
|
|
707
|
-
scope: 'verification', operation: 'failed', error: detail
|
|
708
|
-
}, consumedCapabilities);
|
|
709
|
-
}
|
|
710
|
-
else {
|
|
828
|
+
catch (error) {
|
|
829
|
+
const detail = errorOf(error, 'VERIFY_FAILED', true);
|
|
830
|
+
try {
|
|
711
831
|
const warnings = replaceWarning(receipt, warningFromError('verification', detail), 'open');
|
|
712
832
|
receipt = updateReceipt(repoRoot, receipt, {
|
|
713
833
|
verification: 'pending', taskComment: 'pending', warningProjection: 'pending', warnings, lastError: detail
|
|
714
834
|
});
|
|
835
|
+
receipt = reconcileWarningProjection(repoRoot, taskId, receipt, consumedCapabilities);
|
|
715
836
|
}
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
837
|
+
catch (writeError) {
|
|
838
|
+
const persistence = errorOf(writeError, 'FINALIZATION_RECEIPT_WRITE_FAILED', true);
|
|
839
|
+
const hardError = {
|
|
840
|
+
code: 'FINALIZATION_RECEIPT_WRITE_FAILED',
|
|
841
|
+
message: `Unable to persist verification failure (${detail.code}: ${detail.message}): ${persistence.message}`,
|
|
842
|
+
retryable: true
|
|
843
|
+
};
|
|
844
|
+
return terminalResult(taskId, receipt, {
|
|
845
|
+
backfill: backfillResult, lifecycle: lifecycleResult, taskComment, verification: { status: 'blocked', changed: false, error: detail }
|
|
846
|
+
}, changed, hardError);
|
|
847
|
+
}
|
|
848
|
+
try {
|
|
849
|
+
const warningComment = await syncPendingTaskComment({ repoRoot, taskId, agent: request.agent, receipt, commentSync, consumedCapabilities });
|
|
850
|
+
receipt = warningComment.receipt;
|
|
851
|
+
if (warningComment.step.status !== 'no-op')
|
|
852
|
+
taskComment = warningComment.step;
|
|
853
|
+
changed = changed || warningComment.changed;
|
|
854
|
+
}
|
|
855
|
+
catch { /* preserve the primary error */ }
|
|
856
|
+
return terminalResult(taskId, receipt, { backfill: backfillResult, lifecycle: lifecycleResult, taskComment, verification: { status: 'blocked', changed: false, error: detail } }, changed, detail);
|
|
735
857
|
}
|
|
736
|
-
catch { /* preserve the primary error */ }
|
|
737
|
-
return terminalResult(taskId, receipt, { backfill: backfillResult, lifecycle: lifecycleResult, taskComment, verification: { status: 'blocked', changed: false, error: detail } }, changed, detail);
|
|
738
858
|
}
|
|
739
|
-
let summary =
|
|
859
|
+
let summary = receipt.summary === 'pending'
|
|
860
|
+
? await syncPendingSummary({ repoRoot, taskId, agent: request.agent, receipt, commentSync })
|
|
861
|
+
: { receipt, step: { status: 'no-op', changed: false, error: null }, changed: false, error: null };
|
|
740
862
|
receipt = summary.receipt;
|
|
741
863
|
changed = changed || summary.changed;
|
|
742
864
|
if (summary.error) {
|
|
@@ -744,9 +866,20 @@ async function applyUnderLock(request, taskId, options) {
|
|
|
744
866
|
return terminalResult(taskId, receipt, { backfill: backfillResult, lifecycle: lifecycleResult, taskComment, verification, summary: summary.step }, changed, summary.error);
|
|
745
867
|
}
|
|
746
868
|
receipt = reconcileWarningProjection(repoRoot, taskId, receipt, consumedCapabilities);
|
|
747
|
-
|
|
869
|
+
if (receipt.taskComment === 'pending') {
|
|
870
|
+
const finalComment = await syncPendingTaskComment({
|
|
871
|
+
repoRoot, taskId, agent: request.agent, receipt, commentSync, consumedCapabilities
|
|
872
|
+
});
|
|
873
|
+
receipt = finalComment.receipt;
|
|
874
|
+
taskComment = finalComment.step;
|
|
875
|
+
changed = changed || finalComment.changed;
|
|
876
|
+
if (finalComment.error) {
|
|
877
|
+
return terminalResult(taskId, receipt, { backfill: backfillResult, lifecycle: lifecycleResult, taskComment, verification, summary: summary.step }, changed, finalComment.error);
|
|
878
|
+
}
|
|
879
|
+
}
|
|
880
|
+
return preparedResult(taskId, receipt, { backfill: backfillResult, lifecycle: lifecycleResult, taskComment, verification, summary: summary.step }, changed);
|
|
748
881
|
}
|
|
749
|
-
async function
|
|
882
|
+
async function prepareTaskFinalization(request, options) {
|
|
750
883
|
if (request.intent !== 'complete' || !request.taskRef || !request.agent) {
|
|
751
884
|
return failed(null, { code: 'TASK_FINALIZATION_PAYLOAD_INVALID', message: 'complete finalization requires taskRef and agent', retryable: false });
|
|
752
885
|
}
|
|
@@ -755,7 +888,7 @@ async function applyTaskFinalization(request, options) {
|
|
|
755
888
|
if (!resolved.ok)
|
|
756
889
|
return failed(resolved.taskId, { code: resolved.code, message: resolved.message, retryable: false });
|
|
757
890
|
try {
|
|
758
|
-
return await withTaskExecutionLock(repoRoot, resolved.taskId, 'task-finalization.
|
|
891
|
+
return await withTaskExecutionLock(repoRoot, resolved.taskId, 'task-finalization.prepare', () => prepareUnderLock(request, resolved.taskId, options));
|
|
759
892
|
}
|
|
760
893
|
catch (error) {
|
|
761
894
|
const detail = error instanceof TaskExecutionLockError
|
|
@@ -764,5 +897,141 @@ async function applyTaskFinalization(request, options) {
|
|
|
764
897
|
return failed(resolved.taskId, detail);
|
|
765
898
|
}
|
|
766
899
|
}
|
|
767
|
-
|
|
900
|
+
async function commitPreparedTaskFinalization(request, options) {
|
|
901
|
+
if (request.intent !== 'complete' || !request.taskRef || !request.agent) {
|
|
902
|
+
return failed(null, { code: 'TASK_FINALIZATION_PAYLOAD_INVALID', message: 'complete finalization requires taskRef and agent', retryable: false });
|
|
903
|
+
}
|
|
904
|
+
const repoRoot = path.resolve(options.repoRoot);
|
|
905
|
+
const resolved = resolveTaskRef(request.taskRef, { repoRoot });
|
|
906
|
+
if (!resolved.ok)
|
|
907
|
+
return failed(resolved.taskId, { code: resolved.code, message: resolved.message, retryable: false });
|
|
908
|
+
try {
|
|
909
|
+
return await withTaskExecutionLock(repoRoot, resolved.taskId, 'task-finalization.commit', () => {
|
|
910
|
+
if (options.controlBinding && options.manifestPath) {
|
|
911
|
+
try {
|
|
912
|
+
const manifest = readSandboxControlManifest(options.manifestPath);
|
|
913
|
+
const identity = validateSandboxControlIdentity({
|
|
914
|
+
publicStatusDir: manifest.publicStatusDir,
|
|
915
|
+
root: path.dirname(path.resolve(options.manifestPath)),
|
|
916
|
+
mode: manifest.mode,
|
|
917
|
+
taskId: manifest.taskId,
|
|
918
|
+
generation: manifest.generation,
|
|
919
|
+
controlRootId: manifest.controlRootId
|
|
920
|
+
});
|
|
921
|
+
if (identity.state !== 'valid' || manifest.taskId !== resolved.taskId
|
|
922
|
+
|| manifest.generation !== options.controlBinding.generation
|
|
923
|
+
|| fs.realpathSync.native(manifest.repoRoot) !== fs.realpathSync.native(repoRoot)) {
|
|
924
|
+
throw new Error('current sandbox manifest does not match finalization request');
|
|
925
|
+
}
|
|
926
|
+
}
|
|
927
|
+
catch (error) {
|
|
928
|
+
return failed(resolved.taskId, {
|
|
929
|
+
code: 'TASK_FINALIZATION_RECOVERY_PROOF_UNAVAILABLE',
|
|
930
|
+
message: error instanceof Error ? error.message : String(error), retryable: false
|
|
931
|
+
});
|
|
932
|
+
}
|
|
933
|
+
}
|
|
934
|
+
let receipt = readReceipt(repoRoot, resolved.taskId);
|
|
935
|
+
if (!receipt && options.controlBinding && options.handoffDirectory && request.handoffSha256) {
|
|
936
|
+
try {
|
|
937
|
+
const handoff = readTaskFinalizationHandoff(options.handoffDirectory, resolved.taskId, options.controlBinding, request.handoffSha256);
|
|
938
|
+
receipt = validateReceipt(handoff.receipt, resolved.taskId);
|
|
939
|
+
if (receipt.lifecycle !== 'pending')
|
|
940
|
+
throw new Error('handoff lifecycle must be pending');
|
|
941
|
+
if (inspectTaskLifecycleProgress(repoRoot, resolved.taskId, request.agent) === 'unknown') {
|
|
942
|
+
throw new Error('TASK_FINALIZATION_RECOVERY_PROOF_UNAVAILABLE');
|
|
943
|
+
}
|
|
944
|
+
writeReceipt(repoRoot, receipt);
|
|
945
|
+
try {
|
|
946
|
+
handoff.cleanup();
|
|
947
|
+
}
|
|
948
|
+
catch { /* keep the persisted canonical receipt usable */ }
|
|
949
|
+
}
|
|
950
|
+
catch (error) {
|
|
951
|
+
return failed(resolved.taskId, {
|
|
952
|
+
code: 'TASK_FINALIZATION_HANDOFF_INVALID',
|
|
953
|
+
message: error instanceof Error ? error.message : String(error), retryable: false
|
|
954
|
+
});
|
|
955
|
+
}
|
|
956
|
+
}
|
|
957
|
+
if (!receipt)
|
|
958
|
+
return failed(resolved.taskId, {
|
|
959
|
+
code: 'TASK_FINALIZATION_PREPARATION_REQUIRED', message: 'sandbox preparation receipt is unavailable', retryable: true
|
|
960
|
+
});
|
|
961
|
+
if (receipt.lifecycle === 'done' && resolved.state === 'completed') {
|
|
962
|
+
return terminalResult(resolved.taskId, receipt, { lifecycle: { status: 'no-op', changed: false, error: null } }, false);
|
|
963
|
+
}
|
|
964
|
+
if (options.controlBinding) {
|
|
965
|
+
const proof = inspectTaskLifecycleProgress(repoRoot, resolved.taskId, request.agent);
|
|
966
|
+
const oldBinding = receipt.controlBinding;
|
|
967
|
+
if (!oldBinding || oldBinding.generation !== options.controlBinding.generation || proof === 'unknown') {
|
|
968
|
+
return failed(resolved.taskId, {
|
|
969
|
+
code: 'TASK_FINALIZATION_RECOVERY_PROOF_UNAVAILABLE',
|
|
970
|
+
message: 'canonical receipt has no valid host recovery proof', retryable: false
|
|
971
|
+
});
|
|
972
|
+
}
|
|
973
|
+
if (oldBinding.requestId !== options.controlBinding.requestId && proof === 'not-started') {
|
|
974
|
+
if (receipt.lifecycle !== 'pending' || !options.handoffDirectory || !request.handoffSha256) {
|
|
975
|
+
return failed(resolved.taskId, {
|
|
976
|
+
code: 'TASK_FINALIZATION_RECOVERY_PROOF_UNAVAILABLE',
|
|
977
|
+
message: 'pending receipt cannot be rebound without a current handoff', retryable: false
|
|
978
|
+
});
|
|
979
|
+
}
|
|
980
|
+
try {
|
|
981
|
+
const handoff = readTaskFinalizationHandoff(options.handoffDirectory, resolved.taskId, options.controlBinding, request.handoffSha256);
|
|
982
|
+
const nextReceipt = validateReceipt(handoff.receipt, resolved.taskId);
|
|
983
|
+
if (nextReceipt.receiptId !== receipt.receiptId || nextReceipt.intent !== receipt.intent
|
|
984
|
+
|| nextReceipt.lifecycle !== 'pending')
|
|
985
|
+
throw new Error('handoff is not continuous with canonical receipt');
|
|
986
|
+
receipt = updateReceipt(repoRoot, receipt, { controlBinding: options.controlBinding });
|
|
987
|
+
try {
|
|
988
|
+
handoff.cleanup();
|
|
989
|
+
}
|
|
990
|
+
catch { /* keep the persisted canonical receipt usable */ }
|
|
991
|
+
}
|
|
992
|
+
catch (error) {
|
|
993
|
+
return failed(resolved.taskId, {
|
|
994
|
+
code: 'TASK_FINALIZATION_HANDOFF_INVALID',
|
|
995
|
+
message: error instanceof Error ? error.message : String(error), retryable: false
|
|
996
|
+
});
|
|
997
|
+
}
|
|
998
|
+
}
|
|
999
|
+
}
|
|
1000
|
+
if (receipt.taskComment === 'pending' || receipt.verification === 'pending' || receipt.summary === 'pending' || receipt.warningProjection === 'pending') {
|
|
1001
|
+
return failed(resolved.taskId, {
|
|
1002
|
+
code: 'TASK_FINALIZATION_PREPARATION_REQUIRED', message: 'sandbox preparation has pending external steps', retryable: true
|
|
1003
|
+
}, { completedSteps: completedSteps(receipt), pendingSteps: pendingSteps(receipt), warnings: openWarnings(receipt) });
|
|
1004
|
+
}
|
|
1005
|
+
const registry = inspectShortIdRegistry(repoRoot);
|
|
1006
|
+
if (registry.status !== 'valid')
|
|
1007
|
+
return failed(resolved.taskId, {
|
|
1008
|
+
code: 'TASK_FINALIZATION_SHORT_ID_REGISTRY_UNAVAILABLE',
|
|
1009
|
+
message: `cannot verify canonical short-id registry: ${registry.error.code}: ${registry.error.message}`,
|
|
1010
|
+
retryable: false
|
|
1011
|
+
});
|
|
1012
|
+
const lifecycle = options.lifecycle ?? applyTaskLifecycle;
|
|
1013
|
+
const result = lifecycle({ taskRef: resolved.taskId, intent: 'complete', agent: request.agent }, { repoRoot, ...(options.metadataProvider ? { metadataProvider: options.metadataProvider } : {}) });
|
|
1014
|
+
const step = lifecycleStep(result);
|
|
1015
|
+
if (result.status !== 'applied' && result.status !== 'no-op') {
|
|
1016
|
+
const updated = updateReceipt(repoRoot, receipt, { lifecycle: 'pending', lastError: step.error });
|
|
1017
|
+
return terminalResult(resolved.taskId, updated, { lifecycle: step }, result.changed, step.error);
|
|
1018
|
+
}
|
|
1019
|
+
const updated = updateReceipt(repoRoot, receipt, { lifecycle: 'done', lastError: null });
|
|
1020
|
+
return terminalResult(resolved.taskId, updated, { lifecycle: step }, result.changed);
|
|
1021
|
+
});
|
|
1022
|
+
}
|
|
1023
|
+
catch (error) {
|
|
1024
|
+
const detail = error instanceof TaskExecutionLockError
|
|
1025
|
+
? { code: error.code, message: error.message, retryable: error.code === 'ORCHESTRATION_LOCK_BUSY' }
|
|
1026
|
+
: errorOf(error, 'TASK_FINALIZATION_FAILED');
|
|
1027
|
+
return failed(resolved.taskId, detail);
|
|
1028
|
+
}
|
|
1029
|
+
}
|
|
1030
|
+
async function applyTaskFinalization(request, options) {
|
|
1031
|
+
const prepared = await prepareTaskFinalization(request, options);
|
|
1032
|
+
if (prepared.status !== 'prepared')
|
|
1033
|
+
return prepared;
|
|
1034
|
+
return commitPreparedTaskFinalization(request, options);
|
|
1035
|
+
}
|
|
1036
|
+
export { applyFinalizationReceiptMutation, commitPreparedTaskFinalization, applyTaskFinalization, prepareTaskFinalization, issueCapability as createFinalizationCapability, readTaskFinalizationReceipt, terminalResult };
|
|
768
1037
|
//# sourceMappingURL=finalization.js.map
|