loopx 0.4.8__py3-none-any.whl

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 (811) hide show
  1. loopx/__init__.py +5 -0
  2. loopx/agent_onboarding.py +654 -0
  3. loopx/agent_registry.py +112 -0
  4. loopx/ark_managed_agent_host.py +59 -0
  5. loopx/authority.py +805 -0
  6. loopx/benchmark.py +2875 -0
  7. loopx/benchmark_adapters/__init__.py +1 -0
  8. loopx/benchmark_adapters/agentissue.py +2644 -0
  9. loopx/benchmark_adapters/agents_last_exam.py +3998 -0
  10. loopx/benchmark_adapters/edgebench.py +322 -0
  11. loopx/benchmark_adapters/skillsbench.py +5978 -0
  12. loopx/benchmark_adapters/skillsbench_acp_failure_policy.py +143 -0
  13. loopx/benchmark_adapters/skillsbench_acp_process.py +31 -0
  14. loopx/benchmark_adapters/skillsbench_acp_relay.py +4832 -0
  15. loopx/benchmark_adapters/skillsbench_batch.py +124 -0
  16. loopx/benchmark_adapters/skillsbench_bridge_guard.py +209 -0
  17. loopx/benchmark_adapters/skillsbench_bridge_summary.py +203 -0
  18. loopx/benchmark_adapters/skillsbench_codex_goal_recovery.py +271 -0
  19. loopx/benchmark_adapters/skillsbench_codex_goal_trace.py +81 -0
  20. loopx/benchmark_adapters/skillsbench_codex_runtime.py +339 -0
  21. loopx/benchmark_adapters/skillsbench_dockerfile_runtime.py +467 -0
  22. loopx/benchmark_adapters/skillsbench_failure_signals.py +652 -0
  23. loopx/benchmark_adapters/skillsbench_proxy_runtime.py +327 -0
  24. loopx/benchmark_adapters/skillsbench_remote_bridge.py +402 -0
  25. loopx/benchmark_adapters/skillsbench_result_discovery.py +143 -0
  26. loopx/benchmark_adapters/skillsbench_runner_profile.py +436 -0
  27. loopx/benchmark_adapters/skillsbench_runner_source.py +99 -0
  28. loopx/benchmark_adapters/skillsbench_setup_preflight.py +771 -0
  29. loopx/benchmark_adapters/skillsbench_signals.py +15 -0
  30. loopx/benchmark_adapters/skillsbench_task_source.py +141 -0
  31. loopx/benchmark_adapters/skillsbench_turn_route.py +723 -0
  32. loopx/benchmark_adapters/skillsbench_turn_runtime.py +1069 -0
  33. loopx/benchmark_adapters/skillsbench_typed_repair.py +689 -0
  34. loopx/benchmark_adapters/skillsbench_uv_cache.py +111 -0
  35. loopx/benchmark_adapters/skillsbench_verifier_bootstrap.py +227 -0
  36. loopx/benchmark_adapters/skillsbench_verifier_cache.py +138 -0
  37. loopx/benchmark_adapters/terminal_bench.py +10078 -0
  38. loopx/benchmark_case_analysis.py +1276 -0
  39. loopx/benchmark_case_state.py +1079 -0
  40. loopx/benchmark_core/__init__.py +239 -0
  41. loopx/benchmark_core/adapter.py +84 -0
  42. loopx/benchmark_core/artifacts.py +517 -0
  43. loopx/benchmark_core/attempts.py +199 -0
  44. loopx/benchmark_core/container_exec.py +216 -0
  45. loopx/benchmark_core/io.py +68 -0
  46. loopx/benchmark_core/lifecycle.py +211 -0
  47. loopx/benchmark_core/loop_protocol.py +689 -0
  48. loopx/benchmark_core/observable_handles.py +348 -0
  49. loopx/benchmark_core/parity.py +256 -0
  50. loopx/benchmark_core/remote_closeout.py +482 -0
  51. loopx/benchmark_core/rounds.py +215 -0
  52. loopx/benchmark_core/route_profile.py +509 -0
  53. loopx/benchmark_core/run_permissions.py +206 -0
  54. loopx/benchmark_core/split_control.py +925 -0
  55. loopx/benchmark_core/turn_fidelity.py +326 -0
  56. loopx/benchmark_ledger.py +3793 -0
  57. loopx/benchmark_ledger_countability.py +372 -0
  58. loopx/benchmark_ledger_current.py +724 -0
  59. loopx/benchmark_trajectory.py +405 -0
  60. loopx/benchmarks/__init__.py +1 -0
  61. loopx/benchmarks/qualification/__init__.py +1 -0
  62. loopx/benchmarks/qualification/release_outcome_baseline.py +360 -0
  63. loopx/benchmarks/read_models/__init__.py +1 -0
  64. loopx/benchmarks/read_models/benchmark_attempt_accounting.py +53 -0
  65. loopx/benchmarks/read_models/benchmark_comparison.py +414 -0
  66. loopx/benchmarks/read_models/benchmark_event_timeline.py +113 -0
  67. loopx/benchmarks/read_models/benchmark_experiment_report.py +475 -0
  68. loopx/benchmarks/read_models/benchmark_learning_ledger.py +137 -0
  69. loopx/benchmarks/read_models/benchmark_lifecycle_contracts.py +228 -0
  70. loopx/benchmarks/read_models/benchmark_projection.py +723 -0
  71. loopx/benchmarks/read_models/benchmark_result.py +146 -0
  72. loopx/benchmarks/read_models/benchmark_run_execution_contract.py +116 -0
  73. loopx/benchmarks/read_models/benchmark_run_failure.py +157 -0
  74. loopx/benchmarks/read_models/benchmark_run_metrics.py +213 -0
  75. loopx/benchmarks/read_models/benchmark_run_post_execution.py +635 -0
  76. loopx/benchmarks/read_models/benchmark_run_pre_execution.py +541 -0
  77. loopx/benchmarks/read_models/benchmark_status_compaction.py +1255 -0
  78. loopx/benchmarks/read_models/benchmark_status_runner.py +780 -0
  79. loopx/benchmarks/read_models/goal_start_control_score.py +857 -0
  80. loopx/benchmarks/read_models/skillsbench_post_run_debug.py +746 -0
  81. loopx/benchmarks/read_models/skillsbench_verifier_attribution.py +269 -0
  82. loopx/bootstrap.py +1116 -0
  83. loopx/bootstrap_command_pack.py +2167 -0
  84. loopx/boundary_authority.py +199 -0
  85. loopx/canary/__init__.py +1 -0
  86. loopx/canary/maintainability_ratchet.py +800 -0
  87. loopx/canary/planner.py +1984 -0
  88. loopx/canary/premerge.py +1130 -0
  89. loopx/canary/qualification_profiles.py +309 -0
  90. loopx/canary/quality_surface_catalog.py +838 -0
  91. loopx/canary/release_profiles.py +51 -0
  92. loopx/canary/runner.py +1107 -0
  93. loopx/canary/smoke_health.py +581 -0
  94. loopx/canary/smoke_profiles.py +212 -0
  95. loopx/capabilities/__init__.py +0 -0
  96. loopx/capabilities/agent_turn_recall/__init__.py +17 -0
  97. loopx/capabilities/agent_turn_recall/cli.py +369 -0
  98. loopx/capabilities/agent_turn_recall/core.py +296 -0
  99. loopx/capabilities/auto_research/__init__.py +16 -0
  100. loopx/capabilities/auto_research/bootstrap_contract.py +157 -0
  101. loopx/capabilities/auto_research/cli.py +1468 -0
  102. loopx/capabilities/auto_research/core.py +11 -0
  103. loopx/capabilities/auto_research/defaults.py +79 -0
  104. loopx/capabilities/auto_research/demo_e2e.py +1848 -0
  105. loopx/capabilities/auto_research/demo_supervisor.py +186 -0
  106. loopx/capabilities/auto_research/evidence_packet.py +767 -0
  107. loopx/capabilities/auto_research/human_view.py +794 -0
  108. loopx/capabilities/auto_research/kernel.py +191 -0
  109. loopx/capabilities/auto_research/knn_demo_workspace.py +322 -0
  110. loopx/capabilities/auto_research/live_evidence.py +248 -0
  111. loopx/capabilities/auto_research/preset.py +176 -0
  112. loopx/capabilities/auto_research/research_state.py +1085 -0
  113. loopx/capabilities/auto_research/role_profiles.py +394 -0
  114. loopx/capabilities/auto_research/rollout_append.py +97 -0
  115. loopx/capabilities/auto_research/terminal_result_contract.py +422 -0
  116. loopx/capabilities/auto_research/terminal_result_projection.py +171 -0
  117. loopx/capabilities/auto_research/terminal_result_query.py +233 -0
  118. loopx/capabilities/auto_research/terminal_results.py +349 -0
  119. loopx/capabilities/auto_research/user_contract.py +190 -0
  120. loopx/capabilities/auto_research/worker_loop.py +163 -0
  121. loopx/capabilities/auto_research/worker_runtime.py +777 -0
  122. loopx/capabilities/auto_research/worker_skill/SKILL.md +343 -0
  123. loopx/capabilities/benchmark_toolkit/__init__.py +19 -0
  124. loopx/capabilities/benchmark_toolkit/integrity.py +387 -0
  125. loopx/capabilities/catalog.py +1875 -0
  126. loopx/capabilities/change_quality/__init__.py +19 -0
  127. loopx/capabilities/change_quality/cli.py +171 -0
  128. loopx/capabilities/change_quality/context.py +156 -0
  129. loopx/capabilities/change_quality/oracles.py +269 -0
  130. loopx/capabilities/change_quality/policy.py +34 -0
  131. loopx/capabilities/change_quality/receipt.py +482 -0
  132. loopx/capabilities/change_quality/result.py +493 -0
  133. loopx/capabilities/change_quality/scope.py +171 -0
  134. loopx/capabilities/change_quality/shadow.py +680 -0
  135. loopx/capabilities/content_ops/__init__.py +0 -0
  136. loopx/capabilities/content_ops/cli.py +649 -0
  137. loopx/capabilities/content_ops/connector_packets.py +164 -0
  138. loopx/capabilities/content_ops/item_lifecycle.py +1000 -0
  139. loopx/capabilities/content_ops/layout.py +451 -0
  140. loopx/capabilities/content_ops/markdown.py +456 -0
  141. loopx/capabilities/content_ops/schemas.py +51 -0
  142. loopx/capabilities/content_ops/social_browser_x.py +107 -0
  143. loopx/capabilities/content_ops/surface.py +1956 -0
  144. loopx/capabilities/content_ops/templates/layout-catalog-v0.json +72 -0
  145. loopx/capabilities/context_providers/__init__.py +36 -0
  146. loopx/capabilities/context_providers/base.py +189 -0
  147. loopx/capabilities/context_providers/factory.py +32 -0
  148. loopx/capabilities/context_providers/openviking.py +702 -0
  149. loopx/capabilities/context_providers/service_ownership.py +185 -0
  150. loopx/capabilities/decision_context/__init__.py +129 -0
  151. loopx/capabilities/decision_context/architecture.py +83 -0
  152. loopx/capabilities/decision_context/assembler.py +849 -0
  153. loopx/capabilities/decision_context/catalog_entry.py +195 -0
  154. loopx/capabilities/decision_context/cli.py +310 -0
  155. loopx/capabilities/decision_context/cursor_commit.py +535 -0
  156. loopx/capabilities/decision_context/outcome_feedback.py +352 -0
  157. loopx/capabilities/decision_context/packets.py +654 -0
  158. loopx/capabilities/decision_context/private_state.py +189 -0
  159. loopx/capabilities/decision_context/profile.py +453 -0
  160. loopx/capabilities/decision_context/providers.py +228 -0
  161. loopx/capabilities/decision_context/review_settlement.py +136 -0
  162. loopx/capabilities/decision_context/runtime.py +273 -0
  163. loopx/capabilities/decision_context/sources.py +415 -0
  164. loopx/capabilities/explore/__init__.py +1 -0
  165. loopx/capabilities/explore/activation.py +198 -0
  166. loopx/capabilities/explore/adaptive_replay_planner.py +221 -0
  167. loopx/capabilities/explore/child_replay_runtime.py +463 -0
  168. loopx/capabilities/explore/composition_frontier.py +291 -0
  169. loopx/capabilities/explore/counterfactual_runtime.py +578 -0
  170. loopx/capabilities/explore/episode_runtime.py +647 -0
  171. loopx/capabilities/explore/harness_checkpoint.py +171 -0
  172. loopx/capabilities/explore/harness_gate.py +115 -0
  173. loopx/capabilities/explore/harness_runtime.py +1124 -0
  174. loopx/capabilities/explore/replay_metrics.py +206 -0
  175. loopx/capabilities/explore/replay_runtime.py +1271 -0
  176. loopx/capabilities/explore/resource_portfolio.py +173 -0
  177. loopx/capabilities/explore/result_log.py +974 -0
  178. loopx/capabilities/explore/router_state.py +432 -0
  179. loopx/capabilities/explore/source_history_reconcile.py +255 -0
  180. loopx/capabilities/explore/speculative_scheduler.py +498 -0
  181. loopx/capabilities/explore/todo_branch_plan.py +650 -0
  182. loopx/capabilities/explore/todo_evidence.py +141 -0
  183. loopx/capabilities/explore/trace_runtime.py +284 -0
  184. loopx/capabilities/explore/worker_branch_plan.py +1257 -0
  185. loopx/capabilities/integration_branch/__init__.py +13 -0
  186. loopx/capabilities/integration_branch/cli.py +148 -0
  187. loopx/capabilities/integration_branch/core.py +916 -0
  188. loopx/capabilities/issue_fix/__init__.py +19 -0
  189. loopx/capabilities/issue_fix/acceptance_loop.py +1050 -0
  190. loopx/capabilities/issue_fix/candidate_evidence.py +503 -0
  191. loopx/capabilities/issue_fix/candidate_preflight.py +676 -0
  192. loopx/capabilities/issue_fix/cli.py +1822 -0
  193. loopx/capabilities/issue_fix/cli_input.py +87 -0
  194. loopx/capabilities/issue_fix/content_ops_cli.py +148 -0
  195. loopx/capabilities/issue_fix/discovered_issue_promotion.py +947 -0
  196. loopx/capabilities/issue_fix/explore_projection.py +710 -0
  197. loopx/capabilities/issue_fix/feasibility.py +542 -0
  198. loopx/capabilities/issue_fix/github_public.py +661 -0
  199. loopx/capabilities/issue_fix/intake_surface.py +832 -0
  200. loopx/capabilities/issue_fix/metadata_preview.py +218 -0
  201. loopx/capabilities/issue_fix/metrics_projection.py +1340 -0
  202. loopx/capabilities/issue_fix/metrics_supplement.py +634 -0
  203. loopx/capabilities/issue_fix/metrics_supplement_cli.py +127 -0
  204. loopx/capabilities/issue_fix/outcome_projection.py +1235 -0
  205. loopx/capabilities/issue_fix/periodic_report.py +189 -0
  206. loopx/capabilities/issue_fix/pr_description.py +418 -0
  207. loopx/capabilities/issue_fix/pr_gate_reconcile.py +496 -0
  208. loopx/capabilities/issue_fix/pr_gate_reconcile_cli.py +464 -0
  209. loopx/capabilities/issue_fix/pr_lifecycle.py +1327 -0
  210. loopx/capabilities/issue_fix/pr_lifecycle_rollout.py +85 -0
  211. loopx/capabilities/issue_fix/pr_monitor_materialization.py +257 -0
  212. loopx/capabilities/issue_fix/pr_review_ack.py +439 -0
  213. loopx/capabilities/issue_fix/provider_hooks.py +24 -0
  214. loopx/capabilities/issue_fix/repository_commit_evidence.py +186 -0
  215. loopx/capabilities/issue_fix/repository_context.py +457 -0
  216. loopx/capabilities/issue_fix/repository_memory.py +459 -0
  217. loopx/capabilities/issue_fix/repository_memory_provider.py +1454 -0
  218. loopx/capabilities/issue_fix/repository_snapshot.py +454 -0
  219. loopx/capabilities/issue_fix/reviewer_cli.py +917 -0
  220. loopx/capabilities/issue_fix/reviewer_notification.py +882 -0
  221. loopx/capabilities/issue_fix/reviewer_notification_drain.py +942 -0
  222. loopx/capabilities/issue_fix/reviewer_recommendation.py +1057 -0
  223. loopx/capabilities/issue_fix/reviewer_request.py +1282 -0
  224. loopx/capabilities/issue_fix/reward_memory.py +879 -0
  225. loopx/capabilities/issue_fix/workflow_plan.py +1286 -0
  226. loopx/capabilities/material_lifecycle/__init__.py +161 -0
  227. loopx/capabilities/material_lifecycle/_validation.py +183 -0
  228. loopx/capabilities/material_lifecycle/apply.py +672 -0
  229. loopx/capabilities/material_lifecycle/architecture.py +122 -0
  230. loopx/capabilities/material_lifecycle/cli.py +161 -0
  231. loopx/capabilities/material_lifecycle/decision_planning.py +470 -0
  232. loopx/capabilities/material_lifecycle/explore_execution.py +306 -0
  233. loopx/capabilities/material_lifecycle/intake.py +869 -0
  234. loopx/capabilities/material_lifecycle/inventory.py +147 -0
  235. loopx/capabilities/material_lifecycle/lifecycle.py +98 -0
  236. loopx/capabilities/material_lifecycle/preparation.py +147 -0
  237. loopx/capabilities/material_lifecycle/project_skill.py +83 -0
  238. loopx/capabilities/material_lifecycle/ranking.py +267 -0
  239. loopx/capabilities/material_lifecycle/readable_projection.py +500 -0
  240. loopx/capabilities/material_lifecycle/rebuild.py +480 -0
  241. loopx/capabilities/material_lifecycle/settlement.py +238 -0
  242. loopx/capabilities/periodic_report/__init__.py +71 -0
  243. loopx/capabilities/periodic_report/adapters.py +939 -0
  244. loopx/capabilities/periodic_report/archive.py +422 -0
  245. loopx/capabilities/periodic_report/bindings.py +705 -0
  246. loopx/capabilities/periodic_report/cli.py +277 -0
  247. loopx/capabilities/periodic_report/core.py +691 -0
  248. loopx/capabilities/periodic_report/extension_envelope.py +66 -0
  249. loopx/capabilities/periodic_report/presets.py +103 -0
  250. loopx/capabilities/periodic_report/profile.py +235 -0
  251. loopx/capabilities/periodic_report/project_progress.py +179 -0
  252. loopx/capabilities/periodic_report/triggers.py +452 -0
  253. loopx/capabilities/pr_review_queue/__init__.py +17 -0
  254. loopx/capabilities/pr_review_queue/core.py +506 -0
  255. loopx/capabilities/pr_review_queue/review_contract.py +506 -0
  256. loopx/capabilities/registry.py +192 -0
  257. loopx/capabilities/reward_memory/__init__.py +75 -0
  258. loopx/capabilities/reward_memory/application.py +819 -0
  259. loopx/capabilities/reward_memory/architecture.py +572 -0
  260. loopx/capabilities/reward_memory/candidate_review.py +511 -0
  261. loopx/capabilities/reward_memory/cli.py +469 -0
  262. loopx/capabilities/reward_memory/dogfood.py +574 -0
  263. loopx/capabilities/reward_memory/evaluation.py +296 -0
  264. loopx/capabilities/reward_memory/evaluation_fixtures.py +362 -0
  265. loopx/capabilities/reward_memory/experiment.py +567 -0
  266. loopx/capabilities/reward_memory/health.py +222 -0
  267. loopx/capabilities/reward_memory/ingestion.py +519 -0
  268. loopx/capabilities/reward_memory/registry.py +600 -0
  269. loopx/capabilities/reward_memory/runtime_hooks.py +312 -0
  270. loopx/capabilities/reward_memory/scoped_feedback.py +173 -0
  271. loopx/capabilities/semantic_preference/__init__.py +12 -0
  272. loopx/capabilities/semantic_preference/cli.py +189 -0
  273. loopx/capabilities/semantic_preference/contract.py +592 -0
  274. loopx/capabilities/semantic_preference/reward_memory.py +62 -0
  275. loopx/capabilities/value_connectors/__init__.py +1 -0
  276. loopx/capabilities/value_connectors/cli.py +401 -0
  277. loopx/capabilities/value_connectors/finance_extension_migration.py +108 -0
  278. loopx/capabilities/value_connectors/install_check.py +147 -0
  279. loopx/capabilities/value_connectors/planner.py +733 -0
  280. loopx/capabilities/value_connectors/source_map.py +446 -0
  281. loopx/claude_goal_baseline.py +138 -0
  282. loopx/claude_goal_mode/__init__.py +23 -0
  283. loopx/claude_goal_mode/hooks/goal_policy.py +212 -0
  284. loopx/claude_goal_mode/hooks/goal_state.py +139 -0
  285. loopx/claude_goal_mode/mcp/loopx_mcp.py +167 -0
  286. loopx/claude_goal_mode/scripts/connect.py +103 -0
  287. loopx/claude_goal_mode/scripts/goalmode_cmd.py +241 -0
  288. loopx/claude_goal_mode/scripts/install.py +328 -0
  289. loopx/claude_goal_mode/statusline/goal_status.py +97 -0
  290. loopx/cli.py +836 -0
  291. loopx/cli_commands/__init__.py +334 -0
  292. loopx/cli_commands/_host_thread.py +13 -0
  293. loopx/cli_commands/agentissue_runner_flow.py +447 -0
  294. loopx/cli_commands/agents_last_exam.py +160 -0
  295. loopx/cli_commands/agents_last_exam_baked_input.py +302 -0
  296. loopx/cli_commands/agents_last_exam_host_codex.py +374 -0
  297. loopx/cli_commands/agents_last_exam_launch_dry_run.py +372 -0
  298. loopx/cli_commands/agents_last_exam_local_plan.py +322 -0
  299. loopx/cli_commands/agents_last_exam_runner_source.py +352 -0
  300. loopx/cli_commands/agents_last_exam_task_material.py +335 -0
  301. loopx/cli_commands/agents_last_exam_validation_gate.py +236 -0
  302. loopx/cli_commands/benchmark_boundary.py +499 -0
  303. loopx/cli_commands/benchmark_dispatch.py +161 -0
  304. loopx/cli_commands/benchmark_release_outcome.py +123 -0
  305. loopx/cli_commands/benchmark_review_lifecycle.py +1275 -0
  306. loopx/cli_commands/benchmark_run_ledger.py +763 -0
  307. loopx/cli_commands/benchmark_run_ledger_case_analysis.py +249 -0
  308. loopx/cli_commands/benchmark_run_ledger_classification.py +45 -0
  309. loopx/cli_commands/benchmark_run_ledger_maintenance.py +486 -0
  310. loopx/cli_commands/benchmark_run_ledger_maintenance_registration.py +342 -0
  311. loopx/cli_commands/benchmark_run_ledger_maintenance_rendering.py +233 -0
  312. loopx/cli_commands/benchmark_run_ledger_parity.py +92 -0
  313. loopx/cli_commands/bootstrap_connect.py +238 -0
  314. loopx/cli_commands/canary.py +707 -0
  315. loopx/cli_commands/canary_release_qualification.py +79 -0
  316. loopx/cli_commands/capability.py +96 -0
  317. loopx/cli_commands/doctor.py +43 -0
  318. loopx/cli_commands/dreaming.py +143 -0
  319. loopx/cli_commands/edgebench.py +205 -0
  320. loopx/cli_commands/evidence_log.py +275 -0
  321. loopx/cli_commands/explore.py +989 -0
  322. loopx/cli_commands/explore_planning_commands.py +157 -0
  323. loopx/cli_commands/extension.py +271 -0
  324. loopx/cli_commands/first_run_report.py +73 -0
  325. loopx/cli_commands/goal_channel.py +656 -0
  326. loopx/cli_commands/handoff_mode.py +158 -0
  327. loopx/cli_commands/history.py +622 -0
  328. loopx/cli_commands/host_mode_plan.py +113 -0
  329. loopx/cli_commands/lark_inbox.py +431 -0
  330. loopx/cli_commands/lark_kanban.py +629 -0
  331. loopx/cli_commands/ml_experiment.py +321 -0
  332. loopx/cli_commands/multi_agent.py +211 -0
  333. loopx/cli_commands/opencode2_goal_worker.py +217 -0
  334. loopx/cli_commands/pr_review.py +167 -0
  335. loopx/cli_commands/presentation.py +218 -0
  336. loopx/cli_commands/preset.py +96 -0
  337. loopx/cli_commands/project.py +150 -0
  338. loopx/cli_commands/project_lifecycle.py +915 -0
  339. loopx/cli_commands/quota.py +859 -0
  340. loopx/cli_commands/quota_registration.py +241 -0
  341. loopx/cli_commands/quota_request.py +113 -0
  342. loopx/cli_commands/ready_score.py +110 -0
  343. loopx/cli_commands/registry_admin.py +975 -0
  344. loopx/cli_commands/registry_admin_configure.py +344 -0
  345. loopx/cli_commands/registry_admin_peer.py +84 -0
  346. loopx/cli_commands/registry_authority.py +218 -0
  347. loopx/cli_commands/review_batch.py +146 -0
  348. loopx/cli_commands/slash_commands.py +145 -0
  349. loopx/cli_commands/start_goal.py +251 -0
  350. loopx/cli_commands/starter.py +175 -0
  351. loopx/cli_commands/starter_bootstrap.py +179 -0
  352. loopx/cli_commands/starter_bootstrap_registration.py +198 -0
  353. loopx/cli_commands/starter_runtime_idle.py +107 -0
  354. loopx/cli_commands/starter_scheduler.py +207 -0
  355. loopx/cli_commands/starter_session_runtime.py +152 -0
  356. loopx/cli_commands/starter_visible_common.py +54 -0
  357. loopx/cli_commands/starter_visible_driver.py +161 -0
  358. loopx/cli_commands/starter_visible_pilot.py +278 -0
  359. loopx/cli_commands/status.py +867 -0
  360. loopx/cli_commands/status_registration.py +239 -0
  361. loopx/cli_commands/summary_all.py +222 -0
  362. loopx/cli_commands/support_control.py +809 -0
  363. loopx/cli_commands/support_control_registry.py +68 -0
  364. loopx/cli_commands/support_control_supervisor.py +289 -0
  365. loopx/cli_commands/task_lease.py +306 -0
  366. loopx/cli_commands/terminal_bench_adapter.py +717 -0
  367. loopx/cli_commands/terminal_bench_environment_result.py +1246 -0
  368. loopx/cli_commands/todo.py +940 -0
  369. loopx/cli_commands/todo_argument_validation.py +572 -0
  370. loopx/cli_commands/todo_event.py +114 -0
  371. loopx/cli_commands/turn.py +804 -0
  372. loopx/cli_commands/version.py +46 -0
  373. loopx/cli_commands/worker_bridge.py +659 -0
  374. loopx/cli_rollout.py +314 -0
  375. loopx/codex_cli_goal_tui.py +672 -0
  376. loopx/codex_cli_probe.py +1530 -0
  377. loopx/codex_cli_probe_markdown.py +935 -0
  378. loopx/codex_cli_runtime_probe.py +733 -0
  379. loopx/codex_cli_scheduler.py +564 -0
  380. loopx/codex_goal_baseline.py +620 -0
  381. loopx/configuration_catalog.py +617 -0
  382. loopx/configure_goal.py +1375 -0
  383. loopx/contract.py +996 -0
  384. loopx/control_plane/__init__.py +71 -0
  385. loopx/control_plane/agents/__init__.py +1 -0
  386. loopx/control_plane/agents/agent_lane_recommendation.py +516 -0
  387. loopx/control_plane/agents/agent_scope.py +1578 -0
  388. loopx/control_plane/agents/agent_scope_frontier.py +60 -0
  389. loopx/control_plane/agents/capability_gate.py +531 -0
  390. loopx/control_plane/agents/identity.py +140 -0
  391. loopx/control_plane/agents/legacy_migration.py +169 -0
  392. loopx/control_plane/agents/management_projection.py +658 -0
  393. loopx/control_plane/agents/material_frontier.py +608 -0
  394. loopx/control_plane/agents/material_handoff.py +156 -0
  395. loopx/control_plane/agents/multi_agent/__init__.py +1 -0
  396. loopx/control_plane/agents/multi_agent/codex_executable.py +207 -0
  397. loopx/control_plane/agents/multi_agent/collective_round_ledger.py +387 -0
  398. loopx/control_plane/agents/multi_agent/contract.py +474 -0
  399. loopx/control_plane/agents/multi_agent/recipe.py +110 -0
  400. loopx/control_plane/agents/multi_agent/role_successor.py +297 -0
  401. loopx/control_plane/agents/multi_agent/runtime_scripts.py +426 -0
  402. loopx/control_plane/agents/multi_agent/visible_launch_policy.py +149 -0
  403. loopx/control_plane/agents/multi_agent/visible_wake_scheduler.py +392 -0
  404. loopx/control_plane/agents/profile.py +216 -0
  405. loopx/control_plane/agents/runtime_model.py +73 -0
  406. loopx/control_plane/agents/subagent_activity.py +164 -0
  407. loopx/control_plane/agents/supervisor.py +544 -0
  408. loopx/control_plane/agents/supervisor_events.py +462 -0
  409. loopx/control_plane/agents/supervisor_inject.py +204 -0
  410. loopx/control_plane/agents/work_mode.py +56 -0
  411. loopx/control_plane/agents/workspace_guard.py +364 -0
  412. loopx/control_plane/effect_program.py +644 -0
  413. loopx/control_plane/goals/__init__.py +1 -0
  414. loopx/control_plane/goals/active_state_event_projection.py +103 -0
  415. loopx/control_plane/goals/active_state_metadata.py +47 -0
  416. loopx/control_plane/goals/active_state_sections.py +58 -0
  417. loopx/control_plane/goals/configure_goal_service.py +354 -0
  418. loopx/control_plane/goals/contract_health.py +132 -0
  419. loopx/control_plane/goals/dreaming.py +152 -0
  420. loopx/control_plane/goals/global_registry_health.py +199 -0
  421. loopx/control_plane/goals/global_registry_shadow.py +33 -0
  422. loopx/control_plane/goals/goal_channel.py +34 -0
  423. loopx/control_plane/goals/goal_channel_projection.py +560 -0
  424. loopx/control_plane/goals/goal_frontier/__init__.py +1917 -0
  425. loopx/control_plane/goals/goal_frontier/ack_policy.py +149 -0
  426. loopx/control_plane/goals/goal_frontier/outcome_continuity.py +437 -0
  427. loopx/control_plane/goals/goal_frontier/replan_rules.py +210 -0
  428. loopx/control_plane/goals/goal_frontier/semantic_history.py +314 -0
  429. loopx/control_plane/goals/goal_frontier/terminal.py +180 -0
  430. loopx/control_plane/goals/goal_vision.py +443 -0
  431. loopx/control_plane/goals/goal_vision_policy.py +36 -0
  432. loopx/control_plane/goals/goal_vision_state.py +62 -0
  433. loopx/control_plane/goals/goal_vision_wait.py +290 -0
  434. loopx/control_plane/goals/path_resolution.py +20 -0
  435. loopx/control_plane/goals/start_contract.py +206 -0
  436. loopx/control_plane/goals/vision_checkpoint.py +92 -0
  437. loopx/control_plane/handoff/__init__.py +1 -0
  438. loopx/control_plane/handoff/cross_runtime_impl_review.py +311 -0
  439. loopx/control_plane/handoff/delivery_contract.py +161 -0
  440. loopx/control_plane/handoff/handoff_runs.py +71 -0
  441. loopx/control_plane/handoff/project_handoff.py +155 -0
  442. loopx/control_plane/handoff/review_batch.py +463 -0
  443. loopx/control_plane/handoff/review_packet_context.py +216 -0
  444. loopx/control_plane/heartbeat/agent.py +173 -0
  445. loopx/control_plane/heartbeat/budget.py +66 -0
  446. loopx/control_plane/heartbeat/builder.py +501 -0
  447. loopx/control_plane/heartbeat/host.py +64 -0
  448. loopx/control_plane/heartbeat/rules.py +68 -0
  449. loopx/control_plane/heartbeat/task_body.py +759 -0
  450. loopx/control_plane/heartbeat/visible_goal.py +86 -0
  451. loopx/control_plane/projects/__init__.py +1 -0
  452. loopx/control_plane/projects/contract.py +25 -0
  453. loopx/control_plane/projects/registry.py +663 -0
  454. loopx/control_plane/quota/__init__.py +1 -0
  455. loopx/control_plane/quota/cli_projection.py +704 -0
  456. loopx/control_plane/quota/decision_summary.py +431 -0
  457. loopx/control_plane/quota/effect_program.py +152 -0
  458. loopx/control_plane/quota/error_codes.py +19 -0
  459. loopx/control_plane/quota/goal_boundary.py +464 -0
  460. loopx/control_plane/quota/heartbeat_receipt.py +277 -0
  461. loopx/control_plane/quota/heartbeat_recommendation.py +718 -0
  462. loopx/control_plane/quota/host_poll_receipts.py +162 -0
  463. loopx/control_plane/quota/live_decision.py +142 -0
  464. loopx/control_plane/quota/monitor_poll.py +786 -0
  465. loopx/control_plane/quota/policy_constants.py +40 -0
  466. loopx/control_plane/quota/projection_repair.py +262 -0
  467. loopx/control_plane/quota/recent_runs.py +210 -0
  468. loopx/control_plane/quota/scheduler_ack.py +490 -0
  469. loopx/control_plane/quota/selected_todo_projection.py +139 -0
  470. loopx/control_plane/quota/settlement.py +437 -0
  471. loopx/control_plane/quota/settlement_cli.py +246 -0
  472. loopx/control_plane/quota/settlement_validation.py +64 -0
  473. loopx/control_plane/quota/settlement_workspace_causality.py +180 -0
  474. loopx/control_plane/quota/should_run.py +249 -0
  475. loopx/control_plane/quota/should_run_packet.py +1165 -0
  476. loopx/control_plane/quota/should_run_prepare.py +675 -0
  477. loopx/control_plane/quota/slot_accounting.py +1123 -0
  478. loopx/control_plane/quota/spend_sources.py +11 -0
  479. loopx/control_plane/quota/stall_repair.py +397 -0
  480. loopx/control_plane/quota/states.py +29 -0
  481. loopx/control_plane/quota/task_orchestration.py +448 -0
  482. loopx/control_plane/quota/task_orchestration_admission.py +497 -0
  483. loopx/control_plane/quota/turn_envelope.py +889 -0
  484. loopx/control_plane/quota/usage_summary.py +140 -0
  485. loopx/control_plane/reward_memory.py +43 -0
  486. loopx/control_plane/runtime/__init__.py +2 -0
  487. loopx/control_plane/runtime/active_user_assisted_pilot.py +275 -0
  488. loopx/control_plane/runtime/agent_scoped_evidence_log.py +435 -0
  489. loopx/control_plane/runtime/decision_freshness.py +203 -0
  490. loopx/control_plane/runtime/event_ledger.py +197 -0
  491. loopx/control_plane/runtime/event_store_migration_bridge.py +196 -0
  492. loopx/control_plane/runtime/goal_project_route.py +70 -0
  493. loopx/control_plane/runtime/local_state_write_correctness.py +242 -0
  494. loopx/control_plane/runtime/promotion_readiness.py +152 -0
  495. loopx/control_plane/runtime/public_safety.py +120 -0
  496. loopx/control_plane/runtime/run_artifacts.py +78 -0
  497. loopx/control_plane/runtime/run_compaction.py +397 -0
  498. loopx/control_plane/runtime/run_context_retention.py +241 -0
  499. loopx/control_plane/runtime/run_history.py +132 -0
  500. loopx/control_plane/runtime/run_index_duplicates.py +205 -0
  501. loopx/control_plane/runtime/run_index_rebuild.py +263 -0
  502. loopx/control_plane/runtime/run_ingest_health.py +336 -0
  503. loopx/control_plane/runtime/runtime_projection_route.py +624 -0
  504. loopx/control_plane/runtime/runtime_projection_writer.py +98 -0
  505. loopx/control_plane/runtime/session_runtime.py +339 -0
  506. loopx/control_plane/runtime/shared_runtime_material_projection.py +332 -0
  507. loopx/control_plane/runtime/shared_runtime_refresh_projection.py +183 -0
  508. loopx/control_plane/runtime/stale_latest_run.py +90 -0
  509. loopx/control_plane/runtime/status_classifications.py +49 -0
  510. loopx/control_plane/runtime/status_projection_cache.py +235 -0
  511. loopx/control_plane/runtime/stride_observation.py +144 -0
  512. loopx/control_plane/runtime/time.py +39 -0
  513. loopx/control_plane/runtime/trajectory_hygiene.py +149 -0
  514. loopx/control_plane/runtime/validation_command.py +69 -0
  515. loopx/control_plane/scheduler/__init__.py +1 -0
  516. loopx/control_plane/scheduler/ack.py +329 -0
  517. loopx/control_plane/scheduler/arbitration.py +188 -0
  518. loopx/control_plane/scheduler/automation_liveness.py +183 -0
  519. loopx/control_plane/scheduler/execution_context.py +555 -0
  520. loopx/control_plane/scheduler/external_evidence_observation.py +428 -0
  521. loopx/control_plane/scheduler/monitor_display.py +143 -0
  522. loopx/control_plane/scheduler/monitor_poll_policy.py +161 -0
  523. loopx/control_plane/scheduler/monitor_poll_writeback.py +351 -0
  524. loopx/control_plane/scheduler/monitor_target.py +64 -0
  525. loopx/control_plane/scheduler/monitor_todo.py +146 -0
  526. loopx/control_plane/scheduler/monitor_wait.py +237 -0
  527. loopx/control_plane/scheduler/scheduler_hint.py +1284 -0
  528. loopx/control_plane/scheduler/state.py +354 -0
  529. loopx/control_plane/scheduler/state_transition_rules.py +179 -0
  530. loopx/control_plane/scheduler/time.py +10 -0
  531. loopx/control_plane/settlement_driver.py +293 -0
  532. loopx/control_plane/status/__init__.py +6 -0
  533. loopx/control_plane/status/active_state_projection.py +105 -0
  534. loopx/control_plane/status/agent_lane_projection.py +375 -0
  535. loopx/control_plane/status/attention_projection.py +74 -0
  536. loopx/control_plane/status/autonomous_replan_projection.py +103 -0
  537. loopx/control_plane/status/collection.py +140 -0
  538. loopx/control_plane/status/contract_projection.py +31 -0
  539. loopx/control_plane/status/dreaming_projection.py +52 -0
  540. loopx/control_plane/status/goal_attention_projection.py +157 -0
  541. loopx/control_plane/status/lifecycle_projection.py +110 -0
  542. loopx/control_plane/status/monitor_display_projection.py +69 -0
  543. loopx/control_plane/status/registry_health_projection.py +75 -0
  544. loopx/control_plane/status/run_projection.py +70 -0
  545. loopx/control_plane/status/runtime_summaries.py +161 -0
  546. loopx/control_plane/testing/__init__.py +1 -0
  547. loopx/control_plane/testing/actual_default_model_behavior_portfolio.py +1371 -0
  548. loopx/control_plane/testing/canary_harness.py +182 -0
  549. loopx/control_plane/testing/capability_monitor_repair_tool_behavior.py +674 -0
  550. loopx/control_plane/testing/cli_output_budget.py +807 -0
  551. loopx/control_plane/testing/cli_output_differential.py +250 -0
  552. loopx/control_plane/testing/cli_output_semantics.py +87 -0
  553. loopx/control_plane/testing/control_plane_composition_scenarios.py +225 -0
  554. loopx/control_plane/testing/decision_replay.py +268 -0
  555. loopx/control_plane/testing/doubao_model_behavior_actor.py +559 -0
  556. loopx/control_plane/testing/model_behavior_corpus.py +344 -0
  557. loopx/control_plane/testing/model_behavior_qualification.py +769 -0
  558. loopx/control_plane/testing/model_behavior_retained_cases.py +235 -0
  559. loopx/control_plane/testing/model_tool_behavior.py +536 -0
  560. loopx/control_plane/testing/onboarding_model_behavior_qualification.py +642 -0
  561. loopx/control_plane/testing/quota_fixtures.py +208 -0
  562. loopx/control_plane/testing/quota_should_run_parity.py +57 -0
  563. loopx/control_plane/testing/release_commit_qualification.py +671 -0
  564. loopx/control_plane/testing/replan_semantic_action_behavior.py +1302 -0
  565. loopx/control_plane/testing/scoped_gate_successor_tool_behavior.py +527 -0
  566. loopx/control_plane/testing/selected_todo_tool_behavior.py +1002 -0
  567. loopx/control_plane/testing/terminal_settlement_tool_behavior.py +656 -0
  568. loopx/control_plane/todos/__init__.py +1 -0
  569. loopx/control_plane/todos/active_state_editing.py +296 -0
  570. loopx/control_plane/todos/active_state_todo_parser.py +138 -0
  571. loopx/control_plane/todos/active_state_todos.py +175 -0
  572. loopx/control_plane/todos/addition.py +103 -0
  573. loopx/control_plane/todos/claim_visibility.py +253 -0
  574. loopx/control_plane/todos/completed_archive.py +139 -0
  575. loopx/control_plane/todos/completion_fence.py +49 -0
  576. loopx/control_plane/todos/completion_policy.py +153 -0
  577. loopx/control_plane/todos/completion_validation.py +248 -0
  578. loopx/control_plane/todos/completion_validation_accountability.py +27 -0
  579. loopx/control_plane/todos/completion_validation_projection.py +57 -0
  580. loopx/control_plane/todos/contract.py +1476 -0
  581. loopx/control_plane/todos/decision_scope.py +554 -0
  582. loopx/control_plane/todos/deferred_resume.py +546 -0
  583. loopx/control_plane/todos/durable_completion.py +201 -0
  584. loopx/control_plane/todos/event_writeback.py +484 -0
  585. loopx/control_plane/todos/frontier_deadline.py +132 -0
  586. loopx/control_plane/todos/handoff_gate.py +283 -0
  587. loopx/control_plane/todos/handoff_mode.py +444 -0
  588. loopx/control_plane/todos/handoff_note.py +202 -0
  589. loopx/control_plane/todos/line_update.py +361 -0
  590. loopx/control_plane/todos/list_projection.py +205 -0
  591. loopx/control_plane/todos/markdown.py +199 -0
  592. loopx/control_plane/todos/monitor_metadata.py +88 -0
  593. loopx/control_plane/todos/mutation_authority.py +299 -0
  594. loopx/control_plane/todos/projection.py +655 -0
  595. loopx/control_plane/todos/quota_summary.py +1138 -0
  596. loopx/control_plane/todos/route_continuation.py +267 -0
  597. loopx/control_plane/todos/succession_warning.py +174 -0
  598. loopx/control_plane/todos/summary_item.py +223 -0
  599. loopx/control_plane/todos/text.py +30 -0
  600. loopx/control_plane/todos/todo_index.py +226 -0
  601. loopx/control_plane/todos/todo_summary.py +1458 -0
  602. loopx/control_plane/todos/unblock_resume.py +326 -0
  603. loopx/control_plane/todos/user_gate.py +263 -0
  604. loopx/control_plane/todos/write_hint.py +63 -0
  605. loopx/control_plane/todos/write_policy.py +135 -0
  606. loopx/control_plane/turn_driver/__init__.py +85 -0
  607. loopx/control_plane/turn_driver/codex_cli.py +502 -0
  608. loopx/control_plane/turn_driver/driver.py +355 -0
  609. loopx/control_plane/turn_driver/executor.py +1468 -0
  610. loopx/control_plane/turn_driver/loop_controller.py +669 -0
  611. loopx/control_plane/turn_driver/settlement.py +318 -0
  612. loopx/control_plane/turn_driver/transaction.py +375 -0
  613. loopx/control_plane/work_items/__init__.py +1 -0
  614. loopx/control_plane/work_items/attention_fields.py +56 -0
  615. loopx/control_plane/work_items/attention_item.py +77 -0
  616. loopx/control_plane/work_items/attention_queue.py +322 -0
  617. loopx/control_plane/work_items/attention_routing.py +213 -0
  618. loopx/control_plane/work_items/autonomous_candidates.py +135 -0
  619. loopx/control_plane/work_items/autonomous_replan_ack.py +276 -0
  620. loopx/control_plane/work_items/autonomous_replan_obligation.py +786 -0
  621. loopx/control_plane/work_items/backlog_hygiene.py +59 -0
  622. loopx/control_plane/work_items/capability_monitor_fallback.py +221 -0
  623. loopx/control_plane/work_items/delivery_batch_scale.py +66 -0
  624. loopx/control_plane/work_items/delivery_outcome.py +152 -0
  625. loopx/control_plane/work_items/delivery_signals.py +113 -0
  626. loopx/control_plane/work_items/execution_obligation.py +235 -0
  627. loopx/control_plane/work_items/goal_route_hint.py +320 -0
  628. loopx/control_plane/work_items/interaction_contract.py +1540 -0
  629. loopx/control_plane/work_items/issue_meta_surface.py +159 -0
  630. loopx/control_plane/work_items/lifecycle.py +139 -0
  631. loopx/control_plane/work_items/operator_inbox.py +266 -0
  632. loopx/control_plane/work_items/outcome_followthrough.py +69 -0
  633. loopx/control_plane/work_items/primary_action.py +326 -0
  634. loopx/control_plane/work_items/progress_observation.py +630 -0
  635. loopx/control_plane/work_items/project_asset.py +675 -0
  636. loopx/control_plane/work_items/repair_delta.py +693 -0
  637. loopx/control_plane/work_items/runtime_capability_reentry.py +168 -0
  638. loopx/control_plane/work_items/semantic_replan_writeback.py +177 -0
  639. loopx/control_plane/work_items/status_contract.py +49 -0
  640. loopx/control_plane/work_items/task_graph.py +1046 -0
  641. loopx/control_plane/work_items/task_lease.py +1254 -0
  642. loopx/control_plane/work_items/task_lease_settlement.py +422 -0
  643. loopx/control_plane/work_items/work_lane.py +510 -0
  644. loopx/control_plane/work_items/work_lane_context.py +161 -0
  645. loopx/demo.py +247 -0
  646. loopx/diagnose.py +633 -0
  647. loopx/doctor.py +1251 -0
  648. loopx/domain_packs/__init__.py +1 -0
  649. loopx/domain_packs/issue_fix.py +571 -0
  650. loopx/domain_packs/ml_experiment.py +854 -0
  651. loopx/domain_state.py +137 -0
  652. loopx/dreaming.py +706 -0
  653. loopx/entrypoint.py +16 -0
  654. loopx/event_sourced_state.py +981 -0
  655. loopx/execution_profile.py +286 -0
  656. loopx/experiments/__init__.py +1 -0
  657. loopx/experiments/planner_worker/__init__.py +1 -0
  658. loopx/experiments/planner_worker/contract.py +523 -0
  659. loopx/experiments/planner_worker/runtime.py +391 -0
  660. loopx/experiments/planner_worker/traex.py +461 -0
  661. loopx/explore_graph.py +11 -0
  662. loopx/extensions/__init__.py +1 -0
  663. loopx/extensions/bundled.py +28 -0
  664. loopx/extensions/execution_envelope.py +126 -0
  665. loopx/extensions/lark/__init__.py +11 -0
  666. loopx/extensions/lark/event_collector.py +478 -0
  667. loopx/extensions/lark/event_collector_runtime.py +506 -0
  668. loopx/extensions/lark/event_inbox.py +454 -0
  669. loopx/extensions/lark/extension.toml +88 -0
  670. loopx/extensions/lark/goal_channel.py +44 -0
  671. loopx/extensions/lark/goal_channel_contracts.py +388 -0
  672. loopx/extensions/lark/goal_channel_lifecycle.py +218 -0
  673. loopx/extensions/lark/goal_channel_runtime.py +792 -0
  674. loopx/extensions/lark/goal_channel_setup.py +805 -0
  675. loopx/extensions/lark/goal_channel_targets.py +215 -0
  676. loopx/extensions/lark/goal_channel_transport.py +281 -0
  677. loopx/extensions/lark/inbox_reactions.py +650 -0
  678. loopx/extensions/lark/inbox_reply.py +430 -0
  679. loopx/extensions/lark/presentation/__init__.py +11 -0
  680. loopx/extensions/lark/presentation/explore_results.py +2276 -0
  681. loopx/extensions/lark/presentation/explore_singleflight.py +127 -0
  682. loopx/extensions/lark/presentation/explore_source_guard.py +121 -0
  683. loopx/extensions/lark/presentation/explore_stage_document.py +703 -0
  684. loopx/extensions/lark/presentation/explore_visual_integrity.py +122 -0
  685. loopx/extensions/lark/presentation/explore_visual_readback.py +452 -0
  686. loopx/extensions/lark/presentation/explore_visual_styles.py +156 -0
  687. loopx/extensions/lark/presentation/issue_fix_surface.py +612 -0
  688. loopx/extensions/lark/presentation/kanban.py +2791 -0
  689. loopx/extensions/lark/presentation/message_card.py +112 -0
  690. loopx/extensions/lark/presentation/periodic_report.py +261 -0
  691. loopx/extensions/lark/presentation/projection_rows.py +600 -0
  692. loopx/extensions/lark/presentation/record_io.py +95 -0
  693. loopx/extensions/lark/presentation/sync_receipt.py +145 -0
  694. loopx/extensions/lark/private_json.py +40 -0
  695. loopx/extensions/lark/provider.py +86 -0
  696. loopx/extensions/lark/reviewer_notification.py +604 -0
  697. loopx/extensions/manifest.py +385 -0
  698. loopx/extensions/openviking_periodic_report/__init__.py +17 -0
  699. loopx/extensions/openviking_periodic_report/activation.py +173 -0
  700. loopx/extensions/openviking_periodic_report/extension.toml +17 -0
  701. loopx/extensions/openviking_periodic_report/provider.py +355 -0
  702. loopx/extensions/openviking_periodic_report/sink.py +117 -0
  703. loopx/extensions/openviking_semantic_preference/__init__.py +5 -0
  704. loopx/extensions/openviking_semantic_preference/extension.toml +16 -0
  705. loopx/extensions/openviking_semantic_preference/history_export.py +484 -0
  706. loopx/extensions/openviking_semantic_preference/project_peer.py +68 -0
  707. loopx/extensions/openviking_semantic_preference/provider.py +312 -0
  708. loopx/extensions/presentation.py +979 -0
  709. loopx/extensions/process_runtime.py +204 -0
  710. loopx/extensions/readiness.py +168 -0
  711. loopx/extensions/runtime.py +931 -0
  712. loopx/extensions/scaffold.py +335 -0
  713. loopx/feedback.py +581 -0
  714. loopx/file_lock.py +382 -0
  715. loopx/global_registry.py +842 -0
  716. loopx/global_risks.py +970 -0
  717. loopx/global_todos.py +568 -0
  718. loopx/handoff_budget.py +28 -0
  719. loopx/heartbeat_prequota.py +80 -0
  720. loopx/heartbeat_prompt.py +159 -0
  721. loopx/help_surface.py +516 -0
  722. loopx/history.py +1507 -0
  723. loopx/host_loop_activation.py +1311 -0
  724. loopx/host_mode_planner.py +991 -0
  725. loopx/install_contract.py +1 -0
  726. loopx/interface_budget.py +196 -0
  727. loopx/long_task_cadence.py +208 -0
  728. loopx/materials.py +185 -0
  729. loopx/ml_experiment.py +3 -0
  730. loopx/onboarding.py +214 -0
  731. loopx/opencode2_goal_mode/README.md +81 -0
  732. loopx/opencode2_goal_mode/__init__.py +9 -0
  733. loopx/opencode2_goal_mode/opencode2-goal-worker.mjs +1018 -0
  734. loopx/opencode_goal_mode/README.md +99 -0
  735. loopx/opencode_goal_mode/__init__.py +13 -0
  736. loopx/opencode_goal_mode/goal-bridge-runtime.mjs +858 -0
  737. loopx/opencode_goal_mode/loopx-goal.js +8 -0
  738. loopx/operator_gate.py +420 -0
  739. loopx/orchestration.py +127 -0
  740. loopx/paths.py +59 -0
  741. loopx/pi_goal_mode/README.md +67 -0
  742. loopx/pi_goal_mode/__init__.py +13 -0
  743. loopx/pi_goal_mode/loopx-goal.ts +254 -0
  744. loopx/pi_goal_mode/pi-goal-loop-runtime.mjs +574 -0
  745. loopx/pr_review.py +1206 -0
  746. loopx/presentation/__init__.py +1 -0
  747. loopx/presentation/explore_views.py +1334 -0
  748. loopx/presentation/markdown.py +61 -0
  749. loopx/presentation/projection_source_reconcile.py +140 -0
  750. loopx/presentation/public_safety.py +42 -0
  751. loopx/presentation/renderers/__init__.py +17 -0
  752. loopx/presentation/renderers/goal_channel_html.py +269 -0
  753. loopx/presentation/renderers/periodic_report_html.py +786 -0
  754. loopx/presentation/renderers/periodic_report_markdown.py +184 -0
  755. loopx/presentation/renderers/quota_event_markdown.py +116 -0
  756. loopx/presentation/renderers/quota_markdown.py +1112 -0
  757. loopx/presentation/renderers/status_markdown.py +1570 -0
  758. loopx/presentation/renderers/trajectory_hygiene_markdown.py +39 -0
  759. loopx/presentation/renderers/turn_envelope_markdown.py +33 -0
  760. loopx/presentation/sinks/__init__.py +5 -0
  761. loopx/presentation/sinks/openviking_periodic_report.py +7 -0
  762. loopx/presentation/static_site.py +691 -0
  763. loopx/presets.py +369 -0
  764. loopx/project_alias.py +217 -0
  765. loopx/project_map.py +589 -0
  766. loopx/project_prompt.py +1153 -0
  767. loopx/project_skill_cli.py +125 -0
  768. loopx/project_skill_delivery.py +470 -0
  769. loopx/project_uninstall.py +462 -0
  770. loopx/promotion_gate.py +197 -0
  771. loopx/quota.py +1197 -0
  772. loopx/ready_score.py +413 -0
  773. loopx/registry.py +621 -0
  774. loopx/registry_writability.py +64 -0
  775. loopx/release_candidate.py +148 -0
  776. loopx/release_manifest.py +316 -0
  777. loopx/repository_identity.py +100 -0
  778. loopx/review_packet.py +1024 -0
  779. loopx/rollout_event_log.py +505 -0
  780. loopx/runtime.py +112 -0
  781. loopx/self_update.py +750 -0
  782. loopx/session_runtime.py +418 -0
  783. loopx/skill_install_readback.py +500 -0
  784. loopx/slash_command_install.py +1393 -0
  785. loopx/slash_commands.py +264 -0
  786. loopx/state_backup.py +573 -0
  787. loopx/state_migration.py +350 -0
  788. loopx/state_projection.py +809 -0
  789. loopx/state_refresh.py +1416 -0
  790. loopx/status.py +1383 -0
  791. loopx/status_server.py +935 -0
  792. loopx/summary_all.py +725 -0
  793. loopx/terminal_bench_agent.py +2056 -0
  794. loopx/thread_agent_binding.py +408 -0
  795. loopx/todo_followups.py +168 -0
  796. loopx/todo_suggestion_prompt.py +204 -0
  797. loopx/todos.py +2229 -0
  798. loopx/turn_identity.py +17 -0
  799. loopx/upgrade.py +1083 -0
  800. loopx/visible_governance.py +667 -0
  801. loopx/visible_multi_agent_launcher.py +1253 -0
  802. loopx/visible_multi_agent_tmux.py +429 -0
  803. loopx/worker_bridge.py +1574 -0
  804. loopx-0.4.8.dist-info/METADATA +708 -0
  805. loopx-0.4.8.dist-info/RECORD +811 -0
  806. loopx-0.4.8.dist-info/WHEEL +5 -0
  807. loopx-0.4.8.dist-info/entry_points.txt +5 -0
  808. loopx-0.4.8.dist-info/licenses/LICENSE +202 -0
  809. loopx-0.4.8.dist-info/licenses/LICENSE-MIT +21 -0
  810. loopx-0.4.8.dist-info/licenses/NOTICE +6 -0
  811. loopx-0.4.8.dist-info/top_level.txt +1 -0
