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.
Files changed (178) hide show
  1. {specsmith-0.6.0.dev229/src/specsmith.egg-info → specsmith-0.6.0.dev231}/PKG-INFO +1 -1
  2. {specsmith-0.6.0.dev229 → 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.dev229 → specsmith-0.6.0.dev231}/src/specsmith/cli.py +63 -8
  5. {specsmith-0.6.0.dev229 → specsmith-0.6.0.dev231/src/specsmith.egg-info}/PKG-INFO +1 -1
  6. {specsmith-0.6.0.dev229 → specsmith-0.6.0.dev231}/src/specsmith.egg-info/SOURCES.txt +2 -0
  7. specsmith-0.6.0.dev231/tests/test_mcp_client.py +157 -0
  8. specsmith-0.6.0.dev231/tests/test_phase34_completion.py +243 -0
  9. specsmith-0.6.0.dev229/src/specsmith/agent/mcp.py +0 -117
  10. {specsmith-0.6.0.dev229 → specsmith-0.6.0.dev231}/LICENSE +0 -0
  11. {specsmith-0.6.0.dev229 → specsmith-0.6.0.dev231}/README.md +0 -0
  12. {specsmith-0.6.0.dev229 → specsmith-0.6.0.dev231}/setup.cfg +0 -0
  13. {specsmith-0.6.0.dev229 → specsmith-0.6.0.dev231}/src/epistemic/__init__.py +0 -0
  14. {specsmith-0.6.0.dev229 → specsmith-0.6.0.dev231}/src/epistemic/belief.py +0 -0
  15. {specsmith-0.6.0.dev229 → specsmith-0.6.0.dev231}/src/epistemic/certainty.py +0 -0
  16. {specsmith-0.6.0.dev229 → specsmith-0.6.0.dev231}/src/epistemic/failure_graph.py +0 -0
  17. {specsmith-0.6.0.dev229 → specsmith-0.6.0.dev231}/src/epistemic/py.typed +0 -0
  18. {specsmith-0.6.0.dev229 → specsmith-0.6.0.dev231}/src/epistemic/recovery.py +0 -0
  19. {specsmith-0.6.0.dev229 → specsmith-0.6.0.dev231}/src/epistemic/session.py +0 -0
  20. {specsmith-0.6.0.dev229 → specsmith-0.6.0.dev231}/src/epistemic/stress_tester.py +0 -0
  21. {specsmith-0.6.0.dev229 → specsmith-0.6.0.dev231}/src/epistemic/trace.py +0 -0
  22. {specsmith-0.6.0.dev229 → specsmith-0.6.0.dev231}/src/specsmith/__init__.py +0 -0
  23. {specsmith-0.6.0.dev229 → specsmith-0.6.0.dev231}/src/specsmith/__main__.py +0 -0
  24. {specsmith-0.6.0.dev229 → specsmith-0.6.0.dev231}/src/specsmith/agent/__init__.py +0 -0
  25. {specsmith-0.6.0.dev229 → specsmith-0.6.0.dev231}/src/specsmith/agent/broker.py +0 -0
  26. {specsmith-0.6.0.dev229 → specsmith-0.6.0.dev231}/src/specsmith/agent/chat_runner.py +0 -0
  27. {specsmith-0.6.0.dev229 → specsmith-0.6.0.dev231}/src/specsmith/agent/cleanup.py +0 -0
  28. {specsmith-0.6.0.dev229 → specsmith-0.6.0.dev231}/src/specsmith/agent/events.py +0 -0
  29. {specsmith-0.6.0.dev229 → specsmith-0.6.0.dev231}/src/specsmith/agent/indexer.py +0 -0
  30. {specsmith-0.6.0.dev229 → specsmith-0.6.0.dev231}/src/specsmith/agent/memory.py +0 -0
  31. {specsmith-0.6.0.dev229 → specsmith-0.6.0.dev231}/src/specsmith/agent/orchestrator.py +0 -0
  32. {specsmith-0.6.0.dev229 → specsmith-0.6.0.dev231}/src/specsmith/agent/repl.py +0 -0
  33. {specsmith-0.6.0.dev229 → specsmith-0.6.0.dev231}/src/specsmith/agent/router.py +0 -0
  34. {specsmith-0.6.0.dev229 → specsmith-0.6.0.dev231}/src/specsmith/agent/rules.py +0 -0
  35. {specsmith-0.6.0.dev229 → specsmith-0.6.0.dev231}/src/specsmith/agent/safety.py +0 -0
  36. {specsmith-0.6.0.dev229 → specsmith-0.6.0.dev231}/src/specsmith/agent/tools.py +0 -0
  37. {specsmith-0.6.0.dev229 → specsmith-0.6.0.dev231}/src/specsmith/agent/verifier.py +0 -0
  38. {specsmith-0.6.0.dev229 → specsmith-0.6.0.dev231}/src/specsmith/architect.py +0 -0
  39. {specsmith-0.6.0.dev229 → specsmith-0.6.0.dev231}/src/specsmith/auditor.py +0 -0
  40. {specsmith-0.6.0.dev229 → specsmith-0.6.0.dev231}/src/specsmith/auth.py +0 -0
  41. {specsmith-0.6.0.dev229 → specsmith-0.6.0.dev231}/src/specsmith/commands/__init__.py +0 -0
  42. {specsmith-0.6.0.dev229 → specsmith-0.6.0.dev231}/src/specsmith/compressor.py +0 -0
  43. {specsmith-0.6.0.dev229 → specsmith-0.6.0.dev231}/src/specsmith/config.py +0 -0
  44. {specsmith-0.6.0.dev229 → specsmith-0.6.0.dev231}/src/specsmith/console_utils.py +0 -0
  45. {specsmith-0.6.0.dev229 → specsmith-0.6.0.dev231}/src/specsmith/credit_analyzer.py +0 -0
  46. {specsmith-0.6.0.dev229 → specsmith-0.6.0.dev231}/src/specsmith/credits.py +0 -0
  47. {specsmith-0.6.0.dev229 → specsmith-0.6.0.dev231}/src/specsmith/differ.py +0 -0
  48. {specsmith-0.6.0.dev229 → specsmith-0.6.0.dev231}/src/specsmith/doctor.py +0 -0
  49. {specsmith-0.6.0.dev229 → specsmith-0.6.0.dev231}/src/specsmith/epistemic/__init__.py +0 -0
  50. {specsmith-0.6.0.dev229 → specsmith-0.6.0.dev231}/src/specsmith/epistemic/belief.py +0 -0
  51. {specsmith-0.6.0.dev229 → specsmith-0.6.0.dev231}/src/specsmith/epistemic/certainty.py +0 -0
  52. {specsmith-0.6.0.dev229 → specsmith-0.6.0.dev231}/src/specsmith/epistemic/failure_graph.py +0 -0
  53. {specsmith-0.6.0.dev229 → specsmith-0.6.0.dev231}/src/specsmith/epistemic/recovery.py +0 -0
  54. {specsmith-0.6.0.dev229 → specsmith-0.6.0.dev231}/src/specsmith/epistemic/stress_tester.py +0 -0
  55. {specsmith-0.6.0.dev229 → specsmith-0.6.0.dev231}/src/specsmith/executor.py +0 -0
  56. {specsmith-0.6.0.dev229 → specsmith-0.6.0.dev231}/src/specsmith/exporter.py +0 -0
  57. {specsmith-0.6.0.dev229 → specsmith-0.6.0.dev231}/src/specsmith/gui/__init__.py +0 -0
  58. {specsmith-0.6.0.dev229 → specsmith-0.6.0.dev231}/src/specsmith/gui/app.py +0 -0
  59. {specsmith-0.6.0.dev229 → specsmith-0.6.0.dev231}/src/specsmith/gui/main_window.py +0 -0
  60. {specsmith-0.6.0.dev229 → specsmith-0.6.0.dev231}/src/specsmith/gui/session_tab.py +0 -0
  61. {specsmith-0.6.0.dev229 → specsmith-0.6.0.dev231}/src/specsmith/gui/theme.py +0 -0
  62. {specsmith-0.6.0.dev229 → specsmith-0.6.0.dev231}/src/specsmith/gui/widgets/__init__.py +0 -0
  63. {specsmith-0.6.0.dev229 → specsmith-0.6.0.dev231}/src/specsmith/gui/widgets/chat_view.py +0 -0
  64. {specsmith-0.6.0.dev229 → specsmith-0.6.0.dev231}/src/specsmith/gui/widgets/input_bar.py +0 -0
  65. {specsmith-0.6.0.dev229 → specsmith-0.6.0.dev231}/src/specsmith/gui/widgets/provider_bar.py +0 -0
  66. {specsmith-0.6.0.dev229 → specsmith-0.6.0.dev231}/src/specsmith/gui/widgets/token_meter.py +0 -0
  67. {specsmith-0.6.0.dev229 → specsmith-0.6.0.dev231}/src/specsmith/gui/widgets/tool_panel.py +0 -0
  68. {specsmith-0.6.0.dev229 → specsmith-0.6.0.dev231}/src/specsmith/gui/widgets/update_checker.py +0 -0
  69. {specsmith-0.6.0.dev229 → specsmith-0.6.0.dev231}/src/specsmith/gui/worker.py +0 -0
  70. {specsmith-0.6.0.dev229 → specsmith-0.6.0.dev231}/src/specsmith/importer.py +0 -0
  71. {specsmith-0.6.0.dev229 → specsmith-0.6.0.dev231}/src/specsmith/integrations/__init__.py +0 -0
  72. {specsmith-0.6.0.dev229 → specsmith-0.6.0.dev231}/src/specsmith/integrations/agent_skill.py +0 -0
  73. {specsmith-0.6.0.dev229 → specsmith-0.6.0.dev231}/src/specsmith/integrations/aider.py +0 -0
  74. {specsmith-0.6.0.dev229 → specsmith-0.6.0.dev231}/src/specsmith/integrations/base.py +0 -0
  75. {specsmith-0.6.0.dev229 → specsmith-0.6.0.dev231}/src/specsmith/integrations/claude_code.py +0 -0
  76. {specsmith-0.6.0.dev229 → specsmith-0.6.0.dev231}/src/specsmith/integrations/copilot.py +0 -0
  77. {specsmith-0.6.0.dev229 → specsmith-0.6.0.dev231}/src/specsmith/integrations/cursor.py +0 -0
  78. {specsmith-0.6.0.dev229 → specsmith-0.6.0.dev231}/src/specsmith/integrations/gemini.py +0 -0
  79. {specsmith-0.6.0.dev229 → specsmith-0.6.0.dev231}/src/specsmith/integrations/windsurf.py +0 -0
  80. {specsmith-0.6.0.dev229 → specsmith-0.6.0.dev231}/src/specsmith/languages.py +0 -0
  81. {specsmith-0.6.0.dev229 → specsmith-0.6.0.dev231}/src/specsmith/ledger.py +0 -0
  82. {specsmith-0.6.0.dev229 → specsmith-0.6.0.dev231}/src/specsmith/patent.py +0 -0
  83. {specsmith-0.6.0.dev229 → specsmith-0.6.0.dev231}/src/specsmith/phase.py +0 -0
  84. {specsmith-0.6.0.dev229 → specsmith-0.6.0.dev231}/src/specsmith/plugins.py +0 -0
  85. {specsmith-0.6.0.dev229 → specsmith-0.6.0.dev231}/src/specsmith/profiles.py +0 -0
  86. {specsmith-0.6.0.dev229 → specsmith-0.6.0.dev231}/src/specsmith/rate_limits.py +0 -0
  87. {specsmith-0.6.0.dev229 → specsmith-0.6.0.dev231}/src/specsmith/releaser.py +0 -0
  88. {specsmith-0.6.0.dev229 → specsmith-0.6.0.dev231}/src/specsmith/requirements.py +0 -0
  89. {specsmith-0.6.0.dev229 → specsmith-0.6.0.dev231}/src/specsmith/requirements_parser.py +0 -0
  90. {specsmith-0.6.0.dev229 → specsmith-0.6.0.dev231}/src/specsmith/retrieval.py +0 -0
  91. {specsmith-0.6.0.dev229 → specsmith-0.6.0.dev231}/src/specsmith/scaffolder.py +0 -0
  92. {specsmith-0.6.0.dev229 → specsmith-0.6.0.dev231}/src/specsmith/serve.py +0 -0
  93. {specsmith-0.6.0.dev229 → specsmith-0.6.0.dev231}/src/specsmith/session.py +0 -0
  94. {specsmith-0.6.0.dev229 → specsmith-0.6.0.dev231}/src/specsmith/skills.py +0 -0
  95. {specsmith-0.6.0.dev229 → specsmith-0.6.0.dev231}/src/specsmith/templates/agents.md.j2 +0 -0
  96. {specsmith-0.6.0.dev229 → specsmith-0.6.0.dev231}/src/specsmith/templates/community/bug_report.md.j2 +0 -0
  97. {specsmith-0.6.0.dev229 → specsmith-0.6.0.dev231}/src/specsmith/templates/community/code_of_conduct.md.j2 +0 -0
  98. {specsmith-0.6.0.dev229 → specsmith-0.6.0.dev231}/src/specsmith/templates/community/contributing.md.j2 +0 -0
  99. {specsmith-0.6.0.dev229 → specsmith-0.6.0.dev231}/src/specsmith/templates/community/feature_request.md.j2 +0 -0
  100. {specsmith-0.6.0.dev229 → specsmith-0.6.0.dev231}/src/specsmith/templates/community/license-Apache-2.0.j2 +0 -0
  101. {specsmith-0.6.0.dev229 → specsmith-0.6.0.dev231}/src/specsmith/templates/community/license-MIT.j2 +0 -0
  102. {specsmith-0.6.0.dev229 → specsmith-0.6.0.dev231}/src/specsmith/templates/community/pull_request_template.md.j2 +0 -0
  103. {specsmith-0.6.0.dev229 → specsmith-0.6.0.dev231}/src/specsmith/templates/community/security.md.j2 +0 -0
  104. {specsmith-0.6.0.dev229 → specsmith-0.6.0.dev231}/src/specsmith/templates/docs/architecture.md.j2 +0 -0
  105. {specsmith-0.6.0.dev229 → specsmith-0.6.0.dev231}/src/specsmith/templates/docs/mkdocs.yml.j2 +0 -0
  106. {specsmith-0.6.0.dev229 → specsmith-0.6.0.dev231}/src/specsmith/templates/docs/readthedocs.yaml.j2 +0 -0
  107. {specsmith-0.6.0.dev229 → specsmith-0.6.0.dev231}/src/specsmith/templates/docs/requirements.md.j2 +0 -0
  108. {specsmith-0.6.0.dev229 → specsmith-0.6.0.dev231}/src/specsmith/templates/docs/test-spec.md.j2 +0 -0
  109. {specsmith-0.6.0.dev229 → specsmith-0.6.0.dev231}/src/specsmith/templates/editorconfig.j2 +0 -0
  110. {specsmith-0.6.0.dev229 → specsmith-0.6.0.dev231}/src/specsmith/templates/gitattributes.j2 +0 -0
  111. {specsmith-0.6.0.dev229 → specsmith-0.6.0.dev231}/src/specsmith/templates/gitignore.j2 +0 -0
  112. {specsmith-0.6.0.dev229 → specsmith-0.6.0.dev231}/src/specsmith/templates/go/go.mod.j2 +0 -0
  113. {specsmith-0.6.0.dev229 → specsmith-0.6.0.dev231}/src/specsmith/templates/go/main.go.j2 +0 -0
  114. {specsmith-0.6.0.dev229 → specsmith-0.6.0.dev231}/src/specsmith/templates/governance/belief-registry.md.j2 +0 -0
  115. {specsmith-0.6.0.dev229 → specsmith-0.6.0.dev231}/src/specsmith/templates/governance/context-budget.md.j2 +0 -0
  116. {specsmith-0.6.0.dev229 → specsmith-0.6.0.dev231}/src/specsmith/templates/governance/drift-metrics.md.j2 +0 -0
  117. {specsmith-0.6.0.dev229 → specsmith-0.6.0.dev231}/src/specsmith/templates/governance/epistemic-axioms.md.j2 +0 -0
  118. {specsmith-0.6.0.dev229 → specsmith-0.6.0.dev231}/src/specsmith/templates/governance/failure-modes.md.j2 +0 -0
  119. {specsmith-0.6.0.dev229 → specsmith-0.6.0.dev231}/src/specsmith/templates/governance/lifecycle.md.j2 +0 -0
  120. {specsmith-0.6.0.dev229 → specsmith-0.6.0.dev231}/src/specsmith/templates/governance/roles.md.j2 +0 -0
  121. {specsmith-0.6.0.dev229 → specsmith-0.6.0.dev231}/src/specsmith/templates/governance/rules.md.j2 +0 -0
  122. {specsmith-0.6.0.dev229 → specsmith-0.6.0.dev231}/src/specsmith/templates/governance/session-protocol.md.j2 +0 -0
  123. {specsmith-0.6.0.dev229 → specsmith-0.6.0.dev231}/src/specsmith/templates/governance/uncertainty-map.md.j2 +0 -0
  124. {specsmith-0.6.0.dev229 → specsmith-0.6.0.dev231}/src/specsmith/templates/governance/verification.md.j2 +0 -0
  125. {specsmith-0.6.0.dev229 → specsmith-0.6.0.dev231}/src/specsmith/templates/js/package.json.j2 +0 -0
  126. {specsmith-0.6.0.dev229 → specsmith-0.6.0.dev231}/src/specsmith/templates/ledger.md.j2 +0 -0
  127. {specsmith-0.6.0.dev229 → specsmith-0.6.0.dev231}/src/specsmith/templates/python/cli.py.j2 +0 -0
  128. {specsmith-0.6.0.dev229 → specsmith-0.6.0.dev231}/src/specsmith/templates/python/init.py.j2 +0 -0
  129. {specsmith-0.6.0.dev229 → specsmith-0.6.0.dev231}/src/specsmith/templates/python/pyproject.toml.j2 +0 -0
  130. {specsmith-0.6.0.dev229 → specsmith-0.6.0.dev231}/src/specsmith/templates/readme.md.j2 +0 -0
  131. {specsmith-0.6.0.dev229 → specsmith-0.6.0.dev231}/src/specsmith/templates/rust/Cargo.toml.j2 +0 -0
  132. {specsmith-0.6.0.dev229 → specsmith-0.6.0.dev231}/src/specsmith/templates/rust/main.rs.j2 +0 -0
  133. {specsmith-0.6.0.dev229 → specsmith-0.6.0.dev231}/src/specsmith/templates/scripts/exec.cmd.j2 +0 -0
  134. {specsmith-0.6.0.dev229 → specsmith-0.6.0.dev231}/src/specsmith/templates/scripts/exec.sh.j2 +0 -0
  135. {specsmith-0.6.0.dev229 → specsmith-0.6.0.dev231}/src/specsmith/templates/scripts/run.cmd.j2 +0 -0
  136. {specsmith-0.6.0.dev229 → specsmith-0.6.0.dev231}/src/specsmith/templates/scripts/run.sh.j2 +0 -0
  137. {specsmith-0.6.0.dev229 → specsmith-0.6.0.dev231}/src/specsmith/templates/scripts/setup.cmd.j2 +0 -0
  138. {specsmith-0.6.0.dev229 → specsmith-0.6.0.dev231}/src/specsmith/templates/scripts/setup.sh.j2 +0 -0
  139. {specsmith-0.6.0.dev229 → specsmith-0.6.0.dev231}/src/specsmith/templates/workflows/release.yml.j2 +0 -0
  140. {specsmith-0.6.0.dev229 → specsmith-0.6.0.dev231}/src/specsmith/tool_installer.py +0 -0
  141. {specsmith-0.6.0.dev229 → specsmith-0.6.0.dev231}/src/specsmith/toolrules.py +0 -0
  142. {specsmith-0.6.0.dev229 → specsmith-0.6.0.dev231}/src/specsmith/tools.py +0 -0
  143. {specsmith-0.6.0.dev229 → specsmith-0.6.0.dev231}/src/specsmith/trace.py +0 -0
  144. {specsmith-0.6.0.dev229 → specsmith-0.6.0.dev231}/src/specsmith/updater.py +0 -0
  145. {specsmith-0.6.0.dev229 → specsmith-0.6.0.dev231}/src/specsmith/upgrader.py +0 -0
  146. {specsmith-0.6.0.dev229 → specsmith-0.6.0.dev231}/src/specsmith/validator.py +0 -0
  147. {specsmith-0.6.0.dev229 → specsmith-0.6.0.dev231}/src/specsmith/vcs/__init__.py +0 -0
  148. {specsmith-0.6.0.dev229 → specsmith-0.6.0.dev231}/src/specsmith/vcs/base.py +0 -0
  149. {specsmith-0.6.0.dev229 → specsmith-0.6.0.dev231}/src/specsmith/vcs/bitbucket.py +0 -0
  150. {specsmith-0.6.0.dev229 → specsmith-0.6.0.dev231}/src/specsmith/vcs/github.py +0 -0
  151. {specsmith-0.6.0.dev229 → specsmith-0.6.0.dev231}/src/specsmith/vcs/gitlab.py +0 -0
  152. {specsmith-0.6.0.dev229 → specsmith-0.6.0.dev231}/src/specsmith/vcs_commands.py +0 -0
  153. {specsmith-0.6.0.dev229 → specsmith-0.6.0.dev231}/src/specsmith/wireframes.py +0 -0
  154. {specsmith-0.6.0.dev229 → specsmith-0.6.0.dev231}/src/specsmith/workspace.py +0 -0
  155. {specsmith-0.6.0.dev229 → specsmith-0.6.0.dev231}/src/specsmith.egg-info/dependency_links.txt +0 -0
  156. {specsmith-0.6.0.dev229 → specsmith-0.6.0.dev231}/src/specsmith.egg-info/entry_points.txt +0 -0
  157. {specsmith-0.6.0.dev229 → specsmith-0.6.0.dev231}/src/specsmith.egg-info/requires.txt +0 -0
  158. {specsmith-0.6.0.dev229 → specsmith-0.6.0.dev231}/src/specsmith.egg-info/top_level.txt +0 -0
  159. {specsmith-0.6.0.dev229 → specsmith-0.6.0.dev231}/tests/test_CMD_001.py +0 -0
  160. {specsmith-0.6.0.dev229 → specsmith-0.6.0.dev231}/tests/test_auditor.py +0 -0
  161. {specsmith-0.6.0.dev229 → specsmith-0.6.0.dev231}/tests/test_chat_diff_decision.py +0 -0
  162. {specsmith-0.6.0.dev229 → specsmith-0.6.0.dev231}/tests/test_chat_stdin_protocol.py +0 -0
  163. {specsmith-0.6.0.dev229 → specsmith-0.6.0.dev231}/tests/test_cli.py +0 -0
  164. {specsmith-0.6.0.dev229 → specsmith-0.6.0.dev231}/tests/test_cli_workflows_history_drive.py +0 -0
  165. {specsmith-0.6.0.dev229 → specsmith-0.6.0.dev231}/tests/test_compressor.py +0 -0
  166. {specsmith-0.6.0.dev229 → specsmith-0.6.0.dev231}/tests/test_e2e_nexus.py +0 -0
  167. {specsmith-0.6.0.dev229 → specsmith-0.6.0.dev231}/tests/test_epistemic.py +0 -0
  168. {specsmith-0.6.0.dev229 → specsmith-0.6.0.dev231}/tests/test_importer.py +0 -0
  169. {specsmith-0.6.0.dev229 → specsmith-0.6.0.dev231}/tests/test_integrations.py +0 -0
  170. {specsmith-0.6.0.dev229 → specsmith-0.6.0.dev231}/tests/test_nexus.py +0 -0
  171. {specsmith-0.6.0.dev229 → specsmith-0.6.0.dev231}/tests/test_phase1_4_new.py +0 -0
  172. {specsmith-0.6.0.dev229 → specsmith-0.6.0.dev231}/tests/test_rate_limits.py +0 -0
  173. {specsmith-0.6.0.dev229 → specsmith-0.6.0.dev231}/tests/test_scaffolder.py +0 -0
  174. {specsmith-0.6.0.dev229 → specsmith-0.6.0.dev231}/tests/test_skill_marketplace.py +0 -0
  175. {specsmith-0.6.0.dev229 → specsmith-0.6.0.dev231}/tests/test_smoke.py +0 -0
  176. {specsmith-0.6.0.dev229 → specsmith-0.6.0.dev231}/tests/test_tools.py +0 -0
  177. {specsmith-0.6.0.dev229 → specsmith-0.6.0.dev231}/tests/test_validator.py +0 -0
  178. {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.dev229
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.dev229"
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
- def notebook_record(slug: str, project_dir: str, work_item_id: str) -> None:
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
- Reads `.specsmith/runs/<work_item_id>/` (logs.txt, decision.json, etc.)
5552
- and writes a self-contained Markdown notebook to
5553
- `docs/notebooks/<slug>.md`.
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 {slug}\n"]
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
- else:
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 `.specsmith/runs/<WI>/` directory found. "
5580
- "Run `specsmith preflight` and `specsmith verify` first to capture evidence._\n"
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.dev229
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