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.
Files changed (310) hide show
  1. agentic_project_kit/__init__.py +1 -0
  2. agentic_project_kit/absolute_path_portability_audit.py +241 -0
  3. agentic_project_kit/access_level_policy.py +14 -0
  4. agentic_project_kit/access_levels.py +22 -0
  5. agentic_project_kit/action_registry.py +195 -0
  6. agentic_project_kit/action_specs.py +286 -0
  7. agentic_project_kit/agent_command_runner.py +516 -0
  8. agentic_project_kit/audit_test_matrix.py +36 -0
  9. agentic_project_kit/audit_test_matrix_cli.py +43 -0
  10. agentic_project_kit/chat_bootloader.py +257 -0
  11. agentic_project_kit/chat_entrypoint_contract.py +297 -0
  12. agentic_project_kit/checks.py +511 -0
  13. agentic_project_kit/ci_readiness.py +211 -0
  14. agentic_project_kit/cli.py +147 -0
  15. agentic_project_kit/cli_commands/__init__.py +1 -0
  16. agentic_project_kit/cli_commands/absolute_path_audit.py +25 -0
  17. agentic_project_kit/cli_commands/actions.py +21 -0
  18. agentic_project_kit/cli_commands/artifact_gc.py +240 -0
  19. agentic_project_kit/cli_commands/boot.py +55 -0
  20. agentic_project_kit/cli_commands/chat.py +34 -0
  21. agentic_project_kit/cli_commands/checks.py +305 -0
  22. agentic_project_kit/cli_commands/cockpit.py +103 -0
  23. agentic_project_kit/cli_commands/command_taxonomy.py +25 -0
  24. agentic_project_kit/cli_commands/commands.py +129 -0
  25. agentic_project_kit/cli_commands/dev.py +23 -0
  26. agentic_project_kit/cli_commands/direction.py +92 -0
  27. agentic_project_kit/cli_commands/doc_currency_audit.py +25 -0
  28. agentic_project_kit/cli_commands/doc_registry.py +126 -0
  29. agentic_project_kit/cli_commands/docs.py +223 -0
  30. agentic_project_kit/cli_commands/dpa.py +1338 -0
  31. agentic_project_kit/cli_commands/evidence.py +251 -0
  32. agentic_project_kit/cli_commands/github.py +34 -0
  33. agentic_project_kit/cli_commands/governance.py +14 -0
  34. agentic_project_kit/cli_commands/gui.py +23 -0
  35. agentic_project_kit/cli_commands/gui_readiness_gate.py +27 -0
  36. agentic_project_kit/cli_commands/handoff.py +183 -0
  37. agentic_project_kit/cli_commands/human_workflows.py +430 -0
  38. agentic_project_kit/cli_commands/init.py +132 -0
  39. agentic_project_kit/cli_commands/instruction.py +75 -0
  40. agentic_project_kit/cli_commands/mutation_lock_audit.py +24 -0
  41. agentic_project_kit/cli_commands/ns_legacy_audit.py +25 -0
  42. agentic_project_kit/cli_commands/pass_already_done.py +66 -0
  43. agentic_project_kit/cli_commands/patch_failure_discipline_audit.py +26 -0
  44. agentic_project_kit/cli_commands/patch_preflight.py +37 -0
  45. agentic_project_kit/cli_commands/path_literal_audit.py +39 -0
  46. agentic_project_kit/cli_commands/patterns.py +40 -0
  47. agentic_project_kit/cli_commands/planning_docs_audit.py +25 -0
  48. agentic_project_kit/cli_commands/pr.py +133 -0
  49. agentic_project_kit/cli_commands/pr_hygiene.py +27 -0
  50. agentic_project_kit/cli_commands/profiles.py +26 -0
  51. agentic_project_kit/cli_commands/program_redundancy_audit.py +25 -0
  52. agentic_project_kit/cli_commands/project_direction.py +25 -0
  53. agentic_project_kit/cli_commands/release.py +392 -0
  54. agentic_project_kit/cli_commands/release_publish_orchestration.py +36 -0
  55. agentic_project_kit/cli_commands/remote_branch_hygiene.py +152 -0
  56. agentic_project_kit/cli_commands/remote_next.py +57 -0
  57. agentic_project_kit/cli_commands/removed_ns_commands.py +65 -0
  58. agentic_project_kit/cli_commands/rule_registry.py +129 -0
  59. agentic_project_kit/cli_commands/rules.py +222 -0
  60. agentic_project_kit/cli_commands/scaffold.py +46 -0
  61. agentic_project_kit/cli_commands/slice.py +21 -0
  62. agentic_project_kit/cli_commands/standard_gates_audit_suite.py +27 -0
  63. agentic_project_kit/cli_commands/state.py +51 -0
  64. agentic_project_kit/cli_commands/status_current_state_audit.py +30 -0
  65. agentic_project_kit/cli_commands/todo.py +53 -0
  66. agentic_project_kit/cli_commands/transfer.py +19 -0
  67. agentic_project_kit/cli_commands/transfer_context_flow.py +398 -0
  68. agentic_project_kit/cli_commands/transfer_context_helpers.py +398 -0
  69. agentic_project_kit/cli_commands/transfer_diagnostics.py +767 -0
  70. agentic_project_kit/cli_commands/transfer_evidence_flow.py +368 -0
  71. agentic_project_kit/cli_commands/transfer_handoff_flow.py +594 -0
  72. agentic_project_kit/cli_commands/transfer_order_contract.py +74 -0
  73. agentic_project_kit/cli_commands/transfer_post_merge_complete.py +442 -0
  74. agentic_project_kit/cli_commands/transfer_post_merge_settle.py +171 -0
  75. agentic_project_kit/cli_commands/transfer_pr_closeout_complete.py +124 -0
  76. agentic_project_kit/cli_commands/transfer_pr_create_flow.py +743 -0
  77. agentic_project_kit/cli_commands/transfer_pr_merge_flow.py +547 -0
  78. agentic_project_kit/cli_commands/transfer_repo_after_pr.py +160 -0
  79. agentic_project_kit/cli_commands/transfer_repo_pre_pr.py +664 -0
  80. agentic_project_kit/cli_commands/transfer_runner_flow.py +131 -0
  81. agentic_project_kit/cli_commands/transfer_runtime_initial.py +194 -0
  82. agentic_project_kit/cli_commands/transfer_shared.py +274 -0
  83. agentic_project_kit/cli_commands/validation.py +200 -0
  84. agentic_project_kit/cli_commands/version.py +27 -0
  85. agentic_project_kit/cli_commands/work_orders.py +219 -0
  86. agentic_project_kit/cli_commands/workflow.py +471 -0
  87. agentic_project_kit/cli_commands/workflow_guard.py +26 -0
  88. agentic_project_kit/cli_commands/workspace.py +252 -0
  89. agentic_project_kit/cockpit.py +608 -0
  90. agentic_project_kit/cockpit_readiness.py +67 -0
  91. agentic_project_kit/command_authority_audit.py +192 -0
  92. agentic_project_kit/command_inbox_check.py +103 -0
  93. agentic_project_kit/command_manifest.py +591 -0
  94. agentic_project_kit/command_queue_contract.py +161 -0
  95. agentic_project_kit/command_selector.py +245 -0
  96. agentic_project_kit/command_taxonomy.py +264 -0
  97. agentic_project_kit/commit_guard.py +67 -0
  98. agentic_project_kit/communication_artifact_gc.py +495 -0
  99. agentic_project_kit/communication_rule_context.py +437 -0
  100. agentic_project_kit/communication_state.py +115 -0
  101. agentic_project_kit/composed_wrapper_invariants.py +97 -0
  102. agentic_project_kit/composite_short_circuit.py +140 -0
  103. agentic_project_kit/contract.py +251 -0
  104. agentic_project_kit/control_state.py +83 -0
  105. agentic_project_kit/doc_currency_audit.py +536 -0
  106. agentic_project_kit/doc_lifecycle.py +1179 -0
  107. agentic_project_kit/doc_lifecycle_signals.py +446 -0
  108. agentic_project_kit/doc_lifecycle_sweep.py +639 -0
  109. agentic_project_kit/doc_mesh.py +491 -0
  110. agentic_project_kit/doctor.py +379 -0
  111. agentic_project_kit/documentation_registry.py +1122 -0
  112. agentic_project_kit/documentation_system_audit.py +310 -0
  113. agentic_project_kit/dpa_current_handoff_lifecycle.py +549 -0
  114. agentic_project_kit/dpa_dp2_decision_readiness.py +362 -0
  115. agentic_project_kit/dpa_dp3_dp4_adjudication.py +604 -0
  116. agentic_project_kit/dpa_dp5_block_new_gate.py +294 -0
  117. agentic_project_kit/dpa_dp5_stage_adoption.py +615 -0
  118. agentic_project_kit/dpa_dp5_strict_gate.py +187 -0
  119. agentic_project_kit/dpa_final_closeout.py +627 -0
  120. agentic_project_kit/dpa_fixture_evidence.py +680 -0
  121. agentic_project_kit/dpa_maintainer_record_check.py +711 -0
  122. agentic_project_kit/dpa_post_dp2_scope_assessment.py +734 -0
  123. agentic_project_kit/dpa_probe_002_readiness.py +509 -0
  124. agentic_project_kit/dpa_probe_003_readiness.py +417 -0
  125. agentic_project_kit/dpa_probe_004_readiness.py +437 -0
  126. agentic_project_kit/dpa_readiness.py +505 -0
  127. agentic_project_kit/dpa_readonly_probe_execution.py +500 -0
  128. agentic_project_kit/dpa_registry_contracts.py +312 -0
  129. agentic_project_kit/dpa_renderer_readiness.py +407 -0
  130. agentic_project_kit/dpa_repo_adoption_assessment.py +574 -0
  131. agentic_project_kit/dpa_stable_readiness.py +743 -0
  132. agentic_project_kit/dpa_successor_projection.py +168 -0
  133. agentic_project_kit/dpa_workspace_init_projection.py +93 -0
  134. agentic_project_kit/dpa_wrt_ch001_evidence.py +327 -0
  135. agentic_project_kit/entrypoint_slice_runner.py +153 -0
  136. agentic_project_kit/evidence_clean.py +152 -0
  137. agentic_project_kit/evidence_commit_paths.py +214 -0
  138. agentic_project_kit/evidence_finalize_log.py +204 -0
  139. agentic_project_kit/evidence_guard.py +114 -0
  140. agentic_project_kit/evidence_inspector.py +507 -0
  141. agentic_project_kit/evidence_state_contract.py +99 -0
  142. agentic_project_kit/execution_mode_state.py +148 -0
  143. agentic_project_kit/final_summary_contract.py +85 -0
  144. agentic_project_kit/finalize_guard.py +139 -0
  145. agentic_project_kit/gatekeeper_core.py +232 -0
  146. agentic_project_kit/github.py +25 -0
  147. agentic_project_kit/governance.py +59 -0
  148. agentic_project_kit/gui_action_execution.py +105 -0
  149. agentic_project_kit/gui_action_renderer.py +81 -0
  150. agentic_project_kit/gui_action_run_intent.py +98 -0
  151. agentic_project_kit/gui_action_views.py +195 -0
  152. agentic_project_kit/gui_activity_log.py +76 -0
  153. agentic_project_kit/gui_button_catalog.py +605 -0
  154. agentic_project_kit/gui_cockpit.py +365 -0
  155. agentic_project_kit/gui_cockpit_actions.py +695 -0
  156. agentic_project_kit/gui_cockpit_common.py +172 -0
  157. agentic_project_kit/gui_cockpit_header.py +501 -0
  158. agentic_project_kit/gui_cockpit_sidebar.py +387 -0
  159. agentic_project_kit/gui_cockpit_task.py +748 -0
  160. agentic_project_kit/gui_command_projection.py +355 -0
  161. agentic_project_kit/gui_communication_modes.py +130 -0
  162. agentic_project_kit/gui_dry_run.py +219 -0
  163. agentic_project_kit/gui_gatekeeper_status.py +259 -0
  164. agentic_project_kit/gui_layout_plan.py +148 -0
  165. agentic_project_kit/gui_open_folder.py +18 -0
  166. agentic_project_kit/gui_output_status_panel.py +61 -0
  167. agentic_project_kit/gui_panel_state.py +47 -0
  168. agentic_project_kit/gui_presenter.py +62 -0
  169. agentic_project_kit/gui_readiness_gate.py +206 -0
  170. agentic_project_kit/gui_release_flow.py +59 -0
  171. agentic_project_kit/gui_task_editor.py +1077 -0
  172. agentic_project_kit/gui_tk_widgets.py +177 -0
  173. agentic_project_kit/gui_tkinter_renderer.py +87 -0
  174. agentic_project_kit/gui_tkinter_shell.py +1176 -0
  175. agentic_project_kit/gui_transfer_contract.py +28 -0
  176. agentic_project_kit/gui_viewmodel.py +698 -0
  177. agentic_project_kit/gui_window_guard.py +46 -0
  178. agentic_project_kit/handoff_freshness.py +390 -0
  179. agentic_project_kit/handoff_prompt.py +180 -0
  180. agentic_project_kit/handoff_state.py +254 -0
  181. agentic_project_kit/instruction_lint.py +455 -0
  182. agentic_project_kit/llm_context_carriers.py +66 -0
  183. agentic_project_kit/llm_execution_context.py +285 -0
  184. agentic_project_kit/local_command_stack.py +133 -0
  185. agentic_project_kit/local_feature_gate.py +82 -0
  186. agentic_project_kit/local_garbage_collector.py +269 -0
  187. agentic_project_kit/models.py +18 -0
  188. agentic_project_kit/mutation_lock_audit.py +421 -0
  189. agentic_project_kit/next_turn_evidence.py +253 -0
  190. agentic_project_kit/next_turn_merge_if_green.py +459 -0
  191. agentic_project_kit/next_turn_pr_status.py +255 -0
  192. agentic_project_kit/next_turn_result.py +183 -0
  193. agentic_project_kit/next_turn_runner.py +159 -0
  194. agentic_project_kit/next_turn_slot.py +107 -0
  195. agentic_project_kit/next_turn_status.py +274 -0
  196. agentic_project_kit/ns_legacy_reference_audit.py +258 -0
  197. agentic_project_kit/ns_slice_runner.py +16 -0
  198. agentic_project_kit/operational_handoff_projection.py +124 -0
  199. agentic_project_kit/output_contract.py +80 -0
  200. agentic_project_kit/output_repair.py +61 -0
  201. agentic_project_kit/pass_already_done.py +216 -0
  202. agentic_project_kit/patch_artifact_preflight.py +147 -0
  203. agentic_project_kit/patch_cycle_workflow.py +440 -0
  204. agentic_project_kit/patch_failure_discipline_audit.py +223 -0
  205. agentic_project_kit/patch_preflight.py +215 -0
  206. agentic_project_kit/path_literal_audit.py +612 -0
  207. agentic_project_kit/patterns.py +99 -0
  208. agentic_project_kit/planning_docs_consolidation_audit.py +313 -0
  209. agentic_project_kit/portability_shell_gate.py +40 -0
  210. agentic_project_kit/post_merge_handoff_refresh.py +56 -0
  211. agentic_project_kit/post_release.py +252 -0
  212. agentic_project_kit/post_release_closeout.py +616 -0
  213. agentic_project_kit/pr_cleanup.py +172 -0
  214. agentic_project_kit/pr_closeout.py +50 -0
  215. agentic_project_kit/pr_closeout_complete.py +344 -0
  216. agentic_project_kit/pr_create_or_skip.py +71 -0
  217. agentic_project_kit/pr_hygiene.py +210 -0
  218. agentic_project_kit/program_redundancy_audit.py +251 -0
  219. agentic_project_kit/project_direction.py +840 -0
  220. agentic_project_kit/protected_change_planner.py +194 -0
  221. agentic_project_kit/release.py +401 -0
  222. agentic_project_kit/release_doi_safety.py +24 -0
  223. agentic_project_kit/release_gate_core.py +174 -0
  224. agentic_project_kit/release_metadata_authority_gate.py +328 -0
  225. agentic_project_kit/release_metadata_prep.py +43 -0
  226. agentic_project_kit/release_notes.py +823 -0
  227. agentic_project_kit/release_prep_core.py +204 -0
  228. agentic_project_kit/release_prepare.py +369 -0
  229. agentic_project_kit/release_process_guardrails.py +247 -0
  230. agentic_project_kit/release_publish_core.py +114 -0
  231. agentic_project_kit/release_publish_orchestration.py +469 -0
  232. agentic_project_kit/release_state.py +552 -0
  233. agentic_project_kit/release_verify_core.py +175 -0
  234. agentic_project_kit/remote_branch_hygiene.py +223 -0
  235. agentic_project_kit/remote_next.py +89 -0
  236. agentic_project_kit/remote_next_closeout.py +101 -0
  237. agentic_project_kit/remote_next_order_contract.py +391 -0
  238. agentic_project_kit/removed_ns_commands.py +458 -0
  239. agentic_project_kit/removed_source_audit.py +225 -0
  240. agentic_project_kit/rendering.py +13 -0
  241. agentic_project_kit/repair_report.py +79 -0
  242. agentic_project_kit/repo_identity.py +59 -0
  243. agentic_project_kit/result_report_classifier.py +57 -0
  244. agentic_project_kit/rule_ack.py +105 -0
  245. agentic_project_kit/rule_preservation.py +58 -0
  246. agentic_project_kit/rule_refresh.py +174 -0
  247. agentic_project_kit/rule_registry_registration.py +322 -0
  248. agentic_project_kit/rule_registry_report.py +206 -0
  249. agentic_project_kit/rule_registry_validator.py +704 -0
  250. agentic_project_kit/rule_snapshot.py +98 -0
  251. agentic_project_kit/rule_source_validator.py +133 -0
  252. agentic_project_kit/run_summary.py +68 -0
  253. agentic_project_kit/run_summary_renderer.py +238 -0
  254. agentic_project_kit/runtime_validator.py +68 -0
  255. agentic_project_kit/safe_file_replace.py +35 -0
  256. agentic_project_kit/safe_patch.py +47 -0
  257. agentic_project_kit/safe_push.py +218 -0
  258. agentic_project_kit/safe_remove_diagnostic.py +51 -0
  259. agentic_project_kit/scaffold.py +69 -0
  260. agentic_project_kit/site_claims.py +444 -0
  261. agentic_project_kit/site_generator.py +1140 -0
  262. agentic_project_kit/slice_gate.py +225 -0
  263. agentic_project_kit/slice_runner.py +75 -0
  264. agentic_project_kit/standard_gates_audit_suite.py +167 -0
  265. agentic_project_kit/state_freshness.py +135 -0
  266. agentic_project_kit/status_current_state_audit.py +762 -0
  267. agentic_project_kit/successor_handoff_package.py +1763 -0
  268. agentic_project_kit/templates.py +292 -0
  269. agentic_project_kit/terminal_block_guard.py +166 -0
  270. agentic_project_kit/terminal_logging.py +275 -0
  271. agentic_project_kit/todo.py +97 -0
  272. agentic_project_kit/transfer_closeout.py +136 -0
  273. agentic_project_kit/transfer_continue.py +215 -0
  274. agentic_project_kit/transfer_local_runner.py +101 -0
  275. agentic_project_kit/transfer_operation_monitor.py +236 -0
  276. agentic_project_kit/transfer_post_merge_lifecycle.py +356 -0
  277. agentic_project_kit/transfer_post_merge_settle.py +383 -0
  278. agentic_project_kit/transfer_pr_actions.py +164 -0
  279. agentic_project_kit/transfer_remote_next.py +959 -0
  280. agentic_project_kit/transfer_repo_actions.py +2253 -0
  281. agentic_project_kit/transfer_runner.py +347 -0
  282. agentic_project_kit/transfer_safety_context.py +291 -0
  283. agentic_project_kit/transfer_state.py +483 -0
  284. agentic_project_kit/transfer_uplink.py +471 -0
  285. agentic_project_kit/transfer_workflow_next.py +148 -0
  286. agentic_project_kit/typed_work_order_evidence.py +58 -0
  287. agentic_project_kit/typed_work_order_queue.py +166 -0
  288. agentic_project_kit/typed_work_order_runner.py +168 -0
  289. agentic_project_kit/work_cycle.py +317 -0
  290. agentic_project_kit/work_discard_changes.py +129 -0
  291. agentic_project_kit/work_order_patch.py +199 -0
  292. agentic_project_kit/work_order_runner.py +120 -0
  293. agentic_project_kit/work_order_uploader.py +220 -0
  294. agentic_project_kit/work_order_validator.py +185 -0
  295. agentic_project_kit/work_orders.py +246 -0
  296. agentic_project_kit/workflow_guard.py +262 -0
  297. agentic_project_kit/workflow_summary_runner.py +124 -0
  298. agentic_project_kit/workspace.py +665 -0
  299. agentic_project_kit/workspace_adopt.py +471 -0
  300. agentic_project_kit/workspace_dpa_intake.py +406 -0
  301. agentic_project_kit/workspace_init.py +487 -0
  302. agentic_project_kit/workspace_lock.py +129 -0
  303. agentic_project_kit/workspace_remove.py +381 -0
  304. agentic_project_kit/workspace_upgrade.py +324 -0
  305. agentic_project_kit/wrapper_live_status.py +104 -0
  306. agentic_project_kit-1.0.1.dist-info/METADATA +724 -0
  307. agentic_project_kit-1.0.1.dist-info/RECORD +310 -0
  308. agentic_project_kit-1.0.1.dist-info/WHEEL +4 -0
  309. agentic_project_kit-1.0.1.dist-info/entry_points.txt +3 -0
  310. 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"