specsmith 0.5.0.dev226__tar.gz → 0.5.0.dev228__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.dev226/src/specsmith.egg-info → specsmith-0.5.0.dev228}/PKG-INFO +1 -1
- {specsmith-0.5.0.dev226 → specsmith-0.5.0.dev228}/pyproject.toml +1 -1
- specsmith-0.5.0.dev228/src/specsmith/agent/chat_runner.py +337 -0
- {specsmith-0.5.0.dev226 → specsmith-0.5.0.dev228}/src/specsmith/cli.py +42 -10
- {specsmith-0.5.0.dev226 → specsmith-0.5.0.dev228/src/specsmith.egg-info}/PKG-INFO +1 -1
- {specsmith-0.5.0.dev226 → specsmith-0.5.0.dev228}/src/specsmith.egg-info/SOURCES.txt +2 -0
- specsmith-0.5.0.dev228/tests/test_chat_diff_decision.py +158 -0
- {specsmith-0.5.0.dev226 → specsmith-0.5.0.dev228}/LICENSE +0 -0
- {specsmith-0.5.0.dev226 → specsmith-0.5.0.dev228}/README.md +0 -0
- {specsmith-0.5.0.dev226 → specsmith-0.5.0.dev228}/setup.cfg +0 -0
- {specsmith-0.5.0.dev226 → specsmith-0.5.0.dev228}/src/epistemic/__init__.py +0 -0
- {specsmith-0.5.0.dev226 → specsmith-0.5.0.dev228}/src/epistemic/belief.py +0 -0
- {specsmith-0.5.0.dev226 → specsmith-0.5.0.dev228}/src/epistemic/certainty.py +0 -0
- {specsmith-0.5.0.dev226 → specsmith-0.5.0.dev228}/src/epistemic/failure_graph.py +0 -0
- {specsmith-0.5.0.dev226 → specsmith-0.5.0.dev228}/src/epistemic/py.typed +0 -0
- {specsmith-0.5.0.dev226 → specsmith-0.5.0.dev228}/src/epistemic/recovery.py +0 -0
- {specsmith-0.5.0.dev226 → specsmith-0.5.0.dev228}/src/epistemic/session.py +0 -0
- {specsmith-0.5.0.dev226 → specsmith-0.5.0.dev228}/src/epistemic/stress_tester.py +0 -0
- {specsmith-0.5.0.dev226 → specsmith-0.5.0.dev228}/src/epistemic/trace.py +0 -0
- {specsmith-0.5.0.dev226 → specsmith-0.5.0.dev228}/src/specsmith/__init__.py +0 -0
- {specsmith-0.5.0.dev226 → specsmith-0.5.0.dev228}/src/specsmith/__main__.py +0 -0
- {specsmith-0.5.0.dev226 → specsmith-0.5.0.dev228}/src/specsmith/agent/__init__.py +0 -0
- {specsmith-0.5.0.dev226 → specsmith-0.5.0.dev228}/src/specsmith/agent/broker.py +0 -0
- {specsmith-0.5.0.dev226 → specsmith-0.5.0.dev228}/src/specsmith/agent/cleanup.py +0 -0
- {specsmith-0.5.0.dev226 → specsmith-0.5.0.dev228}/src/specsmith/agent/events.py +0 -0
- {specsmith-0.5.0.dev226 → specsmith-0.5.0.dev228}/src/specsmith/agent/indexer.py +0 -0
- {specsmith-0.5.0.dev226 → specsmith-0.5.0.dev228}/src/specsmith/agent/mcp.py +0 -0
- {specsmith-0.5.0.dev226 → specsmith-0.5.0.dev228}/src/specsmith/agent/memory.py +0 -0
- {specsmith-0.5.0.dev226 → specsmith-0.5.0.dev228}/src/specsmith/agent/orchestrator.py +0 -0
- {specsmith-0.5.0.dev226 → specsmith-0.5.0.dev228}/src/specsmith/agent/repl.py +0 -0
- {specsmith-0.5.0.dev226 → specsmith-0.5.0.dev228}/src/specsmith/agent/router.py +0 -0
- {specsmith-0.5.0.dev226 → specsmith-0.5.0.dev228}/src/specsmith/agent/rules.py +0 -0
- {specsmith-0.5.0.dev226 → specsmith-0.5.0.dev228}/src/specsmith/agent/safety.py +0 -0
- {specsmith-0.5.0.dev226 → specsmith-0.5.0.dev228}/src/specsmith/agent/tools.py +0 -0
- {specsmith-0.5.0.dev226 → specsmith-0.5.0.dev228}/src/specsmith/agent/verifier.py +0 -0
- {specsmith-0.5.0.dev226 → specsmith-0.5.0.dev228}/src/specsmith/architect.py +0 -0
- {specsmith-0.5.0.dev226 → specsmith-0.5.0.dev228}/src/specsmith/auditor.py +0 -0
- {specsmith-0.5.0.dev226 → specsmith-0.5.0.dev228}/src/specsmith/auth.py +0 -0
- {specsmith-0.5.0.dev226 → specsmith-0.5.0.dev228}/src/specsmith/commands/__init__.py +0 -0
- {specsmith-0.5.0.dev226 → specsmith-0.5.0.dev228}/src/specsmith/compressor.py +0 -0
- {specsmith-0.5.0.dev226 → specsmith-0.5.0.dev228}/src/specsmith/config.py +0 -0
- {specsmith-0.5.0.dev226 → specsmith-0.5.0.dev228}/src/specsmith/console_utils.py +0 -0
- {specsmith-0.5.0.dev226 → specsmith-0.5.0.dev228}/src/specsmith/credit_analyzer.py +0 -0
- {specsmith-0.5.0.dev226 → specsmith-0.5.0.dev228}/src/specsmith/credits.py +0 -0
- {specsmith-0.5.0.dev226 → specsmith-0.5.0.dev228}/src/specsmith/differ.py +0 -0
- {specsmith-0.5.0.dev226 → specsmith-0.5.0.dev228}/src/specsmith/doctor.py +0 -0
- {specsmith-0.5.0.dev226 → specsmith-0.5.0.dev228}/src/specsmith/epistemic/__init__.py +0 -0
- {specsmith-0.5.0.dev226 → specsmith-0.5.0.dev228}/src/specsmith/epistemic/belief.py +0 -0
- {specsmith-0.5.0.dev226 → specsmith-0.5.0.dev228}/src/specsmith/epistemic/certainty.py +0 -0
- {specsmith-0.5.0.dev226 → specsmith-0.5.0.dev228}/src/specsmith/epistemic/failure_graph.py +0 -0
- {specsmith-0.5.0.dev226 → specsmith-0.5.0.dev228}/src/specsmith/epistemic/recovery.py +0 -0
- {specsmith-0.5.0.dev226 → specsmith-0.5.0.dev228}/src/specsmith/epistemic/stress_tester.py +0 -0
- {specsmith-0.5.0.dev226 → specsmith-0.5.0.dev228}/src/specsmith/executor.py +0 -0
- {specsmith-0.5.0.dev226 → specsmith-0.5.0.dev228}/src/specsmith/exporter.py +0 -0
- {specsmith-0.5.0.dev226 → specsmith-0.5.0.dev228}/src/specsmith/gui/__init__.py +0 -0
- {specsmith-0.5.0.dev226 → specsmith-0.5.0.dev228}/src/specsmith/gui/app.py +0 -0
- {specsmith-0.5.0.dev226 → specsmith-0.5.0.dev228}/src/specsmith/gui/main_window.py +0 -0
- {specsmith-0.5.0.dev226 → specsmith-0.5.0.dev228}/src/specsmith/gui/session_tab.py +0 -0
- {specsmith-0.5.0.dev226 → specsmith-0.5.0.dev228}/src/specsmith/gui/theme.py +0 -0
- {specsmith-0.5.0.dev226 → specsmith-0.5.0.dev228}/src/specsmith/gui/widgets/__init__.py +0 -0
- {specsmith-0.5.0.dev226 → specsmith-0.5.0.dev228}/src/specsmith/gui/widgets/chat_view.py +0 -0
- {specsmith-0.5.0.dev226 → specsmith-0.5.0.dev228}/src/specsmith/gui/widgets/input_bar.py +0 -0
- {specsmith-0.5.0.dev226 → specsmith-0.5.0.dev228}/src/specsmith/gui/widgets/provider_bar.py +0 -0
- {specsmith-0.5.0.dev226 → specsmith-0.5.0.dev228}/src/specsmith/gui/widgets/token_meter.py +0 -0
- {specsmith-0.5.0.dev226 → specsmith-0.5.0.dev228}/src/specsmith/gui/widgets/tool_panel.py +0 -0
- {specsmith-0.5.0.dev226 → specsmith-0.5.0.dev228}/src/specsmith/gui/widgets/update_checker.py +0 -0
- {specsmith-0.5.0.dev226 → specsmith-0.5.0.dev228}/src/specsmith/gui/worker.py +0 -0
- {specsmith-0.5.0.dev226 → specsmith-0.5.0.dev228}/src/specsmith/importer.py +0 -0
- {specsmith-0.5.0.dev226 → specsmith-0.5.0.dev228}/src/specsmith/integrations/__init__.py +0 -0
- {specsmith-0.5.0.dev226 → specsmith-0.5.0.dev228}/src/specsmith/integrations/agent_skill.py +0 -0
- {specsmith-0.5.0.dev226 → specsmith-0.5.0.dev228}/src/specsmith/integrations/aider.py +0 -0
- {specsmith-0.5.0.dev226 → specsmith-0.5.0.dev228}/src/specsmith/integrations/base.py +0 -0
- {specsmith-0.5.0.dev226 → specsmith-0.5.0.dev228}/src/specsmith/integrations/claude_code.py +0 -0
- {specsmith-0.5.0.dev226 → specsmith-0.5.0.dev228}/src/specsmith/integrations/copilot.py +0 -0
- {specsmith-0.5.0.dev226 → specsmith-0.5.0.dev228}/src/specsmith/integrations/cursor.py +0 -0
- {specsmith-0.5.0.dev226 → specsmith-0.5.0.dev228}/src/specsmith/integrations/gemini.py +0 -0
- {specsmith-0.5.0.dev226 → specsmith-0.5.0.dev228}/src/specsmith/integrations/windsurf.py +0 -0
- {specsmith-0.5.0.dev226 → specsmith-0.5.0.dev228}/src/specsmith/languages.py +0 -0
- {specsmith-0.5.0.dev226 → specsmith-0.5.0.dev228}/src/specsmith/ledger.py +0 -0
- {specsmith-0.5.0.dev226 → specsmith-0.5.0.dev228}/src/specsmith/patent.py +0 -0
- {specsmith-0.5.0.dev226 → specsmith-0.5.0.dev228}/src/specsmith/phase.py +0 -0
- {specsmith-0.5.0.dev226 → specsmith-0.5.0.dev228}/src/specsmith/plugins.py +0 -0
- {specsmith-0.5.0.dev226 → specsmith-0.5.0.dev228}/src/specsmith/profiles.py +0 -0
- {specsmith-0.5.0.dev226 → specsmith-0.5.0.dev228}/src/specsmith/rate_limits.py +0 -0
- {specsmith-0.5.0.dev226 → specsmith-0.5.0.dev228}/src/specsmith/releaser.py +0 -0
- {specsmith-0.5.0.dev226 → specsmith-0.5.0.dev228}/src/specsmith/requirements.py +0 -0
- {specsmith-0.5.0.dev226 → specsmith-0.5.0.dev228}/src/specsmith/requirements_parser.py +0 -0
- {specsmith-0.5.0.dev226 → specsmith-0.5.0.dev228}/src/specsmith/retrieval.py +0 -0
- {specsmith-0.5.0.dev226 → specsmith-0.5.0.dev228}/src/specsmith/scaffolder.py +0 -0
- {specsmith-0.5.0.dev226 → specsmith-0.5.0.dev228}/src/specsmith/serve.py +0 -0
- {specsmith-0.5.0.dev226 → specsmith-0.5.0.dev228}/src/specsmith/session.py +0 -0
- {specsmith-0.5.0.dev226 → specsmith-0.5.0.dev228}/src/specsmith/skills.py +0 -0
- {specsmith-0.5.0.dev226 → specsmith-0.5.0.dev228}/src/specsmith/templates/agents.md.j2 +0 -0
- {specsmith-0.5.0.dev226 → specsmith-0.5.0.dev228}/src/specsmith/templates/community/bug_report.md.j2 +0 -0
- {specsmith-0.5.0.dev226 → specsmith-0.5.0.dev228}/src/specsmith/templates/community/code_of_conduct.md.j2 +0 -0
- {specsmith-0.5.0.dev226 → specsmith-0.5.0.dev228}/src/specsmith/templates/community/contributing.md.j2 +0 -0
- {specsmith-0.5.0.dev226 → specsmith-0.5.0.dev228}/src/specsmith/templates/community/feature_request.md.j2 +0 -0
- {specsmith-0.5.0.dev226 → specsmith-0.5.0.dev228}/src/specsmith/templates/community/license-Apache-2.0.j2 +0 -0
- {specsmith-0.5.0.dev226 → specsmith-0.5.0.dev228}/src/specsmith/templates/community/license-MIT.j2 +0 -0
- {specsmith-0.5.0.dev226 → specsmith-0.5.0.dev228}/src/specsmith/templates/community/pull_request_template.md.j2 +0 -0
- {specsmith-0.5.0.dev226 → specsmith-0.5.0.dev228}/src/specsmith/templates/community/security.md.j2 +0 -0
- {specsmith-0.5.0.dev226 → specsmith-0.5.0.dev228}/src/specsmith/templates/docs/architecture.md.j2 +0 -0
- {specsmith-0.5.0.dev226 → specsmith-0.5.0.dev228}/src/specsmith/templates/docs/mkdocs.yml.j2 +0 -0
- {specsmith-0.5.0.dev226 → specsmith-0.5.0.dev228}/src/specsmith/templates/docs/readthedocs.yaml.j2 +0 -0
- {specsmith-0.5.0.dev226 → specsmith-0.5.0.dev228}/src/specsmith/templates/docs/requirements.md.j2 +0 -0
- {specsmith-0.5.0.dev226 → specsmith-0.5.0.dev228}/src/specsmith/templates/docs/test-spec.md.j2 +0 -0
- {specsmith-0.5.0.dev226 → specsmith-0.5.0.dev228}/src/specsmith/templates/editorconfig.j2 +0 -0
- {specsmith-0.5.0.dev226 → specsmith-0.5.0.dev228}/src/specsmith/templates/gitattributes.j2 +0 -0
- {specsmith-0.5.0.dev226 → specsmith-0.5.0.dev228}/src/specsmith/templates/gitignore.j2 +0 -0
- {specsmith-0.5.0.dev226 → specsmith-0.5.0.dev228}/src/specsmith/templates/go/go.mod.j2 +0 -0
- {specsmith-0.5.0.dev226 → specsmith-0.5.0.dev228}/src/specsmith/templates/go/main.go.j2 +0 -0
- {specsmith-0.5.0.dev226 → specsmith-0.5.0.dev228}/src/specsmith/templates/governance/belief-registry.md.j2 +0 -0
- {specsmith-0.5.0.dev226 → specsmith-0.5.0.dev228}/src/specsmith/templates/governance/context-budget.md.j2 +0 -0
- {specsmith-0.5.0.dev226 → specsmith-0.5.0.dev228}/src/specsmith/templates/governance/drift-metrics.md.j2 +0 -0
- {specsmith-0.5.0.dev226 → specsmith-0.5.0.dev228}/src/specsmith/templates/governance/epistemic-axioms.md.j2 +0 -0
- {specsmith-0.5.0.dev226 → specsmith-0.5.0.dev228}/src/specsmith/templates/governance/failure-modes.md.j2 +0 -0
- {specsmith-0.5.0.dev226 → specsmith-0.5.0.dev228}/src/specsmith/templates/governance/lifecycle.md.j2 +0 -0
- {specsmith-0.5.0.dev226 → specsmith-0.5.0.dev228}/src/specsmith/templates/governance/roles.md.j2 +0 -0
- {specsmith-0.5.0.dev226 → specsmith-0.5.0.dev228}/src/specsmith/templates/governance/rules.md.j2 +0 -0
- {specsmith-0.5.0.dev226 → specsmith-0.5.0.dev228}/src/specsmith/templates/governance/session-protocol.md.j2 +0 -0
- {specsmith-0.5.0.dev226 → specsmith-0.5.0.dev228}/src/specsmith/templates/governance/uncertainty-map.md.j2 +0 -0
- {specsmith-0.5.0.dev226 → specsmith-0.5.0.dev228}/src/specsmith/templates/governance/verification.md.j2 +0 -0
- {specsmith-0.5.0.dev226 → specsmith-0.5.0.dev228}/src/specsmith/templates/js/package.json.j2 +0 -0
- {specsmith-0.5.0.dev226 → specsmith-0.5.0.dev228}/src/specsmith/templates/ledger.md.j2 +0 -0
- {specsmith-0.5.0.dev226 → specsmith-0.5.0.dev228}/src/specsmith/templates/python/cli.py.j2 +0 -0
- {specsmith-0.5.0.dev226 → specsmith-0.5.0.dev228}/src/specsmith/templates/python/init.py.j2 +0 -0
- {specsmith-0.5.0.dev226 → specsmith-0.5.0.dev228}/src/specsmith/templates/python/pyproject.toml.j2 +0 -0
- {specsmith-0.5.0.dev226 → specsmith-0.5.0.dev228}/src/specsmith/templates/readme.md.j2 +0 -0
- {specsmith-0.5.0.dev226 → specsmith-0.5.0.dev228}/src/specsmith/templates/rust/Cargo.toml.j2 +0 -0
- {specsmith-0.5.0.dev226 → specsmith-0.5.0.dev228}/src/specsmith/templates/rust/main.rs.j2 +0 -0
- {specsmith-0.5.0.dev226 → specsmith-0.5.0.dev228}/src/specsmith/templates/scripts/exec.cmd.j2 +0 -0
- {specsmith-0.5.0.dev226 → specsmith-0.5.0.dev228}/src/specsmith/templates/scripts/exec.sh.j2 +0 -0
- {specsmith-0.5.0.dev226 → specsmith-0.5.0.dev228}/src/specsmith/templates/scripts/run.cmd.j2 +0 -0
- {specsmith-0.5.0.dev226 → specsmith-0.5.0.dev228}/src/specsmith/templates/scripts/run.sh.j2 +0 -0
- {specsmith-0.5.0.dev226 → specsmith-0.5.0.dev228}/src/specsmith/templates/scripts/setup.cmd.j2 +0 -0
- {specsmith-0.5.0.dev226 → specsmith-0.5.0.dev228}/src/specsmith/templates/scripts/setup.sh.j2 +0 -0
- {specsmith-0.5.0.dev226 → specsmith-0.5.0.dev228}/src/specsmith/templates/workflows/release.yml.j2 +0 -0
- {specsmith-0.5.0.dev226 → specsmith-0.5.0.dev228}/src/specsmith/tool_installer.py +0 -0
- {specsmith-0.5.0.dev226 → specsmith-0.5.0.dev228}/src/specsmith/toolrules.py +0 -0
- {specsmith-0.5.0.dev226 → specsmith-0.5.0.dev228}/src/specsmith/tools.py +0 -0
- {specsmith-0.5.0.dev226 → specsmith-0.5.0.dev228}/src/specsmith/trace.py +0 -0
- {specsmith-0.5.0.dev226 → specsmith-0.5.0.dev228}/src/specsmith/updater.py +0 -0
- {specsmith-0.5.0.dev226 → specsmith-0.5.0.dev228}/src/specsmith/upgrader.py +0 -0
- {specsmith-0.5.0.dev226 → specsmith-0.5.0.dev228}/src/specsmith/validator.py +0 -0
- {specsmith-0.5.0.dev226 → specsmith-0.5.0.dev228}/src/specsmith/vcs/__init__.py +0 -0
- {specsmith-0.5.0.dev226 → specsmith-0.5.0.dev228}/src/specsmith/vcs/base.py +0 -0
- {specsmith-0.5.0.dev226 → specsmith-0.5.0.dev228}/src/specsmith/vcs/bitbucket.py +0 -0
- {specsmith-0.5.0.dev226 → specsmith-0.5.0.dev228}/src/specsmith/vcs/github.py +0 -0
- {specsmith-0.5.0.dev226 → specsmith-0.5.0.dev228}/src/specsmith/vcs/gitlab.py +0 -0
- {specsmith-0.5.0.dev226 → specsmith-0.5.0.dev228}/src/specsmith/vcs_commands.py +0 -0
- {specsmith-0.5.0.dev226 → specsmith-0.5.0.dev228}/src/specsmith/wireframes.py +0 -0
- {specsmith-0.5.0.dev226 → specsmith-0.5.0.dev228}/src/specsmith/workspace.py +0 -0
- {specsmith-0.5.0.dev226 → specsmith-0.5.0.dev228}/src/specsmith.egg-info/dependency_links.txt +0 -0
- {specsmith-0.5.0.dev226 → specsmith-0.5.0.dev228}/src/specsmith.egg-info/entry_points.txt +0 -0
- {specsmith-0.5.0.dev226 → specsmith-0.5.0.dev228}/src/specsmith.egg-info/requires.txt +0 -0
- {specsmith-0.5.0.dev226 → specsmith-0.5.0.dev228}/src/specsmith.egg-info/top_level.txt +0 -0
- {specsmith-0.5.0.dev226 → specsmith-0.5.0.dev228}/tests/test_CMD_001.py +0 -0
- {specsmith-0.5.0.dev226 → specsmith-0.5.0.dev228}/tests/test_auditor.py +0 -0
- {specsmith-0.5.0.dev226 → specsmith-0.5.0.dev228}/tests/test_chat_stdin_protocol.py +0 -0
- {specsmith-0.5.0.dev226 → specsmith-0.5.0.dev228}/tests/test_cli.py +0 -0
- {specsmith-0.5.0.dev226 → specsmith-0.5.0.dev228}/tests/test_cli_workflows_history_drive.py +0 -0
- {specsmith-0.5.0.dev226 → specsmith-0.5.0.dev228}/tests/test_compressor.py +0 -0
- {specsmith-0.5.0.dev226 → specsmith-0.5.0.dev228}/tests/test_e2e_nexus.py +0 -0
- {specsmith-0.5.0.dev226 → specsmith-0.5.0.dev228}/tests/test_epistemic.py +0 -0
- {specsmith-0.5.0.dev226 → specsmith-0.5.0.dev228}/tests/test_importer.py +0 -0
- {specsmith-0.5.0.dev226 → specsmith-0.5.0.dev228}/tests/test_integrations.py +0 -0
- {specsmith-0.5.0.dev226 → specsmith-0.5.0.dev228}/tests/test_nexus.py +0 -0
- {specsmith-0.5.0.dev226 → specsmith-0.5.0.dev228}/tests/test_phase1_4_new.py +0 -0
- {specsmith-0.5.0.dev226 → specsmith-0.5.0.dev228}/tests/test_rate_limits.py +0 -0
- {specsmith-0.5.0.dev226 → specsmith-0.5.0.dev228}/tests/test_scaffolder.py +0 -0
- {specsmith-0.5.0.dev226 → specsmith-0.5.0.dev228}/tests/test_skill_marketplace.py +0 -0
- {specsmith-0.5.0.dev226 → specsmith-0.5.0.dev228}/tests/test_smoke.py +0 -0
- {specsmith-0.5.0.dev226 → specsmith-0.5.0.dev228}/tests/test_tools.py +0 -0
- {specsmith-0.5.0.dev226 → specsmith-0.5.0.dev228}/tests/test_validator.py +0 -0
- {specsmith-0.5.0.dev226 → specsmith-0.5.0.dev228}/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.dev228
|
|
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.dev228"
|
|
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"]
|
|
@@ -5258,6 +5258,7 @@ def chat_cmd(
|
|
|
5258
5258
|
events instead of executing tool calls (REQ-115).
|
|
5259
5259
|
"""
|
|
5260
5260
|
import json as _json
|
|
5261
|
+
import os
|
|
5261
5262
|
import uuid as _uuid
|
|
5262
5263
|
|
|
5263
5264
|
from specsmith.agent.events import EventEmitter
|
|
@@ -5363,10 +5364,38 @@ def chat_cmd(
|
|
|
5363
5364
|
emitter.tool_call(msg_block, "execute_with_governance", {"utterance": utterance})
|
|
5364
5365
|
emitter.plan_step(plan_block, "s2", "complete")
|
|
5365
5366
|
|
|
5366
|
-
#
|
|
5367
|
-
|
|
5368
|
-
|
|
5369
|
-
|
|
5367
|
+
# Real LLM turn — try Ollama / Anthropic / OpenAI / Gemini via
|
|
5368
|
+
# specsmith.agent.chat_runner. Any failure (no provider, network
|
|
5369
|
+
# error, missing SDK) returns ``None`` so we fall back to the
|
|
5370
|
+
# deterministic stub below. This keeps the test suite green on
|
|
5371
|
+
# machines without an LLM configured at all.
|
|
5372
|
+
real_result = None
|
|
5373
|
+
if os.environ.get("SPECSMITH_DISABLE_REAL_CHAT", "").lower() not in ("1", "true", "yes"):
|
|
5374
|
+
try:
|
|
5375
|
+
from specsmith.agent.chat_runner import run_chat as _run_chat
|
|
5376
|
+
|
|
5377
|
+
real_result = _run_chat(
|
|
5378
|
+
utterance,
|
|
5379
|
+
project_dir=root,
|
|
5380
|
+
profile=profile,
|
|
5381
|
+
session_id=sid,
|
|
5382
|
+
emitter=emitter,
|
|
5383
|
+
msg_block=msg_block,
|
|
5384
|
+
history=history,
|
|
5385
|
+
rules_prefix=rules_prefix,
|
|
5386
|
+
)
|
|
5387
|
+
except Exception: # noqa: BLE001 - real chat is best-effort
|
|
5388
|
+
real_result = None
|
|
5389
|
+
|
|
5390
|
+
if real_result is not None:
|
|
5391
|
+
verdict = real_result.verdict
|
|
5392
|
+
summary = real_result.summary or (verdict.summary if verdict else "")
|
|
5393
|
+
else:
|
|
5394
|
+
# Verifier sketch (deterministic, no LLM needed for this stub):
|
|
5395
|
+
verdict = None
|
|
5396
|
+
summary = (
|
|
5397
|
+
f"Preflight intent={real_intent.value}, matched_reqs={len(scope.matched_requirements)}."
|
|
5398
|
+
)
|
|
5370
5399
|
if reviewer_comment:
|
|
5371
5400
|
summary += f" reviewer_comment={reviewer_comment!r}"
|
|
5372
5401
|
emitter.plan_step(plan_block, "s3", "complete", summary=summary)
|
|
@@ -5383,13 +5412,13 @@ def chat_cmd(
|
|
|
5383
5412
|
for req in scope.matched_requirements[:3]:
|
|
5384
5413
|
diff_block = emitter.diff(
|
|
5385
5414
|
path=f"docs/{req.req_id}.md",
|
|
5386
|
-
|
|
5415
|
+
body=f"--- {req.req_id} (review)\n+++ {req.req_id} (proposed)\n",
|
|
5387
5416
|
)
|
|
5388
5417
|
decision = _read_stdin_decision("diff_decision", decision_timeout)
|
|
5389
|
-
|
|
5418
|
+
decision_status = (decision or {}).get("decision", "timeout")
|
|
5390
5419
|
comment = (decision or {}).get("comment", "")
|
|
5391
|
-
emitter.block_complete(diff_block, status=
|
|
5392
|
-
if
|
|
5420
|
+
emitter.block_complete(diff_block, status=decision_status)
|
|
5421
|
+
if decision_status != "accept" and comment:
|
|
5393
5422
|
extra_comment = comment
|
|
5394
5423
|
break
|
|
5395
5424
|
|
|
@@ -5397,9 +5426,12 @@ def chat_cmd(
|
|
|
5397
5426
|
if extra_comment:
|
|
5398
5427
|
final_summary += f" reviewer_comment={extra_comment!r}"
|
|
5399
5428
|
|
|
5429
|
+
final_confidence = (
|
|
5430
|
+
verdict.confidence if real_result is not None and verdict is not None else 0.7
|
|
5431
|
+
)
|
|
5400
5432
|
emitter.task_complete(
|
|
5401
|
-
success=
|
|
5402
|
-
confidence=
|
|
5433
|
+
success=real_result is None or (verdict is not None and verdict.equilibrium),
|
|
5434
|
+
confidence=final_confidence,
|
|
5403
5435
|
summary=final_summary,
|
|
5404
5436
|
profile=profile,
|
|
5405
5437
|
session_id=sid,
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: specsmith
|
|
3
|
-
Version: 0.5.0.
|
|
3
|
+
Version: 0.5.0.dev228
|
|
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
|
|
@@ -59,6 +59,7 @@ src/specsmith.egg-info/requires.txt
|
|
|
59
59
|
src/specsmith.egg-info/top_level.txt
|
|
60
60
|
src/specsmith/agent/__init__.py
|
|
61
61
|
src/specsmith/agent/broker.py
|
|
62
|
+
src/specsmith/agent/chat_runner.py
|
|
62
63
|
src/specsmith/agent/cleanup.py
|
|
63
64
|
src/specsmith/agent/events.py
|
|
64
65
|
src/specsmith/agent/indexer.py
|
|
@@ -152,6 +153,7 @@ src/specsmith/vcs/github.py
|
|
|
152
153
|
src/specsmith/vcs/gitlab.py
|
|
153
154
|
tests/test_CMD_001.py
|
|
154
155
|
tests/test_auditor.py
|
|
156
|
+
tests/test_chat_diff_decision.py
|
|
155
157
|
tests/test_chat_stdin_protocol.py
|
|
156
158
|
tests/test_cli.py
|
|
157
159
|
tests/test_cli_workflows_history_drive.py
|
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
# SPDX-License-Identifier: MIT
|
|
2
|
+
# Copyright (c) 2026 BitConcepts, LLC. All rights reserved.
|
|
3
|
+
"""Tests for `specsmith chat --interactive` inline diff-decision flow.
|
|
4
|
+
|
|
5
|
+
The diff-decision protocol is the second half of the interactive contract
|
|
6
|
+
(``test_chat_stdin_protocol`` covers tool_decision). When the broker
|
|
7
|
+
matches a requirement, the chat command emits one diff block per matched
|
|
8
|
+
REQ and consumes a ``diff_decision`` JSON line from stdin per block. A
|
|
9
|
+
non-accept verdict with a ``comment`` field becomes the next-retry
|
|
10
|
+
reviewer comment (REQ-116).
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
from __future__ import annotations
|
|
14
|
+
|
|
15
|
+
import json
|
|
16
|
+
from pathlib import Path
|
|
17
|
+
|
|
18
|
+
from click.testing import CliRunner
|
|
19
|
+
|
|
20
|
+
from specsmith.cli import main
|
|
21
|
+
|
|
22
|
+
REQUIREMENTS_MD = """# Requirements
|
|
23
|
+
|
|
24
|
+
## 1. Hello world greeter
|
|
25
|
+
|
|
26
|
+
- **ID:** REQ-001
|
|
27
|
+
- **Description:** Implement a hello world greeter so the agent can introduce itself.
|
|
28
|
+
"""
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def _seed_project(tmp_path: Path) -> None:
|
|
32
|
+
"""Write the minimum REQUIREMENTS.md so the broker has a scope match."""
|
|
33
|
+
(tmp_path / "REQUIREMENTS.md").write_text(REQUIREMENTS_MD, encoding="utf-8")
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def _events(output: str) -> list[dict]:
|
|
37
|
+
return [
|
|
38
|
+
json.loads(line)
|
|
39
|
+
for line in output.splitlines()
|
|
40
|
+
if line.startswith("{") and '"type"' in line
|
|
41
|
+
]
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def test_chat_interactive_diff_decision_emits_diff_block(tmp_path: Path) -> None:
|
|
45
|
+
"""A matched REQ should produce a diff block_start whose status accepts."""
|
|
46
|
+
_seed_project(tmp_path)
|
|
47
|
+
runner = CliRunner()
|
|
48
|
+
decisions = (
|
|
49
|
+
json.dumps({"type": "tool_decision", "decision": "approve"})
|
|
50
|
+
+ "\n"
|
|
51
|
+
+ json.dumps({"type": "diff_decision", "decision": "accept"})
|
|
52
|
+
+ "\n"
|
|
53
|
+
)
|
|
54
|
+
res = runner.invoke(
|
|
55
|
+
main,
|
|
56
|
+
[
|
|
57
|
+
"chat",
|
|
58
|
+
"add hello world greeter",
|
|
59
|
+
"--project-dir",
|
|
60
|
+
str(tmp_path),
|
|
61
|
+
"--profile",
|
|
62
|
+
"safe",
|
|
63
|
+
"--interactive",
|
|
64
|
+
"--decision-timeout",
|
|
65
|
+
"5",
|
|
66
|
+
],
|
|
67
|
+
input=decisions,
|
|
68
|
+
env={"SPECSMITH_DISABLE_REAL_CHAT": "1"},
|
|
69
|
+
)
|
|
70
|
+
assert res.exit_code == 0, res.output
|
|
71
|
+
events = _events(res.output)
|
|
72
|
+
diff_blocks = [e for e in events if e.get("type") == "block_start" and e.get("kind") == "diff"]
|
|
73
|
+
assert diff_blocks, "expected at least one diff block_start"
|
|
74
|
+
diff_completes = [
|
|
75
|
+
e
|
|
76
|
+
for e in events
|
|
77
|
+
if e.get("type") == "block_complete" and e.get("block_id") == diff_blocks[0]["block_id"]
|
|
78
|
+
]
|
|
79
|
+
assert diff_completes, "diff block should have been completed"
|
|
80
|
+
assert diff_completes[-1]["payload"].get("status") == "accept"
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
def test_chat_interactive_diff_decision_reject_threads_comment(tmp_path: Path) -> None:
|
|
84
|
+
"""A non-accept verdict with a comment should fold into the final summary."""
|
|
85
|
+
_seed_project(tmp_path)
|
|
86
|
+
runner = CliRunner()
|
|
87
|
+
decisions = (
|
|
88
|
+
json.dumps({"type": "tool_decision", "decision": "approve"})
|
|
89
|
+
+ "\n"
|
|
90
|
+
+ json.dumps(
|
|
91
|
+
{
|
|
92
|
+
"type": "diff_decision",
|
|
93
|
+
"decision": "reject",
|
|
94
|
+
"comment": "use uppercase greeting",
|
|
95
|
+
}
|
|
96
|
+
)
|
|
97
|
+
+ "\n"
|
|
98
|
+
)
|
|
99
|
+
res = runner.invoke(
|
|
100
|
+
main,
|
|
101
|
+
[
|
|
102
|
+
"chat",
|
|
103
|
+
"add hello world greeter",
|
|
104
|
+
"--project-dir",
|
|
105
|
+
str(tmp_path),
|
|
106
|
+
"--profile",
|
|
107
|
+
"safe",
|
|
108
|
+
"--interactive",
|
|
109
|
+
"--decision-timeout",
|
|
110
|
+
"5",
|
|
111
|
+
],
|
|
112
|
+
input=decisions,
|
|
113
|
+
env={"SPECSMITH_DISABLE_REAL_CHAT": "1"},
|
|
114
|
+
)
|
|
115
|
+
assert res.exit_code == 0, res.output
|
|
116
|
+
events = _events(res.output)
|
|
117
|
+
diff_completes = [
|
|
118
|
+
e
|
|
119
|
+
for e in events
|
|
120
|
+
if e.get("type") == "block_complete" and e.get("payload", {}).get("status") == "reject"
|
|
121
|
+
]
|
|
122
|
+
assert diff_completes, "expected the diff block to be completed with reject status"
|
|
123
|
+
completes = [e for e in events if e.get("type") == "task_complete"]
|
|
124
|
+
assert completes
|
|
125
|
+
assert "use uppercase greeting" in completes[-1].get("summary", "")
|
|
126
|
+
|
|
127
|
+
|
|
128
|
+
def test_chat_interactive_diff_decision_timeout_uses_timeout_status(
|
|
129
|
+
tmp_path: Path,
|
|
130
|
+
) -> None:
|
|
131
|
+
"""No diff_decision on stdin should mark the diff block as timeout."""
|
|
132
|
+
_seed_project(tmp_path)
|
|
133
|
+
runner = CliRunner()
|
|
134
|
+
decisions = json.dumps({"type": "tool_decision", "decision": "approve"}) + "\n"
|
|
135
|
+
res = runner.invoke(
|
|
136
|
+
main,
|
|
137
|
+
[
|
|
138
|
+
"chat",
|
|
139
|
+
"add hello world greeter",
|
|
140
|
+
"--project-dir",
|
|
141
|
+
str(tmp_path),
|
|
142
|
+
"--profile",
|
|
143
|
+
"safe",
|
|
144
|
+
"--interactive",
|
|
145
|
+
"--decision-timeout",
|
|
146
|
+
"1",
|
|
147
|
+
],
|
|
148
|
+
input=decisions,
|
|
149
|
+
env={"SPECSMITH_DISABLE_REAL_CHAT": "1"},
|
|
150
|
+
)
|
|
151
|
+
assert res.exit_code == 0, res.output
|
|
152
|
+
events = _events(res.output)
|
|
153
|
+
diff_completes = [
|
|
154
|
+
e
|
|
155
|
+
for e in events
|
|
156
|
+
if e.get("type") == "block_complete" and e.get("payload", {}).get("status") == "timeout"
|
|
157
|
+
]
|
|
158
|
+
assert diff_completes, "expected at least one diff block to time out"
|
|
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
|
|
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
|