specsmith 0.6.0.dev230__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.
Files changed (178) hide show
  1. {specsmith-0.6.0.dev230/src/specsmith.egg-info → specsmith-0.6.0.dev231}/PKG-INFO +1 -1
  2. {specsmith-0.6.0.dev230 → specsmith-0.6.0.dev231}/pyproject.toml +1 -1
  3. specsmith-0.6.0.dev231/src/specsmith/agent/mcp.py +387 -0
  4. {specsmith-0.6.0.dev230 → specsmith-0.6.0.dev231}/src/specsmith/cli.py +12 -5
  5. {specsmith-0.6.0.dev230 → specsmith-0.6.0.dev231/src/specsmith.egg-info}/PKG-INFO +1 -1
  6. {specsmith-0.6.0.dev230 → specsmith-0.6.0.dev231}/src/specsmith.egg-info/SOURCES.txt +1 -0
  7. specsmith-0.6.0.dev231/tests/test_mcp_client.py +157 -0
  8. {specsmith-0.6.0.dev230 → specsmith-0.6.0.dev231}/tests/test_phase34_completion.py +29 -18
  9. specsmith-0.6.0.dev230/src/specsmith/agent/mcp.py +0 -117
  10. {specsmith-0.6.0.dev230 → specsmith-0.6.0.dev231}/LICENSE +0 -0
  11. {specsmith-0.6.0.dev230 → specsmith-0.6.0.dev231}/README.md +0 -0
  12. {specsmith-0.6.0.dev230 → specsmith-0.6.0.dev231}/setup.cfg +0 -0
  13. {specsmith-0.6.0.dev230 → specsmith-0.6.0.dev231}/src/epistemic/__init__.py +0 -0
  14. {specsmith-0.6.0.dev230 → specsmith-0.6.0.dev231}/src/epistemic/belief.py +0 -0
  15. {specsmith-0.6.0.dev230 → specsmith-0.6.0.dev231}/src/epistemic/certainty.py +0 -0
  16. {specsmith-0.6.0.dev230 → specsmith-0.6.0.dev231}/src/epistemic/failure_graph.py +0 -0
  17. {specsmith-0.6.0.dev230 → specsmith-0.6.0.dev231}/src/epistemic/py.typed +0 -0
  18. {specsmith-0.6.0.dev230 → specsmith-0.6.0.dev231}/src/epistemic/recovery.py +0 -0
  19. {specsmith-0.6.0.dev230 → specsmith-0.6.0.dev231}/src/epistemic/session.py +0 -0
  20. {specsmith-0.6.0.dev230 → specsmith-0.6.0.dev231}/src/epistemic/stress_tester.py +0 -0
  21. {specsmith-0.6.0.dev230 → specsmith-0.6.0.dev231}/src/epistemic/trace.py +0 -0
  22. {specsmith-0.6.0.dev230 → specsmith-0.6.0.dev231}/src/specsmith/__init__.py +0 -0
  23. {specsmith-0.6.0.dev230 → specsmith-0.6.0.dev231}/src/specsmith/__main__.py +0 -0
  24. {specsmith-0.6.0.dev230 → specsmith-0.6.0.dev231}/src/specsmith/agent/__init__.py +0 -0
  25. {specsmith-0.6.0.dev230 → specsmith-0.6.0.dev231}/src/specsmith/agent/broker.py +0 -0
  26. {specsmith-0.6.0.dev230 → specsmith-0.6.0.dev231}/src/specsmith/agent/chat_runner.py +0 -0
  27. {specsmith-0.6.0.dev230 → specsmith-0.6.0.dev231}/src/specsmith/agent/cleanup.py +0 -0
  28. {specsmith-0.6.0.dev230 → specsmith-0.6.0.dev231}/src/specsmith/agent/events.py +0 -0
  29. {specsmith-0.6.0.dev230 → specsmith-0.6.0.dev231}/src/specsmith/agent/indexer.py +0 -0
  30. {specsmith-0.6.0.dev230 → specsmith-0.6.0.dev231}/src/specsmith/agent/memory.py +0 -0
  31. {specsmith-0.6.0.dev230 → specsmith-0.6.0.dev231}/src/specsmith/agent/orchestrator.py +0 -0
  32. {specsmith-0.6.0.dev230 → specsmith-0.6.0.dev231}/src/specsmith/agent/repl.py +0 -0
  33. {specsmith-0.6.0.dev230 → specsmith-0.6.0.dev231}/src/specsmith/agent/router.py +0 -0
  34. {specsmith-0.6.0.dev230 → specsmith-0.6.0.dev231}/src/specsmith/agent/rules.py +0 -0
  35. {specsmith-0.6.0.dev230 → specsmith-0.6.0.dev231}/src/specsmith/agent/safety.py +0 -0
  36. {specsmith-0.6.0.dev230 → specsmith-0.6.0.dev231}/src/specsmith/agent/tools.py +0 -0
  37. {specsmith-0.6.0.dev230 → specsmith-0.6.0.dev231}/src/specsmith/agent/verifier.py +0 -0
  38. {specsmith-0.6.0.dev230 → specsmith-0.6.0.dev231}/src/specsmith/architect.py +0 -0
  39. {specsmith-0.6.0.dev230 → specsmith-0.6.0.dev231}/src/specsmith/auditor.py +0 -0
  40. {specsmith-0.6.0.dev230 → specsmith-0.6.0.dev231}/src/specsmith/auth.py +0 -0
  41. {specsmith-0.6.0.dev230 → specsmith-0.6.0.dev231}/src/specsmith/commands/__init__.py +0 -0
  42. {specsmith-0.6.0.dev230 → specsmith-0.6.0.dev231}/src/specsmith/compressor.py +0 -0
  43. {specsmith-0.6.0.dev230 → specsmith-0.6.0.dev231}/src/specsmith/config.py +0 -0
  44. {specsmith-0.6.0.dev230 → specsmith-0.6.0.dev231}/src/specsmith/console_utils.py +0 -0
  45. {specsmith-0.6.0.dev230 → specsmith-0.6.0.dev231}/src/specsmith/credit_analyzer.py +0 -0
  46. {specsmith-0.6.0.dev230 → specsmith-0.6.0.dev231}/src/specsmith/credits.py +0 -0
  47. {specsmith-0.6.0.dev230 → specsmith-0.6.0.dev231}/src/specsmith/differ.py +0 -0
  48. {specsmith-0.6.0.dev230 → specsmith-0.6.0.dev231}/src/specsmith/doctor.py +0 -0
  49. {specsmith-0.6.0.dev230 → specsmith-0.6.0.dev231}/src/specsmith/epistemic/__init__.py +0 -0
  50. {specsmith-0.6.0.dev230 → specsmith-0.6.0.dev231}/src/specsmith/epistemic/belief.py +0 -0
  51. {specsmith-0.6.0.dev230 → specsmith-0.6.0.dev231}/src/specsmith/epistemic/certainty.py +0 -0
  52. {specsmith-0.6.0.dev230 → specsmith-0.6.0.dev231}/src/specsmith/epistemic/failure_graph.py +0 -0
  53. {specsmith-0.6.0.dev230 → specsmith-0.6.0.dev231}/src/specsmith/epistemic/recovery.py +0 -0
  54. {specsmith-0.6.0.dev230 → specsmith-0.6.0.dev231}/src/specsmith/epistemic/stress_tester.py +0 -0
  55. {specsmith-0.6.0.dev230 → specsmith-0.6.0.dev231}/src/specsmith/executor.py +0 -0
  56. {specsmith-0.6.0.dev230 → specsmith-0.6.0.dev231}/src/specsmith/exporter.py +0 -0
  57. {specsmith-0.6.0.dev230 → specsmith-0.6.0.dev231}/src/specsmith/gui/__init__.py +0 -0
  58. {specsmith-0.6.0.dev230 → specsmith-0.6.0.dev231}/src/specsmith/gui/app.py +0 -0
  59. {specsmith-0.6.0.dev230 → specsmith-0.6.0.dev231}/src/specsmith/gui/main_window.py +0 -0
  60. {specsmith-0.6.0.dev230 → specsmith-0.6.0.dev231}/src/specsmith/gui/session_tab.py +0 -0
  61. {specsmith-0.6.0.dev230 → specsmith-0.6.0.dev231}/src/specsmith/gui/theme.py +0 -0
  62. {specsmith-0.6.0.dev230 → specsmith-0.6.0.dev231}/src/specsmith/gui/widgets/__init__.py +0 -0
  63. {specsmith-0.6.0.dev230 → specsmith-0.6.0.dev231}/src/specsmith/gui/widgets/chat_view.py +0 -0
  64. {specsmith-0.6.0.dev230 → specsmith-0.6.0.dev231}/src/specsmith/gui/widgets/input_bar.py +0 -0
  65. {specsmith-0.6.0.dev230 → specsmith-0.6.0.dev231}/src/specsmith/gui/widgets/provider_bar.py +0 -0
  66. {specsmith-0.6.0.dev230 → specsmith-0.6.0.dev231}/src/specsmith/gui/widgets/token_meter.py +0 -0
  67. {specsmith-0.6.0.dev230 → specsmith-0.6.0.dev231}/src/specsmith/gui/widgets/tool_panel.py +0 -0
  68. {specsmith-0.6.0.dev230 → specsmith-0.6.0.dev231}/src/specsmith/gui/widgets/update_checker.py +0 -0
  69. {specsmith-0.6.0.dev230 → specsmith-0.6.0.dev231}/src/specsmith/gui/worker.py +0 -0
  70. {specsmith-0.6.0.dev230 → specsmith-0.6.0.dev231}/src/specsmith/importer.py +0 -0
  71. {specsmith-0.6.0.dev230 → specsmith-0.6.0.dev231}/src/specsmith/integrations/__init__.py +0 -0
  72. {specsmith-0.6.0.dev230 → specsmith-0.6.0.dev231}/src/specsmith/integrations/agent_skill.py +0 -0
  73. {specsmith-0.6.0.dev230 → specsmith-0.6.0.dev231}/src/specsmith/integrations/aider.py +0 -0
  74. {specsmith-0.6.0.dev230 → specsmith-0.6.0.dev231}/src/specsmith/integrations/base.py +0 -0
  75. {specsmith-0.6.0.dev230 → specsmith-0.6.0.dev231}/src/specsmith/integrations/claude_code.py +0 -0
  76. {specsmith-0.6.0.dev230 → specsmith-0.6.0.dev231}/src/specsmith/integrations/copilot.py +0 -0
  77. {specsmith-0.6.0.dev230 → specsmith-0.6.0.dev231}/src/specsmith/integrations/cursor.py +0 -0
  78. {specsmith-0.6.0.dev230 → specsmith-0.6.0.dev231}/src/specsmith/integrations/gemini.py +0 -0
  79. {specsmith-0.6.0.dev230 → specsmith-0.6.0.dev231}/src/specsmith/integrations/windsurf.py +0 -0
  80. {specsmith-0.6.0.dev230 → specsmith-0.6.0.dev231}/src/specsmith/languages.py +0 -0
  81. {specsmith-0.6.0.dev230 → specsmith-0.6.0.dev231}/src/specsmith/ledger.py +0 -0
  82. {specsmith-0.6.0.dev230 → specsmith-0.6.0.dev231}/src/specsmith/patent.py +0 -0
  83. {specsmith-0.6.0.dev230 → specsmith-0.6.0.dev231}/src/specsmith/phase.py +0 -0
  84. {specsmith-0.6.0.dev230 → specsmith-0.6.0.dev231}/src/specsmith/plugins.py +0 -0
  85. {specsmith-0.6.0.dev230 → specsmith-0.6.0.dev231}/src/specsmith/profiles.py +0 -0
  86. {specsmith-0.6.0.dev230 → specsmith-0.6.0.dev231}/src/specsmith/rate_limits.py +0 -0
  87. {specsmith-0.6.0.dev230 → specsmith-0.6.0.dev231}/src/specsmith/releaser.py +0 -0
  88. {specsmith-0.6.0.dev230 → specsmith-0.6.0.dev231}/src/specsmith/requirements.py +0 -0
  89. {specsmith-0.6.0.dev230 → specsmith-0.6.0.dev231}/src/specsmith/requirements_parser.py +0 -0
  90. {specsmith-0.6.0.dev230 → specsmith-0.6.0.dev231}/src/specsmith/retrieval.py +0 -0
  91. {specsmith-0.6.0.dev230 → specsmith-0.6.0.dev231}/src/specsmith/scaffolder.py +0 -0
  92. {specsmith-0.6.0.dev230 → specsmith-0.6.0.dev231}/src/specsmith/serve.py +0 -0
  93. {specsmith-0.6.0.dev230 → specsmith-0.6.0.dev231}/src/specsmith/session.py +0 -0
  94. {specsmith-0.6.0.dev230 → specsmith-0.6.0.dev231}/src/specsmith/skills.py +0 -0
  95. {specsmith-0.6.0.dev230 → specsmith-0.6.0.dev231}/src/specsmith/templates/agents.md.j2 +0 -0
  96. {specsmith-0.6.0.dev230 → specsmith-0.6.0.dev231}/src/specsmith/templates/community/bug_report.md.j2 +0 -0
  97. {specsmith-0.6.0.dev230 → specsmith-0.6.0.dev231}/src/specsmith/templates/community/code_of_conduct.md.j2 +0 -0
  98. {specsmith-0.6.0.dev230 → specsmith-0.6.0.dev231}/src/specsmith/templates/community/contributing.md.j2 +0 -0
  99. {specsmith-0.6.0.dev230 → specsmith-0.6.0.dev231}/src/specsmith/templates/community/feature_request.md.j2 +0 -0
  100. {specsmith-0.6.0.dev230 → specsmith-0.6.0.dev231}/src/specsmith/templates/community/license-Apache-2.0.j2 +0 -0
  101. {specsmith-0.6.0.dev230 → specsmith-0.6.0.dev231}/src/specsmith/templates/community/license-MIT.j2 +0 -0
  102. {specsmith-0.6.0.dev230 → specsmith-0.6.0.dev231}/src/specsmith/templates/community/pull_request_template.md.j2 +0 -0
  103. {specsmith-0.6.0.dev230 → specsmith-0.6.0.dev231}/src/specsmith/templates/community/security.md.j2 +0 -0
  104. {specsmith-0.6.0.dev230 → specsmith-0.6.0.dev231}/src/specsmith/templates/docs/architecture.md.j2 +0 -0
  105. {specsmith-0.6.0.dev230 → specsmith-0.6.0.dev231}/src/specsmith/templates/docs/mkdocs.yml.j2 +0 -0
  106. {specsmith-0.6.0.dev230 → specsmith-0.6.0.dev231}/src/specsmith/templates/docs/readthedocs.yaml.j2 +0 -0
  107. {specsmith-0.6.0.dev230 → specsmith-0.6.0.dev231}/src/specsmith/templates/docs/requirements.md.j2 +0 -0
  108. {specsmith-0.6.0.dev230 → specsmith-0.6.0.dev231}/src/specsmith/templates/docs/test-spec.md.j2 +0 -0
  109. {specsmith-0.6.0.dev230 → specsmith-0.6.0.dev231}/src/specsmith/templates/editorconfig.j2 +0 -0
  110. {specsmith-0.6.0.dev230 → specsmith-0.6.0.dev231}/src/specsmith/templates/gitattributes.j2 +0 -0
  111. {specsmith-0.6.0.dev230 → specsmith-0.6.0.dev231}/src/specsmith/templates/gitignore.j2 +0 -0
  112. {specsmith-0.6.0.dev230 → specsmith-0.6.0.dev231}/src/specsmith/templates/go/go.mod.j2 +0 -0
  113. {specsmith-0.6.0.dev230 → specsmith-0.6.0.dev231}/src/specsmith/templates/go/main.go.j2 +0 -0
  114. {specsmith-0.6.0.dev230 → specsmith-0.6.0.dev231}/src/specsmith/templates/governance/belief-registry.md.j2 +0 -0
  115. {specsmith-0.6.0.dev230 → specsmith-0.6.0.dev231}/src/specsmith/templates/governance/context-budget.md.j2 +0 -0
  116. {specsmith-0.6.0.dev230 → specsmith-0.6.0.dev231}/src/specsmith/templates/governance/drift-metrics.md.j2 +0 -0
  117. {specsmith-0.6.0.dev230 → specsmith-0.6.0.dev231}/src/specsmith/templates/governance/epistemic-axioms.md.j2 +0 -0
  118. {specsmith-0.6.0.dev230 → specsmith-0.6.0.dev231}/src/specsmith/templates/governance/failure-modes.md.j2 +0 -0
  119. {specsmith-0.6.0.dev230 → specsmith-0.6.0.dev231}/src/specsmith/templates/governance/lifecycle.md.j2 +0 -0
  120. {specsmith-0.6.0.dev230 → specsmith-0.6.0.dev231}/src/specsmith/templates/governance/roles.md.j2 +0 -0
  121. {specsmith-0.6.0.dev230 → specsmith-0.6.0.dev231}/src/specsmith/templates/governance/rules.md.j2 +0 -0
  122. {specsmith-0.6.0.dev230 → specsmith-0.6.0.dev231}/src/specsmith/templates/governance/session-protocol.md.j2 +0 -0
  123. {specsmith-0.6.0.dev230 → specsmith-0.6.0.dev231}/src/specsmith/templates/governance/uncertainty-map.md.j2 +0 -0
  124. {specsmith-0.6.0.dev230 → specsmith-0.6.0.dev231}/src/specsmith/templates/governance/verification.md.j2 +0 -0
  125. {specsmith-0.6.0.dev230 → specsmith-0.6.0.dev231}/src/specsmith/templates/js/package.json.j2 +0 -0
  126. {specsmith-0.6.0.dev230 → specsmith-0.6.0.dev231}/src/specsmith/templates/ledger.md.j2 +0 -0
  127. {specsmith-0.6.0.dev230 → specsmith-0.6.0.dev231}/src/specsmith/templates/python/cli.py.j2 +0 -0
  128. {specsmith-0.6.0.dev230 → specsmith-0.6.0.dev231}/src/specsmith/templates/python/init.py.j2 +0 -0
  129. {specsmith-0.6.0.dev230 → specsmith-0.6.0.dev231}/src/specsmith/templates/python/pyproject.toml.j2 +0 -0
  130. {specsmith-0.6.0.dev230 → specsmith-0.6.0.dev231}/src/specsmith/templates/readme.md.j2 +0 -0
  131. {specsmith-0.6.0.dev230 → specsmith-0.6.0.dev231}/src/specsmith/templates/rust/Cargo.toml.j2 +0 -0
  132. {specsmith-0.6.0.dev230 → specsmith-0.6.0.dev231}/src/specsmith/templates/rust/main.rs.j2 +0 -0
  133. {specsmith-0.6.0.dev230 → specsmith-0.6.0.dev231}/src/specsmith/templates/scripts/exec.cmd.j2 +0 -0
  134. {specsmith-0.6.0.dev230 → specsmith-0.6.0.dev231}/src/specsmith/templates/scripts/exec.sh.j2 +0 -0
  135. {specsmith-0.6.0.dev230 → specsmith-0.6.0.dev231}/src/specsmith/templates/scripts/run.cmd.j2 +0 -0
  136. {specsmith-0.6.0.dev230 → specsmith-0.6.0.dev231}/src/specsmith/templates/scripts/run.sh.j2 +0 -0
  137. {specsmith-0.6.0.dev230 → specsmith-0.6.0.dev231}/src/specsmith/templates/scripts/setup.cmd.j2 +0 -0
  138. {specsmith-0.6.0.dev230 → specsmith-0.6.0.dev231}/src/specsmith/templates/scripts/setup.sh.j2 +0 -0
  139. {specsmith-0.6.0.dev230 → specsmith-0.6.0.dev231}/src/specsmith/templates/workflows/release.yml.j2 +0 -0
  140. {specsmith-0.6.0.dev230 → specsmith-0.6.0.dev231}/src/specsmith/tool_installer.py +0 -0
  141. {specsmith-0.6.0.dev230 → specsmith-0.6.0.dev231}/src/specsmith/toolrules.py +0 -0
  142. {specsmith-0.6.0.dev230 → specsmith-0.6.0.dev231}/src/specsmith/tools.py +0 -0
  143. {specsmith-0.6.0.dev230 → specsmith-0.6.0.dev231}/src/specsmith/trace.py +0 -0
  144. {specsmith-0.6.0.dev230 → specsmith-0.6.0.dev231}/src/specsmith/updater.py +0 -0
  145. {specsmith-0.6.0.dev230 → specsmith-0.6.0.dev231}/src/specsmith/upgrader.py +0 -0
  146. {specsmith-0.6.0.dev230 → specsmith-0.6.0.dev231}/src/specsmith/validator.py +0 -0
  147. {specsmith-0.6.0.dev230 → specsmith-0.6.0.dev231}/src/specsmith/vcs/__init__.py +0 -0
  148. {specsmith-0.6.0.dev230 → specsmith-0.6.0.dev231}/src/specsmith/vcs/base.py +0 -0
  149. {specsmith-0.6.0.dev230 → specsmith-0.6.0.dev231}/src/specsmith/vcs/bitbucket.py +0 -0
  150. {specsmith-0.6.0.dev230 → specsmith-0.6.0.dev231}/src/specsmith/vcs/github.py +0 -0
  151. {specsmith-0.6.0.dev230 → specsmith-0.6.0.dev231}/src/specsmith/vcs/gitlab.py +0 -0
  152. {specsmith-0.6.0.dev230 → specsmith-0.6.0.dev231}/src/specsmith/vcs_commands.py +0 -0
  153. {specsmith-0.6.0.dev230 → specsmith-0.6.0.dev231}/src/specsmith/wireframes.py +0 -0
  154. {specsmith-0.6.0.dev230 → specsmith-0.6.0.dev231}/src/specsmith/workspace.py +0 -0
  155. {specsmith-0.6.0.dev230 → specsmith-0.6.0.dev231}/src/specsmith.egg-info/dependency_links.txt +0 -0
  156. {specsmith-0.6.0.dev230 → specsmith-0.6.0.dev231}/src/specsmith.egg-info/entry_points.txt +0 -0
  157. {specsmith-0.6.0.dev230 → specsmith-0.6.0.dev231}/src/specsmith.egg-info/requires.txt +0 -0
  158. {specsmith-0.6.0.dev230 → specsmith-0.6.0.dev231}/src/specsmith.egg-info/top_level.txt +0 -0
  159. {specsmith-0.6.0.dev230 → specsmith-0.6.0.dev231}/tests/test_CMD_001.py +0 -0
  160. {specsmith-0.6.0.dev230 → specsmith-0.6.0.dev231}/tests/test_auditor.py +0 -0
  161. {specsmith-0.6.0.dev230 → specsmith-0.6.0.dev231}/tests/test_chat_diff_decision.py +0 -0
  162. {specsmith-0.6.0.dev230 → specsmith-0.6.0.dev231}/tests/test_chat_stdin_protocol.py +0 -0
  163. {specsmith-0.6.0.dev230 → specsmith-0.6.0.dev231}/tests/test_cli.py +0 -0
  164. {specsmith-0.6.0.dev230 → specsmith-0.6.0.dev231}/tests/test_cli_workflows_history_drive.py +0 -0
  165. {specsmith-0.6.0.dev230 → specsmith-0.6.0.dev231}/tests/test_compressor.py +0 -0
  166. {specsmith-0.6.0.dev230 → specsmith-0.6.0.dev231}/tests/test_e2e_nexus.py +0 -0
  167. {specsmith-0.6.0.dev230 → specsmith-0.6.0.dev231}/tests/test_epistemic.py +0 -0
  168. {specsmith-0.6.0.dev230 → specsmith-0.6.0.dev231}/tests/test_importer.py +0 -0
  169. {specsmith-0.6.0.dev230 → specsmith-0.6.0.dev231}/tests/test_integrations.py +0 -0
  170. {specsmith-0.6.0.dev230 → specsmith-0.6.0.dev231}/tests/test_nexus.py +0 -0
  171. {specsmith-0.6.0.dev230 → specsmith-0.6.0.dev231}/tests/test_phase1_4_new.py +0 -0
  172. {specsmith-0.6.0.dev230 → specsmith-0.6.0.dev231}/tests/test_rate_limits.py +0 -0
  173. {specsmith-0.6.0.dev230 → specsmith-0.6.0.dev231}/tests/test_scaffolder.py +0 -0
  174. {specsmith-0.6.0.dev230 → specsmith-0.6.0.dev231}/tests/test_skill_marketplace.py +0 -0
  175. {specsmith-0.6.0.dev230 → specsmith-0.6.0.dev231}/tests/test_smoke.py +0 -0
  176. {specsmith-0.6.0.dev230 → specsmith-0.6.0.dev231}/tests/test_tools.py +0 -0
  177. {specsmith-0.6.0.dev230 → specsmith-0.6.0.dev231}/tests/test_validator.py +0 -0
  178. {specsmith-0.6.0.dev230 → 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.dev230
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.dev230"
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
+ ]
@@ -5288,13 +5288,20 @@ def chat_cmd(
5288
5288
  if rules_prefix:
5289
5289
  emitter.token(msg_block, "[project rules loaded]\n")
5290
5290
 
5291
- # Surface configured MCP servers (REQ-121). The actual invocation
5292
- # path still flows through the safety middleware; here we just announce
5293
- # availability so consumers can render the list.
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.
5294
5295
  mcp_tools = load_mcp_tools(root)
5295
5296
  if mcp_tools:
5296
- names = ", ".join(t.name for t in mcp_tools)
5297
- emitter.token(msg_block, f"[mcp servers: {names}]\n")
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
+ )
5298
5305
 
