@fitlab-ai/agent-infra 0.11.1 → 0.11.3

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 (131) hide show
  1. package/dist/bin/internal-cli.js +7 -2
  2. package/dist/lib/agent-clients/adapter.js +3 -0
  3. package/dist/lib/agent-clients/adapters/codex-lifecycle/recovery.js +234 -0
  4. package/dist/lib/agent-clients/adapters/codex-lifecycle/sandbox-controller.js +8 -35
  5. package/dist/lib/agent-clients/adapters/codex-lifecycle/store.js +1 -34
  6. package/dist/lib/agent-clients/adapters/codex.js +10 -0
  7. package/dist/lib/internal/cli-route-inventory.js +1 -1
  8. package/dist/lib/internal/manual-validation.js +22 -27
  9. package/dist/lib/internal/platform-comment.js +59 -0
  10. package/dist/lib/internal/sandbox-control.js +2 -171
  11. package/dist/lib/internal/task-artifact.js +1 -1
  12. package/dist/lib/internal/task-lifecycle.js +1 -1
  13. package/dist/lib/internal/task-operation-registry.js +7 -2
  14. package/dist/lib/internal/task-review.js +5 -5
  15. package/dist/lib/internal/task-validate.js +0 -6
  16. package/dist/lib/platform/comment-safety.js +7 -1
  17. package/dist/lib/platform/github-provider.js +13 -3
  18. package/dist/lib/platform/issue-comments.js +93 -15
  19. package/dist/lib/platform/pr-summary.js +7 -2
  20. package/dist/lib/platform/provider-contract.d.ts +2 -0
  21. package/dist/lib/platform/provider-validation.js +7 -2
  22. package/dist/lib/platform/verification-sync.js +19 -2
  23. package/dist/lib/sandbox/control/client.js +3 -44
  24. package/dist/lib/sandbox/control/executor.js +14 -117
  25. package/dist/lib/sandbox/control/finalization-response.js +8 -2
  26. package/dist/lib/sandbox/control/protocol.js +5 -64
  27. package/dist/lib/sandbox/control/server.js +29 -206
  28. package/dist/lib/sandbox/control/state.js +1 -7
  29. package/dist/lib/sandbox/control/task-view.js +2 -1
  30. package/dist/lib/task/artifact-command.js +6 -6
  31. package/dist/lib/task/codex-orchestration.js +10 -208
  32. package/dist/lib/task/commit-operation.js +3 -18
  33. package/dist/lib/task/control-authority.js +35 -44
  34. package/dist/lib/task/control-recovery.js +3 -42
  35. package/dist/lib/task/delegation-receipts.js +16 -78
  36. package/dist/lib/task/events.js +89 -237
  37. package/dist/lib/task/finalization-artifacts.js +1 -3
  38. package/dist/lib/task/finalization-state.js +2 -1
  39. package/dist/lib/task/finalization.js +208 -57
  40. package/dist/lib/task/lifecycle-execution.js +2 -11
  41. package/dist/lib/task/lifecycle.js +1 -1
  42. package/dist/lib/task/local-artifact-finalization.js +25 -242
  43. package/dist/lib/task/manual-validation-receipt.js +8 -3
  44. package/dist/lib/task/orchestration.js +12 -24
  45. package/dist/lib/task/review-command.js +0 -1
  46. package/dist/lib/task/review-finalization.js +46 -211
  47. package/dist/lifecycle-build-manifest.json +3 -3
  48. package/lib/agent-clients/adapter.ts +46 -0
  49. package/lib/agent-clients/adapters/codex-lifecycle/recovery.ts +297 -0
  50. package/lib/agent-clients/adapters/codex-lifecycle/sandbox-controller.ts +13 -33
  51. package/lib/agent-clients/adapters/codex-lifecycle/store.ts +1 -44
  52. package/lib/agent-clients/adapters/codex.ts +10 -0
  53. package/lib/internal/cli-route-inventory.ts +1 -1
  54. package/lib/internal/manual-validation.ts +22 -26
  55. package/lib/internal/platform-comment.ts +38 -0
  56. package/lib/internal/sandbox-control.ts +1 -185
  57. package/lib/internal/task-artifact.ts +1 -1
  58. package/lib/internal/task-lifecycle.ts +2 -2
  59. package/lib/internal/task-operation-registry.ts +8 -4
  60. package/lib/internal/task-review.ts +5 -5
  61. package/lib/internal/task-validate.ts +0 -5
  62. package/lib/platform/comment-safety.ts +8 -0
  63. package/lib/platform/github-provider.ts +11 -2
  64. package/lib/platform/issue-comments.ts +102 -17
  65. package/lib/platform/pr-summary.ts +10 -2
  66. package/lib/platform/provider-contract.ts +2 -0
  67. package/lib/platform/provider-validation.ts +6 -2
  68. package/lib/platform/verification-sync.ts +19 -2
  69. package/lib/sandbox/control/client.ts +5 -68
  70. package/lib/sandbox/control/executor.ts +14 -125
  71. package/lib/sandbox/control/finalization-response.ts +8 -2
  72. package/lib/sandbox/control/protocol.ts +7 -83
  73. package/lib/sandbox/control/server.ts +30 -220
  74. package/lib/sandbox/control/state.ts +1 -9
  75. package/lib/sandbox/control/task-view.ts +2 -1
  76. package/lib/task/artifact-command.ts +6 -7
  77. package/lib/task/codex-orchestration.ts +8 -241
  78. package/lib/task/commit-operation.ts +3 -23
  79. package/lib/task/control-authority.ts +39 -55
  80. package/lib/task/control-recovery.ts +3 -43
  81. package/lib/task/delegation-receipts.ts +14 -81
  82. package/lib/task/events.ts +106 -255
  83. package/lib/task/finalization-artifacts.ts +1 -3
  84. package/lib/task/finalization-state.ts +2 -1
  85. package/lib/task/finalization.ts +221 -65
  86. package/lib/task/lifecycle-execution.ts +10 -21
  87. package/lib/task/lifecycle.ts +2 -2
  88. package/lib/task/local-artifact-finalization.ts +23 -281
  89. package/lib/task/manual-validation-receipt.ts +8 -2
  90. package/lib/task/orchestration.ts +30 -53
  91. package/lib/task/review-command.ts +0 -1
  92. package/lib/task/review-finalization.ts +45 -193
  93. package/package.json +2 -2
  94. package/templates/.agents/rules/lifecycle-orchestration.en.md +23 -9
  95. package/templates/.agents/rules/lifecycle-orchestration.zh-CN.md +23 -9
  96. package/templates/.agents/rules/local-artifact-repair.en.md +14 -60
  97. package/templates/.agents/rules/local-artifact-repair.zh-CN.md +13 -60
  98. package/templates/.agents/skills/analyze-task/SKILL.en.md +3 -3
  99. package/templates/.agents/skills/analyze-task/SKILL.zh-CN.md +3 -3
  100. package/templates/.agents/skills/code-task/SKILL.en.md +3 -3
  101. package/templates/.agents/skills/code-task/SKILL.zh-CN.md +3 -3
  102. package/templates/.agents/skills/complete-manual-validation/SKILL.en.md +2 -2
  103. package/templates/.agents/skills/complete-manual-validation/SKILL.zh-CN.md +2 -2
  104. package/templates/.agents/skills/complete-task/SKILL.en.md +7 -8
  105. package/templates/.agents/skills/complete-task/SKILL.zh-CN.md +7 -8
  106. package/templates/.agents/skills/plan-task/SKILL.en.md +3 -3
  107. package/templates/.agents/skills/plan-task/SKILL.zh-CN.md +3 -3
  108. package/templates/.agents/skills/review-analysis/SKILL.en.md +2 -2
  109. package/templates/.agents/skills/review-analysis/SKILL.zh-CN.md +2 -2
  110. package/templates/.agents/skills/review-code/SKILL.en.md +3 -3
  111. package/templates/.agents/skills/review-code/SKILL.zh-CN.md +3 -3
  112. package/templates/.agents/skills/review-plan/SKILL.en.md +2 -2
  113. package/templates/.agents/skills/review-plan/SKILL.zh-CN.md +2 -2
  114. package/templates/.agents/skills/run-manual-validation/SKILL.en.md +2 -2
  115. package/templates/.agents/skills/run-manual-validation/SKILL.zh-CN.md +2 -2
  116. package/templates/.agents/skills/run-task/SKILL.en.md +10 -9
  117. package/templates/.agents/skills/run-task/SKILL.zh-CN.md +10 -9
  118. package/templates/.agents/skills/run-task/reference/host-validation.en.md +2 -2
  119. package/templates/.agents/skills/run-task/reference/host-validation.zh-CN.md +2 -2
  120. package/dist/lib/sandbox/control/task-workflow.js +0 -74
  121. package/dist/lib/sandbox/control/workflow-executor.js +0 -157
  122. package/dist/lib/task/lifecycle-recovery.js +0 -549
  123. package/dist/lib/task/recovery-warning.js +0 -19
  124. package/dist/lib/task/workflow-command.js +0 -18
  125. package/dist/lib/task/workflow-dispatch.js +0 -161
  126. package/lib/sandbox/control/task-workflow.ts +0 -95
  127. package/lib/sandbox/control/workflow-executor.ts +0 -183
  128. package/lib/task/lifecycle-recovery.ts +0 -691
  129. package/lib/task/recovery-warning.ts +0 -21
  130. package/lib/task/workflow-command.ts +0 -19
  131. package/lib/task/workflow-dispatch.ts +0 -171
