loopx 0.4.8__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (811) hide show
  1. loopx/__init__.py +5 -0
  2. loopx/agent_onboarding.py +654 -0
  3. loopx/agent_registry.py +112 -0
  4. loopx/ark_managed_agent_host.py +59 -0
  5. loopx/authority.py +805 -0
  6. loopx/benchmark.py +2875 -0
  7. loopx/benchmark_adapters/__init__.py +1 -0
  8. loopx/benchmark_adapters/agentissue.py +2644 -0
  9. loopx/benchmark_adapters/agents_last_exam.py +3998 -0
  10. loopx/benchmark_adapters/edgebench.py +322 -0
  11. loopx/benchmark_adapters/skillsbench.py +5978 -0
  12. loopx/benchmark_adapters/skillsbench_acp_failure_policy.py +143 -0
  13. loopx/benchmark_adapters/skillsbench_acp_process.py +31 -0
  14. loopx/benchmark_adapters/skillsbench_acp_relay.py +4832 -0
  15. loopx/benchmark_adapters/skillsbench_batch.py +124 -0
  16. loopx/benchmark_adapters/skillsbench_bridge_guard.py +209 -0
  17. loopx/benchmark_adapters/skillsbench_bridge_summary.py +203 -0
  18. loopx/benchmark_adapters/skillsbench_codex_goal_recovery.py +271 -0
  19. loopx/benchmark_adapters/skillsbench_codex_goal_trace.py +81 -0
  20. loopx/benchmark_adapters/skillsbench_codex_runtime.py +339 -0
  21. loopx/benchmark_adapters/skillsbench_dockerfile_runtime.py +467 -0
  22. loopx/benchmark_adapters/skillsbench_failure_signals.py +652 -0
  23. loopx/benchmark_adapters/skillsbench_proxy_runtime.py +327 -0
  24. loopx/benchmark_adapters/skillsbench_remote_bridge.py +402 -0
  25. loopx/benchmark_adapters/skillsbench_result_discovery.py +143 -0
  26. loopx/benchmark_adapters/skillsbench_runner_profile.py +436 -0
  27. loopx/benchmark_adapters/skillsbench_runner_source.py +99 -0
  28. loopx/benchmark_adapters/skillsbench_setup_preflight.py +771 -0
  29. loopx/benchmark_adapters/skillsbench_signals.py +15 -0
  30. loopx/benchmark_adapters/skillsbench_task_source.py +141 -0
  31. loopx/benchmark_adapters/skillsbench_turn_route.py +723 -0
  32. loopx/benchmark_adapters/skillsbench_turn_runtime.py +1069 -0
  33. loopx/benchmark_adapters/skillsbench_typed_repair.py +689 -0
  34. loopx/benchmark_adapters/skillsbench_uv_cache.py +111 -0
  35. loopx/benchmark_adapters/skillsbench_verifier_bootstrap.py +227 -0
  36. loopx/benchmark_adapters/skillsbench_verifier_cache.py +138 -0
  37. loopx/benchmark_adapters/terminal_bench.py +10078 -0
  38. loopx/benchmark_case_analysis.py +1276 -0
  39. loopx/benchmark_case_state.py +1079 -0
  40. loopx/benchmark_core/__init__.py +239 -0
  41. loopx/benchmark_core/adapter.py +84 -0
  42. loopx/benchmark_core/artifacts.py +517 -0
  43. loopx/benchmark_core/attempts.py +199 -0
  44. loopx/benchmark_core/container_exec.py +216 -0
  45. loopx/benchmark_core/io.py +68 -0
  46. loopx/benchmark_core/lifecycle.py +211 -0
  47. loopx/benchmark_core/loop_protocol.py +689 -0
  48. loopx/benchmark_core/observable_handles.py +348 -0
  49. loopx/benchmark_core/parity.py +256 -0
  50. loopx/benchmark_core/remote_closeout.py +482 -0
  51. loopx/benchmark_core/rounds.py +215 -0
  52. loopx/benchmark_core/route_profile.py +509 -0
  53. loopx/benchmark_core/run_permissions.py +206 -0
  54. loopx/benchmark_core/split_control.py +925 -0
  55. loopx/benchmark_core/turn_fidelity.py +326 -0
  56. loopx/benchmark_ledger.py +3793 -0
  57. loopx/benchmark_ledger_countability.py +372 -0
  58. loopx/benchmark_ledger_current.py +724 -0
  59. loopx/benchmark_trajectory.py +405 -0
  60. loopx/benchmarks/__init__.py +1 -0
  61. loopx/benchmarks/qualification/__init__.py +1 -0
  62. loopx/benchmarks/qualification/release_outcome_baseline.py +360 -0
  63. loopx/benchmarks/read_models/__init__.py +1 -0
  64. loopx/benchmarks/read_models/benchmark_attempt_accounting.py +53 -0
  65. loopx/benchmarks/read_models/benchmark_comparison.py +414 -0
  66. loopx/benchmarks/read_models/benchmark_event_timeline.py +113 -0
  67. loopx/benchmarks/read_models/benchmark_experiment_report.py +475 -0
  68. loopx/benchmarks/read_models/benchmark_learning_ledger.py +137 -0
  69. loopx/benchmarks/read_models/benchmark_lifecycle_contracts.py +228 -0
  70. loopx/benchmarks/read_models/benchmark_projection.py +723 -0
  71. loopx/benchmarks/read_models/benchmark_result.py +146 -0
  72. loopx/benchmarks/read_models/benchmark_run_execution_contract.py +116 -0
  73. loopx/benchmarks/read_models/benchmark_run_failure.py +157 -0
  74. loopx/benchmarks/read_models/benchmark_run_metrics.py +213 -0
  75. loopx/benchmarks/read_models/benchmark_run_post_execution.py +635 -0
  76. loopx/benchmarks/read_models/benchmark_run_pre_execution.py +541 -0
  77. loopx/benchmarks/read_models/benchmark_status_compaction.py +1255 -0
  78. loopx/benchmarks/read_models/benchmark_status_runner.py +780 -0
  79. loopx/benchmarks/read_models/goal_start_control_score.py +857 -0
  80. loopx/benchmarks/read_models/skillsbench_post_run_debug.py +746 -0
  81. loopx/benchmarks/read_models/skillsbench_verifier_attribution.py +269 -0
  82. loopx/bootstrap.py +1116 -0
  83. loopx/bootstrap_command_pack.py +2167 -0
  84. loopx/boundary_authority.py +199 -0
  85. loopx/canary/__init__.py +1 -0
  86. loopx/canary/maintainability_ratchet.py +800 -0
  87. loopx/canary/planner.py +1984 -0
  88. loopx/canary/premerge.py +1130 -0
  89. loopx/canary/qualification_profiles.py +309 -0
  90. loopx/canary/quality_surface_catalog.py +838 -0
  91. loopx/canary/release_profiles.py +51 -0
  92. loopx/canary/runner.py +1107 -0
  93. loopx/canary/smoke_health.py +581 -0
  94. loopx/canary/smoke_profiles.py +212 -0
  95. loopx/capabilities/__init__.py +0 -0
  96. loopx/capabilities/agent_turn_recall/__init__.py +17 -0
  97. loopx/capabilities/agent_turn_recall/cli.py +369 -0
  98. loopx/capabilities/agent_turn_recall/core.py +296 -0
  99. loopx/capabilities/auto_research/__init__.py +16 -0
  100. loopx/capabilities/auto_research/bootstrap_contract.py +157 -0
  101. loopx/capabilities/auto_research/cli.py +1468 -0
  102. loopx/capabilities/auto_research/core.py +11 -0
  103. loopx/capabilities/auto_research/defaults.py +79 -0
  104. loopx/capabilities/auto_research/demo_e2e.py +1848 -0
  105. loopx/capabilities/auto_research/demo_supervisor.py +186 -0
  106. loopx/capabilities/auto_research/evidence_packet.py +767 -0
  107. loopx/capabilities/auto_research/human_view.py +794 -0
  108. loopx/capabilities/auto_research/kernel.py +191 -0
  109. loopx/capabilities/auto_research/knn_demo_workspace.py +322 -0
  110. loopx/capabilities/auto_research/live_evidence.py +248 -0
  111. loopx/capabilities/auto_research/preset.py +176 -0
  112. loopx/capabilities/auto_research/research_state.py +1085 -0
  113. loopx/capabilities/auto_research/role_profiles.py +394 -0
  114. loopx/capabilities/auto_research/rollout_append.py +97 -0
  115. loopx/capabilities/auto_research/terminal_result_contract.py +422 -0
  116. loopx/capabilities/auto_research/terminal_result_projection.py +171 -0
  117. loopx/capabilities/auto_research/terminal_result_query.py +233 -0
  118. loopx/capabilities/auto_research/terminal_results.py +349 -0
  119. loopx/capabilities/auto_research/user_contract.py +190 -0
  120. loopx/capabilities/auto_research/worker_loop.py +163 -0
  121. loopx/capabilities/auto_research/worker_runtime.py +777 -0
  122. loopx/capabilities/auto_research/worker_skill/SKILL.md +343 -0
  123. loopx/capabilities/benchmark_toolkit/__init__.py +19 -0
  124. loopx/capabilities/benchmark_toolkit/integrity.py +387 -0
  125. loopx/capabilities/catalog.py +1875 -0
  126. loopx/capabilities/change_quality/__init__.py +19 -0
  127. loopx/capabilities/change_quality/cli.py +171 -0
  128. loopx/capabilities/change_quality/context.py +156 -0
  129. loopx/capabilities/change_quality/oracles.py +269 -0
  130. loopx/capabilities/change_quality/policy.py +34 -0
  131. loopx/capabilities/change_quality/receipt.py +482 -0
  132. loopx/capabilities/change_quality/result.py +493 -0
  133. loopx/capabilities/change_quality/scope.py +171 -0
  134. loopx/capabilities/change_quality/shadow.py +680 -0
  135. loopx/capabilities/content_ops/__init__.py +0 -0
  136. loopx/capabilities/content_ops/cli.py +649 -0
  137. loopx/capabilities/content_ops/connector_packets.py +164 -0
  138. loopx/capabilities/content_ops/item_lifecycle.py +1000 -0
  139. loopx/capabilities/content_ops/layout.py +451 -0
  140. loopx/capabilities/content_ops/markdown.py +456 -0
  141. loopx/capabilities/content_ops/schemas.py +51 -0
  142. loopx/capabilities/content_ops/social_browser_x.py +107 -0
  143. loopx/capabilities/content_ops/surface.py +1956 -0
  144. loopx/capabilities/content_ops/templates/layout-catalog-v0.json +72 -0
  145. loopx/capabilities/context_providers/__init__.py +36 -0
  146. loopx/capabilities/context_providers/base.py +189 -0
  147. loopx/capabilities/context_providers/factory.py +32 -0
  148. loopx/capabilities/context_providers/openviking.py +702 -0
  149. loopx/capabilities/context_providers/service_ownership.py +185 -0
  150. loopx/capabilities/decision_context/__init__.py +129 -0
  151. loopx/capabilities/decision_context/architecture.py +83 -0
  152. loopx/capabilities/decision_context/assembler.py +849 -0
  153. loopx/capabilities/decision_context/catalog_entry.py +195 -0
  154. loopx/capabilities/decision_context/cli.py +310 -0
  155. loopx/capabilities/decision_context/cursor_commit.py +535 -0
  156. loopx/capabilities/decision_context/outcome_feedback.py +352 -0
  157. loopx/capabilities/decision_context/packets.py +654 -0
  158. loopx/capabilities/decision_context/private_state.py +189 -0
  159. loopx/capabilities/decision_context/profile.py +453 -0
  160. loopx/capabilities/decision_context/providers.py +228 -0
  161. loopx/capabilities/decision_context/review_settlement.py +136 -0
  162. loopx/capabilities/decision_context/runtime.py +273 -0
  163. loopx/capabilities/decision_context/sources.py +415 -0
  164. loopx/capabilities/explore/__init__.py +1 -0
  165. loopx/capabilities/explore/activation.py +198 -0
  166. loopx/capabilities/explore/adaptive_replay_planner.py +221 -0
  167. loopx/capabilities/explore/child_replay_runtime.py +463 -0
  168. loopx/capabilities/explore/composition_frontier.py +291 -0
  169. loopx/capabilities/explore/counterfactual_runtime.py +578 -0
  170. loopx/capabilities/explore/episode_runtime.py +647 -0
  171. loopx/capabilities/explore/harness_checkpoint.py +171 -0
  172. loopx/capabilities/explore/harness_gate.py +115 -0
  173. loopx/capabilities/explore/harness_runtime.py +1124 -0
  174. loopx/capabilities/explore/replay_metrics.py +206 -0
  175. loopx/capabilities/explore/replay_runtime.py +1271 -0
  176. loopx/capabilities/explore/resource_portfolio.py +173 -0
  177. loopx/capabilities/explore/result_log.py +974 -0
  178. loopx/capabilities/explore/router_state.py +432 -0
  179. loopx/capabilities/explore/source_history_reconcile.py +255 -0
  180. loopx/capabilities/explore/speculative_scheduler.py +498 -0
  181. loopx/capabilities/explore/todo_branch_plan.py +650 -0
  182. loopx/capabilities/explore/todo_evidence.py +141 -0
  183. loopx/capabilities/explore/trace_runtime.py +284 -0
  184. loopx/capabilities/explore/worker_branch_plan.py +1257 -0
  185. loopx/capabilities/integration_branch/__init__.py +13 -0
  186. loopx/capabilities/integration_branch/cli.py +148 -0
  187. loopx/capabilities/integration_branch/core.py +916 -0
  188. loopx/capabilities/issue_fix/__init__.py +19 -0
  189. loopx/capabilities/issue_fix/acceptance_loop.py +1050 -0
  190. loopx/capabilities/issue_fix/candidate_evidence.py +503 -0
  191. loopx/capabilities/issue_fix/candidate_preflight.py +676 -0
  192. loopx/capabilities/issue_fix/cli.py +1822 -0
  193. loopx/capabilities/issue_fix/cli_input.py +87 -0
  194. loopx/capabilities/issue_fix/content_ops_cli.py +148 -0
  195. loopx/capabilities/issue_fix/discovered_issue_promotion.py +947 -0
  196. loopx/capabilities/issue_fix/explore_projection.py +710 -0
  197. loopx/capabilities/issue_fix/feasibility.py +542 -0
  198. loopx/capabilities/issue_fix/github_public.py +661 -0
  199. loopx/capabilities/issue_fix/intake_surface.py +832 -0
  200. loopx/capabilities/issue_fix/metadata_preview.py +218 -0
  201. loopx/capabilities/issue_fix/metrics_projection.py +1340 -0
  202. loopx/capabilities/issue_fix/metrics_supplement.py +634 -0
  203. loopx/capabilities/issue_fix/metrics_supplement_cli.py +127 -0
  204. loopx/capabilities/issue_fix/outcome_projection.py +1235 -0
  205. loopx/capabilities/issue_fix/periodic_report.py +189 -0
  206. loopx/capabilities/issue_fix/pr_description.py +418 -0
  207. loopx/capabilities/issue_fix/pr_gate_reconcile.py +496 -0
  208. loopx/capabilities/issue_fix/pr_gate_reconcile_cli.py +464 -0
  209. loopx/capabilities/issue_fix/pr_lifecycle.py +1327 -0
  210. loopx/capabilities/issue_fix/pr_lifecycle_rollout.py +85 -0
  211. loopx/capabilities/issue_fix/pr_monitor_materialization.py +257 -0
  212. loopx/capabilities/issue_fix/pr_review_ack.py +439 -0
  213. loopx/capabilities/issue_fix/provider_hooks.py +24 -0
  214. loopx/capabilities/issue_fix/repository_commit_evidence.py +186 -0
  215. loopx/capabilities/issue_fix/repository_context.py +457 -0
  216. loopx/capabilities/issue_fix/repository_memory.py +459 -0
  217. loopx/capabilities/issue_fix/repository_memory_provider.py +1454 -0
  218. loopx/capabilities/issue_fix/repository_snapshot.py +454 -0
  219. loopx/capabilities/issue_fix/reviewer_cli.py +917 -0
  220. loopx/capabilities/issue_fix/reviewer_notification.py +882 -0
  221. loopx/capabilities/issue_fix/reviewer_notification_drain.py +942 -0
  222. loopx/capabilities/issue_fix/reviewer_recommendation.py +1057 -0
  223. loopx/capabilities/issue_fix/reviewer_request.py +1282 -0
  224. loopx/capabilities/issue_fix/reward_memory.py +879 -0
  225. loopx/capabilities/issue_fix/workflow_plan.py +1286 -0
  226. loopx/capabilities/material_lifecycle/__init__.py +161 -0
  227. loopx/capabilities/material_lifecycle/_validation.py +183 -0
  228. loopx/capabilities/material_lifecycle/apply.py +672 -0
  229. loopx/capabilities/material_lifecycle/architecture.py +122 -0
  230. loopx/capabilities/material_lifecycle/cli.py +161 -0
  231. loopx/capabilities/material_lifecycle/decision_planning.py +470 -0
  232. loopx/capabilities/material_lifecycle/explore_execution.py +306 -0
  233. loopx/capabilities/material_lifecycle/intake.py +869 -0
  234. loopx/capabilities/material_lifecycle/inventory.py +147 -0
  235. loopx/capabilities/material_lifecycle/lifecycle.py +98 -0
  236. loopx/capabilities/material_lifecycle/preparation.py +147 -0
  237. loopx/capabilities/material_lifecycle/project_skill.py +83 -0
  238. loopx/capabilities/material_lifecycle/ranking.py +267 -0
  239. loopx/capabilities/material_lifecycle/readable_projection.py +500 -0
  240. loopx/capabilities/material_lifecycle/rebuild.py +480 -0
  241. loopx/capabilities/material_lifecycle/settlement.py +238 -0
  242. loopx/capabilities/periodic_report/__init__.py +71 -0
  243. loopx/capabilities/periodic_report/adapters.py +939 -0
  244. loopx/capabilities/periodic_report/archive.py +422 -0
  245. loopx/capabilities/periodic_report/bindings.py +705 -0
  246. loopx/capabilities/periodic_report/cli.py +277 -0
  247. loopx/capabilities/periodic_report/core.py +691 -0
  248. loopx/capabilities/periodic_report/extension_envelope.py +66 -0
  249. loopx/capabilities/periodic_report/presets.py +103 -0
  250. loopx/capabilities/periodic_report/profile.py +235 -0
  251. loopx/capabilities/periodic_report/project_progress.py +179 -0
  252. loopx/capabilities/periodic_report/triggers.py +452 -0
  253. loopx/capabilities/pr_review_queue/__init__.py +17 -0
  254. loopx/capabilities/pr_review_queue/core.py +506 -0
  255. loopx/capabilities/pr_review_queue/review_contract.py +506 -0
  256. loopx/capabilities/registry.py +192 -0
  257. loopx/capabilities/reward_memory/__init__.py +75 -0
  258. loopx/capabilities/reward_memory/application.py +819 -0
  259. loopx/capabilities/reward_memory/architecture.py +572 -0
  260. loopx/capabilities/reward_memory/candidate_review.py +511 -0
  261. loopx/capabilities/reward_memory/cli.py +469 -0
  262. loopx/capabilities/reward_memory/dogfood.py +574 -0
  263. loopx/capabilities/reward_memory/evaluation.py +296 -0
  264. loopx/capabilities/reward_memory/evaluation_fixtures.py +362 -0
  265. loopx/capabilities/reward_memory/experiment.py +567 -0
  266. loopx/capabilities/reward_memory/health.py +222 -0
  267. loopx/capabilities/reward_memory/ingestion.py +519 -0
  268. loopx/capabilities/reward_memory/registry.py +600 -0
  269. loopx/capabilities/reward_memory/runtime_hooks.py +312 -0
  270. loopx/capabilities/reward_memory/scoped_feedback.py +173 -0
  271. loopx/capabilities/semantic_preference/__init__.py +12 -0
  272. loopx/capabilities/semantic_preference/cli.py +189 -0
  273. loopx/capabilities/semantic_preference/contract.py +592 -0
  274. loopx/capabilities/semantic_preference/reward_memory.py +62 -0
  275. loopx/capabilities/value_connectors/__init__.py +1 -0
  276. loopx/capabilities/value_connectors/cli.py +401 -0
  277. loopx/capabilities/value_connectors/finance_extension_migration.py +108 -0
  278. loopx/capabilities/value_connectors/install_check.py +147 -0
  279. loopx/capabilities/value_connectors/planner.py +733 -0
  280. loopx/capabilities/value_connectors/source_map.py +446 -0
  281. loopx/claude_goal_baseline.py +138 -0
  282. loopx/claude_goal_mode/__init__.py +23 -0
  283. loopx/claude_goal_mode/hooks/goal_policy.py +212 -0
  284. loopx/claude_goal_mode/hooks/goal_state.py +139 -0
  285. loopx/claude_goal_mode/mcp/loopx_mcp.py +167 -0
  286. loopx/claude_goal_mode/scripts/connect.py +103 -0
  287. loopx/claude_goal_mode/scripts/goalmode_cmd.py +241 -0
  288. loopx/claude_goal_mode/scripts/install.py +328 -0
  289. loopx/claude_goal_mode/statusline/goal_status.py +97 -0
  290. loopx/cli.py +836 -0
  291. loopx/cli_commands/__init__.py +334 -0
  292. loopx/cli_commands/_host_thread.py +13 -0
  293. loopx/cli_commands/agentissue_runner_flow.py +447 -0
  294. loopx/cli_commands/agents_last_exam.py +160 -0
  295. loopx/cli_commands/agents_last_exam_baked_input.py +302 -0
  296. loopx/cli_commands/agents_last_exam_host_codex.py +374 -0
  297. loopx/cli_commands/agents_last_exam_launch_dry_run.py +372 -0
  298. loopx/cli_commands/agents_last_exam_local_plan.py +322 -0
  299. loopx/cli_commands/agents_last_exam_runner_source.py +352 -0
  300. loopx/cli_commands/agents_last_exam_task_material.py +335 -0
  301. loopx/cli_commands/agents_last_exam_validation_gate.py +236 -0
  302. loopx/cli_commands/benchmark_boundary.py +499 -0
  303. loopx/cli_commands/benchmark_dispatch.py +161 -0
  304. loopx/cli_commands/benchmark_release_outcome.py +123 -0
  305. loopx/cli_commands/benchmark_review_lifecycle.py +1275 -0
  306. loopx/cli_commands/benchmark_run_ledger.py +763 -0
  307. loopx/cli_commands/benchmark_run_ledger_case_analysis.py +249 -0
  308. loopx/cli_commands/benchmark_run_ledger_classification.py +45 -0
  309. loopx/cli_commands/benchmark_run_ledger_maintenance.py +486 -0
  310. loopx/cli_commands/benchmark_run_ledger_maintenance_registration.py +342 -0
  311. loopx/cli_commands/benchmark_run_ledger_maintenance_rendering.py +233 -0
  312. loopx/cli_commands/benchmark_run_ledger_parity.py +92 -0
  313. loopx/cli_commands/bootstrap_connect.py +238 -0
  314. loopx/cli_commands/canary.py +707 -0
  315. loopx/cli_commands/canary_release_qualification.py +79 -0
  316. loopx/cli_commands/capability.py +96 -0
  317. loopx/cli_commands/doctor.py +43 -0
  318. loopx/cli_commands/dreaming.py +143 -0
  319. loopx/cli_commands/edgebench.py +205 -0
  320. loopx/cli_commands/evidence_log.py +275 -0
  321. loopx/cli_commands/explore.py +989 -0
  322. loopx/cli_commands/explore_planning_commands.py +157 -0
  323. loopx/cli_commands/extension.py +271 -0
  324. loopx/cli_commands/first_run_report.py +73 -0
  325. loopx/cli_commands/goal_channel.py +656 -0
  326. loopx/cli_commands/handoff_mode.py +158 -0
  327. loopx/cli_commands/history.py +622 -0
  328. loopx/cli_commands/host_mode_plan.py +113 -0
  329. loopx/cli_commands/lark_inbox.py +431 -0
  330. loopx/cli_commands/lark_kanban.py +629 -0
  331. loopx/cli_commands/ml_experiment.py +321 -0
  332. loopx/cli_commands/multi_agent.py +211 -0
  333. loopx/cli_commands/opencode2_goal_worker.py +217 -0
  334. loopx/cli_commands/pr_review.py +167 -0
  335. loopx/cli_commands/presentation.py +218 -0
  336. loopx/cli_commands/preset.py +96 -0
  337. loopx/cli_commands/project.py +150 -0
  338. loopx/cli_commands/project_lifecycle.py +915 -0
  339. loopx/cli_commands/quota.py +859 -0
  340. loopx/cli_commands/quota_registration.py +241 -0
  341. loopx/cli_commands/quota_request.py +113 -0
  342. loopx/cli_commands/ready_score.py +110 -0
  343. loopx/cli_commands/registry_admin.py +975 -0
  344. loopx/cli_commands/registry_admin_configure.py +344 -0
  345. loopx/cli_commands/registry_admin_peer.py +84 -0
  346. loopx/cli_commands/registry_authority.py +218 -0
  347. loopx/cli_commands/review_batch.py +146 -0
  348. loopx/cli_commands/slash_commands.py +145 -0
  349. loopx/cli_commands/start_goal.py +251 -0
  350. loopx/cli_commands/starter.py +175 -0
  351. loopx/cli_commands/starter_bootstrap.py +179 -0
  352. loopx/cli_commands/starter_bootstrap_registration.py +198 -0
  353. loopx/cli_commands/starter_runtime_idle.py +107 -0
  354. loopx/cli_commands/starter_scheduler.py +207 -0
  355. loopx/cli_commands/starter_session_runtime.py +152 -0
  356. loopx/cli_commands/starter_visible_common.py +54 -0
  357. loopx/cli_commands/starter_visible_driver.py +161 -0
  358. loopx/cli_commands/starter_visible_pilot.py +278 -0
  359. loopx/cli_commands/status.py +867 -0
  360. loopx/cli_commands/status_registration.py +239 -0
  361. loopx/cli_commands/summary_all.py +222 -0
  362. loopx/cli_commands/support_control.py +809 -0
  363. loopx/cli_commands/support_control_registry.py +68 -0
  364. loopx/cli_commands/support_control_supervisor.py +289 -0
  365. loopx/cli_commands/task_lease.py +306 -0
  366. loopx/cli_commands/terminal_bench_adapter.py +717 -0
  367. loopx/cli_commands/terminal_bench_environment_result.py +1246 -0
  368. loopx/cli_commands/todo.py +940 -0
  369. loopx/cli_commands/todo_argument_validation.py +572 -0
  370. loopx/cli_commands/todo_event.py +114 -0
  371. loopx/cli_commands/turn.py +804 -0
  372. loopx/cli_commands/version.py +46 -0
  373. loopx/cli_commands/worker_bridge.py +659 -0
  374. loopx/cli_rollout.py +314 -0
  375. loopx/codex_cli_goal_tui.py +672 -0
  376. loopx/codex_cli_probe.py +1530 -0
  377. loopx/codex_cli_probe_markdown.py +935 -0
  378. loopx/codex_cli_runtime_probe.py +733 -0
  379. loopx/codex_cli_scheduler.py +564 -0
  380. loopx/codex_goal_baseline.py +620 -0
  381. loopx/configuration_catalog.py +617 -0
  382. loopx/configure_goal.py +1375 -0
  383. loopx/contract.py +996 -0
  384. loopx/control_plane/__init__.py +71 -0
  385. loopx/control_plane/agents/__init__.py +1 -0
  386. loopx/control_plane/agents/agent_lane_recommendation.py +516 -0
  387. loopx/control_plane/agents/agent_scope.py +1578 -0
  388. loopx/control_plane/agents/agent_scope_frontier.py +60 -0
  389. loopx/control_plane/agents/capability_gate.py +531 -0
  390. loopx/control_plane/agents/identity.py +140 -0
  391. loopx/control_plane/agents/legacy_migration.py +169 -0
  392. loopx/control_plane/agents/management_projection.py +658 -0
  393. loopx/control_plane/agents/material_frontier.py +608 -0
  394. loopx/control_plane/agents/material_handoff.py +156 -0
  395. loopx/control_plane/agents/multi_agent/__init__.py +1 -0
  396. loopx/control_plane/agents/multi_agent/codex_executable.py +207 -0
  397. loopx/control_plane/agents/multi_agent/collective_round_ledger.py +387 -0
  398. loopx/control_plane/agents/multi_agent/contract.py +474 -0
  399. loopx/control_plane/agents/multi_agent/recipe.py +110 -0
  400. loopx/control_plane/agents/multi_agent/role_successor.py +297 -0
  401. loopx/control_plane/agents/multi_agent/runtime_scripts.py +426 -0
  402. loopx/control_plane/agents/multi_agent/visible_launch_policy.py +149 -0
  403. loopx/control_plane/agents/multi_agent/visible_wake_scheduler.py +392 -0
  404. loopx/control_plane/agents/profile.py +216 -0
  405. loopx/control_plane/agents/runtime_model.py +73 -0
  406. loopx/control_plane/agents/subagent_activity.py +164 -0
  407. loopx/control_plane/agents/supervisor.py +544 -0
  408. loopx/control_plane/agents/supervisor_events.py +462 -0
  409. loopx/control_plane/agents/supervisor_inject.py +204 -0
  410. loopx/control_plane/agents/work_mode.py +56 -0
  411. loopx/control_plane/agents/workspace_guard.py +364 -0
  412. loopx/control_plane/effect_program.py +644 -0
  413. loopx/control_plane/goals/__init__.py +1 -0
  414. loopx/control_plane/goals/active_state_event_projection.py +103 -0
  415. loopx/control_plane/goals/active_state_metadata.py +47 -0
  416. loopx/control_plane/goals/active_state_sections.py +58 -0
  417. loopx/control_plane/goals/configure_goal_service.py +354 -0
  418. loopx/control_plane/goals/contract_health.py +132 -0
  419. loopx/control_plane/goals/dreaming.py +152 -0
  420. loopx/control_plane/goals/global_registry_health.py +199 -0
  421. loopx/control_plane/goals/global_registry_shadow.py +33 -0
  422. loopx/control_plane/goals/goal_channel.py +34 -0
  423. loopx/control_plane/goals/goal_channel_projection.py +560 -0
  424. loopx/control_plane/goals/goal_frontier/__init__.py +1917 -0
  425. loopx/control_plane/goals/goal_frontier/ack_policy.py +149 -0
  426. loopx/control_plane/goals/goal_frontier/outcome_continuity.py +437 -0
  427. loopx/control_plane/goals/goal_frontier/replan_rules.py +210 -0
  428. loopx/control_plane/goals/goal_frontier/semantic_history.py +314 -0
  429. loopx/control_plane/goals/goal_frontier/terminal.py +180 -0
  430. loopx/control_plane/goals/goal_vision.py +443 -0
  431. loopx/control_plane/goals/goal_vision_policy.py +36 -0
  432. loopx/control_plane/goals/goal_vision_state.py +62 -0
  433. loopx/control_plane/goals/goal_vision_wait.py +290 -0
  434. loopx/control_plane/goals/path_resolution.py +20 -0
  435. loopx/control_plane/goals/start_contract.py +206 -0
  436. loopx/control_plane/goals/vision_checkpoint.py +92 -0
  437. loopx/control_plane/handoff/__init__.py +1 -0
  438. loopx/control_plane/handoff/cross_runtime_impl_review.py +311 -0
  439. loopx/control_plane/handoff/delivery_contract.py +161 -0
  440. loopx/control_plane/handoff/handoff_runs.py +71 -0
  441. loopx/control_plane/handoff/project_handoff.py +155 -0
  442. loopx/control_plane/handoff/review_batch.py +463 -0
  443. loopx/control_plane/handoff/review_packet_context.py +216 -0
  444. loopx/control_plane/heartbeat/agent.py +173 -0
  445. loopx/control_plane/heartbeat/budget.py +66 -0
  446. loopx/control_plane/heartbeat/builder.py +501 -0
  447. loopx/control_plane/heartbeat/host.py +64 -0
  448. loopx/control_plane/heartbeat/rules.py +68 -0
  449. loopx/control_plane/heartbeat/task_body.py +759 -0
  450. loopx/control_plane/heartbeat/visible_goal.py +86 -0
  451. loopx/control_plane/projects/__init__.py +1 -0
  452. loopx/control_plane/projects/contract.py +25 -0
  453. loopx/control_plane/projects/registry.py +663 -0
  454. loopx/control_plane/quota/__init__.py +1 -0
  455. loopx/control_plane/quota/cli_projection.py +704 -0
  456. loopx/control_plane/quota/decision_summary.py +431 -0
  457. loopx/control_plane/quota/effect_program.py +152 -0
  458. loopx/control_plane/quota/error_codes.py +19 -0
  459. loopx/control_plane/quota/goal_boundary.py +464 -0
  460. loopx/control_plane/quota/heartbeat_receipt.py +277 -0
  461. loopx/control_plane/quota/heartbeat_recommendation.py +718 -0
  462. loopx/control_plane/quota/host_poll_receipts.py +162 -0
  463. loopx/control_plane/quota/live_decision.py +142 -0
  464. loopx/control_plane/quota/monitor_poll.py +786 -0
  465. loopx/control_plane/quota/policy_constants.py +40 -0
  466. loopx/control_plane/quota/projection_repair.py +262 -0
  467. loopx/control_plane/quota/recent_runs.py +210 -0
  468. loopx/control_plane/quota/scheduler_ack.py +490 -0
  469. loopx/control_plane/quota/selected_todo_projection.py +139 -0
  470. loopx/control_plane/quota/settlement.py +437 -0
  471. loopx/control_plane/quota/settlement_cli.py +246 -0
  472. loopx/control_plane/quota/settlement_validation.py +64 -0
  473. loopx/control_plane/quota/settlement_workspace_causality.py +180 -0
  474. loopx/control_plane/quota/should_run.py +249 -0
  475. loopx/control_plane/quota/should_run_packet.py +1165 -0
  476. loopx/control_plane/quota/should_run_prepare.py +675 -0
  477. loopx/control_plane/quota/slot_accounting.py +1123 -0
  478. loopx/control_plane/quota/spend_sources.py +11 -0
  479. loopx/control_plane/quota/stall_repair.py +397 -0
  480. loopx/control_plane/quota/states.py +29 -0
  481. loopx/control_plane/quota/task_orchestration.py +448 -0
  482. loopx/control_plane/quota/task_orchestration_admission.py +497 -0
  483. loopx/control_plane/quota/turn_envelope.py +889 -0
  484. loopx/control_plane/quota/usage_summary.py +140 -0
  485. loopx/control_plane/reward_memory.py +43 -0
  486. loopx/control_plane/runtime/__init__.py +2 -0
  487. loopx/control_plane/runtime/active_user_assisted_pilot.py +275 -0
  488. loopx/control_plane/runtime/agent_scoped_evidence_log.py +435 -0
  489. loopx/control_plane/runtime/decision_freshness.py +203 -0
  490. loopx/control_plane/runtime/event_ledger.py +197 -0
  491. loopx/control_plane/runtime/event_store_migration_bridge.py +196 -0
  492. loopx/control_plane/runtime/goal_project_route.py +70 -0
  493. loopx/control_plane/runtime/local_state_write_correctness.py +242 -0
  494. loopx/control_plane/runtime/promotion_readiness.py +152 -0
  495. loopx/control_plane/runtime/public_safety.py +120 -0
  496. loopx/control_plane/runtime/run_artifacts.py +78 -0
  497. loopx/control_plane/runtime/run_compaction.py +397 -0
  498. loopx/control_plane/runtime/run_context_retention.py +241 -0
  499. loopx/control_plane/runtime/run_history.py +132 -0
  500. loopx/control_plane/runtime/run_index_duplicates.py +205 -0
  501. loopx/control_plane/runtime/run_index_rebuild.py +263 -0
  502. loopx/control_plane/runtime/run_ingest_health.py +336 -0
  503. loopx/control_plane/runtime/runtime_projection_route.py +624 -0
  504. loopx/control_plane/runtime/runtime_projection_writer.py +98 -0
  505. loopx/control_plane/runtime/session_runtime.py +339 -0
  506. loopx/control_plane/runtime/shared_runtime_material_projection.py +332 -0
  507. loopx/control_plane/runtime/shared_runtime_refresh_projection.py +183 -0
  508. loopx/control_plane/runtime/stale_latest_run.py +90 -0
  509. loopx/control_plane/runtime/status_classifications.py +49 -0
  510. loopx/control_plane/runtime/status_projection_cache.py +235 -0
  511. loopx/control_plane/runtime/stride_observation.py +144 -0
  512. loopx/control_plane/runtime/time.py +39 -0
  513. loopx/control_plane/runtime/trajectory_hygiene.py +149 -0
  514. loopx/control_plane/runtime/validation_command.py +69 -0
  515. loopx/control_plane/scheduler/__init__.py +1 -0
  516. loopx/control_plane/scheduler/ack.py +329 -0
  517. loopx/control_plane/scheduler/arbitration.py +188 -0
  518. loopx/control_plane/scheduler/automation_liveness.py +183 -0
  519. loopx/control_plane/scheduler/execution_context.py +555 -0
  520. loopx/control_plane/scheduler/external_evidence_observation.py +428 -0
  521. loopx/control_plane/scheduler/monitor_display.py +143 -0
  522. loopx/control_plane/scheduler/monitor_poll_policy.py +161 -0
  523. loopx/control_plane/scheduler/monitor_poll_writeback.py +351 -0
  524. loopx/control_plane/scheduler/monitor_target.py +64 -0
  525. loopx/control_plane/scheduler/monitor_todo.py +146 -0
  526. loopx/control_plane/scheduler/monitor_wait.py +237 -0
  527. loopx/control_plane/scheduler/scheduler_hint.py +1284 -0
  528. loopx/control_plane/scheduler/state.py +354 -0
  529. loopx/control_plane/scheduler/state_transition_rules.py +179 -0
  530. loopx/control_plane/scheduler/time.py +10 -0
  531. loopx/control_plane/settlement_driver.py +293 -0
  532. loopx/control_plane/status/__init__.py +6 -0
  533. loopx/control_plane/status/active_state_projection.py +105 -0
  534. loopx/control_plane/status/agent_lane_projection.py +375 -0
  535. loopx/control_plane/status/attention_projection.py +74 -0
  536. loopx/control_plane/status/autonomous_replan_projection.py +103 -0
  537. loopx/control_plane/status/collection.py +140 -0
  538. loopx/control_plane/status/contract_projection.py +31 -0
  539. loopx/control_plane/status/dreaming_projection.py +52 -0
  540. loopx/control_plane/status/goal_attention_projection.py +157 -0
  541. loopx/control_plane/status/lifecycle_projection.py +110 -0
  542. loopx/control_plane/status/monitor_display_projection.py +69 -0
  543. loopx/control_plane/status/registry_health_projection.py +75 -0
  544. loopx/control_plane/status/run_projection.py +70 -0
  545. loopx/control_plane/status/runtime_summaries.py +161 -0
  546. loopx/control_plane/testing/__init__.py +1 -0
  547. loopx/control_plane/testing/actual_default_model_behavior_portfolio.py +1371 -0
  548. loopx/control_plane/testing/canary_harness.py +182 -0
  549. loopx/control_plane/testing/capability_monitor_repair_tool_behavior.py +674 -0
  550. loopx/control_plane/testing/cli_output_budget.py +807 -0
  551. loopx/control_plane/testing/cli_output_differential.py +250 -0
  552. loopx/control_plane/testing/cli_output_semantics.py +87 -0
  553. loopx/control_plane/testing/control_plane_composition_scenarios.py +225 -0
  554. loopx/control_plane/testing/decision_replay.py +268 -0
  555. loopx/control_plane/testing/doubao_model_behavior_actor.py +559 -0
  556. loopx/control_plane/testing/model_behavior_corpus.py +344 -0
  557. loopx/control_plane/testing/model_behavior_qualification.py +769 -0
  558. loopx/control_plane/testing/model_behavior_retained_cases.py +235 -0
  559. loopx/control_plane/testing/model_tool_behavior.py +536 -0
  560. loopx/control_plane/testing/onboarding_model_behavior_qualification.py +642 -0
  561. loopx/control_plane/testing/quota_fixtures.py +208 -0
  562. loopx/control_plane/testing/quota_should_run_parity.py +57 -0
  563. loopx/control_plane/testing/release_commit_qualification.py +671 -0
  564. loopx/control_plane/testing/replan_semantic_action_behavior.py +1302 -0
  565. loopx/control_plane/testing/scoped_gate_successor_tool_behavior.py +527 -0
  566. loopx/control_plane/testing/selected_todo_tool_behavior.py +1002 -0
  567. loopx/control_plane/testing/terminal_settlement_tool_behavior.py +656 -0
  568. loopx/control_plane/todos/__init__.py +1 -0
  569. loopx/control_plane/todos/active_state_editing.py +296 -0
  570. loopx/control_plane/todos/active_state_todo_parser.py +138 -0
  571. loopx/control_plane/todos/active_state_todos.py +175 -0
  572. loopx/control_plane/todos/addition.py +103 -0
  573. loopx/control_plane/todos/claim_visibility.py +253 -0
  574. loopx/control_plane/todos/completed_archive.py +139 -0
  575. loopx/control_plane/todos/completion_fence.py +49 -0
  576. loopx/control_plane/todos/completion_policy.py +153 -0
  577. loopx/control_plane/todos/completion_validation.py +248 -0
  578. loopx/control_plane/todos/completion_validation_accountability.py +27 -0
  579. loopx/control_plane/todos/completion_validation_projection.py +57 -0
  580. loopx/control_plane/todos/contract.py +1476 -0
  581. loopx/control_plane/todos/decision_scope.py +554 -0
  582. loopx/control_plane/todos/deferred_resume.py +546 -0
  583. loopx/control_plane/todos/durable_completion.py +201 -0
  584. loopx/control_plane/todos/event_writeback.py +484 -0
  585. loopx/control_plane/todos/frontier_deadline.py +132 -0
  586. loopx/control_plane/todos/handoff_gate.py +283 -0
  587. loopx/control_plane/todos/handoff_mode.py +444 -0
  588. loopx/control_plane/todos/handoff_note.py +202 -0
  589. loopx/control_plane/todos/line_update.py +361 -0
  590. loopx/control_plane/todos/list_projection.py +205 -0
  591. loopx/control_plane/todos/markdown.py +199 -0
  592. loopx/control_plane/todos/monitor_metadata.py +88 -0
  593. loopx/control_plane/todos/mutation_authority.py +299 -0
  594. loopx/control_plane/todos/projection.py +655 -0
  595. loopx/control_plane/todos/quota_summary.py +1138 -0
  596. loopx/control_plane/todos/route_continuation.py +267 -0
  597. loopx/control_plane/todos/succession_warning.py +174 -0
  598. loopx/control_plane/todos/summary_item.py +223 -0
  599. loopx/control_plane/todos/text.py +30 -0
  600. loopx/control_plane/todos/todo_index.py +226 -0
  601. loopx/control_plane/todos/todo_summary.py +1458 -0
  602. loopx/control_plane/todos/unblock_resume.py +326 -0
  603. loopx/control_plane/todos/user_gate.py +263 -0
  604. loopx/control_plane/todos/write_hint.py +63 -0
  605. loopx/control_plane/todos/write_policy.py +135 -0
  606. loopx/control_plane/turn_driver/__init__.py +85 -0
  607. loopx/control_plane/turn_driver/codex_cli.py +502 -0
  608. loopx/control_plane/turn_driver/driver.py +355 -0
  609. loopx/control_plane/turn_driver/executor.py +1468 -0
  610. loopx/control_plane/turn_driver/loop_controller.py +669 -0
  611. loopx/control_plane/turn_driver/settlement.py +318 -0
  612. loopx/control_plane/turn_driver/transaction.py +375 -0
  613. loopx/control_plane/work_items/__init__.py +1 -0
  614. loopx/control_plane/work_items/attention_fields.py +56 -0
  615. loopx/control_plane/work_items/attention_item.py +77 -0
  616. loopx/control_plane/work_items/attention_queue.py +322 -0
  617. loopx/control_plane/work_items/attention_routing.py +213 -0
  618. loopx/control_plane/work_items/autonomous_candidates.py +135 -0
  619. loopx/control_plane/work_items/autonomous_replan_ack.py +276 -0
  620. loopx/control_plane/work_items/autonomous_replan_obligation.py +786 -0
  621. loopx/control_plane/work_items/backlog_hygiene.py +59 -0
  622. loopx/control_plane/work_items/capability_monitor_fallback.py +221 -0
  623. loopx/control_plane/work_items/delivery_batch_scale.py +66 -0
  624. loopx/control_plane/work_items/delivery_outcome.py +152 -0
  625. loopx/control_plane/work_items/delivery_signals.py +113 -0
  626. loopx/control_plane/work_items/execution_obligation.py +235 -0
  627. loopx/control_plane/work_items/goal_route_hint.py +320 -0
  628. loopx/control_plane/work_items/interaction_contract.py +1540 -0
  629. loopx/control_plane/work_items/issue_meta_surface.py +159 -0
  630. loopx/control_plane/work_items/lifecycle.py +139 -0
  631. loopx/control_plane/work_items/operator_inbox.py +266 -0
  632. loopx/control_plane/work_items/outcome_followthrough.py +69 -0
  633. loopx/control_plane/work_items/primary_action.py +326 -0
  634. loopx/control_plane/work_items/progress_observation.py +630 -0
  635. loopx/control_plane/work_items/project_asset.py +675 -0
  636. loopx/control_plane/work_items/repair_delta.py +693 -0
  637. loopx/control_plane/work_items/runtime_capability_reentry.py +168 -0
  638. loopx/control_plane/work_items/semantic_replan_writeback.py +177 -0
  639. loopx/control_plane/work_items/status_contract.py +49 -0
  640. loopx/control_plane/work_items/task_graph.py +1046 -0
  641. loopx/control_plane/work_items/task_lease.py +1254 -0
  642. loopx/control_plane/work_items/task_lease_settlement.py +422 -0
  643. loopx/control_plane/work_items/work_lane.py +510 -0
  644. loopx/control_plane/work_items/work_lane_context.py +161 -0
  645. loopx/demo.py +247 -0
  646. loopx/diagnose.py +633 -0
  647. loopx/doctor.py +1251 -0
  648. loopx/domain_packs/__init__.py +1 -0
  649. loopx/domain_packs/issue_fix.py +571 -0
  650. loopx/domain_packs/ml_experiment.py +854 -0
  651. loopx/domain_state.py +137 -0
  652. loopx/dreaming.py +706 -0
  653. loopx/entrypoint.py +16 -0
  654. loopx/event_sourced_state.py +981 -0
  655. loopx/execution_profile.py +286 -0
  656. loopx/experiments/__init__.py +1 -0
  657. loopx/experiments/planner_worker/__init__.py +1 -0
  658. loopx/experiments/planner_worker/contract.py +523 -0
  659. loopx/experiments/planner_worker/runtime.py +391 -0
  660. loopx/experiments/planner_worker/traex.py +461 -0
  661. loopx/explore_graph.py +11 -0
  662. loopx/extensions/__init__.py +1 -0
  663. loopx/extensions/bundled.py +28 -0
  664. loopx/extensions/execution_envelope.py +126 -0
  665. loopx/extensions/lark/__init__.py +11 -0
  666. loopx/extensions/lark/event_collector.py +478 -0
  667. loopx/extensions/lark/event_collector_runtime.py +506 -0
  668. loopx/extensions/lark/event_inbox.py +454 -0
  669. loopx/extensions/lark/extension.toml +88 -0
  670. loopx/extensions/lark/goal_channel.py +44 -0
  671. loopx/extensions/lark/goal_channel_contracts.py +388 -0
  672. loopx/extensions/lark/goal_channel_lifecycle.py +218 -0
  673. loopx/extensions/lark/goal_channel_runtime.py +792 -0
  674. loopx/extensions/lark/goal_channel_setup.py +805 -0
  675. loopx/extensions/lark/goal_channel_targets.py +215 -0
  676. loopx/extensions/lark/goal_channel_transport.py +281 -0
  677. loopx/extensions/lark/inbox_reactions.py +650 -0
  678. loopx/extensions/lark/inbox_reply.py +430 -0
  679. loopx/extensions/lark/presentation/__init__.py +11 -0
  680. loopx/extensions/lark/presentation/explore_results.py +2276 -0
  681. loopx/extensions/lark/presentation/explore_singleflight.py +127 -0
  682. loopx/extensions/lark/presentation/explore_source_guard.py +121 -0
  683. loopx/extensions/lark/presentation/explore_stage_document.py +703 -0
  684. loopx/extensions/lark/presentation/explore_visual_integrity.py +122 -0
  685. loopx/extensions/lark/presentation/explore_visual_readback.py +452 -0
  686. loopx/extensions/lark/presentation/explore_visual_styles.py +156 -0
  687. loopx/extensions/lark/presentation/issue_fix_surface.py +612 -0
  688. loopx/extensions/lark/presentation/kanban.py +2791 -0
  689. loopx/extensions/lark/presentation/message_card.py +112 -0
  690. loopx/extensions/lark/presentation/periodic_report.py +261 -0
  691. loopx/extensions/lark/presentation/projection_rows.py +600 -0
  692. loopx/extensions/lark/presentation/record_io.py +95 -0
  693. loopx/extensions/lark/presentation/sync_receipt.py +145 -0
  694. loopx/extensions/lark/private_json.py +40 -0
  695. loopx/extensions/lark/provider.py +86 -0
  696. loopx/extensions/lark/reviewer_notification.py +604 -0
  697. loopx/extensions/manifest.py +385 -0
  698. loopx/extensions/openviking_periodic_report/__init__.py +17 -0
  699. loopx/extensions/openviking_periodic_report/activation.py +173 -0
  700. loopx/extensions/openviking_periodic_report/extension.toml +17 -0
  701. loopx/extensions/openviking_periodic_report/provider.py +355 -0
  702. loopx/extensions/openviking_periodic_report/sink.py +117 -0
  703. loopx/extensions/openviking_semantic_preference/__init__.py +5 -0
  704. loopx/extensions/openviking_semantic_preference/extension.toml +16 -0
  705. loopx/extensions/openviking_semantic_preference/history_export.py +484 -0
  706. loopx/extensions/openviking_semantic_preference/project_peer.py +68 -0
  707. loopx/extensions/openviking_semantic_preference/provider.py +312 -0
  708. loopx/extensions/presentation.py +979 -0
  709. loopx/extensions/process_runtime.py +204 -0
  710. loopx/extensions/readiness.py +168 -0
  711. loopx/extensions/runtime.py +931 -0
  712. loopx/extensions/scaffold.py +335 -0
  713. loopx/feedback.py +581 -0
  714. loopx/file_lock.py +382 -0
  715. loopx/global_registry.py +842 -0
  716. loopx/global_risks.py +970 -0
  717. loopx/global_todos.py +568 -0
  718. loopx/handoff_budget.py +28 -0
  719. loopx/heartbeat_prequota.py +80 -0
  720. loopx/heartbeat_prompt.py +159 -0
  721. loopx/help_surface.py +516 -0
  722. loopx/history.py +1507 -0
  723. loopx/host_loop_activation.py +1311 -0
  724. loopx/host_mode_planner.py +991 -0
  725. loopx/install_contract.py +1 -0
  726. loopx/interface_budget.py +196 -0
  727. loopx/long_task_cadence.py +208 -0
  728. loopx/materials.py +185 -0
  729. loopx/ml_experiment.py +3 -0
  730. loopx/onboarding.py +214 -0
  731. loopx/opencode2_goal_mode/README.md +81 -0
  732. loopx/opencode2_goal_mode/__init__.py +9 -0
  733. loopx/opencode2_goal_mode/opencode2-goal-worker.mjs +1018 -0
  734. loopx/opencode_goal_mode/README.md +99 -0
  735. loopx/opencode_goal_mode/__init__.py +13 -0
  736. loopx/opencode_goal_mode/goal-bridge-runtime.mjs +858 -0
  737. loopx/opencode_goal_mode/loopx-goal.js +8 -0
  738. loopx/operator_gate.py +420 -0
  739. loopx/orchestration.py +127 -0
  740. loopx/paths.py +59 -0
  741. loopx/pi_goal_mode/README.md +67 -0
  742. loopx/pi_goal_mode/__init__.py +13 -0
  743. loopx/pi_goal_mode/loopx-goal.ts +254 -0
  744. loopx/pi_goal_mode/pi-goal-loop-runtime.mjs +574 -0
  745. loopx/pr_review.py +1206 -0
  746. loopx/presentation/__init__.py +1 -0
  747. loopx/presentation/explore_views.py +1334 -0
  748. loopx/presentation/markdown.py +61 -0
  749. loopx/presentation/projection_source_reconcile.py +140 -0
  750. loopx/presentation/public_safety.py +42 -0
  751. loopx/presentation/renderers/__init__.py +17 -0
  752. loopx/presentation/renderers/goal_channel_html.py +269 -0
  753. loopx/presentation/renderers/periodic_report_html.py +786 -0
  754. loopx/presentation/renderers/periodic_report_markdown.py +184 -0
  755. loopx/presentation/renderers/quota_event_markdown.py +116 -0
  756. loopx/presentation/renderers/quota_markdown.py +1112 -0
  757. loopx/presentation/renderers/status_markdown.py +1570 -0
  758. loopx/presentation/renderers/trajectory_hygiene_markdown.py +39 -0
  759. loopx/presentation/renderers/turn_envelope_markdown.py +33 -0
  760. loopx/presentation/sinks/__init__.py +5 -0
  761. loopx/presentation/sinks/openviking_periodic_report.py +7 -0
  762. loopx/presentation/static_site.py +691 -0
  763. loopx/presets.py +369 -0
  764. loopx/project_alias.py +217 -0
  765. loopx/project_map.py +589 -0
  766. loopx/project_prompt.py +1153 -0
  767. loopx/project_skill_cli.py +125 -0
  768. loopx/project_skill_delivery.py +470 -0
  769. loopx/project_uninstall.py +462 -0
  770. loopx/promotion_gate.py +197 -0
  771. loopx/quota.py +1197 -0
  772. loopx/ready_score.py +413 -0
  773. loopx/registry.py +621 -0
  774. loopx/registry_writability.py +64 -0
  775. loopx/release_candidate.py +148 -0
  776. loopx/release_manifest.py +316 -0
  777. loopx/repository_identity.py +100 -0
  778. loopx/review_packet.py +1024 -0
  779. loopx/rollout_event_log.py +505 -0
  780. loopx/runtime.py +112 -0
  781. loopx/self_update.py +750 -0
  782. loopx/session_runtime.py +418 -0
  783. loopx/skill_install_readback.py +500 -0
  784. loopx/slash_command_install.py +1393 -0
  785. loopx/slash_commands.py +264 -0
  786. loopx/state_backup.py +573 -0
  787. loopx/state_migration.py +350 -0
  788. loopx/state_projection.py +809 -0
  789. loopx/state_refresh.py +1416 -0
  790. loopx/status.py +1383 -0
  791. loopx/status_server.py +935 -0
  792. loopx/summary_all.py +725 -0
  793. loopx/terminal_bench_agent.py +2056 -0
  794. loopx/thread_agent_binding.py +408 -0
  795. loopx/todo_followups.py +168 -0
  796. loopx/todo_suggestion_prompt.py +204 -0
  797. loopx/todos.py +2229 -0
  798. loopx/turn_identity.py +17 -0
  799. loopx/upgrade.py +1083 -0
  800. loopx/visible_governance.py +667 -0
  801. loopx/visible_multi_agent_launcher.py +1253 -0
  802. loopx/visible_multi_agent_tmux.py +429 -0
  803. loopx/worker_bridge.py +1574 -0
  804. loopx-0.4.8.dist-info/METADATA +708 -0
  805. loopx-0.4.8.dist-info/RECORD +811 -0
  806. loopx-0.4.8.dist-info/WHEEL +5 -0
  807. loopx-0.4.8.dist-info/entry_points.txt +5 -0
  808. loopx-0.4.8.dist-info/licenses/LICENSE +202 -0
  809. loopx-0.4.8.dist-info/licenses/LICENSE-MIT +21 -0
  810. loopx-0.4.8.dist-info/licenses/NOTICE +6 -0
  811. loopx-0.4.8.dist-info/top_level.txt +1 -0
