@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,13 +1,15 @@
1
1
  import fs from 'node:fs';
2
2
  import path from 'node:path';
3
+ import { execFileSync } from 'node:child_process';
3
4
  import { appendActivityEntry, locateActivityLog, pairEntries, startedBackedRows } from './activity-log.js';
4
- import { buildArtifactLinkSection, inspectArtifactDirectory, parseReviewedInputReference, parseCodePlanInputReference, resolveArtifactContext, validateCompletedArtifact } from './artifact-lifecycle.js';
5
+ import { buildArtifactLinkSection, inspectArtifactDirectory, resolveArtifactContext, validateCompletedArtifact } from './artifact-lifecycle.js';
5
6
  import { artifactName, parseArtifactName } from './artifact-name.js';
6
7
  import { ArtifactReceiptError, parseArtifactReceipts, sha256File, upsertArtifactReceipt } from './artifact-receipts.js';
7
8
  import { parseTypedTaskFrontmatter } from './frontmatter.js';
8
9
  import { consumeImplementationInput, IMPLEMENTATION_INPUT_ALIASES, parseImplementationInputs, renderImplementationInputs } from './implementation-inputs.js';
9
10
  import { LEDGER_SECTION_MISSING_CODE, LEDGER_SECTION_MISSING_MESSAGE, parseLedgerDocument, summarizeLedgerStage, validateLedgerRows } from './ledger.js';
10
11
  import { parseReviewSummary, resolveCanonicalVerdict } from './review-artifacts.js';
12
+ import { extractReviewBaseline, extractReviewedHead, extractReviewedSnapshotTree } from './review-fingerprint.js';
11
13
  import { resolveTaskRef } from './resolve-ref.js';
12
14
  import { findSectionHeading } from './sections.js';
13
15
  import { validateLifecycleExecution } from './lifecycle-execution.js';
@@ -15,11 +17,10 @@ import { commitOrchestrationStageCompletion } from './orchestration.js';
15
17
  import { TaskExecutionLockError, withTaskExecutionLock } from './task-execution-lock.js';
16
18
  import { captureTaskWriteMetadata, writeTask } from './write.js';
17
19
  import { allowsManualOverride } from './guard-override.js';
18
- import { consumeLocalArtifactFinalizationIntent, readLocalArtifactFinalizationIntent, validateLocalArtifact } from './local-artifact-finalization.js';
19
- import { writeArtifactRecoveryIntent, readArtifactRecoveryIntent } from './artifact-repair-intent.js';
20
- import { reconcileArtifactRecovery, recoveryContextFromIntent, sha256Content } from './artifact-recovery.js';
20
+ import { validateLocalArtifact } from './local-artifact-finalization.js';
21
21
  import { buildLifecycleFacts, canStart } from './capabilities.js';
22
22
  import { createInvalidationOperation, invalidationMutation, parseInvalidationDocument, targetIdFor, upsertInvalidation } from './invalidation.js';
23
+ import { reconcileTaskInvalidation } from './invalidation-command.js';
23
24
  import { consumeReworkIntents, parseReworkIntentDocument, reworkIntentMutation, supersedeReworkIntents } from './rework-intent.js';
24
25
  import { ARTIFACT_FAMILIES, expectedQualificationRelations, parseQualificationAudit, parseTaskQualification, upstreamArtifactDigest, validateQualificationAudit } from './qualification-audit.js';
25
26
  import { getArtifactSchema } from './artifact-schema.js';
@@ -36,6 +37,27 @@ const eventCatalog = [
36
37
  'manual-validation.started', 'manual-validation.completed',
37
38
  'validation-run.started', 'validation-run.completed'
38
39
  ];
40
+ function approvedCleanReviewedCommit(reviewContent, verdict, repoRoot) {
41
+ if (verdict !== 'approved')
42
+ return null;
43
+ const reviewedHead = extractReviewedHead(reviewContent) || extractReviewBaseline(reviewContent);
44
+ const reviewedTree = extractReviewedSnapshotTree(reviewContent);
45
+ if (!/^[a-f0-9]{40}$/.test(reviewedHead) || !/^[a-f0-9]{40}$/.test(reviewedTree))
46
+ return null;
47
+ try {
48
+ const currentHead = execFileSync('git', ['-C', repoRoot, 'rev-parse', 'HEAD'], { encoding: 'utf8' }).trim();
49
+ if (currentHead !== reviewedHead)
50
+ return null;
51
+ const currentTree = execFileSync('git', ['-C', repoRoot, 'rev-parse', 'HEAD^{tree}'], { encoding: 'utf8' }).trim();
52
+ if (currentTree !== reviewedTree)
53
+ return null;
54
+ execFileSync('git', ['-C', repoRoot, 'diff-index', '--quiet', 'HEAD', '--']);
55
+ return reviewedHead;
56
+ }
57
+ catch {
58
+ return null;
59
+ }
60
+ }
39
61
  const BASE_FIELDS = new Set(['taskRef', 'event', 'agent', 'dryRun', 'overrideTicket', 'overrideTarget', 'overrideScope', 'initiator', 'requestId', 'reasonCode', 'sourceFinding', 'sourceArtifact', 'sourceSha256']);
