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,275 @@
|
|
|
1
|
+
"""Terminal log wrapper utilities for local workflow runs.
|
|
2
|
+
|
|
3
|
+
The module supports a deterministic handoff protocol:
|
|
4
|
+
- run a command while teeing stdout/stderr to a terminal log,
|
|
5
|
+
- keep a pointer to the latest terminal log,
|
|
6
|
+
- inspect upload readiness,
|
|
7
|
+
- upload only terminal-log artifacts on failure handoff.
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
from __future__ import annotations
|
|
11
|
+
|
|
12
|
+
import datetime as _dt
|
|
13
|
+
import os
|
|
14
|
+
import subprocess
|
|
15
|
+
import sys
|
|
16
|
+
from pathlib import Path
|
|
17
|
+
|
|
18
|
+
from agentic_project_kit.safe_push import safe_push
|
|
19
|
+
from agentic_project_kit.workspace import LEGACY_DEFAULTS, load_workspace
|
|
20
|
+
|
|
21
|
+
TERMINAL_DIR = Path(LEGACY_DEFAULTS.terminal_reports_root)
|
|
22
|
+
LATEST_POINTER = TERMINAL_DIR / "LATEST_TERMINAL_LOG.txt"
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def _safe_name(name: str) -> str:
|
|
26
|
+
safe = "".join(ch if ch.isalnum() or ch in "-_" else "_" for ch in name.strip())
|
|
27
|
+
return safe or "run"
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def _terminal_dir(root: Path = Path(".")) -> Path:
|
|
31
|
+
if TERMINAL_DIR != Path(LEGACY_DEFAULTS.terminal_reports_root):
|
|
32
|
+
return TERMINAL_DIR
|
|
33
|
+
return load_workspace(root).terminal_reports_dir()
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def _latest_pointer(root: Path = Path(".")) -> Path:
|
|
37
|
+
if LATEST_POINTER != TERMINAL_DIR / "LATEST_TERMINAL_LOG.txt":
|
|
38
|
+
return LATEST_POINTER
|
|
39
|
+
return load_workspace(root).latest_terminal_log_pointer()
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def make_log_path(name: str, now: _dt.datetime | None = None) -> Path:
|
|
43
|
+
current = now or _dt.datetime.now()
|
|
44
|
+
stamp = current.strftime("%Y%m%d-%H%M%S")
|
|
45
|
+
return _terminal_dir() / f"{stamp}_{_safe_name(name)}.log"
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def write_latest_pointer(log_path: Path) -> None:
|
|
49
|
+
pointer = _latest_pointer()
|
|
50
|
+
pointer.parent.mkdir(parents=True, exist_ok=True)
|
|
51
|
+
pointer.write_text(log_path.as_posix() + "\n", encoding="utf-8")
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def read_latest_pointer() -> Path | None:
|
|
55
|
+
pointer = _latest_pointer()
|
|
56
|
+
if not pointer.exists():
|
|
57
|
+
return None
|
|
58
|
+
value = pointer.read_text(encoding="utf-8").strip()
|
|
59
|
+
if not value:
|
|
60
|
+
return None
|
|
61
|
+
return Path(value)
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
def _is_allowed_terminal_artifact(path_text: str) -> bool:
|
|
65
|
+
workspace = load_workspace(Path("."))
|
|
66
|
+
latest = workspace.path_text(_latest_pointer())
|
|
67
|
+
terminal_dir = workspace.path_text(_terminal_dir())
|
|
68
|
+
return path_text == latest or (path_text.startswith(terminal_dir + "/") and path_text.endswith(".log"))
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
def git_dirty_paths() -> list[str]:
|
|
72
|
+
proc = subprocess.run(["git", "status", "--porcelain"], text=True, capture_output=True, check=False)
|
|
73
|
+
paths: list[str] = []
|
|
74
|
+
for line in proc.stdout.splitlines():
|
|
75
|
+
if not line.strip():
|
|
76
|
+
continue
|
|
77
|
+
paths.append(line[3:])
|
|
78
|
+
return paths
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
def terminal_clean_check() -> tuple[str, str]:
|
|
84
|
+
"""Classify dirty state while allowing terminal-log artifacts."""
|
|
85
|
+
|
|
86
|
+
dirty = git_dirty_paths()
|
|
87
|
+
if not dirty:
|
|
88
|
+
return "PASS_CLEAN", "Working tree is clean."
|
|
89
|
+
forbidden = [path for path in dirty if not _is_allowed_terminal_artifact(path)]
|
|
90
|
+
if forbidden:
|
|
91
|
+
return "FAIL_DIRTY_NON_LOG_FILES", chr(10).join(forbidden)
|
|
92
|
+
return "PASS_ONLY_TERMINAL_LOG_DIRTY", chr(10).join(dirty)
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
def remote_mutation_preflight() -> tuple[str, str]:
|
|
96
|
+
"""Require a fully clean worktree before remote mutations or merge/sync verification."""
|
|
97
|
+
|
|
98
|
+
dirty = git_dirty_paths()
|
|
99
|
+
if dirty:
|
|
100
|
+
return "FAIL_DIRTY_WORKTREE_BEFORE_REMOTE_MUTATION", chr(10).join(dirty)
|
|
101
|
+
return "PASS_CLEAN_FOR_REMOTE_MUTATION", "Working tree is clean for remote mutation or merge verification."
|
|
102
|
+
def terminal_status() -> tuple[str, str]:
|
|
103
|
+
latest = read_latest_pointer()
|
|
104
|
+
if latest is None:
|
|
105
|
+
return "PASS_NO_LOG", "No latest terminal log pointer exists."
|
|
106
|
+
if not latest.exists():
|
|
107
|
+
return "FAIL_INVALID_POINTER", f"Latest terminal log pointer targets missing file: {latest.as_posix()}"
|
|
108
|
+
return "PASS_LOG_READY", f"Latest terminal log: {latest.as_posix()}"
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
def _has_result_marker(text: str) -> bool:
|
|
112
|
+
return "### RESULT: PASS ###" in text or "### RESULT: FAIL ###" in text or "### RESULT: PENDING ###" in text
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
def finalize_terminal_log(tmp_log: Path, name: str) -> tuple[str, str]:
|
|
116
|
+
source = Path(tmp_log)
|
|
117
|
+
if not source.exists():
|
|
118
|
+
return "FAIL_SOURCE_MISSING", source.as_posix()
|
|
119
|
+
source_text = source.read_text(encoding="utf-8")
|
|
120
|
+
if not _has_result_marker(source_text):
|
|
121
|
+
return "FAIL_MISSING_RESULT_MARKER", source.as_posix()
|
|
122
|
+
source_resolved = source.resolve()
|
|
123
|
+
terminal_dir = _terminal_dir()
|
|
124
|
+
terminal_resolved = terminal_dir.resolve()
|
|
125
|
+
try:
|
|
126
|
+
source_resolved.relative_to(terminal_resolved)
|
|
127
|
+
except ValueError:
|
|
128
|
+
source_inside_terminal_dir = False
|
|
129
|
+
else:
|
|
130
|
+
source_inside_terminal_dir = True
|
|
131
|
+
if source_inside_terminal_dir:
|
|
132
|
+
return "FAIL_SOURCE_INSIDE_TERMINAL_DIR", source.as_posix()
|
|
133
|
+
terminal_dir.mkdir(parents=True, exist_ok=True)
|
|
134
|
+
target = make_log_path(name)
|
|
135
|
+
target.write_text(source_text, encoding="utf-8")
|
|
136
|
+
write_latest_pointer(target)
|
|
137
|
+
return "PASS_FINALIZED", target.as_posix()
|
|
138
|
+
|
|
139
|
+
|
|
140
|
+
def run_logged(name: str, command: list[str]) -> int:
|
|
141
|
+
if not command:
|
|
142
|
+
print("FAIL_NO_COMMAND")
|
|
143
|
+
return 2
|
|
144
|
+
_terminal_dir().mkdir(parents=True, exist_ok=True)
|
|
145
|
+
log_path = make_log_path(name)
|
|
146
|
+
write_latest_pointer(log_path)
|
|
147
|
+
with log_path.open("w", encoding="utf-8") as log:
|
|
148
|
+
def emit(line: str) -> None:
|
|
149
|
+
print(line)
|
|
150
|
+
log.write(line + "\n")
|
|
151
|
+
log.flush()
|
|
152
|
+
emit(f"terminal_log={log_path.as_posix()}")
|
|
153
|
+
emit("### RUN: " + " ".join(command))
|
|
154
|
+
proc = subprocess.Popen(command, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, text=True, env=os.environ.copy())
|
|
155
|
+
assert proc.stdout is not None
|
|
156
|
+
for raw in proc.stdout:
|
|
157
|
+
line = raw.rstrip("\n")
|
|
158
|
+
print(line)
|
|
159
|
+
log.write(line + "\n")
|
|
160
|
+
code = proc.wait()
|
|
161
|
+
emit(f"### EXIT: {code}")
|
|
162
|
+
emit("### RESULT: PASS ###" if code == 0 else "### RESULT: FAIL ###")
|
|
163
|
+
return code
|
|
164
|
+
|
|
165
|
+
|
|
166
|
+
def _current_branch() -> str:
|
|
167
|
+
proc = subprocess.run(["git", "branch", "--show-current"], text=True, capture_output=True, check=False)
|
|
168
|
+
return proc.stdout.strip() if proc.returncode == 0 else ""
|
|
169
|
+
|
|
170
|
+
|
|
171
|
+
def _refuse_unsafe_mutation_branch(required_branch: str = "", allow_main: bool = False) -> tuple[bool, str]:
|
|
172
|
+
branch = _current_branch()
|
|
173
|
+
if required_branch and branch != required_branch:
|
|
174
|
+
return False, f"FAIL_BRANCH_MISMATCH actual={branch or 'UNKNOWN'} required={required_branch}"
|
|
175
|
+
if branch == "main" and not allow_main:
|
|
176
|
+
return False, "FAIL_MAIN_MUTATION_NOT_ALLOWED"
|
|
177
|
+
if not branch:
|
|
178
|
+
return False, "FAIL_NO_CURRENT_BRANCH"
|
|
179
|
+
return True, branch
|
|
180
|
+
|
|
181
|
+
|
|
182
|
+
def upload_terminal_output(required_branch: str = "", allow_main: bool = False) -> int:
|
|
183
|
+
branch_ok, branch_message = _refuse_unsafe_mutation_branch(required_branch=required_branch, allow_main=allow_main)
|
|
184
|
+
if not branch_ok:
|
|
185
|
+
print(branch_message)
|
|
186
|
+
return 1
|
|
187
|
+
outcome, message = terminal_status()
|
|
188
|
+
print(outcome)
|
|
189
|
+
print(message)
|
|
190
|
+
if outcome != "PASS_LOG_READY":
|
|
191
|
+
return 1
|
|
192
|
+
dirty = git_dirty_paths()
|
|
193
|
+
forbidden = [p for p in dirty if not _is_allowed_terminal_artifact(p)]
|
|
194
|
+
if forbidden:
|
|
195
|
+
print("FAIL_DIRTY_NON_LOG_FILES")
|
|
196
|
+
for item in forbidden:
|
|
197
|
+
print(item)
|
|
198
|
+
return 1
|
|
199
|
+
push_preflight = safe_push(
|
|
200
|
+
Path("."),
|
|
201
|
+
target_branch=branch_message,
|
|
202
|
+
purpose="upload terminal output log",
|
|
203
|
+
dry_run=True,
|
|
204
|
+
)
|
|
205
|
+
if not push_preflight.ok:
|
|
206
|
+
print("FAIL_PUSH_PREFLIGHT_BLOCKED")
|
|
207
|
+
for reason in push_preflight.reasons:
|
|
208
|
+
print(reason)
|
|
209
|
+
return push_preflight.returncode
|
|
210
|
+
latest = read_latest_pointer()
|
|
211
|
+
assert latest is not None
|
|
212
|
+
subprocess.run(["git", "add", latest.as_posix(), _latest_pointer().as_posix()], check=True)
|
|
213
|
+
diff = subprocess.run(["git", "diff", "--cached", "--quiet"], check=False)
|
|
214
|
+
if diff.returncode == 0:
|
|
215
|
+
print("PASS_ALREADY_UPLOADED")
|
|
216
|
+
return 0
|
|
217
|
+
subprocess.run(["git", "commit", "-m", "Upload terminal output log"], check=True)
|
|
218
|
+
pushed = safe_push(
|
|
219
|
+
Path("."),
|
|
220
|
+
target_branch=branch_message,
|
|
221
|
+
purpose="upload terminal output log",
|
|
222
|
+
)
|
|
223
|
+
if not pushed.ok:
|
|
224
|
+
print("FAIL_PUSH_FAILED")
|
|
225
|
+
return pushed.returncode
|
|
226
|
+
print("PASS_UPLOADED")
|
|
227
|
+
return 0
|
|
228
|
+
|
|
229
|
+
|
|
230
|
+
def main(argv: list[str] | None = None) -> int:
|
|
231
|
+
args = list(sys.argv[1:] if argv is None else argv)
|
|
232
|
+
if not args:
|
|
233
|
+
print("Usage: terminal_logging.py run-logged <name> -- <command...> | terminal-status | terminal-upload")
|
|
234
|
+
return 2
|
|
235
|
+
mode = args.pop(0)
|
|
236
|
+
if mode == "terminal-status":
|
|
237
|
+
outcome, message = terminal_status()
|
|
238
|
+
print(outcome)
|
|
239
|
+
print(message)
|
|
240
|
+
return 0 if outcome.startswith("PASS") else 1
|
|
241
|
+
if mode == "terminal-clean-check":
|
|
242
|
+
outcome, message = terminal_clean_check()
|
|
243
|
+
print(outcome)
|
|
244
|
+
print(message)
|
|
245
|
+
return 0 if outcome.startswith("PASS") else 1
|
|
246
|
+
if mode == "terminal-remote-preflight":
|
|
247
|
+
outcome, message = remote_mutation_preflight()
|
|
248
|
+
print(outcome)
|
|
249
|
+
print(message)
|
|
250
|
+
return 0 if outcome.startswith("PASS") else 1
|
|
251
|
+
if mode == "terminal-upload":
|
|
252
|
+
return upload_terminal_output()
|
|
253
|
+
if mode == "terminal-finalize":
|
|
254
|
+
if len(args) != 2:
|
|
255
|
+
print("FAIL_USAGE")
|
|
256
|
+
print("Usage: terminal-finalize <tmp-log> <name>")
|
|
257
|
+
return 2
|
|
258
|
+
outcome, message = finalize_terminal_log(Path(args[0]), args[1])
|
|
259
|
+
print(outcome)
|
|
260
|
+
print(message)
|
|
261
|
+
return 0 if outcome == "PASS_FINALIZED" else 1
|
|
262
|
+
if mode == "run-logged":
|
|
263
|
+
if "--" not in args:
|
|
264
|
+
print("FAIL_MISSING_SEPARATOR")
|
|
265
|
+
return 2
|
|
266
|
+
sep = args.index("--")
|
|
267
|
+
name = args[0] if sep > 0 else "run"
|
|
268
|
+
command = args[sep + 1:]
|
|
269
|
+
return run_logged(name, command)
|
|
270
|
+
print("FAIL_UNKNOWN_MODE")
|
|
271
|
+
return 2
|
|
272
|
+
|
|
273
|
+
|
|
274
|
+
if __name__ == "__main__":
|
|
275
|
+
raise SystemExit(main())
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from datetime import datetime, timezone
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
from typing import Any
|
|
6
|
+
|
|
7
|
+
import yaml
|
|
8
|
+
|
|
9
|
+
from agentic_project_kit.workspace import LEGACY_DEFAULTS, load_workspace
|
|
10
|
+
|
|
11
|
+
DEFAULT_TODO_PATH = Path(".agentic/todo.yaml")
|
|
12
|
+
DEFAULT_RENDER_PATH = Path(LEGACY_DEFAULTS.docs_root) / "TODO.md"
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def load_todo(path: Path = DEFAULT_TODO_PATH) -> dict[str, Any]:
|
|
16
|
+
if not path.exists():
|
|
17
|
+
raise FileNotFoundError(f"TODO file not found: {path}")
|
|
18
|
+
data = yaml.safe_load(path.read_text(encoding="utf-8")) or {}
|
|
19
|
+
items = data.get("items")
|
|
20
|
+
if not isinstance(items, list):
|
|
21
|
+
raise ValueError(f"TODO file must contain an 'items' list: {path}")
|
|
22
|
+
return data
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def save_todo(data: dict[str, Any], path: Path = DEFAULT_TODO_PATH) -> None:
|
|
26
|
+
path.parent.mkdir(parents=True, exist_ok=True)
|
|
27
|
+
path.write_text(
|
|
28
|
+
yaml.safe_dump(data, sort_keys=False, allow_unicode=True),
|
|
29
|
+
encoding="utf-8",
|
|
30
|
+
)
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def list_items(data: dict[str, Any], include_done: bool = False) -> list[dict[str, Any]]:
|
|
34
|
+
items = data.get("items", [])
|
|
35
|
+
if include_done:
|
|
36
|
+
return list(items)
|
|
37
|
+
return [item for item in items if item.get("status", "open") != "done"]
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def complete_item(
|
|
41
|
+
item_id: str,
|
|
42
|
+
evidence: str,
|
|
43
|
+
path: Path = DEFAULT_TODO_PATH,
|
|
44
|
+
) -> dict[str, Any]:
|
|
45
|
+
data = load_todo(path)
|
|
46
|
+
for item in data["items"]:
|
|
47
|
+
if item.get("id") == item_id:
|
|
48
|
+
item["status"] = "done"
|
|
49
|
+
item["evidence"] = evidence
|
|
50
|
+
item["completed_at"] = datetime.now(timezone.utc).replace(microsecond=0).isoformat()
|
|
51
|
+
save_todo(data, path)
|
|
52
|
+
return item
|
|
53
|
+
raise KeyError(f"TODO item not found: {item_id}")
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def render_markdown(
|
|
57
|
+
todo_path: Path = DEFAULT_TODO_PATH,
|
|
58
|
+
output_path: Path = DEFAULT_RENDER_PATH,
|
|
59
|
+
) -> str:
|
|
60
|
+
data = load_todo(todo_path)
|
|
61
|
+
if output_path == DEFAULT_RENDER_PATH:
|
|
62
|
+
output_path = load_workspace(Path(".")).docs_file("TODO.md")
|
|
63
|
+
lines: list[str] = [
|
|
64
|
+
"# TODO",
|
|
65
|
+
"",
|
|
66
|
+
"This file is generated from `.agentic/todo.yaml`.",
|
|
67
|
+
"",
|
|
68
|
+
"## Items",
|
|
69
|
+
"",
|
|
70
|
+
]
|
|
71
|
+
|
|
72
|
+
for item in data["items"]:
|
|
73
|
+
done = item.get("status") == "done"
|
|
74
|
+
marker = "x" if done else " "
|
|
75
|
+
item_id = item.get("id", "UNKNOWN")
|
|
76
|
+
title = item.get("title", "")
|
|
77
|
+
owner = item.get("owner", "unknown")
|
|
78
|
+
priority = item.get("priority", "normal")
|
|
79
|
+
evidence_required = item.get("evidence_required", "")
|
|
80
|
+
evidence = item.get("evidence", "")
|
|
81
|
+
completed_at = item.get("completed_at", "")
|
|
82
|
+
|
|
83
|
+
lines.append(f"- [{marker}] **{item_id}** {title} ")
|
|
84
|
+
lines.append(f" Owner: {owner} ")
|
|
85
|
+
lines.append(f" Priority: {priority} ")
|
|
86
|
+
if evidence_required:
|
|
87
|
+
lines.append(f" Evidence required: `{evidence_required}` ")
|
|
88
|
+
if evidence:
|
|
89
|
+
lines.append(f" Evidence: `{evidence}` ")
|
|
90
|
+
if completed_at:
|
|
91
|
+
lines.append(f" Completed at: `{completed_at}` ")
|
|
92
|
+
lines.append("")
|
|
93
|
+
|
|
94
|
+
output_path.parent.mkdir(parents=True, exist_ok=True)
|
|
95
|
+
text = "\n".join(lines).rstrip() + "\n"
|
|
96
|
+
output_path.write_text(text, encoding="utf-8")
|
|
97
|
+
return text
|
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
import shutil
|
|
5
|
+
import subprocess
|
|
6
|
+
from dataclasses import dataclass
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
from typing import Any
|
|
9
|
+
|
|
10
|
+
from agentic_project_kit.transfer_state import build_transfer_state
|
|
11
|
+
from agentic_project_kit.workspace import LEGACY_DEFAULTS, load_workspace
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
LATEST_COMMAND_RUN = Path(LEGACY_DEFAULTS.command_runs_root) / "LATEST_COMMAND_RUN.txt"
|
|
15
|
+
TRANSFER_ROOT = Path(".agentic/transfer")
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
@dataclass(frozen=True)
|
|
19
|
+
class TransferCloseout:
|
|
20
|
+
schema_version: int
|
|
21
|
+
removed_transfer_dir: bool
|
|
22
|
+
latest_command_run_path: str | None
|
|
23
|
+
latest_report_exists: bool
|
|
24
|
+
allowed_dirty_paths: list[str]
|
|
25
|
+
blocked_dirty_paths: list[str]
|
|
26
|
+
state: dict[str, Any]
|
|
27
|
+
result_status: str
|
|
28
|
+
returncode: int
|
|
29
|
+
next_action: str
|
|
30
|
+
|
|
31
|
+
def as_json_data(self) -> dict[str, Any]:
|
|
32
|
+
return {
|
|
33
|
+
"schema_version": self.schema_version,
|
|
34
|
+
"removed_transfer_dir": self.removed_transfer_dir,
|
|
35
|
+
"latest_command_run_path": self.latest_command_run_path,
|
|
36
|
+
"latest_report_exists": self.latest_report_exists,
|
|
37
|
+
"allowed_dirty_paths": self.allowed_dirty_paths,
|
|
38
|
+
"blocked_dirty_paths": self.blocked_dirty_paths,
|
|
39
|
+
"state": self.state,
|
|
40
|
+
"result_status": self.result_status,
|
|
41
|
+
"returncode": self.returncode,
|
|
42
|
+
"next_action": self.next_action,
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def _git_status_short(root: Path) -> list[str]:
|
|
47
|
+
process = subprocess.run(
|
|
48
|
+
["git", "status", "--short", "--untracked-files=all"],
|
|
49
|
+
cwd=root,
|
|
50
|
+
text=True,
|
|
51
|
+
stdout=subprocess.PIPE,
|
|
52
|
+
stderr=subprocess.PIPE,
|
|
53
|
+
check=False,
|
|
54
|
+
)
|
|
55
|
+
if process.returncode != 0:
|
|
56
|
+
raise RuntimeError(f"git status failed: {process.stderr.strip()}")
|
|
57
|
+
return [line.rstrip() for line in process.stdout.splitlines() if line.strip()]
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def _status_path(line: str) -> str:
|
|
61
|
+
# Handles normal short status lines. Rename lines are intentionally treated by their final token.
|
|
62
|
+
return line[3:].strip().split(" -> ")[-1]
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
def _is_allowed_dirty(path: str, latest_report_path: str | None, latest_command_run: str) -> bool:
|
|
66
|
+
if path == latest_command_run:
|
|
67
|
+
return True
|
|
68
|
+
if latest_report_path and path == latest_report_path:
|
|
69
|
+
return True
|
|
70
|
+
return False
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
def _read_latest_report_path(root: Path) -> tuple[str | None, bool]:
|
|
74
|
+
latest = load_workspace(root).latest_command_run_pointer()
|
|
75
|
+
if not latest.exists():
|
|
76
|
+
return None, False
|
|
77
|
+
value = latest.read_text(encoding="utf-8").strip()
|
|
78
|
+
if not value:
|
|
79
|
+
return None, False
|
|
80
|
+
report_path = value.splitlines()[-1].strip()
|
|
81
|
+
return report_path, (root / report_path).exists()
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
def closeout_transfer(project_root: Path = Path("."), remove_transfer_dir: bool = True) -> TransferCloseout:
|
|
85
|
+
root = project_root.resolve()
|
|
86
|
+
removed = False
|
|
87
|
+
|
|
88
|
+
if remove_transfer_dir and (root / TRANSFER_ROOT).exists():
|
|
89
|
+
shutil.rmtree(root / TRANSFER_ROOT)
|
|
90
|
+
removed = True
|
|
91
|
+
|
|
92
|
+
latest_report_path, latest_report_exists = _read_latest_report_path(root)
|
|
93
|
+
workspace = load_workspace(root)
|
|
94
|
+
latest_command_run = workspace.path_text(workspace.latest_command_run_pointer())
|
|
95
|
+
|
|
96
|
+
status_lines = _git_status_short(root)
|
|
97
|
+
allowed: list[str] = []
|
|
98
|
+
blocked: list[str] = []
|
|
99
|
+
for line in status_lines:
|
|
100
|
+
path = _status_path(line)
|
|
101
|
+
if _is_allowed_dirty(path, latest_report_path, latest_command_run):
|
|
102
|
+
allowed.append(path)
|
|
103
|
+
else:
|
|
104
|
+
blocked.append(path)
|
|
105
|
+
|
|
106
|
+
state = build_transfer_state(root).as_json_data()
|
|
107
|
+
|
|
108
|
+
if blocked:
|
|
109
|
+
result_status = "BLOCKED"
|
|
110
|
+
returncode = 1
|
|
111
|
+
next_action = "Review blocked dirty paths before committing or running another transfer."
|
|
112
|
+
else:
|
|
113
|
+
result_status = "PASS"
|
|
114
|
+
returncode = 0
|
|
115
|
+
next_action = "Review allowed dirty evidence paths, then run project gates before commit."
|
|
116
|
+
|
|
117
|
+
return TransferCloseout(
|
|
118
|
+
schema_version=1,
|
|
119
|
+
removed_transfer_dir=removed,
|
|
120
|
+
latest_command_run_path=latest_report_path,
|
|
121
|
+
latest_report_exists=latest_report_exists,
|
|
122
|
+
allowed_dirty_paths=allowed,
|
|
123
|
+
blocked_dirty_paths=blocked,
|
|
124
|
+
state=state,
|
|
125
|
+
result_status=result_status,
|
|
126
|
+
returncode=returncode,
|
|
127
|
+
next_action=next_action,
|
|
128
|
+
)
|
|
129
|
+
|
|
130
|
+
|
|
131
|
+
def closeout_transfer_json(project_root: Path = Path("."), remove_transfer_dir: bool = True) -> str:
|
|
132
|
+
return json.dumps(
|
|
133
|
+
closeout_transfer(project_root, remove_transfer_dir=remove_transfer_dir).as_json_data(),
|
|
134
|
+
indent=2,
|
|
135
|
+
sort_keys=True,
|
|
136
|
+
)
|