@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
@@ -15,12 +15,6 @@ function hasExactKeys(value, required, optional = []) {
15
15
  function exactText(value) {
16
16
  return typeof value === 'string' && value.length > 0 && value.trim() === value;
17
17
  }
18
- function validCodexBuildMetadata(value) {
19
- return Number.isSafeInteger(value.protocolVersion) && value.protocolVersion > 0
20
- && exactText(value.packageVersion)
21
- && exactText(value.internalExecutableBuildHash)
22
- && exactText(value.lifecycleContractHash);
23
- }
24
18
  function nullableText(value) {
25
19
  return value === null || exactText(value);
26
20
  }
@@ -88,51 +82,6 @@ function isDelegationHostEvidence(value) {
88
82
  && nullableText(value.controllerInstanceDigest)
89
83
  && nullableText(value.controlGeneration);
90
84
  }
91
- function hasCurrentCodexEvidence(receipt) {
92
- const provenance = receipt.lifecycleProvenance;
93
- if (!provenance)
94
- return false;
95
- const activated = ['activated', 'stage-completed', 'sealed', 'consumed'].includes(receipt.status)
96
- || (receipt.status === 'aborted' && receipt.activatedAt !== null);
97
- if (!activated)
98
- return receipt.hostEvidence === null;
99
- const host = receipt.hostEvidence;
100
- if (host?.kind !== 'codex-lifecycle-v2'
101
- || host.protocolVersion !== provenance.protocolVersion
102
- || host.hookDefinitionHash !== provenance.hookDefinitionHash
103
- || host.hookSource !== provenance.hookSource
104
- || host.hookSourcePathDigest !== provenance.hookSourcePathDigest
105
- || host.hookSourceHash !== provenance.hookSourceHash
106
- || host.capabilitySessionId !== provenance.capabilitySessionId
107
- || host.capabilityTurnId !== provenance.capabilityTurnId
108
- || host.controllerInstanceDigest !== provenance.controllerInstanceDigest
109
- || host.controlGeneration !== provenance.controlGeneration
110
- || receipt.parentId !== provenance.capabilitySessionId
111
- || host.spawnToolUseId === provenance.capabilityToolUseId)
112
- return false;
113
- const spawnObservedAt = Date.parse(host.spawnObservedAt ?? '');
114
- const spawnDispatchedAt = Date.parse(receipt.spawnDispatchedAt ?? '');
115
- const activationDeadlineAt = Date.parse(receipt.activationDeadlineAt ?? '');
116
- if (!Number.isFinite(spawnObservedAt)
117
- || !Number.isFinite(spawnDispatchedAt)
118
- || !Number.isFinite(activationDeadlineAt)
119
- || spawnObservedAt < spawnDispatchedAt
120
- || spawnObservedAt > activationDeadlineAt)
121
- return false;
122
- if (['sealed', 'consumed'].includes(receipt.status)) {
123
- return Number.isSafeInteger(host.stopRevision)
124
- && host.stopRevision > host.startRevision
125
- && host.consumer === receipt.id
126
- && exactText(host.consumedAt);
127
- }
128
- if (receipt.status === 'aborted') {
129
- return Number.isSafeInteger(host.stopRevision)
130
- && host.stopRevision > host.startRevision
131
- && host.consumer === `lifecycle-recovery:${receipt.taskId}:${receipt.id}`
132
- && exactText(host.consumedAt);
133
- }
134
- return host.stopRevision === null && host.consumer === null && host.consumedAt === null;
135
- }
136
85
  const RECEIPT_KEYS = [
137
86
  'id', 'taskId', 'runId', 'role', 'stage', 'round', 'artifact', 'client',
138
87
  'requestedModel', 'requestedReasoningEffort', 'actualModel', 'actualReasoningEffort',
@@ -200,7 +149,7 @@ function hasStatusBoundEvidence(receipt) {
200
149
  && host !== undefined
201
150
  && Number.isSafeInteger(host.stopRevision)
202
151
  && host.stopRevision > host.startRevision
203
- && host.consumer === `lifecycle-recovery:${receipt.taskId}:${receipt.id}`
152
+ && host.consumer === receipt.id
204
153
  && exactText(host.consumedAt);
205
154
  }
206
155
  if (!exactText(receipt.agent)
@@ -263,8 +212,17 @@ function isDelegationReceipt(value) {
263
212
  if (!structurallyValid)
264
213
  return false;
265
214
  const receipt = value;
266
- return hasStatusBoundEvidence(receipt)
267
- && (receipt.client !== 'codex' || hasCurrentCodexEvidence(receipt));
215
+ if (!hasStatusBoundEvidence(receipt))
216
+ return false;
217
+ const host = receipt.hostEvidence;
218
+ if (receipt.client === 'codex' && receipt.activatedAt !== null) {
219
+ if (host?.kind !== 'codex-lifecycle-v2' || host.capabilitySessionId !== receipt.parentId)
220
+ return false;
221
+ if (['sealed', 'consumed'].includes(receipt.status)) {
222
+ return Number.isSafeInteger(host.stopRevision) && host.stopRevision > host.startRevision;
223
+ }
224
+ }
225
+ return true;
268
226
  }
269
227
  const MANAGED_AGENTS = {
270
228
  'agent-infra-lifecycle-executor': 'executor',
@@ -397,29 +355,9 @@ function activateDelegation(receipt, event, options = {}) {
397
355
  || !Number.isSafeInteger(event.hostEvidence.startRevision)
398
356
  || event.hostEvidence.startRevision < 1))
399
357
  return fail('DELEGATION_HOST_EVIDENCE_INVALID', 'Codex start evidence reference is invalid');
400
- if (event.hostEvidence?.kind === 'codex-lifecycle-v2') {
401
- const expected = receipt.lifecycleProvenance;
402
- const spawnObservedAt = Date.parse(event.hostEvidence.spawnObservedAt ?? '');
403
- const spawnDispatchedAt = Date.parse(receipt.spawnDispatchedAt);
404
- const activationDeadlineAt = Date.parse(receipt.activationDeadlineAt);
405
- if (!expected
406
- || event.hostEvidence.protocolVersion !== expected.protocolVersion
407
- || event.hostEvidence.hookDefinitionHash !== expected.hookDefinitionHash
408
- || event.hostEvidence.hookSource !== expected.hookSource
409
- || event.hostEvidence.hookSourcePathDigest !== expected.hookSourcePathDigest
410
- || event.hostEvidence.hookSourceHash !== expected.hookSourceHash
411
- || event.hostEvidence.capabilitySessionId !== expected.capabilitySessionId
412
- || event.hostEvidence.capabilityTurnId !== expected.capabilityTurnId
413
- || !event.hostEvidence.spawnToolUseId?.trim()
414
- || event.hostEvidence.spawnToolUseId === expected.capabilityToolUseId
415
- || !Number.isFinite(spawnObservedAt)
416
- || spawnObservedAt < spawnDispatchedAt
417
- || spawnObservedAt > activationDeadlineAt
418
- || event.parentId !== expected.capabilitySessionId
419
- || (event.hostEvidence.controllerInstanceDigest ?? null) !== expected.controllerInstanceDigest
420
- || (event.hostEvidence.controlGeneration ?? null) !== expected.controlGeneration
421
- || !validCodexBuildMetadata(event.hostEvidence))
422
- return fail('DELEGATION_HOST_EVIDENCE_INVALID', 'Codex lifecycle provenance does not match the prepared receipt');
358
+ if (event.hostEvidence?.kind === 'codex-lifecycle-v2'
359
+ && event.hostEvidence.capabilitySessionId !== event.parentId) {
360
+ return fail('DELEGATION_HOST_EVIDENCE_INVALID', 'Codex start evidence does not match the observed parent');
423
361
  }
424
362
  return { ok: true, receipt: Object.freeze({
425
363
  ...receipt,
@@ -467,7 +405,7 @@ function abortActivatedDelegation(receipt, event) {
467
405
  if (event.childId !== receipt.childId
468
406
  || !Number.isSafeInteger(event.stopRevision)
469
407
  || event.stopRevision <= receipt.hostEvidence.startRevision
470
- || event.consumer !== `lifecycle-recovery:${receipt.taskId}:${receipt.id}`
408
+ || event.consumer !== receipt.id
471
409
  || !exactText(event.consumedAt)) {
472
410
  return fail('DELEGATION_RECOVERY_EVIDENCE_INVALID', 'recovery stop evidence does not match the activated delegation');
473
411
  }
@@ -17,9 +17,7 @@ import { commitOrchestrationStageCompletion } from './orchestration.js';
17
17
  import { TaskExecutionLockError, withTaskExecutionLock } from './task-execution-lock.js';
18
18
  import { captureTaskWriteMetadata, writeTask } from './write.js';
19
19
  import { allowsManualOverride } from './guard-override.js';
20
- import { consumeLocalArtifactFinalizationIntent, readLocalArtifactFinalizationIntent, validateLocalArtifact } from './local-artifact-finalization.js';
21
- import { writeArtifactRecoveryIntent, readArtifactRecoveryIntent } from './artifact-repair-intent.js';
22
- import { reconcileArtifactRecovery, recoveryContextFromIntent, sha256Content } from './artifact-recovery.js';
20
+ import { validateLocalArtifact } from './local-artifact-finalization.js';
23
21
  import { buildLifecycleFacts, canStart } from './capabilities.js';
24
22
  import { createInvalidationOperation, invalidationMutation, parseInvalidationDocument, targetIdFor, upsertInvalidation } from './invalidation.js';
25
23
  import { reconcileTaskInvalidation } from './invalidation-command.js';
@@ -339,57 +337,6 @@ function openStartedIdentity(rows, family) {
339
337
  function reviewInputFamily(family) {
340
338
  return family === 'review-analysis' ? 'analysis' : family === 'review-plan' ? 'plan' : 'code';
341
339
  }
342
- function reconcileReviewCompletionReplay(repoRoot, taskDir, taskId, family, artifact, round) {
343
- const stage = REVIEW_LEDGER_STAGES[family];
344
- if (!stage)
345
- return null;
346
- const reviewFamily = family;
347
- try {
348
- let intent = readArtifactRecoveryIntent(repoRoot, taskId, reviewFamily, artifact);
349
- if (!intent)
350
- return null;
351
- if (intent.state === 'commit-started') {
352
- const reconciled = reconcileArtifactRecovery(recoveryContextFromIntent(repoRoot, taskDir, intent), { lockAlreadyHeld: true });
353
- if (reconciled.status !== 'passed' && reconciled.status !== 'consumed') {
354
- return {
355
- code: 'EVENT_ARTIFACT_CONFLICT',
356
- message: `review finalizer recovery remains ${reconciled.status} for ${artifact}`
357
- };
358
- }
359
- intent = reconciled.intent;
360
- }
361
- if (!['passed', 'consumption-started', 'consumed'].includes(intent.state)) {
362
- return {
363
- code: 'EVENT_ARTIFACT_CONFLICT',
364
- message: `review finalizer provenance is incomplete for ${artifact}`
365
- };
366
- }
367
- const validated = validateCompletedArtifact(taskDir, FAMILY[family].artifact, artifact, round);
368
- if (!validated.ok) {
369
- return { code: 'EVENT_ARTIFACT_CONFLICT', message: validated.error.message };
370
- }
371
- const content = fs.readFileSync(validated.artifact.path, 'utf8');
372
- if (intent.round !== round
373
- || intent.finalArtifactSha256 !== sha256Content(content)
374
- || intent.finalSemanticDigest !== canonicalSemanticDigest(content)) {
375
- return {
376
- code: 'EVENT_ARTIFACT_CONFLICT',
377
- message: `review finalizer provenance does not match ${artifact}`
378
- };
379
- }
380
- if (intent.state === 'passed' || intent.state === 'consumption-started') {
381
- const consumed = { ...intent, state: 'consumed', updatedAt: Date.now() };
382
- writeArtifactRecoveryIntent(repoRoot, consumed, { expected: intent });
383
- }
384
- return null;
385
- }
386
- catch (error) {
387
- return {
388
- code: 'EVENT_ARTIFACT_CONFLICT',
389
- message: `review finalizer recovery could not be reconciled for ${artifact}: ${error instanceof Error ? error.message : String(error)}`
390
- };
391
- }
392
- }
393
340
  function relationForStartedInput(family, input) {
394
341
  if (family.startsWith('review-')) {
395
342
  return input.family === reviewInputFamily(family) ? 'reviewed-input' : 'approval-context';
@@ -615,10 +562,20 @@ function reworkIntentMutationForCompletion(content, taskDir, family, artifact, t
615
562
  }
616
563
  return { mutation: JSON.stringify(next) === JSON.stringify(parsed.intents) ? null : reworkIntentMutation(content, next) };
617
564
  }
618
- function buildCompletionReceipt(taskDir, family, artifact, completedAt, frontmatter) {
565
+ function buildCompletionReceipt(content, taskDir, family, artifact, completedAt, frontmatter) {
566
+ const existing = (() => {
567
+ try {
568
+ return parseArtifactReceipts(content).rows.find((receipt) => receipt.output === artifact.name) ?? null;
569
+ }
570
+ catch {
571
+ return null;
572
+ }
573
+ })();
619
574
  if (family === 'code') {
620
- const startedInput = typeof frontmatter.code_input_artifact === 'string' ? frontmatter.code_input_artifact : '';
621
- const startedSha256 = typeof frontmatter.code_input_sha256 === 'string' ? frontmatter.code_input_sha256 : '';
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 ?? '';
622
579
  if (!startedInput || !startedSha256)
623
580
  return { ok: false, message: 'code.started plan input context is missing' };
624
581
  const plan = inspectArtifactDirectory(taskDir, 'plan');
@@ -633,7 +590,7 @@ function buildCompletionReceipt(taskDir, family, artifact, completedAt, frontmat
633
590
  ok: true,
634
591
  receipt: {
635
592
  event: 'code.completed', output: artifact.name, input: startedInput,
636
- inputSha256, completedAt
593
+ inputSha256, completedAt: existing?.completedAt ?? completedAt
637
594
  }
638
595
  };
639
596
  }
@@ -644,8 +601,10 @@ function buildCompletionReceipt(taskDir, family, artifact, completedAt, frontmat
644
601
  if (!family.startsWith('review-'))
645
602
  return null;
646
603
  const expectedFamily = reviewInputFamily(family);
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
610
  const current = inspectArtifactDirectory(taskDir, expectedFamily);
@@ -666,7 +625,7 @@ function buildCompletionReceipt(taskDir, family, artifact, completedAt, frontmat
666
625
  ok: true,
667
626
  receipt: {
668
627
  event, output: artifact.name, input: startedInput,
669
- inputSha256, completedAt
628
+ inputSha256, completedAt: existing?.completedAt ?? completedAt
670
629
  }
671
630
  };
672
631
  }
@@ -674,6 +633,57 @@ function buildCompletionReceipt(taskDir, family, artifact, completedAt, frontmat
674
633
  return { ok: false, message: `cannot hash review input ${startedInput}: ${error instanceof Error ? error.message : String(error)}` };
675
634
  }
676
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 [];
653
+ }
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
+ }
677
687
  function applyTaskEventUnlocked(request, options = {}) {
678
688
  const invalid = validateTaskEventRequest(request);
679
689
  if (invalid)
@@ -777,14 +787,12 @@ function applyTaskEventUnlocked(request, options = {}) {
777
787
  const currentStep = typeof frontmatter.current_step === 'string' ? frontmatter.current_step : '';
778
788
  const matchingRows = rows.filter((item) => item.step === eventIdentity.action);
779
789
  const manual = eventIdentity.family === 'manual-validation';
780
- if (!manual && matchingRows.length > 1)
781
- 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 });
782
790
  const openRows = matchingRows.filter((item) => item.started && !item.done);
783
791
  const completedRows = matchingRows.filter((item) => item.done);
784
- 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);
785
795
  let completedArtifact = null;
786
- let localFinalizationIntent = null;
787
- let reviewFinalizationIntent = null;
788
796
  let reviewContent = null;
789
797
  if (eventIdentity.phase === 'started' && row) {
790
798
  if (row.agent !== normalized.agent)
@@ -797,21 +805,8 @@ function applyTaskEventUnlocked(request, options = {}) {
797
805
  }
798
806
  return successNoOp(normalized, resolved.taskId, resolved.taskMdPath, currentStep, eventIdentity, row.started, frontmatter, artifactContext);
799
807
  }
800
- if (eventIdentity.phase === 'started' && !manual && completedRows.length > 0)
801
- return failed(normalized, { code: 'EVENT_ALREADY_COMPLETED', message: 'event identity is already completed' }, { taskId: resolved.taskId, taskMdPath: resolved.taskMdPath });
802
- const done = completedRows.find((item) => item.note === eventIdentity.note);
803
- if (eventIdentity.phase === 'completed' && done) {
804
- if (!normalized.dryRun && eventIdentity.family.startsWith('review-') && normalized.artifact && normalized.round) {
805
- const recoveryError = reconcileReviewCompletionReplay(resolved.repoRoot, resolved.taskDir, resolved.taskId, eventIdentity.family, normalized.artifact, normalized.round);
806
- if (recoveryError)
807
- return failed(normalized, recoveryError, { taskId: resolved.taskId, taskMdPath: resolved.taskMdPath, fromStep: currentStep, toStep: currentStep, action: eventIdentity.action, phase: eventIdentity.phase });
808
- }
809
- return successNoOp(normalized, resolved.taskId, resolved.taskMdPath, currentStep, eventIdentity, done.done, frontmatter, artifactContext);
810
- }
811
- if (eventIdentity.phase === 'completed' && completedRows.length > 0 && openRows.length === 0)
812
- 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 });
813
- if (eventIdentity.phase === 'completed' && !row?.started && !allowsManualOverride(options.manualOverride, 'task-event', 'EVENT_START_MISSING'))
814
- 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 });
815
810
  if (eventIdentity.phase === 'completed') {
816
811
  const validated = validateCompletedArtifact(resolved.taskDir, FAMILY[eventIdentity.family].artifact, normalized.artifact, normalized.round);
817
812
  if (!validated.ok)
@@ -851,20 +846,6 @@ function applyTaskEventUnlocked(request, options = {}) {
851
846
  const localFamily = eventIdentity.family === 'analyze'
852
847
  ? 'analysis'
853
848
  : eventIdentity.family === 'plan' ? 'plan' : 'code';
854
- try {
855
- localFinalizationIntent = readLocalArtifactFinalizationIntent(resolved.repoRoot, resolved.taskId, localFamily, completedArtifact.name);
856
- if (localFinalizationIntent?.state === 'commit-started') {
857
- const reconciled = reconcileArtifactRecovery(recoveryContextFromIntent(resolved.repoRoot, resolved.taskDir, localFinalizationIntent), { lockAlreadyHeld: true });
858
- if (reconciled.status === 'passed' || reconciled.status === 'consumed')
859
- localFinalizationIntent = reconciled.intent;
860
- }
861
- }
862
- catch (error) {
863
- return failed(normalized, {
864
- code: 'EVENT_ARTIFACT_CONFLICT',
865
- message: `local finalizer recovery could not be reconciled for ${completedArtifact.name}: ${error instanceof Error ? error.message : String(error)}`
866
- }, { taskId: resolved.taskId, taskMdPath: resolved.taskMdPath, fromStep: currentStep, toStep: currentStep, action: eventIdentity.action, phase: eventIdentity.phase });
867
- }
868
849
  let artifactContent;
869
850
  try {
870
851
  artifactContent = fs.readFileSync(completedArtifact.path, 'utf8');
@@ -896,29 +877,6 @@ function applyTaskEventUnlocked(request, options = {}) {
896
877
  message: `semantic digest does not match finalizer result for ${completedArtifact.name}`
897
878
  }, { taskId: resolved.taskId, taskMdPath: resolved.taskMdPath, fromStep: currentStep, toStep: currentStep, action: eventIdentity.action, phase: eventIdentity.phase });
898
879
  }
899
- if (!localFinalizationIntent || !['passed', 'consumed'].includes(localFinalizationIntent.state)) {
900
- return failed(normalized, {
901
- code: 'EVENT_ARTIFACT_CONFLICT',
902
- message: `local finalizer provenance is missing or incomplete for ${completedArtifact.name}`
903
- }, { taskId: resolved.taskId, taskMdPath: resolved.taskMdPath, fromStep: currentStep, toStep: currentStep, action: eventIdentity.action, phase: eventIdentity.phase });
904
- }
905
- if (localFinalizationIntent.finalArtifactSha256 !== actualSha256 || localFinalizationIntent.finalSemanticDigest !== local.semanticDigest) {
906
- return failed(normalized, {
907
- code: 'EVENT_ARTIFACT_CONFLICT',
908
- message: `local finalizer provenance does not match ${completedArtifact.name}`
909
- }, { taskId: resolved.taskId, taskMdPath: resolved.taskMdPath, fromStep: currentStep, toStep: currentStep, action: eventIdentity.action, phase: eventIdentity.phase });
910
- }
911
- if (localFinalizationIntent.authorityDigest !== null) {
912
- if (!lifecycleAuthority
913
- || localFinalizationIntent.recoveryOperationId !== lifecycleAuthority.operationId
914
- || localFinalizationIntent.requestId !== lifecycleAuthority.lifecycleRequestId
915
- || localFinalizationIntent.authorityDigest !== lifecycleRecoveryAttestationDigest(lifecycleAuthority)) {
916
- return failed(normalized, {
917
- code: 'EVENT_ARTIFACT_CONFLICT',
918
- message: `local finalizer provenance does not match lifecycle completion authority for ${completedArtifact.name}`
919
- }, { taskId: resolved.taskId, taskMdPath: resolved.taskMdPath, fromStep: currentStep, toStep: currentStep, action: eventIdentity.action, phase: eventIdentity.phase, artifactContext });
920
- }
921
- }
922
880
  }
923
881
  }
924
882
  if (eventIdentity.phase === 'waiting' && section.entries.some((entry) => entry.step === eventIdentity.action && entry.note === eventIdentity.note)) {
@@ -942,38 +900,6 @@ function applyTaskEventUnlocked(request, options = {}) {
942
900
  const findingCountError = validateReviewFindingCounts(normalized, content, eventIdentity.family, completedArtifact?.path ?? null);
943
901
  if (findingCountError)
944
902
  return failed(normalized, findingCountError, { taskId: resolved.taskId, taskMdPath: resolved.taskMdPath, fromStep: currentStep, toStep: currentStep, action: eventIdentity.action, phase: eventIdentity.phase });
945
- if (eventIdentity.phase === 'completed' && eventIdentity.family.startsWith('review-') && completedArtifact && reviewContent !== null) {
946
- try {
947
- reviewFinalizationIntent = readArtifactRecoveryIntent(resolved.repoRoot, resolved.taskId, eventIdentity.family, completedArtifact.name);
948
- if (!normalized.dryRun && reviewFinalizationIntent?.state === 'commit-started') {
949
- const reconciled = reconcileArtifactRecovery(recoveryContextFromIntent(resolved.repoRoot, resolved.taskDir, reviewFinalizationIntent), { lockAlreadyHeld: true });
950
- if (reconciled.status === 'passed' || reconciled.status === 'consumed')
951
- reviewFinalizationIntent = reconciled.intent;
952
- }
953
- if (!reviewFinalizationIntent || !['passed', 'consumption-started', 'consumed'].includes(reviewFinalizationIntent.state)) {
954
- return failed(normalized, {
955
- code: 'EVENT_ARTIFACT_CONFLICT',
956
- message: `review finalizer provenance is missing or incomplete for ${completedArtifact.name}`
957
- }, { taskId: resolved.taskId, taskMdPath: resolved.taskMdPath, fromStep: currentStep, toStep: currentStep, action: eventIdentity.action, phase: eventIdentity.phase });
958
- }
959
- const actualSha256 = sha256File(completedArtifact.path);
960
- const actualSemanticDigest = canonicalSemanticDigest(reviewContent);
961
- if (reviewFinalizationIntent.round !== normalized.round
962
- || reviewFinalizationIntent.finalArtifactSha256 !== actualSha256
963
- || reviewFinalizationIntent.finalSemanticDigest !== actualSemanticDigest) {
964
- return failed(normalized, {
965
- code: 'EVENT_ARTIFACT_CONFLICT',
966
- message: `review finalizer provenance does not match ${completedArtifact.name}`
967
- }, { taskId: resolved.taskId, taskMdPath: resolved.taskMdPath, fromStep: currentStep, toStep: currentStep, action: eventIdentity.action, phase: eventIdentity.phase });
968
- }
969
- }
970
- catch (error) {
971
- return failed(normalized, {
972
- code: 'EVENT_ARTIFACT_CONFLICT',
973
- message: `review finalizer recovery could not be reconciled for ${completedArtifact.name}: ${error instanceof Error ? error.message : String(error)}`
974
- }, { taskId: resolved.taskId, taskMdPath: resolved.taskMdPath, fromStep: currentStep, toStep: currentStep, action: eventIdentity.action, phase: eventIdentity.phase });
975
- }
976
- }
977
903
  let orchestrationCompletion = null;
978
904
  if (eventIdentity.phase === 'completed' && eventIdentity.family !== 'manual-validation' && eventIdentity.family !== 'validation-run') {
979
905
  const orchestrationStage = eventIdentity.family === 'analyze' ? 'analysis' : eventIdentity.family;
@@ -1004,14 +930,24 @@ function applyTaskEventUnlocked(request, options = {}) {
1004
930
  return failed(normalized, { code: 'METADATA_CAPTURE_FAILED', message: String(error) }, { taskId: resolved.taskId, taskMdPath: resolved.taskMdPath });
1005
931
  }
1006
932
  let completionReceipt = null;
933
+ let currentFact = null;
1007
934
  let completionInvalidation = null;
1008
935
  let completionRework = null;
1009
936
  if (eventIdentity.phase === 'completed' && completedArtifact) {
1010
- const receipt = buildCompletionReceipt(resolved.taskDir, eventIdentity.family, completedArtifact, metadata.timestamp, frontmatter);
937
+ const receipt = buildCompletionReceipt(content, resolved.taskDir, eventIdentity.family, completedArtifact, metadata.timestamp, frontmatter);
1011
938
  if (receipt && !receipt.ok) {
1012
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 });
1013
940
  }
1014
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
+ }
1015
951
  try {
1016
952
  const invalidation = invalidationMutationForCompletion(content, resolved.taskDir, eventIdentity.family, completedArtifact, metadata.timestamp);
1017
953
  if ('error' in invalidation)
@@ -1030,6 +966,8 @@ function applyTaskEventUnlocked(request, options = {}) {
1030
966
  const logStep = eventIdentity.phase === 'started' ? `${eventIdentity.action} [started]` : eventIdentity.action;
1031
967
  const body = appendActivityEntry(section, { time: metadata.timestamp, step: logStep, agent: normalized.agent, note: eventIdentity.note });
1032
968
  const frontmatterSet = { current_step: step, assigned_to: normalized.agent };
969
+ if (currentFact)
970
+ frontmatterSet.completion_facts = JSON.stringify(replaceCompletionFact(completionFacts(frontmatter), currentFact));
1033
971
  let frontmatterRemove;
1034
972
  if (eventIdentity.phase === 'started' && artifactContext) {
1035
973
  try {
@@ -1117,97 +1055,11 @@ function applyTaskEventUnlocked(request, options = {}) {
1117
1055
  });
1118
1056
  }
1119
1057
  mutations.push({ kind: 'section', aliases: ['活动日志', 'Activity Log'], heading: section.heading, body });
1120
- if (!normalized.dryRun && localFinalizationIntent && completedArtifact) {
1121
- try {
1122
- if (lifecycleAuthority && localFinalizationIntent.state === 'passed') {
1123
- const expectedIntent = localFinalizationIntent;
1124
- localFinalizationIntent = {
1125
- ...localFinalizationIntent,
1126
- state: 'consumption-started',
1127
- phase: 'task-event.completed',
1128
- authorityDigest: lifecycleRecoveryAttestationDigest(lifecycleAuthority),
1129
- requestId: lifecycleAuthority.lifecycleRequestId,
1130
- updatedAt: Date.now()
1131
- };
1132
- writeArtifactRecoveryIntent(resolved.repoRoot, localFinalizationIntent, { expected: expectedIntent });
1133
- }
1134
- else if (!lifecycleAuthority && localFinalizationIntent.state === 'passed') {
1135
- localFinalizationIntent = consumeLocalArtifactFinalizationIntent(resolved.repoRoot, localFinalizationIntent, { lockAlreadyHeld: options.lockAlreadyHeld });
1136
- }
1137
- }
1138
- catch (error) {
1139
- return failed(normalized, {
1140
- code: 'EVENT_ARTIFACT_CONFLICT',
1141
- message: `local finalizer provenance could not be advanced before task write for ${completedArtifact.name}: ${error instanceof Error ? error.message : String(error)}`
1142
- }, {
1143
- taskId: resolved.taskId,
1144
- taskMdPath: resolved.taskMdPath,
1145
- fromStep: currentStep,
1146
- toStep: step,
1147
- action: eventIdentity.action,
1148
- phase: eventIdentity.phase,
1149
- artifactContext
1150
- });
1151
- }
1152
- }
1153
- if (!normalized.dryRun && reviewFinalizationIntent && completedArtifact && reviewFinalizationIntent.state === 'passed') {
1154
- try {
1155
- const expectedIntent = reviewFinalizationIntent;
1156
- reviewFinalizationIntent = {
1157
- ...reviewFinalizationIntent,
1158
- state: 'consumption-started',
1159
- phase: 'task-event.completed',
1160
- updatedAt: Date.now()
1161
- };
1162
- writeArtifactRecoveryIntent(resolved.repoRoot, reviewFinalizationIntent, { expected: expectedIntent });
1163
- }
1164
- catch (error) {
1165
- return failed(normalized, {
1166
- code: 'EVENT_ARTIFACT_CONFLICT',
1167
- message: `review finalizer provenance could not be advanced before task write for ${completedArtifact.name}: ${error instanceof Error ? error.message : String(error)}`
1168
- }, {
1169
- taskId: resolved.taskId,
1170
- taskMdPath: resolved.taskMdPath,
1171
- fromStep: currentStep,
1172
- toStep: step,
1173
- action: eventIdentity.action,
1174
- phase: eventIdentity.phase,
1175
- artifactContext
1176
- });
1177
- }
1178
- }
1179
1058
  const sourceCompletion = eventIdentity.phase === 'completed'
1180
1059
  && ['analyze', 'plan', 'code'].includes(eventIdentity.family);
1181
1060
  const result = writeTask({ taskRef: normalized.taskRef, expectedState: stateOverride ? resolved.state : 'active', dryRun: normalized.dryRun, mutations }, { ...options, invalidationContext: sourceCompletion ? 'source-completion' : 'standard', metadataProvider: () => metadata });
1182
1061
  if (result.status === 'failed')
1183
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 });
1184
- if (!normalized.dryRun && lifecycleAuthority && localFinalizationIntent && completedArtifact) {
1185
- try {
1186
- if (!options.deferLifecycleRecoveryConsumption)
1187
- consumeLifecycleRecoveryAttestation(lifecycleAuthority);
1188
- writeArtifactRecoveryIntent(resolved.repoRoot, { ...localFinalizationIntent, state: 'consumed', updatedAt: Date.now() }, { expected: localFinalizationIntent });
1189
- }
1190
- catch (error) {
1191
- return failed(normalized, {
1192
- code: 'EVENT_ARTIFACT_CONFLICT',
1193
- message: `task completion was written but lifecycle recovery consumption is incomplete: ${error instanceof Error ? error.message : String(error)}`
1194
- }, { taskId: result.taskId, taskMdPath: result.taskMdPath, fromStep: currentStep, toStep: step, action: eventIdentity.action, phase: eventIdentity.phase, timestamp: result.timestamp, agentInfraVersion: result.agentInfraVersion, operations: result.operations, artifactContext });
1195
- }
1196
- }
1197
- if (!normalized.dryRun && reviewFinalizationIntent && completedArtifact) {
1198
- try {
1199
- if (reviewFinalizationIntent.state === 'consumption-started') {
1200
- const consumed = { ...reviewFinalizationIntent, state: 'consumed', updatedAt: Date.now() };
1201
- writeArtifactRecoveryIntent(resolved.repoRoot, consumed, { expected: reviewFinalizationIntent });
1202
- }
1203
- }
1204
- catch (error) {
1205
- return failed(normalized, {
1206
- code: 'EVENT_ARTIFACT_CONFLICT',
1207
- message: `review finalizer recovery consumption is incomplete: ${error instanceof Error ? error.message : String(error)}`
1208
- }, { 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 });
1209
- }
1210
- }
1211
1063
  if (!normalized.dryRun && orchestrationCompletion) {
1212
1064
  try {
1213
1065
  (options.commitOrchestrationCompletion ?? commitOrchestrationStageCompletion)(orchestrationCompletion);
@@ -5,9 +5,7 @@ const COMPLETION_BACKFILL_FAMILIES = [
5
5
  'plan',
6
6
  'review-plan',
7
7
  'code',
8
- 'review-code',
9
- 'manual-validation',
10
- 'validation-run'
8
+ 'review-code'
11
9
  ];
12
10
  function inspectCompletionArtifacts(taskRef, options = {}) {
13
11
  const artifacts = [];
@@ -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.warningProjection !== 'done')
4
5
  return 'pending';
5
6
  if (receipt.lastError !== null || receipt.warnings.some((warning) => warning.status === 'open'))
6
7
  return 'unresolved';