specsmith 0.6.0.dev231__tar.gz → 0.6.0.dev233__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (184) hide show
  1. {specsmith-0.6.0.dev231/src/specsmith.egg-info → specsmith-0.6.0.dev233}/PKG-INFO +6 -1
  2. {specsmith-0.6.0.dev231 → specsmith-0.6.0.dev233}/pyproject.toml +12 -1
  3. specsmith-0.6.0.dev233/src/specsmith/agent/suggester.py +264 -0
  4. specsmith-0.6.0.dev233/src/specsmith/block_export.py +106 -0
  5. {specsmith-0.6.0.dev231 → specsmith-0.6.0.dev233}/src/specsmith/cli.py +153 -0
  6. specsmith-0.6.0.dev233/src/specsmith/cloud_serve.py +150 -0
  7. specsmith-0.6.0.dev233/src/specsmith/drive.py +126 -0
  8. specsmith-0.6.0.dev233/src/specsmith/history_search.py +159 -0
  9. {specsmith-0.6.0.dev231 → specsmith-0.6.0.dev233/src/specsmith.egg-info}/PKG-INFO +6 -1
  10. {specsmith-0.6.0.dev231 → specsmith-0.6.0.dev233}/src/specsmith.egg-info/SOURCES.txt +8 -1
  11. {specsmith-0.6.0.dev231 → specsmith-0.6.0.dev233}/src/specsmith.egg-info/requires.txt +7 -0
  12. specsmith-0.6.0.dev233/tests/test_suggester.py +88 -0
  13. specsmith-0.6.0.dev233/tests/test_warp_parity.py +421 -0
  14. {specsmith-0.6.0.dev231 → specsmith-0.6.0.dev233}/LICENSE +0 -0
  15. {specsmith-0.6.0.dev231 → specsmith-0.6.0.dev233}/README.md +0 -0
  16. {specsmith-0.6.0.dev231 → specsmith-0.6.0.dev233}/setup.cfg +0 -0
  17. {specsmith-0.6.0.dev231 → specsmith-0.6.0.dev233}/src/epistemic/__init__.py +0 -0
  18. {specsmith-0.6.0.dev231 → specsmith-0.6.0.dev233}/src/epistemic/belief.py +0 -0
  19. {specsmith-0.6.0.dev231 → specsmith-0.6.0.dev233}/src/epistemic/certainty.py +0 -0
  20. {specsmith-0.6.0.dev231 → specsmith-0.6.0.dev233}/src/epistemic/failure_graph.py +0 -0
  21. {specsmith-0.6.0.dev231 → specsmith-0.6.0.dev233}/src/epistemic/py.typed +0 -0
  22. {specsmith-0.6.0.dev231 → specsmith-0.6.0.dev233}/src/epistemic/recovery.py +0 -0
  23. {specsmith-0.6.0.dev231 → specsmith-0.6.0.dev233}/src/epistemic/session.py +0 -0
  24. {specsmith-0.6.0.dev231 → specsmith-0.6.0.dev233}/src/epistemic/stress_tester.py +0 -0
  25. {specsmith-0.6.0.dev231 → specsmith-0.6.0.dev233}/src/epistemic/trace.py +0 -0
  26. {specsmith-0.6.0.dev231 → specsmith-0.6.0.dev233}/src/specsmith/__init__.py +0 -0
  27. {specsmith-0.6.0.dev231 → specsmith-0.6.0.dev233}/src/specsmith/__main__.py +0 -0
  28. {specsmith-0.6.0.dev231 → specsmith-0.6.0.dev233}/src/specsmith/agent/__init__.py +0 -0
  29. {specsmith-0.6.0.dev231 → specsmith-0.6.0.dev233}/src/specsmith/agent/broker.py +0 -0
  30. {specsmith-0.6.0.dev231 → specsmith-0.6.0.dev233}/src/specsmith/agent/chat_runner.py +0 -0
  31. {specsmith-0.6.0.dev231 → specsmith-0.6.0.dev233}/src/specsmith/agent/cleanup.py +0 -0
  32. {specsmith-0.6.0.dev231 → specsmith-0.6.0.dev233}/src/specsmith/agent/events.py +0 -0
  33. {specsmith-0.6.0.dev231 → specsmith-0.6.0.dev233}/src/specsmith/agent/indexer.py +0 -0
  34. {specsmith-0.6.0.dev231 → specsmith-0.6.0.dev233}/src/specsmith/agent/mcp.py +0 -0
  35. {specsmith-0.6.0.dev231 → specsmith-0.6.0.dev233}/src/specsmith/agent/memory.py +0 -0
  36. {specsmith-0.6.0.dev231 → specsmith-0.6.0.dev233}/src/specsmith/agent/orchestrator.py +0 -0
  37. {specsmith-0.6.0.dev231 → specsmith-0.6.0.dev233}/src/specsmith/agent/repl.py +0 -0
  38. {specsmith-0.6.0.dev231 → specsmith-0.6.0.dev233}/src/specsmith/agent/router.py +0 -0
  39. {specsmith-0.6.0.dev231 → specsmith-0.6.0.dev233}/src/specsmith/agent/rules.py +0 -0
  40. {specsmith-0.6.0.dev231 → specsmith-0.6.0.dev233}/src/specsmith/agent/safety.py +0 -0
  41. {specsmith-0.6.0.dev231 → specsmith-0.6.0.dev233}/src/specsmith/agent/tools.py +0 -0
  42. {specsmith-0.6.0.dev231 → specsmith-0.6.0.dev233}/src/specsmith/agent/verifier.py +0 -0
  43. {specsmith-0.6.0.dev231 → specsmith-0.6.0.dev233}/src/specsmith/architect.py +0 -0
  44. {specsmith-0.6.0.dev231 → specsmith-0.6.0.dev233}/src/specsmith/auditor.py +0 -0
  45. {specsmith-0.6.0.dev231 → specsmith-0.6.0.dev233}/src/specsmith/auth.py +0 -0
  46. {specsmith-0.6.0.dev231 → specsmith-0.6.0.dev233}/src/specsmith/commands/__init__.py +0 -0
  47. {specsmith-0.6.0.dev231 → specsmith-0.6.0.dev233}/src/specsmith/compressor.py +0 -0
  48. {specsmith-0.6.0.dev231 → specsmith-0.6.0.dev233}/src/specsmith/config.py +0 -0
  49. {specsmith-0.6.0.dev231 → specsmith-0.6.0.dev233}/src/specsmith/console_utils.py +0 -0
  50. {specsmith-0.6.0.dev231 → specsmith-0.6.0.dev233}/src/specsmith/credit_analyzer.py +0 -0
  51. {specsmith-0.6.0.dev231 → specsmith-0.6.0.dev233}/src/specsmith/credits.py +0 -0
  52. {specsmith-0.6.0.dev231 → specsmith-0.6.0.dev233}/src/specsmith/differ.py +0 -0
  53. {specsmith-0.6.0.dev231 → specsmith-0.6.0.dev233}/src/specsmith/doctor.py +0 -0
  54. {specsmith-0.6.0.dev231 → specsmith-0.6.0.dev233}/src/specsmith/epistemic/__init__.py +0 -0
  55. {specsmith-0.6.0.dev231 → specsmith-0.6.0.dev233}/src/specsmith/epistemic/belief.py +0 -0
  56. {specsmith-0.6.0.dev231 → specsmith-0.6.0.dev233}/src/specsmith/epistemic/certainty.py +0 -0
  57. {specsmith-0.6.0.dev231 → specsmith-0.6.0.dev233}/src/specsmith/epistemic/failure_graph.py +0 -0
  58. {specsmith-0.6.0.dev231 → specsmith-0.6.0.dev233}/src/specsmith/epistemic/recovery.py +0 -0
  59. {specsmith-0.6.0.dev231 → specsmith-0.6.0.dev233}/src/specsmith/epistemic/stress_tester.py +0 -0
  60. {specsmith-0.6.0.dev231 → specsmith-0.6.0.dev233}/src/specsmith/executor.py +0 -0
  61. {specsmith-0.6.0.dev231 → specsmith-0.6.0.dev233}/src/specsmith/exporter.py +0 -0
  62. {specsmith-0.6.0.dev231 → specsmith-0.6.0.dev233}/src/specsmith/gui/__init__.py +0 -0
  63. {specsmith-0.6.0.dev231 → specsmith-0.6.0.dev233}/src/specsmith/gui/app.py +0 -0
  64. {specsmith-0.6.0.dev231 → specsmith-0.6.0.dev233}/src/specsmith/gui/main_window.py +0 -0
  65. {specsmith-0.6.0.dev231 → specsmith-0.6.0.dev233}/src/specsmith/gui/session_tab.py +0 -0
  66. {specsmith-0.6.0.dev231 → specsmith-0.6.0.dev233}/src/specsmith/gui/theme.py +0 -0
  67. {specsmith-0.6.0.dev231 → specsmith-0.6.0.dev233}/src/specsmith/gui/widgets/__init__.py +0 -0
  68. {specsmith-0.6.0.dev231 → specsmith-0.6.0.dev233}/src/specsmith/gui/widgets/chat_view.py +0 -0
  69. {specsmith-0.6.0.dev231 → specsmith-0.6.0.dev233}/src/specsmith/gui/widgets/input_bar.py +0 -0
  70. {specsmith-0.6.0.dev231 → specsmith-0.6.0.dev233}/src/specsmith/gui/widgets/provider_bar.py +0 -0
  71. {specsmith-0.6.0.dev231 → specsmith-0.6.0.dev233}/src/specsmith/gui/widgets/token_meter.py +0 -0
  72. {specsmith-0.6.0.dev231 → specsmith-0.6.0.dev233}/src/specsmith/gui/widgets/tool_panel.py +0 -0
  73. {specsmith-0.6.0.dev231 → specsmith-0.6.0.dev233}/src/specsmith/gui/widgets/update_checker.py +0 -0
  74. {specsmith-0.6.0.dev231 → specsmith-0.6.0.dev233}/src/specsmith/gui/worker.py +0 -0
  75. {specsmith-0.6.0.dev231 → specsmith-0.6.0.dev233}/src/specsmith/importer.py +0 -0
  76. {specsmith-0.6.0.dev231 → specsmith-0.6.0.dev233}/src/specsmith/integrations/__init__.py +0 -0
  77. {specsmith-0.6.0.dev231 → specsmith-0.6.0.dev233}/src/specsmith/integrations/agent_skill.py +0 -0
  78. {specsmith-0.6.0.dev231 → specsmith-0.6.0.dev233}/src/specsmith/integrations/aider.py +0 -0
  79. {specsmith-0.6.0.dev231 → specsmith-0.6.0.dev233}/src/specsmith/integrations/base.py +0 -0
  80. {specsmith-0.6.0.dev231 → specsmith-0.6.0.dev233}/src/specsmith/integrations/claude_code.py +0 -0
  81. {specsmith-0.6.0.dev231 → specsmith-0.6.0.dev233}/src/specsmith/integrations/copilot.py +0 -0
  82. {specsmith-0.6.0.dev231 → specsmith-0.6.0.dev233}/src/specsmith/integrations/cursor.py +0 -0
  83. {specsmith-0.6.0.dev231 → specsmith-0.6.0.dev233}/src/specsmith/integrations/gemini.py +0 -0
  84. {specsmith-0.6.0.dev231 → specsmith-0.6.0.dev233}/src/specsmith/integrations/windsurf.py +0 -0
  85. {specsmith-0.6.0.dev231 → specsmith-0.6.0.dev233}/src/specsmith/languages.py +0 -0
  86. {specsmith-0.6.0.dev231 → specsmith-0.6.0.dev233}/src/specsmith/ledger.py +0 -0
  87. {specsmith-0.6.0.dev231 → specsmith-0.6.0.dev233}/src/specsmith/patent.py +0 -0
  88. {specsmith-0.6.0.dev231 → specsmith-0.6.0.dev233}/src/specsmith/phase.py +0 -0
  89. {specsmith-0.6.0.dev231 → specsmith-0.6.0.dev233}/src/specsmith/plugins.py +0 -0
  90. {specsmith-0.6.0.dev231 → specsmith-0.6.0.dev233}/src/specsmith/profiles.py +0 -0
  91. {specsmith-0.6.0.dev231 → specsmith-0.6.0.dev233}/src/specsmith/rate_limits.py +0 -0
  92. {specsmith-0.6.0.dev231 → specsmith-0.6.0.dev233}/src/specsmith/releaser.py +0 -0
  93. {specsmith-0.6.0.dev231 → specsmith-0.6.0.dev233}/src/specsmith/requirements.py +0 -0
  94. {specsmith-0.6.0.dev231 → specsmith-0.6.0.dev233}/src/specsmith/requirements_parser.py +0 -0
  95. {specsmith-0.6.0.dev231 → specsmith-0.6.0.dev233}/src/specsmith/retrieval.py +0 -0
  96. {specsmith-0.6.0.dev231 → specsmith-0.6.0.dev233}/src/specsmith/scaffolder.py +0 -0
  97. {specsmith-0.6.0.dev231 → specsmith-0.6.0.dev233}/src/specsmith/serve.py +0 -0
  98. {specsmith-0.6.0.dev231 → specsmith-0.6.0.dev233}/src/specsmith/session.py +0 -0
  99. {specsmith-0.6.0.dev231 → specsmith-0.6.0.dev233}/src/specsmith/skills.py +0 -0
  100. {specsmith-0.6.0.dev231 → specsmith-0.6.0.dev233}/src/specsmith/templates/agents.md.j2 +0 -0
  101. {specsmith-0.6.0.dev231 → specsmith-0.6.0.dev233}/src/specsmith/templates/community/bug_report.md.j2 +0 -0
  102. {specsmith-0.6.0.dev231 → specsmith-0.6.0.dev233}/src/specsmith/templates/community/code_of_conduct.md.j2 +0 -0
  103. {specsmith-0.6.0.dev231 → specsmith-0.6.0.dev233}/src/specsmith/templates/community/contributing.md.j2 +0 -0
  104. {specsmith-0.6.0.dev231 → specsmith-0.6.0.dev233}/src/specsmith/templates/community/feature_request.md.j2 +0 -0
  105. {specsmith-0.6.0.dev231 → specsmith-0.6.0.dev233}/src/specsmith/templates/community/license-Apache-2.0.j2 +0 -0
  106. {specsmith-0.6.0.dev231 → specsmith-0.6.0.dev233}/src/specsmith/templates/community/license-MIT.j2 +0 -0
  107. {specsmith-0.6.0.dev231 → specsmith-0.6.0.dev233}/src/specsmith/templates/community/pull_request_template.md.j2 +0 -0
  108. {specsmith-0.6.0.dev231 → specsmith-0.6.0.dev233}/src/specsmith/templates/community/security.md.j2 +0 -0
  109. {specsmith-0.6.0.dev231 → specsmith-0.6.0.dev233}/src/specsmith/templates/docs/architecture.md.j2 +0 -0
  110. {specsmith-0.6.0.dev231 → specsmith-0.6.0.dev233}/src/specsmith/templates/docs/mkdocs.yml.j2 +0 -0
  111. {specsmith-0.6.0.dev231 → specsmith-0.6.0.dev233}/src/specsmith/templates/docs/readthedocs.yaml.j2 +0 -0
  112. {specsmith-0.6.0.dev231 → specsmith-0.6.0.dev233}/src/specsmith/templates/docs/requirements.md.j2 +0 -0
  113. {specsmith-0.6.0.dev231 → specsmith-0.6.0.dev233}/src/specsmith/templates/docs/test-spec.md.j2 +0 -0
  114. {specsmith-0.6.0.dev231 → specsmith-0.6.0.dev233}/src/specsmith/templates/editorconfig.j2 +0 -0
  115. {specsmith-0.6.0.dev231 → specsmith-0.6.0.dev233}/src/specsmith/templates/gitattributes.j2 +0 -0
  116. {specsmith-0.6.0.dev231 → specsmith-0.6.0.dev233}/src/specsmith/templates/gitignore.j2 +0 -0
  117. {specsmith-0.6.0.dev231 → specsmith-0.6.0.dev233}/src/specsmith/templates/go/go.mod.j2 +0 -0
  118. {specsmith-0.6.0.dev231 → specsmith-0.6.0.dev233}/src/specsmith/templates/go/main.go.j2 +0 -0
  119. {specsmith-0.6.0.dev231 → specsmith-0.6.0.dev233}/src/specsmith/templates/governance/belief-registry.md.j2 +0 -0
  120. {specsmith-0.6.0.dev231 → specsmith-0.6.0.dev233}/src/specsmith/templates/governance/context-budget.md.j2 +0 -0
  121. {specsmith-0.6.0.dev231 → specsmith-0.6.0.dev233}/src/specsmith/templates/governance/drift-metrics.md.j2 +0 -0
  122. {specsmith-0.6.0.dev231 → specsmith-0.6.0.dev233}/src/specsmith/templates/governance/epistemic-axioms.md.j2 +0 -0
  123. {specsmith-0.6.0.dev231 → specsmith-0.6.0.dev233}/src/specsmith/templates/governance/failure-modes.md.j2 +0 -0
  124. {specsmith-0.6.0.dev231 → specsmith-0.6.0.dev233}/src/specsmith/templates/governance/lifecycle.md.j2 +0 -0
  125. {specsmith-0.6.0.dev231 → specsmith-0.6.0.dev233}/src/specsmith/templates/governance/roles.md.j2 +0 -0
  126. {specsmith-0.6.0.dev231 → specsmith-0.6.0.dev233}/src/specsmith/templates/governance/rules.md.j2 +0 -0
  127. {specsmith-0.6.0.dev231 → specsmith-0.6.0.dev233}/src/specsmith/templates/governance/session-protocol.md.j2 +0 -0
  128. {specsmith-0.6.0.dev231 → specsmith-0.6.0.dev233}/src/specsmith/templates/governance/uncertainty-map.md.j2 +0 -0
  129. {specsmith-0.6.0.dev231 → specsmith-0.6.0.dev233}/src/specsmith/templates/governance/verification.md.j2 +0 -0
  130. {specsmith-0.6.0.dev231 → specsmith-0.6.0.dev233}/src/specsmith/templates/js/package.json.j2 +0 -0
  131. {specsmith-0.6.0.dev231 → specsmith-0.6.0.dev233}/src/specsmith/templates/ledger.md.j2 +0 -0
  132. {specsmith-0.6.0.dev231 → specsmith-0.6.0.dev233}/src/specsmith/templates/python/cli.py.j2 +0 -0
  133. {specsmith-0.6.0.dev231 → specsmith-0.6.0.dev233}/src/specsmith/templates/python/init.py.j2 +0 -0
  134. {specsmith-0.6.0.dev231 → specsmith-0.6.0.dev233}/src/specsmith/templates/python/pyproject.toml.j2 +0 -0
  135. {specsmith-0.6.0.dev231 → specsmith-0.6.0.dev233}/src/specsmith/templates/readme.md.j2 +0 -0
  136. {specsmith-0.6.0.dev231 → specsmith-0.6.0.dev233}/src/specsmith/templates/rust/Cargo.toml.j2 +0 -0
  137. {specsmith-0.6.0.dev231 → specsmith-0.6.0.dev233}/src/specsmith/templates/rust/main.rs.j2 +0 -0
  138. {specsmith-0.6.0.dev231 → specsmith-0.6.0.dev233}/src/specsmith/templates/scripts/exec.cmd.j2 +0 -0
  139. {specsmith-0.6.0.dev231 → specsmith-0.6.0.dev233}/src/specsmith/templates/scripts/exec.sh.j2 +0 -0
  140. {specsmith-0.6.0.dev231 → specsmith-0.6.0.dev233}/src/specsmith/templates/scripts/run.cmd.j2 +0 -0
  141. {specsmith-0.6.0.dev231 → specsmith-0.6.0.dev233}/src/specsmith/templates/scripts/run.sh.j2 +0 -0
  142. {specsmith-0.6.0.dev231 → specsmith-0.6.0.dev233}/src/specsmith/templates/scripts/setup.cmd.j2 +0 -0
  143. {specsmith-0.6.0.dev231 → specsmith-0.6.0.dev233}/src/specsmith/templates/scripts/setup.sh.j2 +0 -0
  144. {specsmith-0.6.0.dev231 → specsmith-0.6.0.dev233}/src/specsmith/templates/workflows/release.yml.j2 +0 -0
  145. {specsmith-0.6.0.dev231 → specsmith-0.6.0.dev233}/src/specsmith/tool_installer.py +0 -0
  146. {specsmith-0.6.0.dev231 → specsmith-0.6.0.dev233}/src/specsmith/toolrules.py +0 -0
  147. {specsmith-0.6.0.dev231 → specsmith-0.6.0.dev233}/src/specsmith/tools.py +0 -0
  148. {specsmith-0.6.0.dev231 → specsmith-0.6.0.dev233}/src/specsmith/trace.py +0 -0
  149. {specsmith-0.6.0.dev231 → specsmith-0.6.0.dev233}/src/specsmith/updater.py +0 -0
  150. {specsmith-0.6.0.dev231 → specsmith-0.6.0.dev233}/src/specsmith/upgrader.py +0 -0
  151. {specsmith-0.6.0.dev231 → specsmith-0.6.0.dev233}/src/specsmith/validator.py +0 -0
  152. {specsmith-0.6.0.dev231 → specsmith-0.6.0.dev233}/src/specsmith/vcs/__init__.py +0 -0
  153. {specsmith-0.6.0.dev231 → specsmith-0.6.0.dev233}/src/specsmith/vcs/base.py +0 -0
  154. {specsmith-0.6.0.dev231 → specsmith-0.6.0.dev233}/src/specsmith/vcs/bitbucket.py +0 -0
  155. {specsmith-0.6.0.dev231 → specsmith-0.6.0.dev233}/src/specsmith/vcs/github.py +0 -0
  156. {specsmith-0.6.0.dev231 → specsmith-0.6.0.dev233}/src/specsmith/vcs/gitlab.py +0 -0
  157. {specsmith-0.6.0.dev231 → specsmith-0.6.0.dev233}/src/specsmith/vcs_commands.py +0 -0
  158. {specsmith-0.6.0.dev231 → specsmith-0.6.0.dev233}/src/specsmith/wireframes.py +0 -0
  159. {specsmith-0.6.0.dev231 → specsmith-0.6.0.dev233}/src/specsmith/workspace.py +0 -0
  160. {specsmith-0.6.0.dev231 → specsmith-0.6.0.dev233}/src/specsmith.egg-info/dependency_links.txt +0 -0
  161. {specsmith-0.6.0.dev231 → specsmith-0.6.0.dev233}/src/specsmith.egg-info/entry_points.txt +0 -0
  162. {specsmith-0.6.0.dev231 → specsmith-0.6.0.dev233}/src/specsmith.egg-info/top_level.txt +0 -0
  163. {specsmith-0.6.0.dev231 → specsmith-0.6.0.dev233}/tests/test_CMD_001.py +0 -0
  164. {specsmith-0.6.0.dev231 → specsmith-0.6.0.dev233}/tests/test_auditor.py +0 -0
  165. {specsmith-0.6.0.dev231 → specsmith-0.6.0.dev233}/tests/test_chat_diff_decision.py +0 -0
  166. {specsmith-0.6.0.dev231 → specsmith-0.6.0.dev233}/tests/test_chat_stdin_protocol.py +0 -0
  167. {specsmith-0.6.0.dev231 → specsmith-0.6.0.dev233}/tests/test_cli.py +0 -0
  168. {specsmith-0.6.0.dev231 → specsmith-0.6.0.dev233}/tests/test_cli_workflows_history_drive.py +0 -0
  169. {specsmith-0.6.0.dev231 → specsmith-0.6.0.dev233}/tests/test_compressor.py +0 -0
  170. {specsmith-0.6.0.dev231 → specsmith-0.6.0.dev233}/tests/test_e2e_nexus.py +0 -0
  171. {specsmith-0.6.0.dev231 → specsmith-0.6.0.dev233}/tests/test_epistemic.py +0 -0
  172. {specsmith-0.6.0.dev231 → specsmith-0.6.0.dev233}/tests/test_importer.py +0 -0
  173. {specsmith-0.6.0.dev231 → specsmith-0.6.0.dev233}/tests/test_integrations.py +0 -0
  174. {specsmith-0.6.0.dev231 → specsmith-0.6.0.dev233}/tests/test_mcp_client.py +0 -0
  175. {specsmith-0.6.0.dev231 → specsmith-0.6.0.dev233}/tests/test_nexus.py +0 -0
  176. {specsmith-0.6.0.dev231 → specsmith-0.6.0.dev233}/tests/test_phase1_4_new.py +0 -0
  177. {specsmith-0.6.0.dev231 → specsmith-0.6.0.dev233}/tests/test_phase34_completion.py +0 -0
  178. {specsmith-0.6.0.dev231 → specsmith-0.6.0.dev233}/tests/test_rate_limits.py +0 -0
  179. {specsmith-0.6.0.dev231 → specsmith-0.6.0.dev233}/tests/test_scaffolder.py +0 -0
  180. {specsmith-0.6.0.dev231 → specsmith-0.6.0.dev233}/tests/test_skill_marketplace.py +0 -0
  181. {specsmith-0.6.0.dev231 → specsmith-0.6.0.dev233}/tests/test_smoke.py +0 -0
  182. {specsmith-0.6.0.dev231 → specsmith-0.6.0.dev233}/tests/test_tools.py +0 -0
  183. {specsmith-0.6.0.dev231 → specsmith-0.6.0.dev233}/tests/test_validator.py +0 -0
  184. {specsmith-0.6.0.dev231 → specsmith-0.6.0.dev233}/tests/test_vcs.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: specsmith
