voidx 3.2.2__tar.gz → 3.3.0__tar.gz

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 (290) hide show
  1. {voidx-3.2.2 → voidx-3.3.0}/PKG-INFO +1 -1
  2. {voidx-3.2.2 → voidx-3.3.0}/pyproject.toml +1 -1
  3. {voidx-3.2.2 → voidx-3.3.0}/src/voidx/__init__.py +1 -1
  4. {voidx-3.2.2 → voidx-3.3.0}/src/voidx/agent/graph/compaction_coordinator.py +6 -3
  5. {voidx-3.2.2 → voidx-3.3.0}/src/voidx/agent/graph/core/_voidx_graph.py +2 -0
  6. {voidx-3.2.2 → voidx-3.3.0}/src/voidx/agent/graph/subagent.py +60 -0
  7. {voidx-3.2.2 → voidx-3.3.0}/src/voidx/agent/prompts.py +7 -0
  8. {voidx-3.2.2 → voidx-3.3.0}/src/voidx/permission/rules.py +8 -1
  9. {voidx-3.2.2 → voidx-3.3.0}/src/voidx/skills/registry.py +41 -1
  10. {voidx-3.2.2 → voidx-3.3.0}/src/voidx/tools/agent.py +8 -13
  11. {voidx-3.2.2 → voidx-3.3.0}/src/voidx/tools/bash/core.py +0 -5
  12. {voidx-3.2.2 → voidx-3.3.0}/src/voidx/tools/bash/hint/file.py +2 -2
  13. {voidx-3.2.2 → voidx-3.3.0}/src/voidx/tools/bash/hint/git.py +4 -8
  14. {voidx-3.2.2 → voidx-3.3.0}/src/voidx/tools/bash/hint/search.py +6 -6
  15. {voidx-3.2.2 → voidx-3.3.0}/src/voidx/tools/bash/tool.py +8 -4
  16. {voidx-3.2.2 → voidx-3.3.0}/src/voidx/tools/clarify.py +4 -1
  17. {voidx-3.2.2 → voidx-3.3.0}/src/voidx/tools/compact_context.py +4 -1
  18. {voidx-3.2.2 → voidx-3.3.0}/src/voidx/tools/file_ops/edit_execute.py +22 -23
  19. {voidx-3.2.2 → voidx-3.3.0}/src/voidx/tools/file_ops/edit_resolve.py +8 -8
  20. {voidx-3.2.2 → voidx-3.3.0}/src/voidx/tools/file_ops/file.py +7 -4
  21. {voidx-3.2.2 → voidx-3.3.0}/src/voidx/tools/file_ops/read.py +5 -2
  22. {voidx-3.2.2 → voidx-3.3.0}/src/voidx/tools/file_ops/write.py +4 -4
  23. {voidx-3.2.2 → voidx-3.3.0}/src/voidx/tools/file_state.py +12 -4
  24. {voidx-3.2.2 → voidx-3.3.0}/src/voidx/tools/git.py +25 -15
  25. {voidx-3.2.2 → voidx-3.3.0}/src/voidx/tools/load_doc_template.py +4 -1
  26. voidx-3.3.0/src/voidx/tools/load_skills.py +12 -0
  27. {voidx-3.2.2 → voidx-3.3.0}/src/voidx/tools/lsp.py +10 -4
  28. {voidx-3.2.2 → voidx-3.3.0}/src/voidx/tools/plan_checkpoint.py +4 -1
  29. {voidx-3.2.2 → voidx-3.3.0}/src/voidx/tools/registry.py +3 -3
  30. {voidx-3.2.2 → voidx-3.3.0}/src/voidx/tools/search.py +14 -8
  31. voidx-3.3.0/src/voidx/tools/skills.py +252 -0
  32. {voidx-3.2.2 → voidx-3.3.0}/src/voidx/tools/task_status.py +17 -4
  33. {voidx-3.2.2 → voidx-3.3.0}/src/voidx/tools/todo.py +22 -23
  34. {voidx-3.2.2 → voidx-3.3.0}/src/voidx/tools/webfetch.py +4 -1
  35. {voidx-3.2.2 → voidx-3.3.0}/src/voidx/tools/websearch.py +4 -1
  36. {voidx-3.2.2 → voidx-3.3.0}/src/voidx/tools/workflow.py +4 -1
  37. {voidx-3.2.2 → voidx-3.3.0}/src/voidx/ui/output/console/formatting.py +5 -1
  38. {voidx-3.2.2 → voidx-3.3.0}/src/voidx/ui/output/diff.py +1 -1
  39. {voidx-3.2.2 → voidx-3.3.0}/src/voidx/ui/output/dock/__init__.py +6 -0
  40. {voidx-3.2.2 → voidx-3.3.0}/src/voidx/ui/output/dock/app.py +3 -0
  41. {voidx-3.2.2 → voidx-3.3.0}/src/voidx/ui/output/dock/formatting.py +2 -2
  42. {voidx-3.2.2 → voidx-3.3.0}/src/voidx/ui/output/dock/nodes.py +5 -3
  43. {voidx-3.2.2 → voidx-3.3.0}/src/voidx/ui/output/dock/nodes_checkpoint.py +1 -0
  44. {voidx-3.2.2 → voidx-3.3.0}/src/voidx/ui/output/dock/nodes_permission.py +2 -3
  45. {voidx-3.2.2 → voidx-3.3.0}/src/voidx/ui/output/dock/status.py +35 -16
  46. {voidx-3.2.2 → voidx-3.3.0}/src/voidx/ui/output/events/consumers.py +219 -18
  47. {voidx-3.2.2 → voidx-3.3.0}/src/voidx/ui/output/events/schema.py +1 -0
  48. {voidx-3.2.2 → voidx-3.3.0}/src/voidx/ui/output/tree.py +16 -6
  49. {voidx-3.2.2 → voidx-3.3.0}/src/voidx/ui/tui/render_activity.py +28 -2
  50. {voidx-3.2.2 → voidx-3.3.0}/src/voidx.egg-info/PKG-INFO +1 -1
  51. {voidx-3.2.2 → voidx-3.3.0}/src/voidx.egg-info/SOURCES.txt +1 -0
  52. voidx-3.2.2/src/voidx/tools/load_skills.py +0 -206
  53. {voidx-3.2.2 → voidx-3.3.0}/README.md +0 -0
  54. {voidx-3.2.2 → voidx-3.3.0}/setup.cfg +0 -0
  55. {voidx-3.2.2 → voidx-3.3.0}/src/voidx/agent/__init__.py +0 -0
  56. {voidx-3.2.2 → voidx-3.3.0}/src/voidx/agent/agents.py +0 -0
  57. {voidx-3.2.2 → voidx-3.3.0}/src/voidx/agent/attachments.py +0 -0
  58. {voidx-3.2.2 → voidx-3.3.0}/src/voidx/agent/goal_resolver.py +0 -0
  59. {voidx-3.2.2 → voidx-3.3.0}/src/voidx/agent/graph/__init__.py +0 -0
  60. {voidx-3.2.2 → voidx-3.3.0}/src/voidx/agent/graph/compaction.py +0 -0
  61. {voidx-3.2.2 → voidx-3.3.0}/src/voidx/agent/graph/contracts.py +0 -0
  62. {voidx-3.2.2 → voidx-3.3.0}/src/voidx/agent/graph/convergence.py +0 -0
  63. {voidx-3.2.2 → voidx-3.3.0}/src/voidx/agent/graph/core/__init__.py +0 -0
  64. {voidx-3.2.2 → voidx-3.3.0}/src/voidx/agent/graph/core/helpers.py +0 -0
  65. {voidx-3.2.2 → voidx-3.3.0}/src/voidx/agent/graph/core/llm.py +0 -0
  66. {voidx-3.2.2 → voidx-3.3.0}/src/voidx/agent/graph/permissions.py +0 -0
  67. {voidx-3.2.2 → voidx-3.3.0}/src/voidx/agent/graph/run_loop.py +0 -0
  68. {voidx-3.2.2 → voidx-3.3.0}/src/voidx/agent/graph/runtime.py +0 -0
  69. {voidx-3.2.2 → voidx-3.3.0}/src/voidx/agent/graph/runtime_guards.py +0 -0
  70. {voidx-3.2.2 → voidx-3.3.0}/src/voidx/agent/graph/session_mixin.py +0 -0
  71. {voidx-3.2.2 → voidx-3.3.0}/src/voidx/agent/graph/session_runtime.py +0 -0
  72. {voidx-3.2.2 → voidx-3.3.0}/src/voidx/agent/graph/streaming.py +0 -0
  73. {voidx-3.2.2 → voidx-3.3.0}/src/voidx/agent/graph/title_mixin.py +0 -0
  74. {voidx-3.2.2 → voidx-3.3.0}/src/voidx/agent/graph/todo_events.py +0 -0
  75. {voidx-3.2.2 → voidx-3.3.0}/src/voidx/agent/graph/tool_execution.py +0 -0
  76. {voidx-3.2.2 → voidx-3.3.0}/src/voidx/agent/graph/tool_executor/__init__.py +0 -0
  77. {voidx-3.2.2 → voidx-3.3.0}/src/voidx/agent/graph/tool_executor/executor.py +0 -0
  78. {voidx-3.2.2 → voidx-3.3.0}/src/voidx/agent/graph/tool_executor/guards.py +0 -0
  79. {voidx-3.2.2 → voidx-3.3.0}/src/voidx/agent/graph/tool_executor/helpers.py +0 -0
  80. {voidx-3.2.2 → voidx-3.3.0}/src/voidx/agent/graph/tool_executor/types.py +0 -0
  81. {voidx-3.2.2 → voidx-3.3.0}/src/voidx/agent/graph/tool_executor/ui.py +0 -0
  82. {voidx-3.2.2 → voidx-3.3.0}/src/voidx/agent/graph/tool_executor/workflow.py +0 -0
  83. {voidx-3.2.2 → voidx-3.3.0}/src/voidx/agent/graph/topology.py +0 -0
  84. {voidx-3.2.2 → voidx-3.3.0}/src/voidx/agent/graph/transcript_mixin.py +0 -0
  85. {voidx-3.2.2 → voidx-3.3.0}/src/voidx/agent/graph/turn_mixin.py +0 -0
  86. {voidx-3.2.2 → voidx-3.3.0}/src/voidx/agent/graph/turn_runner.py +0 -0
  87. {voidx-3.2.2 → voidx-3.3.0}/src/voidx/agent/graph/wiring.py +0 -0
  88. {voidx-3.2.2 → voidx-3.3.0}/src/voidx/agent/graph/workflow_utils.py +0 -0
  89. {voidx-3.2.2 → voidx-3.3.0}/src/voidx/agent/message_rows.py +0 -0
  90. {voidx-3.2.2 → voidx-3.3.0}/src/voidx/agent/runtime_context.py +0 -0
  91. {voidx-3.2.2 → voidx-3.3.0}/src/voidx/agent/slash/__init__.py +0 -0
  92. {voidx-3.2.2 → voidx-3.3.0}/src/voidx/agent/slash/code_ide.py +0 -0
  93. {voidx-3.2.2 → voidx-3.3.0}/src/voidx/agent/slash/guide.py +0 -0
  94. {voidx-3.2.2 → voidx-3.3.0}/src/voidx/agent/slash/handler.py +0 -0
  95. {voidx-3.2.2 → voidx-3.3.0}/src/voidx/agent/slash/host.py +0 -0
  96. {voidx-3.2.2 → voidx-3.3.0}/src/voidx/agent/slash/init.py +0 -0
  97. {voidx-3.2.2 → voidx-3.3.0}/src/voidx/agent/slash/lsp.py +0 -0
  98. {voidx-3.2.2 → voidx-3.3.0}/src/voidx/agent/slash/mcp.py +0 -0
  99. {voidx-3.2.2 → voidx-3.3.0}/src/voidx/agent/slash/model.py +0 -0
  100. {voidx-3.2.2 → voidx-3.3.0}/src/voidx/agent/slash/profile.py +0 -0
  101. {voidx-3.2.2 → voidx-3.3.0}/src/voidx/agent/slash/runtime.py +0 -0
  102. {voidx-3.2.2 → voidx-3.3.0}/src/voidx/agent/slash/session.py +0 -0
  103. {voidx-3.2.2 → voidx-3.3.0}/src/voidx/agent/slash/skills.py +0 -0
  104. {voidx-3.2.2 → voidx-3.3.0}/src/voidx/agent/slash/upgrade.py +0 -0
  105. {voidx-3.2.2 → voidx-3.3.0}/src/voidx/agent/state.py +0 -0
  106. {voidx-3.2.2 → voidx-3.3.0}/src/voidx/agent/task_state.py +0 -0
  107. {voidx-3.2.2 → voidx-3.3.0}/src/voidx/agent/todo_state.py +0 -0
  108. {voidx-3.2.2 → voidx-3.3.0}/src/voidx/agent/tool_call_ids.py +0 -0
  109. {voidx-3.2.2 → voidx-3.3.0}/src/voidx/agent/tool_exchange_sanitizer.py +0 -0
  110. {voidx-3.2.2 → voidx-3.3.0}/src/voidx/agent/tool_filters.py +0 -0
  111. {voidx-3.2.2 → voidx-3.3.0}/src/voidx/agent/tool_messages.py +0 -0
  112. {voidx-3.2.2 → voidx-3.3.0}/src/voidx/agent/tool_result_storage.py +0 -0
  113. {voidx-3.2.2 → voidx-3.3.0}/src/voidx/config/__init__.py +0 -0
  114. {voidx-3.2.2 → voidx-3.3.0}/src/voidx/config/enums.py +0 -0
  115. {voidx-3.2.2 → voidx-3.3.0}/src/voidx/config/models.py +0 -0
  116. {voidx-3.2.2 → voidx-3.3.0}/src/voidx/config/permissions.py +0 -0
  117. {voidx-3.2.2 → voidx-3.3.0}/src/voidx/config/settings.py +0 -0
  118. {voidx-3.2.2 → voidx-3.3.0}/src/voidx/config/settings_agent.py +0 -0
  119. {voidx-3.2.2 → voidx-3.3.0}/src/voidx/config/settings_api_keys.py +0 -0
  120. {voidx-3.2.2 → voidx-3.3.0}/src/voidx/config/settings_code_ide.py +0 -0
  121. {voidx-3.2.2 → voidx-3.3.0}/src/voidx/config/settings_custom.py +0 -0
  122. {voidx-3.2.2 → voidx-3.3.0}/src/voidx/config/settings_mcp.py +0 -0
  123. {voidx-3.2.2 → voidx-3.3.0}/src/voidx/config/settings_permissions.py +0 -0
  124. {voidx-3.2.2 → voidx-3.3.0}/src/voidx/config/settings_skills.py +0 -0
  125. {voidx-3.2.2 → voidx-3.3.0}/src/voidx/config/settings_update.py +0 -0
  126. {voidx-3.2.2 → voidx-3.3.0}/src/voidx/config/settings_utils.py +0 -0
  127. {voidx-3.2.2 → voidx-3.3.0}/src/voidx/config/settings_web.py +0 -0
  128. {voidx-3.2.2 → voidx-3.3.0}/src/voidx/data/__init__.py +0 -0
  129. {voidx-3.2.2 → voidx-3.3.0}/src/voidx/data/templates/api-doc.md +0 -0
  130. {voidx-3.2.2 → voidx-3.3.0}/src/voidx/data/templates/prd.md +0 -0
  131. {voidx-3.2.2 → voidx-3.3.0}/src/voidx/data/templates/readme.md +0 -0
  132. {voidx-3.2.2 → voidx-3.3.0}/src/voidx/data/templates/rfc.md +0 -0
  133. {voidx-3.2.2 → voidx-3.3.0}/src/voidx/data/templates/tech-design.md +0 -0
  134. {voidx-3.2.2 → voidx-3.3.0}/src/voidx/diffing.py +0 -0
  135. {voidx-3.2.2 → voidx-3.3.0}/src/voidx/llm/__init__.py +0 -0
  136. {voidx-3.2.2 → voidx-3.3.0}/src/voidx/llm/catalog.py +0 -0
  137. {voidx-3.2.2 → voidx-3.3.0}/src/voidx/llm/compaction.py +0 -0
  138. {voidx-3.2.2 → voidx-3.3.0}/src/voidx/llm/context.py +0 -0
  139. {voidx-3.2.2 → voidx-3.3.0}/src/voidx/llm/instruction.py +0 -0
  140. {voidx-3.2.2 → voidx-3.3.0}/src/voidx/llm/message_markers.py +0 -0
  141. {voidx-3.2.2 → voidx-3.3.0}/src/voidx/llm/message_status.py +0 -0
  142. {voidx-3.2.2 → voidx-3.3.0}/src/voidx/llm/provider.py +0 -0
  143. {voidx-3.2.2 → voidx-3.3.0}/src/voidx/llm/service.py +0 -0
  144. {voidx-3.2.2 → voidx-3.3.0}/src/voidx/llm/usage.py +0 -0
  145. {voidx-3.2.2 → voidx-3.3.0}/src/voidx/logging/__init__.py +0 -0
  146. {voidx-3.2.2 → voidx-3.3.0}/src/voidx/logging/request_log.py +0 -0
  147. {voidx-3.2.2 → voidx-3.3.0}/src/voidx/logging/tool_log.py +0 -0
  148. {voidx-3.2.2 → voidx-3.3.0}/src/voidx/lsp/__init__.py +0 -0
  149. {voidx-3.2.2 → voidx-3.3.0}/src/voidx/lsp/client.py +0 -0
  150. {voidx-3.2.2 → voidx-3.3.0}/src/voidx/lsp/config.py +0 -0
  151. {voidx-3.2.2 → voidx-3.3.0}/src/voidx/lsp/detector.py +0 -0
  152. {voidx-3.2.2 → voidx-3.3.0}/src/voidx/lsp/detector_data.py +0 -0
  153. {voidx-3.2.2 → voidx-3.3.0}/src/voidx/lsp/errors.py +0 -0
  154. {voidx-3.2.2 → voidx-3.3.0}/src/voidx/lsp/manager.py +0 -0
  155. {voidx-3.2.2 → voidx-3.3.0}/src/voidx/lsp/schema.py +0 -0
  156. {voidx-3.2.2 → voidx-3.3.0}/src/voidx/lsp/service.py +0 -0
  157. {voidx-3.2.2 → voidx-3.3.0}/src/voidx/main.py +0 -0
  158. {voidx-3.2.2 → voidx-3.3.0}/src/voidx/mcp/__init__.py +0 -0
  159. {voidx-3.2.2 → voidx-3.3.0}/src/voidx/mcp/client/__init__.py +0 -0
  160. {voidx-3.2.2 → voidx-3.3.0}/src/voidx/mcp/client/base.py +0 -0
  161. {voidx-3.2.2 → voidx-3.3.0}/src/voidx/mcp/client/errors.py +0 -0
  162. {voidx-3.2.2 → voidx-3.3.0}/src/voidx/mcp/client/http_transport.py +0 -0
  163. {voidx-3.2.2 → voidx-3.3.0}/src/voidx/mcp/client/sse_transport.py +0 -0
  164. {voidx-3.2.2 → voidx-3.3.0}/src/voidx/mcp/client/stdio_transport.py +0 -0
  165. {voidx-3.2.2 → voidx-3.3.0}/src/voidx/mcp/manager.py +0 -0
  166. {voidx-3.2.2 → voidx-3.3.0}/src/voidx/mcp/schema.py +0 -0
  167. {voidx-3.2.2 → voidx-3.3.0}/src/voidx/mcp/tool.py +0 -0
  168. {voidx-3.2.2 → voidx-3.3.0}/src/voidx/mcp_servers/__init__.py +0 -0
  169. {voidx-3.2.2 → voidx-3.3.0}/src/voidx/mcp_servers/web.py +0 -0
  170. {voidx-3.2.2 → voidx-3.3.0}/src/voidx/memory/__init__.py +0 -0
  171. {voidx-3.2.2 → voidx-3.3.0}/src/voidx/memory/cleanup.py +0 -0
  172. {voidx-3.2.2 → voidx-3.3.0}/src/voidx/memory/context_frames.py +0 -0
  173. {voidx-3.2.2 → voidx-3.3.0}/src/voidx/memory/jsonl_store.py +0 -0
  174. {voidx-3.2.2 → voidx-3.3.0}/src/voidx/memory/model_profiles.py +0 -0
  175. {voidx-3.2.2 → voidx-3.3.0}/src/voidx/memory/runtime_state.py +0 -0
  176. {voidx-3.2.2 → voidx-3.3.0}/src/voidx/memory/service.py +0 -0
  177. {voidx-3.2.2 → voidx-3.3.0}/src/voidx/memory/session.py +0 -0
  178. {voidx-3.2.2 → voidx-3.3.0}/src/voidx/memory/store.py +0 -0
  179. {voidx-3.2.2 → voidx-3.3.0}/src/voidx/memory/subagents.py +0 -0
  180. {voidx-3.2.2 → voidx-3.3.0}/src/voidx/memory/transcript.py +0 -0
  181. {voidx-3.2.2 → voidx-3.3.0}/src/voidx/permission/__init__.py +0 -0
  182. {voidx-3.2.2 → voidx-3.3.0}/src/voidx/permission/context.py +0 -0
  183. {voidx-3.2.2 → voidx-3.3.0}/src/voidx/permission/engine.py +0 -0
  184. {voidx-3.2.2 → voidx-3.3.0}/src/voidx/permission/evaluate.py +0 -0
  185. {voidx-3.2.2 → voidx-3.3.0}/src/voidx/permission/sandbox.py +0 -0
  186. {voidx-3.2.2 → voidx-3.3.0}/src/voidx/permission/schema.py +0 -0
  187. {voidx-3.2.2 → voidx-3.3.0}/src/voidx/permission/service.py +0 -0
  188. {voidx-3.2.2 → voidx-3.3.0}/src/voidx/permission/wildcard.py +0 -0
  189. {voidx-3.2.2 → voidx-3.3.0}/src/voidx/runtime/__init__.py +0 -0
  190. {voidx-3.2.2 → voidx-3.3.0}/src/voidx/runtime/attachments.py +0 -0
  191. {voidx-3.2.2 → voidx-3.3.0}/src/voidx/runtime/intent.py +0 -0
  192. {voidx-3.2.2 → voidx-3.3.0}/src/voidx/runtime/reference_tokens.py +0 -0
  193. {voidx-3.2.2 → voidx-3.3.0}/src/voidx/runtime/task_state.py +0 -0
  194. {voidx-3.2.2 → voidx-3.3.0}/src/voidx/runtime/todo.py +0 -0
  195. {voidx-3.2.2 → voidx-3.3.0}/src/voidx/runtime/ui.py +0 -0
  196. {voidx-3.2.2 → voidx-3.3.0}/src/voidx/runtime/ui_port.py +0 -0
  197. {voidx-3.2.2 → voidx-3.3.0}/src/voidx/selfupdate.py +0 -0
  198. {voidx-3.2.2 → voidx-3.3.0}/src/voidx/skills/__init__.py +0 -0
  199. {voidx-3.2.2 → voidx-3.3.0}/src/voidx/skills/context.py +0 -0
  200. {voidx-3.2.2 → voidx-3.3.0}/src/voidx/skills/references.py +0 -0
  201. {voidx-3.2.2 → voidx-3.3.0}/src/voidx/skills/schema.py +0 -0
  202. {voidx-3.2.2 → voidx-3.3.0}/src/voidx/skills/service.py +0 -0
  203. {voidx-3.2.2 → voidx-3.3.0}/src/voidx/tools/__init__.py +0 -0
  204. {voidx-3.2.2 → voidx-3.3.0}/src/voidx/tools/base.py +0 -0
  205. {voidx-3.2.2 → voidx-3.3.0}/src/voidx/tools/bash/__init__.py +0 -0
  206. {voidx-3.2.2 → voidx-3.3.0}/src/voidx/tools/bash/hint/__init__.py +0 -0
  207. {voidx-3.2.2 → voidx-3.3.0}/src/voidx/tools/bash/router.py +0 -0
  208. {voidx-3.2.2 → voidx-3.3.0}/src/voidx/tools/bash/safety.py +0 -0
  209. {voidx-3.2.2 → voidx-3.3.0}/src/voidx/tools/file_ops/__init__.py +0 -0
  210. {voidx-3.2.2 → voidx-3.3.0}/src/voidx/tools/file_ops/types.py +0 -0
  211. {voidx-3.2.2 → voidx-3.3.0}/src/voidx/tools/service.py +0 -0
  212. {voidx-3.2.2 → voidx-3.3.0}/src/voidx/tools/task_tracker.py +0 -0
  213. {voidx-3.2.2 → voidx-3.3.0}/src/voidx/tools/web_content.py +0 -0
  214. {voidx-3.2.2 → voidx-3.3.0}/src/voidx/tools/web_mcp.py +0 -0
  215. {voidx-3.2.2 → voidx-3.3.0}/src/voidx/ui/__init__.py +0 -0
  216. {voidx-3.2.2 → voidx-3.3.0}/src/voidx/ui/commands.py +0 -0
  217. {voidx-3.2.2 → voidx-3.3.0}/src/voidx/ui/frontend.py +0 -0
  218. {voidx-3.2.2 → voidx-3.3.0}/src/voidx/ui/gateway/__init__.py +0 -0
  219. {voidx-3.2.2 → voidx-3.3.0}/src/voidx/ui/gateway/bootstrap.py +0 -0
  220. {voidx-3.2.2 → voidx-3.3.0}/src/voidx/ui/gateway/server.py +0 -0
  221. {voidx-3.2.2 → voidx-3.3.0}/src/voidx/ui/gateway/session.py +0 -0
  222. {voidx-3.2.2 → voidx-3.3.0}/src/voidx/ui/output/__init__.py +0 -0
  223. {voidx-3.2.2 → voidx-3.3.0}/src/voidx/ui/output/agent_display.py +0 -0
  224. {voidx-3.2.2 → voidx-3.3.0}/src/voidx/ui/output/browse.py +0 -0
  225. {voidx-3.2.2 → voidx-3.3.0}/src/voidx/ui/output/capture.py +0 -0
  226. {voidx-3.2.2 → voidx-3.3.0}/src/voidx/ui/output/console/__init__.py +0 -0
  227. {voidx-3.2.2 → voidx-3.3.0}/src/voidx/ui/output/console/app.py +0 -0
  228. {voidx-3.2.2 → voidx-3.3.0}/src/voidx/ui/output/console/streaming.py +0 -0
  229. {voidx-3.2.2 → voidx-3.3.0}/src/voidx/ui/output/display_policy.py +0 -0
  230. {voidx-3.2.2 → voidx-3.3.0}/src/voidx/ui/output/dock/agent_placeholder.py +0 -0
  231. {voidx-3.2.2 → voidx-3.3.0}/src/voidx/ui/output/dock/nodes_startup.py +0 -0
  232. {voidx-3.2.2 → voidx-3.3.0}/src/voidx/ui/output/dock/nodes_status.py +0 -0
  233. {voidx-3.2.2 → voidx-3.3.0}/src/voidx/ui/output/dock/state.py +0 -0
  234. {voidx-3.2.2 → voidx-3.3.0}/src/voidx/ui/output/dock/stream.py +0 -0
  235. {voidx-3.2.2 → voidx-3.3.0}/src/voidx/ui/output/dock/todo.py +0 -0
  236. {voidx-3.2.2 → voidx-3.3.0}/src/voidx/ui/output/events/__init__.py +0 -0
  237. {voidx-3.2.2 → voidx-3.3.0}/src/voidx/ui/output/events/bus.py +0 -0
  238. {voidx-3.2.2 → voidx-3.3.0}/src/voidx/ui/output/types.py +0 -0
  239. {voidx-3.2.2 → voidx-3.3.0}/src/voidx/ui/protocol/__init__.py +0 -0
  240. {voidx-3.2.2 → voidx-3.3.0}/src/voidx/ui/protocol/commands.py +0 -0
  241. {voidx-3.2.2 → voidx-3.3.0}/src/voidx/ui/protocol/envelope.py +0 -0
  242. {voidx-3.2.2 → voidx-3.3.0}/src/voidx/ui/protocol/requests.py +0 -0
  243. {voidx-3.2.2 → voidx-3.3.0}/src/voidx/ui/protocol/schema.py +0 -0
  244. {voidx-3.2.2 → voidx-3.3.0}/src/voidx/ui/protocol/transcript.py +0 -0
  245. {voidx-3.2.2 → voidx-3.3.0}/src/voidx/ui/session.py +0 -0
  246. {voidx-3.2.2 → voidx-3.3.0}/src/voidx/ui/tools/__init__.py +0 -0
  247. {voidx-3.2.2 → voidx-3.3.0}/src/voidx/ui/tools/attachment_tokens.py +0 -0
  248. {voidx-3.2.2 → voidx-3.3.0}/src/voidx/ui/tools/clipboard_image.py +0 -0
  249. {voidx-3.2.2 → voidx-3.3.0}/src/voidx/ui/tools/clipboard_text.py +0 -0
  250. {voidx-3.2.2 → voidx-3.3.0}/src/voidx/ui/tools/code_ide.py +0 -0
  251. {voidx-3.2.2 → voidx-3.3.0}/src/voidx/ui/tools/file_picker.py +0 -0
  252. {voidx-3.2.2 → voidx-3.3.0}/src/voidx/ui/tools/skill_picker.py +0 -0
  253. {voidx-3.2.2 → voidx-3.3.0}/src/voidx/ui/transcript.py +0 -0
  254. {voidx-3.2.2 → voidx-3.3.0}/src/voidx/ui/tui/__init__.py +0 -0
  255. {voidx-3.2.2 → voidx-3.3.0}/src/voidx/ui/tui/activity.py +0 -0
  256. {voidx-3.2.2 → voidx-3.3.0}/src/voidx/ui/tui/app.py +0 -0
  257. {voidx-3.2.2 → voidx-3.3.0}/src/voidx/ui/tui/choice_mixin.py +0 -0
  258. {voidx-3.2.2 → voidx-3.3.0}/src/voidx/ui/tui/clipboard_mixin.py +0 -0
  259. {voidx-3.2.2 → voidx-3.3.0}/src/voidx/ui/tui/helpers.py +0 -0
  260. {voidx-3.2.2 → voidx-3.3.0}/src/voidx/ui/tui/input.py +0 -0
  261. {voidx-3.2.2 → voidx-3.3.0}/src/voidx/ui/tui/overlays.py +0 -0
  262. {voidx-3.2.2 → voidx-3.3.0}/src/voidx/ui/tui/panels.py +0 -0
  263. {voidx-3.2.2 → voidx-3.3.0}/src/voidx/ui/tui/parser.py +0 -0
  264. {voidx-3.2.2 → voidx-3.3.0}/src/voidx/ui/tui/render_frame.py +0 -0
  265. {voidx-3.2.2 → voidx-3.3.0}/src/voidx/ui/tui/render_input.py +0 -0
  266. {voidx-3.2.2 → voidx-3.3.0}/src/voidx/ui/tui/render_status.py +0 -0
  267. {voidx-3.2.2 → voidx-3.3.0}/src/voidx/ui/tui/render_todo.py +0 -0
  268. {voidx-3.2.2 → voidx-3.3.0}/src/voidx/ui/tui/renderer.py +0 -0
  269. {voidx-3.2.2 → voidx-3.3.0}/src/voidx/ui/tui/state.py +0 -0
  270. {voidx-3.2.2 → voidx-3.3.0}/src/voidx/ui/tui/terminal_mixin.py +0 -0
  271. {voidx-3.2.2 → voidx-3.3.0}/src/voidx/ui/tui/text_prompt_mixin.py +0 -0
  272. {voidx-3.2.2 → voidx-3.3.0}/src/voidx/workflow/__init__.py +0 -0
  273. {voidx-3.2.2 → voidx-3.3.0}/src/voidx/workflow/auto_advance.py +0 -0
  274. {voidx-3.2.2 → voidx-3.3.0}/src/voidx/workflow/context.py +0 -0
  275. {voidx-3.2.2 → voidx-3.3.0}/src/voidx/workflow/dag.py +0 -0
  276. {voidx-3.2.2 → voidx-3.3.0}/src/voidx/workflow/nodes.py +0 -0
  277. {voidx-3.2.2 → voidx-3.3.0}/src/voidx/workflow/policy.py +0 -0
  278. {voidx-3.2.2 → voidx-3.3.0}/src/voidx/workflow/reconcile.py +0 -0
  279. {voidx-3.2.2 → voidx-3.3.0}/src/voidx/workflow/render.py +0 -0
  280. {voidx-3.2.2 → voidx-3.3.0}/src/voidx/workflow/route.py +0 -0
  281. {voidx-3.2.2 → voidx-3.3.0}/src/voidx/workflow/runtime.py +0 -0
  282. {voidx-3.2.2 → voidx-3.3.0}/src/voidx/workflow/schema.py +0 -0
  283. {voidx-3.2.2 → voidx-3.3.0}/src/voidx/workflow/service.py +0 -0
  284. {voidx-3.2.2 → voidx-3.3.0}/src/voidx/workflow/types.py +0 -0
  285. {voidx-3.2.2 → voidx-3.3.0}/src/voidx.egg-info/dependency_links.txt +0 -0
  286. {voidx-3.2.2 → voidx-3.3.0}/src/voidx.egg-info/entry_points.txt +0 -0
  287. {voidx-3.2.2 → voidx-3.3.0}/src/voidx.egg-info/requires.txt +0 -0
  288. {voidx-3.2.2 → voidx-3.3.0}/src/voidx.egg-info/top_level.txt +0 -0
  289. {voidx-3.2.2 → voidx-3.3.0}/tests/test_install_sh.py +0 -0
  290. {voidx-3.2.2 → voidx-3.3.0}/tests/test_npm_package.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: voidx
