engineering-platform 2.3.68__tar.gz → 2.3.71__tar.gz

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 (163) hide show
  1. {engineering_platform-2.3.68 → engineering_platform-2.3.71}/PKG-INFO +1 -1
  2. {engineering_platform-2.3.68 → engineering_platform-2.3.71}/pyproject.toml +2 -1
  3. {engineering_platform-2.3.68 → engineering_platform-2.3.71}/src/engineering_platform/ENGINEERING_PLATFORM_CONFIG.json +1 -1
  4. {engineering_platform-2.3.68 → engineering_platform-2.3.71}/src/engineering_platform/ENGINEERING_PLATFORM_VERSION.json +5 -5
  5. {engineering_platform-2.3.68 → engineering_platform-2.3.71}/src/engineering_platform/assets/dashboard.js +18 -0
  6. {engineering_platform-2.3.68 → engineering_platform-2.3.71}/src/engineering_platform/assets/dashboard_locales.mjs +10 -0
  7. {engineering_platform-2.3.68 → engineering_platform-2.3.71}/src/engineering_platform/contracts/projection.py +7 -0
  8. {engineering_platform-2.3.68 → engineering_platform-2.3.71}/src/engineering_platform/execution_finalization.py +4 -1
  9. {engineering_platform-2.3.68 → engineering_platform-2.3.71}/src/engineering_platform/execution_host.py +150 -52
  10. {engineering_platform-2.3.68 → engineering_platform-2.3.71}/src/engineering_platform/execution_lifecycle.py +3 -2
  11. {engineering_platform-2.3.68 → engineering_platform-2.3.71}/src/engineering_platform/execution_reporting.py +4 -1
  12. {engineering_platform-2.3.68 → engineering_platform-2.3.71}/src/engineering_platform/execution_repository.py +13 -3
  13. {engineering_platform-2.3.68 → engineering_platform-2.3.71}/src/engineering_platform/managed_autonomy.py +21 -4
  14. {engineering_platform-2.3.68 → engineering_platform-2.3.71}/src/engineering_platform/platform_version.py +1 -1
  15. {engineering_platform-2.3.68 → engineering_platform-2.3.71}/src/engineering_platform/qualification_runtime.py +71 -17
  16. engineering_platform-2.3.71/src/engineering_platform/reconciliation_adoption.py +186 -0
  17. {engineering_platform-2.3.68 → engineering_platform-2.3.71}/src/engineering_platform/server.py +21 -1
  18. {engineering_platform-2.3.68 → engineering_platform-2.3.71}/src/engineering_platform/storage.py +54 -1
  19. {engineering_platform-2.3.68 → engineering_platform-2.3.71}/src/engineering_platform/templates/workspace-config.json +1 -1
  20. {engineering_platform-2.3.68 → engineering_platform-2.3.71}/src/engineering_platform.egg-info/PKG-INFO +1 -1
  21. {engineering_platform-2.3.68 → engineering_platform-2.3.71}/src/engineering_platform.egg-info/SOURCES.txt +1 -0
  22. {engineering_platform-2.3.68 → engineering_platform-2.3.71}/src/engineering_platform.egg-info/entry_points.txt +1 -0
  23. {engineering_platform-2.3.68 → engineering_platform-2.3.71}/README.md +0 -0
  24. {engineering_platform-2.3.68 → engineering_platform-2.3.71}/setup.cfg +0 -0
  25. {engineering_platform-2.3.68 → engineering_platform-2.3.71}/src/engineering_platform/__init__.py +0 -0
  26. {engineering_platform-2.3.68 → engineering_platform-2.3.71}/src/engineering_platform/__main__.py +0 -0
  27. {engineering_platform-2.3.68 → engineering_platform-2.3.71}/src/engineering_platform/agent_state.py +0 -0
  28. {engineering_platform-2.3.68 → engineering_platform-2.3.71}/src/engineering_platform/agent_trust.py +0 -0
  29. {engineering_platform-2.3.68 → engineering_platform-2.3.71}/src/engineering_platform/assets/dashboard.css +0 -0
  30. {engineering_platform-2.3.68 → engineering_platform-2.3.71}/src/engineering_platform/assets/dashboard_status_store.mjs +0 -0
  31. {engineering_platform-2.3.68 → engineering_platform-2.3.71}/src/engineering_platform/assets/dashboard_translation.mjs +0 -0
  32. {engineering_platform-2.3.68 → engineering_platform-2.3.71}/src/engineering_platform/assets/operations-console/apple-touch-icon-dark.png +0 -0
  33. {engineering_platform-2.3.68 → engineering_platform-2.3.71}/src/engineering_platform/assets/operations-console/apple-touch-icon-light.png +0 -0
  34. {engineering_platform-2.3.68 → engineering_platform-2.3.71}/src/engineering_platform/assets/operations-console/icon-dark.png +0 -0
  35. {engineering_platform-2.3.68 → engineering_platform-2.3.71}/src/engineering_platform/assets/operations-console/icon-light.png +0 -0
  36. {engineering_platform-2.3.68 → engineering_platform-2.3.71}/src/engineering_platform/assets/operations-console/icon-transparent.png +0 -0
  37. {engineering_platform-2.3.68 → engineering_platform-2.3.71}/src/engineering_platform/assets/operations-console/manifest.webmanifest +0 -0
  38. {engineering_platform-2.3.68 → engineering_platform-2.3.71}/src/engineering_platform/capability_preflight.py +0 -0
  39. {engineering_platform-2.3.68 → engineering_platform-2.3.71}/src/engineering_platform/capability_review.py +0 -0
  40. {engineering_platform-2.3.68 → engineering_platform-2.3.71}/src/engineering_platform/central_data_transfer.py +0 -0
  41. {engineering_platform-2.3.68 → engineering_platform-2.3.71}/src/engineering_platform/central_database.py +0 -0
  42. {engineering_platform-2.3.68 → engineering_platform-2.3.71}/src/engineering_platform/central_store_migration.py +0 -0
  43. {engineering_platform-2.3.68 → engineering_platform-2.3.71}/src/engineering_platform/codex_capacity.py +0 -0
  44. {engineering_platform-2.3.68 → engineering_platform-2.3.71}/src/engineering_platform/codex_chat.py +0 -0
  45. {engineering_platform-2.3.68 → engineering_platform-2.3.71}/src/engineering_platform/codex_observability.py +0 -0
  46. {engineering_platform-2.3.68 → engineering_platform-2.3.71}/src/engineering_platform/component_lock.py +0 -0
  47. {engineering_platform-2.3.68 → engineering_platform-2.3.71}/src/engineering_platform/component_logging.py +0 -0
  48. {engineering_platform-2.3.68 → engineering_platform-2.3.71}/src/engineering_platform/console_presentation.py +0 -0
  49. {engineering_platform-2.3.68 → engineering_platform-2.3.71}/src/engineering_platform/console_route_ownership.py +0 -0
  50. {engineering_platform-2.3.68 → engineering_platform-2.3.71}/src/engineering_platform/contracts/__init__.py +0 -0
  51. {engineering_platform-2.3.68 → engineering_platform-2.3.71}/src/engineering_platform/contracts/ep_consumer.py +0 -0
  52. {engineering_platform-2.3.68 → engineering_platform-2.3.71}/src/engineering_platform/contracts/models.py +0 -0
  53. {engineering_platform-2.3.68 → engineering_platform-2.3.71}/src/engineering_platform/dashboard_browser_validation.py +0 -0
  54. {engineering_platform-2.3.68 → engineering_platform-2.3.71}/src/engineering_platform/dashboard_state.py +0 -0
  55. {engineering_platform-2.3.68 → engineering_platform-2.3.71}/src/engineering_platform/dashboard_supervisor.swift +0 -0
  56. {engineering_platform-2.3.68 → engineering_platform-2.3.71}/src/engineering_platform/dashboard_translation.py +0 -0
  57. {engineering_platform-2.3.68 → engineering_platform-2.3.71}/src/engineering_platform/dependabot_producer.py +0 -0
  58. {engineering_platform-2.3.68 → engineering_platform-2.3.71}/src/engineering_platform/development_profile.py +0 -0
  59. {engineering_platform-2.3.68 → engineering_platform-2.3.71}/src/engineering_platform/drift_diagnostics.py +0 -0
  60. {engineering_platform-2.3.68 → engineering_platform-2.3.71}/src/engineering_platform/emergency_recovery.py +0 -0
  61. {engineering_platform-2.3.68 → engineering_platform-2.3.71}/src/engineering_platform/engineering_memory.py +0 -0
  62. {engineering_platform-2.3.68 → engineering_platform-2.3.71}/src/engineering_platform/ep_consumer_credentials.py +0 -0
  63. {engineering_platform-2.3.68 → engineering_platform-2.3.71}/src/engineering_platform/evidence_projection.py +0 -0
  64. {engineering_platform-2.3.68 → engineering_platform-2.3.71}/src/engineering_platform/execution_activity.py +0 -0
  65. {engineering_platform-2.3.68 → engineering_platform-2.3.71}/src/engineering_platform/execution_context.py +0 -0
  66. {engineering_platform-2.3.68 → engineering_platform-2.3.71}/src/engineering_platform/execution_errors.py +0 -0
  67. {engineering_platform-2.3.68 → engineering_platform-2.3.71}/src/engineering_platform/execution_evidence.py +0 -0
  68. {engineering_platform-2.3.68 → engineering_platform-2.3.71}/src/engineering_platform/execution_executor.py +0 -0
  69. {engineering_platform-2.3.68 → engineering_platform-2.3.71}/src/engineering_platform/execution_host_evidence.py +0 -0
  70. {engineering_platform-2.3.68 → engineering_platform-2.3.71}/src/engineering_platform/execution_lease.py +0 -0
  71. {engineering_platform-2.3.68 → engineering_platform-2.3.71}/src/engineering_platform/execution_models.py +0 -0
  72. {engineering_platform-2.3.68 → engineering_platform-2.3.71}/src/engineering_platform/execution_readiness.py +0 -0
  73. {engineering_platform-2.3.68 → engineering_platform-2.3.71}/src/engineering_platform/execution_timeout_policy.py +0 -0
  74. {engineering_platform-2.3.68 → engineering_platform-2.3.71}/src/engineering_platform/execution_timing.py +0 -0
  75. {engineering_platform-2.3.68 → engineering_platform-2.3.71}/src/engineering_platform/execution_transaction.py +0 -0
  76. {engineering_platform-2.3.68 → engineering_platform-2.3.71}/src/engineering_platform/external_producer_binding.py +0 -0
  77. {engineering_platform-2.3.68 → engineering_platform-2.3.71}/src/engineering_platform/file_inbox.py +0 -0
  78. {engineering_platform-2.3.68 → engineering_platform-2.3.71}/src/engineering_platform/forensic_attribution.py +0 -0
  79. {engineering_platform-2.3.68 → engineering_platform-2.3.71}/src/engineering_platform/forensic_attribution_v2.py +0 -0
  80. {engineering_platform-2.3.68 → engineering_platform-2.3.71}/src/engineering_platform/forensic_delta.py +0 -0
  81. {engineering_platform-2.3.68 → engineering_platform-2.3.71}/src/engineering_platform/golden_scenario.py +0 -0
  82. {engineering_platform-2.3.68 → engineering_platform-2.3.71}/src/engineering_platform/historical_dashboard_configuration.py +0 -0
  83. {engineering_platform-2.3.68 → engineering_platform-2.3.71}/src/engineering_platform/host_admin.py +0 -0
  84. {engineering_platform-2.3.68 → engineering_platform-2.3.71}/src/engineering_platform/host_preflight.py +0 -0
  85. {engineering_platform-2.3.68 → engineering_platform-2.3.71}/src/engineering_platform/installation_relocation.py +0 -0
  86. {engineering_platform-2.3.68 → engineering_platform-2.3.71}/src/engineering_platform/installation_update_activation.py +0 -0
  87. {engineering_platform-2.3.68 → engineering_platform-2.3.71}/src/engineering_platform/installation_update_admission.py +0 -0
  88. {engineering_platform-2.3.68 → engineering_platform-2.3.71}/src/engineering_platform/installation_update_backup.py +0 -0
  89. {engineering_platform-2.3.68 → engineering_platform-2.3.71}/src/engineering_platform/installation_update_composition.py +0 -0
  90. {engineering_platform-2.3.68 → engineering_platform-2.3.71}/src/engineering_platform/installation_update_executor.py +0 -0
  91. {engineering_platform-2.3.68 → engineering_platform-2.3.71}/src/engineering_platform/installation_update_migration.py +0 -0
  92. {engineering_platform-2.3.68 → engineering_platform-2.3.71}/src/engineering_platform/installation_update_operation.py +0 -0
  93. {engineering_platform-2.3.68 → engineering_platform-2.3.71}/src/engineering_platform/installation_update_plan.py +0 -0
  94. {engineering_platform-2.3.68 → engineering_platform-2.3.71}/src/engineering_platform/installation_update_preparation.py +0 -0
  95. {engineering_platform-2.3.68 → engineering_platform-2.3.71}/src/engineering_platform/investigation_ledger.py +0 -0
  96. {engineering_platform-2.3.68 → engineering_platform-2.3.71}/src/engineering_platform/legacy_inbox_migration.py +0 -0
  97. {engineering_platform-2.3.68 → engineering_platform-2.3.71}/src/engineering_platform/legacy_installation_adoption.py +0 -0
  98. {engineering_platform-2.3.68 → engineering_platform-2.3.71}/src/engineering_platform/lifecycle_worker.py +0 -0
  99. {engineering_platform-2.3.68 → engineering_platform-2.3.71}/src/engineering_platform/live_status.py +0 -0
  100. {engineering_platform-2.3.68 → engineering_platform-2.3.71}/src/engineering_platform/local_api.py +0 -0
  101. {engineering_platform-2.3.68 → engineering_platform-2.3.71}/src/engineering_platform/local_api_keychain.py +0 -0
  102. {engineering_platform-2.3.68 → engineering_platform-2.3.71}/src/engineering_platform/local_repository_binding.py +0 -0
  103. {engineering_platform-2.3.68 → engineering_platform-2.3.71}/src/engineering_platform/managed_codex_runtime.py +0 -0
  104. {engineering_platform-2.3.68 → engineering_platform-2.3.71}/src/engineering_platform/operational_installation.py +0 -0
  105. {engineering_platform-2.3.68 → engineering_platform-2.3.71}/src/engineering_platform/operational_installation_lock.py +0 -0
  106. {engineering_platform-2.3.68 → engineering_platform-2.3.71}/src/engineering_platform/operational_installation_record.py +0 -0
  107. {engineering_platform-2.3.68 → engineering_platform-2.3.71}/src/engineering_platform/owner_credential_recovery.py +0 -0
  108. {engineering_platform-2.3.68 → engineering_platform-2.3.71}/src/engineering_platform/parity_context.py +0 -0
  109. {engineering_platform-2.3.68 → engineering_platform-2.3.71}/src/engineering_platform/parity_lifecycle_dispatcher.py +0 -0
  110. {engineering_platform-2.3.68 → engineering_platform-2.3.71}/src/engineering_platform/platform_admin.py +0 -0
  111. {engineering_platform-2.3.68 → engineering_platform-2.3.71}/src/engineering_platform/platform_api.py +0 -0
  112. {engineering_platform-2.3.68 → engineering_platform-2.3.71}/src/engineering_platform/platform_bootstrap.py +0 -0
  113. {engineering_platform-2.3.68 → engineering_platform-2.3.71}/src/engineering_platform/platform_components.py +0 -0
  114. {engineering_platform-2.3.68 → engineering_platform-2.3.71}/src/engineering_platform/pr_check_repair.py +0 -0
  115. {engineering_platform-2.3.68 → engineering_platform-2.3.71}/src/engineering_platform/pr_evidence_backfill.py +0 -0
  116. {engineering_platform-2.3.68 → engineering_platform-2.3.71}/src/engineering_platform/producer.py +0 -0
  117. {engineering_platform-2.3.68 → engineering_platform-2.3.71}/src/engineering_platform/product_installation_readback.py +0 -0
  118. {engineering_platform-2.3.68 → engineering_platform-2.3.71}/src/engineering_platform/project_agent.py +0 -0
  119. {engineering_platform-2.3.68 → engineering_platform-2.3.71}/src/engineering_platform/project_agent_service.py +0 -0
  120. {engineering_platform-2.3.68 → engineering_platform-2.3.71}/src/engineering_platform/project_topology.py +0 -0
  121. {engineering_platform-2.3.68 → engineering_platform-2.3.71}/src/engineering_platform/prompt_history.py +0 -0
  122. {engineering_platform-2.3.68 → engineering_platform-2.3.71}/src/engineering_platform/provider_context.py +0 -0
  123. {engineering_platform-2.3.68 → engineering_platform-2.3.71}/src/engineering_platform/provider_context_benchmark.py +0 -0
  124. {engineering_platform-2.3.68 → engineering_platform-2.3.71}/src/engineering_platform/provider_context_scope.py +0 -0
  125. {engineering_platform-2.3.68 → engineering_platform-2.3.71}/src/engineering_platform/provider_interruption.py +0 -0
  126. {engineering_platform-2.3.68 → engineering_platform-2.3.71}/src/engineering_platform/provider_process_identity.py +0 -0
  127. {engineering_platform-2.3.68 → engineering_platform-2.3.71}/src/engineering_platform/provider_readiness.py +0 -0
  128. {engineering_platform-2.3.68 → engineering_platform-2.3.71}/src/engineering_platform/provider_recovery.py +0 -0
  129. {engineering_platform-2.3.68 → engineering_platform-2.3.71}/src/engineering_platform/provider_usage.py +0 -0
  130. {engineering_platform-2.3.68 → engineering_platform-2.3.71}/src/engineering_platform/providers.py +0 -0
  131. {engineering_platform-2.3.68 → engineering_platform-2.3.71}/src/engineering_platform/qualification.py +0 -0
  132. {engineering_platform-2.3.68 → engineering_platform-2.3.71}/src/engineering_platform/recommendation_handoff.py +0 -0
  133. {engineering_platform-2.3.68 → engineering_platform-2.3.71}/src/engineering_platform/release_operation.py +0 -0
  134. {engineering_platform-2.3.68 → engineering_platform-2.3.71}/src/engineering_platform/report_analysis.py +0 -0
  135. {engineering_platform-2.3.68 → engineering_platform-2.3.71}/src/engineering_platform/repository_attachment.py +0 -0
  136. {engineering_platform-2.3.68 → engineering_platform-2.3.71}/src/engineering_platform/repository_handoff.py +0 -0
  137. {engineering_platform-2.3.68 → engineering_platform-2.3.71}/src/engineering_platform/resources.py +0 -0
  138. {engineering_platform-2.3.68 → engineering_platform-2.3.71}/src/engineering_platform/reviewer_evidence.py +0 -0
  139. {engineering_platform-2.3.68 → engineering_platform-2.3.71}/src/engineering_platform/revision_binding.py +0 -0
  140. {engineering_platform-2.3.68 → engineering_platform-2.3.71}/src/engineering_platform/schemas/producer-readback-v1.2.schema.json +0 -0
  141. {engineering_platform-2.3.68 → engineering_platform-2.3.71}/src/engineering_platform/schemas/producer-readback-v1.schema.json +0 -0
  142. {engineering_platform-2.3.68 → engineering_platform-2.3.71}/src/engineering_platform/schemas/repository-attachment.schema.json +0 -0
  143. {engineering_platform-2.3.68 → engineering_platform-2.3.71}/src/engineering_platform/server_console_services.py +0 -0
  144. {engineering_platform-2.3.68 → engineering_platform-2.3.71}/src/engineering_platform/server_relay.py +0 -0
  145. {engineering_platform-2.3.68 → engineering_platform-2.3.71}/src/engineering_platform/server_service.py +0 -0
  146. {engineering_platform-2.3.68 → engineering_platform-2.3.71}/src/engineering_platform/status_model.py +0 -0
  147. {engineering_platform-2.3.68 → engineering_platform-2.3.71}/src/engineering_platform/status_reconciliation.py +0 -0
  148. {engineering_platform-2.3.68 → engineering_platform-2.3.71}/src/engineering_platform/submission_cli.py +0 -0
  149. {engineering_platform-2.3.68 → engineering_platform-2.3.71}/src/engineering_platform/submission_intake.py +0 -0
  150. {engineering_platform-2.3.68 → engineering_platform-2.3.71}/src/engineering_platform/submission_service.py +0 -0
  151. {engineering_platform-2.3.68 → engineering_platform-2.3.71}/src/engineering_platform/system_installation_topology.py +0 -0
  152. {engineering_platform-2.3.68 → engineering_platform-2.3.71}/src/engineering_platform/system_server_service.py +0 -0
  153. {engineering_platform-2.3.68 → engineering_platform-2.3.71}/src/engineering_platform/telemetry.py +0 -0
  154. {engineering_platform-2.3.68 → engineering_platform-2.3.71}/src/engineering_platform/validation_identity.py +0 -0
  155. {engineering_platform-2.3.68 → engineering_platform-2.3.71}/src/engineering_platform/validation_profile.py +0 -0
  156. {engineering_platform-2.3.68 → engineering_platform-2.3.71}/src/engineering_platform/version_preparation_delivery.py +0 -0
  157. {engineering_platform-2.3.68 → engineering_platform-2.3.71}/src/engineering_platform/workspace_preflight.py +0 -0
  158. {engineering_platform-2.3.68 → engineering_platform-2.3.71}/src/engineering_platform/worktree_provenance.py +0 -0
  159. {engineering_platform-2.3.68 → engineering_platform-2.3.71}/src/engineering_platform/worktree_tooling.py +0 -0
  160. {engineering_platform-2.3.68 → engineering_platform-2.3.71}/src/engineering_platform.egg-info/dependency_links.txt +0 -0
  161. {engineering_platform-2.3.68 → engineering_platform-2.3.71}/src/engineering_platform.egg-info/top_level.txt +0 -0
  162. {engineering_platform-2.3.68 → engineering_platform-2.3.71}/tests/test_adaptive_action_sizing_design.py +0 -0
  163. {engineering_platform-2.3.68 → engineering_platform-2.3.71}/tests/test_role_task_model_policy_design.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: engineering-platform