@@ -33,15 +33,8 @@ 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';
@@ -393,65 +386,6 @@ function reviewInputFamily(family: EventFamily): ArtifactFamily {
393
386
  return family === 'review-analysis' ? 'analysis' : family === 'review-plan' ? 'plan' : 'code';
394
387
  }
395
388
 
396
- function reconcileReviewCompletionReplay(
397
- repoRoot: string,
398
- taskDir: string,
399
- taskId: string,
400
- family: EventFamily,
401
- artifact: string,
402
- round: number
403
- ): TaskEventError | null {
404
- const stage = REVIEW_LEDGER_STAGES[family];
405
- if (!stage) return null;
406
- const reviewFamily = family as 'review-analysis' | 'review-plan' | 'review-code';
407
- try {
408
- let intent = readArtifactRecoveryIntent(repoRoot, taskId, reviewFamily, artifact);
409
- if (!intent) return null;
410
- if (intent.state === 'commit-started') {
411
- const reconciled = reconcileArtifactRecovery(
412
- recoveryContextFromIntent(repoRoot, taskDir, intent),
413
- { lockAlreadyHeld: true }
414
- );
415
- if (reconciled.status !== 'passed' && reconciled.status !== 'consumed') {
416
- return {
417
- code: 'EVENT_ARTIFACT_CONFLICT',
418
- message: `review finalizer recovery remains ${reconciled.status} for ${artifact}`
419
- };
420
- }
421
- intent = reconciled.intent;
422
- }
423
- if (!['passed', 'consumption-started', 'consumed'].includes(intent.state)) {
424
- return {
425
- code: 'EVENT_ARTIFACT_CONFLICT',
426
- message: `review finalizer provenance is incomplete for ${artifact}`
427
- };
428
- }
429
- const validated = validateCompletedArtifact(taskDir, FAMILY[family].artifact, artifact, round);
430
- if (!validated.ok) {
431
- return { code: 'EVENT_ARTIFACT_CONFLICT', message: validated.error.message };
432
- }
433
- const content = fs.readFileSync(validated.artifact.path, 'utf8');
434
- if (intent.round !== round
435
- || intent.finalArtifactSha256 !== sha256Content(content)
436
- || intent.finalSemanticDigest !== canonicalSemanticDigest(content)) {
437
- return {
438
- code: 'EVENT_ARTIFACT_CONFLICT',
439
- message: `review finalizer provenance does not match ${artifact}`
440
- };
441
- }
442
- if (intent.state === 'passed' || intent.state === 'consumption-started') {
443
- const consumed = { ...intent, state: 'consumed' as const, updatedAt: Date.now() };
444
- writeArtifactRecoveryIntent(repoRoot, consumed, { expected: intent });
445
- }
446
- return null;
447
- } catch (error) {
448
- return {
449
- code: 'EVENT_ARTIFACT_CONFLICT',
450
- message: `review finalizer recovery could not be reconciled for ${artifact}: ${error instanceof Error ? error.message : String(error)}`
451
- };
452
- }
453
- }
454
-
455
389
  function relationForStartedInput(family: EventFamily, input: ArtifactIdentity): UpstreamRelation['relation'] {
456
390
  if (family.startsWith('review-')) {
457
391
  return input.family === reviewInputFamily(family) ? 'reviewed-input' : 'approval-context';
@@ -661,15 +595,25 @@ function reworkIntentMutationForCompletion(
661
595
  }
662
596
 
663
597
  function buildCompletionReceipt(
598
+ content: string,
664
599
  taskDir: string,
665
600
  family: EventFamily,
666
601
  artifact: ArtifactIdentity,
667
602
  completedAt: string,
668
603
  frontmatter: Record<string, unknown>
669
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
+ })();
670
612
  if (family === 'code') {
671
- const startedInput = typeof frontmatter.code_input_artifact === 'string' ? frontmatter.code_input_artifact : '';
672
- 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 ?? '';
673
617
  if (!startedInput || !startedSha256) return { ok: false, message: 'code.started plan input context is missing' };
674
618
  const plan = inspectArtifactDirectory(taskDir, 'plan');
675
619
  if (plan.status !== 'ready' || !plan.latest || plan.latest.name !== startedInput) {
@@ -682,7 +626,7 @@ function buildCompletionReceipt(
682
626
  ok: true,
683
627
  receipt: {
684
628
  event: 'code.completed', output: artifact.name, input: startedInput,
685
- inputSha256, completedAt
629
+ inputSha256, completedAt: existing?.completedAt ?? completedAt
686
630
  }
687
631
  };
688
632
  } catch (error) {
@@ -691,8 +635,10 @@ function buildCompletionReceipt(
691
635
  }
692
636
  if (!family.startsWith('review-')) return null;
693
637
  const expectedFamily = reviewInputFamily(family);
694
- const startedInput = typeof frontmatter.review_input_artifact === 'string' ? frontmatter.review_input_artifact : '';
695
- 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 ?? '';
696
642
  if (!startedInput || !startedSha256) return { ok: false, message: 'review started input context is missing' };
697
643
  const current = inspectArtifactDirectory(taskDir, expectedFamily);
698
644
  if (current.status !== 'ready' || !current.latest || current.latest.name !== startedInput) {
@@ -710,7 +656,7 @@ function buildCompletionReceipt(
710
656
  ok: true,
711
657
  receipt: {
712
658
  event, output: artifact.name, input: startedInput,
713
- inputSha256, completedAt
659
+ inputSha256, completedAt: existing?.completedAt ?? completedAt
714
660
  }
715
661
  };
716
662
  } catch (error) {
@@ -718,6 +664,69 @@ function buildCompletionReceipt(
718
664
  }
719
665
  }
720
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
+
721
730
  function applyTaskEventUnlocked(request: TaskEventRequest, options: TaskEventOptions = {}): TaskEventResult {
722
731
  const invalid = validateTaskEventRequest(request);
723
732
  if (invalid) return failed(request, invalid);
@@ -794,13 +803,11 @@ function applyTaskEventUnlocked(request: TaskEventRequest, options: TaskEventOpt
794
803
  const currentStep = typeof frontmatter.current_step === 'string' ? frontmatter.current_step : '';
795
804
  const matchingRows = rows.filter((item) => item.step === eventIdentity.action);
796
805
  const manual = eventIdentity.family === 'manual-validation';
797
- 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 });
798
806
  const openRows = matchingRows.filter((item) => item.started && !item.done);
799
807
  const completedRows = matchingRows.filter((item) => item.done);
800
- 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);
801
810
  let completedArtifact: ArtifactIdentity | null = null;
802
- let localFinalizationIntent: LocalArtifactFinalizationIntent | null = null;
803
- let reviewFinalizationIntent: ArtifactRecoveryIntent | null = null;
804
811
  let reviewContent: string | null = null;
805
812
  if (eventIdentity.phase === 'started' && row) {
806
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 });
@@ -812,24 +819,7 @@ function applyTaskEventUnlocked(request: TaskEventRequest, options: TaskEventOpt
812
819
  }
813
820
  return successNoOp(normalized, resolved.taskId, resolved.taskMdPath, currentStep, eventIdentity, row.started, frontmatter, artifactContext);
814
821
  }
