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,1468 @@
1
+ """One bounded LoopX Turn host execution with resumable local receipts."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import json
6
+ import os
7
+ import re
8
+ import subprocess
9
+ import tempfile
10
+ from collections.abc import Callable, Mapping, Sequence
11
+ from pathlib import Path
12
+ from typing import Any
13
+
14
+ from ...authority import validate_public_safe_text
15
+ from ...file_lock import LockAcquireTimeoutError, exclusive_file_lock
16
+ from ...runtime import validate_goal_id_path_segment
17
+ from ..effect_program import (
18
+ SettlementResult,
19
+ SettlementStepKind,
20
+ interpret_turn_journal,
21
+ interpret_turn_result_packet,
22
+ settlement_result_payload,
23
+ )
24
+ from ..goals.goal_vision import normalize_goal_vision_packet
25
+ from ..work_items.delivery_batch_scale import require_delivery_batch_scale
26
+ from ..work_items.delivery_outcome import require_delivery_outcome
27
+ from .driver import selected_turn_todo
28
+ from .settlement import (
29
+ completion_writeback_outcome,
30
+ execute_turn_driver_settlement,
31
+ execute_verified_turn_terminal_closeout,
32
+ terminal_closeout_requirement,
33
+ )
34
+ from .transaction import (
35
+ LOOPX_TURN_EXECUTION_SCHEMA_VERSION,
36
+ LOOPX_TURN_RESULT_SCHEMA_VERSION,
37
+ TRANSACTION_PHASES,
38
+ LoopXTurnResultKind,
39
+ build_loopx_turn_transaction_plan,
40
+ validate_loopx_turn_receipt,
41
+ )
42
+
43
+ LOOPX_TURN_HOST_REQUEST_SCHEMA_VERSION = "loopx_turn_host_request_v0"
44
+ LOOPX_TURN_JOURNAL_INSPECTION_SCHEMA_VERSION = "loopx_turn_journal_inspection_v0"
45
+ LOOPX_TURN_JOURNAL_SCHEMA_VERSION = "loopx_turn_journal_v0"
46
+ LOOPX_TURN_TASK_VALIDATION_SCHEMA_VERSION = "loopx_turn_task_validation_v0"
47
+ HOST_RESULT_MAX_BYTES = 12_000
48
+ HOST_ARG_MAX_COUNT = 32
49
+ HOST_ARG_MAX_CHARS = 1_024
50
+ HOST_AGENT_VISION_JSON_MAX_CHARS = 3_200
51
+ HOST_PATH_DELTA_MODES = {"", "unchanged", "material_replan"}
52
+ HOST_RESULT_TEXT_LIMITS = (
53
+ ("classification", 120),
54
+ ("recommended_action", 1_200),
55
+ ("next_action", 1_200),
56
+ ("vision_unchanged_reason", 240),
57
+ ("summary", 400),
58
+ )
59
+ TURN_KEY_RE = re.compile(r"^sha256:(?P<digest>[0-9a-f]{64})$")
60
+
61
+ MATERIAL_HOST_RESULT_KINDS = {
62
+ LoopXTurnResultKind.VALIDATED_PROGRESS,
63
+ LoopXTurnResultKind.VALIDATED_COMPLETION,
64
+ LoopXTurnResultKind.REPAIR_REQUIRED,
65
+ LoopXTurnResultKind.REPLAN_REQUIRED,
66
+ }
67
+ STOP_HOST_RESULT_KINDS = {
68
+ LoopXTurnResultKind.USER_ACTION_REQUIRED,
69
+ LoopXTurnResultKind.WAIT,
70
+ }
71
+ HOST_RESULT_FIELDS = {
72
+ "schema_version",
73
+ "turn_key",
74
+ "result_kind",
75
+ "completed_phases",
76
+ "classification",
77
+ "recommended_action",
78
+ "next_action",
79
+ "delivery_batch_scale",
80
+ "delivery_outcome",
81
+ "vision_unchanged_reason",
82
+ "path_delta_mode",
83
+ "agent_vision_json",
84
+ "summary",
85
+ }
86
+
87
+ Writeback = Callable[[dict[str, Any]], dict[str, Any]]
88
+ CompletionWriteback = Callable[[dict[str, Any]], dict[str, Any]]
89
+ CompletionIntent = Callable[[dict[str, Any]], dict[str, Any]]
90
+ TerminalCloseout = Callable[[dict[str, Any]], dict[str, Any]]
91
+ Spend = Callable[[], dict[str, Any]]
92
+ Scheduler = Callable[[dict[str, Any]], dict[str, Any]]
93
+ HostRunner = Callable[[Mapping[str, Any]], dict[str, Any]]
94
+ TaskValidator = Callable[
95
+ [Mapping[str, Any], Mapping[str, Any]],
96
+ Mapping[str, Any],
97
+ ]
98
+
99
+
100
+ class BuiltInHostError(RuntimeError):
101
+ """A public-safe built-in host failure classification."""
102
+
103
+ def __init__(self, reason: str) -> None:
104
+ super().__init__(reason)
105
+ self.reason = reason
106
+
107
+
108
+ def _normalize_argv(value: Sequence[str], *, label: str) -> list[str]:
109
+ argv = [str(item) for item in value]
110
+ if not argv:
111
+ raise ValueError(f"{label} command must contain at least one argv item")
112
+ if len(argv) > HOST_ARG_MAX_COUNT:
113
+ raise ValueError(f"{label} command exceeds {HOST_ARG_MAX_COUNT} argv items")
114
+ for item in argv:
115
+ if not item or "\x00" in item or len(item) > HOST_ARG_MAX_CHARS:
116
+ raise ValueError(
117
+ f"{label} command contains an empty, NUL, or oversized argv item"
118
+ )
119
+ return argv
120
+
121
+
122
+ def normalize_host_argv(value: Sequence[str]) -> list[str]:
123
+ return _normalize_argv(value, label="host")
124
+
125
+
126
+ def _normalize_task_validator_argv(value: Sequence[str]) -> list[str]:
127
+ return _normalize_argv(value, label="task validator")
128
+
129
+
130
+ def build_loopx_turn_host_request(plan: Mapping[str, Any]) -> dict[str, Any]:
131
+ transaction = plan.get("transaction") if isinstance(plan.get("transaction"), dict) else {}
132
+ turn_key = str(transaction.get("turn_key") or "")
133
+ if not TURN_KEY_RE.fullmatch(turn_key):
134
+ raise ValueError("LoopX Turn plan has no valid transaction turn_key")
135
+ route = plan.get("route") if isinstance(plan.get("route"), dict) else {}
136
+ if route.get("would_invoke_host") is not True:
137
+ raise ValueError("LoopX Turn route is not host executable")
138
+ request = {
139
+ "schema_version": LOOPX_TURN_HOST_REQUEST_SCHEMA_VERSION,
140
+ "turn_key": turn_key,
141
+ "route": route.get("kind"),
142
+ "session": plan.get("session"),
143
+ "turn_envelope": plan.get("turn_envelope"),
144
+ "result_contract": {
145
+ "schema_version": LOOPX_TURN_RESULT_SCHEMA_VERSION,
146
+ "completed_phases": list(TRANSACTION_PHASES[:2]),
147
+ "stdout": "one public-safe JSON object",
148
+ },
149
+ }
150
+ child_operations = plan.get("child_operations")
151
+ if isinstance(child_operations, list) and child_operations:
152
+ request["child_operations"] = child_operations
153
+ return request
154
+
155
+
156
+ def _bounded_public_text(
157
+ result: Mapping[str, Any],
158
+ field: str,
159
+ *,
160
+ limit: int,
161
+ required: bool,
162
+ errors: list[str],
163
+ ) -> str | None:
164
+ text = str(result.get(field) or "").strip()
165
+ if required and not text:
166
+ errors.append(f"{field} is required")
167
+ return None
168
+ if not text:
169
+ return None
170
+ if len(text) > limit:
171
+ errors.append(f"{field} exceeds {limit} characters")
172
+ return None
173
+ try:
174
+ validate_public_safe_text(f"host_result.{field}", text)
175
+ except ValueError as exc:
176
+ errors.append(str(exc))
177
+ return None
178
+ return text
179
+
180
+
181
+ def _normalize_host_path_delta(
182
+ plan: Mapping[str, Any],
183
+ result: Mapping[str, Any],
184
+ *,
185
+ unchanged_reason: str,
186
+ errors: list[str],
187
+ ) -> tuple[str, dict[str, Any] | None]:
188
+ path_delta_mode = str(result.get("path_delta_mode") or "").strip()
189
+ raw_agent_vision = result.get("agent_vision_json")
190
+ if raw_agent_vision is None:
191
+ agent_vision_json = ""
192
+ elif isinstance(raw_agent_vision, str):
193
+ agent_vision_json = raw_agent_vision.strip()
194
+ else:
195
+ agent_vision_json = ""
196
+ errors.append("agent_vision_json must be a JSON string")
197
+
198
+ # Older generic hosts only supplied an unchanged reason. Keep that
199
+ # contract valid while making new hosts classify material path changes.
200
+ if not path_delta_mode:
201
+ path_delta_mode = "material_replan" if agent_vision_json else "unchanged"
202
+ if path_delta_mode not in HOST_PATH_DELTA_MODES - {""}:
203
+ errors.append("path_delta_mode must be unchanged or material_replan")
204
+
205
+ agent_vision: dict[str, Any] | None = None
206
+ if agent_vision_json:
207
+ if len(agent_vision_json) > HOST_AGENT_VISION_JSON_MAX_CHARS:
208
+ errors.append(
209
+ "agent_vision_json exceeds "
210
+ f"{HOST_AGENT_VISION_JSON_MAX_CHARS} characters"
211
+ )
212
+ else:
213
+ try:
214
+ packet = json.loads(agent_vision_json)
215
+ if not isinstance(packet, dict):
216
+ raise TypeError("agent_vision_json must decode to a JSON object")
217
+ envelope = (
218
+ plan.get("turn_envelope")
219
+ if isinstance(plan.get("turn_envelope"), dict)
220
+ else {}
221
+ )
222
+ agent_vision = normalize_goal_vision_packet(
223
+ packet,
224
+ goal_id=str(envelope.get("goal_id") or ""),
225
+ agent_id=str(envelope.get("agent_id") or "") or None,
226
+ )
227
+ except (json.JSONDecodeError, ValueError) as exc:
228
+ errors.append(f"invalid agent_vision_json: {exc}")
229
+
230
+ if path_delta_mode == "material_replan":
231
+ if agent_vision is None:
232
+ errors.append(
233
+ "material_replan requires agent_vision_json with goal_path_delta_v0"
234
+ )
235
+ elif not isinstance(agent_vision.get("path_delta"), dict):
236
+ errors.append(
237
+ "material_replan agent_vision_json requires goal_path_delta_v0"
238
+ )
239
+ elif agent_vision["path_delta"].get("outcome") != "replan":
240
+ errors.append(
241
+ "material_replan goal_path_delta_v0 outcome must be replan"
242
+ )
243
+ if unchanged_reason:
244
+ errors.append(
245
+ "material_replan cannot also declare vision_unchanged_reason"
246
+ )
247
+ elif path_delta_mode == "unchanged":
248
+ if agent_vision is not None:
249
+ errors.append("unchanged path_delta_mode cannot include agent_vision_json")
250
+ if not unchanged_reason:
251
+ errors.append("unchanged path_delta_mode requires vision_unchanged_reason")
252
+
253
+ return path_delta_mode, agent_vision
254
+
255
+
256
+ def validate_loopx_turn_host_result(
257
+ plan: Mapping[str, Any],
258
+ value: Mapping[str, Any],
259
+ *,
260
+ completion_lifecycle_configured: bool = False,
261
+ ) -> dict[str, Any]:
262
+ result = dict(value)
263
+ errors: list[str] = []
264
+ unknown = sorted(set(result) - HOST_RESULT_FIELDS)
265
+ if unknown:
266
+ errors.append("unsupported host result fields: " + ", ".join(unknown))
267
+ if result.get("schema_version") != LOOPX_TURN_RESULT_SCHEMA_VERSION:
268
+ errors.append("unsupported host result schema_version")
269
+
270
+ transaction = plan.get("transaction") if isinstance(plan.get("transaction"), dict) else {}
271
+ turn_key = str(transaction.get("turn_key") or "")
272
+ if not turn_key or str(result.get("turn_key") or "") != turn_key:
273
+ errors.append("host result turn_key does not match the transaction plan")
274
+
275
+ try:
276
+ kind = LoopXTurnResultKind(str(result.get("result_kind") or ""))
277
+ except ValueError:
278
+ kind = None
279
+ errors.append("unsupported host result kind")
280
+ if (
281
+ kind is LoopXTurnResultKind.VALIDATED_COMPLETION
282
+ and not completion_lifecycle_configured
283
+ ):
284
+ errors.append("validated_completion requires a todo lifecycle adapter")
285
+ if (
286
+ kind not in MATERIAL_HOST_RESULT_KINDS | STOP_HOST_RESULT_KINDS
287
+ and kind is not LoopXTurnResultKind.VALIDATED_COMPLETION
288
+ ):
289
+ errors.append("host result kind is not accepted by run-once")
290
+
291
+ phases = result.get("completed_phases")
292
+ if phases != list(TRANSACTION_PHASES[:2]):
293
+ errors.append("host result completed_phases must be host_execute, typed_result")
294
+
295
+ material = kind in MATERIAL_HOST_RESULT_KINDS
296
+ normalized = {
297
+ "schema_version": LOOPX_TURN_RESULT_SCHEMA_VERSION,
298
+ "turn_key": turn_key,
299
+ "result_kind": kind.value if kind else None,
300
+ "completed_phases": list(TRANSACTION_PHASES[:2]),
301
+ }
302
+ for field, limit in HOST_RESULT_TEXT_LIMITS:
303
+ text = _bounded_public_text(
304
+ result,
305
+ field,
306
+ limit=limit,
307
+ required=material and field not in {"summary", "vision_unchanged_reason"},
308
+ errors=errors,
309
+ )
310
+ if text:
311
+ normalized[field] = text
312
+ if material:
313
+ try:
314
+ normalized["delivery_batch_scale"] = require_delivery_batch_scale(
315
+ result.get("delivery_batch_scale")
316
+ ).value
317
+ except ValueError as exc:
318
+ errors.append(str(exc))
319
+ try:
320
+ normalized["delivery_outcome"] = require_delivery_outcome(
321
+ result.get("delivery_outcome")
322
+ ).value
323
+ except ValueError as exc:
324
+ errors.append(str(exc))
325
+
326
+ unchanged_reason = str(normalized.get("vision_unchanged_reason") or "")
327
+ path_delta_mode, agent_vision = _normalize_host_path_delta(
328
+ plan,
329
+ result,
330
+ unchanged_reason=unchanged_reason,
331
+ errors=errors,
332
+ )
333
+ if (
334
+ path_delta_mode == "material_replan"
335
+ and kind is not LoopXTurnResultKind.REPLAN_REQUIRED
336
+ ):
337
+ errors.append(
338
+ "material_replan path_delta_mode requires result_kind replan_required"
339
+ )
340
+
341
+ normalized["path_delta_mode"] = path_delta_mode
342
+ if agent_vision is not None:
343
+ normalized["agent_vision"] = agent_vision
344
+ elif str(result.get("path_delta_mode") or "").strip() or str(
345
+ result.get("agent_vision_json") or ""
346
+ ).strip():
347
+ errors.append(
348
+ "wait and user_action_required results cannot declare a path delta"
349
+ )
350
+ return {
351
+ "ok": not errors,
352
+ "result": normalized,
353
+ "errors": errors,
354
+ }
355
+
356
+
357
+ def _task_validation_receipt(
358
+ *,
359
+ status: str,
360
+ validator_kind: str,
361
+ summary: str,
362
+ recovery_kind: str | None = None,
363
+ exit_code: int | None = None,
364
+ ) -> dict[str, Any]:
365
+ errors: list[str] = []
366
+ if status not in {
367
+ "passed",
368
+ "progress",
369
+ "failed",
370
+ "inconclusive",
371
+ "unavailable",
372
+ "not_required",
373
+ }:
374
+ errors.append("unsupported task validation status")
375
+ if not validator_kind or len(validator_kind) > 80:
376
+ errors.append("task validator kind must contain at most 80 characters")
377
+ if not summary or len(summary) > 240:
378
+ errors.append("task validation summary must contain at most 240 characters")
379
+ for field, text in (("validator_kind", validator_kind), ("summary", summary)):
380
+ if text:
381
+ try:
382
+ validate_public_safe_text(f"task_validation.{field}", text)
383
+ except ValueError as exc:
384
+ errors.append(str(exc))
385
+ if recovery_kind is not None and recovery_kind not in {
386
+ LoopXTurnResultKind.REPAIR_REQUIRED.value,
387
+ LoopXTurnResultKind.REPLAN_REQUIRED.value,
388
+ }:
389
+ errors.append("task validation recovery_kind must be repair_required or replan_required")
390
+ if status in {"failed", "inconclusive", "unavailable"} and recovery_kind is None:
391
+ errors.append("failed task validation requires a typed recovery_kind")
392
+ if status in {"passed", "progress", "not_required"} and recovery_kind is not None:
393
+ errors.append("successful task validation cannot declare recovery_kind")
394
+ if exit_code is not None and (not isinstance(exit_code, int) or exit_code < 0):
395
+ errors.append("task validation exit_code must be a non-negative integer")
396
+ if status == "passed" and exit_code not in {None, 0}:
397
+ errors.append("passed task validation cannot declare a non-zero exit_code")
398
+ if status == "progress" and exit_code in {None, 0}:
399
+ errors.append("progress task validation requires a non-zero exit_code")
400
+ effective_status = status if not errors else "inconclusive"
401
+ effective_recovery_kind = recovery_kind
402
+ if errors and effective_recovery_kind is None:
403
+ effective_recovery_kind = LoopXTurnResultKind.REPAIR_REQUIRED.value
404
+ return {
405
+ "ok": not errors and status in {"passed", "progress", "not_required"},
406
+ "schema_version": LOOPX_TURN_TASK_VALIDATION_SCHEMA_VERSION,
407
+ "status": effective_status,
408
+ "validator_kind": validator_kind or "invalid",
409
+ "summary": summary or "task validation receipt is invalid",
410
+ "recovery_kind": effective_recovery_kind,
411
+ "exit_code": exit_code,
412
+ "errors": errors,
413
+ }
414
+
415
+
416
+ def _run_task_validator(
417
+ plan: Mapping[str, Any],
418
+ result: Mapping[str, Any],
419
+ *,
420
+ validator: TaskValidator | None,
421
+ ) -> dict[str, Any]:
422
+ if validator is None:
423
+ return _task_validation_receipt(
424
+ status="unavailable",
425
+ validator_kind="none",
426
+ summary="independent task validator is required for material host results",
427
+ recovery_kind=LoopXTurnResultKind.REPAIR_REQUIRED.value,
428
+ )
429
+ try:
430
+ value = validator(plan, result)
431
+ except Exception: # noqa: BLE001 - validator plugins fail closed at this boundary
432
+ return _task_validation_receipt(
433
+ status="inconclusive",
434
+ validator_kind="callback",
435
+ summary="independent task validator did not produce a receipt",
436
+ recovery_kind=LoopXTurnResultKind.REPAIR_REQUIRED.value,
437
+ )
438
+ if not isinstance(value, Mapping):
439
+ return _task_validation_receipt(
440
+ status="inconclusive",
441
+ validator_kind="callback",
442
+ summary="independent task validator returned an invalid receipt",
443
+ recovery_kind=LoopXTurnResultKind.REPAIR_REQUIRED.value,
444
+ )
445
+ unknown = sorted(
446
+ set(value)
447
+ - {
448
+ "status",
449
+ "validator_kind",
450
+ "summary",
451
+ "recovery_kind",
452
+ "exit_code",
453
+ }
454
+ )
455
+ if unknown:
456
+ return _task_validation_receipt(
457
+ status="inconclusive",
458
+ validator_kind="callback",
459
+ summary="independent task validator returned unsupported receipt fields",
460
+ recovery_kind=LoopXTurnResultKind.REPAIR_REQUIRED.value,
461
+ )
462
+ status = str(value.get("status") or "")
463
+ if status == "not_required":
464
+ return _task_validation_receipt(
465
+ status="inconclusive",
466
+ validator_kind="callback",
467
+ summary="material host results cannot skip independent task validation",
468
+ recovery_kind=LoopXTurnResultKind.REPAIR_REQUIRED.value,
469
+ )
470
+ exit_code_value = value.get("exit_code")
471
+ if exit_code_value is not None and (
472
+ not isinstance(exit_code_value, int) or isinstance(exit_code_value, bool)
473
+ ):
474
+ return _task_validation_receipt(
475
+ status="inconclusive",
476
+ validator_kind="callback",
477
+ summary="independent task validator returned an invalid exit code",
478
+ recovery_kind=LoopXTurnResultKind.REPAIR_REQUIRED.value,
479
+ )
480
+ return _task_validation_receipt(
481
+ status=status,
482
+ validator_kind=str(value.get("validator_kind") or ""),
483
+ summary=str(value.get("summary") or ""),
484
+ recovery_kind=(
485
+ str(value["recovery_kind"])
486
+ if value.get("recovery_kind") is not None
487
+ else None
488
+ ),
489
+ exit_code=exit_code_value,
490
+ )
491
+
492
+
493
+ def build_loopx_turn_command_validator(
494
+ argv: Sequence[str],
495
+ *,
496
+ project: Path,
497
+ timeout_seconds: float,
498
+ failure_recovery_kind: str = LoopXTurnResultKind.REPAIR_REQUIRED.value,
499
+ ) -> TaskValidator:
500
+ """Build a trusted argv-only postcondition validator for one Turn host workspace."""
501
+
502
+ normalized = _normalize_task_validator_argv(argv)
503
+ if failure_recovery_kind not in {
504
+ LoopXTurnResultKind.REPAIR_REQUIRED.value,
505
+ LoopXTurnResultKind.REPLAN_REQUIRED.value,
506
+ }:
507
+ raise ValueError(
508
+ "task validator failure recovery must be repair_required or replan_required"
509
+ )
510
+
511
+ def validate(
512
+ _plan: Mapping[str, Any],
513
+ result: Mapping[str, Any],
514
+ ) -> Mapping[str, Any]:
515
+ try:
516
+ completed = subprocess.run(
517
+ normalized,
518
+ cwd=project,
519
+ input=json.dumps(result, ensure_ascii=False, separators=(",", ":")),
520
+ text=True,
521
+ stdout=subprocess.DEVNULL,
522
+ stderr=subprocess.DEVNULL,
523
+ timeout=max(1.0, timeout_seconds),
524
+ check=False,
525
+ )
526
+ except (OSError, subprocess.TimeoutExpired):
527
+ return {
528
+ "status": "inconclusive",
529
+ "validator_kind": "command",
530
+ "summary": "independent task validation command could not complete",
531
+ "recovery_kind": LoopXTurnResultKind.REPAIR_REQUIRED.value,
532
+ }
533
+ if completed.returncode != 0:
534
+ return {
535
+ "status": "failed",
536
+ "validator_kind": "command",
537
+ "summary": "independent task validation command returned non-zero",
538
+ "recovery_kind": failure_recovery_kind,
539
+ "exit_code": completed.returncode,
540
+ }
541
+ return {
542
+ "status": "passed",
543
+ "validator_kind": "command",
544
+ "summary": "independent task validation command passed",
545
+ "exit_code": 0,
546
+ }
547
+
548
+ return validate
549
+
550
+
551
+ def turn_journal_path(runtime_root: Path, *, goal_id: str, turn_key: str) -> Path:
552
+ match = TURN_KEY_RE.fullmatch(turn_key)
553
+ if not match:
554
+ raise ValueError("turn_key must be a sha256 digest")
555
+ return runtime_root / "goals" / goal_id / "turns" / f"{match.group('digest')}.json"
556
+
557
+
558
+ def _write_journal(path: Path, journal: Mapping[str, Any]) -> None:
559
+ path.parent.mkdir(parents=True, exist_ok=True)
560
+ descriptor, temporary_name = tempfile.mkstemp(prefix=f".{path.name}.", dir=path.parent)
561
+ temporary = Path(temporary_name)
562
+ try:
563
+ with os.fdopen(descriptor, "w", encoding="utf-8") as handle:
564
+ json.dump(journal, handle, ensure_ascii=False, indent=2, sort_keys=True)
565
+ handle.write("\n")
566
+ os.replace(temporary, path)
567
+ finally:
568
+ temporary.unlink(missing_ok=True)
569
+
570
+
571
+ def _load_journal(path: Path) -> dict[str, Any] | None:
572
+ if not path.exists():
573
+ return None
574
+ value = json.loads(path.read_text(encoding="utf-8"))
575
+ if not isinstance(value, dict) or value.get("schema_version") != LOOPX_TURN_JOURNAL_SCHEMA_VERSION:
576
+ raise ValueError("LoopX Turn journal has an unsupported schema")
577
+ return value
578
+
579
+
580
+ def inspect_loopx_turn_journal(
581
+ runtime_root: Path,
582
+ *,
583
+ goal_id: str,
584
+ agent_id: str,
585
+ turn_key: str,
586
+ ) -> dict[str, object]:
587
+ """Inspect one canonical Turn journal without granting execution authority."""
588
+
589
+ safe_goal_id = validate_goal_id_path_segment(goal_id)
590
+ if not agent_id or agent_id != agent_id.strip():
591
+ raise ValueError("agent_id must be a non-empty exact identity")
592
+ path = turn_journal_path(
593
+ runtime_root,
594
+ goal_id=safe_goal_id,
595
+ turn_key=turn_key,
596
+ )
597
+ try:
598
+ with exclusive_file_lock(path):
599
+ journal = _load_journal(path)
600
+ except json.JSONDecodeError:
601
+ raise ValueError("LoopX Turn journal contains malformed JSON") from None
602
+ except LockAcquireTimeoutError:
603
+ raise
604
+ except OSError:
605
+ raise ValueError("LoopX Turn journal could not be read") from None
606
+ if journal is None:
607
+ raise ValueError("LoopX Turn journal does not exist")
608
+
609
+ turn = interpret_turn_journal(
610
+ journal,
611
+ goal_id=safe_goal_id,
612
+ agent_id=agent_id,
613
+ turn_key=turn_key,
614
+ )
615
+ context = turn.request.context
616
+ return {
617
+ "ok": True,
618
+ "schema_version": LOOPX_TURN_JOURNAL_INSPECTION_SCHEMA_VERSION,
619
+ "decision": turn.observation.decision,
620
+ "journal_status": context["journal_status"],
621
+ "replay_legal": context["replay_legal"],
622
+ "goal_matches": context["goal_matches"],
623
+ "owner_matches": context["owner_matches"],
624
+ "turn_key_matches": context["turn_key_matches"],
625
+ "phases_form_ordered_prefix": context["phases_form_ordered_prefix"],
626
+ "completed_phases": list(context["completed_phases"]),
627
+ "tombstone_retained": context["tombstone_retained"],
628
+ "violations": list(context["violations"]),
629
+ "effects": [],
630
+ }
631
+
632
+
633
+ def _journal_committed_effect_id(journal: Mapping[str, Any]) -> str | None:
634
+ """Return the settlement effect id under which the journal was written.
635
+
636
+ Legacy journals created before the typed settlement binding have no
637
+ ``settlement_plan``; callers then cannot prove provenance and the
638
+ identity cross-check is skipped, preserving legacy resume behavior.
639
+ """
640
+
641
+ stored_plan = journal.get("plan")
642
+ if not isinstance(stored_plan, Mapping):
643
+ return None
644
+ transaction = stored_plan.get("transaction")
645
+ if not isinstance(transaction, Mapping):
646
+ return None
647
+ settlement_plan = transaction.get("settlement_plan")
648
+ if not isinstance(settlement_plan, Mapping):
649
+ return None
650
+ identity = settlement_plan.get("identity")
651
+ if not isinstance(identity, Mapping):
652
+ return None
653
+ effect_id = str(identity.get("effect_id") or "").strip()
654
+ return effect_id or None
655
+
656
+
657
+ def load_loopx_turn_plan_from_journal(
658
+ runtime_root: Path,
659
+ *,
660
+ goal_id: str,
661
+ turn_key: str,
662
+ ) -> dict[str, Any]:
663
+ path = turn_journal_path(runtime_root, goal_id=goal_id, turn_key=turn_key)
664
+ with exclusive_file_lock(path):
665
+ journal = _load_journal(path)
666
+ if journal is None:
667
+ raise ValueError("LoopX Turn resume journal does not exist")
668
+ plan = journal.get("plan")
669
+ if not isinstance(plan, dict):
670
+ raise TypeError("LoopX Turn resume journal does not contain a plan")
671
+ transaction = plan.get("transaction") if isinstance(plan.get("transaction"), dict) else {}
672
+ if transaction.get("turn_key") != turn_key or journal.get("turn_key") != turn_key:
673
+ raise ValueError("LoopX Turn resume journal has mismatched turn lineage")
674
+ envelope = plan.get("turn_envelope") if isinstance(plan.get("turn_envelope"), dict) else {}
675
+ if envelope.get("goal_id") != goal_id or journal.get("goal_id") != goal_id:
676
+ raise ValueError("LoopX Turn resume journal belongs to another goal")
677
+ return dict(plan)
678
+
679
+
680
+ def _receipt(
681
+ plan: Mapping[str, Any],
682
+ result: Mapping[str, Any],
683
+ *,
684
+ completed_phases: Sequence[str],
685
+ failure_kind: LoopXTurnResultKind | None = None,
686
+ failed_phase: str | None = None,
687
+ ) -> dict[str, Any]:
688
+ payload = {
689
+ "schema_version": LOOPX_TURN_RESULT_SCHEMA_VERSION,
690
+ "turn_key": result.get("turn_key"),
691
+ "result_kind": (
692
+ failure_kind.value if failure_kind else result.get("result_kind")
693
+ ),
694
+ "completed_phases": list(completed_phases),
695
+ "failed_phase": failed_phase,
696
+ }
697
+ return validate_loopx_turn_receipt(
698
+ plan.get("transaction") if isinstance(plan.get("transaction"), dict) else {},
699
+ payload,
700
+ )
701
+
702
+
703
+ def _host_failure(
704
+ plan: Mapping[str, Any],
705
+ *,
706
+ kind: LoopXTurnResultKind,
707
+ completed_phases: Sequence[str],
708
+ failed_phase: str,
709
+ reason: str,
710
+ ) -> dict[str, Any]:
711
+ transaction = plan.get("transaction") if isinstance(plan.get("transaction"), dict) else {}
712
+ result = {"turn_key": transaction.get("turn_key"), "result_kind": kind.value}
713
+ return {
714
+ "ok": False,
715
+ "reason": reason,
716
+ "receipt": _receipt(
717
+ plan,
718
+ result,
719
+ completed_phases=completed_phases,
720
+ failure_kind=kind,
721
+ failed_phase=failed_phase,
722
+ ),
723
+ }
724
+
725
+
726
+ def _run_host(
727
+ request: Mapping[str, Any],
728
+ *,
729
+ argv: Sequence[str],
730
+ project: Path,
731
+ timeout_seconds: float,
732
+ ) -> dict[str, Any]:
733
+ try:
734
+ completed = subprocess.run(
735
+ list(argv),
736
+ cwd=project,
737
+ input=json.dumps(request, ensure_ascii=False, separators=(",", ":")),
738
+ text=True,
739
+ capture_output=True,
740
+ timeout=max(1.0, timeout_seconds),
741
+ check=False,
742
+ )
743
+ except (OSError, subprocess.TimeoutExpired) as exc:
744
+ return {"ok": False, "reason": type(exc).__name__, "returncode": None}
745
+ if completed.returncode != 0:
746
+ return {
747
+ "ok": False,
748
+ "reason": "host command returned non-zero",
749
+ "returncode": completed.returncode,
750
+ "stderr_chars": len(completed.stderr),
751
+ }
752
+ encoded = completed.stdout.encode("utf-8")
753
+ if len(encoded) > HOST_RESULT_MAX_BYTES:
754
+ return {"ok": False, "reason": "host stdout exceeded the result budget", "returncode": 0}
755
+ try:
756
+ value = json.loads(completed.stdout)
757
+ except json.JSONDecodeError:
758
+ return {"ok": False, "reason": "host stdout is not one JSON value", "returncode": 0}
759
+ if not isinstance(value, dict):
760
+ return {"ok": False, "reason": "host stdout must be one JSON object", "returncode": 0}
761
+ return {"ok": True, "value": value, "returncode": 0}
762
+
763
+
764
+ def _run_host_runner(
765
+ request: Mapping[str, Any],
766
+ *,
767
+ runner: HostRunner,
768
+ ) -> dict[str, Any]:
769
+ try:
770
+ value = runner(request)
771
+ except BuiltInHostError as exc:
772
+ return {"ok": False, "reason": exc.reason, "returncode": None}
773
+ except Exception as exc: # noqa: BLE001 - host adapters fail closed at boundary
774
+ return {"ok": False, "reason": type(exc).__name__, "returncode": None}
775
+ if not isinstance(value, dict):
776
+ return {"ok": False, "reason": "built-in host result must be one JSON object"}
777
+ encoded = json.dumps(value, ensure_ascii=False, separators=(",", ":")).encode("utf-8")
778
+ if len(encoded) > HOST_RESULT_MAX_BYTES:
779
+ return {"ok": False, "reason": "built-in host result exceeded the result budget"}
780
+ return {"ok": True, "value": value, "returncode": 0}
781
+
782
+
783
+ def _compact_callback(payload: Mapping[str, Any]) -> dict[str, Any]:
784
+ return {
785
+ key: payload.get(key)
786
+ for key in ("ok", "appended", "classification", "generated_at", "slots", "reason")
787
+ if key in payload
788
+ }
789
+
790
+
791
+ def _mapping(value: Any) -> dict[str, Any]:
792
+ return dict(value) if isinstance(value, Mapping) else {}
793
+
794
+
795
+ def _execution_payload(
796
+ plan: Mapping[str, Any],
797
+ journal: Mapping[str, Any],
798
+ *,
799
+ execute: bool,
800
+ replayed: bool,
801
+ effects: Mapping[str, bool],
802
+ ) -> dict[str, Any]:
803
+ transaction = plan.get("transaction") if isinstance(plan.get("transaction"), dict) else {}
804
+ turn_key = str(transaction.get("turn_key") or "")
805
+ planned_host = plan.get("host") if isinstance(plan.get("host"), dict) else {}
806
+ writeback = _mapping(journal.get("writeback"))
807
+ todo_completion = _mapping(writeback.get("completion"))
808
+ quota_spent = effects.get("quota_spent") is True or "quota_spend" in list(
809
+ journal.get("completed_phases") or []
810
+ )
811
+ return {
812
+ "ok": journal.get("status") in {
813
+ "preview",
814
+ "committed",
815
+ "stopped",
816
+ "scheduler_action_required",
817
+ },
818
+ "schema_version": LOOPX_TURN_EXECUTION_SCHEMA_VERSION,
819
+ "mode": "run_once",
820
+ "dry_run": not execute,
821
+ "replayed": replayed,
822
+ "resume_turn_key": turn_key,
823
+ "journal_ref": f"turn:{turn_key.removeprefix('sha256:')[:16]}",
824
+ "status": journal.get("status"),
825
+ "execution_mode": planned_host.get("execution_mode"),
826
+ "host": journal.get("host"),
827
+ "result_kind": journal.get("result_kind"),
828
+ "validation": journal.get("task_validation"),
829
+ "receipt": journal.get("receipt"),
830
+ "scheduler": journal.get("scheduler"),
831
+ "effects": dict(effects),
832
+ "quota_slot_spend_count": 1 if quota_spent else 0,
833
+ **(
834
+ {"settlement_result": journal["settlement_result"]}
835
+ if isinstance(journal.get("settlement_result"), Mapping)
836
+ else {}
837
+ ),
838
+ **({"todo_completion": todo_completion} if todo_completion else {}),
839
+ **({"reason": journal.get("reason")} if journal.get("reason") else {}),
840
+ }
841
+
842
+
843
+ def _host_result_stage(
844
+ plan: Mapping[str, Any],
845
+ request: Mapping[str, Any],
846
+ *,
847
+ host_runner: HostRunner | None,
848
+ argv: Sequence[str] | None,
849
+ completion_lifecycle_configured: bool,
850
+ project: Path,
851
+ timeout_seconds: float,
852
+ journal: dict[str, Any],
853
+ journal_path: Path,
854
+ effects: dict[str, bool],
855
+ ) -> tuple[dict[str, Any] | None, list[str], dict[str, Any] | None]:
856
+ completed_phases = list(journal.get("completed_phases") or [])
857
+ result = (
858
+ journal.get("host_result")
859
+ if isinstance(journal.get("host_result"), dict)
860
+ else None
861
+ )
862
+ if "typed_result" not in completed_phases:
863
+ host_observation = (
864
+ _run_host_runner(request, runner=host_runner)
865
+ if host_runner is not None
866
+ else _run_host(
867
+ request,
868
+ argv=argv or [],
869
+ project=project,
870
+ timeout_seconds=timeout_seconds,
871
+ )
872
+ )
873
+ effects["host_invoked"] = True
874
+ if not host_observation.get("ok"):
875
+ failure = _host_failure(
876
+ plan,
877
+ kind=LoopXTurnResultKind.HOST_FAILURE,
878
+ completed_phases=[],
879
+ failed_phase="host_execute",
880
+ reason=str(host_observation.get("reason") or "host execution failed"),
881
+ )
882
+ journal.update(
883
+ status="failed",
884
+ reason=failure["reason"],
885
+ receipt=failure["receipt"],
886
+ completed_phases=[],
887
+ result_kind=LoopXTurnResultKind.HOST_FAILURE.value,
888
+ )
889
+ _write_journal(journal_path, journal)
890
+ return (
891
+ None,
892
+ [],
893
+ _execution_payload(
894
+ plan,
895
+ journal,
896
+ execute=True,
897
+ replayed=False,
898
+ effects=effects,
899
+ ),
900
+ )
901
+ result = dict(host_observation["value"])
902
+ completed_phases = list(TRANSACTION_PHASES[:2])
903
+
904
+ validation = validate_loopx_turn_host_result(
905
+ plan,
906
+ result or {},
907
+ completion_lifecycle_configured=completion_lifecycle_configured,
908
+ )
909
+ if not validation.get("ok"):
910
+ failure = _host_failure(
911
+ plan,
912
+ kind=LoopXTurnResultKind.VALIDATION_FAILED,
913
+ completed_phases=list(TRANSACTION_PHASES[:2]),
914
+ failed_phase="validation",
915
+ reason="; ".join(
916
+ validation.get("errors") or ["host result validation failed"]
917
+ ),
918
+ )
919
+ journal.update(
920
+ status="failed",
921
+ reason=failure["reason"],
922
+ receipt=failure["receipt"],
923
+ completed_phases=list(TRANSACTION_PHASES[:2]),
924
+ result_kind=LoopXTurnResultKind.VALIDATION_FAILED.value,
925
+ validation_stage="host_result_contract",
926
+ )
927
+ _write_journal(journal_path, journal)
928
+ return (
929
+ None,
930
+ list(TRANSACTION_PHASES[:2]),
931
+ _execution_payload(
932
+ plan,
933
+ journal,
934
+ execute=True,
935
+ replayed=False,
936
+ effects=effects,
937
+ ),
938
+ )
939
+
940
+ normalized = dict(validation["result"])
941
+ if len(completed_phases) < 2:
942
+ completed_phases = list(TRANSACTION_PHASES[:2])
943
+ journal.update(
944
+ host_result=normalized,
945
+ result_kind=normalized.get("result_kind"),
946
+ completed_phases=completed_phases,
947
+ )
948
+ _write_journal(journal_path, journal)
949
+ return normalized, completed_phases, None
950
+
951
+
952
+ def _task_validation_stage(
953
+ plan: Mapping[str, Any],
954
+ result: Mapping[str, Any],
955
+ *,
956
+ task_validator: TaskValidator | None,
957
+ completed_phases: list[str],
958
+ journal: dict[str, Any],
959
+ journal_path: Path,
960
+ effects: dict[str, bool],
961
+ ) -> tuple[list[str], dict[str, Any] | None]:
962
+ turn = interpret_turn_result_packet(result)
963
+ kind = LoopXTurnResultKind(turn.observation.decision)
964
+ if kind in STOP_HOST_RESULT_KINDS:
965
+ completed_phases = list(TRANSACTION_PHASES[:3])
966
+ journal.update(
967
+ status="stopped",
968
+ completed_phases=completed_phases,
969
+ task_validation=_task_validation_receipt(
970
+ status="not_required",
971
+ validator_kind="stop_result",
972
+ summary="task validation is not required for a typed stop result",
973
+ ),
974
+ receipt=_receipt(plan, result, completed_phases=completed_phases),
975
+ scheduler={"disposition": "not_applicable"},
976
+ )
977
+ _write_journal(journal_path, journal)
978
+ return completed_phases, _execution_payload(
979
+ plan,
980
+ journal,
981
+ execute=True,
982
+ replayed=False,
983
+ effects=effects,
984
+ )
985
+
986
+ stored_task_validation = (
987
+ journal.get("task_validation")
988
+ if isinstance(journal.get("task_validation"), dict)
989
+ else None
990
+ )
991
+ task_validation = (
992
+ stored_task_validation
993
+ if "validation" in completed_phases
994
+ and stored_task_validation is not None
995
+ and stored_task_validation.get("ok") is True
996
+ else _run_task_validator(
997
+ plan,
998
+ result,
999
+ validator=task_validator,
1000
+ )
1001
+ )
1002
+ journal["task_validation"] = task_validation
1003
+ if not task_validation.get("ok"):
1004
+ reason = str(
1005
+ task_validation.get("summary") or "independent task validation failed"
1006
+ )
1007
+ failure = _host_failure(
1008
+ plan,
1009
+ kind=LoopXTurnResultKind.VALIDATION_FAILED,
1010
+ completed_phases=list(TRANSACTION_PHASES[:2]),
1011
+ failed_phase="validation",
1012
+ reason=reason,
1013
+ )
1014
+ journal.update(
1015
+ status="failed",
1016
+ reason=reason,
1017
+ receipt=failure["receipt"],
1018
+ completed_phases=list(TRANSACTION_PHASES[:2]),
1019
+ result_kind=LoopXTurnResultKind.VALIDATION_FAILED.value,
1020
+ validation_stage="task_postcondition",
1021
+ )
1022
+ _write_journal(journal_path, journal)
1023
+ return list(TRANSACTION_PHASES[:2]), _execution_payload(
1024
+ plan,
1025
+ journal,
1026
+ execute=True,
1027
+ replayed=False,
1028
+ effects=effects,
1029
+ )
1030
+
1031
+ if "validation" not in completed_phases:
1032
+ completed_phases = list(TRANSACTION_PHASES[:3])
1033
+ journal.update(
1034
+ result_kind=result.get("result_kind"),
1035
+ completed_phases=completed_phases,
1036
+ validation_stage="task_postcondition",
1037
+ )
1038
+ _write_journal(journal_path, journal)
1039
+ return completed_phases, None
1040
+
1041
+
1042
+ def _ensure_turn_settlement_plan(
1043
+ plan: Mapping[str, Any],
1044
+ transaction_plan: dict[str, Any],
1045
+ ) -> None:
1046
+ """Upgrade a legacy transaction plan with a typed settlement plan.
1047
+
1048
+ Turn plans produced by older LoopX releases (for example plans read from a
1049
+ scored-workspace image) do not carry ``settlement_plan``. Rebuild it from
1050
+ the plan's own ``turn_envelope`` lineage so the typed closeout can bind
1051
+ validation -> writeback -> spend instead of failing every legacy run.
1052
+ """
1053
+
1054
+ if isinstance(transaction_plan.get("settlement_plan"), Mapping):
1055
+ return
1056
+ envelope = plan.get("turn_envelope")
1057
+ if not isinstance(envelope, Mapping):
1058
+ return
1059
+ selected_todo = selected_turn_todo(envelope)
1060
+ lineage = {
1061
+ "goal_id": str(envelope.get("goal_id") or ""),
1062
+ "agent_id": str(envelope.get("agent_id") or ""),
1063
+ "todo_id": str(selected_todo.get("todo_id") or ""),
1064
+ }
1065
+ if not all(lineage.values()):
1066
+ return
1067
+ host = plan.get("host")
1068
+ host_fields = host if isinstance(host, Mapping) else {}
1069
+ built = build_loopx_turn_transaction_plan(
1070
+ planned=True,
1071
+ lineage=lineage,
1072
+ host=str(host_fields.get("kind") or "generic-cli"),
1073
+ execution_mode=str(
1074
+ host_fields.get("execution_mode") or "isolated-headless"
1075
+ ),
1076
+ session_action=str(host_fields.get("session_action") or "resume"),
1077
+ turn_instance_id=transaction_plan.get("turn_instance_id"),
1078
+ )
1079
+ settlement_plan = built.get("settlement_plan")
1080
+ if isinstance(settlement_plan, Mapping):
1081
+ transaction_plan["settlement_plan"] = settlement_plan
1082
+
1083
+
1084
+ def _typed_settlement_stage(
1085
+ plan: Mapping[str, Any],
1086
+ result: dict[str, Any],
1087
+ *,
1088
+ completed_phases: list[str],
1089
+ journal: dict[str, Any],
1090
+ journal_path: Path,
1091
+ effects: dict[str, bool],
1092
+ writeback: Writeback,
1093
+ completion_writeback: CompletionWriteback | None,
1094
+ completion_intent: CompletionIntent | None,
1095
+ terminal_closeout: TerminalCloseout | None,
1096
+ spend: Spend,
1097
+ scheduler: Scheduler,
1098
+ ) -> dict[str, Any]:
1099
+ transaction_plan = (
1100
+ plan.get("transaction")
1101
+ if isinstance(plan.get("transaction"), Mapping)
1102
+ else {}
1103
+ )
1104
+ _ensure_turn_settlement_plan(plan, transaction_plan)
1105
+
1106
+ terminal_closeout_required = False
1107
+ completion_intent_error: str | None = None
1108
+ if result.get("result_kind") == LoopXTurnResultKind.VALIDATED_COMPLETION.value:
1109
+ if (
1110
+ completion_writeback is None
1111
+ or completion_intent is None
1112
+ or terminal_closeout is None
1113
+ ):
1114
+ raise ValueError(
1115
+ "validated_completion requires intent, lifecycle writeback, "
1116
+ "and terminal closeout adapters"
1117
+ )
1118
+ terminal_closeout_required, completion_intent_error = (
1119
+ terminal_closeout_requirement(
1120
+ plan=plan,
1121
+ result=result,
1122
+ journal=journal,
1123
+ completion_intent=completion_intent,
1124
+ )
1125
+ )
1126
+
1127
+ def writeback_effect() -> Mapping[str, Any]:
1128
+ if completion_intent_error:
1129
+ return {
1130
+ "ok": False,
1131
+ "appended": False,
1132
+ "reason": completion_intent_error,
1133
+ }
1134
+ if (
1135
+ result.get("result_kind")
1136
+ == LoopXTurnResultKind.VALIDATED_COMPLETION.value
1137
+ and not terminal_closeout_required
1138
+ ):
1139
+ if completion_writeback is None:
1140
+ raise ValueError("validated_completion requires a todo lifecycle adapter")
1141
+ callback_payload = completion_writeback(result)
1142
+ completion_outcome = completion_writeback_outcome(
1143
+ callback_payload,
1144
+ plan=plan,
1145
+ )
1146
+ if completion_outcome is None:
1147
+ return {
1148
+ "ok": False,
1149
+ "appended": False,
1150
+ "reason": str(
1151
+ callback_payload.get("reason")
1152
+ or callback_payload.get("error")
1153
+ or (
1154
+ "todo lifecycle adapter returned an invalid "
1155
+ "completion outcome"
1156
+ )
1157
+ ),
1158
+ }
1159
+ return {
1160
+ **callback_payload,
1161
+ "completion": completion_outcome,
1162
+ }
1163
+ return writeback(result)
1164
+
1165
+ def checkpoint(
1166
+ step_kind: SettlementStepKind,
1167
+ payload: Mapping[str, Any],
1168
+ phases: tuple[str, ...],
1169
+ ) -> None:
1170
+ if step_kind is SettlementStepKind.DURABLE_WRITEBACK:
1171
+ effects["state_written"] = True
1172
+ journal["writeback"] = {
1173
+ **_compact_callback(payload),
1174
+ **(
1175
+ {"completion": payload["completion"]}
1176
+ if isinstance(payload.get("completion"), dict)
1177
+ else {}
1178
+ ),
1179
+ }
1180
+ elif step_kind is SettlementStepKind.QUOTA_SPEND:
1181
+ effects["quota_spent"] = True
1182
+ journal["quota_spend"] = _compact_callback(payload)
1183
+ journal["completed_phases"] = list(phases)
1184
+ _write_journal(journal_path, journal)
1185
+
1186
+ settlement_result = execute_turn_driver_settlement(
1187
+ transaction_plan,
1188
+ transaction_phases=TRANSACTION_PHASES,
1189
+ completed_phases=completed_phases,
1190
+ writeback_payload=(
1191
+ journal.get("writeback")
1192
+ if isinstance(journal.get("writeback"), Mapping)
1193
+ else None
1194
+ ),
1195
+ quota_spend_payload=(
1196
+ journal.get("quota_spend")
1197
+ if isinstance(journal.get("quota_spend"), Mapping)
1198
+ else None
1199
+ ),
1200
+ writeback=writeback_effect,
1201
+ spend=spend,
1202
+ checkpoint=checkpoint,
1203
+ committed_effect_id=_journal_committed_effect_id(journal),
1204
+ )
1205
+ settlement_state = settlement_result.value
1206
+ if settlement_result.failure is None and terminal_closeout_required:
1207
+ assert terminal_closeout is not None
1208
+
1209
+ def terminal_checkpoint(payload: Mapping[str, Any]) -> None:
1210
+ compact = {
1211
+ **_compact_callback(payload),
1212
+ **(
1213
+ {"completion": payload["completion"]}
1214
+ if isinstance(payload.get("completion"), Mapping)
1215
+ else {}
1216
+ ),
1217
+ }
1218
+ journal["terminal_closeout"] = compact
1219
+ journal["writeback"] = {
1220
+ **_mapping(journal.get("writeback")),
1221
+ "completion": compact.get("completion"),
1222
+ }
1223
+ _write_journal(journal_path, journal)
1224
+
1225
+ terminal_result = execute_verified_turn_terminal_closeout(
1226
+ transaction_plan,
1227
+ plan=plan,
1228
+ committed_payload=(
1229
+ journal.get("terminal_closeout")
1230
+ if isinstance(journal.get("terminal_closeout"), Mapping)
1231
+ else None
1232
+ ),
1233
+ closeout=lambda: terminal_closeout(result),
1234
+ checkpoint=terminal_checkpoint,
1235
+ committed_effect_id=_journal_committed_effect_id(journal),
1236
+ )
1237
+ settlement_result = SettlementResult(
1238
+ value=settlement_state if terminal_result.failure is None else None,
1239
+ receipts=(*settlement_result.receipts, *terminal_result.receipts),
1240
+ failure=terminal_result.failure,
1241
+ )
1242
+
1243
+ journal["settlement_result"] = settlement_result_payload(settlement_result)
1244
+ if settlement_result.failure is not None:
1245
+ failure_step = settlement_result.failure.step_kind
1246
+ result_kind = (
1247
+ LoopXTurnResultKind.VALIDATION_FAILED
1248
+ if failure_step is SettlementStepKind.VALIDATION
1249
+ else LoopXTurnResultKind.WRITEBACK_FAILED
1250
+ if failure_step is SettlementStepKind.DURABLE_WRITEBACK
1251
+ else LoopXTurnResultKind.QUOTA_SPEND_FAILED
1252
+ if failure_step is SettlementStepKind.QUOTA_SPEND
1253
+ else LoopXTurnResultKind.TERMINAL_CLOSEOUT_FAILED
1254
+ )
1255
+ completed_phases = list(journal.get("completed_phases") or completed_phases)
1256
+ failure = _host_failure(
1257
+ plan,
1258
+ kind=result_kind,
1259
+ completed_phases=completed_phases,
1260
+ failed_phase=failure_step.value,
1261
+ reason=settlement_result.failure.reason,
1262
+ )
1263
+ journal.update(
1264
+ status="failed",
1265
+ result_kind=result_kind.value,
1266
+ reason=failure["reason"],
1267
+ receipt=failure["receipt"],
1268
+ )
1269
+ _write_journal(journal_path, journal)
1270
+ return _execution_payload(
1271
+ plan,
1272
+ journal,
1273
+ execute=True,
1274
+ replayed=False,
1275
+ effects=effects,
1276
+ )
1277
+
1278
+ settlement_state = settlement_result.value
1279
+ if settlement_state is None or settlement_state.quota_spend is None:
1280
+ raise ValueError("typed Turn settlement completed without a quota spend receipt")
1281
+ completed_phases = list(settlement_state.completed_phases)
1282
+ spend_payload = dict(settlement_state.quota_spend)
1283
+ _write_journal(journal_path, journal)
1284
+
1285
+ scheduler_payload = scheduler(spend_payload)
1286
+ journal["scheduler"] = scheduler_payload
1287
+ if scheduler_payload.get("completed") is not True:
1288
+ journal.update(
1289
+ status="scheduler_action_required",
1290
+ receipt=_receipt(plan, result, completed_phases=completed_phases),
1291
+ )
1292
+ _write_journal(journal_path, journal)
1293
+ return _execution_payload(
1294
+ plan,
1295
+ journal,
1296
+ execute=True,
1297
+ replayed=False,
1298
+ effects=effects,
1299
+ )
1300
+
1301
+ completed_phases = list(TRANSACTION_PHASES)
1302
+ effects["scheduler_acknowledged"] = bool(scheduler_payload.get("acknowledged"))
1303
+ journal.update(
1304
+ status="committed",
1305
+ completed_phases=completed_phases,
1306
+ receipt=_receipt(plan, result, completed_phases=completed_phases),
1307
+ )
1308
+ _write_journal(journal_path, journal)
1309
+ return _execution_payload(
1310
+ plan,
1311
+ journal,
1312
+ execute=True,
1313
+ replayed=False,
1314
+ effects=effects,
1315
+ )
1316
+
1317
+
1318
+ def run_loopx_turn_once(
1319
+ plan: Mapping[str, Any],
1320
+ *,
1321
+ host_argv: Sequence[str] | None = None,
1322
+ host_runner: HostRunner | None = None,
1323
+ project: Path,
1324
+ runtime_root: Path,
1325
+ goal_id: str,
1326
+ timeout_seconds: float,
1327
+ execute: bool,
1328
+ retry_failed: bool = False,
1329
+ task_validator: TaskValidator | None = None,
1330
+ writeback: Writeback | None = None,
1331
+ completion_writeback: CompletionWriteback | None = None,
1332
+ completion_intent: CompletionIntent | None = None,
1333
+ terminal_closeout: TerminalCloseout | None = None,
1334
+ spend: Spend | None = None,
1335
+ scheduler: Scheduler | None = None,
1336
+ ) -> dict[str, Any]:
1337
+ if host_runner is not None and host_argv is not None:
1338
+ raise ValueError("run-once accepts either host_argv or host_runner, not both")
1339
+ if host_runner is None:
1340
+ argv = normalize_host_argv(host_argv or [])
1341
+ host_projection = {"executable": Path(argv[0]).name, "argv_count": len(argv)}
1342
+ else:
1343
+ argv = None
1344
+ planned_host = plan.get("host") if isinstance(plan.get("host"), dict) else {}
1345
+ host_projection = {
1346
+ "executable": "built-in",
1347
+ "kind": str(planned_host.get("kind") or "codex-cli"),
1348
+ }
1349
+ request = build_loopx_turn_host_request(plan)
1350
+ empty_effects = {
1351
+ "host_invoked": False,
1352
+ "state_written": False,
1353
+ "quota_spent": False,
1354
+ "scheduler_acknowledged": False,
1355
+ }
1356
+ if not execute:
1357
+ preview = {
1358
+ "schema_version": LOOPX_TURN_JOURNAL_SCHEMA_VERSION,
1359
+ "status": "preview",
1360
+ "host": host_projection,
1361
+ "result_kind": None,
1362
+ "receipt": None,
1363
+ "scheduler": {"disposition": "not_evaluated"},
1364
+ }
1365
+ return _execution_payload(
1366
+ plan,
1367
+ preview,
1368
+ execute=False,
1369
+ replayed=False,
1370
+ effects=empty_effects,
1371
+ )
1372
+ if writeback is None or spend is None or scheduler is None:
1373
+ raise ValueError("executing run-once requires writeback, spend, and scheduler callbacks")
1374
+
1375
+ turn_key = str(request["turn_key"])
1376
+ journal_path = turn_journal_path(runtime_root, goal_id=goal_id, turn_key=turn_key)
1377
+ with exclusive_file_lock(journal_path):
1378
+ journal = _load_journal(journal_path)
1379
+ if journal and (
1380
+ journal.get("status") in {"committed", "stopped"}
1381
+ or journal.get("status") == "failed" and not retry_failed
1382
+ ):
1383
+ return _execution_payload(
1384
+ plan,
1385
+ journal,
1386
+ execute=True,
1387
+ replayed=True,
1388
+ effects=empty_effects,
1389
+ )
1390
+ if journal and journal.get("status") == "failed":
1391
+ receipt = journal.get("receipt") if isinstance(journal.get("receipt"), dict) else {}
1392
+ if receipt.get("failed_phase") == "validation":
1393
+ if journal.get("validation_stage") != "task_postcondition":
1394
+ journal.pop("host_result", None)
1395
+ journal.pop("result_kind", None)
1396
+ journal["completed_phases"] = (
1397
+ list(TRANSACTION_PHASES[:2])
1398
+ if isinstance(journal.get("host_result"), dict)
1399
+ else []
1400
+ )
1401
+ journal.pop("task_validation", None)
1402
+ journal.pop("validation_stage", None)
1403
+ journal.pop("reason", None)
1404
+ journal.pop("receipt", None)
1405
+ journal["status"] = "in_progress"
1406
+ _write_journal(journal_path, journal)
1407
+ if journal is None:
1408
+ journal = {
1409
+ "schema_version": LOOPX_TURN_JOURNAL_SCHEMA_VERSION,
1410
+ "turn_key": turn_key,
1411
+ "goal_id": goal_id,
1412
+ "status": "in_progress",
1413
+ "host": host_projection,
1414
+ "completed_phases": [],
1415
+ "plan": dict(plan),
1416
+ }
1417
+ _write_journal(journal_path, journal)
1418
+
1419
+ effects = dict(empty_effects)
1420
+ result, completed_phases, terminal = _host_result_stage(
1421
+ plan,
1422
+ request,
1423
+ host_runner=host_runner,
1424
+ argv=argv,
1425
+ completion_lifecycle_configured=all(
1426
+ callback is not None
1427
+ for callback in (
1428
+ completion_writeback,
1429
+ completion_intent,
1430
+ terminal_closeout,
1431
+ )
1432
+ ),
1433
+ project=project,
1434
+ timeout_seconds=timeout_seconds,
1435
+ journal=journal,
1436
+ journal_path=journal_path,
1437
+ effects=effects,
1438
+ )
1439
+ if terminal is not None:
1440
+ return terminal
1441
+ assert result is not None
1442
+
1443
+ completed_phases, terminal = _task_validation_stage(
1444
+ plan,
1445
+ result,
1446
+ task_validator=task_validator,
1447
+ completed_phases=completed_phases,
1448
+ journal=journal,
1449
+ journal_path=journal_path,
1450
+ effects=effects,
1451
+ )
1452
+ if terminal is not None:
1453
+ return terminal
1454
+
1455
+ return _typed_settlement_stage(
1456
+ plan,
1457
+ result,
1458
+ completed_phases=completed_phases,
1459
+ journal=journal,
1460
+ journal_path=journal_path,
1461
+ effects=effects,
1462
+ writeback=writeback,
1463
+ completion_writeback=completion_writeback,
1464
+ completion_intent=completion_intent,
1465
+ terminal_closeout=terminal_closeout,
1466
+ spend=spend,
1467
+ scheduler=scheduler,
1468
+ )