sliceagent 0.1.6__tar.gz → 0.1.8__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 (206) hide show
  1. {sliceagent-0.1.6 → sliceagent-0.1.8}/CHANGELOG.md +32 -0
  2. {sliceagent-0.1.6 → sliceagent-0.1.8}/PKG-INFO +1 -1
  3. {sliceagent-0.1.6 → sliceagent-0.1.8}/src/sliceagent/__init__.py +1 -1
  4. {sliceagent-0.1.6 → sliceagent-0.1.8}/src/sliceagent/cli.py +40 -10
  5. {sliceagent-0.1.6 → sliceagent-0.1.8}/src/sliceagent/config.py +6 -1
  6. {sliceagent-0.1.6 → sliceagent-0.1.8}/src/sliceagent/llm.py +6 -0
  7. {sliceagent-0.1.6 → sliceagent-0.1.8}/src/sliceagent/onboarding.py +57 -7
  8. {sliceagent-0.1.6 → sliceagent-0.1.8}/src/sliceagent/policy.py +28 -4
  9. {sliceagent-0.1.6 → sliceagent-0.1.8}/src/sliceagent/tui.py +10 -4
  10. {sliceagent-0.1.6 → sliceagent-0.1.8}/tests/test_bughunt_fixes.py +4 -1
  11. sliceagent-0.1.8/tests/test_config_journey.py +260 -0
  12. sliceagent-0.1.6/tests/test_config_journey.py +0 -107
  13. {sliceagent-0.1.6 → sliceagent-0.1.8}/.dockerignore +0 -0
  14. {sliceagent-0.1.6 → sliceagent-0.1.8}/.env.example +0 -0
  15. {sliceagent-0.1.6 → sliceagent-0.1.8}/.github/ISSUE_TEMPLATE/bug_report.md +0 -0
  16. {sliceagent-0.1.6 → sliceagent-0.1.8}/.github/ISSUE_TEMPLATE/feature_request.md +0 -0
  17. {sliceagent-0.1.6 → sliceagent-0.1.8}/.github/PULL_REQUEST_TEMPLATE.md +0 -0
  18. {sliceagent-0.1.6 → sliceagent-0.1.8}/.github/dependabot.yml +0 -0
  19. {sliceagent-0.1.6 → sliceagent-0.1.8}/.github/workflows/ci.yml +0 -0
  20. {sliceagent-0.1.6 → sliceagent-0.1.8}/.github/workflows/publish.yml +0 -0
  21. {sliceagent-0.1.6 → sliceagent-0.1.8}/.gitignore +0 -0
  22. {sliceagent-0.1.6 → sliceagent-0.1.8}/CODE_OF_CONDUCT.md +0 -0
  23. {sliceagent-0.1.6 → sliceagent-0.1.8}/CONTRIBUTING.md +0 -0
  24. {sliceagent-0.1.6 → sliceagent-0.1.8}/Dockerfile +0 -0
  25. {sliceagent-0.1.6 → sliceagent-0.1.8}/LICENSE +0 -0
  26. {sliceagent-0.1.6 → sliceagent-0.1.8}/NOTICE +0 -0
  27. {sliceagent-0.1.6 → sliceagent-0.1.8}/QUICKSTART.md +0 -0
  28. {sliceagent-0.1.6 → sliceagent-0.1.8}/README.md +0 -0
  29. {sliceagent-0.1.6 → sliceagent-0.1.8}/SECURITY.md +0 -0
  30. {sliceagent-0.1.6 → sliceagent-0.1.8}/examples/plugins/hello/__init__.py +0 -0
  31. {sliceagent-0.1.6 → sliceagent-0.1.8}/examples/plugins/hello/plugin.toml +0 -0
  32. {sliceagent-0.1.6 → sliceagent-0.1.8}/install.sh +0 -0
  33. {sliceagent-0.1.6 → sliceagent-0.1.8}/pyproject.toml +0 -0
  34. {sliceagent-0.1.6 → sliceagent-0.1.8}/scripts/gen_config_reference.py +0 -0
  35. {sliceagent-0.1.6 → sliceagent-0.1.8}/scripts/run_tests.sh +0 -0
  36. {sliceagent-0.1.6 → sliceagent-0.1.8}/sliceagent.toml.example +0 -0
  37. {sliceagent-0.1.6 → sliceagent-0.1.8}/src/sliceagent/__main__.py +0 -0
  38. {sliceagent-0.1.6 → sliceagent-0.1.8}/src/sliceagent/access.py +0 -0
  39. {sliceagent-0.1.6 → sliceagent-0.1.8}/src/sliceagent/agents.py +0 -0
  40. {sliceagent-0.1.6 → sliceagent-0.1.8}/src/sliceagent/background_review.py +0 -0
  41. {sliceagent-0.1.6 → sliceagent-0.1.8}/src/sliceagent/binsniff.py +0 -0
  42. {sliceagent-0.1.6 → sliceagent-0.1.8}/src/sliceagent/clock.py +0 -0
  43. {sliceagent-0.1.6 → sliceagent-0.1.8}/src/sliceagent/code_grep.py +0 -0
  44. {sliceagent-0.1.6 → sliceagent-0.1.8}/src/sliceagent/code_index.py +0 -0
  45. {sliceagent-0.1.6 → sliceagent-0.1.8}/src/sliceagent/context_overflow.py +0 -0
  46. {sliceagent-0.1.6 → sliceagent-0.1.8}/src/sliceagent/envspec.py +0 -0
  47. {sliceagent-0.1.6 → sliceagent-0.1.8}/src/sliceagent/errors.py +0 -0
  48. {sliceagent-0.1.6 → sliceagent-0.1.8}/src/sliceagent/events.py +0 -0
  49. {sliceagent-0.1.6 → sliceagent-0.1.8}/src/sliceagent/finding_types.py +0 -0
  50. {sliceagent-0.1.6 → sliceagent-0.1.8}/src/sliceagent/flags.py +0 -0
  51. {sliceagent-0.1.6 → sliceagent-0.1.8}/src/sliceagent/fuzzy.py +0 -0
  52. {sliceagent-0.1.6 → sliceagent-0.1.8}/src/sliceagent/guardrails.py +0 -0
  53. {sliceagent-0.1.6 → sliceagent-0.1.8}/src/sliceagent/guidance.py +0 -0
  54. {sliceagent-0.1.6 → sliceagent-0.1.8}/src/sliceagent/hippocampus.py +0 -0
  55. {sliceagent-0.1.6 → sliceagent-0.1.8}/src/sliceagent/hooks.py +0 -0
  56. {sliceagent-0.1.6 → sliceagent-0.1.8}/src/sliceagent/interfaces.py +0 -0
  57. {sliceagent-0.1.6 → sliceagent-0.1.8}/src/sliceagent/loop.py +0 -0
  58. {sliceagent-0.1.6 → sliceagent-0.1.8}/src/sliceagent/mcp_client.py +0 -0
  59. {sliceagent-0.1.6 → sliceagent-0.1.8}/src/sliceagent/mcp_security.py +0 -0
  60. {sliceagent-0.1.6 → sliceagent-0.1.8}/src/sliceagent/memory.py +0 -0
  61. {sliceagent-0.1.6 → sliceagent-0.1.8}/src/sliceagent/metrics.py +0 -0
  62. {sliceagent-0.1.6 → sliceagent-0.1.8}/src/sliceagent/model_catalog.py +0 -0
  63. {sliceagent-0.1.6 → sliceagent-0.1.8}/src/sliceagent/monitor.py +0 -0
  64. {sliceagent-0.1.6 → sliceagent-0.1.8}/src/sliceagent/neocortex.py +0 -0
  65. {sliceagent-0.1.6 → sliceagent-0.1.8}/src/sliceagent/oracle.py +0 -0
  66. {sliceagent-0.1.6 → sliceagent-0.1.8}/src/sliceagent/pagetable.py +0 -0
  67. {sliceagent-0.1.6 → sliceagent-0.1.8}/src/sliceagent/pfc.py +0 -0
  68. {sliceagent-0.1.6 → sliceagent-0.1.8}/src/sliceagent/plugins.py +0 -0
  69. {sliceagent-0.1.6 → sliceagent-0.1.8}/src/sliceagent/procman.py +0 -0
  70. {sliceagent-0.1.6 → sliceagent-0.1.8}/src/sliceagent/prompt.py +0 -0
  71. {sliceagent-0.1.6 → sliceagent-0.1.8}/src/sliceagent/records.py +0 -0
  72. {sliceagent-0.1.6 → sliceagent-0.1.8}/src/sliceagent/recovery.py +0 -0
  73. {sliceagent-0.1.6 → sliceagent-0.1.8}/src/sliceagent/regions.py +0 -0
  74. {sliceagent-0.1.6 → sliceagent-0.1.8}/src/sliceagent/registry.py +0 -0
  75. {sliceagent-0.1.6 → sliceagent-0.1.8}/src/sliceagent/retriever.py +0 -0
  76. {sliceagent-0.1.6 → sliceagent-0.1.8}/src/sliceagent/safety.py +0 -0
  77. {sliceagent-0.1.6 → sliceagent-0.1.8}/src/sliceagent/sandbox.py +0 -0
  78. {sliceagent-0.1.6 → sliceagent-0.1.8}/src/sliceagent/scheduler.py +0 -0
  79. {sliceagent-0.1.6 → sliceagent-0.1.8}/src/sliceagent/search_index.py +0 -0
  80. {sliceagent-0.1.6 → sliceagent-0.1.8}/src/sliceagent/seed.py +0 -0
  81. {sliceagent-0.1.6 → sliceagent-0.1.8}/src/sliceagent/sensory_cortex.py +0 -0
  82. {sliceagent-0.1.6 → sliceagent-0.1.8}/src/sliceagent/session.py +0 -0
  83. {sliceagent-0.1.6 → sliceagent-0.1.8}/src/sliceagent/skill_provenance.py +0 -0
  84. {sliceagent-0.1.6 → sliceagent-0.1.8}/src/sliceagent/skill_usage.py +0 -0
  85. {sliceagent-0.1.6 → sliceagent-0.1.8}/src/sliceagent/skills.py +0 -0
  86. {sliceagent-0.1.6 → sliceagent-0.1.8}/src/sliceagent/subagent.py +0 -0
  87. {sliceagent-0.1.6 → sliceagent-0.1.8}/src/sliceagent/subdir_hints.py +0 -0
  88. {sliceagent-0.1.6 → sliceagent-0.1.8}/src/sliceagent/swap.py +0 -0
  89. {sliceagent-0.1.6 → sliceagent-0.1.8}/src/sliceagent/taskstate.py +0 -0
  90. {sliceagent-0.1.6 → sliceagent-0.1.8}/src/sliceagent/telemetry.py +0 -0
  91. {sliceagent-0.1.6 → sliceagent-0.1.8}/src/sliceagent/terminal.py +0 -0
  92. {sliceagent-0.1.6 → sliceagent-0.1.8}/src/sliceagent/text_utils.py +0 -0
  93. {sliceagent-0.1.6 → sliceagent-0.1.8}/src/sliceagent/tool_summary.py +0 -0
  94. {sliceagent-0.1.6 → sliceagent-0.1.8}/src/sliceagent/tools.py +0 -0
  95. {sliceagent-0.1.6 → sliceagent-0.1.8}/src/sliceagent/web.py +0 -0
  96. {sliceagent-0.1.6 → sliceagent-0.1.8}/tests/README.md +0 -0
  97. {sliceagent-0.1.6 → sliceagent-0.1.8}/tests/test_active_focus.py +0 -0
  98. {sliceagent-0.1.6 → sliceagent-0.1.8}/tests/test_agents.py +0 -0
  99. {sliceagent-0.1.6 → sliceagent-0.1.8}/tests/test_ask_echo.py +0 -0
  100. {sliceagent-0.1.6 → sliceagent-0.1.8}/tests/test_ask_user.py +0 -0
  101. {sliceagent-0.1.6 → sliceagent-0.1.8}/tests/test_background_review.py +0 -0
  102. {sliceagent-0.1.6 → sliceagent-0.1.8}/tests/test_binary_view.py +0 -0
  103. {sliceagent-0.1.6 → sliceagent-0.1.8}/tests/test_bound_is_relevance.py +0 -0
  104. {sliceagent-0.1.6 → sliceagent-0.1.8}/tests/test_breadth_e2e.py +0 -0
  105. {sliceagent-0.1.6 → sliceagent-0.1.8}/tests/test_bugfix_2b.py +0 -0
  106. {sliceagent-0.1.6 → sliceagent-0.1.8}/tests/test_bugfix_breadth_wave.py +0 -0
  107. {sliceagent-0.1.6 → sliceagent-0.1.8}/tests/test_bugfix_current_request.py +0 -0
  108. {sliceagent-0.1.6 → sliceagent-0.1.8}/tests/test_bugfix_linenum.py +0 -0
  109. {sliceagent-0.1.6 → sliceagent-0.1.8}/tests/test_bugfix_loop_wave.py +0 -0
  110. {sliceagent-0.1.6 → sliceagent-0.1.8}/tests/test_bugfix_memory_wave.py +0 -0
  111. {sliceagent-0.1.6 → sliceagent-0.1.8}/tests/test_bugfix_p0a.py +0 -0
  112. {sliceagent-0.1.6 → sliceagent-0.1.8}/tests/test_bugfix_p0b.py +0 -0
  113. {sliceagent-0.1.6 → sliceagent-0.1.8}/tests/test_bugfix_p0c.py +0 -0
  114. {sliceagent-0.1.6 → sliceagent-0.1.8}/tests/test_bugfix_selfreview.py +0 -0
  115. {sliceagent-0.1.6 → sliceagent-0.1.8}/tests/test_bugfix_selfreview_siblings.py +0 -0
  116. {sliceagent-0.1.6 → sliceagent-0.1.8}/tests/test_bugfix_skills_h1.py +0 -0
  117. {sliceagent-0.1.6 → sliceagent-0.1.8}/tests/test_bugfix_slice_wave.py +0 -0
  118. {sliceagent-0.1.6 → sliceagent-0.1.8}/tests/test_bugfix_subagent_wave.py +0 -0
  119. {sliceagent-0.1.6 → sliceagent-0.1.8}/tests/test_bugfix_tools_wave.py +0 -0
  120. {sliceagent-0.1.6 → sliceagent-0.1.8}/tests/test_cache_manifest.py +0 -0
  121. {sliceagent-0.1.6 → sliceagent-0.1.8}/tests/test_checkpoint.py +0 -0
  122. {sliceagent-0.1.6 → sliceagent-0.1.8}/tests/test_cli_smoke.py +0 -0
  123. {sliceagent-0.1.6 → sliceagent-0.1.8}/tests/test_closure.py +0 -0
  124. {sliceagent-0.1.6 → sliceagent-0.1.8}/tests/test_code_grep.py +0 -0
  125. {sliceagent-0.1.6 → sliceagent-0.1.8}/tests/test_code_index.py +0 -0
  126. {sliceagent-0.1.6 → sliceagent-0.1.8}/tests/test_consolidate.py +0 -0
  127. {sliceagent-0.1.6 → sliceagent-0.1.8}/tests/test_context_overflow.py +0 -0
  128. {sliceagent-0.1.6 → sliceagent-0.1.8}/tests/test_conversation.py +0 -0
  129. {sliceagent-0.1.6 → sliceagent-0.1.8}/tests/test_coresidency.py +0 -0
  130. {sliceagent-0.1.6 → sliceagent-0.1.8}/tests/test_echo_before_blocking.py +0 -0
  131. {sliceagent-0.1.6 → sliceagent-0.1.8}/tests/test_episode.py +0 -0
  132. {sliceagent-0.1.6 → sliceagent-0.1.8}/tests/test_errors_backoff.py +0 -0
  133. {sliceagent-0.1.6 → sliceagent-0.1.8}/tests/test_esc_sentinel.py +0 -0
  134. {sliceagent-0.1.6 → sliceagent-0.1.8}/tests/test_exec_env.py +0 -0
  135. {sliceagent-0.1.6 → sliceagent-0.1.8}/tests/test_finding_types.py +0 -0
  136. {sliceagent-0.1.6 → sliceagent-0.1.8}/tests/test_flags.py +0 -0
  137. {sliceagent-0.1.6 → sliceagent-0.1.8}/tests/test_fuzzy.py +0 -0
  138. {sliceagent-0.1.6 → sliceagent-0.1.8}/tests/test_ghost_index.py +0 -0
  139. {sliceagent-0.1.6 → sliceagent-0.1.8}/tests/test_guardrails.py +0 -0
  140. {sliceagent-0.1.6 → sliceagent-0.1.8}/tests/test_guidance.py +0 -0
  141. {sliceagent-0.1.6 → sliceagent-0.1.8}/tests/test_history.py +0 -0
  142. {sliceagent-0.1.6 → sliceagent-0.1.8}/tests/test_invariant_fixes.py +0 -0
  143. {sliceagent-0.1.6 → sliceagent-0.1.8}/tests/test_live_composer.py +0 -0
  144. {sliceagent-0.1.6 → sliceagent-0.1.8}/tests/test_llm_streaming.py +0 -0
  145. {sliceagent-0.1.6 → sliceagent-0.1.8}/tests/test_llm_usage_cache.py +0 -0
  146. {sliceagent-0.1.6 → sliceagent-0.1.8}/tests/test_llm_watchdog.py +0 -0
  147. {sliceagent-0.1.6 → sliceagent-0.1.8}/tests/test_log_redaction.py +0 -0
  148. {sliceagent-0.1.6 → sliceagent-0.1.8}/tests/test_loop_overflow.py +0 -0
  149. {sliceagent-0.1.6 → sliceagent-0.1.8}/tests/test_mcp_output_cap.py +0 -0
  150. {sliceagent-0.1.6 → sliceagent-0.1.8}/tests/test_mcp_runtime.py +0 -0
  151. {sliceagent-0.1.6 → sliceagent-0.1.8}/tests/test_memory.py +0 -0
  152. {sliceagent-0.1.6 → sliceagent-0.1.8}/tests/test_memory_persist.py +0 -0
  153. {sliceagent-0.1.6 → sliceagent-0.1.8}/tests/test_menu_select.py +0 -0
  154. {sliceagent-0.1.6 → sliceagent-0.1.8}/tests/test_metrics.py +0 -0
  155. {sliceagent-0.1.6 → sliceagent-0.1.8}/tests/test_mining.py +0 -0
  156. {sliceagent-0.1.6 → sliceagent-0.1.8}/tests/test_mission_tier.py +0 -0
  157. {sliceagent-0.1.6 → sliceagent-0.1.8}/tests/test_model_catalog.py +0 -0
  158. {sliceagent-0.1.6 → sliceagent-0.1.8}/tests/test_monitor.py +0 -0
  159. {sliceagent-0.1.6 → sliceagent-0.1.8}/tests/test_onboarding.py +0 -0
  160. {sliceagent-0.1.6 → sliceagent-0.1.8}/tests/test_pageout_compaction.py +0 -0
  161. {sliceagent-0.1.6 → sliceagent-0.1.8}/tests/test_permission_patterns.py +0 -0
  162. {sliceagent-0.1.6 → sliceagent-0.1.8}/tests/test_pinned_composer.py +0 -0
  163. {sliceagent-0.1.6 → sliceagent-0.1.8}/tests/test_plan_tier.py +0 -0
  164. {sliceagent-0.1.6 → sliceagent-0.1.8}/tests/test_procman.py +0 -0
  165. {sliceagent-0.1.6 → sliceagent-0.1.8}/tests/test_product_features.py +0 -0
  166. {sliceagent-0.1.6 → sliceagent-0.1.8}/tests/test_progress_status.py +0 -0
  167. {sliceagent-0.1.6 → sliceagent-0.1.8}/tests/test_prompt_ab.py +0 -0
  168. {sliceagent-0.1.6 → sliceagent-0.1.8}/tests/test_provenance.py +0 -0
  169. {sliceagent-0.1.6 → sliceagent-0.1.8}/tests/test_provider_lineup.py +0 -0
  170. {sliceagent-0.1.6 → sliceagent-0.1.8}/tests/test_proxy_interrupt.py +0 -0
  171. {sliceagent-0.1.6 → sliceagent-0.1.8}/tests/test_reach.py +0 -0
  172. {sliceagent-0.1.6 → sliceagent-0.1.8}/tests/test_readonly_subagent.py +0 -0
  173. {sliceagent-0.1.6 → sliceagent-0.1.8}/tests/test_recall_search.py +0 -0
  174. {sliceagent-0.1.6 → sliceagent-0.1.8}/tests/test_records.py +0 -0
  175. {sliceagent-0.1.6 → sliceagent-0.1.8}/tests/test_refault.py +0 -0
  176. {sliceagent-0.1.6 → sliceagent-0.1.8}/tests/test_registry_validation.py +0 -0
  177. {sliceagent-0.1.6 → sliceagent-0.1.8}/tests/test_reliability.py +0 -0
  178. {sliceagent-0.1.6 → sliceagent-0.1.8}/tests/test_repo_map_bounded.py +0 -0
  179. {sliceagent-0.1.6 → sliceagent-0.1.8}/tests/test_requirements.py +0 -0
  180. {sliceagent-0.1.6 → sliceagent-0.1.8}/tests/test_review_fixes.py +0 -0
  181. {sliceagent-0.1.6 → sliceagent-0.1.8}/tests/test_route_lexical.py +0 -0
  182. {sliceagent-0.1.6 → sliceagent-0.1.8}/tests/test_safety.py +0 -0
  183. {sliceagent-0.1.6 → sliceagent-0.1.8}/tests/test_seal_markdown.py +0 -0
  184. {sliceagent-0.1.6 → sliceagent-0.1.8}/tests/test_search_index.py +0 -0
  185. {sliceagent-0.1.6 → sliceagent-0.1.8}/tests/test_skill_meta.py +0 -0
  186. {sliceagent-0.1.6 → sliceagent-0.1.8}/tests/test_skill_metadata.py +0 -0
  187. {sliceagent-0.1.6 → sliceagent-0.1.8}/tests/test_subdir_hints.py +0 -0
  188. {sliceagent-0.1.6 → sliceagent-0.1.8}/tests/test_task_agnostic.py +0 -0
  189. {sliceagent-0.1.6 → sliceagent-0.1.8}/tests/test_task_state_roundtrip.py +0 -0
  190. {sliceagent-0.1.6 → sliceagent-0.1.8}/tests/test_telemetry_convergence.py +0 -0
  191. {sliceagent-0.1.6 → sliceagent-0.1.8}/tests/test_terminal.py +0 -0
  192. {sliceagent-0.1.6 → sliceagent-0.1.8}/tests/test_tool_dedup.py +0 -0
  193. {sliceagent-0.1.6 → sliceagent-0.1.8}/tests/test_tool_result_ok.py +0 -0
  194. {sliceagent-0.1.6 → sliceagent-0.1.8}/tests/test_tools_robust.py +0 -0
  195. {sliceagent-0.1.6 → sliceagent-0.1.8}/tests/test_topic_switch.py +0 -0
  196. {sliceagent-0.1.6 → sliceagent-0.1.8}/tests/test_trajectory.py +0 -0
  197. {sliceagent-0.1.6 → sliceagent-0.1.8}/tests/test_tui.py +0 -0
  198. {sliceagent-0.1.6 → sliceagent-0.1.8}/tests/test_tui_menus.py +0 -0
  199. {sliceagent-0.1.6 → sliceagent-0.1.8}/tests/test_tui_render.py +0 -0
  200. {sliceagent-0.1.6 → sliceagent-0.1.8}/tests/test_tui_widgets.py +0 -0
  201. {sliceagent-0.1.6 → sliceagent-0.1.8}/tests/test_turn_budget.py +0 -0
  202. {sliceagent-0.1.6 → sliceagent-0.1.8}/tests/test_verification_agent.py +0 -0
  203. {sliceagent-0.1.6 → sliceagent-0.1.8}/tests/test_web.py +0 -0
  204. {sliceagent-0.1.6 → sliceagent-0.1.8}/tests/test_workspace.py +0 -0
  205. {sliceagent-0.1.6 → sliceagent-0.1.8}/tests/test_world_region.py +0 -0
  206. {sliceagent-0.1.6 → sliceagent-0.1.8}/tests/test_world_state.py +0 -0
