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,1018 @@
1
+ // LoopX OpenCode 2 goal worker.
2
+ //
3
+ // A persistent out-of-process goal loop driver for OpenCode 2. OpenCode 2
4
+ // ships a new plugin API and its plugins cannot run the OpenCode 1 goal
5
+ // bridge, so LoopX drives OpenCode 2 sessions through the OpenCode 2 HTTP
6
+ // API instead:
7
+ //
8
+ // prompt -> wait for the assistant turn to complete -> loopx quota
9
+ // should-run -> run_now: continue | wait: backoff ladder | stop: visible
10
+ // pause | terminal_no_followup: visible completion
11
+ //
12
+ // The worker owns the timers, so recurring operation survives TUI close and
13
+ // one-shot CLI usage. Session state, pause reason, and poll bookkeeping are
14
+ // persisted so a crashed worker can be restarted.
15
+ //
16
+ // This file has no runtime dependencies. It shells out to `opencode2 api`
17
+ // (which performs its own service discovery and authentication) and to
18
+ // `loopx` for quota decisions.
19
+
20
+ import { execFile as execFileCallback } from "node:child_process"
21
+ import { promises as fs, realpathSync } from "node:fs"
22
+ import os from "node:os"
23
+ import path from "node:path"
24
+ import { promisify } from "node:util"
25
+ import { fileURLToPath } from "node:url"
26
+
27
+ const execFile = promisify(execFileCallback)
28
+
29
+ export const STATE_SCHEMA_VERSION = "loopx_opencode2_goal_worker_v0"
30
+ export const TERMINAL_STATE_SCHEMA_VERSION = "goal_terminal_state_v0"
31
+ export const SOURCE_COMPLETENESS_SCHEMA_VERSION = "goal_terminal_source_completeness_v0"
32
+ export const DEFAULT_RETRY_MINUTES = 3
33
+ export const MAX_RETRY_MINUTES = 30
34
+ export const MESSAGE_POLL_MS = 3_000
35
+ export const DEFAULT_TURN_TIMEOUT_MS = 120 * 60 * 1000
36
+ export const DEFAULT_MAX_TURNS = 10_000
37
+ export const DEFAULT_MAX_DURATION_MS = 30 * 24 * 60 * 60 * 1000
38
+ export const LEASE_STALE_MS = 5 * 60 * 1000
39
+
40
+ export function continuationPrompt(directory) {
41
+ const anchor = directory
42
+ ? `\nWorking directory: ${directory}\nStay inside this project; do not touch other goals or repositories, and never run loopx install/update/rollback/slash-commands operations or modify the host loopx installation.`
43
+ : ""
44
+ return [
45
+ "LoopX quota granted the next automatic turn.",
46
+ "Continue the goal work and report progress.",
47
+ "Do not self-declare completion; LoopX validates closure.",
48
+ anchor,
49
+ ]
50
+ .filter(Boolean)
51
+ .join(" ")
52
+ }
53
+
54
+ export const CONTINUATION_PROMPT = continuationPrompt()
55
+
56
+ export const STANDBY_MINUTES = 5
57
+
58
+ const TERMINAL_NOTICE = [
59
+ "LoopX validated terminal closure for the current work.",
60
+ "The worker stays attached and will resume automatically when new work appears.",
61
+ ].join(" ")
62
+ const LIMIT_NOTICE = [
63
+ "LoopX stopped this goal after the unchanged-poll limit.",
64
+ "No new work was scheduled; review LoopX todos and resume explicitly when ready.",
65
+ ].join(" ")
66
+ const TURN_LIMIT_NOTICE = [
67
+ "LoopX stopped this goal after the turn budget was exhausted.",
68
+ "Review LoopX todos before requesting another run.",
69
+ ].join(" ")
70
+ const DURATION_LIMIT_NOTICE = [
71
+ "LoopX stopped this goal after the duration budget was exhausted.",
72
+ "Review LoopX todos before requesting another run.",
73
+ ].join(" ")
74
+
75
+
76
+ export function sanitizedStateName(value) {
77
+ const text = String(value || "").replace(/[^A-Za-z0-9_-]/g, "_")
78
+ if (!text) throw new Error("goal id is required")
79
+ return text.slice(0, 160)
80
+ }
81
+
82
+
83
+ export function defaultStateRoot() {
84
+ if (process.env.LOOPX_OPENCODE2_STATE_DIR) {
85
+ return path.resolve(process.env.LOOPX_OPENCODE2_STATE_DIR)
86
+ }
87
+ const xdgState = process.env.XDG_STATE_HOME || path.join(os.homedir(), ".local", "state")
88
+ return path.join(xdgState, "loopx", "opencode2")
89
+ }
90
+
91
+
92
+ export function createStateStore({ root = defaultStateRoot(), now = () => Date.now() } = {}) {
93
+ const target = (goalId) => path.join(root, `${sanitizedStateName(goalId)}.json`)
94
+ return {
95
+ async read(goalId) {
96
+ try {
97
+ const payload = JSON.parse(await fs.readFile(target(goalId), "utf8"))
98
+ if (payload?.schemaVersion !== STATE_SCHEMA_VERSION || payload?.goalId !== goalId) {
99
+ throw new Error("invalid LoopX OpenCode 2 worker state")
100
+ }
101
+ return payload
102
+ } catch (error) {
103
+ if (error?.code === "ENOENT") return null
104
+ throw error
105
+ }
106
+ },
107
+ async write(goalId, state) {
108
+ await fs.mkdir(root, { recursive: true, mode: 0o700 })
109
+ const destination = target(goalId)
110
+ const temporary = `${destination}.${process.pid}.${now()}.tmp`
111
+ const payload = {
112
+ schemaVersion: STATE_SCHEMA_VERSION,
113
+ goalId,
114
+ ...state,
115
+ updatedAt: new Date(now()).toISOString(),
116
+ }
117
+ await fs.writeFile(temporary, `${JSON.stringify(payload, null, 2)}\n`, {
118
+ encoding: "utf8",
119
+ mode: 0o600,
120
+ })
121
+ await fs.rename(temporary, destination)
122
+ await fs.chmod(destination, 0o600)
123
+ return payload
124
+ },
125
+ lockPath(goalId) {
126
+ return `${target(goalId)}.lock`
127
+ },
128
+ async acquireLock(goalId, { staleMs = LEASE_STALE_MS, heartbeatMs = 60_000 } = {}) {
129
+ const lockPath = this.lockPath(goalId)
130
+ const release = async () => {
131
+ try {
132
+ await fs.unlink(lockPath)
133
+ } catch (error) {
134
+ if (error?.code !== "ENOENT") throw error
135
+ }
136
+ }
137
+ const heartbeat = async () => {
138
+ await fs.mkdir(root, { recursive: true, mode: 0o700 })
139
+ const temporary = `${lockPath}.${process.pid}.tmp`
140
+ await fs.writeFile(
141
+ temporary,
142
+ `${JSON.stringify({ pid: process.pid, heartbeatAt: new Date().toISOString() })}\n`,
143
+ { encoding: "utf8", mode: 0o600 },
144
+ )
145
+ await fs.rename(temporary, lockPath)
146
+ await fs.chmod(lockPath, 0o600)
147
+ }
148
+ const existing = await readLock(lockPath)
149
+ if (existing && (await leaseIsLive(existing, { staleMs }))) {
150
+ return {
151
+ acquired: false,
152
+ holder: existing,
153
+ reason: `held by pid ${existing.pid} with a fresh heartbeat`,
154
+ }
155
+ }
156
+ await heartbeat()
157
+ const timer = setInterval(() => {
158
+ heartbeat().catch(() => {})
159
+ }, heartbeatMs)
160
+ if (typeof timer?.unref === "function") timer.unref()
161
+ return {
162
+ acquired: true,
163
+ holder: { pid: process.pid, heartbeatAt: new Date().toISOString() },
164
+ reason: "",
165
+ release: async () => {
166
+ clearInterval(timer)
167
+ await release()
168
+ },
169
+ }
170
+ },
171
+ }
172
+ }
173
+
174
+
175
+ async function readLock(lockPath) {
176
+ try {
177
+ const payload = JSON.parse(await fs.readFile(lockPath, "utf8"))
178
+ if (!Number.isInteger(payload?.pid)) return null
179
+ return payload
180
+ } catch (error) {
181
+ if (error?.code === "ENOENT") return null
182
+ try {
183
+ await fs.unlink(lockPath)
184
+ } catch {
185
+ // A malformed lock is retried as unreadable; removal is best-effort.
186
+ }
187
+ return null
188
+ }
189
+ }
190
+
191
+
192
+ export function pidIsAlive(pid) {
193
+ try {
194
+ process.kill(pid, 0)
195
+ return true
196
+ } catch (error) {
197
+ return error?.code === "EPERM"
198
+ }
199
+ }
200
+
201
+
202
+ export async function leaseIsLive(lock, { staleMs = LEASE_STALE_MS, now = () => Date.now() } = {}) {
203
+ const heartbeatAt = lock?.heartbeatAt ? Date.parse(lock.heartbeatAt) : NaN
204
+ if (Number.isFinite(heartbeatAt) && now() - heartbeatAt < staleMs) return true
205
+ return pidIsAlive(lock?.pid)
206
+ }
207
+
208
+
209
+ export function isTerminalNoFollowup(decision) {
210
+ const frontier = decision?.goal_frontier_projection
211
+ const terminal = frontier?.terminal_state
212
+ const completeness = frontier?.source_completeness
213
+ return Boolean(
214
+ decision?.should_run === false &&
215
+ decision?.effective_action === "terminal_no_followup" &&
216
+ terminal?.schema_version === TERMINAL_STATE_SCHEMA_VERSION &&
217
+ terminal?.kind === "no_followup" &&
218
+ terminal?.derived === true &&
219
+ terminal?.source === "validated_goal_closure" &&
220
+ completeness?.schema_version === SOURCE_COMPLETENESS_SCHEMA_VERSION &&
221
+ completeness?.user_todos === "valid" &&
222
+ completeness?.agent_todos === "valid",
223
+ )
224
+ }
225
+
226
+
227
+ export function shouldRunNow(decision) {
228
+ // The scheduler hint is authoritative: a should_run=true decision that the
229
+ // scheduler classifies as a backoff (for example waiting for the user)
230
+ // must not prompt the model.
231
+ return decision?.scheduler_hint?.action === "run_now"
232
+ }
233
+
234
+
235
+ export function waitPlan(decision, state) {
236
+ const local = decision?.scheduler_hint?.unchanged_poll?.local_scheduler
237
+ if (!local || local === "stop") return { stop: true }
238
+ const token = decision?.scheduler_hint?.reset_policy?.reset_token || ""
239
+ const sameIdentity = token && token === state.schedulerToken
240
+ const unchangedPolls = sameIdentity ? Number(state.unchangedPolls || 0) : 0
241
+ const limit = Number.isInteger(local.unchanged_poll_limit) ? local.unchanged_poll_limit : null
242
+ if (limit !== null && unchangedPolls >= limit) return { stop: true }
243
+ const progression = Array.isArray(local.example_progression_minutes)
244
+ ? local.example_progression_minutes.filter((value) => Number(value) > 0)
245
+ : []
246
+ const fallback = Number(local.recommended_interval_minutes || DEFAULT_RETRY_MINUTES)
247
+ const minutes = progression.length
248
+ ? Number(progression[Math.min(unchangedPolls, progression.length - 1)])
249
+ : fallback
250
+ return {
251
+ stop: false,
252
+ minutes: Number.isFinite(minutes) && minutes > 0 ? minutes : DEFAULT_RETRY_MINUTES,
253
+ schedulerToken: token,
254
+ unchangedPolls: unchangedPolls + 1,
255
+ }
256
+ }
257
+
258
+
259
+ export function retryPlan(decision, state) {
260
+ // Fail-closed quota probe retry with a bounded backoff. Resets on success.
261
+ const count = Number(state.probeRetryCount || 0)
262
+ const minutes = Math.min(DEFAULT_RETRY_MINUTES * 2 ** Math.min(count, 4), MAX_RETRY_MINUTES)
263
+ return {
264
+ minutes,
265
+ probeRetryCount: count + 1,
266
+ }
267
+ }
268
+
269
+
270
+ export function turnVerdict(messages, sentPromptIds, { fallbackPromptCreated } = {}) {
271
+ // A turn completes only when an assistant message finished at or after the
272
+ // newest user message (regardless of who sent it) and that message is not
273
+ // an intermediate tool-call step. User input never pauses the loop: the
274
+ // model answers it, then quota gating continues as usual.
275
+ //
276
+ // Long tool-heavy turns can fill the newest message page entirely with
277
+ // assistant messages, so the page may contain no user message at all. The
278
+ // caller supplies fallbackPromptCreated (the worker's last prompt time) to
279
+ // bound the verdict in that case; a user message on the page always wins.
280
+ let newestUser = null
281
+ let newestAssistant = null
282
+ for (const message of messages || []) {
283
+ const type = message?.type
284
+ const created = Number(message?.time?.created)
285
+ if (type === "user") {
286
+ if (!newestUser || created > newestUser.time.created) {
287
+ newestUser = { id: message?.id, time: { created }, text: String(message?.text || "").slice(0, 120) }
288
+ }
289
+ }
290
+ if (type === "assistant") {
291
+ const completed = Number(message?.time?.completed)
292
+ if (!newestAssistant || created > newestAssistant.time.created) {
293
+ newestAssistant = { time: { created, completed }, finish: message?.finish }
294
+ }
295
+ }
296
+ }
297
+ const boundaryCreated = newestUser
298
+ ? newestUser.time.created
299
+ : Number(fallbackPromptCreated)
300
+ if (!Number.isFinite(boundaryCreated)) return { completed: false, waiting: true }
301
+ const finished = Boolean(newestAssistant) && Number.isFinite(newestAssistant.time.completed)
302
+ const terminalStep = finished && newestAssistant.finish !== "tool-calls"
303
+ const afterPrompt = finished && newestAssistant.time.completed >= boundaryCreated
304
+ const completed = terminalStep && afterPrompt
305
+ return {
306
+ completed,
307
+ waiting: !completed,
308
+ }
309
+ }
310
+
311
+
312
+ export function buildQuotaArgs(state) {
313
+ const args = []
314
+ if (state.registryPath) args.push("--registry", state.registryPath)
315
+ args.push(
316
+ "--format",
317
+ "json",
318
+ "quota",
319
+ "should-run",
320
+ "--goal-id",
321
+ state.goalId,
322
+ "--runtime-profile",
323
+ "generic_cli",
324
+ "--include-detail",
325
+ "scheduler",
326
+ "--record-host-poll",
327
+ )
328
+ if (state.agentId) args.push("--agent-id", state.agentId)
329
+ for (const capability of state.availableCapabilities || []) {
330
+ args.push("--available-capability", capability)
331
+ }
332
+ return args
333
+ }
334
+
335
+
336
+ export function parseExecFailure(error) {
337
+ // opencode2 api reports HTTP failures through a non-zero exit code and a
338
+ // stderr payload. Normalize the most useful part.
339
+ const stderr = String(error?.stderr || "")
340
+ let status = null
341
+ const statusMatch = /status[ =:]+(\d{3})/i.exec(stderr)
342
+ if (statusMatch) status = Number(statusMatch[1])
343
+ const messageMatch = /(?:error|message)[ =:]+"?([^"\n]{0,180})/i.exec(stderr)
344
+ return {
345
+ code: error?.code || null,
346
+ status,
347
+ stderr: stderr.slice(0, 400),
348
+ message: messageMatch ? messageMatch[1] : stderr.split("\n")[0]?.slice(0, 180) || String(error?.message || error),
349
+ }
350
+ }
351
+
352
+
353
+ export function createApiTransport({ bin = process.env.OPENCODE2_BIN || "opencode2", execFileImpl = execFile } = {}) {
354
+ const call = async (method, apiPath, body = null, timeoutMs = 60_000) => {
355
+ const args = ["api", method, apiPath]
356
+ if (body !== null) args.push("-d", JSON.stringify(body))
357
+ const { stdout } = await execFileImpl(bin, args, {
358
+ timeout: timeoutMs,
359
+ maxBuffer: 32 * 1024 * 1024,
360
+ })
361
+ return JSON.parse(stdout || "{}")
362
+ }
363
+ return {
364
+ async createSession(directory) {
365
+ const response = await call("post", "/api/session", { directory })
366
+ return response?.data?.id || null
367
+ },
368
+ async sessionExists(sessionID) {
369
+ try {
370
+ await call("get", `/api/session/${sessionID}`)
371
+ return true
372
+ } catch (error) {
373
+ const failure = parseExecFailure(error)
374
+ if (failure.status === 404) return false
375
+ throw error
376
+ }
377
+ },
378
+ async listMessages(sessionID) {
379
+ // Bounded newest-first page: long sessions grow unbounded message
380
+ // lists, and the turn verdict only needs the recent tail.
381
+ try {
382
+ const response = await call("get", `/api/session/${sessionID}/message?limit=30&order=desc`)
383
+ return response?.data || []
384
+ } catch (error) {
385
+ if (!String(error?.message || "").includes("JSON")) throw error
386
+ // Some hosts truncate oversized payloads; a smaller tail is enough
387
+ // for the completion verdict.
388
+ const response = await call("get", `/api/session/${sessionID}/message?limit=5&order=desc`)
389
+ return response?.data || []
390
+ }
391
+ },
392
+ async sessionInfo(sessionID) {
393
+ const response = await call("get", `/api/session/${sessionID}`)
394
+ return response?.data || null
395
+ },
396
+ async waitForSession(sessionID, { timeoutMs = 60_000 } = {}) {
397
+ try {
398
+ await call("post", `/api/session/${sessionID}/wait`, null, timeoutMs)
399
+ return true
400
+ } catch (error) {
401
+ // A chunk timeout means the turn is still running; the caller
402
+ // verifies the message tail and re-issues the wait.
403
+ if (error?.killed === true || error?.code === "ETIMEDOUT") return false
404
+ throw error
405
+ }
406
+ },
407
+ async sendPrompt(sessionID, text) {
408
+ const response = await call("post", `/api/session/${sessionID}/prompt`, {
409
+ text,
410
+ delivery: "queue",
411
+ })
412
+ return response?.data?.id || null
413
+ },
414
+ async sendSynthetic(sessionID, text) {
415
+ const response = await call("post", `/api/session/${sessionID}/synthetic`, { text })
416
+ return response?.data?.id || null
417
+ },
418
+ }
419
+ }
420
+
421
+
422
+ export function createQuotaProbe({ bin = process.env.LOOPX_BIN || "loopx", execFileImpl = execFile } = {}) {
423
+ return async function probe(state, { directory }) {
424
+ const { stdout } = await runQuotaProbe(state, directory, bin, execFileImpl)
425
+ const decision = JSON.parse(stdout || "{}")
426
+ if (!decision || typeof decision !== "object" || Array.isArray(decision)) {
427
+ throw new Error("loopx quota should-run returned a non-object payload")
428
+ }
429
+ return decision
430
+ }
431
+ }
432
+
433
+
434
+ async function runQuotaProbe(state, directory, bin, execFileImpl) {
435
+ try {
436
+ return await execFileImpl(bin, buildQuotaArgs(state), {
437
+ cwd: state.directory || directory,
438
+ timeout: 30_000,
439
+ maxBuffer: 4 * 1024 * 1024,
440
+ })
441
+ } catch (error) {
442
+ // A denied or gated decision exits non-zero but still carries the
443
+ // authoritative JSON payload on stdout. Only fail closed when the
444
+ // payload itself is unavailable.
445
+ if (typeof error?.stdout === "string" && error.stdout.trim()) {
446
+ return { stdout: error.stdout }
447
+ }
448
+ throw error
449
+ }
450
+ }
451
+
452
+
453
+ export function sleep(ms) {
454
+ return new Promise((resolve) => setTimeout(resolve, ms))
455
+ }
456
+
457
+
458
+ export async function waitForTurn({
459
+ transport,
460
+ sessionID,
461
+ state,
462
+ sentPromptIds,
463
+ pollMs = MESSAGE_POLL_MS,
464
+ timeoutMs = DEFAULT_TURN_TIMEOUT_MS,
465
+ stallMs = 30 * 60 * 1000,
466
+ holdMs = 15 * 60 * 1000,
467
+ waitChunkMs = 60_000,
468
+ shouldPause = async () => false,
469
+ }) {
470
+ // Block on the session wait endpoint (resolves at idle) in bounded chunks,
471
+ // then verify the message tail. Intermediate tool-call steps complete their
472
+ // messages without ending the turn, so the verdict requires a non-tool-call
473
+ // final message after the newest own prompt. A session whose message
474
+ // activity stops moving is reported as stalled instead of blocking until
475
+ // the turn budget. Session updated timestamps are not a reliable liveness
476
+ // signal in every build, so stall detection keys on message activity.
477
+ //
478
+ // A user pause (for example ESC in the host TUI) also stops message
479
+ // activity mid-turn. Before escalating to a permanent stall, the worker
480
+ // returns a quiet hold once the hold window passes; the main loop then
481
+ // waits without prompting and re-enters with the persisted activity time,
482
+ // so a paused session is never prompted and the stall window still
483
+ // escalates from the true last activity.
484
+ const startedAt = Date.now()
485
+ let lastActivityAt = Number(state?.pendingTurnActivityAt) || Date.now()
486
+ const fallbackPromptCreated = Number(
487
+ state?.lastPromptAt ?? state?.startedAtMs ?? Date.now()
488
+ )
489
+ while (true) {
490
+ if (await shouldPause(state)) return { kind: "paused_state" }
491
+ await transport.waitForSession(sessionID, { timeoutMs: waitChunkMs })
492
+ const messages = await transport.listMessages(sessionID)
493
+ const activity = latestMessageActivity(messages)
494
+ if (activity !== null && activity > lastActivityAt) lastActivityAt = activity
495
+ const quietForMs = Date.now() - lastActivityAt
496
+ if (quietForMs > stallMs) {
497
+ return { kind: "session_stalled", elapsedMs: Date.now() - startedAt }
498
+ }
499
+ if (quietForMs > holdMs) {
500
+ return { kind: "turn_hold", activityAt: lastActivityAt, quietForMs }
501
+ }
502
+ const verdict = turnVerdict(messages, sentPromptIds, { fallbackPromptCreated })
503
+ if (verdict.completed) return { kind: "completed", ...verdict }
504
+ if (Date.now() - startedAt > timeoutMs) {
505
+ return {
506
+ kind: "turn_timeout",
507
+ elapsedMs: Date.now() - startedAt,
508
+ lastMessageCount: messages.length,
509
+ }
510
+ }
511
+ await sleep(pollMs)
512
+ }
513
+ }
514
+
515
+
516
+ export function latestMessageActivity(messages) {
517
+ let latest = null
518
+ for (const message of messages || []) {
519
+ const created = Number(message?.time?.created)
520
+ const completed = Number(message?.time?.completed)
521
+ for (const candidate of [created, completed]) {
522
+ if (Number.isFinite(candidate) && (latest === null || candidate > latest)) {
523
+ latest = candidate
524
+ }
525
+ }
526
+ }
527
+ return latest
528
+ }
529
+
530
+
531
+ export async function runWorker({
532
+ goalId,
533
+ directory,
534
+ agentId = "",
535
+ registryPath = "",
536
+ availableCapabilities = [],
537
+ sessionId = null,
538
+ taskBody = null,
539
+ maxTurns = DEFAULT_MAX_TURNS,
540
+ maxDurationMs = DEFAULT_MAX_DURATION_MS,
541
+ forceResume = false,
542
+ waitTurnOptions = {},
543
+ log = async () => {},
544
+ stateStore = createStateStore(),
545
+ transport = null,
546
+ quotaProbe = null,
547
+ sleepImpl = sleep,
548
+ } = {}) {
549
+ if (!goalId) throw new Error("--goal-id is required")
550
+ if (!directory) throw new Error("--directory is required")
551
+ const effectiveTransport = transport || createApiTransport()
552
+ const effectiveProbe = quotaProbe || createQuotaProbe()
553
+
554
+ const lease = await stateStore.acquireLock(goalId)
555
+ if (!lease.acquired) {
556
+ await log("warn", "worker lease is held; refusing to start a second driver", {
557
+ goalId,
558
+ ...lease.holder,
559
+ })
560
+ return { kind: "lease_held", holder: lease.holder }
561
+ }
562
+ try {
563
+ return await runWithLease({
564
+ goalId,
565
+ directory,
566
+ agentId,
567
+ registryPath,
568
+ availableCapabilities,
569
+ sessionId,
570
+ taskBody,
571
+ maxTurns,
572
+ maxDurationMs,
573
+ forceResume,
574
+ waitTurnOptions,
575
+ log,
576
+ stateStore,
577
+ transport: effectiveTransport,
578
+ quotaProbe: effectiveProbe,
579
+ sleepImpl,
580
+ })
581
+ } finally {
582
+ await lease.release()
583
+ }
584
+ }
585
+
586
+
587
+ async function runWithLease({
588
+ goalId,
589
+ directory,
590
+ agentId,
591
+ registryPath,
592
+ availableCapabilities,
593
+ sessionId,
594
+ taskBody,
595
+ maxTurns,
596
+ maxDurationMs,
597
+ forceResume,
598
+ waitTurnOptions,
599
+ log,
600
+ stateStore,
601
+ transport,
602
+ quotaProbe,
603
+ sleepImpl,
604
+ }) {
605
+ let state = await stateStore.read(goalId)
606
+ const startedAt = Date.now()
607
+
608
+ if (!state) {
609
+ if (!sessionId && !taskBody) {
610
+ throw new Error(
611
+ "first run requires --task-body (or an existing --session-id); no worker state exists for this goal",
612
+ )
613
+ }
614
+ state = await stateStore.write(goalId, {
615
+ directory,
616
+ agentId: String(agentId || ""),
617
+ registryPath: String(registryPath || ""),
618
+ availableCapabilities: Array.isArray(availableCapabilities)
619
+ ? availableCapabilities.map(String)
620
+ : [],
621
+ sessionID: sessionId ? String(sessionId) : "",
622
+ autoResume: true,
623
+ phase: "starting",
624
+ schedulerToken: "",
625
+ unchangedPolls: 0,
626
+ probeRetryCount: 0,
627
+ turnCount: 0,
628
+ pendingTurn: false,
629
+ startedAt: new Date().toISOString(),
630
+ })
631
+ }
632
+
633
+ if (state.autoResume === false && !forceResume) {
634
+ await log("info", "worker state is paused; pass --force-resume to restart", { goalId })
635
+ return { kind: "paused_state" }
636
+ }
637
+ if (state.autoResume === false && forceResume) {
638
+ state = await stateStore.write(goalId, {
639
+ ...state,
640
+ autoResume: true,
641
+ lastPausedReason: "",
642
+ pausedReason: "",
643
+ })
644
+ await log("info", "worker state resumed with --force-resume", { goalId })
645
+ }
646
+ if (!Number.isFinite(Number(state.lastPromptAt))) {
647
+ const migratedAt = Date.parse(state.startedAt) || Date.now()
648
+ state = await stateStore.write(goalId, { ...state, lastPromptAt: migratedAt })
649
+ await log("info", "migrated worker state with a lastPromptAt boundary", {
650
+ goalId,
651
+ lastPromptAt: migratedAt,
652
+ })
653
+ }
654
+
655
+ const directoryNow = directory || state.directory
656
+
657
+ let sessionID = state.sessionID || sessionId
658
+ if (!sessionID) {
659
+ sessionID = await transport.createSession(directoryNow)
660
+ if (!sessionID) throw new Error("opencode2 session creation returned no id")
661
+ state = await stateStore.write(goalId, { ...state, sessionID })
662
+ await log("info", "created OpenCode 2 session", { goalId, sessionID })
663
+ }
664
+ if (!(await transport.sessionExists(sessionID))) {
665
+ state = await stateStore.write(goalId, {
666
+ ...state,
667
+ phase: "session_deleted",
668
+ pausedReason: "opencode2_session_deleted",
669
+ })
670
+ await log("error", "bound OpenCode 2 session no longer exists", { goalId, sessionID })
671
+ return { kind: "session_deleted", sessionID }
672
+ }
673
+
674
+ if (state.turnCount === 0 && taskBody) {
675
+ await transport.sendPrompt(sessionID, taskBody)
676
+ state = await stateStore.write(goalId, {
677
+ ...state,
678
+ turnCount: 1,
679
+ pendingTurn: true,
680
+ lastPromptAt: Date.now(),
681
+ pendingTurnActivityAt: Date.now(),
682
+ phase: "working",
683
+ })
684
+ await log("info", "prompted the session with the task body", { goalId, sessionID })
685
+ }
686
+
687
+ while (true) {
688
+ if (Date.now() - startedAt > maxDurationMs) {
689
+ state = await stateStore.write(goalId, { ...state, phase: "duration_limit" })
690
+ await transport.sendSynthetic(sessionID, DURATION_LIMIT_NOTICE)
691
+ await log("info", "stopped after the duration budget", { goalId })
692
+ return { kind: "duration_limit" }
693
+ }
694
+ if (!state.pendingTurn && state.turnCount >= maxTurns) {
695
+ state = await stateStore.write(goalId, { ...state, phase: "turn_limit" })
696
+ await transport.sendSynthetic(sessionID, TURN_LIMIT_NOTICE)
697
+ await log("info", "stopped after the turn budget", { goalId })
698
+ return { kind: "turn_limit" }
699
+ }
700
+
701
+ if (state.pendingTurn) {
702
+ const waitOutcome = await waitForTurn({
703
+ transport,
704
+ sessionID,
705
+ state,
706
+ sentPromptIds: state.sentPromptIds || [],
707
+ ...waitTurnOptions,
708
+ shouldPause: async () => {
709
+ const stored = await stateStore.read(goalId)
710
+ return Boolean(stored && stored.autoResume === false)
711
+ },
712
+ })
713
+ if (waitOutcome.kind === "turn_hold") {
714
+ const wasHolding = state.phase === "session_paused_hold"
715
+ state = await stateStore.write(goalId, {
716
+ ...state,
717
+ phase: "session_paused_hold",
718
+ pendingTurnActivityAt: Number(waitOutcome.activityAt) || state.pendingTurnActivityAt,
719
+ pausedReason: "session_paused_hold",
720
+ })
721
+ if (!wasHolding) {
722
+ await transport.sendSynthetic(
723
+ sessionID,
724
+ "The session appears paused. The LoopX worker holds quietly and resumes automatically when the session continues.",
725
+ )
726
+ await log("info", "pending turn went quiet; holding without prompting", {
727
+ goalId,
728
+ sessionID,
729
+ quietForMs: waitOutcome.quietForMs,
730
+ })
731
+ }
732
+ await sleepImpl(5 * 60_000)
733
+ continue
734
+ }
735
+ if (waitOutcome.kind === "paused_state") {
736
+ await log("info", "worker state was paused while waiting for the turn", { goalId, sessionID })
737
+ return { kind: "paused_state" }
738
+ }
739
+ if (waitOutcome.kind === "session_stalled") {
740
+ state = await stateStore.write(goalId, {
741
+ ...state,
742
+ phase: "session_stalled",
743
+ autoResume: false,
744
+ pausedReason: "session_stalled",
745
+ })
746
+ await transport.sendSynthetic(
747
+ sessionID,
748
+ "LoopX paused this goal because the OpenCode session stopped making progress.",
749
+ )
750
+ await log("warn", "session stalled; worker paused", { goalId, sessionID })
751
+ return { kind: "session_stalled" }
752
+ }
753
+ if (waitOutcome.kind === "turn_timeout") {
754
+ state = await stateStore.write(goalId, {
755
+ ...state,
756
+ phase: "turn_timeout",
757
+ autoResume: false,
758
+ pausedReason: "turn_timeout",
759
+ })
760
+ await transport.sendSynthetic(
761
+ sessionID,
762
+ "LoopX paused this goal because the last OpenCode turn exceeded the wait budget.",
763
+ )
764
+ await log("warn", "turn wait budget exhausted", { goalId, sessionID })
765
+ return { kind: "turn_timeout" }
766
+ }
767
+
768
+ state = await stateStore.write(goalId, { ...state, pendingTurn: false })
769
+ if (state.turnCount >= maxTurns) {
770
+ state = await stateStore.write(goalId, { ...state, phase: "turn_limit" })
771
+ await transport.sendSynthetic(sessionID, TURN_LIMIT_NOTICE)
772
+ await log("info", "stopped after the turn budget", { goalId, sessionID })
773
+ return { kind: "turn_limit" }
774
+ }
775
+ }
776
+
777
+ state = await stateStore.read(goalId)
778
+ if (!state || state.autoResume === false) {
779
+ return { kind: "paused_state" }
780
+ }
781
+
782
+ let decision
783
+ try {
784
+ decision = await quotaProbe(state, { directory: directoryNow })
785
+ state = await stateStore.write(goalId, {
786
+ ...state,
787
+ probeRetryCount: 0,
788
+ lastPollAt: new Date().toISOString(),
789
+ })
790
+ } catch (error) {
791
+ const plan = retryPlan(decision, state)
792
+ state = await stateStore.write(goalId, {
793
+ ...state,
794
+ phase: "quota_probe_retry",
795
+ probeRetryCount: plan.probeRetryCount,
796
+ lastPollAt: new Date().toISOString(),
797
+ })
798
+ await log("warn", "LoopX quota probe failed closed; scheduling a bounded retry", {
799
+ goalId,
800
+ error: String(error?.message || error).slice(0, 200),
801
+ retryMinutes: plan.minutes,
802
+ })
803
+ await sleepImpl(plan.minutes * 60_000)
804
+ continue
805
+ }
806
+
807
+ if (decision?.status === "goal_not_found" || decision?.state === "unknown") {
808
+ state = await stateStore.write(goalId, {
809
+ ...state,
810
+ phase: "goal_gone",
811
+ autoResume: false,
812
+ pausedReason: "goal_not_found",
813
+ })
814
+ await log("info", "goal is no longer registered; worker finished", { goalId, sessionID })
815
+ return { kind: "goal_gone" }
816
+ }
817
+
818
+ if (isTerminalNoFollowup(decision)) {
819
+ // Validated closure means the CURRENT work is done, not that the loop
820
+ // should end. Stand by and keep polling so new work resumes the
821
+ // session automatically instead of pausing after one task.
822
+ const standbyPolls = Number(state.standbyPolls || 0) + 1
823
+ state = await stateStore.write(goalId, {
824
+ ...state,
825
+ phase: "standby",
826
+ schedulerToken: "",
827
+ unchangedPolls: 0,
828
+ standbyPolls,
829
+ })
830
+ if (standbyPolls === 1) {
831
+ await transport.sendSynthetic(sessionID, TERMINAL_NOTICE)
832
+ }
833
+ await log("info", "LoopX validated terminal closure; standing by for new work", {
834
+ goalId,
835
+ sessionID,
836
+ standbyPolls,
837
+ })
838
+ await sleepImpl(STANDBY_MINUTES * 60_000)
839
+ continue
840
+ }
841
+
842
+ if (shouldRunNow(decision)) {
843
+ const wasStandby = state.phase === "standby"
844
+ state = await stateStore.write(goalId, {
845
+ ...state,
846
+ phase: "working",
847
+ schedulerToken: "",
848
+ unchangedPolls: 0,
849
+ standbyPolls: 0,
850
+ })
851
+ if (wasStandby) {
852
+ await log("info", "new work appeared; resuming from standby", { goalId, sessionID })
853
+ }
854
+ await transport.sendPrompt(sessionID, continuationPrompt(directoryNow))
855
+ state = await stateStore.write(goalId, {
856
+ ...state,
857
+ pendingTurn: true,
858
+ lastPromptAt: Date.now(),
859
+ pendingTurnActivityAt: Date.now(),
860
+ turnCount: Number(state.turnCount || 0) + 1,
861
+ })
862
+ await log("info", "quota granted another turn", { goalId, sessionID, turnCount: state.turnCount })
863
+ continue
864
+ }
865
+
866
+ const wait = waitPlan(decision, state)
867
+ if (wait.stop) {
868
+ state = await stateStore.write(goalId, {
869
+ ...state,
870
+ phase: "limit_stopped",
871
+ autoResume: false,
872
+ pausedReason: "unchanged_poll_limit",
873
+ })
874
+ await transport.sendSynthetic(sessionID, LIMIT_NOTICE)
875
+ await log("info", "unchanged-poll limit stopped the worker", { goalId, sessionID })
876
+ return { kind: "limit_stopped" }
877
+ }
878
+ state = await stateStore.write(goalId, {
879
+ ...state,
880
+ phase: "waiting",
881
+ schedulerToken: wait.schedulerToken,
882
+ unchangedPolls: wait.unchangedPolls,
883
+ })
884
+ await log("info", "waiting before the next quota poll", {
885
+ goalId,
886
+ sessionID,
887
+ minutes: wait.minutes,
888
+ })
889
+ await sleepImpl(wait.minutes * 60_000)
890
+ }
891
+ }
892
+
893
+
894
+ export function parseWorkerArgs(argv) {
895
+ const args = { availableCapabilities: [] }
896
+ for (let index = 0; index < argv.length; index += 1) {
897
+ const token = argv[index]
898
+ const value = argv[index + 1]
899
+ const readValue = (name) => {
900
+ if (value === undefined || value === "") throw new Error(`${name} requires a value`)
901
+ index += 1
902
+ return value
903
+ }
904
+ switch (token) {
905
+ case "--goal-id":
906
+ args.goalId = readValue(token)
907
+ break
908
+ case "--directory":
909
+ args.directory = readValue(token)
910
+ break
911
+ case "--agent-id":
912
+ args.agentId = readValue(token)
913
+ break
914
+ case "--registry":
915
+ args.registryPath = readValue(token)
916
+ break
917
+ case "--session-id":
918
+ args.sessionId = readValue(token)
919
+ break
920
+ case "--task-body":
921
+ args.taskBody = readValue(token)
922
+ break
923
+ case "--capability":
924
+ args.availableCapabilities.push(readValue(token))
925
+ break
926
+ case "--max-turns": {
927
+ const parsed = Number(readValue(token))
928
+ if (!Number.isInteger(parsed) || parsed < 1) throw new Error("--max-turns must be a positive integer")
929
+ args.maxTurns = parsed
930
+ break
931
+ }
932
+ case "--max-duration-minutes": {
933
+ const parsed = Number(readValue(token))
934
+ if (!Number.isFinite(parsed) || parsed < 1) throw new Error("--max-duration-minutes must be a positive number")
935
+ args.maxDurationMs = Math.round(parsed * 60 * 1000)
936
+ break
937
+ }
938
+ case "--force-resume":
939
+ args.forceResume = true
940
+ break
941
+ case "--state-dir":
942
+ process.env.LOOPX_OPENCODE2_STATE_DIR = readValue(token)
943
+ break
944
+ case "--help":
945
+ args.help = true
946
+ break
947
+ default:
948
+ throw new Error(`unknown argument: ${token}`)
949
+ }
950
+ }
951
+ return args
952
+ }
953
+
954
+
955
+ export function renderResult(result) {
956
+ return JSON.stringify({ version: 1, operation: "opencode2_goal_worker", ok: true, result }, null, 2)
957
+ }
958
+
959
+
960
+ function isMainModule() {
961
+ // Compare realpaths: on macOS /var is a symlink to /private/var, so the
962
+ // argv[1] used to invoke the materialized worker can resolve differently
963
+ // from import.meta.url even though they name the same file. A mismatch
964
+ // silently skips main() and the CLI reports a fake success.
965
+ if (!process.argv[1]) return false
966
+ const argvPath = path.resolve(process.argv[1])
967
+ const modulePath = fileURLToPath(import.meta.url)
968
+ try {
969
+ return realpathSync(argvPath) === realpathSync(modulePath)
970
+ } catch {
971
+ return argvPath === modulePath
972
+ }
973
+ }
974
+
975
+
976
+ async function main() {
977
+ const args = parseWorkerArgs(process.argv.slice(2))
978
+ if (args.help || !args.goalId) {
979
+ process.stdout.write(
980
+ [
981
+ "LoopX OpenCode 2 goal worker",
982
+ "",
983
+ "usage: opencode2-goal-worker.mjs --goal-id <id> --directory <dir>",
984
+ " [--agent-id <id>] [--registry <path>] [--capability <name>]...",
985
+ " [--session-id <id>] [--task-body <text>] [--max-turns <n>]",
986
+ " [--max-duration-minutes <n>] [--force-resume] [--state-dir <dir>]",
987
+ "",
988
+ ].join("\n"),
989
+ )
990
+ return 0
991
+ }
992
+ const log = async (level, message, extra = {}) => {
993
+ process.stderr.write(`${new Date().toISOString()} ${level} ${message} ${JSON.stringify(extra)}\n`)
994
+ }
995
+ try {
996
+ const result = await runWorker({ ...args, log })
997
+ process.stdout.write(`${renderResult(result)}\n`)
998
+ return 0
999
+ } catch (error) {
1000
+ await log("error", "worker failed", { error: String(error?.stack || error) })
1001
+ process.stdout.write(
1002
+ `${JSON.stringify({
1003
+ version: 1,
1004
+ operation: "opencode2_goal_worker",
1005
+ ok: false,
1006
+ error: String(error?.message || error),
1007
+ })}\n`,
1008
+ )
1009
+ return 1
1010
+ }
1011
+ }
1012
+
1013
+
1014
+ if (isMainModule()) {
1015
+ main().then((code) => {
1016
+ process.exitCode = code
1017
+ })
1018
+ }