specsmith 0.6.0.dev232__tar.gz → 0.6.0.dev233__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.dev232/src/specsmith.egg-info → specsmith-0.6.0.dev233}/PKG-INFO +6 -1
- {specsmith-0.6.0.dev232 → specsmith-0.6.0.dev233}/pyproject.toml +12 -1
- specsmith-0.6.0.dev233/src/specsmith/block_export.py +106 -0
- {specsmith-0.6.0.dev232 → specsmith-0.6.0.dev233}/src/specsmith/cli.py +123 -0
- specsmith-0.6.0.dev233/src/specsmith/cloud_serve.py +150 -0
- specsmith-0.6.0.dev233/src/specsmith/drive.py +126 -0
- specsmith-0.6.0.dev233/src/specsmith/history_search.py +159 -0
- {specsmith-0.6.0.dev232 → specsmith-0.6.0.dev233/src/specsmith.egg-info}/PKG-INFO +6 -1
- {specsmith-0.6.0.dev232 → specsmith-0.6.0.dev233}/src/specsmith.egg-info/SOURCES.txt +6 -1
- {specsmith-0.6.0.dev232 → specsmith-0.6.0.dev233}/src/specsmith.egg-info/requires.txt +7 -0
- specsmith-0.6.0.dev233/tests/test_warp_parity.py +421 -0
- {specsmith-0.6.0.dev232 → specsmith-0.6.0.dev233}/LICENSE +0 -0
- {specsmith-0.6.0.dev232 → specsmith-0.6.0.dev233}/README.md +0 -0
- {specsmith-0.6.0.dev232 → specsmith-0.6.0.dev233}/setup.cfg +0 -0
- {specsmith-0.6.0.dev232 → specsmith-0.6.0.dev233}/src/epistemic/__init__.py +0 -0
- {specsmith-0.6.0.dev232 → specsmith-0.6.0.dev233}/src/epistemic/belief.py +0 -0
- {specsmith-0.6.0.dev232 → specsmith-0.6.0.dev233}/src/epistemic/certainty.py +0 -0
- {specsmith-0.6.0.dev232 → specsmith-0.6.0.dev233}/src/epistemic/failure_graph.py +0 -0
- {specsmith-0.6.0.dev232 → specsmith-0.6.0.dev233}/src/epistemic/py.typed +0 -0
- {specsmith-0.6.0.dev232 → specsmith-0.6.0.dev233}/src/epistemic/recovery.py +0 -0
- {specsmith-0.6.0.dev232 → specsmith-0.6.0.dev233}/src/epistemic/session.py +0 -0
- {specsmith-0.6.0.dev232 → specsmith-0.6.0.dev233}/src/epistemic/stress_tester.py +0 -0
- {specsmith-0.6.0.dev232 → specsmith-0.6.0.dev233}/src/epistemic/trace.py +0 -0
- {specsmith-0.6.0.dev232 → specsmith-0.6.0.dev233}/src/specsmith/__init__.py +0 -0
- {specsmith-0.6.0.dev232 → specsmith-0.6.0.dev233}/src/specsmith/__main__.py +0 -0
- {specsmith-0.6.0.dev232 → specsmith-0.6.0.dev233}/src/specsmith/agent/__init__.py +0 -0
- {specsmith-0.6.0.dev232 → specsmith-0.6.0.dev233}/src/specsmith/agent/broker.py +0 -0
- {specsmith-0.6.0.dev232 → specsmith-0.6.0.dev233}/src/specsmith/agent/chat_runner.py +0 -0
- {specsmith-0.6.0.dev232 → specsmith-0.6.0.dev233}/src/specsmith/agent/cleanup.py +0 -0
- {specsmith-0.6.0.dev232 → specsmith-0.6.0.dev233}/src/specsmith/agent/events.py +0 -0
- {specsmith-0.6.0.dev232 → specsmith-0.6.0.dev233}/src/specsmith/agent/indexer.py +0 -0
- {specsmith-0.6.0.dev232 → specsmith-0.6.0.dev233}/src/specsmith/agent/mcp.py +0 -0
- {specsmith-0.6.0.dev232 → specsmith-0.6.0.dev233}/src/specsmith/agent/memory.py +0 -0
- {specsmith-0.6.0.dev232 → specsmith-0.6.0.dev233}/src/specsmith/agent/orchestrator.py +0 -0
- {specsmith-0.6.0.dev232 → specsmith-0.6.0.dev233}/src/specsmith/agent/repl.py +0 -0
- {specsmith-0.6.0.dev232 → specsmith-0.6.0.dev233}/src/specsmith/agent/router.py +0 -0
- {specsmith-0.6.0.dev232 → specsmith-0.6.0.dev233}/src/specsmith/agent/rules.py +0 -0
- {specsmith-0.6.0.dev232 → specsmith-0.6.0.dev233}/src/specsmith/agent/safety.py +0 -0
- {specsmith-0.6.0.dev232 → specsmith-0.6.0.dev233}/src/specsmith/agent/suggester.py +0 -0
- {specsmith-0.6.0.dev232 → specsmith-0.6.0.dev233}/src/specsmith/agent/tools.py +0 -0
- {specsmith-0.6.0.dev232 → specsmith-0.6.0.dev233}/src/specsmith/agent/verifier.py +0 -0
- {specsmith-0.6.0.dev232 → specsmith-0.6.0.dev233}/src/specsmith/architect.py +0 -0
- {specsmith-0.6.0.dev232 → specsmith-0.6.0.dev233}/src/specsmith/auditor.py +0 -0
- {specsmith-0.6.0.dev232 → specsmith-0.6.0.dev233}/src/specsmith/auth.py +0 -0
- {specsmith-0.6.0.dev232 → specsmith-0.6.0.dev233}/src/specsmith/commands/__init__.py +0 -0
- {specsmith-0.6.0.dev232 → specsmith-0.6.0.dev233}/src/specsmith/compressor.py +0 -0
- {specsmith-0.6.0.dev232 → specsmith-0.6.0.dev233}/src/specsmith/config.py +0 -0
- {specsmith-0.6.0.dev232 → specsmith-0.6.0.dev233}/src/specsmith/console_utils.py +0 -0
- {specsmith-0.6.0.dev232 → specsmith-0.6.0.dev233}/src/specsmith/credit_analyzer.py +0 -0
- {specsmith-0.6.0.dev232 → specsmith-0.6.0.dev233}/src/specsmith/credits.py +0 -0
- {specsmith-0.6.0.dev232 → specsmith-0.6.0.dev233}/src/specsmith/differ.py +0 -0
- {specsmith-0.6.0.dev232 → specsmith-0.6.0.dev233}/src/specsmith/doctor.py +0 -0
- {specsmith-0.6.0.dev232 → specsmith-0.6.0.dev233}/src/specsmith/epistemic/__init__.py +0 -0
- {specsmith-0.6.0.dev232 → specsmith-0.6.0.dev233}/src/specsmith/epistemic/belief.py +0 -0
- {specsmith-0.6.0.dev232 → specsmith-0.6.0.dev233}/src/specsmith/epistemic/certainty.py +0 -0
- {specsmith-0.6.0.dev232 → specsmith-0.6.0.dev233}/src/specsmith/epistemic/failure_graph.py +0 -0
- {specsmith-0.6.0.dev232 → specsmith-0.6.0.dev233}/src/specsmith/epistemic/recovery.py +0 -0
- {specsmith-0.6.0.dev232 → specsmith-0.6.0.dev233}/src/specsmith/epistemic/stress_tester.py +0 -0
- {specsmith-0.6.0.dev232 → specsmith-0.6.0.dev233}/src/specsmith/executor.py +0 -0
- {specsmith-0.6.0.dev232 → specsmith-0.6.0.dev233}/src/specsmith/exporter.py +0 -0
- {specsmith-0.6.0.dev232 → specsmith-0.6.0.dev233}/src/specsmith/gui/__init__.py +0 -0
- {specsmith-0.6.0.dev232 → specsmith-0.6.0.dev233}/src/specsmith/gui/app.py +0 -0
- {specsmith-0.6.0.dev232 → specsmith-0.6.0.dev233}/src/specsmith/gui/main_window.py +0 -0
- {specsmith-0.6.0.dev232 → specsmith-0.6.0.dev233}/src/specsmith/gui/session_tab.py +0 -0
- {specsmith-0.6.0.dev232 → specsmith-0.6.0.dev233}/src/specsmith/gui/theme.py +0 -0
- {specsmith-0.6.0.dev232 → specsmith-0.6.0.dev233}/src/specsmith/gui/widgets/__init__.py +0 -0
- {specsmith-0.6.0.dev232 → specsmith-0.6.0.dev233}/src/specsmith/gui/widgets/chat_view.py +0 -0
- {specsmith-0.6.0.dev232 → specsmith-0.6.0.dev233}/src/specsmith/gui/widgets/input_bar.py +0 -0
- {specsmith-0.6.0.dev232 → specsmith-0.6.0.dev233}/src/specsmith/gui/widgets/provider_bar.py +0 -0
- {specsmith-0.6.0.dev232 → specsmith-0.6.0.dev233}/src/specsmith/gui/widgets/token_meter.py +0 -0
- {specsmith-0.6.0.dev232 → specsmith-0.6.0.dev233}/src/specsmith/gui/widgets/tool_panel.py +0 -0
- {specsmith-0.6.0.dev232 → specsmith-0.6.0.dev233}/src/specsmith/gui/widgets/update_checker.py +0 -0
- {specsmith-0.6.0.dev232 → specsmith-0.6.0.dev233}/src/specsmith/gui/worker.py +0 -0
- {specsmith-0.6.0.dev232 → specsmith-0.6.0.dev233}/src/specsmith/importer.py +0 -0
- {specsmith-0.6.0.dev232 → specsmith-0.6.0.dev233}/src/specsmith/integrations/__init__.py +0 -0
- {specsmith-0.6.0.dev232 → specsmith-0.6.0.dev233}/src/specsmith/integrations/agent_skill.py +0 -0
- {specsmith-0.6.0.dev232 → specsmith-0.6.0.dev233}/src/specsmith/integrations/aider.py +0 -0
- {specsmith-0.6.0.dev232 → specsmith-0.6.0.dev233}/src/specsmith/integrations/base.py +0 -0
- {specsmith-0.6.0.dev232 → specsmith-0.6.0.dev233}/src/specsmith/integrations/claude_code.py +0 -0
- {specsmith-0.6.0.dev232 → specsmith-0.6.0.dev233}/src/specsmith/integrations/copilot.py +0 -0
- {specsmith-0.6.0.dev232 → specsmith-0.6.0.dev233}/src/specsmith/integrations/cursor.py +0 -0
- {specsmith-0.6.0.dev232 → specsmith-0.6.0.dev233}/src/specsmith/integrations/gemini.py +0 -0
- {specsmith-0.6.0.dev232 → specsmith-0.6.0.dev233}/src/specsmith/integrations/windsurf.py +0 -0
- {specsmith-0.6.0.dev232 → specsmith-0.6.0.dev233}/src/specsmith/languages.py +0 -0
- {specsmith-0.6.0.dev232 → specsmith-0.6.0.dev233}/src/specsmith/ledger.py +0 -0
- {specsmith-0.6.0.dev232 → specsmith-0.6.0.dev233}/src/specsmith/patent.py +0 -0
- {specsmith-0.6.0.dev232 → specsmith-0.6.0.dev233}/src/specsmith/phase.py +0 -0
- {specsmith-0.6.0.dev232 → specsmith-0.6.0.dev233}/src/specsmith/plugins.py +0 -0
- {specsmith-0.6.0.dev232 → specsmith-0.6.0.dev233}/src/specsmith/profiles.py +0 -0
- {specsmith-0.6.0.dev232 → specsmith-0.6.0.dev233}/src/specsmith/rate_limits.py +0 -0
- {specsmith-0.6.0.dev232 → specsmith-0.6.0.dev233}/src/specsmith/releaser.py +0 -0
- {specsmith-0.6.0.dev232 → specsmith-0.6.0.dev233}/src/specsmith/requirements.py +0 -0
- {specsmith-0.6.0.dev232 → specsmith-0.6.0.dev233}/src/specsmith/requirements_parser.py +0 -0
- {specsmith-0.6.0.dev232 → specsmith-0.6.0.dev233}/src/specsmith/retrieval.py +0 -0
- {specsmith-0.6.0.dev232 → specsmith-0.6.0.dev233}/src/specsmith/scaffolder.py +0 -0
- {specsmith-0.6.0.dev232 → specsmith-0.6.0.dev233}/src/specsmith/serve.py +0 -0
- {specsmith-0.6.0.dev232 → specsmith-0.6.0.dev233}/src/specsmith/session.py +0 -0
- {specsmith-0.6.0.dev232 → specsmith-0.6.0.dev233}/src/specsmith/skills.py +0 -0
- {specsmith-0.6.0.dev232 → specsmith-0.6.0.dev233}/src/specsmith/templates/agents.md.j2 +0 -0
- {specsmith-0.6.0.dev232 → specsmith-0.6.0.dev233}/src/specsmith/templates/community/bug_report.md.j2 +0 -0
- {specsmith-0.6.0.dev232 → specsmith-0.6.0.dev233}/src/specsmith/templates/community/code_of_conduct.md.j2 +0 -0
- {specsmith-0.6.0.dev232 → specsmith-0.6.0.dev233}/src/specsmith/templates/community/contributing.md.j2 +0 -0
- {specsmith-0.6.0.dev232 → specsmith-0.6.0.dev233}/src/specsmith/templates/community/feature_request.md.j2 +0 -0
- {specsmith-0.6.0.dev232 → specsmith-0.6.0.dev233}/src/specsmith/templates/community/license-Apache-2.0.j2 +0 -0
- {specsmith-0.6.0.dev232 → specsmith-0.6.0.dev233}/src/specsmith/templates/community/license-MIT.j2 +0 -0
- {specsmith-0.6.0.dev232 → specsmith-0.6.0.dev233}/src/specsmith/templates/community/pull_request_template.md.j2 +0 -0
- {specsmith-0.6.0.dev232 → specsmith-0.6.0.dev233}/src/specsmith/templates/community/security.md.j2 +0 -0
- {specsmith-0.6.0.dev232 → specsmith-0.6.0.dev233}/src/specsmith/templates/docs/architecture.md.j2 +0 -0
- {specsmith-0.6.0.dev232 → specsmith-0.6.0.dev233}/src/specsmith/templates/docs/mkdocs.yml.j2 +0 -0
- {specsmith-0.6.0.dev232 → specsmith-0.6.0.dev233}/src/specsmith/templates/docs/readthedocs.yaml.j2 +0 -0
- {specsmith-0.6.0.dev232 → specsmith-0.6.0.dev233}/src/specsmith/templates/docs/requirements.md.j2 +0 -0
- {specsmith-0.6.0.dev232 → specsmith-0.6.0.dev233}/src/specsmith/templates/docs/test-spec.md.j2 +0 -0
- {specsmith-0.6.0.dev232 → specsmith-0.6.0.dev233}/src/specsmith/templates/editorconfig.j2 +0 -0
- {specsmith-0.6.0.dev232 → specsmith-0.6.0.dev233}/src/specsmith/templates/gitattributes.j2 +0 -0
- {specsmith-0.6.0.dev232 → specsmith-0.6.0.dev233}/src/specsmith/templates/gitignore.j2 +0 -0
- {specsmith-0.6.0.dev232 → specsmith-0.6.0.dev233}/src/specsmith/templates/go/go.mod.j2 +0 -0
- {specsmith-0.6.0.dev232 → specsmith-0.6.0.dev233}/src/specsmith/templates/go/main.go.j2 +0 -0
- {specsmith-0.6.0.dev232 → specsmith-0.6.0.dev233}/src/specsmith/templates/governance/belief-registry.md.j2 +0 -0
- {specsmith-0.6.0.dev232 → specsmith-0.6.0.dev233}/src/specsmith/templates/governance/context-budget.md.j2 +0 -0
- {specsmith-0.6.0.dev232 → specsmith-0.6.0.dev233}/src/specsmith/templates/governance/drift-metrics.md.j2 +0 -0
- {specsmith-0.6.0.dev232 → specsmith-0.6.0.dev233}/src/specsmith/templates/governance/epistemic-axioms.md.j2 +0 -0
- {specsmith-0.6.0.dev232 → specsmith-0.6.0.dev233}/src/specsmith/templates/governance/failure-modes.md.j2 +0 -0
- {specsmith-0.6.0.dev232 → specsmith-0.6.0.dev233}/src/specsmith/templates/governance/lifecycle.md.j2 +0 -0
- {specsmith-0.6.0.dev232 → specsmith-0.6.0.dev233}/src/specsmith/templates/governance/roles.md.j2 +0 -0
- {specsmith-0.6.0.dev232 → specsmith-0.6.0.dev233}/src/specsmith/templates/governance/rules.md.j2 +0 -0
- {specsmith-0.6.0.dev232 → specsmith-0.6.0.dev233}/src/specsmith/templates/governance/session-protocol.md.j2 +0 -0
- {specsmith-0.6.0.dev232 → specsmith-0.6.0.dev233}/src/specsmith/templates/governance/uncertainty-map.md.j2 +0 -0
- {specsmith-0.6.0.dev232 → specsmith-0.6.0.dev233}/src/specsmith/templates/governance/verification.md.j2 +0 -0
- {specsmith-0.6.0.dev232 → specsmith-0.6.0.dev233}/src/specsmith/templates/js/package.json.j2 +0 -0
- {specsmith-0.6.0.dev232 → specsmith-0.6.0.dev233}/src/specsmith/templates/ledger.md.j2 +0 -0
- {specsmith-0.6.0.dev232 → specsmith-0.6.0.dev233}/src/specsmith/templates/python/cli.py.j2 +0 -0
- {specsmith-0.6.0.dev232 → specsmith-0.6.0.dev233}/src/specsmith/templates/python/init.py.j2 +0 -0
- {specsmith-0.6.0.dev232 → specsmith-0.6.0.dev233}/src/specsmith/templates/python/pyproject.toml.j2 +0 -0
- {specsmith-0.6.0.dev232 → specsmith-0.6.0.dev233}/src/specsmith/templates/readme.md.j2 +0 -0
- {specsmith-0.6.0.dev232 → specsmith-0.6.0.dev233}/src/specsmith/templates/rust/Cargo.toml.j2 +0 -0
- {specsmith-0.6.0.dev232 → specsmith-0.6.0.dev233}/src/specsmith/templates/rust/main.rs.j2 +0 -0
- {specsmith-0.6.0.dev232 → specsmith-0.6.0.dev233}/src/specsmith/templates/scripts/exec.cmd.j2 +0 -0
- {specsmith-0.6.0.dev232 → specsmith-0.6.0.dev233}/src/specsmith/templates/scripts/exec.sh.j2 +0 -0
- {specsmith-0.6.0.dev232 → specsmith-0.6.0.dev233}/src/specsmith/templates/scripts/run.cmd.j2 +0 -0
- {specsmith-0.6.0.dev232 → specsmith-0.6.0.dev233}/src/specsmith/templates/scripts/run.sh.j2 +0 -0
- {specsmith-0.6.0.dev232 → specsmith-0.6.0.dev233}/src/specsmith/templates/scripts/setup.cmd.j2 +0 -0
- {specsmith-0.6.0.dev232 → specsmith-0.6.0.dev233}/src/specsmith/templates/scripts/setup.sh.j2 +0 -0
- {specsmith-0.6.0.dev232 → specsmith-0.6.0.dev233}/src/specsmith/templates/workflows/release.yml.j2 +0 -0
- {specsmith-0.6.0.dev232 → specsmith-0.6.0.dev233}/src/specsmith/tool_installer.py +0 -0
- {specsmith-0.6.0.dev232 → specsmith-0.6.0.dev233}/src/specsmith/toolrules.py +0 -0
- {specsmith-0.6.0.dev232 → specsmith-0.6.0.dev233}/src/specsmith/tools.py +0 -0
- {specsmith-0.6.0.dev232 → specsmith-0.6.0.dev233}/src/specsmith/trace.py +0 -0
- {specsmith-0.6.0.dev232 → specsmith-0.6.0.dev233}/src/specsmith/updater.py +0 -0
- {specsmith-0.6.0.dev232 → specsmith-0.6.0.dev233}/src/specsmith/upgrader.py +0 -0
- {specsmith-0.6.0.dev232 → specsmith-0.6.0.dev233}/src/specsmith/validator.py +0 -0
- {specsmith-0.6.0.dev232 → specsmith-0.6.0.dev233}/src/specsmith/vcs/__init__.py +0 -0
- {specsmith-0.6.0.dev232 → specsmith-0.6.0.dev233}/src/specsmith/vcs/base.py +0 -0
- {specsmith-0.6.0.dev232 → specsmith-0.6.0.dev233}/src/specsmith/vcs/bitbucket.py +0 -0
- {specsmith-0.6.0.dev232 → specsmith-0.6.0.dev233}/src/specsmith/vcs/github.py +0 -0
- {specsmith-0.6.0.dev232 → specsmith-0.6.0.dev233}/src/specsmith/vcs/gitlab.py +0 -0
- {specsmith-0.6.0.dev232 → specsmith-0.6.0.dev233}/src/specsmith/vcs_commands.py +0 -0
- {specsmith-0.6.0.dev232 → specsmith-0.6.0.dev233}/src/specsmith/wireframes.py +0 -0
- {specsmith-0.6.0.dev232 → specsmith-0.6.0.dev233}/src/specsmith/workspace.py +0 -0
- {specsmith-0.6.0.dev232 → specsmith-0.6.0.dev233}/src/specsmith.egg-info/dependency_links.txt +0 -0
- {specsmith-0.6.0.dev232 → specsmith-0.6.0.dev233}/src/specsmith.egg-info/entry_points.txt +0 -0
- {specsmith-0.6.0.dev232 → specsmith-0.6.0.dev233}/src/specsmith.egg-info/top_level.txt +0 -0
- {specsmith-0.6.0.dev232 → specsmith-0.6.0.dev233}/tests/test_CMD_001.py +0 -0
- {specsmith-0.6.0.dev232 → specsmith-0.6.0.dev233}/tests/test_auditor.py +0 -0
- {specsmith-0.6.0.dev232 → specsmith-0.6.0.dev233}/tests/test_chat_diff_decision.py +0 -0
- {specsmith-0.6.0.dev232 → specsmith-0.6.0.dev233}/tests/test_chat_stdin_protocol.py +0 -0
- {specsmith-0.6.0.dev232 → specsmith-0.6.0.dev233}/tests/test_cli.py +0 -0
- {specsmith-0.6.0.dev232 → specsmith-0.6.0.dev233}/tests/test_cli_workflows_history_drive.py +0 -0
- {specsmith-0.6.0.dev232 → specsmith-0.6.0.dev233}/tests/test_compressor.py +0 -0
- {specsmith-0.6.0.dev232 → specsmith-0.6.0.dev233}/tests/test_e2e_nexus.py +0 -0
- {specsmith-0.6.0.dev232 → specsmith-0.6.0.dev233}/tests/test_epistemic.py +0 -0
- {specsmith-0.6.0.dev232 → specsmith-0.6.0.dev233}/tests/test_importer.py +0 -0
- {specsmith-0.6.0.dev232 → specsmith-0.6.0.dev233}/tests/test_integrations.py +0 -0
- {specsmith-0.6.0.dev232 → specsmith-0.6.0.dev233}/tests/test_mcp_client.py +0 -0
- {specsmith-0.6.0.dev232 → specsmith-0.6.0.dev233}/tests/test_nexus.py +0 -0
- {specsmith-0.6.0.dev232 → specsmith-0.6.0.dev233}/tests/test_phase1_4_new.py +0 -0
- {specsmith-0.6.0.dev232 → specsmith-0.6.0.dev233}/tests/test_phase34_completion.py +0 -0
- {specsmith-0.6.0.dev232 → specsmith-0.6.0.dev233}/tests/test_rate_limits.py +0 -0
- {specsmith-0.6.0.dev232 → specsmith-0.6.0.dev233}/tests/test_scaffolder.py +0 -0
- {specsmith-0.6.0.dev232 → specsmith-0.6.0.dev233}/tests/test_skill_marketplace.py +0 -0
- {specsmith-0.6.0.dev232 → specsmith-0.6.0.dev233}/tests/test_smoke.py +0 -0
- {specsmith-0.6.0.dev232 → specsmith-0.6.0.dev233}/tests/test_suggester.py +0 -0
- {specsmith-0.6.0.dev232 → specsmith-0.6.0.dev233}/tests/test_tools.py +0 -0
- {specsmith-0.6.0.dev232 → specsmith-0.6.0.dev233}/tests/test_validator.py +0 -0
- {specsmith-0.6.0.dev232 → specsmith-0.6.0.dev233}/tests/test_vcs.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: specsmith
|
|
3
|
-
Version: 0.6.0.
|
|
3
|
+
Version: 0.6.0.dev233
|
|
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
|
|
@@ -53,6 +53,11 @@ Provides-Extra: gui
|
|
|
53
53
|
Requires-Dist: PySide6>=6.6; extra == "gui"
|
|
54
54
|
Provides-Extra: ag2
|
|
55
55
|
Requires-Dist: ag2[ollama]; extra == "ag2"
|
|
56
|
+
Provides-Extra: history-semantic
|
|
57
|
+
Requires-Dist: sentence-transformers>=2.2; extra == "history-semantic"
|
|
58
|
+
Requires-Dist: numpy>=1.24; extra == "history-semantic"
|
|
59
|
+
Provides-Extra: voice
|
|
60
|
+
Requires-Dist: whisper-cpp-python>=0.2; extra == "voice"
|
|
56
61
|
Provides-Extra: agent
|
|
57
62
|
Requires-Dist: anthropic>=0.56; extra == "agent"
|
|
58
63
|
Requires-Dist: openai>=1.0; extra == "agent"
|
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "specsmith"
|
|
7
|
-
version = "0.6.0.
|
|
7
|
+
version = "0.6.0.dev233"
|
|
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"
|
|
@@ -67,6 +67,11 @@ mistral = ["openai>=1.0"] # Mistral uses the openai SDK pointed at api.mistral.
|
|
|
67
67
|
gui = ["PySide6>=6.6"]
|
|
68
68
|
# AG2 agent shell (Planner/Builder/Verifier over Ollama)
|
|
69
69
|
ag2 = ["ag2[ollama]"]
|
|
70
|
+
# Optional semantic backend for `specsmith history search --semantic` (REQ-135).
|
|
71
|
+
# Falls back gracefully to keyword matching if these are not installed.
|
|
72
|
+
history-semantic = ["sentence-transformers>=2.2", "numpy>=1.24"]
|
|
73
|
+
# Optional whisper-cpp wrapper for the voice agent input (REQ-141).
|
|
74
|
+
voice = ["whisper-cpp-python>=0.2"]
|
|
70
75
|
# Install all optional LLM providers
|
|
71
76
|
agent = ["anthropic>=0.56", "openai>=1.0"]
|
|
72
77
|
# Convenience bundle: everything
|
|
@@ -138,6 +143,12 @@ module = [
|
|
|
138
143
|
"yaml.*",
|
|
139
144
|
"keyring", # optional OS credential store; stubs not published
|
|
140
145
|
"keyring.*",
|
|
146
|
+
"numpy", # optional [history-semantic] extra (REQ-135)
|
|
147
|
+
"numpy.*",
|
|
148
|
+
"sentence_transformers", # optional [history-semantic] extra (REQ-135)
|
|
149
|
+
"sentence_transformers.*",
|
|
150
|
+
"whisper_cpp_python", # optional [voice] extra (REQ-141)
|
|
151
|
+
"whisper_cpp_python.*",
|
|
141
152
|
]
|
|
142
153
|
ignore_missing_imports = true
|
|
143
154
|
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
# SPDX-License-Identifier: MIT
|
|
2
|
+
# Copyright (c) 2026 BitConcepts, LLC. All rights reserved.
|
|
3
|
+
"""Per-block share/export for `specsmith chat` (REQ-134).
|
|
4
|
+
|
|
5
|
+
Reads ``.specsmith/sessions/<session_id>/events.jsonl`` (the chat replay log
|
|
6
|
+
or, fallback, ``turns.jsonl``) and slices a single block out as a
|
|
7
|
+
self-contained Markdown / JSON / HTML snippet.
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
from __future__ import annotations
|
|
11
|
+
|
|
12
|
+
import html
|
|
13
|
+
import json
|
|
14
|
+
from pathlib import Path
|
|
15
|
+
from typing import Any
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def _events_path(project_dir: Path, session_id: str) -> Path | None:
|
|
19
|
+
base = project_dir / ".specsmith" / "sessions" / session_id
|
|
20
|
+
candidates = [
|
|
21
|
+
base / "events.jsonl",
|
|
22
|
+
base / "turns.jsonl",
|
|
23
|
+
]
|
|
24
|
+
for c in candidates:
|
|
25
|
+
if c.is_file():
|
|
26
|
+
return c
|
|
27
|
+
return None
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def _read_events(events_path: Path) -> list[dict[str, Any]]:
|
|
31
|
+
out: list[dict[str, Any]] = []
|
|
32
|
+
for line in events_path.read_text(encoding="utf-8").splitlines():
|
|
33
|
+
line = line.strip()
|
|
34
|
+
if not line:
|
|
35
|
+
continue
|
|
36
|
+
try:
|
|
37
|
+
obj = json.loads(line)
|
|
38
|
+
except ValueError:
|
|
39
|
+
continue
|
|
40
|
+
if isinstance(obj, dict):
|
|
41
|
+
out.append(obj)
|
|
42
|
+
return out
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def slice_block(events: list[dict[str, Any]], block_id: str) -> list[dict[str, Any]]:
|
|
46
|
+
"""Return all events tagged with ``block_id``, plus the bracketing
|
|
47
|
+
block_start/block_complete events that defined it.
|
|
48
|
+
"""
|
|
49
|
+
out: list[dict[str, Any]] = []
|
|
50
|
+
for evt in events:
|
|
51
|
+
if evt.get("block_id") == block_id or evt.get("id") == block_id:
|
|
52
|
+
out.append(evt)
|
|
53
|
+
return out
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def export_block(
|
|
57
|
+
project_dir: Path,
|
|
58
|
+
session_id: str,
|
|
59
|
+
block_id: str,
|
|
60
|
+
*,
|
|
61
|
+
fmt: str = "md",
|
|
62
|
+
) -> str:
|
|
63
|
+
"""Export the events for ``block_id`` as a string in ``fmt``.
|
|
64
|
+
|
|
65
|
+
Raises FileNotFoundError if no session log exists.
|
|
66
|
+
Raises KeyError if the block is not found.
|
|
67
|
+
"""
|
|
68
|
+
events_path = _events_path(project_dir, session_id)
|
|
69
|
+
if events_path is None:
|
|
70
|
+
raise FileNotFoundError(f"No session log for {session_id} in {project_dir}")
|
|
71
|
+
events = _read_events(events_path)
|
|
72
|
+
matching = slice_block(events, block_id)
|
|
73
|
+
if not matching:
|
|
74
|
+
raise KeyError(f"block_id {block_id} not found in session {session_id}")
|
|
75
|
+
if fmt == "json":
|
|
76
|
+
return json.dumps(matching, indent=2)
|
|
77
|
+
if fmt == "html":
|
|
78
|
+
rows = "".join(
|
|
79
|
+
f"<li><pre>{html.escape(json.dumps(evt, indent=2))}</pre></li>" for evt in matching
|
|
80
|
+
)
|
|
81
|
+
return (
|
|
82
|
+
f"<!DOCTYPE html><html><head><meta charset='utf-8'>"
|
|
83
|
+
f"<title>specsmith block {html.escape(block_id)}</title></head>"
|
|
84
|
+
f"<body><h1>Block {html.escape(block_id)}</h1>"
|
|
85
|
+
f"<p>session {html.escape(session_id)}</p><ol>{rows}</ol></body></html>"
|
|
86
|
+
)
|
|
87
|
+
# default: markdown
|
|
88
|
+
lines: list[str] = [
|
|
89
|
+
f"# Block `{block_id}`",
|
|
90
|
+
f"_session_: `{session_id}`",
|
|
91
|
+
"",
|
|
92
|
+
]
|
|
93
|
+
for evt in matching:
|
|
94
|
+
kind = str(evt.get("type", "event"))
|
|
95
|
+
lines.append(f"## {kind}")
|
|
96
|
+
if "text" in evt:
|
|
97
|
+
lines.append(str(evt["text"]))
|
|
98
|
+
else:
|
|
99
|
+
lines.append("```json")
|
|
100
|
+
lines.append(json.dumps(evt, indent=2))
|
|
101
|
+
lines.append("```")
|
|
102
|
+
lines.append("")
|
|
103
|
+
return "\n".join(lines)
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
__all__ = ["export_block", "slice_block"]
|
|
@@ -4440,6 +4440,129 @@ def info_cmd(as_json: bool, section: str) -> None:
|
|
|
4440
4440
|
# ---------------------------------------------------------------------------
|
|
4441
4441
|
|
|
4442
4442
|
|
|
4443
|
+
# ---------------------------------------------------------------------------
|
|
4444
|
+
# specsmith chat-export-block — self-contained block share (REQ-134)
|
|
4445
|
+
# ---------------------------------------------------------------------------
|
|
4446
|
+
#
|
|
4447
|
+
# This is exposed at the top level (rather than under ``chat``) because the
|
|
4448
|
+
# existing ``specsmith chat <utterance>`` command takes a positional argument
|
|
4449
|
+
# and cannot simultaneously act as a Click group.
|
|
4450
|
+
|
|
4451
|
+
|
|
4452
|
+
@main.command(name="chat-export-block")
|
|
4453
|
+
@click.option("--project-dir", type=click.Path(exists=True), default=".")
|
|
4454
|
+
@click.option("--session-id", "session_id", required=True)
|
|
4455
|
+
@click.option("--block-id", "block_id", required=True)
|
|
4456
|
+
@click.option(
|
|
4457
|
+
"--format",
|
|
4458
|
+
"fmt",
|
|
4459
|
+
type=click.Choice(["md", "json", "html"]),
|
|
4460
|
+
default="md",
|
|
4461
|
+
)
|
|
4462
|
+
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
|
+
from specsmith.block_export import export_block
|
|
4465
|
+
|
|
4466
|
+
try:
|
|
4467
|
+
out = export_block(
|
|
4468
|
+
Path(project_dir).resolve(),
|
|
4469
|
+
session_id,
|
|
4470
|
+
block_id,
|
|
4471
|
+
fmt=fmt,
|
|
4472
|
+
)
|
|
4473
|
+
except FileNotFoundError as exc:
|
|
4474
|
+
console.print(f"[red]{exc}[/red]")
|
|
4475
|
+
raise SystemExit(1) from exc
|
|
4476
|
+
except KeyError as exc:
|
|
4477
|
+
console.print(f"[red]{exc}[/red]")
|
|
4478
|
+
raise SystemExit(1) from exc
|
|
4479
|
+
click.echo(out)
|
|
4480
|
+
|
|
4481
|
+
|
|
4482
|
+
# ---------------------------------------------------------------------------
|
|
4483
|
+
# specsmith cloud serve — reference cloud-agent receiver (REQ-136)
|
|
4484
|
+
# ---------------------------------------------------------------------------
|
|
4485
|
+
|
|
4486
|
+
|
|
4487
|
+
@main.command(name="cloud-serve")
|
|
4488
|
+
@click.option("--host", default="127.0.0.1")
|
|
4489
|
+
@click.option("--port", type=int, default=9000)
|
|
4490
|
+
@click.option("--token", default="", help="Optional bearer token.")
|
|
4491
|
+
@click.option("--allow-cidr", default="", help="CIDR range required to bind non-loopback.")
|
|
4492
|
+
def cloud_serve_cmd(host: str, port: int, token: str, allow_cidr: str) -> None:
|
|
4493
|
+
"""Run the reference cloud-agent receiver (REQ-136).
|
|
4494
|
+
|
|
4495
|
+
Accepts POST /spawn with a JSON manifest, persists it under
|
|
4496
|
+
~/.specsmith/cloud-runs/<run_id>/manifest.json, and returns 202 with
|
|
4497
|
+
a stream_url placeholder.
|
|
4498
|
+
"""
|
|
4499
|
+
from specsmith.cloud_serve import CloudReceiverConfig, make_server
|
|
4500
|
+
|
|
4501
|
+
config = CloudReceiverConfig(host=host, port=port, token=token, allow_cidr=allow_cidr)
|
|
4502
|
+
try:
|
|
4503
|
+
server = make_server(config)
|
|
4504
|
+
except RuntimeError as exc:
|
|
4505
|
+
console.print(f"[red]{exc}[/red]")
|
|
4506
|
+
raise SystemExit(2) from exc
|
|
4507
|
+
console.print(
|
|
4508
|
+
f"[bold]specsmith cloud serve[/bold] on http://{config.host}:{config.port}\n"
|
|
4509
|
+
f" storage: {config.storage_dir}\n"
|
|
4510
|
+
f" token: {'(set)' if token else '(none)'}\n"
|
|
4511
|
+
" Press Ctrl+C to stop."
|
|
4512
|
+
)
|
|
4513
|
+
try:
|
|
4514
|
+
server.serve_forever()
|
|
4515
|
+
except KeyboardInterrupt:
|
|
4516
|
+
console.print("\n[dim]cloud serve stopped.[/dim]")
|
|
4517
|
+
server.server_close()
|
|
4518
|
+
|
|
4519
|
+
|
|
4520
|
+
# ---------------------------------------------------------------------------
|
|
4521
|
+
# specsmith api-surface — 1.0 stability snapshot (REQ-140)
|
|
4522
|
+
# ---------------------------------------------------------------------------
|
|
4523
|
+
|
|
4524
|
+
|
|
4525
|
+
@main.command(name="api-surface")
|
|
4526
|
+
@click.option(
|
|
4527
|
+
"--snapshot",
|
|
4528
|
+
type=click.Path(),
|
|
4529
|
+
default="",
|
|
4530
|
+
help="Write the current public surface to this JSON file.",
|
|
4531
|
+
)
|
|
4532
|
+
def api_surface_cmd(snapshot: str) -> None:
|
|
4533
|
+
"""Print the frozen public CLI/API surface as JSON (REQ-140)."""
|
|
4534
|
+
import json as _json
|
|
4535
|
+
|
|
4536
|
+
surface = {
|
|
4537
|
+
"cli_commands": sorted(
|
|
4538
|
+
cmd_name for cmd_name in main.commands if not cmd_name.startswith("_")
|
|
4539
|
+
),
|
|
4540
|
+
"exit_codes": {
|
|
4541
|
+
"preflight_accepted": 0,
|
|
4542
|
+
"preflight_needs_clarification": 2,
|
|
4543
|
+
"preflight_blocked": 3,
|
|
4544
|
+
"verify_ok": 0,
|
|
4545
|
+
"verify_retry": 2,
|
|
4546
|
+
"verify_stop": 3,
|
|
4547
|
+
},
|
|
4548
|
+
"event_types": [
|
|
4549
|
+
"block_start",
|
|
4550
|
+
"block_complete",
|
|
4551
|
+
"token",
|
|
4552
|
+
"plan_step",
|
|
4553
|
+
"tool_call",
|
|
4554
|
+
"tool_request",
|
|
4555
|
+
"tool_result",
|
|
4556
|
+
"diff",
|
|
4557
|
+
"task_complete",
|
|
4558
|
+
],
|
|
4559
|
+
}
|
|
4560
|
+
payload = _json.dumps(surface, indent=2, sort_keys=True)
|
|
4561
|
+
if snapshot:
|
|
4562
|
+
Path(snapshot).write_text(payload, encoding="utf-8")
|
|
4563
|
+
click.echo(payload)
|
|
4564
|
+
|
|
4565
|
+
|
|
4443
4566
|
# ---------------------------------------------------------------------------
|
|
4444
4567
|
# specsmith suggest-command — NL-to-command suggester (REQ-131)
|
|
4445
4568
|
# ---------------------------------------------------------------------------
|
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
# SPDX-License-Identifier: MIT
|
|
2
|
+
# Copyright (c) 2026 BitConcepts, LLC. All rights reserved.
|
|
3
|
+
"""Reference cloud-agent receiver for `specsmith cloud spawn` (REQ-136).
|
|
4
|
+
|
|
5
|
+
A minimal stdlib HTTP server that accepts manifest-only POSTs at ``/spawn``
|
|
6
|
+
and acks them. The full streaming-back-of-results contract is documented
|
|
7
|
+
but kept narrow (and intentionally local-only) so we ship a working
|
|
8
|
+
endpoint without baking in vendor coupling.
|
|
9
|
+
|
|
10
|
+
Auth model: optional ``Authorization: Bearer <token>``. When the server
|
|
11
|
+
is started with ``--token``, every request must present it.
|
|
12
|
+
Defense-in-depth: the server refuses to bind to any address other than
|
|
13
|
+
``127.0.0.1`` unless explicitly given ``--host`` AND ``--allow-cidr``.
|
|
14
|
+
"""
|
|
15
|
+
|
|
16
|
+
from __future__ import annotations
|
|
17
|
+
|
|
18
|
+
import ipaddress
|
|
19
|
+
import json
|
|
20
|
+
import threading
|
|
21
|
+
from dataclasses import dataclass, field
|
|
22
|
+
from http.server import BaseHTTPRequestHandler, HTTPServer
|
|
23
|
+
from pathlib import Path
|
|
24
|
+
from typing import Any
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
@dataclass
|
|
28
|
+
class CloudReceiverConfig:
|
|
29
|
+
host: str = "127.0.0.1"
|
|
30
|
+
port: int = 9000
|
|
31
|
+
token: str = ""
|
|
32
|
+
allow_cidr: str = ""
|
|
33
|
+
storage_dir: Path = field(default_factory=lambda: Path.home() / ".specsmith" / "cloud-runs")
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
class _Handler(BaseHTTPRequestHandler):
|
|
37
|
+
config: CloudReceiverConfig = CloudReceiverConfig()
|
|
38
|
+
|
|
39
|
+
# noqa: N802 -- BaseHTTPRequestHandler API.
|
|
40
|
+
def do_POST(self) -> None: # noqa: N802
|
|
41
|
+
if not self._authorize():
|
|
42
|
+
self._respond(401, {"error": "unauthorized"})
|
|
43
|
+
return
|
|
44
|
+
if self.path != "/spawn":
|
|
45
|
+
self._respond(404, {"error": f"unknown path {self.path}"})
|
|
46
|
+
return
|
|
47
|
+
length = int(self.headers.get("Content-Length", "0") or "0")
|
|
48
|
+
body = self.rfile.read(length) if length else b""
|
|
49
|
+
try:
|
|
50
|
+
payload = json.loads(body.decode("utf-8") or "{}")
|
|
51
|
+
except ValueError:
|
|
52
|
+
self._respond(400, {"error": "invalid json"})
|
|
53
|
+
return
|
|
54
|
+
run_id = str(payload.get("run_id", "")).strip() or _new_run_id()
|
|
55
|
+
target = self.config.storage_dir / run_id
|
|
56
|
+
try:
|
|
57
|
+
target.mkdir(parents=True, exist_ok=True)
|
|
58
|
+
(target / "manifest.json").write_text(
|
|
59
|
+
json.dumps(payload, indent=2),
|
|
60
|
+
encoding="utf-8",
|
|
61
|
+
)
|
|
62
|
+
except OSError as exc:
|
|
63
|
+
self._respond(500, {"error": f"storage failed: {exc}"})
|
|
64
|
+
return
|
|
65
|
+
self._respond(
|
|
66
|
+
202,
|
|
67
|
+
{
|
|
68
|
+
"run_id": run_id,
|
|
69
|
+
"status": "accepted",
|
|
70
|
+
"stream_url": f"/runs/{run_id}/events",
|
|
71
|
+
},
|
|
72
|
+
)
|
|
73
|
+
|
|
74
|
+
def do_GET(self) -> None: # noqa: N802
|
|
75
|
+
if not self._authorize():
|
|
76
|
+
self._respond(401, {"error": "unauthorized"})
|
|
77
|
+
return
|
|
78
|
+
if self.path == "/health":
|
|
79
|
+
self._respond(200, {"ok": True})
|
|
80
|
+
return
|
|
81
|
+
self._respond(404, {"error": f"unknown path {self.path}"})
|
|
82
|
+
|
|
83
|
+
def log_message(self, format: str, *args: Any) -> None: # noqa: A002
|
|
84
|
+
# Quiet by default — caller sees JSON responses.
|
|
85
|
+
return
|
|
86
|
+
|
|
87
|
+
# ── helpers ───────────────────────────────────────────────────────────
|
|
88
|
+
|
|
89
|
+
def _authorize(self) -> bool:
|
|
90
|
+
if self.config.token:
|
|
91
|
+
header = self.headers.get("Authorization", "")
|
|
92
|
+
if header != f"Bearer {self.config.token}":
|
|
93
|
+
return False
|
|
94
|
+
if self.config.allow_cidr:
|
|
95
|
+
try:
|
|
96
|
+
net = ipaddress.ip_network(self.config.allow_cidr, strict=False)
|
|
97
|
+
client = ipaddress.ip_address(self.client_address[0])
|
|
98
|
+
if client not in net:
|
|
99
|
+
return False
|
|
100
|
+
except (ValueError, TypeError):
|
|
101
|
+
return False
|
|
102
|
+
return True
|
|
103
|
+
|
|
104
|
+
def _respond(self, status: int, payload: dict[str, Any]) -> None:
|
|
105
|
+
body = json.dumps(payload).encode("utf-8")
|
|
106
|
+
self.send_response(status)
|
|
107
|
+
self.send_header("Content-Type", "application/json")
|
|
108
|
+
self.send_header("Content-Length", str(len(body)))
|
|
109
|
+
self.end_headers()
|
|
110
|
+
self.wfile.write(body)
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
def _new_run_id() -> str:
|
|
114
|
+
import uuid
|
|
115
|
+
|
|
116
|
+
return f"cloud_{uuid.uuid4().hex[:12]}"
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
def _validate_host(config: CloudReceiverConfig) -> None:
|
|
120
|
+
if config.host not in {"127.0.0.1", "::1", "localhost"} and not config.allow_cidr:
|
|
121
|
+
raise RuntimeError(
|
|
122
|
+
"specsmith cloud serve refuses to bind to a non-loopback address "
|
|
123
|
+
"unless --allow-cidr is also set. This is a security guardrail."
|
|
124
|
+
)
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
def make_server(config: CloudReceiverConfig) -> HTTPServer:
|
|
128
|
+
_validate_host(config)
|
|
129
|
+
config.storage_dir.mkdir(parents=True, exist_ok=True)
|
|
130
|
+
|
|
131
|
+
class _Bound(_Handler):
|
|
132
|
+
pass
|
|
133
|
+
|
|
134
|
+
_Bound.config = config
|
|
135
|
+
return HTTPServer((config.host, config.port), _Bound)
|
|
136
|
+
|
|
137
|
+
|
|
138
|
+
def run_in_thread(config: CloudReceiverConfig) -> tuple[HTTPServer, threading.Thread]:
|
|
139
|
+
"""Start the server in a background thread; useful for tests."""
|
|
140
|
+
server = make_server(config)
|
|
141
|
+
thread = threading.Thread(target=server.serve_forever, daemon=True)
|
|
142
|
+
thread.start()
|
|
143
|
+
return server, thread
|
|
144
|
+
|
|
145
|
+
|
|
146
|
+
__all__ = [
|
|
147
|
+
"CloudReceiverConfig",
|
|
148
|
+
"make_server",
|
|
149
|
+
"run_in_thread",
|
|
150
|
+
]
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
# SPDX-License-Identifier: MIT
|
|
2
|
+
# Copyright (c) 2026 BitConcepts, LLC. All rights reserved.
|
|
3
|
+
"""Specsmith Drive (REQ-133) — sync rules/workflows/notebooks across machines.
|
|
4
|
+
|
|
5
|
+
Backend agnostic: the default backend is a local filesystem mirror under
|
|
6
|
+
``~/.specsmith/drive/`` which the user can ``git push`` themselves. An
|
|
7
|
+
HTTP backend is documented but not bundled (see ``examples/drive_http_server.py``
|
|
8
|
+
once it exists).
|
|
9
|
+
|
|
10
|
+
Supported artifact kinds:
|
|
11
|
+
* ``rules`` — files under ``docs/governance/*_RULES.md``
|
|
12
|
+
* ``workflows`` — files under ``.specsmith/workflows/*.yml``
|
|
13
|
+
* ``notebooks`` — files under ``docs/notebooks/*.md``
|
|
14
|
+
|
|
15
|
+
Each project's artifacts go into ``<drive>/<project_name>/<kind>/<file>``.
|
|
16
|
+
"""
|
|
17
|
+
|
|
18
|
+
from __future__ import annotations
|
|
19
|
+
|
|
20
|
+
import shutil
|
|
21
|
+
from dataclasses import dataclass
|
|
22
|
+
from pathlib import Path
|
|
23
|
+
|
|
24
|
+
KINDS = ("rules", "workflows", "notebooks")
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def default_drive_dir() -> Path:
|
|
28
|
+
return Path.home() / ".specsmith" / "drive"
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def _kind_sources(project_dir: Path) -> dict[str, list[Path]]:
|
|
32
|
+
return {
|
|
33
|
+
"rules": sorted((project_dir / "docs" / "governance").glob("*_RULES.md"))
|
|
34
|
+
if (project_dir / "docs" / "governance").is_dir()
|
|
35
|
+
else [],
|
|
36
|
+
"workflows": sorted((project_dir / ".specsmith" / "workflows").glob("*.yml"))
|
|
37
|
+
if (project_dir / ".specsmith" / "workflows").is_dir()
|
|
38
|
+
else [],
|
|
39
|
+
"notebooks": sorted((project_dir / "docs" / "notebooks").glob("*.md"))
|
|
40
|
+
if (project_dir / "docs" / "notebooks").is_dir()
|
|
41
|
+
else [],
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def _kind_dest(drive_dir: Path, project_name: str, kind: str) -> Path:
|
|
46
|
+
return drive_dir / project_name / kind
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
@dataclass
|
|
50
|
+
class DriveResult:
|
|
51
|
+
pushed: list[str]
|
|
52
|
+
pulled: list[str]
|
|
53
|
+
skipped: list[str]
|
|
54
|
+
errors: list[str]
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def push(project_dir: Path, drive_dir: Path | None = None) -> DriveResult:
|
|
58
|
+
"""Mirror project artifacts into the drive directory."""
|
|
59
|
+
drive_dir = drive_dir or default_drive_dir()
|
|
60
|
+
project_name = project_dir.name
|
|
61
|
+
pushed: list[str] = []
|
|
62
|
+
skipped: list[str] = []
|
|
63
|
+
errors: list[str] = []
|
|
64
|
+
for kind, files in _kind_sources(project_dir).items():
|
|
65
|
+
dest = _kind_dest(drive_dir, project_name, kind)
|
|
66
|
+
dest.mkdir(parents=True, exist_ok=True)
|
|
67
|
+
for src in files:
|
|
68
|
+
try:
|
|
69
|
+
shutil.copy2(src, dest / src.name)
|
|
70
|
+
pushed.append(f"{kind}/{src.name}")
|
|
71
|
+
except OSError as exc:
|
|
72
|
+
errors.append(f"{kind}/{src.name}: {exc}")
|
|
73
|
+
if not files:
|
|
74
|
+
skipped.append(f"{kind} (no source files)")
|
|
75
|
+
return DriveResult(pushed=pushed, pulled=[], skipped=skipped, errors=errors)
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
def pull(project_dir: Path, drive_dir: Path | None = None) -> DriveResult:
|
|
79
|
+
"""Mirror drive artifacts back into the project."""
|
|
80
|
+
drive_dir = drive_dir or default_drive_dir()
|
|
81
|
+
project_name = project_dir.name
|
|
82
|
+
pulled: list[str] = []
|
|
83
|
+
skipped: list[str] = []
|
|
84
|
+
errors: list[str] = []
|
|
85
|
+
project_targets = {
|
|
86
|
+
"rules": project_dir / "docs" / "governance",
|
|
87
|
+
"workflows": project_dir / ".specsmith" / "workflows",
|
|
88
|
+
"notebooks": project_dir / "docs" / "notebooks",
|
|
89
|
+
}
|
|
90
|
+
for kind, target in project_targets.items():
|
|
91
|
+
src_dir = _kind_dest(drive_dir, project_name, kind)
|
|
92
|
+
if not src_dir.is_dir():
|
|
93
|
+
skipped.append(f"{kind} (no drive entry)")
|
|
94
|
+
continue
|
|
95
|
+
target.mkdir(parents=True, exist_ok=True)
|
|
96
|
+
for src in sorted(src_dir.iterdir()):
|
|
97
|
+
if not src.is_file():
|
|
98
|
+
continue
|
|
99
|
+
try:
|
|
100
|
+
shutil.copy2(src, target / src.name)
|
|
101
|
+
pulled.append(f"{kind}/{src.name}")
|
|
102
|
+
except OSError as exc:
|
|
103
|
+
errors.append(f"{kind}/{src.name}: {exc}")
|
|
104
|
+
return DriveResult(pushed=[], pulled=pulled, skipped=skipped, errors=errors)
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
def listing(drive_dir: Path | None = None) -> dict[str, dict[str, list[str]]]:
|
|
108
|
+
"""Return ``{project: {kind: [filenames]}}`` for everything in the drive."""
|
|
109
|
+
drive_dir = drive_dir or default_drive_dir()
|
|
110
|
+
out: dict[str, dict[str, list[str]]] = {}
|
|
111
|
+
if not drive_dir.is_dir():
|
|
112
|
+
return out
|
|
113
|
+
for project_path in sorted(drive_dir.iterdir()):
|
|
114
|
+
if not project_path.is_dir():
|
|
115
|
+
continue
|
|
116
|
+
kinds: dict[str, list[str]] = {}
|
|
117
|
+
for kind in KINDS:
|
|
118
|
+
d = project_path / kind
|
|
119
|
+
if d.is_dir():
|
|
120
|
+
kinds[kind] = sorted(p.name for p in d.iterdir() if p.is_file())
|
|
121
|
+
if kinds:
|
|
122
|
+
out[project_path.name] = kinds
|
|
123
|
+
return out
|
|
124
|
+
|
|
125
|
+
|
|
126
|
+
__all__ = ["DriveResult", "KINDS", "default_drive_dir", "listing", "pull", "push"]
|