loopx 0.4.8__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (811) hide show
  1. loopx/__init__.py +5 -0
  2. loopx/agent_onboarding.py +654 -0
  3. loopx/agent_registry.py +112 -0
  4. loopx/ark_managed_agent_host.py +59 -0
  5. loopx/authority.py +805 -0
  6. loopx/benchmark.py +2875 -0
  7. loopx/benchmark_adapters/__init__.py +1 -0
  8. loopx/benchmark_adapters/agentissue.py +2644 -0
  9. loopx/benchmark_adapters/agents_last_exam.py +3998 -0
  10. loopx/benchmark_adapters/edgebench.py +322 -0
  11. loopx/benchmark_adapters/skillsbench.py +5978 -0
  12. loopx/benchmark_adapters/skillsbench_acp_failure_policy.py +143 -0
  13. loopx/benchmark_adapters/skillsbench_acp_process.py +31 -0
  14. loopx/benchmark_adapters/skillsbench_acp_relay.py +4832 -0
  15. loopx/benchmark_adapters/skillsbench_batch.py +124 -0
  16. loopx/benchmark_adapters/skillsbench_bridge_guard.py +209 -0
  17. loopx/benchmark_adapters/skillsbench_bridge_summary.py +203 -0
  18. loopx/benchmark_adapters/skillsbench_codex_goal_recovery.py +271 -0
  19. loopx/benchmark_adapters/skillsbench_codex_goal_trace.py +81 -0
  20. loopx/benchmark_adapters/skillsbench_codex_runtime.py +339 -0
  21. loopx/benchmark_adapters/skillsbench_dockerfile_runtime.py +467 -0
  22. loopx/benchmark_adapters/skillsbench_failure_signals.py +652 -0
  23. loopx/benchmark_adapters/skillsbench_proxy_runtime.py +327 -0
  24. loopx/benchmark_adapters/skillsbench_remote_bridge.py +402 -0
  25. loopx/benchmark_adapters/skillsbench_result_discovery.py +143 -0
  26. loopx/benchmark_adapters/skillsbench_runner_profile.py +436 -0
  27. loopx/benchmark_adapters/skillsbench_runner_source.py +99 -0
  28. loopx/benchmark_adapters/skillsbench_setup_preflight.py +771 -0
  29. loopx/benchmark_adapters/skillsbench_signals.py +15 -0
  30. loopx/benchmark_adapters/skillsbench_task_source.py +141 -0
  31. loopx/benchmark_adapters/skillsbench_turn_route.py +723 -0
  32. loopx/benchmark_adapters/skillsbench_turn_runtime.py +1069 -0
  33. loopx/benchmark_adapters/skillsbench_typed_repair.py +689 -0
  34. loopx/benchmark_adapters/skillsbench_uv_cache.py +111 -0
  35. loopx/benchmark_adapters/skillsbench_verifier_bootstrap.py +227 -0
  36. loopx/benchmark_adapters/skillsbench_verifier_cache.py +138 -0
  37. loopx/benchmark_adapters/terminal_bench.py +10078 -0
  38. loopx/benchmark_case_analysis.py +1276 -0
  39. loopx/benchmark_case_state.py +1079 -0
  40. loopx/benchmark_core/__init__.py +239 -0
  41. loopx/benchmark_core/adapter.py +84 -0
  42. loopx/benchmark_core/artifacts.py +517 -0
  43. loopx/benchmark_core/attempts.py +199 -0
  44. loopx/benchmark_core/container_exec.py +216 -0
  45. loopx/benchmark_core/io.py +68 -0
  46. loopx/benchmark_core/lifecycle.py +211 -0
  47. loopx/benchmark_core/loop_protocol.py +689 -0
  48. loopx/benchmark_core/observable_handles.py +348 -0
  49. loopx/benchmark_core/parity.py +256 -0
  50. loopx/benchmark_core/remote_closeout.py +482 -0
  51. loopx/benchmark_core/rounds.py +215 -0
  52. loopx/benchmark_core/route_profile.py +509 -0
  53. loopx/benchmark_core/run_permissions.py +206 -0
  54. loopx/benchmark_core/split_control.py +925 -0
  55. loopx/benchmark_core/turn_fidelity.py +326 -0
  56. loopx/benchmark_ledger.py +3793 -0
  57. loopx/benchmark_ledger_countability.py +372 -0
  58. loopx/benchmark_ledger_current.py +724 -0
  59. loopx/benchmark_trajectory.py +405 -0
  60. loopx/benchmarks/__init__.py +1 -0
  61. loopx/benchmarks/qualification/__init__.py +1 -0
  62. loopx/benchmarks/qualification/release_outcome_baseline.py +360 -0
  63. loopx/benchmarks/read_models/__init__.py +1 -0
  64. loopx/benchmarks/read_models/benchmark_attempt_accounting.py +53 -0
  65. loopx/benchmarks/read_models/benchmark_comparison.py +414 -0
  66. loopx/benchmarks/read_models/benchmark_event_timeline.py +113 -0
  67. loopx/benchmarks/read_models/benchmark_experiment_report.py +475 -0
  68. loopx/benchmarks/read_models/benchmark_learning_ledger.py +137 -0
  69. loopx/benchmarks/read_models/benchmark_lifecycle_contracts.py +228 -0
  70. loopx/benchmarks/read_models/benchmark_projection.py +723 -0
  71. loopx/benchmarks/read_models/benchmark_result.py +146 -0
  72. loopx/benchmarks/read_models/benchmark_run_execution_contract.py +116 -0
  73. loopx/benchmarks/read_models/benchmark_run_failure.py +157 -0
  74. loopx/benchmarks/read_models/benchmark_run_metrics.py +213 -0
  75. loopx/benchmarks/read_models/benchmark_run_post_execution.py +635 -0
  76. loopx/benchmarks/read_models/benchmark_run_pre_execution.py +541 -0
  77. loopx/benchmarks/read_models/benchmark_status_compaction.py +1255 -0
  78. loopx/benchmarks/read_models/benchmark_status_runner.py +780 -0
  79. loopx/benchmarks/read_models/goal_start_control_score.py +857 -0
  80. loopx/benchmarks/read_models/skillsbench_post_run_debug.py +746 -0
  81. loopx/benchmarks/read_models/skillsbench_verifier_attribution.py +269 -0
  82. loopx/bootstrap.py +1116 -0
  83. loopx/bootstrap_command_pack.py +2167 -0
  84. loopx/boundary_authority.py +199 -0
  85. loopx/canary/__init__.py +1 -0
  86. loopx/canary/maintainability_ratchet.py +800 -0
  87. loopx/canary/planner.py +1984 -0
  88. loopx/canary/premerge.py +1130 -0
  89. loopx/canary/qualification_profiles.py +309 -0
  90. loopx/canary/quality_surface_catalog.py +838 -0
  91. loopx/canary/release_profiles.py +51 -0
  92. loopx/canary/runner.py +1107 -0
  93. loopx/canary/smoke_health.py +581 -0
  94. loopx/canary/smoke_profiles.py +212 -0
  95. loopx/capabilities/__init__.py +0 -0
  96. loopx/capabilities/agent_turn_recall/__init__.py +17 -0
  97. loopx/capabilities/agent_turn_recall/cli.py +369 -0
  98. loopx/capabilities/agent_turn_recall/core.py +296 -0
  99. loopx/capabilities/auto_research/__init__.py +16 -0
  100. loopx/capabilities/auto_research/bootstrap_contract.py +157 -0
  101. loopx/capabilities/auto_research/cli.py +1468 -0
  102. loopx/capabilities/auto_research/core.py +11 -0
  103. loopx/capabilities/auto_research/defaults.py +79 -0
  104. loopx/capabilities/auto_research/demo_e2e.py +1848 -0
  105. loopx/capabilities/auto_research/demo_supervisor.py +186 -0
  106. loopx/capabilities/auto_research/evidence_packet.py +767 -0
  107. loopx/capabilities/auto_research/human_view.py +794 -0
  108. loopx/capabilities/auto_research/kernel.py +191 -0
  109. loopx/capabilities/auto_research/knn_demo_workspace.py +322 -0
  110. loopx/capabilities/auto_research/live_evidence.py +248 -0
  111. loopx/capabilities/auto_research/preset.py +176 -0
  112. loopx/capabilities/auto_research/research_state.py +1085 -0
  113. loopx/capabilities/auto_research/role_profiles.py +394 -0
  114. loopx/capabilities/auto_research/rollout_append.py +97 -0
  115. loopx/capabilities/auto_research/terminal_result_contract.py +422 -0
  116. loopx/capabilities/auto_research/terminal_result_projection.py +171 -0
  117. loopx/capabilities/auto_research/terminal_result_query.py +233 -0
  118. loopx/capabilities/auto_research/terminal_results.py +349 -0
  119. loopx/capabilities/auto_research/user_contract.py +190 -0
  120. loopx/capabilities/auto_research/worker_loop.py +163 -0
  121. loopx/capabilities/auto_research/worker_runtime.py +777 -0
  122. loopx/capabilities/auto_research/worker_skill/SKILL.md +343 -0
  123. loopx/capabilities/benchmark_toolkit/__init__.py +19 -0
  124. loopx/capabilities/benchmark_toolkit/integrity.py +387 -0
  125. loopx/capabilities/catalog.py +1875 -0
  126. loopx/capabilities/change_quality/__init__.py +19 -0
  127. loopx/capabilities/change_quality/cli.py +171 -0
  128. loopx/capabilities/change_quality/context.py +156 -0
  129. loopx/capabilities/change_quality/oracles.py +269 -0
  130. loopx/capabilities/change_quality/policy.py +34 -0
  131. loopx/capabilities/change_quality/receipt.py +482 -0
  132. loopx/capabilities/change_quality/result.py +493 -0
  133. loopx/capabilities/change_quality/scope.py +171 -0
  134. loopx/capabilities/change_quality/shadow.py +680 -0
  135. loopx/capabilities/content_ops/__init__.py +0 -0
  136. loopx/capabilities/content_ops/cli.py +649 -0
  137. loopx/capabilities/content_ops/connector_packets.py +164 -0
  138. loopx/capabilities/content_ops/item_lifecycle.py +1000 -0
  139. loopx/capabilities/content_ops/layout.py +451 -0
  140. loopx/capabilities/content_ops/markdown.py +456 -0
  141. loopx/capabilities/content_ops/schemas.py +51 -0
  142. loopx/capabilities/content_ops/social_browser_x.py +107 -0
  143. loopx/capabilities/content_ops/surface.py +1956 -0
  144. loopx/capabilities/content_ops/templates/layout-catalog-v0.json +72 -0
  145. loopx/capabilities/context_providers/__init__.py +36 -0
  146. loopx/capabilities/context_providers/base.py +189 -0
  147. loopx/capabilities/context_providers/factory.py +32 -0
  148. loopx/capabilities/context_providers/openviking.py +702 -0
  149. loopx/capabilities/context_providers/service_ownership.py +185 -0
  150. loopx/capabilities/decision_context/__init__.py +129 -0
  151. loopx/capabilities/decision_context/architecture.py +83 -0
  152. loopx/capabilities/decision_context/assembler.py +849 -0
  153. loopx/capabilities/decision_context/catalog_entry.py +195 -0
  154. loopx/capabilities/decision_context/cli.py +310 -0
  155. loopx/capabilities/decision_context/cursor_commit.py +535 -0
  156. loopx/capabilities/decision_context/outcome_feedback.py +352 -0
  157. loopx/capabilities/decision_context/packets.py +654 -0
  158. loopx/capabilities/decision_context/private_state.py +189 -0
  159. loopx/capabilities/decision_context/profile.py +453 -0
  160. loopx/capabilities/decision_context/providers.py +228 -0
  161. loopx/capabilities/decision_context/review_settlement.py +136 -0
  162. loopx/capabilities/decision_context/runtime.py +273 -0
  163. loopx/capabilities/decision_context/sources.py +415 -0
  164. loopx/capabilities/explore/__init__.py +1 -0
  165. loopx/capabilities/explore/activation.py +198 -0
  166. loopx/capabilities/explore/adaptive_replay_planner.py +221 -0
  167. loopx/capabilities/explore/child_replay_runtime.py +463 -0
  168. loopx/capabilities/explore/composition_frontier.py +291 -0
  169. loopx/capabilities/explore/counterfactual_runtime.py +578 -0
  170. loopx/capabilities/explore/episode_runtime.py +647 -0
  171. loopx/capabilities/explore/harness_checkpoint.py +171 -0
  172. loopx/capabilities/explore/harness_gate.py +115 -0
  173. loopx/capabilities/explore/harness_runtime.py +1124 -0
  174. loopx/capabilities/explore/replay_metrics.py +206 -0
  175. loopx/capabilities/explore/replay_runtime.py +1271 -0
  176. loopx/capabilities/explore/resource_portfolio.py +173 -0
  177. loopx/capabilities/explore/result_log.py +974 -0
  178. loopx/capabilities/explore/router_state.py +432 -0
  179. loopx/capabilities/explore/source_history_reconcile.py +255 -0
  180. loopx/capabilities/explore/speculative_scheduler.py +498 -0
  181. loopx/capabilities/explore/todo_branch_plan.py +650 -0
  182. loopx/capabilities/explore/todo_evidence.py +141 -0
  183. loopx/capabilities/explore/trace_runtime.py +284 -0
  184. loopx/capabilities/explore/worker_branch_plan.py +1257 -0
  185. loopx/capabilities/integration_branch/__init__.py +13 -0
  186. loopx/capabilities/integration_branch/cli.py +148 -0
  187. loopx/capabilities/integration_branch/core.py +916 -0
  188. loopx/capabilities/issue_fix/__init__.py +19 -0
  189. loopx/capabilities/issue_fix/acceptance_loop.py +1050 -0
  190. loopx/capabilities/issue_fix/candidate_evidence.py +503 -0
  191. loopx/capabilities/issue_fix/candidate_preflight.py +676 -0
  192. loopx/capabilities/issue_fix/cli.py +1822 -0
  193. loopx/capabilities/issue_fix/cli_input.py +87 -0
  194. loopx/capabilities/issue_fix/content_ops_cli.py +148 -0
  195. loopx/capabilities/issue_fix/discovered_issue_promotion.py +947 -0
  196. loopx/capabilities/issue_fix/explore_projection.py +710 -0
  197. loopx/capabilities/issue_fix/feasibility.py +542 -0
  198. loopx/capabilities/issue_fix/github_public.py +661 -0
  199. loopx/capabilities/issue_fix/intake_surface.py +832 -0
  200. loopx/capabilities/issue_fix/metadata_preview.py +218 -0
  201. loopx/capabilities/issue_fix/metrics_projection.py +1340 -0
  202. loopx/capabilities/issue_fix/metrics_supplement.py +634 -0
  203. loopx/capabilities/issue_fix/metrics_supplement_cli.py +127 -0
  204. loopx/capabilities/issue_fix/outcome_projection.py +1235 -0
  205. loopx/capabilities/issue_fix/periodic_report.py +189 -0
  206. loopx/capabilities/issue_fix/pr_description.py +418 -0
  207. loopx/capabilities/issue_fix/pr_gate_reconcile.py +496 -0
  208. loopx/capabilities/issue_fix/pr_gate_reconcile_cli.py +464 -0
  209. loopx/capabilities/issue_fix/pr_lifecycle.py +1327 -0
  210. loopx/capabilities/issue_fix/pr_lifecycle_rollout.py +85 -0
  211. loopx/capabilities/issue_fix/pr_monitor_materialization.py +257 -0
  212. loopx/capabilities/issue_fix/pr_review_ack.py +439 -0
  213. loopx/capabilities/issue_fix/provider_hooks.py +24 -0
  214. loopx/capabilities/issue_fix/repository_commit_evidence.py +186 -0
  215. loopx/capabilities/issue_fix/repository_context.py +457 -0
  216. loopx/capabilities/issue_fix/repository_memory.py +459 -0
  217. loopx/capabilities/issue_fix/repository_memory_provider.py +1454 -0
  218. loopx/capabilities/issue_fix/repository_snapshot.py +454 -0
  219. loopx/capabilities/issue_fix/reviewer_cli.py +917 -0
  220. loopx/capabilities/issue_fix/reviewer_notification.py +882 -0
  221. loopx/capabilities/issue_fix/reviewer_notification_drain.py +942 -0
  222. loopx/capabilities/issue_fix/reviewer_recommendation.py +1057 -0
  223. loopx/capabilities/issue_fix/reviewer_request.py +1282 -0
  224. loopx/capabilities/issue_fix/reward_memory.py +879 -0
  225. loopx/capabilities/issue_fix/workflow_plan.py +1286 -0
  226. loopx/capabilities/material_lifecycle/__init__.py +161 -0
  227. loopx/capabilities/material_lifecycle/_validation.py +183 -0
  228. loopx/capabilities/material_lifecycle/apply.py +672 -0
  229. loopx/capabilities/material_lifecycle/architecture.py +122 -0
  230. loopx/capabilities/material_lifecycle/cli.py +161 -0
  231. loopx/capabilities/material_lifecycle/decision_planning.py +470 -0
  232. loopx/capabilities/material_lifecycle/explore_execution.py +306 -0
  233. loopx/capabilities/material_lifecycle/intake.py +869 -0
  234. loopx/capabilities/material_lifecycle/inventory.py +147 -0
  235. loopx/capabilities/material_lifecycle/lifecycle.py +98 -0
  236. loopx/capabilities/material_lifecycle/preparation.py +147 -0
  237. loopx/capabilities/material_lifecycle/project_skill.py +83 -0
  238. loopx/capabilities/material_lifecycle/ranking.py +267 -0
  239. loopx/capabilities/material_lifecycle/readable_projection.py +500 -0
  240. loopx/capabilities/material_lifecycle/rebuild.py +480 -0
  241. loopx/capabilities/material_lifecycle/settlement.py +238 -0
  242. loopx/capabilities/periodic_report/__init__.py +71 -0
  243. loopx/capabilities/periodic_report/adapters.py +939 -0
  244. loopx/capabilities/periodic_report/archive.py +422 -0
  245. loopx/capabilities/periodic_report/bindings.py +705 -0
  246. loopx/capabilities/periodic_report/cli.py +277 -0
  247. loopx/capabilities/periodic_report/core.py +691 -0
  248. loopx/capabilities/periodic_report/extension_envelope.py +66 -0
  249. loopx/capabilities/periodic_report/presets.py +103 -0
  250. loopx/capabilities/periodic_report/profile.py +235 -0
  251. loopx/capabilities/periodic_report/project_progress.py +179 -0
  252. loopx/capabilities/periodic_report/triggers.py +452 -0
  253. loopx/capabilities/pr_review_queue/__init__.py +17 -0
  254. loopx/capabilities/pr_review_queue/core.py +506 -0
  255. loopx/capabilities/pr_review_queue/review_contract.py +506 -0
  256. loopx/capabilities/registry.py +192 -0
  257. loopx/capabilities/reward_memory/__init__.py +75 -0
  258. loopx/capabilities/reward_memory/application.py +819 -0
  259. loopx/capabilities/reward_memory/architecture.py +572 -0
  260. loopx/capabilities/reward_memory/candidate_review.py +511 -0
  261. loopx/capabilities/reward_memory/cli.py +469 -0
  262. loopx/capabilities/reward_memory/dogfood.py +574 -0
  263. loopx/capabilities/reward_memory/evaluation.py +296 -0
  264. loopx/capabilities/reward_memory/evaluation_fixtures.py +362 -0
  265. loopx/capabilities/reward_memory/experiment.py +567 -0
  266. loopx/capabilities/reward_memory/health.py +222 -0
  267. loopx/capabilities/reward_memory/ingestion.py +519 -0
  268. loopx/capabilities/reward_memory/registry.py +600 -0
  269. loopx/capabilities/reward_memory/runtime_hooks.py +312 -0
  270. loopx/capabilities/reward_memory/scoped_feedback.py +173 -0
  271. loopx/capabilities/semantic_preference/__init__.py +12 -0
  272. loopx/capabilities/semantic_preference/cli.py +189 -0
  273. loopx/capabilities/semantic_preference/contract.py +592 -0
  274. loopx/capabilities/semantic_preference/reward_memory.py +62 -0
  275. loopx/capabilities/value_connectors/__init__.py +1 -0
  276. loopx/capabilities/value_connectors/cli.py +401 -0
  277. loopx/capabilities/value_connectors/finance_extension_migration.py +108 -0
  278. loopx/capabilities/value_connectors/install_check.py +147 -0
  279. loopx/capabilities/value_connectors/planner.py +733 -0
  280. loopx/capabilities/value_connectors/source_map.py +446 -0
  281. loopx/claude_goal_baseline.py +138 -0
  282. loopx/claude_goal_mode/__init__.py +23 -0
  283. loopx/claude_goal_mode/hooks/goal_policy.py +212 -0
  284. loopx/claude_goal_mode/hooks/goal_state.py +139 -0
  285. loopx/claude_goal_mode/mcp/loopx_mcp.py +167 -0
  286. loopx/claude_goal_mode/scripts/connect.py +103 -0
  287. loopx/claude_goal_mode/scripts/goalmode_cmd.py +241 -0
  288. loopx/claude_goal_mode/scripts/install.py +328 -0
  289. loopx/claude_goal_mode/statusline/goal_status.py +97 -0
  290. loopx/cli.py +836 -0
  291. loopx/cli_commands/__init__.py +334 -0
  292. loopx/cli_commands/_host_thread.py +13 -0
  293. loopx/cli_commands/agentissue_runner_flow.py +447 -0
  294. loopx/cli_commands/agents_last_exam.py +160 -0
  295. loopx/cli_commands/agents_last_exam_baked_input.py +302 -0
  296. loopx/cli_commands/agents_last_exam_host_codex.py +374 -0
  297. loopx/cli_commands/agents_last_exam_launch_dry_run.py +372 -0
  298. loopx/cli_commands/agents_last_exam_local_plan.py +322 -0
  299. loopx/cli_commands/agents_last_exam_runner_source.py +352 -0
  300. loopx/cli_commands/agents_last_exam_task_material.py +335 -0
  301. loopx/cli_commands/agents_last_exam_validation_gate.py +236 -0
  302. loopx/cli_commands/benchmark_boundary.py +499 -0
  303. loopx/cli_commands/benchmark_dispatch.py +161 -0
  304. loopx/cli_commands/benchmark_release_outcome.py +123 -0
  305. loopx/cli_commands/benchmark_review_lifecycle.py +1275 -0
  306. loopx/cli_commands/benchmark_run_ledger.py +763 -0
  307. loopx/cli_commands/benchmark_run_ledger_case_analysis.py +249 -0
  308. loopx/cli_commands/benchmark_run_ledger_classification.py +45 -0
  309. loopx/cli_commands/benchmark_run_ledger_maintenance.py +486 -0
  310. loopx/cli_commands/benchmark_run_ledger_maintenance_registration.py +342 -0
  311. loopx/cli_commands/benchmark_run_ledger_maintenance_rendering.py +233 -0
  312. loopx/cli_commands/benchmark_run_ledger_parity.py +92 -0
  313. loopx/cli_commands/bootstrap_connect.py +238 -0
  314. loopx/cli_commands/canary.py +707 -0
  315. loopx/cli_commands/canary_release_qualification.py +79 -0
  316. loopx/cli_commands/capability.py +96 -0
  317. loopx/cli_commands/doctor.py +43 -0
  318. loopx/cli_commands/dreaming.py +143 -0
  319. loopx/cli_commands/edgebench.py +205 -0
  320. loopx/cli_commands/evidence_log.py +275 -0
  321. loopx/cli_commands/explore.py +989 -0
  322. loopx/cli_commands/explore_planning_commands.py +157 -0
  323. loopx/cli_commands/extension.py +271 -0
  324. loopx/cli_commands/first_run_report.py +73 -0
  325. loopx/cli_commands/goal_channel.py +656 -0
  326. loopx/cli_commands/handoff_mode.py +158 -0
  327. loopx/cli_commands/history.py +622 -0
  328. loopx/cli_commands/host_mode_plan.py +113 -0
  329. loopx/cli_commands/lark_inbox.py +431 -0
  330. loopx/cli_commands/lark_kanban.py +629 -0
  331. loopx/cli_commands/ml_experiment.py +321 -0
  332. loopx/cli_commands/multi_agent.py +211 -0
  333. loopx/cli_commands/opencode2_goal_worker.py +217 -0
  334. loopx/cli_commands/pr_review.py +167 -0
  335. loopx/cli_commands/presentation.py +218 -0
  336. loopx/cli_commands/preset.py +96 -0
  337. loopx/cli_commands/project.py +150 -0
  338. loopx/cli_commands/project_lifecycle.py +915 -0
  339. loopx/cli_commands/quota.py +859 -0
  340. loopx/cli_commands/quota_registration.py +241 -0
  341. loopx/cli_commands/quota_request.py +113 -0
  342. loopx/cli_commands/ready_score.py +110 -0
  343. loopx/cli_commands/registry_admin.py +975 -0
  344. loopx/cli_commands/registry_admin_configure.py +344 -0
  345. loopx/cli_commands/registry_admin_peer.py +84 -0
  346. loopx/cli_commands/registry_authority.py +218 -0
  347. loopx/cli_commands/review_batch.py +146 -0
  348. loopx/cli_commands/slash_commands.py +145 -0
  349. loopx/cli_commands/start_goal.py +251 -0
  350. loopx/cli_commands/starter.py +175 -0
  351. loopx/cli_commands/starter_bootstrap.py +179 -0
  352. loopx/cli_commands/starter_bootstrap_registration.py +198 -0
  353. loopx/cli_commands/starter_runtime_idle.py +107 -0
  354. loopx/cli_commands/starter_scheduler.py +207 -0
  355. loopx/cli_commands/starter_session_runtime.py +152 -0
  356. loopx/cli_commands/starter_visible_common.py +54 -0
  357. loopx/cli_commands/starter_visible_driver.py +161 -0
  358. loopx/cli_commands/starter_visible_pilot.py +278 -0
  359. loopx/cli_commands/status.py +867 -0
  360. loopx/cli_commands/status_registration.py +239 -0
  361. loopx/cli_commands/summary_all.py +222 -0
  362. loopx/cli_commands/support_control.py +809 -0
  363. loopx/cli_commands/support_control_registry.py +68 -0
  364. loopx/cli_commands/support_control_supervisor.py +289 -0
  365. loopx/cli_commands/task_lease.py +306 -0
  366. loopx/cli_commands/terminal_bench_adapter.py +717 -0
  367. loopx/cli_commands/terminal_bench_environment_result.py +1246 -0
  368. loopx/cli_commands/todo.py +940 -0
  369. loopx/cli_commands/todo_argument_validation.py +572 -0
  370. loopx/cli_commands/todo_event.py +114 -0
  371. loopx/cli_commands/turn.py +804 -0
  372. loopx/cli_commands/version.py +46 -0
  373. loopx/cli_commands/worker_bridge.py +659 -0
  374. loopx/cli_rollout.py +314 -0
  375. loopx/codex_cli_goal_tui.py +672 -0
  376. loopx/codex_cli_probe.py +1530 -0
  377. loopx/codex_cli_probe_markdown.py +935 -0
  378. loopx/codex_cli_runtime_probe.py +733 -0
  379. loopx/codex_cli_scheduler.py +564 -0
  380. loopx/codex_goal_baseline.py +620 -0
  381. loopx/configuration_catalog.py +617 -0
  382. loopx/configure_goal.py +1375 -0
  383. loopx/contract.py +996 -0
  384. loopx/control_plane/__init__.py +71 -0
  385. loopx/control_plane/agents/__init__.py +1 -0
  386. loopx/control_plane/agents/agent_lane_recommendation.py +516 -0
  387. loopx/control_plane/agents/agent_scope.py +1578 -0
  388. loopx/control_plane/agents/agent_scope_frontier.py +60 -0
  389. loopx/control_plane/agents/capability_gate.py +531 -0
  390. loopx/control_plane/agents/identity.py +140 -0
  391. loopx/control_plane/agents/legacy_migration.py +169 -0
  392. loopx/control_plane/agents/management_projection.py +658 -0
  393. loopx/control_plane/agents/material_frontier.py +608 -0
  394. loopx/control_plane/agents/material_handoff.py +156 -0
  395. loopx/control_plane/agents/multi_agent/__init__.py +1 -0
  396. loopx/control_plane/agents/multi_agent/codex_executable.py +207 -0
  397. loopx/control_plane/agents/multi_agent/collective_round_ledger.py +387 -0
  398. loopx/control_plane/agents/multi_agent/contract.py +474 -0
  399. loopx/control_plane/agents/multi_agent/recipe.py +110 -0
  400. loopx/control_plane/agents/multi_agent/role_successor.py +297 -0
  401. loopx/control_plane/agents/multi_agent/runtime_scripts.py +426 -0
  402. loopx/control_plane/agents/multi_agent/visible_launch_policy.py +149 -0
  403. loopx/control_plane/agents/multi_agent/visible_wake_scheduler.py +392 -0
  404. loopx/control_plane/agents/profile.py +216 -0
  405. loopx/control_plane/agents/runtime_model.py +73 -0
  406. loopx/control_plane/agents/subagent_activity.py +164 -0
  407. loopx/control_plane/agents/supervisor.py +544 -0
  408. loopx/control_plane/agents/supervisor_events.py +462 -0
  409. loopx/control_plane/agents/supervisor_inject.py +204 -0
  410. loopx/control_plane/agents/work_mode.py +56 -0
  411. loopx/control_plane/agents/workspace_guard.py +364 -0
  412. loopx/control_plane/effect_program.py +644 -0
  413. loopx/control_plane/goals/__init__.py +1 -0
  414. loopx/control_plane/goals/active_state_event_projection.py +103 -0
  415. loopx/control_plane/goals/active_state_metadata.py +47 -0
  416. loopx/control_plane/goals/active_state_sections.py +58 -0
  417. loopx/control_plane/goals/configure_goal_service.py +354 -0
  418. loopx/control_plane/goals/contract_health.py +132 -0
  419. loopx/control_plane/goals/dreaming.py +152 -0
  420. loopx/control_plane/goals/global_registry_health.py +199 -0
  421. loopx/control_plane/goals/global_registry_shadow.py +33 -0
  422. loopx/control_plane/goals/goal_channel.py +34 -0
  423. loopx/control_plane/goals/goal_channel_projection.py +560 -0
  424. loopx/control_plane/goals/goal_frontier/__init__.py +1917 -0
  425. loopx/control_plane/goals/goal_frontier/ack_policy.py +149 -0
  426. loopx/control_plane/goals/goal_frontier/outcome_continuity.py +437 -0
  427. loopx/control_plane/goals/goal_frontier/replan_rules.py +210 -0
  428. loopx/control_plane/goals/goal_frontier/semantic_history.py +314 -0
  429. loopx/control_plane/goals/goal_frontier/terminal.py +180 -0
  430. loopx/control_plane/goals/goal_vision.py +443 -0
  431. loopx/control_plane/goals/goal_vision_policy.py +36 -0
  432. loopx/control_plane/goals/goal_vision_state.py +62 -0
  433. loopx/control_plane/goals/goal_vision_wait.py +290 -0
  434. loopx/control_plane/goals/path_resolution.py +20 -0
  435. loopx/control_plane/goals/start_contract.py +206 -0
  436. loopx/control_plane/goals/vision_checkpoint.py +92 -0
  437. loopx/control_plane/handoff/__init__.py +1 -0
  438. loopx/control_plane/handoff/cross_runtime_impl_review.py +311 -0
  439. loopx/control_plane/handoff/delivery_contract.py +161 -0
  440. loopx/control_plane/handoff/handoff_runs.py +71 -0
  441. loopx/control_plane/handoff/project_handoff.py +155 -0
  442. loopx/control_plane/handoff/review_batch.py +463 -0
  443. loopx/control_plane/handoff/review_packet_context.py +216 -0
  444. loopx/control_plane/heartbeat/agent.py +173 -0
  445. loopx/control_plane/heartbeat/budget.py +66 -0
  446. loopx/control_plane/heartbeat/builder.py +501 -0
  447. loopx/control_plane/heartbeat/host.py +64 -0
  448. loopx/control_plane/heartbeat/rules.py +68 -0
  449. loopx/control_plane/heartbeat/task_body.py +759 -0
  450. loopx/control_plane/heartbeat/visible_goal.py +86 -0
  451. loopx/control_plane/projects/__init__.py +1 -0
  452. loopx/control_plane/projects/contract.py +25 -0
  453. loopx/control_plane/projects/registry.py +663 -0
  454. loopx/control_plane/quota/__init__.py +1 -0
  455. loopx/control_plane/quota/cli_projection.py +704 -0
  456. loopx/control_plane/quota/decision_summary.py +431 -0
  457. loopx/control_plane/quota/effect_program.py +152 -0
  458. loopx/control_plane/quota/error_codes.py +19 -0
  459. loopx/control_plane/quota/goal_boundary.py +464 -0
  460. loopx/control_plane/quota/heartbeat_receipt.py +277 -0
  461. loopx/control_plane/quota/heartbeat_recommendation.py +718 -0
  462. loopx/control_plane/quota/host_poll_receipts.py +162 -0
  463. loopx/control_plane/quota/live_decision.py +142 -0
  464. loopx/control_plane/quota/monitor_poll.py +786 -0
  465. loopx/control_plane/quota/policy_constants.py +40 -0
  466. loopx/control_plane/quota/projection_repair.py +262 -0
  467. loopx/control_plane/quota/recent_runs.py +210 -0
  468. loopx/control_plane/quota/scheduler_ack.py +490 -0
  469. loopx/control_plane/quota/selected_todo_projection.py +139 -0
  470. loopx/control_plane/quota/settlement.py +437 -0
  471. loopx/control_plane/quota/settlement_cli.py +246 -0
  472. loopx/control_plane/quota/settlement_validation.py +64 -0
  473. loopx/control_plane/quota/settlement_workspace_causality.py +180 -0
  474. loopx/control_plane/quota/should_run.py +249 -0
  475. loopx/control_plane/quota/should_run_packet.py +1165 -0
  476. loopx/control_plane/quota/should_run_prepare.py +675 -0
  477. loopx/control_plane/quota/slot_accounting.py +1123 -0
  478. loopx/control_plane/quota/spend_sources.py +11 -0
  479. loopx/control_plane/quota/stall_repair.py +397 -0
  480. loopx/control_plane/quota/states.py +29 -0
  481. loopx/control_plane/quota/task_orchestration.py +448 -0
  482. loopx/control_plane/quota/task_orchestration_admission.py +497 -0
  483. loopx/control_plane/quota/turn_envelope.py +889 -0
  484. loopx/control_plane/quota/usage_summary.py +140 -0
  485. loopx/control_plane/reward_memory.py +43 -0
  486. loopx/control_plane/runtime/__init__.py +2 -0
  487. loopx/control_plane/runtime/active_user_assisted_pilot.py +275 -0
  488. loopx/control_plane/runtime/agent_scoped_evidence_log.py +435 -0
  489. loopx/control_plane/runtime/decision_freshness.py +203 -0
  490. loopx/control_plane/runtime/event_ledger.py +197 -0
  491. loopx/control_plane/runtime/event_store_migration_bridge.py +196 -0
  492. loopx/control_plane/runtime/goal_project_route.py +70 -0
  493. loopx/control_plane/runtime/local_state_write_correctness.py +242 -0
  494. loopx/control_plane/runtime/promotion_readiness.py +152 -0
  495. loopx/control_plane/runtime/public_safety.py +120 -0
  496. loopx/control_plane/runtime/run_artifacts.py +78 -0
  497. loopx/control_plane/runtime/run_compaction.py +397 -0
  498. loopx/control_plane/runtime/run_context_retention.py +241 -0
  499. loopx/control_plane/runtime/run_history.py +132 -0
  500. loopx/control_plane/runtime/run_index_duplicates.py +205 -0
  501. loopx/control_plane/runtime/run_index_rebuild.py +263 -0
  502. loopx/control_plane/runtime/run_ingest_health.py +336 -0
  503. loopx/control_plane/runtime/runtime_projection_route.py +624 -0
  504. loopx/control_plane/runtime/runtime_projection_writer.py +98 -0
  505. loopx/control_plane/runtime/session_runtime.py +339 -0
  506. loopx/control_plane/runtime/shared_runtime_material_projection.py +332 -0
  507. loopx/control_plane/runtime/shared_runtime_refresh_projection.py +183 -0
  508. loopx/control_plane/runtime/stale_latest_run.py +90 -0
  509. loopx/control_plane/runtime/status_classifications.py +49 -0
  510. loopx/control_plane/runtime/status_projection_cache.py +235 -0
  511. loopx/control_plane/runtime/stride_observation.py +144 -0
  512. loopx/control_plane/runtime/time.py +39 -0
  513. loopx/control_plane/runtime/trajectory_hygiene.py +149 -0
  514. loopx/control_plane/runtime/validation_command.py +69 -0
  515. loopx/control_plane/scheduler/__init__.py +1 -0
  516. loopx/control_plane/scheduler/ack.py +329 -0
  517. loopx/control_plane/scheduler/arbitration.py +188 -0
  518. loopx/control_plane/scheduler/automation_liveness.py +183 -0
  519. loopx/control_plane/scheduler/execution_context.py +555 -0
  520. loopx/control_plane/scheduler/external_evidence_observation.py +428 -0
  521. loopx/control_plane/scheduler/monitor_display.py +143 -0
  522. loopx/control_plane/scheduler/monitor_poll_policy.py +161 -0
  523. loopx/control_plane/scheduler/monitor_poll_writeback.py +351 -0
  524. loopx/control_plane/scheduler/monitor_target.py +64 -0
  525. loopx/control_plane/scheduler/monitor_todo.py +146 -0
  526. loopx/control_plane/scheduler/monitor_wait.py +237 -0
  527. loopx/control_plane/scheduler/scheduler_hint.py +1284 -0
  528. loopx/control_plane/scheduler/state.py +354 -0
  529. loopx/control_plane/scheduler/state_transition_rules.py +179 -0
  530. loopx/control_plane/scheduler/time.py +10 -0
  531. loopx/control_plane/settlement_driver.py +293 -0
  532. loopx/control_plane/status/__init__.py +6 -0
  533. loopx/control_plane/status/active_state_projection.py +105 -0
  534. loopx/control_plane/status/agent_lane_projection.py +375 -0
  535. loopx/control_plane/status/attention_projection.py +74 -0
  536. loopx/control_plane/status/autonomous_replan_projection.py +103 -0
  537. loopx/control_plane/status/collection.py +140 -0
  538. loopx/control_plane/status/contract_projection.py +31 -0
  539. loopx/control_plane/status/dreaming_projection.py +52 -0
  540. loopx/control_plane/status/goal_attention_projection.py +157 -0
  541. loopx/control_plane/status/lifecycle_projection.py +110 -0
  542. loopx/control_plane/status/monitor_display_projection.py +69 -0
  543. loopx/control_plane/status/registry_health_projection.py +75 -0
  544. loopx/control_plane/status/run_projection.py +70 -0
  545. loopx/control_plane/status/runtime_summaries.py +161 -0
  546. loopx/control_plane/testing/__init__.py +1 -0
  547. loopx/control_plane/testing/actual_default_model_behavior_portfolio.py +1371 -0
  548. loopx/control_plane/testing/canary_harness.py +182 -0
  549. loopx/control_plane/testing/capability_monitor_repair_tool_behavior.py +674 -0
  550. loopx/control_plane/testing/cli_output_budget.py +807 -0
  551. loopx/control_plane/testing/cli_output_differential.py +250 -0
  552. loopx/control_plane/testing/cli_output_semantics.py +87 -0
  553. loopx/control_plane/testing/control_plane_composition_scenarios.py +225 -0
  554. loopx/control_plane/testing/decision_replay.py +268 -0
  555. loopx/control_plane/testing/doubao_model_behavior_actor.py +559 -0
  556. loopx/control_plane/testing/model_behavior_corpus.py +344 -0
  557. loopx/control_plane/testing/model_behavior_qualification.py +769 -0
  558. loopx/control_plane/testing/model_behavior_retained_cases.py +235 -0
  559. loopx/control_plane/testing/model_tool_behavior.py +536 -0
  560. loopx/control_plane/testing/onboarding_model_behavior_qualification.py +642 -0
  561. loopx/control_plane/testing/quota_fixtures.py +208 -0
  562. loopx/control_plane/testing/quota_should_run_parity.py +57 -0
  563. loopx/control_plane/testing/release_commit_qualification.py +671 -0
  564. loopx/control_plane/testing/replan_semantic_action_behavior.py +1302 -0
  565. loopx/control_plane/testing/scoped_gate_successor_tool_behavior.py +527 -0
  566. loopx/control_plane/testing/selected_todo_tool_behavior.py +1002 -0
  567. loopx/control_plane/testing/terminal_settlement_tool_behavior.py +656 -0
  568. loopx/control_plane/todos/__init__.py +1 -0
  569. loopx/control_plane/todos/active_state_editing.py +296 -0
  570. loopx/control_plane/todos/active_state_todo_parser.py +138 -0
  571. loopx/control_plane/todos/active_state_todos.py +175 -0
  572. loopx/control_plane/todos/addition.py +103 -0
  573. loopx/control_plane/todos/claim_visibility.py +253 -0
  574. loopx/control_plane/todos/completed_archive.py +139 -0
  575. loopx/control_plane/todos/completion_fence.py +49 -0
  576. loopx/control_plane/todos/completion_policy.py +153 -0
  577. loopx/control_plane/todos/completion_validation.py +248 -0
  578. loopx/control_plane/todos/completion_validation_accountability.py +27 -0
  579. loopx/control_plane/todos/completion_validation_projection.py +57 -0
  580. loopx/control_plane/todos/contract.py +1476 -0
  581. loopx/control_plane/todos/decision_scope.py +554 -0
  582. loopx/control_plane/todos/deferred_resume.py +546 -0
  583. loopx/control_plane/todos/durable_completion.py +201 -0
  584. loopx/control_plane/todos/event_writeback.py +484 -0
  585. loopx/control_plane/todos/frontier_deadline.py +132 -0
  586. loopx/control_plane/todos/handoff_gate.py +283 -0
  587. loopx/control_plane/todos/handoff_mode.py +444 -0
  588. loopx/control_plane/todos/handoff_note.py +202 -0
  589. loopx/control_plane/todos/line_update.py +361 -0
  590. loopx/control_plane/todos/list_projection.py +205 -0
  591. loopx/control_plane/todos/markdown.py +199 -0
  592. loopx/control_plane/todos/monitor_metadata.py +88 -0
  593. loopx/control_plane/todos/mutation_authority.py +299 -0
  594. loopx/control_plane/todos/projection.py +655 -0
  595. loopx/control_plane/todos/quota_summary.py +1138 -0
  596. loopx/control_plane/todos/route_continuation.py +267 -0
  597. loopx/control_plane/todos/succession_warning.py +174 -0
  598. loopx/control_plane/todos/summary_item.py +223 -0
  599. loopx/control_plane/todos/text.py +30 -0
  600. loopx/control_plane/todos/todo_index.py +226 -0
  601. loopx/control_plane/todos/todo_summary.py +1458 -0
  602. loopx/control_plane/todos/unblock_resume.py +326 -0
  603. loopx/control_plane/todos/user_gate.py +263 -0
  604. loopx/control_plane/todos/write_hint.py +63 -0
  605. loopx/control_plane/todos/write_policy.py +135 -0
  606. loopx/control_plane/turn_driver/__init__.py +85 -0
  607. loopx/control_plane/turn_driver/codex_cli.py +502 -0
  608. loopx/control_plane/turn_driver/driver.py +355 -0
  609. loopx/control_plane/turn_driver/executor.py +1468 -0
  610. loopx/control_plane/turn_driver/loop_controller.py +669 -0
  611. loopx/control_plane/turn_driver/settlement.py +318 -0
  612. loopx/control_plane/turn_driver/transaction.py +375 -0
  613. loopx/control_plane/work_items/__init__.py +1 -0
  614. loopx/control_plane/work_items/attention_fields.py +56 -0
  615. loopx/control_plane/work_items/attention_item.py +77 -0
  616. loopx/control_plane/work_items/attention_queue.py +322 -0
  617. loopx/control_plane/work_items/attention_routing.py +213 -0
  618. loopx/control_plane/work_items/autonomous_candidates.py +135 -0
  619. loopx/control_plane/work_items/autonomous_replan_ack.py +276 -0
  620. loopx/control_plane/work_items/autonomous_replan_obligation.py +786 -0
  621. loopx/control_plane/work_items/backlog_hygiene.py +59 -0
  622. loopx/control_plane/work_items/capability_monitor_fallback.py +221 -0
  623. loopx/control_plane/work_items/delivery_batch_scale.py +66 -0
  624. loopx/control_plane/work_items/delivery_outcome.py +152 -0
  625. loopx/control_plane/work_items/delivery_signals.py +113 -0
  626. loopx/control_plane/work_items/execution_obligation.py +235 -0
  627. loopx/control_plane/work_items/goal_route_hint.py +320 -0
  628. loopx/control_plane/work_items/interaction_contract.py +1540 -0
  629. loopx/control_plane/work_items/issue_meta_surface.py +159 -0
  630. loopx/control_plane/work_items/lifecycle.py +139 -0
  631. loopx/control_plane/work_items/operator_inbox.py +266 -0
  632. loopx/control_plane/work_items/outcome_followthrough.py +69 -0
  633. loopx/control_plane/work_items/primary_action.py +326 -0
  634. loopx/control_plane/work_items/progress_observation.py +630 -0
  635. loopx/control_plane/work_items/project_asset.py +675 -0
  636. loopx/control_plane/work_items/repair_delta.py +693 -0
  637. loopx/control_plane/work_items/runtime_capability_reentry.py +168 -0
  638. loopx/control_plane/work_items/semantic_replan_writeback.py +177 -0
  639. loopx/control_plane/work_items/status_contract.py +49 -0
  640. loopx/control_plane/work_items/task_graph.py +1046 -0
  641. loopx/control_plane/work_items/task_lease.py +1254 -0
  642. loopx/control_plane/work_items/task_lease_settlement.py +422 -0
  643. loopx/control_plane/work_items/work_lane.py +510 -0
  644. loopx/control_plane/work_items/work_lane_context.py +161 -0
  645. loopx/demo.py +247 -0
  646. loopx/diagnose.py +633 -0
  647. loopx/doctor.py +1251 -0
  648. loopx/domain_packs/__init__.py +1 -0
  649. loopx/domain_packs/issue_fix.py +571 -0
  650. loopx/domain_packs/ml_experiment.py +854 -0
  651. loopx/domain_state.py +137 -0
  652. loopx/dreaming.py +706 -0
  653. loopx/entrypoint.py +16 -0
  654. loopx/event_sourced_state.py +981 -0
  655. loopx/execution_profile.py +286 -0
  656. loopx/experiments/__init__.py +1 -0
  657. loopx/experiments/planner_worker/__init__.py +1 -0
  658. loopx/experiments/planner_worker/contract.py +523 -0
  659. loopx/experiments/planner_worker/runtime.py +391 -0
  660. loopx/experiments/planner_worker/traex.py +461 -0
  661. loopx/explore_graph.py +11 -0
  662. loopx/extensions/__init__.py +1 -0
  663. loopx/extensions/bundled.py +28 -0
  664. loopx/extensions/execution_envelope.py +126 -0
  665. loopx/extensions/lark/__init__.py +11 -0
  666. loopx/extensions/lark/event_collector.py +478 -0
  667. loopx/extensions/lark/event_collector_runtime.py +506 -0
  668. loopx/extensions/lark/event_inbox.py +454 -0
  669. loopx/extensions/lark/extension.toml +88 -0
  670. loopx/extensions/lark/goal_channel.py +44 -0
  671. loopx/extensions/lark/goal_channel_contracts.py +388 -0
  672. loopx/extensions/lark/goal_channel_lifecycle.py +218 -0
  673. loopx/extensions/lark/goal_channel_runtime.py +792 -0
  674. loopx/extensions/lark/goal_channel_setup.py +805 -0
  675. loopx/extensions/lark/goal_channel_targets.py +215 -0
  676. loopx/extensions/lark/goal_channel_transport.py +281 -0
  677. loopx/extensions/lark/inbox_reactions.py +650 -0
  678. loopx/extensions/lark/inbox_reply.py +430 -0
  679. loopx/extensions/lark/presentation/__init__.py +11 -0
  680. loopx/extensions/lark/presentation/explore_results.py +2276 -0
  681. loopx/extensions/lark/presentation/explore_singleflight.py +127 -0
  682. loopx/extensions/lark/presentation/explore_source_guard.py +121 -0
  683. loopx/extensions/lark/presentation/explore_stage_document.py +703 -0
  684. loopx/extensions/lark/presentation/explore_visual_integrity.py +122 -0
  685. loopx/extensions/lark/presentation/explore_visual_readback.py +452 -0
  686. loopx/extensions/lark/presentation/explore_visual_styles.py +156 -0
  687. loopx/extensions/lark/presentation/issue_fix_surface.py +612 -0
  688. loopx/extensions/lark/presentation/kanban.py +2791 -0
  689. loopx/extensions/lark/presentation/message_card.py +112 -0
  690. loopx/extensions/lark/presentation/periodic_report.py +261 -0
  691. loopx/extensions/lark/presentation/projection_rows.py +600 -0
  692. loopx/extensions/lark/presentation/record_io.py +95 -0
  693. loopx/extensions/lark/presentation/sync_receipt.py +145 -0
  694. loopx/extensions/lark/private_json.py +40 -0
  695. loopx/extensions/lark/provider.py +86 -0
  696. loopx/extensions/lark/reviewer_notification.py +604 -0
  697. loopx/extensions/manifest.py +385 -0
  698. loopx/extensions/openviking_periodic_report/__init__.py +17 -0
  699. loopx/extensions/openviking_periodic_report/activation.py +173 -0
  700. loopx/extensions/openviking_periodic_report/extension.toml +17 -0
  701. loopx/extensions/openviking_periodic_report/provider.py +355 -0
  702. loopx/extensions/openviking_periodic_report/sink.py +117 -0
  703. loopx/extensions/openviking_semantic_preference/__init__.py +5 -0
  704. loopx/extensions/openviking_semantic_preference/extension.toml +16 -0
  705. loopx/extensions/openviking_semantic_preference/history_export.py +484 -0
  706. loopx/extensions/openviking_semantic_preference/project_peer.py +68 -0
  707. loopx/extensions/openviking_semantic_preference/provider.py +312 -0
  708. loopx/extensions/presentation.py +979 -0
  709. loopx/extensions/process_runtime.py +204 -0
  710. loopx/extensions/readiness.py +168 -0
  711. loopx/extensions/runtime.py +931 -0
  712. loopx/extensions/scaffold.py +335 -0
  713. loopx/feedback.py +581 -0
  714. loopx/file_lock.py +382 -0
  715. loopx/global_registry.py +842 -0
  716. loopx/global_risks.py +970 -0
  717. loopx/global_todos.py +568 -0
  718. loopx/handoff_budget.py +28 -0
  719. loopx/heartbeat_prequota.py +80 -0
  720. loopx/heartbeat_prompt.py +159 -0
  721. loopx/help_surface.py +516 -0
  722. loopx/history.py +1507 -0
  723. loopx/host_loop_activation.py +1311 -0
  724. loopx/host_mode_planner.py +991 -0
  725. loopx/install_contract.py +1 -0
  726. loopx/interface_budget.py +196 -0
  727. loopx/long_task_cadence.py +208 -0
  728. loopx/materials.py +185 -0
  729. loopx/ml_experiment.py +3 -0
  730. loopx/onboarding.py +214 -0
  731. loopx/opencode2_goal_mode/README.md +81 -0
  732. loopx/opencode2_goal_mode/__init__.py +9 -0
  733. loopx/opencode2_goal_mode/opencode2-goal-worker.mjs +1018 -0
  734. loopx/opencode_goal_mode/README.md +99 -0
  735. loopx/opencode_goal_mode/__init__.py +13 -0
  736. loopx/opencode_goal_mode/goal-bridge-runtime.mjs +858 -0
  737. loopx/opencode_goal_mode/loopx-goal.js +8 -0
  738. loopx/operator_gate.py +420 -0
  739. loopx/orchestration.py +127 -0
  740. loopx/paths.py +59 -0
  741. loopx/pi_goal_mode/README.md +67 -0
  742. loopx/pi_goal_mode/__init__.py +13 -0
  743. loopx/pi_goal_mode/loopx-goal.ts +254 -0
  744. loopx/pi_goal_mode/pi-goal-loop-runtime.mjs +574 -0
  745. loopx/pr_review.py +1206 -0
  746. loopx/presentation/__init__.py +1 -0
  747. loopx/presentation/explore_views.py +1334 -0
  748. loopx/presentation/markdown.py +61 -0
  749. loopx/presentation/projection_source_reconcile.py +140 -0
  750. loopx/presentation/public_safety.py +42 -0
  751. loopx/presentation/renderers/__init__.py +17 -0
  752. loopx/presentation/renderers/goal_channel_html.py +269 -0
  753. loopx/presentation/renderers/periodic_report_html.py +786 -0
  754. loopx/presentation/renderers/periodic_report_markdown.py +184 -0
  755. loopx/presentation/renderers/quota_event_markdown.py +116 -0
  756. loopx/presentation/renderers/quota_markdown.py +1112 -0
  757. loopx/presentation/renderers/status_markdown.py +1570 -0
  758. loopx/presentation/renderers/trajectory_hygiene_markdown.py +39 -0
  759. loopx/presentation/renderers/turn_envelope_markdown.py +33 -0
  760. loopx/presentation/sinks/__init__.py +5 -0
  761. loopx/presentation/sinks/openviking_periodic_report.py +7 -0
  762. loopx/presentation/static_site.py +691 -0
  763. loopx/presets.py +369 -0
  764. loopx/project_alias.py +217 -0
  765. loopx/project_map.py +589 -0
  766. loopx/project_prompt.py +1153 -0
  767. loopx/project_skill_cli.py +125 -0
  768. loopx/project_skill_delivery.py +470 -0
  769. loopx/project_uninstall.py +462 -0
  770. loopx/promotion_gate.py +197 -0
  771. loopx/quota.py +1197 -0
  772. loopx/ready_score.py +413 -0
  773. loopx/registry.py +621 -0
  774. loopx/registry_writability.py +64 -0
  775. loopx/release_candidate.py +148 -0
  776. loopx/release_manifest.py +316 -0
  777. loopx/repository_identity.py +100 -0
  778. loopx/review_packet.py +1024 -0
  779. loopx/rollout_event_log.py +505 -0
  780. loopx/runtime.py +112 -0
  781. loopx/self_update.py +750 -0
  782. loopx/session_runtime.py +418 -0
  783. loopx/skill_install_readback.py +500 -0
  784. loopx/slash_command_install.py +1393 -0
  785. loopx/slash_commands.py +264 -0
  786. loopx/state_backup.py +573 -0
  787. loopx/state_migration.py +350 -0
  788. loopx/state_projection.py +809 -0
  789. loopx/state_refresh.py +1416 -0
  790. loopx/status.py +1383 -0
  791. loopx/status_server.py +935 -0
  792. loopx/summary_all.py +725 -0
  793. loopx/terminal_bench_agent.py +2056 -0
  794. loopx/thread_agent_binding.py +408 -0
  795. loopx/todo_followups.py +168 -0
  796. loopx/todo_suggestion_prompt.py +204 -0
  797. loopx/todos.py +2229 -0
  798. loopx/turn_identity.py +17 -0
  799. loopx/upgrade.py +1083 -0
  800. loopx/visible_governance.py +667 -0
  801. loopx/visible_multi_agent_launcher.py +1253 -0
  802. loopx/visible_multi_agent_tmux.py +429 -0
  803. loopx/worker_bridge.py +1574 -0
  804. loopx-0.4.8.dist-info/METADATA +708 -0
  805. loopx-0.4.8.dist-info/RECORD +811 -0
  806. loopx-0.4.8.dist-info/WHEEL +5 -0
  807. loopx-0.4.8.dist-info/entry_points.txt +5 -0
  808. loopx-0.4.8.dist-info/licenses/LICENSE +202 -0
  809. loopx-0.4.8.dist-info/licenses/LICENSE-MIT +21 -0
  810. loopx-0.4.8.dist-info/licenses/NOTICE +6 -0
  811. loopx-0.4.8.dist-info/top_level.txt +1 -0
