agentic-project-kit 1.0.1__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.
- agentic_project_kit/__init__.py +1 -0
- agentic_project_kit/absolute_path_portability_audit.py +241 -0
- agentic_project_kit/access_level_policy.py +14 -0
- agentic_project_kit/access_levels.py +22 -0
- agentic_project_kit/action_registry.py +195 -0
- agentic_project_kit/action_specs.py +286 -0
- agentic_project_kit/agent_command_runner.py +516 -0
- agentic_project_kit/audit_test_matrix.py +36 -0
- agentic_project_kit/audit_test_matrix_cli.py +43 -0
- agentic_project_kit/chat_bootloader.py +257 -0
- agentic_project_kit/chat_entrypoint_contract.py +297 -0
- agentic_project_kit/checks.py +511 -0
- agentic_project_kit/ci_readiness.py +211 -0
- agentic_project_kit/cli.py +147 -0
- agentic_project_kit/cli_commands/__init__.py +1 -0
- agentic_project_kit/cli_commands/absolute_path_audit.py +25 -0
- agentic_project_kit/cli_commands/actions.py +21 -0
- agentic_project_kit/cli_commands/artifact_gc.py +240 -0
- agentic_project_kit/cli_commands/boot.py +55 -0
- agentic_project_kit/cli_commands/chat.py +34 -0
- agentic_project_kit/cli_commands/checks.py +305 -0
- agentic_project_kit/cli_commands/cockpit.py +103 -0
- agentic_project_kit/cli_commands/command_taxonomy.py +25 -0
- agentic_project_kit/cli_commands/commands.py +129 -0
- agentic_project_kit/cli_commands/dev.py +23 -0
- agentic_project_kit/cli_commands/direction.py +92 -0
- agentic_project_kit/cli_commands/doc_currency_audit.py +25 -0
- agentic_project_kit/cli_commands/doc_registry.py +126 -0
- agentic_project_kit/cli_commands/docs.py +223 -0
- agentic_project_kit/cli_commands/dpa.py +1338 -0
- agentic_project_kit/cli_commands/evidence.py +251 -0
- agentic_project_kit/cli_commands/github.py +34 -0
- agentic_project_kit/cli_commands/governance.py +14 -0
- agentic_project_kit/cli_commands/gui.py +23 -0
- agentic_project_kit/cli_commands/gui_readiness_gate.py +27 -0
- agentic_project_kit/cli_commands/handoff.py +183 -0
- agentic_project_kit/cli_commands/human_workflows.py +430 -0
- agentic_project_kit/cli_commands/init.py +132 -0
- agentic_project_kit/cli_commands/instruction.py +75 -0
- agentic_project_kit/cli_commands/mutation_lock_audit.py +24 -0
- agentic_project_kit/cli_commands/ns_legacy_audit.py +25 -0
- agentic_project_kit/cli_commands/pass_already_done.py +66 -0
- agentic_project_kit/cli_commands/patch_failure_discipline_audit.py +26 -0
- agentic_project_kit/cli_commands/patch_preflight.py +37 -0
- agentic_project_kit/cli_commands/path_literal_audit.py +39 -0
- agentic_project_kit/cli_commands/patterns.py +40 -0
- agentic_project_kit/cli_commands/planning_docs_audit.py +25 -0
- agentic_project_kit/cli_commands/pr.py +133 -0
- agentic_project_kit/cli_commands/pr_hygiene.py +27 -0
- agentic_project_kit/cli_commands/profiles.py +26 -0
- agentic_project_kit/cli_commands/program_redundancy_audit.py +25 -0
- agentic_project_kit/cli_commands/project_direction.py +25 -0
- agentic_project_kit/cli_commands/release.py +392 -0
- agentic_project_kit/cli_commands/release_publish_orchestration.py +36 -0
- agentic_project_kit/cli_commands/remote_branch_hygiene.py +152 -0
- agentic_project_kit/cli_commands/remote_next.py +57 -0
- agentic_project_kit/cli_commands/removed_ns_commands.py +65 -0
- agentic_project_kit/cli_commands/rule_registry.py +129 -0
- agentic_project_kit/cli_commands/rules.py +222 -0
- agentic_project_kit/cli_commands/scaffold.py +46 -0
- agentic_project_kit/cli_commands/slice.py +21 -0
- agentic_project_kit/cli_commands/standard_gates_audit_suite.py +27 -0
- agentic_project_kit/cli_commands/state.py +51 -0
- agentic_project_kit/cli_commands/status_current_state_audit.py +30 -0
- agentic_project_kit/cli_commands/todo.py +53 -0
- agentic_project_kit/cli_commands/transfer.py +19 -0
- agentic_project_kit/cli_commands/transfer_context_flow.py +398 -0
- agentic_project_kit/cli_commands/transfer_context_helpers.py +398 -0
- agentic_project_kit/cli_commands/transfer_diagnostics.py +767 -0
- agentic_project_kit/cli_commands/transfer_evidence_flow.py +368 -0
- agentic_project_kit/cli_commands/transfer_handoff_flow.py +594 -0
- agentic_project_kit/cli_commands/transfer_order_contract.py +74 -0
- agentic_project_kit/cli_commands/transfer_post_merge_complete.py +442 -0
- agentic_project_kit/cli_commands/transfer_post_merge_settle.py +171 -0
- agentic_project_kit/cli_commands/transfer_pr_closeout_complete.py +124 -0
- agentic_project_kit/cli_commands/transfer_pr_create_flow.py +743 -0
- agentic_project_kit/cli_commands/transfer_pr_merge_flow.py +547 -0
- agentic_project_kit/cli_commands/transfer_repo_after_pr.py +160 -0
- agentic_project_kit/cli_commands/transfer_repo_pre_pr.py +664 -0
- agentic_project_kit/cli_commands/transfer_runner_flow.py +131 -0
- agentic_project_kit/cli_commands/transfer_runtime_initial.py +194 -0
- agentic_project_kit/cli_commands/transfer_shared.py +274 -0
- agentic_project_kit/cli_commands/validation.py +200 -0
- agentic_project_kit/cli_commands/version.py +27 -0
- agentic_project_kit/cli_commands/work_orders.py +219 -0
- agentic_project_kit/cli_commands/workflow.py +471 -0
- agentic_project_kit/cli_commands/workflow_guard.py +26 -0
- agentic_project_kit/cli_commands/workspace.py +252 -0
- agentic_project_kit/cockpit.py +608 -0
- agentic_project_kit/cockpit_readiness.py +67 -0
- agentic_project_kit/command_authority_audit.py +192 -0
- agentic_project_kit/command_inbox_check.py +103 -0
- agentic_project_kit/command_manifest.py +591 -0
- agentic_project_kit/command_queue_contract.py +161 -0
- agentic_project_kit/command_selector.py +245 -0
- agentic_project_kit/command_taxonomy.py +264 -0
- agentic_project_kit/commit_guard.py +67 -0
- agentic_project_kit/communication_artifact_gc.py +495 -0
- agentic_project_kit/communication_rule_context.py +437 -0
- agentic_project_kit/communication_state.py +115 -0
- agentic_project_kit/composed_wrapper_invariants.py +97 -0
- agentic_project_kit/composite_short_circuit.py +140 -0
- agentic_project_kit/contract.py +251 -0
- agentic_project_kit/control_state.py +83 -0
- agentic_project_kit/doc_currency_audit.py +536 -0
- agentic_project_kit/doc_lifecycle.py +1179 -0
- agentic_project_kit/doc_lifecycle_signals.py +446 -0
- agentic_project_kit/doc_lifecycle_sweep.py +639 -0
- agentic_project_kit/doc_mesh.py +491 -0
- agentic_project_kit/doctor.py +379 -0
- agentic_project_kit/documentation_registry.py +1122 -0
- agentic_project_kit/documentation_system_audit.py +310 -0
- agentic_project_kit/dpa_current_handoff_lifecycle.py +549 -0
- agentic_project_kit/dpa_dp2_decision_readiness.py +362 -0
- agentic_project_kit/dpa_dp3_dp4_adjudication.py +604 -0
- agentic_project_kit/dpa_dp5_block_new_gate.py +294 -0
- agentic_project_kit/dpa_dp5_stage_adoption.py +615 -0
- agentic_project_kit/dpa_dp5_strict_gate.py +187 -0
- agentic_project_kit/dpa_final_closeout.py +627 -0
- agentic_project_kit/dpa_fixture_evidence.py +680 -0
- agentic_project_kit/dpa_maintainer_record_check.py +711 -0
- agentic_project_kit/dpa_post_dp2_scope_assessment.py +734 -0
- agentic_project_kit/dpa_probe_002_readiness.py +509 -0
- agentic_project_kit/dpa_probe_003_readiness.py +417 -0
- agentic_project_kit/dpa_probe_004_readiness.py +437 -0
- agentic_project_kit/dpa_readiness.py +505 -0
- agentic_project_kit/dpa_readonly_probe_execution.py +500 -0
- agentic_project_kit/dpa_registry_contracts.py +312 -0
- agentic_project_kit/dpa_renderer_readiness.py +407 -0
- agentic_project_kit/dpa_repo_adoption_assessment.py +574 -0
- agentic_project_kit/dpa_stable_readiness.py +743 -0
- agentic_project_kit/dpa_successor_projection.py +168 -0
- agentic_project_kit/dpa_workspace_init_projection.py +93 -0
- agentic_project_kit/dpa_wrt_ch001_evidence.py +327 -0
- agentic_project_kit/entrypoint_slice_runner.py +153 -0
- agentic_project_kit/evidence_clean.py +152 -0
- agentic_project_kit/evidence_commit_paths.py +214 -0
- agentic_project_kit/evidence_finalize_log.py +204 -0
- agentic_project_kit/evidence_guard.py +114 -0
- agentic_project_kit/evidence_inspector.py +507 -0
- agentic_project_kit/evidence_state_contract.py +99 -0
- agentic_project_kit/execution_mode_state.py +148 -0
- agentic_project_kit/final_summary_contract.py +85 -0
- agentic_project_kit/finalize_guard.py +139 -0
- agentic_project_kit/gatekeeper_core.py +232 -0
- agentic_project_kit/github.py +25 -0
- agentic_project_kit/governance.py +59 -0
- agentic_project_kit/gui_action_execution.py +105 -0
- agentic_project_kit/gui_action_renderer.py +81 -0
- agentic_project_kit/gui_action_run_intent.py +98 -0
- agentic_project_kit/gui_action_views.py +195 -0
- agentic_project_kit/gui_activity_log.py +76 -0
- agentic_project_kit/gui_button_catalog.py +605 -0
- agentic_project_kit/gui_cockpit.py +365 -0
- agentic_project_kit/gui_cockpit_actions.py +695 -0
- agentic_project_kit/gui_cockpit_common.py +172 -0
- agentic_project_kit/gui_cockpit_header.py +501 -0
- agentic_project_kit/gui_cockpit_sidebar.py +387 -0
- agentic_project_kit/gui_cockpit_task.py +748 -0
- agentic_project_kit/gui_command_projection.py +355 -0
- agentic_project_kit/gui_communication_modes.py +130 -0
- agentic_project_kit/gui_dry_run.py +219 -0
- agentic_project_kit/gui_gatekeeper_status.py +259 -0
- agentic_project_kit/gui_layout_plan.py +148 -0
- agentic_project_kit/gui_open_folder.py +18 -0
- agentic_project_kit/gui_output_status_panel.py +61 -0
- agentic_project_kit/gui_panel_state.py +47 -0
- agentic_project_kit/gui_presenter.py +62 -0
- agentic_project_kit/gui_readiness_gate.py +206 -0
- agentic_project_kit/gui_release_flow.py +59 -0
- agentic_project_kit/gui_task_editor.py +1077 -0
- agentic_project_kit/gui_tk_widgets.py +177 -0
- agentic_project_kit/gui_tkinter_renderer.py +87 -0
- agentic_project_kit/gui_tkinter_shell.py +1176 -0
- agentic_project_kit/gui_transfer_contract.py +28 -0
- agentic_project_kit/gui_viewmodel.py +698 -0
- agentic_project_kit/gui_window_guard.py +46 -0
- agentic_project_kit/handoff_freshness.py +390 -0
- agentic_project_kit/handoff_prompt.py +180 -0
- agentic_project_kit/handoff_state.py +254 -0
- agentic_project_kit/instruction_lint.py +455 -0
- agentic_project_kit/llm_context_carriers.py +66 -0
- agentic_project_kit/llm_execution_context.py +285 -0
- agentic_project_kit/local_command_stack.py +133 -0
- agentic_project_kit/local_feature_gate.py +82 -0
- agentic_project_kit/local_garbage_collector.py +269 -0
- agentic_project_kit/models.py +18 -0
- agentic_project_kit/mutation_lock_audit.py +421 -0
- agentic_project_kit/next_turn_evidence.py +253 -0
- agentic_project_kit/next_turn_merge_if_green.py +459 -0
- agentic_project_kit/next_turn_pr_status.py +255 -0
- agentic_project_kit/next_turn_result.py +183 -0
- agentic_project_kit/next_turn_runner.py +159 -0
- agentic_project_kit/next_turn_slot.py +107 -0
- agentic_project_kit/next_turn_status.py +274 -0
- agentic_project_kit/ns_legacy_reference_audit.py +258 -0
- agentic_project_kit/ns_slice_runner.py +16 -0
- agentic_project_kit/operational_handoff_projection.py +124 -0
- agentic_project_kit/output_contract.py +80 -0
- agentic_project_kit/output_repair.py +61 -0
- agentic_project_kit/pass_already_done.py +216 -0
- agentic_project_kit/patch_artifact_preflight.py +147 -0
- agentic_project_kit/patch_cycle_workflow.py +440 -0
- agentic_project_kit/patch_failure_discipline_audit.py +223 -0
- agentic_project_kit/patch_preflight.py +215 -0
- agentic_project_kit/path_literal_audit.py +612 -0
- agentic_project_kit/patterns.py +99 -0
- agentic_project_kit/planning_docs_consolidation_audit.py +313 -0
- agentic_project_kit/portability_shell_gate.py +40 -0
- agentic_project_kit/post_merge_handoff_refresh.py +56 -0
- agentic_project_kit/post_release.py +252 -0
- agentic_project_kit/post_release_closeout.py +616 -0
- agentic_project_kit/pr_cleanup.py +172 -0
- agentic_project_kit/pr_closeout.py +50 -0
- agentic_project_kit/pr_closeout_complete.py +344 -0
- agentic_project_kit/pr_create_or_skip.py +71 -0
- agentic_project_kit/pr_hygiene.py +210 -0
- agentic_project_kit/program_redundancy_audit.py +251 -0
- agentic_project_kit/project_direction.py +840 -0
- agentic_project_kit/protected_change_planner.py +194 -0
- agentic_project_kit/release.py +401 -0
- agentic_project_kit/release_doi_safety.py +24 -0
- agentic_project_kit/release_gate_core.py +174 -0
- agentic_project_kit/release_metadata_authority_gate.py +328 -0
- agentic_project_kit/release_metadata_prep.py +43 -0
- agentic_project_kit/release_notes.py +823 -0
- agentic_project_kit/release_prep_core.py +204 -0
- agentic_project_kit/release_prepare.py +369 -0
- agentic_project_kit/release_process_guardrails.py +247 -0
- agentic_project_kit/release_publish_core.py +114 -0
- agentic_project_kit/release_publish_orchestration.py +469 -0
- agentic_project_kit/release_state.py +552 -0
- agentic_project_kit/release_verify_core.py +175 -0
- agentic_project_kit/remote_branch_hygiene.py +223 -0
- agentic_project_kit/remote_next.py +89 -0
- agentic_project_kit/remote_next_closeout.py +101 -0
- agentic_project_kit/remote_next_order_contract.py +391 -0
- agentic_project_kit/removed_ns_commands.py +458 -0
- agentic_project_kit/removed_source_audit.py +225 -0
- agentic_project_kit/rendering.py +13 -0
- agentic_project_kit/repair_report.py +79 -0
- agentic_project_kit/repo_identity.py +59 -0
- agentic_project_kit/result_report_classifier.py +57 -0
- agentic_project_kit/rule_ack.py +105 -0
- agentic_project_kit/rule_preservation.py +58 -0
- agentic_project_kit/rule_refresh.py +174 -0
- agentic_project_kit/rule_registry_registration.py +322 -0
- agentic_project_kit/rule_registry_report.py +206 -0
- agentic_project_kit/rule_registry_validator.py +704 -0
- agentic_project_kit/rule_snapshot.py +98 -0
- agentic_project_kit/rule_source_validator.py +133 -0
- agentic_project_kit/run_summary.py +68 -0
- agentic_project_kit/run_summary_renderer.py +238 -0
- agentic_project_kit/runtime_validator.py +68 -0
- agentic_project_kit/safe_file_replace.py +35 -0
- agentic_project_kit/safe_patch.py +47 -0
- agentic_project_kit/safe_push.py +218 -0
- agentic_project_kit/safe_remove_diagnostic.py +51 -0
- agentic_project_kit/scaffold.py +69 -0
- agentic_project_kit/site_claims.py +444 -0
- agentic_project_kit/site_generator.py +1140 -0
- agentic_project_kit/slice_gate.py +225 -0
- agentic_project_kit/slice_runner.py +75 -0
- agentic_project_kit/standard_gates_audit_suite.py +167 -0
- agentic_project_kit/state_freshness.py +135 -0
- agentic_project_kit/status_current_state_audit.py +762 -0
- agentic_project_kit/successor_handoff_package.py +1763 -0
- agentic_project_kit/templates.py +292 -0
- agentic_project_kit/terminal_block_guard.py +166 -0
- agentic_project_kit/terminal_logging.py +275 -0
- agentic_project_kit/todo.py +97 -0
- agentic_project_kit/transfer_closeout.py +136 -0
- agentic_project_kit/transfer_continue.py +215 -0
- agentic_project_kit/transfer_local_runner.py +101 -0
- agentic_project_kit/transfer_operation_monitor.py +236 -0
- agentic_project_kit/transfer_post_merge_lifecycle.py +356 -0
- agentic_project_kit/transfer_post_merge_settle.py +383 -0
- agentic_project_kit/transfer_pr_actions.py +164 -0
- agentic_project_kit/transfer_remote_next.py +959 -0
- agentic_project_kit/transfer_repo_actions.py +2253 -0
- agentic_project_kit/transfer_runner.py +347 -0
- agentic_project_kit/transfer_safety_context.py +291 -0
- agentic_project_kit/transfer_state.py +483 -0
- agentic_project_kit/transfer_uplink.py +471 -0
- agentic_project_kit/transfer_workflow_next.py +148 -0
- agentic_project_kit/typed_work_order_evidence.py +58 -0
- agentic_project_kit/typed_work_order_queue.py +166 -0
- agentic_project_kit/typed_work_order_runner.py +168 -0
- agentic_project_kit/work_cycle.py +317 -0
- agentic_project_kit/work_discard_changes.py +129 -0
- agentic_project_kit/work_order_patch.py +199 -0
- agentic_project_kit/work_order_runner.py +120 -0
- agentic_project_kit/work_order_uploader.py +220 -0
- agentic_project_kit/work_order_validator.py +185 -0
- agentic_project_kit/work_orders.py +246 -0
- agentic_project_kit/workflow_guard.py +262 -0
- agentic_project_kit/workflow_summary_runner.py +124 -0
- agentic_project_kit/workspace.py +665 -0
- agentic_project_kit/workspace_adopt.py +471 -0
- agentic_project_kit/workspace_dpa_intake.py +406 -0
- agentic_project_kit/workspace_init.py +487 -0
- agentic_project_kit/workspace_lock.py +129 -0
- agentic_project_kit/workspace_remove.py +381 -0
- agentic_project_kit/workspace_upgrade.py +324 -0
- agentic_project_kit/wrapper_live_status.py +104 -0
- agentic_project_kit-1.0.1.dist-info/METADATA +724 -0
- agentic_project_kit-1.0.1.dist-info/RECORD +310 -0
- agentic_project_kit-1.0.1.dist-info/WHEEL +4 -0
- agentic_project_kit-1.0.1.dist-info/entry_points.txt +3 -0
- agentic_project_kit-1.0.1.dist-info/licenses/LICENSE +10 -0
|
@@ -0,0 +1,255 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import argparse
|
|
4
|
+
from collections.abc import Callable
|
|
5
|
+
from dataclasses import asdict
|
|
6
|
+
import json
|
|
7
|
+
import re
|
|
8
|
+
import subprocess
|
|
9
|
+
from dataclasses import dataclass, replace
|
|
10
|
+
from typing import Any, Literal
|
|
11
|
+
|
|
12
|
+
Decision = Literal["green", "red", "pending", "no-checks", "unknown", "not-open"]
|
|
13
|
+
FailedLogStatus = Literal["not-fetched", "fetched", "unavailable", "missing-run-id"]
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
@dataclass(frozen=True)
|
|
17
|
+
class FailedRunDiagnostic:
|
|
18
|
+
check_name: str
|
|
19
|
+
conclusion: str
|
|
20
|
+
run_id: str
|
|
21
|
+
details_url: str
|
|
22
|
+
command: str
|
|
23
|
+
log_status: FailedLogStatus = "not-fetched"
|
|
24
|
+
log_excerpt: str = ""
|
|
25
|
+
error: str = ""
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
@dataclass(frozen=True)
|
|
29
|
+
class PrStatusDecision:
|
|
30
|
+
pr: str
|
|
31
|
+
state: str
|
|
32
|
+
merge_state_status: str
|
|
33
|
+
head_ref_oid: str
|
|
34
|
+
decision: Decision
|
|
35
|
+
successful_checks: tuple[str, ...]
|
|
36
|
+
pending_checks: tuple[str, ...]
|
|
37
|
+
failed_checks: tuple[str, ...]
|
|
38
|
+
unknown_checks: tuple[str, ...]
|
|
39
|
+
failed_run_log_hint: str
|
|
40
|
+
failed_run_diagnostics: tuple[FailedRunDiagnostic, ...]
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def _check_name(item: dict[str, Any]) -> str:
|
|
44
|
+
return str(item.get("name") or item.get("workflowName") or item.get("__typename") or "unknown")
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def _run_id_from_details_url(details_url: str) -> str:
|
|
48
|
+
match = re.search(r"/actions/runs/([0-9]+)(?:/|$)", details_url)
|
|
49
|
+
return match.group(1) if match else ""
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def _failed_run_diagnostic(item: dict[str, Any], *, name: str, conclusion: str) -> FailedRunDiagnostic:
|
|
53
|
+
details_url = str(item.get("detailsUrl") or item.get("details_url") or "")
|
|
54
|
+
run_id = _run_id_from_details_url(details_url)
|
|
55
|
+
command = f"gh run view {run_id} --log-failed" if run_id else ""
|
|
56
|
+
return FailedRunDiagnostic(
|
|
57
|
+
check_name=name,
|
|
58
|
+
conclusion=conclusion,
|
|
59
|
+
run_id=run_id,
|
|
60
|
+
details_url=details_url,
|
|
61
|
+
command=command,
|
|
62
|
+
)
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
def classify_pr_status(payload: dict[str, Any], *, pr: str = "") -> PrStatusDecision:
|
|
66
|
+
state = str(payload.get("state") or "UNKNOWN")
|
|
67
|
+
checks = payload.get("statusCheckRollup") or []
|
|
68
|
+
successful: list[str] = []
|
|
69
|
+
pending: list[str] = []
|
|
70
|
+
failed: list[str] = []
|
|
71
|
+
unknown: list[str] = []
|
|
72
|
+
failed_diagnostics: list[FailedRunDiagnostic] = []
|
|
73
|
+
|
|
74
|
+
if state != "OPEN":
|
|
75
|
+
decision: Decision = "not-open"
|
|
76
|
+
elif not isinstance(checks, list) or not checks:
|
|
77
|
+
decision = "no-checks"
|
|
78
|
+
else:
|
|
79
|
+
for item in checks:
|
|
80
|
+
if not isinstance(item, dict):
|
|
81
|
+
unknown.append("unknown")
|
|
82
|
+
continue
|
|
83
|
+
name = _check_name(item)
|
|
84
|
+
status = str(item.get("status") or "").upper()
|
|
85
|
+
conclusion = str(item.get("conclusion") or "").upper()
|
|
86
|
+
if status != "COMPLETED":
|
|
87
|
+
pending.append(name)
|
|
88
|
+
elif conclusion == "SUCCESS":
|
|
89
|
+
successful.append(name)
|
|
90
|
+
elif conclusion in {"FAILURE", "CANCELLED", "TIMED_OUT", "ACTION_REQUIRED"}:
|
|
91
|
+
failed.append(name)
|
|
92
|
+
failed_diagnostics.append(_failed_run_diagnostic(item, name=name, conclusion=conclusion))
|
|
93
|
+
else:
|
|
94
|
+
unknown.append(name)
|
|
95
|
+
|
|
96
|
+
if failed:
|
|
97
|
+
decision = "red"
|
|
98
|
+
elif pending:
|
|
99
|
+
decision = "pending"
|
|
100
|
+
elif unknown:
|
|
101
|
+
decision = "unknown"
|
|
102
|
+
else:
|
|
103
|
+
decision = "green"
|
|
104
|
+
|
|
105
|
+
hint = "none"
|
|
106
|
+
if decision == "red":
|
|
107
|
+
commands = [diagnostic.command for diagnostic in failed_diagnostics if diagnostic.command]
|
|
108
|
+
hint = f"run: {commands[0]}" if commands else "run: gh run view <failed-run-id> --log-failed"
|
|
109
|
+
|
|
110
|
+
return PrStatusDecision(
|
|
111
|
+
pr=pr,
|
|
112
|
+
state=state,
|
|
113
|
+
merge_state_status=str(payload.get("mergeStateStatus") or "UNKNOWN"),
|
|
114
|
+
head_ref_oid=str(payload.get("headRefOid") or ""),
|
|
115
|
+
decision=decision,
|
|
116
|
+
successful_checks=tuple(successful),
|
|
117
|
+
pending_checks=tuple(pending),
|
|
118
|
+
failed_checks=tuple(failed),
|
|
119
|
+
unknown_checks=tuple(unknown),
|
|
120
|
+
failed_run_log_hint=hint,
|
|
121
|
+
failed_run_diagnostics=tuple(failed_diagnostics),
|
|
122
|
+
)
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
def _excerpt(text: str, *, max_lines: int) -> str:
|
|
126
|
+
lines = text.strip().splitlines()
|
|
127
|
+
if max_lines <= 0 or len(lines) <= max_lines:
|
|
128
|
+
return "\n".join(lines)
|
|
129
|
+
omitted = len(lines) - max_lines
|
|
130
|
+
return "\n".join([*lines[:max_lines], f"... ({omitted} lines omitted)"])
|
|
131
|
+
|
|
132
|
+
|
|
133
|
+
def _fetch_failed_run_log(run_id: str) -> tuple[int, str, str]:
|
|
134
|
+
completed = subprocess.run(["gh", "run", "view", run_id, "--log-failed"], text=True, capture_output=True, check=False)
|
|
135
|
+
return completed.returncode, completed.stdout, completed.stderr
|
|
136
|
+
|
|
137
|
+
|
|
138
|
+
def attach_failed_run_logs(
|
|
139
|
+
decision: PrStatusDecision,
|
|
140
|
+
*,
|
|
141
|
+
max_lines: int = 120,
|
|
142
|
+
fetcher: Callable[[str], tuple[int, str, str]] = _fetch_failed_run_log,
|
|
143
|
+
) -> PrStatusDecision:
|
|
144
|
+
diagnostics: list[FailedRunDiagnostic] = []
|
|
145
|
+
for diagnostic in decision.failed_run_diagnostics:
|
|
146
|
+
if not diagnostic.run_id:
|
|
147
|
+
diagnostics.append(
|
|
148
|
+
replace(
|
|
149
|
+
diagnostic,
|
|
150
|
+
log_status="missing-run-id",
|
|
151
|
+
error="failed check detailsUrl did not contain a GitHub Actions run id",
|
|
152
|
+
)
|
|
153
|
+
)
|
|
154
|
+
continue
|
|
155
|
+
returncode, stdout, stderr = fetcher(diagnostic.run_id)
|
|
156
|
+
if returncode == 0:
|
|
157
|
+
diagnostics.append(replace(diagnostic, log_status="fetched", log_excerpt=_excerpt(stdout, max_lines=max_lines)))
|
|
158
|
+
continue
|
|
159
|
+
diagnostics.append(
|
|
160
|
+
replace(
|
|
161
|
+
diagnostic,
|
|
162
|
+
log_status="unavailable",
|
|
163
|
+
log_excerpt=_excerpt(stdout, max_lines=max_lines),
|
|
164
|
+
error=(stderr.strip() or stdout.strip() or f"gh run view exited with {returncode}"),
|
|
165
|
+
)
|
|
166
|
+
)
|
|
167
|
+
return replace(decision, failed_run_diagnostics=tuple(diagnostics))
|
|
168
|
+
|
|
169
|
+
|
|
170
|
+
def _render_indented_block(text: str, *, indent: str = " ") -> list[str]:
|
|
171
|
+
if not text:
|
|
172
|
+
return [f"{indent}(none)"]
|
|
173
|
+
return [f"{indent}{line}" for line in text.splitlines()]
|
|
174
|
+
|
|
175
|
+
|
|
176
|
+
def render_decision(decision: PrStatusDecision) -> str:
|
|
177
|
+
lines = [
|
|
178
|
+
"NEXT_TURN_PR_STATUS",
|
|
179
|
+
f"pr={decision.pr}",
|
|
180
|
+
f"state={decision.state}",
|
|
181
|
+
f"merge_state_status={decision.merge_state_status}",
|
|
182
|
+
f"head_ref_oid={decision.head_ref_oid}",
|
|
183
|
+
f"decision={decision.decision}",
|
|
184
|
+
"successful_checks:",
|
|
185
|
+
*[f"- {item}" for item in decision.successful_checks],
|
|
186
|
+
"pending_checks:",
|
|
187
|
+
*[f"- {item}" for item in decision.pending_checks],
|
|
188
|
+
"failed_checks:",
|
|
189
|
+
*[f"- {item}" for item in decision.failed_checks],
|
|
190
|
+
"unknown_checks:",
|
|
191
|
+
*[f"- {item}" for item in decision.unknown_checks],
|
|
192
|
+
f"failed_run_log_hint={decision.failed_run_log_hint}",
|
|
193
|
+
"failed_run_diagnostics:",
|
|
194
|
+
]
|
|
195
|
+
for diagnostic in decision.failed_run_diagnostics:
|
|
196
|
+
lines.extend(
|
|
197
|
+
[
|
|
198
|
+
(
|
|
199
|
+
f"- check={diagnostic.check_name} conclusion={diagnostic.conclusion} "
|
|
200
|
+
f"run_id={diagnostic.run_id or '(missing)'} log_status={diagnostic.log_status}"
|
|
201
|
+
),
|
|
202
|
+
f" command={diagnostic.command or '(unavailable)'}",
|
|
203
|
+
f" details_url={diagnostic.details_url or '(missing)'}",
|
|
204
|
+
]
|
|
205
|
+
)
|
|
206
|
+
if diagnostic.error:
|
|
207
|
+
lines.append(f" error={diagnostic.error}")
|
|
208
|
+
lines.append(" log_excerpt:")
|
|
209
|
+
lines.extend(_render_indented_block(diagnostic.log_excerpt, indent=" "))
|
|
210
|
+
lines.append("### RESULT: PASS ###")
|
|
211
|
+
return "\n".join(lines)
|
|
212
|
+
|
|
213
|
+
|
|
214
|
+
def _run_gh(args: list[str]) -> str:
|
|
215
|
+
completed = subprocess.run(["gh", *args], text=True, capture_output=True, check=False)
|
|
216
|
+
if completed.returncode != 0:
|
|
217
|
+
raise RuntimeError(completed.stderr.strip() or completed.stdout.strip())
|
|
218
|
+
return completed.stdout
|
|
219
|
+
|
|
220
|
+
|
|
221
|
+
def fetch_pr_payload(pr: str) -> dict[str, Any]:
|
|
222
|
+
raw = _run_gh([
|
|
223
|
+
"pr",
|
|
224
|
+
"view",
|
|
225
|
+
pr,
|
|
226
|
+
"--json",
|
|
227
|
+
"baseRefName,baseRefOid,headRefName,headRefOid,state,mergeStateStatus,statusCheckRollup,url",
|
|
228
|
+
])
|
|
229
|
+
payload = json.loads(raw)
|
|
230
|
+
if not isinstance(payload, dict):
|
|
231
|
+
raise RuntimeError("gh pr view did not return a JSON object")
|
|
232
|
+
return payload
|
|
233
|
+
|
|
234
|
+
|
|
235
|
+
def main() -> int:
|
|
236
|
+
parser = argparse.ArgumentParser(prog="next-turn-pr-status")
|
|
237
|
+
parser.add_argument("pr")
|
|
238
|
+
parser.add_argument("--json", action="store_true")
|
|
239
|
+
parser.add_argument("--no-failed-log-fetch", action="store_true")
|
|
240
|
+
parser.add_argument("--failed-log-lines", type=int, default=120)
|
|
241
|
+
args = parser.parse_args()
|
|
242
|
+
|
|
243
|
+
payload = fetch_pr_payload(args.pr)
|
|
244
|
+
decision = classify_pr_status(payload, pr=args.pr)
|
|
245
|
+
if decision.decision == "red" and not args.no_failed_log_fetch:
|
|
246
|
+
decision = attach_failed_run_logs(decision, max_lines=args.failed_log_lines)
|
|
247
|
+
if args.json:
|
|
248
|
+
print(json.dumps(asdict(decision), indent=2, sort_keys=True))
|
|
249
|
+
else:
|
|
250
|
+
print(render_decision(decision))
|
|
251
|
+
return 0
|
|
252
|
+
|
|
253
|
+
|
|
254
|
+
if __name__ == "__main__":
|
|
255
|
+
raise SystemExit(main())
|
|
@@ -0,0 +1,183 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import argparse
|
|
4
|
+
import json
|
|
5
|
+
from dataclasses import asdict, dataclass
|
|
6
|
+
from datetime import UTC, datetime
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
from typing import Literal
|
|
9
|
+
|
|
10
|
+
WorkResult = Literal["PASS", "FAIL", "PENDING", "HARD_FAIL", "PASS_ALREADY_DONE", "NOOP"]
|
|
11
|
+
EvidenceResult = Literal["PASS", "FAIL", "PARTIAL", "CHAT_ONLY", "NOT_REQUIRED", "LOCAL_LOG"]
|
|
12
|
+
RemoteEvidence = Literal["PASS", "FAIL", "PARTIAL", "NOT_REQUIRED"]
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
@dataclass(frozen=True)
|
|
16
|
+
class NextTurnResult:
|
|
17
|
+
run_id: str
|
|
18
|
+
work_result: WorkResult
|
|
19
|
+
evidence_result: EvidenceResult
|
|
20
|
+
overall_result: WorkResult
|
|
21
|
+
remote_evidence: RemoteEvidence
|
|
22
|
+
terminal_log: str
|
|
23
|
+
remote_terminal_log: str
|
|
24
|
+
command_report: str
|
|
25
|
+
next_chat_reply: str
|
|
26
|
+
timestamp_utc: str
|
|
27
|
+
reason: str = ""
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def classify_next_chat_reply(overall_result: str, remote_evidence: str) -> str:
|
|
31
|
+
if overall_result in {"PASS", "PASS_ALREADY_DONE", "NOOP"}:
|
|
32
|
+
return "d"
|
|
33
|
+
if remote_evidence in {"PASS", "PARTIAL"}:
|
|
34
|
+
return "f"
|
|
35
|
+
return "paste-output"
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def build_result(
|
|
39
|
+
*,
|
|
40
|
+
run_id: str,
|
|
41
|
+
work_result: WorkResult,
|
|
42
|
+
evidence_result: EvidenceResult,
|
|
43
|
+
remote_evidence: RemoteEvidence,
|
|
44
|
+
terminal_log: str = "NONE",
|
|
45
|
+
remote_terminal_log: str = "NONE",
|
|
46
|
+
command_report: str = "NONE",
|
|
47
|
+
reason: str = "",
|
|
48
|
+
) -> NextTurnResult:
|
|
49
|
+
next_chat_reply = classify_next_chat_reply(work_result, remote_evidence)
|
|
50
|
+
return NextTurnResult(
|
|
51
|
+
run_id=run_id,
|
|
52
|
+
work_result=work_result,
|
|
53
|
+
evidence_result=evidence_result,
|
|
54
|
+
overall_result=work_result,
|
|
55
|
+
remote_evidence=remote_evidence,
|
|
56
|
+
terminal_log=terminal_log,
|
|
57
|
+
remote_terminal_log=remote_terminal_log,
|
|
58
|
+
command_report=command_report,
|
|
59
|
+
next_chat_reply=next_chat_reply,
|
|
60
|
+
timestamp_utc=datetime.now(UTC).isoformat(),
|
|
61
|
+
reason=reason,
|
|
62
|
+
)
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
def result_paths(run_id: str, root: Path | str = ".") -> tuple[Path, Path, Path]:
|
|
66
|
+
root_path = Path(root)
|
|
67
|
+
report = root_path / "docs" / "reports" / "command_runs" / f"{run_id}.json"
|
|
68
|
+
latest = root_path / "docs" / "reports" / "command_runs" / "next-turn-latest.json"
|
|
69
|
+
terminal = root_path / "docs" / "reports" / "terminal" / f"{run_id}.log"
|
|
70
|
+
return report, latest, terminal
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
def write_result(result: NextTurnResult, root: Path | str = ".") -> tuple[Path, Path]:
|
|
74
|
+
report, latest, _terminal = result_paths(result.run_id, root)
|
|
75
|
+
report.parent.mkdir(parents=True, exist_ok=True)
|
|
76
|
+
payload = asdict(result)
|
|
77
|
+
report.write_text(json.dumps(payload, indent=2, sort_keys=True) + "\n", encoding="utf-8")
|
|
78
|
+
latest.write_text(json.dumps(payload, indent=2, sort_keys=True) + "\n", encoding="utf-8")
|
|
79
|
+
return report, latest
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
def publish_local_evidence(
|
|
84
|
+
*,
|
|
85
|
+
run_id: str,
|
|
86
|
+
local_terminal_log: Path | str,
|
|
87
|
+
work_result: WorkResult,
|
|
88
|
+
evidence_result: EvidenceResult = "PASS",
|
|
89
|
+
reason: str = "",
|
|
90
|
+
root: Path | str = ".",
|
|
91
|
+
) -> NextTurnResult:
|
|
92
|
+
root_path = Path(root)
|
|
93
|
+
source = Path(local_terminal_log)
|
|
94
|
+
_report, _latest, remote_terminal = result_paths(run_id, root_path)
|
|
95
|
+
remote_terminal.parent.mkdir(parents=True, exist_ok=True)
|
|
96
|
+
|
|
97
|
+
if not source.exists():
|
|
98
|
+
result = build_result(
|
|
99
|
+
run_id=run_id,
|
|
100
|
+
work_result=work_result,
|
|
101
|
+
evidence_result="FAIL",
|
|
102
|
+
remote_evidence="FAIL",
|
|
103
|
+
terminal_log=str(source),
|
|
104
|
+
remote_terminal_log=str(remote_terminal.relative_to(root_path)),
|
|
105
|
+
reason=f"local terminal log not found: {source}",
|
|
106
|
+
)
|
|
107
|
+
write_result(result, root_path)
|
|
108
|
+
return result
|
|
109
|
+
|
|
110
|
+
remote_terminal.write_text(source.read_text(encoding="utf-8", errors="replace"), encoding="utf-8")
|
|
111
|
+
|
|
112
|
+
result = build_result(
|
|
113
|
+
run_id=run_id,
|
|
114
|
+
work_result=work_result,
|
|
115
|
+
evidence_result=evidence_result,
|
|
116
|
+
remote_evidence="PARTIAL",
|
|
117
|
+
terminal_log=str(source),
|
|
118
|
+
remote_terminal_log=str(remote_terminal.relative_to(root_path)),
|
|
119
|
+
command_report=str((root_path / "docs" / "reports" / "command_runs" / f"{run_id}.json").relative_to(root_path)),
|
|
120
|
+
reason=reason,
|
|
121
|
+
)
|
|
122
|
+
write_result(result, root_path)
|
|
123
|
+
return result
|
|
124
|
+
|
|
125
|
+
def render_summary(result: NextTurnResult) -> str:
|
|
126
|
+
return "\n".join(
|
|
127
|
+
[
|
|
128
|
+
"================================================================",
|
|
129
|
+
"SUMMARY",
|
|
130
|
+
f"WORK RESULT: {result.work_result}",
|
|
131
|
+
f"EVIDENCE RESULT: {result.evidence_result}",
|
|
132
|
+
f"OVERALL RESULT: {result.overall_result}",
|
|
133
|
+
f"REMOTE_EVIDENCE: {result.remote_evidence}",
|
|
134
|
+
f"terminal_log={result.terminal_log}",
|
|
135
|
+
f"remote_terminal_log={result.remote_terminal_log}",
|
|
136
|
+
f"command_report={result.command_report}",
|
|
137
|
+
f"NEXT_CHAT_REPLY: {result.next_chat_reply}",
|
|
138
|
+
f"CHAT_REPLY: {result.next_chat_reply}",
|
|
139
|
+
f"### RESULT: {result.overall_result} ###",
|
|
140
|
+
"================================================================",
|
|
141
|
+
]
|
|
142
|
+
)
|
|
143
|
+
|
|
144
|
+
|
|
145
|
+
def main() -> int:
|
|
146
|
+
parser = argparse.ArgumentParser(prog="next-turn-result")
|
|
147
|
+
parser.add_argument("--run-id", required=True)
|
|
148
|
+
parser.add_argument("--work-result", required=True, choices=["PASS", "FAIL", "PENDING", "HARD_FAIL", "PASS_ALREADY_DONE", "NOOP"])
|
|
149
|
+
parser.add_argument("--evidence-result", default="LOCAL_LOG")
|
|
150
|
+
parser.add_argument("--remote-evidence", default="NOT_REQUIRED")
|
|
151
|
+
parser.add_argument("--terminal-log", default="NONE")
|
|
152
|
+
parser.add_argument("--remote-terminal-log", default="NONE")
|
|
153
|
+
parser.add_argument("--command-report", default="NONE")
|
|
154
|
+
parser.add_argument("--reason", default="")
|
|
155
|
+
parser.add_argument("--publish-local-log", default="")
|
|
156
|
+
args = parser.parse_args()
|
|
157
|
+
|
|
158
|
+
if args.publish_local_log:
|
|
159
|
+
result = publish_local_evidence(
|
|
160
|
+
run_id=args.run_id,
|
|
161
|
+
local_terminal_log=args.publish_local_log,
|
|
162
|
+
work_result=args.work_result,
|
|
163
|
+
evidence_result=args.evidence_result,
|
|
164
|
+
reason=args.reason,
|
|
165
|
+
)
|
|
166
|
+
else:
|
|
167
|
+
result = build_result(
|
|
168
|
+
run_id=args.run_id,
|
|
169
|
+
work_result=args.work_result,
|
|
170
|
+
evidence_result=args.evidence_result,
|
|
171
|
+
remote_evidence=args.remote_evidence,
|
|
172
|
+
terminal_log=args.terminal_log,
|
|
173
|
+
remote_terminal_log=args.remote_terminal_log,
|
|
174
|
+
command_report=args.command_report,
|
|
175
|
+
reason=args.reason,
|
|
176
|
+
)
|
|
177
|
+
write_result(result)
|
|
178
|
+
print(render_summary(result))
|
|
179
|
+
return 0
|
|
180
|
+
|
|
181
|
+
|
|
182
|
+
if __name__ == "__main__":
|
|
183
|
+
raise SystemExit(main())
|
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import argparse
|
|
4
|
+
import json
|
|
5
|
+
import subprocess
|
|
6
|
+
import sys
|
|
7
|
+
from dataclasses import asdict, dataclass
|
|
8
|
+
from datetime import datetime, timezone
|
|
9
|
+
from pathlib import Path
|
|
10
|
+
import os
|
|
11
|
+
|
|
12
|
+
from agentic_project_kit.next_turn_evidence import commit_and_push_evidence, publish_and_stage_evidence, render_finalize_result
|
|
13
|
+
from agentic_project_kit.next_turn_slot import FIXED_SLOT_SCRIPT, FIXED_SLOT_YAML
|
|
14
|
+
from agentic_project_kit.work_order_validator import default_local_result_log_path
|
|
15
|
+
from agentic_project_kit.workspace import LEGACY_DEFAULTS, load_workspace
|
|
16
|
+
|
|
17
|
+
LATEST_TERMINAL_LOG = Path(
|
|
18
|
+
os.environ.get(
|
|
19
|
+
"AGENTIC_KIT_NEXT_TURN_LOG",
|
|
20
|
+
(Path(LEGACY_DEFAULTS.tmp_root) / "next-turn-latest.log").as_posix(),
|
|
21
|
+
)
|
|
22
|
+
)
|
|
23
|
+
LATEST_COMMAND_REPORT = Path(
|
|
24
|
+
os.environ.get(
|
|
25
|
+
"AGENTIC_KIT_NEXT_TURN_REPORT",
|
|
26
|
+
(Path(LEGACY_DEFAULTS.tmp_root) / "next-turn-latest.json").as_posix(),
|
|
27
|
+
)
|
|
28
|
+
)
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
@dataclass(frozen=True)
|
|
32
|
+
class NextTurnRunResult:
|
|
33
|
+
command_id: str
|
|
34
|
+
outcome: str
|
|
35
|
+
exit_code: int
|
|
36
|
+
script_path: str
|
|
37
|
+
yaml_path: str
|
|
38
|
+
terminal_log: str
|
|
39
|
+
command_report: str
|
|
40
|
+
timestamp_utc: str
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def _command_id_from_yaml(path: Path) -> str:
|
|
44
|
+
if not path.exists():
|
|
45
|
+
return "next-turn"
|
|
46
|
+
for line in path.read_text(encoding="utf-8").splitlines():
|
|
47
|
+
if line.startswith("command_id:"):
|
|
48
|
+
return line.split(":", 1)[1].strip() or "next-turn"
|
|
49
|
+
return "next-turn"
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def run_fixed_slot(root: Path | str = ".") -> NextTurnRunResult:
|
|
53
|
+
root_path = Path(root)
|
|
54
|
+
yaml_path = root_path / FIXED_SLOT_YAML
|
|
55
|
+
script_path = root_path / FIXED_SLOT_SCRIPT
|
|
56
|
+
terminal_log = (
|
|
57
|
+
root_path / LATEST_TERMINAL_LOG
|
|
58
|
+
if LATEST_TERMINAL_LOG != Path(LEGACY_DEFAULTS.tmp_root) / "next-turn-latest.log"
|
|
59
|
+
else default_local_result_log_path(root_path)
|
|
60
|
+
)
|
|
61
|
+
command_report = (
|
|
62
|
+
root_path / LATEST_COMMAND_REPORT
|
|
63
|
+
if LATEST_COMMAND_REPORT != Path(LEGACY_DEFAULTS.tmp_root) / "next-turn-latest.json"
|
|
64
|
+
else load_workspace(root_path).tmp_file("next-turn-latest.json")
|
|
65
|
+
)
|
|
66
|
+
|
|
67
|
+
terminal_log.parent.mkdir(parents=True, exist_ok=True)
|
|
68
|
+
command_report.parent.mkdir(parents=True, exist_ok=True)
|
|
69
|
+
|
|
70
|
+
command_id = _command_id_from_yaml(yaml_path)
|
|
71
|
+
|
|
72
|
+
if not yaml_path.exists() or not script_path.exists():
|
|
73
|
+
result = NextTurnRunResult(
|
|
74
|
+
command_id=command_id,
|
|
75
|
+
outcome="FAIL_NO_FIXED_SLOT",
|
|
76
|
+
exit_code=2,
|
|
77
|
+
script_path=str(FIXED_SLOT_SCRIPT),
|
|
78
|
+
yaml_path=str(FIXED_SLOT_YAML),
|
|
79
|
+
terminal_log=str(terminal_log),
|
|
80
|
+
command_report=str(command_report),
|
|
81
|
+
timestamp_utc=datetime.now(timezone.utc).isoformat(),
|
|
82
|
+
)
|
|
83
|
+
terminal_log.write_text("NEXT_TURN_RUNNER\noutcome=FAIL_NO_FIXED_SLOT\n### RESULT: FAIL ###\n", encoding="utf-8")
|
|
84
|
+
command_report.write_text(json.dumps(asdict(result), indent=2, sort_keys=True) + "\n", encoding="utf-8")
|
|
85
|
+
return result
|
|
86
|
+
|
|
87
|
+
completed = subprocess.run(
|
|
88
|
+
[sys.executable, str(script_path)],
|
|
89
|
+
cwd=root_path,
|
|
90
|
+
text=True,
|
|
91
|
+
capture_output=True,
|
|
92
|
+
check=False,
|
|
93
|
+
)
|
|
94
|
+
output = (completed.stdout or "") + (completed.stderr or "")
|
|
95
|
+
terminal_log.write_text(output, encoding="utf-8")
|
|
96
|
+
|
|
97
|
+
outcome = "PASS_EXECUTED" if completed.returncode == 0 else "FAIL_EXECUTED"
|
|
98
|
+
result = NextTurnRunResult(
|
|
99
|
+
command_id=command_id,
|
|
100
|
+
outcome=outcome,
|
|
101
|
+
exit_code=completed.returncode,
|
|
102
|
+
script_path=str(FIXED_SLOT_SCRIPT),
|
|
103
|
+
yaml_path=str(FIXED_SLOT_YAML),
|
|
104
|
+
terminal_log=str(terminal_log),
|
|
105
|
+
command_report=str(command_report),
|
|
106
|
+
timestamp_utc=datetime.now(timezone.utc).isoformat(),
|
|
107
|
+
)
|
|
108
|
+
command_report.write_text(json.dumps(asdict(result), indent=2, sort_keys=True) + "\n", encoding="utf-8")
|
|
109
|
+
if outcome == "PASS_EXECUTED":
|
|
110
|
+
yaml_path.unlink(missing_ok=True)
|
|
111
|
+
script_path.unlink(missing_ok=True)
|
|
112
|
+
return result
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
def render_result(result: NextTurnRunResult) -> str:
|
|
116
|
+
status = "PASS" if result.outcome == "PASS_EXECUTED" else "FAIL"
|
|
117
|
+
return "\n".join(
|
|
118
|
+
[
|
|
119
|
+
"NEXT_TURN_RUNNER",
|
|
120
|
+
f"command_id={result.command_id}",
|
|
121
|
+
f"outcome={result.outcome}",
|
|
122
|
+
f"exit_code={result.exit_code}",
|
|
123
|
+
f"terminal_log={result.terminal_log}",
|
|
124
|
+
f"command_report={result.command_report}",
|
|
125
|
+
f"subresult={status}",
|
|
126
|
+
]
|
|
127
|
+
)
|
|
128
|
+
|
|
129
|
+
|
|
130
|
+
def work_result_from_outcome(outcome: str) -> str:
|
|
131
|
+
return "PASS" if outcome == "PASS_EXECUTED" else "FAIL"
|
|
132
|
+
|
|
133
|
+
|
|
134
|
+
def publish_run_evidence(result: NextTurnRunResult, *, run_id: str, push: bool = True) -> str:
|
|
135
|
+
plan = publish_and_stage_evidence(
|
|
136
|
+
run_id=run_id,
|
|
137
|
+
local_terminal_log=result.terminal_log,
|
|
138
|
+
work_result=work_result_from_outcome(result.outcome),
|
|
139
|
+
)
|
|
140
|
+
finalize_result = commit_and_push_evidence(plan=plan, push=push)
|
|
141
|
+
return render_finalize_result(finalize_result)
|
|
142
|
+
|
|
143
|
+
|
|
144
|
+
def main() -> int:
|
|
145
|
+
parser = argparse.ArgumentParser(prog="next-turn-run")
|
|
146
|
+
parser.add_argument("--publish-evidence", action="store_true")
|
|
147
|
+
parser.add_argument("--run-id", default="")
|
|
148
|
+
parser.add_argument("--no-push", action="store_true")
|
|
149
|
+
args = parser.parse_args()
|
|
150
|
+
result = run_fixed_slot()
|
|
151
|
+
print(render_result(result))
|
|
152
|
+
if args.publish_evidence:
|
|
153
|
+
run_id = args.run_id or result.command_id
|
|
154
|
+
print(publish_run_evidence(result, run_id=run_id, push=not args.no_push))
|
|
155
|
+
return 0 if result.outcome == "PASS_EXECUTED" else 1
|
|
156
|
+
|
|
157
|
+
|
|
158
|
+
if __name__ == "__main__":
|
|
159
|
+
raise SystemExit(main())
|