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
loopx/pr_review.py ADDED
@@ -0,0 +1,1206 @@
1
+ from __future__ import annotations
2
+
3
+ import json
4
+ import re
5
+ import subprocess
6
+ from collections.abc import Mapping
7
+ from datetime import datetime, timezone
8
+ from pathlib import Path
9
+ from typing import Any
10
+
11
+ from .capabilities.pr_review_queue import (
12
+ build_agent_response_contract,
13
+ build_review_plan,
14
+ build_review_template,
15
+ )
16
+ from .control_plane.runtime.time import now_utc_iso
17
+ from .presentation.markdown import as_dict as _as_dict
18
+ from .presentation.markdown import as_list as _as_list
19
+ from .presentation.public_safety import public_safe_boundary, redact_public_text
20
+
21
+ COMMAND = "/loopx-pr-review"
22
+ SCHEMA_VERSION = "loopx_pr_review_command_response_v0"
23
+
24
+ BOUNDARY = public_safe_boundary()
25
+
26
+ SOURCE_SURFACES = [
27
+ "GitHub pull request metadata",
28
+ "GitHub pull request body summary",
29
+ "GitHub pull request changed-file list",
30
+ "GitHub pull request status check rollup",
31
+ ]
32
+
33
+ RUNTIME_OR_CLI_PREFIXES = (
34
+ "src/",
35
+ "lib/",
36
+ "pkg/",
37
+ "packages/",
38
+ "cmd/",
39
+ "internal/",
40
+ "server/",
41
+ "backend/",
42
+ "app/",
43
+ "apps/",
44
+ "scripts/",
45
+ "bin/",
46
+ "tools/",
47
+ )
48
+
49
+ CODE_EXTENSIONS = (
50
+ ".py",
51
+ ".js",
52
+ ".jsx",
53
+ ".ts",
54
+ ".tsx",
55
+ ".go",
56
+ ".rs",
57
+ ".java",
58
+ ".kt",
59
+ ".kts",
60
+ ".c",
61
+ ".cc",
62
+ ".cpp",
63
+ ".h",
64
+ ".hpp",
65
+ ".cs",
66
+ ".rb",
67
+ ".php",
68
+ ".swift",
69
+ ".m",
70
+ ".mm",
71
+ )
72
+
73
+ UI_PREFIXES = (
74
+ "apps/presentation/dashboard/",
75
+ "apps/web/",
76
+ "apps/frontend/",
77
+ "apps/site/",
78
+ "web/",
79
+ "frontend/",
80
+ "ui/",
81
+ "components/",
82
+ "pages/",
83
+ "views/",
84
+ "public/",
85
+ )
86
+
87
+
88
+ def _now_iso() -> str:
89
+ return now_utc_iso()
90
+
91
+
92
+ def _redact_text(value: object, *, limit: int = 320) -> str:
93
+ return redact_public_text(value, limit=limit)
94
+
95
+
96
+ def _join_short(items: list[str], *, limit: int = 3, fallback: str = "未提供") -> str:
97
+ compact = [str(item).strip() for item in items if str(item).strip()]
98
+ if not compact:
99
+ return fallback
100
+ return "、".join(compact[:limit])
101
+
102
+
103
+ def _run_gh_json(args: list[str], *, cwd: Path | None = None) -> Any:
104
+ proc = subprocess.run(
105
+ ["gh", *args],
106
+ cwd=cwd,
107
+ check=True,
108
+ text=True,
109
+ stdout=subprocess.PIPE,
110
+ stderr=subprocess.PIPE,
111
+ )
112
+ return json.loads(proc.stdout or "null")
113
+
114
+
115
+ def _attach_status_check_rollup(
116
+ row: dict[str, Any],
117
+ *,
118
+ repository: str | None,
119
+ cwd: Path | None = None,
120
+ ) -> None:
121
+ """Attach check-run status for one exact PR head.
122
+
123
+ ``gh pr list`` does not support ``statusCheckRollup`` in its ``--json``
124
+ field set, so the queue scan fetches check runs by exact ``headRefOid``
125
+ through the commits check-runs API instead. A failed lookup leaves the
126
+ rollup absent; the risk hint then reports that no rollup was available.
127
+ """
128
+
129
+ head_oid = str(row.get("headRefOid") or "").strip()
130
+ if not head_oid or not repository:
131
+ return
132
+ try:
133
+ runs = _run_gh_json(
134
+ [
135
+ "api",
136
+ f"repos/{repository}/commits/{head_oid}/check-runs",
137
+ "--paginate",
138
+ "-q",
139
+ ".check_runs",
140
+ ],
141
+ cwd=cwd,
142
+ )
143
+ except Exception:
144
+ return
145
+ if not isinstance(runs, list):
146
+ return
147
+ row["statusCheckRollup"] = [
148
+ {
149
+ "name": str(item.get("name") or item.get("context") or ""),
150
+ "status": str(item.get("status") or ""),
151
+ "conclusion": str(item.get("conclusion") or ""),
152
+ }
153
+ for item in runs
154
+ if isinstance(item, dict)
155
+ ]
156
+
157
+
158
+ def resolve_current_github_repository(*, cwd: Path | None = None) -> str | None:
159
+ try:
160
+ payload = _run_gh_json(["repo", "view", "--json", "nameWithOwner"], cwd=cwd)
161
+ except Exception:
162
+ return None
163
+ if not isinstance(payload, dict):
164
+ return None
165
+ return str(payload.get("nameWithOwner") or "") or None
166
+
167
+
168
+ def _parse_timestamp(value: object) -> datetime | None:
169
+ text = str(value or "").strip()
170
+ if not text:
171
+ return None
172
+ try:
173
+ parsed = datetime.fromisoformat(text.replace("Z", "+00:00"))
174
+ except ValueError:
175
+ return None
176
+ if parsed.tzinfo is None:
177
+ return parsed.replace(tzinfo=timezone.utc)
178
+ return parsed
179
+
180
+
181
+ def _github_search_date(value: object) -> str | None:
182
+ parsed = _parse_timestamp(value)
183
+ if parsed:
184
+ return parsed.astimezone(timezone.utc).date().isoformat()
185
+ text = str(value or "").strip()
186
+ if re.fullmatch(r"\d{4}-\d{2}-\d{2}", text):
187
+ return text
188
+ return None
189
+
190
+
191
+ def _pr_window_timestamp(pr: dict[str, Any]) -> datetime | None:
192
+ return (
193
+ _parse_timestamp(pr.get("mergedAt") or pr.get("merged_at"))
194
+ or _parse_timestamp(pr.get("closedAt") or pr.get("closed_at"))
195
+ or _parse_timestamp(pr.get("updatedAt") or pr.get("updated_at"))
196
+ )
197
+
198
+
199
+ def _include_pr_in_window(pr: dict[str, Any], *, since: object | None) -> bool:
200
+ since_dt = _parse_timestamp(since)
201
+ if since_dt is None:
202
+ return True
203
+ activity_dt = _pr_window_timestamp(pr)
204
+ return bool(activity_dt and activity_dt >= since_dt)
205
+
206
+
207
+ def normalize_pr_state_filter(value: object) -> str:
208
+ state = str(value or "all").strip().lower()
209
+ return state if state in {"open", "merged", "all"} else "all"
210
+
211
+
212
+ def fetch_github_pull_requests(
213
+ *,
214
+ repo: str | None,
215
+ limit: int,
216
+ cwd: Path | None = None,
217
+ state_filter: str = "all",
218
+ since: str | None = None,
219
+ ) -> list[dict[str, Any]]:
220
+ scan = scan_github_pull_requests(
221
+ repo=repo,
222
+ limit=limit,
223
+ cwd=cwd,
224
+ state_filter=state_filter,
225
+ since=since,
226
+ )
227
+ return list(scan["pull_requests"])
228
+
229
+
230
+ def scan_github_pull_requests(
231
+ *,
232
+ repo: str | None,
233
+ limit: int,
234
+ cwd: Path | None = None,
235
+ state_filter: str = "all",
236
+ since: str | None = None,
237
+ ) -> dict[str, Any]:
238
+ repo_args = ["--repo", repo] if repo else []
239
+ api_repository = repo or resolve_current_github_repository(cwd=cwd)
240
+ normalized_state = normalize_pr_state_filter(state_filter)
241
+ search_args: list[str] = []
242
+ search_date = _github_search_date(since)
243
+ if search_date:
244
+ search_args = ["--search", f"updated:>={search_date}"]
245
+ list_fields = [
246
+ "number",
247
+ "title",
248
+ "url",
249
+ "state",
250
+ "isDraft",
251
+ "reviewDecision",
252
+ "mergeStateStatus",
253
+ "headRefName",
254
+ "headRefOid",
255
+ "baseRefName",
256
+ "author",
257
+ "updatedAt",
258
+ "closedAt",
259
+ "mergedAt",
260
+ "mergeCommit",
261
+ "body",
262
+ "files",
263
+ "changedFiles",
264
+ "additions",
265
+ "deletions",
266
+ ]
267
+ fetch_limit = max(1, limit)
268
+ if since:
269
+ fetch_limit = max(fetch_limit, min(100, fetch_limit * 3))
270
+
271
+ detailed: list[dict[str, Any]] = []
272
+ seen_numbers: set[str] = set()
273
+ state_scans: list[dict[str, Any]] = []
274
+
275
+ def append_state(state: str) -> None:
276
+ rows = _run_gh_json(
277
+ [
278
+ "pr",
279
+ "list",
280
+ "--state",
281
+ state,
282
+ "--limit",
283
+ str(fetch_limit),
284
+ "--json",
285
+ ",".join(list_fields),
286
+ *search_args,
287
+ *repo_args,
288
+ ],
289
+ cwd=cwd,
290
+ )
291
+ if not isinstance(rows, list):
292
+ state_scans.append(
293
+ {
294
+ "state": state,
295
+ "fetch_limit": fetch_limit,
296
+ "fetched_count": 0,
297
+ "included_after_window": 0,
298
+ "source_saturated": False,
299
+ "source_read_valid": False,
300
+ }
301
+ )
302
+ return
303
+ included_before = len(detailed)
304
+ for row in rows:
305
+ if not isinstance(row, dict):
306
+ continue
307
+ if not _include_pr_in_window(row, since=since):
308
+ continue
309
+ number = str(row.get("number") or row.get("url") or "")
310
+ if number and number in seen_numbers:
311
+ continue
312
+ if number:
313
+ seen_numbers.add(number)
314
+ _attach_status_check_rollup(
315
+ row,
316
+ repository=api_repository,
317
+ cwd=cwd,
318
+ )
319
+ detailed.append(row)
320
+ state_scans.append(
321
+ {
322
+ "state": state,
323
+ "fetch_limit": fetch_limit,
324
+ "fetched_count": len(rows),
325
+ "included_after_window": len(detailed) - included_before,
326
+ "source_saturated": len(rows) >= fetch_limit,
327
+ "source_read_valid": True,
328
+ }
329
+ )
330
+
331
+ if normalized_state == "all":
332
+ append_state("open")
333
+ append_state("closed")
334
+ else:
335
+ append_state(normalized_state)
336
+ return {
337
+ "schema_version": "pr_review_source_scan_v0",
338
+ "complete": all(
339
+ item["source_read_valid"] is True
340
+ and item["source_saturated"] is False
341
+ for item in state_scans
342
+ ),
343
+ "pull_requests": detailed,
344
+ "states": state_scans,
345
+ }
346
+
347
+
348
+ def load_pr_fixture(path: Path) -> tuple[str | None, list[dict[str, Any]]]:
349
+ payload = json.loads(path.read_text(encoding="utf-8"))
350
+ if isinstance(payload, list):
351
+ return None, [item for item in payload if isinstance(item, dict)]
352
+ if not isinstance(payload, dict):
353
+ return None, []
354
+ items = payload.get("pull_requests") or payload.get("prs") or []
355
+ return (
356
+ str(payload.get("repository") or "") or None,
357
+ [item for item in _as_list(items) if isinstance(item, dict)],
358
+ )
359
+
360
+
361
+ def _clean_body_lines(body: object) -> list[str]:
362
+ lines: list[str] = []
363
+ for raw in str(body or "").splitlines():
364
+ line = raw.strip()
365
+ if not line:
366
+ continue
367
+ if line.startswith("<!--") or line.startswith("-->"):
368
+ continue
369
+ if line.startswith("#"):
370
+ continue
371
+ if line.startswith("- [") or line.startswith("* ["):
372
+ continue
373
+ if line.lower() in {"summary", "motivation", "changes", "testing"}:
374
+ continue
375
+ lines.append(line.strip("-* "))
376
+ return lines
377
+
378
+
379
+ def _motivation(pr: dict[str, Any]) -> str:
380
+ lines = _clean_body_lines(pr.get("body"))
381
+ if lines:
382
+ return _redact_text(" ".join(lines[:2]), limit=380)
383
+ title = pr.get("title") or f"PR #{pr.get('number')}"
384
+ return _redact_text(f"Review the intent described by the title: {title}", limit=220)
385
+
386
+
387
+ def _commit_headlines(pr: dict[str, Any], *, limit: int = 5) -> list[str]:
388
+ commits: list[str] = []
389
+ for item in _as_list(pr.get("commits")):
390
+ if not isinstance(item, dict):
391
+ continue
392
+ headline = item.get("messageHeadline") or _as_dict(item.get("commit")).get("messageHeadline")
393
+ if headline:
394
+ commits.append(_redact_text(headline, limit=140))
395
+ if len(commits) >= limit:
396
+ break
397
+ return commits
398
+
399
+
400
+ def _file_area(path: str) -> str:
401
+ name = path.rsplit("/", 1)[-1]
402
+ lowered = path.lower()
403
+ if path in {
404
+ "README.md",
405
+ "README.zh-CN.md",
406
+ "AGENTS.md",
407
+ "CONTRIBUTING.md",
408
+ "CHANGELOG.md",
409
+ "LICENSE",
410
+ "CODE_OF_CONDUCT.md",
411
+ ".github/SECURITY.md",
412
+ }:
413
+ return "public_entry_or_policy"
414
+ if path.startswith(".github/workflows/"):
415
+ return "ci_or_release"
416
+ if path.startswith("docs/"):
417
+ return "public_docs"
418
+ if path.startswith(("examples/", "fixtures/", "test/", "tests/", "spec/", "smoke/")):
419
+ return "test_or_example"
420
+ if path.startswith(UI_PREFIXES):
421
+ return "app_or_ui_surface"
422
+ if path.startswith(RUNTIME_OR_CLI_PREFIXES):
423
+ return "product_runtime"
424
+ if lowered.endswith(CODE_EXTENSIONS):
425
+ return "product_runtime"
426
+ if path.endswith((".toml", ".yaml", ".yml", ".json", ".ini")) or name in {
427
+ "package.json",
428
+ "pyproject.toml",
429
+ "Cargo.toml",
430
+ "go.mod",
431
+ "Makefile",
432
+ }:
433
+ return "build_or_config"
434
+ if lowered.endswith((".md", ".mdx", ".rst")):
435
+ return "public_docs"
436
+ return "other"
437
+
438
+
439
+ def _files(pr: dict[str, Any]) -> list[dict[str, Any]]:
440
+ files: list[dict[str, Any]] = []
441
+ for item in _as_list(pr.get("files")):
442
+ if isinstance(item, str):
443
+ path = item
444
+ additions = None
445
+ deletions = None
446
+ elif isinstance(item, dict):
447
+ path = str(item.get("path") or item.get("filename") or "")
448
+ additions = item.get("additions")
449
+ deletions = item.get("deletions")
450
+ else:
451
+ continue
452
+ if not path:
453
+ continue
454
+ files.append(
455
+ {
456
+ "path": _redact_text(path, limit=180),
457
+ "area": _file_area(path),
458
+ "additions": additions,
459
+ "deletions": deletions,
460
+ }
461
+ )
462
+ return files
463
+
464
+
465
+ def _area_counts(files: list[dict[str, Any]]) -> dict[str, int]:
466
+ counts: dict[str, int] = {}
467
+ for item in files:
468
+ area = str(item.get("area") or "other")
469
+ counts[area] = counts.get(area, 0) + 1
470
+ return counts
471
+
472
+
473
+ AREA_LABELS = {
474
+ "public_entry_or_policy": "README/政策入口",
475
+ "public_docs": "公开文档",
476
+ "test_or_example": "smoke/示例",
477
+ "app_or_ui_surface": "前端/展示面",
478
+ "product_runtime": "运行时/CLI",
479
+ "ci_or_release": "CI/发布",
480
+ "build_or_config": "构建配置",
481
+ "other": "其他文件",
482
+ }
483
+
484
+
485
+ RISK_LEVEL_LABELS = {
486
+ "high": "高",
487
+ "medium": "中",
488
+ "low": "低",
489
+ "unknown": "未知",
490
+ }
491
+
492
+
493
+ def _area_phrase(areas: dict[str, Any]) -> str:
494
+ parts: list[str] = []
495
+ for area, count in sorted(areas.items(), key=lambda item: (-int(item[1] or 0), str(item[0]))):
496
+ label = AREA_LABELS.get(str(area), str(area))
497
+ parts.append(f"{label} {count}")
498
+ return _join_short(parts, limit=4, fallback="未知区域")
499
+
500
+
501
+ def _top_file_phrase(files: list[dict[str, Any]], *, limit: int = 3) -> str:
502
+ return _join_short([str(item.get("path") or "") for item in files if item.get("path")], limit=limit, fallback="未返回关键文件")
503
+
504
+
505
+ def _int_or_zero(value: object) -> int:
506
+ try:
507
+ return int(value or 0)
508
+ except (TypeError, ValueError):
509
+ return 0
510
+
511
+
512
+ def _check_brief_phrase(checks: dict[str, Any]) -> str:
513
+ counts = _as_dict(checks.get("counts"))
514
+ if checks.get("failures"):
515
+ return f"{len(_as_list(checks.get('failures')))} fail"
516
+ if checks.get("pending"):
517
+ return f"{len(_as_list(checks.get('pending')))} pending"
518
+ if counts.get("success"):
519
+ return f"{counts.get('success')} pass"
520
+ if int(checks.get("total") or 0) == 0:
521
+ return "无 checks"
522
+ return str(checks.get("summary") or "unknown")
523
+
524
+
525
+ def _metadata_risk_hint(pr: dict[str, Any], files: list[dict[str, Any]], checks: dict[str, Any]) -> dict[str, Any]:
526
+ areas = _area_counts(files)
527
+ changed = int(pr.get("changedFiles") or len(files) or 0)
528
+ additions = int(pr.get("additions") or 0)
529
+ deletions = int(pr.get("deletions") or 0)
530
+ has_runtime = any(
531
+ str(item.get("area")) in {"product_runtime", "app_or_ui_surface", "ci_or_release", "build_or_config"}
532
+ for item in files
533
+ )
534
+ if checks.get("failures") or changed >= 12 or additions + deletions >= 800:
535
+ level = "high"
536
+ elif has_runtime or checks.get("pending") or not checks.get("total"):
537
+ level = "medium"
538
+ else:
539
+ level = "low"
540
+ return {
541
+ "schema_version": "pr_metadata_risk_hint_v0",
542
+ "level": level,
543
+ "basis": [
544
+ f"areas={_area_phrase(areas)}",
545
+ f"scale={changed} files +{additions}/-{deletions}",
546
+ f"checks={_check_brief_phrase(checks)}",
547
+ ],
548
+ "disclaimer": "Metadata-only hint for queue ordering; agentloop must read the PR diff before judging main risk.",
549
+ }
550
+
551
+
552
+ def _main_regression_analysis(pr: dict[str, Any], files: list[dict[str, Any]]) -> dict[str, Any]:
553
+ areas = _area_counts(files)
554
+ checks = _checks(pr)
555
+ state = str(pr.get("state") or "").upper()
556
+ changed = int(pr.get("changedFiles") or len(files) or 0)
557
+ additions = int(pr.get("additions") or 0)
558
+ deletions = int(pr.get("deletions") or 0)
559
+ churn = additions + deletions
560
+ area_names = {str(item.get("area") or "other") for item in files}
561
+
562
+ potential_regressions: list[str] = []
563
+ bug_risks: list[str] = []
564
+ verification_focus: list[str] = []
565
+
566
+ if "product_runtime" in area_names:
567
+ potential_regressions.append(
568
+ "Runtime or CLI behavior can regress: command output, data parsing, scheduling/routing, or public API compatibility may change on main."
569
+ )
570
+ bug_risks.append("A small runtime mismatch can break automation, hide actionable work, or make existing scripts parse stale fields.")
571
+ verification_focus.append("Run the affected CLI/runtime smoke and inspect one representative JSON or markdown packet.")
572
+ if "app_or_ui_surface" in area_names:
573
+ potential_regressions.append(
574
+ "User-facing surfaces can regress: navigation, first-screen hierarchy, responsive layout, or hover/review affordances may change."
575
+ )
576
+ bug_risks.append("Fixture data can pass while the browser view hides important review context or renders controls poorly.")
577
+ verification_focus.append("Run the affected UI/frontstage smoke or capture the route in a browser when presentation changes.")
578
+ if "ci_or_release" in area_names or "build_or_config" in area_names:
579
+ potential_regressions.append(
580
+ "Build, install, or CI behavior can regress: workflow triggers, dependency resolution, or required checks may change for later PRs."
581
+ )
582
+ bug_risks.append("A config-only diff can block future validation even when product code is unchanged.")
583
+ verification_focus.append("Run `git diff --check` and the narrow build/check command that consumes the touched config.")
584
+ if "public_entry_or_policy" in area_names:
585
+ potential_regressions.append(
586
+ "Public entry or policy wording can regress: first-screen promises, maintainer rules, or contributor expectations may drift."
587
+ )
588
+ bug_risks.append("A prominent docs change can authorize behavior that the runtime or repository policy does not actually support.")
589
+ verification_focus.append("Compare the public entry text with current CLI help, AGENTS policy, and any first-screen review gate.")
590
+ if areas and area_names <= {"public_docs", "test_or_example"}:
591
+ potential_regressions.append(
592
+ "Runtime regression risk is low, but public guidance or smoke expectations can drift from shipped behavior."
593
+ )
594
+ bug_risks.append("Docs-only or smoke-only changes can bless stale contracts if examples no longer match the real command path.")
595
+ verification_focus.append("Run `git diff --check` and the touched smoke; compare command examples with current CLI help when syntax is involved.")
596
+ if not potential_regressions:
597
+ potential_regressions.append(
598
+ "Regression shape is unclear from metadata alone; inspect the highest-churn files before approving."
599
+ )
600
+ bug_risks.append("Unclassified files may still affect generated assets, packaging, or reviewer workflow assumptions.")
601
+ verification_focus.append("Review the diff for the top changed files and run the nearest project smoke.")
602
+
603
+ if checks.get("failures"):
604
+ bug_risks.insert(0, "Failing status checks indicate the branch may already break a required validation surface.")
605
+ verification_focus.insert(0, "Inspect failing checks before merge and rerun them after fixes.")
606
+ elif checks.get("pending"):
607
+ bug_risks.append("Pending checks leave merge readiness uncertain.")
608
+ verification_focus.append("Wait for pending checks or run the equivalent local smoke before merge.")
609
+ elif not checks.get("total"):
610
+ bug_risks.append("No status-check rollup was available, so validation coverage must be inferred from local evidence.")
611
+ verification_focus.append("Run at least one focused local validation command before approving.")
612
+
613
+ has_sensitive_area = bool(area_names & {"product_runtime", "app_or_ui_surface", "ci_or_release", "build_or_config"})
614
+ if checks.get("failures") or changed >= 12 or churn >= 800 or (state == "MERGED" and has_sensitive_area):
615
+ level = "high"
616
+ elif has_sensitive_area or checks.get("pending") or not checks.get("total"):
617
+ level = "medium"
618
+ else:
619
+ level = "low"
620
+
621
+ return {
622
+ "schema_version": "main_regression_analysis_v0",
623
+ "risk_level": level,
624
+ "risk_summary": (
625
+ f"{RISK_LEVEL_LABELS.get(level, level)} main regression risk across {_area_phrase(areas)}; "
626
+ f"{changed} file(s), +{additions}/-{deletions}; checks={_check_brief_phrase(checks)}."
627
+ ),
628
+ "potential_regressions": potential_regressions[:5],
629
+ "bug_risks": bug_risks[:5],
630
+ "verification_focus": verification_focus[:5],
631
+ "post_merge_review": state == "MERGED" or bool(pr.get("mergedAt") or pr.get("merged_at")),
632
+ }
633
+
634
+
635
+ def _check_name(item: dict[str, Any]) -> str:
636
+ return str(item.get("name") or item.get("context") or item.get("workflowName") or "check")
637
+
638
+
639
+ def _check_state(item: dict[str, Any]) -> str:
640
+ conclusion = str(item.get("conclusion") or "").upper()
641
+ status = str(item.get("status") or "").upper()
642
+ if conclusion in {"SUCCESS", "PASSED", "NEUTRAL", "SKIPPED"}:
643
+ return "success"
644
+ if conclusion in {"FAILURE", "FAILED", "ERROR", "CANCELLED", "TIMED_OUT", "ACTION_REQUIRED"}:
645
+ return "failure"
646
+ if status in {"COMPLETED"} and conclusion:
647
+ return conclusion.lower()
648
+ if status in {"QUEUED", "IN_PROGRESS", "PENDING", "WAITING"}:
649
+ return "pending"
650
+ return "unknown"
651
+
652
+
653
+ def _checks(pr: dict[str, Any]) -> dict[str, Any]:
654
+ items = [item for item in _as_list(pr.get("statusCheckRollup")) if isinstance(item, dict)]
655
+ counts: dict[str, int] = {}
656
+ failures: list[str] = []
657
+ pending: list[str] = []
658
+ for item in items:
659
+ state = _check_state(item)
660
+ counts[state] = counts.get(state, 0) + 1
661
+ if state == "failure":
662
+ failures.append(_redact_text(_check_name(item), limit=120))
663
+ elif state == "pending":
664
+ pending.append(_redact_text(_check_name(item), limit=120))
665
+ if not items:
666
+ return {
667
+ "total": 0,
668
+ "counts": {},
669
+ "summary": "No status-check rollup was available from the source.",
670
+ "failures": [],
671
+ "pending": [],
672
+ }
673
+ if failures:
674
+ summary = f"{len(failures)} failing check(s)."
675
+ elif pending:
676
+ summary = f"{len(pending)} pending check(s)."
677
+ else:
678
+ summary = f"{counts.get('success', 0)} successful check(s)."
679
+ return {
680
+ "total": len(items),
681
+ "counts": counts,
682
+ "summary": summary,
683
+ "failures": failures[:5],
684
+ "pending": pending[:5],
685
+ }
686
+
687
+
688
+ def _risk_notes(pr: dict[str, Any], files: list[dict[str, Any]]) -> list[str]:
689
+ notes: list[str] = []
690
+ state = str(pr.get("state") or "").upper()
691
+ if state == "MERGED" or pr.get("mergedAt") or pr.get("merged_at"):
692
+ notes.append("Already merged: review for post-merge quality, regression risk, and follow-up work.")
693
+ elif state in {"CLOSED"}:
694
+ notes.append("Closed without a merge signal; review only if it still informs a replacement path.")
695
+ if pr.get("isDraft"):
696
+ notes.append("Draft PR: review may be advisory until it is marked ready.")
697
+ if state != "MERGED" and str(pr.get("mergeStateStatus") or "").upper() not in {"", "CLEAN", "HAS_HOOKS", "UNKNOWN"}:
698
+ notes.append(f"Merge state is {pr.get('mergeStateStatus')}; check conflict or branch-protection details.")
699
+ if any(str(item.get("path") or "").startswith(RUNTIME_OR_CLI_PREFIXES) for item in files):
700
+ notes.append("Touches runtime, app, CLI, or automation paths; review behavior and compatibility before merge.")
701
+ changed = int(pr.get("changedFiles") or len(files) or 0)
702
+ additions = int(pr.get("additions") or 0)
703
+ deletions = int(pr.get("deletions") or 0)
704
+ if changed >= 12 or additions + deletions >= 800:
705
+ notes.append("Large review surface; split the review by area before approving.")
706
+ checks = _checks(pr)
707
+ if checks.get("failures"):
708
+ notes.append("Failing status checks block a clean merge decision.")
709
+ return notes
710
+
711
+
712
+ def _review_depth(files: list[dict[str, Any]]) -> str:
713
+ areas = {str(item.get("area") or "") for item in files}
714
+ if "product_runtime" in areas:
715
+ return "runtime_behavior_review"
716
+ if "app_or_ui_surface" in areas or "public_entry_or_policy" in areas:
717
+ return "presentation_or_policy_review"
718
+ if areas <= {"public_docs", "test_or_example"}:
719
+ return "docs_and_smoke_review"
720
+ return "standard_review"
721
+
722
+
723
+ def _parse_updated_epoch(value: object) -> float:
724
+ parsed = _parse_timestamp(value)
725
+ return parsed.timestamp() if parsed else 0.0
726
+
727
+
728
+ def _review_priority(pr: dict[str, Any], files: list[dict[str, Any]]) -> tuple[int, float]:
729
+ is_draft = bool(pr.get("isDraft") or pr.get("is_draft"))
730
+ review_decision = str(pr.get("reviewDecision") or pr.get("review_decision") or "").upper()
731
+ state = str(pr.get("state") or "").upper()
732
+ activity_at = (
733
+ pr.get("mergedAt")
734
+ or pr.get("merged_at")
735
+ or pr.get("closedAt")
736
+ or pr.get("closed_at")
737
+ or pr.get("updatedAt")
738
+ or pr.get("updated_at")
739
+ )
740
+ if is_draft:
741
+ bucket = 4
742
+ elif state == "MERGED":
743
+ bucket = 3
744
+ elif state == "CLOSED":
745
+ bucket = 5
746
+ elif review_decision in {"", "REVIEW_REQUIRED", "CHANGES_REQUESTED", "UNKNOWN"}:
747
+ bucket = 0
748
+ elif _review_depth(files) == "runtime_behavior_review":
749
+ bucket = 1
750
+ else:
751
+ bucket = 2
752
+ return (bucket, -_parse_updated_epoch(activity_at))
753
+
754
+
755
+ def _review_sequence_entry(item: dict[str, Any], *, rank: int) -> dict[str, Any]:
756
+ main_risk = _as_dict(item.get("main_regression_analysis"))
757
+ return {
758
+ "rank": rank,
759
+ "number": item.get("number"),
760
+ "title": item.get("title"),
761
+ "url": item.get("url"),
762
+ "state": item.get("state"),
763
+ "review_depth": item.get("review_depth"),
764
+ "risk_hint_level": _as_dict(item.get("metadata_risk_hint")).get("level"),
765
+ "main_risk_level": main_risk.get("risk_level"),
766
+ "why_now": _review_why_now(item),
767
+ }
768
+
769
+
770
+ def _normalize_pr(pr: dict[str, Any]) -> dict[str, Any]:
771
+ files = _files(pr)
772
+ checks = _checks(pr)
773
+ number = pr.get("number")
774
+ url = pr.get("url") or (f"https://github.com/pull/{number}" if number else "")
775
+ raw_state = str(pr.get("state") or "").upper()
776
+ if not raw_state:
777
+ raw_state = "MERGED" if pr.get("mergedAt") or pr.get("merged_at") else "OPEN"
778
+ merge_commit = pr.get("mergeCommit") or pr.get("merge_commit")
779
+ merge_commit_oid = _as_dict(merge_commit).get("oid") if isinstance(merge_commit, dict) else None
780
+ item: dict[str, Any] = {
781
+ "number": number,
782
+ "title": _redact_text(pr.get("title"), limit=180),
783
+ "url": _redact_text(url, limit=220),
784
+ "state": raw_state,
785
+ "author": _redact_text(_as_dict(pr.get("author")).get("login") or pr.get("author"), limit=80),
786
+ "updated_at": pr.get("updatedAt"),
787
+ "closed_at": pr.get("closedAt"),
788
+ "merged_at": pr.get("mergedAt"),
789
+ "merge_commit": _redact_text(merge_commit_oid, limit=80) if merge_commit_oid else None,
790
+ "base_ref": _redact_text(pr.get("baseRefName"), limit=80),
791
+ "head_ref": _redact_text(pr.get("headRefName"), limit=120),
792
+ "head_oid": _redact_text(pr.get("headRefOid"), limit=80),
793
+ "is_draft": bool(pr.get("isDraft")),
794
+ "review_decision": str(pr.get("reviewDecision") or "UNKNOWN"),
795
+ "merge_state": str(pr.get("mergeStateStatus") or pr.get("mergeable") or "UNKNOWN"),
796
+ "motivation": _motivation(pr),
797
+ "scale": {
798
+ "changed_files": int(pr.get("changedFiles") or len(files) or 0),
799
+ "additions": int(pr.get("additions") or 0),
800
+ "deletions": int(pr.get("deletions") or 0),
801
+ },
802
+ "areas": _area_counts(files),
803
+ "key_files": files[:10],
804
+ "commit_headlines": _commit_headlines(pr),
805
+ "checks": checks,
806
+ "review_depth": _review_depth(files),
807
+ "risk_notes": _risk_notes(pr, files),
808
+ "metadata_risk_hint": _metadata_risk_hint(pr, files, checks),
809
+ "main_regression_analysis": _main_regression_analysis(pr, files),
810
+ "review_goal": "Fill the five-block review template after reading the PR body and diff.",
811
+ "evidence_commands": [
812
+ f"gh pr view {number} --json title,body,files,commits,statusCheckRollup,headRefOid,updatedAt",
813
+ f"gh pr diff {number} --name-only",
814
+ f"gh pr diff {number} --patch",
815
+ f"gh pr view {number} --json headRefOid,updatedAt",
816
+ ]
817
+ if number
818
+ else [],
819
+ }
820
+ item["review_plan"] = build_review_plan(item)
821
+ item["review_template"] = build_review_template(item)
822
+ return item
823
+
824
+
825
+ def build_pr_review_packet(
826
+ *,
827
+ pull_requests: list[dict[str, Any]],
828
+ repository: str | None,
829
+ limit: int,
830
+ source: str,
831
+ state_filter: str = "all",
832
+ since: str | None = None,
833
+ source_scan: Mapping[str, Any] | None = None,
834
+ ) -> dict[str, Any]:
835
+ normalized_state_filter = normalize_pr_state_filter(state_filter)
836
+ normalized_all = [_normalize_pr(item) for item in pull_requests]
837
+ normalized_all = [
838
+ item
839
+ for item in normalized_all
840
+ if (normalized_state_filter == "all" or str(item.get("state") or "").lower() == normalized_state_filter)
841
+ and _include_pr_in_window(item, since=since)
842
+ ]
843
+ normalized_all.sort(key=lambda item: _review_priority(item, item.get("key_files") or []))
844
+ packet_limit = max(1, limit)
845
+ unmerged_all = [item for item in normalized_all if str(item.get("state") or "").upper() != "MERGED"]
846
+ merged_all = [item for item in normalized_all if str(item.get("state") or "").upper() == "MERGED"]
847
+ if normalized_state_filter == "all":
848
+ unmerged_items = unmerged_all[:packet_limit]
849
+ merged_items = merged_all[:packet_limit]
850
+ normalized = unmerged_items + merged_items
851
+ else:
852
+ normalized = normalized_all[:packet_limit]
853
+ unmerged_items = [item for item in normalized if str(item.get("state") or "").upper() != "MERGED"]
854
+ merged_items = [item for item in normalized if str(item.get("state") or "").upper() == "MERGED"]
855
+ source_scan_complete = (
856
+ source_scan.get("complete") is True
857
+ if isinstance(source_scan, Mapping)
858
+ else True
859
+ )
860
+ group_observed_counts = {
861
+ "unmerged": len(unmerged_all),
862
+ "merged": len(merged_all),
863
+ }
864
+ group_included_counts = {
865
+ "unmerged": len(unmerged_items),
866
+ "merged": len(merged_items),
867
+ }
868
+ group_completeness = {
869
+ key: source_scan_complete
870
+ and group_included_counts[key] == group_observed_counts[key]
871
+ for key in group_observed_counts
872
+ }
873
+ complete = all(group_completeness.values())
874
+ recommended_limit = None
875
+ if not complete:
876
+ recommended_limit = max(
877
+ packet_limit * 2,
878
+ max(group_observed_counts.values(), default=0) + 1,
879
+ )
880
+ source_scan_summary = None
881
+ if isinstance(source_scan, Mapping):
882
+ source_scan_summary = {
883
+ key: value
884
+ for key, value in source_scan.items()
885
+ if key != "pull_requests"
886
+ }
887
+ result_completeness = {
888
+ "schema_version": "pr_review_result_completeness_v0",
889
+ "complete": complete,
890
+ "truncated": not complete,
891
+ "limit": packet_limit,
892
+ "limit_scope": "per_group" if normalized_state_filter == "all" else "filtered_queue",
893
+ "source_scan_complete": source_scan_complete,
894
+ "observed_count_is_lower_bound": not source_scan_complete,
895
+ "observed_pr_count": len(normalized_all),
896
+ "included_pr_count": len(normalized),
897
+ "groups": {
898
+ key: {
899
+ "complete": group_completeness[key],
900
+ "observed_count": group_observed_counts[key],
901
+ "included_count": group_included_counts[key],
902
+ "truncated": not group_completeness[key],
903
+ }
904
+ for key in ("unmerged", "merged")
905
+ },
906
+ "recommended_limit": recommended_limit,
907
+ "rerun_cli_args": (
908
+ ["--limit", str(recommended_limit)] if recommended_limit else []
909
+ ),
910
+ "source_scan": source_scan_summary,
911
+ }
912
+ review_sequence = [_review_sequence_entry(item, rank=index) for index, item in enumerate(normalized, start=1)]
913
+ open_review_required = [
914
+ item
915
+ for item in normalized
916
+ if str(item.get("state") or "").upper() == "OPEN"
917
+ and not item.get("is_draft")
918
+ and str(item.get("review_decision") or "").upper() in {"", "REVIEW_REQUIRED", "CHANGES_REQUESTED", "UNKNOWN"}
919
+ ]
920
+ closed_items = [item for item in normalized if str(item.get("state") or "").upper() == "CLOSED"]
921
+ first = review_sequence[0] if review_sequence else None
922
+ review_attention_count = len(open_review_required) + len(merged_items)
923
+ open_items = [item for item in normalized if str(item.get("state") or "").upper() == "OPEN"]
924
+ review_groups = {
925
+ "unmerged": {
926
+ "schema_version": "pr_review_group_v0",
927
+ "group_id": "unmerged",
928
+ "title": "Unmerged PRs",
929
+ "intent": "Review before merge: decide approve, request changes, defer, or wait for checks.",
930
+ "count": len(unmerged_items),
931
+ "complete": group_completeness["unmerged"],
932
+ "observed_count": group_observed_counts["unmerged"],
933
+ "truncated": not group_completeness["unmerged"],
934
+ "pr_numbers": [item.get("number") for item in unmerged_items],
935
+ "review_sequence": [
936
+ _review_sequence_entry(item, rank=index)
937
+ for index, item in enumerate(unmerged_items, start=1)
938
+ ],
939
+ },
940
+ "merged": {
941
+ "schema_version": "pr_review_group_v0",
942
+ "group_id": "merged",
943
+ "title": "Merged PRs",
944
+ "intent": "Post-merge audit: check outcome, regression risk, and follow-up quality without blocking already-merged work.",
945
+ "count": len(merged_items),
946
+ "complete": group_completeness["merged"],
947
+ "observed_count": group_observed_counts["merged"],
948
+ "truncated": not group_completeness["merged"],
949
+ "pr_numbers": [item.get("number") for item in merged_items],
950
+ "review_sequence": [
951
+ _review_sequence_entry(item, rank=index)
952
+ for index, item in enumerate(merged_items, start=1)
953
+ ],
954
+ },
955
+ }
956
+ headline = (
957
+ (
958
+ f"{len(normalized)} PR(s) in review window: "
959
+ f"{len(open_items)} open, "
960
+ f"{len(merged_items)} merged; {review_attention_count} need review attention."
961
+ )
962
+ if normalized
963
+ else "No pull requests found for the requested review window."
964
+ )
965
+ return {
966
+ "ok": True,
967
+ "schema_version": SCHEMA_VERSION,
968
+ "request": {
969
+ "schema_version": "loopx_pr_review_command_request_v0",
970
+ "command": COMMAND,
971
+ "cli_command": "loopx pr-review [--repo owner/repo] [--state open|merged|all] [--since ISO]",
972
+ "repository": repository,
973
+ "limit": max(1, limit),
974
+ "state_filter": normalized_state_filter,
975
+ "since": since,
976
+ "window": {
977
+ "since": since,
978
+ "state_filter": normalized_state_filter,
979
+ },
980
+ "source": source,
981
+ "include": [
982
+ "pull_request_list",
983
+ "result_completeness",
984
+ "review_groups",
985
+ "review_plan",
986
+ "review_template",
987
+ "agent_response_contract",
988
+ "change_scope",
989
+ "checks",
990
+ "metadata_risk_hint",
991
+ "main_regression_analysis",
992
+ "risk_notes",
993
+ "review_sequence",
994
+ ],
995
+ "privacy_mode": "public_safe_github_metadata",
996
+ "dry_run": True,
997
+ },
998
+ "generated_at": _now_iso(),
999
+ "summary": {
1000
+ "headline": headline,
1001
+ "total_pr_count": len(normalized),
1002
+ "open_pr_count": len(open_items),
1003
+ "merged_pr_count": len(merged_items),
1004
+ "closed_pr_count": len(closed_items),
1005
+ "review_attention_count": review_attention_count,
1006
+ "open_review_attention_count": len(open_review_required),
1007
+ "post_merge_review_count": len(merged_items),
1008
+ "draft_count": sum(1 for item in normalized if item.get("is_draft")),
1009
+ "source_surfaces": SOURCE_SURFACES,
1010
+ "recommended_first_pr": first,
1011
+ },
1012
+ "result_completeness": result_completeness,
1013
+ "review_sequence": review_sequence,
1014
+ "review_groups": review_groups,
1015
+ "pull_requests": normalized,
1016
+ "agent_response_contract": build_agent_response_contract(),
1017
+ "actions": [
1018
+ {
1019
+ "action_id": "act_review_next_pr",
1020
+ "kind": "review",
1021
+ "requires_user_approval": False,
1022
+ "requires_maintainer_authority": False,
1023
+ "preview": "Start with the first PR in review_sequence, read its motivation, inspect key files, then decide approve/request changes/defer.",
1024
+ },
1025
+ {
1026
+ "action_id": "act_merge_after_review",
1027
+ "kind": "merge_or_publish",
1028
+ "requires_user_approval": False,
1029
+ "requires_maintainer_authority": True,
1030
+ "preview": "Merge only after repository policy, validation, and public/private boundary checks pass.",
1031
+ },
1032
+ ],
1033
+ "omissions": [
1034
+ "Raw logs, private connector payloads, credentials, local paths, and private source bodies were intentionally omitted.",
1035
+ "The command summarizes public PR metadata and does not post review comments, approve, merge, or spend quota.",
1036
+ ],
1037
+ "boundary": BOUNDARY,
1038
+ }
1039
+
1040
+
1041
+ def _review_why_now(item: dict[str, Any]) -> str:
1042
+ state = str(item.get("state") or "").upper()
1043
+ if state == "MERGED":
1044
+ return "Merged in the review window; audit outcome, validation, and follow-up quality."
1045
+ if state == "CLOSED":
1046
+ return "Closed without a merge signal; check whether a replacement or cleanup is needed."
1047
+ if item.get("is_draft"):
1048
+ return "Draft PR; skim for early direction but do not treat as merge-ready."
1049
+ decision = str(item.get("review_decision") or "").upper()
1050
+ if decision in {"REVIEW_REQUIRED", "UNKNOWN", ""}:
1051
+ return "Open and awaiting reviewer decision."
1052
+ if decision == "CHANGES_REQUESTED":
1053
+ return "Changes were requested; verify whether the latest diff addresses them."
1054
+ if decision == "APPROVED":
1055
+ return "Approved but still open; check merge state and final validation."
1056
+ return "Open PR; confirm current merge readiness."
1057
+
1058
+
1059
+ def render_pr_review_markdown(payload: dict[str, Any]) -> str:
1060
+ if not payload.get("ok"):
1061
+ return "# Project PR Review Queue\n\n- ok: `False`\n- error: " + _redact_text(payload.get("error"))
1062
+
1063
+ summary = _as_dict(payload.get("summary"))
1064
+ request = _as_dict(payload.get("request"))
1065
+ completeness = _as_dict(payload.get("result_completeness"))
1066
+ lines = [
1067
+ "# Project PR Review Queue",
1068
+ "",
1069
+ f"- command: `{request.get('command')}`",
1070
+ f"- repository: `{request.get('repository') or 'current gh repository'}`",
1071
+ f"- state_filter: `{request.get('state_filter') or 'all'}`",
1072
+ f"- since: `{request.get('since') or 'not set'}`",
1073
+ f"- headline: {summary.get('headline')}",
1074
+ f"- complete: `{completeness.get('complete')}`; truncated=`{completeness.get('truncated')}`; recommended_limit=`{completeness.get('recommended_limit')}`",
1075
+ f"- counts: total=`{summary.get('total_pr_count')}`, open=`{summary.get('open_pr_count')}`, merged=`{summary.get('merged_pr_count')}`, review_attention=`{summary.get('review_attention_count')}`, draft=`{summary.get('draft_count')}`",
1076
+ "- tool contract: run `loopx pr-review` first and use its `review_groups`; use ad hoc `gh` commands only after selecting a PR from this packet.",
1077
+ "- final answer contract: queue/table is only a preface; `/loopx-pr-review` must return filled five-block review cards after reading PR evidence.",
1078
+ "- intent contract: the `/loopx-pr-review` prefix dominates; words like open/closed/merged/today are filters unless the user explicitly asks for stats/list-only.",
1079
+ "",
1080
+ "## Agent Output Contract",
1081
+ "",
1082
+ "- Do not stop at the queue/table summary.",
1083
+ "- Do not collapse this packet to `.summary` and `.review_sequence` only; preserve the paths named by `agent_response_contract.required_packet_fields_to_preserve`.",
1084
+ "- For each selected PR, read PR body/files/diff/checks first, then return one review card.",
1085
+ "- Execute each `pull_requests[].review_plan` against `agent_response_contract.review_execution_contract`; that capability-owned contract is the evidence and completeness authority.",
1086
+ "- Code-changing PRs must include a `关键代码讲解` subsection under `具体改动`, grounded in exact-head symbols and short excerpts or equivalent pseudocode.",
1087
+ "- Bind the verdict to the remote head SHA and recheck it before answering.",
1088
+ "- Required card headings: `动机`, `改动思路`, `具体改动`, `对主干的风险`, `我的整体评价`.",
1089
+ "- `metadata_risk_hint` is only queue-ordering metadata; do not copy it as the final risk judgment.",
1090
+ "",
1091
+ "## Unmerged PRs",
1092
+ ]
1093
+ review_groups = _as_dict(payload.get("review_groups"))
1094
+ unmerged = _as_dict(review_groups.get("unmerged"))
1095
+ merged = _as_dict(review_groups.get("merged"))
1096
+
1097
+ def append_group(group: dict[str, Any]) -> None:
1098
+ sequence = [item for item in _as_list(group.get("review_sequence")) if isinstance(item, dict)]
1099
+ if not sequence:
1100
+ lines.append("- none")
1101
+ return
1102
+ for item in sequence:
1103
+ lines.append(
1104
+ f"{item.get('rank')}. [#{item.get('number')} {item.get('title')}]({item.get('url')}) "
1105
+ f"[{item.get('state')}] - "
1106
+ f"{item.get('review_depth')} / main_risk=`{item.get('main_risk_level')}` "
1107
+ f"/ risk_hint=`{item.get('risk_hint_level')}`: {item.get('why_now')}"
1108
+ )
1109
+
1110
+ append_group(unmerged)
1111
+ lines.extend(["", "## Merged PRs"])
1112
+ append_group(merged)
1113
+ lines.extend(["", "## Combined Review Sequence"])
1114
+ sequence = [item for item in _as_list(payload.get("review_sequence")) if isinstance(item, dict)]
1115
+ if not sequence:
1116
+ lines.append("- none")
1117
+ for item in sequence:
1118
+ lines.append(
1119
+ f"{item.get('rank')}. [#{item.get('number')} {item.get('title')}]({item.get('url')}) "
1120
+ f"[{item.get('state')}] - "
1121
+ f"{item.get('review_depth')} / main_risk=`{item.get('main_risk_level')}` "
1122
+ f"/ risk_hint=`{item.get('risk_hint_level')}`: {item.get('why_now')}"
1123
+ )
1124
+
1125
+ for pr in [item for item in _as_list(payload.get("pull_requests")) if isinstance(item, dict)]:
1126
+ template = _as_dict(pr.get("review_template"))
1127
+ review_plan = _as_dict(pr.get("review_plan"))
1128
+ lines.extend(
1129
+ [
1130
+ "",
1131
+ f"## PR #{pr.get('number')}: {pr.get('title')}",
1132
+ "",
1133
+ "> Agentloop should fill the five-block review after reading the PR body and diff. The template below is intentionally blank.",
1134
+ "",
1135
+ f"- url: {pr.get('url')}",
1136
+ f"- state: `{pr.get('state')}`",
1137
+ f"- merged_at: `{pr.get('merged_at') or 'n/a'}`",
1138
+ f"- branch: `{pr.get('head_ref')}` -> `{pr.get('base_ref')}`",
1139
+ f"- status: review=`{pr.get('review_decision')}`, merge=`{pr.get('merge_state')}`, draft=`{pr.get('is_draft')}`",
1140
+ f"- scale: files=`{_as_dict(pr.get('scale')).get('changed_files')}`, +`{_as_dict(pr.get('scale')).get('additions')}`, -`{_as_dict(pr.get('scale')).get('deletions')}`",
1141
+ f"- areas: `{json.dumps(pr.get('areas') or {}, ensure_ascii=False)}`",
1142
+ f"- checks: {_as_dict(pr.get('checks')).get('summary')}",
1143
+ ]
1144
+ )
1145
+ applicability = _as_dict(review_plan.get("applicability"))
1146
+ if review_plan:
1147
+ lines.append(
1148
+ "- review plan: "
1149
+ f"exact_head=`{_as_dict(review_plan.get('target')).get('exact_head_key') or 'unresolved'}`; "
1150
+ f"code_change=`{applicability.get('code_change')}`; "
1151
+ f"symbol_map_required=`{applicability.get('symbol_map_required')}`; "
1152
+ f"negative_walkthrough_required=`{applicability.get('negative_walkthrough_required')}`"
1153
+ )
1154
+ risk_hint = _as_dict(pr.get("metadata_risk_hint"))
1155
+ if risk_hint:
1156
+ lines.append(
1157
+ f"- metadata risk hint: `{risk_hint.get('level')}` "
1158
+ f"({'; '.join(str(item) for item in _as_list(risk_hint.get('basis')))})"
1159
+ )
1160
+ lines.append(f"- risk hint disclaimer: {risk_hint.get('disclaimer')}")
1161
+ main_risk = _as_dict(pr.get("main_regression_analysis"))
1162
+ if main_risk:
1163
+ lines.append(
1164
+ f"- main regression analysis: `{main_risk.get('risk_level')}` - "
1165
+ f"{main_risk.get('risk_summary')}"
1166
+ )
1167
+ for label, key in (
1168
+ ("potential regressions", "potential_regressions"),
1169
+ ("bug risks", "bug_risks"),
1170
+ ("verification focus", "verification_focus"),
1171
+ ):
1172
+ values = [str(item) for item in _as_list(main_risk.get(key)) if item]
1173
+ if values:
1174
+ lines.append(f" - {label}: " + "; ".join(values[:3]))
1175
+ commits = [item for item in _as_list(pr.get("commit_headlines")) if item]
1176
+ if commits:
1177
+ lines.append("- commits: " + "; ".join(f"`{item}`" for item in commits))
1178
+ key_files = [item for item in _as_list(pr.get("key_files")) if isinstance(item, dict)]
1179
+ if key_files:
1180
+ lines.append("- key files:")
1181
+ for item in key_files[:8]:
1182
+ lines.append(
1183
+ f" - `{item.get('path')}` ({item.get('area')}, "
1184
+ f"+{_int_or_zero(item.get('additions'))}/-{_int_or_zero(item.get('deletions'))})"
1185
+ )
1186
+ commands = [item for item in _as_list(pr.get("evidence_commands")) if item]
1187
+ if commands:
1188
+ lines.append("- suggested read commands:")
1189
+ for item in commands[:4]:
1190
+ lines.append(f" - `{item}`")
1191
+ review_order = [str(item) for item in _as_list(template.get("review_order")) if item]
1192
+ if review_order:
1193
+ lines.append("- 推荐阅读顺序: " + " -> ".join(f"`{item}`" for item in review_order))
1194
+ risks = [item for item in _as_list(pr.get("risk_notes")) if item]
1195
+ lines.append("- risk notes: " + ("; ".join(str(item) for item in risks) if risks else "none"))
1196
+ sections = [item for item in _as_list(template.get("sections")) if isinstance(item, dict)]
1197
+ if sections:
1198
+ lines.append("- 五块模板(留空给 agentloop 填写):")
1199
+ for item in sections:
1200
+ lines.append(
1201
+ f" - {item.get('label')}({item.get('word_hint')}):"
1202
+ f" {item.get('agent_instruction')}"
1203
+ )
1204
+
1205
+ lines.extend(["", "## Boundary", "- Public PR metadata only; raw/private material is omitted."])
1206
+ return "\n".join(lines)