815
- 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 });
816
- const done = completedRows.find((item) => item.note === eventIdentity.note);
817
- if (eventIdentity.phase === 'completed' && done) {
818
- if (!normalized.dryRun && eventIdentity.family.startsWith('review-') && normalized.artifact && normalized.round) {
819
- const recoveryError = reconcileReviewCompletionReplay(
820
- resolved.repoRoot,
821
- resolved.taskDir,
822
- resolved.taskId,
823
- eventIdentity.family,
824
- normalized.artifact,
825
- normalized.round
826
- );
827
- if (recoveryError) return failed(normalized, recoveryError, { taskId: resolved.taskId, taskMdPath: resolved.taskMdPath, fromStep: currentStep, toStep: currentStep, action: eventIdentity.action, phase: eventIdentity.phase });
828
- }
829
- return successNoOp(normalized, resolved.taskId, resolved.taskMdPath, currentStep, eventIdentity, done.done, frontmatter, artifactContext);
830
- }
831
- 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 });
832
- 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 });
833
823
  if (eventIdentity.phase === 'completed') {
834
824
  const validated = validateCompletedArtifact(resolved.taskDir, FAMILY[eventIdentity.family].artifact, normalized.artifact!, normalized.round);
835
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 });
@@ -870,23 +860,6 @@ function applyTaskEventUnlocked(request: TaskEventRequest, options: TaskEventOpt
870
860
  const localFamily = eventIdentity.family === 'analyze'
871
861
  ? 'analysis'
872
862
  : eventIdentity.family === 'plan' ? 'plan' : 'code';
873
- try {
874
- localFinalizationIntent = readLocalArtifactFinalizationIntent(
875
- resolved.repoRoot, resolved.taskId, localFamily, completedArtifact.name
876
- );
877
- if (localFinalizationIntent?.state === 'commit-started') {
878
- const reconciled = reconcileArtifactRecovery(
879
- recoveryContextFromIntent(resolved.repoRoot, resolved.taskDir, localFinalizationIntent),
880
- { lockAlreadyHeld: true }
881
- );
882
- if (reconciled.status === 'passed' || reconciled.status === 'consumed') localFinalizationIntent = reconciled.intent;
883
- }
884
- } catch (error) {
885
- return failed(normalized, {
886
- code: 'EVENT_ARTIFACT_CONFLICT',
887
- message: `local finalizer recovery could not be reconciled for ${completedArtifact.name}: ${error instanceof Error ? error.message : String(error)}`
888
- }, { taskId: resolved.taskId, taskMdPath: resolved.taskMdPath, fromStep: currentStep, toStep: currentStep, action: eventIdentity.action, phase: eventIdentity.phase });
889
- }
890
863
  let artifactContent: string;
891
864
  try { artifactContent = fs.readFileSync(completedArtifact.path, 'utf8'); }
892
865
  catch (error) {
@@ -916,29 +889,6 @@ function applyTaskEventUnlocked(request: TaskEventRequest, options: TaskEventOpt
916
889
  message: `semantic digest does not match finalizer result for ${completedArtifact.name}`
917
890
  }, { taskId: resolved.taskId, taskMdPath: resolved.taskMdPath, fromStep: currentStep, toStep: currentStep, action: eventIdentity.action, phase: eventIdentity.phase });
918
891
  }
