kiwimatecoder 0.1.0__tar.gz → 0.2.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 (193) hide show
  1. {kiwimatecoder-0.1.0 → kiwimatecoder-0.2.0}/PKG-INFO +36 -1
  2. {kiwimatecoder-0.1.0 → kiwimatecoder-0.2.0}/README.md +35 -0
  3. {kiwimatecoder-0.1.0 → kiwimatecoder-0.2.0}/kiwimatecoder/agent.py +19 -5
  4. {kiwimatecoder-0.1.0 → kiwimatecoder-0.2.0}/kiwimatecoder/commands.py +108 -1
  5. kiwimatecoder-0.2.0/kiwimatecoder/goal.py +229 -0
  6. {kiwimatecoder-0.1.0 → kiwimatecoder-0.2.0}/kiwimatecoder/repl.py +47 -0
  7. {kiwimatecoder-0.1.0 → kiwimatecoder-0.2.0}/kiwimatecoder/session.py +5 -0
  8. {kiwimatecoder-0.1.0 → kiwimatecoder-0.2.0}/kiwimatecoder/tools/__init__.py +2 -0
  9. kiwimatecoder-0.2.0/kiwimatecoder/tools/goal.py +69 -0
  10. {kiwimatecoder-0.1.0 → kiwimatecoder-0.2.0}/kiwimatecoder.egg-info/PKG-INFO +36 -1
  11. {kiwimatecoder-0.1.0 → kiwimatecoder-0.2.0}/kiwimatecoder.egg-info/SOURCES.txt +3 -0
  12. {kiwimatecoder-0.1.0 → kiwimatecoder-0.2.0}/pyproject.toml +1 -1
  13. {kiwimatecoder-0.1.0 → kiwimatecoder-0.2.0}/tests/test_config_markup.py +3 -1
  14. kiwimatecoder-0.2.0/tests/test_goal.py +828 -0
  15. {kiwimatecoder-0.1.0 → kiwimatecoder-0.2.0}/tests/test_slash_markup.py +7 -3
  16. {kiwimatecoder-0.1.0 → kiwimatecoder-0.2.0}/tests/test_tool_registry.py +1 -0
  17. {kiwimatecoder-0.1.0 → kiwimatecoder-0.2.0}/LICENSE +0 -0
  18. {kiwimatecoder-0.1.0 → kiwimatecoder-0.2.0}/kiwimatecoder/__init__.py +0 -0
  19. {kiwimatecoder-0.1.0 → kiwimatecoder-0.2.0}/kiwimatecoder/__main__.py +0 -0
  20. {kiwimatecoder-0.1.0 → kiwimatecoder-0.2.0}/kiwimatecoder/acp/__init__.py +0 -0
  21. {kiwimatecoder-0.1.0 → kiwimatecoder-0.2.0}/kiwimatecoder/acp/protocol.py +0 -0
  22. {kiwimatecoder-0.1.0 → kiwimatecoder-0.2.0}/kiwimatecoder/acp/server.py +0 -0
  23. {kiwimatecoder-0.1.0 → kiwimatecoder-0.2.0}/kiwimatecoder/acp/stdio.py +0 -0
  24. {kiwimatecoder-0.1.0 → kiwimatecoder-0.2.0}/kiwimatecoder/ai.py +0 -0
  25. {kiwimatecoder-0.1.0 → kiwimatecoder-0.2.0}/kiwimatecoder/audit.py +0 -0
  26. {kiwimatecoder-0.1.0 → kiwimatecoder-0.2.0}/kiwimatecoder/browser.py +0 -0
  27. {kiwimatecoder-0.1.0 → kiwimatecoder-0.2.0}/kiwimatecoder/catalog.py +0 -0
  28. {kiwimatecoder-0.1.0 → kiwimatecoder-0.2.0}/kiwimatecoder/checkpoints.py +0 -0
  29. {kiwimatecoder-0.1.0 → kiwimatecoder-0.2.0}/kiwimatecoder/client.py +0 -0
  30. {kiwimatecoder-0.1.0 → kiwimatecoder-0.2.0}/kiwimatecoder/clipboard.py +0 -0
  31. {kiwimatecoder-0.1.0 → kiwimatecoder-0.2.0}/kiwimatecoder/config.py +0 -0
  32. {kiwimatecoder-0.1.0 → kiwimatecoder-0.2.0}/kiwimatecoder/diagnostics.py +0 -0
  33. {kiwimatecoder-0.1.0 → kiwimatecoder-0.2.0}/kiwimatecoder/documents.py +0 -0
  34. {kiwimatecoder-0.1.0 → kiwimatecoder-0.2.0}/kiwimatecoder/evals/__init__.py +0 -0
  35. {kiwimatecoder-0.1.0 → kiwimatecoder-0.2.0}/kiwimatecoder/evals/cases.py +0 -0
  36. {kiwimatecoder-0.1.0 → kiwimatecoder-0.2.0}/kiwimatecoder/evals/render.py +0 -0
  37. {kiwimatecoder-0.1.0 → kiwimatecoder-0.2.0}/kiwimatecoder/evals/runner.py +0 -0
  38. {kiwimatecoder-0.1.0 → kiwimatecoder-0.2.0}/kiwimatecoder/events.py +0 -0
  39. {kiwimatecoder-0.1.0 → kiwimatecoder-0.2.0}/kiwimatecoder/forge.py +0 -0
  40. {kiwimatecoder-0.1.0 → kiwimatecoder-0.2.0}/kiwimatecoder/git.py +0 -0
  41. {kiwimatecoder-0.1.0 → kiwimatecoder-0.2.0}/kiwimatecoder/headless.py +0 -0
  42. {kiwimatecoder-0.1.0 → kiwimatecoder-0.2.0}/kiwimatecoder/hooks.py +0 -0
  43. {kiwimatecoder-0.1.0 → kiwimatecoder-0.2.0}/kiwimatecoder/hunks.py +0 -0
  44. {kiwimatecoder-0.1.0 → kiwimatecoder-0.2.0}/kiwimatecoder/i18n.py +0 -0
  45. {kiwimatecoder-0.1.0 → kiwimatecoder-0.2.0}/kiwimatecoder/images.py +0 -0
  46. {kiwimatecoder-0.1.0 → kiwimatecoder-0.2.0}/kiwimatecoder/index/__init__.py +0 -0
  47. {kiwimatecoder-0.1.0 → kiwimatecoder-0.2.0}/kiwimatecoder/index/builder.py +0 -0
  48. {kiwimatecoder-0.1.0 → kiwimatecoder-0.2.0}/kiwimatecoder/index/embeddings.py +0 -0
  49. {kiwimatecoder-0.1.0 → kiwimatecoder-0.2.0}/kiwimatecoder/index/search.py +0 -0
  50. {kiwimatecoder-0.1.0 → kiwimatecoder-0.2.0}/kiwimatecoder/index/store.py +0 -0
  51. {kiwimatecoder-0.1.0 → kiwimatecoder-0.2.0}/kiwimatecoder/instructions.py +0 -0
  52. {kiwimatecoder-0.1.0 → kiwimatecoder-0.2.0}/kiwimatecoder/jobs.py +0 -0
  53. {kiwimatecoder-0.1.0 → kiwimatecoder-0.2.0}/kiwimatecoder/lsp/__init__.py +0 -0
  54. {kiwimatecoder-0.1.0 → kiwimatecoder-0.2.0}/kiwimatecoder/lsp/client.py +0 -0
  55. {kiwimatecoder-0.1.0 → kiwimatecoder-0.2.0}/kiwimatecoder/lsp/manager.py +0 -0
  56. {kiwimatecoder-0.1.0 → kiwimatecoder-0.2.0}/kiwimatecoder/lsp/protocol.py +0 -0
  57. {kiwimatecoder-0.1.0 → kiwimatecoder-0.2.0}/kiwimatecoder/main.py +0 -0
  58. {kiwimatecoder-0.1.0 → kiwimatecoder-0.2.0}/kiwimatecoder/mcp/__init__.py +0 -0
  59. {kiwimatecoder-0.1.0 → kiwimatecoder-0.2.0}/kiwimatecoder/mcp/client.py +0 -0
  60. {kiwimatecoder-0.1.0 → kiwimatecoder-0.2.0}/kiwimatecoder/mcp/errors.py +0 -0
  61. {kiwimatecoder-0.1.0 → kiwimatecoder-0.2.0}/kiwimatecoder/mcp/manager.py +0 -0
  62. {kiwimatecoder-0.1.0 → kiwimatecoder-0.2.0}/kiwimatecoder/mcp/protocol.py +0 -0
  63. {kiwimatecoder-0.1.0 → kiwimatecoder-0.2.0}/kiwimatecoder/media.py +0 -0
  64. {kiwimatecoder-0.1.0 → kiwimatecoder-0.2.0}/kiwimatecoder/memory.py +0 -0
  65. {kiwimatecoder-0.1.0 → kiwimatecoder-0.2.0}/kiwimatecoder/network.py +0 -0
  66. {kiwimatecoder-0.1.0 → kiwimatecoder-0.2.0}/kiwimatecoder/notify.py +0 -0
  67. {kiwimatecoder-0.1.0 → kiwimatecoder-0.2.0}/kiwimatecoder/permissions.py +0 -0
  68. {kiwimatecoder-0.1.0 → kiwimatecoder-0.2.0}/kiwimatecoder/pickers.py +0 -0
  69. {kiwimatecoder-0.1.0 → kiwimatecoder-0.2.0}/kiwimatecoder/plugins.py +0 -0
  70. {kiwimatecoder-0.1.0 → kiwimatecoder-0.2.0}/kiwimatecoder/pricing.py +0 -0
  71. {kiwimatecoder-0.1.0 → kiwimatecoder-0.2.0}/kiwimatecoder/prompts.py +0 -0
  72. {kiwimatecoder-0.1.0 → kiwimatecoder-0.2.0}/kiwimatecoder/providers.py +0 -0
  73. {kiwimatecoder-0.1.0 → kiwimatecoder-0.2.0}/kiwimatecoder/redaction.py +0 -0
  74. {kiwimatecoder-0.1.0 → kiwimatecoder-0.2.0}/kiwimatecoder/remote.py +0 -0
  75. {kiwimatecoder-0.1.0 → kiwimatecoder-0.2.0}/kiwimatecoder/routing.py +0 -0
  76. {kiwimatecoder-0.1.0 → kiwimatecoder-0.2.0}/kiwimatecoder/sandbox.py +0 -0
  77. {kiwimatecoder-0.1.0 → kiwimatecoder-0.2.0}/kiwimatecoder/sdk.py +0 -0
  78. {kiwimatecoder-0.1.0 → kiwimatecoder-0.2.0}/kiwimatecoder/share.py +0 -0
  79. {kiwimatecoder-0.1.0 → kiwimatecoder-0.2.0}/kiwimatecoder/shell.py +0 -0
  80. {kiwimatecoder-0.1.0 → kiwimatecoder-0.2.0}/kiwimatecoder/skills.py +0 -0
  81. {kiwimatecoder-0.1.0 → kiwimatecoder-0.2.0}/kiwimatecoder/sync.py +0 -0
  82. {kiwimatecoder-0.1.0 → kiwimatecoder-0.2.0}/kiwimatecoder/team.py +0 -0
  83. {kiwimatecoder-0.1.0 → kiwimatecoder-0.2.0}/kiwimatecoder/telemetry.py +0 -0
  84. {kiwimatecoder-0.1.0 → kiwimatecoder-0.2.0}/kiwimatecoder/templates.py +0 -0
  85. {kiwimatecoder-0.1.0 → kiwimatecoder-0.2.0}/kiwimatecoder/tools/ask.py +0 -0
  86. {kiwimatecoder-0.1.0 → kiwimatecoder-0.2.0}/kiwimatecoder/tools/base.py +0 -0
  87. {kiwimatecoder-0.1.0 → kiwimatecoder-0.2.0}/kiwimatecoder/tools/browser.py +0 -0
  88. {kiwimatecoder-0.1.0 → kiwimatecoder-0.2.0}/kiwimatecoder/tools/edit_file.py +0 -0
  89. {kiwimatecoder-0.1.0 → kiwimatecoder-0.2.0}/kiwimatecoder/tools/forge.py +0 -0
  90. {kiwimatecoder-0.1.0 → kiwimatecoder-0.2.0}/kiwimatecoder/tools/git.py +0 -0
  91. {kiwimatecoder-0.1.0 → kiwimatecoder-0.2.0}/kiwimatecoder/tools/image.py +0 -0
  92. {kiwimatecoder-0.1.0 → kiwimatecoder-0.2.0}/kiwimatecoder/tools/io.py +0 -0
  93. {kiwimatecoder-0.1.0 → kiwimatecoder-0.2.0}/kiwimatecoder/tools/list_dir.py +0 -0
  94. {kiwimatecoder-0.1.0 → kiwimatecoder-0.2.0}/kiwimatecoder/tools/lsp.py +0 -0
  95. {kiwimatecoder-0.1.0 → kiwimatecoder-0.2.0}/kiwimatecoder/tools/memory.py +0 -0
  96. {kiwimatecoder-0.1.0 → kiwimatecoder-0.2.0}/kiwimatecoder/tools/paths.py +0 -0
  97. {kiwimatecoder-0.1.0 → kiwimatecoder-0.2.0}/kiwimatecoder/tools/read_file.py +0 -0
  98. {kiwimatecoder-0.1.0 → kiwimatecoder-0.2.0}/kiwimatecoder/tools/registry.py +0 -0
  99. {kiwimatecoder-0.1.0 → kiwimatecoder-0.2.0}/kiwimatecoder/tools/run_bash.py +0 -0
  100. {kiwimatecoder-0.1.0 → kiwimatecoder-0.2.0}/kiwimatecoder/tools/search.py +0 -0
  101. {kiwimatecoder-0.1.0 → kiwimatecoder-0.2.0}/kiwimatecoder/tools/selection.py +0 -0
  102. {kiwimatecoder-0.1.0 → kiwimatecoder-0.2.0}/kiwimatecoder/tools/shell.py +0 -0
  103. {kiwimatecoder-0.1.0 → kiwimatecoder-0.2.0}/kiwimatecoder/tools/skill.py +0 -0
  104. {kiwimatecoder-0.1.0 → kiwimatecoder-0.2.0}/kiwimatecoder/tools/task.py +0 -0
  105. {kiwimatecoder-0.1.0 → kiwimatecoder-0.2.0}/kiwimatecoder/tools/todo.py +0 -0
  106. {kiwimatecoder-0.1.0 → kiwimatecoder-0.2.0}/kiwimatecoder/tools/web.py +0 -0
  107. {kiwimatecoder-0.1.0 → kiwimatecoder-0.2.0}/kiwimatecoder/tools/write_file.py +0 -0
  108. {kiwimatecoder-0.1.0 → kiwimatecoder-0.2.0}/kiwimatecoder/ui.py +0 -0
  109. {kiwimatecoder-0.1.0 → kiwimatecoder-0.2.0}/kiwimatecoder/updater.py +0 -0
  110. {kiwimatecoder-0.1.0 → kiwimatecoder-0.2.0}/kiwimatecoder/web.py +0 -0
  111. {kiwimatecoder-0.1.0 → kiwimatecoder-0.2.0}/kiwimatecoder.egg-info/dependency_links.txt +0 -0
  112. {kiwimatecoder-0.1.0 → kiwimatecoder-0.2.0}/kiwimatecoder.egg-info/entry_points.txt +0 -0
  113. {kiwimatecoder-0.1.0 → kiwimatecoder-0.2.0}/kiwimatecoder.egg-info/requires.txt +0 -0
  114. {kiwimatecoder-0.1.0 → kiwimatecoder-0.2.0}/kiwimatecoder.egg-info/top_level.txt +0 -0
  115. {kiwimatecoder-0.1.0 → kiwimatecoder-0.2.0}/setup.cfg +0 -0
  116. {kiwimatecoder-0.1.0 → kiwimatecoder-0.2.0}/tests/test_accessibility.py +0 -0
  117. {kiwimatecoder-0.1.0 → kiwimatecoder-0.2.0}/tests/test_acp_protocol.py +0 -0
  118. {kiwimatecoder-0.1.0 → kiwimatecoder-0.2.0}/tests/test_acp_server.py +0 -0
  119. {kiwimatecoder-0.1.0 → kiwimatecoder-0.2.0}/tests/test_agent.py +0 -0
  120. {kiwimatecoder-0.1.0 → kiwimatecoder-0.2.0}/tests/test_ai.py +0 -0
  121. {kiwimatecoder-0.1.0 → kiwimatecoder-0.2.0}/tests/test_anthropic.py +0 -0
  122. {kiwimatecoder-0.1.0 → kiwimatecoder-0.2.0}/tests/test_audit.py +0 -0
  123. {kiwimatecoder-0.1.0 → kiwimatecoder-0.2.0}/tests/test_browser.py +0 -0
  124. {kiwimatecoder-0.1.0 → kiwimatecoder-0.2.0}/tests/test_catalog.py +0 -0
  125. {kiwimatecoder-0.1.0 → kiwimatecoder-0.2.0}/tests/test_checkpoints.py +0 -0
  126. {kiwimatecoder-0.1.0 → kiwimatecoder-0.2.0}/tests/test_ci_assets.py +0 -0
  127. {kiwimatecoder-0.1.0 → kiwimatecoder-0.2.0}/tests/test_cli_interfaces.py +0 -0
  128. {kiwimatecoder-0.1.0 → kiwimatecoder-0.2.0}/tests/test_cli_markup.py +0 -0
  129. {kiwimatecoder-0.1.0 → kiwimatecoder-0.2.0}/tests/test_client.py +0 -0
  130. {kiwimatecoder-0.1.0 → kiwimatecoder-0.2.0}/tests/test_clipboard.py +0 -0
  131. {kiwimatecoder-0.1.0 → kiwimatecoder-0.2.0}/tests/test_commands.py +0 -0
  132. {kiwimatecoder-0.1.0 → kiwimatecoder-0.2.0}/tests/test_config.py +0 -0
  133. {kiwimatecoder-0.1.0 → kiwimatecoder-0.2.0}/tests/test_config_menu.py +0 -0
  134. {kiwimatecoder-0.1.0 → kiwimatecoder-0.2.0}/tests/test_config_nonfinite.py +0 -0
  135. {kiwimatecoder-0.1.0 → kiwimatecoder-0.2.0}/tests/test_config_shapes.py +0 -0
  136. {kiwimatecoder-0.1.0 → kiwimatecoder-0.2.0}/tests/test_config_v2.py +0 -0
  137. {kiwimatecoder-0.1.0 → kiwimatecoder-0.2.0}/tests/test_diagnostics.py +0 -0
  138. {kiwimatecoder-0.1.0 → kiwimatecoder-0.2.0}/tests/test_documents.py +0 -0
  139. {kiwimatecoder-0.1.0 → kiwimatecoder-0.2.0}/tests/test_edit.py +0 -0
  140. {kiwimatecoder-0.1.0 → kiwimatecoder-0.2.0}/tests/test_evals.py +0 -0
  141. {kiwimatecoder-0.1.0 → kiwimatecoder-0.2.0}/tests/test_events.py +0 -0
  142. {kiwimatecoder-0.1.0 → kiwimatecoder-0.2.0}/tests/test_forge.py +0 -0
  143. {kiwimatecoder-0.1.0 → kiwimatecoder-0.2.0}/tests/test_git.py +0 -0
  144. {kiwimatecoder-0.1.0 → kiwimatecoder-0.2.0}/tests/test_headless.py +0 -0
  145. {kiwimatecoder-0.1.0 → kiwimatecoder-0.2.0}/tests/test_hooks.py +0 -0
  146. {kiwimatecoder-0.1.0 → kiwimatecoder-0.2.0}/tests/test_hunks.py +0 -0
  147. {kiwimatecoder-0.1.0 → kiwimatecoder-0.2.0}/tests/test_i18n.py +0 -0
  148. {kiwimatecoder-0.1.0 → kiwimatecoder-0.2.0}/tests/test_images.py +0 -0
  149. {kiwimatecoder-0.1.0 → kiwimatecoder-0.2.0}/tests/test_index.py +0 -0
  150. {kiwimatecoder-0.1.0 → kiwimatecoder-0.2.0}/tests/test_instructions.py +0 -0
  151. {kiwimatecoder-0.1.0 → kiwimatecoder-0.2.0}/tests/test_io_tools.py +0 -0
  152. {kiwimatecoder-0.1.0 → kiwimatecoder-0.2.0}/tests/test_jobs.py +0 -0
  153. {kiwimatecoder-0.1.0 → kiwimatecoder-0.2.0}/tests/test_lsp.py +0 -0
  154. {kiwimatecoder-0.1.0 → kiwimatecoder-0.2.0}/tests/test_lsp_protocol.py +0 -0
  155. {kiwimatecoder-0.1.0 → kiwimatecoder-0.2.0}/tests/test_main.py +0 -0
  156. {kiwimatecoder-0.1.0 → kiwimatecoder-0.2.0}/tests/test_markup_literals.py +0 -0
  157. {kiwimatecoder-0.1.0 → kiwimatecoder-0.2.0}/tests/test_mcp.py +0 -0
  158. {kiwimatecoder-0.1.0 → kiwimatecoder-0.2.0}/tests/test_media.py +0 -0
  159. {kiwimatecoder-0.1.0 → kiwimatecoder-0.2.0}/tests/test_media_models.py +0 -0
  160. {kiwimatecoder-0.1.0 → kiwimatecoder-0.2.0}/tests/test_media_video.py +0 -0
  161. {kiwimatecoder-0.1.0 → kiwimatecoder-0.2.0}/tests/test_memory.py +0 -0
  162. {kiwimatecoder-0.1.0 → kiwimatecoder-0.2.0}/tests/test_network.py +0 -0
  163. {kiwimatecoder-0.1.0 → kiwimatecoder-0.2.0}/tests/test_notify.py +0 -0
  164. {kiwimatecoder-0.1.0 → kiwimatecoder-0.2.0}/tests/test_output_markup.py +0 -0
  165. {kiwimatecoder-0.1.0 → kiwimatecoder-0.2.0}/tests/test_packaging_assets.py +0 -0
  166. {kiwimatecoder-0.1.0 → kiwimatecoder-0.2.0}/tests/test_permissions.py +0 -0
  167. {kiwimatecoder-0.1.0 → kiwimatecoder-0.2.0}/tests/test_pickers.py +0 -0
  168. {kiwimatecoder-0.1.0 → kiwimatecoder-0.2.0}/tests/test_plugins.py +0 -0
  169. {kiwimatecoder-0.1.0 → kiwimatecoder-0.2.0}/tests/test_pricing.py +0 -0
  170. {kiwimatecoder-0.1.0 → kiwimatecoder-0.2.0}/tests/test_profiles.py +0 -0
  171. {kiwimatecoder-0.1.0 → kiwimatecoder-0.2.0}/tests/test_prompts.py +0 -0
  172. {kiwimatecoder-0.1.0 → kiwimatecoder-0.2.0}/tests/test_providers.py +0 -0
  173. {kiwimatecoder-0.1.0 → kiwimatecoder-0.2.0}/tests/test_redaction.py +0 -0
  174. {kiwimatecoder-0.1.0 → kiwimatecoder-0.2.0}/tests/test_remote.py +0 -0
  175. {kiwimatecoder-0.1.0 → kiwimatecoder-0.2.0}/tests/test_repl.py +0 -0
  176. {kiwimatecoder-0.1.0 → kiwimatecoder-0.2.0}/tests/test_routing.py +0 -0
  177. {kiwimatecoder-0.1.0 → kiwimatecoder-0.2.0}/tests/test_sandbox.py +0 -0
  178. {kiwimatecoder-0.1.0 → kiwimatecoder-0.2.0}/tests/test_sdk.py +0 -0
  179. {kiwimatecoder-0.1.0 → kiwimatecoder-0.2.0}/tests/test_search_optimized.py +0 -0
  180. {kiwimatecoder-0.1.0 → kiwimatecoder-0.2.0}/tests/test_session_persistence.py +0 -0
  181. {kiwimatecoder-0.1.0 → kiwimatecoder-0.2.0}/tests/test_share.py +0 -0
  182. {kiwimatecoder-0.1.0 → kiwimatecoder-0.2.0}/tests/test_shell.py +0 -0
  183. {kiwimatecoder-0.1.0 → kiwimatecoder-0.2.0}/tests/test_skills.py +0 -0
  184. {kiwimatecoder-0.1.0 → kiwimatecoder-0.2.0}/tests/test_slash_command_coverage.py +0 -0
  185. {kiwimatecoder-0.1.0 → kiwimatecoder-0.2.0}/tests/test_subagents.py +0 -0
  186. {kiwimatecoder-0.1.0 → kiwimatecoder-0.2.0}/tests/test_sync.py +0 -0
  187. {kiwimatecoder-0.1.0 → kiwimatecoder-0.2.0}/tests/test_team.py +0 -0
  188. {kiwimatecoder-0.1.0 → kiwimatecoder-0.2.0}/tests/test_telemetry.py +0 -0
  189. {kiwimatecoder-0.1.0 → kiwimatecoder-0.2.0}/tests/test_templates.py +0 -0
  190. {kiwimatecoder-0.1.0 → kiwimatecoder-0.2.0}/tests/test_tools.py +0 -0
  191. {kiwimatecoder-0.1.0 → kiwimatecoder-0.2.0}/tests/test_ui.py +0 -0
  192. {kiwimatecoder-0.1.0 → kiwimatecoder-0.2.0}/tests/test_updater.py +0 -0
  193. {kiwimatecoder-0.1.0 → kiwimatecoder-0.2.0}/tests/test_web.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: kiwimatecoder