3
- Version: 3.2.2
3
+ Version: 3.3.0
4
4
  Summary: A coding agent that quantifies everything and solves with tools, not fuzzy prompts.
5
5
  Requires-Python: >=3.11
6
6
  Description-Content-Type: text/markdown
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "voidx"
3
- version = "3.2.2"
3
+ version = "3.3.0"
4
4
  description = "A coding agent that quantifies everything and solves with tools, not fuzzy prompts."
5
5
  readme = "README.md"
6
6
  requires-python = ">=3.11"
@@ -1,3 +1,3 @@
1
1
  """VoidX - A coding agent that quantifies everything."""
2
2
 
3
- __version__ = "3.2.2"
3
+ __version__ = "3.3.0"
@@ -189,7 +189,7 @@ class GraphCompactionCoordinator:
189
189
  if host._ui.via_events():
190
190
  await host._ui.events.emit(StatusUpdated(
191
191
  status_id="compaction",
192
- label="Compacting context",
192
+ label="Compacting",
193
193
  detail=_compaction_status_detail(total_tokens, force=force, preflight=preflight),
194
194
  stage="compacting",
195
195
  display="record_only",
@@ -198,7 +198,7 @@ class GraphCompactionCoordinator:
198
198
  host._ui.ui.print(
199
199
  "[yellow]Context overflow — compacting...[/yellow]"
200
200
  if not force
201
- else "[yellow]Compacting context...[/yellow]"
201
+ else "[yellow]Compacting...[/yellow]"
202
202
  )
203
203
 
204
204
  runtime_prefix = _runtime_prefix(messages)
@@ -251,9 +251,10 @@ class GraphCompactionCoordinator:
251
251
  retry_label = f" (attempt {attempt})" if attempt > 1 else ""
252
252
  await host._ui.events.emit(StatusUpdated(
253
253
  status_id="compaction",
254
- label="Compacting context",
254
+ label="Compacting",
255
255
  detail=f"summarizing {len(head_msgs)} old messages{retry_label}",
256
256
  stage="compacting",
257
+ display="record_only",
257
258
  ))
258
259
  summary = await run_agent(head_msgs, previous_summary)
259
260
  if summary:
@@ -270,6 +271,7 @@ class GraphCompactionCoordinator:
270
271
  label="Compaction agent failed",
271
272
  detail=f"{e}; retrying ({attempt}/{COMPACTION_MAX_RETRIES})",
272
273
  stage="compacting",
274
+ display="record_only",
273
275
  ))
