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,1018 @@
|
|
|
1
|
+
// LoopX OpenCode 2 goal worker.
|
|
2
|
+
//
|
|
3
|
+
// A persistent out-of-process goal loop driver for OpenCode 2. OpenCode 2
|
|
4
|
+
// ships a new plugin API and its plugins cannot run the OpenCode 1 goal
|
|
5
|
+
// bridge, so LoopX drives OpenCode 2 sessions through the OpenCode 2 HTTP
|
|
6
|
+
// API instead:
|
|
7
|
+
//
|
|
8
|
+
// prompt -> wait for the assistant turn to complete -> loopx quota
|
|
9
|
+
// should-run -> run_now: continue | wait: backoff ladder | stop: visible
|
|
10
|
+
// pause | terminal_no_followup: visible completion
|
|
11
|
+
//
|
|
12
|
+
// The worker owns the timers, so recurring operation survives TUI close and
|
|
13
|
+
// one-shot CLI usage. Session state, pause reason, and poll bookkeeping are
|
|
14
|
+
// persisted so a crashed worker can be restarted.
|
|
15
|
+
//
|
|
16
|
+
// This file has no runtime dependencies. It shells out to `opencode2 api`
|
|
17
|
+
// (which performs its own service discovery and authentication) and to
|
|
18
|
+
// `loopx` for quota decisions.
|
|
19
|
+
|
|
20
|
+
import { execFile as execFileCallback } from "node:child_process"
|
|
21
|
+
import { promises as fs, realpathSync } from "node:fs"
|
|
22
|
+
import os from "node:os"
|
|
23
|
+
import path from "node:path"
|
|
24
|
+
import { promisify } from "node:util"
|
|
25
|
+
import { fileURLToPath } from "node:url"
|
|
26
|
+
|
|
27
|
+
const execFile = promisify(execFileCallback)
|
|
28
|
+
|
|
29
|
+
export const STATE_SCHEMA_VERSION = "loopx_opencode2_goal_worker_v0"
|
|
30
|
+
export const TERMINAL_STATE_SCHEMA_VERSION = "goal_terminal_state_v0"
|
|
31
|
+
export const SOURCE_COMPLETENESS_SCHEMA_VERSION = "goal_terminal_source_completeness_v0"
|
|
32
|
+
export const DEFAULT_RETRY_MINUTES = 3
|
|
33
|
+
export const MAX_RETRY_MINUTES = 30
|
|
34
|
+
export const MESSAGE_POLL_MS = 3_000
|
|
35
|
+
export const DEFAULT_TURN_TIMEOUT_MS = 120 * 60 * 1000
|
|
36
|
+
export const DEFAULT_MAX_TURNS = 10_000
|
|
37
|
+
export const DEFAULT_MAX_DURATION_MS = 30 * 24 * 60 * 60 * 1000
|
|
38
|
+
export const LEASE_STALE_MS = 5 * 60 * 1000
|
|
39
|
+
|
|
40
|
+
export function continuationPrompt(directory) {
|
|
41
|
+
const anchor = directory
|
|
42
|
+
? `\nWorking directory: ${directory}\nStay inside this project; do not touch other goals or repositories, and never run loopx install/update/rollback/slash-commands operations or modify the host loopx installation.`
|
|
43
|
+
: ""
|
|
44
|
+
return [
|
|
45
|
+
"LoopX quota granted the next automatic turn.",
|
|
46
|
+
"Continue the goal work and report progress.",
|
|
47
|
+
"Do not self-declare completion; LoopX validates closure.",
|
|
48
|
+
anchor,
|
|
49
|
+
]
|
|
50
|
+
.filter(Boolean)
|
|
51
|
+
.join(" ")
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export const CONTINUATION_PROMPT = continuationPrompt()
|
|
55
|
+
|
|
56
|
+
export const STANDBY_MINUTES = 5
|
|
57
|
+
|
|
58
|
+
const TERMINAL_NOTICE = [
|
|
59
|
+
"LoopX validated terminal closure for the current work.",
|
|
60
|
+
"The worker stays attached and will resume automatically when new work appears.",
|
|
61
|
+
].join(" ")
|
|
62
|
+
const LIMIT_NOTICE = [
|
|
63
|
+
"LoopX stopped this goal after the unchanged-poll limit.",
|
|
64
|
+
"No new work was scheduled; review LoopX todos and resume explicitly when ready.",
|
|
65
|
+
].join(" ")
|
|
66
|
+
const TURN_LIMIT_NOTICE = [
|
|
67
|
+
"LoopX stopped this goal after the turn budget was exhausted.",
|
|
68
|
+
"Review LoopX todos before requesting another run.",
|
|
69
|
+
].join(" ")
|
|
70
|
+
const DURATION_LIMIT_NOTICE = [
|
|
71
|
+
"LoopX stopped this goal after the duration budget was exhausted.",
|
|
72
|
+
"Review LoopX todos before requesting another run.",
|
|
73
|
+
].join(" ")
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
export function sanitizedStateName(value) {
|
|
77
|
+
const text = String(value || "").replace(/[^A-Za-z0-9_-]/g, "_")
|
|
78
|
+
if (!text) throw new Error("goal id is required")
|
|
79
|
+
return text.slice(0, 160)
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
export function defaultStateRoot() {
|
|
84
|
+
if (process.env.LOOPX_OPENCODE2_STATE_DIR) {
|
|
85
|
+
return path.resolve(process.env.LOOPX_OPENCODE2_STATE_DIR)
|
|
86
|
+
}
|
|
87
|
+
const xdgState = process.env.XDG_STATE_HOME || path.join(os.homedir(), ".local", "state")
|
|
88
|
+
return path.join(xdgState, "loopx", "opencode2")
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
export function createStateStore({ root = defaultStateRoot(), now = () => Date.now() } = {}) {
|
|
93
|
+
const target = (goalId) => path.join(root, `${sanitizedStateName(goalId)}.json`)
|
|
94
|
+
return {
|
|
95
|
+
async read(goalId) {
|
|
96
|
+
try {
|
|
97
|
+
const payload = JSON.parse(await fs.readFile(target(goalId), "utf8"))
|
|
98
|
+
if (payload?.schemaVersion !== STATE_SCHEMA_VERSION || payload?.goalId !== goalId) {
|
|
99
|
+
throw new Error("invalid LoopX OpenCode 2 worker state")
|
|
100
|
+
}
|
|
101
|
+
return payload
|
|
102
|
+
} catch (error) {
|
|
103
|
+
if (error?.code === "ENOENT") return null
|
|
104
|
+
throw error
|
|
105
|
+
}
|
|
106
|
+
},
|
|
107
|
+
async write(goalId, state) {
|
|
108
|
+
await fs.mkdir(root, { recursive: true, mode: 0o700 })
|
|
109
|
+
const destination = target(goalId)
|
|
110
|
+
const temporary = `${destination}.${process.pid}.${now()}.tmp`
|
|
111
|
+
const payload = {
|
|
112
|
+
schemaVersion: STATE_SCHEMA_VERSION,
|
|
113
|
+
goalId,
|
|
114
|
+
...state,
|
|
115
|
+
updatedAt: new Date(now()).toISOString(),
|
|
116
|
+
}
|
|
117
|
+
await fs.writeFile(temporary, `${JSON.stringify(payload, null, 2)}\n`, {
|
|
118
|
+
encoding: "utf8",
|
|
119
|
+
mode: 0o600,
|
|
120
|
+
})
|
|
121
|
+
await fs.rename(temporary, destination)
|
|
122
|
+
await fs.chmod(destination, 0o600)
|
|
123
|
+
return payload
|
|
124
|
+
},
|
|
125
|
+
lockPath(goalId) {
|
|
126
|
+
return `${target(goalId)}.lock`
|
|
127
|
+
},
|
|
128
|
+
async acquireLock(goalId, { staleMs = LEASE_STALE_MS, heartbeatMs = 60_000 } = {}) {
|
|
129
|
+
const lockPath = this.lockPath(goalId)
|
|
130
|
+
const release = async () => {
|
|
131
|
+
try {
|
|
132
|
+
await fs.unlink(lockPath)
|
|
133
|
+
} catch (error) {
|
|
134
|
+
if (error?.code !== "ENOENT") throw error
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
const heartbeat = async () => {
|
|
138
|
+
await fs.mkdir(root, { recursive: true, mode: 0o700 })
|
|
139
|
+
const temporary = `${lockPath}.${process.pid}.tmp`
|
|
140
|
+
await fs.writeFile(
|
|
141
|
+
temporary,
|
|
142
|
+
`${JSON.stringify({ pid: process.pid, heartbeatAt: new Date().toISOString() })}\n`,
|
|
143
|
+
{ encoding: "utf8", mode: 0o600 },
|
|
144
|
+
)
|
|
145
|
+
await fs.rename(temporary, lockPath)
|
|
146
|
+
await fs.chmod(lockPath, 0o600)
|
|
147
|
+
}
|
|
148
|
+
const existing = await readLock(lockPath)
|
|
149
|
+
if (existing && (await leaseIsLive(existing, { staleMs }))) {
|
|
150
|
+
return {
|
|
151
|
+
acquired: false,
|
|
152
|
+
holder: existing,
|
|
153
|
+
reason: `held by pid ${existing.pid} with a fresh heartbeat`,
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
await heartbeat()
|
|
157
|
+
const timer = setInterval(() => {
|
|
158
|
+
heartbeat().catch(() => {})
|
|
159
|
+
}, heartbeatMs)
|
|
160
|
+
if (typeof timer?.unref === "function") timer.unref()
|
|
161
|
+
return {
|
|
162
|
+
acquired: true,
|
|
163
|
+
holder: { pid: process.pid, heartbeatAt: new Date().toISOString() },
|
|
164
|
+
reason: "",
|
|
165
|
+
release: async () => {
|
|
166
|
+
clearInterval(timer)
|
|
167
|
+
await release()
|
|
168
|
+
},
|
|
169
|
+
}
|
|
170
|
+
},
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
|
|
175
|
+
async function readLock(lockPath) {
|
|
176
|
+
try {
|
|
177
|
+
const payload = JSON.parse(await fs.readFile(lockPath, "utf8"))
|
|
178
|
+
if (!Number.isInteger(payload?.pid)) return null
|
|
179
|
+
return payload
|
|
180
|
+
} catch (error) {
|
|
181
|
+
if (error?.code === "ENOENT") return null
|
|
182
|
+
try {
|
|
183
|
+
await fs.unlink(lockPath)
|
|
184
|
+
} catch {
|
|
185
|
+
// A malformed lock is retried as unreadable; removal is best-effort.
|
|
186
|
+
}
|
|
187
|
+
return null
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
|
|
192
|
+
export function pidIsAlive(pid) {
|
|
193
|
+
try {
|
|
194
|
+
process.kill(pid, 0)
|
|
195
|
+
return true
|
|
196
|
+
} catch (error) {
|
|
197
|
+
return error?.code === "EPERM"
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
|
|
202
|
+
export async function leaseIsLive(lock, { staleMs = LEASE_STALE_MS, now = () => Date.now() } = {}) {
|
|
203
|
+
const heartbeatAt = lock?.heartbeatAt ? Date.parse(lock.heartbeatAt) : NaN
|
|
204
|
+
if (Number.isFinite(heartbeatAt) && now() - heartbeatAt < staleMs) return true
|
|
205
|
+
return pidIsAlive(lock?.pid)
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
|
|
209
|
+
export function isTerminalNoFollowup(decision) {
|
|
210
|
+
const frontier = decision?.goal_frontier_projection
|
|
211
|
+
const terminal = frontier?.terminal_state
|
|
212
|
+
const completeness = frontier?.source_completeness
|
|
213
|
+
return Boolean(
|
|
214
|
+
decision?.should_run === false &&
|
|
215
|
+
decision?.effective_action === "terminal_no_followup" &&
|
|
216
|
+
terminal?.schema_version === TERMINAL_STATE_SCHEMA_VERSION &&
|
|
217
|
+
terminal?.kind === "no_followup" &&
|
|
218
|
+
terminal?.derived === true &&
|
|
219
|
+
terminal?.source === "validated_goal_closure" &&
|
|
220
|
+
completeness?.schema_version === SOURCE_COMPLETENESS_SCHEMA_VERSION &&
|
|
221
|
+
completeness?.user_todos === "valid" &&
|
|
222
|
+
completeness?.agent_todos === "valid",
|
|
223
|
+
)
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
|
|
227
|
+
export function shouldRunNow(decision) {
|
|
228
|
+
// The scheduler hint is authoritative: a should_run=true decision that the
|
|
229
|
+
// scheduler classifies as a backoff (for example waiting for the user)
|
|
230
|
+
// must not prompt the model.
|
|
231
|
+
return decision?.scheduler_hint?.action === "run_now"
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
|
|
235
|
+
export function waitPlan(decision, state) {
|
|
236
|
+
const local = decision?.scheduler_hint?.unchanged_poll?.local_scheduler
|
|
237
|
+
if (!local || local === "stop") return { stop: true }
|
|
238
|
+
const token = decision?.scheduler_hint?.reset_policy?.reset_token || ""
|
|
239
|
+
const sameIdentity = token && token === state.schedulerToken
|
|
240
|
+
const unchangedPolls = sameIdentity ? Number(state.unchangedPolls || 0) : 0
|
|
241
|
+
const limit = Number.isInteger(local.unchanged_poll_limit) ? local.unchanged_poll_limit : null
|
|
242
|
+
if (limit !== null && unchangedPolls >= limit) return { stop: true }
|
|
243
|
+
const progression = Array.isArray(local.example_progression_minutes)
|
|
244
|
+
? local.example_progression_minutes.filter((value) => Number(value) > 0)
|
|
245
|
+
: []
|
|
246
|
+
const fallback = Number(local.recommended_interval_minutes || DEFAULT_RETRY_MINUTES)
|
|
247
|
+
const minutes = progression.length
|
|
248
|
+
? Number(progression[Math.min(unchangedPolls, progression.length - 1)])
|
|
249
|
+
: fallback
|
|
250
|
+
return {
|
|
251
|
+
stop: false,
|
|
252
|
+
minutes: Number.isFinite(minutes) && minutes > 0 ? minutes : DEFAULT_RETRY_MINUTES,
|
|
253
|
+
schedulerToken: token,
|
|
254
|
+
unchangedPolls: unchangedPolls + 1,
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
|
|
259
|
+
export function retryPlan(decision, state) {
|
|
260
|
+
// Fail-closed quota probe retry with a bounded backoff. Resets on success.
|
|
261
|
+
const count = Number(state.probeRetryCount || 0)
|
|
262
|
+
const minutes = Math.min(DEFAULT_RETRY_MINUTES * 2 ** Math.min(count, 4), MAX_RETRY_MINUTES)
|
|
263
|
+
return {
|
|
264
|
+
minutes,
|
|
265
|
+
probeRetryCount: count + 1,
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
|
|
270
|
+
export function turnVerdict(messages, sentPromptIds, { fallbackPromptCreated } = {}) {
|
|
271
|
+
// A turn completes only when an assistant message finished at or after the
|
|
272
|
+
// newest user message (regardless of who sent it) and that message is not
|
|
273
|
+
// an intermediate tool-call step. User input never pauses the loop: the
|
|
274
|
+
// model answers it, then quota gating continues as usual.
|
|
275
|
+
//
|
|
276
|
+
// Long tool-heavy turns can fill the newest message page entirely with
|
|
277
|
+
// assistant messages, so the page may contain no user message at all. The
|
|
278
|
+
// caller supplies fallbackPromptCreated (the worker's last prompt time) to
|
|
279
|
+
// bound the verdict in that case; a user message on the page always wins.
|
|
280
|
+
let newestUser = null
|
|
281
|
+
let newestAssistant = null
|
|
282
|
+
for (const message of messages || []) {
|
|
283
|
+
const type = message?.type
|
|
284
|
+
const created = Number(message?.time?.created)
|
|
285
|
+
if (type === "user") {
|
|
286
|
+
if (!newestUser || created > newestUser.time.created) {
|
|
287
|
+
newestUser = { id: message?.id, time: { created }, text: String(message?.text || "").slice(0, 120) }
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
if (type === "assistant") {
|
|
291
|
+
const completed = Number(message?.time?.completed)
|
|
292
|
+
if (!newestAssistant || created > newestAssistant.time.created) {
|
|
293
|
+
newestAssistant = { time: { created, completed }, finish: message?.finish }
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
const boundaryCreated = newestUser
|
|
298
|
+
? newestUser.time.created
|
|
299
|
+
: Number(fallbackPromptCreated)
|
|
300
|
+
if (!Number.isFinite(boundaryCreated)) return { completed: false, waiting: true }
|
|
301
|
+
const finished = Boolean(newestAssistant) && Number.isFinite(newestAssistant.time.completed)
|
|
302
|
+
const terminalStep = finished && newestAssistant.finish !== "tool-calls"
|
|
303
|
+
const afterPrompt = finished && newestAssistant.time.completed >= boundaryCreated
|
|
304
|
+
const completed = terminalStep && afterPrompt
|
|
305
|
+
return {
|
|
306
|
+
completed,
|
|
307
|
+
waiting: !completed,
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
|
|
312
|
+
export function buildQuotaArgs(state) {
|
|
313
|
+
const args = []
|
|
314
|
+
if (state.registryPath) args.push("--registry", state.registryPath)
|
|
315
|
+
args.push(
|
|
316
|
+
"--format",
|
|
317
|
+
"json",
|
|
318
|
+
"quota",
|
|
319
|
+
"should-run",
|
|
320
|
+
"--goal-id",
|
|
321
|
+
state.goalId,
|
|
322
|
+
"--runtime-profile",
|
|
323
|
+
"generic_cli",
|
|
324
|
+
"--include-detail",
|
|
325
|
+
"scheduler",
|
|
326
|
+
"--record-host-poll",
|
|
327
|
+
)
|
|
328
|
+
if (state.agentId) args.push("--agent-id", state.agentId)
|
|
329
|
+
for (const capability of state.availableCapabilities || []) {
|
|
330
|
+
args.push("--available-capability", capability)
|
|
331
|
+
}
|
|
332
|
+
return args
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
|
|
336
|
+
export function parseExecFailure(error) {
|
|
337
|
+
// opencode2 api reports HTTP failures through a non-zero exit code and a
|
|
338
|
+
// stderr payload. Normalize the most useful part.
|
|
339
|
+
const stderr = String(error?.stderr || "")
|
|
340
|
+
let status = null
|
|
341
|
+
const statusMatch = /status[ =:]+(\d{3})/i.exec(stderr)
|
|
342
|
+
if (statusMatch) status = Number(statusMatch[1])
|
|
343
|
+
const messageMatch = /(?:error|message)[ =:]+"?([^"\n]{0,180})/i.exec(stderr)
|
|
344
|
+
return {
|
|
345
|
+
code: error?.code || null,
|
|
346
|
+
status,
|
|
347
|
+
stderr: stderr.slice(0, 400),
|
|
348
|
+
message: messageMatch ? messageMatch[1] : stderr.split("\n")[0]?.slice(0, 180) || String(error?.message || error),
|
|
349
|
+
}
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
|
|
353
|
+
export function createApiTransport({ bin = process.env.OPENCODE2_BIN || "opencode2", execFileImpl = execFile } = {}) {
|
|
354
|
+
const call = async (method, apiPath, body = null, timeoutMs = 60_000) => {
|
|
355
|
+
const args = ["api", method, apiPath]
|
|
356
|
+
if (body !== null) args.push("-d", JSON.stringify(body))
|
|
357
|
+
const { stdout } = await execFileImpl(bin, args, {
|
|
358
|
+
timeout: timeoutMs,
|
|
359
|
+
maxBuffer: 32 * 1024 * 1024,
|
|
360
|
+
})
|
|
361
|
+
return JSON.parse(stdout || "{}")
|
|
362
|
+
}
|
|
363
|
+
return {
|
|
364
|
+
async createSession(directory) {
|
|
365
|
+
const response = await call("post", "/api/session", { directory })
|
|
366
|
+
return response?.data?.id || null
|
|
367
|
+
},
|
|
368
|
+
async sessionExists(sessionID) {
|
|
369
|
+
try {
|
|
370
|
+
await call("get", `/api/session/${sessionID}`)
|
|
371
|
+
return true
|
|
372
|
+
} catch (error) {
|
|
373
|
+
const failure = parseExecFailure(error)
|
|
374
|
+
if (failure.status === 404) return false
|
|
375
|
+
throw error
|
|
376
|
+
}
|
|
377
|
+
},
|
|
378
|
+
async listMessages(sessionID) {
|
|
379
|
+
// Bounded newest-first page: long sessions grow unbounded message
|
|
380
|
+
// lists, and the turn verdict only needs the recent tail.
|
|
381
|
+
try {
|
|
382
|
+
const response = await call("get", `/api/session/${sessionID}/message?limit=30&order=desc`)
|
|
383
|
+
return response?.data || []
|
|
384
|
+
} catch (error) {
|
|
385
|
+
if (!String(error?.message || "").includes("JSON")) throw error
|
|
386
|
+
// Some hosts truncate oversized payloads; a smaller tail is enough
|
|
387
|
+
// for the completion verdict.
|
|
388
|
+
const response = await call("get", `/api/session/${sessionID}/message?limit=5&order=desc`)
|
|
389
|
+
return response?.data || []
|
|
390
|
+
}
|
|
391
|
+
},
|
|
392
|
+
async sessionInfo(sessionID) {
|
|
393
|
+
const response = await call("get", `/api/session/${sessionID}`)
|
|
394
|
+
return response?.data || null
|
|
395
|
+
},
|
|
396
|
+
async waitForSession(sessionID, { timeoutMs = 60_000 } = {}) {
|
|
397
|
+
try {
|
|
398
|
+
await call("post", `/api/session/${sessionID}/wait`, null, timeoutMs)
|
|
399
|
+
return true
|
|
400
|
+
} catch (error) {
|
|
401
|
+
// A chunk timeout means the turn is still running; the caller
|
|
402
|
+
// verifies the message tail and re-issues the wait.
|
|
403
|
+
if (error?.killed === true || error?.code === "ETIMEDOUT") return false
|
|
404
|
+
throw error
|
|
405
|
+
}
|
|
406
|
+
},
|
|
407
|
+
async sendPrompt(sessionID, text) {
|
|
408
|
+
const response = await call("post", `/api/session/${sessionID}/prompt`, {
|
|
409
|
+
text,
|
|
410
|
+
delivery: "queue",
|
|
411
|
+
})
|
|
412
|
+
return response?.data?.id || null
|
|
413
|
+
},
|
|
414
|
+
async sendSynthetic(sessionID, text) {
|
|
415
|
+
const response = await call("post", `/api/session/${sessionID}/synthetic`, { text })
|
|
416
|
+
return response?.data?.id || null
|
|
417
|
+
},
|
|
418
|
+
}
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
|
|
422
|
+
export function createQuotaProbe({ bin = process.env.LOOPX_BIN || "loopx", execFileImpl = execFile } = {}) {
|
|
423
|
+
return async function probe(state, { directory }) {
|
|
424
|
+
const { stdout } = await runQuotaProbe(state, directory, bin, execFileImpl)
|
|
425
|
+
const decision = JSON.parse(stdout || "{}")
|
|
426
|
+
if (!decision || typeof decision !== "object" || Array.isArray(decision)) {
|
|
427
|
+
throw new Error("loopx quota should-run returned a non-object payload")
|
|
428
|
+
}
|
|
429
|
+
return decision
|
|
430
|
+
}
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
|
|
434
|
+
async function runQuotaProbe(state, directory, bin, execFileImpl) {
|
|
435
|
+
try {
|
|
436
|
+
return await execFileImpl(bin, buildQuotaArgs(state), {
|
|
437
|
+
cwd: state.directory || directory,
|
|
438
|
+
timeout: 30_000,
|
|
439
|
+
maxBuffer: 4 * 1024 * 1024,
|
|
440
|
+
})
|
|
441
|
+
} catch (error) {
|
|
442
|
+
// A denied or gated decision exits non-zero but still carries the
|
|
443
|
+
// authoritative JSON payload on stdout. Only fail closed when the
|
|
444
|
+
// payload itself is unavailable.
|
|
445
|
+
if (typeof error?.stdout === "string" && error.stdout.trim()) {
|
|
446
|
+
return { stdout: error.stdout }
|
|
447
|
+
}
|
|
448
|
+
throw error
|
|
449
|
+
}
|
|
450
|
+
}
|
|
451
|
+
|
|
452
|
+
|
|
453
|
+
export function sleep(ms) {
|
|
454
|
+
return new Promise((resolve) => setTimeout(resolve, ms))
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
|
|
458
|
+
export async function waitForTurn({
|
|
459
|
+
transport,
|
|
460
|
+
sessionID,
|
|
461
|
+
state,
|
|
462
|
+
sentPromptIds,
|
|
463
|
+
pollMs = MESSAGE_POLL_MS,
|
|
464
|
+
timeoutMs = DEFAULT_TURN_TIMEOUT_MS,
|
|
465
|
+
stallMs = 30 * 60 * 1000,
|
|
466
|
+
holdMs = 15 * 60 * 1000,
|
|
467
|
+
waitChunkMs = 60_000,
|
|
468
|
+
shouldPause = async () => false,
|
|
469
|
+
}) {
|
|
470
|
+
// Block on the session wait endpoint (resolves at idle) in bounded chunks,
|
|
471
|
+
// then verify the message tail. Intermediate tool-call steps complete their
|
|
472
|
+
// messages without ending the turn, so the verdict requires a non-tool-call
|
|
473
|
+
// final message after the newest own prompt. A session whose message
|
|
474
|
+
// activity stops moving is reported as stalled instead of blocking until
|
|
475
|
+
// the turn budget. Session updated timestamps are not a reliable liveness
|
|
476
|
+
// signal in every build, so stall detection keys on message activity.
|
|
477
|
+
//
|
|
478
|
+
// A user pause (for example ESC in the host TUI) also stops message
|
|
479
|
+
// activity mid-turn. Before escalating to a permanent stall, the worker
|
|
480
|
+
// returns a quiet hold once the hold window passes; the main loop then
|
|
481
|
+
// waits without prompting and re-enters with the persisted activity time,
|
|
482
|
+
// so a paused session is never prompted and the stall window still
|
|
483
|
+
// escalates from the true last activity.
|
|
484
|
+
const startedAt = Date.now()
|
|
485
|
+
let lastActivityAt = Number(state?.pendingTurnActivityAt) || Date.now()
|
|
486
|
+
const fallbackPromptCreated = Number(
|
|
487
|
+
state?.lastPromptAt ?? state?.startedAtMs ?? Date.now()
|
|
488
|
+
)
|
|
489
|
+
while (true) {
|
|
490
|
+
if (await shouldPause(state)) return { kind: "paused_state" }
|
|
491
|
+
await transport.waitForSession(sessionID, { timeoutMs: waitChunkMs })
|
|
492
|
+
const messages = await transport.listMessages(sessionID)
|
|
493
|
+
const activity = latestMessageActivity(messages)
|
|
494
|
+
if (activity !== null && activity > lastActivityAt) lastActivityAt = activity
|
|
495
|
+
const quietForMs = Date.now() - lastActivityAt
|
|
496
|
+
if (quietForMs > stallMs) {
|
|
497
|
+
return { kind: "session_stalled", elapsedMs: Date.now() - startedAt }
|
|
498
|
+
}
|
|
499
|
+
if (quietForMs > holdMs) {
|
|
500
|
+
return { kind: "turn_hold", activityAt: lastActivityAt, quietForMs }
|
|
501
|
+
}
|
|
502
|
+
const verdict = turnVerdict(messages, sentPromptIds, { fallbackPromptCreated })
|
|
503
|
+
if (verdict.completed) return { kind: "completed", ...verdict }
|
|
504
|
+
if (Date.now() - startedAt > timeoutMs) {
|
|
505
|
+
return {
|
|
506
|
+
kind: "turn_timeout",
|
|
507
|
+
elapsedMs: Date.now() - startedAt,
|
|
508
|
+
lastMessageCount: messages.length,
|
|
509
|
+
}
|
|
510
|
+
}
|
|
511
|
+
await sleep(pollMs)
|
|
512
|
+
}
|
|
513
|
+
}
|
|
514
|
+
|
|
515
|
+
|
|
516
|
+
export function latestMessageActivity(messages) {
|
|
517
|
+
let latest = null
|
|
518
|
+
for (const message of messages || []) {
|
|
519
|
+
const created = Number(message?.time?.created)
|
|
520
|
+
const completed = Number(message?.time?.completed)
|
|
521
|
+
for (const candidate of [created, completed]) {
|
|
522
|
+
if (Number.isFinite(candidate) && (latest === null || candidate > latest)) {
|
|
523
|
+
latest = candidate
|
|
524
|
+
}
|
|
525
|
+
}
|
|
526
|
+
}
|
|
527
|
+
return latest
|
|
528
|
+
}
|
|
529
|
+
|
|
530
|
+
|
|
531
|
+
export async function runWorker({
|
|
532
|
+
goalId,
|
|
533
|
+
directory,
|
|
534
|
+
agentId = "",
|
|
535
|
+
registryPath = "",
|
|
536
|
+
availableCapabilities = [],
|
|
537
|
+
sessionId = null,
|
|
538
|
+
taskBody = null,
|
|
539
|
+
maxTurns = DEFAULT_MAX_TURNS,
|
|
540
|
+
maxDurationMs = DEFAULT_MAX_DURATION_MS,
|
|
541
|
+
forceResume = false,
|
|
542
|
+
waitTurnOptions = {},
|
|
543
|
+
log = async () => {},
|
|
544
|
+
stateStore = createStateStore(),
|
|
545
|
+
transport = null,
|
|
546
|
+
quotaProbe = null,
|
|
547
|
+
sleepImpl = sleep,
|
|
548
|
+
} = {}) {
|
|
549
|
+
if (!goalId) throw new Error("--goal-id is required")
|
|
550
|
+
if (!directory) throw new Error("--directory is required")
|
|
551
|
+
const effectiveTransport = transport || createApiTransport()
|
|
552
|
+
const effectiveProbe = quotaProbe || createQuotaProbe()
|
|
553
|
+
|
|
554
|
+
const lease = await stateStore.acquireLock(goalId)
|
|
555
|
+
if (!lease.acquired) {
|
|
556
|
+
await log("warn", "worker lease is held; refusing to start a second driver", {
|
|
557
|
+
goalId,
|
|
558
|
+
...lease.holder,
|
|
559
|
+
})
|
|
560
|
+
return { kind: "lease_held", holder: lease.holder }
|
|
561
|
+
}
|
|
562
|
+
try {
|
|
563
|
+
return await runWithLease({
|
|
564
|
+
goalId,
|
|
565
|
+
directory,
|
|
566
|
+
agentId,
|
|
567
|
+
registryPath,
|
|
568
|
+
availableCapabilities,
|
|
569
|
+
sessionId,
|
|
570
|
+
taskBody,
|
|
571
|
+
maxTurns,
|
|
572
|
+
maxDurationMs,
|
|
573
|
+
forceResume,
|
|
574
|
+
waitTurnOptions,
|
|
575
|
+
log,
|
|
576
|
+
stateStore,
|
|
577
|
+
transport: effectiveTransport,
|
|
578
|
+
quotaProbe: effectiveProbe,
|
|
579
|
+
sleepImpl,
|
|
580
|
+
})
|
|
581
|
+
} finally {
|
|
582
|
+
await lease.release()
|
|
583
|
+
}
|
|
584
|
+
}
|
|
585
|
+
|
|
586
|
+
|
|
587
|
+
async function runWithLease({
|
|
588
|
+
goalId,
|
|
589
|
+
directory,
|
|
590
|
+
agentId,
|
|
591
|
+
registryPath,
|
|
592
|
+
availableCapabilities,
|
|
593
|
+
sessionId,
|
|
594
|
+
taskBody,
|
|
595
|
+
maxTurns,
|
|
596
|
+
maxDurationMs,
|
|
597
|
+
forceResume,
|
|
598
|
+
waitTurnOptions,
|
|
599
|
+
log,
|
|
600
|
+
stateStore,
|
|
601
|
+
transport,
|
|
602
|
+
quotaProbe,
|
|
603
|
+
sleepImpl,
|
|
604
|
+
}) {
|
|
605
|
+
let state = await stateStore.read(goalId)
|
|
606
|
+
const startedAt = Date.now()
|
|
607
|
+
|
|
608
|
+
if (!state) {
|
|
609
|
+
if (!sessionId && !taskBody) {
|
|
610
|
+
throw new Error(
|
|
611
|
+
"first run requires --task-body (or an existing --session-id); no worker state exists for this goal",
|
|
612
|
+
)
|
|
613
|
+
}
|
|
614
|
+
state = await stateStore.write(goalId, {
|
|
615
|
+
directory,
|
|
616
|
+
agentId: String(agentId || ""),
|
|
617
|
+
registryPath: String(registryPath || ""),
|
|
618
|
+
availableCapabilities: Array.isArray(availableCapabilities)
|
|
619
|
+
? availableCapabilities.map(String)
|
|
620
|
+
: [],
|
|
621
|
+
sessionID: sessionId ? String(sessionId) : "",
|
|
622
|
+
autoResume: true,
|
|
623
|
+
phase: "starting",
|
|
624
|
+
schedulerToken: "",
|
|
625
|
+
unchangedPolls: 0,
|
|
626
|
+
probeRetryCount: 0,
|
|
627
|
+
turnCount: 0,
|
|
628
|
+
pendingTurn: false,
|
|
629
|
+
startedAt: new Date().toISOString(),
|
|
630
|
+
})
|
|
631
|
+
}
|
|
632
|
+
|
|
633
|
+
if (state.autoResume === false && !forceResume) {
|
|
634
|
+
await log("info", "worker state is paused; pass --force-resume to restart", { goalId })
|
|
635
|
+
return { kind: "paused_state" }
|
|
636
|
+
}
|
|
637
|
+
if (state.autoResume === false && forceResume) {
|
|
638
|
+
state = await stateStore.write(goalId, {
|
|
639
|
+
...state,
|
|
640
|
+
autoResume: true,
|
|
641
|
+
lastPausedReason: "",
|
|
642
|
+
pausedReason: "",
|
|
643
|
+
})
|
|
644
|
+
await log("info", "worker state resumed with --force-resume", { goalId })
|
|
645
|
+
}
|
|
646
|
+
if (!Number.isFinite(Number(state.lastPromptAt))) {
|
|
647
|
+
const migratedAt = Date.parse(state.startedAt) || Date.now()
|
|
648
|
+
state = await stateStore.write(goalId, { ...state, lastPromptAt: migratedAt })
|
|
649
|
+
await log("info", "migrated worker state with a lastPromptAt boundary", {
|
|
650
|
+
goalId,
|
|
651
|
+
lastPromptAt: migratedAt,
|
|
652
|
+
})
|
|
653
|
+
}
|
|
654
|
+
|
|
655
|
+
const directoryNow = directory || state.directory
|
|
656
|
+
|
|
657
|
+
let sessionID = state.sessionID || sessionId
|
|
658
|
+
if (!sessionID) {
|
|
659
|
+
sessionID = await transport.createSession(directoryNow)
|
|
660
|
+
if (!sessionID) throw new Error("opencode2 session creation returned no id")
|
|
661
|
+
state = await stateStore.write(goalId, { ...state, sessionID })
|
|
662
|
+
await log("info", "created OpenCode 2 session", { goalId, sessionID })
|
|
663
|
+
}
|
|
664
|
+
if (!(await transport.sessionExists(sessionID))) {
|
|
665
|
+
state = await stateStore.write(goalId, {
|
|
666
|
+
...state,
|
|
667
|
+
phase: "session_deleted",
|
|
668
|
+
pausedReason: "opencode2_session_deleted",
|
|
669
|
+
})
|
|
670
|
+
await log("error", "bound OpenCode 2 session no longer exists", { goalId, sessionID })
|
|
671
|
+
return { kind: "session_deleted", sessionID }
|
|
672
|
+
}
|
|
673
|
+
|
|
674
|
+
if (state.turnCount === 0 && taskBody) {
|
|
675
|
+
await transport.sendPrompt(sessionID, taskBody)
|
|
676
|
+
state = await stateStore.write(goalId, {
|
|
677
|
+
...state,
|
|
678
|
+
turnCount: 1,
|
|
679
|
+
pendingTurn: true,
|
|
680
|
+
lastPromptAt: Date.now(),
|
|
681
|
+
pendingTurnActivityAt: Date.now(),
|
|
682
|
+
phase: "working",
|
|
683
|
+
})
|
|
684
|
+
await log("info", "prompted the session with the task body", { goalId, sessionID })
|
|
685
|
+
}
|
|
686
|
+
|
|
687
|
+
while (true) {
|
|
688
|
+
if (Date.now() - startedAt > maxDurationMs) {
|
|
689
|
+
state = await stateStore.write(goalId, { ...state, phase: "duration_limit" })
|
|
690
|
+
await transport.sendSynthetic(sessionID, DURATION_LIMIT_NOTICE)
|
|
691
|
+
await log("info", "stopped after the duration budget", { goalId })
|
|
692
|
+
return { kind: "duration_limit" }
|
|
693
|
+
}
|
|
694
|
+
if (!state.pendingTurn && state.turnCount >= maxTurns) {
|
|
695
|
+
state = await stateStore.write(goalId, { ...state, phase: "turn_limit" })
|
|
696
|
+
await transport.sendSynthetic(sessionID, TURN_LIMIT_NOTICE)
|
|
697
|
+
await log("info", "stopped after the turn budget", { goalId })
|
|
698
|
+
return { kind: "turn_limit" }
|
|
699
|
+
}
|
|
700
|
+
|
|
701
|
+
if (state.pendingTurn) {
|
|
702
|
+
const waitOutcome = await waitForTurn({
|
|
703
|
+
transport,
|
|
704
|
+
sessionID,
|
|
705
|
+
state,
|
|
706
|
+
sentPromptIds: state.sentPromptIds || [],
|
|
707
|
+
...waitTurnOptions,
|
|
708
|
+
shouldPause: async () => {
|
|
709
|
+
const stored = await stateStore.read(goalId)
|
|
710
|
+
return Boolean(stored && stored.autoResume === false)
|
|
711
|
+
},
|
|
712
|
+
})
|
|
713
|
+
if (waitOutcome.kind === "turn_hold") {
|
|
714
|
+
const wasHolding = state.phase === "session_paused_hold"
|
|
715
|
+
state = await stateStore.write(goalId, {
|
|
716
|
+
...state,
|
|
717
|
+
phase: "session_paused_hold",
|
|
718
|
+
pendingTurnActivityAt: Number(waitOutcome.activityAt) || state.pendingTurnActivityAt,
|
|
719
|
+
pausedReason: "session_paused_hold",
|
|
720
|
+
})
|
|
721
|
+
if (!wasHolding) {
|
|
722
|
+
await transport.sendSynthetic(
|
|
723
|
+
sessionID,
|
|
724
|
+
"The session appears paused. The LoopX worker holds quietly and resumes automatically when the session continues.",
|
|
725
|
+
)
|
|
726
|
+
await log("info", "pending turn went quiet; holding without prompting", {
|
|
727
|
+
goalId,
|
|
728
|
+
sessionID,
|
|
729
|
+
quietForMs: waitOutcome.quietForMs,
|
|
730
|
+
})
|
|
731
|
+
}
|
|
732
|
+
await sleepImpl(5 * 60_000)
|
|
733
|
+
continue
|
|
734
|
+
}
|
|
735
|
+
if (waitOutcome.kind === "paused_state") {
|
|
736
|
+
await log("info", "worker state was paused while waiting for the turn", { goalId, sessionID })
|
|
737
|
+
return { kind: "paused_state" }
|
|
738
|
+
}
|
|
739
|
+
if (waitOutcome.kind === "session_stalled") {
|
|
740
|
+
state = await stateStore.write(goalId, {
|
|
741
|
+
...state,
|
|
742
|
+
phase: "session_stalled",
|
|
743
|
+
autoResume: false,
|
|
744
|
+
pausedReason: "session_stalled",
|
|
745
|
+
})
|
|
746
|
+
await transport.sendSynthetic(
|
|
747
|
+
sessionID,
|
|
748
|
+
"LoopX paused this goal because the OpenCode session stopped making progress.",
|
|
749
|
+
)
|
|
750
|
+
await log("warn", "session stalled; worker paused", { goalId, sessionID })
|
|
751
|
+
return { kind: "session_stalled" }
|
|
752
|
+
}
|
|
753
|
+
if (waitOutcome.kind === "turn_timeout") {
|
|
754
|
+
state = await stateStore.write(goalId, {
|
|
755
|
+
...state,
|
|
756
|
+
phase: "turn_timeout",
|
|
757
|
+
autoResume: false,
|
|
758
|
+
pausedReason: "turn_timeout",
|
|
759
|
+
})
|
|
760
|
+
await transport.sendSynthetic(
|
|
761
|
+
sessionID,
|
|
762
|
+
"LoopX paused this goal because the last OpenCode turn exceeded the wait budget.",
|
|
763
|
+
)
|
|
764
|
+
await log("warn", "turn wait budget exhausted", { goalId, sessionID })
|
|
765
|
+
return { kind: "turn_timeout" }
|
|
766
|
+
}
|
|
767
|
+
|
|
768
|
+
state = await stateStore.write(goalId, { ...state, pendingTurn: false })
|
|
769
|
+
if (state.turnCount >= maxTurns) {
|
|
770
|
+
state = await stateStore.write(goalId, { ...state, phase: "turn_limit" })
|
|
771
|
+
await transport.sendSynthetic(sessionID, TURN_LIMIT_NOTICE)
|
|
772
|
+
await log("info", "stopped after the turn budget", { goalId, sessionID })
|
|
773
|
+
return { kind: "turn_limit" }
|
|
774
|
+
}
|
|
775
|
+
}
|
|
776
|
+
|
|
777
|
+
state = await stateStore.read(goalId)
|
|
778
|
+
if (!state || state.autoResume === false) {
|
|
779
|
+
return { kind: "paused_state" }
|
|
780
|
+
}
|
|
781
|
+
|
|
782
|
+
let decision
|
|
783
|
+
try {
|
|
784
|
+
decision = await quotaProbe(state, { directory: directoryNow })
|
|
785
|
+
state = await stateStore.write(goalId, {
|
|
786
|
+
...state,
|
|
787
|
+
probeRetryCount: 0,
|
|
788
|
+
lastPollAt: new Date().toISOString(),
|
|
789
|
+
})
|
|
790
|
+
} catch (error) {
|
|
791
|
+
const plan = retryPlan(decision, state)
|
|
792
|
+
state = await stateStore.write(goalId, {
|
|
793
|
+
...state,
|
|
794
|
+
phase: "quota_probe_retry",
|
|
795
|
+
probeRetryCount: plan.probeRetryCount,
|
|
796
|
+
lastPollAt: new Date().toISOString(),
|
|
797
|
+
})
|
|
798
|
+
await log("warn", "LoopX quota probe failed closed; scheduling a bounded retry", {
|
|
799
|
+
goalId,
|
|
800
|
+
error: String(error?.message || error).slice(0, 200),
|
|
801
|
+
retryMinutes: plan.minutes,
|
|
802
|
+
})
|
|
803
|
+
await sleepImpl(plan.minutes * 60_000)
|
|
804
|
+
continue
|
|
805
|
+
}
|
|
806
|
+
|
|
807
|
+
if (decision?.status === "goal_not_found" || decision?.state === "unknown") {
|
|
808
|
+
state = await stateStore.write(goalId, {
|
|
809
|
+
...state,
|
|
810
|
+
phase: "goal_gone",
|
|
811
|
+
autoResume: false,
|
|
812
|
+
pausedReason: "goal_not_found",
|
|
813
|
+
})
|
|
814
|
+
await log("info", "goal is no longer registered; worker finished", { goalId, sessionID })
|
|
815
|
+
return { kind: "goal_gone" }
|
|
816
|
+
}
|
|
817
|
+
|
|
818
|
+
if (isTerminalNoFollowup(decision)) {
|
|
819
|
+
// Validated closure means the CURRENT work is done, not that the loop
|
|
820
|
+
// should end. Stand by and keep polling so new work resumes the
|
|
821
|
+
// session automatically instead of pausing after one task.
|
|
822
|
+
const standbyPolls = Number(state.standbyPolls || 0) + 1
|
|
823
|
+
state = await stateStore.write(goalId, {
|
|
824
|
+
...state,
|
|
825
|
+
phase: "standby",
|
|
826
|
+
schedulerToken: "",
|
|
827
|
+
unchangedPolls: 0,
|
|
828
|
+
standbyPolls,
|
|
829
|
+
})
|
|
830
|
+
if (standbyPolls === 1) {
|
|
831
|
+
await transport.sendSynthetic(sessionID, TERMINAL_NOTICE)
|
|
832
|
+
}
|
|
833
|
+
await log("info", "LoopX validated terminal closure; standing by for new work", {
|
|
834
|
+
goalId,
|
|
835
|
+
sessionID,
|
|
836
|
+
standbyPolls,
|
|
837
|
+
})
|
|
838
|
+
await sleepImpl(STANDBY_MINUTES * 60_000)
|
|
839
|
+
continue
|
|
840
|
+
}
|
|
841
|
+
|
|
842
|
+
if (shouldRunNow(decision)) {
|
|
843
|
+
const wasStandby = state.phase === "standby"
|
|
844
|
+
state = await stateStore.write(goalId, {
|
|
845
|
+
...state,
|
|
846
|
+
phase: "working",
|
|
847
|
+
schedulerToken: "",
|
|
848
|
+
unchangedPolls: 0,
|
|
849
|
+
standbyPolls: 0,
|
|
850
|
+
})
|
|
851
|
+
if (wasStandby) {
|
|
852
|
+
await log("info", "new work appeared; resuming from standby", { goalId, sessionID })
|
|
853
|
+
}
|
|
854
|
+
await transport.sendPrompt(sessionID, continuationPrompt(directoryNow))
|
|
855
|
+
state = await stateStore.write(goalId, {
|
|
856
|
+
...state,
|
|
857
|
+
pendingTurn: true,
|
|
858
|
+
lastPromptAt: Date.now(),
|
|
859
|
+
pendingTurnActivityAt: Date.now(),
|
|
860
|
+
turnCount: Number(state.turnCount || 0) + 1,
|
|
861
|
+
})
|
|
862
|
+
await log("info", "quota granted another turn", { goalId, sessionID, turnCount: state.turnCount })
|
|
863
|
+
continue
|
|
864
|
+
}
|
|
865
|
+
|
|
866
|
+
const wait = waitPlan(decision, state)
|
|
867
|
+
if (wait.stop) {
|
|
868
|
+
state = await stateStore.write(goalId, {
|
|
869
|
+
...state,
|
|
870
|
+
phase: "limit_stopped",
|
|
871
|
+
autoResume: false,
|
|
872
|
+
pausedReason: "unchanged_poll_limit",
|
|
873
|
+
})
|
|
874
|
+
await transport.sendSynthetic(sessionID, LIMIT_NOTICE)
|
|
875
|
+
await log("info", "unchanged-poll limit stopped the worker", { goalId, sessionID })
|
|
876
|
+
return { kind: "limit_stopped" }
|
|
877
|
+
}
|
|
878
|
+
state = await stateStore.write(goalId, {
|
|
879
|
+
...state,
|
|
880
|
+
phase: "waiting",
|
|
881
|
+
schedulerToken: wait.schedulerToken,
|
|
882
|
+
unchangedPolls: wait.unchangedPolls,
|
|
883
|
+
})
|
|
884
|
+
await log("info", "waiting before the next quota poll", {
|
|
885
|
+
goalId,
|
|
886
|
+
sessionID,
|
|
887
|
+
minutes: wait.minutes,
|
|
888
|
+
})
|
|
889
|
+
await sleepImpl(wait.minutes * 60_000)
|
|
890
|
+
}
|
|
891
|
+
}
|
|
892
|
+
|
|
893
|
+
|
|
894
|
+
export function parseWorkerArgs(argv) {
|
|
895
|
+
const args = { availableCapabilities: [] }
|
|
896
|
+
for (let index = 0; index < argv.length; index += 1) {
|
|
897
|
+
const token = argv[index]
|
|
898
|
+
const value = argv[index + 1]
|
|
899
|
+
const readValue = (name) => {
|
|
900
|
+
if (value === undefined || value === "") throw new Error(`${name} requires a value`)
|
|
901
|
+
index += 1
|
|
902
|
+
return value
|
|
903
|
+
}
|
|
904
|
+
switch (token) {
|
|
905
|
+
case "--goal-id":
|
|
906
|
+
args.goalId = readValue(token)
|
|
907
|
+
break
|
|
908
|
+
case "--directory":
|
|
909
|
+
args.directory = readValue(token)
|
|
910
|
+
break
|
|
911
|
+
case "--agent-id":
|
|
912
|
+
args.agentId = readValue(token)
|
|
913
|
+
break
|
|
914
|
+
case "--registry":
|
|
915
|
+
args.registryPath = readValue(token)
|
|
916
|
+
break
|
|
917
|
+
case "--session-id":
|
|
918
|
+
args.sessionId = readValue(token)
|
|
919
|
+
break
|
|
920
|
+
case "--task-body":
|
|
921
|
+
args.taskBody = readValue(token)
|
|
922
|
+
break
|
|
923
|
+
case "--capability":
|
|
924
|
+
args.availableCapabilities.push(readValue(token))
|
|
925
|
+
break
|
|
926
|
+
case "--max-turns": {
|
|
927
|
+
const parsed = Number(readValue(token))
|
|
928
|
+
if (!Number.isInteger(parsed) || parsed < 1) throw new Error("--max-turns must be a positive integer")
|
|
929
|
+
args.maxTurns = parsed
|
|
930
|
+
break
|
|
931
|
+
}
|
|
932
|
+
case "--max-duration-minutes": {
|
|
933
|
+
const parsed = Number(readValue(token))
|
|
934
|
+
if (!Number.isFinite(parsed) || parsed < 1) throw new Error("--max-duration-minutes must be a positive number")
|
|
935
|
+
args.maxDurationMs = Math.round(parsed * 60 * 1000)
|
|
936
|
+
break
|
|
937
|
+
}
|
|
938
|
+
case "--force-resume":
|
|
939
|
+
args.forceResume = true
|
|
940
|
+
break
|
|
941
|
+
case "--state-dir":
|
|
942
|
+
process.env.LOOPX_OPENCODE2_STATE_DIR = readValue(token)
|
|
943
|
+
break
|
|
944
|
+
case "--help":
|
|
945
|
+
args.help = true
|
|
946
|
+
break
|
|
947
|
+
default:
|
|
948
|
+
throw new Error(`unknown argument: ${token}`)
|
|
949
|
+
}
|
|
950
|
+
}
|
|
951
|
+
return args
|
|
952
|
+
}
|
|
953
|
+
|
|
954
|
+
|
|
955
|
+
export function renderResult(result) {
|
|
956
|
+
return JSON.stringify({ version: 1, operation: "opencode2_goal_worker", ok: true, result }, null, 2)
|
|
957
|
+
}
|
|
958
|
+
|
|
959
|
+
|
|
960
|
+
function isMainModule() {
|
|
961
|
+
// Compare realpaths: on macOS /var is a symlink to /private/var, so the
|
|
962
|
+
// argv[1] used to invoke the materialized worker can resolve differently
|
|
963
|
+
// from import.meta.url even though they name the same file. A mismatch
|
|
964
|
+
// silently skips main() and the CLI reports a fake success.
|
|
965
|
+
if (!process.argv[1]) return false
|
|
966
|
+
const argvPath = path.resolve(process.argv[1])
|
|
967
|
+
const modulePath = fileURLToPath(import.meta.url)
|
|
968
|
+
try {
|
|
969
|
+
return realpathSync(argvPath) === realpathSync(modulePath)
|
|
970
|
+
} catch {
|
|
971
|
+
return argvPath === modulePath
|
|
972
|
+
}
|
|
973
|
+
}
|
|
974
|
+
|
|
975
|
+
|
|
976
|
+
async function main() {
|
|
977
|
+
const args = parseWorkerArgs(process.argv.slice(2))
|
|
978
|
+
if (args.help || !args.goalId) {
|
|
979
|
+
process.stdout.write(
|
|
980
|
+
[
|
|
981
|
+
"LoopX OpenCode 2 goal worker",
|
|
982
|
+
"",
|
|
983
|
+
"usage: opencode2-goal-worker.mjs --goal-id <id> --directory <dir>",
|
|
984
|
+
" [--agent-id <id>] [--registry <path>] [--capability <name>]...",
|
|
985
|
+
" [--session-id <id>] [--task-body <text>] [--max-turns <n>]",
|
|
986
|
+
" [--max-duration-minutes <n>] [--force-resume] [--state-dir <dir>]",
|
|
987
|
+
"",
|
|
988
|
+
].join("\n"),
|
|
989
|
+
)
|
|
990
|
+
return 0
|
|
991
|
+
}
|
|
992
|
+
const log = async (level, message, extra = {}) => {
|
|
993
|
+
process.stderr.write(`${new Date().toISOString()} ${level} ${message} ${JSON.stringify(extra)}\n`)
|
|
994
|
+
}
|
|
995
|
+
try {
|
|
996
|
+
const result = await runWorker({ ...args, log })
|
|
997
|
+
process.stdout.write(`${renderResult(result)}\n`)
|
|
998
|
+
return 0
|
|
999
|
+
} catch (error) {
|
|
1000
|
+
await log("error", "worker failed", { error: String(error?.stack || error) })
|
|
1001
|
+
process.stdout.write(
|
|
1002
|
+
`${JSON.stringify({
|
|
1003
|
+
version: 1,
|
|
1004
|
+
operation: "opencode2_goal_worker",
|
|
1005
|
+
ok: false,
|
|
1006
|
+
error: String(error?.message || error),
|
|
1007
|
+
})}\n`,
|
|
1008
|
+
)
|
|
1009
|
+
return 1
|
|
1010
|
+
}
|
|
1011
|
+
}
|
|
1012
|
+
|
|
1013
|
+
|
|
1014
|
+
if (isMainModule()) {
|
|
1015
|
+
main().then((code) => {
|
|
1016
|
+
process.exitCode = code
|
|
1017
|
+
})
|
|
1018
|
+
}
|