@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.
Files changed (146) hide show
  1. package/dist/lib/internal/cli-route-inventory.js +1 -1
  2. package/dist/lib/internal/manual-validation.js +6 -2
  3. package/dist/lib/internal/platform-comment.js +59 -0
  4. package/dist/lib/internal/sandbox-control.js +4 -131
  5. package/dist/lib/internal/task-artifact.js +1 -1
  6. package/dist/lib/internal/task-operation-registry.js +1 -0
  7. package/dist/lib/internal/task-review.js +5 -5
  8. package/dist/lib/internal/task-validate.js +0 -6
  9. package/dist/lib/platform/comment-safety.js +7 -1
  10. package/dist/lib/platform/github-provider.js +13 -3
  11. package/dist/lib/platform/issue-comments.js +120 -13
  12. package/dist/lib/platform/issues.js +3 -4
  13. package/dist/lib/platform/provider-contract.d.ts +2 -0
  14. package/dist/lib/platform/provider-validation.js +7 -2
  15. package/dist/lib/platform/verification-sync.js +161 -156
  16. package/dist/lib/sandbox/control/executor.js +0 -38
  17. package/dist/lib/sandbox/control/finalization-response.js +10 -2
  18. package/dist/lib/sandbox/control/task-view.js +3 -1
  19. package/dist/lib/sandbox/control/workflow-executor.js +32 -41
  20. package/dist/lib/task/artifact-command.js +6 -6
  21. package/dist/lib/task/artifact-lifecycle.js +15 -52
  22. package/dist/lib/task/artifact-operations.js +27 -1
  23. package/dist/lib/task/capabilities.js +0 -2
  24. package/dist/lib/task/codex-orchestration.js +10 -208
  25. package/dist/lib/task/commit-operation.js +3 -18
  26. package/dist/lib/task/control-authority.js +1 -8
  27. package/dist/lib/task/delegation-receipts.js +14 -76
  28. package/dist/lib/task/events.js +147 -278
  29. package/dist/lib/task/finalization-state.js +2 -1
  30. package/dist/lib/task/finalization.js +273 -49
  31. package/dist/lib/task/gate-policy.js +94 -0
  32. package/dist/lib/task/lifecycle-execution.js +2 -11
  33. package/dist/lib/task/local-artifact-finalization.js +31 -245
  34. package/dist/lib/task/orchestration.js +8 -24
  35. package/dist/lib/task/review-artifacts.js +12 -6
  36. package/dist/lib/task/review-command.js +0 -1
  37. package/dist/lib/task/review-finalization.js +47 -218
  38. package/dist/lib/task/verification-engine.js +28 -17
  39. package/dist/lib/task/verification.js +25 -9
  40. package/dist/lib/task/workflow-command.js +4 -1
  41. package/dist/lib/task/workflow-dispatch.js +55 -3
  42. package/dist/lifecycle-build-manifest.json +3 -3
  43. package/lib/internal/cli-route-inventory.ts +1 -1
  44. package/lib/internal/manual-validation.ts +6 -2
  45. package/lib/internal/platform-comment.ts +38 -0
  46. package/lib/internal/sandbox-control.ts +3 -145
  47. package/lib/internal/task-artifact.ts +1 -1
  48. package/lib/internal/task-operation-registry.ts +1 -0
  49. package/lib/internal/task-review.ts +5 -5
  50. package/lib/internal/task-validate.ts +0 -5
  51. package/lib/platform/comment-safety.ts +8 -0
  52. package/lib/platform/github-provider.ts +11 -2
  53. package/lib/platform/issue-comments.ts +132 -13
  54. package/lib/platform/issues.ts +3 -4
  55. package/lib/platform/provider-contract.ts +2 -0
  56. package/lib/platform/provider-validation.ts +6 -2
  57. package/lib/platform/verification-sync.ts +178 -179
  58. package/lib/sandbox/control/executor.ts +0 -44
  59. package/lib/sandbox/control/finalization-response.ts +10 -2
  60. package/lib/sandbox/control/task-view.ts +3 -1
  61. package/lib/sandbox/control/workflow-executor.ts +31 -48
  62. package/lib/task/artifact-command.ts +6 -7
  63. package/lib/task/artifact-lifecycle.ts +12 -49
  64. package/lib/task/artifact-operations.ts +27 -1
  65. package/lib/task/capabilities.ts +0 -1
  66. package/lib/task/codex-orchestration.ts +8 -241
  67. package/lib/task/commit-operation.ts +3 -23
  68. package/lib/task/control-authority.ts +1 -8
  69. package/lib/task/delegation-receipts.ts +12 -79
  70. package/lib/task/events.ts +155 -287
  71. package/lib/task/finalization-state.ts +2 -1
  72. package/lib/task/finalization.ts +277 -57
  73. package/lib/task/gate-policy.ts +133 -0
  74. package/lib/task/lifecycle-execution.ts +10 -21
  75. package/lib/task/local-artifact-finalization.ts +29 -285
  76. package/lib/task/orchestration.ts +12 -41
  77. package/lib/task/review-artifacts.ts +8 -2
  78. package/lib/task/review-command.ts +0 -1
  79. package/lib/task/review-finalization.ts +46 -200
  80. package/lib/task/verification-engine.ts +24 -14
  81. package/lib/task/verification-types.ts +7 -0
  82. package/lib/task/verification.ts +25 -9
  83. package/lib/task/workflow-command.ts +4 -1
  84. package/lib/task/workflow-dispatch.ts +46 -3
  85. package/package.json +2 -2
  86. package/templates/.agents/rules/issue-sync.en.md +2 -0
  87. package/templates/.agents/rules/issue-sync.zh-CN.md +5 -8
  88. package/templates/.agents/rules/lifecycle-orchestration.en.md +7 -7
  89. package/templates/.agents/rules/lifecycle-orchestration.zh-CN.md +7 -7
  90. package/templates/.agents/rules/local-artifact-repair.en.md +14 -52
  91. package/templates/.agents/rules/local-artifact-repair.zh-CN.md +13 -52
  92. package/templates/.agents/skills/analyze-task/SKILL.en.md +3 -3
  93. package/templates/.agents/skills/analyze-task/SKILL.zh-CN.md +3 -3
  94. package/templates/.agents/skills/analyze-task/config/verify.en.json +1 -10
  95. package/templates/.agents/skills/analyze-task/config/verify.zh-CN.json +1 -10
  96. package/templates/.agents/skills/block-task/config/verify.json +1 -3
  97. package/templates/.agents/skills/cancel-task/config/verify.json +1 -4
  98. package/templates/.agents/skills/code-task/SKILL.en.md +3 -3
  99. package/templates/.agents/skills/code-task/SKILL.zh-CN.md +3 -3
  100. package/templates/.agents/skills/code-task/config/verify.en.json +1 -12
  101. package/templates/.agents/skills/code-task/config/verify.zh-CN.json +1 -12
  102. package/templates/.agents/skills/code-task/reference/dual-mode.en.md +1 -1
  103. package/templates/.agents/skills/code-task/reference/dual-mode.zh-CN.md +1 -1
  104. package/templates/.agents/skills/code-task/reference/report-template.en.md +11 -0
  105. package/templates/.agents/skills/code-task/reference/report-template.zh-CN.md +11 -0
  106. package/templates/.agents/skills/commit/config/verify.json +1 -5
  107. package/templates/.agents/skills/complete-manual-validation/config/verify.en.json +6 -6
  108. package/templates/.agents/skills/complete-manual-validation/config/verify.zh-CN.json +6 -6
  109. package/templates/.agents/skills/complete-task/SKILL.en.md +3 -3
  110. package/templates/.agents/skills/complete-task/SKILL.zh-CN.md +3 -3
  111. package/templates/.agents/skills/complete-task/config/verify.en.json +3 -14
  112. package/templates/.agents/skills/complete-task/config/verify.zh-CN.json +3 -14
  113. package/templates/.agents/skills/create-pr/config/verify.json +1 -10
  114. package/templates/.agents/skills/create-task/config/verify.json +1 -4
  115. package/templates/.agents/skills/import-issue/config/verify.json +1 -2
  116. package/templates/.agents/skills/plan-task/SKILL.en.md +3 -3
  117. package/templates/.agents/skills/plan-task/SKILL.zh-CN.md +3 -3
  118. package/templates/.agents/skills/plan-task/config/verify.en.json +1 -10
  119. package/templates/.agents/skills/plan-task/config/verify.zh-CN.json +1 -10
  120. package/templates/.agents/skills/restore-task/SKILL.zh-CN.md +4 -4
  121. package/templates/.agents/skills/review-analysis/SKILL.en.md +2 -2
  122. package/templates/.agents/skills/review-analysis/SKILL.zh-CN.md +2 -2
  123. package/templates/.agents/skills/review-analysis/config/verify.en.json +1 -10
  124. package/templates/.agents/skills/review-analysis/config/verify.zh-CN.json +1 -10
  125. package/templates/.agents/skills/review-analysis/reference/report-template.en.md +2 -0
  126. package/templates/.agents/skills/review-analysis/reference/report-template.zh-CN.md +2 -0
  127. package/templates/.agents/skills/review-code/SKILL.en.md +3 -3
  128. package/templates/.agents/skills/review-code/SKILL.zh-CN.md +3 -3
  129. package/templates/.agents/skills/review-code/config/verify.en.json +1 -10
  130. package/templates/.agents/skills/review-code/config/verify.zh-CN.json +1 -10
  131. package/templates/.agents/skills/review-code/reference/report-template.en.md +2 -0
  132. package/templates/.agents/skills/review-code/reference/report-template.zh-CN.md +2 -0
  133. package/templates/.agents/skills/review-plan/SKILL.en.md +2 -2
  134. package/templates/.agents/skills/review-plan/SKILL.zh-CN.md +2 -2
  135. package/templates/.agents/skills/review-plan/config/verify.en.json +1 -10
  136. package/templates/.agents/skills/review-plan/config/verify.zh-CN.json +1 -10
  137. package/templates/.agents/skills/review-plan/reference/report-template.en.md +2 -0
  138. package/templates/.agents/skills/review-plan/reference/report-template.zh-CN.md +2 -0
  139. package/templates/.agents/skills/review-pr/config/verify.en.json +1 -8
  140. package/templates/.agents/skills/review-pr/config/verify.zh-CN.json +1 -8
  141. package/templates/.agents/skills/run-manual-validation/config/verify.en.json +38 -13
  142. package/templates/.agents/skills/run-manual-validation/config/verify.zh-CN.json +38 -13
  143. package/templates/.agents/skills/run-task/SKILL.en.md +6 -6
  144. package/templates/.agents/skills/run-task/SKILL.zh-CN.md +6 -6
  145. package/templates/.agents/skills/run-task/reference/host-validation.en.md +2 -2
  146. package/templates/.agents/skills/run-task/reference/host-validation.zh-CN.md +2 -2
