engineering-platform 2.3.88__tar.gz → 2.3.90__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 (170) hide show
  1. {engineering_platform-2.3.88 → engineering_platform-2.3.90}/PKG-INFO +1 -1
  2. {engineering_platform-2.3.88 → engineering_platform-2.3.90}/pyproject.toml +1 -1
  3. {engineering_platform-2.3.88 → engineering_platform-2.3.90}/src/engineering_platform/ENGINEERING_PLATFORM_CONFIG.json +1 -1
  4. {engineering_platform-2.3.88 → engineering_platform-2.3.90}/src/engineering_platform/ENGINEERING_PLATFORM_VERSION.json +4 -4
  5. {engineering_platform-2.3.88 → engineering_platform-2.3.90}/src/engineering_platform/central_operational_reset.py +34 -8
  6. {engineering_platform-2.3.88 → engineering_platform-2.3.90}/src/engineering_platform/execution_host.py +37 -13
  7. {engineering_platform-2.3.88 → engineering_platform-2.3.90}/src/engineering_platform/execution_repository.py +138 -18
  8. {engineering_platform-2.3.88 → engineering_platform-2.3.90}/src/engineering_platform/merge_delegation.py +127 -13
  9. {engineering_platform-2.3.88 → engineering_platform-2.3.90}/src/engineering_platform/platform_version.py +1 -1
  10. {engineering_platform-2.3.88 → engineering_platform-2.3.90}/src/engineering_platform/server.py +103 -5
  11. {engineering_platform-2.3.88 → engineering_platform-2.3.90}/src/engineering_platform/submission_service.py +2 -1
  12. {engineering_platform-2.3.88 → engineering_platform-2.3.90}/src/engineering_platform/templates/workspace-config.json +1 -1
  13. {engineering_platform-2.3.88 → engineering_platform-2.3.90}/src/engineering_platform.egg-info/PKG-INFO +1 -1
  14. {engineering_platform-2.3.88 → engineering_platform-2.3.90}/README.md +0 -0
  15. {engineering_platform-2.3.88 → engineering_platform-2.3.90}/setup.cfg +0 -0
  16. {engineering_platform-2.3.88 → engineering_platform-2.3.90}/src/engineering_platform/__init__.py +0 -0
  17. {engineering_platform-2.3.88 → engineering_platform-2.3.90}/src/engineering_platform/__main__.py +0 -0
  18. {engineering_platform-2.3.88 → engineering_platform-2.3.90}/src/engineering_platform/agent_state.py +0 -0
  19. {engineering_platform-2.3.88 → engineering_platform-2.3.90}/src/engineering_platform/agent_trust.py +0 -0
  20. {engineering_platform-2.3.88 → engineering_platform-2.3.90}/src/engineering_platform/assets/dashboard.css +0 -0
  21. {engineering_platform-2.3.88 → engineering_platform-2.3.90}/src/engineering_platform/assets/dashboard.js +0 -0
  22. {engineering_platform-2.3.88 → engineering_platform-2.3.90}/src/engineering_platform/assets/dashboard_locales.mjs +0 -0
  23. {engineering_platform-2.3.88 → engineering_platform-2.3.90}/src/engineering_platform/assets/dashboard_status_store.mjs +0 -0
  24. {engineering_platform-2.3.88 → engineering_platform-2.3.90}/src/engineering_platform/assets/dashboard_translation.mjs +0 -0
  25. {engineering_platform-2.3.88 → engineering_platform-2.3.90}/src/engineering_platform/assets/operations-console/apple-touch-icon-dark.png +0 -0
  26. {engineering_platform-2.3.88 → engineering_platform-2.3.90}/src/engineering_platform/assets/operations-console/apple-touch-icon-light.png +0 -0
  27. {engineering_platform-2.3.88 → engineering_platform-2.3.90}/src/engineering_platform/assets/operations-console/icon-dark.png +0 -0
  28. {engineering_platform-2.3.88 → engineering_platform-2.3.90}/src/engineering_platform/assets/operations-console/icon-light.png +0 -0
  29. {engineering_platform-2.3.88 → engineering_platform-2.3.90}/src/engineering_platform/assets/operations-console/icon-transparent.png +0 -0
  30. {engineering_platform-2.3.88 → engineering_platform-2.3.90}/src/engineering_platform/assets/operations-console/manifest.webmanifest +0 -0
  31. {engineering_platform-2.3.88 → engineering_platform-2.3.90}/src/engineering_platform/assurance_scope.py +0 -0
  32. {engineering_platform-2.3.88 → engineering_platform-2.3.90}/src/engineering_platform/capability_preflight.py +0 -0
  33. {engineering_platform-2.3.88 → engineering_platform-2.3.90}/src/engineering_platform/capability_review.py +0 -0
  34. {engineering_platform-2.3.88 → engineering_platform-2.3.90}/src/engineering_platform/central_data_transfer.py +0 -0
  35. {engineering_platform-2.3.88 → engineering_platform-2.3.90}/src/engineering_platform/central_database.py +0 -0
  36. {engineering_platform-2.3.88 → engineering_platform-2.3.90}/src/engineering_platform/central_store_migration.py +0 -0
  37. {engineering_platform-2.3.88 → engineering_platform-2.3.90}/src/engineering_platform/codex_capacity.py +0 -0
  38. {engineering_platform-2.3.88 → engineering_platform-2.3.90}/src/engineering_platform/codex_chat.py +0 -0
  39. {engineering_platform-2.3.88 → engineering_platform-2.3.90}/src/engineering_platform/codex_observability.py +0 -0
  40. {engineering_platform-2.3.88 → engineering_platform-2.3.90}/src/engineering_platform/component_lock.py +0 -0
  41. {engineering_platform-2.3.88 → engineering_platform-2.3.90}/src/engineering_platform/component_logging.py +0 -0
  42. {engineering_platform-2.3.88 → engineering_platform-2.3.90}/src/engineering_platform/console_presentation.py +0 -0
  43. {engineering_platform-2.3.88 → engineering_platform-2.3.90}/src/engineering_platform/console_route_ownership.py +0 -0
  44. {engineering_platform-2.3.88 → engineering_platform-2.3.90}/src/engineering_platform/contracts/__init__.py +0 -0
  45. {engineering_platform-2.3.88 → engineering_platform-2.3.90}/src/engineering_platform/contracts/ep_consumer.py +0 -0
  46. {engineering_platform-2.3.88 → engineering_platform-2.3.90}/src/engineering_platform/contracts/models.py +0 -0
  47. {engineering_platform-2.3.88 → engineering_platform-2.3.90}/src/engineering_platform/contracts/projection.py +0 -0
  48. {engineering_platform-2.3.88 → engineering_platform-2.3.90}/src/engineering_platform/dashboard_browser_validation.py +0 -0
  49. {engineering_platform-2.3.88 → engineering_platform-2.3.90}/src/engineering_platform/dashboard_state.py +0 -0
  50. {engineering_platform-2.3.88 → engineering_platform-2.3.90}/src/engineering_platform/dashboard_supervisor.swift +0 -0
  51. {engineering_platform-2.3.88 → engineering_platform-2.3.90}/src/engineering_platform/dashboard_translation.py +0 -0
  52. {engineering_platform-2.3.88 → engineering_platform-2.3.90}/src/engineering_platform/dependabot_producer.py +0 -0
  53. {engineering_platform-2.3.88 → engineering_platform-2.3.90}/src/engineering_platform/development_profile.py +0 -0
  54. {engineering_platform-2.3.88 → engineering_platform-2.3.90}/src/engineering_platform/drift_diagnostics.py +0 -0
  55. {engineering_platform-2.3.88 → engineering_platform-2.3.90}/src/engineering_platform/emergency_recovery.py +0 -0
  56. {engineering_platform-2.3.88 → engineering_platform-2.3.90}/src/engineering_platform/engineering_memory.py +0 -0
  57. {engineering_platform-2.3.88 → engineering_platform-2.3.90}/src/engineering_platform/ep_consumer_credentials.py +0 -0
  58. {engineering_platform-2.3.88 → engineering_platform-2.3.90}/src/engineering_platform/evidence_projection.py +0 -0
  59. {engineering_platform-2.3.88 → engineering_platform-2.3.90}/src/engineering_platform/execution_activity.py +0 -0
  60. {engineering_platform-2.3.88 → engineering_platform-2.3.90}/src/engineering_platform/execution_context.py +0 -0
  61. {engineering_platform-2.3.88 → engineering_platform-2.3.90}/src/engineering_platform/execution_errors.py +0 -0
  62. {engineering_platform-2.3.88 → engineering_platform-2.3.90}/src/engineering_platform/execution_evidence.py +0 -0
  63. {engineering_platform-2.3.88 → engineering_platform-2.3.90}/src/engineering_platform/execution_executor.py +0 -0
  64. {engineering_platform-2.3.88 → engineering_platform-2.3.90}/src/engineering_platform/execution_finalization.py +0 -0
  65. {engineering_platform-2.3.88 → engineering_platform-2.3.90}/src/engineering_platform/execution_host_evidence.py +0 -0
  66. {engineering_platform-2.3.88 → engineering_platform-2.3.90}/src/engineering_platform/execution_lease.py +0 -0
  67. {engineering_platform-2.3.88 → engineering_platform-2.3.90}/src/engineering_platform/execution_lifecycle.py +0 -0
  68. {engineering_platform-2.3.88 → engineering_platform-2.3.90}/src/engineering_platform/execution_models.py +0 -0
  69. {engineering_platform-2.3.88 → engineering_platform-2.3.90}/src/engineering_platform/execution_readiness.py +0 -0
  70. {engineering_platform-2.3.88 → engineering_platform-2.3.90}/src/engineering_platform/execution_reporting.py +0 -0
  71. {engineering_platform-2.3.88 → engineering_platform-2.3.90}/src/engineering_platform/execution_timeout_policy.py +0 -0
  72. {engineering_platform-2.3.88 → engineering_platform-2.3.90}/src/engineering_platform/execution_timing.py +0 -0
  73. {engineering_platform-2.3.88 → engineering_platform-2.3.90}/src/engineering_platform/execution_transaction.py +0 -0
  74. {engineering_platform-2.3.88 → engineering_platform-2.3.90}/src/engineering_platform/external_producer_binding.py +0 -0
  75. {engineering_platform-2.3.88 → engineering_platform-2.3.90}/src/engineering_platform/file_inbox.py +0 -0
  76. {engineering_platform-2.3.88 → engineering_platform-2.3.90}/src/engineering_platform/forensic_attribution.py +0 -0
  77. {engineering_platform-2.3.88 → engineering_platform-2.3.90}/src/engineering_platform/forensic_attribution_v2.py +0 -0
  78. {engineering_platform-2.3.88 → engineering_platform-2.3.90}/src/engineering_platform/forensic_delta.py +0 -0
  79. {engineering_platform-2.3.88 → engineering_platform-2.3.90}/src/engineering_platform/golden_scenario.py +0 -0
  80. {engineering_platform-2.3.88 → engineering_platform-2.3.90}/src/engineering_platform/historical_dashboard_configuration.py +0 -0
  81. {engineering_platform-2.3.88 → engineering_platform-2.3.90}/src/engineering_platform/host_admin.py +0 -0
  82. {engineering_platform-2.3.88 → engineering_platform-2.3.90}/src/engineering_platform/host_preflight.py +0 -0
  83. {engineering_platform-2.3.88 → engineering_platform-2.3.90}/src/engineering_platform/installation_relocation.py +0 -0
  84. {engineering_platform-2.3.88 → engineering_platform-2.3.90}/src/engineering_platform/installation_update_activation.py +0 -0
  85. {engineering_platform-2.3.88 → engineering_platform-2.3.90}/src/engineering_platform/installation_update_admission.py +0 -0
  86. {engineering_platform-2.3.88 → engineering_platform-2.3.90}/src/engineering_platform/installation_update_backup.py +0 -0
  87. {engineering_platform-2.3.88 → engineering_platform-2.3.90}/src/engineering_platform/installation_update_composition.py +0 -0
  88. {engineering_platform-2.3.88 → engineering_platform-2.3.90}/src/engineering_platform/installation_update_executor.py +0 -0
  89. {engineering_platform-2.3.88 → engineering_platform-2.3.90}/src/engineering_platform/installation_update_migration.py +0 -0
  90. {engineering_platform-2.3.88 → engineering_platform-2.3.90}/src/engineering_platform/installation_update_operation.py +0 -0
  91. {engineering_platform-2.3.88 → engineering_platform-2.3.90}/src/engineering_platform/installation_update_plan.py +0 -0
  92. {engineering_platform-2.3.88 → engineering_platform-2.3.90}/src/engineering_platform/installation_update_preparation.py +0 -0
  93. {engineering_platform-2.3.88 → engineering_platform-2.3.90}/src/engineering_platform/investigation_ledger.py +0 -0
  94. {engineering_platform-2.3.88 → engineering_platform-2.3.90}/src/engineering_platform/legacy_inbox_migration.py +0 -0
  95. {engineering_platform-2.3.88 → engineering_platform-2.3.90}/src/engineering_platform/legacy_installation_adoption.py +0 -0
  96. {engineering_platform-2.3.88 → engineering_platform-2.3.90}/src/engineering_platform/lifecycle_worker.py +0 -0
  97. {engineering_platform-2.3.88 → engineering_platform-2.3.90}/src/engineering_platform/live_status.py +0 -0
  98. {engineering_platform-2.3.88 → engineering_platform-2.3.90}/src/engineering_platform/local_api.py +0 -0
  99. {engineering_platform-2.3.88 → engineering_platform-2.3.90}/src/engineering_platform/local_api_keychain.py +0 -0
  100. {engineering_platform-2.3.88 → engineering_platform-2.3.90}/src/engineering_platform/local_repository_binding.py +0 -0
  101. {engineering_platform-2.3.88 → engineering_platform-2.3.90}/src/engineering_platform/managed_autonomy.py +0 -0
  102. {engineering_platform-2.3.88 → engineering_platform-2.3.90}/src/engineering_platform/managed_codex_runtime.py +0 -0
  103. {engineering_platform-2.3.88 → engineering_platform-2.3.90}/src/engineering_platform/operational_installation.py +0 -0
  104. {engineering_platform-2.3.88 → engineering_platform-2.3.90}/src/engineering_platform/operational_installation_lock.py +0 -0
  105. {engineering_platform-2.3.88 → engineering_platform-2.3.90}/src/engineering_platform/operational_installation_record.py +0 -0
  106. {engineering_platform-2.3.88 → engineering_platform-2.3.90}/src/engineering_platform/owner_credential_recovery.py +0 -0
  107. {engineering_platform-2.3.88 → engineering_platform-2.3.90}/src/engineering_platform/parity_context.py +0 -0
  108. {engineering_platform-2.3.88 → engineering_platform-2.3.90}/src/engineering_platform/parity_lifecycle_dispatcher.py +0 -0
  109. {engineering_platform-2.3.88 → engineering_platform-2.3.90}/src/engineering_platform/platform_admin.py +0 -0
  110. {engineering_platform-2.3.88 → engineering_platform-2.3.90}/src/engineering_platform/platform_api.py +0 -0
  111. {engineering_platform-2.3.88 → engineering_platform-2.3.90}/src/engineering_platform/platform_bootstrap.py +0 -0
  112. {engineering_platform-2.3.88 → engineering_platform-2.3.90}/src/engineering_platform/platform_components.py +0 -0
  113. {engineering_platform-2.3.88 → engineering_platform-2.3.90}/src/engineering_platform/pr_check_repair.py +0 -0
  114. {engineering_platform-2.3.88 → engineering_platform-2.3.90}/src/engineering_platform/pr_evidence_backfill.py +0 -0
  115. {engineering_platform-2.3.88 → engineering_platform-2.3.90}/src/engineering_platform/producer.py +0 -0
  116. {engineering_platform-2.3.88 → engineering_platform-2.3.90}/src/engineering_platform/product_installation_readback.py +0 -0
  117. {engineering_platform-2.3.88 → engineering_platform-2.3.90}/src/engineering_platform/project_agent.py +0 -0
  118. {engineering_platform-2.3.88 → engineering_platform-2.3.90}/src/engineering_platform/project_agent_service.py +0 -0
  119. {engineering_platform-2.3.88 → engineering_platform-2.3.90}/src/engineering_platform/project_topology.py +0 -0
  120. {engineering_platform-2.3.88 → engineering_platform-2.3.90}/src/engineering_platform/prompt_history.py +0 -0
  121. {engineering_platform-2.3.88 → engineering_platform-2.3.90}/src/engineering_platform/provider_context.py +0 -0
  122. {engineering_platform-2.3.88 → engineering_platform-2.3.90}/src/engineering_platform/provider_context_benchmark.py +0 -0
  123. {engineering_platform-2.3.88 → engineering_platform-2.3.90}/src/engineering_platform/provider_context_scope.py +0 -0
  124. {engineering_platform-2.3.88 → engineering_platform-2.3.90}/src/engineering_platform/provider_interruption.py +0 -0
  125. {engineering_platform-2.3.88 → engineering_platform-2.3.90}/src/engineering_platform/provider_process_identity.py +0 -0
  126. {engineering_platform-2.3.88 → engineering_platform-2.3.90}/src/engineering_platform/provider_readiness.py +0 -0
  127. {engineering_platform-2.3.88 → engineering_platform-2.3.90}/src/engineering_platform/provider_recovery.py +0 -0
  128. {engineering_platform-2.3.88 → engineering_platform-2.3.90}/src/engineering_platform/provider_usage.py +0 -0
  129. {engineering_platform-2.3.88 → engineering_platform-2.3.90}/src/engineering_platform/providers.py +0 -0
  130. {engineering_platform-2.3.88 → engineering_platform-2.3.90}/src/engineering_platform/qualification.py +0 -0
  131. {engineering_platform-2.3.88 → engineering_platform-2.3.90}/src/engineering_platform/qualification_runtime.py +0 -0
  132. {engineering_platform-2.3.88 → engineering_platform-2.3.90}/src/engineering_platform/recommendation_handoff.py +0 -0
  133. {engineering_platform-2.3.88 → engineering_platform-2.3.90}/src/engineering_platform/reconciliation_adoption.py +0 -0
  134. {engineering_platform-2.3.88 → engineering_platform-2.3.90}/src/engineering_platform/release_operation.py +0 -0
  135. {engineering_platform-2.3.88 → engineering_platform-2.3.90}/src/engineering_platform/report_analysis.py +0 -0
  136. {engineering_platform-2.3.88 → engineering_platform-2.3.90}/src/engineering_platform/repository_attachment.py +0 -0
  137. {engineering_platform-2.3.88 → engineering_platform-2.3.90}/src/engineering_platform/repository_handoff.py +0 -0
  138. {engineering_platform-2.3.88 → engineering_platform-2.3.90}/src/engineering_platform/resources.py +0 -0
  139. {engineering_platform-2.3.88 → engineering_platform-2.3.90}/src/engineering_platform/reviewer_evidence.py +0 -0
  140. {engineering_platform-2.3.88 → engineering_platform-2.3.90}/src/engineering_platform/revision_binding.py +0 -0
  141. {engineering_platform-2.3.88 → engineering_platform-2.3.90}/src/engineering_platform/schemas/producer-readback-v1.2.schema.json +0 -0
  142. {engineering_platform-2.3.88 → engineering_platform-2.3.90}/src/engineering_platform/schemas/producer-readback-v1.schema.json +0 -0
  143. {engineering_platform-2.3.88 → engineering_platform-2.3.90}/src/engineering_platform/schemas/repository-attachment.schema.json +0 -0
  144. {engineering_platform-2.3.88 → engineering_platform-2.3.90}/src/engineering_platform/server_console_services.py +0 -0
  145. {engineering_platform-2.3.88 → engineering_platform-2.3.90}/src/engineering_platform/server_relay.py +0 -0
  146. {engineering_platform-2.3.88 → engineering_platform-2.3.90}/src/engineering_platform/server_service.py +0 -0
  147. {engineering_platform-2.3.88 → engineering_platform-2.3.90}/src/engineering_platform/status_model.py +0 -0
  148. {engineering_platform-2.3.88 → engineering_platform-2.3.90}/src/engineering_platform/status_reconciliation.py +0 -0
  149. {engineering_platform-2.3.88 → engineering_platform-2.3.90}/src/engineering_platform/storage.py +0 -0
  150. {engineering_platform-2.3.88 → engineering_platform-2.3.90}/src/engineering_platform/submission_cli.py +0 -0
  151. {engineering_platform-2.3.88 → engineering_platform-2.3.90}/src/engineering_platform/submission_intake.py +0 -0
  152. {engineering_platform-2.3.88 → engineering_platform-2.3.90}/src/engineering_platform/system_installation_topology.py +0 -0
  153. {engineering_platform-2.3.88 → engineering_platform-2.3.90}/src/engineering_platform/system_server_service.py +0 -0
  154. {engineering_platform-2.3.88 → engineering_platform-2.3.90}/src/engineering_platform/telemetry.py +0 -0
  155. {engineering_platform-2.3.88 → engineering_platform-2.3.90}/src/engineering_platform/telemetry_contract.py +0 -0
  156. {engineering_platform-2.3.88 → engineering_platform-2.3.90}/src/engineering_platform/telemetry_export.py +0 -0
  157. {engineering_platform-2.3.88 → engineering_platform-2.3.90}/src/engineering_platform/telemetry_metrics.py +0 -0
  158. {engineering_platform-2.3.88 → engineering_platform-2.3.90}/src/engineering_platform/terminal_evidence_reconciliation.py +0 -0
  159. {engineering_platform-2.3.88 → engineering_platform-2.3.90}/src/engineering_platform/validation_identity.py +0 -0
  160. {engineering_platform-2.3.88 → engineering_platform-2.3.90}/src/engineering_platform/validation_profile.py +0 -0
  161. {engineering_platform-2.3.88 → engineering_platform-2.3.90}/src/engineering_platform/version_preparation_delivery.py +0 -0
  162. {engineering_platform-2.3.88 → engineering_platform-2.3.90}/src/engineering_platform/workspace_preflight.py +0 -0
  163. {engineering_platform-2.3.88 → engineering_platform-2.3.90}/src/engineering_platform/worktree_provenance.py +0 -0
  164. {engineering_platform-2.3.88 → engineering_platform-2.3.90}/src/engineering_platform/worktree_tooling.py +0 -0
  165. {engineering_platform-2.3.88 → engineering_platform-2.3.90}/src/engineering_platform.egg-info/SOURCES.txt +0 -0
  166. {engineering_platform-2.3.88 → engineering_platform-2.3.90}/src/engineering_platform.egg-info/dependency_links.txt +0 -0
  167. {engineering_platform-2.3.88 → engineering_platform-2.3.90}/src/engineering_platform.egg-info/entry_points.txt +0 -0
  168. {engineering_platform-2.3.88 → engineering_platform-2.3.90}/src/engineering_platform.egg-info/top_level.txt +0 -0
  169. {engineering_platform-2.3.88 → engineering_platform-2.3.90}/tests/test_adaptive_action_sizing_design.py +0 -0
  170. {engineering_platform-2.3.88 → engineering_platform-2.3.90}/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.88