@@ -0,0 +1,1282 @@
1
+ from __future__ import annotations
2
+
3
+ import json
4
+ import re
5
+ import subprocess
6
+ from collections.abc import Callable, Mapping, Sequence
7
+ from pathlib import Path
8
+ from typing import Any
9
+
10
+ from ...control_plane.runtime.public_safety import public_safe_compact_text
11
+ from .metadata_preview import normalise_github_issue_reference
12
+ from .pr_description import validate_issue_fix_pr_description_publication
13
+ from .reviewer_notification import (
14
+ NotificationSinkAdapter,
15
+ build_issue_fix_reviewer_notification_sinks_result,
16
+ )
17
+ from .reward_memory import (
18
+ run_issue_fix_reviewer_artifact_automatic_reward_memory,
19
+ run_issue_fix_reviewer_notification_automatic_reward_memory,
20
+ )
21
+ from .reviewer_recommendation import build_issue_fix_reviewer_recommendation_packet
22
+
23
+
24
+ ISSUE_FIX_REVIEWER_REQUEST_SCHEMA_VERSION = "issue_fix_reviewer_request_v0"
25
+ REVIEWER_BOT_HANDLE_PATTERN = re.compile(
26
+ r"(?:^|[-_.])bot(?:$|[-_.])|\[bot\]$",
27
+ re.IGNORECASE,
28
+ )
29
+ REVIEWER_COMMENT_MARKER_PATTERN = re.compile(
30
+ r"<!--\s*loopx:\s*issue-fix-reviewer-notification\s+"
31
+ r"reviewer=@?([A-Za-z0-9-]+(?:/[A-Za-z0-9_.-]+)?)\s*-->",
32
+ re.IGNORECASE,
33
+ )
34
+ REVIEWER_COMMENT_MENTION_PATTERN = re.compile(
35
+ r"(?<![A-Za-z0-9_])@([A-Za-z0-9-]+(?:/[A-Za-z0-9_.-]+)?)",
36
+ re.IGNORECASE,
37
+ )
38
+ REVIEWER_COMMENT_INTENT_PATTERNS = (
39
+ re.compile(
40
+ r"\b(?:could|can|would)\s+you(?:\s+please)?\s+"
41
+ r"(?:review|take\s+a\s+look|look\s+over)\b",
42
+ re.IGNORECASE,
43
+ ),
44
+ re.compile(
45
+ r"\bplease\s+(?:review|take\s+a\s+look|look\s+over)\b",
46
+ re.IGNORECASE,
47
+ ),
48
+ re.compile(
49
+ r"(?:请|麻烦|辛苦|帮忙|协助)[^\n]{0,32}"
50
+ r"(?:review|code\s*review|\bcr\b|看一下|看下|审阅|评审)",
51
+ re.IGNORECASE,
52
+ ),
53
+ )
54
+ GITHUB_REVIEW_REQUEST_PERMISSION_PATTERN = re.compile(
55
+ r"(?:HTTP\s+(?:403|404)|resource not accessible|"
56
+ r"must have (?:push|triage|write) access|not found|"
57
+ r"reviews? may only be requested from collaborators|not a collaborator)",
58
+ re.IGNORECASE,
59
+ )
60
+
61
+ CommandRunner = Callable[[Sequence[str]], Mapping[str, Any]]
62
+
63
+
64
+ def _default_runner(args: Sequence[str]) -> Mapping[str, Any]:
65
+ result = subprocess.run(
66
+ list(args),
67
+ text=True,
68
+ stdout=subprocess.PIPE,
69
+ stderr=subprocess.PIPE,
70
+ timeout=30,
71
+ )
72
+ return {
73
+ "returncode": result.returncode,
74
+ "stdout": result.stdout,
75
+ "stderr": result.stderr,
76
+ }
77
+
78
+
79
+ def _normalise_login(value: Any) -> str | None:
80
+ text = public_safe_compact_text(value, limit=100)
81
+ if not text:
82
+ return None
83
+ text = text.strip().lstrip("@").lower()
84
+ return f"@{text}" if text else None
85
+
86
+
87
+ def _is_automated_reviewer_handle(value: Any) -> bool:
88
+ handle = _normalise_login(value)
89
+ return bool(handle and REVIEWER_BOT_HANDLE_PATTERN.search(handle.lstrip("@")))
90
+
91
+
92
+ def _secondary_notification_targets(
93
+ *,
94
+ primary_handles: Sequence[str],
95
+ candidates: Sequence[Any],
96
+ sinks_input: Mapping[str, Any],
97
+ allow_candidate_fallback: bool = True,
98
+ ) -> tuple[list[str], list[str]]:
99
+ """Choose sink-resolvable reviewers without changing GitHub coverage.
100
+
101
+ The primary handles are reviewers already covered on GitHub or selected for
102
+ a new GitHub request. Ranked recommendation candidates are fallback-only for
103
+ a newly selected request. Existing GitHub coverage must stay bound to the
104
+ same reviewer across secondary sinks; an unresolved mapping skips the sink
105
+ instead of notifying another person. Sink adapters still perform the
106
+ authoritative provider and membership verification before sending.
107
+ """
108
+
109
+ primary = list(
110
+ dict.fromkeys(
111
+ handle
112
+ for value in primary_handles
113
+ if (handle := _normalise_login(value)) is not None
114
+ )
115
+ )[:3]
116
+ if not primary:
117
+ return [], []
118
+
119
+ pool = list(primary)
120
+ for candidate in candidates:
121
+ if (
122
+ not isinstance(candidate, Mapping)
123
+ or candidate.get("requestable") is not True
124
+ ):
125
+ continue
126
+ handle = _normalise_login(candidate.get("reviewer_handle"))
127
+ if handle and not _is_automated_reviewer_handle(handle) and handle not in pool:
128
+ pool.append(handle)
129
+
130
+ raw_sinks = sinks_input.get("sinks")
131
+ sinks = raw_sinks if isinstance(raw_sinks, list) else []
132
+ identity_maps: list[dict[str, Any]] = []
133
+ for sink in sinks:
134
+ if not isinstance(sink, Mapping):
135
+ return primary, pool
136
+ raw_identities = sink.get("reviewer_identities")
137
+ if not isinstance(raw_identities, Mapping):
138
+ return primary, pool
139
+ identity_maps.append(
140
+ {
141
+ handle: value
142
+ for raw_handle, value in raw_identities.items()
143
+ if (handle := _normalise_login(raw_handle)) is not None
144
+ }
145
+ )
146
+ if not identity_maps:
147
+ return primary, pool
148
+
149
+ target_count = len(primary)
150
+ if not allow_candidate_fallback:
151
+ return (
152
+ [
153
+ handle
154
+ for handle in primary
155
+ if all(
156
+ isinstance(identities.get(handle), Mapping)
157
+ for identities in identity_maps
158
+ )
159
+ ][:target_count],
160
+ pool,
161
+ )
162
+ resolved = [
163
+ handle
164
+ for handle in pool
165
+ if all(
166
+ isinstance(identities.get(handle), Mapping) for identities in identity_maps
167
+ )
168
+ ][:target_count]
169
+ return (resolved or primary), pool
170
+
171
+
172
+ def _is_mention_cluster_separator(value: str) -> bool:
173
+ if "\n" in value or "\r" in value:
174
+ return False
175
+ remainder = re.sub(r"\b(?:and|or)\b|[和与及]", "", value, flags=re.IGNORECASE)
176
+ return bool(re.fullmatch(r"[ \t,,、::&+/]*", remainder))
177
+
178
+
179
+ def _semantic_review_request_mentions(body: str) -> list[str]:
180
+ visible_body = re.sub(r"<!--.*?-->", " ", body, flags=re.DOTALL)
181
+ visible_body = re.sub(r"```.*?```", " ", visible_body, flags=re.DOTALL)
182
+ visible_body = "\n".join(
183
+ line for line in visible_body.splitlines() if not line.lstrip().startswith(">")
184
+ )
185
+ handles: list[str] = []
186
+ mentions = list(REVIEWER_COMMENT_MENTION_PATTERN.finditer(visible_body))
187
+ for intent_pattern in REVIEWER_COMMENT_INTENT_PATTERNS:
188
+ for intent in intent_pattern.finditer(visible_body):
189
+ preceding = [
190
+ mention
191
+ for mention in mentions
192
+ if mention.end() <= intent.start()
193
+ and intent.start() - mention.end() <= 160
194
+ ]
195
+ if not preceding:
196
+ continue
197
+ cluster = [preceding[-1]]
198
+ if not _is_mention_cluster_separator(
199
+ visible_body[cluster[0].end() : intent.start()]
200
+ ):
201
+ continue
202
+ for mention in reversed(preceding[:-1]):
203
+ gap = visible_body[mention.end() : cluster[0].start()]
204
+ if not _is_mention_cluster_separator(gap):
205
+ break
206
+ cluster.insert(0, mention)
207
+ for mention in cluster:
208
+ handle = _normalise_login(mention.group(1))
209
+ if handle and handle not in handles:
210
+ handles.append(handle)
211
+
212
+ for index, mention in enumerate(mentions):
213
+ next_mention_start = (
214
+ mentions[index + 1].start()
215
+ if index + 1 < len(mentions)
216
+ else len(visible_body)
217
+ )
218
+ window = visible_body[
219
+ mention.end() : min(next_mention_start, mention.end() + 160)
220
+ ]
221
+ if not any(
222
+ pattern.search(window) for pattern in REVIEWER_COMMENT_INTENT_PATTERNS
223
+ ):
224
+ continue
225
+ handle = _normalise_login(mention.group(1))
226
+ if handle and handle not in handles:
227
+ handles.append(handle)
228
+ return handles
229
+
230
+
231
+ def _metadata_identities(
232
+ payload: Mapping[str, Any],
233
+ *,
234
+ repo: str,
235
+ number: int,
236
+ ) -> dict[str, Any]:
237
+ author = payload.get("author")
238
+ author = author if isinstance(author, Mapping) else {}
239
+ author_handle = _normalise_login(author.get("login"))
240
+ pr_title = public_safe_compact_text(payload.get("title"), limit=180)
241
+ linked_issue_refs: list[str] = []
242
+ linked_issue_summaries: list[str] = []
243
+ raw_issue_refs = payload.get("closingIssuesReferences") or payload.get(
244
+ "closing_issues_references"
245
+ )
246
+ for item in raw_issue_refs or []:
247
+ issue_number = item.get("number") if isinstance(item, Mapping) else None
248
+ if isinstance(issue_number, int):
249
+ issue_ref = f"#{issue_number}"
250
+ if issue_ref not in linked_issue_refs:
251
+ linked_issue_refs.append(issue_ref)
252
+ issue_title = public_safe_compact_text(
253
+ item.get("title") if isinstance(item, Mapping) else None,
254
+ limit=140,
255
+ )
256
+ issue_summary = (
257
+ f"{issue_ref}「{issue_title}」" if issue_title else issue_ref
258
+ )
259
+ if issue_summary not in linked_issue_summaries:
260
+ linked_issue_summaries.append(issue_summary)
261
+
262
+ requested: list[str] = []
263
+ for item in payload.get("reviewRequests") or payload.get("review_requests") or []:
264
+ if not isinstance(item, Mapping):
265
+ continue
266
+ handle = _normalise_login(item.get("login"))
267
+ if not handle and item.get("slug"):
268
+ owner = repo.partition("/")[0]
269
+ handle = _normalise_login(f"{owner}/{item.get('slug')}")
270
+ if handle and handle not in requested:
271
+ requested.append(handle)
272
+
273
+ reviewed: list[str] = []
274
+ for item in payload.get("reviews") or []:
275
+ if not isinstance(item, Mapping):
276
+ continue
277
+ review_author = item.get("author")
278
+ review_author = review_author if isinstance(review_author, Mapping) else {}
279
+ handle = _normalise_login(review_author.get("login"))
280
+ if handle and handle not in reviewed:
281
+ reviewed.append(handle)
282
+ comment_notified: list[str] = []
283
+ marker_comment_notified: list[str] = []
284
+ semantic_comment_notified: list[str] = []
285
+ comment_urls: dict[str, str] = {}
286
+ for item in payload.get("comments") or []:
287
+ if not isinstance(item, Mapping):
288
+ continue
289
+ body = str(item.get("body") or "")
290
+ raw_url = public_safe_compact_text(item.get("url"), limit=300)
291
+ url = (
292
+ raw_url
293
+ if re.fullmatch(
294
+ rf"https://github\.com/{re.escape(repo)}/pull/{number}"
295
+ r"#issuecomment-\d+",
296
+ raw_url,
297
+ )
298
+ else ""
299
+ )
300
+ marker_handles: list[str] = []
301
+ for login in REVIEWER_COMMENT_MARKER_PATTERN.findall(body):
302
+ handle = _normalise_login(login)
303
+ if handle and handle not in marker_handles:
304
+ marker_handles.append(handle)
305
+ if handle and handle not in marker_comment_notified:
306
+ marker_comment_notified.append(handle)
307
+ if handle and handle not in comment_notified:
308
+ comment_notified.append(handle)
309
+ if handle and url:
310
+ comment_urls[handle] = url
311
+ for handle in _semantic_review_request_mentions(body):
312
+ if handle in marker_handles:
313
+ continue
314
+ if handle not in semantic_comment_notified:
315
+ semantic_comment_notified.append(handle)
316
+ if handle not in comment_notified:
317
+ comment_notified.append(handle)
318
+ if url:
319
+ comment_urls[handle] = url
320
+ return {
321
+ "author_handle": author_handle,
322
+ "pr_title": pr_title,
323
+ "linked_issue_refs": linked_issue_refs[:3],
324
+ "linked_issue_summaries": linked_issue_summaries[:3],
325
+ "requested_reviewers": requested,
326
+ "reviewed_by": reviewed,
327
+ "comment_notified_reviewers": comment_notified,
328
+ "marker_comment_notified_reviewers": marker_comment_notified,
329
+ "semantic_comment_notified_reviewers": semantic_comment_notified,
330
+ "reviewer_comment_urls": comment_urls,
331
+ "state": str(payload.get("state") or "UNKNOWN").upper(),
332
+ "review_decision": str(
333
+ payload.get("reviewDecision") or payload.get("review_decision") or "UNKNOWN"
334
+ ).upper(),
335
+ "is_draft": payload.get("isDraft") is True or payload.get("is_draft") is True,
336
+ }
337
+
338
+
339
+ def _fetch_pr_metadata(
340
+ *,
341
+ repo: str,
342
+ number: int,
343
+ runner: CommandRunner,
344
+ ) -> tuple[dict[str, Any] | None, str | None]:
345
+ try:
346
+ result = runner(
347
+ [
348
+ "gh",
349
+ "pr",
350
+ "view",
351
+ str(number),
352
+ "--repo",
353
+ repo,
354
+ "--json",
355
+ "author,closingIssuesReferences,comments,isDraft,reviewRequests,"
356
+ "mergeStateStatus,reviewDecision,reviews,state,statusCheckRollup,"
357
+ "title,url",
358
+ ]
359
+ )
360
+ except (OSError, subprocess.SubprocessError):
361
+ return None, "github_pr_metadata_unavailable"
362
+ if result.get("returncode") != 0:
363
+ return None, "github_pr_metadata_unavailable"
364
+ try:
365
+ payload = json.loads(str(result.get("stdout") or "{}"))
366
+ except json.JSONDecodeError:
367
+ return None, "github_pr_metadata_invalid"
368
+ return (
369
+ (dict(payload), None)
370
+ if isinstance(payload, Mapping)
371
+ else (None, "github_pr_metadata_invalid")
372
+ )
373
+
374
+
375
+ def fetch_issue_fix_reviewer_notification_metadata(
376
+ *,
377
+ repo: str,
378
+ number: int,
379
+ runner: CommandRunner = _default_runner,
380
+ ) -> tuple[dict[str, Any] | None, str | None]:
381
+ """Fetch compact live PR facts required before a queued group notification."""
382
+
383
+ payload, error = _fetch_pr_metadata(repo=repo, number=number, runner=runner)
384
+ if payload is None:
385
+ return None, error
386
+ identities = _metadata_identities(payload, repo=repo, number=number)
387
+ from .pr_lifecycle import build_issue_fix_pr_lifecycle_monitor_packet
388
+
389
+ lifecycle = build_issue_fix_pr_lifecycle_monitor_packet(
390
+ url=f"https://github.com/{repo}/pull/{number}",
391
+ provider_payload=payload,
392
+ generated_at=None,
393
+ )
394
+ grouped = lifecycle.get("grouped_monitor_projection")
395
+ grouped = grouped if isinstance(grouped, Mapping) else {}
396
+ identities["state_bucket"] = str(grouped.get("state_bucket") or "unknown")
397
+ # The grouped queue drain must persist the same verified lifecycle facts it
398
+ # used to cancel a stale notification. Keep the full public-safe packet
399
+ # internal to this adapter instead of reconstructing only a few fields.
400
+ identities["_lifecycle_packet"] = lifecycle
401
+ return identities, None
402
+
403
+
404
+ def _request_reviewers(
405
+ *,
406
+ repo: str,
407
+ number: int,
408
+ reviewer_handles: Sequence[str],
409
+ runner: CommandRunner,
410
+ ) -> tuple[str | None, bool]:
411
+ args = ["gh", "pr", "edit", str(number), "--repo", repo]
412
+ for handle in reviewer_handles:
413
+ login = handle.lstrip("@")
414
+ if "/" in login:
415
+ args.extend(["--add-team-reviewer", login.partition("/")[2]])
416
+ else:
417
+ args.extend(["--add-reviewer", login])
418
+ try:
419
+ result = runner(args)
420
+ except (OSError, subprocess.SubprocessError):
421
+ return "github_review_request_failed", False
422
+ if result.get("returncode") == 0:
423
+ return None, False
424
+ provider_error = " ".join(
425
+ str(result.get(key) or "") for key in ("stderr", "stdout")
426
+ )
427
+ permission_denied = bool(
428
+ GITHUB_REVIEW_REQUEST_PERMISSION_PATTERN.search(provider_error)
429
+ )
430
+ return (
431
+ "github_review_request_permission_denied"
432
+ if permission_denied
433
+ else "github_review_request_failed",
434
+ permission_denied,
435
+ )
436
+
437
+
438
+ def _reviewer_comment_body(
439
+ reviewer_handles: Sequence[str],
440
+ *,
441
+ pr_title: str | None,
442
+ linked_issue_summaries: Sequence[str],
443
+ ) -> str:
444
+ review_requests = ";".join(
445
+ f"{handle if handle.startswith('@') else f'@{handle}'} 请协助 review"
446
+ for handle in reviewer_handles
447
+ )
448
+ issue_context = "、".join(linked_issue_summaries) or "关联 issue"
449
+ change_context = public_safe_compact_text(pr_title, limit=180)
450
+ change_summary = f",改动是「{change_context}」" if change_context else ""
451
+ return (
452
+ f"{review_requests}:本 PR 修复 {issue_context}{change_summary}。"
453
+ "必要性、验证与风险已写在 PR 描述中,谢谢!"
454
+ )
455
+
456
+
457
+ def _comment_reviewer_notification(
458
+ *,
459
+ repo: str,
460
+ number: int,
461
+ reviewer_handles: Sequence[str],
462
+ pr_title: str | None,
463
+ linked_issue_summaries: Sequence[str],
464
+ runner: CommandRunner,
465
+ ) -> tuple[str | None, str | None, bool]:
466
+ try:
467
+ result = runner(
468
+ [
469
+ "gh",
470
+ "pr",
471
+ "comment",
472
+ str(number),
473
+ "--repo",
474
+ repo,
475
+ "--body",
476
+ _reviewer_comment_body(
477
+ reviewer_handles,
478
+ pr_title=pr_title,
479
+ linked_issue_summaries=linked_issue_summaries,
480
+ ),
481
+ ]
482
+ )
483
+ except (OSError, subprocess.SubprocessError):
484
+ return None, "github_reviewer_comment_fallback_failed", False
485
+ if result.get("returncode") != 0:
486
+ return None, "github_reviewer_comment_fallback_failed", False
487
+ expected = re.compile(
488
+ rf"https://github\.com/{re.escape(repo)}/pull/{number}#issuecomment-\d+"
489
+ )
490
+ match = expected.search(str(result.get("stdout") or ""))
491
+ return (match.group(0) if match else None), None, True
492
+
493
+
494
+ def _transition(
495
+ *,
496
+ decision: str,
497
+ action_kind: str,
498
+ reason: str,
499
+ material_change: bool,
500
+ ) -> dict[str, Any]:
501
+ return {
502
+ "schema_version": "issue_fix_reviewer_request_transition_v0",
503
+ "decision": decision,
504
+ "action_kind": action_kind,
505
+ "reason": reason,
506
+ "material_change": material_change,
507
+ "external_write_gate_required": True,
508
+ }
509
+
510
+
511
+ def build_issue_fix_reviewer_request_packet(
512
+ *,
513
+ repo_path: str | Path,
514
+ url: str,
515
+ changed_files: Sequence[str] = (),
516
+ base_ref: str = "origin/main",
517
+ history_limit: int = 40,
518
+ max_candidates: int = 5,
519
+ max_reviewers: int = 1,
520
+ exclude_reviewers: Sequence[str] = (),
521
+ exclude_author_names: Sequence[str] = (),
522
+ resolved_identities: Mapping[str, Any] | None = None,
523
+ reviewer_sources_input: Mapping[str, Any] | None = None,
524
+ notification_sinks_input: Mapping[str, Any] | None = None,
525
+ notification_sink_adapters: Mapping[str, NotificationSinkAdapter] | None = None,
526
+ reviewer_artifact_reward_memory: Mapping[str, Any] | None = None,
527
+ reviewer_notification_reward_memory: Mapping[str, Any] | None = None,
528
+ reviewer_artifact_required: bool = False,
529
+ pr_description_build: Mapping[str, Any] | None = None,
530
+ provider_payload: Mapping[str, Any] | None = None,
531
+ execute: bool = False,
532
+ generated_at: str | None = "2026-07-10T00:00:00Z",
533
+ notification_delivery_observed_at: str | None = None,
534
+ runner: CommandRunner = _default_runner,
535
+ ) -> dict[str, Any]:
536
+ """Select and optionally notify the top repository-native reviewer."""
537
+
538
+ if execute and provider_payload is not None:
539
+ raise ValueError(
540
+ "execute mode requires live GitHub PR metadata; metadata JSON is preview-only"
541
+ )
542
+
543
+ reference = normalise_github_issue_reference(
544
+ repo="public_repo_fixture",
545
+ issue_ref="pull_request_fixture",
546
+ url=url,
547
+ )
548
+ if reference.get("kind") != "pull_request" or not isinstance(
549
+ reference.get("number"), int
550
+ ):
551
+ raise ValueError("reviewer request requires a numeric GitHub pull request URL")
552
+ repo = str(reference["repo"])
553
+ number = int(reference["number"])
554
+ max_reviewers = min(max(int(max_reviewers), 1), 3)
555
+
556
+ external_reads = False
557
+ metadata_error: str | None = None
558
+ metadata = dict(provider_payload or {})
559
+ if not metadata and execute:
560
+ fetched, metadata_error = _fetch_pr_metadata(
561
+ repo=repo,
562
+ number=number,
563
+ runner=runner,
564
+ )
565
+ external_reads = True
566
+ metadata = fetched or {}
567
+ publication_gate = validate_issue_fix_pr_description_publication(
568
+ project=repo_path,
569
+ build_packet=pr_description_build,
570
+ )
571
+ identities = _metadata_identities(metadata, repo=repo, number=number)
572
+ excluded = list(exclude_reviewers)
573
+ if identities["author_handle"]:
574
+ excluded.append(str(identities["author_handle"]))
575
+ excluded.extend(identities["requested_reviewers"])
576
+ excluded.extend(identities["reviewed_by"])
577
+ excluded.extend(identities["comment_notified_reviewers"])
578
+
579
+ recommendation = build_issue_fix_reviewer_recommendation_packet(
580
+ repo_path=repo_path,
581
+ repo=repo,
582
+ changed_files=changed_files,
583
+ base_ref=base_ref,
584
+ history_limit=history_limit,
585
+ max_candidates=max_candidates,
586
+ exclude_reviewers=excluded,
587
+ exclude_author_names=exclude_author_names,
588
+ resolved_identities=resolved_identities,
589
+ reviewer_sources_input=reviewer_sources_input,
590
+ execute=True,
591
+ generated_at=generated_at,
592
+ )
593
+ candidates = recommendation.get("candidates")
594
+ candidates = candidates if isinstance(candidates, list) else []
595
+ existing_coverage = len(
596
+ set(
597
+ identities["requested_reviewers"]
598
+ + identities["reviewed_by"]
599
+ + identities["comment_notified_reviewers"]
600
+ )
601
+ )
602
+ remaining_slots = max(0, max_reviewers - existing_coverage)
603
+ selected = [
604
+ str(candidate.get("reviewer_handle"))
605
+ for candidate in candidates
606
+ if isinstance(candidate, Mapping)
607
+ and candidate.get("requestable") is True
608
+ and candidate.get("reviewer_handle")
609
+ and not _is_automated_reviewer_handle(candidate.get("reviewer_handle"))
610
+ ][:remaining_slots]
611
+ author_exclusion_verified = bool(identities["author_handle"])
612
+ pr_state_verified = identities["state"] in {"OPEN", "CLOSED", "MERGED"}
613
+ if (
614
+ not author_exclusion_verified
615
+ or not pr_state_verified
616
+ or publication_gate["ok"] is not True
617
+ ):
618
+ selected = []
619
+
620
+ existing_notified_reviewers = list(
621
+ dict.fromkeys(
622
+ identities["requested_reviewers"]
623
+ + identities["reviewed_by"]
624
+ + identities["comment_notified_reviewers"]
625
+ )
626
+ )
627
+ existing_comment_url = next(
628
+ iter(identities["reviewer_comment_urls"].values()),
629
+ None,
630
+ )
631
+ existing_notification_mode = (
632
+ "comment_fallback"
633
+ if identities["marker_comment_notified_reviewers"]
634
+ else "existing_review_comment"
635
+ if identities["semantic_comment_notified_reviewers"]
636
+ else (
637
+ "formal_request"
638
+ if identities["requested_reviewers"]
639
+ else "existing_review"
640
+ if identities["reviewed_by"]
641
+ else "none"
642
+ )
643
+ )
644
+
645
+ packet: dict[str, Any] = {
646
+ "ok": metadata_error is None and publication_gate["ok"] is True,
647
+ "schema_version": ISSUE_FIX_REVIEWER_REQUEST_SCHEMA_VERSION,
648
+ "mode": "issue-fix-reviewer-request",
649
+ "generated_at": generated_at,
650
+ "repo": repo,
651
+ "pr_ref": reference["issue_ref"],
652
+ "number": number,
653
+ "permalink": reference["permalink"],
654
+ "execute": execute,
655
+ "external_write_authority_asserted": execute,
656
+ "selection_policy": "request_top_requestable_when_authorized",
657
+ "max_reviewers": max_reviewers,
658
+ "author_handle": identities["author_handle"],
659
+ "pr_title": identities["pr_title"],
660
+ "linked_issue_refs": identities["linked_issue_refs"],
661
+ "linked_issue_summaries": identities["linked_issue_summaries"],
662
+ "author_exclusion_verified": author_exclusion_verified,
663
+ "pr_state_verified": pr_state_verified,
664
+ "existing_requested_reviewers": identities["requested_reviewers"],
665
+ "existing_reviewed_by": identities["reviewed_by"],
666
+ "existing_comment_notified_reviewers": identities["comment_notified_reviewers"],
667
+ "existing_marker_comment_notified_reviewers": identities[
668
+ "marker_comment_notified_reviewers"
669
+ ],
670
+ "existing_semantic_comment_notified_reviewers": identities[
671
+ "semantic_comment_notified_reviewers"
672
+ ],
673
+ "selected_reviewers": selected,
674
+ "requested_reviewers": [],
675
+ "notified_reviewers": existing_notified_reviewers,
676
+ "recommendation_status": recommendation.get("recommendation_status"),
677
+ "recommendation_candidates": candidates,
678
+ "reviewer_source_count": recommendation.get("reviewer_source_count"),
679
+ "reviewer_source_refs": recommendation.get("reviewer_source_refs"),
680
+ "external_reads_performed": external_reads,
681
+ "external_writes_performed": False,
682
+ "review_request_performed": False,
683
+ "review_request_verified": False,
684
+ "reviewer_notification_mode": existing_notification_mode,
685
+ "reviewer_notification_verified": bool(execute and existing_notified_reviewers),
686
+ "comment_fallback_performed": False,
687
+ "comment_fallback_verified": bool(
688
+ execute and identities["marker_comment_notified_reviewers"]
689
+ ),
690
+ "existing_comment_notification_verified": bool(
691
+ execute and identities["semantic_comment_notified_reviewers"]
692
+ ),
693
+ "reviewer_comment_url": existing_comment_url,
694
+ "pr_description_publication_gate": publication_gate,
695
+ "private_repo_state_read": True,
696
+ "local_paths_captured": False,
697
+ "raw_provider_payload_captured": False,
698
+ "raw_git_output_captured": False,
699
+ "commit_emails_captured": False,
700
+ }
701
+ if not execute and provider_payload is None:
702
+ packet["ok"] = False
703
+ packet["blocker"] = "github_pr_metadata_required_for_safe_preview"
704
+ packet["transition"] = _transition(
705
+ decision="blocker",
706
+ action_kind="issue_fix_reviewer_request_metadata_blocker",
707
+ reason=(
708
+ "Provide compact PR metadata for preview, or execute with "
709
+ "external-write authority so LoopX can exclude the live PR author."
710
+ ),
711
+ material_change=False,
712
+ )
713
+ elif metadata_error:
714
+ packet["blocker"] = metadata_error
715
+ packet["transition"] = _transition(
716
+ decision="blocker",
717
+ action_kind="issue_fix_reviewer_request_environment_blocker",
718
+ reason=(
719
+ "GitHub PR metadata is unavailable; do not request a reviewer "
720
+ "without excluding the PR author."
721
+ ),
722
+ material_change=True,
723
+ )
724
+ elif publication_gate["ok"] is not True:
725
+ packet["blocker"] = publication_gate["blocker"]
726
+ packet["transition"] = _transition(
727
+ decision="blocker",
728
+ action_kind="issue_fix_pr_description_recall_evidence",
729
+ reason=(
730
+ "Run the PR-description recall path and pass its compact build "
731
+ "packet before requesting review or sending a notification."
732
+ ),
733
+ material_change=False,
734
+ )
735
+ elif not author_exclusion_verified:
736
+ packet["ok"] = False
737
+ packet["blocker"] = "github_pr_author_unavailable"
738
+ packet["transition"] = _transition(
739
+ decision="blocker",
740
+ action_kind="issue_fix_reviewer_request_author_blocker",
741
+ reason=(
742
+ "PR author identity is unavailable; fail closed before reviewer "
743
+ "selection or external write."
744
+ ),
745
+ material_change=False,
746
+ )
747
+ elif not pr_state_verified:
748
+ packet["ok"] = False
749
+ packet["blocker"] = "github_pr_state_unavailable"
750
+ packet["transition"] = _transition(
751
+ decision="blocker",
752
+ action_kind="issue_fix_reviewer_request_state_blocker",
753
+ reason=(
754
+ "PR state is unavailable; fail closed before reviewer selection "
755
+ "or external write."
756
+ ),
757
+ material_change=False,
758
+ )
759
+ elif identities["state"] != "OPEN":
760
+ packet["transition"] = _transition(
761
+ decision="no_followup",
762
+ action_kind="issue_fix_reviewer_request_terminal_skip",
763
+ reason="PR is not open; no reviewer request should be sent.",
764
+ material_change=False,
765
+ )
766
+ elif not selected:
767
+ already_covered = bool(
768
+ identities["requested_reviewers"]
769
+ or identities["reviewed_by"]
770
+ or identities["comment_notified_reviewers"]
771
+ )
772
+ packet["transition"] = _transition(
773
+ decision="monitor_continuation"
774
+ if already_covered
775
+ else "runnable_successor",
776
+ action_kind=(
777
+ "issue_fix_reviewer_request_already_covered"
778
+ if already_covered
779
+ else "issue_fix_reviewer_identity_resolution"
780
+ ),
781
+ reason=(
782
+ "A reviewer is already requested, has reviewed, or is covered "
783
+ "by an existing explicit review-request comment; keep lifecycle "
784
+ "monitoring."
785
+ if already_covered
786
+ else (
787
+ "No requestable non-author reviewer identity is available; "
788
+ "resolve a candidate handle."
789
+ )
790
+ ),
791
+ material_change=False,
792
+ )
793
+ elif not execute:
794
+ packet["transition"] = _transition(
795
+ decision="runnable_successor",
796
+ action_kind="issue_fix_request_top_reviewer",
797
+ reason=(
798
+ "External-write authority may execute the top requestable "
799
+ "reviewer request."
800
+ ),
801
+ material_change=False,
802
+ )
803
+ else:
804
+ request_error, permission_denied = _request_reviewers(
805
+ repo=repo,
806
+ number=number,
807
+ reviewer_handles=selected,
808
+ runner=runner,
809
+ )
810
+ packet["external_writes_performed"] = request_error is None
811
+ if request_error and permission_denied:
812
+ packet["formal_request_error"] = request_error
813
+ comment_url, comment_error, comment_performed = (
814
+ _comment_reviewer_notification(
815
+ repo=repo,
816
+ number=number,
817
+ reviewer_handles=selected,
818
+ pr_title=identities["pr_title"],
819
+ linked_issue_summaries=identities["linked_issue_summaries"],
820
+ runner=runner,
821
+ )
822
+ )
823
+ packet["reviewer_notification_mode"] = "comment_fallback"
824
+ packet["reviewer_comment_url"] = comment_url
825
+ packet["comment_fallback_performed"] = comment_performed
826
+ packet["external_writes_performed"] = comment_performed
827
+ if comment_error:
828
+ packet["ok"] = False
829
+ packet["blocker"] = comment_error
830
+ packet["transition"] = _transition(
831
+ decision="blocker",
832
+ action_kind="issue_fix_reviewer_comment_fallback_blocker",
833
+ reason=(
834
+ "GitHub denied the formal reviewer request and the "
835
+ "fallback reviewer comment could not be published."
836
+ ),
837
+ material_change=True,
838
+ )
839
+ else:
840
+ verified_payload, verify_error = _fetch_pr_metadata(
841
+ repo=repo,
842
+ number=number,
843
+ runner=runner,
844
+ )
845
+ packet["external_reads_performed"] = True
846
+ verified = _metadata_identities(
847
+ verified_payload or {},
848
+ repo=repo,
849
+ number=number,
850
+ )
851
+ notified = [
852
+ handle
853
+ for handle in selected
854
+ if handle in verified["comment_notified_reviewers"]
855
+ ]
856
+ verified_urls = [
857
+ verified["reviewer_comment_urls"].get(handle)
858
+ for handle in selected
859
+ if verified["reviewer_comment_urls"].get(handle)
860
+ ]
861
+ readback_url = (
862
+ verified_urls[0]
863
+ if len(verified_urls) == len(selected)
864
+ and len(set(verified_urls)) == 1
865
+ else None
866
+ )
867
+ packet["reviewer_comment_url"] = readback_url or comment_url
868
+ packet["notified_reviewers"] = notified
869
+ packet["comment_fallback_verified"] = bool(
870
+ not verify_error
871
+ and readback_url
872
+ and len(notified) == len(selected)
873
+ and (not comment_url or readback_url == comment_url)
874
+ )
875
+ packet["reviewer_notification_verified"] = packet[
876
+ "comment_fallback_verified"
877
+ ]
878
+ if not packet["comment_fallback_verified"]:
879
+ packet["ok"] = False
880
+ packet["blocker"] = "github_reviewer_comment_not_verified"
881
+ packet["transition"] = _transition(
882
+ decision="blocker",
883
+ action_kind="issue_fix_reviewer_comment_verification_blocker",
884
+ reason=(
885
+ "Fallback comment command returned success but PR "
886
+ "readback did not confirm the semantic review request "
887
+ "and URL."
888
+ ),
889
+ material_change=True,
890
+ )
891
+ else:
892
+ packet["transition"] = _transition(
893
+ decision="monitor_continuation",
894
+ action_kind="issue_fix_reviewer_comment_fallback_verified",
895
+ reason=(
896
+ "Formal reviewer request lacked permission; a verified "
897
+ "public comment notified the selected reviewer."
898
+ ),
899
+ material_change=True,
900
+ )
901
+ elif request_error:
902
+ packet["ok"] = False
903
+ packet["blocker"] = request_error
904
+ packet["transition"] = _transition(
905
+ decision="blocker",
906
+ action_kind="issue_fix_reviewer_request_provider_blocker",
907
+ reason=(
908
+ "GitHub reviewer-request failure was not a confirmed permission "
909
+ "denial; do not post a fallback comment until the provider or "
910
+ "network error is classified."
911
+ ),
912
+ material_change=True,
913
+ )
914
+ else:
915
+ verified_payload, verify_error = _fetch_pr_metadata(
916
+ repo=repo,
917
+ number=number,
918
+ runner=runner,
919
+ )
920
+ packet["external_reads_performed"] = True
921
+ verified = _metadata_identities(
922
+ verified_payload or {},
923
+ repo=repo,
924
+ number=number,
925
+ )
926
+ requested = [
927
+ handle
928
+ for handle in selected
929
+ if handle in verified["requested_reviewers"]
930
+ ]
931
+ packet["requested_reviewers"] = requested
932
+ packet["review_request_performed"] = bool(requested)
933
+ packet["review_request_verified"] = len(requested) == len(selected)
934
+ packet["notified_reviewers"] = requested
935
+ packet["reviewer_notification_mode"] = "formal_request"
936
+ packet["reviewer_notification_verified"] = packet["review_request_verified"]
937
+ if verify_error or not packet["review_request_verified"]:
938
+ packet["ok"] = False
939
+ packet["blocker"] = "github_review_request_not_verified"
940
+ packet["transition"] = _transition(
941
+ decision="blocker",
942
+ action_kind="issue_fix_reviewer_request_verification_blocker",
943
+ reason=(
944
+ "Reviewer request command returned success but the PR "
945
+ "did not confirm every selected reviewer."
946
+ ),
947
+ material_change=True,
948
+ )
949
+ else:
950
+ packet["transition"] = _transition(
951
+ decision="monitor_continuation",
952
+ action_kind="issue_fix_reviewer_request_verified",
953
+ reason=(
954
+ "Reviewer request is visible on the PR; continue lifecycle "
955
+ "monitoring."
956
+ ),
957
+ material_change=True,
958
+ )
959
+
960
+ packet["secondary_notification_configured"] = bool(notification_sinks_input)
961
+ packet["secondary_notification_status"] = "not_configured"
962
+ packet["secondary_notification_verified"] = False
963
+ reviewer_artifact_application: dict[str, Any] | None = None
964
+ reward_memory_error: str | None = None
965
+ if reviewer_artifact_reward_memory and (
966
+ packet.get("selected_reviewers") or packet.get("notified_reviewers")
967
+ ):
968
+ config = reviewer_artifact_reward_memory.get("config")
969
+ config = config if isinstance(config, Mapping) else {}
970
+ try:
971
+ reviewer_artifact_application = (
972
+ run_issue_fix_reviewer_artifact_automatic_reward_memory(
973
+ {
974
+ "repo": repo,
975
+ "pr_ref": f"#{number}",
976
+ "permalink": str(reference["permalink"]),
977
+ "source_title": identities["pr_title"],
978
+ "summary": "",
979
+ },
980
+ reviewer_summary=str(
981
+ reviewer_artifact_reward_memory.get("reviewer_summary") or ""
982
+ ),
983
+ reasoning_summary=str(
984
+ reviewer_artifact_reward_memory.get("reasoning_summary") or ""
985
+ ),
986
+ experiment_config=config,
987
+ revision_ref=f"pr:{number}:{identities['state'].lower()}",
988
+ observed_at=str(
989
+ reviewer_artifact_reward_memory.get("observed_at")
990
+ or generated_at
991
+ ),
992
+ freshness_context={
993
+ "source_truth_current": pr_state_verified,
994
+ "source_revision": (
995
+ f"pr:{number}:{identities['state'].lower()}"
996
+ ),
997
+ },
998
+ conflict_state="clear",
999
+ application_id=f"issue-fix:reviewer-artifact:{repo}:{number}",
1000
+ artifact_ref=f"github:{repo}#pr-{number}",
1001
+ provider=reviewer_artifact_reward_memory.get("provider"),
1002
+ )
1003
+ )
1004
+ except (OSError, RuntimeError, ValueError):
1005
+ reward_memory_error = "reward_memory_application_unavailable"
1006
+ packet["reviewer_artifact_reward_memory_required"] = reviewer_artifact_required
1007
+ packet["reviewer_artifact_reward_memory_status"] = (
1008
+ (reviewer_artifact_application or {}).get("notification_gate", {}).get("status")
1009
+ if reviewer_artifact_application
1010
+ else "unavailable"
1011
+ if reviewer_artifact_reward_memory
1012
+ else "not_configured"
1013
+ )
1014
+ if reviewer_artifact_application is not None:
1015
+ packet["reviewer_artifact_reward_memory_preview"] = (
1016
+ reviewer_artifact_application
1017
+ )
1018
+ if reward_memory_error:
1019
+ packet["reviewer_artifact_reward_memory_blocker"] = reward_memory_error
1020
+ if notification_sinks_input:
1021
+ notification_targets = list(packet.get("selected_reviewers") or [])
1022
+ if packet.get("notified_reviewers"):
1023
+ notification_targets = list(packet.get("notified_reviewers") or [])
1024
+ if execute:
1025
+ completed_reviewers = set(packet.get("existing_reviewed_by") or [])
1026
+ notification_targets = [
1027
+ handle
1028
+ for handle in notification_targets
1029
+ if handle not in completed_reviewers
1030
+ ]
1031
+ primary_notification_targets = list(notification_targets)
1032
+ notification_targets, notification_candidate_pool = (
1033
+ _secondary_notification_targets(
1034
+ primary_handles=primary_notification_targets,
1035
+ candidates=candidates,
1036
+ sinks_input=notification_sinks_input,
1037
+ allow_candidate_fallback=bool(packet.get("selected_reviewers")),
1038
+ )
1039
+ )
1040
+ packet["secondary_notification_primary_targets"] = primary_notification_targets
1041
+ packet["secondary_notification_candidate_pool"] = notification_candidate_pool
1042
+ packet["secondary_notification_targets"] = notification_targets
1043
+ packet["secondary_notification_fallback_used"] = bool(
1044
+ notification_targets
1045
+ and notification_targets != primary_notification_targets
1046
+ )
1047
+ if notification_targets:
1048
+ reviewer_notification_policy_application: dict[str, Any] | None = None
1049
+ if reviewer_notification_reward_memory:
1050
+ config = reviewer_notification_reward_memory.get("config")
1051
+ config = config if isinstance(config, Mapping) else {}
1052
+ try:
1053
+ reviewer_notification_policy_application = (
1054
+ run_issue_fix_reviewer_notification_automatic_reward_memory(
1055
+ repo=repo,
1056
+ pr_number=number,
1057
+ pr_url=str(reference["permalink"]),
1058
+ delivery_policy=(
1059
+ notification_sinks_input.get("delivery_policy")
1060
+ if isinstance(notification_sinks_input, Mapping)
1061
+ else None
1062
+ ),
1063
+ experiment_config=config,
1064
+ revision_ref=f"pr:{number}:{identities['state'].lower()}",
1065
+ observed_at=str(
1066
+ reviewer_notification_reward_memory.get("observed_at")
1067
+ or generated_at
1068
+ ),
1069
+ freshness_context={
1070
+ "source_truth_current": pr_state_verified,
1071
+ "source_revision": (
1072
+ f"pr:{number}:{identities['state'].lower()}"
1073
+ ),
1074
+ },
1075
+ conflict_state="clear",
1076
+ application_id=(
1077
+ f"issue-fix:reviewer-notification:{repo}:{number}"
1078
+ ),
1079
+ provider=reviewer_notification_reward_memory.get(
1080
+ "provider"
1081
+ ),
1082
+ )
1083
+ )
1084
+ except (OSError, RuntimeError, ValueError):
1085
+ reviewer_notification_policy_application = None
1086
+ packet["reviewer_notification_reward_memory_status"] = (
1087
+ (
1088
+ reviewer_notification_policy_application.get("before_send_gate", {})
1089
+ if reviewer_notification_policy_application
1090
+ else {}
1091
+ ).get("status", "unavailable")
1092
+ if reviewer_notification_reward_memory
1093
+ else "not_configured"
1094
+ )
1095
+ if reviewer_notification_policy_application is not None:
1096
+ packet["reviewer_notification_reward_memory_preview"] = (
1097
+ reviewer_notification_policy_application
1098
+ )
1099
+ secondary = build_issue_fix_reviewer_notification_sinks_result(
1100
+ repo=repo,
1101
+ pr_number=number,
1102
+ pr_url=str(reference["permalink"]),
1103
+ pr_title=identities["pr_title"],
1104
+ linked_issue_refs=identities["linked_issue_refs"],
1105
+ author_handle=identities["author_handle"],
1106
+ reviewer_handles=notification_targets,
1107
+ sinks_input=notification_sinks_input,
1108
+ reviewer_artifact_application=reviewer_artifact_application,
1109
+ reviewer_notification_policy_application=(
1110
+ reviewer_notification_policy_application
1111
+ ),
1112
+ reviewer_artifact_required=reviewer_artifact_required,
1113
+ execute=execute,
1114
+ delivery_observed_at=notification_delivery_observed_at,
1115
+ runner=runner,
1116
+ sink_adapters=notification_sink_adapters,
1117
+ )
1118
+ packet["secondary_notifications"] = secondary
1119
+ packet["secondary_notification_status"] = secondary.get("status")
1120
+ packet["secondary_notification_blocker"] = secondary.get("blocker")
1121
+ packet["secondary_notification_verified"] = bool(
1122
+ secondary.get("notification_verified")
1123
+ )
1124
+ packet["external_writes_performed"] = bool(
1125
+ packet["external_writes_performed"]
1126
+ or secondary.get("external_writes_performed")
1127
+ )
1128
+ else:
1129
+ packet["secondary_notification_status"] = (
1130
+ "skipped_reviewer_already_reviewed"
1131
+ if execute and packet.get("existing_reviewed_by")
1132
+ else "skipped_reviewer_unavailable"
1133
+ )
1134
+
1135
+ validation = validate_issue_fix_reviewer_request_packet(packet)
1136
+ packet["ok"] = bool(packet["ok"] and validation["ok"])
1137
+ packet["validation"] = validation
1138
+ return packet
1139
+
1140
+
1141
+ def validate_issue_fix_reviewer_request_packet(
1142
+ packet: Mapping[str, Any],
1143
+ ) -> dict[str, Any]:
1144
+ errors: list[str] = []
1145
+ if packet.get("schema_version") != ISSUE_FIX_REVIEWER_REQUEST_SCHEMA_VERSION:
1146
+ errors.append("schema_version must be issue_fix_reviewer_request_v0")
1147
+ if packet.get("local_paths_captured") is not False:
1148
+ errors.append("local_paths_captured must be false")
1149
+ if packet.get("raw_provider_payload_captured") is not False:
1150
+ errors.append("raw_provider_payload_captured must be false")
1151
+ if packet.get("raw_git_output_captured") is not False:
1152
+ errors.append("raw_git_output_captured must be false")
1153
+ if packet.get("commit_emails_captured") is not False:
1154
+ errors.append("commit_emails_captured must be false")
1155
+ performed = packet.get("review_request_performed") is True
1156
+ verified = packet.get("review_request_verified") is True
1157
+ requested = packet.get("requested_reviewers")
1158
+ if not isinstance(requested, list):
1159
+ errors.append("requested_reviewers must be a list")
1160
+ requested = []
1161
+ if performed != bool(requested):
1162
+ errors.append(
1163
+ "review_request_performed must reflect verified requested_reviewers"
1164
+ )
1165
+ if performed and packet.get("external_writes_performed") is not True:
1166
+ errors.append(
1167
+ "verified reviewer requests require external_writes_performed=true"
1168
+ )
1169
+ if verified and not performed:
1170
+ errors.append("review_request_verified requires a performed request")
1171
+ selected = packet.get("selected_reviewers")
1172
+ if not isinstance(selected, list):
1173
+ errors.append("selected_reviewers must be a list")
1174
+ selected = []
1175
+ author = packet.get("author_handle")
1176
+ if selected and packet.get("author_exclusion_verified") is not True:
1177
+ errors.append("reviewer selection requires verified PR author exclusion")
1178
+ if selected and packet.get("pr_state_verified") is not True:
1179
+ errors.append("reviewer selection requires verified open PR state")
1180
+ if author and author in selected:
1181
+ errors.append("the PR author must not be selected as reviewer")
1182
+ if verified and set(requested) != set(selected):
1183
+ errors.append("verified reviewer requests must match selected_reviewers")
1184
+ fallback_performed = packet.get("comment_fallback_performed") is True
1185
+ fallback_verified = packet.get("comment_fallback_verified") is True
1186
+ comment_url = packet.get("reviewer_comment_url")
1187
+ notified = packet.get("notified_reviewers")
1188
+ if not isinstance(notified, list):
1189
+ errors.append("notified_reviewers must be a list")
1190
+ notified = []
1191
+ if fallback_performed and packet.get("external_writes_performed") is not True:
1192
+ errors.append("comment fallback requires external_writes_performed=true")
1193
+ if fallback_verified and not comment_url:
1194
+ errors.append("verified comment fallback requires reviewer_comment_url")
1195
+ if fallback_verified and set(notified) != set(selected):
1196
+ existing_comment_notified = packet.get("existing_comment_notified_reviewers")
1197
+ existing_comment_notified = (
1198
+ existing_comment_notified
1199
+ if isinstance(existing_comment_notified, list)
1200
+ else []
1201
+ )
1202
+ if set(notified) != set(existing_comment_notified):
1203
+ errors.append(
1204
+ "verified comment fallback must notify selected or existing reviewers"
1205
+ )
1206
+ notification_verified = packet.get("reviewer_notification_verified") is True
1207
+ existing_comment_verified = (
1208
+ packet.get("existing_comment_notification_verified") is True
1209
+ )
1210
+ existing_semantic_comment_notified = packet.get(
1211
+ "existing_semantic_comment_notified_reviewers"
1212
+ )
1213
+ existing_semantic_comment_notified = (
1214
+ existing_semantic_comment_notified
1215
+ if isinstance(existing_semantic_comment_notified, list)
1216
+ else []
1217
+ )
1218
+ if existing_comment_verified and not existing_semantic_comment_notified:
1219
+ errors.append(
1220
+ "existing comment verification requires semantic notified reviewers"
1221
+ )
1222
+ if existing_comment_verified and not comment_url:
1223
+ errors.append("existing comment verification requires reviewer_comment_url")
1224
+ existing_notification_verified = bool(
1225
+ packet.get("execute") is True
1226
+ and (
1227
+ packet.get("existing_requested_reviewers")
1228
+ or packet.get("existing_reviewed_by")
1229
+ or existing_comment_verified
1230
+ )
1231
+ )
1232
+ if notification_verified != bool(
1233
+ verified or fallback_verified or existing_notification_verified
1234
+ ):
1235
+ errors.append(
1236
+ "reviewer_notification_verified must reflect formal or comment verification"
1237
+ )
1238
+ secondary = packet.get("secondary_notifications")
1239
+ if secondary is not None:
1240
+ if not isinstance(secondary, Mapping):
1241
+ errors.append("secondary_notifications must be an object")
1242
+ else:
1243
+ secondary_validation = secondary.get("validation")
1244
+ if not isinstance(secondary_validation, Mapping) or (
1245
+ secondary_validation.get("ok") is not True
1246
+ ):
1247
+ errors.append("secondary notification contract validation must pass")
1248
+ return {
1249
+ "ok": not errors,
1250
+ "schema_version": "issue_fix_reviewer_request_validation_v0",
1251
+ "errors": errors,
1252
+ }
1253
+
1254
+
1255
+ def render_issue_fix_reviewer_request_markdown(
1256
+ payload: Mapping[str, Any],
1257
+ ) -> str:
1258
+ return "\n".join(
1259
+ [
1260
+ "# LoopX Issue-Fix Reviewer Request",
1261
+ "",
1262
+ f"- ok: {payload.get('ok')}",
1263
+ f"- repo: {payload.get('repo')}",
1264
+ f"- pr_ref: {payload.get('pr_ref')}",
1265
+ f"- selected_reviewers: {','.join(payload.get('selected_reviewers') or [])}",
1266
+ f"- requested_reviewers: {','.join(payload.get('requested_reviewers') or [])}",
1267
+ f"- review_request_performed: {payload.get('review_request_performed')}",
1268
+ f"- review_request_verified: {payload.get('review_request_verified')}",
1269
+ f"- reviewer_notification_mode: {payload.get('reviewer_notification_mode')}",
1270
+ "- reviewer_notification_verified: "
1271
+ f"{payload.get('reviewer_notification_verified')}",
1272
+ f"- reviewer_comment_url: {payload.get('reviewer_comment_url')}",
1273
+ "- secondary_notification_status: "
1274
+ f"{payload.get('secondary_notification_status')}",
1275
+ "- secondary_notification_verified: "
1276
+ f"{payload.get('secondary_notification_verified')}",
1277
+ "- secondary_notification_blocker: "
1278
+ f"{payload.get('secondary_notification_blocker')}",
1279
+ f"- transition: {(payload.get('transition') or {}).get('decision')}",
1280
+ f"- next: {(payload.get('transition') or {}).get('reason')}",
1281
+ ]
1282
+ )