3
- Version: 0.6.0.dev231
3
+ Version: 0.6.0.dev233
4
4
  Summary: Applied Epistemic Engineering toolkit — AEE agent sessions, execution profiles, FPGA/HDL governance, tool installer, 50+ CLI commands.
5
5
  Author: BitConcepts
6
6
  License-Expression: MIT
@@ -53,6 +53,11 @@ Provides-Extra: gui
53
53
  Requires-Dist: PySide6>=6.6; extra == "gui"
54
54
  Provides-Extra: ag2
55
55
  Requires-Dist: ag2[ollama]; extra == "ag2"
56
+ Provides-Extra: history-semantic
57
+ Requires-Dist: sentence-transformers>=2.2; extra == "history-semantic"
58
+ Requires-Dist: numpy>=1.24; extra == "history-semantic"
59
+ Provides-Extra: voice
60
+ Requires-Dist: whisper-cpp-python>=0.2; extra == "voice"
56
61
  Provides-Extra: agent
57
62
  Requires-Dist: anthropic>=0.56; extra == "agent"
58
63
  Requires-Dist: openai>=1.0; extra == "agent"
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "specsmith"
7
- version = "0.6.0.dev231"
7
+ version = "0.6.0.dev233"
8
8
  description = "Applied Epistemic Engineering toolkit — AEE agent sessions, execution profiles, FPGA/HDL governance, tool installer, 50+ CLI commands."
