specsmith 0.5.0.dev225__tar.gz → 0.5.0.dev226__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 (173) hide show
  1. {specsmith-0.5.0.dev225/src/specsmith.egg-info → specsmith-0.5.0.dev226}/PKG-INFO +1 -1
  2. {specsmith-0.5.0.dev225 → specsmith-0.5.0.dev226}/pyproject.toml +1 -1
  3. {specsmith-0.5.0.dev225 → specsmith-0.5.0.dev226}/src/specsmith/cli.py +262 -27
  4. specsmith-0.5.0.dev226/src/specsmith/skills.py +199 -0
  5. {specsmith-0.5.0.dev225 → specsmith-0.5.0.dev226/src/specsmith.egg-info}/PKG-INFO +1 -1
  6. {specsmith-0.5.0.dev225 → specsmith-0.5.0.dev226}/src/specsmith.egg-info/SOURCES.txt +3 -0
  7. specsmith-0.5.0.dev226/tests/test_chat_stdin_protocol.py +89 -0
  8. specsmith-0.5.0.dev226/tests/test_skill_marketplace.py +185 -0
  9. {specsmith-0.5.0.dev225 → specsmith-0.5.0.dev226}/LICENSE +0 -0
  10. {specsmith-0.5.0.dev225 → specsmith-0.5.0.dev226}/README.md +0 -0
  11. {specsmith-0.5.0.dev225 → specsmith-0.5.0.dev226}/setup.cfg +0 -0
  12. {specsmith-0.5.0.dev225 → specsmith-0.5.0.dev226}/src/epistemic/__init__.py +0 -0
  13. {specsmith-0.5.0.dev225 → specsmith-0.5.0.dev226}/src/epistemic/belief.py +0 -0
  14. {specsmith-0.5.0.dev225 → specsmith-0.5.0.dev226}/src/epistemic/certainty.py +0 -0
  15. {specsmith-0.5.0.dev225 → specsmith-0.5.0.dev226}/src/epistemic/failure_graph.py +0 -0
  16. {specsmith-0.5.0.dev225 → specsmith-0.5.0.dev226}/src/epistemic/py.typed +0 -0
  17. {specsmith-0.5.0.dev225 → specsmith-0.5.0.dev226}/src/epistemic/recovery.py +0 -0
  18. {specsmith-0.5.0.dev225 → specsmith-0.5.0.dev226}/src/epistemic/session.py +0 -0
  19. {specsmith-0.5.0.dev225 → specsmith-0.5.0.dev226}/src/epistemic/stress_tester.py +0 -0
  20. {specsmith-0.5.0.dev225 → specsmith-0.5.0.dev226}/src/epistemic/trace.py +0 -0
  21. {specsmith-0.5.0.dev225 → specsmith-0.5.0.dev226}/src/specsmith/__init__.py +0 -0
  22. {specsmith-0.5.0.dev225 → specsmith-0.5.0.dev226}/src/specsmith/__main__.py +0 -0
  23. {specsmith-0.5.0.dev225 → specsmith-0.5.0.dev226}/src/specsmith/agent/__init__.py +0 -0
  24. {specsmith-0.5.0.dev225 → specsmith-0.5.0.dev226}/src/specsmith/agent/broker.py +0 -0
  25. {specsmith-0.5.0.dev225 → specsmith-0.5.0.dev226}/src/specsmith/agent/cleanup.py +0 -0
  26. {specsmith-0.5.0.dev225 → specsmith-0.5.0.dev226}/src/specsmith/agent/events.py +0 -0
  27. {specsmith-0.5.0.dev225 → specsmith-0.5.0.dev226}/src/specsmith/agent/indexer.py +0 -0
  28. {specsmith-0.5.0.dev225 → specsmith-0.5.0.dev226}/src/specsmith/agent/mcp.py +0 -0
  29. {specsmith-0.5.0.dev225 → specsmith-0.5.0.dev226}/src/specsmith/agent/memory.py +0 -0
  30. {specsmith-0.5.0.dev225 → specsmith-0.5.0.dev226}/src/specsmith/agent/orchestrator.py +0 -0
  31. {specsmith-0.5.0.dev225 → specsmith-0.5.0.dev226}/src/specsmith/agent/repl.py +0 -0
  32. {specsmith-0.5.0.dev225 → specsmith-0.5.0.dev226}/src/specsmith/agent/router.py +0 -0
  33. {specsmith-0.5.0.dev225 → specsmith-0.5.0.dev226}/src/specsmith/agent/rules.py +0 -0
  34. {specsmith-0.5.0.dev225 → specsmith-0.5.0.dev226}/src/specsmith/agent/safety.py +0 -0
  35. {specsmith-0.5.0.dev225 → specsmith-0.5.0.dev226}/src/specsmith/agent/tools.py +0 -0
  36. {specsmith-0.5.0.dev225 → specsmith-0.5.0.dev226}/src/specsmith/agent/verifier.py +0 -0
  37. {specsmith-0.5.0.dev225 → specsmith-0.5.0.dev226}/src/specsmith/architect.py +0 -0
  38. {specsmith-0.5.0.dev225 → specsmith-0.5.0.dev226}/src/specsmith/auditor.py +0 -0
  39. {specsmith-0.5.0.dev225 → specsmith-0.5.0.dev226}/src/specsmith/auth.py +0 -0
  40. {specsmith-0.5.0.dev225 → specsmith-0.5.0.dev226}/src/specsmith/commands/__init__.py +0 -0
  41. {specsmith-0.5.0.dev225 → specsmith-0.5.0.dev226}/src/specsmith/compressor.py +0 -0
  42. {specsmith-0.5.0.dev225 → specsmith-0.5.0.dev226}/src/specsmith/config.py +0 -0
  43. {specsmith-0.5.0.dev225 → specsmith-0.5.0.dev226}/src/specsmith/console_utils.py +0 -0
  44. {specsmith-0.5.0.dev225 → specsmith-0.5.0.dev226}/src/specsmith/credit_analyzer.py +0 -0
  45. {specsmith-0.5.0.dev225 → specsmith-0.5.0.dev226}/src/specsmith/credits.py +0 -0
  46. {specsmith-0.5.0.dev225 → specsmith-0.5.0.dev226}/src/specsmith/differ.py +0 -0
  47. {specsmith-0.5.0.dev225 → specsmith-0.5.0.dev226}/src/specsmith/doctor.py +0 -0
  48. {specsmith-0.5.0.dev225 → specsmith-0.5.0.dev226}/src/specsmith/epistemic/__init__.py +0 -0
  49. {specsmith-0.5.0.dev225 → specsmith-0.5.0.dev226}/src/specsmith/epistemic/belief.py +0 -0
  50. {specsmith-0.5.0.dev225 → specsmith-0.5.0.dev226}/src/specsmith/epistemic/certainty.py +0 -0
  51. {specsmith-0.5.0.dev225 → specsmith-0.5.0.dev226}/src/specsmith/epistemic/failure_graph.py +0 -0
  52. {specsmith-0.5.0.dev225 → specsmith-0.5.0.dev226}/src/specsmith/epistemic/recovery.py +0 -0
  53. {specsmith-0.5.0.dev225 → specsmith-0.5.0.dev226}/src/specsmith/epistemic/stress_tester.py +0 -0
  54. {specsmith-0.5.0.dev225 → specsmith-0.5.0.dev226}/src/specsmith/executor.py +0 -0
  55. {specsmith-0.5.0.dev225 → specsmith-0.5.0.dev226}/src/specsmith/exporter.py +0 -0
  56. {specsmith-0.5.0.dev225 → specsmith-0.5.0.dev226}/src/specsmith/gui/__init__.py +0 -0
  57. {specsmith-0.5.0.dev225 → specsmith-0.5.0.dev226}/src/specsmith/gui/app.py +0 -0
  58. {specsmith-0.5.0.dev225 → specsmith-0.5.0.dev226}/src/specsmith/gui/main_window.py +0 -0
  59. {specsmith-0.5.0.dev225 → specsmith-0.5.0.dev226}/src/specsmith/gui/session_tab.py +0 -0
  60. {specsmith-0.5.0.dev225 → specsmith-0.5.0.dev226}/src/specsmith/gui/theme.py +0 -0
  61. {specsmith-0.5.0.dev225 → specsmith-0.5.0.dev226}/src/specsmith/gui/widgets/__init__.py +0 -0
  62. {specsmith-0.5.0.dev225 → specsmith-0.5.0.dev226}/src/specsmith/gui/widgets/chat_view.py +0 -0
  63. {specsmith-0.5.0.dev225 → specsmith-0.5.0.dev226}/src/specsmith/gui/widgets/input_bar.py +0 -0
  64. {specsmith-0.5.0.dev225 → specsmith-0.5.0.dev226}/src/specsmith/gui/widgets/provider_bar.py +0 -0
  65. {specsmith-0.5.0.dev225 → specsmith-0.5.0.dev226}/src/specsmith/gui/widgets/token_meter.py +0 -0
  66. {specsmith-0.5.0.dev225 → specsmith-0.5.0.dev226}/src/specsmith/gui/widgets/tool_panel.py +0 -0
  67. {specsmith-0.5.0.dev225 → specsmith-0.5.0.dev226}/src/specsmith/gui/widgets/update_checker.py +0 -0
  68. {specsmith-0.5.0.dev225 → specsmith-0.5.0.dev226}/src/specsmith/gui/worker.py +0 -0
  69. {specsmith-0.5.0.dev225 → specsmith-0.5.0.dev226}/src/specsmith/importer.py +0 -0
  70. {specsmith-0.5.0.dev225 → specsmith-0.5.0.dev226}/src/specsmith/integrations/__init__.py +0 -0
  71. {specsmith-0.5.0.dev225 → specsmith-0.5.0.dev226}/src/specsmith/integrations/agent_skill.py +0 -0
  72. {specsmith-0.5.0.dev225 → specsmith-0.5.0.dev226}/src/specsmith/integrations/aider.py +0 -0
  73. {specsmith-0.5.0.dev225 → specsmith-0.5.0.dev226}/src/specsmith/integrations/base.py +0 -0
  74. {specsmith-0.5.0.dev225 → specsmith-0.5.0.dev226}/src/specsmith/integrations/claude_code.py +0 -0
  75. {specsmith-0.5.0.dev225 → specsmith-0.5.0.dev226}/src/specsmith/integrations/copilot.py +0 -0
  76. {specsmith-0.5.0.dev225 → specsmith-0.5.0.dev226}/src/specsmith/integrations/cursor.py +0 -0
  77. {specsmith-0.5.0.dev225 → specsmith-0.5.0.dev226}/src/specsmith/integrations/gemini.py +0 -0
  78. {specsmith-0.5.0.dev225 → specsmith-0.5.0.dev226}/src/specsmith/integrations/windsurf.py +0 -0
  79. {specsmith-0.5.0.dev225 → specsmith-0.5.0.dev226}/src/specsmith/languages.py +0 -0
  80. {specsmith-0.5.0.dev225 → specsmith-0.5.0.dev226}/src/specsmith/ledger.py +0 -0
  81. {specsmith-0.5.0.dev225 → specsmith-0.5.0.dev226}/src/specsmith/patent.py +0 -0
  82. {specsmith-0.5.0.dev225 → specsmith-0.5.0.dev226}/src/specsmith/phase.py +0 -0
  83. {specsmith-0.5.0.dev225 → specsmith-0.5.0.dev226}/src/specsmith/plugins.py +0 -0
  84. {specsmith-0.5.0.dev225 → specsmith-0.5.0.dev226}/src/specsmith/profiles.py +0 -0
  85. {specsmith-0.5.0.dev225 → specsmith-0.5.0.dev226}/src/specsmith/rate_limits.py +0 -0
  86. {specsmith-0.5.0.dev225 → specsmith-0.5.0.dev226}/src/specsmith/releaser.py +0 -0
  87. {specsmith-0.5.0.dev225 → specsmith-0.5.0.dev226}/src/specsmith/requirements.py +0 -0
  88. {specsmith-0.5.0.dev225 → specsmith-0.5.0.dev226}/src/specsmith/requirements_parser.py +0 -0
  89. {specsmith-0.5.0.dev225 → specsmith-0.5.0.dev226}/src/specsmith/retrieval.py +0 -0
  90. {specsmith-0.5.0.dev225 → specsmith-0.5.0.dev226}/src/specsmith/scaffolder.py +0 -0
  91. {specsmith-0.5.0.dev225 → specsmith-0.5.0.dev226}/src/specsmith/serve.py +0 -0
  92. {specsmith-0.5.0.dev225 → specsmith-0.5.0.dev226}/src/specsmith/session.py +0 -0
  93. {specsmith-0.5.0.dev225 → specsmith-0.5.0.dev226}/src/specsmith/templates/agents.md.j2 +0 -0
  94. {specsmith-0.5.0.dev225 → specsmith-0.5.0.dev226}/src/specsmith/templates/community/bug_report.md.j2 +0 -0
  95. {specsmith-0.5.0.dev225 → specsmith-0.5.0.dev226}/src/specsmith/templates/community/code_of_conduct.md.j2 +0 -0
  96. {specsmith-0.5.0.dev225 → specsmith-0.5.0.dev226}/src/specsmith/templates/community/contributing.md.j2 +0 -0
  97. {specsmith-0.5.0.dev225 → specsmith-0.5.0.dev226}/src/specsmith/templates/community/feature_request.md.j2 +0 -0
  98. {specsmith-0.5.0.dev225 → specsmith-0.5.0.dev226}/src/specsmith/templates/community/license-Apache-2.0.j2 +0 -0
  99. {specsmith-0.5.0.dev225 → specsmith-0.5.0.dev226}/src/specsmith/templates/community/license-MIT.j2 +0 -0
  100. {specsmith-0.5.0.dev225 → specsmith-0.5.0.dev226}/src/specsmith/templates/community/pull_request_template.md.j2 +0 -0
  101. {specsmith-0.5.0.dev225 → specsmith-0.5.0.dev226}/src/specsmith/templates/community/security.md.j2 +0 -0
  102. {specsmith-0.5.0.dev225 → specsmith-0.5.0.dev226}/src/specsmith/templates/docs/architecture.md.j2 +0 -0
  103. {specsmith-0.5.0.dev225 → specsmith-0.5.0.dev226}/src/specsmith/templates/docs/mkdocs.yml.j2 +0 -0
  104. {specsmith-0.5.0.dev225 → specsmith-0.5.0.dev226}/src/specsmith/templates/docs/readthedocs.yaml.j2 +0 -0
  105. {specsmith-0.5.0.dev225 → specsmith-0.5.0.dev226}/src/specsmith/templates/docs/requirements.md.j2 +0 -0
  106. {specsmith-0.5.0.dev225 → specsmith-0.5.0.dev226}/src/specsmith/templates/docs/test-spec.md.j2 +0 -0
  107. {specsmith-0.5.0.dev225 → specsmith-0.5.0.dev226}/src/specsmith/templates/editorconfig.j2 +0 -0
  108. {specsmith-0.5.0.dev225 → specsmith-0.5.0.dev226}/src/specsmith/templates/gitattributes.j2 +0 -0
  109. {specsmith-0.5.0.dev225 → specsmith-0.5.0.dev226}/src/specsmith/templates/gitignore.j2 +0 -0
  110. {specsmith-0.5.0.dev225 → specsmith-0.5.0.dev226}/src/specsmith/templates/go/go.mod.j2 +0 -0
  111. {specsmith-0.5.0.dev225 → specsmith-0.5.0.dev226}/src/specsmith/templates/go/main.go.j2 +0 -0
  112. {specsmith-0.5.0.dev225 → specsmith-0.5.0.dev226}/src/specsmith/templates/governance/belief-registry.md.j2 +0 -0
  113. {specsmith-0.5.0.dev225 → specsmith-0.5.0.dev226}/src/specsmith/templates/governance/context-budget.md.j2 +0 -0
  114. {specsmith-0.5.0.dev225 → specsmith-0.5.0.dev226}/src/specsmith/templates/governance/drift-metrics.md.j2 +0 -0
  115. {specsmith-0.5.0.dev225 → specsmith-0.5.0.dev226}/src/specsmith/templates/governance/epistemic-axioms.md.j2 +0 -0
  116. {specsmith-0.5.0.dev225 → specsmith-0.5.0.dev226}/src/specsmith/templates/governance/failure-modes.md.j2 +0 -0
  117. {specsmith-0.5.0.dev225 → specsmith-0.5.0.dev226}/src/specsmith/templates/governance/lifecycle.md.j2 +0 -0
  118. {specsmith-0.5.0.dev225 → specsmith-0.5.0.dev226}/src/specsmith/templates/governance/roles.md.j2 +0 -0
  119. {specsmith-0.5.0.dev225 → specsmith-0.5.0.dev226}/src/specsmith/templates/governance/rules.md.j2 +0 -0
  120. {specsmith-0.5.0.dev225 → specsmith-0.5.0.dev226}/src/specsmith/templates/governance/session-protocol.md.j2 +0 -0
  121. {specsmith-0.5.0.dev225 → specsmith-0.5.0.dev226}/src/specsmith/templates/governance/uncertainty-map.md.j2 +0 -0
  122. {specsmith-0.5.0.dev225 → specsmith-0.5.0.dev226}/src/specsmith/templates/governance/verification.md.j2 +0 -0
  123. {specsmith-0.5.0.dev225 → specsmith-0.5.0.dev226}/src/specsmith/templates/js/package.json.j2 +0 -0
  124. {specsmith-0.5.0.dev225 → specsmith-0.5.0.dev226}/src/specsmith/templates/ledger.md.j2 +0 -0
  125. {specsmith-0.5.0.dev225 → specsmith-0.5.0.dev226}/src/specsmith/templates/python/cli.py.j2 +0 -0
  126. {specsmith-0.5.0.dev225 → specsmith-0.5.0.dev226}/src/specsmith/templates/python/init.py.j2 +0 -0
  127. {specsmith-0.5.0.dev225 → specsmith-0.5.0.dev226}/src/specsmith/templates/python/pyproject.toml.j2 +0 -0
  128. {specsmith-0.5.0.dev225 → specsmith-0.5.0.dev226}/src/specsmith/templates/readme.md.j2 +0 -0
  129. {specsmith-0.5.0.dev225 → specsmith-0.5.0.dev226}/src/specsmith/templates/rust/Cargo.toml.j2 +0 -0
  130. {specsmith-0.5.0.dev225 → specsmith-0.5.0.dev226}/src/specsmith/templates/rust/main.rs.j2 +0 -0
  131. {specsmith-0.5.0.dev225 → specsmith-0.5.0.dev226}/src/specsmith/templates/scripts/exec.cmd.j2 +0 -0
  132. {specsmith-0.5.0.dev225 → specsmith-0.5.0.dev226}/src/specsmith/templates/scripts/exec.sh.j2 +0 -0
  133. {specsmith-0.5.0.dev225 → specsmith-0.5.0.dev226}/src/specsmith/templates/scripts/run.cmd.j2 +0 -0
  134. {specsmith-0.5.0.dev225 → specsmith-0.5.0.dev226}/src/specsmith/templates/scripts/run.sh.j2 +0 -0
  135. {specsmith-0.5.0.dev225 → specsmith-0.5.0.dev226}/src/specsmith/templates/scripts/setup.cmd.j2 +0 -0
  136. {specsmith-0.5.0.dev225 → specsmith-0.5.0.dev226}/src/specsmith/templates/scripts/setup.sh.j2 +0 -0
  137. {specsmith-0.5.0.dev225 → specsmith-0.5.0.dev226}/src/specsmith/templates/workflows/release.yml.j2 +0 -0
  138. {specsmith-0.5.0.dev225 → specsmith-0.5.0.dev226}/src/specsmith/tool_installer.py +0 -0
  139. {specsmith-0.5.0.dev225 → specsmith-0.5.0.dev226}/src/specsmith/toolrules.py +0 -0
  140. {specsmith-0.5.0.dev225 → specsmith-0.5.0.dev226}/src/specsmith/tools.py +0 -0
  141. {specsmith-0.5.0.dev225 → specsmith-0.5.0.dev226}/src/specsmith/trace.py +0 -0
  142. {specsmith-0.5.0.dev225 → specsmith-0.5.0.dev226}/src/specsmith/updater.py +0 -0
  143. {specsmith-0.5.0.dev225 → specsmith-0.5.0.dev226}/src/specsmith/upgrader.py +0 -0
  144. {specsmith-0.5.0.dev225 → specsmith-0.5.0.dev226}/src/specsmith/validator.py +0 -0
  145. {specsmith-0.5.0.dev225 → specsmith-0.5.0.dev226}/src/specsmith/vcs/__init__.py +0 -0
  146. {specsmith-0.5.0.dev225 → specsmith-0.5.0.dev226}/src/specsmith/vcs/base.py +0 -0
  147. {specsmith-0.5.0.dev225 → specsmith-0.5.0.dev226}/src/specsmith/vcs/bitbucket.py +0 -0
  148. {specsmith-0.5.0.dev225 → specsmith-0.5.0.dev226}/src/specsmith/vcs/github.py +0 -0
  149. {specsmith-0.5.0.dev225 → specsmith-0.5.0.dev226}/src/specsmith/vcs/gitlab.py +0 -0
  150. {specsmith-0.5.0.dev225 → specsmith-0.5.0.dev226}/src/specsmith/vcs_commands.py +0 -0
  151. {specsmith-0.5.0.dev225 → specsmith-0.5.0.dev226}/src/specsmith/wireframes.py +0 -0
  152. {specsmith-0.5.0.dev225 → specsmith-0.5.0.dev226}/src/specsmith/workspace.py +0 -0
  153. {specsmith-0.5.0.dev225 → specsmith-0.5.0.dev226}/src/specsmith.egg-info/dependency_links.txt +0 -0
  154. {specsmith-0.5.0.dev225 → specsmith-0.5.0.dev226}/src/specsmith.egg-info/entry_points.txt +0 -0
  155. {specsmith-0.5.0.dev225 → specsmith-0.5.0.dev226}/src/specsmith.egg-info/requires.txt +0 -0
  156. {specsmith-0.5.0.dev225 → specsmith-0.5.0.dev226}/src/specsmith.egg-info/top_level.txt +0 -0
  157. {specsmith-0.5.0.dev225 → specsmith-0.5.0.dev226}/tests/test_CMD_001.py +0 -0
  158. {specsmith-0.5.0.dev225 → specsmith-0.5.0.dev226}/tests/test_auditor.py +0 -0
  159. {specsmith-0.5.0.dev225 → specsmith-0.5.0.dev226}/tests/test_cli.py +0 -0
  160. {specsmith-0.5.0.dev225 → specsmith-0.5.0.dev226}/tests/test_cli_workflows_history_drive.py +0 -0
  161. {specsmith-0.5.0.dev225 → specsmith-0.5.0.dev226}/tests/test_compressor.py +0 -0
  162. {specsmith-0.5.0.dev225 → specsmith-0.5.0.dev226}/tests/test_e2e_nexus.py +0 -0
  163. {specsmith-0.5.0.dev225 → specsmith-0.5.0.dev226}/tests/test_epistemic.py +0 -0
  164. {specsmith-0.5.0.dev225 → specsmith-0.5.0.dev226}/tests/test_importer.py +0 -0
  165. {specsmith-0.5.0.dev225 → specsmith-0.5.0.dev226}/tests/test_integrations.py +0 -0
  166. {specsmith-0.5.0.dev225 → specsmith-0.5.0.dev226}/tests/test_nexus.py +0 -0
  167. {specsmith-0.5.0.dev225 → specsmith-0.5.0.dev226}/tests/test_phase1_4_new.py +0 -0
  168. {specsmith-0.5.0.dev225 → specsmith-0.5.0.dev226}/tests/test_rate_limits.py +0 -0
  169. {specsmith-0.5.0.dev225 → specsmith-0.5.0.dev226}/tests/test_scaffolder.py +0 -0
  170. {specsmith-0.5.0.dev225 → specsmith-0.5.0.dev226}/tests/test_smoke.py +0 -0
  171. {specsmith-0.5.0.dev225 → specsmith-0.5.0.dev226}/tests/test_tools.py +0 -0
  172. {specsmith-0.5.0.dev225 → specsmith-0.5.0.dev226}/tests/test_validator.py +0 -0
  173. {specsmith-0.5.0.dev225 → specsmith-0.5.0.dev226}/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.dev226
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.dev226"
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"
@@ -5219,6 +5219,24 @@ main.add_command(index_group)
5219
5219
  default=True,