5299
5306
  # Pick a tier (REQ-122) so consumers know which model is in play.
5300
5307
  _utt_lower = utterance.lower()
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: specsmith
3
- Version: 0.6.0.dev230
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,6 +162,7 @@ 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
167
168
  tests/test_phase34_completion.py
@@ -0,0 +1,157 @@
1
+ # SPDX-License-Identifier: MIT
2
+ # Copyright (c) 2026 BitConcepts, LLC. All rights reserved.
3
+ """End-to-end tests for the real MCP JSON-RPC client (REQ-130 / TEST-130).
4
+
5
+ Uses ``tests/fixtures/mcp_fake_server.py`` so we can drive the full
6
+ handshake without depending on any external MCP server installation.
7
+ """
8
+
9
+ from __future__ import annotations
10
+
11
+ import sys
12
+ from pathlib import Path
13
+
14
+ import pytest
15
+
16
+ from specsmith.agent.mcp import (
17
+ MCP_PROTOCOL_VERSION,
18
+ MCPError,
19
+ MCPServerSpec,
20
+ MCPSession,
21
+ MCPTool,
22
+ open_mcp_sessions,
23
+ )
24
+
25
+ FIXTURES = Path(__file__).resolve().parent / "fixtures"
26
+ FAKE = FIXTURES / "mcp_fake_server.py"
27
+
28
+
29
+ def _spec(env: dict[str, str] | None = None) -> MCPServerSpec:
30
+ return MCPServerSpec(
31
+ name="fake",
32
+ command=sys.executable,
33
+ args=[str(FAKE)],
34
+ env=env or {},
35
+ )
36
+
37
+
38
+ # ── Discovery / handshake (TEST-130a..c) ─────────────────────────────────
39
+
40
+
41
+ def test_session_open_runs_handshake_and_lists_tools() -> None:
42
+ session = MCPSession(_spec())
43
+ try:
44
+ tools = session.open()
45
+ assert {t.name for t in tools} == {"echo", "boom"}
46
+ echo = next(t for t in tools if t.name == "echo")
47
+ assert "Echo" in echo.description
48
+ assert echo.input_schema.get("required") == ["text"]
49
+ assert echo.server_name == "fake"
50
+ finally:
51
+ session.close()
52
+
53
+
54
+ def test_session_open_pins_protocol_version_constant() -> None:
55
+ # Make sure the public protocol-pin constant is the latest stable.
56
+ assert MCP_PROTOCOL_VERSION == "2024-11-05"
57
+
58
+
59
+ def test_session_close_is_idempotent() -> None:
60
+ session = MCPSession(_spec())
61
+ session.open()
62
+ session.close()
63
+ session.close() # second close must not raise
64
+
65
+
66
+ # ── Tool invocation (TEST-130d..g) ───────────────────────────────────────
67
+
68
+
69
+ def test_call_tool_returns_concatenated_text_blocks() -> None:
70
+ session = MCPSession(_spec())
71
+ try:
72
+ session.open()
73
+ result = session.call_tool("echo", {"text": "hello world"})
74
+ assert result == "hello world"
75
+ finally:
76
+ session.close()
77
+
78
+
79
+ def test_call_tool_iserror_is_prefixed() -> None:
80
+ session = MCPSession(_spec())
81
+ try:
82
+ session.open()
83
+ result = session.call_tool("boom", {})
84
+ assert result.startswith("mcp error:")
85
+ assert "intentional boom" in result
86
+ finally:
87
+ session.close()
88
+
89
+
90
+ def test_call_tool_unknown_name_raises_mcp_error() -> None:
91
+ session = MCPSession(_spec())
92
+ try:
93
+ session.open()
94
+ with pytest.raises(MCPError) as exc:
95
+ session.call_tool("does-not-exist", {})
96
+ assert exc.value.code == -32601
97
+ finally:
98
+ session.close()
99
+
100
+
101
+ def test_mcp_tool_invoke_with_safety_blocks_disallowed_payloads() -> None:
102
+ session = MCPSession(_spec())
103
+ try:
104
+ session.open()
105
+ echo = next(t for t in session.tools if t.name == "echo")
106
+ tool = MCPTool(descriptor=echo, session=session)
107
+
108
+ def _check(name: str, args: dict[str, object]) -> tuple[bool, str]:
109
+ text = str(args.get("text", ""))
110
+ if "rm -rf" in text:
111
+ return False, "destructive command refused"
112
+ return True, ""
113
+
114
+ # Allowed → flows through to call_tool.
115
+ assert tool.invoke_with_safety({"text": "ok"}, _check) == "ok"
116
+ # Disallowed → returns redacted message and never calls the server.
117
+ blocked = tool.invoke_with_safety({"text": "rm -rf /"}, _check)
118
+ assert blocked.startswith("mcp blocked by safety:")
119
+ finally:
120
+ session.close()
121
+
122
+
123
+ # ── Failure modes (TEST-130h..j) ─────────────────────────────────────────
124
+
125
+
126
+ def test_session_open_raises_when_server_crashes_during_initialize() -> None:
127
+ session = MCPSession(_spec(env={"MCP_FAKE_CRASH_ON": "initialize"}))
128
+ try:
129
+ with pytest.raises(MCPError) as exc:
130
+ session.open()
131
+ assert exc.value.code == -32002
132
+ finally:
133
+ session.close()
134
+
135
+
136
+ def test_open_mcp_sessions_skips_servers_that_fail_to_start(
137
+ tmp_path: Path,
138
+ ) -> None:
139
+ cfg = tmp_path / ".specsmith"
140
+ cfg.mkdir()
141
+ # First entry resolves, second does not.
142
+ cfg.joinpath("mcp.yml").write_text(
143
+ "- name: real\n"
144
+ f" command: {sys.executable}\n"
145
+ f" args: ['{FAKE.as_posix()}']\n"
146
+ "- name: missing\n"
147
+ " command: definitely-not-a-real-mcp-binary-xyz\n",
148
+ encoding="utf-8",
149
+ )
150
+ sessions = open_mcp_sessions(tmp_path)
151
+ try:
152
+ assert len(sessions) == 1
153
+ assert sessions[0].spec.name == "real"
154
+ assert {t.name for t in sessions[0].tools} == {"echo", "boom"}
155
+ finally:
156
+ for s in sessions:
157
+ s.close()