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,153 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from dataclasses import dataclass
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
import re
|
|
6
|
+
import subprocess
|
|
7
|
+
from typing import Iterable, Literal
|
|
8
|
+
|
|
9
|
+
StepClassification = Literal["PASS", "PENDING", "FAIL"]
|
|
10
|
+
|
|
11
|
+
FAIL_RE = re.compile(r"### RESULT: (FAIL|NEEDS_HUMAN_REVIEW) ###|STATUS: (FAIL|NEEDS_HUMAN_REVIEW)|RESULT: (FAIL|NEEDS_HUMAN_REVIEW)")
|
|
12
|
+
PENDING_RE = re.compile(r"### RESULT: (PENDING|WAIT) ###|STATUS: (PENDING|WAIT)|RESULT: (PENDING|WAIT)")
|
|
13
|
+
PASS_RE = re.compile(
|
|
14
|
+
r"### RESULT: (PASS|DONE|NOOP|ALREADY_ON_MAIN|ALREADY_MERGED|ALREADY_RELEASED|DOI_VERIFIED|SUPERSEDED) ###"
|
|
15
|
+
r"|STATUS: (PASS|DONE|NOOP|ALREADY_ON_MAIN|ALREADY_MERGED|ALREADY_RELEASED|DOI_VERIFIED|SUPERSEDED)"
|
|
16
|
+
r"|RESULT: (PASS|DONE|NOOP|ALREADY_ON_MAIN|ALREADY_MERGED|ALREADY_RELEASED|DOI_VERIFIED|SUPERSEDED)"
|
|
17
|
+
)
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
@dataclass(frozen=True)
|
|
21
|
+
class CommandStepResult:
|
|
22
|
+
number: int
|
|
23
|
+
command: str
|
|
24
|
+
exit_status: int
|
|
25
|
+
classification: StepClassification
|
|
26
|
+
output: str
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def classify_step_output(output: str, exit_status: int) -> StepClassification:
|
|
30
|
+
if FAIL_RE.search(output):
|
|
31
|
+
return "FAIL"
|
|
32
|
+
if PENDING_RE.search(output):
|
|
33
|
+
return "PENDING"
|
|
34
|
+
if PASS_RE.search(output):
|
|
35
|
+
return "PASS"
|
|
36
|
+
return "PASS" if exit_status == 0 else "FAIL"
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def load_plan(path: Path) -> list[str]:
|
|
40
|
+
if not path.exists():
|
|
41
|
+
raise FileNotFoundError(f"plan file not found: {path}")
|
|
42
|
+
commands: list[str] = []
|
|
43
|
+
for raw_line in path.read_text(encoding="utf-8").splitlines():
|
|
44
|
+
line = raw_line.strip()
|
|
45
|
+
if not line or line.startswith("#"):
|
|
46
|
+
continue
|
|
47
|
+
commands.append(raw_line)
|
|
48
|
+
return commands
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def run_command(repo_root: Path, command: str) -> tuple[int, str]:
|
|
52
|
+
completed = subprocess.run(
|
|
53
|
+
["sh", "-c", command],
|
|
54
|
+
cwd=repo_root,
|
|
55
|
+
text=True,
|
|
56
|
+
stdout=subprocess.PIPE,
|
|
57
|
+
stderr=subprocess.STDOUT,
|
|
58
|
+
check=False,
|
|
59
|
+
)
|
|
60
|
+
return completed.returncode, completed.stdout
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def run_steps(repo_root: Path, commands: Iterable[str]) -> tuple[int, list[CommandStepResult]]:
|
|
64
|
+
status = 0
|
|
65
|
+
results: list[CommandStepResult] = []
|
|
66
|
+
for index, command in enumerate(commands, start=1):
|
|
67
|
+
exit_status, output = run_command(repo_root, command)
|
|
68
|
+
classification = classify_step_output(output, exit_status)
|
|
69
|
+
results.append(CommandStepResult(index, command, exit_status, classification, output))
|
|
70
|
+
if classification == "PASS":
|
|
71
|
+
continue
|
|
72
|
+
status = 2 if classification == "PENDING" else 1
|
|
73
|
+
break
|
|
74
|
+
return status, results
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
def _git(repo_root: Path, args: list[str]) -> tuple[int, str]:
|
|
78
|
+
completed = subprocess.run(
|
|
79
|
+
["git", *args],
|
|
80
|
+
cwd=repo_root,
|
|
81
|
+
text=True,
|
|
82
|
+
stdout=subprocess.PIPE,
|
|
83
|
+
stderr=subprocess.STDOUT,
|
|
84
|
+
check=False,
|
|
85
|
+
)
|
|
86
|
+
return completed.returncode, completed.stdout.strip()
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
def render_run(plan_file: Path, results: list[CommandStepResult], final_status: int, branch: str, git_status: str) -> str:
|
|
90
|
+
lines = [
|
|
91
|
+
"",
|
|
92
|
+
"",
|
|
93
|
+
"",
|
|
94
|
+
"--------------------------------------------------------------------------------",
|
|
95
|
+
"--------------------------------------------------------------------------------",
|
|
96
|
+
"--------------------------------------------------------------------------------",
|
|
97
|
+
"",
|
|
98
|
+
"",
|
|
99
|
+
"",
|
|
100
|
+
"NS SLICE RUNNER",
|
|
101
|
+
"",
|
|
102
|
+
"### SAFETY ###",
|
|
103
|
+
"Safety: deterministic sequential runner; advances only after target-state PASS and stops immediately on retryable or failing states.",
|
|
104
|
+
f"plan={plan_file.as_posix()}",
|
|
105
|
+
]
|
|
106
|
+
for result in results:
|
|
107
|
+
lines.extend(["", f"### STEP {result.number} ###", result.command])
|
|
108
|
+
if result.output:
|
|
109
|
+
lines.append(result.output.rstrip())
|
|
110
|
+
lines.append(f"STEP {result.number} RESULT: {result.classification}")
|
|
111
|
+
if result.classification == "PENDING":
|
|
112
|
+
lines.append("Stopping slice runner at retryable state; no dependent follow-up actions were run.")
|
|
113
|
+
elif result.classification == "FAIL":
|
|
114
|
+
lines.append("Stopping slice runner at first failing step.")
|
|
115
|
+
lines.extend(["", "### FINAL STATE ###", branch])
|
|
116
|
+
if git_status:
|
|
117
|
+
lines.append(git_status)
|
|
118
|
+
if final_status == 0:
|
|
119
|
+
lines.append("\n### RESULT: PASS ###")
|
|
120
|
+
elif final_status == 2:
|
|
121
|
+
lines.append("\n### RESULT: PENDING ###")
|
|
122
|
+
else:
|
|
123
|
+
lines.append("\n### RESULT: FAIL ###")
|
|
124
|
+
return "\n".join(lines)
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
def run_slice(plan_file: Path, repo_root: Path) -> int:
|
|
128
|
+
try:
|
|
129
|
+
commands = load_plan(plan_file)
|
|
130
|
+
except FileNotFoundError:
|
|
131
|
+
print(f"ERROR: plan file not found: {plan_file}")
|
|
132
|
+
return 1
|
|
133
|
+
status, results = run_steps(repo_root, commands)
|
|
134
|
+
branch_rc, branch = _git(repo_root, ["branch", "--show-current"])
|
|
135
|
+
status_rc, git_status = _git(repo_root, ["status", "--short"])
|
|
136
|
+
final_status = status if branch_rc == 0 and status_rc == 0 else 1
|
|
137
|
+
print(render_run(plan_file, results, final_status, branch, git_status))
|
|
138
|
+
return final_status
|
|
139
|
+
|
|
140
|
+
|
|
141
|
+
def main(argv: list[str] | None = None) -> int:
|
|
142
|
+
args = argv if argv is not None else []
|
|
143
|
+
if not args:
|
|
144
|
+
print("ERROR: missing plan file. Usage: ./ns slice-runner <plan-file>")
|
|
145
|
+
print("Plan format: one shell command per line; blank lines and lines starting with # are ignored.")
|
|
146
|
+
return 1
|
|
147
|
+
return run_slice(Path(args[0]), Path(".").resolve())
|
|
148
|
+
|
|
149
|
+
|
|
150
|
+
if __name__ == "__main__":
|
|
151
|
+
import sys
|
|
152
|
+
|
|
153
|
+
raise SystemExit(main(sys.argv[1:]))
|
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from dataclasses import dataclass
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
import shutil
|
|
6
|
+
import subprocess
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
TRACKED_WORKFLOW_EVIDENCE: tuple[str, ...] = (
|
|
10
|
+
".agentic/workflow_state",
|
|
11
|
+
"docs/reports/CURRENT_WORKFLOW_OUTPUT.md",
|
|
12
|
+
)
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
@dataclass(frozen=True)
|
|
16
|
+
class CleanCheckResult:
|
|
17
|
+
ok: bool
|
|
18
|
+
expected_log: str
|
|
19
|
+
unexpected_lines: tuple[str, ...]
|
|
20
|
+
raw_lines: tuple[str, ...]
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
@dataclass(frozen=True)
|
|
24
|
+
class EvidenceCleanResult:
|
|
25
|
+
ok: bool
|
|
26
|
+
before_status: tuple[str, ...]
|
|
27
|
+
restore_lines: tuple[str, ...]
|
|
28
|
+
removed_tmp_evidence: bool
|
|
29
|
+
untracked_doc_reports: tuple[str, ...]
|
|
30
|
+
after_status: tuple[str, ...]
|
|
31
|
+
errors: tuple[str, ...]
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def normalize_expected_log(repo_root: Path, expected_log: str | Path) -> str:
|
|
35
|
+
expected = Path(expected_log)
|
|
36
|
+
if expected.is_absolute():
|
|
37
|
+
expected = expected.resolve().relative_to(repo_root.resolve())
|
|
38
|
+
return expected.as_posix()
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def unexpected_status_lines(status_lines: list[str] | tuple[str, ...], expected_log: str) -> tuple[str, ...]:
|
|
42
|
+
allowed = "?? " + expected_log
|
|
43
|
+
unexpected: list[str] = []
|
|
44
|
+
for line in status_lines:
|
|
45
|
+
stripped = line.rstrip()
|
|
46
|
+
if stripped == "":
|
|
47
|
+
continue
|
|
48
|
+
if stripped == allowed:
|
|
49
|
+
continue
|
|
50
|
+
unexpected.append(stripped)
|
|
51
|
+
return tuple(unexpected)
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def _run_git(repo_root: Path, args: list[str]) -> tuple[int, str]:
|
|
55
|
+
completed = subprocess.run(
|
|
56
|
+
["git", *args],
|
|
57
|
+
cwd=repo_root,
|
|
58
|
+
text=True,
|
|
59
|
+
stdout=subprocess.PIPE,
|
|
60
|
+
stderr=subprocess.STDOUT,
|
|
61
|
+
check=False,
|
|
62
|
+
)
|
|
63
|
+
return completed.returncode, completed.stdout.strip()
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
def read_git_status(repo_root: Path, *paths: str) -> tuple[str, ...]:
|
|
67
|
+
completed = subprocess.run(
|
|
68
|
+
["git", "status", "--short", *paths],
|
|
69
|
+
cwd=repo_root,
|
|
70
|
+
text=True,
|
|
71
|
+
stdout=subprocess.PIPE,
|
|
72
|
+
stderr=subprocess.STDOUT,
|
|
73
|
+
check=False,
|
|
74
|
+
)
|
|
75
|
+
if completed.returncode != 0:
|
|
76
|
+
return (completed.stdout.strip() or "git status failed",)
|
|
77
|
+
return tuple(line for line in completed.stdout.splitlines() if line.strip())
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
def check_clean_except_expected_log(repo_root: Path, expected_log: str | Path) -> CleanCheckResult:
|
|
81
|
+
normalized = normalize_expected_log(repo_root, expected_log)
|
|
82
|
+
raw = read_git_status(repo_root)
|
|
83
|
+
unexpected = unexpected_status_lines(raw, normalized)
|
|
84
|
+
return CleanCheckResult(
|
|
85
|
+
ok=len(unexpected) == 0,
|
|
86
|
+
expected_log=normalized,
|
|
87
|
+
unexpected_lines=unexpected,
|
|
88
|
+
raw_lines=raw,
|
|
89
|
+
)
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
def _path_is_tracked(repo_root: Path, path: str) -> bool:
|
|
93
|
+
returncode, _ = _run_git(repo_root, ["ls-files", "--error-unmatch", path])
|
|
94
|
+
return returncode == 0
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
def _path_is_dirty(repo_root: Path, path: str) -> bool:
|
|
98
|
+
worktree_rc, _ = _run_git(repo_root, ["diff", "--quiet", "--", path])
|
|
99
|
+
index_rc, _ = _run_git(repo_root, ["diff", "--cached", "--quiet", "--", path])
|
|
100
|
+
return worktree_rc != 0 or index_rc != 0
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
def _restore_known_tracked_workflow_evidence(repo_root: Path) -> tuple[tuple[str, ...], tuple[str, ...]]:
|
|
104
|
+
lines: list[str] = []
|
|
105
|
+
errors: list[str] = []
|
|
106
|
+
for path in TRACKED_WORKFLOW_EVIDENCE:
|
|
107
|
+
if not _path_is_tracked(repo_root, path):
|
|
108
|
+
lines.append(f"skip_untracked_path={path}")
|
|
109
|
+
continue
|
|
110
|
+
if not _path_is_dirty(repo_root, path):
|
|
111
|
+
lines.append(f"clean_tracked={path}")
|
|
112
|
+
continue
|
|
113
|
+
lines.append(f"restore_tracked={path}")
|
|
114
|
+
returncode, output = _run_git(repo_root, ["restore", "--staged", "--worktree", path])
|
|
115
|
+
if returncode != 0:
|
|
116
|
+
errors.append(output or f"git restore failed for {path}")
|
|
117
|
+
return tuple(lines), tuple(errors)
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
def _remove_ignored_tmp_evidence(repo_root: Path) -> bool:
|
|
121
|
+
target = repo_root / "tmp" / "agent-evidence"
|
|
122
|
+
if not target.exists():
|
|
123
|
+
return False
|
|
124
|
+
shutil.rmtree(target)
|
|
125
|
+
return True
|
|
126
|
+
|
|
127
|
+
|
|
128
|
+
def _untracked_doc_reports(repo_root: Path) -> tuple[str, ...]:
|
|
129
|
+
return tuple(line for line in read_git_status(repo_root, "docs/reports") if line.startswith("?? "))
|
|
130
|
+
|
|
131
|
+
|
|
132
|
+
def clean_local_evidence(repo_root: Path) -> EvidenceCleanResult:
|
|
133
|
+
before = read_git_status(repo_root)
|
|
134
|
+
restore_lines, restore_errors = _restore_known_tracked_workflow_evidence(repo_root)
|
|
135
|
+
errors = list(restore_errors)
|
|
136
|
+
try:
|
|
137
|
+
removed_tmp = _remove_ignored_tmp_evidence(repo_root)
|
|
138
|
+
except OSError as exc:
|
|
139
|
+
removed_tmp = False
|
|
140
|
+
errors.append(str(exc))
|
|
141
|
+
untracked_reports = _untracked_doc_reports(repo_root)
|
|
142
|
+
after = read_git_status(repo_root)
|
|
143
|
+
ok = not errors and not untracked_reports
|
|
144
|
+
return EvidenceCleanResult(
|
|
145
|
+
ok=ok,
|
|
146
|
+
before_status=before,
|
|
147
|
+
restore_lines=restore_lines,
|
|
148
|
+
removed_tmp_evidence=removed_tmp,
|
|
149
|
+
untracked_doc_reports=untracked_reports,
|
|
150
|
+
after_status=after,
|
|
151
|
+
errors=tuple(errors),
|
|
152
|
+
)
|
|
@@ -0,0 +1,214 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from dataclasses import dataclass
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
import subprocess
|
|
6
|
+
|
|
7
|
+
from agentic_project_kit.safe_push import safe_push
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
@dataclass(frozen=True)
|
|
11
|
+
class EvidenceCommitPathsResult:
|
|
12
|
+
success: bool
|
|
13
|
+
branch: str
|
|
14
|
+
head_before: str
|
|
15
|
+
commit_sha: str
|
|
16
|
+
paths: tuple[str, ...]
|
|
17
|
+
log_path: str | None
|
|
18
|
+
findings: tuple[str, ...]
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def _run_git(root: Path, args: list[str]) -> subprocess.CompletedProcess[str]:
|
|
22
|
+
return subprocess.run(["git", *args], cwd=root, text=True, capture_output=True, check=False)
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def _git_stdout(root: Path, args: list[str]) -> str:
|
|
26
|
+
result = _run_git(root, args)
|
|
27
|
+
if result.returncode != 0:
|
|
28
|
+
return ""
|
|
29
|
+
return result.stdout.strip()
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def _status_lines(root: Path) -> tuple[str, ...]:
|
|
33
|
+
result = _run_git(root, ["status", "--short", "--untracked-files=all"])
|
|
34
|
+
if result.returncode != 0:
|
|
35
|
+
return ()
|
|
36
|
+
return tuple(line for line in result.stdout.splitlines() if line.strip())
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def _path_is_dirty(status_line: str, rel_path: str) -> bool:
|
|
40
|
+
return status_line.strip().endswith(rel_path)
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def commit_paths(
|
|
44
|
+
*,
|
|
45
|
+
root: Path | str = Path("."),
|
|
46
|
+
paths: tuple[str, ...],
|
|
47
|
+
message: str,
|
|
48
|
+
log_path: str | None = None,
|
|
49
|
+
push: bool = False,
|
|
50
|
+
required_branch: str = "",
|
|
51
|
+
allow_main: bool = False,
|
|
52
|
+
) -> EvidenceCommitPathsResult:
|
|
53
|
+
root_path = Path(root).resolve()
|
|
54
|
+
if not paths:
|
|
55
|
+
raise ValueError("at least one path is required")
|
|
56
|
+
if log_path:
|
|
57
|
+
if Path(log_path).is_absolute():
|
|
58
|
+
raise ValueError("log_path must be repository-relative")
|
|
59
|
+
if not log_path.startswith("docs/reports/terminal/"):
|
|
60
|
+
raise ValueError("log_path must start with docs/reports/terminal/")
|
|
61
|
+
if log_path not in paths:
|
|
62
|
+
paths = (*paths, log_path)
|
|
63
|
+
|
|
64
|
+
normalized_paths = tuple(dict.fromkeys(paths))
|
|
65
|
+
for rel in normalized_paths:
|
|
66
|
+
if Path(rel).is_absolute():
|
|
67
|
+
raise ValueError(f"path must be repository-relative: {rel}")
|
|
68
|
+
if rel.startswith(".git/") or rel == ".git":
|
|
69
|
+
raise ValueError(".git paths are not allowed")
|
|
70
|
+
if rel.startswith(".venv/") or rel == ".venv":
|
|
71
|
+
raise ValueError(".venv paths are not allowed")
|
|
72
|
+
|
|
73
|
+
branch = _git_stdout(root_path, ["branch", "--show-current"]) or "UNKNOWN"
|
|
74
|
+
if required_branch and branch != required_branch:
|
|
75
|
+
raise ValueError(f"current branch {branch!r} does not match required branch {required_branch!r}")
|
|
76
|
+
if branch == "main" and not allow_main:
|
|
77
|
+
raise ValueError("evidence commit-paths refuses to commit on main without --allow-main")
|
|
78
|
+
head_before = _git_stdout(root_path, ["rev-parse", "HEAD"]) or "UNKNOWN"
|
|
79
|
+
status_before = _status_lines(root_path)
|
|
80
|
+
|
|
81
|
+
if push:
|
|
82
|
+
push_preflight = safe_push(
|
|
83
|
+
root_path,
|
|
84
|
+
target_branch=branch,
|
|
85
|
+
purpose="evidence commit-paths publish branch",
|
|
86
|
+
dry_run=True,
|
|
87
|
+
)
|
|
88
|
+
if not push_preflight.ok:
|
|
89
|
+
return EvidenceCommitPathsResult(
|
|
90
|
+
False,
|
|
91
|
+
branch,
|
|
92
|
+
head_before,
|
|
93
|
+
"",
|
|
94
|
+
normalized_paths,
|
|
95
|
+
log_path,
|
|
96
|
+
tuple(["push preflight blocked evidence commit", *push_preflight.reasons]),
|
|
97
|
+
)
|
|
98
|
+
|
|
99
|
+
unexpected = [
|
|
100
|
+
line for line in status_before
|
|
101
|
+
if not any(_path_is_dirty(line, rel) for rel in normalized_paths)
|
|
102
|
+
]
|
|
103
|
+
if unexpected:
|
|
104
|
+
return EvidenceCommitPathsResult(
|
|
105
|
+
False,
|
|
106
|
+
branch,
|
|
107
|
+
head_before,
|
|
108
|
+
"",
|
|
109
|
+
normalized_paths,
|
|
110
|
+
log_path,
|
|
111
|
+
tuple(["unexpected dirty paths before commit", *unexpected]),
|
|
112
|
+
)
|
|
113
|
+
|
|
114
|
+
deletion_lines = {line[3:] for line in status_before if line.startswith(" D ") or line.startswith("D ")}
|
|
115
|
+
missing = [
|
|
116
|
+
rel for rel in normalized_paths
|
|
117
|
+
if not (root_path / rel).exists() and rel not in deletion_lines
|
|
118
|
+
]
|
|
119
|
+
if missing:
|
|
120
|
+
return EvidenceCommitPathsResult(
|
|
121
|
+
False,
|
|
122
|
+
branch,
|
|
123
|
+
head_before,
|
|
124
|
+
"",
|
|
125
|
+
normalized_paths,
|
|
126
|
+
log_path,
|
|
127
|
+
tuple(f"missing path: {rel}" for rel in missing),
|
|
128
|
+
)
|
|
129
|
+
|
|
130
|
+
add_result = _run_git(root_path, ["add", "-A", "--", *normalized_paths])
|
|
131
|
+
if add_result.returncode != 0:
|
|
132
|
+
return EvidenceCommitPathsResult(
|
|
133
|
+
False,
|
|
134
|
+
branch,
|
|
135
|
+
head_before,
|
|
136
|
+
"",
|
|
137
|
+
normalized_paths,
|
|
138
|
+
log_path,
|
|
139
|
+
(add_result.stderr.strip() or add_result.stdout.strip() or "git add failed",),
|
|
140
|
+
)
|
|
141
|
+
|
|
142
|
+
diff_result = _run_git(root_path, ["diff", "--cached", "--quiet"])
|
|
143
|
+
if diff_result.returncode == 0:
|
|
144
|
+
return EvidenceCommitPathsResult(
|
|
145
|
+
True,
|
|
146
|
+
branch,
|
|
147
|
+
head_before,
|
|
148
|
+
head_before,
|
|
149
|
+
normalized_paths,
|
|
150
|
+
log_path,
|
|
151
|
+
("no staged diff; no commit created",),
|
|
152
|
+
)
|
|
153
|
+
|
|
154
|
+
commit_result = _run_git(root_path, ["commit", "-m", message])
|
|
155
|
+
if commit_result.returncode != 0:
|
|
156
|
+
return EvidenceCommitPathsResult(
|
|
157
|
+
False,
|
|
158
|
+
branch,
|
|
159
|
+
head_before,
|
|
160
|
+
"",
|
|
161
|
+
normalized_paths,
|
|
162
|
+
log_path,
|
|
163
|
+
(commit_result.stderr.strip() or commit_result.stdout.strip() or "git commit failed",),
|
|
164
|
+
)
|
|
165
|
+
|
|
166
|
+
commit_sha = _git_stdout(root_path, ["rev-parse", "HEAD"]) or "UNKNOWN"
|
|
167
|
+
if push:
|
|
168
|
+
push_result = safe_push(
|
|
169
|
+
root_path,
|
|
170
|
+
target_branch=branch,
|
|
171
|
+
purpose="evidence commit-paths publish branch",
|
|
172
|
+
)
|
|
173
|
+
if not push_result.ok:
|
|
174
|
+
return EvidenceCommitPathsResult(
|
|
175
|
+
False,
|
|
176
|
+
branch,
|
|
177
|
+
head_before,
|
|
178
|
+
commit_sha,
|
|
179
|
+
normalized_paths,
|
|
180
|
+
log_path,
|
|
181
|
+
(push_result.stderr.strip() or push_result.stdout.strip() or "git push failed",),
|
|
182
|
+
)
|
|
183
|
+
|
|
184
|
+
status_after = _status_lines(root_path)
|
|
185
|
+
if status_after:
|
|
186
|
+
return EvidenceCommitPathsResult(
|
|
187
|
+
False,
|
|
188
|
+
branch,
|
|
189
|
+
head_before,
|
|
190
|
+
commit_sha,
|
|
191
|
+
normalized_paths,
|
|
192
|
+
log_path,
|
|
193
|
+
tuple(["dirty worktree after commit", *status_after]),
|
|
194
|
+
)
|
|
195
|
+
|
|
196
|
+
return EvidenceCommitPathsResult(True, branch, head_before, commit_sha, normalized_paths, log_path, ())
|
|
197
|
+
|
|
198
|
+
|
|
199
|
+
def render_commit_paths_result(result: EvidenceCommitPathsResult) -> str:
|
|
200
|
+
lines = [
|
|
201
|
+
"EVIDENCE_COMMIT_PATHS",
|
|
202
|
+
f"success={'yes' if result.success else 'no'}",
|
|
203
|
+
f"branch={result.branch}",
|
|
204
|
+
f"head_before={result.head_before}",
|
|
205
|
+
f"commit_sha={result.commit_sha or 'NONE'}",
|
|
206
|
+
]
|
|
207
|
+
if result.log_path:
|
|
208
|
+
lines.append(f"log_path={result.log_path}")
|
|
209
|
+
for path in result.paths:
|
|
210
|
+
lines.append(f"path={path}")
|
|
211
|
+
for finding in result.findings:
|
|
212
|
+
lines.append(f"finding={finding}")
|
|
213
|
+
lines.append(f"### RESULT: {'PASS' if result.success else 'FAIL'} ###")
|
|
214
|
+
return "\n".join(lines)
|