@fitlab-ai/agent-infra 0.11.0 → 0.11.2
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/internal/cli-route-inventory.js +1 -1
- package/dist/lib/internal/manual-validation.js +6 -2
- package/dist/lib/internal/platform-comment.js +59 -0
- package/dist/lib/internal/sandbox-control.js +4 -131
- package/dist/lib/internal/task-artifact.js +1 -1
- package/dist/lib/internal/task-operation-registry.js +1 -0
- 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 +120 -13
- package/dist/lib/platform/issues.js +3 -4
- 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 +161 -156
- package/dist/lib/sandbox/control/executor.js +0 -38
- package/dist/lib/sandbox/control/finalization-response.js +10 -2
- package/dist/lib/sandbox/control/task-view.js +3 -1
- package/dist/lib/sandbox/control/workflow-executor.js +32 -41
- package/dist/lib/task/artifact-command.js +6 -6
- package/dist/lib/task/artifact-lifecycle.js +15 -52
- package/dist/lib/task/artifact-operations.js +27 -1
- package/dist/lib/task/capabilities.js +0 -2
- 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 +1 -8
- package/dist/lib/task/delegation-receipts.js +14 -76
- package/dist/lib/task/events.js +147 -278
- package/dist/lib/task/finalization-state.js +2 -1
- package/dist/lib/task/finalization.js +273 -49
- package/dist/lib/task/gate-policy.js +94 -0
- package/dist/lib/task/lifecycle-execution.js +2 -11
- package/dist/lib/task/local-artifact-finalization.js +31 -245
- package/dist/lib/task/orchestration.js +8 -24
- package/dist/lib/task/review-artifacts.js +12 -6
- package/dist/lib/task/review-command.js +0 -1
- package/dist/lib/task/review-finalization.js +47 -218
- package/dist/lib/task/verification-engine.js +28 -17
- package/dist/lib/task/verification.js +25 -9
- package/dist/lib/task/workflow-command.js +4 -1
- package/dist/lib/task/workflow-dispatch.js +55 -3
- package/dist/lifecycle-build-manifest.json +3 -3
- package/lib/internal/cli-route-inventory.ts +1 -1
- package/lib/internal/manual-validation.ts +6 -2
- package/lib/internal/platform-comment.ts +38 -0
- package/lib/internal/sandbox-control.ts +3 -145
- package/lib/internal/task-artifact.ts +1 -1
- package/lib/internal/task-operation-registry.ts +1 -0
- 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 +132 -13
- package/lib/platform/issues.ts +3 -4
- package/lib/platform/provider-contract.ts +2 -0
- package/lib/platform/provider-validation.ts +6 -2
- package/lib/platform/verification-sync.ts +178 -179
- package/lib/sandbox/control/executor.ts +0 -44
- package/lib/sandbox/control/finalization-response.ts +10 -2
- package/lib/sandbox/control/task-view.ts +3 -1
- package/lib/sandbox/control/workflow-executor.ts +31 -48
- package/lib/task/artifact-command.ts +6 -7
- package/lib/task/artifact-lifecycle.ts +12 -49
- package/lib/task/artifact-operations.ts +27 -1
- package/lib/task/capabilities.ts +0 -1
- package/lib/task/codex-orchestration.ts +8 -241
- package/lib/task/commit-operation.ts +3 -23
- package/lib/task/control-authority.ts +1 -8
- package/lib/task/delegation-receipts.ts +12 -79
- package/lib/task/events.ts +155 -287
- package/lib/task/finalization-state.ts +2 -1
- package/lib/task/finalization.ts +277 -57
- package/lib/task/gate-policy.ts +133 -0
- package/lib/task/lifecycle-execution.ts +10 -21
- package/lib/task/local-artifact-finalization.ts +29 -285
- package/lib/task/orchestration.ts +12 -41
- package/lib/task/review-artifacts.ts +8 -2
- package/lib/task/review-command.ts +0 -1
- package/lib/task/review-finalization.ts +46 -200
- package/lib/task/verification-engine.ts +24 -14
- package/lib/task/verification-types.ts +7 -0
- package/lib/task/verification.ts +25 -9
- package/lib/task/workflow-command.ts +4 -1
- package/lib/task/workflow-dispatch.ts +46 -3
- package/package.json +2 -2
- package/templates/.agents/rules/issue-sync.en.md +2 -0
- package/templates/.agents/rules/issue-sync.zh-CN.md +5 -8
- package/templates/.agents/rules/lifecycle-orchestration.en.md +7 -7
- package/templates/.agents/rules/lifecycle-orchestration.zh-CN.md +7 -7
- package/templates/.agents/rules/local-artifact-repair.en.md +14 -52
- package/templates/.agents/rules/local-artifact-repair.zh-CN.md +13 -52
- 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/analyze-task/config/verify.en.json +1 -10
- package/templates/.agents/skills/analyze-task/config/verify.zh-CN.json +1 -10
- package/templates/.agents/skills/block-task/config/verify.json +1 -3
- package/templates/.agents/skills/cancel-task/config/verify.json +1 -4
- 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/code-task/config/verify.en.json +1 -12
- package/templates/.agents/skills/code-task/config/verify.zh-CN.json +1 -12
- package/templates/.agents/skills/code-task/reference/dual-mode.en.md +1 -1
- package/templates/.agents/skills/code-task/reference/dual-mode.zh-CN.md +1 -1
- package/templates/.agents/skills/code-task/reference/report-template.en.md +11 -0
- package/templates/.agents/skills/code-task/reference/report-template.zh-CN.md +11 -0
- package/templates/.agents/skills/commit/config/verify.json +1 -5
- package/templates/.agents/skills/complete-manual-validation/config/verify.en.json +6 -6
- package/templates/.agents/skills/complete-manual-validation/config/verify.zh-CN.json +6 -6
- package/templates/.agents/skills/complete-task/SKILL.en.md +3 -3
- package/templates/.agents/skills/complete-task/SKILL.zh-CN.md +3 -3
- package/templates/.agents/skills/complete-task/config/verify.en.json +3 -14
- package/templates/.agents/skills/complete-task/config/verify.zh-CN.json +3 -14
- package/templates/.agents/skills/create-pr/config/verify.json +1 -10
- package/templates/.agents/skills/create-task/config/verify.json +1 -4
- package/templates/.agents/skills/import-issue/config/verify.json +1 -2
- 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/plan-task/config/verify.en.json +1 -10
- package/templates/.agents/skills/plan-task/config/verify.zh-CN.json +1 -10
- package/templates/.agents/skills/restore-task/SKILL.zh-CN.md +4 -4
- 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 -10
- package/templates/.agents/skills/review-analysis/config/verify.zh-CN.json +1 -10
- package/templates/.agents/skills/review-analysis/reference/report-template.en.md +2 -0
- package/templates/.agents/skills/review-analysis/reference/report-template.zh-CN.md +2 -0
- 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-code/config/verify.en.json +1 -10
- package/templates/.agents/skills/review-code/config/verify.zh-CN.json +1 -10
- package/templates/.agents/skills/review-code/reference/report-template.en.md +2 -0
- package/templates/.agents/skills/review-code/reference/report-template.zh-CN.md +2 -0
- 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 -10
- package/templates/.agents/skills/review-plan/config/verify.zh-CN.json +1 -10
- package/templates/.agents/skills/review-plan/reference/report-template.en.md +2 -0
- package/templates/.agents/skills/review-plan/reference/report-template.zh-CN.md +2 -0
- package/templates/.agents/skills/review-pr/config/verify.en.json +1 -8
- package/templates/.agents/skills/review-pr/config/verify.zh-CN.json +1 -8
- package/templates/.agents/skills/run-manual-validation/config/verify.en.json +38 -13
- package/templates/.agents/skills/run-manual-validation/config/verify.zh-CN.json +38 -13
- package/templates/.agents/skills/run-task/SKILL.en.md +6 -6
- package/templates/.agents/skills/run-task/SKILL.zh-CN.md +6 -6
- 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/lib/task/events.ts
CHANGED
|
@@ -1,12 +1,11 @@
|
|
|
1
1
|
import fs from 'node:fs';
|
|
2
2
|
import path from 'node:path';
|
|
3
|
+
import { execFileSync } from 'node:child_process';
|
|
3
4
|
|
|
4
5
|
import { appendActivityEntry, locateActivityLog, pairEntries, startedBackedRows } from './activity-log.ts';
|
|
5
6
|
import {
|
|
6
7
|
buildArtifactLinkSection,
|
|
7
8
|
inspectArtifactDirectory,
|
|
8
|
-
parseReviewedInputReference,
|
|
9
|
-
parseCodePlanInputReference,
|
|
10
9
|
resolveArtifactContext,
|
|
11
10
|
validateCompletedArtifact
|
|
12
11
|
} from './artifact-lifecycle.ts';
|
|
@@ -24,6 +23,7 @@ import {
|
|
|
24
23
|
import { LEDGER_SECTION_MISSING_CODE, LEDGER_SECTION_MISSING_MESSAGE, parseLedgerDocument, summarizeLedgerStage, validateLedgerRows } from './ledger.ts';
|
|
25
24
|
import type { ReviewStage } from './ledger.ts';
|
|
26
25
|
import { parseReviewSummary, resolveCanonicalVerdict } from './review-artifacts.ts';
|
|
26
|
+
import { extractReviewBaseline, extractReviewedHead, extractReviewedSnapshotTree } from './review-fingerprint.ts';
|
|
27
27
|
import { resolveTaskRef } from './resolve-ref.ts';
|
|
28
28
|
import { findSectionHeading } from './sections.ts';
|
|
29
29
|
import { validateLifecycleExecution } from './lifecycle-execution.ts';
|
|
@@ -33,19 +33,13 @@ 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';
|
|
48
41
|
import type { InvalidationTargetKind } from './invalidation.ts';
|
|
42
|
+
import { reconcileTaskInvalidation } from './invalidation-command.ts';
|
|
49
43
|
import { consumeReworkIntents, parseReworkIntentDocument, reworkIntentMutation, supersedeReworkIntents } from './rework-intent.ts';
|
|
50
44
|
import { ARTIFACT_FAMILIES, expectedQualificationRelations, parseQualificationAudit, parseTaskQualification, upstreamArtifactDigest, validateQualificationAudit } from './qualification-audit.ts';
|
|
51
45
|
import type { QualificationAudit, UpstreamRelation } from './qualification-audit.ts';
|
|
@@ -113,6 +107,23 @@ type TaskEventResult = {
|
|
|
113
107
|
operations: readonly TaskOperationSummary[]; error: TaskEventError | null;
|
|
114
108
|
};
|
|
115
109
|
|
|
110
|
+
function approvedCleanReviewedCommit(reviewContent: string, verdict: Verdict | undefined, repoRoot: string): string | null {
|
|
111
|
+
if (verdict !== 'approved') return null;
|
|
112
|
+
const reviewedHead = extractReviewedHead(reviewContent) || extractReviewBaseline(reviewContent);
|
|
113
|
+
const reviewedTree = extractReviewedSnapshotTree(reviewContent);
|
|
114
|
+
if (!/^[a-f0-9]{40}$/.test(reviewedHead) || !/^[a-f0-9]{40}$/.test(reviewedTree)) return null;
|
|
115
|
+
try {
|
|
116
|
+
const currentHead = execFileSync('git', ['-C', repoRoot, 'rev-parse', 'HEAD'], { encoding: 'utf8' }).trim();
|
|
117
|
+
if (currentHead !== reviewedHead) return null;
|
|
118
|
+
const currentTree = execFileSync('git', ['-C', repoRoot, 'rev-parse', 'HEAD^{tree}'], { encoding: 'utf8' }).trim();
|
|
119
|
+
if (currentTree !== reviewedTree) return null;
|
|
120
|
+
execFileSync('git', ['-C', repoRoot, 'diff-index', '--quiet', 'HEAD', '--']);
|
|
121
|
+
return reviewedHead;
|
|
122
|
+
} catch {
|
|
123
|
+
return null;
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
|
|
116
127
|
const BASE_FIELDS = new Set(['taskRef', 'event', 'agent', 'dryRun', 'overrideTicket', 'overrideTarget', 'overrideScope', 'initiator', 'requestId', 'reasonCode', 'sourceFinding', 'sourceArtifact', 'sourceSha256']);
|
|
117
128
|
const SCHEMAS: Record<TaskEventName, { required?: string[]; optional?: string[] }> = {
|
|
118
129
|
'analyze.started': { optional: ['round'] },
|
|
@@ -375,67 +386,6 @@ function reviewInputFamily(family: EventFamily): ArtifactFamily {
|
|
|
375
386
|
return family === 'review-analysis' ? 'analysis' : family === 'review-plan' ? 'plan' : 'code';
|
|
376
387
|
}
|
|
377
388
|
|
|
378
|
-
function reconcileReviewCompletionReplay(
|
|
379
|
-
repoRoot: string,
|
|
380
|
-
taskDir: string,
|
|
381
|
-
taskId: string,
|
|
382
|
-
family: EventFamily,
|
|
383
|
-
artifact: string,
|
|
384
|
-
round: number
|
|
385
|
-
): TaskEventError | null {
|
|
386
|
-
const stage = REVIEW_LEDGER_STAGES[family];
|
|
387
|
-
if (!stage) return null;
|
|
388
|
-
const reviewFamily = family as 'review-analysis' | 'review-plan' | 'review-code';
|
|
389
|
-
try {
|
|
390
|
-
let intent = readArtifactRecoveryIntent(repoRoot, taskId, reviewFamily, artifact);
|
|
391
|
-
if (!intent) return null;
|
|
392
|
-
if (intent.state === 'commit-started') {
|
|
393
|
-
const reconciled = reconcileArtifactRecovery(
|
|
394
|
-
recoveryContextFromIntent(repoRoot, taskDir, intent),
|
|
395
|
-
{ lockAlreadyHeld: true }
|
|
396
|
-
);
|
|
397
|
-
if (reconciled.status !== 'passed' && reconciled.status !== 'consumed') {
|
|
398
|
-
return {
|
|
399
|
-
code: 'EVENT_ARTIFACT_CONFLICT',
|
|
400
|
-
message: `review finalizer recovery remains ${reconciled.status} for ${artifact}`
|
|
401
|
-
};
|
|
402
|
-
}
|
|
403
|
-
intent = reconciled.intent;
|
|
404
|
-
}
|
|
405
|
-
if (!['passed', 'consumption-started', 'consumed'].includes(intent.state)) {
|
|
406
|
-
return {
|
|
407
|
-
code: 'EVENT_ARTIFACT_CONFLICT',
|
|
408
|
-
message: `review finalizer provenance is incomplete for ${artifact}`
|
|
409
|
-
};
|
|
410
|
-
}
|
|
411
|
-
const validated = validateCompletedArtifact(taskDir, FAMILY[family].artifact, artifact, round);
|
|
412
|
-
if (!validated.ok) {
|
|
413
|
-
return { code: 'EVENT_ARTIFACT_CONFLICT', message: validated.error.message };
|
|
414
|
-
}
|
|
415
|
-
const content = fs.readFileSync(validated.artifact.path, 'utf8');
|
|
416
|
-
const expectedRequestId = `review-finalize:${taskId}:${stage}:${round}`;
|
|
417
|
-
if (intent.round !== round
|
|
418
|
-
|| intent.requestId !== expectedRequestId
|
|
419
|
-
|| intent.finalArtifactSha256 !== sha256Content(content)
|
|
420
|
-
|| intent.finalSemanticDigest !== canonicalSemanticDigest(content)) {
|
|
421
|
-
return {
|
|
422
|
-
code: 'EVENT_ARTIFACT_CONFLICT',
|
|
423
|
-
message: `review finalizer provenance does not match ${artifact}`
|
|
424
|
-
};
|
|
425
|
-
}
|
|
426
|
-
if (intent.state === 'passed' || intent.state === 'consumption-started') {
|
|
427
|
-
const consumed = { ...intent, state: 'consumed' as const, updatedAt: Date.now() };
|
|
428
|
-
writeArtifactRecoveryIntent(repoRoot, consumed, { expected: intent });
|
|
429
|
-
}
|
|
430
|
-
return null;
|
|
431
|
-
} catch (error) {
|
|
432
|
-
return {
|
|
433
|
-
code: 'EVENT_ARTIFACT_CONFLICT',
|
|
434
|
-
message: `review finalizer recovery could not be reconciled for ${artifact}: ${error instanceof Error ? error.message : String(error)}`
|
|
435
|
-
};
|
|
436
|
-
}
|
|
437
|
-
}
|
|
438
|
-
|
|
439
389
|
function relationForStartedInput(family: EventFamily, input: ArtifactIdentity): UpstreamRelation['relation'] {
|
|
440
390
|
if (family.startsWith('review-')) {
|
|
441
391
|
return input.family === reviewInputFamily(family) ? 'reviewed-input' : 'approval-context';
|
|
@@ -645,34 +595,38 @@ function reworkIntentMutationForCompletion(
|
|
|
645
595
|
}
|
|
646
596
|
|
|
647
597
|
function buildCompletionReceipt(
|
|
598
|
+
content: string,
|
|
648
599
|
taskDir: string,
|
|
649
600
|
family: EventFamily,
|
|
650
601
|
artifact: ArtifactIdentity,
|
|
651
602
|
completedAt: string,
|
|
652
603
|
frontmatter: Record<string, unknown>
|
|
653
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
|
+
})();
|
|
654
612
|
if (family === 'code') {
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
if (!input) return { ok: false, message: `${artifact.name} does not reference a canonical plan artifact` };
|
|
660
|
-
const startedInput = typeof frontmatter.code_input_artifact === 'string' ? frontmatter.code_input_artifact : '';
|
|
661
|
-
const startedSha256 = typeof frontmatter.code_input_sha256 === 'string' ? frontmatter.code_input_sha256 : '';
|
|
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 ?? '';
|
|
662
617
|
if (!startedInput || !startedSha256) return { ok: false, message: 'code.started plan input context is missing' };
|
|
663
|
-
if (input.name !== startedInput) return { ok: false, message: `${artifact.name} plan input '${input.name}' does not match code.started input '${startedInput}'` };
|
|
664
618
|
const plan = inspectArtifactDirectory(taskDir, 'plan');
|
|
665
|
-
if (plan.status !== 'ready' || !plan.latest || plan.latest.name !==
|
|
666
|
-
return { ok: false, message: `code input '${
|
|
619
|
+
if (plan.status !== 'ready' || !plan.latest || plan.latest.name !== startedInput) {
|
|
620
|
+
return { ok: false, message: `code input '${startedInput}' is not the latest plan artifact` };
|
|
667
621
|
}
|
|
668
622
|
try {
|
|
669
623
|
const inputSha256 = sha256File(plan.latest.path);
|
|
670
|
-
if (inputSha256 !== startedSha256) return { ok: false, message: `code input ${
|
|
624
|
+
if (inputSha256 !== startedSha256) return { ok: false, message: `code input ${startedInput} changed after code.started` };
|
|
671
625
|
return {
|
|
672
626
|
ok: true,
|
|
673
627
|
receipt: {
|
|
674
|
-
event: 'code.completed', output: artifact.name, input:
|
|
675
|
-
inputSha256, completedAt
|
|
628
|
+
event: 'code.completed', output: artifact.name, input: startedInput,
|
|
629
|
+
inputSha256, completedAt: existing?.completedAt ?? completedAt
|
|
676
630
|
}
|
|
677
631
|
};
|
|
678
632
|
} catch (error) {
|
|
@@ -681,18 +635,14 @@ function buildCompletionReceipt(
|
|
|
681
635
|
}
|
|
682
636
|
if (!family.startsWith('review-')) return null;
|
|
683
637
|
const expectedFamily = reviewInputFamily(family);
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
if (!input) return { ok: false, message: `${artifact.name} does not reference a canonical ${expectedFamily} artifact` };
|
|
689
|
-
const startedInput = typeof frontmatter.review_input_artifact === 'string' ? frontmatter.review_input_artifact : '';
|
|
690
|
-
const startedSha256 = typeof frontmatter.review_input_sha256 === 'string' ? frontmatter.review_input_sha256 : '';
|
|
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 ?? '';
|
|
691
642
|
if (!startedInput || !startedSha256) return { ok: false, message: 'review started input context is missing' };
|
|
692
|
-
if (input.name !== startedInput) return { ok: false, message: `${artifact.name} input '${input.name}' does not match review.started input '${startedInput}'` };
|
|
693
643
|
const current = inspectArtifactDirectory(taskDir, expectedFamily);
|
|
694
|
-
if (current.status !== 'ready' || !current.latest || current.latest.name !==
|
|
695
|
-
return { ok: false, message: `review input '${
|
|
644
|
+
if (current.status !== 'ready' || !current.latest || current.latest.name !== startedInput) {
|
|
645
|
+
return { ok: false, message: `review input '${startedInput}' is not the latest ${expectedFamily} artifact` };
|
|
696
646
|
}
|
|
697
647
|
const event = family === 'review-analysis'
|
|
698
648
|
? 'review-analysis.completed' as const
|
|
@@ -701,19 +651,82 @@ function buildCompletionReceipt(
|
|
|
701
651
|
const stat = fs.lstatSync(current.latest.path);
|
|
702
652
|
if (!stat.isFile() || stat.isSymbolicLink()) throw new Error('review input is not a regular file');
|
|
703
653
|
const inputSha256 = sha256File(current.latest.path);
|
|
704
|
-
if (inputSha256 !== startedSha256) return { ok: false, message: `review input ${
|
|
654
|
+
if (inputSha256 !== startedSha256) return { ok: false, message: `review input ${startedInput} changed after review.started` };
|
|
705
655
|
return {
|
|
706
656
|
ok: true,
|
|
707
657
|
receipt: {
|
|
708
|
-
event, output: artifact.name, input:
|
|
709
|
-
inputSha256, completedAt
|
|
658
|
+
event, output: artifact.name, input: startedInput,
|
|
659
|
+
inputSha256, completedAt: existing?.completedAt ?? completedAt
|
|
710
660
|
}
|
|
711
661
|
};
|
|
712
662
|
} catch (error) {
|
|
713
|
-
return { ok: false, message: `cannot hash review input ${
|
|
663
|
+
return { ok: false, message: `cannot hash review input ${startedInput}: ${error instanceof Error ? error.message : String(error)}` };
|
|
714
664
|
}
|
|
715
665
|
}
|
|
716
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
|
+
|
|
717
730
|
function applyTaskEventUnlocked(request: TaskEventRequest, options: TaskEventOptions = {}): TaskEventResult {
|
|
718
731
|
const invalid = validateTaskEventRequest(request);
|
|
719
732
|
if (invalid) return failed(request, invalid);
|
|
@@ -724,6 +737,20 @@ function applyTaskEventUnlocked(request: TaskEventRequest, options: TaskEventOpt
|
|
|
724
737
|
let content: string;
|
|
725
738
|
try { content = fs.readFileSync(resolved.taskMdPath, 'utf8'); }
|
|
726
739
|
catch (error) { return failed(request, { code: 'TASK_READ_FAILED', message: String(error) }, { taskId: resolved.taskId, taskMdPath: resolved.taskMdPath }); }
|
|
740
|
+
const initialParts = eventParts(request.event);
|
|
741
|
+
if (initialParts.phase === 'started' && !request.dryRun) {
|
|
742
|
+
const reconciled = reconcileTaskInvalidation(request.taskRef, { repoRoot: resolved.repoRoot });
|
|
743
|
+
if (reconciled.status === 'failed') {
|
|
744
|
+
return failed(request, {
|
|
745
|
+
code: 'EVENT_TRANSITION_INVALID',
|
|
746
|
+
message: `INVALIDATION_RECONCILE_FAILED: ${reconciled.error?.code ?? 'UNKNOWN'}: ${reconciled.error?.message ?? ''}`
|
|
747
|
+
}, { taskId: resolved.taskId, taskMdPath: resolved.taskMdPath });
|
|
748
|
+
}
|
|
749
|
+
if (reconciled.changed) {
|
|
750
|
+
try { content = fs.readFileSync(resolved.taskMdPath, 'utf8'); }
|
|
751
|
+
catch (error) { return failed(request, { code: 'TASK_READ_FAILED', message: String(error) }, { taskId: resolved.taskId, taskMdPath: resolved.taskMdPath }); }
|
|
752
|
+
}
|
|
753
|
+
}
|
|
727
754
|
let frontmatter;
|
|
728
755
|
try { frontmatter = parseTypedTaskFrontmatter(content); }
|
|
729
756
|
catch (error) { return failed(request, { code: 'TASK_DOCUMENT_INVALID', message: error instanceof Error ? error.message : String(error) }, { taskId: resolved.taskId, taskMdPath: resolved.taskMdPath }); }
|
|
@@ -732,7 +759,6 @@ function applyTaskEventUnlocked(request: TaskEventRequest, options: TaskEventOpt
|
|
|
732
759
|
const rows = startedBackedRows(pairEntries(section.entries));
|
|
733
760
|
let normalized = request;
|
|
734
761
|
let artifactContext: ArtifactContextResult | null = null;
|
|
735
|
-
const initialParts = eventParts(request.event);
|
|
736
762
|
if (initialParts.phase === 'started') {
|
|
737
763
|
const openIdentity = openStartedIdentity(rows, initialParts.family);
|
|
738
764
|
if (openIdentity && 'conflict' in openIdentity) return failed(request, { code: 'EVENT_LOG_CONFLICT', message: 'artifact family has more than one open started event' }, { taskId: resolved.taskId, taskMdPath: resolved.taskMdPath });
|
|
@@ -777,13 +803,11 @@ function applyTaskEventUnlocked(request: TaskEventRequest, options: TaskEventOpt
|
|
|
777
803
|
const currentStep = typeof frontmatter.current_step === 'string' ? frontmatter.current_step : '';
|
|
778
804
|
const matchingRows = rows.filter((item) => item.step === eventIdentity.action);
|
|
779
805
|
const manual = eventIdentity.family === 'manual-validation';
|
|
780
|
-
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 });
|
|
781
806
|
const openRows = matchingRows.filter((item) => item.started && !item.done);
|
|
782
807
|
const completedRows = matchingRows.filter((item) => item.done);
|
|
783
|
-
|
|
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);
|
|
784
810
|
let completedArtifact: ArtifactIdentity | null = null;
|
|
785
|
-
let localFinalizationIntent: LocalArtifactFinalizationIntent | null = null;
|
|
786
|
-
let reviewFinalizationIntent: ArtifactRecoveryIntent | null = null;
|
|
787
811
|
let reviewContent: string | null = null;
|
|
788
812
|
if (eventIdentity.phase === 'started' && row) {
|
|
789
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 });
|
|
@@ -795,24 +819,7 @@ function applyTaskEventUnlocked(request: TaskEventRequest, options: TaskEventOpt
|
|
|
795
819
|
}
|
|
796
820
|
return successNoOp(normalized, resolved.taskId, resolved.taskMdPath, currentStep, eventIdentity, row.started, frontmatter, artifactContext);
|
|
797
821
|
}
|
|
798
|
-
if (eventIdentity.phase === '
|
|
799
|
-
const done = completedRows.find((item) => item.note === eventIdentity.note);
|
|
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
|
-
}
|
|
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 });
|
|
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 });
|
|
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 });
|
|
816
823
|
if (eventIdentity.phase === 'completed') {
|
|
817
824
|
const validated = validateCompletedArtifact(resolved.taskDir, FAMILY[eventIdentity.family].artifact, normalized.artifact!, normalized.round);
|
|
818
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 });
|
|
@@ -853,23 +860,6 @@ function applyTaskEventUnlocked(request: TaskEventRequest, options: TaskEventOpt
|
|
|
853
860
|
const localFamily = eventIdentity.family === 'analyze'
|
|
854
861
|
? 'analysis'
|
|
855
862
|
: 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
|
-
}
|
|
873
863
|
let artifactContent: string;
|
|
874
864
|
try { artifactContent = fs.readFileSync(completedArtifact.path, 'utf8'); }
|
|
875
865
|
catch (error) {
|
|
@@ -899,29 +889,6 @@ function applyTaskEventUnlocked(request: TaskEventRequest, options: TaskEventOpt
|
|
|
899
889
|
message: `semantic digest does not match finalizer result for ${completedArtifact.name}`
|
|
900
890
|
}, { taskId: resolved.taskId, taskMdPath: resolved.taskMdPath, fromStep: currentStep, toStep: currentStep, action: eventIdentity.action, phase: eventIdentity.phase });
|
|
901
891
|
}
|
|
902
|
-
if (!localFinalizationIntent || !['passed', 'consumed'].includes(localFinalizationIntent.state)) {
|
|
903
|
-
return failed(normalized, {
|
|
904
|
-
code: 'EVENT_ARTIFACT_CONFLICT',
|
|
905
|
-
message: `local finalizer provenance is missing or incomplete for ${completedArtifact.name}`
|
|
906
|
-
}, { taskId: resolved.taskId, taskMdPath: resolved.taskMdPath, fromStep: currentStep, toStep: currentStep, action: eventIdentity.action, phase: eventIdentity.phase });
|
|
907
|
-
}
|
|
908
|
-
if (localFinalizationIntent.finalArtifactSha256 !== actualSha256 || localFinalizationIntent.finalSemanticDigest !== local.semanticDigest) {
|
|
909
|
-
return failed(normalized, {
|
|
910
|
-
code: 'EVENT_ARTIFACT_CONFLICT',
|
|
911
|
-
message: `local finalizer provenance does not match ${completedArtifact.name}`
|
|
912
|
-
}, { taskId: resolved.taskId, taskMdPath: resolved.taskMdPath, fromStep: currentStep, toStep: currentStep, action: eventIdentity.action, phase: eventIdentity.phase });
|
|
913
|
-
}
|
|
914
|
-
if (localFinalizationIntent.authorityDigest !== null) {
|
|
915
|
-
if (!lifecycleAuthority
|
|
916
|
-
|| localFinalizationIntent.recoveryOperationId !== lifecycleAuthority.operationId
|
|
917
|
-
|| localFinalizationIntent.requestId !== lifecycleAuthority.lifecycleRequestId
|
|
918
|
-
|| localFinalizationIntent.authorityDigest !== lifecycleRecoveryAttestationDigest(lifecycleAuthority)) {
|
|
919
|
-
return failed(normalized, {
|
|
920
|
-
code: 'EVENT_ARTIFACT_CONFLICT',
|
|
921
|
-
message: `local finalizer provenance does not match lifecycle completion authority for ${completedArtifact.name}`
|
|
922
|
-
}, { taskId: resolved.taskId, taskMdPath: resolved.taskMdPath, fromStep: currentStep, toStep: currentStep, action: eventIdentity.action, phase: eventIdentity.phase, artifactContext });
|
|
923
|
-
}
|
|
924
|
-
}
|
|
925
892
|
}
|
|
926
893
|
}
|
|
927
894
|
if (eventIdentity.phase === 'waiting' && section.entries.some((entry) => entry.step === eventIdentity.action && entry.note === eventIdentity.note)) {
|
|
@@ -948,49 +915,6 @@ function applyTaskEventUnlocked(request: TaskEventRequest, options: TaskEventOpt
|
|
|
948
915
|
completedArtifact?.path ?? null
|
|
949
916
|
);
|
|
950
917
|
if (findingCountError) return failed(normalized, findingCountError, { taskId: resolved.taskId, taskMdPath: resolved.taskMdPath, fromStep: currentStep, toStep: currentStep, action: eventIdentity.action, phase: eventIdentity.phase });
|
|
951
|
-
if (eventIdentity.phase === 'completed' && eventIdentity.family.startsWith('review-') && completedArtifact && reviewContent !== null) {
|
|
952
|
-
try {
|
|
953
|
-
reviewFinalizationIntent = readArtifactRecoveryIntent(
|
|
954
|
-
resolved.repoRoot,
|
|
955
|
-
resolved.taskId,
|
|
956
|
-
eventIdentity.family as 'review-analysis' | 'review-plan' | 'review-code',
|
|
957
|
-
completedArtifact.name
|
|
958
|
-
);
|
|
959
|
-
if (!normalized.dryRun && reviewFinalizationIntent?.state === 'commit-started') {
|
|
960
|
-
const reconciled = reconcileArtifactRecovery(
|
|
961
|
-
recoveryContextFromIntent(resolved.repoRoot, resolved.taskDir, reviewFinalizationIntent),
|
|
962
|
-
{ lockAlreadyHeld: true }
|
|
963
|
-
);
|
|
964
|
-
if (reconciled.status === 'passed' || reconciled.status === 'consumed') reviewFinalizationIntent = reconciled.intent;
|
|
965
|
-
}
|
|
966
|
-
if (!reviewFinalizationIntent || !['passed', 'consumption-started', 'consumed'].includes(reviewFinalizationIntent.state)) {
|
|
967
|
-
return failed(normalized, {
|
|
968
|
-
code: 'EVENT_ARTIFACT_CONFLICT',
|
|
969
|
-
message: `review finalizer provenance is missing or incomplete for ${completedArtifact.name}`
|
|
970
|
-
}, { taskId: resolved.taskId, taskMdPath: resolved.taskMdPath, fromStep: currentStep, toStep: currentStep, action: eventIdentity.action, phase: eventIdentity.phase });
|
|
971
|
-
}
|
|
972
|
-
const reviewStage = eventIdentity.family === 'review-analysis'
|
|
973
|
-
? 'analysis'
|
|
974
|
-
: eventIdentity.family === 'review-plan' ? 'plan' : 'code';
|
|
975
|
-
const actualSha256 = sha256File(completedArtifact.path);
|
|
976
|
-
const actualSemanticDigest = canonicalSemanticDigest(reviewContent);
|
|
977
|
-
const expectedRequestId = `review-finalize:${resolved.taskId}:${reviewStage}:${normalized.round}`;
|
|
978
|
-
if (reviewFinalizationIntent.round !== normalized.round
|
|
979
|
-
|| reviewFinalizationIntent.requestId !== expectedRequestId
|
|
980
|
-
|| reviewFinalizationIntent.finalArtifactSha256 !== actualSha256
|
|
981
|
-
|| reviewFinalizationIntent.finalSemanticDigest !== actualSemanticDigest) {
|
|
982
|
-
return failed(normalized, {
|
|
983
|
-
code: 'EVENT_ARTIFACT_CONFLICT',
|
|
984
|
-
message: `review finalizer provenance does not match ${completedArtifact.name}`
|
|
985
|
-
}, { taskId: resolved.taskId, taskMdPath: resolved.taskMdPath, fromStep: currentStep, toStep: currentStep, action: eventIdentity.action, phase: eventIdentity.phase });
|
|
986
|
-
}
|
|
987
|
-
} catch (error) {
|
|
988
|
-
return failed(normalized, {
|
|
989
|
-
code: 'EVENT_ARTIFACT_CONFLICT',
|
|
990
|
-
message: `review finalizer recovery could not be reconciled for ${completedArtifact.name}: ${error instanceof Error ? error.message : String(error)}`
|
|
991
|
-
}, { taskId: resolved.taskId, taskMdPath: resolved.taskMdPath, fromStep: currentStep, toStep: currentStep, action: eventIdentity.action, phase: eventIdentity.phase });
|
|
992
|
-
}
|
|
993
|
-
}
|
|
994
918
|
let orchestrationCompletion: OrchestrationStageCompletion | null = null;
|
|
995
919
|
if (eventIdentity.phase === 'completed' && eventIdentity.family !== 'manual-validation' && eventIdentity.family !== 'validation-run') {
|
|
996
920
|
const orchestrationStage = eventIdentity.family === 'analyze' ? 'analysis' : eventIdentity.family;
|
|
@@ -1017,14 +941,32 @@ function applyTaskEventUnlocked(request: TaskEventRequest, options: TaskEventOpt
|
|
|
1017
941
|
try { metadata = (options.metadataProvider ?? captureTaskWriteMetadata)(); }
|
|
1018
942
|
catch (error) { return failed(normalized, { code: 'METADATA_CAPTURE_FAILED', message: String(error) }, { taskId: resolved.taskId, taskMdPath: resolved.taskMdPath }); }
|
|
1019
943
|
let completionReceipt: ArtifactReceipt | null = null;
|
|
944
|
+
let currentFact: CompletionFact | null = null;
|
|
1020
945
|
let completionInvalidation: ReturnType<typeof invalidationMutation> | null = null;
|
|
1021
946
|
let completionRework: ReturnType<typeof reworkIntentMutation> | null = null;
|
|
1022
947
|
if (eventIdentity.phase === 'completed' && completedArtifact) {
|
|
1023
|
-
const receipt = buildCompletionReceipt(resolved.taskDir, eventIdentity.family, completedArtifact, metadata.timestamp, frontmatter);
|
|
948
|
+
const receipt = buildCompletionReceipt(content, resolved.taskDir, eventIdentity.family, completedArtifact, metadata.timestamp, frontmatter);
|
|
1024
949
|
if (receipt && !receipt.ok) {
|
|
1025
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 });
|
|
1026
951
|
}
|
|
1027
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
|
+
}
|
|
1028
970
|
try {
|
|
1029
971
|
const invalidation = invalidationMutationForCompletion(content, resolved.taskDir, eventIdentity.family, completedArtifact, metadata.timestamp);
|
|
1030
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 });
|
|
@@ -1040,6 +982,7 @@ function applyTaskEventUnlocked(request: TaskEventRequest, options: TaskEventOpt
|
|
|
1040
982
|
const logStep = eventIdentity.phase === 'started' ? `${eventIdentity.action} [started]` : eventIdentity.action;
|
|
1041
983
|
const body = appendActivityEntry(section, { time: metadata.timestamp, step: logStep, agent: normalized.agent, note: eventIdentity.note });
|
|
1042
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));
|
|
1043
986
|
let frontmatterRemove: string[] | undefined;
|
|
1044
987
|
if (eventIdentity.phase === 'started' && artifactContext) {
|
|
1045
988
|
try {
|
|
@@ -1072,6 +1015,11 @@ function applyTaskEventUnlocked(request: TaskEventRequest, options: TaskEventOpt
|
|
|
1072
1015
|
} else if (eventIdentity.phase === 'completed' && eventIdentity.family.startsWith('review-')) {
|
|
1073
1016
|
frontmatterRemove = ['review_input_artifact', 'review_input_sha256'];
|
|
1074
1017
|
}
|
|
1018
|
+
if (eventIdentity.phase === 'completed' && eventIdentity.family === 'review-code' && reviewContent !== null) {
|
|
1019
|
+
const reviewedCommit = approvedCleanReviewedCommit(reviewContent, normalized.verdict, resolved.repoRoot);
|
|
1020
|
+
if (reviewedCommit) frontmatterSet.last_reviewed_commit = reviewedCommit;
|
|
1021
|
+
else if (normalized.verdict === 'approved') frontmatterSet.last_reviewed_commit = '';
|
|
1022
|
+
}
|
|
1075
1023
|
if (eventIdentity.phase === 'completed') frontmatterRemove = [...(frontmatterRemove ?? []), 'qualification_input_relations'];
|
|
1076
1024
|
if (eventIdentity.phase === 'started' && normalized.implementationInput) frontmatterSet.last_reviewed_commit = '';
|
|
1077
1025
|
const mutations: Parameters<typeof writeTask>[0]['mutations'][number][] = [
|
|
@@ -1110,90 +1058,10 @@ function applyTaskEventUnlocked(request: TaskEventRequest, options: TaskEventOpt
|
|
|
1110
1058
|
});
|
|
1111
1059
|
}
|
|
1112
1060
|
mutations.push({ kind: 'section', aliases: ['活动日志', 'Activity Log'], heading: section.heading, body });
|
|
1113
|
-
if (!normalized.dryRun && localFinalizationIntent && completedArtifact) {
|
|
1114
|
-
try {
|
|
1115
|
-
if (lifecycleAuthority && localFinalizationIntent.state === 'passed') {
|
|
1116
|
-
const expectedIntent = localFinalizationIntent;
|
|
1117
|
-
localFinalizationIntent = {
|
|
1118
|
-
...localFinalizationIntent,
|
|
1119
|
-
state: 'consumption-started',
|
|
1120
|
-
phase: 'task-event.completed',
|
|
1121
|
-
authorityDigest: lifecycleRecoveryAttestationDigest(lifecycleAuthority),
|
|
1122
|
-
requestId: lifecycleAuthority.lifecycleRequestId,
|
|
1123
|
-
updatedAt: Date.now()
|
|
1124
|
-
};
|
|
1125
|
-
writeArtifactRecoveryIntent(resolved.repoRoot, localFinalizationIntent, { expected: expectedIntent });
|
|
1126
|
-
} else if (!lifecycleAuthority && localFinalizationIntent.state === 'passed') {
|
|
1127
|
-
localFinalizationIntent = consumeLocalArtifactFinalizationIntent(resolved.repoRoot, localFinalizationIntent, { lockAlreadyHeld: options.lockAlreadyHeld });
|
|
1128
|
-
}
|
|
1129
|
-
} catch (error) {
|
|
1130
|
-
return failed(normalized, {
|
|
1131
|
-
code: 'EVENT_ARTIFACT_CONFLICT',
|
|
1132
|
-
message: `local finalizer provenance could not be advanced before task write for ${completedArtifact.name}: ${error instanceof Error ? error.message : String(error)}`
|
|
1133
|
-
}, {
|
|
1134
|
-
taskId: resolved.taskId,
|
|
1135
|
-
taskMdPath: resolved.taskMdPath,
|
|
1136
|
-
fromStep: currentStep,
|
|
1137
|
-
toStep: step,
|
|
1138
|
-
action: eventIdentity.action,
|
|
1139
|
-
phase: eventIdentity.phase,
|
|
1140
|
-
artifactContext
|
|
1141
|
-
});
|
|
1142
|
-
}
|
|
1143
|
-
}
|
|
1144
|
-
if (!normalized.dryRun && reviewFinalizationIntent && completedArtifact && reviewFinalizationIntent.state === 'passed') {
|
|
1145
|
-
try {
|
|
1146
|
-
const expectedIntent = reviewFinalizationIntent;
|
|
1147
|
-
reviewFinalizationIntent = {
|
|
1148
|
-
...reviewFinalizationIntent,
|
|
1149
|
-
state: 'consumption-started',
|
|
1150
|
-
phase: 'task-event.completed',
|
|
1151
|
-
updatedAt: Date.now()
|
|
1152
|
-
};
|
|
1153
|
-
writeArtifactRecoveryIntent(resolved.repoRoot, reviewFinalizationIntent, { expected: expectedIntent });
|
|
1154
|
-
} catch (error) {
|
|
1155
|
-
return failed(normalized, {
|
|
1156
|
-
code: 'EVENT_ARTIFACT_CONFLICT',
|
|
1157
|
-
message: `review finalizer provenance could not be advanced before task write for ${completedArtifact.name}: ${error instanceof Error ? error.message : String(error)}`
|
|
1158
|
-
}, {
|
|
1159
|
-
taskId: resolved.taskId,
|
|
1160
|
-
taskMdPath: resolved.taskMdPath,
|
|
1161
|
-
fromStep: currentStep,
|
|
1162
|
-
toStep: step,
|
|
1163
|
-
action: eventIdentity.action,
|
|
1164
|
-
phase: eventIdentity.phase,
|
|
1165
|
-
artifactContext
|
|
1166
|
-
});
|
|
1167
|
-
}
|
|
1168
|
-
}
|
|
1169
1061
|
const sourceCompletion = eventIdentity.phase === 'completed'
|
|
1170
1062
|
&& ['analyze', 'plan', 'code'].includes(eventIdentity.family);
|
|
1171
1063
|
const result = writeTask({ taskRef: normalized.taskRef, expectedState: stateOverride ? resolved.state : 'active', dryRun: normalized.dryRun, mutations }, { ...options, invalidationContext: sourceCompletion ? 'source-completion' : 'standard', metadataProvider: () => metadata });
|
|
1172
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 });
|
|
1173
|
-
if (!normalized.dryRun && lifecycleAuthority && localFinalizationIntent && completedArtifact) {
|
|
1174
|
-
try {
|
|
1175
|
-
if (!options.deferLifecycleRecoveryConsumption) consumeLifecycleRecoveryAttestation(lifecycleAuthority);
|
|
1176
|
-
writeArtifactRecoveryIntent(resolved.repoRoot, { ...localFinalizationIntent, state: 'consumed', updatedAt: Date.now() }, { expected: localFinalizationIntent });
|
|
1177
|
-
} catch (error) {
|
|
1178
|
-
return failed(normalized, {
|
|
1179
|
-
code: 'EVENT_ARTIFACT_CONFLICT',
|
|
1180
|
-
message: `task completion was written but lifecycle recovery consumption is incomplete: ${error instanceof Error ? error.message : String(error)}`
|
|
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 });
|
|
1182
|
-
}
|
|
1183
|
-
}
|
|
1184
|
-
if (!normalized.dryRun && reviewFinalizationIntent && completedArtifact) {
|
|
1185
|
-
try {
|
|
1186
|
-
if (reviewFinalizationIntent.state === 'consumption-started') {
|
|
1187
|
-
const consumed = { ...reviewFinalizationIntent, state: 'consumed' as const, updatedAt: Date.now() };
|
|
1188
|
-
writeArtifactRecoveryIntent(resolved.repoRoot, consumed, { expected: reviewFinalizationIntent });
|
|
1189
|
-
}
|
|
1190
|
-
} catch (error) {
|
|
1191
|
-
return failed(normalized, {
|
|
1192
|
-
code: 'EVENT_ARTIFACT_CONFLICT',
|
|
1193
|
-
message: `review finalizer recovery consumption is incomplete: ${error instanceof Error ? error.message : String(error)}`
|
|
1194
|
-
}, { taskId: result.taskId, taskMdPath: result.taskMdPath, fromStep: currentStep, toStep: step, action: eventIdentity.action, phase: eventIdentity.phase, timestamp: result.timestamp, agentInfraVersion: result.agentInfraVersion, operations: result.operations, artifactContext });
|
|
1195
|
-
}
|
|
1196
|
-
}
|
|
1197
1065
|
if (!normalized.dryRun && orchestrationCompletion) {
|
|
1198
1066
|
try {
|
|
1199
1067
|
(options.commitOrchestrationCompletion ?? commitOrchestrationStageCompletion)(orchestrationCompletion);
|
|
@@ -1231,7 +1099,7 @@ function applyTaskEvent(request: TaskEventRequest, options: TaskEventOptions = {
|
|
|
1231
1099
|
if (invalid) return failed(request, invalid);
|
|
1232
1100
|
const parts = eventParts(request.event);
|
|
1233
1101
|
if (options.lockAlreadyHeld) return applyTaskEventUnlocked(request, options);
|
|
1234
|
-
if (request.dryRun ||
|
|
1102
|
+
if (request.dryRun || !['started', 'completed'].includes(parts.phase)) {
|
|
1235
1103
|
return applyTaskEventUnlocked(request, options);
|
|
1236
1104
|
}
|
|
1237
1105
|
const resolved = resolveTaskRef(request.taskRef, { repoRoot: options.repoRoot });
|
|
@@ -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.postSummaryVerification === 'pending' || 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
|
}
|