919
- if (!localFinalizationIntent || !['passed', 'consumed'].includes(localFinalizationIntent.state)) {
920
- return failed(normalized, {
921
- code: 'EVENT_ARTIFACT_CONFLICT',
922
- message: `local finalizer provenance is missing or incomplete for ${completedArtifact.name}`
923
- }, { taskId: resolved.taskId, taskMdPath: resolved.taskMdPath, fromStep: currentStep, toStep: currentStep, action: eventIdentity.action, phase: eventIdentity.phase });
924
- }
925
- if (localFinalizationIntent.finalArtifactSha256 !== actualSha256 || localFinalizationIntent.finalSemanticDigest !== local.semanticDigest) {
926
- return failed(normalized, {
927
- code: 'EVENT_ARTIFACT_CONFLICT',
928
- message: `local finalizer provenance does not match ${completedArtifact.name}`
929
- }, { taskId: resolved.taskId, taskMdPath: resolved.taskMdPath, fromStep: currentStep, toStep: currentStep, action: eventIdentity.action, phase: eventIdentity.phase });
930
- }
931
- if (localFinalizationIntent.authorityDigest !== null) {
932
- if (!lifecycleAuthority
933
- || localFinalizationIntent.recoveryOperationId !== lifecycleAuthority.operationId
934
- || localFinalizationIntent.requestId !== lifecycleAuthority.lifecycleRequestId
935
- || localFinalizationIntent.authorityDigest !== lifecycleRecoveryAttestationDigest(lifecycleAuthority)) {
936
- return failed(normalized, {
937
- code: 'EVENT_ARTIFACT_CONFLICT',
938
- message: `local finalizer provenance does not match lifecycle completion authority for ${completedArtifact.name}`
939
- }, { taskId: resolved.taskId, taskMdPath: resolved.taskMdPath, fromStep: currentStep, toStep: currentStep, action: eventIdentity.action, phase: eventIdentity.phase, artifactContext });
940
- }
941
- }
942
892
  }
