specsmith 0.4.0.dev221__tar.gz → 0.4.0.dev223__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.4.0.dev221/src/specsmith.egg-info → specsmith-0.4.0.dev223}/PKG-INFO +1 -1
- {specsmith-0.4.0.dev221 → specsmith-0.4.0.dev223}/pyproject.toml +9 -6
- {specsmith-0.4.0.dev221 → specsmith-0.4.0.dev223}/src/specsmith/agent/broker.py +6 -5
- specsmith-0.4.0.dev223/src/specsmith/agent/events.py +176 -0
- {specsmith-0.4.0.dev221 → specsmith-0.4.0.dev223}/src/specsmith/agent/indexer.py +1 -1
- specsmith-0.4.0.dev223/src/specsmith/agent/mcp.py +117 -0
- specsmith-0.4.0.dev223/src/specsmith/agent/memory.py +81 -0
- {specsmith-0.4.0.dev221 → specsmith-0.4.0.dev223}/src/specsmith/agent/orchestrator.py +24 -8
- specsmith-0.4.0.dev223/src/specsmith/agent/router.py +78 -0
- specsmith-0.4.0.dev223/src/specsmith/agent/rules.py +62 -0
- specsmith-0.4.0.dev223/src/specsmith/agent/verifier.py +123 -0
- {specsmith-0.4.0.dev221 → specsmith-0.4.0.dev223}/src/specsmith/cli.py +476 -3
- {specsmith-0.4.0.dev221 → specsmith-0.4.0.dev223}/src/specsmith/console_utils.py +4 -3
- {specsmith-0.4.0.dev221 → specsmith-0.4.0.dev223/src/specsmith.egg-info}/PKG-INFO +1 -1
- {specsmith-0.4.0.dev221 → specsmith-0.4.0.dev223}/src/specsmith.egg-info/SOURCES.txt +8 -0
- specsmith-0.4.0.dev223/tests/test_e2e_nexus.py +144 -0
- specsmith-0.4.0.dev223/tests/test_phase1_4_new.py +127 -0
- {specsmith-0.4.0.dev221 → specsmith-0.4.0.dev223}/LICENSE +0 -0
- {specsmith-0.4.0.dev221 → specsmith-0.4.0.dev223}/README.md +0 -0
- {specsmith-0.4.0.dev221 → specsmith-0.4.0.dev223}/setup.cfg +0 -0
- {specsmith-0.4.0.dev221 → specsmith-0.4.0.dev223}/src/epistemic/__init__.py +0 -0
- {specsmith-0.4.0.dev221 → specsmith-0.4.0.dev223}/src/epistemic/belief.py +0 -0
- {specsmith-0.4.0.dev221 → specsmith-0.4.0.dev223}/src/epistemic/certainty.py +0 -0
- {specsmith-0.4.0.dev221 → specsmith-0.4.0.dev223}/src/epistemic/failure_graph.py +0 -0
- {specsmith-0.4.0.dev221 → specsmith-0.4.0.dev223}/src/epistemic/py.typed +0 -0
- {specsmith-0.4.0.dev221 → specsmith-0.4.0.dev223}/src/epistemic/recovery.py +0 -0
- {specsmith-0.4.0.dev221 → specsmith-0.4.0.dev223}/src/epistemic/session.py +0 -0
- {specsmith-0.4.0.dev221 → specsmith-0.4.0.dev223}/src/epistemic/stress_tester.py +0 -0
- {specsmith-0.4.0.dev221 → specsmith-0.4.0.dev223}/src/epistemic/trace.py +0 -0
- {specsmith-0.4.0.dev221 → specsmith-0.4.0.dev223}/src/specsmith/__init__.py +0 -0
- {specsmith-0.4.0.dev221 → specsmith-0.4.0.dev223}/src/specsmith/__main__.py +0 -0
- {specsmith-0.4.0.dev221 → specsmith-0.4.0.dev223}/src/specsmith/agent/__init__.py +0 -0
- {specsmith-0.4.0.dev221 → specsmith-0.4.0.dev223}/src/specsmith/agent/cleanup.py +0 -0
- {specsmith-0.4.0.dev221 → specsmith-0.4.0.dev223}/src/specsmith/agent/repl.py +0 -0
- {specsmith-0.4.0.dev221 → specsmith-0.4.0.dev223}/src/specsmith/agent/safety.py +0 -0
- {specsmith-0.4.0.dev221 → specsmith-0.4.0.dev223}/src/specsmith/agent/tools.py +0 -0
- {specsmith-0.4.0.dev221 → specsmith-0.4.0.dev223}/src/specsmith/architect.py +0 -0
- {specsmith-0.4.0.dev221 → specsmith-0.4.0.dev223}/src/specsmith/auditor.py +0 -0
- {specsmith-0.4.0.dev221 → specsmith-0.4.0.dev223}/src/specsmith/auth.py +0 -0
- {specsmith-0.4.0.dev221 → specsmith-0.4.0.dev223}/src/specsmith/commands/__init__.py +0 -0
- {specsmith-0.4.0.dev221 → specsmith-0.4.0.dev223}/src/specsmith/compressor.py +0 -0
- {specsmith-0.4.0.dev221 → specsmith-0.4.0.dev223}/src/specsmith/config.py +0 -0
- {specsmith-0.4.0.dev221 → specsmith-0.4.0.dev223}/src/specsmith/credit_analyzer.py +0 -0
- {specsmith-0.4.0.dev221 → specsmith-0.4.0.dev223}/src/specsmith/credits.py +0 -0
- {specsmith-0.4.0.dev221 → specsmith-0.4.0.dev223}/src/specsmith/differ.py +0 -0
- {specsmith-0.4.0.dev221 → specsmith-0.4.0.dev223}/src/specsmith/doctor.py +0 -0
- {specsmith-0.4.0.dev221 → specsmith-0.4.0.dev223}/src/specsmith/epistemic/__init__.py +0 -0
- {specsmith-0.4.0.dev221 → specsmith-0.4.0.dev223}/src/specsmith/epistemic/belief.py +0 -0
- {specsmith-0.4.0.dev221 → specsmith-0.4.0.dev223}/src/specsmith/epistemic/certainty.py +0 -0
- {specsmith-0.4.0.dev221 → specsmith-0.4.0.dev223}/src/specsmith/epistemic/failure_graph.py +0 -0
- {specsmith-0.4.0.dev221 → specsmith-0.4.0.dev223}/src/specsmith/epistemic/recovery.py +0 -0
- {specsmith-0.4.0.dev221 → specsmith-0.4.0.dev223}/src/specsmith/epistemic/stress_tester.py +0 -0
- {specsmith-0.4.0.dev221 → specsmith-0.4.0.dev223}/src/specsmith/executor.py +0 -0
- {specsmith-0.4.0.dev221 → specsmith-0.4.0.dev223}/src/specsmith/exporter.py +0 -0
- {specsmith-0.4.0.dev221 → specsmith-0.4.0.dev223}/src/specsmith/gui/__init__.py +0 -0
- {specsmith-0.4.0.dev221 → specsmith-0.4.0.dev223}/src/specsmith/gui/app.py +0 -0
- {specsmith-0.4.0.dev221 → specsmith-0.4.0.dev223}/src/specsmith/gui/main_window.py +0 -0
- {specsmith-0.4.0.dev221 → specsmith-0.4.0.dev223}/src/specsmith/gui/session_tab.py +0 -0
- {specsmith-0.4.0.dev221 → specsmith-0.4.0.dev223}/src/specsmith/gui/theme.py +0 -0
- {specsmith-0.4.0.dev221 → specsmith-0.4.0.dev223}/src/specsmith/gui/widgets/__init__.py +0 -0
- {specsmith-0.4.0.dev221 → specsmith-0.4.0.dev223}/src/specsmith/gui/widgets/chat_view.py +0 -0
- {specsmith-0.4.0.dev221 → specsmith-0.4.0.dev223}/src/specsmith/gui/widgets/input_bar.py +0 -0
- {specsmith-0.4.0.dev221 → specsmith-0.4.0.dev223}/src/specsmith/gui/widgets/provider_bar.py +0 -0
- {specsmith-0.4.0.dev221 → specsmith-0.4.0.dev223}/src/specsmith/gui/widgets/token_meter.py +0 -0
- {specsmith-0.4.0.dev221 → specsmith-0.4.0.dev223}/src/specsmith/gui/widgets/tool_panel.py +0 -0
- {specsmith-0.4.0.dev221 → specsmith-0.4.0.dev223}/src/specsmith/gui/widgets/update_checker.py +0 -0
- {specsmith-0.4.0.dev221 → specsmith-0.4.0.dev223}/src/specsmith/gui/worker.py +0 -0
- {specsmith-0.4.0.dev221 → specsmith-0.4.0.dev223}/src/specsmith/importer.py +0 -0
- {specsmith-0.4.0.dev221 → specsmith-0.4.0.dev223}/src/specsmith/integrations/__init__.py +0 -0
- {specsmith-0.4.0.dev221 → specsmith-0.4.0.dev223}/src/specsmith/integrations/aider.py +0 -0
- {specsmith-0.4.0.dev221 → specsmith-0.4.0.dev223}/src/specsmith/integrations/base.py +0 -0
- {specsmith-0.4.0.dev221 → specsmith-0.4.0.dev223}/src/specsmith/integrations/claude_code.py +0 -0
- {specsmith-0.4.0.dev221 → specsmith-0.4.0.dev223}/src/specsmith/integrations/copilot.py +0 -0
- {specsmith-0.4.0.dev221 → specsmith-0.4.0.dev223}/src/specsmith/integrations/cursor.py +0 -0
- {specsmith-0.4.0.dev221 → specsmith-0.4.0.dev223}/src/specsmith/integrations/gemini.py +0 -0
- {specsmith-0.4.0.dev221 → specsmith-0.4.0.dev223}/src/specsmith/integrations/warp.py +0 -0
- {specsmith-0.4.0.dev221 → specsmith-0.4.0.dev223}/src/specsmith/integrations/windsurf.py +0 -0
- {specsmith-0.4.0.dev221 → specsmith-0.4.0.dev223}/src/specsmith/languages.py +0 -0
- {specsmith-0.4.0.dev221 → specsmith-0.4.0.dev223}/src/specsmith/ledger.py +0 -0
- {specsmith-0.4.0.dev221 → specsmith-0.4.0.dev223}/src/specsmith/patent.py +0 -0
- {specsmith-0.4.0.dev221 → specsmith-0.4.0.dev223}/src/specsmith/phase.py +0 -0
- {specsmith-0.4.0.dev221 → specsmith-0.4.0.dev223}/src/specsmith/plugins.py +0 -0
- {specsmith-0.4.0.dev221 → specsmith-0.4.0.dev223}/src/specsmith/profiles.py +0 -0
- {specsmith-0.4.0.dev221 → specsmith-0.4.0.dev223}/src/specsmith/rate_limits.py +0 -0
- {specsmith-0.4.0.dev221 → specsmith-0.4.0.dev223}/src/specsmith/releaser.py +0 -0
- {specsmith-0.4.0.dev221 → specsmith-0.4.0.dev223}/src/specsmith/requirements.py +0 -0
- {specsmith-0.4.0.dev221 → specsmith-0.4.0.dev223}/src/specsmith/requirements_parser.py +0 -0
- {specsmith-0.4.0.dev221 → specsmith-0.4.0.dev223}/src/specsmith/retrieval.py +0 -0
- {specsmith-0.4.0.dev221 → specsmith-0.4.0.dev223}/src/specsmith/scaffolder.py +0 -0
- {specsmith-0.4.0.dev221 → specsmith-0.4.0.dev223}/src/specsmith/serve.py +0 -0
- {specsmith-0.4.0.dev221 → specsmith-0.4.0.dev223}/src/specsmith/session.py +0 -0
- {specsmith-0.4.0.dev221 → specsmith-0.4.0.dev223}/src/specsmith/templates/agents.md.j2 +0 -0
- {specsmith-0.4.0.dev221 → specsmith-0.4.0.dev223}/src/specsmith/templates/community/bug_report.md.j2 +0 -0
- {specsmith-0.4.0.dev221 → specsmith-0.4.0.dev223}/src/specsmith/templates/community/code_of_conduct.md.j2 +0 -0
- {specsmith-0.4.0.dev221 → specsmith-0.4.0.dev223}/src/specsmith/templates/community/contributing.md.j2 +0 -0
- {specsmith-0.4.0.dev221 → specsmith-0.4.0.dev223}/src/specsmith/templates/community/feature_request.md.j2 +0 -0
- {specsmith-0.4.0.dev221 → specsmith-0.4.0.dev223}/src/specsmith/templates/community/license-Apache-2.0.j2 +0 -0
- {specsmith-0.4.0.dev221 → specsmith-0.4.0.dev223}/src/specsmith/templates/community/license-MIT.j2 +0 -0
- {specsmith-0.4.0.dev221 → specsmith-0.4.0.dev223}/src/specsmith/templates/community/pull_request_template.md.j2 +0 -0
- {specsmith-0.4.0.dev221 → specsmith-0.4.0.dev223}/src/specsmith/templates/community/security.md.j2 +0 -0
- {specsmith-0.4.0.dev221 → specsmith-0.4.0.dev223}/src/specsmith/templates/docs/architecture.md.j2 +0 -0
- {specsmith-0.4.0.dev221 → specsmith-0.4.0.dev223}/src/specsmith/templates/docs/mkdocs.yml.j2 +0 -0
- {specsmith-0.4.0.dev221 → specsmith-0.4.0.dev223}/src/specsmith/templates/docs/readthedocs.yaml.j2 +0 -0
- {specsmith-0.4.0.dev221 → specsmith-0.4.0.dev223}/src/specsmith/templates/docs/requirements.md.j2 +0 -0
- {specsmith-0.4.0.dev221 → specsmith-0.4.0.dev223}/src/specsmith/templates/docs/test-spec.md.j2 +0 -0
- {specsmith-0.4.0.dev221 → specsmith-0.4.0.dev223}/src/specsmith/templates/editorconfig.j2 +0 -0
- {specsmith-0.4.0.dev221 → specsmith-0.4.0.dev223}/src/specsmith/templates/gitattributes.j2 +0 -0
- {specsmith-0.4.0.dev221 → specsmith-0.4.0.dev223}/src/specsmith/templates/gitignore.j2 +0 -0
- {specsmith-0.4.0.dev221 → specsmith-0.4.0.dev223}/src/specsmith/templates/go/go.mod.j2 +0 -0
- {specsmith-0.4.0.dev221 → specsmith-0.4.0.dev223}/src/specsmith/templates/go/main.go.j2 +0 -0
- {specsmith-0.4.0.dev221 → specsmith-0.4.0.dev223}/src/specsmith/templates/governance/belief-registry.md.j2 +0 -0
- {specsmith-0.4.0.dev221 → specsmith-0.4.0.dev223}/src/specsmith/templates/governance/context-budget.md.j2 +0 -0
- {specsmith-0.4.0.dev221 → specsmith-0.4.0.dev223}/src/specsmith/templates/governance/drift-metrics.md.j2 +0 -0
- {specsmith-0.4.0.dev221 → specsmith-0.4.0.dev223}/src/specsmith/templates/governance/epistemic-axioms.md.j2 +0 -0
- {specsmith-0.4.0.dev221 → specsmith-0.4.0.dev223}/src/specsmith/templates/governance/failure-modes.md.j2 +0 -0
- {specsmith-0.4.0.dev221 → specsmith-0.4.0.dev223}/src/specsmith/templates/governance/lifecycle.md.j2 +0 -0
- {specsmith-0.4.0.dev221 → specsmith-0.4.0.dev223}/src/specsmith/templates/governance/roles.md.j2 +0 -0
- {specsmith-0.4.0.dev221 → specsmith-0.4.0.dev223}/src/specsmith/templates/governance/rules.md.j2 +0 -0
- {specsmith-0.4.0.dev221 → specsmith-0.4.0.dev223}/src/specsmith/templates/governance/session-protocol.md.j2 +0 -0
- {specsmith-0.4.0.dev221 → specsmith-0.4.0.dev223}/src/specsmith/templates/governance/uncertainty-map.md.j2 +0 -0
- {specsmith-0.4.0.dev221 → specsmith-0.4.0.dev223}/src/specsmith/templates/governance/verification.md.j2 +0 -0
- {specsmith-0.4.0.dev221 → specsmith-0.4.0.dev223}/src/specsmith/templates/js/package.json.j2 +0 -0
- {specsmith-0.4.0.dev221 → specsmith-0.4.0.dev223}/src/specsmith/templates/ledger.md.j2 +0 -0
- {specsmith-0.4.0.dev221 → specsmith-0.4.0.dev223}/src/specsmith/templates/python/cli.py.j2 +0 -0
- {specsmith-0.4.0.dev221 → specsmith-0.4.0.dev223}/src/specsmith/templates/python/init.py.j2 +0 -0
- {specsmith-0.4.0.dev221 → specsmith-0.4.0.dev223}/src/specsmith/templates/python/pyproject.toml.j2 +0 -0
- {specsmith-0.4.0.dev221 → specsmith-0.4.0.dev223}/src/specsmith/templates/readme.md.j2 +0 -0
- {specsmith-0.4.0.dev221 → specsmith-0.4.0.dev223}/src/specsmith/templates/rust/Cargo.toml.j2 +0 -0
- {specsmith-0.4.0.dev221 → specsmith-0.4.0.dev223}/src/specsmith/templates/rust/main.rs.j2 +0 -0
- {specsmith-0.4.0.dev221 → specsmith-0.4.0.dev223}/src/specsmith/templates/scripts/exec.cmd.j2 +0 -0
- {specsmith-0.4.0.dev221 → specsmith-0.4.0.dev223}/src/specsmith/templates/scripts/exec.sh.j2 +0 -0
- {specsmith-0.4.0.dev221 → specsmith-0.4.0.dev223}/src/specsmith/templates/scripts/run.cmd.j2 +0 -0
- {specsmith-0.4.0.dev221 → specsmith-0.4.0.dev223}/src/specsmith/templates/scripts/run.sh.j2 +0 -0
- {specsmith-0.4.0.dev221 → specsmith-0.4.0.dev223}/src/specsmith/templates/scripts/setup.cmd.j2 +0 -0
- {specsmith-0.4.0.dev221 → specsmith-0.4.0.dev223}/src/specsmith/templates/scripts/setup.sh.j2 +0 -0
- {specsmith-0.4.0.dev221 → specsmith-0.4.0.dev223}/src/specsmith/templates/workflows/release.yml.j2 +0 -0
- {specsmith-0.4.0.dev221 → specsmith-0.4.0.dev223}/src/specsmith/tool_installer.py +0 -0
- {specsmith-0.4.0.dev221 → specsmith-0.4.0.dev223}/src/specsmith/toolrules.py +0 -0
- {specsmith-0.4.0.dev221 → specsmith-0.4.0.dev223}/src/specsmith/tools.py +0 -0
- {specsmith-0.4.0.dev221 → specsmith-0.4.0.dev223}/src/specsmith/trace.py +0 -0
- {specsmith-0.4.0.dev221 → specsmith-0.4.0.dev223}/src/specsmith/updater.py +0 -0
- {specsmith-0.4.0.dev221 → specsmith-0.4.0.dev223}/src/specsmith/upgrader.py +0 -0
- {specsmith-0.4.0.dev221 → specsmith-0.4.0.dev223}/src/specsmith/validator.py +0 -0
- {specsmith-0.4.0.dev221 → specsmith-0.4.0.dev223}/src/specsmith/vcs/__init__.py +0 -0
- {specsmith-0.4.0.dev221 → specsmith-0.4.0.dev223}/src/specsmith/vcs/base.py +0 -0
- {specsmith-0.4.0.dev221 → specsmith-0.4.0.dev223}/src/specsmith/vcs/bitbucket.py +0 -0
- {specsmith-0.4.0.dev221 → specsmith-0.4.0.dev223}/src/specsmith/vcs/github.py +0 -0
- {specsmith-0.4.0.dev221 → specsmith-0.4.0.dev223}/src/specsmith/vcs/gitlab.py +0 -0
- {specsmith-0.4.0.dev221 → specsmith-0.4.0.dev223}/src/specsmith/vcs_commands.py +0 -0
- {specsmith-0.4.0.dev221 → specsmith-0.4.0.dev223}/src/specsmith/wireframes.py +0 -0
- {specsmith-0.4.0.dev221 → specsmith-0.4.0.dev223}/src/specsmith/workspace.py +0 -0
- {specsmith-0.4.0.dev221 → specsmith-0.4.0.dev223}/src/specsmith.egg-info/dependency_links.txt +0 -0
- {specsmith-0.4.0.dev221 → specsmith-0.4.0.dev223}/src/specsmith.egg-info/entry_points.txt +0 -0
- {specsmith-0.4.0.dev221 → specsmith-0.4.0.dev223}/src/specsmith.egg-info/requires.txt +0 -0
- {specsmith-0.4.0.dev221 → specsmith-0.4.0.dev223}/src/specsmith.egg-info/top_level.txt +0 -0
- {specsmith-0.4.0.dev221 → specsmith-0.4.0.dev223}/tests/test_CMD_001.py +0 -0
- {specsmith-0.4.0.dev221 → specsmith-0.4.0.dev223}/tests/test_auditor.py +0 -0
- {specsmith-0.4.0.dev221 → specsmith-0.4.0.dev223}/tests/test_cli.py +0 -0
- {specsmith-0.4.0.dev221 → specsmith-0.4.0.dev223}/tests/test_compressor.py +0 -0
- {specsmith-0.4.0.dev221 → specsmith-0.4.0.dev223}/tests/test_epistemic.py +0 -0
- {specsmith-0.4.0.dev221 → specsmith-0.4.0.dev223}/tests/test_importer.py +0 -0
- {specsmith-0.4.0.dev221 → specsmith-0.4.0.dev223}/tests/test_integrations.py +0 -0
- {specsmith-0.4.0.dev221 → specsmith-0.4.0.dev223}/tests/test_nexus.py +0 -0
- {specsmith-0.4.0.dev221 → specsmith-0.4.0.dev223}/tests/test_rate_limits.py +0 -0
- {specsmith-0.4.0.dev221 → specsmith-0.4.0.dev223}/tests/test_scaffolder.py +0 -0
- {specsmith-0.4.0.dev221 → specsmith-0.4.0.dev223}/tests/test_smoke.py +0 -0
- {specsmith-0.4.0.dev221 → specsmith-0.4.0.dev223}/tests/test_tools.py +0 -0
- {specsmith-0.4.0.dev221 → specsmith-0.4.0.dev223}/tests/test_validator.py +0 -0
- {specsmith-0.4.0.dev221 → specsmith-0.4.0.dev223}/tests/test_vcs.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: specsmith
|
|
3
|
-
Version: 0.4.0.
|
|
3
|
+
Version: 0.4.0.dev223
|
|
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.4.0.
|
|
7
|
+
version = "0.4.0.dev223"
|
|
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"
|
|
@@ -107,6 +107,11 @@ select = ["E", "F", "W", "I", "UP", "B", "SIM"]
|
|
|
107
107
|
# Long rule text and install command strings in documentation modules
|
|
108
108
|
"src/specsmith/toolrules.py" = ["E501"]
|
|
109
109
|
"src/specsmith/tool_installer.py" = ["E501"]
|
|
110
|
+
# One-shot rebuild scripts contain literal REQ description strings that are
|
|
111
|
+
# intentionally long; wrapping them would damage readability of the source
|
|
112
|
+
# of truth they reconstruct.
|
|
113
|
+
"scripts/rebuild_requirements_json.py" = ["E501"]
|
|
114
|
+
"scripts/rebuild_requirements_md.py" = ["E501"]
|
|
110
115
|
|
|
111
116
|
[tool.mypy]
|
|
112
117
|
python_version = "3.10"
|
|
@@ -141,9 +146,11 @@ ignore_missing_imports = true
|
|
|
141
146
|
module = ["epistemic", "epistemic.*"]
|
|
142
147
|
ignore_errors = true
|
|
143
148
|
|
|
144
|
-
#
|
|
149
|
+
# Modules that use dynamic typing patterns incompatible with strict mypy.
|
|
145
150
|
# These are correct at runtime; the Any-heavy urllib, json.loads, etc. patterns
|
|
146
151
|
# are the source of most errors here. Exclude from strict type checking.
|
|
152
|
+
# REQ-111: shrunk this list as part of the pre-1.0 cleanup; broker, safety,
|
|
153
|
+
# console_utils, and indexer have been graduated to strict mypy.
|
|
147
154
|
[[tool.mypy.overrides]]
|
|
148
155
|
module = [
|
|
149
156
|
"specsmith.ollama_cmds",
|
|
@@ -153,14 +160,10 @@ module = [
|
|
|
153
160
|
"specsmith.importer",
|
|
154
161
|
"specsmith.agent.providers.gemini",
|
|
155
162
|
"specsmith.agent.runner",
|
|
156
|
-
"specsmith.agent.broker",
|
|
157
163
|
"specsmith.agent.cleanup",
|
|
158
|
-
"specsmith.agent.indexer",
|
|
159
164
|
"specsmith.agent.orchestrator",
|
|
160
165
|
"specsmith.agent.repl",
|
|
161
|
-
"specsmith.agent.safety",
|
|
162
166
|
"specsmith.agent.tools",
|
|
163
|
-
"specsmith.console_utils",
|
|
164
167
|
"specsmith.profiles",
|
|
165
168
|
"specsmith.serve",
|
|
166
169
|
"specsmith.toolrules",
|
|
@@ -40,6 +40,7 @@ from collections.abc import Callable
|
|
|
40
40
|
from dataclasses import dataclass, field
|
|
41
41
|
from enum import Enum
|
|
42
42
|
from pathlib import Path
|
|
43
|
+
from typing import Any
|
|
43
44
|
|
|
44
45
|
# ---------------------------------------------------------------------------
|
|
45
46
|
# Intent classification
|
|
@@ -285,7 +286,7 @@ def infer_scope(
|
|
|
285
286
|
class PreflightDecision:
|
|
286
287
|
"""Wrapped Specsmith preflight outcome."""
|
|
287
288
|
|
|
288
|
-
raw: dict
|
|
289
|
+
raw: dict[str, Any]
|
|
289
290
|
decision: str = "unknown"
|
|
290
291
|
work_item_id: str = ""
|
|
291
292
|
requirement_ids: list[str] = field(default_factory=list)
|
|
@@ -294,7 +295,7 @@ class PreflightDecision:
|
|
|
294
295
|
instruction: str = ""
|
|
295
296
|
|
|
296
297
|
@classmethod
|
|
297
|
-
def from_json(cls, payload: dict) -> PreflightDecision:
|
|
298
|
+
def from_json(cls, payload: dict[str, Any]) -> PreflightDecision:
|
|
298
299
|
return cls(
|
|
299
300
|
raw=payload,
|
|
300
301
|
decision=str(payload.get("decision", "unknown")),
|
|
@@ -476,7 +477,7 @@ RETRY_STRATEGIES = (
|
|
|
476
477
|
)
|
|
477
478
|
|
|
478
479
|
|
|
479
|
-
def classify_retry_strategy(report: dict, decision: PreflightDecision) -> str:
|
|
480
|
+
def classify_retry_strategy(report: dict[str, Any], decision: PreflightDecision) -> str:
|
|
480
481
|
"""Map an executor failure report to one of the canonical retry strategies.
|
|
481
482
|
|
|
482
483
|
The classification is deterministic and inspects:
|
|
@@ -529,7 +530,7 @@ def classify_retry_strategy(report: dict, decision: PreflightDecision) -> str:
|
|
|
529
530
|
def execute_with_governance(
|
|
530
531
|
decision: PreflightDecision,
|
|
531
532
|
*,
|
|
532
|
-
executor: Callable[[PreflightDecision, int], dict],
|
|
533
|
+
executor: Callable[[PreflightDecision, int], dict[str, Any]],
|
|
533
534
|
retry_budget: int = DEFAULT_RETRY_BUDGET,
|
|
534
535
|
) -> RunResult:
|
|
535
536
|
"""Run the work with Specsmith governance and a hard retry budget.
|
|
@@ -546,7 +547,7 @@ def execute_with_governance(
|
|
|
546
547
|
|
|
547
548
|
last_summary = ""
|
|
548
549
|
last_confidence = 0.0
|
|
549
|
-
last_report: dict = {}
|
|
550
|
+
last_report: dict[str, Any] = {}
|
|
550
551
|
for attempt in range(1, retry_budget + 1):
|
|
551
552
|
report = executor(decision, attempt) or {}
|
|
552
553
|
last_report = report
|
|
@@ -0,0 +1,176 @@
|
|
|
1
|
+
# SPDX-License-Identifier: MIT
|
|
2
|
+
# Copyright (c) 2026 BitConcepts, LLC. All rights reserved.
|
|
3
|
+
"""Block-based JSONL event protocol for `specsmith chat` (REQ-112, REQ-113, REQ-114).
|
|
4
|
+
|
|
5
|
+
The protocol is the contract between the Specsmith chat backend and any
|
|
6
|
+
client (the Nexus REPL itself, the VS Code extension, or future TUIs).
|
|
7
|
+
Every event is a single JSON object on its own line with a ``type`` key.
|
|
8
|
+
|
|
9
|
+
Event kinds
|
|
10
|
+
-----------
|
|
11
|
+
* ``block_start`` - begins a new block (kinds: ``plan``, ``message``,
|
|
12
|
+
``tool_call``, ``tool_result``, ``diff``,
|
|
13
|
+
``test_results``, ``verdict``).
|
|
14
|
+
* ``block_complete`` - closes the block opened by ``block_start``.
|
|
15
|
+
* ``token`` - incremental LLM token within a ``message`` block.
|
|
16
|
+
* ``tool_call`` - the LLM has decided to invoke a tool.
|
|
17
|
+
* ``tool_request`` - safe-mode permission request (REQ-115).
|
|
18
|
+
* ``tool_result`` - completed tool execution.
|
|
19
|
+
* ``plan_step`` - status transition for a step in the active plan
|
|
20
|
+
block (REQ-114).
|
|
21
|
+
* ``task_complete`` - final block; carries final summary + profile.
|
|
22
|
+
"""
|
|
23
|
+
|
|
24
|
+
from __future__ import annotations
|
|
25
|
+
|
|
26
|
+
import contextlib
|
|
27
|
+
import json
|
|
28
|
+
import sys
|
|
29
|
+
import time
|
|
30
|
+
import uuid
|
|
31
|
+
from dataclasses import dataclass, field
|
|
32
|
+
from typing import IO, Any
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def _now_iso() -> str:
|
|
36
|
+
"""Return a UTC ISO-8601 timestamp (second precision)."""
|
|
37
|
+
return time.strftime("%Y-%m-%dT%H:%M:%SZ", time.gmtime())
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def _new_block_id() -> str:
|
|
41
|
+
return f"blk_{uuid.uuid4().hex[:12]}"
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
@dataclass
|
|
45
|
+
class EventEmitter:
|
|
46
|
+
"""Writes JSONL events to a stream (default stdout).
|
|
47
|
+
|
|
48
|
+
Used by the `specsmith chat` CLI and by the test suite. Each event is
|
|
49
|
+
flushed immediately so consumers can react in real time.
|
|
50
|
+
"""
|
|
51
|
+
|
|
52
|
+
stream: IO[str] = field(default_factory=lambda: sys.stdout)
|
|
53
|
+
|
|
54
|
+
def emit(self, event: dict[str, Any]) -> None:
|
|
55
|
+
line = json.dumps(event, ensure_ascii=False)
|
|
56
|
+
self.stream.write(line + "\n")
|
|
57
|
+
# Some test buffers (e.g. capsys) don't support flush; ignore.
|
|
58
|
+
with contextlib.suppress(Exception):
|
|
59
|
+
self.stream.flush()
|
|
60
|
+
|
|
61
|
+
# ── Block helpers ────────────────────────────────────────────────────
|
|
62
|
+
|
|
63
|
+
def block_start(self, kind: str, *, agent: str = "nexus", **payload: Any) -> str:
|
|
64
|
+
"""Open a new block of ``kind`` and return its id."""
|
|
65
|
+
block_id = _new_block_id()
|
|
66
|
+
self.emit(
|
|
67
|
+
{
|
|
68
|
+
"type": "block_start",
|
|
69
|
+
"block_id": block_id,
|
|
70
|
+
"kind": kind,
|
|
71
|
+
"agent": agent,
|
|
72
|
+
"timestamp": _now_iso(),
|
|
73
|
+
"payload": payload,
|
|
74
|
+
}
|
|
75
|
+
)
|
|
76
|
+
return block_id
|
|
77
|
+
|
|
78
|
+
def block_complete(self, block_id: str, **payload: Any) -> None:
|
|
79
|
+
self.emit(
|
|
80
|
+
{
|
|
81
|
+
"type": "block_complete",
|
|
82
|
+
"block_id": block_id,
|
|
83
|
+
"timestamp": _now_iso(),
|
|
84
|
+
"payload": payload,
|
|
85
|
+
}
|
|
86
|
+
)
|
|
87
|
+
|
|
88
|
+
def token(self, block_id: str, text: str) -> None:
|
|
89
|
+
self.emit(
|
|
90
|
+
{
|
|
91
|
+
"type": "token",
|
|
92
|
+
"block_id": block_id,
|
|
93
|
+
"text": text,
|
|
94
|
+
}
|
|
95
|
+
)
|
|
96
|
+
|
|
97
|
+
def tool_call(self, block_id: str, name: str, args: dict[str, Any]) -> None:
|
|
98
|
+
self.emit(
|
|
99
|
+
{
|
|
100
|
+
"type": "tool_call",
|
|
101
|
+
"block_id": block_id,
|
|
102
|
+
"name": name,
|
|
103
|
+
"args": args,
|
|
104
|
+
}
|
|
105
|
+
)
|
|
106
|
+
|
|
107
|
+
def tool_request(self, block_id: str, name: str, args: dict[str, Any]) -> None:
|
|
108
|
+
self.emit(
|
|
109
|
+
{
|
|
110
|
+
"type": "tool_request",
|
|
111
|
+
"block_id": block_id,
|
|
112
|
+
"name": name,
|
|
113
|
+
"args": args,
|
|
114
|
+
}
|
|
115
|
+
)
|
|
116
|
+
|
|
117
|
+
def tool_result(self, block_id: str, name: str, ok: bool, output: str) -> None:
|
|
118
|
+
self.emit(
|
|
119
|
+
{
|
|
120
|
+
"type": "tool_result",
|
|
121
|
+
"block_id": block_id,
|
|
122
|
+
"name": name,
|
|
123
|
+
"ok": ok,
|
|
124
|
+
"output": output,
|
|
125
|
+
}
|
|
126
|
+
)
|
|
127
|
+
|
|
128
|
+
def plan(self, steps: list[dict[str, Any]]) -> str:
|
|
129
|
+
return self.block_start("plan", steps=steps)
|
|
130
|
+
|
|
131
|
+
def plan_step(
|
|
132
|
+
self,
|
|
133
|
+
block_id: str,
|
|
134
|
+
step_id: str,
|
|
135
|
+
status: str,
|
|
136
|
+
**payload: Any,
|
|
137
|
+
) -> None:
|
|
138
|
+
self.emit(
|
|
139
|
+
{
|
|
140
|
+
"type": "plan_step",
|
|
141
|
+
"block_id": block_id,
|
|
142
|
+
"step_id": step_id,
|
|
143
|
+
"status": status,
|
|
144
|
+
"timestamp": _now_iso(),
|
|
145
|
+
"payload": payload,
|
|
146
|
+
}
|
|
147
|
+
)
|
|
148
|
+
|
|
149
|
+
def diff(self, path: str, body: str) -> str:
|
|
150
|
+
return self.block_start("diff", path=path, body=body)
|
|
151
|
+
|
|
152
|
+
def task_complete(
|
|
153
|
+
self,
|
|
154
|
+
*,
|
|
155
|
+
success: bool,
|
|
156
|
+
confidence: float,
|
|
157
|
+
summary: str,
|
|
158
|
+
profile: str,
|
|
159
|
+
comments: list[dict[str, Any]] | None = None,
|
|
160
|
+
**extra: Any,
|
|
161
|
+
) -> None:
|
|
162
|
+
self.emit(
|
|
163
|
+
{
|
|
164
|
+
"type": "task_complete",
|
|
165
|
+
"timestamp": _now_iso(),
|
|
166
|
+
"success": success,
|
|
167
|
+
"confidence": confidence,
|
|
168
|
+
"summary": summary,
|
|
169
|
+
"profile": profile,
|
|
170
|
+
"comments": comments or [],
|
|
171
|
+
**extra,
|
|
172
|
+
}
|
|
173
|
+
)
|
|
174
|
+
|
|
175
|
+
|
|
176
|
+
__all__ = ["EventEmitter"]
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
# SPDX-License-Identifier: MIT
|
|
2
|
+
# Copyright (c) 2026 BitConcepts, LLC. All rights reserved.
|
|
3
|
+
"""MCP (Model Context Protocol) tool consumption for Nexus (REQ-121).
|
|
4
|
+
|
|
5
|
+
Reads ``.specsmith/mcp.yml`` (a list of server configs) and returns a list
|
|
6
|
+
of tool wrappers that Nexus can register alongside its built-in tool set.
|
|
7
|
+
The wrappers are invoked over stdio per the MCP spec (subprocess +
|
|
8
|
+
JSON-RPC framing). For 1.0 we ship the loader and the wrapper interface;
|
|
9
|
+
the actual stdio JSON-RPC client is implemented but kept narrow so the
|
|
10
|
+
Specsmith safety middleware fully wraps every call.
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
from __future__ import annotations
|
|
14
|
+
|
|
15
|
+
import json
|
|
16
|
+
import subprocess
|
|
17
|
+
from dataclasses import dataclass
|
|
18
|
+
from pathlib import Path
|
|
19
|
+
from typing import Any
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
@dataclass
|
|
23
|
+
class MCPServerSpec:
|
|
24
|
+
"""Static configuration for an MCP server.
|
|
25
|
+
|
|
26
|
+
Mirrors `.specsmith/mcp.yml` entries; the YAML parser turns each
|
|
27
|
+
entry into one of these.
|
|
28
|
+
"""
|
|
29
|
+
|
|
30
|
+
name: str
|
|
31
|
+
command: str
|
|
32
|
+
args: list[str]
|
|
33
|
+
env: dict[str, str]
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
@dataclass
|
|
37
|
+
class MCPTool:
|
|
38
|
+
"""A Nexus-side handle to an MCP server.
|
|
39
|
+
|
|
40
|
+
Calling ``invoke(payload)`` opens a subprocess, sends the payload as
|
|
41
|
+
a JSON-RPC ``tools/call`` request, and returns the response. Errors
|
|
42
|
+
surface as plain strings; the orchestrator wraps the call with the
|
|
43
|
+
standard Specsmith safety middleware so destructive payloads are
|
|
44
|
+
blocked exactly the same way as native Nexus tools.
|
|
45
|
+
"""
|
|
46
|
+
|
|
47
|
+
spec: MCPServerSpec
|
|
48
|
+
|
|
49
|
+
@property
|
|
50
|
+
def name(self) -> str:
|
|
51
|
+
return self.spec.name
|
|
52
|
+
|
|
53
|
+
def invoke(self, payload: dict[str, Any]) -> str:
|
|
54
|
+
request = {
|
|
55
|
+
"jsonrpc": "2.0",
|
|
56
|
+
"id": 1,
|
|
57
|
+
"method": "tools/call",
|
|
58
|
+
"params": payload,
|
|
59
|
+
}
|
|
60
|
+
body = json.dumps(request) + "\n"
|
|
61
|
+
try:
|
|
62
|
+
proc = subprocess.run( # noqa: S603 - argv is configured by user
|
|
63
|
+
[self.spec.command, *self.spec.args],
|
|
64
|
+
input=body,
|
|
65
|
+
capture_output=True,
|
|
66
|
+
text=True,
|
|
67
|
+
timeout=30,
|
|
68
|
+
env={**self.spec.env},
|
|
69
|
+
check=False,
|
|
70
|
+
)
|
|
71
|
+
except (OSError, subprocess.TimeoutExpired) as exc:
|
|
72
|
+
return f"mcp error: {exc}"
|
|
73
|
+
if proc.returncode != 0:
|
|
74
|
+
return f"mcp error: {proc.stderr.strip() or 'non-zero exit'}"
|
|
75
|
+
return proc.stdout.strip() or "(empty mcp response)"
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
def load_mcp_tools(project_dir: Path) -> list[MCPTool]:
|
|
79
|
+
"""Read ``.specsmith/mcp.yml`` and return a list of :class:`MCPTool`.
|
|
80
|
+
|
|
81
|
+
Returns an empty list when the file is absent or unparseable so the
|
|
82
|
+
rest of the orchestrator continues to function with zero MCP servers
|
|
83
|
+
configured (the default).
|
|
84
|
+
"""
|
|
85
|
+
cfg_path = Path(project_dir) / ".specsmith" / "mcp.yml"
|
|
86
|
+
if not cfg_path.is_file():
|
|
87
|
+
return []
|
|
88
|
+
try:
|
|
89
|
+
import yaml
|
|
90
|
+
|
|
91
|
+
raw = yaml.safe_load(cfg_path.read_text(encoding="utf-8")) or []
|
|
92
|
+
except Exception: # noqa: BLE001
|
|
93
|
+
return []
|
|
94
|
+
if not isinstance(raw, list):
|
|
95
|
+
return []
|
|
96
|
+
|
|
97
|
+
out: list[MCPTool] = []
|
|
98
|
+
for entry in raw:
|
|
99
|
+
if not isinstance(entry, dict):
|
|
100
|
+
continue
|
|
101
|
+
name = str(entry.get("name", "")).strip()
|
|
102
|
+
command = str(entry.get("command", "")).strip()
|
|
103
|
+
if not name or not command:
|
|
104
|
+
continue
|
|
105
|
+
args_raw = entry.get("args", []) or []
|
|
106
|
+
env_raw = entry.get("env", {}) or {}
|
|
107
|
+
spec = MCPServerSpec(
|
|
108
|
+
name=name,
|
|
109
|
+
command=command,
|
|
110
|
+
args=[str(a) for a in args_raw if isinstance(a, (str, int, float))],
|
|
111
|
+
env={str(k): str(v) for k, v in env_raw.items()},
|
|
112
|
+
)
|
|
113
|
+
out.append(MCPTool(spec=spec))
|
|
114
|
+
return out
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
__all__ = ["MCPServerSpec", "MCPTool", "load_mcp_tools"]
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
# SPDX-License-Identifier: MIT
|
|
2
|
+
# Copyright (c) 2026 BitConcepts, LLC. All rights reserved.
|
|
3
|
+
"""Persistent session memory for the Nexus chat surface (REQ-120, REQ-125).
|
|
4
|
+
|
|
5
|
+
Every chat turn (user utterance, broker decision, task result, tool calls)
|
|
6
|
+
is appended as JSONL to ``.specsmith/sessions/<session_id>/turns.jsonl``.
|
|
7
|
+
The orchestrator prepends the most-recent turns (capped by character
|
|
8
|
+
budget) to its first message so the LLM has continuity across runs.
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
from __future__ import annotations
|
|
12
|
+
|
|
13
|
+
import json
|
|
14
|
+
import time
|
|
15
|
+
from pathlib import Path
|
|
16
|
+
from typing import Any
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def _session_dir(project_dir: Path, session_id: str) -> Path:
|
|
20
|
+
return Path(project_dir) / ".specsmith" / "sessions" / session_id
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def _turns_path(project_dir: Path, session_id: str) -> Path:
|
|
24
|
+
return _session_dir(project_dir, session_id) / "turns.jsonl"
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def append_turn(
|
|
28
|
+
project_dir: Path,
|
|
29
|
+
session_id: str,
|
|
30
|
+
turn: dict[str, Any],
|
|
31
|
+
) -> None:
|
|
32
|
+
"""Append ``turn`` to the session log. Adds a UTC timestamp if missing."""
|
|
33
|
+
path = _turns_path(project_dir, session_id)
|
|
34
|
+
path.parent.mkdir(parents=True, exist_ok=True)
|
|
35
|
+
record = dict(turn)
|
|
36
|
+
record.setdefault("timestamp", time.strftime("%Y-%m-%dT%H:%M:%SZ", time.gmtime()))
|
|
37
|
+
with path.open("a", encoding="utf-8") as fh:
|
|
38
|
+
fh.write(json.dumps(record, ensure_ascii=False) + "\n")
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def all_turns(project_dir: Path, session_id: str) -> list[dict[str, Any]]:
|
|
42
|
+
"""Return every recorded turn for ``session_id`` (oldest-first)."""
|
|
43
|
+
path = _turns_path(project_dir, session_id)
|
|
44
|
+
if not path.is_file():
|
|
45
|
+
return []
|
|
46
|
+
out: list[dict[str, Any]] = []
|
|
47
|
+
for line in path.read_text(encoding="utf-8").splitlines():
|
|
48
|
+
line = line.strip()
|
|
49
|
+
if not line:
|
|
50
|
+
continue
|
|
51
|
+
try:
|
|
52
|
+
out.append(json.loads(line))
|
|
53
|
+
except ValueError:
|
|
54
|
+
continue
|
|
55
|
+
return out
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def recent_turns(
|
|
59
|
+
project_dir: Path,
|
|
60
|
+
session_id: str,
|
|
61
|
+
*,
|
|
62
|
+
max_chars: int = 20_000,
|
|
63
|
+
) -> list[dict[str, Any]]:
|
|
64
|
+
"""Return the most recent turns whose serialized size fits ``max_chars``.
|
|
65
|
+
|
|
66
|
+
Truncates oldest-first so the prompt always carries the latest context.
|
|
67
|
+
"""
|
|
68
|
+
turns = all_turns(project_dir, session_id)
|
|
69
|
+
out: list[dict[str, Any]] = []
|
|
70
|
+
used = 0
|
|
71
|
+
for turn in reversed(turns):
|
|
72
|
+
size = len(json.dumps(turn, ensure_ascii=False))
|
|
73
|
+
if used + size > max_chars:
|
|
74
|
+
break
|
|
75
|
+
out.append(turn)
|
|
76
|
+
used += size
|
|
77
|
+
out.reverse()
|
|
78
|
+
return out
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
__all__ = ["append_turn", "all_turns", "recent_turns"]
|
|
@@ -207,13 +207,15 @@ Next action:
|
|
|
207
207
|
|
|
208
208
|
AG2's ``initiate_chat`` returns a ``ChatResult`` whose ``summary`` is
|
|
209
209
|
the last assistant message and whose ``chat_history`` lists every
|
|
210
|
-
turn. We
|
|
211
|
-
|
|
212
|
-
``
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
210
|
+
turn. We parse the Nexus output contract out of the summary and feed
|
|
211
|
+
the structured signal through :func:`specsmith.agent.verifier.score`
|
|
212
|
+
(REQ-108) so ``equilibrium`` and ``confidence`` reflect real test /
|
|
213
|
+
ruff / mypy state instead of a hardcoded heuristic. When the LLM
|
|
214
|
+
returns no contract sections at all we fall back to the previous
|
|
215
|
+
conservative defaults so behaviour stays the same on degraded runs.
|
|
216
216
|
"""
|
|
217
|
+
from specsmith.agent.verifier import report_from_chat_sections, score
|
|
218
|
+
|
|
217
219
|
summary = ""
|
|
218
220
|
if chat_result is not None:
|
|
219
221
|
summary = getattr(chat_result, "summary", "") or ""
|
|
@@ -221,8 +223,6 @@ Next action:
|
|
|
221
223
|
summary = str(summary)
|
|
222
224
|
|
|
223
225
|
sections = self._parse_output_contract(summary)
|
|
224
|
-
equilibrium = bool(sections) and "next_action" in sections
|
|
225
|
-
confidence = 0.85 if equilibrium else (0.4 if summary else 0.0)
|
|
226
226
|
|
|
227
227
|
files_changed: list[str] = []
|
|
228
228
|
files_section = sections.get("files_changed", "")
|
|
@@ -236,6 +236,22 @@ Next action:
|
|
|
236
236
|
if tests_section:
|
|
237
237
|
test_results["raw"] = tests_section
|
|
238
238
|
|
|
239
|
+
# REQ-108: derive confidence and equilibrium from the real verifier
|
|
240
|
+
# signal rather than guessing from the presence of contract sections.
|
|
241
|
+
if sections:
|
|
242
|
+
report = report_from_chat_sections(sections, files_changed=files_changed)
|
|
243
|
+
verdict = score(report, confidence_target=0.7)
|
|
244
|
+
confidence = verdict.confidence
|
|
245
|
+
# Treat "reached the next_action section" as a soft floor for
|
|
246
|
+
# equilibrium so the harness still distinguishes a structurally
|
|
247
|
+
# complete contract from a totally empty one.
|
|
248
|
+
equilibrium = verdict.equilibrium or (
|
|
249
|
+
"next_action" in sections and report.has_changes and report.test_failed == 0
|
|
250
|
+
)
|
|
251
|
+
else:
|
|
252
|
+
equilibrium = False
|
|
253
|
+
confidence = 0.4 if summary else 0.0
|
|
254
|
+
|
|
239
255
|
return TaskResult(
|
|
240
256
|
equilibrium=equilibrium,
|
|
241
257
|
confidence=confidence,
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
# SPDX-License-Identifier: MIT
|
|
2
|
+
# Copyright (c) 2026 BitConcepts, LLC. All rights reserved.
|
|
3
|
+
"""Dynamic agent / model routing for the Nexus orchestrator (REQ-122).
|
|
4
|
+
|
|
5
|
+
The orchestrator asks ``choose_tier`` which model tier should run a given
|
|
6
|
+
task. Three tiers are recognized:
|
|
7
|
+
|
|
8
|
+
* ``coder`` - the local `l1-nexus` Qwen-Coder server (default).
|
|
9
|
+
* ``heavy`` - a larger reasoning model for governance / architecture work.
|
|
10
|
+
* ``fast`` - a quick lightweight model for read-only asks and summaries.
|
|
11
|
+
|
|
12
|
+
The default mapping is overridable per project via
|
|
13
|
+
``.specsmith/config.yml``::
|
|
14
|
+
|
|
15
|
+
routing:
|
|
16
|
+
change: coder
|
|
17
|
+
release: heavy
|
|
18
|
+
destructive: heavy
|
|
19
|
+
read_only_ask: fast
|
|
20
|
+
"""
|
|
21
|
+
|
|
22
|
+
from __future__ import annotations
|
|
23
|
+
|
|
24
|
+
from pathlib import Path
|
|
25
|
+
from typing import Literal
|
|
26
|
+
|
|
27
|
+
Tier = Literal["coder", "heavy", "fast"]
|
|
28
|
+
|
|
29
|
+
DEFAULT_MAPPING: dict[str, Tier] = {
|
|
30
|
+
"read_only_ask": "fast",
|
|
31
|
+
"change": "coder",
|
|
32
|
+
"release": "heavy",
|
|
33
|
+
"destructive": "heavy",
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def choose_tier(
|
|
38
|
+
intent: str,
|
|
39
|
+
*,
|
|
40
|
+
project_dir: Path | None = None,
|
|
41
|
+
retry_count: int = 0,
|
|
42
|
+
) -> Tier:
|
|
43
|
+
"""Pick a model tier for ``intent``.
|
|
44
|
+
|
|
45
|
+
Repeated retries escalate from ``coder`` to ``heavy`` so a stuck task
|
|
46
|
+
gets a more capable model on the next try (Phase-3 behaviour from the
|
|
47
|
+
plan).
|
|
48
|
+
"""
|
|
49
|
+
mapping = dict(DEFAULT_MAPPING)
|
|
50
|
+
if project_dir is not None:
|
|
51
|
+
mapping.update(_load_routing_overrides(project_dir))
|
|
52
|
+
tier: Tier = mapping.get(intent, "coder")
|
|
53
|
+
if retry_count >= 2 and tier == "coder":
|
|
54
|
+
tier = "heavy"
|
|
55
|
+
return tier
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def _load_routing_overrides(project_dir: Path) -> dict[str, Tier]:
|
|
59
|
+
cfg = Path(project_dir) / ".specsmith" / "config.yml"
|
|
60
|
+
if not cfg.is_file():
|
|
61
|
+
return {}
|
|
62
|
+
try:
|
|
63
|
+
import yaml
|
|
64
|
+
|
|
65
|
+
raw = yaml.safe_load(cfg.read_text(encoding="utf-8")) or {}
|
|
66
|
+
except Exception: # noqa: BLE001
|
|
67
|
+
return {}
|
|
68
|
+
section = raw.get("routing") if isinstance(raw, dict) else None
|
|
69
|
+
if not isinstance(section, dict):
|
|
70
|
+
return {}
|
|
71
|
+
out: dict[str, Tier] = {}
|
|
72
|
+
for key, val in section.items():
|
|
73
|
+
if isinstance(val, str) and val in ("coder", "heavy", "fast"):
|
|
74
|
+
out[str(key)] = val # type: ignore[assignment]
|
|
75
|
+
return out
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
__all__ = ["DEFAULT_MAPPING", "Tier", "choose_tier"]
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
# SPDX-License-Identifier: MIT
|
|
2
|
+
# Copyright (c) 2026 BitConcepts, LLC. All rights reserved.
|
|
3
|
+
"""Project rules auto-injection for the Nexus orchestrator (REQ-119).
|
|
4
|
+
|
|
5
|
+
Combines `docs/governance/*_RULES.md` files and the H-rules from
|
|
6
|
+
`AGENTS.md` into a single deterministic system-prompt prefix that the
|
|
7
|
+
orchestrator prepends to every AG2 agent's `system_message`.
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
from __future__ import annotations
|
|
11
|
+
|
|
12
|
+
import re
|
|
13
|
+
from pathlib import Path
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def load_rules(project_dir: Path) -> str:
|
|
17
|
+
"""Return the combined rules prefix for ``project_dir``.
|
|
18
|
+
|
|
19
|
+
The returned string is empty when no governance rule files are present
|
|
20
|
+
(so older projects keep working unchanged). When rules exist, they are
|
|
21
|
+
rendered as a single compact block so AG2 token costs stay reasonable.
|
|
22
|
+
"""
|
|
23
|
+
project_dir = Path(project_dir)
|
|
24
|
+
sections: list[str] = []
|
|
25
|
+
|
|
26
|
+
governance_dir = project_dir / "docs" / "governance"
|
|
27
|
+
if governance_dir.is_dir():
|
|
28
|
+
for path in sorted(governance_dir.glob("*_RULES.md")):
|
|
29
|
+
try:
|
|
30
|
+
text = path.read_text(encoding="utf-8").strip()
|
|
31
|
+
except OSError:
|
|
32
|
+
continue
|
|
33
|
+
if text:
|
|
34
|
+
sections.append(f"# {path.stem}\n{text}")
|
|
35
|
+
|
|
36
|
+
agents_md = project_dir / "AGENTS.md"
|
|
37
|
+
if agents_md.is_file():
|
|
38
|
+
try:
|
|
39
|
+
agents_text = agents_md.read_text(encoding="utf-8")
|
|
40
|
+
except OSError:
|
|
41
|
+
agents_text = ""
|
|
42
|
+
h_rules = _extract_h_rules(agents_text)
|
|
43
|
+
if h_rules:
|
|
44
|
+
sections.append("# AGENTS.md hard rules\n" + h_rules)
|
|
45
|
+
|
|
46
|
+
if not sections:
|
|
47
|
+
return ""
|
|
48
|
+
|
|
49
|
+
return "## Project Governance Rules (auto-loaded)\n" + "\n\n".join(sections) + "\n"
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def _extract_h_rules(text: str) -> str:
|
|
53
|
+
"""Extract numbered hard-rules (`H1`, `H2`, ...) from AGENTS.md."""
|
|
54
|
+
lines: list[str] = []
|
|
55
|
+
for line in text.splitlines():
|
|
56
|
+
stripped = line.strip()
|
|
57
|
+
if re.match(r"^[*\-]?\s*\*?\*?H\d+\b", stripped):
|
|
58
|
+
lines.append(stripped.lstrip("*-").lstrip())
|
|
59
|
+
return "\n".join(lines)
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
__all__ = ["load_rules"]
|