alysis-code 0.13.0__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 (458) hide show
  1. alysis_code/__init__.py +3 -0
  2. alysis_code/__main__.py +4 -0
  3. alysis_code/_build_info.py +14 -0
  4. alysis_code/account_login.py +468 -0
  5. alysis_code/agent/README.md +35 -0
  6. alysis_code/agent/__init__.py +11 -0
  7. alysis_code/agent/acceptance_contract.py +2217 -0
  8. alysis_code/agent/blast_radius.py +1403 -0
  9. alysis_code/agent/cache_keepalive.py +227 -0
  10. alysis_code/agent/completion_certificate.py +366 -0
  11. alysis_code/agent/completion_gate.py +306 -0
  12. alysis_code/agent/empty_response_stall.py +403 -0
  13. alysis_code/agent/errors.py +28 -0
  14. alysis_code/agent/llm_calls.py +475 -0
  15. alysis_code/agent/mutation_classification.py +227 -0
  16. alysis_code/agent/prompt_context.py +2508 -0
  17. alysis_code/agent/read_ledger.py +253 -0
  18. alysis_code/agent/regression_baseline.py +642 -0
  19. alysis_code/agent/reproduction_first.py +610 -0
  20. alysis_code/agent/sensitive_output.py +629 -0
  21. alysis_code/agent/session.py +3218 -0
  22. alysis_code/agent/steering.py +191 -0
  23. alysis_code/agent/subagent_execution.py +5177 -0
  24. alysis_code/agent/subagent_workspace.py +666 -0
  25. alysis_code/agent/tools_assembly.py +4728 -0
  26. alysis_code/agent/turn/__init__.py +101 -0
  27. alysis_code/agent/turn/core.py +8483 -0
  28. alysis_code/agent/turn/events.py +113 -0
  29. alysis_code/agent/turn/exploration.py +590 -0
  30. alysis_code/agent/turn/interventions.py +65 -0
  31. alysis_code/agent/turn/read_cache.py +420 -0
  32. alysis_code/agent/turn/snapshot.py +179 -0
  33. alysis_code/agent/turn_contract.py +661 -0
  34. alysis_code/agent/turn_path.py +129 -0
  35. alysis_code/agent/verification.py +2885 -0
  36. alysis_code/agent/verification_commands.py +512 -0
  37. alysis_code/agent/verification_evidence.py +738 -0
  38. alysis_code/agent_loop.py +693 -0
  39. alysis_code/agent_runtimes/__init__.py +51 -0
  40. alysis_code/agent_runtimes/base.py +114 -0
  41. alysis_code/agent_runtimes/builtins.py +129 -0
  42. alysis_code/agent_runtimes/codex_cli.py +664 -0
  43. alysis_code/agent_runtimes/host.py +263 -0
  44. alysis_code/agent_runtimes/registry.py +64 -0
  45. alysis_code/agent_runtimes/service.py +150 -0
  46. alysis_code/agentbox_client.py +416 -0
  47. alysis_code/agentbox_integration.py +310 -0
  48. alysis_code/alysis_cloud.py +152 -0
  49. alysis_code/approval_scope.py +276 -0
  50. alysis_code/assets/README.md +33 -0
  51. alysis_code/assets/__init__.py +126 -0
  52. alysis_code/assets/asset_read_core.py +281 -0
  53. alysis_code/assets/budget_allocator.py +456 -0
  54. alysis_code/assets/comprehender.py +759 -0
  55. alysis_code/assets/index.py +654 -0
  56. alysis_code/assets/ingestion.py +275 -0
  57. alysis_code/assets/legacy_migration.py +413 -0
  58. alysis_code/assets/models.py +263 -0
  59. alysis_code/assets/ocr.py +239 -0
  60. alysis_code/assets/owl/ascii/f-000.txt +13 -0
  61. alysis_code/assets/owl/ascii/f-001.txt +13 -0
  62. alysis_code/assets/owl/ascii/f-002.txt +13 -0
  63. alysis_code/assets/owl/ascii/f-003.txt +13 -0
  64. alysis_code/assets/owl/ascii/f-004.txt +13 -0
  65. alysis_code/assets/owl/ascii/f-005.txt +13 -0
  66. alysis_code/assets/owl/ascii/f-006.txt +13 -0
  67. alysis_code/assets/owl/ascii/f-007.txt +13 -0
  68. alysis_code/assets/owl/ascii/f-008.txt +13 -0
  69. alysis_code/assets/owl/ascii/f-009.txt +13 -0
  70. alysis_code/assets/owl/ascii/f-010.txt +13 -0
  71. alysis_code/assets/owl/ascii/f-011.txt +13 -0
  72. alysis_code/assets/owl/ascii/f-012.txt +13 -0
  73. alysis_code/assets/owl/ascii/f-013.txt +13 -0
  74. alysis_code/assets/owl/ascii/f-014.txt +13 -0
  75. alysis_code/assets/owl/ascii/f-015.txt +13 -0
  76. alysis_code/assets/owl/ascii/f-016.txt +13 -0
  77. alysis_code/assets/owl/ascii/f-017.txt +13 -0
  78. alysis_code/assets/owl/ascii/f-018.txt +13 -0
  79. alysis_code/assets/owl/ascii/f-019.txt +13 -0
  80. alysis_code/assets/owl/ascii/f-020.txt +13 -0
  81. alysis_code/assets/owl/index.html +98 -0
  82. alysis_code/assets/owl/show-owl.sh +761 -0
  83. alysis_code/assets/paths.py +49 -0
  84. alysis_code/assets/plan_binding.py +326 -0
  85. alysis_code/assets/planner_context.py +466 -0
  86. alysis_code/assets/planner_tools.py +184 -0
  87. alysis_code/assets/prompts.py +101 -0
  88. alysis_code/assets/replanner_context.py +239 -0
  89. alysis_code/assets/surface.py +521 -0
  90. alysis_code/assets/untrusted_content.py +48 -0
  91. alysis_code/assets/usage_logger.py +94 -0
  92. alysis_code/assets/worker_mirror.py +428 -0
  93. alysis_code/assets/worker_section.py +303 -0
  94. alysis_code/assets/worker_tools.py +468 -0
  95. alysis_code/atomic_io.py +83 -0
  96. alysis_code/auth_diagnostics.py +272 -0
  97. alysis_code/background_runner.py +366 -0
  98. alysis_code/branding.py +270 -0
  99. alysis_code/budget_policy.py +390 -0
  100. alysis_code/build_identity.py +465 -0
  101. alysis_code/builtin_hooks/__init__.py +7 -0
  102. alysis_code/builtin_hooks/notify_done_windows.py +65 -0
  103. alysis_code/bwrap_etc.py +76 -0
  104. alysis_code/cancellation.py +41 -0
  105. alysis_code/capabilities.py +137 -0
  106. alysis_code/chatgpt_codex_static_provider.py +133 -0
  107. alysis_code/cli.py +51 -0
  108. alysis_code/cli_impl/__init__.py +1 -0
  109. alysis_code/cli_impl/assets_cli.py +537 -0
  110. alysis_code/cli_impl/assets_modal.py +412 -0
  111. alysis_code/cli_impl/chat/__init__.py +156 -0
  112. alysis_code/cli_impl/chat/commands.py +2616 -0
  113. alysis_code/cli_impl/chat/loop.py +4508 -0
  114. alysis_code/cli_impl/chat/mid_turn_policy.py +125 -0
  115. alysis_code/cli_impl/chat/rendering.py +444 -0
  116. alysis_code/cli_impl/chat/state.py +124 -0
  117. alysis_code/cli_impl/chat_resume.py +830 -0
  118. alysis_code/cli_impl/chat_slash_completer.py +258 -0
  119. alysis_code/cli_impl/commands/__init__.py +11 -0
  120. alysis_code/cli_impl/commands/_shared.py +89 -0
  121. alysis_code/cli_impl/commands/auth.py +623 -0
  122. alysis_code/cli_impl/commands/chat_resume_helpers.py +1531 -0
  123. alysis_code/cli_impl/commands/chat_state.py +158 -0
  124. alysis_code/cli_impl/commands/chat_status.py +1248 -0
  125. alysis_code/cli_impl/commands/chat_terminal.py +942 -0
  126. alysis_code/cli_impl/commands/chat_tui_panels.py +1018 -0
  127. alysis_code/cli_impl/commands/cli_common.py +1223 -0
  128. alysis_code/cli_impl/commands/cli_surface.py +77 -0
  129. alysis_code/cli_impl/commands/config.py +131 -0
  130. alysis_code/cli_impl/commands/conventions.py +85 -0
  131. alysis_code/cli_impl/commands/execution_helpers.py +350 -0
  132. alysis_code/cli_impl/commands/extensions.py +401 -0
  133. alysis_code/cli_impl/commands/forge.py +1282 -0
  134. alysis_code/cli_impl/commands/forge_asset_view.py +121 -0
  135. alysis_code/cli_impl/commands/forge_helpers.py +1215 -0
  136. alysis_code/cli_impl/commands/hooks.py +737 -0
  137. alysis_code/cli_impl/commands/ide_bridge.py +31 -0
  138. alysis_code/cli_impl/commands/mcp.py +700 -0
  139. alysis_code/cli_impl/commands/profile.py +453 -0
  140. alysis_code/cli_impl/commands/prompt_helpers.py +307 -0
  141. alysis_code/cli_impl/commands/report.py +88 -0
  142. alysis_code/cli_impl/commands/root.py +1118 -0
  143. alysis_code/cli_impl/commands/sandbox.py +184 -0
  144. alysis_code/cli_impl/commands/server.py +54 -0
  145. alysis_code/cli_impl/commands/sessions.py +252 -0
  146. alysis_code/cli_impl/commands/skills.py +404 -0
  147. alysis_code/cli_impl/commands/startup.py +946 -0
  148. alysis_code/cli_impl/commands/tools.py +335 -0
  149. alysis_code/cli_impl/commands/update.py +364 -0
  150. alysis_code/cli_impl/commands/welcome.py +972 -0
  151. alysis_code/cli_impl/config_menu.py +3882 -0
  152. alysis_code/cli_impl/forge.py +4509 -0
  153. alysis_code/cli_impl/forge_recovery.py +485 -0
  154. alysis_code/cli_impl/setup_wizard.py +2409 -0
  155. alysis_code/cli_impl/tui/__init__.py +58 -0
  156. alysis_code/cli_impl/tui/app.py +4551 -0
  157. alysis_code/cli_impl/tui/config.py +32 -0
  158. alysis_code/cli_impl/tui/config_flow.py +2754 -0
  159. alysis_code/cli_impl/tui/config_overlay.py +566 -0
  160. alysis_code/cli_impl/tui/content.py +78 -0
  161. alysis_code/cli_impl/tui/footer.py +218 -0
  162. alysis_code/cli_impl/tui/forge_status.py +136 -0
  163. alysis_code/cli_impl/tui/markdown.py +244 -0
  164. alysis_code/cli_impl/tui/owl.py +109 -0
  165. alysis_code/cli_impl/tui/plan_meta.py +477 -0
  166. alysis_code/cli_impl/tui/setup_app.py +519 -0
  167. alysis_code/cli_impl/tui/setup_flow.py +1622 -0
  168. alysis_code/cli_impl/tui/state.py +101 -0
  169. alysis_code/cli_impl/tui/subagent_identity.py +66 -0
  170. alysis_code/cli_impl/tui/subagent_panel.py +186 -0
  171. alysis_code/cli_impl/tui/surface.py +796 -0
  172. alysis_code/cli_impl/tui/transcript.py +514 -0
  173. alysis_code/cli_impl/tui/update_prompt.py +79 -0
  174. alysis_code/cli_impl/tui/workspace_guard.py +384 -0
  175. alysis_code/clipboard.py +172 -0
  176. alysis_code/code_review.py +1211 -0
  177. alysis_code/compaction/__init__.py +28 -0
  178. alysis_code/compaction/conversation_compactor.py +2932 -0
  179. alysis_code/compaction/importance.py +177 -0
  180. alysis_code/compaction/settings.py +297 -0
  181. alysis_code/compaction/tool_output_offload.py +447 -0
  182. alysis_code/config.py +3509 -0
  183. alysis_code/conflict_auto_resolver.py +895 -0
  184. alysis_code/context/__init__.py +1 -0
  185. alysis_code/context/tool_schema_budgeter.py +220 -0
  186. alysis_code/crash_diagnostics.py +282 -0
  187. alysis_code/custom_tools/README.md +34 -0
  188. alysis_code/custom_tools/__init__.py +43 -0
  189. alysis_code/custom_tools/discovery.py +903 -0
  190. alysis_code/custom_tools/runtime.py +1516 -0
  191. alysis_code/custom_tools/session.py +227 -0
  192. alysis_code/custom_tools/trust.py +232 -0
  193. alysis_code/diff_paths.py +113 -0
  194. alysis_code/direction_change.py +293 -0
  195. alysis_code/dispatch_timing.py +306 -0
  196. alysis_code/durable_service_manager.py +1236 -0
  197. alysis_code/edit_discipline.py +659 -0
  198. alysis_code/error_text.py +73 -0
  199. alysis_code/execution_budget.py +411 -0
  200. alysis_code/execution_context.py +915 -0
  201. alysis_code/execution_deadline.py +1065 -0
  202. alysis_code/execution_shared.py +1904 -0
  203. alysis_code/extensions/README.md +30 -0
  204. alysis_code/extensions/__init__.py +93 -0
  205. alysis_code/extensions/activation.py +138 -0
  206. alysis_code/extensions/install.py +1436 -0
  207. alysis_code/extensions/manifest.py +487 -0
  208. alysis_code/extensions/models.py +74 -0
  209. alysis_code/extensions/paths.py +56 -0
  210. alysis_code/extensions/registry.json +4 -0
  211. alysis_code/extensions/registry.py +52 -0
  212. alysis_code/extensions/state.py +83 -0
  213. alysis_code/extensions/workspace_trust.py +101 -0
  214. alysis_code/failed_task_evidence.py +369 -0
  215. alysis_code/failure_category.py +315 -0
  216. alysis_code/feedback_report.py +1647 -0
  217. alysis_code/file_classification.py +485 -0
  218. alysis_code/forge.py +2064 -0
  219. alysis_code/forge_completion.py +362 -0
  220. alysis_code/forge_events.py +475 -0
  221. alysis_code/frontmatter_utils.py +95 -0
  222. alysis_code/git_evidence.py +1181 -0
  223. alysis_code/git_ops.py +560 -0
  224. alysis_code/git_safe.py +62 -0
  225. alysis_code/git_worktrees.py +190 -0
  226. alysis_code/hooks/README.md +33 -0
  227. alysis_code/hooks/__init__.py +67 -0
  228. alysis_code/hooks/audit.py +171 -0
  229. alysis_code/hooks/config.py +225 -0
  230. alysis_code/hooks/dispatcher.py +1110 -0
  231. alysis_code/hooks/models.py +447 -0
  232. alysis_code/hooks/trust.py +202 -0
  233. alysis_code/host_actions.py +543 -0
  234. alysis_code/host_browser.py +103 -0
  235. alysis_code/ide/__init__.py +5 -0
  236. alysis_code/ide/activity_events.py +399 -0
  237. alysis_code/ide/approvals.py +337 -0
  238. alysis_code/ide/artifacts.py +153 -0
  239. alysis_code/ide/browser_egress_proxy.py +1076 -0
  240. alysis_code/ide/cdp_websocket_transport.py +1192 -0
  241. alysis_code/ide/change_ledger.py +1721 -0
  242. alysis_code/ide/context_blocks.py +979 -0
  243. alysis_code/ide/event_stream.py +531 -0
  244. alysis_code/ide/forge_protocol.py +3112 -0
  245. alysis_code/ide/forge_request_ledger.py +737 -0
  246. alysis_code/ide/health.py +965 -0
  247. alysis_code/ide/managed_browser.py +2251 -0
  248. alysis_code/ide/management_protocol.py +3414 -0
  249. alysis_code/ide/mcp_oauth_coordinator.py +744 -0
  250. alysis_code/ide/mcp_oauth_lifecycle.py +1504 -0
  251. alysis_code/ide/prompt_queue.py +1070 -0
  252. alysis_code/ide/protocol.py +191 -0
  253. alysis_code/ide/resumable_swarm.py +1543 -0
  254. alysis_code/ide/session_search.py +295 -0
  255. alysis_code/ide/stdio_bridge.py +9935 -0
  256. alysis_code/ide/structured_state.py +1579 -0
  257. alysis_code/ide/swarm_protocol.py +816 -0
  258. alysis_code/integration_gate.py +506 -0
  259. alysis_code/interactive_input_guard.py +39 -0
  260. alysis_code/interactive_plan_mode.py +26 -0
  261. alysis_code/internal_artifacts.py +179 -0
  262. alysis_code/knowledge_base.py +1409 -0
  263. alysis_code/knowledge_capture.py +1190 -0
  264. alysis_code/knowledge_librarian.py +605 -0
  265. alysis_code/language_policy.py +34 -0
  266. alysis_code/litellm_static_provider.py +535 -0
  267. alysis_code/llm/__init__.py +1 -0
  268. alysis_code/llm/anthropic_messages.py +2288 -0
  269. alysis_code/llm/base.py +71 -0
  270. alysis_code/llm/cache_capabilities.py +985 -0
  271. alysis_code/llm/cache_control_blocks.py +244 -0
  272. alysis_code/llm/cache_policy.py +388 -0
  273. alysis_code/llm/factory.py +373 -0
  274. alysis_code/llm/gemini_generate_content.py +2652 -0
  275. alysis_code/llm/gemini_interactions.py +739 -0
  276. alysis_code/llm/metadata.py +450 -0
  277. alysis_code/llm/openai_compat.py +2947 -0
  278. alysis_code/llm/openai_responses.py +2604 -0
  279. alysis_code/llm/protocols.py +609 -0
  280. alysis_code/llm/provider_limits.py +525 -0
  281. alysis_code/llm/request_plan.py +389 -0
  282. alysis_code/llm/request_shape.py +238 -0
  283. alysis_code/llm/streaming.py +108 -0
  284. alysis_code/llm/temperature_compat.py +78 -0
  285. alysis_code/llm/types.py +195 -0
  286. alysis_code/llm/usage_normalization.py +222 -0
  287. alysis_code/llm_error_display.py +315 -0
  288. alysis_code/logging_redaction.py +326 -0
  289. alysis_code/managed_host_deadline.py +196 -0
  290. alysis_code/mcp/README.md +33 -0
  291. alysis_code/mcp/__init__.py +24 -0
  292. alysis_code/mcp/client.py +1137 -0
  293. alysis_code/mcp/config.py +597 -0
  294. alysis_code/mcp/errors.py +113 -0
  295. alysis_code/mcp/forge_scope.py +154 -0
  296. alysis_code/mcp/jsonrpc.py +214 -0
  297. alysis_code/mcp/manager.py +2308 -0
  298. alysis_code/mcp/models.py +666 -0
  299. alysis_code/mcp/oauth.py +972 -0
  300. alysis_code/mcp/oauth_runtime.py +310 -0
  301. alysis_code/mcp/oauth_store.py +276 -0
  302. alysis_code/mcp/prompts.py +329 -0
  303. alysis_code/mcp/resources.py +295 -0
  304. alysis_code/mcp/roots.py +106 -0
  305. alysis_code/mcp/server_requests.py +75 -0
  306. alysis_code/mcp/token_store.py +859 -0
  307. alysis_code/mcp/transport_http.py +1338 -0
  308. alysis_code/mcp/transport_stdio.py +1267 -0
  309. alysis_code/mcp/untrusted_content.py +119 -0
  310. alysis_code/merge_conflict_reviewer.py +729 -0
  311. alysis_code/model_catalog/__init__.py +1 -0
  312. alysis_code/model_catalog/chatgpt_codex_subscription_snapshot.json +186 -0
  313. alysis_code/model_catalog/litellm_model_prices_snapshot.json +44715 -0
  314. alysis_code/model_catalog/litellm_model_prices_snapshot.meta.json +17 -0
  315. alysis_code/model_metadata_policy.py +223 -0
  316. alysis_code/model_metadata_utils.py +103 -0
  317. alysis_code/model_registry.py +1420 -0
  318. alysis_code/model_router.py +147 -0
  319. alysis_code/permission_policy.py +1016 -0
  320. alysis_code/personas.py +451 -0
  321. alysis_code/pipeline_facts.py +233 -0
  322. alysis_code/plan_assistant.py +4763 -0
  323. alysis_code/plan_mode.py +393 -0
  324. alysis_code/plan_reconciliation.py +1228 -0
  325. alysis_code/plan_repair.py +652 -0
  326. alysis_code/plan_validation.py +1099 -0
  327. alysis_code/planning_constraints.py +904 -0
  328. alysis_code/policy.py +95 -0
  329. alysis_code/preview_server.py +457 -0
  330. alysis_code/process_reaping.py +566 -0
  331. alysis_code/profile_presets.py +1834 -0
  332. alysis_code/profiles.py +666 -0
  333. alysis_code/provider_auth/__init__.py +29 -0
  334. alysis_code/provider_auth/base.py +99 -0
  335. alysis_code/provider_auth/openai_codex.py +951 -0
  336. alysis_code/provider_auth/registry.py +76 -0
  337. alysis_code/provider_auth/store.py +125 -0
  338. alysis_code/provider_diagnostics.py +1209 -0
  339. alysis_code/provider_model_catalog.py +685 -0
  340. alysis_code/provider_telemetry.py +1699 -0
  341. alysis_code/provider_url.py +75 -0
  342. alysis_code/reasoning_contracts.py +911 -0
  343. alysis_code/remote_sync.py +350 -0
  344. alysis_code/replanning.py +1195 -0
  345. alysis_code/repo_scan.py +1152 -0
  346. alysis_code/request_estimation.py +296 -0
  347. alysis_code/review_gate.py +617 -0
  348. alysis_code/run_lock.py +1141 -0
  349. alysis_code/run_outcome.py +58 -0
  350. alysis_code/run_provenance.py +774 -0
  351. alysis_code/run_state.py +445 -0
  352. alysis_code/runtime_artifacts.py +116 -0
  353. alysis_code/runtime_context_features.py +78 -0
  354. alysis_code/runtime_kind.py +52 -0
  355. alysis_code/safety/__init__.py +11 -0
  356. alysis_code/safety/mcp_sanitize.py +29 -0
  357. alysis_code/safety/safe_http.py +297 -0
  358. alysis_code/safety/subagent_report.py +184 -0
  359. alysis_code/sandbox_doctor.py +682 -0
  360. alysis_code/sandbox_runner.py +1025 -0
  361. alysis_code/sandbox_settings.py +423 -0
  362. alysis_code/serialized_paths.py +355 -0
  363. alysis_code/server/__init__.py +3 -0
  364. alysis_code/server/app.py +367 -0
  365. alysis_code/server/auth.py +34 -0
  366. alysis_code/server/job_config.py +30 -0
  367. alysis_code/server/settings.py +215 -0
  368. alysis_code/server/store.py +193 -0
  369. alysis_code/server/worker_runner.py +657 -0
  370. alysis_code/service_persistence.py +355 -0
  371. alysis_code/session_artifacts.py +108 -0
  372. alysis_code/session_metrics.py +331 -0
  373. alysis_code/session_store.py +624 -0
  374. alysis_code/skills/README.md +34 -0
  375. alysis_code/skills/__init__.py +104 -0
  376. alysis_code/skills/conventions.py +84 -0
  377. alysis_code/skills/discovery.py +176 -0
  378. alysis_code/skills/eval_models.py +232 -0
  379. alysis_code/skills/eval_runner.py +372 -0
  380. alysis_code/skills/evals.py +1344 -0
  381. alysis_code/skills/install.py +293 -0
  382. alysis_code/skills/loader.py +118 -0
  383. alysis_code/skills/matching.py +103 -0
  384. alysis_code/skills/models.py +71 -0
  385. alysis_code/skills/paths.py +56 -0
  386. alysis_code/skills/prompting.py +500 -0
  387. alysis_code/skills/scaffold.py +142 -0
  388. alysis_code/skills/state.py +441 -0
  389. alysis_code/skills/transactions.py +125 -0
  390. alysis_code/skills/validation.py +304 -0
  391. alysis_code/step_budget.py +238 -0
  392. alysis_code/subagent_labels.py +49 -0
  393. alysis_code/subagents.py +1072 -0
  394. alysis_code/surface/__init__.py +80 -0
  395. alysis_code/surface/base.py +305 -0
  396. alysis_code/surface/console.py +387 -0
  397. alysis_code/surface/events.py +372 -0
  398. alysis_code/surface/hidden_surface.py +529 -0
  399. alysis_code/surface/noop_surface.py +219 -0
  400. alysis_code/surface/rich_surface.py +1555 -0
  401. alysis_code/surface/styles.py +67 -0
  402. alysis_code/surface/theme.py +455 -0
  403. alysis_code/surface/types.py +100 -0
  404. alysis_code/swarm_backend.py +926 -0
  405. alysis_code/swarm_orchestrator.py +4020 -0
  406. alysis_code/swarm_scheduler.py +441 -0
  407. alysis_code/swarm_trace.py +429 -0
  408. alysis_code/swarm_worker.py +2119 -0
  409. alysis_code/swarm_write_guard.py +348 -0
  410. alysis_code/task_dependencies.py +170 -0
  411. alysis_code/task_readiness.py +992 -0
  412. alysis_code/task_scope.py +2148 -0
  413. alysis_code/terminal_manager.py +762 -0
  414. alysis_code/terminal_ownership.py +460 -0
  415. alysis_code/text_normalization.py +30 -0
  416. alysis_code/token_budget.py +97 -0
  417. alysis_code/tools/README.md +34 -0
  418. alysis_code/tools/__init__.py +1 -0
  419. alysis_code/tools/artifacts.py +127 -0
  420. alysis_code/tools/availability.py +188 -0
  421. alysis_code/tools/fs.py +1456 -0
  422. alysis_code/tools/git.py +461 -0
  423. alysis_code/tools/history.py +229 -0
  424. alysis_code/tools/http_timeout.py +78 -0
  425. alysis_code/tools/image_generation.py +552 -0
  426. alysis_code/tools/registry.py +2936 -0
  427. alysis_code/tools/repo_map.py +476 -0
  428. alysis_code/tools/search.py +563 -0
  429. alysis_code/tools/shell.py +135 -0
  430. alysis_code/tools/symbols.py +1350 -0
  431. alysis_code/tools/test_discovery.py +643 -0
  432. alysis_code/tools/web.py +482 -0
  433. alysis_code/tools/web_search.py +2012 -0
  434. alysis_code/tools/web_search_dashscope.py +557 -0
  435. alysis_code/tools/web_search_ddgs.py +221 -0
  436. alysis_code/tools/web_search_provider_adapters.py +1429 -0
  437. alysis_code/tools/web_search_tavily.py +194 -0
  438. alysis_code/updates.py +933 -0
  439. alysis_code/usage_tracker.py +1990 -0
  440. alysis_code/verification_command_analysis.py +1004 -0
  441. alysis_code/verification_contract.py +574 -0
  442. alysis_code/verification_failure_summary.py +273 -0
  443. alysis_code/verification_repair.py +385 -0
  444. alysis_code/verify_gate.py +3129 -0
  445. alysis_code/web_research.py +1872 -0
  446. alysis_code/web_search_adapters.py +66 -0
  447. alysis_code/web_search_policy.py +27 -0
  448. alysis_code/workspace_binding.py +389 -0
  449. alysis_code/workspace_binding_ui.py +408 -0
  450. alysis_code/workspace_context.py +273 -0
  451. alysis_code/workspace_isolation.py +138 -0
  452. alysis_code/workspace_provisioning.py +455 -0
  453. alysis_code-0.13.0.dist-info/METADATA +507 -0
  454. alysis_code-0.13.0.dist-info/RECORD +458 -0
  455. alysis_code-0.13.0.dist-info/WHEEL +4 -0
  456. alysis_code-0.13.0.dist-info/entry_points.txt +3 -0
  457. alysis_code-0.13.0.dist-info/licenses/LICENSE +176 -0
  458. alysis_code-0.13.0.dist-info/licenses/NOTICE +4 -0
