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,1057 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import fnmatch
|
|
4
|
+
import hashlib
|
|
5
|
+
import ipaddress
|
|
6
|
+
import re
|
|
7
|
+
import subprocess
|
|
8
|
+
from collections.abc import Mapping, Sequence
|
|
9
|
+
from datetime import datetime
|
|
10
|
+
from pathlib import Path, PurePosixPath
|
|
11
|
+
from typing import Any
|
|
12
|
+
from urllib.parse import urlsplit
|
|
13
|
+
|
|
14
|
+
from ...control_plane.runtime.public_safety import public_safe_compact_text
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
ISSUE_FIX_REVIEWER_RECOMMENDATION_SCHEMA_VERSION = (
|
|
18
|
+
"issue_fix_reviewer_recommendation_v0"
|
|
19
|
+
)
|
|
20
|
+
ISSUE_FIX_REVIEWER_SOURCES_INPUT_SCHEMA_VERSION = "issue_fix_reviewer_sources_input_v0"
|
|
21
|
+
CODEOWNERS_LOCATIONS = (
|
|
22
|
+
".github/CODEOWNERS",
|
|
23
|
+
"CODEOWNERS",
|
|
24
|
+
"docs/CODEOWNERS",
|
|
25
|
+
)
|
|
26
|
+
GITHUB_NOREPLY_PATTERN = re.compile(
|
|
27
|
+
r"^(?:\d+\+)?(?P<login>[A-Za-z0-9](?:[A-Za-z0-9-]{0,38}))@users\.noreply\.github\.com$",
|
|
28
|
+
re.IGNORECASE,
|
|
29
|
+
)
|
|
30
|
+
REVIEWER_HANDLE_PATTERN = re.compile(
|
|
31
|
+
r"^@[A-Za-z0-9](?:[A-Za-z0-9-]{0,38})(?:/[A-Za-z0-9_.-]+)?$"
|
|
32
|
+
)
|
|
33
|
+
SAFE_GIT_REF_PATTERN = re.compile(r"^[A-Za-z0-9][A-Za-z0-9._/~^{}-]{0,119}$")
|
|
34
|
+
AUTOMATED_HISTORY_IDENTITY_PATTERN = re.compile(
|
|
35
|
+
r"(?:^|[-_.\s])bot(?:$|[-_.\s])|\[bot\]$",
|
|
36
|
+
re.IGNORECASE,
|
|
37
|
+
)
|
|
38
|
+
REVIEWER_SOURCE_KINDS = {"maintainer_map"}
|
|
39
|
+
REVIEWER_SOURCE_TRUST_LEVELS = {"authoritative", "verified", "advisory"}
|
|
40
|
+
REVIEWER_SOURCE_FRESHNESS_STATES = {"current", "stale", "unknown"}
|
|
41
|
+
REVIEWER_SOURCE_MATCH_KINDS = {
|
|
42
|
+
"path_prefix",
|
|
43
|
+
"path_glob",
|
|
44
|
+
"repository_fallback",
|
|
45
|
+
}
|
|
46
|
+
REVIEWER_SOURCE_ID_PATTERN = re.compile(r"^[A-Za-z0-9][A-Za-z0-9_.:-]{0,119}$")
|
|
47
|
+
REVIEWER_SOURCE_INPUT_FIELDS = {"schema_version", "sources"}
|
|
48
|
+
REVIEWER_SOURCE_FIELDS = {
|
|
49
|
+
"source_id",
|
|
50
|
+
"source_kind",
|
|
51
|
+
"reference",
|
|
52
|
+
"trust",
|
|
53
|
+
"freshness",
|
|
54
|
+
"observed_at",
|
|
55
|
+
"routes",
|
|
56
|
+
}
|
|
57
|
+
REVIEWER_SOURCE_ROUTE_FIELDS = {
|
|
58
|
+
"route_id",
|
|
59
|
+
"match_kind",
|
|
60
|
+
"pattern",
|
|
61
|
+
"primary_reviewers",
|
|
62
|
+
"fallback_reviewers",
|
|
63
|
+
}
|
|
64
|
+
MAX_REVIEWER_SOURCES = 12
|
|
65
|
+
MAX_REVIEWER_SOURCE_ROUTES = 100
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
def _run_git(repo_path: Path, args: Sequence[str]) -> str:
|
|
69
|
+
result = subprocess.run(
|
|
70
|
+
["git", *args],
|
|
71
|
+
cwd=repo_path,
|
|
72
|
+
text=True,
|
|
73
|
+
stdout=subprocess.PIPE,
|
|
74
|
+
stderr=subprocess.PIPE,
|
|
75
|
+
timeout=20,
|
|
76
|
+
)
|
|
77
|
+
if result.returncode != 0:
|
|
78
|
+
raise RuntimeError(f"git {' '.join(args)} failed with exit code {result.returncode}")
|
|
79
|
+
return result.stdout
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
def _normalise_changed_file(value: Any) -> str:
|
|
83
|
+
text = str(value or "").strip().replace("\\", "/")
|
|
84
|
+
while text.startswith("./"):
|
|
85
|
+
text = text[2:]
|
|
86
|
+
path = PurePosixPath(text)
|
|
87
|
+
if not text or path.as_posix() == "." or path.is_absolute() or ".." in path.parts:
|
|
88
|
+
raise ValueError("changed files must be non-empty repo-relative paths")
|
|
89
|
+
return path.as_posix()
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
def _normalise_changed_files(values: Sequence[Any]) -> list[str]:
|
|
93
|
+
files: list[str] = []
|
|
94
|
+
for raw in values:
|
|
95
|
+
path = _normalise_changed_file(raw)
|
|
96
|
+
if path not in files:
|
|
97
|
+
files.append(path)
|
|
98
|
+
if len(files) > 100:
|
|
99
|
+
raise ValueError("reviewer recommendation accepts at most 100 changed files")
|
|
100
|
+
return files
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
def _normalise_base_ref(value: Any) -> str:
|
|
104
|
+
text = str(value or "").strip()
|
|
105
|
+
if not SAFE_GIT_REF_PATTERN.fullmatch(text):
|
|
106
|
+
raise ValueError("base_ref must be a compact non-option git revision")
|
|
107
|
+
return text
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
def _normalise_reviewer_handle(value: Any) -> str | None:
|
|
111
|
+
text = str(value or "").strip()
|
|
112
|
+
if text and not text.startswith("@"):
|
|
113
|
+
text = f"@{text}"
|
|
114
|
+
return text.lower() if REVIEWER_HANDLE_PATTERN.fullmatch(text) else None
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
def _normalise_author_name(value: Any) -> str | None:
|
|
118
|
+
text = public_safe_compact_text(value, limit=80)
|
|
119
|
+
return " ".join(text.lower().split()) if text else None
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
def _normalise_resolved_identities(
|
|
123
|
+
values: Mapping[str, Any] | None,
|
|
124
|
+
) -> dict[str, str]:
|
|
125
|
+
resolved: dict[str, str] = {}
|
|
126
|
+
for raw_name, raw_handle in (values or {}).items():
|
|
127
|
+
name = _normalise_author_name(raw_name)
|
|
128
|
+
handle = _normalise_reviewer_handle(raw_handle)
|
|
129
|
+
if not name or not handle:
|
|
130
|
+
raise ValueError(
|
|
131
|
+
"resolved reviewer identities require a public-safe git display "
|
|
132
|
+
"name and normalized GitHub handle"
|
|
133
|
+
)
|
|
134
|
+
resolved[name] = handle
|
|
135
|
+
if len(resolved) > 50:
|
|
136
|
+
raise ValueError("at most 50 reviewer identities may be resolved per request")
|
|
137
|
+
return resolved
|
|
138
|
+
|
|
139
|
+
|
|
140
|
+
def _normalise_source_reference(value: Any, *, field: str) -> str:
|
|
141
|
+
reference = public_safe_compact_text(value, limit=300)
|
|
142
|
+
if not reference:
|
|
143
|
+
raise ValueError(f"{field} must be compact and public-safe")
|
|
144
|
+
if "://" in reference:
|
|
145
|
+
parsed = urlsplit(reference)
|
|
146
|
+
if (
|
|
147
|
+
parsed.scheme != "https"
|
|
148
|
+
or not parsed.netloc
|
|
149
|
+
or parsed.username
|
|
150
|
+
or parsed.password
|
|
151
|
+
or parsed.query
|
|
152
|
+
):
|
|
153
|
+
raise ValueError(
|
|
154
|
+
f"{field} URL must be public https without user info or query"
|
|
155
|
+
)
|
|
156
|
+
hostname = (parsed.hostname or "").lower()
|
|
157
|
+
if hostname == "localhost" or hostname.endswith((".localhost", ".local")):
|
|
158
|
+
raise ValueError(f"{field} URL must not target a local host")
|
|
159
|
+
try:
|
|
160
|
+
address = ipaddress.ip_address(hostname)
|
|
161
|
+
except ValueError:
|
|
162
|
+
address = None
|
|
163
|
+
if address and (
|
|
164
|
+
address.is_private
|
|
165
|
+
or address.is_loopback
|
|
166
|
+
or address.is_link_local
|
|
167
|
+
or address.is_reserved
|
|
168
|
+
):
|
|
169
|
+
raise ValueError(f"{field} URL must target a public host")
|
|
170
|
+
return reference
|
|
171
|
+
if reference.startswith(("/", "~")) or re.match(r"^[A-Za-z]:[\\/]", reference):
|
|
172
|
+
raise ValueError(f"{field} must not be an absolute or home-relative path")
|
|
173
|
+
path = PurePosixPath(reference.replace("\\", "/"))
|
|
174
|
+
if path.as_posix() == "." or ".." in path.parts:
|
|
175
|
+
raise ValueError(f"{field} must be a repo-relative public reference")
|
|
176
|
+
return path.as_posix()
|
|
177
|
+
|
|
178
|
+
|
|
179
|
+
def _normalise_source_handle_list(value: Any, *, field: str) -> list[str]:
|
|
180
|
+
if value is None:
|
|
181
|
+
return []
|
|
182
|
+
if not isinstance(value, Sequence) or isinstance(value, (str, bytes)):
|
|
183
|
+
raise ValueError(f"{field} must be a list of GitHub handles")
|
|
184
|
+
handles: list[str] = []
|
|
185
|
+
for raw in value:
|
|
186
|
+
handle = _normalise_reviewer_handle(raw)
|
|
187
|
+
if not handle:
|
|
188
|
+
raise ValueError(f"{field} must contain normalized GitHub handles")
|
|
189
|
+
if handle not in handles:
|
|
190
|
+
handles.append(handle)
|
|
191
|
+
if len(handles) > 10:
|
|
192
|
+
raise ValueError(f"{field} accepts at most 10 reviewer handles")
|
|
193
|
+
return handles
|
|
194
|
+
|
|
195
|
+
|
|
196
|
+
def _normalise_source_observed_at(value: Any, *, field: str) -> str:
|
|
197
|
+
observed_at = public_safe_compact_text(value, limit=50)
|
|
198
|
+
if not observed_at:
|
|
199
|
+
raise ValueError(f"{field} must be a timezone-aware ISO timestamp")
|
|
200
|
+
try:
|
|
201
|
+
parsed = datetime.fromisoformat(observed_at.replace("Z", "+00:00"))
|
|
202
|
+
except ValueError as exc:
|
|
203
|
+
raise ValueError(f"{field} must be a timezone-aware ISO timestamp") from exc
|
|
204
|
+
if parsed.tzinfo is None:
|
|
205
|
+
raise ValueError(f"{field} must be a timezone-aware ISO timestamp")
|
|
206
|
+
return observed_at
|
|
207
|
+
|
|
208
|
+
|
|
209
|
+
def _normalise_source_route_pattern(value: Any, *, field: str) -> str:
|
|
210
|
+
pattern = public_safe_compact_text(value, limit=220).replace("\\", "/")
|
|
211
|
+
while pattern.startswith("./"):
|
|
212
|
+
pattern = pattern[2:]
|
|
213
|
+
if not pattern or pattern.startswith(("/", "~")):
|
|
214
|
+
raise ValueError(f"{field} must be a repo-relative path pattern")
|
|
215
|
+
path = PurePosixPath(pattern)
|
|
216
|
+
if ".." in path.parts:
|
|
217
|
+
raise ValueError(f"{field} must not traverse outside the repository")
|
|
218
|
+
return pattern.rstrip("/")
|
|
219
|
+
|
|
220
|
+
|
|
221
|
+
def _normalise_reviewer_source_route(raw: Any, *, index: int) -> dict[str, Any]:
|
|
222
|
+
if not isinstance(raw, Mapping):
|
|
223
|
+
raise ValueError(f"reviewer source routes[{index}] must be an object")
|
|
224
|
+
unknown = sorted(set(raw) - REVIEWER_SOURCE_ROUTE_FIELDS)
|
|
225
|
+
if unknown:
|
|
226
|
+
raise ValueError(
|
|
227
|
+
f"reviewer source routes[{index}] has unsupported fields: {unknown}"
|
|
228
|
+
)
|
|
229
|
+
route_id = public_safe_compact_text(raw.get("route_id"), limit=120)
|
|
230
|
+
if not route_id or not REVIEWER_SOURCE_ID_PATTERN.fullmatch(route_id):
|
|
231
|
+
raise ValueError(f"reviewer source routes[{index}].route_id has invalid shape")
|
|
232
|
+
match_kind = str(raw.get("match_kind") or "").strip()
|
|
233
|
+
if match_kind not in REVIEWER_SOURCE_MATCH_KINDS:
|
|
234
|
+
raise ValueError(
|
|
235
|
+
f"reviewer source routes[{index}].match_kind must be one of "
|
|
236
|
+
f"{sorted(REVIEWER_SOURCE_MATCH_KINDS)}"
|
|
237
|
+
)
|
|
238
|
+
pattern = None
|
|
239
|
+
if match_kind != "repository_fallback":
|
|
240
|
+
pattern = _normalise_source_route_pattern(
|
|
241
|
+
raw.get("pattern"), field=f"reviewer source routes[{index}].pattern"
|
|
242
|
+
)
|
|
243
|
+
elif raw.get("pattern") not in {None, ""}:
|
|
244
|
+
raise ValueError("repository_fallback routes must not declare a pattern")
|
|
245
|
+
primary = _normalise_source_handle_list(
|
|
246
|
+
raw.get("primary_reviewers"),
|
|
247
|
+
field=f"reviewer source routes[{index}].primary_reviewers",
|
|
248
|
+
)
|
|
249
|
+
fallback = _normalise_source_handle_list(
|
|
250
|
+
raw.get("fallback_reviewers"),
|
|
251
|
+
field=f"reviewer source routes[{index}].fallback_reviewers",
|
|
252
|
+
)
|
|
253
|
+
if not primary and not fallback:
|
|
254
|
+
raise ValueError(
|
|
255
|
+
"reviewer source routes require a primary or fallback reviewer"
|
|
256
|
+
)
|
|
257
|
+
return {
|
|
258
|
+
"schema_version": "issue_fix_reviewer_source_route_v0",
|
|
259
|
+
"route_id": route_id,
|
|
260
|
+
"match_kind": match_kind,
|
|
261
|
+
"pattern": pattern,
|
|
262
|
+
"primary_reviewers": primary,
|
|
263
|
+
"fallback_reviewers": fallback,
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
|
|
267
|
+
def _normalise_reviewer_sources(
|
|
268
|
+
value: Mapping[str, Any] | None,
|
|
269
|
+
) -> list[dict[str, Any]]:
|
|
270
|
+
if value is None:
|
|
271
|
+
return []
|
|
272
|
+
if not isinstance(value, Mapping):
|
|
273
|
+
raise ValueError("reviewer sources input must be an object")
|
|
274
|
+
if value.get("schema_version") != ISSUE_FIX_REVIEWER_SOURCES_INPUT_SCHEMA_VERSION:
|
|
275
|
+
raise ValueError(
|
|
276
|
+
"reviewer sources schema_version must be "
|
|
277
|
+
"issue_fix_reviewer_sources_input_v0"
|
|
278
|
+
)
|
|
279
|
+
unknown = sorted(set(value) - REVIEWER_SOURCE_INPUT_FIELDS)
|
|
280
|
+
if unknown:
|
|
281
|
+
raise ValueError(f"reviewer sources input has unsupported fields: {unknown}")
|
|
282
|
+
raw_sources = value.get("sources") or []
|
|
283
|
+
if not isinstance(raw_sources, Sequence) or isinstance(raw_sources, (str, bytes)):
|
|
284
|
+
raise ValueError("reviewer sources must be a list")
|
|
285
|
+
if len(raw_sources) > MAX_REVIEWER_SOURCES:
|
|
286
|
+
raise ValueError(
|
|
287
|
+
f"reviewer sources accepts at most {MAX_REVIEWER_SOURCES} sources"
|
|
288
|
+
)
|
|
289
|
+
|
|
290
|
+
sources: list[dict[str, Any]] = []
|
|
291
|
+
total_routes = 0
|
|
292
|
+
for index, raw in enumerate(raw_sources):
|
|
293
|
+
if not isinstance(raw, Mapping):
|
|
294
|
+
raise ValueError(f"reviewer sources[{index}] must be an object")
|
|
295
|
+
unknown = sorted(set(raw) - REVIEWER_SOURCE_FIELDS)
|
|
296
|
+
if unknown:
|
|
297
|
+
raise ValueError(
|
|
298
|
+
f"reviewer sources[{index}] has unsupported fields: {unknown}"
|
|
299
|
+
)
|
|
300
|
+
source_id = public_safe_compact_text(raw.get("source_id"), limit=120)
|
|
301
|
+
if not source_id or not REVIEWER_SOURCE_ID_PATTERN.fullmatch(source_id):
|
|
302
|
+
raise ValueError(f"reviewer sources[{index}].source_id has invalid shape")
|
|
303
|
+
source_kind = str(raw.get("source_kind") or "").strip()
|
|
304
|
+
if source_kind not in REVIEWER_SOURCE_KINDS:
|
|
305
|
+
raise ValueError(
|
|
306
|
+
f"reviewer sources[{index}].source_kind must be one of "
|
|
307
|
+
f"{sorted(REVIEWER_SOURCE_KINDS)}"
|
|
308
|
+
)
|
|
309
|
+
trust = str(raw.get("trust") or "").strip()
|
|
310
|
+
if trust not in REVIEWER_SOURCE_TRUST_LEVELS:
|
|
311
|
+
raise ValueError(
|
|
312
|
+
f"reviewer sources[{index}].trust must be one of "
|
|
313
|
+
f"{sorted(REVIEWER_SOURCE_TRUST_LEVELS)}"
|
|
314
|
+
)
|
|
315
|
+
freshness = str(raw.get("freshness") or "unknown").strip()
|
|
316
|
+
if freshness not in REVIEWER_SOURCE_FRESHNESS_STATES:
|
|
317
|
+
raise ValueError(
|
|
318
|
+
f"reviewer sources[{index}].freshness must be one of "
|
|
319
|
+
f"{sorted(REVIEWER_SOURCE_FRESHNESS_STATES)}"
|
|
320
|
+
)
|
|
321
|
+
raw_routes = raw.get("routes") or []
|
|
322
|
+
if not isinstance(raw_routes, Sequence) or isinstance(raw_routes, (str, bytes)):
|
|
323
|
+
raise ValueError(f"reviewer sources[{index}].routes must be a list")
|
|
324
|
+
if not raw_routes:
|
|
325
|
+
raise ValueError(f"reviewer sources[{index}].routes must not be empty")
|
|
326
|
+
total_routes += len(raw_routes)
|
|
327
|
+
if total_routes > MAX_REVIEWER_SOURCE_ROUTES:
|
|
328
|
+
raise ValueError(
|
|
329
|
+
f"reviewer sources accepts at most {MAX_REVIEWER_SOURCE_ROUTES} routes"
|
|
330
|
+
)
|
|
331
|
+
routes = [
|
|
332
|
+
_normalise_reviewer_source_route(route, index=route_index)
|
|
333
|
+
for route_index, route in enumerate(raw_routes)
|
|
334
|
+
]
|
|
335
|
+
route_ids = [route["route_id"] for route in routes]
|
|
336
|
+
if len(route_ids) != len(set(route_ids)):
|
|
337
|
+
raise ValueError(
|
|
338
|
+
f"reviewer sources[{index}] route_id values must be unique"
|
|
339
|
+
)
|
|
340
|
+
sources.append(
|
|
341
|
+
{
|
|
342
|
+
"schema_version": "issue_fix_reviewer_source_v0",
|
|
343
|
+
"source_id": source_id,
|
|
344
|
+
"source_kind": source_kind,
|
|
345
|
+
"reference": _normalise_source_reference(
|
|
346
|
+
raw.get("reference"),
|
|
347
|
+
field=f"reviewer sources[{index}].reference",
|
|
348
|
+
),
|
|
349
|
+
"trust": trust,
|
|
350
|
+
"freshness": freshness,
|
|
351
|
+
"observed_at": _normalise_source_observed_at(
|
|
352
|
+
raw.get("observed_at"),
|
|
353
|
+
field=f"reviewer sources[{index}].observed_at",
|
|
354
|
+
),
|
|
355
|
+
"routes": routes,
|
|
356
|
+
"raw_content_captured": False,
|
|
357
|
+
}
|
|
358
|
+
)
|
|
359
|
+
source_ids = [source["source_id"] for source in sources]
|
|
360
|
+
if len(source_ids) != len(set(source_ids)):
|
|
361
|
+
raise ValueError("reviewer source_id values must be unique")
|
|
362
|
+
return sources
|
|
363
|
+
|
|
364
|
+
|
|
365
|
+
def _codeowners_tokens(line: str) -> tuple[str, list[str]] | None:
|
|
366
|
+
stripped = line.strip()
|
|
367
|
+
if not stripped or stripped.startswith("#"):
|
|
368
|
+
return None
|
|
369
|
+
parts = stripped.split()
|
|
370
|
+
if len(parts) < 2:
|
|
371
|
+
return None
|
|
372
|
+
owners = [
|
|
373
|
+
handle
|
|
374
|
+
for handle in (_normalise_reviewer_handle(item) for item in parts[1:])
|
|
375
|
+
if handle
|
|
376
|
+
]
|
|
377
|
+
return (parts[0], owners) if owners else None
|
|
378
|
+
|
|
379
|
+
|
|
380
|
+
def _codeowners_pattern_matches(pattern: str, path: str) -> bool:
|
|
381
|
+
pattern = pattern.strip()
|
|
382
|
+
if not pattern or pattern.startswith("!"):
|
|
383
|
+
return False
|
|
384
|
+
anchored = pattern.startswith("/")
|
|
385
|
+
pattern = pattern.lstrip("/")
|
|
386
|
+
if pattern.endswith("/"):
|
|
387
|
+
prefix = pattern.rstrip("/")
|
|
388
|
+
return path == prefix or path.startswith(f"{prefix}/")
|
|
389
|
+
if "/" not in pattern:
|
|
390
|
+
return any(fnmatch.fnmatchcase(part, pattern) for part in path.split("/"))
|
|
391
|
+
if anchored:
|
|
392
|
+
return fnmatch.fnmatchcase(path, pattern)
|
|
393
|
+
return fnmatch.fnmatchcase(path, pattern) or PurePosixPath(path).match(pattern)
|
|
394
|
+
|
|
395
|
+
|
|
396
|
+
def _load_codeowners(repo_path: Path) -> tuple[str | None, list[tuple[str, list[str]]]]:
|
|
397
|
+
for relative in CODEOWNERS_LOCATIONS:
|
|
398
|
+
candidate = repo_path / relative
|
|
399
|
+
if not candidate.is_file():
|
|
400
|
+
continue
|
|
401
|
+
rows: list[tuple[str, list[str]]] = []
|
|
402
|
+
for line in candidate.read_text(encoding="utf-8", errors="replace").splitlines():
|
|
403
|
+
parsed = _codeowners_tokens(line)
|
|
404
|
+
if parsed is not None:
|
|
405
|
+
rows.append(parsed)
|
|
406
|
+
return relative, rows
|
|
407
|
+
return None, []
|
|
408
|
+
|
|
409
|
+
|
|
410
|
+
def _owners_for_path(
|
|
411
|
+
rules: Sequence[tuple[str, list[str]]],
|
|
412
|
+
path: str,
|
|
413
|
+
) -> tuple[str | None, list[str]]:
|
|
414
|
+
matched_pattern: str | None = None
|
|
415
|
+
owners: list[str] = []
|
|
416
|
+
for pattern, rule_owners in rules:
|
|
417
|
+
if _codeowners_pattern_matches(pattern, path):
|
|
418
|
+
matched_pattern = pattern
|
|
419
|
+
owners = list(rule_owners)
|
|
420
|
+
return matched_pattern, owners
|
|
421
|
+
|
|
422
|
+
|
|
423
|
+
def _github_handle_from_email(value: str) -> str | None:
|
|
424
|
+
match = GITHUB_NOREPLY_PATTERN.fullmatch(value.strip())
|
|
425
|
+
return f"@{match.group('login').lower()}" if match else None
|
|
426
|
+
|
|
427
|
+
|
|
428
|
+
def _looks_like_automated_history_identity(
|
|
429
|
+
*,
|
|
430
|
+
display_name: str,
|
|
431
|
+
handle: str | None,
|
|
432
|
+
) -> bool:
|
|
433
|
+
values = [display_name, (handle or "").lstrip("@")]
|
|
434
|
+
return any(AUTOMATED_HISTORY_IDENTITY_PATTERN.search(value) for value in values)
|
|
435
|
+
|
|
436
|
+
|
|
437
|
+
def _candidate_key(*, handle: str | None, display_name: str) -> str:
|
|
438
|
+
if handle:
|
|
439
|
+
return f"handle:{handle}"
|
|
440
|
+
normalised_name = " ".join(display_name.lower().split())
|
|
441
|
+
digest = hashlib.sha256(normalised_name.encode("utf-8")).hexdigest()[:12]
|
|
442
|
+
return f"git-author:{digest}"
|
|
443
|
+
|
|
444
|
+
|
|
445
|
+
def _candidate_id(key: str) -> str:
|
|
446
|
+
return "reviewer_" + hashlib.sha256(key.encode("utf-8")).hexdigest()[:12]
|
|
447
|
+
|
|
448
|
+
|
|
449
|
+
def _new_candidate(
|
|
450
|
+
*,
|
|
451
|
+
key: str,
|
|
452
|
+
handle: str | None,
|
|
453
|
+
display_name: str,
|
|
454
|
+
) -> dict[str, Any]:
|
|
455
|
+
return {
|
|
456
|
+
"candidate_id": _candidate_id(key),
|
|
457
|
+
"reviewer_handle": handle,
|
|
458
|
+
"display_name": display_name,
|
|
459
|
+
"requestable": bool(handle),
|
|
460
|
+
"score": 0,
|
|
461
|
+
"source_kinds": [],
|
|
462
|
+
"reason_codes": [],
|
|
463
|
+
"matched_files": [],
|
|
464
|
+
"codeowners_patterns": [],
|
|
465
|
+
"source_refs": [],
|
|
466
|
+
"reviewer_source_evidence": [],
|
|
467
|
+
"history_commit_count": 0,
|
|
468
|
+
"most_recent_history_rank": None,
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
|
|
472
|
+
def _append_unique(row: dict[str, Any], field: str, value: str, *, limit: int = 20) -> None:
|
|
473
|
+
values = row[field]
|
|
474
|
+
if value not in values and len(values) < limit:
|
|
475
|
+
values.append(value)
|
|
476
|
+
|
|
477
|
+
|
|
478
|
+
def _reviewer_source_route_matches(route: Mapping[str, Any], path: str) -> bool:
|
|
479
|
+
match_kind = route.get("match_kind")
|
|
480
|
+
pattern = str(route.get("pattern") or "")
|
|
481
|
+
if match_kind == "path_prefix":
|
|
482
|
+
return path == pattern or path.startswith(f"{pattern}/")
|
|
483
|
+
if match_kind == "path_glob":
|
|
484
|
+
return fnmatch.fnmatchcase(path, pattern) or PurePosixPath(path).match(pattern)
|
|
485
|
+
return False
|
|
486
|
+
|
|
487
|
+
|
|
488
|
+
def _reviewer_source_route_specificity(route: Mapping[str, Any]) -> int:
|
|
489
|
+
pattern = str(route.get("pattern") or "")
|
|
490
|
+
return len(re.sub(r"[*?\[\]]", "", pattern))
|
|
491
|
+
|
|
492
|
+
|
|
493
|
+
def _reviewer_source_routes_for_path(
|
|
494
|
+
source: Mapping[str, Any], path: str
|
|
495
|
+
) -> list[Mapping[str, Any]]:
|
|
496
|
+
routes = source.get("routes")
|
|
497
|
+
routes = routes if isinstance(routes, list) else []
|
|
498
|
+
scoped = [
|
|
499
|
+
route
|
|
500
|
+
for route in routes
|
|
501
|
+
if isinstance(route, Mapping) and _reviewer_source_route_matches(route, path)
|
|
502
|
+
]
|
|
503
|
+
if scoped:
|
|
504
|
+
max_specificity = max(
|
|
505
|
+
_reviewer_source_route_specificity(route) for route in scoped
|
|
506
|
+
)
|
|
507
|
+
return [
|
|
508
|
+
route
|
|
509
|
+
for route in scoped
|
|
510
|
+
if _reviewer_source_route_specificity(route) == max_specificity
|
|
511
|
+
]
|
|
512
|
+
return [
|
|
513
|
+
route
|
|
514
|
+
for route in routes
|
|
515
|
+
if isinstance(route, Mapping)
|
|
516
|
+
and route.get("match_kind") == "repository_fallback"
|
|
517
|
+
]
|
|
518
|
+
|
|
519
|
+
|
|
520
|
+
def _reviewer_source_score(*, role: str, trust: str, freshness: str) -> int:
|
|
521
|
+
base = 950 if role == "primary" else 250
|
|
522
|
+
trust_factor = {
|
|
523
|
+
"authoritative": 1.0,
|
|
524
|
+
"verified": 0.9,
|
|
525
|
+
"advisory": 0.5,
|
|
526
|
+
}[trust]
|
|
527
|
+
freshness_factor = {"current": 1.0, "unknown": 0.75, "stale": 0.4}[freshness]
|
|
528
|
+
return max(1, int(base * trust_factor * freshness_factor))
|
|
529
|
+
|
|
530
|
+
|
|
531
|
+
def _append_reviewer_source_evidence(
|
|
532
|
+
row: dict[str, Any], evidence: Mapping[str, Any]
|
|
533
|
+
) -> None:
|
|
534
|
+
values = row["reviewer_source_evidence"]
|
|
535
|
+
identity = (
|
|
536
|
+
evidence.get("source_id"),
|
|
537
|
+
evidence.get("route_id"),
|
|
538
|
+
evidence.get("role"),
|
|
539
|
+
evidence.get("matched_file"),
|
|
540
|
+
)
|
|
541
|
+
if any(
|
|
542
|
+
(
|
|
543
|
+
item.get("source_id"),
|
|
544
|
+
item.get("route_id"),
|
|
545
|
+
item.get("role"),
|
|
546
|
+
item.get("matched_file"),
|
|
547
|
+
)
|
|
548
|
+
== identity
|
|
549
|
+
for item in values
|
|
550
|
+
if isinstance(item, Mapping)
|
|
551
|
+
):
|
|
552
|
+
return
|
|
553
|
+
if len(values) < 30:
|
|
554
|
+
values.append(dict(evidence))
|
|
555
|
+
|
|
556
|
+
|
|
557
|
+
def _apply_reviewer_sources_for_path(
|
|
558
|
+
*,
|
|
559
|
+
candidates: dict[str, dict[str, Any]],
|
|
560
|
+
sources: Sequence[Mapping[str, Any]],
|
|
561
|
+
changed_file: str,
|
|
562
|
+
excluded: set[str],
|
|
563
|
+
) -> None:
|
|
564
|
+
for source in sources:
|
|
565
|
+
reference = str(source["reference"])
|
|
566
|
+
trust = str(source["trust"])
|
|
567
|
+
freshness = str(source["freshness"])
|
|
568
|
+
observed_at = str(source["observed_at"])
|
|
569
|
+
for route in _reviewer_source_routes_for_path(source, changed_file):
|
|
570
|
+
match_kind = str(route["match_kind"])
|
|
571
|
+
for role, field in (
|
|
572
|
+
("primary", "primary_reviewers"),
|
|
573
|
+
("fallback", "fallback_reviewers"),
|
|
574
|
+
):
|
|
575
|
+
for handle in route.get(field) or []:
|
|
576
|
+
if handle in excluded:
|
|
577
|
+
continue
|
|
578
|
+
key = _candidate_key(handle=handle, display_name=handle)
|
|
579
|
+
row = candidates.setdefault(
|
|
580
|
+
key,
|
|
581
|
+
_new_candidate(key=key, handle=handle, display_name=handle),
|
|
582
|
+
)
|
|
583
|
+
row["score"] += _reviewer_source_score(
|
|
584
|
+
role=role,
|
|
585
|
+
trust=trust,
|
|
586
|
+
freshness=freshness,
|
|
587
|
+
)
|
|
588
|
+
_append_unique(row, "source_kinds", "repository_maintainer_map")
|
|
589
|
+
_append_unique(row, "source_refs", reference)
|
|
590
|
+
_append_unique(row, "matched_files", changed_file)
|
|
591
|
+
_append_unique(
|
|
592
|
+
row,
|
|
593
|
+
"reason_codes",
|
|
594
|
+
(
|
|
595
|
+
"repository_declared_primary_contact"
|
|
596
|
+
if role == "primary"
|
|
597
|
+
else "repository_declared_fallback_contact"
|
|
598
|
+
),
|
|
599
|
+
)
|
|
600
|
+
if match_kind == "repository_fallback":
|
|
601
|
+
_append_unique(
|
|
602
|
+
row,
|
|
603
|
+
"reason_codes",
|
|
604
|
+
"repository_declared_cross_module_fallback",
|
|
605
|
+
)
|
|
606
|
+
_append_reviewer_source_evidence(
|
|
607
|
+
row,
|
|
608
|
+
{
|
|
609
|
+
"schema_version": "issue_fix_reviewer_source_evidence_v0",
|
|
610
|
+
"source_id": source["source_id"],
|
|
611
|
+
"source_kind": source["source_kind"],
|
|
612
|
+
"reference": reference,
|
|
613
|
+
"trust": trust,
|
|
614
|
+
"freshness": freshness,
|
|
615
|
+
"observed_at": observed_at,
|
|
616
|
+
"route_id": route["route_id"],
|
|
617
|
+
"match_kind": match_kind,
|
|
618
|
+
"pattern": route.get("pattern"),
|
|
619
|
+
"role": role,
|
|
620
|
+
"matched_file": changed_file,
|
|
621
|
+
"raw_content_captured": False,
|
|
622
|
+
},
|
|
623
|
+
)
|
|
624
|
+
|
|
625
|
+
|
|
626
|
+
def _derive_changed_files(repo_path: Path, base_ref: str) -> list[str]:
|
|
627
|
+
output = _run_git(repo_path, ["diff", "--name-only", f"{base_ref}...HEAD"])
|
|
628
|
+
return _normalise_changed_files(output.splitlines())
|
|
629
|
+
|
|
630
|
+
|
|
631
|
+
def _collect_history(
|
|
632
|
+
repo_path: Path,
|
|
633
|
+
path: str,
|
|
634
|
+
*,
|
|
635
|
+
revision: str,
|
|
636
|
+
history_limit: int,
|
|
637
|
+
) -> list[tuple[str, str]]:
|
|
638
|
+
output = _run_git(
|
|
639
|
+
repo_path,
|
|
640
|
+
[
|
|
641
|
+
"log",
|
|
642
|
+
f"--max-count={history_limit}",
|
|
643
|
+
"--format=%aN%x1f%aE",
|
|
644
|
+
revision,
|
|
645
|
+
"--",
|
|
646
|
+
path,
|
|
647
|
+
],
|
|
648
|
+
)
|
|
649
|
+
rows: list[tuple[str, str]] = []
|
|
650
|
+
for line in output.splitlines():
|
|
651
|
+
name, separator, email = line.partition("\x1f")
|
|
652
|
+
if not separator:
|
|
653
|
+
continue
|
|
654
|
+
safe_name = public_safe_compact_text(name, limit=80)
|
|
655
|
+
if safe_name:
|
|
656
|
+
rows.append((safe_name, email.strip()))
|
|
657
|
+
return rows
|
|
658
|
+
|
|
659
|
+
|
|
660
|
+
def _confidence(candidate: Mapping[str, Any]) -> str:
|
|
661
|
+
sources = set(candidate.get("source_kinds") or [])
|
|
662
|
+
coverage = len(candidate.get("matched_files") or [])
|
|
663
|
+
if "git_history" in sources and sources.intersection(
|
|
664
|
+
{"codeowners", "repository_maintainer_map"}
|
|
665
|
+
):
|
|
666
|
+
return "high"
|
|
667
|
+
if (
|
|
668
|
+
"codeowners" in sources
|
|
669
|
+
or "repository_declared_primary_contact"
|
|
670
|
+
in set(candidate.get("reason_codes") or [])
|
|
671
|
+
or coverage >= 2
|
|
672
|
+
):
|
|
673
|
+
return "medium"
|
|
674
|
+
return "low"
|
|
675
|
+
|
|
676
|
+
|
|
677
|
+
def _recommendation_status(candidates: Sequence[Mapping[str, Any]]) -> str:
|
|
678
|
+
if any(candidate.get("requestable") for candidate in candidates):
|
|
679
|
+
return "candidates_ready"
|
|
680
|
+
if candidates:
|
|
681
|
+
return "identity_resolution_required"
|
|
682
|
+
return "no_candidates"
|
|
683
|
+
|
|
684
|
+
|
|
685
|
+
def build_issue_fix_reviewer_recommendation_packet(
|
|
686
|
+
*,
|
|
687
|
+
repo_path: str | Path,
|
|
688
|
+
repo: str = "approved_local_repo",
|
|
689
|
+
changed_files: Sequence[str] = (),
|
|
690
|
+
base_ref: str = "origin/main",
|
|
691
|
+
history_limit: int = 40,
|
|
692
|
+
max_candidates: int = 5,
|
|
693
|
+
exclude_reviewers: Sequence[str] = (),
|
|
694
|
+
exclude_author_names: Sequence[str] = (),
|
|
695
|
+
resolved_identities: Mapping[str, Any] | None = None,
|
|
696
|
+
reviewer_sources_input: Mapping[str, Any] | None = None,
|
|
697
|
+
execute: bool = False,
|
|
698
|
+
generated_at: str | None = "2026-07-10T00:00:00Z",
|
|
699
|
+
) -> dict[str, Any]:
|
|
700
|
+
"""Recommend reviewers from caller-approved repository ownership evidence."""
|
|
701
|
+
|
|
702
|
+
safe_repo = public_safe_compact_text(repo, limit=120)
|
|
703
|
+
if not safe_repo:
|
|
704
|
+
raise ValueError("repo must be a compact public-safe label")
|
|
705
|
+
safe_base_ref = _normalise_base_ref(
|
|
706
|
+
public_safe_compact_text(base_ref, limit=120)
|
|
707
|
+
)
|
|
708
|
+
history_limit = min(max(int(history_limit), 1), 200)
|
|
709
|
+
max_candidates = min(max(int(max_candidates), 1), 20)
|
|
710
|
+
explicit_files = _normalise_changed_files(changed_files)
|
|
711
|
+
excluded = {
|
|
712
|
+
handle
|
|
713
|
+
for handle in (_normalise_reviewer_handle(item) for item in exclude_reviewers)
|
|
714
|
+
if handle
|
|
715
|
+
}
|
|
716
|
+
excluded_author_names = {
|
|
717
|
+
name
|
|
718
|
+
for name in (_normalise_author_name(item) for item in exclude_author_names)
|
|
719
|
+
if name
|
|
720
|
+
}
|
|
721
|
+
excluded_author_names.update(handle.lstrip("@").lower() for handle in excluded)
|
|
722
|
+
resolved = _normalise_resolved_identities(resolved_identities)
|
|
723
|
+
reviewer_sources = _normalise_reviewer_sources(reviewer_sources_input)
|
|
724
|
+
reviewer_source_refs = list(
|
|
725
|
+
dict.fromkeys(str(source["reference"]) for source in reviewer_sources)
|
|
726
|
+
)
|
|
727
|
+
|
|
728
|
+
packet: dict[str, Any] = {
|
|
729
|
+
"ok": True,
|
|
730
|
+
"schema_version": ISSUE_FIX_REVIEWER_RECOMMENDATION_SCHEMA_VERSION,
|
|
731
|
+
"mode": "issue-fix-reviewer-recommendation",
|
|
732
|
+
"generated_at": generated_at,
|
|
733
|
+
"repo": safe_repo,
|
|
734
|
+
"execute": execute,
|
|
735
|
+
"recommendation_status": "preview_only",
|
|
736
|
+
"changed_files": explicit_files,
|
|
737
|
+
"changed_file_count": len(explicit_files),
|
|
738
|
+
"candidates": [],
|
|
739
|
+
"excluded_reviewer_handles": sorted(excluded),
|
|
740
|
+
"excluded_author_name_count": len(excluded_author_names),
|
|
741
|
+
"resolved_identity_count": len(resolved),
|
|
742
|
+
"reviewer_source_count": len(reviewer_sources),
|
|
743
|
+
"reviewer_source_refs": reviewer_source_refs,
|
|
744
|
+
"evidence_summary": {
|
|
745
|
+
"schema_version": "issue_fix_reviewer_evidence_summary_v0",
|
|
746
|
+
"authority_order": [
|
|
747
|
+
"repository_codeowners",
|
|
748
|
+
"repository_declared_primary_contact",
|
|
749
|
+
"changed_path_git_history",
|
|
750
|
+
"changed_module_git_history_fallback",
|
|
751
|
+
"repository_declared_fallback_contact",
|
|
752
|
+
],
|
|
753
|
+
"codeowners_source": None,
|
|
754
|
+
"codeowners_pattern_support": "common_subset",
|
|
755
|
+
"history_limit_per_file": history_limit,
|
|
756
|
+
"history_revision": safe_base_ref,
|
|
757
|
+
"module_history_fallback": True,
|
|
758
|
+
"reviewer_source_count": len(reviewer_sources),
|
|
759
|
+
"reviewer_source_refs": reviewer_source_refs,
|
|
760
|
+
"automated_history_identities_excluded": True,
|
|
761
|
+
"raw_reviewer_source_input_captured": False,
|
|
762
|
+
"raw_codeowners_captured": False,
|
|
763
|
+
"raw_git_output_captured": False,
|
|
764
|
+
"commit_emails_captured": False,
|
|
765
|
+
},
|
|
766
|
+
"policy": {
|
|
767
|
+
"schema_version": "issue_fix_reviewer_policy_v0",
|
|
768
|
+
"recommendation_is_assignment": False,
|
|
769
|
+
"automatic_review_request_allowed": True,
|
|
770
|
+
"automatic_request_policy": "request_top_requestable_when_authorized",
|
|
771
|
+
"external_review_request_authority_required": True,
|
|
772
|
+
"default_max_review_requests": 1,
|
|
773
|
+
"repository_policy_still_applies": True,
|
|
774
|
+
"current_author_should_be_excluded_by_request_command": True,
|
|
775
|
+
"identity_resolution_required_for_non_handle_candidates": True,
|
|
776
|
+
"next_action": (
|
|
777
|
+
"After PR creation, run reviewer-request under external-review-request "
|
|
778
|
+
"authority; it excludes the live PR author and requests the top "
|
|
779
|
+
"requestable candidate."
|
|
780
|
+
),
|
|
781
|
+
},
|
|
782
|
+
"external_reads_performed": False,
|
|
783
|
+
"external_writes_performed": False,
|
|
784
|
+
"review_request_performed": False,
|
|
785
|
+
"private_repo_state_read": False,
|
|
786
|
+
"local_paths_captured": False,
|
|
787
|
+
"raw_git_output_captured": False,
|
|
788
|
+
"commit_emails_captured": False,
|
|
789
|
+
"raw_reviewer_source_input_captured": False,
|
|
790
|
+
}
|
|
791
|
+
if not execute:
|
|
792
|
+
packet["next_safe_action"] = (
|
|
793
|
+
"Rerun with --execute only for the caller-approved local repository."
|
|
794
|
+
)
|
|
795
|
+
packet["validation"] = validate_issue_fix_reviewer_recommendation_packet(packet)
|
|
796
|
+
return packet
|
|
797
|
+
|
|
798
|
+
path = Path(repo_path).expanduser()
|
|
799
|
+
if not path.is_dir():
|
|
800
|
+
raise ValueError("repo_path must be an existing caller-approved directory")
|
|
801
|
+
_run_git(path, ["rev-parse", "--is-inside-work-tree"])
|
|
802
|
+
files = explicit_files or _derive_changed_files(path, safe_base_ref)
|
|
803
|
+
if not files:
|
|
804
|
+
raise ValueError("reviewer recommendation requires at least one changed file")
|
|
805
|
+
|
|
806
|
+
codeowners_source, codeowners_rules = _load_codeowners(path)
|
|
807
|
+
candidates: dict[str, dict[str, Any]] = {}
|
|
808
|
+
for changed_file in files:
|
|
809
|
+
pattern, owners = _owners_for_path(codeowners_rules, changed_file)
|
|
810
|
+
for handle in owners:
|
|
811
|
+
if handle in excluded:
|
|
812
|
+
continue
|
|
813
|
+
key = _candidate_key(handle=handle, display_name=handle)
|
|
814
|
+
row = candidates.setdefault(
|
|
815
|
+
key,
|
|
816
|
+
_new_candidate(key=key, handle=handle, display_name=handle),
|
|
817
|
+
)
|
|
818
|
+
row["score"] += 1000
|
|
819
|
+
_append_unique(row, "source_kinds", "codeowners")
|
|
820
|
+
_append_unique(row, "reason_codes", "repository_codeowners_match")
|
|
821
|
+
_append_unique(row, "matched_files", changed_file)
|
|
822
|
+
if pattern:
|
|
823
|
+
_append_unique(row, "codeowners_patterns", pattern)
|
|
824
|
+
|
|
825
|
+
_apply_reviewer_sources_for_path(
|
|
826
|
+
candidates=candidates,
|
|
827
|
+
sources=reviewer_sources,
|
|
828
|
+
changed_file=changed_file,
|
|
829
|
+
excluded=excluded,
|
|
830
|
+
)
|
|
831
|
+
|
|
832
|
+
history_rows = _collect_history(
|
|
833
|
+
path,
|
|
834
|
+
changed_file,
|
|
835
|
+
revision=safe_base_ref,
|
|
836
|
+
history_limit=history_limit,
|
|
837
|
+
)
|
|
838
|
+
history_reason = "changed_path_commit_history"
|
|
839
|
+
usable_history_rows = [
|
|
840
|
+
row
|
|
841
|
+
for row in history_rows
|
|
842
|
+
if (
|
|
843
|
+
_github_handle_from_email(row[1])
|
|
844
|
+
or resolved.get(_normalise_author_name(row[0]) or "")
|
|
845
|
+
or ""
|
|
846
|
+
)
|
|
847
|
+
not in excluded
|
|
848
|
+
and _normalise_author_name(row[0]) not in excluded_author_names
|
|
849
|
+
and not _looks_like_automated_history_identity(
|
|
850
|
+
display_name=row[0],
|
|
851
|
+
handle=(
|
|
852
|
+
_github_handle_from_email(row[1])
|
|
853
|
+
or resolved.get(_normalise_author_name(row[0]) or "")
|
|
854
|
+
),
|
|
855
|
+
)
|
|
856
|
+
]
|
|
857
|
+
if not usable_history_rows:
|
|
858
|
+
parent = PurePosixPath(changed_file).parent.as_posix()
|
|
859
|
+
if parent not in {"", "."}:
|
|
860
|
+
history_rows = _collect_history(
|
|
861
|
+
path,
|
|
862
|
+
parent,
|
|
863
|
+
revision=safe_base_ref,
|
|
864
|
+
history_limit=history_limit,
|
|
865
|
+
)
|
|
866
|
+
history_reason = "changed_module_commit_history"
|
|
867
|
+
for rank, (name, email) in enumerate(history_rows, start=1):
|
|
868
|
+
resolved_handle = resolved.get(_normalise_author_name(name) or "")
|
|
869
|
+
handle = _github_handle_from_email(email) or resolved_handle
|
|
870
|
+
if handle and handle in excluded:
|
|
871
|
+
continue
|
|
872
|
+
if _normalise_author_name(name) in excluded_author_names:
|
|
873
|
+
continue
|
|
874
|
+
if _looks_like_automated_history_identity(
|
|
875
|
+
display_name=name,
|
|
876
|
+
handle=handle,
|
|
877
|
+
):
|
|
878
|
+
continue
|
|
879
|
+
key = _candidate_key(handle=handle, display_name=name)
|
|
880
|
+
row = candidates.setdefault(
|
|
881
|
+
key,
|
|
882
|
+
_new_candidate(key=key, handle=handle, display_name=name),
|
|
883
|
+
)
|
|
884
|
+
row["score"] += max(1, history_limit - rank + 1)
|
|
885
|
+
row["history_commit_count"] += 1
|
|
886
|
+
recent_rank = row["most_recent_history_rank"]
|
|
887
|
+
if recent_rank is None or rank < recent_rank:
|
|
888
|
+
row["most_recent_history_rank"] = rank
|
|
889
|
+
_append_unique(row, "source_kinds", "git_history")
|
|
890
|
+
if resolved_handle:
|
|
891
|
+
_append_unique(row, "source_kinds", "verified_identity_mapping")
|
|
892
|
+
_append_unique(row, "reason_codes", "caller_verified_github_identity")
|
|
893
|
+
_append_unique(row, "reason_codes", history_reason)
|
|
894
|
+
if rank <= 5:
|
|
895
|
+
_append_unique(row, "reason_codes", "recent_changed_path_contributor")
|
|
896
|
+
if handle is None:
|
|
897
|
+
_append_unique(row, "reason_codes", "github_identity_resolution_required")
|
|
898
|
+
_append_unique(row, "matched_files", changed_file)
|
|
899
|
+
|
|
900
|
+
ranked = sorted(
|
|
901
|
+
candidates.values(),
|
|
902
|
+
key=lambda row: (
|
|
903
|
+
-int(row["score"]),
|
|
904
|
+
-len(row["matched_files"]),
|
|
905
|
+
str(row.get("reviewer_handle") or row.get("display_name") or ""),
|
|
906
|
+
),
|
|
907
|
+
)[:max_candidates]
|
|
908
|
+
for rank, row in enumerate(ranked, start=1):
|
|
909
|
+
row["rank"] = rank
|
|
910
|
+
row["confidence"] = _confidence(row)
|
|
911
|
+
row["path_coverage_count"] = len(row["matched_files"])
|
|
912
|
+
|
|
913
|
+
packet.update(
|
|
914
|
+
{
|
|
915
|
+
"recommendation_status": _recommendation_status(ranked),
|
|
916
|
+
"changed_files": files,
|
|
917
|
+
"changed_file_count": len(files),
|
|
918
|
+
"candidates": ranked,
|
|
919
|
+
"private_repo_state_read": True,
|
|
920
|
+
"next_safe_action": (
|
|
921
|
+
"Run reviewer-request after PR creation when external-review-request "
|
|
922
|
+
"authority is active."
|
|
923
|
+
),
|
|
924
|
+
}
|
|
925
|
+
)
|
|
926
|
+
packet["evidence_summary"]["codeowners_source"] = codeowners_source
|
|
927
|
+
validation = validate_issue_fix_reviewer_recommendation_packet(packet)
|
|
928
|
+
packet["ok"] = validation["ok"]
|
|
929
|
+
packet["validation"] = validation
|
|
930
|
+
return packet
|
|
931
|
+
|
|
932
|
+
|
|
933
|
+
def validate_issue_fix_reviewer_recommendation_packet(
|
|
934
|
+
packet: Mapping[str, Any],
|
|
935
|
+
) -> dict[str, Any]:
|
|
936
|
+
errors: list[str] = []
|
|
937
|
+
if packet.get("schema_version") != ISSUE_FIX_REVIEWER_RECOMMENDATION_SCHEMA_VERSION:
|
|
938
|
+
errors.append("schema_version must be issue_fix_reviewer_recommendation_v0")
|
|
939
|
+
for key in (
|
|
940
|
+
"external_reads_performed",
|
|
941
|
+
"external_writes_performed",
|
|
942
|
+
"review_request_performed",
|
|
943
|
+
"local_paths_captured",
|
|
944
|
+
"raw_git_output_captured",
|
|
945
|
+
"commit_emails_captured",
|
|
946
|
+
"raw_reviewer_source_input_captured",
|
|
947
|
+
):
|
|
948
|
+
if packet.get(key) is not False:
|
|
949
|
+
errors.append(f"{key} must be false")
|
|
950
|
+
execute = packet.get("execute") is True
|
|
951
|
+
if packet.get("private_repo_state_read") is not execute:
|
|
952
|
+
errors.append("private_repo_state_read must match execute")
|
|
953
|
+
changed_files = packet.get("changed_files")
|
|
954
|
+
if not isinstance(changed_files, list):
|
|
955
|
+
errors.append("changed_files must be a list")
|
|
956
|
+
changed_files = []
|
|
957
|
+
for path in changed_files:
|
|
958
|
+
try:
|
|
959
|
+
_normalise_changed_file(path)
|
|
960
|
+
except ValueError:
|
|
961
|
+
errors.append("changed_files must contain only repo-relative paths")
|
|
962
|
+
break
|
|
963
|
+
reviewer_source_refs = packet.get("reviewer_source_refs")
|
|
964
|
+
if not isinstance(reviewer_source_refs, list):
|
|
965
|
+
errors.append("reviewer_source_refs must be a list")
|
|
966
|
+
reviewer_source_refs = []
|
|
967
|
+
for reference in reviewer_source_refs:
|
|
968
|
+
try:
|
|
969
|
+
_normalise_source_reference(reference, field="reviewer_source_refs")
|
|
970
|
+
except ValueError:
|
|
971
|
+
errors.append("reviewer_source_refs must contain public-safe references")
|
|
972
|
+
break
|
|
973
|
+
if not isinstance(packet.get("reviewer_source_count"), int):
|
|
974
|
+
errors.append("reviewer_source_count must be an integer")
|
|
975
|
+
candidates = packet.get("candidates")
|
|
976
|
+
if not isinstance(candidates, list):
|
|
977
|
+
errors.append("candidates must be a list")
|
|
978
|
+
candidates = []
|
|
979
|
+
for candidate in candidates:
|
|
980
|
+
if not isinstance(candidate, Mapping):
|
|
981
|
+
errors.append("reviewer candidate must be an object")
|
|
982
|
+
continue
|
|
983
|
+
handle = candidate.get("reviewer_handle")
|
|
984
|
+
if handle is not None and _normalise_reviewer_handle(handle) != handle:
|
|
985
|
+
errors.append("reviewer_handle must be a normalized GitHub handle")
|
|
986
|
+
if candidate.get("requestable") is not bool(handle):
|
|
987
|
+
errors.append("requestable must reflect reviewer_handle availability")
|
|
988
|
+
source_refs = candidate.get("source_refs")
|
|
989
|
+
if not isinstance(source_refs, list):
|
|
990
|
+
errors.append("candidate source_refs must be a list")
|
|
991
|
+
evidence = candidate.get("reviewer_source_evidence")
|
|
992
|
+
if not isinstance(evidence, list):
|
|
993
|
+
errors.append("candidate reviewer_source_evidence must be a list")
|
|
994
|
+
evidence = []
|
|
995
|
+
for item in evidence:
|
|
996
|
+
if not isinstance(item, Mapping):
|
|
997
|
+
errors.append("reviewer source evidence must be an object")
|
|
998
|
+
continue
|
|
999
|
+
if item.get("raw_content_captured") is not False:
|
|
1000
|
+
errors.append("reviewer source evidence must not capture raw content")
|
|
1001
|
+
try:
|
|
1002
|
+
_normalise_source_reference(
|
|
1003
|
+
item.get("reference"), field="reviewer source evidence reference"
|
|
1004
|
+
)
|
|
1005
|
+
_normalise_source_observed_at(
|
|
1006
|
+
item.get("observed_at"),
|
|
1007
|
+
field="reviewer source evidence observed_at",
|
|
1008
|
+
)
|
|
1009
|
+
except ValueError:
|
|
1010
|
+
errors.append("reviewer source evidence must have safe provenance")
|
|
1011
|
+
policy = packet.get("policy")
|
|
1012
|
+
if not isinstance(policy, Mapping):
|
|
1013
|
+
errors.append("policy is required")
|
|
1014
|
+
elif policy.get("automatic_review_request_allowed") is not True:
|
|
1015
|
+
errors.append("reviewer policy must allow the authority-gated default request")
|
|
1016
|
+
elif policy.get("external_review_request_authority_required") is not True:
|
|
1017
|
+
errors.append("automatic reviewer requests require external-write authority")
|
|
1018
|
+
return {
|
|
1019
|
+
"ok": not errors,
|
|
1020
|
+
"schema_version": "issue_fix_reviewer_recommendation_validation_v0",
|
|
1021
|
+
"errors": errors,
|
|
1022
|
+
}
|
|
1023
|
+
|
|
1024
|
+
|
|
1025
|
+
def render_issue_fix_reviewer_recommendation_markdown(
|
|
1026
|
+
payload: Mapping[str, Any],
|
|
1027
|
+
) -> str:
|
|
1028
|
+
lines = [
|
|
1029
|
+
"# LoopX Issue-Fix Reviewer Recommendation",
|
|
1030
|
+
"",
|
|
1031
|
+
f"- ok: `{payload.get('ok')}`",
|
|
1032
|
+
f"- repo: `{payload.get('repo')}`",
|
|
1033
|
+
f"- execute: `{payload.get('execute')}`",
|
|
1034
|
+
f"- recommendation_status: `{payload.get('recommendation_status')}`",
|
|
1035
|
+
f"- changed_file_count: `{payload.get('changed_file_count')}`",
|
|
1036
|
+
f"- external_writes_performed: `{payload.get('external_writes_performed')}`",
|
|
1037
|
+
f"- review_request_performed: `{payload.get('review_request_performed')}`",
|
|
1038
|
+
"",
|
|
1039
|
+
"## Candidates",
|
|
1040
|
+
"",
|
|
1041
|
+
]
|
|
1042
|
+
candidates = payload.get("candidates")
|
|
1043
|
+
if not isinstance(candidates, list) or not candidates:
|
|
1044
|
+
lines.append("- none")
|
|
1045
|
+
else:
|
|
1046
|
+
for candidate in candidates:
|
|
1047
|
+
if not isinstance(candidate, Mapping):
|
|
1048
|
+
continue
|
|
1049
|
+
identity = candidate.get("reviewer_handle") or candidate.get("display_name")
|
|
1050
|
+
lines.append(
|
|
1051
|
+
f"{candidate.get('rank')}. `{identity}` score=`{candidate.get('score')}` "
|
|
1052
|
+
f"confidence=`{candidate.get('confidence')}` "
|
|
1053
|
+
f"sources=`{','.join(candidate.get('source_kinds') or [])}` "
|
|
1054
|
+
f"refs=`{','.join(candidate.get('source_refs') or [])}`"
|
|
1055
|
+
)
|
|
1056
|
+
lines.extend(["", f"Next: {payload.get('next_safe_action')}"])
|
|
1057
|
+
return "\n".join(lines)
|