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
loopx/todos.py ADDED
@@ -0,0 +1,2229 @@
1
+ from __future__ import annotations
2
+
3
+ from contextlib import ExitStack
4
+ from json import dumps as json_dumps
5
+ from json import loads as json_loads
6
+ from pathlib import Path
7
+ from typing import Any
8
+
9
+ from .agent_registry import registered_agent_ids_from_registry, require_registered_agent_id
10
+ from .file_lock import exclusive_file_lock
11
+ from .history import load_registry
12
+ from .paths import resolve_runtime_root
13
+ from .rollout_event_log import load_rollout_events, rollout_event_log_path
14
+ from .control_plane.runtime.local_state_write_correctness import build_todo_write_correctness_dry_run_packet
15
+ from .state_refresh import now_local, resolve_goal_state
16
+ from .status import (
17
+ MAX_ACTIVE_DONE_TODOS_BEFORE_ARCHIVE,
18
+ active_state_event_projection_fields,
19
+ )
20
+ from .control_plane.todos.active_state_todo_parser import parse_active_state_todos
21
+ from .control_plane.todos.contract import (
22
+ TodoContinuationPolicy,
23
+ TODO_STATUS_DEFERRED,
24
+ TODO_STATUS_DONE,
25
+ TODO_STATUS_OPEN,
26
+ TODO_TASK_CLASS_USER_GATE,
27
+ build_todo_id,
28
+ format_todo_metadata_line,
29
+ metadata_line_for_todo_block,
30
+ merge_todo_id_lists,
31
+ normalize_required_capabilities,
32
+ normalize_required_write_scopes,
33
+ normalize_explore_result_node_refs,
34
+ normalize_target_capabilities,
35
+ normalize_todo_blocks_agent,
36
+ normalize_todo_bound_agent,
37
+ normalize_todo_capability_binding_ref,
38
+ normalize_todo_claimed_by,
39
+ normalize_todo_continuation_policy,
40
+ normalize_todo_decision_scope,
41
+ normalize_todo_excluded_agents,
42
+ normalize_todo_global_gate,
43
+ normalize_todo_goal_bound,
44
+ normalize_todo_id,
45
+ normalize_todo_id_list,
46
+ normalize_todo_required_decision_scopes,
47
+ normalize_todo_replan_obligation_id,
48
+ normalize_todo_resume_when,
49
+ normalize_supported_todo_resume_when,
50
+ normalize_todo_status,
51
+ normalize_todo_task_domain,
52
+ normalize_todo_task_repository,
53
+ parse_todo_metadata_line,
54
+ require_todo_excluded_agents,
55
+ resolve_next_user_task_class,
56
+ resolve_todo_continuation_policy,
57
+ require_supported_todo_resume_when,
58
+ todo_marker_for_status,
59
+ )
60
+ from .control_plane.todos.active_state_editing import (
61
+ TODO_SECTION_HEADINGS,
62
+ find_todo_block,
63
+ insert_into_existing_section,
64
+ insert_new_section,
65
+ replace_updated_at,
66
+ section_bounds,
67
+ set_todo_marker,
68
+ todo_blocks,
69
+ )
70
+ from .control_plane.todos.addition import matching_todo_block, require_replan_successor_rebinding, require_replan_successor_scope
71
+ from .control_plane.todos.completed_archive import archive_completed_todo_lines
72
+ from .control_plane.todos.completion_policy import (
73
+ linked_successor_from_todo,
74
+ resolve_completion_policy,
75
+ )
76
+ from .control_plane.todos.completion_fence import completed_todo_replay
77
+ from .control_plane.todos.completion_validation import (
78
+ COMPLETION_VALIDATION_TIMEOUT_MAX_SECONDS,
79
+ run_completion_validation_gate,
80
+ )
81
+ from .control_plane.todos.event_writeback import (
82
+ complete_event_projected_goal_todo,
83
+ event_projection_todo_context,
84
+ )
85
+ from .control_plane.todos.line_update import (
86
+ apply_todo_update_to_lines,
87
+ link_generated_successor_todo_ids,
88
+ upsert_todo_metadata,
89
+ )
90
+ from .control_plane.todos.list_projection import (
91
+ compact_agent_lane_todo_summary,
92
+ compact_todo_projection_overlay,
93
+ todo_item_relations,
94
+ todo_list_projection_contract,
95
+ )
96
+ from .control_plane.todos import monitor_metadata as todo_monitor_metadata
97
+ from .control_plane.todos.mutation_authority import authorize_todo_lifecycle_mutation, todo_update_authority_action
98
+ from .control_plane.todos.todo_summary import compact_todo_group, todo_item_status
99
+ from .control_plane.todos.succession_warning import build_open_parent_successor_advisory
100
+ from .control_plane.todos.todo_index import MAX_TODO_INDEX_ROLLOUT_EVENTS_PER_GOAL
101
+ from .control_plane.todos.text import (
102
+ inherit_todo_priority,
103
+ normalize_new_todo,
104
+ )
105
+ from .control_plane.todos.unblock_resume import (
106
+ apply_completed_user_todo_lifecycle,
107
+ require_completion_decision_outcome,
108
+ )
109
+ from .control_plane.todos.write_policy import (
110
+ require_user_gate_scope,
111
+ require_user_todo_binding,
112
+ require_user_todo_task_class,
113
+ resolve_user_gate_global_gate_update,
114
+ )
115
+ from .control_plane.todos.handoff_mode import (
116
+ enter_added_todo_ownership_handoff_gate,
117
+ enter_todo_ownership_handoff_gate,
118
+ resolve_todo_completion_handoff,
119
+ )
120
+ from .control_plane.work_items.task_lease import (
121
+ hold_task_lease_mutation_fence,
122
+ release_verified_task_lease_fence,
123
+ )
124
+
125
+
126
+ ARCHIVE_COMPLETED_DEFAULT_MAX_ACTIVE_DONE = max(0, MAX_ACTIVE_DONE_TODOS_BEFORE_ARCHIVE - 2)
127
+
128
+
129
+ def require_registered_todo_excluded_agents(
130
+ *,
131
+ registry_path: Path,
132
+ goal_id: str,
133
+ excluded_agents: Any,
134
+ field: str = "excluded_agents",
135
+ ) -> list[str]:
136
+ return sorted(
137
+ require_registered_agent_id(
138
+ registry_path=registry_path,
139
+ goal_id=goal_id,
140
+ agent_id=agent_id,
141
+ field=field,
142
+ )
143
+ for agent_id in require_todo_excluded_agents(excluded_agents, field=field)
144
+ )
145
+
146
+
147
+ def _attach_todo_write_correctness_dry_run_packet(
148
+ payload: dict[str, Any],
149
+ *,
150
+ goal_id: str,
151
+ write_class: str,
152
+ state_text: str,
153
+ ) -> dict[str, Any]:
154
+ if not payload.get("dry_run"):
155
+ return payload
156
+ todo_id = normalize_todo_id(str(payload.get("todo_id") or "")) or None
157
+ claimed_by = normalize_todo_claimed_by(payload.get("claimed_by"))
158
+ changed = bool(
159
+ payload.get("changed")
160
+ or payload.get("added")
161
+ or payload.get("metadata_updated")
162
+ or payload.get("completed")
163
+ or payload.get("superseded")
164
+ )
165
+ payload["local_state_write_correctness"] = build_todo_write_correctness_dry_run_packet(
166
+ goal_id=goal_id,
167
+ write_class=write_class,
168
+ state_text=state_text,
169
+ todo_id=todo_id,
170
+ role=str(payload.get("role") or ""),
171
+ section=str(payload.get("section") or ""),
172
+ claimed_by=claimed_by,
173
+ changed=changed,
174
+ )
175
+ return payload
176
+ def resolve_todo_state_path(
177
+ *,
178
+ registry_path: Path,
179
+ goal_id: str,
180
+ project: Path | None = None,
181
+ state_file: Path | None = None,
182
+ ) -> tuple[Path | None, Path]:
183
+ registry = load_registry(registry_path)
184
+ goal, resolved_project, resolved_state_file = resolve_goal_state(
185
+ registry=registry,
186
+ goal_id=goal_id,
187
+ project_override=project,
188
+ state_file_override=state_file,
189
+ )
190
+ if goal is None:
191
+ raise ValueError(f"goal {goal_id!r} is not present in the registry")
192
+ if not resolved_state_file.exists():
193
+ raise ValueError(f"active state file does not exist: {resolved_state_file}")
194
+ return resolved_project, resolved_state_file
195
+
196
+
197
+ def empty_todo_summary(*, role: str) -> dict[str, Any]:
198
+ return {
199
+ "schema_version": "todo_summary_v0",
200
+ "role": role,
201
+ "source_section": TODO_SECTION_HEADINGS[role],
202
+ "total_count": 0,
203
+ "open_count": 0,
204
+ "done_count": 0,
205
+ "items": [],
206
+ "first_open_items": [],
207
+ }
208
+
209
+
210
+ def _user_todo_visible_to_agent(item: dict[str, Any], agent_id: str) -> bool:
211
+ if bool(item.get("global_gate")):
212
+ return True
213
+ blocks_agent = normalize_todo_blocks_agent(item.get("blocks_agent"))
214
+ if blocks_agent:
215
+ return blocks_agent == agent_id
216
+ bound_agent = normalize_todo_bound_agent(item.get("bound_agent"))
217
+ if bound_agent:
218
+ return bound_agent == agent_id
219
+ return True
220
+
221
+
222
+ def filtered_todo_summary(
223
+ summary: dict[str, Any] | None,
224
+ *,
225
+ role: str,
226
+ status: str | None = None,
227
+ todo_id: str | None = None,
228
+ agent_id: str | None = None,
229
+ resume_source_items: list[dict[str, Any]] | None = None,
230
+ rollout_events: list[dict[str, Any]] | None = None,
231
+ ) -> dict[str, Any]:
232
+ items = list((summary or {}).get("items") or [])
233
+ normalized_status = normalize_todo_status(status)
234
+ if normalized_status:
235
+ items = [item for item in items if todo_item_status(item) == normalized_status]
236
+ normalized_todo_id = normalize_todo_id(todo_id) if todo_id else None
237
+ if normalized_todo_id:
238
+ items = [
239
+ item
240
+ for item in items
241
+ if normalize_todo_id(item.get("todo_id")) == normalized_todo_id
242
+ ]
243
+ normalized_agent_id = normalize_todo_claimed_by(agent_id) if agent_id else None
244
+ if normalized_agent_id:
245
+ if role == "agent":
246
+ items = [
247
+ item
248
+ for item in items
249
+ if normalized_agent_id
250
+ not in normalize_todo_excluded_agents(item.get("excluded_agents"))
251
+ and (
252
+ not normalize_todo_claimed_by(item.get("claimed_by"))
253
+ or normalize_todo_claimed_by(item.get("claimed_by"))
254
+ == normalized_agent_id
255
+ )
256
+ ]
257
+ elif role == "user":
258
+ items = [
259
+ item
260
+ for item in items
261
+ if _user_todo_visible_to_agent(item, normalized_agent_id)
262
+ ]
263
+ source_section = str((summary or {}).get("source_section") or TODO_SECTION_HEADINGS[role])
264
+ return (
265
+ compact_todo_group(
266
+ items,
267
+ source_section=source_section,
268
+ role=role,
269
+ resume_source_items=resume_source_items,
270
+ rollout_events=rollout_events,
271
+ item_limit=None,
272
+ )
273
+ or empty_todo_summary(role=role)
274
+ )
275
+
276
+
277
+ def _summary_items(fields: dict[str, Any], role: str) -> list[dict[str, Any]]:
278
+ summary = fields.get(f"{role}_todos") if isinstance(fields, dict) else None
279
+ if not isinstance(summary, dict):
280
+ return []
281
+ return [item for item in summary.get("items") or [] if isinstance(item, dict)]
282
+
283
+
284
+ def _merge_todo_projection_fields(
285
+ *,
286
+ markdown_fields: dict[str, Any],
287
+ event_fields: dict[str, Any],
288
+ ) -> tuple[dict[str, Any], dict[str, Any]]:
289
+ merged: dict[str, Any] = {}
290
+ merged_items: dict[str, list[dict[str, Any]]] = {"user": [], "agent": []}
291
+ source_sections: dict[str, str] = {}
292
+ overlay: dict[str, Any] = {
293
+ "schema_version": "todo_list_projection_overlay_v0",
294
+ "base": "markdown_active_state",
295
+ "overlay": "event_projection",
296
+ "markdown_only_todo_ids": [],
297
+ "event_only_todo_ids": [],
298
+ "overlaid_todo_ids": [],
299
+ }
300
+
301
+ # A todo_id is goal-wide identity. Merge both sources before splitting by
302
+ # role so an event-projected role change replaces the stale Markdown item.
303
+ by_id: dict[str, dict[str, Any]] = {}
304
+ order: list[str] = []
305
+ markdown_ids: set[str] = set()
306
+ markdown_ids_by_role: dict[str, set[str]] = {"user": set(), "agent": set()}
307
+ event_ids: set[str] = set()
308
+ event_order: list[str] = []
309
+ for role in ("user", "agent"):
310
+ markdown_items = _summary_items(markdown_fields, role)
311
+ for item in markdown_items:
312
+ todo_id = normalize_todo_id(item.get("todo_id")) or build_todo_id(
313
+ role=role,
314
+ source_section=item.get("source_section"),
315
+ index=item.get("index"),
316
+ text=item.get("text"),
317
+ )
318
+ if todo_id not in by_id:
319
+ order.append(todo_id)
320
+ markdown_ids.add(todo_id)
321
+ markdown_ids_by_role[role].add(todo_id)
322
+ by_id[todo_id] = dict(item)
323
+
324
+ for role in ("user", "agent"):
325
+ event_items = _summary_items(event_fields, role)
326
+ for item in event_items:
327
+ todo_id = normalize_todo_id(item.get("todo_id")) or build_todo_id(
328
+ role=role,
329
+ source_section=item.get("source_section"),
330
+ index=item.get("index"),
331
+ text=item.get("text"),
332
+ )
333
+ if todo_id not in by_id:
334
+ order.append(todo_id)
335
+ if todo_id not in event_ids:
336
+ event_order.append(todo_id)
337
+ event_ids.add(todo_id)
338
+ by_id[todo_id] = dict(item)
339
+
340
+ markdown_only_todo_ids: list[str] = []
341
+ seen_markdown_only_ids: set[str] = set()
342
+ for role in ("user", "agent"):
343
+ for todo_id in sorted(markdown_ids_by_role[role] - event_ids):
344
+ if todo_id not in seen_markdown_only_ids:
345
+ markdown_only_todo_ids.append(todo_id)
346
+ seen_markdown_only_ids.add(todo_id)
347
+ overlay["markdown_only_todo_ids"] = markdown_only_todo_ids
348
+ overlay["event_only_todo_ids"] = [
349
+ todo_id for todo_id in event_order if todo_id not in markdown_ids
350
+ ]
351
+ overlay["overlaid_todo_ids"] = [
352
+ todo_id for todo_id in event_order if todo_id in markdown_ids
353
+ ]
354
+
355
+ for todo_id in order:
356
+ item = by_id[todo_id]
357
+ final_role = "user" if item.get("role") == "user" else "agent"
358
+ merged_items[final_role].append(item)
359
+
360
+ for role in ("user", "agent"):
361
+ source_section = str(
362
+ (markdown_fields.get(f"{role}_todos") or {}).get("source_section")
363
+ or (event_fields.get(f"{role}_todos") or {}).get("source_section")
364
+ or TODO_SECTION_HEADINGS[role]
365
+ )
366
+ source_sections[role] = source_section
367
+
368
+ resume_source_items = [*merged_items["user"], *merged_items["agent"]]
369
+ for role in ("user", "agent"):
370
+ if not merged_items[role]:
371
+ continue
372
+ summary = compact_todo_group(
373
+ merged_items[role],
374
+ source_section=source_sections[role],
375
+ role=role,
376
+ resume_source_items=resume_source_items,
377
+ item_limit=None,
378
+ )
379
+ if summary:
380
+ merged[f"{role}_todos"] = summary
381
+ return merged, overlay
382
+
383
+
384
+ def list_goal_todos(
385
+ *,
386
+ registry_path: Path,
387
+ goal_id: str,
388
+ role: str | None = None,
389
+ status: str | None = None,
390
+ todo_id: str | None = None,
391
+ agent_id: str | None = None,
392
+ project: Path | None = None,
393
+ state_file: Path | None = None,
394
+ runtime_root_arg: str | None = None,
395
+ ) -> dict[str, Any]:
396
+ normalized_todo_id = normalize_todo_id(todo_id) if todo_id else None
397
+ if todo_id and not normalized_todo_id:
398
+ raise ValueError("todo_id must use the public token shape todo_<letters-digits-underscore-hyphen>")
399
+ normalized_agent_id = normalize_todo_claimed_by(agent_id) if agent_id else None
400
+ if agent_id and not normalized_agent_id:
401
+ raise ValueError("agent_id must be a public-safe agent token such as codex-main-control")
402
+ registry = load_registry(registry_path)
403
+ goal, resolved_project, resolved_state_file = resolve_goal_state(
404
+ registry=registry,
405
+ goal_id=goal_id,
406
+ project_override=project,
407
+ state_file_override=state_file,
408
+ )
409
+ if goal is None:
410
+ raise ValueError(f"goal {goal_id!r} is not present in the registry")
411
+ if not resolved_state_file.exists():
412
+ raise ValueError(f"active state file does not exist: {resolved_state_file}")
413
+
414
+ runtime_root = resolve_runtime_root(registry, runtime_root_arg)
415
+ rollout_events = load_rollout_events(
416
+ rollout_event_log_path(runtime_root, goal_id),
417
+ limit=MAX_TODO_INDEX_ROLLOUT_EVENTS_PER_GOAL,
418
+ )
419
+
420
+ projection_fields = active_state_event_projection_fields(
421
+ goal,
422
+ state_path=resolved_state_file,
423
+ item_limit=None,
424
+ rollout_events=rollout_events,
425
+ )
426
+ projection_has_todos = bool(
427
+ projection_fields.get("user_todos") or projection_fields.get("agent_todos")
428
+ )
429
+ markdown_fields = parse_active_state_todos(
430
+ resolved_state_file.read_text(encoding="utf-8"),
431
+ goal=goal,
432
+ state_path=resolved_state_file,
433
+ item_limit=None,
434
+ rollout_events=rollout_events,
435
+ )
436
+ markdown_has_todos = bool(
437
+ markdown_fields.get("user_todos") or markdown_fields.get("agent_todos")
438
+ )
439
+ projection_overlay: dict[str, Any] | None = None
440
+ if projection_has_todos and markdown_has_todos:
441
+ fields, projection_overlay = _merge_todo_projection_fields(
442
+ markdown_fields=markdown_fields,
443
+ event_fields=projection_fields,
444
+ )
445
+ source = "event_projection_with_markdown_overlay"
446
+ elif projection_has_todos:
447
+ fields = projection_fields
448
+ source = "event_projection"
449
+ else:
450
+ fields = markdown_fields
451
+ source = "markdown_active_state"
452
+
453
+ roles = [role] if role else ["user", "agent"]
454
+ resume_source_items = [
455
+ *_summary_items(fields, "user"),
456
+ *_summary_items(fields, "agent"),
457
+ ]
458
+ summaries: dict[str, dict[str, Any]] = {}
459
+ todos: list[dict[str, Any]] = []
460
+ unfiltered_count = 0
461
+ for item_role in roles:
462
+ key = f"{item_role}_todos"
463
+ raw_summary = fields.get(key) if isinstance(fields, dict) else None
464
+ unfiltered_count += len((raw_summary or {}).get("items") or [])
465
+ summary = filtered_todo_summary(
466
+ raw_summary,
467
+ role=item_role,
468
+ status=status,
469
+ todo_id=normalized_todo_id,
470
+ agent_id=normalized_agent_id,
471
+ resume_source_items=resume_source_items,
472
+ rollout_events=rollout_events,
473
+ )
474
+ summaries[key] = summary
475
+ todos.extend(summary.get("items") or [])
476
+
477
+ matched_todo_count = len(todos)
478
+ agent_lane_hot_path = bool(
479
+ normalized_agent_id
480
+ and role is None
481
+ and status is None
482
+ and normalized_todo_id is None
483
+ )
484
+ if agent_lane_hot_path:
485
+ summaries = {
486
+ key: compact_agent_lane_todo_summary(
487
+ summary,
488
+ role=key.removesuffix("_todos"),
489
+ )
490
+ for key, summary in summaries.items()
491
+ }
492
+ todos = [
493
+ item
494
+ for key in ("user_todos", "agent_todos")
495
+ for item in summaries.get(key, {}).get("items") or []
496
+ if isinstance(item, dict)
497
+ ]
498
+
499
+ matched_todo = todos[0] if len(todos) == 1 else None
500
+ payload: dict[str, Any] = {
501
+ "ok": True,
502
+ "dry_run": True,
503
+ "read_only": True,
504
+ "command": "list",
505
+ "goal_id": goal_id,
506
+ "role": role or "all",
507
+ "status_filter": normalize_todo_status(status) if status else None,
508
+ "source": source,
509
+ "todo_count": matched_todo_count,
510
+ "todos": todos,
511
+ "state_file": str(resolved_state_file),
512
+ "project": str(resolved_project) if resolved_project else None,
513
+ }
514
+ if normalized_agent_id:
515
+ payload["agent_id_filter"] = normalized_agent_id
516
+ payload["unfiltered_todo_count"] = unfiltered_count
517
+ payload["filter_semantics"] = (
518
+ "agent todos include unclaimed items plus claimed_by=<agent>; "
519
+ "user todos include global, unscoped legacy, blocks_agent=<agent> gates, "
520
+ "and bound_agent=<agent> actions"
521
+ )
522
+ if agent_lane_hot_path:
523
+ payload["returned_todo_count"] = len(todos)
524
+ payload["todo_list_projection"] = todo_list_projection_contract(
525
+ matched_todo_count=matched_todo_count,
526
+ returned_todo_count=len(todos),
527
+ )
528
+ if normalized_todo_id:
529
+ payload["todo_id_filter"] = normalized_todo_id
530
+ payload["matched"] = bool(todos)
531
+ payload["todo"] = matched_todo
532
+ payload["relations"] = todo_item_relations(matched_todo) if matched_todo else {}
533
+ if len(todos) > 1:
534
+ payload["ambiguous"] = True
535
+ if not todos:
536
+ payload["not_found"] = True
537
+ payload.update(summaries)
538
+ if source == "event_projection" and projection_fields.get("state_event_projection"):
539
+ payload["state_event_projection"] = projection_fields["state_event_projection"]
540
+ if source == "event_projection_with_markdown_overlay":
541
+ if projection_fields.get("state_event_projection"):
542
+ payload["state_event_projection"] = projection_fields["state_event_projection"]
543
+ payload["projection_overlay"] = (
544
+ compact_todo_projection_overlay(projection_overlay)
545
+ if agent_lane_hot_path
546
+ else projection_overlay
547
+ )
548
+ if projection_fields.get("state_event_projection_warning"):
549
+ payload["state_event_projection_warning"] = projection_fields["state_event_projection_warning"]
550
+ return payload
551
+
552
+
553
+ def _normalize_validation_command_json(raw: str | None) -> list[str] | None:
554
+ """Validate a ``--validation-command-json`` payload (run-once precedent).
555
+
556
+ Returns the argv list, or ``None`` when no JSON form is declared. Raises
557
+ ``ValueError`` when the payload is not a non-empty JSON string array —
558
+ the same shape rule the Turn-level ``--validation-command-json`` applies.
559
+ """
560
+ if raw is None:
561
+ return None
562
+ try:
563
+ argv = json_loads(raw)
564
+ except ValueError as exc:
565
+ raise ValueError(
566
+ "--validation-command-json must be a JSON string array"
567
+ ) from exc
568
+ if not isinstance(argv, list) or not argv or not all(
569
+ isinstance(item, str) and item for item in argv
570
+ ):
571
+ raise ValueError("--validation-command-json must be a JSON string array")
572
+ return argv
573
+
574
+
575
+ def add_todo_to_lines(
576
+ lines: list[str],
577
+ *,
578
+ role: str,
579
+ text: str,
580
+ status: str | None = None,
581
+ task_class: str | None = None,
582
+ action_kind: str | None = None,
583
+ task_domain: str | None = None,
584
+ capability_binding_ref: str | None = None,
585
+ task_repository: str | None = None,
586
+ continuation_policy: str | None = None,
587
+ required_write_scopes: list[str] | None = None,
588
+ required_capabilities: list[str] | None = None,
589
+ target_capabilities: list[str] | None = None,
590
+ explore_result_node_refs: list[str] | None = None,
591
+ decision_scope: Any = None,
592
+ required_decision_scopes: Any = None,
593
+ claimed_by: str | None = None,
594
+ bound_agent: str | None = None,
595
+ goal_bound: bool | None = None,
596
+ blocks_agent: str | None = None,
597
+ excluded_agents: list[str] | None = None,
598
+ global_gate: bool | None = None,
599
+ unblocks_todo_id: str | None = None,
600
+ replan_obligation_id: str | None = None,
601
+ resume_when: str | None = None,
602
+ validation_command: str | None = None,
603
+ validation_command_json: str | None = None,
604
+ validation_label: str | None = None,
605
+ validation_timeout_seconds: int | None = None,
606
+ monitor_metadata: dict[str, Any] | None = None,
607
+ evidence: str | None = None,
608
+ updated_at: str | None = None,
609
+ ) -> dict[str, Any]:
610
+ if validation_command and validation_command_json:
611
+ raise ValueError(
612
+ "--validation-command and --validation-command-json are mutually "
613
+ "exclusive; declare the validation command in exactly one form"
614
+ )
615
+ validation_argv = _normalize_validation_command_json(validation_command_json)
616
+ if validation_timeout_seconds is not None:
617
+ if not validation_command and validation_argv is None:
618
+ raise ValueError(
619
+ "--validation-timeout-seconds requires --validation-command "
620
+ "or --validation-command-json"
621
+ )
622
+ if not (
623
+ 1
624
+ <= validation_timeout_seconds
625
+ <= COMPLETION_VALIDATION_TIMEOUT_MAX_SECONDS
626
+ ):
627
+ raise ValueError(
628
+ "--validation-timeout-seconds must be between 1 and "
629
+ f"{COMPLETION_VALIDATION_TIMEOUT_MAX_SECONDS} (the outer "
630
+ "CLI/MCP subprocess budget is 30s, and a timed-out "
631
+ "validation must still produce a typed receipt)"
632
+ )
633
+ if role == "agent" and blocks_agent:
634
+ raise ValueError(
635
+ "blocks_agent is only valid for user gates; use excluded_agents for "
636
+ "agent executor constraints"
637
+ )
638
+ if role != "agent" and excluded_agents:
639
+ raise ValueError("excluded_agents is only valid for agent todos")
640
+ if role != "agent" and capability_binding_ref:
641
+ raise ValueError("capability_binding_ref is only valid for agent todos")
642
+ require_user_todo_task_class(
643
+ role=role,
644
+ task_class=task_class,
645
+ blocks_agent=blocks_agent,
646
+ global_gate=global_gate,
647
+ )
648
+ todo_text = normalize_new_todo(text)
649
+ normalized_status = normalize_todo_status(status) if status else TODO_STATUS_OPEN
650
+ if status and not normalized_status:
651
+ raise ValueError("todo status must be one of: open, done, blocked, deferred")
652
+ normalized_resume_when = require_supported_todo_resume_when(resume_when)
653
+ normalized_monitor_metadata = todo_monitor_metadata.require_monitor_metadata_scope(
654
+ monitor_metadata=monitor_metadata,
655
+ role=role,
656
+ task_class=task_class,
657
+ )
658
+ bounds = section_bounds(lines, role)
659
+ section = bounds[2] if bounds else TODO_SECTION_HEADINGS[role]
660
+ existing_blocks = (
661
+ todo_blocks(lines, bounds[0], bounds[1], role=role, source_section=section)
662
+ if bounds
663
+ else []
664
+ )
665
+ block = matching_todo_block(
666
+ lines,
667
+ bounds[0],
668
+ bounds[1],
669
+ todo_text,
670
+ role=role,
671
+ source_section=section,
672
+ ) if bounds else None
673
+ added = block is None
674
+ metadata_updated = False
675
+ status_changed = False
676
+
677
+ if block is None:
678
+ todo_id = build_todo_id(
679
+ role=role,
680
+ source_section=section,
681
+ index=len(existing_blocks) + 1,
682
+ text=todo_text,
683
+ )
684
+ metadata_line = format_todo_metadata_line(
685
+ todo_id=todo_id,
686
+ status=normalized_status,
687
+ task_class=task_class,
688
+ action_kind=action_kind,
689
+ task_domain=task_domain,
690
+ capability_binding_ref=capability_binding_ref,
691
+ task_repository=task_repository,
692
+ continuation_policy=continuation_policy,
693
+ required_write_scopes=required_write_scopes,
694
+ required_capabilities=required_capabilities,
695
+ target_capabilities=target_capabilities,
696
+ explore_result_node_refs=explore_result_node_refs,
697
+ decision_scope=decision_scope,
698
+ required_decision_scopes=required_decision_scopes,
699
+ claimed_by=claimed_by,
700
+ bound_agent=bound_agent,
701
+ goal_bound=goal_bound,
702
+ blocks_agent=blocks_agent,
703
+ excluded_agents=excluded_agents,
704
+ global_gate=global_gate,
705
+ unblocks_todo_id=unblocks_todo_id,
706
+ replan_obligation_id=replan_obligation_id,
707
+ resume_when=normalized_resume_when,
708
+ validation_command=validation_command,
709
+ validation_command_argv=(
710
+ json_dumps(validation_argv)
711
+ if validation_argv is not None
712
+ else None
713
+ ),
714
+ validation_label=validation_label,
715
+ validation_timeout_seconds=(
716
+ str(validation_timeout_seconds)
717
+ if validation_timeout_seconds is not None
718
+ else None
719
+ ),
720
+ **normalized_monitor_metadata,
721
+ evidence=evidence,
722
+ updated_at=updated_at,
723
+ )
724
+ marker = todo_marker_for_status(normalized_status)
725
+ todo_line = "\n".join([f"- [{marker}] {todo_text}", metadata_line] if metadata_line else [f"- [{marker}] {todo_text}"])
726
+ if bounds:
727
+ insert_into_existing_section(lines, bounds[0], bounds[1], todo_line)
728
+ else:
729
+ insert_new_section(lines, role, todo_line)
730
+ effective_metadata = parse_todo_metadata_line(metadata_line or "") or {}
731
+ else:
732
+ updates: dict[str, Any] = {
733
+ "todo_id": block.get("todo_id"),
734
+ "status": normalized_status if status else block.get("status") or TODO_STATUS_OPEN,
735
+ }
736
+ if status:
737
+ status_changed = set_todo_marker(lines, block, normalized_status)
738
+ if task_class:
739
+ updates["task_class"] = task_class
740
+ if action_kind:
741
+ updates["action_kind"] = action_kind
742
+ if task_domain:
743
+ updates["task_domain"] = task_domain
744
+ if capability_binding_ref:
745
+ requested_binding_ref = normalize_todo_capability_binding_ref(
746
+ capability_binding_ref
747
+ )
748
+ if not requested_binding_ref:
749
+ raise ValueError(
750
+ "capability_binding_ref must be a public-safe namespaced token"
751
+ )
752
+ existing_binding_ref = normalize_todo_capability_binding_ref(
753
+ block.get("capability_binding_ref")
754
+ )
755
+ if (
756
+ existing_binding_ref
757
+ and existing_binding_ref != requested_binding_ref
758
+ ):
759
+ raise ValueError(
760
+ "capability_binding_ref is immutable once set"
761
+ )
762
+ updates["capability_binding_ref"] = requested_binding_ref
763
+ if task_repository:
764
+ updates["task_repository"] = task_repository
765
+ if continuation_policy:
766
+ updates["continuation_policy"] = continuation_policy
767
+ if required_write_scopes is not None:
768
+ updates["required_write_scopes"] = required_write_scopes
769
+ if required_capabilities is not None:
770
+ updates["required_capabilities"] = required_capabilities
771
+ if target_capabilities is not None:
772
+ updates["target_capabilities"] = target_capabilities
773
+ if explore_result_node_refs is not None:
774
+ updates["explore_result_node_refs"] = explore_result_node_refs
775
+ if decision_scope is not None:
776
+ updates["decision_scope"] = decision_scope
777
+ if required_decision_scopes is not None:
778
+ updates["required_decision_scopes"] = required_decision_scopes
779
+ if claimed_by:
780
+ updates["claimed_by"] = claimed_by
781
+ if bound_agent:
782
+ updates["bound_agent"] = bound_agent
783
+ updates["goal_bound"] = None
784
+ elif goal_bound is not None:
785
+ updates["bound_agent"] = None
786
+ updates["goal_bound"] = goal_bound
787
+ if blocks_agent:
788
+ updates["blocks_agent"] = blocks_agent
789
+ if excluded_agents is not None:
790
+ updates["excluded_agents"] = excluded_agents
791
+ if global_gate is not None:
792
+ updates["global_gate"] = global_gate
793
+ if unblocks_todo_id:
794
+ updates["unblocks_todo_id"] = unblocks_todo_id
795
+ if replan_obligation_id:
796
+ updates["replan_obligation_id"] = require_replan_successor_rebinding(
797
+ existing_obligation_id=block.get("replan_obligation_id"),
798
+ requested_obligation_id=replan_obligation_id,
799
+ )
800
+ if normalized_resume_when:
801
+ updates["resume_when"] = normalized_resume_when
802
+ updates.update(normalized_monitor_metadata)
803
+ if evidence:
804
+ updates["evidence"] = evidence
805
+ if updated_at and not block.get("updated_at"):
806
+ updates["updated_at"] = updated_at
807
+ metadata_line = metadata_line_for_todo_block(block, updates)
808
+ metadata_updated = upsert_todo_metadata(lines, block, metadata_line)
809
+ todo_id = str(block.get("todo_id") or "")
810
+ effective_metadata = parse_todo_metadata_line(metadata_line or "") or {}
811
+
812
+ return {
813
+ "added": added,
814
+ "already_exists": not added,
815
+ "metadata_updated": metadata_updated,
816
+ "status_changed": status_changed,
817
+ "changed": added or metadata_updated or status_changed,
818
+ "role": role,
819
+ "section": section,
820
+ "todo": todo_text,
821
+ "todo_id": todo_id,
822
+ "status": normalize_todo_status(effective_metadata.get("status")) or normalized_status,
823
+ "task_class": effective_metadata.get("task_class") or task_class,
824
+ "action_kind": effective_metadata.get("action_kind") or action_kind,
825
+ "task_domain": normalize_todo_task_domain(
826
+ effective_metadata.get("task_domain") or task_domain
827
+ ),
828
+ "capability_binding_ref": effective_metadata.get("capability_binding_ref")
829
+ or capability_binding_ref,
830
+ "task_repository": normalize_todo_task_repository(
831
+ effective_metadata.get("task_repository") or task_repository
832
+ ),
833
+ "continuation_policy": normalize_todo_continuation_policy(
834
+ effective_metadata.get("continuation_policy") or continuation_policy
835
+ ),
836
+ "required_write_scopes": normalize_required_write_scopes(
837
+ effective_metadata.get("required_write_scopes") or required_write_scopes
838
+ ),
839
+ "required_capabilities": normalize_required_capabilities(
840
+ effective_metadata.get("required_capabilities") or required_capabilities
841
+ ),
842
+ "target_capabilities": normalize_target_capabilities(
843
+ effective_metadata.get("target_capabilities") or target_capabilities
844
+ ),
845
+ "explore_result_node_refs": normalize_explore_result_node_refs(
846
+ effective_metadata.get("explore_result_node_refs") or explore_result_node_refs
847
+ ),
848
+ "decision_scope": normalize_todo_decision_scope(
849
+ effective_metadata.get("decision_scope") or decision_scope
850
+ ),
851
+ "required_decision_scopes": normalize_todo_required_decision_scopes(
852
+ effective_metadata.get("required_decision_scopes") or required_decision_scopes
853
+ ),
854
+ "claimed_by": normalize_todo_claimed_by(effective_metadata.get("claimed_by")),
855
+ "bound_agent": normalize_todo_bound_agent(effective_metadata.get("bound_agent")),
856
+ "goal_bound": normalize_todo_goal_bound(effective_metadata.get("goal_bound")),
857
+ "blocks_agent": normalize_todo_blocks_agent(effective_metadata.get("blocks_agent")),
858
+ "excluded_agents": normalize_todo_excluded_agents(
859
+ effective_metadata.get("excluded_agents")
860
+ ),
861
+ "global_gate": normalize_todo_global_gate(effective_metadata.get("global_gate")),
862
+ "unblocks_todo_id": normalize_todo_id(effective_metadata.get("unblocks_todo_id")),
863
+ "replan_obligation_id": normalize_todo_replan_obligation_id(
864
+ effective_metadata.get("replan_obligation_id")
865
+ ),
866
+ "resume_when": normalize_todo_resume_when(effective_metadata.get("resume_when")),
867
+ "target_key": effective_metadata.get("target_key"),
868
+ "cadence": effective_metadata.get("cadence"),
869
+ "next_due_at": effective_metadata.get("next_due_at"),
870
+ "expires_at": effective_metadata.get("expires_at"),
871
+ "watch_only": effective_metadata.get("watch_only"),
872
+ "evidence": effective_metadata.get("evidence") or evidence,
873
+ "updated_at": effective_metadata.get("updated_at") or updated_at,
874
+ }
875
+
876
+
877
+ def add_goal_todo(
878
+ *,
879
+ registry_path: Path,
880
+ goal_id: str,
881
+ role: str,
882
+ text: str,
883
+ status: str | None = None,
884
+ task_class: str | None = None,
885
+ action_kind: str | None = None,
886
+ task_domain: str | None = None,
887
+ capability_binding_ref: str | None = None,
888
+ task_repository: str | None = None,
889
+ continuation_policy: str | None = None,
890
+ required_write_scopes: list[str] | None = None,
891
+ required_capabilities: list[str] | None = None,
892
+ target_capabilities: list[str] | None = None,
893
+ explore_result_node_refs: list[str] | None = None,
894
+ decision_scope: Any = None,
895
+ required_decision_scopes: Any = None,
896
+ claimed_by: str | None = None,
897
+ bound_agent: str | None = None,
898
+ goal_bound: bool = False,
899
+ blocks_agent: str | None = None,
900
+ excluded_agents: list[str] | None = None,
901
+ global_gate: bool = False,
902
+ agent_id: str | None = None,
903
+ unblocks_todo_id: str | None = None,
904
+ replan_obligation_id: str | None = None,
905
+ resume_when: str | None = None,
906
+ validation_command: str | None = None,
907
+ validation_command_json: str | None = None,
908
+ validation_label: str | None = None,
909
+ validation_timeout_seconds: int | None = None,
910
+ monitor_metadata: dict[str, Any] | None = None,
911
+ project: Path | None = None,
912
+ state_file: Path | None = None,
913
+ dry_run: bool = False,
914
+ ) -> dict[str, Any]:
915
+ if role not in TODO_SECTION_HEADINGS:
916
+ raise ValueError("todo role must be one of: user, agent")
917
+ require_user_todo_task_class(
918
+ role=role,
919
+ task_class=task_class,
920
+ blocks_agent=blocks_agent,
921
+ global_gate=True if global_gate else None,
922
+ )
923
+ if global_gate and not (role == "user" and task_class == TODO_TASK_CLASS_USER_GATE):
924
+ raise ValueError("global_gate is only valid for `--role user --task-class user_gate`")
925
+ if role == "agent" and blocks_agent:
926
+ raise ValueError(
927
+ "blocks_agent is only valid for user gates; use --excluded-agent for "
928
+ "agent executor constraints"
929
+ )
930
+ if role == "user" and claimed_by:
931
+ raise ValueError(
932
+ "claimed_by is execution ownership for agent todos, not a user-todo "
933
+ "binding; use --bound-agent or --goal-bound"
934
+ )
935
+ if task_repository and role != "agent":
936
+ raise ValueError("task_repository is only valid for agent todos")
937
+ if task_domain and role != "agent":
938
+ raise ValueError("task_domain is only valid for agent todos")
939
+ if capability_binding_ref and role != "agent":
940
+ raise ValueError("capability_binding_ref is only valid for agent todos")
941
+ replan_obligation_id = require_replan_successor_scope(
942
+ role=role,
943
+ task_class=task_class,
944
+ claimed_by=claimed_by,
945
+ obligation_id=replan_obligation_id,
946
+ action_kind=action_kind,
947
+ target_key=(monitor_metadata or {}).get("target_key"),
948
+ explore_result_node_refs=explore_result_node_refs,
949
+ )
950
+ normalized_status = normalize_todo_status(status) if status else TODO_STATUS_OPEN
951
+ if status and not normalized_status:
952
+ raise ValueError("todo status must be one of: open, done, blocked, deferred")
953
+ if normalized_status == TODO_STATUS_DONE:
954
+ raise ValueError("todo add cannot create completed work; add it open and use `loopx todo complete`")
955
+ todo_text = normalize_new_todo(text)
956
+ resolved_project, resolved_state_file = resolve_todo_state_path(
957
+ registry_path=registry_path,
958
+ goal_id=goal_id,
959
+ project=project,
960
+ state_file=state_file,
961
+ )
962
+
963
+ with exclusive_file_lock(
964
+ resolved_state_file,
965
+ agent_id=agent_id or claimed_by,
966
+ operation="todo_add",
967
+ ), ExitStack() as handoff_gate_stack:
968
+ original = resolved_state_file.read_text(encoding="utf-8")
969
+ lines = original.splitlines()
970
+ updated_at = now_local()
971
+ effective_claimed_by = (
972
+ require_registered_agent_id(
973
+ registry_path=registry_path,
974
+ goal_id=goal_id,
975
+ agent_id=claimed_by,
976
+ )
977
+ if claimed_by
978
+ else None
979
+ )
980
+ effective_agent_id = (
981
+ require_registered_agent_id(
982
+ registry_path=registry_path,
983
+ goal_id=goal_id,
984
+ agent_id=agent_id,
985
+ field="agent_id",
986
+ )
987
+ if agent_id
988
+ else None
989
+ )
990
+ registered_agents = registered_agent_ids_from_registry(registry_path, goal_id)
991
+ inferred_blocks_agent = blocks_agent
992
+ if (
993
+ effective_agent_id
994
+ and not inferred_blocks_agent
995
+ and role == "user"
996
+ and task_class == TODO_TASK_CLASS_USER_GATE
997
+ ):
998
+ inferred_blocks_agent = effective_agent_id
999
+ effective_blocks_agent = (
1000
+ require_registered_agent_id(
1001
+ registry_path=registry_path,
1002
+ goal_id=goal_id,
1003
+ agent_id=inferred_blocks_agent,
1004
+ field="blocks_agent",
1005
+ )
1006
+ if inferred_blocks_agent
1007
+ else None
1008
+ )
1009
+ inferred_bound_agent = bound_agent
1010
+ if role == "user" and not inferred_bound_agent and not goal_bound:
1011
+ if effective_agent_id:
1012
+ inferred_bound_agent = effective_agent_id
1013
+ elif task_class == TODO_TASK_CLASS_USER_GATE and effective_blocks_agent:
1014
+ inferred_bound_agent = effective_blocks_agent
1015
+ elif len(registered_agents) == 1:
1016
+ inferred_bound_agent = registered_agents[0]
1017
+ effective_bound_agent = (
1018
+ require_registered_agent_id(
1019
+ registry_path=registry_path,
1020
+ goal_id=goal_id,
1021
+ agent_id=inferred_bound_agent,
1022
+ field="bound_agent",
1023
+ )
1024
+ if inferred_bound_agent
1025
+ else None
1026
+ )
1027
+ effective_goal_bound = bool(goal_bound or global_gate)
1028
+ effective_excluded_agents = require_registered_todo_excluded_agents(
1029
+ registry_path=registry_path,
1030
+ goal_id=goal_id,
1031
+ excluded_agents=excluded_agents,
1032
+ )
1033
+ if role != "agent" and effective_excluded_agents:
1034
+ raise ValueError("excluded_agents is only valid for agent todos")
1035
+ require_user_gate_scope(
1036
+ registry_path=registry_path,
1037
+ goal_id=goal_id,
1038
+ role=role,
1039
+ task_class=task_class,
1040
+ blocks_agent=effective_blocks_agent,
1041
+ global_gate=True if global_gate else None,
1042
+ )
1043
+ require_user_todo_binding(
1044
+ registry_path=registry_path,
1045
+ goal_id=goal_id,
1046
+ role=role,
1047
+ task_class=task_class,
1048
+ bound_agent=effective_bound_agent,
1049
+ goal_bound=effective_goal_bound,
1050
+ blocks_agent=effective_blocks_agent,
1051
+ global_gate=True if global_gate else None,
1052
+ )
1053
+ normalized_unblocks_todo_id = normalize_todo_id(unblocks_todo_id) if unblocks_todo_id else None
1054
+ if unblocks_todo_id and not normalized_unblocks_todo_id:
1055
+ raise ValueError("unblocks_todo_id must use the public token shape todo_<letters-digits-underscore-hyphen>")
1056
+ normalized_resume_when = require_supported_todo_resume_when(resume_when)
1057
+ if normalized_status == TODO_STATUS_DEFERRED and not normalized_resume_when:
1058
+ raise ValueError("deferred todo add requires --resume-when with a supported condition")
1059
+ normalized_monitor_metadata = todo_monitor_metadata.require_monitor_metadata_scope(
1060
+ monitor_metadata=monitor_metadata,
1061
+ role=role,
1062
+ task_class=task_class,
1063
+ )
1064
+ todo_monitor_metadata.require_continuous_monitor_boundedness(
1065
+ task_class=task_class,
1066
+ resume_when=normalized_resume_when,
1067
+ monitor_metadata=normalized_monitor_metadata,
1068
+ )
1069
+ handoff_gate = enter_added_todo_ownership_handoff_gate(
1070
+ handoff_gate_stack,
1071
+ lines=lines,
1072
+ state_text=original,
1073
+ registry_path=registry_path,
1074
+ goal_id=goal_id,
1075
+ role=role,
1076
+ text=todo_text,
1077
+ claimed_by=effective_claimed_by,
1078
+ actor_agent_id=effective_agent_id or effective_claimed_by,
1079
+ )
1080
+ add_result = add_todo_to_lines(
1081
+ lines,
1082
+ role=role,
1083
+ text=todo_text,
1084
+ status=normalized_status,
1085
+ task_class=task_class,
1086
+ action_kind=action_kind,
1087
+ task_domain=task_domain,
1088
+ capability_binding_ref=capability_binding_ref,
1089
+ task_repository=task_repository,
1090
+ continuation_policy=continuation_policy,
1091
+ required_write_scopes=required_write_scopes,
1092
+ required_capabilities=required_capabilities,
1093
+ target_capabilities=target_capabilities,
1094
+ explore_result_node_refs=explore_result_node_refs,
1095
+ decision_scope=decision_scope,
1096
+ required_decision_scopes=required_decision_scopes,
1097
+ claimed_by=effective_claimed_by,
1098
+ bound_agent=effective_bound_agent,
1099
+ goal_bound=(
1100
+ True if role == "user" and effective_goal_bound else None
1101
+ ),
1102
+ blocks_agent=effective_blocks_agent,
1103
+ excluded_agents=effective_excluded_agents,
1104
+ global_gate=True if global_gate else None,
1105
+ unblocks_todo_id=normalized_unblocks_todo_id,
1106
+ replan_obligation_id=replan_obligation_id,
1107
+ resume_when=normalized_resume_when,
1108
+ validation_command=validation_command,
1109
+ validation_command_json=validation_command_json,
1110
+ validation_label=validation_label,
1111
+ validation_timeout_seconds=validation_timeout_seconds,
1112
+ monitor_metadata=normalized_monitor_metadata,
1113
+ updated_at=updated_at,
1114
+ )
1115
+ added = bool(add_result["added"])
1116
+ metadata_updated = bool(add_result["metadata_updated"])
1117
+ changed = bool(add_result["changed"])
1118
+
1119
+ new_text = "\n".join(lines) + ("\n" if original.endswith("\n") else "")
1120
+ if changed:
1121
+ new_text = replace_updated_at(new_text, updated_at)
1122
+ if changed and not dry_run:
1123
+ resolved_state_file.write_text(new_text, encoding="utf-8")
1124
+
1125
+ payload = {
1126
+ "ok": True,
1127
+ "dry_run": dry_run,
1128
+ "added": added,
1129
+ "already_exists": bool(add_result["already_exists"]),
1130
+ "metadata_updated": metadata_updated,
1131
+ "status_changed": bool(add_result.get("status_changed")),
1132
+ "goal_id": goal_id,
1133
+ "role": role,
1134
+ "section": add_result.get("section"),
1135
+ "todo": todo_text,
1136
+ "todo_id": add_result.get("todo_id"),
1137
+ "status": add_result.get("status"),
1138
+ "task_class": add_result.get("task_class"),
1139
+ "action_kind": add_result.get("action_kind"),
1140
+ "capability_binding_ref": add_result.get("capability_binding_ref"),
1141
+ "task_repository": add_result.get("task_repository"),
1142
+ "continuation_policy": add_result.get("continuation_policy"),
1143
+ "required_write_scopes": add_result.get("required_write_scopes"),
1144
+ "required_capabilities": add_result.get("required_capabilities"),
1145
+ "target_capabilities": add_result.get("target_capabilities"),
1146
+ "explore_result_node_refs": add_result.get("explore_result_node_refs"),
1147
+ "decision_scope": add_result.get("decision_scope"),
1148
+ "required_decision_scopes": add_result.get("required_decision_scopes"),
1149
+ "claimed_by": add_result.get("claimed_by"),
1150
+ "bound_agent": add_result.get("bound_agent"),
1151
+ "goal_bound": add_result.get("goal_bound"),
1152
+ "agent_id": effective_agent_id,
1153
+ "blocks_agent": add_result.get("blocks_agent"),
1154
+ "excluded_agents": add_result.get("excluded_agents"),
1155
+ "global_gate": add_result.get("global_gate"),
1156
+ "unblocks_todo_id": add_result.get("unblocks_todo_id"),
1157
+ "replan_obligation_id": add_result.get("replan_obligation_id"),
1158
+ "resume_when": add_result.get("resume_when"),
1159
+ "target_key": add_result.get("target_key"),
1160
+ "cadence": add_result.get("cadence"),
1161
+ "next_due_at": add_result.get("next_due_at"),
1162
+ "expires_at": add_result.get("expires_at"),
1163
+ "watch_only": add_result.get("watch_only"),
1164
+ "state_file": str(resolved_state_file),
1165
+ "project": str(resolved_project) if resolved_project else None,
1166
+ "updated_at": updated_at if changed else None,
1167
+ **handoff_gate,
1168
+ }
1169
+ return _attach_todo_write_correctness_dry_run_packet(
1170
+ payload,
1171
+ goal_id=goal_id,
1172
+ write_class="todo_add",
1173
+ state_text=original,
1174
+ )
1175
+
1176
+
1177
+ def resolve_todo_state(
1178
+ *,
1179
+ registry_path: Path,
1180
+ goal_id: str,
1181
+ project: Path | None = None,
1182
+ state_file: Path | None = None,
1183
+ ) -> tuple[Path | None, Path, str, list[str]]:
1184
+ resolved_project, resolved_state_file = resolve_todo_state_path(
1185
+ registry_path=registry_path,
1186
+ goal_id=goal_id,
1187
+ project=project,
1188
+ state_file=state_file,
1189
+ )
1190
+ original = resolved_state_file.read_text(encoding="utf-8")
1191
+ return resolved_project, resolved_state_file, original, original.splitlines()
1192
+
1193
+
1194
+ def update_goal_todo(
1195
+ *,
1196
+ registry_path: Path,
1197
+ goal_id: str,
1198
+ todo_id: str,
1199
+ text: str | None = None,
1200
+ status: str | None = None,
1201
+ role: str | None = None,
1202
+ note: str | None = None,
1203
+ evidence: str | None = None,
1204
+ reason: str | None = None,
1205
+ task_class: str | None = None,
1206
+ action_kind: str | None = None,
1207
+ task_domain: str | None = None,
1208
+ task_repository: str | None = None,
1209
+ continuation_policy: str | None = None,
1210
+ required_write_scopes: list[str] | None = None,
1211
+ required_capabilities: list[str] | None = None,
1212
+ target_capabilities: list[str] | None = None,
1213
+ explore_result_node_refs: list[str] | None = None,
1214
+ decision_scope: Any = None,
1215
+ required_decision_scopes: Any = None,
1216
+ claimed_by: str | None = None,
1217
+ bound_agent: str | None = None,
1218
+ goal_bound: bool = False,
1219
+ blocks_agent: str | None = None,
1220
+ clear_blocks_agent: bool = False,
1221
+ excluded_agents: list[str] | None = None,
1222
+ clear_excluded_agents: bool = False,
1223
+ global_gate: bool = False, clear_global_gate: bool = False,
1224
+ agent_id: str | None = None, authority_reason: str | None = None,
1225
+ unblocks_todo_id: str | None = None,
1226
+ successor_todo_ids: list[str] | None = None,
1227
+ resume_when: str | None = None,
1228
+ clear_resume_when: bool = False,
1229
+ no_followup: bool | None = None,
1230
+ monitor_metadata: dict[str, Any] | None = None,
1231
+ enforce_monitor_boundedness: bool = True,
1232
+ clear_claim: bool = False,
1233
+ claim_only: bool = False,
1234
+ project: Path | None = None,
1235
+ state_file: Path | None = None,
1236
+ dry_run: bool = False,
1237
+ ) -> dict[str, Any]:
1238
+ if excluded_agents and clear_excluded_agents:
1239
+ raise ValueError(
1240
+ "todo update accepts either excluded_agents or clear_excluded_agents, not both"
1241
+ )
1242
+ if blocks_agent and clear_blocks_agent:
1243
+ raise ValueError("todo update accepts either blocks_agent or clear_blocks_agent, not both")
1244
+ if bound_agent and goal_bound:
1245
+ raise ValueError("todo update accepts either bound_agent or goal_bound, not both")
1246
+ if resume_when and clear_resume_when:
1247
+ raise ValueError(
1248
+ "todo update accepts either resume_when or clear_resume_when, not both"
1249
+ )
1250
+ resolved_project, resolved_state_file = resolve_todo_state_path(
1251
+ registry_path=registry_path,
1252
+ goal_id=goal_id,
1253
+ project=project,
1254
+ state_file=state_file,
1255
+ )
1256
+ with exclusive_file_lock(
1257
+ resolved_state_file,
1258
+ agent_id=agent_id or claimed_by,
1259
+ operation="todo_update",
1260
+ ), ExitStack() as handoff_gate_stack:
1261
+ original = resolved_state_file.read_text(encoding="utf-8")
1262
+ lines = original.splitlines()
1263
+ updated_at = now_local()
1264
+ effective_claimed_by = (
1265
+ require_registered_agent_id(
1266
+ registry_path=registry_path,
1267
+ goal_id=goal_id,
1268
+ agent_id=claimed_by,
1269
+ )
1270
+ if claimed_by
1271
+ else None
1272
+ )
1273
+ effective_agent_id = (
1274
+ require_registered_agent_id(
1275
+ registry_path=registry_path,
1276
+ goal_id=goal_id,
1277
+ agent_id=agent_id,
1278
+ field="agent_id",
1279
+ )
1280
+ if agent_id
1281
+ else None
1282
+ )
1283
+ effective_blocks_agent = (
1284
+ require_registered_agent_id(
1285
+ registry_path=registry_path,
1286
+ goal_id=goal_id,
1287
+ agent_id=blocks_agent,
1288
+ field="blocks_agent",
1289
+ )
1290
+ if blocks_agent
1291
+ else None
1292
+ )
1293
+ effective_bound_agent = (
1294
+ require_registered_agent_id(
1295
+ registry_path=registry_path,
1296
+ goal_id=goal_id,
1297
+ agent_id=bound_agent,
1298
+ field="bound_agent",
1299
+ )
1300
+ if bound_agent
1301
+ else None
1302
+ )
1303
+ existing_block_match = find_todo_block(lines, todo_id=todo_id, role=role)
1304
+ if not existing_block_match:
1305
+ normalized_todo_id = normalize_todo_id(todo_id) or todo_id
1306
+ raise ValueError(f"todo_id {normalized_todo_id!r} was not found in active user or agent todos")
1307
+ existing_role, _section, _start, _end, existing_block = existing_block_match
1308
+ target_role = role or existing_role
1309
+ authority_todo = dict(existing_block)
1310
+ authority_todo["role"] = target_role
1311
+ authority_action = todo_update_authority_action(
1312
+ existing_role=existing_role,
1313
+ role=role,
1314
+ claimed_by=claimed_by,
1315
+ clear_claim=clear_claim,
1316
+ other_values=(
1317
+ text, status, note, evidence, reason, task_class, action_kind,
1318
+ task_domain,
1319
+ task_repository, continuation_policy, required_write_scopes,
1320
+ required_capabilities, target_capabilities,
1321
+ explore_result_node_refs, decision_scope,
1322
+ required_decision_scopes, blocks_agent, clear_blocks_agent,
1323
+ bound_agent, goal_bound,
1324
+ excluded_agents, clear_excluded_agents, global_gate,
1325
+ clear_global_gate, unblocks_todo_id, successor_todo_ids,
1326
+ resume_when, clear_resume_when, no_followup,
1327
+ ),
1328
+ monitor_metadata=monitor_metadata,
1329
+ )
1330
+ mutation_authority = authorize_todo_lifecycle_mutation(
1331
+ registry_path=registry_path,
1332
+ goal_id=goal_id,
1333
+ command="claim" if claim_only else "update",
1334
+ todo=authority_todo,
1335
+ actor_agent_id=effective_agent_id,
1336
+ authority_action=None if claim_only else authority_action,
1337
+ authority_reason=authority_reason,
1338
+ requested_claimed_by=effective_claimed_by,
1339
+ )
1340
+ handoff_gate = enter_todo_ownership_handoff_gate(
1341
+ handoff_gate_stack,
1342
+ state_text=original,
1343
+ registry_path=registry_path,
1344
+ goal_id=goal_id,
1345
+ todo_id=str(authority_todo.get("todo_id") or todo_id),
1346
+ mutation_authority=mutation_authority,
1347
+ actor_agent_id=effective_agent_id or effective_claimed_by,
1348
+ ownership_mutation=(claimed_by is not None or clear_claim) and target_role == "agent",
1349
+ )
1350
+ target_task_class = task_class or str(existing_block.get("task_class") or "")
1351
+ if target_role == "user" and claimed_by:
1352
+ raise ValueError(
1353
+ "claimed_by is execution ownership for agent todos, not a user-todo "
1354
+ "binding; use --bound-agent or --goal-bound"
1355
+ )
1356
+ if target_role == "agent" and blocks_agent:
1357
+ raise ValueError(
1358
+ "blocks_agent is only valid for user gates; use excluded_agents for "
1359
+ "agent executor constraints"
1360
+ )
1361
+ if task_repository and target_role != "agent":
1362
+ raise ValueError("task_repository is only valid for agent todos")
1363
+ if task_domain and target_role != "agent":
1364
+ raise ValueError("task_domain is only valid for agent todos")
1365
+ effective_excluded_agents = (
1366
+ []
1367
+ if clear_excluded_agents
1368
+ else require_registered_todo_excluded_agents(
1369
+ registry_path=registry_path,
1370
+ goal_id=goal_id,
1371
+ excluded_agents=excluded_agents,
1372
+ )
1373
+ if excluded_agents is not None
1374
+ else None
1375
+ )
1376
+ existing_excluded_agents = normalize_todo_excluded_agents(
1377
+ existing_block.get("excluded_agents")
1378
+ )
1379
+ target_excluded_agents = (
1380
+ effective_excluded_agents
1381
+ if effective_excluded_agents is not None
1382
+ else existing_excluded_agents
1383
+ )
1384
+ if target_role != "agent" and target_excluded_agents:
1385
+ raise ValueError(
1386
+ "excluded_agents is only valid for agent todos; clear exclusions before "
1387
+ "moving this todo to a user role"
1388
+ )
1389
+ target_status = (
1390
+ normalize_todo_status(status)
1391
+ if status
1392
+ else str(existing_block.get("status") or TODO_STATUS_OPEN)
1393
+ )
1394
+ if status and target_role == "agent" and target_status == TODO_STATUS_DONE:
1395
+ raise ValueError(
1396
+ "agent todo completion must use complete_goal_todo "
1397
+ "(CLI: `loopx todo complete`) so completion policy, successor, "
1398
+ "and no-follow-up contracts are enforced"
1399
+ )
1400
+ existing_blocks_agent = normalize_todo_blocks_agent(existing_block.get("blocks_agent"))
1401
+ existing_global_gate = normalize_todo_global_gate(existing_block.get("global_gate"))
1402
+ existing_bound_agent = normalize_todo_bound_agent(existing_block.get("bound_agent"))
1403
+ existing_goal_bound = normalize_todo_goal_bound(existing_block.get("goal_bound"))
1404
+ target_blocks_agent = None if clear_blocks_agent else effective_blocks_agent or existing_blocks_agent
1405
+ target_global_gate = resolve_user_gate_global_gate_update(
1406
+ role=target_role,
1407
+ task_class=target_task_class,
1408
+ existing_global_gate=existing_global_gate,
1409
+ global_gate=global_gate,
1410
+ clear_global_gate=clear_global_gate,
1411
+ )
1412
+ target_bound_agent = (
1413
+ effective_bound_agent
1414
+ if bound_agent
1415
+ else None
1416
+ if goal_bound
1417
+ else existing_bound_agent
1418
+ )
1419
+ target_goal_bound = True if goal_bound else False if bound_agent else existing_goal_bound
1420
+ registered_agents = registered_agent_ids_from_registry(registry_path, goal_id)
1421
+ if target_role != "user":
1422
+ target_bound_agent = None
1423
+ target_goal_bound = None
1424
+ elif target_task_class == TODO_TASK_CLASS_USER_GATE and target_global_gate:
1425
+ target_bound_agent = None
1426
+ target_goal_bound = True
1427
+ elif target_task_class == TODO_TASK_CLASS_USER_GATE and target_blocks_agent:
1428
+ target_bound_agent = target_blocks_agent
1429
+ target_goal_bound = False
1430
+ elif not target_bound_agent and not target_goal_bound:
1431
+ if len(registered_agents) == 1:
1432
+ target_bound_agent = registered_agents[0]
1433
+ if target_status != TODO_STATUS_DONE:
1434
+ require_user_todo_task_class(
1435
+ role=target_role,
1436
+ task_class=target_task_class,
1437
+ blocks_agent=target_blocks_agent,
1438
+ global_gate=target_global_gate,
1439
+ )
1440
+ require_user_todo_binding(
1441
+ registry_path=registry_path,
1442
+ goal_id=goal_id,
1443
+ role=target_role,
1444
+ task_class=target_task_class,
1445
+ bound_agent=target_bound_agent,
1446
+ goal_bound=target_goal_bound,
1447
+ blocks_agent=target_blocks_agent,
1448
+ global_gate=target_global_gate,
1449
+ )
1450
+ require_user_gate_scope(
1451
+ registry_path=registry_path,
1452
+ goal_id=goal_id,
1453
+ role=target_role,
1454
+ task_class=target_task_class,
1455
+ blocks_agent=target_blocks_agent,
1456
+ global_gate=target_global_gate,
1457
+ )
1458
+ normalized_unblocks_todo_id = normalize_todo_id(unblocks_todo_id) if unblocks_todo_id else None
1459
+ if unblocks_todo_id and not normalized_unblocks_todo_id:
1460
+ raise ValueError("unblocks_todo_id must use the public token shape todo_<letters-digits-underscore-hyphen>")
1461
+ normalized_successor_todo_ids = normalize_todo_id_list(successor_todo_ids)
1462
+ if successor_todo_ids and not normalized_successor_todo_ids:
1463
+ raise ValueError("successor_todo_ids must contain public todo_<letters-digits-underscore-hyphen> tokens")
1464
+ normalized_resume_when = require_supported_todo_resume_when(resume_when)
1465
+ effective_resume_when = (
1466
+ None
1467
+ if clear_resume_when
1468
+ else normalized_resume_when
1469
+ or normalize_supported_todo_resume_when(existing_block.get("resume_when"))
1470
+ )
1471
+ if target_status == TODO_STATUS_DEFERRED and not effective_resume_when:
1472
+ raise ValueError("transition to deferred requires --resume-when with a supported condition")
1473
+ normalized_monitor_metadata = todo_monitor_metadata.require_monitor_metadata_scope(
1474
+ monitor_metadata=monitor_metadata,
1475
+ role=target_role,
1476
+ task_class=target_task_class,
1477
+ )
1478
+ effective_monitor_metadata = {
1479
+ key: existing_block.get(key)
1480
+ for key in (
1481
+ "expires_at",
1482
+ "watch_only",
1483
+ )
1484
+ if existing_block.get(key) is not None
1485
+ }
1486
+ effective_monitor_metadata.update(normalized_monitor_metadata)
1487
+ if enforce_monitor_boundedness:
1488
+ todo_monitor_metadata.require_continuous_monitor_boundedness(
1489
+ task_class=target_task_class,
1490
+ resume_when=effective_resume_when,
1491
+ monitor_metadata=effective_monitor_metadata,
1492
+ )
1493
+ update_result = apply_todo_update_to_lines(
1494
+ lines,
1495
+ todo_id=todo_id,
1496
+ text=text,
1497
+ status=status,
1498
+ role=role,
1499
+ note=note,
1500
+ evidence=evidence,
1501
+ reason=reason,
1502
+ task_class=task_class,
1503
+ action_kind=action_kind,
1504
+ task_domain=task_domain,
1505
+ task_repository=task_repository,
1506
+ continuation_policy=continuation_policy,
1507
+ required_write_scopes=required_write_scopes,
1508
+ required_capabilities=required_capabilities,
1509
+ target_capabilities=target_capabilities,
1510
+ explore_result_node_refs=explore_result_node_refs,
1511
+ decision_scope=decision_scope,
1512
+ required_decision_scopes=required_decision_scopes,
1513
+ claimed_by=effective_claimed_by,
1514
+ bound_agent=target_bound_agent if target_role == "user" else None,
1515
+ goal_bound=(
1516
+ True
1517
+ if target_role == "user" and target_goal_bound
1518
+ else None
1519
+ ),
1520
+ clear_user_binding=(
1521
+ target_role != "user"
1522
+ and bool(existing_bound_agent or existing_goal_bound is not None)
1523
+ ),
1524
+ blocks_agent=effective_blocks_agent,
1525
+ clear_blocks_agent=clear_blocks_agent,
1526
+ excluded_agents=effective_excluded_agents,
1527
+ global_gate=True if global_gate else None,
1528
+ clear_global_gate=clear_global_gate,
1529
+ unblocks_todo_id=normalized_unblocks_todo_id,
1530
+ successor_todo_ids=normalized_successor_todo_ids if successor_todo_ids is not None else None,
1531
+ resume_when=normalized_resume_when,
1532
+ clear_resume_when=clear_resume_when,
1533
+ no_followup=no_followup,
1534
+ monitor_metadata=normalized_monitor_metadata,
1535
+ clear_claim=clear_claim,
1536
+ claim_only=claim_only,
1537
+ updated_at=updated_at,
1538
+ )
1539
+ changed = bool(update_result["changed"])
1540
+ new_text = "\n".join(lines) + ("\n" if original.endswith("\n") else "")
1541
+ if changed:
1542
+ new_text = replace_updated_at(new_text, updated_at)
1543
+ if changed and not dry_run:
1544
+ resolved_state_file.write_text(new_text, encoding="utf-8")
1545
+ write_class = "todo_claim" if claim_only else "todo_update"
1546
+ payload = {
1547
+ "ok": True,
1548
+ "dry_run": dry_run,
1549
+ "changed": changed,
1550
+ "goal_id": goal_id,
1551
+ "agent_id": effective_agent_id,
1552
+ "mutation_authority": mutation_authority,
1553
+ **handoff_gate,
1554
+ **update_result,
1555
+ "state_file": str(resolved_state_file),
1556
+ "project": str(resolved_project) if resolved_project else None,
1557
+ "updated_at": updated_at if changed else None,
1558
+ }
1559
+ if successor_todo_ids is not None:
1560
+ parent_successor_advisory = build_open_parent_successor_advisory(
1561
+ todo_id=update_result.get("todo_id"),
1562
+ status=update_result.get("status"),
1563
+ successor_todo_ids=update_result.get("successor_todo_ids"),
1564
+ )
1565
+ if parent_successor_advisory:
1566
+ payload["parent_successor_advisory"] = parent_successor_advisory
1567
+ return _attach_todo_write_correctness_dry_run_packet(
1568
+ payload,
1569
+ goal_id=goal_id,
1570
+ write_class=write_class,
1571
+ state_text=original,
1572
+ )
1573
+
1574
+
1575
+
1576
+
1577
+ def complete_goal_todo(
1578
+ *,
1579
+ registry_path: Path,
1580
+ goal_id: str,
1581
+ todo_id: str,
1582
+ role: str | None = None,
1583
+ decision_outcome: str | None = None,
1584
+ evidence: str | None = None,
1585
+ completion_turn_key: str | None = None,
1586
+ task_lease_idempotency_key: str | None = None,
1587
+ task_lease_expected_version: int | None = None,
1588
+ note: str | None = None,
1589
+ no_followup: bool = False,
1590
+ successor_todo_ids: list[str] | None = None,
1591
+ claimed_by: str | None = None,
1592
+ clear_claim: bool = False,
1593
+ next_agent_todo: str | None = None,
1594
+ next_user_todo: str | None = None,
1595
+ next_user_task_class: str | None = None,
1596
+ next_claimed_by: str | None = None,
1597
+ next_task_class: str | None = None,
1598
+ next_action_kind: str | None = None,
1599
+ next_task_repository: str | None = None,
1600
+ next_required_capabilities: list[str] | None = None,
1601
+ next_continuation_policy: str | None = None,
1602
+ next_excluded_agents: list[str] | None = None,
1603
+ self_merged: bool = False,
1604
+ agent_id: str | None = None, authority_reason: str | None = None,
1605
+ project: Path | None = None,
1606
+ state_file: Path | None = None,
1607
+ dry_run: bool = False,
1608
+ ) -> dict[str, Any]:
1609
+ if next_task_repository and not next_agent_todo:
1610
+ raise ValueError("--next-task-repository requires --next-agent-todo")
1611
+ if next_required_capabilities and not next_agent_todo:
1612
+ raise ValueError("--next-required-capability requires --next-agent-todo")
1613
+ effective_next_user_task_class = resolve_next_user_task_class(
1614
+ next_user_todo,
1615
+ next_user_task_class,
1616
+ )
1617
+ resolved_project, resolved_state_file = resolve_todo_state_path(
1618
+ registry_path=registry_path,
1619
+ goal_id=goal_id,
1620
+ project=project,
1621
+ state_file=state_file,
1622
+ )
1623
+ # Run caller-approved validation BEFORE acquiring the mutation lock so a
1624
+ # slow validation command does not block concurrent todo operations on the
1625
+ # same goal (the MUTATION lock deadline is 5s). Returns a typed failure
1626
+ # payload (ok=False) when validation blocks; otherwise None.
1627
+ validation_failure = run_completion_validation_gate(
1628
+ state_file=resolved_state_file,
1629
+ todo_id=todo_id,
1630
+ role=role,
1631
+ registry_path=registry_path,
1632
+ goal_id=goal_id,
1633
+ dry_run=dry_run,
1634
+ )
1635
+ if validation_failure is not None:
1636
+ return validation_failure
1637
+ with exclusive_file_lock(
1638
+ resolved_state_file,
1639
+ agent_id=agent_id or claimed_by,
1640
+ operation="todo_complete",
1641
+ ), ExitStack() as lease_fence_stack:
1642
+ original = resolved_state_file.read_text(encoding="utf-8")
1643
+ lines = original.splitlines()
1644
+ updated_at = now_local()
1645
+ completion_match = find_todo_block(lines, todo_id=todo_id, role=role)
1646
+ completion_todo = None
1647
+ event_context = None
1648
+ if completion_match:
1649
+ completion_role, _section, _start, _end, completion_block = completion_match
1650
+ completion_todo = dict(completion_block)
1651
+ completion_todo["role"] = completion_role
1652
+ else:
1653
+ event_context = event_projection_todo_context(
1654
+ registry_path=registry_path,
1655
+ goal_id=goal_id,
1656
+ state_path=resolved_state_file,
1657
+ todo_id=todo_id,
1658
+ role=role,
1659
+ )
1660
+ if event_context:
1661
+ event_context["state_file"] = resolved_state_file
1662
+ event_context["project"] = resolved_project
1663
+ completion_todo = dict(event_context["item"])
1664
+ completion_todo["role"] = event_context["role"]
1665
+ effective_decision_outcome = require_completion_decision_outcome(
1666
+ completion_todo,
1667
+ decision_outcome,
1668
+ materialized=bool(completion_match),
1669
+ )
1670
+ if completion_todo is None:
1671
+ normalized_todo_id = normalize_todo_id(todo_id) or todo_id
1672
+ raise ValueError(
1673
+ f"todo_id {normalized_todo_id!r} was not found in active user or agent todos"
1674
+ )
1675
+ decision_target = None
1676
+ target_todo_id = normalize_todo_id(completion_todo.get("unblocks_todo_id"))
1677
+ if target_todo_id:
1678
+ target_match = find_todo_block(
1679
+ lines,
1680
+ todo_id=target_todo_id,
1681
+ role="agent",
1682
+ )
1683
+ if target_match:
1684
+ target_role, _target_section, _target_start, _target_end, target_block = (
1685
+ target_match
1686
+ )
1687
+ decision_target = dict(target_block)
1688
+ decision_target["role"] = target_role
1689
+ mutation_authority = authorize_todo_lifecycle_mutation(
1690
+ registry_path=registry_path,
1691
+ goal_id=goal_id,
1692
+ command="complete",
1693
+ todo=completion_todo,
1694
+ actor_agent_id=agent_id, authority_reason=authority_reason,
1695
+ requested_claimed_by=claimed_by,
1696
+ decision_outcome=effective_decision_outcome,
1697
+ decision_target=decision_target,
1698
+ )
1699
+ completion_handoff = resolve_todo_completion_handoff(state_text=original, mutation_authority=mutation_authority)
1700
+ terminal_replay = completed_todo_replay(
1701
+ todo=completion_todo,
1702
+ goal_id=goal_id,
1703
+ todo_id=todo_id,
1704
+ completion_turn_key=completion_turn_key,
1705
+ handoff_mode=completion_handoff["handoff_mode"],
1706
+ mutation_authority=mutation_authority,
1707
+ state_file=str(resolved_state_file),
1708
+ project=str(resolved_project) if resolved_project else None,
1709
+ dry_run=dry_run,
1710
+ )
1711
+ if terminal_replay:
1712
+ return terminal_replay
1713
+ task_lease_fence = lease_fence_stack.enter_context(
1714
+ hold_task_lease_mutation_fence(
1715
+ registry_path=registry_path,
1716
+ goal_id=goal_id,
1717
+ todo_id=todo_id,
1718
+ todo=completion_todo,
1719
+ actor_agent_id=agent_id or claimed_by,
1720
+ idempotency_key=(
1721
+ task_lease_idempotency_key or completion_turn_key
1722
+ ),
1723
+ expected_version=task_lease_expected_version,
1724
+ require_active_when_key_supplied=(
1725
+ task_lease_idempotency_key is not None
1726
+ or task_lease_expected_version is not None
1727
+ ),
1728
+ handoff=completion_handoff,
1729
+ )
1730
+ )
1731
+ normalized_successor_todo_ids = normalize_todo_id_list(successor_todo_ids)
1732
+ if successor_todo_ids and not normalized_successor_todo_ids:
1733
+ raise ValueError("successor_todo_ids must contain public todo_<letters-digits-underscore-hyphen> tokens")
1734
+ linked_successors = []
1735
+ for successor_todo_id in normalized_successor_todo_ids:
1736
+ successor_match = find_todo_block(lines, todo_id=successor_todo_id)
1737
+ if successor_match:
1738
+ successor_role, _section, _start, _end, successor_block = successor_match
1739
+ successor_item = dict(successor_block)
1740
+ successor_item["role"] = successor_role
1741
+ linked_successors.append(linked_successor_from_todo(successor_item))
1742
+ continue
1743
+ if event_context:
1744
+ for successor_role in ("user", "agent"):
1745
+ summary = event_context["fields"].get(f"{successor_role}_todos")
1746
+ items = summary.get("items") if isinstance(summary, dict) else []
1747
+ successor_item = next(
1748
+ (
1749
+ dict(item)
1750
+ for item in items or []
1751
+ if isinstance(item, dict)
1752
+ and normalize_todo_id(item.get("todo_id"))
1753
+ == successor_todo_id
1754
+ ),
1755
+ None,
1756
+ )
1757
+ if successor_item:
1758
+ successor_item["role"] = successor_role
1759
+ linked_successors.append(
1760
+ linked_successor_from_todo(successor_item)
1761
+ )
1762
+ break
1763
+ completion_policy = resolve_completion_policy(
1764
+ registry_path=registry_path,
1765
+ goal_id=goal_id,
1766
+ claimed_by=claimed_by,
1767
+ next_claimed_by=next_claimed_by,
1768
+ next_agent_todo=next_agent_todo,
1769
+ next_action_kind=next_action_kind,
1770
+ next_continuation_policy=next_continuation_policy,
1771
+ next_excluded_agents=next_excluded_agents or [],
1772
+ self_merged=self_merged,
1773
+ evidence=evidence,
1774
+ no_followup=no_followup,
1775
+ linked_successors=linked_successors,
1776
+ completion_todo=completion_todo,
1777
+ )
1778
+ effective_claimed_by = completion_policy.effective_claimed_by
1779
+ registered_agents = completion_policy.registered_agents
1780
+ effective_next_claimed_by = completion_policy.effective_next_claimed_by
1781
+ effective_next_excluded_agents = (
1782
+ completion_policy.effective_next_excluded_agents
1783
+ )
1784
+ effective_self_merged = completion_policy.self_merged
1785
+ if not completion_match:
1786
+ if event_context:
1787
+ event_result = complete_event_projected_goal_todo(
1788
+ goal_id=goal_id,
1789
+ context=event_context,
1790
+ evidence=evidence,
1791
+ completion_turn_key=completion_turn_key,
1792
+ note=note,
1793
+ no_followup=no_followup,
1794
+ successor_todo_ids=normalized_successor_todo_ids,
1795
+ claimed_by=effective_claimed_by,
1796
+ clear_claim=clear_claim,
1797
+ next_agent_todo=next_agent_todo,
1798
+ next_user_todo=next_user_todo,
1799
+ next_user_task_class=effective_next_user_task_class,
1800
+ next_claimed_by=effective_next_claimed_by,
1801
+ next_task_class=next_task_class,
1802
+ next_action_kind=next_action_kind,
1803
+ next_task_repository=next_task_repository,
1804
+ next_required_capabilities=next_required_capabilities,
1805
+ next_continuation_policy=next_continuation_policy,
1806
+ self_merged=effective_self_merged,
1807
+ next_excluded_agents=effective_next_excluded_agents,
1808
+ registered_agents=registered_agents,
1809
+ updated_at=updated_at,
1810
+ dry_run=dry_run,
1811
+ actor_agent_id=mutation_authority.get("actor_agent_id"),
1812
+ )
1813
+ event_result["linked_successor_id"] = completion_policy.linked_successor_id
1814
+ event_result["mutation_authority"] = mutation_authority
1815
+ event_result["task_lease_fence"] = task_lease_fence
1816
+ event_result.update(completion_handoff)
1817
+ release_verified_task_lease_fence(
1818
+ task_lease_fence,
1819
+ committed=bool(event_result.get("changed")) and not dry_run,
1820
+ )
1821
+ return event_result
1822
+ update_result = apply_todo_update_to_lines(
1823
+ lines,
1824
+ todo_id=todo_id,
1825
+ status=TODO_STATUS_DONE,
1826
+ role=role,
1827
+ decision_outcome=effective_decision_outcome,
1828
+ note=note,
1829
+ evidence=evidence,
1830
+ completion_turn_key=completion_turn_key,
1831
+ claimed_by=effective_claimed_by,
1832
+ clear_claim=clear_claim,
1833
+ no_followup=True if no_followup else None,
1834
+ successor_todo_ids=normalized_successor_todo_ids if successor_todo_ids is not None else None,
1835
+ updated_at=updated_at,
1836
+ )
1837
+ unblock_resume, decision_scope_resolution = (
1838
+ apply_completed_user_todo_lifecycle(
1839
+ lines,
1840
+ completion_todo=completion_todo,
1841
+ update_result=update_result,
1842
+ fallback_todo_id=todo_id,
1843
+ decision_outcome=effective_decision_outcome,
1844
+ updated_at=updated_at,
1845
+ apply_update=apply_todo_update_to_lines,
1846
+ )
1847
+ )
1848
+ next_unblocks_todo_id = (
1849
+ normalize_todo_id(str(update_result.get("todo_id") or todo_id))
1850
+ if next_agent_todo
1851
+ else None
1852
+ )
1853
+ next_user_bound_agent = None
1854
+ if next_user_todo and len(registered_agents) > 1:
1855
+ next_user_bound_agent = effective_claimed_by
1856
+ if not next_user_bound_agent:
1857
+ raise ValueError(
1858
+ "multi-agent --next-user-todo requires a completing --claimed-by "
1859
+ "agent so the user todo can be bound"
1860
+ )
1861
+ next_results: list[dict[str, Any]] = []
1862
+ if next_agent_todo:
1863
+ next_results.append(
1864
+ add_todo_to_lines(
1865
+ lines,
1866
+ role="agent",
1867
+ text=inherit_todo_priority(
1868
+ next_agent_todo,
1869
+ str(update_result.get("todo") or ""),
1870
+ ),
1871
+ task_class=next_task_class or "advancement_task",
1872
+ action_kind=next_action_kind,
1873
+ capability_binding_ref=completion_todo.get(
1874
+ "capability_binding_ref"
1875
+ ),
1876
+ task_repository=next_task_repository,
1877
+ required_capabilities=next_required_capabilities,
1878
+ continuation_policy=next_continuation_policy,
1879
+ claimed_by=effective_next_claimed_by,
1880
+ excluded_agents=effective_next_excluded_agents,
1881
+ unblocks_todo_id=next_unblocks_todo_id,
1882
+ updated_at=updated_at,
1883
+ )
1884
+ )
1885
+ if next_user_todo:
1886
+ next_results.append(
1887
+ add_todo_to_lines(
1888
+ lines,
1889
+ role="user",
1890
+ text=inherit_todo_priority(
1891
+ next_user_todo,
1892
+ str(update_result.get("todo") or ""),
1893
+ ),
1894
+ task_class=effective_next_user_task_class,
1895
+ action_kind=(
1896
+ "gate"
1897
+ if effective_next_user_task_class == TODO_TASK_CLASS_USER_GATE
1898
+ else None
1899
+ ),
1900
+ bound_agent=next_user_bound_agent,
1901
+ blocks_agent=(
1902
+ next_user_bound_agent
1903
+ if effective_next_user_task_class == TODO_TASK_CLASS_USER_GATE
1904
+ else None
1905
+ ),
1906
+ updated_at=updated_at,
1907
+ )
1908
+ )
1909
+ generated_successor_todo_ids = [
1910
+ todo_id
1911
+ for todo_id in normalize_todo_id_list([item.get("todo_id") for item in next_results])
1912
+ ]
1913
+ successor_metadata_updated = link_generated_successor_todo_ids(
1914
+ lines,
1915
+ update_result=update_result,
1916
+ role=role,
1917
+ successor_todo_ids=generated_successor_todo_ids,
1918
+ )
1919
+ next_changed = any(item.get("added") or item.get("metadata_updated") for item in next_results)
1920
+ changed = bool(
1921
+ update_result["changed"]
1922
+ or next_changed
1923
+ or successor_metadata_updated
1924
+ or (unblock_resume or {}).get("changed")
1925
+ or (decision_scope_resolution or {}).get("changed")
1926
+ )
1927
+ new_text = "\n".join(lines) + ("\n" if original.endswith("\n") else "")
1928
+ if changed:
1929
+ new_text = replace_updated_at(new_text, updated_at)
1930
+ if changed and not dry_run:
1931
+ resolved_state_file.write_text(new_text, encoding="utf-8")
1932
+ release_verified_task_lease_fence(
1933
+ task_lease_fence,
1934
+ committed=changed and not dry_run,
1935
+ )
1936
+ result = {
1937
+ "ok": True,
1938
+ "dry_run": dry_run,
1939
+ "completed": True,
1940
+ "goal_id": goal_id,
1941
+ **update_result,
1942
+ "changed": changed,
1943
+ "next_todos": next_results,
1944
+ "linked_successor_id": completion_policy.linked_successor_id,
1945
+ "mutation_authority": mutation_authority,
1946
+ "task_lease_fence": task_lease_fence,
1947
+ **completion_handoff,
1948
+ "state_file": str(resolved_state_file),
1949
+ "project": str(resolved_project) if resolved_project else None,
1950
+ "updated_at": updated_at if changed else None,
1951
+ }
1952
+ if unblock_resume:
1953
+ result["unblock_resume"] = unblock_resume
1954
+ if decision_scope_resolution:
1955
+ result["decision_scope_resolution"] = decision_scope_resolution
1956
+ if effective_decision_outcome:
1957
+ result["decision_outcome"] = effective_decision_outcome
1958
+ result["self_merged"] = effective_self_merged
1959
+ return result
1960
+
1961
+ def supersede_goal_todo(
1962
+ *,
1963
+ registry_path: Path,
1964
+ goal_id: str,
1965
+ todo_id: str,
1966
+ role: str | None = None,
1967
+ reason: str | None = None,
1968
+ next_agent_todo: str | None = None,
1969
+ next_user_todo: str | None = None,
1970
+ next_user_task_class: str | None = None,
1971
+ next_claimed_by: str | None = None,
1972
+ next_task_class: str | None = None,
1973
+ next_action_kind: str | None = None,
1974
+ next_task_repository: str | None = None,
1975
+ next_required_capabilities: list[str] | None = None,
1976
+ next_continuation_policy: str | None = None,
1977
+ next_excluded_agents: list[str] | None = None,
1978
+ agent_id: str | None = None, authority_reason: str | None = None,
1979
+ project: Path | None = None,
1980
+ state_file: Path | None = None,
1981
+ dry_run: bool = False,
1982
+ ) -> dict[str, Any]:
1983
+ if next_task_repository and not next_agent_todo:
1984
+ raise ValueError("--next-task-repository requires --next-agent-todo")
1985
+ if next_required_capabilities and not next_agent_todo:
1986
+ raise ValueError("--next-required-capability requires --next-agent-todo")
1987
+ effective_next_user_task_class = resolve_next_user_task_class(
1988
+ next_user_todo,
1989
+ next_user_task_class,
1990
+ )
1991
+ resolved_project, resolved_state_file = resolve_todo_state_path(
1992
+ registry_path=registry_path,
1993
+ goal_id=goal_id,
1994
+ project=project,
1995
+ state_file=state_file,
1996
+ )
1997
+ with exclusive_file_lock(
1998
+ resolved_state_file,
1999
+ agent_id=agent_id,
2000
+ operation="todo_supersede",
2001
+ ):
2002
+ original = resolved_state_file.read_text(encoding="utf-8")
2003
+ lines = original.splitlines()
2004
+ updated_at = now_local()
2005
+ current_match = find_todo_block(lines, todo_id=todo_id, role=role)
2006
+ if not current_match:
2007
+ normalized_todo_id = normalize_todo_id(todo_id) or todo_id
2008
+ raise ValueError(
2009
+ f"todo_id {normalized_todo_id!r} was not found in active user or agent todos"
2010
+ )
2011
+ current_role, _section, _start, _end, current_block = current_match
2012
+ authority_todo = dict(current_block)
2013
+ authority_todo["role"] = current_role
2014
+ mutation_authority = authorize_todo_lifecycle_mutation(
2015
+ registry_path=registry_path,
2016
+ goal_id=goal_id,
2017
+ command="supersede",
2018
+ todo=authority_todo,
2019
+ actor_agent_id=agent_id, authority_reason=authority_reason,
2020
+ )
2021
+ effective_next_claimed_by = (
2022
+ require_registered_agent_id(
2023
+ registry_path=registry_path,
2024
+ goal_id=goal_id,
2025
+ agent_id=next_claimed_by,
2026
+ field="next_claimed_by",
2027
+ )
2028
+ if next_claimed_by
2029
+ else None
2030
+ )
2031
+ effective_next_excluded_agents = require_registered_todo_excluded_agents(
2032
+ registry_path=registry_path,
2033
+ goal_id=goal_id,
2034
+ excluded_agents=next_excluded_agents,
2035
+ field="next_excluded_agents",
2036
+ )
2037
+ if effective_next_claimed_by and not next_agent_todo:
2038
+ raise ValueError("--next-claimed-by requires --next-agent-todo")
2039
+ if effective_next_excluded_agents and not next_agent_todo:
2040
+ raise ValueError("--next-excluded-agent requires --next-agent-todo")
2041
+ update_result = apply_todo_update_to_lines(
2042
+ lines,
2043
+ todo_id=todo_id,
2044
+ status=TODO_STATUS_DONE,
2045
+ role=role,
2046
+ reason=reason,
2047
+ note="superseded",
2048
+ updated_at=updated_at,
2049
+ )
2050
+ current_claimed_by = normalize_todo_claimed_by(update_result.get("claimed_by"))
2051
+ next_policy = resolve_todo_continuation_policy(
2052
+ next_continuation_policy,
2053
+ action_kind=next_action_kind,
2054
+ )
2055
+ if (
2056
+ next_agent_todo
2057
+ and not effective_next_claimed_by
2058
+ and next_policy == TodoContinuationPolicy.SAME_AGENT_NON_DELIVERY
2059
+ ):
2060
+ effective_next_claimed_by = current_claimed_by
2061
+ if effective_next_claimed_by in effective_next_excluded_agents:
2062
+ raise ValueError(
2063
+ f"next_claimed_by={effective_next_claimed_by!r} cannot also appear in "
2064
+ "next_excluded_agents"
2065
+ )
2066
+ next_unblocks_todo_id = normalize_todo_id(update_result.get("unblocks_todo_id"))
2067
+ registered_agents = registered_agent_ids_from_registry(registry_path, goal_id)
2068
+ next_user_bound_agent = (
2069
+ normalize_todo_bound_agent(update_result.get("bound_agent"))
2070
+ or normalize_todo_blocks_agent(update_result.get("blocks_agent"))
2071
+ )
2072
+ if next_user_todo and len(registered_agents) > 1 and not next_user_bound_agent:
2073
+ next_user_bound_agent = (
2074
+ normalize_todo_claimed_by(update_result.get("claimed_by"))
2075
+ or effective_next_claimed_by
2076
+ )
2077
+ if not next_user_bound_agent:
2078
+ raise ValueError(
2079
+ "multi-agent supersede --next-user-todo requires inherited "
2080
+ "blocks_agent, current claimed_by, or next_claimed_by "
2081
+ "so the user todo can be bound"
2082
+ )
2083
+ next_results: list[dict[str, Any]] = []
2084
+ if next_agent_todo:
2085
+ next_results.append(
2086
+ add_todo_to_lines(
2087
+ lines,
2088
+ role="agent",
2089
+ text=inherit_todo_priority(
2090
+ next_agent_todo,
2091
+ str(update_result.get("todo") or ""),
2092
+ ),
2093
+ task_class=next_task_class or "advancement_task",
2094
+ action_kind=next_action_kind,
2095
+ capability_binding_ref=current_block.get(
2096
+ "capability_binding_ref"
2097
+ ),
2098
+ task_repository=next_task_repository,
2099
+ required_capabilities=next_required_capabilities,
2100
+ continuation_policy=next_continuation_policy,
2101
+ claimed_by=effective_next_claimed_by,
2102
+ excluded_agents=effective_next_excluded_agents,
2103
+ unblocks_todo_id=next_unblocks_todo_id,
2104
+ updated_at=updated_at,
2105
+ )
2106
+ )
2107
+ if next_user_todo:
2108
+ next_results.append(
2109
+ add_todo_to_lines(
2110
+ lines,
2111
+ role="user",
2112
+ text=inherit_todo_priority(
2113
+ next_user_todo,
2114
+ str(update_result.get("todo") or ""),
2115
+ ),
2116
+ task_class=effective_next_user_task_class,
2117
+ action_kind=(
2118
+ "gate"
2119
+ if effective_next_user_task_class == TODO_TASK_CLASS_USER_GATE
2120
+ else None
2121
+ ),
2122
+ bound_agent=next_user_bound_agent,
2123
+ blocks_agent=(
2124
+ next_user_bound_agent
2125
+ if effective_next_user_task_class == TODO_TASK_CLASS_USER_GATE
2126
+ else None
2127
+ ),
2128
+ updated_at=updated_at,
2129
+ )
2130
+ )
2131
+ superseded_by = next((item.get("todo_id") for item in next_results if item.get("todo_id")), None)
2132
+ generated_successor_todo_ids = [
2133
+ todo_id
2134
+ for todo_id in normalize_todo_id_list([item.get("todo_id") for item in next_results])
2135
+ ]
2136
+ if superseded_by:
2137
+ block_match = find_todo_block(lines, todo_id=str(update_result["todo_id"]), role=role)
2138
+ if block_match:
2139
+ _resolved_role, _section, _start, _end, block = block_match
2140
+ update_result["metadata_updated"] = upsert_todo_metadata(
2141
+ lines,
2142
+ block,
2143
+ metadata_line_for_todo_block(
2144
+ block,
2145
+ {
2146
+ "superseded_by": superseded_by,
2147
+ "successor_todo_ids": merge_todo_id_lists(
2148
+ update_result.get("successor_todo_ids"),
2149
+ generated_successor_todo_ids,
2150
+ ),
2151
+ },
2152
+ ),
2153
+ ) or update_result["metadata_updated"]
2154
+ update_result["superseded_by"] = superseded_by
2155
+ update_result["successor_todo_ids"] = merge_todo_id_lists(
2156
+ update_result.get("successor_todo_ids"),
2157
+ generated_successor_todo_ids,
2158
+ )
2159
+ update_result["changed"] = True
2160
+ next_changed = any(item.get("added") or item.get("metadata_updated") for item in next_results)
2161
+ changed = bool(update_result["changed"] or next_changed)
2162
+ new_text = "\n".join(lines) + ("\n" if original.endswith("\n") else "")
2163
+ if changed:
2164
+ new_text = replace_updated_at(new_text, updated_at)
2165
+ if changed and not dry_run:
2166
+ resolved_state_file.write_text(new_text, encoding="utf-8")
2167
+ return {
2168
+ "ok": True,
2169
+ "dry_run": dry_run,
2170
+ "superseded": True,
2171
+ "goal_id": goal_id,
2172
+ **update_result,
2173
+ "changed": changed,
2174
+ "mutation_authority": mutation_authority,
2175
+ "next_todos": next_results,
2176
+ "state_file": str(resolved_state_file),
2177
+ "project": str(resolved_project) if resolved_project else None,
2178
+ "updated_at": updated_at if changed else None,
2179
+ }
2180
+
2181
+
2182
+ def archive_completed_todos(
2183
+ *,
2184
+ registry_path: Path,
2185
+ goal_id: str,
2186
+ role: str = "agent",
2187
+ max_active_done: int = ARCHIVE_COMPLETED_DEFAULT_MAX_ACTIVE_DONE,
2188
+ project: Path | None = None,
2189
+ state_file: Path | None = None,
2190
+ dry_run: bool = True,
2191
+ ) -> dict[str, Any]:
2192
+ if role not in TODO_SECTION_HEADINGS:
2193
+ raise ValueError("todo role must be one of: user, agent")
2194
+ if max_active_done < 0:
2195
+ raise ValueError("max_active_done must be non-negative")
2196
+ resolved_project, resolved_state_file = resolve_todo_state_path(
2197
+ registry_path=registry_path,
2198
+ goal_id=goal_id,
2199
+ project=project,
2200
+ state_file=state_file,
2201
+ )
2202
+
2203
+ with exclusive_file_lock(resolved_state_file, operation="todo_archive_completed"):
2204
+ original = resolved_state_file.read_text(encoding="utf-8")
2205
+ lines = original.splitlines()
2206
+ archive_result = archive_completed_todo_lines(
2207
+ lines,
2208
+ role=role,
2209
+ max_active_done=max_active_done,
2210
+ )
2211
+ lines = archive_result.pop("lines")
2212
+
2213
+ updated_at = now_local()
2214
+ changed = bool(archive_result["changed"])
2215
+ new_text = "\n".join(lines) + ("\n" if original.endswith("\n") else "")
2216
+ if changed:
2217
+ new_text = replace_updated_at(new_text, updated_at)
2218
+ if changed and not dry_run:
2219
+ resolved_state_file.write_text(new_text, encoding="utf-8")
2220
+
2221
+ return {
2222
+ "ok": True,
2223
+ "dry_run": dry_run,
2224
+ "goal_id": goal_id,
2225
+ **archive_result,
2226
+ "state_file": str(resolved_state_file),
2227
+ "project": str(resolved_project) if resolved_project else None,
2228
+ "updated_at": updated_at if changed else None,
2229
+ }