3
+ Version: 2.3.90
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.88"
7
+ version = "2.3.90"
8
8
  description = "Local-first Engineering Platform execution operations runtime"
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.14,<3.15"
@@ -3,7 +3,7 @@
3
3
  "platform": {
4
4
  "id": "engineering-platform",
5
5
  "name": "Engineering Platform",
6
- "version": "2.3.88",
6
+ "version": "2.3.90",
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.88",
4
+ "dashboard_version": "2.3.90",
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.88",
9
+ "platform_version": "2.3.90",
10
10
  "report_format": 2,
11
- "runner_version": "2.3.88",
11
+ "runner_version": "2.3.90",
12
12
  "status_model": 1,
13
13
  "storage_schema": 45,
14
- "watcher_version": "2.3.88"
14
+ "watcher_version": "2.3.90"
15
15
  }
@@ -36,7 +36,7 @@ from .storage import sqlite_connection
36
36
 
37
37
  PROFILE = "EP_CENTRAL_OPERATIONAL_HISTORY_V1"
38
38
  PLAN_VERSION = 2
39
- SCHEMA_VERSION = 71
39
+ SCHEMA_VERSION = 72
40
40
  _OPERATION = re.compile(r"[A-Za-z0-9][A-Za-z0-9._-]{7,127}")
