@fitlab-ai/agent-infra 0.11.1 → 0.11.2
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/dist/lib/internal/cli-route-inventory.js +1 -1
- package/dist/lib/internal/platform-comment.js +59 -0
- package/dist/lib/internal/sandbox-control.js +4 -131
- package/dist/lib/internal/task-artifact.js +1 -1
- package/dist/lib/internal/task-operation-registry.js +1 -0
- package/dist/lib/internal/task-review.js +5 -5
- package/dist/lib/internal/task-validate.js +0 -6
- package/dist/lib/platform/comment-safety.js +7 -1
- package/dist/lib/platform/github-provider.js +13 -3
- package/dist/lib/platform/issue-comments.js +120 -13
- package/dist/lib/platform/provider-contract.d.ts +2 -0
- package/dist/lib/platform/provider-validation.js +7 -2
- package/dist/lib/platform/verification-sync.js +19 -2
- package/dist/lib/sandbox/control/executor.js +0 -38
- package/dist/lib/sandbox/control/finalization-response.js +10 -2
- package/dist/lib/sandbox/control/task-view.js +3 -1
- package/dist/lib/sandbox/control/workflow-executor.js +28 -43
- package/dist/lib/task/artifact-command.js +6 -6
- package/dist/lib/task/codex-orchestration.js +10 -208
- package/dist/lib/task/commit-operation.js +3 -18
- package/dist/lib/task/control-authority.js +1 -8
- package/dist/lib/task/delegation-receipts.js +14 -76
- package/dist/lib/task/events.js +89 -237
- package/dist/lib/task/finalization-state.js +2 -1
- package/dist/lib/task/finalization.js +273 -49
- package/dist/lib/task/lifecycle-execution.js +2 -11
- package/dist/lib/task/local-artifact-finalization.js +25 -242
- package/dist/lib/task/orchestration.js +0 -18
- package/dist/lib/task/review-command.js +0 -1
- package/dist/lib/task/review-finalization.js +46 -211
- package/dist/lib/task/workflow-command.js +4 -1
- package/dist/lib/task/workflow-dispatch.js +55 -3
- package/dist/lifecycle-build-manifest.json +3 -3
- package/lib/internal/cli-route-inventory.ts +1 -1
- package/lib/internal/platform-comment.ts +38 -0
- package/lib/internal/sandbox-control.ts +3 -145
- package/lib/internal/task-artifact.ts +1 -1
- package/lib/internal/task-operation-registry.ts +1 -0
- package/lib/internal/task-review.ts +5 -5
- package/lib/internal/task-validate.ts +0 -5
- package/lib/platform/comment-safety.ts +8 -0
- package/lib/platform/github-provider.ts +11 -2
- package/lib/platform/issue-comments.ts +132 -13
- package/lib/platform/provider-contract.ts +2 -0
- package/lib/platform/provider-validation.ts +6 -2
- package/lib/platform/verification-sync.ts +19 -2
- package/lib/sandbox/control/executor.ts +0 -44
- package/lib/sandbox/control/finalization-response.ts +10 -2
- package/lib/sandbox/control/task-view.ts +3 -1
- package/lib/sandbox/control/workflow-executor.ts +23 -50
- package/lib/task/artifact-command.ts +6 -7
- package/lib/task/codex-orchestration.ts +8 -241
- package/lib/task/commit-operation.ts +3 -23
- package/lib/task/control-authority.ts +1 -8
- package/lib/task/delegation-receipts.ts +12 -79
- package/lib/task/events.ts +106 -255
- package/lib/task/finalization-state.ts +2 -1
- package/lib/task/finalization.ts +277 -57
- package/lib/task/lifecycle-execution.ts +10 -21
- package/lib/task/local-artifact-finalization.ts +23 -281
- package/lib/task/orchestration.ts +0 -34
- package/lib/task/review-command.ts +0 -1
- package/lib/task/review-finalization.ts +45 -193
- package/lib/task/workflow-command.ts +4 -1
- package/lib/task/workflow-dispatch.ts +46 -3
- package/package.json +2 -2
- package/templates/.agents/rules/lifecycle-orchestration.en.md +7 -7
- package/templates/.agents/rules/lifecycle-orchestration.zh-CN.md +7 -7
- package/templates/.agents/rules/local-artifact-repair.en.md +14 -60
- package/templates/.agents/rules/local-artifact-repair.zh-CN.md +13 -60
- package/templates/.agents/skills/analyze-task/SKILL.en.md +3 -3
- package/templates/.agents/skills/analyze-task/SKILL.zh-CN.md +3 -3
- package/templates/.agents/skills/code-task/SKILL.en.md +3 -3
- package/templates/.agents/skills/code-task/SKILL.zh-CN.md +3 -3
- package/templates/.agents/skills/complete-task/SKILL.en.md +3 -3
- package/templates/.agents/skills/complete-task/SKILL.zh-CN.md +3 -3
- package/templates/.agents/skills/plan-task/SKILL.en.md +3 -3
- package/templates/.agents/skills/plan-task/SKILL.zh-CN.md +3 -3
- package/templates/.agents/skills/review-analysis/SKILL.en.md +2 -2
- package/templates/.agents/skills/review-analysis/SKILL.zh-CN.md +2 -2
- package/templates/.agents/skills/review-code/SKILL.en.md +3 -3
- package/templates/.agents/skills/review-code/SKILL.zh-CN.md +3 -3
- package/templates/.agents/skills/review-plan/SKILL.en.md +2 -2
- package/templates/.agents/skills/review-plan/SKILL.zh-CN.md +2 -2
- package/templates/.agents/skills/run-task/SKILL.en.md +6 -6
- package/templates/.agents/skills/run-task/SKILL.zh-CN.md +6 -6
- package/templates/.agents/skills/run-task/reference/host-validation.en.md +2 -2
- package/templates/.agents/skills/run-task/reference/host-validation.zh-CN.md +2 -2
package/lib/task/finalization.ts
CHANGED
|
@@ -1,9 +1,12 @@
|
|
|
1
1
|
import fs from 'node:fs';
|
|
2
2
|
import path from 'node:path';
|
|
3
|
-
import { randomUUID } from 'node:crypto';
|
|
3
|
+
import { createHash, randomUUID } from 'node:crypto';
|
|
4
4
|
|
|
5
5
|
import { syncPlatformComment } from '../platform/issue-comments.ts';
|
|
6
|
+
import { canonicalizeSummaryBody } from '../platform/comment-safety.ts';
|
|
6
7
|
import type { PlatformResult } from '../platform/types.ts';
|
|
8
|
+
import { taskIssueIdentity } from '../platform/task-identities.ts';
|
|
9
|
+
import { parseTaskFrontmatter } from './frontmatter.ts';
|
|
7
10
|
import {
|
|
8
11
|
applyTaskLifecycle,
|
|
9
12
|
type TaskLifecycleOptions,
|
|
@@ -22,8 +25,8 @@ import {
|
|
|
22
25
|
type OperationWarningSeverity
|
|
23
26
|
} from './operation-outcome.ts';
|
|
24
27
|
|
|
25
|
-
const RECEIPT_VERSION =
|
|
26
|
-
const FINALIZATION_STEPS = ['lifecycle', 'task-comment', 'verification'] as const;
|
|
28
|
+
const RECEIPT_VERSION = 3 as const;
|
|
29
|
+
const FINALIZATION_STEPS = ['lifecycle', 'task-comment', 'verification', 'summary', 'post-summary-verification'] as const;
|
|
27
30
|
type FinalizationStep = typeof FINALIZATION_STEPS[number];
|
|
28
31
|
type FinalizationStepState = 'pending' | 'done' | 'skipped';
|
|
29
32
|
type FinalizationError = { code: string; message: string; retryable: boolean };
|
|
@@ -72,8 +75,11 @@ type TaskFinalizationReceipt = Readonly<{
|
|
|
72
75
|
lifecycle: FinalizationStepState;
|
|
73
76
|
taskComment: FinalizationStepState;
|
|
74
77
|
verification: FinalizationStepState;
|
|
78
|
+
summary: FinalizationStepState;
|
|
79
|
+
postSummaryVerification: FinalizationStepState;
|
|
75
80
|
warningProjection: WarningProjectionState;
|
|
76
81
|
warnings: readonly FinalizationWarning[];
|
|
82
|
+
summarySha256: string | null;
|
|
77
83
|
controlBinding?: Readonly<{ generation: string; requestId: string }>;
|
|
78
84
|
updatedAt: string;
|
|
79
85
|
lastError: FinalizationError | null;
|
|
@@ -101,6 +107,8 @@ type TaskFinalizationResult = Readonly<{
|
|
|
101
107
|
lifecycle: TaskFinalizationStep | null;
|
|
102
108
|
taskComment: TaskFinalizationStep | null;
|
|
103
109
|
verification: TaskFinalizationStep | null;
|
|
110
|
+
summary: TaskFinalizationStep | null;
|
|
111
|
+
postSummaryVerification: TaskFinalizationStep | null;
|
|
104
112
|
completedSteps: readonly FinalizationStep[];
|
|
105
113
|
pendingSteps: readonly FinalizationStep[];
|
|
106
114
|
result: 'completed' | 'completed_with_warnings' | 'failed' | 'blocked';
|
|
@@ -139,6 +147,8 @@ function failed(
|
|
|
139
147
|
lifecycle: null,
|
|
140
148
|
taskComment: null,
|
|
141
149
|
verification: null,
|
|
150
|
+
summary: null,
|
|
151
|
+
postSummaryVerification: null,
|
|
142
152
|
completedSteps: [],
|
|
143
153
|
pendingSteps: [...FINALIZATION_STEPS],
|
|
144
154
|
result: error.retryable ? 'blocked' : 'failed',
|
|
@@ -158,8 +168,11 @@ function emptyReceipt(taskId: string, controlBinding?: Readonly<{ generation: st
|
|
|
158
168
|
lifecycle: 'pending',
|
|
159
169
|
taskComment: 'pending',
|
|
160
170
|
verification: 'pending',
|
|
171
|
+
summary: 'pending',
|
|
172
|
+
postSummaryVerification: 'pending',
|
|
161
173
|
warningProjection: 'done',
|
|
162
174
|
warnings: [],
|
|
175
|
+
summarySha256: null,
|
|
163
176
|
...(controlBinding ? { controlBinding } : {}),
|
|
164
177
|
updatedAt: now(),
|
|
165
178
|
lastError: null
|
|
@@ -194,8 +207,11 @@ function validateReceipt(value: unknown, taskId: string): TaskFinalizationReceip
|
|
|
194
207
|
|| !states.includes(String(receipt.lifecycle))
|
|
195
208
|
|| !states.includes(String(receipt.taskComment))
|
|
196
209
|
|| !states.includes(String(receipt.verification))
|
|
210
|
+
|| !states.includes(String(receipt.summary))
|
|
211
|
+
|| !states.includes(String(receipt.postSummaryVerification))
|
|
197
212
|
|| !['pending', 'done'].includes(String(receipt.warningProjection))
|
|
198
213
|
|| !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)))
|
|
199
215
|
|| typeof receipt.updatedAt !== 'string'
|
|
200
216
|
|| (receipt.lastError !== null && (typeof receipt.lastError !== 'object' || Array.isArray(receipt.lastError)))
|
|
201
217
|
|| (receipt.controlBinding !== undefined && (!controlBinding || typeof controlBinding !== 'object'
|
|
@@ -217,14 +233,6 @@ function readTaskFinalizationReceipt(repoRoot: string, taskId: string): TaskFina
|
|
|
217
233
|
return readReceipt(path.resolve(repoRoot), taskId);
|
|
218
234
|
}
|
|
219
235
|
|
|
220
|
-
function controlBindingMatches(
|
|
221
|
-
receipt: TaskFinalizationReceipt,
|
|
222
|
-
binding: Readonly<{ generation: string; requestId: string }>
|
|
223
|
-
): boolean {
|
|
224
|
-
return receipt.controlBinding?.generation === binding.generation
|
|
225
|
-
&& receipt.controlBinding.requestId === binding.requestId;
|
|
226
|
-
}
|
|
227
|
-
|
|
228
236
|
function writeReceipt(repoRoot: string, receipt: TaskFinalizationReceipt): void {
|
|
229
237
|
const directory = finalizationRoot(repoRoot);
|
|
230
238
|
fs.mkdirSync(directory, { recursive: true, mode: 0o700 });
|
|
@@ -242,25 +250,30 @@ function writeReceipt(repoRoot: string, receipt: TaskFinalizationReceipt): void
|
|
|
242
250
|
function updateReceipt(
|
|
243
251
|
repoRoot: string,
|
|
244
252
|
receipt: TaskFinalizationReceipt,
|
|
245
|
-
patch: Partial<Pick<TaskFinalizationReceipt, 'lifecycle' | 'taskComment' | 'verification' | 'warningProjection' | 'warnings' | 'lastError'>>
|
|
253
|
+
patch: Partial<Pick<TaskFinalizationReceipt, 'lifecycle' | 'taskComment' | 'verification' | 'summary' | 'postSummaryVerification' | 'warningProjection' | 'warnings' | 'summarySha256' | 'controlBinding' | 'lastError'>>
|
|
246
254
|
): TaskFinalizationReceipt {
|
|
247
255
|
const current = readReceipt(repoRoot, receipt.taskId);
|
|
248
|
-
if (!current || current.receiptId !== receipt.receiptId
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
}
|
|
256
|
+
if (!current || current.receiptId !== receipt.receiptId) throw capabilityError(
|
|
257
|
+
'FINALIZATION_SCOPE_INVALID',
|
|
258
|
+
'finalization receipt does not belong to the current task'
|
|
259
|
+
);
|
|
253
260
|
const next = { ...current, ...patch, revision: current.revision + 1, updatedAt: now() };
|
|
254
261
|
writeReceipt(repoRoot, next);
|
|
255
262
|
return next;
|
|
256
263
|
}
|
|
257
264
|
|
|
258
265
|
function completedSteps(receipt: TaskFinalizationReceipt): FinalizationStep[] {
|
|
259
|
-
return FINALIZATION_STEPS.filter((step) => receipt
|
|
266
|
+
return FINALIZATION_STEPS.filter((step) => stepState(receipt, step) !== 'pending');
|
|
260
267
|
}
|
|
261
268
|
|
|
262
269
|
function pendingSteps(receipt: TaskFinalizationReceipt): FinalizationStep[] {
|
|
263
|
-
return FINALIZATION_STEPS.filter((step) => receipt
|
|
270
|
+
return FINALIZATION_STEPS.filter((step) => stepState(receipt, step) === 'pending');
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
function stepState(receipt: TaskFinalizationReceipt, step: FinalizationStep): FinalizationStepState {
|
|
274
|
+
if (step === 'task-comment') return receipt.taskComment;
|
|
275
|
+
if (step === 'post-summary-verification') return receipt.postSummaryVerification;
|
|
276
|
+
return receipt[step];
|
|
264
277
|
}
|
|
265
278
|
|
|
266
279
|
function lifecycleStep(result: TaskLifecycleResult): TaskFinalizationStep {
|
|
@@ -376,9 +389,10 @@ function validateCapabilityMutation(
|
|
|
376
389
|
mutation: FinalizationMutation,
|
|
377
390
|
consumed: Set<string>
|
|
378
391
|
): void {
|
|
379
|
-
if (capability.receiptId !== current.receiptId
|
|
380
|
-
|
|
381
|
-
|
|
392
|
+
if (capability.receiptId !== current.receiptId) throw capabilityError(
|
|
393
|
+
'FINALIZATION_SCOPE_INVALID',
|
|
394
|
+
'finalization mutation belongs to a different task receipt'
|
|
395
|
+
);
|
|
382
396
|
if (capability.scope !== mutation.scope) {
|
|
383
397
|
throw capabilityError('FINALIZATION_SCOPE_INVALID', 'finalization capability mutation scope does not match its capability');
|
|
384
398
|
}
|
|
@@ -556,15 +570,98 @@ async function syncPendingTaskComment(input: {
|
|
|
556
570
|
}
|
|
557
571
|
}
|
|
558
572
|
|
|
573
|
+
function readDeliverySummary(taskDir: string, taskId: string): { body: string; sha256: string } {
|
|
574
|
+
const file = path.join(taskDir, '.delivery-summary.json');
|
|
575
|
+
let value: Record<string, unknown>;
|
|
576
|
+
try {
|
|
577
|
+
value = JSON.parse(fs.readFileSync(file, 'utf8')) as Record<string, unknown>;
|
|
578
|
+
} catch {
|
|
579
|
+
throw Object.assign(new Error('delivery summary staging record is unavailable'), { code: 'SUMMARY_STAGING_INVALID' });
|
|
580
|
+
}
|
|
581
|
+
if (value.taskId !== taskId || typeof value.body !== 'string' || typeof value.sha256 !== 'string') {
|
|
582
|
+
throw Object.assign(new Error('delivery summary staging record is invalid'), { code: 'SUMMARY_STAGING_INVALID' });
|
|
583
|
+
}
|
|
584
|
+
const canonical = canonicalizeSummaryBody(value.body);
|
|
585
|
+
if (!canonical.ok || canonical.value !== value.body) {
|
|
586
|
+
throw Object.assign(new Error('delivery summary staging body is not canonical'), { code: 'SUMMARY_STAGING_INVALID' });
|
|
587
|
+
}
|
|
588
|
+
const digest = createHash('sha256').update(canonical.value).digest('hex');
|
|
589
|
+
if (digest !== value.sha256) throw Object.assign(new Error('delivery summary staging digest does not match body'), { code: 'SUMMARY_STAGING_INVALID' });
|
|
590
|
+
return { body: canonical.value, sha256: value.sha256 };
|
|
591
|
+
}
|
|
592
|
+
|
|
593
|
+
async function syncPendingSummary(input: {
|
|
594
|
+
repoRoot: string;
|
|
595
|
+
taskId: string;
|
|
596
|
+
agent: string;
|
|
597
|
+
receipt: TaskFinalizationReceipt;
|
|
598
|
+
commentSync: typeof syncPlatformComment;
|
|
599
|
+
}): Promise<{ receipt: TaskFinalizationReceipt; step: TaskFinalizationStep; changed: boolean; error: FinalizationError | null }> {
|
|
600
|
+
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
|
+
try {
|
|
605
|
+
const resolved = resolveTaskRef(input.taskId, { repoRoot: input.repoRoot });
|
|
606
|
+
if (!resolved.ok || resolved.state !== 'completed') throw Object.assign(new Error('completed task is unavailable for summary sync'), { code: 'SUMMARY_STAGING_INVALID' });
|
|
607
|
+
if (!taskIssueIdentity(parseTaskFrontmatter(fs.readFileSync(resolved.taskMdPath, 'utf8')))) {
|
|
608
|
+
receipt = updateReceipt(input.repoRoot, receipt, {
|
|
609
|
+
summary: 'skipped', postSummaryVerification: 'skipped', summarySha256: null, lastError: null
|
|
610
|
+
});
|
|
611
|
+
return { receipt, step: { status: 'skipped', changed: false, error: null }, changed: false, error: null };
|
|
612
|
+
}
|
|
613
|
+
const summary = readDeliverySummary(resolved.taskDir, input.taskId);
|
|
614
|
+
const result = await input.commentSync(input.taskId, {
|
|
615
|
+
kind: 'summary', agent: input.agent, body: summary.body, cwd: input.repoRoot,
|
|
616
|
+
summaryAuthorization: { sha256: summary.sha256 }
|
|
617
|
+
});
|
|
618
|
+
const step = commentStep(result);
|
|
619
|
+
if (result.status === 'applied' || result.status === 'no-op') {
|
|
620
|
+
const skipped = result.error?.code === 'ISSUE_NOT_LINKED';
|
|
621
|
+
const resolvedWarning = receipt.warnings.some((warning) => warning.step === 'summary' && warning.status === 'open');
|
|
622
|
+
const warnings = resolveStepWarnings(receipt, 'summary');
|
|
623
|
+
receipt = updateReceipt(input.repoRoot, receipt, {
|
|
624
|
+
summary: skipped ? 'skipped' : 'done', postSummaryVerification: skipped ? 'skipped' : receipt.postSummaryVerification,
|
|
625
|
+
summarySha256: skipped ? null : summary.sha256,
|
|
626
|
+
taskComment: resolvedWarning ? 'pending' : receipt.taskComment,
|
|
627
|
+
warningProjection: warnings.some((warning) => warning.status === 'open') ? 'pending' : 'done',
|
|
628
|
+
warnings,
|
|
629
|
+
lastError: null
|
|
630
|
+
});
|
|
631
|
+
return { receipt, step: skipped ? { ...step, status: 'skipped' } : step, changed: result.changed, error: null };
|
|
632
|
+
}
|
|
633
|
+
const error = step.error ?? { code: 'SUMMARY_SYNC_FAILED', message: 'delivery summary synchronization failed', retryable: true };
|
|
634
|
+
const warning = warningFromError('summary', error);
|
|
635
|
+
const alreadyOpen = receipt.warnings.some((item) => item.step === warning.step && item.code === warning.code && item.target === warning.target && item.status === 'open');
|
|
636
|
+
const warnings = replaceWarning(receipt, warning, 'open');
|
|
637
|
+
receipt = updateReceipt(input.repoRoot, receipt, { summary: 'pending', taskComment: alreadyOpen ? receipt.taskComment : 'pending', warningProjection: 'pending', warnings, lastError: error });
|
|
638
|
+
return { receipt, step, changed: result.changed, error };
|
|
639
|
+
} catch (cause) {
|
|
640
|
+
const error = errorOf(cause, 'SUMMARY_SYNC_FAILED', true);
|
|
641
|
+
try {
|
|
642
|
+
const warning = warningFromError('summary', error);
|
|
643
|
+
const alreadyOpen = receipt.warnings.some((item) => item.step === warning.step && item.code === warning.code && item.target === warning.target && item.status === 'open');
|
|
644
|
+
const warnings = replaceWarning(receipt, warning, 'open');
|
|
645
|
+
receipt = updateReceipt(input.repoRoot, receipt, { summary: 'pending', taskComment: alreadyOpen ? receipt.taskComment : 'pending', warningProjection: 'pending', warnings, lastError: error });
|
|
646
|
+
} catch { /* preserve primary error */ }
|
|
647
|
+
return { receipt, step: { status: 'blocked', changed: false, error }, changed: false, error };
|
|
648
|
+
}
|
|
649
|
+
}
|
|
650
|
+
|
|
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
|
+
|
|
559
656
|
function terminalResult(
|
|
560
657
|
taskId: string,
|
|
561
658
|
receipt: TaskFinalizationReceipt,
|
|
562
|
-
steps: Pick<TaskFinalizationResult, 'lifecycle' | 'taskComment' | 'verification'
|
|
659
|
+
steps: Partial<Pick<TaskFinalizationResult, 'lifecycle' | 'taskComment' | 'verification' | 'summary' | 'postSummaryVerification'>>,
|
|
563
660
|
changed: boolean,
|
|
564
661
|
error: FinalizationError | null = null
|
|
565
662
|
): TaskFinalizationResult {
|
|
566
663
|
const pending = pendingSteps(receipt);
|
|
567
|
-
const blocked = [steps.lifecycle, steps.taskComment, steps.verification].some((step) => step?.status === 'blocked');
|
|
664
|
+
const blocked = [steps.lifecycle, steps.taskComment, steps.verification, steps.summary, steps.postSummaryVerification].some((step) => step?.status === 'blocked');
|
|
568
665
|
const warnings = openWarnings(receipt);
|
|
569
666
|
const postLifecyclePending = receipt.lifecycle === 'done' && (pending.some((step) => step !== 'lifecycle') || receipt.warningProjection === 'pending');
|
|
570
667
|
const hardError = error?.code.startsWith('FINALIZATION_') || error?.code === 'TASK_FINALIZATION_RECEIPT_INVALID';
|
|
@@ -572,13 +669,17 @@ function terminalResult(
|
|
|
572
669
|
status: hardError ? (error?.retryable ? 'blocked' : 'failed') : pending.length === 0 ? 'completed' : postLifecyclePending ? 'completed' : blocked ? 'blocked' : 'failed',
|
|
573
670
|
changed,
|
|
574
671
|
taskId,
|
|
575
|
-
|
|
672
|
+
lifecycle: steps.lifecycle ?? null,
|
|
673
|
+
taskComment: steps.taskComment ?? null,
|
|
674
|
+
verification: steps.verification ?? null,
|
|
675
|
+
summary: steps.summary ?? null,
|
|
676
|
+
postSummaryVerification: steps.postSummaryVerification ?? null,
|
|
576
677
|
completedSteps: completedSteps(receipt),
|
|
577
678
|
pendingSteps: pending,
|
|
578
|
-
result:
|
|
579
|
-
? '
|
|
580
|
-
:
|
|
581
|
-
?
|
|
679
|
+
result: hardError
|
|
680
|
+
? (error?.retryable ? 'blocked' : 'failed')
|
|
681
|
+
: postLifecyclePending && (warnings.length > 0 || receipt.warningProjection === 'pending')
|
|
682
|
+
? 'completed_with_warnings'
|
|
582
683
|
: pending.length === 0 || postLifecyclePending
|
|
583
684
|
? 'completed'
|
|
584
685
|
: blocked
|
|
@@ -608,17 +709,10 @@ async function applyUnderLock(
|
|
|
608
709
|
} catch (error) {
|
|
609
710
|
return failed(taskId, errorOf(error, 'TASK_FINALIZATION_RECEIPT_INVALID'));
|
|
610
711
|
}
|
|
611
|
-
if (options.controlBinding &&
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
retryable: false
|
|
616
|
-
};
|
|
617
|
-
return failed(taskId, error, {
|
|
618
|
-
completedSteps: completedSteps(receipt),
|
|
619
|
-
pendingSteps: pendingSteps(receipt)
|
|
620
|
-
});
|
|
621
|
-
}
|
|
712
|
+
if (options.controlBinding && (
|
|
713
|
+
receipt.controlBinding?.generation !== options.controlBinding.generation
|
|
714
|
+
|| receipt.controlBinding.requestId !== options.controlBinding.requestId
|
|
715
|
+
)) receipt = updateReceipt(repoRoot, receipt, { controlBinding: options.controlBinding });
|
|
622
716
|
|
|
623
717
|
const preflightState = resolveTaskRef(taskId, { repoRoot });
|
|
624
718
|
if (options.preflight && preflightState.ok && preflightState.state === 'active') {
|
|
@@ -712,15 +806,14 @@ async function applyUnderLock(
|
|
|
712
806
|
let verification: TaskFinalizationStep | null = null;
|
|
713
807
|
try {
|
|
714
808
|
receipt = reconcileWarningProjection(repoRoot, taskId, receipt, consumedCapabilities);
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
if (result.status === 'pass') {
|
|
809
|
+
const verificationPending = receipt.verification === 'pending';
|
|
810
|
+
const result = await verify(
|
|
811
|
+
{ taskRef: taskId, event: 'complete-task.completed' },
|
|
812
|
+
{ repoRoot }
|
|
813
|
+
);
|
|
814
|
+
verification = verificationStep(result);
|
|
815
|
+
if (result.status === 'pass') {
|
|
816
|
+
if (verificationPending) {
|
|
724
817
|
const capability = issueCapability(receipt, 'verification');
|
|
725
818
|
receipt = applyFinalizationReceiptMutationUnderLock(repoRoot, receipt, capability, {
|
|
726
819
|
scope: 'verification', operation: 'succeeded'
|
|
@@ -736,28 +829,154 @@ async function applyUnderLock(
|
|
|
736
829
|
return terminalResult(taskId, receipt, { lifecycle: lifecycleResult, taskComment, verification }, changed, finalComment.error);
|
|
737
830
|
}
|
|
738
831
|
} else {
|
|
739
|
-
|
|
832
|
+
receipt = updateReceipt(repoRoot, receipt, { lastError: null });
|
|
833
|
+
}
|
|
834
|
+
} else {
|
|
835
|
+
const detail = verification.error ?? { code: 'VERIFY_FAILED', message: 'verification failed', retryable: true };
|
|
836
|
+
if (verificationPending) {
|
|
740
837
|
const capability = issueCapability(receipt, 'verification');
|
|
741
838
|
receipt = applyFinalizationReceiptMutationUnderLock(repoRoot, receipt, capability, {
|
|
742
839
|
scope: 'verification', operation: 'failed', error: detail
|
|
743
840
|
}, consumedCapabilities);
|
|
744
841
|
receipt = reconcileWarningProjection(repoRoot, taskId, receipt, consumedCapabilities);
|
|
745
|
-
|
|
842
|
+
} else {
|
|
843
|
+
const warnings = replaceWarning(receipt, warningFromError('verification', detail), 'open');
|
|
844
|
+
receipt = updateReceipt(repoRoot, receipt, {
|
|
845
|
+
verification: 'pending', taskComment: 'pending', warningProjection: 'pending', warnings, lastError: detail
|
|
846
|
+
});
|
|
847
|
+
receipt = reconcileWarningProjection(repoRoot, taskId, receipt, consumedCapabilities);
|
|
848
|
+
const warningComment = await syncPendingTaskComment({ repoRoot, taskId, agent: request.agent, receipt, commentSync, consumedCapabilities });
|
|
849
|
+
receipt = warningComment.receipt;
|
|
850
|
+
if (warningComment.step.status !== 'no-op') taskComment = warningComment.step;
|
|
851
|
+
changed = changed || warningComment.changed;
|
|
746
852
|
}
|
|
853
|
+
return terminalResult(taskId, receipt, { lifecycle: lifecycleResult, taskComment, verification }, changed, detail);
|
|
747
854
|
}
|
|
748
855
|
} catch (error) {
|
|
749
856
|
const detail = errorOf(error, 'VERIFY_FAILED', true);
|
|
750
857
|
try {
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
858
|
+
if (receipt.verification === 'pending') {
|
|
859
|
+
const capability = issueCapability(receipt, 'verification');
|
|
860
|
+
receipt = applyFinalizationReceiptMutationUnderLock(repoRoot, receipt, capability, {
|
|
861
|
+
scope: 'verification', operation: 'failed', error: detail
|
|
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
|
+
}
|
|
755
869
|
receipt = reconcileWarningProjection(repoRoot, taskId, receipt, consumedCapabilities);
|
|
870
|
+
} catch (writeError) {
|
|
871
|
+
const persistence = errorOf(writeError, 'FINALIZATION_RECEIPT_WRITE_FAILED', true);
|
|
872
|
+
const hardError: FinalizationError = {
|
|
873
|
+
code: 'FINALIZATION_RECEIPT_WRITE_FAILED',
|
|
874
|
+
message: `Unable to persist verification failure (${detail.code}: ${detail.message}): ${persistence.message}`,
|
|
875
|
+
retryable: true
|
|
876
|
+
};
|
|
877
|
+
return terminalResult(taskId, receipt, {
|
|
878
|
+
lifecycle: lifecycleResult, taskComment, verification: { status: 'blocked', changed: false, error: detail }
|
|
879
|
+
}, changed, hardError);
|
|
880
|
+
}
|
|
881
|
+
try {
|
|
882
|
+
const warningComment = await syncPendingTaskComment({ repoRoot, taskId, agent: request.agent, receipt, commentSync, consumedCapabilities });
|
|
883
|
+
receipt = warningComment.receipt;
|
|
884
|
+
if (warningComment.step.status !== 'no-op') taskComment = warningComment.step;
|
|
885
|
+
changed = changed || warningComment.changed;
|
|
756
886
|
} catch { /* preserve the primary error */ }
|
|
757
887
|
return terminalResult(taskId, receipt, { lifecycle: lifecycleResult, taskComment, verification: { status: 'blocked', changed: false, error: detail } }, changed, detail);
|
|
758
888
|
}
|
|
889
|
+
|
|
890
|
+
const summary = await syncPendingSummary({ repoRoot, taskId, agent: request.agent, receipt, commentSync });
|
|
891
|
+
receipt = summary.receipt;
|
|
892
|
+
changed = changed || summary.changed;
|
|
893
|
+
if (summary.error) {
|
|
894
|
+
receipt = reconcileWarningProjection(repoRoot, taskId, receipt, consumedCapabilities);
|
|
895
|
+
const warningComment = await syncPendingTaskComment({
|
|
896
|
+
repoRoot, taskId, agent: request.agent, receipt, commentSync, consumedCapabilities
|
|
897
|
+
});
|
|
898
|
+
receipt = warningComment.receipt;
|
|
899
|
+
if (warningComment.step.status !== 'no-op') taskComment = warningComment.step;
|
|
900
|
+
changed = changed || warningComment.changed;
|
|
901
|
+
return terminalResult(taskId, receipt, { lifecycle: lifecycleResult, taskComment, verification, summary: summary.step }, changed, summary.error);
|
|
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);
|
|
972
|
+
}
|
|
973
|
+
}
|
|
759
974
|
receipt = reconcileWarningProjection(repoRoot, taskId, receipt, consumedCapabilities);
|
|
760
|
-
|
|
975
|
+
if (receipt.summary !== 'pending' && receipt.postSummaryVerification !== 'pending' && receipt.warningProjection === 'done' && openWarnings(receipt).length === 0) {
|
|
976
|
+
const resolved = resolveTaskRef(taskId, { repoRoot });
|
|
977
|
+
if (resolved.ok && resolved.state === 'completed') clearDeliverySummary(resolved.taskDir);
|
|
978
|
+
}
|
|
979
|
+
return terminalResult(taskId, receipt, { lifecycle: lifecycleResult, taskComment, verification, summary: summary.step, postSummaryVerification }, changed);
|
|
761
980
|
}
|
|
762
981
|
|
|
763
982
|
async function applyTaskFinalization(request: TaskFinalizationRequest, options: TaskFinalizationOptions): Promise<TaskFinalizationResult> {
|
|
@@ -781,7 +1000,8 @@ export {
|
|
|
781
1000
|
applyFinalizationReceiptMutation,
|
|
782
1001
|
applyTaskFinalization,
|
|
783
1002
|
issueCapability as createFinalizationCapability,
|
|
784
|
-
readTaskFinalizationReceipt
|
|
1003
|
+
readTaskFinalizationReceipt,
|
|
1004
|
+
terminalResult
|
|
785
1005
|
};
|
|
786
1006
|
export type {
|
|
787
1007
|
FinalizationError,
|
|
@@ -1,8 +1,7 @@
|
|
|
1
1
|
import {
|
|
2
2
|
inspectOrchestrationStage,
|
|
3
3
|
pauseOrchestration,
|
|
4
|
-
planOrchestrationStageCompletion
|
|
5
|
-
readRun
|
|
4
|
+
planOrchestrationStageCompletion
|
|
6
5
|
} from './orchestration.ts';
|
|
7
6
|
import type {
|
|
8
7
|
OrchestrationStageCompletion,
|
|
@@ -31,28 +30,15 @@ function failure(mode: LifecycleExecutionMode, code: string, message: string): L
|
|
|
31
30
|
function inspectLifecycleExecution(
|
|
32
31
|
taskRef: string,
|
|
33
32
|
request: LifecycleExecutionRequest,
|
|
34
|
-
options: Readonly<{
|
|
33
|
+
options: Readonly<{
|
|
34
|
+
repoRoot?: string;
|
|
35
|
+
now?: () => string;
|
|
36
|
+
}> = {}
|
|
35
37
|
): LifecycleExecutionResult {
|
|
36
38
|
const resolved = resolveTaskRef(taskRef, { repoRoot: options.repoRoot });
|
|
37
39
|
if (!resolved.ok) return failure(request.mode, resolved.code, resolved.message);
|
|
38
40
|
if (request.mode === 'standalone') {
|
|
39
|
-
|
|
40
|
-
const run = readRun(resolved.taskDir);
|
|
41
|
-
if (run?.pendingDelegation) {
|
|
42
|
-
return failure(
|
|
43
|
-
request.mode,
|
|
44
|
-
'ORCHESTRATION_STANDALONE_BUSY',
|
|
45
|
-
'standalone lifecycle execution is blocked by a pending orchestration delegation'
|
|
46
|
-
);
|
|
47
|
-
}
|
|
48
|
-
return { ok: true, mode: request.mode, completionPlan: null, error: null };
|
|
49
|
-
} catch (error) {
|
|
50
|
-
return failure(
|
|
51
|
-
request.mode,
|
|
52
|
-
'ORCHESTRATION_STATE_INVALID',
|
|
53
|
-
error instanceof Error ? error.message : String(error)
|
|
54
|
-
);
|
|
55
|
-
}
|
|
41
|
+
return { ok: true, mode: request.mode, completionPlan: null, error: null };
|
|
56
42
|
}
|
|
57
43
|
|
|
58
44
|
if (request.agent) {
|
|
@@ -84,7 +70,10 @@ function inspectLifecycleExecution(
|
|
|
84
70
|
function validateLifecycleExecution(
|
|
85
71
|
taskRef: string,
|
|
86
72
|
request: LifecycleExecutionRequest,
|
|
87
|
-
options: Readonly<{
|
|
73
|
+
options: Readonly<{
|
|
74
|
+
repoRoot?: string;
|
|
75
|
+
now?: () => string;
|
|
76
|
+
}> = {}
|
|
88
77
|
): LifecycleExecutionResult {
|
|
89
78
|
const inspected = inspectLifecycleExecution(taskRef, request, options);
|
|
90
79
|
if (inspected.ok || request.mode === 'standalone' || request.dryRun) return inspected;
|