3
- Version: 0.1.0
3
+ Version: 0.2.0
4
4
  Summary: AI-powered coding assistant CLI. Bring your own API key.
5
5
  License: MIT License
6
6
 
@@ -217,6 +217,7 @@ paths outside the workspace root; writes stay sandboxed.
217
217
  | `/dry-run [on\|off\|toggle]` | Preview mutating actions without running them. |
218
218
  | `/undo [count]`, `/checkpoints` | Restore files changed by recent tools, or list checkpoints. |
219
219
  | `/todos` | Show the agent's task list. |
220
+ | `/goal [--rounds N] <objective>`, `/goal pause\|resume\|clear` | Keep the agent working on an objective across rounds until it reports the goal achieved or blocked, or the round limit is reached. Bare `/goal` shows the current goal (see [Goals](#goals)). |
220
221
  | `/templates` | List custom prompt templates discovered in the workspace and user command directories. |
221
222
  | `/compact [budget]` | Trim older history to fit a token budget. |
222
223
  | `/save`, `/load`, `/sessions`, `/fork [name]`, `/export [path]` | Save, resume, branch, or export sessions as Markdown. |
@@ -316,6 +317,8 @@ The assistant has these capabilities, all scoped to the workspace:
316
317
  - `shell_jobs` — start, list, read, or kill detached background commands
317
318
  (approval-gated; see below).
318
319
  - `update_todos` — keep the visible task list in sync with multi-step work.
320
+ - `update_goal` — report progress on, or the outcome of, a `/goal`. It is only
321
+ offered to the model while a goal is active (see Goals below).
319
322
  - `task` — delegate a focused investigation to a subagent with its own context
320
323
  (approval-gated; see below).
321
324
  - `ask_user` — ask a clarifying question with optional choices (interactive
@@ -1659,6 +1662,38 @@ kiwimatecoder config verify show
1659
1662
  kiwimatecoder config verify clear
1660
1663
  ```
1661
1664
 
1665
+ ## Goals
1666
+
1667
+ `/goal` keeps the agent working on an objective across several rounds, so a
1668
+ larger task can run without you prompting each step:
1669
+
1670
+ ```text
1671
+ /goal make every test in tests/parser pass without changing expected output
1672
+ /goal --rounds 20 migrate the config loader to TOML
1673
+ /goal
1674
+ /goal pause
1675
+ /goal resume
1676
+ /goal clear
1677
+ ```
1678
+
1679
+ Each round is an ordinary agent turn. After a round, the agent starts another
1680
+ unless it reports the goal with `update_goal`:
1681
+
1682
+ - `achieved`, once it has checked the result (the summary says how);
1683
+ - `blocked`, when it needs something only you can provide (the summary says what).
1684
+
1685
+ A goal also pauses when the round limit is reached (10 by default, up to 100
1686
+ with `--rounds`), when a round ends abnormally (the budget is spent, the provider
1687
+ fails, or the tool-call limit is hit), or when you press Ctrl-C or run
1688
+ `/goal pause`. Messages you type during a round steer it, as usual.
1689
+ `/goal resume` gives it a fresh round allowance. A goal saved with `/save`
1690
+ comes back paused, so nothing runs until you resume it.
1691
+
1692
+ Permission prompts still apply to every tool call. For an unattended run, pick
1693
+ `/mode auto-accept`, or answer `a` (always) at a prompt for the tools you trust.
1694
+ If the objective starts with a word such as `clear`, put `--` first:
1695
+ `/goal -- clear the build cache and rerun the tests`.
1696
+
1662
1697
  ## Budgets
1663
1698
 
1664
1699
  Cap a session's spend by tokens, cost, or both. The agent warns at 80% and
@@ -176,6 +176,7 @@ paths outside the workspace root; writes stay sandboxed.
176
176
  | `/dry-run [on\|off\|toggle]` | Preview mutating actions without running them. |
177
177
  | `/undo [count]`, `/checkpoints` | Restore files changed by recent tools, or list checkpoints. |
178
178
  | `/todos` | Show the agent's task list. |
179
+ | `/goal [--rounds N] <objective>`, `/goal pause\|resume\|clear` | Keep the agent working on an objective across rounds until it reports the goal achieved or blocked, or the round limit is reached. Bare `/goal` shows the current goal (see [Goals](#goals)). |
179
180
  | `/templates` | List custom prompt templates discovered in the workspace and user command directories. |
180
181
  | `/compact [budget]` | Trim older history to fit a token budget. |
181
182
  | `/save`, `/load`, `/sessions`, `/fork [name]`, `/export [path]` | Save, resume, branch, or export sessions as Markdown. |
@@ -275,6 +276,8 @@ The assistant has these capabilities, all scoped to the workspace:
275
276
  - `shell_jobs` — start, list, read, or kill detached background commands
276
277
  (approval-gated; see below).
277
278
  - `update_todos` — keep the visible task list in sync with multi-step work.
279
+ - `update_goal` — report progress on, or the outcome of, a `/goal`. It is only
280
+ offered to the model while a goal is active (see Goals below).
278
281
  - `task` — delegate a focused investigation to a subagent with its own context
279
282
  (approval-gated; see below).
280
283
  - `ask_user` — ask a clarifying question with optional choices (interactive
@@ -1618,6 +1621,38 @@ kiwimatecoder config verify show
1618
1621
  kiwimatecoder config verify clear
1619
1622
  ```
1620
1623
 
1624
+ ## Goals
1625
+
1626
+ `/goal` keeps the agent working on an objective across several rounds, so a
1627
+ larger task can run without you prompting each step:
1628
+
1629
+ ```text
1630
+ /goal make every test in tests/parser pass without changing expected output
1631
+ /goal --rounds 20 migrate the config loader to TOML
1632
+ /goal
1633
+ /goal pause
1634
+ /goal resume
1635
+ /goal clear
1636
+ ```
1637
+
1638
+ Each round is an ordinary agent turn. After a round, the agent starts another
1639
+ unless it reports the goal with `update_goal`:
1640
+
1641
+ - `achieved`, once it has checked the result (the summary says how);
1642
+ - `blocked`, when it needs something only you can provide (the summary says what).
1643
+
1644
+ A goal also pauses when the round limit is reached (10 by default, up to 100
1645
+ with `--rounds`), when a round ends abnormally (the budget is spent, the provider
1646
+ fails, or the tool-call limit is hit), or when you press Ctrl-C or run
1647
+ `/goal pause`. Messages you type during a round steer it, as usual.
1648
+ `/goal resume` gives it a fresh round allowance. A goal saved with `/save`
1649
+ comes back paused, so nothing runs until you resume it.
1650
+
1651
+ Permission prompts still apply to every tool call. For an unattended run, pick
1652
+ `/mode auto-accept`, or answer `a` (always) at a prompt for the tools you trust.
1653
+ If the objective starts with a word such as `clear`, put `--` first:
1654
+ `/goal -- clear the build cache and rerun the tests`.
1655
+
1621
1656
  ## Budgets
1622
1657
 
1623
1658
  Cap a session's spend by tokens, cost, or both. The agent warns at 80% and
@@ -116,6 +116,9 @@ class Agent:
116
116
  self.confirm = confirm
117
117
  self.bus = bus if bus is not None else events.BUS
118
118
  self._budget_warned = False
119
+ # Why the most recent run_turn ended ("stop", "budget", ...); the REPL
120
+ # reads it to decide whether a /goal keeps going. None while a turn runs.
121
+ self.last_stop_reason: str | None = None
119
122
  self.ascii_mode = bool(ascii_mode)
120
123
  self.output_mode = output_mode if output_mode in ui.OUTPUT_MODES else "normal"
121
124
  self.event_handler = event_handler
@@ -133,6 +136,11 @@ class Agent:
133
136
  except Exception:
134
137
  _log.debug("event handler failed for %s", name, exc_info=True)
135
138
 
139
+ def _done(self, reason: str, **payload: Any) -> None:
140
+ """Record how a turn ended, then tell the event handler."""
141
+ self.last_stop_reason = reason
142
+ self._emit("done", reason=reason, **payload)
143
+
136
144
  def _redacted_args(self, args: dict[str, Any]) -> dict[str, Any]:
137
145
  """Return a JSON-safe copy of tool arguments with secrets redacted."""
138
146
  try:
@@ -211,10 +219,11 @@ class Agent:
211
219
 
212
220
  async def run_turn(self, user_input: str) -> None:
213
221
  """Process one user message, looping over tool calls until the model stops."""
222
+ self.last_stop_reason = None
214
223
  blocked, reason = self._budget_exceeded()
215
224
  if blocked:
216
225
  self.console.print(f"[red]{escape(reason)}[/red]")
217
- self._emit("done", reason="budget", error=reason)
226
+ self._done("budget", error=reason)
218
227
  return
219
228
 
220
229
  from kiwimatecoder.config import get_model_routing
@@ -246,7 +255,7 @@ class Agent:
246
255
  except ProviderError as exc:
247
256
  self.console.print(f"\n[red]{escape(str(exc))}[/red]")
248
257
  self._emit("error", kind="provider", error=str(exc))
249
- self._emit("done", reason="provider_error")
258
+ self._done("provider_error")
250
259
  return
251
260
 
252
261
  self.session.messages.append(assistant_msg)
@@ -260,11 +269,11 @@ class Agent:
260
269
  # The model must answer the steered message.
261
270
  continue
262
271
  self._warn_budget()
263
- self._emit("done", reason="stop")
272
+ self._done("stop")
264
273
  return
265
274
 
266
275
  if self.max_turns is not None and tool_turns >= self.max_turns:
267
- self._emit("done", reason="max_turns", limit=self.max_turns)
276
+ self._done("max_turns", limit=self.max_turns)
268
277
  return
269
278
  edited = await self._handle_tool_calls(tool_calls) or edited
270
279
  tool_turns += 1
@@ -384,9 +393,11 @@ class Agent:
384
393
  rest as a second line of defense). Subagents never see ``task`` (no
385
394
  nested subagents) or ``ask_user`` (no interactive user), and ``task``
386
395
  is hidden for everyone while subagents are disabled. ``browser`` is
387
- hidden until browser automation is enabled in config.
396
+ hidden until browser automation is enabled in config. ``update_goal``
397
+ is offered only while a /goal is active.
388
398
  """
389
399
  from kiwimatecoder.config import get_browser, get_subagents
400
+ from kiwimatecoder.goal import ACTIVE
390
401
 
391
402
  schemas = tools.tool_schemas(
392
403
  read_only=self.session.mode is PermissionMode.PLAN
@@ -398,6 +409,9 @@ class Agent:
398
409
  excluded.add("task")
399
410
  if not get_browser()["enabled"]:
400
411
  excluded.add("browser")
412
+ goal = self.session.goal
413
+ if goal is None or goal.status != ACTIVE:
414
+ excluded.add("update_goal")
401
415
  if excluded:
402
416
  schemas = [
403
417
  schema
@@ -18,6 +18,7 @@ from rich.markup import escape
18
18
  from rich.panel import Panel
19
19
  from rich.table import Table
20
20
 
21
+ from kiwimatecoder import goal as goal_module
21
22
  from kiwimatecoder import tools
22
23
  from kiwimatecoder import media as media_module
23
24
  from kiwimatecoder import memory as memory_module
@@ -956,13 +957,17 @@ def _expand_context_input(
956
957
  if pattern.is_absolute() or pattern.root or pattern.drive:
957
958
  return [], [f"Glob patterns must be relative to the workspace: {raw_path}"]
958
959
 
960
+ # Python 3.10-3.12 raise ValueError for "a**b", but 3.13 just returns no matches,
961
+ # so the rule is checked here: '**' is a wildcard only as a whole path component.
962
+ if any("**" in part and part != "**" for part in pattern.parts):
963
+ return [], ["Invalid glob pattern: '**' can only be an entire path component"]
964
+
959
965
  root = session.workspace_root.resolve()
960
966
  errors: list[str] = []
961
967
  matches: list[Path] = []
962
968
  try:
963
969
  candidates = sorted(root.glob(raw_path))
964
970
  except (NotImplementedError, ValueError, OSError) as exc:
965
- # e.g. "a**b": '**' can only be an entire path component
966
971
  return [], [f"Invalid glob pattern: {exc}"]
967
972
  for candidate in candidates:
968
973
  try:
@@ -6279,6 +6284,92 @@ def _media(
6279
6284
  return CommandResult.CONTINUE
6280
6285
 
6281
6286
 
6287
+ def _goal(arg: str, session: Session, console: Console) -> str:
6288
+ """Show, set, pause, resume or clear the session goal (see kiwimatecoder.goal).
6289
+
6290
+ Setting or resuming a goal only marks it due; the REPL runs the rounds once
6291
+ this command returns.
6292
+ """
6293
+ try:
6294
+ command = goal_module.parse_goal_command(arg)
6295
+ except ValueError as exc:
6296
+ console.print(f"[yellow]{escape(str(exc))}[/yellow]")
6297
+ return CommandResult.CONTINUE
6298
+
6299
+ goal = session.goal
6300
+ if command.verb == "show":
6301
+ _print_goal(goal, console)
6302
+ elif command.verb == "set":
6303
+ session.goal = goal_module.new_goal(command.objective, command.max_rounds)
6304
+ console.print(
6305
+ f"[green]Goal set[/green] (up to {session.goal.max_rounds} rounds): "
6306
+ f"{escape(session.goal.objective)}"
6307
+ )
6308
+ console.print(
6309
+ "[dim]Ctrl-C stops it. Permission prompts still apply; /mode auto-accept "
6310
+ "lets it run unattended.[/dim]"
6311
+ )
6312
+ elif command.verb == "pause":
6313
+ if goal is None or goal.status != goal_module.ACTIVE:
6314
+ console.print("[yellow]No running goal to pause.[/yellow]")
6315
+ else:
6316
+ # Not due means a round is in progress, which finishes before the pause.
6317
+ running = not goal.due
6318
+ goal_module.pause(goal, "paused by you")
6319
+ console.print(
6320
+ "[yellow]Goal will pause after the current round.[/yellow]"
6321
+ if running
6322
+ else "[yellow]Goal paused.[/yellow]"
6323
+ )
6324
+ console.print("[dim]/goal resume continues it.[/dim]")
6325
+ elif command.verb == "resume":
6326
+ if goal is None:
6327
+ console.print("[yellow]No goal to resume. Set one with /goal <objective>.[/yellow]")
6328
+ elif goal.status == goal_module.ACHIEVED:
6329
+ console.print(
6330
+ "[dim]That goal is already achieved. Set a new one with /goal <objective>.[/dim]"
6331
+ )
6332
+ elif goal.status == goal_module.ACTIVE:
6333
+ console.print("[dim]The goal is already running.[/dim]")
6334
+ else:
6335
+ goal_module.resume(goal)
6336
+ console.print(f"[green]Resuming goal[/green] (up to {goal.max_rounds} more rounds).")
6337
+ elif command.verb == "clear":
6338
+ if goal is None:
6339
+ console.print("[dim]No goal to clear.[/dim]")
6340
+ else:
6341
+ session.goal = None
6342
+ console.print("[dim]Goal cleared.[/dim]")
6343
+ return CommandResult.CONTINUE
6344
+
6345
+
6346
+ def _print_goal(goal: goal_module.Goal | None, console: Console) -> None:
6347
+ if goal is None:
6348
+ console.print("[dim]No goal set. Start one with /goal <objective>.[/dim]")
6349
+ return
6350
+ detail = f"{goal.status}, round {goal.rounds} of {goal.max_rounds}"
6351
+ if not goal.rounds:
6352
+ detail = f"{goal.status}, up to {goal.max_rounds} rounds"
6353
+ console.print(f"[bold]Goal:[/bold] {escape(goal.objective)}")
6354
+ console.print(f"Status: {escape(detail)}")
6355
+ if goal.progress:
6356
+ console.print(f"Last progress: {escape(goal.progress)}")
6357
+ labels = {
6358
+ goal_module.ACHIEVED: "Result",
6359
+ goal_module.BLOCKED: "Needs",
6360
+ goal_module.PAUSED: "Why",
6361
+ }
6362
+ if goal.outcome and goal.status in labels:
6363
+ console.print(f"{labels[goal.status]}: {escape(goal.outcome)}")
6364
+ hints = {
6365
+ goal_module.ACTIVE: "Running. /goal pause stops it after this round.",
6366
+ goal_module.PAUSED: "/goal resume continues it. /goal clear drops it.",
6367
+ goal_module.BLOCKED: "Answer the question above, then /goal resume.",
6368
+ goal_module.ACHIEVED: "Done. Set a new goal with /goal <objective>.",
6369
+ }
6370
+ console.print(f"[dim]{hints[goal.status]}[/dim]")
6371
+
6372
+
6282
6373
  _COMMANDS: dict[str, Callable[[str, Session, Console], str]] = {
6283
6374
  "help": _help,
6284
6375
  "exit": _exit,
@@ -6311,6 +6402,7 @@ _COMMANDS: dict[str, Callable[[str, Session, Console], str]] = {
6311
6402
  "rewind": _undo,
6312
6403
  "checkpoints": _checkpoints,
6313
6404
  "todos": _todos,
6405
+ "goal": _goal,
6314
6406
  "memory": _memory,
6315
6407
  "compact": _compact,
6316
6408
  "index": _index,
@@ -6400,6 +6492,12 @@ _HELP_GROUPS = [
6400
6492
  ("/undo [count]", "Restore files changed by recent tool actions."),
6401
6493
  ("/checkpoints", "List captured file checkpoints."),
6402
6494
  ("/todos", "Show the agent's task list."),
6495
+ (
6496
+ "/goal [--rounds N] <objective> | pause | resume | clear",
6497
+ "Work toward an objective across rounds until the agent reports it "
6498
+ "achieved or blocked, or the round limit (default 10) is reached. "
6499
+ "Bare /goal shows the current goal.",
6500
+ ),
6403
6501
  (
6404
6502
  "/memory [list|add|add-user|clear]",
6405
6503
  "Show or edit persistent project and user memory.",
@@ -6534,6 +6632,7 @@ _COMMAND_DESCRIPTIONS = {
6534
6632
  "rewind": "Alias for /undo.",
6535
6633
  "checkpoints": "List captured file checkpoints.",
6536
6634
  "todos": "Show the agent's task list.",
6635
+ "goal": "Work toward an objective across rounds, or show, pause, resume, or clear it.",
6537
6636
  "memory": "Show or edit persistent project and user memory.",
6538
6637
  "compact": "Trim older history to fit a token budget.",
6539
6638
  "index": "Show, refresh, or delete the codebase index used by semantic search.",
@@ -6576,6 +6675,12 @@ _MODE_DESCRIPTIONS = {
6576
6675
  "plan": "Read-only planning mode.",
6577
6676
  }
6578
6677
 
6678
+ _GOAL_ACTION_DESCRIPTIONS = {
6679
+ "pause": "Stop the goal after the current round.",
6680
+ "resume": "Continue a paused or blocked goal.",
6681
+ "clear": "Drop the goal.",
6682
+ }
6683
+
6579
6684
  _MEMORY_ACTION_DESCRIPTIONS = {
6580
6685
  "list": "Show project and user memory.",
6581
6686
  "add": "Append a fact to project memory.",
@@ -6717,6 +6822,8 @@ def slash_argument_completions(
6717
6822
  choices = _MODE_DESCRIPTIONS
6718
6823
  elif command == "memory":
6719
6824
  choices = _MEMORY_ACTION_DESCRIPTIONS
6825
+ elif command == "goal":
6826
+ choices = _GOAL_ACTION_DESCRIPTIONS
6720
6827
  elif command == "index":
6721
6828
  choices = _INDEX_ACTION_DESCRIPTIONS
6722
6829
  elif command == "model" and session is not None:
@@ -0,0 +1,229 @@
1
+ """Session goals: an objective the agent keeps working on across rounds.
2
+
3
+ ``/goal <objective>`` sets one and the REPL runs agent rounds back to back until
4
+ the model reports it achieved or blocked, the round cap is reached, or a round
5
+ ends abnormally (budget, provider error, tool-call limit, Ctrl-C). This module
6
+ holds the state and builds the prompts; the REPL drives the rounds.
7
+ """
8
+
9
+ from __future__ import annotations
10
+
11
+ import re
12
+ from dataclasses import dataclass
13
+ from typing import Any
14
+
15
+ ACTIVE = "active"
16
+ PAUSED = "paused"
17
+ ACHIEVED = "achieved"
18
+ BLOCKED = "blocked"
19
+ STATUSES = (ACTIVE, PAUSED, ACHIEVED, BLOCKED)
20
+
21
+ DEFAULT_MAX_ROUNDS = 10
22
+ MAX_ROUNDS_LIMIT = 100
23
+ MAX_OBJECTIVE_CHARS = 4000
24
+
25
+ # Why a round ended, from the agent's "done" reason. Anything but a normal stop
26
+ # pauses the goal, and the reason is shown so the user knows what happened.
27
+ _STOP_REASONS = {
28
+ "interrupted": "you interrupted it",
29
+ "budget": "the budget is spent",
30
+ "provider_error": "the provider returned an error",
31
+ "max_turns": "the tool-call limit was reached",
32
+ }
33
+
34
+ _USAGE = (
35
+ "Usage: /goal <objective> · /goal --rounds N <objective> · "
36
+ "/goal pause | resume | clear · /goal"
37
+ )
38
+ _ROUNDS_FLAG = re.compile(r"^--rounds(?=\s|$)\s*(\S*)\s*(.*)$", re.DOTALL)
39
+
40
+
41
+ @dataclass
42
+ class Goal:
43
+ """One standing objective and how far the agent has got with it."""
44
+
45
+ objective: str
46
+ max_rounds: int = DEFAULT_MAX_ROUNDS
47
+ status: str = ACTIVE
48
+ rounds: int = 0 # rounds run since the goal was set or last resumed
49
+ progress: str = "" # latest in_progress note from update_goal
50
+ outcome: str = "" # summary given with achieved/blocked, or why it paused
51
+ due: bool = True # runtime only: another round should run now
52
+
53
+ def to_dict(self) -> dict[str, Any]:
54
+ return {
55
+ "objective": self.objective,
56
+ "max_rounds": self.max_rounds,
57
+ "status": self.status,
58
+ "rounds": self.rounds,
59
+ "progress": self.progress,
60
+ "outcome": self.outcome,
61
+ }
62
+
63
+ @classmethod
64
+ def from_dict(cls, data: Any) -> Goal | None:
65
+ """Restore a saved goal. A goal that was running is restored as paused,
66
+ so the user decides when to continue it (``/goal resume``)."""
67
+ if not isinstance(data, dict):
68
+ return None
69
+ raw_objective = data.get("objective")
70
+ objective = raw_objective.strip() if isinstance(raw_objective, str) else ""
71
+ if not objective:
72
+ return None
73
+ status = str(data.get("status") or PAUSED)
74
+ outcome = str(data.get("outcome") or "")
75
+ if status not in STATUSES or status == ACTIVE:
76
+ status = PAUSED
77
+ outcome = "restored from a saved session"
78
+ return cls(
79
+ objective=objective,
80
+ max_rounds=_clamp_rounds(data.get("max_rounds")),
81
+ status=status,
82
+ rounds=max(0, _as_int(data.get("rounds"))),
83
+ progress=str(data.get("progress") or ""),
84
+ outcome=outcome,
85
+ due=False,
86
+ )
87
+
88
+
89
+ @dataclass(frozen=True)
90
+ class GoalCommand:
91
+ """A parsed ``/goal`` argument."""
92
+
93
+ verb: str # "show", "set", "pause", "resume" or "clear"
94
+ objective: str = ""
95
+ max_rounds: int = DEFAULT_MAX_ROUNDS
96
+
97
+
98
+ def parse_goal_command(arg: str) -> GoalCommand:
99
+ """Parse the text after ``/goal``. Raises ``ValueError`` with a message."""
100
+ text = arg.strip()
101
+ if not text:
102
+ return GoalCommand("show")
103
+
104
+ # "--" makes the rest an objective even when it reads like a verb.
105
+ if text == "--" or text.startswith("-- "):
106
+ objective = text[2:].strip()
107
+ if not objective:
108
+ raise ValueError(_USAGE)
109
+ return GoalCommand("set", _checked_objective(objective))
110
+
111
+ max_rounds = DEFAULT_MAX_ROUNDS
112
+ flag = _ROUNDS_FLAG.match(text)
113
+ if flag:
114
+ value, text = flag.group(1), flag.group(2).strip()
115
+ # isascii: str.isdigit() accepts forms such as "²" that int() rejects.
116
+ if not (value.isascii() and value.isdigit()) or not (
117
+ 1 <= int(value) <= MAX_ROUNDS_LIMIT
118
+ ):
119
+ raise ValueError(f"--rounds takes a number from 1 to {MAX_ROUNDS_LIMIT}.")
120
+ max_rounds = int(value)
121
+ if not text:
122
+ raise ValueError("--rounds needs an objective, e.g. /goal --rounds 20 fix the tests.")
123
+ return GoalCommand("set", _checked_objective(text), max_rounds)
124
+
125
+ verb = text.lower()
126
+ if verb in ("pause", "resume", "clear"):
127
+ return GoalCommand(verb)
128
+ return GoalCommand("set", _checked_objective(text), max_rounds)
129
+
130
+
131
+ def _checked_objective(text: str) -> str:
132
+ if len(text) > MAX_OBJECTIVE_CHARS:
133
+ raise ValueError(f"A goal can be at most {MAX_OBJECTIVE_CHARS} characters.")
134
+ return text
135
+
136
+
137
+ def new_goal(objective: str, max_rounds: int = DEFAULT_MAX_ROUNDS) -> Goal:
138
+ return Goal(objective=objective.strip(), max_rounds=_clamp_rounds(max_rounds))
139
+
140
+
141
+ def start_round(goal: Goal) -> str:
142
+ """Begin the next round and return the user message that starts it."""
143
+ goal.rounds += 1
144
+ goal.due = False
145
+ if goal.rounds == 1:
146
+ return opening_prompt(goal)
147
+ return continuation_prompt(goal)
148
+
149
+
150
+ def finish_round(goal: Goal, stop_reason: str | None) -> None:
151
+ """Decide, after a round, whether another round is due.
152
+
153
+ The goal keeps going only when the model stopped on its own and the round
154
+ budget is not spent. A status set by ``update_goal`` during the round
155
+ (achieved or blocked) is left alone.
156
+ """
157
+ goal.due = False
158
+ if goal.status != ACTIVE:
159
+ return
160
+ if stop_reason != "stop":
161
+ pause(goal, _STOP_REASONS.get(stop_reason or "", "the round ended early"))
162
+ return
163
+ if goal.rounds >= goal.max_rounds:
164
+ pause(goal, f"reached the {goal.max_rounds}-round limit")
165
+ return
166
+ goal.due = True
167
+
168
+
169
+ def pause(goal: Goal, reason: str) -> None:
170
+ goal.status = PAUSED
171
+ goal.outcome = reason
172
+ goal.due = False
173
+
174
+
175
+ def resume(goal: Goal) -> None:
176
+ """Make the goal active again with a fresh round allowance."""
177
+ goal.status = ACTIVE
178
+ goal.rounds = 0
179
+ goal.outcome = ""
180
+ goal.due = True
181
+
182
+
183
+ def close(goal: Goal, status: str, summary: str) -> None:
184
+ """Record the outcome reported through ``update_goal``."""
185
+ goal.status = status
186
+ goal.outcome = summary
187
+ goal.due = False
188
+
189
+
190
+ def opening_prompt(goal: Goal) -> str:
191
+ return (
192
+ "You are working toward a goal the user set. Work through it step by step, "
193
+ "using tools as needed, until the objective is fully met. Do not stop to ask "
194
+ "for confirmation unless you are genuinely blocked.\n\n"
195
+ f"Goal: {goal.objective}\n\n"
196
+ "Report through the update_goal tool:\n"
197
+ '- status "in_progress" with a one-line summary after meaningful progress;\n'
198
+ '- status "achieved" only once you have checked the result, with a summary of '
199
+ "how you verified it;\n"
200
+ '- status "blocked" only if you need something only the user can provide, with '
201
+ "a summary of exactly what you need."
202
+ )
203
+
204
+
205
+ def continuation_prompt(goal: Goal) -> str:
206
+ note = (
207
+ f"Last progress note: {goal.progress}"
208
+ if goal.progress
209
+ else "No progress note has been recorded yet."
210
+ )
211
+ return (
212
+ f"Keep working toward the goal (round {goal.rounds} of {goal.max_rounds}).\n"
213
+ f"Goal: {goal.objective}\n"
214
+ f"{note}\n\n"
215
+ "Continue from where you left off. When the objective is fully met and "
216
+ 'verified, call update_goal with status "achieved"; if you are blocked on '
217
+ 'something only the user can give, call it with status "blocked".'
218
+ )
219
+
220
+
221
+ def _clamp_rounds(value: Any) -> int:
222
+ return min(MAX_ROUNDS_LIMIT, max(1, _as_int(value, DEFAULT_MAX_ROUNDS)))
223
+
224
+
225
+ def _as_int(value: Any, default: int = 0) -> int:
226
+ try:
227
+ return int(value)
228
+ except (TypeError, ValueError):
229
+ return default
@@ -60,6 +60,7 @@ from rich.panel import Panel
60
60
  from rich.syntax import Syntax
61
61
 
62
62
  from kiwimatecoder import __version__, browser, events, hooks, images, lsp, mcp, notify, plugins, ui
63
+ from kiwimatecoder import goal as goal_module
63
64
  from kiwimatecoder.agent import Agent
64
65
  from kiwimatecoder.commands import (
65
66
  CommandOption,
@@ -1168,6 +1169,7 @@ async def _run_turn_with_steering(
1168
1169
  raise
1169
1170
 
1170
1171
  if interrupted:
1172
+ agent.last_stop_reason = "interrupted"
1171
1173
  console.print(f"\n[yellow]{t('prompt.interrupted')}[/yellow]")
1172
1174
 
1173
1175
  if await _process_deferred_commands(session):
@@ -1175,6 +1177,48 @@ async def _run_turn_with_steering(
1175
1177
  return exit_requested
1176
1178
 
1177
1179
 
1180
+ def _announce_goal_stop(goal: goal_module.Goal) -> None:
1181
+ """Say why a goal stopped after a round (it is not due another one)."""
1182
+ if goal.status == goal_module.ACHIEVED:
1183
+ console.print(
1184
+ f"[green]Goal achieved after {goal.rounds} round(s).[/green] "
1185
+ f"{escape(goal.outcome)}"
1186
+ )
1187
+ elif goal.status == goal_module.BLOCKED:
1188
+ console.print(
1189
+ f"[yellow]Goal blocked: {escape(goal.outcome)}[/yellow]\n"
1190
+ "[dim]Answer above, then /goal resume.[/dim]"
1191
+ )
1192
+ elif goal.status == goal_module.PAUSED:
1193
+ console.print(
1194
+ f"[yellow]Goal paused: {escape(goal.outcome)}.[/yellow] "
1195
+ "[dim]/goal resume continues it, /goal clear drops it.[/dim]"
1196
+ )
1197
+
1198
+
1199
+ async def _drive_goal(
1200
+ agent: Agent, pt_session: PromptSession[str], session: Session
1201
+ ) -> bool:
1202
+ """Run goal rounds back to back while the session goal is due.
1203
+
1204
+ Each round is an ordinary agent turn, so steering, Ctrl-C and approvals all
1205
+ work as they do elsewhere. Returns True when the REPL should exit.
1206
+ """
1207
+ while session.goal is not None and session.goal.due:
1208
+ goal = session.goal
1209
+ prompt = goal_module.start_round(goal)
1210
+ # Plain text rather than a box-drawing rule, which ASCII mode cannot show.
1211
+ console.print(f"[dim]goal round {goal.rounds} of {goal.max_rounds}[/dim]")
1212
+ if await _run_turn_with_steering(agent, pt_session, session, prompt):
1213
+ return True
1214
+ # A /goal command may have replaced or cleared the goal mid-round.
1215
+ if session.goal is goal:
1216
+ goal_module.finish_round(goal, agent.last_stop_reason)
1217
+ if not goal.due:
1218
+ _announce_goal_stop(goal)
1219
+ return False
1220
+
1221
+
1178
1222
  def _run_lifecycle_hooks(
1179
1223
  bus: events.EventBus, event: str, session: Session
1180
1224
  ) -> None:
@@ -1296,6 +1340,9 @@ async def _run_interactive(
1296
1340
 
1297
1341
  try:
1298
1342
  while True:
1343
+ # A goal set by /goal (or queued during a turn) runs before the next prompt.
1344
+ if await _drive_goal(agent, pt_session, session):
1345
+ break
1299
1346
  try:
1300
1347
  prompt_str = (
1301
1348
  HTML("<ansicyan>... </ansicyan>")