sliceagent 0.1.1__tar.gz → 0.1.3__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 (203) hide show
  1. {sliceagent-0.1.1 → sliceagent-0.1.3}/CHANGELOG.md +33 -0
  2. {sliceagent-0.1.1 → sliceagent-0.1.3}/PKG-INFO +3 -4
  3. {sliceagent-0.1.1 → sliceagent-0.1.3}/QUICKSTART.md +6 -4
  4. {sliceagent-0.1.1 → sliceagent-0.1.3}/README.md +2 -3
  5. {sliceagent-0.1.1 → sliceagent-0.1.3}/install.sh +2 -3
  6. {sliceagent-0.1.1 → sliceagent-0.1.3}/src/sliceagent/__init__.py +1 -1
  7. {sliceagent-0.1.1 → sliceagent-0.1.3}/src/sliceagent/cli.py +9 -2
  8. {sliceagent-0.1.1 → sliceagent-0.1.3}/src/sliceagent/llm.py +18 -1
  9. {sliceagent-0.1.1 → sliceagent-0.1.3}/src/sliceagent/onboarding.py +91 -11
  10. {sliceagent-0.1.1 → sliceagent-0.1.3}/src/sliceagent/sensory_cortex.py +24 -3
  11. {sliceagent-0.1.1 → sliceagent-0.1.3}/tests/test_onboarding.py +7 -7
  12. sliceagent-0.1.3/tests/test_provider_lineup.py +95 -0
  13. sliceagent-0.1.3/tests/test_repo_map_bounded.py +91 -0
  14. {sliceagent-0.1.1 → sliceagent-0.1.3}/.dockerignore +0 -0
  15. {sliceagent-0.1.1 → sliceagent-0.1.3}/.env.example +0 -0
  16. {sliceagent-0.1.1 → sliceagent-0.1.3}/.github/ISSUE_TEMPLATE/bug_report.md +0 -0
  17. {sliceagent-0.1.1 → sliceagent-0.1.3}/.github/ISSUE_TEMPLATE/feature_request.md +0 -0
  18. {sliceagent-0.1.1 → sliceagent-0.1.3}/.github/PULL_REQUEST_TEMPLATE.md +0 -0
  19. {sliceagent-0.1.1 → sliceagent-0.1.3}/.github/dependabot.yml +0 -0
  20. {sliceagent-0.1.1 → sliceagent-0.1.3}/.github/workflows/ci.yml +0 -0
  21. {sliceagent-0.1.1 → sliceagent-0.1.3}/.github/workflows/publish.yml +0 -0
  22. {sliceagent-0.1.1 → sliceagent-0.1.3}/.gitignore +0 -0
  23. {sliceagent-0.1.1 → sliceagent-0.1.3}/CODE_OF_CONDUCT.md +0 -0
  24. {sliceagent-0.1.1 → sliceagent-0.1.3}/CONTRIBUTING.md +0 -0
  25. {sliceagent-0.1.1 → sliceagent-0.1.3}/Dockerfile +0 -0
  26. {sliceagent-0.1.1 → sliceagent-0.1.3}/LICENSE +0 -0
  27. {sliceagent-0.1.1 → sliceagent-0.1.3}/NOTICE +0 -0
  28. {sliceagent-0.1.1 → sliceagent-0.1.3}/SECURITY.md +0 -0
  29. {sliceagent-0.1.1 → sliceagent-0.1.3}/examples/plugins/hello/__init__.py +0 -0
  30. {sliceagent-0.1.1 → sliceagent-0.1.3}/examples/plugins/hello/plugin.toml +0 -0
  31. {sliceagent-0.1.1 → sliceagent-0.1.3}/pyproject.toml +0 -0
  32. {sliceagent-0.1.1 → sliceagent-0.1.3}/scripts/gen_config_reference.py +0 -0
  33. {sliceagent-0.1.1 → sliceagent-0.1.3}/scripts/run_tests.sh +0 -0
  34. {sliceagent-0.1.1 → sliceagent-0.1.3}/sliceagent.toml.example +0 -0
  35. {sliceagent-0.1.1 → sliceagent-0.1.3}/src/sliceagent/__main__.py +0 -0
  36. {sliceagent-0.1.1 → sliceagent-0.1.3}/src/sliceagent/access.py +0 -0
  37. {sliceagent-0.1.1 → sliceagent-0.1.3}/src/sliceagent/agents.py +0 -0
  38. {sliceagent-0.1.1 → sliceagent-0.1.3}/src/sliceagent/background_review.py +0 -0
  39. {sliceagent-0.1.1 → sliceagent-0.1.3}/src/sliceagent/binsniff.py +0 -0
  40. {sliceagent-0.1.1 → sliceagent-0.1.3}/src/sliceagent/clock.py +0 -0
  41. {sliceagent-0.1.1 → sliceagent-0.1.3}/src/sliceagent/code_grep.py +0 -0
  42. {sliceagent-0.1.1 → sliceagent-0.1.3}/src/sliceagent/code_index.py +0 -0
  43. {sliceagent-0.1.1 → sliceagent-0.1.3}/src/sliceagent/config.py +0 -0
  44. {sliceagent-0.1.1 → sliceagent-0.1.3}/src/sliceagent/context_overflow.py +0 -0
  45. {sliceagent-0.1.1 → sliceagent-0.1.3}/src/sliceagent/envspec.py +0 -0
  46. {sliceagent-0.1.1 → sliceagent-0.1.3}/src/sliceagent/errors.py +0 -0
  47. {sliceagent-0.1.1 → sliceagent-0.1.3}/src/sliceagent/events.py +0 -0
  48. {sliceagent-0.1.1 → sliceagent-0.1.3}/src/sliceagent/finding_types.py +0 -0
  49. {sliceagent-0.1.1 → sliceagent-0.1.3}/src/sliceagent/flags.py +0 -0
  50. {sliceagent-0.1.1 → sliceagent-0.1.3}/src/sliceagent/fuzzy.py +0 -0
  51. {sliceagent-0.1.1 → sliceagent-0.1.3}/src/sliceagent/guardrails.py +0 -0
  52. {sliceagent-0.1.1 → sliceagent-0.1.3}/src/sliceagent/guidance.py +0 -0
  53. {sliceagent-0.1.1 → sliceagent-0.1.3}/src/sliceagent/hippocampus.py +0 -0
  54. {sliceagent-0.1.1 → sliceagent-0.1.3}/src/sliceagent/hooks.py +0 -0
  55. {sliceagent-0.1.1 → sliceagent-0.1.3}/src/sliceagent/interfaces.py +0 -0
  56. {sliceagent-0.1.1 → sliceagent-0.1.3}/src/sliceagent/loop.py +0 -0
  57. {sliceagent-0.1.1 → sliceagent-0.1.3}/src/sliceagent/mcp_client.py +0 -0
  58. {sliceagent-0.1.1 → sliceagent-0.1.3}/src/sliceagent/mcp_security.py +0 -0
  59. {sliceagent-0.1.1 → sliceagent-0.1.3}/src/sliceagent/memory.py +0 -0
  60. {sliceagent-0.1.1 → sliceagent-0.1.3}/src/sliceagent/metrics.py +0 -0
  61. {sliceagent-0.1.1 → sliceagent-0.1.3}/src/sliceagent/model_catalog.py +0 -0
  62. {sliceagent-0.1.1 → sliceagent-0.1.3}/src/sliceagent/monitor.py +0 -0
  63. {sliceagent-0.1.1 → sliceagent-0.1.3}/src/sliceagent/neocortex.py +0 -0
  64. {sliceagent-0.1.1 → sliceagent-0.1.3}/src/sliceagent/oracle.py +0 -0
  65. {sliceagent-0.1.1 → sliceagent-0.1.3}/src/sliceagent/pagetable.py +0 -0
  66. {sliceagent-0.1.1 → sliceagent-0.1.3}/src/sliceagent/pfc.py +0 -0
  67. {sliceagent-0.1.1 → sliceagent-0.1.3}/src/sliceagent/plugins.py +0 -0
  68. {sliceagent-0.1.1 → sliceagent-0.1.3}/src/sliceagent/policy.py +0 -0
  69. {sliceagent-0.1.1 → sliceagent-0.1.3}/src/sliceagent/procman.py +0 -0
  70. {sliceagent-0.1.1 → sliceagent-0.1.3}/src/sliceagent/prompt.py +0 -0
  71. {sliceagent-0.1.1 → sliceagent-0.1.3}/src/sliceagent/records.py +0 -0
  72. {sliceagent-0.1.1 → sliceagent-0.1.3}/src/sliceagent/recovery.py +0 -0
  73. {sliceagent-0.1.1 → sliceagent-0.1.3}/src/sliceagent/regions.py +0 -0
  74. {sliceagent-0.1.1 → sliceagent-0.1.3}/src/sliceagent/registry.py +0 -0
  75. {sliceagent-0.1.1 → sliceagent-0.1.3}/src/sliceagent/retriever.py +0 -0
  76. {sliceagent-0.1.1 → sliceagent-0.1.3}/src/sliceagent/safety.py +0 -0
  77. {sliceagent-0.1.1 → sliceagent-0.1.3}/src/sliceagent/sandbox.py +0 -0
  78. {sliceagent-0.1.1 → sliceagent-0.1.3}/src/sliceagent/scheduler.py +0 -0
  79. {sliceagent-0.1.1 → sliceagent-0.1.3}/src/sliceagent/search_index.py +0 -0
  80. {sliceagent-0.1.1 → sliceagent-0.1.3}/src/sliceagent/seed.py +0 -0
  81. {sliceagent-0.1.1 → sliceagent-0.1.3}/src/sliceagent/session.py +0 -0
  82. {sliceagent-0.1.1 → sliceagent-0.1.3}/src/sliceagent/skill_provenance.py +0 -0
  83. {sliceagent-0.1.1 → sliceagent-0.1.3}/src/sliceagent/skill_usage.py +0 -0
  84. {sliceagent-0.1.1 → sliceagent-0.1.3}/src/sliceagent/skills.py +0 -0
  85. {sliceagent-0.1.1 → sliceagent-0.1.3}/src/sliceagent/subagent.py +0 -0
  86. {sliceagent-0.1.1 → sliceagent-0.1.3}/src/sliceagent/subdir_hints.py +0 -0
  87. {sliceagent-0.1.1 → sliceagent-0.1.3}/src/sliceagent/swap.py +0 -0
  88. {sliceagent-0.1.1 → sliceagent-0.1.3}/src/sliceagent/taskstate.py +0 -0
  89. {sliceagent-0.1.1 → sliceagent-0.1.3}/src/sliceagent/telemetry.py +0 -0
  90. {sliceagent-0.1.1 → sliceagent-0.1.3}/src/sliceagent/terminal.py +0 -0
  91. {sliceagent-0.1.1 → sliceagent-0.1.3}/src/sliceagent/text_utils.py +0 -0
  92. {sliceagent-0.1.1 → sliceagent-0.1.3}/src/sliceagent/tool_summary.py +0 -0
  93. {sliceagent-0.1.1 → sliceagent-0.1.3}/src/sliceagent/tools.py +0 -0
  94. {sliceagent-0.1.1 → sliceagent-0.1.3}/src/sliceagent/tui.py +0 -0
  95. {sliceagent-0.1.1 → sliceagent-0.1.3}/src/sliceagent/web.py +0 -0
  96. {sliceagent-0.1.1 → sliceagent-0.1.3}/tests/README.md +0 -0
  97. {sliceagent-0.1.1 → sliceagent-0.1.3}/tests/test_active_focus.py +0 -0
  98. {sliceagent-0.1.1 → sliceagent-0.1.3}/tests/test_agents.py +0 -0
  99. {sliceagent-0.1.1 → sliceagent-0.1.3}/tests/test_ask_echo.py +0 -0
  100. {sliceagent-0.1.1 → sliceagent-0.1.3}/tests/test_ask_user.py +0 -0
  101. {sliceagent-0.1.1 → sliceagent-0.1.3}/tests/test_background_review.py +0 -0
  102. {sliceagent-0.1.1 → sliceagent-0.1.3}/tests/test_binary_view.py +0 -0
  103. {sliceagent-0.1.1 → sliceagent-0.1.3}/tests/test_bound_is_relevance.py +0 -0
  104. {sliceagent-0.1.1 → sliceagent-0.1.3}/tests/test_breadth_e2e.py +0 -0
  105. {sliceagent-0.1.1 → sliceagent-0.1.3}/tests/test_bugfix_2b.py +0 -0
  106. {sliceagent-0.1.1 → sliceagent-0.1.3}/tests/test_bugfix_breadth_wave.py +0 -0
  107. {sliceagent-0.1.1 → sliceagent-0.1.3}/tests/test_bugfix_current_request.py +0 -0
  108. {sliceagent-0.1.1 → sliceagent-0.1.3}/tests/test_bugfix_linenum.py +0 -0
  109. {sliceagent-0.1.1 → sliceagent-0.1.3}/tests/test_bugfix_loop_wave.py +0 -0
  110. {sliceagent-0.1.1 → sliceagent-0.1.3}/tests/test_bugfix_memory_wave.py +0 -0
  111. {sliceagent-0.1.1 → sliceagent-0.1.3}/tests/test_bugfix_p0a.py +0 -0
  112. {sliceagent-0.1.1 → sliceagent-0.1.3}/tests/test_bugfix_p0b.py +0 -0
  113. {sliceagent-0.1.1 → sliceagent-0.1.3}/tests/test_bugfix_p0c.py +0 -0
  114. {sliceagent-0.1.1 → sliceagent-0.1.3}/tests/test_bugfix_selfreview.py +0 -0
  115. {sliceagent-0.1.1 → sliceagent-0.1.3}/tests/test_bugfix_selfreview_siblings.py +0 -0
  116. {sliceagent-0.1.1 → sliceagent-0.1.3}/tests/test_bugfix_skills_h1.py +0 -0
  117. {sliceagent-0.1.1 → sliceagent-0.1.3}/tests/test_bugfix_slice_wave.py +0 -0
  118. {sliceagent-0.1.1 → sliceagent-0.1.3}/tests/test_bugfix_subagent_wave.py +0 -0
  119. {sliceagent-0.1.1 → sliceagent-0.1.3}/tests/test_bugfix_tools_wave.py +0 -0
  120. {sliceagent-0.1.1 → sliceagent-0.1.3}/tests/test_bughunt_fixes.py +0 -0
  121. {sliceagent-0.1.1 → sliceagent-0.1.3}/tests/test_cache_manifest.py +0 -0
  122. {sliceagent-0.1.1 → sliceagent-0.1.3}/tests/test_checkpoint.py +0 -0
  123. {sliceagent-0.1.1 → sliceagent-0.1.3}/tests/test_cli_smoke.py +0 -0
  124. {sliceagent-0.1.1 → sliceagent-0.1.3}/tests/test_closure.py +0 -0
  125. {sliceagent-0.1.1 → sliceagent-0.1.3}/tests/test_code_grep.py +0 -0
  126. {sliceagent-0.1.1 → sliceagent-0.1.3}/tests/test_code_index.py +0 -0
  127. {sliceagent-0.1.1 → sliceagent-0.1.3}/tests/test_consolidate.py +0 -0
  128. {sliceagent-0.1.1 → sliceagent-0.1.3}/tests/test_context_overflow.py +0 -0
  129. {sliceagent-0.1.1 → sliceagent-0.1.3}/tests/test_conversation.py +0 -0
  130. {sliceagent-0.1.1 → sliceagent-0.1.3}/tests/test_coresidency.py +0 -0
  131. {sliceagent-0.1.1 → sliceagent-0.1.3}/tests/test_echo_before_blocking.py +0 -0
  132. {sliceagent-0.1.1 → sliceagent-0.1.3}/tests/test_episode.py +0 -0
  133. {sliceagent-0.1.1 → sliceagent-0.1.3}/tests/test_errors_backoff.py +0 -0
  134. {sliceagent-0.1.1 → sliceagent-0.1.3}/tests/test_esc_sentinel.py +0 -0
  135. {sliceagent-0.1.1 → sliceagent-0.1.3}/tests/test_exec_env.py +0 -0
  136. {sliceagent-0.1.1 → sliceagent-0.1.3}/tests/test_finding_types.py +0 -0
  137. {sliceagent-0.1.1 → sliceagent-0.1.3}/tests/test_flags.py +0 -0
  138. {sliceagent-0.1.1 → sliceagent-0.1.3}/tests/test_fuzzy.py +0 -0
  139. {sliceagent-0.1.1 → sliceagent-0.1.3}/tests/test_ghost_index.py +0 -0
  140. {sliceagent-0.1.1 → sliceagent-0.1.3}/tests/test_guardrails.py +0 -0
  141. {sliceagent-0.1.1 → sliceagent-0.1.3}/tests/test_guidance.py +0 -0
  142. {sliceagent-0.1.1 → sliceagent-0.1.3}/tests/test_history.py +0 -0
  143. {sliceagent-0.1.1 → sliceagent-0.1.3}/tests/test_invariant_fixes.py +0 -0
  144. {sliceagent-0.1.1 → sliceagent-0.1.3}/tests/test_live_composer.py +0 -0
  145. {sliceagent-0.1.1 → sliceagent-0.1.3}/tests/test_llm_streaming.py +0 -0
  146. {sliceagent-0.1.1 → sliceagent-0.1.3}/tests/test_llm_usage_cache.py +0 -0
  147. {sliceagent-0.1.1 → sliceagent-0.1.3}/tests/test_llm_watchdog.py +0 -0
  148. {sliceagent-0.1.1 → sliceagent-0.1.3}/tests/test_log_redaction.py +0 -0
  149. {sliceagent-0.1.1 → sliceagent-0.1.3}/tests/test_loop_overflow.py +0 -0
  150. {sliceagent-0.1.1 → sliceagent-0.1.3}/tests/test_mcp_output_cap.py +0 -0
  151. {sliceagent-0.1.1 → sliceagent-0.1.3}/tests/test_mcp_runtime.py +0 -0
  152. {sliceagent-0.1.1 → sliceagent-0.1.3}/tests/test_memory.py +0 -0
  153. {sliceagent-0.1.1 → sliceagent-0.1.3}/tests/test_memory_persist.py +0 -0
  154. {sliceagent-0.1.1 → sliceagent-0.1.3}/tests/test_metrics.py +0 -0
  155. {sliceagent-0.1.1 → sliceagent-0.1.3}/tests/test_mining.py +0 -0
  156. {sliceagent-0.1.1 → sliceagent-0.1.3}/tests/test_mission_tier.py +0 -0
  157. {sliceagent-0.1.1 → sliceagent-0.1.3}/tests/test_model_catalog.py +0 -0
  158. {sliceagent-0.1.1 → sliceagent-0.1.3}/tests/test_monitor.py +0 -0
  159. {sliceagent-0.1.1 → sliceagent-0.1.3}/tests/test_pageout_compaction.py +0 -0
  160. {sliceagent-0.1.1 → sliceagent-0.1.3}/tests/test_permission_patterns.py +0 -0
  161. {sliceagent-0.1.1 → sliceagent-0.1.3}/tests/test_pinned_composer.py +0 -0
  162. {sliceagent-0.1.1 → sliceagent-0.1.3}/tests/test_plan_tier.py +0 -0
  163. {sliceagent-0.1.1 → sliceagent-0.1.3}/tests/test_procman.py +0 -0
  164. {sliceagent-0.1.1 → sliceagent-0.1.3}/tests/test_product_features.py +0 -0
  165. {sliceagent-0.1.1 → sliceagent-0.1.3}/tests/test_progress_status.py +0 -0
  166. {sliceagent-0.1.1 → sliceagent-0.1.3}/tests/test_prompt_ab.py +0 -0
  167. {sliceagent-0.1.1 → sliceagent-0.1.3}/tests/test_provenance.py +0 -0
  168. {sliceagent-0.1.1 → sliceagent-0.1.3}/tests/test_proxy_interrupt.py +0 -0
  169. {sliceagent-0.1.1 → sliceagent-0.1.3}/tests/test_reach.py +0 -0
  170. {sliceagent-0.1.1 → sliceagent-0.1.3}/tests/test_readonly_subagent.py +0 -0
  171. {sliceagent-0.1.1 → sliceagent-0.1.3}/tests/test_recall_search.py +0 -0
  172. {sliceagent-0.1.1 → sliceagent-0.1.3}/tests/test_records.py +0 -0
  173. {sliceagent-0.1.1 → sliceagent-0.1.3}/tests/test_refault.py +0 -0
  174. {sliceagent-0.1.1 → sliceagent-0.1.3}/tests/test_registry_validation.py +0 -0
  175. {sliceagent-0.1.1 → sliceagent-0.1.3}/tests/test_reliability.py +0 -0
  176. {sliceagent-0.1.1 → sliceagent-0.1.3}/tests/test_requirements.py +0 -0
  177. {sliceagent-0.1.1 → sliceagent-0.1.3}/tests/test_review_fixes.py +0 -0
  178. {sliceagent-0.1.1 → sliceagent-0.1.3}/tests/test_route_lexical.py +0 -0
  179. {sliceagent-0.1.1 → sliceagent-0.1.3}/tests/test_safety.py +0 -0
  180. {sliceagent-0.1.1 → sliceagent-0.1.3}/tests/test_seal_markdown.py +0 -0
  181. {sliceagent-0.1.1 → sliceagent-0.1.3}/tests/test_search_index.py +0 -0
  182. {sliceagent-0.1.1 → sliceagent-0.1.3}/tests/test_skill_meta.py +0 -0
  183. {sliceagent-0.1.1 → sliceagent-0.1.3}/tests/test_skill_metadata.py +0 -0
  184. {sliceagent-0.1.1 → sliceagent-0.1.3}/tests/test_subdir_hints.py +0 -0
  185. {sliceagent-0.1.1 → sliceagent-0.1.3}/tests/test_task_agnostic.py +0 -0
  186. {sliceagent-0.1.1 → sliceagent-0.1.3}/tests/test_task_state_roundtrip.py +0 -0
  187. {sliceagent-0.1.1 → sliceagent-0.1.3}/tests/test_telemetry_convergence.py +0 -0
  188. {sliceagent-0.1.1 → sliceagent-0.1.3}/tests/test_terminal.py +0 -0
  189. {sliceagent-0.1.1 → sliceagent-0.1.3}/tests/test_tool_dedup.py +0 -0
  190. {sliceagent-0.1.1 → sliceagent-0.1.3}/tests/test_tool_result_ok.py +0 -0
  191. {sliceagent-0.1.1 → sliceagent-0.1.3}/tests/test_tools_robust.py +0 -0
  192. {sliceagent-0.1.1 → sliceagent-0.1.3}/tests/test_topic_switch.py +0 -0
  193. {sliceagent-0.1.1 → sliceagent-0.1.3}/tests/test_trajectory.py +0 -0
  194. {sliceagent-0.1.1 → sliceagent-0.1.3}/tests/test_tui.py +0 -0
  195. {sliceagent-0.1.1 → sliceagent-0.1.3}/tests/test_tui_menus.py +0 -0
  196. {sliceagent-0.1.1 → sliceagent-0.1.3}/tests/test_tui_render.py +0 -0
  197. {sliceagent-0.1.1 → sliceagent-0.1.3}/tests/test_tui_widgets.py +0 -0
  198. {sliceagent-0.1.1 → sliceagent-0.1.3}/tests/test_turn_budget.py +0 -0
  199. {sliceagent-0.1.1 → sliceagent-0.1.3}/tests/test_verification_agent.py +0 -0
  200. {sliceagent-0.1.1 → sliceagent-0.1.3}/tests/test_web.py +0 -0
  201. {sliceagent-0.1.1 → sliceagent-0.1.3}/tests/test_workspace.py +0 -0
  202. {sliceagent-0.1.1 → sliceagent-0.1.3}/tests/test_world_region.py +0 -0
  203. {sliceagent-0.1.1 → sliceagent-0.1.3}/tests/test_world_state.py +0 -0
