specsmith 0.6.0.dev229__tar.gz → 0.6.0.dev231__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.dev229/src/specsmith.egg-info → specsmith-0.6.0.dev231}/PKG-INFO +1 -1
- {specsmith-0.6.0.dev229 → specsmith-0.6.0.dev231}/pyproject.toml +1 -1
- specsmith-0.6.0.dev231/src/specsmith/agent/mcp.py +387 -0
- {specsmith-0.6.0.dev229 → specsmith-0.6.0.dev231}/src/specsmith/cli.py +63 -8
- {specsmith-0.6.0.dev229 → specsmith-0.6.0.dev231/src/specsmith.egg-info}/PKG-INFO +1 -1
- {specsmith-0.6.0.dev229 → specsmith-0.6.0.dev231}/src/specsmith.egg-info/SOURCES.txt +2 -0
- specsmith-0.6.0.dev231/tests/test_mcp_client.py +157 -0
- specsmith-0.6.0.dev231/tests/test_phase34_completion.py +243 -0
- specsmith-0.6.0.dev229/src/specsmith/agent/mcp.py +0 -117
- {specsmith-0.6.0.dev229 → specsmith-0.6.0.dev231}/LICENSE +0 -0
- {specsmith-0.6.0.dev229 → specsmith-0.6.0.dev231}/README.md +0 -0
- {specsmith-0.6.0.dev229 → specsmith-0.6.0.dev231}/setup.cfg +0 -0
- {specsmith-0.6.0.dev229 → specsmith-0.6.0.dev231}/src/epistemic/__init__.py +0 -0
- {specsmith-0.6.0.dev229 → specsmith-0.6.0.dev231}/src/epistemic/belief.py +0 -0
- {specsmith-0.6.0.dev229 → specsmith-0.6.0.dev231}/src/epistemic/certainty.py +0 -0
- {specsmith-0.6.0.dev229 → specsmith-0.6.0.dev231}/src/epistemic/failure_graph.py +0 -0
- {specsmith-0.6.0.dev229 → specsmith-0.6.0.dev231}/src/epistemic/py.typed +0 -0
- {specsmith-0.6.0.dev229 → specsmith-0.6.0.dev231}/src/epistemic/recovery.py +0 -0
- {specsmith-0.6.0.dev229 → specsmith-0.6.0.dev231}/src/epistemic/session.py +0 -0
- {specsmith-0.6.0.dev229 → specsmith-0.6.0.dev231}/src/epistemic/stress_tester.py +0 -0
- {specsmith-0.6.0.dev229 → specsmith-0.6.0.dev231}/src/epistemic/trace.py +0 -0
- {specsmith-0.6.0.dev229 → specsmith-0.6.0.dev231}/src/specsmith/__init__.py +0 -0
- {specsmith-0.6.0.dev229 → specsmith-0.6.0.dev231}/src/specsmith/__main__.py +0 -0
- {specsmith-0.6.0.dev229 → specsmith-0.6.0.dev231}/src/specsmith/agent/__init__.py +0 -0
- {specsmith-0.6.0.dev229 → specsmith-0.6.0.dev231}/src/specsmith/agent/broker.py +0 -0
- {specsmith-0.6.0.dev229 → specsmith-0.6.0.dev231}/src/specsmith/agent/chat_runner.py +0 -0
- {specsmith-0.6.0.dev229 → specsmith-0.6.0.dev231}/src/specsmith/agent/cleanup.py +0 -0
- {specsmith-0.6.0.dev229 → specsmith-0.6.0.dev231}/src/specsmith/agent/events.py +0 -0
- {specsmith-0.6.0.dev229 → specsmith-0.6.0.dev231}/src/specsmith/agent/indexer.py +0 -0
- {specsmith-0.6.0.dev229 → specsmith-0.6.0.dev231}/src/specsmith/agent/memory.py +0 -0
- {specsmith-0.6.0.dev229 → specsmith-0.6.0.dev231}/src/specsmith/agent/orchestrator.py +0 -0
- {specsmith-0.6.0.dev229 → specsmith-0.6.0.dev231}/src/specsmith/agent/repl.py +0 -0
- {specsmith-0.6.0.dev229 → specsmith-0.6.0.dev231}/src/specsmith/agent/router.py +0 -0
- {specsmith-0.6.0.dev229 → specsmith-0.6.0.dev231}/src/specsmith/agent/rules.py +0 -0
- {specsmith-0.6.0.dev229 → specsmith-0.6.0.dev231}/src/specsmith/agent/safety.py +0 -0
- {specsmith-0.6.0.dev229 → specsmith-0.6.0.dev231}/src/specsmith/agent/tools.py +0 -0
- {specsmith-0.6.0.dev229 → specsmith-0.6.0.dev231}/src/specsmith/agent/verifier.py +0 -0
- {specsmith-0.6.0.dev229 → specsmith-0.6.0.dev231}/src/specsmith/architect.py +0 -0
- {specsmith-0.6.0.dev229 → specsmith-0.6.0.dev231}/src/specsmith/auditor.py +0 -0
- {specsmith-0.6.0.dev229 → specsmith-0.6.0.dev231}/src/specsmith/auth.py +0 -0
- {specsmith-0.6.0.dev229 → specsmith-0.6.0.dev231}/src/specsmith/commands/__init__.py +0 -0
- {specsmith-0.6.0.dev229 → specsmith-0.6.0.dev231}/src/specsmith/compressor.py +0 -0
- {specsmith-0.6.0.dev229 → specsmith-0.6.0.dev231}/src/specsmith/config.py +0 -0
- {specsmith-0.6.0.dev229 → specsmith-0.6.0.dev231}/src/specsmith/console_utils.py +0 -0
- {specsmith-0.6.0.dev229 → specsmith-0.6.0.dev231}/src/specsmith/credit_analyzer.py +0 -0
- {specsmith-0.6.0.dev229 → specsmith-0.6.0.dev231}/src/specsmith/credits.py +0 -0
- {specsmith-0.6.0.dev229 → specsmith-0.6.0.dev231}/src/specsmith/differ.py +0 -0
- {specsmith-0.6.0.dev229 → specsmith-0.6.0.dev231}/src/specsmith/doctor.py +0 -0
- {specsmith-0.6.0.dev229 → specsmith-0.6.0.dev231}/src/specsmith/epistemic/__init__.py +0 -0
- {specsmith-0.6.0.dev229 → specsmith-0.6.0.dev231}/src/specsmith/epistemic/belief.py +0 -0
- {specsmith-0.6.0.dev229 → specsmith-0.6.0.dev231}/src/specsmith/epistemic/certainty.py +0 -0
- {specsmith-0.6.0.dev229 → specsmith-0.6.0.dev231}/src/specsmith/epistemic/failure_graph.py +0 -0
- {specsmith-0.6.0.dev229 → specsmith-0.6.0.dev231}/src/specsmith/epistemic/recovery.py +0 -0
- {specsmith-0.6.0.dev229 → specsmith-0.6.0.dev231}/src/specsmith/epistemic/stress_tester.py +0 -0
- {specsmith-0.6.0.dev229 → specsmith-0.6.0.dev231}/src/specsmith/executor.py +0 -0
- {specsmith-0.6.0.dev229 → specsmith-0.6.0.dev231}/src/specsmith/exporter.py +0 -0
- {specsmith-0.6.0.dev229 → specsmith-0.6.0.dev231}/src/specsmith/gui/__init__.py +0 -0
- {specsmith-0.6.0.dev229 → specsmith-0.6.0.dev231}/src/specsmith/gui/app.py +0 -0
- {specsmith-0.6.0.dev229 → specsmith-0.6.0.dev231}/src/specsmith/gui/main_window.py +0 -0
- {specsmith-0.6.0.dev229 → specsmith-0.6.0.dev231}/src/specsmith/gui/session_tab.py +0 -0
- {specsmith-0.6.0.dev229 → specsmith-0.6.0.dev231}/src/specsmith/gui/theme.py +0 -0
- {specsmith-0.6.0.dev229 → specsmith-0.6.0.dev231}/src/specsmith/gui/widgets/__init__.py +0 -0
- {specsmith-0.6.0.dev229 → specsmith-0.6.0.dev231}/src/specsmith/gui/widgets/chat_view.py +0 -0
- {specsmith-0.6.0.dev229 → specsmith-0.6.0.dev231}/src/specsmith/gui/widgets/input_bar.py +0 -0
- {specsmith-0.6.0.dev229 → specsmith-0.6.0.dev231}/src/specsmith/gui/widgets/provider_bar.py +0 -0
- {specsmith-0.6.0.dev229 → specsmith-0.6.0.dev231}/src/specsmith/gui/widgets/token_meter.py +0 -0
- {specsmith-0.6.0.dev229 → specsmith-0.6.0.dev231}/src/specsmith/gui/widgets/tool_panel.py +0 -0
- {specsmith-0.6.0.dev229 → specsmith-0.6.0.dev231}/src/specsmith/gui/widgets/update_checker.py +0 -0
- {specsmith-0.6.0.dev229 → specsmith-0.6.0.dev231}/src/specsmith/gui/worker.py +0 -0
- {specsmith-0.6.0.dev229 → specsmith-0.6.0.dev231}/src/specsmith/importer.py +0 -0
- {specsmith-0.6.0.dev229 → specsmith-0.6.0.dev231}/src/specsmith/integrations/__init__.py +0 -0
- {specsmith-0.6.0.dev229 → specsmith-0.6.0.dev231}/src/specsmith/integrations/agent_skill.py +0 -0
- {specsmith-0.6.0.dev229 → specsmith-0.6.0.dev231}/src/specsmith/integrations/aider.py +0 -0
- {specsmith-0.6.0.dev229 → specsmith-0.6.0.dev231}/src/specsmith/integrations/base.py +0 -0
- {specsmith-0.6.0.dev229 → specsmith-0.6.0.dev231}/src/specsmith/integrations/claude_code.py +0 -0
- {specsmith-0.6.0.dev229 → specsmith-0.6.0.dev231}/src/specsmith/integrations/copilot.py +0 -0
- {specsmith-0.6.0.dev229 → specsmith-0.6.0.dev231}/src/specsmith/integrations/cursor.py +0 -0
- {specsmith-0.6.0.dev229 → specsmith-0.6.0.dev231}/src/specsmith/integrations/gemini.py +0 -0
- {specsmith-0.6.0.dev229 → specsmith-0.6.0.dev231}/src/specsmith/integrations/windsurf.py +0 -0
- {specsmith-0.6.0.dev229 → specsmith-0.6.0.dev231}/src/specsmith/languages.py +0 -0
- {specsmith-0.6.0.dev229 → specsmith-0.6.0.dev231}/src/specsmith/ledger.py +0 -0
- {specsmith-0.6.0.dev229 → specsmith-0.6.0.dev231}/src/specsmith/patent.py +0 -0
- {specsmith-0.6.0.dev229 → specsmith-0.6.0.dev231}/src/specsmith/phase.py +0 -0
- {specsmith-0.6.0.dev229 → specsmith-0.6.0.dev231}/src/specsmith/plugins.py +0 -0
- {specsmith-0.6.0.dev229 → specsmith-0.6.0.dev231}/src/specsmith/profiles.py +0 -0
- {specsmith-0.6.0.dev229 → specsmith-0.6.0.dev231}/src/specsmith/rate_limits.py +0 -0
- {specsmith-0.6.0.dev229 → specsmith-0.6.0.dev231}/src/specsmith/releaser.py +0 -0
- {specsmith-0.6.0.dev229 → specsmith-0.6.0.dev231}/src/specsmith/requirements.py +0 -0
- {specsmith-0.6.0.dev229 → specsmith-0.6.0.dev231}/src/specsmith/requirements_parser.py +0 -0
- {specsmith-0.6.0.dev229 → specsmith-0.6.0.dev231}/src/specsmith/retrieval.py +0 -0
- {specsmith-0.6.0.dev229 → specsmith-0.6.0.dev231}/src/specsmith/scaffolder.py +0 -0
- {specsmith-0.6.0.dev229 → specsmith-0.6.0.dev231}/src/specsmith/serve.py +0 -0
- {specsmith-0.6.0.dev229 → specsmith-0.6.0.dev231}/src/specsmith/session.py +0 -0
- {specsmith-0.6.0.dev229 → specsmith-0.6.0.dev231}/src/specsmith/skills.py +0 -0
- {specsmith-0.6.0.dev229 → specsmith-0.6.0.dev231}/src/specsmith/templates/agents.md.j2 +0 -0
- {specsmith-0.6.0.dev229 → specsmith-0.6.0.dev231}/src/specsmith/templates/community/bug_report.md.j2 +0 -0
- {specsmith-0.6.0.dev229 → specsmith-0.6.0.dev231}/src/specsmith/templates/community/code_of_conduct.md.j2 +0 -0
- {specsmith-0.6.0.dev229 → specsmith-0.6.0.dev231}/src/specsmith/templates/community/contributing.md.j2 +0 -0
- {specsmith-0.6.0.dev229 → specsmith-0.6.0.dev231}/src/specsmith/templates/community/feature_request.md.j2 +0 -0
- {specsmith-0.6.0.dev229 → specsmith-0.6.0.dev231}/src/specsmith/templates/community/license-Apache-2.0.j2 +0 -0
- {specsmith-0.6.0.dev229 → specsmith-0.6.0.dev231}/src/specsmith/templates/community/license-MIT.j2 +0 -0
- {specsmith-0.6.0.dev229 → specsmith-0.6.0.dev231}/src/specsmith/templates/community/pull_request_template.md.j2 +0 -0
- {specsmith-0.6.0.dev229 → specsmith-0.6.0.dev231}/src/specsmith/templates/community/security.md.j2 +0 -0
- {specsmith-0.6.0.dev229 → specsmith-0.6.0.dev231}/src/specsmith/templates/docs/architecture.md.j2 +0 -0
- {specsmith-0.6.0.dev229 → specsmith-0.6.0.dev231}/src/specsmith/templates/docs/mkdocs.yml.j2 +0 -0
- {specsmith-0.6.0.dev229 → specsmith-0.6.0.dev231}/src/specsmith/templates/docs/readthedocs.yaml.j2 +0 -0
- {specsmith-0.6.0.dev229 → specsmith-0.6.0.dev231}/src/specsmith/templates/docs/requirements.md.j2 +0 -0
- {specsmith-0.6.0.dev229 → specsmith-0.6.0.dev231}/src/specsmith/templates/docs/test-spec.md.j2 +0 -0
- {specsmith-0.6.0.dev229 → specsmith-0.6.0.dev231}/src/specsmith/templates/editorconfig.j2 +0 -0
- {specsmith-0.6.0.dev229 → specsmith-0.6.0.dev231}/src/specsmith/templates/gitattributes.j2 +0 -0
- {specsmith-0.6.0.dev229 → specsmith-0.6.0.dev231}/src/specsmith/templates/gitignore.j2 +0 -0
- {specsmith-0.6.0.dev229 → specsmith-0.6.0.dev231}/src/specsmith/templates/go/go.mod.j2 +0 -0
- {specsmith-0.6.0.dev229 → specsmith-0.6.0.dev231}/src/specsmith/templates/go/main.go.j2 +0 -0
- {specsmith-0.6.0.dev229 → specsmith-0.6.0.dev231}/src/specsmith/templates/governance/belief-registry.md.j2 +0 -0
- {specsmith-0.6.0.dev229 → specsmith-0.6.0.dev231}/src/specsmith/templates/governance/context-budget.md.j2 +0 -0
- {specsmith-0.6.0.dev229 → specsmith-0.6.0.dev231}/src/specsmith/templates/governance/drift-metrics.md.j2 +0 -0
- {specsmith-0.6.0.dev229 → specsmith-0.6.0.dev231}/src/specsmith/templates/governance/epistemic-axioms.md.j2 +0 -0
- {specsmith-0.6.0.dev229 → specsmith-0.6.0.dev231}/src/specsmith/templates/governance/failure-modes.md.j2 +0 -0
- {specsmith-0.6.0.dev229 → specsmith-0.6.0.dev231}/src/specsmith/templates/governance/lifecycle.md.j2 +0 -0
- {specsmith-0.6.0.dev229 → specsmith-0.6.0.dev231}/src/specsmith/templates/governance/roles.md.j2 +0 -0
- {specsmith-0.6.0.dev229 → specsmith-0.6.0.dev231}/src/specsmith/templates/governance/rules.md.j2 +0 -0
- {specsmith-0.6.0.dev229 → specsmith-0.6.0.dev231}/src/specsmith/templates/governance/session-protocol.md.j2 +0 -0
- {specsmith-0.6.0.dev229 → specsmith-0.6.0.dev231}/src/specsmith/templates/governance/uncertainty-map.md.j2 +0 -0
- {specsmith-0.6.0.dev229 → specsmith-0.6.0.dev231}/src/specsmith/templates/governance/verification.md.j2 +0 -0
- {specsmith-0.6.0.dev229 → specsmith-0.6.0.dev231}/src/specsmith/templates/js/package.json.j2 +0 -0
- {specsmith-0.6.0.dev229 → specsmith-0.6.0.dev231}/src/specsmith/templates/ledger.md.j2 +0 -0
- {specsmith-0.6.0.dev229 → specsmith-0.6.0.dev231}/src/specsmith/templates/python/cli.py.j2 +0 -0
- {specsmith-0.6.0.dev229 → specsmith-0.6.0.dev231}/src/specsmith/templates/python/init.py.j2 +0 -0
- {specsmith-0.6.0.dev229 → specsmith-0.6.0.dev231}/src/specsmith/templates/python/pyproject.toml.j2 +0 -0
- {specsmith-0.6.0.dev229 → specsmith-0.6.0.dev231}/src/specsmith/templates/readme.md.j2 +0 -0
- {specsmith-0.6.0.dev229 → specsmith-0.6.0.dev231}/src/specsmith/templates/rust/Cargo.toml.j2 +0 -0
- {specsmith-0.6.0.dev229 → specsmith-0.6.0.dev231}/src/specsmith/templates/rust/main.rs.j2 +0 -0
- {specsmith-0.6.0.dev229 → specsmith-0.6.0.dev231}/src/specsmith/templates/scripts/exec.cmd.j2 +0 -0
- {specsmith-0.6.0.dev229 → specsmith-0.6.0.dev231}/src/specsmith/templates/scripts/exec.sh.j2 +0 -0
- {specsmith-0.6.0.dev229 → specsmith-0.6.0.dev231}/src/specsmith/templates/scripts/run.cmd.j2 +0 -0
- {specsmith-0.6.0.dev229 → specsmith-0.6.0.dev231}/src/specsmith/templates/scripts/run.sh.j2 +0 -0
- {specsmith-0.6.0.dev229 → specsmith-0.6.0.dev231}/src/specsmith/templates/scripts/setup.cmd.j2 +0 -0
- {specsmith-0.6.0.dev229 → specsmith-0.6.0.dev231}/src/specsmith/templates/scripts/setup.sh.j2 +0 -0
- {specsmith-0.6.0.dev229 → specsmith-0.6.0.dev231}/src/specsmith/templates/workflows/release.yml.j2 +0 -0
- {specsmith-0.6.0.dev229 → specsmith-0.6.0.dev231}/src/specsmith/tool_installer.py +0 -0
- {specsmith-0.6.0.dev229 → specsmith-0.6.0.dev231}/src/specsmith/toolrules.py +0 -0
- {specsmith-0.6.0.dev229 → specsmith-0.6.0.dev231}/src/specsmith/tools.py +0 -0
- {specsmith-0.6.0.dev229 → specsmith-0.6.0.dev231}/src/specsmith/trace.py +0 -0
- {specsmith-0.6.0.dev229 → specsmith-0.6.0.dev231}/src/specsmith/updater.py +0 -0
- {specsmith-0.6.0.dev229 → specsmith-0.6.0.dev231}/src/specsmith/upgrader.py +0 -0
- {specsmith-0.6.0.dev229 → specsmith-0.6.0.dev231}/src/specsmith/validator.py +0 -0
- {specsmith-0.6.0.dev229 → specsmith-0.6.0.dev231}/src/specsmith/vcs/__init__.py +0 -0
- {specsmith-0.6.0.dev229 → specsmith-0.6.0.dev231}/src/specsmith/vcs/base.py +0 -0
- {specsmith-0.6.0.dev229 → specsmith-0.6.0.dev231}/src/specsmith/vcs/bitbucket.py +0 -0
- {specsmith-0.6.0.dev229 → specsmith-0.6.0.dev231}/src/specsmith/vcs/github.py +0 -0
- {specsmith-0.6.0.dev229 → specsmith-0.6.0.dev231}/src/specsmith/vcs/gitlab.py +0 -0
- {specsmith-0.6.0.dev229 → specsmith-0.6.0.dev231}/src/specsmith/vcs_commands.py +0 -0
- {specsmith-0.6.0.dev229 → specsmith-0.6.0.dev231}/src/specsmith/wireframes.py +0 -0
- {specsmith-0.6.0.dev229 → specsmith-0.6.0.dev231}/src/specsmith/workspace.py +0 -0
- {specsmith-0.6.0.dev229 → specsmith-0.6.0.dev231}/src/specsmith.egg-info/dependency_links.txt +0 -0
- {specsmith-0.6.0.dev229 → specsmith-0.6.0.dev231}/src/specsmith.egg-info/entry_points.txt +0 -0
- {specsmith-0.6.0.dev229 → specsmith-0.6.0.dev231}/src/specsmith.egg-info/requires.txt +0 -0
- {specsmith-0.6.0.dev229 → specsmith-0.6.0.dev231}/src/specsmith.egg-info/top_level.txt +0 -0
- {specsmith-0.6.0.dev229 → specsmith-0.6.0.dev231}/tests/test_CMD_001.py +0 -0
- {specsmith-0.6.0.dev229 → specsmith-0.6.0.dev231}/tests/test_auditor.py +0 -0
- {specsmith-0.6.0.dev229 → specsmith-0.6.0.dev231}/tests/test_chat_diff_decision.py +0 -0
- {specsmith-0.6.0.dev229 → specsmith-0.6.0.dev231}/tests/test_chat_stdin_protocol.py +0 -0
- {specsmith-0.6.0.dev229 → specsmith-0.6.0.dev231}/tests/test_cli.py +0 -0
- {specsmith-0.6.0.dev229 → specsmith-0.6.0.dev231}/tests/test_cli_workflows_history_drive.py +0 -0
- {specsmith-0.6.0.dev229 → specsmith-0.6.0.dev231}/tests/test_compressor.py +0 -0
- {specsmith-0.6.0.dev229 → specsmith-0.6.0.dev231}/tests/test_e2e_nexus.py +0 -0
- {specsmith-0.6.0.dev229 → specsmith-0.6.0.dev231}/tests/test_epistemic.py +0 -0
- {specsmith-0.6.0.dev229 → specsmith-0.6.0.dev231}/tests/test_importer.py +0 -0
- {specsmith-0.6.0.dev229 → specsmith-0.6.0.dev231}/tests/test_integrations.py +0 -0
- {specsmith-0.6.0.dev229 → specsmith-0.6.0.dev231}/tests/test_nexus.py +0 -0
- {specsmith-0.6.0.dev229 → specsmith-0.6.0.dev231}/tests/test_phase1_4_new.py +0 -0
- {specsmith-0.6.0.dev229 → specsmith-0.6.0.dev231}/tests/test_rate_limits.py +0 -0
- {specsmith-0.6.0.dev229 → specsmith-0.6.0.dev231}/tests/test_scaffolder.py +0 -0
- {specsmith-0.6.0.dev229 → specsmith-0.6.0.dev231}/tests/test_skill_marketplace.py +0 -0
- {specsmith-0.6.0.dev229 → specsmith-0.6.0.dev231}/tests/test_smoke.py +0 -0
- {specsmith-0.6.0.dev229 → specsmith-0.6.0.dev231}/tests/test_tools.py +0 -0
- {specsmith-0.6.0.dev229 → specsmith-0.6.0.dev231}/tests/test_validator.py +0 -0
- {specsmith-0.6.0.dev229 → specsmith-0.6.0.dev231}/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.dev231
|
|
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.6.0.
|
|
7
|
+
version = "0.6.0.dev231"
|
|
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,387 @@
|
|
|
1
|
+
# SPDX-License-Identifier: MIT
|
|
2
|
+
# Copyright (c) 2026 BitConcepts, LLC. All rights reserved.
|
|
3
|
+
"""Real MCP (Model Context Protocol) client for Nexus (REQ-121, REQ-130).
|
|
4
|
+
|
|
5
|
+
Replaces the prior loader-only stub with a working JSON-RPC 2.0 client
|
|
6
|
+
that drives the official MCP handshake over stdio:
|
|
7
|
+
|
|
8
|
+
* ``initialize`` request -> response (capability negotiation).
|
|
9
|
+
* ``notifications/initialized`` notification.
|
|
10
|
+
* ``tools/list`` request -> response (tool catalog discovery).
|
|
11
|
+
* ``tools/call`` requests -> responses (per-tool invocation).
|
|
12
|
+
|
|
13
|
+
The Specsmith safety middleware still wraps every call: see
|
|
14
|
+
``MCPTool.invoke_with_safety``. Servers configured via ``.specsmith/mcp.yml``
|
|
15
|
+
are listed at the top of every ``specsmith chat`` session and exposed to
|
|
16
|
+
the orchestrator as additional Nexus tools.
|
|
17
|
+
|
|
18
|
+
Protocol pin: 2024-11-05 (current stable). Servers that advertise a newer
|
|
19
|
+
version still work because MCP guarantees backwards compatibility within
|
|
20
|
+
the same major track.
|
|
21
|
+
"""
|
|
22
|
+
|
|
23
|
+
from __future__ import annotations
|
|
24
|
+
|
|
25
|
+
import contextlib
|
|
26
|
+
import json
|
|
27
|
+
import subprocess
|
|
28
|
+
import threading
|
|
29
|
+
import time
|
|
30
|
+
from collections.abc import Callable
|
|
31
|
+
from dataclasses import dataclass, field
|
|
32
|
+
from pathlib import Path
|
|
33
|
+
from typing import Any
|
|
34
|
+
|
|
35
|
+
MCP_PROTOCOL_VERSION = "2024-11-05"
|
|
36
|
+
DEFAULT_REQUEST_TIMEOUT = 30.0
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
@dataclass
|
|
40
|
+
class MCPServerSpec:
|
|
41
|
+
"""Static configuration for an MCP server (mirrors `.specsmith/mcp.yml`)."""
|
|
42
|
+
|
|
43
|
+
name: str
|
|
44
|
+
command: str
|
|
45
|
+
args: list[str] = field(default_factory=list)
|
|
46
|
+
env: dict[str, str] = field(default_factory=dict)
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
@dataclass
|
|
50
|
+
class MCPToolDescriptor:
|
|
51
|
+
"""One tool advertised by an MCP server's ``tools/list`` response."""
|
|
52
|
+
|
|
53
|
+
name: str
|
|
54
|
+
description: str
|
|
55
|
+
input_schema: dict[str, Any]
|
|
56
|
+
server_name: str
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
class MCPError(RuntimeError):
|
|
60
|
+
"""Raised on transport or JSON-RPC errors from an MCP server."""
|
|
61
|
+
|
|
62
|
+
def __init__(self, *, code: int, message: str, data: Any = None) -> None:
|
|
63
|
+
super().__init__(f"MCP error {code}: {message}")
|
|
64
|
+
self.code = code
|
|
65
|
+
self.detail = message
|
|
66
|
+
self.data = data
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
class MCPSession:
|
|
70
|
+
"""One stdio-attached MCP server with full JSON-RPC framing.
|
|
71
|
+
|
|
72
|
+
The session owns the subprocess lifecycle. ``open()`` performs the
|
|
73
|
+
initialize handshake and discovery; ``call_tool()`` drives ``tools/call``;
|
|
74
|
+
``close()`` flushes pending requests and terminates the child.
|
|
75
|
+
Concurrent calls into a single session are not supported (one in-flight
|
|
76
|
+
request at a time, matching the stdio MCP transport model).
|
|
77
|
+
"""
|
|
78
|
+
|
|
79
|
+
def __init__(self, spec: MCPServerSpec) -> None:
|
|
80
|
+
self.spec = spec
|
|
81
|
+
self._proc: subprocess.Popen[bytes] | None = None
|
|
82
|
+
self._next_id = 1
|
|
83
|
+
self._lock = threading.Lock()
|
|
84
|
+
self._tools: list[MCPToolDescriptor] = []
|
|
85
|
+
self._closed = False
|
|
86
|
+
|
|
87
|
+
# ── Lifecycle ─────────────────────────────────────────────────────────
|
|
88
|
+
|
|
89
|
+
def open(self, *, timeout: float = DEFAULT_REQUEST_TIMEOUT) -> list[MCPToolDescriptor]:
|
|
90
|
+
"""Spawn the server, run the initialize handshake, return discovered tools."""
|
|
91
|
+
env = {**self.spec.env}
|
|
92
|
+
self._proc = subprocess.Popen( # noqa: S603 - argv is user-configured
|
|
93
|
+
[self.spec.command, *self.spec.args],
|
|
94
|
+
stdin=subprocess.PIPE,
|
|
95
|
+
stdout=subprocess.PIPE,
|
|
96
|
+
stderr=subprocess.PIPE,
|
|
97
|
+
env=env or None,
|
|
98
|
+
bufsize=0,
|
|
99
|
+
)
|
|
100
|
+
self._request(
|
|
101
|
+
"initialize",
|
|
102
|
+
{
|
|
103
|
+
"protocolVersion": MCP_PROTOCOL_VERSION,
|
|
104
|
+
"capabilities": {"tools": {}},
|
|
105
|
+
"clientInfo": {"name": "specsmith", "version": "0"},
|
|
106
|
+
},
|
|
107
|
+
timeout=timeout,
|
|
108
|
+
)
|
|
109
|
+
# Per spec: send notifications/initialized after a successful initialize.
|
|
110
|
+
self._notify("notifications/initialized", {})
|
|
111
|
+
result = self._request("tools/list", {}, timeout=timeout)
|
|
112
|
+
raw_tools = result.get("tools", []) if isinstance(result, dict) else []
|
|
113
|
+
self._tools = []
|
|
114
|
+
for entry in raw_tools:
|
|
115
|
+
if not isinstance(entry, dict):
|
|
116
|
+
continue
|
|
117
|
+
name = entry.get("name")
|
|
118
|
+
if not name:
|
|
119
|
+
continue
|
|
120
|
+
schema = entry.get("inputSchema", {})
|
|
121
|
+
self._tools.append(
|
|
122
|
+
MCPToolDescriptor(
|
|
123
|
+
name=str(name),
|
|
124
|
+
description=str(entry.get("description", "")),
|
|
125
|
+
input_schema=schema if isinstance(schema, dict) else {},
|
|
126
|
+
server_name=self.spec.name,
|
|
127
|
+
)
|
|
128
|
+
)
|
|
129
|
+
return list(self._tools)
|
|
130
|
+
|
|
131
|
+
def close(self) -> None:
|
|
132
|
+
"""Terminate the server. Idempotent."""
|
|
133
|
+
if self._closed:
|
|
134
|
+
return
|
|
135
|
+
self._closed = True
|
|
136
|
+
if self._proc is None:
|
|
137
|
+
return
|
|
138
|
+
try:
|
|
139
|
+
if self._proc.stdin and not self._proc.stdin.closed:
|
|
140
|
+
self._proc.stdin.close()
|
|
141
|
+
except OSError:
|
|
142
|
+
pass
|
|
143
|
+
try:
|
|
144
|
+
self._proc.terminate()
|
|
145
|
+
self._proc.wait(timeout=2.0)
|
|
146
|
+
except (OSError, subprocess.TimeoutExpired):
|
|
147
|
+
with contextlib.suppress(OSError):
|
|
148
|
+
self._proc.kill()
|
|
149
|
+
|
|
150
|
+
# ── Public API ────────────────────────────────────────────────────────
|
|
151
|
+
|
|
152
|
+
@property
|
|
153
|
+
def tools(self) -> list[MCPToolDescriptor]:
|
|
154
|
+
"""Return the catalog discovered during ``open()``."""
|
|
155
|
+
return list(self._tools)
|
|
156
|
+
|
|
157
|
+
def call_tool(
|
|
158
|
+
self,
|
|
159
|
+
name: str,
|
|
160
|
+
arguments: dict[str, Any] | None = None,
|
|
161
|
+
*,
|
|
162
|
+
timeout: float = DEFAULT_REQUEST_TIMEOUT,
|
|
163
|
+
) -> str:
|
|
164
|
+
"""Invoke ``tools/call`` and return a flat string result.
|
|
165
|
+
|
|
166
|
+
MCP returns content blocks; we concatenate text blocks and report
|
|
167
|
+
non-text blocks descriptively so downstream consumers can render a
|
|
168
|
+
single string.
|
|
169
|
+
"""
|
|
170
|
+
params: dict[str, Any] = {"name": name}
|
|
171
|
+
if arguments:
|
|
172
|
+
params["arguments"] = arguments
|
|
173
|
+
result = self._request("tools/call", params, timeout=timeout)
|
|
174
|
+
if not isinstance(result, dict):
|
|
175
|
+
return str(result)
|
|
176
|
+
if result.get("isError"):
|
|
177
|
+
return f"mcp error: {_format_content(result.get('content', []))}"
|
|
178
|
+
return _format_content(result.get("content", []))
|
|
179
|
+
|
|
180
|
+
# ── JSON-RPC framing ──────────────────────────────────────────────────
|
|
181
|
+
|
|
182
|
+
def _request(
|
|
183
|
+
self,
|
|
184
|
+
method: str,
|
|
185
|
+
params: dict[str, Any],
|
|
186
|
+
*,
|
|
187
|
+
timeout: float,
|
|
188
|
+
) -> Any:
|
|
189
|
+
with self._lock:
|
|
190
|
+
req_id = self._next_id
|
|
191
|
+
self._next_id += 1
|
|
192
|
+
self._send({"jsonrpc": "2.0", "id": req_id, "method": method, "params": params})
|
|
193
|
+
response = self._read_response_for(req_id, timeout)
|
|
194
|
+
if "error" in response:
|
|
195
|
+
err = response["error"]
|
|
196
|
+
raise MCPError(
|
|
197
|
+
code=int(err.get("code", -1)),
|
|
198
|
+
message=str(err.get("message", "(no message)")),
|
|
199
|
+
data=err.get("data"),
|
|
200
|
+
)
|
|
201
|
+
return response.get("result", {})
|
|
202
|
+
|
|
203
|
+
def _notify(self, method: str, params: dict[str, Any]) -> None:
|
|
204
|
+
with self._lock:
|
|
205
|
+
self._send({"jsonrpc": "2.0", "method": method, "params": params})
|
|
206
|
+
|
|
207
|
+
def _send(self, payload: dict[str, Any]) -> None:
|
|
208
|
+
if self._proc is None or self._proc.stdin is None:
|
|
209
|
+
raise MCPError(code=-32000, message="server not open")
|
|
210
|
+
line = (json.dumps(payload, ensure_ascii=False) + "\n").encode("utf-8")
|
|
211
|
+
try:
|
|
212
|
+
self._proc.stdin.write(line)
|
|
213
|
+
self._proc.stdin.flush()
|
|
214
|
+
except (OSError, BrokenPipeError) as exc:
|
|
215
|
+
raise MCPError(code=-32001, message=f"send failed: {exc}") from exc
|
|
216
|
+
|
|
217
|
+
def _read_response_for(self, req_id: int, timeout: float) -> dict[str, Any]:
|
|
218
|
+
if self._proc is None or self._proc.stdout is None:
|
|
219
|
+
raise MCPError(code=-32000, message="server not open")
|
|
220
|
+
deadline = time.monotonic() + timeout
|
|
221
|
+
while time.monotonic() < deadline:
|
|
222
|
+
line = self._proc.stdout.readline()
|
|
223
|
+
if not line:
|
|
224
|
+
stderr_tail = b""
|
|
225
|
+
if self._proc.stderr is not None:
|
|
226
|
+
try:
|
|
227
|
+
stderr_tail = self._proc.stderr.read() or b""
|
|
228
|
+
except OSError:
|
|
229
|
+
stderr_tail = b""
|
|
230
|
+
raise MCPError(
|
|
231
|
+
code=-32002,
|
|
232
|
+
message=f"mcp server closed: {stderr_tail.decode('utf-8', 'replace').strip()}",
|
|
233
|
+
)
|
|
234
|
+
try:
|
|
235
|
+
msg = json.loads(line.decode("utf-8", "replace"))
|
|
236
|
+
except ValueError:
|
|
237
|
+
continue
|
|
238
|
+
if not isinstance(msg, dict):
|
|
239
|
+
continue
|
|
240
|
+
if msg.get("id") == req_id:
|
|
241
|
+
return msg
|
|
242
|
+
raise MCPError(code=-32003, message=f"timeout waiting for response to id={req_id}")
|
|
243
|
+
|
|
244
|
+
|
|
245
|
+
def _format_content(blocks: Any) -> str:
|
|
246
|
+
"""Concatenate MCP content blocks into a single human-readable string."""
|
|
247
|
+
if not isinstance(blocks, list):
|
|
248
|
+
return str(blocks)
|
|
249
|
+
parts: list[str] = []
|
|
250
|
+
for block in blocks:
|
|
251
|
+
if not isinstance(block, dict):
|
|
252
|
+
continue
|
|
253
|
+
kind = block.get("type", "")
|
|
254
|
+
if kind == "text":
|
|
255
|
+
parts.append(str(block.get("text", "")))
|
|
256
|
+
elif kind == "image":
|
|
257
|
+
parts.append(f"[image: {block.get('mimeType', 'unknown')}]")
|
|
258
|
+
elif kind == "resource":
|
|
259
|
+
uri = (block.get("resource") or {}).get("uri", "?")
|
|
260
|
+
parts.append(f"[resource: {uri}]")
|
|
261
|
+
else:
|
|
262
|
+
parts.append(f"[unknown block: {kind}]")
|
|
263
|
+
return "\n".join(parts) if parts else "(empty mcp response)"
|
|
264
|
+
|
|
265
|
+
|
|
266
|
+
@dataclass
|
|
267
|
+
class MCPTool:
|
|
268
|
+
"""A Nexus-side handle that wraps one descriptor + an open session."""
|
|
269
|
+
|
|
270
|
+
descriptor: MCPToolDescriptor
|
|
271
|
+
session: MCPSession
|
|
272
|
+
|
|
273
|
+
@property
|
|
274
|
+
def name(self) -> str:
|
|
275
|
+
return self.descriptor.name
|
|
276
|
+
|
|
277
|
+
@property
|
|
278
|
+
def server(self) -> str:
|
|
279
|
+
return self.descriptor.server_name
|
|
280
|
+
|
|
281
|
+
@property
|
|
282
|
+
def description(self) -> str:
|
|
283
|
+
return self.descriptor.description
|
|
284
|
+
|
|
285
|
+
@property
|
|
286
|
+
def spec(self) -> MCPServerSpec:
|
|
287
|
+
"""Back-compat shim — older callers expect a `.spec` attribute."""
|
|
288
|
+
return self.session.spec
|
|
289
|
+
|
|
290
|
+
def invoke(self, arguments: dict[str, Any] | None = None) -> str:
|
|
291
|
+
"""Direct invocation (no safety middleware)."""
|
|
292
|
+
return self.session.call_tool(self.descriptor.name, arguments)
|
|
293
|
+
|
|
294
|
+
def invoke_with_safety(
|
|
295
|
+
self,
|
|
296
|
+
arguments: dict[str, Any] | None,
|
|
297
|
+
safety_check: Callable[[str, dict[str, Any]], tuple[bool, str]] | None,
|
|
298
|
+
) -> str:
|
|
299
|
+
"""Invoke after running the supplied safety check.
|
|
300
|
+
|
|
301
|
+
The check returns ``(allowed, reason)``. When disallowed, the call
|
|
302
|
+
is not made and a redacted error string is returned.
|
|
303
|
+
"""
|
|
304
|
+
if safety_check is not None:
|
|
305
|
+
allowed, reason = safety_check(self.descriptor.name, arguments or {})
|
|
306
|
+
if not allowed:
|
|
307
|
+
return f"mcp blocked by safety: {reason}"
|
|
308
|
+
return self.invoke(arguments or None)
|
|
309
|
+
|
|
310
|
+
|
|
311
|
+
# ── Loader-style helpers (back-compat with prior callers) ────────────────
|
|
312
|
+
|
|
313
|
+
|
|
314
|
+
def _read_specs(project_dir: Path) -> list[MCPServerSpec]:
|
|
315
|
+
cfg_path = Path(project_dir) / ".specsmith" / "mcp.yml"
|
|
316
|
+
if not cfg_path.is_file():
|
|
317
|
+
return []
|
|
318
|
+
try:
|
|
319
|
+
import yaml
|
|
320
|
+
|
|
321
|
+
raw = yaml.safe_load(cfg_path.read_text(encoding="utf-8")) or []
|
|
322
|
+
except Exception: # noqa: BLE001
|
|
323
|
+
return []
|
|
324
|
+
if not isinstance(raw, list):
|
|
325
|
+
return []
|
|
326
|
+
out: list[MCPServerSpec] = []
|
|
327
|
+
for entry in raw:
|
|
328
|
+
if not isinstance(entry, dict):
|
|
329
|
+
continue
|
|
330
|
+
name = str(entry.get("name", "")).strip()
|
|
331
|
+
command = str(entry.get("command", "")).strip()
|
|
332
|
+
if not name or not command:
|
|
333
|
+
continue
|
|
334
|
+
args_raw = entry.get("args", []) or []
|
|
335
|
+
env_raw = entry.get("env", {}) or {}
|
|
336
|
+
out.append(
|
|
337
|
+
MCPServerSpec(
|
|
338
|
+
name=name,
|
|
339
|
+
command=command,
|
|
340
|
+
args=[str(a) for a in args_raw if isinstance(a, (str, int, float))],
|
|
341
|
+
env={str(k): str(v) for k, v in env_raw.items()},
|
|
342
|
+
)
|
|
343
|
+
)
|
|
344
|
+
return out
|
|
345
|
+
|
|
346
|
+
|
|
347
|
+
def load_mcp_tools(project_dir: Path) -> list[MCPTool]:
|
|
348
|
+
"""Open every configured MCP server and return its tools (back-compat).
|
|
349
|
+
|
|
350
|
+
Servers that fail to open are silently skipped. Returns an empty list
|
|
351
|
+
when no servers are configured. The underlying sessions remain open
|
|
352
|
+
until the process exits — convenient for one-shot scripts and tests.
|
|
353
|
+
Long-running consumers should prefer :func:`open_mcp_sessions` and
|
|
354
|
+
explicitly ``close()`` each session.
|
|
355
|
+
"""
|
|
356
|
+
sessions = open_mcp_sessions(project_dir)
|
|
357
|
+
out: list[MCPTool] = []
|
|
358
|
+
for session in sessions:
|
|
359
|
+
for descriptor in session.tools:
|
|
360
|
+
out.append(MCPTool(descriptor=descriptor, session=session))
|
|
361
|
+
return out
|
|
362
|
+
|
|
363
|
+
|
|
364
|
+
def open_mcp_sessions(project_dir: Path) -> list[MCPSession]:
|
|
365
|
+
"""Open all configured MCP sessions and return them. Caller owns close."""
|
|
366
|
+
out: list[MCPSession] = []
|
|
367
|
+
for spec in _read_specs(project_dir):
|
|
368
|
+
session = MCPSession(spec)
|
|
369
|
+
try:
|
|
370
|
+
session.open()
|
|
371
|
+
except (OSError, MCPError):
|
|
372
|
+
session.close()
|
|
373
|
+
continue
|
|
374
|
+
out.append(session)
|
|
375
|
+
return out
|
|
376
|
+
|
|
377
|
+
|
|
378
|
+
__all__ = [
|
|
379
|
+
"MCP_PROTOCOL_VERSION",
|
|
380
|
+
"MCPError",
|
|
381
|
+
"MCPServerSpec",
|
|
382
|
+
"MCPSession",
|
|
383
|
+
"MCPTool",
|
|
384
|
+
"MCPToolDescriptor",
|
|
385
|
+
"load_mcp_tools",
|
|
386
|
+
"open_mcp_sessions",
|
|
387
|
+
]
|
|
@@ -5262,6 +5262,7 @@ def chat_cmd(
|
|
|
5262
5262
|
import uuid as _uuid
|
|
5263
5263
|
|
|
5264
5264
|
from specsmith.agent.events import EventEmitter
|
|
5265
|
+
from specsmith.agent.mcp import load_mcp_tools
|
|
5265
5266
|
from specsmith.agent.memory import append_turn, recent_turns
|
|
5266
5267
|
from specsmith.agent.router import choose_tier
|
|
5267
5268
|
from specsmith.agent.rules import load_rules
|
|
@@ -5287,6 +5288,21 @@ def chat_cmd(
|
|
|
5287
5288
|
if rules_prefix:
|
|
5288
5289
|
emitter.token(msg_block, "[project rules loaded]\n")
|
|
5289
5290
|
|
|
5291
|
+
# Surface configured MCP servers (REQ-121, REQ-130). The real client
|
|
5292
|
+
# opens each server, runs the initialize handshake, and discovers its
|
|
5293
|
+
# tools; the safety middleware still gates every actual invocation.
|
|
5294
|
+
# Here we just announce availability so consumers can render the list.
|
|
5295
|
+
mcp_tools = load_mcp_tools(root)
|
|
5296
|
+
if mcp_tools:
|
|
5297
|
+
servers: dict[str, list[str]] = {}
|
|
5298
|
+
for tool in mcp_tools:
|
|
5299
|
+
servers.setdefault(tool.server, []).append(tool.name)
|
|
5300
|
+
summary = ", ".join(f"{srv} ({len(names)})" for srv, names in servers.items())
|
|
5301
|
+
emitter.token(
|
|
5302
|
+
msg_block,
|
|
5303
|
+
f"[mcp: {len(mcp_tools)} tool(s) across {len(servers)} server(s): {summary}]\n",
|
|
5304
|
+
)
|
|
5305
|
+
|
|
5290
5306
|
# Pick a tier (REQ-122) so consumers know which model is in play.
|
|
5291
5307
|
_utt_lower = utterance.lower()
|
|
5292
5308
|
if any(k in _utt_lower for k in ("add", "fix", "refactor")):
|
|
@@ -5545,13 +5561,27 @@ def notebook_group() -> None:
|
|
|
5545
5561
|
default="",
|
|
5546
5562
|
help="Work item id whose .specsmith/runs/<WI>/ artifacts should be captured.",
|
|
5547
5563
|
)
|
|
5548
|
-
|
|
5564
|
+
@click.option(
|
|
5565
|
+
"--session-id",
|
|
5566
|
+
"session_id",
|
|
5567
|
+
default="",
|
|
5568
|
+
help="Session id whose .specsmith/sessions/<id>/turns.jsonl should be captured.",
|
|
5569
|
+
)
|
|
5570
|
+
def notebook_record(slug: str, project_dir: str, work_item_id: str, session_id: str) -> None:
|
|
5549
5571
|
"""Record a notebook for the given SLUG (REQ-123).
|
|
5550
5572
|
|
|
5551
|
-
|
|
5552
|
-
|
|
5553
|
-
|
|
5573
|
+
Two artifact sources are supported and may be combined:
|
|
5574
|
+
|
|
5575
|
+
* ``--work-item-id`` reads `.specsmith/runs/<WI>/` (preflight/verify
|
|
5576
|
+
logs, decision.json, etc.).
|
|
5577
|
+
* ``--session-id`` reads `.specsmith/sessions/<id>/turns.jsonl` so a
|
|
5578
|
+
conversational chat session can be replayed later.
|
|
5579
|
+
|
|
5580
|
+
Either flag may be omitted; both may be combined to produce a single
|
|
5581
|
+
notebook that captures the full evidence trail.
|
|
5554
5582
|
"""
|
|
5583
|
+
import json as _json
|
|
5584
|
+
|
|
5555
5585
|
root = Path(project_dir).resolve()
|
|
5556
5586
|
nb_dir = root / "docs" / "notebooks"
|
|
5557
5587
|
nb_dir.mkdir(parents=True, exist_ok=True)
|
|
@@ -5559,10 +5589,15 @@ def notebook_record(slug: str, project_dir: str, work_item_id: str) -> None:
|
|
|
5559
5589
|
|
|
5560
5590
|
runs_dir = root / ".specsmith" / "runs"
|
|
5561
5591
|
artifact_dir = runs_dir / work_item_id if work_item_id else None
|
|
5562
|
-
sections: list[str] = [f"# Notebook
|
|
5592
|
+
sections: list[str] = [f"# Notebook \u2014 {slug}\n"]
|
|
5563
5593
|
if work_item_id:
|
|
5564
5594
|
sections.append(f"- **Work item**: `{work_item_id}`")
|
|
5595
|
+
if session_id:
|
|
5596
|
+
sections.append(f"- **Session**: `{session_id}`")
|
|
5597
|
+
|
|
5598
|
+
captured_any = False
|
|
5565
5599
|
if artifact_dir and artifact_dir.is_dir():
|
|
5600
|
+
captured_any = True
|
|
5566
5601
|
sections.append("\n## Captured artifacts\n")
|
|
5567
5602
|
for path in sorted(artifact_dir.rglob("*")):
|
|
5568
5603
|
if path.is_file():
|
|
@@ -5574,10 +5609,30 @@ def notebook_record(slug: str, project_dir: str, work_item_id: str) -> None:
|
|
|
5574
5609
|
continue
|
|
5575
5610
|
fence = "```"
|
|
5576
5611
|
sections.append(f"### `{rel}`\n\n{fence}\n{body}\n{fence}\n")
|
|
5577
|
-
|
|
5612
|
+
|
|
5613
|
+
if session_id:
|
|
5614
|
+
turns_path = root / ".specsmith" / "sessions" / session_id / "turns.jsonl"
|
|
5615
|
+
if turns_path.is_file():
|
|
5616
|
+
captured_any = True
|
|
5617
|
+
sections.append("\n## Session turns\n")
|
|
5618
|
+
for line in turns_path.read_text(encoding="utf-8").splitlines():
|
|
5619
|
+
line = line.strip()
|
|
5620
|
+
if not line:
|
|
5621
|
+
continue
|
|
5622
|
+
try:
|
|
5623
|
+
turn = _json.loads(line)
|
|
5624
|
+
except ValueError:
|
|
5625
|
+
continue
|
|
5626
|
+
role = str(turn.get("role", "?"))
|
|
5627
|
+
utterance = str(turn.get("utterance") or turn.get("text") or "").strip()
|
|
5628
|
+
ts = str(turn.get("timestamp", "")).strip()
|
|
5629
|
+
header = f"### `{role}`" + (f" \u2014 {ts}" if ts else "")
|
|
5630
|
+
sections.append(f"{header}\n\n{utterance}\n")
|
|
5631
|
+
|
|
5632
|
+
if not captured_any:
|
|
5578
5633
|
sections.append(
|
|
5579
|
-
"\n_No
|
|
5580
|
-
"
|
|
5634
|
+
"\n_No artifacts captured. Pass `--work-item-id <WI>` or "
|
|
5635
|
+
"`--session-id <id>` to populate this notebook._\n"
|
|
5581
5636
|
)
|
|
5582
5637
|
target.write_text("\n".join(sections), encoding="utf-8")
|
|
5583
5638
|
console.print(f"[green]\u2713[/green] Notebook recorded at {target.relative_to(root)}")
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: specsmith
|
|
3
|
-
Version: 0.6.0.
|
|
3
|
+
Version: 0.6.0.dev231
|
|
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
|
|
@@ -162,8 +162,10 @@ tests/test_e2e_nexus.py
|
|
|
162
162
|
tests/test_epistemic.py
|
|
163
163
|
tests/test_importer.py
|
|
164
164
|
tests/test_integrations.py
|
|
165
|
+
tests/test_mcp_client.py
|
|
165
166
|
tests/test_nexus.py
|
|
166
167
|
tests/test_phase1_4_new.py
|
|
168
|
+
tests/test_phase34_completion.py
|
|
167
169
|
tests/test_rate_limits.py
|
|
168
170
|
tests/test_scaffolder.py
|
|
169
171
|
tests/test_skill_marketplace.py
|