274
276
  else:
275
277
  host._ui.ui.print(f"[dim]Compaction agent failed ({e}) — retrying ({attempt}/{COMPACTION_MAX_RETRIES})[/dim]")
@@ -288,6 +290,7 @@ class GraphCompactionCoordinator:
288
290
  label="Compaction agent failed",
289
291
  detail=f"{failure_detail}; using extracted summary",
290
292
  stage="compacting",
293
+ display="record_only",
291
294
  ))
292
295
  else:
293
296
  err_msg = f" ({failure_detail})"
@@ -399,6 +399,7 @@ class VoidXGraph(
399
399
 
400
400
  ok = False
401
401
  run_metadata: dict[str, object] = {}
402
+ result = ""
402
403
  try:
403
404
  kwargs = {
404
405
  "sub_messages": sub_buffer,
@@ -439,6 +440,7 @@ class VoidXGraph(
439
440
  ok=ok,
440
441
  elapsed=time.monotonic() - started_at,
441
442
  finish_reason=str(run_metadata.get("finish_reason") or ("final_answer" if ok else "error")),
443
+ summary=result if ok else "",
442
444
  ))
443
445
  if self._session:
444
446
  await append_subagent_event(session_id, agent_run_id, {
@@ -3,6 +3,7 @@
3
3
  from __future__ import annotations
4
4
 
5
5
  import asyncio
6
+ import re
6
7
  import time
7
8
 
8
9
  from langchain_core.messages import AIMessage, HumanMessage, ToolMessage
@@ -43,6 +44,7 @@ from voidx.runtime.ui_port import AgentUiPort, runtime_ui_port
43
44
 
44
45
 
45
46
  _SAFETY_STEP_LIMIT = 50
47
+ _RESULT_CONTRACT_RETRY_LIMIT = 2
46
48
 
47
49
  async def run_subagent(
48
50
  agent_def: AgentDef,
@@ -105,6 +107,8 @@ async def run_subagent(
105
107
  )
106
108
  guard_state = RuntimeGuardState(wall_clock=WallClockGuardState.for_subagent())
107
109
  pending_guard_guidance: list[str] = []
110
+ contract_retry_count = 0
111
+ has_successful_tool_work = False
108
112
 
109
113
  context, context_cache = RuntimeContextBuilder(
110
114
  config=context_config,
@@ -212,6 +216,18 @@ async def run_subagent(
212
216
 
213
217
  if not assistant_msg.tool_calls:
214
218
  text = extract_text(assistant_msg)
219
+ if has_successful_tool_work and not _satisfies_result_contract(text, result_contract):
220
+ if contract_retry_count < _RESULT_CONTRACT_RETRY_LIMIT:
221
+ contract_retry_count += 1
222
+ step -= 1
223
+ guidance = _result_contract_retry_message(result_contract)
224
+ messages.append(HumanMessage(content=guidance))
225
+ continue
226
+ if tracker:
227
+ tracker.update(task_id, last_output=text[:200])
228
+ tracker.finish(task_id, "completed")
229
+ mark_finished("contract_unsatisfied")
230
+ return text
215
231
  if tracker:
216
232
  tracker.update(task_id, last_output=text[:200])
217
233
  tracker.finish(task_id, "completed")
@@ -318,6 +334,7 @@ async def run_subagent(
318
334
  if metadata.get("runtime_guard"):
319
335
  continue
320
336
  if result_ok(item["result"]):
337
+ has_successful_tool_work = True
321
338
  guard_state.tool_failures.record_success(item["tool_call"])
322
339
  continue
323
340
  key = build_failure_key(item["tool_call"], item["result"])
@@ -391,3 +408,46 @@ def _task_payload(task_description: str, result_contract) -> str:
391
408
  "Return the final answer using this contract."
392
409
  )
393
410
  return "\n\n".join(parts)
411
+
412
+
413
+ def _result_contract_fields(result_contract) -> list[str]:
414
+ result_format = str(getattr(result_contract, "format", "") or "")
415
+ fields: list[str] = []
416
+ for raw_part in result_format.split(","):
417
+ part = raw_part.strip()
418
+ if not part:
419
+ continue
420
+ match = re.match(r"([A-Za-z_][A-Za-z0-9_]*)", part)
421
+ if match:
422
+ fields.append(match.group(1))
423
+ return fields
424
+
425
+
426
+ def _satisfies_result_contract(text: str, result_contract) -> bool:
427
+ fields = _result_contract_fields(result_contract)
428
+ if not fields:
429
+ return True
430
+ if not text.strip():
431
+ return False
432
+
433
+ matched = [
434
+ field
435
+ for field in fields
436
+ if re.search(rf"(?im)^\s*(?:[-*]\s*)?{re.escape(field)}\s*[:=]", text)
437
+ ]
438
+ required = 1 if len(fields) == 1 else 2
439
+ if fields[0] not in matched:
440
+ return False
441
+ return len(matched) >= required
442
+
443
+
444
+ def _result_contract_retry_message(result_contract) -> str:
445
+ schema_name = str(getattr(result_contract, "schema_name", "") or "agent_result")
446
+ result_format = str(getattr(result_contract, "format", "") or "").strip()
447
+ return (
448
+ "Your previous response did not satisfy the child-agent result contract. "
449
+ "Do not return raw tool output or code snippets as the final answer.\n"
450
+ "Summarize the completed delegated task using the required contract:\n"
451
+ f"- schema_name: {schema_name}\n"
452
+ f"- format: {result_format}"
453
+ )
@@ -116,6 +116,13 @@ BASE_SYSTEM = BaseSystemPrompt(
116
116
  PromptRule(detail="Assess before acting — evaluate what's already known and what's still needed."),
117
117
  PromptRule(detail="Stay aligned with the user's actual goal."),
118
118
  PromptRule(detail="Pick the smallest next action that makes progress toward the goal."),
119
+ PromptRule(
120
+ detail=(
121
+ "Delegate to child agents only for parallel independent tasks or when the user "
122
+ "explicitly asks. Do not delegate single-file reads, simple searches, or "
123
+ "straightforward tasks you can do directly."
124
+ ),
125
+ ),
119
126
  PromptRule(detail="skill can return project/global skill bodies for the current turn."),
120
127
  PromptRule(
121
128
  detail=(
@@ -38,6 +38,7 @@ BASIC_RULES: Ruleset = [
38
38
  Rule(permission="compact", pattern="*", action="allow"),
39
39
  Rule(permission="task_status", pattern="*", action="allow"),
40
40
  Rule(permission="skill", pattern="*", action="allow"),
41
+ Rule(permission="skill", pattern="create", action="ask"),
41
42
  Rule(permission="lsp", pattern="*", action="allow"),
42
43
  Rule(permission="agent", pattern="voidx", action="allow"),
43
44
  Rule(permission="edit", pattern="*", action="ask"),
@@ -118,6 +119,8 @@ def build_pattern(tool: str, args: dict) -> str:
118
119
  return "voidx"
119
120
  if tool == "git":
120
121
  return "read" if _is_read_only_git_tool_command(args) else "write"
122
+ if tool == "skill":
123
+ return "create" if args.get("op") == "create" else "*"
121
124
  return "*"
122
125
 
123
126
 
@@ -359,10 +362,14 @@ def _is_read_only_git_ref_command(subcommand: str, args: list[str]) -> bool:
359
362
  def capability_for_tool(tool: str, args: dict) -> PermissionCapability:
360
363
  if tool in {
361
364
  "read", "glob", "grep", "webfetch", "websearch", "todo", "task_status",
362
- "skill", "workflow", "compact",
365
+ "workflow", "compact",
363
366
  "lsp",
364
367
  }:
365
368
  return PermissionCapability.READ_TOOLS
369
+ if tool == "skill":
370
+ if args.get("op") == "create":
371
+ return PermissionCapability.FILE_WRITE
372
+ return PermissionCapability.READ_TOOLS
366
373
  if tool in {"file", "write", "replace"}:
367
374
  return PermissionCapability.FILE_WRITE
368
375
  if tool == "bash":
@@ -2,14 +2,16 @@
2
2
 
3
3
  from __future__ import annotations
4
4
 
5
+ import re
5
6
  from dataclasses import dataclass
6
7
  from pathlib import Path
7
- from typing import Any
8
+ from typing import Any, Literal
8
9
 
9
10
  from voidx.skills.schema import SkillDefinition, SkillMeta, SkillScope
10
11
 
11
12
  SKILL_FILENAME = "SKILL.md"
12
13
  DEFAULT_BUNDLED_DIR = Path(__file__).resolve().parent / "bundled"
14
+ SKILL_NAME_RE = re.compile(r"^(?=.{1,64}$)[a-z0-9]([a-z0-9-]*[a-z0-9])?$")
13
15
 
14
16
 
15
17
  @dataclass
@@ -72,6 +74,44 @@ class SkillRegistry:
72
74
  self._cache = None
73
75
  self._cache_signature = None
74
76
 
77
+ def create_skill(
78
+ self,
79
+ name: str,
80
+ description: str,
81
+ body: str,
82
+ *,
83
+ scope: Literal["project", "global"] = "project",
84
+ ) -> Path | None:
85
+ """Create a SKILL.md file. Returns the path, or None if it already exists.
86
+
87
+ Security: name is validated against SKILL_NAME_RE on the first line.
88
+ This is the sole path-escape defense since sandbox skips the check
89
+ (skill create has no file_path arg). Must not rely on tool-layer validation.
90
+ """
91
+ if not SKILL_NAME_RE.match(name):
92
+ raise ValueError(
93
+ f"Invalid skill name '{name}': must be 1-64 chars, lowercase "
94
+ f"alphanumeric with hyphens, not starting/ending with a hyphen."
95
+ )
96
+
97
+ root = self.project_dir if scope == "project" else self.global_dir
98
+ path = root / name / SKILL_FILENAME
99
+ if path.exists():
100
+ return None
101
+
102
+ path.parent.mkdir(parents=True, exist_ok=True)
103
+ frontmatter = (
104
+ "---\n"
105
+ f"name: {name}\n"
106
+ f"description: {description}\n"
107
+ "enabled: true\n"
108
+ "---\n\n"
109
+ )
110
+ path.write_text(frontmatter + body, encoding="utf-8")
111
+ self.invalidate()
112
+ return path
113
+
114
+
75
115
  def get(self, name: str) -> SkillDefinition | None:
76
116
  target = normalize_skill_name(name)
77
117
  for skill in self.discover():
@@ -127,15 +127,8 @@ _RESULT_PRESETS: dict[str, AgentResultContract] = {
127
127
  class AgentTool(BaseTool):
128
128
  id = "agent"
129
129
  description = (
130
- "Start an isolated child agent for a delegated task. Use ONLY when "
131
- "you need to run multiple independent tasks in parallel, or the user "
132
- "explicitly asks for a child agent. Do not use for single-file reads, "
133
- "simple searches, or straightforward tasks you can do directly. Each "
134
- "call must include mode, task, and one concrete target. Use "
135
- "success_criteria for implement and feedback modes, and result_preset "
136
- "when the child output shape should be explicit. "
137
- "The child agent receives your task description and runtime context, "
138
- "but not caller conversation history."
130
+ "Start an isolated child agent for a delegated task. The child receives "
131
+ "the task brief and runtime context, but not caller conversation history."
139
132
  )
140
133
 
141
134
  def __init__(
@@ -196,19 +189,21 @@ class AgentTool(BaseTool):
196
189
 
197
190
  if self._agent_resolver is None:
198
191
  return ToolResult(
199
- output=f"Child agent execution not available. Task: {normalized.description[:200]}"
192
+ output=f"Child agent execution not available. Task: {normalized.description[:200]}",
193
+ metadata={"error": True, "reason": "no_resolver"},
200
194
  )
201
195
 
202
196
  agent_def = self._agent_resolver(requested_agent) if self._agent_resolver else None
203
197
  if not agent_def:
204
198
  available = self._available_agents
205
- return ToolResult(output=f"Unknown child agent: {inp.agent}. Available: {available}")
199
+ return ToolResult(output=f"Unknown child agent: {inp.agent}. Available: {available}", metadata={"error": True, "reason": "unknown_agent"})
206
200
 
207
201
  agent_def_name = str(getattr(agent_def, "name", inp.agent))
208
202
 
209
203
  if not self._run_child_agent:
210
204
  return ToolResult(
211
- output=f"Child agent execution not available. Task: {normalized.description[:200]}"
205
+ output=f"Child agent execution not available. Task: {normalized.description[:200]}",
206
+ metadata={"error": True, "reason": "no_runner"},
212
207
  )
213
208
 
214
209
  try:
@@ -235,7 +230,7 @@ class AgentTool(BaseTool):
235
230
  except Exception as exc:
236
231
  return ToolResult(
237
232
  output=f"Child agent '{agent_def_name}' failed: {exc}",
238
- metadata={"agent": agent_def_name, "error": str(exc)},
233
+ metadata={"agent": agent_def_name, "error": True, "reason": "exception", "detail": str(exc)[:200]},
239
234
  )
240
235
 
241
236
 
@@ -81,11 +81,6 @@ def _strip_cd_prefix(command: str) -> str:
81
81
  return remainder
82
82
 
83
83
 
84
- _UNHINTABLE_GIT_SUBCOMMANDS = frozenset({
85
- "push", "pull", "merge", "rebase", "cherry-pick",
86
- "reset", "checkout", "fetch", "clone", "init",
87
- "submodule", "filter-branch", "bisect",
88
- })
89
84
 
90
85
  _GIT_GLOBAL_OPTIONS_WITH_VALUE = frozenset({
91
86
  "-C", "-c", "--git-dir", "--work-tree", "--namespace", "--exec-path",
@@ -132,7 +132,7 @@ def _hint_write_echo(stripped: str, words: list[str]) -> RouteHint | None:
132
132
  )
133
133
  return RouteHint(
134
134
  tool_id="file", ui_label="→ file",
135
- llm_hint=f'Prefer file(file_path="{path}", op="create") then line(file_path="{path}", op="append", new_string="{content}") for file tracking and diff output.',
135
+ llm_hint=f'Prefer file(file_path="{path}", op="create") then write(file_path="{path}", op="append", new_string="{content}") for file tracking and diff output.',
136
136
  )
137
137
 
138
138
 
@@ -180,7 +180,7 @@ def _hint_write_heredoc(stripped: str) -> RouteHint | None:
180
180
  )
181
181
  return RouteHint(
182
182
  tool_id="file", ui_label="→ file",
183
- llm_hint=f'Prefer file(file_path="{path}", op="create") then line(file_path="{path}", op="append", new_string="{content}") for file tracking and diff output.',
183
+ llm_hint=f'Prefer file(file_path="{path}", op="create") then write(file_path="{path}", op="append", new_string="{content}") for file tracking and diff output.',
184
184
  )
185
185
 
186
186
 
@@ -5,20 +5,16 @@ from __future__ import annotations
5
5
  from voidx.tools.bash.core import (
6
6
  RouteHint,
7
7
  _git_subcommand,
8
- _UNHINTABLE_GIT_SUBCOMMANDS,
9
8
  )
10
9
 
11
10
 
12
11
  def _hint_git(stripped: str, words: list[str]) -> RouteHint | None:
13
- subcommand, rest = _git_subcommand(words)
12
+ subcommand, _ = _git_subcommand(words)
14
13
  if not subcommand:
15
14
  return None
16
- if subcommand in _UNHINTABLE_GIT_SUBCOMMANDS:
17
- return None
18
15
  git_args = stripped[len("git"):].strip()
19
- if '"' in git_args:
20
- return None
16
+ llm_hint = f"Prefer git tool with args={git_args!r} for structured output."
21
17
  return RouteHint(
22
18
  tool_id="git", ui_label="→ git",
23
- llm_hint=f'Prefer git tool with args="{git_args}" for structured output.',
24
- )
19
+ llm_hint=llm_hint,
20
+ )
@@ -233,16 +233,16 @@ def _hint_sed(words: list[str]) -> RouteHint | None:
233
233
  line_no = int(line_prefix)
234
234
  return RouteHint(
235
235
  tool_id="replace", ui_label="→ replace",
236
- llm_hint=f'Prefer replace(file_path="{path}", start_no={line_no}, end_no={line_no}, prefix="{old_text}", suffix="{old_text}", new_string="{new_text}") — prefix/suffix are line content anchors for locating the edit, new_string is the replacement. Enables staleness checking and diff output.',
236
+ llm_hint=f'Prefer replace(file_path="{path}", start_no={line_no}, end_no={line_no}, start_anchor="{old_text}", end_anchor="{old_text}", new_string="{new_text}").',
237
237
  )
238
238
  if is_global:
239
239
  return RouteHint(
240
240
  tool_id="replace", ui_label="→ replace",
241
- llm_hint=f'For global substitution: first read {path} to locate lines, then use replace(file_path, start_no, end_no, prefix="{old_text}", suffix="{old_text}", new_string="{new_text}") — prefix/suffix are line content anchors for locating the edit.',
241
+ llm_hint=f'For global substitution: first read {path} to locate lines, then use replace(file_path, start_no, end_no, start_anchor="{old_text}", end_anchor="{old_text}", new_string="{new_text}").',
242
242
  )
243
243
  return RouteHint(
244
244
  tool_id="replace", ui_label="→ replace",
245
- llm_hint=f'Prefer replace(file_path="{path}", start_no, end_no, prefix="{old_text}", suffix="{old_text}", new_string="{new_text}") — prefix/suffix are line content anchors for locating the edit, new_string is the replacement. Enables staleness checking and diff output.',
245
+ llm_hint=f'Prefer replace(file_path="{path}", start_no, end_no, start_anchor="{old_text}", end_anchor="{old_text}", new_string="{new_text}").',
246
246
  )
247
247
 
248
248
  m = _SED_RANGE_DELETE.match(script)
@@ -250,7 +250,7 @@ def _hint_sed(words: list[str]) -> RouteHint | None:
250
250
  start, end = int(m.group(1)), int(m.group(2))
251
251
  return RouteHint(
252
252
  tool_id="replace", ui_label="→ replace",
253
- llm_hint=f'For line range deletion: first read {path} to see lines {start}-{end}, then use replace(file_path="{path}", start_no={start}, end_no={end}, prefix="...", suffix="...", new_string="").',
253
+ llm_hint=f'For line range deletion: first read {path} to see lines {start}-{end}, then use replace(file_path="{path}", start_no={start}, end_no={end}, start_anchor="...", end_anchor="...", new_string="").',
254
254
  )
255
255
 
256
256
  m = _SED_LINE_DELETE.match(script)
@@ -258,14 +258,14 @@ def _hint_sed(words: list[str]) -> RouteHint | None:
258
258
  line_no = int(m.group(1))
259
259
  return RouteHint(
260
260
  tool_id="replace", ui_label="→ replace",
261
- llm_hint=f'For single line deletion: first read {path} to see line {line_no}, then use replace(file_path="{path}", start_no={line_no}, end_no={line_no}, prefix="...", suffix="...", new_string="").',
261
+ llm_hint=f'For single line deletion: first read {path} to see line {line_no}, then use replace(file_path="{path}", start_no={line_no}, end_no={line_no}, start_anchor="...", end_anchor="...", new_string="").',
262
262
  )
263
263
  m = _SED_PATTERN_DELETE.match(script)
264
264
  if m:
265
265
  pat = m.group(1)
266
266
  return RouteHint(
267
267
  tool_id="replace", ui_label="→ replace",
268
- llm_hint=f'For pattern-based deletion: first grep "{pat}" {path} to locate lines, then use replace(..., new_string="").',
268
+ llm_hint=f'For pattern-based deletion: first grep "{pat}" {path} to locate matching lines, then use replace(file_path="{path}", start_no, end_no, start_anchor, end_anchor, new_string="") with the matched line numbers and content.',
269
269
  )
270
270
 
271
271
  return None
@@ -27,7 +27,10 @@ class BashTool(BaseTool):
27
27
  return model_to_json_schema(BashInput)
28
28
 
29
29
  async def execute(self, args: dict, ctx: ToolContext) -> ToolResult:
30
- inp = BashInput.model_validate(args)
30
+ try:
31
+ inp = BashInput.model_validate(args)
32
+ except Exception as exc:
33
+ return ToolResult(output=f"Invalid arguments: {exc}", metadata={"error": True})
31
34
 
32
35
  blocked = _check_command(inp.command)
33
36
  if blocked:
@@ -35,7 +38,7 @@ class BashTool(BaseTool):
35
38
  return ToolResult(
36
39
  output=json.dumps(payload, ensure_ascii=False),
37
40
  display=blocked,
38
- metadata={"command": inp.command, "blocked": True},
41
+ metadata={"command": inp.command, "blocked": True, "error": True},
39
42
  )
40
43
 
41
44
  blocked = _sandbox_denial(inp.command, ctx)
@@ -44,7 +47,7 @@ class BashTool(BaseTool):
44
47
  return ToolResult(
45
48
  output=json.dumps(payload, ensure_ascii=False),
46
49
  display=blocked,
47
- metadata={"command": inp.command, "blocked": True},
50
+ metadata={"command": inp.command, "blocked": True, "error": True},
48
51
  )
49
52
 
50
53
  hint = try_hint(inp.command)
@@ -83,7 +86,7 @@ class BashTool(BaseTool):
83
86
  return ToolResult(
84
87
  output=json.dumps(payload, ensure_ascii=False),
85
88
  display=display,
86
- metadata={"command": inp.command, "exit_code": -1, "timeout": True},
89
+ metadata={"command": inp.command, "exit_code": -1, "timeout": True, "error": True},
87
90
  )
88
91
 
89
92
  stdout_text = stdout.decode("utf-8", errors="replace") if stdout else ""
@@ -117,6 +120,7 @@ class BashTool(BaseTool):
117
120
  "command": inp.command,
118
121
  "exit_code": exit_code,
119
122
  "ok": exit_code == 0,
123
+ **({"error": True} if exit_code != 0 else {}),
120
124
  },
121
125
  )
122
126
 
@@ -45,7 +45,10 @@ class ClarifyTool(BaseTool):
45
45
  return model_to_json_schema(ClarifyInput)
46
46
 
47
47
  async def execute(self, args: dict, ctx: ToolContext) -> ToolResult:
48
- inp = ClarifyInput.model_validate(args)
48
+ try:
49
+ inp = ClarifyInput.model_validate(args)
50
+ except Exception as exc:
51
+ return ToolResult(output=f"Invalid arguments: {exc}", metadata={"error": True})
49
52
  if ctx.interact is None:
50
53
  return ToolResult(
51
54
  title="clarify: unavailable",
@@ -37,7 +37,10 @@ class CompactContextTool(BaseTool):
37
37
  return model_to_json_schema(CompactContextInput)
38
38
 
39
39
  async def execute(self, args: dict, ctx: ToolContext) -> ToolResult:
40
- inp = CompactContextInput.model_validate(args)
40
+ try:
41
+ inp = CompactContextInput.model_validate(args)
42
+ except Exception as exc:
43
+ return ToolResult(output=f"Invalid arguments: {exc}", metadata={"error": True})
41
44
  summary = inp.summary.strip()
42
45
  tail_anchor_id = inp.tail_anchor_id.strip()
43
46
  return ToolResult(
@@ -25,7 +25,7 @@ from .types import DisplayLines, ResolvedEdit
25
25
 
26
26
 
27
27
  class FileReplaceInput(BaseModel):
28
- file_path: str = Field(description="Path to edit")
28
+ file_path: str = Field(description="Absolute or relative path to the file")
29
29
  start_no: int = Field(
30
30
  ge=1,
31
31
  description=(
@@ -40,26 +40,22 @@ class FileReplaceInput(BaseModel):
40
40
  "Use the line number from the latest read output."
41
41
  ),
42
42
  )
43
- prefix: str = Field(
43
+ start_anchor: str = Field(
44
44
  description=(
45
- "Substring expected anywhere on the first line to replace. "
46
- "Use an empty string only when the first line is empty. "
47
- "Aim for a distinctive snippet."
45
+ "Content anchor on the first line to replace — a substring "
46
+ "expected anywhere on that line. Use an empty string only "
47
+ "when the first line is empty. Aim for a distinctive snippet."
48
48
  ),
49
49
  )
50
- suffix: str = Field(
50
+ end_anchor: str = Field(
51
51
  description=(
52
- "Substring expected anywhere on the last line to replace. "
53
- "Use an empty string only when the last line is empty. "
54
- "Aim for a distinctive snippet."
52
+ "Content anchor on the last line to replace — a substring "
53
+ "expected anywhere on that line. Use an empty string only "
54
+ "when the last line is empty. Aim for a distinctive snippet."
55
55
  ),
56
56
  )
57
57
  new_string: str = Field(
58
- description=(
59
- "Replacement content. May contain any number of lines. "
60
- "A trailing newline does not add an extra blank line; "
61
- "start with a newline only when an intentional blank first line is desired."
62
- ),
58
+ description="Replacement content. May contain any number of lines.",
63
59
  )
64
60
 
65
61
  @model_validator(mode="after")
@@ -74,7 +70,7 @@ class FileReplaceTool(BaseTool):
74
70
  description = (
75
71
  "Replace whole lines in a file. "
76
72
  "Provide the exact start_no/end_no from the latest read output, "
77
- "plus prefix/suffix substrings from the first and last lines. "
73
+ "plus start_anchor/end_anchor substrings from the first and last lines. "
78
74
  "Read the target lines first."
79
75
  )
80
76
 
@@ -82,14 +78,17 @@ class FileReplaceTool(BaseTool):
82
78
  return model_to_json_schema(FileReplaceInput)
83
79
 
84
80
  async def execute(self, args: dict, ctx: ToolContext) -> ToolResult:
85
- inp = FileReplaceInput.model_validate(args)
81
+ try:
82
+ inp = FileReplaceInput.model_validate(args)
83
+ except Exception as exc:
84
+ return ToolResult(output=f"Invalid arguments: {exc}", metadata={"error": True})
86
85
  return await _execute_text_replace(
87
86
  ctx,
88
87
  file_path=inp.file_path,
89
88
  start_no=inp.start_no,
90
89
  end_no=inp.end_no,
91
- prefix=inp.prefix,
92
- suffix=inp.suffix,
90
+ start_anchor=inp.start_anchor,
91
+ end_anchor=inp.end_anchor,
93
92
  new_string=inp.new_string,
94
93
  tool_name=self.id,
95
94
  )
@@ -114,8 +113,8 @@ async def _execute_text_replace(
114
113
  file_path: str,
115
114
  start_no: int,
116
115
  end_no: int,
117
- prefix: str,
118
- suffix: str,
116
+ start_anchor: str,
117
+ end_anchor: str,
119
118
  new_string: str,
120
119
  tool_name: str,
121
120
  ) -> ToolResult:
@@ -126,12 +125,12 @@ async def _execute_text_replace(
126
125
 
127
126
  original = path.read_text(encoding="utf-8", errors="replace")
128
127
  display = _split_display_lines(original)
129
- match = _find_text_segment(display.lines, start_no, end_no, prefix, suffix)
128
+ match = _find_text_segment(display.lines, start_no, end_no, start_anchor, end_anchor)
130
129
  if isinstance(match, str):
131
130
  return ToolResult(output=match, metadata={"error": True})
132
131
 
133
132
  _, _, start_line, end_line = match
134
- coverage_error = check_read_coverage(ctx, path, start_line, end_line)
133
+ coverage_error = check_read_coverage(ctx, path, start_line, end_line, display_path=file_path)
135
134
  if coverage_error:
136
135
  return ToolResult(output=f"Edit 0: {coverage_error}", metadata={"error": True})
137
136
 
@@ -222,7 +221,7 @@ async def _apply_resolved_edits(
222
221
  continue
223
222
  if edit.operation == "insert" and edit.start_line == total_lines and edit.end_line == total_lines:
224
223
  continue
225
- coverage_error = check_read_coverage(ctx, path, edit.start_line, edit.end_line)
224
+ coverage_error = check_read_coverage(ctx, path, edit.start_line, edit.end_line, display_path=file_path)
226
225
  if coverage_error:
227
226
  return ToolResult(output=f"Edit {i}: {coverage_error}", metadata={"error": True})
228
227