specsmith 0.6.0.dev233__tar.gz → 0.7.0__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.6.0.dev233/src/specsmith.egg-info → specsmith-0.7.0}/PKG-INFO +1 -1
- {specsmith-0.6.0.dev233 → specsmith-0.7.0}/pyproject.toml +1 -1
- specsmith-0.7.0/src/specsmith/agent/voice.py +135 -0
- {specsmith-0.6.0.dev233 → specsmith-0.7.0}/src/specsmith/cli.py +185 -100
- {specsmith-0.6.0.dev233 → specsmith-0.7.0}/src/specsmith/serve.py +58 -4
- {specsmith-0.6.0.dev233 → specsmith-0.7.0/src/specsmith.egg-info}/PKG-INFO +1 -1
- {specsmith-0.6.0.dev233 → specsmith-0.7.0}/src/specsmith.egg-info/SOURCES.txt +3 -1
- {specsmith-0.6.0.dev233 → specsmith-0.7.0}/tests/test_phase34_completion.py +14 -18
- specsmith-0.7.0/tests/test_warp_parity_followup.py +368 -0
- {specsmith-0.6.0.dev233 → specsmith-0.7.0}/LICENSE +0 -0
- {specsmith-0.6.0.dev233 → specsmith-0.7.0}/README.md +0 -0
- {specsmith-0.6.0.dev233 → specsmith-0.7.0}/setup.cfg +0 -0
- {specsmith-0.6.0.dev233 → specsmith-0.7.0}/src/epistemic/__init__.py +0 -0
- {specsmith-0.6.0.dev233 → specsmith-0.7.0}/src/epistemic/belief.py +0 -0
- {specsmith-0.6.0.dev233 → specsmith-0.7.0}/src/epistemic/certainty.py +0 -0
- {specsmith-0.6.0.dev233 → specsmith-0.7.0}/src/epistemic/failure_graph.py +0 -0
- {specsmith-0.6.0.dev233 → specsmith-0.7.0}/src/epistemic/py.typed +0 -0
- {specsmith-0.6.0.dev233 → specsmith-0.7.0}/src/epistemic/recovery.py +0 -0
- {specsmith-0.6.0.dev233 → specsmith-0.7.0}/src/epistemic/session.py +0 -0
- {specsmith-0.6.0.dev233 → specsmith-0.7.0}/src/epistemic/stress_tester.py +0 -0
- {specsmith-0.6.0.dev233 → specsmith-0.7.0}/src/epistemic/trace.py +0 -0
- {specsmith-0.6.0.dev233 → specsmith-0.7.0}/src/specsmith/__init__.py +0 -0
- {specsmith-0.6.0.dev233 → specsmith-0.7.0}/src/specsmith/__main__.py +0 -0
- {specsmith-0.6.0.dev233 → specsmith-0.7.0}/src/specsmith/agent/__init__.py +0 -0
- {specsmith-0.6.0.dev233 → specsmith-0.7.0}/src/specsmith/agent/broker.py +0 -0
- {specsmith-0.6.0.dev233 → specsmith-0.7.0}/src/specsmith/agent/chat_runner.py +0 -0
- {specsmith-0.6.0.dev233 → specsmith-0.7.0}/src/specsmith/agent/cleanup.py +0 -0
- {specsmith-0.6.0.dev233 → specsmith-0.7.0}/src/specsmith/agent/events.py +0 -0
- {specsmith-0.6.0.dev233 → specsmith-0.7.0}/src/specsmith/agent/indexer.py +0 -0
- {specsmith-0.6.0.dev233 → specsmith-0.7.0}/src/specsmith/agent/mcp.py +0 -0
- {specsmith-0.6.0.dev233 → specsmith-0.7.0}/src/specsmith/agent/memory.py +0 -0
- {specsmith-0.6.0.dev233 → specsmith-0.7.0}/src/specsmith/agent/orchestrator.py +0 -0
- {specsmith-0.6.0.dev233 → specsmith-0.7.0}/src/specsmith/agent/repl.py +0 -0
- {specsmith-0.6.0.dev233 → specsmith-0.7.0}/src/specsmith/agent/router.py +0 -0
- {specsmith-0.6.0.dev233 → specsmith-0.7.0}/src/specsmith/agent/rules.py +0 -0
- {specsmith-0.6.0.dev233 → specsmith-0.7.0}/src/specsmith/agent/safety.py +0 -0
- {specsmith-0.6.0.dev233 → specsmith-0.7.0}/src/specsmith/agent/suggester.py +0 -0
- {specsmith-0.6.0.dev233 → specsmith-0.7.0}/src/specsmith/agent/tools.py +0 -0
- {specsmith-0.6.0.dev233 → specsmith-0.7.0}/src/specsmith/agent/verifier.py +0 -0
- {specsmith-0.6.0.dev233 → specsmith-0.7.0}/src/specsmith/architect.py +0 -0
- {specsmith-0.6.0.dev233 → specsmith-0.7.0}/src/specsmith/auditor.py +0 -0
- {specsmith-0.6.0.dev233 → specsmith-0.7.0}/src/specsmith/auth.py +0 -0
- {specsmith-0.6.0.dev233 → specsmith-0.7.0}/src/specsmith/block_export.py +0 -0
- {specsmith-0.6.0.dev233 → specsmith-0.7.0}/src/specsmith/cloud_serve.py +0 -0
- {specsmith-0.6.0.dev233 → specsmith-0.7.0}/src/specsmith/commands/__init__.py +0 -0
- {specsmith-0.6.0.dev233 → specsmith-0.7.0}/src/specsmith/compressor.py +0 -0
- {specsmith-0.6.0.dev233 → specsmith-0.7.0}/src/specsmith/config.py +0 -0
- {specsmith-0.6.0.dev233 → specsmith-0.7.0}/src/specsmith/console_utils.py +0 -0
- {specsmith-0.6.0.dev233 → specsmith-0.7.0}/src/specsmith/credit_analyzer.py +0 -0
- {specsmith-0.6.0.dev233 → specsmith-0.7.0}/src/specsmith/credits.py +0 -0
- {specsmith-0.6.0.dev233 → specsmith-0.7.0}/src/specsmith/differ.py +0 -0
- {specsmith-0.6.0.dev233 → specsmith-0.7.0}/src/specsmith/doctor.py +0 -0
- {specsmith-0.6.0.dev233 → specsmith-0.7.0}/src/specsmith/drive.py +0 -0
- {specsmith-0.6.0.dev233 → specsmith-0.7.0}/src/specsmith/epistemic/__init__.py +0 -0
- {specsmith-0.6.0.dev233 → specsmith-0.7.0}/src/specsmith/epistemic/belief.py +0 -0
- {specsmith-0.6.0.dev233 → specsmith-0.7.0}/src/specsmith/epistemic/certainty.py +0 -0
- {specsmith-0.6.0.dev233 → specsmith-0.7.0}/src/specsmith/epistemic/failure_graph.py +0 -0
- {specsmith-0.6.0.dev233 → specsmith-0.7.0}/src/specsmith/epistemic/recovery.py +0 -0
- {specsmith-0.6.0.dev233 → specsmith-0.7.0}/src/specsmith/epistemic/stress_tester.py +0 -0
- {specsmith-0.6.0.dev233 → specsmith-0.7.0}/src/specsmith/executor.py +0 -0
- {specsmith-0.6.0.dev233 → specsmith-0.7.0}/src/specsmith/exporter.py +0 -0
- {specsmith-0.6.0.dev233 → specsmith-0.7.0}/src/specsmith/gui/__init__.py +0 -0
- {specsmith-0.6.0.dev233 → specsmith-0.7.0}/src/specsmith/gui/app.py +0 -0
- {specsmith-0.6.0.dev233 → specsmith-0.7.0}/src/specsmith/gui/main_window.py +0 -0
- {specsmith-0.6.0.dev233 → specsmith-0.7.0}/src/specsmith/gui/session_tab.py +0 -0
- {specsmith-0.6.0.dev233 → specsmith-0.7.0}/src/specsmith/gui/theme.py +0 -0
- {specsmith-0.6.0.dev233 → specsmith-0.7.0}/src/specsmith/gui/widgets/__init__.py +0 -0
- {specsmith-0.6.0.dev233 → specsmith-0.7.0}/src/specsmith/gui/widgets/chat_view.py +0 -0
- {specsmith-0.6.0.dev233 → specsmith-0.7.0}/src/specsmith/gui/widgets/input_bar.py +0 -0
- {specsmith-0.6.0.dev233 → specsmith-0.7.0}/src/specsmith/gui/widgets/provider_bar.py +0 -0
- {specsmith-0.6.0.dev233 → specsmith-0.7.0}/src/specsmith/gui/widgets/token_meter.py +0 -0
- {specsmith-0.6.0.dev233 → specsmith-0.7.0}/src/specsmith/gui/widgets/tool_panel.py +0 -0
- {specsmith-0.6.0.dev233 → specsmith-0.7.0}/src/specsmith/gui/widgets/update_checker.py +0 -0
- {specsmith-0.6.0.dev233 → specsmith-0.7.0}/src/specsmith/gui/worker.py +0 -0
- {specsmith-0.6.0.dev233 → specsmith-0.7.0}/src/specsmith/history_search.py +0 -0
- {specsmith-0.6.0.dev233 → specsmith-0.7.0}/src/specsmith/importer.py +0 -0
- {specsmith-0.6.0.dev233 → specsmith-0.7.0}/src/specsmith/integrations/__init__.py +0 -0
- {specsmith-0.6.0.dev233 → specsmith-0.7.0}/src/specsmith/integrations/agent_skill.py +0 -0
- {specsmith-0.6.0.dev233 → specsmith-0.7.0}/src/specsmith/integrations/aider.py +0 -0
- {specsmith-0.6.0.dev233 → specsmith-0.7.0}/src/specsmith/integrations/base.py +0 -0
- {specsmith-0.6.0.dev233 → specsmith-0.7.0}/src/specsmith/integrations/claude_code.py +0 -0
- {specsmith-0.6.0.dev233 → specsmith-0.7.0}/src/specsmith/integrations/copilot.py +0 -0
- {specsmith-0.6.0.dev233 → specsmith-0.7.0}/src/specsmith/integrations/cursor.py +0 -0
- {specsmith-0.6.0.dev233 → specsmith-0.7.0}/src/specsmith/integrations/gemini.py +0 -0
- {specsmith-0.6.0.dev233 → specsmith-0.7.0}/src/specsmith/integrations/windsurf.py +0 -0
- {specsmith-0.6.0.dev233 → specsmith-0.7.0}/src/specsmith/languages.py +0 -0
- {specsmith-0.6.0.dev233 → specsmith-0.7.0}/src/specsmith/ledger.py +0 -0
- {specsmith-0.6.0.dev233 → specsmith-0.7.0}/src/specsmith/patent.py +0 -0
- {specsmith-0.6.0.dev233 → specsmith-0.7.0}/src/specsmith/phase.py +0 -0
- {specsmith-0.6.0.dev233 → specsmith-0.7.0}/src/specsmith/plugins.py +0 -0
- {specsmith-0.6.0.dev233 → specsmith-0.7.0}/src/specsmith/profiles.py +0 -0
- {specsmith-0.6.0.dev233 → specsmith-0.7.0}/src/specsmith/rate_limits.py +0 -0
- {specsmith-0.6.0.dev233 → specsmith-0.7.0}/src/specsmith/releaser.py +0 -0
- {specsmith-0.6.0.dev233 → specsmith-0.7.0}/src/specsmith/requirements.py +0 -0
- {specsmith-0.6.0.dev233 → specsmith-0.7.0}/src/specsmith/requirements_parser.py +0 -0
- {specsmith-0.6.0.dev233 → specsmith-0.7.0}/src/specsmith/retrieval.py +0 -0
- {specsmith-0.6.0.dev233 → specsmith-0.7.0}/src/specsmith/scaffolder.py +0 -0
- {specsmith-0.6.0.dev233 → specsmith-0.7.0}/src/specsmith/session.py +0 -0
- {specsmith-0.6.0.dev233 → specsmith-0.7.0}/src/specsmith/skills.py +0 -0
- {specsmith-0.6.0.dev233 → specsmith-0.7.0}/src/specsmith/templates/agents.md.j2 +0 -0
- {specsmith-0.6.0.dev233 → specsmith-0.7.0}/src/specsmith/templates/community/bug_report.md.j2 +0 -0
- {specsmith-0.6.0.dev233 → specsmith-0.7.0}/src/specsmith/templates/community/code_of_conduct.md.j2 +0 -0
- {specsmith-0.6.0.dev233 → specsmith-0.7.0}/src/specsmith/templates/community/contributing.md.j2 +0 -0
- {specsmith-0.6.0.dev233 → specsmith-0.7.0}/src/specsmith/templates/community/feature_request.md.j2 +0 -0
- {specsmith-0.6.0.dev233 → specsmith-0.7.0}/src/specsmith/templates/community/license-Apache-2.0.j2 +0 -0
- {specsmith-0.6.0.dev233 → specsmith-0.7.0}/src/specsmith/templates/community/license-MIT.j2 +0 -0
- {specsmith-0.6.0.dev233 → specsmith-0.7.0}/src/specsmith/templates/community/pull_request_template.md.j2 +0 -0
- {specsmith-0.6.0.dev233 → specsmith-0.7.0}/src/specsmith/templates/community/security.md.j2 +0 -0
- {specsmith-0.6.0.dev233 → specsmith-0.7.0}/src/specsmith/templates/docs/architecture.md.j2 +0 -0
- {specsmith-0.6.0.dev233 → specsmith-0.7.0}/src/specsmith/templates/docs/mkdocs.yml.j2 +0 -0
- {specsmith-0.6.0.dev233 → specsmith-0.7.0}/src/specsmith/templates/docs/readthedocs.yaml.j2 +0 -0
- {specsmith-0.6.0.dev233 → specsmith-0.7.0}/src/specsmith/templates/docs/requirements.md.j2 +0 -0
- {specsmith-0.6.0.dev233 → specsmith-0.7.0}/src/specsmith/templates/docs/test-spec.md.j2 +0 -0
- {specsmith-0.6.0.dev233 → specsmith-0.7.0}/src/specsmith/templates/editorconfig.j2 +0 -0
- {specsmith-0.6.0.dev233 → specsmith-0.7.0}/src/specsmith/templates/gitattributes.j2 +0 -0
- {specsmith-0.6.0.dev233 → specsmith-0.7.0}/src/specsmith/templates/gitignore.j2 +0 -0
- {specsmith-0.6.0.dev233 → specsmith-0.7.0}/src/specsmith/templates/go/go.mod.j2 +0 -0
- {specsmith-0.6.0.dev233 → specsmith-0.7.0}/src/specsmith/templates/go/main.go.j2 +0 -0
- {specsmith-0.6.0.dev233 → specsmith-0.7.0}/src/specsmith/templates/governance/belief-registry.md.j2 +0 -0
- {specsmith-0.6.0.dev233 → specsmith-0.7.0}/src/specsmith/templates/governance/context-budget.md.j2 +0 -0
- {specsmith-0.6.0.dev233 → specsmith-0.7.0}/src/specsmith/templates/governance/drift-metrics.md.j2 +0 -0
- {specsmith-0.6.0.dev233 → specsmith-0.7.0}/src/specsmith/templates/governance/epistemic-axioms.md.j2 +0 -0
- {specsmith-0.6.0.dev233 → specsmith-0.7.0}/src/specsmith/templates/governance/failure-modes.md.j2 +0 -0
- {specsmith-0.6.0.dev233 → specsmith-0.7.0}/src/specsmith/templates/governance/lifecycle.md.j2 +0 -0
- {specsmith-0.6.0.dev233 → specsmith-0.7.0}/src/specsmith/templates/governance/roles.md.j2 +0 -0
- {specsmith-0.6.0.dev233 → specsmith-0.7.0}/src/specsmith/templates/governance/rules.md.j2 +0 -0
- {specsmith-0.6.0.dev233 → specsmith-0.7.0}/src/specsmith/templates/governance/session-protocol.md.j2 +0 -0
- {specsmith-0.6.0.dev233 → specsmith-0.7.0}/src/specsmith/templates/governance/uncertainty-map.md.j2 +0 -0
- {specsmith-0.6.0.dev233 → specsmith-0.7.0}/src/specsmith/templates/governance/verification.md.j2 +0 -0
- {specsmith-0.6.0.dev233 → specsmith-0.7.0}/src/specsmith/templates/js/package.json.j2 +0 -0
- {specsmith-0.6.0.dev233 → specsmith-0.7.0}/src/specsmith/templates/ledger.md.j2 +0 -0
- {specsmith-0.6.0.dev233 → specsmith-0.7.0}/src/specsmith/templates/python/cli.py.j2 +0 -0
- {specsmith-0.6.0.dev233 → specsmith-0.7.0}/src/specsmith/templates/python/init.py.j2 +0 -0
- {specsmith-0.6.0.dev233 → specsmith-0.7.0}/src/specsmith/templates/python/pyproject.toml.j2 +0 -0
- {specsmith-0.6.0.dev233 → specsmith-0.7.0}/src/specsmith/templates/readme.md.j2 +0 -0
- {specsmith-0.6.0.dev233 → specsmith-0.7.0}/src/specsmith/templates/rust/Cargo.toml.j2 +0 -0
- {specsmith-0.6.0.dev233 → specsmith-0.7.0}/src/specsmith/templates/rust/main.rs.j2 +0 -0
- {specsmith-0.6.0.dev233 → specsmith-0.7.0}/src/specsmith/templates/scripts/exec.cmd.j2 +0 -0
- {specsmith-0.6.0.dev233 → specsmith-0.7.0}/src/specsmith/templates/scripts/exec.sh.j2 +0 -0
- {specsmith-0.6.0.dev233 → specsmith-0.7.0}/src/specsmith/templates/scripts/run.cmd.j2 +0 -0
- {specsmith-0.6.0.dev233 → specsmith-0.7.0}/src/specsmith/templates/scripts/run.sh.j2 +0 -0
- {specsmith-0.6.0.dev233 → specsmith-0.7.0}/src/specsmith/templates/scripts/setup.cmd.j2 +0 -0
- {specsmith-0.6.0.dev233 → specsmith-0.7.0}/src/specsmith/templates/scripts/setup.sh.j2 +0 -0
- {specsmith-0.6.0.dev233 → specsmith-0.7.0}/src/specsmith/templates/workflows/release.yml.j2 +0 -0
- {specsmith-0.6.0.dev233 → specsmith-0.7.0}/src/specsmith/tool_installer.py +0 -0
- {specsmith-0.6.0.dev233 → specsmith-0.7.0}/src/specsmith/toolrules.py +0 -0
- {specsmith-0.6.0.dev233 → specsmith-0.7.0}/src/specsmith/tools.py +0 -0
- {specsmith-0.6.0.dev233 → specsmith-0.7.0}/src/specsmith/trace.py +0 -0
- {specsmith-0.6.0.dev233 → specsmith-0.7.0}/src/specsmith/updater.py +0 -0
- {specsmith-0.6.0.dev233 → specsmith-0.7.0}/src/specsmith/upgrader.py +0 -0
- {specsmith-0.6.0.dev233 → specsmith-0.7.0}/src/specsmith/validator.py +0 -0
- {specsmith-0.6.0.dev233 → specsmith-0.7.0}/src/specsmith/vcs/__init__.py +0 -0
- {specsmith-0.6.0.dev233 → specsmith-0.7.0}/src/specsmith/vcs/base.py +0 -0
- {specsmith-0.6.0.dev233 → specsmith-0.7.0}/src/specsmith/vcs/bitbucket.py +0 -0
- {specsmith-0.6.0.dev233 → specsmith-0.7.0}/src/specsmith/vcs/github.py +0 -0
- {specsmith-0.6.0.dev233 → specsmith-0.7.0}/src/specsmith/vcs/gitlab.py +0 -0
- {specsmith-0.6.0.dev233 → specsmith-0.7.0}/src/specsmith/vcs_commands.py +0 -0
- {specsmith-0.6.0.dev233 → specsmith-0.7.0}/src/specsmith/wireframes.py +0 -0
- {specsmith-0.6.0.dev233 → specsmith-0.7.0}/src/specsmith/workspace.py +0 -0
- {specsmith-0.6.0.dev233 → specsmith-0.7.0}/src/specsmith.egg-info/dependency_links.txt +0 -0
- {specsmith-0.6.0.dev233 → specsmith-0.7.0}/src/specsmith.egg-info/entry_points.txt +0 -0
- {specsmith-0.6.0.dev233 → specsmith-0.7.0}/src/specsmith.egg-info/requires.txt +0 -0
- {specsmith-0.6.0.dev233 → specsmith-0.7.0}/src/specsmith.egg-info/top_level.txt +0 -0
- {specsmith-0.6.0.dev233 → specsmith-0.7.0}/tests/test_CMD_001.py +0 -0
- {specsmith-0.6.0.dev233 → specsmith-0.7.0}/tests/test_auditor.py +0 -0
- {specsmith-0.6.0.dev233 → specsmith-0.7.0}/tests/test_chat_diff_decision.py +0 -0
- {specsmith-0.6.0.dev233 → specsmith-0.7.0}/tests/test_chat_stdin_protocol.py +0 -0
- {specsmith-0.6.0.dev233 → specsmith-0.7.0}/tests/test_cli.py +0 -0
- {specsmith-0.6.0.dev233 → specsmith-0.7.0}/tests/test_cli_workflows_history_drive.py +0 -0
- {specsmith-0.6.0.dev233 → specsmith-0.7.0}/tests/test_compressor.py +0 -0
- {specsmith-0.6.0.dev233 → specsmith-0.7.0}/tests/test_e2e_nexus.py +0 -0
- {specsmith-0.6.0.dev233 → specsmith-0.7.0}/tests/test_epistemic.py +0 -0
- {specsmith-0.6.0.dev233 → specsmith-0.7.0}/tests/test_importer.py +0 -0
- {specsmith-0.6.0.dev233 → specsmith-0.7.0}/tests/test_integrations.py +0 -0
- {specsmith-0.6.0.dev233 → specsmith-0.7.0}/tests/test_mcp_client.py +0 -0
- {specsmith-0.6.0.dev233 → specsmith-0.7.0}/tests/test_nexus.py +0 -0
- {specsmith-0.6.0.dev233 → specsmith-0.7.0}/tests/test_phase1_4_new.py +0 -0
- {specsmith-0.6.0.dev233 → specsmith-0.7.0}/tests/test_rate_limits.py +0 -0
- {specsmith-0.6.0.dev233 → specsmith-0.7.0}/tests/test_scaffolder.py +0 -0
- {specsmith-0.6.0.dev233 → specsmith-0.7.0}/tests/test_skill_marketplace.py +0 -0
- {specsmith-0.6.0.dev233 → specsmith-0.7.0}/tests/test_smoke.py +0 -0
- {specsmith-0.6.0.dev233 → specsmith-0.7.0}/tests/test_suggester.py +0 -0
- {specsmith-0.6.0.dev233 → specsmith-0.7.0}/tests/test_tools.py +0 -0
- {specsmith-0.6.0.dev233 → specsmith-0.7.0}/tests/test_validator.py +0 -0
- {specsmith-0.6.0.dev233 → specsmith-0.7.0}/tests/test_vcs.py +0 -0
- {specsmith-0.6.0.dev233 → specsmith-0.7.0}/tests/test_warp_parity.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: specsmith
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.7.0
|
|
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.
|
|
7
|
+
version = "0.7.0"
|
|
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,135 @@
|
|
|
1
|
+
# SPDX-License-Identifier: MIT
|
|
2
|
+
# Copyright (c) 2026 BitConcepts, LLC. All rights reserved.
|
|
3
|
+
"""Voice transcription wrapper (REQ-141).
|
|
4
|
+
|
|
5
|
+
Wraps the optional ``whisper_cpp_python`` library so the rest of specsmith
|
|
6
|
+
can call ``transcribe(path)`` without caring whether the extra is
|
|
7
|
+
installed. When the library is missing, ``transcribe`` raises
|
|
8
|
+
``VoiceUnavailableError`` with a friendly install hint so the caller can
|
|
9
|
+
surface it to the user.
|
|
10
|
+
|
|
11
|
+
The wrapper supports three modes:
|
|
12
|
+
|
|
13
|
+
* **real** -- ``whisper_cpp_python`` is installed and a model file is
|
|
14
|
+
available (auto-located under ``~/.specsmith/voice/`` or pointed to via
|
|
15
|
+
``SPECSMITH_VOICE_MODEL``). Real audio decoding.
|
|
16
|
+
* **stub** -- ``SPECSMITH_VOICE_STUB=<text>`` is set. Returns the literal
|
|
17
|
+
text without touching the audio file. Used by tests and CI so we don't
|
|
18
|
+
need to ship a 500MB model file.
|
|
19
|
+
* **unavailable** -- neither of the above. ``transcribe`` raises.
|
|
20
|
+
|
|
21
|
+
The CLI exposes this as ``specsmith voice transcribe <wav>``.
|
|
22
|
+
"""
|
|
23
|
+
|
|
24
|
+
from __future__ import annotations
|
|
25
|
+
|
|
26
|
+
import os
|
|
27
|
+
from dataclasses import dataclass
|
|
28
|
+
from pathlib import Path
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
class VoiceUnavailableError(RuntimeError):
|
|
32
|
+
"""Raised when whisper-cpp is not installed and no stub is set."""
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
@dataclass
|
|
36
|
+
class TranscribeResult:
|
|
37
|
+
text: str
|
|
38
|
+
backend: str # 'whisper-cpp', 'stub', 'unavailable'
|
|
39
|
+
model: str = ""
|
|
40
|
+
duration_s: float = 0.0
|
|
41
|
+
|
|
42
|
+
def to_dict(self) -> dict[str, object]:
|
|
43
|
+
return {
|
|
44
|
+
"text": self.text,
|
|
45
|
+
"backend": self.backend,
|
|
46
|
+
"model": self.model,
|
|
47
|
+
"duration_s": round(self.duration_s, 3),
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def default_model_dir() -> Path:
|
|
52
|
+
return Path.home() / ".specsmith" / "voice"
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
def _resolve_model_path() -> Path | None:
|
|
56
|
+
"""Return the on-disk model path, or None if no model is configured."""
|
|
57
|
+
env = os.environ.get("SPECSMITH_VOICE_MODEL", "").strip()
|
|
58
|
+
if env:
|
|
59
|
+
p = Path(env)
|
|
60
|
+
if p.is_file():
|
|
61
|
+
return p
|
|
62
|
+
# Auto-locate the smallest .bin under ~/.specsmith/voice/
|
|
63
|
+
voice_dir = default_model_dir()
|
|
64
|
+
if voice_dir.is_dir():
|
|
65
|
+
candidates = sorted(voice_dir.glob("*.bin"), key=lambda x: x.stat().st_size)
|
|
66
|
+
if candidates:
|
|
67
|
+
return candidates[0]
|
|
68
|
+
return None
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
def is_available() -> bool:
|
|
72
|
+
"""Cheap probe: True iff transcription would succeed without raising."""
|
|
73
|
+
if os.environ.get("SPECSMITH_VOICE_STUB", "").strip():
|
|
74
|
+
return True
|
|
75
|
+
try:
|
|
76
|
+
import whisper_cpp_python # noqa: F401 (presence-only check)
|
|
77
|
+
except Exception: # noqa: BLE001
|
|
78
|
+
return False
|
|
79
|
+
return _resolve_model_path() is not None
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
def transcribe(path: Path) -> TranscribeResult:
|
|
83
|
+
"""Transcribe a wav/flac/mp3 file to text.
|
|
84
|
+
|
|
85
|
+
Order of resolution:
|
|
86
|
+
1. If ``SPECSMITH_VOICE_STUB`` is set, return its value verbatim. This
|
|
87
|
+
lets tests run without a model file.
|
|
88
|
+
2. Otherwise import ``whisper_cpp_python`` and run a real transcription.
|
|
89
|
+
3. If neither is available, raise :class:`VoiceUnavailableError` with
|
|
90
|
+
an actionable message.
|
|
91
|
+
"""
|
|
92
|
+
import time as _time
|
|
93
|
+
|
|
94
|
+
if not path.exists():
|
|
95
|
+
raise FileNotFoundError(f"audio file not found: {path}")
|
|
96
|
+
|
|
97
|
+
stub = os.environ.get("SPECSMITH_VOICE_STUB", "").strip()
|
|
98
|
+
if stub:
|
|
99
|
+
return TranscribeResult(text=stub, backend="stub")
|
|
100
|
+
|
|
101
|
+
try:
|
|
102
|
+
import whisper_cpp_python
|
|
103
|
+
except Exception as exc: # noqa: BLE001
|
|
104
|
+
raise VoiceUnavailableError(
|
|
105
|
+
"whisper-cpp-python is not installed. Run "
|
|
106
|
+
"`pipx inject specsmith whisper-cpp-python` "
|
|
107
|
+
"(or `pip install specsmith[voice]`)."
|
|
108
|
+
) from exc
|
|
109
|
+
|
|
110
|
+
model_path = _resolve_model_path()
|
|
111
|
+
if model_path is None:
|
|
112
|
+
raise VoiceUnavailableError(
|
|
113
|
+
"No whisper model found. Set SPECSMITH_VOICE_MODEL or place a "
|
|
114
|
+
f".bin model under {default_model_dir()}."
|
|
115
|
+
)
|
|
116
|
+
|
|
117
|
+
start = _time.perf_counter()
|
|
118
|
+
whisper = whisper_cpp_python.Whisper(model_path=str(model_path))
|
|
119
|
+
out = whisper.transcribe(str(path))
|
|
120
|
+
text = out.get("text") if isinstance(out, dict) else str(out)
|
|
121
|
+
return TranscribeResult(
|
|
122
|
+
text=str(text or "").strip(),
|
|
123
|
+
backend="whisper-cpp",
|
|
124
|
+
model=model_path.name,
|
|
125
|
+
duration_s=_time.perf_counter() - start,
|
|
126
|
+
)
|
|
127
|
+
|
|
128
|
+
|
|
129
|
+
__all__ = [
|
|
130
|
+
"TranscribeResult",
|
|
131
|
+
"VoiceUnavailableError",
|
|
132
|
+
"default_model_dir",
|
|
133
|
+
"is_available",
|
|
134
|
+
"transcribe",
|
|
135
|
+
]
|
|
@@ -2796,12 +2796,23 @@ def run_cmd(
|
|
|
2796
2796
|
@click.option("--model", default="", help="Model name (blank = provider default).")
|
|
2797
2797
|
@click.option("--port", type=int, default=8421, help="HTTP port to listen on.")
|
|
2798
2798
|
@click.option("--host", default="127.0.0.1", help="Bind address (use 0.0.0.0 for network access).")
|
|
2799
|
+
@click.option(
|
|
2800
|
+
"--auth-token",
|
|
2801
|
+
"auth_token",
|
|
2802
|
+
default="",
|
|
2803
|
+
help=(
|
|
2804
|
+
"Optional bearer token (REQ-137). When set, every /api/* request must "
|
|
2805
|
+
"present `Authorization: Bearer <token>`. /api/health stays open so "
|
|
2806
|
+
"liveness probes still work."
|
|
2807
|
+
),
|
|
2808
|
+
)
|
|
2799
2809
|
def serve_cmd(
|
|
2800
2810
|
project_dir: str,
|
|
2801
2811
|
provider: str,
|
|
2802
2812
|
model: str,
|
|
2803
2813
|
port: int,
|
|
2804
2814
|
host: str,
|
|
2815
|
+
auth_token: str,
|
|
2805
2816
|
) -> None:
|
|
2806
2817
|
"""Start a persistent HTTP server for agent sessions.
|
|
2807
2818
|
|
|
@@ -2810,7 +2821,8 @@ def serve_cmd(
|
|
|
2810
2821
|
POST /api/send.
|
|
2811
2822
|
|
|
2812
2823
|
Example:
|
|
2813
|
-
specsmith serve --port 8421 --provider ollama --model qwen2.5:14b
|
|
2824
|
+
specsmith serve --port 8421 --provider ollama --model qwen2.5:14b \
|
|
2825
|
+
--auth-token $(specsmith auth get serve)
|
|
2814
2826
|
"""
|
|
2815
2827
|
from specsmith.serve import run_server
|
|
2816
2828
|
|
|
@@ -2820,6 +2832,7 @@ def serve_cmd(
|
|
|
2820
2832
|
model=model,
|
|
2821
2833
|
port=port,
|
|
2822
2834
|
host=host,
|
|
2835
|
+
auth_token=auth_token,
|
|
2823
2836
|
)
|
|
2824
2837
|
|
|
2825
2838
|
|
|
@@ -4444,9 +4457,28 @@ def info_cmd(as_json: bool, section: str) -> None:
|
|
|
4444
4457
|
# specsmith chat-export-block — self-contained block share (REQ-134)
|
|
4445
4458
|
# ---------------------------------------------------------------------------
|
|
4446
4459
|
#
|
|
4447
|
-
#
|
|
4448
|
-
#
|
|
4449
|
-
#
|
|
4460
|
+
# Top-level alias kept for back-compat with v0.6.x which only exposed
|
|
4461
|
+
# ``specsmith chat-export-block``. The canonical 1.0 spelling is
|
|
4462
|
+
# ``specsmith chat export-block`` under the chat group below.
|
|
4463
|
+
|
|
4464
|
+
|
|
4465
|
+
def _do_chat_export_block(project_dir: str, session_id: str, block_id: str, fmt: str) -> None:
|
|
4466
|
+
from specsmith.block_export import export_block
|
|
4467
|
+
|
|
4468
|
+
try:
|
|
4469
|
+
out = export_block(
|
|
4470
|
+
Path(project_dir).resolve(),
|
|
4471
|
+
session_id,
|
|
4472
|
+
block_id,
|
|
4473
|
+
fmt=fmt,
|
|
4474
|
+
)
|
|
4475
|
+
except FileNotFoundError as exc:
|
|
4476
|
+
console.print(f"[red]{exc}[/red]")
|
|
4477
|
+
raise SystemExit(1) from exc
|
|
4478
|
+
except KeyError as exc:
|
|
4479
|
+
console.print(f"[red]{exc}[/red]")
|
|
4480
|
+
raise SystemExit(1) from exc
|
|
4481
|
+
click.echo(out)
|
|
4450
4482
|
|
|
4451
4483
|
|
|
4452
4484
|
@main.command(name="chat-export-block")
|
|
@@ -4460,23 +4492,156 @@ def info_cmd(as_json: bool, section: str) -> None:
|
|
|
4460
4492
|
default="md",
|
|
4461
4493
|
)
|
|
4462
4494
|
def chat_export_block_cmd(project_dir: str, session_id: str, block_id: str, fmt: str) -> None:
|
|
4463
|
-
"""Export one chat block as a self-contained snippet (REQ-134)."""
|
|
4464
|
-
|
|
4495
|
+
"""Export one chat block as a self-contained snippet (REQ-134, top-level alias)."""
|
|
4496
|
+
_do_chat_export_block(project_dir, session_id, block_id, fmt)
|
|
4497
|
+
|
|
4498
|
+
|
|
4499
|
+
# ---------------------------------------------------------------------------
|
|
4500
|
+
# specsmith voice transcribe — wav/flac transcription via whisper-cpp (REQ-141)
|
|
4501
|
+
# ---------------------------------------------------------------------------
|
|
4502
|
+
|
|
4503
|
+
|
|
4504
|
+
@main.group(name="voice")
|
|
4505
|
+
def voice_group() -> None:
|
|
4506
|
+
"""Voice agent input (REQ-141). Requires the ``[voice]`` extra."""
|
|
4507
|
+
|
|
4508
|
+
|
|
4509
|
+
@voice_group.command(name="transcribe")
|
|
4510
|
+
@click.argument("audio_path", type=click.Path(exists=True))
|
|
4511
|
+
@click.option(
|
|
4512
|
+
"--json",
|
|
4513
|
+
"as_json",
|
|
4514
|
+
is_flag=True,
|
|
4515
|
+
default=False,
|
|
4516
|
+
help="Emit the full transcription record as JSON.",
|
|
4517
|
+
)
|
|
4518
|
+
def voice_transcribe_cmd(audio_path: str, as_json: bool) -> None:
|
|
4519
|
+
"""Transcribe AUDIO_PATH to text using whisper-cpp.
|
|
4520
|
+
|
|
4521
|
+
Three resolution modes:
|
|
4522
|
+
|
|
4523
|
+
\b
|
|
4524
|
+
* SPECSMITH_VOICE_STUB=<text> — returns the literal text (used by tests)
|
|
4525
|
+
* whisper-cpp installed + model present — real transcription
|
|
4526
|
+
* neither — exits 2 with an actionable install hint
|
|
4527
|
+
"""
|
|
4528
|
+
import json as _json
|
|
4529
|
+
|
|
4530
|
+
from specsmith.agent.voice import VoiceUnavailableError, transcribe
|
|
4465
4531
|
|
|
4466
4532
|
try:
|
|
4467
|
-
|
|
4468
|
-
Path(project_dir).resolve(),
|
|
4469
|
-
session_id,
|
|
4470
|
-
block_id,
|
|
4471
|
-
fmt=fmt,
|
|
4472
|
-
)
|
|
4533
|
+
result = transcribe(Path(audio_path))
|
|
4473
4534
|
except FileNotFoundError as exc:
|
|
4474
4535
|
console.print(f"[red]{exc}[/red]")
|
|
4475
4536
|
raise SystemExit(1) from exc
|
|
4476
|
-
except
|
|
4537
|
+
except VoiceUnavailableError as exc:
|
|
4477
4538
|
console.print(f"[red]{exc}[/red]")
|
|
4539
|
+
raise SystemExit(2) from exc
|
|
4540
|
+
|
|
4541
|
+
if as_json:
|
|
4542
|
+
click.echo(_json.dumps(result.to_dict(), indent=2))
|
|
4543
|
+
else:
|
|
4544
|
+
click.echo(result.text)
|
|
4545
|
+
|
|
4546
|
+
|
|
4547
|
+
@voice_group.command(name="status")
|
|
4548
|
+
def voice_status_cmd() -> None:
|
|
4549
|
+
"""Report whether voice transcription is available right now."""
|
|
4550
|
+
from specsmith.agent.voice import default_model_dir, is_available
|
|
4551
|
+
|
|
4552
|
+
if is_available():
|
|
4553
|
+
console.print("[green]\u2713[/green] voice available")
|
|
4554
|
+
console.print(f" model dir: {default_model_dir()}")
|
|
4555
|
+
else:
|
|
4556
|
+
console.print("[yellow]\u2014[/yellow] voice unavailable")
|
|
4557
|
+
console.print(
|
|
4558
|
+
" Install: [bold]pipx inject specsmith whisper-cpp-python[/bold] "
|
|
4559
|
+
"and place a model under ~/.specsmith/voice/."
|
|
4560
|
+
)
|
|
4561
|
+
raise SystemExit(2)
|
|
4562
|
+
|
|
4563
|
+
|
|
4564
|
+
# ---------------------------------------------------------------------------
|
|
4565
|
+
# specsmith cloud spawn — client side of the receiver (REQ-136)
|
|
4566
|
+
# ---------------------------------------------------------------------------
|
|
4567
|
+
|
|
4568
|
+
|
|
4569
|
+
@main.group(name="cloud")
|
|
4570
|
+
def cloud_group() -> None:
|
|
4571
|
+
"""Cloud-agent receiver client (REQ-136)."""
|
|
4572
|
+
|
|
4573
|
+
|
|
4574
|
+
@cloud_group.command(name="spawn")
|
|
4575
|
+
@click.argument("manifest_path", type=click.Path(exists=True))
|
|
4576
|
+
@click.option(
|
|
4577
|
+
"--endpoint",
|
|
4578
|
+
default="http://127.0.0.1:9000",
|
|
4579
|
+
help="Cloud-serve base URL (default: http://127.0.0.1:9000).",
|
|
4580
|
+
)
|
|
4581
|
+
@click.option("--token", default="", help="Bearer token for the receiver.")
|
|
4582
|
+
@click.option(
|
|
4583
|
+
"--dry-run",
|
|
4584
|
+
is_flag=True,
|
|
4585
|
+
default=False,
|
|
4586
|
+
help="Validate the manifest locally and print what would be posted.",
|
|
4587
|
+
)
|
|
4588
|
+
def cloud_spawn_cmd(manifest_path: str, endpoint: str, token: str, dry_run: bool) -> None:
|
|
4589
|
+
"""Post a manifest to a `specsmith cloud-serve` endpoint (REQ-136).
|
|
4590
|
+
|
|
4591
|
+
The MANIFEST_PATH is a YAML or JSON file describing the run. The CLI
|
|
4592
|
+
reads it, posts it to ``<endpoint>/spawn`` with optional bearer auth,
|
|
4593
|
+
and prints the response as JSON.
|
|
4594
|
+
"""
|
|
4595
|
+
import json as _json
|
|
4596
|
+
import urllib.error
|
|
4597
|
+
import urllib.request
|
|
4598
|
+
|
|
4599
|
+
raw = Path(manifest_path).read_text(encoding="utf-8")
|
|
4600
|
+
payload: dict[str, object]
|
|
4601
|
+
if manifest_path.endswith((".yml", ".yaml")):
|
|
4602
|
+
try:
|
|
4603
|
+
import yaml as _yaml
|
|
4604
|
+
|
|
4605
|
+
payload = _yaml.safe_load(raw) or {}
|
|
4606
|
+
except Exception as exc: # noqa: BLE001
|
|
4607
|
+
console.print(f"[red]Invalid YAML manifest: {exc}[/red]")
|
|
4608
|
+
raise SystemExit(2) from exc
|
|
4609
|
+
else:
|
|
4610
|
+
try:
|
|
4611
|
+
payload = _json.loads(raw)
|
|
4612
|
+
except ValueError as exc:
|
|
4613
|
+
console.print(f"[red]Invalid JSON manifest: {exc}[/red]")
|
|
4614
|
+
raise SystemExit(2) from exc
|
|
4615
|
+
|
|
4616
|
+
if not isinstance(payload, dict):
|
|
4617
|
+
console.print("[red]Manifest must be a mapping (YAML/JSON object).[/red]")
|
|
4618
|
+
raise SystemExit(2)
|
|
4619
|
+
|
|
4620
|
+
if dry_run:
|
|
4621
|
+
click.echo(_json.dumps({"endpoint": endpoint, "manifest": payload}, indent=2))
|
|
4622
|
+
return
|
|
4623
|
+
|
|
4624
|
+
body = _json.dumps(payload).encode("utf-8")
|
|
4625
|
+
req = urllib.request.Request( # noqa: S310 - user-supplied endpoint
|
|
4626
|
+
endpoint.rstrip("/") + "/spawn",
|
|
4627
|
+
data=body,
|
|
4628
|
+
method="POST",
|
|
4629
|
+
headers={"Content-Type": "application/json"},
|
|
4630
|
+
)
|
|
4631
|
+
if token:
|
|
4632
|
+
req.add_header("Authorization", f"Bearer {token}")
|
|
4633
|
+
try:
|
|
4634
|
+
with urllib.request.urlopen(req, timeout=30) as resp: # noqa: S310
|
|
4635
|
+
response = _json.loads(resp.read().decode("utf-8"))
|
|
4636
|
+
except urllib.error.HTTPError as exc:
|
|
4637
|
+
body_text = exc.read().decode("utf-8") or "{}"
|
|
4638
|
+
console.print(f"[red]HTTP {exc.code}[/red]: {body_text}")
|
|
4639
|
+
raise SystemExit(1) from exc
|
|
4640
|
+
except urllib.error.URLError as exc:
|
|
4641
|
+
console.print(f"[red]Network error[/red]: {exc.reason}")
|
|
4478
4642
|
raise SystemExit(1) from exc
|
|
4479
|
-
|
|
4643
|
+
|
|
4644
|
+
click.echo(_json.dumps(response, indent=2))
|
|
4480
4645
|
|
|
4481
4646
|
|
|
4482
4647
|
# ---------------------------------------------------------------------------
|
|
@@ -5808,93 +5973,13 @@ main.add_command(notebook_group)
|
|
|
5808
5973
|
|
|
5809
5974
|
|
|
5810
5975
|
# ---------------------------------------------------------------------------
|
|
5811
|
-
# Cloud — spawn
|
|
5976
|
+
# Cloud — REQ-126 placeholder (cloud spawn lives above under REQ-136).
|
|
5812
5977
|
# ---------------------------------------------------------------------------
|
|
5813
|
-
|
|
5814
|
-
|
|
5815
|
-
|
|
5816
|
-
|
|
5817
|
-
|
|
5818
|
-
|
|
5819
|
-
|
|
5820
|
-
@cloud_group.command(name="spawn")
|
|
5821
|
-
@click.argument("utterance")
|
|
5822
|
-
@click.option("--project-dir", type=click.Path(exists=True), default=".")
|
|
5823
|
-
@click.option(
|
|
5824
|
-
"--endpoint",
|
|
5825
|
-
default="",
|
|
5826
|
-
help="Cloud endpoint URL (default: SPECSMITH_CLOUD_ENDPOINT env var).",
|
|
5827
|
-
)
|
|
5828
|
-
@click.option(
|
|
5829
|
-
"--dry-run",
|
|
5830
|
-
is_flag=True,
|
|
5831
|
-
default=False,
|
|
5832
|
-
help="Build the tarball + manifest but do not POST.",
|
|
5833
|
-
)
|
|
5834
|
-
def cloud_spawn(utterance: str, project_dir: str, endpoint: str, dry_run: bool) -> None:
|
|
5835
|
-
"""Tarball the working tree and POST to a cloud agent endpoint (REQ-126).
|
|
5836
|
-
|
|
5837
|
-
Always emits a manifest at `.specsmith/cloud/<run_id>/manifest.json`
|
|
5838
|
-
so the operation is auditable. When the endpoint is reachable the
|
|
5839
|
-
response stream is tailed to stdout as JSONL events. Without an
|
|
5840
|
-
endpoint, the command stays local: the manifest is recorded and a
|
|
5841
|
-
helpful error is printed.
|
|
5842
|
-
"""
|
|
5843
|
-
import json as _json
|
|
5844
|
-
import os
|
|
5845
|
-
import tarfile
|
|
5846
|
-
import uuid as _uuid
|
|
5847
|
-
from urllib import error as _urlerror
|
|
5848
|
-
from urllib import request as _urlreq
|
|
5849
|
-
|
|
5850
|
-
root = Path(project_dir).resolve()
|
|
5851
|
-
run_id = f"cloud_{_uuid.uuid4().hex[:12]}"
|
|
5852
|
-
run_dir = root / ".specsmith" / "cloud" / run_id
|
|
5853
|
-
run_dir.mkdir(parents=True, exist_ok=True)
|
|
5854
|
-
|
|
5855
|
-
tar_path = run_dir / "workspace.tar.gz"
|
|
5856
|
-
skip = {".git", ".venv", "__pycache__", ".specsmith", "node_modules", "dist", "build"}
|
|
5857
|
-
with tarfile.open(tar_path, "w:gz") as tar:
|
|
5858
|
-
for item in sorted(root.iterdir()):
|
|
5859
|
-
if item.name in skip:
|
|
5860
|
-
continue
|
|
5861
|
-
tar.add(item, arcname=item.name)
|
|
5862
|
-
|
|
5863
|
-
manifest = {
|
|
5864
|
-
"run_id": run_id,
|
|
5865
|
-
"utterance": utterance,
|
|
5866
|
-
"workspace": str(tar_path.relative_to(root)),
|
|
5867
|
-
"endpoint": endpoint or os.environ.get("SPECSMITH_CLOUD_ENDPOINT", ""),
|
|
5868
|
-
"dry_run": dry_run,
|
|
5869
|
-
}
|
|
5870
|
-
(run_dir / "manifest.json").write_text(_json.dumps(manifest, indent=2), encoding="utf-8")
|
|
5871
|
-
|
|
5872
|
-
if dry_run or not manifest["endpoint"]:
|
|
5873
|
-
console.print(
|
|
5874
|
-
f"[yellow]Cloud endpoint not configured.[/yellow] Manifest written to {run_dir}."
|
|
5875
|
-
)
|
|
5876
|
-
console.print(
|
|
5877
|
-
" Set [bold]SPECSMITH_CLOUD_ENDPOINT[/bold] or pass [bold]--endpoint[/bold] to POST."
|
|
5878
|
-
)
|
|
5879
|
-
return
|
|
5880
|
-
|
|
5881
|
-
body = _json.dumps(manifest).encode("utf-8")
|
|
5882
|
-
req = _urlreq.Request(
|
|
5883
|
-
manifest["endpoint"],
|
|
5884
|
-
data=body,
|
|
5885
|
-
headers={"Content-Type": "application/json"},
|
|
5886
|
-
method="POST",
|
|
5887
|
-
)
|
|
5888
|
-
try:
|
|
5889
|
-
with _urlreq.urlopen(req, timeout=30) as resp: # noqa: S310 - user-configured
|
|
5890
|
-
for line in resp:
|
|
5891
|
-
click.echo(line.decode("utf-8", errors="replace").rstrip())
|
|
5892
|
-
except (_urlerror.URLError, OSError) as exc:
|
|
5893
|
-
console.print(f"[red]Cloud spawn failed:[/red] {exc}")
|
|
5894
|
-
raise SystemExit(1) from None
|
|
5895
|
-
|
|
5896
|
-
|
|
5897
|
-
main.add_command(cloud_group)
|
|
5978
|
+
# The original REQ-126 stub built a workspace tarball and posted to a free-
|
|
5979
|
+
# form endpoint with no auth. REQ-136 supersedes it with a manifest-based
|
|
5980
|
+
# command that posts to ``<endpoint>/spawn`` with optional bearer auth.
|
|
5981
|
+
# Keeping a single ``cloud spawn`` avoids surface drift; see
|
|
5982
|
+
# tests/test_warp_parity_followup.py for coverage.
|
|
5898
5983
|
|
|
5899
5984
|
|
|
5900
5985
|
# ---------------------------------------------------------------------------
|
|
@@ -153,11 +153,29 @@ class _Handler(BaseHTTPRequestHandler):
|
|
|
153
153
|
|
|
154
154
|
bus: _EventBus
|
|
155
155
|
agent: _AgentThread
|
|
156
|
+
auth_token: str = "" # populated by run_server / make_server when set
|
|
156
157
|
|
|
157
158
|
def log_message(self, format: str, *args: Any) -> None: # noqa: A002
|
|
158
159
|
"""Suppress default stderr logging."""
|
|
159
160
|
|
|
161
|
+
# ── Auth ─────────────────────────────────────────────────────────
|
|
162
|
+
# REQ-137: when run_server is started with --auth-token, every
|
|
163
|
+
# request must present `Authorization: Bearer <token>`. /api/health
|
|
164
|
+
# is the only unauthenticated endpoint so liveness probes still
|
|
165
|
+
# work behind a load balancer that strips Authorization.
|
|
166
|
+
def _authorize(self) -> bool:
|
|
167
|
+
token = type(self).auth_token
|
|
168
|
+
if not token:
|
|
169
|
+
return True
|
|
170
|
+
if self.path == "/api/health":
|
|
171
|
+
return True
|
|
172
|
+
header = self.headers.get("Authorization", "")
|
|
173
|
+
return header == f"Bearer {token}"
|
|
174
|
+
|
|
160
175
|
def do_GET(self) -> None: # noqa: N802
|
|
176
|
+
if not self._authorize():
|
|
177
|
+
self._json_response({"error": "unauthorized"}, code=401)
|
|
178
|
+
return
|
|
161
179
|
if self.path == "/api/events":
|
|
162
180
|
self._sse()
|
|
163
181
|
elif self.path == "/api/status":
|
|
@@ -168,6 +186,9 @@ class _Handler(BaseHTTPRequestHandler):
|
|
|
168
186
|
self.send_error(404)
|
|
169
187
|
|
|
170
188
|
def do_POST(self) -> None: # noqa: N802
|
|
189
|
+
if not self._authorize():
|
|
190
|
+
self._json_response({"error": "unauthorized"}, code=401)
|
|
191
|
+
return
|
|
171
192
|
if self.path == "/api/send":
|
|
172
193
|
body = self._read_json()
|
|
173
194
|
text = body.get("text", "").strip() if body else ""
|
|
@@ -183,6 +204,9 @@ class _Handler(BaseHTTPRequestHandler):
|
|
|
183
204
|
self.send_error(404)
|
|
184
205
|
|
|
185
206
|
def do_DELETE(self) -> None: # noqa: N802
|
|
207
|
+
if not self._authorize():
|
|
208
|
+
self._json_response({"error": "unauthorized"}, code=401)
|
|
209
|
+
return
|
|
186
210
|
if self.path == "/api/session":
|
|
187
211
|
self.agent.send(None) # type: ignore[arg-type]
|
|
188
212
|
self._json_response({"ok": True, "message": "session ending"})
|
|
@@ -246,38 +270,68 @@ class _Handler(BaseHTTPRequestHandler):
|
|
|
246
270
|
self.wfile.write(body)
|
|
247
271
|
|
|
248
272
|
|
|
249
|
-
def
|
|
273
|
+
def make_server(
|
|
250
274
|
*,
|
|
251
275
|
project_dir: str = ".",
|
|
252
276
|
provider: str = "ollama",
|
|
253
277
|
model: str = "",
|
|
254
278
|
port: int = 8421,
|
|
255
279
|
host: str = "127.0.0.1",
|
|
256
|
-
|
|
257
|
-
|
|
280
|
+
auth_token: str = "",
|
|
281
|
+
) -> tuple[_ThreadedHTTPServer, _AgentThread]:
|
|
282
|
+
"""Build the HTTP server + agent thread without serving yet.
|
|
283
|
+
|
|
284
|
+
Used by tests so they can drive a fresh server inside the same
|
|
285
|
+
process. Production callers go through ``run_server`` which adds
|
|
286
|
+
the banner + serve_forever loop.
|
|
287
|
+
"""
|
|
258
288
|
project_dir = str(Path(project_dir).resolve())
|
|
259
289
|
bus = _EventBus()
|
|
260
290
|
agent = _AgentThread(project_dir, provider, model, bus)
|
|
261
291
|
|
|
262
|
-
# Subclass to carry shared state into the handler
|
|
263
292
|
class Handler(_Handler):
|
|
264
293
|
pass
|
|
265
294
|
|
|
266
295
|
Handler.bus = bus
|
|
267
296
|
Handler.agent = agent
|
|
297
|
+
Handler.auth_token = auth_token
|
|
268
298
|
|
|
269
299
|
server = _ThreadedHTTPServer((host, port), Handler)
|
|
300
|
+
return server, agent
|
|
301
|
+
|
|
302
|
+
|
|
303
|
+
def run_server(
|
|
304
|
+
*,
|
|
305
|
+
project_dir: str = ".",
|
|
306
|
+
provider: str = "ollama",
|
|
307
|
+
model: str = "",
|
|
308
|
+
port: int = 8421,
|
|
309
|
+
host: str = "127.0.0.1",
|
|
310
|
+
auth_token: str = "",
|
|
311
|
+
) -> None:
|
|
312
|
+
"""Start the specsmith HTTP server."""
|
|
313
|
+
server, agent = make_server(
|
|
314
|
+
project_dir=project_dir,
|
|
315
|
+
provider=provider,
|
|
316
|
+
model=model,
|
|
317
|
+
port=port,
|
|
318
|
+
host=host,
|
|
319
|
+
auth_token=auth_token,
|
|
320
|
+
)
|
|
270
321
|
agent.start()
|
|
271
322
|
|
|
323
|
+
auth_note = " Auth: bearer-token required\n" if auth_token else ""
|
|
272
324
|
print( # noqa: T201
|
|
273
325
|
f"specsmith serve — http://{host}:{port}\n"
|
|
274
326
|
f" Project: {project_dir}\n"
|
|
275
327
|
f" Provider: {provider}/{model or '(default)'}\n"
|
|
328
|
+
f"{auth_note}"
|
|
276
329
|
f" Endpoints:\n"
|
|
277
330
|
f" GET /api/events — SSE event stream\n"
|
|
278
331
|
f" POST /api/send — send a message\n"
|
|
279
332
|
f" GET /api/status — session status\n"
|
|
280
333
|
f" POST /api/stop — stop current turn\n"
|
|
334
|
+
f" GET /api/health — unauthenticated liveness\n"
|
|
281
335
|
f" Press Ctrl+C to stop.\n",
|
|
282
336
|
file=sys.stderr,
|
|
283
337
|
)
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: specsmith
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.7.0
|
|
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
|
|
@@ -77,6 +77,7 @@ src/specsmith/agent/safety.py
|
|
|
77
77
|
src/specsmith/agent/suggester.py
|
|
78
78
|
src/specsmith/agent/tools.py
|
|
79
79
|
src/specsmith/agent/verifier.py
|
|
80
|
+
src/specsmith/agent/voice.py
|
|
80
81
|
src/specsmith/commands/__init__.py
|
|
81
82
|
src/specsmith/epistemic/__init__.py
|
|
82
83
|
src/specsmith/epistemic/belief.py
|
|
@@ -179,4 +180,5 @@ tests/test_suggester.py
|
|
|
179
180
|
tests/test_tools.py
|
|
180
181
|
tests/test_validator.py
|
|
181
182
|
tests/test_vcs.py
|
|
182
|
-
tests/test_warp_parity.py
|
|
183
|
+
tests/test_warp_parity.py
|
|
184
|
+
tests/test_warp_parity_followup.py
|