specsmith 0.5.0.dev225__tar.gz → 0.5.0.dev227__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.5.0.dev225/src/specsmith.egg-info → specsmith-0.5.0.dev227}/PKG-INFO +1 -1
- {specsmith-0.5.0.dev225 → specsmith-0.5.0.dev227}/pyproject.toml +1 -1
- specsmith-0.5.0.dev227/src/specsmith/agent/chat_runner.py +337 -0
- {specsmith-0.5.0.dev225 → specsmith-0.5.0.dev227}/src/specsmith/cli.py +300 -33
- specsmith-0.5.0.dev227/src/specsmith/skills.py +199 -0
- {specsmith-0.5.0.dev225 → specsmith-0.5.0.dev227/src/specsmith.egg-info}/PKG-INFO +1 -1
- {specsmith-0.5.0.dev225 → specsmith-0.5.0.dev227}/src/specsmith.egg-info/SOURCES.txt +5 -0
- specsmith-0.5.0.dev227/tests/test_chat_diff_decision.py +158 -0
- specsmith-0.5.0.dev227/tests/test_chat_stdin_protocol.py +89 -0
- specsmith-0.5.0.dev227/tests/test_skill_marketplace.py +185 -0
- {specsmith-0.5.0.dev225 → specsmith-0.5.0.dev227}/LICENSE +0 -0
- {specsmith-0.5.0.dev225 → specsmith-0.5.0.dev227}/README.md +0 -0
- {specsmith-0.5.0.dev225 → specsmith-0.5.0.dev227}/setup.cfg +0 -0
- {specsmith-0.5.0.dev225 → specsmith-0.5.0.dev227}/src/epistemic/__init__.py +0 -0
- {specsmith-0.5.0.dev225 → specsmith-0.5.0.dev227}/src/epistemic/belief.py +0 -0
- {specsmith-0.5.0.dev225 → specsmith-0.5.0.dev227}/src/epistemic/certainty.py +0 -0
- {specsmith-0.5.0.dev225 → specsmith-0.5.0.dev227}/src/epistemic/failure_graph.py +0 -0
- {specsmith-0.5.0.dev225 → specsmith-0.5.0.dev227}/src/epistemic/py.typed +0 -0
- {specsmith-0.5.0.dev225 → specsmith-0.5.0.dev227}/src/epistemic/recovery.py +0 -0
- {specsmith-0.5.0.dev225 → specsmith-0.5.0.dev227}/src/epistemic/session.py +0 -0
- {specsmith-0.5.0.dev225 → specsmith-0.5.0.dev227}/src/epistemic/stress_tester.py +0 -0
- {specsmith-0.5.0.dev225 → specsmith-0.5.0.dev227}/src/epistemic/trace.py +0 -0
- {specsmith-0.5.0.dev225 → specsmith-0.5.0.dev227}/src/specsmith/__init__.py +0 -0
- {specsmith-0.5.0.dev225 → specsmith-0.5.0.dev227}/src/specsmith/__main__.py +0 -0
- {specsmith-0.5.0.dev225 → specsmith-0.5.0.dev227}/src/specsmith/agent/__init__.py +0 -0
- {specsmith-0.5.0.dev225 → specsmith-0.5.0.dev227}/src/specsmith/agent/broker.py +0 -0
- {specsmith-0.5.0.dev225 → specsmith-0.5.0.dev227}/src/specsmith/agent/cleanup.py +0 -0
- {specsmith-0.5.0.dev225 → specsmith-0.5.0.dev227}/src/specsmith/agent/events.py +0 -0
- {specsmith-0.5.0.dev225 → specsmith-0.5.0.dev227}/src/specsmith/agent/indexer.py +0 -0
- {specsmith-0.5.0.dev225 → specsmith-0.5.0.dev227}/src/specsmith/agent/mcp.py +0 -0
- {specsmith-0.5.0.dev225 → specsmith-0.5.0.dev227}/src/specsmith/agent/memory.py +0 -0
- {specsmith-0.5.0.dev225 → specsmith-0.5.0.dev227}/src/specsmith/agent/orchestrator.py +0 -0
- {specsmith-0.5.0.dev225 → specsmith-0.5.0.dev227}/src/specsmith/agent/repl.py +0 -0
- {specsmith-0.5.0.dev225 → specsmith-0.5.0.dev227}/src/specsmith/agent/router.py +0 -0
- {specsmith-0.5.0.dev225 → specsmith-0.5.0.dev227}/src/specsmith/agent/rules.py +0 -0
- {specsmith-0.5.0.dev225 → specsmith-0.5.0.dev227}/src/specsmith/agent/safety.py +0 -0
- {specsmith-0.5.0.dev225 → specsmith-0.5.0.dev227}/src/specsmith/agent/tools.py +0 -0
- {specsmith-0.5.0.dev225 → specsmith-0.5.0.dev227}/src/specsmith/agent/verifier.py +0 -0
- {specsmith-0.5.0.dev225 → specsmith-0.5.0.dev227}/src/specsmith/architect.py +0 -0
- {specsmith-0.5.0.dev225 → specsmith-0.5.0.dev227}/src/specsmith/auditor.py +0 -0
- {specsmith-0.5.0.dev225 → specsmith-0.5.0.dev227}/src/specsmith/auth.py +0 -0
- {specsmith-0.5.0.dev225 → specsmith-0.5.0.dev227}/src/specsmith/commands/__init__.py +0 -0
- {specsmith-0.5.0.dev225 → specsmith-0.5.0.dev227}/src/specsmith/compressor.py +0 -0
- {specsmith-0.5.0.dev225 → specsmith-0.5.0.dev227}/src/specsmith/config.py +0 -0
- {specsmith-0.5.0.dev225 → specsmith-0.5.0.dev227}/src/specsmith/console_utils.py +0 -0
- {specsmith-0.5.0.dev225 → specsmith-0.5.0.dev227}/src/specsmith/credit_analyzer.py +0 -0
- {specsmith-0.5.0.dev225 → specsmith-0.5.0.dev227}/src/specsmith/credits.py +0 -0
- {specsmith-0.5.0.dev225 → specsmith-0.5.0.dev227}/src/specsmith/differ.py +0 -0
- {specsmith-0.5.0.dev225 → specsmith-0.5.0.dev227}/src/specsmith/doctor.py +0 -0
- {specsmith-0.5.0.dev225 → specsmith-0.5.0.dev227}/src/specsmith/epistemic/__init__.py +0 -0
- {specsmith-0.5.0.dev225 → specsmith-0.5.0.dev227}/src/specsmith/epistemic/belief.py +0 -0
- {specsmith-0.5.0.dev225 → specsmith-0.5.0.dev227}/src/specsmith/epistemic/certainty.py +0 -0
- {specsmith-0.5.0.dev225 → specsmith-0.5.0.dev227}/src/specsmith/epistemic/failure_graph.py +0 -0
- {specsmith-0.5.0.dev225 → specsmith-0.5.0.dev227}/src/specsmith/epistemic/recovery.py +0 -0
- {specsmith-0.5.0.dev225 → specsmith-0.5.0.dev227}/src/specsmith/epistemic/stress_tester.py +0 -0
- {specsmith-0.5.0.dev225 → specsmith-0.5.0.dev227}/src/specsmith/executor.py +0 -0
- {specsmith-0.5.0.dev225 → specsmith-0.5.0.dev227}/src/specsmith/exporter.py +0 -0
- {specsmith-0.5.0.dev225 → specsmith-0.5.0.dev227}/src/specsmith/gui/__init__.py +0 -0
- {specsmith-0.5.0.dev225 → specsmith-0.5.0.dev227}/src/specsmith/gui/app.py +0 -0
- {specsmith-0.5.0.dev225 → specsmith-0.5.0.dev227}/src/specsmith/gui/main_window.py +0 -0
- {specsmith-0.5.0.dev225 → specsmith-0.5.0.dev227}/src/specsmith/gui/session_tab.py +0 -0
- {specsmith-0.5.0.dev225 → specsmith-0.5.0.dev227}/src/specsmith/gui/theme.py +0 -0
- {specsmith-0.5.0.dev225 → specsmith-0.5.0.dev227}/src/specsmith/gui/widgets/__init__.py +0 -0
- {specsmith-0.5.0.dev225 → specsmith-0.5.0.dev227}/src/specsmith/gui/widgets/chat_view.py +0 -0
- {specsmith-0.5.0.dev225 → specsmith-0.5.0.dev227}/src/specsmith/gui/widgets/input_bar.py +0 -0
- {specsmith-0.5.0.dev225 → specsmith-0.5.0.dev227}/src/specsmith/gui/widgets/provider_bar.py +0 -0
- {specsmith-0.5.0.dev225 → specsmith-0.5.0.dev227}/src/specsmith/gui/widgets/token_meter.py +0 -0
- {specsmith-0.5.0.dev225 → specsmith-0.5.0.dev227}/src/specsmith/gui/widgets/tool_panel.py +0 -0
- {specsmith-0.5.0.dev225 → specsmith-0.5.0.dev227}/src/specsmith/gui/widgets/update_checker.py +0 -0
- {specsmith-0.5.0.dev225 → specsmith-0.5.0.dev227}/src/specsmith/gui/worker.py +0 -0
- {specsmith-0.5.0.dev225 → specsmith-0.5.0.dev227}/src/specsmith/importer.py +0 -0
- {specsmith-0.5.0.dev225 → specsmith-0.5.0.dev227}/src/specsmith/integrations/__init__.py +0 -0
- {specsmith-0.5.0.dev225 → specsmith-0.5.0.dev227}/src/specsmith/integrations/agent_skill.py +0 -0
- {specsmith-0.5.0.dev225 → specsmith-0.5.0.dev227}/src/specsmith/integrations/aider.py +0 -0
- {specsmith-0.5.0.dev225 → specsmith-0.5.0.dev227}/src/specsmith/integrations/base.py +0 -0
- {specsmith-0.5.0.dev225 → specsmith-0.5.0.dev227}/src/specsmith/integrations/claude_code.py +0 -0
- {specsmith-0.5.0.dev225 → specsmith-0.5.0.dev227}/src/specsmith/integrations/copilot.py +0 -0
- {specsmith-0.5.0.dev225 → specsmith-0.5.0.dev227}/src/specsmith/integrations/cursor.py +0 -0
- {specsmith-0.5.0.dev225 → specsmith-0.5.0.dev227}/src/specsmith/integrations/gemini.py +0 -0
- {specsmith-0.5.0.dev225 → specsmith-0.5.0.dev227}/src/specsmith/integrations/windsurf.py +0 -0
- {specsmith-0.5.0.dev225 → specsmith-0.5.0.dev227}/src/specsmith/languages.py +0 -0
- {specsmith-0.5.0.dev225 → specsmith-0.5.0.dev227}/src/specsmith/ledger.py +0 -0
- {specsmith-0.5.0.dev225 → specsmith-0.5.0.dev227}/src/specsmith/patent.py +0 -0
- {specsmith-0.5.0.dev225 → specsmith-0.5.0.dev227}/src/specsmith/phase.py +0 -0
- {specsmith-0.5.0.dev225 → specsmith-0.5.0.dev227}/src/specsmith/plugins.py +0 -0
- {specsmith-0.5.0.dev225 → specsmith-0.5.0.dev227}/src/specsmith/profiles.py +0 -0
- {specsmith-0.5.0.dev225 → specsmith-0.5.0.dev227}/src/specsmith/rate_limits.py +0 -0
- {specsmith-0.5.0.dev225 → specsmith-0.5.0.dev227}/src/specsmith/releaser.py +0 -0
- {specsmith-0.5.0.dev225 → specsmith-0.5.0.dev227}/src/specsmith/requirements.py +0 -0
- {specsmith-0.5.0.dev225 → specsmith-0.5.0.dev227}/src/specsmith/requirements_parser.py +0 -0
- {specsmith-0.5.0.dev225 → specsmith-0.5.0.dev227}/src/specsmith/retrieval.py +0 -0
- {specsmith-0.5.0.dev225 → specsmith-0.5.0.dev227}/src/specsmith/scaffolder.py +0 -0
- {specsmith-0.5.0.dev225 → specsmith-0.5.0.dev227}/src/specsmith/serve.py +0 -0
- {specsmith-0.5.0.dev225 → specsmith-0.5.0.dev227}/src/specsmith/session.py +0 -0
- {specsmith-0.5.0.dev225 → specsmith-0.5.0.dev227}/src/specsmith/templates/agents.md.j2 +0 -0
- {specsmith-0.5.0.dev225 → specsmith-0.5.0.dev227}/src/specsmith/templates/community/bug_report.md.j2 +0 -0
- {specsmith-0.5.0.dev225 → specsmith-0.5.0.dev227}/src/specsmith/templates/community/code_of_conduct.md.j2 +0 -0
- {specsmith-0.5.0.dev225 → specsmith-0.5.0.dev227}/src/specsmith/templates/community/contributing.md.j2 +0 -0
- {specsmith-0.5.0.dev225 → specsmith-0.5.0.dev227}/src/specsmith/templates/community/feature_request.md.j2 +0 -0
- {specsmith-0.5.0.dev225 → specsmith-0.5.0.dev227}/src/specsmith/templates/community/license-Apache-2.0.j2 +0 -0
- {specsmith-0.5.0.dev225 → specsmith-0.5.0.dev227}/src/specsmith/templates/community/license-MIT.j2 +0 -0
- {specsmith-0.5.0.dev225 → specsmith-0.5.0.dev227}/src/specsmith/templates/community/pull_request_template.md.j2 +0 -0
- {specsmith-0.5.0.dev225 → specsmith-0.5.0.dev227}/src/specsmith/templates/community/security.md.j2 +0 -0
- {specsmith-0.5.0.dev225 → specsmith-0.5.0.dev227}/src/specsmith/templates/docs/architecture.md.j2 +0 -0
- {specsmith-0.5.0.dev225 → specsmith-0.5.0.dev227}/src/specsmith/templates/docs/mkdocs.yml.j2 +0 -0
- {specsmith-0.5.0.dev225 → specsmith-0.5.0.dev227}/src/specsmith/templates/docs/readthedocs.yaml.j2 +0 -0
- {specsmith-0.5.0.dev225 → specsmith-0.5.0.dev227}/src/specsmith/templates/docs/requirements.md.j2 +0 -0
- {specsmith-0.5.0.dev225 → specsmith-0.5.0.dev227}/src/specsmith/templates/docs/test-spec.md.j2 +0 -0
- {specsmith-0.5.0.dev225 → specsmith-0.5.0.dev227}/src/specsmith/templates/editorconfig.j2 +0 -0
- {specsmith-0.5.0.dev225 → specsmith-0.5.0.dev227}/src/specsmith/templates/gitattributes.j2 +0 -0
- {specsmith-0.5.0.dev225 → specsmith-0.5.0.dev227}/src/specsmith/templates/gitignore.j2 +0 -0
- {specsmith-0.5.0.dev225 → specsmith-0.5.0.dev227}/src/specsmith/templates/go/go.mod.j2 +0 -0
- {specsmith-0.5.0.dev225 → specsmith-0.5.0.dev227}/src/specsmith/templates/go/main.go.j2 +0 -0
- {specsmith-0.5.0.dev225 → specsmith-0.5.0.dev227}/src/specsmith/templates/governance/belief-registry.md.j2 +0 -0
- {specsmith-0.5.0.dev225 → specsmith-0.5.0.dev227}/src/specsmith/templates/governance/context-budget.md.j2 +0 -0
- {specsmith-0.5.0.dev225 → specsmith-0.5.0.dev227}/src/specsmith/templates/governance/drift-metrics.md.j2 +0 -0
- {specsmith-0.5.0.dev225 → specsmith-0.5.0.dev227}/src/specsmith/templates/governance/epistemic-axioms.md.j2 +0 -0
- {specsmith-0.5.0.dev225 → specsmith-0.5.0.dev227}/src/specsmith/templates/governance/failure-modes.md.j2 +0 -0
- {specsmith-0.5.0.dev225 → specsmith-0.5.0.dev227}/src/specsmith/templates/governance/lifecycle.md.j2 +0 -0
- {specsmith-0.5.0.dev225 → specsmith-0.5.0.dev227}/src/specsmith/templates/governance/roles.md.j2 +0 -0
- {specsmith-0.5.0.dev225 → specsmith-0.5.0.dev227}/src/specsmith/templates/governance/rules.md.j2 +0 -0
- {specsmith-0.5.0.dev225 → specsmith-0.5.0.dev227}/src/specsmith/templates/governance/session-protocol.md.j2 +0 -0
- {specsmith-0.5.0.dev225 → specsmith-0.5.0.dev227}/src/specsmith/templates/governance/uncertainty-map.md.j2 +0 -0
- {specsmith-0.5.0.dev225 → specsmith-0.5.0.dev227}/src/specsmith/templates/governance/verification.md.j2 +0 -0
- {specsmith-0.5.0.dev225 → specsmith-0.5.0.dev227}/src/specsmith/templates/js/package.json.j2 +0 -0
- {specsmith-0.5.0.dev225 → specsmith-0.5.0.dev227}/src/specsmith/templates/ledger.md.j2 +0 -0
- {specsmith-0.5.0.dev225 → specsmith-0.5.0.dev227}/src/specsmith/templates/python/cli.py.j2 +0 -0
- {specsmith-0.5.0.dev225 → specsmith-0.5.0.dev227}/src/specsmith/templates/python/init.py.j2 +0 -0
- {specsmith-0.5.0.dev225 → specsmith-0.5.0.dev227}/src/specsmith/templates/python/pyproject.toml.j2 +0 -0
- {specsmith-0.5.0.dev225 → specsmith-0.5.0.dev227}/src/specsmith/templates/readme.md.j2 +0 -0
- {specsmith-0.5.0.dev225 → specsmith-0.5.0.dev227}/src/specsmith/templates/rust/Cargo.toml.j2 +0 -0
- {specsmith-0.5.0.dev225 → specsmith-0.5.0.dev227}/src/specsmith/templates/rust/main.rs.j2 +0 -0
- {specsmith-0.5.0.dev225 → specsmith-0.5.0.dev227}/src/specsmith/templates/scripts/exec.cmd.j2 +0 -0
- {specsmith-0.5.0.dev225 → specsmith-0.5.0.dev227}/src/specsmith/templates/scripts/exec.sh.j2 +0 -0
- {specsmith-0.5.0.dev225 → specsmith-0.5.0.dev227}/src/specsmith/templates/scripts/run.cmd.j2 +0 -0
- {specsmith-0.5.0.dev225 → specsmith-0.5.0.dev227}/src/specsmith/templates/scripts/run.sh.j2 +0 -0
- {specsmith-0.5.0.dev225 → specsmith-0.5.0.dev227}/src/specsmith/templates/scripts/setup.cmd.j2 +0 -0
- {specsmith-0.5.0.dev225 → specsmith-0.5.0.dev227}/src/specsmith/templates/scripts/setup.sh.j2 +0 -0
- {specsmith-0.5.0.dev225 → specsmith-0.5.0.dev227}/src/specsmith/templates/workflows/release.yml.j2 +0 -0
- {specsmith-0.5.0.dev225 → specsmith-0.5.0.dev227}/src/specsmith/tool_installer.py +0 -0
- {specsmith-0.5.0.dev225 → specsmith-0.5.0.dev227}/src/specsmith/toolrules.py +0 -0
- {specsmith-0.5.0.dev225 → specsmith-0.5.0.dev227}/src/specsmith/tools.py +0 -0
- {specsmith-0.5.0.dev225 → specsmith-0.5.0.dev227}/src/specsmith/trace.py +0 -0
- {specsmith-0.5.0.dev225 → specsmith-0.5.0.dev227}/src/specsmith/updater.py +0 -0
- {specsmith-0.5.0.dev225 → specsmith-0.5.0.dev227}/src/specsmith/upgrader.py +0 -0
- {specsmith-0.5.0.dev225 → specsmith-0.5.0.dev227}/src/specsmith/validator.py +0 -0
- {specsmith-0.5.0.dev225 → specsmith-0.5.0.dev227}/src/specsmith/vcs/__init__.py +0 -0
- {specsmith-0.5.0.dev225 → specsmith-0.5.0.dev227}/src/specsmith/vcs/base.py +0 -0
- {specsmith-0.5.0.dev225 → specsmith-0.5.0.dev227}/src/specsmith/vcs/bitbucket.py +0 -0
- {specsmith-0.5.0.dev225 → specsmith-0.5.0.dev227}/src/specsmith/vcs/github.py +0 -0
- {specsmith-0.5.0.dev225 → specsmith-0.5.0.dev227}/src/specsmith/vcs/gitlab.py +0 -0
- {specsmith-0.5.0.dev225 → specsmith-0.5.0.dev227}/src/specsmith/vcs_commands.py +0 -0
- {specsmith-0.5.0.dev225 → specsmith-0.5.0.dev227}/src/specsmith/wireframes.py +0 -0
- {specsmith-0.5.0.dev225 → specsmith-0.5.0.dev227}/src/specsmith/workspace.py +0 -0
- {specsmith-0.5.0.dev225 → specsmith-0.5.0.dev227}/src/specsmith.egg-info/dependency_links.txt +0 -0
- {specsmith-0.5.0.dev225 → specsmith-0.5.0.dev227}/src/specsmith.egg-info/entry_points.txt +0 -0
- {specsmith-0.5.0.dev225 → specsmith-0.5.0.dev227}/src/specsmith.egg-info/requires.txt +0 -0
- {specsmith-0.5.0.dev225 → specsmith-0.5.0.dev227}/src/specsmith.egg-info/top_level.txt +0 -0
- {specsmith-0.5.0.dev225 → specsmith-0.5.0.dev227}/tests/test_CMD_001.py +0 -0
- {specsmith-0.5.0.dev225 → specsmith-0.5.0.dev227}/tests/test_auditor.py +0 -0
- {specsmith-0.5.0.dev225 → specsmith-0.5.0.dev227}/tests/test_cli.py +0 -0
- {specsmith-0.5.0.dev225 → specsmith-0.5.0.dev227}/tests/test_cli_workflows_history_drive.py +0 -0
- {specsmith-0.5.0.dev225 → specsmith-0.5.0.dev227}/tests/test_compressor.py +0 -0
- {specsmith-0.5.0.dev225 → specsmith-0.5.0.dev227}/tests/test_e2e_nexus.py +0 -0
- {specsmith-0.5.0.dev225 → specsmith-0.5.0.dev227}/tests/test_epistemic.py +0 -0
- {specsmith-0.5.0.dev225 → specsmith-0.5.0.dev227}/tests/test_importer.py +0 -0
- {specsmith-0.5.0.dev225 → specsmith-0.5.0.dev227}/tests/test_integrations.py +0 -0
- {specsmith-0.5.0.dev225 → specsmith-0.5.0.dev227}/tests/test_nexus.py +0 -0
- {specsmith-0.5.0.dev225 → specsmith-0.5.0.dev227}/tests/test_phase1_4_new.py +0 -0
- {specsmith-0.5.0.dev225 → specsmith-0.5.0.dev227}/tests/test_rate_limits.py +0 -0
- {specsmith-0.5.0.dev225 → specsmith-0.5.0.dev227}/tests/test_scaffolder.py +0 -0
- {specsmith-0.5.0.dev225 → specsmith-0.5.0.dev227}/tests/test_smoke.py +0 -0
- {specsmith-0.5.0.dev225 → specsmith-0.5.0.dev227}/tests/test_tools.py +0 -0
- {specsmith-0.5.0.dev225 → specsmith-0.5.0.dev227}/tests/test_validator.py +0 -0
- {specsmith-0.5.0.dev225 → specsmith-0.5.0.dev227}/tests/test_vcs.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: specsmith
|
|
3
|
-
Version: 0.5.0.
|
|
3
|
+
Version: 0.5.0.dev227
|
|
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
|
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "specsmith"
|
|
7
|
-
version = "0.5.0.
|
|
7
|
+
version = "0.5.0.dev227"
|
|
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"
|
|
@@ -0,0 +1,337 @@
|
|
|
1
|
+
# SPDX-License-Identifier: MIT
|
|
2
|
+
# Copyright (c) 2026 BitConcepts, LLC. All rights reserved.
|
|
3
|
+
"""Real LLM-backed runner for `specsmith chat` (REQ-108, REQ-112-118).
|
|
4
|
+
|
|
5
|
+
This module replaces the deterministic stub that previously lived inside
|
|
6
|
+
`chat_cmd`. It selects the first available provider (Ollama → Anthropic →
|
|
7
|
+
OpenAI → Gemini) and streams the model's response as `token` events
|
|
8
|
+
through the supplied :class:`EventEmitter`. Output is then parsed for
|
|
9
|
+
``Files changed:`` and ``Test results:`` sections so the verifier can
|
|
10
|
+
emit a real verdict.
|
|
11
|
+
|
|
12
|
+
The runner is deliberately defensive: any provider error (missing SDK,
|
|
13
|
+
unreachable endpoint, network failure) returns ``None`` so the caller
|
|
14
|
+
can fall back to the deterministic stub. This keeps the test suite
|
|
15
|
+
green on machines that have no LLM configured at all.
|
|
16
|
+
"""
|
|
17
|
+
|
|
18
|
+
from __future__ import annotations
|
|
19
|
+
|
|
20
|
+
import json
|
|
21
|
+
import os
|
|
22
|
+
from dataclasses import dataclass, field
|
|
23
|
+
from pathlib import Path
|
|
24
|
+
from typing import Any
|
|
25
|
+
from urllib.error import URLError
|
|
26
|
+
from urllib.request import Request, urlopen
|
|
27
|
+
|
|
28
|
+
from specsmith.agent.events import EventEmitter
|
|
29
|
+
from specsmith.agent.verifier import (
|
|
30
|
+
VerifierVerdict,
|
|
31
|
+
report_from_chat_sections,
|
|
32
|
+
score,
|
|
33
|
+
)
|
|
34
|
+
|
|
35
|
+
DEFAULT_OLLAMA_HOST = "http://127.0.0.1:11434"
|
|
36
|
+
DEFAULT_OLLAMA_MODEL = os.environ.get("SPECSMITH_OLLAMA_MODEL", "qwen2.5:7b")
|
|
37
|
+
SYSTEM_PROMPT = (
|
|
38
|
+
"You are Nexus, the local-first agentic developer assistant inside "
|
|
39
|
+
"Specsmith. Always end your response with the canonical contract:\n"
|
|
40
|
+
"Plan:\n"
|
|
41
|
+
"Files changed:\n"
|
|
42
|
+
"Test results:\n"
|
|
43
|
+
"Next action:\n"
|
|
44
|
+
)
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
@dataclass
|
|
48
|
+
class ChatRunResult:
|
|
49
|
+
"""Return value of :func:`run_chat`."""
|
|
50
|
+
|
|
51
|
+
provider: str
|
|
52
|
+
summary: str
|
|
53
|
+
files_changed: list[str] = field(default_factory=list)
|
|
54
|
+
verdict: VerifierVerdict | None = None
|
|
55
|
+
raw_text: str = ""
|
|
56
|
+
|
|
57
|
+
def to_dict(self) -> dict[str, Any]:
|
|
58
|
+
return {
|
|
59
|
+
"provider": self.provider,
|
|
60
|
+
"summary": self.summary,
|
|
61
|
+
"files_changed": list(self.files_changed),
|
|
62
|
+
"confidence": self.verdict.confidence if self.verdict else 0.0,
|
|
63
|
+
"equilibrium": self.verdict.equilibrium if self.verdict else False,
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
# ---------------------------------------------------------------------------
|
|
68
|
+
# Public entry point
|
|
69
|
+
# ---------------------------------------------------------------------------
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
def run_chat(
|
|
73
|
+
utterance: str,
|
|
74
|
+
*,
|
|
75
|
+
project_dir: Path,
|
|
76
|
+
profile: str,
|
|
77
|
+
session_id: str,
|
|
78
|
+
emitter: EventEmitter,
|
|
79
|
+
msg_block: str,
|
|
80
|
+
history: list[dict[str, Any]] | None = None,
|
|
81
|
+
confidence_target: float = 0.7,
|
|
82
|
+
rules_prefix: str = "",
|
|
83
|
+
) -> ChatRunResult | None:
|
|
84
|
+
"""Drive a real LLM turn. Return ``None`` if no provider is reachable."""
|
|
85
|
+
history = history or []
|
|
86
|
+
messages = _build_messages(utterance, history, rules_prefix)
|
|
87
|
+
|
|
88
|
+
# Order matters: Ollama first because it's local-first and free.
|
|
89
|
+
for provider in (_run_ollama, _run_anthropic, _run_openai, _run_gemini):
|
|
90
|
+
try:
|
|
91
|
+
full_text = provider(messages, emitter, msg_block)
|
|
92
|
+
except Exception: # noqa: BLE001 - any failure → next provider
|
|
93
|
+
continue
|
|
94
|
+
if full_text is None:
|
|
95
|
+
continue
|
|
96
|
+
return _finalize(full_text, provider.__name__, project_dir, confidence_target)
|
|
97
|
+
return None
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
def _finalize(
|
|
101
|
+
full_text: str,
|
|
102
|
+
provider_fn_name: str,
|
|
103
|
+
project_dir: Path,
|
|
104
|
+
confidence_target: float,
|
|
105
|
+
) -> ChatRunResult:
|
|
106
|
+
sections = _parse_output_contract(full_text)
|
|
107
|
+
files_changed = _split_files_list(sections.get("files_changed", ""))
|
|
108
|
+
report = report_from_chat_sections(sections, files_changed=files_changed)
|
|
109
|
+
verdict = score(report, confidence_target=confidence_target)
|
|
110
|
+
summary = (sections.get("plan") or full_text.strip()[:200]).strip() or verdict.summary
|
|
111
|
+
return ChatRunResult(
|
|
112
|
+
provider=provider_fn_name.removeprefix("_run_"),
|
|
113
|
+
summary=summary,
|
|
114
|
+
files_changed=files_changed,
|
|
115
|
+
verdict=verdict,
|
|
116
|
+
raw_text=full_text,
|
|
117
|
+
)
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
# ---------------------------------------------------------------------------
|
|
121
|
+
# Provider drivers — each returns the full assembled text or None
|
|
122
|
+
# ---------------------------------------------------------------------------
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
def _run_ollama(
|
|
126
|
+
messages: list[dict[str, str]],
|
|
127
|
+
emitter: EventEmitter,
|
|
128
|
+
block_id: str,
|
|
129
|
+
) -> str | None:
|
|
130
|
+
"""Stream from a local Ollama daemon using only stdlib."""
|
|
131
|
+
host = os.environ.get("OLLAMA_HOST", DEFAULT_OLLAMA_HOST).rstrip("/")
|
|
132
|
+
model = os.environ.get("SPECSMITH_OLLAMA_MODEL", DEFAULT_OLLAMA_MODEL)
|
|
133
|
+
|
|
134
|
+
if not _ollama_alive(host):
|
|
135
|
+
return None
|
|
136
|
+
|
|
137
|
+
payload = json.dumps({"model": model, "messages": messages, "stream": True}).encode("utf-8")
|
|
138
|
+
req = Request( # noqa: S310 - URL is a hardcoded localhost default
|
|
139
|
+
f"{host}/api/chat",
|
|
140
|
+
data=payload,
|
|
141
|
+
headers={"Content-Type": "application/json"},
|
|
142
|
+
method="POST",
|
|
143
|
+
)
|
|
144
|
+
|
|
145
|
+
pieces: list[str] = []
|
|
146
|
+
with urlopen(req, timeout=120) as resp: # noqa: S310
|
|
147
|
+
for raw_line in resp:
|
|
148
|
+
line = raw_line.decode("utf-8", errors="replace").strip()
|
|
149
|
+
if not line:
|
|
150
|
+
continue
|
|
151
|
+
try:
|
|
152
|
+
obj = json.loads(line)
|
|
153
|
+
except ValueError:
|
|
154
|
+
continue
|
|
155
|
+
chunk = ((obj.get("message") or {}).get("content")) or ""
|
|
156
|
+
if chunk:
|
|
157
|
+
emitter.token(block_id, chunk)
|
|
158
|
+
pieces.append(chunk)
|
|
159
|
+
if obj.get("done"):
|
|
160
|
+
break
|
|
161
|
+
return "".join(pieces) if pieces else None
|
|
162
|
+
|
|
163
|
+
|
|
164
|
+
def _ollama_alive(host: str) -> bool:
|
|
165
|
+
try:
|
|
166
|
+
with urlopen(f"{host}/api/tags", timeout=2): # noqa: S310
|
|
167
|
+
return True
|
|
168
|
+
except (URLError, TimeoutError, OSError):
|
|
169
|
+
return False
|
|
170
|
+
|
|
171
|
+
|
|
172
|
+
def _run_anthropic(
|
|
173
|
+
messages: list[dict[str, str]],
|
|
174
|
+
emitter: EventEmitter,
|
|
175
|
+
block_id: str,
|
|
176
|
+
) -> str | None:
|
|
177
|
+
"""Use the anthropic SDK if installed and a key is configured."""
|
|
178
|
+
if not os.environ.get("ANTHROPIC_API_KEY"):
|
|
179
|
+
return None
|
|
180
|
+
try:
|
|
181
|
+
import anthropic
|
|
182
|
+
except ImportError:
|
|
183
|
+
return None
|
|
184
|
+
|
|
185
|
+
system = "\n".join(m["content"] for m in messages if m["role"] == "system")
|
|
186
|
+
user_msgs = [m for m in messages if m["role"] != "system"]
|
|
187
|
+
client = anthropic.Anthropic()
|
|
188
|
+
pieces: list[str] = []
|
|
189
|
+
with client.messages.stream(
|
|
190
|
+
model=os.environ.get("ANTHROPIC_MODEL", "claude-haiku-4-5"),
|
|
191
|
+
max_tokens=2048,
|
|
192
|
+
system=system,
|
|
193
|
+
messages=[{"role": m["role"], "content": m["content"]} for m in user_msgs],
|
|
194
|
+
) as stream:
|
|
195
|
+
for event in stream:
|
|
196
|
+
text = getattr(getattr(event, "delta", None), "text", None)
|
|
197
|
+
if text:
|
|
198
|
+
emitter.token(block_id, text)
|
|
199
|
+
pieces.append(text)
|
|
200
|
+
return "".join(pieces) if pieces else None
|
|
201
|
+
|
|
202
|
+
|
|
203
|
+
def _run_openai(
|
|
204
|
+
messages: list[dict[str, str]],
|
|
205
|
+
emitter: EventEmitter,
|
|
206
|
+
block_id: str,
|
|
207
|
+
) -> str | None:
|
|
208
|
+
"""Use the openai SDK if installed and a key is configured."""
|
|
209
|
+
if not os.environ.get("OPENAI_API_KEY"):
|
|
210
|
+
return None
|
|
211
|
+
try:
|
|
212
|
+
from openai import OpenAI
|
|
213
|
+
except ImportError:
|
|
214
|
+
return None
|
|
215
|
+
|
|
216
|
+
client = OpenAI()
|
|
217
|
+
stream = client.chat.completions.create(
|
|
218
|
+
model=os.environ.get("OPENAI_MODEL", "gpt-4o-mini"),
|
|
219
|
+
messages=messages,
|
|
220
|
+
stream=True,
|
|
221
|
+
)
|
|
222
|
+
pieces: list[str] = []
|
|
223
|
+
for chunk in stream:
|
|
224
|
+
text = (chunk.choices[0].delta.content or "") if chunk.choices else ""
|
|
225
|
+
if text:
|
|
226
|
+
emitter.token(block_id, text)
|
|
227
|
+
pieces.append(text)
|
|
228
|
+
return "".join(pieces) if pieces else None
|
|
229
|
+
|
|
230
|
+
|
|
231
|
+
def _run_gemini(
|
|
232
|
+
messages: list[dict[str, str]],
|
|
233
|
+
emitter: EventEmitter,
|
|
234
|
+
block_id: str,
|
|
235
|
+
) -> str | None:
|
|
236
|
+
"""Use google-genai SDK if installed and a key is configured."""
|
|
237
|
+
if not os.environ.get("GOOGLE_API_KEY"):
|
|
238
|
+
return None
|
|
239
|
+
try:
|
|
240
|
+
from google import genai
|
|
241
|
+
except ImportError:
|
|
242
|
+
return None
|
|
243
|
+
|
|
244
|
+
client = genai.Client()
|
|
245
|
+
prompt = "\n".join(f"{m['role']}: {m['content']}" for m in messages)
|
|
246
|
+
pieces: list[str] = []
|
|
247
|
+
for chunk in client.models.generate_content_stream(
|
|
248
|
+
model=os.environ.get("GEMINI_MODEL", "gemini-2.5-flash"),
|
|
249
|
+
contents=prompt,
|
|
250
|
+
):
|
|
251
|
+
text = getattr(chunk, "text", "") or ""
|
|
252
|
+
if text:
|
|
253
|
+
emitter.token(block_id, text)
|
|
254
|
+
pieces.append(text)
|
|
255
|
+
return "".join(pieces) if pieces else None
|
|
256
|
+
|
|
257
|
+
|
|
258
|
+
# ---------------------------------------------------------------------------
|
|
259
|
+
# Helpers
|
|
260
|
+
# ---------------------------------------------------------------------------
|
|
261
|
+
|
|
262
|
+
|
|
263
|
+
def _build_messages(
|
|
264
|
+
utterance: str,
|
|
265
|
+
history: list[dict[str, Any]],
|
|
266
|
+
rules_prefix: str,
|
|
267
|
+
) -> list[dict[str, str]]:
|
|
268
|
+
system = SYSTEM_PROMPT
|
|
269
|
+
if rules_prefix:
|
|
270
|
+
system = f"{system}\n\nProject rules:\n{rules_prefix}"
|
|
271
|
+
msgs: list[dict[str, str]] = [{"role": "system", "content": system}]
|
|
272
|
+
for turn in history[-10:]:
|
|
273
|
+
text = str(turn.get("utterance") or turn.get("text") or "").strip()
|
|
274
|
+
if text:
|
|
275
|
+
msgs.append({"role": "user", "content": text})
|
|
276
|
+
msgs.append({"role": "user", "content": utterance})
|
|
277
|
+
return msgs
|
|
278
|
+
|
|
279
|
+
|
|
280
|
+
def _parse_output_contract(text: str) -> dict[str, str]:
|
|
281
|
+
"""Extract canonical Nexus output sections from free-form text.
|
|
282
|
+
|
|
283
|
+
The contract is `Plan:`, `Commands to run:`, `Files changed:`,
|
|
284
|
+
`Diff:`, `Test results:`, `Next action:`. Sections that don't
|
|
285
|
+
appear are returned as empty strings.
|
|
286
|
+
"""
|
|
287
|
+
keys = [
|
|
288
|
+
("plan", "Plan:"),
|
|
289
|
+
("commands_to_run", "Commands to run:"),
|
|
290
|
+
("files_changed", "Files changed:"),
|
|
291
|
+
("diff", "Diff:"),
|
|
292
|
+
("test_results", "Test results:"),
|
|
293
|
+
("next_action", "Next action:"),
|
|
294
|
+
]
|
|
295
|
+
out: dict[str, str] = {key: "" for key, _ in keys}
|
|
296
|
+
lower = text.lower()
|
|
297
|
+
bounds: list[tuple[str, int]] = []
|
|
298
|
+
for key, header in keys:
|
|
299
|
+
idx = lower.find(header.lower())
|
|
300
|
+
if idx >= 0:
|
|
301
|
+
bounds.append((key, idx + len(header)))
|
|
302
|
+
bounds.sort(key=lambda b: b[1])
|
|
303
|
+
for i, (key, start) in enumerate(bounds):
|
|
304
|
+
if i + 1 < len(bounds):
|
|
305
|
+
next_key, next_pos = bounds[i + 1]
|
|
306
|
+
end = next_pos - len(_section_header(next_key))
|
|
307
|
+
else:
|
|
308
|
+
end = len(text)
|
|
309
|
+
out[key] = text[start:end].strip()
|
|
310
|
+
return out
|
|
311
|
+
|
|
312
|
+
|
|
313
|
+
def _section_header(key: str) -> str:
|
|
314
|
+
return {
|
|
315
|
+
"plan": "Plan:",
|
|
316
|
+
"commands_to_run": "Commands to run:",
|
|
317
|
+
"files_changed": "Files changed:",
|
|
318
|
+
"diff": "Diff:",
|
|
319
|
+
"test_results": "Test results:",
|
|
320
|
+
"next_action": "Next action:",
|
|
321
|
+
}[key]
|
|
322
|
+
|
|
323
|
+
|
|
324
|
+
def _split_files_list(text: str) -> list[str]:
|
|
325
|
+
items: list[str] = []
|
|
326
|
+
for raw in text.splitlines():
|
|
327
|
+
line = raw.strip()
|
|
328
|
+
if not line:
|
|
329
|
+
continue
|
|
330
|
+
if line.startswith(("-", "*", "+")):
|
|
331
|
+
line = line[1:].strip()
|
|
332
|
+
if line:
|
|
333
|
+
items.append(line)
|
|
334
|
+
return items
|
|
335
|
+
|
|
336
|
+
|
|
337
|
+
__all__ = ["ChatRunResult", "run_chat"]
|