40
62
  const SCHEMAS = {
41
63
  'analyze.started': { optional: ['round'] },
@@ -315,59 +337,6 @@ function openStartedIdentity(rows, family) {
315
337
  function reviewInputFamily(family) {
316
338
  return family === 'review-analysis' ? 'analysis' : family === 'review-plan' ? 'plan' : 'code';
317
339
  }
318
- function reconcileReviewCompletionReplay(repoRoot, taskDir, taskId, family, artifact, round) {
319
- const stage = REVIEW_LEDGER_STAGES[family];
320
- if (!stage)
321
- return null;
322
- const reviewFamily = family;
323
- try {
324
- let intent = readArtifactRecoveryIntent(repoRoot, taskId, reviewFamily, artifact);
325
- if (!intent)
326
- return null;
327
- if (intent.state === 'commit-started') {
328
- const reconciled = reconcileArtifactRecovery(recoveryContextFromIntent(repoRoot, taskDir, intent), { lockAlreadyHeld: true });
329
- if (reconciled.status !== 'passed' && reconciled.status !== 'consumed') {
330
- return {
331
- code: 'EVENT_ARTIFACT_CONFLICT',
332
- message: `review finalizer recovery remains ${reconciled.status} for ${artifact}`
333
- };
334
- }
335
- intent = reconciled.intent;
336
- }
337
- if (!['passed', 'consumption-started', 'consumed'].includes(intent.state)) {
338
- return {
339
- code: 'EVENT_ARTIFACT_CONFLICT',
340
- message: `review finalizer provenance is incomplete for ${artifact}`
341
- };
342
- }
343
- const validated = validateCompletedArtifact(taskDir, FAMILY[family].artifact, artifact, round);
344
- if (!validated.ok) {
345
- return { code: 'EVENT_ARTIFACT_CONFLICT', message: validated.error.message };
346
- }
347
- const content = fs.readFileSync(validated.artifact.path, 'utf8');
348
- const expectedRequestId = `review-finalize:${taskId}:${stage}:${round}`;
349
- if (intent.round !== round
350
- || intent.requestId !== expectedRequestId
351
- || intent.finalArtifactSha256 !== sha256Content(content)
352
- || intent.finalSemanticDigest !== canonicalSemanticDigest(content)) {
353
- return {
354
- code: 'EVENT_ARTIFACT_CONFLICT',
355
- message: `review finalizer provenance does not match ${artifact}`
356
- };
357
- }
358
- if (intent.state === 'passed' || intent.state === 'consumption-started') {
359
- const consumed = { ...intent, state: 'consumed', updatedAt: Date.now() };
360
- writeArtifactRecoveryIntent(repoRoot, consumed, { expected: intent });
361
- }
362
- return null;
363
- }
364
- catch (error) {
365
- return {
366
- code: 'EVENT_ARTIFACT_CONFLICT',
367
- message: `review finalizer recovery could not be reconciled for ${artifact}: ${error instanceof Error ? error.message : String(error)}`
368
- };
369
- }
370
- }
371
340
  function relationForStartedInput(family, input) {
372
341
  if (family.startsWith('review-')) {
373
342
  return input.family === reviewInputFamily(family) ? 'reviewed-input' : 'approval-context';
@@ -593,37 +562,35 @@ function reworkIntentMutationForCompletion(content, taskDir, family, artifact, t
593
562
  }
594
563
  return { mutation: JSON.stringify(next) === JSON.stringify(parsed.intents) ? null : reworkIntentMutation(content, next) };
595
564
  }
596
- function buildCompletionReceipt(taskDir, family, artifact, completedAt, frontmatter) {
597
- if (family === 'code') {
598
- let content;
565
+ function buildCompletionReceipt(content, taskDir, family, artifact, completedAt, frontmatter) {
566
+ const existing = (() => {
599
567
  try {
600
- content = fs.readFileSync(artifact.path, 'utf8');
568
+ return parseArtifactReceipts(content).rows.find((receipt) => receipt.output === artifact.name) ?? null;
601
569
  }
602
- catch (error) {
603
- return { ok: false, message: `cannot read ${artifact.name}: ${String(error)}` };
570
+ catch {
571
+ return null;
604
572
  }
605
- const input = parseCodePlanInputReference(content);
606
- if (!input)
607
- return { ok: false, message: `${artifact.name} does not reference a canonical plan artifact` };
608
- const startedInput = typeof frontmatter.code_input_artifact === 'string' ? frontmatter.code_input_artifact : '';
609
- const startedSha256 = typeof frontmatter.code_input_sha256 === 'string' ? frontmatter.code_input_sha256 : '';
573
+ })();
574
+ if (family === 'code') {
575
+ const startedInput = typeof frontmatter.code_input_artifact === 'string'
576
+ ? frontmatter.code_input_artifact : existing?.input ?? '';
577
+ const startedSha256 = typeof frontmatter.code_input_sha256 === 'string'
578
+ ? frontmatter.code_input_sha256 : existing?.inputSha256 ?? '';
610
579
  if (!startedInput || !startedSha256)
611
580
  return { ok: false, message: 'code.started plan input context is missing' };
612
- if (input.name !== startedInput)
613
- return { ok: false, message: `${artifact.name} plan input '${input.name}' does not match code.started input '${startedInput}'` };
614
581
  const plan = inspectArtifactDirectory(taskDir, 'plan');
615
- if (plan.status !== 'ready' || !plan.latest || plan.latest.name !== input.name) {
616
- return { ok: false, message: `code input '${input.name}' is not the latest plan artifact` };
582
+ if (plan.status !== 'ready' || !plan.latest || plan.latest.name !== startedInput) {
583
+ return { ok: false, message: `code input '${startedInput}' is not the latest plan artifact` };
617
584
  }
618
585
  try {
619
586
  const inputSha256 = sha256File(plan.latest.path);
620
587
  if (inputSha256 !== startedSha256)
621
- return { ok: false, message: `code input ${input.name} changed after code.started` };
588
+ return { ok: false, message: `code input ${startedInput} changed after code.started` };
622
589
  return {
623
590
  ok: true,
624
591
  receipt: {
625
- event: 'code.completed', output: artifact.name, input: input.name,
626
- inputSha256, completedAt
592
+ event: 'code.completed', output: artifact.name, input: startedInput,
593
+ inputSha256, completedAt: existing?.completedAt ?? completedAt
627
594
  }
628
595
  };
629
596
  }
@@ -634,25 +601,15 @@ function buildCompletionReceipt(taskDir, family, artifact, completedAt, frontmat
634
601
  if (!family.startsWith('review-'))
635
602
  return null;
636
603
  const expectedFamily = reviewInputFamily(family);
637
- let content;
638
- try {
639
- content = fs.readFileSync(artifact.path, 'utf8');
640
- }
641
- catch (error) {
642
- return { ok: false, message: `cannot read ${artifact.name}: ${String(error)}` };
643
- }
644
- const input = parseReviewedInputReference(content, expectedFamily);
645
- if (!input)
646
- return { ok: false, message: `${artifact.name} does not reference a canonical ${expectedFamily} artifact` };
647
- const startedInput = typeof frontmatter.review_input_artifact === 'string' ? frontmatter.review_input_artifact : '';
648
- const startedSha256 = typeof frontmatter.review_input_sha256 === 'string' ? frontmatter.review_input_sha256 : '';
604
+ const startedInput = typeof frontmatter.review_input_artifact === 'string'
605
+ ? frontmatter.review_input_artifact : existing?.input ?? '';
606
+ const startedSha256 = typeof frontmatter.review_input_sha256 === 'string'
607
+ ? frontmatter.review_input_sha256 : existing?.inputSha256 ?? '';
649
608
  if (!startedInput || !startedSha256)
650
609
  return { ok: false, message: 'review started input context is missing' };
651
- if (input.name !== startedInput)
652
- return { ok: false, message: `${artifact.name} input '${input.name}' does not match review.started input '${startedInput}'` };
653
610
  const current = inspectArtifactDirectory(taskDir, expectedFamily);
654
- if (current.status !== 'ready' || !current.latest || current.latest.name !== input.name) {
655
- return { ok: false, message: `review input '${input.name}' is not the latest ${expectedFamily} artifact` };
611
+ if (current.status !== 'ready' || !current.latest || current.latest.name !== startedInput) {
612
+ return { ok: false, message: `review input '${startedInput}' is not the latest ${expectedFamily} artifact` };
656
613
  }
657
614
  const event = family === 'review-analysis'
658
615
  ? 'review-analysis.completed'
@@ -663,19 +620,70 @@ function buildCompletionReceipt(taskDir, family, artifact, completedAt, frontmat
663
620
  throw new Error('review input is not a regular file');
664
621
  const inputSha256 = sha256File(current.latest.path);
665
622
  if (inputSha256 !== startedSha256)
666
- return { ok: false, message: `review input ${input.name} changed after review.started` };
623
+ return { ok: false, message: `review input ${startedInput} changed after review.started` };
667
624
  return {
668
625
  ok: true,
669
626
  receipt: {
670
- event, output: artifact.name, input: input.name,
671
- inputSha256, completedAt
627
+ event, output: artifact.name, input: startedInput,
628
+ inputSha256, completedAt: existing?.completedAt ?? completedAt
672
629
  }
673
630
  };
674
631
  }
675
632
  catch (error) {
676
- return { ok: false, message: `cannot hash review input ${input.name}: ${error instanceof Error ? error.message : String(error)}` };
633
+ return { ok: false, message: `cannot hash review input ${startedInput}: ${error instanceof Error ? error.message : String(error)}` };
634
+ }
635
+ }
636
+ function completionFacts(frontmatter) {
637
+ if (typeof frontmatter.completion_facts !== 'string')
638
+ return [];
639
+ try {
640
+ const facts = JSON.parse(frontmatter.completion_facts);
641
+ if (!Array.isArray(facts))
642
+ return [];
643
+ return facts.filter((fact) => Boolean(fact && typeof fact === 'object' && !Array.isArray(fact)
644
+ && typeof fact.event === 'string'
645
+ && typeof fact.output === 'string'
646
+ && /^[a-f0-9]{64}$/u.test(fact.outputSha256)
647
+ && /^[a-f0-9]{64}$/u.test(fact.semanticDigest)
648
+ && typeof fact.requestId === 'string'
649
+ && typeof fact.result === 'string'));
650
+ }
651
+ catch {
652
+ return [];
677
653
  }
678
654
  }
655
+ function currentCompletionFact(request, artifact) {
656
+ const content = fs.readFileSync(artifact.path, 'utf8');
657
+ return {
658
+ event: request.event,
659
+ output: artifact.name,
660
+ outputSha256: sha256File(artifact.path),
661
+ semanticDigest: canonicalSemanticDigest(content),
662
+ requestId: request.requestId ?? '',
663
+ result: JSON.stringify({
664
+ filesModified: request.filesModified,
665
+ testsPassed: request.testsPassed,
666
+ blockers: request.blockers,
667
+ major: request.major,
668
+ minor: request.minor,
669
+ manualValidation: request.manualValidation ?? 0,
670
+ verdict: request.verdict,
671
+ fixFor: request.fixFor,
672
+ implementationInput: request.implementationInput
673
+ })
674
+ };
675
+ }
676
+ function sameCompletionFact(left, right) {
677
+ return left.event === right.event
678
+ && left.output === right.output
679
+ && left.outputSha256 === right.outputSha256
680
+ && left.semanticDigest === right.semanticDigest
681
+ && left.requestId === right.requestId
682
+ && left.result === right.result;
683
+ }
684
+ function replaceCompletionFact(facts, next) {
685
+ return [...facts.filter((fact) => !(fact.event === next.event && fact.output === next.output)), next];
686
+ }
679
687
  function applyTaskEventUnlocked(request, options = {}) {
680
688
  const invalid = validateTaskEventRequest(request);
681
689
  if (invalid)
@@ -693,6 +701,24 @@ function applyTaskEventUnlocked(request, options = {}) {
693
701
  catch (error) {
694
702
  return failed(request, { code: 'TASK_READ_FAILED', message: String(error) }, { taskId: resolved.taskId, taskMdPath: resolved.taskMdPath });
695
703
  }
704
+ const initialParts = eventParts(request.event);
705
+ if (initialParts.phase === 'started' && !request.dryRun) {
706
+ const reconciled = reconcileTaskInvalidation(request.taskRef, { repoRoot: resolved.repoRoot });
707
+ if (reconciled.status === 'failed') {
708
+ return failed(request, {
709
+ code: 'EVENT_TRANSITION_INVALID',
710
+ message: `INVALIDATION_RECONCILE_FAILED: ${reconciled.error?.code ?? 'UNKNOWN'}: ${reconciled.error?.message ?? ''}`
711
+ }, { taskId: resolved.taskId, taskMdPath: resolved.taskMdPath });
712
+ }
713
+ if (reconciled.changed) {
714
+ try {
715
+ content = fs.readFileSync(resolved.taskMdPath, 'utf8');
716
+ }
717
+ catch (error) {
718
+ return failed(request, { code: 'TASK_READ_FAILED', message: String(error) }, { taskId: resolved.taskId, taskMdPath: resolved.taskMdPath });
719
+ }
720
+ }
721
+ }
696
722
  let frontmatter;
697
723
  try {
698
724
  frontmatter = parseTypedTaskFrontmatter(content);
@@ -706,7 +732,6 @@ function applyTaskEventUnlocked(request, options = {}) {
706
732
  const rows = startedBackedRows(pairEntries(section.entries));
707
733
  let normalized = request;
708
734
  let artifactContext = null;
709
- const initialParts = eventParts(request.event);
710
735
  if (initialParts.phase === 'started') {
711
736
  const openIdentity = openStartedIdentity(rows, initialParts.family);
712
737
  if (openIdentity && 'conflict' in openIdentity)
@@ -762,14 +787,12 @@ function applyTaskEventUnlocked(request, options = {}) {
762
787
  const currentStep = typeof frontmatter.current_step === 'string' ? frontmatter.current_step : '';
763
788
  const matchingRows = rows.filter((item) => item.step === eventIdentity.action);
764
789
  const manual = eventIdentity.family === 'manual-validation';
765
- if (!manual && matchingRows.length > 1)
766
- 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 });
767
790
  const openRows = matchingRows.filter((item) => item.started && !item.done);
768
791
  const completedRows = matchingRows.filter((item) => item.done);
769
- const row = manual ? openRows.at(-1) : matchingRows[0];
792
+ if (!manual && openRows.length > 1)
793
+ 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 });
794
+ const row = openRows.at(-1);
770
795
  let completedArtifact = null;
771
- let localFinalizationIntent = null;
772
- let reviewFinalizationIntent = null;
773
796
  let reviewContent = null;
774
797
  if (eventIdentity.phase === 'started' && row) {
775
798
  if (row.agent !== normalized.agent)
@@ -782,21 +805,8 @@ function applyTaskEventUnlocked(request, options = {}) {
782
805
  }
783
806
  return successNoOp(normalized, resolved.taskId, resolved.taskMdPath, currentStep, eventIdentity, row.started, frontmatter, artifactContext);
784
807
  }
785
- if (eventIdentity.phase === 'started' && !manual && completedRows.length > 0)
786
- return failed(normalized, { code: 'EVENT_ALREADY_COMPLETED', message: 'event identity is already completed' }, { taskId: resolved.taskId, taskMdPath: resolved.taskMdPath });
787
- const done = completedRows.find((item) => item.note === eventIdentity.note);
788
- if (eventIdentity.phase === 'completed' && done) {
789
- if (!normalized.dryRun && eventIdentity.family.startsWith('review-') && normalized.artifact && normalized.round) {
790
- const recoveryError = reconcileReviewCompletionReplay(resolved.repoRoot, resolved.taskDir, resolved.taskId, eventIdentity.family, normalized.artifact, normalized.round);
791
- if (recoveryError)
792
- return failed(normalized, recoveryError, { taskId: resolved.taskId, taskMdPath: resolved.taskMdPath, fromStep: currentStep, toStep: currentStep, action: eventIdentity.action, phase: eventIdentity.phase });
793
- }
794
- return successNoOp(normalized, resolved.taskId, resolved.taskMdPath, currentStep, eventIdentity, done.done, frontmatter, artifactContext);
795
- }
796
- if (eventIdentity.phase === 'completed' && completedRows.length > 0 && openRows.length === 0)
797
- return failed(normalized, { code: 'EVENT_LOG_CONFLICT', message: 'event identity is already completed with a different payload' }, { taskId: resolved.taskId, taskMdPath: resolved.taskMdPath, fromStep: currentStep, toStep: currentStep, action: eventIdentity.action, phase: eventIdentity.phase });
798
- if (eventIdentity.phase === 'completed' && !row?.started && !allowsManualOverride(options.manualOverride, 'task-event', 'EVENT_START_MISSING'))
799
- 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 });
808
+ if (eventIdentity.phase === 'completed' && !row?.started && completedRows.length === 0 && !allowsManualOverride(options.manualOverride, 'task-event', 'EVENT_START_MISSING'))
809
+ 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 });
800
810
  if (eventIdentity.phase === 'completed') {
801
811
  const validated = validateCompletedArtifact(resolved.taskDir, FAMILY[eventIdentity.family].artifact, normalized.artifact, normalized.round);
802
812
  if (!validated.ok)
@@ -836,20 +846,6 @@ function applyTaskEventUnlocked(request, options = {}) {
836
846
  const localFamily = eventIdentity.family === 'analyze'
837
847
  ? 'analysis'
838
848
  : eventIdentity.family === 'plan' ? 'plan' : 'code';
839
- try {
840
- localFinalizationIntent = readLocalArtifactFinalizationIntent(resolved.repoRoot, resolved.taskId, localFamily, completedArtifact.name);
841
- if (localFinalizationIntent?.state === 'commit-started') {
842
- const reconciled = reconcileArtifactRecovery(recoveryContextFromIntent(resolved.repoRoot, resolved.taskDir, localFinalizationIntent), { lockAlreadyHeld: true });
843
- if (reconciled.status === 'passed' || reconciled.status === 'consumed')
844
- localFinalizationIntent = reconciled.intent;
845
- }
846
- }
847
- catch (error) {
848
- return failed(normalized, {
849
- code: 'EVENT_ARTIFACT_CONFLICT',
850
- message: `local finalizer recovery could not be reconciled for ${completedArtifact.name}: ${error instanceof Error ? error.message : String(error)}`
851
- }, { taskId: resolved.taskId, taskMdPath: resolved.taskMdPath, fromStep: currentStep, toStep: currentStep, action: eventIdentity.action, phase: eventIdentity.phase });
852
- }
853
849
  let artifactContent;
854
850
  try {
855
851
  artifactContent = fs.readFileSync(completedArtifact.path, 'utf8');
@@ -881,29 +877,6 @@ function applyTaskEventUnlocked(request, options = {}) {
881
877
  message: `semantic digest does not match finalizer result for ${completedArtifact.name}`
882
878
  }, { taskId: resolved.taskId, taskMdPath: resolved.taskMdPath, fromStep: currentStep, toStep: currentStep, action: eventIdentity.action, phase: eventIdentity.phase });
883
879
  }
884
- if (!localFinalizationIntent || !['passed', 'consumed'].includes(localFinalizationIntent.state)) {
885
- return failed(normalized, {
886
- code: 'EVENT_ARTIFACT_CONFLICT',
887
- message: `local finalizer provenance is missing or incomplete for ${completedArtifact.name}`
888
- }, { taskId: resolved.taskId, taskMdPath: resolved.taskMdPath, fromStep: currentStep, toStep: currentStep, action: eventIdentity.action, phase: eventIdentity.phase });
889
- }
890
- if (localFinalizationIntent.finalArtifactSha256 !== actualSha256 || localFinalizationIntent.finalSemanticDigest !== local.semanticDigest) {
891
- return failed(normalized, {
892
- code: 'EVENT_ARTIFACT_CONFLICT',
893
- message: `local finalizer provenance does not match ${completedArtifact.name}`
894
- }, { taskId: resolved.taskId, taskMdPath: resolved.taskMdPath, fromStep: currentStep, toStep: currentStep, action: eventIdentity.action, phase: eventIdentity.phase });
895
- }
896
- if (localFinalizationIntent.authorityDigest !== null) {
897
- if (!lifecycleAuthority
898
- || localFinalizationIntent.recoveryOperationId !== lifecycleAuthority.operationId
899
- || localFinalizationIntent.requestId !== lifecycleAuthority.lifecycleRequestId
900
- || localFinalizationIntent.authorityDigest !== lifecycleRecoveryAttestationDigest(lifecycleAuthority)) {
901
- return failed(normalized, {
902
- code: 'EVENT_ARTIFACT_CONFLICT',
903
- message: `local finalizer provenance does not match lifecycle completion authority for ${completedArtifact.name}`
904
- }, { taskId: resolved.taskId, taskMdPath: resolved.taskMdPath, fromStep: currentStep, toStep: currentStep, action: eventIdentity.action, phase: eventIdentity.phase, artifactContext });
905
- }
906
- }
907
880
  }
908
881
  }
909
882
  if (eventIdentity.phase === 'waiting' && section.entries.some((entry) => entry.step === eventIdentity.action && entry.note === eventIdentity.note)) {
@@ -927,43 +900,6 @@ function applyTaskEventUnlocked(request, options = {}) {
927
900
  const findingCountError = validateReviewFindingCounts(normalized, content, eventIdentity.family, completedArtifact?.path ?? null);
928
901
  if (findingCountError)
929
902
  return failed(normalized, findingCountError, { taskId: resolved.taskId, taskMdPath: resolved.taskMdPath, fromStep: currentStep, toStep: currentStep, action: eventIdentity.action, phase: eventIdentity.phase });
930
- if (eventIdentity.phase === 'completed' && eventIdentity.family.startsWith('review-') && completedArtifact && reviewContent !== null) {
931
- try {
932
- reviewFinalizationIntent = readArtifactRecoveryIntent(resolved.repoRoot, resolved.taskId, eventIdentity.family, completedArtifact.name);
933
- if (!normalized.dryRun && reviewFinalizationIntent?.state === 'commit-started') {
934
- const reconciled = reconcileArtifactRecovery(recoveryContextFromIntent(resolved.repoRoot, resolved.taskDir, reviewFinalizationIntent), { lockAlreadyHeld: true });
935
- if (reconciled.status === 'passed' || reconciled.status === 'consumed')
936
- reviewFinalizationIntent = reconciled.intent;
937
- }
938
- if (!reviewFinalizationIntent || !['passed', 'consumption-started', 'consumed'].includes(reviewFinalizationIntent.state)) {
939
- return failed(normalized, {
940
- code: 'EVENT_ARTIFACT_CONFLICT',
941
- message: `review finalizer provenance is missing or incomplete for ${completedArtifact.name}`
942
- }, { taskId: resolved.taskId, taskMdPath: resolved.taskMdPath, fromStep: currentStep, toStep: currentStep, action: eventIdentity.action, phase: eventIdentity.phase });
943
- }
944
- const reviewStage = eventIdentity.family === 'review-analysis'
945
- ? 'analysis'
946
- : eventIdentity.family === 'review-plan' ? 'plan' : 'code';
947
- const actualSha256 = sha256File(completedArtifact.path);
948
- const actualSemanticDigest = canonicalSemanticDigest(reviewContent);
949
- const expectedRequestId = `review-finalize:${resolved.taskId}:${reviewStage}:${normalized.round}`;
950
- if (reviewFinalizationIntent.round !== normalized.round
951
- || reviewFinalizationIntent.requestId !== expectedRequestId
952
- || reviewFinalizationIntent.finalArtifactSha256 !== actualSha256
953
- || reviewFinalizationIntent.finalSemanticDigest !== actualSemanticDigest) {
954
- return failed(normalized, {
955
- code: 'EVENT_ARTIFACT_CONFLICT',
956
- message: `review finalizer provenance does not match ${completedArtifact.name}`
957
- }, { taskId: resolved.taskId, taskMdPath: resolved.taskMdPath, fromStep: currentStep, toStep: currentStep, action: eventIdentity.action, phase: eventIdentity.phase });
958
- }
959
- }
960
- catch (error) {
961
- return failed(normalized, {
962
- code: 'EVENT_ARTIFACT_CONFLICT',
963
- message: `review finalizer recovery could not be reconciled for ${completedArtifact.name}: ${error instanceof Error ? error.message : String(error)}`
964
- }, { taskId: resolved.taskId, taskMdPath: resolved.taskMdPath, fromStep: currentStep, toStep: currentStep, action: eventIdentity.action, phase: eventIdentity.phase });
965
- }
966
- }
967
903
  let orchestrationCompletion = null;
968
904
  if (eventIdentity.phase === 'completed' && eventIdentity.family !== 'manual-validation' && eventIdentity.family !== 'validation-run') {
969
905
  const orchestrationStage = eventIdentity.family === 'analyze' ? 'analysis' : eventIdentity.family;
@@ -994,14 +930,24 @@ function applyTaskEventUnlocked(request, options = {}) {
994
930
  return failed(normalized, { code: 'METADATA_CAPTURE_FAILED', message: String(error) }, { taskId: resolved.taskId, taskMdPath: resolved.taskMdPath });
995
931
  }
996
932
  let completionReceipt = null;
933
+ let currentFact = null;
997
934
  let completionInvalidation = null;
998
935
  let completionRework = null;
999
936
  if (eventIdentity.phase === 'completed' && completedArtifact) {
1000
- const receipt = buildCompletionReceipt(resolved.taskDir, eventIdentity.family, completedArtifact, metadata.timestamp, frontmatter);
937
+ const receipt = buildCompletionReceipt(content, resolved.taskDir, eventIdentity.family, completedArtifact, metadata.timestamp, frontmatter);
1001
938
  if (receipt && !receipt.ok) {
1002
939
  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 });
1003
940
  }
1004
941
  completionReceipt = receipt?.receipt ?? null;
942
+ try {
943
+ currentFact = currentCompletionFact(normalized, completedArtifact);
944
+ }
945
+ catch (error) {
946
+ 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 });
947
+ }
948
+ if (completionFacts(frontmatter).some((fact) => sameCompletionFact(fact, currentFact))) {
949
+ return successNoOp(normalized, resolved.taskId, resolved.taskMdPath, currentStep, eventIdentity, completionReceipt?.completedAt ?? metadata.timestamp, frontmatter, artifactContext);
950
+ }
1005
951
  try {
1006
952
  const invalidation = invalidationMutationForCompletion(content, resolved.taskDir, eventIdentity.family, completedArtifact, metadata.timestamp);
1007
953
  if ('error' in invalidation)
@@ -1020,6 +966,8 @@ function applyTaskEventUnlocked(request, options = {}) {
1020
966
  const logStep = eventIdentity.phase === 'started' ? `${eventIdentity.action} [started]` : eventIdentity.action;
1021
967
  const body = appendActivityEntry(section, { time: metadata.timestamp, step: logStep, agent: normalized.agent, note: eventIdentity.note });
1022
968
  const frontmatterSet = { current_step: step, assigned_to: normalized.agent };
969
+ if (currentFact)
970
+ frontmatterSet.completion_facts = JSON.stringify(replaceCompletionFact(completionFacts(frontmatter), currentFact));
1023
971
  let frontmatterRemove;
1024
972
  if (eventIdentity.phase === 'started' && artifactContext) {
1025
973
  try {
@@ -1060,6 +1008,13 @@ function applyTaskEventUnlocked(request, options = {}) {
1060
1008
  else if (eventIdentity.phase === 'completed' && eventIdentity.family.startsWith('review-')) {
1061
1009
  frontmatterRemove = ['review_input_artifact', 'review_input_sha256'];
1062
1010
  }
1011
+ if (eventIdentity.phase === 'completed' && eventIdentity.family === 'review-code' && reviewContent !== null) {
1012
+ const reviewedCommit = approvedCleanReviewedCommit(reviewContent, normalized.verdict, resolved.repoRoot);
1013
+ if (reviewedCommit)
1014
+ frontmatterSet.last_reviewed_commit = reviewedCommit;
1015
+ else if (normalized.verdict === 'approved')
1016
+ frontmatterSet.last_reviewed_commit = '';
1017
+ }
1063
1018
  if (eventIdentity.phase === 'completed')
1064
1019
  frontmatterRemove = [...(frontmatterRemove ?? []), 'qualification_input_relations'];
1065
1020
  if (eventIdentity.phase === 'started' && normalized.implementationInput)
@@ -1100,97 +1055,11 @@ function applyTaskEventUnlocked(request, options = {}) {
1100
1055
  });
1101
1056
  }
1102
1057
  mutations.push({ kind: 'section', aliases: ['活动日志', 'Activity Log'], heading: section.heading, body });
1103
- if (!normalized.dryRun && localFinalizationIntent && completedArtifact) {
1104
- try {
1105
- if (lifecycleAuthority && localFinalizationIntent.state === 'passed') {
1106
- const expectedIntent = localFinalizationIntent;
1107
- localFinalizationIntent = {
1108
- ...localFinalizationIntent,
1109
- state: 'consumption-started',
1110
- phase: 'task-event.completed',
1111
- authorityDigest: lifecycleRecoveryAttestationDigest(lifecycleAuthority),
1112
- requestId: lifecycleAuthority.lifecycleRequestId,
1113
- updatedAt: Date.now()
1114
- };
1115
- writeArtifactRecoveryIntent(resolved.repoRoot, localFinalizationIntent, { expected: expectedIntent });
1116
- }
1117
- else if (!lifecycleAuthority && localFinalizationIntent.state === 'passed') {
1118
- localFinalizationIntent = consumeLocalArtifactFinalizationIntent(resolved.repoRoot, localFinalizationIntent, { lockAlreadyHeld: options.lockAlreadyHeld });
1119
- }
1120
- }
1121
- catch (error) {
1122
- return failed(normalized, {
1123
- code: 'EVENT_ARTIFACT_CONFLICT',
1124
- message: `local finalizer provenance could not be advanced before task write for ${completedArtifact.name}: ${error instanceof Error ? error.message : String(error)}`
1125
- }, {
1126
- taskId: resolved.taskId,
1127
- taskMdPath: resolved.taskMdPath,
1128
- fromStep: currentStep,
1129
- toStep: step,
1130
- action: eventIdentity.action,
1131
- phase: eventIdentity.phase,
1132
- artifactContext
1133
- });
1134
- }
1135
- }
1136
- if (!normalized.dryRun && reviewFinalizationIntent && completedArtifact && reviewFinalizationIntent.state === 'passed') {
1137
- try {
1138
- const expectedIntent = reviewFinalizationIntent;
1139
- reviewFinalizationIntent = {
1140
- ...reviewFinalizationIntent,
1141
- state: 'consumption-started',
1142
- phase: 'task-event.completed',
1143
- updatedAt: Date.now()
1144
- };
1145
- writeArtifactRecoveryIntent(resolved.repoRoot, reviewFinalizationIntent, { expected: expectedIntent });
1146
- }
1147
- catch (error) {
1148
- return failed(normalized, {
1149
- code: 'EVENT_ARTIFACT_CONFLICT',
1150
- message: `review finalizer provenance could not be advanced before task write for ${completedArtifact.name}: ${error instanceof Error ? error.message : String(error)}`
1151
- }, {
1152
- taskId: resolved.taskId,
1153
- taskMdPath: resolved.taskMdPath,
1154
- fromStep: currentStep,
1155
- toStep: step,
1156
- action: eventIdentity.action,
1157
- phase: eventIdentity.phase,
1158
- artifactContext
1159
- });
1160
- }
1161
- }
1162
1058
  const sourceCompletion = eventIdentity.phase === 'completed'
1163
1059
  && ['analyze', 'plan', 'code'].includes(eventIdentity.family);
1164
1060
  const result = writeTask({ taskRef: normalized.taskRef, expectedState: stateOverride ? resolved.state : 'active', dryRun: normalized.dryRun, mutations }, { ...options, invalidationContext: sourceCompletion ? 'source-completion' : 'standard', metadataProvider: () => metadata });
1165
1061
  if (result.status === 'failed')
1166
1062
  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 });
1167
- if (!normalized.dryRun && lifecycleAuthority && localFinalizationIntent && completedArtifact) {
1168
- try {
1169
- if (!options.deferLifecycleRecoveryConsumption)
1170
- consumeLifecycleRecoveryAttestation(lifecycleAuthority);
1171
- writeArtifactRecoveryIntent(resolved.repoRoot, { ...localFinalizationIntent, state: 'consumed', updatedAt: Date.now() }, { expected: localFinalizationIntent });
1172
- }
1173
- catch (error) {
1174
- return failed(normalized, {
1175
- code: 'EVENT_ARTIFACT_CONFLICT',
1176
- message: `task completion was written but lifecycle recovery consumption is incomplete: ${error instanceof Error ? error.message : String(error)}`
1177
- }, { taskId: result.taskId, taskMdPath: result.taskMdPath, fromStep: currentStep, toStep: step, action: eventIdentity.action, phase: eventIdentity.phase, timestamp: result.timestamp, agentInfraVersion: result.agentInfraVersion, operations: result.operations, artifactContext });
1178
- }
1179
- }
1180
- if (!normalized.dryRun && reviewFinalizationIntent && completedArtifact) {
1181
- try {
1182
- if (reviewFinalizationIntent.state === 'consumption-started') {
1183
- const consumed = { ...reviewFinalizationIntent, state: 'consumed', updatedAt: Date.now() };
1184
- writeArtifactRecoveryIntent(resolved.repoRoot, consumed, { expected: reviewFinalizationIntent });
1185
- }
1186
- }
1187
- catch (error) {
1188
- return failed(normalized, {
1189
- code: 'EVENT_ARTIFACT_CONFLICT',
1190
- message: `review finalizer recovery consumption is incomplete: ${error instanceof Error ? error.message : String(error)}`
1191
- }, { taskId: result.taskId, taskMdPath: result.taskMdPath, fromStep: currentStep, toStep: step, action: eventIdentity.action, phase: eventIdentity.phase, timestamp: result.timestamp, agentInfraVersion: result.agentInfraVersion, operations: result.operations, artifactContext });
1192
- }
1193
- }
1194
1063
  if (!normalized.dryRun && orchestrationCompletion) {
1195
1064
  try {
1196
1065
  (options.commitOrchestrationCompletion ?? commitOrchestrationStageCompletion)(orchestrationCompletion);
@@ -1230,7 +1099,7 @@ function applyTaskEvent(request, options = {}) {
1230
1099
  const parts = eventParts(request.event);
1231
1100
  if (options.lockAlreadyHeld)
1232
1101
  return applyTaskEventUnlocked(request, options);
1233
- if (request.dryRun || parts.phase !== 'completed' || parts.family === 'manual-validation') {
1102
+ if (request.dryRun || !['started', 'completed'].includes(parts.phase)) {
1234
1103
  return applyTaskEventUnlocked(request, options);
1235
1104
  }
1236
1105
  const resolved = resolveTaskRef(request.taskRef, { repoRoot: options.repoRoot });
@@ -1,6 +1,7 @@
1
1
  export function taskFinalizationReceiptState(receipt) {
2
2
  if (receipt.lifecycle !== 'done' || receipt.taskComment === 'pending'
3
- || receipt.verification === 'pending' || receipt.warningProjection !== 'done')
3
+ || receipt.verification === 'pending' || receipt.summary === 'pending'
4
+ || receipt.postSummaryVerification === 'pending' || receipt.warningProjection !== 'done')
4
5
  return 'pending';
5
6
  if (receipt.lastError !== null || receipt.warnings.some((warning) => warning.status === 'open'))
6
7
  return 'unresolved';