specsmith 0.5.0.dev225__tar.gz → 0.5.0.dev227__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 (175) hide show
  1. {specsmith-0.5.0.dev225/src/specsmith.egg-info → specsmith-0.5.0.dev227}/PKG-INFO +1 -1
  2. {specsmith-0.5.0.dev225 → specsmith-0.5.0.dev227}/pyproject.toml +1 -1
  3. specsmith-0.5.0.dev227/src/specsmith/agent/chat_runner.py +337 -0
  4. {specsmith-0.5.0.dev225 → specsmith-0.5.0.dev227}/src/specsmith/cli.py +300 -33
  5. specsmith-0.5.0.dev227/src/specsmith/skills.py +199 -0
  6. {specsmith-0.5.0.dev225 → specsmith-0.5.0.dev227/src/specsmith.egg-info}/PKG-INFO +1 -1
  7. {specsmith-0.5.0.dev225 → specsmith-0.5.0.dev227}/src/specsmith.egg-info/SOURCES.txt +5 -0
  8. specsmith-0.5.0.dev227/tests/test_chat_diff_decision.py +158 -0
  9. specsmith-0.5.0.dev227/tests/test_chat_stdin_protocol.py +89 -0
  10. specsmith-0.5.0.dev227/tests/test_skill_marketplace.py +185 -0
  11. {specsmith-0.5.0.dev225 → specsmith-0.5.0.dev227}/LICENSE +0 -0
  12. {specsmith-0.5.0.dev225 → specsmith-0.5.0.dev227}/README.md +0 -0
  13. {specsmith-0.5.0.dev225 → specsmith-0.5.0.dev227}/setup.cfg +0 -0
  14. {specsmith-0.5.0.dev225 → specsmith-0.5.0.dev227}/src/epistemic/__init__.py +0 -0
  15. {specsmith-0.5.0.dev225 → specsmith-0.5.0.dev227}/src/epistemic/belief.py +0 -0
  16. {specsmith-0.5.0.dev225 → specsmith-0.5.0.dev227}/src/epistemic/certainty.py +0 -0
  17. {specsmith-0.5.0.dev225 → specsmith-0.5.0.dev227}/src/epistemic/failure_graph.py +0 -0
  18. {specsmith-0.5.0.dev225 → specsmith-0.5.0.dev227}/src/epistemic/py.typed +0 -0
  19. {specsmith-0.5.0.dev225 → specsmith-0.5.0.dev227}/src/epistemic/recovery.py +0 -0
  20. {specsmith-0.5.0.dev225 → specsmith-0.5.0.dev227}/src/epistemic/session.py +0 -0
  21. {specsmith-0.5.0.dev225 → specsmith-0.5.0.dev227}/src/epistemic/stress_tester.py +0 -0
  22. {specsmith-0.5.0.dev225 → specsmith-0.5.0.dev227}/src/epistemic/trace.py +0 -0
  23. {specsmith-0.5.0.dev225 → specsmith-0.5.0.dev227}/src/specsmith/__init__.py +0 -0
  24. {specsmith-0.5.0.dev225 → specsmith-0.5.0.dev227}/src/specsmith/__main__.py +0 -0
  25. {specsmith-0.5.0.dev225 → specsmith-0.5.0.dev227}/src/specsmith/agent/__init__.py +0 -0
  26. {specsmith-0.5.0.dev225 → specsmith-0.5.0.dev227}/src/specsmith/agent/broker.py +0 -0
  27. {specsmith-0.5.0.dev225 → specsmith-0.5.0.dev227}/src/specsmith/agent/cleanup.py +0 -0
  28. {specsmith-0.5.0.dev225 → specsmith-0.5.0.dev227}/src/specsmith/agent/events.py +0 -0
  29. {specsmith-0.5.0.dev225 → specsmith-0.5.0.dev227}/src/specsmith/agent/indexer.py +0 -0
  30. {specsmith-0.5.0.dev225 → specsmith-0.5.0.dev227}/src/specsmith/agent/mcp.py +0 -0
  31. {specsmith-0.5.0.dev225 → specsmith-0.5.0.dev227}/src/specsmith/agent/memory.py +0 -0
  32. {specsmith-0.5.0.dev225 → specsmith-0.5.0.dev227}/src/specsmith/agent/orchestrator.py +0 -0
  33. {specsmith-0.5.0.dev225 → specsmith-0.5.0.dev227}/src/specsmith/agent/repl.py +0 -0
  34. {specsmith-0.5.0.dev225 → specsmith-0.5.0.dev227}/src/specsmith/agent/router.py +0 -0
  35. {specsmith-0.5.0.dev225 → specsmith-0.5.0.dev227}/src/specsmith/agent/rules.py +0 -0
  36. {specsmith-0.5.0.dev225 → specsmith-0.5.0.dev227}/src/specsmith/agent/safety.py +0 -0
  37. {specsmith-0.5.0.dev225 → specsmith-0.5.0.dev227}/src/specsmith/agent/tools.py +0 -0
  38. {specsmith-0.5.0.dev225 → specsmith-0.5.0.dev227}/src/specsmith/agent/verifier.py +0 -0
  39. {specsmith-0.5.0.dev225 → specsmith-0.5.0.dev227}/src/specsmith/architect.py +0 -0
  40. {specsmith-0.5.0.dev225 → specsmith-0.5.0.dev227}/src/specsmith/auditor.py +0 -0
  41. {specsmith-0.5.0.dev225 → specsmith-0.5.0.dev227}/src/specsmith/auth.py +0 -0
  42. {specsmith-0.5.0.dev225 → specsmith-0.5.0.dev227}/src/specsmith/commands/__init__.py +0 -0
  43. {specsmith-0.5.0.dev225 → specsmith-0.5.0.dev227}/src/specsmith/compressor.py +0 -0
  44. {specsmith-0.5.0.dev225 → specsmith-0.5.0.dev227}/src/specsmith/config.py +0 -0
  45. {specsmith-0.5.0.dev225 → specsmith-0.5.0.dev227}/src/specsmith/console_utils.py +0 -0
  46. {specsmith-0.5.0.dev225 → specsmith-0.5.0.dev227}/src/specsmith/credit_analyzer.py +0 -0
  47. {specsmith-0.5.0.dev225 → specsmith-0.5.0.dev227}/src/specsmith/credits.py +0 -0
  48. {specsmith-0.5.0.dev225 → specsmith-0.5.0.dev227}/src/specsmith/differ.py +0 -0
  49. {specsmith-0.5.0.dev225 → specsmith-0.5.0.dev227}/src/specsmith/doctor.py +0 -0
  50. {specsmith-0.5.0.dev225 → specsmith-0.5.0.dev227}/src/specsmith/epistemic/__init__.py +0 -0
  51. {specsmith-0.5.0.dev225 → specsmith-0.5.0.dev227}/src/specsmith/epistemic/belief.py +0 -0
  52. {specsmith-0.5.0.dev225 → specsmith-0.5.0.dev227}/src/specsmith/epistemic/certainty.py +0 -0
  53. {specsmith-0.5.0.dev225 → specsmith-0.5.0.dev227}/src/specsmith/epistemic/failure_graph.py +0 -0
  54. {specsmith-0.5.0.dev225 → specsmith-0.5.0.dev227}/src/specsmith/epistemic/recovery.py +0 -0
  55. {specsmith-0.5.0.dev225 → specsmith-0.5.0.dev227}/src/specsmith/epistemic/stress_tester.py +0 -0
  56. {specsmith-0.5.0.dev225 → specsmith-0.5.0.dev227}/src/specsmith/executor.py +0 -0
  57. {specsmith-0.5.0.dev225 → specsmith-0.5.0.dev227}/src/specsmith/exporter.py +0 -0
  58. {specsmith-0.5.0.dev225 → specsmith-0.5.0.dev227}/src/specsmith/gui/__init__.py +0 -0
  59. {specsmith-0.5.0.dev225 → specsmith-0.5.0.dev227}/src/specsmith/gui/app.py +0 -0
  60. {specsmith-0.5.0.dev225 → specsmith-0.5.0.dev227}/src/specsmith/gui/main_window.py +0 -0
  61. {specsmith-0.5.0.dev225 → specsmith-0.5.0.dev227}/src/specsmith/gui/session_tab.py +0 -0
  62. {specsmith-0.5.0.dev225 → specsmith-0.5.0.dev227}/src/specsmith/gui/theme.py +0 -0
  63. {specsmith-0.5.0.dev225 → specsmith-0.5.0.dev227}/src/specsmith/gui/widgets/__init__.py +0 -0
  64. {specsmith-0.5.0.dev225 → specsmith-0.5.0.dev227}/src/specsmith/gui/widgets/chat_view.py +0 -0
  65. {specsmith-0.5.0.dev225 → specsmith-0.5.0.dev227}/src/specsmith/gui/widgets/input_bar.py +0 -0
  66. {specsmith-0.5.0.dev225 → specsmith-0.5.0.dev227}/src/specsmith/gui/widgets/provider_bar.py +0 -0
  67. {specsmith-0.5.0.dev225 → specsmith-0.5.0.dev227}/src/specsmith/gui/widgets/token_meter.py +0 -0
  68. {specsmith-0.5.0.dev225 → specsmith-0.5.0.dev227}/src/specsmith/gui/widgets/tool_panel.py +0 -0
  69. {specsmith-0.5.0.dev225 → specsmith-0.5.0.dev227}/src/specsmith/gui/widgets/update_checker.py +0 -0
  70. {specsmith-0.5.0.dev225 → specsmith-0.5.0.dev227}/src/specsmith/gui/worker.py +0 -0
  71. {specsmith-0.5.0.dev225 → specsmith-0.5.0.dev227}/src/specsmith/importer.py +0 -0
  72. {specsmith-0.5.0.dev225 → specsmith-0.5.0.dev227}/src/specsmith/integrations/__init__.py +0 -0
  73. {specsmith-0.5.0.dev225 → specsmith-0.5.0.dev227}/src/specsmith/integrations/agent_skill.py +0 -0
  74. {specsmith-0.5.0.dev225 → specsmith-0.5.0.dev227}/src/specsmith/integrations/aider.py +0 -0
  75. {specsmith-0.5.0.dev225 → specsmith-0.5.0.dev227}/src/specsmith/integrations/base.py +0 -0
  76. {specsmith-0.5.0.dev225 → specsmith-0.5.0.dev227}/src/specsmith/integrations/claude_code.py +0 -0
  77. {specsmith-0.5.0.dev225 → specsmith-0.5.0.dev227}/src/specsmith/integrations/copilot.py +0 -0
  78. {specsmith-0.5.0.dev225 → specsmith-0.5.0.dev227}/src/specsmith/integrations/cursor.py +0 -0
  79. {specsmith-0.5.0.dev225 → specsmith-0.5.0.dev227}/src/specsmith/integrations/gemini.py +0 -0
  80. {specsmith-0.5.0.dev225 → specsmith-0.5.0.dev227}/src/specsmith/integrations/windsurf.py +0 -0
  81. {specsmith-0.5.0.dev225 → specsmith-0.5.0.dev227}/src/specsmith/languages.py +0 -0
  82. {specsmith-0.5.0.dev225 → specsmith-0.5.0.dev227}/src/specsmith/ledger.py +0 -0
  83. {specsmith-0.5.0.dev225 → specsmith-0.5.0.dev227}/src/specsmith/patent.py +0 -0
  84. {specsmith-0.5.0.dev225 → specsmith-0.5.0.dev227}/src/specsmith/phase.py +0 -0
  85. {specsmith-0.5.0.dev225 → specsmith-0.5.0.dev227}/src/specsmith/plugins.py +0 -0
  86. {specsmith-0.5.0.dev225 → specsmith-0.5.0.dev227}/src/specsmith/profiles.py +0 -0
  87. {specsmith-0.5.0.dev225 → specsmith-0.5.0.dev227}/src/specsmith/rate_limits.py +0 -0
  88. {specsmith-0.5.0.dev225 → specsmith-0.5.0.dev227}/src/specsmith/releaser.py +0 -0
  89. {specsmith-0.5.0.dev225 → specsmith-0.5.0.dev227}/src/specsmith/requirements.py +0 -0
  90. {specsmith-0.5.0.dev225 → specsmith-0.5.0.dev227}/src/specsmith/requirements_parser.py +0 -0
  91. {specsmith-0.5.0.dev225 → specsmith-0.5.0.dev227}/src/specsmith/retrieval.py +0 -0
  92. {specsmith-0.5.0.dev225 → specsmith-0.5.0.dev227}/src/specsmith/scaffolder.py +0 -0
  93. {specsmith-0.5.0.dev225 → specsmith-0.5.0.dev227}/src/specsmith/serve.py +0 -0
  94. {specsmith-0.5.0.dev225 → specsmith-0.5.0.dev227}/src/specsmith/session.py +0 -0
  95. {specsmith-0.5.0.dev225 → specsmith-0.5.0.dev227}/src/specsmith/templates/agents.md.j2 +0 -0
  96. {specsmith-0.5.0.dev225 → specsmith-0.5.0.dev227}/src/specsmith/templates/community/bug_report.md.j2 +0 -0
  97. {specsmith-0.5.0.dev225 → specsmith-0.5.0.dev227}/src/specsmith/templates/community/code_of_conduct.md.j2 +0 -0
  98. {specsmith-0.5.0.dev225 → specsmith-0.5.0.dev227}/src/specsmith/templates/community/contributing.md.j2 +0 -0
  99. {specsmith-0.5.0.dev225 → specsmith-0.5.0.dev227}/src/specsmith/templates/community/feature_request.md.j2 +0 -0
  100. {specsmith-0.5.0.dev225 → specsmith-0.5.0.dev227}/src/specsmith/templates/community/license-Apache-2.0.j2 +0 -0
  101. {specsmith-0.5.0.dev225 → specsmith-0.5.0.dev227}/src/specsmith/templates/community/license-MIT.j2 +0 -0
  102. {specsmith-0.5.0.dev225 → specsmith-0.5.0.dev227}/src/specsmith/templates/community/pull_request_template.md.j2 +0 -0
  103. {specsmith-0.5.0.dev225 → specsmith-0.5.0.dev227}/src/specsmith/templates/community/security.md.j2 +0 -0
  104. {specsmith-0.5.0.dev225 → specsmith-0.5.0.dev227}/src/specsmith/templates/docs/architecture.md.j2 +0 -0
  105. {specsmith-0.5.0.dev225 → specsmith-0.5.0.dev227}/src/specsmith/templates/docs/mkdocs.yml.j2 +0 -0
  106. {specsmith-0.5.0.dev225 → specsmith-0.5.0.dev227}/src/specsmith/templates/docs/readthedocs.yaml.j2 +0 -0
  107. {specsmith-0.5.0.dev225 → specsmith-0.5.0.dev227}/src/specsmith/templates/docs/requirements.md.j2 +0 -0
  108. {specsmith-0.5.0.dev225 → specsmith-0.5.0.dev227}/src/specsmith/templates/docs/test-spec.md.j2 +0 -0
  109. {specsmith-0.5.0.dev225 → specsmith-0.5.0.dev227}/src/specsmith/templates/editorconfig.j2 +0 -0
  110. {specsmith-0.5.0.dev225 → specsmith-0.5.0.dev227}/src/specsmith/templates/gitattributes.j2 +0 -0
  111. {specsmith-0.5.0.dev225 → specsmith-0.5.0.dev227}/src/specsmith/templates/gitignore.j2 +0 -0
  112. {specsmith-0.5.0.dev225 → specsmith-0.5.0.dev227}/src/specsmith/templates/go/go.mod.j2 +0 -0
  113. {specsmith-0.5.0.dev225 → specsmith-0.5.0.dev227}/src/specsmith/templates/go/main.go.j2 +0 -0
  114. {specsmith-0.5.0.dev225 → specsmith-0.5.0.dev227}/src/specsmith/templates/governance/belief-registry.md.j2 +0 -0
  115. {specsmith-0.5.0.dev225 → specsmith-0.5.0.dev227}/src/specsmith/templates/governance/context-budget.md.j2 +0 -0
  116. {specsmith-0.5.0.dev225 → specsmith-0.5.0.dev227}/src/specsmith/templates/governance/drift-metrics.md.j2 +0 -0
  117. {specsmith-0.5.0.dev225 → specsmith-0.5.0.dev227}/src/specsmith/templates/governance/epistemic-axioms.md.j2 +0 -0
  118. {specsmith-0.5.0.dev225 → specsmith-0.5.0.dev227}/src/specsmith/templates/governance/failure-modes.md.j2 +0 -0
  119. {specsmith-0.5.0.dev225 → specsmith-0.5.0.dev227}/src/specsmith/templates/governance/lifecycle.md.j2 +0 -0
  120. {specsmith-0.5.0.dev225 → specsmith-0.5.0.dev227}/src/specsmith/templates/governance/roles.md.j2 +0 -0
  121. {specsmith-0.5.0.dev225 → specsmith-0.5.0.dev227}/src/specsmith/templates/governance/rules.md.j2 +0 -0
  122. {specsmith-0.5.0.dev225 → specsmith-0.5.0.dev227}/src/specsmith/templates/governance/session-protocol.md.j2 +0 -0
  123. {specsmith-0.5.0.dev225 → specsmith-0.5.0.dev227}/src/specsmith/templates/governance/uncertainty-map.md.j2 +0 -0
  124. {specsmith-0.5.0.dev225 → specsmith-0.5.0.dev227}/src/specsmith/templates/governance/verification.md.j2 +0 -0
  125. {specsmith-0.5.0.dev225 → specsmith-0.5.0.dev227}/src/specsmith/templates/js/package.json.j2 +0 -0
  126. {specsmith-0.5.0.dev225 → specsmith-0.5.0.dev227}/src/specsmith/templates/ledger.md.j2 +0 -0
  127. {specsmith-0.5.0.dev225 → specsmith-0.5.0.dev227}/src/specsmith/templates/python/cli.py.j2 +0 -0
  128. {specsmith-0.5.0.dev225 → specsmith-0.5.0.dev227}/src/specsmith/templates/python/init.py.j2 +0 -0
  129. {specsmith-0.5.0.dev225 → specsmith-0.5.0.dev227}/src/specsmith/templates/python/pyproject.toml.j2 +0 -0
  130. {specsmith-0.5.0.dev225 → specsmith-0.5.0.dev227}/src/specsmith/templates/readme.md.j2 +0 -0
  131. {specsmith-0.5.0.dev225 → specsmith-0.5.0.dev227}/src/specsmith/templates/rust/Cargo.toml.j2 +0 -0
  132. {specsmith-0.5.0.dev225 → specsmith-0.5.0.dev227}/src/specsmith/templates/rust/main.rs.j2 +0 -0
  133. {specsmith-0.5.0.dev225 → specsmith-0.5.0.dev227}/src/specsmith/templates/scripts/exec.cmd.j2 +0 -0
  134. {specsmith-0.5.0.dev225 → specsmith-0.5.0.dev227}/src/specsmith/templates/scripts/exec.sh.j2 +0 -0
  135. {specsmith-0.5.0.dev225 → specsmith-0.5.0.dev227}/src/specsmith/templates/scripts/run.cmd.j2 +0 -0
  136. {specsmith-0.5.0.dev225 → specsmith-0.5.0.dev227}/src/specsmith/templates/scripts/run.sh.j2 +0 -0
  137. {specsmith-0.5.0.dev225 → specsmith-0.5.0.dev227}/src/specsmith/templates/scripts/setup.cmd.j2 +0 -0
  138. {specsmith-0.5.0.dev225 → specsmith-0.5.0.dev227}/src/specsmith/templates/scripts/setup.sh.j2 +0 -0
  139. {specsmith-0.5.0.dev225 → specsmith-0.5.0.dev227}/src/specsmith/templates/workflows/release.yml.j2 +0 -0
  140. {specsmith-0.5.0.dev225 → specsmith-0.5.0.dev227}/src/specsmith/tool_installer.py +0 -0
  141. {specsmith-0.5.0.dev225 → specsmith-0.5.0.dev227}/src/specsmith/toolrules.py +0 -0
  142. {specsmith-0.5.0.dev225 → specsmith-0.5.0.dev227}/src/specsmith/tools.py +0 -0
  143. {specsmith-0.5.0.dev225 → specsmith-0.5.0.dev227}/src/specsmith/trace.py +0 -0
  144. {specsmith-0.5.0.dev225 → specsmith-0.5.0.dev227}/src/specsmith/updater.py +0 -0
  145. {specsmith-0.5.0.dev225 → specsmith-0.5.0.dev227}/src/specsmith/upgrader.py +0 -0
  146. {specsmith-0.5.0.dev225 → specsmith-0.5.0.dev227}/src/specsmith/validator.py +0 -0
  147. {specsmith-0.5.0.dev225 → specsmith-0.5.0.dev227}/src/specsmith/vcs/__init__.py +0 -0
  148. {specsmith-0.5.0.dev225 → specsmith-0.5.0.dev227}/src/specsmith/vcs/base.py +0 -0
  149. {specsmith-0.5.0.dev225 → specsmith-0.5.0.dev227}/src/specsmith/vcs/bitbucket.py +0 -0
  150. {specsmith-0.5.0.dev225 → specsmith-0.5.0.dev227}/src/specsmith/vcs/github.py +0 -0
  151. {specsmith-0.5.0.dev225 → specsmith-0.5.0.dev227}/src/specsmith/vcs/gitlab.py +0 -0
  152. {specsmith-0.5.0.dev225 → specsmith-0.5.0.dev227}/src/specsmith/vcs_commands.py +0 -0
  153. {specsmith-0.5.0.dev225 → specsmith-0.5.0.dev227}/src/specsmith/wireframes.py +0 -0
  154. {specsmith-0.5.0.dev225 → specsmith-0.5.0.dev227}/src/specsmith/workspace.py +0 -0
  155. {specsmith-0.5.0.dev225 → specsmith-0.5.0.dev227}/src/specsmith.egg-info/dependency_links.txt +0 -0
  156. {specsmith-0.5.0.dev225 → specsmith-0.5.0.dev227}/src/specsmith.egg-info/entry_points.txt +0 -0
  157. {specsmith-0.5.0.dev225 → specsmith-0.5.0.dev227}/src/specsmith.egg-info/requires.txt +0 -0
  158. {specsmith-0.5.0.dev225 → specsmith-0.5.0.dev227}/src/specsmith.egg-info/top_level.txt +0 -0
  159. {specsmith-0.5.0.dev225 → specsmith-0.5.0.dev227}/tests/test_CMD_001.py +0 -0
  160. {specsmith-0.5.0.dev225 → specsmith-0.5.0.dev227}/tests/test_auditor.py +0 -0
  161. {specsmith-0.5.0.dev225 → specsmith-0.5.0.dev227}/tests/test_cli.py +0 -0
  162. {specsmith-0.5.0.dev225 → specsmith-0.5.0.dev227}/tests/test_cli_workflows_history_drive.py +0 -0
  163. {specsmith-0.5.0.dev225 → specsmith-0.5.0.dev227}/tests/test_compressor.py +0 -0
  164. {specsmith-0.5.0.dev225 → specsmith-0.5.0.dev227}/tests/test_e2e_nexus.py +0 -0
  165. {specsmith-0.5.0.dev225 → specsmith-0.5.0.dev227}/tests/test_epistemic.py +0 -0
  166. {specsmith-0.5.0.dev225 → specsmith-0.5.0.dev227}/tests/test_importer.py +0 -0
  167. {specsmith-0.5.0.dev225 → specsmith-0.5.0.dev227}/tests/test_integrations.py +0 -0
  168. {specsmith-0.5.0.dev225 → specsmith-0.5.0.dev227}/tests/test_nexus.py +0 -0
  169. {specsmith-0.5.0.dev225 → specsmith-0.5.0.dev227}/tests/test_phase1_4_new.py +0 -0
  170. {specsmith-0.5.0.dev225 → specsmith-0.5.0.dev227}/tests/test_rate_limits.py +0 -0
  171. {specsmith-0.5.0.dev225 → specsmith-0.5.0.dev227}/tests/test_scaffolder.py +0 -0
  172. {specsmith-0.5.0.dev225 → specsmith-0.5.0.dev227}/tests/test_smoke.py +0 -0
  173. {specsmith-0.5.0.dev225 → specsmith-0.5.0.dev227}/tests/test_tools.py +0 -0
  174. {specsmith-0.5.0.dev225 → specsmith-0.5.0.dev227}/tests/test_validator.py +0 -0
  175. {specsmith-0.5.0.dev225 → specsmith-0.5.0.dev227}/tests/test_vcs.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: specsmith