@@ -0,0 +1,1057 @@
1
+ from __future__ import annotations
2
+
3
+ import fnmatch
4
+ import hashlib
5
+ import ipaddress
6
+ import re
7
+ import subprocess
8
+ from collections.abc import Mapping, Sequence
9
+ from datetime import datetime
10
+ from pathlib import Path, PurePosixPath
11
+ from typing import Any
12
+ from urllib.parse import urlsplit
13
+
14
+ from ...control_plane.runtime.public_safety import public_safe_compact_text
15
+
16
+
17
+ ISSUE_FIX_REVIEWER_RECOMMENDATION_SCHEMA_VERSION = (
18
+ "issue_fix_reviewer_recommendation_v0"
19
+ )
20
+ ISSUE_FIX_REVIEWER_SOURCES_INPUT_SCHEMA_VERSION = "issue_fix_reviewer_sources_input_v0"
21
+ CODEOWNERS_LOCATIONS = (
22
+ ".github/CODEOWNERS",
23
+ "CODEOWNERS",
24
+ "docs/CODEOWNERS",
25
+ )
26
+ GITHUB_NOREPLY_PATTERN = re.compile(
27
+ r"^(?:\d+\+)?(?P<login>[A-Za-z0-9](?:[A-Za-z0-9-]{0,38}))@users\.noreply\.github\.com$",
28
+ re.IGNORECASE,
29
+ )
30
+ REVIEWER_HANDLE_PATTERN = re.compile(
31
+ r"^@[A-Za-z0-9](?:[A-Za-z0-9-]{0,38})(?:/[A-Za-z0-9_.-]+)?$"
32
+ )
33
+ SAFE_GIT_REF_PATTERN = re.compile(r"^[A-Za-z0-9][A-Za-z0-9._/~^{}-]{0,119}$")
34
+ AUTOMATED_HISTORY_IDENTITY_PATTERN = re.compile(
35
+ r"(?:^|[-_.\s])bot(?:$|[-_.\s])|\[bot\]$",
36
+ re.IGNORECASE,
37
+ )
38
+ REVIEWER_SOURCE_KINDS = {"maintainer_map"}
39
+ REVIEWER_SOURCE_TRUST_LEVELS = {"authoritative", "verified", "advisory"}
40
+ REVIEWER_SOURCE_FRESHNESS_STATES = {"current", "stale", "unknown"}
41
+ REVIEWER_SOURCE_MATCH_KINDS = {
42
+ "path_prefix",
43
+ "path_glob",
44
+ "repository_fallback",
45
+ }
46
+ REVIEWER_SOURCE_ID_PATTERN = re.compile(r"^[A-Za-z0-9][A-Za-z0-9_.:-]{0,119}$")
47
+ REVIEWER_SOURCE_INPUT_FIELDS = {"schema_version", "sources"}
48
+ REVIEWER_SOURCE_FIELDS = {
49
+ "source_id",
50
+ "source_kind",
51
+ "reference",
52
+ "trust",
53
+ "freshness",
54
+ "observed_at",
55
+ "routes",
56
+ }
57
+ REVIEWER_SOURCE_ROUTE_FIELDS = {
58
+ "route_id",
59
+ "match_kind",
60
+ "pattern",
61
+ "primary_reviewers",
62
+ "fallback_reviewers",
63
+ }
64
+ MAX_REVIEWER_SOURCES = 12
65
+ MAX_REVIEWER_SOURCE_ROUTES = 100
66
+
67
+
68
+ def _run_git(repo_path: Path, args: Sequence[str]) -> str:
69
+ result = subprocess.run(
70
+ ["git", *args],
71
+ cwd=repo_path,
72
+ text=True,
73
+ stdout=subprocess.PIPE,
74
+ stderr=subprocess.PIPE,
75
+ timeout=20,
76
+ )
77
+ if result.returncode != 0:
78
+ raise RuntimeError(f"git {' '.join(args)} failed with exit code {result.returncode}")
79
+ return result.stdout
80
+
81
+
82
+ def _normalise_changed_file(value: Any) -> str:
83
+ text = str(value or "").strip().replace("\\", "/")
84
+ while text.startswith("./"):
85
+ text = text[2:]
86
+ path = PurePosixPath(text)
87
+ if not text or path.as_posix() == "." or path.is_absolute() or ".." in path.parts:
88
+ raise ValueError("changed files must be non-empty repo-relative paths")
89
+ return path.as_posix()
90
+
91
+
92
+ def _normalise_changed_files(values: Sequence[Any]) -> list[str]:
93
+ files: list[str] = []
94
+ for raw in values:
95
+ path = _normalise_changed_file(raw)
96
+ if path not in files:
97
+ files.append(path)
98
+ if len(files) > 100:
99
+ raise ValueError("reviewer recommendation accepts at most 100 changed files")
100
+ return files
101
+
102
+
103
+ def _normalise_base_ref(value: Any) -> str:
104
+ text = str(value or "").strip()
105
+ if not SAFE_GIT_REF_PATTERN.fullmatch(text):
106
+ raise ValueError("base_ref must be a compact non-option git revision")
107
+ return text
108
+
109
+
110
+ def _normalise_reviewer_handle(value: Any) -> str | None:
111
+ text = str(value or "").strip()
112
+ if text and not text.startswith("@"):
113
+ text = f"@{text}"
114
+ return text.lower() if REVIEWER_HANDLE_PATTERN.fullmatch(text) else None
115
+
116
+
117
+ def _normalise_author_name(value: Any) -> str | None:
118
+ text = public_safe_compact_text(value, limit=80)
119
+ return " ".join(text.lower().split()) if text else None
120
+
121
+
122
+ def _normalise_resolved_identities(
123
+ values: Mapping[str, Any] | None,
124
+ ) -> dict[str, str]:
125
+ resolved: dict[str, str] = {}
126
+ for raw_name, raw_handle in (values or {}).items():
127
+ name = _normalise_author_name(raw_name)
128
+ handle = _normalise_reviewer_handle(raw_handle)
129
+ if not name or not handle:
130
+ raise ValueError(
131
+ "resolved reviewer identities require a public-safe git display "
132
+ "name and normalized GitHub handle"
133
+ )
134
+ resolved[name] = handle
135
+ if len(resolved) > 50:
136
+ raise ValueError("at most 50 reviewer identities may be resolved per request")
137
+ return resolved
138
+
139
+
140
+ def _normalise_source_reference(value: Any, *, field: str) -> str:
141
+ reference = public_safe_compact_text(value, limit=300)
142
+ if not reference:
143
+ raise ValueError(f"{field} must be compact and public-safe")
144
+ if "://" in reference:
145
+ parsed = urlsplit(reference)
146
+ if (
147
+ parsed.scheme != "https"
148
+ or not parsed.netloc
149
+ or parsed.username
150
+ or parsed.password
151
+ or parsed.query
152
+ ):
153
+ raise ValueError(
154
+ f"{field} URL must be public https without user info or query"
155
+ )
156
+ hostname = (parsed.hostname or "").lower()
157
+ if hostname == "localhost" or hostname.endswith((".localhost", ".local")):
158
+ raise ValueError(f"{field} URL must not target a local host")
159
+ try:
160
+ address = ipaddress.ip_address(hostname)
161
+ except ValueError:
162
+ address = None
163
+ if address and (
164
+ address.is_private
165
+ or address.is_loopback
166
+ or address.is_link_local
167
+ or address.is_reserved
168
+ ):
169
+ raise ValueError(f"{field} URL must target a public host")
170
+ return reference
171
+ if reference.startswith(("/", "~")) or re.match(r"^[A-Za-z]:[\\/]", reference):
172
+ raise ValueError(f"{field} must not be an absolute or home-relative path")
173
+ path = PurePosixPath(reference.replace("\\", "/"))
174
+ if path.as_posix() == "." or ".." in path.parts:
175
+ raise ValueError(f"{field} must be a repo-relative public reference")
176
+ return path.as_posix()
177
+
178
+
179
+ def _normalise_source_handle_list(value: Any, *, field: str) -> list[str]:
180
+ if value is None:
181
+ return []
182
+ if not isinstance(value, Sequence) or isinstance(value, (str, bytes)):
183
+ raise ValueError(f"{field} must be a list of GitHub handles")
184
+ handles: list[str] = []
185
+ for raw in value:
186
+ handle = _normalise_reviewer_handle(raw)
187
+ if not handle:
188
+ raise ValueError(f"{field} must contain normalized GitHub handles")
189
+ if handle not in handles:
190
+ handles.append(handle)
191
+ if len(handles) > 10:
192
+ raise ValueError(f"{field} accepts at most 10 reviewer handles")
193
+ return handles
194
+
195
+
196
+ def _normalise_source_observed_at(value: Any, *, field: str) -> str:
197
+ observed_at = public_safe_compact_text(value, limit=50)
198
+ if not observed_at:
199
+ raise ValueError(f"{field} must be a timezone-aware ISO timestamp")
200
+ try:
201
+ parsed = datetime.fromisoformat(observed_at.replace("Z", "+00:00"))
202
+ except ValueError as exc:
203
+ raise ValueError(f"{field} must be a timezone-aware ISO timestamp") from exc
204
+ if parsed.tzinfo is None:
205
+ raise ValueError(f"{field} must be a timezone-aware ISO timestamp")
206
+ return observed_at
207
+
208
+
209
+ def _normalise_source_route_pattern(value: Any, *, field: str) -> str:
210
+ pattern = public_safe_compact_text(value, limit=220).replace("\\", "/")
211
+ while pattern.startswith("./"):
212
+ pattern = pattern[2:]
213
+ if not pattern or pattern.startswith(("/", "~")):
214
+ raise ValueError(f"{field} must be a repo-relative path pattern")
215
+ path = PurePosixPath(pattern)
216
+ if ".." in path.parts:
217
+ raise ValueError(f"{field} must not traverse outside the repository")
218
+ return pattern.rstrip("/")
219
+
220
+
221
+ def _normalise_reviewer_source_route(raw: Any, *, index: int) -> dict[str, Any]:
222
+ if not isinstance(raw, Mapping):
223
+ raise ValueError(f"reviewer source routes[{index}] must be an object")
224
+ unknown = sorted(set(raw) - REVIEWER_SOURCE_ROUTE_FIELDS)
225
+ if unknown:
226
+ raise ValueError(
227
+ f"reviewer source routes[{index}] has unsupported fields: {unknown}"
228
+ )
229
+ route_id = public_safe_compact_text(raw.get("route_id"), limit=120)
230
+ if not route_id or not REVIEWER_SOURCE_ID_PATTERN.fullmatch(route_id):
231
+ raise ValueError(f"reviewer source routes[{index}].route_id has invalid shape")
232
+ match_kind = str(raw.get("match_kind") or "").strip()
233
+ if match_kind not in REVIEWER_SOURCE_MATCH_KINDS:
234
+ raise ValueError(
235
+ f"reviewer source routes[{index}].match_kind must be one of "
236
+ f"{sorted(REVIEWER_SOURCE_MATCH_KINDS)}"
237
+ )
238
+ pattern = None
239
+ if match_kind != "repository_fallback":
240
+ pattern = _normalise_source_route_pattern(
241
+ raw.get("pattern"), field=f"reviewer source routes[{index}].pattern"
242
+ )
243
+ elif raw.get("pattern") not in {None, ""}:
244
+ raise ValueError("repository_fallback routes must not declare a pattern")
245
+ primary = _normalise_source_handle_list(
246
+ raw.get("primary_reviewers"),
247
+ field=f"reviewer source routes[{index}].primary_reviewers",
248
+ )
249
+ fallback = _normalise_source_handle_list(
250
+ raw.get("fallback_reviewers"),
251
+ field=f"reviewer source routes[{index}].fallback_reviewers",
252
+ )
253
+ if not primary and not fallback:
254
+ raise ValueError(
255
+ "reviewer source routes require a primary or fallback reviewer"
256
+ )
257
+ return {
258
+ "schema_version": "issue_fix_reviewer_source_route_v0",
259
+ "route_id": route_id,
260
+ "match_kind": match_kind,
261
+ "pattern": pattern,
262
+ "primary_reviewers": primary,
263
+ "fallback_reviewers": fallback,
264
+ }
265
+
266
+
267
+ def _normalise_reviewer_sources(
268
+ value: Mapping[str, Any] | None,
269
+ ) -> list[dict[str, Any]]:
270
+ if value is None:
271
+ return []
272
+ if not isinstance(value, Mapping):
273
+ raise ValueError("reviewer sources input must be an object")
274
+ if value.get("schema_version") != ISSUE_FIX_REVIEWER_SOURCES_INPUT_SCHEMA_VERSION:
275
+ raise ValueError(
276
+ "reviewer sources schema_version must be "
277
+ "issue_fix_reviewer_sources_input_v0"
278
+ )
279
+ unknown = sorted(set(value) - REVIEWER_SOURCE_INPUT_FIELDS)
280
+ if unknown:
281
+ raise ValueError(f"reviewer sources input has unsupported fields: {unknown}")
282
+ raw_sources = value.get("sources") or []
283
+ if not isinstance(raw_sources, Sequence) or isinstance(raw_sources, (str, bytes)):
284
+ raise ValueError("reviewer sources must be a list")
285
+ if len(raw_sources) > MAX_REVIEWER_SOURCES:
286
+ raise ValueError(
287
+ f"reviewer sources accepts at most {MAX_REVIEWER_SOURCES} sources"
288
+ )
289
+
290
+ sources: list[dict[str, Any]] = []
291
+ total_routes = 0
292
+ for index, raw in enumerate(raw_sources):
293
+ if not isinstance(raw, Mapping):
294
+ raise ValueError(f"reviewer sources[{index}] must be an object")
295
+ unknown = sorted(set(raw) - REVIEWER_SOURCE_FIELDS)
296
+ if unknown:
297
+ raise ValueError(
298
+ f"reviewer sources[{index}] has unsupported fields: {unknown}"
299
+ )
300
+ source_id = public_safe_compact_text(raw.get("source_id"), limit=120)
301
+ if not source_id or not REVIEWER_SOURCE_ID_PATTERN.fullmatch(source_id):
302
+ raise ValueError(f"reviewer sources[{index}].source_id has invalid shape")
303
+ source_kind = str(raw.get("source_kind") or "").strip()
304
+ if source_kind not in REVIEWER_SOURCE_KINDS:
305
+ raise ValueError(
306
+ f"reviewer sources[{index}].source_kind must be one of "
307
+ f"{sorted(REVIEWER_SOURCE_KINDS)}"
308
+ )
309
+ trust = str(raw.get("trust") or "").strip()
310
+ if trust not in REVIEWER_SOURCE_TRUST_LEVELS:
311
+ raise ValueError(
312
+ f"reviewer sources[{index}].trust must be one of "
313
+ f"{sorted(REVIEWER_SOURCE_TRUST_LEVELS)}"
314
+ )
315
+ freshness = str(raw.get("freshness") or "unknown").strip()
316
+ if freshness not in REVIEWER_SOURCE_FRESHNESS_STATES:
317
+ raise ValueError(
318
+ f"reviewer sources[{index}].freshness must be one of "
319
+ f"{sorted(REVIEWER_SOURCE_FRESHNESS_STATES)}"
320
+ )
321
+ raw_routes = raw.get("routes") or []
322
+ if not isinstance(raw_routes, Sequence) or isinstance(raw_routes, (str, bytes)):
323
+ raise ValueError(f"reviewer sources[{index}].routes must be a list")
324
+ if not raw_routes:
325
+ raise ValueError(f"reviewer sources[{index}].routes must not be empty")
326
+ total_routes += len(raw_routes)
327
+ if total_routes > MAX_REVIEWER_SOURCE_ROUTES:
328
+ raise ValueError(
329
+ f"reviewer sources accepts at most {MAX_REVIEWER_SOURCE_ROUTES} routes"
330
+ )
331
+ routes = [
332
+ _normalise_reviewer_source_route(route, index=route_index)
333
+ for route_index, route in enumerate(raw_routes)
334
+ ]
335
+ route_ids = [route["route_id"] for route in routes]
336
+ if len(route_ids) != len(set(route_ids)):
337
+ raise ValueError(
338
+ f"reviewer sources[{index}] route_id values must be unique"
339
+ )
340
+ sources.append(
341
+ {
342
+ "schema_version": "issue_fix_reviewer_source_v0",
343
+ "source_id": source_id,
344
+ "source_kind": source_kind,
345
+ "reference": _normalise_source_reference(
346
+ raw.get("reference"),
347
+ field=f"reviewer sources[{index}].reference",
348
+ ),
349
+ "trust": trust,
350
+ "freshness": freshness,
351
+ "observed_at": _normalise_source_observed_at(
352
+ raw.get("observed_at"),
353
+ field=f"reviewer sources[{index}].observed_at",
354
+ ),
355
+ "routes": routes,
356
+ "raw_content_captured": False,
357
+ }
358
+ )
359
+ source_ids = [source["source_id"] for source in sources]
360
+ if len(source_ids) != len(set(source_ids)):
361
+ raise ValueError("reviewer source_id values must be unique")
362
+ return sources
363
+
364
+
365
+ def _codeowners_tokens(line: str) -> tuple[str, list[str]] | None:
366
+ stripped = line.strip()
367
+ if not stripped or stripped.startswith("#"):
368
+ return None
369
+ parts = stripped.split()
370
+ if len(parts) < 2:
371
+ return None
372
+ owners = [
373
+ handle
374
+ for handle in (_normalise_reviewer_handle(item) for item in parts[1:])
375
+ if handle
376
+ ]
377
+ return (parts[0], owners) if owners else None
378
+
379
+
380
+ def _codeowners_pattern_matches(pattern: str, path: str) -> bool:
381
+ pattern = pattern.strip()
382
+ if not pattern or pattern.startswith("!"):
383
+ return False
384
+ anchored = pattern.startswith("/")
385
+ pattern = pattern.lstrip("/")
386
+ if pattern.endswith("/"):
387
+ prefix = pattern.rstrip("/")
388
+ return path == prefix or path.startswith(f"{prefix}/")
389
+ if "/" not in pattern:
390
+ return any(fnmatch.fnmatchcase(part, pattern) for part in path.split("/"))
391
+ if anchored:
392
+ return fnmatch.fnmatchcase(path, pattern)
393
+ return fnmatch.fnmatchcase(path, pattern) or PurePosixPath(path).match(pattern)
394
+
395
+
396
+ def _load_codeowners(repo_path: Path) -> tuple[str | None, list[tuple[str, list[str]]]]:
397
+ for relative in CODEOWNERS_LOCATIONS:
398
+ candidate = repo_path / relative
399
+ if not candidate.is_file():
400
+ continue
401
+ rows: list[tuple[str, list[str]]] = []
402
+ for line in candidate.read_text(encoding="utf-8", errors="replace").splitlines():
403
+ parsed = _codeowners_tokens(line)
404
+ if parsed is not None:
405
+ rows.append(parsed)
406
+ return relative, rows
407
+ return None, []
408
+
409
+
410
+ def _owners_for_path(
411
+ rules: Sequence[tuple[str, list[str]]],
412
+ path: str,
413
+ ) -> tuple[str | None, list[str]]:
414
+ matched_pattern: str | None = None
415
+ owners: list[str] = []
416
+ for pattern, rule_owners in rules:
417
+ if _codeowners_pattern_matches(pattern, path):
418
+ matched_pattern = pattern
419
+ owners = list(rule_owners)
420
+ return matched_pattern, owners
421
+
422
+
423
+ def _github_handle_from_email(value: str) -> str | None:
424
+ match = GITHUB_NOREPLY_PATTERN.fullmatch(value.strip())
425
+ return f"@{match.group('login').lower()}" if match else None
426
+
427
+
428
+ def _looks_like_automated_history_identity(
429
+ *,
430
+ display_name: str,
431
+ handle: str | None,
432
+ ) -> bool:
433
+ values = [display_name, (handle or "").lstrip("@")]
434
+ return any(AUTOMATED_HISTORY_IDENTITY_PATTERN.search(value) for value in values)
435
+
436
+
437
+ def _candidate_key(*, handle: str | None, display_name: str) -> str:
438
+ if handle:
439
+ return f"handle:{handle}"
440
+ normalised_name = " ".join(display_name.lower().split())
441
+ digest = hashlib.sha256(normalised_name.encode("utf-8")).hexdigest()[:12]
442
+ return f"git-author:{digest}"
443
+
444
+
445
+ def _candidate_id(key: str) -> str:
446
+ return "reviewer_" + hashlib.sha256(key.encode("utf-8")).hexdigest()[:12]
447
+
448
+
449
+ def _new_candidate(
450
+ *,
451
+ key: str,
452
+ handle: str | None,
453
+ display_name: str,
454
+ ) -> dict[str, Any]:
455
+ return {
456
+ "candidate_id": _candidate_id(key),
457
+ "reviewer_handle": handle,
458
+ "display_name": display_name,
459
+ "requestable": bool(handle),
460
+ "score": 0,
461
+ "source_kinds": [],
462
+ "reason_codes": [],
463
+ "matched_files": [],
464
+ "codeowners_patterns": [],
465
+ "source_refs": [],
466
+ "reviewer_source_evidence": [],
467
+ "history_commit_count": 0,
468
+ "most_recent_history_rank": None,
469
+ }
470
+
471
+
472
+ def _append_unique(row: dict[str, Any], field: str, value: str, *, limit: int = 20) -> None:
473
+ values = row[field]
474
+ if value not in values and len(values) < limit:
475
+ values.append(value)
476
+
477
+
478
+ def _reviewer_source_route_matches(route: Mapping[str, Any], path: str) -> bool:
479
+ match_kind = route.get("match_kind")
480
+ pattern = str(route.get("pattern") or "")
481
+ if match_kind == "path_prefix":
482
+ return path == pattern or path.startswith(f"{pattern}/")
483
+ if match_kind == "path_glob":
484
+ return fnmatch.fnmatchcase(path, pattern) or PurePosixPath(path).match(pattern)
485
+ return False
486
+
487
+
488
+ def _reviewer_source_route_specificity(route: Mapping[str, Any]) -> int:
489
+ pattern = str(route.get("pattern") or "")
490
+ return len(re.sub(r"[*?\[\]]", "", pattern))
491
+
492
+
493
+ def _reviewer_source_routes_for_path(
494
+ source: Mapping[str, Any], path: str
495
+ ) -> list[Mapping[str, Any]]:
496
+ routes = source.get("routes")
497
+ routes = routes if isinstance(routes, list) else []
498
+ scoped = [
499
+ route
500
+ for route in routes
501
+ if isinstance(route, Mapping) and _reviewer_source_route_matches(route, path)
502
+ ]
503
+ if scoped:
504
+ max_specificity = max(
505
+ _reviewer_source_route_specificity(route) for route in scoped
506
+ )
507
+ return [
508
+ route
509
+ for route in scoped
510
+ if _reviewer_source_route_specificity(route) == max_specificity
511
+ ]
512
+ return [
513
+ route
514
+ for route in routes
515
+ if isinstance(route, Mapping)
516
+ and route.get("match_kind") == "repository_fallback"
517
+ ]
518
+
519
+
520
+ def _reviewer_source_score(*, role: str, trust: str, freshness: str) -> int:
521
+ base = 950 if role == "primary" else 250
522
+ trust_factor = {
523
+ "authoritative": 1.0,
524
+ "verified": 0.9,
525
+ "advisory": 0.5,
526
+ }[trust]
527
+ freshness_factor = {"current": 1.0, "unknown": 0.75, "stale": 0.4}[freshness]
528
+ return max(1, int(base * trust_factor * freshness_factor))
529
+
530
+
531
+ def _append_reviewer_source_evidence(
532
+ row: dict[str, Any], evidence: Mapping[str, Any]
533
+ ) -> None:
534
+ values = row["reviewer_source_evidence"]
535
+ identity = (
536
+ evidence.get("source_id"),
537
+ evidence.get("route_id"),
538
+ evidence.get("role"),
539
+ evidence.get("matched_file"),
540
+ )
541
+ if any(
542
+ (
543
+ item.get("source_id"),
544
+ item.get("route_id"),
545
+ item.get("role"),
546
+ item.get("matched_file"),
547
+ )
548
+ == identity
549
+ for item in values
550
+ if isinstance(item, Mapping)
551
+ ):
552
+ return
553
+ if len(values) < 30:
554
+ values.append(dict(evidence))
555
+
556
+
557
+ def _apply_reviewer_sources_for_path(
558
+ *,
559
+ candidates: dict[str, dict[str, Any]],
560
+ sources: Sequence[Mapping[str, Any]],
561
+ changed_file: str,
562
+ excluded: set[str],
563
+ ) -> None:
564
+ for source in sources:
565
+ reference = str(source["reference"])
566
+ trust = str(source["trust"])
567
+ freshness = str(source["freshness"])
568
+ observed_at = str(source["observed_at"])
569
+ for route in _reviewer_source_routes_for_path(source, changed_file):
570
+ match_kind = str(route["match_kind"])
571
+ for role, field in (
572
+ ("primary", "primary_reviewers"),
573
+ ("fallback", "fallback_reviewers"),
574
+ ):
575
+ for handle in route.get(field) or []:
576
+ if handle in excluded:
577
+ continue
578
+ key = _candidate_key(handle=handle, display_name=handle)
579
+ row = candidates.setdefault(
580
+ key,
581
+ _new_candidate(key=key, handle=handle, display_name=handle),
582
+ )
583
+ row["score"] += _reviewer_source_score(
584
+ role=role,
585
+ trust=trust,
586
+ freshness=freshness,
587
+ )
588
+ _append_unique(row, "source_kinds", "repository_maintainer_map")
589
+ _append_unique(row, "source_refs", reference)
590
+ _append_unique(row, "matched_files", changed_file)
591
+ _append_unique(
592
+ row,
593
+ "reason_codes",
594
+ (
595
+ "repository_declared_primary_contact"
596
+ if role == "primary"
597
+ else "repository_declared_fallback_contact"
598
+ ),
599
+ )
600
+ if match_kind == "repository_fallback":
601
+ _append_unique(
602
+ row,
603
+ "reason_codes",
604
+ "repository_declared_cross_module_fallback",
605
+ )
606
+ _append_reviewer_source_evidence(
607
+ row,
608
+ {
609
+ "schema_version": "issue_fix_reviewer_source_evidence_v0",
610
+ "source_id": source["source_id"],
611
+ "source_kind": source["source_kind"],
612
+ "reference": reference,
613
+ "trust": trust,
614
+ "freshness": freshness,
615
+ "observed_at": observed_at,
616
+ "route_id": route["route_id"],
617
+ "match_kind": match_kind,
618
+ "pattern": route.get("pattern"),
619
+ "role": role,
620
+ "matched_file": changed_file,
621
+ "raw_content_captured": False,
622
+ },
623
+ )
624
+
625
+
626
+ def _derive_changed_files(repo_path: Path, base_ref: str) -> list[str]:
627
+ output = _run_git(repo_path, ["diff", "--name-only", f"{base_ref}...HEAD"])
628
+ return _normalise_changed_files(output.splitlines())
629
+
630
+
631
+ def _collect_history(
632
+ repo_path: Path,
633
+ path: str,
634
+ *,
635
+ revision: str,
636
+ history_limit: int,
637
+ ) -> list[tuple[str, str]]:
638
+ output = _run_git(
639
+ repo_path,
640
+ [
641
+ "log",
642
+ f"--max-count={history_limit}",
643
+ "--format=%aN%x1f%aE",
644
+ revision,
645
+ "--",
646
+ path,
647
+ ],
648
+ )
649
+ rows: list[tuple[str, str]] = []
650
+ for line in output.splitlines():
651
+ name, separator, email = line.partition("\x1f")
652
+ if not separator:
653
+ continue
654
+ safe_name = public_safe_compact_text(name, limit=80)
655
+ if safe_name:
656
+ rows.append((safe_name, email.strip()))
657
+ return rows
658
+
659
+
660
+ def _confidence(candidate: Mapping[str, Any]) -> str:
661
+ sources = set(candidate.get("source_kinds") or [])
662
+ coverage = len(candidate.get("matched_files") or [])
663
+ if "git_history" in sources and sources.intersection(
664
+ {"codeowners", "repository_maintainer_map"}
665
+ ):
666
+ return "high"
667
+ if (
668
+ "codeowners" in sources
669
+ or "repository_declared_primary_contact"
670
+ in set(candidate.get("reason_codes") or [])
671
+ or coverage >= 2
672
+ ):
673
+ return "medium"
674
+ return "low"
675
+
676
+
677
+ def _recommendation_status(candidates: Sequence[Mapping[str, Any]]) -> str:
678
+ if any(candidate.get("requestable") for candidate in candidates):
679
+ return "candidates_ready"
680
+ if candidates:
681
+ return "identity_resolution_required"
682
+ return "no_candidates"
683
+
684
+
685
+ def build_issue_fix_reviewer_recommendation_packet(
686
+ *,
687
+ repo_path: str | Path,
688
+ repo: str = "approved_local_repo",
689
+ changed_files: Sequence[str] = (),
690
+ base_ref: str = "origin/main",
691
+ history_limit: int = 40,
692
+ max_candidates: int = 5,
693
+ exclude_reviewers: Sequence[str] = (),
694
+ exclude_author_names: Sequence[str] = (),
695
+ resolved_identities: Mapping[str, Any] | None = None,
696
+ reviewer_sources_input: Mapping[str, Any] | None = None,
697
+ execute: bool = False,
698
+ generated_at: str | None = "2026-07-10T00:00:00Z",
699
+ ) -> dict[str, Any]:
700
+ """Recommend reviewers from caller-approved repository ownership evidence."""
701
+
702
+ safe_repo = public_safe_compact_text(repo, limit=120)
703
+ if not safe_repo:
704
+ raise ValueError("repo must be a compact public-safe label")
705
+ safe_base_ref = _normalise_base_ref(
706
+ public_safe_compact_text(base_ref, limit=120)
707
+ )
708
+ history_limit = min(max(int(history_limit), 1), 200)
709
+ max_candidates = min(max(int(max_candidates), 1), 20)
710
+ explicit_files = _normalise_changed_files(changed_files)
711
+ excluded = {
712
+ handle
713
+ for handle in (_normalise_reviewer_handle(item) for item in exclude_reviewers)
714
+ if handle
715
+ }
716
+ excluded_author_names = {
717
+ name
718
+ for name in (_normalise_author_name(item) for item in exclude_author_names)
719
+ if name
720
+ }
721
+ excluded_author_names.update(handle.lstrip("@").lower() for handle in excluded)
722
+ resolved = _normalise_resolved_identities(resolved_identities)
723
+ reviewer_sources = _normalise_reviewer_sources(reviewer_sources_input)
724
+ reviewer_source_refs = list(
725
+ dict.fromkeys(str(source["reference"]) for source in reviewer_sources)
726
+ )
727
+
728
+ packet: dict[str, Any] = {
729
+ "ok": True,
730
+ "schema_version": ISSUE_FIX_REVIEWER_RECOMMENDATION_SCHEMA_VERSION,
731
+ "mode": "issue-fix-reviewer-recommendation",
732
+ "generated_at": generated_at,
733
+ "repo": safe_repo,
734
+ "execute": execute,
735
+ "recommendation_status": "preview_only",
736
+ "changed_files": explicit_files,
737
+ "changed_file_count": len(explicit_files),
738
+ "candidates": [],
739
+ "excluded_reviewer_handles": sorted(excluded),
740
+ "excluded_author_name_count": len(excluded_author_names),
741
+ "resolved_identity_count": len(resolved),
742
+ "reviewer_source_count": len(reviewer_sources),
743
+ "reviewer_source_refs": reviewer_source_refs,
744
+ "evidence_summary": {
745
+ "schema_version": "issue_fix_reviewer_evidence_summary_v0",
746
+ "authority_order": [
747
+ "repository_codeowners",
748
+ "repository_declared_primary_contact",
749
+ "changed_path_git_history",
750
+ "changed_module_git_history_fallback",
751
+ "repository_declared_fallback_contact",
752
+ ],
753
+ "codeowners_source": None,
754
+ "codeowners_pattern_support": "common_subset",
755
+ "history_limit_per_file": history_limit,
756
+ "history_revision": safe_base_ref,
757
+ "module_history_fallback": True,
758
+ "reviewer_source_count": len(reviewer_sources),
759
+ "reviewer_source_refs": reviewer_source_refs,
760
+ "automated_history_identities_excluded": True,
761
+ "raw_reviewer_source_input_captured": False,
762
+ "raw_codeowners_captured": False,
763
+ "raw_git_output_captured": False,
764
+ "commit_emails_captured": False,
765
+ },
766
+ "policy": {
767
+ "schema_version": "issue_fix_reviewer_policy_v0",
768
+ "recommendation_is_assignment": False,
769
+ "automatic_review_request_allowed": True,
770
+ "automatic_request_policy": "request_top_requestable_when_authorized",
771
+ "external_review_request_authority_required": True,
772
+ "default_max_review_requests": 1,
773
+ "repository_policy_still_applies": True,
774
+ "current_author_should_be_excluded_by_request_command": True,
775
+ "identity_resolution_required_for_non_handle_candidates": True,
776
+ "next_action": (
777
+ "After PR creation, run reviewer-request under external-review-request "
778
+ "authority; it excludes the live PR author and requests the top "
779
+ "requestable candidate."
780
+ ),
781
+ },
782
+ "external_reads_performed": False,
783
+ "external_writes_performed": False,
784
+ "review_request_performed": False,
785
+ "private_repo_state_read": False,
786
+ "local_paths_captured": False,
787
+ "raw_git_output_captured": False,
788
+ "commit_emails_captured": False,
789
+ "raw_reviewer_source_input_captured": False,
790
+ }
791
+ if not execute:
792
+ packet["next_safe_action"] = (
793
+ "Rerun with --execute only for the caller-approved local repository."
794
+ )
795
+ packet["validation"] = validate_issue_fix_reviewer_recommendation_packet(packet)
796
+ return packet
797
+
798
+ path = Path(repo_path).expanduser()
799
+ if not path.is_dir():
800
+ raise ValueError("repo_path must be an existing caller-approved directory")
801
+ _run_git(path, ["rev-parse", "--is-inside-work-tree"])
802
+ files = explicit_files or _derive_changed_files(path, safe_base_ref)
803
+ if not files:
804
+ raise ValueError("reviewer recommendation requires at least one changed file")
805
+
806
+ codeowners_source, codeowners_rules = _load_codeowners(path)
807
+ candidates: dict[str, dict[str, Any]] = {}
808
+ for changed_file in files:
809
+ pattern, owners = _owners_for_path(codeowners_rules, changed_file)
810
+ for handle in owners:
811
+ if handle in excluded:
812
+ continue
813
+ key = _candidate_key(handle=handle, display_name=handle)
814
+ row = candidates.setdefault(
815
+ key,
816
+ _new_candidate(key=key, handle=handle, display_name=handle),
817
+ )
818
+ row["score"] += 1000
819
+ _append_unique(row, "source_kinds", "codeowners")
820
+ _append_unique(row, "reason_codes", "repository_codeowners_match")
821
+ _append_unique(row, "matched_files", changed_file)
822
+ if pattern:
823
+ _append_unique(row, "codeowners_patterns", pattern)
824
+
825
+ _apply_reviewer_sources_for_path(
826
+ candidates=candidates,
827
+ sources=reviewer_sources,
828
+ changed_file=changed_file,
829
+ excluded=excluded,
830
+ )
831
+
832
+ history_rows = _collect_history(
833
+ path,
834
+ changed_file,
835
+ revision=safe_base_ref,
836
+ history_limit=history_limit,
837
+ )
838
+ history_reason = "changed_path_commit_history"
839
+ usable_history_rows = [
840
+ row
841
+ for row in history_rows
842
+ if (
843
+ _github_handle_from_email(row[1])
844
+ or resolved.get(_normalise_author_name(row[0]) or "")
845
+ or ""
846
+ )
847
+ not in excluded
848
+ and _normalise_author_name(row[0]) not in excluded_author_names
849
+ and not _looks_like_automated_history_identity(
850
+ display_name=row[0],
851
+ handle=(
852
+ _github_handle_from_email(row[1])
853
+ or resolved.get(_normalise_author_name(row[0]) or "")
854
+ ),
855
+ )
856
+ ]
857
+ if not usable_history_rows:
858
+ parent = PurePosixPath(changed_file).parent.as_posix()
859
+ if parent not in {"", "."}:
860
+ history_rows = _collect_history(
861
+ path,
862
+ parent,
863
+ revision=safe_base_ref,
864
+ history_limit=history_limit,
865
+ )
866
+ history_reason = "changed_module_commit_history"
867
+ for rank, (name, email) in enumerate(history_rows, start=1):
868
+ resolved_handle = resolved.get(_normalise_author_name(name) or "")
869
+ handle = _github_handle_from_email(email) or resolved_handle
870
+ if handle and handle in excluded:
871
+ continue
872
+ if _normalise_author_name(name) in excluded_author_names:
873
+ continue
874
+ if _looks_like_automated_history_identity(
875
+ display_name=name,
876
+ handle=handle,
877
+ ):
878
+ continue
879
+ key = _candidate_key(handle=handle, display_name=name)
880
+ row = candidates.setdefault(
881
+ key,
882
+ _new_candidate(key=key, handle=handle, display_name=name),
883
+ )
884
+ row["score"] += max(1, history_limit - rank + 1)
885
+ row["history_commit_count"] += 1
886
+ recent_rank = row["most_recent_history_rank"]
887
+ if recent_rank is None or rank < recent_rank:
888
+ row["most_recent_history_rank"] = rank
889
+ _append_unique(row, "source_kinds", "git_history")
890
+ if resolved_handle:
891
+ _append_unique(row, "source_kinds", "verified_identity_mapping")
892
+ _append_unique(row, "reason_codes", "caller_verified_github_identity")
893
+ _append_unique(row, "reason_codes", history_reason)
894
+ if rank <= 5:
895
+ _append_unique(row, "reason_codes", "recent_changed_path_contributor")
896
+ if handle is None:
897
+ _append_unique(row, "reason_codes", "github_identity_resolution_required")
898
+ _append_unique(row, "matched_files", changed_file)
899
+
900
+ ranked = sorted(
901
+ candidates.values(),
902
+ key=lambda row: (
903
+ -int(row["score"]),
904
+ -len(row["matched_files"]),
905
+ str(row.get("reviewer_handle") or row.get("display_name") or ""),
906
+ ),
907
+ )[:max_candidates]
908
+ for rank, row in enumerate(ranked, start=1):
909
+ row["rank"] = rank
910
+ row["confidence"] = _confidence(row)
911
+ row["path_coverage_count"] = len(row["matched_files"])
912
+
913
+ packet.update(
914
+ {
915
+ "recommendation_status": _recommendation_status(ranked),
916
+ "changed_files": files,
917
+ "changed_file_count": len(files),
918
+ "candidates": ranked,
919
+ "private_repo_state_read": True,
920
+ "next_safe_action": (
921
+ "Run reviewer-request after PR creation when external-review-request "
922
+ "authority is active."
923
+ ),
924
+ }
925
+ )
926
+ packet["evidence_summary"]["codeowners_source"] = codeowners_source
927
+ validation = validate_issue_fix_reviewer_recommendation_packet(packet)
928
+ packet["ok"] = validation["ok"]
929
+ packet["validation"] = validation
930
+ return packet
931
+
932
+
933
+ def validate_issue_fix_reviewer_recommendation_packet(
934
+ packet: Mapping[str, Any],
935
+ ) -> dict[str, Any]:
936
+ errors: list[str] = []
937
+ if packet.get("schema_version") != ISSUE_FIX_REVIEWER_RECOMMENDATION_SCHEMA_VERSION:
938
+ errors.append("schema_version must be issue_fix_reviewer_recommendation_v0")
939
+ for key in (
940
+ "external_reads_performed",
941
+ "external_writes_performed",
942
+ "review_request_performed",
943
+ "local_paths_captured",
944
+ "raw_git_output_captured",
945
+ "commit_emails_captured",
946
+ "raw_reviewer_source_input_captured",
947
+ ):
948
+ if packet.get(key) is not False:
949
+ errors.append(f"{key} must be false")
950
+ execute = packet.get("execute") is True
951
+ if packet.get("private_repo_state_read") is not execute:
952
+ errors.append("private_repo_state_read must match execute")
953
+ changed_files = packet.get("changed_files")
954
+ if not isinstance(changed_files, list):
955
+ errors.append("changed_files must be a list")
956
+ changed_files = []
957
+ for path in changed_files:
958
+ try:
959
+ _normalise_changed_file(path)
960
+ except ValueError:
961
+ errors.append("changed_files must contain only repo-relative paths")
962
+ break
963
+ reviewer_source_refs = packet.get("reviewer_source_refs")
964
+ if not isinstance(reviewer_source_refs, list):
965
+ errors.append("reviewer_source_refs must be a list")
966
+ reviewer_source_refs = []
967
+ for reference in reviewer_source_refs:
968
+ try:
969
+ _normalise_source_reference(reference, field="reviewer_source_refs")
970
+ except ValueError:
971
+ errors.append("reviewer_source_refs must contain public-safe references")
972
+ break
973
+ if not isinstance(packet.get("reviewer_source_count"), int):
974
+ errors.append("reviewer_source_count must be an integer")
975
+ candidates = packet.get("candidates")
976
+ if not isinstance(candidates, list):
977
+ errors.append("candidates must be a list")
978
+ candidates = []
979
+ for candidate in candidates:
980
+ if not isinstance(candidate, Mapping):
981
+ errors.append("reviewer candidate must be an object")
982
+ continue
983
+ handle = candidate.get("reviewer_handle")
984
+ if handle is not None and _normalise_reviewer_handle(handle) != handle:
985
+ errors.append("reviewer_handle must be a normalized GitHub handle")
986
+ if candidate.get("requestable") is not bool(handle):
987
+ errors.append("requestable must reflect reviewer_handle availability")
988
+ source_refs = candidate.get("source_refs")
989
+ if not isinstance(source_refs, list):
990
+ errors.append("candidate source_refs must be a list")
991
+ evidence = candidate.get("reviewer_source_evidence")
992
+ if not isinstance(evidence, list):
993
+ errors.append("candidate reviewer_source_evidence must be a list")
994
+ evidence = []
995
+ for item in evidence:
996
+ if not isinstance(item, Mapping):
997
+ errors.append("reviewer source evidence must be an object")
998
+ continue
999
+ if item.get("raw_content_captured") is not False:
1000
+ errors.append("reviewer source evidence must not capture raw content")
1001
+ try:
1002
+ _normalise_source_reference(
1003
+ item.get("reference"), field="reviewer source evidence reference"
1004
+ )
1005
+ _normalise_source_observed_at(
1006
+ item.get("observed_at"),
1007
+ field="reviewer source evidence observed_at",
1008
+ )
1009
+ except ValueError:
1010
+ errors.append("reviewer source evidence must have safe provenance")
1011
+ policy = packet.get("policy")
1012
+ if not isinstance(policy, Mapping):
1013
+ errors.append("policy is required")
1014
+ elif policy.get("automatic_review_request_allowed") is not True:
1015
+ errors.append("reviewer policy must allow the authority-gated default request")
1016
+ elif policy.get("external_review_request_authority_required") is not True:
1017
+ errors.append("automatic reviewer requests require external-write authority")
1018
+ return {
1019
+ "ok": not errors,
1020
+ "schema_version": "issue_fix_reviewer_recommendation_validation_v0",
1021
+ "errors": errors,
1022
+ }
1023
+
1024
+
1025
+ def render_issue_fix_reviewer_recommendation_markdown(
1026
+ payload: Mapping[str, Any],
1027
+ ) -> str:
1028
+ lines = [
1029
+ "# LoopX Issue-Fix Reviewer Recommendation",
1030
+ "",
1031
+ f"- ok: `{payload.get('ok')}`",
1032
+ f"- repo: `{payload.get('repo')}`",
1033
+ f"- execute: `{payload.get('execute')}`",
1034
+ f"- recommendation_status: `{payload.get('recommendation_status')}`",
1035
+ f"- changed_file_count: `{payload.get('changed_file_count')}`",
1036
+ f"- external_writes_performed: `{payload.get('external_writes_performed')}`",
1037
+ f"- review_request_performed: `{payload.get('review_request_performed')}`",
1038
+ "",
1039
+ "## Candidates",
1040
+ "",
1041
+ ]
1042
+ candidates = payload.get("candidates")
1043
+ if not isinstance(candidates, list) or not candidates:
1044
+ lines.append("- none")
1045
+ else:
1046
+ for candidate in candidates:
1047
+ if not isinstance(candidate, Mapping):
1048
+ continue
1049
+ identity = candidate.get("reviewer_handle") or candidate.get("display_name")
1050
+ lines.append(
1051
+ f"{candidate.get('rank')}. `{identity}` score=`{candidate.get('score')}` "
1052
+ f"confidence=`{candidate.get('confidence')}` "
1053
+ f"sources=`{','.join(candidate.get('source_kinds') or [])}` "
1054
+ f"refs=`{','.join(candidate.get('source_refs') or [])}`"
1055
+ )
1056
+ lines.extend(["", f"Next: {payload.get('next_safe_action')}"])
1057
+ return "\n".join(lines)