@@ -5,6 +5,38 @@ this project aims for [Semantic Versioning](https://semver.org/).
5
5
 
6
6
  ## [Unreleased]
7
7
 
8
+ ## [0.1.8] — 2026-07-02
9
+
10
+ Launch-day bug-hunt round: a security fix plus config-robustness and /model correctness.
11
+
12
+ ### Security
13
+ - `run_command` in the default *teenager* mode no longer auto-runs exec/write commands disguised as
14
+ "read-only": `env <program>`, `sort -o FILE`, `date -s`, `tree -o FILE`, `uniq IN OUT`, and
15
+ `git grep --open-files-in-pager` now take the confirm path (arbitrary code-exec / file-overwrite
16
+ confirm-bypass). Read-only siblings like `du -s` / `grep -o` still auto-run.
17
+
18
+ ### Fixed
19
+ - Boot no longer cross-wires a prefs-pinned provider's key with the DEFAULT provider's endpoint (an
20
+ env `gpt-5.5` @ OpenAI pin on a DeepSeek-default config used to 401 every call on relaunch).
21
+ - A prefs `provider`/model pin whose `[providers.<id>]` table was removed from config.toml is now
22
+ dropped at boot instead of forcing a model onto the wrong endpoint.
23
+ - The wizard no longer silently ERASES other providers' API keys when config.toml is unparseable —
24
+ the old file is moved to a non-clobbering `.bak` first (keys recoverable).
25
+ - `sliceagent config` / `config --use` / the init wizard no longer crash on a non-UTF-8 config.toml
26
+ or a scalar value under `[providers]` (they degrade).
27
+ - `llm.switch()` (a `/model` provider hop) closes the replaced HTTP client — no more fd leak per hop.
28
+ - The `/model` reasoning menu offers `high` (not a misleading `max`) for OpenRouter models, and offers
29
+ the levels the *target* provider supports when the pick rebinds the endpoint.
30
+ - The wizard's typed provider fallback rejects an unknown choice instead of silently configuring
31
+ OpenRouter.
32
+
33
+ ## [0.1.7] — 2026-07-02
34
+
35
+ ### Fixed
36
+ - `/model`: the current env-configured model row is now labeled `current (env)` instead of the
37
+ provider family name — it could masquerade as a configured provider (e.g. an env `gpt-5.5` on a
38
+ DeepSeek endpoint showed as "deepseek").
39
+
8
40
  ## [0.1.6] — 2026-07-02
9
41
 
10
42
  The clear config journey: one wizard, two doors, an honest /model.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: sliceagent
3
- Version: 0.1.6
3
+ Version: 0.1.8
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
@@ -1,3 +1,3 @@
1
1
  """sliceagent — a memory-native coding agent (deterministic, bounded, reconstructed context)."""
2
2
 
3
- __version__ = "0.1.6"
3
+ __version__ = "0.1.8"
@@ -151,6 +151,21 @@ def _ws_name(path: str) -> str:
151
151
  return "~" if p == os.path.realpath(os.path.expanduser("~")) else (os.path.basename(p) or p)
152
152
 
153
153
 
154
+ def _env_from_config(c, pid: str | None = None) -> None:
155
+ """Populate LLM_API_KEY/LLM_BASE_URL from config (ENV still wins). A prefs-pinned provider `pid`
156
+ binds AS A UNIT: its key with ITS OWN endpoint — an absent base_url means the SDK default (OpenAI),
157
+ NOT the default provider's base_url (that fallback cross-wired an openai key onto the deepseek
158
+ endpoint after `/model`-hop + restart when default_provider pointed elsewhere)."""
159
+ tbl = (c.providers() or {}).get(pid) if pid else None
160
+ if isinstance(tbl, dict) and tbl.get("api_key"):
161
+ key, base = tbl["api_key"], tbl.get("base_url") or ""
162
+ else: # no/unusable pin → the default provider's own pairing
163
+ key, base = c.api_key, c.base_url
164
+ for _env, _val in (("LLM_API_KEY", key), ("LLM_BASE_URL", base)):
165
+ if not os.environ.get(_env) and _val:
166
+ os.environ[_env] = _val
167
+
168
+
154
169
  def main() -> None:
155
170
  # subcommands (onboarding / discovery) are handled BEFORE any key gate, so `sliceagent init` runs on a
156
171
  # machine with nothing configured yet. A bare `sliceagent` (or one with non-subcommand args) falls through.
@@ -165,14 +180,6 @@ def main() -> None:
165
180
  from .config import load_config, load_prefs
166
181
  _boot_prefs = load_prefs() # the last /model choice may pin a PROVIDER (endpoint+key), not just a model
167
182
 
168
- def _env_from_config(c, pid: str | None = None) -> None:
169
- tbl = (c.providers() or {}).get(pid) if pid else None
170
- key = (tbl or {}).get("api_key") or c.api_key
171
- base = ((tbl or {}).get("base_url") if tbl and tbl.get("api_key") else None) or c.base_url
172
- for _env, _val in (("LLM_API_KEY", key), ("LLM_BASE_URL", base)):
173
- if not os.environ.get(_env) and _val:
174
- os.environ[_env] = _val
175
-
176
183
  def _key_present() -> bool:
177
184
  return bool(os.environ.get("LLM_API_KEY") or os.environ.get("OPENAI_API_KEY")
178
185
  or os.environ.get("ANTHROPIC_API_KEY") or os.environ.get("DEEPSEEK_API_KEY")
@@ -189,6 +196,14 @@ def main() -> None:
189
196
  return run_init() == 0
190
197
 
191
198
  cfg = load_config()
199
+ # A prefs `provider` pin whose [providers.<id>] table was since removed from config.toml is STALE:
200
+ # its key/endpoint already fall back to the default provider (see _env_from_config), but its pinned
201
+ # MODEL would still win at the resolution below and get sent to the wrong endpoint (model_not_found
202
+ # every turn). Drop the whole stale pin so model/endpoint resolve together from the live config.
203
+ if _boot_prefs.get("provider") and _boot_prefs["provider"] not in (cfg.providers() or {}):
204
+ from .config import save_prefs as _sp
205
+ _sp({"provider": None, "model": None}) # None DELETES (see save_prefs)
206
+ _boot_prefs.pop("provider", None); _boot_prefs.pop("model", None)
192
207
  _env_from_config(cfg, _boot_prefs.get("provider"))
193
208
  if not _key_present() and _first_run_setup("Welcome! No API key configured yet"):
194
209
  cfg = load_config() # the wizard just wrote ~/.sliceagent/config.toml — pick it up
@@ -468,6 +483,18 @@ def main() -> None:
468
483
  ans = input(f" ⚠ allow {name} {str(detail)[:60]!r}? ({reason}) [y]es/[n]o/[a]lways: ").strip().lower()
469
484
  return {"y": "yes", "yes": "yes", "a": "always", "always": "always"}.get(ans, "no")
470
485
 
486
+ def _live_pid():
487
+ """The configured provider the LIVE llm is actually bound to (endpoint+key match), or None.
488
+ Prefs must pin what's IN EFFECT, not the last menu pick — a typed `/model <name>` keeps the
489
+ endpoint, so blindly re-saving the old pin resurrected a dead endpoint+model pairing at boot."""
490
+ lbase = getattr(llm, "_base_url", "") or ""
491
+ lkey = getattr(llm.client, "api_key", None)
492
+ for p, t in (cfg.providers() or {}).items():
493
+ if isinstance(t, dict) and t.get("api_key") and t["api_key"] == lkey \
494
+ and (t.get("base_url") or "") == lbase:
495
+ return p
496
+ return None
497
+
471
498
  def _handle_slash(line): # TUI navigation palette — wired to existing session ops
472
499
  nonlocal cfg # /config hot-reloads the config after the in-session wizard
473
500
  parts = line.split(maxsplit=1)
@@ -595,8 +622,9 @@ def main() -> None:
595
622
  _kw = {"base_url": _tbl.get("base_url") or "", "api_key": _tbl["api_key"]}
596
623
  llm.switch(model=_model, reasoning=_reasoning, **_kw)
597
624
  _stats["model"] = llm.model
625
+ # pin the provider ACTUALLY in effect (None DELETES a stale pin — see save_prefs)
598
626
  save_prefs({"model": llm.model, "reasoning": llm.reasoning,
599
- **({"provider": _pid} if _pid else {})})
627
+ "provider": _pid or _live_pid()})
600
628
  note = _reasoning_note(llm)