943
893
  }
944
894
  if (eventIdentity.phase === 'waiting' && section.entries.some((entry) => entry.step === eventIdentity.action && entry.note === eventIdentity.note)) {
@@ -965,44 +915,6 @@ function applyTaskEventUnlocked(request: TaskEventRequest, options: TaskEventOpt
965
915
  completedArtifact?.path ?? null
966
916
  );
967
917
  if (findingCountError) return failed(normalized, findingCountError, { taskId: resolved.taskId, taskMdPath: resolved.taskMdPath, fromStep: currentStep, toStep: currentStep, action: eventIdentity.action, phase: eventIdentity.phase });
968
- if (eventIdentity.phase === 'completed' && eventIdentity.family.startsWith('review-') && completedArtifact && reviewContent !== null) {
969
- try {
970
- reviewFinalizationIntent = readArtifactRecoveryIntent(
971
- resolved.repoRoot,
972
- resolved.taskId,
973
- eventIdentity.family as 'review-analysis' | 'review-plan' | 'review-code',
974
- completedArtifact.name
975
- );
976
- if (!normalized.dryRun && reviewFinalizationIntent?.state === 'commit-started') {
977
- const reconciled = reconcileArtifactRecovery(
978
- recoveryContextFromIntent(resolved.repoRoot, resolved.taskDir, reviewFinalizationIntent),
979
- { lockAlreadyHeld: true }
980
- );
981
- if (reconciled.status === 'passed' || reconciled.status === 'consumed') reviewFinalizationIntent = reconciled.intent;
982
- }
983
- if (!reviewFinalizationIntent || !['passed', 'consumption-started', 'consumed'].includes(reviewFinalizationIntent.state)) {
984
- return failed(normalized, {
985
- code: 'EVENT_ARTIFACT_CONFLICT',
986
- message: `review finalizer provenance is missing or incomplete for ${completedArtifact.name}`
987
- }, { taskId: resolved.taskId, taskMdPath: resolved.taskMdPath, fromStep: currentStep, toStep: currentStep, action: eventIdentity.action, phase: eventIdentity.phase });
988
- }
989
- const actualSha256 = sha256File(completedArtifact.path);
990
- const actualSemanticDigest = canonicalSemanticDigest(reviewContent);
991
- if (reviewFinalizationIntent.round !== normalized.round
992
- || reviewFinalizationIntent.finalArtifactSha256 !== actualSha256
993
- || reviewFinalizationIntent.finalSemanticDigest !== actualSemanticDigest) {
994
- return failed(normalized, {
995
- code: 'EVENT_ARTIFACT_CONFLICT',
996
- message: `review finalizer provenance does not match ${completedArtifact.name}`
997
- }, { taskId: resolved.taskId, taskMdPath: resolved.taskMdPath, fromStep: currentStep, toStep: currentStep, action: eventIdentity.action, phase: eventIdentity.phase });
998
- }
999
- } catch (error) {
1000
- return failed(normalized, {
1001
- code: 'EVENT_ARTIFACT_CONFLICT',
1002
- message: `review finalizer recovery could not be reconciled for ${completedArtifact.name}: ${error instanceof Error ? error.message : String(error)}`
1003
- }, { taskId: resolved.taskId, taskMdPath: resolved.taskMdPath, fromStep: currentStep, toStep: currentStep, action: eventIdentity.action, phase: eventIdentity.phase });
1004
- }
1005
- }
1006
918
  let orchestrationCompletion: OrchestrationStageCompletion | null = null;
1007
919
  if (eventIdentity.phase === 'completed' && eventIdentity.family !== 'manual-validation' && eventIdentity.family !== 'validation-run') {
1008
920
  const orchestrationStage = eventIdentity.family === 'analyze' ? 'analysis' : eventIdentity.family;
@@ -1029,14 +941,32 @@ function applyTaskEventUnlocked(request: TaskEventRequest, options: TaskEventOpt
1029
941
  try { metadata = (options.metadataProvider ?? captureTaskWriteMetadata)(); }
1030
942
  catch (error) { return failed(normalized, { code: 'METADATA_CAPTURE_FAILED', message: String(error) }, { taskId: resolved.taskId, taskMdPath: resolved.taskMdPath }); }
1031
943
  let completionReceipt: ArtifactReceipt | null = null;
944
+ let currentFact: CompletionFact | null = null;
1032
945
  let completionInvalidation: ReturnType<typeof invalidationMutation> | null = null;
1033
946
  let completionRework: ReturnType<typeof reworkIntentMutation> | null = null;
1034
947
  if (eventIdentity.phase === 'completed' && completedArtifact) {
1035
- const receipt = buildCompletionReceipt(resolved.taskDir, eventIdentity.family, completedArtifact, metadata.timestamp, frontmatter);
948
+ const receipt = buildCompletionReceipt(content, resolved.taskDir, eventIdentity.family, completedArtifact, metadata.timestamp, frontmatter);
1036
949
  if (receipt && !receipt.ok) {
1037
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 });
1038
951
  }
1039
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
+ }
1040
970
  try {
1041
971
  const invalidation = invalidationMutationForCompletion(content, resolved.taskDir, eventIdentity.family, completedArtifact, metadata.timestamp);
1042
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 });
@@ -1052,6 +982,7 @@ function applyTaskEventUnlocked(request: TaskEventRequest, options: TaskEventOpt
1052
982
  const logStep = eventIdentity.phase === 'started' ? `${eventIdentity.action} [started]` : eventIdentity.action;
1053
983
  const body = appendActivityEntry(section, { time: metadata.timestamp, step: logStep, agent: normalized.agent, note: eventIdentity.note });
1054
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));
1055
986
  let frontmatterRemove: string[] | undefined;