41
41
  _INSTANCE_ID = re.compile(
42
42
  r"[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}"
@@ -66,6 +66,7 @@ INSTALLATION_AND_CONFIGURATION = frozenset({
66
66
  "ep_project_registrations", "ep_repository_registrations",
67
67
  "ep_agent_registrations", "ep_agent_repository_attachments",
68
68
  "ep_local_repository_bindings", "ep_external_producer_bindings",
69
+ "ep_assurance_target_selections",
69
70
  "execution_migration_provenance",
70
71
  })
71
72
  SECURITY_AND_AUTHORITY_LEDGER = frozenset({
@@ -222,6 +223,31 @@ def _implementation_digest() -> str:
222
223
  })
223
224
 
224
225
 
226
+ # The published 2.3.89 reset implementation can leave an operation at
227
+ # ARTIFACTS_ARCHIVED when operational engineering_metadata rows are present.
228
+ # Permit only that exact installed implementation to cross the fixed apply
229
+ # boundary; the plan, backup, identity, schema, generation and fences are still
230
+ # verified by apply before any database effect.
231
+ _ARCHIVED_RESET_COMPATIBILITY = frozenset({
232
+ ("2.3.89", "UNAVAILABLE_IN_INSTALLED_PACKAGE",
233
+ "sha256:17ec03725e15672ef9511382e0b3d2c72e3f502823a35b89bea170882f4a14b8"),
234
+ })
235
+
236
+
237
+ def _implementation_binding_valid(plan: dict[str, object], state: str) -> bool:
238
+ binding = (
239
+ plan.get("product_version"), plan.get("implementation_source_revision"),
240
+ plan.get("implementation_digest"),
241
+ )
242
+ current = (
243
+ CURRENT_PLATFORM_VERSION, _implementation_source_revision(),
244
+ _implementation_digest(),
245
+ )
246
+ return binding == current or (
247
+ state == "ARTIFACTS_ARCHIVED" and binding in _ARCHIVED_RESET_COMPATIBILITY
248
+ )
249
+
250
+
225
251
  def _implementation_source_revision() -> str:
226
252
  try:
227
253
  module = Path(__file__).resolve()
@@ -2301,11 +2327,7 @@ def apply(data_root: Path, *, operation_id: str, plan_digest: str) -> dict[str,
2301
2327
  raise OperationalResetError("PRESERVED_BINDINGS_CHANGED")
2302
2328
  if _schema_objects(connection) != plan["schema_objects"]:
2303
2329
  raise OperationalResetError("SCHEMA_OBJECTS_CHANGED")
2304
- if (
2305
- plan.get("product_version") != CURRENT_PLATFORM_VERSION
2306
- or plan.get("implementation_source_revision") != _implementation_source_revision()
2307
- or plan.get("implementation_digest") != _implementation_digest()
2308
- ):
2330
+ if not _implementation_binding_valid(plan, "ARTIFACTS_ARCHIVED"):
2309
2331
  raise OperationalResetError("IMPLEMENTATION_PROVENANCE_CHANGED")
2310
2332
  generation_before = int(connection.execute(
2311
2333
  "SELECT generation FROM ep_operational_dataset_state WHERE singleton=1"
@@ -2315,11 +2337,15 @@ def apply(data_root: Path, *, operation_id: str, plan_digest: str) -> dict[str,
2315
2337
  # Immutable-evidence and maintenance-block triggers are removed and
2316
2338
  # recreated inside this one uncommitted transaction. Other writers
2317
2339
  # cannot observe an unfenced schema window.
2340
+ reset_trigger_tables = (
2341
+ OPERATIONAL_HISTORY | DERIVED_CACHE_OR_PROJECTION
2342
+ | {"engineering_metadata"}
2343
+ )
2318
2344
  triggers = [
2319
2345
  (str(name), str(sql)) for name, sql in connection.execute(
2320
2346
  "SELECT name,sql FROM sqlite_master WHERE type='trigger' AND tbl_name IN ("
2321
- + ",".join("?" for _ in (OPERATIONAL_HISTORY | DERIVED_CACHE_OR_PROJECTION)) + ")",
2322
- tuple(sorted(OPERATIONAL_HISTORY | DERIVED_CACHE_OR_PROJECTION)),
2347
+ + ",".join("?" for _ in reset_trigger_tables) + ")",
2348
+ tuple(sorted(reset_trigger_tables)),
2323
2349
  ).fetchall() if sql is not None
2324
2350
  ]
2325
2351
  for name, _sql in triggers:
@@ -3898,12 +3898,14 @@ First implementation pull-request publication gate:
3898
3898
  try:
3899
3899
  with sqlite_connection(self.store.central_database) as connection:
3900
3900
  grant = merge_delegation.load(connection, state.merge_delegation_id)
3901
+ selection_current = bool(grant and merge_delegation.target_selection_permits(connection, grant))
3901
3902
  except (EngineeringStorageError, sqlite3.Error):
3902
3903
  return False
3903
- return bool(grant and grant.assurance_profile_id == "qualification-autonomous-qs"
3904
- and grant.assurance_profile_revision == "1"
3904
+ return bool(grant and selection_current and grant.assurance_profile_revision == "1"
3905
3905
  and grant.github_repository == state.repository
3906
- == merge_delegation.AUTONOMOUS_ASSURANCE_REPOSITORY)
3906
+ and (grant.assurance_profile_id == merge_delegation.REPOSITORY_ASSURANCE_ID
3907
+ or (grant.assurance_profile_id == "qualification-autonomous-qs"
3908
+ and state.repository == merge_delegation.AUTONOMOUS_ASSURANCE_REPOSITORY)))
3907
3909
 
3908
3910
  def _recover_autonomous_pr_assurance(
3909
3911
  self, state: TransactionState, candidate: PullRequestEvidence,
@@ -4013,7 +4015,8 @@ First implementation pull-request publication gate:
4013
4015
  (state.run_id,),
4014
4016
  ).fetchone()
4015
4017
  grant = merge_delegation.load(connection, state.merge_delegation_id)
4016
- if scope is None or grant is None or not isinstance(accepted, dict):
4018
+ selection_current = bool(grant and merge_delegation.target_selection_permits(connection, grant))
4019
+ if scope is None or grant is None or not selection_current or not isinstance(accepted, dict):
4017
4020
  return None
4018
4021
  if Path(str(scope[2])).resolve() != self.root.resolve():
4019
4022
  return None
@@ -4057,17 +4060,21 @@ First implementation pull-request publication gate:
4057
4060
  qualifier = getattr(self.github, "delegated_merge_qualification", None)
4058
4061
  if not callable(qualifier):
4059
4062
  return None
4060
- autonomous_profile = (
4061
- grant.assurance_profile_id == "qualification-autonomous-qs"
4062
- and grant.assurance_profile_revision == "1"
4063
- )
4063
+ autonomous_profile = (grant.assurance_profile_revision == "1" and
4064
+ grant.assurance_profile_id in {
4065
+ "qualification-autonomous-qs",
4066
+ merge_delegation.REPOSITORY_ASSURANCE_ID})
4064
4067
  if grant.assurance_profile_id or grant.assurance_profile_revision:
4065
4068
  if (not autonomous_profile
4066
- or grant.github_repository != merge_delegation.AUTONOMOUS_ASSURANCE_REPOSITORY
4069
+ or (grant.assurance_profile_id == "qualification-autonomous-qs"
4070
+ and grant.github_repository != merge_delegation.AUTONOMOUS_ASSURANCE_REPOSITORY)
4067
4071
  or not self._autonomous_merge_assurance_passes(state, expected_head)):
4068
4072
  return None
4073
+ selected_profile = (merge_delegation.REPOSITORY_ASSURANCE_PROFILE
4074
+ if grant.assurance_profile_id == merge_delegation.REPOSITORY_ASSURANCE_ID
4075
+ else merge_delegation.AUTONOMOUS_ASSURANCE_PROFILE)
4069
4076
  qualification = (qualifier(fresh.number, expected_head,
4070
- assurance_profile=merge_delegation.AUTONOMOUS_ASSURANCE_PROFILE)
4077
+ assurance_profile=selected_profile)
4071
4078
  if autonomous_profile else qualifier(fresh.number, expected_head))
4072
4079
  if (not isinstance(qualification, dict) or qualification.get("conclusion") != "PASS"
4073
4080
  or qualification.get("exact_qualified_sha") != expected_head
@@ -4094,7 +4101,8 @@ First implementation pull-request publication gate:
4094
4101
  return None
4095
4102
  with sqlite_connection(self.store.central_database) as connection:
4096
4103
  current_grant = merge_delegation.load(connection, state.merge_delegation_id)
4097
- if (current_grant is None or not current_grant.permits(
4104
+ current_selection = bool(current_grant and merge_delegation.target_selection_permits(connection, current_grant))
4105
+ if (current_grant is None or not current_selection or not current_grant.permits(
4098
4106
  project_id=str(scope[0]), repository_id=str(scope[1]),
4099
4107
  mission_id=str(accepted.get("mission_id")), mission_revision=str(mission_revision),
4100
4108
  role=state.transaction_kind, base_branch="main",
@@ -4108,7 +4116,9 @@ First implementation pull-request publication gate:
4108
4116
  policy_reader = getattr(self.github, "_autonomous_effective_policy", None)
4109
4117
  if not callable(policy_reader):
4110
4118
  return None
4111
- current_policy = policy_reader()
4119
+ current_policy = (policy_reader(assurance_profile=selected_profile)
4120
+ if grant.assurance_profile_id == merge_delegation.REPOSITORY_ASSURANCE_ID
4121
+ else policy_reader())
4112
4122
  if (not isinstance(current_policy, dict)
4113
4123
  or current_policy.get("digest") != current_grant.assurance_policy_digest):
4114
4124
  return None
@@ -4118,7 +4128,21 @@ First implementation pull-request publication gate:
4118
4128
  attempted = replace(state, delegated_merge_attempt=attempt,
4119
4129
  delegated_merge_actor_reference=None)
4120
4130
  self.store.save(attempted)
4121
- self.github.merge(fresh.number, expected_head_sha=expected_head)
4131
+ # Linearize revocation against the external merge call. A revoke
4132
+ # committed before this lock is observed; one waiting for this lock
4133
+ # can only commit after the bounded call has returned.
4134
+ with sqlite_connection(self.store.central_database) as authority:
4135
+ authority.execute("BEGIN IMMEDIATE")
4136
+ final_grant = merge_delegation.load(authority, state.merge_delegation_id)
4137
+ if (final_grant is None or not final_grant.permits(
4138
+ project_id=str(scope[0]), repository_id=str(scope[1]),
4139
+ mission_id=str(accepted.get("mission_id")),
4140
+ mission_revision=str(mission_revision),
4141
+ role=state.transaction_kind, base_branch="main")
4142
+ or not merge_delegation.target_selection_permits(authority, final_grant)
4143
+ or final_grant != current_grant):
4144
+ raise ValueError("merge authority changed before the external call")
4145
+ self.github.merge(fresh.number, expected_head_sha=expected_head)
4122
4146
  merged = self.github.pull_request(fresh.number)
4123
4147
  self.repository.refresh_main_reference(self.root)
4124
4148
  if (merged.state == "MERGED" and merged.head_sha == expected_head
@@ -360,10 +360,17 @@ class GhCliClient:
360
360
  except RuntimeError as error:
361
361
  raise RunnerError("Pull request Markdown could not be normalized.") from error
362
362
  return True
363
- def _autonomous_effective_policy(self) -> dict[str, object]:
363
+ def _autonomous_effective_policy(self, *, assurance_profile: str = "qualification-autonomous-qs@1") -> dict[str, object]:
364
364
  """Read all active main rules and classic protection without inferring zero from errors."""
365
- if self.repository != "pcvantol/forge-mission-qualification":
365
+ if (assurance_profile == "qualification-autonomous-qs@1"
366
+ and self.repository != "pcvantol/forge-mission-qualification"):
366
367
  raise RunnerError("Autonomous assurance profile has a different repository scope.")
368
+ if assurance_profile not in {"qualification-autonomous-qs@1", "repository-autonomous-qs@1"}:
369
+ raise RunnerError("Unknown delegated merge assurance profile.")
370
+ repository_profile = assurance_profile == "repository-autonomous-qs@1"
371
+ if repository_profile and (not isinstance(self.repository, str)
372
+ or re.fullmatch(r"[A-Za-z0-9_.-]+/[A-Za-z0-9_.-]+", self.repository) is None):
373
+ raise RunnerError("Autonomous assurance requires an exact repository.")
367
374
  try:
368
375
  raw_rules = json.loads(self._github("api", f"repos/{self.repository}/rules/branches/main?per_page=100"))
369
376
  except (RuntimeError, json.JSONDecodeError) as error:
@@ -415,6 +422,13 @@ class GhCliClient:
415
422
  required_apps: dict[str, set[int]] = {}
416
423
  strict_values: list[bool] = []
417
424
  required_approvals: list[int] = []
425
+ required_thread_resolution = False
426
+ classic_conversations = classic.get("required_conversation_resolution") if classic else None
427
+ if repository_profile and classic_conversations is not None:
428
+ if (not isinstance(classic_conversations, dict)
429
+ or not isinstance(classic_conversations.get("enabled"), bool)):
430
+ raise RunnerError("Classic review-thread policy is incomplete.")
431
+ required_thread_resolution = classic_conversations["enabled"]
418
432
  classic_checks = classic.get("required_status_checks") if classic else None
419
433
  if classic_checks is not None:
420
434
  if not isinstance(classic_checks, dict) or not isinstance(classic_checks.get("strict"), bool):
@@ -442,9 +456,10 @@ class GhCliClient:
442
456
  if (isinstance(count, bool) or not isinstance(count, int) or count < 0
443
457
  or classic_reviews.get("require_code_owner_reviews") is True
444
458
  or classic_reviews.get("require_last_push_approval") is True
445
- or classic_reviews.get("required_review_thread_resolution") is True):
459
+ or (classic_reviews.get("required_review_thread_resolution") is True and not repository_profile)):
446
460
  raise RunnerError("Classic review policy requires unsupported or unknown approval evidence.")
447
461
  required_approvals.append(count)
462
+ required_thread_resolution |= classic_reviews.get("required_review_thread_resolution") is True
448
463
  supported = {"required_status_checks", "pull_request", "non_fast_forward", "deletion", "required_linear_history"}
449
464
  for rule in verified_rules:
450
465
  rule_type, parameters = rule.get("type"), rule.get("parameters")
@@ -467,13 +482,28 @@ class GhCliClient:
467
482
  elif rule_type == "pull_request":
468
483
  if not isinstance(parameters, dict):
469
484
  raise RunnerError("Ruleset pull-request policy is incomplete.")
485
+ if repository_profile:
486
+ supported_parameters = {
487
+ "allowed_merge_methods", "dismiss_stale_reviews_on_push",
488
+ "require_code_owner_review", "require_extra_approval_for_unattributed_changes",
489
+ "require_last_push_approval", "required_approving_review_count",
490
+ "required_review_thread_resolution", "required_reviewers",
491
+ }
492
+ if (set(parameters) - supported_parameters
493
+ or parameters.get("require_extra_approval_for_unattributed_changes") is True
494
+ or parameters.get("require_last_push_approval") is True
495
+ or ("allowed_merge_methods" in parameters
496
+ and (not isinstance(parameters["allowed_merge_methods"], list)
497
+ or "squash" not in parameters["allowed_merge_methods"]))):
498
+ raise RunnerError("Ruleset pull-request policy has an unsupported merge condition.")
470
499
  count = parameters.get("required_approving_review_count")
471
500
  if (isinstance(count, bool) or not isinstance(count, int) or count < 0
472
501
  or parameters.get("require_code_owner_review") is True
473
- or parameters.get("required_review_thread_resolution") is True
502
+ or (parameters.get("required_review_thread_resolution") is True and not repository_profile)
474
503
  or parameters.get("required_reviewers")):
475
504
  raise RunnerError("Ruleset review policy requires unsupported or unknown approval evidence.")
476
505
  required_approvals.append(count)
506
+ required_thread_resolution |= parameters.get("required_review_thread_resolution") is True
477
507
  if not required_checks or not strict_values or not all(strict_values):
478
508
  raise RunnerError("Autonomous assurance requires strict protected status checks.")
479
509
  effective_rule_types = {rule.get("type") for rule in verified_rules}
@@ -484,16 +514,74 @@ class GhCliClient:
484
514
  policy = {"source": "github-effective-main", "classic": classic,
485
515
  "rulesets": verified_rules, "required_checks": sorted(required_checks),
486
516
  "required_apps": {name: sorted(apps) for name, apps in sorted(required_apps.items())},
487
- "required_approvals": max(required_approvals, default=0), "strict_checks": True}
488
- policy["digest"] = "sha256:" + hashlib.sha256(json.dumps(policy, sort_keys=True, separators=(",", ":")).encode()).hexdigest()
517
+ "required_approvals": max(required_approvals, default=0), "strict_checks": True,
518
+ "required_thread_resolution": required_thread_resolution}
519
+ if repository_profile:
520
+ # GitHub embeds observation URLs and other changing metadata in
521
+ # protection responses. Bind only the effective rule semantics.
522
+ classic_binding = None
523
+ if classic is not None:
524
+ classic_binding = {
525
+ "enforce_admins": classic["enforce_admins"]["enabled"],
526
+ "allow_force_pushes": classic["allow_force_pushes"]["enabled"],
527
+ "allow_deletions": classic["allow_deletions"]["enabled"],
528
+ "required_conversation_resolution": None if classic_conversations is None
529
+ else classic_conversations["enabled"],
530
+ "required_status_checks": None if classic_checks is None else {
531
+ "strict": classic_checks["strict"],
532
+ "contexts": sorted(classic_checks["contexts"]),
533
+ "checks": sorted(({"context": item["context"],
534
+ "app_id": item.get("app_id")}
535
+ for item in classic_checks["checks"]),
536
+ key=lambda item: (item["context"], str(item["app_id"]))),
537
+ },
538
+ "required_pull_request_reviews": None if classic_reviews is None else {
539
+ "required_approving_review_count": classic_reviews["required_approving_review_count"],
540
+ "dismiss_stale_reviews": classic_reviews.get("dismiss_stale_reviews", False),
541
+ "require_code_owner_reviews": classic_reviews.get("require_code_owner_reviews", False),
542
+ "require_last_push_approval": classic_reviews.get("require_last_push_approval", False),
543
+ "required_review_thread_resolution": classic_reviews.get("required_review_thread_resolution", False),
544
+ },
545
+ }
546
+ stable_rules = []
547
+ for item in verified_rules:
548
+ parameters = item.get("parameters")
549
+ if isinstance(parameters, dict):
550
+ parameters = dict(parameters)
551
+ if item["type"] == "required_status_checks":
552
+ parameters["required_status_checks"] = sorted(
553
+ parameters["required_status_checks"],
554
+ key=lambda check: (check["context"], str(check.get("integration_id"))))
555
+ elif item["type"] == "pull_request" and "allowed_merge_methods" in parameters:
556
+ parameters["allowed_merge_methods"] = sorted(parameters["allowed_merge_methods"])
557
+ stable_rules.append({"ruleset_id": item["ruleset_id"],
558
+ "type": item["type"], "parameters": parameters})
559
+ policy_binding = {
560
+ "version": "repository-effective-main/v1",
561
+ "repository": self.repository,
562
+ "classic": classic_binding,
563
+ "rulesets": sorted(stable_rules,
564
+ key=lambda item: (item["ruleset_id"], item["type"],
565
+ json.dumps(item["parameters"], sort_keys=True))),
566
+ "required_checks": policy["required_checks"],
567
+ "required_apps": policy["required_apps"],
568
+ "required_approvals": policy["required_approvals"],
569
+ "required_thread_resolution": required_thread_resolution,
570
+ "strict_checks": True,
571
+ }
572
+ else:
573
+ # Preserve the original synthetic profile's immutable digest semantics.
574
+ policy_binding = {key: value for key, value in policy.items()
575
+ if key != "required_thread_resolution"}
576
+ policy["digest"] = "sha256:" + hashlib.sha256(json.dumps(policy_binding, sort_keys=True, separators=(",", ":")).encode()).hexdigest()
489
577
  return policy
490
578
 
491
579
  def delegated_merge_qualification(self, number: int, head_sha: str, *, assurance_profile: str | None = None) -> dict[str, object]:
492
580
  """Fresh protected checks and independent head-bound GitHub reviews."""
493
- autonomous = assurance_profile == "qualification-autonomous-qs@1"
581
+ autonomous = assurance_profile in {"qualification-autonomous-qs@1", "repository-autonomous-qs@1"}
494
582
  if assurance_profile is not None and not autonomous:
495
583
  raise RunnerError("Unknown delegated merge assurance profile.")
496
- policy = self._autonomous_effective_policy() if autonomous else None
584
+ policy = self._autonomous_effective_policy(assurance_profile=assurance_profile) if autonomous else None
497
585
  if policy is None:
498
586
  checks = self.qualification_for_exact_head(number, head_sha)
499
587
  else:
@@ -506,12 +594,24 @@ class GhCliClient:
506
594
  if not isinstance(raw, dict) or raw.get("headRefOid") != head_sha or raw.get("baseRefName") != "main":
507
595
  raise RunnerError("Autonomous PR qualification has a changed head or base.")
508
596
  rollup = raw.get("statusCheckRollup")
509
- if not isinstance(rollup, list) or not rollup or any(
510
- not isinstance(item, dict) or item.get("status") != "COMPLETED"
511
- or item.get("conclusion") not in {"SUCCESS", "NEUTRAL", "SKIPPED"} for item in rollup
512
- ):
597
+ if not isinstance(rollup, list) or not rollup:
513
598
  raise RunnerError("Autonomous PR required checks are incomplete or failed.")
514
- observed = {item.get("name") for item in rollup}
599
+ observed: set[str] = set()
600
+ for item in rollup:
601
+ if not isinstance(item, dict):
602
+ raise RunnerError("Autonomous PR required checks are incomplete or failed.")
603
+ if item.get("__typename") == "StatusContext" or (
604
+ "context" in item and "state" in item and "name" not in item
605
+ ):
606
+ name = item.get("context")
607
+ passed = item.get("state") == "SUCCESS"
608
+ else:
609
+ name = item.get("name")
610
+ passed = (item.get("status") == "COMPLETED" and
611
+ item.get("conclusion") in {"SUCCESS", "NEUTRAL", "SKIPPED"})
612
+ if not isinstance(name, str) or not name or not passed:
613
+ raise RunnerError("Autonomous PR required checks are incomplete or failed.")
614
+ observed.add(name)
515
615
  if not set(policy["required_checks"]) <= observed:
516
616
  raise RunnerError("Autonomous PR is missing effective required checks.")
517
617
  if policy["required_apps"]:
@@ -562,17 +662,37 @@ class GhCliClient:
562
662
  raise RunnerError("Delegated merge requires protected independent review policy.")
563
663
  latest: dict[str, tuple[str, str]] = {}
564
664
  for review in reviews:
565
- if not isinstance(review, dict):
566
- continue
567
- reviewer = (review.get("user") or {}).get("login")
568
- if isinstance(reviewer, str) and reviewer:
569
- latest[reviewer] = (str(review.get("state")), str(review.get("commit_id")))
665
+ if not isinstance(review, dict) or not isinstance(review.get("user"), dict):
666
+ raise RunnerError("Delegated merge review evidence is incomplete.")
667
+ reviewer = review["user"].get("login")
668
+ decision = review.get("state")
669
+ if not isinstance(reviewer, str) or not reviewer or not isinstance(decision, str):
670
+ raise RunnerError("Delegated merge review evidence is incomplete.")
671
+ latest[reviewer] = (decision, str(review.get("commit_id")))
672
+ if any(decision == "CHANGES_REQUESTED" for decision, _commit in latest.values()):
673
+ raise RunnerError("Delegated merge has an unresolved changes-requested review.")
570
674
  approvals = sorted(
571
675
  reviewer for reviewer, (decision, commit) in latest.items()
572
676
  if reviewer != author and decision == "APPROVED" and commit == head_sha
573
677
  )
574
678
  if len(approvals) < required:
575
679
  raise RunnerError("Delegated merge lacks required independent approvals for this exact head.")
680
+ if policy is not None and policy["required_thread_resolution"]:
681
+ owner, repository_name = self.repository.split("/", 1)
682
+ query = ("query { repository(owner:\"" + owner + "\",name:\"" + repository_name
683
+ + "\") { pullRequest(number:" + str(number)
684
+ + ") { reviewThreads(first:100) { totalCount nodes { isResolved } } } } }")
685
+ try:
686
+ response = json.loads(self._github("api", "graphql", "-f", "query=" + query))
687
+ threads = response["data"]["repository"]["pullRequest"]["reviewThreads"]
688
+ except (RuntimeError, ValueError, KeyError, TypeError) as error:
689
+ raise RunnerError("Required review-thread resolution could not be read.") from error
690
+ nodes = threads.get("nodes") if isinstance(threads, dict) else None
691
+ if (not isinstance(nodes, list) or not isinstance(threads.get("totalCount"), int)
692
+ or threads["totalCount"] != len(nodes) or len(nodes) >= 100
693
+ or any(not isinstance(item, dict) or item.get("isResolved") is not True
694
+ for item in nodes)):
695
+ raise RunnerError("Delegated merge has unresolved or unreadable review threads.")
576
696
  return {**checks, "reviewers": approvals, "required_approvals": required}
577
697
 
578
698
  def merge(self, number: int, *, expected_head_sha: str | None = None) -> None: