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,421 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import re
|
|
4
|
+
from dataclasses import asdict, dataclass
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
from typing import Any, Mapping
|
|
7
|
+
|
|
8
|
+
LOCK_MARKER = "workspace_mutation_lock"
|
|
9
|
+
|
|
10
|
+
@dataclass(frozen=True)
|
|
11
|
+
class MutationLockFindingClassification:
|
|
12
|
+
"""Human-facing classification for mutation-lock audit findings."""
|
|
13
|
+
|
|
14
|
+
kind: str
|
|
15
|
+
counts_as_blocking: bool
|
|
16
|
+
rationale: str
|
|
17
|
+
|
|
18
|
+
def to_dict(self) -> dict[str, object]:
|
|
19
|
+
return {
|
|
20
|
+
"kind": self.kind,
|
|
21
|
+
"counts_as_blocking": self.counts_as_blocking,
|
|
22
|
+
"rationale": self.rationale,
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
MODULE_LEVEL_STRING_TABLE_PATHS = frozenset(
|
|
27
|
+
{
|
|
28
|
+
"src/agentic_project_kit/command_manifest.py",
|
|
29
|
+
"src/agentic_project_kit/work_order_validator.py",
|
|
30
|
+
"src/agentic_project_kit/command_inbox_check.py",
|
|
31
|
+
"src/agentic_project_kit/composite_short_circuit.py",
|
|
32
|
+
"src/agentic_project_kit/llm_execution_context.py",
|
|
33
|
+
}
|
|
34
|
+
)
|
|
35
|
+
|
|
36
|
+
METADATA_LITERAL_SYMBOLS = frozenset(
|
|
37
|
+
{
|
|
38
|
+
"_category_for",
|
|
39
|
+
"_composition_level_for",
|
|
40
|
+
"_task_tags",
|
|
41
|
+
"_button",
|
|
42
|
+
"_actions",
|
|
43
|
+
}
|
|
44
|
+
)
|
|
45
|
+
|
|
46
|
+
GENERATED_REFERENCE_SYMBOLS = frozenset(
|
|
47
|
+
{
|
|
48
|
+
"write_reference",
|
|
49
|
+
"render_markdown",
|
|
50
|
+
"commands_render_md_command",
|
|
51
|
+
"direction_render_command",
|
|
52
|
+
}
|
|
53
|
+
)
|
|
54
|
+
|
|
55
|
+
REPORT_WRITER_SYMBOL_HINTS = (
|
|
56
|
+
"write_",
|
|
57
|
+
"_write_",
|
|
58
|
+
"report",
|
|
59
|
+
"evidence",
|
|
60
|
+
"summary",
|
|
61
|
+
"log",
|
|
62
|
+
"finalize",
|
|
63
|
+
)
|
|
64
|
+
|
|
65
|
+
RUNTIME_MUTATION_SYMBOL_HINTS = (
|
|
66
|
+
"branch",
|
|
67
|
+
"commit",
|
|
68
|
+
"push",
|
|
69
|
+
"merge",
|
|
70
|
+
"restore",
|
|
71
|
+
"sync",
|
|
72
|
+
"pr_",
|
|
73
|
+
"release",
|
|
74
|
+
)
|
|
75
|
+
|
|
76
|
+
CORE_RUNTIME_MUTATOR_SYMBOLS = frozenset(
|
|
77
|
+
{
|
|
78
|
+
"admin_refresh_pr",
|
|
79
|
+
"_admin_refresh_pr_unlocked",
|
|
80
|
+
"branch_create",
|
|
81
|
+
"branch_delete",
|
|
82
|
+
"branch_switch",
|
|
83
|
+
"commit_paths",
|
|
84
|
+
"_commit_paths_unlocked",
|
|
85
|
+
"pr_create",
|
|
86
|
+
"_pr_create_unlocked",
|
|
87
|
+
"pr_merge_safe",
|
|
88
|
+
"_pr_merge_safe_unlocked",
|
|
89
|
+
"pull_current",
|
|
90
|
+
"push_current",
|
|
91
|
+
"_push_current_unlocked",
|
|
92
|
+
}
|
|
93
|
+
)
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
def _finding_value(finding: object, key: str) -> str:
|
|
97
|
+
if isinstance(finding, Mapping):
|
|
98
|
+
return str(finding.get(key) or "")
|
|
99
|
+
return str(getattr(finding, key, "") or "")
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
def _is_enforced_core_runtime_path(path: str) -> bool:
|
|
103
|
+
return path == "src/agentic_project_kit/transfer_repo_actions.py"
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
def classify_mutation_lock_finding(finding: object) -> MutationLockFindingClassification:
|
|
107
|
+
"""Classify a mutation-lock audit finding without changing audit behavior."""
|
|
108
|
+
|
|
109
|
+
path = _finding_value(finding, "path")
|
|
110
|
+
symbol = _finding_value(finding, "symbol")
|
|
111
|
+
category = _finding_value(finding, "category")
|
|
112
|
+
reason = _finding_value(finding, "reason")
|
|
113
|
+
|
|
114
|
+
symbol_l = symbol.lower()
|
|
115
|
+
|
|
116
|
+
if symbol == "<module>" and path in MODULE_LEVEL_STRING_TABLE_PATHS:
|
|
117
|
+
return MutationLockFindingClassification(
|
|
118
|
+
kind="metadata_literal",
|
|
119
|
+
counts_as_blocking=False,
|
|
120
|
+
rationale="module-level command metadata is audit vocabulary, not executed mutation",
|
|
121
|
+
)
|
|
122
|
+
|
|
123
|
+
if symbol in METADATA_LITERAL_SYMBOLS:
|
|
124
|
+
return MutationLockFindingClassification(
|
|
125
|
+
kind="metadata_literal",
|
|
126
|
+
counts_as_blocking=False,
|
|
127
|
+
rationale="symbol describes command/category metadata rather than executing a mutation",
|
|
128
|
+
)
|
|
129
|
+
|
|
130
|
+
if symbol in GENERATED_REFERENCE_SYMBOLS:
|
|
131
|
+
return MutationLockFindingClassification(
|
|
132
|
+
kind="generated_reference",
|
|
133
|
+
counts_as_blocking=False,
|
|
134
|
+
rationale="symbol writes generated command/reference material, not a workspace mutation target",
|
|
135
|
+
)
|
|
136
|
+
|
|
137
|
+
if category == "C" or "filesystem_" in reason:
|
|
138
|
+
if any(hint in symbol_l for hint in REPORT_WRITER_SYMBOL_HINTS):
|
|
139
|
+
return MutationLockFindingClassification(
|
|
140
|
+
kind="report_writer",
|
|
141
|
+
counts_as_blocking=False,
|
|
142
|
+
rationale="filesystem mutation writes report, evidence, summary, or log output",
|
|
143
|
+
)
|
|
144
|
+
return MutationLockFindingClassification(
|
|
145
|
+
kind="filesystem_side_effect",
|
|
146
|
+
counts_as_blocking=False,
|
|
147
|
+
rationale="filesystem side-effect remains visible for review; LC3 hard blocking is limited to runtime git/github mutators",
|
|
148
|
+
)
|
|
149
|
+
|
|
150
|
+
if category in {"A", "B"} or "git_" in reason or "github_" in reason:
|
|
151
|
+
if _is_enforced_core_runtime_path(path) and symbol in CORE_RUNTIME_MUTATOR_SYMBOLS:
|
|
152
|
+
return MutationLockFindingClassification(
|
|
153
|
+
kind="runtime_mutation",
|
|
154
|
+
counts_as_blocking=True,
|
|
155
|
+
rationale="finding is in a runtime git/github mutation path",
|
|
156
|
+
)
|
|
157
|
+
if any(hint in symbol_l for hint in RUNTIME_MUTATION_SYMBOL_HINTS):
|
|
158
|
+
return MutationLockFindingClassification(
|
|
159
|
+
kind="delegated_runtime_reference",
|
|
160
|
+
counts_as_blocking=False,
|
|
161
|
+
rationale="runtime-like command text is outside the LC3 enforced core mutator body or delegates to locked core mutators",
|
|
162
|
+
)
|
|
163
|
+
|
|
164
|
+
return MutationLockFindingClassification(
|
|
165
|
+
kind="review_visible_reference",
|
|
166
|
+
counts_as_blocking=False,
|
|
167
|
+
rationale="finding remains visible for review but does not match an enforced LC3 runtime mutator",
|
|
168
|
+
)
|
|
169
|
+
|
|
170
|
+
ENFORCED_CORE_MUTATION_LOCK_PATHS = (
|
|
171
|
+
"transfer_repo_actions.py",
|
|
172
|
+
"transfer_pr_create_flow.py",
|
|
173
|
+
"transfer_pr_merge_flow.py",
|
|
174
|
+
)
|
|
175
|
+
|
|
176
|
+
|
|
177
|
+
@dataclass(frozen=True)
|
|
178
|
+
class MutationLockFinding:
|
|
179
|
+
category: str
|
|
180
|
+
symbol: str
|
|
181
|
+
path: str
|
|
182
|
+
line: int
|
|
183
|
+
reason: str
|
|
184
|
+
required_lock: str = "workspace_mutation_lock"
|
|
185
|
+
|
|
186
|
+
|
|
187
|
+
@dataclass(frozen=True)
|
|
188
|
+
class MutationLockCoverageAuditResult:
|
|
189
|
+
kind: str
|
|
190
|
+
findings: list[MutationLockFinding]
|
|
191
|
+
|
|
192
|
+
@property
|
|
193
|
+
def blocking_findings(self) -> list[MutationLockFinding]:
|
|
194
|
+
return [
|
|
195
|
+
finding
|
|
196
|
+
for finding in self.findings
|
|
197
|
+
if classify_mutation_lock_finding(finding).counts_as_blocking
|
|
198
|
+
]
|
|
199
|
+
|
|
200
|
+
@property
|
|
201
|
+
def non_blocking_findings(self) -> list[MutationLockFinding]:
|
|
202
|
+
return [
|
|
203
|
+
finding
|
|
204
|
+
for finding in self.findings
|
|
205
|
+
if not classify_mutation_lock_finding(finding).counts_as_blocking
|
|
206
|
+
]
|
|
207
|
+
|
|
208
|
+
@property
|
|
209
|
+
def result_status(self) -> str:
|
|
210
|
+
return "BLOCK" if self.blocking_findings else "PASS"
|
|
211
|
+
|
|
212
|
+
@property
|
|
213
|
+
def returncode(self) -> int:
|
|
214
|
+
return 0 if self.result_status == "PASS" else 1
|
|
215
|
+
|
|
216
|
+
def to_dict(self) -> dict[str, Any]:
|
|
217
|
+
findings = [_finding_to_dict(finding) for finding in self.findings]
|
|
218
|
+
blocking_findings = [
|
|
219
|
+
finding
|
|
220
|
+
for finding in findings
|
|
221
|
+
if finding["classification"]["counts_as_blocking"]
|
|
222
|
+
]
|
|
223
|
+
non_blocking_findings = [
|
|
224
|
+
finding
|
|
225
|
+
for finding in findings
|
|
226
|
+
if not finding["classification"]["counts_as_blocking"]
|
|
227
|
+
]
|
|
228
|
+
return {
|
|
229
|
+
"kind": self.kind,
|
|
230
|
+
"result_status": self.result_status,
|
|
231
|
+
"finding_count": len(findings),
|
|
232
|
+
"blocking_finding_count": len(blocking_findings),
|
|
233
|
+
"non_blocking_finding_count": len(non_blocking_findings),
|
|
234
|
+
"classification_summary": _classification_summary(self.findings),
|
|
235
|
+
"findings": findings,
|
|
236
|
+
"blocking_findings": blocking_findings,
|
|
237
|
+
"non_blocking_findings": non_blocking_findings,
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
|
|
241
|
+
def _finding_to_dict(finding: MutationLockFinding) -> dict[str, Any]:
|
|
242
|
+
classification = classify_mutation_lock_finding(finding)
|
|
243
|
+
data = asdict(finding)
|
|
244
|
+
data["classification"] = classification.to_dict()
|
|
245
|
+
return data
|
|
246
|
+
|
|
247
|
+
|
|
248
|
+
def _classification_summary(findings: list[MutationLockFinding]) -> dict[str, dict[str, object]]:
|
|
249
|
+
summary: dict[str, dict[str, object]] = {}
|
|
250
|
+
for finding in findings:
|
|
251
|
+
classification = classify_mutation_lock_finding(finding)
|
|
252
|
+
entry = summary.setdefault(
|
|
253
|
+
classification.kind,
|
|
254
|
+
{
|
|
255
|
+
"count": 0,
|
|
256
|
+
"counts_as_blocking": classification.counts_as_blocking,
|
|
257
|
+
},
|
|
258
|
+
)
|
|
259
|
+
entry["count"] = int(entry["count"]) + 1
|
|
260
|
+
return dict(sorted(summary.items()))
|
|
261
|
+
|
|
262
|
+
|
|
263
|
+
_MUTATION_PATTERNS: tuple[tuple[str, re.Pattern[str]], ...] = (
|
|
264
|
+
("git_branch_mutation", re.compile(r"\bgit\s+(?:switch|checkout|branch)\b|[\"']git[\"']\s*,\s*[\"'](?:switch|checkout|branch)[\"']|branch_create|branch-switch")),
|
|
265
|
+
("git_history_mutation", re.compile(r"\bgit\s+(?:commit|merge|reset|restore|push)\b|[\"']git[\"']\s*,\s*[\"'](?:commit|merge|reset|restore|push)[\"']|pr-create-complete|pr-complete")),
|
|
266
|
+
("filesystem_mutation", re.compile(r"\.(?:write_text|unlink|replace|rename)\(|(?:mkdir|rmtree)\(")),
|
|
267
|
+
("github_mutation", re.compile(r"\bgh\s+pr\s+(?:create|merge|close|edit)\b")),
|
|
268
|
+
)
|
|
269
|
+
|
|
270
|
+
_LOCK_MARKERS = (
|
|
271
|
+
"workspace_lock",
|
|
272
|
+
"acquire_workspace_lock",
|
|
273
|
+
"WorkspaceLock",
|
|
274
|
+
"mutation_lock",
|
|
275
|
+
"with_workspace_lock",
|
|
276
|
+
)
|
|
277
|
+
|
|
278
|
+
|
|
279
|
+
def _nearest_symbol(lines: list[str], index: int) -> str:
|
|
280
|
+
for line_no in range(index, -1, -1):
|
|
281
|
+
line = lines[line_no].strip()
|
|
282
|
+
match = re.match(r"def\s+([a-zA-Z_][a-zA-Z0-9_]*)\(", line)
|
|
283
|
+
if match:
|
|
284
|
+
return match.group(1)
|
|
285
|
+
match = re.match(r"class\s+([a-zA-Z_][a-zA-Z0-9_]*)\b", line)
|
|
286
|
+
if match:
|
|
287
|
+
return match.group(1)
|
|
288
|
+
return "<module>"
|
|
289
|
+
|
|
290
|
+
|
|
291
|
+
def _function_body(lines: list[str], symbol: str) -> str:
|
|
292
|
+
start: int | None = None
|
|
293
|
+
for line_no, line in enumerate(lines):
|
|
294
|
+
if re.match(rf"def\s+{re.escape(symbol)}\(", line):
|
|
295
|
+
start = line_no
|
|
296
|
+
break
|
|
297
|
+
if start is None:
|
|
298
|
+
return ""
|
|
299
|
+
end = len(lines)
|
|
300
|
+
for line_no in range(start + 1, len(lines)):
|
|
301
|
+
if re.match(r"def\s+[a-zA-Z_][a-zA-Z0-9_]*\(", lines[line_no]):
|
|
302
|
+
end = line_no
|
|
303
|
+
break
|
|
304
|
+
return "\n".join(lines[start:end])
|
|
305
|
+
|
|
306
|
+
|
|
307
|
+
def _symbol_has_lock_marker(lines: list[str], symbol: str) -> bool:
|
|
308
|
+
body = _function_body(lines, symbol)
|
|
309
|
+
if any(marker in body for marker in _LOCK_MARKERS):
|
|
310
|
+
return True
|
|
311
|
+
if symbol.startswith("_") and symbol.endswith("_unlocked"):
|
|
312
|
+
wrapper_symbol = symbol.removeprefix("_").removesuffix("_unlocked")
|
|
313
|
+
wrapper_body = _function_body(lines, wrapper_symbol)
|
|
314
|
+
return any(marker in wrapper_body for marker in _LOCK_MARKERS)
|
|
315
|
+
return False
|
|
316
|
+
|
|
317
|
+
|
|
318
|
+
def _has_lock_marker_nearby(lines: list[str], index: int) -> bool:
|
|
319
|
+
start = max(0, index - 12)
|
|
320
|
+
end = min(len(lines), index + 8)
|
|
321
|
+
window = "\n".join(lines[start:end])
|
|
322
|
+
return any(marker in window for marker in _LOCK_MARKERS)
|
|
323
|
+
|
|
324
|
+
|
|
325
|
+
def audit_mutation_lock_coverage(root: Path | str = ".") -> MutationLockCoverageAuditResult:
|
|
326
|
+
repo_root = Path(root)
|
|
327
|
+
src_root = repo_root / "src" / "agentic_project_kit"
|
|
328
|
+
findings: list[MutationLockFinding] = []
|
|
329
|
+
|
|
330
|
+
if not src_root.exists():
|
|
331
|
+
return MutationLockCoverageAuditResult(
|
|
332
|
+
kind="mutation_lock_coverage_audit",
|
|
333
|
+
findings=[
|
|
334
|
+
MutationLockFinding(
|
|
335
|
+
category="A",
|
|
336
|
+
symbol="src_root_missing",
|
|
337
|
+
path=str(src_root),
|
|
338
|
+
line=0,
|
|
339
|
+
reason="agentic_project_kit source root is missing",
|
|
340
|
+
)
|
|
341
|
+
],
|
|
342
|
+
)
|
|
343
|
+
|
|
344
|
+
for path in sorted(src_root.rglob("*.py")):
|
|
345
|
+
rel = path.relative_to(repo_root).as_posix()
|
|
346
|
+
if path.name == "mutation_lock_audit.py":
|
|
347
|
+
continue
|
|
348
|
+
|
|
349
|
+
lines = path.read_text(encoding="utf-8", errors="replace").splitlines()
|
|
350
|
+
for index, line in enumerate(lines):
|
|
351
|
+
stripped = line.strip()
|
|
352
|
+
if stripped.startswith("#"):
|
|
353
|
+
continue
|
|
354
|
+
|
|
355
|
+
for category_name, pattern in _MUTATION_PATTERNS:
|
|
356
|
+
if not pattern.search(line):
|
|
357
|
+
continue
|
|
358
|
+
|
|
359
|
+
if _has_lock_marker_nearby(lines, index):
|
|
360
|
+
continue
|
|
361
|
+
|
|
362
|
+
symbol = _nearest_symbol(lines, index)
|
|
363
|
+
if _symbol_has_lock_marker(lines, symbol):
|
|
364
|
+
continue
|
|
365
|
+
category = {
|
|
366
|
+
"git_branch_mutation": "A",
|
|
367
|
+
"git_history_mutation": "A",
|
|
368
|
+
"github_mutation": "B",
|
|
369
|
+
"filesystem_mutation": "C",
|
|
370
|
+
}[category_name]
|
|
371
|
+
|
|
372
|
+
findings.append(
|
|
373
|
+
MutationLockFinding(
|
|
374
|
+
category=category,
|
|
375
|
+
symbol=symbol,
|
|
376
|
+
path=rel,
|
|
377
|
+
line=index + 1,
|
|
378
|
+
reason=f"{category_name} without nearby workspace mutation lock marker",
|
|
379
|
+
)
|
|
380
|
+
)
|
|
381
|
+
|
|
382
|
+
return MutationLockCoverageAuditResult(
|
|
383
|
+
kind="mutation_lock_coverage_audit",
|
|
384
|
+
findings=findings,
|
|
385
|
+
)
|
|
386
|
+
|
|
387
|
+
|
|
388
|
+
def render_mutation_lock_coverage_audit(result: MutationLockCoverageAuditResult) -> str:
|
|
389
|
+
lines = [
|
|
390
|
+
"MUTATION_LOCK_COVERAGE_AUDIT",
|
|
391
|
+
f"RESULT: {result.result_status}",
|
|
392
|
+
f"FINDINGS: {len(result.findings)}",
|
|
393
|
+
f"BLOCKING_FINDINGS: {len(result.blocking_findings)}",
|
|
394
|
+
f"NON_BLOCKING_FINDINGS: {len(result.non_blocking_findings)}",
|
|
395
|
+
]
|
|
396
|
+
if result.findings:
|
|
397
|
+
lines.append("CLASSIFICATION_SUMMARY:")
|
|
398
|
+
for kind, summary in _classification_summary(result.findings).items():
|
|
399
|
+
disposition = "BLOCK" if summary["counts_as_blocking"] else "non-blocking"
|
|
400
|
+
lines.append(f"- {kind}: {summary['count']} ({disposition})")
|
|
401
|
+
|
|
402
|
+
if result.blocking_findings:
|
|
403
|
+
lines.append("BLOCKING:")
|
|
404
|
+
for finding in result.blocking_findings:
|
|
405
|
+
classification = classify_mutation_lock_finding(finding)
|
|
406
|
+
lines.append(
|
|
407
|
+
f"- [{finding.category}] {finding.symbol} {finding.path}:{finding.line} "
|
|
408
|
+
f"— classification={classification.kind} disposition=BLOCK "
|
|
409
|
+
f"— {finding.reason} — {classification.rationale}"
|
|
410
|
+
)
|
|
411
|
+
|
|
412
|
+
if result.non_blocking_findings:
|
|
413
|
+
lines.append("NON_BLOCKING:")
|
|
414
|
+
for finding in result.non_blocking_findings:
|
|
415
|
+
classification = classify_mutation_lock_finding(finding)
|
|
416
|
+
lines.append(
|
|
417
|
+
f"- [{finding.category}] {finding.symbol} {finding.path}:{finding.line} "
|
|
418
|
+
f"— classification={classification.kind} disposition=non-blocking "
|
|
419
|
+
f"— {finding.reason} — {classification.rationale}"
|
|
420
|
+
)
|
|
421
|
+
return "\n".join(lines) + "\n"
|
|
@@ -0,0 +1,253 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import argparse
|
|
4
|
+
import subprocess
|
|
5
|
+
from dataclasses import dataclass
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
|
|
8
|
+
from agentic_project_kit.safe_push import safe_push
|
|
9
|
+
|
|
10
|
+
from agentic_project_kit.next_turn_result import publish_local_evidence, render_summary
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
@dataclass(frozen=True)
|
|
14
|
+
class EvidencePublishPlan:
|
|
15
|
+
run_id: str
|
|
16
|
+
local_terminal_log: str
|
|
17
|
+
work_result: str
|
|
18
|
+
files_to_stage: tuple[str, ...]
|
|
19
|
+
commit_message: str
|
|
20
|
+
branch: str
|
|
21
|
+
push_command: str
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def _run_git(args: list[str], *, root: Path, check: bool = True) -> subprocess.CompletedProcess[str]:
|
|
25
|
+
completed = subprocess.run(
|
|
26
|
+
["git", *args],
|
|
27
|
+
cwd=root,
|
|
28
|
+
text=True,
|
|
29
|
+
capture_output=True,
|
|
30
|
+
check=False,
|
|
31
|
+
)
|
|
32
|
+
if check and completed.returncode != 0:
|
|
33
|
+
raise RuntimeError(completed.stderr.strip() or completed.stdout.strip())
|
|
34
|
+
return completed
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def _git_stdout(args: list[str], *, root: Path) -> str:
|
|
38
|
+
return _run_git(args, root=root).stdout.strip()
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def current_branch(root: Path | str = ".") -> str:
|
|
42
|
+
return _git_stdout(["branch", "--show-current"], root=Path(root))
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def build_evidence_publish_plan(
|
|
46
|
+
*,
|
|
47
|
+
run_id: str,
|
|
48
|
+
local_terminal_log: str,
|
|
49
|
+
work_result: str,
|
|
50
|
+
root: Path | str = ".",
|
|
51
|
+
) -> EvidencePublishPlan:
|
|
52
|
+
root_path = Path(root)
|
|
53
|
+
branch = current_branch(root_path)
|
|
54
|
+
files = (
|
|
55
|
+
f"docs/reports/terminal/{run_id}.log",
|
|
56
|
+
f"docs/reports/command_runs/{run_id}.json",
|
|
57
|
+
"docs/reports/command_runs/next-turn-latest.json",
|
|
58
|
+
)
|
|
59
|
+
return EvidencePublishPlan(
|
|
60
|
+
run_id=run_id,
|
|
61
|
+
local_terminal_log=local_terminal_log,
|
|
62
|
+
work_result=work_result,
|
|
63
|
+
files_to_stage=files,
|
|
64
|
+
commit_message=f"Record {run_id} evidence",
|
|
65
|
+
branch=branch,
|
|
66
|
+
push_command=f"git push -u origin {branch}",
|
|
67
|
+
)
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
def publish_and_stage_evidence(
|
|
71
|
+
*,
|
|
72
|
+
run_id: str,
|
|
73
|
+
local_terminal_log: str,
|
|
74
|
+
work_result: str,
|
|
75
|
+
root: Path | str = ".",
|
|
76
|
+
) -> EvidencePublishPlan:
|
|
77
|
+
root_path = Path(root)
|
|
78
|
+
result = publish_local_evidence(
|
|
79
|
+
run_id=run_id,
|
|
80
|
+
local_terminal_log=local_terminal_log,
|
|
81
|
+
work_result=work_result, # type: ignore[arg-type]
|
|
82
|
+
root=root_path,
|
|
83
|
+
)
|
|
84
|
+
plan = build_evidence_publish_plan(
|
|
85
|
+
run_id=run_id,
|
|
86
|
+
local_terminal_log=local_terminal_log,
|
|
87
|
+
work_result=work_result,
|
|
88
|
+
root=root_path,
|
|
89
|
+
)
|
|
90
|
+
_run_git(["add", *plan.files_to_stage], root=root_path)
|
|
91
|
+
print(render_summary(result))
|
|
92
|
+
return plan
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
@dataclass(frozen=True)
|
|
97
|
+
class EvidenceFinalizeResult:
|
|
98
|
+
plan: EvidencePublishPlan
|
|
99
|
+
committed: bool
|
|
100
|
+
pushed: bool
|
|
101
|
+
already_clean: bool
|
|
102
|
+
commit_sha: str
|
|
103
|
+
message: str
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
def has_staged_changes(root: Path | str = ".") -> bool:
|
|
107
|
+
completed = _run_git(["diff", "--cached", "--quiet"], root=Path(root), check=False)
|
|
108
|
+
return completed.returncode == 1
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
def commit_and_push_evidence(
|
|
112
|
+
*,
|
|
113
|
+
plan: EvidencePublishPlan,
|
|
114
|
+
root: Path | str = ".",
|
|
115
|
+
push: bool = True,
|
|
116
|
+
required_branch: str = "",
|
|
117
|
+
allow_main: bool = False,
|
|
118
|
+
) -> EvidenceFinalizeResult:
|
|
119
|
+
root_path = Path(root)
|
|
120
|
+
|
|
121
|
+
if not has_staged_changes(root_path):
|
|
122
|
+
return EvidenceFinalizeResult(
|
|
123
|
+
plan=plan,
|
|
124
|
+
committed=False,
|
|
125
|
+
pushed=False,
|
|
126
|
+
already_clean=True,
|
|
127
|
+
commit_sha="",
|
|
128
|
+
message="PASS_ALREADY_DONE: no staged evidence changes",
|
|
129
|
+
)
|
|
130
|
+
|
|
131
|
+
branch = current_branch(root_path)
|
|
132
|
+
if required_branch and branch != required_branch:
|
|
133
|
+
raise RuntimeError(f"current branch {branch!r} does not match required branch {required_branch!r}")
|
|
134
|
+
if branch == "main" and not allow_main:
|
|
135
|
+
raise RuntimeError("next-turn evidence refuses to commit on main without allow_main=True")
|
|
136
|
+
if not branch:
|
|
137
|
+
raise RuntimeError("next-turn evidence refuses to commit without a current branch")
|
|
138
|
+
if push:
|
|
139
|
+
push_preflight = safe_push(
|
|
140
|
+
root_path,
|
|
141
|
+
target_branch=plan.branch,
|
|
142
|
+
purpose="next-turn evidence publish branch",
|
|
143
|
+
dry_run=True,
|
|
144
|
+
)
|
|
145
|
+
if not push_preflight.ok:
|
|
146
|
+
raise RuntimeError("push preflight blocked next-turn evidence: " + "; ".join(push_preflight.reasons))
|
|
147
|
+
|
|
148
|
+
commit = _run_git(["commit", "-m", plan.commit_message], root=root_path, check=False)
|
|
149
|
+
if commit.returncode != 0:
|
|
150
|
+
combined = (commit.stderr + commit.stdout).strip()
|
|
151
|
+
if "nothing to commit" in combined or "no changes added to commit" in combined:
|
|
152
|
+
return EvidenceFinalizeResult(
|
|
153
|
+
plan=plan,
|
|
154
|
+
committed=False,
|
|
155
|
+
pushed=False,
|
|
156
|
+
already_clean=True,
|
|
157
|
+
commit_sha="",
|
|
158
|
+
message="PASS_ALREADY_DONE: nothing to commit",
|
|
159
|
+
)
|
|
160
|
+
raise RuntimeError(combined)
|
|
161
|
+
|
|
162
|
+
sha = _git_stdout(["rev-parse", "HEAD"], root=root_path)
|
|
163
|
+
|
|
164
|
+
pushed = False
|
|
165
|
+
if push:
|
|
166
|
+
push_result = safe_push(
|
|
167
|
+
root_path,
|
|
168
|
+
target_branch=plan.branch,
|
|
169
|
+
purpose="next-turn evidence publish branch",
|
|
170
|
+
set_upstream=True,
|
|
171
|
+
)
|
|
172
|
+
if not push_result.ok:
|
|
173
|
+
raise RuntimeError((push_result.stderr + push_result.stdout).strip())
|
|
174
|
+
pushed = True
|
|
175
|
+
|
|
176
|
+
return EvidenceFinalizeResult(
|
|
177
|
+
plan=plan,
|
|
178
|
+
committed=True,
|
|
179
|
+
pushed=pushed,
|
|
180
|
+
already_clean=False,
|
|
181
|
+
commit_sha=sha,
|
|
182
|
+
message="PASS: evidence committed" + (" and pushed" if pushed else ""),
|
|
183
|
+
)
|
|
184
|
+
|
|
185
|
+
|
|
186
|
+
def render_finalize_result(result: EvidenceFinalizeResult) -> str:
|
|
187
|
+
lines = [
|
|
188
|
+
"NEXT_TURN_EVIDENCE_FINALIZE_RESULT",
|
|
189
|
+
f"run_id={result.plan.run_id}",
|
|
190
|
+
f"committed={str(result.committed).lower()}",
|
|
191
|
+
f"pushed={str(result.pushed).lower()}",
|
|
192
|
+
f"already_clean={str(result.already_clean).lower()}",
|
|
193
|
+
f"commit_sha={result.commit_sha}",
|
|
194
|
+
f"message={result.message}",
|
|
195
|
+
f"branch={result.plan.branch}",
|
|
196
|
+
"subresult=PASS",
|
|
197
|
+
]
|
|
198
|
+
return "\n".join(lines)
|
|
199
|
+
|
|
200
|
+
def render_plan(plan: EvidencePublishPlan) -> str:
|
|
201
|
+
lines = [
|
|
202
|
+
"NEXT_TURN_EVIDENCE_PUBLISH_PLAN",
|
|
203
|
+
f"run_id={plan.run_id}",
|
|
204
|
+
f"local_terminal_log={plan.local_terminal_log}",
|
|
205
|
+
f"work_result={plan.work_result}",
|
|
206
|
+
f"branch={plan.branch}",
|
|
207
|
+
f"commit_message={plan.commit_message}",
|
|
208
|
+
f"push_command={plan.push_command}",
|
|
209
|
+
"files_to_stage:",
|
|
210
|
+
]
|
|
211
|
+
lines.extend(f"- {item}" for item in plan.files_to_stage)
|
|
212
|
+
lines.append("subresult=PASS")
|
|
213
|
+
return "\n".join(lines)
|
|
214
|
+
|
|
215
|
+
|
|
216
|
+
def main() -> int:
|
|
217
|
+
parser = argparse.ArgumentParser(prog="next-turn-evidence")
|
|
218
|
+
parser.add_argument("--run-id", required=True)
|
|
219
|
+
parser.add_argument("--local-terminal-log", required=True)
|
|
220
|
+
parser.add_argument("--work-result", required=True, choices=["PASS", "FAIL", "PENDING", "HARD_FAIL", "PASS_ALREADY_DONE", "NOOP"])
|
|
221
|
+
parser.add_argument("--plan-only", action="store_true")
|
|
222
|
+
parser.add_argument("--commit", action="store_true")
|
|
223
|
+
parser.add_argument("--no-push", action="store_true")
|
|
224
|
+
args = parser.parse_args()
|
|
225
|
+
|
|
226
|
+
if args.plan_only:
|
|
227
|
+
plan = build_evidence_publish_plan(
|
|
228
|
+
run_id=args.run_id,
|
|
229
|
+
local_terminal_log=args.local_terminal_log,
|
|
230
|
+
work_result=args.work_result,
|
|
231
|
+
)
|
|
232
|
+
print(render_plan(plan))
|
|
233
|
+
return 0
|
|
234
|
+
|
|
235
|
+
plan = publish_and_stage_evidence(
|
|
236
|
+
run_id=args.run_id,
|
|
237
|
+
local_terminal_log=args.local_terminal_log,
|
|
238
|
+
work_result=args.work_result,
|
|
239
|
+
)
|
|
240
|
+
|
|
241
|
+
if args.commit:
|
|
242
|
+
finalize_result = commit_and_push_evidence(
|
|
243
|
+
plan=plan,
|
|
244
|
+
push=not args.no_push,
|
|
245
|
+
)
|
|
246
|
+
print(render_finalize_result(finalize_result))
|
|
247
|
+
else:
|
|
248
|
+
print(render_plan(plan))
|
|
249
|
+
return 0
|
|
250
|
+
|
|
251
|
+
|
|
252
|
+
if __name__ == "__main__":
|
|
253
|
+
raise SystemExit(main())
|