graph-agents-cli 0.3.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 (291) hide show
  1. graph_agents_cli/__init__.py +26 -0
  2. graph_agents_cli/_api_policy.py +2145 -0
  3. graph_agents_cli/_approvals.py +400 -0
  4. graph_agents_cli/_build.py +186 -0
  5. graph_agents_cli/_build_info.json +7 -0
  6. graph_agents_cli/_chat_client.py +462 -0
  7. graph_agents_cli/_click.py +157 -0
  8. graph_agents_cli/_defaults.py +139 -0
  9. graph_agents_cli/_experiments.py +64 -0
  10. graph_agents_cli/_http.py +192 -0
  11. graph_agents_cli/_output.py +83 -0
  12. graph_agents_cli/_project.py +462 -0
  13. graph_agents_cli/_remote.py +220 -0
  14. graph_agents_cli/_response_schema.py +264 -0
  15. graph_agents_cli/_runner.py +319 -0
  16. graph_agents_cli/_skills_check.py +274 -0
  17. graph_agents_cli/_tools.py +189 -0
  18. graph_agents_cli/_trust.py +66 -0
  19. graph_agents_cli/api/__init__.py +15 -0
  20. graph_agents_cli/api/_changes.py +506 -0
  21. graph_agents_cli/api/_files.py +658 -0
  22. graph_agents_cli/api/cmd_api.py +2480 -0
  23. graph_agents_cli/deploy/__init__.py +15 -0
  24. graph_agents_cli/deploy/_config.py +171 -0
  25. graph_agents_cli/deploy/_image.py +128 -0
  26. graph_agents_cli/deploy/_kube.py +286 -0
  27. graph_agents_cli/deploy/_modes.py +234 -0
  28. graph_agents_cli/deploy/_preflight.py +370 -0
  29. graph_agents_cli/deploy/_values.py +168 -0
  30. graph_agents_cli/deploy/cmd_deploy.py +1866 -0
  31. graph_agents_cli/deploy/gitops.py +562 -0
  32. graph_agents_cli/deploy/local_load.py +273 -0
  33. graph_agents_cli/dev/__init__.py +13 -0
  34. graph_agents_cli/dev/cmd_build.py +131 -0
  35. graph_agents_cli/dev/cmd_install.py +78 -0
  36. graph_agents_cli/dev/cmd_lint.py +119 -0
  37. graph_agents_cli/dev/cmd_playground.py +297 -0
  38. graph_agents_cli/dev/policy_check.py +1287 -0
  39. graph_agents_cli/eval/__init__.py +22 -0
  40. graph_agents_cli/eval/_client.py +670 -0
  41. graph_agents_cli/eval/_common.py +177 -0
  42. graph_agents_cli/eval/_judge.py +168 -0
  43. graph_agents_cli/eval/_judge_runner.py +238 -0
  44. graph_agents_cli/eval/_paths.py +212 -0
  45. graph_agents_cli/eval/checks.py +581 -0
  46. graph_agents_cli/eval/cmd_analyze.py +278 -0
  47. graph_agents_cli/eval/cmd_compare.py +284 -0
  48. graph_agents_cli/eval/cmd_eval_group.py +80 -0
  49. graph_agents_cli/eval/cmd_generate.py +558 -0
  50. graph_agents_cli/eval/cmd_grade.py +466 -0
  51. graph_agents_cli/eval/cmd_metric.py +156 -0
  52. graph_agents_cli/eval/cmd_run.py +370 -0
  53. graph_agents_cli/eval/cmd_submit.py +400 -0
  54. graph_agents_cli/eval/config.py +435 -0
  55. graph_agents_cli/eval/dataset.py +350 -0
  56. graph_agents_cli/eval/gate.py +420 -0
  57. graph_agents_cli/eval/transcript.py +192 -0
  58. graph_agents_cli/extension/__init__.py +13 -0
  59. graph_agents_cli/extension/_compat.py +86 -0
  60. graph_agents_cli/extension/_loader.py +293 -0
  61. graph_agents_cli/extension/_manifest.py +135 -0
  62. graph_agents_cli/extension/_overrides.py +195 -0
  63. graph_agents_cli/extension/_paths.py +91 -0
  64. graph_agents_cli/extension/_refs.py +193 -0
  65. graph_agents_cli/extension/_resolver.py +453 -0
  66. graph_agents_cli/extension/_schema.py +106 -0
  67. graph_agents_cli/extension/_spec.py +253 -0
  68. graph_agents_cli/extension/_sync.py +102 -0
  69. graph_agents_cli/extension/_trust.py +58 -0
  70. graph_agents_cli/extension/cmd_extension_add.py +259 -0
  71. graph_agents_cli/extension/cmd_extension_group.py +57 -0
  72. graph_agents_cli/extension/cmd_extension_list.py +56 -0
  73. graph_agents_cli/extension/cmd_extension_remove.py +61 -0
  74. graph_agents_cli/extension/cmd_extension_update.py +195 -0
  75. graph_agents_cli/info/__init__.py +13 -0
  76. graph_agents_cli/info/cmd_info.py +222 -0
  77. graph_agents_cli/infra/__init__.py +15 -0
  78. graph_agents_cli/infra/checks.py +1169 -0
  79. graph_agents_cli/infra/cmd_infra.py +103 -0
  80. graph_agents_cli/main.py +591 -0
  81. graph_agents_cli/peer/__init__.py +15 -0
  82. graph_agents_cli/peer/_generate.py +254 -0
  83. graph_agents_cli/peer/cmd_peer.py +1151 -0
  84. graph_agents_cli/run/__init__.py +13 -0
  85. graph_agents_cli/run/_local_server.py +1157 -0
  86. graph_agents_cli/run/_signals.py +141 -0
  87. graph_agents_cli/run/cmd_approvals.py +530 -0
  88. graph_agents_cli/run/cmd_run.py +1421 -0
  89. graph_agents_cli/scaffold/__init__.py +19 -0
  90. graph_agents_cli/scaffold/agents/README.md +24 -0
  91. graph_agents_cli/scaffold/agents/empty_py/.template/templateconfig.yaml +22 -0
  92. graph_agents_cli/scaffold/agents/langgraph/.env.example +292 -0
  93. graph_agents_cli/scaffold/agents/langgraph/.template/templateconfig.yaml +28 -0
  94. graph_agents_cli/scaffold/agents/langgraph/Dockerfile +59 -0
  95. graph_agents_cli/scaffold/agents/langgraph/Dockerfile.langgraph-server +59 -0
  96. graph_agents_cli/scaffold/agents/langgraph/README.md +571 -0
  97. graph_agents_cli/scaffold/agents/langgraph/api-policy.yaml +60 -0
  98. graph_agents_cli/scaffold/agents/langgraph/app/__init__.py +20 -0
  99. graph_agents_cli/scaffold/agents/langgraph/app/agent.py +174 -0
  100. graph_agents_cli/scaffold/agents/langgraph/app/app_utils/__init__.py +15 -0
  101. graph_agents_cli/scaffold/agents/langgraph/app/app_utils/a2a.py +2162 -0
  102. graph_agents_cli/scaffold/agents/langgraph/app/app_utils/a2a_client.py +1167 -0
  103. graph_agents_cli/scaffold/agents/langgraph/app/app_utils/api_client.py +4220 -0
  104. graph_agents_cli/scaffold/agents/langgraph/app/app_utils/approvals.py +1349 -0
  105. graph_agents_cli/scaffold/agents/langgraph/app/app_utils/auth.py +1986 -0
  106. graph_agents_cli/scaffold/agents/langgraph/app/app_utils/chat.py +2962 -0
  107. graph_agents_cli/scaffold/agents/langgraph/app/app_utils/checkpointer.py +432 -0
  108. graph_agents_cli/scaffold/agents/langgraph/app/app_utils/content.py +569 -0
  109. graph_agents_cli/scaffold/agents/langgraph/app/app_utils/db.py +580 -0
  110. graph_agents_cli/scaffold/agents/langgraph/app/app_utils/limits.py +203 -0
  111. graph_agents_cli/scaffold/agents/langgraph/app/app_utils/metrics.py +231 -0
  112. graph_agents_cli/scaffold/agents/langgraph/app/app_utils/middleware.py +361 -0
  113. graph_agents_cli/scaffold/agents/langgraph/app/app_utils/model.py +611 -0
  114. graph_agents_cli/scaffold/agents/langgraph/app/app_utils/playground.py +230 -0
  115. graph_agents_cli/scaffold/agents/langgraph/app/app_utils/run_locks.py +459 -0
  116. graph_agents_cli/scaffold/agents/langgraph/app/app_utils/structured.py +755 -0
  117. graph_agents_cli/scaffold/agents/langgraph/app/app_utils/telemetry.py +681 -0
  118. graph_agents_cli/scaffold/agents/langgraph/app/app_utils/threads.py +493 -0
  119. graph_agents_cli/scaffold/agents/langgraph/app/app_utils/token_exchange.py +959 -0
  120. graph_agents_cli/scaffold/agents/langgraph/app/fast_api_app.py +770 -0
  121. graph_agents_cli/scaffold/agents/langgraph/app/policies/__init__.py +55 -0
  122. graph_agents_cli/scaffold/agents/langgraph/app/policies/custom.py +97 -0
  123. graph_agents_cli/scaffold/agents/langgraph/app/tools/__init__.py +46 -0
  124. graph_agents_cli/scaffold/agents/langgraph/app/tools/example_api.py +92 -0
  125. graph_agents_cli/scaffold/agents/langgraph/app/tools/weather.py +33 -0
  126. graph_agents_cli/scaffold/agents/langgraph/langgraph.json +14 -0
  127. graph_agents_cli/scaffold/agents/langgraph/pyproject.toml +78 -0
  128. graph_agents_cli/scaffold/agents/langgraph/tests/conftest.py +376 -0
  129. graph_agents_cli/scaffold/agents/langgraph/tests/eval/datasets/basic-dataset.json +53 -0
  130. graph_agents_cli/scaffold/agents/langgraph/tests/eval/eval_config.yaml +32 -0
  131. graph_agents_cli/scaffold/agents/langgraph/tests/integration/approval_graph.py +137 -0
  132. graph_agents_cli/scaffold/agents/langgraph/tests/integration/fake_issuer.py +216 -0
  133. graph_agents_cli/scaffold/agents/langgraph/tests/integration/fake_openai.py +357 -0
  134. graph_agents_cli/scaffold/agents/langgraph/tests/integration/test_a2a_outcomes.py +569 -0
  135. graph_agents_cli/scaffold/agents/langgraph/tests/integration/test_a2a_relay.py +479 -0
  136. graph_agents_cli/scaffold/agents/langgraph/tests/integration/test_api_surface.py +812 -0
  137. graph_agents_cli/scaffold/agents/langgraph/tests/integration/test_approvals.py +1367 -0
  138. graph_agents_cli/scaffold/agents/langgraph/tests/integration/test_approvals_server.py +794 -0
  139. graph_agents_cli/scaffold/agents/langgraph/tests/integration/test_cross_actor.py +497 -0
  140. graph_agents_cli/scaffold/agents/langgraph/tests/integration/test_cross_actor_server.py +247 -0
  141. graph_agents_cli/scaffold/agents/langgraph/tests/integration/test_history_repair.py +278 -0
  142. graph_agents_cli/scaffold/agents/langgraph/tests/integration/test_model_apis.py +242 -0
  143. graph_agents_cli/scaffold/agents/langgraph/tests/integration/test_postgres.py +637 -0
  144. graph_agents_cli/scaffold/agents/langgraph/tests/integration/test_resilience_postgres.py +770 -0
  145. graph_agents_cli/scaffold/agents/langgraph/tests/integration/test_runtime_guardrails.py +854 -0
  146. graph_agents_cli/scaffold/agents/langgraph/tests/integration/test_server_e2e.py +340 -0
  147. graph_agents_cli/scaffold/agents/langgraph/tests/integration/test_server_runtime.py +989 -0
  148. graph_agents_cli/scaffold/agents/langgraph/tests/integration/test_structured_answers.py +584 -0
  149. graph_agents_cli/scaffold/agents/langgraph/tests/integration/test_structured_server.py +222 -0
  150. graph_agents_cli/scaffold/agents/langgraph/tests/integration/test_token_exchange_issuer.py +650 -0
  151. graph_agents_cli/scaffold/agents/langgraph/tests/load_test/.results/.placeholder +0 -0
  152. graph_agents_cli/scaffold/agents/langgraph/tests/load_test/README.md +22 -0
  153. graph_agents_cli/scaffold/agents/langgraph/tests/load_test/conftest.py +21 -0
  154. graph_agents_cli/scaffold/agents/langgraph/tests/load_test/load_test.py +81 -0
  155. graph_agents_cli/scaffold/agents/langgraph/tests/unit/test_a2a_client.py +824 -0
  156. graph_agents_cli/scaffold/agents/langgraph/tests/unit/test_a2a_scoping.py +724 -0
  157. graph_agents_cli/scaffold/agents/langgraph/tests/unit/test_api_client.py +1214 -0
  158. graph_agents_cli/scaffold/agents/langgraph/tests/unit/test_api_client_hardening.py +716 -0
  159. graph_agents_cli/scaffold/agents/langgraph/tests/unit/test_api_policy_rpc.py +767 -0
  160. graph_agents_cli/scaffold/agents/langgraph/tests/unit/test_approval_ledger.py +1536 -0
  161. graph_agents_cli/scaffold/agents/langgraph/tests/unit/test_fake_model.py +115 -0
  162. graph_agents_cli/scaffold/agents/langgraph/tests/unit/test_jwt_policy.py +991 -0
  163. graph_agents_cli/scaffold/agents/langgraph/tests/unit/test_limits.py +310 -0
  164. graph_agents_cli/scaffold/agents/langgraph/tests/unit/test_logging.py +148 -0
  165. graph_agents_cli/scaffold/agents/langgraph/tests/unit/test_logging_hardening.py +271 -0
  166. graph_agents_cli/scaffold/agents/langgraph/tests/unit/test_policy.py +378 -0
  167. graph_agents_cli/scaffold/agents/langgraph/tests/unit/test_resilience.py +610 -0
  168. graph_agents_cli/scaffold/agents/langgraph/tests/unit/test_server_auth.py +702 -0
  169. graph_agents_cli/scaffold/agents/langgraph/tests/unit/test_structured.py +673 -0
  170. graph_agents_cli/scaffold/agents/langgraph/tests/unit/test_telemetry.py +404 -0
  171. graph_agents_cli/scaffold/agents/langgraph/tests/unit/test_thread_listing.py +255 -0
  172. graph_agents_cli/scaffold/agents/langgraph/tests/unit/test_threads.py +268 -0
  173. graph_agents_cli/scaffold/agents/langgraph/tests/unit/test_token_exchange.py +1320 -0
  174. graph_agents_cli/scaffold/agents/langgraph/tests/unit/test_untrusted_content.py +393 -0
  175. graph_agents_cli/scaffold/agents/langgraph/uv-fastapi.lock +2084 -0
  176. graph_agents_cli/scaffold/agents/langgraph/uv-langgraph-server.lock +2106 -0
  177. graph_agents_cli/scaffold/agents/langgraph/{{cookiecutter.agent_guidance_filename}} +129 -0
  178. graph_agents_cli/scaffold/base_templates/_shared/graph-agents-cli-manifest.yaml +36 -0
  179. graph_agents_cli/scaffold/base_templates/python/.dockerignore +32 -0
  180. graph_agents_cli/scaffold/base_templates/python/.github/CODEOWNERS +30 -0
  181. graph_agents_cli/scaffold/base_templates/python/.github/agent.env +7 -0
  182. graph_agents_cli/scaffold/base_templates/python/.github/workflows/pr_checks.yaml +214 -0
  183. graph_agents_cli/scaffold/base_templates/python/.gitignore +209 -0
  184. graph_agents_cli/scaffold/base_templates/python/tests/unit/test_dummy.py +23 -0
  185. graph_agents_cli/scaffold/base_templates/python/{{cookiecutter.agent_guidance_filename}} +35 -0
  186. graph_agents_cli/scaffold/cmd_scaffold_group.py +49 -0
  187. graph_agents_cli/scaffold/commands/__init__.py +13 -0
  188. graph_agents_cli/scaffold/commands/create.py +1424 -0
  189. graph_agents_cli/scaffold/commands/enhance.py +1652 -0
  190. graph_agents_cli/scaffold/commands/upgrade.py +570 -0
  191. graph_agents_cli/scaffold/deployment_targets/kubernetes/python/.github/agent.env +12 -0
  192. graph_agents_cli/scaffold/deployment_targets/kubernetes/python/.github/workflows/promote-to-prod.yaml +371 -0
  193. graph_agents_cli/scaffold/deployment_targets/kubernetes/python/.github/workflows/staging.yaml +450 -0
  194. graph_agents_cli/scaffold/deployment_targets/kubernetes/python/deployment/argocd/application-dev.yaml +43 -0
  195. graph_agents_cli/scaffold/deployment_targets/kubernetes/python/deployment/argocd/application-prod.yaml +41 -0
  196. graph_agents_cli/scaffold/deployment_targets/kubernetes/python/deployment/argocd/application-staging.yaml +43 -0
  197. graph_agents_cli/scaffold/deployment_targets/kubernetes/python/deployment/helm/{{cookiecutter.project_name}}/.helmignore +14 -0
  198. graph_agents_cli/scaffold/deployment_targets/kubernetes/python/deployment/helm/{{cookiecutter.project_name}}/Chart.yaml +21 -0
  199. graph_agents_cli/scaffold/deployment_targets/kubernetes/python/deployment/helm/{{cookiecutter.project_name}}/examples/networkpolicy.yaml +103 -0
  200. graph_agents_cli/scaffold/deployment_targets/kubernetes/python/deployment/helm/{{cookiecutter.project_name}}/templates/NOTES.txt +48 -0
  201. graph_agents_cli/scaffold/deployment_targets/kubernetes/python/deployment/helm/{{cookiecutter.project_name}}/templates/_helpers.tpl +189 -0
  202. graph_agents_cli/scaffold/deployment_targets/kubernetes/python/deployment/helm/{{cookiecutter.project_name}}/templates/certificate.yaml +15 -0
  203. graph_agents_cli/scaffold/deployment_targets/kubernetes/python/deployment/helm/{{cookiecutter.project_name}}/templates/configmap.yaml +10 -0
  204. graph_agents_cli/scaffold/deployment_targets/kubernetes/python/deployment/helm/{{cookiecutter.project_name}}/templates/deployment.yaml +199 -0
  205. graph_agents_cli/scaffold/deployment_targets/kubernetes/python/deployment/helm/{{cookiecutter.project_name}}/templates/hpa.yaml +22 -0
  206. graph_agents_cli/scaffold/deployment_targets/kubernetes/python/deployment/helm/{{cookiecutter.project_name}}/templates/httproute.yaml +30 -0
  207. graph_agents_cli/scaffold/deployment_targets/kubernetes/python/deployment/helm/{{cookiecutter.project_name}}/templates/ingress.yaml +39 -0
  208. graph_agents_cli/scaffold/deployment_targets/kubernetes/python/deployment/helm/{{cookiecutter.project_name}}/templates/networkpolicy.yaml +48 -0
  209. graph_agents_cli/scaffold/deployment_targets/kubernetes/python/deployment/helm/{{cookiecutter.project_name}}/templates/pdb.yaml +13 -0
  210. graph_agents_cli/scaffold/deployment_targets/kubernetes/python/deployment/helm/{{cookiecutter.project_name}}/templates/postgresql-secret.yaml +37 -0
  211. graph_agents_cli/scaffold/deployment_targets/kubernetes/python/deployment/helm/{{cookiecutter.project_name}}/templates/service.yaml +15 -0
  212. graph_agents_cli/scaffold/deployment_targets/kubernetes/python/deployment/helm/{{cookiecutter.project_name}}/templates/serviceaccount.yaml +13 -0
  213. graph_agents_cli/scaffold/deployment_targets/kubernetes/python/deployment/helm/{{cookiecutter.project_name}}/templates/servicemonitor.yaml +42 -0
  214. graph_agents_cli/scaffold/deployment_targets/kubernetes/python/deployment/helm/{{cookiecutter.project_name}}/values-dev.yaml +22 -0
  215. graph_agents_cli/scaffold/deployment_targets/kubernetes/python/deployment/helm/{{cookiecutter.project_name}}/values-prod.yaml +45 -0
  216. graph_agents_cli/scaffold/deployment_targets/kubernetes/python/deployment/helm/{{cookiecutter.project_name}}/values-staging.yaml +29 -0
  217. graph_agents_cli/scaffold/deployment_targets/kubernetes/python/deployment/helm/{{cookiecutter.project_name}}/values.yaml +396 -0
  218. graph_agents_cli/scaffold/deployment_targets/kubernetes/python/tests/integration/test_chart.py +269 -0
  219. graph_agents_cli/scaffold/deployment_targets/none/README.md +5 -0
  220. graph_agents_cli/scaffold/deployment_targets/none/python/README.md +6 -0
  221. graph_agents_cli/scaffold/utils/__init__.py +13 -0
  222. graph_agents_cli/scaffold/utils/backup.py +212 -0
  223. graph_agents_cli/scaffold/utils/build_record.py +257 -0
  224. graph_agents_cli/scaffold/utils/cli_options.py +184 -0
  225. graph_agents_cli/scaffold/utils/fs.py +83 -0
  226. graph_agents_cli/scaffold/utils/generate_locks.py +214 -0
  227. graph_agents_cli/scaffold/utils/generation_metadata.py +88 -0
  228. graph_agents_cli/scaffold/utils/keyedit.py +768 -0
  229. graph_agents_cli/scaffold/utils/keymerge.py +537 -0
  230. graph_agents_cli/scaffold/utils/language.py +138 -0
  231. graph_agents_cli/scaffold/utils/lock_utils.py +94 -0
  232. graph_agents_cli/scaffold/utils/logging.py +77 -0
  233. graph_agents_cli/scaffold/utils/manifest.py +292 -0
  234. graph_agents_cli/scaffold/utils/merge.py +970 -0
  235. graph_agents_cli/scaffold/utils/merge3.py +216 -0
  236. graph_agents_cli/scaffold/utils/openapi_seed.py +199 -0
  237. graph_agents_cli/scaffold/utils/remote_template.py +376 -0
  238. graph_agents_cli/scaffold/utils/template.py +1352 -0
  239. graph_agents_cli/scaffold/utils/upgrade.py +894 -0
  240. graph_agents_cli/scaffold/utils/version.py +438 -0
  241. graph_agents_cli/secrets/__init__.py +15 -0
  242. graph_agents_cli/secrets/_apply.py +954 -0
  243. graph_agents_cli/secrets/_required.py +188 -0
  244. graph_agents_cli/secrets/cmd_secrets.py +211 -0
  245. graph_agents_cli/setup/__init__.py +13 -0
  246. graph_agents_cli/setup/_antigravity.py +221 -0
  247. graph_agents_cli/setup/cmd_auth.py +1030 -0
  248. graph_agents_cli/setup/cmd_dev_token.py +513 -0
  249. graph_agents_cli/setup/cmd_setup.py +428 -0
  250. graph_agents_cli/setup/cmd_update.py +140 -0
  251. graph_agents_cli/skills/__init__.py +13 -0
  252. graph_agents_cli/skills/_bundle.py +65 -0
  253. graph_agents_cli/skills/data/README.md +19 -0
  254. graph_agents_cli/skills/data/graph-agents-cli-deploy/SKILL.md +357 -0
  255. graph_agents_cli/skills/data/graph-agents-cli-deploy/references/github-settings.md +113 -0
  256. graph_agents_cli/skills/data/graph-agents-cli-deploy/references/gitops.md +137 -0
  257. graph_agents_cli/skills/data/graph-agents-cli-deploy/references/kubernetes.md +315 -0
  258. graph_agents_cli/skills/data/graph-agents-cli-deploy/references/secrets.md +160 -0
  259. graph_agents_cli/skills/data/graph-agents-cli-eval/SKILL.md +303 -0
  260. graph_agents_cli/skills/data/graph-agents-cli-eval/references/dataset_schema.md +282 -0
  261. graph_agents_cli/skills/data/graph-agents-cli-eval/references/metrics-guide.md +143 -0
  262. graph_agents_cli/skills/data/graph-agents-cli-langgraph-code/SKILL.md +659 -0
  263. graph_agents_cli/skills/data/graph-agents-cli-langgraph-code/references/langchain-models.md +124 -0
  264. graph_agents_cli/skills/data/graph-agents-cli-langgraph-code/references/langgraph.md +235 -0
  265. graph_agents_cli/skills/data/graph-agents-cli-langgraph-code/references/template-contract.md +477 -0
  266. graph_agents_cli/skills/data/graph-agents-cli-observability/SKILL.md +231 -0
  267. graph_agents_cli/skills/data/graph-agents-cli-observability/references/langsmith.md +46 -0
  268. graph_agents_cli/skills/data/graph-agents-cli-observability/references/otel.md +59 -0
  269. graph_agents_cli/skills/data/graph-agents-cli-scaffold/SKILL.md +414 -0
  270. graph_agents_cli/skills/data/graph-agents-cli-scaffold/references/flags.md +134 -0
  271. graph_agents_cli/skills/data/graph-agents-cli-workflow/SKILL.md +478 -0
  272. graph_agents_cli/skills/data/graph-agents-cli-workflow/references/brainstorming.md +118 -0
  273. graph_agents_cli/skills/data/graph-agents-cli-workflow/references/commands.md +419 -0
  274. graph_agents_cli/skills/data/graph-agents-cli-workflow/references/extension.md +156 -0
  275. graph_agents_cli/skills/data/graph-agents-cli-workflow/references/internals.md +67 -0
  276. graph_agents_cli/skills/data/graph-agents-cli-workflow/references/spec-template.md +56 -0
  277. graph_agents_cli/skills/data/graph-agents-cli-workflow/references/terminology.md +119 -0
  278. graph_agents_cli/system/__init__.py +15 -0
  279. graph_agents_cli/system/_apply.py +519 -0
  280. graph_agents_cli/system/_checks.py +1023 -0
  281. graph_agents_cli/system/_deploy.py +215 -0
  282. graph_agents_cli/system/_model.py +363 -0
  283. graph_agents_cli/system/_system.py +664 -0
  284. graph_agents_cli/system/_views.py +208 -0
  285. graph_agents_cli/system/cmd_system.py +423 -0
  286. graph_agents_cli-0.3.1.dist-info/METADATA +162 -0
  287. graph_agents_cli-0.3.1.dist-info/RECORD +291 -0
  288. graph_agents_cli-0.3.1.dist-info/WHEEL +4 -0
  289. graph_agents_cli-0.3.1.dist-info/entry_points.txt +2 -0
  290. graph_agents_cli-0.3.1.dist-info/licenses/LICENSE +201 -0
  291. graph_agents_cli-0.3.1.dist-info/licenses/NOTICE +19 -0