@@ -5,6 +5,39 @@ this project aims for [Semantic Versioning](https://semver.org/).
5
5
 
6
6
  ## [Unreleased]
7
7
 
8
+ ## [0.1.3] — 2026-07-02
9
+
10
+ The five-door provider lineup + a wizard that feels like one.
11
+
12
+ ### Added
13
+ - **Providers**: OpenRouter (hundreds of models, one key — now the first door), OpenAI,
14
+ **Anthropic/Claude** (new — via Anthropic's OpenAI-compatible endpoint), DeepSeek, Moonshot/Kimi,
15
+ plus custom endpoints. All five ride the single adapter — zero new dependencies.
16
+ - **OpenRouter quirks**: reasoning intent maps to OpenRouter's unified `reasoning` object (works
17
+ WITH tools — the raw `reasoning_effort` param never could); tool-calling requests pin routing to
18
+ hosts that honor every param (`require_parameters`) so nothing silently degrades; per-call
19
+ `usage.cost` is parsed into the cost meter.
20
+ - `ANTHROPIC_API_KEY` / `DEEPSEEK_API_KEY` / `OPENROUTER_API_KEY` accepted as key fallbacks.
21
+
22
+ ### Changed
23
+ - **Wizard UX**: API key shows `******` as you type (no more invisible field); provider and model
24
+ are arrow-key menus (↑/↓ + Enter, Esc cancels) with per-provider model suggestions +
25
+ "type another model id…". Scripted/CI runs keep the plain typed flow.
26
+
27
+ ## [0.1.2] — 2026-07-02
28
+
29
+ Nothing may hang the user — three fixes from a live first-run reproduction.
30
+
31
+ ### Fixed
32
+ - **Repo-map walk is hard-bounded** (`max_dirs` budget): the first slice build can no longer hang for
33
+ minutes when the workspace root is huge (a home directory mistaken for a project, a giant monorepo).
34
+ Output caps existed; now the walk itself is bounded — worst case ~1s, maps what it saw.
35
+ - **The OS-account home never counts as a project root**, independent of `$HOME`: a stray
36
+ `package.json` in the real home no longer turns the entire home directory into a "project"
37
+ (the prior guard compared against `$HOME`, which containers/sandboxes/sudo can override).
38
+ - **Ctrl-C during the slice build cancels the turn cleanly** (`· cancelled`) instead of crashing the
39
+ REPL with a traceback — the build phase ran before the turn's interrupt handling.
40
+
8
41
  ## [0.1.1] — 2026-07-02
9
42
 
10
43
  First-run onboarding, hardened by a live stranger walkthrough.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: sliceagent
3
- Version: 0.1.1
3
+ Version: 0.1.3
4
4
  Summary: A coding agent with a new context-engineering framework: bounded, deterministic, reconstructed context (slice architecture) — built for long-horizon work.
5
5
  Author: TT-Wang
6
6
  License-Expression: MIT
@@ -145,11 +145,10 @@ Footprint is light (no torch). `pip install -e .` works for a clone too. Homebre
145
145
  ## Quickstart
146
146
 
147
147
  ```bash
148
- sliceagent init # guided setup: provider, API key, model ~/.sliceagent/config.toml (tests your key)
149
- sliceagent # start the agent
148
+ sliceagent # that's it — first run walks you through setup (provider, API key, live-tested), then you're in
150
149
  ```
151
150
 
152
- `init` writes the config so the next run needs no env vars. Prefer env vars? Export **both** `LLM_API_KEY` and `AGENT_MODEL` (plus `LLM_BASE_URL` for non-OpenAI endpoints) and skip `init` — there is no default model; sliceagent never picks one for you. Discover every setting with `sliceagent config --list`.
151
+ Setup happens once, in-process, and writes `~/.sliceagent/config.toml` (0600) so every later run just starts. Re-configure anytime with `sliceagent init` (add/switch providers). Prefer env vars? Export **both** `LLM_API_KEY` and `AGENT_MODEL` (plus `LLM_BASE_URL` for non-OpenAI endpoints) — there is no default model; sliceagent never picks one for you. Discover every setting with `sliceagent config --list`.
153
152
 
154
153
  → Full walkthrough in **[QUICKSTART.md](QUICKSTART.md)** · **[CONTRIBUTING.md](CONTRIBUTING.md)** · **[CHANGELOG.md](CHANGELOG.md)**
155
154
 
@@ -15,12 +15,14 @@ installed by default; everything still works without it (plain stdout).
15
15
  ## 2. Set up your provider
16
16
 
17
17
  ```bash
18
- sliceagent init
18
+ sliceagent
19
19
  ```
20
20
 
21
- `init` walks you through it: pick a provider (Moonshot/Kimi, OpenAI, DeepSeek, or a custom OpenAI-compatible
22
- endpoint), paste your API key (hidden), choose a model. It then **tests the key with one request** and writes
23
- `~/.sliceagent/config.toml` (mode `0600` — it holds your key). The next `sliceagent` just works.
21
+ Nothing to memorize the **first run walks you through setup automatically**: pick a provider
22
+ (OpenRouter hundreds of models with one key — or OpenAI, Anthropic/Claude, DeepSeek, Moonshot/Kimi, or a custom OpenAI-compatible endpoint), paste your API key (hidden),
23
+ choose a model. It **tests the key with one request**, writes `~/.sliceagent/config.toml` (mode `0600` —
24
+ it holds your key), and drops you straight into the session. Every later `sliceagent` just starts.
25
+ (Re-configure anytime — add or switch providers — with `sliceagent init`.)
24
26
 
25
27
  Prefer environment variables? Skip `init` and export them instead:
26
28
 
@@ -112,11 +112,10 @@ Footprint is light (no torch). `pip install -e .` works for a clone too. Homebre
112
112
  ## Quickstart
113
113
 
114
114
  ```bash
115
- sliceagent init # guided setup: provider, API key, model ~/.sliceagent/config.toml (tests your key)
116
- sliceagent # start the agent
115
+ sliceagent # that's it — first run walks you through setup (provider, API key, live-tested), then you're in
117
116
  ```
118
117
 
119
- `init` writes the config so the next run needs no env vars. Prefer env vars? Export **both** `LLM_API_KEY` and `AGENT_MODEL` (plus `LLM_BASE_URL` for non-OpenAI endpoints) and skip `init` — there is no default model; sliceagent never picks one for you. Discover every setting with `sliceagent config --list`.
118
+ Setup happens once, in-process, and writes `~/.sliceagent/config.toml` (0600) so every later run just starts. Re-configure anytime with `sliceagent init` (add/switch providers). Prefer env vars? Export **both** `LLM_API_KEY` and `AGENT_MODEL` (plus `LLM_BASE_URL` for non-OpenAI endpoints) — there is no default model; sliceagent never picks one for you. Discover every setting with `sliceagent config --list`.
120
119
 
121
120
  → Full walkthrough in **[QUICKSTART.md](QUICKSTART.md)** · **[CONTRIBUTING.md](CONTRIBUTING.md)** · **[CHANGELOG.md](CHANGELOG.md)**
122
121
 
@@ -96,9 +96,8 @@ cat <<'EOF'
96
96
 
97
97
  ✓ sliceagent installed.
98
98
 
99
- Next:
100
- sliceagent init # guided setup: provider, API key, model (tests your key)
101
- sliceagent # start the agent
99
+ Next — just one command:
100
+ sliceagent # first run walks you through setup (provider, API key), then you're chatting
102
101
 
103
102
  If 'sliceagent' isn't found, open a NEW terminal (PATH was just updated).
104
103
  Docs: https://github.com/TT-Wang/sliceagent
@@ -1,3 +1,3 @@
1
1
  """sliceagent — a memory-native coding agent (deterministic, bounded, reconstructed context)."""
2
2
 
3
- __version__ = "0.1.1"
3
+ __version__ = "0.1.3"
@@ -171,7 +171,8 @@ def main() -> None:
171
171
 
172
172
  def _key_present() -> bool:
173
173
  return bool(os.environ.get("LLM_API_KEY") or os.environ.get("OPENAI_API_KEY")
174
- or os.environ.get("MOONSHOT_API_KEY"))
174
+ or os.environ.get("ANTHROPIC_API_KEY") or os.environ.get("DEEPSEEK_API_KEY")
175
+ or os.environ.get("OPENROUTER_API_KEY") or os.environ.get("MOONSHOT_API_KEY"))
175
176
 