9
9
  readme = "README.md"
10
10
  license = "MIT"
@@ -67,6 +67,11 @@ mistral = ["openai>=1.0"] # Mistral uses the openai SDK pointed at api.mistral.
67
67
  gui = ["PySide6>=6.6"]
68
68
  # AG2 agent shell (Planner/Builder/Verifier over Ollama)
69
69
  ag2 = ["ag2[ollama]"]
70
+ # Optional semantic backend for `specsmith history search --semantic` (REQ-135).
71
+ # Falls back gracefully to keyword matching if these are not installed.
72
+ history-semantic = ["sentence-transformers>=2.2", "numpy>=1.24"]
73
+ # Optional whisper-cpp wrapper for the voice agent input (REQ-141).
74
+ voice = ["whisper-cpp-python>=0.2"]
70
75
  # Install all optional LLM providers
71
76
  agent = ["anthropic>=0.56", "openai>=1.0"]
72
77
  # Convenience bundle: everything
@@ -138,6 +143,12 @@ module = [
138
143
  "yaml.*",
139
144
  "keyring", # optional OS credential store; stubs not published
140
145
  "keyring.*",
146
+ "numpy", # optional [history-semantic] extra (REQ-135)
147
+ "numpy.*",
148
+ "sentence_transformers", # optional [history-semantic] extra (REQ-135)
149
+ "sentence_transformers.*",
150
+ "whisper_cpp_python", # optional [voice] extra (REQ-141)
151
+ "whisper_cpp_python.*",
141
152
  ]
