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.
- {specsmith-0.5.0.dev225/src/specsmith.egg-info → specsmith-0.5.0.dev226}/PKG-INFO +1 -1
- {specsmith-0.5.0.dev225 → specsmith-0.5.0.dev226}/pyproject.toml +1 -1
- {specsmith-0.5.0.dev225 → specsmith-0.5.0.dev226}/src/specsmith/cli.py +262 -27
- specsmith-0.5.0.dev226/src/specsmith/skills.py +199 -0
- {specsmith-0.5.0.dev225 → specsmith-0.5.0.dev226/src/specsmith.egg-info}/PKG-INFO +1 -1
- {specsmith-0.5.0.dev225 → specsmith-0.5.0.dev226}/src/specsmith.egg-info/SOURCES.txt +3 -0
- specsmith-0.5.0.dev226/tests/test_chat_stdin_protocol.py +89 -0
- specsmith-0.5.0.dev226/tests/test_skill_marketplace.py +185 -0
- {specsmith-0.5.0.dev225 → specsmith-0.5.0.dev226}/LICENSE +0 -0
- {specsmith-0.5.0.dev225 → specsmith-0.5.0.dev226}/README.md +0 -0
- {specsmith-0.5.0.dev225 → specsmith-0.5.0.dev226}/setup.cfg +0 -0
- {specsmith-0.5.0.dev225 → specsmith-0.5.0.dev226}/src/epistemic/__init__.py +0 -0
- {specsmith-0.5.0.dev225 → specsmith-0.5.0.dev226}/src/epistemic/belief.py +0 -0
- {specsmith-0.5.0.dev225 → specsmith-0.5.0.dev226}/src/epistemic/certainty.py +0 -0
- {specsmith-0.5.0.dev225 → specsmith-0.5.0.dev226}/src/epistemic/failure_graph.py +0 -0
- {specsmith-0.5.0.dev225 → specsmith-0.5.0.dev226}/src/epistemic/py.typed +0 -0
- {specsmith-0.5.0.dev225 → specsmith-0.5.0.dev226}/src/epistemic/recovery.py +0 -0
- {specsmith-0.5.0.dev225 → specsmith-0.5.0.dev226}/src/epistemic/session.py +0 -0
- {specsmith-0.5.0.dev225 → specsmith-0.5.0.dev226}/src/epistemic/stress_tester.py +0 -0
- {specsmith-0.5.0.dev225 → specsmith-0.5.0.dev226}/src/epistemic/trace.py +0 -0
- {specsmith-0.5.0.dev225 → specsmith-0.5.0.dev226}/src/specsmith/__init__.py +0 -0
- {specsmith-0.5.0.dev225 → specsmith-0.5.0.dev226}/src/specsmith/__main__.py +0 -0
- {specsmith-0.5.0.dev225 → specsmith-0.5.0.dev226}/src/specsmith/agent/__init__.py +0 -0
- {specsmith-0.5.0.dev225 → specsmith-0.5.0.dev226}/src/specsmith/agent/broker.py +0 -0
- {specsmith-0.5.0.dev225 → specsmith-0.5.0.dev226}/src/specsmith/agent/cleanup.py +0 -0
- {specsmith-0.5.0.dev225 → specsmith-0.5.0.dev226}/src/specsmith/agent/events.py +0 -0
- {specsmith-0.5.0.dev225 → specsmith-0.5.0.dev226}/src/specsmith/agent/indexer.py +0 -0
- {specsmith-0.5.0.dev225 → specsmith-0.5.0.dev226}/src/specsmith/agent/mcp.py +0 -0
- {specsmith-0.5.0.dev225 → specsmith-0.5.0.dev226}/src/specsmith/agent/memory.py +0 -0
- {specsmith-0.5.0.dev225 → specsmith-0.5.0.dev226}/src/specsmith/agent/orchestrator.py +0 -0
- {specsmith-0.5.0.dev225 → specsmith-0.5.0.dev226}/src/specsmith/agent/repl.py +0 -0
- {specsmith-0.5.0.dev225 → specsmith-0.5.0.dev226}/src/specsmith/agent/router.py +0 -0
- {specsmith-0.5.0.dev225 → specsmith-0.5.0.dev226}/src/specsmith/agent/rules.py +0 -0
- {specsmith-0.5.0.dev225 → specsmith-0.5.0.dev226}/src/specsmith/agent/safety.py +0 -0
- {specsmith-0.5.0.dev225 → specsmith-0.5.0.dev226}/src/specsmith/agent/tools.py +0 -0
- {specsmith-0.5.0.dev225 → specsmith-0.5.0.dev226}/src/specsmith/agent/verifier.py +0 -0
- {specsmith-0.5.0.dev225 → specsmith-0.5.0.dev226}/src/specsmith/architect.py +0 -0
- {specsmith-0.5.0.dev225 → specsmith-0.5.0.dev226}/src/specsmith/auditor.py +0 -0
- {specsmith-0.5.0.dev225 → specsmith-0.5.0.dev226}/src/specsmith/auth.py +0 -0
- {specsmith-0.5.0.dev225 → specsmith-0.5.0.dev226}/src/specsmith/commands/__init__.py +0 -0
- {specsmith-0.5.0.dev225 → specsmith-0.5.0.dev226}/src/specsmith/compressor.py +0 -0
- {specsmith-0.5.0.dev225 → specsmith-0.5.0.dev226}/src/specsmith/config.py +0 -0
- {specsmith-0.5.0.dev225 → specsmith-0.5.0.dev226}/src/specsmith/console_utils.py +0 -0
- {specsmith-0.5.0.dev225 → specsmith-0.5.0.dev226}/src/specsmith/credit_analyzer.py +0 -0
- {specsmith-0.5.0.dev225 → specsmith-0.5.0.dev226}/src/specsmith/credits.py +0 -0
- {specsmith-0.5.0.dev225 → specsmith-0.5.0.dev226}/src/specsmith/differ.py +0 -0
- {specsmith-0.5.0.dev225 → specsmith-0.5.0.dev226}/src/specsmith/doctor.py +0 -0
- {specsmith-0.5.0.dev225 → specsmith-0.5.0.dev226}/src/specsmith/epistemic/__init__.py +0 -0
- {specsmith-0.5.0.dev225 → specsmith-0.5.0.dev226}/src/specsmith/epistemic/belief.py +0 -0
- {specsmith-0.5.0.dev225 → specsmith-0.5.0.dev226}/src/specsmith/epistemic/certainty.py +0 -0
- {specsmith-0.5.0.dev225 → specsmith-0.5.0.dev226}/src/specsmith/epistemic/failure_graph.py +0 -0
- {specsmith-0.5.0.dev225 → specsmith-0.5.0.dev226}/src/specsmith/epistemic/recovery.py +0 -0
- {specsmith-0.5.0.dev225 → specsmith-0.5.0.dev226}/src/specsmith/epistemic/stress_tester.py +0 -0
- {specsmith-0.5.0.dev225 → specsmith-0.5.0.dev226}/src/specsmith/executor.py +0 -0
- {specsmith-0.5.0.dev225 → specsmith-0.5.0.dev226}/src/specsmith/exporter.py +0 -0
- {specsmith-0.5.0.dev225 → specsmith-0.5.0.dev226}/src/specsmith/gui/__init__.py +0 -0
- {specsmith-0.5.0.dev225 → specsmith-0.5.0.dev226}/src/specsmith/gui/app.py +0 -0
- {specsmith-0.5.0.dev225 → specsmith-0.5.0.dev226}/src/specsmith/gui/main_window.py +0 -0
- {specsmith-0.5.0.dev225 → specsmith-0.5.0.dev226}/src/specsmith/gui/session_tab.py +0 -0
- {specsmith-0.5.0.dev225 → specsmith-0.5.0.dev226}/src/specsmith/gui/theme.py +0 -0
- {specsmith-0.5.0.dev225 → specsmith-0.5.0.dev226}/src/specsmith/gui/widgets/__init__.py +0 -0
- {specsmith-0.5.0.dev225 → specsmith-0.5.0.dev226}/src/specsmith/gui/widgets/chat_view.py +0 -0
- {specsmith-0.5.0.dev225 → specsmith-0.5.0.dev226}/src/specsmith/gui/widgets/input_bar.py +0 -0
- {specsmith-0.5.0.dev225 → specsmith-0.5.0.dev226}/src/specsmith/gui/widgets/provider_bar.py +0 -0
- {specsmith-0.5.0.dev225 → specsmith-0.5.0.dev226}/src/specsmith/gui/widgets/token_meter.py +0 -0
- {specsmith-0.5.0.dev225 → specsmith-0.5.0.dev226}/src/specsmith/gui/widgets/tool_panel.py +0 -0
- {specsmith-0.5.0.dev225 → specsmith-0.5.0.dev226}/src/specsmith/gui/widgets/update_checker.py +0 -0
- {specsmith-0.5.0.dev225 → specsmith-0.5.0.dev226}/src/specsmith/gui/worker.py +0 -0
- {specsmith-0.5.0.dev225 → specsmith-0.5.0.dev226}/src/specsmith/importer.py +0 -0
- {specsmith-0.5.0.dev225 → specsmith-0.5.0.dev226}/src/specsmith/integrations/__init__.py +0 -0
- {specsmith-0.5.0.dev225 → specsmith-0.5.0.dev226}/src/specsmith/integrations/agent_skill.py +0 -0
- {specsmith-0.5.0.dev225 → specsmith-0.5.0.dev226}/src/specsmith/integrations/aider.py +0 -0
- {specsmith-0.5.0.dev225 → specsmith-0.5.0.dev226}/src/specsmith/integrations/base.py +0 -0
- {specsmith-0.5.0.dev225 → specsmith-0.5.0.dev226}/src/specsmith/integrations/claude_code.py +0 -0
- {specsmith-0.5.0.dev225 → specsmith-0.5.0.dev226}/src/specsmith/integrations/copilot.py +0 -0
- {specsmith-0.5.0.dev225 → specsmith-0.5.0.dev226}/src/specsmith/integrations/cursor.py +0 -0
- {specsmith-0.5.0.dev225 → specsmith-0.5.0.dev226}/src/specsmith/integrations/gemini.py +0 -0
- {specsmith-0.5.0.dev225 → specsmith-0.5.0.dev226}/src/specsmith/integrations/windsurf.py +0 -0
- {specsmith-0.5.0.dev225 → specsmith-0.5.0.dev226}/src/specsmith/languages.py +0 -0
- {specsmith-0.5.0.dev225 → specsmith-0.5.0.dev226}/src/specsmith/ledger.py +0 -0
- {specsmith-0.5.0.dev225 → specsmith-0.5.0.dev226}/src/specsmith/patent.py +0 -0
- {specsmith-0.5.0.dev225 → specsmith-0.5.0.dev226}/src/specsmith/phase.py +0 -0
- {specsmith-0.5.0.dev225 → specsmith-0.5.0.dev226}/src/specsmith/plugins.py +0 -0
- {specsmith-0.5.0.dev225 → specsmith-0.5.0.dev226}/src/specsmith/profiles.py +0 -0
- {specsmith-0.5.0.dev225 → specsmith-0.5.0.dev226}/src/specsmith/rate_limits.py +0 -0
- {specsmith-0.5.0.dev225 → specsmith-0.5.0.dev226}/src/specsmith/releaser.py +0 -0
- {specsmith-0.5.0.dev225 → specsmith-0.5.0.dev226}/src/specsmith/requirements.py +0 -0
- {specsmith-0.5.0.dev225 → specsmith-0.5.0.dev226}/src/specsmith/requirements_parser.py +0 -0
- {specsmith-0.5.0.dev225 → specsmith-0.5.0.dev226}/src/specsmith/retrieval.py +0 -0
- {specsmith-0.5.0.dev225 → specsmith-0.5.0.dev226}/src/specsmith/scaffolder.py +0 -0
- {specsmith-0.5.0.dev225 → specsmith-0.5.0.dev226}/src/specsmith/serve.py +0 -0
- {specsmith-0.5.0.dev225 → specsmith-0.5.0.dev226}/src/specsmith/session.py +0 -0
- {specsmith-0.5.0.dev225 → specsmith-0.5.0.dev226}/src/specsmith/templates/agents.md.j2 +0 -0
- {specsmith-0.5.0.dev225 → specsmith-0.5.0.dev226}/src/specsmith/templates/community/bug_report.md.j2 +0 -0
- {specsmith-0.5.0.dev225 → specsmith-0.5.0.dev226}/src/specsmith/templates/community/code_of_conduct.md.j2 +0 -0
- {specsmith-0.5.0.dev225 → specsmith-0.5.0.dev226}/src/specsmith/templates/community/contributing.md.j2 +0 -0
- {specsmith-0.5.0.dev225 → specsmith-0.5.0.dev226}/src/specsmith/templates/community/feature_request.md.j2 +0 -0
- {specsmith-0.5.0.dev225 → specsmith-0.5.0.dev226}/src/specsmith/templates/community/license-Apache-2.0.j2 +0 -0
- {specsmith-0.5.0.dev225 → specsmith-0.5.0.dev226}/src/specsmith/templates/community/license-MIT.j2 +0 -0
- {specsmith-0.5.0.dev225 → specsmith-0.5.0.dev226}/src/specsmith/templates/community/pull_request_template.md.j2 +0 -0
- {specsmith-0.5.0.dev225 → specsmith-0.5.0.dev226}/src/specsmith/templates/community/security.md.j2 +0 -0
- {specsmith-0.5.0.dev225 → specsmith-0.5.0.dev226}/src/specsmith/templates/docs/architecture.md.j2 +0 -0
- {specsmith-0.5.0.dev225 → specsmith-0.5.0.dev226}/src/specsmith/templates/docs/mkdocs.yml.j2 +0 -0
- {specsmith-0.5.0.dev225 → specsmith-0.5.0.dev226}/src/specsmith/templates/docs/readthedocs.yaml.j2 +0 -0
- {specsmith-0.5.0.dev225 → specsmith-0.5.0.dev226}/src/specsmith/templates/docs/requirements.md.j2 +0 -0
- {specsmith-0.5.0.dev225 → specsmith-0.5.0.dev226}/src/specsmith/templates/docs/test-spec.md.j2 +0 -0
- {specsmith-0.5.0.dev225 → specsmith-0.5.0.dev226}/src/specsmith/templates/editorconfig.j2 +0 -0
- {specsmith-0.5.0.dev225 → specsmith-0.5.0.dev226}/src/specsmith/templates/gitattributes.j2 +0 -0
- {specsmith-0.5.0.dev225 → specsmith-0.5.0.dev226}/src/specsmith/templates/gitignore.j2 +0 -0
- {specsmith-0.5.0.dev225 → specsmith-0.5.0.dev226}/src/specsmith/templates/go/go.mod.j2 +0 -0
- {specsmith-0.5.0.dev225 → specsmith-0.5.0.dev226}/src/specsmith/templates/go/main.go.j2 +0 -0
- {specsmith-0.5.0.dev225 → specsmith-0.5.0.dev226}/src/specsmith/templates/governance/belief-registry.md.j2 +0 -0
- {specsmith-0.5.0.dev225 → specsmith-0.5.0.dev226}/src/specsmith/templates/governance/context-budget.md.j2 +0 -0
- {specsmith-0.5.0.dev225 → specsmith-0.5.0.dev226}/src/specsmith/templates/governance/drift-metrics.md.j2 +0 -0
- {specsmith-0.5.0.dev225 → specsmith-0.5.0.dev226}/src/specsmith/templates/governance/epistemic-axioms.md.j2 +0 -0
- {specsmith-0.5.0.dev225 → specsmith-0.5.0.dev226}/src/specsmith/templates/governance/failure-modes.md.j2 +0 -0
- {specsmith-0.5.0.dev225 → specsmith-0.5.0.dev226}/src/specsmith/templates/governance/lifecycle.md.j2 +0 -0
- {specsmith-0.5.0.dev225 → specsmith-0.5.0.dev226}/src/specsmith/templates/governance/roles.md.j2 +0 -0
- {specsmith-0.5.0.dev225 → specsmith-0.5.0.dev226}/src/specsmith/templates/governance/rules.md.j2 +0 -0
- {specsmith-0.5.0.dev225 → specsmith-0.5.0.dev226}/src/specsmith/templates/governance/session-protocol.md.j2 +0 -0
- {specsmith-0.5.0.dev225 → specsmith-0.5.0.dev226}/src/specsmith/templates/governance/uncertainty-map.md.j2 +0 -0
- {specsmith-0.5.0.dev225 → specsmith-0.5.0.dev226}/src/specsmith/templates/governance/verification.md.j2 +0 -0
- {specsmith-0.5.0.dev225 → specsmith-0.5.0.dev226}/src/specsmith/templates/js/package.json.j2 +0 -0
- {specsmith-0.5.0.dev225 → specsmith-0.5.0.dev226}/src/specsmith/templates/ledger.md.j2 +0 -0
- {specsmith-0.5.0.dev225 → specsmith-0.5.0.dev226}/src/specsmith/templates/python/cli.py.j2 +0 -0
- {specsmith-0.5.0.dev225 → specsmith-0.5.0.dev226}/src/specsmith/templates/python/init.py.j2 +0 -0
- {specsmith-0.5.0.dev225 → specsmith-0.5.0.dev226}/src/specsmith/templates/python/pyproject.toml.j2 +0 -0
- {specsmith-0.5.0.dev225 → specsmith-0.5.0.dev226}/src/specsmith/templates/readme.md.j2 +0 -0
- {specsmith-0.5.0.dev225 → specsmith-0.5.0.dev226}/src/specsmith/templates/rust/Cargo.toml.j2 +0 -0
- {specsmith-0.5.0.dev225 → specsmith-0.5.0.dev226}/src/specsmith/templates/rust/main.rs.j2 +0 -0
- {specsmith-0.5.0.dev225 → specsmith-0.5.0.dev226}/src/specsmith/templates/scripts/exec.cmd.j2 +0 -0
- {specsmith-0.5.0.dev225 → specsmith-0.5.0.dev226}/src/specsmith/templates/scripts/exec.sh.j2 +0 -0
- {specsmith-0.5.0.dev225 → specsmith-0.5.0.dev226}/src/specsmith/templates/scripts/run.cmd.j2 +0 -0
- {specsmith-0.5.0.dev225 → specsmith-0.5.0.dev226}/src/specsmith/templates/scripts/run.sh.j2 +0 -0
- {specsmith-0.5.0.dev225 → specsmith-0.5.0.dev226}/src/specsmith/templates/scripts/setup.cmd.j2 +0 -0
- {specsmith-0.5.0.dev225 → specsmith-0.5.0.dev226}/src/specsmith/templates/scripts/setup.sh.j2 +0 -0
- {specsmith-0.5.0.dev225 → specsmith-0.5.0.dev226}/src/specsmith/templates/workflows/release.yml.j2 +0 -0
- {specsmith-0.5.0.dev225 → specsmith-0.5.0.dev226}/src/specsmith/tool_installer.py +0 -0
- {specsmith-0.5.0.dev225 → specsmith-0.5.0.dev226}/src/specsmith/toolrules.py +0 -0
- {specsmith-0.5.0.dev225 → specsmith-0.5.0.dev226}/src/specsmith/tools.py +0 -0
- {specsmith-0.5.0.dev225 → specsmith-0.5.0.dev226}/src/specsmith/trace.py +0 -0
- {specsmith-0.5.0.dev225 → specsmith-0.5.0.dev226}/src/specsmith/updater.py +0 -0
- {specsmith-0.5.0.dev225 → specsmith-0.5.0.dev226}/src/specsmith/upgrader.py +0 -0
- {specsmith-0.5.0.dev225 → specsmith-0.5.0.dev226}/src/specsmith/validator.py +0 -0
- {specsmith-0.5.0.dev225 → specsmith-0.5.0.dev226}/src/specsmith/vcs/__init__.py +0 -0
- {specsmith-0.5.0.dev225 → specsmith-0.5.0.dev226}/src/specsmith/vcs/base.py +0 -0
- {specsmith-0.5.0.dev225 → specsmith-0.5.0.dev226}/src/specsmith/vcs/bitbucket.py +0 -0
- {specsmith-0.5.0.dev225 → specsmith-0.5.0.dev226}/src/specsmith/vcs/github.py +0 -0
- {specsmith-0.5.0.dev225 → specsmith-0.5.0.dev226}/src/specsmith/vcs/gitlab.py +0 -0
- {specsmith-0.5.0.dev225 → specsmith-0.5.0.dev226}/src/specsmith/vcs_commands.py +0 -0
- {specsmith-0.5.0.dev225 → specsmith-0.5.0.dev226}/src/specsmith/wireframes.py +0 -0
- {specsmith-0.5.0.dev225 → specsmith-0.5.0.dev226}/src/specsmith/workspace.py +0 -0
- {specsmith-0.5.0.dev225 → specsmith-0.5.0.dev226}/src/specsmith.egg-info/dependency_links.txt +0 -0
- {specsmith-0.5.0.dev225 → specsmith-0.5.0.dev226}/src/specsmith.egg-info/entry_points.txt +0 -0
- {specsmith-0.5.0.dev225 → specsmith-0.5.0.dev226}/src/specsmith.egg-info/requires.txt +0 -0
- {specsmith-0.5.0.dev225 → specsmith-0.5.0.dev226}/src/specsmith.egg-info/top_level.txt +0 -0
- {specsmith-0.5.0.dev225 → specsmith-0.5.0.dev226}/tests/test_CMD_001.py +0 -0
- {specsmith-0.5.0.dev225 → specsmith-0.5.0.dev226}/tests/test_auditor.py +0 -0
- {specsmith-0.5.0.dev225 → specsmith-0.5.0.dev226}/tests/test_cli.py +0 -0
- {specsmith-0.5.0.dev225 → specsmith-0.5.0.dev226}/tests/test_cli_workflows_history_drive.py +0 -0
- {specsmith-0.5.0.dev225 → specsmith-0.5.0.dev226}/tests/test_compressor.py +0 -0
- {specsmith-0.5.0.dev225 → specsmith-0.5.0.dev226}/tests/test_e2e_nexus.py +0 -0
- {specsmith-0.5.0.dev225 → specsmith-0.5.0.dev226}/tests/test_epistemic.py +0 -0
- {specsmith-0.5.0.dev225 → specsmith-0.5.0.dev226}/tests/test_importer.py +0 -0
- {specsmith-0.5.0.dev225 → specsmith-0.5.0.dev226}/tests/test_integrations.py +0 -0
- {specsmith-0.5.0.dev225 → specsmith-0.5.0.dev226}/tests/test_nexus.py +0 -0
- {specsmith-0.5.0.dev225 → specsmith-0.5.0.dev226}/tests/test_phase1_4_new.py +0 -0
- {specsmith-0.5.0.dev225 → specsmith-0.5.0.dev226}/tests/test_rate_limits.py +0 -0
- {specsmith-0.5.0.dev225 → specsmith-0.5.0.dev226}/tests/test_scaffolder.py +0 -0
- {specsmith-0.5.0.dev225 → specsmith-0.5.0.dev226}/tests/test_smoke.py +0 -0
- {specsmith-0.5.0.dev225 → specsmith-0.5.0.dev226}/tests/test_tools.py +0 -0
- {specsmith-0.5.0.dev225 → specsmith-0.5.0.dev226}/tests/test_validator.py +0 -0
- {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.
|
|
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.
|
|
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
|
-
|
|
5333
|
-
|
|
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=
|
|
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.
|
|
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
|