176
177
  def _first_run_setup(reason: str) -> bool:
177
178
  """First-run UX: a bare interactive `sliceagent` with nothing configured drops STRAIGHT into the
@@ -831,7 +832,13 @@ def main() -> None:
831
832
  _stats["topic"] = one_line(session.active().goal, 40) if session.active_id else ""
832
833
  record_user(session.active(), line) # short-range continuity: the RECENT CONVERSATION tier
833
834
  _expand_mentions(line) # @path → pin the file into OPEN FILES
834
- build = make_build_slice(session, tools, retriever, memory, line, session.session_id)
835
+ try:
836
+ # slice-build phase happens BEFORE run_turn's own KeyboardInterrupt handling — a ctrl-c
837
+ # here (e.g. during the one-time repo-map build) must cancel the turn, not crash the REPL.
838
+ build = make_build_slice(session, tools, retriever, memory, line, session.session_id)
839
+ except KeyboardInterrupt:
840
+ print("\n · cancelled")
841
+ continue
835
842
  if os.environ.get("AGENT_TIMING"): # per-turn latency breakdown (build vs model) → find the hang
836
843
  import time as _tt
837
844
  _b = build
@@ -81,6 +81,9 @@ def _usage_dict(raw) -> dict | None:
81
81
  }
82
82
  if cache_read:
83
83
  usage["cached_tokens"] = cache_read # legacy key (only when present)
84
+ _cost = getattr(raw, "cost", None) # OpenRouter: authoritative $ per call
85
+ if isinstance(_cost, (int, float)) and _cost >= 0:
86
+ usage["cost_usd"] = float(_cost)
84
87
  return usage
85
88
 
86
89
 
@@ -189,7 +192,9 @@ class OpenAILLM:
189
192
  # export OPENAI_API_KEY) — the surface the user configures says "LLM", not a provider name.
190
193
  # Resolve the ENDPOINT first: the proxy choice below depends on which provider it is.
191
194
  kwargs: dict = {"api_key": api_key or os.environ.get("LLM_API_KEY")
192
- or os.environ.get("OPENAI_API_KEY") or os.environ.get("MOONSHOT_API_KEY")}
195
+ or os.environ.get("OPENAI_API_KEY") or os.environ.get("ANTHROPIC_API_KEY")
196
+ or os.environ.get("DEEPSEEK_API_KEY") or os.environ.get("OPENROUTER_API_KEY")
197
+ or os.environ.get("MOONSHOT_API_KEY")}
193
198
  resolved_base = base_url or os.environ.get("LLM_BASE_URL") or os.environ.get("OPENAI_BASE_URL")
194
199
  if not resolved_base and os.environ.get("MOONSHOT_API_KEY") and not (
195
200
  os.environ.get("LLM_API_KEY") or os.environ.get("OPENAI_API_KEY")):
@@ -466,6 +471,12 @@ class OpenAILLM:
466
471
  from .model_catalog import capability
467
472
  r = self.reasoning
468
473
  model, base = self.model.lower(), self._base_url.lower()
474
+ if "openrouter" in base:
475
+ # OpenRouter's UNIFIED reasoning object translates effort per upstream vendor (OpenAI
476
+ # reasoning_effort / Anthropic thinking budget / Gemini thinkingLevel) and — unlike raw
477
+ # reasoning_effort on chat/completions — works WITH tools. "full" stays provider-default.
478
+ effort = {"fast": "low", "high": "high", "max": "high"}.get(r)
479
+ return {"extra_body": {"reasoning": {"effort": effort}}} if effort else {}
469
480
  if "deepseek" in model or "deepseek" in base:
470
481
  return {"extra_body": {"thinking": {"type": "disabled"}}} if r == "fast" else {}
471
482
  if not capability(self.model, self._base_url).supports_reasoning_effort:
@@ -637,6 +648,12 @@ class OpenAILLM:
637
648
  self._merge_kwargs(kwargs, self._cache_routing_kwargs()) # session-stable cache routing (0 added tokens)
638
649
  self._merge_kwargs(kwargs, self._reasoning_kwargs())
639
650
  self._merge_kwargs(kwargs, self._cache_kwargs(messages))
651
+ if "openrouter" in self._base_url.lower() and tools:
652
+ # OpenRouter routes one model slug across MANY hosts; some serve quantizations with broken
653
+ # tool calling, and unsupported params are DROPPED silently by default. require_parameters
654
+ # pins routing to hosts that honor every param we sent — a silent degrade becomes a visible
655
+ # error instead (the exact failure class behind the old reasoning-effort ceiling).
656
+ self._merge_kwargs(kwargs, {"extra_body": {"provider": {"require_parameters": True}}})
640
657
  # Provider quirk (isolated here, llm-agnostic): some reasoning models (gpt-5.5) reject
641
658
  # reasoning_effort TOGETHER with function tools on /v1/chat/completions (400 — "use /v1/responses").
642
659
  # Once seen, drop reasoning_effort whenever tools are present so we degrade to default reasoning
@@ -13,14 +13,64 @@ import os
13
13
  from .envspec import GROUPS, REGISTRY, current_value
14
14
 
15
15
  # provider presets: key → (label, base_url, default_model). 'custom' prompts for the base_url.
16
+ # The lineup: one AGGREGATOR door (OpenRouter — breadth, one key) + four first-party doors
17
+ # (OpenAI, Anthropic, DeepSeek, Moonshot). Anthropic rides its OpenAI-compatible endpoint, so all
18
+ # five share the single OpenAILLM adapter — no per-provider SDKs.
16
19
  PROVIDERS = {
17
- "1": ("moonshot", "Moonshot (Kimi)", "https://api.moonshot.cn/v1", "kimi-k2.7-code"),
20
+ "1": ("openrouter", "OpenRouter (hundreds of models, one key)", "https://openrouter.ai/api/v1", "openai/gpt-5.5"),
18
21
  "2": ("openai", "OpenAI", "", "gpt-5.5"),
19
- "3": ("deepseek", "DeepSeek", "https://api.deepseek.com/v1", "deepseek-chat"),
20
- "4": ("custom", "Custom OpenAI-compatible endpoint", "", ""),
22
+ "3": ("anthropic", "Anthropic (Claude)", "https://api.anthropic.com/v1", "claude-sonnet-5"),
23
+ "4": ("deepseek", "DeepSeek", "https://api.deepseek.com/v1", "deepseek-chat"),
24
+ "5": ("moonshot", "Moonshot (Kimi)", "https://api.moonshot.cn/v1", "kimi-k2.7-code"),
25
+ "6": ("custom", "Custom OpenAI-compatible endpoint", "", ""),
26
+ }
27
+
28
+ # wizard model MENU per provider — suggestions for the arrow picker, NOT a cap: "type another…"
29
+ # always accepts any model id (the catalog/pricing layers stay the capability source of truth).
30
+ MODEL_SUGGESTIONS = {
31
+ "openrouter": ["openai/gpt-5.5", "anthropic/claude-sonnet-5", "deepseek/deepseek-chat",
32
+ "moonshotai/kimi-k2.7"],
33
+ "openai": ["gpt-5.5", "gpt-5", "gpt-5-mini", "o3"],
34
+ "anthropic": ["claude-sonnet-5", "claude-opus-4-8", "claude-haiku-4-5"],
35
+ "deepseek": ["deepseek-chat", "deepseek-reasoner"],
36
+ "moonshot": ["kimi-k2.7-code", "kimi-k2-0905-preview"],
37
+ "custom": [],
21
38
  }
22
39
 
23
40
 
41
+ def _tty() -> bool:
42
+ import sys
43
+ try:
44
+ return sys.stdin.isatty() and sys.stdout.isatty()
45
+ except Exception: # noqa: BLE001
46
+ return False
47
+
48
+
49
+ def _masked_input(prompt_text: str, fallback):
50
+ """API-key entry with VISIBLE feedback — asterisks per keystroke (prompt_toolkit is_password)
51
+ instead of getpass's fully-invisible field, which first-run users read as 'nothing is happening'.
52
+ Falls back to getpass when the TUI stack is absent or anything goes wrong."""
53
+ try:
54
+ from prompt_toolkit import prompt as _ptk_prompt
55
+ return _ptk_prompt(prompt_text, is_password=True)
56
+ except (ImportError, OSError, EOFError):
57
+ return fallback(prompt_text)
58
+
59
+
60
+ def _pick(options: list, default: int = 0):
61
+ """Arrow-key selector for the wizard (reuses the TUI's _arrow_select: ↑/↓ or ←/→ + Enter).
62
+ Returns the index; raises KeyboardInterrupt on Esc (→ the wizard's normal 'cancelled' path);
63
+ returns None when a selector can't safely run so the caller falls back to typed input."""
64
+ try:
65
+ from .tui import _arrow_select
66
+ except ImportError:
67
+ return None
68
+ idx = _arrow_select(options, default=default)
69
+ if idx == -1:
70
+ raise KeyboardInterrupt
71
+ return idx
72
+
73
+
24
74
  def _version() -> str:
25
75
  try:
26
76
  from . import __version__
@@ -182,11 +232,24 @@ def run_init(*, inp=input, getpw=None, llm_factory=None, home=None) -> int:
182
232
  if ans in ("n", "no"):
183
233
  out(" Leaving the existing config unchanged. Run `sliceagent` to start."); return 0
184
234
 
185
- out("\n Choose a provider:")
186
- for k, (_id, label, base, model) in PROVIDERS.items():
187
- out(f" {k}. {label}" + (f" ({model})" if model else ""))
235
+ # arrow-key niceties only on a REAL interactive run with the default input hooks — injected
236
+ # inp/getpw (tests, scripted runs) keep the typed flow byte-for-byte.
237
+ fancy = inp is input and getpw is __import__("getpass").getpass and _tty()
238
+ keys = sorted(PROVIDERS)
188
239
  try:
189
- choice = inp(" > ").strip() or "1"
240
+ out("\n Choose a provider (↑/↓ + Enter):" if fancy else "\n Choose a provider:")
241
+ idx = None
242
+ if fancy:
243
+ labels = [f"{PROVIDERS[k][1]}" + (f" — {PROVIDERS[k][3]}" if PROVIDERS[k][3] else "")
244
+ for k in keys]
245
+ idx = _pick(labels) # Esc → KeyboardInterrupt → 'cancelled' below
246
+ if idx is not None:
247
+ choice = keys[idx]
248
+ else: # typed fallback (no tty / selector unavailable)
249
+ for k in keys:
250
+ _id, label, _base, model0 = PROVIDERS[k]
251
+ out(f" {k}. {label}" + (f" ({model0})" if model0 else ""))
252
+ choice = inp(" > ").strip() or "1"
190
253
  pid, label, base_url, model = PROVIDERS.get(choice, PROVIDERS["1"])
191
254
  if pid == "custom":
192
255
  base_url = inp(" Base URL (OpenAI-compatible, e.g. https://host/v1): ").strip()
@@ -198,10 +261,27 @@ def run_init(*, inp=input, getpw=None, llm_factory=None, home=None) -> int:
198
261
  existing = existing.get(pid) if isinstance(existing, dict) else None
199
262
  existing = existing if isinstance(existing, dict) else {}
200
263
  existing_key = existing.get("api_key") or ""
201
- key_prompt = " API key (hidden, Enter to keep existing): " if existing_key else " API key (hidden): "
202
- key = getpw(key_prompt).strip() or existing_key
203
- model = (inp(f" Model [{existing.get('model') or model or 'required'}]: ").strip()
204
- or existing.get("model") or model)
264
+ key_prompt = (" API key (typed as ******, Enter to keep existing): " if existing_key
265
+ else " API key (typed as ******): ") if fancy else (
266
+ " API key (hidden, Enter to keep existing): " if existing_key
267
+ else " API key (hidden): ")
268
+ key = (_masked_input(key_prompt, getpw) if fancy else getpw(key_prompt)).strip() or existing_key
269
+ picked_model = None
270
+ if fancy:
271
+ cur = existing.get("model") or model
272
+ opts = [m for m in ([cur] if cur else []) + MODEL_SUGGESTIONS.get(pid, []) if m]
273
+ opts = list(dict.fromkeys(opts)) + ["type another model id…"]
274
+ out(" Model (↑/↓ + Enter):")
275
+ midx = _pick(opts)
276
+ if midx is not None:
277
+ picked_model = None if opts[midx].startswith("type another") else opts[midx]
278
+ if picked_model is None:
279
+ picked_model = inp(" Model id: ").strip()
280
+ if picked_model:
281
+ model = picked_model
282
+ else:
283
+ model = (inp(f" Model [{existing.get('model') or model or 'required'}]: ").strip()
284
+ or existing.get("model") or model)
205
285
  except (EOFError, KeyboardInterrupt):
206
286
  out("\n cancelled."); return 1
207
287
  if not key:
@@ -94,6 +94,17 @@ def _home() -> Optional[Path]:
94
94
  return None
95
95
 
96
96
 
97
+ def _os_home() -> Optional[Path]:
98
+ """The account's home from the OS user database — independent of the $HOME env var. Guards the
99
+ home-is-not-a-project rule even when $HOME is overridden (containers, sudo, test sandboxes):
100
+ without this, a stray package.json in the real home turns the whole home dir into a 'project'."""
101
+ try:
102
+ import pwd
103
+ return Path(pwd.getpwuid(os.getuid()).pw_dir).resolve()
104
+ except (ImportError, KeyError, OSError): # pwd is POSIX-only; degrade to the env-based home
105
+ return None
106
+
107
+
97
108
  def _marker_root(cwd: Path) -> Optional[Path]:
98
109
  """Nearest ancestor (≤6 levels) that looks like a project root, or ``None``.
99
110
 
@@ -104,11 +115,11 @@ def _marker_root(cwd: Path) -> Optional[Path]:
104
115
  current = cwd.resolve()
105
116
  except (OSError, RuntimeError, ValueError):
106
117
  return None
107
- home = _home()
118
+ homes = {h for h in (_home(), _os_home()) if h is not None}
108
119
  for depth, parent in enumerate([current, *current.parents]):
109
120
  if depth > 6:
110
121
  break
111
- if parent == home:
122
+ if parent in homes:
112
123
  continue
113
124
  try:
114
125
  for marker in _PROJECT_MARKERS:
@@ -443,7 +454,8 @@ _CODE_SUFFIX = (".py", ".js", ".ts", ".jsx", ".tsx", ".go", ".rs", ".java", ".rb
443
454
  ".exs", ".clj", ".r", ".jl", ".vue", ".sql")
444
455
 
445
456
 
446
- def repo_map(root: str, *, max_entries: int = 300, max_per_dir: int = 25, max_chars: int = 12000) -> str:
457
+ def repo_map(root: str, *, max_entries: int = 300, max_per_dir: int = 25, max_chars: int = 12000,
458
+ max_dirs: int = 4000) -> str:
447
459
  """A compact, ignore-aware STRUCTURAL MAP of the project (SENSORY CORTEX — the derived-view tier-B
448
460
  resident view, memoized for the session, never a persisted store): directories with their files,
449
461
  pruned of VCS/venv/cache + asset/log noise, RANKED by source-density so the real code tree shows
@@ -456,8 +468,17 @@ def repo_map(root: str, *, max_entries: int = 300, max_per_dir: int = 25, max_ch
456
468
  if not root or not os.path.isdir(root):
457
469
  return ""
458
470
  rows: list[tuple[str, list[str], int, int]] = [] # (rel, files, total, code_count)
471
+ walked = 0
459
472
  try:
460
473
  for dirpath, dirnames, filenames in os.walk(root): # symlinks not followed
474
+ # HARD WALK BUDGET — the invariant is that repo_map completes in bounded time on ANY root.
475
+ # max_entries/max_chars cap the OUTPUT but only after the walk; without this, a huge root
476
+ # (a home dir mistaken for a project, a giant monorepo) hangs the first slice build for
477
+ # minutes inside os.walk. Past the budget we map what we saw — bounded beats complete.
478
+ walked += 1
479
+ if walked > max_dirs:
480
+ dirnames[:] = []
481
+ break
461
482
  dirnames[:] = sorted(d for d in dirnames if not _is_ignored(d))
462
483
  files = sorted(f for f in filenames
463
484
  if not _is_ignored(f) and not f.endswith(_MAP_SKIP_SUFFIX))
@@ -134,7 +134,7 @@ class _OkLLM:
134
134
  def init_writes_a_valid_config_on_success():
135
135
  home = tempfile.mkdtemp(prefix="init-")
136
136
  rc = onboarding.run_init(
137
- inp=_seq("1", ""), # provider=moonshot, model=default
137
+ inp=_seq("5", ""), # provider=moonshot (menu slot 5), model=default
138
138
  getpw=_seq("sk-test-key"),
139
139
  llm_factory=lambda model: _OkLLM(),
140
140
  home=home)
@@ -180,7 +180,7 @@ def init_offers_to_save_after_a_failed_key_test():
180
180
  def init_custom_provider_prompts_base_url():
181
181
  home = tempfile.mkdtemp(prefix="init-custom-")
182
182
  rc = onboarding.run_init(
183
- inp=_seq("4", "https://my.host/v1", "my-model"), # custom → base url → model
183
+ inp=_seq("6", "https://my.host/v1", "my-model"), # custom → base url → model (menu: 6 = custom)
184
184
  getpw=_seq("k"),
185
185
  llm_factory=lambda model: _OkLLM(),
186
186
  home=home)
@@ -193,7 +193,7 @@ def init_custom_provider_prompts_base_url():
193
193
  @check
194
194
  def init_aborts_without_a_key():
195
195
  home = tempfile.mkdtemp(prefix="init-nokey-")
196
- rc = onboarding.run_init(inp=_seq("1", ""), getpw=_seq(""), llm_factory=lambda m: _OkLLM(), home=home)
196
+ rc = onboarding.run_init(inp=_seq("5", ""), getpw=_seq(""), llm_factory=lambda m: _OkLLM(), home=home)
197
197
  assert rc == 1, "no key → abort"
198
198
  assert not os.path.exists(os.path.join(home, ".sliceagent", "config.toml"))
199
199
 
@@ -224,7 +224,7 @@ def write_config_is_atomic_and_cleans_up_on_failure():
224
224
  @check
225
225
  def init_merge_keeps_existing_providers():
226
226
  home = tempfile.mkdtemp(prefix="init-merge-")
227
- onboarding.run_init(inp=_seq("1", ""), getpw=_seq("k-moon"), llm_factory=lambda m: _OkLLM(), home=home)
227
+ onboarding.run_init(inp=_seq("5", ""), getpw=_seq("k-moon"), llm_factory=lambda m: _OkLLM(), home=home)
228
228
  # 2nd init on the existing config: "Add/update? [Y/n]" → "" (yes) → provider 2 (openai) → model ""
229
229
  onboarding.run_init(inp=_seq("", "2", ""), getpw=_seq("k-oai"), llm_factory=lambda m: _OkLLM(), home=home)
230
230
  data = tomllib.load(open(os.path.join(home, ".sliceagent", "config.toml"), "rb"))
@@ -239,10 +239,10 @@ def reinit_blank_key_keeps_existing_key_for_the_same_provider():
239
239
  # prompt (no retype) must KEEP the saved key/model, not abort — the abort-on-blank behavior
240
240
  # (init_aborts_without_a_key) is only correct for a provider with no existing entry.
241
241
  home = tempfile.mkdtemp(prefix="init-reblank-")
242
- onboarding.run_init(inp=_seq("1", ""), getpw=_seq("k-moon"), llm_factory=lambda m: _OkLLM(), home=home)
242
+ onboarding.run_init(inp=_seq("5", ""), getpw=_seq("k-moon"), llm_factory=lambda m: _OkLLM(), home=home)
243
243
  # 2nd init: "Add/update? [Y/n]" → "" (yes) → provider 1 (moonshot, already saved) → key "" (blank,
244
244
  # keep existing) → model "" (blank, keep existing)
245
- rc = onboarding.run_init(inp=_seq("", "1", ""), getpw=_seq(""), llm_factory=lambda m: _OkLLM(), home=home)
245
+ rc = onboarding.run_init(inp=_seq("", "5", ""), getpw=_seq(""), llm_factory=lambda m: _OkLLM(), home=home)
246
246
  assert rc == 0, "blank key on an already-configured provider must NOT abort"
247
247
  data = tomllib.load(open(os.path.join(home, ".sliceagent", "config.toml"), "rb"))
248
248
  assert data["providers"]["moonshot"]["api_key"] == "k-moon", "the existing key must be kept, not wiped"
@@ -252,7 +252,7 @@ def reinit_blank_key_keeps_existing_key_for_the_same_provider():
252
252
  @check
253
253
  def config_use_switches_default_provider():
254
254
  home = tempfile.mkdtemp(prefix="cfg-use-")
255
- onboarding.run_init(inp=_seq("1", ""), getpw=_seq("k1"), llm_factory=lambda m: _OkLLM(), home=home)
255
+ onboarding.run_init(inp=_seq("5", ""), getpw=_seq("k1"), llm_factory=lambda m: _OkLLM(), home=home)
256
256
  onboarding.run_init(inp=_seq("", "2", ""), getpw=_seq("k2"), llm_factory=lambda m: _OkLLM(), home=home)
257
257
  assert onboarding.run_config(["--use", "moonshot"], home=home) == 0
258
258
  data = tomllib.load(open(os.path.join(home, ".sliceagent", "config.toml"), "rb"))
@@ -0,0 +1,95 @@
1
+ """The five-door provider lineup (OpenRouter + OpenAI/Anthropic/DeepSeek/Moonshot + custom) and the
2
+ OpenRouter/Anthropic adapter quirks. All offline — stub objects, no network, no pytest.
3
+ Run: PYTHONPATH=src python tests/test_provider_lineup.py
4
+ """
5
+ import os
6
+ import sys
7
+ from types import SimpleNamespace as NS
8
+
9
+ sys.path.insert(0, os.path.join(os.path.dirname(__file__), "..", "src"))
10
+
11
+ from sliceagent.llm import OpenAILLM, _usage_dict # noqa: E402
12
+ from sliceagent.onboarding import MODEL_SUGGESTIONS, PROVIDERS # noqa: E402
13
+
14
+ CHECKS = []
15
+ def check(fn):
16
+ CHECKS.append(fn)
17
+ return fn
18
+
19
+
20
+ def _stub(model: str, base: str, reasoning: str = "full") -> OpenAILLM:
21
+ obj = OpenAILLM.__new__(OpenAILLM) # bypass __init__ (no network)
22
+ obj.model = model
23
+ obj._base_url = base
24
+ obj.reasoning = reasoning
25
+ return obj
26
+
27
+
28
+ @check
29
+ def lineup_is_the_agreed_five_doors_plus_custom():
30
+ ids = [PROVIDERS[k][0] for k in sorted(PROVIDERS)]
31
+ assert ids == ["openrouter", "openai", "anthropic", "deepseek", "moonshot", "custom"], ids
32
+ for _pid, _label, base, model in PROVIDERS.values():
33
+ if _pid != "custom":
34
+ assert model, f"{_pid} preset needs a default model"
35
+ assert PROVIDERS["3"][2] == "https://api.anthropic.com/v1" # Claude via the OpenAI-compat endpoint
36
+ assert all(PROVIDERS[k][0] in MODEL_SUGGESTIONS for k in PROVIDERS), "every preset needs a model menu"
37
+
38
+
39
+ @check
40
+ def openrouter_reasoning_maps_to_the_unified_object():
41
+ llm = _stub("anthropic/claude-sonnet-5", "https://openrouter.ai/api/v1", reasoning="fast")
42
+ assert llm._reasoning_kwargs() == {"extra_body": {"reasoning": {"effort": "low"}}}
43
+ llm.reasoning = "high"
44
+ assert llm._reasoning_kwargs() == {"extra_body": {"reasoning": {"effort": "high"}}}
45
+ llm.reasoning = "full" # default → provider default, no param at all (never force spend)
46
+ assert llm._reasoning_kwargs() == {}
47
+
48
+
49
+ @check
50
+ def openrouter_never_sends_raw_reasoning_effort():
51
+ # the raw chat param is exactly what 400s/DROPS silently upstream — the unified object replaces it
52
+ llm = _stub("openai/gpt-5.5", "https://openrouter.ai/api/v1", reasoning="max")
53
+ kw = llm._reasoning_kwargs()
54
+ assert "reasoning_effort" not in kw and kw["extra_body"]["reasoning"]["effort"] == "high"
55
+
56
+
57
+ @check
58
+ def anthropic_direct_stays_on_provider_defaults():
59
+ # claude via api.anthropic.com: no reasoning_effort (capability=False), no prompt_cache_key routing
60
+ llm = _stub("claude-sonnet-5", "https://api.anthropic.com/v1", reasoning="high")
61
+ assert llm._reasoning_kwargs() == {}, "anthropic family must not receive reasoning_effort"
62
+ assert llm._cache_routing_kwargs() == {}, "OpenAI cache routing keys must not go to Anthropic"
63
+
64
+
65
+ @check
66
+ def usage_dict_parses_openrouter_cost():
67
+ raw = NS(prompt_tokens=100, completion_tokens=10, prompt_tokens_details=None,
68
+ cache_creation_input_tokens=0, cost=0.0042)
69
+ u = _usage_dict(raw)
70
+ assert u["cost_usd"] == 0.0042 and u["prompt_tokens"] == 100
71
+ raw_no_cost = NS(prompt_tokens=1, completion_tokens=1, prompt_tokens_details=None,
72
+ cache_creation_input_tokens=0)
73
+ assert "cost_usd" not in _usage_dict(raw_no_cost), "absent cost must not fabricate a field"
74
+
75
+
76
+ @check
77
+ def openrouter_tools_requests_pin_param_honoring_hosts():
78
+ llm = _stub("openai/gpt-5.5", "https://openrouter.ai/api/v1", reasoning="fast")
79
+ kwargs = {"extra_body": {}}
80
+ llm._merge_kwargs(kwargs, llm._reasoning_kwargs())
81
+ llm._merge_kwargs(kwargs, {"extra_body": {"provider": {"require_parameters": True}}})
82
+ eb = kwargs["extra_body"]
83
+ assert eb["reasoning"]["effort"] == "low" and eb["provider"]["require_parameters"] is True, \
84
+ "both quirks must coexist in one extra_body (deep merge)"
85
+
86
+
87
+ if __name__ == "__main__":
88
+ ok = 0
89
+ for fn in CHECKS:
90
+ try:
91
+ fn(); ok += 1; print(f"PASS {fn.__name__}")
92
+ except Exception as e: # noqa: BLE001
93
+ print(f"FAIL {fn.__name__}: {type(e).__name__}: {e}")
94
+ print(f"\n{ok}/{len(CHECKS)} passed")
95
+ sys.exit(0 if ok == len(CHECKS) else 1)
@@ -0,0 +1,91 @@
1
+ """The repo-map walk must be BOUNDED — it can never hang the first slice build, no matter what the
2
+ workspace root is (a huge monorepo, or a home dir that a stray package.json turned into a 'project').
3
+ Found live: launching from /Users/<u> with HOME overridden made os.walk crawl the entire home dir for
4
+ minutes; ctrl-c then killed the REPL with a traceback. Invariant: repo_map completes within its walk
5
+ budget on ANY root, and the OS-account home never counts as a project root regardless of $HOME.
6
+ Run: PYTHONPATH=src python tests/test_repo_map_bounded.py
7
+ """
8
+ import os
9
+ import pathlib
10
+ import sys
11
+ import time
12
+
13
+ sys.path.insert(0, os.path.join(os.path.dirname(__file__), "..", "src"))
14
+
15
+ from sliceagent import sensory_cortex as sc # noqa: E402
16
+
17
+ CHECKS = []
18
+ def check(fn):
19
+ CHECKS.append(fn)
20
+ return fn
21
+
22
+
23
+ def _make_tree(root: pathlib.Path, n_dirs: int) -> None:
24
+ for i in range(n_dirs):
25
+ d = root / f"pkg{i:03d}"
26
+ d.mkdir(parents=True)
27
+ (d / "mod.py").write_text("x = 1\n")
28
+
29
+
30
+ @check
31
+ def walk_stops_at_the_dir_budget():
32
+ import tempfile
33
+ root = pathlib.Path(tempfile.mkdtemp())
34
+ _make_tree(root, 60)
35
+ out = sc.repo_map(str(root), max_dirs=10)
36
+ # bounded: only what the budget allowed got mapped (root dir itself may count as one visit)
37
+ mapped = [ln for ln in out.splitlines() if ln.strip()]
38
+ assert 0 < len(mapped) <= 10, f"expected ≤10 mapped dirs under a 10-dir budget, got {len(mapped)}"
39
+
40
+
41
+ @check
42
+ def bounded_walk_is_fast_even_on_a_big_tree():
43
+ import tempfile
44
+ root = pathlib.Path(tempfile.mkdtemp())
45
+ _make_tree(root, 300)
46
+ t0 = time.monotonic()
47
+ out = sc.repo_map(str(root), max_dirs=50)
48
+ dt = time.monotonic() - t0
49
+ assert out, "map must still be produced within the budget"
50
+ assert dt < 5.0, f"bounded walk took {dt:.1f}s — the budget is not bounding"
51
+
52
+
53
+ @check
54
+ def default_budget_maps_a_normal_project_fully():
55
+ import tempfile
56
+ root = pathlib.Path(tempfile.mkdtemp())
57
+ _make_tree(root, 20)
58
+ out = sc.repo_map(str(root)) # default max_dirs — far above a normal project
59
+ assert sum(1 for ln in out.splitlines() if "mod.py" in ln) == 20, "normal projects must be unaffected"
60
+
61
+
62
+ @check
63
+ def os_account_home_never_counts_as_a_project_even_with_home_overridden():
64
+ import tempfile
65
+ fake_os_home = pathlib.Path(tempfile.mkdtemp()) # stands in for pw_dir
66
+ (fake_os_home / "package.json").write_text("{}") # the stray marker that caused the live bug
67
+ orig = sc._os_home
68
+ sc._os_home = lambda: fake_os_home.resolve()
69
+ try:
70
+ # $HOME points elsewhere (the live-repro condition), yet the OS home must still be skipped
71
+ assert sc._marker_root(fake_os_home) is None, \
72
+ "a marker in the OS-account home must not make the home a project"
73
+ # ...while a real project UNDER the home is still detected
74
+ proj = fake_os_home / "code" / "myproj"
75
+ proj.mkdir(parents=True)
76
+ (proj / "pyproject.toml").write_text("[project]\nname='x'\n")
77
+ got = sc._marker_root(proj)
78
+ assert got is not None and got.name == "myproj", f"real subproject must still resolve, got {got}"
79
+ finally:
80
+ sc._os_home = orig
81
+
82
+
83
+ if __name__ == "__main__":
84
+ ok = 0
85
+ for fn in CHECKS:
86
+ try:
87
+ fn(); ok += 1; print(f"PASS {fn.__name__}")
88
+ except Exception as e: # noqa: BLE001
89
+ print(f"FAIL {fn.__name__}: {type(e).__name__}: {e}")
90
+ print(f"\n{ok}/{len(CHECKS)} passed")
91
+ sys.exit(0 if ok == len(CHECKS) else 1)
File without changes
File without changes