@fitlab-ai/agent-infra 0.11.1 → 0.11.3
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/bin/internal-cli.js +7 -2
- package/dist/lib/agent-clients/adapter.js +3 -0
- package/dist/lib/agent-clients/adapters/codex-lifecycle/recovery.js +234 -0
- package/dist/lib/agent-clients/adapters/codex-lifecycle/sandbox-controller.js +8 -35
- package/dist/lib/agent-clients/adapters/codex-lifecycle/store.js +1 -34
- package/dist/lib/agent-clients/adapters/codex.js +10 -0
- package/dist/lib/internal/cli-route-inventory.js +1 -1
- package/dist/lib/internal/manual-validation.js +22 -27
- package/dist/lib/internal/platform-comment.js +59 -0
- package/dist/lib/internal/sandbox-control.js +2 -171
- package/dist/lib/internal/task-artifact.js +1 -1
- package/dist/lib/internal/task-lifecycle.js +1 -1
- package/dist/lib/internal/task-operation-registry.js +7 -2
- 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 +93 -15
- package/dist/lib/platform/pr-summary.js +7 -2
- 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/client.js +3 -44
- package/dist/lib/sandbox/control/executor.js +14 -117
- package/dist/lib/sandbox/control/finalization-response.js +8 -2
- package/dist/lib/sandbox/control/protocol.js +5 -64
- package/dist/lib/sandbox/control/server.js +29 -206
- package/dist/lib/sandbox/control/state.js +1 -7
- package/dist/lib/sandbox/control/task-view.js +2 -1
- 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 +35 -44
- package/dist/lib/task/control-recovery.js +3 -42
- package/dist/lib/task/delegation-receipts.js +16 -78
- package/dist/lib/task/events.js +89 -237
- package/dist/lib/task/finalization-artifacts.js +1 -3
- package/dist/lib/task/finalization-state.js +2 -1
- package/dist/lib/task/finalization.js +208 -57
- package/dist/lib/task/lifecycle-execution.js +2 -11
- package/dist/lib/task/lifecycle.js +1 -1
- package/dist/lib/task/local-artifact-finalization.js +25 -242
- package/dist/lib/task/manual-validation-receipt.js +8 -3
- package/dist/lib/task/orchestration.js +12 -24
- package/dist/lib/task/review-command.js +0 -1
- package/dist/lib/task/review-finalization.js +46 -211
- package/dist/lifecycle-build-manifest.json +3 -3
- package/lib/agent-clients/adapter.ts +46 -0
- package/lib/agent-clients/adapters/codex-lifecycle/recovery.ts +297 -0
- package/lib/agent-clients/adapters/codex-lifecycle/sandbox-controller.ts +13 -33
- package/lib/agent-clients/adapters/codex-lifecycle/store.ts +1 -44
- package/lib/agent-clients/adapters/codex.ts +10 -0
- package/lib/internal/cli-route-inventory.ts +1 -1
- package/lib/internal/manual-validation.ts +22 -26
- package/lib/internal/platform-comment.ts +38 -0
- package/lib/internal/sandbox-control.ts +1 -185
- package/lib/internal/task-artifact.ts +1 -1
- package/lib/internal/task-lifecycle.ts +2 -2
- package/lib/internal/task-operation-registry.ts +8 -4
- 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 +102 -17
- package/lib/platform/pr-summary.ts +10 -2
- 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/client.ts +5 -68
- package/lib/sandbox/control/executor.ts +14 -125
- package/lib/sandbox/control/finalization-response.ts +8 -2
- package/lib/sandbox/control/protocol.ts +7 -83
- package/lib/sandbox/control/server.ts +30 -220
- package/lib/sandbox/control/state.ts +1 -9
- package/lib/sandbox/control/task-view.ts +2 -1
- 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 +39 -55
- package/lib/task/control-recovery.ts +3 -43
- package/lib/task/delegation-receipts.ts +14 -81
- package/lib/task/events.ts +106 -255
- package/lib/task/finalization-artifacts.ts +1 -3
- package/lib/task/finalization-state.ts +2 -1
- package/lib/task/finalization.ts +221 -65
- package/lib/task/lifecycle-execution.ts +10 -21
- package/lib/task/lifecycle.ts +2 -2
- package/lib/task/local-artifact-finalization.ts +23 -281
- package/lib/task/manual-validation-receipt.ts +8 -2
- package/lib/task/orchestration.ts +30 -53
- package/lib/task/review-command.ts +0 -1
- package/lib/task/review-finalization.ts +45 -193
- package/package.json +2 -2
- package/templates/.agents/rules/lifecycle-orchestration.en.md +23 -9
- package/templates/.agents/rules/lifecycle-orchestration.zh-CN.md +23 -9
- 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-manual-validation/SKILL.en.md +2 -2
- package/templates/.agents/skills/complete-manual-validation/SKILL.zh-CN.md +2 -2
- package/templates/.agents/skills/complete-task/SKILL.en.md +7 -8
- package/templates/.agents/skills/complete-task/SKILL.zh-CN.md +7 -8
- package/templates/.agents/skills/plan-task/SKILL.en.md +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-manual-validation/SKILL.en.md +2 -2
- package/templates/.agents/skills/run-manual-validation/SKILL.zh-CN.md +2 -2
- package/templates/.agents/skills/run-task/SKILL.en.md +10 -9
- package/templates/.agents/skills/run-task/SKILL.zh-CN.md +10 -9
- 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/dist/lib/sandbox/control/task-workflow.js +0 -74
- package/dist/lib/sandbox/control/workflow-executor.js +0 -157
- package/dist/lib/task/lifecycle-recovery.js +0 -549
- package/dist/lib/task/recovery-warning.js +0 -19
- package/dist/lib/task/workflow-command.js +0 -18
- package/dist/lib/task/workflow-dispatch.js +0 -161
- package/lib/sandbox/control/task-workflow.ts +0 -95
- package/lib/sandbox/control/workflow-executor.ts +0 -183
- package/lib/task/lifecycle-recovery.ts +0 -691
- package/lib/task/recovery-warning.ts +0 -21
- package/lib/task/workflow-command.ts +0 -19
- package/lib/task/workflow-dispatch.ts +0 -171
package/lib/task/finalization.ts
CHANGED
|
@@ -1,9 +1,13 @@
|
|
|
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';
|
|
7
|
+
import { backfillCompletionComments } from '../platform/completion-backfill.ts';
|
|
6
8
|
import type { PlatformResult } from '../platform/types.ts';
|
|
9
|
+
import { taskIssueIdentity } from '../platform/task-identities.ts';
|
|
10
|
+
import { parseTaskFrontmatter } from './frontmatter.ts';
|
|
7
11
|
import {
|
|
8
12
|
applyTaskLifecycle,
|
|
9
13
|
type TaskLifecycleOptions,
|
|
@@ -22,8 +26,8 @@ import {
|
|
|
22
26
|
type OperationWarningSeverity
|
|
23
27
|
} from './operation-outcome.ts';
|
|
24
28
|
|
|
25
|
-
const RECEIPT_VERSION =
|
|
26
|
-
const FINALIZATION_STEPS = ['lifecycle', 'task-comment', 'verification'] as const;
|
|
29
|
+
const RECEIPT_VERSION = 4 as const;
|
|
30
|
+
const FINALIZATION_STEPS = ['lifecycle', 'task-comment', 'verification', 'summary'] as const;
|
|
27
31
|
type FinalizationStep = typeof FINALIZATION_STEPS[number];
|
|
28
32
|
type FinalizationStepState = 'pending' | 'done' | 'skipped';
|
|
29
33
|
type FinalizationError = { code: string; message: string; retryable: boolean };
|
|
@@ -58,6 +62,7 @@ type TaskFinalizationOptions = Readonly<{
|
|
|
58
62
|
controlBinding?: Readonly<{ generation: string; requestId: string }>;
|
|
59
63
|
metadataProvider?: TaskLifecycleOptions['metadataProvider'];
|
|
60
64
|
lifecycle?: typeof applyTaskLifecycle;
|
|
65
|
+
backfill?: typeof backfillCompletionComments;
|
|
61
66
|
commentSync?: typeof syncPlatformComment;
|
|
62
67
|
verify?: typeof verifyTaskEvent;
|
|
63
68
|
preflight?: typeof verifyTaskEvent;
|
|
@@ -72,6 +77,7 @@ type TaskFinalizationReceipt = Readonly<{
|
|
|
72
77
|
lifecycle: FinalizationStepState;
|
|
73
78
|
taskComment: FinalizationStepState;
|
|
74
79
|
verification: FinalizationStepState;
|
|
80
|
+
summary: FinalizationStepState;
|
|
75
81
|
warningProjection: WarningProjectionState;
|
|
76
82
|
warnings: readonly FinalizationWarning[];
|
|
77
83
|
controlBinding?: Readonly<{ generation: string; requestId: string }>;
|
|
@@ -98,9 +104,11 @@ type TaskFinalizationResult = Readonly<{
|
|
|
98
104
|
status: 'completed' | 'failed' | 'blocked';
|
|
99
105
|
changed: boolean;
|
|
100
106
|
taskId: string | null;
|
|
107
|
+
backfill: TaskFinalizationStep | null;
|
|
101
108
|
lifecycle: TaskFinalizationStep | null;
|
|
102
109
|
taskComment: TaskFinalizationStep | null;
|
|
103
110
|
verification: TaskFinalizationStep | null;
|
|
111
|
+
summary: TaskFinalizationStep | null;
|
|
104
112
|
completedSteps: readonly FinalizationStep[];
|
|
105
113
|
pendingSteps: readonly FinalizationStep[];
|
|
106
114
|
result: 'completed' | 'completed_with_warnings' | 'failed' | 'blocked';
|
|
@@ -136,9 +144,11 @@ function failed(
|
|
|
136
144
|
status: error.retryable ? 'blocked' : 'failed',
|
|
137
145
|
changed: false,
|
|
138
146
|
taskId,
|
|
147
|
+
backfill: null,
|
|
139
148
|
lifecycle: null,
|
|
140
149
|
taskComment: null,
|
|
141
150
|
verification: null,
|
|
151
|
+
summary: null,
|
|
142
152
|
completedSteps: [],
|
|
143
153
|
pendingSteps: [...FINALIZATION_STEPS],
|
|
144
154
|
result: error.retryable ? 'blocked' : 'failed',
|
|
@@ -158,6 +168,7 @@ function emptyReceipt(taskId: string, controlBinding?: Readonly<{ generation: st
|
|
|
158
168
|
lifecycle: 'pending',
|
|
159
169
|
taskComment: 'pending',
|
|
160
170
|
verification: 'pending',
|
|
171
|
+
summary: 'pending',
|
|
161
172
|
warningProjection: 'done',
|
|
162
173
|
warnings: [],
|
|
163
174
|
...(controlBinding ? { controlBinding } : {}),
|
|
@@ -194,6 +205,7 @@ function validateReceipt(value: unknown, taskId: string): TaskFinalizationReceip
|
|
|
194
205
|
|| !states.includes(String(receipt.lifecycle))
|
|
195
206
|
|| !states.includes(String(receipt.taskComment))
|
|
196
207
|
|| !states.includes(String(receipt.verification))
|
|
208
|
+
|| !states.includes(String(receipt.summary))
|
|
197
209
|
|| !['pending', 'done'].includes(String(receipt.warningProjection))
|
|
198
210
|
|| !Array.isArray(receipt.warnings) || receipt.warnings.some((warning) => !validWarning(warning))
|
|
199
211
|
|| typeof receipt.updatedAt !== 'string'
|
|
@@ -217,14 +229,6 @@ function readTaskFinalizationReceipt(repoRoot: string, taskId: string): TaskFina
|
|
|
217
229
|
return readReceipt(path.resolve(repoRoot), taskId);
|
|
218
230
|
}
|
|
219
231
|
|
|
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
232
|
function writeReceipt(repoRoot: string, receipt: TaskFinalizationReceipt): void {
|
|
229
233
|
const directory = finalizationRoot(repoRoot);
|
|
230
234
|
fs.mkdirSync(directory, { recursive: true, mode: 0o700 });
|
|
@@ -242,25 +246,29 @@ function writeReceipt(repoRoot: string, receipt: TaskFinalizationReceipt): void
|
|
|
242
246
|
function updateReceipt(
|
|
243
247
|
repoRoot: string,
|
|
244
248
|
receipt: TaskFinalizationReceipt,
|
|
245
|
-
patch: Partial<Pick<TaskFinalizationReceipt, 'lifecycle' | 'taskComment' | 'verification' | 'warningProjection' | 'warnings' | 'lastError'>>
|
|
249
|
+
patch: Partial<Pick<TaskFinalizationReceipt, 'lifecycle' | 'taskComment' | 'verification' | 'summary' | 'warningProjection' | 'warnings' | 'controlBinding' | 'lastError'>>
|
|
246
250
|
): TaskFinalizationReceipt {
|
|
247
251
|
const current = readReceipt(repoRoot, receipt.taskId);
|
|
248
|
-
if (!current || current.receiptId !== receipt.receiptId
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
}
|
|
252
|
+
if (!current || current.receiptId !== receipt.receiptId) throw capabilityError(
|
|
253
|
+
'FINALIZATION_SCOPE_INVALID',
|
|
254
|
+
'finalization receipt does not belong to the current task'
|
|
255
|
+
);
|
|
253
256
|
const next = { ...current, ...patch, revision: current.revision + 1, updatedAt: now() };
|
|
254
257
|
writeReceipt(repoRoot, next);
|
|
255
258
|
return next;
|
|
256
259
|
}
|
|
257
260
|
|
|
258
261
|
function completedSteps(receipt: TaskFinalizationReceipt): FinalizationStep[] {
|
|
259
|
-
return FINALIZATION_STEPS.filter((step) => receipt
|
|
262
|
+
return FINALIZATION_STEPS.filter((step) => stepState(receipt, step) !== 'pending');
|
|
260
263
|
}
|
|
261
264
|
|
|
262
265
|
function pendingSteps(receipt: TaskFinalizationReceipt): FinalizationStep[] {
|
|
263
|
-
return FINALIZATION_STEPS.filter((step) => receipt
|
|
266
|
+
return FINALIZATION_STEPS.filter((step) => stepState(receipt, step) === 'pending');
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
function stepState(receipt: TaskFinalizationReceipt, step: FinalizationStep): FinalizationStepState {
|
|
270
|
+
if (step === 'task-comment') return receipt.taskComment;
|
|
271
|
+
return receipt[step];
|
|
264
272
|
}
|
|
265
273
|
|
|
266
274
|
function lifecycleStep(result: TaskLifecycleResult): TaskFinalizationStep {
|
|
@@ -376,9 +384,10 @@ function validateCapabilityMutation(
|
|
|
376
384
|
mutation: FinalizationMutation,
|
|
377
385
|
consumed: Set<string>
|
|
378
386
|
): void {
|
|
379
|
-
if (capability.receiptId !== current.receiptId
|
|
380
|
-
|
|
381
|
-
|
|
387
|
+
if (capability.receiptId !== current.receiptId) throw capabilityError(
|
|
388
|
+
'FINALIZATION_SCOPE_INVALID',
|
|
389
|
+
'finalization mutation belongs to a different task receipt'
|
|
390
|
+
);
|
|
382
391
|
if (capability.scope !== mutation.scope) {
|
|
383
392
|
throw capabilityError('FINALIZATION_SCOPE_INVALID', 'finalization capability mutation scope does not match its capability');
|
|
384
393
|
}
|
|
@@ -556,15 +565,89 @@ async function syncPendingTaskComment(input: {
|
|
|
556
565
|
}
|
|
557
566
|
}
|
|
558
567
|
|
|
568
|
+
function readDeliverySummary(taskDir: string, taskId: string): { body: string; sha256: string } {
|
|
569
|
+
const file = path.join(taskDir, '.delivery-summary.json');
|
|
570
|
+
let value: Record<string, unknown>;
|
|
571
|
+
try {
|
|
572
|
+
value = JSON.parse(fs.readFileSync(file, 'utf8')) as Record<string, unknown>;
|
|
573
|
+
} catch {
|
|
574
|
+
throw Object.assign(new Error('delivery summary staging record is unavailable'), { code: 'SUMMARY_STAGING_INVALID' });
|
|
575
|
+
}
|
|
576
|
+
if (value.taskId !== taskId || typeof value.body !== 'string' || typeof value.sha256 !== 'string') {
|
|
577
|
+
throw Object.assign(new Error('delivery summary staging record is invalid'), { code: 'SUMMARY_STAGING_INVALID' });
|
|
578
|
+
}
|
|
579
|
+
const canonical = canonicalizeSummaryBody(value.body);
|
|
580
|
+
if (!canonical.ok || canonical.value !== value.body) {
|
|
581
|
+
throw Object.assign(new Error('delivery summary staging body is not canonical'), { code: 'SUMMARY_STAGING_INVALID' });
|
|
582
|
+
}
|
|
583
|
+
const digest = createHash('sha256').update(canonical.value).digest('hex');
|
|
584
|
+
if (digest !== value.sha256) throw Object.assign(new Error('delivery summary staging digest does not match body'), { code: 'SUMMARY_STAGING_INVALID' });
|
|
585
|
+
return { body: canonical.value, sha256: value.sha256 };
|
|
586
|
+
}
|
|
587
|
+
|
|
588
|
+
async function syncPendingSummary(input: {
|
|
589
|
+
repoRoot: string;
|
|
590
|
+
taskId: string;
|
|
591
|
+
agent: string;
|
|
592
|
+
receipt: TaskFinalizationReceipt;
|
|
593
|
+
commentSync: typeof syncPlatformComment;
|
|
594
|
+
}): Promise<{ receipt: TaskFinalizationReceipt; step: TaskFinalizationStep; changed: boolean; error: FinalizationError | null }> {
|
|
595
|
+
let { receipt } = input;
|
|
596
|
+
try {
|
|
597
|
+
const resolved = resolveTaskRef(input.taskId, { repoRoot: input.repoRoot });
|
|
598
|
+
if (!resolved.ok || resolved.state !== 'completed') throw Object.assign(new Error('completed task is unavailable for summary sync'), { code: 'SUMMARY_STAGING_INVALID' });
|
|
599
|
+
if (!taskIssueIdentity(parseTaskFrontmatter(fs.readFileSync(resolved.taskMdPath, 'utf8')))) {
|
|
600
|
+
receipt = updateReceipt(input.repoRoot, receipt, {
|
|
601
|
+
summary: 'skipped', lastError: null
|
|
602
|
+
});
|
|
603
|
+
return { receipt, step: { status: 'skipped', changed: false, error: null }, changed: false, error: null };
|
|
604
|
+
}
|
|
605
|
+
const summary = readDeliverySummary(resolved.taskDir, input.taskId);
|
|
606
|
+
const result = await input.commentSync(input.taskId, {
|
|
607
|
+
kind: 'summary', agent: input.agent, body: summary.body, cwd: input.repoRoot,
|
|
608
|
+
summaryAuthorization: { sha256: summary.sha256 }
|
|
609
|
+
});
|
|
610
|
+
const step = commentStep(result);
|
|
611
|
+
if (result.status === 'applied' || result.status === 'no-op') {
|
|
612
|
+
const skipped = result.error?.code === 'ISSUE_NOT_LINKED';
|
|
613
|
+
const resolvedWarning = receipt.warnings.some((warning) => warning.step === 'summary' && warning.status === 'open');
|
|
614
|
+
const warnings = resolveStepWarnings(receipt, 'summary');
|
|
615
|
+
receipt = updateReceipt(input.repoRoot, receipt, {
|
|
616
|
+
summary: skipped ? 'skipped' : 'done',
|
|
617
|
+
taskComment: resolvedWarning ? 'pending' : receipt.taskComment,
|
|
618
|
+
warningProjection: warnings.some((warning) => warning.status === 'open') ? 'pending' : 'done',
|
|
619
|
+
warnings,
|
|
620
|
+
lastError: null
|
|
621
|
+
});
|
|
622
|
+
return { receipt, step: skipped ? { ...step, status: 'skipped' } : step, changed: result.changed, error: null };
|
|
623
|
+
}
|
|
624
|
+
const error = step.error ?? { code: 'SUMMARY_SYNC_FAILED', message: 'delivery summary synchronization failed', retryable: true };
|
|
625
|
+
const warning = warningFromError('summary', error);
|
|
626
|
+
const alreadyOpen = receipt.warnings.some((item) => item.step === warning.step && item.code === warning.code && item.target === warning.target && item.status === 'open');
|
|
627
|
+
const warnings = replaceWarning(receipt, warning, 'open');
|
|
628
|
+
receipt = updateReceipt(input.repoRoot, receipt, { summary: 'pending', taskComment: alreadyOpen ? receipt.taskComment : 'pending', warningProjection: 'pending', warnings, lastError: error });
|
|
629
|
+
return { receipt, step, changed: result.changed, error };
|
|
630
|
+
} catch (cause) {
|
|
631
|
+
const error = errorOf(cause, 'SUMMARY_SYNC_FAILED', true);
|
|
632
|
+
try {
|
|
633
|
+
const warning = warningFromError('summary', error);
|
|
634
|
+
const alreadyOpen = receipt.warnings.some((item) => item.step === warning.step && item.code === warning.code && item.target === warning.target && item.status === 'open');
|
|
635
|
+
const warnings = replaceWarning(receipt, warning, 'open');
|
|
636
|
+
receipt = updateReceipt(input.repoRoot, receipt, { summary: 'pending', taskComment: alreadyOpen ? receipt.taskComment : 'pending', warningProjection: 'pending', warnings, lastError: error });
|
|
637
|
+
} catch { /* preserve primary error */ }
|
|
638
|
+
return { receipt, step: { status: 'blocked', changed: false, error }, changed: false, error };
|
|
639
|
+
}
|
|
640
|
+
}
|
|
641
|
+
|
|
559
642
|
function terminalResult(
|
|
560
643
|
taskId: string,
|
|
561
644
|
receipt: TaskFinalizationReceipt,
|
|
562
|
-
steps: Pick<TaskFinalizationResult, 'lifecycle' | 'taskComment' | 'verification'
|
|
645
|
+
steps: Partial<Pick<TaskFinalizationResult, 'backfill' | 'lifecycle' | 'taskComment' | 'verification' | 'summary'>>,
|
|
563
646
|
changed: boolean,
|
|
564
647
|
error: FinalizationError | null = null
|
|
565
648
|
): TaskFinalizationResult {
|
|
566
649
|
const pending = pendingSteps(receipt);
|
|
567
|
-
const blocked = [steps.lifecycle, steps.taskComment, steps.verification].some((step) => step?.status === 'blocked');
|
|
650
|
+
const blocked = [steps.backfill, steps.lifecycle, steps.taskComment, steps.verification, steps.summary].some((step) => step?.status === 'blocked');
|
|
568
651
|
const warnings = openWarnings(receipt);
|
|
569
652
|
const postLifecyclePending = receipt.lifecycle === 'done' && (pending.some((step) => step !== 'lifecycle') || receipt.warningProjection === 'pending');
|
|
570
653
|
const hardError = error?.code.startsWith('FINALIZATION_') || error?.code === 'TASK_FINALIZATION_RECEIPT_INVALID';
|
|
@@ -572,14 +655,18 @@ function terminalResult(
|
|
|
572
655
|
status: hardError ? (error?.retryable ? 'blocked' : 'failed') : pending.length === 0 ? 'completed' : postLifecyclePending ? 'completed' : blocked ? 'blocked' : 'failed',
|
|
573
656
|
changed,
|
|
574
657
|
taskId,
|
|
575
|
-
|
|
658
|
+
backfill: steps.backfill ?? null,
|
|
659
|
+
lifecycle: steps.lifecycle ?? null,
|
|
660
|
+
taskComment: steps.taskComment ?? null,
|
|
661
|
+
verification: steps.verification ?? null,
|
|
662
|
+
summary: steps.summary ?? null,
|
|
576
663
|
completedSteps: completedSteps(receipt),
|
|
577
664
|
pendingSteps: pending,
|
|
578
|
-
result:
|
|
579
|
-
? '
|
|
580
|
-
:
|
|
581
|
-
?
|
|
582
|
-
: pending.length === 0
|
|
665
|
+
result: hardError
|
|
666
|
+
? (error?.retryable ? 'blocked' : 'failed')
|
|
667
|
+
: postLifecyclePending
|
|
668
|
+
? 'completed_with_warnings'
|
|
669
|
+
: pending.length === 0
|
|
583
670
|
? 'completed'
|
|
584
671
|
: blocked
|
|
585
672
|
? 'blocked'
|
|
@@ -596,6 +683,7 @@ async function applyUnderLock(
|
|
|
596
683
|
): Promise<TaskFinalizationResult> {
|
|
597
684
|
const repoRoot = path.resolve(options.repoRoot);
|
|
598
685
|
const lifecycle = options.lifecycle ?? applyTaskLifecycle;
|
|
686
|
+
const backfill = options.backfill ?? backfillCompletionComments;
|
|
599
687
|
const commentSync = options.commentSync ?? syncPlatformComment;
|
|
600
688
|
const verify = options.verify ?? verifyTaskEvent;
|
|
601
689
|
const consumedCapabilities = new Set<string>();
|
|
@@ -608,17 +696,10 @@ async function applyUnderLock(
|
|
|
608
696
|
} catch (error) {
|
|
609
697
|
return failed(taskId, errorOf(error, 'TASK_FINALIZATION_RECEIPT_INVALID'));
|
|
610
698
|
}
|
|
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
|
-
}
|
|
699
|
+
if (options.controlBinding && (
|
|
700
|
+
receipt.controlBinding?.generation !== options.controlBinding.generation
|
|
701
|
+
|| receipt.controlBinding.requestId !== options.controlBinding.requestId
|
|
702
|
+
)) receipt = updateReceipt(repoRoot, receipt, { controlBinding: options.controlBinding });
|
|
622
703
|
|
|
623
704
|
const preflightState = resolveTaskRef(taskId, { repoRoot });
|
|
624
705
|
if (options.preflight && preflightState.ok && preflightState.state === 'active') {
|
|
@@ -642,8 +723,36 @@ async function applyUnderLock(
|
|
|
642
723
|
}
|
|
643
724
|
}
|
|
644
725
|
|
|
645
|
-
let lifecycleResult: TaskFinalizationStep | null = null;
|
|
646
726
|
let changed = false;
|
|
727
|
+
let backfillResult: TaskFinalizationStep | null = null;
|
|
728
|
+
try {
|
|
729
|
+
const result = await backfill(taskId, { agent: request.agent, cwd: repoRoot });
|
|
730
|
+
backfillResult = commentStep(result);
|
|
731
|
+
const issueNotLinked = result.status === 'no-op' && result.error?.code === 'ISSUE_NOT_LINKED';
|
|
732
|
+
if (!issueNotLinked && (result.status !== 'applied' && result.status !== 'no-op' || result.error)) {
|
|
733
|
+
const detail = backfillResult.error ?? {
|
|
734
|
+
code: 'COMPLETION_BACKFILL_FAILED', message: 'completion artifact backfill did not reach a successful terminal result', retryable: true
|
|
735
|
+
};
|
|
736
|
+
return failed(taskId, detail, {
|
|
737
|
+
backfill: backfillResult,
|
|
738
|
+
completedSteps: completedSteps(receipt),
|
|
739
|
+
pendingSteps: pendingSteps(receipt)
|
|
740
|
+
});
|
|
741
|
+
}
|
|
742
|
+
changed = result.changed;
|
|
743
|
+
if (result.changed && receipt.taskComment !== 'pending') {
|
|
744
|
+
receipt = updateReceipt(repoRoot, receipt, { taskComment: 'pending' });
|
|
745
|
+
}
|
|
746
|
+
} catch (error) {
|
|
747
|
+
const detail = errorOf(error, 'COMPLETION_BACKFILL_FAILED', true);
|
|
748
|
+
return failed(taskId, detail, {
|
|
749
|
+
backfill: { status: detail.retryable ? 'blocked' : 'failed', changed: false, error: detail },
|
|
750
|
+
completedSteps: completedSteps(receipt),
|
|
751
|
+
pendingSteps: pendingSteps(receipt)
|
|
752
|
+
});
|
|
753
|
+
}
|
|
754
|
+
|
|
755
|
+
let lifecycleResult: TaskFinalizationStep | null = null;
|
|
647
756
|
try {
|
|
648
757
|
const resolved = resolveTaskRef(taskId, { repoRoot });
|
|
649
758
|
const registry = resolved.ok && resolved.state === 'completed' ? inspectShortIdRegistry(repoRoot) : null;
|
|
@@ -655,6 +764,7 @@ async function applyUnderLock(
|
|
|
655
764
|
};
|
|
656
765
|
receipt = updateReceipt(repoRoot, receipt, { lifecycle: 'pending', lastError: detail });
|
|
657
766
|
return terminalResult(taskId, receipt, {
|
|
767
|
+
backfill: backfillResult,
|
|
658
768
|
lifecycle: { status: 'failed', changed: false, error: detail },
|
|
659
769
|
taskComment: null,
|
|
660
770
|
verification: null
|
|
@@ -669,6 +779,7 @@ async function applyUnderLock(
|
|
|
669
779
|
};
|
|
670
780
|
receipt = updateReceipt(repoRoot, receipt, { lifecycle: 'pending', lastError: detail });
|
|
671
781
|
return terminalResult(taskId, receipt, {
|
|
782
|
+
backfill: backfillResult,
|
|
672
783
|
lifecycle: { status: 'failed', changed: false, error: detail },
|
|
673
784
|
taskComment: null,
|
|
674
785
|
verification: null
|
|
@@ -686,15 +797,15 @@ async function applyUnderLock(
|
|
|
686
797
|
lifecycleResult = lifecycleStep(result);
|
|
687
798
|
if (result.status !== 'applied' && result.status !== 'no-op') {
|
|
688
799
|
receipt = updateReceipt(repoRoot, receipt, { lifecycle: 'pending', lastError: lifecycleResult.error });
|
|
689
|
-
return terminalResult(taskId, receipt, { lifecycle: lifecycleResult, taskComment: null, verification: null }, result.changed, lifecycleResult.error);
|
|
800
|
+
return terminalResult(taskId, receipt, { backfill: backfillResult, lifecycle: lifecycleResult, taskComment: null, verification: null }, result.changed, lifecycleResult.error);
|
|
690
801
|
}
|
|
691
|
-
changed = result.changed;
|
|
802
|
+
changed = changed || result.changed;
|
|
692
803
|
receipt = updateReceipt(repoRoot, receipt, { lifecycle: 'done', lastError: null });
|
|
693
804
|
}
|
|
694
805
|
} catch (error) {
|
|
695
806
|
const detail = errorOf(error, 'TASK_FINALIZATION_LIFECYCLE_FAILED');
|
|
696
807
|
try { receipt = updateReceipt(repoRoot, receipt, { lifecycle: 'pending', lastError: detail }); } catch { /* preserve the primary error */ }
|
|
697
|
-
return failed(taskId, detail, { lifecycle: lifecycleResult });
|
|
808
|
+
return failed(taskId, detail, { backfill: backfillResult, lifecycle: lifecycleResult });
|
|
698
809
|
}
|
|
699
810
|
|
|
700
811
|
let taskComment: TaskFinalizationStep | null = null;
|
|
@@ -706,21 +817,20 @@ async function applyUnderLock(
|
|
|
706
817
|
taskComment = initialComment.step;
|
|
707
818
|
changed = changed || initialComment.changed;
|
|
708
819
|
if (initialComment.error) {
|
|
709
|
-
return terminalResult(taskId, receipt, { lifecycle: lifecycleResult, taskComment, verification: null }, changed, initialComment.error);
|
|
820
|
+
return terminalResult(taskId, receipt, { backfill: backfillResult, lifecycle: lifecycleResult, taskComment, verification: null }, changed, initialComment.error);
|
|
710
821
|
}
|
|
711
822
|
|
|
712
823
|
let verification: TaskFinalizationStep | null = null;
|
|
713
824
|
try {
|
|
714
825
|
receipt = reconcileWarningProjection(repoRoot, taskId, receipt, consumedCapabilities);
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
if (result.status === 'pass') {
|
|
826
|
+
const verificationPending = receipt.verification === 'pending';
|
|
827
|
+
const result = await verify(
|
|
828
|
+
{ taskRef: taskId, event: 'complete-task.completed' },
|
|
829
|
+
{ repoRoot }
|
|
830
|
+
);
|
|
831
|
+
verification = verificationStep(result);
|
|
832
|
+
if (result.status === 'pass') {
|
|
833
|
+
if (verificationPending) {
|
|
724
834
|
const capability = issueCapability(receipt, 'verification');
|
|
725
835
|
receipt = applyFinalizationReceiptMutationUnderLock(repoRoot, receipt, capability, {
|
|
726
836
|
scope: 'verification', operation: 'succeeded'
|
|
@@ -733,31 +843,76 @@ async function applyUnderLock(
|
|
|
733
843
|
if (finalComment.step.status !== 'no-op') taskComment = finalComment.step;
|
|
734
844
|
changed = changed || finalComment.changed;
|
|
735
845
|
if (finalComment.error) {
|
|
736
|
-
return terminalResult(taskId, receipt, { lifecycle: lifecycleResult, taskComment, verification }, changed, finalComment.error);
|
|
846
|
+
return terminalResult(taskId, receipt, { backfill: backfillResult, lifecycle: lifecycleResult, taskComment, verification }, changed, finalComment.error);
|
|
737
847
|
}
|
|
738
848
|
} else {
|
|
739
|
-
|
|
849
|
+
receipt = updateReceipt(repoRoot, receipt, { lastError: null });
|
|
850
|
+
}
|
|
851
|
+
} else {
|
|
852
|
+
const detail = verification.error ?? { code: 'VERIFY_FAILED', message: 'verification failed', retryable: true };
|
|
853
|
+
if (verificationPending) {
|
|
740
854
|
const capability = issueCapability(receipt, 'verification');
|
|
741
855
|
receipt = applyFinalizationReceiptMutationUnderLock(repoRoot, receipt, capability, {
|
|
742
856
|
scope: 'verification', operation: 'failed', error: detail
|
|
743
857
|
}, consumedCapabilities);
|
|
744
858
|
receipt = reconcileWarningProjection(repoRoot, taskId, receipt, consumedCapabilities);
|
|
745
|
-
|
|
859
|
+
} else {
|
|
860
|
+
const warnings = replaceWarning(receipt, warningFromError('verification', detail), 'open');
|
|
861
|
+
receipt = updateReceipt(repoRoot, receipt, {
|
|
862
|
+
verification: 'pending', taskComment: 'pending', warningProjection: 'pending', warnings, lastError: detail
|
|
863
|
+
});
|
|
864
|
+
receipt = reconcileWarningProjection(repoRoot, taskId, receipt, consumedCapabilities);
|
|
865
|
+
const warningComment = await syncPendingTaskComment({ repoRoot, taskId, agent: request.agent, receipt, commentSync, consumedCapabilities });
|
|
866
|
+
receipt = warningComment.receipt;
|
|
867
|
+
if (warningComment.step.status !== 'no-op') taskComment = warningComment.step;
|
|
868
|
+
changed = changed || warningComment.changed;
|
|
746
869
|
}
|
|
870
|
+
return terminalResult(taskId, receipt, { backfill: backfillResult, lifecycle: lifecycleResult, taskComment, verification }, changed, detail);
|
|
747
871
|
}
|
|
748
872
|
} catch (error) {
|
|
749
873
|
const detail = errorOf(error, 'VERIFY_FAILED', true);
|
|
750
874
|
try {
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
875
|
+
if (receipt.verification === 'pending') {
|
|
876
|
+
const capability = issueCapability(receipt, 'verification');
|
|
877
|
+
receipt = applyFinalizationReceiptMutationUnderLock(repoRoot, receipt, capability, {
|
|
878
|
+
scope: 'verification', operation: 'failed', error: detail
|
|
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
|
+
}
|
|
755
886
|
receipt = reconcileWarningProjection(repoRoot, taskId, receipt, consumedCapabilities);
|
|
887
|
+
} catch (writeError) {
|
|
888
|
+
const persistence = errorOf(writeError, 'FINALIZATION_RECEIPT_WRITE_FAILED', true);
|
|
889
|
+
const hardError: FinalizationError = {
|
|
890
|
+
code: 'FINALIZATION_RECEIPT_WRITE_FAILED',
|
|
891
|
+
message: `Unable to persist verification failure (${detail.code}: ${detail.message}): ${persistence.message}`,
|
|
892
|
+
retryable: true
|
|
893
|
+
};
|
|
894
|
+
return terminalResult(taskId, receipt, {
|
|
895
|
+
backfill: backfillResult, lifecycle: lifecycleResult, taskComment, verification: { status: 'blocked', changed: false, error: detail }
|
|
896
|
+
}, changed, hardError);
|
|
897
|
+
}
|
|
898
|
+
try {
|
|
899
|
+
const warningComment = await syncPendingTaskComment({ repoRoot, taskId, agent: request.agent, receipt, commentSync, consumedCapabilities });
|
|
900
|
+
receipt = warningComment.receipt;
|
|
901
|
+
if (warningComment.step.status !== 'no-op') taskComment = warningComment.step;
|
|
902
|
+
changed = changed || warningComment.changed;
|
|
756
903
|
} catch { /* preserve the primary error */ }
|
|
757
|
-
return terminalResult(taskId, receipt, { lifecycle: lifecycleResult, taskComment, verification: { status: 'blocked', changed: false, error: detail } }, changed, detail);
|
|
904
|
+
return terminalResult(taskId, receipt, { backfill: backfillResult, lifecycle: lifecycleResult, taskComment, verification: { status: 'blocked', changed: false, error: detail } }, changed, detail);
|
|
905
|
+
}
|
|
906
|
+
|
|
907
|
+
let summary = await syncPendingSummary({ repoRoot, taskId, agent: request.agent, receipt, commentSync });
|
|
908
|
+
receipt = summary.receipt;
|
|
909
|
+
changed = changed || summary.changed;
|
|
910
|
+
if (summary.error) {
|
|
911
|
+
receipt = reconcileWarningProjection(repoRoot, taskId, receipt, consumedCapabilities);
|
|
912
|
+
return terminalResult(taskId, receipt, { backfill: backfillResult, lifecycle: lifecycleResult, taskComment, verification, summary: summary.step }, changed, summary.error);
|
|
758
913
|
}
|
|
759
914
|
receipt = reconcileWarningProjection(repoRoot, taskId, receipt, consumedCapabilities);
|
|
760
|
-
return terminalResult(taskId, receipt, { lifecycle: lifecycleResult, taskComment, verification }, changed);
|
|
915
|
+
return terminalResult(taskId, receipt, { backfill: backfillResult, lifecycle: lifecycleResult, taskComment, verification, summary: summary.step }, changed);
|
|
761
916
|
}
|
|
762
917
|
|
|
763
918
|
async function applyTaskFinalization(request: TaskFinalizationRequest, options: TaskFinalizationOptions): Promise<TaskFinalizationResult> {
|
|
@@ -781,7 +936,8 @@ export {
|
|
|
781
936
|
applyFinalizationReceiptMutation,
|
|
782
937
|
applyTaskFinalization,
|
|
783
938
|
issueCapability as createFinalizationCapability,
|
|
784
|
-
readTaskFinalizationReceipt
|
|
939
|
+
readTaskFinalizationReceipt,
|
|
940
|
+
terminalResult
|
|
785
941
|
};
|
|
786
942
|
export type {
|
|
787
943
|
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;
|
package/lib/task/lifecycle.ts
CHANGED
|
@@ -58,7 +58,7 @@ type TaskLifecycleRequest =
|
|
|
58
58
|
| { taskRef: string; intent: 'close-codescan'; agent: string; alertNumber: number; reason: string; dryRun?: boolean }
|
|
59
59
|
| { taskRef: string; intent: 'close-dependabot'; agent: string; alertNumber: number; reason: string; dryRun?: boolean }
|
|
60
60
|
| { taskRef: string; intent: 'restore'; agent: string; stagingDir: string; issueNumber: number; dryRun?: boolean }
|
|
61
|
-
| { taskRef: string; intent: 'recover-started'; agent: string;
|
|
61
|
+
| { taskRef: string; intent: 'recover-started'; agent: string; auto: true; dryRun?: boolean };
|
|
62
62
|
|
|
63
63
|
type HotState = 'active' | 'blocked' | 'completed';
|
|
64
64
|
type SourceState = HotState | 'staging';
|
|
@@ -209,7 +209,7 @@ function actionAndNote(request: TaskLifecycleRequest, restoredFiles = 0): { acti
|
|
|
209
209
|
if (request.intent === 'complete') return { action: 'Complete Task', note: 'Task moved to completed/' };
|
|
210
210
|
if (request.intent === 'close-codescan') return { action: 'Close Codescan', note: `Code Scanning alert #${request.alertNumber} dismissed: ${request.reason}` };
|
|
211
211
|
if (request.intent === 'close-dependabot') return { action: 'Close Dependabot', note: `Dependabot alert #${request.alertNumber} dismissed: ${request.reason}` };
|
|
212
|
-
if (request.intent === 'recover-started') return { action: 'Recover Started', note:
|
|
212
|
+
if (request.intent === 'recover-started') return { action: 'Recover Started', note: 'automatic recovery' };
|
|
213
213
|
return { action: 'Restore Task', note: `Restored ${restoredFiles} files from Issue #${request.issueNumber}` };
|
|
214
214
|
}
|
|
215
215
|
|