@@ -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
- consumeLocalArtifactFinalizationIntent,
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
- let content: string;
656
- try { content = fs.readFileSync(artifact.path, 'utf8'); }
657
- catch (error) { return { ok: false, message: `cannot read ${artifact.name}: ${String(error)}` }; }
658
- const input = parseCodePlanInputReference(content);
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 !== input.name) {
666
- return { ok: false, message: `code input '${input.name}' is not the latest plan artifact` };
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 ${input.name} changed after code.started` };
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: input.name,
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
- let content: string;
685
- try { content = fs.readFileSync(artifact.path, 'utf8'); }
686
- catch (error) { return { ok: false, message: `cannot read ${artifact.name}: ${String(error)}` }; }
687
- const input = parseReviewedInputReference(content, expectedFamily);
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 !== input.name) {
695
- return { ok: false, message: `review input '${input.name}' is not the latest ${expectedFamily} artifact` };
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 ${input.name} changed after review.started` };
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: input.name,
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 ${input.name}: ${error instanceof Error ? error.message : String(error)}` };
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
- const row = manual ? openRows.at(-1) : matchingRows[0];
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 === 'started' && !manual && completedRows.length > 0) return failed(normalized, { code: 'EVENT_ALREADY_COMPLETED', message: 'event identity is already completed' }, { taskId: resolved.taskId, taskMdPath: resolved.taskMdPath });
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 || parts.phase !== 'completed' || parts.family === 'manual-validation') {
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.warningProjection !== 'done') return 'pending';
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
  }