142
153
  ignore_missing_imports = true
143
154
 
@@ -0,0 +1,264 @@
1
+ # SPDX-License-Identifier: MIT
2
+ # Copyright (c) 2026 BitConcepts, LLC. All rights reserved.
3
+ """Lightweight NL-to-command suggester for `specsmith suggest-command` (REQ-131).
4
+
5
+ Given a partial natural-language fragment, return a structured suggestion
6
+ that the VS Code extension renders as inline ghost-text in the chat input.
7
+ Three classification buckets:
8
+
9
+ * ``command`` -- the input is shell-y (starts with an imperative verb that
10
+ maps to a known CLI). Suggest a concrete shell command.
11
+ * ``utterance`` -- the input is plain English meant for the agent. Suggest
12
+ a refined utterance that names a likely component (best-effort).
13
+ * ``passthrough`` -- input is too short or ambiguous; echo it back so the
14
+ ghost-text matches what the user typed (no-op suggestion).
15
+
16
+ The suggester is **deterministic and LLM-free**. The IDE may layer an LLM
17
+ predictor on top, but the CLI baseline must always succeed quickly. If the
18
+ extension wants a richer suggestion, it can call `specsmith preflight
19
+ --predict-only` separately for utterances.
20
+ """
21
+
22
+ from __future__ import annotations
23
+
24
+ import re
25
+ from dataclasses import dataclass, field
26
+ from pathlib import Path
27
+ from typing import Any
28
+
29
+ # Common imperative verbs that map to shell-y intents.
30
+ _SHELL_VERBS = {
31
+ "run",
32
+ "exec",
33
+ "execute",
34
+ "kill",
35
+ "stop",
36
+ "start",
37
+ "restart",
38
+ "build",
39
+ "test",
40
+ "lint",
41
+ "format",
42
+ "git",
43
+ "cd",
44
+ "ls",
45
+ "cat",
46
+ "rm",
47
+ "mv",
48
+ "cp",
49
+ "find",
50
+ "grep",
51
+ "ps",
52
+ "top",
53
+ "open",
54
+ "edit",
55
+ "tail",
56
+ "head",
57
+ "make",
58
+ "npm",
59
+ "pnpm",
60
+ "yarn",
61
+ "pip",
62
+ "pipx",
63
+ "uv",
64
+ "pytest",
65
+ "ruff",
66
+ "mypy",
67
+ "cargo",
68
+ "go",
69
+ "docker",
70
+ "kubectl",
71
+ "terraform",
72
+ }
73
+
74
+ # Map verb -> default refined command.
75
+ _VERB_TEMPLATES: dict[str, str] = {
76
+ "run tests": "pytest -q",
77
+ "run lint": "ruff check .",
78
+ "run mypy": "mypy src/",
79
+ "format": "ruff format .",
80
+ "lint": "ruff check .",
81
+ "test": "pytest -q",
82
+ "build": "python -m build",
83
+ "git status": "git --no-pager status",
84
+ "git log": "git --no-pager log --oneline -20",
85
+ "git diff": "git --no-pager diff",
86
+ }
87
+
88
+
89
+ @dataclass
90
+ class CommandSuggestion:
91
+ """Output payload of :func:`suggest_command`."""
92
+
93
+ kind: str # "command" | "utterance" | "passthrough"
94
+ suggestion: str
95
+ confidence: float = 0.5
96
+ reasoning: str = ""
97
+ candidates: list[str] = field(default_factory=list)
98
+
99
+ def to_dict(self) -> dict[str, Any]:
100
+ return {
101
+ "kind": self.kind,
102
+ "suggestion": self.suggestion,
103
+ "confidence": round(self.confidence, 3),
104
+ "reasoning": self.reasoning,
105
+ "candidates": list(self.candidates),
106
+ }
107
+
108
+
109
+ def classify(text: str) -> str:
110
+ """Return ``command``, ``utterance``, or ``passthrough``."""
111
+ stripped = text.strip()
112
+ if len(stripped) < 2:
113
+ return "passthrough"
114
+ first = stripped.split()[0].lower()
115
+ if first in _SHELL_VERBS:
116
+ return "command"
117
+ return "utterance"
118
+
119
+
120
+ def suggest_command(text: str, *, project_dir: Path | None = None) -> CommandSuggestion:
121
+ """Return a structured suggestion for ``text``.
122
+
123
+ The suggester is deterministic. It looks for verb prefixes and a short
124
+ catalogue of common templates; if nothing matches, it returns the input
125
+ unchanged with kind=``passthrough``.
126
+ """
127
+ stripped = text.strip()
128
+ kind = classify(stripped)
129
+ if kind == "passthrough":
130
+ return CommandSuggestion(
131
+ kind="passthrough",
132
+ suggestion=text,
133
+ confidence=0.0,
134
+ reasoning="input too short to suggest",
135
+ )
136
+ if kind == "utterance":
137
+ return _suggest_utterance(stripped, project_dir=project_dir)
138
+ return _suggest_shell(stripped)
139
+
140
+
141
+ def _suggest_shell(text: str) -> CommandSuggestion:
142
+ lower = text.lower()
143
+ # Direct multi-word template match (e.g. "run tests").
144
+ for phrase, command in _VERB_TEMPLATES.items():
145
+ if lower.startswith(phrase):
146
+ return CommandSuggestion(
147
+ kind="command",
148
+ suggestion=command,
149
+ confidence=0.85,
150
+ reasoning=f"matched template '{phrase}'",
151
+ )
152
+ # Single-verb fallback: if the user typed "git" alone, propose
153
+ # `git status`. If "test", propose pytest -q.
154
+ first = lower.split()[0]
155
+ fallback = {
156
+ "git": "git --no-pager status",
157
+ "ls": "ls -la",
158
+ "test": "pytest -q",
159
+ "lint": "ruff check .",
160
+ "format": "ruff format .",
161
+ "build": "python -m build",
162
+ "find": "find . -name '*.py'",
163
+ }.get(first)
164
+ if fallback and lower.strip() == first:
165
+ return CommandSuggestion(
166
+ kind="command",
167
+ suggestion=fallback,
168
+ confidence=0.7,
169
+ reasoning=f"single verb '{first}' resolved to default command",
170
+ )
171
+ # Pass through what the user typed; mark as command anyway so the IDE
172
+ # knows it's shell-y rather than NL.
173
+ return CommandSuggestion(
174
+ kind="command",
175
+ suggestion=text,
176
+ confidence=0.3,
177
+ reasoning="recognised as shell command but no template applied",
178
+ )
179
+
180
+
181
+ _REQ_REGEX = re.compile(r"REQ-[A-Z0-9-]+", re.IGNORECASE)
182
+ _KNOWN_VERBS = ("add", "fix", "refactor", "remove", "rename", "document", "test")
183
+
184
+
185
+ def _suggest_utterance(text: str, *, project_dir: Path | None) -> CommandSuggestion:
186
+ lower = text.lower()
187
+ candidates: list[str] = []
188
+
189
+ # If the text already names a REQ, surface it verbatim with a higher
190
+ # confidence — the user is already specific.
191
+ matched = _REQ_REGEX.findall(text)
192
+ if matched:
193
+ return CommandSuggestion(
194
+ kind="utterance",
195
+ suggestion=text,
196
+ confidence=0.9,
197
+ reasoning=f"references {matched[0]} explicitly",
198
+ candidates=matched,
199
+ )
200
+
201
+ # If the text starts with a change verb but doesn't name a component,
202
+ # suggest a refined version that asks the user to add a target.
203
+ first = lower.split()[0] if lower.split() else ""
204
+ if first in _KNOWN_VERBS and len(lower.split()) <= 3:
205
+ return CommandSuggestion(
206
+ kind="utterance",
207
+ suggestion=f"{text.rstrip()} (please name the component or file)",
208
+ confidence=0.6,
209
+ reasoning=f"verb '{first}' lacks an explicit target",
210
+ )
211
+
212
+ # Project-aware refinement: scan REQUIREMENTS.md for keywords that match
213
+ # the input and propose the first hit. Best-effort; never blocks.
214
+ if project_dir is not None:
215
+ candidates = _scan_requirements(text, project_dir)
216
+ if candidates:
217
+ return CommandSuggestion(
218
+ kind="utterance",
219
+ suggestion=f"{text.rstrip()} ({candidates[0]})",
220
+ confidence=0.65,
221
+ reasoning=f"matched {candidates[0]} from REQUIREMENTS.md",
222
+ candidates=candidates,
223
+ )
224
+
225
+ # Default: echo back unchanged.
226
+ return CommandSuggestion(
227
+ kind="utterance",
228
+ suggestion=text,
229
+ confidence=0.4,
230
+ reasoning="no project-specific refinement available",
231
+ )
232
+
233
+
234
+ def _scan_requirements(text: str, project_dir: Path) -> list[str]:
235
+ """Return up to 5 REQ ids whose description shares words with ``text``."""
236
+ candidates: list[tuple[int, str]] = []
237
+ for path in (
238
+ project_dir / "REQUIREMENTS.md",
239
+ project_dir / "docs" / "REQUIREMENTS.md",
240
+ ):
241
+ if not path.is_file():
242
+ continue
243
+ try:
244
+ content = path.read_text(encoding="utf-8")
245
+ except OSError:
246
+ continue
247
+ words = {w.lower() for w in re.findall(r"[A-Za-z]{4,}", text)}
248
+ if not words:
249
+ return []
250
+ for match in re.finditer(
251
+ r"^###?\s+(REQ-[A-Z0-9-]+)\s*(.*?)(?=^###?\s+REQ|^##\s|\Z)",
252
+ content,
253
+ re.MULTILINE | re.DOTALL,
254
+ ):
255
+ req_id, body = match.group(1), match.group(2)
256
+ body_words = {w.lower() for w in re.findall(r"[A-Za-z]{4,}", body)}
257
+ score = len(words & body_words)
258
+ if score > 0:
259
+ candidates.append((score, req_id))
260
+ candidates.sort(key=lambda x: (-x[0], x[1]))
261
+ return [req for _, req in candidates[:5]]
262
+
263
+
264
+ __all__ = ["CommandSuggestion", "classify", "suggest_command"]
@@ -0,0 +1,106 @@
1
+ # SPDX-License-Identifier: MIT
2
+ # Copyright (c) 2026 BitConcepts, LLC. All rights reserved.
3
+ """Per-block share/export for `specsmith chat` (REQ-134).
4
+
5
+ Reads ``.specsmith/sessions/<session_id>/events.jsonl`` (the chat replay log
6
+ or, fallback, ``turns.jsonl``) and slices a single block out as a
7
+ self-contained Markdown / JSON / HTML snippet.
8
+ """
9
+
10
+ from __future__ import annotations
11
+
12
+ import html
13
+ import json
14
+ from pathlib import Path
15
+ from typing import Any
16
+
17
+
18
+ def _events_path(project_dir: Path, session_id: str) -> Path | None:
19
+ base = project_dir / ".specsmith" / "sessions" / session_id
20
+ candidates = [
21
+ base / "events.jsonl",
22
+ base / "turns.jsonl",
23
+ ]
24
+ for c in candidates:
25
+ if c.is_file():
26
+ return c
27
+ return None
28
+
29
+
30
+ def _read_events(events_path: Path) -> list[dict[str, Any]]:
31
+ out: list[dict[str, Any]] = []
32
+ for line in events_path.read_text(encoding="utf-8").splitlines():
33
+ line = line.strip()
34
+ if not line:
35
+ continue
36
+ try:
37
+ obj = json.loads(line)
38
+ except ValueError:
39
+ continue
40
+ if isinstance(obj, dict):
41
+ out.append(obj)
42
+ return out
43
+
44
+
45
+ def slice_block(events: list[dict[str, Any]], block_id: str) -> list[dict[str, Any]]:
46
+ """Return all events tagged with ``block_id``, plus the bracketing
47
+ block_start/block_complete events that defined it.
48
+ """
49
+ out: list[dict[str, Any]] = []
50
+ for evt in events:
51
+ if evt.get("block_id") == block_id or evt.get("id") == block_id:
52
+ out.append(evt)
53
+ return out
54
+
55
+
56
+ def export_block(
57
+ project_dir: Path,
58
+ session_id: str,
59
+ block_id: str,
60
+ *,
61
+ fmt: str = "md",
62
+ ) -> str:
63
+ """Export the events for ``block_id`` as a string in ``fmt``.
64
+
65
+ Raises FileNotFoundError if no session log exists.
66
+ Raises KeyError if the block is not found.
67
+ """
68
+ events_path = _events_path(project_dir, session_id)
69
+ if events_path is None:
70
+ raise FileNotFoundError(f"No session log for {session_id} in {project_dir}")
71
+ events = _read_events(events_path)
72
+ matching = slice_block(events, block_id)
73
+ if not matching:
74
+ raise KeyError(f"block_id {block_id} not found in session {session_id}")
75
+ if fmt == "json":
76
+ return json.dumps(matching, indent=2)
77
+ if fmt == "html":
78
+ rows = "".join(
79
+ f"<li><pre>{html.escape(json.dumps(evt, indent=2))}</pre></li>" for evt in matching
80
+ )
81
+ return (
82
+ f"<!DOCTYPE html><html><head><meta charset='utf-8'>"
83
+ f"<title>specsmith block {html.escape(block_id)}</title></head>"
84
+ f"<body><h1>Block {html.escape(block_id)}</h1>"
85
+ f"<p>session {html.escape(session_id)}</p><ol>{rows}</ol></body></html>"
86
+ )
87
+ # default: markdown
88
+ lines: list[str] = [
89
+ f"# Block `{block_id}`",
90
+ f"_session_: `{session_id}`",
91
+ "",
92
+ ]
93
+ for evt in matching:
94
+ kind = str(evt.get("type", "event"))
95
+ lines.append(f"## {kind}")
96
+ if "text" in evt:
97
+ lines.append(str(evt["text"]))
98
+ else:
99
+ lines.append("```json")
100
+ lines.append(json.dumps(evt, indent=2))
101
+ lines.append("```")
102
+ lines.append("")
103
+ return "\n".join(lines)
104
+
105
+
106
+ __all__ = ["export_block", "slice_block"]
@@ -4440,6 +4440,159 @@ def info_cmd(as_json: bool, section: str) -> None:
4440
4440
  # ---------------------------------------------------------------------------