1056
987
  if (eventIdentity.phase === 'started' && artifactContext) {
1057
988
  try {
@@ -1127,90 +1058,10 @@ function applyTaskEventUnlocked(request: TaskEventRequest, options: TaskEventOpt
1127
1058
  });
1128
1059
  }
1129
1060
  mutations.push({ kind: 'section', aliases: ['活动日志', 'Activity Log'], heading: section.heading, body });
1130
- if (!normalized.dryRun && localFinalizationIntent && completedArtifact) {
1131
- try {
1132
- if (lifecycleAuthority && localFinalizationIntent.state === 'passed') {
1133
- const expectedIntent = localFinalizationIntent;
1134
- localFinalizationIntent = {
1135
- ...localFinalizationIntent,
1136
- state: 'consumption-started',
1137
- phase: 'task-event.completed',
1138
- authorityDigest: lifecycleRecoveryAttestationDigest(lifecycleAuthority),
1139
- requestId: lifecycleAuthority.lifecycleRequestId,
1140
- updatedAt: Date.now()
1141
- };
1142
- writeArtifactRecoveryIntent(resolved.repoRoot, localFinalizationIntent, { expected: expectedIntent });
1143
- } else if (!lifecycleAuthority && localFinalizationIntent.state === 'passed') {
1144
- localFinalizationIntent = consumeLocalArtifactFinalizationIntent(resolved.repoRoot, localFinalizationIntent, { lockAlreadyHeld: options.lockAlreadyHeld });
1145
- }
1146
- } catch (error) {
1147
- return failed(normalized, {
1148
- code: 'EVENT_ARTIFACT_CONFLICT',
1149
- message: `local finalizer provenance could not be advanced before task write for ${completedArtifact.name}: ${error instanceof Error ? error.message : String(error)}`
1150
- }, {
1151
- taskId: resolved.taskId,
1152
- taskMdPath: resolved.taskMdPath,
1153
- fromStep: currentStep,
1154
- toStep: step,
1155
- action: eventIdentity.action,
1156
- phase: eventIdentity.phase,
1157
- artifactContext
1158
- });
1159
- }
1160
- }
1161
- if (!normalized.dryRun && reviewFinalizationIntent && completedArtifact && reviewFinalizationIntent.state === 'passed') {
1162
- try {
1163
- const expectedIntent = reviewFinalizationIntent;
1164
- reviewFinalizationIntent = {
1165
- ...reviewFinalizationIntent,
1166
- state: 'consumption-started',
1167
- phase: 'task-event.completed',
1168
- updatedAt: Date.now()
1169
- };
1170
- writeArtifactRecoveryIntent(resolved.repoRoot, reviewFinalizationIntent, { expected: expectedIntent });
1171
- } catch (error) {
1172
- return failed(normalized, {
1173
- code: 'EVENT_ARTIFACT_CONFLICT',
1174
- message: `review finalizer provenance could not be advanced before task write for ${completedArtifact.name}: ${error instanceof Error ? error.message : String(error)}`
1175
- }, {
1176
- taskId: resolved.taskId,
1177
- taskMdPath: resolved.taskMdPath,
1178
- fromStep: currentStep,
1179
- toStep: step,
1180
- action: eventIdentity.action,
1181
- phase: eventIdentity.phase,
1182
- artifactContext
1183
- });
1184
- }
1185
- }
1186
1061
  const sourceCompletion = eventIdentity.phase === 'completed'