5220
5220
  help="Emit block-protocol JSONL events (REQ-113). On by default for chat.",
5221
5221
  )
5222
+ @click.option(
5223
+ "--interactive",
5224
+ "interactive",
5225
+ is_flag=True,
5226
+ default=False,
5227
+ help=(
5228
+ "Read decision events (tool_decision / diff_decision / comment) from "
5229
+ "stdin. Used by IDE consumers like the VS Code extension to drive "
5230
+ "the safe-mode approval flow and inline diff review."
5231
+ ),
5232
+ )
5233
+ @click.option(
5234
+ "--decision-timeout",
5235
+ "decision_timeout",
5236
+ type=float,
5237
+ default=120.0,
5238
+ help="Seconds to wait for a stdin decision before falling back to deny.",
5239
+ )
5222
5240
  def chat_cmd(
5223
5241
  utterance: str,
5224
5242
  project_dir: str,
@@ -5227,6 +5245,8 @@ def chat_cmd(
5227
5245
  profile: str,
5228
5246
  reviewer_comment: str,
5229
5247
  json_events: bool,
5248
+ interactive: bool,
5249
+ decision_timeout: float,
5230
5250
  ) -> None:
5231
5251
  """Run a single chat turn, streaming JSONL block events to stdout.
5232
5252
 
@@ -5302,35 +5322,44 @@ def chat_cmd(
5302
5322
  matched=len(scope.matched_requirements),
5303
5323
  )
5304
5324
 
5305
- # Permission gate (REQ-115). In safe mode every tool becomes a request.
5325
+ # Permission gate (REQ-115). In safe mode every tool becomes a request,
5326
+ # and (with --interactive) we then block on stdin for the user's decision.
5306
5327
  if profile == "safe":
5307
5328
  emitter.tool_request(msg_block, "execute_with_governance", {"utterance": utterance})
5308
5329
  emitter.plan_step(plan_block, "s2", "awaiting_approval")
5309
- emitter.block_complete(plan_block, status="paused")
5310
- emitter.block_complete(msg_block)
5311
- emitter.task_complete(
5312
- success=False,
5313
- confidence=0.0,
5314
- summary="Safe mode: tool execution awaiting user approval.",
5315
- profile=profile,
5316
- )
5317
- # Persist turn for memory continuity.
5318
- append_turn(
5319
- root,
5320
- sid,
5321
- {
5322
- "role": "user",
5323
- "utterance": utterance,
5324
- "profile": profile,
5325
- "intent": real_intent.value,
5326
- "status": "awaiting_approval",
5327
- },
5328
- )
5329
- click.echo(_json.dumps({"session_id": sid, "status": "awaiting_approval"}))
5330
- return
5331
5330
 
5332
- # Standard / yolo: emit a tool_call event for execute_with_governance and
5333
- # let downstream consumers route to the real harness if configured.
5331
+ decision = _read_stdin_decision("tool_decision", decision_timeout) if interactive else None
5332
+ if decision and decision.get("decision") == "approve":
5333
+ # User approved — fall through into the standard flow as if the
5334
+ # tool had been pre-authorised.
5335
+ emitter.plan_step(plan_block, "s2", "approved")
5336
+ else:
5337
+ denied_reason = (decision or {}).get("reason", "awaiting_approval")
5338
+ emitter.block_complete(plan_block, status="paused")
5339
+ emitter.block_complete(msg_block)
5340
+ emitter.task_complete(
5341
+ success=False,
5342
+ confidence=0.0,
5343
+ summary=f"Safe mode: {denied_reason}.",
5344
+ profile=profile,
5345
+ )
5346
+ append_turn(
5347
+ root,
5348
+ sid,
5349
+ {
5350
+ "role": "user",
5351
+ "utterance": utterance,
5352
+ "profile": profile,
5353
+ "intent": real_intent.value,
5354
+ "status": denied_reason,
5355
+ },
5356
+ )
5357
+ click.echo(_json.dumps({"session_id": sid, "status": denied_reason}))
5358
+ return
5359
+
5360
+ # Standard / yolo / safe-approved: emit a tool_call event for
5361
+ # execute_with_governance and let downstream consumers route to the
5362
+ # real harness if configured.
5334
5363
  emitter.tool_call(msg_block, "execute_with_governance", {"utterance": utterance})
5335
5364
  emitter.plan_step(plan_block, "s2", "complete")
5336
5365
 
@@ -5344,10 +5373,34 @@ def chat_cmd(
5344
5373
  emitter.block_complete(plan_block, status="complete")
5345
5374
  emitter.token(msg_block, summary + "\n")
5346
5375
  emitter.block_complete(msg_block)
5376
+
5377
+ # Optional inline-diff review (REQ-116) when interactive: emit one
5378
+ # representative diff block per matched requirement and read each
5379
+ # diff_decision from stdin. The first non-accept decision becomes the
5380
+ # next retry's reviewer_comment so the harness can adjust.
5381
+ extra_comment = ""
5382
+ if interactive and scope.matched_requirements:
5383
+ for req in scope.matched_requirements[:3]:
5384
+ diff_block = emitter.diff(
5385
+ path=f"docs/{req.req_id}.md",
5386
+ diff=f"--- {req.req_id} (review)\n+++ {req.req_id} (proposed)\n",
5387
+ )
5388
+ decision = _read_stdin_decision("diff_decision", decision_timeout)
5389
+ verdict = (decision or {}).get("decision", "timeout")
5390
+ comment = (decision or {}).get("comment", "")
5391
+ emitter.block_complete(diff_block, status=verdict)
5392
+ if verdict != "accept" and comment:
5393
+ extra_comment = comment
5394
+ break
5395
+
5396
+ final_summary = summary
5397
+ if extra_comment:
5398
+ final_summary += f" reviewer_comment={extra_comment!r}"
5399
+
5347
5400
  emitter.task_complete(
5348
5401
  success=True,
5349
5402
  confidence=0.7,
5350
- summary=summary,
5403
+ summary=final_summary,
5351
5404
  profile=profile,
5352
5405
  session_id=sid,
5353
5406
  parent_session=parent_session or None,
@@ -5362,13 +5415,85 @@ def chat_cmd(
5362
5415
  "utterance": utterance,
5363
5416
  "profile": profile,
5364
5417
  "intent": real_intent.value,
5365
- "reviewer_comment": reviewer_comment,
5418
+ "reviewer_comment": reviewer_comment or extra_comment,
5366
5419
  "parent_session": parent_session or None,
5367
5420
  "json_events": json_events,
5368
5421
  },
5369
5422
  )
5370
5423
 
5371
5424
 
5425
+ def _read_stdin_decision(expected_type: str, timeout_seconds: float) -> dict[str, Any] | None:
5426
+ """Read a single JSON decision line from stdin with a timeout.
5427
+
5428
+ Used by ``specsmith chat --interactive`` to wait for ``tool_decision``
5429
+ or ``diff_decision`` events emitted by an IDE client. Returns the
5430
+ parsed JSON object or ``None`` if the timeout fires, the line cannot
5431
+ be parsed, or its ``type`` does not match the expected type.
5432
+
5433
+ Cross-platform: uses ``select`` on POSIX and a polling reader thread
5434
+ on Windows so the flow stays non-blocking on either OS.
5435
+ """
5436
+ import json as _json
5437
+ import sys as _sys
5438
+
5439
+ line: str | None = None
5440
+
5441
+ # ``select`` only works on real file descriptors. Under test runners
5442
+ # (CliRunner) and other in-memory stdins, ``sys.stdin.fileno()`` raises;
5443
+ # in that case fall back to a direct ``readline()`` which the runner
5444
+ # has already pre-buffered with the supplied ``input``.
5445
+ has_fileno = True
5446
+ try:
5447
+ _sys.stdin.fileno()
5448
+ except (OSError, ValueError, AttributeError):
5449
+ has_fileno = False
5450
+
5451
+ if not has_fileno:
5452
+ try:
5453
+ line = _sys.stdin.readline()
5454
+ except Exception: # noqa: BLE001 - never let stdin issues kill chat
5455
+ line = None
5456
+ elif _sys.platform == "win32":
5457
+ # Windows has no select() on file descriptors; spawn a tiny reader
5458
+ # thread and poll a queue.
5459
+ import queue as _queue
5460
+ import threading as _threading
5461
+
5462
+ q: _queue.Queue[str] = _queue.Queue()
5463
+
5464
+ def _reader() -> None:
5465
+ data = _sys.stdin.readline()
5466
+ q.put(data)
5467
+
5468
+ t = _threading.Thread(target=_reader, daemon=True)
5469
+ t.start()
5470
+ try:
5471
+ line = q.get(timeout=timeout_seconds)
5472
+ except _queue.Empty:
5473
+ line = None
5474
+ else:
5475
+ import select as _select
5476
+
5477
+ try:
5478
+ ready, _, _ = _select.select([_sys.stdin], [], [], timeout_seconds)
5479
+ except (OSError, ValueError):
5480
+ ready = []
5481
+ if ready:
5482
+ line = _sys.stdin.readline()
5483
+
5484
+ if not line or not line.strip():
5485
+ return None
5486
+ try:
5487
+ payload = _json.loads(line.strip())
5488
+ except (TypeError, ValueError):
5489
+ return None
5490
+ if not isinstance(payload, dict):
5491
+ return None
5492
+ if payload.get("type") != expected_type:
5493
+ return None
5494
+ return payload
5495
+
5496
+
5372
5497
  # ---------------------------------------------------------------------------
5373
5498
  # Notebook — capture / replay run artifacts (REQ-123)
5374
5499
  # ---------------------------------------------------------------------------
@@ -5941,6 +6066,116 @@ def drive_pull(kind: str, project_dir: str, force: bool) -> None:
5941
6066
  main.add_command(drive_group)
5942
6067
 
5943
6068
 
6069
+ # ---------------------------------------------------------------------------
6070
+ # Skill marketplace — search / list / install community skills
6071
+ # ---------------------------------------------------------------------------
6072
+
6073
+
6074
+ @main.group(name="skill")
6075
+ def skill_group() -> None:
6076
+ """Discover, list, and install community SKILL.md files.
6077
+
6078
+ specsmith ships a small built-in catalog of reusable skills. Each entry
6079
+ is a short Markdown file describing a workflow the agent should follow
6080
+ (verifier, planner, diff-reviewer, onboarding-coach, release-pilot).
6081
+ ``specsmith skill install <slug>`` copies the SKILL.md into
6082
+ ``.agents/skills/`` so the local Nexus runtime picks it up alongside any
6083
+ project-specific skills.
6084
+ """
6085
+
6086
+
6087
+ @skill_group.command(name="search")
6088
+ @click.argument("query", required=False, default="")
6089
+ @click.option("--json", "as_json", is_flag=True, default=False)
6090
+ def skill_search(query: str, as_json: bool) -> None:
6091
+ """Search the catalog for skills matching QUERY (case-insensitive)."""
6092
+ import json as _json
6093
+
6094
+ from specsmith import skills as _skills
6095
+
6096
+ matches = _skills.search(query)
6097
+ if as_json:
6098
+ click.echo(
6099
+ _json.dumps(
6100
+ [
6101
+ {
6102
+ "slug": m.slug,
6103
+ "name": m.name,
6104
+ "description": m.description,
6105
+ "tags": list(m.tags),
6106
+ }
6107
+ for m in matches
6108
+ ],
6109
+ indent=2,
6110
+ )
6111
+ )
6112
+ return
6113
+ if not matches:
6114
+ console.print("[dim]No matching skills.[/dim]")
6115
+ return
6116
+ for entry in matches:
6117
+ console.print(f"[bold]{entry.slug}[/bold] \u2014 {entry.name}")
6118
+ console.print(f" {entry.description}")
6119
+ if entry.tags:
6120
+ console.print(f" [dim]tags: {', '.join(entry.tags)}[/dim]")
6121
+
6122
+
6123
+ @skill_group.command(name="list")
6124
+ @click.option("--project-dir", type=click.Path(exists=True), default=".")
6125
+ @click.option("--json", "as_json", is_flag=True, default=False)
6126
+ def skill_list(project_dir: str, as_json: bool) -> None:
6127
+ """Show installed skills (under .agents/skills/) and the catalog."""
6128
+ import json as _json
6129
+
6130
+ from specsmith import skills as _skills
6131
+
6132
+ root = Path(project_dir).resolve()
6133
+ installed = [p.name for p in _skills.installed_skills(root)]
6134
+ catalog = [
6135
+ {"slug": entry.slug, "name": entry.name, "installed": f"{entry.slug}.md" in installed}
6136
+ for entry in _skills.CATALOG
6137
+ ]
6138
+ if as_json:
6139
+ click.echo(_json.dumps({"installed": installed, "catalog": catalog}, indent=2))
6140
+ return
6141
+ console.print(f"[bold]Installed skills[/bold] ({len(installed)})")
6142
+ for name in installed:
6143
+ console.print(f" [green]\u2713[/green] {name}")
6144
+ if not installed:
6145
+ console.print(" [dim](none)[/dim]")
6146
+ console.print()
6147
+ console.print("[bold]Catalog[/bold]")
6148
+ for entry in catalog:
6149
+ marker = "[green]\u2713[/green]" if entry["installed"] else "[dim]\u2014[/dim]"
6150
+ console.print(f" {marker} {entry['slug']:20s} {entry['name']}")
6151
+
6152
+
6153
+ @skill_group.command(name="install")
6154
+ @click.argument("slug")
6155
+ @click.option("--project-dir", type=click.Path(exists=True), default=".")
6156
+ @click.option("--force", is_flag=True, default=False, help="Overwrite an existing file.")
6157
+ def skill_install(slug: str, project_dir: str, force: bool) -> None:
6158
+ """Install SLUG into the project's .agents/skills/ directory."""
6159
+ from specsmith import skills as _skills
6160
+
6161
+ root = Path(project_dir).resolve()
6162
+ try:
6163
+ target = _skills.install(slug, root, force=force)
6164
+ except KeyError:
6165
+ console.print(f"[red]Unknown skill: {slug}[/red]")
6166
+ console.print(" Run [bold]specsmith skill search[/bold] to browse the catalog.")
6167
+ raise SystemExit(1) from None
6168
+ except FileExistsError as exc:
6169
+ console.print(f"[yellow]{exc}[/yellow]")
6170
+ raise SystemExit(2) from None
6171
+ console.print(
6172
+ f"[green]\u2713[/green] Installed [bold]{slug}[/bold] at {target.relative_to(root)}"
6173
+ )
6174
+
6175
+
6176
+ main.add_command(skill_group)
6177
+
6178
+
5944
6179
  # ---------------------------------------------------------------------------
5945
6180
  # AG2 Agent Shell
5946
6181
  # ---------------------------------------------------------------------------
@@ -0,0 +1,199 @@
1
+ # SPDX-License-Identifier: MIT
2
+ # Copyright (c) 2026 BitConcepts, LLC. All rights reserved.
3
+ """Skill marketplace — discover and install reusable agent skill files.
4
+
5
+ specsmith ships a small built-in catalog of community skills (Markdown
6
+ SKILL.md files) that any user can drop into their project's
7
+ ``.agents/skills/`` directory. The catalog is keyed by slug; each entry
8
+ has a name, description, tags, and the SKILL.md body. Future versions
9
+ may federate to a remote registry behind a ``--registry-url`` flag, but
10
+ the built-in catalog is sufficient for the 1.0-class user experience:
11
+ ``specsmith skill search testing`` lists matching skills,
12
+ ``specsmith skill install verifier`` copies the SKILL.md into the
13
+ project, and ``specsmith skill list`` shows what's already installed.
14
+ """
15
+
16
+ from __future__ import annotations
17
+
18
+ from dataclasses import dataclass, field
19
+ from pathlib import Path
20
+
21
+
22
+ @dataclass
23
+ class SkillEntry:
24
+ """A single skill catalog entry."""
25
+
26
+ slug: str
27
+ name: str
28
+ description: str
29
+ tags: list[str] = field(default_factory=list)
30
+ body: str = ""
31
+
32
+
33
+ # ---------------------------------------------------------------------------
34
+ # Built-in catalog
35
+ # ---------------------------------------------------------------------------
36
+
37
+ CATALOG: list[SkillEntry] = [
38
+ SkillEntry(
39
+ slug="verifier",
40
+ name="Verifier — five-gate verification",
41
+ description=(
42
+ "Runs the standard five-gate verification loop: ruff, mypy, pytest, "
43
+ "pip-audit, and the project's own audit/validate. Halts with a clear "
44
+ "report at the first failing gate."
45
+ ),
46
+ tags=["verification", "ci", "python"],
47
+ body=(
48
+ "# Verifier Skill\n\n"
49
+ "## When to use\n"
50
+ "Run this skill before committing any change.\n\n"
51
+ "## Gates (in order)\n"
52
+ "1. `ruff check .` — lint clean.\n"
53
+ "2. `ruff format --check src tests` — format clean.\n"
54
+ "3. `mypy src/` — type-check clean.\n"
55
+ "4. `pytest -q` — tests pass.\n"
56
+ "5. `specsmith audit && specsmith validate` — governance clean.\n\n"
57
+ "Halt at the first failing gate and surface its output verbatim.\n"
58
+ ),
59
+ ),
60
+ SkillEntry(
61
+ slug="planner",
62
+ name="Planner — propose-then-execute",
63
+ description=(
64
+ "Forces the agent to emit a Plan block before any tool call. Each "
65
+ "plan step is recorded with explicit success criteria so the user "
66
+ "can review the approach before any code changes."
67
+ ),
68
+ tags=["planning", "governance"],
69
+ body=(
70
+ "# Planner Skill\n\n"
71
+ "## Protocol\n"
72
+ "1. Emit a Plan block listing each intended step with a success "
73
+ "criterion.\n"
74
+ "2. Wait for user confirmation when the profile is `safe`.\n"
75
+ "3. Execute steps one at a time, updating plan_step status as each "
76
+ "completes or fails.\n"
77
+ "4. Never run tool calls outside the plan.\n"
78
+ ),
79
+ ),
80
+ SkillEntry(
81
+ slug="diff-reviewer",
82
+ name="Diff Reviewer — surface changes for approval",
83
+ description=(
84
+ "After every change set, emit a `diff` block per modified file and "
85
+ "wait for an Accept / Reject decision before committing. Comments "
86
+ "are fed into the next retry as additional context."
87
+ ),
88
+ tags=["review", "diff", "governance"],
89
+ body=(
90
+ "# Diff Reviewer Skill\n\n"
91
+ "## When to use\n"
92
+ "Any task that modifies files.\n\n"
93
+ "## Protocol\n"
94
+ "1. Emit one `diff` block per file in `Files changed`.\n"
95
+ "2. Wait for `diff_decision` events on stdin (accept / reject / "
96
+ "comment).\n"
97
+ "3. If any diff is rejected, fold the comment into the next harness "
98
+ "retry.\n"
99
+ "4. Only commit once every diff has been accepted.\n"
100
+ ),
101
+ ),
102
+ SkillEntry(
103
+ slug="onboarding-coach",
104
+ name="Onboarding Coach — guided first session",
105
+ description=(
106
+ "Walks a brand-new user through a project: scaffold check, "
107
+ "REQUIREMENTS.md tour, AGENTS.md tour, suggested next preflight "
108
+ "utterance. Pairs with `specsmith doctor --onboarding`."
109
+ ),
110
+ tags=["onboarding", "documentation"],
111
+ body=(
112
+ "# Onboarding Coach Skill\n\n"
113
+ "## Sequence\n"
114
+ "1. Run `specsmith doctor --onboarding` and surface any failing "
115
+ "step.\n"
116
+ "2. Read AGENTS.md and summarise the project's hard rules in 5 "
117
+ "bullets.\n"
118
+ "3. List the top 5 P1 requirements from REQUIREMENTS.md.\n"
119
+ "4. Suggest one preflight utterance the user can run next.\n"
120
+ ),
121
+ ),
122
+ SkillEntry(
123
+ slug="release-pilot",
124
+ name="Release Pilot — gitflow release cut",
125
+ description=(
126
+ "Drives a full gitflow release: develop -> main fast-forward, "
127
+ "version bump, CHANGELOG entry, tag, PyPI publish, GitHub release. "
128
+ "Refuses to run if CI is not green."
129
+ ),
130
+ tags=["release", "vcs", "automation"],
131
+ body=(
132
+ "# Release Pilot Skill\n\n"
133
+ "## Preconditions\n"
134
+ "- `gh pr list --state open` returns 0 open PRs.\n"
135
+ "- All CI checks on develop are SUCCESS.\n"
136
+ "- CHANGELOG.md has the new version entry already drafted.\n\n"
137
+ "## Sequence\n"
138
+ "1. Bump `pyproject.toml` version.\n"
139
+ "2. Commit and push to develop.\n"
140
+ "3. Fast-forward main from develop.\n"
141
+ "4. Create annotated tag.\n"
142
+ "5. Push tags. Release workflow handles PyPI + GitHub Release.\n"
143
+ ),
144
+ ),
145
+ ]
146
+
147
+
148
+ # ---------------------------------------------------------------------------
149
+ # Helpers
150
+ # ---------------------------------------------------------------------------
151
+
152
+
153
+ def search(query: str) -> list[SkillEntry]:
154
+ """Case-insensitive substring search across slug, name, description, tags."""
155
+ needle = query.strip().lower()
156
+ if not needle:
157
+ return list(CATALOG)
158
+ matches: list[SkillEntry] = []
159
+ for entry in CATALOG:
160
+ haystack = " ".join(
161
+ [entry.slug, entry.name, entry.description, " ".join(entry.tags)]
162
+ ).lower()
163
+ if needle in haystack:
164
+ matches.append(entry)
165
+ return matches
166
+
167
+
168
+ def get(slug: str) -> SkillEntry | None:
169
+ """Return the catalog entry for ``slug`` or ``None``."""
170
+ for entry in CATALOG:
171
+ if entry.slug == slug:
172
+ return entry
173
+ return None
174
+
175
+
176
+ def installed_skills(project_dir: Path) -> list[Path]:
177
+ """Return SKILL.md files installed under ``.agents/skills/``."""
178
+ base = project_dir / ".agents" / "skills"
179
+ if not base.is_dir():
180
+ return []
181
+ return sorted(p for p in base.iterdir() if p.is_file() and p.suffix == ".md")
182
+
183
+
184
+ def install(slug: str, project_dir: Path, *, force: bool = False) -> Path:
185
+ """Copy the catalog skill into ``project_dir/.agents/skills/<slug>.md``.
186
+
187
+ Raises ``FileExistsError`` if the file is already present and ``force``
188
+ is ``False``. Raises ``KeyError`` if the slug is unknown.
189
+ """
190
+ entry = get(slug)
191
+ if entry is None:
192
+ raise KeyError(f"Unknown skill: {slug}")
193
+ base = project_dir / ".agents" / "skills"
194
+ base.mkdir(parents=True, exist_ok=True)
195
+ target = base / f"{slug}.md"
196
+ if target.exists() and not force:
197
+ raise FileExistsError(f"Already installed: {target}. Pass --force to overwrite.")
198
+ target.write_text(entry.body, encoding="utf-8")
199
+ return target
@@ -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.dev226
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
@@ -40,6 +40,7 @@ src/specsmith/retrieval.py
40
40
  src/specsmith/scaffolder.py
41
41
  src/specsmith/serve.py
42
42
  src/specsmith/session.py
43
+ src/specsmith/skills.py
43
44
  src/specsmith/tool_installer.py
44
45
  src/specsmith/toolrules.py
45
46
  src/specsmith/tools.py
@@ -151,6 +152,7 @@ src/specsmith/vcs/github.py
151
152
  src/specsmith/vcs/gitlab.py
152
153
  tests/test_CMD_001.py
153
154
  tests/test_auditor.py
155
+ tests/test_chat_stdin_protocol.py
154
156
  tests/test_cli.py
155
157
  tests/test_cli_workflows_history_drive.py
156
158
  tests/test_compressor.py
@@ -162,6 +164,7 @@ tests/test_nexus.py
162
164
  tests/test_phase1_4_new.py
163
165
  tests/test_rate_limits.py
164
166
  tests/test_scaffolder.py
167
+ tests/test_skill_marketplace.py
165
168
  tests/test_smoke.py
166
169
  tests/test_tools.py
167
170
  tests/test_validator.py
@@ -0,0 +1,89 @@
1
+ # SPDX-License-Identifier: MIT
2
+ # Copyright (c) 2026 BitConcepts, LLC. All rights reserved.
3
+ """Tests for `specsmith chat --interactive` stdin decision protocol.
4
+
5
+ The interactive flow lets an IDE consumer (e.g. the VS Code extension)
6
+ drive the safe-mode approval and inline diff review by writing JSON
7
+ decision lines to the CLI's stdin.
8
+ """
9
+
10
+ from __future__ import annotations
11
+
12
+ import json
13
+ from pathlib import Path
14
+
15
+ from click.testing import CliRunner
16
+
17
+ from specsmith.cli import main
18
+
19
+
20
+ def test_chat_safe_mode_denies_when_no_stdin(tmp_path: Path) -> None:
21
+ """Without --interactive, safe profile emits tool_request and stops."""
22
+ runner = CliRunner()
23
+ res = runner.invoke(
24
+ main,
25
+ ["chat", "add hello world", "--project-dir", str(tmp_path), "--profile", "safe"],
26
+ )
27
+ assert res.exit_code == 0, res.output
28
+ events = [json.loads(line) for line in res.output.strip().splitlines() if line.startswith("{")]
29
+ assert "tool_request" in [e.get("type") for e in events]
30
+
31
+
32
+ def test_chat_interactive_safe_mode_approve(tmp_path: Path) -> None:
33
+ """With --interactive and an approve decision on stdin, chat continues."""
34
+ runner = CliRunner()
35
+ decision = json.dumps({"type": "tool_decision", "decision": "approve"}) + "\n"
36
+ res = runner.invoke(
37
+ main,
38
+ [
39
+ "chat",
40
+ "add hello world",
41
+ "--project-dir",
42
+ str(tmp_path),
43
+ "--profile",
44
+ "safe",
45
+ "--interactive",
46
+ "--decision-timeout",
47
+ "5",
48
+ ],
49
+ input=decision,
50
+ )
51
+ assert res.exit_code == 0, res.output
52
+ # When approved, we should see a tool_call event after the tool_request.
53
+ types = [
54
+ json.loads(line).get("type")
55
+ for line in res.output.strip().splitlines()
56
+ if line.startswith("{")
57
+ ]
58
+ assert "tool_request" in types
59
+ assert "tool_call" in types
60
+
61
+
62
+ def test_chat_interactive_safe_mode_deny(tmp_path: Path) -> None:
63
+ """With --interactive and a deny decision, chat exits with task_complete success=False."""
64
+ runner = CliRunner()
65
+ decision = json.dumps({"type": "tool_decision", "decision": "deny", "reason": "not_now"}) + "\n"
66
+ res = runner.invoke(
67
+ main,
68
+ [
69
+ "chat",
70
+ "add hello world",
71
+ "--project-dir",
72
+ str(tmp_path),
73
+ "--profile",
74
+ "safe",
75
+ "--interactive",
76
+ "--decision-timeout",
77
+ "5",
78
+ ],
79
+ input=decision,
80
+ )
81
+ assert res.exit_code == 0, res.output
82
+ parsed = [
83
+ json.loads(line)
84
+ for line in res.output.strip().splitlines()
85
+ if line.startswith("{") and '"type"' in line
86
+ ]
87
+ completes = [e for e in parsed if e.get("type") == "task_complete"]
88
+ assert completes
89
+ assert completes[-1].get("success") is False