@fitlab-ai/agent-infra 0.9.15 → 0.10.0
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/agent-clients/adapters/codex-lifecycle/sandbox-controller.js +6 -2
- package/dist/lib/agent-clients/adapters/codex-lifecycle/store.js +43 -3
- package/dist/lib/fs/durable-write.js +2 -0
- package/dist/lib/internal/cli-route-inventory.js +1 -1
- package/dist/lib/internal/manual-validation.js +17 -8
- package/dist/lib/internal/task-artifact.js +1 -1
- package/dist/lib/internal/task-lifecycle.js +2 -2
- package/dist/lib/internal/task-operation-registry.js +0 -1
- package/dist/lib/internal/task-review.js +4 -4
- package/dist/lib/platform/issue-comments.js +16 -10
- package/dist/lib/platform/issues.js +1 -1
- package/dist/lib/platform/pr-change-report.js +5 -4
- package/dist/lib/platform/pr-summary.js +64 -32
- package/dist/lib/platform/pull-requests.js +3 -3
- package/dist/lib/platform/task-identities.js +2 -9
- package/dist/lib/platform/verification-sync.js +6 -2
- package/dist/lib/pr-review/evidence-grading.js +4 -8
- package/dist/lib/sandbox/commands/rm.js +21 -2
- package/dist/lib/sandbox/control/server.js +29 -9
- package/dist/lib/sandbox/control/state.js +7 -1
- package/dist/lib/sandbox/control/workflow-executor.js +4 -14
- package/dist/lib/sandbox/index.js +1 -2
- package/dist/lib/sandbox/removal.js +116 -1011
- package/dist/lib/sandbox/workspace-identity.js +17 -1
- package/dist/lib/task/artifact-command.js +6 -28
- package/dist/lib/task/artifact-operations.js +16 -230
- package/dist/lib/task/artifact-recovery.js +430 -0
- package/dist/lib/task/artifact-repair-intent.js +73 -39
- package/dist/lib/task/commit-operation.js +31 -4
- package/dist/lib/task/control-authority.js +38 -7
- package/dist/lib/task/control-recovery.js +14 -2
- package/dist/lib/task/create-service.js +1 -8
- package/dist/lib/task/delegation-receipts.js +64 -3
- package/dist/lib/task/events.js +164 -22
- package/dist/lib/task/human-override.js +5 -8
- package/dist/lib/task/intermediate-cleanup.js +66 -8
- package/dist/lib/task/lifecycle-recovery.js +549 -0
- package/dist/lib/task/lifecycle.js +12 -3
- package/dist/lib/task/local-artifact-finalization.js +148 -123
- package/dist/lib/task/manual-validation-receipt.js +5 -1
- package/dist/lib/task/orchestration.js +52 -2
- package/dist/lib/task/pr-delivery-fact.js +12 -56
- package/dist/lib/task/recovery-warning.js +19 -0
- package/dist/lib/task/review-command.js +1 -0
- package/dist/lib/task/review-finalization.js +104 -77
- package/dist/lib/task/short-id.js +15 -7
- package/dist/lib/task/task-execution-lock.js +1 -1
- package/dist/lib/task/verification-engine.js +3 -14
- package/dist/lib/task/workflow-command.js +0 -1
- package/dist/lib/version.js +1 -11
- package/dist/lifecycle-build-manifest.json +3 -3
- package/lib/agent-clients/adapters/codex-lifecycle/sandbox-controller.ts +7 -2
- package/lib/agent-clients/adapters/codex-lifecycle/store.ts +55 -3
- package/lib/fs/durable-write.ts +1 -0
- package/lib/internal/cli-route-inventory.ts +1 -1
- package/lib/internal/manual-validation.ts +17 -8
- package/lib/internal/task-artifact.ts +1 -1
- package/lib/internal/task-lifecycle.ts +4 -3
- package/lib/internal/task-operation-registry.ts +0 -1
- package/lib/internal/task-review.ts +4 -4
- package/lib/platform/issue-comments.ts +18 -10
- package/lib/platform/issues.ts +1 -1
- package/lib/platform/pr-change-report.ts +7 -5
- package/lib/platform/pr-summary.ts +64 -32
- package/lib/platform/pull-requests.ts +3 -3
- package/lib/platform/task-identities.ts +3 -14
- package/lib/platform/verification-sync.ts +6 -2
- package/lib/pr-review/evidence-grading.ts +4 -9
- package/lib/sandbox/commands/rm.ts +34 -2
- package/lib/sandbox/control/protocol.ts +5 -0
- package/lib/sandbox/control/server.ts +30 -10
- package/lib/sandbox/control/state.ts +9 -1
- package/lib/sandbox/control/workflow-executor.ts +4 -14
- package/lib/sandbox/index.ts +1 -2
- package/lib/sandbox/removal.ts +126 -1128
- package/lib/sandbox/workspace-identity.ts +19 -3
- package/lib/task/artifact-command.ts +8 -27
- package/lib/task/artifact-operations.ts +21 -251
- package/lib/task/artifact-recovery.ts +519 -0
- package/lib/task/artifact-repair-intent.ts +99 -51
- package/lib/task/commit-operation.ts +35 -4
- package/lib/task/control-authority.ts +47 -14
- package/lib/task/control-recovery.ts +13 -1
- package/lib/task/create-service.ts +1 -10
- package/lib/task/delegation-receipts.ts +73 -2
- package/lib/task/events.ts +187 -23
- package/lib/task/human-override.ts +5 -8
- package/lib/task/intermediate-cleanup.ts +50 -12
- package/lib/task/lifecycle-recovery.ts +691 -0
- package/lib/task/lifecycle.ts +13 -4
- package/lib/task/local-artifact-finalization.ts +176 -132
- package/lib/task/manual-validation-receipt.ts +5 -1
- package/lib/task/orchestration.ts +65 -0
- package/lib/task/pr-delivery-fact.ts +15 -62
- package/lib/task/recovery-warning.ts +21 -0
- package/lib/task/review-command.ts +1 -0
- package/lib/task/review-finalization.ts +106 -87
- package/lib/task/short-id.ts +27 -5
- package/lib/task/task-execution-lock.ts +1 -1
- package/lib/task/verification-engine.ts +3 -17
- package/lib/task/verification-types.ts +0 -2
- package/lib/task/workflow-command.ts +0 -1
- package/lib/version.ts +1 -15
- package/package.json +1 -1
- package/templates/.agents/rules/create-issue.en.md +1 -1
- package/templates/.agents/rules/create-issue.zh-CN.md +1 -1
- package/templates/.agents/rules/issue-sync.en.md +4 -2
- package/templates/.agents/rules/issue-sync.zh-CN.md +4 -3
- package/templates/.agents/rules/local-artifact-repair.en.md +46 -39
- package/templates/.agents/rules/local-artifact-repair.zh-CN.md +43 -36
- package/templates/.agents/skills/analyze-task/SKILL.en.md +6 -6
- package/templates/.agents/skills/analyze-task/SKILL.zh-CN.md +6 -6
- package/templates/.agents/skills/analyze-task/config/verify.en.json +1 -1
- package/templates/.agents/skills/analyze-task/config/verify.zh-CN.json +1 -1
- package/templates/.agents/skills/block-task/SKILL.en.md +2 -2
- package/templates/.agents/skills/block-task/SKILL.zh-CN.md +2 -2
- package/templates/.agents/skills/block-task/config/verify.json +1 -1
- package/templates/.agents/skills/cancel-task/SKILL.en.md +3 -3
- package/templates/.agents/skills/cancel-task/SKILL.zh-CN.md +3 -3
- package/templates/.agents/skills/cancel-task/config/verify.json +1 -1
- package/templates/.agents/skills/code-task/SKILL.en.md +7 -7
- package/templates/.agents/skills/code-task/SKILL.zh-CN.md +7 -7
- package/templates/.agents/skills/code-task/config/verify.en.json +1 -1
- package/templates/.agents/skills/code-task/config/verify.zh-CN.json +1 -1
- package/templates/.agents/skills/commit/reference/issue-metadata-sync.en.md +1 -1
- package/templates/.agents/skills/commit/reference/issue-metadata-sync.zh-CN.md +1 -1
- package/templates/.agents/skills/complete-manual-validation/SKILL.en.md +1 -1
- package/templates/.agents/skills/complete-manual-validation/SKILL.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/complete-task/config/verify.en.json +2 -2
- package/templates/.agents/skills/complete-task/config/verify.zh-CN.json +2 -2
- package/templates/.agents/skills/complete-task/reference/external-delivery.en.md +1 -1
- package/templates/.agents/skills/complete-task/reference/external-delivery.zh-CN.md +1 -1
- package/templates/.agents/skills/create-pr/SKILL.en.md +2 -2
- package/templates/.agents/skills/create-pr/SKILL.zh-CN.md +2 -2
- package/templates/.agents/skills/create-pr/config/verify.json +1 -1
- package/templates/.agents/skills/create-pr/reference/pr-body-template.en.md +2 -2
- package/templates/.agents/skills/create-pr/reference/pr-body-template.zh-CN.md +2 -2
- package/templates/.agents/skills/create-task/SKILL.en.md +5 -5
- package/templates/.agents/skills/create-task/SKILL.zh-CN.md +5 -5
- package/templates/.agents/skills/create-task/config/verify.json +1 -1
- package/templates/.agents/skills/import-issue/SKILL.en.md +8 -8
- package/templates/.agents/skills/import-issue/SKILL.zh-CN.md +8 -8
- package/templates/.agents/skills/import-issue/config/verify.json +2 -2
- package/templates/.agents/skills/plan-task/SKILL.en.md +4 -4
- package/templates/.agents/skills/plan-task/SKILL.zh-CN.md +4 -4
- package/templates/.agents/skills/plan-task/config/verify.en.json +1 -1
- package/templates/.agents/skills/plan-task/config/verify.zh-CN.json +1 -1
- package/templates/.agents/skills/restore-task/SKILL.en.md +2 -2
- package/templates/.agents/skills/restore-task/SKILL.zh-CN.md +2 -2
- package/templates/.agents/skills/restore-task/config/verify.json +1 -1
- 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-analysis/config/verify.en.json +1 -1
- package/templates/.agents/skills/review-analysis/config/verify.zh-CN.json +1 -1
- package/templates/.agents/skills/review-analysis/reference/report-template.en.md +7 -7
- package/templates/.agents/skills/review-analysis/reference/report-template.zh-CN.md +6 -6
- package/templates/.agents/skills/review-code/SKILL.en.md +2 -2
- package/templates/.agents/skills/review-code/SKILL.zh-CN.md +2 -2
- package/templates/.agents/skills/review-code/config/verify.en.json +1 -1
- package/templates/.agents/skills/review-code/config/verify.zh-CN.json +1 -1
- package/templates/.agents/skills/review-code/reference/report-template.en.md +12 -12
- package/templates/.agents/skills/review-code/reference/report-template.zh-CN.md +12 -12
- 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/review-plan/config/verify.en.json +1 -1
- package/templates/.agents/skills/review-plan/config/verify.zh-CN.json +1 -1
- package/templates/.agents/skills/review-plan/reference/report-template.en.md +7 -7
- package/templates/.agents/skills/review-plan/reference/report-template.zh-CN.md +7 -7
- package/templates/.agents/skills/review-pr/config/verify.en.json +1 -1
- package/templates/.agents/skills/review-pr/config/verify.zh-CN.json +1 -1
- package/templates/.agents/skills/review-pr/reference/evidence-grading.en.md +3 -3
- package/templates/.agents/skills/review-pr/reference/evidence-grading.zh-CN.md +3 -3
- package/templates/.agents/skills/run-manual-validation/config/verify.en.json +1 -1
- package/templates/.agents/skills/run-manual-validation/config/verify.zh-CN.json +1 -1
- package/templates/.agents/skills/run-task/SKILL.en.md +1 -1
- package/templates/.agents/skills/run-task/SKILL.zh-CN.md +1 -1
- package/templates/.agents/templates/task.en.md +1 -1
- package/templates/.agents/templates/task.zh-CN.md +1 -1
- package/templates/.claude/commands/import-issue.en.md +1 -1
- package/templates/.claude/commands/import-issue.zh-CN.md +1 -1
- package/templates/.claude/commands/restore-task.en.md +1 -1
- package/templates/.claude/commands/restore-task.zh-CN.md +1 -1
|
@@ -181,6 +181,16 @@ function checkpointIdentity(input, task, delivery) {
|
|
|
181
181
|
function checkpointTimestamp() {
|
|
182
182
|
return new Date().toISOString();
|
|
183
183
|
}
|
|
184
|
+
function samePreparedCheckpointIdentity(pending, identity) {
|
|
185
|
+
return pending.state === 'prepared'
|
|
186
|
+
&& pending.taskId === identity.taskId
|
|
187
|
+
&& pending.branch === identity.branch
|
|
188
|
+
&& pending.mode === identity.mode
|
|
189
|
+
&& pending.expectedHead === identity.expectedHead
|
|
190
|
+
&& pending.message === identity.message
|
|
191
|
+
&& pending.round === identity.round
|
|
192
|
+
&& JSON.stringify(pending.paths) === JSON.stringify(identity.paths);
|
|
193
|
+
}
|
|
184
194
|
function executeUnlocked(input, task, mode) {
|
|
185
195
|
const inspected = inspectGitWorkflow(input.cwd);
|
|
186
196
|
if (!inspected.snapshot)
|
|
@@ -214,11 +224,28 @@ function executeUnlocked(input, task, mode) {
|
|
|
214
224
|
error: { code: 'COMMIT_INTENT_INVALID', message: error instanceof Error ? error.message : String(error) }
|
|
215
225
|
};
|
|
216
226
|
}
|
|
217
|
-
if (pendingIntent && !sameCheckpointIntent(pendingIntent, identity))
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
227
|
+
if (pendingIntent && !sameCheckpointIntent(pendingIntent, identity)) {
|
|
228
|
+
if (!input.recoverPreparedIntent || !samePreparedCheckpointIdentity(pendingIntent, identity))
|
|
229
|
+
return {
|
|
230
|
+
status: 'failed', changed: false, snapshot: inspected.snapshot, operations: [],
|
|
231
|
+
error: { code: 'COMMIT_INTENT_CONFLICT', message: 'A different checkpoint intent is still pending for this task' }
|
|
232
|
+
};
|
|
233
|
+
pendingIntent = {
|
|
234
|
+
...pendingIntent,
|
|
235
|
+
expectedTree: identity.expectedTree,
|
|
236
|
+
digest: checkpointIntentDigest(identity),
|
|
237
|
+
updatedAt: checkpointTimestamp()
|
|
221
238
|
};
|
|
239
|
+
try {
|
|
240
|
+
writeCheckpointIntent(task.repoRoot, pendingIntent);
|
|
241
|
+
}
|
|
242
|
+
catch (error) {
|
|
243
|
+
return {
|
|
244
|
+
status: 'failed', changed: false, snapshot: inspected.snapshot, operations: [],
|
|
245
|
+
error: { code: 'COMMIT_INTENT_WRITE_FAILED', message: error instanceof Error ? error.message : String(error) }
|
|
246
|
+
};
|
|
247
|
+
}
|
|
248
|
+
}
|
|
222
249
|
if (!pendingIntent) {
|
|
223
250
|
const timestamp = checkpointTimestamp();
|
|
224
251
|
pendingIntent = {
|
|
@@ -8,12 +8,13 @@ import { activateMatchingOrchestrationDelegation, activateOrchestrationDelegatio
|
|
|
8
8
|
import { prepareCodexOrchestrationDelegation } from './codex-orchestration.js';
|
|
9
9
|
import { applyTaskFinalization } from './finalization.js';
|
|
10
10
|
import { applyTaskLifecycle, taskLifecycleFailure } from './lifecycle.js';
|
|
11
|
+
import { recoverStartedLifecycleUnderLock, recoveryFailure } from './lifecycle-recovery.js';
|
|
11
12
|
import { locateActivityLog } from './activity-log.js';
|
|
12
13
|
import { resolveTaskRef, TASK_ID_RE } from './resolve-ref.js';
|
|
13
14
|
import { TaskExecutionLockError, withTaskExecutionLock } from './task-execution-lock.js';
|
|
14
15
|
import { verifyTaskEvent } from './verification.js';
|
|
15
16
|
import { createCodexCapabilityStore } from '../agent-clients/adapters/codex-lifecycle/capability-store.js';
|
|
16
|
-
import {
|
|
17
|
+
import { findArtifactRecoveryIntentsByOperation, writeArtifactRecoveryIntent } from './artifact-repair-intent.js';
|
|
17
18
|
import { computeLifecycleBuildIdentity, verifyLifecycleBuildIdentity } from '../agent-clients/adapters/codex-lifecycle/build-identity.js';
|
|
18
19
|
const lifecycleAuthorityPhases = [
|
|
19
20
|
'orchestration.prepare', 'artifact.finalize-local', 'task-event.completed'
|
|
@@ -184,7 +185,7 @@ export function queryLifecycleRecoveryOperation(operationId, options = {}) {
|
|
|
184
185
|
const store = options.capabilityStore ?? createCodexCapabilityStore();
|
|
185
186
|
const registry = authorityOperationMap.get(store);
|
|
186
187
|
const known = [...(registry?.values() ?? [])].filter((value) => value.operationId === operationId);
|
|
187
|
-
const intents = options.repoRoot ?
|
|
188
|
+
const intents = options.repoRoot ? findArtifactRecoveryIntentsByOperation(options.repoRoot, operationId) : [];
|
|
188
189
|
const capabilities = store.findByRecoveryOperation(operationId);
|
|
189
190
|
const phaseMap = new Map();
|
|
190
191
|
for (const capability of capabilities) {
|
|
@@ -312,19 +313,19 @@ export function recoverLifecycleRecoveryOperation(input, options) {
|
|
|
312
313
|
if (!phase) {
|
|
313
314
|
throw lifecycleRecoveryCompensationError('LIFECYCLE_RECOVERY_PHASE_MISSING', 'durable task completion phase is missing');
|
|
314
315
|
}
|
|
315
|
-
const intents =
|
|
316
|
+
const intents = findArtifactRecoveryIntentsByOperation(options.repoRoot, selector.operationId)
|
|
316
317
|
.filter((intent) => intent.taskId === selector.taskId
|
|
317
318
|
&& intent.family === selector.family
|
|
318
319
|
&& intent.artifact === selector.artifact);
|
|
319
320
|
if (intents.length !== 1) {
|
|
320
|
-
throw lifecycleRecoveryCompensationError('LIFECYCLE_RECOVERY_INTENT_INVALID', 'durable artifact
|
|
321
|
+
throw lifecycleRecoveryCompensationError('LIFECYCLE_RECOVERY_INTENT_INVALID', 'durable artifact recovery intent does not identify this task event');
|
|
321
322
|
}
|
|
322
323
|
const intent = intents[0];
|
|
323
324
|
if (!lifecycleRecoveryIntentMatches(intent, selector)
|
|
324
325
|
|| intent.phase !== selector.phase
|
|
325
326
|
|| intent.requestId !== selector.lifecycleRequestId
|
|
326
327
|
|| intent.authorityDigest === null) {
|
|
327
|
-
throw lifecycleRecoveryCompensationError('LIFECYCLE_RECOVERY_INTENT_INVALID', 'durable artifact
|
|
328
|
+
throw lifecycleRecoveryCompensationError('LIFECYCLE_RECOVERY_INTENT_INVALID', 'durable artifact recovery intent does not match the recovery request');
|
|
328
329
|
}
|
|
329
330
|
if (!lifecycleRecoveryEventCommitted(selector, options.repoRoot)) {
|
|
330
331
|
throw lifecycleRecoveryCompensationError('LIFECYCLE_RECOVERY_COMMIT_UNCONFIRMED', 'task completion is not durably present; compensation is not allowed');
|
|
@@ -339,7 +340,7 @@ export function recoverLifecycleRecoveryOperation(input, options) {
|
|
|
339
340
|
throw lifecycleRecoveryCompensationError('LIFECYCLE_RECOVERY_STATE_INVALID', 'consumed capability has an unconsumed completion phase');
|
|
340
341
|
}
|
|
341
342
|
if (intent.state !== 'consumed') {
|
|
342
|
-
|
|
343
|
+
writeArtifactRecoveryIntent(options.repoRoot, {
|
|
343
344
|
...intent,
|
|
344
345
|
state: 'consumed',
|
|
345
346
|
updatedAt: (options.now ?? Date.now)()
|
|
@@ -473,6 +474,9 @@ async function applyLifecycleWithAuthority(context, request) {
|
|
|
473
474
|
return taskLifecycleFailure(request, { code: 'LIFECYCLE_PAYLOAD_INVALID', message: conflict.message });
|
|
474
475
|
}
|
|
475
476
|
const execute = async () => {
|
|
477
|
+
if (request.intent === 'recover-started') {
|
|
478
|
+
return recoverStartedLifecycleUnderLock(request, { repoRoot: context.repoRoot });
|
|
479
|
+
}
|
|
476
480
|
const lifecycleResult = applyTaskLifecycle(request, { repoRoot: context.repoRoot });
|
|
477
481
|
if (lifecycleResult.status !== 'failed' || !request.overrideTicket)
|
|
478
482
|
return lifecycleResult;
|
|
@@ -506,6 +510,9 @@ async function applyLifecycleWithAuthority(context, request) {
|
|
|
506
510
|
catch (error) {
|
|
507
511
|
if (!(error instanceof TaskExecutionLockError))
|
|
508
512
|
throw error;
|
|
513
|
+
if (request.intent === 'recover-started') {
|
|
514
|
+
return recoveryFailure(request, 'owner-unknown', error.code, error.message);
|
|
515
|
+
}
|
|
509
516
|
return taskLifecycleFailure(request, { code: error.code, message: error.message }, taskId);
|
|
510
517
|
}
|
|
511
518
|
}
|
|
@@ -616,7 +623,7 @@ function operationInvalid(message) {
|
|
|
616
623
|
}
|
|
617
624
|
const LIFECYCLE_FLAGS = new Set([
|
|
618
625
|
'--agent', '--reason', '--unblock-condition', '--note', '--alert-number', '--staging-dir', '--issue-number',
|
|
619
|
-
'--override-ticket', '--override-target', '--override-scope', '--dry-run'
|
|
626
|
+
'--stage', '--round', '--artifact', '--override-ticket', '--override-target', '--override-scope', '--dry-run'
|
|
620
627
|
]);
|
|
621
628
|
const FINALIZATION_FLAGS = new Set(['--agent']);
|
|
622
629
|
const ORCHESTRATION_FLAGS = new Set([
|
|
@@ -692,8 +699,32 @@ export function parseTaskControlOperation(family, args) {
|
|
|
692
699
|
...(value(values, '--override-scope') ? { overrideScope: value(values, '--override-scope') } : {}),
|
|
693
700
|
...(value(values, '--alert-number') ? { alertNumber: Number(value(values, '--alert-number')) } : {}),
|
|
694
701
|
...(value(values, '--issue-number') ? { issueNumber: Number(value(values, '--issue-number')) } : {}),
|
|
702
|
+
...(value(values, '--stage') ? { stage: value(values, '--stage') } : {}),
|
|
703
|
+
...(value(values, '--round') ? { round: Number(value(values, '--round')) } : {}),
|
|
704
|
+
...(value(values, '--artifact') ? { artifact: value(values, '--artifact') } : {}),
|
|
695
705
|
...(values['--dry-run'] === true ? { dryRun: true } : {})
|
|
696
706
|
};
|
|
707
|
+
if (intent === 'recover-started') {
|
|
708
|
+
const recoveryFlags = new Set(['--agent', '--stage', '--round', '--artifact', '--reason']);
|
|
709
|
+
for (const flag of Object.keys(values)) {
|
|
710
|
+
if (!recoveryFlags.has(flag))
|
|
711
|
+
operationInvalid(`option '${flag}' is not supported for recover-started`);
|
|
712
|
+
}
|
|
713
|
+
required(values, ['--stage', '--round', '--artifact', '--reason']);
|
|
714
|
+
const stage = value(values, '--stage');
|
|
715
|
+
if (!['analysis', 'review-analysis', 'plan', 'review-plan', 'code', 'review-code'].includes(stage ?? '')) {
|
|
716
|
+
operationInvalid('--stage must be a supported lifecycle stage');
|
|
717
|
+
}
|
|
718
|
+
const round = Number(value(values, '--round'));
|
|
719
|
+
if (!Number.isSafeInteger(round) || round < 1)
|
|
720
|
+
operationInvalid('--round must be a positive integer');
|
|
721
|
+
if (value(values, '--reason').includes('\n') || value(values, '--reason').includes('\r')) {
|
|
722
|
+
operationInvalid('--reason must be a single line');
|
|
723
|
+
}
|
|
724
|
+
}
|
|
725
|
+
else if (values['--stage'] !== undefined || values['--round'] !== undefined || values['--artifact'] !== undefined) {
|
|
726
|
+
operationInvalid('--stage, --round, and --artifact are only supported for recover-started');
|
|
727
|
+
}
|
|
697
728
|
return { family, request: input };
|
|
698
729
|
}
|
|
699
730
|
const values = parseValues(args, 2, ORCHESTRATION_FLAGS);
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { createHash } from 'node:crypto';
|
|
2
2
|
import { isCompletionEvidence } from './orchestration.js';
|
|
3
3
|
import { TASK_WORKFLOW_OPERATIONS } from './workflow-command.js';
|
|
4
|
+
import { sameRecoveryWarning } from './recovery-warning.js';
|
|
4
5
|
/** Normalize the command result once at the control transport boundary. */
|
|
5
6
|
export function parseControlOutput(output) {
|
|
6
7
|
if (output === null)
|
|
@@ -18,7 +19,7 @@ export function parseControlOutput(output) {
|
|
|
18
19
|
export const SANDBOX_CONTROL_REQUIRED_COMPLETION_PHASES = Object.freeze([
|
|
19
20
|
'completed', 'evidence-written', 'publish-authorized'
|
|
20
21
|
]);
|
|
21
|
-
const LIFECYCLE_INTENTS = ['block', 'activate', 'cancel', 'complete', 'close-codescan', 'close-dependabot', 'restore'];
|
|
22
|
+
const LIFECYCLE_INTENTS = ['block', 'activate', 'cancel', 'complete', 'close-codescan', 'close-dependabot', 'restore', 'recover-started'];
|
|
22
23
|
const ORCHESTRATION_INTENTS = [
|
|
23
24
|
'begin-or-resume', 'route.read', 'route.clean-completion', 'status', 'prepare', 'dispatch',
|
|
24
25
|
'await-activation', 'recover-prepared', 'hook-start', 'hook-stop', 'advance', 'pause'
|
|
@@ -65,6 +66,15 @@ function domainEvidenceMatches(operation, result, domain) {
|
|
|
65
66
|
&& snapshot.worktreeTree === completion.worktreeTree
|
|
66
67
|
&& lastReviewedCommit === completion.lastReviewedCommit;
|
|
67
68
|
}
|
|
69
|
+
if (operation.family === 'task-lifecycle' && operation.intent === 'recover-started') {
|
|
70
|
+
const retryRequired = domain.recoveryState === 'retry-required'
|
|
71
|
+
&& sameRecoveryWarning(domain.warning, result.warning);
|
|
72
|
+
return result.targetState === 'active'
|
|
73
|
+
&& ['applied', 'no-op'].includes(String(result.status))
|
|
74
|
+
&& domain.recovery === true
|
|
75
|
+
&& domain.targetState === 'active'
|
|
76
|
+
&& (domain.recoveryState === 'released' || retryRequired);
|
|
77
|
+
}
|
|
68
78
|
if (operation.class === 'read-only')
|
|
69
79
|
return result.changed === false && domain.snapshotValid === true;
|
|
70
80
|
return true;
|
|
@@ -119,7 +129,9 @@ export function classifySandboxControlRecovery(input) {
|
|
|
119
129
|
responseReconstructable: true,
|
|
120
130
|
reasonCode: input.operation.class === 'route.clean-completion' && result.changed === false
|
|
121
131
|
? 'RECOVERY_ROUTE_COMPLETION_NOOP'
|
|
122
|
-
: '
|
|
132
|
+
: input.operation.intent === 'recover-started' && input.domain?.recoveryState === 'retry-required'
|
|
133
|
+
? 'RECOVERY_RELEASE_RETRY_REQUIRED'
|
|
134
|
+
: 'RECOVERY_TERMINAL_AND_DOMAIN_MATCH'
|
|
123
135
|
};
|
|
124
136
|
}
|
|
125
137
|
export function operationRecoveryBinding(requestId, generation, taskId, family, intent) {
|
|
@@ -1,5 +1,3 @@
|
|
|
1
|
-
import fs from 'node:fs';
|
|
2
|
-
import path from 'node:path';
|
|
3
1
|
import { syncPlatformComment } from '../platform/issue-comments.js';
|
|
4
2
|
import { createPlatformIssue, syncPlatformIssue } from '../platform/issues.js';
|
|
5
3
|
import { VERSION } from '../version.js';
|
|
@@ -139,11 +137,6 @@ function platformOperations(prefix, operations) {
|
|
|
139
137
|
reasonCode: operation.reasonCode ?? null
|
|
140
138
|
}));
|
|
141
139
|
}
|
|
142
|
-
function issueFromTask(repoRoot, taskId) {
|
|
143
|
-
const content = fs.readFileSync(path.join(repoRoot, '.agents', 'workspace', 'active', taskId, 'task.md'), 'utf8');
|
|
144
|
-
const match = /^issue_number:\s*(\d+)\s*$/m.exec(content);
|
|
145
|
-
return match ? { number: Number(match[1]), url: '' } : null;
|
|
146
|
-
}
|
|
147
140
|
async function createTask(value, options) {
|
|
148
141
|
const dependencies = { ...DEFAULT_DEPENDENCIES, ...options.dependencies };
|
|
149
142
|
let candidate;
|
|
@@ -163,7 +156,7 @@ async function createTask(value, options) {
|
|
|
163
156
|
const operations = [{
|
|
164
157
|
name: 'task:local', status: local.status, reasonCode: null
|
|
165
158
|
}];
|
|
166
|
-
let issue =
|
|
159
|
+
let issue = null;
|
|
167
160
|
const created = await dependencies.createIssue(local.task.id, { cwd: options.repoRoot, agent: candidate.agent });
|
|
168
161
|
operations.push(...platformOperations('platform-create', created.operations));
|
|
169
162
|
if (created.issue)
|
|
@@ -92,7 +92,8 @@ function hasCurrentCodexEvidence(receipt) {
|
|
|
92
92
|
const provenance = receipt.lifecycleProvenance;
|
|
93
93
|
if (!provenance)
|
|
94
94
|
return false;
|
|
95
|
-
const activated = ['activated', 'stage-completed', 'sealed', 'consumed'].includes(receipt.status)
|
|
95
|
+
const activated = ['activated', 'stage-completed', 'sealed', 'consumed'].includes(receipt.status)
|
|
96
|
+
|| (receipt.status === 'aborted' && receipt.activatedAt !== null);
|
|
96
97
|
if (!activated)
|
|
97
98
|
return receipt.hostEvidence === null;
|
|
98
99
|
const host = receipt.hostEvidence;
|
|
@@ -124,6 +125,12 @@ function hasCurrentCodexEvidence(receipt) {
|
|
|
124
125
|
&& host.consumer === receipt.id
|
|
125
126
|
&& exactText(host.consumedAt);
|
|
126
127
|
}
|
|
128
|
+
if (receipt.status === 'aborted') {
|
|
129
|
+
return Number.isSafeInteger(host.stopRevision)
|
|
130
|
+
&& host.stopRevision > host.startRevision
|
|
131
|
+
&& host.consumer === `lifecycle-recovery:${receipt.taskId}:${receipt.id}`
|
|
132
|
+
&& exactText(host.consumedAt);
|
|
133
|
+
}
|
|
127
134
|
return host.stopRevision === null && host.consumer === null && host.consumedAt === null;
|
|
128
135
|
}
|
|
129
136
|
const RECEIPT_KEYS = [
|
|
@@ -147,7 +154,8 @@ function hasStatusBoundEvidence(receipt) {
|
|
|
147
154
|
const dispatchComplete = dispatchFields.every((field) => field !== null);
|
|
148
155
|
if (!dispatchEmpty && !dispatchComplete)
|
|
149
156
|
return false;
|
|
150
|
-
const beforeActivation = ['prepared', '
|
|
157
|
+
const beforeActivation = ['prepared', 'expired'].includes(receipt.status)
|
|
158
|
+
|| (receipt.status === 'aborted' && receipt.activatedAt === null);
|
|
151
159
|
if (beforeActivation) {
|
|
152
160
|
return receipt.parentId === null
|
|
153
161
|
&& receipt.childId === null
|
|
@@ -181,6 +189,20 @@ function hasStatusBoundEvidence(receipt) {
|
|
|
181
189
|
&& receipt.sealedAt === null
|
|
182
190
|
&& receipt.consumedAt === null;
|
|
183
191
|
}
|
|
192
|
+
if (receipt.status === 'aborted') {
|
|
193
|
+
const host = receipt.hostEvidence;
|
|
194
|
+
return receipt.agent === null
|
|
195
|
+
&& receipt.afterFingerprint === null
|
|
196
|
+
&& receipt.changedPaths.length === 0
|
|
197
|
+
&& receipt.sealedAt === null
|
|
198
|
+
&& receipt.consumedAt === null
|
|
199
|
+
&& host !== null
|
|
200
|
+
&& host !== undefined
|
|
201
|
+
&& Number.isSafeInteger(host.stopRevision)
|
|
202
|
+
&& host.stopRevision > host.startRevision
|
|
203
|
+
&& host.consumer === `lifecycle-recovery:${receipt.taskId}:${receipt.id}`
|
|
204
|
+
&& exactText(host.consumedAt);
|
|
205
|
+
}
|
|
184
206
|
if (!exactText(receipt.agent)
|
|
185
207
|
|| normalizeAgentToken(receipt.agent) !== normalizeAgentToken(receipt.client))
|
|
186
208
|
return false;
|
|
@@ -429,6 +451,45 @@ function abortPreparedDelegation(receipt) {
|
|
|
429
451
|
receipt: Object.freeze({ ...receipt, status: 'aborted' })
|
|
430
452
|
};
|
|
431
453
|
}
|
|
454
|
+
function abortActivatedDelegation(receipt, event) {
|
|
455
|
+
if (receipt.status !== 'activated') {
|
|
456
|
+
return fail('DELEGATION_STATE_INVALID', `delegation ${receipt.id} is ${receipt.status}, expected activated`);
|
|
457
|
+
}
|
|
458
|
+
if (receipt.client !== 'codex'
|
|
459
|
+
|| !receipt.lifecycleProvenance
|
|
460
|
+
|| receipt.hostEvidence?.kind !== 'codex-lifecycle-v2'
|
|
461
|
+
|| typeof receipt.lifecycleProvenance.controllerInstanceDigest !== 'string'
|
|
462
|
+
|| typeof receipt.lifecycleProvenance.controlGeneration !== 'string'
|
|
463
|
+
|| typeof receipt.hostEvidence.controllerInstanceDigest !== 'string'
|
|
464
|
+
|| typeof receipt.hostEvidence.controlGeneration !== 'string') {
|
|
465
|
+
return fail('DELEGATION_RECOVERY_EVIDENCE_INVALID', 'activated delegation lacks the current Codex controller binding');
|
|
466
|
+
}
|
|
467
|
+
if (event.childId !== receipt.childId
|
|
468
|
+
|| !Number.isSafeInteger(event.stopRevision)
|
|
469
|
+
|| event.stopRevision <= receipt.hostEvidence.startRevision
|
|
470
|
+
|| event.consumer !== `lifecycle-recovery:${receipt.taskId}:${receipt.id}`
|
|
471
|
+
|| !exactText(event.consumedAt)) {
|
|
472
|
+
return fail('DELEGATION_RECOVERY_EVIDENCE_INVALID', 'recovery stop evidence does not match the activated delegation');
|
|
473
|
+
}
|
|
474
|
+
return {
|
|
475
|
+
ok: true,
|
|
476
|
+
receipt: Object.freeze({
|
|
477
|
+
...receipt,
|
|
478
|
+
status: 'aborted',
|
|
479
|
+
agent: null,
|
|
480
|
+
afterFingerprint: null,
|
|
481
|
+
changedPaths: Object.freeze([]),
|
|
482
|
+
sealedAt: null,
|
|
483
|
+
consumedAt: null,
|
|
484
|
+
hostEvidence: Object.freeze({
|
|
485
|
+
...receipt.hostEvidence,
|
|
486
|
+
stopRevision: event.stopRevision,
|
|
487
|
+
consumer: event.consumer,
|
|
488
|
+
consumedAt: event.consumedAt
|
|
489
|
+
})
|
|
490
|
+
})
|
|
491
|
+
};
|
|
492
|
+
}
|
|
432
493
|
function completeDelegationStage(receipt, event) {
|
|
433
494
|
if (receipt.status !== 'activated')
|
|
434
495
|
return fail('DELEGATION_STATE_INVALID', `delegation ${receipt.id} is ${receipt.status}, expected activated`);
|
|
@@ -494,5 +555,5 @@ function consumeDelegation(receipt, options = {}) {
|
|
|
494
555
|
consumedAt: (options.now ?? (() => new Date().toISOString()))()
|
|
495
556
|
}) };
|
|
496
557
|
}
|
|
497
|
-
export { activateDelegation, abortPreparedDelegation, completeDelegationStage, consumeDelegation, dispatchDelegation, foldBlankToNull, isDelegationReceipt, managedDelegationRole, prepareDelegation, sealDelegation };
|
|
558
|
+
export { activateDelegation, abortActivatedDelegation, abortPreparedDelegation, completeDelegationStage, consumeDelegation, dispatchDelegation, foldBlankToNull, isDelegationReceipt, managedDelegationRole, prepareDelegation, sealDelegation };
|
|
498
559
|
//# sourceMappingURL=delegation-receipts.js.map
|
package/dist/lib/task/events.js
CHANGED
|
@@ -16,13 +16,14 @@ import { TaskExecutionLockError, withTaskExecutionLock } from './task-execution-
|
|
|
16
16
|
import { captureTaskWriteMetadata, writeTask } from './write.js';
|
|
17
17
|
import { allowsManualOverride } from './guard-override.js';
|
|
18
18
|
import { consumeLocalArtifactFinalizationIntent, readLocalArtifactFinalizationIntent, validateLocalArtifact } from './local-artifact-finalization.js';
|
|
19
|
-
import {
|
|
19
|
+
import { writeArtifactRecoveryIntent, readArtifactRecoveryIntent } from './artifact-repair-intent.js';
|
|
20
|
+
import { reconcileArtifactRecovery, recoveryContextFromIntent, sha256Content } from './artifact-recovery.js';
|
|
20
21
|
import { buildLifecycleFacts, canStart } from './capabilities.js';
|
|
21
22
|
import { createInvalidationOperation, invalidationMutation, parseInvalidationDocument, targetIdFor, upsertInvalidation } from './invalidation.js';
|
|
22
23
|
import { consumeReworkIntents, parseReworkIntentDocument, reworkIntentMutation, supersedeReworkIntents } from './rework-intent.js';
|
|
23
24
|
import { ARTIFACT_FAMILIES, expectedQualificationRelations, parseQualificationAudit, parseTaskQualification, upstreamArtifactDigest, validateQualificationAudit } from './qualification-audit.js';
|
|
24
25
|
import { getArtifactSchema } from './artifact-schema.js';
|
|
25
|
-
import { inspectArtifactContract } from './artifact-operations.js';
|
|
26
|
+
import { canonicalSemanticDigest, inspectArtifactContract } from './artifact-operations.js';
|
|
26
27
|
import { consumeLifecycleRecoveryAttestation, lifecycleRecoveryAttestationDigest, validateLifecycleRecoveryAttestation } from './control-authority.js';
|
|
27
28
|
import { readManualValidationCompletion } from './manual-validation-completion.js';
|
|
28
29
|
const eventCatalog = [
|
|
@@ -314,6 +315,59 @@ function openStartedIdentity(rows, family) {
|
|
|
314
315
|
function reviewInputFamily(family) {
|
|
315
316
|
return family === 'review-analysis' ? 'analysis' : family === 'review-plan' ? 'plan' : 'code';
|
|
316
317
|
}
|
|
318
|
+
function reconcileReviewCompletionReplay(repoRoot, taskDir, taskId, family, artifact, round) {
|
|
319
|
+
const stage = REVIEW_LEDGER_STAGES[family];
|
|
320
|
+
if (!stage)
|
|
321
|
+
return null;
|
|
322
|
+
const reviewFamily = family;
|
|
323
|
+
try {
|
|
324
|
+
let intent = readArtifactRecoveryIntent(repoRoot, taskId, reviewFamily, artifact);
|
|
325
|
+
if (!intent)
|
|
326
|
+
return null;
|
|
327
|
+
if (intent.state === 'commit-started') {
|
|
328
|
+
const reconciled = reconcileArtifactRecovery(recoveryContextFromIntent(repoRoot, taskDir, intent), { lockAlreadyHeld: true });
|
|
329
|
+
if (reconciled.status !== 'passed' && reconciled.status !== 'consumed') {
|
|
330
|
+
return {
|
|
331
|
+
code: 'EVENT_ARTIFACT_CONFLICT',
|
|
332
|
+
message: `review finalizer recovery remains ${reconciled.status} for ${artifact}`
|
|
333
|
+
};
|
|
334
|
+
}
|
|
335
|
+
intent = reconciled.intent;
|
|
336
|
+
}
|
|
337
|
+
if (!['passed', 'consumed'].includes(intent.state)) {
|
|
338
|
+
return {
|
|
339
|
+
code: 'EVENT_ARTIFACT_CONFLICT',
|
|
340
|
+
message: `review finalizer provenance is incomplete for ${artifact}`
|
|
341
|
+
};
|
|
342
|
+
}
|
|
343
|
+
const validated = validateCompletedArtifact(taskDir, FAMILY[family].artifact, artifact, round);
|
|
344
|
+
if (!validated.ok) {
|
|
345
|
+
return { code: 'EVENT_ARTIFACT_CONFLICT', message: validated.error.message };
|
|
346
|
+
}
|
|
347
|
+
const content = fs.readFileSync(validated.artifact.path, 'utf8');
|
|
348
|
+
const expectedRequestId = `review-finalize:${taskId}:${stage}:${round}`;
|
|
349
|
+
if (intent.round !== round
|
|
350
|
+
|| intent.requestId !== expectedRequestId
|
|
351
|
+
|| intent.finalArtifactSha256 !== sha256Content(content)
|
|
352
|
+
|| intent.finalSemanticDigest !== canonicalSemanticDigest(content)) {
|
|
353
|
+
return {
|
|
354
|
+
code: 'EVENT_ARTIFACT_CONFLICT',
|
|
355
|
+
message: `review finalizer provenance does not match ${artifact}`
|
|
356
|
+
};
|
|
357
|
+
}
|
|
358
|
+
if (intent.state === 'passed') {
|
|
359
|
+
const consumed = { ...intent, state: 'consumed', updatedAt: Date.now() };
|
|
360
|
+
writeArtifactRecoveryIntent(repoRoot, consumed, { expected: intent });
|
|
361
|
+
}
|
|
362
|
+
return null;
|
|
363
|
+
}
|
|
364
|
+
catch (error) {
|
|
365
|
+
return {
|
|
366
|
+
code: 'EVENT_ARTIFACT_CONFLICT',
|
|
367
|
+
message: `review finalizer recovery could not be reconciled for ${artifact}: ${error instanceof Error ? error.message : String(error)}`
|
|
368
|
+
};
|
|
369
|
+
}
|
|
370
|
+
}
|
|
317
371
|
function relationForStartedInput(family, input) {
|
|
318
372
|
if (family.startsWith('review-')) {
|
|
319
373
|
return input.family === reviewInputFamily(family) ? 'reviewed-input' : 'approval-context';
|
|
@@ -715,6 +769,8 @@ function applyTaskEventUnlocked(request, options = {}) {
|
|
|
715
769
|
const row = manual ? openRows.at(-1) : matchingRows[0];
|
|
716
770
|
let completedArtifact = null;
|
|
717
771
|
let localFinalizationIntent = null;
|
|
772
|
+
let reviewFinalizationIntent = null;
|
|
773
|
+
let reviewContent = null;
|
|
718
774
|
if (eventIdentity.phase === 'started' && row) {
|
|
719
775
|
if (row.agent !== normalized.agent)
|
|
720
776
|
return failed(normalized, { code: 'EVENT_LOG_CONFLICT', message: 'open started event has a different agent' }, { taskId: resolved.taskId, taskMdPath: resolved.taskMdPath });
|
|
@@ -729,8 +785,14 @@ function applyTaskEventUnlocked(request, options = {}) {
|
|
|
729
785
|
if (eventIdentity.phase === 'started' && !manual && completedRows.length > 0)
|
|
730
786
|
return failed(normalized, { code: 'EVENT_ALREADY_COMPLETED', message: 'event identity is already completed' }, { taskId: resolved.taskId, taskMdPath: resolved.taskMdPath });
|
|
731
787
|
const done = completedRows.find((item) => item.note === eventIdentity.note);
|
|
732
|
-
if (eventIdentity.phase === 'completed' && done)
|
|
788
|
+
if (eventIdentity.phase === 'completed' && done) {
|
|
789
|
+
if (!normalized.dryRun && eventIdentity.family.startsWith('review-') && normalized.artifact && normalized.round) {
|
|
790
|
+
const recoveryError = reconcileReviewCompletionReplay(resolved.repoRoot, resolved.taskDir, resolved.taskId, eventIdentity.family, normalized.artifact, normalized.round);
|
|
791
|
+
if (recoveryError)
|
|
792
|
+
return failed(normalized, recoveryError, { taskId: resolved.taskId, taskMdPath: resolved.taskMdPath, fromStep: currentStep, toStep: currentStep, action: eventIdentity.action, phase: eventIdentity.phase });
|
|
793
|
+
}
|
|
733
794
|
return successNoOp(normalized, resolved.taskId, resolved.taskMdPath, currentStep, eventIdentity, done.done, frontmatter, artifactContext);
|
|
795
|
+
}
|
|
734
796
|
if (eventIdentity.phase === 'completed' && completedRows.length > 0 && openRows.length === 0)
|
|
735
797
|
return failed(normalized, { code: 'EVENT_LOG_CONFLICT', message: 'event identity is already completed with a different payload' }, { taskId: resolved.taskId, taskMdPath: resolved.taskMdPath, fromStep: currentStep, toStep: currentStep, action: eventIdentity.action, phase: eventIdentity.phase });
|
|
736
798
|
if (eventIdentity.phase === 'completed' && !row?.started && !allowsManualOverride(options.manualOverride, 'task-event', 'EVENT_START_MISSING'))
|
|
@@ -746,7 +808,6 @@ function applyTaskEventUnlocked(request, options = {}) {
|
|
|
746
808
|
return failed(normalized, validationError, { taskId: resolved.taskId, taskMdPath: resolved.taskMdPath, fromStep: currentStep, toStep: currentStep, action: eventIdentity.action, phase: eventIdentity.phase });
|
|
747
809
|
}
|
|
748
810
|
if (eventIdentity.family.startsWith('review-')) {
|
|
749
|
-
let reviewContent;
|
|
750
811
|
try {
|
|
751
812
|
reviewContent = fs.readFileSync(completedArtifact.path, 'utf8');
|
|
752
813
|
}
|
|
@@ -775,6 +836,20 @@ function applyTaskEventUnlocked(request, options = {}) {
|
|
|
775
836
|
const localFamily = eventIdentity.family === 'analyze'
|
|
776
837
|
? 'analysis'
|
|
777
838
|
: eventIdentity.family === 'plan' ? 'plan' : 'code';
|
|
839
|
+
try {
|
|
840
|
+
localFinalizationIntent = readLocalArtifactFinalizationIntent(resolved.repoRoot, resolved.taskId, localFamily, completedArtifact.name);
|
|
841
|
+
if (localFinalizationIntent?.state === 'commit-started') {
|
|
842
|
+
const reconciled = reconcileArtifactRecovery(recoveryContextFromIntent(resolved.repoRoot, resolved.taskDir, localFinalizationIntent), { lockAlreadyHeld: true });
|
|
843
|
+
if (reconciled.status === 'passed' || reconciled.status === 'consumed')
|
|
844
|
+
localFinalizationIntent = reconciled.intent;
|
|
845
|
+
}
|
|
846
|
+
}
|
|
847
|
+
catch (error) {
|
|
848
|
+
return failed(normalized, {
|
|
849
|
+
code: 'EVENT_ARTIFACT_CONFLICT',
|
|
850
|
+
message: `local finalizer recovery could not be reconciled for ${completedArtifact.name}: ${error instanceof Error ? error.message : String(error)}`
|
|
851
|
+
}, { taskId: resolved.taskId, taskMdPath: resolved.taskMdPath, fromStep: currentStep, toStep: currentStep, action: eventIdentity.action, phase: eventIdentity.phase });
|
|
852
|
+
}
|
|
778
853
|
let artifactContent;
|
|
779
854
|
try {
|
|
780
855
|
artifactContent = fs.readFileSync(completedArtifact.path, 'utf8');
|
|
@@ -806,33 +881,23 @@ function applyTaskEventUnlocked(request, options = {}) {
|
|
|
806
881
|
message: `semantic digest does not match finalizer result for ${completedArtifact.name}`
|
|
807
882
|
}, { taskId: resolved.taskId, taskMdPath: resolved.taskMdPath, fromStep: currentStep, toStep: currentStep, action: eventIdentity.action, phase: eventIdentity.phase });
|
|
808
883
|
}
|
|
809
|
-
|
|
810
|
-
localFinalizationIntent = readLocalArtifactFinalizationIntent(resolved.repoRoot, resolved.taskId, localFamily, completedArtifact.name);
|
|
811
|
-
}
|
|
812
|
-
catch (error) {
|
|
813
|
-
return failed(normalized, {
|
|
814
|
-
code: 'EVENT_ARTIFACT_CONFLICT',
|
|
815
|
-
message: `cannot read local finalizer provenance for ${completedArtifact.name}: ${error instanceof Error ? error.message : String(error)}`
|
|
816
|
-
}, { taskId: resolved.taskId, taskMdPath: resolved.taskMdPath, fromStep: currentStep, toStep: currentStep, action: eventIdentity.action, phase: eventIdentity.phase });
|
|
817
|
-
}
|
|
818
|
-
if (!localFinalizationIntent || !['passed', 'consumed', 'finalize-ready', 'commit-started'].includes(localFinalizationIntent.state)) {
|
|
884
|
+
if (!localFinalizationIntent || !['passed', 'consumed'].includes(localFinalizationIntent.state)) {
|
|
819
885
|
return failed(normalized, {
|
|
820
886
|
code: 'EVENT_ARTIFACT_CONFLICT',
|
|
821
887
|
message: `local finalizer provenance is missing or incomplete for ${completedArtifact.name}`
|
|
822
888
|
}, { taskId: resolved.taskId, taskMdPath: resolved.taskMdPath, fromStep: currentStep, toStep: currentStep, action: eventIdentity.action, phase: eventIdentity.phase });
|
|
823
889
|
}
|
|
824
|
-
if (localFinalizationIntent.
|
|
890
|
+
if (localFinalizationIntent.finalArtifactSha256 !== actualSha256 || localFinalizationIntent.finalSemanticDigest !== local.semanticDigest) {
|
|
825
891
|
return failed(normalized, {
|
|
826
892
|
code: 'EVENT_ARTIFACT_CONFLICT',
|
|
827
893
|
message: `local finalizer provenance does not match ${completedArtifact.name}`
|
|
828
894
|
}, { taskId: resolved.taskId, taskMdPath: resolved.taskMdPath, fromStep: currentStep, toStep: currentStep, action: eventIdentity.action, phase: eventIdentity.phase });
|
|
829
895
|
}
|
|
830
|
-
if (localFinalizationIntent.
|
|
896
|
+
if (localFinalizationIntent.authorityDigest !== null) {
|
|
831
897
|
if (!lifecycleAuthority
|
|
832
898
|
|| localFinalizationIntent.recoveryOperationId !== lifecycleAuthority.operationId
|
|
833
899
|
|| localFinalizationIntent.requestId !== lifecycleAuthority.lifecycleRequestId
|
|
834
|
-
||
|
|
835
|
-
&& localFinalizationIntent.authorityDigest !== lifecycleRecoveryAttestationDigest(lifecycleAuthority))) {
|
|
900
|
+
|| localFinalizationIntent.authorityDigest !== lifecycleRecoveryAttestationDigest(lifecycleAuthority)) {
|
|
836
901
|
return failed(normalized, {
|
|
837
902
|
code: 'EVENT_ARTIFACT_CONFLICT',
|
|
838
903
|
message: `local finalizer provenance does not match lifecycle completion authority for ${completedArtifact.name}`
|
|
@@ -862,6 +927,43 @@ function applyTaskEventUnlocked(request, options = {}) {
|
|
|
862
927
|
const findingCountError = validateReviewFindingCounts(normalized, content, eventIdentity.family, completedArtifact?.path ?? null);
|
|
863
928
|
if (findingCountError)
|
|
864
929
|
return failed(normalized, findingCountError, { taskId: resolved.taskId, taskMdPath: resolved.taskMdPath, fromStep: currentStep, toStep: currentStep, action: eventIdentity.action, phase: eventIdentity.phase });
|
|
930
|
+
if (eventIdentity.phase === 'completed' && eventIdentity.family.startsWith('review-') && completedArtifact && reviewContent !== null) {
|
|
931
|
+
try {
|
|
932
|
+
reviewFinalizationIntent = readArtifactRecoveryIntent(resolved.repoRoot, resolved.taskId, eventIdentity.family, completedArtifact.name);
|
|
933
|
+
if (!normalized.dryRun && reviewFinalizationIntent?.state === 'commit-started') {
|
|
934
|
+
const reconciled = reconcileArtifactRecovery(recoveryContextFromIntent(resolved.repoRoot, resolved.taskDir, reviewFinalizationIntent), { lockAlreadyHeld: true });
|
|
935
|
+
if (reconciled.status === 'passed' || reconciled.status === 'consumed')
|
|
936
|
+
reviewFinalizationIntent = reconciled.intent;
|
|
937
|
+
}
|
|
938
|
+
if (!reviewFinalizationIntent || !['passed', 'consumed'].includes(reviewFinalizationIntent.state)) {
|
|
939
|
+
return failed(normalized, {
|
|
940
|
+
code: 'EVENT_ARTIFACT_CONFLICT',
|
|
941
|
+
message: `review finalizer provenance is missing or incomplete for ${completedArtifact.name}`
|
|
942
|
+
}, { taskId: resolved.taskId, taskMdPath: resolved.taskMdPath, fromStep: currentStep, toStep: currentStep, action: eventIdentity.action, phase: eventIdentity.phase });
|
|
943
|
+
}
|
|
944
|
+
const reviewStage = eventIdentity.family === 'review-analysis'
|
|
945
|
+
? 'analysis'
|
|
946
|
+
: eventIdentity.family === 'review-plan' ? 'plan' : 'code';
|
|
947
|
+
const actualSha256 = sha256File(completedArtifact.path);
|
|
948
|
+
const actualSemanticDigest = canonicalSemanticDigest(reviewContent);
|
|
949
|
+
const expectedRequestId = `review-finalize:${resolved.taskId}:${reviewStage}:${normalized.round}`;
|
|
950
|
+
if (reviewFinalizationIntent.round !== normalized.round
|
|
951
|
+
|| reviewFinalizationIntent.requestId !== expectedRequestId
|
|
952
|
+
|| reviewFinalizationIntent.finalArtifactSha256 !== actualSha256
|
|
953
|
+
|| reviewFinalizationIntent.finalSemanticDigest !== actualSemanticDigest) {
|
|
954
|
+
return failed(normalized, {
|
|
955
|
+
code: 'EVENT_ARTIFACT_CONFLICT',
|
|
956
|
+
message: `review finalizer provenance does not match ${completedArtifact.name}`
|
|
957
|
+
}, { taskId: resolved.taskId, taskMdPath: resolved.taskMdPath, fromStep: currentStep, toStep: currentStep, action: eventIdentity.action, phase: eventIdentity.phase });
|
|
958
|
+
}
|
|
959
|
+
}
|
|
960
|
+
catch (error) {
|
|
961
|
+
return failed(normalized, {
|
|
962
|
+
code: 'EVENT_ARTIFACT_CONFLICT',
|
|
963
|
+
message: `review finalizer recovery could not be reconciled for ${completedArtifact.name}: ${error instanceof Error ? error.message : String(error)}`
|
|
964
|
+
}, { taskId: resolved.taskId, taskMdPath: resolved.taskMdPath, fromStep: currentStep, toStep: currentStep, action: eventIdentity.action, phase: eventIdentity.phase });
|
|
965
|
+
}
|
|
966
|
+
}
|
|
865
967
|
let orchestrationCompletion = null;
|
|
866
968
|
if (eventIdentity.phase === 'completed' && eventIdentity.family !== 'manual-validation' && eventIdentity.family !== 'validation-run') {
|
|
867
969
|
const orchestrationStage = eventIdentity.family === 'analyze' ? 'analysis' : eventIdentity.family;
|
|
@@ -1010,10 +1112,10 @@ function applyTaskEventUnlocked(request, options = {}) {
|
|
|
1010
1112
|
requestId: lifecycleAuthority.lifecycleRequestId,
|
|
1011
1113
|
updatedAt: Date.now()
|
|
1012
1114
|
};
|
|
1013
|
-
|
|
1115
|
+
writeArtifactRecoveryIntent(resolved.repoRoot, localFinalizationIntent, { expected: expectedIntent });
|
|
1014
1116
|
}
|
|
1015
1117
|
else if (!lifecycleAuthority && localFinalizationIntent.state === 'passed') {
|
|
1016
|
-
localFinalizationIntent = consumeLocalArtifactFinalizationIntent(resolved.repoRoot, localFinalizationIntent);
|
|
1118
|
+
localFinalizationIntent = consumeLocalArtifactFinalizationIntent(resolved.repoRoot, localFinalizationIntent, { lockAlreadyHeld: options.lockAlreadyHeld });
|
|
1017
1119
|
}
|
|
1018
1120
|
}
|
|
1019
1121
|
catch (error) {
|
|
@@ -1031,6 +1133,32 @@ function applyTaskEventUnlocked(request, options = {}) {
|
|
|
1031
1133
|
});
|
|
1032
1134
|
}
|
|
1033
1135
|
}
|
|
1136
|
+
if (!normalized.dryRun && reviewFinalizationIntent && completedArtifact && reviewFinalizationIntent.state === 'passed') {
|
|
1137
|
+
try {
|
|
1138
|
+
const expectedIntent = reviewFinalizationIntent;
|
|
1139
|
+
reviewFinalizationIntent = {
|
|
1140
|
+
...reviewFinalizationIntent,
|
|
1141
|
+
state: 'commit-started',
|
|
1142
|
+
phase: 'task-event.completed',
|
|
1143
|
+
updatedAt: Date.now()
|
|
1144
|
+
};
|
|
1145
|
+
writeArtifactRecoveryIntent(resolved.repoRoot, reviewFinalizationIntent, { expected: expectedIntent });
|
|
1146
|
+
}
|
|
1147
|
+
catch (error) {
|
|
1148
|
+
return failed(normalized, {
|
|
1149
|
+
code: 'EVENT_ARTIFACT_CONFLICT',
|
|
1150
|
+
message: `review finalizer provenance could not be advanced before task write for ${completedArtifact.name}: ${error instanceof Error ? error.message : String(error)}`
|
|
1151
|
+
}, {
|
|
1152
|
+
taskId: resolved.taskId,
|
|
1153
|
+
taskMdPath: resolved.taskMdPath,
|
|
1154
|
+
fromStep: currentStep,
|
|
1155
|
+
toStep: step,
|
|
1156
|
+
action: eventIdentity.action,
|
|
1157
|
+
phase: eventIdentity.phase,
|
|
1158
|
+
artifactContext
|
|
1159
|
+
});
|
|
1160
|
+
}
|
|
1161
|
+
}
|
|
1034
1162
|
const sourceCompletion = eventIdentity.phase === 'completed'
|
|
1035
1163
|
&& ['analyze', 'plan', 'code'].includes(eventIdentity.family);
|
|
1036
1164
|
const result = writeTask({ taskRef: normalized.taskRef, expectedState: stateOverride ? resolved.state : 'active', dryRun: normalized.dryRun, mutations }, { ...options, invalidationContext: sourceCompletion ? 'source-completion' : 'standard', metadataProvider: () => metadata });
|
|
@@ -1040,7 +1168,7 @@ function applyTaskEventUnlocked(request, options = {}) {
|
|
|
1040
1168
|
try {
|
|
1041
1169
|
if (!options.deferLifecycleRecoveryConsumption)
|
|
1042
1170
|
consumeLifecycleRecoveryAttestation(lifecycleAuthority);
|
|
1043
|
-
|
|
1171
|
+
writeArtifactRecoveryIntent(resolved.repoRoot, { ...localFinalizationIntent, state: 'consumed', updatedAt: Date.now() }, { expected: localFinalizationIntent });
|
|
1044
1172
|
}
|
|
1045
1173
|
catch (error) {
|
|
1046
1174
|
return failed(normalized, {
|
|
@@ -1049,6 +1177,20 @@ function applyTaskEventUnlocked(request, options = {}) {
|
|
|
1049
1177
|
}, { taskId: result.taskId, taskMdPath: result.taskMdPath, fromStep: currentStep, toStep: step, action: eventIdentity.action, phase: eventIdentity.phase, timestamp: result.timestamp, agentInfraVersion: result.agentInfraVersion, operations: result.operations, artifactContext });
|
|
1050
1178
|
}
|
|
1051
1179
|
}
|
|
1180
|
+
if (!normalized.dryRun && reviewFinalizationIntent && completedArtifact) {
|
|
1181
|
+
try {
|
|
1182
|
+
if (reviewFinalizationIntent.state === 'commit-started') {
|
|
1183
|
+
const consumed = { ...reviewFinalizationIntent, state: 'consumed', updatedAt: Date.now() };
|
|
1184
|
+
writeArtifactRecoveryIntent(resolved.repoRoot, consumed, { expected: reviewFinalizationIntent });
|
|
1185
|
+
}
|
|
1186
|
+
}
|
|
1187
|
+
catch (error) {
|
|
1188
|
+
return failed(normalized, {
|
|
1189
|
+
code: 'EVENT_ARTIFACT_CONFLICT',
|
|
1190
|
+
message: `review finalizer recovery consumption is incomplete: ${error instanceof Error ? error.message : String(error)}`
|
|
1191
|
+
}, { taskId: result.taskId, taskMdPath: result.taskMdPath, fromStep: currentStep, toStep: step, action: eventIdentity.action, phase: eventIdentity.phase, timestamp: result.timestamp, agentInfraVersion: result.agentInfraVersion, operations: result.operations, artifactContext });
|
|
1192
|
+
}
|
|
1193
|
+
}
|
|
1052
1194
|
if (!normalized.dryRun && orchestrationCompletion) {
|
|
1053
1195
|
try {
|
|
1054
1196
|
(options.commitOrchestrationCompletion ?? commitOrchestrationStageCompletion)(orchestrationCompletion);
|
|
@@ -1095,7 +1237,7 @@ function applyTaskEvent(request, options = {}) {
|
|
|
1095
1237
|
if (!resolved.ok)
|
|
1096
1238
|
return applyTaskEventUnlocked(request, options);
|
|
1097
1239
|
try {
|
|
1098
|
-
return withTaskExecutionLock(resolved.repoRoot, resolved.taskId, `task-event.${request.event}`, () => applyTaskEventUnlocked(request, options));
|
|
1240
|
+
return withTaskExecutionLock(resolved.repoRoot, resolved.taskId, `task-event.${request.event}`, () => applyTaskEventUnlocked(request, { ...options, lockAlreadyHeld: true }));
|
|
1099
1241
|
}
|
|
1100
1242
|
catch (error) {
|
|
1101
1243
|
if (!(error instanceof TaskExecutionLockError))
|