3
- Version: 2.3.68
3
+ Version: 2.3.71
4
4
  Summary: Local-first Engineering Platform execution operations runtime
5
5
  Author: Peter van Tol
6
6
  License: MIT
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "engineering-platform"
7
- version = "2.3.68"
7
+ version = "2.3.71"
8
8
  description = "Local-first Engineering Platform execution operations runtime"
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.14,<3.15"
@@ -18,6 +18,7 @@ engineering-platform-host = "engineering_platform.__main__:main"
18
18
  engineering-platform-server = "engineering_platform.server:main"
19
19
  engineering-platform = "engineering_platform.submission_cli:main"
20
20
  engineering-project-agent = "engineering_platform.project_agent:main"
21
+ engineering-reconciliation-adopt = "engineering_platform.reconciliation_adoption:main"
21
22
 
22
23
  [tool.setuptools]
23
24
  package-dir = {"" = "src"}
@@ -3,7 +3,7 @@
3
3
  "platform": {
4
4
  "id": "engineering-platform",
5
5
  "name": "Engineering Platform",
6
- "version": "2.3.68",
6
+ "version": "2.3.71",
7
7
  "generation": 2,
8
8
  "documentation_namespace": "engineering-platform",
9
9
  "capability_registry_version": 1
@@ -1,15 +1,15 @@
1
1
  {
2
2
  "bootstrap_contract": "2026.12",
3
3
  "checkpoint_format": 1,
4
- "dashboard_version": "2.3.68",
4
+ "dashboard_version": "2.3.71",
5
5
  "handoff_protocol": 1,
6
6
  "memory_format": 2,
7
7
  "minimum_codex_cli": "0.146.0",
8
8
  "inbox_protocol": 1,
9
- "platform_version": "2.3.68",
9
+ "platform_version": "2.3.71",
10
10
  "report_format": 2,
11
- "runner_version": "2.3.68",
11
+ "runner_version": "2.3.71",
12
12
  "status_model": 1,
13
- "storage_schema": 44,
14
- "watcher_version": "2.3.68"
13
+ "storage_schema": 45,
14
+ "watcher_version": "2.3.71"
15
15
  }
@@ -349,10 +349,22 @@ function phaseAwareRange(estimate) {
349
349
  if (estimate?.phase_aware !== true || samples < 2 || !Number.isFinite(lower) || !Number.isFinite(upper)) return null;
350
350
  return [Math.max(1, Math.round(lower / 60)), Math.max(1, Math.ceil(upper / 60))];
351
351
  }
352
+ function exceededEstimateContext(durationEstimate, elapsedContext = "") {
353
+ const values = [elapsedContext, t("estimate.exceeded_context")];
354
+ if (hasHistoricalEstimate(durationEstimate)) values.push(historicalContext(durationEstimate, ""));
355
+ return values.filter(Boolean).join("\n");
356
+ }
352
357
  function estimate(x, durationEstimate = {}) {
353
358
  const phase = x.current_phase || "";
354
359
  const phaseRange = phaseAwareRange(durationEstimate);
355
360
  if (["INITIALIZE", "EXECUTE_AGENT", "REPAIR_AGENT", "FINALIZATION_REPAIR_AGENT", "FINALIZE_AGENT", "REPOSITORY_CLEANUP"].includes(phase) && phaseRange) {
361
+ const remainingUpperSeconds = Number(durationEstimate?.remaining_upper_seconds);
362
+ if (Number.isFinite(remainingUpperSeconds) && remainingUpperSeconds <= 0) {
363
+ return {
364
+ summary: t("estimate.exceeded"),
365
+ context: exceededEstimateContext(durationEstimate),
366
+ };
367
+ }
356
368
  const [minimum, maximum] = phaseRange;
357
369
  return {
358
370
  summary: t("estimate.remaining", { minimum, maximum }),
@@ -375,6 +387,12 @@ function estimate(x, durationEstimate = {}) {
375
387
  remainingMinimum = Math.max(1, minimum - elapsed),
376
388
  remainingMaximum = Math.max(remainingMinimum, maximum - elapsed);
377
389
  const elapsedContext = t("estimate.elapsed", { elapsed, minutes: pluralMinutes(elapsed) });
390
+ if (maximum - elapsed <= 0) {
391
+ return {
392
+ summary: t("estimate.exceeded"),
393
+ context: exceededEstimateContext(durationEstimate, elapsedContext),
394
+ };
395
+ }
378
396
  return {
379
397
  summary: t("estimate.remaining", { minimum: remainingMinimum, maximum: remainingMaximum }),
380
398
  context: hasHistoricalEstimate(durationEstimate)
@@ -251,6 +251,8 @@ export const DASHBOARD_MESSAGES = {
251
251
  "estimate.cleanup": "Cleanup in progress",
252
252
  "estimate.cleanup_context": "Remaining time depends on the local repository.",
253
253
  "estimate.elapsed": "{elapsed} {minutes} elapsed.\nBased on execution size, phase and elapsed time. No live Codex progress or token usage.",
254
+ "estimate.exceeded": "Estimate exceeded; remaining time uncertain",
255
+ "estimate.exceeded_context": "The prior range has elapsed; no reliable remaining time is available.",
254
256
  "estimate.finalizing": "Finalization in progress",
255
257
  "estimate.finalizing_context": "Remaining time is only reliable with live Codex progress.",
256
258
  "estimate.historical_context": "Refined with {count} comparable completed executions using the same reported runtime settings.",
@@ -899,6 +901,8 @@ export const DASHBOARD_MESSAGES = {
899
901
  "estimate.cleanup": "Opschoning in uitvoering",
900
902
  "estimate.cleanup_context": "De resterende tijd hangt af van de lokale repository.",
901
903
  "estimate.elapsed": "{elapsed} {minutes} verstreken.\nGebaseerd op opdrachtomvang, fase en verstreken tijd. Geen live Codex-voortgang of tokenverbruik.",
904
+ "estimate.exceeded": "Schatting overschreden; resterende tijd onzeker",
905
+ "estimate.exceeded_context": "De eerdere bandbreedte is verstreken; er is geen betrouwbare resterende tijd beschikbaar.",
902
906
  "estimate.finalizing": "Finalisatie in uitvoering",
903
907
  "estimate.finalizing_context": "De resterende tijd is pas betrouwbaar met live Codex-voortgang.",
904
908
  "estimate.historical_context": "Verfijnd met {count} vergelijkbare voltooide uitvoeringen met dezelfde gerapporteerde runtime-instellingen.",
@@ -1547,6 +1551,8 @@ export const DASHBOARD_MESSAGES = {
1547
1551
  "estimate.cleanup": "Bereinigung läuft",
1548
1552
  "estimate.cleanup_context": "Die Restzeit hängt vom lokalen Repository ab.",
1549
1553
  "estimate.elapsed": "{elapsed} {minutes} vergangen.\nBasierend auf Auftragsumfang, Phase und verstrichener Zeit. Kein Live-Codex-Fortschritt oder Tokenverbrauch.",
1554
+ "estimate.exceeded": "Schätzung überschritten; Restzeit ungewiss",
1555
+ "estimate.exceeded_context": "Die bisherige Zeitspanne ist verstrichen; es ist keine verlässliche Restzeit verfügbar.",
1550
1556
  "estimate.finalizing": "Finalisierung läuft",
1551
1557
  "estimate.finalizing_context": "Die Restzeit ist nur mit Live-Codex-Fortschritt zuverlässig.",
1552
1558
  "estimate.historical_context": "Verfeinert mit {count} vergleichbaren abgeschlossenen Ausführungen mit denselben gemeldeten Laufzeiteinstellungen.",
@@ -2129,6 +2135,8 @@ export const DASHBOARD_MESSAGES = {
2129
2135
  "estimate.cleanup": "Nettoyage en cours",
2130
2136
  "estimate.cleanup_context": "Le temps restant dépend du dépôt local.",
2131
2137
  "estimate.elapsed": "{elapsed} {minutes} écoulées.\nBasé sur la périmètre de l’exécution, la phase et le temps écoulé. Aucune progression Codex en direct ni consommation de jetons.",
2138
+ "estimate.exceeded": "Estimation dépassée ; temps restant incertain",
2139
+ "estimate.exceeded_context": "La plage précédente est écoulée ; aucun temps restant fiable n’est disponible.",
2132
2140
  "estimate.finalizing": "Finalisation en cours",
2133
2141
  "estimate.finalizing_context": "Le temps restant n’est fiable qu’avec la progression Codex en direct.",
2134
2142
  "estimate.historical_context": "Affiné à partir de {count} exécutions terminées comparables utilisant les mêmes paramètres d’exécution signalés.",
@@ -2711,6 +2719,8 @@ export const DASHBOARD_MESSAGES = {
2711
2719
  "estimate.cleanup": "Limpieza en curso",
2712
2720
  "estimate.cleanup_context": "El tiempo restante depende del repositorio local.",
2713
2721
  "estimate.elapsed": "{elapsed} {minutes} transcurridos.\nBasado en el alcance de la ejecución, la fase y el tiempo transcurrido. Sin progreso en directo de Codex ni consumo de tokens.",
2722
+ "estimate.exceeded": "Estimación superada; tiempo restante incierto",
2723
+ "estimate.exceeded_context": "El intervalo anterior ya transcurrió; no hay un tiempo restante fiable.",
2714
2724
  "estimate.finalizing": "Finalización en curso",
2715
2725
  "estimate.finalizing_context": "El tiempo restante solo es fiable con progreso de Codex en directo.",
2716
2726
  "estimate.historical_context": "Afinado con {count} ejecuciones completadas comparables que usan la misma configuración de ejecución informada.",
@@ -306,6 +306,7 @@ def get_run_context(root: Path, run_id: str) -> dict[str, object]:
306
306
  snapshot_checks = qualification_source.get("pr_checks", {}) if qualification_source else {}
307
307
  implementation = snapshot_checks.get("IMPLEMENTATION", {}) if qualification_source and isinstance(snapshot_checks, dict) else checks.get("IMPLEMENTATION", {})
308
308
  finalization = snapshot_checks.get("FINALIZATION", {}) if qualification_source and isinstance(snapshot_checks, dict) else checks.get("FINALIZATION", {})
309
+ reconciliation = snapshot_checks.get("RECONCILIATION", {}) if qualification_source and isinstance(snapshot_checks, dict) else checks.get("RECONCILIATION", {})
309
310
  objective = _safe_objective(submission[3] if submission else {})
310
311
  validation_only = action_intent == "VALIDATION_ONLY"
311
312
  persisted_required_validation = qualification_source.get("validation_profile", UNAVAILABLE) if qualification_source else qualification_validation or UNAVAILABLE
@@ -337,8 +338,14 @@ def get_run_context(root: Path, run_id: str) -> dict[str, object]:
337
338
  "finalization_pr": "NOT_REQUIRED" if validation_only else _value(qualification_source.get("finalization_pr") if qualification_source else checkpoint.get("finalization_pull_request") or finalization.get("pr_number")), "finalization_pr_current_state": "NOT_REQUIRED" if validation_only else _value(finalization.get("pr_state")),
338
339
  "finalization_merge_state": _value(finalization.get("merge_state")), "finalization_merge_commit": _value(finalization.get("merge_commit") if qualification_source else checkpoint.get("finalization_merge_commit") or finalization.get("merge_commit")),
339
340
  "finalization_required_checks_state": _value(finalization.get("required_checks_state")), "finalization_merge_gate": "EXPECTED_OPERATOR_GATE" if phase == "WAIT_FOR_FINALIZATION_MERGE" else UNAVAILABLE,
341
+ "reconciliation_pr": "NOT_REQUIRED" if validation_only else _value(qualification_source.get("reconciliation_pr") if qualification_source else checkpoint.get("reconciliation_pull_request") or reconciliation.get("pr_number")),
342
+ "reconciliation_pr_current_state": "NOT_REQUIRED" if validation_only else _value(reconciliation.get("pr_state")),
343
+ "reconciliation_merge_state": _value(reconciliation.get("merge_state")),
344
+ "reconciliation_required_checks_state": _value(reconciliation.get("required_checks_state")),
345
+ "reconciliation_merge_gate": "EXPECTED_OPERATOR_GATE" if phase == "WAIT_FOR_RECONCILIATION_MERGE" else UNAVAILABLE,
340
346
  "implementation_delivery": _value(qualification_source.get("implementation_delivery")),
341
347
  "finalization_delivery": _value(qualification_source.get("finalization_delivery")),
348
+ "reconciliation_delivery": _value(qualification_source.get("reconciliation_delivery")),
342
349
  "run_delivery_commit": _value(checkpoint.get("implementation_head_sha") or checkpoint.get("last_verified_sha")), "current_repository_head": _value(checkpoint.get("last_verified_sha")), "delivery_commit_head_relationship": UNAVAILABLE},
343
350
  "validation": {"engineering_platform_qualification": UNAVAILABLE, "controls": validation_controls,
344
351
  "required_validation": persisted_required_validation,
@@ -33,7 +33,10 @@ class FinalizationCoordinator:
33
33
  if not callable(operation):
34
34
  return save_terminal(cleanup, "BLOCKED", "cleanup_unavailable", "Cleanup client is unavailable; resume with repository cleanup evidence.")
35
35
  try:
36
- result = operation(root, (cleanup.implementation_branch, cleanup.finalization_branch))
36
+ branches = (cleanup.implementation_branch, cleanup.finalization_branch)
37
+ if cleanup.transaction_kind == "RECONCILIATION":
38
+ branches += (cleanup.branch,)
39
+ result = operation(root, branches)
37
40
  except RunnerError as error:
38
41
  return save_terminal(cleanup, "BLOCKED", "repository_cleanup_required", str(error))
39
42
  return save_terminal(
@@ -82,7 +82,7 @@ from .reviewer_evidence import ReviewerEvidence
82
82
  from .investigation_ledger import InvocationInvestigationLedger
83
83
  from .execution_errors import CodexHandoffTimeout, CodexInvocationError, RunnerError
84
84
  from .execution_errors import ProviderReadinessBlocked
85
- from .execution_timeout_policy import FINALIZATION, REPAIR, agent_timeout
85
+ from .execution_timeout_policy import END_RECONCILIATION, FINALIZATION, REPAIR, agent_timeout
86
86
  from .provider_readiness import failures as provider_readiness_failures
87
87
  from .execution_repository import GitHubClient as ProviderGitHubClient, RepositoryClient as ProviderRepositoryClient
88
88
  from .execution_repository import GhCliClient as ProviderGhCliClient, SubprocessRepositoryClient as ProviderRepositoryClientImpl
@@ -125,6 +125,7 @@ from .component_logging import component_logger, record_technical_diagnostic, sh
125
125
 
126
126
  # Compatibility exports for integrations that already import these names.
127
127
  FINALIZATION_PR_HANDOFF_MAX_SECONDS = FINALIZATION.seconds
128
+ RECONCILIATION_PR_HANDOFF_MAX_SECONDS = END_RECONCILIATION.seconds
128
129
  REPAIR_AGENT_MAX_SECONDS = REPAIR.seconds
129
130
 
130
131
  # A repair remains scoped to its original PR, but it must also have a finite
@@ -305,11 +306,12 @@ def assemble_prompt(
305
306
  )
306
307
  authority = (
307
308
  """This is the sole automatic post-Finalization reconciliation. You may only update
308
- the four canonical rolling records, commit them directly to the already synchronized `main`,
309
- and push that one commit. Do not create a branch or pull request. Do not change runtime code,
310
- authority, lifecycle, retry, validation, provider, Forge, queue, or delivery semantics. Return
311
- `COMPLETE`, `repository_reconciled`, and the pushed main commit SHA only after verifying a clean
312
- workspace and that `main` contains that exact commit."""
309
+ the four canonical rolling records on the exact checkpointed reconciliation branch, commit and
310
+ push that branch, and create one draft pull request to `main`. Do not change runtime code,
311
+ authority, lifecycle, retry, validation, provider, Forge, queue, or delivery semantics. Do not
312
+ merge the pull request or push directly to `main`. Return `COMPLETE`, `repository_reconciled`,
313
+ the branch, pull-request number, and exact candidate commit SHA only after verifying a clean
314
+ workspace."""
313
315
  if state and state.transaction_kind == "RECONCILIATION"
314
316
  else
315
317
  """The runner holds explicit owner authorization for this exact bounded transaction. You may create, commit and push one bounded branch and draft pull request, or repair that same pull request. The runner may mark that pull request ready for review, but only the human operator may merge it. Do not merge, release, deploy, tag, publish, upload, change repository settings, bypass protection, or expand the objective."""
@@ -1090,7 +1092,7 @@ class EngineeringRunner:
1090
1092
  def _managed_pr_check(self, state: TransactionState, pr: PullRequestEvidence) -> None:
1091
1093
  """Persist current GitHub required-check evidence separately from historical waits."""
1092
1094
  role = state.transaction_kind
1093
- if role not in {"IMPLEMENTATION", "FINALIZATION"}:
1095
+ if role not in {"IMPLEMENTATION", "FINALIZATION", "RECONCILIATION"}:
1094
1096
  return
1095
1097
  check_state = "PASS" if pr.checks_terminal and pr.checks_passed else "FAIL" if pr.checks_terminal else "WAITING"
1096
1098
  try:
@@ -1418,12 +1420,20 @@ class EngineeringRunner:
1418
1420
  """Return whether an agent supplied bounded evidence of a failed local check."""
1419
1421
  if not result.validation_evidence:
1420
1422
  return False
1421
- summaries = " ".join(
1422
- item.get("result", "").casefold()
1423
- for item in result.validation_evidence
1424
- if isinstance(item, dict)
1425
- )
1426
- return any(token in summaries for token in ("fail", "failed", "timeout", "timed out", "error"))
1423
+ for item in result.validation_evidence:
1424
+ if not isinstance(item, dict):
1425
+ continue
1426
+ summary = item.get("result", "").strip().casefold()
1427
+ # Result summaries are dispositions followed by bounded detail.
1428
+ # A passing disposition takes precedence over words such as
1429
+ # "fail-closed" or "failure mode" in that descriptive detail.
1430
+ if re.match(r"^(?:pass(?:ed)?|success(?:ful|fully)?|succeeded)\b", summary):
1431
+ continue
1432
+ if re.match(r"^(?:fail(?:ed|ure)?|error|timeout|timed\s+out)\b", summary):
1433
+ return True
1434
+ if re.search(r"\b(?:failed|failure|error|timeout|timed\s+out)\b", summary):
1435
+ return True
1436
+ return False
1427
1437
 
1428
1438
  @staticmethod
1429
1439
  def _is_external_agent_block(result: AgentResult) -> bool:
@@ -2723,6 +2733,8 @@ First implementation pull-request publication gate:
2723
2733
  return self._advance_after_repair_agent_result(state, result)
2724
2734
  if lifecycle_phase == "FINALIZE_AGENT":
2725
2735
  return self._advance_after_finalization_agent_result(state, result)
2736
+ if lifecycle_phase == "RECONCILE_AGENT":
2737
+ return self._advance_after_reconciliation_agent_result(state, result)
2726
2738
  return self._save_terminal(state, "BLOCKED", "recovered_provider_phase_invalid", "Recovered provider result has an unsupported lifecycle phase.")
2727
2739
 
2728
2740
  def run(
@@ -2890,7 +2902,7 @@ First implementation pull-request publication gate:
2890
2902
  recovered_resume = (
2891
2903
  isinstance(recovery_snapshot, dict)
2892
2904
  and recovery_snapshot.get("state") == "RECOVERED"
2893
- and recovery_snapshot.get("lifecycle_phase") in {"EXECUTE_AGENT", "QUALITY_CONTROL_AGENT", "REPAIR_AGENT", "FINALIZE_AGENT"}
2905
+ and recovery_snapshot.get("lifecycle_phase") in {"EXECUTE_AGENT", "QUALITY_CONTROL_AGENT", "REPAIR_AGENT", "FINALIZE_AGENT", "RECONCILE_AGENT"}
2894
2906
  )
2895
2907
  try:
2896
2908
  persisted_submission = load_submission_for_run(self.root, state.run_id, central_database=self.store.central_database)
@@ -3273,6 +3285,8 @@ First implementation pull-request publication gate:
3273
3285
  state = self._record_agent_execution_time(state)
3274
3286
  self.console_detail = error.console_detail
3275
3287
  return self._terminalize_provider_invocation_error(state, error)
3288
+ if state.transaction_kind == "RECONCILIATION":
3289
+ return self._advance_after_reconciliation_agent_result(state, result)
3276
3290
  return self._advance_after_primary_agent_result(state, result, evidence)
3277
3291
 
3278
3292
  def _active_genesis_transaction(self, target: Path, run_id: str) -> str | None:
@@ -3389,10 +3403,7 @@ First implementation pull-request publication gate:
3389
3403
  next_action="create_finalization",
3390
3404
  )
3391
3405
  if state.transaction_kind == "RECONCILIATION" and not state.reconciliation_pull_request:
3392
- return replace(
3393
- state, phase="RECONCILE_AGENT", last_verified_sha=evidence.head_sha,
3394
- next_action="create_reconciliation",
3395
- )
3406
+ return self._recover_reconciliation_pull_request(state, evidence)
3396
3407
  if (
3397
3408
  state.transaction_kind == "FINALIZATION"
3398
3409
  and state.finalization_merge_commit
@@ -3470,6 +3481,54 @@ First implementation pull-request publication gate:
3470
3481
  write_live_status(self.root, recovered, recovered.next_action)
3471
3482
  return self._poll(recovered)
3472
3483
 
3484
+ def _recover_reconciliation_pull_request(
3485
+ self, state: TransactionState, evidence: RepositoryEvidence,
3486
+ ) -> TransactionState:
3487
+ """Recover only the PR bound to the durable reconciliation branch."""
3488
+ if state.reconciliation_pull_request:
3489
+ return replace(
3490
+ state, pull_request=state.reconciliation_pull_request,
3491
+ phase="WAIT_FOR_TERMINAL_EVIDENCE", next_action="poll_required_checks",
3492
+ )
3493
+ if (
3494
+ not state.branch
3495
+ or state.branch == "main"
3496
+ or not state.finalization_merge_commit
3497
+ or evidence.branch not in {"main", state.branch}
3498
+ or not evidence.clean
3499
+ or not self.repository.main_contains(self.root, state.finalization_merge_commit)
3500
+ ):
3501
+ return self._save_terminal(
3502
+ state, "BLOCKED", "reconciliation_recovery_evidence_required",
3503
+ "Reconciliation recovery requires its checkpointed branch and a verified Finalization merge.",
3504
+ )
3505
+ candidate = self.github.pull_request_for_head_branch(state.branch)
3506
+ if candidate is None:
3507
+ return self._save_terminal(
3508
+ state, "BLOCKED", "reconciliation_recovery_evidence_required",
3509
+ "No reconciliation pull request matches the checkpointed branch; no replacement was created.",
3510
+ )
3511
+ if (
3512
+ candidate.head_branch != state.branch
3513
+ or candidate.base_branch != "main"
3514
+ or candidate.state not in {"OPEN", "MERGED"}
3515
+ ):
3516
+ return self._save_terminal(
3517
+ state, "BLOCKED", "reconciliation_recovery_evidence_invalid",
3518
+ "Reconciliation pull request evidence does not match the checkpointed branch and main base.",
3519
+ )
3520
+ recovered = replace(
3521
+ state, pull_request=candidate.number,
3522
+ reconciliation_pull_request=candidate.number,
3523
+ phase="WAIT_FOR_TERMINAL_EVIDENCE", next_action="poll_required_checks",
3524
+ last_verified_sha=evidence.head_sha,
3525
+ latest_repository_evidence=_repository_summary(evidence),
3526
+ latest_github_evidence=_pull_request_summary(candidate),
3527
+ )
3528
+ self.store.save(recovered)
3529
+ write_live_status(self.root, recovered, recovered.next_action)
3530
+ return self._poll(recovered)
3531
+
3473
3532
  def _poll(self, state: TransactionState, result: AgentResult | None = None) -> TransactionState:
3474
3533
  if state.pull_request:
3475
3534
  state = self._provider_readiness_gate(
@@ -3489,32 +3548,10 @@ First implementation pull-request publication gate:
3489
3548
  "Agent result referenced a pull request without a transaction branch; the current main branch cannot be reused as execution evidence.",
3490
3549
  )
3491
3550
  if not state.pull_request:
3492
- if state.transaction_kind == "RECONCILIATION" and result and result.pull_request:
3493
- return self._save_terminal(
3494
- state, "BLOCKED", "reconciliation_pull_request_forbidden",
3495
- "Automatic reconciliation must commit directly to main and must not create a pull request.",
3496
- )
3497
- if state.transaction_kind == "RECONCILIATION" and result and result.terminal_state == "COMPLETE":
3498
- evidence = self.repository.inspect(self.root)
3499
- if (
3500
- result.terminal_condition == "repository_reconciled"
3501
- and result.commit_sha
3502
- and evidence.clean
3503
- and evidence.branch == "main"
3504
- and evidence.head_sha == result.commit_sha
3505
- and evidence.main_contains_head
3506
- ):
3507
- reconciled = replace(state, last_verified_sha=result.commit_sha)
3508
- reconciled = self._append_verified_commit_evidence(
3509
- reconciled,
3510
- phase="RECONCILE_AGENT",
3511
- commit_sha=result.commit_sha,
3512
- description="end_reconciliation_commit_verified",
3513
- )
3514
- return self._cleanup(reconciled)
3551
+ if state.transaction_kind == "RECONCILIATION" and result:
3515
3552
  return self._save_terminal(
3516
- state, "BLOCKED", "automatic_reconciliation_evidence_required",
3517
- "Automatic reconciliation requires a clean main checkout containing its reported commit.",
3553
+ state, "BLOCKED", "reconciliation_pr_required",
3554
+ "Automatic reconciliation requires its checkpointed pull request before cleanup.",
3518
3555
  )
3519
3556
  if result and result.terminal_state == "COMPLETE":
3520
3557
  evidence = self.repository.inspect(self.root)
@@ -3877,12 +3914,7 @@ First implementation pull-request publication gate:
3877
3914
  return self._poll(finalization, result)
3878
3915
 
3879
3916
  def _start_automatic_reconciliation(self, state: TransactionState) -> TransactionState:
3880
- """Apply the bounded rolling-record update after Finalization merges.
3881
-
3882
- This is deliberately a direct, post-merge `main` commit: it has no PR,
3883
- review, approval, or operator merge boundary. Its scope is enforced by
3884
- the supplied reconciliation prompt and the exact commit evidence below.
3885
- """
3917
+ """Apply the bounded rolling-record update through a protected PR."""
3886
3918
  synchronize = getattr(self.repository, "synchronize_main", None)
3887
3919
  if callable(synchronize):
3888
3920
  synchronize(self.root)
@@ -3892,22 +3924,37 @@ First implementation pull-request publication gate:
3892
3924
  state,
3893
3925
  "End reconciliation is waiting for a clean, synchronized main checkout.",
3894
3926
  )
3927
+ expected_branch = (
3928
+ state.branch
3929
+ if state.transaction_kind == "RECONCILIATION" and state.branch != "main"
3930
+ else None
3931
+ ) or f"codex/reconcile-{state.run_id}"
3895
3932
  reconciliation = replace(
3896
3933
  state, phase="RECONCILE_AGENT", transaction_kind="RECONCILIATION",
3897
- branch=None, pull_request=None, reconciliation_pull_request=None,
3898
- next_action="reconcile_rolling_records_on_main", waiting_for_merge_since=None,
3934
+ branch=expected_branch, pull_request=None, reconciliation_pull_request=None,
3935
+ next_action="create_reconciliation_pull_request", waiting_for_merge_since=None,
3899
3936
  latest_repository_evidence=_repository_summary(evidence),
3900
3937
  )
3901
3938
  self._managed_action(reconciliation, "AUTOMATIC_RECONCILIATION")
3902
3939
  self.store.save(reconciliation)
3903
3940
  write_live_status(self.root, reconciliation, reconciliation.next_action)
3941
+ reconciliation_span = self._start_phase(state.run_id, "RECONCILIATION")
3942
+ handoff_started = time.monotonic()
3943
+ set_handoff_deadline = getattr(self.agent, "set_handoff_deadline_callback", None)
3944
+ if callable(set_handoff_deadline):
3945
+ set_handoff_deadline(
3946
+ lambda: time.monotonic() - handoff_started
3947
+ >= RECONCILIATION_PR_HANDOFF_MAX_SECONDS
3948
+ )
3904
3949
  try:
3905
3950
  result = self._invoke_agent_with_timing(
3906
3951
  reconciliation,
3907
3952
  assemble_prompt(
3908
3953
  Path(reconciliation.prompt_path), reconciliation,
3909
3954
  managed_target=self.root if reconciliation.execution_mode == "MANAGED" else None,
3910
- ) + "\n\nReconcile only the four canonical rolling current-state records after the verified Finalization merge. Preserve immutable Prompt History.",
3955
+ )
3956
+ + "\n\nReconcile only the four canonical rolling current-state records after the verified Finalization merge. "
3957
+ f"Preserve immutable Prompt History and create one draft pull request on exactly `{expected_branch}`.",
3911
3958
  )
3912
3959
  reconciliation = self._record_agent_execution_time(reconciliation)
3913
3960
  reconciliation = self._record_validation_evidence(reconciliation, result)
@@ -3918,12 +3965,63 @@ First implementation pull-request publication gate:
3918
3965
  description="end_reconciliation_commit_verified",
3919
3966
  )
3920
3967
  self._persist_agent_usage(reconciliation.run_id)
3968
+ except CodexHandoffTimeout:
3969
+ complete_phase(self.root, reconciliation_span, outcome="FAILED")
3970
+ reconciliation = self._record_agent_execution_time(reconciliation)
3971
+ return self._recover_reconciliation_pull_request(
3972
+ reconciliation, self.repository.inspect(self.root)
3973
+ )
3921
3974
  except ProviderReadinessBlocked as blocked:
3975
+ complete_phase(self.root, reconciliation_span, outcome="BLOCKED")
3922
3976
  return blocked.state
3923
3977
  except CodexInvocationError as error:
3978
+ complete_phase(
3979
+ self.root, reconciliation_span,
3980
+ outcome="INTERRUPTED" if error.provider_turn_interrupted else "FAILED",
3981
+ )
3924
3982
  reconciliation = self._record_agent_execution_time(reconciliation)
3925
3983
  self.console_detail = error.console_detail
3926
3984
  return self._terminalize_provider_invocation_error(reconciliation, error)
3985
+ finally:
3986
+ if callable(set_handoff_deadline):
3987
+ set_handoff_deadline(None)
3988
+ complete_phase(self.root, reconciliation_span)
3989
+ return self._advance_after_reconciliation_agent_result(reconciliation, result)
3990
+
3991
+ def _advance_after_reconciliation_agent_result(
3992
+ self, reconciliation: TransactionState, result: AgentResult,
3993
+ ) -> TransactionState:
3994
+ """Bind a live or recovered reconciliation result to its one PR."""
3995
+ if result.terminal_state in {"BLOCKED", "FAILED"} or not result.pull_request:
3996
+ return self._save_terminal(
3997
+ reconciliation,
3998
+ result.terminal_state
3999
+ if result.terminal_state in {"BLOCKED", "FAILED"}
4000
+ else "BLOCKED",
4001
+ "reconciliation_pr_required",
4002
+ result.diagnostic or "Reconciliation pull request was not created.",
4003
+ )
4004
+ expected_branch = reconciliation.branch
4005
+ if not expected_branch or expected_branch == "main" or result.branch != expected_branch:
4006
+ return self._save_terminal(
4007
+ reconciliation, "BLOCKED", "reconciliation_branch_mismatch",
4008
+ "Reconciliation returned a pull request outside the durable reconciliation branch.",
4009
+ )
4010
+ reconciliation = replace(
4011
+ reconciliation,
4012
+ phase="WAIT_FOR_TERMINAL_EVIDENCE",
4013
+ pull_request=result.pull_request,
4014
+ reconciliation_pull_request=result.pull_request,
4015
+ terminal_condition="repository_reconciled",
4016
+ next_action="poll_required_checks",
4017
+ )
4018
+ self.store.save(reconciliation)
4019
+ write_live_status(self.root, reconciliation, reconciliation.next_action)
4020
+ evidence = self.github.pull_request(result.pull_request)
4021
+ if evidence.state == "MERGED":
4022
+ return self._poll(reconciliation, result)
4023
+ self.github.normalize_markdown_body(result.pull_request)
4024
+ self.github.ready(result.pull_request)
3927
4025
  return self._poll(reconciliation, result)
3928
4026
 
3929
4027
  def _save_terminal(
@@ -19,12 +19,12 @@ TERMINAL = frozenset({"COMPLETE", "BLOCKED", "FAILED"})
19
19
  _MANAGED_PATH = (
20
20
  "START", "INITIALIZE", "CAPABILITY_REVIEW", "EXECUTE_AGENT", "LOCAL_REPOSITORY_VALIDATION", "QUALITY_CONTROL_AGENT", "REPAIR_AGENT",
21
21
  "WAIT_FOR_OPERATOR_MERGE", "FINALIZE_AGENT", "WAIT_FOR_FINALIZATION_MERGE",
22
- "RECONCILE_AGENT", "REPOSITORY_CLEANUP", "TERMINAL",
22
+ "RECONCILE_AGENT", "WAIT_FOR_RECONCILIATION_MERGE", "REPOSITORY_CLEANUP", "TERMINAL",
23
23
  )
24
24
  _FINALIZATION_REPAIR_PATH = (
25
25
  "START", "INITIALIZE", "CAPABILITY_REVIEW", "EXECUTE_AGENT", "QUALITY_CONTROL_AGENT", "REPAIR_AGENT",
26
26
  "WAIT_FOR_OPERATOR_MERGE", "FINALIZE_AGENT", "FINALIZATION_REPAIR_AGENT", "WAIT_FOR_FINALIZATION_MERGE",
27
- "RECONCILE_AGENT", "REPOSITORY_CLEANUP", "TERMINAL",
27
+ "RECONCILE_AGENT", "WAIT_FOR_RECONCILIATION_MERGE", "REPOSITORY_CLEANUP", "TERMINAL",
28
28
  )
29
29
  # Genesis has no pull-request merge boundary. This is presentation of the
30
30
  # existing mode contract, not a new execution sequence.
@@ -52,6 +52,7 @@ _STEP_PHASES = {
52
52
  "WAIT_FOR_OPERATOR_MERGE": frozenset({"PR_OR_MERGE", "EXTERNAL_CI_WAIT"}),
53
53
  "FINALIZE_AGENT": frozenset({"REPOSITORY_FINALIZATION", "FINALIZATION"}),
54
54
  "RECONCILE_AGENT": frozenset({"RECONCILIATION", "REPORT_GENERATION", "EVIDENCE_PERSISTENCE"}),
55
+ "WAIT_FOR_RECONCILIATION_MERGE": frozenset({"PR_OR_MERGE", "EXTERNAL_CI_WAIT"}),
55
56
  "REPOSITORY_CLEANUP": frozenset({"REPOSITORY_CLEANUP"}),
56
57
  "TERMINAL": frozenset({"TOTAL_EXECUTION"}),
57
58
  }
@@ -397,6 +397,7 @@ def _commit_strategy(state: TransactionState, bundle: TerminalEvidenceBundle) ->
397
397
  f"- Implementation merge: `{state.implementation_merge_commit or 'not recorded'}`",
398
398
  f"- Finalization PR: `{state.finalization_pull_request or 'not recorded'}`",
399
399
  f"- Finalization merge: `{state.finalization_merge_commit or 'not recorded'}`",
400
+ f"- Reconciliation PR: `{state.reconciliation_pull_request or 'not recorded'}`",
400
401
  )
401
402
 
402
403
 
@@ -1080,6 +1081,7 @@ def _managed_autonomy_projection(root: Path, state: TransactionState, bundle: Te
1080
1081
  snapshot = managed_autonomy_snapshot(
1081
1082
  root, run_id=state.run_id, execution_outcome=state.phase,
1082
1083
  implementation_pr=state.implementation_pull_request, finalization_pr=state.finalization_pull_request,
1084
+ reconciliation_pr=state.reconciliation_pull_request,
1083
1085
  repository_state="MERGED_RECONCILED" if state.phase == "COMPLETE" and (state.finalization_merge_commit or state.action_intent == "VALIDATION_ONLY") else "UNAVAILABLE",
1084
1086
  workspace_state="WORKSPACE_READY" if state.phase == "COMPLETE" and bundle.worktree_state == "clean" else "UNAVAILABLE",
1085
1087
  main_origin_sync="YES" if bundle.target_branch == "main" else "UNAVAILABLE",
@@ -1503,6 +1505,7 @@ def generate_terminal_report(
1503
1505
  "## Pull Requests",
1504
1506
  f"- Implementation: branch `{state.implementation_branch}`, PR `{state.implementation_pull_request}`, merge `{state.implementation_merge_commit}`",
1505
1507
  f"- Finalization: branch `{state.finalization_branch}`, PR `{state.finalization_pull_request}`, merge `{state.finalization_merge_commit}`",
1508
+ f"- Reconciliation: branch `{state.branch if state.transaction_kind == 'RECONCILIATION' else None}`, PR `{state.reconciliation_pull_request}`",
1506
1509
  "",
1507
1510
  *_retry_relationship(state),
1508
1511
  "## Initial Repository Assessment",
@@ -1600,7 +1603,7 @@ def generate_terminal_report(
1600
1603
  "## Metrics",
1601
1604
  f"- Codex CLI execution time: {state.agent_execution_seconds if state.agent_execution_seconds is not None else 'not measured'} seconds",
1602
1605
  f"- Repair iterations: {state.repair_iterations}",
1603
- f"- PRs created: {sum(value is not None for value in (state.implementation_pull_request, state.finalization_pull_request))}",
1606
+ f"- PRs created: {sum(value is not None for value in (state.implementation_pull_request, state.finalization_pull_request, state.reconciliation_pull_request))}",
1604
1607
  f"- Merges performed: {sum(value is not None for value in (state.implementation_merge_commit, state.finalization_merge_commit))}",
1605
1608
  "",
1606
1609
  )
@@ -44,7 +44,9 @@ class GitHubClient(Protocol):
44
44
  def ready(self, number: int) -> None: ...
45
45
  def normalize_markdown_body(self, number: int) -> bool: ...
46
46
  def merge(self, number: int) -> None: ...
47
- def create_or_recover_pull_request(self, branch: str, base: str, title: str, body: str) -> PullRequestEvidence: ...
47
+ def create_or_recover_pull_request(
48
+ self, branch: str, base: str, title: str, body: str, *, draft: bool = False,
49
+ ) -> PullRequestEvidence: ...
48
50
  def qualification_for_exact_head(self, number: int, head_sha: str) -> dict[str, object]: ...
49
51
  def version_preparation_writer(self) -> dict[str, object]: ...
50
52
 
@@ -250,7 +252,9 @@ class GhCliClient:
250
252
  raise RunnerError("Finalization recovery found more than one pull request for its checkpointed branch.")
251
253
  return self.pull_request(numbers[0])
252
254
 
253
- def create_or_recover_pull_request(self, branch: str, base: str, title: str, body: str) -> PullRequestEvidence:
255
+ def create_or_recover_pull_request(
256
+ self, branch: str, base: str, title: str, body: str, *, draft: bool = False,
257
+ ) -> PullRequestEvidence:
254
258
  """Create one bounded PR or recover the sole existing branch identity."""
255
259
  existing = self.pull_request_for_head_branch(branch)
256
260
  if existing is not None:
@@ -258,7 +262,13 @@ class GhCliClient:
258
262
  raise RunnerError("Version preparation branch already has a pull request for another base.")
259
263
  return existing
260
264
  try:
261
- raw = self._github("pr", "create", "--head", branch, "--base", base, "--title", title, "--body", body)
265
+ arguments = [
266
+ "pr", "create", "--head", branch, "--base", base,
267
+ "--title", title, "--body", body,
268
+ ]
269
+ if draft:
270
+ arguments.append("--draft")
271
+ raw = self._github(*arguments)
262
272
  except RuntimeError as error:
263
273
  # A successful create may lose its acknowledgement. Only recover
264
274
  # the deterministic branch identity; never create a second PR.