@fitlab-ai/agent-infra 0.11.1 → 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 (88) hide show
  1. package/dist/lib/internal/cli-route-inventory.js +1 -1
  2. package/dist/lib/internal/platform-comment.js +59 -0
  3. package/dist/lib/internal/sandbox-control.js +4 -131
  4. package/dist/lib/internal/task-artifact.js +1 -1
  5. package/dist/lib/internal/task-operation-registry.js +1 -0
  6. package/dist/lib/internal/task-review.js +5 -5
  7. package/dist/lib/internal/task-validate.js +0 -6
  8. package/dist/lib/platform/comment-safety.js +7 -1
  9. package/dist/lib/platform/github-provider.js +13 -3
  10. package/dist/lib/platform/issue-comments.js +120 -13
  11. package/dist/lib/platform/provider-contract.d.ts +2 -0
  12. package/dist/lib/platform/provider-validation.js +7 -2
  13. package/dist/lib/platform/verification-sync.js +19 -2
  14. package/dist/lib/sandbox/control/executor.js +0 -38
  15. package/dist/lib/sandbox/control/finalization-response.js +10 -2
  16. package/dist/lib/sandbox/control/task-view.js +3 -1
  17. package/dist/lib/sandbox/control/workflow-executor.js +28 -43
  18. package/dist/lib/task/artifact-command.js +6 -6
  19. package/dist/lib/task/codex-orchestration.js +10 -208
  20. package/dist/lib/task/commit-operation.js +3 -18
  21. package/dist/lib/task/control-authority.js +1 -8
  22. package/dist/lib/task/delegation-receipts.js +14 -76
  23. package/dist/lib/task/events.js +89 -237
  24. package/dist/lib/task/finalization-state.js +2 -1
  25. package/dist/lib/task/finalization.js +273 -49
  26. package/dist/lib/task/lifecycle-execution.js +2 -11
  27. package/dist/lib/task/local-artifact-finalization.js +25 -242
  28. package/dist/lib/task/orchestration.js +0 -18
  29. package/dist/lib/task/review-command.js +0 -1
  30. package/dist/lib/task/review-finalization.js +46 -211
  31. package/dist/lib/task/workflow-command.js +4 -1
  32. package/dist/lib/task/workflow-dispatch.js +55 -3
  33. package/dist/lifecycle-build-manifest.json +3 -3
  34. package/lib/internal/cli-route-inventory.ts +1 -1
  35. package/lib/internal/platform-comment.ts +38 -0
  36. package/lib/internal/sandbox-control.ts +3 -145
  37. package/lib/internal/task-artifact.ts +1 -1
  38. package/lib/internal/task-operation-registry.ts +1 -0
  39. package/lib/internal/task-review.ts +5 -5
  40. package/lib/internal/task-validate.ts +0 -5
  41. package/lib/platform/comment-safety.ts +8 -0
  42. package/lib/platform/github-provider.ts +11 -2
  43. package/lib/platform/issue-comments.ts +132 -13
  44. package/lib/platform/provider-contract.ts +2 -0
  45. package/lib/platform/provider-validation.ts +6 -2
  46. package/lib/platform/verification-sync.ts +19 -2
  47. package/lib/sandbox/control/executor.ts +0 -44
  48. package/lib/sandbox/control/finalization-response.ts +10 -2
  49. package/lib/sandbox/control/task-view.ts +3 -1
  50. package/lib/sandbox/control/workflow-executor.ts +23 -50
  51. package/lib/task/artifact-command.ts +6 -7
  52. package/lib/task/codex-orchestration.ts +8 -241
  53. package/lib/task/commit-operation.ts +3 -23
  54. package/lib/task/control-authority.ts +1 -8
  55. package/lib/task/delegation-receipts.ts +12 -79
  56. package/lib/task/events.ts +106 -255
  57. package/lib/task/finalization-state.ts +2 -1
  58. package/lib/task/finalization.ts +277 -57
  59. package/lib/task/lifecycle-execution.ts +10 -21
  60. package/lib/task/local-artifact-finalization.ts +23 -281
  61. package/lib/task/orchestration.ts +0 -34
  62. package/lib/task/review-command.ts +0 -1
  63. package/lib/task/review-finalization.ts +45 -193
  64. package/lib/task/workflow-command.ts +4 -1
  65. package/lib/task/workflow-dispatch.ts +46 -3
  66. package/package.json +2 -2
  67. package/templates/.agents/rules/lifecycle-orchestration.en.md +7 -7
  68. package/templates/.agents/rules/lifecycle-orchestration.zh-CN.md +7 -7
  69. package/templates/.agents/rules/local-artifact-repair.en.md +14 -60
  70. package/templates/.agents/rules/local-artifact-repair.zh-CN.md +13 -60
  71. package/templates/.agents/skills/analyze-task/SKILL.en.md +3 -3
  72. package/templates/.agents/skills/analyze-task/SKILL.zh-CN.md +3 -3
  73. package/templates/.agents/skills/code-task/SKILL.en.md +3 -3
  74. package/templates/.agents/skills/code-task/SKILL.zh-CN.md +3 -3
  75. package/templates/.agents/skills/complete-task/SKILL.en.md +3 -3
  76. package/templates/.agents/skills/complete-task/SKILL.zh-CN.md +3 -3
  77. package/templates/.agents/skills/plan-task/SKILL.en.md +3 -3
  78. package/templates/.agents/skills/plan-task/SKILL.zh-CN.md +3 -3
  79. package/templates/.agents/skills/review-analysis/SKILL.en.md +2 -2
  80. package/templates/.agents/skills/review-analysis/SKILL.zh-CN.md +2 -2
  81. package/templates/.agents/skills/review-code/SKILL.en.md +3 -3
  82. package/templates/.agents/skills/review-code/SKILL.zh-CN.md +3 -3
  83. package/templates/.agents/skills/review-plan/SKILL.en.md +2 -2
  84. package/templates/.agents/skills/review-plan/SKILL.zh-CN.md +2 -2
  85. package/templates/.agents/skills/run-task/SKILL.en.md +6 -6
  86. package/templates/.agents/skills/run-task/SKILL.zh-CN.md +6 -6
  87. package/templates/.agents/skills/run-task/reference/host-validation.en.md +2 -2
  88. package/templates/.agents/skills/run-task/reference/host-validation.zh-CN.md +2 -2
