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.
- loopx/__init__.py +5 -0
- loopx/agent_onboarding.py +654 -0
- loopx/agent_registry.py +112 -0
- loopx/ark_managed_agent_host.py +59 -0
- loopx/authority.py +805 -0
- loopx/benchmark.py +2875 -0
- loopx/benchmark_adapters/__init__.py +1 -0
- loopx/benchmark_adapters/agentissue.py +2644 -0
- loopx/benchmark_adapters/agents_last_exam.py +3998 -0
- loopx/benchmark_adapters/edgebench.py +322 -0
- loopx/benchmark_adapters/skillsbench.py +5978 -0
- loopx/benchmark_adapters/skillsbench_acp_failure_policy.py +143 -0
- loopx/benchmark_adapters/skillsbench_acp_process.py +31 -0
- loopx/benchmark_adapters/skillsbench_acp_relay.py +4832 -0
- loopx/benchmark_adapters/skillsbench_batch.py +124 -0
- loopx/benchmark_adapters/skillsbench_bridge_guard.py +209 -0
- loopx/benchmark_adapters/skillsbench_bridge_summary.py +203 -0
- loopx/benchmark_adapters/skillsbench_codex_goal_recovery.py +271 -0
- loopx/benchmark_adapters/skillsbench_codex_goal_trace.py +81 -0
- loopx/benchmark_adapters/skillsbench_codex_runtime.py +339 -0
- loopx/benchmark_adapters/skillsbench_dockerfile_runtime.py +467 -0
- loopx/benchmark_adapters/skillsbench_failure_signals.py +652 -0
- loopx/benchmark_adapters/skillsbench_proxy_runtime.py +327 -0
- loopx/benchmark_adapters/skillsbench_remote_bridge.py +402 -0
- loopx/benchmark_adapters/skillsbench_result_discovery.py +143 -0
- loopx/benchmark_adapters/skillsbench_runner_profile.py +436 -0
- loopx/benchmark_adapters/skillsbench_runner_source.py +99 -0
- loopx/benchmark_adapters/skillsbench_setup_preflight.py +771 -0
- loopx/benchmark_adapters/skillsbench_signals.py +15 -0
- loopx/benchmark_adapters/skillsbench_task_source.py +141 -0
- loopx/benchmark_adapters/skillsbench_turn_route.py +723 -0
- loopx/benchmark_adapters/skillsbench_turn_runtime.py +1069 -0
- loopx/benchmark_adapters/skillsbench_typed_repair.py +689 -0
- loopx/benchmark_adapters/skillsbench_uv_cache.py +111 -0
- loopx/benchmark_adapters/skillsbench_verifier_bootstrap.py +227 -0
- loopx/benchmark_adapters/skillsbench_verifier_cache.py +138 -0
- loopx/benchmark_adapters/terminal_bench.py +10078 -0
- loopx/benchmark_case_analysis.py +1276 -0
- loopx/benchmark_case_state.py +1079 -0
- loopx/benchmark_core/__init__.py +239 -0
- loopx/benchmark_core/adapter.py +84 -0
- loopx/benchmark_core/artifacts.py +517 -0
- loopx/benchmark_core/attempts.py +199 -0
- loopx/benchmark_core/container_exec.py +216 -0
- loopx/benchmark_core/io.py +68 -0
- loopx/benchmark_core/lifecycle.py +211 -0
- loopx/benchmark_core/loop_protocol.py +689 -0
- loopx/benchmark_core/observable_handles.py +348 -0
- loopx/benchmark_core/parity.py +256 -0
- loopx/benchmark_core/remote_closeout.py +482 -0
- loopx/benchmark_core/rounds.py +215 -0
- loopx/benchmark_core/route_profile.py +509 -0
- loopx/benchmark_core/run_permissions.py +206 -0
- loopx/benchmark_core/split_control.py +925 -0
- loopx/benchmark_core/turn_fidelity.py +326 -0
- loopx/benchmark_ledger.py +3793 -0
- loopx/benchmark_ledger_countability.py +372 -0
- loopx/benchmark_ledger_current.py +724 -0
- loopx/benchmark_trajectory.py +405 -0
- loopx/benchmarks/__init__.py +1 -0
- loopx/benchmarks/qualification/__init__.py +1 -0
- loopx/benchmarks/qualification/release_outcome_baseline.py +360 -0
- loopx/benchmarks/read_models/__init__.py +1 -0
- loopx/benchmarks/read_models/benchmark_attempt_accounting.py +53 -0
- loopx/benchmarks/read_models/benchmark_comparison.py +414 -0
- loopx/benchmarks/read_models/benchmark_event_timeline.py +113 -0
- loopx/benchmarks/read_models/benchmark_experiment_report.py +475 -0
- loopx/benchmarks/read_models/benchmark_learning_ledger.py +137 -0
- loopx/benchmarks/read_models/benchmark_lifecycle_contracts.py +228 -0
- loopx/benchmarks/read_models/benchmark_projection.py +723 -0
- loopx/benchmarks/read_models/benchmark_result.py +146 -0
- loopx/benchmarks/read_models/benchmark_run_execution_contract.py +116 -0
- loopx/benchmarks/read_models/benchmark_run_failure.py +157 -0
- loopx/benchmarks/read_models/benchmark_run_metrics.py +213 -0
- loopx/benchmarks/read_models/benchmark_run_post_execution.py +635 -0
- loopx/benchmarks/read_models/benchmark_run_pre_execution.py +541 -0
- loopx/benchmarks/read_models/benchmark_status_compaction.py +1255 -0
- loopx/benchmarks/read_models/benchmark_status_runner.py +780 -0
- loopx/benchmarks/read_models/goal_start_control_score.py +857 -0
- loopx/benchmarks/read_models/skillsbench_post_run_debug.py +746 -0
- loopx/benchmarks/read_models/skillsbench_verifier_attribution.py +269 -0
- loopx/bootstrap.py +1116 -0
- loopx/bootstrap_command_pack.py +2167 -0
- loopx/boundary_authority.py +199 -0
- loopx/canary/__init__.py +1 -0
- loopx/canary/maintainability_ratchet.py +800 -0
- loopx/canary/planner.py +1984 -0
- loopx/canary/premerge.py +1130 -0
- loopx/canary/qualification_profiles.py +309 -0
- loopx/canary/quality_surface_catalog.py +838 -0
- loopx/canary/release_profiles.py +51 -0
- loopx/canary/runner.py +1107 -0
- loopx/canary/smoke_health.py +581 -0
- loopx/canary/smoke_profiles.py +212 -0
- loopx/capabilities/__init__.py +0 -0
- loopx/capabilities/agent_turn_recall/__init__.py +17 -0
- loopx/capabilities/agent_turn_recall/cli.py +369 -0
- loopx/capabilities/agent_turn_recall/core.py +296 -0
- loopx/capabilities/auto_research/__init__.py +16 -0
- loopx/capabilities/auto_research/bootstrap_contract.py +157 -0
- loopx/capabilities/auto_research/cli.py +1468 -0
- loopx/capabilities/auto_research/core.py +11 -0
- loopx/capabilities/auto_research/defaults.py +79 -0
- loopx/capabilities/auto_research/demo_e2e.py +1848 -0
- loopx/capabilities/auto_research/demo_supervisor.py +186 -0
- loopx/capabilities/auto_research/evidence_packet.py +767 -0
- loopx/capabilities/auto_research/human_view.py +794 -0
- loopx/capabilities/auto_research/kernel.py +191 -0
- loopx/capabilities/auto_research/knn_demo_workspace.py +322 -0
- loopx/capabilities/auto_research/live_evidence.py +248 -0
- loopx/capabilities/auto_research/preset.py +176 -0
- loopx/capabilities/auto_research/research_state.py +1085 -0
- loopx/capabilities/auto_research/role_profiles.py +394 -0
- loopx/capabilities/auto_research/rollout_append.py +97 -0
- loopx/capabilities/auto_research/terminal_result_contract.py +422 -0
- loopx/capabilities/auto_research/terminal_result_projection.py +171 -0
- loopx/capabilities/auto_research/terminal_result_query.py +233 -0
- loopx/capabilities/auto_research/terminal_results.py +349 -0
- loopx/capabilities/auto_research/user_contract.py +190 -0
- loopx/capabilities/auto_research/worker_loop.py +163 -0
- loopx/capabilities/auto_research/worker_runtime.py +777 -0
- loopx/capabilities/auto_research/worker_skill/SKILL.md +343 -0
- loopx/capabilities/benchmark_toolkit/__init__.py +19 -0
- loopx/capabilities/benchmark_toolkit/integrity.py +387 -0
- loopx/capabilities/catalog.py +1875 -0
- loopx/capabilities/change_quality/__init__.py +19 -0
- loopx/capabilities/change_quality/cli.py +171 -0
- loopx/capabilities/change_quality/context.py +156 -0
- loopx/capabilities/change_quality/oracles.py +269 -0
- loopx/capabilities/change_quality/policy.py +34 -0
- loopx/capabilities/change_quality/receipt.py +482 -0
- loopx/capabilities/change_quality/result.py +493 -0
- loopx/capabilities/change_quality/scope.py +171 -0
- loopx/capabilities/change_quality/shadow.py +680 -0
- loopx/capabilities/content_ops/__init__.py +0 -0
- loopx/capabilities/content_ops/cli.py +649 -0
- loopx/capabilities/content_ops/connector_packets.py +164 -0
- loopx/capabilities/content_ops/item_lifecycle.py +1000 -0
- loopx/capabilities/content_ops/layout.py +451 -0
- loopx/capabilities/content_ops/markdown.py +456 -0
- loopx/capabilities/content_ops/schemas.py +51 -0
- loopx/capabilities/content_ops/social_browser_x.py +107 -0
- loopx/capabilities/content_ops/surface.py +1956 -0
- loopx/capabilities/content_ops/templates/layout-catalog-v0.json +72 -0
- loopx/capabilities/context_providers/__init__.py +36 -0
- loopx/capabilities/context_providers/base.py +189 -0
- loopx/capabilities/context_providers/factory.py +32 -0
- loopx/capabilities/context_providers/openviking.py +702 -0
- loopx/capabilities/context_providers/service_ownership.py +185 -0
- loopx/capabilities/decision_context/__init__.py +129 -0
- loopx/capabilities/decision_context/architecture.py +83 -0
- loopx/capabilities/decision_context/assembler.py +849 -0
- loopx/capabilities/decision_context/catalog_entry.py +195 -0
- loopx/capabilities/decision_context/cli.py +310 -0
- loopx/capabilities/decision_context/cursor_commit.py +535 -0
- loopx/capabilities/decision_context/outcome_feedback.py +352 -0
- loopx/capabilities/decision_context/packets.py +654 -0
- loopx/capabilities/decision_context/private_state.py +189 -0
- loopx/capabilities/decision_context/profile.py +453 -0
- loopx/capabilities/decision_context/providers.py +228 -0
- loopx/capabilities/decision_context/review_settlement.py +136 -0
- loopx/capabilities/decision_context/runtime.py +273 -0
- loopx/capabilities/decision_context/sources.py +415 -0
- loopx/capabilities/explore/__init__.py +1 -0
- loopx/capabilities/explore/activation.py +198 -0
- loopx/capabilities/explore/adaptive_replay_planner.py +221 -0
- loopx/capabilities/explore/child_replay_runtime.py +463 -0
- loopx/capabilities/explore/composition_frontier.py +291 -0
- loopx/capabilities/explore/counterfactual_runtime.py +578 -0
- loopx/capabilities/explore/episode_runtime.py +647 -0
- loopx/capabilities/explore/harness_checkpoint.py +171 -0
- loopx/capabilities/explore/harness_gate.py +115 -0
- loopx/capabilities/explore/harness_runtime.py +1124 -0
- loopx/capabilities/explore/replay_metrics.py +206 -0
- loopx/capabilities/explore/replay_runtime.py +1271 -0
- loopx/capabilities/explore/resource_portfolio.py +173 -0
- loopx/capabilities/explore/result_log.py +974 -0
- loopx/capabilities/explore/router_state.py +432 -0
- loopx/capabilities/explore/source_history_reconcile.py +255 -0
- loopx/capabilities/explore/speculative_scheduler.py +498 -0
- loopx/capabilities/explore/todo_branch_plan.py +650 -0
- loopx/capabilities/explore/todo_evidence.py +141 -0
- loopx/capabilities/explore/trace_runtime.py +284 -0
- loopx/capabilities/explore/worker_branch_plan.py +1257 -0
- loopx/capabilities/integration_branch/__init__.py +13 -0
- loopx/capabilities/integration_branch/cli.py +148 -0
- loopx/capabilities/integration_branch/core.py +916 -0
- loopx/capabilities/issue_fix/__init__.py +19 -0
- loopx/capabilities/issue_fix/acceptance_loop.py +1050 -0
- loopx/capabilities/issue_fix/candidate_evidence.py +503 -0
- loopx/capabilities/issue_fix/candidate_preflight.py +676 -0
- loopx/capabilities/issue_fix/cli.py +1822 -0
- loopx/capabilities/issue_fix/cli_input.py +87 -0
- loopx/capabilities/issue_fix/content_ops_cli.py +148 -0
- loopx/capabilities/issue_fix/discovered_issue_promotion.py +947 -0
- loopx/capabilities/issue_fix/explore_projection.py +710 -0
- loopx/capabilities/issue_fix/feasibility.py +542 -0
- loopx/capabilities/issue_fix/github_public.py +661 -0
- loopx/capabilities/issue_fix/intake_surface.py +832 -0
- loopx/capabilities/issue_fix/metadata_preview.py +218 -0
- loopx/capabilities/issue_fix/metrics_projection.py +1340 -0
- loopx/capabilities/issue_fix/metrics_supplement.py +634 -0
- loopx/capabilities/issue_fix/metrics_supplement_cli.py +127 -0
- loopx/capabilities/issue_fix/outcome_projection.py +1235 -0
- loopx/capabilities/issue_fix/periodic_report.py +189 -0
- loopx/capabilities/issue_fix/pr_description.py +418 -0
- loopx/capabilities/issue_fix/pr_gate_reconcile.py +496 -0
- loopx/capabilities/issue_fix/pr_gate_reconcile_cli.py +464 -0
- loopx/capabilities/issue_fix/pr_lifecycle.py +1327 -0
- loopx/capabilities/issue_fix/pr_lifecycle_rollout.py +85 -0
- loopx/capabilities/issue_fix/pr_monitor_materialization.py +257 -0
- loopx/capabilities/issue_fix/pr_review_ack.py +439 -0
- loopx/capabilities/issue_fix/provider_hooks.py +24 -0
- loopx/capabilities/issue_fix/repository_commit_evidence.py +186 -0
- loopx/capabilities/issue_fix/repository_context.py +457 -0
- loopx/capabilities/issue_fix/repository_memory.py +459 -0
- loopx/capabilities/issue_fix/repository_memory_provider.py +1454 -0
- loopx/capabilities/issue_fix/repository_snapshot.py +454 -0
- loopx/capabilities/issue_fix/reviewer_cli.py +917 -0
- loopx/capabilities/issue_fix/reviewer_notification.py +882 -0
- loopx/capabilities/issue_fix/reviewer_notification_drain.py +942 -0
- loopx/capabilities/issue_fix/reviewer_recommendation.py +1057 -0
- loopx/capabilities/issue_fix/reviewer_request.py +1282 -0
- loopx/capabilities/issue_fix/reward_memory.py +879 -0
- loopx/capabilities/issue_fix/workflow_plan.py +1286 -0
- loopx/capabilities/material_lifecycle/__init__.py +161 -0
- loopx/capabilities/material_lifecycle/_validation.py +183 -0
- loopx/capabilities/material_lifecycle/apply.py +672 -0
- loopx/capabilities/material_lifecycle/architecture.py +122 -0
- loopx/capabilities/material_lifecycle/cli.py +161 -0
- loopx/capabilities/material_lifecycle/decision_planning.py +470 -0
- loopx/capabilities/material_lifecycle/explore_execution.py +306 -0
- loopx/capabilities/material_lifecycle/intake.py +869 -0
- loopx/capabilities/material_lifecycle/inventory.py +147 -0
- loopx/capabilities/material_lifecycle/lifecycle.py +98 -0
- loopx/capabilities/material_lifecycle/preparation.py +147 -0
- loopx/capabilities/material_lifecycle/project_skill.py +83 -0
- loopx/capabilities/material_lifecycle/ranking.py +267 -0
- loopx/capabilities/material_lifecycle/readable_projection.py +500 -0
- loopx/capabilities/material_lifecycle/rebuild.py +480 -0
- loopx/capabilities/material_lifecycle/settlement.py +238 -0
- loopx/capabilities/periodic_report/__init__.py +71 -0
- loopx/capabilities/periodic_report/adapters.py +939 -0
- loopx/capabilities/periodic_report/archive.py +422 -0
- loopx/capabilities/periodic_report/bindings.py +705 -0
- loopx/capabilities/periodic_report/cli.py +277 -0
- loopx/capabilities/periodic_report/core.py +691 -0
- loopx/capabilities/periodic_report/extension_envelope.py +66 -0
- loopx/capabilities/periodic_report/presets.py +103 -0
- loopx/capabilities/periodic_report/profile.py +235 -0
- loopx/capabilities/periodic_report/project_progress.py +179 -0
- loopx/capabilities/periodic_report/triggers.py +452 -0
- loopx/capabilities/pr_review_queue/__init__.py +17 -0
- loopx/capabilities/pr_review_queue/core.py +506 -0
- loopx/capabilities/pr_review_queue/review_contract.py +506 -0
- loopx/capabilities/registry.py +192 -0
- loopx/capabilities/reward_memory/__init__.py +75 -0
- loopx/capabilities/reward_memory/application.py +819 -0
- loopx/capabilities/reward_memory/architecture.py +572 -0
- loopx/capabilities/reward_memory/candidate_review.py +511 -0
- loopx/capabilities/reward_memory/cli.py +469 -0
- loopx/capabilities/reward_memory/dogfood.py +574 -0
- loopx/capabilities/reward_memory/evaluation.py +296 -0
- loopx/capabilities/reward_memory/evaluation_fixtures.py +362 -0
- loopx/capabilities/reward_memory/experiment.py +567 -0
- loopx/capabilities/reward_memory/health.py +222 -0
- loopx/capabilities/reward_memory/ingestion.py +519 -0
- loopx/capabilities/reward_memory/registry.py +600 -0
- loopx/capabilities/reward_memory/runtime_hooks.py +312 -0
- loopx/capabilities/reward_memory/scoped_feedback.py +173 -0
- loopx/capabilities/semantic_preference/__init__.py +12 -0
- loopx/capabilities/semantic_preference/cli.py +189 -0
- loopx/capabilities/semantic_preference/contract.py +592 -0
- loopx/capabilities/semantic_preference/reward_memory.py +62 -0
- loopx/capabilities/value_connectors/__init__.py +1 -0
- loopx/capabilities/value_connectors/cli.py +401 -0
- loopx/capabilities/value_connectors/finance_extension_migration.py +108 -0
- loopx/capabilities/value_connectors/install_check.py +147 -0
- loopx/capabilities/value_connectors/planner.py +733 -0
- loopx/capabilities/value_connectors/source_map.py +446 -0
- loopx/claude_goal_baseline.py +138 -0
- loopx/claude_goal_mode/__init__.py +23 -0
- loopx/claude_goal_mode/hooks/goal_policy.py +212 -0
- loopx/claude_goal_mode/hooks/goal_state.py +139 -0
- loopx/claude_goal_mode/mcp/loopx_mcp.py +167 -0
- loopx/claude_goal_mode/scripts/connect.py +103 -0
- loopx/claude_goal_mode/scripts/goalmode_cmd.py +241 -0
- loopx/claude_goal_mode/scripts/install.py +328 -0
- loopx/claude_goal_mode/statusline/goal_status.py +97 -0
- loopx/cli.py +836 -0
- loopx/cli_commands/__init__.py +334 -0
- loopx/cli_commands/_host_thread.py +13 -0
- loopx/cli_commands/agentissue_runner_flow.py +447 -0
- loopx/cli_commands/agents_last_exam.py +160 -0
- loopx/cli_commands/agents_last_exam_baked_input.py +302 -0
- loopx/cli_commands/agents_last_exam_host_codex.py +374 -0
- loopx/cli_commands/agents_last_exam_launch_dry_run.py +372 -0
- loopx/cli_commands/agents_last_exam_local_plan.py +322 -0
- loopx/cli_commands/agents_last_exam_runner_source.py +352 -0
- loopx/cli_commands/agents_last_exam_task_material.py +335 -0
- loopx/cli_commands/agents_last_exam_validation_gate.py +236 -0
- loopx/cli_commands/benchmark_boundary.py +499 -0
- loopx/cli_commands/benchmark_dispatch.py +161 -0
- loopx/cli_commands/benchmark_release_outcome.py +123 -0
- loopx/cli_commands/benchmark_review_lifecycle.py +1275 -0
- loopx/cli_commands/benchmark_run_ledger.py +763 -0
- loopx/cli_commands/benchmark_run_ledger_case_analysis.py +249 -0
- loopx/cli_commands/benchmark_run_ledger_classification.py +45 -0
- loopx/cli_commands/benchmark_run_ledger_maintenance.py +486 -0
- loopx/cli_commands/benchmark_run_ledger_maintenance_registration.py +342 -0
- loopx/cli_commands/benchmark_run_ledger_maintenance_rendering.py +233 -0
- loopx/cli_commands/benchmark_run_ledger_parity.py +92 -0
- loopx/cli_commands/bootstrap_connect.py +238 -0
- loopx/cli_commands/canary.py +707 -0
- loopx/cli_commands/canary_release_qualification.py +79 -0
- loopx/cli_commands/capability.py +96 -0
- loopx/cli_commands/doctor.py +43 -0
- loopx/cli_commands/dreaming.py +143 -0
- loopx/cli_commands/edgebench.py +205 -0
- loopx/cli_commands/evidence_log.py +275 -0
- loopx/cli_commands/explore.py +989 -0
- loopx/cli_commands/explore_planning_commands.py +157 -0
- loopx/cli_commands/extension.py +271 -0
- loopx/cli_commands/first_run_report.py +73 -0
- loopx/cli_commands/goal_channel.py +656 -0
- loopx/cli_commands/handoff_mode.py +158 -0
- loopx/cli_commands/history.py +622 -0
- loopx/cli_commands/host_mode_plan.py +113 -0
- loopx/cli_commands/lark_inbox.py +431 -0
- loopx/cli_commands/lark_kanban.py +629 -0
- loopx/cli_commands/ml_experiment.py +321 -0
- loopx/cli_commands/multi_agent.py +211 -0
- loopx/cli_commands/opencode2_goal_worker.py +217 -0
- loopx/cli_commands/pr_review.py +167 -0
- loopx/cli_commands/presentation.py +218 -0
- loopx/cli_commands/preset.py +96 -0
- loopx/cli_commands/project.py +150 -0
- loopx/cli_commands/project_lifecycle.py +915 -0
- loopx/cli_commands/quota.py +859 -0
- loopx/cli_commands/quota_registration.py +241 -0
- loopx/cli_commands/quota_request.py +113 -0
- loopx/cli_commands/ready_score.py +110 -0
- loopx/cli_commands/registry_admin.py +975 -0
- loopx/cli_commands/registry_admin_configure.py +344 -0
- loopx/cli_commands/registry_admin_peer.py +84 -0
- loopx/cli_commands/registry_authority.py +218 -0
- loopx/cli_commands/review_batch.py +146 -0
- loopx/cli_commands/slash_commands.py +145 -0
- loopx/cli_commands/start_goal.py +251 -0
- loopx/cli_commands/starter.py +175 -0
- loopx/cli_commands/starter_bootstrap.py +179 -0
- loopx/cli_commands/starter_bootstrap_registration.py +198 -0
- loopx/cli_commands/starter_runtime_idle.py +107 -0
- loopx/cli_commands/starter_scheduler.py +207 -0
- loopx/cli_commands/starter_session_runtime.py +152 -0
- loopx/cli_commands/starter_visible_common.py +54 -0
- loopx/cli_commands/starter_visible_driver.py +161 -0
- loopx/cli_commands/starter_visible_pilot.py +278 -0
- loopx/cli_commands/status.py +867 -0
- loopx/cli_commands/status_registration.py +239 -0
- loopx/cli_commands/summary_all.py +222 -0
- loopx/cli_commands/support_control.py +809 -0
- loopx/cli_commands/support_control_registry.py +68 -0
- loopx/cli_commands/support_control_supervisor.py +289 -0
- loopx/cli_commands/task_lease.py +306 -0
- loopx/cli_commands/terminal_bench_adapter.py +717 -0
- loopx/cli_commands/terminal_bench_environment_result.py +1246 -0
- loopx/cli_commands/todo.py +940 -0
- loopx/cli_commands/todo_argument_validation.py +572 -0
- loopx/cli_commands/todo_event.py +114 -0
- loopx/cli_commands/turn.py +804 -0
- loopx/cli_commands/version.py +46 -0
- loopx/cli_commands/worker_bridge.py +659 -0
- loopx/cli_rollout.py +314 -0
- loopx/codex_cli_goal_tui.py +672 -0
- loopx/codex_cli_probe.py +1530 -0
- loopx/codex_cli_probe_markdown.py +935 -0
- loopx/codex_cli_runtime_probe.py +733 -0
- loopx/codex_cli_scheduler.py +564 -0
- loopx/codex_goal_baseline.py +620 -0
- loopx/configuration_catalog.py +617 -0
- loopx/configure_goal.py +1375 -0
- loopx/contract.py +996 -0
- loopx/control_plane/__init__.py +71 -0
- loopx/control_plane/agents/__init__.py +1 -0
- loopx/control_plane/agents/agent_lane_recommendation.py +516 -0
- loopx/control_plane/agents/agent_scope.py +1578 -0
- loopx/control_plane/agents/agent_scope_frontier.py +60 -0
- loopx/control_plane/agents/capability_gate.py +531 -0
- loopx/control_plane/agents/identity.py +140 -0
- loopx/control_plane/agents/legacy_migration.py +169 -0
- loopx/control_plane/agents/management_projection.py +658 -0
- loopx/control_plane/agents/material_frontier.py +608 -0
- loopx/control_plane/agents/material_handoff.py +156 -0
- loopx/control_plane/agents/multi_agent/__init__.py +1 -0
- loopx/control_plane/agents/multi_agent/codex_executable.py +207 -0
- loopx/control_plane/agents/multi_agent/collective_round_ledger.py +387 -0
- loopx/control_plane/agents/multi_agent/contract.py +474 -0
- loopx/control_plane/agents/multi_agent/recipe.py +110 -0
- loopx/control_plane/agents/multi_agent/role_successor.py +297 -0
- loopx/control_plane/agents/multi_agent/runtime_scripts.py +426 -0
- loopx/control_plane/agents/multi_agent/visible_launch_policy.py +149 -0
- loopx/control_plane/agents/multi_agent/visible_wake_scheduler.py +392 -0
- loopx/control_plane/agents/profile.py +216 -0
- loopx/control_plane/agents/runtime_model.py +73 -0
- loopx/control_plane/agents/subagent_activity.py +164 -0
- loopx/control_plane/agents/supervisor.py +544 -0
- loopx/control_plane/agents/supervisor_events.py +462 -0
- loopx/control_plane/agents/supervisor_inject.py +204 -0
- loopx/control_plane/agents/work_mode.py +56 -0
- loopx/control_plane/agents/workspace_guard.py +364 -0
- loopx/control_plane/effect_program.py +644 -0
- loopx/control_plane/goals/__init__.py +1 -0
- loopx/control_plane/goals/active_state_event_projection.py +103 -0
- loopx/control_plane/goals/active_state_metadata.py +47 -0
- loopx/control_plane/goals/active_state_sections.py +58 -0
- loopx/control_plane/goals/configure_goal_service.py +354 -0
- loopx/control_plane/goals/contract_health.py +132 -0
- loopx/control_plane/goals/dreaming.py +152 -0
- loopx/control_plane/goals/global_registry_health.py +199 -0
- loopx/control_plane/goals/global_registry_shadow.py +33 -0
- loopx/control_plane/goals/goal_channel.py +34 -0
- loopx/control_plane/goals/goal_channel_projection.py +560 -0
- loopx/control_plane/goals/goal_frontier/__init__.py +1917 -0
- loopx/control_plane/goals/goal_frontier/ack_policy.py +149 -0
- loopx/control_plane/goals/goal_frontier/outcome_continuity.py +437 -0
- loopx/control_plane/goals/goal_frontier/replan_rules.py +210 -0
- loopx/control_plane/goals/goal_frontier/semantic_history.py +314 -0
- loopx/control_plane/goals/goal_frontier/terminal.py +180 -0
- loopx/control_plane/goals/goal_vision.py +443 -0
- loopx/control_plane/goals/goal_vision_policy.py +36 -0
- loopx/control_plane/goals/goal_vision_state.py +62 -0
- loopx/control_plane/goals/goal_vision_wait.py +290 -0
- loopx/control_plane/goals/path_resolution.py +20 -0
- loopx/control_plane/goals/start_contract.py +206 -0
- loopx/control_plane/goals/vision_checkpoint.py +92 -0
- loopx/control_plane/handoff/__init__.py +1 -0
- loopx/control_plane/handoff/cross_runtime_impl_review.py +311 -0
- loopx/control_plane/handoff/delivery_contract.py +161 -0
- loopx/control_plane/handoff/handoff_runs.py +71 -0
- loopx/control_plane/handoff/project_handoff.py +155 -0
- loopx/control_plane/handoff/review_batch.py +463 -0
- loopx/control_plane/handoff/review_packet_context.py +216 -0
- loopx/control_plane/heartbeat/agent.py +173 -0
- loopx/control_plane/heartbeat/budget.py +66 -0
- loopx/control_plane/heartbeat/builder.py +501 -0
- loopx/control_plane/heartbeat/host.py +64 -0
- loopx/control_plane/heartbeat/rules.py +68 -0
- loopx/control_plane/heartbeat/task_body.py +759 -0
- loopx/control_plane/heartbeat/visible_goal.py +86 -0
- loopx/control_plane/projects/__init__.py +1 -0
- loopx/control_plane/projects/contract.py +25 -0
- loopx/control_plane/projects/registry.py +663 -0
- loopx/control_plane/quota/__init__.py +1 -0
- loopx/control_plane/quota/cli_projection.py +704 -0
- loopx/control_plane/quota/decision_summary.py +431 -0
- loopx/control_plane/quota/effect_program.py +152 -0
- loopx/control_plane/quota/error_codes.py +19 -0
- loopx/control_plane/quota/goal_boundary.py +464 -0
- loopx/control_plane/quota/heartbeat_receipt.py +277 -0
- loopx/control_plane/quota/heartbeat_recommendation.py +718 -0
- loopx/control_plane/quota/host_poll_receipts.py +162 -0
- loopx/control_plane/quota/live_decision.py +142 -0
- loopx/control_plane/quota/monitor_poll.py +786 -0
- loopx/control_plane/quota/policy_constants.py +40 -0
- loopx/control_plane/quota/projection_repair.py +262 -0
- loopx/control_plane/quota/recent_runs.py +210 -0
- loopx/control_plane/quota/scheduler_ack.py +490 -0
- loopx/control_plane/quota/selected_todo_projection.py +139 -0
- loopx/control_plane/quota/settlement.py +437 -0
- loopx/control_plane/quota/settlement_cli.py +246 -0
- loopx/control_plane/quota/settlement_validation.py +64 -0
- loopx/control_plane/quota/settlement_workspace_causality.py +180 -0
- loopx/control_plane/quota/should_run.py +249 -0
- loopx/control_plane/quota/should_run_packet.py +1165 -0
- loopx/control_plane/quota/should_run_prepare.py +675 -0
- loopx/control_plane/quota/slot_accounting.py +1123 -0
- loopx/control_plane/quota/spend_sources.py +11 -0
- loopx/control_plane/quota/stall_repair.py +397 -0
- loopx/control_plane/quota/states.py +29 -0
- loopx/control_plane/quota/task_orchestration.py +448 -0
- loopx/control_plane/quota/task_orchestration_admission.py +497 -0
- loopx/control_plane/quota/turn_envelope.py +889 -0
- loopx/control_plane/quota/usage_summary.py +140 -0
- loopx/control_plane/reward_memory.py +43 -0
- loopx/control_plane/runtime/__init__.py +2 -0
- loopx/control_plane/runtime/active_user_assisted_pilot.py +275 -0
- loopx/control_plane/runtime/agent_scoped_evidence_log.py +435 -0
- loopx/control_plane/runtime/decision_freshness.py +203 -0
- loopx/control_plane/runtime/event_ledger.py +197 -0
- loopx/control_plane/runtime/event_store_migration_bridge.py +196 -0
- loopx/control_plane/runtime/goal_project_route.py +70 -0
- loopx/control_plane/runtime/local_state_write_correctness.py +242 -0
- loopx/control_plane/runtime/promotion_readiness.py +152 -0
- loopx/control_plane/runtime/public_safety.py +120 -0
- loopx/control_plane/runtime/run_artifacts.py +78 -0
- loopx/control_plane/runtime/run_compaction.py +397 -0
- loopx/control_plane/runtime/run_context_retention.py +241 -0
- loopx/control_plane/runtime/run_history.py +132 -0
- loopx/control_plane/runtime/run_index_duplicates.py +205 -0
- loopx/control_plane/runtime/run_index_rebuild.py +263 -0
- loopx/control_plane/runtime/run_ingest_health.py +336 -0
- loopx/control_plane/runtime/runtime_projection_route.py +624 -0
- loopx/control_plane/runtime/runtime_projection_writer.py +98 -0
- loopx/control_plane/runtime/session_runtime.py +339 -0
- loopx/control_plane/runtime/shared_runtime_material_projection.py +332 -0
- loopx/control_plane/runtime/shared_runtime_refresh_projection.py +183 -0
- loopx/control_plane/runtime/stale_latest_run.py +90 -0
- loopx/control_plane/runtime/status_classifications.py +49 -0
- loopx/control_plane/runtime/status_projection_cache.py +235 -0
- loopx/control_plane/runtime/stride_observation.py +144 -0
- loopx/control_plane/runtime/time.py +39 -0
- loopx/control_plane/runtime/trajectory_hygiene.py +149 -0
- loopx/control_plane/runtime/validation_command.py +69 -0
- loopx/control_plane/scheduler/__init__.py +1 -0
- loopx/control_plane/scheduler/ack.py +329 -0
- loopx/control_plane/scheduler/arbitration.py +188 -0
- loopx/control_plane/scheduler/automation_liveness.py +183 -0
- loopx/control_plane/scheduler/execution_context.py +555 -0
- loopx/control_plane/scheduler/external_evidence_observation.py +428 -0
- loopx/control_plane/scheduler/monitor_display.py +143 -0
- loopx/control_plane/scheduler/monitor_poll_policy.py +161 -0
- loopx/control_plane/scheduler/monitor_poll_writeback.py +351 -0
- loopx/control_plane/scheduler/monitor_target.py +64 -0
- loopx/control_plane/scheduler/monitor_todo.py +146 -0
- loopx/control_plane/scheduler/monitor_wait.py +237 -0
- loopx/control_plane/scheduler/scheduler_hint.py +1284 -0
- loopx/control_plane/scheduler/state.py +354 -0
- loopx/control_plane/scheduler/state_transition_rules.py +179 -0
- loopx/control_plane/scheduler/time.py +10 -0
- loopx/control_plane/settlement_driver.py +293 -0
- loopx/control_plane/status/__init__.py +6 -0
- loopx/control_plane/status/active_state_projection.py +105 -0
- loopx/control_plane/status/agent_lane_projection.py +375 -0
- loopx/control_plane/status/attention_projection.py +74 -0
- loopx/control_plane/status/autonomous_replan_projection.py +103 -0
- loopx/control_plane/status/collection.py +140 -0
- loopx/control_plane/status/contract_projection.py +31 -0
- loopx/control_plane/status/dreaming_projection.py +52 -0
- loopx/control_plane/status/goal_attention_projection.py +157 -0
- loopx/control_plane/status/lifecycle_projection.py +110 -0
- loopx/control_plane/status/monitor_display_projection.py +69 -0
- loopx/control_plane/status/registry_health_projection.py +75 -0
- loopx/control_plane/status/run_projection.py +70 -0
- loopx/control_plane/status/runtime_summaries.py +161 -0
- loopx/control_plane/testing/__init__.py +1 -0
- loopx/control_plane/testing/actual_default_model_behavior_portfolio.py +1371 -0
- loopx/control_plane/testing/canary_harness.py +182 -0
- loopx/control_plane/testing/capability_monitor_repair_tool_behavior.py +674 -0
- loopx/control_plane/testing/cli_output_budget.py +807 -0
- loopx/control_plane/testing/cli_output_differential.py +250 -0
- loopx/control_plane/testing/cli_output_semantics.py +87 -0
- loopx/control_plane/testing/control_plane_composition_scenarios.py +225 -0
- loopx/control_plane/testing/decision_replay.py +268 -0
- loopx/control_plane/testing/doubao_model_behavior_actor.py +559 -0
- loopx/control_plane/testing/model_behavior_corpus.py +344 -0
- loopx/control_plane/testing/model_behavior_qualification.py +769 -0
- loopx/control_plane/testing/model_behavior_retained_cases.py +235 -0
- loopx/control_plane/testing/model_tool_behavior.py +536 -0
- loopx/control_plane/testing/onboarding_model_behavior_qualification.py +642 -0
- loopx/control_plane/testing/quota_fixtures.py +208 -0
- loopx/control_plane/testing/quota_should_run_parity.py +57 -0
- loopx/control_plane/testing/release_commit_qualification.py +671 -0
- loopx/control_plane/testing/replan_semantic_action_behavior.py +1302 -0
- loopx/control_plane/testing/scoped_gate_successor_tool_behavior.py +527 -0
- loopx/control_plane/testing/selected_todo_tool_behavior.py +1002 -0
- loopx/control_plane/testing/terminal_settlement_tool_behavior.py +656 -0
- loopx/control_plane/todos/__init__.py +1 -0
- loopx/control_plane/todos/active_state_editing.py +296 -0
- loopx/control_plane/todos/active_state_todo_parser.py +138 -0
- loopx/control_plane/todos/active_state_todos.py +175 -0
- loopx/control_plane/todos/addition.py +103 -0
- loopx/control_plane/todos/claim_visibility.py +253 -0
- loopx/control_plane/todos/completed_archive.py +139 -0
- loopx/control_plane/todos/completion_fence.py +49 -0
- loopx/control_plane/todos/completion_policy.py +153 -0
- loopx/control_plane/todos/completion_validation.py +248 -0
- loopx/control_plane/todos/completion_validation_accountability.py +27 -0
- loopx/control_plane/todos/completion_validation_projection.py +57 -0
- loopx/control_plane/todos/contract.py +1476 -0
- loopx/control_plane/todos/decision_scope.py +554 -0
- loopx/control_plane/todos/deferred_resume.py +546 -0
- loopx/control_plane/todos/durable_completion.py +201 -0
- loopx/control_plane/todos/event_writeback.py +484 -0
- loopx/control_plane/todos/frontier_deadline.py +132 -0
- loopx/control_plane/todos/handoff_gate.py +283 -0
- loopx/control_plane/todos/handoff_mode.py +444 -0
- loopx/control_plane/todos/handoff_note.py +202 -0
- loopx/control_plane/todos/line_update.py +361 -0
- loopx/control_plane/todos/list_projection.py +205 -0
- loopx/control_plane/todos/markdown.py +199 -0
- loopx/control_plane/todos/monitor_metadata.py +88 -0
- loopx/control_plane/todos/mutation_authority.py +299 -0
- loopx/control_plane/todos/projection.py +655 -0
- loopx/control_plane/todos/quota_summary.py +1138 -0
- loopx/control_plane/todos/route_continuation.py +267 -0
- loopx/control_plane/todos/succession_warning.py +174 -0
- loopx/control_plane/todos/summary_item.py +223 -0
- loopx/control_plane/todos/text.py +30 -0
- loopx/control_plane/todos/todo_index.py +226 -0
- loopx/control_plane/todos/todo_summary.py +1458 -0
- loopx/control_plane/todos/unblock_resume.py +326 -0
- loopx/control_plane/todos/user_gate.py +263 -0
- loopx/control_plane/todos/write_hint.py +63 -0
- loopx/control_plane/todos/write_policy.py +135 -0
- loopx/control_plane/turn_driver/__init__.py +85 -0
- loopx/control_plane/turn_driver/codex_cli.py +502 -0
- loopx/control_plane/turn_driver/driver.py +355 -0
- loopx/control_plane/turn_driver/executor.py +1468 -0
- loopx/control_plane/turn_driver/loop_controller.py +669 -0
- loopx/control_plane/turn_driver/settlement.py +318 -0
- loopx/control_plane/turn_driver/transaction.py +375 -0
- loopx/control_plane/work_items/__init__.py +1 -0
- loopx/control_plane/work_items/attention_fields.py +56 -0
- loopx/control_plane/work_items/attention_item.py +77 -0
- loopx/control_plane/work_items/attention_queue.py +322 -0
- loopx/control_plane/work_items/attention_routing.py +213 -0
- loopx/control_plane/work_items/autonomous_candidates.py +135 -0
- loopx/control_plane/work_items/autonomous_replan_ack.py +276 -0
- loopx/control_plane/work_items/autonomous_replan_obligation.py +786 -0
- loopx/control_plane/work_items/backlog_hygiene.py +59 -0
- loopx/control_plane/work_items/capability_monitor_fallback.py +221 -0
- loopx/control_plane/work_items/delivery_batch_scale.py +66 -0
- loopx/control_plane/work_items/delivery_outcome.py +152 -0
- loopx/control_plane/work_items/delivery_signals.py +113 -0
- loopx/control_plane/work_items/execution_obligation.py +235 -0
- loopx/control_plane/work_items/goal_route_hint.py +320 -0
- loopx/control_plane/work_items/interaction_contract.py +1540 -0
- loopx/control_plane/work_items/issue_meta_surface.py +159 -0
- loopx/control_plane/work_items/lifecycle.py +139 -0
- loopx/control_plane/work_items/operator_inbox.py +266 -0
- loopx/control_plane/work_items/outcome_followthrough.py +69 -0
- loopx/control_plane/work_items/primary_action.py +326 -0
- loopx/control_plane/work_items/progress_observation.py +630 -0
- loopx/control_plane/work_items/project_asset.py +675 -0
- loopx/control_plane/work_items/repair_delta.py +693 -0
- loopx/control_plane/work_items/runtime_capability_reentry.py +168 -0
- loopx/control_plane/work_items/semantic_replan_writeback.py +177 -0
- loopx/control_plane/work_items/status_contract.py +49 -0
- loopx/control_plane/work_items/task_graph.py +1046 -0
- loopx/control_plane/work_items/task_lease.py +1254 -0
- loopx/control_plane/work_items/task_lease_settlement.py +422 -0
- loopx/control_plane/work_items/work_lane.py +510 -0
- loopx/control_plane/work_items/work_lane_context.py +161 -0
- loopx/demo.py +247 -0
- loopx/diagnose.py +633 -0
- loopx/doctor.py +1251 -0
- loopx/domain_packs/__init__.py +1 -0
- loopx/domain_packs/issue_fix.py +571 -0
- loopx/domain_packs/ml_experiment.py +854 -0
- loopx/domain_state.py +137 -0
- loopx/dreaming.py +706 -0
- loopx/entrypoint.py +16 -0
- loopx/event_sourced_state.py +981 -0
- loopx/execution_profile.py +286 -0
- loopx/experiments/__init__.py +1 -0
- loopx/experiments/planner_worker/__init__.py +1 -0
- loopx/experiments/planner_worker/contract.py +523 -0
- loopx/experiments/planner_worker/runtime.py +391 -0
- loopx/experiments/planner_worker/traex.py +461 -0
- loopx/explore_graph.py +11 -0
- loopx/extensions/__init__.py +1 -0
- loopx/extensions/bundled.py +28 -0
- loopx/extensions/execution_envelope.py +126 -0
- loopx/extensions/lark/__init__.py +11 -0
- loopx/extensions/lark/event_collector.py +478 -0
- loopx/extensions/lark/event_collector_runtime.py +506 -0
- loopx/extensions/lark/event_inbox.py +454 -0
- loopx/extensions/lark/extension.toml +88 -0
- loopx/extensions/lark/goal_channel.py +44 -0
- loopx/extensions/lark/goal_channel_contracts.py +388 -0
- loopx/extensions/lark/goal_channel_lifecycle.py +218 -0
- loopx/extensions/lark/goal_channel_runtime.py +792 -0
- loopx/extensions/lark/goal_channel_setup.py +805 -0
- loopx/extensions/lark/goal_channel_targets.py +215 -0
- loopx/extensions/lark/goal_channel_transport.py +281 -0
- loopx/extensions/lark/inbox_reactions.py +650 -0
- loopx/extensions/lark/inbox_reply.py +430 -0
- loopx/extensions/lark/presentation/__init__.py +11 -0
- loopx/extensions/lark/presentation/explore_results.py +2276 -0
- loopx/extensions/lark/presentation/explore_singleflight.py +127 -0
- loopx/extensions/lark/presentation/explore_source_guard.py +121 -0
- loopx/extensions/lark/presentation/explore_stage_document.py +703 -0
- loopx/extensions/lark/presentation/explore_visual_integrity.py +122 -0
- loopx/extensions/lark/presentation/explore_visual_readback.py +452 -0
- loopx/extensions/lark/presentation/explore_visual_styles.py +156 -0
- loopx/extensions/lark/presentation/issue_fix_surface.py +612 -0
- loopx/extensions/lark/presentation/kanban.py +2791 -0
- loopx/extensions/lark/presentation/message_card.py +112 -0
- loopx/extensions/lark/presentation/periodic_report.py +261 -0
- loopx/extensions/lark/presentation/projection_rows.py +600 -0
- loopx/extensions/lark/presentation/record_io.py +95 -0
- loopx/extensions/lark/presentation/sync_receipt.py +145 -0
- loopx/extensions/lark/private_json.py +40 -0
- loopx/extensions/lark/provider.py +86 -0
- loopx/extensions/lark/reviewer_notification.py +604 -0
- loopx/extensions/manifest.py +385 -0
- loopx/extensions/openviking_periodic_report/__init__.py +17 -0
- loopx/extensions/openviking_periodic_report/activation.py +173 -0
- loopx/extensions/openviking_periodic_report/extension.toml +17 -0
- loopx/extensions/openviking_periodic_report/provider.py +355 -0
- loopx/extensions/openviking_periodic_report/sink.py +117 -0
- loopx/extensions/openviking_semantic_preference/__init__.py +5 -0
- loopx/extensions/openviking_semantic_preference/extension.toml +16 -0
- loopx/extensions/openviking_semantic_preference/history_export.py +484 -0
- loopx/extensions/openviking_semantic_preference/project_peer.py +68 -0
- loopx/extensions/openviking_semantic_preference/provider.py +312 -0
- loopx/extensions/presentation.py +979 -0
- loopx/extensions/process_runtime.py +204 -0
- loopx/extensions/readiness.py +168 -0
- loopx/extensions/runtime.py +931 -0
- loopx/extensions/scaffold.py +335 -0
- loopx/feedback.py +581 -0
- loopx/file_lock.py +382 -0
- loopx/global_registry.py +842 -0
- loopx/global_risks.py +970 -0
- loopx/global_todos.py +568 -0
- loopx/handoff_budget.py +28 -0
- loopx/heartbeat_prequota.py +80 -0
- loopx/heartbeat_prompt.py +159 -0
- loopx/help_surface.py +516 -0
- loopx/history.py +1507 -0
- loopx/host_loop_activation.py +1311 -0
- loopx/host_mode_planner.py +991 -0
- loopx/install_contract.py +1 -0
- loopx/interface_budget.py +196 -0
- loopx/long_task_cadence.py +208 -0
- loopx/materials.py +185 -0
- loopx/ml_experiment.py +3 -0
- loopx/onboarding.py +214 -0
- loopx/opencode2_goal_mode/README.md +81 -0
- loopx/opencode2_goal_mode/__init__.py +9 -0
- loopx/opencode2_goal_mode/opencode2-goal-worker.mjs +1018 -0
- loopx/opencode_goal_mode/README.md +99 -0
- loopx/opencode_goal_mode/__init__.py +13 -0
- loopx/opencode_goal_mode/goal-bridge-runtime.mjs +858 -0
- loopx/opencode_goal_mode/loopx-goal.js +8 -0
- loopx/operator_gate.py +420 -0
- loopx/orchestration.py +127 -0
- loopx/paths.py +59 -0
- loopx/pi_goal_mode/README.md +67 -0
- loopx/pi_goal_mode/__init__.py +13 -0
- loopx/pi_goal_mode/loopx-goal.ts +254 -0
- loopx/pi_goal_mode/pi-goal-loop-runtime.mjs +574 -0
- loopx/pr_review.py +1206 -0
- loopx/presentation/__init__.py +1 -0
- loopx/presentation/explore_views.py +1334 -0
- loopx/presentation/markdown.py +61 -0
- loopx/presentation/projection_source_reconcile.py +140 -0
- loopx/presentation/public_safety.py +42 -0
- loopx/presentation/renderers/__init__.py +17 -0
- loopx/presentation/renderers/goal_channel_html.py +269 -0
- loopx/presentation/renderers/periodic_report_html.py +786 -0
- loopx/presentation/renderers/periodic_report_markdown.py +184 -0
- loopx/presentation/renderers/quota_event_markdown.py +116 -0
- loopx/presentation/renderers/quota_markdown.py +1112 -0
- loopx/presentation/renderers/status_markdown.py +1570 -0
- loopx/presentation/renderers/trajectory_hygiene_markdown.py +39 -0
- loopx/presentation/renderers/turn_envelope_markdown.py +33 -0
- loopx/presentation/sinks/__init__.py +5 -0
- loopx/presentation/sinks/openviking_periodic_report.py +7 -0
- loopx/presentation/static_site.py +691 -0
- loopx/presets.py +369 -0
- loopx/project_alias.py +217 -0
- loopx/project_map.py +589 -0
- loopx/project_prompt.py +1153 -0
- loopx/project_skill_cli.py +125 -0
- loopx/project_skill_delivery.py +470 -0
- loopx/project_uninstall.py +462 -0
- loopx/promotion_gate.py +197 -0
- loopx/quota.py +1197 -0
- loopx/ready_score.py +413 -0
- loopx/registry.py +621 -0
- loopx/registry_writability.py +64 -0
- loopx/release_candidate.py +148 -0
- loopx/release_manifest.py +316 -0
- loopx/repository_identity.py +100 -0
- loopx/review_packet.py +1024 -0
- loopx/rollout_event_log.py +505 -0
- loopx/runtime.py +112 -0
- loopx/self_update.py +750 -0
- loopx/session_runtime.py +418 -0
- loopx/skill_install_readback.py +500 -0
- loopx/slash_command_install.py +1393 -0
- loopx/slash_commands.py +264 -0
- loopx/state_backup.py +573 -0
- loopx/state_migration.py +350 -0
- loopx/state_projection.py +809 -0
- loopx/state_refresh.py +1416 -0
- loopx/status.py +1383 -0
- loopx/status_server.py +935 -0
- loopx/summary_all.py +725 -0
- loopx/terminal_bench_agent.py +2056 -0
- loopx/thread_agent_binding.py +408 -0
- loopx/todo_followups.py +168 -0
- loopx/todo_suggestion_prompt.py +204 -0
- loopx/todos.py +2229 -0
- loopx/turn_identity.py +17 -0
- loopx/upgrade.py +1083 -0
- loopx/visible_governance.py +667 -0
- loopx/visible_multi_agent_launcher.py +1253 -0
- loopx/visible_multi_agent_tmux.py +429 -0
- loopx/worker_bridge.py +1574 -0
- loopx-0.4.8.dist-info/METADATA +708 -0
- loopx-0.4.8.dist-info/RECORD +811 -0
- loopx-0.4.8.dist-info/WHEEL +5 -0
- loopx-0.4.8.dist-info/entry_points.txt +5 -0
- loopx-0.4.8.dist-info/licenses/LICENSE +202 -0
- loopx-0.4.8.dist-info/licenses/LICENSE-MIT +21 -0
- loopx-0.4.8.dist-info/licenses/NOTICE +6 -0
- loopx-0.4.8.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,1254 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import fnmatch
|
|
4
|
+
import json
|
|
5
|
+
import re
|
|
6
|
+
from collections.abc import Callable, Iterator
|
|
7
|
+
from contextlib import contextmanager
|
|
8
|
+
from datetime import datetime, timedelta
|
|
9
|
+
from pathlib import Path
|
|
10
|
+
from typing import Any
|
|
11
|
+
|
|
12
|
+
from ...agent_registry import (
|
|
13
|
+
registered_agent_ids_from_registry,
|
|
14
|
+
require_registered_agent_id,
|
|
15
|
+
)
|
|
16
|
+
from ...file_lock import exclusive_file_lock
|
|
17
|
+
from ...history import load_registry
|
|
18
|
+
from ...paths import resolve_runtime_root
|
|
19
|
+
from ..runtime.time import now_utc as runtime_now_utc
|
|
20
|
+
from ..runtime.time import parse_timestamp, utc_isoformat
|
|
21
|
+
from ..todos.contract import (
|
|
22
|
+
TODO_TASK_CLASS_USER_GATE,
|
|
23
|
+
normalize_required_write_scopes,
|
|
24
|
+
normalize_todo_claimed_by,
|
|
25
|
+
normalize_todo_excluded_agents,
|
|
26
|
+
normalize_todo_id,
|
|
27
|
+
)
|
|
28
|
+
from ..todos.handoff_mode import (
|
|
29
|
+
HANDOFF_MODE_HARD_LEASE,
|
|
30
|
+
HANDOFF_MODE_LEGACY,
|
|
31
|
+
HANDOFF_MODE_SOFT_CLAIM,
|
|
32
|
+
HandoffModeError,
|
|
33
|
+
goal_handoff_mode_for_goal,
|
|
34
|
+
)
|
|
35
|
+
|
|
36
|
+
TASK_LEASE_SCHEMA_VERSION = "task_lease_v0"
|
|
37
|
+
DEFAULT_TASK_LEASE_TTL_SECONDS = 45 * 60
|
|
38
|
+
MAX_TASK_LEASE_TTL_SECONDS = 24 * 60 * 60
|
|
39
|
+
IDEMPOTENCY_KEY_PATTERN = re.compile(r"^[A-Za-z0-9_.:@/-]{1,160}$")
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
class TaskLeaseError(ValueError):
|
|
43
|
+
def __init__(self, message: str, *, code: str, payload: dict[str, Any] | None = None) -> None:
|
|
44
|
+
super().__init__(message)
|
|
45
|
+
self.code = code
|
|
46
|
+
self.payload = payload or {}
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def now_utc() -> datetime:
|
|
50
|
+
return runtime_now_utc()
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def isoformat(value: datetime) -> str:
|
|
54
|
+
return utc_isoformat(value)
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def normalize_idempotency_key(value: Any) -> str:
|
|
58
|
+
candidate = str(value or "").strip()
|
|
59
|
+
if not candidate or not IDEMPOTENCY_KEY_PATTERN.match(candidate):
|
|
60
|
+
raise TaskLeaseError(
|
|
61
|
+
"idempotency key must be a public-safe token",
|
|
62
|
+
code="invalid_idempotency_key",
|
|
63
|
+
)
|
|
64
|
+
return candidate
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
def normalize_owner(value: Any) -> str:
|
|
68
|
+
owner = normalize_todo_claimed_by(value)
|
|
69
|
+
if not owner:
|
|
70
|
+
raise TaskLeaseError("owner must be a public-safe agent id", code="invalid_owner")
|
|
71
|
+
return owner
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
def normalize_ttl_seconds(value: int | None) -> int:
|
|
75
|
+
ttl = DEFAULT_TASK_LEASE_TTL_SECONDS if value is None else int(value)
|
|
76
|
+
if ttl <= 0 or ttl > MAX_TASK_LEASE_TTL_SECONDS:
|
|
77
|
+
raise TaskLeaseError(
|
|
78
|
+
f"ttl seconds must be between 1 and {MAX_TASK_LEASE_TTL_SECONDS}",
|
|
79
|
+
code="invalid_ttl",
|
|
80
|
+
)
|
|
81
|
+
return ttl
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
def normalize_goal_id(value: Any) -> str:
|
|
85
|
+
goal_id = str(value or "").strip()
|
|
86
|
+
if not goal_id or goal_id in {".", ".."} or "/" in goal_id or "\\" in goal_id:
|
|
87
|
+
raise TaskLeaseError("goal id must be a single path segment", code="invalid_goal_id")
|
|
88
|
+
if Path(goal_id).name != goal_id:
|
|
89
|
+
raise TaskLeaseError("goal id must not include path traversal", code="invalid_goal_id")
|
|
90
|
+
return goal_id
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
def normalize_lease_todo_id(value: Any) -> str:
|
|
94
|
+
todo_id = normalize_todo_id(value)
|
|
95
|
+
if not todo_id:
|
|
96
|
+
raise TaskLeaseError("todo id must use the todo_<token> shape", code="invalid_todo_id")
|
|
97
|
+
return todo_id
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
def task_lease_dir(*, runtime_root: Path, goal_id: str) -> Path:
|
|
101
|
+
return runtime_root / "goals" / normalize_goal_id(goal_id) / "task-leases"
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
def task_lease_path(*, runtime_root: Path, goal_id: str, todo_id: str) -> Path:
|
|
105
|
+
return task_lease_dir(runtime_root=runtime_root, goal_id=goal_id) / f"{normalize_lease_todo_id(todo_id)}.json"
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
def task_lease_lock_path(*, runtime_root: Path, goal_id: str) -> Path:
|
|
109
|
+
return task_lease_dir(runtime_root=runtime_root, goal_id=goal_id) / ".task-leases"
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
class _VerifiedTaskLeaseFence(dict):
|
|
113
|
+
"""Key-verified fence payload with its release hook held out-of-band.
|
|
114
|
+
|
|
115
|
+
The hook lives on the instance attribute, never inside the mapping, so
|
|
116
|
+
the payload stays JSON-serializable for every consumer at all times.
|
|
117
|
+
"""
|
|
118
|
+
|
|
119
|
+
release_hook: Callable[[], None] | None = None
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
def goal_handoff_mode_for_lease(registry_path: Path, goal_id: str) -> str:
|
|
123
|
+
"""Resolve the goal handoff mode for lease verbs.
|
|
124
|
+
|
|
125
|
+
An invalid ``handoff_mode`` value is re-typed as a TaskLeaseError. A goal
|
|
126
|
+
or state file that cannot be resolved falls back to legacy: each lease
|
|
127
|
+
verb's own todo-projection checks already own that failure surface.
|
|
128
|
+
"""
|
|
129
|
+
|
|
130
|
+
try:
|
|
131
|
+
return goal_handoff_mode_for_goal(registry_path=registry_path, goal_id=goal_id)
|
|
132
|
+
except HandoffModeError as exc:
|
|
133
|
+
raise TaskLeaseError(str(exc), code=exc.code, payload=exc.payload) from exc
|
|
134
|
+
except (OSError, ValueError):
|
|
135
|
+
return HANDOFF_MODE_LEGACY
|
|
136
|
+
|
|
137
|
+
|
|
138
|
+
def _require_lease_mutation_allowed_by_handoff_mode(
|
|
139
|
+
*,
|
|
140
|
+
registry_path: Path,
|
|
141
|
+
goal_id: str,
|
|
142
|
+
todo_id: str,
|
|
143
|
+
action: str,
|
|
144
|
+
) -> str:
|
|
145
|
+
handoff_mode = goal_handoff_mode_for_lease(registry_path, goal_id)
|
|
146
|
+
if handoff_mode == HANDOFF_MODE_SOFT_CLAIM:
|
|
147
|
+
raise TaskLeaseError(
|
|
148
|
+
f"goal handoff mode 'soft_claim' forbids task lease {action}; "
|
|
149
|
+
"release and inspect remain available for legacy leftovers",
|
|
150
|
+
code="handoff_mode_forbids_lease",
|
|
151
|
+
payload={
|
|
152
|
+
"goal_id": goal_id,
|
|
153
|
+
"todo_id": todo_id,
|
|
154
|
+
"action": action,
|
|
155
|
+
"handoff_mode": handoff_mode,
|
|
156
|
+
},
|
|
157
|
+
)
|
|
158
|
+
return handoff_mode
|
|
159
|
+
|
|
160
|
+
|
|
161
|
+
def _optional_handoff_mode(registry_path: Path | None, goal_id: str) -> str | None:
|
|
162
|
+
if registry_path is None:
|
|
163
|
+
return None
|
|
164
|
+
try:
|
|
165
|
+
return goal_handoff_mode_for_lease(registry_path, goal_id)
|
|
166
|
+
except (TaskLeaseError, OSError, ValueError):
|
|
167
|
+
return None
|
|
168
|
+
|
|
169
|
+
|
|
170
|
+
@contextmanager
|
|
171
|
+
def hold_handoff_lease_holder_gate(
|
|
172
|
+
*,
|
|
173
|
+
registry_path: Path,
|
|
174
|
+
goal_id: str,
|
|
175
|
+
todo_id: str,
|
|
176
|
+
actor_agent_id: str | None,
|
|
177
|
+
) -> Iterator[dict[str, Any]]:
|
|
178
|
+
"""Hold the per-goal lease lock while proving the actor owns the lease.
|
|
179
|
+
|
|
180
|
+
hard_lease handoff mode only: an ownership mutation (claim, claimed_by
|
|
181
|
+
update, clear) on an existing todo requires the acting agent to hold that
|
|
182
|
+
todo's time-active lease. Identity is compared with
|
|
183
|
+
normalize_todo_claimed_by on both sides. Callers must already hold the
|
|
184
|
+
markdown state lock; this guard then takes the per-goal lease lock, the
|
|
185
|
+
same markdown-lock-then-lease-lock order used by the completion fence,
|
|
186
|
+
and the caller keeps it held until the markdown write commits.
|
|
187
|
+
"""
|
|
188
|
+
|
|
189
|
+
normalized_goal_id = normalize_goal_id(goal_id)
|
|
190
|
+
normalized_todo_id = normalize_lease_todo_id(todo_id)
|
|
191
|
+
actor = normalize_todo_claimed_by(actor_agent_id)
|
|
192
|
+
runtime_root = runtime_root_from_registry(registry_path, None)
|
|
193
|
+
lease_path = task_lease_path(
|
|
194
|
+
runtime_root=runtime_root,
|
|
195
|
+
goal_id=normalized_goal_id,
|
|
196
|
+
todo_id=normalized_todo_id,
|
|
197
|
+
)
|
|
198
|
+
base_payload: dict[str, Any] = {
|
|
199
|
+
"goal_id": normalized_goal_id,
|
|
200
|
+
"todo_id": normalized_todo_id,
|
|
201
|
+
"handoff_mode": HANDOFF_MODE_HARD_LEASE,
|
|
202
|
+
"lease_path": str(lease_path),
|
|
203
|
+
}
|
|
204
|
+
if not actor:
|
|
205
|
+
raise TaskLeaseError(
|
|
206
|
+
"hard_lease handoff mode requires an attributed actor for "
|
|
207
|
+
"ownership changes; provide --agent-id",
|
|
208
|
+
code="handoff_mode_requires_lease",
|
|
209
|
+
payload={**base_payload, "reason": "missing_actor"},
|
|
210
|
+
)
|
|
211
|
+
lock_target = task_lease_lock_path(
|
|
212
|
+
runtime_root=runtime_root,
|
|
213
|
+
goal_id=normalized_goal_id,
|
|
214
|
+
)
|
|
215
|
+
with exclusive_file_lock(
|
|
216
|
+
lock_target,
|
|
217
|
+
agent_id=actor,
|
|
218
|
+
operation="handoff_lease_holder_gate",
|
|
219
|
+
):
|
|
220
|
+
lease = read_lease(lease_path)
|
|
221
|
+
if not lease_is_active(lease):
|
|
222
|
+
raise TaskLeaseError(
|
|
223
|
+
"hard_lease handoff mode requires a time-active task lease "
|
|
224
|
+
"before ownership of an existing todo can change; acquire one "
|
|
225
|
+
"with `loopx task-lease acquire`",
|
|
226
|
+
code="handoff_mode_requires_lease",
|
|
227
|
+
payload={**base_payload, "reason": "no_active_lease"},
|
|
228
|
+
)
|
|
229
|
+
assert lease is not None
|
|
230
|
+
owner = normalize_todo_claimed_by(lease.get("owner"))
|
|
231
|
+
if owner != actor:
|
|
232
|
+
raise TaskLeaseError(
|
|
233
|
+
f"hard_lease handoff mode: actor {actor!r} does not own the "
|
|
234
|
+
f"time-active task lease held by {owner!r}",
|
|
235
|
+
code="handoff_mode_requires_lease",
|
|
236
|
+
payload={
|
|
237
|
+
**base_payload,
|
|
238
|
+
"reason": "lease_owner_mismatch",
|
|
239
|
+
"actor_agent_id": actor,
|
|
240
|
+
"lease_owner": owner,
|
|
241
|
+
"lease_version": lease.get("version"),
|
|
242
|
+
"expires_at": lease.get("expires_at"),
|
|
243
|
+
},
|
|
244
|
+
)
|
|
245
|
+
yield {
|
|
246
|
+
"schema_version": TASK_LEASE_SCHEMA_VERSION,
|
|
247
|
+
"checked": True,
|
|
248
|
+
"active": True,
|
|
249
|
+
"owner": owner,
|
|
250
|
+
"version": lease.get("version"),
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
|
|
254
|
+
@contextmanager
|
|
255
|
+
def hold_task_lease_mutation_fence(
|
|
256
|
+
*,
|
|
257
|
+
registry_path: Path,
|
|
258
|
+
goal_id: str,
|
|
259
|
+
todo_id: str,
|
|
260
|
+
todo: dict[str, Any],
|
|
261
|
+
actor_agent_id: str | None,
|
|
262
|
+
idempotency_key: str | None,
|
|
263
|
+
expected_version: int | None = None,
|
|
264
|
+
require_active_when_key_supplied: bool = True,
|
|
265
|
+
handoff: dict[str, Any] | None = None,
|
|
266
|
+
) -> Iterator[dict[str, Any]]:
|
|
267
|
+
"""Hold the per-goal lease lock while one todo lifecycle write commits.
|
|
268
|
+
|
|
269
|
+
Task leases are optional in legacy and soft_claim handoff modes. Once an
|
|
270
|
+
effective lease exists, however, the lifecycle writer must prove that it
|
|
271
|
+
is the execution instance that acquired the lease. The idempotency key is
|
|
272
|
+
the execution-instance fence; agent ids alone are insufficient because
|
|
273
|
+
multiple host processes may share one peer identity.
|
|
274
|
+
|
|
275
|
+
``handoff`` carries the resolved goal handoff mode plus the delegated
|
|
276
|
+
authority door marker. In hard_lease mode (without the door) the fence is
|
|
277
|
+
mandatory: no effective lease is a typed error instead of a silent
|
|
278
|
+
``{"required": False}``, and a time-active lease whose owner constraint is
|
|
279
|
+
no longer effective (the legacy self-disarm state) fails loudly instead of
|
|
280
|
+
letting a keyless completion through. For a user-role ``user_gate``
|
|
281
|
+
completion that supplies no explicit lease credentials, the fence mints
|
|
282
|
+
the key itself under the same per-goal lease lock; an existing
|
|
283
|
+
time-active lease is never displaced.
|
|
284
|
+
"""
|
|
285
|
+
|
|
286
|
+
normalized_goal_id = normalize_goal_id(goal_id)
|
|
287
|
+
normalized_todo_id = normalize_lease_todo_id(todo_id)
|
|
288
|
+
requested_key = (
|
|
289
|
+
normalize_idempotency_key(idempotency_key)
|
|
290
|
+
if idempotency_key is not None
|
|
291
|
+
else None
|
|
292
|
+
)
|
|
293
|
+
runtime_root = runtime_root_from_registry(registry_path, None)
|
|
294
|
+
lease_path = task_lease_path(
|
|
295
|
+
runtime_root=runtime_root,
|
|
296
|
+
goal_id=normalized_goal_id,
|
|
297
|
+
todo_id=normalized_todo_id,
|
|
298
|
+
)
|
|
299
|
+
lock_target = task_lease_lock_path(
|
|
300
|
+
runtime_root=runtime_root,
|
|
301
|
+
goal_id=normalized_goal_id,
|
|
302
|
+
)
|
|
303
|
+
handoff = handoff or {}
|
|
304
|
+
handoff_mode = str(handoff.get("handoff_mode") or HANDOFF_MODE_LEGACY)
|
|
305
|
+
handoff_gate_overridden = handoff.get("handoff_gate_overridden") is True
|
|
306
|
+
auto_acquire_lease = (
|
|
307
|
+
handoff_mode == HANDOFF_MODE_HARD_LEASE
|
|
308
|
+
and not handoff_gate_overridden
|
|
309
|
+
and not require_active_when_key_supplied
|
|
310
|
+
and str(todo.get("role") or "") == "user"
|
|
311
|
+
and str(todo.get("task_class") or "") == TODO_TASK_CLASS_USER_GATE
|
|
312
|
+
)
|
|
313
|
+
auto_acquired = False
|
|
314
|
+
with exclusive_file_lock(
|
|
315
|
+
lock_target,
|
|
316
|
+
agent_id=actor_agent_id,
|
|
317
|
+
operation="task_lease_mutation_fence",
|
|
318
|
+
):
|
|
319
|
+
lease = read_lease(lease_path)
|
|
320
|
+
time_active = lease_is_active(lease)
|
|
321
|
+
active = time_active
|
|
322
|
+
constraint: dict[str, Any] | None = None
|
|
323
|
+
if active and lease:
|
|
324
|
+
constraint = task_lease_owner_constraint(
|
|
325
|
+
todo,
|
|
326
|
+
owner=lease.get("owner"),
|
|
327
|
+
registered_agents=registered_agent_ids_from_registry(
|
|
328
|
+
registry_path,
|
|
329
|
+
normalized_goal_id,
|
|
330
|
+
),
|
|
331
|
+
)
|
|
332
|
+
active = constraint.get("effective") is True
|
|
333
|
+
|
|
334
|
+
if (
|
|
335
|
+
auto_acquire_lease
|
|
336
|
+
and not active
|
|
337
|
+
and not (time_active and lease)
|
|
338
|
+
):
|
|
339
|
+
# Mint the completion key under the same per-goal lease lock. A
|
|
340
|
+
# time-active lease is never displaced: if one exists but its
|
|
341
|
+
# owner constraint is no longer effective, the hard branch below
|
|
342
|
+
# keeps the loud divergence error.
|
|
343
|
+
normalized_actor = normalize_todo_claimed_by(actor_agent_id)
|
|
344
|
+
if not normalized_actor:
|
|
345
|
+
raise TaskLeaseError(
|
|
346
|
+
"hard_lease handoff mode auto-acquire requires an "
|
|
347
|
+
"attributed actor; provide --agent-id",
|
|
348
|
+
code="handoff_mode_requires_lease",
|
|
349
|
+
payload={
|
|
350
|
+
"goal_id": normalized_goal_id,
|
|
351
|
+
"todo_id": normalized_todo_id,
|
|
352
|
+
"handoff_mode": handoff_mode,
|
|
353
|
+
"lease_path": str(lease_path),
|
|
354
|
+
"reason": "missing_actor",
|
|
355
|
+
},
|
|
356
|
+
)
|
|
357
|
+
constraint = task_lease_owner_constraint(
|
|
358
|
+
todo,
|
|
359
|
+
owner=normalized_actor,
|
|
360
|
+
registered_agents=registered_agent_ids_from_registry(
|
|
361
|
+
registry_path,
|
|
362
|
+
normalized_goal_id,
|
|
363
|
+
),
|
|
364
|
+
)
|
|
365
|
+
if constraint.get("effective") is not True:
|
|
366
|
+
raise TaskLeaseError(
|
|
367
|
+
"hard_lease handoff mode auto-acquire rejected for "
|
|
368
|
+
"the acting agent",
|
|
369
|
+
code="handoff_mode_requires_lease",
|
|
370
|
+
payload={
|
|
371
|
+
"goal_id": normalized_goal_id,
|
|
372
|
+
"todo_id": normalized_todo_id,
|
|
373
|
+
"handoff_mode": handoff_mode,
|
|
374
|
+
"lease_path": str(lease_path),
|
|
375
|
+
"reason": str(
|
|
376
|
+
constraint.get("reason") or "owner_not_allowed"
|
|
377
|
+
),
|
|
378
|
+
"constraint": constraint,
|
|
379
|
+
},
|
|
380
|
+
)
|
|
381
|
+
at = now_utc()
|
|
382
|
+
auto_key = requested_key or f"auto-{normalized_todo_id}"
|
|
383
|
+
updated_at = isoformat(at)
|
|
384
|
+
expires_at = isoformat(
|
|
385
|
+
at + timedelta(seconds=DEFAULT_TASK_LEASE_TTL_SECONDS)
|
|
386
|
+
)
|
|
387
|
+
lease = build_lease(
|
|
388
|
+
goal_id=normalized_goal_id,
|
|
389
|
+
todo_id=normalized_todo_id,
|
|
390
|
+
owner=normalized_actor,
|
|
391
|
+
idempotency_key=auto_key,
|
|
392
|
+
write_scopes=[],
|
|
393
|
+
acquire_ttl_seconds=DEFAULT_TASK_LEASE_TTL_SECONDS,
|
|
394
|
+
version=int((lease or {}).get("version") or 0) + 1,
|
|
395
|
+
acquired_at=updated_at,
|
|
396
|
+
updated_at=updated_at,
|
|
397
|
+
expires_at=expires_at,
|
|
398
|
+
)
|
|
399
|
+
write_lease(lease_path, lease)
|
|
400
|
+
requested_key = auto_key
|
|
401
|
+
auto_acquired = True
|
|
402
|
+
active = True
|
|
403
|
+
time_active = True
|
|
404
|
+
|
|
405
|
+
if not active:
|
|
406
|
+
if handoff_mode == HANDOFF_MODE_HARD_LEASE and not handoff_gate_overridden:
|
|
407
|
+
if time_active and lease:
|
|
408
|
+
raise TaskLeaseError(
|
|
409
|
+
"hard_lease handoff mode found a time-active lease "
|
|
410
|
+
"diverged from the todo projection; refusing keyless "
|
|
411
|
+
"completion. Repair ownership (release or transfer the "
|
|
412
|
+
"lease, or restore claimed_by) before completing.",
|
|
413
|
+
code="handoff_mode_lease_claim_divergence",
|
|
414
|
+
payload={
|
|
415
|
+
"goal_id": normalized_goal_id,
|
|
416
|
+
"todo_id": normalized_todo_id,
|
|
417
|
+
"handoff_mode": handoff_mode,
|
|
418
|
+
"lease_owner": lease.get("owner"),
|
|
419
|
+
"lease_version": lease.get("version"),
|
|
420
|
+
"constraint": constraint,
|
|
421
|
+
"lease_path": str(lease_path),
|
|
422
|
+
},
|
|
423
|
+
)
|
|
424
|
+
raise TaskLeaseError(
|
|
425
|
+
"hard_lease handoff mode requires an effective task lease "
|
|
426
|
+
"to complete this todo; acquire one with "
|
|
427
|
+
"`loopx task-lease acquire`",
|
|
428
|
+
code="handoff_mode_requires_lease",
|
|
429
|
+
payload={
|
|
430
|
+
"goal_id": normalized_goal_id,
|
|
431
|
+
"todo_id": normalized_todo_id,
|
|
432
|
+
"handoff_mode": handoff_mode,
|
|
433
|
+
"lease_path": str(lease_path),
|
|
434
|
+
},
|
|
435
|
+
)
|
|
436
|
+
if (
|
|
437
|
+
require_active_when_key_supplied
|
|
438
|
+
and (requested_key is not None or expected_version is not None)
|
|
439
|
+
):
|
|
440
|
+
raise TaskLeaseError(
|
|
441
|
+
"task lease fence was supplied but no effective lease is active",
|
|
442
|
+
code="lease_not_active",
|
|
443
|
+
payload={
|
|
444
|
+
"goal_id": normalized_goal_id,
|
|
445
|
+
"todo_id": normalized_todo_id,
|
|
446
|
+
"lease_path": str(lease_path),
|
|
447
|
+
},
|
|
448
|
+
)
|
|
449
|
+
yield {
|
|
450
|
+
"schema_version": TASK_LEASE_SCHEMA_VERSION,
|
|
451
|
+
"required": False,
|
|
452
|
+
"active": False,
|
|
453
|
+
}
|
|
454
|
+
return
|
|
455
|
+
|
|
456
|
+
assert lease is not None
|
|
457
|
+
if requested_key is None:
|
|
458
|
+
raise TaskLeaseError(
|
|
459
|
+
"todo has an active task lease; lifecycle mutation requires its idempotency key",
|
|
460
|
+
code="lease_fence_required",
|
|
461
|
+
payload={
|
|
462
|
+
"goal_id": normalized_goal_id,
|
|
463
|
+
"todo_id": normalized_todo_id,
|
|
464
|
+
"lease_owner": lease.get("owner"),
|
|
465
|
+
"lease_version": lease.get("version"),
|
|
466
|
+
"lease_path": str(lease_path),
|
|
467
|
+
},
|
|
468
|
+
)
|
|
469
|
+
normalized_actor = normalize_todo_claimed_by(actor_agent_id)
|
|
470
|
+
if (
|
|
471
|
+
lease.get("owner") != normalized_actor
|
|
472
|
+
or lease.get("idempotency_key") != requested_key
|
|
473
|
+
):
|
|
474
|
+
raise TaskLeaseError(
|
|
475
|
+
"task lease owner or execution-instance key mismatch",
|
|
476
|
+
code="lease_cas_mismatch",
|
|
477
|
+
payload={
|
|
478
|
+
"goal_id": normalized_goal_id,
|
|
479
|
+
"todo_id": normalized_todo_id,
|
|
480
|
+
"lease_owner": lease.get("owner"),
|
|
481
|
+
"lease_version": lease.get("version"),
|
|
482
|
+
"actor_agent_id": normalized_actor,
|
|
483
|
+
"lease_path": str(lease_path),
|
|
484
|
+
},
|
|
485
|
+
)
|
|
486
|
+
assert_expected_version(lease, expected_version)
|
|
487
|
+
fence_payload = {
|
|
488
|
+
"schema_version": TASK_LEASE_SCHEMA_VERSION,
|
|
489
|
+
"required": True,
|
|
490
|
+
"active": True,
|
|
491
|
+
"owner": normalized_actor,
|
|
492
|
+
"version": lease.get("version"),
|
|
493
|
+
"execution_instance_verified": True,
|
|
494
|
+
}
|
|
495
|
+
if auto_acquired:
|
|
496
|
+
fence_payload["auto_acquired"] = True
|
|
497
|
+
fence = _VerifiedTaskLeaseFence(fence_payload)
|
|
498
|
+
fence.release_hook = lambda: remove_lease(lease_path)
|
|
499
|
+
yield fence
|
|
500
|
+
|
|
501
|
+
|
|
502
|
+
def release_verified_task_lease_fence(
|
|
503
|
+
fence: dict[str, Any] | None,
|
|
504
|
+
*,
|
|
505
|
+
committed: bool,
|
|
506
|
+
) -> None:
|
|
507
|
+
"""Release the lease behind a key-verified fence once its write committed.
|
|
508
|
+
|
|
509
|
+
Must run while the hold_task_lease_mutation_fence context is still open so
|
|
510
|
+
the per-goal lease lock it acquired is still held; the lease therefore
|
|
511
|
+
cannot have been renewed, transferred, or re-acquired since the fence
|
|
512
|
+
verified the owner and idempotency key. The release reuses the CLI release
|
|
513
|
+
semantics (the lease file is removed; no new lifecycle state).
|
|
514
|
+
|
|
515
|
+
The private release hook rides on the fence object's attribute, not inside
|
|
516
|
+
the payload mapping, and is disarmed here on every call. Only a committed,
|
|
517
|
+
key-verified fence releases the lease; non-verified fences carry no hook
|
|
518
|
+
and are never touched. A release failure never unwinds the committed
|
|
519
|
+
lifecycle write: it is surfaced additively as fence["released"] = False
|
|
520
|
+
and the lease file is left for an explicit `loopx task-lease release` or
|
|
521
|
+
TTL expiry.
|
|
522
|
+
"""
|
|
523
|
+
|
|
524
|
+
hook = getattr(fence, "release_hook", None)
|
|
525
|
+
if hook is None or not isinstance(fence, dict):
|
|
526
|
+
return
|
|
527
|
+
fence.release_hook = None # type: ignore[attr-defined]
|
|
528
|
+
if not committed or fence.get("execution_instance_verified") is not True:
|
|
529
|
+
return
|
|
530
|
+
try:
|
|
531
|
+
hook()
|
|
532
|
+
except OSError:
|
|
533
|
+
fence["released"] = False
|
|
534
|
+
return
|
|
535
|
+
fence["released"] = True
|
|
536
|
+
|
|
537
|
+
|
|
538
|
+
def runtime_root_from_registry(registry_path: Path, runtime_root_override: str | None) -> Path:
|
|
539
|
+
registry = load_registry(registry_path)
|
|
540
|
+
return resolve_runtime_root(registry, runtime_root_override)
|
|
541
|
+
|
|
542
|
+
|
|
543
|
+
def task_lease_todo_projection(
|
|
544
|
+
*,
|
|
545
|
+
registry_path: Path,
|
|
546
|
+
goal_id: str,
|
|
547
|
+
todo_id: str,
|
|
548
|
+
) -> dict[str, Any] | None:
|
|
549
|
+
"""Resolve the canonical Markdown/event todo projection for lease guards."""
|
|
550
|
+
|
|
551
|
+
from ...todos import list_goal_todos
|
|
552
|
+
|
|
553
|
+
try:
|
|
554
|
+
payload = list_goal_todos(
|
|
555
|
+
registry_path=registry_path,
|
|
556
|
+
goal_id=goal_id,
|
|
557
|
+
todo_id=todo_id,
|
|
558
|
+
)
|
|
559
|
+
except (OSError, ValueError) as exc:
|
|
560
|
+
raise TaskLeaseError(
|
|
561
|
+
"cannot resolve todo projection for task lease",
|
|
562
|
+
code="todo_projection_unavailable",
|
|
563
|
+
payload={"goal_id": goal_id, "todo_id": todo_id, "error": str(exc)},
|
|
564
|
+
) from exc
|
|
565
|
+
todo = payload.get("todo")
|
|
566
|
+
return dict(todo) if isinstance(todo, dict) else None
|
|
567
|
+
|
|
568
|
+
|
|
569
|
+
def task_lease_owner_constraint(
|
|
570
|
+
todo: dict[str, Any] | None,
|
|
571
|
+
*,
|
|
572
|
+
owner: Any,
|
|
573
|
+
registered_agents: list[str] | None = None,
|
|
574
|
+
) -> dict[str, Any]:
|
|
575
|
+
if todo is None:
|
|
576
|
+
return {"effective": False, "reason": "todo_not_found"}
|
|
577
|
+
todo_status = str(todo.get("status") or "").strip().lower()
|
|
578
|
+
if todo_status != "open":
|
|
579
|
+
return {
|
|
580
|
+
"effective": False,
|
|
581
|
+
"reason": "todo_not_open",
|
|
582
|
+
"todo_status": todo_status or "unknown",
|
|
583
|
+
}
|
|
584
|
+
normalized_owner = normalize_todo_claimed_by(owner)
|
|
585
|
+
if not normalized_owner:
|
|
586
|
+
return {"effective": False, "reason": "invalid_owner"}
|
|
587
|
+
if registered_agents is not None and normalized_owner not in registered_agents:
|
|
588
|
+
return {
|
|
589
|
+
"effective": False,
|
|
590
|
+
"reason": "owner_not_registered",
|
|
591
|
+
"registered_agents": registered_agents,
|
|
592
|
+
}
|
|
593
|
+
excluded_agents = normalize_todo_excluded_agents(todo.get("excluded_agents"))
|
|
594
|
+
if normalized_owner in excluded_agents:
|
|
595
|
+
return {
|
|
596
|
+
"effective": False,
|
|
597
|
+
"reason": "owner_excluded_from_todo",
|
|
598
|
+
"excluded_agents": excluded_agents,
|
|
599
|
+
}
|
|
600
|
+
claimed_by = normalize_todo_claimed_by(todo.get("claimed_by"))
|
|
601
|
+
if claimed_by and claimed_by != normalized_owner:
|
|
602
|
+
return {
|
|
603
|
+
"effective": False,
|
|
604
|
+
"reason": "owner_conflicts_with_claim",
|
|
605
|
+
"claimed_by": claimed_by,
|
|
606
|
+
}
|
|
607
|
+
return {"effective": True}
|
|
608
|
+
|
|
609
|
+
|
|
610
|
+
def require_registered_task_lease_owner(
|
|
611
|
+
*,
|
|
612
|
+
registry_path: Path,
|
|
613
|
+
goal_id: str,
|
|
614
|
+
todo_id: str,
|
|
615
|
+
owner: str,
|
|
616
|
+
) -> str:
|
|
617
|
+
try:
|
|
618
|
+
return require_registered_agent_id(
|
|
619
|
+
registry_path=registry_path,
|
|
620
|
+
goal_id=goal_id,
|
|
621
|
+
agent_id=owner,
|
|
622
|
+
field="task lease owner",
|
|
623
|
+
)
|
|
624
|
+
except ValueError as exc:
|
|
625
|
+
raise TaskLeaseError(
|
|
626
|
+
str(exc),
|
|
627
|
+
code="owner_not_registered",
|
|
628
|
+
payload={"goal_id": goal_id, "todo_id": todo_id, "owner": owner},
|
|
629
|
+
) from exc
|
|
630
|
+
|
|
631
|
+
|
|
632
|
+
def require_task_lease_owner_allowed(
|
|
633
|
+
*,
|
|
634
|
+
registry_path: Path,
|
|
635
|
+
goal_id: str,
|
|
636
|
+
todo_id: str,
|
|
637
|
+
owner: str,
|
|
638
|
+
) -> dict[str, Any]:
|
|
639
|
+
owner = require_registered_task_lease_owner(
|
|
640
|
+
registry_path=registry_path,
|
|
641
|
+
goal_id=goal_id,
|
|
642
|
+
todo_id=todo_id,
|
|
643
|
+
owner=owner,
|
|
644
|
+
)
|
|
645
|
+
todo = task_lease_todo_projection(
|
|
646
|
+
registry_path=registry_path,
|
|
647
|
+
goal_id=goal_id,
|
|
648
|
+
todo_id=todo_id,
|
|
649
|
+
)
|
|
650
|
+
constraint = task_lease_owner_constraint(
|
|
651
|
+
todo,
|
|
652
|
+
owner=owner,
|
|
653
|
+
registered_agents=registered_agent_ids_from_registry(registry_path, goal_id),
|
|
654
|
+
)
|
|
655
|
+
if constraint.get("effective") is not True:
|
|
656
|
+
reason = str(constraint.get("reason") or "owner_not_allowed")
|
|
657
|
+
if reason == "todo_not_found":
|
|
658
|
+
message = "todo is missing from the canonical projection"
|
|
659
|
+
elif reason == "todo_not_open":
|
|
660
|
+
message = (
|
|
661
|
+
f"task lease requires an open todo; "
|
|
662
|
+
f"{todo_id!r} is {constraint.get('todo_status')!r}"
|
|
663
|
+
)
|
|
664
|
+
elif reason == "owner_excluded_from_todo":
|
|
665
|
+
message = f"task lease owner {owner!r} is excluded from todo {todo_id!r}"
|
|
666
|
+
elif reason == "owner_conflicts_with_claim":
|
|
667
|
+
message = (
|
|
668
|
+
f"task lease owner {owner!r} conflicts with todo claim "
|
|
669
|
+
f"{constraint.get('claimed_by')!r}"
|
|
670
|
+
)
|
|
671
|
+
elif reason == "owner_not_registered":
|
|
672
|
+
message = f"task lease owner {owner!r} is not registered for goal {goal_id!r}"
|
|
673
|
+
else:
|
|
674
|
+
message = f"task lease owner {owner!r} is not eligible for todo {todo_id!r}"
|
|
675
|
+
raise TaskLeaseError(
|
|
676
|
+
message,
|
|
677
|
+
code=reason,
|
|
678
|
+
payload={
|
|
679
|
+
"goal_id": goal_id,
|
|
680
|
+
"todo_id": todo_id,
|
|
681
|
+
"owner": owner,
|
|
682
|
+
**constraint,
|
|
683
|
+
},
|
|
684
|
+
)
|
|
685
|
+
return todo
|
|
686
|
+
|
|
687
|
+
|
|
688
|
+
def read_lease(path: Path) -> dict[str, Any] | None:
|
|
689
|
+
if not path.exists():
|
|
690
|
+
return None
|
|
691
|
+
try:
|
|
692
|
+
payload = json.loads(path.read_text(encoding="utf-8"))
|
|
693
|
+
except json.JSONDecodeError as exc:
|
|
694
|
+
raise TaskLeaseError(
|
|
695
|
+
f"lease file is not valid JSON: {path}",
|
|
696
|
+
code="corrupt_lease",
|
|
697
|
+
payload={"lease_path": str(path), "error": str(exc)},
|
|
698
|
+
) from exc
|
|
699
|
+
if not isinstance(payload, dict):
|
|
700
|
+
raise TaskLeaseError(
|
|
701
|
+
f"lease file must contain an object: {path}",
|
|
702
|
+
code="corrupt_lease",
|
|
703
|
+
payload={"lease_path": str(path)},
|
|
704
|
+
)
|
|
705
|
+
return payload
|
|
706
|
+
|
|
707
|
+
|
|
708
|
+
def write_lease(path: Path, payload: dict[str, Any]) -> None:
|
|
709
|
+
path.parent.mkdir(parents=True, exist_ok=True)
|
|
710
|
+
temp_path = path.with_name(f".{path.name}.{id(payload)}.tmp")
|
|
711
|
+
temp_path.write_text(json.dumps(payload, ensure_ascii=False, indent=2) + "\n", encoding="utf-8")
|
|
712
|
+
temp_path.replace(path)
|
|
713
|
+
|
|
714
|
+
|
|
715
|
+
def remove_lease(path: Path) -> None:
|
|
716
|
+
try:
|
|
717
|
+
path.unlink()
|
|
718
|
+
except FileNotFoundError:
|
|
719
|
+
pass
|
|
720
|
+
|
|
721
|
+
|
|
722
|
+
def lease_expires_at(lease: dict[str, Any] | None) -> datetime | None:
|
|
723
|
+
return parse_timestamp((lease or {}).get("expires_at"))
|
|
724
|
+
|
|
725
|
+
|
|
726
|
+
def lease_is_active(lease: dict[str, Any] | None, *, at: datetime | None = None) -> bool:
|
|
727
|
+
if not lease or lease.get("schema_version") != TASK_LEASE_SCHEMA_VERSION:
|
|
728
|
+
return False
|
|
729
|
+
if lease.get("status") != "active":
|
|
730
|
+
return False
|
|
731
|
+
expires_at = lease_expires_at(lease)
|
|
732
|
+
return bool(expires_at and expires_at > (at or now_utc()))
|
|
733
|
+
|
|
734
|
+
|
|
735
|
+
def scope_root(scope: str) -> str:
|
|
736
|
+
value = scope.strip()
|
|
737
|
+
if value in {"*", "**", "./"}:
|
|
738
|
+
return ""
|
|
739
|
+
wildcard_indexes = [index for index in (value.find("*"), value.find("?"), value.find("[")) if index >= 0]
|
|
740
|
+
if wildcard_indexes:
|
|
741
|
+
value = value[: min(wildcard_indexes)]
|
|
742
|
+
if "/" in value:
|
|
743
|
+
value = value[: value.rfind("/") + 1]
|
|
744
|
+
return value.rstrip("/")
|
|
745
|
+
|
|
746
|
+
|
|
747
|
+
def _scope_has_glob(scope: str) -> bool:
|
|
748
|
+
return any(token in scope for token in ("*", "?", "["))
|
|
749
|
+
|
|
750
|
+
|
|
751
|
+
def _scope_literal_prefix(scope: str) -> str:
|
|
752
|
+
indexes = [scope.find(token) for token in ("*", "?", "[") if scope.find(token) >= 0]
|
|
753
|
+
return scope[: min(indexes)] if indexes else scope
|
|
754
|
+
|
|
755
|
+
|
|
756
|
+
def _scope_pair_overlaps(left: str, right: str) -> bool:
|
|
757
|
+
if left == right:
|
|
758
|
+
return True
|
|
759
|
+
if left in {"*", "**", "./"} or right in {"*", "**", "./"}:
|
|
760
|
+
return True
|
|
761
|
+
|
|
762
|
+
left_has_glob = _scope_has_glob(left)
|
|
763
|
+
right_has_glob = _scope_has_glob(right)
|
|
764
|
+
if left_has_glob and not right_has_glob:
|
|
765
|
+
prefix = _scope_literal_prefix(left)
|
|
766
|
+
return fnmatch.fnmatchcase(right, left) or (
|
|
767
|
+
prefix.endswith("/") and right.rstrip("/") == prefix.rstrip("/")
|
|
768
|
+
)
|
|
769
|
+
if right_has_glob and not left_has_glob:
|
|
770
|
+
prefix = _scope_literal_prefix(right)
|
|
771
|
+
return fnmatch.fnmatchcase(left, right) or (
|
|
772
|
+
prefix.endswith("/") and left.rstrip("/") == prefix.rstrip("/")
|
|
773
|
+
)
|
|
774
|
+
if left_has_glob and right_has_glob:
|
|
775
|
+
left_prefix = _scope_literal_prefix(left)
|
|
776
|
+
right_prefix = _scope_literal_prefix(right)
|
|
777
|
+
return bool(
|
|
778
|
+
not left_prefix
|
|
779
|
+
or not right_prefix
|
|
780
|
+
or left_prefix.startswith(right_prefix)
|
|
781
|
+
or right_prefix.startswith(left_prefix)
|
|
782
|
+
)
|
|
783
|
+
|
|
784
|
+
left_root = left.rstrip("/")
|
|
785
|
+
right_root = right.rstrip("/")
|
|
786
|
+
return bool(
|
|
787
|
+
(left.endswith("/") and right.startswith(left_root + "/"))
|
|
788
|
+
or (right.endswith("/") and left.startswith(right_root + "/"))
|
|
789
|
+
)
|
|
790
|
+
|
|
791
|
+
|
|
792
|
+
def write_scopes_overlap(left: list[str], right: list[str]) -> bool:
|
|
793
|
+
left_scopes = normalize_required_write_scopes(left)
|
|
794
|
+
right_scopes = normalize_required_write_scopes(right)
|
|
795
|
+
if not left_scopes or not right_scopes:
|
|
796
|
+
return False
|
|
797
|
+
for left_scope in left_scopes:
|
|
798
|
+
for right_scope in right_scopes:
|
|
799
|
+
if _scope_pair_overlaps(left_scope, right_scope):
|
|
800
|
+
return True
|
|
801
|
+
return False
|
|
802
|
+
|
|
803
|
+
|
|
804
|
+
def active_conflicts(
|
|
805
|
+
*,
|
|
806
|
+
registry_path: Path,
|
|
807
|
+
runtime_root: Path,
|
|
808
|
+
goal_id: str,
|
|
809
|
+
todo_id: str,
|
|
810
|
+
write_scopes: list[str],
|
|
811
|
+
at: datetime,
|
|
812
|
+
) -> list[dict[str, Any]]:
|
|
813
|
+
conflicts: list[dict[str, Any]] = []
|
|
814
|
+
registered_agents = registered_agent_ids_from_registry(registry_path, goal_id)
|
|
815
|
+
lease_dir = task_lease_dir(runtime_root=runtime_root, goal_id=goal_id)
|
|
816
|
+
if not lease_dir.exists():
|
|
817
|
+
return conflicts
|
|
818
|
+
for path in sorted(lease_dir.glob("todo_*.json")):
|
|
819
|
+
lease = read_lease(path)
|
|
820
|
+
if not lease_is_active(lease, at=at):
|
|
821
|
+
continue
|
|
822
|
+
lease_todo_id = normalize_lease_todo_id(lease.get("todo_id"))
|
|
823
|
+
if lease_todo_id == todo_id:
|
|
824
|
+
continue
|
|
825
|
+
todo = task_lease_todo_projection(
|
|
826
|
+
registry_path=registry_path,
|
|
827
|
+
goal_id=goal_id,
|
|
828
|
+
todo_id=lease_todo_id,
|
|
829
|
+
)
|
|
830
|
+
if task_lease_owner_constraint(
|
|
831
|
+
todo,
|
|
832
|
+
owner=lease.get("owner"),
|
|
833
|
+
registered_agents=registered_agents,
|
|
834
|
+
).get("effective") is not True:
|
|
835
|
+
continue
|
|
836
|
+
if write_scopes_overlap(write_scopes, normalize_required_write_scopes(lease.get("write_scopes"))):
|
|
837
|
+
conflicts.append(
|
|
838
|
+
{
|
|
839
|
+
"todo_id": lease.get("todo_id"),
|
|
840
|
+
"owner": lease.get("owner"),
|
|
841
|
+
"expires_at": lease.get("expires_at"),
|
|
842
|
+
"write_scopes": lease.get("write_scopes") or [],
|
|
843
|
+
"lease_path": str(path),
|
|
844
|
+
}
|
|
845
|
+
)
|
|
846
|
+
return conflicts
|
|
847
|
+
|
|
848
|
+
|
|
849
|
+
def assert_expected_version(lease: dict[str, Any] | None, expected_version: int | None) -> None:
|
|
850
|
+
if expected_version is None:
|
|
851
|
+
return
|
|
852
|
+
actual = int((lease or {}).get("version") or 0)
|
|
853
|
+
if actual != expected_version:
|
|
854
|
+
raise TaskLeaseError(
|
|
855
|
+
f"lease version mismatch: expected {expected_version}, got {actual}",
|
|
856
|
+
code="version_mismatch",
|
|
857
|
+
payload={"expected_version": expected_version, "actual_version": actual},
|
|
858
|
+
)
|
|
859
|
+
|
|
860
|
+
|
|
861
|
+
def build_lease(
|
|
862
|
+
*,
|
|
863
|
+
goal_id: str,
|
|
864
|
+
todo_id: str,
|
|
865
|
+
owner: str,
|
|
866
|
+
idempotency_key: str,
|
|
867
|
+
write_scopes: list[str],
|
|
868
|
+
acquire_ttl_seconds: int,
|
|
869
|
+
version: int,
|
|
870
|
+
acquired_at: str,
|
|
871
|
+
updated_at: str,
|
|
872
|
+
expires_at: str,
|
|
873
|
+
) -> dict[str, Any]:
|
|
874
|
+
return {
|
|
875
|
+
"schema_version": TASK_LEASE_SCHEMA_VERSION,
|
|
876
|
+
"goal_id": goal_id,
|
|
877
|
+
"todo_id": todo_id,
|
|
878
|
+
"owner": owner,
|
|
879
|
+
"idempotency_key": idempotency_key,
|
|
880
|
+
"write_scopes": write_scopes,
|
|
881
|
+
"acquire_ttl_seconds": acquire_ttl_seconds,
|
|
882
|
+
"version": version,
|
|
883
|
+
"acquired_at": acquired_at,
|
|
884
|
+
"updated_at": updated_at,
|
|
885
|
+
"expires_at": expires_at,
|
|
886
|
+
"status": "active",
|
|
887
|
+
}
|
|
888
|
+
|
|
889
|
+
|
|
890
|
+
def acquire_task_lease(
|
|
891
|
+
*,
|
|
892
|
+
registry_path: Path,
|
|
893
|
+
runtime_root: Path,
|
|
894
|
+
goal_id: str,
|
|
895
|
+
todo_id: str,
|
|
896
|
+
owner: str,
|
|
897
|
+
idempotency_key: str,
|
|
898
|
+
ttl_seconds: int | None = None,
|
|
899
|
+
write_scopes: list[str] | None = None,
|
|
900
|
+
expected_version: int | None = None,
|
|
901
|
+
) -> dict[str, Any]:
|
|
902
|
+
goal_id = normalize_goal_id(goal_id)
|
|
903
|
+
todo_id = normalize_lease_todo_id(todo_id)
|
|
904
|
+
owner = normalize_owner(owner)
|
|
905
|
+
idempotency_key = normalize_idempotency_key(idempotency_key)
|
|
906
|
+
ttl = normalize_ttl_seconds(ttl_seconds)
|
|
907
|
+
normalized_write_scopes = normalize_required_write_scopes(write_scopes)
|
|
908
|
+
lease_dir = task_lease_dir(runtime_root=runtime_root, goal_id=goal_id)
|
|
909
|
+
lock_target = task_lease_lock_path(runtime_root=runtime_root, goal_id=goal_id)
|
|
910
|
+
lease_path = task_lease_path(runtime_root=runtime_root, goal_id=goal_id, todo_id=todo_id)
|
|
911
|
+
at = now_utc()
|
|
912
|
+
with exclusive_file_lock(
|
|
913
|
+
lock_target,
|
|
914
|
+
agent_id=owner,
|
|
915
|
+
operation="task_lease_acquire",
|
|
916
|
+
):
|
|
917
|
+
handoff_mode = _require_lease_mutation_allowed_by_handoff_mode(
|
|
918
|
+
registry_path=registry_path,
|
|
919
|
+
goal_id=goal_id,
|
|
920
|
+
todo_id=todo_id,
|
|
921
|
+
action="acquire",
|
|
922
|
+
)
|
|
923
|
+
todo = require_task_lease_owner_allowed(
|
|
924
|
+
registry_path=registry_path,
|
|
925
|
+
goal_id=goal_id,
|
|
926
|
+
todo_id=todo_id,
|
|
927
|
+
owner=owner,
|
|
928
|
+
)
|
|
929
|
+
existing = read_lease(lease_path)
|
|
930
|
+
assert_expected_version(existing, expected_version)
|
|
931
|
+
existing_is_effective = bool(
|
|
932
|
+
lease_is_active(existing, at=at)
|
|
933
|
+
and task_lease_owner_constraint(
|
|
934
|
+
todo,
|
|
935
|
+
owner=(existing or {}).get("owner"),
|
|
936
|
+
registered_agents=registered_agent_ids_from_registry(registry_path, goal_id),
|
|
937
|
+
).get("effective")
|
|
938
|
+
is True
|
|
939
|
+
)
|
|
940
|
+
if existing_is_effective:
|
|
941
|
+
if (
|
|
942
|
+
existing.get("owner") == owner
|
|
943
|
+
and existing.get("idempotency_key") == idempotency_key
|
|
944
|
+
):
|
|
945
|
+
existing_write_scopes = normalize_required_write_scopes(
|
|
946
|
+
existing.get("write_scopes")
|
|
947
|
+
)
|
|
948
|
+
existing_ttl = existing.get("acquire_ttl_seconds")
|
|
949
|
+
request_matches = set(existing_write_scopes) == set(normalized_write_scopes)
|
|
950
|
+
if existing_ttl is not None:
|
|
951
|
+
request_matches = request_matches and int(existing_ttl) == ttl
|
|
952
|
+
if not request_matches:
|
|
953
|
+
raise TaskLeaseError(
|
|
954
|
+
"idempotency key was reused with different acquire parameters",
|
|
955
|
+
code="idempotency_key_reuse",
|
|
956
|
+
payload={
|
|
957
|
+
"lease": existing,
|
|
958
|
+
"lease_path": str(lease_path),
|
|
959
|
+
"requested_write_scopes": normalized_write_scopes,
|
|
960
|
+
"requested_ttl_seconds": ttl,
|
|
961
|
+
},
|
|
962
|
+
)
|
|
963
|
+
return {
|
|
964
|
+
"ok": True,
|
|
965
|
+
"schema_version": TASK_LEASE_SCHEMA_VERSION,
|
|
966
|
+
"action": "acquire",
|
|
967
|
+
"acquired": False,
|
|
968
|
+
"idempotent": True,
|
|
969
|
+
"lease": existing,
|
|
970
|
+
"lease_path": str(lease_path),
|
|
971
|
+
"handoff_mode": handoff_mode,
|
|
972
|
+
}
|
|
973
|
+
raise TaskLeaseError(
|
|
974
|
+
"todo already has an active lease",
|
|
975
|
+
code="todo_lease_conflict",
|
|
976
|
+
payload={"lease": existing, "lease_path": str(lease_path)},
|
|
977
|
+
)
|
|
978
|
+
conflicts = active_conflicts(
|
|
979
|
+
registry_path=registry_path,
|
|
980
|
+
runtime_root=runtime_root,
|
|
981
|
+
goal_id=goal_id,
|
|
982
|
+
todo_id=todo_id,
|
|
983
|
+
write_scopes=normalized_write_scopes,
|
|
984
|
+
at=at,
|
|
985
|
+
)
|
|
986
|
+
if conflicts:
|
|
987
|
+
raise TaskLeaseError(
|
|
988
|
+
"write scope overlaps another active task lease",
|
|
989
|
+
code="write_scope_conflict",
|
|
990
|
+
payload={"conflicts": conflicts},
|
|
991
|
+
)
|
|
992
|
+
updated_at = isoformat(at)
|
|
993
|
+
expires_at = isoformat(at + timedelta(seconds=ttl))
|
|
994
|
+
lease = build_lease(
|
|
995
|
+
goal_id=goal_id,
|
|
996
|
+
todo_id=todo_id,
|
|
997
|
+
owner=owner,
|
|
998
|
+
idempotency_key=idempotency_key,
|
|
999
|
+
write_scopes=normalized_write_scopes,
|
|
1000
|
+
acquire_ttl_seconds=ttl,
|
|
1001
|
+
version=int((existing or {}).get("version") or 0) + 1,
|
|
1002
|
+
acquired_at=updated_at,
|
|
1003
|
+
updated_at=updated_at,
|
|
1004
|
+
expires_at=expires_at,
|
|
1005
|
+
)
|
|
1006
|
+
lease_dir.mkdir(parents=True, exist_ok=True)
|
|
1007
|
+
write_lease(lease_path, lease)
|
|
1008
|
+
return {
|
|
1009
|
+
"ok": True,
|
|
1010
|
+
"schema_version": TASK_LEASE_SCHEMA_VERSION,
|
|
1011
|
+
"action": "acquire",
|
|
1012
|
+
"acquired": True,
|
|
1013
|
+
"idempotent": False,
|
|
1014
|
+
"lease": lease,
|
|
1015
|
+
"lease_path": str(lease_path),
|
|
1016
|
+
"handoff_mode": handoff_mode,
|
|
1017
|
+
}
|
|
1018
|
+
|
|
1019
|
+
|
|
1020
|
+
def renew_task_lease(
|
|
1021
|
+
*,
|
|
1022
|
+
registry_path: Path,
|
|
1023
|
+
runtime_root: Path,
|
|
1024
|
+
goal_id: str,
|
|
1025
|
+
todo_id: str,
|
|
1026
|
+
owner: str,
|
|
1027
|
+
idempotency_key: str,
|
|
1028
|
+
ttl_seconds: int | None = None,
|
|
1029
|
+
expected_version: int | None = None,
|
|
1030
|
+
) -> dict[str, Any]:
|
|
1031
|
+
goal_id = normalize_goal_id(goal_id)
|
|
1032
|
+
todo_id = normalize_lease_todo_id(todo_id)
|
|
1033
|
+
owner = normalize_owner(owner)
|
|
1034
|
+
idempotency_key = normalize_idempotency_key(idempotency_key)
|
|
1035
|
+
ttl = normalize_ttl_seconds(ttl_seconds)
|
|
1036
|
+
lock_target = task_lease_lock_path(runtime_root=runtime_root, goal_id=goal_id)
|
|
1037
|
+
lease_path = task_lease_path(runtime_root=runtime_root, goal_id=goal_id, todo_id=todo_id)
|
|
1038
|
+
at = now_utc()
|
|
1039
|
+
with exclusive_file_lock(
|
|
1040
|
+
lock_target,
|
|
1041
|
+
agent_id=owner,
|
|
1042
|
+
operation="task_lease_renew",
|
|
1043
|
+
):
|
|
1044
|
+
handoff_mode = _require_lease_mutation_allowed_by_handoff_mode(
|
|
1045
|
+
registry_path=registry_path,
|
|
1046
|
+
goal_id=goal_id,
|
|
1047
|
+
todo_id=todo_id,
|
|
1048
|
+
action="renew",
|
|
1049
|
+
)
|
|
1050
|
+
require_task_lease_owner_allowed(
|
|
1051
|
+
registry_path=registry_path,
|
|
1052
|
+
goal_id=goal_id,
|
|
1053
|
+
todo_id=todo_id,
|
|
1054
|
+
owner=owner,
|
|
1055
|
+
)
|
|
1056
|
+
lease = read_lease(lease_path)
|
|
1057
|
+
assert_expected_version(lease, expected_version)
|
|
1058
|
+
if not lease_is_active(lease, at=at):
|
|
1059
|
+
raise TaskLeaseError("lease is missing or expired", code="lease_not_active")
|
|
1060
|
+
if lease.get("owner") != owner or lease.get("idempotency_key") != idempotency_key:
|
|
1061
|
+
raise TaskLeaseError("lease owner or idempotency key mismatch", code="lease_cas_mismatch")
|
|
1062
|
+
lease = dict(lease)
|
|
1063
|
+
lease["version"] = int(lease.get("version") or 0) + 1
|
|
1064
|
+
lease["updated_at"] = isoformat(at)
|
|
1065
|
+
lease["expires_at"] = isoformat(at + timedelta(seconds=ttl))
|
|
1066
|
+
write_lease(lease_path, lease)
|
|
1067
|
+
return {
|
|
1068
|
+
"ok": True,
|
|
1069
|
+
"schema_version": TASK_LEASE_SCHEMA_VERSION,
|
|
1070
|
+
"action": "renew",
|
|
1071
|
+
"renewed": True,
|
|
1072
|
+
"lease": lease,
|
|
1073
|
+
"lease_path": str(lease_path),
|
|
1074
|
+
"handoff_mode": handoff_mode,
|
|
1075
|
+
}
|
|
1076
|
+
|
|
1077
|
+
|
|
1078
|
+
def transfer_task_lease(
|
|
1079
|
+
*,
|
|
1080
|
+
registry_path: Path,
|
|
1081
|
+
runtime_root: Path,
|
|
1082
|
+
goal_id: str,
|
|
1083
|
+
todo_id: str,
|
|
1084
|
+
owner: str,
|
|
1085
|
+
idempotency_key: str,
|
|
1086
|
+
new_owner: str,
|
|
1087
|
+
new_idempotency_key: str,
|
|
1088
|
+
ttl_seconds: int | None = None,
|
|
1089
|
+
expected_version: int | None = None,
|
|
1090
|
+
) -> dict[str, Any]:
|
|
1091
|
+
goal_id = normalize_goal_id(goal_id)
|
|
1092
|
+
todo_id = normalize_lease_todo_id(todo_id)
|
|
1093
|
+
owner = normalize_owner(owner)
|
|
1094
|
+
idempotency_key = normalize_idempotency_key(idempotency_key)
|
|
1095
|
+
new_owner = normalize_owner(new_owner)
|
|
1096
|
+
new_idempotency_key = normalize_idempotency_key(new_idempotency_key)
|
|
1097
|
+
ttl = normalize_ttl_seconds(ttl_seconds)
|
|
1098
|
+
lock_target = task_lease_lock_path(runtime_root=runtime_root, goal_id=goal_id)
|
|
1099
|
+
lease_path = task_lease_path(runtime_root=runtime_root, goal_id=goal_id, todo_id=todo_id)
|
|
1100
|
+
at = now_utc()
|
|
1101
|
+
with exclusive_file_lock(
|
|
1102
|
+
lock_target,
|
|
1103
|
+
agent_id=owner,
|
|
1104
|
+
operation="task_lease_transfer",
|
|
1105
|
+
):
|
|
1106
|
+
handoff_mode = _require_lease_mutation_allowed_by_handoff_mode(
|
|
1107
|
+
registry_path=registry_path,
|
|
1108
|
+
goal_id=goal_id,
|
|
1109
|
+
todo_id=todo_id,
|
|
1110
|
+
action="transfer",
|
|
1111
|
+
)
|
|
1112
|
+
require_registered_task_lease_owner(
|
|
1113
|
+
registry_path=registry_path,
|
|
1114
|
+
goal_id=goal_id,
|
|
1115
|
+
todo_id=todo_id,
|
|
1116
|
+
owner=owner,
|
|
1117
|
+
)
|
|
1118
|
+
require_task_lease_owner_allowed(
|
|
1119
|
+
registry_path=registry_path,
|
|
1120
|
+
goal_id=goal_id,
|
|
1121
|
+
todo_id=todo_id,
|
|
1122
|
+
owner=new_owner,
|
|
1123
|
+
)
|
|
1124
|
+
lease = read_lease(lease_path)
|
|
1125
|
+
assert_expected_version(lease, expected_version)
|
|
1126
|
+
if not lease_is_active(lease, at=at):
|
|
1127
|
+
raise TaskLeaseError("lease is missing or expired", code="lease_not_active")
|
|
1128
|
+
if lease.get("owner") != owner or lease.get("idempotency_key") != idempotency_key:
|
|
1129
|
+
raise TaskLeaseError("lease owner or idempotency key mismatch", code="lease_cas_mismatch")
|
|
1130
|
+
lease = dict(lease)
|
|
1131
|
+
lease["owner"] = new_owner
|
|
1132
|
+
lease["idempotency_key"] = new_idempotency_key
|
|
1133
|
+
lease["version"] = int(lease.get("version") or 0) + 1
|
|
1134
|
+
lease["updated_at"] = isoformat(at)
|
|
1135
|
+
lease["expires_at"] = isoformat(at + timedelta(seconds=ttl))
|
|
1136
|
+
write_lease(lease_path, lease)
|
|
1137
|
+
return {
|
|
1138
|
+
"ok": True,
|
|
1139
|
+
"schema_version": TASK_LEASE_SCHEMA_VERSION,
|
|
1140
|
+
"action": "transfer",
|
|
1141
|
+
"transferred": True,
|
|
1142
|
+
"lease": lease,
|
|
1143
|
+
"lease_path": str(lease_path),
|
|
1144
|
+
"handoff_mode": handoff_mode,
|
|
1145
|
+
}
|
|
1146
|
+
|
|
1147
|
+
|
|
1148
|
+
def release_task_lease(
|
|
1149
|
+
*,
|
|
1150
|
+
runtime_root: Path,
|
|
1151
|
+
goal_id: str,
|
|
1152
|
+
todo_id: str,
|
|
1153
|
+
owner: str,
|
|
1154
|
+
idempotency_key: str,
|
|
1155
|
+
expected_version: int | None = None,
|
|
1156
|
+
registry_path: Path | None = None,
|
|
1157
|
+
) -> dict[str, Any]:
|
|
1158
|
+
goal_id = normalize_goal_id(goal_id)
|
|
1159
|
+
todo_id = normalize_lease_todo_id(todo_id)
|
|
1160
|
+
owner = normalize_owner(owner)
|
|
1161
|
+
idempotency_key = normalize_idempotency_key(idempotency_key)
|
|
1162
|
+
# Release stays allowed in every handoff mode (cleanup of legacy
|
|
1163
|
+
# leftovers); the mode is reported additively when resolvable.
|
|
1164
|
+
handoff_mode = _optional_handoff_mode(registry_path, goal_id)
|
|
1165
|
+
handoff_extra = {"handoff_mode": handoff_mode} if handoff_mode else {}
|
|
1166
|
+
lock_target = task_lease_lock_path(runtime_root=runtime_root, goal_id=goal_id)
|
|
1167
|
+
lease_path = task_lease_path(runtime_root=runtime_root, goal_id=goal_id, todo_id=todo_id)
|
|
1168
|
+
at = now_utc()
|
|
1169
|
+
with exclusive_file_lock(
|
|
1170
|
+
lock_target,
|
|
1171
|
+
agent_id=owner,
|
|
1172
|
+
operation="task_lease_release",
|
|
1173
|
+
):
|
|
1174
|
+
lease = read_lease(lease_path)
|
|
1175
|
+
assert_expected_version(lease, expected_version)
|
|
1176
|
+
if not lease:
|
|
1177
|
+
return {
|
|
1178
|
+
"ok": True,
|
|
1179
|
+
"schema_version": TASK_LEASE_SCHEMA_VERSION,
|
|
1180
|
+
"action": "release",
|
|
1181
|
+
"released": False,
|
|
1182
|
+
"missing": True,
|
|
1183
|
+
"lease_path": str(lease_path),
|
|
1184
|
+
**handoff_extra,
|
|
1185
|
+
}
|
|
1186
|
+
if lease_is_active(lease, at=at) and (
|
|
1187
|
+
lease.get("owner") != owner or lease.get("idempotency_key") != idempotency_key
|
|
1188
|
+
):
|
|
1189
|
+
raise TaskLeaseError("lease owner or idempotency key mismatch", code="lease_cas_mismatch")
|
|
1190
|
+
remove_lease(lease_path)
|
|
1191
|
+
released_lease = dict(lease)
|
|
1192
|
+
released_lease["status"] = "released"
|
|
1193
|
+
released_lease["released_at"] = isoformat(at)
|
|
1194
|
+
released_lease["updated_at"] = isoformat(at)
|
|
1195
|
+
return {
|
|
1196
|
+
"ok": True,
|
|
1197
|
+
"schema_version": TASK_LEASE_SCHEMA_VERSION,
|
|
1198
|
+
"action": "release",
|
|
1199
|
+
"released": True,
|
|
1200
|
+
"lease": released_lease,
|
|
1201
|
+
"lease_path": str(lease_path),
|
|
1202
|
+
**handoff_extra,
|
|
1203
|
+
}
|
|
1204
|
+
|
|
1205
|
+
|
|
1206
|
+
def inspect_task_lease(
|
|
1207
|
+
*,
|
|
1208
|
+
registry_path: Path,
|
|
1209
|
+
runtime_root: Path,
|
|
1210
|
+
goal_id: str,
|
|
1211
|
+
todo_id: str,
|
|
1212
|
+
) -> dict[str, Any]:
|
|
1213
|
+
goal_id = normalize_goal_id(goal_id)
|
|
1214
|
+
todo_id = normalize_lease_todo_id(todo_id)
|
|
1215
|
+
lease_path = task_lease_path(runtime_root=runtime_root, goal_id=goal_id, todo_id=todo_id)
|
|
1216
|
+
lease = read_lease(lease_path)
|
|
1217
|
+
active = lease_is_active(lease)
|
|
1218
|
+
executor_constraint: dict[str, Any] | None = None
|
|
1219
|
+
if active and lease:
|
|
1220
|
+
try:
|
|
1221
|
+
todo = task_lease_todo_projection(
|
|
1222
|
+
registry_path=registry_path,
|
|
1223
|
+
goal_id=goal_id,
|
|
1224
|
+
todo_id=todo_id,
|
|
1225
|
+
)
|
|
1226
|
+
except TaskLeaseError as exc:
|
|
1227
|
+
active = False
|
|
1228
|
+
executor_constraint = {
|
|
1229
|
+
"effective": False,
|
|
1230
|
+
"reason": exc.code,
|
|
1231
|
+
}
|
|
1232
|
+
else:
|
|
1233
|
+
executor_constraint = task_lease_owner_constraint(
|
|
1234
|
+
todo,
|
|
1235
|
+
owner=lease.get("owner"),
|
|
1236
|
+
registered_agents=registered_agent_ids_from_registry(registry_path, goal_id),
|
|
1237
|
+
)
|
|
1238
|
+
if executor_constraint.get("effective") is not True:
|
|
1239
|
+
active = False
|
|
1240
|
+
else:
|
|
1241
|
+
executor_constraint = None
|
|
1242
|
+
handoff_mode = _optional_handoff_mode(registry_path, goal_id)
|
|
1243
|
+
return {
|
|
1244
|
+
"ok": True,
|
|
1245
|
+
"schema_version": TASK_LEASE_SCHEMA_VERSION,
|
|
1246
|
+
"action": "inspect",
|
|
1247
|
+
"goal_id": goal_id,
|
|
1248
|
+
"todo_id": todo_id,
|
|
1249
|
+
"active": active,
|
|
1250
|
+
"lease": lease,
|
|
1251
|
+
"lease_path": str(lease_path),
|
|
1252
|
+
**({"handoff_mode": handoff_mode} if handoff_mode else {}),
|
|
1253
|
+
**({"executor_constraint": executor_constraint} if executor_constraint else {}),
|
|
1254
|
+
}
|