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,161 @@
|
|
|
1
|
+
"""Machine-readable command queue state contract for GUI readiness.
|
|
2
|
+
|
|
3
|
+
The contract is intentionally read-only. It classifies the repo-backed
|
|
4
|
+
agent command inbox without executing, deleting, staging, or committing files.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
from dataclasses import dataclass
|
|
10
|
+
from pathlib import Path
|
|
11
|
+
import json
|
|
12
|
+
import subprocess
|
|
13
|
+
|
|
14
|
+
QUEUE_NO_COMMAND = "NO_COMMAND"
|
|
15
|
+
QUEUE_EXACTLY_ONE_COMMAND = "EXACTLY_ONE_COMMAND"
|
|
16
|
+
QUEUE_MULTIPLE_COMMANDS = "MULTIPLE_COMMANDS"
|
|
17
|
+
QUEUE_ALREADY_EXECUTED_COMMAND = "ALREADY_EXECUTED_COMMAND"
|
|
18
|
+
QUEUE_DIRTY_INBOX = "DIRTY_INBOX"
|
|
19
|
+
QUEUE_MISSING_METADATA = "MISSING_COMMAND_METADATA"
|
|
20
|
+
QUEUE_INCOMPLETE_COMMAND = "INCOMPLETE_COMMAND"
|
|
21
|
+
|
|
22
|
+
COMMAND_DIR = Path(".agentic/commands")
|
|
23
|
+
INBOX_DIR = COMMAND_DIR / "inbox"
|
|
24
|
+
EXECUTED_JSONL = COMMAND_DIR / "executed.jsonl"
|
|
25
|
+
|
|
26
|
+
@dataclass(frozen=True)
|
|
27
|
+
class QueueCommandCandidate:
|
|
28
|
+
command_id: str | None
|
|
29
|
+
stem: str
|
|
30
|
+
yaml_path: str
|
|
31
|
+
script_path: str
|
|
32
|
+
has_yaml: bool
|
|
33
|
+
has_script: bool
|
|
34
|
+
|
|
35
|
+
@dataclass(frozen=True)
|
|
36
|
+
class QueueState:
|
|
37
|
+
schema_version: int
|
|
38
|
+
queue_state: str
|
|
39
|
+
valid_for_agent_next: bool
|
|
40
|
+
command_count: int
|
|
41
|
+
candidates: tuple[QueueCommandCandidate, ...]
|
|
42
|
+
dirty_paths: tuple[str, ...]
|
|
43
|
+
reason: str
|
|
44
|
+
next_allowed_actions: tuple[str, ...]
|
|
45
|
+
|
|
46
|
+
def _root(root: Path) -> Path:
|
|
47
|
+
return root.resolve()
|
|
48
|
+
|
|
49
|
+
def _parse_simple_yaml(path: Path) -> dict[str, str]:
|
|
50
|
+
data: dict[str, str] = {}
|
|
51
|
+
if not path.exists():
|
|
52
|
+
return data
|
|
53
|
+
for raw in path.read_text(encoding="utf-8").splitlines():
|
|
54
|
+
line = raw.strip()
|
|
55
|
+
if not line or line.startswith("#"):
|
|
56
|
+
continue
|
|
57
|
+
if ":" not in line:
|
|
58
|
+
continue
|
|
59
|
+
key, value = line.split(":", 1)
|
|
60
|
+
value = value.strip()
|
|
61
|
+
if (value.startswith(chr(34)) and value.endswith(chr(34))) or (value.startswith(chr(39)) and value.endswith(chr(39))):
|
|
62
|
+
value = value[1:-1]
|
|
63
|
+
data[key.strip()] = value
|
|
64
|
+
return data
|
|
65
|
+
|
|
66
|
+
def _executed_ids(root: Path) -> set[str]:
|
|
67
|
+
path = _root(root) / EXECUTED_JSONL
|
|
68
|
+
if not path.exists():
|
|
69
|
+
return set()
|
|
70
|
+
ids: set[str] = set()
|
|
71
|
+
for raw in path.read_text(encoding="utf-8").splitlines():
|
|
72
|
+
if not raw.strip():
|
|
73
|
+
continue
|
|
74
|
+
try:
|
|
75
|
+
item = json.loads(raw)
|
|
76
|
+
except json.JSONDecodeError:
|
|
77
|
+
continue
|
|
78
|
+
command_id = item.get("command_id")
|
|
79
|
+
if isinstance(command_id, str):
|
|
80
|
+
ids.add(command_id)
|
|
81
|
+
return ids
|
|
82
|
+
|
|
83
|
+
def _dirty_inbox_paths(root: Path) -> tuple[str, ...]:
|
|
84
|
+
proc = subprocess.run(["git", "status", "--porcelain", ".agentic/commands/inbox"], cwd=_root(root), text=True, capture_output=True, check=False)
|
|
85
|
+
if proc.returncode != 0:
|
|
86
|
+
return ()
|
|
87
|
+
paths: list[str] = []
|
|
88
|
+
for raw in proc.stdout.splitlines():
|
|
89
|
+
if raw.strip():
|
|
90
|
+
paths.append(raw[3:])
|
|
91
|
+
return tuple(paths)
|
|
92
|
+
|
|
93
|
+
def _candidates(root: Path) -> tuple[QueueCommandCandidate, ...]:
|
|
94
|
+
inbox = _root(root) / INBOX_DIR
|
|
95
|
+
if not inbox.exists():
|
|
96
|
+
return ()
|
|
97
|
+
stems = {path.stem for path in inbox.glob("*.yaml")} | {path.stem for path in inbox.glob("*.sh")}
|
|
98
|
+
result: list[QueueCommandCandidate] = []
|
|
99
|
+
for stem in sorted(stems):
|
|
100
|
+
yaml_path = inbox / f"{stem}.yaml"
|
|
101
|
+
script_path = inbox / f"{stem}.sh"
|
|
102
|
+
data = _parse_simple_yaml(yaml_path)
|
|
103
|
+
command_id = data.get("command_id") or None
|
|
104
|
+
result.append(QueueCommandCandidate(command_id, stem, yaml_path.relative_to(_root(root)).as_posix(), script_path.relative_to(_root(root)).as_posix(), yaml_path.exists(), script_path.exists()))
|
|
105
|
+
return tuple(result)
|
|
106
|
+
|
|
107
|
+
def inspect_queue_state(root: Path = Path(".")) -> QueueState:
|
|
108
|
+
root = _root(root)
|
|
109
|
+
dirty = _dirty_inbox_paths(root)
|
|
110
|
+
candidates = _candidates(root)
|
|
111
|
+
complete = tuple(item for item in candidates if item.has_yaml and item.has_script)
|
|
112
|
+
incomplete = tuple(item for item in candidates if not (item.has_yaml and item.has_script))
|
|
113
|
+
executed = _executed_ids(root)
|
|
114
|
+
already = tuple(item for item in complete if item.command_id in executed)
|
|
115
|
+
missing_meta = tuple(item for item in complete if not item.command_id)
|
|
116
|
+
if dirty:
|
|
117
|
+
state = QUEUE_DIRTY_INBOX
|
|
118
|
+
reason = "command inbox has uncommitted changes"
|
|
119
|
+
elif incomplete:
|
|
120
|
+
state = QUEUE_INCOMPLETE_COMMAND
|
|
121
|
+
reason = "command inbox contains unmatched yaml or shell file"
|
|
122
|
+
elif not complete:
|
|
123
|
+
state = QUEUE_NO_COMMAND
|
|
124
|
+
reason = "no complete pending command pair"
|
|
125
|
+
elif len(complete) > 1:
|
|
126
|
+
state = QUEUE_MULTIPLE_COMMANDS
|
|
127
|
+
reason = "multiple complete pending command pairs"
|
|
128
|
+
elif missing_meta:
|
|
129
|
+
state = QUEUE_MISSING_METADATA
|
|
130
|
+
reason = "pending command is missing command_id metadata"
|
|
131
|
+
elif already:
|
|
132
|
+
state = QUEUE_ALREADY_EXECUTED_COMMAND
|
|
133
|
+
reason = "pending command_id is already recorded as executed"
|
|
134
|
+
else:
|
|
135
|
+
state = QUEUE_EXACTLY_ONE_COMMAND
|
|
136
|
+
reason = "exactly one complete pending command pair is ready"
|
|
137
|
+
valid = state == QUEUE_EXACTLY_ONE_COMMAND
|
|
138
|
+
next_actions = ("agent-next",) if valid else ("cockpit.actions",)
|
|
139
|
+
return QueueState(1, state, valid, len(complete), candidates, dirty, reason, next_actions)
|
|
140
|
+
|
|
141
|
+
def queue_state_as_json_data(state: QueueState) -> dict[str, object]:
|
|
142
|
+
return {
|
|
143
|
+
"schema_version": state.schema_version,
|
|
144
|
+
"queue_state": state.queue_state,
|
|
145
|
+
"valid_for_agent_next": state.valid_for_agent_next,
|
|
146
|
+
"command_count": state.command_count,
|
|
147
|
+
"dirty_paths": list(state.dirty_paths),
|
|
148
|
+
"reason": state.reason,
|
|
149
|
+
"next_allowed_actions": list(state.next_allowed_actions),
|
|
150
|
+
"candidates": [
|
|
151
|
+
{
|
|
152
|
+
"command_id": item.command_id,
|
|
153
|
+
"stem": item.stem,
|
|
154
|
+
"yaml_path": item.yaml_path,
|
|
155
|
+
"script_path": item.script_path,
|
|
156
|
+
"has_yaml": item.has_yaml,
|
|
157
|
+
"has_script": item.has_script,
|
|
158
|
+
}
|
|
159
|
+
for item in state.candidates
|
|
160
|
+
],
|
|
161
|
+
}
|
|
@@ -0,0 +1,245 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from dataclasses import dataclass
|
|
4
|
+
from typing import Any
|
|
5
|
+
|
|
6
|
+
from agentic_project_kit.command_manifest import SURFACE_VALUES
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
SAFETY_SORT_ORDER = {"READ_ONLY": 0, "BOUNDED": 1, "DESTRUCTIVE": 2}
|
|
10
|
+
SURFACE_SORT_ORDER = {"orchestrator": 0, "diagnostic": 1, "primitive": 2}
|
|
11
|
+
DIAGNOSTIC_SURFACE_SORT_ORDER = {"diagnostic": 0, "orchestrator": 1, "primitive": 2}
|
|
12
|
+
DIAGNOSTIC_TASK_TERMS = (
|
|
13
|
+
"audit",
|
|
14
|
+
"check",
|
|
15
|
+
"diagnose",
|
|
16
|
+
"diagnostic",
|
|
17
|
+
"doctor",
|
|
18
|
+
"inspect",
|
|
19
|
+
"list",
|
|
20
|
+
"readiness",
|
|
21
|
+
"report",
|
|
22
|
+
"show",
|
|
23
|
+
"status",
|
|
24
|
+
"validate",
|
|
25
|
+
)
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
@dataclass(frozen=True)
|
|
29
|
+
class CommandSelection:
|
|
30
|
+
status: str
|
|
31
|
+
payload: dict[str, Any]
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def normalize_raw_command(command_line: str) -> str:
|
|
35
|
+
"""Normalize a pasted shell command for deterministic manifest lookup."""
|
|
36
|
+
text = command_line.strip()
|
|
37
|
+
while text and text[0] in "$#>":
|
|
38
|
+
text = text[1:].lstrip()
|
|
39
|
+
return " ".join(text.split())
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def _matches_prefix(command_line: str, prefix: str) -> bool:
|
|
43
|
+
return command_line == prefix or command_line.startswith(prefix + " ")
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def _command_summary(command: dict[str, Any]) -> dict[str, Any]:
|
|
47
|
+
return {
|
|
48
|
+
"qualified_name": str(command.get("qualified_name") or ""),
|
|
49
|
+
"safety": str(command.get("safety") or ""),
|
|
50
|
+
"surface": str(command.get("surface") or ""),
|
|
51
|
+
"when_to_use": str(command.get("when_to_use") or ""),
|
|
52
|
+
"dry_run_available": bool(command.get("dry_run_available")),
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def _invalid_surface_summaries(commands: list[dict[str, Any]]) -> list[dict[str, str]]:
|
|
57
|
+
invalid = []
|
|
58
|
+
for command in commands:
|
|
59
|
+
surface = str(command.get("surface") or "")
|
|
60
|
+
if surface not in SURFACE_VALUES:
|
|
61
|
+
invalid.append(
|
|
62
|
+
{
|
|
63
|
+
"qualified_name": str(command.get("qualified_name") or ""),
|
|
64
|
+
"surface": surface,
|
|
65
|
+
}
|
|
66
|
+
)
|
|
67
|
+
return invalid
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
def _surface_sort_order(task_tag: str) -> dict[str, int]:
|
|
71
|
+
normalized = task_tag.casefold()
|
|
72
|
+
if any(term in normalized for term in DIAGNOSTIC_TASK_TERMS):
|
|
73
|
+
return DIAGNOSTIC_SURFACE_SORT_ORDER
|
|
74
|
+
return SURFACE_SORT_ORDER
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
def select_for_raw(manifest: dict[str, Any], command_line: str) -> CommandSelection:
|
|
78
|
+
normalized = normalize_raw_command(command_line)
|
|
79
|
+
matches: list[tuple[str, dict[str, Any]]] = []
|
|
80
|
+
for command in manifest.get("commands") or []:
|
|
81
|
+
if not isinstance(command, dict):
|
|
82
|
+
continue
|
|
83
|
+
for raw_prefix in command.get("replaces_raw") or []:
|
|
84
|
+
prefix = normalize_raw_command(str(raw_prefix))
|
|
85
|
+
if prefix and _matches_prefix(normalized, prefix):
|
|
86
|
+
matches.append((prefix, command))
|
|
87
|
+
|
|
88
|
+
if not matches:
|
|
89
|
+
return CommandSelection(
|
|
90
|
+
status="no_match",
|
|
91
|
+
payload={
|
|
92
|
+
"mode": "raw",
|
|
93
|
+
"status": "no_match",
|
|
94
|
+
"raw": command_line,
|
|
95
|
+
"normalized_raw": normalized,
|
|
96
|
+
"message": "no mapping; if this mutates the repo, check the manifest before running raw",
|
|
97
|
+
},
|
|
98
|
+
)
|
|
99
|
+
|
|
100
|
+
longest = max(len(prefix) for prefix, _command in matches)
|
|
101
|
+
selected = [
|
|
102
|
+
command for prefix, command in matches if len(prefix) == longest
|
|
103
|
+
]
|
|
104
|
+
invalid = _invalid_surface_summaries(selected)
|
|
105
|
+
if invalid:
|
|
106
|
+
return CommandSelection(
|
|
107
|
+
status="invalid_manifest",
|
|
108
|
+
payload={
|
|
109
|
+
"mode": "raw",
|
|
110
|
+
"status": "invalid_manifest",
|
|
111
|
+
"raw": command_line,
|
|
112
|
+
"normalized_raw": normalized,
|
|
113
|
+
"invalid_surfaces": invalid,
|
|
114
|
+
},
|
|
115
|
+
)
|
|
116
|
+
selected.sort(
|
|
117
|
+
key=lambda command: (
|
|
118
|
+
SAFETY_SORT_ORDER.get(str(command.get("safety") or ""), 99),
|
|
119
|
+
SURFACE_SORT_ORDER.get(str(command.get("surface") or ""), 99),
|
|
120
|
+
str(command.get("qualified_name") or ""),
|
|
121
|
+
)
|
|
122
|
+
)
|
|
123
|
+
matched_prefix = sorted({prefix for prefix, _command in matches if len(prefix) == longest})[0]
|
|
124
|
+
return CommandSelection(
|
|
125
|
+
status="match",
|
|
126
|
+
payload={
|
|
127
|
+
"mode": "raw",
|
|
128
|
+
"status": "match",
|
|
129
|
+
"raw": command_line,
|
|
130
|
+
"normalized_raw": normalized,
|
|
131
|
+
"matched_prefix": matched_prefix,
|
|
132
|
+
"commands": [_command_summary(command) for command in selected],
|
|
133
|
+
},
|
|
134
|
+
)
|
|
135
|
+
|
|
136
|
+
|
|
137
|
+
def _all_task_tags(manifest: dict[str, Any]) -> list[str]:
|
|
138
|
+
tags: set[str] = set()
|
|
139
|
+
for command in manifest.get("commands") or []:
|
|
140
|
+
if not isinstance(command, dict):
|
|
141
|
+
continue
|
|
142
|
+
tags.update(str(tag) for tag in command.get("task_tags") or [] if str(tag).strip())
|
|
143
|
+
return sorted(tags)
|
|
144
|
+
|
|
145
|
+
|
|
146
|
+
def select_for_task(manifest: dict[str, Any], task_tag: str) -> CommandSelection:
|
|
147
|
+
commands = [
|
|
148
|
+
command
|
|
149
|
+
for command in manifest.get("commands") or []
|
|
150
|
+
if isinstance(command, dict) and task_tag in set(command.get("task_tags") or [])
|
|
151
|
+
]
|
|
152
|
+
if not commands:
|
|
153
|
+
return CommandSelection(
|
|
154
|
+
status="unknown_tag",
|
|
155
|
+
payload={
|
|
156
|
+
"mode": "task",
|
|
157
|
+
"status": "unknown_tag",
|
|
158
|
+
"task_tag": task_tag,
|
|
159
|
+
"available_tags": _all_task_tags(manifest),
|
|
160
|
+
},
|
|
161
|
+
)
|
|
162
|
+
|
|
163
|
+
invalid = _invalid_surface_summaries(commands)
|
|
164
|
+
if invalid:
|
|
165
|
+
return CommandSelection(
|
|
166
|
+
status="invalid_manifest",
|
|
167
|
+
payload={
|
|
168
|
+
"mode": "task",
|
|
169
|
+
"status": "invalid_manifest",
|
|
170
|
+
"task_tag": task_tag,
|
|
171
|
+
"invalid_surfaces": invalid,
|
|
172
|
+
},
|
|
173
|
+
)
|
|
174
|
+
|
|
175
|
+
surface_order = _surface_sort_order(task_tag)
|
|
176
|
+
commands.sort(
|
|
177
|
+
key=lambda command: (
|
|
178
|
+
SAFETY_SORT_ORDER.get(str(command.get("safety") or ""), 99),
|
|
179
|
+
surface_order.get(str(command.get("surface") or ""), 99),
|
|
180
|
+
str(command.get("qualified_name") or ""),
|
|
181
|
+
)
|
|
182
|
+
)
|
|
183
|
+
return CommandSelection(
|
|
184
|
+
status="match",
|
|
185
|
+
payload={
|
|
186
|
+
"mode": "task",
|
|
187
|
+
"status": "match",
|
|
188
|
+
"task_tag": task_tag,
|
|
189
|
+
"commands": [_command_summary(command) for command in commands],
|
|
190
|
+
},
|
|
191
|
+
)
|
|
192
|
+
|
|
193
|
+
|
|
194
|
+
def render_command_selection(selection: CommandSelection) -> str:
|
|
195
|
+
payload = selection.payload
|
|
196
|
+
if payload["mode"] == "raw":
|
|
197
|
+
lines = [
|
|
198
|
+
"COMMAND_FOR",
|
|
199
|
+
f"STATUS={payload['status']}",
|
|
200
|
+
f"RAW={payload['raw']}",
|
|
201
|
+
f"NORMALIZED_RAW={payload['normalized_raw']}",
|
|
202
|
+
]
|
|
203
|
+
if payload["status"] == "no_match":
|
|
204
|
+
lines.append(str(payload["message"]))
|
|
205
|
+
return "\n".join(lines) + "\n"
|
|
206
|
+
if payload["status"] == "invalid_manifest":
|
|
207
|
+
for invalid in payload["invalid_surfaces"]:
|
|
208
|
+
lines.append(
|
|
209
|
+
"INVALID_SURFACE="
|
|
210
|
+
f"{invalid['qualified_name']} | surface={invalid['surface']}"
|
|
211
|
+
)
|
|
212
|
+
return "\n".join(lines) + "\n"
|
|
213
|
+
lines.append(f"MATCHED_PREFIX={payload['matched_prefix']}")
|
|
214
|
+
for command in payload["commands"]:
|
|
215
|
+
dry_run = "yes" if command["dry_run_available"] else "no"
|
|
216
|
+
lines.append(
|
|
217
|
+
"WRAPPER="
|
|
218
|
+
f"{command['qualified_name']} | safety={command['safety']} | "
|
|
219
|
+
f"surface={command['surface']} | "
|
|
220
|
+
f"dry_run_available={dry_run} | when_to_use={command['when_to_use']}"
|
|
221
|
+
)
|
|
222
|
+
return "\n".join(lines) + "\n"
|
|
223
|
+
|
|
224
|
+
lines = [
|
|
225
|
+
"COMMAND_FOR",
|
|
226
|
+
f"STATUS={payload['status']}",
|
|
227
|
+
f"TASK_TAG={payload['task_tag']}",
|
|
228
|
+
]
|
|
229
|
+
if payload["status"] == "unknown_tag":
|
|
230
|
+
lines.append("AVAILABLE_TAGS=" + ", ".join(payload["available_tags"]))
|
|
231
|
+
return "\n".join(lines) + "\n"
|
|
232
|
+
if payload["status"] == "invalid_manifest":
|
|
233
|
+
for invalid in payload["invalid_surfaces"]:
|
|
234
|
+
lines.append(
|
|
235
|
+
"INVALID_SURFACE="
|
|
236
|
+
f"{invalid['qualified_name']} | surface={invalid['surface']}"
|
|
237
|
+
)
|
|
238
|
+
return "\n".join(lines) + "\n"
|
|
239
|
+
for command in payload["commands"]:
|
|
240
|
+
lines.append(
|
|
241
|
+
f"COMMAND={command['qualified_name']} | safety={command['safety']} | "
|
|
242
|
+
f"surface={command['surface']} | "
|
|
243
|
+
f"when_to_use={command['when_to_use']}"
|
|
244
|
+
)
|
|
245
|
+
return "\n".join(lines) + "\n"
|
|
@@ -0,0 +1,264 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from dataclasses import asdict, dataclass
|
|
4
|
+
import json
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
from typing import Any
|
|
7
|
+
|
|
8
|
+
from agentic_project_kit.workspace import LEGACY_DEFAULTS, load_workspace
|
|
9
|
+
|
|
10
|
+
ALLOWED_COMMAND_CATEGORIES: tuple[str, ...] = (
|
|
11
|
+
"core",
|
|
12
|
+
"transfer",
|
|
13
|
+
"audit",
|
|
14
|
+
"release",
|
|
15
|
+
"handoff",
|
|
16
|
+
"gui-readiness",
|
|
17
|
+
"diagnostic",
|
|
18
|
+
"docs",
|
|
19
|
+
"workflow",
|
|
20
|
+
"governance",
|
|
21
|
+
"state",
|
|
22
|
+
"work-order",
|
|
23
|
+
"advanced/internal",
|
|
24
|
+
)
|
|
25
|
+
|
|
26
|
+
COMMAND_REFERENCE_PATH = Path(LEGACY_DEFAULTS.reference_root) / "agentic-kit-commands.json"
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
@dataclass(frozen=True)
|
|
30
|
+
class CommandTaxonomyEntry:
|
|
31
|
+
qualified_name: str
|
|
32
|
+
group: str
|
|
33
|
+
category: str
|
|
34
|
+
role: str
|
|
35
|
+
composition_level: str
|
|
36
|
+
recommendation_weight: int
|
|
37
|
+
|
|
38
|
+
def as_dict(self) -> dict[str, object]:
|
|
39
|
+
return asdict(self)
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
@dataclass(frozen=True)
|
|
43
|
+
class CommandTaxonomyReport:
|
|
44
|
+
command_count: int
|
|
45
|
+
entries: tuple[CommandTaxonomyEntry, ...]
|
|
46
|
+
findings: tuple[str, ...]
|
|
47
|
+
|
|
48
|
+
@property
|
|
49
|
+
def ok(self) -> bool:
|
|
50
|
+
return not self.findings
|
|
51
|
+
|
|
52
|
+
@property
|
|
53
|
+
def status(self) -> str:
|
|
54
|
+
return "PASS" if self.ok else "FAIL"
|
|
55
|
+
|
|
56
|
+
def as_dict(self) -> dict[str, object]:
|
|
57
|
+
return {
|
|
58
|
+
"schema_version": 1,
|
|
59
|
+
"kind": "command_taxonomy_report",
|
|
60
|
+
"status": self.status,
|
|
61
|
+
"command_count": self.command_count,
|
|
62
|
+
"category_count": len({entry.category for entry in self.entries}),
|
|
63
|
+
"findings": list(self.findings),
|
|
64
|
+
"entries": [entry.as_dict() for entry in self.entries],
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
def load_command_reference(project_root: Path) -> list[dict[str, Any]]:
|
|
69
|
+
path = load_workspace(project_root).reference_file("agentic-kit-commands.json")
|
|
70
|
+
data = json.loads(path.read_text(encoding="utf-8"))
|
|
71
|
+
commands = data.get("commands") if isinstance(data, dict) else data
|
|
72
|
+
if not isinstance(commands, list):
|
|
73
|
+
raise ValueError(f"Unsupported command reference shape in {path}")
|
|
74
|
+
return [command for command in commands if isinstance(command, dict)]
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
def classify_command(command: dict[str, Any]) -> CommandTaxonomyEntry:
|
|
78
|
+
qualified = str(command.get("qualified_name") or command.get("name") or "")
|
|
79
|
+
group = str(command.get("group") or "")
|
|
80
|
+
path = command.get("path")
|
|
81
|
+
path_parts = tuple(str(part) for part in path) if isinstance(path, list) else ()
|
|
82
|
+
leaf = path_parts[-1] if path_parts else qualified.rsplit(" ", 1)[-1]
|
|
83
|
+
|
|
84
|
+
category = _category_for(group, leaf, qualified)
|
|
85
|
+
composition_level = _composition_level_for(group, leaf, qualified)
|
|
86
|
+
recommendation_weight = _recommendation_weight_for(category, composition_level, qualified)
|
|
87
|
+
role = _role_for(category, leaf, qualified, composition_level=composition_level)
|
|
88
|
+
return CommandTaxonomyEntry(
|
|
89
|
+
qualified_name=qualified,
|
|
90
|
+
group=group,
|
|
91
|
+
category=category,
|
|
92
|
+
role=role,
|
|
93
|
+
composition_level=composition_level,
|
|
94
|
+
recommendation_weight=recommendation_weight,
|
|
95
|
+
)
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
def _category_for(group: str, leaf: str, qualified: str) -> str:
|
|
99
|
+
if group == "transfer" or qualified.startswith("agentic-kit transfer "):
|
|
100
|
+
return "transfer"
|
|
101
|
+
if group in {"handoff", "boot"} or "handoff" in qualified:
|
|
102
|
+
return "handoff"
|
|
103
|
+
if group in {"rules", "rule-registry", "governance"}:
|
|
104
|
+
return "governance"
|
|
105
|
+
if group in {"workflow", "workflow-guard"}:
|
|
106
|
+
return "workflow"
|
|
107
|
+
if group == "state":
|
|
108
|
+
return "state"
|
|
109
|
+
if group == "work-order":
|
|
110
|
+
return "work-order"
|
|
111
|
+
if group == "workspace":
|
|
112
|
+
return "core"
|
|
113
|
+
if group in {"evidence", "actions", "patterns", "pass-already-done", "dev"}:
|
|
114
|
+
return "advanced/internal"
|
|
115
|
+
if group == "todo":
|
|
116
|
+
return "core"
|
|
117
|
+
if group == "cockpit":
|
|
118
|
+
return "gui-readiness"
|
|
119
|
+
|
|
120
|
+
if leaf.startswith("audit-") or leaf.endswith("-audit"):
|
|
121
|
+
return "audit"
|
|
122
|
+
if group == "direction":
|
|
123
|
+
return "docs"
|
|
124
|
+
if leaf in {"docs-audit", "docs-registry", "doc-mesh-repair", "project-direction"} or leaf.startswith("doc-"):
|
|
125
|
+
return "docs"
|
|
126
|
+
if leaf in {"release-publish", "release-prep", "post-release-check", "release-metadata-authority-gate"}:
|
|
127
|
+
return "release"
|
|
128
|
+
if leaf == "gui-readiness-gate":
|
|
129
|
+
return "gui-readiness"
|
|
130
|
+
if leaf in {"doctor", "check", "check-docs", "check-todo", "standard-gates-audit-suite", "command-taxonomy-check", "patch-preflight", "patch-scope-preflight"}:
|
|
131
|
+
return "diagnostic"
|
|
132
|
+
if group in {"root", "scaffold"}:
|
|
133
|
+
return "core"
|
|
134
|
+
return "advanced/internal"
|
|
135
|
+
|
|
136
|
+
|
|
137
|
+
PREFERRED_COMPOSITE_COMMANDS: tuple[str, ...] = (
|
|
138
|
+
"agentic-kit transfer pr-closeout-complete",
|
|
139
|
+
"agentic-kit transfer post-merge-settle",
|
|
140
|
+
"agentic-kit transfer post-merge-complete",
|
|
141
|
+
"agentic-kit transfer pr-create-complete",
|
|
142
|
+
"agentic-kit work start",
|
|
143
|
+
"agentic-kit work check",
|
|
144
|
+
"agentic-kit work finish",
|
|
145
|
+
"agentic-kit work recover",
|
|
146
|
+
"agentic-kit release ready",
|
|
147
|
+
"agentic-kit release prepare",
|
|
148
|
+
"agentic-kit release-publish",
|
|
149
|
+
)
|
|
150
|
+
|
|
151
|
+
|
|
152
|
+
def _composition_level_for(group: str, leaf: str, qualified: str) -> str:
|
|
153
|
+
if qualified in PREFERRED_COMPOSITE_COMMANDS:
|
|
154
|
+
return "composite"
|
|
155
|
+
if leaf in {
|
|
156
|
+
"pr-closeout-complete",
|
|
157
|
+
"post-merge-settle",
|
|
158
|
+
"post-merge-complete",
|
|
159
|
+
"pr-create-complete",
|
|
160
|
+
"patch-cycle-status",
|
|
161
|
+
"standard-gates-audit-suite",
|
|
162
|
+
}:
|
|
163
|
+
return "composite"
|
|
164
|
+
if group in {"actions", "dev", "evidence", "patterns"}:
|
|
165
|
+
return "elementary/internal"
|
|
166
|
+
return "elementary"
|
|
167
|
+
|
|
168
|
+
|
|
169
|
+
def _recommendation_weight_for(category: str, composition_level: str, qualified: str) -> int:
|
|
170
|
+
if qualified == "agentic-kit transfer pr-closeout-complete":
|
|
171
|
+
return 100
|
|
172
|
+
if composition_level == "composite":
|
|
173
|
+
return 90
|
|
174
|
+
if category == "advanced/internal" or composition_level == "elementary/internal":
|
|
175
|
+
return 20
|
|
176
|
+
return 50
|
|
177
|
+
|
|
178
|
+
|
|
179
|
+
|
|
180
|
+
def _role_for(category: str, leaf: str, qualified: str, *, composition_level: str = "elementary") -> str:
|
|
181
|
+
if category == "audit":
|
|
182
|
+
return "Runs a focused repository safety or consistency audit."
|
|
183
|
+
if category == "transfer":
|
|
184
|
+
if composition_level == "composite":
|
|
185
|
+
return "Preferred high-level wrapper for guarded transfer or PR lifecycle workflows; LLMs should choose this before low-level commands."
|
|
186
|
+
return "Supports guarded GitHub/local transfer and PR lifecycle workflows."
|
|
187
|
+
if category == "release":
|
|
188
|
+
return "Supports release preparation, verification, or controlled publishing."
|
|
189
|
+
if category == "gui-readiness":
|
|
190
|
+
return "Supports GUI gatekeeper readiness or cockpit workflows."
|
|
191
|
+
if category == "docs":
|
|
192
|
+
return "Maintains documentation coverage, lifecycle, or mesh consistency."
|
|
193
|
+
if category == "diagnostic":
|
|
194
|
+
return "Summarizes health, taxonomy, preflight, or standard gate state."
|
|
195
|
+
if category == "handoff":
|
|
196
|
+
return "Supports successor-chat handoff and bootstrap continuity."
|
|
197
|
+
if category == "governance":
|
|
198
|
+
return "Maintains rules, policy, or governance state."
|
|
199
|
+
if category == "workflow":
|
|
200
|
+
return "Runs workflow planning or guard utilities."
|
|
201
|
+
if category == "state":
|
|
202
|
+
return "Inspects or repairs project state metadata."
|
|
203
|
+
if category == "work-order":
|
|
204
|
+
return "Supports typed work-order execution and validation."
|
|
205
|
+
if category == "advanced/internal":
|
|
206
|
+
return "Advanced or internal helper; not a primary GUI action."
|
|
207
|
+
return f"Classified command: {qualified or leaf}"
|
|
208
|
+
|
|
209
|
+
|
|
210
|
+
def build_command_taxonomy_report(project_root: Path) -> CommandTaxonomyReport:
|
|
211
|
+
commands = load_command_reference(project_root.resolve())
|
|
212
|
+
entries = tuple(classify_command(command) for command in commands)
|
|
213
|
+
findings: list[str] = []
|
|
214
|
+
|
|
215
|
+
if not entries:
|
|
216
|
+
findings.append("command reference contains no commands")
|
|
217
|
+
|
|
218
|
+
allowed = set(ALLOWED_COMMAND_CATEGORIES)
|
|
219
|
+
for entry in entries:
|
|
220
|
+
if not entry.qualified_name:
|
|
221
|
+
findings.append("command without qualified_name")
|
|
222
|
+
if entry.category not in allowed:
|
|
223
|
+
findings.append(f"{entry.qualified_name}: unsupported category {entry.category}")
|
|
224
|
+
if not entry.role:
|
|
225
|
+
findings.append(f"{entry.qualified_name}: missing role")
|
|
226
|
+
if entry.composition_level not in {"composite", "elementary", "elementary/internal"}:
|
|
227
|
+
findings.append(f"{entry.qualified_name}: unsupported composition level {entry.composition_level}")
|
|
228
|
+
if not 0 <= entry.recommendation_weight <= 100:
|
|
229
|
+
findings.append(f"{entry.qualified_name}: recommendation weight outside 0..100")
|
|
230
|
+
if entry.group == "root" and entry.category == "core" and entry.qualified_name.startswith("agentic-kit audit-"):
|
|
231
|
+
findings.append(f"{entry.qualified_name}: audit command classified as core")
|
|
232
|
+
|
|
233
|
+
return CommandTaxonomyReport(
|
|
234
|
+
command_count=len(commands),
|
|
235
|
+
entries=entries,
|
|
236
|
+
findings=tuple(findings),
|
|
237
|
+
)
|
|
238
|
+
|
|
239
|
+
|
|
240
|
+
def render_command_taxonomy_report(report: CommandTaxonomyReport) -> str:
|
|
241
|
+
lines = [
|
|
242
|
+
"COMMAND_TAXONOMY_CHECK",
|
|
243
|
+
f"STATUS={report.status}",
|
|
244
|
+
f"COMMAND_COUNT={report.command_count}",
|
|
245
|
+
f"FINDING_COUNT={len(report.findings)}",
|
|
246
|
+
]
|
|
247
|
+
by_category: dict[str, int] = {}
|
|
248
|
+
for entry in report.entries:
|
|
249
|
+
by_category[entry.category] = by_category.get(entry.category, 0) + 1
|
|
250
|
+
for category, count in sorted(by_category.items()):
|
|
251
|
+
lines.append(f"CATEGORY={category}|{count}")
|
|
252
|
+
by_level: dict[str, int] = {}
|
|
253
|
+
for entry in report.entries:
|
|
254
|
+
by_level[entry.composition_level] = by_level.get(entry.composition_level, 0) + 1
|
|
255
|
+
for level, count in sorted(by_level.items()):
|
|
256
|
+
lines.append(f"COMPOSITION_LEVEL={level}|{count}")
|
|
257
|
+
for entry in sorted(report.entries, key=lambda item: (-item.recommendation_weight, item.qualified_name))[:12]:
|
|
258
|
+
if entry.recommendation_weight >= 90:
|
|
259
|
+
lines.append(
|
|
260
|
+
f"PREFERRED={entry.qualified_name}|level={entry.composition_level}|weight={entry.recommendation_weight}"
|
|
261
|
+
)
|
|
262
|
+
for finding in report.findings:
|
|
263
|
+
lines.append(f"FINDING={finding}")
|
|
264
|
+
return "\n".join(lines) + "\n"
|