@fitlab-ai/agent-infra 0.11.3 → 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 +1 -1
- package/dist/lib/internal/cli-route-inventory.js +1 -1
- 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-operation-registry.js +1 -0
- package/dist/lib/platform/completion-backfill.js +63 -17
- package/dist/lib/sandbox/control/server.js +26 -143
- 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 +3 -2
- package/dist/lib/task/decision-intents.js +12 -0
- package/dist/lib/task/delivery.js +1 -43
- package/dist/lib/task/events.js +80 -64
- package/dist/lib/task/finalization.js +301 -143
- package/dist/lib/task/ledger-intents.js +157 -15
- package/dist/lib/task/lifecycle-path.js +75 -0
- package/dist/lib/task/lifecycle.js +10 -0
- package/dist/lib/task/local-artifact-finalization.js +1 -20
- package/dist/lib/task/orchestration.js +17 -7
- 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/internal/cli-route-inventory.ts +1 -1
- 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-operation-registry.ts +1 -0
- package/lib/platform/completion-backfill.ts +78 -16
- package/lib/sandbox/control/server.ts +21 -146
- 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 +3 -1
- package/lib/task/decision-intents.ts +9 -0
- package/lib/task/delivery.ts +1 -40
- package/lib/task/events.ts +82 -64
- package/lib/task/finalization.ts +269 -111
- package/lib/task/ledger-intents.ts +155 -15
- package/lib/task/lifecycle-path.ts +98 -0
- package/lib/task/lifecycle.ts +13 -0
- package/lib/task/local-artifact-finalization.ts +2 -25
- package/lib/task/orchestration.ts +26 -8
- 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/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-task/SKILL.en.md +2 -2
- package/templates/.agents/skills/complete-task/SKILL.zh-CN.md +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-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/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 -33
- package/lib/sandbox/control/finalization-response.ts +0 -35
package/lib/task/finalization.ts
CHANGED
|
@@ -4,7 +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 } from '../platform/completion-backfill.ts';
|
|
7
|
+
import { backfillCompletionComments, inspectCompletionBackfillEligibility } from '../platform/completion-backfill.ts';
|
|
8
8
|
import type { PlatformResult } from '../platform/types.ts';
|
|
9
9
|
import { taskIssueIdentity } from '../platform/task-identities.ts';
|
|
10
10
|
import { parseTaskFrontmatter } from './frontmatter.ts';
|
|
@@ -101,7 +101,7 @@ 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
107
|
backfill: TaskFinalizationStep | null;
|
|
@@ -111,7 +111,7 @@ type TaskFinalizationResult = Readonly<{
|
|
|
111
111
|
summary: 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
|
}>;
|
|
@@ -314,7 +314,7 @@ function verificationFailure(result: TaskVerificationResult): FinalizationError
|
|
|
314
314
|
}
|
|
315
315
|
|
|
316
316
|
function warningFromError(
|
|
317
|
-
step:
|
|
317
|
+
step: string,
|
|
318
318
|
error: FinalizationError,
|
|
319
319
|
severity: OperationWarningSeverity = 'ACTION_REQUIRED'
|
|
320
320
|
): OperationWarning {
|
|
@@ -357,6 +357,76 @@ function resolveStepWarnings(receipt: TaskFinalizationReceipt, step: Finalizatio
|
|
|
357
357
|
: warning);
|
|
358
358
|
}
|
|
359
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
|
+
|
|
360
430
|
function issueCapability(receipt: TaskFinalizationReceipt, scope: Exclude<FinalizationStep, 'lifecycle'> | 'warning-projection'): FinalizationCapability {
|
|
361
431
|
return {
|
|
362
432
|
receiptId: receipt.receiptId,
|
|
@@ -374,6 +444,15 @@ function capabilityError(code: 'FINALIZATION_CAPABILITY_STALE' | 'FINALIZATION_S
|
|
|
374
444
|
return error;
|
|
375
445
|
}
|
|
376
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
|
+
|
|
377
456
|
function mutationKeys(value: FinalizationMutation): string[] {
|
|
378
457
|
return Object.keys(value).sort();
|
|
379
458
|
}
|
|
@@ -451,9 +530,8 @@ function applyFinalizationReceiptMutationUnderLock(
|
|
|
451
530
|
mutation: FinalizationMutation,
|
|
452
531
|
consumed: Set<string>
|
|
453
532
|
): TaskFinalizationReceipt {
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
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');
|
|
457
535
|
}
|
|
458
536
|
const current = readReceipt(repoRoot, receipt.taskId);
|
|
459
537
|
if (!current || current.receiptId !== receipt.receiptId || current.revision !== receipt.revision) {
|
|
@@ -471,11 +549,10 @@ function applyFinalizationReceiptMutation(
|
|
|
471
549
|
capability: FinalizationCapability,
|
|
472
550
|
mutation: FinalizationMutation
|
|
473
551
|
): TaskFinalizationReceipt {
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
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');
|
|
477
554
|
}
|
|
478
|
-
return withTaskExecutionLock(repoRoot,
|
|
555
|
+
return withTaskExecutionLock(repoRoot, receipt.taskId, 'task-finalization.receipt-mutation', () =>
|
|
479
556
|
applyFinalizationReceiptMutationUnderLock(repoRoot, receipt, capability, mutation, new Set<string>())
|
|
480
557
|
);
|
|
481
558
|
}
|
|
@@ -489,7 +566,10 @@ function reconcileWarningProjection(repoRoot: string, taskId: string, receipt: T
|
|
|
489
566
|
const warnings = [...new Map(receipt.warnings.map((warning) => [warningKey(warning), warning])).values()];
|
|
490
567
|
for (const warning of warnings) {
|
|
491
568
|
try {
|
|
492
|
-
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' });
|
|
493
573
|
if (projected.status === 'failed') {
|
|
494
574
|
const detail: FinalizationError = {
|
|
495
575
|
code: projected.error?.code || 'FINALIZATION_WARNING_PROJECTION_FAILED',
|
|
@@ -595,7 +675,11 @@ async function syncPendingSummary(input: {
|
|
|
595
675
|
let { receipt } = input;
|
|
596
676
|
try {
|
|
597
677
|
const resolved = resolveTaskRef(input.taskId, { repoRoot: input.repoRoot });
|
|
598
|
-
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
|
+
}
|
|
599
683
|
if (!taskIssueIdentity(parseTaskFrontmatter(fs.readFileSync(resolved.taskMdPath, 'utf8')))) {
|
|
600
684
|
receipt = updateReceipt(input.repoRoot, receipt, {
|
|
601
685
|
summary: 'skipped', lastError: null
|
|
@@ -676,7 +760,25 @@ function terminalResult(
|
|
|
676
760
|
};
|
|
677
761
|
}
|
|
678
762
|
|
|
679
|
-
|
|
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(
|
|
680
782
|
request: TaskFinalizationRequest,
|
|
681
783
|
taskId: string,
|
|
682
784
|
options: TaskFinalizationOptions
|
|
@@ -725,7 +827,18 @@ async function applyUnderLock(
|
|
|
725
827
|
|
|
726
828
|
let changed = false;
|
|
727
829
|
let backfillResult: TaskFinalizationStep | null = null;
|
|
728
|
-
|
|
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 {
|
|
729
842
|
const result = await backfill(taskId, { agent: request.agent, cwd: repoRoot });
|
|
730
843
|
backfillResult = commentStep(result);
|
|
731
844
|
const issueNotLinked = result.status === 'no-op' && result.error?.code === 'ISSUE_NOT_LINKED';
|
|
@@ -733,66 +846,57 @@ async function applyUnderLock(
|
|
|
733
846
|
const detail = backfillResult.error ?? {
|
|
734
847
|
code: 'COMPLETION_BACKFILL_FAILED', message: 'completion artifact backfill did not reach a successful terminal result', retryable: true
|
|
735
848
|
};
|
|
736
|
-
return failed(taskId, detail, {
|
|
849
|
+
if (receipt.lifecycle !== 'done') return failed(taskId, detail, {
|
|
737
850
|
backfill: backfillResult,
|
|
738
851
|
completedSteps: completedSteps(receipt),
|
|
739
852
|
pendingSteps: pendingSteps(receipt)
|
|
740
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);
|
|
741
862
|
}
|
|
742
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
|
+
}
|
|
743
870
|
if (result.changed && receipt.taskComment !== 'pending') {
|
|
744
871
|
receipt = updateReceipt(repoRoot, receipt, { taskComment: 'pending' });
|
|
745
872
|
}
|
|
746
873
|
} catch (error) {
|
|
747
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);
|
|
879
|
+
return terminalResult(taskId, receipt, {
|
|
880
|
+
backfill: { status: 'blocked', changed: false, error: detail }
|
|
881
|
+
}, changed, detail);
|
|
882
|
+
}
|
|
748
883
|
return failed(taskId, detail, {
|
|
749
884
|
backfill: { status: detail.retryable ? 'blocked' : 'failed', changed: false, error: detail },
|
|
750
885
|
completedSteps: completedSteps(receipt),
|
|
751
886
|
pendingSteps: pendingSteps(receipt)
|
|
752
887
|
});
|
|
753
|
-
}
|
|
888
|
+
} else backfillResult = { status: 'no-op', changed: false, error: null };
|
|
754
889
|
|
|
755
890
|
let lifecycleResult: TaskFinalizationStep | null = null;
|
|
756
891
|
try {
|
|
757
892
|
const resolved = resolveTaskRef(taskId, { repoRoot });
|
|
758
|
-
|
|
759
|
-
if (registry && registry.status !== 'valid') {
|
|
760
|
-
const detail: FinalizationError = {
|
|
761
|
-
code: 'TASK_FINALIZATION_SHORT_ID_REGISTRY_UNAVAILABLE',
|
|
762
|
-
message: `cannot verify canonical short-id registry: ${registry.error.code}: ${registry.error.message}`,
|
|
763
|
-
retryable: false
|
|
764
|
-
};
|
|
765
|
-
receipt = updateReceipt(repoRoot, receipt, { lifecycle: 'pending', lastError: detail });
|
|
766
|
-
return terminalResult(taskId, receipt, {
|
|
767
|
-
backfill: backfillResult,
|
|
768
|
-
lifecycle: { status: 'failed', changed: false, error: detail },
|
|
769
|
-
taskComment: null,
|
|
770
|
-
verification: null
|
|
771
|
-
}, changed, detail);
|
|
772
|
-
}
|
|
773
|
-
const shortIds = registry?.status === 'valid' ? registry.shortIds : null;
|
|
774
|
-
if (shortIds?.has(taskId)) {
|
|
775
|
-
const detail: FinalizationError = {
|
|
776
|
-
code: 'TASK_FINALIZATION_CANONICAL_STATE_INVALID',
|
|
777
|
-
message: 'completed task still has an active short-id registry entry',
|
|
778
|
-
retryable: false
|
|
779
|
-
};
|
|
780
|
-
receipt = updateReceipt(repoRoot, receipt, { lifecycle: 'pending', lastError: detail });
|
|
781
|
-
return terminalResult(taskId, receipt, {
|
|
782
|
-
backfill: backfillResult,
|
|
783
|
-
lifecycle: { status: 'failed', changed: false, error: detail },
|
|
784
|
-
taskComment: null,
|
|
785
|
-
verification: null
|
|
786
|
-
}, changed, detail);
|
|
787
|
-
}
|
|
788
|
-
const lifecycleDone = receipt.lifecycle === 'done' && resolved.ok && resolved.state === 'completed' && shortIds !== null;
|
|
789
|
-
if (lifecycleDone) {
|
|
893
|
+
if (receipt.lifecycle === 'done' && resolved.ok && resolved.state === 'completed') {
|
|
790
894
|
lifecycleResult = { status: 'no-op', changed: false, error: null };
|
|
791
895
|
receipt = updateReceipt(repoRoot, receipt, { lifecycle: 'done', lastError: null });
|
|
792
896
|
} else {
|
|
793
897
|
const result = lifecycle(
|
|
794
898
|
{ taskRef: taskId, intent: 'complete', agent: request.agent },
|
|
795
|
-
{ repoRoot, ...(options.metadataProvider ? { metadataProvider: options.metadataProvider } : {}) }
|
|
899
|
+
{ repoRoot, prepareOnly: true, ...(options.metadataProvider ? { metadataProvider: options.metadataProvider } : {}) }
|
|
796
900
|
);
|
|
797
901
|
lifecycleResult = lifecycleStep(result);
|
|
798
902
|
if (result.status !== 'applied' && result.status !== 'no-op') {
|
|
@@ -800,7 +904,6 @@ async function applyUnderLock(
|
|
|
800
904
|
return terminalResult(taskId, receipt, { backfill: backfillResult, lifecycle: lifecycleResult, taskComment: null, verification: null }, result.changed, lifecycleResult.error);
|
|
801
905
|
}
|
|
802
906
|
changed = changed || result.changed;
|
|
803
|
-
receipt = updateReceipt(repoRoot, receipt, { lifecycle: 'done', lastError: null });
|
|
804
907
|
}
|
|
805
908
|
} catch (error) {
|
|
806
909
|
const detail = errorOf(error, 'TASK_FINALIZATION_LIFECYCLE_FAILED');
|
|
@@ -810,79 +913,64 @@ async function applyUnderLock(
|
|
|
810
913
|
|
|
811
914
|
let taskComment: TaskFinalizationStep | null = null;
|
|
812
915
|
receipt = reconcileWarningProjection(repoRoot, taskId, receipt, consumedCapabilities);
|
|
813
|
-
const initialComment = await syncPendingTaskComment({
|
|
814
|
-
repoRoot, taskId, agent: request.agent, receipt, commentSync, consumedCapabilities
|
|
815
|
-
});
|
|
816
|
-
receipt = initialComment.receipt;
|
|
817
|
-
taskComment = initialComment.step;
|
|
818
|
-
changed = changed || initialComment.changed;
|
|
819
|
-
if (initialComment.error) {
|
|
820
|
-
return terminalResult(taskId, receipt, { backfill: backfillResult, lifecycle: lifecycleResult, taskComment, verification: null }, changed, initialComment.error);
|
|
821
|
-
}
|
|
822
916
|
|
|
823
917
|
let verification: TaskFinalizationStep | null = null;
|
|
824
|
-
|
|
918
|
+
if (receipt.verification !== 'pending') {
|
|
919
|
+
verification = { status: receipt.verification === 'skipped' ? 'skipped' : 'no-op', changed: false, error: null };
|
|
920
|
+
} else {
|
|
921
|
+
try {
|
|
825
922
|
receipt = reconcileWarningProjection(repoRoot, taskId, receipt, consumedCapabilities);
|
|
826
|
-
const
|
|
923
|
+
const verificationState = resolveTaskRef(taskId, { repoRoot });
|
|
924
|
+
const verificationEvent = verificationState.ok && verificationState.state === 'completed'
|
|
925
|
+
? 'complete-task.completed'
|
|
926
|
+
: 'complete-task.prepared';
|
|
827
927
|
const result = await verify(
|
|
828
|
-
{ taskRef: taskId, event:
|
|
928
|
+
{ taskRef: taskId, event: verificationEvent },
|
|
829
929
|
{ repoRoot }
|
|
830
930
|
);
|
|
831
931
|
verification = verificationStep(result);
|
|
832
932
|
if (result.status === 'pass') {
|
|
833
|
-
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
|
|
844
|
-
|
|
845
|
-
|
|
846
|
-
|
|
847
|
-
}
|
|
848
|
-
} else {
|
|
849
|
-
receipt = updateReceipt(repoRoot, receipt, { lastError: null });
|
|
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);
|
|
850
948
|
}
|
|
851
949
|
} else {
|
|
852
950
|
const detail = verification.error ?? { code: 'VERIFY_FAILED', message: 'verification failed', retryable: true };
|
|
853
|
-
|
|
854
|
-
|
|
855
|
-
|
|
856
|
-
|
|
857
|
-
|
|
858
|
-
|
|
859
|
-
|
|
860
|
-
|
|
861
|
-
|
|
862
|
-
|
|
863
|
-
|
|
864
|
-
|
|
865
|
-
|
|
866
|
-
receipt = warningComment.receipt;
|
|
867
|
-
if (warningComment.step.status !== 'no-op') taskComment = warningComment.step;
|
|
868
|
-
changed = changed || warningComment.changed;
|
|
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);
|
|
869
964
|
}
|
|
870
965
|
return terminalResult(taskId, receipt, { backfill: backfillResult, lifecycle: lifecycleResult, taskComment, verification }, changed, detail);
|
|
871
966
|
}
|
|
872
967
|
} catch (error) {
|
|
873
968
|
const detail = errorOf(error, 'VERIFY_FAILED', true);
|
|
874
969
|
try {
|
|
875
|
-
|
|
876
|
-
|
|
877
|
-
|
|
878
|
-
|
|
879
|
-
}, consumedCapabilities);
|
|
880
|
-
} else {
|
|
881
|
-
const warnings = replaceWarning(receipt, warningFromError('verification', detail), 'open');
|
|
882
|
-
receipt = updateReceipt(repoRoot, receipt, {
|
|
883
|
-
verification: 'pending', taskComment: 'pending', warningProjection: 'pending', warnings, lastError: detail
|
|
884
|
-
});
|
|
885
|
-
}
|
|
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
|
+
});
|
|
886
974
|
receipt = reconcileWarningProjection(repoRoot, taskId, receipt, consumedCapabilities);
|
|
887
975
|
} catch (writeError) {
|
|
888
976
|
const persistence = errorOf(writeError, 'FINALIZATION_RECEIPT_WRITE_FAILED', true);
|
|
@@ -902,9 +990,12 @@ async function applyUnderLock(
|
|
|
902
990
|
changed = changed || warningComment.changed;
|
|
903
991
|
} catch { /* preserve the primary error */ }
|
|
904
992
|
return terminalResult(taskId, receipt, { backfill: backfillResult, lifecycle: lifecycleResult, taskComment, verification: { status: 'blocked', changed: false, error: detail } }, changed, detail);
|
|
993
|
+
}
|
|
905
994
|
}
|
|
906
995
|
|
|
907
|
-
let summary =
|
|
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 };
|
|
908
999
|
receipt = summary.receipt;
|
|
909
1000
|
changed = changed || summary.changed;
|
|
910
1001
|
if (summary.error) {
|
|
@@ -912,10 +1003,21 @@ async function applyUnderLock(
|
|
|
912
1003
|
return terminalResult(taskId, receipt, { backfill: backfillResult, lifecycle: lifecycleResult, taskComment, verification, summary: summary.step }, changed, summary.error);
|
|
913
1004
|
}
|
|
914
1005
|
receipt = reconcileWarningProjection(repoRoot, taskId, receipt, consumedCapabilities);
|
|
915
|
-
|
|
1006
|
+
if (receipt.taskComment === 'pending') {
|
|
1007
|
+
const finalComment = await syncPendingTaskComment({
|
|
1008
|
+
repoRoot, taskId, agent: request.agent, receipt, commentSync, consumedCapabilities
|
|
1009
|
+
});
|
|
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);
|
|
1015
|
+
}
|
|
1016
|
+
}
|
|
1017
|
+
return preparedResult(taskId, receipt, { backfill: backfillResult, lifecycle: lifecycleResult, taskComment, verification, summary: summary.step }, changed);
|
|
916
1018
|
}
|
|
917
1019
|
|
|
918
|
-
async function
|
|
1020
|
+
async function prepareTaskFinalization(request: TaskFinalizationRequest, options: TaskFinalizationOptions): Promise<TaskFinalizationResult> {
|
|
919
1021
|
if (request.intent !== 'complete' || !request.taskRef || !request.agent) {
|
|
920
1022
|
return failed(null, { code: 'TASK_FINALIZATION_PAYLOAD_INVALID', message: 'complete finalization requires taskRef and agent', retryable: false });
|
|
921
1023
|
}
|
|
@@ -923,7 +1025,7 @@ async function applyTaskFinalization(request: TaskFinalizationRequest, options:
|
|
|
923
1025
|
const resolved = resolveTaskRef(request.taskRef, { repoRoot });
|
|
924
1026
|
if (!resolved.ok) return failed(resolved.taskId, { code: resolved.code, message: resolved.message, retryable: false });
|
|
925
1027
|
try {
|
|
926
|
-
return await withTaskExecutionLock(repoRoot, resolved.taskId, 'task-finalization.
|
|
1028
|
+
return await withTaskExecutionLock(repoRoot, resolved.taskId, 'task-finalization.prepare', () => prepareUnderLock(request, resolved.taskId, options));
|
|
927
1029
|
} catch (error) {
|
|
928
1030
|
const detail = error instanceof TaskExecutionLockError
|
|
929
1031
|
? { code: error.code, message: error.message, retryable: error.code === 'ORCHESTRATION_LOCK_BUSY' }
|
|
@@ -932,9 +1034,65 @@ async function applyTaskFinalization(request: TaskFinalizationRequest, options:
|
|
|
932
1034
|
}
|
|
933
1035
|
}
|
|
934
1036
|
|
|
1037
|
+
async function commitPreparedTaskFinalization(request: TaskFinalizationRequest, options: TaskFinalizationOptions): Promise<TaskFinalizationResult> {
|
|
1038
|
+
if (request.intent !== 'complete' || !request.taskRef || !request.agent) {
|
|
1039
|
+
return failed(null, { code: 'TASK_FINALIZATION_PAYLOAD_INVALID', message: 'complete finalization requires taskRef and agent', retryable: false });
|
|
1040
|
+
}
|
|
1041
|
+
const repoRoot = path.resolve(options.repoRoot);
|
|
1042
|
+
const resolved = resolveTaskRef(request.taskRef, { repoRoot });
|
|
1043
|
+
if (!resolved.ok) return failed(resolved.taskId, { code: resolved.code, message: resolved.message, retryable: false });
|
|
1044
|
+
try {
|
|
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
|
+
});
|
|
1077
|
+
} catch (error) {
|
|
1078
|
+
const detail = error instanceof TaskExecutionLockError
|
|
1079
|
+
? { code: error.code, message: error.message, retryable: error.code === 'ORCHESTRATION_LOCK_BUSY' }
|
|
1080
|
+
: errorOf(error, 'TASK_FINALIZATION_FAILED');
|
|
1081
|
+
return failed(resolved.taskId, detail);
|
|
1082
|
+
}
|
|
1083
|
+
}
|
|
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
|
+
|
|
935
1091
|
export {
|
|
936
1092
|
applyFinalizationReceiptMutation,
|
|
1093
|
+
commitPreparedTaskFinalization,
|
|
937
1094
|
applyTaskFinalization,
|
|
1095
|
+
prepareTaskFinalization,
|
|
938
1096
|
issueCapability as createFinalizationCapability,
|
|
939
1097
|
readTaskFinalizationReceipt,
|
|
940
1098
|
terminalResult
|