@fitlab-ai/agent-infra 0.11.1 → 0.11.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/bin/internal-cli.js +7 -2
- package/dist/lib/agent-clients/adapter.js +3 -0
- package/dist/lib/agent-clients/adapters/codex-lifecycle/recovery.js +234 -0
- package/dist/lib/agent-clients/adapters/codex-lifecycle/sandbox-controller.js +8 -35
- package/dist/lib/agent-clients/adapters/codex-lifecycle/store.js +1 -34
- package/dist/lib/agent-clients/adapters/codex.js +10 -0
- package/dist/lib/internal/cli-route-inventory.js +1 -1
- package/dist/lib/internal/manual-validation.js +22 -27
- package/dist/lib/internal/platform-comment.js +59 -0
- package/dist/lib/internal/sandbox-control.js +2 -171
- package/dist/lib/internal/task-artifact.js +1 -1
- package/dist/lib/internal/task-lifecycle.js +1 -1
- package/dist/lib/internal/task-operation-registry.js +7 -2
- package/dist/lib/internal/task-review.js +5 -5
- package/dist/lib/internal/task-validate.js +0 -6
- package/dist/lib/platform/comment-safety.js +7 -1
- package/dist/lib/platform/github-provider.js +13 -3
- package/dist/lib/platform/issue-comments.js +93 -15
- package/dist/lib/platform/pr-summary.js +7 -2
- package/dist/lib/platform/provider-contract.d.ts +2 -0
- package/dist/lib/platform/provider-validation.js +7 -2
- package/dist/lib/platform/verification-sync.js +19 -2
- package/dist/lib/sandbox/control/client.js +3 -44
- package/dist/lib/sandbox/control/executor.js +14 -117
- package/dist/lib/sandbox/control/finalization-response.js +8 -2
- package/dist/lib/sandbox/control/protocol.js +5 -64
- package/dist/lib/sandbox/control/server.js +29 -206
- package/dist/lib/sandbox/control/state.js +1 -7
- package/dist/lib/sandbox/control/task-view.js +2 -1
- package/dist/lib/task/artifact-command.js +6 -6
- package/dist/lib/task/codex-orchestration.js +10 -208
- package/dist/lib/task/commit-operation.js +3 -18
- package/dist/lib/task/control-authority.js +35 -44
- package/dist/lib/task/control-recovery.js +3 -42
- package/dist/lib/task/delegation-receipts.js +16 -78
- package/dist/lib/task/events.js +89 -237
- package/dist/lib/task/finalization-artifacts.js +1 -3
- package/dist/lib/task/finalization-state.js +2 -1
- package/dist/lib/task/finalization.js +208 -57
- package/dist/lib/task/lifecycle-execution.js +2 -11
- package/dist/lib/task/lifecycle.js +1 -1
- package/dist/lib/task/local-artifact-finalization.js +25 -242
- package/dist/lib/task/manual-validation-receipt.js +8 -3
- package/dist/lib/task/orchestration.js +12 -24
- package/dist/lib/task/review-command.js +0 -1
- package/dist/lib/task/review-finalization.js +46 -211
- package/dist/lifecycle-build-manifest.json +3 -3
- package/lib/agent-clients/adapter.ts +46 -0
- package/lib/agent-clients/adapters/codex-lifecycle/recovery.ts +297 -0
- package/lib/agent-clients/adapters/codex-lifecycle/sandbox-controller.ts +13 -33
- package/lib/agent-clients/adapters/codex-lifecycle/store.ts +1 -44
- package/lib/agent-clients/adapters/codex.ts +10 -0
- package/lib/internal/cli-route-inventory.ts +1 -1
- package/lib/internal/manual-validation.ts +22 -26
- package/lib/internal/platform-comment.ts +38 -0
- package/lib/internal/sandbox-control.ts +1 -185
- package/lib/internal/task-artifact.ts +1 -1
- package/lib/internal/task-lifecycle.ts +2 -2
- package/lib/internal/task-operation-registry.ts +8 -4
- package/lib/internal/task-review.ts +5 -5
- package/lib/internal/task-validate.ts +0 -5
- package/lib/platform/comment-safety.ts +8 -0
- package/lib/platform/github-provider.ts +11 -2
- package/lib/platform/issue-comments.ts +102 -17
- package/lib/platform/pr-summary.ts +10 -2
- package/lib/platform/provider-contract.ts +2 -0
- package/lib/platform/provider-validation.ts +6 -2
- package/lib/platform/verification-sync.ts +19 -2
- package/lib/sandbox/control/client.ts +5 -68
- package/lib/sandbox/control/executor.ts +14 -125
- package/lib/sandbox/control/finalization-response.ts +8 -2
- package/lib/sandbox/control/protocol.ts +7 -83
- package/lib/sandbox/control/server.ts +30 -220
- package/lib/sandbox/control/state.ts +1 -9
- package/lib/sandbox/control/task-view.ts +2 -1
- package/lib/task/artifact-command.ts +6 -7
- package/lib/task/codex-orchestration.ts +8 -241
- package/lib/task/commit-operation.ts +3 -23
- package/lib/task/control-authority.ts +39 -55
- package/lib/task/control-recovery.ts +3 -43
- package/lib/task/delegation-receipts.ts +14 -81
- package/lib/task/events.ts +106 -255
- package/lib/task/finalization-artifacts.ts +1 -3
- package/lib/task/finalization-state.ts +2 -1
- package/lib/task/finalization.ts +221 -65
- package/lib/task/lifecycle-execution.ts +10 -21
- package/lib/task/lifecycle.ts +2 -2
- package/lib/task/local-artifact-finalization.ts +23 -281
- package/lib/task/manual-validation-receipt.ts +8 -2
- package/lib/task/orchestration.ts +30 -53
- package/lib/task/review-command.ts +0 -1
- package/lib/task/review-finalization.ts +45 -193
- package/package.json +2 -2
- package/templates/.agents/rules/lifecycle-orchestration.en.md +23 -9
- package/templates/.agents/rules/lifecycle-orchestration.zh-CN.md +23 -9
- package/templates/.agents/rules/local-artifact-repair.en.md +14 -60
- package/templates/.agents/rules/local-artifact-repair.zh-CN.md +13 -60
- package/templates/.agents/skills/analyze-task/SKILL.en.md +3 -3
- package/templates/.agents/skills/analyze-task/SKILL.zh-CN.md +3 -3
- package/templates/.agents/skills/code-task/SKILL.en.md +3 -3
- package/templates/.agents/skills/code-task/SKILL.zh-CN.md +3 -3
- package/templates/.agents/skills/complete-manual-validation/SKILL.en.md +2 -2
- package/templates/.agents/skills/complete-manual-validation/SKILL.zh-CN.md +2 -2
- package/templates/.agents/skills/complete-task/SKILL.en.md +7 -8
- package/templates/.agents/skills/complete-task/SKILL.zh-CN.md +7 -8
- package/templates/.agents/skills/plan-task/SKILL.en.md +3 -3
- package/templates/.agents/skills/plan-task/SKILL.zh-CN.md +3 -3
- package/templates/.agents/skills/review-analysis/SKILL.en.md +2 -2
- package/templates/.agents/skills/review-analysis/SKILL.zh-CN.md +2 -2
- package/templates/.agents/skills/review-code/SKILL.en.md +3 -3
- package/templates/.agents/skills/review-code/SKILL.zh-CN.md +3 -3
- package/templates/.agents/skills/review-plan/SKILL.en.md +2 -2
- package/templates/.agents/skills/review-plan/SKILL.zh-CN.md +2 -2
- package/templates/.agents/skills/run-manual-validation/SKILL.en.md +2 -2
- package/templates/.agents/skills/run-manual-validation/SKILL.zh-CN.md +2 -2
- package/templates/.agents/skills/run-task/SKILL.en.md +10 -9
- package/templates/.agents/skills/run-task/SKILL.zh-CN.md +10 -9
- package/templates/.agents/skills/run-task/reference/host-validation.en.md +2 -2
- package/templates/.agents/skills/run-task/reference/host-validation.zh-CN.md +2 -2
- package/dist/lib/sandbox/control/task-workflow.js +0 -74
- package/dist/lib/sandbox/control/workflow-executor.js +0 -157
- package/dist/lib/task/lifecycle-recovery.js +0 -549
- package/dist/lib/task/recovery-warning.js +0 -19
- package/dist/lib/task/workflow-command.js +0 -18
- package/dist/lib/task/workflow-dispatch.js +0 -161
- package/lib/sandbox/control/task-workflow.ts +0 -95
- package/lib/sandbox/control/workflow-executor.ts +0 -183
- package/lib/task/lifecycle-recovery.ts +0 -691
- package/lib/task/recovery-warning.ts +0 -21
- package/lib/task/workflow-command.ts +0 -19
- package/lib/task/workflow-dispatch.ts +0 -171
package/lib/task/events.ts
CHANGED
|
@@ -33,15 +33,8 @@ import { TaskExecutionLockError, withTaskExecutionLock } from './task-execution-
|
|
|
33
33
|
import { captureTaskWriteMetadata, writeTask } from './write.ts';
|
|
34
34
|
import type { TaskOperationSummary, TaskWriteErrorCode, TaskWriteOptions } from './write.ts';
|
|
35
35
|
import { allowsManualOverride } from './guard-override.ts';
|
|
36
|
-
import {
|
|
37
|
-
|
|
38
|
-
readLocalArtifactFinalizationIntent,
|
|
39
|
-
validateLocalArtifact
|
|
40
|
-
} from './local-artifact-finalization.ts';
|
|
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';
|
|
44
|
-
import type { LocalArtifactFamily, LocalArtifactFinalizationIntent } from './local-artifact-finalization.ts';
|
|
36
|
+
import { validateLocalArtifact } from './local-artifact-finalization.ts';
|
|
37
|
+
import type { LocalArtifactFamily } from './local-artifact-finalization.ts';
|
|
45
38
|
import { buildLifecycleFacts, canStart } from './capabilities.ts';
|
|
46
39
|
import type { ExplicitTrigger, LifecycleAction, TriggerInitiator, TriggerReason } from './capabilities.ts';
|
|
47
40
|
import { createInvalidationOperation, invalidationMutation, parseInvalidationDocument, targetIdFor, upsertInvalidation } from './invalidation.ts';
|
|
@@ -393,65 +386,6 @@ function reviewInputFamily(family: EventFamily): ArtifactFamily {
|
|
|
393
386
|
return family === 'review-analysis' ? 'analysis' : family === 'review-plan' ? 'plan' : 'code';
|
|
394
387
|
}
|
|
395
388
|
|
|
396
|
-
function reconcileReviewCompletionReplay(
|
|
397
|
-
repoRoot: string,
|
|
398
|
-
taskDir: string,
|
|
399
|
-
taskId: string,
|
|
400
|
-
family: EventFamily,
|
|
401
|
-
artifact: string,
|
|
402
|
-
round: number
|
|
403
|
-
): TaskEventError | null {
|
|
404
|
-
const stage = REVIEW_LEDGER_STAGES[family];
|
|
405
|
-
if (!stage) return null;
|
|
406
|
-
const reviewFamily = family as 'review-analysis' | 'review-plan' | 'review-code';
|
|
407
|
-
try {
|
|
408
|
-
let intent = readArtifactRecoveryIntent(repoRoot, taskId, reviewFamily, artifact);
|
|
409
|
-
if (!intent) return null;
|
|
410
|
-
if (intent.state === 'commit-started') {
|
|
411
|
-
const reconciled = reconcileArtifactRecovery(
|
|
412
|
-
recoveryContextFromIntent(repoRoot, taskDir, intent),
|
|
413
|
-
{ lockAlreadyHeld: true }
|
|
414
|
-
);
|
|
415
|
-
if (reconciled.status !== 'passed' && reconciled.status !== 'consumed') {
|
|
416
|
-
return {
|
|
417
|
-
code: 'EVENT_ARTIFACT_CONFLICT',
|
|
418
|
-
message: `review finalizer recovery remains ${reconciled.status} for ${artifact}`
|
|
419
|
-
};
|
|
420
|
-
}
|
|
421
|
-
intent = reconciled.intent;
|
|
422
|
-
}
|
|
423
|
-
if (!['passed', 'consumption-started', 'consumed'].includes(intent.state)) {
|
|
424
|
-
return {
|
|
425
|
-
code: 'EVENT_ARTIFACT_CONFLICT',
|
|
426
|
-
message: `review finalizer provenance is incomplete for ${artifact}`
|
|
427
|
-
};
|
|
428
|
-
}
|
|
429
|
-
const validated = validateCompletedArtifact(taskDir, FAMILY[family].artifact, artifact, round);
|
|
430
|
-
if (!validated.ok) {
|
|
431
|
-
return { code: 'EVENT_ARTIFACT_CONFLICT', message: validated.error.message };
|
|
432
|
-
}
|
|
433
|
-
const content = fs.readFileSync(validated.artifact.path, 'utf8');
|
|
434
|
-
if (intent.round !== round
|
|
435
|
-
|| intent.finalArtifactSha256 !== sha256Content(content)
|
|
436
|
-
|| intent.finalSemanticDigest !== canonicalSemanticDigest(content)) {
|
|
437
|
-
return {
|
|
438
|
-
code: 'EVENT_ARTIFACT_CONFLICT',
|
|
439
|
-
message: `review finalizer provenance does not match ${artifact}`
|
|
440
|
-
};
|
|
441
|
-
}
|
|
442
|
-
if (intent.state === 'passed' || intent.state === 'consumption-started') {
|
|
443
|
-
const consumed = { ...intent, state: 'consumed' as const, updatedAt: Date.now() };
|
|
444
|
-
writeArtifactRecoveryIntent(repoRoot, consumed, { expected: intent });
|
|
445
|
-
}
|
|
446
|
-
return null;
|
|
447
|
-
} catch (error) {
|
|
448
|
-
return {
|
|
449
|
-
code: 'EVENT_ARTIFACT_CONFLICT',
|
|
450
|
-
message: `review finalizer recovery could not be reconciled for ${artifact}: ${error instanceof Error ? error.message : String(error)}`
|
|
451
|
-
};
|
|
452
|
-
}
|
|
453
|
-
}
|
|
454
|
-
|
|
455
389
|
function relationForStartedInput(family: EventFamily, input: ArtifactIdentity): UpstreamRelation['relation'] {
|
|
456
390
|
if (family.startsWith('review-')) {
|
|
457
391
|
return input.family === reviewInputFamily(family) ? 'reviewed-input' : 'approval-context';
|
|
@@ -661,15 +595,25 @@ function reworkIntentMutationForCompletion(
|
|
|
661
595
|
}
|
|
662
596
|
|
|
663
597
|
function buildCompletionReceipt(
|
|
598
|
+
content: string,
|
|
664
599
|
taskDir: string,
|
|
665
600
|
family: EventFamily,
|
|
666
601
|
artifact: ArtifactIdentity,
|
|
667
602
|
completedAt: string,
|
|
668
603
|
frontmatter: Record<string, unknown>
|
|
669
604
|
): { ok: true; receipt: ArtifactReceipt } | { ok: false; message: string } | null {
|
|
605
|
+
const existing = (() => {
|
|
606
|
+
try {
|
|
607
|
+
return parseArtifactReceipts(content).rows.find((receipt) => receipt.output === artifact.name) ?? null;
|
|
608
|
+
} catch {
|
|
609
|
+
return null;
|
|
610
|
+
}
|
|
611
|
+
})();
|
|
670
612
|
if (family === 'code') {
|
|
671
|
-
const startedInput = typeof frontmatter.code_input_artifact === 'string'
|
|
672
|
-
|
|
613
|
+
const startedInput = typeof frontmatter.code_input_artifact === 'string'
|
|
614
|
+
? frontmatter.code_input_artifact : existing?.input ?? '';
|
|
615
|
+
const startedSha256 = typeof frontmatter.code_input_sha256 === 'string'
|
|
616
|
+
? frontmatter.code_input_sha256 : existing?.inputSha256 ?? '';
|
|
673
617
|
if (!startedInput || !startedSha256) return { ok: false, message: 'code.started plan input context is missing' };
|
|
674
618
|
const plan = inspectArtifactDirectory(taskDir, 'plan');
|
|
675
619
|
if (plan.status !== 'ready' || !plan.latest || plan.latest.name !== startedInput) {
|
|
@@ -682,7 +626,7 @@ function buildCompletionReceipt(
|
|
|
682
626
|
ok: true,
|
|
683
627
|
receipt: {
|
|
684
628
|
event: 'code.completed', output: artifact.name, input: startedInput,
|
|
685
|
-
inputSha256, completedAt
|
|
629
|
+
inputSha256, completedAt: existing?.completedAt ?? completedAt
|
|
686
630
|
}
|
|
687
631
|
};
|
|
688
632
|
} catch (error) {
|
|
@@ -691,8 +635,10 @@ function buildCompletionReceipt(
|
|
|
691
635
|
}
|
|
692
636
|
if (!family.startsWith('review-')) return null;
|
|
693
637
|
const expectedFamily = reviewInputFamily(family);
|
|
694
|
-
const startedInput = typeof frontmatter.review_input_artifact === 'string'
|
|
695
|
-
|
|
638
|
+
const startedInput = typeof frontmatter.review_input_artifact === 'string'
|
|
639
|
+
? frontmatter.review_input_artifact : existing?.input ?? '';
|
|
640
|
+
const startedSha256 = typeof frontmatter.review_input_sha256 === 'string'
|
|
641
|
+
? frontmatter.review_input_sha256 : existing?.inputSha256 ?? '';
|
|
696
642
|
if (!startedInput || !startedSha256) return { ok: false, message: 'review started input context is missing' };
|
|
697
643
|
const current = inspectArtifactDirectory(taskDir, expectedFamily);
|
|
698
644
|
if (current.status !== 'ready' || !current.latest || current.latest.name !== startedInput) {
|
|
@@ -710,7 +656,7 @@ function buildCompletionReceipt(
|
|
|
710
656
|
ok: true,
|
|
711
657
|
receipt: {
|
|
712
658
|
event, output: artifact.name, input: startedInput,
|
|
713
|
-
inputSha256, completedAt
|
|
659
|
+
inputSha256, completedAt: existing?.completedAt ?? completedAt
|
|
714
660
|
}
|
|
715
661
|
};
|
|
716
662
|
} catch (error) {
|
|
@@ -718,6 +664,69 @@ function buildCompletionReceipt(
|
|
|
718
664
|
}
|
|
719
665
|
}
|
|
720
666
|
|
|
667
|
+
type CompletionFact = Readonly<{
|
|
668
|
+
event: string;
|
|
669
|
+
output: string;
|
|
670
|
+
outputSha256: string;
|
|
671
|
+
semanticDigest: string;
|
|
672
|
+
requestId: string;
|
|
673
|
+
result: string;
|
|
674
|
+
}>;
|
|
675
|
+
|
|
676
|
+
function completionFacts(frontmatter: Record<string, unknown>): CompletionFact[] {
|
|
677
|
+
if (typeof frontmatter.completion_facts !== 'string') return [];
|
|
678
|
+
try {
|
|
679
|
+
const facts: unknown = JSON.parse(frontmatter.completion_facts);
|
|
680
|
+
if (!Array.isArray(facts)) return [];
|
|
681
|
+
return facts.filter((fact): fact is CompletionFact => Boolean(
|
|
682
|
+
fact && typeof fact === 'object' && !Array.isArray(fact)
|
|
683
|
+
&& typeof (fact as CompletionFact).event === 'string'
|
|
684
|
+
&& typeof (fact as CompletionFact).output === 'string'
|
|
685
|
+
&& /^[a-f0-9]{64}$/u.test((fact as CompletionFact).outputSha256)
|
|
686
|
+
&& /^[a-f0-9]{64}$/u.test((fact as CompletionFact).semanticDigest)
|
|
687
|
+
&& typeof (fact as CompletionFact).requestId === 'string'
|
|
688
|
+
&& typeof (fact as CompletionFact).result === 'string'
|
|
689
|
+
));
|
|
690
|
+
} catch {
|
|
691
|
+
return [];
|
|
692
|
+
}
|
|
693
|
+
}
|
|
694
|
+
|
|
695
|
+
function currentCompletionFact(request: TaskEventRequest, artifact: ArtifactIdentity): CompletionFact {
|
|
696
|
+
const content = fs.readFileSync(artifact.path, 'utf8');
|
|
697
|
+
return {
|
|
698
|
+
event: request.event,
|
|
699
|
+
output: artifact.name,
|
|
700
|
+
outputSha256: sha256File(artifact.path),
|
|
701
|
+
semanticDigest: canonicalSemanticDigest(content),
|
|
702
|
+
requestId: request.requestId ?? '',
|
|
703
|
+
result: JSON.stringify({
|
|
704
|
+
filesModified: request.filesModified,
|
|
705
|
+
testsPassed: request.testsPassed,
|
|
706
|
+
blockers: request.blockers,
|
|
707
|
+
major: request.major,
|
|
708
|
+
minor: request.minor,
|
|
709
|
+
manualValidation: request.manualValidation ?? 0,
|
|
710
|
+
verdict: request.verdict,
|
|
711
|
+
fixFor: request.fixFor,
|
|
712
|
+
implementationInput: request.implementationInput
|
|
713
|
+
})
|
|
714
|
+
};
|
|
715
|
+
}
|
|
716
|
+
|
|
717
|
+
function sameCompletionFact(left: CompletionFact, right: CompletionFact): boolean {
|
|
718
|
+
return left.event === right.event
|
|
719
|
+
&& left.output === right.output
|
|
720
|
+
&& left.outputSha256 === right.outputSha256
|
|
721
|
+
&& left.semanticDigest === right.semanticDigest
|
|
722
|
+
&& left.requestId === right.requestId
|
|
723
|
+
&& left.result === right.result;
|
|
724
|
+
}
|
|
725
|
+
|
|
726
|
+
function replaceCompletionFact(facts: readonly CompletionFact[], next: CompletionFact): CompletionFact[] {
|
|
727
|
+
return [...facts.filter((fact) => !(fact.event === next.event && fact.output === next.output)), next];
|
|
728
|
+
}
|
|
729
|
+
|
|
721
730
|
function applyTaskEventUnlocked(request: TaskEventRequest, options: TaskEventOptions = {}): TaskEventResult {
|
|
722
731
|
const invalid = validateTaskEventRequest(request);
|
|
723
732
|
if (invalid) return failed(request, invalid);
|
|
@@ -794,13 +803,11 @@ function applyTaskEventUnlocked(request: TaskEventRequest, options: TaskEventOpt
|
|
|
794
803
|
const currentStep = typeof frontmatter.current_step === 'string' ? frontmatter.current_step : '';
|
|
795
804
|
const matchingRows = rows.filter((item) => item.step === eventIdentity.action);
|
|
796
805
|
const manual = eventIdentity.family === 'manual-validation';
|
|
797
|
-
if (!manual && matchingRows.length > 1) return failed(normalized, { code: 'EVENT_LOG_CONFLICT', message: 'event identity appears more than once' }, { taskId: resolved.taskId, taskMdPath: resolved.taskMdPath, fromStep: currentStep, toStep: currentStep, action: eventIdentity.action, phase: eventIdentity.phase, artifactContext });
|
|
798
806
|
const openRows = matchingRows.filter((item) => item.started && !item.done);
|
|
799
807
|
const completedRows = matchingRows.filter((item) => item.done);
|
|
800
|
-
|
|
808
|
+
if (!manual && openRows.length > 1) return failed(normalized, { code: 'EVENT_LOG_CONFLICT', message: 'event identity has more than one open attempt' }, { taskId: resolved.taskId, taskMdPath: resolved.taskMdPath, fromStep: currentStep, toStep: currentStep, action: eventIdentity.action, phase: eventIdentity.phase, artifactContext });
|
|
809
|
+
const row = openRows.at(-1);
|
|
801
810
|
let completedArtifact: ArtifactIdentity | null = null;
|
|
802
|
-
let localFinalizationIntent: LocalArtifactFinalizationIntent | null = null;
|
|
803
|
-
let reviewFinalizationIntent: ArtifactRecoveryIntent | null = null;
|
|
804
811
|
let reviewContent: string | null = null;
|
|
805
812
|
if (eventIdentity.phase === 'started' && row) {
|
|
806
813
|
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 });
|
|
@@ -812,24 +819,7 @@ function applyTaskEventUnlocked(request: TaskEventRequest, options: TaskEventOpt
|
|
|
812
819
|
}
|
|
813
820
|
return successNoOp(normalized, resolved.taskId, resolved.taskMdPath, currentStep, eventIdentity, row.started, frontmatter, artifactContext);
|
|
814
821
|
}
|
|
815
|
-
if (eventIdentity.phase === '
|
|
816
|
-
const done = completedRows.find((item) => item.note === eventIdentity.note);
|
|
817
|
-
if (eventIdentity.phase === 'completed' && done) {
|
|
818
|
-
if (!normalized.dryRun && eventIdentity.family.startsWith('review-') && normalized.artifact && normalized.round) {
|
|
819
|
-
const recoveryError = reconcileReviewCompletionReplay(
|
|
820
|
-
resolved.repoRoot,
|
|
821
|
-
resolved.taskDir,
|
|
822
|
-
resolved.taskId,
|
|
823
|
-
eventIdentity.family,
|
|
824
|
-
normalized.artifact,
|
|
825
|
-
normalized.round
|
|
826
|
-
);
|
|
827
|
-
if (recoveryError) return failed(normalized, recoveryError, { taskId: resolved.taskId, taskMdPath: resolved.taskMdPath, fromStep: currentStep, toStep: currentStep, action: eventIdentity.action, phase: eventIdentity.phase });
|
|
828
|
-
}
|
|
829
|
-
return successNoOp(normalized, resolved.taskId, resolved.taskMdPath, currentStep, eventIdentity, done.done, frontmatter, artifactContext);
|
|
830
|
-
}
|
|
831
|
-
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 });
|
|
832
|
-
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 });
|
|
822
|
+
if (eventIdentity.phase === 'completed' && !row?.started && completedRows.length === 0 && !allowsManualOverride(options.manualOverride, 'task-event', 'EVENT_START_MISSING')) return failed(normalized, { code: 'EVENT_START_MISSING', message: 'completion requires a started event or an earlier completed attempt' }, { taskId: resolved.taskId, taskMdPath: resolved.taskMdPath, fromStep: currentStep, toStep: currentStep, action: eventIdentity.action, phase: eventIdentity.phase });
|
|
833
823
|
if (eventIdentity.phase === 'completed') {
|
|
834
824
|
const validated = validateCompletedArtifact(resolved.taskDir, FAMILY[eventIdentity.family].artifact, normalized.artifact!, normalized.round);
|
|
835
825
|
if (!validated.ok) return failed(normalized, validated.error, { taskId: resolved.taskId, taskMdPath: resolved.taskMdPath, fromStep: currentStep, toStep: currentStep, action: eventIdentity.action, phase: eventIdentity.phase });
|
|
@@ -870,23 +860,6 @@ function applyTaskEventUnlocked(request: TaskEventRequest, options: TaskEventOpt
|
|
|
870
860
|
const localFamily = eventIdentity.family === 'analyze'
|
|
871
861
|
? 'analysis'
|
|
872
862
|
: eventIdentity.family === 'plan' ? 'plan' : 'code';
|
|
873
|
-
try {
|
|
874
|
-
localFinalizationIntent = readLocalArtifactFinalizationIntent(
|
|
875
|
-
resolved.repoRoot, resolved.taskId, localFamily, completedArtifact.name
|
|
876
|
-
);
|
|
877
|
-
if (localFinalizationIntent?.state === 'commit-started') {
|
|
878
|
-
const reconciled = reconcileArtifactRecovery(
|
|
879
|
-
recoveryContextFromIntent(resolved.repoRoot, resolved.taskDir, localFinalizationIntent),
|
|
880
|
-
{ lockAlreadyHeld: true }
|
|
881
|
-
);
|
|
882
|
-
if (reconciled.status === 'passed' || reconciled.status === 'consumed') localFinalizationIntent = reconciled.intent;
|
|
883
|
-
}
|
|
884
|
-
} catch (error) {
|
|
885
|
-
return failed(normalized, {
|
|
886
|
-
code: 'EVENT_ARTIFACT_CONFLICT',
|
|
887
|
-
message: `local finalizer recovery could not be reconciled for ${completedArtifact.name}: ${error instanceof Error ? error.message : String(error)}`
|
|
888
|
-
}, { taskId: resolved.taskId, taskMdPath: resolved.taskMdPath, fromStep: currentStep, toStep: currentStep, action: eventIdentity.action, phase: eventIdentity.phase });
|
|
889
|
-
}
|
|
890
863
|
let artifactContent: string;
|
|
891
864
|
try { artifactContent = fs.readFileSync(completedArtifact.path, 'utf8'); }
|
|
892
865
|
catch (error) {
|
|
@@ -916,29 +889,6 @@ function applyTaskEventUnlocked(request: TaskEventRequest, options: TaskEventOpt
|
|
|
916
889
|
message: `semantic digest does not match finalizer result for ${completedArtifact.name}`
|
|
917
890
|
}, { taskId: resolved.taskId, taskMdPath: resolved.taskMdPath, fromStep: currentStep, toStep: currentStep, action: eventIdentity.action, phase: eventIdentity.phase });
|
|
918
891
|
}
|
|
919
|
-
if (!localFinalizationIntent || !['passed', 'consumed'].includes(localFinalizationIntent.state)) {
|
|
920
|
-
return failed(normalized, {
|
|
921
|
-
code: 'EVENT_ARTIFACT_CONFLICT',
|
|
922
|
-
message: `local finalizer provenance is missing or incomplete for ${completedArtifact.name}`
|
|
923
|
-
}, { taskId: resolved.taskId, taskMdPath: resolved.taskMdPath, fromStep: currentStep, toStep: currentStep, action: eventIdentity.action, phase: eventIdentity.phase });
|
|
924
|
-
}
|
|
925
|
-
if (localFinalizationIntent.finalArtifactSha256 !== actualSha256 || localFinalizationIntent.finalSemanticDigest !== local.semanticDigest) {
|
|
926
|
-
return failed(normalized, {
|
|
927
|
-
code: 'EVENT_ARTIFACT_CONFLICT',
|
|
928
|
-
message: `local finalizer provenance does not match ${completedArtifact.name}`
|
|
929
|
-
}, { taskId: resolved.taskId, taskMdPath: resolved.taskMdPath, fromStep: currentStep, toStep: currentStep, action: eventIdentity.action, phase: eventIdentity.phase });
|
|
930
|
-
}
|
|
931
|
-
if (localFinalizationIntent.authorityDigest !== null) {
|
|
932
|
-
if (!lifecycleAuthority
|
|
933
|
-
|| localFinalizationIntent.recoveryOperationId !== lifecycleAuthority.operationId
|
|
934
|
-
|| localFinalizationIntent.requestId !== lifecycleAuthority.lifecycleRequestId
|
|
935
|
-
|| localFinalizationIntent.authorityDigest !== lifecycleRecoveryAttestationDigest(lifecycleAuthority)) {
|
|
936
|
-
return failed(normalized, {
|
|
937
|
-
code: 'EVENT_ARTIFACT_CONFLICT',
|
|
938
|
-
message: `local finalizer provenance does not match lifecycle completion authority for ${completedArtifact.name}`
|
|
939
|
-
}, { taskId: resolved.taskId, taskMdPath: resolved.taskMdPath, fromStep: currentStep, toStep: currentStep, action: eventIdentity.action, phase: eventIdentity.phase, artifactContext });
|
|
940
|
-
}
|
|
941
|
-
}
|
|
942
892
|
}
|
|
943
893
|
}
|
|
944
894
|
if (eventIdentity.phase === 'waiting' && section.entries.some((entry) => entry.step === eventIdentity.action && entry.note === eventIdentity.note)) {
|
|
@@ -965,44 +915,6 @@ function applyTaskEventUnlocked(request: TaskEventRequest, options: TaskEventOpt
|
|
|
965
915
|
completedArtifact?.path ?? null
|
|
966
916
|
);
|
|
967
917
|
if (findingCountError) return failed(normalized, findingCountError, { taskId: resolved.taskId, taskMdPath: resolved.taskMdPath, fromStep: currentStep, toStep: currentStep, action: eventIdentity.action, phase: eventIdentity.phase });
|
|
968
|
-
if (eventIdentity.phase === 'completed' && eventIdentity.family.startsWith('review-') && completedArtifact && reviewContent !== null) {
|
|
969
|
-
try {
|
|
970
|
-
reviewFinalizationIntent = readArtifactRecoveryIntent(
|
|
971
|
-
resolved.repoRoot,
|
|
972
|
-
resolved.taskId,
|
|
973
|
-
eventIdentity.family as 'review-analysis' | 'review-plan' | 'review-code',
|
|
974
|
-
completedArtifact.name
|
|
975
|
-
);
|
|
976
|
-
if (!normalized.dryRun && reviewFinalizationIntent?.state === 'commit-started') {
|
|
977
|
-
const reconciled = reconcileArtifactRecovery(
|
|
978
|
-
recoveryContextFromIntent(resolved.repoRoot, resolved.taskDir, reviewFinalizationIntent),
|
|
979
|
-
{ lockAlreadyHeld: true }
|
|
980
|
-
);
|
|
981
|
-
if (reconciled.status === 'passed' || reconciled.status === 'consumed') reviewFinalizationIntent = reconciled.intent;
|
|
982
|
-
}
|
|
983
|
-
if (!reviewFinalizationIntent || !['passed', 'consumption-started', 'consumed'].includes(reviewFinalizationIntent.state)) {
|
|
984
|
-
return failed(normalized, {
|
|
985
|
-
code: 'EVENT_ARTIFACT_CONFLICT',
|
|
986
|
-
message: `review finalizer provenance is missing or incomplete for ${completedArtifact.name}`
|
|
987
|
-
}, { taskId: resolved.taskId, taskMdPath: resolved.taskMdPath, fromStep: currentStep, toStep: currentStep, action: eventIdentity.action, phase: eventIdentity.phase });
|
|
988
|
-
}
|
|
989
|
-
const actualSha256 = sha256File(completedArtifact.path);
|
|
990
|
-
const actualSemanticDigest = canonicalSemanticDigest(reviewContent);
|
|
991
|
-
if (reviewFinalizationIntent.round !== normalized.round
|
|
992
|
-
|| reviewFinalizationIntent.finalArtifactSha256 !== actualSha256
|
|
993
|
-
|| reviewFinalizationIntent.finalSemanticDigest !== actualSemanticDigest) {
|
|
994
|
-
return failed(normalized, {
|
|
995
|
-
code: 'EVENT_ARTIFACT_CONFLICT',
|
|
996
|
-
message: `review finalizer provenance does not match ${completedArtifact.name}`
|
|
997
|
-
}, { taskId: resolved.taskId, taskMdPath: resolved.taskMdPath, fromStep: currentStep, toStep: currentStep, action: eventIdentity.action, phase: eventIdentity.phase });
|
|
998
|
-
}
|
|
999
|
-
} catch (error) {
|
|
1000
|
-
return failed(normalized, {
|
|
1001
|
-
code: 'EVENT_ARTIFACT_CONFLICT',
|
|
1002
|
-
message: `review finalizer recovery could not be reconciled for ${completedArtifact.name}: ${error instanceof Error ? error.message : String(error)}`
|
|
1003
|
-
}, { taskId: resolved.taskId, taskMdPath: resolved.taskMdPath, fromStep: currentStep, toStep: currentStep, action: eventIdentity.action, phase: eventIdentity.phase });
|
|
1004
|
-
}
|
|
1005
|
-
}
|
|
1006
918
|
let orchestrationCompletion: OrchestrationStageCompletion | null = null;
|
|
1007
919
|
if (eventIdentity.phase === 'completed' && eventIdentity.family !== 'manual-validation' && eventIdentity.family !== 'validation-run') {
|
|
1008
920
|
const orchestrationStage = eventIdentity.family === 'analyze' ? 'analysis' : eventIdentity.family;
|
|
@@ -1029,14 +941,32 @@ function applyTaskEventUnlocked(request: TaskEventRequest, options: TaskEventOpt
|
|
|
1029
941
|
try { metadata = (options.metadataProvider ?? captureTaskWriteMetadata)(); }
|
|
1030
942
|
catch (error) { return failed(normalized, { code: 'METADATA_CAPTURE_FAILED', message: String(error) }, { taskId: resolved.taskId, taskMdPath: resolved.taskMdPath }); }
|
|
1031
943
|
let completionReceipt: ArtifactReceipt | null = null;
|
|
944
|
+
let currentFact: CompletionFact | null = null;
|
|
1032
945
|
let completionInvalidation: ReturnType<typeof invalidationMutation> | null = null;
|
|
1033
946
|
let completionRework: ReturnType<typeof reworkIntentMutation> | null = null;
|
|
1034
947
|
if (eventIdentity.phase === 'completed' && completedArtifact) {
|
|
1035
|
-
const receipt = buildCompletionReceipt(resolved.taskDir, eventIdentity.family, completedArtifact, metadata.timestamp, frontmatter);
|
|
948
|
+
const receipt = buildCompletionReceipt(content, resolved.taskDir, eventIdentity.family, completedArtifact, metadata.timestamp, frontmatter);
|
|
1036
949
|
if (receipt && !receipt.ok) {
|
|
1037
950
|
return failed(normalized, { code: 'EVENT_ARTIFACT_CONFLICT', message: receipt.message }, { taskId: resolved.taskId, taskMdPath: resolved.taskMdPath, fromStep: currentStep, toStep: currentStep, action: eventIdentity.action, phase: eventIdentity.phase });
|
|
1038
951
|
}
|
|
1039
952
|
completionReceipt = receipt?.receipt ?? null;
|
|
953
|
+
try {
|
|
954
|
+
currentFact = currentCompletionFact(normalized, completedArtifact);
|
|
955
|
+
} catch (error) {
|
|
956
|
+
return failed(normalized, { code: 'EVENT_ARTIFACT_CONFLICT', message: `cannot inspect current completion result: ${error instanceof Error ? error.message : String(error)}` }, { taskId: resolved.taskId, taskMdPath: resolved.taskMdPath });
|
|
957
|
+
}
|
|
958
|
+
if (completionFacts(frontmatter).some((fact) => sameCompletionFact(fact, currentFact!))) {
|
|
959
|
+
return successNoOp(
|
|
960
|
+
normalized,
|
|
961
|
+
resolved.taskId,
|
|
962
|
+
resolved.taskMdPath,
|
|
963
|
+
currentStep,
|
|
964
|
+
eventIdentity,
|
|
965
|
+
completionReceipt?.completedAt ?? metadata.timestamp,
|
|
966
|
+
frontmatter,
|
|
967
|
+
artifactContext
|
|
968
|
+
);
|
|
969
|
+
}
|
|
1040
970
|
try {
|
|
1041
971
|
const invalidation = invalidationMutationForCompletion(content, resolved.taskDir, eventIdentity.family, completedArtifact, metadata.timestamp);
|
|
1042
972
|
if ('error' in invalidation) return failed(normalized, { code: 'EVENT_ARTIFACT_CONFLICT', message: invalidation.error }, { taskId: resolved.taskId, taskMdPath: resolved.taskMdPath, fromStep: currentStep, toStep: currentStep, action: eventIdentity.action, phase: eventIdentity.phase });
|
|
@@ -1052,6 +982,7 @@ function applyTaskEventUnlocked(request: TaskEventRequest, options: TaskEventOpt
|
|
|
1052
982
|
const logStep = eventIdentity.phase === 'started' ? `${eventIdentity.action} [started]` : eventIdentity.action;
|
|
1053
983
|
const body = appendActivityEntry(section, { time: metadata.timestamp, step: logStep, agent: normalized.agent, note: eventIdentity.note });
|
|
1054
984
|
const frontmatterSet: Record<string, string> = { current_step: step, assigned_to: normalized.agent };
|
|
985
|
+
if (currentFact) frontmatterSet.completion_facts = JSON.stringify(replaceCompletionFact(completionFacts(frontmatter), currentFact));
|
|
1055
986
|
let frontmatterRemove: string[] | undefined;
|
|
1056
987
|
if (eventIdentity.phase === 'started' && artifactContext) {
|
|
1057
988
|
try {
|
|
@@ -1127,90 +1058,10 @@ function applyTaskEventUnlocked(request: TaskEventRequest, options: TaskEventOpt
|
|
|
1127
1058
|
});
|
|
1128
1059
|
}
|
|
1129
1060
|
mutations.push({ kind: 'section', aliases: ['活动日志', 'Activity Log'], heading: section.heading, body });
|
|
1130
|
-
if (!normalized.dryRun && localFinalizationIntent && completedArtifact) {
|
|
1131
|
-
try {
|
|
1132
|
-
if (lifecycleAuthority && localFinalizationIntent.state === 'passed') {
|
|
1133
|
-
const expectedIntent = localFinalizationIntent;
|
|
1134
|
-
localFinalizationIntent = {
|
|
1135
|
-
...localFinalizationIntent,
|
|
1136
|
-
state: 'consumption-started',
|
|
1137
|
-
phase: 'task-event.completed',
|
|
1138
|
-
authorityDigest: lifecycleRecoveryAttestationDigest(lifecycleAuthority),
|
|
1139
|
-
requestId: lifecycleAuthority.lifecycleRequestId,
|
|
1140
|
-
updatedAt: Date.now()
|
|
1141
|
-
};
|
|
1142
|
-
writeArtifactRecoveryIntent(resolved.repoRoot, localFinalizationIntent, { expected: expectedIntent });
|
|
1143
|
-
} else if (!lifecycleAuthority && localFinalizationIntent.state === 'passed') {
|
|
1144
|
-
localFinalizationIntent = consumeLocalArtifactFinalizationIntent(resolved.repoRoot, localFinalizationIntent, { lockAlreadyHeld: options.lockAlreadyHeld });
|
|
1145
|
-
}
|
|
1146
|
-
} catch (error) {
|
|
1147
|
-
return failed(normalized, {
|
|
1148
|
-
code: 'EVENT_ARTIFACT_CONFLICT',
|
|
1149
|
-
message: `local finalizer provenance could not be advanced before task write for ${completedArtifact.name}: ${error instanceof Error ? error.message : String(error)}`
|
|
1150
|
-
}, {
|
|
1151
|
-
taskId: resolved.taskId,
|
|
1152
|
-
taskMdPath: resolved.taskMdPath,
|
|
1153
|
-
fromStep: currentStep,
|
|
1154
|
-
toStep: step,
|
|
1155
|
-
action: eventIdentity.action,
|
|
1156
|
-
phase: eventIdentity.phase,
|
|
1157
|
-
artifactContext
|
|
1158
|
-
});
|
|
1159
|
-
}
|
|
1160
|
-
}
|
|
1161
|
-
if (!normalized.dryRun && reviewFinalizationIntent && completedArtifact && reviewFinalizationIntent.state === 'passed') {
|
|
1162
|
-
try {
|
|
1163
|
-
const expectedIntent = reviewFinalizationIntent;
|
|
1164
|
-
reviewFinalizationIntent = {
|
|
1165
|
-
...reviewFinalizationIntent,
|
|
1166
|
-
state: 'consumption-started',
|
|
1167
|
-
phase: 'task-event.completed',
|
|
1168
|
-
updatedAt: Date.now()
|
|
1169
|
-
};
|
|
1170
|
-
writeArtifactRecoveryIntent(resolved.repoRoot, reviewFinalizationIntent, { expected: expectedIntent });
|
|
1171
|
-
} catch (error) {
|
|
1172
|
-
return failed(normalized, {
|
|
1173
|
-
code: 'EVENT_ARTIFACT_CONFLICT',
|
|
1174
|
-
message: `review finalizer provenance could not be advanced before task write for ${completedArtifact.name}: ${error instanceof Error ? error.message : String(error)}`
|
|
1175
|
-
}, {
|
|
1176
|
-
taskId: resolved.taskId,
|
|
1177
|
-
taskMdPath: resolved.taskMdPath,
|
|
1178
|
-
fromStep: currentStep,
|
|
1179
|
-
toStep: step,
|
|
1180
|
-
action: eventIdentity.action,
|
|
1181
|
-
phase: eventIdentity.phase,
|
|
1182
|
-
artifactContext
|
|
1183
|
-
});
|
|
1184
|
-
}
|
|
1185
|
-
}
|
|
1186
1061
|
const sourceCompletion = eventIdentity.phase === 'completed'
|
|
1187
1062
|
&& ['analyze', 'plan', 'code'].includes(eventIdentity.family);
|
|
1188
1063
|
const result = writeTask({ taskRef: normalized.taskRef, expectedState: stateOverride ? resolved.state : 'active', dryRun: normalized.dryRun, mutations }, { ...options, invalidationContext: sourceCompletion ? 'source-completion' : 'standard', metadataProvider: () => metadata });
|
|
1189
1064
|
if (result.status === 'failed') return failed(normalized, result.error, { 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 });
|
|
1190
|
-
if (!normalized.dryRun && lifecycleAuthority && localFinalizationIntent && completedArtifact) {
|
|
1191
|
-
try {
|
|
1192
|
-
if (!options.deferLifecycleRecoveryConsumption) consumeLifecycleRecoveryAttestation(lifecycleAuthority);
|
|
1193
|
-
writeArtifactRecoveryIntent(resolved.repoRoot, { ...localFinalizationIntent, state: 'consumed', updatedAt: Date.now() }, { expected: localFinalizationIntent });
|
|
1194
|
-
} catch (error) {
|
|
1195
|
-
return failed(normalized, {
|
|
1196
|
-
code: 'EVENT_ARTIFACT_CONFLICT',
|
|
1197
|
-
message: `task completion was written but lifecycle recovery consumption is incomplete: ${error instanceof Error ? error.message : String(error)}`
|
|
1198
|
-
}, { 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 });
|
|
1199
|
-
}
|
|
1200
|
-
}
|
|
1201
|
-
if (!normalized.dryRun && reviewFinalizationIntent && completedArtifact) {
|
|
1202
|
-
try {
|
|
1203
|
-
if (reviewFinalizationIntent.state === 'consumption-started') {
|
|
1204
|
-
const consumed = { ...reviewFinalizationIntent, state: 'consumed' as const, updatedAt: Date.now() };
|
|
1205
|
-
writeArtifactRecoveryIntent(resolved.repoRoot, consumed, { expected: reviewFinalizationIntent });
|
|
1206
|
-
}
|
|
1207
|
-
} catch (error) {
|
|
1208
|
-
return failed(normalized, {
|
|
1209
|
-
code: 'EVENT_ARTIFACT_CONFLICT',
|
|
1210
|
-
message: `review finalizer recovery consumption is incomplete: ${error instanceof Error ? error.message : String(error)}`
|
|
1211
|
-
}, { 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 });
|
|
1212
|
-
}
|
|
1213
|
-
}
|
|
1214
1065
|
if (!normalized.dryRun && orchestrationCompletion) {
|
|
1215
1066
|
try {
|
|
1216
1067
|
(options.commitOrchestrationCompletion ?? commitOrchestrationStageCompletion)(orchestrationCompletion);
|
|
@@ -5,7 +5,8 @@ export type TaskControlBindingEvidence = (taskId: string, binding: TaskControlBi
|
|
|
5
5
|
|
|
6
6
|
export function taskFinalizationReceiptState(receipt: TaskFinalizationReceipt): 'pending' | 'unresolved' | 'complete' {
|
|
7
7
|
if (receipt.lifecycle !== 'done' || receipt.taskComment === 'pending'
|
|
8
|
-
|| receipt.verification === 'pending' || receipt.
|
|
8
|
+
|| receipt.verification === 'pending' || receipt.summary === 'pending'
|
|
9
|
+
|| receipt.warningProjection !== 'done') return 'pending';
|
|
9
10
|
if (receipt.lastError !== null || receipt.warnings.some((warning) => warning.status === 'open')) return 'unresolved';
|
|
10
11
|
return 'complete';
|
|
11
12
|
}
|