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,1077 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from dataclasses import asdict, dataclass, field
|
|
4
|
+
from datetime import datetime, timezone
|
|
5
|
+
from enum import StrEnum
|
|
6
|
+
import hashlib
|
|
7
|
+
import os
|
|
8
|
+
from pathlib import Path
|
|
9
|
+
from agentic_project_kit.chat_entrypoint_contract import ensure_command_reference_in_prompt
|
|
10
|
+
import platform
|
|
11
|
+
import shlex
|
|
12
|
+
import subprocess
|
|
13
|
+
from typing import Callable
|
|
14
|
+
|
|
15
|
+
import yaml
|
|
16
|
+
|
|
17
|
+
from agentic_project_kit.command_manifest import JSON_PATH as COMMAND_MANIFEST_JSON_PATH
|
|
18
|
+
from agentic_project_kit.command_manifest import load_manifest
|
|
19
|
+
from agentic_project_kit.communication_rule_context import REQUIRED_LOADED_SECTIONS
|
|
20
|
+
from agentic_project_kit import transfer_repo_actions
|
|
21
|
+
from agentic_project_kit.gui_transfer_contract import (
|
|
22
|
+
CANONICAL_REMOTE_TRANSFER_REPORT_PATH,
|
|
23
|
+
CANONICAL_TRANSFER_INBOX_PATH,
|
|
24
|
+
CANONICAL_TRANSFER_OUTBOX_PATH as CANONICAL_TRANSFER_OUTBOX_PATH,
|
|
25
|
+
CANONICAL_TRANSFER_PAYLOAD_PATH,
|
|
26
|
+
CURRENT_USER_TASK_PATH,
|
|
27
|
+
GUI_TRANSFER_TASK_REF,
|
|
28
|
+
fetch_gui_transfer_ref_args,
|
|
29
|
+
remote_gui_task_spec,
|
|
30
|
+
)
|
|
31
|
+
REMOTE_STATUS_COMMAND_ARGS = ("transfer", "patch-cycle-status", "--json")
|
|
32
|
+
FILE_TRANSFER_CONTINUE_COMMAND_ARGS = ("transfer", "continue", "--json")
|
|
33
|
+
POSIX_AGENTIC_KIT = "./.venv/bin/agentic-kit"
|
|
34
|
+
WINDOWS_AGENTIC_KIT = r".venv\Scripts\agentic-kit.exe"
|
|
35
|
+
REMOTE_STATUS_COMMAND = (POSIX_AGENTIC_KIT, *REMOTE_STATUS_COMMAND_ARGS)
|
|
36
|
+
TRANSFER_CONTINUE_COMMAND = (POSIX_AGENTIC_KIT, *FILE_TRANSFER_CONTINUE_COMMAND_ARGS)
|
|
37
|
+
COMMUNICATION_MODE_ALIASES = {
|
|
38
|
+
"a": "remote",
|
|
39
|
+
"remote": "remote",
|
|
40
|
+
"github": "remote",
|
|
41
|
+
"github_pr_ci": "remote",
|
|
42
|
+
"b": "file_transfer",
|
|
43
|
+
"file": "file_transfer",
|
|
44
|
+
"file_transfer": "file_transfer",
|
|
45
|
+
"transfer": "file_transfer",
|
|
46
|
+
"transfer_files": "file_transfer",
|
|
47
|
+
"c": "copy_paste",
|
|
48
|
+
"copy": "copy_paste",
|
|
49
|
+
"copy_paste": "copy_paste",
|
|
50
|
+
"copy-and-paste": "copy_paste",
|
|
51
|
+
}
|
|
52
|
+
COMMAND_MANIFEST_RELATIVE_PATH = COMMAND_MANIFEST_JSON_PATH.as_posix()
|
|
53
|
+
INITIAL_LLM_PROMPT_RELATIVE_PATH = Path(".agentic/INITIAL_LLM_PROMPT.md")
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
class TaskEditorState(StrEnum):
|
|
57
|
+
IDLE = "IDLE"
|
|
58
|
+
SENT = "SENT"
|
|
59
|
+
READ = "READ"
|
|
60
|
+
BLOCKED = "BLOCKED"
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
@dataclass(frozen=True)
|
|
64
|
+
class InitialLlmPrompt:
|
|
65
|
+
result_status: str
|
|
66
|
+
kind: str
|
|
67
|
+
prompt_text: str
|
|
68
|
+
copy_paste_instruction: str
|
|
69
|
+
task_path: str
|
|
70
|
+
task_ref: str
|
|
71
|
+
|
|
72
|
+
def as_json_data(self) -> dict[str, object]:
|
|
73
|
+
return asdict(self)
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
@dataclass(frozen=True)
|
|
77
|
+
class SubmittedUserTask:
|
|
78
|
+
result_status: str
|
|
79
|
+
kind: str
|
|
80
|
+
task_id: str
|
|
81
|
+
title: str
|
|
82
|
+
remote_path: str
|
|
83
|
+
task_path: str
|
|
84
|
+
published_ref: str | None
|
|
85
|
+
next_reply: str
|
|
86
|
+
next_action: str
|
|
87
|
+
button_next_state: str
|
|
88
|
+
body_sha256: str
|
|
89
|
+
created_at_utc: str
|
|
90
|
+
head_sha: str
|
|
91
|
+
blob_sha: str
|
|
92
|
+
commit_status: str
|
|
93
|
+
push_status: str
|
|
94
|
+
local_only: bool
|
|
95
|
+
remote_readable: bool
|
|
96
|
+
reason: str
|
|
97
|
+
communication_mode: str = "file_transfer"
|
|
98
|
+
communication_mode_code: str = "b"
|
|
99
|
+
communication_mode_label: str = "File Transfer"
|
|
100
|
+
local_execution_command: tuple[str, ...] = TRANSFER_CONTINUE_COMMAND
|
|
101
|
+
reply_contract: dict[str, object] = field(default_factory=dict)
|
|
102
|
+
|
|
103
|
+
def as_json_data(self) -> dict[str, object]:
|
|
104
|
+
return asdict(self)
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
def normalize_communication_mode(mode: str) -> str:
|
|
108
|
+
key = str(mode or "").strip().lower().replace(" ", "_")
|
|
109
|
+
return COMMUNICATION_MODE_ALIASES.get(key, "file_transfer")
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
def communication_mode_code(mode: str) -> str:
|
|
113
|
+
normalized = normalize_communication_mode(mode)
|
|
114
|
+
return {"remote": "a", "file_transfer": "b", "copy_paste": "c"}[normalized]
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
def communication_mode_label(mode: str) -> str:
|
|
118
|
+
normalized = normalize_communication_mode(mode)
|
|
119
|
+
return {
|
|
120
|
+
"remote": "Remote: GitHub/PR/CI",
|
|
121
|
+
"file_transfer": "File Transfer: Transfer files",
|
|
122
|
+
"copy_paste": "Copy-and-Paste: Recovery/Fallback",
|
|
123
|
+
}[normalized]
|
|
124
|
+
|
|
125
|
+
|
|
126
|
+
def standard_command_args_for_communication_mode(mode: str) -> tuple[str, ...]:
|
|
127
|
+
normalized = normalize_communication_mode(mode)
|
|
128
|
+
if normalized == "remote":
|
|
129
|
+
return REMOTE_STATUS_COMMAND_ARGS
|
|
130
|
+
if normalized == "file_transfer":
|
|
131
|
+
return FILE_TRANSFER_CONTINUE_COMMAND_ARGS
|
|
132
|
+
return ()
|
|
133
|
+
|
|
134
|
+
|
|
135
|
+
def standard_command_for_communication_mode(
|
|
136
|
+
mode: str,
|
|
137
|
+
*,
|
|
138
|
+
platform_name: str | None = None,
|
|
139
|
+
) -> tuple[str, ...]:
|
|
140
|
+
args = standard_command_args_for_communication_mode(mode)
|
|
141
|
+
if not args:
|
|
142
|
+
return ()
|
|
143
|
+
executable = WINDOWS_AGENTIC_KIT if platform_name == "Windows" else POSIX_AGENTIC_KIT
|
|
144
|
+
return (executable, *args)
|
|
145
|
+
|
|
146
|
+
|
|
147
|
+
def standard_command_label_for_communication_mode(mode: str) -> str:
|
|
148
|
+
normalized = normalize_communication_mode(mode)
|
|
149
|
+
return {
|
|
150
|
+
"remote": "Run mode-a standard",
|
|
151
|
+
"file_transfer": "Run mode-b standard",
|
|
152
|
+
"copy_paste": "No standard command",
|
|
153
|
+
}[normalized]
|
|
154
|
+
|
|
155
|
+
|
|
156
|
+
def standard_command_description_for_communication_mode(mode: str) -> str:
|
|
157
|
+
normalized = normalize_communication_mode(mode)
|
|
158
|
+
return {
|
|
159
|
+
"remote": (
|
|
160
|
+
"Mode a standard command: agentic-kit transfer patch-cycle-status --json "
|
|
161
|
+
"for remote GitHub/PR/CI status and evidence orientation."
|
|
162
|
+
),
|
|
163
|
+
"file_transfer": (
|
|
164
|
+
"Mode b standard command: agentic-kit transfer continue --json reads and "
|
|
165
|
+
"executes the canonical repo-backed transfer reply."
|
|
166
|
+
),
|
|
167
|
+
"copy_paste": (
|
|
168
|
+
"Mode c has no standard agentic-kit execution command; open a terminal "
|
|
169
|
+
"and paste the LLM-provided recovery block."
|
|
170
|
+
),
|
|
171
|
+
}[normalized]
|
|
172
|
+
|
|
173
|
+
|
|
174
|
+
def _command_manifest_sha_for_carrier(project_root: Path) -> str:
|
|
175
|
+
for candidate in (project_root, Path.cwd()):
|
|
176
|
+
try:
|
|
177
|
+
manifest = load_manifest(candidate.resolve())
|
|
178
|
+
except Exception:
|
|
179
|
+
continue
|
|
180
|
+
meta = manifest.get("meta") if isinstance(manifest.get("meta"), dict) else {}
|
|
181
|
+
manifest_sha = str(meta.get("manifest_sha") or "")
|
|
182
|
+
if manifest_sha:
|
|
183
|
+
return manifest_sha
|
|
184
|
+
return ""
|
|
185
|
+
|
|
186
|
+
|
|
187
|
+
def communication_reply_contract(mode: str) -> dict[str, object]:
|
|
188
|
+
normalized = normalize_communication_mode(mode)
|
|
189
|
+
code = communication_mode_code(normalized)
|
|
190
|
+
selected_command = standard_command_for_communication_mode(normalized)
|
|
191
|
+
mode_map = {
|
|
192
|
+
"a": {
|
|
193
|
+
"mode": "remote",
|
|
194
|
+
"label": "Remote: GitHub/PR/CI",
|
|
195
|
+
"standard_local_command": list(REMOTE_STATUS_COMMAND),
|
|
196
|
+
"llm_response": (
|
|
197
|
+
"Finish the remote work through repo-backed PR/CI/evidence mechanisms, "
|
|
198
|
+
"then reply with a compact completion status and evidence pointers."
|
|
199
|
+
),
|
|
200
|
+
"local_followup": (
|
|
201
|
+
"The local user/GUI may inspect the remote workflow state through: "
|
|
202
|
+
+ " ".join(REMOTE_STATUS_COMMAND)
|
|
203
|
+
),
|
|
204
|
+
},
|
|
205
|
+
"b": {
|
|
206
|
+
"mode": "file_transfer",
|
|
207
|
+
"label": "File Transfer: Transfer files",
|
|
208
|
+
"standard_local_command": list(TRANSFER_CONTINUE_COMMAND),
|
|
209
|
+
"llm_response": (
|
|
210
|
+
"Publish a fresh active transfer order through the existing agentic-kit "
|
|
211
|
+
"transfer protocol, then tell the local user to run the standard command. "
|
|
212
|
+
"Do not paste a terminal block as the normal answer."
|
|
213
|
+
),
|
|
214
|
+
"local_followup": (
|
|
215
|
+
"The local user/GUI executes the reply through: "
|
|
216
|
+
+ " ".join(TRANSFER_CONTINUE_COMMAND)
|
|
217
|
+
),
|
|
218
|
+
"response_order_contract": {
|
|
219
|
+
"submitted_gui_task_status": "submitted",
|
|
220
|
+
"active_order_ref": GUI_TRANSFER_TASK_REF,
|
|
221
|
+
"active_order_path": CANONICAL_TRANSFER_INBOX_PATH.as_posix(),
|
|
222
|
+
"active_order_required_status": "active",
|
|
223
|
+
"active_order_required_branch": GUI_TRANSFER_TASK_REF,
|
|
224
|
+
"python_payload_path": CANONICAL_TRANSFER_PAYLOAD_PATH.as_posix(),
|
|
225
|
+
"local_user_command": list(TRANSFER_CONTINUE_COMMAND),
|
|
226
|
+
"forbidden": [
|
|
227
|
+
"using_submitted_gui_task_as_executable_transfer_order",
|
|
228
|
+
"inventing_transfer_files_or_refs",
|
|
229
|
+
"asking_user_to_run_python_payload_directly",
|
|
230
|
+
],
|
|
231
|
+
},
|
|
232
|
+
},
|
|
233
|
+
"c": {
|
|
234
|
+
"mode": "copy_paste",
|
|
235
|
+
"label": "Copy-and-Paste: Recovery/Fallback",
|
|
236
|
+
"standard_local_command": [],
|
|
237
|
+
"llm_response": (
|
|
238
|
+
"Reply with one complete copy-and-paste terminal block and no hidden side protocol. "
|
|
239
|
+
"Use this only as recovery/fallback."
|
|
240
|
+
),
|
|
241
|
+
"local_followup": "The local user may open an OS terminal and paste the provided block.",
|
|
242
|
+
},
|
|
243
|
+
}
|
|
244
|
+
return {
|
|
245
|
+
"schema_version": 1,
|
|
246
|
+
"kind": "gui_task_reply_contract",
|
|
247
|
+
"selected_code": code,
|
|
248
|
+
"selected_mode": normalized,
|
|
249
|
+
"selected_label": communication_mode_label(normalized),
|
|
250
|
+
"mode_map": mode_map,
|
|
251
|
+
"selected_response": mode_map[code]["llm_response"],
|
|
252
|
+
"local_execution_command": list(selected_command),
|
|
253
|
+
"rules": {
|
|
254
|
+
"g_go_reads_repo_backed_state_first": True,
|
|
255
|
+
"forbidden": "answering_g_go_from_chat_memory",
|
|
256
|
+
"compare_task_id_and_body_sha256": True,
|
|
257
|
+
"discard_previous_task_context_when_identity_changes": True,
|
|
258
|
+
"do_not_invent_result_files_branches_refs_or_protocols": True,
|
|
259
|
+
"latest_remote_transfer_report_path": CANONICAL_REMOTE_TRANSFER_REPORT_PATH.as_posix(),
|
|
260
|
+
"gui_task_carrier_ref": GUI_TRANSFER_TASK_REF,
|
|
261
|
+
"gui_task_carrier_path": CANONICAL_TRANSFER_INBOX_PATH.as_posix(),
|
|
262
|
+
"gui_task_carrier_status": "submitted",
|
|
263
|
+
"active_transfer_order_status": "active",
|
|
264
|
+
"g_go_priority": [
|
|
265
|
+
"read_latest_remote_transfer_report_or_outbox_first",
|
|
266
|
+
"if_no_fresh_result_read_gui_task_carrier",
|
|
267
|
+
],
|
|
268
|
+
},
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
|
|
272
|
+
def _loaded_sections_json_block() -> str:
|
|
273
|
+
lines = [f' "{section}"' for section in REQUIRED_LOADED_SECTIONS]
|
|
274
|
+
return "[\n" + ",\n".join(lines) + "\n ]"
|
|
275
|
+
|
|
276
|
+
|
|
277
|
+
def _initial_prompt_bootstrap_block() -> str:
|
|
278
|
+
return """You are working in the repository vfi64/agentic-project-kit.
|
|
279
|
+
|
|
280
|
+
Before doing anything else, read these files from the repository in order:
|
|
281
|
+
|
|
282
|
+
1. docs/handoff/NEXT_CHAT_BOOTSTRAP.md
|
|
283
|
+
2. docs/reports/handoff-packages/latest/successor_prompt.md
|
|
284
|
+
3. docs/reports/handoff-packages/latest/validation_report.json
|
|
285
|
+
4. docs/reports/handoff-packages/latest/source_manifest.json
|
|
286
|
+
5. docs/reports/handoff-packages/latest/execution_contract.json
|
|
287
|
+
|
|
288
|
+
Bootstrap is accepted only after all required bootstrap files have been read
|
|
289
|
+
from the repository and validation_report.json has result_status/status PASS.
|
|
290
|
+
If any required bootstrap file is missing, unreadable, stale, or internally
|
|
291
|
+
inconsistent, reply exactly BOOTSTRAP_BLOCKED and do not mutate anything.
|
|
292
|
+
If HEAD differs from the handoff package without explanation, reply exactly
|
|
293
|
+
BOOTSTRAP_BLOCKED and do not mutate anything.
|
|
294
|
+
"""
|
|
295
|
+
|
|
296
|
+
|
|
297
|
+
def _initial_prompt_file_transfer_block(
|
|
298
|
+
task_path: Path = CURRENT_USER_TASK_PATH,
|
|
299
|
+
task_ref: str = GUI_TRANSFER_TASK_REF,
|
|
300
|
+
) -> str:
|
|
301
|
+
path = task_path.as_posix()
|
|
302
|
+
return f"""GUI task-transfer dialog:
|
|
303
|
+
|
|
304
|
+
When the user writes "g" or "go":
|
|
305
|
+
- Never answer a bare g/go from chat memory or from a previous user question.
|
|
306
|
+
- First inspect the latest repo-backed transfer result, normally
|
|
307
|
+
`{CANONICAL_REMOTE_TRANSFER_REPORT_PATH.as_posix()}`. If that report is fresh
|
|
308
|
+
and belongs to the last local `agentic-kit transfer continue --json` run,
|
|
309
|
+
continue from the report/outbox evidence.
|
|
310
|
+
- If no fresh transfer result exists, read the GUI task carrier `{path}` from
|
|
311
|
+
the remote ref `{task_ref}`.
|
|
312
|
+
Do not read this GUI task carrier from `main` unless the send result
|
|
313
|
+
explicitly says it was published to `main`.
|
|
314
|
+
- If neither a fresh transfer result nor the GUI task carrier exists
|
|
315
|
+
(HTTP 404 or missing): reply exactly
|
|
316
|
+
TASK_NOT_FOUND and do not mutate anything. The user must click Send
|
|
317
|
+
in the GUI first.
|
|
318
|
+
- If the GUI task carrier exists: treat it as a submitted user task for the LLM,
|
|
319
|
+
not as an executable local transfer order. Its status is `submitted`.
|
|
320
|
+
Read the task carrier first every time before planning from it.
|
|
321
|
+
Extract task_id and user_task.body_sha256 from the task carrier.
|
|
322
|
+
Compare them to the last task_id/body_sha256 you handled in this chat.
|
|
323
|
+
If they differ, discard prior task context and handle the newly read task.
|
|
324
|
+
If they match, still use the freshly read task carrier as the source of truth.
|
|
325
|
+
The user task is in `user_task.body`.
|
|
326
|
+
The communication mode is in `reply_contract.selected_code`:
|
|
327
|
+
a = remote GitHub/PR/CI work; answer with a compact completion status.
|
|
328
|
+
The local standard command is `{" ".join(REMOTE_STATUS_COMMAND)}`.
|
|
329
|
+
b = transfer files; publish a fresh active transfer order to remote ref
|
|
330
|
+
`{task_ref}` at `{path}` and tell the user to run
|
|
331
|
+
`{" ".join(TRANSFER_CONTINUE_COMMAND)}`. The active reply order must
|
|
332
|
+
have `status: active` and `branch: {task_ref}`. If a Python payload is
|
|
333
|
+
needed, use `{CANONICAL_TRANSFER_PAYLOAD_PATH.as_posix()}` as the payload
|
|
334
|
+
lane and reference it from the active transfer order; do not ask the user
|
|
335
|
+
to run the payload directly.
|
|
336
|
+
c = copy-and-paste fallback; answer with one complete terminal block.
|
|
337
|
+
Work according to repo rules, gates, protected-file policy, and
|
|
338
|
+
existing agentic-kit wrappers.
|
|
339
|
+
Use existing agentic-kit transfer/result wrappers for result publication.
|
|
340
|
+
Do not invent new result files, branches, refs, or ad-hoc protocols.
|
|
341
|
+
If no suitable wrapper exists, reply exactly REQUIRED_RESULT_WRAPPER_MISSING
|
|
342
|
+
and do not mutate anything.
|
|
343
|
+
Reply with compact machine-readable status and evidence pointers.
|
|
344
|
+
|
|
345
|
+
Copy-and-paste terminal evidence is recovery/fallback only.
|
|
346
|
+
It is not the normal operating path.
|
|
347
|
+
"""
|
|
348
|
+
|
|
349
|
+
|
|
350
|
+
def _initial_prompt_d2_block() -> str:
|
|
351
|
+
return f"""Communication-rule refresh dialog:
|
|
352
|
+
|
|
353
|
+
When the user writes "d2":
|
|
354
|
+
1. Read the pending state:
|
|
355
|
+
.agentic/rule_ack/communication_refresh_pending.json
|
|
356
|
+
2. Read the remote communication rule capsule from the ref specified by
|
|
357
|
+
remote_ref in the pending state, at the pending state's remote_path.
|
|
358
|
+
If remote_ref is absent, read it from `main`.
|
|
359
|
+
If no pending state exists, reply exactly RULE_REFRESH_NOT_PENDING.
|
|
360
|
+
3. Compute the Git blob SHA exactly as Git does for SHA-1 blob objects:
|
|
361
|
+
sha1("blob " + byte_length + "\\0" + raw_file_bytes).
|
|
362
|
+
Verify it against expected_blob_sha in the pending state.
|
|
363
|
+
4. Return a RULE_REFRESH_ACK in exactly this JSON format before any
|
|
364
|
+
further mutation:
|
|
365
|
+
|
|
366
|
+
{{
|
|
367
|
+
"kind": "communication_rule_refresh_ack",
|
|
368
|
+
"result_status": "PASS",
|
|
369
|
+
"source": "<remote_path from pending state>",
|
|
370
|
+
"remote": "<remote_ref from pending state, or main if absent>",
|
|
371
|
+
"blob_sha": "<actual blob SHA you computed>",
|
|
372
|
+
"generated_at": "<generated_at from the capsule file>",
|
|
373
|
+
"loaded_sections": {_loaded_sections_json_block()},
|
|
374
|
+
"rules_loaded": true
|
|
375
|
+
}}
|
|
376
|
+
|
|
377
|
+
If blob_sha does not match expected_blob_sha, reply exactly
|
|
378
|
+
RULE_REFRESH_ACK_BLOCKED and do not mutate anything.
|
|
379
|
+
"""
|
|
380
|
+
|
|
381
|
+
|
|
382
|
+
def _initial_prompt_stop_rules_block() -> str:
|
|
383
|
+
return """Stop and report without mutating when:
|
|
384
|
+
- Bootstrap not accepted or validation_report.json is not PASS. Reply BOOTSTRAP_BLOCKED.
|
|
385
|
+
- g/go received but neither a fresh transfer result nor the GUI task carrier is available. Reply TASK_NOT_FOUND.
|
|
386
|
+
- d2 received but no pending state exists. Reply RULE_REFRESH_NOT_PENDING.
|
|
387
|
+
- d2 received but blob_sha does not match. Reply RULE_REFRESH_ACK_BLOCKED.
|
|
388
|
+
- Communication rule refresh is pending and no valid ACK exists.
|
|
389
|
+
- No suitable agentic-kit transfer/result wrapper exists for result publication. Reply REQUIRED_RESULT_WRAPPER_MISSING.
|
|
390
|
+
- Worktree is dirty in a way that blocks the required operation.
|
|
391
|
+
- Required command, wrapper, or source file is missing or ambiguous.
|
|
392
|
+
|
|
393
|
+
Do not guess. Do not improvise. Stop and report.
|
|
394
|
+
"""
|
|
395
|
+
|
|
396
|
+
|
|
397
|
+
def build_initial_llm_prompt(
|
|
398
|
+
task_path: Path = CURRENT_USER_TASK_PATH,
|
|
399
|
+
task_ref: str = GUI_TRANSFER_TASK_REF,
|
|
400
|
+
project_root: Path | str = Path("."),
|
|
401
|
+
) -> InitialLlmPrompt:
|
|
402
|
+
root = Path(project_root)
|
|
403
|
+
target_prompt = root / INITIAL_LLM_PROMPT_RELATIVE_PATH
|
|
404
|
+
if target_prompt.exists():
|
|
405
|
+
prompt_text = ensure_command_reference_in_prompt(
|
|
406
|
+
target_prompt.read_text(encoding="utf-8").rstrip() + "\n",
|
|
407
|
+
root,
|
|
408
|
+
)
|
|
409
|
+
return InitialLlmPrompt(
|
|
410
|
+
result_status="PASS",
|
|
411
|
+
kind="initial_llm_prompt",
|
|
412
|
+
prompt_text=prompt_text,
|
|
413
|
+
copy_paste_instruction=(
|
|
414
|
+
"Copy the prompt_text from the selected project's initial prompt "
|
|
415
|
+
"and paste it once into the LLM chat at the start of a new session."
|
|
416
|
+
),
|
|
417
|
+
task_path=task_path.as_posix(),
|
|
418
|
+
task_ref=task_ref,
|
|
419
|
+
)
|
|
420
|
+
prompt = "\n\n".join(
|
|
421
|
+
(
|
|
422
|
+
_initial_prompt_bootstrap_block().strip(),
|
|
423
|
+
_initial_prompt_file_transfer_block(task_path, task_ref).strip(),
|
|
424
|
+
_initial_prompt_d2_block().strip(),
|
|
425
|
+
_initial_prompt_stop_rules_block().strip(),
|
|
426
|
+
)
|
|
427
|
+
)
|
|
428
|
+
return InitialLlmPrompt(
|
|
429
|
+
result_status="PASS",
|
|
430
|
+
kind="initial_llm_prompt",
|
|
431
|
+
prompt_text=ensure_command_reference_in_prompt(prompt + "\n", root),
|
|
432
|
+
copy_paste_instruction=(
|
|
433
|
+
"Copy the prompt_text and paste it once into the LLM chat at the start "
|
|
434
|
+
"of a new session. The LLM must read the bootstrap files before any work."
|
|
435
|
+
),
|
|
436
|
+
task_path=task_path.as_posix(),
|
|
437
|
+
task_ref=task_ref,
|
|
438
|
+
)
|
|
439
|
+
|
|
440
|
+
|
|
441
|
+
GitRunner = Callable[[Path, tuple[str, ...]], subprocess.CompletedProcess[str]]
|
|
442
|
+
|
|
443
|
+
|
|
444
|
+
@dataclass(frozen=True)
|
|
445
|
+
class TaskCarrierPublishResult:
|
|
446
|
+
result_status: str
|
|
447
|
+
published_ref: str
|
|
448
|
+
remote_path: str
|
|
449
|
+
remote_readable: bool
|
|
450
|
+
blob_sha: str
|
|
451
|
+
commit_status: str
|
|
452
|
+
push_status: str
|
|
453
|
+
reason: str
|
|
454
|
+
|
|
455
|
+
|
|
456
|
+
def submit_user_task(
|
|
457
|
+
project_root: Path | str,
|
|
458
|
+
*,
|
|
459
|
+
title: str,
|
|
460
|
+
body: str,
|
|
461
|
+
task_path: Path = CURRENT_USER_TASK_PATH,
|
|
462
|
+
communication_mode: str = "file_transfer",
|
|
463
|
+
publish: bool = False,
|
|
464
|
+
created_at_utc: str | None = None,
|
|
465
|
+
git_runner: GitRunner | None = None,
|
|
466
|
+
) -> SubmittedUserTask:
|
|
467
|
+
root = Path(project_root).resolve()
|
|
468
|
+
normalized_title = title.strip() or "GUI file-transfer task"
|
|
469
|
+
normalized_body = body.strip()
|
|
470
|
+
if not normalized_body:
|
|
471
|
+
raise ValueError("task body must not be empty")
|
|
472
|
+
created = created_at_utc or datetime.now(timezone.utc).replace(microsecond=0).isoformat()
|
|
473
|
+
head_sha = _git_output(root, "rev-parse", "HEAD") or "UNKNOWN"
|
|
474
|
+
short_head = _git_output(root, "rev-parse", "--short", "HEAD") or head_sha[:12]
|
|
475
|
+
branch = _git_output(root, "branch", "--show-current") or "UNKNOWN"
|
|
476
|
+
origin_main = _git_output(root, "rev-parse", "origin/main") or "UNKNOWN"
|
|
477
|
+
body_sha = hashlib.sha256(normalized_body.encode("utf-8")).hexdigest()
|
|
478
|
+
task_id = hashlib.sha256(
|
|
479
|
+
f"{created}\n{head_sha}\n{normalized_title}\n{body_sha}".encode("utf-8")
|
|
480
|
+
).hexdigest()[:16]
|
|
481
|
+
relative_path = task_path.as_posix()
|
|
482
|
+
normalized_mode = normalize_communication_mode(communication_mode)
|
|
483
|
+
reply_contract = communication_reply_contract(normalized_mode)
|
|
484
|
+
local_execution_command = standard_command_for_communication_mode(normalized_mode)
|
|
485
|
+
local_execution_description = standard_command_description_for_communication_mode(normalized_mode)
|
|
486
|
+
command_manifest_sha = _command_manifest_sha_for_carrier(root)
|
|
487
|
+
payload = {
|
|
488
|
+
"schema_version": 1,
|
|
489
|
+
"kind": "gui_user_task_transfer_order",
|
|
490
|
+
"manifest": COMMAND_MANIFEST_RELATIVE_PATH,
|
|
491
|
+
"manifest_sha": command_manifest_sha,
|
|
492
|
+
"id": task_id,
|
|
493
|
+
"command_id": task_id,
|
|
494
|
+
"task_id": task_id,
|
|
495
|
+
"title": normalized_title,
|
|
496
|
+
"safety": "remote_llm_user_task",
|
|
497
|
+
"status": "submitted",
|
|
498
|
+
"branch": branch,
|
|
499
|
+
"expected_current_branch": branch,
|
|
500
|
+
"expected_head": short_head,
|
|
501
|
+
"expected_origin_main": origin_main,
|
|
502
|
+
"created_for_head": head_sha,
|
|
503
|
+
"report_path": f"docs/reports/command_runs/{task_id}-gui-user-task.md",
|
|
504
|
+
"communication_mode": normalized_mode,
|
|
505
|
+
"communication_mode_code": communication_mode_code(normalized_mode),
|
|
506
|
+
"communication_mode_label": communication_mode_label(normalized_mode),
|
|
507
|
+
"reply_contract": reply_contract,
|
|
508
|
+
"task_identity": {
|
|
509
|
+
"task_id": task_id,
|
|
510
|
+
"body_sha256": body_sha,
|
|
511
|
+
},
|
|
512
|
+
"g_go_handling": {
|
|
513
|
+
"read_repo_backed_state_first": True,
|
|
514
|
+
"latest_remote_transfer_report_path": CANONICAL_REMOTE_TRANSFER_REPORT_PATH.as_posix(),
|
|
515
|
+
"read_order": [
|
|
516
|
+
"latest_remote_transfer_report_or_outbox_first",
|
|
517
|
+
"gui_task_carrier_if_no_fresh_result_exists",
|
|
518
|
+
],
|
|
519
|
+
"forbidden": "answering_g_go_from_chat_memory",
|
|
520
|
+
"compare_task_id_and_body_sha256": True,
|
|
521
|
+
"discard_previous_task_context_when_identity_changes": True,
|
|
522
|
+
},
|
|
523
|
+
"task_carrier_contract": {
|
|
524
|
+
"role": "gui_user_task_for_llm",
|
|
525
|
+
"status": "submitted",
|
|
526
|
+
"not_executable_by_transfer_continue": True,
|
|
527
|
+
"authoritative_ref": GUI_TRANSFER_TASK_REF,
|
|
528
|
+
"authoritative_path": relative_path,
|
|
529
|
+
"same_file_active_reply_contract": {
|
|
530
|
+
"submitted_gui_task_status": "submitted",
|
|
531
|
+
"active_order_ref": GUI_TRANSFER_TASK_REF,
|
|
532
|
+
"active_order_path": relative_path,
|
|
533
|
+
"active_order_required_status": "active",
|
|
534
|
+
"active_order_required_branch": GUI_TRANSFER_TASK_REF,
|
|
535
|
+
"python_payload_path": CANONICAL_TRANSFER_PAYLOAD_PATH.as_posix(),
|
|
536
|
+
"local_user_command": list(TRANSFER_CONTINUE_COMMAND),
|
|
537
|
+
"forbidden": [
|
|
538
|
+
"using_submitted_gui_task_as_executable_transfer_order",
|
|
539
|
+
"inventing_transfer_files_or_refs",
|
|
540
|
+
"asking_user_to_run_python_payload_directly",
|
|
541
|
+
],
|
|
542
|
+
},
|
|
543
|
+
},
|
|
544
|
+
"local_execution": {
|
|
545
|
+
"standard_command": list(local_execution_command),
|
|
546
|
+
"description": local_execution_description,
|
|
547
|
+
},
|
|
548
|
+
"actions": [
|
|
549
|
+
{
|
|
550
|
+
"type": "run_command",
|
|
551
|
+
"command": ["./.venv/bin/agentic-kit", "transfer", "state", "--json"],
|
|
552
|
+
}
|
|
553
|
+
],
|
|
554
|
+
"user_task": {
|
|
555
|
+
"title": normalized_title,
|
|
556
|
+
"body": normalized_body,
|
|
557
|
+
"body_sha256": body_sha,
|
|
558
|
+
},
|
|
559
|
+
"created_at_utc": created,
|
|
560
|
+
"head_sha": head_sha,
|
|
561
|
+
"task_status": "submitted",
|
|
562
|
+
"next_reply": "g",
|
|
563
|
+
"next_action": (
|
|
564
|
+
"Send g/go to the LLM; assistant must read "
|
|
565
|
+
f"the GUI task carrier {relative_path} from remote ref {GUI_TRANSFER_TASK_REF}."
|
|
566
|
+
if publish
|
|
567
|
+
else (
|
|
568
|
+
"GUI task carrier written locally only. Publish through the guarded "
|
|
569
|
+
"agentic-kit transfer path before sending g/go to the LLM."
|
|
570
|
+
)
|
|
571
|
+
),
|
|
572
|
+
"remote_path": relative_path,
|
|
573
|
+
"task_path": relative_path,
|
|
574
|
+
"published_ref": GUI_TRANSFER_TASK_REF if publish else None,
|
|
575
|
+
"local_only": not publish,
|
|
576
|
+
}
|
|
577
|
+
publish_result = None
|
|
578
|
+
if publish:
|
|
579
|
+
publish_result = _publish_task_carrier(
|
|
580
|
+
root,
|
|
581
|
+
task_path,
|
|
582
|
+
payload,
|
|
583
|
+
git_runner=git_runner or _run_git,
|
|
584
|
+
)
|
|
585
|
+
else:
|
|
586
|
+
_write_task_payload(root / task_path, payload)
|
|
587
|
+
|
|
588
|
+
remote_readable = bool(publish_result and publish_result.remote_readable)
|
|
589
|
+
result_status = "PASS" if not publish or remote_readable else "FAIL"
|
|
590
|
+
commit_status = publish_result.commit_status if publish_result else "SKIPPED"
|
|
591
|
+
push_status = publish_result.push_status if publish_result else "SKIPPED"
|
|
592
|
+
blob_sha = publish_result.blob_sha if publish_result else ""
|
|
593
|
+
reason = (
|
|
594
|
+
publish_result.reason
|
|
595
|
+
if publish_result
|
|
596
|
+
else "task_carrier_local_only"
|
|
597
|
+
)
|
|
598
|
+
next_action = (
|
|
599
|
+
"Send g/go to the LLM; assistant must read "
|
|
600
|
+
f"the GUI task carrier {relative_path} from remote ref {GUI_TRANSFER_TASK_REF}."
|
|
601
|
+
if remote_readable
|
|
602
|
+
else (
|
|
603
|
+
"GUI task carrier written locally only. Publish through the guarded "
|
|
604
|
+
"agentic-kit transfer path before sending g/go to the LLM."
|
|
605
|
+
if not publish
|
|
606
|
+
else "Inspect GUI task carrier publish failure before sending g/go."
|
|
607
|
+
)
|
|
608
|
+
)
|
|
609
|
+
return SubmittedUserTask(
|
|
610
|
+
result_status=result_status,
|
|
611
|
+
kind="gui_file_transfer_user_task_submission",
|
|
612
|
+
task_id=task_id,
|
|
613
|
+
title=normalized_title,
|
|
614
|
+
remote_path=relative_path,
|
|
615
|
+
task_path=relative_path,
|
|
616
|
+
published_ref=GUI_TRANSFER_TASK_REF if publish else None,
|
|
617
|
+
next_reply="g",
|
|
618
|
+
next_action=next_action,
|
|
619
|
+
button_next_state=TaskEditorState.SENT.value if remote_readable else TaskEditorState.BLOCKED.value,
|
|
620
|
+
body_sha256=body_sha,
|
|
621
|
+
created_at_utc=created,
|
|
622
|
+
head_sha=head_sha,
|
|
623
|
+
blob_sha=blob_sha,
|
|
624
|
+
commit_status=commit_status,
|
|
625
|
+
push_status=push_status,
|
|
626
|
+
local_only=not remote_readable,
|
|
627
|
+
remote_readable=remote_readable,
|
|
628
|
+
reason=reason,
|
|
629
|
+
communication_mode=normalized_mode,
|
|
630
|
+
communication_mode_code=communication_mode_code(normalized_mode),
|
|
631
|
+
communication_mode_label=communication_mode_label(normalized_mode),
|
|
632
|
+
local_execution_command=local_execution_command,
|
|
633
|
+
reply_contract=reply_contract,
|
|
634
|
+
)
|
|
635
|
+
|
|
636
|
+
|
|
637
|
+
def _write_task_payload(target: Path, payload: dict[str, object]) -> None:
|
|
638
|
+
target.parent.mkdir(parents=True, exist_ok=True)
|
|
639
|
+
target.write_text(_render_task_payload(payload), encoding="utf-8")
|
|
640
|
+
|
|
641
|
+
|
|
642
|
+
def _render_task_payload(payload: dict[str, object]) -> str:
|
|
643
|
+
return yaml.safe_dump(payload, sort_keys=False, allow_unicode=False)
|
|
644
|
+
|
|
645
|
+
|
|
646
|
+
def _publish_task_carrier(
|
|
647
|
+
root: Path,
|
|
648
|
+
task_path: Path,
|
|
649
|
+
payload: dict[str, object],
|
|
650
|
+
*,
|
|
651
|
+
git_runner: GitRunner,
|
|
652
|
+
) -> TaskCarrierPublishResult:
|
|
653
|
+
payload_text = _render_task_payload(payload)
|
|
654
|
+
with _working_directory(root):
|
|
655
|
+
original_branch = _git_runner_output(root, git_runner, ("branch", "--show-current")) or ""
|
|
656
|
+
ensure_result = _ensure_task_carrier_branch(root, git_runner=git_runner)
|
|
657
|
+
if ensure_result is not None:
|
|
658
|
+
return _restore_branch_after_publish(root, original_branch, ensure_result)
|
|
659
|
+
|
|
660
|
+
_write_task_payload(root / task_path, payload)
|
|
661
|
+
commit_paths = [task_path.as_posix()]
|
|
662
|
+
commit_result = transfer_repo_actions.commit_paths(
|
|
663
|
+
"Publish GUI transfer order",
|
|
664
|
+
commit_paths,
|
|
665
|
+
required_branch=GUI_TRANSFER_TASK_REF,
|
|
666
|
+
)
|
|
667
|
+
if commit_result.returncode != 0:
|
|
668
|
+
result = TaskCarrierPublishResult(
|
|
669
|
+
result_status="FAIL",
|
|
670
|
+
published_ref=GUI_TRANSFER_TASK_REF,
|
|
671
|
+
remote_path=task_path.as_posix(),
|
|
672
|
+
remote_readable=False,
|
|
673
|
+
blob_sha="",
|
|
674
|
+
commit_status=commit_result.result_status,
|
|
675
|
+
push_status="SKIPPED",
|
|
676
|
+
reason="commit_failed",
|
|
677
|
+
)
|
|
678
|
+
return _restore_branch_after_publish(root, original_branch, result)
|
|
679
|
+
|
|
680
|
+
push_result = transfer_repo_actions.push_current(required_branch=GUI_TRANSFER_TASK_REF)
|
|
681
|
+
if push_result.returncode != 0:
|
|
682
|
+
result = TaskCarrierPublishResult(
|
|
683
|
+
result_status="FAIL",
|
|
684
|
+
published_ref=GUI_TRANSFER_TASK_REF,
|
|
685
|
+
remote_path=task_path.as_posix(),
|
|
686
|
+
remote_readable=False,
|
|
687
|
+
blob_sha="",
|
|
688
|
+
commit_status=commit_result.result_status,
|
|
689
|
+
push_status=push_result.result_status,
|
|
690
|
+
reason="push_failed",
|
|
691
|
+
)
|
|
692
|
+
return _restore_branch_after_publish(root, original_branch, result)
|
|
693
|
+
|
|
694
|
+
verification = _verify_remote_task_carrier(
|
|
695
|
+
root,
|
|
696
|
+
task_path,
|
|
697
|
+
expected_payload_text=payload_text,
|
|
698
|
+
git_runner=git_runner,
|
|
699
|
+
)
|
|
700
|
+
result = TaskCarrierPublishResult(
|
|
701
|
+
result_status="PASS" if verification.remote_readable else "FAIL",
|
|
702
|
+
published_ref=GUI_TRANSFER_TASK_REF,
|
|
703
|
+
remote_path=task_path.as_posix(),
|
|
704
|
+
remote_readable=verification.remote_readable,
|
|
705
|
+
blob_sha=verification.blob_sha,
|
|
706
|
+
commit_status=commit_result.result_status,
|
|
707
|
+
push_status=push_result.result_status,
|
|
708
|
+
reason=verification.reason,
|
|
709
|
+
)
|
|
710
|
+
return _restore_branch_after_publish(root, original_branch, result)
|
|
711
|
+
|
|
712
|
+
|
|
713
|
+
def _ensure_task_carrier_branch(
|
|
714
|
+
root: Path,
|
|
715
|
+
*,
|
|
716
|
+
git_runner: GitRunner,
|
|
717
|
+
) -> TaskCarrierPublishResult | None:
|
|
718
|
+
fetch = git_runner(root, _fetch_task_ref_args())
|
|
719
|
+
local_exists = git_runner(root, ("rev-parse", "--verify", "--quiet", GUI_TRANSFER_TASK_REF)).returncode == 0
|
|
720
|
+
remote_exists = git_runner(root, ("rev-parse", "--verify", "--quiet", f"origin/{GUI_TRANSFER_TASK_REF}")).returncode == 0
|
|
721
|
+
if local_exists or remote_exists:
|
|
722
|
+
switch_result = transfer_repo_actions.branch_switch(GUI_TRANSFER_TASK_REF, pull=remote_exists)
|
|
723
|
+
if switch_result.returncode != 0:
|
|
724
|
+
return TaskCarrierPublishResult(
|
|
725
|
+
result_status="FAIL",
|
|
726
|
+
published_ref=GUI_TRANSFER_TASK_REF,
|
|
727
|
+
remote_path=CURRENT_USER_TASK_PATH.as_posix(),
|
|
728
|
+
remote_readable=False,
|
|
729
|
+
blob_sha="",
|
|
730
|
+
commit_status="SKIPPED",
|
|
731
|
+
push_status="SKIPPED",
|
|
732
|
+
reason="branch_switch_failed",
|
|
733
|
+
)
|
|
734
|
+
return None
|
|
735
|
+
|
|
736
|
+
create_result = transfer_repo_actions.branch_create(GUI_TRANSFER_TASK_REF, start_point="main")
|
|
737
|
+
if create_result.returncode != 0:
|
|
738
|
+
return TaskCarrierPublishResult(
|
|
739
|
+
result_status="FAIL",
|
|
740
|
+
published_ref=GUI_TRANSFER_TASK_REF,
|
|
741
|
+
remote_path=CURRENT_USER_TASK_PATH.as_posix(),
|
|
742
|
+
remote_readable=False,
|
|
743
|
+
blob_sha="",
|
|
744
|
+
commit_status="SKIPPED",
|
|
745
|
+
push_status="SKIPPED",
|
|
746
|
+
reason="branch_create_failed" if fetch.returncode != 0 else "branch_create_failed_after_fetch",
|
|
747
|
+
)
|
|
748
|
+
return None
|
|
749
|
+
|
|
750
|
+
|
|
751
|
+
@dataclass(frozen=True)
|
|
752
|
+
class RemoteTaskCarrierVerification:
|
|
753
|
+
remote_readable: bool
|
|
754
|
+
blob_sha: str
|
|
755
|
+
reason: str
|
|
756
|
+
|
|
757
|
+
|
|
758
|
+
@dataclass(frozen=True)
|
|
759
|
+
class TerminalLaunchPlan:
|
|
760
|
+
result_status: str
|
|
761
|
+
platform_name: str
|
|
762
|
+
script_path: str
|
|
763
|
+
command: tuple[str, ...]
|
|
764
|
+
launch_argv: tuple[str, ...]
|
|
765
|
+
reason: str
|
|
766
|
+
|
|
767
|
+
def as_json_data(self) -> dict[str, object]:
|
|
768
|
+
return asdict(self)
|
|
769
|
+
|
|
770
|
+
|
|
771
|
+
def _terminal_script_command_for_platform(
|
|
772
|
+
platform_name: str,
|
|
773
|
+
communication_mode: str,
|
|
774
|
+
) -> tuple[str, ...]:
|
|
775
|
+
return standard_command_for_communication_mode(
|
|
776
|
+
communication_mode,
|
|
777
|
+
platform_name=platform_name,
|
|
778
|
+
)
|
|
779
|
+
|
|
780
|
+
|
|
781
|
+
def _terminal_script_path(root: Path, platform_name: str, communication_mode: str) -> Path:
|
|
782
|
+
suffix = ".cmd" if platform_name == "Windows" else ".command" if platform_name == "Darwin" else ".sh"
|
|
783
|
+
mode_name = normalize_communication_mode(communication_mode).replace("_", "-")
|
|
784
|
+
return root / "tmp" / f"agentic-kit-{mode_name}-standard{suffix}"
|
|
785
|
+
|
|
786
|
+
|
|
787
|
+
def _terminal_script_text(
|
|
788
|
+
root: Path,
|
|
789
|
+
command: tuple[str, ...],
|
|
790
|
+
platform_name: str,
|
|
791
|
+
communication_mode: str,
|
|
792
|
+
) -> str:
|
|
793
|
+
description = standard_command_description_for_communication_mode(communication_mode)
|
|
794
|
+
if platform_name == "Windows":
|
|
795
|
+
command_text = subprocess.list2cmdline(command) if command else ""
|
|
796
|
+
command_line = f"{command_text}\r\n" if command_text else "echo Paste the LLM recovery block here.\r\n"
|
|
797
|
+
return (
|
|
798
|
+
"@echo off\r\n"
|
|
799
|
+
f"cd /d {subprocess.list2cmdline([str(root)])}\r\n"
|
|
800
|
+
f"echo [agentic-kit] {description}\r\n"
|
|
801
|
+
f"{command_line}"
|
|
802
|
+
"echo.\r\n"
|
|
803
|
+
"echo [agentic-kit] command finished with RC=%ERRORLEVEL%\r\n"
|
|
804
|
+
"cmd /k\r\n"
|
|
805
|
+
)
|
|
806
|
+
command_text = " ".join(shlex.quote(part) for part in command)
|
|
807
|
+
command_block = (
|
|
808
|
+
f"{command_text}\n"
|
|
809
|
+
"rc=$?\n"
|
|
810
|
+
"printf '\\n[agentic-kit] command finished with RC=%s\\n' \"$rc\"\n"
|
|
811
|
+
if command_text
|
|
812
|
+
else "printf '%s\\n' '[agentic-kit] Paste the LLM recovery block here.'\n"
|
|
813
|
+
)
|
|
814
|
+
return (
|
|
815
|
+
"#!/bin/sh\n"
|
|
816
|
+
f"cd {shlex.quote(str(root))}\n"
|
|
817
|
+
f"printf '%s\\n' {shlex.quote('[agentic-kit] ' + description)}\n"
|
|
818
|
+
f"{command_block}"
|
|
819
|
+
"exec ${SHELL:-/bin/sh}\n"
|
|
820
|
+
)
|
|
821
|
+
|
|
822
|
+
|
|
823
|
+
def build_terminal_launch_plan(
|
|
824
|
+
project_root: Path | str,
|
|
825
|
+
*,
|
|
826
|
+
communication_mode: str = "file_transfer",
|
|
827
|
+
platform_name: str | None = None,
|
|
828
|
+
) -> TerminalLaunchPlan:
|
|
829
|
+
root = Path(project_root).resolve()
|
|
830
|
+
detected = platform_name or platform.system()
|
|
831
|
+
command = _terminal_script_command_for_platform(detected, communication_mode)
|
|
832
|
+
script_path = _terminal_script_path(root, detected, communication_mode)
|
|
833
|
+
script_path.parent.mkdir(parents=True, exist_ok=True)
|
|
834
|
+
script_path.write_text(
|
|
835
|
+
_terminal_script_text(root, command, detected, communication_mode),
|
|
836
|
+
encoding="utf-8",
|
|
837
|
+
)
|
|
838
|
+
if detected != "Windows":
|
|
839
|
+
script_path.chmod(0o755)
|
|
840
|
+
|
|
841
|
+
if detected == "Darwin":
|
|
842
|
+
argv = ("open", "-a", "Terminal", str(script_path))
|
|
843
|
+
elif detected == "Windows":
|
|
844
|
+
argv = ("cmd.exe", "/c", "start", "Agentic Kit Transfer", str(script_path))
|
|
845
|
+
elif detected == "Linux":
|
|
846
|
+
terminal = _first_available_terminal()
|
|
847
|
+
if not terminal:
|
|
848
|
+
return TerminalLaunchPlan(
|
|
849
|
+
"FAIL",
|
|
850
|
+
detected,
|
|
851
|
+
str(script_path),
|
|
852
|
+
command,
|
|
853
|
+
(),
|
|
854
|
+
"terminal_not_found",
|
|
855
|
+
)
|
|
856
|
+
if terminal.endswith("gnome-terminal") or terminal.endswith("konsole"):
|
|
857
|
+
argv = (terminal, "--", str(script_path))
|
|
858
|
+
else:
|
|
859
|
+
argv = (terminal, "-e", str(script_path))
|
|
860
|
+
else:
|
|
861
|
+
return TerminalLaunchPlan("FAIL", detected, str(script_path), command, (), "unsupported_platform")
|
|
862
|
+
return TerminalLaunchPlan("PASS", detected, str(script_path), command, argv, "terminal_launcher_ready")
|
|
863
|
+
|
|
864
|
+
|
|
865
|
+
def _first_available_terminal() -> str:
|
|
866
|
+
candidates = (
|
|
867
|
+
"x-terminal-emulator",
|
|
868
|
+
"gnome-terminal",
|
|
869
|
+
"konsole",
|
|
870
|
+
"xfce4-terminal",
|
|
871
|
+
"xterm",
|
|
872
|
+
)
|
|
873
|
+
for candidate in candidates:
|
|
874
|
+
resolved = subprocess.run(
|
|
875
|
+
["which", candidate],
|
|
876
|
+
text=True,
|
|
877
|
+
stdout=subprocess.PIPE,
|
|
878
|
+
stderr=subprocess.DEVNULL,
|
|
879
|
+
check=False,
|
|
880
|
+
)
|
|
881
|
+
if resolved.returncode == 0 and resolved.stdout.strip():
|
|
882
|
+
return resolved.stdout.strip()
|
|
883
|
+
return ""
|
|
884
|
+
|
|
885
|
+
|
|
886
|
+
def open_transfer_terminal(
|
|
887
|
+
project_root: Path | str,
|
|
888
|
+
*,
|
|
889
|
+
communication_mode: str = "file_transfer",
|
|
890
|
+
) -> TerminalLaunchPlan:
|
|
891
|
+
plan = build_terminal_launch_plan(project_root, communication_mode=communication_mode)
|
|
892
|
+
if plan.result_status != "PASS":
|
|
893
|
+
return plan
|
|
894
|
+
try:
|
|
895
|
+
subprocess.Popen(list(plan.launch_argv))
|
|
896
|
+
except OSError as exc:
|
|
897
|
+
return TerminalLaunchPlan(
|
|
898
|
+
"FAIL",
|
|
899
|
+
plan.platform_name,
|
|
900
|
+
plan.script_path,
|
|
901
|
+
plan.command,
|
|
902
|
+
plan.launch_argv,
|
|
903
|
+
f"terminal_launch_failed: {exc}",
|
|
904
|
+
)
|
|
905
|
+
return plan
|
|
906
|
+
|
|
907
|
+
|
|
908
|
+
def _verify_remote_task_carrier(
|
|
909
|
+
root: Path,
|
|
910
|
+
task_path: Path,
|
|
911
|
+
*,
|
|
912
|
+
expected_payload_text: str,
|
|
913
|
+
git_runner: GitRunner,
|
|
914
|
+
) -> RemoteTaskCarrierVerification:
|
|
915
|
+
fetch = git_runner(root, _fetch_task_ref_args())
|
|
916
|
+
if fetch.returncode != 0:
|
|
917
|
+
return RemoteTaskCarrierVerification(False, "", "remote_fetch_failed")
|
|
918
|
+
remote_spec = remote_gui_task_spec(task_path)
|
|
919
|
+
blob = git_runner(root, ("rev-parse", "--verify", remote_spec))
|
|
920
|
+
if blob.returncode != 0:
|
|
921
|
+
return RemoteTaskCarrierVerification(False, "", "remote_blob_missing")
|
|
922
|
+
show = git_runner(root, ("show", remote_spec))
|
|
923
|
+
if show.returncode != 0:
|
|
924
|
+
return RemoteTaskCarrierVerification(False, blob.stdout.strip(), "remote_file_missing")
|
|
925
|
+
if show.stdout != expected_payload_text:
|
|
926
|
+
return RemoteTaskCarrierVerification(False, blob.stdout.strip(), "remote_content_mismatch")
|
|
927
|
+
return RemoteTaskCarrierVerification(True, blob.stdout.strip(), "remote_carrier_verified")
|
|
928
|
+
|
|
929
|
+
|
|
930
|
+
def _fetch_task_ref_args() -> tuple[str, str, str]:
|
|
931
|
+
return fetch_gui_transfer_ref_args()
|
|
932
|
+
|
|
933
|
+
|
|
934
|
+
def _restore_branch_after_publish(
|
|
935
|
+
root: Path,
|
|
936
|
+
original_branch: str,
|
|
937
|
+
result: TaskCarrierPublishResult,
|
|
938
|
+
) -> TaskCarrierPublishResult:
|
|
939
|
+
if not original_branch or original_branch == GUI_TRANSFER_TASK_REF:
|
|
940
|
+
return result
|
|
941
|
+
restore = transfer_repo_actions.branch_switch(original_branch)
|
|
942
|
+
if restore.returncode == 0:
|
|
943
|
+
return result
|
|
944
|
+
return TaskCarrierPublishResult(
|
|
945
|
+
result_status="FAIL",
|
|
946
|
+
published_ref=result.published_ref,
|
|
947
|
+
remote_path=result.remote_path,
|
|
948
|
+
remote_readable=False,
|
|
949
|
+
blob_sha=result.blob_sha,
|
|
950
|
+
commit_status=result.commit_status,
|
|
951
|
+
push_status=result.push_status,
|
|
952
|
+
reason=f"{result.reason}; restore_branch_failed",
|
|
953
|
+
)
|
|
954
|
+
|
|
955
|
+
|
|
956
|
+
def task_editor_visible_for_mode(mode: str) -> bool:
|
|
957
|
+
return normalize_communication_mode(mode) in {"remote", "file_transfer", "copy_paste"}
|
|
958
|
+
|
|
959
|
+
|
|
960
|
+
def task_editor_send_enabled(
|
|
961
|
+
body: str,
|
|
962
|
+
*,
|
|
963
|
+
traffic_light_state: str,
|
|
964
|
+
communication_context_fresh: bool,
|
|
965
|
+
required_next_reply: str | None,
|
|
966
|
+
) -> bool:
|
|
967
|
+
return (
|
|
968
|
+
bool(body.strip())
|
|
969
|
+
and traffic_light_state == "READY"
|
|
970
|
+
and communication_context_fresh
|
|
971
|
+
and required_next_reply is None
|
|
972
|
+
)
|
|
973
|
+
|
|
974
|
+
|
|
975
|
+
def task_editor_state_after_send(result_status: str, *, remote_readable: bool = False) -> TaskEditorState:
|
|
976
|
+
return TaskEditorState.SENT if result_status == "PASS" and remote_readable else TaskEditorState.BLOCKED
|
|
977
|
+
|
|
978
|
+
|
|
979
|
+
def read_user_task(project_root: Path | str, task_path: Path = CURRENT_USER_TASK_PATH) -> dict[str, object]:
|
|
980
|
+
root = Path(project_root).resolve()
|
|
981
|
+
target = root / task_path
|
|
982
|
+
if not target.exists():
|
|
983
|
+
return {
|
|
984
|
+
"schema_version": 1,
|
|
985
|
+
"kind": "gui_file_transfer_user_task_read",
|
|
986
|
+
"result_status": "FAIL",
|
|
987
|
+
"reason": "TASK_NOT_FOUND",
|
|
988
|
+
"task_path": task_path.as_posix(),
|
|
989
|
+
"next_action": "Click Send in the GUI first, then publish the transfer order.",
|
|
990
|
+
}
|
|
991
|
+
try:
|
|
992
|
+
payload = yaml.safe_load(target.read_text(encoding="utf-8")) or {}
|
|
993
|
+
except yaml.YAMLError as exc:
|
|
994
|
+
return {
|
|
995
|
+
"schema_version": 1,
|
|
996
|
+
"kind": "gui_file_transfer_user_task_read",
|
|
997
|
+
"result_status": "FAIL",
|
|
998
|
+
"reason": "TASK_YAML_INVALID",
|
|
999
|
+
"task_path": task_path.as_posix(),
|
|
1000
|
+
"error": str(exc),
|
|
1001
|
+
"next_action": "Inspect the transfer order YAML before continuing.",
|
|
1002
|
+
}
|
|
1003
|
+
if not isinstance(payload, dict):
|
|
1004
|
+
return {
|
|
1005
|
+
"schema_version": 1,
|
|
1006
|
+
"kind": "gui_file_transfer_user_task_read",
|
|
1007
|
+
"result_status": "FAIL",
|
|
1008
|
+
"reason": "TASK_YAML_NOT_OBJECT",
|
|
1009
|
+
"task_path": task_path.as_posix(),
|
|
1010
|
+
"next_action": "Inspect the transfer order YAML before continuing.",
|
|
1011
|
+
}
|
|
1012
|
+
return {
|
|
1013
|
+
"schema_version": 1,
|
|
1014
|
+
"kind": "gui_file_transfer_user_task_read",
|
|
1015
|
+
"result_status": "PASS",
|
|
1016
|
+
"reason": "TASK_FOUND",
|
|
1017
|
+
"task_path": task_path.as_posix(),
|
|
1018
|
+
"task": payload,
|
|
1019
|
+
"next_action": "Inspect the transfer order or publish it through the guarded transfer path.",
|
|
1020
|
+
}
|
|
1021
|
+
|
|
1022
|
+
|
|
1023
|
+
def task_editor_state_after_read(result_status: str) -> TaskEditorState:
|
|
1024
|
+
return TaskEditorState.IDLE if result_status == "PASS" else TaskEditorState.BLOCKED
|
|
1025
|
+
|
|
1026
|
+
|
|
1027
|
+
def transfer_state_has_canonical_outbox_result(payload: dict[str, object]) -> bool:
|
|
1028
|
+
transfer_files = payload.get("transfer_files")
|
|
1029
|
+
if isinstance(transfer_files, dict):
|
|
1030
|
+
outbox = transfer_files.get("outbox")
|
|
1031
|
+
else:
|
|
1032
|
+
outbox = payload.get("outbox")
|
|
1033
|
+
if not isinstance(outbox, dict):
|
|
1034
|
+
return False
|
|
1035
|
+
last_result = outbox.get("last_result")
|
|
1036
|
+
if not isinstance(last_result, dict):
|
|
1037
|
+
return False
|
|
1038
|
+
return bool(last_result.get("exists"))
|
|
1039
|
+
|
|
1040
|
+
|
|
1041
|
+
def _git_output(root: Path, *args: str) -> str:
|
|
1042
|
+
completed = _run_git(root, args)
|
|
1043
|
+
if completed.returncode != 0:
|
|
1044
|
+
return ""
|
|
1045
|
+
return completed.stdout.strip()
|
|
1046
|
+
|
|
1047
|
+
|
|
1048
|
+
def _git_runner_output(root: Path, git_runner: GitRunner, args: tuple[str, ...]) -> str:
|
|
1049
|
+
completed = git_runner(root, args)
|
|
1050
|
+
if completed.returncode != 0:
|
|
1051
|
+
return ""
|
|
1052
|
+
return completed.stdout.strip()
|
|
1053
|
+
|
|
1054
|
+
|
|
1055
|
+
def _run_git(root: Path, args: tuple[str, ...]) -> subprocess.CompletedProcess[str]:
|
|
1056
|
+
return subprocess.run(
|
|
1057
|
+
["git", *args],
|
|
1058
|
+
cwd=root,
|
|
1059
|
+
text=True,
|
|
1060
|
+
stdout=subprocess.PIPE,
|
|
1061
|
+
stderr=subprocess.PIPE,
|
|
1062
|
+
check=False,
|
|
1063
|
+
)
|
|
1064
|
+
|
|
1065
|
+
|
|
1066
|
+
class _working_directory:
|
|
1067
|
+
def __init__(self, path: Path) -> None:
|
|
1068
|
+
self.path = path
|
|
1069
|
+
self.previous: Path | None = None
|
|
1070
|
+
|
|
1071
|
+
def __enter__(self) -> None:
|
|
1072
|
+
self.previous = Path.cwd()
|
|
1073
|
+
os.chdir(self.path)
|
|
1074
|
+
|
|
1075
|
+
def __exit__(self, *_exc: object) -> None:
|
|
1076
|
+
if self.previous is not None:
|
|
1077
|
+
os.chdir(self.previous)
|