sliceagent 0.1.0__tar.gz → 0.1.2__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.
- {sliceagent-0.1.0 → sliceagent-0.1.2}/CHANGELOG.md +31 -0
- {sliceagent-0.1.0 → sliceagent-0.1.2}/PKG-INFO +18 -8
- {sliceagent-0.1.0 → sliceagent-0.1.2}/README.md +17 -7
- {sliceagent-0.1.0 → sliceagent-0.1.2}/install.sh +40 -4
- {sliceagent-0.1.0 → sliceagent-0.1.2}/src/sliceagent/__init__.py +1 -1
- {sliceagent-0.1.0 → sliceagent-0.1.2}/src/sliceagent/cli.py +36 -6
- {sliceagent-0.1.0 → sliceagent-0.1.2}/src/sliceagent/sensory_cortex.py +24 -3
- sliceagent-0.1.2/tests/test_cli_smoke.py +159 -0
- {sliceagent-0.1.0 → sliceagent-0.1.2}/tests/test_prompt_ab.py +6 -0
- sliceagent-0.1.2/tests/test_repo_map_bounded.py +91 -0
- sliceagent-0.1.0/tests/test_cli_smoke.py +0 -41
- {sliceagent-0.1.0 → sliceagent-0.1.2}/.dockerignore +0 -0
- {sliceagent-0.1.0 → sliceagent-0.1.2}/.env.example +0 -0
- {sliceagent-0.1.0 → sliceagent-0.1.2}/.github/ISSUE_TEMPLATE/bug_report.md +0 -0
- {sliceagent-0.1.0 → sliceagent-0.1.2}/.github/ISSUE_TEMPLATE/feature_request.md +0 -0
- {sliceagent-0.1.0 → sliceagent-0.1.2}/.github/PULL_REQUEST_TEMPLATE.md +0 -0
- {sliceagent-0.1.0 → sliceagent-0.1.2}/.github/dependabot.yml +0 -0
- {sliceagent-0.1.0 → sliceagent-0.1.2}/.github/workflows/ci.yml +0 -0
- {sliceagent-0.1.0 → sliceagent-0.1.2}/.github/workflows/publish.yml +0 -0
- {sliceagent-0.1.0 → sliceagent-0.1.2}/.gitignore +0 -0
- {sliceagent-0.1.0 → sliceagent-0.1.2}/CODE_OF_CONDUCT.md +0 -0
- {sliceagent-0.1.0 → sliceagent-0.1.2}/CONTRIBUTING.md +0 -0
- {sliceagent-0.1.0 → sliceagent-0.1.2}/Dockerfile +0 -0
- {sliceagent-0.1.0 → sliceagent-0.1.2}/LICENSE +0 -0
- {sliceagent-0.1.0 → sliceagent-0.1.2}/NOTICE +0 -0
- {sliceagent-0.1.0 → sliceagent-0.1.2}/QUICKSTART.md +0 -0
- {sliceagent-0.1.0 → sliceagent-0.1.2}/SECURITY.md +0 -0
- {sliceagent-0.1.0 → sliceagent-0.1.2}/examples/plugins/hello/__init__.py +0 -0
- {sliceagent-0.1.0 → sliceagent-0.1.2}/examples/plugins/hello/plugin.toml +0 -0
- {sliceagent-0.1.0 → sliceagent-0.1.2}/pyproject.toml +0 -0
- {sliceagent-0.1.0 → sliceagent-0.1.2}/scripts/gen_config_reference.py +0 -0
- {sliceagent-0.1.0 → sliceagent-0.1.2}/scripts/run_tests.sh +0 -0
- {sliceagent-0.1.0 → sliceagent-0.1.2}/sliceagent.toml.example +0 -0
- {sliceagent-0.1.0 → sliceagent-0.1.2}/src/sliceagent/__main__.py +0 -0
- {sliceagent-0.1.0 → sliceagent-0.1.2}/src/sliceagent/access.py +0 -0
- {sliceagent-0.1.0 → sliceagent-0.1.2}/src/sliceagent/agents.py +0 -0
- {sliceagent-0.1.0 → sliceagent-0.1.2}/src/sliceagent/background_review.py +0 -0
- {sliceagent-0.1.0 → sliceagent-0.1.2}/src/sliceagent/binsniff.py +0 -0
- {sliceagent-0.1.0 → sliceagent-0.1.2}/src/sliceagent/clock.py +0 -0
- {sliceagent-0.1.0 → sliceagent-0.1.2}/src/sliceagent/code_grep.py +0 -0
- {sliceagent-0.1.0 → sliceagent-0.1.2}/src/sliceagent/code_index.py +0 -0
- {sliceagent-0.1.0 → sliceagent-0.1.2}/src/sliceagent/config.py +0 -0
- {sliceagent-0.1.0 → sliceagent-0.1.2}/src/sliceagent/context_overflow.py +0 -0
- {sliceagent-0.1.0 → sliceagent-0.1.2}/src/sliceagent/envspec.py +0 -0
- {sliceagent-0.1.0 → sliceagent-0.1.2}/src/sliceagent/errors.py +0 -0
- {sliceagent-0.1.0 → sliceagent-0.1.2}/src/sliceagent/events.py +0 -0
- {sliceagent-0.1.0 → sliceagent-0.1.2}/src/sliceagent/finding_types.py +0 -0
- {sliceagent-0.1.0 → sliceagent-0.1.2}/src/sliceagent/flags.py +0 -0
- {sliceagent-0.1.0 → sliceagent-0.1.2}/src/sliceagent/fuzzy.py +0 -0
- {sliceagent-0.1.0 → sliceagent-0.1.2}/src/sliceagent/guardrails.py +0 -0
- {sliceagent-0.1.0 → sliceagent-0.1.2}/src/sliceagent/guidance.py +0 -0
- {sliceagent-0.1.0 → sliceagent-0.1.2}/src/sliceagent/hippocampus.py +0 -0
- {sliceagent-0.1.0 → sliceagent-0.1.2}/src/sliceagent/hooks.py +0 -0
- {sliceagent-0.1.0 → sliceagent-0.1.2}/src/sliceagent/interfaces.py +0 -0
- {sliceagent-0.1.0 → sliceagent-0.1.2}/src/sliceagent/llm.py +0 -0
- {sliceagent-0.1.0 → sliceagent-0.1.2}/src/sliceagent/loop.py +0 -0
- {sliceagent-0.1.0 → sliceagent-0.1.2}/src/sliceagent/mcp_client.py +0 -0
- {sliceagent-0.1.0 → sliceagent-0.1.2}/src/sliceagent/mcp_security.py +0 -0
- {sliceagent-0.1.0 → sliceagent-0.1.2}/src/sliceagent/memory.py +0 -0
- {sliceagent-0.1.0 → sliceagent-0.1.2}/src/sliceagent/metrics.py +0 -0
- {sliceagent-0.1.0 → sliceagent-0.1.2}/src/sliceagent/model_catalog.py +0 -0
- {sliceagent-0.1.0 → sliceagent-0.1.2}/src/sliceagent/monitor.py +0 -0
- {sliceagent-0.1.0 → sliceagent-0.1.2}/src/sliceagent/neocortex.py +0 -0
- {sliceagent-0.1.0 → sliceagent-0.1.2}/src/sliceagent/onboarding.py +0 -0
- {sliceagent-0.1.0 → sliceagent-0.1.2}/src/sliceagent/oracle.py +0 -0
- {sliceagent-0.1.0 → sliceagent-0.1.2}/src/sliceagent/pagetable.py +0 -0
- {sliceagent-0.1.0 → sliceagent-0.1.2}/src/sliceagent/pfc.py +0 -0
- {sliceagent-0.1.0 → sliceagent-0.1.2}/src/sliceagent/plugins.py +0 -0
- {sliceagent-0.1.0 → sliceagent-0.1.2}/src/sliceagent/policy.py +0 -0
- {sliceagent-0.1.0 → sliceagent-0.1.2}/src/sliceagent/procman.py +0 -0
- {sliceagent-0.1.0 → sliceagent-0.1.2}/src/sliceagent/prompt.py +0 -0
- {sliceagent-0.1.0 → sliceagent-0.1.2}/src/sliceagent/records.py +0 -0
- {sliceagent-0.1.0 → sliceagent-0.1.2}/src/sliceagent/recovery.py +0 -0
- {sliceagent-0.1.0 → sliceagent-0.1.2}/src/sliceagent/regions.py +0 -0
- {sliceagent-0.1.0 → sliceagent-0.1.2}/src/sliceagent/registry.py +0 -0
- {sliceagent-0.1.0 → sliceagent-0.1.2}/src/sliceagent/retriever.py +0 -0
- {sliceagent-0.1.0 → sliceagent-0.1.2}/src/sliceagent/safety.py +0 -0
- {sliceagent-0.1.0 → sliceagent-0.1.2}/src/sliceagent/sandbox.py +0 -0
- {sliceagent-0.1.0 → sliceagent-0.1.2}/src/sliceagent/scheduler.py +0 -0
- {sliceagent-0.1.0 → sliceagent-0.1.2}/src/sliceagent/search_index.py +0 -0
- {sliceagent-0.1.0 → sliceagent-0.1.2}/src/sliceagent/seed.py +0 -0
- {sliceagent-0.1.0 → sliceagent-0.1.2}/src/sliceagent/session.py +0 -0
- {sliceagent-0.1.0 → sliceagent-0.1.2}/src/sliceagent/skill_provenance.py +0 -0
- {sliceagent-0.1.0 → sliceagent-0.1.2}/src/sliceagent/skill_usage.py +0 -0
- {sliceagent-0.1.0 → sliceagent-0.1.2}/src/sliceagent/skills.py +0 -0
- {sliceagent-0.1.0 → sliceagent-0.1.2}/src/sliceagent/subagent.py +0 -0
- {sliceagent-0.1.0 → sliceagent-0.1.2}/src/sliceagent/subdir_hints.py +0 -0
- {sliceagent-0.1.0 → sliceagent-0.1.2}/src/sliceagent/swap.py +0 -0
- {sliceagent-0.1.0 → sliceagent-0.1.2}/src/sliceagent/taskstate.py +0 -0
- {sliceagent-0.1.0 → sliceagent-0.1.2}/src/sliceagent/telemetry.py +0 -0
- {sliceagent-0.1.0 → sliceagent-0.1.2}/src/sliceagent/terminal.py +0 -0
- {sliceagent-0.1.0 → sliceagent-0.1.2}/src/sliceagent/text_utils.py +0 -0
- {sliceagent-0.1.0 → sliceagent-0.1.2}/src/sliceagent/tool_summary.py +0 -0
- {sliceagent-0.1.0 → sliceagent-0.1.2}/src/sliceagent/tools.py +0 -0
- {sliceagent-0.1.0 → sliceagent-0.1.2}/src/sliceagent/tui.py +0 -0
- {sliceagent-0.1.0 → sliceagent-0.1.2}/src/sliceagent/web.py +0 -0
- {sliceagent-0.1.0 → sliceagent-0.1.2}/tests/README.md +0 -0
- {sliceagent-0.1.0 → sliceagent-0.1.2}/tests/test_active_focus.py +0 -0
- {sliceagent-0.1.0 → sliceagent-0.1.2}/tests/test_agents.py +0 -0
- {sliceagent-0.1.0 → sliceagent-0.1.2}/tests/test_ask_echo.py +0 -0
- {sliceagent-0.1.0 → sliceagent-0.1.2}/tests/test_ask_user.py +0 -0
- {sliceagent-0.1.0 → sliceagent-0.1.2}/tests/test_background_review.py +0 -0
- {sliceagent-0.1.0 → sliceagent-0.1.2}/tests/test_binary_view.py +0 -0
- {sliceagent-0.1.0 → sliceagent-0.1.2}/tests/test_bound_is_relevance.py +0 -0
- {sliceagent-0.1.0 → sliceagent-0.1.2}/tests/test_breadth_e2e.py +0 -0
- {sliceagent-0.1.0 → sliceagent-0.1.2}/tests/test_bugfix_2b.py +0 -0
- {sliceagent-0.1.0 → sliceagent-0.1.2}/tests/test_bugfix_breadth_wave.py +0 -0
- {sliceagent-0.1.0 → sliceagent-0.1.2}/tests/test_bugfix_current_request.py +0 -0
- {sliceagent-0.1.0 → sliceagent-0.1.2}/tests/test_bugfix_linenum.py +0 -0
- {sliceagent-0.1.0 → sliceagent-0.1.2}/tests/test_bugfix_loop_wave.py +0 -0
- {sliceagent-0.1.0 → sliceagent-0.1.2}/tests/test_bugfix_memory_wave.py +0 -0
- {sliceagent-0.1.0 → sliceagent-0.1.2}/tests/test_bugfix_p0a.py +0 -0
- {sliceagent-0.1.0 → sliceagent-0.1.2}/tests/test_bugfix_p0b.py +0 -0
- {sliceagent-0.1.0 → sliceagent-0.1.2}/tests/test_bugfix_p0c.py +0 -0
- {sliceagent-0.1.0 → sliceagent-0.1.2}/tests/test_bugfix_selfreview.py +0 -0
- {sliceagent-0.1.0 → sliceagent-0.1.2}/tests/test_bugfix_selfreview_siblings.py +0 -0
- {sliceagent-0.1.0 → sliceagent-0.1.2}/tests/test_bugfix_skills_h1.py +0 -0
- {sliceagent-0.1.0 → sliceagent-0.1.2}/tests/test_bugfix_slice_wave.py +0 -0
- {sliceagent-0.1.0 → sliceagent-0.1.2}/tests/test_bugfix_subagent_wave.py +0 -0
- {sliceagent-0.1.0 → sliceagent-0.1.2}/tests/test_bugfix_tools_wave.py +0 -0
- {sliceagent-0.1.0 → sliceagent-0.1.2}/tests/test_bughunt_fixes.py +0 -0
- {sliceagent-0.1.0 → sliceagent-0.1.2}/tests/test_cache_manifest.py +0 -0
- {sliceagent-0.1.0 → sliceagent-0.1.2}/tests/test_checkpoint.py +0 -0
- {sliceagent-0.1.0 → sliceagent-0.1.2}/tests/test_closure.py +0 -0
- {sliceagent-0.1.0 → sliceagent-0.1.2}/tests/test_code_grep.py +0 -0
- {sliceagent-0.1.0 → sliceagent-0.1.2}/tests/test_code_index.py +0 -0
- {sliceagent-0.1.0 → sliceagent-0.1.2}/tests/test_consolidate.py +0 -0
- {sliceagent-0.1.0 → sliceagent-0.1.2}/tests/test_context_overflow.py +0 -0
- {sliceagent-0.1.0 → sliceagent-0.1.2}/tests/test_conversation.py +0 -0
- {sliceagent-0.1.0 → sliceagent-0.1.2}/tests/test_coresidency.py +0 -0
- {sliceagent-0.1.0 → sliceagent-0.1.2}/tests/test_echo_before_blocking.py +0 -0
- {sliceagent-0.1.0 → sliceagent-0.1.2}/tests/test_episode.py +0 -0
- {sliceagent-0.1.0 → sliceagent-0.1.2}/tests/test_errors_backoff.py +0 -0
- {sliceagent-0.1.0 → sliceagent-0.1.2}/tests/test_esc_sentinel.py +0 -0
- {sliceagent-0.1.0 → sliceagent-0.1.2}/tests/test_exec_env.py +0 -0
- {sliceagent-0.1.0 → sliceagent-0.1.2}/tests/test_finding_types.py +0 -0
- {sliceagent-0.1.0 → sliceagent-0.1.2}/tests/test_flags.py +0 -0
- {sliceagent-0.1.0 → sliceagent-0.1.2}/tests/test_fuzzy.py +0 -0
- {sliceagent-0.1.0 → sliceagent-0.1.2}/tests/test_ghost_index.py +0 -0
- {sliceagent-0.1.0 → sliceagent-0.1.2}/tests/test_guardrails.py +0 -0
- {sliceagent-0.1.0 → sliceagent-0.1.2}/tests/test_guidance.py +0 -0
- {sliceagent-0.1.0 → sliceagent-0.1.2}/tests/test_history.py +0 -0
- {sliceagent-0.1.0 → sliceagent-0.1.2}/tests/test_invariant_fixes.py +0 -0
- {sliceagent-0.1.0 → sliceagent-0.1.2}/tests/test_live_composer.py +0 -0
- {sliceagent-0.1.0 → sliceagent-0.1.2}/tests/test_llm_streaming.py +0 -0
- {sliceagent-0.1.0 → sliceagent-0.1.2}/tests/test_llm_usage_cache.py +0 -0
- {sliceagent-0.1.0 → sliceagent-0.1.2}/tests/test_llm_watchdog.py +0 -0
- {sliceagent-0.1.0 → sliceagent-0.1.2}/tests/test_log_redaction.py +0 -0
- {sliceagent-0.1.0 → sliceagent-0.1.2}/tests/test_loop_overflow.py +0 -0
- {sliceagent-0.1.0 → sliceagent-0.1.2}/tests/test_mcp_output_cap.py +0 -0
- {sliceagent-0.1.0 → sliceagent-0.1.2}/tests/test_mcp_runtime.py +0 -0
- {sliceagent-0.1.0 → sliceagent-0.1.2}/tests/test_memory.py +0 -0
- {sliceagent-0.1.0 → sliceagent-0.1.2}/tests/test_memory_persist.py +0 -0
- {sliceagent-0.1.0 → sliceagent-0.1.2}/tests/test_metrics.py +0 -0
- {sliceagent-0.1.0 → sliceagent-0.1.2}/tests/test_mining.py +0 -0
- {sliceagent-0.1.0 → sliceagent-0.1.2}/tests/test_mission_tier.py +0 -0
- {sliceagent-0.1.0 → sliceagent-0.1.2}/tests/test_model_catalog.py +0 -0
- {sliceagent-0.1.0 → sliceagent-0.1.2}/tests/test_monitor.py +0 -0
- {sliceagent-0.1.0 → sliceagent-0.1.2}/tests/test_onboarding.py +0 -0
- {sliceagent-0.1.0 → sliceagent-0.1.2}/tests/test_pageout_compaction.py +0 -0
- {sliceagent-0.1.0 → sliceagent-0.1.2}/tests/test_permission_patterns.py +0 -0
- {sliceagent-0.1.0 → sliceagent-0.1.2}/tests/test_pinned_composer.py +0 -0
- {sliceagent-0.1.0 → sliceagent-0.1.2}/tests/test_plan_tier.py +0 -0
- {sliceagent-0.1.0 → sliceagent-0.1.2}/tests/test_procman.py +0 -0
- {sliceagent-0.1.0 → sliceagent-0.1.2}/tests/test_product_features.py +0 -0
- {sliceagent-0.1.0 → sliceagent-0.1.2}/tests/test_progress_status.py +0 -0
- {sliceagent-0.1.0 → sliceagent-0.1.2}/tests/test_provenance.py +0 -0
- {sliceagent-0.1.0 → sliceagent-0.1.2}/tests/test_proxy_interrupt.py +0 -0
- {sliceagent-0.1.0 → sliceagent-0.1.2}/tests/test_reach.py +0 -0
- {sliceagent-0.1.0 → sliceagent-0.1.2}/tests/test_readonly_subagent.py +0 -0
- {sliceagent-0.1.0 → sliceagent-0.1.2}/tests/test_recall_search.py +0 -0
- {sliceagent-0.1.0 → sliceagent-0.1.2}/tests/test_records.py +0 -0
- {sliceagent-0.1.0 → sliceagent-0.1.2}/tests/test_refault.py +0 -0
- {sliceagent-0.1.0 → sliceagent-0.1.2}/tests/test_registry_validation.py +0 -0
- {sliceagent-0.1.0 → sliceagent-0.1.2}/tests/test_reliability.py +0 -0
- {sliceagent-0.1.0 → sliceagent-0.1.2}/tests/test_requirements.py +0 -0
- {sliceagent-0.1.0 → sliceagent-0.1.2}/tests/test_review_fixes.py +0 -0
- {sliceagent-0.1.0 → sliceagent-0.1.2}/tests/test_route_lexical.py +0 -0
- {sliceagent-0.1.0 → sliceagent-0.1.2}/tests/test_safety.py +0 -0
- {sliceagent-0.1.0 → sliceagent-0.1.2}/tests/test_seal_markdown.py +0 -0
- {sliceagent-0.1.0 → sliceagent-0.1.2}/tests/test_search_index.py +0 -0
- {sliceagent-0.1.0 → sliceagent-0.1.2}/tests/test_skill_meta.py +0 -0
- {sliceagent-0.1.0 → sliceagent-0.1.2}/tests/test_skill_metadata.py +0 -0
- {sliceagent-0.1.0 → sliceagent-0.1.2}/tests/test_subdir_hints.py +0 -0
- {sliceagent-0.1.0 → sliceagent-0.1.2}/tests/test_task_agnostic.py +0 -0
- {sliceagent-0.1.0 → sliceagent-0.1.2}/tests/test_task_state_roundtrip.py +0 -0
- {sliceagent-0.1.0 → sliceagent-0.1.2}/tests/test_telemetry_convergence.py +0 -0
- {sliceagent-0.1.0 → sliceagent-0.1.2}/tests/test_terminal.py +0 -0
- {sliceagent-0.1.0 → sliceagent-0.1.2}/tests/test_tool_dedup.py +0 -0
- {sliceagent-0.1.0 → sliceagent-0.1.2}/tests/test_tool_result_ok.py +0 -0
- {sliceagent-0.1.0 → sliceagent-0.1.2}/tests/test_tools_robust.py +0 -0
- {sliceagent-0.1.0 → sliceagent-0.1.2}/tests/test_topic_switch.py +0 -0
- {sliceagent-0.1.0 → sliceagent-0.1.2}/tests/test_trajectory.py +0 -0
- {sliceagent-0.1.0 → sliceagent-0.1.2}/tests/test_tui.py +0 -0
- {sliceagent-0.1.0 → sliceagent-0.1.2}/tests/test_tui_menus.py +0 -0
- {sliceagent-0.1.0 → sliceagent-0.1.2}/tests/test_tui_render.py +0 -0
- {sliceagent-0.1.0 → sliceagent-0.1.2}/tests/test_tui_widgets.py +0 -0
- {sliceagent-0.1.0 → sliceagent-0.1.2}/tests/test_turn_budget.py +0 -0
- {sliceagent-0.1.0 → sliceagent-0.1.2}/tests/test_verification_agent.py +0 -0
- {sliceagent-0.1.0 → sliceagent-0.1.2}/tests/test_web.py +0 -0
- {sliceagent-0.1.0 → sliceagent-0.1.2}/tests/test_workspace.py +0 -0
- {sliceagent-0.1.0 → sliceagent-0.1.2}/tests/test_world_region.py +0 -0
- {sliceagent-0.1.0 → sliceagent-0.1.2}/tests/test_world_state.py +0 -0
|
@@ -5,6 +5,37 @@ this project aims for [Semantic Versioning](https://semver.org/).
|
|
|
5
5
|
|
|
6
6
|
## [Unreleased]
|
|
7
7
|
|
|
8
|
+
## [0.1.2] — 2026-07-02
|
|
9
|
+
|
|
10
|
+
Nothing may hang the user — three fixes from a live first-run reproduction.
|
|
11
|
+
|
|
12
|
+
### Fixed
|
|
13
|
+
- **Repo-map walk is hard-bounded** (`max_dirs` budget): the first slice build can no longer hang for
|
|
14
|
+
minutes when the workspace root is huge (a home directory mistaken for a project, a giant monorepo).
|
|
15
|
+
Output caps existed; now the walk itself is bounded — worst case ~1s, maps what it saw.
|
|
16
|
+
- **The OS-account home never counts as a project root**, independent of `$HOME`: a stray
|
|
17
|
+
`package.json` in the real home no longer turns the entire home directory into a "project"
|
|
18
|
+
(the prior guard compared against `$HOME`, which containers/sandboxes/sudo can override).
|
|
19
|
+
- **Ctrl-C during the slice build cancels the turn cleanly** (`· cancelled`) instead of crashing the
|
|
20
|
+
REPL with a traceback — the build phase ran before the turn's interrupt handling.
|
|
21
|
+
|
|
22
|
+
## [0.1.1] — 2026-07-02
|
|
23
|
+
|
|
24
|
+
First-run onboarding, hardened by a live stranger walkthrough.
|
|
25
|
+
|
|
26
|
+
### Added
|
|
27
|
+
- **Auto-onboarding** — a bare interactive `sliceagent` with nothing configured now drops straight
|
|
28
|
+
into the guided setup wizard (provider → key → model → live test), then continues into the session.
|
|
29
|
+
No more "go run `sliceagent init`" bounce. Piped/CI runs keep the print-and-exit gate.
|
|
30
|
+
- **Installer handles everything** — `install.sh` now also installs ripgrep (brew when available,
|
|
31
|
+
else an isolated ~2 MB static binary; no sudo) and pins `uv tool install --python 3.12`, so the
|
|
32
|
+
curl path has zero prerequisites even when the default Python is 3.9/3.10 (conda base, Ubuntu 22.04).
|
|
33
|
+
|
|
34
|
+
### Changed
|
|
35
|
+
- README: the curl installer is the primary install path; PyPI/pip is the "you manage the Python"
|
|
36
|
+
alternative. (Project renamed memagent → sliceagent the same day, before 0.1.1: PyPI's
|
|
37
|
+
name-similarity check blocks "memagent".)
|
|
38
|
+
|
|
8
39
|
## [0.1.0] — 2026-07-02
|
|
9
40
|
|
|
10
41
|
First public release.
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: sliceagent
|
|
3
|
-
Version: 0.1.
|
|
3
|
+
Version: 0.1.2
|
|
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
|
|
@@ -116,21 +116,31 @@ Early, but the **core bet is validated** — see the measured head-to-head bench
|
|
|
116
116
|
|
|
117
117
|
## Install
|
|
118
118
|
|
|
119
|
-
|
|
119
|
+
One command — Linux, macOS, WSL2:
|
|
120
120
|
|
|
121
121
|
```bash
|
|
122
|
-
|
|
123
|
-
pipx install "sliceagent[tui]" # pipx
|
|
124
|
-
pip install "sliceagent[tui]" # plain pip
|
|
122
|
+
curl -fsSL https://raw.githubusercontent.com/TT-Wang/sliceagent/main/install.sh | sh
|
|
125
123
|
```
|
|
126
124
|
|
|
127
|
-
|
|
125
|
+
**The installer handles everything**: `uv`, its own Python 3.12, ripgrep, and sliceagent — in an isolated tool env, no sudo, no prerequisites, no conflicts with any Python you already have (conda base at 3.10? Rosetta-Intel conda on an M-series Mac? Doesn't matter). Then:
|
|
128
126
|
|
|
129
127
|
```bash
|
|
130
|
-
|
|
128
|
+
sliceagent # first run drops you straight into guided setup, then start chatting
|
|
131
129
|
```
|
|
132
130
|
|
|
133
|
-
|
|
131
|
+
<details>
|
|
132
|
+
<summary><b>Alternative: install from PyPI yourself</b> (you manage the Python — needs ≥ 3.11)</summary>
|
|
133
|
+
|
|
134
|
+
```bash
|
|
135
|
+
uv tool install --python 3.12 "sliceagent[tui]" # uv — fetches Python itself
|
|
136
|
+
pipx install "sliceagent[tui]" # pipx
|
|
137
|
+
pip install "sliceagent[tui]" # plain pip (use a venv)
|
|
138
|
+
```
|
|
139
|
+
|
|
140
|
+
If `pip` refuses with `Requires-Python >=3.11`: `conda create -n sliceagent python=3.12 -y && conda activate sliceagent`, then pip install. `ripgrep` is recommended (code search degrades gracefully without it).
|
|
141
|
+
</details>
|
|
142
|
+
|
|
143
|
+
Footprint is light (no torch). `pip install -e .` works for a clone too. Homebrew / Docker arrive in v0.2.
|
|
134
144
|
|
|
135
145
|
## Quickstart
|
|
136
146
|
|
|
@@ -83,21 +83,31 @@ Early, but the **core bet is validated** — see the measured head-to-head bench
|
|
|
83
83
|
|
|
84
84
|
## Install
|
|
85
85
|
|
|
86
|
-
|
|
86
|
+
One command — Linux, macOS, WSL2:
|
|
87
87
|
|
|
88
88
|
```bash
|
|
89
|
-
|
|
90
|
-
pipx install "sliceagent[tui]" # pipx
|
|
91
|
-
pip install "sliceagent[tui]" # plain pip
|
|
89
|
+
curl -fsSL https://raw.githubusercontent.com/TT-Wang/sliceagent/main/install.sh | sh
|
|
92
90
|
```
|
|
93
91
|
|
|
94
|
-
|
|
92
|
+
**The installer handles everything**: `uv`, its own Python 3.12, ripgrep, and sliceagent — in an isolated tool env, no sudo, no prerequisites, no conflicts with any Python you already have (conda base at 3.10? Rosetta-Intel conda on an M-series Mac? Doesn't matter). Then:
|
|
95
93
|
|
|
96
94
|
```bash
|
|
97
|
-
|
|
95
|
+
sliceagent # first run drops you straight into guided setup, then start chatting
|
|
98
96
|
```
|
|
99
97
|
|
|
100
|
-
|
|
98
|
+
<details>
|
|
99
|
+
<summary><b>Alternative: install from PyPI yourself</b> (you manage the Python — needs ≥ 3.11)</summary>
|
|
100
|
+
|
|
101
|
+
```bash
|
|
102
|
+
uv tool install --python 3.12 "sliceagent[tui]" # uv — fetches Python itself
|
|
103
|
+
pipx install "sliceagent[tui]" # pipx
|
|
104
|
+
pip install "sliceagent[tui]" # plain pip (use a venv)
|
|
105
|
+
```
|
|
106
|
+
|
|
107
|
+
If `pip` refuses with `Requires-Python >=3.11`: `conda create -n sliceagent python=3.12 -y && conda activate sliceagent`, then pip install. `ripgrep` is recommended (code search degrades gracefully without it).
|
|
108
|
+
</details>
|
|
109
|
+
|
|
110
|
+
Footprint is light (no torch). `pip install -e .` works for a clone too. Homebrew / Docker arrive in v0.2.
|
|
101
111
|
|
|
102
112
|
## Quickstart
|
|
103
113
|
|
|
@@ -44,16 +44,52 @@ if ! command -v uv >/dev/null 2>&1; then
|
|
|
44
44
|
fi
|
|
45
45
|
|
|
46
46
|
# 2. install (or upgrade) sliceagent as an isolated uv tool
|
|
47
|
+
# --python 3.12: don't inherit whatever python happens to be on PATH (conda base = 3.10,
|
|
48
|
+
# Ubuntu 22.04 = 3.10, macOS system = 3.9 — all below the >=3.11 floor). uv fetches a managed
|
|
49
|
+
# CPython 3.12 automatically when none is installed, so the installer has zero prerequisites.
|
|
47
50
|
info "Installing sliceagent …"
|
|
48
|
-
uv tool install --force "$PKG"
|
|
51
|
+
uv tool install --force --python 3.12 "$PKG"
|
|
49
52
|
|
|
50
53
|
# 3. make sure uv's tool bin is on PATH for future shells
|
|
51
54
|
uv tool update-shell >/dev/null 2>&1 || warn "Could not auto-update PATH — you may need to add uv's tool bin (see 'uv tool dir') to your PATH."
|
|
52
55
|
|
|
53
|
-
# 4.
|
|
56
|
+
# 4. ripgrep powers the code index — install it too (brew when available, else a ~2 MB static
|
|
57
|
+
# binary from GitHub into uv's tool bin: no sudo, isolated, removable with the rest).
|
|
54
58
|
if ! command -v rg >/dev/null 2>&1; then
|
|
55
|
-
|
|
56
|
-
|
|
59
|
+
info "Installing ripgrep (code search) …"
|
|
60
|
+
if command -v brew >/dev/null 2>&1; then
|
|
61
|
+
brew install ripgrep >/dev/null 2>&1 || true
|
|
62
|
+
fi
|
|
63
|
+
fi
|
|
64
|
+
if ! command -v rg >/dev/null 2>&1; then
|
|
65
|
+
RG_VER="14.1.1"
|
|
66
|
+
case "$(uname -s)-$(uname -m)" in
|
|
67
|
+
Darwin-arm64) RG_TARGET="aarch64-apple-darwin" ;;
|
|
68
|
+
Darwin-x86_64) RG_TARGET="x86_64-apple-darwin" ;;
|
|
69
|
+
Linux-x86_64) RG_TARGET="x86_64-unknown-linux-musl" ;;
|
|
70
|
+
Linux-aarch64|Linux-arm64) RG_TARGET="aarch64-unknown-linux-gnu" ;;
|
|
71
|
+
*) RG_TARGET="" ;;
|
|
72
|
+
esac
|
|
73
|
+
BIN_DIR="$(uv tool dir --bin 2>/dev/null || true)"
|
|
74
|
+
[ -n "$BIN_DIR" ] || BIN_DIR="$HOME/.local/bin"
|
|
75
|
+
if [ -n "$RG_TARGET" ]; then
|
|
76
|
+
RG_TMP="$(mktemp -d)"
|
|
77
|
+
RG_URL="https://github.com/BurntSushi/ripgrep/releases/download/${RG_VER}/ripgrep-${RG_VER}-${RG_TARGET}.tar.gz"
|
|
78
|
+
if { command -v curl >/dev/null 2>&1 && curl -fsSL "$RG_URL" -o "$RG_TMP/rg.tgz"; } \
|
|
79
|
+
|| { command -v wget >/dev/null 2>&1 && wget -qO "$RG_TMP/rg.tgz" "$RG_URL"; }; then
|
|
80
|
+
mkdir -p "$BIN_DIR" \
|
|
81
|
+
&& tar -xzf "$RG_TMP/rg.tgz" -C "$RG_TMP" \
|
|
82
|
+
&& mv "$RG_TMP/ripgrep-${RG_VER}-${RG_TARGET}/rg" "$BIN_DIR/rg" \
|
|
83
|
+
&& chmod +x "$BIN_DIR/rg" \
|
|
84
|
+
&& info "ripgrep installed to $BIN_DIR/rg" \
|
|
85
|
+
|| warn "Could not unpack ripgrep — sliceagent still works, code search is just weaker without it."
|
|
86
|
+
else
|
|
87
|
+
warn "Could not download ripgrep — sliceagent still works, code search is just weaker without it."
|
|
88
|
+
fi
|
|
89
|
+
rm -rf "$RG_TMP"
|
|
90
|
+
else
|
|
91
|
+
warn "No prebuilt ripgrep for this platform — sliceagent works without it (weaker code search)."
|
|
92
|
+
fi
|
|
57
93
|
fi
|
|
58
94
|
|
|
59
95
|
cat <<'EOF'
|
|
@@ -163,12 +163,32 @@ def main() -> None:
|
|
|
163
163
|
# config-persisted key/endpoint (written by `sliceagent init`) populate the env BEFORE the gate, so a
|
|
164
164
|
# configured user never has to export anything; ENV still wins for one-off overrides.
|
|
165
165
|
from .config import load_config
|
|
166
|
+
|
|
167
|
+
def _env_from_config(c) -> None:
|
|
168
|
+
for _env, _val in (("LLM_API_KEY", c.api_key), ("LLM_BASE_URL", c.base_url)):
|
|
169
|
+
if not os.environ.get(_env) and _val:
|
|
170
|
+
os.environ[_env] = _val
|
|
171
|
+
|
|
172
|
+
def _key_present() -> bool:
|
|
173
|
+
return bool(os.environ.get("LLM_API_KEY") or os.environ.get("OPENAI_API_KEY")
|
|
174
|
+
or os.environ.get("MOONSHOT_API_KEY"))
|
|
175
|
+
|
|
176
|
+
def _first_run_setup(reason: str) -> bool:
|
|
177
|
+
"""First-run UX: a bare interactive `sliceagent` with nothing configured drops STRAIGHT into the
|
|
178
|
+
init wizard instead of bouncing the user to a separate command. Non-interactive (piped/CI) keeps
|
|
179
|
+
the print-and-exit gate — never prompt into a pipe. Returns True when the wizard completed."""
|
|
180
|
+
if not (sys.stdin.isatty() and sys.stdout.isatty()):
|
|
181
|
+
return False
|
|
182
|
+
print(f" {reason} — starting guided setup.\n")
|
|
183
|
+
from .onboarding import run_init
|
|
184
|
+
return run_init() == 0
|
|
185
|
+
|
|
166
186
|
cfg = load_config()
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
187
|
+
_env_from_config(cfg)
|
|
188
|
+
if not _key_present() and _first_run_setup("Welcome! No API key configured yet"):
|
|
189
|
+
cfg = load_config() # the wizard just wrote ~/.sliceagent/config.toml — pick it up
|
|
190
|
+
_env_from_config(cfg)
|
|
191
|
+
if not _key_present():
|
|
172
192
|
print("No API key found. Run `sliceagent init` for guided setup, or set LLM_API_KEY (e.g. in a .env file).")
|
|
173
193
|
sys.exit(1)
|
|
174
194
|
# validate enum env vars (warn + use default; never crash) — a typo'd AGENT_POLICY is now visible.
|
|
@@ -211,6 +231,10 @@ def main() -> None:
|
|
|
211
231
|
policy = make_policy("letitgo" if CONFIRMS.get(canonical) else canonical)
|
|
212
232
|
# model + reasoning resolution: explicit env wins, then the saved /model choice (prefs), then config.
|
|
213
233
|
_model = os.environ.get("AGENT_MODEL") or _prefs.get("model") or cfg.model
|
|
234
|
+
if not _model and _first_run_setup("No model configured yet"):
|
|
235
|
+
cfg = load_config() # wizard picks provider+model → re-resolve from the fresh config
|
|
236
|
+
_env_from_config(cfg)
|
|
237
|
+
_model = os.environ.get("AGENT_MODEL") or _prefs.get("model") or cfg.model
|
|
214
238
|
if not _model: # no built-in default — the user picks the model (parallels the API-key gate above)
|
|
215
239
|
print("No model configured. Run `sliceagent init` to pick a provider + model, "
|
|
216
240
|
"or set AGENT_MODEL to your model name.")
|
|
@@ -807,7 +831,13 @@ def main() -> None:
|
|
|
807
831
|
_stats["topic"] = one_line(session.active().goal, 40) if session.active_id else ""
|
|
808
832
|
record_user(session.active(), line) # short-range continuity: the RECENT CONVERSATION tier
|
|
809
833
|
_expand_mentions(line) # @path → pin the file into OPEN FILES
|
|
810
|
-
|
|
834
|
+
try:
|
|
835
|
+
# slice-build phase happens BEFORE run_turn's own KeyboardInterrupt handling — a ctrl-c
|
|
836
|
+
# here (e.g. during the one-time repo-map build) must cancel the turn, not crash the REPL.
|
|
837
|
+
build = make_build_slice(session, tools, retriever, memory, line, session.session_id)
|
|
838
|
+
except KeyboardInterrupt:
|
|
839
|
+
print("\n · cancelled")
|
|
840
|
+
continue
|
|
811
841
|
if os.environ.get("AGENT_TIMING"): # per-turn latency breakdown (build vs model) → find the hang
|
|
812
842
|
import time as _tt
|
|
813
843
|
_b = build
|
|
@@ -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
|
-
|
|
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
|
|
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
|
|
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))
|
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
"""End-to-end launch smoke test: `sliceagent` must reach the input prompt WITHOUT crashing.
|
|
2
|
+
|
|
3
|
+
Regression guard for the cli.py banner `NameError: name 'policy_mode' is not defined` (the var was
|
|
4
|
+
renamed to the resolved `_eff_mode`/`canonical` in the 3-modes change but the banner still referenced the
|
|
5
|
+
old name). No prior test exercised `main()` end-to-end, so the crash shipped. Runs main() in a subprocess
|
|
6
|
+
with a fake key + AGENT_TUI=off + EOF stdin, so it boots, prints the banner, and exits on EOF — no network.
|
|
7
|
+
"""
|
|
8
|
+
import os
|
|
9
|
+
import subprocess
|
|
10
|
+
import sys
|
|
11
|
+
|
|
12
|
+
_ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def _launch(extra=None):
|
|
16
|
+
import tempfile
|
|
17
|
+
env = dict(os.environ)
|
|
18
|
+
env.update({
|
|
19
|
+
"PYTHONPATH": os.path.join(_ROOT, "src"),
|
|
20
|
+
"HOME": tempfile.mkdtemp(prefix="smoke-home-"), # hermetic: the dev machine's ~/.sliceagent
|
|
21
|
+
# config must not leak in (CI has none either)
|
|
22
|
+
"AGENT_TUI": "off", # plain REPL — no prompt_toolkit app to drive
|
|
23
|
+
"LLM_API_KEY": "sk-dummy-smoke", "OPENAI_API_KEY": "sk-dummy-smoke",
|
|
24
|
+
"AGENT_MODEL": "dummy-model-smoke", # required since the no-default-model gate — without it the
|
|
25
|
+
# CLI exits at the gate before the banner this test asserts on
|
|
26
|
+
"AGENT_PROXY": "off", # don't route through a local proxy that isn't there
|
|
27
|
+
})
|
|
28
|
+
env.update(extra or {})
|
|
29
|
+
return subprocess.run(
|
|
30
|
+
[sys.executable, "-c", "from sliceagent.cli import main; main()"],
|
|
31
|
+
cwd=_ROOT, env=env, stdin=subprocess.DEVNULL,
|
|
32
|
+
capture_output=True, text=True, timeout=120,
|
|
33
|
+
)
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def main_reaches_prompt_without_crashing():
|
|
37
|
+
r = _launch()
|
|
38
|
+
out = r.stdout + r.stderr
|
|
39
|
+
assert "Traceback" not in out and "NameError" not in out, out[-2000:]
|
|
40
|
+
assert "policy=" in out, "startup banner never rendered (the line that held the NameError):\n" + out[-1500:]
|
|
41
|
+
assert r.returncode == 0, f"nonzero exit {r.returncode}\n{out[-1500:]}"
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def _no_key_env():
|
|
45
|
+
"""A truly blank first-run env: temp HOME, every key/model var stripped."""
|
|
46
|
+
import tempfile
|
|
47
|
+
env = {k: v for k, v in os.environ.items()
|
|
48
|
+
if k not in ("LLM_API_KEY", "OPENAI_API_KEY", "MOONSHOT_API_KEY", "DEEPSEEK_API_KEY",
|
|
49
|
+
"AGENT_MODEL", "LLM_BASE_URL", "AGENT_PROVIDER")}
|
|
50
|
+
env.update({"PYTHONPATH": os.path.join(_ROOT, "src"),
|
|
51
|
+
"HOME": tempfile.mkdtemp(prefix="firstrun-home-"),
|
|
52
|
+
"AGENT_TUI": "off", "AGENT_PROXY": "off"})
|
|
53
|
+
return env
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def piped_no_key_run_keeps_the_gate_no_prompt():
|
|
57
|
+
"""Non-interactive (stdin=pipe) + no key must print the gate and exit 1 — never start the wizard
|
|
58
|
+
(a prompt into a pipe would hang CI/scripts)."""
|
|
59
|
+
r = subprocess.run([sys.executable, "-c", "from sliceagent.cli import main; main()"],
|
|
60
|
+
cwd=_ROOT, env=_no_key_env(), stdin=subprocess.DEVNULL,
|
|
61
|
+
capture_output=True, text=True, timeout=60)
|
|
62
|
+
out = r.stdout + r.stderr
|
|
63
|
+
assert "No API key found" in out, out[-800:]
|
|
64
|
+
assert "guided setup.\n\n" not in out and "sliceagent setup" not in out, \
|
|
65
|
+
"wizard must not auto-start without a tty:\n" + out[-800:]
|
|
66
|
+
assert r.returncode == 1, f"expected gate exit 1, got {r.returncode}"
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
def interactive_first_run_auto_starts_the_wizard():
|
|
70
|
+
"""First-run UX: a bare interactive `sliceagent` (tty, nothing configured) drops straight into the
|
|
71
|
+
init wizard — proven on a REAL pty by the wizard header + provider menu appearing unprompted.
|
|
72
|
+
(The abort path is covered in-process below; macOS getpass on a detached pty is not reliably
|
|
73
|
+
drivable, and the wizard's own logic has an injectable seam for exactly that reason.)"""
|
|
74
|
+
import pty
|
|
75
|
+
import select
|
|
76
|
+
import signal
|
|
77
|
+
import time
|
|
78
|
+
try:
|
|
79
|
+
m, s = pty.openpty()
|
|
80
|
+
except OSError:
|
|
81
|
+
return # no pty on this host — skip
|
|
82
|
+
p = subprocess.Popen([sys.executable, "-c", "from sliceagent.cli import main; main()"],
|
|
83
|
+
cwd=_ROOT, env=_no_key_env(), stdin=s, stdout=s, stderr=s,
|
|
84
|
+
start_new_session=True)
|
|
85
|
+
os.close(s)
|
|
86
|
+
buf = bytearray()
|
|
87
|
+
deadline = time.monotonic() + 30
|
|
88
|
+
while time.monotonic() < deadline and b"Choose a provider" not in buf:
|
|
89
|
+
r, _, _ = select.select([m], [], [], 0.2)
|
|
90
|
+
if r:
|
|
91
|
+
try:
|
|
92
|
+
buf.extend(os.read(m, 4096))
|
|
93
|
+
except OSError:
|
|
94
|
+
break
|
|
95
|
+
if p.poll() is not None:
|
|
96
|
+
break
|
|
97
|
+
try:
|
|
98
|
+
os.killpg(p.pid, signal.SIGKILL) # wizard reached (or not) — tear the child down either way
|
|
99
|
+
except (ProcessLookupError, PermissionError):
|
|
100
|
+
pass
|
|
101
|
+
p.wait()
|
|
102
|
+
try:
|
|
103
|
+
os.close(m)
|
|
104
|
+
except OSError:
|
|
105
|
+
pass
|
|
106
|
+
out = buf.decode(errors="replace")
|
|
107
|
+
assert "starting guided setup" in out, "wizard did not auto-start on an interactive first run:\n" + out[-1200:]
|
|
108
|
+
assert "sliceagent setup" in out and "Choose a provider" in out, "wizard header/menu missing:\n" + out[-1200:]
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
def aborted_wizard_falls_back_to_the_gate():
|
|
112
|
+
"""If the auto-started wizard is aborted (returns nonzero), main() must fall back to the plain
|
|
113
|
+
gate message and exit 1 — never proceed keyless. In-process with the wizard mocked, tty faked."""
|
|
114
|
+
import contextlib
|
|
115
|
+
import io
|
|
116
|
+
import tempfile
|
|
117
|
+
from types import SimpleNamespace
|
|
118
|
+
from unittest import mock
|
|
119
|
+
|
|
120
|
+
from sliceagent import cli as cli_mod
|
|
121
|
+
from sliceagent import onboarding as ob
|
|
122
|
+
|
|
123
|
+
env_patch = {k: "" for k in ("LLM_API_KEY", "OPENAI_API_KEY", "MOONSHOT_API_KEY", "AGENT_MODEL")}
|
|
124
|
+
env_patch["HOME"] = tempfile.mkdtemp(prefix="firstrun-abort-")
|
|
125
|
+
out = io.StringIO()
|
|
126
|
+
# one fake object doubling as tty-stdin (isatty only) and capturing tty-stdout: redirect_stdout
|
|
127
|
+
# would swap in a StringIO whose isatty() is False and silently close the wizard path.
|
|
128
|
+
fake_tty = SimpleNamespace(isatty=lambda: True, write=out.write, flush=lambda: None)
|
|
129
|
+
called = {"n": 0}
|
|
130
|
+
|
|
131
|
+
def _fake_init():
|
|
132
|
+
called["n"] += 1
|
|
133
|
+
return 1 # user aborted the wizard
|
|
134
|
+
|
|
135
|
+
_ = contextlib # (kept import shape stable)
|
|
136
|
+
with mock.patch.dict(os.environ, env_patch), \
|
|
137
|
+
mock.patch.object(ob, "run_init", _fake_init), \
|
|
138
|
+
mock.patch.object(sys, "argv", ["sliceagent"]), \
|
|
139
|
+
mock.patch.object(sys, "stdin", fake_tty), \
|
|
140
|
+
mock.patch.object(sys, "stdout", fake_tty):
|
|
141
|
+
try:
|
|
142
|
+
cli_mod.main()
|
|
143
|
+
raise AssertionError("main() must exit after an aborted wizard")
|
|
144
|
+
except SystemExit as e:
|
|
145
|
+
assert e.code == 1, f"expected exit 1, got {e.code}"
|
|
146
|
+
text = out.getvalue()
|
|
147
|
+
assert called["n"] == 1, "the wizard was never invoked"
|
|
148
|
+
assert "No API key found" in text, "aborted wizard must fall back to the gate message:\n" + text
|
|
149
|
+
|
|
150
|
+
|
|
151
|
+
if __name__ == "__main__":
|
|
152
|
+
main_reaches_prompt_without_crashing()
|
|
153
|
+
print("PASS main_reaches_prompt_without_crashing")
|
|
154
|
+
piped_no_key_run_keeps_the_gate_no_prompt()
|
|
155
|
+
print("PASS piped_no_key_run_keeps_the_gate_no_prompt")
|
|
156
|
+
interactive_first_run_auto_starts_the_wizard()
|
|
157
|
+
print("PASS interactive_first_run_auto_starts_the_wizard")
|
|
158
|
+
aborted_wizard_falls_back_to_the_gate()
|
|
159
|
+
print("PASS aborted_wizard_falls_back_to_the_gate")
|
|
@@ -9,6 +9,12 @@ ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
|
|
9
9
|
sys.path.insert(0, os.path.join(ROOT, "src"))
|
|
10
10
|
sys.path.insert(0, os.path.join(ROOT, "evals"))
|
|
11
11
|
|
|
12
|
+
# The prompt A/B harness lives in evals/, which is local-only dev tooling (not shipped in the public
|
|
13
|
+
# repo) — skip cleanly where it's absent (e.g. CI on the public checkout), like the no-pty skips.
|
|
14
|
+
if not os.path.isdir(os.path.join(ROOT, "evals", "prompt_ab")):
|
|
15
|
+
print("SKIP: evals/prompt_ab not present (local-only eval tooling)")
|
|
16
|
+
sys.exit(0)
|
|
17
|
+
|
|
12
18
|
from prompt_ab import stats as S # noqa: E402
|
|
13
19
|
from prompt_ab import variants as V # noqa: E402
|
|
14
20
|
|
|
@@ -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)
|
|
@@ -1,41 +0,0 @@
|
|
|
1
|
-
"""End-to-end launch smoke test: `sliceagent` must reach the input prompt WITHOUT crashing.
|
|
2
|
-
|
|
3
|
-
Regression guard for the cli.py banner `NameError: name 'policy_mode' is not defined` (the var was
|
|
4
|
-
renamed to the resolved `_eff_mode`/`canonical` in the 3-modes change but the banner still referenced the
|
|
5
|
-
old name). No prior test exercised `main()` end-to-end, so the crash shipped. Runs main() in a subprocess
|
|
6
|
-
with a fake key + AGENT_TUI=off + EOF stdin, so it boots, prints the banner, and exits on EOF — no network.
|
|
7
|
-
"""
|
|
8
|
-
import os
|
|
9
|
-
import subprocess
|
|
10
|
-
import sys
|
|
11
|
-
|
|
12
|
-
_ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
def _launch(extra=None):
|
|
16
|
-
env = dict(os.environ)
|
|
17
|
-
env.update({
|
|
18
|
-
"PYTHONPATH": os.path.join(_ROOT, "src"),
|
|
19
|
-
"AGENT_TUI": "off", # plain REPL — no prompt_toolkit app to drive
|
|
20
|
-
"LLM_API_KEY": "sk-dummy-smoke", "OPENAI_API_KEY": "sk-dummy-smoke",
|
|
21
|
-
"AGENT_PROXY": "off", # don't route through a local proxy that isn't there
|
|
22
|
-
})
|
|
23
|
-
env.update(extra or {})
|
|
24
|
-
return subprocess.run(
|
|
25
|
-
[sys.executable, "-c", "from sliceagent.cli import main; main()"],
|
|
26
|
-
cwd=_ROOT, env=env, stdin=subprocess.DEVNULL,
|
|
27
|
-
capture_output=True, text=True, timeout=120,
|
|
28
|
-
)
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
def main_reaches_prompt_without_crashing():
|
|
32
|
-
r = _launch()
|
|
33
|
-
out = r.stdout + r.stderr
|
|
34
|
-
assert "Traceback" not in out and "NameError" not in out, out[-2000:]
|
|
35
|
-
assert "policy=" in out, "startup banner never rendered (the line that held the NameError):\n" + out[-1500:]
|
|
36
|
-
assert r.returncode == 0, f"nonzero exit {r.returncode}\n{out[-1500:]}"
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
if __name__ == "__main__":
|
|
40
|
-
main_reaches_prompt_without_crashing()
|
|
41
|
-
print("PASS main_reaches_prompt_without_crashing")
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|