3
- Version: 0.5.0.dev225
3
+ Version: 0.5.0.dev227
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.5.0.dev225"
7
+ version = "0.5.0.dev227"
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,337 @@
1
+ # SPDX-License-Identifier: MIT
2
+ # Copyright (c) 2026 BitConcepts, LLC. All rights reserved.
3
+ """Real LLM-backed runner for `specsmith chat` (REQ-108, REQ-112-118).
4
+
5
+ This module replaces the deterministic stub that previously lived inside
6
+ `chat_cmd`. It selects the first available provider (Ollama → Anthropic →
7
+ OpenAI → Gemini) and streams the model's response as `token` events
8
+ through the supplied :class:`EventEmitter`. Output is then parsed for
9
+ ``Files changed:`` and ``Test results:`` sections so the verifier can
10
+ emit a real verdict.
11
+
12
+ The runner is deliberately defensive: any provider error (missing SDK,
13
+ unreachable endpoint, network failure) returns ``None`` so the caller
14
+ can fall back to the deterministic stub. This keeps the test suite
15
+ green on machines that have no LLM configured at all.
16
+ """
17
+
18
+ from __future__ import annotations
19
+
20
+ import json
21
+ import os
22
+ from dataclasses import dataclass, field
23
+ from pathlib import Path
24
+ from typing import Any
25
+ from urllib.error import URLError
26
+ from urllib.request import Request, urlopen
27
+
28
+ from specsmith.agent.events import EventEmitter
29
+ from specsmith.agent.verifier import (
30
+ VerifierVerdict,
31
+ report_from_chat_sections,
32
+ score,
33
+ )
34
+
35
+ DEFAULT_OLLAMA_HOST = "http://127.0.0.1:11434"
36
+ DEFAULT_OLLAMA_MODEL = os.environ.get("SPECSMITH_OLLAMA_MODEL", "qwen2.5:7b")
37
+ SYSTEM_PROMPT = (
38
+ "You are Nexus, the local-first agentic developer assistant inside "
39
+ "Specsmith. Always end your response with the canonical contract:\n"
40
+ "Plan:\n"
41
+ "Files changed:\n"
42
+ "Test results:\n"
43
+ "Next action:\n"
44
+ )
45
+
46
+
47
+ @dataclass
48
+ class ChatRunResult:
49
+ """Return value of :func:`run_chat`."""
50
+
51
+ provider: str
52
+ summary: str
53
+ files_changed: list[str] = field(default_factory=list)
54
+ verdict: VerifierVerdict | None = None
55
+ raw_text: str = ""
56
+
57
+ def to_dict(self) -> dict[str, Any]:
58
+ return {
59
+ "provider": self.provider,
60
+ "summary": self.summary,
61
+ "files_changed": list(self.files_changed),
62
+ "confidence": self.verdict.confidence if self.verdict else 0.0,
63
+ "equilibrium": self.verdict.equilibrium if self.verdict else False,
64
+ }
65
+
66
+
67
+ # ---------------------------------------------------------------------------
68
+ # Public entry point
69
+ # ---------------------------------------------------------------------------
70
+
71
+
72
+ def run_chat(
73
+ utterance: str,
74
+ *,
75
+ project_dir: Path,
76
+ profile: str,
77
+ session_id: str,
78
+ emitter: EventEmitter,
79
+ msg_block: str,
80
+ history: list[dict[str, Any]] | None = None,
81
+ confidence_target: float = 0.7,
82
+ rules_prefix: str = "",
83
+ ) -> ChatRunResult | None:
84
+ """Drive a real LLM turn. Return ``None`` if no provider is reachable."""
85
+ history = history or []
86
+ messages = _build_messages(utterance, history, rules_prefix)
87
+
88
+ # Order matters: Ollama first because it's local-first and free.
89
+ for provider in (_run_ollama, _run_anthropic, _run_openai, _run_gemini):
90
+ try:
91
+ full_text = provider(messages, emitter, msg_block)
92
+ except Exception: # noqa: BLE001 - any failure → next provider
93
+ continue
94
+ if full_text is None:
95
+ continue
96
+ return _finalize(full_text, provider.__name__, project_dir, confidence_target)
97
+ return None
98
+
99
+
100
+ def _finalize(
101
+ full_text: str,
102
+ provider_fn_name: str,
103
+ project_dir: Path,
104
+ confidence_target: float,
105
+ ) -> ChatRunResult:
106
+ sections = _parse_output_contract(full_text)
107
+ files_changed = _split_files_list(sections.get("files_changed", ""))
108
+ report = report_from_chat_sections(sections, files_changed=files_changed)
109
+ verdict = score(report, confidence_target=confidence_target)
110
+ summary = (sections.get("plan") or full_text.strip()[:200]).strip() or verdict.summary
111
+ return ChatRunResult(
112
+ provider=provider_fn_name.removeprefix("_run_"),
113
+ summary=summary,
114
+ files_changed=files_changed,
115
+ verdict=verdict,
116
+ raw_text=full_text,
117
+ )
118
+
119
+
120
+ # ---------------------------------------------------------------------------
121
+ # Provider drivers — each returns the full assembled text or None
122
+ # ---------------------------------------------------------------------------
123
+
124
+
125
+ def _run_ollama(
126
+ messages: list[dict[str, str]],
127
+ emitter: EventEmitter,
128
+ block_id: str,
129
+ ) -> str | None:
130
+ """Stream from a local Ollama daemon using only stdlib."""
131
+ host = os.environ.get("OLLAMA_HOST", DEFAULT_OLLAMA_HOST).rstrip("/")
132
+ model = os.environ.get("SPECSMITH_OLLAMA_MODEL", DEFAULT_OLLAMA_MODEL)
133
+
134
+ if not _ollama_alive(host):
135
+ return None
136
+
137
+ payload = json.dumps({"model": model, "messages": messages, "stream": True}).encode("utf-8")
138
+ req = Request( # noqa: S310 - URL is a hardcoded localhost default
139
+ f"{host}/api/chat",
140
+ data=payload,
141
+ headers={"Content-Type": "application/json"},
142
+ method="POST",
143
+ )
144
+
145
+ pieces: list[str] = []
146
+ with urlopen(req, timeout=120) as resp: # noqa: S310
147
+ for raw_line in resp:
148
+ line = raw_line.decode("utf-8", errors="replace").strip()
149
+ if not line:
150
+ continue
151
+ try:
152
+ obj = json.loads(line)
153
+ except ValueError:
154
+ continue
155
+ chunk = ((obj.get("message") or {}).get("content")) or ""
156
+ if chunk:
157
+ emitter.token(block_id, chunk)
158
+ pieces.append(chunk)
159
+ if obj.get("done"):
160
+ break
161
+ return "".join(pieces) if pieces else None
162
+
163
+
164
+ def _ollama_alive(host: str) -> bool:
165
+ try:
166
+ with urlopen(f"{host}/api/tags", timeout=2): # noqa: S310
167
+ return True
168
+ except (URLError, TimeoutError, OSError):
169
+ return False
170
+
171
+
172
+ def _run_anthropic(
173
+ messages: list[dict[str, str]],
174
+ emitter: EventEmitter,
175
+ block_id: str,
176
+ ) -> str | None:
177
+ """Use the anthropic SDK if installed and a key is configured."""
178
+ if not os.environ.get("ANTHROPIC_API_KEY"):
179
+ return None
180
+ try:
181
+ import anthropic
182
+ except ImportError:
183
+ return None
184
+
185
+ system = "\n".join(m["content"] for m in messages if m["role"] == "system")
186
+ user_msgs = [m for m in messages if m["role"] != "system"]
187
+ client = anthropic.Anthropic()
188
+ pieces: list[str] = []
189
+ with client.messages.stream(
190
+ model=os.environ.get("ANTHROPIC_MODEL", "claude-haiku-4-5"),
191
+ max_tokens=2048,
192
+ system=system,
193
+ messages=[{"role": m["role"], "content": m["content"]} for m in user_msgs],
194
+ ) as stream:
195
+ for event in stream:
196
+ text = getattr(getattr(event, "delta", None), "text", None)
197
+ if text:
198
+ emitter.token(block_id, text)
199
+ pieces.append(text)
200
+ return "".join(pieces) if pieces else None
201
+
202
+
203
+ def _run_openai(
204
+ messages: list[dict[str, str]],
205
+ emitter: EventEmitter,
206
+ block_id: str,
207
+ ) -> str | None:
208
+ """Use the openai SDK if installed and a key is configured."""
209
+ if not os.environ.get("OPENAI_API_KEY"):
210
+ return None
211
+ try:
212
+ from openai import OpenAI
213
+ except ImportError:
214
+ return None
215
+
216
+ client = OpenAI()
217
+ stream = client.chat.completions.create(
218
+ model=os.environ.get("OPENAI_MODEL", "gpt-4o-mini"),
219
+ messages=messages,
220
+ stream=True,
221
+ )
222
+ pieces: list[str] = []
223
+ for chunk in stream:
224
+ text = (chunk.choices[0].delta.content or "") if chunk.choices else ""
225
+ if text:
226
+ emitter.token(block_id, text)
227
+ pieces.append(text)
228
+ return "".join(pieces) if pieces else None
229
+
230
+
231
+ def _run_gemini(
232
+ messages: list[dict[str, str]],
233
+ emitter: EventEmitter,
234
+ block_id: str,
235
+ ) -> str | None:
236
+ """Use google-genai SDK if installed and a key is configured."""
237
+ if not os.environ.get("GOOGLE_API_KEY"):
238
+ return None
239
+ try:
240
+ from google import genai
241
+ except ImportError:
242
+ return None
243
+
244
+ client = genai.Client()
245
+ prompt = "\n".join(f"{m['role']}: {m['content']}" for m in messages)
246
+ pieces: list[str] = []
247
+ for chunk in client.models.generate_content_stream(
248
+ model=os.environ.get("GEMINI_MODEL", "gemini-2.5-flash"),
249
+ contents=prompt,
250
+ ):
251
+ text = getattr(chunk, "text", "") or ""
252
+ if text:
253
+ emitter.token(block_id, text)
254
+ pieces.append(text)
255
+ return "".join(pieces) if pieces else None
256
+
257
+
258
+ # ---------------------------------------------------------------------------
259
+ # Helpers
260
+ # ---------------------------------------------------------------------------
261
+
262
+
263
+ def _build_messages(
264
+ utterance: str,
265
+ history: list[dict[str, Any]],
266
+ rules_prefix: str,
267
+ ) -> list[dict[str, str]]:
268
+ system = SYSTEM_PROMPT
269
+ if rules_prefix:
270
+ system = f"{system}\n\nProject rules:\n{rules_prefix}"
271
+ msgs: list[dict[str, str]] = [{"role": "system", "content": system}]
272
+ for turn in history[-10:]:
273
+ text = str(turn.get("utterance") or turn.get("text") or "").strip()
274
+ if text:
275
+ msgs.append({"role": "user", "content": text})
276
+ msgs.append({"role": "user", "content": utterance})
277
+ return msgs
278
+
279
+
280
+ def _parse_output_contract(text: str) -> dict[str, str]:
281
+ """Extract canonical Nexus output sections from free-form text.
282
+
283
+ The contract is `Plan:`, `Commands to run:`, `Files changed:`,
284
+ `Diff:`, `Test results:`, `Next action:`. Sections that don't
285
+ appear are returned as empty strings.
286
+ """
287
+ keys = [
288
+ ("plan", "Plan:"),
289
+ ("commands_to_run", "Commands to run:"),
290
+ ("files_changed", "Files changed:"),
291
+ ("diff", "Diff:"),
292
+ ("test_results", "Test results:"),
293
+ ("next_action", "Next action:"),
294
+ ]
295
+ out: dict[str, str] = {key: "" for key, _ in keys}
296
+ lower = text.lower()
297
+ bounds: list[tuple[str, int]] = []
298
+ for key, header in keys:
299
+ idx = lower.find(header.lower())
300
+ if idx >= 0:
301
+ bounds.append((key, idx + len(header)))
302
+ bounds.sort(key=lambda b: b[1])
303
+ for i, (key, start) in enumerate(bounds):
304
+ if i + 1 < len(bounds):
305
+ next_key, next_pos = bounds[i + 1]
306
+ end = next_pos - len(_section_header(next_key))
307
+ else:
308
+ end = len(text)
309
+ out[key] = text[start:end].strip()
310
+ return out
311
+
312
+
313
+ def _section_header(key: str) -> str:
314
+ return {
315
+ "plan": "Plan:",
316
+ "commands_to_run": "Commands to run:",
317
+ "files_changed": "Files changed:",
318
+ "diff": "Diff:",
319
+ "test_results": "Test results:",
320
+ "next_action": "Next action:",
321
+ }[key]
322
+
323
+
324
+ def _split_files_list(text: str) -> list[str]:
325
+ items: list[str] = []
326
+ for raw in text.splitlines():
327
+ line = raw.strip()
328
+ if not line:
329
+ continue
330
+ if line.startswith(("-", "*", "+")):
331
+ line = line[1:].strip()
332
+ if line:
333
+ items.append(line)
334
+ return items
335
+
336
+
337
+ __all__ = ["ChatRunResult", "run_chat"]