4441
4441
 
4442
4442
 
4443
+ # ---------------------------------------------------------------------------
4444
+ # specsmith chat-export-block — self-contained block share (REQ-134)
4445
+ # ---------------------------------------------------------------------------
4446
+ #
4447
+ # This is exposed at the top level (rather than under ``chat``) because the
4448
+ # existing ``specsmith chat <utterance>`` command takes a positional argument
4449
+ # and cannot simultaneously act as a Click group.
4450
+
4451
+
4452
+ @main.command(name="chat-export-block")
4453
+ @click.option("--project-dir", type=click.Path(exists=True), default=".")
4454
+ @click.option("--session-id", "session_id", required=True)
4455
+ @click.option("--block-id", "block_id", required=True)
4456
+ @click.option(
4457
+ "--format",
4458
+ "fmt",
4459
+ type=click.Choice(["md", "json", "html"]),
4460
+ default="md",
4461
+ )
4462
+ def chat_export_block_cmd(project_dir: str, session_id: str, block_id: str, fmt: str) -> None:
4463
+ """Export one chat block as a self-contained snippet (REQ-134)."""
4464
+ from specsmith.block_export import export_block
4465
+
4466
+ try:
4467
+ out = export_block(
4468
+ Path(project_dir).resolve(),
4469
+ session_id,
4470
+ block_id,
4471
+ fmt=fmt,
4472
+ )
4473
+ except FileNotFoundError as exc:
4474
+ console.print(f"[red]{exc}[/red]")
4475
+ raise SystemExit(1) from exc
4476
+ except KeyError as exc:
4477
+ console.print(f"[red]{exc}[/red]")
4478
+ raise SystemExit(1) from exc
4479
+ click.echo(out)
4480
+
4481
+
4482
+ # ---------------------------------------------------------------------------
4483
+ # specsmith cloud serve — reference cloud-agent receiver (REQ-136)
4484
+ # ---------------------------------------------------------------------------
4485
+
4486
+
4487
+ @main.command(name="cloud-serve")
4488
+ @click.option("--host", default="127.0.0.1")
4489
+ @click.option("--port", type=int, default=9000)
4490
+ @click.option("--token", default="", help="Optional bearer token.")
4491
+ @click.option("--allow-cidr", default="", help="CIDR range required to bind non-loopback.")
4492
+ def cloud_serve_cmd(host: str, port: int, token: str, allow_cidr: str) -> None:
4493
+ """Run the reference cloud-agent receiver (REQ-136).
4494
+
4495
+ Accepts POST /spawn with a JSON manifest, persists it under
4496
+ ~/.specsmith/cloud-runs/<run_id>/manifest.json, and returns 202 with
4497
+ a stream_url placeholder.
4498
+ """
4499
+ from specsmith.cloud_serve import CloudReceiverConfig, make_server
4500
+
4501
+ config = CloudReceiverConfig(host=host, port=port, token=token, allow_cidr=allow_cidr)
4502
+ try:
4503
+ server = make_server(config)
4504
+ except RuntimeError as exc:
4505
+ console.print(f"[red]{exc}[/red]")
4506
+ raise SystemExit(2) from exc
4507
+ console.print(
4508
+ f"[bold]specsmith cloud serve[/bold] on http://{config.host}:{config.port}\n"
4509
+ f" storage: {config.storage_dir}\n"
4510
+ f" token: {'(set)' if token else '(none)'}\n"
4511
+ " Press Ctrl+C to stop."
4512
+ )
4513
+ try:
4514
+ server.serve_forever()
4515
+ except KeyboardInterrupt:
4516
+ console.print("\n[dim]cloud serve stopped.[/dim]")
4517
+ server.server_close()
4518
+
4519
+
4520
+ # ---------------------------------------------------------------------------
4521
+ # specsmith api-surface — 1.0 stability snapshot (REQ-140)
4522
+ # ---------------------------------------------------------------------------
4523
+
4524
+
4525
+ @main.command(name="api-surface")
4526
+ @click.option(
4527
+ "--snapshot",
4528
+ type=click.Path(),
4529
+ default="",
4530
+ help="Write the current public surface to this JSON file.",
4531
+ )
4532
+ def api_surface_cmd(snapshot: str) -> None:
4533
+ """Print the frozen public CLI/API surface as JSON (REQ-140)."""
4534
+ import json as _json
4535
+
4536
+ surface = {
4537
+ "cli_commands": sorted(
4538
+ cmd_name for cmd_name in main.commands if not cmd_name.startswith("_")
4539
+ ),
4540
+ "exit_codes": {
4541
+ "preflight_accepted": 0,
4542
+ "preflight_needs_clarification": 2,
4543
+ "preflight_blocked": 3,
4544
+ "verify_ok": 0,
4545
+ "verify_retry": 2,
4546
+ "verify_stop": 3,
4547
+ },
4548
+ "event_types": [
4549
+ "block_start",
4550
+ "block_complete",
4551
+ "token",
4552
+ "plan_step",
4553
+ "tool_call",
4554
+ "tool_request",
4555
+ "tool_result",
4556
+ "diff",
4557
+ "task_complete",
4558
+ ],
4559
+ }
4560
+ payload = _json.dumps(surface, indent=2, sort_keys=True)
4561
+ if snapshot:
4562
+ Path(snapshot).write_text(payload, encoding="utf-8")
4563
+ click.echo(payload)
4564
+
4565
+
4566
+ # ---------------------------------------------------------------------------
4567
+ # specsmith suggest-command — NL-to-command suggester (REQ-131)
4568
+ # ---------------------------------------------------------------------------
4569
+
4570
+
4571
+ @main.command(name="suggest-command")
4572
+ @click.argument("text")
4573
+ @click.option("--project-dir", type=click.Path(exists=True), default=".")
4574
+ @click.option(
4575
+ "--json",
4576
+ "as_json",
4577
+ is_flag=True,
4578
+ default=True,
4579
+ help="Emit suggestion as JSON (default; only mode for now).",
4580
+ )
4581
+ def suggest_command_cmd(text: str, project_dir: str, as_json: bool) -> None:
4582
+ """Suggest a refined command or utterance for a partial input (REQ-131).
4583
+
4584
+ Returns a JSON object: ``{kind, suggestion, confidence, reasoning, candidates}``.
4585
+ ``kind`` is one of ``command``, ``utterance``, ``passthrough``. The
4586
+ extension renders the suggestion as inline ghost-text.
4587
+ """
4588
+ import json as _json
4589
+
4590
+ from specsmith.agent.suggester import suggest_command
4591
+
4592
+ result = suggest_command(text, project_dir=Path(project_dir).resolve())
4593
+ click.echo(_json.dumps(result.to_dict(), indent=2))
4594
+
4595
+
4443
4596
  @main.command(name="scan")
4444
4597
  @click.option("--project-dir", type=click.Path(exists=True), default=".")
4445
4598
  @click.option("--json", "as_json", is_flag=True, default=False, help="Output as JSON.")