@@ -108,13 +108,6 @@ function exactText(value: unknown): value is string {
108
108
  return typeof value === 'string' && value.length > 0 && value.trim() === value;
109
109
  }
110
110
 
111
- function validCodexBuildMetadata(value: Readonly<Record<string, unknown>>): boolean {
112
- return Number.isSafeInteger(value.protocolVersion) && (value.protocolVersion as number) > 0
113
- && exactText(value.packageVersion)
114
- && exactText(value.internalExecutableBuildHash)
115
- && exactText(value.lifecycleContractHash);
116
- }
117
-
118
111
  function nullableText(value: unknown): value is string | null {
119
112
  return value === null || exactText(value);
120
113
  }
@@ -183,52 +176,6 @@ function isDelegationHostEvidence(value: unknown): value is DelegationHostEviden
183
176
  && nullableText(value.controlGeneration);
184
177
  }
185
178
 
186
- function hasCurrentCodexEvidence(receipt: DelegationReceipt): boolean {
187
- const provenance = receipt.lifecycleProvenance;
188
- if (!provenance) return false;
189
- const activated = ['activated', 'stage-completed', 'sealed', 'consumed'].includes(receipt.status)
190
- || (receipt.status === 'aborted' && receipt.activatedAt !== null);
191
- if (!activated) return receipt.hostEvidence === null;
192
- const host = receipt.hostEvidence;
193
- if (
194
- host?.kind !== 'codex-lifecycle-v2'
195
- || host.protocolVersion !== provenance.protocolVersion
196
- || host.hookDefinitionHash !== provenance.hookDefinitionHash
197
- || host.hookSource !== provenance.hookSource
198
- || host.hookSourcePathDigest !== provenance.hookSourcePathDigest
199
- || host.hookSourceHash !== provenance.hookSourceHash
200
- || host.capabilitySessionId !== provenance.capabilitySessionId
201
- || host.capabilityTurnId !== provenance.capabilityTurnId
202
- || host.controllerInstanceDigest !== provenance.controllerInstanceDigest
203
- || host.controlGeneration !== provenance.controlGeneration
204
- || receipt.parentId !== provenance.capabilitySessionId
205
- || host.spawnToolUseId === provenance.capabilityToolUseId
206
- ) return false;
207
- const spawnObservedAt = Date.parse(host.spawnObservedAt ?? '');
208
- const spawnDispatchedAt = Date.parse(receipt.spawnDispatchedAt ?? '');
209
- const activationDeadlineAt = Date.parse(receipt.activationDeadlineAt ?? '');
210
- if (
211
- !Number.isFinite(spawnObservedAt)
212
- || !Number.isFinite(spawnDispatchedAt)
213
- || !Number.isFinite(activationDeadlineAt)
214
- || spawnObservedAt < spawnDispatchedAt
215
- || spawnObservedAt > activationDeadlineAt
216
- ) return false;
217
- if (['sealed', 'consumed'].includes(receipt.status)) {
218
- return Number.isSafeInteger(host.stopRevision)
219
- && (host.stopRevision as number) > host.startRevision
220
- && host.consumer === receipt.id
221
- && exactText(host.consumedAt);
222
- }
223
- if (receipt.status === 'aborted') {
224
- return Number.isSafeInteger(host.stopRevision)
225
- && (host.stopRevision as number) > host.startRevision
226
- && host.consumer === `lifecycle-recovery:${receipt.taskId}:${receipt.id}`
227
- && exactText(host.consumedAt);
228
- }
229
- return host.stopRevision === null && host.consumer === null && host.consumedAt === null;
230
- }
231
-
232
179
  const RECEIPT_KEYS = [
233
180
  'id', 'taskId', 'runId', 'role', 'stage', 'round', 'artifact', 'client',
234
181
  'requestedModel', 'requestedReasoningEffort', 'actualModel', 'actualReasoningEffort',
@@ -366,8 +313,15 @@ function isDelegationReceipt(value: unknown): value is DelegationReceipt {
366
313
  && nullableText(value.consumedAt);
367
314
  if (!structurallyValid) return false;
368
315
  const receipt = value as unknown as DelegationReceipt;
369
- return hasStatusBoundEvidence(receipt)
370
- && (receipt.client !== 'codex' || hasCurrentCodexEvidence(receipt));
316
+ if (!hasStatusBoundEvidence(receipt)) return false;
317
+ const host = receipt.hostEvidence;
318
+ if (receipt.client === 'codex' && receipt.activatedAt !== null) {
319
+ if (host?.kind !== 'codex-lifecycle-v2' || host.capabilitySessionId !== receipt.parentId) return false;
320
+ if (['sealed', 'consumed'].includes(receipt.status)) {
321
+ return Number.isSafeInteger(host.stopRevision) && host.stopRevision! > host.startRevision;
322
+ }
323
+ }
324
+ return true;
371
325
  }
372
326
 
373
327
  const MANAGED_AGENTS = {
@@ -550,30 +504,9 @@ function activateDelegation(
550
504
  || event.hostEvidence.startRevision < 1
551
505
  )
552
506
  ) return fail('DELEGATION_HOST_EVIDENCE_INVALID', 'Codex start evidence reference is invalid');
553
- if (event.hostEvidence?.kind === 'codex-lifecycle-v2') {
554
- const expected = receipt.lifecycleProvenance;
555
- const spawnObservedAt = Date.parse(event.hostEvidence.spawnObservedAt ?? '');
556
- const spawnDispatchedAt = Date.parse(receipt.spawnDispatchedAt);
557
- const activationDeadlineAt = Date.parse(receipt.activationDeadlineAt);
558
- if (
559
- !expected
560
- || event.hostEvidence.protocolVersion !== expected.protocolVersion
561
- || event.hostEvidence.hookDefinitionHash !== expected.hookDefinitionHash
562
- || event.hostEvidence.hookSource !== expected.hookSource
563
- || event.hostEvidence.hookSourcePathDigest !== expected.hookSourcePathDigest
564
- || event.hostEvidence.hookSourceHash !== expected.hookSourceHash
565
- || event.hostEvidence.capabilitySessionId !== expected.capabilitySessionId
566
- || event.hostEvidence.capabilityTurnId !== expected.capabilityTurnId
567
- || !event.hostEvidence.spawnToolUseId?.trim()
568
- || event.hostEvidence.spawnToolUseId === expected.capabilityToolUseId
569
- || !Number.isFinite(spawnObservedAt)
570
- || spawnObservedAt < spawnDispatchedAt
571
- || spawnObservedAt > activationDeadlineAt
572
- || event.parentId !== expected.capabilitySessionId
573
- || (event.hostEvidence.controllerInstanceDigest ?? null) !== expected.controllerInstanceDigest
574
- || (event.hostEvidence.controlGeneration ?? null) !== expected.controlGeneration
575
- || !validCodexBuildMetadata(event.hostEvidence)
576
- ) return fail('DELEGATION_HOST_EVIDENCE_INVALID', 'Codex lifecycle provenance does not match the prepared receipt');
507
+ if (event.hostEvidence?.kind === 'codex-lifecycle-v2'
508
+ && event.hostEvidence.capabilitySessionId !== event.parentId) {
509
+ return fail('DELEGATION_HOST_EVIDENCE_INVALID', 'Codex start evidence does not match the observed parent');
577
510
  }
578
511
  return { ok: true, receipt: Object.freeze({
579
512
  ...receipt,
@@ -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);
@@ -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
  }