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,516 @@
|
|
|
1
|
+
"""Repository-backed command runner for agent handoff.
|
|
2
|
+
|
|
3
|
+
The runner executes exactly one repo-local command file described by
|
|
4
|
+
.agentic/commands/current.yaml and .agentic/commands/current.sh.
|
|
5
|
+
|
|
6
|
+
It is intentionally conservative:
|
|
7
|
+
- no command file means no-op failure,
|
|
8
|
+
- duplicate command_id is refused,
|
|
9
|
+
- shell syntax is checked before execution,
|
|
10
|
+
- execution is delegated to terminal_logging.run_logged(),
|
|
11
|
+
- reports are written under docs/reports/command_runs/,
|
|
12
|
+
- executed command ids are tracked in .agentic/commands/executed.jsonl.
|
|
13
|
+
"""
|
|
14
|
+
|
|
15
|
+
from __future__ import annotations
|
|
16
|
+
|
|
17
|
+
import dataclasses
|
|
18
|
+
import datetime as _dt
|
|
19
|
+
import hashlib
|
|
20
|
+
import json
|
|
21
|
+
import os
|
|
22
|
+
import subprocess
|
|
23
|
+
import sys
|
|
24
|
+
from pathlib import Path
|
|
25
|
+
|
|
26
|
+
from agentic_project_kit.safe_push import safe_push
|
|
27
|
+
|
|
28
|
+
from agentic_project_kit import terminal_logging
|
|
29
|
+
from agentic_project_kit.workspace import LEGACY_DEFAULTS, load_workspace
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def _clean_git_env() -> dict[str, str]:
|
|
33
|
+
env = os.environ.copy()
|
|
34
|
+
for key in ("GIT_DIR", "GIT_WORK_TREE", "GIT_INDEX_FILE", "GIT_OBJECT_DIRECTORY", "GIT_ALTERNATE_OBJECT_DIRECTORIES"):
|
|
35
|
+
env.pop(key, None)
|
|
36
|
+
return env
|
|
37
|
+
|
|
38
|
+
COMMAND_DIR = Path(".agentic/commands")
|
|
39
|
+
CURRENT_YAML = COMMAND_DIR / "current.yaml"
|
|
40
|
+
CURRENT_SCRIPT = COMMAND_DIR / "current.sh"
|
|
41
|
+
INBOX_DIR = COMMAND_DIR / "inbox"
|
|
42
|
+
EXECUTED_JSONL = COMMAND_DIR / "executed.jsonl"
|
|
43
|
+
REPORT_DIR = Path(LEGACY_DEFAULTS.command_runs_root)
|
|
44
|
+
LATEST_COMMAND_RUN_POINTER = REPORT_DIR / "LATEST_COMMAND_RUN.txt"
|
|
45
|
+
|
|
46
|
+
ALLOWED_SAFETY_CLASSES = {
|
|
47
|
+
"read-only",
|
|
48
|
+
"local-only",
|
|
49
|
+
"remote-mutation", "local-verification",
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def normalize_safety_class(value: str) -> str:
|
|
54
|
+
if value == "local-verification":
|
|
55
|
+
return "local-only"
|
|
56
|
+
return value
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
OUTCOME_PASS_EXECUTED = "PASS_EXECUTED"
|
|
60
|
+
OUTCOME_FAIL_NO_COMMAND = "FAIL_NO_COMMAND"
|
|
61
|
+
OUTCOME_FAIL_ALREADY_EXECUTED = "FAIL_ALREADY_EXECUTED"
|
|
62
|
+
OUTCOME_FAIL_INVALID_COMMAND = "FAIL_INVALID_COMMAND"
|
|
63
|
+
OUTCOME_FAIL_SHELL_SYNTAX = "FAIL_SHELL_SYNTAX"
|
|
64
|
+
OUTCOME_FAIL_COMMAND = "FAIL_COMMAND"
|
|
65
|
+
OUTCOME_FAIL_UPLOAD = "FAIL_UPLOAD"
|
|
66
|
+
OUTCOME_FAIL_AMBIGUOUS_COMMANDS = "FAIL_AMBIGUOUS_COMMANDS"
|
|
67
|
+
OUTCOME_FAIL_PULL = "FAIL_PULL"
|
|
68
|
+
OUTCOME_FAIL_POSTCONDITION = "FAIL_POSTCONDITION"
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
@dataclasses.dataclass(frozen=True)
|
|
72
|
+
class AgentCommand:
|
|
73
|
+
command_id: str
|
|
74
|
+
title: str
|
|
75
|
+
safety_class: str
|
|
76
|
+
expected_branch: str | None = None
|
|
77
|
+
description: str = ""
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
def _parse_simple_yaml(path: Path) -> dict[str, str]:
|
|
81
|
+
data: dict[str, str] = {}
|
|
82
|
+
for raw in path.read_text(encoding="utf-8").splitlines():
|
|
83
|
+
line = raw.strip()
|
|
84
|
+
if not line or line.startswith("#"):
|
|
85
|
+
continue
|
|
86
|
+
if ":" not in line:
|
|
87
|
+
raise ValueError(f"Invalid YAML line without colon: {raw}")
|
|
88
|
+
key, value = line.split(":", 1)
|
|
89
|
+
key = key.strip()
|
|
90
|
+
value = value.strip()
|
|
91
|
+
if not key:
|
|
92
|
+
raise ValueError(f"Invalid YAML line without key: {raw}")
|
|
93
|
+
if (value.startswith("\'") and value.endswith("\'")) or (value.startswith(chr(34)) and value.endswith(chr(34))):
|
|
94
|
+
value = value[1:-1]
|
|
95
|
+
data[key] = value
|
|
96
|
+
return data
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
def inbox_yaml_files() -> list[Path]:
|
|
101
|
+
if not INBOX_DIR.exists():
|
|
102
|
+
return []
|
|
103
|
+
return sorted(INBOX_DIR.glob("*.yaml"))
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
def pending_inbox_command_pair() -> tuple[Path, Path]:
|
|
107
|
+
pairs: list[tuple[Path, Path]] = []
|
|
108
|
+
for yaml_path in inbox_yaml_files():
|
|
109
|
+
script_path = yaml_path.with_suffix(".sh")
|
|
110
|
+
if script_path.exists():
|
|
111
|
+
pairs.append((yaml_path, script_path))
|
|
112
|
+
if not pairs:
|
|
113
|
+
raise FileNotFoundError("No complete pending command pair in .agentic/commands/inbox")
|
|
114
|
+
if len(pairs) > 1:
|
|
115
|
+
names = ", ".join(path.name for path, _ in pairs)
|
|
116
|
+
raise RuntimeError("Multiple pending commands: " + names)
|
|
117
|
+
return pairs[0]
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
def prepare_current_from_inbox() -> tuple[Path, Path]:
|
|
121
|
+
yaml_path, script_path = pending_inbox_command_pair()
|
|
122
|
+
COMMAND_DIR.mkdir(parents=True, exist_ok=True)
|
|
123
|
+
CURRENT_YAML.write_text(yaml_path.read_text(encoding="utf-8"), encoding="utf-8")
|
|
124
|
+
CURRENT_SCRIPT.write_text(script_path.read_text(encoding="utf-8"), encoding="utf-8")
|
|
125
|
+
CURRENT_SCRIPT.chmod(0o755)
|
|
126
|
+
yaml_path.unlink()
|
|
127
|
+
script_path.unlink()
|
|
128
|
+
return yaml_path, script_path
|
|
129
|
+
|
|
130
|
+
def remove_current_files() -> None:
|
|
131
|
+
for item in (CURRENT_YAML, CURRENT_SCRIPT):
|
|
132
|
+
if item.exists():
|
|
133
|
+
item.unlink()
|
|
134
|
+
|
|
135
|
+
def load_current_command() -> AgentCommand:
|
|
136
|
+
if not CURRENT_YAML.exists() or not CURRENT_SCRIPT.exists():
|
|
137
|
+
raise FileNotFoundError("Missing .agentic/commands/current.yaml or current.sh")
|
|
138
|
+
data = _parse_simple_yaml(CURRENT_YAML)
|
|
139
|
+
required = ("command_id", "title", "safety_class")
|
|
140
|
+
missing = [key for key in required if not data.get(key)]
|
|
141
|
+
if missing:
|
|
142
|
+
raise ValueError("Missing required command fields: " + ", ".join(missing))
|
|
143
|
+
safety = data["safety_class"]
|
|
144
|
+
safety = normalize_safety_class(safety)
|
|
145
|
+
if safety not in ALLOWED_SAFETY_CLASSES:
|
|
146
|
+
raise ValueError(f"Unsupported safety_class: {safety}")
|
|
147
|
+
return AgentCommand(
|
|
148
|
+
command_id=data["command_id"],
|
|
149
|
+
title=data["title"],
|
|
150
|
+
safety_class=safety,
|
|
151
|
+
expected_branch=data.get("expected_branch") or None,
|
|
152
|
+
description=data.get("description", ""),
|
|
153
|
+
)
|
|
154
|
+
|
|
155
|
+
|
|
156
|
+
def read_executed_ids() -> set[str]:
|
|
157
|
+
if not EXECUTED_JSONL.exists():
|
|
158
|
+
return set()
|
|
159
|
+
ids: set[str] = set()
|
|
160
|
+
for raw in EXECUTED_JSONL.read_text(encoding="utf-8").splitlines():
|
|
161
|
+
if not raw.strip():
|
|
162
|
+
continue
|
|
163
|
+
try:
|
|
164
|
+
item = json.loads(raw)
|
|
165
|
+
except json.JSONDecodeError:
|
|
166
|
+
continue
|
|
167
|
+
command_id = item.get("command_id")
|
|
168
|
+
if isinstance(command_id, str):
|
|
169
|
+
ids.add(command_id)
|
|
170
|
+
return ids
|
|
171
|
+
|
|
172
|
+
|
|
173
|
+
def script_sha256(path: Path = CURRENT_SCRIPT) -> str:
|
|
174
|
+
if not path.exists():
|
|
175
|
+
return "missing"
|
|
176
|
+
return hashlib.sha256(path.read_bytes()).hexdigest()
|
|
177
|
+
|
|
178
|
+
|
|
179
|
+
def current_branch() -> str:
|
|
180
|
+
# Do not let inherited Git environment or an enclosing repository leak into
|
|
181
|
+
# temporary test directories. For this runner, the current directory must
|
|
182
|
+
# itself be a Git worktree root or an unknown branch is safer.
|
|
183
|
+
cwd = Path.cwd().resolve()
|
|
184
|
+
if not (cwd / ".git").exists():
|
|
185
|
+
return "unknown"
|
|
186
|
+
|
|
187
|
+
inside = subprocess.run(
|
|
188
|
+
["git", "rev-parse", "--is-inside-work-tree"],
|
|
189
|
+
text=True,
|
|
190
|
+
capture_output=True,
|
|
191
|
+
check=False,
|
|
192
|
+
env=_clean_git_env(),
|
|
193
|
+
)
|
|
194
|
+
if inside.returncode != 0 or inside.stdout.strip() != "true":
|
|
195
|
+
return "unknown"
|
|
196
|
+
|
|
197
|
+
top_level = subprocess.run(
|
|
198
|
+
["git", "rev-parse", "--show-toplevel"],
|
|
199
|
+
text=True,
|
|
200
|
+
capture_output=True,
|
|
201
|
+
check=False,
|
|
202
|
+
env=_clean_git_env(),
|
|
203
|
+
)
|
|
204
|
+
if top_level.returncode != 0 or Path(top_level.stdout.strip()).resolve() != cwd:
|
|
205
|
+
return "unknown"
|
|
206
|
+
|
|
207
|
+
proc = subprocess.run(
|
|
208
|
+
["git", "branch", "--show-current"],
|
|
209
|
+
text=True,
|
|
210
|
+
capture_output=True,
|
|
211
|
+
check=False,
|
|
212
|
+
env=_clean_git_env(),
|
|
213
|
+
)
|
|
214
|
+
if proc.returncode != 0:
|
|
215
|
+
return "unknown"
|
|
216
|
+
return proc.stdout.strip() or "unknown"
|
|
217
|
+
|
|
218
|
+
|
|
219
|
+
def validate_command(command: AgentCommand) -> tuple[str, str]:
|
|
220
|
+
if command.command_id in read_executed_ids():
|
|
221
|
+
return OUTCOME_FAIL_ALREADY_EXECUTED, command.command_id
|
|
222
|
+
if command.expected_branch and command.expected_branch != current_branch():
|
|
223
|
+
return OUTCOME_FAIL_INVALID_COMMAND, f"Expected branch {command.expected_branch}, got {current_branch()}"
|
|
224
|
+
syntax = subprocess.run(["sh", "-n", CURRENT_SCRIPT.as_posix()], text=True, capture_output=True, check=False)
|
|
225
|
+
if syntax.returncode != 0:
|
|
226
|
+
return OUTCOME_FAIL_SHELL_SYNTAX, syntax.stderr.strip() or syntax.stdout.strip()
|
|
227
|
+
return OUTCOME_PASS_EXECUTED, "valid"
|
|
228
|
+
|
|
229
|
+
|
|
230
|
+
def _report_dir(project_root: Path = Path(".")) -> Path:
|
|
231
|
+
if REPORT_DIR != Path(LEGACY_DEFAULTS.command_runs_root):
|
|
232
|
+
return REPORT_DIR
|
|
233
|
+
return load_workspace(project_root).command_runs_dir()
|
|
234
|
+
|
|
235
|
+
|
|
236
|
+
def _latest_command_run_pointer(project_root: Path = Path(".")) -> Path:
|
|
237
|
+
if LATEST_COMMAND_RUN_POINTER != REPORT_DIR / "LATEST_COMMAND_RUN.txt":
|
|
238
|
+
return LATEST_COMMAND_RUN_POINTER
|
|
239
|
+
return load_workspace(project_root).latest_command_run_pointer()
|
|
240
|
+
|
|
241
|
+
|
|
242
|
+
def report_path(command_id: str, project_root: Path = Path(".")) -> Path:
|
|
243
|
+
safe = "".join(ch if ch.isalnum() or ch in "-_" else "_" for ch in command_id)
|
|
244
|
+
return _report_dir(project_root) / f"{safe}.md"
|
|
245
|
+
|
|
246
|
+
|
|
247
|
+
def write_report(command: AgentCommand, outcome: str, exit_code: int, log_path: Path | None, detail: str = "") -> Path:
|
|
248
|
+
report_dir = _report_dir()
|
|
249
|
+
report_dir.mkdir(parents=True, exist_ok=True)
|
|
250
|
+
path = report_path(command.command_id)
|
|
251
|
+
lines = [
|
|
252
|
+
f"# Agent command run: {command.command_id}",
|
|
253
|
+
"",
|
|
254
|
+
f"- Title: {command.title}",
|
|
255
|
+
f"- Safety class: {command.safety_class}",
|
|
256
|
+
f"- Outcome: {outcome}",
|
|
257
|
+
f"- Exit code: {exit_code}",
|
|
258
|
+
f"- Branch: {current_branch()}",
|
|
259
|
+
f"- Script SHA256: {script_sha256()}",
|
|
260
|
+
]
|
|
261
|
+
if log_path is not None:
|
|
262
|
+
lines.append(f"- Terminal log: {log_path.as_posix()}")
|
|
263
|
+
if detail:
|
|
264
|
+
lines.extend(["", "## Detail", "", detail])
|
|
265
|
+
path.write_text("\n".join(lines) + "\n", encoding="utf-8")
|
|
266
|
+
latest = _latest_command_run_pointer()
|
|
267
|
+
latest.parent.mkdir(parents=True, exist_ok=True)
|
|
268
|
+
latest.write_text(path.as_posix() + "\n", encoding="utf-8")
|
|
269
|
+
return path
|
|
270
|
+
|
|
271
|
+
|
|
272
|
+
def append_executed(command: AgentCommand, outcome: str, exit_code: int) -> None:
|
|
273
|
+
COMMAND_DIR.mkdir(parents=True, exist_ok=True)
|
|
274
|
+
entry = {
|
|
275
|
+
"command_id": command.command_id,
|
|
276
|
+
"outcome": outcome,
|
|
277
|
+
"exit_code": exit_code,
|
|
278
|
+
"script_sha256": script_sha256(),
|
|
279
|
+
"timestamp_utc": _dt.datetime.now(_dt.UTC).isoformat(),
|
|
280
|
+
}
|
|
281
|
+
with EXECUTED_JSONL.open("a", encoding="utf-8") as handle:
|
|
282
|
+
handle.write(json.dumps(entry, sort_keys=True) + "\n")
|
|
283
|
+
|
|
284
|
+
|
|
285
|
+
|
|
286
|
+
def _git_path_is_tracked(path: Path) -> bool:
|
|
287
|
+
result = subprocess.run(
|
|
288
|
+
["git", "ls-files", "--error-unmatch", path.as_posix()],
|
|
289
|
+
stdout=subprocess.DEVNULL,
|
|
290
|
+
stderr=subprocess.DEVNULL,
|
|
291
|
+
check=False,
|
|
292
|
+
)
|
|
293
|
+
return result.returncode == 0
|
|
294
|
+
|
|
295
|
+
|
|
296
|
+
def stage_commit_push(paths: list[Path], message: str, *, required_branch: str = "", allow_main: bool = False) -> int:
|
|
297
|
+
branch = current_branch()
|
|
298
|
+
if required_branch and branch != required_branch:
|
|
299
|
+
print(f"FAIL_BRANCH_MISMATCH actual={branch} required={required_branch}")
|
|
300
|
+
return 1
|
|
301
|
+
if branch == "main" and not allow_main:
|
|
302
|
+
print("FAIL_MAIN_MUTATION_NOT_ALLOWED")
|
|
303
|
+
return 1
|
|
304
|
+
if branch in {"", "unknown"}:
|
|
305
|
+
print("FAIL_NO_CURRENT_BRANCH")
|
|
306
|
+
return 1
|
|
307
|
+
push_preflight = safe_push(
|
|
308
|
+
Path("."),
|
|
309
|
+
target_branch=branch,
|
|
310
|
+
purpose="agent command runner stage commit push",
|
|
311
|
+
set_upstream=True,
|
|
312
|
+
dry_run=True,
|
|
313
|
+
)
|
|
314
|
+
if not push_preflight.ok:
|
|
315
|
+
print("FAIL_PUSH_PREFLIGHT_BLOCKED")
|
|
316
|
+
for reason in push_preflight.reasons:
|
|
317
|
+
print(reason)
|
|
318
|
+
return push_preflight.returncode
|
|
319
|
+
add_paths = [path for path in paths if path.exists() or _git_path_is_tracked(path)]
|
|
320
|
+
if add_paths:
|
|
321
|
+
subprocess.run(["git", "add", *[path.as_posix() for path in add_paths]], check=True)
|
|
322
|
+
subprocess.run(["git", "commit", "-m", message], check=True)
|
|
323
|
+
push = safe_push(
|
|
324
|
+
Path("."),
|
|
325
|
+
target_branch=branch,
|
|
326
|
+
purpose="agent command runner stage commit push",
|
|
327
|
+
set_upstream=True,
|
|
328
|
+
)
|
|
329
|
+
if not push.ok:
|
|
330
|
+
print(push.stderr or push.stdout or "FAIL_PUSH_FAILED")
|
|
331
|
+
return push.returncode
|
|
332
|
+
|
|
333
|
+
def logged_script_has_fail_result_marker(log_path: Path | None) -> bool:
|
|
334
|
+
if log_path is None or not log_path.exists():
|
|
335
|
+
return False
|
|
336
|
+
text = log_path.read_text(encoding="utf-8", errors="replace")
|
|
337
|
+
fail_pos = text.rfind("### RESULT: FAIL ###")
|
|
338
|
+
pass_pos = text.rfind("### RESULT: PASS ###")
|
|
339
|
+
return fail_pos != -1 and fail_pos > pass_pos
|
|
340
|
+
|
|
341
|
+
|
|
342
|
+
def agent_run(extra_upload_paths: list[Path] | None = None) -> int:
|
|
343
|
+
extra_upload_paths = list(extra_upload_paths or [])
|
|
344
|
+
try:
|
|
345
|
+
command = load_current_command()
|
|
346
|
+
except (FileNotFoundError, ValueError) as exc:
|
|
347
|
+
print(OUTCOME_FAIL_NO_COMMAND if isinstance(exc, FileNotFoundError) else OUTCOME_FAIL_INVALID_COMMAND)
|
|
348
|
+
print(str(exc))
|
|
349
|
+
return 1
|
|
350
|
+
|
|
351
|
+
validation_outcome, validation_detail = validate_command(command)
|
|
352
|
+
if validation_outcome == OUTCOME_FAIL_ALREADY_EXECUTED:
|
|
353
|
+
print(validation_outcome)
|
|
354
|
+
print(validation_detail)
|
|
355
|
+
write_report(command, validation_outcome, 0, None, validation_detail)
|
|
356
|
+
return 0
|
|
357
|
+
if validation_outcome != OUTCOME_PASS_EXECUTED:
|
|
358
|
+
print(validation_outcome)
|
|
359
|
+
print(validation_detail)
|
|
360
|
+
write_report(command, validation_outcome, 1, None, validation_detail)
|
|
361
|
+
return 1
|
|
362
|
+
|
|
363
|
+
print(f"COMMAND_ID={command.command_id}")
|
|
364
|
+
print(f"TITLE={command.title}")
|
|
365
|
+
print(f"SAFETY_CLASS={command.safety_class}")
|
|
366
|
+
|
|
367
|
+
exit_code = terminal_logging.run_logged(command.command_id, ["sh", CURRENT_SCRIPT.as_posix()])
|
|
368
|
+
log_path = terminal_logging.read_latest_pointer()
|
|
369
|
+
fail_marker = logged_script_has_fail_result_marker(log_path)
|
|
370
|
+
outcome = OUTCOME_PASS_EXECUTED if exit_code == 0 and not fail_marker else OUTCOME_FAIL_COMMAND
|
|
371
|
+
if fail_marker and exit_code == 0:
|
|
372
|
+
exit_code = 1
|
|
373
|
+
|
|
374
|
+
report = write_report(command, outcome, exit_code, log_path)
|
|
375
|
+
append_executed(command, outcome, exit_code)
|
|
376
|
+
|
|
377
|
+
upload_paths = [report, _latest_command_run_pointer(), EXECUTED_JSONL, *extra_upload_paths]
|
|
378
|
+
if log_path is not None:
|
|
379
|
+
upload_paths.append(log_path)
|
|
380
|
+
upload_paths.append(terminal_logging.LATEST_POINTER)
|
|
381
|
+
|
|
382
|
+
pushed = stage_commit_push(
|
|
383
|
+
upload_paths,
|
|
384
|
+
f"Record agent command run {command.command_id}",
|
|
385
|
+
required_branch=command.expected_branch or "",
|
|
386
|
+
)
|
|
387
|
+
if pushed != 0:
|
|
388
|
+
print(OUTCOME_FAIL_UPLOAD)
|
|
389
|
+
return pushed
|
|
390
|
+
|
|
391
|
+
print(outcome)
|
|
392
|
+
return 0 if outcome == OUTCOME_PASS_EXECUTED else exit_code or 1
|
|
393
|
+
|
|
394
|
+
|
|
395
|
+
|
|
396
|
+
|
|
397
|
+
def print_agent_next_footer(outcome: str, reply: str, reason: str = "") -> None:
|
|
398
|
+
print("")
|
|
399
|
+
print(f"### AGENT-NEXT RESULT: {outcome} ###")
|
|
400
|
+
print(f"reply={reply}")
|
|
401
|
+
if reply == "p":
|
|
402
|
+
print("### NEXT CHAT REPLY: PASS -> p ###")
|
|
403
|
+
elif reply == "f":
|
|
404
|
+
print("### NEXT CHAT REPLY: FAIL -> f ###")
|
|
405
|
+
elif reply == "paste-output":
|
|
406
|
+
print("### NEXT CHAT REPLY: HARD-FAIL -> paste output ###")
|
|
407
|
+
else:
|
|
408
|
+
print("### NEXT CHAT REPLY: NO-COMMAND -> ask agent to queue command ###")
|
|
409
|
+
if reason:
|
|
410
|
+
print(f"reason={reason}")
|
|
411
|
+
latest_command_run = _latest_command_run_pointer()
|
|
412
|
+
if latest_command_run.exists():
|
|
413
|
+
print(f"latest_command_run={latest_command_run.as_posix()}")
|
|
414
|
+
print("### END AGENT-NEXT RESULT ###")
|
|
415
|
+
if outcome == "PASS":
|
|
416
|
+
work_result = "PASS"
|
|
417
|
+
evidence_result = "PASS"
|
|
418
|
+
overall_result = "PASS"
|
|
419
|
+
elif outcome == "FAIL":
|
|
420
|
+
work_result = "FAIL"
|
|
421
|
+
evidence_result = "PASS"
|
|
422
|
+
overall_result = "FAIL"
|
|
423
|
+
elif outcome == "NO-COMMAND":
|
|
424
|
+
work_result = "NO-COMMAND"
|
|
425
|
+
evidence_result = "PASS"
|
|
426
|
+
overall_result = "NO-COMMAND"
|
|
427
|
+
else:
|
|
428
|
+
work_result = "HARD-FAIL"
|
|
429
|
+
evidence_result = "FAIL"
|
|
430
|
+
overall_result = "HARD-FAIL"
|
|
431
|
+
print("================================================================")
|
|
432
|
+
print("SUMMARY")
|
|
433
|
+
print(f"WORK RESULT: {work_result}")
|
|
434
|
+
print(f"EVIDENCE RESULT: {evidence_result}")
|
|
435
|
+
print(f"OVERALL RESULT: {overall_result}")
|
|
436
|
+
if reason:
|
|
437
|
+
print(f"REASON: {reason}")
|
|
438
|
+
print(f"NEXT CHAT REPLY: {reply}")
|
|
439
|
+
print("================================================================")
|
|
440
|
+
|
|
441
|
+
def git_porcelain_paths() -> list[str]:
|
|
442
|
+
proc = subprocess.run(["git", "status", "--porcelain"], text=True, capture_output=True, check=False)
|
|
443
|
+
paths: list[str] = []
|
|
444
|
+
for raw in proc.stdout.splitlines():
|
|
445
|
+
if raw.strip():
|
|
446
|
+
paths.append(raw[3:])
|
|
447
|
+
return paths
|
|
448
|
+
|
|
449
|
+
|
|
450
|
+
def agent_next_postcondition_failures() -> list[str]:
|
|
451
|
+
failures: list[str] = []
|
|
452
|
+
for item in (CURRENT_YAML, CURRENT_SCRIPT):
|
|
453
|
+
if item.exists():
|
|
454
|
+
failures.append("active current file remains: " + item.as_posix())
|
|
455
|
+
complete_pairs: list[str] = []
|
|
456
|
+
if INBOX_DIR.exists():
|
|
457
|
+
for yaml_path in inbox_yaml_files():
|
|
458
|
+
if yaml_path.with_suffix(".sh").exists():
|
|
459
|
+
complete_pairs.append(yaml_path.name)
|
|
460
|
+
if complete_pairs:
|
|
461
|
+
failures.append("complete inbox command remains: " + ", ".join(complete_pairs))
|
|
462
|
+
unstaged_deletions = [path for path in git_porcelain_paths() if path.startswith(".agentic/commands/inbox/")]
|
|
463
|
+
if unstaged_deletions:
|
|
464
|
+
failures.append("inbox path still dirty after run: " + ", ".join(unstaged_deletions))
|
|
465
|
+
return failures
|
|
466
|
+
|
|
467
|
+
|
|
468
|
+
def git_pull_ff_only() -> int:
|
|
469
|
+
branch = current_branch()
|
|
470
|
+
return subprocess.run(["git", "pull", "--ff-only", "origin", branch], check=False).returncode
|
|
471
|
+
|
|
472
|
+
|
|
473
|
+
def agent_next() -> int:
|
|
474
|
+
pulled = git_pull_ff_only()
|
|
475
|
+
if pulled != 0:
|
|
476
|
+
print(OUTCOME_FAIL_PULL)
|
|
477
|
+
print_agent_next_footer("HARD-FAIL", "paste-output", OUTCOME_FAIL_PULL)
|
|
478
|
+
return pulled
|
|
479
|
+
try:
|
|
480
|
+
consumed_paths = list(prepare_current_from_inbox())
|
|
481
|
+
except FileNotFoundError as exc:
|
|
482
|
+
print(OUTCOME_FAIL_NO_COMMAND)
|
|
483
|
+
print(str(exc))
|
|
484
|
+
print_agent_next_footer("NO-COMMAND", "ask-agent-to-queue-command", str(exc))
|
|
485
|
+
return 1
|
|
486
|
+
except RuntimeError as exc:
|
|
487
|
+
print(OUTCOME_FAIL_AMBIGUOUS_COMMANDS)
|
|
488
|
+
print(str(exc))
|
|
489
|
+
print_agent_next_footer("HARD-FAIL", "paste-output", OUTCOME_FAIL_AMBIGUOUS_COMMANDS)
|
|
490
|
+
return 1
|
|
491
|
+
try:
|
|
492
|
+
result = agent_run(extra_upload_paths=consumed_paths)
|
|
493
|
+
finally:
|
|
494
|
+
remove_current_files()
|
|
495
|
+
failures = agent_next_postcondition_failures()
|
|
496
|
+
if failures:
|
|
497
|
+
print(OUTCOME_FAIL_POSTCONDITION)
|
|
498
|
+
for item in failures:
|
|
499
|
+
print(item)
|
|
500
|
+
print_agent_next_footer("HARD-FAIL", "paste-output", OUTCOME_FAIL_POSTCONDITION)
|
|
501
|
+
return 1
|
|
502
|
+
if result == 0:
|
|
503
|
+
print_agent_next_footer("PASS", "p")
|
|
504
|
+
else:
|
|
505
|
+
print_agent_next_footer("FAIL", "f")
|
|
506
|
+
return result
|
|
507
|
+
|
|
508
|
+
def main(argv: list[str] | None = None) -> int:
|
|
509
|
+
args = list(sys.argv[1:] if argv is None else argv)
|
|
510
|
+
if args and args[0] == "next":
|
|
511
|
+
return agent_next()
|
|
512
|
+
return agent_run()
|
|
513
|
+
|
|
514
|
+
|
|
515
|
+
if __name__ == "__main__":
|
|
516
|
+
raise SystemExit(main())
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from pathlib import Path
|
|
4
|
+
|
|
5
|
+
GUI_ENTRY_PATTERNS = ("typed", "work_order", "action_registry", "action_specs", "cockpit")
|
|
6
|
+
|
|
7
|
+
CONTRACT_TEST_CANDIDATES = (
|
|
8
|
+
"tests/test_run_summary_renderer.py",
|
|
9
|
+
"tests/test_v036_summary_and_ack_audit_hardening.py",
|
|
10
|
+
"tests/test_v036_summary_renderer_usage_hardening.py",
|
|
11
|
+
"tests/test_v036_doc_rules_contract_hardening.py",
|
|
12
|
+
"tests/test_v036_zero_ns_shell_adapter_contract.py",
|
|
13
|
+
"tests/test_v036_shell_adapter_inventory_baseline.py",
|
|
14
|
+
"tests/test_v036_release_route_help_safety.py",
|
|
15
|
+
"tests/test_terminal_logging.py",
|
|
16
|
+
"tests/test_evidence_clean.py",
|
|
17
|
+
"tests/test_evidence_clean_cli.py",
|
|
18
|
+
)
|
|
19
|
+
|
|
20
|
+
def discover_gui_entry_tests(root: Path) -> list[str]:
|
|
21
|
+
tests_dir = root / "tests"
|
|
22
|
+
if not tests_dir.exists():
|
|
23
|
+
return []
|
|
24
|
+
return sorted(
|
|
25
|
+
str(path)
|
|
26
|
+
for path in tests_dir.glob("test_*.py")
|
|
27
|
+
if any(pattern in path.name for pattern in GUI_ENTRY_PATTERNS)
|
|
28
|
+
)
|
|
29
|
+
|
|
30
|
+
def existing_contract_tests(root: Path) -> list[str]:
|
|
31
|
+
return [name for name in CONTRACT_TEST_CANDIDATES if (root / name).exists()]
|
|
32
|
+
|
|
33
|
+
def pytest_argv_for(paths: list[str]) -> list[str]:
|
|
34
|
+
if not paths:
|
|
35
|
+
raise ValueError("test matrix is empty")
|
|
36
|
+
return ["-m", "pytest", *paths, "-q"]
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import argparse
|
|
4
|
+
import json
|
|
5
|
+
import subprocess
|
|
6
|
+
import sys
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
|
|
9
|
+
from agentic_project_kit.audit_test_matrix import discover_gui_entry_tests, existing_contract_tests, pytest_argv_for
|
|
10
|
+
|
|
11
|
+
def main(argv: list[str] | None = None) -> int:
|
|
12
|
+
parser = argparse.ArgumentParser(description="Run deterministic audit test matrices without shell xargs or hard-coded missing tests.")
|
|
13
|
+
parser.add_argument("--matrix", choices=("gui-entry", "contracts", "all"), default="all")
|
|
14
|
+
parser.add_argument("--json", action="store_true")
|
|
15
|
+
parser.add_argument("--list-only", action="store_true")
|
|
16
|
+
args = parser.parse_args(argv)
|
|
17
|
+
root = Path.cwd()
|
|
18
|
+
matrices: dict[str, list[str]] = {}
|
|
19
|
+
if args.matrix in ("gui-entry", "all"):
|
|
20
|
+
matrices["gui-entry"] = discover_gui_entry_tests(root)
|
|
21
|
+
if args.matrix in ("contracts", "all"):
|
|
22
|
+
matrices["contracts"] = existing_contract_tests(root)
|
|
23
|
+
if args.json:
|
|
24
|
+
print(json.dumps(matrices, indent=2))
|
|
25
|
+
else:
|
|
26
|
+
for name, paths in matrices.items():
|
|
27
|
+
print(f"{name}_test_file_count={len(paths)}")
|
|
28
|
+
for path in paths:
|
|
29
|
+
print(path)
|
|
30
|
+
if args.list_only:
|
|
31
|
+
return 0 if all(matrices.values()) else 1
|
|
32
|
+
status = 0
|
|
33
|
+
for paths in matrices.values():
|
|
34
|
+
try:
|
|
35
|
+
pytest_args = [sys.executable, *pytest_argv_for(paths)]
|
|
36
|
+
except ValueError:
|
|
37
|
+
return 1
|
|
38
|
+
completed = subprocess.run(pytest_args, cwd=root, check=False)
|
|
39
|
+
status |= int(completed.returncode)
|
|
40
|
+
return status
|
|
41
|
+
|
|
42
|
+
if __name__ == "__main__":
|
|
43
|
+
raise SystemExit(main())
|