@@ -0,0 +1,500 @@
1
+ from __future__ import annotations
2
+
3
+ import json
4
+ import re
5
+ import shlex
6
+
7
+ from .models import SkillBundle, SkillMatch
8
+ from .state import SkillCatalogEntry
9
+
10
+ SKILL_ADVERTISE_FALLBACK_MAX_CHARS = 16_000
11
+ SKILL_ADVERTISE_MAX_ITEMS = 16
12
+ SKILL_MATCH_CONTEXT_MAX_CHARS = 2_200
13
+ SKILL_MATCH_MAX_ITEMS = 4
14
+ EXPLICIT_SKILL_CONTEXT_TOTAL_MAX_CHARS = 10_000
15
+ EXPLICIT_SKILL_ENTRYPOINT_MAX_CHARS = 8_000
16
+ EXPLICIT_SKILL_ARGUMENT_SUBSTITUTION_MAX_CHARS = 1_200
17
+ EXPLICIT_SKILL_ARGUMENTS_MAX_CHARS = 480
18
+ EXPLICIT_SKILL_POSITIONAL_ARG_MAX_CHARS = 160
19
+ EXPLICIT_SKILL_NAME_DISPLAY_MAX_CHARS = 160
20
+ EXPLICIT_SKILL_DESCRIPTION_DISPLAY_MAX_CHARS = 240
21
+ EXPLICIT_SKILL_SOURCE_SCOPE_DISPLAY_MAX_CHARS = 40
22
+ EXPLICIT_SKILL_SOURCE_KIND_DISPLAY_MAX_CHARS = 40
23
+ EXPLICIT_SKILL_SOURCE_PATH_DISPLAY_MAX_CHARS = 320
24
+
25
+
26
+ def build_skill_advertise_block(
27
+ *,
28
+ skills: list[SkillBundle] | tuple[SkillBundle, ...],
29
+ max_chars: int = SKILL_ADVERTISE_FALLBACK_MAX_CHARS,
30
+ max_items: int = SKILL_ADVERTISE_MAX_ITEMS,
31
+ ) -> str | None:
32
+ if not skills:
33
+ return None
34
+ lines = [
35
+ "<skill_context>",
36
+ "source: discovered skill bundles",
37
+ "usage:",
38
+ "- Skills are optional attachable context, not auto-executed workflows.",
39
+ "- Use skill_read(name) to inspect SKILL.md before relying on a skill.",
40
+ "- Use skill_read(name, path) for specific files under that skill bundle.",
41
+ "available_skills:",
42
+ ]
43
+ truncated = False
44
+ for idx, skill in enumerate(skills):
45
+ if idx >= max_items:
46
+ truncated = True
47
+ break
48
+ description = _truncate_text(skill.description, max_chars=140)
49
+ line = (
50
+ f"- {skill.name} | {description} | source={skill.source_scope}/{skill.source_kind} "
51
+ f"| trust={skill.trust_level}"
52
+ )
53
+ projected = "\n".join([*lines, line, "</skill_context>"])
54
+ if len(projected) > max_chars:
55
+ truncated = True
56
+ break
57
+ lines.append(line)
58
+ if truncated:
59
+ lines.append("- ...(truncated)")
60
+ lines.append("</skill_context>")
61
+ return _bounded_block("\n".join(lines).strip() + "\n", max_chars=max_chars)
62
+
63
+
64
+ def build_matched_skill_context(
65
+ *,
66
+ matches: tuple[SkillMatch, ...] | list[SkillMatch],
67
+ max_chars: int = SKILL_MATCH_CONTEXT_MAX_CHARS,
68
+ max_items: int = SKILL_MATCH_MAX_ITEMS,
69
+ ) -> str | None:
70
+ filtered = [match for match in matches if isinstance(match, SkillMatch)]
71
+ if not filtered:
72
+ return None
73
+ lines = [
74
+ "<matched_skill_context>",
75
+ "source: host lexical matcher",
76
+ "trust: lower_priority_than_system_direct_user_and_explicit_skill_context",
77
+ "These skills may be relevant to the current turn. Read them on demand with skill_read before using them.",
78
+ "matched_skills:",
79
+ ]
80
+ truncated = False
81
+ for idx, match in enumerate(filtered):
82
+ if idx >= max_items:
83
+ truncated = True
84
+ break
85
+ matched_terms = ", ".join(match.matched_terms[:4]) if match.matched_terms else "-"
86
+ line = (
87
+ f"- {match.skill.name} | {_truncate_text(match.skill.description, max_chars=120)} "
88
+ f"| matched={matched_terms}"
89
+ )
90
+ projected = "\n".join([*lines, line, "</matched_skill_context>"])
91
+ if len(projected) > max_chars:
92
+ truncated = True
93
+ break
94
+ lines.append(line)
95
+ if truncated:
96
+ lines.append("- ...(truncated)")
97
+ lines.append("</matched_skill_context>")
98
+ return _bounded_block("\n".join(lines).strip() + "\n", max_chars=max_chars)
99
+
100
+
101
+ def build_explicit_skill_context_message(
102
+ *,
103
+ skill: SkillBundle,
104
+ task_text: str = "",
105
+ ) -> str:
106
+ resolved_arguments = _resolve_explicit_skill_arguments(task_text)
107
+ placeholder_tokens = _detect_explicit_skill_placeholders(skill.instructions)
108
+ header_fields = _render_explicit_skill_header_fields(skill)
109
+ message = _build_explicit_skill_context_for_mode(
110
+ header_fields=header_fields,
111
+ instructions=skill.instructions,
112
+ raw_task=str(resolved_arguments["raw_task"]),
113
+ positional_args=tuple(resolved_arguments["positional_args"]),
114
+ parser_name=str(resolved_arguments["parser_name"]),
115
+ placeholder_tokens=placeholder_tokens,
116
+ reduce_arguments=False,
117
+ omit_argument_block=False,
118
+ )
119
+ if len(message) <= EXPLICIT_SKILL_CONTEXT_TOTAL_MAX_CHARS:
120
+ return message
121
+
122
+ reduced_message = _build_explicit_skill_context_for_mode(
123
+ header_fields=header_fields,
124
+ instructions=skill.instructions,
125
+ raw_task=str(resolved_arguments["raw_task"]),
126
+ positional_args=tuple(resolved_arguments["positional_args"]),
127
+ parser_name=str(resolved_arguments["parser_name"]),
128
+ placeholder_tokens=placeholder_tokens,
129
+ reduce_arguments=True,
130
+ omit_argument_block=False,
131
+ )
132
+ if len(reduced_message) <= EXPLICIT_SKILL_CONTEXT_TOTAL_MAX_CHARS:
133
+ return reduced_message
134
+
135
+ omitted_message = _build_explicit_skill_context_for_mode(
136
+ header_fields=header_fields,
137
+ instructions=skill.instructions,
138
+ raw_task=str(resolved_arguments["raw_task"]),
139
+ positional_args=tuple(resolved_arguments["positional_args"]),
140
+ parser_name=str(resolved_arguments["parser_name"]),
141
+ placeholder_tokens=placeholder_tokens,
142
+ reduce_arguments=True,
143
+ omit_argument_block=True,
144
+ )
145
+ if len(omitted_message) <= EXPLICIT_SKILL_CONTEXT_TOTAL_MAX_CHARS:
146
+ return omitted_message
147
+
148
+ return omitted_message
149
+
150
+
151
+ def render_skill_info_text(
152
+ skill: SkillBundle,
153
+ *,
154
+ catalog_entry: SkillCatalogEntry | None = None,
155
+ ) -> str:
156
+ aliases = ", ".join(skill.aliases) if skill.aliases else skill.name
157
+ lifecycle_lines: list[str] = []
158
+ if catalog_entry is not None:
159
+ lifecycle_lines.extend(
160
+ [
161
+ f"enabled: {'yes' if catalog_entry.enabled else 'no'}",
162
+ f"managed: {'yes' if catalog_entry.managed else 'no'}",
163
+ ]
164
+ )
165
+ if catalog_entry.disabled_by:
166
+ lifecycle_lines.append(f"disabled_by: {catalog_entry.disabled_by}")
167
+ record = catalog_entry.install_record
168
+ if record is not None:
169
+ lifecycle_lines.extend(
170
+ [
171
+ f"install_source_kind: {record.source_kind or '-'}",
172
+ f"install_source: {record.source or '-'}",
173
+ f"install_commit: {record.source_commit or '-'}",
174
+ f"installed_at: {record.installed_at or '-'}",
175
+ ]
176
+ )
177
+ lifecycle_block = "\n".join(f"{line}" for line in lifecycle_lines)
178
+ if lifecycle_block:
179
+ lifecycle_block += "\n"
180
+ return (
181
+ f"name: {skill.name}\n"
182
+ f"description: {skill.description}\n"
183
+ f"aliases: {aliases}\n"
184
+ f"{lifecycle_block}"
185
+ f"source_scope: {skill.source_scope}\n"
186
+ f"source_kind: {skill.source_kind}\n"
187
+ f"source_family: {skill.source_family}\n"
188
+ f"trust_level: {skill.trust_level}\n"
189
+ f"source_path: {skill.source_path.as_posix()}\n"
190
+ f"entry_path: {skill.entry_path.as_posix()}\n\n"
191
+ f"{skill.instructions.strip()}\n"
192
+ )
193
+
194
+
195
+ def _truncate_text(text: str, *, max_chars: int) -> str:
196
+ compact = " ".join(str(text or "").split())
197
+ if len(compact) <= max_chars:
198
+ return compact
199
+ if max_chars <= 3:
200
+ return compact[:max_chars]
201
+ return compact[: max_chars - 3] + "..."
202
+
203
+
204
+ def _bounded_block(text: str, *, max_chars: int) -> str:
205
+ if len(text) <= max_chars:
206
+ return text
207
+ if max_chars <= 18:
208
+ return text[:max_chars]
209
+ return text[: max_chars - 18].rstrip() + "\n...(truncated)\n"
210
+
211
+
212
+ def _resolve_explicit_skill_arguments(task_text: str) -> dict[str, object]:
213
+ raw_task = str(task_text or "")
214
+ parser_name = "shlex.split"
215
+ try:
216
+ parsed = tuple(shlex.split(raw_task, posix=True))
217
+ except ValueError:
218
+ parsed = tuple(raw_task.split())
219
+ parser_name = "whitespace_split_fallback"
220
+ return {
221
+ "raw_task": raw_task,
222
+ "positional_args": parsed,
223
+ "parser_name": parser_name,
224
+ }
225
+
226
+
227
+ def _detect_explicit_skill_placeholders(text: str) -> tuple[str, ...]:
228
+ seen: set[str] = set()
229
+ ordered: list[str] = []
230
+ for match in re.finditer(r"\$(ARGUMENTS|\d+)", str(text or "")):
231
+ token = str(match.group(1) or "")
232
+ if token in seen:
233
+ continue
234
+ seen.add(token)
235
+ ordered.append(token)
236
+ return tuple(ordered)
237
+
238
+
239
+ def _substitute_explicit_skill_placeholders(
240
+ text: str,
241
+ *,
242
+ raw_task: str,
243
+ positional_args: tuple[str, ...],
244
+ ) -> str:
245
+ def _replace(match: re.Match[str]) -> str:
246
+ token = str(match.group(1) or "")
247
+ if token == "ARGUMENTS":
248
+ return raw_task
249
+ try:
250
+ index = int(token) - 1
251
+ except ValueError:
252
+ return match.group(0)
253
+ if index < 0:
254
+ return ""
255
+ return positional_args[index] if index < len(positional_args) else ""
256
+
257
+ return re.sub(r"\$(ARGUMENTS|\d+)", _replace, str(text or ""))
258
+
259
+
260
+ def _build_explicit_skill_context_for_mode(
261
+ *,
262
+ header_fields: dict[str, str],
263
+ instructions: str,
264
+ raw_task: str,
265
+ positional_args: tuple[str, ...],
266
+ parser_name: str,
267
+ placeholder_tokens: tuple[str, ...],
268
+ reduce_arguments: bool,
269
+ omit_argument_block: bool,
270
+ ) -> str:
271
+ if reduce_arguments:
272
+ raw_task = _truncate_explicit_argument_value(
273
+ raw_task,
274
+ max_chars=EXPLICIT_SKILL_ARGUMENTS_MAX_CHARS,
275
+ )
276
+ positional_args = tuple(
277
+ _truncate_explicit_argument_value(
278
+ value,
279
+ max_chars=EXPLICIT_SKILL_POSITIONAL_ARG_MAX_CHARS,
280
+ )
281
+ for value in positional_args
282
+ )
283
+ argument_lines, argument_block_reduced = _render_explicit_skill_argument_lines(
284
+ raw_task=raw_task,
285
+ positional_args=positional_args,
286
+ parser_name=parser_name,
287
+ placeholder_tokens=placeholder_tokens,
288
+ omit_argument_block=omit_argument_block,
289
+ )
290
+ substitution_reduced = reduce_arguments and bool(placeholder_tokens)
291
+ instructions_text = _substitute_explicit_skill_placeholders(
292
+ instructions,
293
+ raw_task=raw_task,
294
+ positional_args=positional_args,
295
+ )
296
+
297
+ instruction_truncated = False
298
+ notices = ""
299
+ for _ in range(3):
300
+ fixed_chars = _explicit_skill_context_fixed_chars(
301
+ header_fields=header_fields,
302
+ argument_lines=argument_lines,
303
+ notices=notices,
304
+ )
305
+ available_instruction_chars = min(
306
+ EXPLICIT_SKILL_ENTRYPOINT_MAX_CHARS,
307
+ max(
308
+ 0,
309
+ EXPLICIT_SKILL_CONTEXT_TOTAL_MAX_CHARS - fixed_chars,
310
+ ),
311
+ )
312
+ bounded_instructions, instruction_truncated = _bound_explicit_skill_instructions(
313
+ instructions_text,
314
+ max_chars=available_instruction_chars,
315
+ )
316
+ next_notices = _render_explicit_skill_context_notices(
317
+ instruction_truncated=instruction_truncated,
318
+ substitution_reduced=substitution_reduced,
319
+ argument_block_reduced=argument_block_reduced or omit_argument_block,
320
+ argument_block_omitted=omit_argument_block and bool(placeholder_tokens),
321
+ )
322
+ if next_notices == notices:
323
+ notices = next_notices
324
+ break
325
+ notices = next_notices
326
+
327
+ return (
328
+ "<explicit_skill_context>\n"
329
+ "source: user-selected skill for this turn only\n"
330
+ "trust: lower_priority_than_system_and_direct_user_instructions\n"
331
+ f"name: {header_fields['name']}\n"
332
+ f"description: {header_fields['description']}\n"
333
+ f"source_scope: {header_fields['source_scope']}\n"
334
+ f"source_kind: {header_fields['source_kind']}\n"
335
+ f"source_path: {header_fields['source_path']}\n"
336
+ "This skill is attached only for the current turn. It does not persist across future turns.\n"
337
+ "turn_requirement: Apply this selected skill before taking other actions on the next user task.\n"
338
+ "task_binding: Treat this wrapper and the next user message as one bound instruction set.\n"
339
+ f"{argument_lines}"
340
+ "Read additional bundle files with skill_read(name[, path]) before acting if the skill entrypoint references them.\n\n"
341
+ f"{notices}"
342
+ "<skill_instructions>\n"
343
+ f"{bounded_instructions.rstrip()}\n"
344
+ "</skill_instructions>\n"
345
+ "</explicit_skill_context>\n"
346
+ )
347
+
348
+
349
+ def _explicit_skill_context_fixed_chars(
350
+ *,
351
+ header_fields: dict[str, str],
352
+ argument_lines: str,
353
+ notices: str,
354
+ ) -> int:
355
+ fixed = (
356
+ "<explicit_skill_context>\n"
357
+ "source: user-selected skill for this turn only\n"
358
+ "trust: lower_priority_than_system_and_direct_user_instructions\n"
359
+ f"name: {header_fields['name']}\n"
360
+ f"description: {header_fields['description']}\n"
361
+ f"source_scope: {header_fields['source_scope']}\n"
362
+ f"source_kind: {header_fields['source_kind']}\n"
363
+ f"source_path: {header_fields['source_path']}\n"
364
+ "This skill is attached only for the current turn. It does not persist across future turns.\n"
365
+ "turn_requirement: Apply this selected skill before taking other actions on the next user task.\n"
366
+ "task_binding: Treat this wrapper and the next user message as one bound instruction set.\n"
367
+ f"{argument_lines}"
368
+ "Read additional bundle files with skill_read(name[, path]) before acting if the skill entrypoint references them.\n\n"
369
+ f"{notices}"
370
+ "<skill_instructions>\n"
371
+ "\n"
372
+ "</skill_instructions>\n"
373
+ "</explicit_skill_context>\n"
374
+ )
375
+ return len(fixed)
376
+
377
+
378
+ def _render_explicit_skill_context_notices(
379
+ *,
380
+ instruction_truncated: bool,
381
+ substitution_reduced: bool,
382
+ argument_block_reduced: bool,
383
+ argument_block_omitted: bool,
384
+ ) -> str:
385
+ if not any(
386
+ (
387
+ instruction_truncated,
388
+ substitution_reduced,
389
+ argument_block_reduced,
390
+ argument_block_omitted,
391
+ )
392
+ ):
393
+ return ""
394
+ reasons: list[str] = []
395
+ if instruction_truncated:
396
+ reasons.append("Attached entrypoint preview was truncated")
397
+ if substitution_reduced:
398
+ reasons.append("placeholder substitution was reduced")
399
+ if argument_block_omitted:
400
+ reasons.append("argument_substitution was omitted")
401
+ elif argument_block_reduced:
402
+ reasons.append("argument_substitution was reduced")
403
+ reason_text = "; ".join(reasons)
404
+ return (
405
+ f"entrypoint_notice: {reason_text} to fit the explicit skill wrapper budget. "
406
+ "The direct user task remains available in the next user message. "
407
+ "Use skill_read(name[, path]) for the full SKILL.md entrypoint or additional bundle files.\n"
408
+ )
409
+
410
+
411
+ def _render_explicit_skill_header_fields(skill: SkillBundle) -> dict[str, str]:
412
+ return {
413
+ "name": _truncate_explicit_metadata_value(
414
+ skill.name,
415
+ max_chars=EXPLICIT_SKILL_NAME_DISPLAY_MAX_CHARS,
416
+ ),
417
+ "description": _truncate_explicit_metadata_value(
418
+ skill.description,
419
+ max_chars=EXPLICIT_SKILL_DESCRIPTION_DISPLAY_MAX_CHARS,
420
+ ),
421
+ "source_scope": _truncate_explicit_metadata_value(
422
+ skill.source_scope,
423
+ max_chars=EXPLICIT_SKILL_SOURCE_SCOPE_DISPLAY_MAX_CHARS,
424
+ ),
425
+ "source_kind": _truncate_explicit_metadata_value(
426
+ skill.source_kind,
427
+ max_chars=EXPLICIT_SKILL_SOURCE_KIND_DISPLAY_MAX_CHARS,
428
+ ),
429
+ "source_path": _truncate_explicit_metadata_value(
430
+ skill.source_path.as_posix(),
431
+ max_chars=EXPLICIT_SKILL_SOURCE_PATH_DISPLAY_MAX_CHARS,
432
+ ),
433
+ }
434
+
435
+
436
+ def _render_explicit_skill_argument_lines(
437
+ *,
438
+ raw_task: str,
439
+ positional_args: tuple[str, ...],
440
+ parser_name: str,
441
+ placeholder_tokens: tuple[str, ...],
442
+ omit_argument_block: bool,
443
+ ) -> tuple[str, bool]:
444
+ if not placeholder_tokens or omit_argument_block:
445
+ return "", False
446
+ include_arguments = "ARGUMENTS" in placeholder_tokens
447
+ positional_indexes = sorted(
448
+ {int(token) for token in placeholder_tokens if token.isdigit() and int(token) > 0}
449
+ )
450
+ lines = [
451
+ "argument_substitution:",
452
+ f"- parser = {parser_name}",
453
+ ]
454
+ if include_arguments:
455
+ lines.append(f"- $ARGUMENTS = {_json_literal(raw_task)}")
456
+ for index in positional_indexes:
457
+ value = positional_args[index - 1] if index <= len(positional_args) else ""
458
+ lines.append(f"- ${index} = {_json_literal(value)}")
459
+ if positional_indexes:
460
+ lines.append('- missing positional placeholders resolve to ""')
461
+ rendered = "\n".join(lines) + "\n"
462
+ reduced = False
463
+ if len(rendered) > EXPLICIT_SKILL_ARGUMENT_SUBSTITUTION_MAX_CHARS:
464
+ reduced = True
465
+ rendered = (
466
+ _bounded_block(
467
+ rendered.rstrip(),
468
+ max_chars=EXPLICIT_SKILL_ARGUMENT_SUBSTITUTION_MAX_CHARS,
469
+ ).rstrip()
470
+ + "\n"
471
+ )
472
+ return rendered, reduced
473
+
474
+
475
+ def _json_literal(value: str) -> str:
476
+ return json.dumps(str(value or ""), ensure_ascii=False)
477
+
478
+
479
+ def _truncate_explicit_argument_value(text: str, *, max_chars: int) -> str:
480
+ value = str(text or "")
481
+ if len(value) <= max_chars:
482
+ return value
483
+ if max_chars <= 16:
484
+ return value[:max_chars]
485
+ return value[: max_chars - 16].rstrip() + "...(truncated)"
486
+
487
+
488
+ def _truncate_explicit_metadata_value(text: str, *, max_chars: int) -> str:
489
+ value = " ".join(str(text or "").split())
490
+ if len(value) <= max_chars:
491
+ return value
492
+ if max_chars <= 16:
493
+ return value[:max_chars]
494
+ return value[: max_chars - 16].rstrip() + "...(truncated)"
495
+
496
+
497
+ def _bound_explicit_skill_instructions(text: str, *, max_chars: int) -> tuple[str, bool]:
498
+ normalized = str(text or "").rstrip()
499
+ bounded = _bounded_block(normalized, max_chars=max(0, max_chars))
500
+ return bounded, bounded != normalized
@@ -0,0 +1,142 @@
1
+ from __future__ import annotations
2
+
3
+ import shutil
4
+ from dataclasses import dataclass
5
+ from pathlib import Path
6
+ from time import strftime
7
+
8
+ from .paths import (
9
+ global_managed_skill_root,
10
+ normalize_project_skill_family,
11
+ project_managed_skill_root,
12
+ project_skill_root_for_family,
13
+ skill_bundle_dir_name,
14
+ )
15
+ from .state import (
16
+ ManagedSkillRecord,
17
+ load_global_skill_state,
18
+ load_project_skill_state,
19
+ save_global_skill_state,
20
+ save_project_skill_state,
21
+ with_managed_install,
22
+ )
23
+ from .transactions import commit_managed_bundle_update
24
+
25
+
26
+ @dataclass(frozen=True)
27
+ class SkillScaffoldResult:
28
+ bundle_path: Path
29
+ managed: bool
30
+ scope: str
31
+ family: str
32
+
33
+
34
+ def scaffold_skill_bundle(
35
+ *,
36
+ name: str,
37
+ description: str = "",
38
+ workspace_root: Path,
39
+ project: bool = True,
40
+ family: str = "native",
41
+ force: bool = False,
42
+ user_config_dir: Path | None = None,
43
+ ) -> SkillScaffoldResult:
44
+ normalized_family = normalize_project_skill_family(family) if project else "native"
45
+ bundle_dir = skill_bundle_dir_name(name)
46
+ if project:
47
+ root = (
48
+ project_managed_skill_root(workspace_root)
49
+ if normalized_family == "native"
50
+ else project_skill_root_for_family(workspace_root, family=normalized_family)
51
+ )
52
+ else:
53
+ root = global_managed_skill_root(user_config_dir=user_config_dir)
54
+ bundle_path = root / bundle_dir
55
+ rendered_description = description.strip() or f"Describe when and why to use the {name} skill."
56
+
57
+ managed = normalized_family == "native"
58
+ if managed:
59
+ installed_at = strftime("%Y-%m-%dT%H:%M:%SZ")
60
+ record = ManagedSkillRecord(
61
+ name=name,
62
+ bundle_dir=bundle_dir,
63
+ scope=("project" if project else "user"),
64
+ source_kind="scaffold",
65
+ source=str(bundle_path),
66
+ installed_at=installed_at,
67
+ )
68
+ if project:
69
+ state = with_managed_install(load_project_skill_state(workspace_root), record)
70
+ commit_managed_bundle_update(
71
+ target_path=bundle_path,
72
+ force=force,
73
+ stage_bundle=lambda staged: _write_scaffold_bundle(
74
+ staged,
75
+ name=name,
76
+ description=rendered_description,
77
+ ),
78
+ persist_state=lambda: save_project_skill_state(workspace_root, state),
79
+ )
80
+ else:
81
+ state = with_managed_install(
82
+ load_global_skill_state(user_config_dir=user_config_dir),
83
+ record,
84
+ )
85
+ commit_managed_bundle_update(
86
+ target_path=bundle_path,
87
+ force=force,
88
+ stage_bundle=lambda staged: _write_scaffold_bundle(
89
+ staged,
90
+ name=name,
91
+ description=rendered_description,
92
+ ),
93
+ persist_state=lambda: save_global_skill_state(
94
+ state,
95
+ user_config_dir=user_config_dir,
96
+ ),
97
+ )
98
+ else:
99
+ if bundle_path.exists():
100
+ if not force:
101
+ raise RuntimeError(f"Skill bundle already exists: {bundle_path}")
102
+ shutil.rmtree(bundle_path)
103
+ _write_scaffold_bundle(
104
+ bundle_path,
105
+ name=name,
106
+ description=rendered_description,
107
+ )
108
+ return SkillScaffoldResult(
109
+ bundle_path=bundle_path,
110
+ managed=managed,
111
+ scope=("project" if project else "user"),
112
+ family=normalized_family,
113
+ )
114
+
115
+
116
+ def _write_scaffold_bundle(bundle_path: Path, *, name: str, description: str) -> None:
117
+ bundle_path.mkdir(parents=True, exist_ok=True)
118
+ for child in ("references", "scripts", "assets"):
119
+ (bundle_path / child).mkdir(parents=True, exist_ok=True)
120
+ (bundle_path / "SKILL.md").write_text(
121
+ _skill_template(name=name, description=description),
122
+ encoding="utf-8",
123
+ )
124
+
125
+
126
+ def _skill_template(*, name: str, description: str) -> str:
127
+ return (
128
+ "---\n"
129
+ f"name: {name}\n"
130
+ f"description: {description}\n"
131
+ "---\n\n"
132
+ "# Purpose\n"
133
+ "- Explain when this skill should be used.\n"
134
+ "- Call out the strongest signals that the skill applies.\n\n"
135
+ "# Workflow\n"
136
+ "- Describe the preferred steps the agent should follow.\n"
137
+ "- Keep the guidance specific, short, and actionable.\n\n"
138
+ "# Bundle Notes\n"
139
+ "- Put deeper references in `references/`.\n"
140
+ "- Put runnable helpers in `scripts/`.\n"
141
+ "- Put templates or supporting files in `assets/`.\n"
142
+ )