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,1254 @@
1
+ from __future__ import annotations
2
+
3
+ import fnmatch
4
+ import json
5
+ import re
6
+ from collections.abc import Callable, Iterator
7
+ from contextlib import contextmanager
8
+ from datetime import datetime, timedelta
9
+ from pathlib import Path
10
+ from typing import Any
11
+
12
+ from ...agent_registry import (
13
+ registered_agent_ids_from_registry,
14
+ require_registered_agent_id,
15
+ )
16
+ from ...file_lock import exclusive_file_lock
17
+ from ...history import load_registry
18
+ from ...paths import resolve_runtime_root
19
+ from ..runtime.time import now_utc as runtime_now_utc
20
+ from ..runtime.time import parse_timestamp, utc_isoformat
21
+ from ..todos.contract import (
22
+ TODO_TASK_CLASS_USER_GATE,
23
+ normalize_required_write_scopes,
24
+ normalize_todo_claimed_by,
25
+ normalize_todo_excluded_agents,
26
+ normalize_todo_id,
27
+ )
28
+ from ..todos.handoff_mode import (
29
+ HANDOFF_MODE_HARD_LEASE,
30
+ HANDOFF_MODE_LEGACY,
31
+ HANDOFF_MODE_SOFT_CLAIM,
32
+ HandoffModeError,
33
+ goal_handoff_mode_for_goal,
34
+ )
35
+
36
+ TASK_LEASE_SCHEMA_VERSION = "task_lease_v0"
37
+ DEFAULT_TASK_LEASE_TTL_SECONDS = 45 * 60
38
+ MAX_TASK_LEASE_TTL_SECONDS = 24 * 60 * 60
39
+ IDEMPOTENCY_KEY_PATTERN = re.compile(r"^[A-Za-z0-9_.:@/-]{1,160}$")
40
+
41
+
42
+ class TaskLeaseError(ValueError):
43
+ def __init__(self, message: str, *, code: str, payload: dict[str, Any] | None = None) -> None:
44
+ super().__init__(message)
45
+ self.code = code
46
+ self.payload = payload or {}
47
+
48
+
49
+ def now_utc() -> datetime:
50
+ return runtime_now_utc()
51
+
52
+
53
+ def isoformat(value: datetime) -> str:
54
+ return utc_isoformat(value)
55
+
56
+
57
+ def normalize_idempotency_key(value: Any) -> str:
58
+ candidate = str(value or "").strip()
59
+ if not candidate or not IDEMPOTENCY_KEY_PATTERN.match(candidate):
60
+ raise TaskLeaseError(
61
+ "idempotency key must be a public-safe token",
62
+ code="invalid_idempotency_key",
63
+ )
64
+ return candidate
65
+
66
+
67
+ def normalize_owner(value: Any) -> str:
68
+ owner = normalize_todo_claimed_by(value)
69
+ if not owner:
70
+ raise TaskLeaseError("owner must be a public-safe agent id", code="invalid_owner")
71
+ return owner
72
+
73
+
74
+ def normalize_ttl_seconds(value: int | None) -> int:
75
+ ttl = DEFAULT_TASK_LEASE_TTL_SECONDS if value is None else int(value)
76
+ if ttl <= 0 or ttl > MAX_TASK_LEASE_TTL_SECONDS:
77
+ raise TaskLeaseError(
78
+ f"ttl seconds must be between 1 and {MAX_TASK_LEASE_TTL_SECONDS}",
79
+ code="invalid_ttl",
80
+ )
81
+ return ttl
82
+
83
+
84
+ def normalize_goal_id(value: Any) -> str:
85
+ goal_id = str(value or "").strip()
86
+ if not goal_id or goal_id in {".", ".."} or "/" in goal_id or "\\" in goal_id:
87
+ raise TaskLeaseError("goal id must be a single path segment", code="invalid_goal_id")
88
+ if Path(goal_id).name != goal_id:
89
+ raise TaskLeaseError("goal id must not include path traversal", code="invalid_goal_id")
90
+ return goal_id
91
+
92
+
93
+ def normalize_lease_todo_id(value: Any) -> str:
94
+ todo_id = normalize_todo_id(value)
95
+ if not todo_id:
96
+ raise TaskLeaseError("todo id must use the todo_<token> shape", code="invalid_todo_id")
97
+ return todo_id
98
+
99
+
100
+ def task_lease_dir(*, runtime_root: Path, goal_id: str) -> Path:
101
+ return runtime_root / "goals" / normalize_goal_id(goal_id) / "task-leases"
102
+
103
+
104
+ def task_lease_path(*, runtime_root: Path, goal_id: str, todo_id: str) -> Path:
105
+ return task_lease_dir(runtime_root=runtime_root, goal_id=goal_id) / f"{normalize_lease_todo_id(todo_id)}.json"
106
+
107
+
108
+ def task_lease_lock_path(*, runtime_root: Path, goal_id: str) -> Path:
109
+ return task_lease_dir(runtime_root=runtime_root, goal_id=goal_id) / ".task-leases"
110
+
111
+
112
+ class _VerifiedTaskLeaseFence(dict):
113
+ """Key-verified fence payload with its release hook held out-of-band.
114
+
115
+ The hook lives on the instance attribute, never inside the mapping, so
116
+ the payload stays JSON-serializable for every consumer at all times.
117
+ """
118
+
119
+ release_hook: Callable[[], None] | None = None
120
+
121
+
122
+ def goal_handoff_mode_for_lease(registry_path: Path, goal_id: str) -> str:
123
+ """Resolve the goal handoff mode for lease verbs.
124
+
125
+ An invalid ``handoff_mode`` value is re-typed as a TaskLeaseError. A goal
126
+ or state file that cannot be resolved falls back to legacy: each lease
127
+ verb's own todo-projection checks already own that failure surface.
128
+ """
129
+
130
+ try:
131
+ return goal_handoff_mode_for_goal(registry_path=registry_path, goal_id=goal_id)
132
+ except HandoffModeError as exc:
133
+ raise TaskLeaseError(str(exc), code=exc.code, payload=exc.payload) from exc
134
+ except (OSError, ValueError):
135
+ return HANDOFF_MODE_LEGACY
136
+
137
+
138
+ def _require_lease_mutation_allowed_by_handoff_mode(
139
+ *,
140
+ registry_path: Path,
141
+ goal_id: str,
142
+ todo_id: str,
143
+ action: str,
144
+ ) -> str:
145
+ handoff_mode = goal_handoff_mode_for_lease(registry_path, goal_id)
146
+ if handoff_mode == HANDOFF_MODE_SOFT_CLAIM:
147
+ raise TaskLeaseError(
148
+ f"goal handoff mode 'soft_claim' forbids task lease {action}; "
149
+ "release and inspect remain available for legacy leftovers",
150
+ code="handoff_mode_forbids_lease",
151
+ payload={
152
+ "goal_id": goal_id,
153
+ "todo_id": todo_id,
154
+ "action": action,
155
+ "handoff_mode": handoff_mode,
156
+ },
157
+ )
158
+ return handoff_mode
159
+
160
+
161
+ def _optional_handoff_mode(registry_path: Path | None, goal_id: str) -> str | None:
162
+ if registry_path is None:
163
+ return None
164
+ try:
165
+ return goal_handoff_mode_for_lease(registry_path, goal_id)
166
+ except (TaskLeaseError, OSError, ValueError):
167
+ return None
168
+
169
+
170
+ @contextmanager
171
+ def hold_handoff_lease_holder_gate(
172
+ *,
173
+ registry_path: Path,
174
+ goal_id: str,
175
+ todo_id: str,
176
+ actor_agent_id: str | None,
177
+ ) -> Iterator[dict[str, Any]]:
178
+ """Hold the per-goal lease lock while proving the actor owns the lease.
179
+
180
+ hard_lease handoff mode only: an ownership mutation (claim, claimed_by
181
+ update, clear) on an existing todo requires the acting agent to hold that
182
+ todo's time-active lease. Identity is compared with
183
+ normalize_todo_claimed_by on both sides. Callers must already hold the
184
+ markdown state lock; this guard then takes the per-goal lease lock, the
185
+ same markdown-lock-then-lease-lock order used by the completion fence,
186
+ and the caller keeps it held until the markdown write commits.
187
+ """
188
+
189
+ normalized_goal_id = normalize_goal_id(goal_id)
190
+ normalized_todo_id = normalize_lease_todo_id(todo_id)
191
+ actor = normalize_todo_claimed_by(actor_agent_id)
192
+ runtime_root = runtime_root_from_registry(registry_path, None)
193
+ lease_path = task_lease_path(
194
+ runtime_root=runtime_root,
195
+ goal_id=normalized_goal_id,
196
+ todo_id=normalized_todo_id,
197
+ )
198
+ base_payload: dict[str, Any] = {
199
+ "goal_id": normalized_goal_id,
200
+ "todo_id": normalized_todo_id,
201
+ "handoff_mode": HANDOFF_MODE_HARD_LEASE,
202
+ "lease_path": str(lease_path),
203
+ }
204
+ if not actor:
205
+ raise TaskLeaseError(
206
+ "hard_lease handoff mode requires an attributed actor for "
207
+ "ownership changes; provide --agent-id",
208
+ code="handoff_mode_requires_lease",
209
+ payload={**base_payload, "reason": "missing_actor"},
210
+ )
211
+ lock_target = task_lease_lock_path(
212
+ runtime_root=runtime_root,
213
+ goal_id=normalized_goal_id,
214
+ )
215
+ with exclusive_file_lock(
216
+ lock_target,
217
+ agent_id=actor,
218
+ operation="handoff_lease_holder_gate",
219
+ ):
220
+ lease = read_lease(lease_path)
221
+ if not lease_is_active(lease):
222
+ raise TaskLeaseError(
223
+ "hard_lease handoff mode requires a time-active task lease "
224
+ "before ownership of an existing todo can change; acquire one "
225
+ "with `loopx task-lease acquire`",
226
+ code="handoff_mode_requires_lease",
227
+ payload={**base_payload, "reason": "no_active_lease"},
228
+ )
229
+ assert lease is not None
230
+ owner = normalize_todo_claimed_by(lease.get("owner"))
231
+ if owner != actor:
232
+ raise TaskLeaseError(
233
+ f"hard_lease handoff mode: actor {actor!r} does not own the "
234
+ f"time-active task lease held by {owner!r}",
235
+ code="handoff_mode_requires_lease",
236
+ payload={
237
+ **base_payload,
238
+ "reason": "lease_owner_mismatch",
239
+ "actor_agent_id": actor,
240
+ "lease_owner": owner,
241
+ "lease_version": lease.get("version"),
242
+ "expires_at": lease.get("expires_at"),
243
+ },
244
+ )
245
+ yield {
246
+ "schema_version": TASK_LEASE_SCHEMA_VERSION,
247
+ "checked": True,
248
+ "active": True,
249
+ "owner": owner,
250
+ "version": lease.get("version"),
251
+ }
252
+
253
+
254
+ @contextmanager
255
+ def hold_task_lease_mutation_fence(
256
+ *,
257
+ registry_path: Path,
258
+ goal_id: str,
259
+ todo_id: str,
260
+ todo: dict[str, Any],
261
+ actor_agent_id: str | None,
262
+ idempotency_key: str | None,
263
+ expected_version: int | None = None,
264
+ require_active_when_key_supplied: bool = True,
265
+ handoff: dict[str, Any] | None = None,
266
+ ) -> Iterator[dict[str, Any]]:
267
+ """Hold the per-goal lease lock while one todo lifecycle write commits.
268
+
269
+ Task leases are optional in legacy and soft_claim handoff modes. Once an
270
+ effective lease exists, however, the lifecycle writer must prove that it
271
+ is the execution instance that acquired the lease. The idempotency key is
272
+ the execution-instance fence; agent ids alone are insufficient because
273
+ multiple host processes may share one peer identity.
274
+
275
+ ``handoff`` carries the resolved goal handoff mode plus the delegated
276
+ authority door marker. In hard_lease mode (without the door) the fence is
277
+ mandatory: no effective lease is a typed error instead of a silent
278
+ ``{"required": False}``, and a time-active lease whose owner constraint is
279
+ no longer effective (the legacy self-disarm state) fails loudly instead of
280
+ letting a keyless completion through. For a user-role ``user_gate``
281
+ completion that supplies no explicit lease credentials, the fence mints
282
+ the key itself under the same per-goal lease lock; an existing
283
+ time-active lease is never displaced.
284
+ """
285
+
286
+ normalized_goal_id = normalize_goal_id(goal_id)
287
+ normalized_todo_id = normalize_lease_todo_id(todo_id)
288
+ requested_key = (
289
+ normalize_idempotency_key(idempotency_key)
290
+ if idempotency_key is not None
291
+ else None
292
+ )
293
+ runtime_root = runtime_root_from_registry(registry_path, None)
294
+ lease_path = task_lease_path(
295
+ runtime_root=runtime_root,
296
+ goal_id=normalized_goal_id,
297
+ todo_id=normalized_todo_id,
298
+ )
299
+ lock_target = task_lease_lock_path(
300
+ runtime_root=runtime_root,
301
+ goal_id=normalized_goal_id,
302
+ )
303
+ handoff = handoff or {}
304
+ handoff_mode = str(handoff.get("handoff_mode") or HANDOFF_MODE_LEGACY)
305
+ handoff_gate_overridden = handoff.get("handoff_gate_overridden") is True
306
+ auto_acquire_lease = (
307
+ handoff_mode == HANDOFF_MODE_HARD_LEASE
308
+ and not handoff_gate_overridden
309
+ and not require_active_when_key_supplied
310
+ and str(todo.get("role") or "") == "user"
311
+ and str(todo.get("task_class") or "") == TODO_TASK_CLASS_USER_GATE
312
+ )
313
+ auto_acquired = False
314
+ with exclusive_file_lock(
315
+ lock_target,
316
+ agent_id=actor_agent_id,
317
+ operation="task_lease_mutation_fence",
318
+ ):
319
+ lease = read_lease(lease_path)
320
+ time_active = lease_is_active(lease)
321
+ active = time_active
322
+ constraint: dict[str, Any] | None = None
323
+ if active and lease:
324
+ constraint = task_lease_owner_constraint(
325
+ todo,
326
+ owner=lease.get("owner"),
327
+ registered_agents=registered_agent_ids_from_registry(
328
+ registry_path,
329
+ normalized_goal_id,
330
+ ),
331
+ )
332
+ active = constraint.get("effective") is True
333
+
334
+ if (
335
+ auto_acquire_lease
336
+ and not active
337
+ and not (time_active and lease)
338
+ ):
339
+ # Mint the completion key under the same per-goal lease lock. A
340
+ # time-active lease is never displaced: if one exists but its
341
+ # owner constraint is no longer effective, the hard branch below
342
+ # keeps the loud divergence error.
343
+ normalized_actor = normalize_todo_claimed_by(actor_agent_id)
344
+ if not normalized_actor:
345
+ raise TaskLeaseError(
346
+ "hard_lease handoff mode auto-acquire requires an "
347
+ "attributed actor; provide --agent-id",
348
+ code="handoff_mode_requires_lease",
349
+ payload={
350
+ "goal_id": normalized_goal_id,
351
+ "todo_id": normalized_todo_id,
352
+ "handoff_mode": handoff_mode,
353
+ "lease_path": str(lease_path),
354
+ "reason": "missing_actor",
355
+ },
356
+ )
357
+ constraint = task_lease_owner_constraint(
358
+ todo,
359
+ owner=normalized_actor,
360
+ registered_agents=registered_agent_ids_from_registry(
361
+ registry_path,
362
+ normalized_goal_id,
363
+ ),
364
+ )
365
+ if constraint.get("effective") is not True:
366
+ raise TaskLeaseError(
367
+ "hard_lease handoff mode auto-acquire rejected for "
368
+ "the acting agent",
369
+ code="handoff_mode_requires_lease",
370
+ payload={
371
+ "goal_id": normalized_goal_id,
372
+ "todo_id": normalized_todo_id,
373
+ "handoff_mode": handoff_mode,
374
+ "lease_path": str(lease_path),
375
+ "reason": str(
376
+ constraint.get("reason") or "owner_not_allowed"
377
+ ),
378
+ "constraint": constraint,
379
+ },
380
+ )
381
+ at = now_utc()
382
+ auto_key = requested_key or f"auto-{normalized_todo_id}"
383
+ updated_at = isoformat(at)
384
+ expires_at = isoformat(
385
+ at + timedelta(seconds=DEFAULT_TASK_LEASE_TTL_SECONDS)
386
+ )
387
+ lease = build_lease(
388
+ goal_id=normalized_goal_id,
389
+ todo_id=normalized_todo_id,
390
+ owner=normalized_actor,
391
+ idempotency_key=auto_key,
392
+ write_scopes=[],
393
+ acquire_ttl_seconds=DEFAULT_TASK_LEASE_TTL_SECONDS,
394
+ version=int((lease or {}).get("version") or 0) + 1,
395
+ acquired_at=updated_at,
396
+ updated_at=updated_at,
397
+ expires_at=expires_at,
398
+ )
399
+ write_lease(lease_path, lease)
400
+ requested_key = auto_key
401
+ auto_acquired = True
402
+ active = True
403
+ time_active = True
404
+
405
+ if not active:
406
+ if handoff_mode == HANDOFF_MODE_HARD_LEASE and not handoff_gate_overridden:
407
+ if time_active and lease:
408
+ raise TaskLeaseError(
409
+ "hard_lease handoff mode found a time-active lease "
410
+ "diverged from the todo projection; refusing keyless "
411
+ "completion. Repair ownership (release or transfer the "
412
+ "lease, or restore claimed_by) before completing.",
413
+ code="handoff_mode_lease_claim_divergence",
414
+ payload={
415
+ "goal_id": normalized_goal_id,
416
+ "todo_id": normalized_todo_id,
417
+ "handoff_mode": handoff_mode,
418
+ "lease_owner": lease.get("owner"),
419
+ "lease_version": lease.get("version"),
420
+ "constraint": constraint,
421
+ "lease_path": str(lease_path),
422
+ },
423
+ )
424
+ raise TaskLeaseError(
425
+ "hard_lease handoff mode requires an effective task lease "
426
+ "to complete this todo; acquire one with "
427
+ "`loopx task-lease acquire`",
428
+ code="handoff_mode_requires_lease",
429
+ payload={
430
+ "goal_id": normalized_goal_id,
431
+ "todo_id": normalized_todo_id,
432
+ "handoff_mode": handoff_mode,
433
+ "lease_path": str(lease_path),
434
+ },
435
+ )
436
+ if (
437
+ require_active_when_key_supplied
438
+ and (requested_key is not None or expected_version is not None)
439
+ ):
440
+ raise TaskLeaseError(
441
+ "task lease fence was supplied but no effective lease is active",
442
+ code="lease_not_active",
443
+ payload={
444
+ "goal_id": normalized_goal_id,
445
+ "todo_id": normalized_todo_id,
446
+ "lease_path": str(lease_path),
447
+ },
448
+ )
449
+ yield {
450
+ "schema_version": TASK_LEASE_SCHEMA_VERSION,
451
+ "required": False,
452
+ "active": False,
453
+ }
454
+ return
455
+
456
+ assert lease is not None
457
+ if requested_key is None:
458
+ raise TaskLeaseError(
459
+ "todo has an active task lease; lifecycle mutation requires its idempotency key",
460
+ code="lease_fence_required",
461
+ payload={
462
+ "goal_id": normalized_goal_id,
463
+ "todo_id": normalized_todo_id,
464
+ "lease_owner": lease.get("owner"),
465
+ "lease_version": lease.get("version"),
466
+ "lease_path": str(lease_path),
467
+ },
468
+ )
469
+ normalized_actor = normalize_todo_claimed_by(actor_agent_id)
470
+ if (
471
+ lease.get("owner") != normalized_actor
472
+ or lease.get("idempotency_key") != requested_key
473
+ ):
474
+ raise TaskLeaseError(
475
+ "task lease owner or execution-instance key mismatch",
476
+ code="lease_cas_mismatch",
477
+ payload={
478
+ "goal_id": normalized_goal_id,
479
+ "todo_id": normalized_todo_id,
480
+ "lease_owner": lease.get("owner"),
481
+ "lease_version": lease.get("version"),
482
+ "actor_agent_id": normalized_actor,
483
+ "lease_path": str(lease_path),
484
+ },
485
+ )
486
+ assert_expected_version(lease, expected_version)
487
+ fence_payload = {
488
+ "schema_version": TASK_LEASE_SCHEMA_VERSION,
489
+ "required": True,
490
+ "active": True,
491
+ "owner": normalized_actor,
492
+ "version": lease.get("version"),
493
+ "execution_instance_verified": True,
494
+ }
495
+ if auto_acquired:
496
+ fence_payload["auto_acquired"] = True
497
+ fence = _VerifiedTaskLeaseFence(fence_payload)
498
+ fence.release_hook = lambda: remove_lease(lease_path)
499
+ yield fence
500
+
501
+
502
+ def release_verified_task_lease_fence(
503
+ fence: dict[str, Any] | None,
504
+ *,
505
+ committed: bool,
506
+ ) -> None:
507
+ """Release the lease behind a key-verified fence once its write committed.
508
+
509
+ Must run while the hold_task_lease_mutation_fence context is still open so
510
+ the per-goal lease lock it acquired is still held; the lease therefore
511
+ cannot have been renewed, transferred, or re-acquired since the fence
512
+ verified the owner and idempotency key. The release reuses the CLI release
513
+ semantics (the lease file is removed; no new lifecycle state).
514
+
515
+ The private release hook rides on the fence object's attribute, not inside
516
+ the payload mapping, and is disarmed here on every call. Only a committed,
517
+ key-verified fence releases the lease; non-verified fences carry no hook
518
+ and are never touched. A release failure never unwinds the committed
519
+ lifecycle write: it is surfaced additively as fence["released"] = False
520
+ and the lease file is left for an explicit `loopx task-lease release` or
521
+ TTL expiry.
522
+ """
523
+
524
+ hook = getattr(fence, "release_hook", None)
525
+ if hook is None or not isinstance(fence, dict):
526
+ return
527
+ fence.release_hook = None # type: ignore[attr-defined]
528
+ if not committed or fence.get("execution_instance_verified") is not True:
529
+ return
530
+ try:
531
+ hook()
532
+ except OSError:
533
+ fence["released"] = False
534
+ return
535
+ fence["released"] = True
536
+
537
+
538
+ def runtime_root_from_registry(registry_path: Path, runtime_root_override: str | None) -> Path:
539
+ registry = load_registry(registry_path)
540
+ return resolve_runtime_root(registry, runtime_root_override)
541
+
542
+
543
+ def task_lease_todo_projection(
544
+ *,
545
+ registry_path: Path,
546
+ goal_id: str,
547
+ todo_id: str,
548
+ ) -> dict[str, Any] | None:
549
+ """Resolve the canonical Markdown/event todo projection for lease guards."""
550
+
551
+ from ...todos import list_goal_todos
552
+
553
+ try:
554
+ payload = list_goal_todos(
555
+ registry_path=registry_path,
556
+ goal_id=goal_id,
557
+ todo_id=todo_id,
558
+ )
559
+ except (OSError, ValueError) as exc:
560
+ raise TaskLeaseError(
561
+ "cannot resolve todo projection for task lease",
562
+ code="todo_projection_unavailable",
563
+ payload={"goal_id": goal_id, "todo_id": todo_id, "error": str(exc)},
564
+ ) from exc
565
+ todo = payload.get("todo")
566
+ return dict(todo) if isinstance(todo, dict) else None
567
+
568
+
569
+ def task_lease_owner_constraint(
570
+ todo: dict[str, Any] | None,
571
+ *,
572
+ owner: Any,
573
+ registered_agents: list[str] | None = None,
574
+ ) -> dict[str, Any]:
575
+ if todo is None:
576
+ return {"effective": False, "reason": "todo_not_found"}
577
+ todo_status = str(todo.get("status") or "").strip().lower()
578
+ if todo_status != "open":
579
+ return {
580
+ "effective": False,
581
+ "reason": "todo_not_open",
582
+ "todo_status": todo_status or "unknown",
583
+ }
584
+ normalized_owner = normalize_todo_claimed_by(owner)
585
+ if not normalized_owner:
586
+ return {"effective": False, "reason": "invalid_owner"}
587
+ if registered_agents is not None and normalized_owner not in registered_agents:
588
+ return {
589
+ "effective": False,
590
+ "reason": "owner_not_registered",
591
+ "registered_agents": registered_agents,
592
+ }
593
+ excluded_agents = normalize_todo_excluded_agents(todo.get("excluded_agents"))
594
+ if normalized_owner in excluded_agents:
595
+ return {
596
+ "effective": False,
597
+ "reason": "owner_excluded_from_todo",
598
+ "excluded_agents": excluded_agents,
599
+ }
600
+ claimed_by = normalize_todo_claimed_by(todo.get("claimed_by"))
601
+ if claimed_by and claimed_by != normalized_owner:
602
+ return {
603
+ "effective": False,
604
+ "reason": "owner_conflicts_with_claim",
605
+ "claimed_by": claimed_by,
606
+ }
607
+ return {"effective": True}
608
+
609
+
610
+ def require_registered_task_lease_owner(
611
+ *,
612
+ registry_path: Path,
613
+ goal_id: str,
614
+ todo_id: str,
615
+ owner: str,
616
+ ) -> str:
617
+ try:
618
+ return require_registered_agent_id(
619
+ registry_path=registry_path,
620
+ goal_id=goal_id,
621
+ agent_id=owner,
622
+ field="task lease owner",
623
+ )
624
+ except ValueError as exc:
625
+ raise TaskLeaseError(
626
+ str(exc),
627
+ code="owner_not_registered",
628
+ payload={"goal_id": goal_id, "todo_id": todo_id, "owner": owner},
629
+ ) from exc
630
+
631
+
632
+ def require_task_lease_owner_allowed(
633
+ *,
634
+ registry_path: Path,
635
+ goal_id: str,
636
+ todo_id: str,
637
+ owner: str,
638
+ ) -> dict[str, Any]:
639
+ owner = require_registered_task_lease_owner(
640
+ registry_path=registry_path,
641
+ goal_id=goal_id,
642
+ todo_id=todo_id,
643
+ owner=owner,
644
+ )
645
+ todo = task_lease_todo_projection(
646
+ registry_path=registry_path,
647
+ goal_id=goal_id,
648
+ todo_id=todo_id,
649
+ )
650
+ constraint = task_lease_owner_constraint(
651
+ todo,
652
+ owner=owner,
653
+ registered_agents=registered_agent_ids_from_registry(registry_path, goal_id),
654
+ )
655
+ if constraint.get("effective") is not True:
656
+ reason = str(constraint.get("reason") or "owner_not_allowed")
657
+ if reason == "todo_not_found":
658
+ message = "todo is missing from the canonical projection"
659
+ elif reason == "todo_not_open":
660
+ message = (
661
+ f"task lease requires an open todo; "
662
+ f"{todo_id!r} is {constraint.get('todo_status')!r}"
663
+ )
664
+ elif reason == "owner_excluded_from_todo":
665
+ message = f"task lease owner {owner!r} is excluded from todo {todo_id!r}"
666
+ elif reason == "owner_conflicts_with_claim":
667
+ message = (
668
+ f"task lease owner {owner!r} conflicts with todo claim "
669
+ f"{constraint.get('claimed_by')!r}"
670
+ )
671
+ elif reason == "owner_not_registered":
672
+ message = f"task lease owner {owner!r} is not registered for goal {goal_id!r}"
673
+ else:
674
+ message = f"task lease owner {owner!r} is not eligible for todo {todo_id!r}"
675
+ raise TaskLeaseError(
676
+ message,
677
+ code=reason,
678
+ payload={
679
+ "goal_id": goal_id,
680
+ "todo_id": todo_id,
681
+ "owner": owner,
682
+ **constraint,
683
+ },
684
+ )
685
+ return todo
686
+
687
+
688
+ def read_lease(path: Path) -> dict[str, Any] | None:
689
+ if not path.exists():
690
+ return None
691
+ try:
692
+ payload = json.loads(path.read_text(encoding="utf-8"))
693
+ except json.JSONDecodeError as exc:
694
+ raise TaskLeaseError(
695
+ f"lease file is not valid JSON: {path}",
696
+ code="corrupt_lease",
697
+ payload={"lease_path": str(path), "error": str(exc)},
698
+ ) from exc
699
+ if not isinstance(payload, dict):
700
+ raise TaskLeaseError(
701
+ f"lease file must contain an object: {path}",
702
+ code="corrupt_lease",
703
+ payload={"lease_path": str(path)},
704
+ )
705
+ return payload
706
+
707
+
708
+ def write_lease(path: Path, payload: dict[str, Any]) -> None:
709
+ path.parent.mkdir(parents=True, exist_ok=True)
710
+ temp_path = path.with_name(f".{path.name}.{id(payload)}.tmp")
711
+ temp_path.write_text(json.dumps(payload, ensure_ascii=False, indent=2) + "\n", encoding="utf-8")
712
+ temp_path.replace(path)
713
+
714
+
715
+ def remove_lease(path: Path) -> None:
716
+ try:
717
+ path.unlink()
718
+ except FileNotFoundError:
719
+ pass
720
+
721
+
722
+ def lease_expires_at(lease: dict[str, Any] | None) -> datetime | None:
723
+ return parse_timestamp((lease or {}).get("expires_at"))
724
+
725
+
726
+ def lease_is_active(lease: dict[str, Any] | None, *, at: datetime | None = None) -> bool:
727
+ if not lease or lease.get("schema_version") != TASK_LEASE_SCHEMA_VERSION:
728
+ return False
729
+ if lease.get("status") != "active":
730
+ return False
731
+ expires_at = lease_expires_at(lease)
732
+ return bool(expires_at and expires_at > (at or now_utc()))
733
+
734
+
735
+ def scope_root(scope: str) -> str:
736
+ value = scope.strip()
737
+ if value in {"*", "**", "./"}:
738
+ return ""
739
+ wildcard_indexes = [index for index in (value.find("*"), value.find("?"), value.find("[")) if index >= 0]
740
+ if wildcard_indexes:
741
+ value = value[: min(wildcard_indexes)]
742
+ if "/" in value:
743
+ value = value[: value.rfind("/") + 1]
744
+ return value.rstrip("/")
745
+
746
+
747
+ def _scope_has_glob(scope: str) -> bool:
748
+ return any(token in scope for token in ("*", "?", "["))
749
+
750
+
751
+ def _scope_literal_prefix(scope: str) -> str:
752
+ indexes = [scope.find(token) for token in ("*", "?", "[") if scope.find(token) >= 0]
753
+ return scope[: min(indexes)] if indexes else scope
754
+
755
+
756
+ def _scope_pair_overlaps(left: str, right: str) -> bool:
757
+ if left == right:
758
+ return True
759
+ if left in {"*", "**", "./"} or right in {"*", "**", "./"}:
760
+ return True
761
+
762
+ left_has_glob = _scope_has_glob(left)
763
+ right_has_glob = _scope_has_glob(right)
764
+ if left_has_glob and not right_has_glob:
765
+ prefix = _scope_literal_prefix(left)
766
+ return fnmatch.fnmatchcase(right, left) or (
767
+ prefix.endswith("/") and right.rstrip("/") == prefix.rstrip("/")
768
+ )
769
+ if right_has_glob and not left_has_glob:
770
+ prefix = _scope_literal_prefix(right)
771
+ return fnmatch.fnmatchcase(left, right) or (
772
+ prefix.endswith("/") and left.rstrip("/") == prefix.rstrip("/")
773
+ )
774
+ if left_has_glob and right_has_glob:
775
+ left_prefix = _scope_literal_prefix(left)
776
+ right_prefix = _scope_literal_prefix(right)
777
+ return bool(
778
+ not left_prefix
779
+ or not right_prefix
780
+ or left_prefix.startswith(right_prefix)
781
+ or right_prefix.startswith(left_prefix)
782
+ )
783
+
784
+ left_root = left.rstrip("/")
785
+ right_root = right.rstrip("/")
786
+ return bool(
787
+ (left.endswith("/") and right.startswith(left_root + "/"))
788
+ or (right.endswith("/") and left.startswith(right_root + "/"))
789
+ )
790
+
791
+
792
+ def write_scopes_overlap(left: list[str], right: list[str]) -> bool:
793
+ left_scopes = normalize_required_write_scopes(left)
794
+ right_scopes = normalize_required_write_scopes(right)
795
+ if not left_scopes or not right_scopes:
796
+ return False
797
+ for left_scope in left_scopes:
798
+ for right_scope in right_scopes:
799
+ if _scope_pair_overlaps(left_scope, right_scope):
800
+ return True
801
+ return False
802
+
803
+
804
+ def active_conflicts(
805
+ *,
806
+ registry_path: Path,
807
+ runtime_root: Path,
808
+ goal_id: str,
809
+ todo_id: str,
810
+ write_scopes: list[str],
811
+ at: datetime,
812
+ ) -> list[dict[str, Any]]:
813
+ conflicts: list[dict[str, Any]] = []
814
+ registered_agents = registered_agent_ids_from_registry(registry_path, goal_id)
815
+ lease_dir = task_lease_dir(runtime_root=runtime_root, goal_id=goal_id)
816
+ if not lease_dir.exists():
817
+ return conflicts
818
+ for path in sorted(lease_dir.glob("todo_*.json")):
819
+ lease = read_lease(path)
820
+ if not lease_is_active(lease, at=at):
821
+ continue
822
+ lease_todo_id = normalize_lease_todo_id(lease.get("todo_id"))
823
+ if lease_todo_id == todo_id:
824
+ continue
825
+ todo = task_lease_todo_projection(
826
+ registry_path=registry_path,
827
+ goal_id=goal_id,
828
+ todo_id=lease_todo_id,
829
+ )
830
+ if task_lease_owner_constraint(
831
+ todo,
832
+ owner=lease.get("owner"),
833
+ registered_agents=registered_agents,
834
+ ).get("effective") is not True:
835
+ continue
836
+ if write_scopes_overlap(write_scopes, normalize_required_write_scopes(lease.get("write_scopes"))):
837
+ conflicts.append(
838
+ {
839
+ "todo_id": lease.get("todo_id"),
840
+ "owner": lease.get("owner"),
841
+ "expires_at": lease.get("expires_at"),
842
+ "write_scopes": lease.get("write_scopes") or [],
843
+ "lease_path": str(path),
844
+ }
845
+ )
846
+ return conflicts
847
+
848
+
849
+ def assert_expected_version(lease: dict[str, Any] | None, expected_version: int | None) -> None:
850
+ if expected_version is None:
851
+ return
852
+ actual = int((lease or {}).get("version") or 0)
853
+ if actual != expected_version:
854
+ raise TaskLeaseError(
855
+ f"lease version mismatch: expected {expected_version}, got {actual}",
856
+ code="version_mismatch",
857
+ payload={"expected_version": expected_version, "actual_version": actual},
858
+ )
859
+
860
+
861
+ def build_lease(
862
+ *,
863
+ goal_id: str,
864
+ todo_id: str,
865
+ owner: str,
866
+ idempotency_key: str,
867
+ write_scopes: list[str],
868
+ acquire_ttl_seconds: int,
869
+ version: int,
870
+ acquired_at: str,
871
+ updated_at: str,
872
+ expires_at: str,
873
+ ) -> dict[str, Any]:
874
+ return {
875
+ "schema_version": TASK_LEASE_SCHEMA_VERSION,
876
+ "goal_id": goal_id,
877
+ "todo_id": todo_id,
878
+ "owner": owner,
879
+ "idempotency_key": idempotency_key,
880
+ "write_scopes": write_scopes,
881
+ "acquire_ttl_seconds": acquire_ttl_seconds,
882
+ "version": version,
883
+ "acquired_at": acquired_at,
884
+ "updated_at": updated_at,
885
+ "expires_at": expires_at,
886
+ "status": "active",
887
+ }
888
+
889
+
890
+ def acquire_task_lease(
891
+ *,
892
+ registry_path: Path,
893
+ runtime_root: Path,
894
+ goal_id: str,
895
+ todo_id: str,
896
+ owner: str,
897
+ idempotency_key: str,
898
+ ttl_seconds: int | None = None,
899
+ write_scopes: list[str] | None = None,
900
+ expected_version: int | None = None,
901
+ ) -> dict[str, Any]:
902
+ goal_id = normalize_goal_id(goal_id)
903
+ todo_id = normalize_lease_todo_id(todo_id)
904
+ owner = normalize_owner(owner)
905
+ idempotency_key = normalize_idempotency_key(idempotency_key)
906
+ ttl = normalize_ttl_seconds(ttl_seconds)
907
+ normalized_write_scopes = normalize_required_write_scopes(write_scopes)
908
+ lease_dir = task_lease_dir(runtime_root=runtime_root, goal_id=goal_id)
909
+ lock_target = task_lease_lock_path(runtime_root=runtime_root, goal_id=goal_id)
910
+ lease_path = task_lease_path(runtime_root=runtime_root, goal_id=goal_id, todo_id=todo_id)
911
+ at = now_utc()
912
+ with exclusive_file_lock(
913
+ lock_target,
914
+ agent_id=owner,
915
+ operation="task_lease_acquire",
916
+ ):
917
+ handoff_mode = _require_lease_mutation_allowed_by_handoff_mode(
918
+ registry_path=registry_path,
919
+ goal_id=goal_id,
920
+ todo_id=todo_id,
921
+ action="acquire",
922
+ )
923
+ todo = require_task_lease_owner_allowed(
924
+ registry_path=registry_path,
925
+ goal_id=goal_id,
926
+ todo_id=todo_id,
927
+ owner=owner,
928
+ )
929
+ existing = read_lease(lease_path)
930
+ assert_expected_version(existing, expected_version)
931
+ existing_is_effective = bool(
932
+ lease_is_active(existing, at=at)
933
+ and task_lease_owner_constraint(
934
+ todo,
935
+ owner=(existing or {}).get("owner"),
936
+ registered_agents=registered_agent_ids_from_registry(registry_path, goal_id),
937
+ ).get("effective")
938
+ is True
939
+ )
940
+ if existing_is_effective:
941
+ if (
942
+ existing.get("owner") == owner
943
+ and existing.get("idempotency_key") == idempotency_key
944
+ ):
945
+ existing_write_scopes = normalize_required_write_scopes(
946
+ existing.get("write_scopes")
947
+ )
948
+ existing_ttl = existing.get("acquire_ttl_seconds")
949
+ request_matches = set(existing_write_scopes) == set(normalized_write_scopes)
950
+ if existing_ttl is not None:
951
+ request_matches = request_matches and int(existing_ttl) == ttl
952
+ if not request_matches:
953
+ raise TaskLeaseError(
954
+ "idempotency key was reused with different acquire parameters",
955
+ code="idempotency_key_reuse",
956
+ payload={
957
+ "lease": existing,
958
+ "lease_path": str(lease_path),
959
+ "requested_write_scopes": normalized_write_scopes,
960
+ "requested_ttl_seconds": ttl,
961
+ },
962
+ )
963
+ return {
964
+ "ok": True,
965
+ "schema_version": TASK_LEASE_SCHEMA_VERSION,
966
+ "action": "acquire",
967
+ "acquired": False,
968
+ "idempotent": True,
969
+ "lease": existing,
970
+ "lease_path": str(lease_path),
971
+ "handoff_mode": handoff_mode,
972
+ }
973
+ raise TaskLeaseError(
974
+ "todo already has an active lease",
975
+ code="todo_lease_conflict",
976
+ payload={"lease": existing, "lease_path": str(lease_path)},
977
+ )
978
+ conflicts = active_conflicts(
979
+ registry_path=registry_path,
980
+ runtime_root=runtime_root,
981
+ goal_id=goal_id,
982
+ todo_id=todo_id,
983
+ write_scopes=normalized_write_scopes,
984
+ at=at,
985
+ )
986
+ if conflicts:
987
+ raise TaskLeaseError(
988
+ "write scope overlaps another active task lease",
989
+ code="write_scope_conflict",
990
+ payload={"conflicts": conflicts},
991
+ )
992
+ updated_at = isoformat(at)
993
+ expires_at = isoformat(at + timedelta(seconds=ttl))
994
+ lease = build_lease(
995
+ goal_id=goal_id,
996
+ todo_id=todo_id,
997
+ owner=owner,
998
+ idempotency_key=idempotency_key,
999
+ write_scopes=normalized_write_scopes,
1000
+ acquire_ttl_seconds=ttl,
1001
+ version=int((existing or {}).get("version") or 0) + 1,
1002
+ acquired_at=updated_at,
1003
+ updated_at=updated_at,
1004
+ expires_at=expires_at,
1005
+ )
1006
+ lease_dir.mkdir(parents=True, exist_ok=True)
1007
+ write_lease(lease_path, lease)
1008
+ return {
1009
+ "ok": True,
1010
+ "schema_version": TASK_LEASE_SCHEMA_VERSION,
1011
+ "action": "acquire",
1012
+ "acquired": True,
1013
+ "idempotent": False,
1014
+ "lease": lease,
1015
+ "lease_path": str(lease_path),
1016
+ "handoff_mode": handoff_mode,
1017
+ }
1018
+
1019
+
1020
+ def renew_task_lease(
1021
+ *,
1022
+ registry_path: Path,
1023
+ runtime_root: Path,
1024
+ goal_id: str,
1025
+ todo_id: str,
1026
+ owner: str,
1027
+ idempotency_key: str,
1028
+ ttl_seconds: int | None = None,
1029
+ expected_version: int | None = None,
1030
+ ) -> dict[str, Any]:
1031
+ goal_id = normalize_goal_id(goal_id)
1032
+ todo_id = normalize_lease_todo_id(todo_id)
1033
+ owner = normalize_owner(owner)
1034
+ idempotency_key = normalize_idempotency_key(idempotency_key)
1035
+ ttl = normalize_ttl_seconds(ttl_seconds)
1036
+ lock_target = task_lease_lock_path(runtime_root=runtime_root, goal_id=goal_id)
1037
+ lease_path = task_lease_path(runtime_root=runtime_root, goal_id=goal_id, todo_id=todo_id)
1038
+ at = now_utc()
1039
+ with exclusive_file_lock(
1040
+ lock_target,
1041
+ agent_id=owner,
1042
+ operation="task_lease_renew",
1043
+ ):
1044
+ handoff_mode = _require_lease_mutation_allowed_by_handoff_mode(
1045
+ registry_path=registry_path,
1046
+ goal_id=goal_id,
1047
+ todo_id=todo_id,
1048
+ action="renew",
1049
+ )
1050
+ require_task_lease_owner_allowed(
1051
+ registry_path=registry_path,
1052
+ goal_id=goal_id,
1053
+ todo_id=todo_id,
1054
+ owner=owner,
1055
+ )
1056
+ lease = read_lease(lease_path)
1057
+ assert_expected_version(lease, expected_version)
1058
+ if not lease_is_active(lease, at=at):
1059
+ raise TaskLeaseError("lease is missing or expired", code="lease_not_active")
1060
+ if lease.get("owner") != owner or lease.get("idempotency_key") != idempotency_key:
1061
+ raise TaskLeaseError("lease owner or idempotency key mismatch", code="lease_cas_mismatch")
1062
+ lease = dict(lease)
1063
+ lease["version"] = int(lease.get("version") or 0) + 1
1064
+ lease["updated_at"] = isoformat(at)
1065
+ lease["expires_at"] = isoformat(at + timedelta(seconds=ttl))
1066
+ write_lease(lease_path, lease)
1067
+ return {
1068
+ "ok": True,
1069
+ "schema_version": TASK_LEASE_SCHEMA_VERSION,
1070
+ "action": "renew",
1071
+ "renewed": True,
1072
+ "lease": lease,
1073
+ "lease_path": str(lease_path),
1074
+ "handoff_mode": handoff_mode,
1075
+ }
1076
+
1077
+
1078
+ def transfer_task_lease(
1079
+ *,
1080
+ registry_path: Path,
1081
+ runtime_root: Path,
1082
+ goal_id: str,
1083
+ todo_id: str,
1084
+ owner: str,
1085
+ idempotency_key: str,
1086
+ new_owner: str,
1087
+ new_idempotency_key: str,
1088
+ ttl_seconds: int | None = None,
1089
+ expected_version: int | None = None,
1090
+ ) -> dict[str, Any]:
1091
+ goal_id = normalize_goal_id(goal_id)
1092
+ todo_id = normalize_lease_todo_id(todo_id)
1093
+ owner = normalize_owner(owner)
1094
+ idempotency_key = normalize_idempotency_key(idempotency_key)
1095
+ new_owner = normalize_owner(new_owner)
1096
+ new_idempotency_key = normalize_idempotency_key(new_idempotency_key)
1097
+ ttl = normalize_ttl_seconds(ttl_seconds)
1098
+ lock_target = task_lease_lock_path(runtime_root=runtime_root, goal_id=goal_id)
1099
+ lease_path = task_lease_path(runtime_root=runtime_root, goal_id=goal_id, todo_id=todo_id)
1100
+ at = now_utc()
1101
+ with exclusive_file_lock(
1102
+ lock_target,
1103
+ agent_id=owner,
1104
+ operation="task_lease_transfer",
1105
+ ):
1106
+ handoff_mode = _require_lease_mutation_allowed_by_handoff_mode(
1107
+ registry_path=registry_path,
1108
+ goal_id=goal_id,
1109
+ todo_id=todo_id,
1110
+ action="transfer",
1111
+ )
1112
+ require_registered_task_lease_owner(
1113
+ registry_path=registry_path,
1114
+ goal_id=goal_id,
1115
+ todo_id=todo_id,
1116
+ owner=owner,
1117
+ )
1118
+ require_task_lease_owner_allowed(
1119
+ registry_path=registry_path,
1120
+ goal_id=goal_id,
1121
+ todo_id=todo_id,
1122
+ owner=new_owner,
1123
+ )
1124
+ lease = read_lease(lease_path)
1125
+ assert_expected_version(lease, expected_version)
1126
+ if not lease_is_active(lease, at=at):
1127
+ raise TaskLeaseError("lease is missing or expired", code="lease_not_active")
1128
+ if lease.get("owner") != owner or lease.get("idempotency_key") != idempotency_key:
1129
+ raise TaskLeaseError("lease owner or idempotency key mismatch", code="lease_cas_mismatch")
1130
+ lease = dict(lease)
1131
+ lease["owner"] = new_owner
1132
+ lease["idempotency_key"] = new_idempotency_key
1133
+ lease["version"] = int(lease.get("version") or 0) + 1
1134
+ lease["updated_at"] = isoformat(at)
1135
+ lease["expires_at"] = isoformat(at + timedelta(seconds=ttl))
1136
+ write_lease(lease_path, lease)
1137
+ return {
1138
+ "ok": True,
1139
+ "schema_version": TASK_LEASE_SCHEMA_VERSION,
1140
+ "action": "transfer",
1141
+ "transferred": True,
1142
+ "lease": lease,
1143
+ "lease_path": str(lease_path),
1144
+ "handoff_mode": handoff_mode,
1145
+ }
1146
+
1147
+
1148
+ def release_task_lease(
1149
+ *,
1150
+ runtime_root: Path,
1151
+ goal_id: str,
1152
+ todo_id: str,
1153
+ owner: str,
1154
+ idempotency_key: str,
1155
+ expected_version: int | None = None,
1156
+ registry_path: Path | None = None,
1157
+ ) -> dict[str, Any]:
1158
+ goal_id = normalize_goal_id(goal_id)
1159
+ todo_id = normalize_lease_todo_id(todo_id)
1160
+ owner = normalize_owner(owner)
1161
+ idempotency_key = normalize_idempotency_key(idempotency_key)
1162
+ # Release stays allowed in every handoff mode (cleanup of legacy
1163
+ # leftovers); the mode is reported additively when resolvable.
1164
+ handoff_mode = _optional_handoff_mode(registry_path, goal_id)
1165
+ handoff_extra = {"handoff_mode": handoff_mode} if handoff_mode else {}
1166
+ lock_target = task_lease_lock_path(runtime_root=runtime_root, goal_id=goal_id)
1167
+ lease_path = task_lease_path(runtime_root=runtime_root, goal_id=goal_id, todo_id=todo_id)
1168
+ at = now_utc()
1169
+ with exclusive_file_lock(
1170
+ lock_target,
1171
+ agent_id=owner,
1172
+ operation="task_lease_release",
1173
+ ):
1174
+ lease = read_lease(lease_path)
1175
+ assert_expected_version(lease, expected_version)
1176
+ if not lease:
1177
+ return {
1178
+ "ok": True,
1179
+ "schema_version": TASK_LEASE_SCHEMA_VERSION,
1180
+ "action": "release",
1181
+ "released": False,
1182
+ "missing": True,
1183
+ "lease_path": str(lease_path),
1184
+ **handoff_extra,
1185
+ }
1186
+ if lease_is_active(lease, at=at) and (
1187
+ lease.get("owner") != owner or lease.get("idempotency_key") != idempotency_key
1188
+ ):
1189
+ raise TaskLeaseError("lease owner or idempotency key mismatch", code="lease_cas_mismatch")
1190
+ remove_lease(lease_path)
1191
+ released_lease = dict(lease)
1192
+ released_lease["status"] = "released"
1193
+ released_lease["released_at"] = isoformat(at)
1194
+ released_lease["updated_at"] = isoformat(at)
1195
+ return {
1196
+ "ok": True,
1197
+ "schema_version": TASK_LEASE_SCHEMA_VERSION,
1198
+ "action": "release",
1199
+ "released": True,
1200
+ "lease": released_lease,
1201
+ "lease_path": str(lease_path),
1202
+ **handoff_extra,
1203
+ }
1204
+
1205
+
1206
+ def inspect_task_lease(
1207
+ *,
1208
+ registry_path: Path,
1209
+ runtime_root: Path,
1210
+ goal_id: str,
1211
+ todo_id: str,
1212
+ ) -> dict[str, Any]:
1213
+ goal_id = normalize_goal_id(goal_id)
1214
+ todo_id = normalize_lease_todo_id(todo_id)
1215
+ lease_path = task_lease_path(runtime_root=runtime_root, goal_id=goal_id, todo_id=todo_id)
1216
+ lease = read_lease(lease_path)
1217
+ active = lease_is_active(lease)
1218
+ executor_constraint: dict[str, Any] | None = None
1219
+ if active and lease:
1220
+ try:
1221
+ todo = task_lease_todo_projection(
1222
+ registry_path=registry_path,
1223
+ goal_id=goal_id,
1224
+ todo_id=todo_id,
1225
+ )
1226
+ except TaskLeaseError as exc:
1227
+ active = False
1228
+ executor_constraint = {
1229
+ "effective": False,
1230
+ "reason": exc.code,
1231
+ }
1232
+ else:
1233
+ executor_constraint = task_lease_owner_constraint(
1234
+ todo,
1235
+ owner=lease.get("owner"),
1236
+ registered_agents=registered_agent_ids_from_registry(registry_path, goal_id),
1237
+ )
1238
+ if executor_constraint.get("effective") is not True:
1239
+ active = False
1240
+ else:
1241
+ executor_constraint = None
1242
+ handoff_mode = _optional_handoff_mode(registry_path, goal_id)
1243
+ return {
1244
+ "ok": True,
1245
+ "schema_version": TASK_LEASE_SCHEMA_VERSION,
1246
+ "action": "inspect",
1247
+ "goal_id": goal_id,
1248
+ "todo_id": todo_id,
1249
+ "active": active,
1250
+ "lease": lease,
1251
+ "lease_path": str(lease_path),
1252
+ **({"handoff_mode": handoff_mode} if handoff_mode else {}),
1253
+ **({"executor_constraint": executor_constraint} if executor_constraint else {}),
1254
+ }