@fitlab-ai/agent-infra 0.9.15 → 0.11.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/README.md +2 -6
- package/README.zh-CN.md +2 -6
- package/bin/internal-cli.sh +0 -13
- package/dist/bin/internal-cli.js +11 -49
- 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 +3 -4
- package/dist/lib/internal/manual-validation.js +17 -8
- package/dist/lib/internal/platform-comment.js +60 -2
- package/dist/lib/internal/task-artifact.js +1 -1
- package/dist/lib/internal/task-finalization.js +2 -2
- package/dist/lib/internal/task-lifecycle.js +4 -4
- package/dist/lib/internal/task-operation-registry.js +22 -13
- package/dist/lib/internal/task-orchestration.js +2 -2
- package/dist/lib/internal/task-review.js +12 -7
- package/dist/lib/platform/activity-recovery.js +159 -0
- package/dist/lib/platform/issue-comments.js +89 -31
- 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-comment-projection.js +70 -0
- package/dist/lib/platform/task-identities.js +2 -9
- package/dist/lib/platform/task-recovery.js +55 -0
- package/dist/lib/platform/verification-sync.js +45 -7
- package/dist/lib/pr-review/evidence-grading.js +4 -8
- package/dist/lib/sandbox/commands/rm.js +21 -2
- package/dist/lib/sandbox/control/executor.js +4 -2
- package/dist/lib/sandbox/control/server.js +29 -9
- package/dist/lib/sandbox/control/state.js +7 -1
- package/dist/lib/sandbox/control/task-workflow.js +1 -1
- package/dist/lib/sandbox/control/workflow-executor.js +87 -35
- 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/server/process-state.js +17 -5
- package/dist/lib/task/artifact-command.js +11 -30
- package/dist/lib/task/artifact-operations.js +17 -231
- package/dist/lib/task/artifact-recovery.js +476 -0
- package/dist/lib/task/artifact-repair-intent.js +90 -39
- package/dist/lib/task/commit-operation.js +31 -4
- package/dist/lib/task/control-authority.js +39 -8
- 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 +165 -23
- package/dist/lib/task/human-override.js +5 -8
- package/dist/lib/task/intermediate-cleanup.js +91 -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 +212 -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 +3 -2
- package/dist/lib/task/review-finalization.js +294 -74
- package/dist/lib/{host-control → task}/secure-fs.js +8 -19
- 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 +2 -1
- package/dist/lib/task/workflow-dispatch.js +161 -0
- 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 +3 -4
- package/lib/internal/manual-validation.ts +17 -8
- package/lib/internal/platform-comment.ts +53 -2
- package/lib/internal/task-artifact.ts +1 -1
- package/lib/internal/task-finalization.ts +1 -1
- package/lib/internal/task-lifecycle.ts +5 -4
- package/lib/internal/task-operation-registry.ts +22 -13
- package/lib/internal/task-orchestration.ts +1 -1
- package/lib/internal/task-review.ts +11 -7
- package/lib/platform/activity-recovery.ts +179 -0
- package/lib/platform/issue-comments.ts +105 -30
- 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-comment-projection.ts +79 -0
- package/lib/platform/task-identities.ts +3 -14
- package/lib/platform/task-recovery.ts +54 -0
- package/lib/platform/verification-sync.ts +48 -11
- package/lib/pr-review/evidence-grading.ts +4 -9
- package/lib/sandbox/commands/rm.ts +34 -2
- package/lib/sandbox/control/executor.ts +4 -2
- package/lib/sandbox/control/protocol.ts +6 -1
- package/lib/sandbox/control/server.ts +30 -10
- package/lib/sandbox/control/state.ts +9 -1
- package/lib/sandbox/control/task-workflow.ts +1 -1
- package/lib/sandbox/control/workflow-executor.ts +106 -35
- package/lib/sandbox/index.ts +1 -2
- package/lib/sandbox/removal.ts +126 -1128
- package/lib/sandbox/workspace-identity.ts +19 -3
- package/lib/server/process-state.ts +26 -5
- package/lib/task/artifact-command.ts +12 -29
- package/lib/task/artifact-operations.ts +22 -252
- package/lib/task/artifact-recovery.ts +572 -0
- package/lib/task/artifact-repair-intent.ts +126 -51
- package/lib/task/commit-operation.ts +35 -4
- package/lib/task/control-authority.ts +48 -15
- 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 +188 -24
- package/lib/task/human-override.ts +5 -8
- package/lib/task/intermediate-cleanup.ts +66 -12
- package/lib/task/lifecycle-recovery.ts +691 -0
- package/lib/task/lifecycle.ts +13 -4
- package/lib/task/local-artifact-finalization.ts +249 -131
- 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 +2 -1
- package/lib/task/review-finalization.ts +298 -84
- package/lib/{host-control → task}/secure-fs.ts +10 -32
- 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 +2 -1
- package/lib/task/workflow-dispatch.ts +171 -0
- 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 +12 -2
- package/templates/.agents/rules/issue-sync.zh-CN.md +13 -5
- package/templates/.agents/rules/local-artifact-repair.en.md +47 -39
- package/templates/.agents/rules/local-artifact-repair.zh-CN.md +45 -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 +8 -8
- 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 +11 -6
- package/templates/.agents/skills/restore-task/SKILL.zh-CN.md +11 -6
- package/templates/.agents/skills/restore-task/config/verify.json +1 -1
- package/templates/.agents/skills/review-analysis/SKILL.en.md +3 -3
- package/templates/.agents/skills/review-analysis/SKILL.zh-CN.md +3 -3
- 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 +5 -4
- package/templates/.agents/skills/review-code/SKILL.zh-CN.md +5 -4
- 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 +3 -3
- package/templates/.agents/skills/review-plan/SKILL.zh-CN.md +3 -3
- 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
- package/dist/lib/host-control/audit.js +0 -19
- package/dist/lib/host-control/client.js +0 -142
- package/dist/lib/host-control/command.js +0 -49
- package/dist/lib/host-control/path.js +0 -160
- package/dist/lib/host-control/server.js +0 -159
- package/dist/lib/host-control/service.js +0 -112
- package/dist/lib/internal/host-control.js +0 -28
- package/lib/host-control/audit.ts +0 -22
- package/lib/host-control/client.ts +0 -172
- package/lib/host-control/command.ts +0 -60
- package/lib/host-control/path.ts +0 -167
- package/lib/host-control/server.ts +0 -180
- package/lib/host-control/service.ts +0 -133
- package/lib/internal/host-control.ts +0 -28
|
@@ -186,7 +186,8 @@ function isDelegationHostEvidence(value: unknown): value is DelegationHostEviden
|
|
|
186
186
|
function hasCurrentCodexEvidence(receipt: DelegationReceipt): boolean {
|
|
187
187
|
const provenance = receipt.lifecycleProvenance;
|
|
188
188
|
if (!provenance) return false;
|
|
189
|
-
const activated = ['activated', 'stage-completed', 'sealed', 'consumed'].includes(receipt.status)
|
|
189
|
+
const activated = ['activated', 'stage-completed', 'sealed', 'consumed'].includes(receipt.status)
|
|
190
|
+
|| (receipt.status === 'aborted' && receipt.activatedAt !== null);
|
|
190
191
|
if (!activated) return receipt.hostEvidence === null;
|
|
191
192
|
const host = receipt.hostEvidence;
|
|
192
193
|
if (
|
|
@@ -219,6 +220,12 @@ function hasCurrentCodexEvidence(receipt: DelegationReceipt): boolean {
|
|
|
219
220
|
&& host.consumer === receipt.id
|
|
220
221
|
&& exactText(host.consumedAt);
|
|
221
222
|
}
|
|
223
|
+
if (receipt.status === 'aborted') {
|
|
224
|
+
return Number.isSafeInteger(host.stopRevision)
|
|
225
|
+
&& (host.stopRevision as number) > host.startRevision
|
|
226
|
+
&& host.consumer === `lifecycle-recovery:${receipt.taskId}:${receipt.id}`
|
|
227
|
+
&& exactText(host.consumedAt);
|
|
228
|
+
}
|
|
222
229
|
return host.stopRevision === null && host.consumer === null && host.consumedAt === null;
|
|
223
230
|
}
|
|
224
231
|
|
|
@@ -244,7 +251,8 @@ function hasStatusBoundEvidence(receipt: DelegationReceipt): boolean {
|
|
|
244
251
|
const dispatchComplete = dispatchFields.every((field) => field !== null);
|
|
245
252
|
if (!dispatchEmpty && !dispatchComplete) return false;
|
|
246
253
|
|
|
247
|
-
const beforeActivation = ['prepared', '
|
|
254
|
+
const beforeActivation = ['prepared', 'expired'].includes(receipt.status)
|
|
255
|
+
|| (receipt.status === 'aborted' && receipt.activatedAt === null);
|
|
248
256
|
if (beforeActivation) {
|
|
249
257
|
return receipt.parentId === null
|
|
250
258
|
&& receipt.childId === null
|
|
@@ -282,6 +290,21 @@ function hasStatusBoundEvidence(receipt: DelegationReceipt): boolean {
|
|
|
282
290
|
&& receipt.consumedAt === null;
|
|
283
291
|
}
|
|
284
292
|
|
|
293
|
+
if (receipt.status === 'aborted') {
|
|
294
|
+
const host = receipt.hostEvidence;
|
|
295
|
+
return receipt.agent === null
|
|
296
|
+
&& receipt.afterFingerprint === null
|
|
297
|
+
&& receipt.changedPaths.length === 0
|
|
298
|
+
&& receipt.sealedAt === null
|
|
299
|
+
&& receipt.consumedAt === null
|
|
300
|
+
&& host !== null
|
|
301
|
+
&& host !== undefined
|
|
302
|
+
&& Number.isSafeInteger(host.stopRevision)
|
|
303
|
+
&& (host.stopRevision as number) > host.startRevision
|
|
304
|
+
&& host.consumer === `lifecycle-recovery:${receipt.taskId}:${receipt.id}`
|
|
305
|
+
&& exactText(host.consumedAt);
|
|
306
|
+
}
|
|
307
|
+
|
|
285
308
|
if (
|
|
286
309
|
!exactText(receipt.agent)
|
|
287
310
|
|| normalizeAgentToken(receipt.agent) !== normalizeAgentToken(receipt.client)
|
|
@@ -584,6 +607,53 @@ function abortPreparedDelegation(receipt: DelegationReceipt): ReceiptResult {
|
|
|
584
607
|
};
|
|
585
608
|
}
|
|
586
609
|
|
|
610
|
+
function abortActivatedDelegation(
|
|
611
|
+
receipt: DelegationReceipt,
|
|
612
|
+
event: Readonly<{ childId: string; stopRevision: number; consumer: string; consumedAt: string }>
|
|
613
|
+
): ReceiptResult {
|
|
614
|
+
if (receipt.status !== 'activated') {
|
|
615
|
+
return fail('DELEGATION_STATE_INVALID', `delegation ${receipt.id} is ${receipt.status}, expected activated`);
|
|
616
|
+
}
|
|
617
|
+
if (
|
|
618
|
+
receipt.client !== 'codex'
|
|
619
|
+
|| !receipt.lifecycleProvenance
|
|
620
|
+
|| receipt.hostEvidence?.kind !== 'codex-lifecycle-v2'
|
|
621
|
+
|| typeof receipt.lifecycleProvenance.controllerInstanceDigest !== 'string'
|
|
622
|
+
|| typeof receipt.lifecycleProvenance.controlGeneration !== 'string'
|
|
623
|
+
|| typeof receipt.hostEvidence.controllerInstanceDigest !== 'string'
|
|
624
|
+
|| typeof receipt.hostEvidence.controlGeneration !== 'string'
|
|
625
|
+
) {
|
|
626
|
+
return fail('DELEGATION_RECOVERY_EVIDENCE_INVALID', 'activated delegation lacks the current Codex controller binding');
|
|
627
|
+
}
|
|
628
|
+
if (
|
|
629
|
+
event.childId !== receipt.childId
|
|
630
|
+
|| !Number.isSafeInteger(event.stopRevision)
|
|
631
|
+
|| event.stopRevision <= receipt.hostEvidence.startRevision
|
|
632
|
+
|| event.consumer !== `lifecycle-recovery:${receipt.taskId}:${receipt.id}`
|
|
633
|
+
|| !exactText(event.consumedAt)
|
|
634
|
+
) {
|
|
635
|
+
return fail('DELEGATION_RECOVERY_EVIDENCE_INVALID', 'recovery stop evidence does not match the activated delegation');
|
|
636
|
+
}
|
|
637
|
+
return {
|
|
638
|
+
ok: true,
|
|
639
|
+
receipt: Object.freeze({
|
|
640
|
+
...receipt,
|
|
641
|
+
status: 'aborted' as const,
|
|
642
|
+
agent: null,
|
|
643
|
+
afterFingerprint: null,
|
|
644
|
+
changedPaths: Object.freeze([]),
|
|
645
|
+
sealedAt: null,
|
|
646
|
+
consumedAt: null,
|
|
647
|
+
hostEvidence: Object.freeze({
|
|
648
|
+
...receipt.hostEvidence,
|
|
649
|
+
stopRevision: event.stopRevision,
|
|
650
|
+
consumer: event.consumer,
|
|
651
|
+
consumedAt: event.consumedAt
|
|
652
|
+
})
|
|
653
|
+
})
|
|
654
|
+
};
|
|
655
|
+
}
|
|
656
|
+
|
|
587
657
|
function completeDelegationStage(
|
|
588
658
|
receipt: DelegationReceipt,
|
|
589
659
|
event: Readonly<{ stage: DelegationStage; round: number; artifact: string; agent: string }>
|
|
@@ -666,6 +736,7 @@ function consumeDelegation(receipt: DelegationReceipt, options: { now?: () => st
|
|
|
666
736
|
|
|
667
737
|
export {
|
|
668
738
|
activateDelegation,
|
|
739
|
+
abortActivatedDelegation,
|
|
669
740
|
abortPreparedDelegation,
|
|
670
741
|
completeDelegationStage,
|
|
671
742
|
consumeDelegation,
|
package/lib/task/events.ts
CHANGED
|
@@ -38,7 +38,9 @@ import {
|
|
|
38
38
|
readLocalArtifactFinalizationIntent,
|
|
39
39
|
validateLocalArtifact
|
|
40
40
|
} from './local-artifact-finalization.ts';
|
|
41
|
-
import {
|
|
41
|
+
import { writeArtifactRecoveryIntent, readArtifactRecoveryIntent } from './artifact-repair-intent.ts';
|
|
42
|
+
import { reconcileArtifactRecovery, recoveryContextFromIntent, sha256Content } from './artifact-recovery.ts';
|
|
43
|
+
import type { ArtifactRecoveryIntent } from './artifact-repair-intent.ts';
|
|
42
44
|
import type { LocalArtifactFamily, LocalArtifactFinalizationIntent } from './local-artifact-finalization.ts';
|
|
43
45
|
import { buildLifecycleFacts, canStart } from './capabilities.ts';
|
|
44
46
|
import type { ExplicitTrigger, LifecycleAction, TriggerInitiator, TriggerReason } from './capabilities.ts';
|
|
@@ -48,7 +50,7 @@ import { consumeReworkIntents, parseReworkIntentDocument, reworkIntentMutation,
|
|
|
48
50
|
import { ARTIFACT_FAMILIES, expectedQualificationRelations, parseQualificationAudit, parseTaskQualification, upstreamArtifactDigest, validateQualificationAudit } from './qualification-audit.ts';
|
|
49
51
|
import type { QualificationAudit, UpstreamRelation } from './qualification-audit.ts';
|
|
50
52
|
import { getArtifactSchema } from './artifact-schema.ts';
|
|
51
|
-
import { inspectArtifactContract } from './artifact-operations.ts';
|
|
53
|
+
import { canonicalSemanticDigest, inspectArtifactContract } from './artifact-operations.ts';
|
|
52
54
|
import {
|
|
53
55
|
consumeLifecycleRecoveryAttestation,
|
|
54
56
|
lifecycleRecoveryAttestationDigest,
|
|
@@ -373,6 +375,67 @@ function reviewInputFamily(family: EventFamily): ArtifactFamily {
|
|
|
373
375
|
return family === 'review-analysis' ? 'analysis' : family === 'review-plan' ? 'plan' : 'code';
|
|
374
376
|
}
|
|
375
377
|
|
|
378
|
+
function reconcileReviewCompletionReplay(
|
|
379
|
+
repoRoot: string,
|
|
380
|
+
taskDir: string,
|
|
381
|
+
taskId: string,
|
|
382
|
+
family: EventFamily,
|
|
383
|
+
artifact: string,
|
|
384
|
+
round: number
|
|
385
|
+
): TaskEventError | null {
|
|
386
|
+
const stage = REVIEW_LEDGER_STAGES[family];
|
|
387
|
+
if (!stage) return null;
|
|
388
|
+
const reviewFamily = family as 'review-analysis' | 'review-plan' | 'review-code';
|
|
389
|
+
try {
|
|
390
|
+
let intent = readArtifactRecoveryIntent(repoRoot, taskId, reviewFamily, artifact);
|
|
391
|
+
if (!intent) return null;
|
|
392
|
+
if (intent.state === 'commit-started') {
|
|
393
|
+
const reconciled = reconcileArtifactRecovery(
|
|
394
|
+
recoveryContextFromIntent(repoRoot, taskDir, intent),
|
|
395
|
+
{ lockAlreadyHeld: true }
|
|
396
|
+
);
|
|
397
|
+
if (reconciled.status !== 'passed' && reconciled.status !== 'consumed') {
|
|
398
|
+
return {
|
|
399
|
+
code: 'EVENT_ARTIFACT_CONFLICT',
|
|
400
|
+
message: `review finalizer recovery remains ${reconciled.status} for ${artifact}`
|
|
401
|
+
};
|
|
402
|
+
}
|
|
403
|
+
intent = reconciled.intent;
|
|
404
|
+
}
|
|
405
|
+
if (!['passed', 'consumption-started', 'consumed'].includes(intent.state)) {
|
|
406
|
+
return {
|
|
407
|
+
code: 'EVENT_ARTIFACT_CONFLICT',
|
|
408
|
+
message: `review finalizer provenance is incomplete for ${artifact}`
|
|
409
|
+
};
|
|
410
|
+
}
|
|
411
|
+
const validated = validateCompletedArtifact(taskDir, FAMILY[family].artifact, artifact, round);
|
|
412
|
+
if (!validated.ok) {
|
|
413
|
+
return { code: 'EVENT_ARTIFACT_CONFLICT', message: validated.error.message };
|
|
414
|
+
}
|
|
415
|
+
const content = fs.readFileSync(validated.artifact.path, 'utf8');
|
|
416
|
+
const expectedRequestId = `review-finalize:${taskId}:${stage}:${round}`;
|
|
417
|
+
if (intent.round !== round
|
|
418
|
+
|| intent.requestId !== expectedRequestId
|
|
419
|
+
|| intent.finalArtifactSha256 !== sha256Content(content)
|
|
420
|
+
|| intent.finalSemanticDigest !== canonicalSemanticDigest(content)) {
|
|
421
|
+
return {
|
|
422
|
+
code: 'EVENT_ARTIFACT_CONFLICT',
|
|
423
|
+
message: `review finalizer provenance does not match ${artifact}`
|
|
424
|
+
};
|
|
425
|
+
}
|
|
426
|
+
if (intent.state === 'passed' || intent.state === 'consumption-started') {
|
|
427
|
+
const consumed = { ...intent, state: 'consumed' as const, updatedAt: Date.now() };
|
|
428
|
+
writeArtifactRecoveryIntent(repoRoot, consumed, { expected: intent });
|
|
429
|
+
}
|
|
430
|
+
return null;
|
|
431
|
+
} catch (error) {
|
|
432
|
+
return {
|
|
433
|
+
code: 'EVENT_ARTIFACT_CONFLICT',
|
|
434
|
+
message: `review finalizer recovery could not be reconciled for ${artifact}: ${error instanceof Error ? error.message : String(error)}`
|
|
435
|
+
};
|
|
436
|
+
}
|
|
437
|
+
}
|
|
438
|
+
|
|
376
439
|
function relationForStartedInput(family: EventFamily, input: ArtifactIdentity): UpstreamRelation['relation'] {
|
|
377
440
|
if (family.startsWith('review-')) {
|
|
378
441
|
return input.family === reviewInputFamily(family) ? 'reviewed-input' : 'approval-context';
|
|
@@ -720,6 +783,8 @@ function applyTaskEventUnlocked(request: TaskEventRequest, options: TaskEventOpt
|
|
|
720
783
|
const row = manual ? openRows.at(-1) : matchingRows[0];
|
|
721
784
|
let completedArtifact: ArtifactIdentity | null = null;
|
|
722
785
|
let localFinalizationIntent: LocalArtifactFinalizationIntent | null = null;
|
|
786
|
+
let reviewFinalizationIntent: ArtifactRecoveryIntent | null = null;
|
|
787
|
+
let reviewContent: string | null = null;
|
|
723
788
|
if (eventIdentity.phase === 'started' && row) {
|
|
724
789
|
if (row.agent !== normalized.agent) return failed(normalized, { code: 'EVENT_LOG_CONFLICT', message: 'open started event has a different agent' }, { taskId: resolved.taskId, taskMdPath: resolved.taskMdPath });
|
|
725
790
|
if (manual && normalized.transactionId !== undefined) {
|
|
@@ -732,7 +797,20 @@ function applyTaskEventUnlocked(request: TaskEventRequest, options: TaskEventOpt
|
|
|
732
797
|
}
|
|
733
798
|
if (eventIdentity.phase === 'started' && !manual && completedRows.length > 0) return failed(normalized, { code: 'EVENT_ALREADY_COMPLETED', message: 'event identity is already completed' }, { taskId: resolved.taskId, taskMdPath: resolved.taskMdPath });
|
|
734
799
|
const done = completedRows.find((item) => item.note === eventIdentity.note);
|
|
735
|
-
if (eventIdentity.phase === 'completed' && done)
|
|
800
|
+
if (eventIdentity.phase === 'completed' && done) {
|
|
801
|
+
if (!normalized.dryRun && eventIdentity.family.startsWith('review-') && normalized.artifact && normalized.round) {
|
|
802
|
+
const recoveryError = reconcileReviewCompletionReplay(
|
|
803
|
+
resolved.repoRoot,
|
|
804
|
+
resolved.taskDir,
|
|
805
|
+
resolved.taskId,
|
|
806
|
+
eventIdentity.family,
|
|
807
|
+
normalized.artifact,
|
|
808
|
+
normalized.round
|
|
809
|
+
);
|
|
810
|
+
if (recoveryError) return failed(normalized, recoveryError, { taskId: resolved.taskId, taskMdPath: resolved.taskMdPath, fromStep: currentStep, toStep: currentStep, action: eventIdentity.action, phase: eventIdentity.phase });
|
|
811
|
+
}
|
|
812
|
+
return successNoOp(normalized, resolved.taskId, resolved.taskMdPath, currentStep, eventIdentity, done.done, frontmatter, artifactContext);
|
|
813
|
+
}
|
|
736
814
|
if (eventIdentity.phase === 'completed' && completedRows.length > 0 && openRows.length === 0) 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 });
|
|
737
815
|
if (eventIdentity.phase === 'completed' && !row?.started && !allowsManualOverride(options.manualOverride, 'task-event', 'EVENT_START_MISSING')) return failed(normalized, { code: 'EVENT_START_MISSING', message: 'completion requires one open matching started event' }, { taskId: resolved.taskId, taskMdPath: resolved.taskMdPath, fromStep: currentStep, toStep: currentStep, action: eventIdentity.action, phase: eventIdentity.phase });
|
|
738
816
|
if (eventIdentity.phase === 'completed') {
|
|
@@ -749,7 +827,6 @@ function applyTaskEventUnlocked(request: TaskEventRequest, options: TaskEventOpt
|
|
|
749
827
|
if (validationError) return failed(normalized, validationError, { taskId: resolved.taskId, taskMdPath: resolved.taskMdPath, fromStep: currentStep, toStep: currentStep, action: eventIdentity.action, phase: eventIdentity.phase });
|
|
750
828
|
}
|
|
751
829
|
if (eventIdentity.family.startsWith('review-')) {
|
|
752
|
-
let reviewContent: string;
|
|
753
830
|
try { reviewContent = fs.readFileSync(completedArtifact.path, 'utf8'); }
|
|
754
831
|
catch (error) {
|
|
755
832
|
return failed(normalized, { code: 'EVENT_ARTIFACT_CONFLICT', message: `cannot read qualification audit from ${completedArtifact.name}: ${error instanceof Error ? error.message : String(error)}` }, { taskId: resolved.taskId, taskMdPath: resolved.taskMdPath, fromStep: currentStep, toStep: currentStep, action: eventIdentity.action, phase: eventIdentity.phase });
|
|
@@ -776,6 +853,23 @@ function applyTaskEventUnlocked(request: TaskEventRequest, options: TaskEventOpt
|
|
|
776
853
|
const localFamily = eventIdentity.family === 'analyze'
|
|
777
854
|
? 'analysis'
|
|
778
855
|
: eventIdentity.family === 'plan' ? 'plan' : 'code';
|
|
856
|
+
try {
|
|
857
|
+
localFinalizationIntent = readLocalArtifactFinalizationIntent(
|
|
858
|
+
resolved.repoRoot, resolved.taskId, localFamily, completedArtifact.name
|
|
859
|
+
);
|
|
860
|
+
if (localFinalizationIntent?.state === 'commit-started') {
|
|
861
|
+
const reconciled = reconcileArtifactRecovery(
|
|
862
|
+
recoveryContextFromIntent(resolved.repoRoot, resolved.taskDir, localFinalizationIntent),
|
|
863
|
+
{ lockAlreadyHeld: true }
|
|
864
|
+
);
|
|
865
|
+
if (reconciled.status === 'passed' || reconciled.status === 'consumed') localFinalizationIntent = reconciled.intent;
|
|
866
|
+
}
|
|
867
|
+
} catch (error) {
|
|
868
|
+
return failed(normalized, {
|
|
869
|
+
code: 'EVENT_ARTIFACT_CONFLICT',
|
|
870
|
+
message: `local finalizer recovery could not be reconciled for ${completedArtifact.name}: ${error instanceof Error ? error.message : String(error)}`
|
|
871
|
+
}, { taskId: resolved.taskId, taskMdPath: resolved.taskMdPath, fromStep: currentStep, toStep: currentStep, action: eventIdentity.action, phase: eventIdentity.phase });
|
|
872
|
+
}
|
|
779
873
|
let artifactContent: string;
|
|
780
874
|
try { artifactContent = fs.readFileSync(completedArtifact.path, 'utf8'); }
|
|
781
875
|
catch (error) {
|
|
@@ -805,34 +899,23 @@ function applyTaskEventUnlocked(request: TaskEventRequest, options: TaskEventOpt
|
|
|
805
899
|
message: `semantic digest does not match finalizer result for ${completedArtifact.name}`
|
|
806
900
|
}, { taskId: resolved.taskId, taskMdPath: resolved.taskMdPath, fromStep: currentStep, toStep: currentStep, action: eventIdentity.action, phase: eventIdentity.phase });
|
|
807
901
|
}
|
|
808
|
-
|
|
809
|
-
localFinalizationIntent = readLocalArtifactFinalizationIntent(
|
|
810
|
-
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)) {
|
|
902
|
+
if (!localFinalizationIntent || !['passed', 'consumed'].includes(localFinalizationIntent.state)) {
|
|
819
903
|
return failed(normalized, {
|
|
820
904
|
code: 'EVENT_ARTIFACT_CONFLICT',
|
|
821
905
|
message: `local finalizer provenance is missing or incomplete for ${completedArtifact.name}`
|
|
822
906
|
}, { taskId: resolved.taskId, taskMdPath: resolved.taskMdPath, fromStep: currentStep, toStep: currentStep, action: eventIdentity.action, phase: eventIdentity.phase });
|
|
823
907
|
}
|
|
824
|
-
if (localFinalizationIntent.
|
|
908
|
+
if (localFinalizationIntent.finalArtifactSha256 !== actualSha256 || localFinalizationIntent.finalSemanticDigest !== local.semanticDigest) {
|
|
825
909
|
return failed(normalized, {
|
|
826
910
|
code: 'EVENT_ARTIFACT_CONFLICT',
|
|
827
911
|
message: `local finalizer provenance does not match ${completedArtifact.name}`
|
|
828
912
|
}, { taskId: resolved.taskId, taskMdPath: resolved.taskMdPath, fromStep: currentStep, toStep: currentStep, action: eventIdentity.action, phase: eventIdentity.phase });
|
|
829
913
|
}
|
|
830
|
-
if (localFinalizationIntent.
|
|
914
|
+
if (localFinalizationIntent.authorityDigest !== null) {
|
|
831
915
|
if (!lifecycleAuthority
|
|
832
916
|
|| localFinalizationIntent.recoveryOperationId !== lifecycleAuthority.operationId
|
|
833
917
|
|| localFinalizationIntent.requestId !== lifecycleAuthority.lifecycleRequestId
|
|
834
|
-
||
|
|
835
|
-
&& localFinalizationIntent.authorityDigest !== lifecycleRecoveryAttestationDigest(lifecycleAuthority))) {
|
|
918
|
+
|| localFinalizationIntent.authorityDigest !== lifecycleRecoveryAttestationDigest(lifecycleAuthority)) {
|
|
836
919
|
return failed(normalized, {
|
|
837
920
|
code: 'EVENT_ARTIFACT_CONFLICT',
|
|
838
921
|
message: `local finalizer provenance does not match lifecycle completion authority for ${completedArtifact.name}`
|
|
@@ -865,6 +948,49 @@ function applyTaskEventUnlocked(request: TaskEventRequest, options: TaskEventOpt
|
|
|
865
948
|
completedArtifact?.path ?? null
|
|
866
949
|
);
|
|
867
950
|
if (findingCountError) return failed(normalized, findingCountError, { taskId: resolved.taskId, taskMdPath: resolved.taskMdPath, fromStep: currentStep, toStep: currentStep, action: eventIdentity.action, phase: eventIdentity.phase });
|
|
951
|
+
if (eventIdentity.phase === 'completed' && eventIdentity.family.startsWith('review-') && completedArtifact && reviewContent !== null) {
|
|
952
|
+
try {
|
|
953
|
+
reviewFinalizationIntent = readArtifactRecoveryIntent(
|
|
954
|
+
resolved.repoRoot,
|
|
955
|
+
resolved.taskId,
|
|
956
|
+
eventIdentity.family as 'review-analysis' | 'review-plan' | 'review-code',
|
|
957
|
+
completedArtifact.name
|
|
958
|
+
);
|
|
959
|
+
if (!normalized.dryRun && reviewFinalizationIntent?.state === 'commit-started') {
|
|
960
|
+
const reconciled = reconcileArtifactRecovery(
|
|
961
|
+
recoveryContextFromIntent(resolved.repoRoot, resolved.taskDir, reviewFinalizationIntent),
|
|
962
|
+
{ lockAlreadyHeld: true }
|
|
963
|
+
);
|
|
964
|
+
if (reconciled.status === 'passed' || reconciled.status === 'consumed') reviewFinalizationIntent = reconciled.intent;
|
|
965
|
+
}
|
|
966
|
+
if (!reviewFinalizationIntent || !['passed', 'consumption-started', 'consumed'].includes(reviewFinalizationIntent.state)) {
|
|
967
|
+
return failed(normalized, {
|
|
968
|
+
code: 'EVENT_ARTIFACT_CONFLICT',
|
|
969
|
+
message: `review finalizer provenance is missing or incomplete for ${completedArtifact.name}`
|
|
970
|
+
}, { taskId: resolved.taskId, taskMdPath: resolved.taskMdPath, fromStep: currentStep, toStep: currentStep, action: eventIdentity.action, phase: eventIdentity.phase });
|
|
971
|
+
}
|
|
972
|
+
const reviewStage = eventIdentity.family === 'review-analysis'
|
|
973
|
+
? 'analysis'
|
|
974
|
+
: eventIdentity.family === 'review-plan' ? 'plan' : 'code';
|
|
975
|
+
const actualSha256 = sha256File(completedArtifact.path);
|
|
976
|
+
const actualSemanticDigest = canonicalSemanticDigest(reviewContent);
|
|
977
|
+
const expectedRequestId = `review-finalize:${resolved.taskId}:${reviewStage}:${normalized.round}`;
|
|
978
|
+
if (reviewFinalizationIntent.round !== normalized.round
|
|
979
|
+
|| reviewFinalizationIntent.requestId !== expectedRequestId
|
|
980
|
+
|| reviewFinalizationIntent.finalArtifactSha256 !== actualSha256
|
|
981
|
+
|| reviewFinalizationIntent.finalSemanticDigest !== actualSemanticDigest) {
|
|
982
|
+
return failed(normalized, {
|
|
983
|
+
code: 'EVENT_ARTIFACT_CONFLICT',
|
|
984
|
+
message: `review finalizer provenance does not match ${completedArtifact.name}`
|
|
985
|
+
}, { taskId: resolved.taskId, taskMdPath: resolved.taskMdPath, fromStep: currentStep, toStep: currentStep, action: eventIdentity.action, phase: eventIdentity.phase });
|
|
986
|
+
}
|
|
987
|
+
} catch (error) {
|
|
988
|
+
return failed(normalized, {
|
|
989
|
+
code: 'EVENT_ARTIFACT_CONFLICT',
|
|
990
|
+
message: `review finalizer recovery could not be reconciled for ${completedArtifact.name}: ${error instanceof Error ? error.message : String(error)}`
|
|
991
|
+
}, { taskId: resolved.taskId, taskMdPath: resolved.taskMdPath, fromStep: currentStep, toStep: currentStep, action: eventIdentity.action, phase: eventIdentity.phase });
|
|
992
|
+
}
|
|
993
|
+
}
|
|
868
994
|
let orchestrationCompletion: OrchestrationStageCompletion | null = null;
|
|
869
995
|
if (eventIdentity.phase === 'completed' && eventIdentity.family !== 'manual-validation' && eventIdentity.family !== 'validation-run') {
|
|
870
996
|
const orchestrationStage = eventIdentity.family === 'analyze' ? 'analysis' : eventIdentity.family;
|
|
@@ -990,15 +1116,15 @@ function applyTaskEventUnlocked(request: TaskEventRequest, options: TaskEventOpt
|
|
|
990
1116
|
const expectedIntent = localFinalizationIntent;
|
|
991
1117
|
localFinalizationIntent = {
|
|
992
1118
|
...localFinalizationIntent,
|
|
993
|
-
state: '
|
|
1119
|
+
state: 'consumption-started',
|
|
994
1120
|
phase: 'task-event.completed',
|
|
995
1121
|
authorityDigest: lifecycleRecoveryAttestationDigest(lifecycleAuthority),
|
|
996
1122
|
requestId: lifecycleAuthority.lifecycleRequestId,
|
|
997
1123
|
updatedAt: Date.now()
|
|
998
1124
|
};
|
|
999
|
-
|
|
1125
|
+
writeArtifactRecoveryIntent(resolved.repoRoot, localFinalizationIntent, { expected: expectedIntent });
|
|
1000
1126
|
} else if (!lifecycleAuthority && localFinalizationIntent.state === 'passed') {
|
|
1001
|
-
localFinalizationIntent = consumeLocalArtifactFinalizationIntent(resolved.repoRoot, localFinalizationIntent);
|
|
1127
|
+
localFinalizationIntent = consumeLocalArtifactFinalizationIntent(resolved.repoRoot, localFinalizationIntent, { lockAlreadyHeld: options.lockAlreadyHeld });
|
|
1002
1128
|
}
|
|
1003
1129
|
} catch (error) {
|
|
1004
1130
|
return failed(normalized, {
|
|
@@ -1015,6 +1141,31 @@ function applyTaskEventUnlocked(request: TaskEventRequest, options: TaskEventOpt
|
|
|
1015
1141
|
});
|
|
1016
1142
|
}
|
|
1017
1143
|
}
|
|
1144
|
+
if (!normalized.dryRun && reviewFinalizationIntent && completedArtifact && reviewFinalizationIntent.state === 'passed') {
|
|
1145
|
+
try {
|
|
1146
|
+
const expectedIntent = reviewFinalizationIntent;
|
|
1147
|
+
reviewFinalizationIntent = {
|
|
1148
|
+
...reviewFinalizationIntent,
|
|
1149
|
+
state: 'consumption-started',
|
|
1150
|
+
phase: 'task-event.completed',
|
|
1151
|
+
updatedAt: Date.now()
|
|
1152
|
+
};
|
|
1153
|
+
writeArtifactRecoveryIntent(resolved.repoRoot, reviewFinalizationIntent, { expected: expectedIntent });
|
|
1154
|
+
} catch (error) {
|
|
1155
|
+
return failed(normalized, {
|
|
1156
|
+
code: 'EVENT_ARTIFACT_CONFLICT',
|
|
1157
|
+
message: `review finalizer provenance could not be advanced before task write for ${completedArtifact.name}: ${error instanceof Error ? error.message : String(error)}`
|
|
1158
|
+
}, {
|
|
1159
|
+
taskId: resolved.taskId,
|
|
1160
|
+
taskMdPath: resolved.taskMdPath,
|
|
1161
|
+
fromStep: currentStep,
|
|
1162
|
+
toStep: step,
|
|
1163
|
+
action: eventIdentity.action,
|
|
1164
|
+
phase: eventIdentity.phase,
|
|
1165
|
+
artifactContext
|
|
1166
|
+
});
|
|
1167
|
+
}
|
|
1168
|
+
}
|
|
1018
1169
|
const sourceCompletion = eventIdentity.phase === 'completed'
|
|
1019
1170
|
&& ['analyze', 'plan', 'code'].includes(eventIdentity.family);
|
|
1020
1171
|
const result = writeTask({ taskRef: normalized.taskRef, expectedState: stateOverride ? resolved.state : 'active', dryRun: normalized.dryRun, mutations }, { ...options, invalidationContext: sourceCompletion ? 'source-completion' : 'standard', metadataProvider: () => metadata });
|
|
@@ -1022,7 +1173,7 @@ function applyTaskEventUnlocked(request: TaskEventRequest, options: TaskEventOpt
|
|
|
1022
1173
|
if (!normalized.dryRun && lifecycleAuthority && localFinalizationIntent && completedArtifact) {
|
|
1023
1174
|
try {
|
|
1024
1175
|
if (!options.deferLifecycleRecoveryConsumption) consumeLifecycleRecoveryAttestation(lifecycleAuthority);
|
|
1025
|
-
|
|
1176
|
+
writeArtifactRecoveryIntent(resolved.repoRoot, { ...localFinalizationIntent, state: 'consumed', updatedAt: Date.now() }, { expected: localFinalizationIntent });
|
|
1026
1177
|
} catch (error) {
|
|
1027
1178
|
return failed(normalized, {
|
|
1028
1179
|
code: 'EVENT_ARTIFACT_CONFLICT',
|
|
@@ -1030,6 +1181,19 @@ function applyTaskEventUnlocked(request: TaskEventRequest, options: TaskEventOpt
|
|
|
1030
1181
|
}, { 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 });
|
|
1031
1182
|
}
|
|
1032
1183
|
}
|
|
1184
|
+
if (!normalized.dryRun && reviewFinalizationIntent && completedArtifact) {
|
|
1185
|
+
try {
|
|
1186
|
+
if (reviewFinalizationIntent.state === 'consumption-started') {
|
|
1187
|
+
const consumed = { ...reviewFinalizationIntent, state: 'consumed' as const, updatedAt: Date.now() };
|
|
1188
|
+
writeArtifactRecoveryIntent(resolved.repoRoot, consumed, { expected: reviewFinalizationIntent });
|
|
1189
|
+
}
|
|
1190
|
+
} catch (error) {
|
|
1191
|
+
return failed(normalized, {
|
|
1192
|
+
code: 'EVENT_ARTIFACT_CONFLICT',
|
|
1193
|
+
message: `review finalizer recovery consumption is incomplete: ${error instanceof Error ? error.message : String(error)}`
|
|
1194
|
+
}, { 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 });
|
|
1195
|
+
}
|
|
1196
|
+
}
|
|
1033
1197
|
if (!normalized.dryRun && orchestrationCompletion) {
|
|
1034
1198
|
try {
|
|
1035
1199
|
(options.commitOrchestrationCompletion ?? commitOrchestrationStageCompletion)(orchestrationCompletion);
|
|
@@ -1077,7 +1241,7 @@ function applyTaskEvent(request: TaskEventRequest, options: TaskEventOptions = {
|
|
|
1077
1241
|
resolved.repoRoot,
|
|
1078
1242
|
resolved.taskId,
|
|
1079
1243
|
`task-event.${request.event}`,
|
|
1080
|
-
() => applyTaskEventUnlocked(request, options)
|
|
1244
|
+
() => applyTaskEventUnlocked(request, { ...options, lockAlreadyHeld: true })
|
|
1081
1245
|
);
|
|
1082
1246
|
} catch (error) {
|
|
1083
1247
|
if (!(error instanceof TaskExecutionLockError)) throw error;
|
|
@@ -617,10 +617,7 @@ function resourceForSnapshot(
|
|
|
617
617
|
if (policy.producerId === 'platform.issue') {
|
|
618
618
|
const identity = options.probeIssueNumber !== undefined
|
|
619
619
|
? resourceIdentity(options.probeIssueNumber)
|
|
620
|
-
: taskIssueIdentity((frontmatter || {}) as Record<string, string | number | boolean | null>)
|
|
621
|
-
const number = toPositiveInteger(frontmatter?.issue_number);
|
|
622
|
-
return number === null ? null : resourceIdentity(number);
|
|
623
|
-
})();
|
|
620
|
+
: taskIssueIdentity((frontmatter || {}) as Record<string, string | number | boolean | null>);
|
|
624
621
|
return identity ? { kind: 'issue', identity, number: resourceIdentityNumber(identity) } : null;
|
|
625
622
|
}
|
|
626
623
|
if (policy.producerId === 'platform.pull-request') {
|
|
@@ -719,11 +716,11 @@ async function probePlatformFailure(
|
|
|
719
716
|
}
|
|
720
717
|
const issueIdentity = options.probeIssueNumber !== undefined
|
|
721
718
|
? resourceIdentity(options.probeIssueNumber)
|
|
722
|
-
: taskIssueIdentity(frontmatter)
|
|
723
|
-
const number = toPositiveInteger(frontmatter.issue_number);
|
|
724
|
-
return number === null ? null : resourceIdentity(number);
|
|
725
|
-
})();
|
|
719
|
+
: taskIssueIdentity(frontmatter);
|
|
726
720
|
const fact = readPrDeliveryFact(frontmatter);
|
|
721
|
+
if (fact.status === 'invalid') {
|
|
722
|
+
return error(fact.error.code, fact.error.message);
|
|
723
|
+
}
|
|
727
724
|
const pullRequestIdentity = options.probePullRequestNumber !== undefined
|
|
728
725
|
? resourceIdentity(options.probePullRequestNumber)
|
|
729
726
|
: fact.status === 'valid' && fact.fact.state === 'bound' ? fact.fact.identity.resource : null;
|
|
@@ -15,17 +15,18 @@ import {
|
|
|
15
15
|
type TaskFinalizationReceipt
|
|
16
16
|
} from './finalization.ts';
|
|
17
17
|
import {
|
|
18
|
-
readLocalArtifactFinalizationIntent,
|
|
19
18
|
semanticDigest,
|
|
20
|
-
sha256Content
|
|
21
|
-
type LocalArtifactFinalizationIntent
|
|
19
|
+
sha256Content
|
|
22
20
|
} from './local-artifact-finalization.ts';
|
|
21
|
+
import { artifactRecoveryRoot, readArtifactRecoveryIntent } from './artifact-repair-intent.ts';
|
|
22
|
+
import type { ArtifactRecoveryIntent } from './artifact-repair-intent.ts';
|
|
23
23
|
import { parseArtifactName } from './artifact-name.ts';
|
|
24
|
+
import type { ArtifactSchemaFamily } from './artifact-schema.ts';
|
|
24
25
|
import { enumerateAllTaskDirs, type TaskWorkspaceState } from './resolve-ref.ts';
|
|
25
26
|
import type { TaskControlBindingEvidence } from './finalization-state.ts';
|
|
26
27
|
|
|
27
28
|
const TASK_ID_RE = /^TASK-\d{8}-\d{6}$/;
|
|
28
|
-
const LOCAL_INTENT_RE = /^(TASK-\d{8}-\d{6})-(analysis|plan|code)-(.+\.md)\.json$/;
|
|
29
|
+
const LOCAL_INTENT_RE = /^(TASK-\d{8}-\d{6})-(analysis|review-analysis|plan|review-plan|code|review-code)-(.+\.md)\.json$/;
|
|
29
30
|
const COMMIT_INTENT_RE = /^(TASK-\d{8}-\d{6})\.json$/;
|
|
30
31
|
const AUXILIARY_ROOTS = [
|
|
31
32
|
'.local-artifact-finalization-intents',
|
|
@@ -161,7 +162,7 @@ function localIntentCandidate(
|
|
|
161
162
|
task: TaskRecord | undefined,
|
|
162
163
|
filePath: string,
|
|
163
164
|
taskId: string,
|
|
164
|
-
family:
|
|
165
|
+
family: ArtifactSchemaFamily,
|
|
165
166
|
artifact: string,
|
|
166
167
|
identity: FileIdentity,
|
|
167
168
|
options: ScanOptions
|
|
@@ -172,8 +173,8 @@ function localIntentCandidate(
|
|
|
172
173
|
|| !parseArtifactName(artifact)?.family || parseArtifactName(artifact)!.family !== family) {
|
|
173
174
|
return item('LFAI-CONSUMED', taskId, filePath, 'protected', 'PATH_IDENTITY_MISMATCH', identity);
|
|
174
175
|
}
|
|
175
|
-
let intent:
|
|
176
|
-
try { intent =
|
|
176
|
+
let intent: ArtifactRecoveryIntent | null;
|
|
177
|
+
try { intent = readArtifactRecoveryIntent(repoRoot, taskId, family, artifact); }
|
|
177
178
|
catch { return item('LFAI-CONSUMED', taskId, filePath, 'protected', 'LFAI_SCHEMA_INVALID', identity); }
|
|
178
179
|
if (!intent || intent.state !== 'consumed') {
|
|
179
180
|
return item('LFAI-CONSUMED', taskId, filePath, 'protected', 'LFAI_STATE_PROTECTED', identity);
|
|
@@ -185,7 +186,7 @@ function localIntentCandidate(
|
|
|
185
186
|
let content: string;
|
|
186
187
|
try { content = fs.readFileSync(artifactPath, 'utf8'); }
|
|
187
188
|
catch { return item('LFAI-CONSUMED', taskId, filePath, 'protected', 'ARTIFACT_UNREADABLE', identity); }
|
|
188
|
-
if (intent.
|
|
189
|
+
if (intent.finalArtifactSha256 !== sha256Content(content) || intent.finalSemanticDigest !== semanticDigest(content)) {
|
|
189
190
|
return item('LFAI-CONSUMED', taskId, filePath, 'protected', 'ARTIFACT_DIGEST_MISMATCH', identity);
|
|
190
191
|
}
|
|
191
192
|
const receiptReason = receiptGate(repoRoot, task!, options);
|
|
@@ -306,7 +307,56 @@ function scanIntermediateCleanup(repoRoot: string, options: ScanOptions = {}): I
|
|
|
306
307
|
return buildIntermediateCleanupReport(candidates, true);
|
|
307
308
|
}
|
|
308
309
|
|
|
309
|
-
function
|
|
310
|
+
function cleanupConsumedRecoveryFiles(repoRoot: string, task: TaskRecord | undefined, candidate: IntermediateCleanupItem): boolean {
|
|
311
|
+
if (candidate.kind !== 'LFAI-CONSUMED' || !candidate.taskId) return true;
|
|
312
|
+
if (!task) return false;
|
|
313
|
+
const match = LOCAL_INTENT_RE.exec(path.basename(candidate.path));
|
|
314
|
+
if (!match) return false;
|
|
315
|
+
let intent: ReturnType<typeof readArtifactRecoveryIntent>;
|
|
316
|
+
try { intent = readArtifactRecoveryIntent(repoRoot, candidate.taskId, match[2] as ArtifactSchemaFamily, match[3]!); }
|
|
317
|
+
catch { return false; }
|
|
318
|
+
if (!intent || intent.state !== 'consumed') return false;
|
|
319
|
+
|
|
320
|
+
const recovery = artifactRecoveryRoot(task.taskDir, intent.recoveryOperationId);
|
|
321
|
+
let stat: fs.Stats | null = null;
|
|
322
|
+
try { stat = fs.lstatSync(recovery); } catch (error) {
|
|
323
|
+
if (errorCode(error) === 'ENOENT') return true;
|
|
324
|
+
return false;
|
|
325
|
+
}
|
|
326
|
+
if (!stat.isDirectory() || stat.isSymbolicLink()) return false;
|
|
327
|
+
let entries: fs.Dirent[];
|
|
328
|
+
try { entries = fs.readdirSync(recovery, { withFileTypes: true }); } catch { return false; }
|
|
329
|
+
for (const entry of entries) {
|
|
330
|
+
if (entry.isSymbolicLink()) return false;
|
|
331
|
+
if (entry.isFile()) {
|
|
332
|
+
if (!['baseline.md', 'candidate.md', 'final.md', 'publish.md'].includes(entry.name)) return false;
|
|
333
|
+
continue;
|
|
334
|
+
}
|
|
335
|
+
if (!entry.isDirectory() || entry.name !== 'generations') return false;
|
|
336
|
+
let generations: fs.Dirent[];
|
|
337
|
+
try { generations = fs.readdirSync(path.join(recovery, entry.name), { withFileTypes: true }); } catch { return false; }
|
|
338
|
+
if (generations.some((generation) => generation.isSymbolicLink()
|
|
339
|
+
|| !generation.isFile() || !/^[a-f0-9]{64}\.md$/u.test(generation.name))) return false;
|
|
340
|
+
}
|
|
341
|
+
try {
|
|
342
|
+
for (const entry of entries) {
|
|
343
|
+
const target = path.join(recovery, entry.name);
|
|
344
|
+
if (entry.isFile()) fs.unlinkSync(target);
|
|
345
|
+
else {
|
|
346
|
+
for (const generation of fs.readdirSync(target, { withFileTypes: true })) fs.unlinkSync(path.join(target, generation.name));
|
|
347
|
+
fs.rmdirSync(target);
|
|
348
|
+
}
|
|
349
|
+
}
|
|
350
|
+
fs.rmdirSync(recovery);
|
|
351
|
+
const recoveryRoot = path.dirname(recovery);
|
|
352
|
+
if (fs.readdirSync(recoveryRoot).length === 0) fs.rmdirSync(recoveryRoot);
|
|
353
|
+
return true;
|
|
354
|
+
} catch (error) {
|
|
355
|
+
return errorCode(error) === 'ENOENT';
|
|
356
|
+
}
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
function removeCandidate(repoRoot: string, candidate: IntermediateCleanupItem, task?: TaskRecord): IntermediateCleanupItem {
|
|
310
360
|
if (candidate.disposition !== 'planned') return candidate;
|
|
311
361
|
const root = candidate.kind === 'EMPTY-AUX-PARENT'
|
|
312
362
|
? path.dirname(path.dirname(candidate.path))
|
|
@@ -323,6 +373,9 @@ function removeCandidate(candidate: IntermediateCleanupItem): IntermediateCleanu
|
|
|
323
373
|
const actual = safeLstat(root, candidate.path, candidate.kind === 'EMPTY-AUX-PARENT' ? 'directory' : 'file');
|
|
324
374
|
if (!actual) return { ...candidate, disposition: 'protected', reason: 'PATH_IDENTITY_CHANGED' };
|
|
325
375
|
if (!sameFileIdentity(expected, actual)) return { ...candidate, disposition: 'protected', reason: 'PATH_IDENTITY_CHANGED' };
|
|
376
|
+
if (!cleanupConsumedRecoveryFiles(repoRoot, task, candidate)) {
|
|
377
|
+
return { ...candidate, disposition: 'protected', reason: 'RECOVERY_STAGING_NOT_SAFE_TO_REMOVE' };
|
|
378
|
+
}
|
|
326
379
|
try {
|
|
327
380
|
if (candidate.kind === 'EMPTY-AUX-PARENT') {
|
|
328
381
|
if (fs.readdirSync(candidate.path).length !== 0) return { ...candidate, disposition: 'protected', reason: 'AUXILIARY_ROOT_NOT_EMPTY' };
|
|
@@ -367,6 +420,7 @@ function cleanupIntermediateUnderRemovalCoordinator(
|
|
|
367
420
|
indexes.push(index);
|
|
368
421
|
byTask.set(candidate.taskId, indexes);
|
|
369
422
|
}
|
|
423
|
+
const tasks = inspectTaskRecords(repoRoot, new Set(byTask.keys()));
|
|
370
424
|
|
|
371
425
|
for (const taskId of [...byTask.keys()].sort()) {
|
|
372
426
|
const execute = (): void => {
|
|
@@ -380,7 +434,7 @@ function cleanupIntermediateUnderRemovalCoordinator(
|
|
|
380
434
|
if (initialCandidate.disposition !== 'planned') continue;
|
|
381
435
|
const current = refreshedByKey.get(intermediateCleanupKey(initialCandidate));
|
|
382
436
|
items[index] = current
|
|
383
|
-
? removeCandidate(current)
|
|
437
|
+
? removeCandidate(repoRoot, current, tasks.get(taskId))
|
|
384
438
|
: { ...initialCandidate, disposition: 'protected', reason: 'CANDIDATE_NO_LONGER_ELIGIBLE' };
|
|
385
439
|
}
|
|
386
440
|
};
|
|
@@ -397,14 +451,14 @@ function cleanupIntermediateUnderRemovalCoordinator(
|
|
|
397
451
|
|
|
398
452
|
for (const [index, candidate] of items.entries()) {
|
|
399
453
|
if (candidate.taskId || candidate.disposition !== 'planned') continue;
|
|
400
|
-
items[index] = removeCandidate(candidate);
|
|
454
|
+
items[index] = removeCandidate(repoRoot, candidate);
|
|
401
455
|
}
|
|
402
456
|
|
|
403
457
|
const knownPaths = new Set(items.map((candidate) => intermediateCleanupKey(candidate)));
|
|
404
458
|
for (const candidate of scanIntermediateCleanup(repoRoot, scanOptions).items) {
|
|
405
459
|
if (candidate.kind !== 'EMPTY-AUX-PARENT' || candidate.disposition !== 'planned'
|
|
406
460
|
|| knownPaths.has(intermediateCleanupKey(candidate))) continue;
|
|
407
|
-
items.push(removeCandidate(candidate));
|
|
461
|
+
items.push(removeCandidate(repoRoot, candidate));
|
|
408
462
|
}
|
|
409
463
|
|
|
410
464
|
return buildIntermediateCleanupReport(items);
|