1187
1062
  && ['analyze', 'plan', 'code'].includes(eventIdentity.family);
1188
1063
  const result = writeTask({ taskRef: normalized.taskRef, expectedState: stateOverride ? resolved.state : 'active', dryRun: normalized.dryRun, mutations }, { ...options, invalidationContext: sourceCompletion ? 'source-completion' : 'standard', metadataProvider: () => metadata });
1189
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 });
1190
- if (!normalized.dryRun && lifecycleAuthority && localFinalizationIntent && completedArtifact) {
1191
- try {
1192
- if (!options.deferLifecycleRecoveryConsumption) consumeLifecycleRecoveryAttestation(lifecycleAuthority);
1193
- writeArtifactRecoveryIntent(resolved.repoRoot, { ...localFinalizationIntent, state: 'consumed', updatedAt: Date.now() }, { expected: localFinalizationIntent });
1194
- } catch (error) {
1195
- return failed(normalized, {
1196
- code: 'EVENT_ARTIFACT_CONFLICT',
1197
- message: `task completion was written but lifecycle recovery consumption is incomplete: ${error instanceof Error ? error.message : String(error)}`
1198
- }, { 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 });
1199
- }
1200
- }
1201
- if (!normalized.dryRun && reviewFinalizationIntent && completedArtifact) {
1202
- try {
1203
- if (reviewFinalizationIntent.state === 'consumption-started') {
1204
- const consumed = { ...reviewFinalizationIntent, state: 'consumed' as const, updatedAt: Date.now() };
1205
- writeArtifactRecoveryIntent(resolved.repoRoot, consumed, { expected: reviewFinalizationIntent });
1206
- }
1207
- } catch (error) {
1208
- return failed(normalized, {
1209
- code: 'EVENT_ARTIFACT_CONFLICT',
1210
- message: `review finalizer recovery consumption is incomplete: ${error instanceof Error ? error.message : String(error)}`
1211
- }, { 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 });
1212
- }
1213
- }
1214
1065
  if (!normalized.dryRun && orchestrationCompletion) {
1215
1066
  try {
1216
1067
  (options.commitOrchestrationCompletion ?? commitOrchestrationStageCompletion)(orchestrationCompletion);
@@ -15,9 +15,7 @@ const COMPLETION_BACKFILL_FAMILIES = [
15
15
  'plan',
16
16
  'review-plan',
17
17
  'code',
18
- 'review-code',
19
- 'manual-validation',
20
- 'validation-run'
18
+ 'review-code'
21
19
  ] as const satisfies readonly ArtifactFamily[];
22
20
 
23
21
  type CompletionArtifactResult = {
@@ -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.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
  }