@fitlab-ai/agent-infra 0.9.15 → 0.10.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/lib/agent-clients/adapters/codex-lifecycle/sandbox-controller.js +6 -2
- package/dist/lib/agent-clients/adapters/codex-lifecycle/store.js +43 -3
- package/dist/lib/fs/durable-write.js +2 -0
- package/dist/lib/internal/cli-route-inventory.js +1 -1
- package/dist/lib/internal/manual-validation.js +17 -8
- package/dist/lib/internal/task-artifact.js +1 -1
- package/dist/lib/internal/task-lifecycle.js +2 -2
- package/dist/lib/internal/task-operation-registry.js +0 -1
- package/dist/lib/internal/task-review.js +4 -4
- package/dist/lib/platform/issue-comments.js +16 -10
- package/dist/lib/platform/issues.js +1 -1
- package/dist/lib/platform/pr-change-report.js +5 -4
- package/dist/lib/platform/pr-summary.js +64 -32
- package/dist/lib/platform/pull-requests.js +3 -3
- package/dist/lib/platform/task-identities.js +2 -9
- package/dist/lib/platform/verification-sync.js +6 -2
- package/dist/lib/pr-review/evidence-grading.js +4 -8
- package/dist/lib/sandbox/commands/rm.js +21 -2
- package/dist/lib/sandbox/control/server.js +29 -9
- package/dist/lib/sandbox/control/state.js +7 -1
- package/dist/lib/sandbox/control/workflow-executor.js +4 -14
- package/dist/lib/sandbox/index.js +1 -2
- package/dist/lib/sandbox/removal.js +116 -1011
- package/dist/lib/sandbox/workspace-identity.js +17 -1
- package/dist/lib/task/artifact-command.js +6 -28
- package/dist/lib/task/artifact-operations.js +16 -230
- package/dist/lib/task/artifact-recovery.js +430 -0
- package/dist/lib/task/artifact-repair-intent.js +73 -39
- package/dist/lib/task/commit-operation.js +31 -4
- package/dist/lib/task/control-authority.js +38 -7
- package/dist/lib/task/control-recovery.js +14 -2
- package/dist/lib/task/create-service.js +1 -8
- package/dist/lib/task/delegation-receipts.js +64 -3
- package/dist/lib/task/events.js +164 -22
- package/dist/lib/task/human-override.js +5 -8
- package/dist/lib/task/intermediate-cleanup.js +66 -8
- package/dist/lib/task/lifecycle-recovery.js +549 -0
- package/dist/lib/task/lifecycle.js +12 -3
- package/dist/lib/task/local-artifact-finalization.js +148 -123
- package/dist/lib/task/manual-validation-receipt.js +5 -1
- package/dist/lib/task/orchestration.js +52 -2
- package/dist/lib/task/pr-delivery-fact.js +12 -56
- package/dist/lib/task/recovery-warning.js +19 -0
- package/dist/lib/task/review-command.js +1 -0
- package/dist/lib/task/review-finalization.js +104 -77
- package/dist/lib/task/short-id.js +15 -7
- package/dist/lib/task/task-execution-lock.js +1 -1
- package/dist/lib/task/verification-engine.js +3 -14
- package/dist/lib/task/workflow-command.js +0 -1
- package/dist/lib/version.js +1 -11
- package/dist/lifecycle-build-manifest.json +3 -3
- package/lib/agent-clients/adapters/codex-lifecycle/sandbox-controller.ts +7 -2
- package/lib/agent-clients/adapters/codex-lifecycle/store.ts +55 -3
- package/lib/fs/durable-write.ts +1 -0
- package/lib/internal/cli-route-inventory.ts +1 -1
- package/lib/internal/manual-validation.ts +17 -8
- package/lib/internal/task-artifact.ts +1 -1
- package/lib/internal/task-lifecycle.ts +4 -3
- package/lib/internal/task-operation-registry.ts +0 -1
- package/lib/internal/task-review.ts +4 -4
- package/lib/platform/issue-comments.ts +18 -10
- package/lib/platform/issues.ts +1 -1
- package/lib/platform/pr-change-report.ts +7 -5
- package/lib/platform/pr-summary.ts +64 -32
- package/lib/platform/pull-requests.ts +3 -3
- package/lib/platform/task-identities.ts +3 -14
- package/lib/platform/verification-sync.ts +6 -2
- package/lib/pr-review/evidence-grading.ts +4 -9
- package/lib/sandbox/commands/rm.ts +34 -2
- package/lib/sandbox/control/protocol.ts +5 -0
- package/lib/sandbox/control/server.ts +30 -10
- package/lib/sandbox/control/state.ts +9 -1
- package/lib/sandbox/control/workflow-executor.ts +4 -14
- package/lib/sandbox/index.ts +1 -2
- package/lib/sandbox/removal.ts +126 -1128
- package/lib/sandbox/workspace-identity.ts +19 -3
- package/lib/task/artifact-command.ts +8 -27
- package/lib/task/artifact-operations.ts +21 -251
- package/lib/task/artifact-recovery.ts +519 -0
- package/lib/task/artifact-repair-intent.ts +99 -51
- package/lib/task/commit-operation.ts +35 -4
- package/lib/task/control-authority.ts +47 -14
- package/lib/task/control-recovery.ts +13 -1
- package/lib/task/create-service.ts +1 -10
- package/lib/task/delegation-receipts.ts +73 -2
- package/lib/task/events.ts +187 -23
- package/lib/task/human-override.ts +5 -8
- package/lib/task/intermediate-cleanup.ts +50 -12
- package/lib/task/lifecycle-recovery.ts +691 -0
- package/lib/task/lifecycle.ts +13 -4
- package/lib/task/local-artifact-finalization.ts +176 -132
- package/lib/task/manual-validation-receipt.ts +5 -1
- package/lib/task/orchestration.ts +65 -0
- package/lib/task/pr-delivery-fact.ts +15 -62
- package/lib/task/recovery-warning.ts +21 -0
- package/lib/task/review-command.ts +1 -0
- package/lib/task/review-finalization.ts +106 -87
- package/lib/task/short-id.ts +27 -5
- package/lib/task/task-execution-lock.ts +1 -1
- package/lib/task/verification-engine.ts +3 -17
- package/lib/task/verification-types.ts +0 -2
- package/lib/task/workflow-command.ts +0 -1
- package/lib/version.ts +1 -15
- package/package.json +1 -1
- package/templates/.agents/rules/create-issue.en.md +1 -1
- package/templates/.agents/rules/create-issue.zh-CN.md +1 -1
- package/templates/.agents/rules/issue-sync.en.md +4 -2
- package/templates/.agents/rules/issue-sync.zh-CN.md +4 -3
- package/templates/.agents/rules/local-artifact-repair.en.md +46 -39
- package/templates/.agents/rules/local-artifact-repair.zh-CN.md +43 -36
- package/templates/.agents/skills/analyze-task/SKILL.en.md +6 -6
- package/templates/.agents/skills/analyze-task/SKILL.zh-CN.md +6 -6
- package/templates/.agents/skills/analyze-task/config/verify.en.json +1 -1
- package/templates/.agents/skills/analyze-task/config/verify.zh-CN.json +1 -1
- package/templates/.agents/skills/block-task/SKILL.en.md +2 -2
- package/templates/.agents/skills/block-task/SKILL.zh-CN.md +2 -2
- package/templates/.agents/skills/block-task/config/verify.json +1 -1
- package/templates/.agents/skills/cancel-task/SKILL.en.md +3 -3
- package/templates/.agents/skills/cancel-task/SKILL.zh-CN.md +3 -3
- package/templates/.agents/skills/cancel-task/config/verify.json +1 -1
- package/templates/.agents/skills/code-task/SKILL.en.md +7 -7
- package/templates/.agents/skills/code-task/SKILL.zh-CN.md +7 -7
- package/templates/.agents/skills/code-task/config/verify.en.json +1 -1
- package/templates/.agents/skills/code-task/config/verify.zh-CN.json +1 -1
- package/templates/.agents/skills/commit/reference/issue-metadata-sync.en.md +1 -1
- package/templates/.agents/skills/commit/reference/issue-metadata-sync.zh-CN.md +1 -1
- package/templates/.agents/skills/complete-manual-validation/SKILL.en.md +1 -1
- package/templates/.agents/skills/complete-manual-validation/SKILL.zh-CN.md +1 -1
- package/templates/.agents/skills/complete-task/SKILL.en.md +2 -2
- package/templates/.agents/skills/complete-task/SKILL.zh-CN.md +2 -2
- package/templates/.agents/skills/complete-task/config/verify.en.json +2 -2
- package/templates/.agents/skills/complete-task/config/verify.zh-CN.json +2 -2
- package/templates/.agents/skills/complete-task/reference/external-delivery.en.md +1 -1
- package/templates/.agents/skills/complete-task/reference/external-delivery.zh-CN.md +1 -1
- package/templates/.agents/skills/create-pr/SKILL.en.md +2 -2
- package/templates/.agents/skills/create-pr/SKILL.zh-CN.md +2 -2
- package/templates/.agents/skills/create-pr/config/verify.json +1 -1
- package/templates/.agents/skills/create-pr/reference/pr-body-template.en.md +2 -2
- package/templates/.agents/skills/create-pr/reference/pr-body-template.zh-CN.md +2 -2
- package/templates/.agents/skills/create-task/SKILL.en.md +5 -5
- package/templates/.agents/skills/create-task/SKILL.zh-CN.md +5 -5
- package/templates/.agents/skills/create-task/config/verify.json +1 -1
- package/templates/.agents/skills/import-issue/SKILL.en.md +8 -8
- package/templates/.agents/skills/import-issue/SKILL.zh-CN.md +8 -8
- package/templates/.agents/skills/import-issue/config/verify.json +2 -2
- package/templates/.agents/skills/plan-task/SKILL.en.md +4 -4
- package/templates/.agents/skills/plan-task/SKILL.zh-CN.md +4 -4
- package/templates/.agents/skills/plan-task/config/verify.en.json +1 -1
- package/templates/.agents/skills/plan-task/config/verify.zh-CN.json +1 -1
- package/templates/.agents/skills/restore-task/SKILL.en.md +2 -2
- package/templates/.agents/skills/restore-task/SKILL.zh-CN.md +2 -2
- package/templates/.agents/skills/restore-task/config/verify.json +1 -1
- package/templates/.agents/skills/review-analysis/SKILL.en.md +2 -2
- package/templates/.agents/skills/review-analysis/SKILL.zh-CN.md +2 -2
- package/templates/.agents/skills/review-analysis/config/verify.en.json +1 -1
- package/templates/.agents/skills/review-analysis/config/verify.zh-CN.json +1 -1
- package/templates/.agents/skills/review-analysis/reference/report-template.en.md +7 -7
- package/templates/.agents/skills/review-analysis/reference/report-template.zh-CN.md +6 -6
- package/templates/.agents/skills/review-code/SKILL.en.md +2 -2
- package/templates/.agents/skills/review-code/SKILL.zh-CN.md +2 -2
- package/templates/.agents/skills/review-code/config/verify.en.json +1 -1
- package/templates/.agents/skills/review-code/config/verify.zh-CN.json +1 -1
- package/templates/.agents/skills/review-code/reference/report-template.en.md +12 -12
- package/templates/.agents/skills/review-code/reference/report-template.zh-CN.md +12 -12
- package/templates/.agents/skills/review-plan/SKILL.en.md +2 -2
- package/templates/.agents/skills/review-plan/SKILL.zh-CN.md +2 -2
- package/templates/.agents/skills/review-plan/config/verify.en.json +1 -1
- package/templates/.agents/skills/review-plan/config/verify.zh-CN.json +1 -1
- package/templates/.agents/skills/review-plan/reference/report-template.en.md +7 -7
- package/templates/.agents/skills/review-plan/reference/report-template.zh-CN.md +7 -7
- package/templates/.agents/skills/review-pr/config/verify.en.json +1 -1
- package/templates/.agents/skills/review-pr/config/verify.zh-CN.json +1 -1
- package/templates/.agents/skills/review-pr/reference/evidence-grading.en.md +3 -3
- package/templates/.agents/skills/review-pr/reference/evidence-grading.zh-CN.md +3 -3
- package/templates/.agents/skills/run-manual-validation/config/verify.en.json +1 -1
- package/templates/.agents/skills/run-manual-validation/config/verify.zh-CN.json +1 -1
- package/templates/.agents/skills/run-task/SKILL.en.md +1 -1
- package/templates/.agents/skills/run-task/SKILL.zh-CN.md +1 -1
- package/templates/.agents/templates/task.en.md +1 -1
- package/templates/.agents/templates/task.zh-CN.md +1 -1
- package/templates/.claude/commands/import-issue.en.md +1 -1
- package/templates/.claude/commands/import-issue.zh-CN.md +1 -1
- package/templates/.claude/commands/restore-task.en.md +1 -1
- package/templates/.claude/commands/restore-task.zh-CN.md +1 -1
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', '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') {
|
|
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', '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;
|
|
@@ -996,9 +1122,9 @@ function applyTaskEventUnlocked(request: TaskEventRequest, options: TaskEventOpt
|
|
|
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: 'commit-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 === 'commit-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,40 @@ 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() || !entry.isFile() || !['baseline.md', 'candidate.md', 'final.md', 'publish.md'].includes(entry.name)) return false;
|
|
331
|
+
}
|
|
332
|
+
try {
|
|
333
|
+
for (const entry of entries) fs.unlinkSync(path.join(recovery, entry.name));
|
|
334
|
+
fs.rmdirSync(recovery);
|
|
335
|
+
const recoveryRoot = path.dirname(recovery);
|
|
336
|
+
if (fs.readdirSync(recoveryRoot).length === 0) fs.rmdirSync(recoveryRoot);
|
|
337
|
+
return true;
|
|
338
|
+
} catch (error) {
|
|
339
|
+
return errorCode(error) === 'ENOENT';
|
|
340
|
+
}
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
function removeCandidate(repoRoot: string, candidate: IntermediateCleanupItem, task?: TaskRecord): IntermediateCleanupItem {
|
|
310
344
|
if (candidate.disposition !== 'planned') return candidate;
|
|
311
345
|
const root = candidate.kind === 'EMPTY-AUX-PARENT'
|
|
312
346
|
? path.dirname(path.dirname(candidate.path))
|
|
@@ -323,6 +357,9 @@ function removeCandidate(candidate: IntermediateCleanupItem): IntermediateCleanu
|
|
|
323
357
|
const actual = safeLstat(root, candidate.path, candidate.kind === 'EMPTY-AUX-PARENT' ? 'directory' : 'file');
|
|
324
358
|
if (!actual) return { ...candidate, disposition: 'protected', reason: 'PATH_IDENTITY_CHANGED' };
|
|
325
359
|
if (!sameFileIdentity(expected, actual)) return { ...candidate, disposition: 'protected', reason: 'PATH_IDENTITY_CHANGED' };
|
|
360
|
+
if (!cleanupConsumedRecoveryFiles(repoRoot, task, candidate)) {
|
|
361
|
+
return { ...candidate, disposition: 'protected', reason: 'RECOVERY_STAGING_NOT_SAFE_TO_REMOVE' };
|
|
362
|
+
}
|
|
326
363
|
try {
|
|
327
364
|
if (candidate.kind === 'EMPTY-AUX-PARENT') {
|
|
328
365
|
if (fs.readdirSync(candidate.path).length !== 0) return { ...candidate, disposition: 'protected', reason: 'AUXILIARY_ROOT_NOT_EMPTY' };
|
|
@@ -367,6 +404,7 @@ function cleanupIntermediateUnderRemovalCoordinator(
|
|
|
367
404
|
indexes.push(index);
|
|
368
405
|
byTask.set(candidate.taskId, indexes);
|
|
369
406
|
}
|
|
407
|
+
const tasks = inspectTaskRecords(repoRoot, new Set(byTask.keys()));
|
|
370
408
|
|
|
371
409
|
for (const taskId of [...byTask.keys()].sort()) {
|
|
372
410
|
const execute = (): void => {
|
|
@@ -380,7 +418,7 @@ function cleanupIntermediateUnderRemovalCoordinator(
|
|
|
380
418
|
if (initialCandidate.disposition !== 'planned') continue;
|
|
381
419
|
const current = refreshedByKey.get(intermediateCleanupKey(initialCandidate));
|
|
382
420
|
items[index] = current
|
|
383
|
-
? removeCandidate(current)
|
|
421
|
+
? removeCandidate(repoRoot, current, tasks.get(taskId))
|
|
384
422
|
: { ...initialCandidate, disposition: 'protected', reason: 'CANDIDATE_NO_LONGER_ELIGIBLE' };
|
|
385
423
|
}
|
|
386
424
|
};
|
|
@@ -397,14 +435,14 @@ function cleanupIntermediateUnderRemovalCoordinator(
|
|
|
397
435
|
|
|
398
436
|
for (const [index, candidate] of items.entries()) {
|
|
399
437
|
if (candidate.taskId || candidate.disposition !== 'planned') continue;
|
|
400
|
-
items[index] = removeCandidate(candidate);
|
|
438
|
+
items[index] = removeCandidate(repoRoot, candidate);
|
|
401
439
|
}
|
|
402
440
|
|
|
403
441
|
const knownPaths = new Set(items.map((candidate) => intermediateCleanupKey(candidate)));
|
|
404
442
|
for (const candidate of scanIntermediateCleanup(repoRoot, scanOptions).items) {
|
|
405
443
|
if (candidate.kind !== 'EMPTY-AUX-PARENT' || candidate.disposition !== 'planned'
|
|
406
444
|
|| knownPaths.has(intermediateCleanupKey(candidate))) continue;
|
|
407
|
-
items.push(removeCandidate(candidate));
|
|
445
|
+
items.push(removeCandidate(repoRoot, candidate));
|
|
408
446
|
}
|
|
409
447
|
|
|
410
448
|
return buildIntermediateCleanupReport(items);
|