601
629
  _console.print(f" ✓ model → [bold]{llm.model}[/]"
602
630
  + (f" @ [bold]{_pid}[/]" if _pid else "")
@@ -619,7 +647,9 @@ def main() -> None:
619
647
  _console.print(" effort must be one of: fast | full | high | max"); return True
620
648
  llm.switch(model=name, reasoning=eff)
621
649
  _stats["model"] = llm.model
622
- save_prefs({"model": llm.model, "reasoning": llm.reasoning})
650
+ # typed /model keeps the endpoint — re-resolve the pin against the LIVE binding so a
651
+ # stale provider pin can't resurrect the old endpoint under this model at next boot
652
+ save_prefs({"model": llm.model, "reasoning": llm.reasoning, "provider": _live_pid()})
623
653
  note = _reasoning_note(llm)
624
654
  _console.print(f" ✓ model → [bold]{llm.model}[/]"
625
655
  + (f" · reasoning [bold]{llm.reasoning}[/]" if eff else "")
@@ -52,12 +52,17 @@ def load_prefs() -> dict:
52
52
 
53
53
 
54
54
  def save_prefs(updates: dict) -> None:
55
- """Merge non-empty `updates` into the prefs sidecar (atomic write). Best-effort; never raises."""
55
+ """Merge non-empty `updates` into the prefs sidecar (atomic write); an explicit None DELETES the
56
+ key (a stale `provider` pin must be removable — merge-only let an old endpoint pin resurrect at
57
+ the next boot under a model it doesn't serve). Best-effort; never raises."""
56
58
  try:
57
59
  import json
58
60
  path = _prefs_path()
59
61
  os.makedirs(os.path.dirname(path), exist_ok=True)
60
62
  cur = load_prefs()
63
+ for k, v in updates.items():
64
+ if v is None:
65
+ cur.pop(k, None)
61
66
  cur.update({k: v for k, v in updates.items() if v})
62
67
  tmp = path + ".tmp"
63
68
  with open(tmp, "w", encoding="utf-8") as f:
@@ -283,7 +283,13 @@ class OpenAILLM:
283
283
  ckw: dict = {"api_key": new_key}
284
284
  if new_base:
285
285
  ckw["base_url"] = new_base
286
+ old_client = getattr(self, "client", None)
286
287
  self.client = OpenAI(http_client=http_client, timeout=timeout, max_retries=2, **ckw)
288
+ if old_client is not None: # close the replaced connection pool — /model hops leaked fds
289
+ try:
290
+ old_client.close()
291
+ except Exception: # noqa: BLE001 — cleanup must never break the switch
292
+ pass
287
293
  self._base_url = new_base or ""
288
294
  self._drop_reasoning_effort = False
289
295
  if model:
@@ -140,10 +140,42 @@ def _read_config(path: str) -> dict:
140
140
  try:
141
141
  with open(path, "rb") as f:
142
142
  return tomllib.load(f)
143
- except (OSError, tomllib.TOMLDecodeError):
143
+ except (OSError, tomllib.TOMLDecodeError, UnicodeDecodeError, ValueError):
144
+ # sibling parity with config.py:_read_toml — a non-UTF-8 or malformed config must DEGRADE, not
145
+ # crash `sliceagent config` / `config --use` / the init wizard with a traceback.
144
146
  return {}
145
147
 
146
148
 
149
+ def _backup_if_corrupt(path: str) -> "str | None":
150
+ """A rewrite reads the existing config to MERGE into; but _read_config returns {} for BOTH 'missing'
151
+ and 'unparseable', so a rewrite over a corrupt file would silently ERASE every provider key + section
152
+ it couldn't parse. Guard the writers: if `path` exists and is non-empty but does NOT parse, move it
153
+ aside to a non-clobbering .bak first (the keys survive, recoverable) and return the backup path; else
154
+ None (absent / empty / valid — nothing to protect)."""
155
+ import tomllib
156
+ try:
157
+ with open(path, "rb") as f:
158
+ raw = f.read()
159
+ except OSError:
160
+ return None
161
+ if not raw.strip():
162
+ return None
163
+ try:
164
+ tomllib.loads(raw.decode("utf-8"))
165
+ return None # parses fine → a normal merge is safe
166
+ except (tomllib.TOMLDecodeError, UnicodeDecodeError, ValueError):
167
+ pass
168
+ bak, n = path + ".bak", 0
169
+ while os.path.exists(bak): # never clobber an earlier backup
170
+ n += 1
171
+ bak = f"{path}.bak.{n}"
172
+ try:
173
+ os.replace(path, bak)
174
+ return bak
175
+ except OSError:
176
+ return None
177
+
178
+
147
179
  def _atomic_write(path: str, body: str) -> None:
148
180
  """ATOMIC 0600 write (the file holds an API key — never leave it half-written): write a temp in the same
149
181
  dir, fsync, then os.replace(); on ANY failure remove the temp so no key-bearing fragment is left behind."""
@@ -170,8 +202,10 @@ def _atomic_write(path: str, body: str) -> None:
170
202
  pass
171
203
 
172
204
 
173
- def _save_provider(path: str, *, pid: str, model: str, api_key: str, base_url: str) -> None:
174
- """Merge a provider into the config: add/update [providers.<pid>], set it as the default, keep the rest."""
205
+ def _save_provider(path: str, *, pid: str, model: str, api_key: str, base_url: str) -> "str | None":
206
+ """Merge a provider into the config: add/update [providers.<pid>], set it as the default, keep the rest.
207
+ Returns the path of a backup taken when the existing config was unparseable (else None)."""
208
+ bak = _backup_if_corrupt(path) # never silently erase keys in a corrupt config
175
209
  data = _read_config(path)
176
210
  provs = data.setdefault("providers", {})
177
211
  if not isinstance(provs, dict): # a corrupt non-dict providers must not crash on provs[pid]=
@@ -186,6 +220,7 @@ def _save_provider(path: str, *, pid: str, model: str, api_key: str, base_url: s
186
220
  agent["default_provider"] = pid
187
221
  agent["model"] = model # keep top-level model in sync (back-compat)
188
222
  _atomic_write(path, _emit_toml(data))
223
+ return bak
189
224
 
190
225
 
191
226
  def _test_key(model: str, api_key: str, base_url: str, llm_factory) -> tuple[bool, str]:
@@ -250,8 +285,20 @@ def run_init(*, inp=input, getpw=None, llm_factory=None, home=None) -> int:
250
285
  for k in keys:
251
286
  _id, label, _base, model0 = PROVIDERS[k]
252
287
  out(f" {k}. {label}" + (f" ({model0})" if model0 else ""))
253
- choice = inp(" > ").strip() or "1"
254
- pid, label, base_url, model = PROVIDERS.get(choice, PROVIDERS["1"])
288
+ # an unrecognized answer must NOT silently become provider 1 (a typed "openai" used to
289
+ # configure OpenRouter without a word) — accept a number OR a provider id, else re-ask.
290
+ choice = None
291
+ for _attempt in range(3):
292
+ raw = inp(" > ").strip() or "1"
293
+ if raw in PROVIDERS:
294
+ choice = raw; break
295
+ byname = next((k for k in keys if PROVIDERS[k][0] == raw.lower()), None)
296
+ if byname:
297
+ choice = byname; break
298
+ out(f" not a valid choice: {raw!r} — enter 1-{len(PROVIDERS)} or a provider name")
299
+ if choice is None:
300
+ out(" no valid choice — aborting."); return 1
301
+ pid, label, base_url, model = PROVIDERS[choice]
255
302
  if pid == "custom":
256
303
  base_url = inp(" Base URL (OpenAI-compatible, e.g. https://host/v1): ").strip()
257
304
  # RE-CONFIGURING an ALREADY-SAVED provider (re-running `sliceagent init` to update the model,
@@ -303,7 +350,9 @@ def run_init(*, inp=input, getpw=None, llm_factory=None, home=None) -> int:
303
350
  if cont not in ("y", "yes"):
304
351
  out(" Not saved. Re-run `sliceagent init` to try again."); return 1
305
352
 
306
- _save_provider(path, pid=pid, model=model, api_key=key, base_url=base_url)
353
+ bak = _save_provider(path, pid=pid, model=model, api_key=key, base_url=base_url)
354
+ if bak:
355
+ out(f" ⚠ the existing config didn't parse — moved it to {bak} (its keys are recoverable there).")
307
356
  out(f"\n Saved provider '{pid}' (model {model}) → {path} (0600)")
308
357
  out(" Ready. Run: sliceagent\n")
309
358
  return 0
@@ -356,7 +405,8 @@ def run_config(argv=None, *, home=None, env=None) -> int:
356
405
  out(" providers (* = default · `config --use <id>` to switch):")
357
406
  for pid, p in provs.items():
358
407
  mark = "*" if pid == default else " "
359
- out(f" {mark} {pid} ({(p or {}).get('model', '?')})")
408
+ model = p.get("model", "?") if isinstance(p, dict) else "?" # a scalar under [providers] must not crash
409
+ out(f" {mark} {pid} ({model})")
360
410
  out(" set values:")
361
411
  any_set = False
362
412
  for e in REGISTRY:
@@ -96,11 +96,30 @@ def ask_mutations(name: str, args: dict) -> Optional[ToolDecision]:
96
96
  # mode runs it without a confirm prompt. Deny-by-default: an unknown verb still asks. The catastrophic floor
97
97
  # (no_dangerous_commands) runs BEFORE this, so e.g. `cat /etc/passwd` is still denied.
98
98
  _RO_VERBS = frozenset((
99
- "ls", "pwd", "cat", "head", "tail", "wc", "echo", "which", "type", "env", "printenv", "date",
99
+ "ls", "pwd", "cat", "head", "tail", "wc", "echo", "which", "type", "printenv", "date",
100
100
  "whoami", "hostname", "uname", "id", "groups", "tree", "stat", "file", "du", "df", "realpath",
101
- "dirname", "basename", "grep", "rg", "egrep", "fgrep", "sort", "uniq", "nl", "cut", "column",
101
+ "dirname", "basename", "grep", "rg", "egrep", "fgrep", "sort", "nl", "cut", "column",
102
102
  "cksum", "md5", "md5sum", "sha1sum", "sha256sum", "true",
103
103
  ))
104
+ # `env` and `uniq` were REMOVED from the allowlist: `env <program>` executes an arbitrary program, and
105
+ # `uniq [IN] OUT` OVERWRITES its optional second positional — neither is provably read-only, so they now
106
+ # take the normal confirm path. A few remaining allowlisted verbs stay read-only ONLY without a specific
107
+ # WRITE/EXEC option; deny-by-default when that option is present (verb-scoped so read-only siblings like
108
+ # `du -s` / `grep -o` keep auto-running). Value-taking flags match `-o`, `-o=…`, `-oFILE`, `--output=…`.
109
+ _UNSAFE_OPTS = {
110
+ "sort": ("-o", "--output"), # -o FILE / --output=FILE overwrite a file
111
+ "date": ("-s", "--set"), # set the system clock (a mutation)
112
+ "tree": ("-o",), # -o FILE writes the listing to a file
113
+ }
114
+
115
+
116
+ def _has_unsafe_opt(verb: str, toks: list) -> bool:
117
+ bad = _UNSAFE_OPTS.get(verb, ())
118
+ for t in toks[1:]:
119
+ for b in bad:
120
+ if t == b or t.startswith(b + "=") or (len(b) == 2 and len(t) > 2 and t.startswith(b)):
121
+ return True
122
+ return False
104
123
  # git subcommands with NO mutating form (excludes branch/tag/config/remote/stash/reflog, which can write).
105
124
  _RO_GIT_SUB = frozenset((
106
125
  "status", "log", "diff", "show", "rev-parse", "ls-files", "ls-tree", "describe", "blame",
@@ -127,10 +146,15 @@ def _is_readonly_command(cmd: str) -> bool:
127
146
  verb = os.path.basename(toks[0])
128
147
  if verb == "git":
129
148
  sub = next((t for t in toks[1:] if not t.startswith("-")), "")
130
- return sub in _RO_GIT_SUB
149
+ if sub not in _RO_GIT_SUB:
150
+ return False
151
+ # `git grep --open-files-in-pager[=<cmd>]` / `-O[<cmd>]` runs a PAGER program (arbitrary exec).
152
+ if sub == "grep" and any(t.startswith("--open-files-in-pager") or t.startswith("-O") for t in toks):
153
+ return False
154
+ return True
131
155
  if verb == "find":
132
156
  return not any(t in _FIND_MUTATORS for t in toks)
133
- return verb in _RO_VERBS
157
+ return verb in _RO_VERBS and not _has_unsafe_opt(verb, toks)
134
158
 
135
159
 
136
160
  def ask_commands(name: str, args: dict) -> Optional[ToolDecision]:
@@ -849,8 +849,7 @@ def _model_candidates(llm, cfg):
849
849
  out.append((m, pid, pid))
850
850
  out.sort(key=lambda t: (t[1], t[0]))
851
851
  if all(m != llm.model for m, _, _ in out): # an env-overridden current model still shows first
852
- base = getattr(llm, "_base_url", "")
853
- out.insert(0, (llm.model, capability(llm.model, base).family, None))
852
+ out.insert(0, (llm.model, "current (env)", None)) # not a configured provider — label honestly
854
853
  else:
855
854
  known = ["gpt-5.5", "gpt-5", "gpt-5-mini", "o3", "deepseek-chat", "kimi-k2-0905-preview",
856
855
  "claude-sonnet-5"]
@@ -865,12 +864,16 @@ def _model_candidates(llm, cfg):
865
864
 
866
865
  def _reasoning_levels(model, base_url):
867
866
  """Reasoning levels valid for a model, derived from its capability (provider-aware). Effort-capable
868
- models (gpt-5/o-series) expose all four; others only fast/full (high/max would degrade to default)."""
867
+ models (gpt-5/o-series) expose all four; OpenRouter exposes fast/full/high (its unified reasoning
868
+ object maps max→high, so offering max would be a lie); others only fast/full (high/max would
869
+ degrade to default)."""
869
870
  from .model_catalog import capability
870
871
  full4 = [("fast", "minimal reasoning — fastest, cheapest"),
871
872
  ("full", "provider default reasoning"),
872
873
  ("high", "deeper reasoning (effort=high, /v1/responses)"),
873
874
  ("max", "deepest reasoning (effort=xhigh)")]
875
+ if "openrouter" in (base_url or "").lower(): # unified reasoning object honors effort WITH tools
876
+ return full4[:2] + [("high", "deeper reasoning (unified reasoning, effort=high)")]
874
877
  if capability(model, base_url).supports_reasoning_effort:
875
878
  return full4
876
879
  return full4[:2] # fast | full only — the model has no effort knob
@@ -889,7 +892,10 @@ def select_model_reasoning(llm, cfg, *, pt_input=None, pt_output=None):
889
892
  if pick is None:
890
893
  return None
891
894
  model, _grp, pid = cands[pick]
892
- base = getattr(llm, "_base_url", "") if model == llm.model else ""
895
+ if pid: # the pick will REBIND to this provider offer the levels ITS endpoint supports
896
+ base = ((cfg.providers() or {}).get(pid) or {}).get("base_url") or ""
897
+ else:
898
+ base = getattr(llm, "_base_url", "") if model == llm.model else ""
893
899
  levels = _reasoning_levels(model, base)
894
900
  lvl_rows = [(name, desc) for name, desc in levels]
895
901
  lvl_cur = next((i for i, (n, _) in enumerate(levels) if n == llm.reasoning), -1)
@@ -918,7 +918,10 @@ def cost_chart_renders_flat_vs_rising():
918
918
  import sys
919
919
  root = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
920
920
  sys.path.insert(0, os.path.join(root, "evals"))
921
- import realenv_multiturn as rem # importable WITHOUT the heavy swebench dep
921
+ try:
922
+ import realenv_multiturn as rem # importable WITHOUT the heavy swebench dep
923
+ except ModuleNotFoundError:
924
+ return # evals/ is a local-only dev tree, not shipped in the installed package (CI) — skip, don't fail
922
925
  rows = [{"turn": i, "peak_in": 6000, "transcript": 6000 * i} for i in range(1, 13)]
923
926
  chart = rem.render_cost_chart(rows)
924
927
  assert "sliceagent" in chart and "transcript" in chart and "12×" in chart, chart