specsmith 0.10.0.dev238__tar.gz → 0.10.0.dev239__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.
- {specsmith-0.10.0.dev238/src/specsmith.egg-info → specsmith-0.10.0.dev239}/PKG-INFO +20 -1
- {specsmith-0.10.0.dev238 → specsmith-0.10.0.dev239}/README.md +19 -0
- {specsmith-0.10.0.dev238 → specsmith-0.10.0.dev239}/pyproject.toml +1 -1
- {specsmith-0.10.0.dev238 → specsmith-0.10.0.dev239}/src/specsmith/agent/chat_runner.py +151 -30
- {specsmith-0.10.0.dev238 → specsmith-0.10.0.dev239}/src/specsmith/agent/profiles.py +87 -1
- {specsmith-0.10.0.dev238 → specsmith-0.10.0.dev239}/src/specsmith/agent/runner.py +41 -6
- {specsmith-0.10.0.dev238 → specsmith-0.10.0.dev239}/src/specsmith/cli.py +63 -9
- {specsmith-0.10.0.dev238 → specsmith-0.10.0.dev239/src/specsmith.egg-info}/PKG-INFO +20 -1
- {specsmith-0.10.0.dev238 → specsmith-0.10.0.dev239}/tests/test_chat_runner_openai_compat.py +6 -3
- {specsmith-0.10.0.dev238 → specsmith-0.10.0.dev239}/LICENSE +0 -0
- {specsmith-0.10.0.dev238 → specsmith-0.10.0.dev239}/setup.cfg +0 -0
- {specsmith-0.10.0.dev238 → specsmith-0.10.0.dev239}/src/epistemic/__init__.py +0 -0
- {specsmith-0.10.0.dev238 → specsmith-0.10.0.dev239}/src/epistemic/belief.py +0 -0
- {specsmith-0.10.0.dev238 → specsmith-0.10.0.dev239}/src/epistemic/certainty.py +0 -0
- {specsmith-0.10.0.dev238 → specsmith-0.10.0.dev239}/src/epistemic/failure_graph.py +0 -0
- {specsmith-0.10.0.dev238 → specsmith-0.10.0.dev239}/src/epistemic/py.typed +0 -0
- {specsmith-0.10.0.dev238 → specsmith-0.10.0.dev239}/src/epistemic/recovery.py +0 -0
- {specsmith-0.10.0.dev238 → specsmith-0.10.0.dev239}/src/epistemic/session.py +0 -0
- {specsmith-0.10.0.dev238 → specsmith-0.10.0.dev239}/src/epistemic/stress_tester.py +0 -0
- {specsmith-0.10.0.dev238 → specsmith-0.10.0.dev239}/src/epistemic/trace.py +0 -0
- {specsmith-0.10.0.dev238 → specsmith-0.10.0.dev239}/src/specsmith/__init__.py +0 -0
- {specsmith-0.10.0.dev238 → specsmith-0.10.0.dev239}/src/specsmith/__main__.py +0 -0
- {specsmith-0.10.0.dev238 → specsmith-0.10.0.dev239}/src/specsmith/agent/__init__.py +0 -0
- {specsmith-0.10.0.dev238 → specsmith-0.10.0.dev239}/src/specsmith/agent/broker.py +0 -0
- {specsmith-0.10.0.dev238 → specsmith-0.10.0.dev239}/src/specsmith/agent/cleanup.py +0 -0
- {specsmith-0.10.0.dev238 → specsmith-0.10.0.dev239}/src/specsmith/agent/core.py +0 -0
- {specsmith-0.10.0.dev238 → specsmith-0.10.0.dev239}/src/specsmith/agent/endpoints.py +0 -0
- {specsmith-0.10.0.dev238 → specsmith-0.10.0.dev239}/src/specsmith/agent/events.py +0 -0
- {specsmith-0.10.0.dev238 → specsmith-0.10.0.dev239}/src/specsmith/agent/fallback.py +0 -0
- {specsmith-0.10.0.dev238 → specsmith-0.10.0.dev239}/src/specsmith/agent/indexer.py +0 -0
- {specsmith-0.10.0.dev238 → specsmith-0.10.0.dev239}/src/specsmith/agent/mcp.py +0 -0
- {specsmith-0.10.0.dev238 → specsmith-0.10.0.dev239}/src/specsmith/agent/memory.py +0 -0
- {specsmith-0.10.0.dev238 → specsmith-0.10.0.dev239}/src/specsmith/agent/orchestrator.py +0 -0
- {specsmith-0.10.0.dev238 → specsmith-0.10.0.dev239}/src/specsmith/agent/repl.py +0 -0
- {specsmith-0.10.0.dev238 → specsmith-0.10.0.dev239}/src/specsmith/agent/router.py +0 -0
- {specsmith-0.10.0.dev238 → specsmith-0.10.0.dev239}/src/specsmith/agent/rules.py +0 -0
- {specsmith-0.10.0.dev238 → specsmith-0.10.0.dev239}/src/specsmith/agent/safety.py +0 -0
- {specsmith-0.10.0.dev238 → specsmith-0.10.0.dev239}/src/specsmith/agent/suggester.py +0 -0
- {specsmith-0.10.0.dev238 → specsmith-0.10.0.dev239}/src/specsmith/agent/tools.py +0 -0
- {specsmith-0.10.0.dev238 → specsmith-0.10.0.dev239}/src/specsmith/agent/verifier.py +0 -0
- {specsmith-0.10.0.dev238 → specsmith-0.10.0.dev239}/src/specsmith/agent/voice.py +0 -0
- {specsmith-0.10.0.dev238 → specsmith-0.10.0.dev239}/src/specsmith/architect.py +0 -0
- {specsmith-0.10.0.dev238 → specsmith-0.10.0.dev239}/src/specsmith/auditor.py +0 -0
- {specsmith-0.10.0.dev238 → specsmith-0.10.0.dev239}/src/specsmith/auth.py +0 -0
- {specsmith-0.10.0.dev238 → specsmith-0.10.0.dev239}/src/specsmith/block_export.py +0 -0
- {specsmith-0.10.0.dev238 → specsmith-0.10.0.dev239}/src/specsmith/commands/__init__.py +0 -0
- {specsmith-0.10.0.dev238 → specsmith-0.10.0.dev239}/src/specsmith/compressor.py +0 -0
- {specsmith-0.10.0.dev238 → specsmith-0.10.0.dev239}/src/specsmith/config.py +0 -0
- {specsmith-0.10.0.dev238 → specsmith-0.10.0.dev239}/src/specsmith/console_utils.py +0 -0
- {specsmith-0.10.0.dev238 → specsmith-0.10.0.dev239}/src/specsmith/credit_analyzer.py +0 -0
- {specsmith-0.10.0.dev238 → specsmith-0.10.0.dev239}/src/specsmith/credits.py +0 -0
- {specsmith-0.10.0.dev238 → specsmith-0.10.0.dev239}/src/specsmith/differ.py +0 -0
- {specsmith-0.10.0.dev238 → specsmith-0.10.0.dev239}/src/specsmith/doctor.py +0 -0
- {specsmith-0.10.0.dev238 → specsmith-0.10.0.dev239}/src/specsmith/drive.py +0 -0
- {specsmith-0.10.0.dev238 → specsmith-0.10.0.dev239}/src/specsmith/epistemic/__init__.py +0 -0
- {specsmith-0.10.0.dev238 → specsmith-0.10.0.dev239}/src/specsmith/epistemic/belief.py +0 -0
- {specsmith-0.10.0.dev238 → specsmith-0.10.0.dev239}/src/specsmith/epistemic/certainty.py +0 -0
- {specsmith-0.10.0.dev238 → specsmith-0.10.0.dev239}/src/specsmith/epistemic/failure_graph.py +0 -0
- {specsmith-0.10.0.dev238 → specsmith-0.10.0.dev239}/src/specsmith/epistemic/recovery.py +0 -0
- {specsmith-0.10.0.dev238 → specsmith-0.10.0.dev239}/src/specsmith/epistemic/stress_tester.py +0 -0
- {specsmith-0.10.0.dev238 → specsmith-0.10.0.dev239}/src/specsmith/executor.py +0 -0
- {specsmith-0.10.0.dev238 → specsmith-0.10.0.dev239}/src/specsmith/exporter.py +0 -0
- {specsmith-0.10.0.dev238 → specsmith-0.10.0.dev239}/src/specsmith/gui/__init__.py +0 -0
- {specsmith-0.10.0.dev238 → specsmith-0.10.0.dev239}/src/specsmith/gui/app.py +0 -0
- {specsmith-0.10.0.dev238 → specsmith-0.10.0.dev239}/src/specsmith/gui/main_window.py +0 -0
- {specsmith-0.10.0.dev238 → specsmith-0.10.0.dev239}/src/specsmith/gui/session_tab.py +0 -0
- {specsmith-0.10.0.dev238 → specsmith-0.10.0.dev239}/src/specsmith/gui/theme.py +0 -0
- {specsmith-0.10.0.dev238 → specsmith-0.10.0.dev239}/src/specsmith/gui/widgets/__init__.py +0 -0
- {specsmith-0.10.0.dev238 → specsmith-0.10.0.dev239}/src/specsmith/gui/widgets/chat_view.py +0 -0
- {specsmith-0.10.0.dev238 → specsmith-0.10.0.dev239}/src/specsmith/gui/widgets/input_bar.py +0 -0
- {specsmith-0.10.0.dev238 → specsmith-0.10.0.dev239}/src/specsmith/gui/widgets/provider_bar.py +0 -0
- {specsmith-0.10.0.dev238 → specsmith-0.10.0.dev239}/src/specsmith/gui/widgets/token_meter.py +0 -0
- {specsmith-0.10.0.dev238 → specsmith-0.10.0.dev239}/src/specsmith/gui/widgets/tool_panel.py +0 -0
- {specsmith-0.10.0.dev238 → specsmith-0.10.0.dev239}/src/specsmith/gui/widgets/update_checker.py +0 -0
- {specsmith-0.10.0.dev238 → specsmith-0.10.0.dev239}/src/specsmith/gui/worker.py +0 -0
- {specsmith-0.10.0.dev238 → specsmith-0.10.0.dev239}/src/specsmith/history_search.py +0 -0
- {specsmith-0.10.0.dev238 → specsmith-0.10.0.dev239}/src/specsmith/importer.py +0 -0
- {specsmith-0.10.0.dev238 → specsmith-0.10.0.dev239}/src/specsmith/integrations/__init__.py +0 -0
- {specsmith-0.10.0.dev238 → specsmith-0.10.0.dev239}/src/specsmith/integrations/agent_skill.py +0 -0
- {specsmith-0.10.0.dev238 → specsmith-0.10.0.dev239}/src/specsmith/integrations/aider.py +0 -0
- {specsmith-0.10.0.dev238 → specsmith-0.10.0.dev239}/src/specsmith/integrations/base.py +0 -0
- {specsmith-0.10.0.dev238 → specsmith-0.10.0.dev239}/src/specsmith/integrations/claude_code.py +0 -0
- {specsmith-0.10.0.dev238 → specsmith-0.10.0.dev239}/src/specsmith/integrations/copilot.py +0 -0
- {specsmith-0.10.0.dev238 → specsmith-0.10.0.dev239}/src/specsmith/integrations/cursor.py +0 -0
- {specsmith-0.10.0.dev238 → specsmith-0.10.0.dev239}/src/specsmith/integrations/gemini.py +0 -0
- {specsmith-0.10.0.dev238 → specsmith-0.10.0.dev239}/src/specsmith/integrations/windsurf.py +0 -0
- {specsmith-0.10.0.dev238 → specsmith-0.10.0.dev239}/src/specsmith/languages.py +0 -0
- {specsmith-0.10.0.dev238 → specsmith-0.10.0.dev239}/src/specsmith/ledger.py +0 -0
- {specsmith-0.10.0.dev238 → specsmith-0.10.0.dev239}/src/specsmith/patent.py +0 -0
- {specsmith-0.10.0.dev238 → specsmith-0.10.0.dev239}/src/specsmith/phase.py +0 -0
- {specsmith-0.10.0.dev238 → specsmith-0.10.0.dev239}/src/specsmith/plugins.py +0 -0
- {specsmith-0.10.0.dev238 → specsmith-0.10.0.dev239}/src/specsmith/profiles.py +0 -0
- {specsmith-0.10.0.dev238 → specsmith-0.10.0.dev239}/src/specsmith/rate_limits.py +0 -0
- {specsmith-0.10.0.dev238 → specsmith-0.10.0.dev239}/src/specsmith/releaser.py +0 -0
- {specsmith-0.10.0.dev238 → specsmith-0.10.0.dev239}/src/specsmith/requirements.py +0 -0
- {specsmith-0.10.0.dev238 → specsmith-0.10.0.dev239}/src/specsmith/requirements_parser.py +0 -0
- {specsmith-0.10.0.dev238 → specsmith-0.10.0.dev239}/src/specsmith/retrieval.py +0 -0
- {specsmith-0.10.0.dev238 → specsmith-0.10.0.dev239}/src/specsmith/scaffolder.py +0 -0
- {specsmith-0.10.0.dev238 → specsmith-0.10.0.dev239}/src/specsmith/serve.py +0 -0
- {specsmith-0.10.0.dev238 → specsmith-0.10.0.dev239}/src/specsmith/session.py +0 -0
- {specsmith-0.10.0.dev238 → specsmith-0.10.0.dev239}/src/specsmith/skills.py +0 -0
- {specsmith-0.10.0.dev238 → specsmith-0.10.0.dev239}/src/specsmith/templates/agents.md.j2 +0 -0
- {specsmith-0.10.0.dev238 → specsmith-0.10.0.dev239}/src/specsmith/templates/community/bug_report.md.j2 +0 -0
- {specsmith-0.10.0.dev238 → specsmith-0.10.0.dev239}/src/specsmith/templates/community/code_of_conduct.md.j2 +0 -0
- {specsmith-0.10.0.dev238 → specsmith-0.10.0.dev239}/src/specsmith/templates/community/contributing.md.j2 +0 -0
- {specsmith-0.10.0.dev238 → specsmith-0.10.0.dev239}/src/specsmith/templates/community/feature_request.md.j2 +0 -0
- {specsmith-0.10.0.dev238 → specsmith-0.10.0.dev239}/src/specsmith/templates/community/license-Apache-2.0.j2 +0 -0
- {specsmith-0.10.0.dev238 → specsmith-0.10.0.dev239}/src/specsmith/templates/community/license-MIT.j2 +0 -0
- {specsmith-0.10.0.dev238 → specsmith-0.10.0.dev239}/src/specsmith/templates/community/pull_request_template.md.j2 +0 -0
- {specsmith-0.10.0.dev238 → specsmith-0.10.0.dev239}/src/specsmith/templates/community/security.md.j2 +0 -0
- {specsmith-0.10.0.dev238 → specsmith-0.10.0.dev239}/src/specsmith/templates/docs/architecture.md.j2 +0 -0
- {specsmith-0.10.0.dev238 → specsmith-0.10.0.dev239}/src/specsmith/templates/docs/mkdocs.yml.j2 +0 -0
- {specsmith-0.10.0.dev238 → specsmith-0.10.0.dev239}/src/specsmith/templates/docs/readthedocs.yaml.j2 +0 -0
- {specsmith-0.10.0.dev238 → specsmith-0.10.0.dev239}/src/specsmith/templates/docs/requirements.md.j2 +0 -0
- {specsmith-0.10.0.dev238 → specsmith-0.10.0.dev239}/src/specsmith/templates/docs/test-spec.md.j2 +0 -0
- {specsmith-0.10.0.dev238 → specsmith-0.10.0.dev239}/src/specsmith/templates/editorconfig.j2 +0 -0
- {specsmith-0.10.0.dev238 → specsmith-0.10.0.dev239}/src/specsmith/templates/gitattributes.j2 +0 -0
- {specsmith-0.10.0.dev238 → specsmith-0.10.0.dev239}/src/specsmith/templates/gitignore.j2 +0 -0
- {specsmith-0.10.0.dev238 → specsmith-0.10.0.dev239}/src/specsmith/templates/go/go.mod.j2 +0 -0
- {specsmith-0.10.0.dev238 → specsmith-0.10.0.dev239}/src/specsmith/templates/go/main.go.j2 +0 -0
- {specsmith-0.10.0.dev238 → specsmith-0.10.0.dev239}/src/specsmith/templates/governance/belief-registry.md.j2 +0 -0
- {specsmith-0.10.0.dev238 → specsmith-0.10.0.dev239}/src/specsmith/templates/governance/context-budget.md.j2 +0 -0
- {specsmith-0.10.0.dev238 → specsmith-0.10.0.dev239}/src/specsmith/templates/governance/drift-metrics.md.j2 +0 -0
- {specsmith-0.10.0.dev238 → specsmith-0.10.0.dev239}/src/specsmith/templates/governance/epistemic-axioms.md.j2 +0 -0
- {specsmith-0.10.0.dev238 → specsmith-0.10.0.dev239}/src/specsmith/templates/governance/failure-modes.md.j2 +0 -0
- {specsmith-0.10.0.dev238 → specsmith-0.10.0.dev239}/src/specsmith/templates/governance/lifecycle.md.j2 +0 -0
- {specsmith-0.10.0.dev238 → specsmith-0.10.0.dev239}/src/specsmith/templates/governance/roles.md.j2 +0 -0
- {specsmith-0.10.0.dev238 → specsmith-0.10.0.dev239}/src/specsmith/templates/governance/rules.md.j2 +0 -0
- {specsmith-0.10.0.dev238 → specsmith-0.10.0.dev239}/src/specsmith/templates/governance/session-protocol.md.j2 +0 -0
- {specsmith-0.10.0.dev238 → specsmith-0.10.0.dev239}/src/specsmith/templates/governance/uncertainty-map.md.j2 +0 -0
- {specsmith-0.10.0.dev238 → specsmith-0.10.0.dev239}/src/specsmith/templates/governance/verification.md.j2 +0 -0
- {specsmith-0.10.0.dev238 → specsmith-0.10.0.dev239}/src/specsmith/templates/js/package.json.j2 +0 -0
- {specsmith-0.10.0.dev238 → specsmith-0.10.0.dev239}/src/specsmith/templates/ledger.md.j2 +0 -0
- {specsmith-0.10.0.dev238 → specsmith-0.10.0.dev239}/src/specsmith/templates/python/cli.py.j2 +0 -0
- {specsmith-0.10.0.dev238 → specsmith-0.10.0.dev239}/src/specsmith/templates/python/init.py.j2 +0 -0
- {specsmith-0.10.0.dev238 → specsmith-0.10.0.dev239}/src/specsmith/templates/python/pyproject.toml.j2 +0 -0
- {specsmith-0.10.0.dev238 → specsmith-0.10.0.dev239}/src/specsmith/templates/readme.md.j2 +0 -0
- {specsmith-0.10.0.dev238 → specsmith-0.10.0.dev239}/src/specsmith/templates/rust/Cargo.toml.j2 +0 -0
- {specsmith-0.10.0.dev238 → specsmith-0.10.0.dev239}/src/specsmith/templates/rust/main.rs.j2 +0 -0
- {specsmith-0.10.0.dev238 → specsmith-0.10.0.dev239}/src/specsmith/templates/scripts/exec.cmd.j2 +0 -0
- {specsmith-0.10.0.dev238 → specsmith-0.10.0.dev239}/src/specsmith/templates/scripts/exec.sh.j2 +0 -0
- {specsmith-0.10.0.dev238 → specsmith-0.10.0.dev239}/src/specsmith/templates/scripts/run.cmd.j2 +0 -0
- {specsmith-0.10.0.dev238 → specsmith-0.10.0.dev239}/src/specsmith/templates/scripts/run.sh.j2 +0 -0
- {specsmith-0.10.0.dev238 → specsmith-0.10.0.dev239}/src/specsmith/templates/scripts/setup.cmd.j2 +0 -0
- {specsmith-0.10.0.dev238 → specsmith-0.10.0.dev239}/src/specsmith/templates/scripts/setup.sh.j2 +0 -0
- {specsmith-0.10.0.dev238 → specsmith-0.10.0.dev239}/src/specsmith/templates/workflows/release.yml.j2 +0 -0
- {specsmith-0.10.0.dev238 → specsmith-0.10.0.dev239}/src/specsmith/tool_installer.py +0 -0
- {specsmith-0.10.0.dev238 → specsmith-0.10.0.dev239}/src/specsmith/toolrules.py +0 -0
- {specsmith-0.10.0.dev238 → specsmith-0.10.0.dev239}/src/specsmith/tools.py +0 -0
- {specsmith-0.10.0.dev238 → specsmith-0.10.0.dev239}/src/specsmith/trace.py +0 -0
- {specsmith-0.10.0.dev238 → specsmith-0.10.0.dev239}/src/specsmith/updater.py +0 -0
- {specsmith-0.10.0.dev238 → specsmith-0.10.0.dev239}/src/specsmith/upgrader.py +0 -0
- {specsmith-0.10.0.dev238 → specsmith-0.10.0.dev239}/src/specsmith/validator.py +0 -0
- {specsmith-0.10.0.dev238 → specsmith-0.10.0.dev239}/src/specsmith/vcs/__init__.py +0 -0
- {specsmith-0.10.0.dev238 → specsmith-0.10.0.dev239}/src/specsmith/vcs/base.py +0 -0
- {specsmith-0.10.0.dev238 → specsmith-0.10.0.dev239}/src/specsmith/vcs/bitbucket.py +0 -0
- {specsmith-0.10.0.dev238 → specsmith-0.10.0.dev239}/src/specsmith/vcs/github.py +0 -0
- {specsmith-0.10.0.dev238 → specsmith-0.10.0.dev239}/src/specsmith/vcs/gitlab.py +0 -0
- {specsmith-0.10.0.dev238 → specsmith-0.10.0.dev239}/src/specsmith/vcs_commands.py +0 -0
- {specsmith-0.10.0.dev238 → specsmith-0.10.0.dev239}/src/specsmith/wireframes.py +0 -0
- {specsmith-0.10.0.dev238 → specsmith-0.10.0.dev239}/src/specsmith/workspace.py +0 -0
- {specsmith-0.10.0.dev238 → specsmith-0.10.0.dev239}/src/specsmith.egg-info/SOURCES.txt +0 -0
- {specsmith-0.10.0.dev238 → specsmith-0.10.0.dev239}/src/specsmith.egg-info/dependency_links.txt +0 -0
- {specsmith-0.10.0.dev238 → specsmith-0.10.0.dev239}/src/specsmith.egg-info/entry_points.txt +0 -0
- {specsmith-0.10.0.dev238 → specsmith-0.10.0.dev239}/src/specsmith.egg-info/requires.txt +0 -0
- {specsmith-0.10.0.dev238 → specsmith-0.10.0.dev239}/src/specsmith.egg-info/top_level.txt +0 -0
- {specsmith-0.10.0.dev238 → specsmith-0.10.0.dev239}/tests/test_CMD_001.py +0 -0
- {specsmith-0.10.0.dev238 → specsmith-0.10.0.dev239}/tests/test_agent_profiles.py +0 -0
- {specsmith-0.10.0.dev238 → specsmith-0.10.0.dev239}/tests/test_agent_runner_ready.py +0 -0
- {specsmith-0.10.0.dev238 → specsmith-0.10.0.dev239}/tests/test_auditor.py +0 -0
- {specsmith-0.10.0.dev238 → specsmith-0.10.0.dev239}/tests/test_chat_diff_decision.py +0 -0
- {specsmith-0.10.0.dev238 → specsmith-0.10.0.dev239}/tests/test_chat_stdin_protocol.py +0 -0
- {specsmith-0.10.0.dev238 → specsmith-0.10.0.dev239}/tests/test_cli.py +0 -0
- {specsmith-0.10.0.dev238 → specsmith-0.10.0.dev239}/tests/test_cli_workflows_history_drive.py +0 -0
- {specsmith-0.10.0.dev238 → specsmith-0.10.0.dev239}/tests/test_compressor.py +0 -0
- {specsmith-0.10.0.dev238 → specsmith-0.10.0.dev239}/tests/test_e2e_nexus.py +0 -0
- {specsmith-0.10.0.dev238 → specsmith-0.10.0.dev239}/tests/test_endpoints_cli.py +0 -0
- {specsmith-0.10.0.dev238 → specsmith-0.10.0.dev239}/tests/test_endpoints_store.py +0 -0
- {specsmith-0.10.0.dev238 → specsmith-0.10.0.dev239}/tests/test_epistemic.py +0 -0
- {specsmith-0.10.0.dev238 → specsmith-0.10.0.dev239}/tests/test_fallback_chain.py +0 -0
- {specsmith-0.10.0.dev238 → specsmith-0.10.0.dev239}/tests/test_importer.py +0 -0
- {specsmith-0.10.0.dev238 → specsmith-0.10.0.dev239}/tests/test_integrations.py +0 -0
- {specsmith-0.10.0.dev238 → specsmith-0.10.0.dev239}/tests/test_mcp_client.py +0 -0
- {specsmith-0.10.0.dev238 → specsmith-0.10.0.dev239}/tests/test_nexus.py +0 -0
- {specsmith-0.10.0.dev238 → specsmith-0.10.0.dev239}/tests/test_phase1_4_new.py +0 -0
- {specsmith-0.10.0.dev238 → specsmith-0.10.0.dev239}/tests/test_phase34_completion.py +0 -0
- {specsmith-0.10.0.dev238 → specsmith-0.10.0.dev239}/tests/test_rate_limits.py +0 -0
- {specsmith-0.10.0.dev238 → specsmith-0.10.0.dev239}/tests/test_scaffolder.py +0 -0
- {specsmith-0.10.0.dev238 → specsmith-0.10.0.dev239}/tests/test_skill_marketplace.py +0 -0
- {specsmith-0.10.0.dev238 → specsmith-0.10.0.dev239}/tests/test_smoke.py +0 -0
- {specsmith-0.10.0.dev238 → specsmith-0.10.0.dev239}/tests/test_suggester.py +0 -0
- {specsmith-0.10.0.dev238 → specsmith-0.10.0.dev239}/tests/test_tools.py +0 -0
- {specsmith-0.10.0.dev238 → specsmith-0.10.0.dev239}/tests/test_validator.py +0 -0
- {specsmith-0.10.0.dev238 → specsmith-0.10.0.dev239}/tests/test_vcs.py +0 -0
- {specsmith-0.10.0.dev238 → specsmith-0.10.0.dev239}/tests/test_warp_parity.py +0 -0
- {specsmith-0.10.0.dev238 → specsmith-0.10.0.dev239}/tests/test_warp_parity_followup.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: specsmith
|
|
3
|
-
Version: 0.10.0.
|
|
3
|
+
Version: 0.10.0.dev239
|
|
4
4
|
Summary: Applied Epistemic Engineering toolkit — AEE agent sessions, execution profiles, FPGA/HDL governance, tool installer, 50+ CLI commands.
|
|
5
5
|
Author: BitConcepts
|
|
6
6
|
License-Expression: MIT
|
|
@@ -88,6 +88,25 @@ specsmith treats belief systems like code: codable, testable, and deployable. It
|
|
|
88
88
|
epistemically-governed projects, stress-tests requirements as BeliefArtifacts, runs
|
|
89
89
|
cryptographically-sealed trace vaults, and orchestrates AI agents under formal AEE governance.
|
|
90
90
|
|
|
91
|
+
**0.10.0 — Multi-Agent + BYOE.** A `/plan` goes to the architect, `/fix`
|
|
92
|
+
goes to the coder, `/review` goes to a reviewer that runs on a different
|
|
93
|
+
model family. Each *profile* is a `(provider, model, endpoint?, fallback_chain)`
|
|
94
|
+
bundle stored in `~/.specsmith/agents.json`; an *activity routing table*
|
|
95
|
+
maps slash commands and AEE phases to profiles; **BYOE endpoints**
|
|
96
|
+
(`~/.specsmith/endpoints.json`) let you point a profile at any
|
|
97
|
+
OpenAI-v1-compatible backend you self-host (vLLM, llama.cpp `server`,
|
|
98
|
+
LM Studio, TGI, ...). Cross-family **diversity guard**, capability
|
|
99
|
+
filtering, transient-failure fallback chains, and TraceVault decision
|
|
100
|
+
seals on every `/agent` pin are wired in by default. See
|
|
101
|
+
[`docs/site/agents.md`](docs/site/agents.md) for the five-minute walkthrough.
|
|
102
|
+
|
|
103
|
+
```bash
|
|
104
|
+
specsmith agents preset apply default # frontier coder + cross-family reviewer
|
|
105
|
+
specsmith endpoints add --id home-vllm \
|
|
106
|
+
--base-url http://10.0.0.4:8000/v1 --auth bearer-keyring
|
|
107
|
+
specsmith run --agent opus-reviewer # one-shot per-session pin
|
|
108
|
+
```
|
|
109
|
+
|
|
91
110
|
It also co-installs the standalone `epistemic` Python library for direct use in any project:
|
|
92
111
|
|
|
93
112
|
```python
|
|
@@ -16,6 +16,25 @@ specsmith treats belief systems like code: codable, testable, and deployable. It
|
|
|
16
16
|
epistemically-governed projects, stress-tests requirements as BeliefArtifacts, runs
|
|
17
17
|
cryptographically-sealed trace vaults, and orchestrates AI agents under formal AEE governance.
|
|
18
18
|
|
|
19
|
+
**0.10.0 — Multi-Agent + BYOE.** A `/plan` goes to the architect, `/fix`
|
|
20
|
+
goes to the coder, `/review` goes to a reviewer that runs on a different
|
|
21
|
+
model family. Each *profile* is a `(provider, model, endpoint?, fallback_chain)`
|
|
22
|
+
bundle stored in `~/.specsmith/agents.json`; an *activity routing table*
|
|
23
|
+
maps slash commands and AEE phases to profiles; **BYOE endpoints**
|
|
24
|
+
(`~/.specsmith/endpoints.json`) let you point a profile at any
|
|
25
|
+
OpenAI-v1-compatible backend you self-host (vLLM, llama.cpp `server`,
|
|
26
|
+
LM Studio, TGI, ...). Cross-family **diversity guard**, capability
|
|
27
|
+
filtering, transient-failure fallback chains, and TraceVault decision
|
|
28
|
+
seals on every `/agent` pin are wired in by default. See
|
|
29
|
+
[`docs/site/agents.md`](docs/site/agents.md) for the five-minute walkthrough.
|
|
30
|
+
|
|
31
|
+
```bash
|
|
32
|
+
specsmith agents preset apply default # frontier coder + cross-family reviewer
|
|
33
|
+
specsmith endpoints add --id home-vllm \
|
|
34
|
+
--base-url http://10.0.0.4:8000/v1 --auth bearer-keyring
|
|
35
|
+
specsmith run --agent opus-reviewer # one-shot per-session pin
|
|
36
|
+
```
|
|
37
|
+
|
|
19
38
|
It also co-installs the standalone `epistemic` Python library for direct use in any project:
|
|
20
39
|
|
|
21
40
|
```python
|
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "specsmith"
|
|
7
|
-
version = "0.10.0.
|
|
7
|
+
version = "0.10.0.dev239"
|
|
8
8
|
description = "Applied Epistemic Engineering toolkit — AEE agent sessions, execution profiles, FPGA/HDL governance, tool installer, 50+ CLI commands."
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
license = "MIT"
|
|
@@ -53,6 +53,14 @@ class ChatRunResult:
|
|
|
53
53
|
files_changed: list[str] = field(default_factory=list)
|
|
54
54
|
verdict: VerifierVerdict | None = None
|
|
55
55
|
raw_text: str = ""
|
|
56
|
+
# C1: per-turn token + cost accounting. Populated by the provider
|
|
57
|
+
# driver when it can read counters from the response (Ollama and
|
|
58
|
+
# Anthropic both expose them). Falls back to a deterministic char-
|
|
59
|
+
# based heuristic so the TokenMeter chip is never zero on Ollama or
|
|
60
|
+
# OpenAI-compat endpoints that don't surface usage in streaming mode.
|
|
61
|
+
tokens_in: int = 0
|
|
62
|
+
tokens_out: int = 0
|
|
63
|
+
cost_usd: float = 0.0
|
|
56
64
|
|
|
57
65
|
def to_dict(self) -> dict[str, Any]:
|
|
58
66
|
return {
|
|
@@ -61,6 +69,9 @@ class ChatRunResult:
|
|
|
61
69
|
"files_changed": list(self.files_changed),
|
|
62
70
|
"confidence": self.verdict.confidence if self.verdict else 0.0,
|
|
63
71
|
"equilibrium": self.verdict.equilibrium if self.verdict else False,
|
|
72
|
+
"tokens_in": int(self.tokens_in),
|
|
73
|
+
"tokens_out": int(self.tokens_out),
|
|
74
|
+
"cost_usd": float(self.cost_usd),
|
|
64
75
|
}
|
|
65
76
|
|
|
66
77
|
|
|
@@ -103,44 +114,99 @@ def run_chat(
|
|
|
103
114
|
endpoint = None
|
|
104
115
|
if endpoint is not None:
|
|
105
116
|
try:
|
|
106
|
-
full_text = _run_openai_compat(
|
|
117
|
+
full_text, usage = _run_openai_compat(
|
|
118
|
+
messages, emitter, msg_block, endpoint=endpoint
|
|
119
|
+
)
|
|
107
120
|
except Exception: # noqa: BLE001 - degrade to auto-detect
|
|
108
|
-
full_text = None
|
|
121
|
+
full_text, usage = None, _UsageDelta()
|
|
109
122
|
if full_text is not None:
|
|
110
|
-
return _finalize(
|
|
123
|
+
return _finalize(
|
|
124
|
+
full_text,
|
|
125
|
+
"openai_compat",
|
|
126
|
+
project_dir,
|
|
127
|
+
confidence_target,
|
|
128
|
+
messages=messages,
|
|
129
|
+
usage=usage,
|
|
130
|
+
)
|
|
111
131
|
|
|
112
132
|
# Order matters: Ollama first because it's local-first and free.
|
|
113
133
|
for provider in (_run_ollama, _run_anthropic, _run_openai, _run_gemini):
|
|
114
134
|
try:
|
|
115
|
-
full_text = provider(messages, emitter, msg_block)
|
|
135
|
+
full_text, usage = provider(messages, emitter, msg_block)
|
|
116
136
|
except Exception: # noqa: BLE001 - any failure → next provider
|
|
117
137
|
continue
|
|
118
138
|
if full_text is None:
|
|
119
139
|
continue
|
|
120
|
-
return _finalize(
|
|
140
|
+
return _finalize(
|
|
141
|
+
full_text,
|
|
142
|
+
provider.__name__,
|
|
143
|
+
project_dir,
|
|
144
|
+
confidence_target,
|
|
145
|
+
messages=messages,
|
|
146
|
+
usage=usage,
|
|
147
|
+
)
|
|
121
148
|
return None
|
|
122
149
|
|
|
123
150
|
|
|
151
|
+
@dataclass
|
|
152
|
+
class _UsageDelta:
|
|
153
|
+
"""Per-turn token + cost counters reported by a provider driver.
|
|
154
|
+
|
|
155
|
+
All fields default to ``0`` so callers can construct a zero-value
|
|
156
|
+
instance without caring whether the provider supports usage tracking.
|
|
157
|
+
"""
|
|
158
|
+
|
|
159
|
+
tokens_in: int = 0
|
|
160
|
+
tokens_out: int = 0
|
|
161
|
+
cost_usd: float = 0.0
|
|
162
|
+
|
|
163
|
+
|
|
124
164
|
def _finalize(
|
|
125
165
|
full_text: str,
|
|
126
166
|
provider_fn_name: str,
|
|
127
167
|
project_dir: Path,
|
|
128
168
|
confidence_target: float,
|
|
169
|
+
*,
|
|
170
|
+
messages: list[dict[str, str]] | None = None,
|
|
171
|
+
usage: _UsageDelta | None = None,
|
|
129
172
|
) -> ChatRunResult:
|
|
130
173
|
sections = _parse_output_contract(full_text)
|
|
131
174
|
files_changed = _split_files_list(sections.get("files_changed", ""))
|
|
132
175
|
report = report_from_chat_sections(sections, files_changed=files_changed)
|
|
133
176
|
verdict = score(report, confidence_target=confidence_target)
|
|
134
177
|
summary = (sections.get("plan") or full_text.strip()[:200]).strip() or verdict.summary
|
|
178
|
+
|
|
179
|
+
# C1: when the provider didn't report exact counts, estimate from text.
|
|
180
|
+
# The four-chars-per-token rule of thumb is OpenAI's published guidance
|
|
181
|
+
# and matches Ollama / Anthropic / Gemini within ~10% across the model
|
|
182
|
+
# families we ship today — close enough for the TokenMeter chip and
|
|
183
|
+
# the ``credits record`` ledger event.
|
|
184
|
+
if usage is None:
|
|
185
|
+
usage = _UsageDelta()
|
|
186
|
+
if usage.tokens_in == 0 and messages is not None:
|
|
187
|
+
usage.tokens_in = _estimate_tokens("\n".join(m.get("content", "") for m in messages))
|
|
188
|
+
if usage.tokens_out == 0:
|
|
189
|
+
usage.tokens_out = _estimate_tokens(full_text)
|
|
190
|
+
|
|
135
191
|
return ChatRunResult(
|
|
136
192
|
provider=provider_fn_name.removeprefix("_run_"),
|
|
137
193
|
summary=summary,
|
|
138
194
|
files_changed=files_changed,
|
|
139
195
|
verdict=verdict,
|
|
140
196
|
raw_text=full_text,
|
|
197
|
+
tokens_in=int(usage.tokens_in),
|
|
198
|
+
tokens_out=int(usage.tokens_out),
|
|
199
|
+
cost_usd=float(usage.cost_usd),
|
|
141
200
|
)
|
|
142
201
|
|
|
143
202
|
|
|
203
|
+
def _estimate_tokens(text: str) -> int:
|
|
204
|
+
"""Rough char→token heuristic (4 chars/token, floor at 1 if non-empty)."""
|
|
205
|
+
if not text:
|
|
206
|
+
return 0
|
|
207
|
+
return max(1, len(text) // 4)
|
|
208
|
+
|
|
209
|
+
|
|
144
210
|
# ---------------------------------------------------------------------------
|
|
145
211
|
# Provider drivers — each returns the full assembled text or None
|
|
146
212
|
# ---------------------------------------------------------------------------
|
|
@@ -150,13 +216,14 @@ def _run_ollama(
|
|
|
150
216
|
messages: list[dict[str, str]],
|
|
151
217
|
emitter: EventEmitter,
|
|
152
218
|
block_id: str,
|
|
153
|
-
) -> str | None:
|
|
219
|
+
) -> tuple[str | None, _UsageDelta]:
|
|
154
220
|
"""Stream from a local Ollama daemon using only stdlib."""
|
|
155
221
|
host = os.environ.get("OLLAMA_HOST", DEFAULT_OLLAMA_HOST).rstrip("/")
|
|
156
222
|
model = os.environ.get("SPECSMITH_OLLAMA_MODEL", DEFAULT_OLLAMA_MODEL)
|
|
223
|
+
usage = _UsageDelta()
|
|
157
224
|
|
|
158
225
|
if not _ollama_alive(host):
|
|
159
|
-
return None
|
|
226
|
+
return None, usage
|
|
160
227
|
|
|
161
228
|
payload = json.dumps({"model": model, "messages": messages, "stream": True}).encode("utf-8")
|
|
162
229
|
req = Request( # noqa: S310 - URL is a hardcoded localhost default
|
|
@@ -181,8 +248,13 @@ def _run_ollama(
|
|
|
181
248
|
emitter.token(block_id, chunk)
|
|
182
249
|
pieces.append(chunk)
|
|
183
250
|
if obj.get("done"):
|
|
251
|
+
# C1: Ollama exposes prompt_eval_count + eval_count on the
|
|
252
|
+
# final ``done`` message. Cost is zero for local models.
|
|
253
|
+
usage.tokens_in = int(obj.get("prompt_eval_count") or 0)
|
|
254
|
+
usage.tokens_out = int(obj.get("eval_count") or 0)
|
|
255
|
+
usage.cost_usd = 0.0
|
|
184
256
|
break
|
|
185
|
-
return "".join(pieces) if pieces else None
|
|
257
|
+
return ("".join(pieces) if pieces else None), usage
|
|
186
258
|
|
|
187
259
|
|
|
188
260
|
def _ollama_alive(host: str) -> bool:
|
|
@@ -197,14 +269,15 @@ def _run_anthropic(
|
|
|
197
269
|
messages: list[dict[str, str]],
|
|
198
270
|
emitter: EventEmitter,
|
|
199
271
|
block_id: str,
|
|
200
|
-
) -> str | None:
|
|
272
|
+
) -> tuple[str | None, _UsageDelta]:
|
|
201
273
|
"""Use the anthropic SDK if installed and a key is configured."""
|
|
274
|
+
usage = _UsageDelta()
|
|
202
275
|
if not os.environ.get("ANTHROPIC_API_KEY"):
|
|
203
|
-
return None
|
|
276
|
+
return None, usage
|
|
204
277
|
try:
|
|
205
278
|
import anthropic
|
|
206
279
|
except ImportError:
|
|
207
|
-
return None
|
|
280
|
+
return None, usage
|
|
208
281
|
|
|
209
282
|
system = "\n".join(m["content"] for m in messages if m["role"] == "system")
|
|
210
283
|
user_msgs = [m for m in messages if m["role"] != "system"]
|
|
@@ -221,35 +294,54 @@ def _run_anthropic(
|
|
|
221
294
|
if text:
|
|
222
295
|
emitter.token(block_id, text)
|
|
223
296
|
pieces.append(text)
|
|
224
|
-
|
|
297
|
+
# C1: pull final usage off the SDK's `final_message`. Cost is the
|
|
298
|
+
# caller's problem (rate-limit module knows the model price); we
|
|
299
|
+
# report tokens here and let the credits ledger compute USD.
|
|
300
|
+
try:
|
|
301
|
+
final = stream.get_final_message()
|
|
302
|
+
usage.tokens_in = int(getattr(final.usage, "input_tokens", 0) or 0)
|
|
303
|
+
usage.tokens_out = int(getattr(final.usage, "output_tokens", 0) or 0)
|
|
304
|
+
except Exception: # noqa: BLE001 - usage is best-effort
|
|
305
|
+
pass
|
|
306
|
+
return ("".join(pieces) if pieces else None), usage
|
|
225
307
|
|
|
226
308
|
|
|
227
309
|
def _run_openai(
|
|
228
310
|
messages: list[dict[str, str]],
|
|
229
311
|
emitter: EventEmitter,
|
|
230
312
|
block_id: str,
|
|
231
|
-
) -> str | None:
|
|
313
|
+
) -> tuple[str | None, _UsageDelta]:
|
|
232
314
|
"""Use the openai SDK if installed and a key is configured."""
|
|
315
|
+
usage = _UsageDelta()
|
|
233
316
|
if not os.environ.get("OPENAI_API_KEY"):
|
|
234
|
-
return None
|
|
317
|
+
return None, usage
|
|
235
318
|
try:
|
|
236
319
|
from openai import OpenAI
|
|
237
320
|
except ImportError:
|
|
238
|
-
return None
|
|
321
|
+
return None, usage
|
|
239
322
|
|
|
240
323
|
client = OpenAI()
|
|
324
|
+
# ``stream_options.include_usage`` makes the final SSE chunk carry a
|
|
325
|
+
# populated ``usage`` block (otherwise streaming responses emit it as
|
|
326
|
+
# ``None``). Older SDK versions silently ignore unknown kwargs.
|
|
241
327
|
stream = client.chat.completions.create(
|
|
242
328
|
model=os.environ.get("OPENAI_MODEL", "gpt-4o-mini"),
|
|
243
329
|
messages=messages,
|
|
244
330
|
stream=True,
|
|
331
|
+
stream_options={"include_usage": True},
|
|
245
332
|
)
|
|
246
333
|
pieces: list[str] = []
|
|
247
334
|
for chunk in stream:
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
335
|
+
if chunk.choices:
|
|
336
|
+
text = chunk.choices[0].delta.content or ""
|
|
337
|
+
if text:
|
|
338
|
+
emitter.token(block_id, text)
|
|
339
|
+
pieces.append(text)
|
|
340
|
+
usage_obj = getattr(chunk, "usage", None)
|
|
341
|
+
if usage_obj is not None:
|
|
342
|
+
usage.tokens_in = int(getattr(usage_obj, "prompt_tokens", 0) or 0)
|
|
343
|
+
usage.tokens_out = int(getattr(usage_obj, "completion_tokens", 0) or 0)
|
|
344
|
+
return ("".join(pieces) if pieces else None), usage
|
|
253
345
|
|
|
254
346
|
|
|
255
347
|
def _run_openai_compat(
|
|
@@ -258,7 +350,7 @@ def _run_openai_compat(
|
|
|
258
350
|
block_id: str,
|
|
259
351
|
*,
|
|
260
352
|
endpoint: Any,
|
|
261
|
-
) -> str | None:
|
|
353
|
+
) -> tuple[str | None, _UsageDelta]:
|
|
262
354
|
"""Stream from a user-registered OpenAI-v1-compatible endpoint (REQ-142).
|
|
263
355
|
|
|
264
356
|
Uses raw stdlib HTTP so the openai SDK is not a hard dependency for
|
|
@@ -266,13 +358,14 @@ def _run_openai_compat(
|
|
|
266
358
|
Server-Sent-Events ``data:`` lines, and forwards each ``content``
|
|
267
359
|
delta as a ``token`` event on ``block_id``.
|
|
268
360
|
"""
|
|
361
|
+
usage = _UsageDelta()
|
|
269
362
|
base_url = endpoint.base_url.rstrip("/")
|
|
270
363
|
url = f"{base_url}/chat/completions"
|
|
271
364
|
model = endpoint.default_model or os.environ.get("SPECSMITH_OPENAI_COMPAT_MODEL", "")
|
|
272
365
|
if not model:
|
|
273
366
|
# The endpoint did not pin a default model and the env override is
|
|
274
367
|
# absent. We cannot fabricate one; fall back to the auto-detect chain.
|
|
275
|
-
return None
|
|
368
|
+
return None, usage
|
|
276
369
|
|
|
277
370
|
headers: dict[str, str] = {
|
|
278
371
|
"Content-Type": "application/json",
|
|
@@ -281,11 +374,20 @@ def _run_openai_compat(
|
|
|
281
374
|
try:
|
|
282
375
|
token = endpoint.resolve_token()
|
|
283
376
|
except Exception: # noqa: BLE001 - fall back to auto-detect chain
|
|
284
|
-
return None
|
|
377
|
+
return None, usage
|
|
285
378
|
if token:
|
|
286
379
|
headers["Authorization"] = f"Bearer {token}"
|
|
287
380
|
|
|
288
|
-
body = json.dumps(
|
|
381
|
+
body = json.dumps(
|
|
382
|
+
{
|
|
383
|
+
"model": model,
|
|
384
|
+
"messages": messages,
|
|
385
|
+
"stream": True,
|
|
386
|
+
# Many vLLM/llama.cpp builds honour OpenAI's stream_options;
|
|
387
|
+
# the request is harmless if they don't.
|
|
388
|
+
"stream_options": {"include_usage": True},
|
|
389
|
+
}
|
|
390
|
+
).encode("utf-8")
|
|
289
391
|
req = Request(url, data=body, headers=headers, method="POST") # noqa: S310 - user-supplied
|
|
290
392
|
|
|
291
393
|
ctx = None
|
|
@@ -313,6 +415,10 @@ def _run_openai_compat(
|
|
|
313
415
|
except ValueError:
|
|
314
416
|
continue
|
|
315
417
|
choices = obj.get("choices") or []
|
|
418
|
+
usage_obj = obj.get("usage")
|
|
419
|
+
if usage_obj:
|
|
420
|
+
usage.tokens_in = int(usage_obj.get("prompt_tokens") or 0)
|
|
421
|
+
usage.tokens_out = int(usage_obj.get("completion_tokens") or 0)
|
|
316
422
|
if not choices:
|
|
317
423
|
continue
|
|
318
424
|
delta = (choices[0] or {}).get("delta") or {}
|
|
@@ -321,35 +427,50 @@ def _run_openai_compat(
|
|
|
321
427
|
emitter.token(block_id, chunk)
|
|
322
428
|
pieces.append(chunk)
|
|
323
429
|
except (URLError, TimeoutError, OSError):
|
|
324
|
-
return None
|
|
325
|
-
return "".join(pieces) if pieces else None
|
|
430
|
+
return None, usage
|
|
431
|
+
return ("".join(pieces) if pieces else None), usage
|
|
326
432
|
|
|
327
433
|
|
|
328
434
|
def _run_gemini(
|
|
329
435
|
messages: list[dict[str, str]],
|
|
330
436
|
emitter: EventEmitter,
|
|
331
437
|
block_id: str,
|
|
332
|
-
) -> str | None:
|
|
438
|
+
) -> tuple[str | None, _UsageDelta]:
|
|
333
439
|
"""Use google-genai SDK if installed and a key is configured."""
|
|
440
|
+
usage = _UsageDelta()
|
|
334
441
|
if not os.environ.get("GOOGLE_API_KEY"):
|
|
335
|
-
return None
|
|
442
|
+
return None, usage
|
|
336
443
|
try:
|
|
337
444
|
from google import genai
|
|
338
445
|
except ImportError:
|
|
339
|
-
return None
|
|
446
|
+
return None, usage
|
|
340
447
|
|
|
341
448
|
client = genai.Client()
|
|
342
449
|
prompt = "\n".join(f"{m['role']}: {m['content']}" for m in messages)
|
|
343
450
|
pieces: list[str] = []
|
|
451
|
+
last_chunk: Any = None
|
|
344
452
|
for chunk in client.models.generate_content_stream(
|
|
345
453
|
model=os.environ.get("GEMINI_MODEL", "gemini-2.5-flash"),
|
|
346
454
|
contents=prompt,
|
|
347
455
|
):
|
|
456
|
+
last_chunk = chunk
|
|
348
457
|
text = getattr(chunk, "text", "") or ""
|
|
349
458
|
if text:
|
|
350
459
|
emitter.token(block_id, text)
|
|
351
460
|
pieces.append(text)
|
|
352
|
-
|
|
461
|
+
# Gemini exposes ``usage_metadata`` on the final chunk. Field names
|
|
462
|
+
# vary across SDK versions; we accept the union.
|
|
463
|
+
meta = getattr(last_chunk, "usage_metadata", None) if last_chunk else None
|
|
464
|
+
if meta is not None:
|
|
465
|
+
usage.tokens_in = int(
|
|
466
|
+
getattr(meta, "prompt_token_count", 0) or getattr(meta, "input_token_count", 0) or 0
|
|
467
|
+
)
|
|
468
|
+
usage.tokens_out = int(
|
|
469
|
+
getattr(meta, "candidates_token_count", 0)
|
|
470
|
+
or getattr(meta, "output_token_count", 0)
|
|
471
|
+
or 0
|
|
472
|
+
)
|
|
473
|
+
return ("".join(pieces) if pieces else None), usage
|
|
353
474
|
|
|
354
475
|
|
|
355
476
|
# ---------------------------------------------------------------------------
|
|
@@ -56,6 +56,33 @@ VALID_ROLES = (
|
|
|
56
56
|
"generalist",
|
|
57
57
|
)
|
|
58
58
|
|
|
59
|
+
# Provider “family” groupings used by the diversity guard (G1). Profiles in
|
|
60
|
+
# the same family are likely to share training data, system prompt biases,
|
|
61
|
+
# and hallucination patterns — so pairing the coder with a reviewer in the
|
|
62
|
+
# same family defeats the cross-check the reviewer is meant to provide.
|
|
63
|
+
#
|
|
64
|
+
# Anything not listed here is treated as its own family.
|
|
65
|
+
PROVIDER_FAMILIES: dict[str, str] = {
|
|
66
|
+
"anthropic": "anthropic",
|
|
67
|
+
"openai": "openai",
|
|
68
|
+
"openai-compat": "openai",
|
|
69
|
+
"azure-openai": "openai",
|
|
70
|
+
"gemini": "google",
|
|
71
|
+
"google": "google",
|
|
72
|
+
"google-genai": "google",
|
|
73
|
+
"mistral": "mistral",
|
|
74
|
+
"ollama": "ollama",
|
|
75
|
+
"llamacpp": "ollama",
|
|
76
|
+
"vllm": "ollama",
|
|
77
|
+
"lmstudio": "ollama",
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
def provider_family(provider: str) -> str:
|
|
82
|
+
"""Return the family name for ``provider`` (or the provider verbatim)."""
|
|
83
|
+
key = (provider or "").strip().lower()
|
|
84
|
+
return PROVIDER_FAMILIES.get(key, key or "unknown")
|
|
85
|
+
|
|
59
86
|
|
|
60
87
|
# Default presets shipped with the CLI so a fresh install Just Works.
|
|
61
88
|
# The exact model strings can be customised per-deployment via
|
|
@@ -493,7 +520,64 @@ class ProfileStore:
|
|
|
493
520
|
def list_all(self) -> list[Profile]:
|
|
494
521
|
return list(self.profiles)
|
|
495
522
|
|
|
496
|
-
|
|
523
|
+
def filter_by_capability(self, capability: str) -> list[Profile]:
|
|
524
|
+
"""Return profiles whose ``capabilities`` list contains ``capability``.
|
|
525
|
+
|
|
526
|
+
Matching is case-insensitive and trims whitespace. An empty
|
|
527
|
+
``capability`` argument returns ``[]`` rather than “everything” so
|
|
528
|
+
callers can distinguish “no filter” (don’t call this method) from
|
|
529
|
+
“filter for an empty value” (which is never meaningful).
|
|
530
|
+
"""
|
|
531
|
+
needle = (capability or "").strip().lower()
|
|
532
|
+
if not needle:
|
|
533
|
+
return []
|
|
534
|
+
return [
|
|
535
|
+
p
|
|
536
|
+
for p in self.profiles
|
|
537
|
+
if any(needle == str(c).strip().lower() for c in p.capabilities)
|
|
538
|
+
]
|
|
539
|
+
|
|
540
|
+
def diversity_warnings(self, *, candidate: Profile | None = None) -> list[str]:
|
|
541
|
+
"""Return a list of plain-English diversity warnings for the store.
|
|
542
|
+
|
|
543
|
+
The reviewer profile exists to cross-check the coder; if both call
|
|
544
|
+
the same provider family the cross-check is degenerate. Same logic
|
|
545
|
+
applies to architect vs. reviewer (both should be skeptical of the
|
|
546
|
+
coder). When ``candidate`` is supplied the candidate is added to
|
|
547
|
+
the population *and* takes precedence over any same-id profile
|
|
548
|
+
already in the store, so a `specsmith agents add` invocation can
|
|
549
|
+
preview the warnings *before* writing the store.
|
|
550
|
+
"""
|
|
551
|
+
population: dict[str, Profile] = {p.id: p for p in self.profiles}
|
|
552
|
+
if candidate is not None:
|
|
553
|
+
population[candidate.id] = candidate
|
|
554
|
+
by_role: dict[str, list[Profile]] = {}
|
|
555
|
+
for p in population.values():
|
|
556
|
+
by_role.setdefault(p.role, []).append(p)
|
|
557
|
+
|
|
558
|
+
warnings: list[str] = []
|
|
559
|
+
for left_role, right_role in (
|
|
560
|
+
("coder", "reviewer"),
|
|
561
|
+
("architect", "reviewer"),
|
|
562
|
+
):
|
|
563
|
+
left = by_role.get(left_role) or []
|
|
564
|
+
right = by_role.get(right_role) or []
|
|
565
|
+
if not left or not right:
|
|
566
|
+
continue
|
|
567
|
+
for lp in left:
|
|
568
|
+
lf = provider_family(lp.provider)
|
|
569
|
+
for rp in right:
|
|
570
|
+
if provider_family(rp.provider) == lf:
|
|
571
|
+
warnings.append(
|
|
572
|
+
f"{rp.id} ({rp.role}, {rp.provider}/{rp.model}) "
|
|
573
|
+
f"shares the {lf!r} family with "
|
|
574
|
+
f"{lp.id} ({lp.role}, {lp.provider}/{lp.model}); "
|
|
575
|
+
"diversity is recommended so the reviewer can catch "
|
|
576
|
+
"the coder's blind spots."
|
|
577
|
+
)
|
|
578
|
+
return warnings
|
|
579
|
+
|
|
580
|
+
# ── Routing ─────────────────────────────────────────────────
|
|
497
581
|
|
|
498
582
|
def set_route(self, activity: str, profile_id: str) -> None:
|
|
499
583
|
activity = activity.strip()
|
|
@@ -558,6 +642,7 @@ def apply_preset(name: str, *, path: Path | None = None) -> ProfileStore:
|
|
|
558
642
|
|
|
559
643
|
__all__ = [
|
|
560
644
|
"DEFAULT_PRESETS",
|
|
645
|
+
"PROVIDER_FAMILIES",
|
|
561
646
|
"Profile",
|
|
562
647
|
"ProfileError",
|
|
563
648
|
"ProfileStore",
|
|
@@ -566,4 +651,5 @@ __all__ = [
|
|
|
566
651
|
"apply_preset",
|
|
567
652
|
"default_store_path",
|
|
568
653
|
"project_store_path",
|
|
654
|
+
"provider_family",
|
|
569
655
|
]
|
|
@@ -277,6 +277,13 @@ class AgentRunner:
|
|
|
277
277
|
self.profile_id = new_profile or None
|
|
278
278
|
self._state.profile_id = new_profile
|
|
279
279
|
self._emit_event(type="system", message=f"profile = {new_profile or '(default)'}")
|
|
280
|
+
# G4: pin the profile choice into the project trace vault so the
|
|
281
|
+
# decision “I explicitly asked for profile X here” is
|
|
282
|
+
# cryptographically chained into the audit trail. Best-effort:
|
|
283
|
+
# missing TraceVault dependency / read-only filesystem must not
|
|
284
|
+
# break the chat loop.
|
|
285
|
+
if new_profile:
|
|
286
|
+
self._seal_profile_pin(new_profile)
|
|
280
287
|
return None
|
|
281
288
|
if text.startswith("/endpoint "):
|
|
282
289
|
new_endpoint = text.split(maxsplit=1)[1].strip()
|
|
@@ -321,14 +328,21 @@ class AgentRunner:
|
|
|
321
328
|
)
|
|
322
329
|
return None
|
|
323
330
|
|
|
324
|
-
# Aggregate metrics into the session state.
|
|
325
|
-
#
|
|
326
|
-
#
|
|
331
|
+
# Aggregate metrics into the session state (C1).
|
|
332
|
+
# ``run_chat`` now reports tokens_in / tokens_out / cost_usd off the
|
|
333
|
+
# provider response (Ollama prompt_eval_count + eval_count, OpenAI
|
|
334
|
+
# streaming usage, Anthropic final_message.usage, Gemini
|
|
335
|
+
# usage_metadata) with a 4-chars-per-token fallback when the SDK
|
|
336
|
+
# omits them. The TokenMeter chip therefore shows real numbers
|
|
337
|
+
# instead of staying pinned at zero.
|
|
338
|
+
tokens_in = int(getattr(result, "tokens_in", 0) or 0) if result is not None else 0
|
|
339
|
+
tokens_out = int(getattr(result, "tokens_out", 0) or 0) if result is not None else 0
|
|
340
|
+
cost_usd = float(getattr(result, "cost_usd", 0.0) or 0.0) if result is not None else 0.0
|
|
327
341
|
self._state.credit(
|
|
328
342
|
profile_id=(profile.id if profile is not None else self.profile_id or ""),
|
|
329
|
-
tokens_in=
|
|
330
|
-
tokens_out=
|
|
331
|
-
cost_usd=
|
|
343
|
+
tokens_in=tokens_in,
|
|
344
|
+
tokens_out=tokens_out,
|
|
345
|
+
cost_usd=cost_usd,
|
|
332
346
|
tool_calls=0,
|
|
333
347
|
)
|
|
334
348
|
self._state.elapsed_minutes = round((time.time() - self._started_at) / 60.0, 2)
|
|
@@ -397,3 +411,24 @@ class AgentRunner:
|
|
|
397
411
|
return _v("specsmith")
|
|
398
412
|
except Exception: # noqa: BLE001
|
|
399
413
|
return "0.0.0"
|
|
414
|
+
|
|
415
|
+
def _seal_profile_pin(self, profile_id: str) -> None:
|
|
416
|
+
"""Append a TraceVault decision seal recording the ``/agent`` pin (G4).
|
|
417
|
+
|
|
418
|
+
Wrapped in best-effort try/except so an unwriteable
|
|
419
|
+
``.specsmith/trace.jsonl`` (read-only fs, missing project root, etc.)
|
|
420
|
+
never breaks the chat loop. The seal type is ``decision`` because
|
|
421
|
+
a profile pin is an explicit governance choice the user made.
|
|
422
|
+
"""
|
|
423
|
+
try:
|
|
424
|
+
from specsmith.trace import SealType, TraceVault
|
|
425
|
+
|
|
426
|
+
vault = TraceVault(Path(self.project_dir))
|
|
427
|
+
vault.seal(
|
|
428
|
+
seal_type=SealType.DECISION,
|
|
429
|
+
description=f"agent profile pinned via /agent: {profile_id}",
|
|
430
|
+
author="runner",
|
|
431
|
+
artifact_ids=[f"profile:{profile_id}"],
|
|
432
|
+
)
|
|
433
|
+
except Exception: # noqa: BLE001 — trace sealing is best-effort
|
|
434
|
+
return
|