@@ -0,0 +1,269 @@
1
+ # Copyright 2026 graph-agents-cli contributors
2
+ #
3
+ # Licensed under the Apache License, Version 2.0 (the "License");
4
+ # you may not use this file except in compliance with the License.
5
+ # You may obtain a copy of the License at
6
+ #
7
+ # https://www.apache.org/licenses/LICENSE-2.0
8
+ #
9
+ # Unless required by applicable law or agreed to in writing, software
10
+ # distributed under the License is distributed on an "AS IS" BASIS,
11
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
+ # See the License for the specific language governing permissions and
13
+ # limitations under the License.
14
+
15
+ """Lint and render the Helm chart when `helm` is on PATH (skipped otherwise).
16
+
17
+ The chart declares optional subcharts; `helm dependency build` needs the
18
+ chart registry, so it is attempted once and the test skips when it cannot run.
19
+
20
+ The values files are yours: every expectation below is read from them
21
+ (values.yaml overlaid with values-<env>.yaml, as helm merges them), so turning
22
+ the gateway off, an ingress on or the bundled Postgres off keeps these tests
23
+ green as long as the chart renders what the values ask for. They also hold
24
+ whatever image tags the values files carry: in argocd mode CI and
25
+ `graph-agents-cli deploy` commit the tag to values-<env>.yaml, and the pull
26
+ request that does so runs them too. What stays fixed is the security posture:
27
+ probes and metrics are never published, pods are hardened.
28
+ """
29
+
30
+ from __future__ import annotations
31
+
32
+ import shutil
33
+ import subprocess
34
+ from pathlib import Path
35
+
36
+ import pytest
37
+ import yaml
38
+
39
+ PROJECT_NAME = "{{cookiecutter.project_name}}"
40
+ CHART = Path(__file__).resolve().parents[2] / "deployment" / "helm" / PROJECT_NAME
41
+ ENVIRONMENTS = ("dev", "staging", "prod")
42
+ HELM = shutil.which("helm")
43
+ # The Gateway the scaffolded staging/prod values leave for the operator to name.
44
+ GATEWAY = ("--set", "gateway.parentRef.name=gw")
45
+ # What `graph-agents-cli deploy` passes: the image tag it deploys (always as a
46
+ # string), and the Gateway.
47
+ DEPLOY_ARGS = ("--set-string", "image.tag=0123abc", *GATEWAY)
48
+
49
+ pytestmark = pytest.mark.skipif(HELM is None, reason="helm is not installed")
50
+
51
+
52
+ def _helm(*args: str) -> subprocess.CompletedProcess[str]:
53
+ return subprocess.run([HELM or "helm", *args], capture_output=True, text=True, check=False)
54
+
55
+
56
+ def _render(env: str, *extra: str) -> subprocess.CompletedProcess[str]:
57
+ return _helm(
58
+ "template",
59
+ PROJECT_NAME,
60
+ str(CHART),
61
+ "-f",
62
+ str(CHART / f"values-{env}.yaml"),
63
+ "--namespace",
64
+ f"{PROJECT_NAME}-{env}",
65
+ *extra,
66
+ )
67
+
68
+
69
+ def _docs(manifests: str) -> list[dict]:
70
+ return [doc for doc in yaml.safe_load_all(manifests) if doc]
71
+
72
+
73
+ def _agent_deployment(manifests: str) -> dict:
74
+ for doc in _docs(manifests):
75
+ if doc["kind"] == "Deployment" and doc["metadata"]["name"] == PROJECT_NAME:
76
+ return doc
77
+ raise AssertionError("the agent Deployment was not rendered")
78
+
79
+
80
+ def _committed_tag(env: str) -> str:
81
+ values = yaml.safe_load((CHART / f"values-{env}.yaml").read_text()) or {}
82
+ return str((values.get("image") or {}).get("tag") or "")
83
+
84
+
85
+ def _merge(base: dict, override: dict) -> dict:
86
+ """Helm's values merge: maps merge key by key, anything else is replaced."""
87
+ merged = dict(base)
88
+ for key, value in override.items():
89
+ if isinstance(value, dict) and isinstance(merged.get(key), dict):
90
+ merged[key] = _merge(merged[key], value)
91
+ else:
92
+ merged[key] = value
93
+ return merged
94
+
95
+
96
+ def _values(env: str) -> dict:
97
+ """The values `helm template -f values-<env>.yaml` renders with."""
98
+ base = yaml.safe_load((CHART / "values.yaml").read_text()) or {}
99
+ return _merge(base, yaml.safe_load((CHART / f"values-{env}.yaml").read_text()) or {})
100
+
101
+
102
+ def _enabled(values: dict, key: str) -> bool:
103
+ return bool((values.get(key) or {}).get("enabled"))
104
+
105
+
106
+ def _published_paths(manifests: str) -> dict[str, str]:
107
+ """Every path the HTTPRoute and the Ingress publish: path -> match type."""
108
+ paths: dict[str, str] = {}
109
+ for doc in _docs(manifests):
110
+ if doc["kind"] == "HTTPRoute":
111
+ for rule in doc["spec"]["rules"]:
112
+ for match in rule["matches"]:
113
+ paths[match["path"]["value"]] = match["path"]["type"]
114
+ elif doc["kind"] == "Ingress":
115
+ for rule in doc["spec"]["rules"]:
116
+ for entry in rule["http"]["paths"]:
117
+ kind = "Exact" if entry["pathType"] == "Exact" else "PathPrefix"
118
+ paths[entry["path"]] = kind
119
+ return paths
120
+
121
+
122
+ def _configured_paths(values: dict) -> dict[str, str]:
123
+ """route.publicPaths, plus route.devPaths while env.APP_ENV is exactly dev (as the chart does)."""
124
+ route = values.get("route") or {}
125
+ entries = list(route.get("publicPaths") or [])
126
+ if str((values.get("env") or {}).get("APP_ENV", "")) == "dev":
127
+ entries += list(route.get("devPaths") or [])
128
+ return {str(e["path"]): str(e["type"]) for e in entries}
129
+
130
+
131
+ @pytest.fixture(scope="module")
132
+ def chart_with_dependencies() -> Path:
133
+ if not (CHART / "charts").exists():
134
+ result = _helm("dependency", "build", str(CHART))
135
+ if result.returncode != 0:
136
+ pytest.skip(
137
+ f"helm dependency build failed (no registry access?): {result.stderr[-400:]}"
138
+ )
139
+ return CHART
140
+
141
+
142
+ def test_helm_lint(chart_with_dependencies: Path) -> None:
143
+ for env in ENVIRONMENTS:
144
+ result = _helm(
145
+ "lint",
146
+ str(chart_with_dependencies),
147
+ "-f",
148
+ str(chart_with_dependencies / f"values-{env}.yaml"),
149
+ *DEPLOY_ARGS,
150
+ )
151
+ assert result.returncode == 0, f"{env}: {result.stdout}\n{result.stderr}"
152
+
153
+
154
+ def test_helm_template_renders_every_environment(chart_with_dependencies: Path) -> None:
155
+ for env in ENVIRONMENTS:
156
+ values = _values(env)
157
+ result = _render(env, *DEPLOY_ARGS)
158
+ assert result.returncode == 0, f"{env}: {result.stderr}"
159
+ out = result.stdout
160
+ assert "kind: Deployment" in out and "kind: Service" in out and "kind: ConfigMap" in out
161
+ kinds = {doc["kind"] for doc in _docs(out)}
162
+ container = _agent_deployment(out)["spec"]["template"]["spec"]["containers"][0]
163
+ secret_ref = container["envFrom"][0]["secretRef"]
164
+ assert secret_ref["name"] == f"{PROJECT_NAME}-app"
165
+ # secretOptional: false (the default) keeps pods without their keys from starting.
166
+ assert secret_ref["optional"] is bool(values.get("secretOptional", False)), env
167
+ assert ("HTTPRoute" in kinds) is _enabled(values, "gateway"), env
168
+ assert ("Ingress" in kinds) is _enabled(values, "ingress"), env
169
+ bundled = f"$(POSTGRES_PASSWORD)@{PROJECT_NAME}-postgresql" in out
170
+ assert bundled is _enabled(values, "postgresql"), env
171
+
172
+
173
+ def test_the_route_publishes_the_api_but_not_probes_or_metrics(
174
+ chart_with_dependencies: Path,
175
+ ) -> None:
176
+ routed = [
177
+ env
178
+ for env in ENVIRONMENTS
179
+ if _enabled(_values(env), "gateway") or _enabled(_values(env), "ingress")
180
+ ]
181
+ if not routed:
182
+ pytest.skip("no environment enables gateway or ingress")
183
+ for env in routed:
184
+ values = _values(env)
185
+ result = _render(env, *DEPLOY_ARGS)
186
+ assert result.returncode == 0, f"{env}: {result.stderr}"
187
+ paths = _published_paths(result.stdout)
188
+ # Exactly what the values list: nothing else reaches the Gateway or Ingress.
189
+ assert paths == _configured_paths(values), env
190
+ for private in ("/", "/health", "/ready", "/metrics"):
191
+ assert private not in paths, (env, private)
192
+
193
+
194
+ def test_pods_are_hardened_probed_and_sized(chart_with_dependencies: Path) -> None:
195
+ for env in ENVIRONMENTS:
196
+ result = _render(env, *DEPLOY_ARGS)
197
+ assert result.returncode == 0, f"{env}: {result.stderr}"
198
+ pod = _agent_deployment(result.stdout)["spec"]["template"]["spec"]
199
+ container = pod["containers"][0]
200
+ assert container["image"].endswith(":0123abc")
201
+ assert pod["automountServiceAccountToken"] is False
202
+ assert pod["securityContext"]["runAsNonRoot"] is True
203
+ assert pod["securityContext"]["seccompProfile"]["type"] == "RuntimeDefault"
204
+ security = container["securityContext"]
205
+ assert security["readOnlyRootFilesystem"] is True
206
+ assert security["allowPrivilegeEscalation"] is False
207
+ assert security["capabilities"]["drop"] == ["ALL"]
208
+ assert {"name": "tmp", "mountPath": "/tmp"} in container["volumeMounts"]
209
+ assert container["readinessProbe"]["httpGet"]["path"] == "/ready"
210
+ assert container["livenessProbe"]["httpGet"]["path"] == "/health"
211
+ assert container["startupProbe"]["httpGet"]["path"] == "/health"
212
+ assert container["resources"]["requests"]["cpu"]
213
+ assert container["resources"]["limits"]["memory"]
214
+
215
+
216
+ def test_pods_drain_before_they_are_killed(chart_with_dependencies: Path) -> None:
217
+ """A stopping pod keeps serving during the preStop pause and drains within the grace period."""
218
+ for env in ENVIRONMENTS:
219
+ result = _render(env, *DEPLOY_ARGS)
220
+ assert result.returncode == 0, f"{env}: {result.stderr}"
221
+ pod = _agent_deployment(result.stdout)["spec"]["template"]["spec"]
222
+ container = pod["containers"][0]
223
+ values = yaml.safe_load((CHART / "values.yaml").read_text()) or {}
224
+ overrides = yaml.safe_load((CHART / f"values-{env}.yaml").read_text()) or {}
225
+ shutdown = {**(values.get("shutdown") or {}), **(overrides.get("shutdown") or {})}
226
+ pause = int(shutdown.get("preStopSleepSeconds") or 0)
227
+ drain = int(shutdown.get("drainSeconds") or 0)
228
+ assert pod["terminationGracePeriodSeconds"] > pause + drain, env
229
+ if pause:
230
+ command = container["lifecycle"]["preStop"]["exec"]["command"]
231
+ assert command[-1] == f"sleep {pause}", env
232
+
233
+
234
+ def test_the_network_policy_example_renders_a_valid_policy(chart_with_dependencies: Path) -> None:
235
+ """examples/networkpolicy.yaml, as documented: copied over the staging or prod values."""
236
+ example = CHART / "examples" / "networkpolicy.yaml"
237
+ if not example.is_file():
238
+ pytest.skip("no examples/networkpolicy.yaml")
239
+ for env in ("staging", "prod"):
240
+ result = _render(env, "-f", str(example), *DEPLOY_ARGS)
241
+ assert result.returncode == 0, f"{env}: {result.stderr}"
242
+ policy = next(d for d in _docs(result.stdout) if d["kind"] == "NetworkPolicy")
243
+ spec = policy["spec"]
244
+ assert spec["policyTypes"] == ["Ingress", "Egress"], env
245
+ assert spec["ingress"] and all(rule.get("ports") for rule in spec["ingress"]), env
246
+ # Every egress rule names its ports: nothing is open to any port anywhere.
247
+ assert all(rule.get("ports") for rule in spec["egress"]), env
248
+ dns = {p["port"] for p in spec["egress"][0]["ports"]}
249
+ assert dns == {53}, env
250
+
251
+
252
+ def test_the_chart_refuses_to_render_without_an_image_tag(chart_with_dependencies: Path) -> None:
253
+ # An explicit empty tag: the committed values-<env>.yaml may already name one.
254
+ for env in ENVIRONMENTS:
255
+ result = _render(env, "--set-string", "image.tag=", *GATEWAY)
256
+ assert result.returncode != 0, env
257
+ assert "image.tag is empty" in result.stderr, env
258
+
259
+
260
+ def test_committed_image_tags_are_the_ones_deployed(chart_with_dependencies: Path) -> None:
261
+ """A tag written into values-<env>.yaml (argocd mode) renders as Argo CD renders it."""
262
+ tagged = {env: tag for env in ENVIRONMENTS if (tag := _committed_tag(env))}
263
+ if not tagged:
264
+ pytest.skip("no values-<env>.yaml names an image tag yet")
265
+ for env, tag in tagged.items():
266
+ result = _render(env, *GATEWAY)
267
+ assert result.returncode == 0, f"{env}: {result.stderr}"
268
+ container = _agent_deployment(result.stdout)["spec"]["template"]["spec"]["containers"][0]
269
+ assert container["image"].endswith(f":{tag}"), (env, container["image"])
@@ -0,0 +1,5 @@
1
+ # Deployment target `none`
2
+
3
+ Marker for the target that ships no deployment files. `python/` holds only a
4
+ fallback README so a project built on the empty base has one; the langgraph
5
+ template overlays its own.
@@ -0,0 +1,6 @@
1
+ # {{cookiecutter.project_name}}
2
+
3
+ Created by graph-agents-cli with `--deployment-target none`: no `deployment/`
4
+ directory, no CD workflows. Run it locally with `graph-agents-cli playground`
5
+ or `graph-agents-cli run "prompt"`; add a Kubernetes target later with
6
+ `graph-agents-cli scaffold enhance --deployment-target kubernetes`.
@@ -0,0 +1,13 @@
1
+ # Copyright 2026 Google LLC
2
+ #
3
+ # Licensed under the Apache License, Version 2.0 (the "License");
4
+ # you may not use this file except in compliance with the License.
5
+ # You may obtain a copy of the License at
6
+ #
7
+ # https://www.apache.org/licenses/LICENSE-2.0
8
+ #
9
+ # Unless required by applicable law or agreed to in writing, software
10
+ # distributed under the License is distributed on an "AS IS" BASIS,
11
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
+ # See the License for the specific language governing permissions and
13
+ # limitations under the License.
@@ -0,0 +1,212 @@
1
+ # Copyright 2026 Google LLC
2
+ #
3
+ # Licensed under the Apache License, Version 2.0 (the "License");
4
+ # you may not use this file except in compliance with the License.
5
+ # You may obtain a copy of the License at
6
+ #
7
+ # https://www.apache.org/licenses/LICENSE-2.0
8
+ #
9
+ # Unless required by applicable law or agreed to in writing, software
10
+ # distributed under the License is distributed on an "AS IS" BASIS,
11
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
+ # See the License for the specific language governing permissions and
13
+ # limitations under the License.
14
+
15
+ """Shared backup utility for project directories.
16
+
17
+ A backup is the undo of ``enhance`` and ``upgrade``, so it copies the whole
18
+ project, ``.env`` included (both commands merge it, and a generated project
19
+ gitignores it, so the backup may hold the user's only copy). Because of that it
20
+ is private: the backups directory and every backup are 0700 and every ``.env*``
21
+ file in them 0600. Only the newest ``KEEP_BACKUPS`` backups of a project are
22
+ kept, so credentials do not pile up in the home directory.
23
+
24
+ A backup is named ``<directory>_<project id>_<timestamp>[_n]``. The project id
25
+ is a hash of the project's resolved absolute path and its manifest ``name``, so
26
+ two checkouts with the same directory name (``my-agent`` in two places) never
27
+ count as one project: pruning only ever deletes backups carrying the id of the
28
+ project being backed up. Backups named without an id (made by an older CLI)
29
+ are never pruned.
30
+ """
31
+
32
+ import contextlib
33
+ import datetime
34
+ import hashlib
35
+ import os
36
+ import pathlib
37
+ import re
38
+ import shutil
39
+ from collections.abc import Callable
40
+
41
+ import click
42
+ import yaml
43
+
44
+ from graph_agents_cli._output import Console
45
+
46
+ from .fs import standard_ignore_patterns
47
+
48
+ BACKUP_BASE_DIR = pathlib.Path.home() / ".graph-agents-cli" / "backups"
49
+ # Backups kept per project (by project id); older ones are deleted.
50
+ KEEP_BACKUPS = 5
51
+ _PRIVATE_DIR = 0o700
52
+ _PRIVATE_FILE = 0o600
53
+ _MANIFEST_FILENAME = "graph-agents-cli-manifest.yaml"
54
+ _ID_LENGTH = 12
55
+
56
+
57
+ def project_backup_id(project_dir: pathlib.Path) -> str:
58
+ """A stable id for ``project_dir``: its resolved absolute path plus its manifest ``name``."""
59
+ name = ""
60
+ try:
61
+ data = yaml.safe_load((project_dir / _MANIFEST_FILENAME).read_text(encoding="utf-8"))
62
+ if isinstance(data, dict) and data.get("name") is not None:
63
+ name = str(data["name"])
64
+ except (OSError, UnicodeDecodeError, yaml.YAMLError):
65
+ pass # no (readable) manifest: the path alone identifies the directory
66
+ key = f"{project_dir.resolve()}\0{name}".encode()
67
+ return hashlib.sha256(key).hexdigest()[:_ID_LENGTH]
68
+
69
+
70
+ def _backup_prefix(project_dir: pathlib.Path) -> str:
71
+ return f"{project_dir.name}_{project_backup_id(project_dir)}"
72
+
73
+
74
+ def create_project_backup(
75
+ project_dir: pathlib.Path,
76
+ console: Console | None = None,
77
+ auto_approve: bool = False,
78
+ interactive: bool = False,
79
+ ) -> pathlib.Path | None:
80
+ """Create a backup of the project directory.
81
+
82
+ Backs up to ~/.graph-agents-cli/backups/<directory>_<project id>_<timestamp>/.
83
+
84
+ Args:
85
+ project_dir: Path to the project directory to back up.
86
+ console: Rich console for output. Created if not provided.
87
+ auto_approve: If True, skip confirmation prompts on failure.
88
+ interactive: If True, show interactive prompt on backup failure.
89
+
90
+ Returns:
91
+ Path to the backup directory on success, None if backup failed
92
+ but user chose to continue.
93
+
94
+ Raises:
95
+ click.Abort: If backup failed and user chose to cancel (interactive mode only).
96
+ """
97
+ if console is None:
98
+ console = Console()
99
+
100
+ timestamp = datetime.datetime.now().strftime("%Y%m%d_%H%M%S")
101
+ prefix = _backup_prefix(project_dir)
102
+ backup_dir = BACKUP_BASE_DIR / f"{prefix}_{timestamp}"
103
+ suffix = 1
104
+ while backup_dir.exists(): # two backups within the same second
105
+ suffix += 1
106
+ backup_dir = BACKUP_BASE_DIR / f"{prefix}_{timestamp}_{suffix}"
107
+
108
+ console.print("📦 [blue]Creating backup before modification...[/blue]")
109
+
110
+ try:
111
+ BACKUP_BASE_DIR.mkdir(parents=True, exist_ok=True)
112
+ _make_private_dir(BACKUP_BASE_DIR)
113
+ shutil.copytree(project_dir, backup_dir, ignore=standard_ignore_patterns)
114
+ _make_private_tree(backup_dir)
115
+ console.print(f"Backup created: [cyan]{backup_dir}[/cyan]")
116
+ pruned = prune_backups(project_dir, keep=KEEP_BACKUPS, current=backup_dir)
117
+ if pruned:
118
+ console.print(
119
+ f"[dim]Removed {len(pruned)} older backup(s) of {project_dir.name} "
120
+ f"(the newest {KEEP_BACKUPS} are kept).[/dim]"
121
+ )
122
+ return backup_dir
123
+ except Exception as e:
124
+ console.print(f"⚠️ [yellow]Warning: Could not create backup: {e}[/yellow]")
125
+ if interactive:
126
+ if not click.confirm("Continue without backup?", default=True):
127
+ raise click.Abort() from e
128
+ return None
129
+
130
+
131
+ def _make_private_dir(path: pathlib.Path) -> None:
132
+ with contextlib.suppress(OSError):
133
+ os.chmod(path, _PRIVATE_DIR)
134
+
135
+
136
+ def _make_private_tree(root: pathlib.Path) -> None:
137
+ """0700 on the backup and its directories, 0600 on the env files it holds."""
138
+ _make_private_dir(root)
139
+ for dirpath, dirnames, filenames in os.walk(root):
140
+ for name in dirnames:
141
+ _make_private_dir(pathlib.Path(dirpath) / name)
142
+ for name in filenames:
143
+ if name.startswith(".env"):
144
+ with contextlib.suppress(OSError):
145
+ os.chmod(pathlib.Path(dirpath) / name, _PRIVATE_FILE)
146
+
147
+
148
+ def prune_backups(
149
+ project_dir: pathlib.Path, *, keep: int = KEEP_BACKUPS, current: pathlib.Path | None = None
150
+ ) -> list[pathlib.Path]:
151
+ """Delete all but the newest ``keep`` backups of ``project_dir``; return what was removed.
152
+
153
+ Only directories named exactly ``<directory>_<project id>_<YYYYmmdd>_<HHMMSS>[_n]``
154
+ with this project's id are considered: another project is never touched,
155
+ whether its name shares a prefix or is the same (another checkout), backups
156
+ without an id are left alone, and ``current`` (the backup just made) is
157
+ never removed.
158
+ """
159
+ if not BACKUP_BASE_DIR.is_dir():
160
+ return []
161
+ prefix = _backup_prefix(project_dir)
162
+ pattern = re.compile(rf"^{re.escape(prefix)}_(\d{{8}}_\d{{6}})(?:_(\d+))?$")
163
+ found: list[tuple[str, int, pathlib.Path]] = []
164
+ for entry in BACKUP_BASE_DIR.iterdir():
165
+ match = pattern.match(entry.name)
166
+ if match and entry.is_dir() and not entry.is_symlink():
167
+ found.append((match.group(1), int(match.group(2) or 1), entry))
168
+ found.sort(reverse=True)
169
+ removed: list[pathlib.Path] = []
170
+ for _stamp, _n, entry in found[max(keep, 1) :]:
171
+ if current is not None and entry == current:
172
+ continue
173
+ shutil.rmtree(entry, ignore_errors=True)
174
+ removed.append(entry)
175
+ return removed
176
+
177
+
178
+ def make_backup_pre_apply_hook(
179
+ *,
180
+ console: Console,
181
+ auto_approve: bool,
182
+ interactive: bool,
183
+ ) -> Callable[[pathlib.Path], bool]:
184
+ """Build a ``run_three_way_merge`` pre-apply hook that backs up the project.
185
+
186
+ The merge invokes the hook right before it writes changes; returning False
187
+ aborts the merge. This wraps ``create_project_backup`` so a user cancelling
188
+ the "continue without backup?" prompt cleanly stops the operation.
189
+
190
+ Args:
191
+ console: Rich console for output.
192
+ auto_approve: If True, skip confirmation prompts on failure.
193
+ interactive: If True, prompt the user on backup failure.
194
+
195
+ Returns:
196
+ A hook taking the project directory and returning True to proceed,
197
+ False if the user cancelled.
198
+ """
199
+
200
+ def _backup(proj_dir: pathlib.Path) -> bool:
201
+ try:
202
+ create_project_backup(
203
+ proj_dir,
204
+ console=console,
205
+ auto_approve=auto_approve,
206
+ interactive=interactive,
207
+ )
208
+ return True
209
+ except click.Abort:
210
+ return False # user cancelled
211
+
212
+ return _backup