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,1393 @@
1
+ from __future__ import annotations
2
+
3
+ import contextlib
4
+ import json
5
+ import os
6
+ import sys
7
+ import tempfile
8
+ from pathlib import Path
9
+ from typing import Any
10
+
11
+ from .opencode_goal_mode import plugin_source, runtime_source
12
+ from .pi_goal_mode import extension_source as pi_extension_source
13
+ from .pi_goal_mode import runtime_source as pi_runtime_source
14
+ from .slash_commands import build_slash_command_catalog
15
+
16
+ SCHEMA_VERSION = "loopx_slash_command_install_v0"
17
+ MANAGED_MARKER_PREFIX = "<!-- loopx-managed-slash-command:v1"
18
+ LEGACY_UPGRADABLE_SIGNATURES = (
19
+ "loopx goal-mode setup (NOT Claude Code's built-in /goal)",
20
+ "The output is loopx control-plane SETUP",
21
+ "goalmode_cmd.py",
22
+ )
23
+ EXISTING_LOOPX_CAPABILITY_SKILL_SIGNATURES = (
24
+ "# LoopX PR Review",
25
+ "Run `loopx pr-review` first",
26
+ )
27
+ OPENCODE_GOAL_DEPENDENCIES = {
28
+ "@opencode-ai/plugin": ">=1.17.15 <2",
29
+ "opencode-goal-plugin": "0.7.0",
30
+ }
31
+
32
+
33
+ def _managed_marker(*, command: str, surface: str) -> str:
34
+ return f"{MANAGED_MARKER_PREFIX} command={command} surface={surface} -->"
35
+
36
+
37
+ def _front_matter(*, fields: dict[str, str]) -> str:
38
+ lines = ["---"]
39
+ for key, value in fields.items():
40
+ escaped = value.replace('"', '\\"')
41
+ lines.append(f'{key}: "{escaped}"')
42
+ lines.append("---")
43
+ return "\n".join(lines)
44
+
45
+
46
+ def _skill_body(
47
+ *,
48
+ command: str,
49
+ title: str,
50
+ description: str,
51
+ argument_hint: str,
52
+ instructions: list[str],
53
+ surface: str,
54
+ front_matter_name: str | None = None,
55
+ ) -> str:
56
+ fields = {
57
+ "description": description,
58
+ "argument-hint": argument_hint,
59
+ }
60
+ if front_matter_name:
61
+ fields = {"name": front_matter_name, **fields}
62
+ surface_label = "slash command" if surface == "claude-skills" else "explicit LoopX command skill"
63
+ return "\n\n".join(
64
+ [
65
+ _front_matter(fields=fields),
66
+ _managed_marker(command=command, surface=surface),
67
+ f"# {title}",
68
+ f"Treat this as the LoopX `{command}` {surface_label}.",
69
+ "\n".join(instructions),
70
+ "Keep public/private boundaries intact and do not perform external writes unless the active LoopX state or owner explicitly authorizes them.",
71
+ ]
72
+ ) + "\n"
73
+
74
+
75
+ def _openai_skill_metadata(*, command: str, display_name: str, short_description: str) -> str:
76
+ return "\n".join(
77
+ [
78
+ f"# {_managed_marker(command=command, surface='codex-skill-metadata')}",
79
+ "interface:",
80
+ f' display_name: "{display_name}"',
81
+ f' short_description: "{short_description}"',
82
+ "policy:",
83
+ " allow_implicit_invocation: false",
84
+ "",
85
+ ]
86
+ )
87
+
88
+
89
+ def _opencode_command_body(spec: dict[str, Any]) -> str:
90
+ return "\n\n".join(
91
+ [
92
+ _front_matter(
93
+ fields={
94
+ "description": str(spec["description"]),
95
+ "agent": "build",
96
+ }
97
+ ),
98
+ _managed_marker(command=str(spec["command"]), surface="opencode-command"),
99
+ f"Treat this as the LoopX `{spec['command']}` OpenCode command.",
100
+ (
101
+ "The exact current host is OpenCode. On OpenCode 1, pass "
102
+ "`--host-surface opencode` and use `loopx_goal_activate` from the "
103
+ "returned host-loop activation packet. On OpenCode 2, pass "
104
+ "`--host-surface opencode2` and run the returned "
105
+ "`loopx opencode2-goal-worker` command with the heartbeat packet values."
106
+ ),
107
+ "\n".join(str(item) for item in spec["instructions"]),
108
+ (
109
+ "Keep public/private boundaries intact and do not perform external "
110
+ "writes unless the active LoopX state or owner explicitly authorizes them."
111
+ ),
112
+ ]
113
+ ) + "\n"
114
+
115
+
116
+ def _loopx_start_goal_arguments_instruction(
117
+ *,
118
+ cli_bin: str,
119
+ host_surface: str | None,
120
+ ) -> str:
121
+ selected_host = host_surface or "<exact-current-host>"
122
+ instruction = (
123
+ "If arguments are present, pass the complete visible command arguments "
124
+ "unchanged as one value to "
125
+ f'`{cli_bin} start-goal --guided --project . --slash-command-arguments='
126
+ f'"<complete visible $ARGUMENTS>" --host-surface {selected_host}`. '
127
+ "The CLI, not the model, owns parsing supported leading switches and "
128
+ "preserving the remaining goal text. Never split or recompose the "
129
+ "arguments, and never infer a route from issue/PR wording or URLs."
130
+ )
131
+ if host_surface is None:
132
+ instruction += (
133
+ " If the host is unclear, omit the host flag once and follow the "
134
+ "returned host-surface selection gate."
135
+ )
136
+ return instruction
137
+
138
+
139
+ def _command_prompt_specs(*, cli_bin: str, include_legacy_aliases: bool) -> list[dict[str, Any]]:
140
+ specs: list[dict[str, Any]] = [
141
+ {
142
+ "command": "/loopx",
143
+ "name": "loopx",
144
+ "description": "Inspect LoopX state, or start concrete project work when arguments are provided.",
145
+ "argument_hint": "[--fine-grained] [--capability-route issue-fix] [task text]",
146
+ "instructions": [
147
+ "Visible command arguments: `$ARGUMENTS`.",
148
+ "Identify the exact current host surface (codex-app, codex-app-ssh, codex-ide-plugin, codex-cli-tui, opencode, opencode2, traex-cli, pi, gemini-cli, cursor-agent, deepseek-harness, or ark-managed-agent).",
149
+ _loopx_start_goal_arguments_instruction(
150
+ cli_bin=cli_bin,
151
+ host_surface=None,
152
+ ),
153
+ "Treat the returned `ordered_steps` and `goal_start_contract` as authoritative. Follow their identity, capability-route, Todo, writeback, host-loop, quota, and stop/gate rules before substantive work; do not reconstruct those rules from skill memory.",
154
+ "If the packet exposes a goal-selection gate, rerun one exact choice before any mutation.",
155
+ f"If arguments are empty and the host already identifies an active LoopX goal, follow its exact CLI `interaction_contract` or quota command first; otherwise inspect `{cli_bin} status` and `{cli_bin} bootstrap-command-pack --project .` before changing files.",
156
+ "If this session cannot mutate the host loop surface, surface the exact pasteable gate instead of claiming autonomous setup.",
157
+ ],
158
+ },
159
+ {
160
+ "command": "/loopx-global-summary",
161
+ "name": "loopx-global-summary",
162
+ "description": "Read the compact global LoopX progress digest.",
163
+ "argument_hint": "[optional focus]",
164
+ "instructions": [
165
+ "Visible command arguments: `$ARGUMENTS`.",
166
+ f"Run `{cli_bin} global-summary` first and summarize visible projects, gates, monitor status, and next safe actions.",
167
+ "This command is read-only unless the user explicitly asks for a state update.",
168
+ ],
169
+ },
170
+ {
171
+ "command": "/loopx-global-gates",
172
+ "name": "loopx-global-gates",
173
+ "description": "List open LoopX user/controller gates and what each blocks.",
174
+ "argument_hint": "[optional focus]",
175
+ "instructions": [
176
+ "Visible command arguments: `$ARGUMENTS`.",
177
+ f"Run `{cli_bin} global-gates` first and summarize formal open gates, "
178
+ "blocked todo or goal scope, owner routing, and exact next questions.",
179
+ "This command is read-only unless the user explicitly asks for a state update.",
180
+ ],
181
+ },
182
+ {
183
+ "command": "/loopx-global-todos",
184
+ "name": "loopx-global-todos",
185
+ "description": "List runnable, blocked, deferred-ready, and review LoopX todos across visible projects.",
186
+ "argument_hint": "[optional focus]",
187
+ "instructions": [
188
+ "Visible command arguments: `$ARGUMENTS`.",
189
+ f"Run `{cli_bin} global-todos` first and summarize prioritized ownership and structured readiness across visible projects without mutating state.",
190
+ "This command is read-only unless the user explicitly asks for a state update.",
191
+ ],
192
+ },
193
+ {
194
+ "command": "/loopx-global-risks",
195
+ "name": "loopx-global-risks",
196
+ "description": "Show stale LoopX runs, boundary risks, failing checks, and rollback candidates.",
197
+ "argument_hint": "[optional focus]",
198
+ "instructions": [
199
+ "Visible command arguments: `$ARGUMENTS`.",
200
+ f"Run `{cli_bin} global-risks` first and summarize structured stale runs, "
201
+ "boundary warnings, failing checks, and whether a formally evidenced "
202
+ "rollback candidate source is available, without mutating state.",
203
+ "This command is read-only unless the user explicitly asks for a state update.",
204
+ ],
205
+ },
206
+ {
207
+ "command": "/loopx-pr-review",
208
+ "name": "loopx-pr-review",
209
+ "description": "Run the LoopX PR-review packet first, then review selected PR groups with evidence.",
210
+ "argument_hint": "[--repo owner/repo] [--state open|merged|all] [--since ISO]",
211
+ "instructions": [
212
+ "Visible command arguments: `$ARGUMENTS`.",
213
+ "Use the installed `loopx-pr-review` skill when available.",
214
+ f"Run `{cli_bin} --format json pr-review $ARGUMENTS` first and keep `agent_response_contract.review_execution_contract`, `review_groups`, `pull_requests[].review_plan`, `pull_requests[].review_template`, and `pull_requests[].evidence_commands` visible.",
215
+ "Do not reconstruct the PR queue manually from ad hoc GitHub calls before reading the LoopX packet.",
216
+ "This command is read-only; do not comment, approve, merge, rerun CI, or spend quota unless separately authorized.",
217
+ ],
218
+ },
219
+ ]
220
+ if include_legacy_aliases:
221
+ legacy_specs = []
222
+ for canonical in specs:
223
+ name = canonical["name"]
224
+ if not str(name).startswith("loopx-global-"):
225
+ continue
226
+ legacy_name = str(name).replace("loopx-global-", "loop-global-", 1)
227
+ legacy_specs.append(
228
+ {
229
+ **canonical,
230
+ "command": "/" + legacy_name,
231
+ "name": legacy_name,
232
+ "description": canonical["description"] + " Legacy alias for the canonical /loopx-global-* command.",
233
+ }
234
+ )
235
+ specs.extend(legacy_specs)
236
+ return specs
237
+
238
+
239
+ def _command_skill_content(spec: dict[str, Any], *, surface: str) -> str:
240
+ return _skill_body(
241
+ command=str(spec["command"]),
242
+ title=f"LoopX {spec['command']}",
243
+ description=str(spec["description"]),
244
+ argument_hint=str(spec["argument_hint"]),
245
+ instructions=list(spec["instructions"]),
246
+ surface=surface,
247
+ front_matter_name=str(spec["name"]),
248
+ )
249
+
250
+
251
+ def materialize_loopx_entry_skill(
252
+ *,
253
+ skills_dir: Path,
254
+ execute: bool,
255
+ cli_bin: str = "loopx",
256
+ host_surface: str | None = None,
257
+ ) -> dict[str, Any]:
258
+ """Materialize the generated ``$loopx`` entry skill into a host skill root."""
259
+
260
+ if host_surface not in {None, "ark-managed-agent"}:
261
+ raise ValueError(f"unsupported fixed LoopX entry host surface: {host_surface}")
262
+ spec = next(
263
+ item
264
+ for item in _command_prompt_specs(
265
+ cli_bin=cli_bin,
266
+ include_legacy_aliases=False,
267
+ )
268
+ if item["name"] == "loopx"
269
+ )
270
+ if host_surface:
271
+ instructions = list(spec["instructions"])
272
+ instructions[1] = (
273
+ "This entry skill is installed for the exact current host "
274
+ f"`{host_surface}`; do not infer or substitute another host surface."
275
+ )
276
+ instructions[2] = _loopx_start_goal_arguments_instruction(
277
+ cli_bin=cli_bin,
278
+ host_surface=host_surface,
279
+ )
280
+ spec = {**spec, "instructions": instructions}
281
+ skill_path = skills_dir / "loopx" / "SKILL.md"
282
+ content = _command_skill_content(spec, surface="codex-skills")
283
+ return {
284
+ "skill_id": "loopx",
285
+ "path": str(skill_path),
286
+ "status": _target_status(skill_path, content, execute=execute),
287
+ }
288
+
289
+
290
+ def _is_legacy_upgradable_loopx_file(existing: str) -> bool:
291
+ return any(signature in existing for signature in LEGACY_UPGRADABLE_SIGNATURES)
292
+
293
+
294
+ def _is_existing_loopx_capability_skill(existing: str) -> bool:
295
+ return any(signature in existing for signature in EXISTING_LOOPX_CAPABILITY_SKILL_SIGNATURES)
296
+
297
+
298
+ def _target_status(path: Path, content: str, *, execute: bool) -> str:
299
+ if path.exists():
300
+ existing = path.read_text(encoding="utf-8")
301
+ if MANAGED_MARKER_PREFIX not in existing:
302
+ if _is_legacy_upgradable_loopx_file(existing):
303
+ if execute:
304
+ path.write_text(content, encoding="utf-8")
305
+ return "upgraded_legacy_managed"
306
+ if path.name == "SKILL.md" and _is_existing_loopx_capability_skill(existing):
307
+ return "preserved_existing_loopx_skill"
308
+ return "skipped_user_file"
309
+ if existing == content:
310
+ return "unchanged"
311
+ if execute:
312
+ path.write_text(content, encoding="utf-8")
313
+ return "updated"
314
+ if execute:
315
+ path.parent.mkdir(parents=True, exist_ok=True)
316
+ path.write_text(content, encoding="utf-8")
317
+ return "created" if execute else "would_create"
318
+
319
+
320
+ def _retire_managed_file(path: Path, *, execute: bool) -> str | None:
321
+ if not path.exists():
322
+ return None
323
+ existing = path.read_text(encoding="utf-8")
324
+ if MANAGED_MARKER_PREFIX not in existing:
325
+ return "skipped_user_file"
326
+ if execute:
327
+ path.unlink()
328
+ return "retired_managed_file" if execute else "would_retire_managed_file"
329
+
330
+
331
+ def _retire_status(path: Path, *, execute: bool) -> str:
332
+ return _retire_managed_file(path, execute=execute) or "absent"
333
+
334
+
335
+ def _codex_home(value: str | None = None) -> Path:
336
+ raw = value or os.environ.get("CODEX_HOME") or str(Path.home() / ".codex")
337
+ return Path(raw).expanduser()
338
+
339
+
340
+ def _claude_home(value: str | None = None) -> Path:
341
+ raw = value or os.environ.get("CLAUDE_HOME") or str(Path.home() / ".claude")
342
+ return Path(raw).expanduser()
343
+
344
+
345
+ def _gemini_home(value: str | None = None) -> Path:
346
+ """Gemini CLI reads user skills from GEMINI_HOME/skills (default ~/.gemini)."""
347
+ raw = value or os.environ.get("GEMINI_HOME") or str(Path.home() / ".gemini")
348
+ return Path(raw).expanduser()
349
+
350
+
351
+ def _cursor_home(value: str | None = None) -> Path:
352
+ """Cursor CLI reads MCP servers from CURSOR_HOME/mcp.json (default ~/.cursor)."""
353
+ raw = value or os.environ.get("CURSOR_HOME") or str(Path.home() / ".cursor")
354
+ return Path(raw).expanduser()
355
+
356
+
357
+ def _opencode_home(value: str | None = None) -> Path:
358
+ raw = value or os.environ.get("OPENCODE_CONFIG_DIR")
359
+ if not raw:
360
+ config_home = os.environ.get("XDG_CONFIG_HOME") or str(Path.home() / ".config")
361
+ raw = str(Path(config_home) / "opencode")
362
+ return Path(raw).expanduser()
363
+
364
+
365
+ def _strip_jsonc_comments(content: str) -> str:
366
+ output: list[str] = []
367
+ index = 0
368
+ in_string = False
369
+ escaped = False
370
+ while index < len(content):
371
+ char = content[index]
372
+ next_char = content[index + 1] if index + 1 < len(content) else ""
373
+ if in_string:
374
+ output.append(char)
375
+ if escaped:
376
+ escaped = False
377
+ elif char == "\\":
378
+ escaped = True
379
+ elif char == '"':
380
+ in_string = False
381
+ index += 1
382
+ continue
383
+ if char == '"':
384
+ in_string = True
385
+ output.append(char)
386
+ index += 1
387
+ continue
388
+ if char == "/" and next_char == "/":
389
+ output.extend((" ", " "))
390
+ index += 2
391
+ while index < len(content) and content[index] not in "\r\n":
392
+ output.append(" ")
393
+ index += 1
394
+ continue
395
+ if char == "/" and next_char == "*":
396
+ output.extend((" ", " "))
397
+ index += 2
398
+ while index < len(content):
399
+ if index + 1 < len(content) and content[index : index + 2] == "*/":
400
+ output.extend((" ", " "))
401
+ index += 2
402
+ break
403
+ output.append("\n" if content[index] == "\n" else " ")
404
+ index += 1
405
+ continue
406
+ output.append(char)
407
+ index += 1
408
+ return "".join(output)
409
+
410
+
411
+ def _strip_jsonc_trailing_commas(content: str) -> str:
412
+ output: list[str] = []
413
+ index = 0
414
+ in_string = False
415
+ escaped = False
416
+ while index < len(content):
417
+ char = content[index]
418
+ if in_string:
419
+ output.append(char)
420
+ if escaped:
421
+ escaped = False
422
+ elif char == "\\":
423
+ escaped = True
424
+ elif char == '"':
425
+ in_string = False
426
+ index += 1
427
+ continue
428
+ if char == '"':
429
+ in_string = True
430
+ output.append(char)
431
+ index += 1
432
+ continue
433
+ if char == ",":
434
+ lookahead = index + 1
435
+ while lookahead < len(content) and content[lookahead].isspace():
436
+ lookahead += 1
437
+ if lookahead < len(content) and content[lookahead] in "]}":
438
+ index += 1
439
+ continue
440
+ output.append(char)
441
+ index += 1
442
+ return "".join(output)
443
+
444
+
445
+ def _opencode_plugin_name(plugin: Any) -> str | None:
446
+ if isinstance(plugin, str):
447
+ return plugin
448
+ if (
449
+ isinstance(plugin, list)
450
+ and plugin
451
+ and isinstance(plugin[0], str)
452
+ ):
453
+ return plugin[0]
454
+ return None
455
+
456
+
457
+ def _opencode_direct_goal_plugin_conflicts(root: Path) -> tuple[list[str], list[str]]:
458
+ conflicts: list[str] = []
459
+ invalid: list[str] = []
460
+ goal_plugins = {
461
+ "opencode-goal-plugin",
462
+ "@heimoshuiyu/opencode-goal-plugin",
463
+ "@prevalentware/opencode-goal-plugin",
464
+ }
465
+ for name in ("opencode.json", "opencode.jsonc"):
466
+ path = root / name
467
+ if not path.exists():
468
+ continue
469
+ try:
470
+ content = path.read_text(encoding="utf-8")
471
+ if path.suffix == ".jsonc":
472
+ content = _strip_jsonc_trailing_commas(_strip_jsonc_comments(content))
473
+ payload = json.loads(content)
474
+ except (json.JSONDecodeError, OSError):
475
+ invalid.append(str(path))
476
+ continue
477
+ if not isinstance(payload, dict):
478
+ invalid.append(str(path))
479
+ continue
480
+ plugins = payload.get("plugin") or []
481
+ if isinstance(plugins, str):
482
+ plugins = [plugins]
483
+ if not isinstance(plugins, list):
484
+ invalid.append(str(path))
485
+ continue
486
+ plugin_names = [
487
+ name
488
+ for plugin in plugins
489
+ if (name := _opencode_plugin_name(plugin)) is not None
490
+ ]
491
+ if any(
492
+ plugin == package or plugin.startswith(f"{package}@")
493
+ for plugin in plugin_names
494
+ for package in goal_plugins
495
+ ):
496
+ conflicts.append(str(path))
497
+ return conflicts, invalid
498
+
499
+
500
+ def _target_package_dependencies(
501
+ path: Path,
502
+ dependencies: dict[str, str],
503
+ *,
504
+ execute: bool,
505
+ ) -> str:
506
+ if path.exists():
507
+ try:
508
+ payload = json.loads(path.read_text(encoding="utf-8"))
509
+ except (json.JSONDecodeError, OSError):
510
+ return "blocked_invalid_user_package_json"
511
+ if not isinstance(payload, dict):
512
+ return "blocked_invalid_user_package_json"
513
+ current = payload.get("dependencies")
514
+ if current is None:
515
+ current = {}
516
+ if not isinstance(current, dict):
517
+ return "blocked_invalid_user_package_json"
518
+ wanted = {**current, **dependencies}
519
+ if wanted == current:
520
+ return "unchanged"
521
+ payload["dependencies"] = wanted
522
+ status = "updated" if execute else "would_update"
523
+ else:
524
+ payload = {"private": True, "dependencies": dependencies}
525
+ status = "created" if execute else "would_create"
526
+ if execute:
527
+ path.parent.mkdir(parents=True, exist_ok=True)
528
+ path.write_text(
529
+ json.dumps(payload, indent=2) + "\n",
530
+ encoding="utf-8",
531
+ )
532
+ return status
533
+
534
+
535
+ def _normalize_surfaces(surfaces: list[str] | None) -> list[str]:
536
+ requested = surfaces or ["all"]
537
+ normalized: list[str] = []
538
+ for surface in requested:
539
+ if surface == "all":
540
+ candidates = ["codex", "claude-code", "opencode"]
541
+ elif surface == "codex":
542
+ candidates = ["codex"]
543
+ elif surface in {"codex-app", "codex-app-ssh", "codex-ide-plugin", "codex-ide", "codex-cli"}:
544
+ candidates = ["codex"]
545
+ elif surface in {"gemini-cli", "gemini-code"}:
546
+ candidates = ["gemini"]
547
+ elif surface in {"cursor-agent", "cursor-cli"}:
548
+ candidates = ["cursor"]
549
+ else:
550
+ candidates = [surface]
551
+ for candidate in candidates:
552
+ if candidate not in normalized:
553
+ normalized.append(candidate)
554
+ return normalized
555
+
556
+
557
+ CURSOR_MCP_KEY = "loopx"
558
+ CURSOR_MCP_MARKER_NAME = ".loopx-managed-mcp.json"
559
+ CURSOR_MCP_MARKER_SCHEMA_VERSION = "loopx_managed_cursor_mcp_v0"
560
+
561
+
562
+ def _loopx_mcp_command() -> tuple[str, str] | None:
563
+ """Interpreter and script for the LoopX stdio MCP server, or None.
564
+
565
+ Reuses the Claude adapter's provisioning so all surfaces share one server
566
+ and one `mcp` dependency. Returns None when `mcp` cannot be provisioned —
567
+ the caller records that instead of writing a config that would fail to
568
+ start.
569
+ """
570
+ try:
571
+ from loopx.claude_goal_mode.scripts import install as claude_install
572
+ except ImportError:
573
+ return None
574
+ try:
575
+ script = claude_install._p("mcp", "loopx_mcp.py")
576
+ # Provisioning narrates to stdout ("[deps] creating mcp venv …"), which
577
+ # would land in the middle of `--format json` and make the whole install
578
+ # result unparseable. The narration is still worth keeping — on stderr.
579
+ with contextlib.redirect_stdout(sys.stderr):
580
+ interpreter = claude_install.provision_mcp_python(dry=False)
581
+ except (OSError, ValueError):
582
+ # Provisioning shells out to venv and pip; a failure there means the
583
+ # surface is skipped, never that the whole install run dies.
584
+ return None
585
+ if not interpreter or not Path(script).exists():
586
+ return None
587
+ if not claude_install._has_mcp(interpreter):
588
+ return None
589
+ return str(interpreter), str(script)
590
+
591
+
592
+ def _cursor_mcp_marker_path(cursor_root: Path) -> Path:
593
+ """Sidecar recording the exact `mcp.json` entry LoopX last wrote.
594
+
595
+ Markdown surfaces carry `MANAGED_MARKER_PREFIX` inside the file, but
596
+ `mcp.json` belongs to Cursor's schema, so provenance is kept beside it
597
+ rather than smuggled into an entry the host has to parse.
598
+ """
599
+ return cursor_root / CURSOR_MCP_MARKER_NAME
600
+
601
+
602
+ def _read_cursor_mcp_marker(cursor_root: Path) -> dict[str, Any]:
603
+ try:
604
+ payload = json.loads(
605
+ _cursor_mcp_marker_path(cursor_root).read_text(encoding="utf-8")
606
+ )
607
+ except (OSError, json.JSONDecodeError):
608
+ return {}
609
+ if not isinstance(payload, dict):
610
+ return {}
611
+ servers = payload.get("servers")
612
+ return servers if isinstance(servers, dict) else {}
613
+
614
+
615
+ def _write_json_atomic(path: Path, payload: Any) -> None:
616
+ """Replace `path` in one step, so an interrupted run cannot truncate it."""
617
+ path.parent.mkdir(parents=True, exist_ok=True)
618
+ mode = path.stat().st_mode if path.exists() else None
619
+ handle, tmp_name = tempfile.mkstemp(dir=str(path.parent), prefix=f".{path.name}.")
620
+ tmp = Path(tmp_name)
621
+ try:
622
+ with os.fdopen(handle, "w", encoding="utf-8") as stream:
623
+ stream.write(json.dumps(payload, indent=2) + "\n")
624
+ if mode is not None:
625
+ os.chmod(tmp, mode & 0o7777)
626
+ os.replace(tmp, path)
627
+ except BaseException:
628
+ tmp.unlink(missing_ok=True)
629
+ raise
630
+
631
+
632
+ def _merge_cursor_mcp(cursor_root: Path, *, uninstall: bool, execute: bool) -> str:
633
+ """Add or remove the `loopx` entry in CURSOR_HOME/mcp.json — and nothing else.
634
+
635
+ `mcp.json` is a user-owned file that LoopX did not create, so every path
636
+ here fails closed: an unexpected shape is reported instead of normalized, a
637
+ same-name entry LoopX cannot prove it wrote is left alone, and uninstall
638
+ removes only an entry that still matches what LoopX recorded. Sharing the
639
+ `loopx` name with a user's own server is unlikely but cheap to survive;
640
+ silently replacing that server, or dropping their `mcpServers` list while
641
+ "merging", is not.
642
+ """
643
+ path = cursor_root / "mcp.json"
644
+ try:
645
+ raw = json.loads(path.read_text(encoding="utf-8")) if path.exists() else {}
646
+ except (OSError, json.JSONDecodeError):
647
+ return "blocked_invalid_cursor_mcp_json"
648
+ if not isinstance(raw, dict):
649
+ return "blocked_invalid_cursor_mcp_json"
650
+ servers = raw.get("mcpServers")
651
+ if servers is None:
652
+ servers = {}
653
+ if not isinstance(servers, dict):
654
+ # A list-valued or scalar `mcpServers` is not something to overwrite:
655
+ # either the file follows a shape LoopX does not understand, or it is
656
+ # damaged. Both are the user's to resolve.
657
+ return "blocked_invalid_cursor_mcp_json"
658
+
659
+ existing = servers.get(CURSOR_MCP_KEY)
660
+ recorded = _read_cursor_mcp_marker(cursor_root).get(CURSOR_MCP_KEY)
661
+ # Ownership is only demonstrable when the entry still matches what LoopX
662
+ # recorded writing. A hand-edited entry counts as the user's from then on.
663
+ loopx_owned = existing is not None and existing == recorded
664
+
665
+ # A marker that no longer describes reality is not just useless, it is
666
+ # dangerous: once the user deletes or edits the entry, the stale marker
667
+ # would still vouch for a future same-name entry and authorize deleting
668
+ # something LoopX never wrote. Ownership has to expire the moment it stops
669
+ # being demonstrable — and only in execute mode, so a dry run stays a
670
+ # read-only report.
671
+ if execute and recorded is not None and not loopx_owned:
672
+ _cursor_mcp_marker_path(cursor_root).unlink(missing_ok=True)
673
+
674
+ if uninstall:
675
+ if existing is None:
676
+ return "absent"
677
+ if not loopx_owned:
678
+ return "skipped_user_owned_mcp_entry"
679
+ if not execute:
680
+ return "would_retire"
681
+ servers.pop(CURSOR_MCP_KEY, None)
682
+ raw["mcpServers"] = servers
683
+ _write_json_atomic(path, raw)
684
+ _cursor_mcp_marker_path(cursor_root).unlink(missing_ok=True)
685
+ return "retired"
686
+
687
+ if existing is not None and not loopx_owned:
688
+ return "skipped_user_owned_mcp_entry"
689
+ if not execute:
690
+ # A dry run must not touch anything: provisioning `mcp` would create a
691
+ # venv on disk during a run whose only job is to report what would happen.
692
+ return "unchanged" if existing is not None else "would_write"
693
+ command = _loopx_mcp_command()
694
+ if command is None:
695
+ return "skipped_mcp_dependency_missing"
696
+ entry = {"command": command[0], "args": [command[1]]}
697
+ if existing == entry:
698
+ return "unchanged"
699
+ servers[CURSOR_MCP_KEY] = entry
700
+ raw["mcpServers"] = servers
701
+ _write_json_atomic(path, raw)
702
+ _write_json_atomic(
703
+ _cursor_mcp_marker_path(cursor_root),
704
+ {
705
+ "schema_version": CURSOR_MCP_MARKER_SCHEMA_VERSION,
706
+ "servers": {CURSOR_MCP_KEY: entry},
707
+ },
708
+ )
709
+ return "written"
710
+
711
+
712
+ def _pi_extension_path(project_root: Path) -> Path:
713
+ return project_root / ".pi" / "extensions" / "loopx-goal.ts"
714
+
715
+
716
+ def _pi_runtime_path(project_root: Path) -> Path:
717
+ return project_root / ".pi" / "extensions" / "pi-goal-loop-runtime.mjs"
718
+
719
+
720
+ def install_slash_commands(
721
+ *,
722
+ execute: bool,
723
+ uninstall: bool = False,
724
+ with_goal_bridge: bool = False,
725
+ surfaces: list[str] | None = None,
726
+ cli_bin: str = "loopx",
727
+ include_legacy_aliases: bool = True,
728
+ codex_home: str | None = None,
729
+ claude_home: str | None = None,
730
+ opencode_home: str | None = None,
731
+ gemini_home: str | None = None,
732
+ cursor_home: str | None = None,
733
+ pi_project: str | None = None,
734
+ ) -> dict[str, Any]:
735
+ specs = _command_prompt_specs(cli_bin=cli_bin, include_legacy_aliases=include_legacy_aliases)
736
+ effective_surfaces = _normalize_surfaces(surfaces)
737
+ codex_root = _codex_home(codex_home)
738
+ claude_root = _claude_home(claude_home)
739
+ opencode_root = _opencode_home(opencode_home)
740
+ gemini_root = _gemini_home(gemini_home)
741
+ cursor_root = _cursor_home(cursor_home)
742
+ pi_project_root = Path(pi_project or ".").expanduser().resolve()
743
+ installed: list[dict[str, Any]] = []
744
+
745
+ if with_goal_bridge and "opencode" not in effective_surfaces:
746
+ installed.append(
747
+ {
748
+ "surface": "opencode",
749
+ "host_surfaces": ["opencode"],
750
+ "mechanism": "opencode_goal_bridge",
751
+ "command": "/goal",
752
+ "path": None,
753
+ "status": "blocked_goal_bridge_requires_opencode_surface",
754
+ "invoke_as": [],
755
+ "reason": "Select --surface opencode when using --with-goal-bridge.",
756
+ }
757
+ )
758
+
759
+ if "codex" in effective_surfaces:
760
+ prompt_dir = codex_root / "prompts"
761
+ for spec in specs:
762
+ prompt_path = prompt_dir / f"{spec['name']}.md"
763
+ if uninstall:
764
+ retire_status = _retire_status(prompt_path, execute=execute)
765
+ installed.append(
766
+ {
767
+ "surface": "codex",
768
+ "host_surfaces": ["codex-cli", "codex-ide-plugin", "codex-app", "codex-app-ssh"],
769
+ "mechanism": "retired_codex_custom_prompt",
770
+ "command": spec["command"],
771
+ "path": str(prompt_path),
772
+ "status": retire_status,
773
+ "invoke_as": [],
774
+ }
775
+ )
776
+ continue
777
+ retire_status = _retire_managed_file(prompt_path, execute=execute)
778
+ if retire_status:
779
+ installed.append(
780
+ {
781
+ "surface": "codex",
782
+ "host_surfaces": ["codex-cli", "codex-ide-plugin", "codex-app", "codex-app-ssh"],
783
+ "mechanism": "retired_codex_custom_prompt",
784
+ "command": spec["command"],
785
+ "path": str(prompt_path),
786
+ "status": retire_status,
787
+ "invoke_as": [],
788
+ }
789
+ )
790
+
791
+ skill_dir = codex_root / "skills"
792
+ for spec in specs:
793
+ skill_path = skill_dir / str(spec["name"]) / "SKILL.md"
794
+ metadata_path = skill_path.parent / "agents" / "openai.yaml"
795
+ if uninstall:
796
+ skill_status = _retire_status(skill_path, execute=execute)
797
+ installed.append(
798
+ {
799
+ "surface": "codex",
800
+ "host_surfaces": ["codex-cli", "codex-ide-plugin", "codex-app", "codex-app-ssh"],
801
+ "mechanism": "codex_explicit_skills",
802
+ "command": spec["command"],
803
+ "path": str(skill_path),
804
+ "status": skill_status,
805
+ "invoke_as": [f"${spec['name']}", "/skills"],
806
+ }
807
+ )
808
+ metadata_status = _retire_status(metadata_path, execute=execute)
809
+ installed.append(
810
+ {
811
+ "surface": "codex",
812
+ "host_surfaces": ["codex-cli", "codex-ide-plugin", "codex-app", "codex-app-ssh"],
813
+ "mechanism": "codex_skill_openai_metadata",
814
+ "command": spec["command"],
815
+ "path": str(metadata_path),
816
+ "status": metadata_status,
817
+ "invoke_as": [f"${spec['name']}", "/skills"],
818
+ }
819
+ )
820
+ continue
821
+ skill_content = _command_skill_content(spec, surface="codex-skills")
822
+ skill_status = _target_status(skill_path, skill_content, execute=execute)
823
+ installed.append(
824
+ {
825
+ "surface": "codex",
826
+ "host_surfaces": ["codex-cli", "codex-ide-plugin", "codex-app", "codex-app-ssh"],
827
+ "mechanism": "codex_explicit_skills",
828
+ "command": spec["command"],
829
+ "path": str(skill_path),
830
+ "status": skill_status,
831
+ "invoke_as": [f"${spec['name']}", "/skills"],
832
+ }
833
+ )
834
+ if skill_status not in {"skipped_user_file", "preserved_existing_loopx_skill"}:
835
+ display_name = (
836
+ "LoopX" if spec["command"] == "/loopx" else f"LoopX {spec['command']}"
837
+ )
838
+ metadata = _openai_skill_metadata(
839
+ command=str(spec["command"]),
840
+ display_name=display_name,
841
+ short_description=str(spec["description"]),
842
+ )
843
+ metadata_status = _target_status(metadata_path, metadata, execute=execute)
844
+ installed.append(
845
+ {
846
+ "surface": "codex",
847
+ "host_surfaces": ["codex-cli", "codex-ide-plugin", "codex-app", "codex-app-ssh"],
848
+ "mechanism": "codex_skill_openai_metadata",
849
+ "command": spec["command"],
850
+ "path": str(metadata_path),
851
+ "status": metadata_status,
852
+ "invoke_as": [f"${spec['name']}", "/skills"],
853
+ }
854
+ )
855
+ elif skill_status in {"skipped_user_file", "preserved_existing_loopx_skill"}:
856
+ retire_status = _retire_managed_file(metadata_path, execute=execute)
857
+ if retire_status:
858
+ installed.append(
859
+ {
860
+ "surface": "codex",
861
+ "host_surfaces": ["codex-cli", "codex-ide-plugin", "codex-app", "codex-app-ssh"],
862
+ "mechanism": "retired_codex_command_metadata",
863
+ "command": spec["command"],
864
+ "path": str(metadata_path),
865
+ "status": retire_status,
866
+ "invoke_as": [],
867
+ }
868
+ )
869
+ for spec in specs:
870
+ installed.append(
871
+ {
872
+ "surface": "codex",
873
+ "host_surfaces": ["codex-cli"],
874
+ "mechanism": "unsupported_native_slash_registry",
875
+ "command": spec["command"],
876
+ "path": None,
877
+ "status": "unsupported_host_surface",
878
+ "invoke_as": [],
879
+ "reason": (
880
+ "Current Codex does not support user-defined native top-level slash "
881
+ "commands. Use explicit skills instead."
882
+ ),
883
+ "native_registry_supported": False,
884
+ "failure_policy": "fail_closed_to_explicit_skill",
885
+ "fallback": (
886
+ f"Use `${spec['name']}` or `/skills` to explicitly invoke the LoopX "
887
+ "command skill; for the visible TUI loop, run "
888
+ "`loopx codex-cli-bootstrap-message --project .`, paste the setup "
889
+ "message, then set `/goal <thin task_body>`."
890
+ ),
891
+ }
892
+ )
893
+
894
+ if "claude-code" in effective_surfaces:
895
+ skills_dir = claude_root / "skills"
896
+ for spec in specs:
897
+ path = skills_dir / str(spec["name"]) / "SKILL.md"
898
+ if uninstall:
899
+ status = _retire_status(path, execute=execute)
900
+ installed.append(
901
+ {
902
+ "surface": "claude-code",
903
+ "mechanism": "claude_code_skills",
904
+ "command": spec["command"],
905
+ "path": str(path),
906
+ "status": status,
907
+ "invoke_as": [str(spec["command"])],
908
+ }
909
+ )
910
+ continue
911
+ content = _skill_body(
912
+ command=str(spec["command"]),
913
+ title=f"LoopX {spec['command']}",
914
+ description=str(spec["description"]),
915
+ argument_hint=str(spec["argument_hint"]),
916
+ instructions=list(spec["instructions"]),
917
+ surface="claude-skills",
918
+ front_matter_name=str(spec["name"]),
919
+ )
920
+ status = _target_status(path, content, execute=execute)
921
+ installed.append(
922
+ {
923
+ "surface": "claude-code",
924
+ "mechanism": "claude_code_skills",
925
+ "command": spec["command"],
926
+ "path": str(path),
927
+ "status": status,
928
+ "invoke_as": [str(spec["command"])],
929
+ }
930
+ )
931
+
932
+ def _install_skill_facade(
933
+ *,
934
+ skills_dir: Path,
935
+ surface: str,
936
+ host_surfaces: list[str],
937
+ mechanism: str,
938
+ invoke_prefix: str = "",
939
+ ) -> None:
940
+ """Write the command facade as SKILL.md files under `skills_dir`.
941
+
942
+ Claude Code, Gemini CLI and Cursor all discover user skills the same
943
+ way — a directory per skill with SKILL.md front matter — so the body is
944
+ identical and only the root differs.
945
+ """
946
+ for spec in specs:
947
+ path = skills_dir / str(spec["name"]) / "SKILL.md"
948
+ if uninstall:
949
+ installed.append(
950
+ {
951
+ "surface": surface,
952
+ "host_surfaces": list(host_surfaces),
953
+ "mechanism": mechanism,
954
+ "command": spec["command"],
955
+ "path": str(path),
956
+ "status": _retire_status(path, execute=execute),
957
+ "invoke_as": [],
958
+ }
959
+ )
960
+ continue
961
+ content = _skill_body(
962
+ command=str(spec["command"]),
963
+ title=f"LoopX {spec['command']}",
964
+ description=str(spec["description"]),
965
+ argument_hint=str(spec["argument_hint"]),
966
+ instructions=list(spec["instructions"]),
967
+ surface="claude-skills",
968
+ front_matter_name=str(spec["name"]),
969
+ )
970
+ installed.append(
971
+ {
972
+ "surface": surface,
973
+ "host_surfaces": list(host_surfaces),
974
+ "mechanism": mechanism,
975
+ "command": spec["command"],
976
+ "path": str(path),
977
+ "status": _target_status(path, content, execute=execute),
978
+ "invoke_as": [f"{invoke_prefix}{spec['name']}"],
979
+ }
980
+ )
981
+
982
+ if "gemini" in effective_surfaces:
983
+ # Gemini CLI discovers user skills from GEMINI_HOME/skills. Files are
984
+ # written directly, not through `gemini skills install --consent`: that
985
+ # command copies from a git URL or an existing local path and the host
986
+ # owns the copy, so LoopX would lose the managed marker, the per-file
987
+ # status and the dry run that every other surface reports — and it would
988
+ # need the `gemini` binary on PATH to install a file it already has.
989
+ _install_skill_facade(
990
+ skills_dir=gemini_root / "skills",
991
+ surface="gemini",
992
+ host_surfaces=["gemini-cli"],
993
+ mechanism="gemini_cli_skills",
994
+ )
995
+
996
+ if "cursor" in effective_surfaces:
997
+ # Cursor reads SKILL.md from CURSOR_HOME/skills (its skill roots also
998
+ # include .claude/skills and .codex/skills, but relying on another
999
+ # host's directory would break the moment that host is uninstalled).
1000
+ _install_skill_facade(
1001
+ skills_dir=cursor_root / "skills",
1002
+ surface="cursor",
1003
+ host_surfaces=["cursor-agent"],
1004
+ mechanism="cursor_skills",
1005
+ )
1006
+ # `cursor-agent mcp` can list and enable servers but not add them, so
1007
+ # the entry is merged into CURSOR_HOME/mcp.json. Only our own `loopx`
1008
+ # key is touched — a user's other servers are left exactly as they are.
1009
+ status = _merge_cursor_mcp(cursor_root, uninstall=uninstall, execute=execute)
1010
+ installed.append(
1011
+ {
1012
+ "surface": "cursor",
1013
+ "host_surfaces": ["cursor-agent"],
1014
+ "mechanism": "cursor_mcp_server",
1015
+ "command": "loopx",
1016
+ "path": str(cursor_root / "mcp.json"),
1017
+ "status": status,
1018
+ "invoke_as": [],
1019
+ }
1020
+ )
1021
+
1022
+ if "opencode" in effective_surfaces:
1023
+ # OpenCode reads global skills from OPENCODE_CONFIG_DIR/skills. The
1024
+ # static command facade below stays as it is — a command is something
1025
+ # the user types, a skill is something the model can reach for itself.
1026
+ _install_skill_facade(
1027
+ skills_dir=opencode_root / "skills",
1028
+ surface="opencode",
1029
+ host_surfaces=["opencode"],
1030
+ mechanism="opencode_skills",
1031
+ )
1032
+ commands_dir = opencode_root / "commands"
1033
+ plugin_path = opencode_root / "plugins" / "loopx-goal.js"
1034
+ runtime_path = opencode_root / "loopx" / "goal-bridge-runtime.mjs"
1035
+ package_path = opencode_root / "package.json"
1036
+ plugin_content = plugin_source()
1037
+ runtime_content = runtime_source()
1038
+
1039
+ bridge_preflight_blocked = False
1040
+ if with_goal_bridge and not uninstall:
1041
+ conflicts, invalid_configs = _opencode_direct_goal_plugin_conflicts(opencode_root)
1042
+ if invalid_configs:
1043
+ installed.append(
1044
+ {
1045
+ "surface": "opencode",
1046
+ "host_surfaces": ["opencode"],
1047
+ "mechanism": "opencode_goal_bridge",
1048
+ "command": "/goal",
1049
+ "path": str(plugin_path),
1050
+ "status": "blocked_invalid_opencode_config",
1051
+ "invoke_as": [],
1052
+ "reason": (
1053
+ "Repair the listed OpenCode JSON/JSONC config before installing "
1054
+ "the bridge so direct plugin conflicts can be checked safely."
1055
+ ),
1056
+ "invalid_configs": invalid_configs,
1057
+ }
1058
+ )
1059
+ bridge_preflight_blocked = True
1060
+ elif conflicts:
1061
+ installed.append(
1062
+ {
1063
+ "surface": "opencode",
1064
+ "host_surfaces": ["opencode"],
1065
+ "mechanism": "opencode_goal_bridge",
1066
+ "command": "/goal",
1067
+ "path": str(plugin_path),
1068
+ "status": "blocked_conflicting_direct_plugin",
1069
+ "invoke_as": [],
1070
+ "reason": (
1071
+ "Remove direct goal-plugin registration from the listed "
1072
+ "OpenCode config, then rerun installation. The bridge imports "
1073
+ "the pinned plugin and both must not be loaded independently."
1074
+ ),
1075
+ "conflicts": conflicts,
1076
+ }
1077
+ )
1078
+ bridge_preflight_blocked = True
1079
+ else:
1080
+ user_owned_bridge_paths = [
1081
+ str(path)
1082
+ for path, content in (
1083
+ (plugin_path, plugin_content),
1084
+ (runtime_path, runtime_content),
1085
+ )
1086
+ if _target_status(path, content, execute=False)
1087
+ == "skipped_user_file"
1088
+ ]
1089
+ if user_owned_bridge_paths:
1090
+ installed.append(
1091
+ {
1092
+ "surface": "opencode",
1093
+ "host_surfaces": ["opencode"],
1094
+ "mechanism": "opencode_goal_bridge",
1095
+ "command": "/goal",
1096
+ "path": str(plugin_path),
1097
+ "status": "blocked_user_owned_bridge_file",
1098
+ "invoke_as": [],
1099
+ "reason": (
1100
+ "Move or rename the listed user-owned OpenCode bridge "
1101
+ "files before installing LoopX so no partial bridge or "
1102
+ "dependency update is applied."
1103
+ ),
1104
+ "conflicts": user_owned_bridge_paths,
1105
+ }
1106
+ )
1107
+ bridge_preflight_blocked = True
1108
+
1109
+ if not bridge_preflight_blocked:
1110
+ package_status = _target_package_dependencies(
1111
+ package_path,
1112
+ OPENCODE_GOAL_DEPENDENCIES,
1113
+ execute=False,
1114
+ )
1115
+ if package_status == "blocked_invalid_user_package_json":
1116
+ installed.append(
1117
+ {
1118
+ "surface": "opencode",
1119
+ "host_surfaces": ["opencode"],
1120
+ "mechanism": "opencode_goal_dependencies",
1121
+ "command": "/goal",
1122
+ "path": str(package_path),
1123
+ "status": package_status,
1124
+ "invoke_as": [],
1125
+ }
1126
+ )
1127
+ bridge_preflight_blocked = True
1128
+
1129
+ if with_goal_bridge and not bridge_preflight_blocked:
1130
+ if uninstall:
1131
+ for mechanism, path in (
1132
+ ("opencode_goal_bridge", plugin_path),
1133
+ ("opencode_goal_bridge_runtime", runtime_path),
1134
+ ):
1135
+ installed.append(
1136
+ {
1137
+ "surface": "opencode",
1138
+ "host_surfaces": ["opencode"],
1139
+ "mechanism": mechanism,
1140
+ "command": "/goal",
1141
+ "path": str(path),
1142
+ "status": _retire_status(path, execute=execute),
1143
+ "invoke_as": ["/goal", "loopx_goal_activate"],
1144
+ }
1145
+ )
1146
+ installed.append(
1147
+ {
1148
+ "surface": "opencode",
1149
+ "host_surfaces": ["opencode"],
1150
+ "mechanism": "opencode_goal_dependencies",
1151
+ "command": "/goal",
1152
+ "path": str(package_path),
1153
+ "status": "preserved_shared_dependencies",
1154
+ "invoke_as": [],
1155
+ }
1156
+ )
1157
+ else:
1158
+ package_status = _target_package_dependencies(
1159
+ package_path,
1160
+ OPENCODE_GOAL_DEPENDENCIES,
1161
+ execute=execute,
1162
+ )
1163
+ installed.append(
1164
+ {
1165
+ "surface": "opencode",
1166
+ "host_surfaces": ["opencode"],
1167
+ "mechanism": "opencode_goal_dependencies",
1168
+ "command": "/goal",
1169
+ "path": str(package_path),
1170
+ "status": package_status,
1171
+ "invoke_as": [],
1172
+ }
1173
+ )
1174
+ if package_status == "blocked_invalid_user_package_json":
1175
+ bridge_preflight_blocked = True
1176
+ else:
1177
+ for mechanism, path, content in (
1178
+ ("opencode_goal_bridge_runtime", runtime_path, runtime_content),
1179
+ ("opencode_goal_bridge", plugin_path, plugin_content),
1180
+ ):
1181
+ installed.append(
1182
+ {
1183
+ "surface": "opencode",
1184
+ "host_surfaces": ["opencode"],
1185
+ "mechanism": mechanism,
1186
+ "command": "/goal",
1187
+ "path": str(path),
1188
+ "status": _target_status(path, content, execute=execute),
1189
+ "invoke_as": ["/goal", "loopx_goal_activate"],
1190
+ }
1191
+ )
1192
+
1193
+ if not bridge_preflight_blocked:
1194
+ for spec in specs:
1195
+ path = commands_dir / f"{spec['name']}.md"
1196
+ status = (
1197
+ _retire_status(path, execute=execute)
1198
+ if uninstall
1199
+ else _target_status(
1200
+ path,
1201
+ _opencode_command_body(spec),
1202
+ execute=execute,
1203
+ )
1204
+ )
1205
+ installed.append(
1206
+ {
1207
+ "surface": "opencode",
1208
+ "host_surfaces": ["opencode"],
1209
+ "mechanism": "opencode_commands",
1210
+ "command": spec["command"],
1211
+ "path": str(path),
1212
+ "status": status,
1213
+ "invoke_as": [str(spec["command"])],
1214
+ }
1215
+ )
1216
+
1217
+ if "pi" in effective_surfaces:
1218
+ extension_path = _pi_extension_path(pi_project_root)
1219
+ runtime_path = _pi_runtime_path(pi_project_root)
1220
+ extension_content = pi_extension_source()
1221
+ runtime_content = pi_runtime_source()
1222
+ if uninstall:
1223
+ for mechanism, path in (
1224
+ ("pi_goal_extension", extension_path),
1225
+ ("pi_goal_extension_runtime", runtime_path),
1226
+ ):
1227
+ installed.append(
1228
+ {
1229
+ "surface": "pi",
1230
+ "host_surfaces": ["pi"],
1231
+ "mechanism": mechanism,
1232
+ "command": "/loopx",
1233
+ "path": str(path),
1234
+ "status": _retire_status(path, execute=execute),
1235
+ "invoke_as": ["/loopx", "loopx_goal_activate"],
1236
+ }
1237
+ )
1238
+ else:
1239
+ # The adapter and its loop runtime are one atomic delivery unit:
1240
+ # preflight both targets and fail closed with zero writes when any
1241
+ # target is a user-owned file, so a newly created managed adapter
1242
+ # can never import an unmanaged runtime that may lack the exports
1243
+ # it needs.
1244
+ user_owned_pi_paths = [
1245
+ str(path)
1246
+ for path, content in (
1247
+ (extension_path, extension_content),
1248
+ (runtime_path, runtime_content),
1249
+ )
1250
+ if _target_status(path, content, execute=False) == "skipped_user_file"
1251
+ ]
1252
+ if user_owned_pi_paths:
1253
+ installed.append(
1254
+ {
1255
+ "surface": "pi",
1256
+ "host_surfaces": ["pi"],
1257
+ "mechanism": "pi_goal_extension",
1258
+ "command": "/loopx",
1259
+ "path": str(extension_path),
1260
+ "status": "blocked_user_owned_pi_file",
1261
+ "invoke_as": ["/loopx", "loopx_goal_activate"],
1262
+ "reason": (
1263
+ "Move or rename the listed user-owned Pi files before "
1264
+ "installing LoopX so the adapter and its loop runtime "
1265
+ "are installed as one atomic unit; no Pi file was written."
1266
+ ),
1267
+ "conflicts": user_owned_pi_paths,
1268
+ }
1269
+ )
1270
+ else:
1271
+ for mechanism, path, content in (
1272
+ ("pi_goal_extension", extension_path, extension_content),
1273
+ ("pi_goal_extension_runtime", runtime_path, runtime_content),
1274
+ ):
1275
+ installed.append(
1276
+ {
1277
+ "surface": "pi",
1278
+ "host_surfaces": ["pi"],
1279
+ "mechanism": mechanism,
1280
+ "command": "/loopx",
1281
+ "path": str(path),
1282
+ "status": _target_status(path, content, execute=execute),
1283
+ "invoke_as": ["/loopx", "loopx_goal_activate"],
1284
+ }
1285
+ )
1286
+
1287
+ status_counts: dict[str, int] = {}
1288
+ for item in installed:
1289
+ status = str(item["status"])
1290
+ status_counts[status] = status_counts.get(status, 0) + 1
1291
+
1292
+ return {
1293
+ "ok": not any(status.startswith("blocked_") for status in status_counts),
1294
+ "schema_version": SCHEMA_VERSION,
1295
+ "operation": "uninstall" if uninstall else "install",
1296
+ "execute": execute,
1297
+ "with_goal_bridge": with_goal_bridge,
1298
+ "requested_surfaces": surfaces or ["all"],
1299
+ "effective_surfaces": effective_surfaces,
1300
+ "catalog_schema_version": build_slash_command_catalog(
1301
+ cli_bin=cli_bin,
1302
+ include_legacy_aliases=include_legacy_aliases,
1303
+ )["schema_version"],
1304
+ "summary": {
1305
+ "codex_prompt_dir": None,
1306
+ "codex_skill_dir": str(codex_root / "skills") if "codex" in effective_surfaces else None,
1307
+ "claude_skill_dir": str(claude_root / "skills") if "claude-code" in effective_surfaces else None,
1308
+ "gemini_skill_dir": str(gemini_root / "skills") if "gemini" in effective_surfaces else None,
1309
+ "cursor_skill_dir": str(cursor_root / "skills") if "cursor" in effective_surfaces else None,
1310
+ "cursor_mcp_path": str(cursor_root / "mcp.json") if "cursor" in effective_surfaces else None,
1311
+ "opencode_skill_dir": str(opencode_root / "skills") if "opencode" in effective_surfaces else None,
1312
+ "opencode_command_dir": str(opencode_root / "commands") if "opencode" in effective_surfaces else None,
1313
+ "opencode_plugin_path": str(opencode_root / "plugins" / "loopx-goal.js") if "opencode" in effective_surfaces and with_goal_bridge else None,
1314
+ "opencode_package_path": str(opencode_root / "package.json") if "opencode" in effective_surfaces and with_goal_bridge else None,
1315
+ "pi_extension_path": str(_pi_extension_path(pi_project_root)) if "pi" in effective_surfaces else None,
1316
+ "pi_runtime_path": str(_pi_runtime_path(pi_project_root)) if "pi" in effective_surfaces else None,
1317
+ "status_counts": status_counts,
1318
+ "skip_policy": (
1319
+ "Uninstall removes only LoopX-managed files; user files without a LoopX managed marker are preserved"
1320
+ if uninstall
1321
+ else "LoopX-managed files are upgraded; same-name user files without a LoopX managed marker or legacy signature are never overwritten"
1322
+ ),
1323
+ },
1324
+ "installed": installed,
1325
+ "notes": [
1326
+ "Codex does not currently support user-defined native top-level slash commands; use explicit skill invocation through `$loopx` or `/skills`.",
1327
+ "Explicit LoopX command-facade skills use agents/openai.yaml policy allow_implicit_invocation=false and remain distinct from richer workflow skills such as loopx-project.",
1328
+ "Claude Code discovers user skills from CLAUDE_HOME/skills and exposes each skill name as a slash command.",
1329
+ "Gemini CLI discovers user skills from GEMINI_HOME/skills with the same SKILL.md front matter; files are written directly because `gemini skills install` copies from a git URL or an existing local path and hands the copy to the host, which would lose the managed marker, per-file status and dry-run reporting every other surface has.",
1330
+ "Cursor discovers skills from CURSOR_HOME/skills and has no user-defined slash commands, so the cursor surface installs the skill facade and registers the LoopX MCP server in CURSOR_HOME/mcp.json; run `cursor-agent mcp enable loopx` once to approve it.",
1331
+ "OpenCode discovers global skills from OPENCODE_CONFIG_DIR/skills in addition to the static command facade; a command is typed by the user, a skill can be reached by the model itself.",
1332
+ "The default all surface installs only OpenCode's static command facade; the executable goal bridge requires --with-goal-bridge.",
1333
+ "The Pi surface is opt-in and installs the self-contained goal extension and its loop runtime into the project's .pi/extensions/; it is not part of the default all surface.",
1334
+ "The OpenCode goal bridge uses Bun-managed config-directory dependencies and must replace any direct goal-plugin registration.",
1335
+ "OpenCode bridge uninstall preserves package.json dependencies because they may be shared by user-owned local plugins.",
1336
+ "Uninstall is fail-closed: it retires only files carrying the LoopX managed marker and leaves user-owned files in place.",
1337
+ ],
1338
+ }
1339
+
1340
+
1341
+ def render_slash_command_install_markdown(payload: dict[str, Any]) -> str:
1342
+ operation = str(payload.get("operation") or "install")
1343
+ lines = [
1344
+ "# LoopX Slash Command Uninstall" if operation == "uninstall" else "# LoopX Slash Command Install",
1345
+ "",
1346
+ f"- operation: `{operation}`",
1347
+ f"- execute: `{payload.get('execute')}`",
1348
+ f"- surfaces: `{','.join(payload.get('effective_surfaces') or [])}`",
1349
+ f"- skip policy: `{payload.get('summary', {}).get('skip_policy')}`",
1350
+ ]
1351
+ codex_prompt_dir = payload.get("summary", {}).get("codex_prompt_dir")
1352
+ codex_skill_dir = payload.get("summary", {}).get("codex_skill_dir")
1353
+ claude_skill_dir = payload.get("summary", {}).get("claude_skill_dir")
1354
+ opencode_command_dir = payload.get("summary", {}).get("opencode_command_dir")
1355
+ opencode_plugin_path = payload.get("summary", {}).get("opencode_plugin_path")
1356
+ if codex_prompt_dir:
1357
+ lines.append(f"- codex prompts: `{codex_prompt_dir}`")
1358
+ if codex_skill_dir:
1359
+ lines.append(f"- codex skills: `{codex_skill_dir}`")
1360
+ if claude_skill_dir:
1361
+ lines.append(f"- claude skills: `{claude_skill_dir}`")
1362
+ if opencode_command_dir:
1363
+ lines.append(f"- opencode commands: `{opencode_command_dir}`")
1364
+ if opencode_plugin_path:
1365
+ lines.append(f"- opencode bridge: `{opencode_plugin_path}`")
1366
+ pi_extension_path = payload.get("summary", {}).get("pi_extension_path")
1367
+ if pi_extension_path:
1368
+ lines.append(f"- pi extension: `{pi_extension_path}`")
1369
+ pi_runtime_path = payload.get("summary", {}).get("pi_runtime_path")
1370
+ if pi_runtime_path:
1371
+ lines.append(f"- pi loop runtime: `{pi_runtime_path}`")
1372
+ counts = payload.get("summary", {}).get("status_counts") or {}
1373
+ if isinstance(counts, dict) and counts:
1374
+ count_text = ", ".join(f"{key}={value}" for key, value in sorted(counts.items()))
1375
+ lines.append(f"- statuses: `{count_text}`")
1376
+ skipped = [
1377
+ item for item in payload.get("installed") or []
1378
+ if isinstance(item, dict) and item.get("status") == "skipped_user_file"
1379
+ ]
1380
+ if skipped:
1381
+ lines.append("")
1382
+ lines.append("Skipped user-owned files:")
1383
+ for item in skipped:
1384
+ lines.append(f"- `{item.get('command')}` at `{item.get('path')}`")
1385
+ notes = [note for note in payload.get("notes") or [] if isinstance(note, str)]
1386
+ if notes:
1387
+ lines.append("")
1388
+ lines.append("Notes:")
1389
+ for note in notes:
1390
+ lines.append(f"- {note}")
1391
+ lines.append("")
1392
+ lines.append("Restart the host if its slash-command menu was already open.")
1393
+ return "\n".join(lines)