@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
package/dist/lib/task/events.js
CHANGED
|
@@ -16,13 +16,14 @@ import { TaskExecutionLockError, withTaskExecutionLock } from './task-execution-
|
|
|
16
16
|
import { captureTaskWriteMetadata, writeTask } from './write.js';
|
|
17
17
|
import { allowsManualOverride } from './guard-override.js';
|
|
18
18
|
import { consumeLocalArtifactFinalizationIntent, readLocalArtifactFinalizationIntent, validateLocalArtifact } from './local-artifact-finalization.js';
|
|
19
|
-
import {
|
|
19
|
+
import { writeArtifactRecoveryIntent, readArtifactRecoveryIntent } from './artifact-repair-intent.js';
|
|
20
|
+
import { reconcileArtifactRecovery, recoveryContextFromIntent, sha256Content } from './artifact-recovery.js';
|
|
20
21
|
import { buildLifecycleFacts, canStart } from './capabilities.js';
|
|
21
22
|
import { createInvalidationOperation, invalidationMutation, parseInvalidationDocument, targetIdFor, upsertInvalidation } from './invalidation.js';
|
|
22
23
|
import { consumeReworkIntents, parseReworkIntentDocument, reworkIntentMutation, supersedeReworkIntents } from './rework-intent.js';
|
|
23
24
|
import { ARTIFACT_FAMILIES, expectedQualificationRelations, parseQualificationAudit, parseTaskQualification, upstreamArtifactDigest, validateQualificationAudit } from './qualification-audit.js';
|
|
24
25
|
import { getArtifactSchema } from './artifact-schema.js';
|
|
25
|
-
import { inspectArtifactContract } from './artifact-operations.js';
|
|
26
|
+
import { canonicalSemanticDigest, inspectArtifactContract } from './artifact-operations.js';
|
|
26
27
|
import { consumeLifecycleRecoveryAttestation, lifecycleRecoveryAttestationDigest, validateLifecycleRecoveryAttestation } from './control-authority.js';
|
|
27
28
|
import { readManualValidationCompletion } from './manual-validation-completion.js';
|
|
28
29
|
const eventCatalog = [
|
|
@@ -314,6 +315,59 @@ function openStartedIdentity(rows, family) {
|
|
|
314
315
|
function reviewInputFamily(family) {
|
|
315
316
|
return family === 'review-analysis' ? 'analysis' : family === 'review-plan' ? 'plan' : 'code';
|
|
316
317
|
}
|
|
318
|
+
function reconcileReviewCompletionReplay(repoRoot, taskDir, taskId, family, artifact, round) {
|
|
319
|
+
const stage = REVIEW_LEDGER_STAGES[family];
|
|
320
|
+
if (!stage)
|
|
321
|
+
return null;
|
|
322
|
+
const reviewFamily = family;
|
|
323
|
+
try {
|
|
324
|
+
let intent = readArtifactRecoveryIntent(repoRoot, taskId, reviewFamily, artifact);
|
|
325
|
+
if (!intent)
|
|
326
|
+
return null;
|
|
327
|
+
if (intent.state === 'commit-started') {
|
|
328
|
+
const reconciled = reconcileArtifactRecovery(recoveryContextFromIntent(repoRoot, taskDir, intent), { lockAlreadyHeld: true });
|
|
329
|
+
if (reconciled.status !== 'passed' && reconciled.status !== 'consumed') {
|
|
330
|
+
return {
|
|
331
|
+
code: 'EVENT_ARTIFACT_CONFLICT',
|
|
332
|
+
message: `review finalizer recovery remains ${reconciled.status} for ${artifact}`
|
|
333
|
+
};
|
|
334
|
+
}
|
|
335
|
+
intent = reconciled.intent;
|
|
336
|
+
}
|
|
337
|
+
if (!['passed', 'consumption-started', 'consumed'].includes(intent.state)) {
|
|
338
|
+
return {
|
|
339
|
+
code: 'EVENT_ARTIFACT_CONFLICT',
|
|
340
|
+
message: `review finalizer provenance is incomplete for ${artifact}`
|
|
341
|
+
};
|
|
342
|
+
}
|
|
343
|
+
const validated = validateCompletedArtifact(taskDir, FAMILY[family].artifact, artifact, round);
|
|
344
|
+
if (!validated.ok) {
|
|
345
|
+
return { code: 'EVENT_ARTIFACT_CONFLICT', message: validated.error.message };
|
|
346
|
+
}
|
|
347
|
+
const content = fs.readFileSync(validated.artifact.path, 'utf8');
|
|
348
|
+
const expectedRequestId = `review-finalize:${taskId}:${stage}:${round}`;
|
|
349
|
+
if (intent.round !== round
|
|
350
|
+
|| intent.requestId !== expectedRequestId
|
|
351
|
+
|| intent.finalArtifactSha256 !== sha256Content(content)
|
|
352
|
+
|| intent.finalSemanticDigest !== canonicalSemanticDigest(content)) {
|
|
353
|
+
return {
|
|
354
|
+
code: 'EVENT_ARTIFACT_CONFLICT',
|
|
355
|
+
message: `review finalizer provenance does not match ${artifact}`
|
|
356
|
+
};
|
|
357
|
+
}
|
|
358
|
+
if (intent.state === 'passed' || intent.state === 'consumption-started') {
|
|
359
|
+
const consumed = { ...intent, state: 'consumed', updatedAt: Date.now() };
|
|
360
|
+
writeArtifactRecoveryIntent(repoRoot, consumed, { expected: intent });
|
|
361
|
+
}
|
|
362
|
+
return null;
|
|
363
|
+
}
|
|
364
|
+
catch (error) {
|
|
365
|
+
return {
|
|
366
|
+
code: 'EVENT_ARTIFACT_CONFLICT',
|
|
367
|
+
message: `review finalizer recovery could not be reconciled for ${artifact}: ${error instanceof Error ? error.message : String(error)}`
|
|
368
|
+
};
|
|
369
|
+
}
|
|
370
|
+
}
|
|
317
371
|
function relationForStartedInput(family, input) {
|
|
318
372
|
if (family.startsWith('review-')) {
|
|
319
373
|
return input.family === reviewInputFamily(family) ? 'reviewed-input' : 'approval-context';
|
|
@@ -715,6 +769,8 @@ function applyTaskEventUnlocked(request, options = {}) {
|
|
|
715
769
|
const row = manual ? openRows.at(-1) : matchingRows[0];
|
|
716
770
|
let completedArtifact = null;
|
|
717
771
|
let localFinalizationIntent = null;
|
|
772
|
+
let reviewFinalizationIntent = null;
|
|
773
|
+
let reviewContent = null;
|
|
718
774
|
if (eventIdentity.phase === 'started' && row) {
|
|
719
775
|
if (row.agent !== normalized.agent)
|
|
720
776
|
return failed(normalized, { code: 'EVENT_LOG_CONFLICT', message: 'open started event has a different agent' }, { taskId: resolved.taskId, taskMdPath: resolved.taskMdPath });
|
|
@@ -729,8 +785,14 @@ function applyTaskEventUnlocked(request, options = {}) {
|
|
|
729
785
|
if (eventIdentity.phase === 'started' && !manual && completedRows.length > 0)
|
|
730
786
|
return failed(normalized, { code: 'EVENT_ALREADY_COMPLETED', message: 'event identity is already completed' }, { taskId: resolved.taskId, taskMdPath: resolved.taskMdPath });
|
|
731
787
|
const done = completedRows.find((item) => item.note === eventIdentity.note);
|
|
732
|
-
if (eventIdentity.phase === 'completed' && done)
|
|
788
|
+
if (eventIdentity.phase === 'completed' && done) {
|
|
789
|
+
if (!normalized.dryRun && eventIdentity.family.startsWith('review-') && normalized.artifact && normalized.round) {
|
|
790
|
+
const recoveryError = reconcileReviewCompletionReplay(resolved.repoRoot, resolved.taskDir, resolved.taskId, eventIdentity.family, normalized.artifact, normalized.round);
|
|
791
|
+
if (recoveryError)
|
|
792
|
+
return failed(normalized, recoveryError, { taskId: resolved.taskId, taskMdPath: resolved.taskMdPath, fromStep: currentStep, toStep: currentStep, action: eventIdentity.action, phase: eventIdentity.phase });
|
|
793
|
+
}
|
|
733
794
|
return successNoOp(normalized, resolved.taskId, resolved.taskMdPath, currentStep, eventIdentity, done.done, frontmatter, artifactContext);
|
|
795
|
+
}
|
|
734
796
|
if (eventIdentity.phase === 'completed' && completedRows.length > 0 && openRows.length === 0)
|
|
735
797
|
return failed(normalized, { code: 'EVENT_LOG_CONFLICT', message: 'event identity is already completed with a different payload' }, { taskId: resolved.taskId, taskMdPath: resolved.taskMdPath, fromStep: currentStep, toStep: currentStep, action: eventIdentity.action, phase: eventIdentity.phase });
|
|
736
798
|
if (eventIdentity.phase === 'completed' && !row?.started && !allowsManualOverride(options.manualOverride, 'task-event', 'EVENT_START_MISSING'))
|
|
@@ -746,7 +808,6 @@ function applyTaskEventUnlocked(request, options = {}) {
|
|
|
746
808
|
return failed(normalized, validationError, { taskId: resolved.taskId, taskMdPath: resolved.taskMdPath, fromStep: currentStep, toStep: currentStep, action: eventIdentity.action, phase: eventIdentity.phase });
|
|
747
809
|
}
|
|
748
810
|
if (eventIdentity.family.startsWith('review-')) {
|
|
749
|
-
let reviewContent;
|
|
750
811
|
try {
|
|
751
812
|
reviewContent = fs.readFileSync(completedArtifact.path, 'utf8');
|
|
752
813
|
}
|
|
@@ -775,6 +836,20 @@ function applyTaskEventUnlocked(request, options = {}) {
|
|
|
775
836
|
const localFamily = eventIdentity.family === 'analyze'
|
|
776
837
|
? 'analysis'
|
|
777
838
|
: eventIdentity.family === 'plan' ? 'plan' : 'code';
|
|
839
|
+
try {
|
|
840
|
+
localFinalizationIntent = readLocalArtifactFinalizationIntent(resolved.repoRoot, resolved.taskId, localFamily, completedArtifact.name);
|
|
841
|
+
if (localFinalizationIntent?.state === 'commit-started') {
|
|
842
|
+
const reconciled = reconcileArtifactRecovery(recoveryContextFromIntent(resolved.repoRoot, resolved.taskDir, localFinalizationIntent), { lockAlreadyHeld: true });
|
|
843
|
+
if (reconciled.status === 'passed' || reconciled.status === 'consumed')
|
|
844
|
+
localFinalizationIntent = reconciled.intent;
|
|
845
|
+
}
|
|
846
|
+
}
|
|
847
|
+
catch (error) {
|
|
848
|
+
return failed(normalized, {
|
|
849
|
+
code: 'EVENT_ARTIFACT_CONFLICT',
|
|
850
|
+
message: `local finalizer recovery could not be reconciled for ${completedArtifact.name}: ${error instanceof Error ? error.message : String(error)}`
|
|
851
|
+
}, { taskId: resolved.taskId, taskMdPath: resolved.taskMdPath, fromStep: currentStep, toStep: currentStep, action: eventIdentity.action, phase: eventIdentity.phase });
|
|
852
|
+
}
|
|
778
853
|
let artifactContent;
|
|
779
854
|
try {
|
|
780
855
|
artifactContent = fs.readFileSync(completedArtifact.path, 'utf8');
|
|
@@ -806,33 +881,23 @@ function applyTaskEventUnlocked(request, options = {}) {
|
|
|
806
881
|
message: `semantic digest does not match finalizer result for ${completedArtifact.name}`
|
|
807
882
|
}, { taskId: resolved.taskId, taskMdPath: resolved.taskMdPath, fromStep: currentStep, toStep: currentStep, action: eventIdentity.action, phase: eventIdentity.phase });
|
|
808
883
|
}
|
|
809
|
-
|
|
810
|
-
localFinalizationIntent = readLocalArtifactFinalizationIntent(resolved.repoRoot, resolved.taskId, localFamily, completedArtifact.name);
|
|
811
|
-
}
|
|
812
|
-
catch (error) {
|
|
813
|
-
return failed(normalized, {
|
|
814
|
-
code: 'EVENT_ARTIFACT_CONFLICT',
|
|
815
|
-
message: `cannot read local finalizer provenance for ${completedArtifact.name}: ${error instanceof Error ? error.message : String(error)}`
|
|
816
|
-
}, { taskId: resolved.taskId, taskMdPath: resolved.taskMdPath, fromStep: currentStep, toStep: currentStep, action: eventIdentity.action, phase: eventIdentity.phase });
|
|
817
|
-
}
|
|
818
|
-
if (!localFinalizationIntent || !['passed', 'consumed', 'finalize-ready', 'commit-started'].includes(localFinalizationIntent.state)) {
|
|
884
|
+
if (!localFinalizationIntent || !['passed', 'consumed'].includes(localFinalizationIntent.state)) {
|
|
819
885
|
return failed(normalized, {
|
|
820
886
|
code: 'EVENT_ARTIFACT_CONFLICT',
|
|
821
887
|
message: `local finalizer provenance is missing or incomplete for ${completedArtifact.name}`
|
|
822
888
|
}, { taskId: resolved.taskId, taskMdPath: resolved.taskMdPath, fromStep: currentStep, toStep: currentStep, action: eventIdentity.action, phase: eventIdentity.phase });
|
|
823
889
|
}
|
|
824
|
-
if (localFinalizationIntent.
|
|
890
|
+
if (localFinalizationIntent.finalArtifactSha256 !== actualSha256 || localFinalizationIntent.finalSemanticDigest !== local.semanticDigest) {
|
|
825
891
|
return failed(normalized, {
|
|
826
892
|
code: 'EVENT_ARTIFACT_CONFLICT',
|
|
827
893
|
message: `local finalizer provenance does not match ${completedArtifact.name}`
|
|
828
894
|
}, { taskId: resolved.taskId, taskMdPath: resolved.taskMdPath, fromStep: currentStep, toStep: currentStep, action: eventIdentity.action, phase: eventIdentity.phase });
|
|
829
895
|
}
|
|
830
|
-
if (localFinalizationIntent.
|
|
896
|
+
if (localFinalizationIntent.authorityDigest !== null) {
|
|
831
897
|
if (!lifecycleAuthority
|
|
832
898
|
|| localFinalizationIntent.recoveryOperationId !== lifecycleAuthority.operationId
|
|
833
899
|
|| localFinalizationIntent.requestId !== lifecycleAuthority.lifecycleRequestId
|
|
834
|
-
||
|
|
835
|
-
&& localFinalizationIntent.authorityDigest !== lifecycleRecoveryAttestationDigest(lifecycleAuthority))) {
|
|
900
|
+
|| localFinalizationIntent.authorityDigest !== lifecycleRecoveryAttestationDigest(lifecycleAuthority)) {
|
|
836
901
|
return failed(normalized, {
|
|
837
902
|
code: 'EVENT_ARTIFACT_CONFLICT',
|
|
838
903
|
message: `local finalizer provenance does not match lifecycle completion authority for ${completedArtifact.name}`
|
|
@@ -862,6 +927,43 @@ function applyTaskEventUnlocked(request, options = {}) {
|
|
|
862
927
|
const findingCountError = validateReviewFindingCounts(normalized, content, eventIdentity.family, completedArtifact?.path ?? null);
|
|
863
928
|
if (findingCountError)
|
|
864
929
|
return failed(normalized, findingCountError, { taskId: resolved.taskId, taskMdPath: resolved.taskMdPath, fromStep: currentStep, toStep: currentStep, action: eventIdentity.action, phase: eventIdentity.phase });
|
|
930
|
+
if (eventIdentity.phase === 'completed' && eventIdentity.family.startsWith('review-') && completedArtifact && reviewContent !== null) {
|
|
931
|
+
try {
|
|
932
|
+
reviewFinalizationIntent = readArtifactRecoveryIntent(resolved.repoRoot, resolved.taskId, eventIdentity.family, completedArtifact.name);
|
|
933
|
+
if (!normalized.dryRun && reviewFinalizationIntent?.state === 'commit-started') {
|
|
934
|
+
const reconciled = reconcileArtifactRecovery(recoveryContextFromIntent(resolved.repoRoot, resolved.taskDir, reviewFinalizationIntent), { lockAlreadyHeld: true });
|
|
935
|
+
if (reconciled.status === 'passed' || reconciled.status === 'consumed')
|
|
936
|
+
reviewFinalizationIntent = reconciled.intent;
|
|
937
|
+
}
|
|
938
|
+
if (!reviewFinalizationIntent || !['passed', 'consumption-started', 'consumed'].includes(reviewFinalizationIntent.state)) {
|
|
939
|
+
return failed(normalized, {
|
|
940
|
+
code: 'EVENT_ARTIFACT_CONFLICT',
|
|
941
|
+
message: `review finalizer provenance is missing or incomplete for ${completedArtifact.name}`
|
|
942
|
+
}, { taskId: resolved.taskId, taskMdPath: resolved.taskMdPath, fromStep: currentStep, toStep: currentStep, action: eventIdentity.action, phase: eventIdentity.phase });
|
|
943
|
+
}
|
|
944
|
+
const reviewStage = eventIdentity.family === 'review-analysis'
|
|
945
|
+
? 'analysis'
|
|
946
|
+
: eventIdentity.family === 'review-plan' ? 'plan' : 'code';
|
|
947
|
+
const actualSha256 = sha256File(completedArtifact.path);
|
|
948
|
+
const actualSemanticDigest = canonicalSemanticDigest(reviewContent);
|
|
949
|
+
const expectedRequestId = `review-finalize:${resolved.taskId}:${reviewStage}:${normalized.round}`;
|
|
950
|
+
if (reviewFinalizationIntent.round !== normalized.round
|
|
951
|
+
|| reviewFinalizationIntent.requestId !== expectedRequestId
|
|
952
|
+
|| reviewFinalizationIntent.finalArtifactSha256 !== actualSha256
|
|
953
|
+
|| reviewFinalizationIntent.finalSemanticDigest !== actualSemanticDigest) {
|
|
954
|
+
return failed(normalized, {
|
|
955
|
+
code: 'EVENT_ARTIFACT_CONFLICT',
|
|
956
|
+
message: `review finalizer provenance does not match ${completedArtifact.name}`
|
|
957
|
+
}, { taskId: resolved.taskId, taskMdPath: resolved.taskMdPath, fromStep: currentStep, toStep: currentStep, action: eventIdentity.action, phase: eventIdentity.phase });
|
|
958
|
+
}
|
|
959
|
+
}
|
|
960
|
+
catch (error) {
|
|
961
|
+
return failed(normalized, {
|
|
962
|
+
code: 'EVENT_ARTIFACT_CONFLICT',
|
|
963
|
+
message: `review finalizer recovery could not be reconciled for ${completedArtifact.name}: ${error instanceof Error ? error.message : String(error)}`
|
|
964
|
+
}, { taskId: resolved.taskId, taskMdPath: resolved.taskMdPath, fromStep: currentStep, toStep: currentStep, action: eventIdentity.action, phase: eventIdentity.phase });
|
|
965
|
+
}
|
|
966
|
+
}
|
|
865
967
|
let orchestrationCompletion = null;
|
|
866
968
|
if (eventIdentity.phase === 'completed' && eventIdentity.family !== 'manual-validation' && eventIdentity.family !== 'validation-run') {
|
|
867
969
|
const orchestrationStage = eventIdentity.family === 'analyze' ? 'analysis' : eventIdentity.family;
|
|
@@ -1004,16 +1106,16 @@ function applyTaskEventUnlocked(request, options = {}) {
|
|
|
1004
1106
|
const expectedIntent = localFinalizationIntent;
|
|
1005
1107
|
localFinalizationIntent = {
|
|
1006
1108
|
...localFinalizationIntent,
|
|
1007
|
-
state: '
|
|
1109
|
+
state: 'consumption-started',
|
|
1008
1110
|
phase: 'task-event.completed',
|
|
1009
1111
|
authorityDigest: lifecycleRecoveryAttestationDigest(lifecycleAuthority),
|
|
1010
1112
|
requestId: lifecycleAuthority.lifecycleRequestId,
|
|
1011
1113
|
updatedAt: Date.now()
|
|
1012
1114
|
};
|
|
1013
|
-
|
|
1115
|
+
writeArtifactRecoveryIntent(resolved.repoRoot, localFinalizationIntent, { expected: expectedIntent });
|
|
1014
1116
|
}
|
|
1015
1117
|
else if (!lifecycleAuthority && localFinalizationIntent.state === 'passed') {
|
|
1016
|
-
localFinalizationIntent = consumeLocalArtifactFinalizationIntent(resolved.repoRoot, localFinalizationIntent);
|
|
1118
|
+
localFinalizationIntent = consumeLocalArtifactFinalizationIntent(resolved.repoRoot, localFinalizationIntent, { lockAlreadyHeld: options.lockAlreadyHeld });
|
|
1017
1119
|
}
|
|
1018
1120
|
}
|
|
1019
1121
|
catch (error) {
|
|
@@ -1031,6 +1133,32 @@ function applyTaskEventUnlocked(request, options = {}) {
|
|
|
1031
1133
|
});
|
|
1032
1134
|
}
|
|
1033
1135
|
}
|
|
1136
|
+
if (!normalized.dryRun && reviewFinalizationIntent && completedArtifact && reviewFinalizationIntent.state === 'passed') {
|
|
1137
|
+
try {
|
|
1138
|
+
const expectedIntent = reviewFinalizationIntent;
|
|
1139
|
+
reviewFinalizationIntent = {
|
|
1140
|
+
...reviewFinalizationIntent,
|
|
1141
|
+
state: 'consumption-started',
|
|
1142
|
+
phase: 'task-event.completed',
|
|
1143
|
+
updatedAt: Date.now()
|
|
1144
|
+
};
|
|
1145
|
+
writeArtifactRecoveryIntent(resolved.repoRoot, reviewFinalizationIntent, { expected: expectedIntent });
|
|
1146
|
+
}
|
|
1147
|
+
catch (error) {
|
|
1148
|
+
return failed(normalized, {
|
|
1149
|
+
code: 'EVENT_ARTIFACT_CONFLICT',
|
|
1150
|
+
message: `review finalizer provenance could not be advanced before task write for ${completedArtifact.name}: ${error instanceof Error ? error.message : String(error)}`
|
|
1151
|
+
}, {
|
|
1152
|
+
taskId: resolved.taskId,
|
|
1153
|
+
taskMdPath: resolved.taskMdPath,
|
|
1154
|
+
fromStep: currentStep,
|
|
1155
|
+
toStep: step,
|
|
1156
|
+
action: eventIdentity.action,
|
|
1157
|
+
phase: eventIdentity.phase,
|
|
1158
|
+
artifactContext
|
|
1159
|
+
});
|
|
1160
|
+
}
|
|
1161
|
+
}
|
|
1034
1162
|
const sourceCompletion = eventIdentity.phase === 'completed'
|
|
1035
1163
|
&& ['analyze', 'plan', 'code'].includes(eventIdentity.family);
|
|
1036
1164
|
const result = writeTask({ taskRef: normalized.taskRef, expectedState: stateOverride ? resolved.state : 'active', dryRun: normalized.dryRun, mutations }, { ...options, invalidationContext: sourceCompletion ? 'source-completion' : 'standard', metadataProvider: () => metadata });
|
|
@@ -1040,7 +1168,7 @@ function applyTaskEventUnlocked(request, options = {}) {
|
|
|
1040
1168
|
try {
|
|
1041
1169
|
if (!options.deferLifecycleRecoveryConsumption)
|
|
1042
1170
|
consumeLifecycleRecoveryAttestation(lifecycleAuthority);
|
|
1043
|
-
|
|
1171
|
+
writeArtifactRecoveryIntent(resolved.repoRoot, { ...localFinalizationIntent, state: 'consumed', updatedAt: Date.now() }, { expected: localFinalizationIntent });
|
|
1044
1172
|
}
|
|
1045
1173
|
catch (error) {
|
|
1046
1174
|
return failed(normalized, {
|
|
@@ -1049,6 +1177,20 @@ function applyTaskEventUnlocked(request, options = {}) {
|
|
|
1049
1177
|
}, { taskId: result.taskId, taskMdPath: result.taskMdPath, fromStep: currentStep, toStep: step, action: eventIdentity.action, phase: eventIdentity.phase, timestamp: result.timestamp, agentInfraVersion: result.agentInfraVersion, operations: result.operations, artifactContext });
|
|
1050
1178
|
}
|
|
1051
1179
|
}
|
|
1180
|
+
if (!normalized.dryRun && reviewFinalizationIntent && completedArtifact) {
|
|
1181
|
+
try {
|
|
1182
|
+
if (reviewFinalizationIntent.state === 'consumption-started') {
|
|
1183
|
+
const consumed = { ...reviewFinalizationIntent, state: 'consumed', updatedAt: Date.now() };
|
|
1184
|
+
writeArtifactRecoveryIntent(resolved.repoRoot, consumed, { expected: reviewFinalizationIntent });
|
|
1185
|
+
}
|
|
1186
|
+
}
|
|
1187
|
+
catch (error) {
|
|
1188
|
+
return failed(normalized, {
|
|
1189
|
+
code: 'EVENT_ARTIFACT_CONFLICT',
|
|
1190
|
+
message: `review finalizer recovery consumption is incomplete: ${error instanceof Error ? error.message : String(error)}`
|
|
1191
|
+
}, { taskId: result.taskId, taskMdPath: result.taskMdPath, fromStep: currentStep, toStep: step, action: eventIdentity.action, phase: eventIdentity.phase, timestamp: result.timestamp, agentInfraVersion: result.agentInfraVersion, operations: result.operations, artifactContext });
|
|
1192
|
+
}
|
|
1193
|
+
}
|
|
1052
1194
|
if (!normalized.dryRun && orchestrationCompletion) {
|
|
1053
1195
|
try {
|
|
1054
1196
|
(options.commitOrchestrationCompletion ?? commitOrchestrationStageCompletion)(orchestrationCompletion);
|
|
@@ -1095,7 +1237,7 @@ function applyTaskEvent(request, options = {}) {
|
|
|
1095
1237
|
if (!resolved.ok)
|
|
1096
1238
|
return applyTaskEventUnlocked(request, options);
|
|
1097
1239
|
try {
|
|
1098
|
-
return withTaskExecutionLock(resolved.repoRoot, resolved.taskId, `task-event.${request.event}`, () => applyTaskEventUnlocked(request, options));
|
|
1240
|
+
return withTaskExecutionLock(resolved.repoRoot, resolved.taskId, `task-event.${request.event}`, () => applyTaskEventUnlocked(request, { ...options, lockAlreadyHeld: true }));
|
|
1099
1241
|
}
|
|
1100
1242
|
catch (error) {
|
|
1101
1243
|
if (!(error instanceof TaskExecutionLockError))
|
|
@@ -407,10 +407,7 @@ function resourceForSnapshot(policy, options, frontmatter) {
|
|
|
407
407
|
if (policy.producerId === 'platform.issue') {
|
|
408
408
|
const identity = options.probeIssueNumber !== undefined
|
|
409
409
|
? resourceIdentity(options.probeIssueNumber)
|
|
410
|
-
: taskIssueIdentity((frontmatter || {}))
|
|
411
|
-
const number = toPositiveInteger(frontmatter?.issue_number);
|
|
412
|
-
return number === null ? null : resourceIdentity(number);
|
|
413
|
-
})();
|
|
410
|
+
: taskIssueIdentity((frontmatter || {}));
|
|
414
411
|
return identity ? { kind: 'issue', identity, number: resourceIdentityNumber(identity) } : null;
|
|
415
412
|
}
|
|
416
413
|
if (policy.producerId === 'platform.pull-request') {
|
|
@@ -481,11 +478,11 @@ async function probePlatformFailure(taskRef, policy, resolved, options) {
|
|
|
481
478
|
}
|
|
482
479
|
const issueIdentity = options.probeIssueNumber !== undefined
|
|
483
480
|
? resourceIdentity(options.probeIssueNumber)
|
|
484
|
-
: taskIssueIdentity(frontmatter)
|
|
485
|
-
const number = toPositiveInteger(frontmatter.issue_number);
|
|
486
|
-
return number === null ? null : resourceIdentity(number);
|
|
487
|
-
})();
|
|
481
|
+
: taskIssueIdentity(frontmatter);
|
|
488
482
|
const fact = readPrDeliveryFact(frontmatter);
|
|
483
|
+
if (fact.status === 'invalid') {
|
|
484
|
+
return error(fact.error.code, fact.error.message);
|
|
485
|
+
}
|
|
489
486
|
const pullRequestIdentity = options.probePullRequestNumber !== undefined
|
|
490
487
|
? resourceIdentity(options.probePullRequestNumber)
|
|
491
488
|
: fact.status === 'valid' && fact.fact.state === 'bound' ? fact.fact.identity.resource : null;
|
|
@@ -7,11 +7,12 @@ import { checkpointCommitMatches } from './commit-identity.js';
|
|
|
7
7
|
import { readCheckpointIntent } from './commit-intent.js';
|
|
8
8
|
import { parseTaskFrontmatter } from './frontmatter.js';
|
|
9
9
|
import { readTaskFinalizationReceipt } from './finalization.js';
|
|
10
|
-
import {
|
|
10
|
+
import { semanticDigest, sha256Content } from './local-artifact-finalization.js';
|
|
11
|
+
import { artifactRecoveryRoot, readArtifactRecoveryIntent } from './artifact-repair-intent.js';
|
|
11
12
|
import { parseArtifactName } from './artifact-name.js';
|
|
12
13
|
import { enumerateAllTaskDirs } from './resolve-ref.js';
|
|
13
14
|
const TASK_ID_RE = /^TASK-\d{8}-\d{6}$/;
|
|
14
|
-
const LOCAL_INTENT_RE = /^(TASK-\d{8}-\d{6})-(analysis|plan|code)-(.+\.md)\.json$/;
|
|
15
|
+
const LOCAL_INTENT_RE = /^(TASK-\d{8}-\d{6})-(analysis|review-analysis|plan|review-plan|code|review-code)-(.+\.md)\.json$/;
|
|
15
16
|
const COMMIT_INTENT_RE = /^(TASK-\d{8}-\d{6})\.json$/;
|
|
16
17
|
const AUXILIARY_ROOTS = [
|
|
17
18
|
'.local-artifact-finalization-intents',
|
|
@@ -110,7 +111,7 @@ function localIntentCandidate(repoRoot, task, filePath, taskId, family, artifact
|
|
|
110
111
|
}
|
|
111
112
|
let intent;
|
|
112
113
|
try {
|
|
113
|
-
intent =
|
|
114
|
+
intent = readArtifactRecoveryIntent(repoRoot, taskId, family, artifact);
|
|
114
115
|
}
|
|
115
116
|
catch {
|
|
116
117
|
return item('LFAI-CONSUMED', taskId, filePath, 'protected', 'LFAI_SCHEMA_INVALID', identity);
|
|
@@ -129,7 +130,7 @@ function localIntentCandidate(repoRoot, task, filePath, taskId, family, artifact
|
|
|
129
130
|
catch {
|
|
130
131
|
return item('LFAI-CONSUMED', taskId, filePath, 'protected', 'ARTIFACT_UNREADABLE', identity);
|
|
131
132
|
}
|
|
132
|
-
if (intent.
|
|
133
|
+
if (intent.finalArtifactSha256 !== sha256Content(content) || intent.finalSemanticDigest !== semanticDigest(content)) {
|
|
133
134
|
return item('LFAI-CONSUMED', taskId, filePath, 'protected', 'ARTIFACT_DIGEST_MISMATCH', identity);
|
|
134
135
|
}
|
|
135
136
|
const receiptReason = receiptGate(repoRoot, task, options);
|
|
@@ -254,7 +255,85 @@ function scanIntermediateCleanup(repoRoot, options = {}) {
|
|
|
254
255
|
const candidates = scanInternal(repoRoot, options);
|
|
255
256
|
return buildIntermediateCleanupReport(candidates, true);
|
|
256
257
|
}
|
|
257
|
-
function
|
|
258
|
+
function cleanupConsumedRecoveryFiles(repoRoot, task, candidate) {
|
|
259
|
+
if (candidate.kind !== 'LFAI-CONSUMED' || !candidate.taskId)
|
|
260
|
+
return true;
|
|
261
|
+
if (!task)
|
|
262
|
+
return false;
|
|
263
|
+
const match = LOCAL_INTENT_RE.exec(path.basename(candidate.path));
|
|
264
|
+
if (!match)
|
|
265
|
+
return false;
|
|
266
|
+
let intent;
|
|
267
|
+
try {
|
|
268
|
+
intent = readArtifactRecoveryIntent(repoRoot, candidate.taskId, match[2], match[3]);
|
|
269
|
+
}
|
|
270
|
+
catch {
|
|
271
|
+
return false;
|
|
272
|
+
}
|
|
273
|
+
if (!intent || intent.state !== 'consumed')
|
|
274
|
+
return false;
|
|
275
|
+
const recovery = artifactRecoveryRoot(task.taskDir, intent.recoveryOperationId);
|
|
276
|
+
let stat = null;
|
|
277
|
+
try {
|
|
278
|
+
stat = fs.lstatSync(recovery);
|
|
279
|
+
}
|
|
280
|
+
catch (error) {
|
|
281
|
+
if (errorCode(error) === 'ENOENT')
|
|
282
|
+
return true;
|
|
283
|
+
return false;
|
|
284
|
+
}
|
|
285
|
+
if (!stat.isDirectory() || stat.isSymbolicLink())
|
|
286
|
+
return false;
|
|
287
|
+
let entries;
|
|
288
|
+
try {
|
|
289
|
+
entries = fs.readdirSync(recovery, { withFileTypes: true });
|
|
290
|
+
}
|
|
291
|
+
catch {
|
|
292
|
+
return false;
|
|
293
|
+
}
|
|
294
|
+
for (const entry of entries) {
|
|
295
|
+
if (entry.isSymbolicLink())
|
|
296
|
+
return false;
|
|
297
|
+
if (entry.isFile()) {
|
|
298
|
+
if (!['baseline.md', 'candidate.md', 'final.md', 'publish.md'].includes(entry.name))
|
|
299
|
+
return false;
|
|
300
|
+
continue;
|
|
301
|
+
}
|
|
302
|
+
if (!entry.isDirectory() || entry.name !== 'generations')
|
|
303
|
+
return false;
|
|
304
|
+
let generations;
|
|
305
|
+
try {
|
|
306
|
+
generations = fs.readdirSync(path.join(recovery, entry.name), { withFileTypes: true });
|
|
307
|
+
}
|
|
308
|
+
catch {
|
|
309
|
+
return false;
|
|
310
|
+
}
|
|
311
|
+
if (generations.some((generation) => generation.isSymbolicLink()
|
|
312
|
+
|| !generation.isFile() || !/^[a-f0-9]{64}\.md$/u.test(generation.name)))
|
|
313
|
+
return false;
|
|
314
|
+
}
|
|
315
|
+
try {
|
|
316
|
+
for (const entry of entries) {
|
|
317
|
+
const target = path.join(recovery, entry.name);
|
|
318
|
+
if (entry.isFile())
|
|
319
|
+
fs.unlinkSync(target);
|
|
320
|
+
else {
|
|
321
|
+
for (const generation of fs.readdirSync(target, { withFileTypes: true }))
|
|
322
|
+
fs.unlinkSync(path.join(target, generation.name));
|
|
323
|
+
fs.rmdirSync(target);
|
|
324
|
+
}
|
|
325
|
+
}
|
|
326
|
+
fs.rmdirSync(recovery);
|
|
327
|
+
const recoveryRoot = path.dirname(recovery);
|
|
328
|
+
if (fs.readdirSync(recoveryRoot).length === 0)
|
|
329
|
+
fs.rmdirSync(recoveryRoot);
|
|
330
|
+
return true;
|
|
331
|
+
}
|
|
332
|
+
catch (error) {
|
|
333
|
+
return errorCode(error) === 'ENOENT';
|
|
334
|
+
}
|
|
335
|
+
}
|
|
336
|
+
function removeCandidate(repoRoot, candidate, task) {
|
|
258
337
|
if (candidate.disposition !== 'planned')
|
|
259
338
|
return candidate;
|
|
260
339
|
const root = candidate.kind === 'EMPTY-AUX-PARENT'
|
|
@@ -279,6 +358,9 @@ function removeCandidate(candidate) {
|
|
|
279
358
|
return { ...candidate, disposition: 'protected', reason: 'PATH_IDENTITY_CHANGED' };
|
|
280
359
|
if (!sameFileIdentity(expected, actual))
|
|
281
360
|
return { ...candidate, disposition: 'protected', reason: 'PATH_IDENTITY_CHANGED' };
|
|
361
|
+
if (!cleanupConsumedRecoveryFiles(repoRoot, task, candidate)) {
|
|
362
|
+
return { ...candidate, disposition: 'protected', reason: 'RECOVERY_STAGING_NOT_SAFE_TO_REMOVE' };
|
|
363
|
+
}
|
|
282
364
|
try {
|
|
283
365
|
if (candidate.kind === 'EMPTY-AUX-PARENT') {
|
|
284
366
|
if (fs.readdirSync(candidate.path).length !== 0)
|
|
@@ -317,6 +399,7 @@ function cleanupIntermediateUnderRemovalCoordinator(repoRoot, options = {}) {
|
|
|
317
399
|
indexes.push(index);
|
|
318
400
|
byTask.set(candidate.taskId, indexes);
|
|
319
401
|
}
|
|
402
|
+
const tasks = inspectTaskRecords(repoRoot, new Set(byTask.keys()));
|
|
320
403
|
for (const taskId of [...byTask.keys()].sort()) {
|
|
321
404
|
const execute = () => {
|
|
322
405
|
const refreshed = scanIntermediateCleanup(repoRoot, {
|
|
@@ -330,7 +413,7 @@ function cleanupIntermediateUnderRemovalCoordinator(repoRoot, options = {}) {
|
|
|
330
413
|
continue;
|
|
331
414
|
const current = refreshedByKey.get(intermediateCleanupKey(initialCandidate));
|
|
332
415
|
items[index] = current
|
|
333
|
-
? removeCandidate(current)
|
|
416
|
+
? removeCandidate(repoRoot, current, tasks.get(taskId))
|
|
334
417
|
: { ...initialCandidate, disposition: 'protected', reason: 'CANDIDATE_NO_LONGER_ELIGIBLE' };
|
|
335
418
|
}
|
|
336
419
|
};
|
|
@@ -351,14 +434,14 @@ function cleanupIntermediateUnderRemovalCoordinator(repoRoot, options = {}) {
|
|
|
351
434
|
for (const [index, candidate] of items.entries()) {
|
|
352
435
|
if (candidate.taskId || candidate.disposition !== 'planned')
|
|
353
436
|
continue;
|
|
354
|
-
items[index] = removeCandidate(candidate);
|
|
437
|
+
items[index] = removeCandidate(repoRoot, candidate);
|
|
355
438
|
}
|
|
356
439
|
const knownPaths = new Set(items.map((candidate) => intermediateCleanupKey(candidate)));
|
|
357
440
|
for (const candidate of scanIntermediateCleanup(repoRoot, scanOptions).items) {
|
|
358
441
|
if (candidate.kind !== 'EMPTY-AUX-PARENT' || candidate.disposition !== 'planned'
|
|
359
442
|
|| knownPaths.has(intermediateCleanupKey(candidate)))
|
|
360
443
|
continue;
|
|
361
|
-
items.push(removeCandidate(candidate));
|
|
444
|
+
items.push(removeCandidate(repoRoot, candidate));
|
|
362
445
|
}
|
|
363
446
|
return buildIntermediateCleanupReport(items);
|
|
364
447
|
}
|