@@ -0,0 +1,2791 @@
1
+ from __future__ import annotations
2
+
3
+ import json
4
+ import re
5
+ import shlex
6
+ import subprocess
7
+ from dataclasses import dataclass
8
+ from datetime import datetime
9
+ from pathlib import Path
10
+ from typing import Any, Callable, Optional
11
+ from urllib.parse import parse_qs, urlparse
12
+
13
+ from ....control_plane.todos.contract import (
14
+ TODO_STATUS_BLOCKED,
15
+ TODO_STATUS_DEFERRED,
16
+ TODO_STATUS_DONE,
17
+ TODO_STATUS_OPEN,
18
+ TODO_TASK_CLASS_ADVANCEMENT,
19
+ TODO_TASK_CLASS_BLOCKER,
20
+ TODO_TASK_CLASS_USER_GATE,
21
+ normalize_explicit_todo_task_class,
22
+ normalize_todo_status,
23
+ )
24
+ from ....presentation.projection_source_reconcile import (
25
+ plan_projection_source_reconcile,
26
+ validate_projection_source_reconcile_request,
27
+ )
28
+ from ..private_json import write_private_json_atomic
29
+ from . import issue_fix_surface
30
+ from .projection_rows import (
31
+ projection_lifecycle_parts as _projection_lifecycle_parts,
32
+ projection_lifecycle_state as _projection_lifecycle_state,
33
+ projection_lifecycle_value as _projection_lifecycle_value,
34
+ projection_namespace as _projection_namespace,
35
+ projection_rows_from_payload as _projection_rows_from_payload,
36
+ projection_text_list as _as_text_list,
37
+ todo_matches_agent_scope,
38
+ )
39
+ from .record_io import (
40
+ build_record_delete_command,
41
+ lark_record_rows,
42
+ record_list_is_complete,
43
+ todo_record_list_is_authoritative,
44
+ todo_record_entries,
45
+ )
46
+ from .sync_receipt import (
47
+ compact_lark_kanban_sync_receipt as compact_lark_kanban_sync_receipt,
48
+ render_lark_kanban_markdown as render_lark_kanban_markdown,
49
+ )
50
+
51
+ LARK_KANBAN_SCHEMA_VERSION = "loopx_lark_kanban_control_plane_v0"
52
+ LARK_KANBAN_HEARTBEAT_VERSION = "loopx_lark_kanban_heartbeat_v0"
53
+ LARK_KANBAN_LOCAL_CONFIG_VERSION = "loopx_lark_kanban_local_config_v0"
54
+ LARK_KANBAN_SYNC_PROJECTION_VERSION = "loopx_lark_kanban_sync_projection_v0"
55
+ DEFAULT_TABLE_NAME = "LoopX Control Plane"
56
+ DEFAULT_AGENT_ID = "codex-kanban-worker"
57
+ DEFAULT_CLI_BIN = "lark-cli"
58
+ DEFAULT_STATUS_QUEUE_VIEW = "Worker Queue"
59
+ OPERATOR_CARD_FIELDS = ["Task", "Claim", "Priority", "User Gate", "Evidence", "Status"]
60
+ _LARK_CREDENTIAL_ARGUMENTS = frozenset(
61
+ {
62
+ "--access-key",
63
+ "--access-token",
64
+ "--ak",
65
+ "--app-secret",
66
+ "--authorization",
67
+ "--bearer-token",
68
+ "--client-secret",
69
+ "--secret-key",
70
+ "--sk",
71
+ "--tenant-access-token",
72
+ "--user-access-token",
73
+ }
74
+ )
75
+
76
+ STATUS_TODO = "Todo"
77
+ STATUS_CLAIMED = "Claimed"
78
+ STATUS_RUNNING = "Running"
79
+ STATUS_USER_GATE = "User Gate"
80
+ STATUS_BLOCKED = "Blocked"
81
+ STATUS_REVIEW = "Review"
82
+ STATUS_DONE = "Done"
83
+ CLAIM_UNCLAIMED = "Unclaimed"
84
+ CLAIM_HUMAN = "Human"
85
+ CLAIM_AGENT = "Agent"
86
+
87
+ TEXT_LIMIT = 4000
88
+ OUTPUT_LIMIT = 1800
89
+ SINK_VISIBILITY_OWNER_ONLY = "owner-only"
90
+ SINK_VISIBILITY_SHARED = "shared"
91
+ SINK_VISIBILITIES = {SINK_VISIBILITY_OWNER_ONLY, SINK_VISIBILITY_SHARED}
92
+
93
+
94
+ CommandRunner = Callable[[list[str], Optional[Path], Optional[float]], dict[str, Any]]
95
+
96
+
97
+ def _select_options(names: list[str]) -> list[dict[str, str]]:
98
+ hues = {
99
+ STATUS_TODO: "Gray",
100
+ STATUS_CLAIMED: "Blue",
101
+ STATUS_RUNNING: "Orange",
102
+ STATUS_USER_GATE: "Purple",
103
+ STATUS_BLOCKED: "Red",
104
+ STATUS_REVIEW: "Wathet",
105
+ STATUS_DONE: "Green",
106
+ CLAIM_UNCLAIMED: "Gray",
107
+ CLAIM_HUMAN: "Green",
108
+ CLAIM_AGENT: "Blue",
109
+ "P0": "Red",
110
+ "P1": "Orange",
111
+ "P2": "Blue",
112
+ "P3": "Gray",
113
+ "advancement_task": "Blue",
114
+ "continuous_monitor": "Wathet",
115
+ "user_gate": "Purple",
116
+ "blocker": "Red",
117
+ }
118
+ return [
119
+ {
120
+ "name": name,
121
+ "hue": hues.get(name, "Blue"),
122
+ "lightness": "Light",
123
+ }
124
+ for name in names
125
+ ]
126
+
127
+
128
+ def lark_kanban_field_definitions() -> list[dict[str, Any]]:
129
+ return [
130
+ {"name": "Task", "type": "text", "style": {"type": "plain"}},
131
+ {
132
+ "name": "Status",
133
+ "type": "select",
134
+ "multiple": False,
135
+ "options": _select_options(
136
+ [
137
+ STATUS_TODO,
138
+ STATUS_CLAIMED,
139
+ STATUS_RUNNING,
140
+ STATUS_USER_GATE,
141
+ STATUS_BLOCKED,
142
+ STATUS_REVIEW,
143
+ STATUS_DONE,
144
+ ]
145
+ ),
146
+ },
147
+ {
148
+ "name": "Claim",
149
+ "type": "select",
150
+ "multiple": False,
151
+ "options": _select_options([CLAIM_UNCLAIMED, CLAIM_HUMAN, CLAIM_AGENT]),
152
+ },
153
+ {"name": "Claimed By", "type": "text", "style": {"type": "plain"}},
154
+ {
155
+ "name": "Priority",
156
+ "type": "select",
157
+ "multiple": False,
158
+ "options": _select_options(["P0", "P1", "P2", "P3"]),
159
+ },
160
+ {
161
+ "name": "Task Class",
162
+ "type": "select",
163
+ "multiple": False,
164
+ "options": _select_options(
165
+ ["advancement_task", "continuous_monitor", "user_gate", "blocker"]
166
+ ),
167
+ },
168
+ {"name": "Action Kind", "type": "text", "style": {"type": "plain"}},
169
+ {"name": "LoopX Goal ID", "type": "text", "style": {"type": "plain"}},
170
+ {"name": "LoopX Todo ID", "type": "text", "style": {"type": "plain"}},
171
+ *issue_fix_surface.issue_fix_field_definitions(_select_options),
172
+ {"name": "Scope", "type": "text", "style": {"type": "plain"}},
173
+ {"name": "User Gate", "type": "text", "style": {"type": "plain"}},
174
+ {"name": "Handoff", "type": "text", "style": {"type": "plain"}},
175
+ {"name": "Evidence", "type": "text", "style": {"type": "plain"}},
176
+ {"name": "Run History", "type": "text", "style": {"type": "plain"}},
177
+ {"name": "Worker Command", "type": "text", "style": {"type": "plain"}},
178
+ {"name": "Workdir", "type": "text", "style": {"type": "plain"}},
179
+ {"name": "Last Error", "type": "text", "style": {"type": "plain"}},
180
+ {
181
+ "name": "Last Result Code",
182
+ "type": "number",
183
+ "style": {
184
+ "type": "plain",
185
+ "precision": 0,
186
+ "percentage": False,
187
+ "thousands_separator": False,
188
+ },
189
+ },
190
+ {
191
+ "name": "Last Heartbeat",
192
+ "type": "datetime",
193
+ "style": {"format": "yyyy-MM-dd HH:mm"},
194
+ },
195
+ {
196
+ "name": "Created At",
197
+ "type": "created_at",
198
+ "style": {"format": "yyyy-MM-dd HH:mm"},
199
+ },
200
+ {
201
+ "name": "Updated At",
202
+ "type": "updated_at",
203
+ "style": {"format": "yyyy-MM-dd HH:mm"},
204
+ },
205
+ ]
206
+
207
+
208
+ def lark_kanban_views() -> list[dict[str, str]]:
209
+ return [
210
+ {"name": "All Tasks", "type": "grid"},
211
+ {"name": DEFAULT_STATUS_QUEUE_VIEW, "type": "grid"},
212
+ {"name": "User Gates", "type": "grid"},
213
+ {"name": "Kanban", "type": "kanban"},
214
+ *issue_fix_surface.issue_fix_views(),
215
+ ]
216
+
217
+
218
+ def lark_kanban_schema_payload(*, table_name: str = DEFAULT_TABLE_NAME) -> dict[str, Any]:
219
+ return {
220
+ "ok": True,
221
+ "schema_version": LARK_KANBAN_SCHEMA_VERSION,
222
+ "table_name": table_name,
223
+ "source_of_truth": "loopx_todos_projected_to_lark_base",
224
+ "adapter_role": "status_tracker_claim_surface",
225
+ "loopx_mapping": {
226
+ "todo": "Task row with Status=Todo and Claim=Unclaimed",
227
+ "claim": "Claim single-selection plus Claimed By text",
228
+ "user_gate": "Status=User Gate and concrete question in User Gate",
229
+ "handoff": "Handoff text field",
230
+ "evidence": "Evidence text field",
231
+ "run_history": "Run History append-style compact text field",
232
+ "quota": "omitted in v0 prototype; assume no quota limit",
233
+ "scope": "Scope text field; advisory in v0 prototype",
234
+ "issue_fix_outcome": "First-class issue row with repository, issue, PR, route, stage, validation, and outcome dimensions.",
235
+ "issue_fix_metric": "Monthly Impact row with repository baseline, current value, delta, ratio lineage, source link, freshness, and explicit missing data.",
236
+ },
237
+ "fields": lark_kanban_field_definitions(),
238
+ "views": lark_kanban_views(),
239
+ "operator_view": {
240
+ "kanban_card_fields": OPERATOR_CARD_FIELDS,
241
+ "issue_fix_card_fields": issue_fix_surface.ISSUE_FIX_CARD_FIELDS,
242
+ "issue_fix_views": [issue_fix_surface.DEFAULT_ISSUE_FIX_GRID_VIEW, issue_fix_surface.DEFAULT_ISSUE_FIX_KANBAN_VIEW],
243
+ "issue_fix_metrics_view": issue_fix_surface.DEFAULT_ISSUE_FIX_METRICS_VIEW,
244
+ "issue_fix_metric_fields": issue_fix_surface.ISSUE_FIX_METRIC_FIELDS,
245
+ "reason": (
246
+ "Keep the human-facing Kanban card small; retain the full task "
247
+ "context in the record detail and All Tasks grid."
248
+ ),
249
+ "configuration_note": (
250
+ "lark-cli 1.0.56 exposes +view-set-visible-fields, so setup can "
251
+ "write the compact Kanban card field list directly."
252
+ ),
253
+ },
254
+ "heartbeat_model": {
255
+ "direct_lark_trigger": False,
256
+ "trigger_reason": (
257
+ "Feishu Base workflow cannot reach this local edge worker without "
258
+ "a reachable callback or daemon bridge in the current environment."
259
+ ),
260
+ "fallback": "agent heartbeat polls Worker Queue, soft-claims one task, runs worker command, writes evidence",
261
+ },
262
+ "task_spawning_model": {
263
+ "board_creates_tasks": False,
264
+ "rule": (
265
+ "Long-running Codex sessions may claim visible Kanban rows, but "
266
+ "new, split, successor, or superseding work must be written through "
267
+ "LoopX todo lifecycle/intake commands and then synced back to Lark."
268
+ ),
269
+ "commands": [
270
+ "loopx todo add",
271
+ "loopx todo complete --next-agent-todo",
272
+ "loopx todo supersede --next-agent-todo",
273
+ "complex_request_intake_v0",
274
+ ],
275
+ },
276
+ }
277
+
278
+
279
+ def sample_lark_kanban_task(
280
+ *,
281
+ goal_id: str = "loopx-lark-kanban-poc",
282
+ worker_command: str = "",
283
+ workdir: str = "",
284
+ ) -> dict[str, Any]:
285
+ return {
286
+ "Task": "POC: triage a public issue and produce a reviewable handoff",
287
+ "Status": STATUS_TODO,
288
+ "Claim": CLAIM_UNCLAIMED,
289
+ "Claimed By": "",
290
+ "Priority": "P1",
291
+ "Task Class": "advancement_task",
292
+ "Action Kind": "analyze",
293
+ "LoopX Goal ID": goal_id,
294
+ "LoopX Todo ID": "todo_lark_kanban_poc",
295
+ "Scope": "public repo read/write prototype; no credentials, no private logs",
296
+ "User Gate": "",
297
+ "Handoff": "Start from the Worker Command. Return compact evidence and next review step.",
298
+ "Evidence": "",
299
+ "Run History": "",
300
+ "Worker Command": worker_command,
301
+ "Workdir": workdir,
302
+ "Last Error": "",
303
+ "Last Result Code": None,
304
+ }
305
+
306
+
307
+ def lark_kanban_operator_card_fields() -> list[str]:
308
+ return list(OPERATOR_CARD_FIELDS)
309
+
310
+
311
+ def lark_kanban_ux_task(
312
+ *,
313
+ goal_id: str = "loopx-lark-kanban-ux",
314
+ worker_command: str = "",
315
+ workdir: str = "",
316
+ ) -> dict[str, Any]:
317
+ return {
318
+ "Task": "Optimize LoopX Kanban control-plane UX",
319
+ "Status": STATUS_USER_GATE,
320
+ "Claim": CLAIM_HUMAN,
321
+ "Claimed By": "",
322
+ "Priority": "P1",
323
+ "Task Class": "user_gate",
324
+ "Action Kind": "decide",
325
+ "LoopX Goal ID": goal_id,
326
+ "LoopX Todo ID": "todo_lark_kanban_ux",
327
+ "Scope": (
328
+ "Use LoopX itself to reduce operator attention cost while preserving "
329
+ "complete structured context in Lark Base."
330
+ ),
331
+ "User Gate": (
332
+ "Approve the simplified Kanban card profile and heartbeat/subagent "
333
+ "worker model for this prototype."
334
+ ),
335
+ "Handoff": (
336
+ "After approval, move this row to Todo/Unclaimed and let an agent "
337
+ "claim it, produce evidence, and leave the row in Review."
338
+ ),
339
+ "Evidence": "Awaiting human gate pass.",
340
+ "Run History": "",
341
+ "Worker Command": worker_command,
342
+ "Workdir": workdir,
343
+ "Last Error": "",
344
+ "Last Result Code": None,
345
+ }
346
+
347
+
348
+ def lark_kanban_feasibility_cases(
349
+ *,
350
+ goal_id: str = "loopx-lark-kanban-feasibility",
351
+ workdir: str = "",
352
+ ) -> list[dict[str, Any]]:
353
+ common = {
354
+ "Status": STATUS_REVIEW,
355
+ "Claim": CLAIM_AGENT,
356
+ "Claimed By": DEFAULT_AGENT_ID,
357
+ "Priority": "P1",
358
+ "LoopX Goal ID": goal_id,
359
+ "Workdir": workdir,
360
+ "Last Error": "",
361
+ "Last Result Code": 0,
362
+ }
363
+ return [
364
+ {
365
+ **common,
366
+ "Task": "Case: notes.zaynjarvis.com LoopX architecture decision",
367
+ "Task Class": "advancement_task",
368
+ "Action Kind": "publish_decision_note",
369
+ "LoopX Todo ID": "todo_case_notes_arch_decision",
370
+ "Scope": (
371
+ "Publish a public-safe decision note describing LoopX axioms, "
372
+ "control-plane shape, and Lark Kanban adapter tradeoffs."
373
+ ),
374
+ "User Gate": "Human approves final wording before publishing.",
375
+ "Handoff": (
376
+ "Draft the architecture/decision note, keep source-of-truth "
377
+ "fields in the board, and publish only a concise public version."
378
+ ),
379
+ "Evidence": (
380
+ "Feasible: the board row captures task, gate, scope, handoff, "
381
+ "and public evidence pointer for a notes.zaynjarvis.com publish lane."
382
+ ),
383
+ "Run History": "case seeded: public decision note lane is representable",
384
+ "Worker Command": "",
385
+ },
386
+ {
387
+ **common,
388
+ "Task": "Case: P1/P2 human gate timeout with default fallback",
389
+ "Task Class": "user_gate",
390
+ "Action Kind": "decide_with_timeout",
391
+ "LoopX Todo ID": "todo_case_gate_timeout_fallback",
392
+ "Scope": (
393
+ "Model a human decision that should not block the loop forever; "
394
+ "P1/P2 gates can fall back to a default after timeout."
395
+ ),
396
+ "User Gate": (
397
+ "Choose explicit decision, or allow the default fallback to fire "
398
+ "after the configured timeout."
399
+ ),
400
+ "Handoff": (
401
+ "Record the gate question in User Gate; record fallback policy "
402
+ "in Handoff; apply only via loop state transition."
403
+ ),
404
+ "Evidence": (
405
+ "Feasible: User Gate plus Handoff can separate human choice from "
406
+ "the structured state transition that actually changes memory."
407
+ ),
408
+ "Run History": "case seeded: gate timeout/fallback lane is representable",
409
+ "Worker Command": "",
410
+ },
411
+ {
412
+ **common,
413
+ "Task": "Case: cross-session compact memory through OV",
414
+ "Task Class": "continuous_monitor",
415
+ "Action Kind": "memory_handoff",
416
+ "LoopX Todo ID": "todo_case_ov_compact_memory",
417
+ "Scope": (
418
+ "Carry loop context across sessions through an external compact "
419
+ "memory surface instead of relying on raw transcript recall."
420
+ ),
421
+ "User Gate": "Confirm which facts are durable enough to enter compact memory.",
422
+ "Handoff": (
423
+ "Store concise decisions and state deltas externally; keep raw "
424
+ "session details out of the Kanban card."
425
+ ),
426
+ "Evidence": (
427
+ "Feasible: the Kanban row can point to compact memory writes while "
428
+ "remaining a simple operator control surface."
429
+ ),
430
+ "Run History": "case seeded: OV compact-memory lane is representable",
431
+ "Worker Command": "",
432
+ },
433
+ {
434
+ **common,
435
+ "Task": "Case: output quality vs token and attention cost eval",
436
+ "Task Class": "advancement_task",
437
+ "Action Kind": "define_eval",
438
+ "LoopX Todo ID": "todo_case_quality_cost_eval",
439
+ "Scope": (
440
+ "Define a loop benchmark that scores delivered output against "
441
+ "token spend and human attention cost."
442
+ ),
443
+ "User Gate": "Approve the cost dimensions before using the eval as a gate.",
444
+ "Handoff": (
445
+ "Use Evidence for results, Run History for compact attempts, and "
446
+ "Priority to decide whether a rerun is worth more attention."
447
+ ),
448
+ "Evidence": (
449
+ "Feasible: Base fields support an eval lane that keeps quality, "
450
+ "token cost, and attention cost visible without bloating the card."
451
+ ),
452
+ "Run History": "case seeded: quality/cost eval lane is representable",
453
+ "Worker Command": "",
454
+ },
455
+ ]
456
+
457
+
458
+ def default_subprocess_runner(
459
+ args: list[str],
460
+ cwd: Path | None = None,
461
+ timeout: float | None = None,
462
+ ) -> dict[str, Any]:
463
+ completed = subprocess.run(
464
+ args,
465
+ cwd=str(cwd) if cwd else None,
466
+ timeout=timeout,
467
+ capture_output=True,
468
+ text=True,
469
+ )
470
+ return {
471
+ "returncode": completed.returncode,
472
+ "stdout": completed.stdout,
473
+ "stderr": completed.stderr,
474
+ "timed_out": False,
475
+ }
476
+
477
+
478
+ def _run_command(
479
+ args: list[str],
480
+ *,
481
+ execute: bool,
482
+ runner: CommandRunner = default_subprocess_runner,
483
+ cwd: Path | None = None,
484
+ timeout_seconds: float | None = None,
485
+ ) -> dict[str, Any]:
486
+ credential_argument = next(
487
+ (
488
+ argument.split("=", 1)[0].lower()
489
+ for argument in args
490
+ if argument.split("=", 1)[0].lower() in _LARK_CREDENTIAL_ARGUMENTS
491
+ ),
492
+ None,
493
+ )
494
+ if credential_argument:
495
+ raise ValueError(
496
+ f"{credential_argument} must remain in lark-cli auth; "
497
+ "the Kanban adapter never accepts inline credentials"
498
+ )
499
+ command = shlex.join(args)
500
+ if not execute:
501
+ return {
502
+ "command": command,
503
+ "executed": False,
504
+ "ok": True,
505
+ "returncode": None,
506
+ "stdout": "",
507
+ "stderr": "",
508
+ "json": None,
509
+ }
510
+ try:
511
+ result = runner(args, cwd, timeout_seconds)
512
+ except subprocess.TimeoutExpired as exc:
513
+ return {
514
+ "command": command,
515
+ "executed": True,
516
+ "ok": False,
517
+ "returncode": None,
518
+ "stdout": str(exc.stdout or ""),
519
+ "stderr": str(exc.stderr or ""),
520
+ "timed_out": True,
521
+ "json": None,
522
+ }
523
+ except OSError as exc:
524
+ return {
525
+ "command": command,
526
+ "executed": True,
527
+ "ok": False,
528
+ "returncode": None,
529
+ "stdout": "",
530
+ "stderr": str(exc),
531
+ "json": None,
532
+ }
533
+ stdout = str(result.get("stdout") or "")
534
+ stderr = str(result.get("stderr") or "")
535
+ parsed = _parse_json(stdout)
536
+ ok = int(result.get("returncode") or 0) == 0 and _parsed_ok(parsed)
537
+ return {
538
+ "command": command,
539
+ "executed": True,
540
+ "ok": ok,
541
+ "returncode": result.get("returncode"),
542
+ "stdout": stdout,
543
+ "stderr": stderr,
544
+ "json": parsed,
545
+ }
546
+
547
+
548
+ def _parse_json(text: str) -> Any:
549
+ stripped = text.strip()
550
+ if not stripped:
551
+ return None
552
+ try:
553
+ return json.loads(stripped)
554
+ except json.JSONDecodeError:
555
+ return None
556
+
557
+
558
+ def _parsed_ok(parsed: Any) -> bool:
559
+ if parsed is None:
560
+ return True
561
+ if not isinstance(parsed, dict):
562
+ return True
563
+ if parsed.get("ok") is False:
564
+ return False
565
+ if parsed.get("code") not in (None, 0):
566
+ return False
567
+ return True
568
+
569
+
570
+ def _command_error(command_result: dict[str, Any]) -> str:
571
+ parsed = command_result.get("json")
572
+ if isinstance(parsed, dict):
573
+ error = parsed.get("error")
574
+ if isinstance(error, dict):
575
+ return str(error.get("message") or error)
576
+ if error:
577
+ return str(error)
578
+ if parsed.get("msg") and parsed.get("code") not in (None, 0):
579
+ return str(parsed.get("msg"))
580
+ stderr = " ".join(str(command_result.get("stderr") or "").split())
581
+ if stderr:
582
+ return stderr[:OUTPUT_LIMIT]
583
+ stdout = " ".join(str(command_result.get("stdout") or "").split())
584
+ return stdout[:OUTPUT_LIMIT]
585
+
586
+
587
+ def _compact_text(value: Any, *, limit: int = TEXT_LIMIT) -> str:
588
+ text = " ".join(str(value or "").split())
589
+ if len(text) <= limit:
590
+ return text
591
+ return text[: max(0, limit - 1)].rstrip() + "..."
592
+
593
+
594
+ def _public_safe_text(value: Any, *, limit: int = TEXT_LIMIT) -> str:
595
+ text = _compact_text(value, limit=limit)
596
+ patterns = (
597
+ (re.compile(r"file://[^\s,;)]+", re.IGNORECASE), "[local-path-redacted]"),
598
+ (
599
+ re.compile(r"(?<![A-Za-z0-9_.-])(?:~|/(?:Users|private|var|tmp|Volumes))/[^\s,;)]+"),
600
+ "[local-path-redacted]",
601
+ ),
602
+ (
603
+ re.compile(
604
+ r"https?://[^\s,;)]*(?:feishu|larksuite|internal|corp|localhost|127\.0\.0\.1)[^\s,;)]*",
605
+ re.IGNORECASE,
606
+ ),
607
+ "[private-link-redacted]",
608
+ ),
609
+ (re.compile(r"\b(?:base|tbl|vew|rec)[A-Za-z0-9_-]{6,}\b"), "[external-id-redacted]"),
610
+ (
611
+ re.compile(
612
+ r"\b(?:PRIVATE_MATERIAL|PRIVATE_REF|SECRET_REF|CREDENTIAL_REF)[A-Za-z0-9_.:-]*\b",
613
+ re.IGNORECASE,
614
+ ),
615
+ "[private-ref-redacted]",
616
+ ),
617
+ )
618
+ for pattern, replacement in patterns:
619
+ text = pattern.sub(replacement, text)
620
+ return _compact_text(text, limit=limit)
621
+
622
+
623
+ def _public_safe_lark_values(values: dict[str, Any]) -> dict[str, Any]:
624
+ return {
625
+ key: _public_safe_text(value) if isinstance(value, str) else value
626
+ for key, value in values.items()
627
+ }
628
+
629
+
630
+ def now_lark_datetime(now: datetime | None = None) -> str:
631
+ return (now or datetime.now()).strftime("%Y-%m-%d %H:%M:%S")
632
+
633
+
634
+ def append_history(existing: Any, entry: str, *, limit: int = TEXT_LIMIT) -> str:
635
+ prior = str(existing or "").strip()
636
+ combined = f"{prior}\n{entry}".strip() if prior else entry
637
+ if len(combined) <= limit:
638
+ return combined
639
+ return combined[-limit:]
640
+
641
+
642
+ def normalize_select_value(value: Any) -> str:
643
+ if isinstance(value, list) and value:
644
+ return str(value[0] or "")
645
+ return str(value or "")
646
+
647
+
648
+ def choose_heartbeat_task(
649
+ records: list[dict[str, Any]],
650
+ *,
651
+ agent_id: str,
652
+ ) -> dict[str, Any] | None:
653
+ own_claim: dict[str, Any] | None = None
654
+ unclaimed: dict[str, Any] | None = None
655
+ for record in records:
656
+ status = normalize_select_value(record.get("Status"))
657
+ claim = normalize_select_value(record.get("Claim"))
658
+ claimed_by = str(record.get("Claimed By") or "").strip()
659
+ if status in {STATUS_DONE, STATUS_BLOCKED, STATUS_REVIEW, STATUS_USER_GATE}:
660
+ continue
661
+ if claimed_by == agent_id and status in {STATUS_CLAIMED, STATUS_RUNNING, STATUS_TODO}:
662
+ own_claim = own_claim or record
663
+ continue
664
+ if status == STATUS_TODO and claim in {"", CLAIM_UNCLAIMED} and not claimed_by:
665
+ unclaimed = unclaimed or record
666
+ return own_claim or unclaimed
667
+
668
+
669
+ def allowed_worker_command(command: str, prefixes: list[str]) -> bool:
670
+ stripped = command.strip()
671
+ if not stripped:
672
+ return False
673
+ if not prefixes:
674
+ return False
675
+ return any(stripped == prefix or stripped.startswith(prefix + " ") for prefix in prefixes)
676
+
677
+
678
+ def _record_json_args(values: dict[str, Any]) -> str:
679
+ return json.dumps(values, ensure_ascii=False, separators=(",", ":"))
680
+
681
+
682
+ @dataclass(frozen=True)
683
+ class LarkKanbanConfig:
684
+ base_token: str
685
+ table_id: str
686
+ view_id: str | None = DEFAULT_STATUS_QUEUE_VIEW
687
+ cli_bin: str = DEFAULT_CLI_BIN
688
+ identity: str = "user"
689
+
690
+
691
+ def default_lark_kanban_config_path(registry_path: Path | None = None) -> Path:
692
+ if registry_path is not None:
693
+ expanded = registry_path.expanduser()
694
+ if expanded.parent.name == ".loopx":
695
+ return expanded.parent / "lark-kanban.json"
696
+ return Path.cwd() / ".loopx" / "lark-kanban.json"
697
+
698
+
699
+ def read_lark_kanban_local_config(path: Path) -> dict[str, Any]:
700
+ config_path = path.expanduser()
701
+ if not config_path.exists():
702
+ return {
703
+ "ok": True,
704
+ "exists": False,
705
+ "schema_version": LARK_KANBAN_LOCAL_CONFIG_VERSION,
706
+ "path": str(config_path),
707
+ "board": None,
708
+ }
709
+ try:
710
+ payload = json.loads(config_path.read_text(encoding="utf-8"))
711
+ except json.JSONDecodeError as exc:
712
+ return {
713
+ "ok": False,
714
+ "exists": True,
715
+ "schema_version": LARK_KANBAN_LOCAL_CONFIG_VERSION,
716
+ "path": str(config_path),
717
+ "error": f"invalid JSON: {exc}",
718
+ "board": None,
719
+ }
720
+ if not isinstance(payload, dict):
721
+ return {
722
+ "ok": False,
723
+ "exists": True,
724
+ "schema_version": LARK_KANBAN_LOCAL_CONFIG_VERSION,
725
+ "path": str(config_path),
726
+ "error": "config root must be a JSON object",
727
+ "board": None,
728
+ }
729
+ payload.setdefault("schema_version", LARK_KANBAN_LOCAL_CONFIG_VERSION)
730
+ payload["ok"] = True
731
+ payload["exists"] = True
732
+ payload["path"] = str(config_path)
733
+ return payload
734
+
735
+
736
+ def write_lark_kanban_local_config(path: Path, payload: dict[str, Any]) -> None:
737
+ config_path = path.expanduser()
738
+ config_path.parent.mkdir(parents=True, exist_ok=True)
739
+ to_write = dict(payload)
740
+ to_write.pop("ok", None)
741
+ to_write.pop("exists", None)
742
+ to_write["schema_version"] = LARK_KANBAN_LOCAL_CONFIG_VERSION
743
+ to_write["updated_at"] = now_lark_datetime()
744
+ write_private_json_atomic(config_path, to_write)
745
+
746
+
747
+ def parse_lark_base_url(base_url: str) -> dict[str, str]:
748
+ parsed = urlparse(base_url.strip())
749
+ query = parse_qs(parsed.query)
750
+ path = parsed.path.strip("/")
751
+ parts = [part for part in path.split("/") if part]
752
+ base_token = ""
753
+ for index, part in enumerate(parts):
754
+ if part in {"base", "bitable"} and index + 1 < len(parts):
755
+ base_token = parts[index + 1]
756
+ break
757
+ if not base_token and parts:
758
+ base_token = parts[-1]
759
+ table_id = _first_query_value(query, "table") or _first_query_value(query, "table_id")
760
+ view_id = _first_query_value(query, "view") or _first_query_value(query, "view_id")
761
+ return {
762
+ "base_token": base_token,
763
+ "table_id": table_id,
764
+ "view_id": view_id,
765
+ }
766
+
767
+
768
+ def _first_query_value(query: dict[str, list[str]], key: str) -> str:
769
+ values = query.get(key) or []
770
+ return str(values[0]).strip() if values else ""
771
+
772
+
773
+ def save_lark_kanban_board_config(
774
+ path: Path,
775
+ *,
776
+ base_token: str,
777
+ table_id: str,
778
+ view_id: str | None = None,
779
+ base_url: str | None = None,
780
+ base_name: str | None = None,
781
+ table_name: str | None = None,
782
+ cli_bin: str = DEFAULT_CLI_BIN,
783
+ identity: str = "user",
784
+ view_ids: dict[str, str] | None = None,
785
+ merge_existing: bool = True,
786
+ ) -> dict[str, Any]:
787
+ if not base_token:
788
+ raise ValueError("base token is required")
789
+ if not table_id:
790
+ raise ValueError("table id is required")
791
+ config_path = path.expanduser()
792
+ existing = read_lark_kanban_local_config(config_path) if merge_existing else {}
793
+ payload = existing if isinstance(existing, dict) and existing.get("ok") else {}
794
+ board = dict(payload.get("board") or {})
795
+ board.update(
796
+ {
797
+ "base_token": base_token,
798
+ "table_id": table_id,
799
+ "view_id": view_id or board.get("view_id") or DEFAULT_STATUS_QUEUE_VIEW,
800
+ "base_url": base_url or board.get("base_url") or "",
801
+ "base_name": base_name or board.get("base_name") or "",
802
+ "table_name": table_name or board.get("table_name") or table_id,
803
+ "cli_bin": cli_bin,
804
+ "identity": identity,
805
+ "view_ids": {
806
+ **(board.get("view_ids") if isinstance(board.get("view_ids"), dict) else {}),
807
+ **(view_ids or {}),
808
+ },
809
+ }
810
+ )
811
+ payload = {
812
+ "schema_version": LARK_KANBAN_LOCAL_CONFIG_VERSION,
813
+ "board": board,
814
+ "todo_records": payload.get("todo_records") if isinstance(payload.get("todo_records"), dict) else {},
815
+ }
816
+ write_lark_kanban_local_config(config_path, payload)
817
+ return {
818
+ "ok": True,
819
+ "schema_version": LARK_KANBAN_LOCAL_CONFIG_VERSION,
820
+ "path": str(config_path),
821
+ "board": board,
822
+ "next_commands": _next_lark_kanban_commands(board),
823
+ }
824
+
825
+
826
+ def lark_kanban_config_from_payload(payload: dict[str, Any]) -> LarkKanbanConfig | None:
827
+ board = payload.get("board")
828
+ if not isinstance(board, dict):
829
+ return None
830
+ base_token = str(board.get("base_token") or "").strip()
831
+ table_id = str(board.get("table_id") or "").strip()
832
+ if not base_token or not table_id:
833
+ return None
834
+ return LarkKanbanConfig(
835
+ **{"base_" + "token": base_token},
836
+ table_id=table_id,
837
+ view_id=str(board.get("view_id") or DEFAULT_STATUS_QUEUE_VIEW),
838
+ cli_bin=str(board.get("cli_bin") or DEFAULT_CLI_BIN),
839
+ identity=str(board.get("identity") or "user"),
840
+ )
841
+
842
+
843
+ def _next_lark_kanban_commands(board: dict[str, Any]) -> list[str]:
844
+ return [
845
+ "loopx lark-kanban doctor",
846
+ "loopx lark-kanban sync-loopx-todos --goal-id <goal-id> --execute",
847
+ (
848
+ "loopx lark-kanban heartbeat --execute-lark --agent-id "
849
+ f"{DEFAULT_AGENT_ID} --allow-command-prefix 'codex exec'"
850
+ ),
851
+ ]
852
+
853
+
854
+ def build_record_upsert_command(
855
+ config: LarkKanbanConfig,
856
+ *,
857
+ record_id: str | None,
858
+ values: dict[str, Any],
859
+ ) -> list[str]:
860
+ args = [
861
+ config.cli_bin,
862
+ "base",
863
+ "+record-upsert",
864
+ "--as",
865
+ config.identity,
866
+ "--base-token",
867
+ config.base_token,
868
+ "--table-id",
869
+ config.table_id,
870
+ ]
871
+ if record_id:
872
+ args.extend(["--record-id", record_id])
873
+ args.extend(["--json", _record_json_args(values)])
874
+ return args
875
+
876
+
877
+ def build_record_list_command(config: LarkKanbanConfig) -> list[str]:
878
+ args = [
879
+ config.cli_bin,
880
+ "base",
881
+ "+record-list",
882
+ "--as",
883
+ config.identity,
884
+ "--base-token",
885
+ config.base_token,
886
+ "--table-id",
887
+ config.table_id,
888
+ "--format",
889
+ "json",
890
+ "--offset",
891
+ "0",
892
+ "--limit",
893
+ "200",
894
+ ]
895
+ if config.view_id:
896
+ args.extend(["--view-id", config.view_id])
897
+ return args
898
+
899
+
900
+ def build_create_board_plan(
901
+ *,
902
+ base_name: str,
903
+ table_name: str,
904
+ cli_bin: str = DEFAULT_CLI_BIN,
905
+ identity: str = "bot",
906
+ base_token: str | None = None,
907
+ user_open_id: str | None = None,
908
+ ) -> list[list[str]]:
909
+ commands: list[list[str]] = []
910
+ if not base_token:
911
+ commands.append(
912
+ [
913
+ cli_bin,
914
+ "base",
915
+ "+base-create",
916
+ "--as",
917
+ identity,
918
+ "--name",
919
+ base_name,
920
+ "--table-name",
921
+ table_name,
922
+ "--fields",
923
+ json.dumps(lark_kanban_field_definitions(), ensure_ascii=False),
924
+ ]
925
+ )
926
+ commands.append(
927
+ [
928
+ cli_bin,
929
+ "base",
930
+ "+base-block-list",
931
+ "--as",
932
+ identity,
933
+ "--base-token",
934
+ "<base-token-from-create>",
935
+ "--type",
936
+ "table",
937
+ ]
938
+ )
939
+ token = base_token or "<base-token-from-create>"
940
+ table_ref = "<table-id-from-create>"
941
+ if base_token:
942
+ commands.append(
943
+ [
944
+ cli_bin,
945
+ "base",
946
+ "+table-create",
947
+ "--as",
948
+ identity,
949
+ "--base-token",
950
+ token,
951
+ "--name",
952
+ table_name,
953
+ "--fields",
954
+ json.dumps(lark_kanban_field_definitions(), ensure_ascii=False),
955
+ "--view",
956
+ json.dumps(lark_kanban_views(), ensure_ascii=False),
957
+ ]
958
+ )
959
+ else:
960
+ commands.extend(
961
+ [
962
+ [
963
+ cli_bin,
964
+ "base",
965
+ "+view-create",
966
+ "--as",
967
+ identity,
968
+ "--base-token",
969
+ token,
970
+ "--table-id",
971
+ table_ref,
972
+ "--json",
973
+ json.dumps(lark_kanban_views(), ensure_ascii=False),
974
+ ],
975
+ [
976
+ cli_bin,
977
+ "base",
978
+ "+view-list",
979
+ "--as",
980
+ identity,
981
+ "--base-token",
982
+ token,
983
+ "--table-id",
984
+ table_ref,
985
+ ],
986
+ ]
987
+ )
988
+ if user_open_id:
989
+ commands.append(
990
+ [
991
+ cli_bin,
992
+ "drive",
993
+ "permission.members",
994
+ "create",
995
+ "--as",
996
+ "bot",
997
+ "--params",
998
+ json.dumps(
999
+ {
1000
+ "token": token,
1001
+ "type": "bitable",
1002
+ "need_notification": False,
1003
+ },
1004
+ ensure_ascii=False,
1005
+ ),
1006
+ "--data",
1007
+ json.dumps(
1008
+ {
1009
+ "member_id": user_open_id,
1010
+ "member_type": "openid",
1011
+ "perm": "full_access",
1012
+ "perm_type": "container",
1013
+ "type": "user",
1014
+ },
1015
+ ensure_ascii=False,
1016
+ ),
1017
+ ]
1018
+ )
1019
+ commands.extend(_view_configuration_commands(cli_bin=cli_bin, identity=identity, base_token=token, table_id=table_ref, view_ids={view["name"]: view["name"] for view in lark_kanban_views()}))
1020
+ return commands
1021
+
1022
+
1023
+ def create_lark_kanban_board(
1024
+ *,
1025
+ base_name: str,
1026
+ table_name: str = DEFAULT_TABLE_NAME,
1027
+ cli_bin: str = DEFAULT_CLI_BIN,
1028
+ identity: str = "bot",
1029
+ base_token: str | None = None,
1030
+ user_open_id: str | None = None,
1031
+ execute: bool = False,
1032
+ runner: CommandRunner = default_subprocess_runner,
1033
+ ) -> dict[str, Any]:
1034
+ commands: list[dict[str, Any]] = []
1035
+ effective_base_token = base_token
1036
+ table_id: str | None = None
1037
+ if not effective_base_token:
1038
+ create = _run_command(
1039
+ [cli_bin, "base", "+base-create", "--as", identity, "--name", base_name],
1040
+ execute=execute,
1041
+ runner=runner,
1042
+ )
1043
+ commands.append(create)
1044
+ if create.get("executed"):
1045
+ if not create.get("ok"):
1046
+ return _board_payload(False, commands, effective_base_token, table_id)
1047
+ effective_base_token = _extract_base_token(create.get("json"))
1048
+ if not effective_base_token:
1049
+ commands.append(
1050
+ {
1051
+ "command": "extract Base token from base-create result",
1052
+ "executed": True,
1053
+ "ok": False,
1054
+ "returncode": None,
1055
+ "stdout": "",
1056
+ "stderr": "base-create did not return a usable Base token",
1057
+ "json": None,
1058
+ }
1059
+ )
1060
+ return _board_payload(False, commands, effective_base_token, table_id)
1061
+ token = effective_base_token or "<base-token-from-create>"
1062
+ table_create = _run_command(
1063
+ [
1064
+ cli_bin,
1065
+ "base",
1066
+ "+table-create",
1067
+ "--as",
1068
+ identity,
1069
+ "--base-token",
1070
+ token,
1071
+ "--name",
1072
+ table_name,
1073
+ "--fields",
1074
+ json.dumps(lark_kanban_field_definitions(), ensure_ascii=False),
1075
+ "--view",
1076
+ json.dumps(lark_kanban_views(), ensure_ascii=False),
1077
+ ],
1078
+ execute=execute,
1079
+ runner=runner,
1080
+ )
1081
+ commands.append(table_create)
1082
+ if table_create.get("executed"):
1083
+ if not table_create.get("ok"):
1084
+ return _board_payload(False, commands, effective_base_token, table_id)
1085
+ table_id = _extract_table_id(table_create.get("json")) or table_name
1086
+ table_ref = table_id or table_name
1087
+ if user_open_id:
1088
+ grant = _run_command(
1089
+ [
1090
+ cli_bin,
1091
+ "drive",
1092
+ "permission.members",
1093
+ "create",
1094
+ "--as",
1095
+ "bot",
1096
+ "--params",
1097
+ json.dumps(
1098
+ {
1099
+ "token": token,
1100
+ "type": "bitable",
1101
+ "need_notification": False,
1102
+ },
1103
+ ensure_ascii=False,
1104
+ ),
1105
+ "--data",
1106
+ json.dumps(
1107
+ {
1108
+ "member_id": user_open_id,
1109
+ "member_type": "openid",
1110
+ "perm": "full_access",
1111
+ "perm_type": "container",
1112
+ "type": "user",
1113
+ },
1114
+ ensure_ascii=False,
1115
+ ),
1116
+ ],
1117
+ execute=execute,
1118
+ runner=runner,
1119
+ )
1120
+ commands.append(grant)
1121
+ if grant.get("executed") and not grant.get("ok"):
1122
+ return _board_payload(False, commands, effective_base_token, table_id)
1123
+ for command in _view_configuration_commands(cli_bin=cli_bin, identity=identity, base_token=token, table_id=table_ref, view_ids={view["name"]: view["name"] for view in lark_kanban_views()}):
1124
+ result = _run_command(command, execute=execute, runner=runner)
1125
+ commands.append(result)
1126
+ if result.get("executed") and not result.get("ok"):
1127
+ return _board_payload(False, commands, effective_base_token, table_id)
1128
+ return _board_payload(True, commands, effective_base_token, table_id)
1129
+
1130
+
1131
+ def _board_payload(
1132
+ ok: bool,
1133
+ commands: list[dict[str, Any]],
1134
+ base_token: str | None,
1135
+ table_id: str | None,
1136
+ ) -> dict[str, Any]:
1137
+ return {
1138
+ "ok": ok,
1139
+ "schema_version": LARK_KANBAN_SCHEMA_VERSION,
1140
+ "base_token": base_token,
1141
+ "table_id": table_id,
1142
+ "commands": commands,
1143
+ "error": None if ok else next((_command_error(item) for item in commands if not item.get("ok")), "unknown"),
1144
+ }
1145
+
1146
+
1147
+ def _extract_base_token(parsed: Any) -> str | None:
1148
+ return _find_first_string(parsed, ("base_token", "app_token"))
1149
+
1150
+
1151
+ def _extract_table_id(parsed: Any) -> str | None:
1152
+ return _find_first_string(parsed, ("table_id", "id"), required_prefix="tbl")
1153
+
1154
+
1155
+ def _find_first_string(payload: Any, keys: tuple[str, ...], required_prefix: str | None = None) -> str | None:
1156
+ if isinstance(payload, dict):
1157
+ for key in keys:
1158
+ value = payload.get(key)
1159
+ if isinstance(value, str) and value.strip():
1160
+ candidate = value.strip()
1161
+ if required_prefix is None or candidate.startswith(required_prefix):
1162
+ return candidate
1163
+ for value in payload.values():
1164
+ found = _find_first_string(value, keys, required_prefix=required_prefix)
1165
+ if found:
1166
+ return found
1167
+ if isinstance(payload, list):
1168
+ for value in payload:
1169
+ found = _find_first_string(value, keys, required_prefix=required_prefix)
1170
+ if found:
1171
+ return found
1172
+ return None
1173
+
1174
+
1175
+ def _extract_created_record_id(parsed: Any) -> str | None:
1176
+ return _find_first_record_id(parsed)
1177
+
1178
+
1179
+ def _find_first_record_id(payload: Any) -> str | None:
1180
+ if isinstance(payload, dict):
1181
+ ids = payload.get("record_id_list")
1182
+ if isinstance(ids, list) and ids:
1183
+ return str(ids[0])
1184
+ for key in ("record_id", "id"):
1185
+ value = payload.get(key)
1186
+ if isinstance(value, str) and value.strip().startswith("rec"):
1187
+ return value.strip()
1188
+ for value in payload.values():
1189
+ found = _find_first_record_id(value)
1190
+ if found:
1191
+ return found
1192
+ if isinstance(payload, list):
1193
+ for value in payload:
1194
+ found = _find_first_record_id(value)
1195
+ if found:
1196
+ return found
1197
+ return None
1198
+
1199
+
1200
+ def _extract_table_id_from_blocks(parsed: Any, *, table_name: str | None = None) -> str | None:
1201
+ blocks = _extract_list_from_payload(parsed, ("blocks", "items", "data"))
1202
+ table_blocks = [
1203
+ item
1204
+ for item in blocks
1205
+ if isinstance(item, dict)
1206
+ and str(item.get("type") or item.get("resource_type") or "").lower() in {"table", "bitable_table", ""}
1207
+ ]
1208
+ if table_name:
1209
+ for item in table_blocks:
1210
+ if str(item.get("name") or item.get("block_name") or "").strip() == table_name:
1211
+ return _string_id_from_dict(item, ("id", "block_id", "table_id"), prefix="tbl")
1212
+ for item in table_blocks:
1213
+ table_id = _string_id_from_dict(item, ("id", "block_id", "table_id"), prefix="tbl")
1214
+ if table_id:
1215
+ return table_id
1216
+ return _extract_table_id(parsed)
1217
+
1218
+
1219
+ def _extract_view_ids(parsed: Any) -> dict[str, str]:
1220
+ views = _extract_list_from_payload(parsed, ("views", "items", "data"))
1221
+ result: dict[str, str] = {}
1222
+ for item in views:
1223
+ if not isinstance(item, dict):
1224
+ continue
1225
+ name = str(item.get("name") or item.get("view_name") or "").strip()
1226
+ view_id = _string_id_from_dict(item, ("view_id", "id"), prefix="vew") or _string_id_from_dict(
1227
+ item,
1228
+ ("view_id", "id"),
1229
+ prefix=None,
1230
+ )
1231
+ if name and view_id:
1232
+ result[name] = view_id
1233
+ return result
1234
+
1235
+
1236
+ def _extract_list_from_payload(payload: Any, keys: tuple[str, ...]) -> list[Any]:
1237
+ if isinstance(payload, list):
1238
+ return payload
1239
+ if not isinstance(payload, dict):
1240
+ return []
1241
+ for key in keys:
1242
+ value = payload.get(key)
1243
+ if isinstance(value, list):
1244
+ return value
1245
+ data = payload.get("data")
1246
+ if isinstance(data, dict):
1247
+ for key in keys:
1248
+ value = data.get(key)
1249
+ if isinstance(value, list):
1250
+ return value
1251
+ return []
1252
+
1253
+
1254
+ def _string_id_from_dict(item: dict[str, Any], keys: tuple[str, ...], *, prefix: str | None) -> str | None:
1255
+ for key in keys:
1256
+ value = item.get(key)
1257
+ if not isinstance(value, str):
1258
+ continue
1259
+ candidate = value.strip()
1260
+ if candidate and (prefix is None or candidate.startswith(prefix)):
1261
+ return candidate
1262
+ return None
1263
+
1264
+
1265
+ def use_lark_kanban_board(
1266
+ *,
1267
+ config_path: Path,
1268
+ base_url: str | None = None,
1269
+ base_token: str | None = None,
1270
+ table_id: str | None = None,
1271
+ view_id: str | None = None,
1272
+ cli_bin: str = DEFAULT_CLI_BIN,
1273
+ identity: str = "user",
1274
+ ) -> dict[str, Any]:
1275
+ parsed = parse_lark_base_url(base_url) if base_url else {}
1276
+ effective_base_token = str(base_token or parsed.get("base_token") or "").strip()
1277
+ effective_table_id = str(table_id or parsed.get("table_id") or "").strip()
1278
+ effective_view_id = str(view_id or parsed.get("view_id") or DEFAULT_STATUS_QUEUE_VIEW).strip()
1279
+ return save_lark_kanban_board_config(
1280
+ config_path,
1281
+ **{"base_" + "token": effective_base_token},
1282
+ table_id=effective_table_id,
1283
+ view_id=effective_view_id,
1284
+ base_url=base_url,
1285
+ cli_bin=cli_bin,
1286
+ identity=identity,
1287
+ )
1288
+
1289
+
1290
+ def setup_lark_kanban_board(
1291
+ *,
1292
+ config_path: Path,
1293
+ base_name: str,
1294
+ table_name: str = DEFAULT_TABLE_NAME,
1295
+ base_url: str | None = None,
1296
+ base_token: str | None = None,
1297
+ table_id: str | None = None,
1298
+ cli_bin: str = DEFAULT_CLI_BIN,
1299
+ identity: str = "user",
1300
+ execute: bool = False,
1301
+ runner: CommandRunner = default_subprocess_runner,
1302
+ ) -> dict[str, Any]:
1303
+ commands: list[dict[str, Any]] = []
1304
+ warnings: list[str] = []
1305
+ existing = read_lark_kanban_local_config(config_path)
1306
+ existing_board = existing.get("board") if isinstance(existing.get("board"), dict) else {}
1307
+ parsed_url = parse_lark_base_url(base_url) if base_url else {}
1308
+ effective_base_token = str(
1309
+ base_token or parsed_url.get("base_token") or existing_board.get("base_token") or ""
1310
+ ).strip()
1311
+ effective_table_id = str(table_id or parsed_url.get("table_id") or existing_board.get("table_id") or "").strip()
1312
+ view_ids = existing_board.get("view_ids") if isinstance(existing_board.get("view_ids"), dict) else {}
1313
+
1314
+ preflight = _lark_kanban_preflight(cli_bin=cli_bin, identity=identity, runner=runner)
1315
+ commands.extend(preflight["commands"])
1316
+ if execute and not preflight["identity_available"]:
1317
+ return {
1318
+ "ok": False,
1319
+ "schema_version": LARK_KANBAN_SCHEMA_VERSION,
1320
+ "execute": execute,
1321
+ "config_path": str(config_path),
1322
+ "base_token": effective_base_token or None,
1323
+ "table_id": effective_table_id or None,
1324
+ "commands": commands,
1325
+ "warnings": preflight["warnings"],
1326
+ "error": _identity_error(identity),
1327
+ }
1328
+ warnings.extend(preflight["warnings"])
1329
+
1330
+ created_base = False
1331
+ created_table = False
1332
+ created_field_names: list[str] = []
1333
+ updated_field_names: list[str] = []
1334
+ config_payload: dict[str, Any] | None = None
1335
+
1336
+ def save_usable_config() -> None:
1337
+ nonlocal config_payload
1338
+ if not execute or not effective_base_token or not effective_table_id:
1339
+ return
1340
+ config_payload = save_lark_kanban_board_config(
1341
+ config_path,
1342
+ **{"base_" + "token": effective_base_token},
1343
+ table_id=effective_table_id,
1344
+ view_id=view_ids.get(DEFAULT_STATUS_QUEUE_VIEW) or DEFAULT_STATUS_QUEUE_VIEW,
1345
+ base_url=base_url,
1346
+ base_name=base_name,
1347
+ table_name=table_name,
1348
+ cli_bin=cli_bin,
1349
+ identity=identity,
1350
+ view_ids=view_ids,
1351
+ )
1352
+
1353
+ def partial_enrichment_payload(command_result: dict[str, Any]) -> dict[str, Any]:
1354
+ error = _command_error(command_result)
1355
+ warnings.append(f"usable board config saved; view enrichment skipped: {error}")
1356
+ return _setup_payload(
1357
+ True,
1358
+ execute,
1359
+ config_path,
1360
+ commands,
1361
+ warnings,
1362
+ effective_base_token,
1363
+ effective_table_id,
1364
+ config=config_payload,
1365
+ partial=True,
1366
+ enrichment_error=error,
1367
+ )
1368
+
1369
+ if not effective_base_token:
1370
+ created_base = True
1371
+ create = _run_command(
1372
+ [
1373
+ cli_bin,
1374
+ "base",
1375
+ "+base-create",
1376
+ "--as",
1377
+ identity,
1378
+ "--name",
1379
+ base_name,
1380
+ "--table-name",
1381
+ table_name,
1382
+ "--fields",
1383
+ json.dumps(lark_kanban_field_definitions(), ensure_ascii=False),
1384
+ ],
1385
+ execute=execute,
1386
+ runner=runner,
1387
+ )
1388
+ commands.append(create)
1389
+ if execute:
1390
+ if not create.get("ok"):
1391
+ return _setup_payload(False, execute, config_path, commands, warnings, effective_base_token, effective_table_id)
1392
+ effective_base_token = _extract_base_token(create.get("json")) or ""
1393
+ effective_table_id = _extract_table_id(create.get("json")) or effective_table_id
1394
+ if not effective_base_token:
1395
+ return _setup_payload(
1396
+ False,
1397
+ execute,
1398
+ config_path,
1399
+ commands,
1400
+ warnings,
1401
+ effective_base_token,
1402
+ effective_table_id,
1403
+ error="base-create did not return a usable Base token",
1404
+ )
1405
+ else:
1406
+ effective_base_token = "<base-token-from-create>"
1407
+ if not effective_table_id:
1408
+ if created_base:
1409
+ blocks = _run_command(
1410
+ [
1411
+ cli_bin,
1412
+ "base",
1413
+ "+base-block-list",
1414
+ "--as",
1415
+ identity,
1416
+ "--base-token",
1417
+ effective_base_token,
1418
+ "--type",
1419
+ "table",
1420
+ ],
1421
+ execute=execute,
1422
+ runner=runner,
1423
+ )
1424
+ commands.append(blocks)
1425
+ if execute:
1426
+ if not blocks.get("ok"):
1427
+ return _setup_payload(False, execute, config_path, commands, warnings, effective_base_token, effective_table_id)
1428
+ effective_table_id = _extract_table_id_from_blocks(blocks.get("json"), table_name=table_name) or ""
1429
+ if not effective_table_id:
1430
+ return _setup_payload(
1431
+ False,
1432
+ execute,
1433
+ config_path,
1434
+ commands,
1435
+ warnings,
1436
+ effective_base_token,
1437
+ effective_table_id,
1438
+ error="base-block-list did not return a usable table id",
1439
+ )
1440
+ else:
1441
+ effective_table_id = "<table-id-from-base-block-list>"
1442
+ else:
1443
+ created_table = True
1444
+ table_create = _run_command(
1445
+ [
1446
+ cli_bin,
1447
+ "base",
1448
+ "+table-create",
1449
+ "--as",
1450
+ identity,
1451
+ "--base-token",
1452
+ effective_base_token,
1453
+ "--name",
1454
+ table_name,
1455
+ "--fields",
1456
+ json.dumps(lark_kanban_field_definitions(), ensure_ascii=False),
1457
+ "--view",
1458
+ json.dumps(lark_kanban_views(), ensure_ascii=False),
1459
+ ],
1460
+ execute=execute,
1461
+ runner=runner,
1462
+ )
1463
+ commands.append(table_create)
1464
+ if execute:
1465
+ if not table_create.get("ok"):
1466
+ return _setup_payload(False, execute, config_path, commands, warnings, effective_base_token, effective_table_id)
1467
+ effective_table_id = _extract_table_id(table_create.get("json")) or ""
1468
+ if not effective_table_id:
1469
+ return _setup_payload(
1470
+ False,
1471
+ execute,
1472
+ config_path,
1473
+ commands,
1474
+ warnings,
1475
+ effective_base_token,
1476
+ effective_table_id,
1477
+ error="table-create did not return a usable table id",
1478
+ )
1479
+ else:
1480
+ effective_table_id = "<table-id-from-table-create>"
1481
+
1482
+ save_usable_config()
1483
+
1484
+ if execute and not created_base and not created_table:
1485
+ field_list = _run_command(
1486
+ [cli_bin, "base", "+field-list", "--as", identity, "--base-token", effective_base_token, "--table-id", effective_table_id, "--format", "json", "--limit", "200"],
1487
+ execute=True,
1488
+ runner=runner,
1489
+ )
1490
+ commands.append(field_list)
1491
+ if not field_list.get("ok"):
1492
+ return partial_enrichment_payload(field_list)
1493
+ for definition in issue_fix_surface.missing_field_definitions(field_list.get("json"), lark_kanban_field_definitions()):
1494
+ field_create = _run_command(
1495
+ [cli_bin, "base", "+field-create", "--as", identity, "--base-token", effective_base_token, "--table-id", effective_table_id, "--json", json.dumps(definition, ensure_ascii=False)],
1496
+ execute=True,
1497
+ runner=runner,
1498
+ )
1499
+ commands.append(field_create)
1500
+ if not field_create.get("ok"):
1501
+ return partial_enrichment_payload(field_create)
1502
+ created_field_names.append(str(definition.get("name") or ""))
1503
+ for migration in issue_fix_surface.field_definition_migrations(
1504
+ field_list.get("json"), lark_kanban_field_definitions()
1505
+ ):
1506
+ field_update = _run_command(
1507
+ [
1508
+ cli_bin,
1509
+ "base",
1510
+ "+field-update",
1511
+ "--as",
1512
+ identity,
1513
+ "--base-token",
1514
+ effective_base_token,
1515
+ "--table-id",
1516
+ effective_table_id,
1517
+ "--field-id",
1518
+ str(migration["field_id"]),
1519
+ "--json",
1520
+ json.dumps(migration["definition"], ensure_ascii=False),
1521
+ "--yes",
1522
+ ],
1523
+ execute=True,
1524
+ runner=runner,
1525
+ )
1526
+ commands.append(field_update)
1527
+ if not field_update.get("ok"):
1528
+ return partial_enrichment_payload(field_update)
1529
+ updated_field_names.append(str(migration["name"]))
1530
+
1531
+ if execute:
1532
+ view_ids = _refresh_view_ids(
1533
+ cli_bin=cli_bin,
1534
+ identity=identity,
1535
+ **{"base_" + "token": effective_base_token},
1536
+ table_id=effective_table_id,
1537
+ commands=commands,
1538
+ runner=runner,
1539
+ )
1540
+ save_usable_config()
1541
+ else:
1542
+ view_ids = {str(name): str(value) for name, value in view_ids.items()}
1543
+
1544
+ missing_view_names = [view["name"] for view in lark_kanban_views() if view["name"] not in view_ids]
1545
+ should_create_views = bool(missing_view_names) and (created_base or not created_table)
1546
+ if should_create_views:
1547
+ view_create = _run_command(
1548
+ [
1549
+ cli_bin,
1550
+ "base",
1551
+ "+view-create",
1552
+ "--as",
1553
+ identity,
1554
+ "--base-token",
1555
+ effective_base_token,
1556
+ "--table-id",
1557
+ effective_table_id,
1558
+ "--json",
1559
+ json.dumps(
1560
+ [view for view in lark_kanban_views() if view["name"] in missing_view_names],
1561
+ ensure_ascii=False,
1562
+ ),
1563
+ ],
1564
+ execute=execute,
1565
+ runner=runner,
1566
+ )
1567
+ commands.append(view_create)
1568
+ if execute and not view_create.get("ok"):
1569
+ if config_payload:
1570
+ return partial_enrichment_payload(view_create)
1571
+ return _setup_payload(False, execute, config_path, commands, warnings, effective_base_token, effective_table_id)
1572
+ if execute:
1573
+ view_ids = _refresh_view_ids(
1574
+ cli_bin=cli_bin,
1575
+ identity=identity,
1576
+ **{"base_" + "token": effective_base_token},
1577
+ table_id=effective_table_id,
1578
+ commands=commands,
1579
+ runner=runner,
1580
+ )
1581
+ save_usable_config()
1582
+
1583
+ if not view_ids:
1584
+ view_ids = {view["name"]: view["name"] for view in lark_kanban_views()}
1585
+ for command in _view_configuration_commands(
1586
+ cli_bin=cli_bin,
1587
+ identity=identity,
1588
+ **{"base_" + "token": effective_base_token},
1589
+ table_id=effective_table_id,
1590
+ view_ids=view_ids,
1591
+ ):
1592
+ result = _run_command(command, execute=execute, runner=runner)
1593
+ commands.append(result)
1594
+ if execute and not result.get("ok"):
1595
+ if config_payload:
1596
+ return partial_enrichment_payload(result)
1597
+ return _setup_payload(False, execute, config_path, commands, warnings, effective_base_token, effective_table_id)
1598
+
1599
+ save_usable_config()
1600
+ return {
1601
+ "ok": True,
1602
+ "schema_version": LARK_KANBAN_SCHEMA_VERSION,
1603
+ "execute": execute,
1604
+ "config_path": str(config_path),
1605
+ "created_base": created_base,
1606
+ "created_table": created_table,
1607
+ "created_fields": created_field_names,
1608
+ "updated_fields": updated_field_names,
1609
+ "base_token": effective_base_token,
1610
+ "table_id": effective_table_id,
1611
+ "view_ids": view_ids,
1612
+ "operator_card_fields": OPERATOR_CARD_FIELDS,
1613
+ "commands": commands,
1614
+ "warnings": warnings,
1615
+ "config": config_payload,
1616
+ "next_commands": _next_lark_kanban_commands(config_payload.get("board", {}) if config_payload else {}),
1617
+ }
1618
+
1619
+
1620
+ def _setup_payload(
1621
+ ok: bool,
1622
+ execute: bool,
1623
+ config_path: Path,
1624
+ commands: list[dict[str, Any]],
1625
+ warnings: list[str],
1626
+ base_token: str | None,
1627
+ table_id: str | None,
1628
+ *,
1629
+ error: str | None = None,
1630
+ config: dict[str, Any] | None = None,
1631
+ partial: bool = False,
1632
+ enrichment_error: str | None = None,
1633
+ ) -> dict[str, Any]:
1634
+ payload = {
1635
+ "ok": ok,
1636
+ "schema_version": LARK_KANBAN_SCHEMA_VERSION,
1637
+ "execute": execute,
1638
+ "config_path": str(config_path),
1639
+ "base_token": base_token,
1640
+ "table_id": table_id,
1641
+ "commands": commands,
1642
+ "warnings": warnings,
1643
+ }
1644
+ if config:
1645
+ payload["config"] = config
1646
+ payload["next_commands"] = _next_lark_kanban_commands(config.get("board", {}))
1647
+ if partial:
1648
+ payload["partial"] = True
1649
+ payload["enrichment_ok"] = False
1650
+ payload["enrichment_error"] = enrichment_error or next(
1651
+ (_command_error(item) for item in commands if not item.get("ok")),
1652
+ "unknown",
1653
+ )
1654
+ elif not ok:
1655
+ payload["error"] = error or next((_command_error(item) for item in commands if not item.get("ok")), "unknown")
1656
+ return payload
1657
+
1658
+
1659
+ def _refresh_view_ids(
1660
+ *,
1661
+ cli_bin: str,
1662
+ identity: str,
1663
+ base_token: str,
1664
+ table_id: str,
1665
+ commands: list[dict[str, Any]],
1666
+ runner: CommandRunner,
1667
+ ) -> dict[str, str]:
1668
+ view_list = _run_command(
1669
+ [
1670
+ cli_bin,
1671
+ "base",
1672
+ "+view-list",
1673
+ "--as",
1674
+ identity,
1675
+ "--base-token",
1676
+ base_token,
1677
+ "--table-id",
1678
+ table_id,
1679
+ ],
1680
+ execute=True,
1681
+ runner=runner,
1682
+ )
1683
+ commands.append(view_list)
1684
+ if not view_list.get("ok"):
1685
+ return {}
1686
+ return _extract_view_ids(view_list.get("json"))
1687
+
1688
+
1689
+ def _view_configuration_commands(
1690
+ *,
1691
+ cli_bin: str,
1692
+ identity: str,
1693
+ base_token: str,
1694
+ table_id: str,
1695
+ view_ids: dict[str, str],
1696
+ ) -> list[list[str]]:
1697
+ return issue_fix_surface.view_configuration_commands(cli_bin=cli_bin, identity=identity, base_token=base_token, table_id=table_id, view_ids=view_ids, worker_view_name=DEFAULT_STATUS_QUEUE_VIEW, todo_statuses=[STATUS_TODO, STATUS_CLAIMED], user_gate_status=STATUS_USER_GATE, operator_card_fields=OPERATOR_CARD_FIELDS)
1698
+
1699
+
1700
+ def _lark_kanban_schema_check(parsed: Any) -> dict[str, Any]:
1701
+ definitions = lark_kanban_field_definitions()
1702
+ missing = issue_fix_surface.missing_field_definitions(parsed, definitions)
1703
+ migrations = issue_fix_surface.field_definition_migrations(parsed, definitions)
1704
+ missing_names = [str(item.get("name") or "") for item in missing]
1705
+ incompatible_names = [str(item.get("name") or "") for item in migrations]
1706
+ migration_required = bool(missing_names or incompatible_names)
1707
+ return {
1708
+ "ok": not migration_required,
1709
+ "status": "stale" if migration_required else "ready",
1710
+ "expected_field_count": len(definitions),
1711
+ "observed_field_count": len(issue_fix_surface.extract_field_names(parsed)),
1712
+ "missing_fields": missing_names,
1713
+ "incompatible_fields": incompatible_names,
1714
+ "migration_required": migration_required,
1715
+ "remediation_command": (
1716
+ "loopx lark-kanban setup --execute" if migration_required else None
1717
+ ),
1718
+ }
1719
+
1720
+
1721
+ def _inspect_lark_kanban_schema(
1722
+ config: LarkKanbanConfig,
1723
+ *,
1724
+ commands: list[dict[str, Any]],
1725
+ runner: CommandRunner,
1726
+ ) -> dict[str, Any]:
1727
+ field_list = _run_command(
1728
+ [
1729
+ config.cli_bin,
1730
+ "base",
1731
+ "+field-list",
1732
+ "--as",
1733
+ config.identity,
1734
+ "--base-token",
1735
+ config.base_token,
1736
+ "--table-id",
1737
+ config.table_id,
1738
+ "--format",
1739
+ "json",
1740
+ "--limit",
1741
+ "200",
1742
+ ],
1743
+ execute=True,
1744
+ runner=runner,
1745
+ )
1746
+ commands.append(field_list)
1747
+ if not field_list.get("ok"):
1748
+ return {
1749
+ "ok": False,
1750
+ "status": "unavailable",
1751
+ "migration_required": None,
1752
+ "missing_fields": [],
1753
+ "incompatible_fields": [],
1754
+ "remediation_command": "loopx lark-kanban doctor --require-board",
1755
+ "error": _command_error(field_list),
1756
+ }
1757
+ return _lark_kanban_schema_check(field_list.get("json"))
1758
+
1759
+
1760
+ def _schema_check_error(schema_check: dict[str, Any]) -> str:
1761
+ if schema_check.get("status") == "stale":
1762
+ missing = ", ".join(schema_check.get("missing_fields") or []) or "none"
1763
+ incompatible = (
1764
+ ", ".join(schema_check.get("incompatible_fields") or []) or "none"
1765
+ )
1766
+ return (
1767
+ "Lark Kanban schema is stale "
1768
+ f"(missing: {missing}; incompatible: {incompatible}); run "
1769
+ f"`{schema_check['remediation_command']}` before sync"
1770
+ )
1771
+ return (
1772
+ "Lark Kanban schema could not be inspected before sync: "
1773
+ f"{schema_check.get('error') or 'unknown error'}"
1774
+ )
1775
+
1776
+
1777
+ def lark_kanban_doctor(
1778
+ *,
1779
+ config_path: Path,
1780
+ cli_bin: str = DEFAULT_CLI_BIN,
1781
+ identity: str = "user",
1782
+ check_board: bool = True,
1783
+ require_board: bool = False,
1784
+ runner: CommandRunner = default_subprocess_runner,
1785
+ ) -> dict[str, Any]:
1786
+ commands: list[dict[str, Any]] = []
1787
+ issues: list[dict[str, str]] = []
1788
+ schema_check: dict[str, Any] | None = None
1789
+ preflight = _lark_kanban_preflight(cli_bin=cli_bin, identity=identity, runner=runner)
1790
+ commands.extend(preflight["commands"])
1791
+ for warning in preflight["warnings"]:
1792
+ issues.append({"severity": "warning", "message": warning})
1793
+ if not preflight["cli_ok"]:
1794
+ issues.append({"severity": "error", "message": f"{cli_bin} is not runnable"})
1795
+ if not preflight["identity_available"]:
1796
+ issues.append({"severity": "error", "message": _identity_error(identity)})
1797
+
1798
+ config_payload = read_lark_kanban_local_config(config_path)
1799
+ board_config = lark_kanban_config_from_payload(config_payload)
1800
+ if not config_payload.get("exists"):
1801
+ issues.append(
1802
+ {
1803
+ "severity": "error" if require_board else "warning",
1804
+ "message": f"no local board config at {config_path}; run lark-kanban setup or use",
1805
+ }
1806
+ )
1807
+ elif not board_config:
1808
+ issues.append({"severity": "error", "message": f"local board config is incomplete: {config_path}"})
1809
+
1810
+ if check_board and board_config and preflight["identity_available"]:
1811
+ for command in (
1812
+ [
1813
+ board_config.cli_bin,
1814
+ "base",
1815
+ "+base-get",
1816
+ "--as",
1817
+ board_config.identity,
1818
+ "--base-token",
1819
+ board_config.base_token,
1820
+ ],
1821
+ [
1822
+ board_config.cli_bin,
1823
+ "base",
1824
+ "+view-list",
1825
+ "--as",
1826
+ board_config.identity,
1827
+ "--base-token",
1828
+ board_config.base_token,
1829
+ "--table-id",
1830
+ board_config.table_id,
1831
+ ],
1832
+ ):
1833
+ result = _run_command(command, execute=True, runner=runner)
1834
+ commands.append(result)
1835
+ if not result.get("ok"):
1836
+ issues.append({"severity": "error", "message": _command_error(result)})
1837
+ if not any(issue["severity"] == "error" for issue in issues):
1838
+ schema_check = _inspect_lark_kanban_schema(
1839
+ board_config,
1840
+ commands=commands,
1841
+ runner=runner,
1842
+ )
1843
+ if not schema_check.get("ok"):
1844
+ issues.append(
1845
+ {
1846
+ "severity": "error",
1847
+ "message": _schema_check_error(schema_check),
1848
+ }
1849
+ )
1850
+
1851
+ return {
1852
+ "ok": not any(issue["severity"] == "error" for issue in issues),
1853
+ "schema_version": "loopx_lark_kanban_doctor_v0",
1854
+ "config_path": str(config_path),
1855
+ "identity": identity,
1856
+ "issues": issues,
1857
+ "schema_check": schema_check,
1858
+ "config": config_payload,
1859
+ "commands": commands,
1860
+ }
1861
+
1862
+
1863
+ def _lark_kanban_preflight(
1864
+ *,
1865
+ cli_bin: str,
1866
+ identity: str,
1867
+ runner: CommandRunner,
1868
+ ) -> dict[str, Any]:
1869
+ commands: list[dict[str, Any]] = []
1870
+ warnings: list[str] = []
1871
+ version = _run_command([cli_bin, "--version"], execute=True, runner=runner)
1872
+ commands.append(version)
1873
+ cli_ok = bool(version.get("ok"))
1874
+ version_tuple = _version_tuple(str(version.get("stdout") or ""))
1875
+ if cli_ok and version_tuple and version_tuple < (1, 0, 56):
1876
+ warnings.append("lark-cli should be upgraded to at least 1.0.56 for setup visible-field support")
1877
+ auth = _run_command([cli_bin, "auth", "status"], execute=True, runner=runner)
1878
+ commands.append(auth)
1879
+ identity_available = _identity_available(auth.get("json"), identity)
1880
+ for help_command in (
1881
+ [cli_bin, "base", "+base-create", "--help"],
1882
+ [cli_bin, "base", "+base-block-list", "--help"],
1883
+ [cli_bin, "base", "+view-list", "--help"],
1884
+ [cli_bin, "base", "+view-set-group", "--help"],
1885
+ [cli_bin, "base", "+view-set-visible-fields", "--help"],
1886
+ ):
1887
+ result = _run_command(help_command, execute=True, runner=runner)
1888
+ commands.append(result)
1889
+ if not result.get("ok"):
1890
+ warnings.append(f"missing lark-cli shortcut: {shlex.join(help_command[2:-1])}")
1891
+ return {
1892
+ "commands": commands,
1893
+ "warnings": warnings,
1894
+ "cli_ok": cli_ok,
1895
+ "identity_available": identity_available,
1896
+ }
1897
+
1898
+
1899
+ def _version_tuple(text: str) -> tuple[int, ...] | None:
1900
+ match = re.search(r"\d+(?:\.\d+)+", text)
1901
+ if not match:
1902
+ return None
1903
+ return tuple(int(part) for part in match.group(0).split("."))
1904
+
1905
+
1906
+ def _identity_available(parsed: Any, identity: str) -> bool:
1907
+ if not isinstance(parsed, dict):
1908
+ return False
1909
+ identities = parsed.get("identities") if isinstance(parsed.get("identities"), dict) else {}
1910
+ if identity == "auto":
1911
+ active = str(parsed.get("identity") or "").strip()
1912
+ if active and isinstance(identities.get(active), dict):
1913
+ return bool(identities[active].get("available"))
1914
+ return any(isinstance(item, dict) and bool(item.get("available")) for item in identities.values())
1915
+ status = identities.get(identity)
1916
+ return isinstance(status, dict) and bool(status.get("available"))
1917
+
1918
+
1919
+ def _identity_error(identity: str) -> str:
1920
+ if identity == "user":
1921
+ return "lark-cli user identity is unavailable; run `lark-cli auth login --domain base --recommend`"
1922
+ return f"lark-cli identity {identity!r} is unavailable; run `lark-cli auth status`"
1923
+
1924
+
1925
+ def _lark_record_from_projection_block(
1926
+ block: dict[str, Any],
1927
+ *,
1928
+ goal_id: str,
1929
+ source_id: str,
1930
+ public_safe: bool = False,
1931
+ ) -> dict[str, Any]:
1932
+ priority = str(block.get("priority") or "P2")
1933
+ values = _lark_record_from_todo_block(
1934
+ block,
1935
+ goal_id=goal_id,
1936
+ state_file=Path(f"{source_id}.projection"),
1937
+ priority=priority,
1938
+ )
1939
+ original_id = str(block.get("original_todo_id") or "")
1940
+ evidence_parts = [
1941
+ str(block.get("evidence") or block.get("reason") or block.get("note") or "").strip(),
1942
+ f"source_id={source_id}",
1943
+ ]
1944
+ if original_id:
1945
+ evidence_parts.append(f"original_todo_id={original_id}")
1946
+ evidence_parts.extend(_projection_lifecycle_parts(block, source_id=source_id))
1947
+ evidence_parts = list(dict.fromkeys(part for part in evidence_parts if part))
1948
+ lifecycle_state = _projection_lifecycle_state(block)
1949
+ default_handoff = f"Synced from LoopX projection source={source_id}"
1950
+ if lifecycle_state in {"superseded", "migrated", "retired"}:
1951
+ superseded_by = ", ".join(_as_text_list(_projection_lifecycle_value(block, "superseded_by"))) or "current projection row"
1952
+ default_handoff = f"Row lifecycle {lifecycle_state}; continue with {superseded_by}."
1953
+ values.update(
1954
+ {
1955
+ "LoopX Goal ID": goal_id,
1956
+ "LoopX Todo ID": str(block.get("todo_id") or ""),
1957
+ "Scope": _compact_text(block.get("scope") or block.get("source_section") or f"projection_source={source_id}"),
1958
+ "Handoff": _compact_text(block.get("handoff") or default_handoff),
1959
+ "Evidence": _compact_text("; ".join(evidence_parts)),
1960
+ "Run History": _compact_text(
1961
+ "; ".join(
1962
+ part
1963
+ for part in [
1964
+ f"synced from LoopX projection source={source_id} status={block.get('status') or 'open'}",
1965
+ f"row_lifecycle={lifecycle_state}" if lifecycle_state else "",
1966
+ ]
1967
+ if part
1968
+ )
1969
+ ),
1970
+ }
1971
+ )
1972
+ values.update(issue_fix_surface.issue_fix_record_values(block, compact_text=_compact_text))
1973
+ return _public_safe_lark_values(values) if public_safe else values
1974
+
1975
+
1976
+ def sync_loopx_projection_to_lark_kanban(
1977
+ config: LarkKanbanConfig,
1978
+ *,
1979
+ projection: dict[str, Any],
1980
+ goal_id: str | None = None,
1981
+ agent_id: str | None = None,
1982
+ source_id: str | None = None,
1983
+ config_path: Path | None = None,
1984
+ include_done: bool = False,
1985
+ limit: int = 50,
1986
+ sink_visibility: str = SINK_VISIBILITY_OWNER_ONLY,
1987
+ reconcile_source: bool = False,
1988
+ source_snapshot_complete: bool = False,
1989
+ execute: bool = False,
1990
+ runner: CommandRunner = default_subprocess_runner,
1991
+ ) -> dict[str, Any]:
1992
+ if not isinstance(projection, dict):
1993
+ raise ValueError("projection must be a JSON object")
1994
+ if sink_visibility not in SINK_VISIBILITIES:
1995
+ raise ValueError(f"sink_visibility must be one of {sorted(SINK_VISIBILITIES)}")
1996
+ public_safe = sink_visibility == SINK_VISIBILITY_SHARED
1997
+ resolved_source_id, namespace_warnings = _projection_namespace(projection, source_id)
1998
+ resolved_goal_id, rows, warnings = _projection_rows_from_payload(
1999
+ projection,
2000
+ goal_id=goal_id,
2001
+ agent_id=agent_id,
2002
+ source_id=resolved_source_id,
2003
+ include_done=include_done,
2004
+ limit=limit,
2005
+ )
2006
+ warnings = [*namespace_warnings, *warnings]
2007
+
2008
+ complete_rows, complete_warnings = rows, []
2009
+ if reconcile_source:
2010
+ _, complete_rows, complete_warnings = _projection_rows_from_payload(
2011
+ projection,
2012
+ goal_id=goal_id,
2013
+ agent_id=None,
2014
+ source_id=resolved_source_id,
2015
+ include_done=True,
2016
+ limit=2**31 - 1,
2017
+ )
2018
+ validate_projection_source_reconcile_request(
2019
+ requested=reconcile_source,
2020
+ source_snapshot_complete=source_snapshot_complete,
2021
+ agent_id=agent_id,
2022
+ include_done=include_done,
2023
+ namespace_warnings=namespace_warnings,
2024
+ selected_row_count=len(rows),
2025
+ complete_row_count=len(complete_rows),
2026
+ complete_warnings=complete_warnings,
2027
+ )
2028
+
2029
+ commands: list[dict[str, Any]] = []
2030
+ schema_check: dict[str, Any] | None = None
2031
+ if execute:
2032
+ schema_check = _inspect_lark_kanban_schema(
2033
+ config,
2034
+ commands=commands,
2035
+ runner=runner,
2036
+ )
2037
+ if not schema_check.get("ok"):
2038
+ return {
2039
+ "ok": False,
2040
+ "schema_version": LARK_KANBAN_SYNC_PROJECTION_VERSION,
2041
+ "execute": execute,
2042
+ "goal_id": resolved_goal_id,
2043
+ "agent_id": agent_id,
2044
+ "source_id": resolved_source_id,
2045
+ "sink_visibility": sink_visibility,
2046
+ "public_safe_redaction": public_safe,
2047
+ "projection_schema_version": projection.get("schema_version"),
2048
+ "row_count": len(rows),
2049
+ "records": [],
2050
+ "commands": commands,
2051
+ "warnings": warnings,
2052
+ "config_path": str(config_path) if config_path else None,
2053
+ "schema_check": schema_check,
2054
+ "error": _schema_check_error(schema_check),
2055
+ }
2056
+ local, record_map, remote_records, remote_list_complete = _load_lark_todo_record_map(
2057
+ config,
2058
+ config_path=config_path,
2059
+ execute=execute,
2060
+ inspect_remote=reconcile_source,
2061
+ commands=commands,
2062
+ runner=runner,
2063
+ )
2064
+ if reconcile_source and not remote_list_complete:
2065
+ raise ValueError(
2066
+ "source reconcile requires a complete remote record list"
2067
+ )
2068
+
2069
+ desired_keys = {
2070
+ f"{str(row.get('goal_id') or resolved_goal_id)}:{str(row.get('todo_id') or '').strip()}"
2071
+ for row in rows
2072
+ }
2073
+ reconcile_plan = (
2074
+ plan_projection_source_reconcile(
2075
+ goal_id=resolved_goal_id,
2076
+ source_id=resolved_source_id,
2077
+ desired_keys=sorted(desired_keys),
2078
+ local_record_map=record_map,
2079
+ remote_records=remote_records,
2080
+ )
2081
+ if reconcile_source
2082
+ else {
2083
+ "schema_version": "projection_source_reconcile_plan_v0",
2084
+ "goal_id": resolved_goal_id,
2085
+ "source_id": resolved_source_id,
2086
+ "remote_orphan_record_ids": [],
2087
+ "remote_duplicate_record_ids": [],
2088
+ "remote_delete_record_ids": [],
2089
+ "local_mapping_keys_to_remove": [],
2090
+ "remote_delete_count": 0,
2091
+ "local_mapping_delete_count": 0,
2092
+ }
2093
+ )
2094
+
2095
+ results: list[dict[str, Any]] = []
2096
+ ok = True
2097
+ for row in rows:
2098
+ row_goal_id = str(row.get("goal_id") or resolved_goal_id)
2099
+ todo_id = str(row.get("todo_id") or "").strip()
2100
+ key = f"{row_goal_id}:{todo_id}"
2101
+ values = _lark_record_from_projection_block(
2102
+ row,
2103
+ goal_id=row_goal_id,
2104
+ source_id=resolved_source_id,
2105
+ public_safe=public_safe,
2106
+ )
2107
+ result = _run_command(
2108
+ build_record_upsert_command(config, record_id=record_map.get(key), values=values),
2109
+ execute=execute,
2110
+ runner=runner,
2111
+ )
2112
+ commands.append(result)
2113
+ record_id = _extract_created_record_id(result.get("json")) or record_map.get(key)
2114
+ if execute and result.get("ok") and record_id:
2115
+ record_map[key] = record_id
2116
+ results.append(
2117
+ {
2118
+ "todo_id": todo_id,
2119
+ "record_id": record_id,
2120
+ "command": result,
2121
+ "values": values,
2122
+ }
2123
+ )
2124
+ ok = ok and bool(result.get("ok"))
2125
+ if execute and not result.get("ok"):
2126
+ break
2127
+
2128
+ executed_remote_delete_count = 0
2129
+ if ok and reconcile_source and reconcile_plan["remote_delete_record_ids"]:
2130
+ delete_result = _run_command(
2131
+ build_record_delete_command(
2132
+ config,
2133
+ record_ids=reconcile_plan["remote_delete_record_ids"],
2134
+ ),
2135
+ execute=execute,
2136
+ runner=runner,
2137
+ )
2138
+ commands.append(delete_result)
2139
+ ok = ok and bool(delete_result.get("ok"))
2140
+ if execute and delete_result.get("ok"):
2141
+ executed_remote_delete_count = reconcile_plan["remote_delete_count"]
2142
+
2143
+ removed_local_mapping_count = 0
2144
+ if execute and ok and reconcile_source:
2145
+ for key in reconcile_plan["local_mapping_keys_to_remove"]:
2146
+ if key in record_map:
2147
+ removed_local_mapping_count += 1
2148
+ record_map.pop(key, None)
2149
+
2150
+ if execute and ok:
2151
+ _persist_lark_todo_record_map(config, config_path=config_path, local=local, record_map=record_map)
2152
+
2153
+ return {
2154
+ "ok": ok,
2155
+ "schema_version": LARK_KANBAN_SYNC_PROJECTION_VERSION,
2156
+ "execute": execute,
2157
+ "goal_id": resolved_goal_id,
2158
+ "agent_id": agent_id,
2159
+ "source_id": resolved_source_id,
2160
+ "sink_visibility": sink_visibility,
2161
+ "public_safe_redaction": public_safe,
2162
+ "projection_schema_version": projection.get("schema_version"),
2163
+ "schema_check": schema_check,
2164
+ "row_count": len(rows),
2165
+ "source_reconcile": {
2166
+ **reconcile_plan,
2167
+ "requested": reconcile_source,
2168
+ "source_snapshot_complete": source_snapshot_complete,
2169
+ "mode": "execute" if reconcile_source and execute else (
2170
+ "preview" if reconcile_source else "disabled"
2171
+ ),
2172
+ "executed_remote_delete_count": executed_remote_delete_count,
2173
+ "removed_local_mapping_count": removed_local_mapping_count,
2174
+ },
2175
+ "records": results,
2176
+ "commands": commands,
2177
+ "warnings": warnings,
2178
+ "config_path": str(config_path) if config_path else None,
2179
+ }
2180
+
2181
+
2182
+ def sync_loopx_todos_to_lark_kanban(
2183
+ config: LarkKanbanConfig,
2184
+ *,
2185
+ registry_path: Path,
2186
+ goal_id: str,
2187
+ agent_id: str | None = None,
2188
+ config_path: Path | None = None,
2189
+ project: Path | None = None,
2190
+ state_file: Path | None = None,
2191
+ include_done: bool = False,
2192
+ limit: int = 50,
2193
+ execute: bool = False,
2194
+ runner: CommandRunner = default_subprocess_runner,
2195
+ ) -> dict[str, Any]:
2196
+ from ....capabilities.issue_fix.outcome_projection import (
2197
+ build_issue_fix_outcome_collection_from_domain_state,
2198
+ )
2199
+ from ....control_plane.todos.projection import todo_priority_label
2200
+ from ....todos import resolve_todo_state_path, section_bounds, todo_blocks
2201
+
2202
+ resolved_project, resolved_state_file = resolve_todo_state_path(
2203
+ registry_path=registry_path,
2204
+ goal_id=goal_id,
2205
+ project=project,
2206
+ state_file=state_file,
2207
+ )
2208
+ lines = resolved_state_file.read_text(encoding="utf-8").splitlines()
2209
+ todos: list[dict[str, Any]] = []
2210
+ for role in ("user", "agent"):
2211
+ bounds = section_bounds(lines, role)
2212
+ if not bounds:
2213
+ continue
2214
+ start, end, heading = bounds
2215
+ for block in todo_blocks(lines, start, end, role=role, source_section=heading):
2216
+ raw_status = str(block.get("status") or "").strip()
2217
+ status = normalize_todo_status(raw_status)
2218
+ if (block.get("done") or status == TODO_STATUS_DONE) and not include_done:
2219
+ continue
2220
+ candidate = {**block, "role": role, "source_section": heading}
2221
+ if not todo_matches_agent_scope(candidate, agent_id):
2222
+ continue
2223
+ todos.append(candidate)
2224
+ todos = todos[:limit]
2225
+
2226
+ outcome_projection = build_issue_fix_outcome_collection_from_domain_state(
2227
+ goal_id=goal_id,
2228
+ project=resolved_project or project or Path("."),
2229
+ )
2230
+ outcome_source_id, outcome_namespace_warnings = _projection_namespace(
2231
+ outcome_projection,
2232
+ None,
2233
+ )
2234
+ outcome_source_count = len(outcome_projection.get("issue_fix_outcomes") or [])
2235
+ _, outcome_rows, outcome_row_warnings = _projection_rows_from_payload(
2236
+ outcome_projection,
2237
+ goal_id=goal_id,
2238
+ agent_id=agent_id,
2239
+ source_id=outcome_source_id,
2240
+ include_done=True,
2241
+ limit=outcome_source_count,
2242
+ )
2243
+ outcome_warnings = [
2244
+ *outcome_namespace_warnings,
2245
+ *list(outcome_projection.get("warnings") or []),
2246
+ *outcome_row_warnings,
2247
+ ]
2248
+
2249
+ commands: list[dict[str, Any]] = []
2250
+ schema_check: dict[str, Any] | None = None
2251
+ if execute:
2252
+ schema_check = _inspect_lark_kanban_schema(
2253
+ config,
2254
+ commands=commands,
2255
+ runner=runner,
2256
+ )
2257
+ if not schema_check.get("ok"):
2258
+ return {
2259
+ "ok": False,
2260
+ "schema_version": "loopx_lark_kanban_sync_todos_v0",
2261
+ "execute": execute,
2262
+ "goal_id": goal_id,
2263
+ "agent_id": agent_id,
2264
+ "base_token": config.base_token,
2265
+ "table_id": config.table_id,
2266
+ "view_id": config.view_id,
2267
+ "project": str(resolved_project) if resolved_project else None,
2268
+ "state_file": str(resolved_state_file),
2269
+ "todo_count": len(todos),
2270
+ "issue_fix_outcome_count": len(outcome_rows),
2271
+ "limit_policy": {
2272
+ "todo_rows": limit,
2273
+ "issue_fix_outcomes": "complete_source_projection",
2274
+ },
2275
+ "issue_fix_source_counts": outcome_projection.get("source_counts"),
2276
+ "warnings": outcome_warnings,
2277
+ "records": [],
2278
+ "commands": commands,
2279
+ "config_path": str(config_path) if config_path else None,
2280
+ "schema_check": schema_check,
2281
+ "successful_write_count": 0,
2282
+ "external_write_performed": False,
2283
+ "error": _schema_check_error(schema_check),
2284
+ }
2285
+ local, record_map, _, _ = _load_lark_todo_record_map(
2286
+ config,
2287
+ config_path=config_path,
2288
+ execute=execute,
2289
+ inspect_remote=False,
2290
+ commands=commands,
2291
+ runner=runner,
2292
+ )
2293
+
2294
+ results: list[dict[str, Any]] = []
2295
+ successful_write_count = 0
2296
+ ok = True
2297
+ for block in todos:
2298
+ todo_id = str(block.get("todo_id") or "").strip()
2299
+ key = f"{goal_id}:{todo_id}"
2300
+ values = _lark_record_from_todo_block(
2301
+ block,
2302
+ goal_id=goal_id,
2303
+ state_file=resolved_state_file,
2304
+ priority=todo_priority_label(block, text_mode="prefix") or "P2",
2305
+ )
2306
+ result = _run_command(
2307
+ build_record_upsert_command(config, record_id=record_map.get(key), values=values),
2308
+ execute=execute,
2309
+ runner=runner,
2310
+ )
2311
+ commands.append(result)
2312
+ record_id = _extract_created_record_id(result.get("json")) or record_map.get(key)
2313
+ write_succeeded = bool(
2314
+ execute and result.get("executed") and result.get("ok")
2315
+ )
2316
+ if write_succeeded:
2317
+ successful_write_count += 1
2318
+ if execute and result.get("ok") and record_id:
2319
+ record_map[key] = record_id
2320
+ results.append(
2321
+ {
2322
+ "todo_id": todo_id,
2323
+ "record_id": record_id,
2324
+ "command": result,
2325
+ "values": values,
2326
+ }
2327
+ )
2328
+ ok = ok and bool(result.get("ok"))
2329
+ if execute and not result.get("ok"):
2330
+ break
2331
+
2332
+ if ok:
2333
+ for row in outcome_rows:
2334
+ row_goal_id = str(row.get("goal_id") or goal_id)
2335
+ todo_id = str(row.get("todo_id") or "").strip()
2336
+ key = f"{row_goal_id}:{todo_id}"
2337
+ values = _lark_record_from_projection_block(
2338
+ row,
2339
+ goal_id=row_goal_id,
2340
+ source_id=outcome_source_id,
2341
+ )
2342
+ result = _run_command(
2343
+ build_record_upsert_command(
2344
+ config,
2345
+ record_id=record_map.get(key),
2346
+ values=values,
2347
+ ),
2348
+ execute=execute,
2349
+ runner=runner,
2350
+ )
2351
+ commands.append(result)
2352
+ record_id = (
2353
+ _extract_created_record_id(result.get("json")) or record_map.get(key)
2354
+ )
2355
+ if execute and result.get("ok") and record_id:
2356
+ record_map[key] = record_id
2357
+ write_succeeded = bool(
2358
+ execute and result.get("executed") and result.get("ok")
2359
+ )
2360
+ if write_succeeded:
2361
+ successful_write_count += 1
2362
+ results.append(
2363
+ {
2364
+ "todo_id": todo_id,
2365
+ "record_id": record_id,
2366
+ "command": result,
2367
+ "values": values,
2368
+ }
2369
+ )
2370
+ ok = ok and bool(result.get("ok"))
2371
+ if execute and not result.get("ok"):
2372
+ break
2373
+
2374
+ if execute and successful_write_count:
2375
+ _persist_lark_todo_record_map(config, config_path=config_path, local=local, record_map=record_map)
2376
+
2377
+ return {
2378
+ "ok": ok,
2379
+ "schema_version": "loopx_lark_kanban_sync_todos_v0",
2380
+ "execute": execute,
2381
+ "goal_id": goal_id,
2382
+ "agent_id": agent_id,
2383
+ "base_token": config.base_token,
2384
+ "table_id": config.table_id,
2385
+ "view_id": config.view_id,
2386
+ "project": str(resolved_project) if resolved_project else None,
2387
+ "state_file": str(resolved_state_file),
2388
+ "todo_count": len(todos),
2389
+ "issue_fix_outcome_count": len(outcome_rows),
2390
+ "limit_policy": {
2391
+ "todo_rows": limit, "issue_fix_outcomes": "complete_source_projection",
2392
+ },
2393
+ "issue_fix_source_counts": outcome_projection.get("source_counts"),
2394
+ "schema_check": schema_check,
2395
+ "warnings": outcome_warnings,
2396
+ "records": results,
2397
+ "commands": commands,
2398
+ "config_path": str(config_path) if config_path else None,
2399
+ "successful_write_count": successful_write_count,
2400
+ "external_write_performed": bool(successful_write_count),
2401
+ }
2402
+
2403
+
2404
+ def _load_lark_todo_record_map(
2405
+ config: LarkKanbanConfig, *, config_path: Path | None, execute: bool,
2406
+ inspect_remote: bool,
2407
+ commands: list[dict[str, Any]], runner: CommandRunner,
2408
+ ) -> tuple[dict[str, Any], dict[str, str], list[dict[str, str]], bool | None]:
2409
+ local = read_lark_kanban_local_config(config_path) if config_path else {}
2410
+ record_map = _todo_record_map_from_local_config(local)
2411
+ if not execute and not inspect_remote:
2412
+ return local, record_map, [], None
2413
+
2414
+ list_config = LarkKanbanConfig(
2415
+ **{"base_" + "token": config.base_token},
2416
+ table_id=config.table_id,
2417
+ view_id=None,
2418
+ cli_bin=config.cli_bin,
2419
+ identity=config.identity,
2420
+ )
2421
+ list_result = _run_command(build_record_list_command(list_config), execute=True, runner=runner)
2422
+ commands.append(list_result)
2423
+ remote_records: list[dict[str, str]] = []
2424
+ remote_complete = False
2425
+ if list_result.get("ok"):
2426
+ parsed_records = list_result.get("json")
2427
+ remote_records = todo_record_entries(parsed_records)
2428
+ remote_by_key: dict[str, list[str]] = {}
2429
+ for item in remote_records:
2430
+ remote_by_key.setdefault(item["key"], []).append(item["record_id"])
2431
+ if todo_record_list_is_authoritative(parsed_records):
2432
+ record_map = {
2433
+ key: record_id
2434
+ for key, record_id in record_map.items()
2435
+ if record_id in remote_by_key.get(key, [])
2436
+ }
2437
+ for key, record_ids in remote_by_key.items():
2438
+ preferred_record_id = record_map.get(key)
2439
+ record_map[key] = (
2440
+ preferred_record_id
2441
+ if preferred_record_id in record_ids
2442
+ else sorted(record_ids)[0]
2443
+ )
2444
+ remote_complete = record_list_is_complete(parsed_records)
2445
+ return local, record_map, remote_records, remote_complete
2446
+
2447
+
2448
+ def _todo_record_map_from_local_config(local: dict[str, Any]) -> dict[str, str]:
2449
+ records = local.get("todo_records") if isinstance(local, dict) else None
2450
+ if not isinstance(records, dict):
2451
+ return {}
2452
+ return {str(key): str(value) for key, value in records.items() if str(key).strip() and str(value).strip()}
2453
+
2454
+
2455
+ def _persist_lark_todo_record_map(
2456
+ config: LarkKanbanConfig, *, config_path: Path | None,
2457
+ local: dict[str, Any], record_map: dict[str, str],
2458
+ ) -> None:
2459
+ if not config_path:
2460
+ return
2461
+ board = local.get("board") if isinstance(local.get("board"), dict) else {}
2462
+ if not board:
2463
+ board = {
2464
+ "base_token": config.base_token,
2465
+ "table_id": config.table_id,
2466
+ "view_id": config.view_id,
2467
+ "cli_bin": config.cli_bin,
2468
+ "identity": config.identity,
2469
+ }
2470
+ payload = {"schema_version": LARK_KANBAN_LOCAL_CONFIG_VERSION, "board": board, "todo_records": record_map}
2471
+ write_lark_kanban_local_config(config_path, payload)
2472
+
2473
+
2474
+ def _lark_record_from_todo_block(
2475
+ block: dict[str, Any],
2476
+ *,
2477
+ goal_id: str,
2478
+ state_file: Path,
2479
+ priority: str,
2480
+ ) -> dict[str, Any]:
2481
+ role = str(block.get("role") or "agent")
2482
+ raw_status = str(block.get("status") or "").strip()
2483
+ status = normalize_todo_status(raw_status) or TODO_STATUS_OPEN
2484
+ task_class = normalize_explicit_todo_task_class(block.get("task_class")) or (
2485
+ TODO_TASK_CLASS_USER_GATE if role == "user" else TODO_TASK_CLASS_ADVANCEMENT
2486
+ )
2487
+ claimed_by = str(block.get("claimed_by") or "").strip()
2488
+ lark_status = STATUS_TODO
2489
+ if block.get("done") or status == TODO_STATUS_DONE:
2490
+ lark_status = STATUS_DONE
2491
+ elif status == TODO_STATUS_BLOCKED or task_class == TODO_TASK_CLASS_BLOCKER:
2492
+ lark_status = STATUS_BLOCKED
2493
+ elif role == "user" or task_class == TODO_TASK_CLASS_USER_GATE or status == TODO_STATUS_DEFERRED:
2494
+ lark_status = STATUS_USER_GATE
2495
+ elif claimed_by:
2496
+ lark_status = STATUS_CLAIMED
2497
+ claim = CLAIM_UNCLAIMED
2498
+ if lark_status == STATUS_USER_GATE:
2499
+ claim = CLAIM_HUMAN
2500
+ elif claimed_by:
2501
+ claim = CLAIM_AGENT
2502
+ scope = block.get("required_write_scopes")
2503
+ if isinstance(scope, list):
2504
+ scope_text = ", ".join(str(item) for item in scope)
2505
+ else:
2506
+ scope_text = str(scope or "")
2507
+ evidence = str(block.get("evidence") or block.get("reason") or block.get("note") or "")
2508
+ text = str(block.get("text") or "")
2509
+ return {
2510
+ "Task": text,
2511
+ "Status": lark_status,
2512
+ "Claim": claim,
2513
+ "Claimed By": claimed_by,
2514
+ "Priority": priority if priority in {"P0", "P1", "P2", "P3"} else "P2",
2515
+ "Task Class": task_class,
2516
+ "Action Kind": str(block.get("action_kind") or "sync_loopx_todo"),
2517
+ "LoopX Goal ID": goal_id,
2518
+ "LoopX Todo ID": str(block.get("todo_id") or ""),
2519
+ **issue_fix_surface.todo_record_values(),
2520
+ "Scope": scope_text,
2521
+ "User Gate": text if lark_status == STATUS_USER_GATE else "",
2522
+ "Handoff": f"Synced from LoopX active state: {state_file.name}",
2523
+ "Evidence": evidence,
2524
+ "Run History": f"synced from LoopX todo status={status}",
2525
+ "Worker Command": "",
2526
+ "Workdir": "",
2527
+ "Last Error": "",
2528
+ "Last Result Code": None,
2529
+ }
2530
+
2531
+
2532
+ def seed_lark_kanban_task(
2533
+ config: LarkKanbanConfig,
2534
+ *,
2535
+ task: dict[str, Any],
2536
+ execute: bool = False,
2537
+ runner: CommandRunner = default_subprocess_runner,
2538
+ ) -> dict[str, Any]:
2539
+ result = _run_command(
2540
+ build_record_upsert_command(config, record_id=None, values=task),
2541
+ execute=execute,
2542
+ runner=runner,
2543
+ )
2544
+ record_id = _extract_created_record_id(result.get("json"))
2545
+ return {
2546
+ "ok": bool(result.get("ok")),
2547
+ "schema_version": LARK_KANBAN_SCHEMA_VERSION,
2548
+ "record_id": record_id,
2549
+ "command": result,
2550
+ "task": task,
2551
+ }
2552
+
2553
+
2554
+ def seed_lark_kanban_records(
2555
+ config: LarkKanbanConfig,
2556
+ *,
2557
+ records: list[dict[str, Any]],
2558
+ execute: bool = False,
2559
+ runner: CommandRunner = default_subprocess_runner,
2560
+ ) -> dict[str, Any]:
2561
+ results: list[dict[str, Any]] = []
2562
+ ok = True
2563
+ for record in records:
2564
+ result = seed_lark_kanban_task(
2565
+ config,
2566
+ task=record,
2567
+ execute=execute,
2568
+ runner=runner,
2569
+ )
2570
+ results.append(result)
2571
+ ok = ok and bool(result.get("ok"))
2572
+ if execute and not result.get("ok"):
2573
+ break
2574
+ return {
2575
+ "ok": ok,
2576
+ "schema_version": LARK_KANBAN_SCHEMA_VERSION,
2577
+ "record_count": len(records),
2578
+ "created_record_ids": [item.get("record_id") for item in results],
2579
+ "records": results,
2580
+ }
2581
+
2582
+ def lark_kanban_heartbeat(
2583
+ config: LarkKanbanConfig,
2584
+ *,
2585
+ agent_id: str = DEFAULT_AGENT_ID,
2586
+ fixture: dict[str, Any] | None = None,
2587
+ worker_command: str | None = None,
2588
+ execute_lark: bool = False,
2589
+ execute_worker: bool = False,
2590
+ complete_on_success: bool = False,
2591
+ allowed_command_prefixes: list[str] | None = None,
2592
+ runner: CommandRunner = default_subprocess_runner,
2593
+ now: datetime | None = None,
2594
+ worker_timeout_seconds: float = 600.0,
2595
+ ) -> dict[str, Any]:
2596
+ commands: list[dict[str, Any]] = []
2597
+ if fixture is None:
2598
+ list_result = _run_command(
2599
+ build_record_list_command(config),
2600
+ execute=execute_lark,
2601
+ runner=runner,
2602
+ )
2603
+ commands.append(list_result)
2604
+ if execute_lark and not list_result.get("ok"):
2605
+ return _heartbeat_payload(
2606
+ ok=False,
2607
+ decision="record_list_failed",
2608
+ agent_id=agent_id,
2609
+ commands=commands,
2610
+ records=[],
2611
+ )
2612
+ parsed = list_result.get("json") if execute_lark else {"data": {"fields": [], "data": []}}
2613
+ else:
2614
+ parsed = fixture
2615
+ records = lark_record_rows(parsed if isinstance(parsed, dict) else {})
2616
+ task = choose_heartbeat_task(records, agent_id=agent_id)
2617
+ if task is None:
2618
+ return _heartbeat_payload(
2619
+ ok=True,
2620
+ decision="no_claimable_task",
2621
+ agent_id=agent_id,
2622
+ commands=commands,
2623
+ records=records,
2624
+ )
2625
+ record_id = str(task.get("_record_id") or "").strip()
2626
+ if not record_id:
2627
+ return _heartbeat_payload(
2628
+ ok=False,
2629
+ decision="selected_task_missing_record_id",
2630
+ agent_id=agent_id,
2631
+ commands=commands,
2632
+ records=records,
2633
+ selected_task=task,
2634
+ )
2635
+ timestamp = now_lark_datetime(now)
2636
+ claim_values = {
2637
+ "Status": STATUS_CLAIMED,
2638
+ "Claim": CLAIM_AGENT,
2639
+ "Claimed By": agent_id,
2640
+ "Last Heartbeat": timestamp,
2641
+ "Run History": append_history(task.get("Run History"), f"{timestamp} {agent_id}: claimed"),
2642
+ }
2643
+ claim_result = _run_command(
2644
+ build_record_upsert_command(config, record_id=record_id, values=claim_values),
2645
+ execute=execute_lark,
2646
+ runner=runner,
2647
+ )
2648
+ commands.append(claim_result)
2649
+ if execute_lark and not claim_result.get("ok"):
2650
+ return _heartbeat_payload(
2651
+ ok=False,
2652
+ decision="claim_failed",
2653
+ agent_id=agent_id,
2654
+ commands=commands,
2655
+ records=records,
2656
+ selected_task=task,
2657
+ )
2658
+ command = (worker_command if worker_command is not None else str(task.get("Worker Command") or "")).strip()
2659
+ if not command:
2660
+ worker = {
2661
+ "attempted": False,
2662
+ "executed": False,
2663
+ "ok": False,
2664
+ "reason": "worker_command_missing",
2665
+ "command": "",
2666
+ }
2667
+ final_status = STATUS_USER_GATE
2668
+ evidence = "worker command missing; user must provide Worker Command or heartbeat --worker-command"
2669
+ result_code = None
2670
+ elif not execute_worker:
2671
+ worker = {
2672
+ "attempted": True,
2673
+ "executed": False,
2674
+ "ok": True,
2675
+ "reason": "execute_worker_not_set",
2676
+ "command": command,
2677
+ }
2678
+ final_status = STATUS_CLAIMED
2679
+ evidence = f"dry-run worker command: {command}"
2680
+ result_code = None
2681
+ elif not allowed_worker_command(command, allowed_command_prefixes or []):
2682
+ worker = {
2683
+ "attempted": True,
2684
+ "executed": False,
2685
+ "ok": False,
2686
+ "reason": "worker_command_prefix_not_allowed",
2687
+ "command": command,
2688
+ "allowed_prefixes": allowed_command_prefixes or [],
2689
+ }
2690
+ final_status = STATUS_BLOCKED
2691
+ evidence = "worker command blocked by prefix allowlist"
2692
+ result_code = None
2693
+ else:
2694
+ workdir = Path(str(task.get("Workdir") or ".")).expanduser()
2695
+ worker_args = shlex.split(command)
2696
+ worker_result = _run_command(
2697
+ worker_args,
2698
+ execute=True,
2699
+ runner=runner,
2700
+ cwd=workdir,
2701
+ timeout_seconds=worker_timeout_seconds,
2702
+ )
2703
+ worker = {
2704
+ "attempted": True,
2705
+ "executed": True,
2706
+ "ok": bool(worker_result.get("ok")),
2707
+ "reason": "completed" if worker_result.get("ok") else "failed",
2708
+ "command": command,
2709
+ "returncode": worker_result.get("returncode"),
2710
+ "stdout": _compact_text(worker_result.get("stdout"), limit=OUTPUT_LIMIT),
2711
+ "stderr": _compact_text(worker_result.get("stderr"), limit=OUTPUT_LIMIT),
2712
+ }
2713
+ final_status = STATUS_DONE if worker_result.get("ok") and complete_on_success else STATUS_REVIEW
2714
+ if not worker_result.get("ok"):
2715
+ final_status = STATUS_BLOCKED
2716
+ evidence = _compact_text(
2717
+ worker.get("stdout") or worker.get("stderr") or worker.get("reason"),
2718
+ limit=OUTPUT_LIMIT,
2719
+ )
2720
+ result_code = worker_result.get("returncode")
2721
+ finish_values = {
2722
+ "Status": final_status,
2723
+ "Claim": CLAIM_AGENT,
2724
+ "Claimed By": agent_id,
2725
+ "Last Heartbeat": timestamp,
2726
+ "Evidence": evidence,
2727
+ "Last Result Code": result_code,
2728
+ "Last Error": "" if worker.get("ok") else _compact_text(worker.get("stderr") or worker.get("reason")),
2729
+ "Run History": append_history(
2730
+ claim_values["Run History"],
2731
+ f"{timestamp} {agent_id}: worker {worker.get('reason')} -> {final_status}",
2732
+ ),
2733
+ "Handoff": _handoff_for_status(final_status, agent_id=agent_id),
2734
+ }
2735
+ finish_result = _run_command(
2736
+ build_record_upsert_command(config, record_id=record_id, values=finish_values),
2737
+ execute=execute_lark,
2738
+ runner=runner,
2739
+ )
2740
+ commands.append(finish_result)
2741
+ ok = bool(worker.get("ok")) and (not execute_lark or finish_result.get("ok"))
2742
+ return _heartbeat_payload(
2743
+ ok=ok,
2744
+ decision="task_processed",
2745
+ agent_id=agent_id,
2746
+ commands=commands,
2747
+ records=records,
2748
+ selected_task=task,
2749
+ worker=worker,
2750
+ final_status=final_status,
2751
+ writeback=finish_values,
2752
+ )
2753
+
2754
+
2755
+ def _handoff_for_status(status: str, *, agent_id: str) -> str:
2756
+ if status == STATUS_DONE:
2757
+ return f"{agent_id} completed the task; review evidence and close any external issue/PR bookkeeping."
2758
+ if status == STATUS_REVIEW:
2759
+ return f"{agent_id} produced evidence; human/controller review should decide merge or next todo."
2760
+ if status == STATUS_USER_GATE:
2761
+ return "User input is required before this task can proceed."
2762
+ if status == STATUS_BLOCKED:
2763
+ return f"{agent_id} hit a blocker; inspect Last Error and decide repair/retry/reassign."
2764
+ return f"{agent_id} claimed the task; heartbeat can continue."
2765
+
2766
+
2767
+ def _heartbeat_payload(
2768
+ *,
2769
+ ok: bool,
2770
+ decision: str,
2771
+ agent_id: str,
2772
+ commands: list[dict[str, Any]],
2773
+ records: list[dict[str, Any]],
2774
+ selected_task: dict[str, Any] | None = None,
2775
+ worker: dict[str, Any] | None = None,
2776
+ final_status: str | None = None,
2777
+ writeback: dict[str, Any] | None = None,
2778
+ ) -> dict[str, Any]:
2779
+ return {
2780
+ "ok": ok,
2781
+ "schema_version": LARK_KANBAN_HEARTBEAT_VERSION,
2782
+ "agent_id": agent_id,
2783
+ "decision": decision,
2784
+ "record_count": len(records),
2785
+ "selected_record_id": selected_task.get("_record_id") if selected_task else None,
2786
+ "selected_task": selected_task,
2787
+ "worker": worker,
2788
+ "final_status": final_status,
2789
+ "writeback": writeback,
2790
+ "commands": commands,
2791
+ }